From e3372194e6929bda949602b4924f5fc9a648958d Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Sun, 20 Oct 2024 17:59:29 +0200 Subject: [PATCH 001/205] add CCubeProjection.hpp, ILinearProjection.hpp & IProjection.hpp headers (temporary to common/include/camera directory) --- .gitignore | 1 - 01_HelloCoreSystemAsset/main.cpp | 4 +- 02_HelloCompute/main.cpp | 10 +- 03_DeviceSelectionAndSharedSources/Testers.h | 28 +- 03_DeviceSelectionAndSharedSources/main.cpp | 43 +- .../CMakeLists.txt | 40 +- .../app_resources/common.hlsl | 7 +- .../app_resources/shader.comp.hlsl | 6 +- .../main.cpp | 30 +- 06_HelloGraphicsQueue/main.cpp | 26 +- 06_MeshLoaders/CMakeLists.txt | 6 + .../config.json.template | 0 06_MeshLoaders/main.cpp | 563 +++ .../pipeline.groovy | 6 +- 07_StagingAndMultipleQueues/CMakeLists.txt | 40 +- .../app_resources/common.hlsl | 6 +- 07_StagingAndMultipleQueues/main.cpp | 78 +- 08_HelloSwapchain/main.cpp | 36 +- 09_GeometryCreator/CMakeLists.txt | 6 +- 09_GeometryCreator/include/common.hpp | 21 +- 09_GeometryCreator/main.cpp | 509 ++- 0_ImportanceSamplingEnvMaps/main.cpp | 16 +- 10_CountingSort/CMakeLists.txt | 59 - 10_CountingSort/app_resources/common.hlsl | 6 - .../app_resources/prefix_sum_shader.comp.hlsl | 1 - .../app_resources/scatter_shader.comp.hlsl | 1 - 10_CountingSort/main.cpp | 72 +- 11_FFT/CMakeLists.txt | 62 - 11_FFT/app_resources/common.hlsl | 12 - 11_FFT/app_resources/shader.comp.hlsl | 74 - 11_FFT/main.cpp | 322 -- 11_FFT/pipeline.groovy | 50 - 12_MeshLoaders/CMakeLists.txt | 26 - 12_MeshLoaders/README.md | 2 - 12_MeshLoaders/include/common.hpp | 18 - 12_MeshLoaders/main.cpp | 581 --- 13_MaterialCompilerTest/CMakeLists.txt | 29 - 13_MaterialCompilerTest/main.cpp | 646 ---- 14_Mortons/CMakeLists.txt | 72 - 14_Mortons/CTester.h | 493 --- 14_Mortons/app_resources/common.hlsl | 233 -- 14_Mortons/app_resources/test.comp.hlsl | 20 - 14_Mortons/app_resources/test2.comp.hlsl | 20 - 14_Mortons/app_resources/testCommon.hlsl | 297 -- 14_Mortons/app_resources/testCommon2.hlsl | 42 - 14_Mortons/config.json.template | 28 - 14_Mortons/main.cpp | 94 - 15_MitsubaLoader/CMakeLists.txt | 9 - 15_MitsubaLoader/main.cpp | 131 - 15_MitsubaLoader/test_scenes.txt | 30 - 16_ZipArchiveLoaderTest/CMakeLists.txt | 32 - 16_ZipArchiveLoaderTest/main.cpp | 266 -- 20_AllocatorTest/main.cpp | 34 - 21_LRUCacheUnitTest/main.cpp | 143 +- 22_CppCompat/CIntrinsicsTester.h | 287 -- 22_CppCompat/CMakeLists.txt | 54 +- 22_CppCompat/CTgmathTester.h | 361 -- 22_CppCompat/app_resources/common.hlsl | 521 +-- .../app_resources/intrinsicsTest.comp.hlsl | 19 - 22_CppCompat/app_resources/test.comp.hlsl | 39 +- .../app_resources/tgmathTest.comp.hlsl | 19 - 22_CppCompat/main.cpp | 82 +- 22_CppCompat/test.hlsl.orig | 30 +- 22_RaytracedAO/CMakeLists.txt | 12 +- 22_RaytracedAO/CommandLineHandler.cpp | 14 +- 22_RaytracedAO/CommandLineHandler.hpp | 105 +- 22_RaytracedAO/README.md | 173 +- 22_RaytracedAO/Renderer.cpp | 2053 ++++++++--- 22_RaytracedAO/Renderer.h | 83 +- 22_RaytracedAO/addEnvironmentEmitters.comp | 51 - 22_RaytracedAO/ci/dummy_4096spp_128depth.xml | 40 - 22_RaytracedAO/closestHit.comp | 84 +- 22_RaytracedAO/common.h | 38 +- 22_RaytracedAO/config.json.template | 28 + 22_RaytracedAO/cull.comp | 74 + 22_RaytracedAO/denoiser_hook.bat | 2 +- 22_RaytracedAO/extractCubemap.bat | 33 + 22_RaytracedAO/fillVisBuffer.frag | 36 + 22_RaytracedAO/fillVisBuffer.vert | 23 - 22_RaytracedAO/main.cpp | 925 +++-- 22_RaytracedAO/mergeCubemap.bat | 31 + .../pipeline.groovy | 8 +- 22_RaytracedAO/raygen.comp | 153 +- 22_RaytracedAO/raytraceCommon.glsl | 297 +- 22_RaytracedAO/raytraceCommon.h | 42 +- 22_RaytracedAO/resolve.comp | 69 +- 22_RaytracedAO/test_scenes.txt | 46 + 22_RaytracedAO/virtualGeometry.glsl | 42 + 23_Arithmetic2UnitTest/CMakeLists.txt | 15 - .../app_resources/shaderCommon.hlsl | 19 - .../app_resources/testSubgroup.comp.hlsl | 55 - .../app_resources/testWorkgroup.comp.hlsl | 75 - 23_Arithmetic2UnitTest/main.cpp | 509 --- .../CMakeLists.txt | 1 + .../app_resources/common.hlsl | 14 +- .../app_resources/shaderCommon.hlsl | 55 + .../app_resources/testSubgroup.comp.hlsl | 18 + .../app_resources/testWorkgroup.comp.hlsl | 107 + .../config.json.template | 0 23_ArithmeticUnitTest/main.cpp | 462 +++ .../pipeline.groovy | 0 24_ColorSpaceTest/CMakeLists.txt | 41 +- 24_ColorSpaceTest/main.cpp | 100 +- 25_FilterTest/main.cpp | 2615 +++++++------ 26_Blur/CMakeLists.txt | 24 - 26_Blur/app_resources/common.hlsl | 11 - 26_Blur/app_resources/shader.comp.hlsl | 161 - 26_Blur/main.cpp | 775 ---- 27_MPMCScheduler/CMakeLists.txt | 50 +- 27_MPMCScheduler/app_resources/common.hlsl | 21 +- .../app_resources/schedulers/mpmc.hlsl | 25 +- .../app_resources/shader.comp.hlsl | 221 +- .../workgroup/pool_allocator.hlsl | 2 +- 27_MPMCScheduler/main.cpp | 153 +- 27_PLYSTLDemo/CMakeLists.txt | 7 + 27_PLYSTLDemo/config.json.template | 28 + 27_PLYSTLDemo/main.cpp | 579 +++ {14_Mortons => 27_PLYSTLDemo}/pipeline.groovy | 6 +- 28_FFTBloom/app_resources/common.hlsl | 51 - 28_FFTBloom/app_resources/fft_common.hlsl | 71 - .../app_resources/fft_convolve_ifft.hlsl | 245 -- .../app_resources/fft_mirror_common.hlsl | 46 - .../app_resources/image_fft_first_axis.hlsl | 96 - .../app_resources/image_ifft_first_axis.hlsl | 161 - .../app_resources/kernel_fft_first_axis.hlsl | 87 - .../app_resources/kernel_fft_second_axis.hlsl | 222 -- .../kernel_spectrum_normalize.hlsl | 21 - 28_FFTBloom/config.json.template | 28 - 28_FFTBloom/main.cpp | 1418 ------- 28_FFTBloom/pipeline.groovy | 50 - 29_Arithmetic2Bench/CMakeLists.txt | 15 - .../app_resources/benchmarkSubgroup.comp.hlsl | 57 - .../benchmarkWorkgroup.comp.hlsl | 125 - 29_Arithmetic2Bench/app_resources/common.hlsl | 34 - .../app_resources/shaderCommon.hlsl | 26 - 29_Arithmetic2Bench/main.cpp | 707 ---- 29_SpecializationConstants/CMakeLists.txt | 7 + .../config.json.template | 2 +- 29_SpecializationConstants/main.cpp | 566 +++ 29_SpecializationConstants/particles.comp | 39 + 29_SpecializationConstants/particles.frag | 12 + 29_SpecializationConstants/particles.vert | 21 + 29_SpecializationConstants/pipeline.groovy | 50 + 31_HLSLPathTracer/CMakeLists.txt | 93 - .../hlsl/compute.render.common.hlsl | 66 - .../compute.render.linear.entrypoints.hlsl | 21 - ...compute.render.persistent.entrypoints.hlsl | 21 - .../hlsl/compute_render_scene_impl.hlsl | 160 - .../app_resources/hlsl/example_common.hlsl | 742 ---- .../app_resources/hlsl/imgui.unified.hlsl | 9 - .../app_resources/hlsl/intersector.hlsl | 131 - .../app_resources/hlsl/material_system.hlsl | 286 -- .../hlsl/next_event_estimator.hlsl | 455 --- .../app_resources/hlsl/present.frag.hlsl | 18 - .../app_resources/hlsl/render_common.hlsl | 58 - .../hlsl/render_rwmc_common.hlsl | 17 - .../app_resources/hlsl/resolve.comp.hlsl | 66 - .../app_resources/hlsl/resolve_common.hlsl | 14 - .../app_resources/hlsl/rwmc_common.hlsl | 8 - .../app_resources/hlsl/scene_base.hlsl | 83 - .../hlsl/scene_rectangle_light.hlsl | 97 - .../hlsl/scene_sphere_light.hlsl | 100 - .../hlsl/scene_triangle_light.hlsl | 97 - .../spirv/pt.compute.rectangle.proxy.hlsl | 1 - ...mpute.rectangle.rwmc.persistent.proxy.hlsl | 1 - .../hlsl/spirv/pt.compute.sphere.proxy.hlsl | 1 - .../spirv/pt.compute.sphere.rwmc.proxy.hlsl | 1 - .../pt.compute.triangle.methods.shared.hlsl | 71 - .../pt.compute.triangle.persistent.proxy.hlsl | 1 - ...ompute.triangle.rwmc.persistent.proxy.hlsl | 1 - .../hlsl/spirv/pt.compute.variant.shared.hlsl | 55 - .../hlsl/spirv/pt.misc.proxy.hlsl | 5 - .../include/nbl/this_example/common.hpp | 17 - .../path_tracer_pipeline_state.hpp | 26 - .../nbl/this_example/path_tracer_ui.hpp | 137 - .../this_example/render_variant_config.hlsl | 40 - .../this_example/render_variant_enums.hlsl | 28 - .../nbl/this_example/render_variant_info.hpp | 55 - .../this_example/render_variant_strings.hpp | 98 - .../include/nbl/this_example/transform.hpp | 167 - 31_HLSLPathTracer/main.cpp | 2946 --------------- 31_HLSLPathTracer/path_tracer.runtime.json.in | 3 - 31_HLSLPathTracer/pt.cmake | 35 - 31_HLSLPathTracer/pt.variant_ids.cmake | 4 - 34_DebugDraw/CMakeLists.txt | 9 - 34_DebugDraw/include/common.hpp | 22 - 34_DebugDraw/main.cpp | 367 -- 36_CUDAInterop/main.cpp | 4 +- 39_DenoiserTonemapper/CMakeLists.txt | 1 + 39_DenoiserTonemapper/CommonPushConstants.h | 3 +- 39_DenoiserTonemapper/ShaderCommon.glsl | 12 +- 39_DenoiserTonemapper/main.cpp | 316 +- 40_PathTracer/CMakeLists.txt | 76 - .../app_resources/pathtrace/beauty.hlsl | 473 --- .../app_resources/pathtrace/common.hlsl | 505 --- .../app_resources/pathtrace/debug.hlsl | 70 - .../app_resources/pathtrace/previs.hlsl | 30 - .../app_resources/present/default.hlsl | 42 - 40_PathTracer/include/common.hpp | 28 - 40_PathTracer/include/io/CSceneLoader.h | 278 -- 40_PathTracer/include/renderer/CRenderer.h | 180 - 40_PathTracer/include/renderer/CScene.h | 97 - 40_PathTracer/include/renderer/CSession.h | 158 - 40_PathTracer/include/renderer/SAASequence.h | 1046 ------ .../renderer/present/CWindowPresenter.h | 94 - .../include/renderer/present/IPresenter.h | 205 - .../renderer/resolve/CBasicRWMCResolver.h | 104 - .../include/renderer/resolve/IResolver.h | 47 - .../include/renderer/shaders/common.hlsl | 79 - .../renderer/shaders/pathtrace/common.hlsl | 23 - .../shaders/pathtrace/push_constants.hlsl | 85 - .../shaders/present/push_constants.hlsl | 60 - .../renderer/shaders/resolve/rwmc.hlsl | 35 - .../include/renderer/shaders/scene.hlsl | 74 - .../include/renderer/shaders/session.hlsl | 110 - 40_PathTracer/main.cpp | 606 --- 40_PathTracer/src/io/CSceneLoader.cpp | 542 --- 40_PathTracer/src/renderer/CRenderer.cpp | 894 ----- 40_PathTracer/src/renderer/CScene.cpp | 80 - 40_PathTracer/src/renderer/CSession.cpp | 375 -- .../src/renderer/present/CWindowPresenter.cpp | 301 -- .../renderer/resolve/CBasicRWMCResolver.cpp | 120 - 41_VisibilityBuffer/CMakeLists.txt | 13 + 41_VisibilityBuffer/common.glsl | 68 + 41_VisibilityBuffer/common.h | 20 + .../config.json.template | 2 +- 41_VisibilityBuffer/cull.comp | 102 + 41_VisibilityBuffer/cullShaderCommon.h | 32 + 41_VisibilityBuffer/fillVBuffer.frag | 28 + 41_VisibilityBuffer/fillVBuffer.vert | 20 + 41_VisibilityBuffer/main.cpp | 1333 +++++++ 41_VisibilityBuffer/occlusionCull.frag | 15 + 41_VisibilityBuffer/occlusionCull.vert | 28 + 41_VisibilityBuffer/occlusionCullMap.comp | 30 + .../occlusionCullingShaderCommon.glsl | 58 + 41_VisibilityBuffer/pipeline.groovy | 50 + 41_VisibilityBuffer/rasterizationCommon.h | 21 + 41_VisibilityBuffer/shadeVBuffer.comp | 199 + 42_FragmentShaderPathTracer/CMakeLists.txt | 7 + .../common.glsl | 91 +- .../config.json.template | 2 +- .../litByRectangle.comp | 2 +- .../litBySphere.comp | 2 +- .../litByTriangle.comp | 2 +- 42_FragmentShaderPathTracer/main.cpp | 693 ++++ 42_FragmentShaderPathTracer/pipeline.groovy | 50 + 43_SumAndCDFFilters/CMakeLists.txt | 7 + .../config.json.template | 4 +- 43_SumAndCDFFilters/main.cpp | 369 ++ 43_SumAndCDFFilters/pipeline.groovy | 50 + 47_DerivMapTest/main.cpp | 2 +- 50.IESViewer/App.hpp | 328 -- 50.IESViewer/AppEvent.cpp | 62 - 50.IESViewer/AppGPU.cpp | 102 - 50.IESViewer/AppInit.cpp | 654 ---- 50.IESViewer/AppInputParser.cpp | 108 - 50.IESViewer/AppInputParser.hpp | 27 - 50.IESViewer/AppRender.cpp | 557 --- 50.IESViewer/AppUI.cpp | 651 ---- 50.IESViewer/CMakeLists.txt | 75 - 50.IESViewer/CSimpleIESRenderer.hpp | 427 --- 50.IESViewer/IES.cpp | 27 - 50.IESViewer/IES.hpp | 193 - 50.IESViewer/app_resources/common.hlsl | 73 - 50.IESViewer/app_resources/false_color.hlsl | 74 - 50.IESViewer/app_resources/ies.unified.hlsl | 165 - 50.IESViewer/app_resources/imgui.opts.hlsl | 16 - 50.IESViewer/app_resources/imgui.unified.hlsl | 7 - 50.IESViewer/inputs.json | 14 - 50.IESViewer/main.cpp | 14 - 53_ComputeShaders/CMakeLists.txt | 6 + 53_ComputeShaders/computeShader.comp | 95 + 53_ComputeShaders/config.json.template | 28 + 53_ComputeShaders/fragmentShader.frag | 12 + 53_ComputeShaders/geometryShader.geom | 27 + 53_ComputeShaders/main.cpp | 694 ++++ 53_ComputeShaders/pipeline.groovy | 50 + 53_ComputeShaders/shaderCommon.glsl | 6 + 53_ComputeShaders/vertexShader.vert | 23 + 56_RayQuery/CMakeLists.txt | 7 + 56_RayQuery/common.glsl | 793 ++++ 56_RayQuery/config.json.template | 28 + 56_RayQuery/litByRectangle.comp | 106 + 56_RayQuery/litBySphere.comp | 61 + 56_RayQuery/litByTriangle.comp | 105 + 56_RayQuery/main.cpp | 1156 ++++++ 56_RayQuery/pipeline.groovy | 50 + 59_QuaternionTests/CMakeLists.txt | 68 - 59_QuaternionTests/CQuaternionTester.h | 186 - 59_QuaternionTests/app_resources/common.hlsl | 65 - .../app_resources/quaternionTest.comp.hlsl | 19 - 59_QuaternionTests/main.cpp | 72 - .../CMakeLists.txt | 0 60_ClusteredRendering/config.json.template | 28 + .../main.cpp | 0 .../pipeline.groovy | 0 61_UI/CMakeLists.txt | 13 +- 61_UI/include/common.hpp | 31 +- 61_UI/include/transform.hpp | 22 +- 61_UI/main.cpp | 1463 ++++---- 62_CAD/CMakeLists.txt | 76 +- 62_CAD/CTriangleMesh.cpp | 1 - 62_CAD/CTriangleMesh.h | 137 - 62_CAD/DrawResourcesFiller.cpp | 3281 +++-------------- 62_CAD/DrawResourcesFiller.h | 1089 ++---- 62_CAD/Hatch.cpp | 273 +- 62_CAD/Hatch.h | 8 +- 62_CAD/Images.cpp | 398 -- 62_CAD/Images.h | 476 --- 62_CAD/Polyline.cpp | 702 ---- 62_CAD/Polyline.h | 867 ++++- 62_CAD/SingleLineText.cpp | 39 +- 62_CAD/SingleLineText.h | 13 +- 62_CAD/TransparencyAndAA.md.orig | 15 + 62_CAD/common.hlsl | 527 +++ 62_CAD/curves.cpp | 4 +- 62_CAD/curves.h | 2 +- 62_CAD/fragment_shader.hlsl | 641 ++++ 62_CAD/fragment_shader_debug.hlsl | 16 + 62_CAD/main.cpp | 2275 ++++-------- 62_CAD/resolve_alphas.hlsl | 47 + 62_CAD/resolve_alphas.hlsl.orig | 37 + 62_CAD/scripts/generate_mipmaps.py | 47 - 62_CAD/scripts/tiled_grid.py | 266 -- 62_CAD/shaders/globals.hlsl | 633 ---- .../main_pipeline/all_fragment_shaders.hlsl | 5 - 62_CAD/shaders/main_pipeline/common.hlsl | 265 -- 62_CAD/shaders/main_pipeline/dtm.hlsl | 524 --- .../main_pipeline/fragment_shader.hlsl | 715 ---- .../main_pipeline/fragment_shader_debug.hlsl | 11 - .../main_pipeline/fragment_shader_georef.hlsl | 23 - .../fragment_shader_resolve_alphas.hlsl | 81 - 62_CAD/shaders/main_pipeline/line_style.hlsl | 297 -- .../shaders/main_pipeline/vertex_shader.hlsl | 779 ---- 62_CAD/shaders/runtimeDeviceConfigCaps.hlsl | 6 - .../fragment_shader_solid_color.hlsl | 13 - .../tools/fragment_shader_sample_texture.hlsl | 12 - .../tools/texture_render_fragment_shader.hlsl | 13 - 62_CAD/vertex_shader.hlsl | 572 +++ 64_EmulatedFloatTest/CMakeLists.txt | 72 - .../benchmark/benchmark.comp.hlsl | 125 - .../app_resources/benchmark/common.hlsl | 24 - .../app_resources/common.hlsl | 60 - .../app_resources/test.comp.hlsl | 43 - 64_EmulatedFloatTest/main.cpp | 1220 ------ 66_HLSLBxDFTests/CMakeLists.txt | 54 - 66_HLSLBxDFTests/app_resources/config.json | 31 - .../app_resources/test_compile.comp.hlsl | 103 - .../app_resources/test_components.hlsl | 342 -- 66_HLSLBxDFTests/app_resources/tests.hlsl | 394 -- .../app_resources/tests_common.hlsl | 642 ---- 66_HLSLBxDFTests/main.cpp | 502 --- 66_HLSLBxDFTests/tests.h | 478 --- 67_RayQueryGeometry/CMakeLists.txt | 66 - 67_RayQueryGeometry/README.md | 5 - 67_RayQueryGeometry/app_resources/common.hlsl | 36 - .../app_resources/render.comp.hlsl | 136 - 67_RayQueryGeometry/include/common.hpp | 34 - 67_RayQueryGeometry/main.cpp | 998 ----- 68_JpegLoading/CMakeLists.txt | 27 - 68_JpegLoading/main.cpp | 197 - 70_FLIPFluids/CMakeLists.txt | 99 - 70_FLIPFluids/README.md | 1 - 70_FLIPFluids/app_resources/cellUtils.hlsl | 202 - 70_FLIPFluids/app_resources/common.hlsl | 34 - .../compute/advectParticles.comp.hlsl | 63 - .../compute/applyBodyForces.comp.hlsl | 35 - .../app_resources/compute/diffusion.comp.hlsl | 261 -- .../compute/genParticleVertices.comp.hlsl | 121 - .../compute/particlesInit.comp.hlsl | 39 - .../compute/prepareCellUpdate.comp.hlsl | 91 - .../compute/pressureSolver.comp.hlsl | 202 - .../compute/updateFluidCells.comp.hlsl | 95 - .../app_resources/descriptor_bindings.hlsl | 436 --- .../fluidParticles.fragment.hlsl | 35 - .../app_resources/fluidParticles.vertex.hlsl | 32 - 70_FLIPFluids/app_resources/gridSampling.hlsl | 106 - 70_FLIPFluids/app_resources/gridUtils.hlsl | 91 - .../app_resources/render_common.hlsl | 34 - 70_FLIPFluids/main.cpp | 1884 ---------- 71_RayTracingPipeline/CMakeLists.txt | 120 - 71_RayTracingPipeline/Readme.md | 11 - .../app_resources/common.hlsl | 326 -- .../light_directional.rcall.hlsl | 11 - .../app_resources/light_point.rcall.hlsl | 13 - .../app_resources/light_spot.rcall.hlsl | 16 - .../app_resources/present.frag.hlsl | 19 - .../app_resources/raytrace.rahit.hlsl | 20 - .../app_resources/raytrace.rchit.hlsl | 98 - .../app_resources/raytrace.rgen.hlsl | 139 - .../app_resources/raytrace.rint.hlsl | 54 - .../app_resources/raytrace.rmiss.hlsl | 7 - .../raytrace_procedural.rchit.hlsl | 20 - .../app_resources/raytrace_shadow.rahit.hlsl | 28 - .../app_resources/raytrace_shadow.rmiss.hlsl | 8 - .../docs/Images/final_result.png | Bin 103835 -> 0 bytes .../docs/Images/shader_binding_table.png | Bin 8569 -> 0 bytes 71_RayTracingPipeline/include/common.hpp | 35 - 71_RayTracingPipeline/main.cpp | 1522 -------- 72_CooperativeBinarySearch/CMakeLists.txt | 24 - .../app_resources/binarySearch.comp.hlsl | 120 - .../app_resources/common.h | 15 - .../include/nbl/this_example/common.hpp | 11 - 72_CooperativeBinarySearch/main.cpp | 284 -- 72_CooperativeBinarySearch/testCaseData.h | 1192 ------ 73_GeometryInspector/CMakeLists.txt | 23 - 73_GeometryInspector/include/common.hpp | 22 - 73_GeometryInspector/include/transform.hpp | 162 - 73_GeometryInspector/main.cpp | 741 ---- 74_QuantizedSequenceTests/CMakeLists.txt | 50 - .../CQuantizedSequenceTester.h | 304 -- .../app_resources/common.hlsl | 253 -- .../quantizedSequenceTest.comp.hlsl | 19 - 74_QuantizedSequenceTests/main.cpp | 72 - CMakeLists.txt | 167 +- common/CMakeLists.txt | 123 +- common/CommonAPI.h | 111 + common/CommonPCH/CMakeLists.txt | 15 + common/CommonPCH/PCH.hpp | 13 + common/CommonPCH/main.cpp | 9 + .../{nbl/examples/cameras => }/CCamera.hpp | 92 +- common/include/CEventCallback.hpp | 49 + common/include/CGeomtryCreatorScene.hpp | 1346 +++++++ .../{nbl/examples/common => }/InputSystem.hpp | 37 +- common/include/SBasicViewParameters.hlsl | 17 + .../common => }/SimpleWindowedApplication.hpp | 3 +- common/include/camera/CCubeProjection.hpp | 31 + common/include/camera/ILinearProjection.hpp | 28 + common/include/camera/IProjection.hpp | 24 + common/include/nbl/examples/PCH.hpp | 29 - common/include/nbl/examples/Tester/ITester.h | 435 --- .../common/BuiltinResourcesApplication.hpp | 83 - .../common/CCachedOwenScrambledSequence.hpp | 198 - .../nbl/examples/common/CEventCallback.hpp | 54 - .../common/CSwapchainFramebuffersAndDepth.hpp | 108 - .../examples/common/CachedPipelineState.hpp | 278 -- .../common/KeyedQuantizedSequence.hlsl | 45 - .../examples/common/MonoWindowApplication.hpp | 190 - .../examples/common/SBasicViewParameters.hlsl | 29 - common/include/nbl/examples/examples.hpp | 34 - .../geometry/CGeometryCreatorScene.hpp | 216 -- .../geometry/CSimpleDebugRenderer.hpp | 431 --- .../nbl/examples/geometry/SPushConstants.hlsl | 40 - .../nbl/examples/workgroup/DataAccessors.hlsl | 131 - common/src/CMakeLists.txt | 14 + common/src/camera/CMakeLists.txt | 7 + common/src/empty.cpp | 0 common/src/geometry/CMakeLists.txt | 1 + common/src/geometry/creator/CMakeLists.txt | 69 + .../creator/shaders/gc.basic.fragment.hlsl | 6 + .../creator/shaders/gc.basic.vertex.hlsl | 6 + .../creator/shaders/gc.cone.vertex.hlsl | 6 + .../creator/shaders/gc.ico.vertex.hlsl | 6 + .../creator/shaders/grid.fragment.hlsl | 12 + .../geometry/creator/shaders/grid.vertex.hlsl | 17 + .../template/gc.basic.vertex.input.hlsl | 16 + .../creator/shaders/template/gc.common.hlsl | 18 + .../template/gc.cone.vertex.input.hlsl | 15 + .../shaders/template/gc.ico.vertex.input.hlsl | 15 + .../creator/shaders/template/gc.vertex.hlsl | 22 + .../creator/shaders/template/grid.common.hlsl | 40 + common/src/nbl/examples/CMakeLists.txt | 86 - common/src/nbl/examples/pch.cpp | 1 - .../examples/shaders/geometry/unified.hlsl | 82 - media | 2 +- old_to_refactor/03_GPU_Mesh/CMakeLists.txt | 7 + old_to_refactor/03_GPU_Mesh/main.cpp | 244 ++ .../03_GPU_Mesh}/pipeline.groovy | 6 +- .../04_Keyframe/config.json.template | 28 + .../05_NablaTutorialExample/CMakeLists.txt | 7 + .../config.json.template | 28 + .../05_NablaTutorialExample/main.cpp | 593 +++ .../05_NablaTutorialExample/pipeline.groovy | 50 + .../07_SubpassBaking/config.json.template | 28 + .../11_LoDSystem/config.json.template | 28 + old_to_refactor/12_glTF/config.json.template | 28 + .../14_ComputeScan/config.json.template | 28 + .../config.json.template | 28 + .../config.json.template | 28 + .../18_MitsubaLoader/config.json.template | 28 + .../20_Megatexture/config.json.template | 28 + old_to_refactor/20_Megatexture/main.cpp | 2 +- .../21_DynamicTextureIndexing/CMakeLists.txt | 7 + .../config.json.template | 28 + .../21_DynamicTextureIndexing/main.cpp | 690 ++++ .../21_DynamicTextureIndexing/mesh.frag | 29 + .../21_DynamicTextureIndexing/mesh.vert | 25 + .../21_DynamicTextureIndexing/pipeline.groovy | 50 + old_to_refactor/49_ComputeFFT/CMakeLists.txt | 11 + .../49_ComputeFFT/config.json.template | 28 + .../49_ComputeFFT/extra_parameters.glsl | 16 + .../49_ComputeFFT/fft_convolve_ifft.comp | 109 + .../49_ComputeFFT/image_first_fft.comp | 56 + old_to_refactor/49_ComputeFFT/last_fft.comp | 72 + old_to_refactor/49_ComputeFFT/main.cpp | 753 ++++ .../49_ComputeFFT/normalization.comp | 34 + old_to_refactor/49_ComputeFFT/pipeline.groovy | 50 + .../51_RadixSort/config.json.template | 30 + .../51_RadixSort/config.json.template.orig | 30 + tmp/.gitignore | 2 + 500 files changed, 24283 insertions(+), 61549 deletions(-) create mode 100644 06_MeshLoaders/CMakeLists.txt rename {23_Arithmetic2UnitTest => 06_MeshLoaders}/config.json.template (100%) create mode 100644 06_MeshLoaders/main.cpp rename {29_Arithmetic2Bench => 06_MeshLoaders}/pipeline.groovy (85%) delete mode 100644 11_FFT/CMakeLists.txt delete mode 100644 11_FFT/app_resources/common.hlsl delete mode 100644 11_FFT/app_resources/shader.comp.hlsl delete mode 100644 11_FFT/main.cpp delete mode 100644 11_FFT/pipeline.groovy delete mode 100644 12_MeshLoaders/CMakeLists.txt delete mode 100644 12_MeshLoaders/README.md delete mode 100644 12_MeshLoaders/include/common.hpp delete mode 100644 12_MeshLoaders/main.cpp delete mode 100644 13_MaterialCompilerTest/CMakeLists.txt delete mode 100644 13_MaterialCompilerTest/main.cpp delete mode 100644 14_Mortons/CMakeLists.txt delete mode 100644 14_Mortons/CTester.h delete mode 100644 14_Mortons/app_resources/common.hlsl delete mode 100644 14_Mortons/app_resources/test.comp.hlsl delete mode 100644 14_Mortons/app_resources/test2.comp.hlsl delete mode 100644 14_Mortons/app_resources/testCommon.hlsl delete mode 100644 14_Mortons/app_resources/testCommon2.hlsl delete mode 100644 14_Mortons/config.json.template delete mode 100644 14_Mortons/main.cpp delete mode 100644 15_MitsubaLoader/CMakeLists.txt delete mode 100644 15_MitsubaLoader/main.cpp delete mode 100644 15_MitsubaLoader/test_scenes.txt delete mode 100644 16_ZipArchiveLoaderTest/CMakeLists.txt delete mode 100644 16_ZipArchiveLoaderTest/main.cpp delete mode 100644 22_CppCompat/CIntrinsicsTester.h delete mode 100644 22_CppCompat/CTgmathTester.h delete mode 100644 22_CppCompat/app_resources/intrinsicsTest.comp.hlsl delete mode 100644 22_CppCompat/app_resources/tgmathTest.comp.hlsl delete mode 100644 22_RaytracedAO/addEnvironmentEmitters.comp delete mode 100644 22_RaytracedAO/ci/dummy_4096spp_128depth.xml create mode 100644 22_RaytracedAO/config.json.template create mode 100644 22_RaytracedAO/cull.comp create mode 100644 22_RaytracedAO/extractCubemap.bat create mode 100644 22_RaytracedAO/fillVisBuffer.frag create mode 100644 22_RaytracedAO/mergeCubemap.bat rename {31_HLSLPathTracer => 22_RaytracedAO}/pipeline.groovy (85%) create mode 100644 22_RaytracedAO/test_scenes.txt create mode 100644 22_RaytracedAO/virtualGeometry.glsl delete mode 100644 23_Arithmetic2UnitTest/CMakeLists.txt delete mode 100644 23_Arithmetic2UnitTest/app_resources/shaderCommon.hlsl delete mode 100644 23_Arithmetic2UnitTest/app_resources/testSubgroup.comp.hlsl delete mode 100644 23_Arithmetic2UnitTest/app_resources/testWorkgroup.comp.hlsl delete mode 100644 23_Arithmetic2UnitTest/main.cpp rename {28_FFTBloom => 23_ArithmeticUnitTest}/CMakeLists.txt (99%) rename {23_Arithmetic2UnitTest => 23_ArithmeticUnitTest}/app_resources/common.hlsl (89%) create mode 100644 23_ArithmeticUnitTest/app_resources/shaderCommon.hlsl create mode 100644 23_ArithmeticUnitTest/app_resources/testSubgroup.comp.hlsl create mode 100644 23_ArithmeticUnitTest/app_resources/testWorkgroup.comp.hlsl rename {29_Arithmetic2Bench => 23_ArithmeticUnitTest}/config.json.template (100%) create mode 100644 23_ArithmeticUnitTest/main.cpp rename {23_Arithmetic2UnitTest => 23_ArithmeticUnitTest}/pipeline.groovy (100%) delete mode 100644 26_Blur/CMakeLists.txt delete mode 100644 26_Blur/app_resources/common.hlsl delete mode 100644 26_Blur/app_resources/shader.comp.hlsl delete mode 100644 26_Blur/main.cpp create mode 100644 27_PLYSTLDemo/CMakeLists.txt create mode 100644 27_PLYSTLDemo/config.json.template create mode 100644 27_PLYSTLDemo/main.cpp rename {14_Mortons => 27_PLYSTLDemo}/pipeline.groovy (82%) delete mode 100644 28_FFTBloom/app_resources/common.hlsl delete mode 100644 28_FFTBloom/app_resources/fft_common.hlsl delete mode 100644 28_FFTBloom/app_resources/fft_convolve_ifft.hlsl delete mode 100644 28_FFTBloom/app_resources/fft_mirror_common.hlsl delete mode 100644 28_FFTBloom/app_resources/image_fft_first_axis.hlsl delete mode 100644 28_FFTBloom/app_resources/image_ifft_first_axis.hlsl delete mode 100644 28_FFTBloom/app_resources/kernel_fft_first_axis.hlsl delete mode 100644 28_FFTBloom/app_resources/kernel_fft_second_axis.hlsl delete mode 100644 28_FFTBloom/app_resources/kernel_spectrum_normalize.hlsl delete mode 100644 28_FFTBloom/config.json.template delete mode 100644 28_FFTBloom/main.cpp delete mode 100644 28_FFTBloom/pipeline.groovy delete mode 100644 29_Arithmetic2Bench/CMakeLists.txt delete mode 100644 29_Arithmetic2Bench/app_resources/benchmarkSubgroup.comp.hlsl delete mode 100644 29_Arithmetic2Bench/app_resources/benchmarkWorkgroup.comp.hlsl delete mode 100644 29_Arithmetic2Bench/app_resources/common.hlsl delete mode 100644 29_Arithmetic2Bench/app_resources/shaderCommon.hlsl delete mode 100644 29_Arithmetic2Bench/main.cpp create mode 100644 29_SpecializationConstants/CMakeLists.txt rename {16_ZipArchiveLoaderTest => 29_SpecializationConstants}/config.json.template (99%) create mode 100644 29_SpecializationConstants/main.cpp create mode 100644 29_SpecializationConstants/particles.comp create mode 100644 29_SpecializationConstants/particles.frag create mode 100644 29_SpecializationConstants/particles.vert create mode 100644 29_SpecializationConstants/pipeline.groovy delete mode 100644 31_HLSLPathTracer/CMakeLists.txt delete mode 100644 31_HLSLPathTracer/app_resources/hlsl/compute.render.common.hlsl delete mode 100644 31_HLSLPathTracer/app_resources/hlsl/compute.render.linear.entrypoints.hlsl delete mode 100644 31_HLSLPathTracer/app_resources/hlsl/compute.render.persistent.entrypoints.hlsl delete mode 100644 31_HLSLPathTracer/app_resources/hlsl/compute_render_scene_impl.hlsl delete mode 100644 31_HLSLPathTracer/app_resources/hlsl/example_common.hlsl delete mode 100644 31_HLSLPathTracer/app_resources/hlsl/imgui.unified.hlsl delete mode 100644 31_HLSLPathTracer/app_resources/hlsl/intersector.hlsl delete mode 100644 31_HLSLPathTracer/app_resources/hlsl/material_system.hlsl delete mode 100644 31_HLSLPathTracer/app_resources/hlsl/next_event_estimator.hlsl delete mode 100644 31_HLSLPathTracer/app_resources/hlsl/present.frag.hlsl delete mode 100644 31_HLSLPathTracer/app_resources/hlsl/render_common.hlsl delete mode 100644 31_HLSLPathTracer/app_resources/hlsl/render_rwmc_common.hlsl delete mode 100644 31_HLSLPathTracer/app_resources/hlsl/resolve.comp.hlsl delete mode 100644 31_HLSLPathTracer/app_resources/hlsl/resolve_common.hlsl delete mode 100644 31_HLSLPathTracer/app_resources/hlsl/rwmc_common.hlsl delete mode 100644 31_HLSLPathTracer/app_resources/hlsl/scene_base.hlsl delete mode 100644 31_HLSLPathTracer/app_resources/hlsl/scene_rectangle_light.hlsl delete mode 100644 31_HLSLPathTracer/app_resources/hlsl/scene_sphere_light.hlsl delete mode 100644 31_HLSLPathTracer/app_resources/hlsl/scene_triangle_light.hlsl delete mode 100644 31_HLSLPathTracer/app_resources/hlsl/spirv/pt.compute.rectangle.proxy.hlsl delete mode 100644 31_HLSLPathTracer/app_resources/hlsl/spirv/pt.compute.rectangle.rwmc.persistent.proxy.hlsl delete mode 100644 31_HLSLPathTracer/app_resources/hlsl/spirv/pt.compute.sphere.proxy.hlsl delete mode 100644 31_HLSLPathTracer/app_resources/hlsl/spirv/pt.compute.sphere.rwmc.proxy.hlsl delete mode 100644 31_HLSLPathTracer/app_resources/hlsl/spirv/pt.compute.triangle.methods.shared.hlsl delete mode 100644 31_HLSLPathTracer/app_resources/hlsl/spirv/pt.compute.triangle.persistent.proxy.hlsl delete mode 100644 31_HLSLPathTracer/app_resources/hlsl/spirv/pt.compute.triangle.rwmc.persistent.proxy.hlsl delete mode 100644 31_HLSLPathTracer/app_resources/hlsl/spirv/pt.compute.variant.shared.hlsl delete mode 100644 31_HLSLPathTracer/app_resources/hlsl/spirv/pt.misc.proxy.hlsl delete mode 100644 31_HLSLPathTracer/include/nbl/this_example/common.hpp delete mode 100644 31_HLSLPathTracer/include/nbl/this_example/path_tracer_pipeline_state.hpp delete mode 100644 31_HLSLPathTracer/include/nbl/this_example/path_tracer_ui.hpp delete mode 100644 31_HLSLPathTracer/include/nbl/this_example/render_variant_config.hlsl delete mode 100644 31_HLSLPathTracer/include/nbl/this_example/render_variant_enums.hlsl delete mode 100644 31_HLSLPathTracer/include/nbl/this_example/render_variant_info.hpp delete mode 100644 31_HLSLPathTracer/include/nbl/this_example/render_variant_strings.hpp delete mode 100644 31_HLSLPathTracer/include/nbl/this_example/transform.hpp delete mode 100644 31_HLSLPathTracer/main.cpp delete mode 100644 31_HLSLPathTracer/path_tracer.runtime.json.in delete mode 100644 31_HLSLPathTracer/pt.cmake delete mode 100644 31_HLSLPathTracer/pt.variant_ids.cmake delete mode 100644 34_DebugDraw/CMakeLists.txt delete mode 100644 34_DebugDraw/include/common.hpp delete mode 100644 34_DebugDraw/main.cpp delete mode 100644 40_PathTracer/CMakeLists.txt delete mode 100644 40_PathTracer/app_resources/pathtrace/beauty.hlsl delete mode 100644 40_PathTracer/app_resources/pathtrace/common.hlsl delete mode 100644 40_PathTracer/app_resources/pathtrace/debug.hlsl delete mode 100644 40_PathTracer/app_resources/pathtrace/previs.hlsl delete mode 100644 40_PathTracer/app_resources/present/default.hlsl delete mode 100644 40_PathTracer/include/common.hpp delete mode 100644 40_PathTracer/include/io/CSceneLoader.h delete mode 100644 40_PathTracer/include/renderer/CRenderer.h delete mode 100644 40_PathTracer/include/renderer/CScene.h delete mode 100644 40_PathTracer/include/renderer/CSession.h delete mode 100644 40_PathTracer/include/renderer/SAASequence.h delete mode 100644 40_PathTracer/include/renderer/present/CWindowPresenter.h delete mode 100644 40_PathTracer/include/renderer/present/IPresenter.h delete mode 100644 40_PathTracer/include/renderer/resolve/CBasicRWMCResolver.h delete mode 100644 40_PathTracer/include/renderer/resolve/IResolver.h delete mode 100644 40_PathTracer/include/renderer/shaders/common.hlsl delete mode 100644 40_PathTracer/include/renderer/shaders/pathtrace/common.hlsl delete mode 100644 40_PathTracer/include/renderer/shaders/pathtrace/push_constants.hlsl delete mode 100644 40_PathTracer/include/renderer/shaders/present/push_constants.hlsl delete mode 100644 40_PathTracer/include/renderer/shaders/resolve/rwmc.hlsl delete mode 100644 40_PathTracer/include/renderer/shaders/scene.hlsl delete mode 100644 40_PathTracer/include/renderer/shaders/session.hlsl delete mode 100644 40_PathTracer/main.cpp delete mode 100644 40_PathTracer/src/io/CSceneLoader.cpp delete mode 100644 40_PathTracer/src/renderer/CRenderer.cpp delete mode 100644 40_PathTracer/src/renderer/CScene.cpp delete mode 100644 40_PathTracer/src/renderer/CSession.cpp delete mode 100644 40_PathTracer/src/renderer/present/CWindowPresenter.cpp delete mode 100644 40_PathTracer/src/renderer/resolve/CBasicRWMCResolver.cpp create mode 100644 41_VisibilityBuffer/CMakeLists.txt create mode 100644 41_VisibilityBuffer/common.glsl create mode 100644 41_VisibilityBuffer/common.h rename {31_HLSLPathTracer => 41_VisibilityBuffer}/config.json.template (99%) create mode 100644 41_VisibilityBuffer/cull.comp create mode 100644 41_VisibilityBuffer/cullShaderCommon.h create mode 100644 41_VisibilityBuffer/fillVBuffer.frag create mode 100644 41_VisibilityBuffer/fillVBuffer.vert create mode 100644 41_VisibilityBuffer/main.cpp create mode 100644 41_VisibilityBuffer/occlusionCull.frag create mode 100644 41_VisibilityBuffer/occlusionCull.vert create mode 100644 41_VisibilityBuffer/occlusionCullMap.comp create mode 100644 41_VisibilityBuffer/occlusionCullingShaderCommon.glsl create mode 100644 41_VisibilityBuffer/pipeline.groovy create mode 100644 41_VisibilityBuffer/rasterizationCommon.h create mode 100644 41_VisibilityBuffer/shadeVBuffer.comp create mode 100644 42_FragmentShaderPathTracer/CMakeLists.txt rename {31_HLSLPathTracer/app_resources/glsl => 42_FragmentShaderPathTracer}/common.glsl (93%) rename {72_CooperativeBinarySearch => 42_FragmentShaderPathTracer}/config.json.template (99%) rename {31_HLSLPathTracer/app_resources/glsl => 42_FragmentShaderPathTracer}/litByRectangle.comp (99%) rename {31_HLSLPathTracer/app_resources/glsl => 42_FragmentShaderPathTracer}/litBySphere.comp (97%) rename {31_HLSLPathTracer/app_resources/glsl => 42_FragmentShaderPathTracer}/litByTriangle.comp (98%) create mode 100644 42_FragmentShaderPathTracer/main.cpp create mode 100644 42_FragmentShaderPathTracer/pipeline.groovy create mode 100644 43_SumAndCDFFilters/CMakeLists.txt rename {11_FFT => 43_SumAndCDFFilters}/config.json.template (75%) create mode 100644 43_SumAndCDFFilters/main.cpp create mode 100644 43_SumAndCDFFilters/pipeline.groovy delete mode 100644 50.IESViewer/App.hpp delete mode 100644 50.IESViewer/AppEvent.cpp delete mode 100644 50.IESViewer/AppGPU.cpp delete mode 100644 50.IESViewer/AppInit.cpp delete mode 100644 50.IESViewer/AppInputParser.cpp delete mode 100644 50.IESViewer/AppInputParser.hpp delete mode 100644 50.IESViewer/AppRender.cpp delete mode 100644 50.IESViewer/AppUI.cpp delete mode 100644 50.IESViewer/CMakeLists.txt delete mode 100644 50.IESViewer/CSimpleIESRenderer.hpp delete mode 100644 50.IESViewer/IES.cpp delete mode 100644 50.IESViewer/IES.hpp delete mode 100644 50.IESViewer/app_resources/common.hlsl delete mode 100644 50.IESViewer/app_resources/false_color.hlsl delete mode 100644 50.IESViewer/app_resources/ies.unified.hlsl delete mode 100644 50.IESViewer/app_resources/imgui.opts.hlsl delete mode 100644 50.IESViewer/app_resources/imgui.unified.hlsl delete mode 100644 50.IESViewer/inputs.json delete mode 100644 50.IESViewer/main.cpp create mode 100644 53_ComputeShaders/CMakeLists.txt create mode 100644 53_ComputeShaders/computeShader.comp create mode 100644 53_ComputeShaders/config.json.template create mode 100644 53_ComputeShaders/fragmentShader.frag create mode 100644 53_ComputeShaders/geometryShader.geom create mode 100644 53_ComputeShaders/main.cpp create mode 100644 53_ComputeShaders/pipeline.groovy create mode 100644 53_ComputeShaders/shaderCommon.glsl create mode 100644 53_ComputeShaders/vertexShader.vert create mode 100644 56_RayQuery/CMakeLists.txt create mode 100644 56_RayQuery/common.glsl create mode 100644 56_RayQuery/config.json.template create mode 100644 56_RayQuery/litByRectangle.comp create mode 100644 56_RayQuery/litBySphere.comp create mode 100644 56_RayQuery/litByTriangle.comp create mode 100644 56_RayQuery/main.cpp create mode 100644 56_RayQuery/pipeline.groovy delete mode 100644 59_QuaternionTests/CMakeLists.txt delete mode 100644 59_QuaternionTests/CQuaternionTester.h delete mode 100644 59_QuaternionTests/app_resources/common.hlsl delete mode 100644 59_QuaternionTests/app_resources/quaternionTest.comp.hlsl delete mode 100644 59_QuaternionTests/main.cpp rename {old_to_refactor/60_ClusteredRendering => 60_ClusteredRendering}/CMakeLists.txt (100%) create mode 100644 60_ClusteredRendering/config.json.template rename {old_to_refactor/60_ClusteredRendering => 60_ClusteredRendering}/main.cpp (100%) rename {old_to_refactor/60_ClusteredRendering => 60_ClusteredRendering}/pipeline.groovy (100%) delete mode 100644 62_CAD/CTriangleMesh.cpp delete mode 100644 62_CAD/CTriangleMesh.h delete mode 100644 62_CAD/Images.cpp delete mode 100644 62_CAD/Images.h delete mode 100644 62_CAD/Polyline.cpp create mode 100644 62_CAD/TransparencyAndAA.md.orig create mode 100644 62_CAD/common.hlsl create mode 100644 62_CAD/fragment_shader.hlsl create mode 100644 62_CAD/fragment_shader_debug.hlsl create mode 100644 62_CAD/resolve_alphas.hlsl create mode 100644 62_CAD/resolve_alphas.hlsl.orig delete mode 100644 62_CAD/scripts/generate_mipmaps.py delete mode 100644 62_CAD/scripts/tiled_grid.py delete mode 100644 62_CAD/shaders/globals.hlsl delete mode 100644 62_CAD/shaders/main_pipeline/all_fragment_shaders.hlsl delete mode 100644 62_CAD/shaders/main_pipeline/common.hlsl delete mode 100644 62_CAD/shaders/main_pipeline/dtm.hlsl delete mode 100644 62_CAD/shaders/main_pipeline/fragment_shader.hlsl delete mode 100644 62_CAD/shaders/main_pipeline/fragment_shader_debug.hlsl delete mode 100644 62_CAD/shaders/main_pipeline/fragment_shader_georef.hlsl delete mode 100644 62_CAD/shaders/main_pipeline/fragment_shader_resolve_alphas.hlsl delete mode 100644 62_CAD/shaders/main_pipeline/line_style.hlsl delete mode 100644 62_CAD/shaders/main_pipeline/vertex_shader.hlsl delete mode 100644 62_CAD/shaders/runtimeDeviceConfigCaps.hlsl delete mode 100644 62_CAD/shaders/solid_color/fragment_shader_solid_color.hlsl delete mode 100644 62_CAD/shaders/tools/fragment_shader_sample_texture.hlsl delete mode 100644 62_CAD/shaders/tools/texture_render_fragment_shader.hlsl create mode 100644 62_CAD/vertex_shader.hlsl delete mode 100644 64_EmulatedFloatTest/CMakeLists.txt delete mode 100644 64_EmulatedFloatTest/app_resources/benchmark/benchmark.comp.hlsl delete mode 100644 64_EmulatedFloatTest/app_resources/benchmark/common.hlsl delete mode 100644 64_EmulatedFloatTest/app_resources/common.hlsl delete mode 100644 64_EmulatedFloatTest/app_resources/test.comp.hlsl delete mode 100644 64_EmulatedFloatTest/main.cpp delete mode 100644 66_HLSLBxDFTests/CMakeLists.txt delete mode 100644 66_HLSLBxDFTests/app_resources/config.json delete mode 100644 66_HLSLBxDFTests/app_resources/test_compile.comp.hlsl delete mode 100644 66_HLSLBxDFTests/app_resources/test_components.hlsl delete mode 100644 66_HLSLBxDFTests/app_resources/tests.hlsl delete mode 100644 66_HLSLBxDFTests/app_resources/tests_common.hlsl delete mode 100644 66_HLSLBxDFTests/main.cpp delete mode 100644 66_HLSLBxDFTests/tests.h delete mode 100644 67_RayQueryGeometry/CMakeLists.txt delete mode 100644 67_RayQueryGeometry/README.md delete mode 100644 67_RayQueryGeometry/app_resources/common.hlsl delete mode 100644 67_RayQueryGeometry/app_resources/render.comp.hlsl delete mode 100644 67_RayQueryGeometry/include/common.hpp delete mode 100644 67_RayQueryGeometry/main.cpp delete mode 100644 68_JpegLoading/CMakeLists.txt delete mode 100644 68_JpegLoading/main.cpp delete mode 100644 70_FLIPFluids/CMakeLists.txt delete mode 100644 70_FLIPFluids/README.md delete mode 100644 70_FLIPFluids/app_resources/cellUtils.hlsl delete mode 100644 70_FLIPFluids/app_resources/common.hlsl delete mode 100644 70_FLIPFluids/app_resources/compute/advectParticles.comp.hlsl delete mode 100644 70_FLIPFluids/app_resources/compute/applyBodyForces.comp.hlsl delete mode 100644 70_FLIPFluids/app_resources/compute/diffusion.comp.hlsl delete mode 100644 70_FLIPFluids/app_resources/compute/genParticleVertices.comp.hlsl delete mode 100644 70_FLIPFluids/app_resources/compute/particlesInit.comp.hlsl delete mode 100644 70_FLIPFluids/app_resources/compute/prepareCellUpdate.comp.hlsl delete mode 100644 70_FLIPFluids/app_resources/compute/pressureSolver.comp.hlsl delete mode 100644 70_FLIPFluids/app_resources/compute/updateFluidCells.comp.hlsl delete mode 100644 70_FLIPFluids/app_resources/descriptor_bindings.hlsl delete mode 100644 70_FLIPFluids/app_resources/fluidParticles.fragment.hlsl delete mode 100644 70_FLIPFluids/app_resources/fluidParticles.vertex.hlsl delete mode 100644 70_FLIPFluids/app_resources/gridSampling.hlsl delete mode 100644 70_FLIPFluids/app_resources/gridUtils.hlsl delete mode 100644 70_FLIPFluids/app_resources/render_common.hlsl delete mode 100644 70_FLIPFluids/main.cpp delete mode 100644 71_RayTracingPipeline/CMakeLists.txt delete mode 100644 71_RayTracingPipeline/Readme.md delete mode 100644 71_RayTracingPipeline/app_resources/common.hlsl delete mode 100644 71_RayTracingPipeline/app_resources/light_directional.rcall.hlsl delete mode 100644 71_RayTracingPipeline/app_resources/light_point.rcall.hlsl delete mode 100644 71_RayTracingPipeline/app_resources/light_spot.rcall.hlsl delete mode 100644 71_RayTracingPipeline/app_resources/present.frag.hlsl delete mode 100644 71_RayTracingPipeline/app_resources/raytrace.rahit.hlsl delete mode 100644 71_RayTracingPipeline/app_resources/raytrace.rchit.hlsl delete mode 100644 71_RayTracingPipeline/app_resources/raytrace.rgen.hlsl delete mode 100644 71_RayTracingPipeline/app_resources/raytrace.rint.hlsl delete mode 100644 71_RayTracingPipeline/app_resources/raytrace.rmiss.hlsl delete mode 100644 71_RayTracingPipeline/app_resources/raytrace_procedural.rchit.hlsl delete mode 100644 71_RayTracingPipeline/app_resources/raytrace_shadow.rahit.hlsl delete mode 100644 71_RayTracingPipeline/app_resources/raytrace_shadow.rmiss.hlsl delete mode 100644 71_RayTracingPipeline/docs/Images/final_result.png delete mode 100644 71_RayTracingPipeline/docs/Images/shader_binding_table.png delete mode 100644 71_RayTracingPipeline/include/common.hpp delete mode 100644 71_RayTracingPipeline/main.cpp delete mode 100644 72_CooperativeBinarySearch/CMakeLists.txt delete mode 100644 72_CooperativeBinarySearch/app_resources/binarySearch.comp.hlsl delete mode 100644 72_CooperativeBinarySearch/app_resources/common.h delete mode 100644 72_CooperativeBinarySearch/include/nbl/this_example/common.hpp delete mode 100644 72_CooperativeBinarySearch/main.cpp delete mode 100644 72_CooperativeBinarySearch/testCaseData.h delete mode 100644 73_GeometryInspector/CMakeLists.txt delete mode 100644 73_GeometryInspector/include/common.hpp delete mode 100644 73_GeometryInspector/include/transform.hpp delete mode 100644 73_GeometryInspector/main.cpp delete mode 100644 74_QuantizedSequenceTests/CMakeLists.txt delete mode 100644 74_QuantizedSequenceTests/CQuantizedSequenceTester.h delete mode 100644 74_QuantizedSequenceTests/app_resources/common.hlsl delete mode 100644 74_QuantizedSequenceTests/app_resources/quantizedSequenceTest.comp.hlsl delete mode 100644 74_QuantizedSequenceTests/main.cpp create mode 100644 common/CommonAPI.h create mode 100644 common/CommonPCH/CMakeLists.txt create mode 100644 common/CommonPCH/PCH.hpp create mode 100644 common/CommonPCH/main.cpp rename common/include/{nbl/examples/cameras => }/CCamera.hpp (72%) create mode 100644 common/include/CEventCallback.hpp create mode 100644 common/include/CGeomtryCreatorScene.hpp rename common/include/{nbl/examples/common => }/InputSystem.hpp (84%) create mode 100644 common/include/SBasicViewParameters.hlsl rename common/include/{nbl/examples/common => }/SimpleWindowedApplication.hpp (99%) create mode 100644 common/include/camera/CCubeProjection.hpp create mode 100644 common/include/camera/ILinearProjection.hpp create mode 100644 common/include/camera/IProjection.hpp delete mode 100644 common/include/nbl/examples/PCH.hpp delete mode 100644 common/include/nbl/examples/Tester/ITester.h delete mode 100644 common/include/nbl/examples/common/BuiltinResourcesApplication.hpp delete mode 100644 common/include/nbl/examples/common/CCachedOwenScrambledSequence.hpp delete mode 100644 common/include/nbl/examples/common/CEventCallback.hpp delete mode 100644 common/include/nbl/examples/common/CSwapchainFramebuffersAndDepth.hpp delete mode 100644 common/include/nbl/examples/common/CachedPipelineState.hpp delete mode 100644 common/include/nbl/examples/common/KeyedQuantizedSequence.hlsl delete mode 100644 common/include/nbl/examples/common/MonoWindowApplication.hpp delete mode 100644 common/include/nbl/examples/common/SBasicViewParameters.hlsl delete mode 100644 common/include/nbl/examples/examples.hpp delete mode 100644 common/include/nbl/examples/geometry/CGeometryCreatorScene.hpp delete mode 100644 common/include/nbl/examples/geometry/CSimpleDebugRenderer.hpp delete mode 100644 common/include/nbl/examples/geometry/SPushConstants.hlsl delete mode 100644 common/include/nbl/examples/workgroup/DataAccessors.hlsl create mode 100644 common/src/CMakeLists.txt create mode 100644 common/src/camera/CMakeLists.txt create mode 100644 common/src/empty.cpp create mode 100644 common/src/geometry/CMakeLists.txt create mode 100644 common/src/geometry/creator/CMakeLists.txt create mode 100644 common/src/geometry/creator/shaders/gc.basic.fragment.hlsl create mode 100644 common/src/geometry/creator/shaders/gc.basic.vertex.hlsl create mode 100644 common/src/geometry/creator/shaders/gc.cone.vertex.hlsl create mode 100644 common/src/geometry/creator/shaders/gc.ico.vertex.hlsl create mode 100644 common/src/geometry/creator/shaders/grid.fragment.hlsl create mode 100644 common/src/geometry/creator/shaders/grid.vertex.hlsl create mode 100644 common/src/geometry/creator/shaders/template/gc.basic.vertex.input.hlsl create mode 100644 common/src/geometry/creator/shaders/template/gc.common.hlsl create mode 100644 common/src/geometry/creator/shaders/template/gc.cone.vertex.input.hlsl create mode 100644 common/src/geometry/creator/shaders/template/gc.ico.vertex.input.hlsl create mode 100644 common/src/geometry/creator/shaders/template/gc.vertex.hlsl create mode 100644 common/src/geometry/creator/shaders/template/grid.common.hlsl delete mode 100644 common/src/nbl/examples/CMakeLists.txt delete mode 100644 common/src/nbl/examples/pch.cpp delete mode 100644 common/src/nbl/examples/shaders/geometry/unified.hlsl create mode 100644 old_to_refactor/03_GPU_Mesh/CMakeLists.txt create mode 100644 old_to_refactor/03_GPU_Mesh/main.cpp rename {68_JpegLoading => old_to_refactor/03_GPU_Mesh}/pipeline.groovy (88%) create mode 100644 old_to_refactor/04_Keyframe/config.json.template create mode 100644 old_to_refactor/05_NablaTutorialExample/CMakeLists.txt create mode 100644 old_to_refactor/05_NablaTutorialExample/config.json.template create mode 100644 old_to_refactor/05_NablaTutorialExample/main.cpp create mode 100644 old_to_refactor/05_NablaTutorialExample/pipeline.groovy create mode 100644 old_to_refactor/07_SubpassBaking/config.json.template create mode 100644 old_to_refactor/11_LoDSystem/config.json.template create mode 100644 old_to_refactor/12_glTF/config.json.template create mode 100644 old_to_refactor/14_ComputeScan/config.json.template create mode 100644 old_to_refactor/16_OrderIndependentTransparency/config.json.template create mode 100644 old_to_refactor/17_SimpleBulletIntegration/config.json.template create mode 100644 old_to_refactor/18_MitsubaLoader/config.json.template create mode 100644 old_to_refactor/20_Megatexture/config.json.template create mode 100644 old_to_refactor/21_DynamicTextureIndexing/CMakeLists.txt create mode 100644 old_to_refactor/21_DynamicTextureIndexing/config.json.template create mode 100644 old_to_refactor/21_DynamicTextureIndexing/main.cpp create mode 100644 old_to_refactor/21_DynamicTextureIndexing/mesh.frag create mode 100644 old_to_refactor/21_DynamicTextureIndexing/mesh.vert create mode 100644 old_to_refactor/21_DynamicTextureIndexing/pipeline.groovy create mode 100644 old_to_refactor/49_ComputeFFT/CMakeLists.txt create mode 100644 old_to_refactor/49_ComputeFFT/config.json.template create mode 100644 old_to_refactor/49_ComputeFFT/extra_parameters.glsl create mode 100644 old_to_refactor/49_ComputeFFT/fft_convolve_ifft.comp create mode 100644 old_to_refactor/49_ComputeFFT/image_first_fft.comp create mode 100644 old_to_refactor/49_ComputeFFT/last_fft.comp create mode 100644 old_to_refactor/49_ComputeFFT/main.cpp create mode 100644 old_to_refactor/49_ComputeFFT/normalization.comp create mode 100644 old_to_refactor/49_ComputeFFT/pipeline.groovy create mode 100644 old_to_refactor/51_RadixSort/config.json.template create mode 100644 old_to_refactor/51_RadixSort/config.json.template.orig create mode 100644 tmp/.gitignore diff --git a/.gitignore b/.gitignore index 615bac335..af9066147 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,3 @@ compiled.spv *.pyc */.vscode/* */__main__.py -tmp diff --git a/01_HelloCoreSystemAsset/main.cpp b/01_HelloCoreSystemAsset/main.cpp index 7ca4badb4..6a9188344 100644 --- a/01_HelloCoreSystemAsset/main.cpp +++ b/01_HelloCoreSystemAsset/main.cpp @@ -2,8 +2,8 @@ // This file is part of the "Nabla Engine". // For conditions of distribution and use, see copyright notice in nabla.h -// public interface and common examples API, always include first before std:: headers -#include "nbl/examples/examples.hpp" +// always include nabla first before std:: headers +#include "nabla.h" #include "nbl/system/IApplicationFramework.h" diff --git a/02_HelloCompute/main.cpp b/02_HelloCompute/main.cpp index 32812fb1a..971961f1a 100644 --- a/02_HelloCompute/main.cpp +++ b/02_HelloCompute/main.cpp @@ -94,9 +94,9 @@ class HelloComputeApp final : public nbl::application_templates::MonoSystemMonoL // The convention is that an `ICPU` object represents a potentially Mutable (and in the past, Serializable) recipe for creating an `IGPU` object, and later examples will show automated systems for doing that. // The Assets always form a Directed Acyclic Graph and our type system enforces that property at compile time (i.e. an `IBuffer` cannot reference an `IImageView` even indirectly). // Another reason for the 1:1 pairing of types is that one can use a CPU-to-GPU associative cache (asset manager has a default one) and use the pointers to the CPU objects as UUIDs. - // The IShader is just a mutable container for source code (can be high level like HLSL needing compilation to SPIR-V or SPIR-V itself) held in an `nbl::asset::ICPUBuffer`. + // The ICPUShader is just a mutable container for source code (can be high level like HLSL needing compilation to SPIR-V or SPIR-V itself) held in an `nbl::asset::ICPUBuffer`. // They can be created: from buffers of code, by compilation from some other source code, or loaded from files (next example will do that). - smart_refctd_ptr cpuShader; + smart_refctd_ptr cpuShader; { // Normally we'd use the ISystem and the IAssetManager to load shaders flexibly from (virtual) files for ease of development (syntax highlighting and Intellisense), // but I want to show the full process of assembling a shader from raw source code at least once. @@ -138,7 +138,7 @@ class HelloComputeApp final : public nbl::application_templates::MonoSystemMonoL } // Note how each ILogicalDevice method takes a smart-pointer r-value, so that the GPU objects refcount their dependencies - smart_refctd_ptr shader = device->compileShader({.source = cpuShader.get()}); + smart_refctd_ptr shader = device->createShader(cpuShader.get()); if (!shader) return logFail("Failed to create a GPU Shader, seems the Driver doesn't like the SPIR-V we're feeding it!\n"); @@ -148,8 +148,8 @@ class HelloComputeApp final : public nbl::application_templates::MonoSystemMonoL .binding=0, .type=nbl::asset::IDescriptor::E_TYPE::ET_STORAGE_BUFFER, .createFlags=IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, // not is not the time for descriptor indexing - .stageFlags=nbl::hlsl::ShaderStage::ESS_COMPUTE, - .count=1 + .stageFlags=IGPUShader::E_SHADER_STAGE::ESS_COMPUTE, + .count=1, } }; smart_refctd_ptr dsLayout = device->createDescriptorSetLayout(bindings); diff --git a/03_DeviceSelectionAndSharedSources/Testers.h b/03_DeviceSelectionAndSharedSources/Testers.h index fcd5c5ee4..a76d4b668 100644 --- a/03_DeviceSelectionAndSharedSources/Testers.h +++ b/03_DeviceSelectionAndSharedSources/Testers.h @@ -4,7 +4,8 @@ #ifndef _NBL_TESTERS_H_INCLUDED_ #define _NBL_TESTERS_H_INCLUDED_ -#include "nbl/examples/examples.hpp" +#include "nbl/application_templates/MonoDeviceApplication.hpp" +#include "nbl/application_templates/MonoAssetManagerAndBuiltinResourceApplication.hpp" using namespace nbl; @@ -23,7 +24,7 @@ class IntrospectionTesterBase const std::string m_functionToTestName = ""; protected: - static std::pair, smart_refctd_ptr> compileHLSLShaderAndTestIntrospection( + static std::pair, smart_refctd_ptr> compileHLSLShaderAndTestIntrospection( video::IPhysicalDevice* physicalDevice, video::ILogicalDevice* device, system::ILogger* logger, asset::IAssetManager* assetMgr, const std::string& shaderPath, CSPIRVIntrospector& introspector) { IAssetLoader::SAssetLoadParams lp = {}; @@ -32,18 +33,15 @@ class IntrospectionTesterBase // this time we load a shader directly from a file auto assetBundle = assetMgr->getAsset(shaderPath, lp); const auto assets = assetBundle.getContents(); - const auto* metadata = assetBundle.getMetadata(); - if (assets.empty() || assetBundle.getAssetType() != IAsset::ET_SHADER) + if (assets.empty()) { logFail(logger, "Could not load shader!"); assert(0); } - const auto hlslMetadata = static_cast(metadata); - const auto shaderStage = hlslMetadata->shaderStages->front(); // It would be super weird if loading a shader from a file produced more than 1 asset assert(assets.size() == 1); - smart_refctd_ptr source = IAsset::castDown(assets[0]); + smart_refctd_ptr source = IAsset::castDown(assets[0]); smart_refctd_ptr introspection; { @@ -55,8 +53,8 @@ class IntrospectionTesterBase // The Shader Asset Loaders deduce the stage from the file extension, // if the extension is generic (.glsl or .hlsl) the stage is unknown. // But it can still be overriden from within the source with a `#pragma shader_stage` - options.stage = shaderStage == IShader::E_SHADER_STAGE::ESS_COMPUTE ? shaderStage : IShader::E_SHADER_STAGE::ESS_VERTEX; // TODO: do smth with it - options.preprocessorOptions.targetSpirvVersion = device->getPhysicalDevice()->getLimits().spirvVersion; + options.stage = source->getStage() == IShader::E_SHADER_STAGE::ESS_COMPUTE ? source->getStage() : IShader::E_SHADER_STAGE::ESS_VERTEX; // TODO: do smth with it + options.targetSpirvVersion = device->getPhysicalDevice()->getLimits().spirvVersion; // we need to perform an unoptimized compilation with source debug info or we'll lose names of variable sin the introspection options.spirvOptimizer = nullptr; options.debugInfoFlags |= IShaderCompiler::E_DEBUG_INFO_FLAGS::EDIF_SOURCE_BIT; @@ -188,7 +186,7 @@ class PredefinedLayoutTester final : public IntrospectionTesterBase constexpr uint32_t MERGE_TEST_SHADERS_CNT = mergeTestShadersPaths.size(); CSPIRVIntrospector introspector[MERGE_TEST_SHADERS_CNT]; - smart_refctd_ptr sources[MERGE_TEST_SHADERS_CNT]; + smart_refctd_ptr sources[MERGE_TEST_SHADERS_CNT]; for (uint32_t i = 0u; i < MERGE_TEST_SHADERS_CNT; ++i) { @@ -203,7 +201,7 @@ class PredefinedLayoutTester final : public IntrospectionTesterBase .binding = 0, .type = nbl::asset::IDescriptor::E_TYPE::ET_STORAGE_BUFFER, .createFlags = ICPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - .stageFlags = IShader::E_SHADER_STAGE::ESS_COMPUTE, + .stageFlags = ICPUShader::E_SHADER_STAGE::ESS_COMPUTE, .count = 1, .immutableSamplers = nullptr } @@ -215,7 +213,7 @@ class PredefinedLayoutTester final : public IntrospectionTesterBase .binding = 0, .type = nbl::asset::IDescriptor::E_TYPE::ET_STORAGE_BUFFER, .createFlags = ICPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - .stageFlags = IShader::E_SHADER_STAGE::ESS_COMPUTE, + .stageFlags = ICPUShader::E_SHADER_STAGE::ESS_COMPUTE, .count = 1, .immutableSamplers = nullptr }, @@ -223,7 +221,7 @@ class PredefinedLayoutTester final : public IntrospectionTesterBase .binding = 1, .type = nbl::asset::IDescriptor::E_TYPE::ET_STORAGE_BUFFER, .createFlags = ICPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - .stageFlags = IShader::E_SHADER_STAGE::ESS_COMPUTE, + .stageFlags = ICPUShader::E_SHADER_STAGE::ESS_COMPUTE, .count = 2, .immutableSamplers = nullptr } @@ -253,9 +251,9 @@ class PredefinedLayoutTester final : public IntrospectionTesterBase bool pplnCreationSuccess[MERGE_TEST_SHADERS_CNT]; for (uint32_t i = 0u; i < MERGE_TEST_SHADERS_CNT; ++i) { - ICPUPipelineBase::SShaderSpecInfo specInfo; + ICPUShader::SSpecInfo specInfo; specInfo.entryPoint = "main"; - specInfo.shader = sources[i]; + specInfo.shader = sources[i].get(); pplnCreationSuccess[i] = static_cast(introspector[i].createApproximateComputePipelineFromIntrospection(specInfo, core::smart_refctd_ptr(predefinedPplnLayout))); } diff --git a/03_DeviceSelectionAndSharedSources/main.cpp b/03_DeviceSelectionAndSharedSources/main.cpp index bcc849a4d..be56791a1 100644 --- a/03_DeviceSelectionAndSharedSources/main.cpp +++ b/03_DeviceSelectionAndSharedSources/main.cpp @@ -2,20 +2,15 @@ // This file is part of the "Nabla Engine". // For conditions of distribution and use, see copyright notice in nabla.h - -#include "nbl/examples/examples.hpp" -// TODO: why isn't this in `nabla.h` ? -#include "nbl/asset/metadata/CHLSLMetadata.h" - +#include "nbl/application_templates/MonoDeviceApplication.hpp" +#include "nbl/application_templates/MonoAssetManagerAndBuiltinResourceApplication.hpp" +#include "CommonPCH/PCH.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; +using namespace core; +using namespace system; +using namespace asset; +using namespace video; // TODO[Przemek]: update comments @@ -26,10 +21,10 @@ using namespace nbl::examples; constexpr bool ENABLE_TESTS = false; // This time we create the device in the base class and also use a base class to give us an Asset Manager and an already mounted built-in resource archive -class DeviceSelectionAndSharedSourcesApp final : public application_templates::MonoDeviceApplication, public BuiltinResourcesApplication +class DeviceSelectionAndSharedSourcesApp final : public application_templates::MonoDeviceApplication, public application_templates::MonoAssetManagerAndBuiltinResourceApplication { using device_base_t = application_templates::MonoDeviceApplication; - using asset_base_t = BuiltinResourcesApplication; + using asset_base_t = application_templates::MonoAssetManagerAndBuiltinResourceApplication; public: // Yay thanks to multiple inheritance we cannot forward ctors anymore DeviceSelectionAndSharedSourcesApp(const path& _localInputCWD, const path& _localOutputCWD, const path& _sharedInputCWD, const path& _sharedOutputCWD) : @@ -65,9 +60,9 @@ class DeviceSelectionAndSharedSourcesApp final : public application_templates::M //shaderIntrospection->debugPrint(m_logger.get()); // We've now skipped the manual creation of a descriptor set layout, pipeline layout - ICPUPipelineBase::SShaderSpecInfo specInfo; + ICPUShader::SSpecInfo specInfo; specInfo.entryPoint = "main"; - specInfo.shader = source; + specInfo.shader = source.get(); smart_refctd_ptr cpuPipeline = introspector.createApproximateComputePipelineFromIntrospection(specInfo); @@ -241,7 +236,7 @@ class DeviceSelectionAndSharedSourcesApp final : public application_templates::M // Whether to keep invoking the above. In this example because its headless GPU compute, we do all the work in the app initialization. bool keepRunning() override { return false; } - std::pair, smart_refctd_ptr> compileShaderAndTestIntrospection( + std::pair, smart_refctd_ptr> compileShaderAndTestIntrospection( const std::string& shaderPath, CSPIRVIntrospector& introspector) { IAssetLoader::SAssetLoadParams lp = {}; @@ -250,19 +245,15 @@ class DeviceSelectionAndSharedSourcesApp final : public application_templates::M // this time we load a shader directly from a file auto assetBundle = m_assetMgr->getAsset(shaderPath, lp); const auto assets = assetBundle.getContents(); - if (assets.empty() || assetBundle.getAssetType() != IAsset::ET_SHADER) + if (assets.empty()) { logFail("Could not load shader!"); assert(0); } - const auto* metadata = assetBundle.getMetadata(); - const auto hlslMetadata = static_cast(metadata); - const auto shaderStage = hlslMetadata->shaderStages->front(); - // It would be super weird if loading a shader from a file produced more than 1 asset assert(assets.size() == 1); - smart_refctd_ptr source = IAsset::castDown(assets[0]); + smart_refctd_ptr source = IAsset::castDown(assets[0]); smart_refctd_ptr introspection; { @@ -274,8 +265,8 @@ class DeviceSelectionAndSharedSourcesApp final : public application_templates::M // The Shader Asset Loaders deduce the stage from the file extension, // if the extension is generic (.glsl or .hlsl) the stage is unknown. // But it can still be overriden from within the source with a `#pragma shader_stage` - options.stage = shaderStage == IShader::E_SHADER_STAGE::ESS_COMPUTE ? shaderStage : IShader::E_SHADER_STAGE::ESS_VERTEX; // TODO: do smth with it - options.preprocessorOptions.targetSpirvVersion = m_device->getPhysicalDevice()->getLimits().spirvVersion; + options.stage = source->getStage() == IShader::E_SHADER_STAGE::ESS_COMPUTE ? source->getStage() : IShader::E_SHADER_STAGE::ESS_VERTEX; // TODO: do smth with it + options.targetSpirvVersion = m_device->getPhysicalDevice()->getLimits().spirvVersion; // we need to perform an unoptimized compilation with source debug info or we'll lose names of variable sin the introspection options.spirvOptimizer = nullptr; options.debugInfoFlags |= IShaderCompiler::E_DEBUG_INFO_FLAGS::EDIF_SOURCE_BIT; @@ -286,7 +277,7 @@ class DeviceSelectionAndSharedSourcesApp final : public application_templates::M options.preprocessorOptions.includeFinder = compilerSet->getShaderCompiler(source->getContentType())->getDefaultIncludeFinder(); auto spirvUnspecialized = compilerSet->compileToSPIRV(source.get(), options); - const CSPIRVIntrospector::CStageIntrospectionData::SParams inspctParams = { .entryPoint = "main", .shader = spirvUnspecialized, .stage = shaderStage }; + const CSPIRVIntrospector::CStageIntrospectionData::SParams inspctParams = { .entryPoint = "main", .shader = spirvUnspecialized }; introspection = introspector.introspect(inspctParams); introspection->debugPrint(m_logger.get()); diff --git a/05_StreamingAndBufferDeviceAddressApp/CMakeLists.txt b/05_StreamingAndBufferDeviceAddressApp/CMakeLists.txt index 6e90f86cb..a434ff32a 100644 --- a/05_StreamingAndBufferDeviceAddressApp/CMakeLists.txt +++ b/05_StreamingAndBufferDeviceAddressApp/CMakeLists.txt @@ -21,42 +21,4 @@ if(NBL_EMBED_BUILTIN_RESOURCES) ADD_CUSTOM_BUILTIN_RESOURCES(${_BR_TARGET_} RESOURCES_TO_EMBED "${_SEARCH_DIRECTORIES_}" "${RESOURCE_DIR}" "nbl::this_example::builtin" "${_OUTPUT_DIRECTORY_HEADER_}" "${_OUTPUT_DIRECTORY_SOURCE_}") LINK_BUILTIN_RESOURCES_TO_TARGET(${EXECUTABLE_NAME} ${_BR_TARGET_}) -endif() - -set(OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/auto-gen") - -set(SM 6_8) -set(JSON [=[ -[ - { - "INPUT": "app_resources/shader.comp.hlsl", - "KEY": "shader", - } -] -]=]) -string(CONFIGURE "${JSON}" JSON) - -set(COMPILE_OPTIONS - -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} -) +endif() \ No newline at end of file diff --git a/05_StreamingAndBufferDeviceAddressApp/app_resources/common.hlsl b/05_StreamingAndBufferDeviceAddressApp/app_resources/common.hlsl index 1f0557d89..6f339aa13 100644 --- a/05_StreamingAndBufferDeviceAddressApp/app_resources/common.hlsl +++ b/05_StreamingAndBufferDeviceAddressApp/app_resources/common.hlsl @@ -1,8 +1,9 @@ #include "nbl/builtin/hlsl/cpp_compat.hlsl" -// -using input_t = nbl::hlsl::float32_t3; -using output_t = nbl::hlsl::float32_t; +// Unfortunately not every piece of C++14 metaprogramming syntax is available in HLSL 202x +// https://github.com/microsoft/DirectXShaderCompiler/issues/5751#issuecomment-1800847954 +typedef nbl::hlsl::float32_t3 input_t; +typedef nbl::hlsl::float32_t output_t; NBL_CONSTEXPR_STATIC_INLINE uint32_t MaxPossibleElementCount = 1 << 20; diff --git a/05_StreamingAndBufferDeviceAddressApp/app_resources/shader.comp.hlsl b/05_StreamingAndBufferDeviceAddressApp/app_resources/shader.comp.hlsl index 31c60aefd..4aeef0e0f 100644 --- a/05_StreamingAndBufferDeviceAddressApp/app_resources/shader.comp.hlsl +++ b/05_StreamingAndBufferDeviceAddressApp/app_resources/shader.comp.hlsl @@ -1,13 +1,15 @@ #include "common.hlsl" +// just a small test +#include "nbl/builtin/hlsl/jit/device_capabilities.hlsl" + [[vk::push_constant]] PushConstantData pushConstants; // does absolutely nothing, a later example will show how it gets used -template +template void dummyTraitTest() {} [numthreads(WorkgroupSize,1,1)] -[shader("compute")] void main(uint32_t3 ID : SV_DispatchThreadID) { dummyTraitTest(); diff --git a/05_StreamingAndBufferDeviceAddressApp/main.cpp b/05_StreamingAndBufferDeviceAddressApp/main.cpp index ab0984a07..e8f7dbd33 100644 --- a/05_StreamingAndBufferDeviceAddressApp/main.cpp +++ b/05_StreamingAndBufferDeviceAddressApp/main.cpp @@ -5,8 +5,7 @@ // I've moved out a tiny part of this example into a shared header for reuse, please open and read it. #include "nbl/application_templates/MonoDeviceApplication.hpp" -#include "nbl/examples/common/BuiltinResourcesApplication.hpp" -#include "nbl/this_example/builtin/build/spirv/keys.hpp" +#include "nbl/application_templates/MonoAssetManagerAndBuiltinResourceApplication.hpp" using namespace nbl; @@ -21,10 +20,10 @@ using namespace video; // In this application we'll cover buffer streaming, Buffer Device Address (BDA) and push constants -class StreamingAndBufferDeviceAddressApp final : public application_templates::MonoDeviceApplication, public examples::BuiltinResourcesApplication +class StreamingAndBufferDeviceAddressApp final : public application_templates::MonoDeviceApplication, public application_templates::MonoAssetManagerAndBuiltinResourceApplication { using device_base_t = application_templates::MonoDeviceApplication; - using asset_base_t = examples::BuiltinResourcesApplication; + using asset_base_t = application_templates::MonoAssetManagerAndBuiltinResourceApplication; // This is the first example that submits multiple workloads in-flight. // What the shader does is it computes the minimum distance of each point against K other random input points. @@ -92,21 +91,25 @@ class StreamingAndBufferDeviceAddressApp final : public application_templates::M return false; // this time we load a shader directly from a file - smart_refctd_ptr shader; + smart_refctd_ptr shader; { IAssetLoader::SAssetLoadParams lp = {}; lp.logger = m_logger.get(); - lp.workingDirectory = "app_resources"; // virtual root - - auto key = nbl::this_example::builtin::build::get_spirv_key<"shader">(m_device.get()); - auto assetBundle = m_assetMgr->getAsset(key.data(), lp); + lp.workingDirectory = ""; // virtual root + auto assetBundle = m_assetMgr->getAsset("app_resources/shader.comp.hlsl",lp); const auto assets = assetBundle.getContents(); if (assets.empty()) return logFail("Could not load shader!"); - shader = IAsset::castDown(assets[0]); + // lets go straight from ICPUSpecializedShader to IGPUSpecializedShader + auto source = IAsset::castDown(assets[0]); // The down-cast should not fail! - assert(shader); + assert(source); + + // this time we skip the use of the asset converter since the ICPUShader->IGPUShader path is quick and simple + shader = m_device->createShader(source.get()); + if (!shader) + return logFail("Creation of a GPU Shader to from CPU Shader source failed!"); } // The StreamingTransientDataBuffers are actually composed on top of another useful utility called `CAsyncSingleBufferSubAllocator` @@ -114,8 +117,8 @@ class StreamingAndBufferDeviceAddressApp final : public application_templates::M // `CAsyncSingleBufferSubAllocator` just allows you suballocate subranges of any `IGPUBuffer` range with deferred/latched frees. constexpr uint32_t DownstreamBufferSize = sizeof(output_t)<<23; constexpr uint32_t UpstreamBufferSize = sizeof(input_t)<<23; - - m_utils = IUtilities::create(smart_refctd_ptr(m_device),smart_refctd_ptr(m_logger),DownstreamBufferSize,UpstreamBufferSize); + + m_utils = make_smart_refctd_ptr(smart_refctd_ptr(m_device),smart_refctd_ptr(m_logger),DownstreamBufferSize,UpstreamBufferSize); if (!m_utils) return logFail("Failed to create Utilities!"); m_upStreamingBuffer = m_utils->getDefaultUpStreamingBuffer(); @@ -136,7 +139,6 @@ class StreamingAndBufferDeviceAddressApp final : public application_templates::M IGPUComputePipeline::SCreationParams params = {}; params.layout = layout.get(); params.shader.shader = shader.get(); - params.shader.entryPoint = "main"; if (!m_device->createComputePipelines(nullptr,{¶ms,1},&m_pipeline)) return logFail("Failed to create compute pipeline!\n"); } diff --git a/06_HelloGraphicsQueue/main.cpp b/06_HelloGraphicsQueue/main.cpp index 07d6affd3..a5e9bb7e7 100644 --- a/06_HelloGraphicsQueue/main.cpp +++ b/06_HelloGraphicsQueue/main.cpp @@ -3,20 +3,18 @@ // For conditions of distribution and use, see copyright notice in nabla.h -#include "nbl/examples/examples.hpp" +// I've moved out a tiny part of this example into a shared header for reuse, please open and read it. +#include "nbl/application_templates/MonoDeviceApplication.hpp" +#include "nbl/application_templates/MonoAssetManagerAndBuiltinResourceApplication.hpp" #include "nbl/ext/ScreenShot/ScreenShot.h" 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; - +using namespace core; +using namespace system; +using namespace asset; +using namespace video; // Here we showcase the use of Graphics Queue only // Steps we take in this example: @@ -28,10 +26,10 @@ using namespace nbl::examples; // - save the smallImg to disk // // all without using IUtilities. -class HelloGraphicsQueueApp final : public application_templates::MonoDeviceApplication, public BuiltinResourcesApplication +class HelloGraphicsQueueApp final : public application_templates::MonoDeviceApplication, public application_templates::MonoAssetManagerAndBuiltinResourceApplication { using device_base_t = application_templates::MonoDeviceApplication; - using asset_base_t = BuiltinResourcesApplication; + using asset_base_t = application_templates::MonoAssetManagerAndBuiltinResourceApplication; public: // Yay thanks to multiple inheritance we cannot forward ctors anymore. @@ -285,9 +283,9 @@ class HelloGraphicsQueueApp final : public application_templates::MonoDeviceAppl // which also means it can be sparsely(with gaps) specified. params.image = ICPUImage::create(ouputImageCreationParams); { - // null_memory_resource is used for creating ICPUBuffer over an already existing memory, without any memcopy operations - // or taking over the memory ownership. null_memory_resource cannot free its memory. - auto cpuOutputImageBuffer = ICPUBuffer::create({ { smallImgByteSize }, imageBufferMemPtr, core::getNullMemoryResource() }, core::adopt_memory_t()); + // CDummyCPUBuffer is used for creating ICPUBuffer over an already existing memory, without any memcopy operations + // or taking over the memory ownership. CDummyCPUBuffer cannot free its memory. + auto cpuOutputImageBuffer = core::make_smart_refctd_ptr(smallImgByteSize, imageBufferMemPtr, core::adopt_memory_t()); ICPUImage::SBufferCopy region = {}; region.imageSubresource.aspectMask = IImage::E_ASPECT_FLAGS::EAF_COLOR_BIT; region.imageSubresource.layerCount = 1; diff --git a/06_MeshLoaders/CMakeLists.txt b/06_MeshLoaders/CMakeLists.txt new file mode 100644 index 000000000..2f9218f93 --- /dev/null +++ b/06_MeshLoaders/CMakeLists.txt @@ -0,0 +1,6 @@ +include(common RESULT_VARIABLE RES) +if(NOT RES) + message(FATAL_ERROR "common.cmake not found. Should be in {repo_root}/cmake directory") +endif() + +nbl_create_executable_project("" "" "" "" "${NBL_EXECUTABLE_PROJECT_CREATION_PCH_TARGET}") \ No newline at end of file diff --git a/23_Arithmetic2UnitTest/config.json.template b/06_MeshLoaders/config.json.template similarity index 100% rename from 23_Arithmetic2UnitTest/config.json.template rename to 06_MeshLoaders/config.json.template diff --git a/06_MeshLoaders/main.cpp b/06_MeshLoaders/main.cpp new file mode 100644 index 000000000..75135c033 --- /dev/null +++ b/06_MeshLoaders/main.cpp @@ -0,0 +1,563 @@ +// 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 + +#define _NBL_STATIC_LIB_ +#include +#include +#include + +#include "CCamera.hpp" +#include "../common/CommonAPI.h" +#include "nbl/ext/ScreenShot/ScreenShot.h" + +using namespace nbl; +using namespace core; +using namespace ui; +/* + Uncomment for more detailed logging +*/ + +// #define NBL_MORE_LOGS + +class MeshLoadersApp : public ApplicationBase +{ + constexpr static uint32_t WIN_W = 1280; + constexpr static uint32_t WIN_H = 720; + constexpr static uint32_t SC_IMG_COUNT = 3u; + constexpr static uint32_t FRAMES_IN_FLIGHT = 5u; + constexpr static uint64_t MAX_TIMEOUT = 99999999999999ull; + constexpr static size_t NBL_FRAMES_TO_AVERAGE = 100ull; + + static_assert(FRAMES_IN_FLIGHT > SC_IMG_COUNT); +public: + nbl::core::smart_refctd_ptr windowManager; + nbl::core::smart_refctd_ptr window; + nbl::core::smart_refctd_ptr windowCb; + nbl::core::smart_refctd_ptr apiConnection; + nbl::core::smart_refctd_ptr surface; + nbl::core::smart_refctd_ptr utilities; + nbl::core::smart_refctd_ptr logicalDevice; + nbl::video::IPhysicalDevice* physicalDevice; + std::array queues; + nbl::core::smart_refctd_ptr swapchain; + nbl::core::smart_refctd_ptr renderpass; + nbl::core::smart_refctd_dynamic_array> fbo; + std::array, CommonAPI::InitOutput::MaxFramesInFlight>, CommonAPI::InitOutput::MaxQueuesCount> commandPools; + nbl::core::smart_refctd_ptr system; + nbl::core::smart_refctd_ptr assetManager; + nbl::video::IGPUObjectFromAssetConverter::SParams cpu2gpuParams; + nbl::core::smart_refctd_ptr logger; + nbl::core::smart_refctd_ptr inputSystem; + + nbl::video::IGPUObjectFromAssetConverter cpu2gpu; + + video::IDeviceMemoryBacked::SDeviceMemoryRequirements ubomemreq; + core::smart_refctd_ptr gpuubo; + core::smart_refctd_ptr gpuds1; + + core::smart_refctd_ptr occlusionQueryPool; + core::smart_refctd_ptr timestampQueryPool; + + asset::ICPUMesh* meshRaw = nullptr; + const asset::COBJMetadata* metaOBJ = nullptr; + + core::smart_refctd_ptr frameComplete[FRAMES_IN_FLIGHT] = { nullptr }; + core::smart_refctd_ptr imageAcquire[FRAMES_IN_FLIGHT] = { nullptr }; + core::smart_refctd_ptr renderFinished[FRAMES_IN_FLIGHT] = { nullptr }; + core::smart_refctd_ptr commandBuffers[FRAMES_IN_FLIGHT]; + + CommonAPI::InputSystem::ChannelReader mouse; + CommonAPI::InputSystem::ChannelReader keyboard; + Camera camera = Camera(vectorSIMDf(0, 0, 0), vectorSIMDf(0, 0, 0), matrix4SIMD()); + + using RENDERPASS_INDEPENDENT_PIPELINE_ADRESS = size_t; + std::map> gpuPipelines; + core::smart_refctd_ptr gpumesh; + const asset::ICPUMeshBuffer* firstMeshBuffer; + const nbl::asset::COBJMetadata::CRenderpassIndependentPipeline* pipelineMetadata; + nbl::video::ISwapchain::SCreationParams m_swapchainCreationParams; + + uint32_t ds1UboBinding = 0; + int resourceIx; + uint32_t acquiredNextFBO = {}; + std::chrono::steady_clock::time_point lastTime; + bool frameDataFilled = false; + size_t frame_count = 0ull; + double time_sum = 0; + double dtList[NBL_FRAMES_TO_AVERAGE] = {}; + + video::CDumbPresentationOracle oracle; + + core::smart_refctd_ptr queryResultsBuffer; + + void setWindow(core::smart_refctd_ptr&& wnd) override + { + window = std::move(wnd); + } + void setSystem(core::smart_refctd_ptr&& s) override + { + system = std::move(s); + } + nbl::ui::IWindow* getWindow() override + { + return window.get(); + } + video::IAPIConnection* getAPIConnection() override + { + return apiConnection.get(); + } + video::ILogicalDevice* getLogicalDevice() override + { + return logicalDevice.get(); + } + video::IGPURenderpass* getRenderpass() override + { + return renderpass.get(); + } + void setSurface(core::smart_refctd_ptr&& s) override + { + surface = std::move(s); + } + void setFBOs(std::vector>& f) override + { + for (int i = 0; i < f.size(); i++) + { + fbo->begin()[i] = core::smart_refctd_ptr(f[i]); + } + } + void setSwapchain(core::smart_refctd_ptr&& s) override + { + swapchain = std::move(s); + } + uint32_t getSwapchainImageCount() override + { + return swapchain->getImageCount(); + } + virtual nbl::asset::E_FORMAT getDepthFormat() override + { + return nbl::asset::EF_D32_SFLOAT; + } + + void getAndLogQueryPoolResults() + { +#ifdef QUERY_POOL_LOGS + { + uint64_t samples_passed[4] = {}; + auto queryResultFlags = core::bitflag(video::IQueryPool::EQRF_WITH_AVAILABILITY_BIT) | video::IQueryPool::EQRF_64_BIT; + logicalDevice->getQueryPoolResults(occlusionQueryPool.get(), 0u, 2u, sizeof(samples_passed), samples_passed, sizeof(uint64_t) * 2, queryResultFlags); + logger->log("[AVAIL+64] SamplesPassed[0] = %d, SamplesPassed[1] = %d, Result Available = %d, %d", system::ILogger::ELL_INFO, samples_passed[0], samples_passed[2], samples_passed[1], samples_passed[3]); + } + { + uint64_t samples_passed[4] = {}; + auto queryResultFlags = core::bitflag(video::IQueryPool::EQRF_WITH_AVAILABILITY_BIT) | video::IQueryPool::EQRF_64_BIT | video::IQueryPool::EQRF_WAIT_BIT; + logicalDevice->getQueryPoolResults(occlusionQueryPool.get(), 0u, 2u, sizeof(samples_passed), samples_passed, sizeof(uint64_t) * 2, queryResultFlags); + logger->log("[WAIT+AVAIL+64] SamplesPassed[0] = %d, SamplesPassed[1] = %d, Result Available = %d, %d", system::ILogger::ELL_INFO, samples_passed[0], samples_passed[2], samples_passed[1], samples_passed[3]); + } + { + uint32_t samples_passed[2] = {}; + auto queryResultFlags = core::bitflag(video::IQueryPool::EQRF_WAIT_BIT); + logicalDevice->getQueryPoolResults(occlusionQueryPool.get(), 0u, 2u, sizeof(samples_passed), samples_passed, sizeof(uint32_t), queryResultFlags); + logger->log("[WAIT] SamplesPassed[0] = %d, SamplesPassed[1] = %d", system::ILogger::ELL_INFO, samples_passed[0], samples_passed[1]); + } + { + uint64_t timestamps[4] = {}; + auto queryResultFlags = core::bitflag(video::IQueryPool::EQRF_WAIT_BIT) | video::IQueryPool::EQRF_WITH_AVAILABILITY_BIT | video::IQueryPool::EQRF_64_BIT; + logicalDevice->getQueryPoolResults(timestampQueryPool.get(), 0u, 2u, sizeof(timestamps), timestamps, sizeof(uint64_t) * 2ull, queryResultFlags); + float timePassed = (timestamps[2] - timestamps[0]) * physicalDevice->getLimits().timestampPeriodInNanoSeconds; + logger->log("Time Passed (Seconds) = %f", system::ILogger::ELL_INFO, (timePassed * 1e-9)); + logger->log("Timestamps availablity: %d, %d", system::ILogger::ELL_INFO, timestamps[1], timestamps[3]); + } +#endif + } + + APP_CONSTRUCTOR(MeshLoadersApp) + void onAppInitialized_impl() override + { + const auto swapchainImageUsage = static_cast(asset::IImage::EUF_COLOR_ATTACHMENT_BIT | asset::IImage::EUF_TRANSFER_SRC_BIT); + CommonAPI::InitParams initParams; + initParams.window = core::smart_refctd_ptr(window); + initParams.apiType = video::EAT_VULKAN; + initParams.appName = { _NBL_APP_NAME_ }; + initParams.framesInFlight = FRAMES_IN_FLIGHT; + initParams.windowWidth = WIN_W; + initParams.windowHeight = WIN_H; + initParams.swapchainImageCount = SC_IMG_COUNT; + initParams.swapchainImageUsage = swapchainImageUsage; + initParams.depthFormat = nbl::asset::EF_D32_SFLOAT; + auto initOutput = CommonAPI::InitWithDefaultExt(std::move(initParams)); + + window = std::move(initParams.window); + windowCb = std::move(initParams.windowCb); + apiConnection = std::move(initOutput.apiConnection); + surface = std::move(initOutput.surface); + utilities = std::move(initOutput.utilities); + logicalDevice = std::move(initOutput.logicalDevice); + physicalDevice = initOutput.physicalDevice; + queues = std::move(initOutput.queues); + renderpass = std::move(initOutput.renderToSwapchainRenderpass); + commandPools = std::move(initOutput.commandPools); + system = std::move(initOutput.system); + assetManager = std::move(initOutput.assetManager); + cpu2gpuParams = std::move(initOutput.cpu2gpuParams); + logger = std::move(initOutput.logger); + inputSystem = std::move(initOutput.inputSystem); + m_swapchainCreationParams = std::move(initOutput.swapchainCreationParams); + + CommonAPI::createSwapchain(std::move(logicalDevice), m_swapchainCreationParams, WIN_W, WIN_H, swapchain); + assert(swapchain); + fbo = CommonAPI::createFBOWithSwapchainImages( + swapchain->getImageCount(), WIN_W, WIN_H, + logicalDevice, swapchain, renderpass, + nbl::asset::EF_D32_SFLOAT + ); + + // Occlusion Query + { + video::IQueryPool::SCreationParams queryPoolCreationParams = {}; + queryPoolCreationParams.queryType = video::IQueryPool::EQT_OCCLUSION; + queryPoolCreationParams.queryCount = 2u; + occlusionQueryPool = logicalDevice->createQueryPool(std::move(queryPoolCreationParams)); + } + + // Timestamp Query + video::IQueryPool::SCreationParams queryPoolCreationParams = {}; + { + video::IQueryPool::SCreationParams queryPoolCreationParams = {}; + queryPoolCreationParams.queryType = video::IQueryPool::EQT_TIMESTAMP; + queryPoolCreationParams.queryCount = 2u; + timestampQueryPool = logicalDevice->createQueryPool(std::move(queryPoolCreationParams)); + } + + { + // SAMPLES_PASSED_0 + AVAILABILIY_0 + SAMPLES_PASSED_1 + AVAILABILIY_1 (uint32_t) + const size_t queriesSize = sizeof(uint32_t) * 4; + video::IGPUBuffer::SCreationParams gpuuboCreationParams; + gpuuboCreationParams.size = queriesSize; + gpuuboCreationParams.usage = core::bitflag(asset::IBuffer::EUF_UNIFORM_BUFFER_BIT)|asset::IBuffer::EUF_TRANSFER_DST_BIT|asset::IBuffer::EUF_INLINE_UPDATE_VIA_CMDBUF; + gpuuboCreationParams.queueFamilyIndexCount = 0u; + gpuuboCreationParams.queueFamilyIndices = nullptr; + + queryResultsBuffer = logicalDevice->createBuffer(std::move(gpuuboCreationParams)); + auto memReqs = queryResultsBuffer->getMemoryReqs(); + memReqs.memoryTypeBits &= physicalDevice->getDeviceLocalMemoryTypeBits(); + auto queriesMem = logicalDevice->allocate(memReqs, queryResultsBuffer.get()); + + queryResultsBuffer->setObjectDebugName("QueryResults"); + } + + nbl::video::IGPUObjectFromAssetConverter cpu2gpu; + { + auto* quantNormalCache = assetManager->getMeshManipulator()->getQuantNormalCache(); + quantNormalCache->loadCacheFromFile(system.get(), sharedOutputCWD / "normalCache101010.sse"); + + system::path archPath = sharedInputCWD / "sponza.zip"; + auto arch = system->openFileArchive(archPath); + // test no alias loading (TODO: fix loading from absolute paths) + system->mount(std::move(arch)); + asset::IAssetLoader::SAssetLoadParams loadParams; + loadParams.workingDirectory = sharedInputCWD; + loadParams.logger = logger.get(); + auto meshes_bundle = assetManager->getAsset((sharedInputCWD / "sponza.zip/sponza.obj").string(), loadParams); + assert(!meshes_bundle.getContents().empty()); + + metaOBJ = meshes_bundle.getMetadata()->selfCast(); + + auto cpuMesh = meshes_bundle.getContents().begin()[0]; + meshRaw = static_cast(cpuMesh.get()); + + quantNormalCache->saveCacheToFile(system.get(), sharedOutputCWD / "normalCache101010.sse"); + } + + // Fix FrontFace and BlendParams for meshBuffers + for (size_t i = 0ull; i < meshRaw->getMeshBuffers().size(); ++i) + { + auto& meshBuffer = meshRaw->getMeshBuffers().begin()[i]; + meshBuffer->getPipeline()->getRasterizationParams().frontFaceIsCCW = false; + } + + // we can safely assume that all meshbuffers within mesh loaded from OBJ has same DS1 layout (used for camera-specific data) + firstMeshBuffer = *meshRaw->getMeshBuffers().begin(); + pipelineMetadata = metaOBJ->getAssetSpecificMetadata(firstMeshBuffer->getPipeline()); + + // so we can create just one DS + const asset::ICPUDescriptorSetLayout* ds1layout = firstMeshBuffer->getPipeline()->getLayout()->getDescriptorSetLayout(1u); + ds1UboBinding = ds1layout->getDescriptorRedirect(asset::IDescriptor::E_TYPE::ET_UNIFORM_BUFFER).getBinding(asset::ICPUDescriptorSetLayout::CBindingRedirect::storage_range_index_t{ 0 }).data; + + size_t neededDS1UBOsz = 0ull; + { + for (const auto& shdrIn : pipelineMetadata->m_inputSemantics) + if (shdrIn.descriptorSection.type == asset::IRenderpassIndependentPipelineMetadata::ShaderInput::E_TYPE::ET_UNIFORM_BUFFER && shdrIn.descriptorSection.uniformBufferObject.set == 1u && shdrIn.descriptorSection.uniformBufferObject.binding == ds1UboBinding) + neededDS1UBOsz = std::max(neededDS1UBOsz, shdrIn.descriptorSection.uniformBufferObject.relByteoffset + shdrIn.descriptorSection.uniformBufferObject.bytesize); + } + + core::smart_refctd_ptr gpuds1layout; + { + auto gpu_array = cpu2gpu.getGPUObjectsFromAssets(&ds1layout, &ds1layout + 1, cpu2gpuParams); + if (!gpu_array || gpu_array->size() < 1u || !(*gpu_array)[0]) + assert(false); + + gpuds1layout = (*gpu_array)[0]; + } + + core::smart_refctd_ptr descriptorPool = nullptr; + { + video::IDescriptorPool::SCreateInfo createInfo = {}; + createInfo.maxSets = 1u; + createInfo.maxDescriptorCount[static_cast(asset::IDescriptor::E_TYPE::ET_UNIFORM_BUFFER)] = 1u; + descriptorPool = logicalDevice->createDescriptorPool(std::move(createInfo)); + } + + video::IGPUBuffer::SCreationParams gpuuboCreationParams; + gpuuboCreationParams.size = neededDS1UBOsz; + gpuuboCreationParams.usage = core::bitflag(asset::IBuffer::EUF_UNIFORM_BUFFER_BIT) | asset::IBuffer::EUF_TRANSFER_DST_BIT | asset::IBuffer::EUF_INLINE_UPDATE_VIA_CMDBUF; + gpuuboCreationParams.queueFamilyIndexCount = 0u; + gpuuboCreationParams.queueFamilyIndices = nullptr; + + gpuubo = logicalDevice->createBuffer(std::move(gpuuboCreationParams)); + auto gpuuboMemReqs = gpuubo->getMemoryReqs(); + gpuuboMemReqs.memoryTypeBits &= physicalDevice->getDeviceLocalMemoryTypeBits(); + auto uboMemoryOffset = logicalDevice->allocate(gpuuboMemReqs, gpuubo.get(), video::IDeviceMemoryAllocation::E_MEMORY_ALLOCATE_FLAGS::EMAF_NONE); + + gpuds1 = descriptorPool->createDescriptorSet(std::move(gpuds1layout)); + + { + video::IGPUDescriptorSet::SWriteDescriptorSet write; + write.dstSet = gpuds1.get(); + write.binding = ds1UboBinding; + write.count = 1u; + write.arrayElement = 0u; + write.descriptorType = asset::IDescriptor::E_TYPE::ET_UNIFORM_BUFFER; + video::IGPUDescriptorSet::SDescriptorInfo info; + { + info.desc = gpuubo; + info.info.buffer.offset = 0ull; + info.info.buffer.size = neededDS1UBOsz; + } + write.info = &info; + logicalDevice->updateDescriptorSets(1u, &write, 0u, nullptr); + } + { + cpu2gpuParams.beginCommandBuffers(); + + auto gpu_array = cpu2gpu.getGPUObjectsFromAssets(&meshRaw, &meshRaw + 1, cpu2gpuParams); + if (!gpu_array || gpu_array->size() < 1u || !(*gpu_array)[0]) + assert(false); + + cpu2gpuParams.waitForCreationToComplete(false); + + gpumesh = (*gpu_array)[0]; + } + + { + for (size_t i = 0; i < gpumesh->getMeshBuffers().size(); ++i) + { + auto gpuIndependentPipeline = gpumesh->getMeshBuffers().begin()[i]->getPipeline(); + + nbl::video::IGPUGraphicsPipeline::SCreationParams graphicsPipelineParams; + graphicsPipelineParams.renderpassIndependent = core::smart_refctd_ptr(const_cast(gpuIndependentPipeline)); + graphicsPipelineParams.renderpass = core::smart_refctd_ptr(renderpass); + + const RENDERPASS_INDEPENDENT_PIPELINE_ADRESS adress = reinterpret_cast(graphicsPipelineParams.renderpassIndependent.get()); + gpuPipelines[adress] = logicalDevice->createGraphicsPipeline(nullptr, std::move(graphicsPipelineParams)); + } + } + + core::vectorSIMDf cameraPosition(-250.0f,177.0f,1.69f); + core::vectorSIMDf cameraTarget(50.0f,125.0f,-3.0f); + matrix4SIMD projectionMatrix = matrix4SIMD::buildProjectionMatrixPerspectiveFovLH(core::radians(60.0f), video::ISurface::getTransformedAspectRatio(swapchain->getPreTransform(), WIN_W, WIN_H), 0.1, 10000); + camera = Camera(cameraPosition, cameraTarget, projectionMatrix, 10.f, 1.f); + lastTime = std::chrono::steady_clock::now(); + + for (size_t i = 0ull; i < NBL_FRAMES_TO_AVERAGE; ++i) + dtList[i] = 0.0; + + oracle.reportBeginFrameRecord(); + + + const auto& graphicsCommandPools = commandPools[CommonAPI::InitOutput::EQT_GRAPHICS]; + for (uint32_t i = 0u; i < FRAMES_IN_FLIGHT; i++) + { + logicalDevice->createCommandBuffers(graphicsCommandPools[i].get(), video::IGPUCommandBuffer::EL_PRIMARY, 1, commandBuffers+i); + imageAcquire[i] = logicalDevice->createSemaphore(); + renderFinished[i] = logicalDevice->createSemaphore(); + } + + constexpr uint64_t MAX_TIMEOUT = 99999999999999ull; + uint32_t acquiredNextFBO = {}; + resourceIx = -1; + } + void onAppTerminated_impl() override + { + const auto& fboCreationParams = fbo->begin()[acquiredNextFBO]->getCreationParameters(); + auto gpuSourceImageView = fboCreationParams.attachments[0]; + + bool status = ext::ScreenShot::createScreenShot( + logicalDevice.get(), + queues[CommonAPI::InitOutput::EQT_TRANSFER_DOWN], + renderFinished[resourceIx].get(), + gpuSourceImageView.get(), + assetManager.get(), + "ScreenShot.png", + asset::IImage::EL_PRESENT_SRC, + asset::EAF_NONE); + + assert(status); + logicalDevice->waitIdle(); + } + void workLoopBody() override + { + ++resourceIx; + if (resourceIx >= FRAMES_IN_FLIGHT) + resourceIx = 0; + + auto& commandBuffer = commandBuffers[resourceIx]; + auto& fence = frameComplete[resourceIx]; + if (fence) + logicalDevice->blockForFences(1u, &fence.get()); + else + fence = logicalDevice->createFence(static_cast(0)); + + commandBuffer->reset(nbl::video::IGPUCommandBuffer::ERF_RELEASE_RESOURCES_BIT); + commandBuffer->begin(nbl::video::IGPUCommandBuffer::EU_NONE); + + const auto nextPresentationTimestamp = oracle.acquireNextImage(swapchain.get(), imageAcquire[resourceIx].get(), nullptr, &acquiredNextFBO); + { + 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); + } + + const auto& viewMatrix = camera.getViewMatrix(); + const auto& viewProjectionMatrix = matrix4SIMD::concatenateBFollowedByAPrecisely( + video::ISurface::getSurfaceTransformationMatrix(swapchain->getPreTransform()), + camera.getConcatenatedMatrix() + ); + + asset::SViewport viewport; + viewport.minDepth = 1.f; + viewport.maxDepth = 0.f; + viewport.x = 0u; + viewport.y = 0u; + viewport.width = WIN_W; + viewport.height = WIN_H; + commandBuffer->setViewport(0u, 1u, &viewport); + + VkRect2D scissor = {}; + scissor.offset = { 0, 0 }; + scissor.extent = { WIN_W, WIN_H }; + commandBuffer->setScissor(0u, 1u, &scissor); + + core::matrix3x4SIMD modelMatrix; + modelMatrix.setTranslation(nbl::core::vectorSIMDf(0, 0, 0, 0)); + core::matrix4SIMD mvp = core::concatenateBFollowedByA(viewProjectionMatrix, modelMatrix); + + const size_t uboSize = gpuubo->getSize(); + core::vector uboData(uboSize); + for (const auto& shdrIn : pipelineMetadata->m_inputSemantics) + { + if (shdrIn.descriptorSection.type == asset::IRenderpassIndependentPipelineMetadata::ShaderInput::E_TYPE::ET_UNIFORM_BUFFER && shdrIn.descriptorSection.uniformBufferObject.set == 1u && shdrIn.descriptorSection.uniformBufferObject.binding == ds1UboBinding) + { + switch (shdrIn.type) + { + case asset::IRenderpassIndependentPipelineMetadata::ECSI_WORLD_VIEW_PROJ: + { + memcpy(uboData.data() + shdrIn.descriptorSection.uniformBufferObject.relByteoffset, mvp.pointer(), shdrIn.descriptorSection.uniformBufferObject.bytesize); + } break; + + case asset::IRenderpassIndependentPipelineMetadata::ECSI_WORLD_VIEW: + { + memcpy(uboData.data() + shdrIn.descriptorSection.uniformBufferObject.relByteoffset, viewMatrix.pointer(), shdrIn.descriptorSection.uniformBufferObject.bytesize); + } break; + + case asset::IRenderpassIndependentPipelineMetadata::ECSI_WORLD_VIEW_INVERSE_TRANSPOSE: + { + memcpy(uboData.data() + shdrIn.descriptorSection.uniformBufferObject.relByteoffset, viewMatrix.pointer(), shdrIn.descriptorSection.uniformBufferObject.bytesize); + } break; + } + } + } + commandBuffer->updateBuffer(gpuubo.get(), 0ull, uboSize, uboData.data()); + + nbl::video::IGPUCommandBuffer::SRenderpassBeginInfo beginInfo; + { + VkRect2D area; + area.offset = { 0,0 }; + area.extent = { WIN_W, WIN_H }; + asset::SClearValue clear[2] = {}; + clear[0].color.float32[0] = 1.f; + clear[0].color.float32[1] = 1.f; + clear[0].color.float32[2] = 1.f; + clear[0].color.float32[3] = 1.f; + clear[1].depthStencil.depth = 0.f; + + beginInfo.clearValueCount = 2u; + beginInfo.framebuffer = fbo->begin()[acquiredNextFBO]; + beginInfo.renderpass = renderpass; + beginInfo.renderArea = area; + beginInfo.clearValues = clear; + } + + commandBuffer->resetQueryPool(occlusionQueryPool.get(), 0u, 2u); + commandBuffer->resetQueryPool(timestampQueryPool.get(), 0u, 2u); + commandBuffer->beginRenderPass(&beginInfo, nbl::asset::ESC_INLINE); + + commandBuffer->writeTimestamp(asset::E_PIPELINE_STAGE_FLAGS::EPSF_TOP_OF_PIPE_BIT, timestampQueryPool.get(), 0u); + for (size_t i = 0; i < gpumesh->getMeshBuffers().size(); ++i) + { + if(i < 2) + commandBuffer->beginQuery(occlusionQueryPool.get(), i); + auto gpuMeshBuffer = gpumesh->getMeshBuffers().begin()[i]; + auto gpuGraphicsPipeline = gpuPipelines[reinterpret_cast(gpuMeshBuffer->getPipeline())]; + + const video::IGPURenderpassIndependentPipeline* gpuRenderpassIndependentPipeline = gpuMeshBuffer->getPipeline(); + const video::IGPUDescriptorSet* ds3 = gpuMeshBuffer->getAttachedDescriptorSet(); + + commandBuffer->bindGraphicsPipeline(gpuGraphicsPipeline.get()); + + const video::IGPUDescriptorSet* gpuds1_ptr = gpuds1.get(); + commandBuffer->bindDescriptorSets(asset::EPBP_GRAPHICS, gpuRenderpassIndependentPipeline->getLayout(), 1u, 1u, &gpuds1_ptr); + const video::IGPUDescriptorSet* gpuds3_ptr = gpuMeshBuffer->getAttachedDescriptorSet(); + if (gpuds3_ptr) + commandBuffer->bindDescriptorSets(asset::EPBP_GRAPHICS, gpuRenderpassIndependentPipeline->getLayout(), 3u, 1u, &gpuds3_ptr); + commandBuffer->pushConstants(gpuRenderpassIndependentPipeline->getLayout(), asset::IShader::ESS_FRAGMENT, 0u, gpuMeshBuffer->MAX_PUSH_CONSTANT_BYTESIZE, gpuMeshBuffer->getPushConstantsDataPtr()); + + commandBuffer->drawMeshBuffer(gpuMeshBuffer); + + if(i < 2) + commandBuffer->endQuery(occlusionQueryPool.get(), i); + } + commandBuffer->writeTimestamp(asset::E_PIPELINE_STAGE_FLAGS::EPSF_BOTTOM_OF_PIPE_BIT, timestampQueryPool.get(), 1u); + + commandBuffer->endRenderPass(); + + auto queryResultFlags = core::bitflag(video::IQueryPool::EQRF_WAIT_BIT) | video::IQueryPool::EQRF_WITH_AVAILABILITY_BIT; + commandBuffer->copyQueryPoolResults(occlusionQueryPool.get(), 0, 2, queryResultsBuffer.get(), 0u, sizeof(uint32_t) * 2, queryResultFlags); + + commandBuffer->end(); + + logicalDevice->resetFences(1, &fence.get()); + CommonAPI::Submit( + logicalDevice.get(), + commandBuffer.get(), + queues[CommonAPI::InitOutput::EQT_COMPUTE], + imageAcquire[resourceIx].get(), + renderFinished[resourceIx].get(), + fence.get()); + CommonAPI::Present(logicalDevice.get(), + swapchain.get(), + queues[CommonAPI::InitOutput::EQT_GRAPHICS], renderFinished[resourceIx].get(), acquiredNextFBO); + + getAndLogQueryPoolResults(); + } + bool keepRunning() override + { + return windowCb->isWindowOpen(); + } +}; + +NBL_COMMON_API_MAIN(MeshLoadersApp) diff --git a/29_Arithmetic2Bench/pipeline.groovy b/06_MeshLoaders/pipeline.groovy similarity index 85% rename from 29_Arithmetic2Bench/pipeline.groovy rename to 06_MeshLoaders/pipeline.groovy index 7ea9947e0..0923d296f 100644 --- a/29_Arithmetic2Bench/pipeline.groovy +++ b/06_MeshLoaders/pipeline.groovy @@ -2,9 +2,9 @@ import org.DevshGraphicsProgramming.Agent import org.DevshGraphicsProgramming.BuilderInfo import org.DevshGraphicsProgramming.IBuilder -class CArithemticUnitTestBuilder extends IBuilder +class CMeshLoadersBuilder extends IBuilder { - public CArithemticUnitTestBuilder(Agent _agent, _info) + public CMeshLoadersBuilder(Agent _agent, _info) { super(_agent, _info) } @@ -44,7 +44,7 @@ class CArithemticUnitTestBuilder extends IBuilder def create(Agent _agent, _info) { - return new CArithemticUnitTestBuilder(_agent, _info) + return new CMeshLoadersBuilder(_agent, _info) } return this \ No newline at end of file diff --git a/07_StagingAndMultipleQueues/CMakeLists.txt b/07_StagingAndMultipleQueues/CMakeLists.txt index b5648de8f..a434ff32a 100644 --- a/07_StagingAndMultipleQueues/CMakeLists.txt +++ b/07_StagingAndMultipleQueues/CMakeLists.txt @@ -21,42 +21,4 @@ if(NBL_EMBED_BUILTIN_RESOURCES) ADD_CUSTOM_BUILTIN_RESOURCES(${_BR_TARGET_} RESOURCES_TO_EMBED "${_SEARCH_DIRECTORIES_}" "${RESOURCE_DIR}" "nbl::this_example::builtin" "${_OUTPUT_DIRECTORY_HEADER_}" "${_OUTPUT_DIRECTORY_SOURCE_}") LINK_BUILTIN_RESOURCES_TO_TARGET(${EXECUTABLE_NAME} ${_BR_TARGET_}) -endif() - -set(OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/auto-gen") - -set(SM 6_8) -set(JSON [=[ -[ - { - "INPUT": "app_resources/comp_shader.hlsl", - "KEY": "comp_shader", - } -] -]=]) -string(CONFIGURE "${JSON}" JSON) - -set(COMPILE_OPTIONS - -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} -) +endif() \ No newline at end of file diff --git a/07_StagingAndMultipleQueues/app_resources/common.hlsl b/07_StagingAndMultipleQueues/app_resources/common.hlsl index de15810c9..259d5069d 100644 --- a/07_StagingAndMultipleQueues/app_resources/common.hlsl +++ b/07_StagingAndMultipleQueues/app_resources/common.hlsl @@ -1,8 +1,8 @@ #include "nbl/builtin/hlsl/cpp_compat.hlsl" -NBL_CONSTEXPR_INLINE_NSPC_SCOPE_VAR uint32_t WorkgroupSizeX = 16; -NBL_CONSTEXPR_INLINE_NSPC_SCOPE_VAR uint32_t WorkgroupSizeY = 16; -NBL_CONSTEXPR_INLINE_NSPC_SCOPE_VAR uint32_t WorkgroupSize = WorkgroupSizeX*WorkgroupSizeY; +NBL_CONSTEXPR uint32_t WorkgroupSizeX = 16; +NBL_CONSTEXPR uint32_t WorkgroupSizeY = 16; +NBL_CONSTEXPR uint32_t WorkgroupSize = WorkgroupSizeX*WorkgroupSizeY; static const uint32_t FRAMES_IN_FLIGHT = 3u; diff --git a/07_StagingAndMultipleQueues/main.cpp b/07_StagingAndMultipleQueues/main.cpp index a850c1c47..658a28a35 100644 --- a/07_StagingAndMultipleQueues/main.cpp +++ b/07_StagingAndMultipleQueues/main.cpp @@ -3,25 +3,26 @@ // For conditions of distribution and use, see copyright notice in nabla.h // I've moved out a tiny part of this example into a shared header for reuse, please open and read it. -#include "nbl/examples/examples.hpp" -#include "nbl/this_example/builtin/build/spirv/keys.hpp" + +#include "nbl/application_templates/BasicMultiQueueApplication.hpp" +#include "nbl/application_templates/MonoAssetManagerAndBuiltinResourceApplication.hpp" + +// get asset converter +#include "CommonPCH/PCH.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; +using namespace core; +using namespace system; +using namespace asset; +using namespace video; #include "app_resources/common.hlsl" // This time we let the new base class score and pick queue families, as well as initialize `nbl::video::IUtilities` for us -class StagingAndMultipleQueuesApp final : public application_templates::BasicMultiQueueApplication, public BuiltinResourcesApplication +class StagingAndMultipleQueuesApp final : public application_templates::BasicMultiQueueApplication, public application_templates::MonoAssetManagerAndBuiltinResourceApplication { using device_base_t = application_templates::BasicMultiQueueApplication; - using asset_base_t = BuiltinResourcesApplication; + using asset_base_t = application_templates::MonoAssetManagerAndBuiltinResourceApplication; // TODO: would be cool if we used `system::ISystem::listItemsInDirectory(sharedInputCWD/"GLI")` as our dataset static constexpr std::array imagesToLoad = { @@ -190,7 +191,7 @@ class StagingAndMultipleQueuesApp final : public application_templates::BasicMul for (uint32_t imageIdx = 0; imageIdx < IMAGE_CNT; ++imageIdx) { const auto imagePathToLoad = imagesToLoad[imageIdx]; - auto cpuImage = loadImageAsset(imagePathToLoad); + auto cpuImage = loadFistAssetInBundle(imagePathToLoad); if (!cpuImage) logFailAndTerminate("Failed to load image from path %s",ILogger::ELL_ERROR,imagePathToLoad); @@ -245,7 +246,7 @@ class StagingAndMultipleQueuesApp final : public application_templates::BasicMul .binding = 0, .type = nbl::asset::IDescriptor::E_TYPE::ET_SAMPLED_IMAGE, .createFlags = IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - .stageFlags = IShader::E_SHADER_STAGE::ESS_COMPUTE, + .stageFlags = IGPUShader::E_SHADER_STAGE::ESS_COMPUTE, .count = 1, .immutableSamplers = nullptr }, @@ -253,7 +254,7 @@ class StagingAndMultipleQueuesApp final : public application_templates::BasicMul .binding = 1, .type = nbl::asset::IDescriptor::E_TYPE::ET_STORAGE_BUFFER, .createFlags = IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - .stageFlags = IShader::E_SHADER_STAGE::ESS_COMPUTE, + .stageFlags = IGPUShader::E_SHADER_STAGE::ESS_COMPUTE, .count = 1, .immutableSamplers = nullptr } @@ -280,10 +281,18 @@ class StagingAndMultipleQueuesApp final : public application_templates::BasicMul } // LOAD SHADER FROM FILE - smart_refctd_ptr shader = loadPreCompiledShader<"comp_shader">(); // "../app_resources/comp_shader.hlsl" + smart_refctd_ptr source; + { + source = loadFistAssetInBundle("../app_resources/comp_shader.hlsl"); + source->setShaderStage(IShader::E_SHADER_STAGE::ESS_COMPUTE); // can also be done via a #pragma in the shader + } + + if (!source) + logFailAndTerminate("Could not create a CPU shader!"); - if (!shader) - logFailAndTerminate("Could not load the precompiled shader!"); + core::smart_refctd_ptr shader = m_device->createShader(source.get()); + if(!shader) + logFailAndTerminate("Could not create a GPU shader!"); // CREATE COMPUTE PIPELINE SPushConstantRange pc[1]; @@ -423,16 +432,15 @@ class StagingAndMultipleQueuesApp final : public application_templates::BasicMul submitInfo[0].waitSemaphores = waitSemaphoreSubmitInfo; // there's no save to wait on, or need to prevent signal-after-submit because Renderdoc freezes because it // starts capturing immediately upon a submit and can't defer a capture till semaphores signal. - const bool isRunningInRenderdoc = m_api->runningInGraphicsDebugger()==IAPIConnection::EDebuggerType::Renderdoc; - if (imageToProcessIdisRunningInRenderdoc()) submitInfo[0].waitSemaphores = {waitSemaphoreSubmitInfo,1}; - if (isRunningInRenderdoc && imageToProcessId>=SUBMITS_IN_FLIGHT) + if (m_api->isRunningInRenderdoc() && imageToProcessId>=SUBMITS_IN_FLIGHT) for (auto old = histogramsSaved.load(); old < histogramSaveWaitSemaphoreValue; old = histogramsSaved.load()) histogramsSaved.wait(old); // Some Devices like all of the Intel GPUs do not have enough queues for us to allocate different queues to compute and transfers, // so our `BasicMultiQueueApplication` will "alias" a single queue to both usages. Normally you don't need to care, but here we're // attempting to do "out-of-order" "submit-before-signal" so we need to "hold back" submissions if the queues are aliased! - if (getTransferUpQueue()==computeQueue || isRunningInRenderdoc) + if (getTransferUpQueue()==computeQueue || m_api->isRunningInRenderdoc()) for (auto old = transfersSubmitted.load(); old <= imageToProcessId; old = transfersSubmitted.load()) transfersSubmitted.wait(old); computeQueue->submit(submitInfo); @@ -528,39 +536,21 @@ class StagingAndMultipleQueuesApp final : public application_templates::BasicMul return false; } - - core::smart_refctd_ptr loadImageAsset(const std::string& path) + + template + core::smart_refctd_ptr loadFistAssetInBundle(const std::string& path) { IAssetLoader::SAssetLoadParams lp; SAssetBundle bundle = m_assetMgr->getAsset(path, lp); if (bundle.getContents().empty()) - logFailAndTerminate("Couldn't load an image.",ILogger::ELL_ERROR); + logFailAndTerminate("Couldn't load an asset.",ILogger::ELL_ERROR); - auto asset = IAsset::castDown(bundle.getContents()[0]); + auto asset = IAsset::castDown(bundle.getContents()[0]); if (!asset) logFailAndTerminate("Incorrect asset loaded.",ILogger::ELL_ERROR); return asset; } - - template - core::smart_refctd_ptr loadPreCompiledShader() - { - IAssetLoader::SAssetLoadParams lp; - lp.logger = m_logger.get(); - lp.workingDirectory = "app_resources"; - - auto key = nbl::this_example::builtin::build::get_spirv_key(m_device.get()); - SAssetBundle bundle = m_assetMgr->getAsset(key.data(), lp); - if (bundle.getContents().empty()) - logFailAndTerminate("Couldn't load a shader.", ILogger::ELL_ERROR); - - auto asset = IAsset::castDown(bundle.getContents()[0]); - if (!asset) - logFailAndTerminate("Incorrect asset loaded.", ILogger::ELL_ERROR); - - return asset; - } }; NBL_MAIN_FUNC(StagingAndMultipleQueuesApp) diff --git a/08_HelloSwapchain/main.cpp b/08_HelloSwapchain/main.cpp index cd294b0d2..c17203a89 100644 --- a/08_HelloSwapchain/main.cpp +++ b/08_HelloSwapchain/main.cpp @@ -1,7 +1,7 @@ // Copyright (C) 2018-2024 - 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 "nbl/examples/examples.hpp" +#include "SimpleWindowedApplication.hpp" // #include "nbl/video/surface/CSurfaceVulkan.h" @@ -247,10 +247,14 @@ class HelloSwapchainApp final : public examples::SimpleWindowedApplication if (!m_surface || !m_surface->init(m_surface->pickQueue(m_device.get()),std::make_unique(),{})) return logFail("Failed to Create a Swapchain!"); + // When a swapchain gets recreated (resize or mode change) the number of images might change. + // So we need the maximum possible number of in-flight resources so we never have too little. + m_maxFramesInFlight = m_surface->getMaxFramesInFlight(); + // Normally you'd want to recreate these images whenever the swapchain is resized in some increment, like 64 pixels or something. // But I'm super lazy here and will just create "worst case sized images" and waste all the VRAM I can get. const auto dpyInfo = m_winMgr->getPrimaryDisplayInfo(); - for (auto i=0; icreateCommandPool(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))) + if (!pool || !pool->createCommandBuffers(IGPUCommandPool::BUFFER_LEVEL::PRIMARY,{m_cmdBufs.data(),m_maxFramesInFlight},core::smart_refctd_ptr(m_logger))) return logFail("Failed to Create CommandBuffers!"); // Help the CI a bit by providing a timeout option @@ -318,17 +322,13 @@ class HelloSwapchainApp final : public examples::SimpleWindowedApplication // We do a very simple thing, and just keep on clearing the swapchain image to red and present void workLoopBody() override { - // framesInFlight: ensuring safe execution of command buffers and acquires, `framesInFlight` only affect semaphore waits, don't use this to index your resources because it can change with swapchain recreation. - const uint32_t framesInFlight = core::min(MaxFramesInFlight, m_surface->getMaxAcquiresInFlight()); - // We block for semaphores for 2 reasons here: - // A) Resource: Can't use resource like a command buffer BEFORE previous use is finished! [MaxFramesInFlight] - // B) Acquire: Can't have more acquires in flight than a certain threshold returned by swapchain or your surface helper class. [MaxAcquiresInFlight] - if (m_realFrameIx>=framesInFlight) + // Can't reset a cmdbuffer before the previous use of commandbuffer is finished! + if (m_realFrameIx>=m_maxFramesInFlight) { const ISemaphore::SWaitInfo cmdbufDonePending[] = { { .semaphore = m_semaphore.get(), - .value = m_realFrameIx+1-framesInFlight + .value = m_realFrameIx+1-m_maxFramesInFlight } }; if (m_device->blockForSemaphores(cmdbufDonePending)!=ISemaphore::WAIT_RESULT::SUCCESS) @@ -343,7 +343,7 @@ class HelloSwapchainApp final : public examples::SimpleWindowedApplication const VkRect2D currentRenderArea = {.offset={0,0},.extent=currentSwapchainExtent}; // You explicitly should not use `getAcquireCount()` see the comment on `m_realFrameIx` - const auto resourceIx = m_realFrameIx%MaxFramesInFlight; + const auto resourceIx = m_realFrameIx%m_maxFramesInFlight; // We will be using this command buffer to produce the frame auto frame = m_tripleBuffers[resourceIx].get(); @@ -475,20 +475,20 @@ class HelloSwapchainApp final : public examples::SimpleWindowedApplication // 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 = 3u; // Use a separate counter to cycle through our resources because `getAcquireCount()` increases upon spontaneous resizes with immediate blit-presents - uint64_t m_realFrameIx = 0; + uint64_t m_realFrameIx : 59 = 0; + // Maximum frames which can be simultaneously rendered + uint64_t m_maxFramesInFlight : 5; // 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; + std::array m_blitWaitValues; // Enough Command Buffers and other resources for all frames in flight! - std::array,MaxFramesInFlight> m_cmdBufs; + std::array,ISwapchain::MaxImages> m_cmdBufs; // Our own persistent images that don't get recreated with the swapchain - std::array,MaxFramesInFlight> m_tripleBuffers; + std::array,ISwapchain::MaxImages> m_tripleBuffers; // Resources derived from the images - std::array,MaxFramesInFlight> m_framebuffers = {}; + std::array,ISwapchain::MaxImages> m_framebuffers = {}; }; // define an entry point as always! diff --git a/09_GeometryCreator/CMakeLists.txt b/09_GeometryCreator/CMakeLists.txt index 2dd253226..928ef5761 100644 --- a/09_GeometryCreator/CMakeLists.txt +++ b/09_GeometryCreator/CMakeLists.txt @@ -2,7 +2,5 @@ set(NBL_INCLUDE_SERACH_DIRECTORIES "${CMAKE_CURRENT_SOURCE_DIR}/include" ) - # 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_INCLUDE_SERACH_DIRECTORIES}" "" "") -# TODO: Arek temporarily disabled cause I haven't figured out how to make this target yet -# LINK_BUILTIN_RESOURCES_TO_TARGET(${EXECUTABLE_NAME} nblExamplesGeometrySpirvBRD) \ No newline at end of file +nbl_create_executable_project("" "" "${NBL_INCLUDE_SERACH_DIRECTORIES}" "" "${NBL_EXECUTABLE_PROJECT_CREATION_PCH_TARGET}") +LINK_BUILTIN_RESOURCES_TO_TARGET(${EXECUTABLE_NAME} geometryCreatorSpirvBRD) \ No newline at end of file diff --git a/09_GeometryCreator/include/common.hpp b/09_GeometryCreator/include/common.hpp index 84cd8118a..3661e5697 100644 --- a/09_GeometryCreator/include/common.hpp +++ b/09_GeometryCreator/include/common.hpp @@ -1,8 +1,20 @@ -#ifndef _NBL_THIS_EXAMPLE_COMMON_H_INCLUDED_ -#define _NBL_THIS_EXAMPLE_COMMON_H_INCLUDED_ +#ifndef __NBL_THIS_EXAMPLE_COMMON_H_INCLUDED__ +#define __NBL_THIS_EXAMPLE_COMMON_H_INCLUDED__ +#include +#include "nbl/asset/utils/CGeometryCreator.h" -#include "nbl/examples/examples.hpp" +#include "SimpleWindowedApplication.hpp" +#include "InputSystem.hpp" +#include "CEventCallback.hpp" + +#include "CCamera.hpp" +#include "SBasicViewParameters.hlsl" + +#include "geometry/creator/spirv/builtin/CArchive.h" +#include "geometry/creator/spirv/builtin/builtinResources.h" + +#include "CGeomtryCreatorScene.hpp" using namespace nbl; using namespace core; @@ -12,7 +24,6 @@ using namespace asset; using namespace ui; using namespace video; using namespace scene; -using namespace nbl::examples; - +using namespace geometrycreator; #endif // __NBL_THIS_EXAMPLE_COMMON_H_INCLUDED__ \ No newline at end of file diff --git a/09_GeometryCreator/main.cpp b/09_GeometryCreator/main.cpp index 6e34a9064..1c7fc0e21 100644 --- a/09_GeometryCreator/main.cpp +++ b/09_GeometryCreator/main.cpp @@ -1,25 +1,144 @@ -// Copyright (C) 2018-2025 - DevSH Graphics Programming Sp. z O.O. +// Copyright (C) 2018-2024 - 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 "common.hpp" -class GeometryCreatorApp final : public MonoWindowApplication, public BuiltinResourcesApplication +class CSwapchainFramebuffersAndDepth final : public nbl::video::CDefaultSwapchainFramebuffers +{ + using base_t = CDefaultSwapchainFramebuffers; + +public: + template + inline CSwapchainFramebuffersAndDepth(ILogicalDevice* device, const asset::E_FORMAT _desiredDepthFormat, Args&&... args) : CDefaultSwapchainFramebuffers(device, std::forward(args)...) + { + const IPhysicalDevice::SImageFormatPromotionRequest req = { + .originalFormat = _desiredDepthFormat, + .usages = {IGPUImage::EUF_RENDER_ATTACHMENT_BIT} + }; + m_depthFormat = m_device->getPhysicalDevice()->promoteImageFormat(req, IGPUImage::TILING::OPTIMAL); + + const static IGPURenderpass::SCreationParams::SDepthStencilAttachmentDescription depthAttachments[] = { + {{ + { + .format = m_depthFormat, + .samples = IGPUImage::ESCF_1_BIT, + .mayAlias = false + }, + /*.loadOp = */{IGPURenderpass::LOAD_OP::CLEAR}, + /*.storeOp = */{IGPURenderpass::STORE_OP::STORE}, + /*.initialLayout = */{IGPUImage::LAYOUT::UNDEFINED}, // because we clear we don't care about contents + /*.finalLayout = */{IGPUImage::LAYOUT::ATTACHMENT_OPTIMAL} // transition to presentation right away so we can skip a barrier + }}, + IGPURenderpass::SCreationParams::DepthStencilAttachmentsEnd + }; + m_params.depthStencilAttachments = depthAttachments; + + static IGPURenderpass::SCreationParams::SSubpassDescription subpasses[] = { + m_params.subpasses[0], + IGPURenderpass::SCreationParams::SubpassesEnd + }; + subpasses[0].depthStencilAttachment.render = { .attachmentIndex = 0,.layout = IGPUImage::LAYOUT::ATTACHMENT_OPTIMAL }; + m_params.subpasses = subpasses; + } + +protected: + inline bool onCreateSwapchain_impl(const uint8_t qFam) override + { + auto device = const_cast(m_renderpass->getOriginDevice()); + + const auto depthFormat = m_renderpass->getCreationParameters().depthStencilAttachments[0].format; + const auto& sharedParams = getSwapchain()->getCreationParameters().sharedParams; + auto image = device->createImage({ IImage::SCreationParams{ + .type = IGPUImage::ET_2D, + .samples = IGPUImage::ESCF_1_BIT, + .format = depthFormat, + .extent = {sharedParams.width,sharedParams.height,1}, + .mipLevels = 1, + .arrayLayers = 1, + .depthUsage = IGPUImage::EUF_RENDER_ATTACHMENT_BIT + } }); + + device->allocate(image->getMemoryReqs(), image.get()); + + m_depthBuffer = device->createImageView({ + .flags = IGPUImageView::ECF_NONE, + .subUsages = IGPUImage::EUF_RENDER_ATTACHMENT_BIT, + .image = std::move(image), + .viewType = IGPUImageView::ET_2D, + .format = depthFormat, + .subresourceRange = {IGPUImage::EAF_DEPTH_BIT,0,1,0,1} + }); + + const auto retval = base_t::onCreateSwapchain_impl(qFam); + m_depthBuffer = nullptr; + return retval; + } + + inline smart_refctd_ptr createFramebuffer(IGPUFramebuffer::SCreationParams&& params) override + { + params.depthStencilAttachments = &m_depthBuffer.get(); + return m_device->createFramebuffer(std::move(params)); + } + + E_FORMAT m_depthFormat; + // only used to pass a parameter from `onCreateSwapchain_impl` to `createFramebuffer` + smart_refctd_ptr m_depthBuffer; +}; + +class GeometryCreatorApp final : public examples::SimpleWindowedApplication { - using device_base_t = MonoWindowApplication; - using asset_base_t = BuiltinResourcesApplication; + using device_base_t = examples::SimpleWindowedApplication; + using clock_t = std::chrono::steady_clock; + + constexpr static inline uint32_t WIN_W = 1280, WIN_H = 720, SC_IMG_COUNT = 3u, FRAMES_IN_FLIGHT = 5u; + static_assert(FRAMES_IN_FLIGHT > SC_IMG_COUNT); + + constexpr static inline clock_t::duration DisplayImageDuration = std::chrono::milliseconds(900); public: - GeometryCreatorApp(const path& _localInputCWD, const path& _localOutputCWD, const path& _sharedInputCWD, const path& _sharedOutputCWD) - : IApplicationFramework(_localInputCWD, _localOutputCWD, _sharedInputCWD, _sharedOutputCWD), - device_base_t({1280,720}, EF_D16_UNORM, _localInputCWD, _localOutputCWD, _sharedInputCWD, _sharedOutputCWD) {} + inline GeometryCreatorApp(const path& _localInputCWD, const path& _localOutputCWD, const path& _sharedInputCWD, const path& _sharedOutputCWD) + : IApplicationFramework(_localInputCWD, _localOutputCWD, _sharedInputCWD, _sharedOutputCWD) {} + + virtual SPhysicalDeviceFeatures getRequiredDeviceFeatures() const override + { + auto retval = device_base_t::getRequiredDeviceFeatures(); + retval.geometryShader = true; + return retval; + } + + inline core::vector getSurfaces() const override + { + if (!m_surface) + { + { + auto windowCallback = core::make_smart_refctd_ptr(smart_refctd_ptr(m_inputSystem), smart_refctd_ptr(m_logger)); + IWindow::SCreationParams params = {}; + params.callback = core::make_smart_refctd_ptr(); + params.width = WIN_W; + params.height = WIN_H; + params.x = 32; + params.y = 32; + params.flags = ui::IWindow::ECF_HIDDEN | IWindow::ECF_BORDERLESS | IWindow::ECF_RESIZABLE; + params.windowCaption = "GeometryCreatorApp"; + params.callback = windowCallback; + 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) = nbl::video::CSimpleResizeSurface::create(std::move(surface)); + } + + if (m_surface) + return { {m_surface->getSurface()/*,EQF_NONE*/} }; + + return {}; + } inline bool onAppInitialized(smart_refctd_ptr&& system) override { - if (!asset_base_t::onAppInitialized(smart_refctd_ptr(system))) - return false; + m_inputSystem = make_smart_refctd_ptr(logger_opt_smart_ptr(smart_refctd_ptr(m_logger))); + if (!device_base_t::onAppInitialized(smart_refctd_ptr(system))) return false; @@ -27,8 +146,66 @@ class GeometryCreatorApp final : public MonoWindowApplication, public BuiltinRes 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; i < MaxFramesInFlight; i++) + ISwapchain::SCreationParams swapchainParams = { .surface = m_surface->getSurface() }; + if (!swapchainParams.deduceFormat(m_physicalDevice)) + return logFail("Could not choose a Surface Format for the Swapchain!"); + + // Subsequent submits don't wait for each other, hence its important to have External Dependencies which prevent users of the depth attachment overlapping. + const static IGPURenderpass::SCreationParams::SSubpassDependency dependencies[] = { + // wipe-transition of Color to ATTACHMENT_OPTIMAL + { + .srcSubpass = IGPURenderpass::SCreationParams::SSubpassDependency::External, + .dstSubpass = 0, + .memoryBarrier = { + // last place where the depth can get modified in previous frame + .srcStageMask = PIPELINE_STAGE_FLAGS::LATE_FRAGMENT_TESTS_BIT, + // only write ops, reads can't be made available + .srcAccessMask = ACCESS_FLAGS::DEPTH_STENCIL_ATTACHMENT_WRITE_BIT, + // destination needs to wait as early as possible + .dstStageMask = PIPELINE_STAGE_FLAGS::EARLY_FRAGMENT_TESTS_BIT, + // because of depth test needing a read and a write + .dstAccessMask = ACCESS_FLAGS::DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | ACCESS_FLAGS::DEPTH_STENCIL_ATTACHMENT_READ_BIT + } + // leave view offsets and flags default + }, + // color from ATTACHMENT_OPTIMAL to PRESENT_SRC + { + .srcSubpass = 0, + .dstSubpass = IGPURenderpass::SCreationParams::SSubpassDependency::External, + .memoryBarrier = { + // last place where the depth can get modified + .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 + }; + + // TODO: promote the depth format if D16 not supported, or quote the spec if there's guaranteed support for it + auto scResources = std::make_unique(m_device.get(), EF_D16_UNORM, swapchainParams.surfaceFormat.format, dependencies); + + auto* renderpass = scResources->getRenderpass(); + + if (!renderpass) + return logFail("Failed to create Renderpass!"); + + auto gQueue = getGraphicsQueue(); + if (!m_surface || !m_surface->init(gQueue, std::move(scResources), swapchainParams.sharedParams)) + return logFail("Could not create Window & Surface or initialize the Surface!"); + + m_maxFramesInFlight = m_surface->getMaxFramesInFlight(); + if (FRAMES_IN_FLIGHT < m_maxFramesInFlight) + { + m_logger->log("Lowering frames in flight!", ILogger::ELL_WARNING); + m_maxFramesInFlight = FRAMES_IN_FLIGHT; + } + + auto pool = m_device->createCommandPool(gQueue->getFamilyIndex(), IGPUCommandPool::CREATE_FLAGS::RESET_COMMAND_BUFFER_BIT); + + for (auto i = 0u; i < m_maxFramesInFlight; i++) { if (!pool) return logFail("Couldn't create Command Pool!"); @@ -36,58 +213,75 @@ class GeometryCreatorApp final : public MonoWindowApplication, public BuiltinRes 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 // we want to use the vertex data through UTBs - ); - - auto scRes = static_cast(m_surface->getSwapchainResources()); - const auto& geometries = m_scene->getInitParams().geometries; - m_renderer = CSimpleDebugRenderer::create(m_assetMgr.get(),scRes->getRenderpass(),0,{&geometries.front().get(),geometries.size()}); - if (!m_renderer || m_renderer->getGeometries().size() != geometries.size()) - return logFail("Could not create Renderer!"); - // special case + m_winMgr->setWindowSize(m_window.get(), WIN_W, WIN_H); + m_surface->recreateSwapchain(); + + auto assetManager = make_smart_refctd_ptr(smart_refctd_ptr(system)); + auto* geometry = assetManager->getGeometryCreator(); + + //using Builder = typename CScene::CreateResourcesDirectlyWithDevice::Builder; + using Builder = typename CScene::CreateResourcesWithAssetConverter::Builder; + auto oneRunCmd = CScene::createCommandBuffer(m_utils->getLogicalDevice(), m_utils->getLogger(), gQueue->getFamilyIndex()); + Builder builder(m_utils.get(), oneRunCmd.get(), m_logger.get(), geometry); + + // gpu resources + if (builder.build()) { - 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++; - } + if (!builder.finalize(resources, gQueue)) + m_logger->log("Could not finalize resource objects to gpu objects!", ILogger::ELL_ERROR); } - m_renderer->m_instances.resize(1); - m_renderer->m_instances[0].world = float32_t3x4( - float32_t4(1,0,0,0), - float32_t4(0,1,0,0), - float32_t4(0,0,1,0) - ); + else + m_logger->log("Could not build resource objects!", ILogger::ELL_ERROR); // 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); + matrix4SIMD projectionMatrix = matrix4SIMD::buildProjectionMatrixPerspectiveFovLH(core::radians(60.0f), float(WIN_W) / WIN_H, 0.1, 10000); camera = Camera(cameraPosition, cameraTarget, projectionMatrix, 1.069f, 0.4f); } - onAppInitializedFinish(); + m_winMgr->show(m_window.get()); + oracle.reportBeginFrameRecord(); + return true; } - inline IQueue::SSubmitInfo::SSemaphoreInfo renderFrame(const std::chrono::microseconds nextPresentationTimestamp) override + inline void workLoopBody() override { + const auto resourceIx = m_realFrameIx % m_maxFramesInFlight; + + if (m_realFrameIx >= m_maxFramesInFlight) + { + const ISemaphore::SWaitInfo cbDonePending[] = + { + { + .semaphore = m_semaphore.get(), + .value = m_realFrameIx + 1 - m_maxFramesInFlight + } + }; + if (m_device->blockForSemaphores(cbDonePending) != ISemaphore::WAIT_RESULT::SUCCESS) + return; + } + m_inputSystem->getDefaultMouse(&mouse); m_inputSystem->getDefaultKeyboard(&keyboard); - const auto resourceIx = m_realFrameIx % device_base_t::MaxFramesInFlight; + auto updatePresentationTimestamp = [&]() + { + m_currentImageAcquire = m_surface->acquireNextImage(); + + oracle.reportEndFrameRecord(); + const auto timestamp = oracle.getNextPresentationTimeStamp(); + oracle.reportBeginFrameRecord(); + + return timestamp; + }; + + const auto nextPresentationTimestamp = updatePresentationTimestamp(); + + if (!m_currentImageAcquire) + return; auto* const cb = m_cmdBufs.data()[resourceIx].get(); cb->reset(IGPUCommandBuffer::RESET_FLAGS::RELEASE_RESOURCES_BIT); @@ -98,8 +292,40 @@ class GeometryCreatorApp final : public MonoWindowApplication, public BuiltinRes 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); + + const auto type = static_cast(gcIndex); + const auto& [gpu, meta] = resources.objects[type]; + + object.meta.type = type; + object.meta.name = meta.name; } + const auto viewMatrix = camera.getViewMatrix(); + const auto viewProjectionMatrix = camera.getConcatenatedMatrix(); + + core::matrix3x4SIMD modelMatrix; + modelMatrix.setTranslation(nbl::core::vectorSIMDf(0, 0, 0, 0)); + modelMatrix.setRotation(quaternion(0, 0, 0)); + + core::matrix3x4SIMD modelViewMatrix = core::concatenateBFollowedByA(viewMatrix, modelMatrix); + core::matrix4SIMD modelViewProjectionMatrix = core::concatenateBFollowedByA(viewProjectionMatrix, modelMatrix); + + core::matrix3x4SIMD normalMatrix; + modelViewMatrix.getSub3x3InverseTranspose(normalMatrix); + + SBasicViewParameters uboData; + memcpy(uboData.MVP, modelViewProjectionMatrix.pointer(), sizeof(uboData.MVP)); + memcpy(uboData.MV, modelViewMatrix.pointer(), sizeof(uboData.MV)); + memcpy(uboData.NormalMat, normalMatrix.pointer(), sizeof(uboData.NormalMat)); + { + SBufferRange range; + range.buffer = core::smart_refctd_ptr(resources.ubo.buffer); + range.size = resources.ubo.buffer->getSize(); + + cb->updateBuffer(range, &uboData); + } + + auto* queue = getGraphicsQueue(); asset::SViewport viewport; { @@ -126,12 +352,12 @@ class GeometryCreatorApp final : public MonoWindowApplication, public BuiltinRes .extent = {m_window->getWidth(),m_window->getHeight()} }; - const IGPUCommandBuffer::SClearColorValue clearValue = { .float32 = {1.f,0.f,1.f,1.f} }; + const IGPUCommandBuffer::SClearColorValue clearValue = { .float32 = {0.f,0.f,0.f,1.f} }; const IGPUCommandBuffer::SClearDepthStencilValue depthValue = { .depth = 0.f }; auto scRes = static_cast(m_surface->getSwapchainResources()); const IGPUCommandBuffer::SRenderpassBeginInfo info = { - .framebuffer = scRes->getFramebuffer(device_base_t::getCurrentAcquire().imageIndex), + .framebuffer = scRes->getFramebuffer(m_currentImageAcquire.imageIndex), .colorClearValues = &clearValue, .depthStencilClearValues = &depthValue, .renderArea = currentRenderArea @@ -140,115 +366,113 @@ 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 viewParams = CSimpleDebugRenderer::SViewParams(viewMatrix,viewProjMatrix); + const auto& [hook, meta] = resources.objects[object.meta.type]; + auto* rawPipeline = hook.pipeline.get(); - // tear down scene every frame - m_renderer->m_instances[0].packedGeo = m_renderer->getGeometries().data()+gcIndex; - m_renderer->render(cb,viewParams); + SBufferBinding vertex = hook.bindings.vertex, index = hook.bindings.index; - cb->endRenderPass(); - cb->endDebugMarker(); - cb->end(); + cb->bindGraphicsPipeline(rawPipeline); + cb->bindDescriptorSets(EPBP_GRAPHICS, rawPipeline->getLayout(), 1, 1, &resources.descriptorSet.get()); + cb->bindVertexBuffers(0, 1, &vertex); - IQueue::SSubmitInfo::SSemaphoreInfo retval = + if (index.buffer && hook.indexType != EIT_UNKNOWN) { - .semaphore = m_semaphore.get(), - .value = ++m_realFrameIx, - .stageMask = PIPELINE_STAGE_FLAGS::ALL_GRAPHICS_BITS - }; - const IQueue::SSubmitInfo::SCommandBufferInfo commandBuffers[] = + cb->bindIndexBuffer(index, hook.indexType); + cb->drawIndexed(hook.indexCount, 1, 0, 0, 0); + } + else + cb->draw(hook.indexCount, 1, 0, 0); + + cb->endRenderPass(); + cb->end(); { - {.cmdbuf = cb } - }; - const IQueue::SSubmitInfo::SSemaphoreInfo acquired[] = { + const IQueue::SSubmitInfo::SSemaphoreInfo rendered[] = { - .semaphore = device_base_t::getCurrentAcquire().semaphore, - .value = device_base_t::getCurrentAcquire().acquireCount, - .stageMask = PIPELINE_STAGE_FLAGS::NONE - } - }; - const IQueue::SSubmitInfo infos[] = - { + { + .semaphore = m_semaphore.get(), + .value = ++m_realFrameIx, + .stageMask = PIPELINE_STAGE_FLAGS::ALL_GRAPHICS_BITS + } + }; { - .waitSemaphores = acquired, - .commandBuffers = commandBuffers, - .signalSemaphores = {&retval,1} + { + const IQueue::SSubmitInfo::SCommandBufferInfo commandBuffers[] = + { + {.cmdbuf = cb } + }; + + const IQueue::SSubmitInfo::SSemaphoreInfo acquired[] = + { + { + .semaphore = m_currentImageAcquire.semaphore, + .value = m_currentImageAcquire.acquireCount, + .stageMask = PIPELINE_STAGE_FLAGS::NONE + } + }; + const IQueue::SSubmitInfo infos[] = + { + { + .waitSemaphores = acquired, + .commandBuffers = commandBuffers, + .signalSemaphores = rendered + } + }; + + if (queue->submit(infos) == IQueue::RESULT::SUCCESS) + { + const nbl::video::ISemaphore::SWaitInfo waitInfos[] = + { { + .semaphore = m_semaphore.get(), + .value = m_realFrameIx + } }; + + m_device->blockForSemaphores(waitInfos); // this is not solution, quick wa to not throw validation errors + } + else + --m_realFrameIx; + } } - }; - if (getGraphicsQueue()->submit(infos) != IQueue::RESULT::SUCCESS) - { - retval.semaphore = nullptr; // so that we don't wait on semaphore that will never signal - m_realFrameIx--; + std::string caption = "[Nabla Engine] Geometry Creator"; + { + caption += ", displaying [" + std::string(object.meta.name.data()) + "]"; + m_window->setCaption(caption); + } + m_surface->present(m_currentImageAcquire.imageIndex, rendered); } + } - std::string caption = "[Nabla Engine] Geometry Creator"; - { - caption += ", displaying ["; - caption += m_scene->getInitParams().geometryNames[gcIndex]; - caption += "]"; - m_window->setCaption(caption); - } - return retval; + inline bool keepRunning() override + { + if (m_surface->irrecoverable()) + return false; + + return true; } - - protected: - const video::IGPURenderpass::SCreationParams::SSubpassDependency* getDefaultSubpassDependencies() const override + + inline bool onAppTerminated() override { - // Subsequent submits don't wait for each other, hence its important to have External Dependencies which prevent users of the depth attachment overlapping. - 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 - .srcStageMask = PIPELINE_STAGE_FLAGS::LATE_FRAGMENT_TESTS_BIT, - // don't want any writes to be available, we'll clear - .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 - }, - // color from ATTACHMENT_OPTIMAL to PRESENT_SRC - { - .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; + return device_base_t::onAppTerminated(); } private: - // - smart_refctd_ptr m_scene; - smart_refctd_ptr m_renderer; - // + smart_refctd_ptr m_window; + smart_refctd_ptr> m_surface; smart_refctd_ptr m_semaphore; - uint64_t m_realFrameIx = 0; - std::array,device_base_t::MaxFramesInFlight> m_cmdBufs; - // + uint64_t m_realFrameIx : 59 = 0; + uint64_t m_maxFramesInFlight : 5; + std::array, ISwapchain::MaxImages> m_cmdBufs; + ISimpleManagedSurface::SAcquireResult m_currentImageAcquire = {}; + + core::smart_refctd_ptr m_inputSystem; InputSystem::ChannelReader mouse; InputSystem::ChannelReader keyboard; - // - Camera camera = Camera(core::vectorSIMDf(0, 0, 0), core::vectorSIMDf(0, 0, 0), hlsl::float32_t4x4()); + Camera camera = Camera(core::vectorSIMDf(0, 0, 0), core::vectorSIMDf(0, 0, 0), core::matrix4SIMD()); + video::CDumbPresentationOracle oracle; + ResourcesBundle resources; + ObjectDrawHookCpu object; uint16_t gcIndex = {}; void mouseProcess(const nbl::ui::IMouseEventChannel::range_t& events) @@ -257,13 +481,10 @@ class GeometryCreatorApp final : public MonoWindowApplication, public BuiltinRes { auto ev = *eventIt; - if (ev.type==nbl::ui::SMouseEvent::EET_SCROLL && m_renderer) - { - gcIndex += int16_t(core::sign(ev.scrollEvent.verticalScroll)); - gcIndex = core::clamp(gcIndex,0ull,m_renderer->getGeometries().size()-1); - } + if (ev.type == nbl::ui::SMouseEvent::EET_SCROLL) + gcIndex = std::clamp(int16_t(gcIndex) + int16_t(core::sign(ev.scrollEvent.verticalScroll)), int64_t(0), int64_t(OT_COUNT - (uint8_t)1u)); } } }; -NBL_MAIN_FUNC(GeometryCreatorApp) +NBL_MAIN_FUNC(GeometryCreatorApp) \ No newline at end of file diff --git a/0_ImportanceSamplingEnvMaps/main.cpp b/0_ImportanceSamplingEnvMaps/main.cpp index 30a75355f..86f3608c3 100644 --- a/0_ImportanceSamplingEnvMaps/main.cpp +++ b/0_ImportanceSamplingEnvMaps/main.cpp @@ -25,7 +25,7 @@ static core::smart_refctd_ptr computeLuminancePdf(smart_refctd_ptr pdfDomainExtent = { envmapExtent.X, envmapExtent.Y }; const size_t outBufferSize = pdfDomainExtent.X * pdfDomainExtent.Y * sizeof(double); - core::smart_refctd_ptr outBuffer = ICPUBuffer::create({ outBufferSize }); + core::smart_refctd_ptr outBuffer = core::make_smart_refctd_ptr(outBufferSize); const double luminanceScales[4] = { 0.2126729 , 0.7151522, 0.0721750, 0.0 }; @@ -364,7 +364,7 @@ class ImportanceSamplingEnvMaps : public ApplicationBase // Create out image const size_t conditionalCdfBufferSize = pdfDomainExtent.X * pdfDomainExtent.Y * sizeof(double); - core::smart_refctd_ptr conditionalCdfBuffer = ICPUBuffer::create({ conditionalCdfBufferSize }); + core::smart_refctd_ptr conditionalCdfBuffer = core::make_smart_refctd_ptr(conditionalCdfBufferSize); memset(conditionalCdfBuffer->getPointer(), 0, conditionalCdfBufferSize); auto conditionalCdfImageParams = luminanceImage->getCreationParameters(); @@ -400,7 +400,7 @@ class ImportanceSamplingEnvMaps : public ApplicationBase // From the outImage you gotta extract integrals and normalize double* conditionalCdfPixel = (double*)conditionalCdfImage->getBuffer()->getPointer(); - conditionalIntegrals = ICPUBuffer::create({ pdfDomainExtent.Y * sizeof(double) }); + conditionalIntegrals = core::make_smart_refctd_ptr(pdfDomainExtent.Y * sizeof(double)); double* conditionalIntegralsPixel = (double*)conditionalIntegrals->getPointer(); for (uint32_t y = 0; y < pdfDomainExtent.Y; ++y) { @@ -449,7 +449,7 @@ class ImportanceSamplingEnvMaps : public ApplicationBase // Ouput: 1d cdf of conditionalIntegrals // Create out image - core::smart_refctd_ptr marginalCdfBuffer = ICPUBuffer::create({ conditionalIntegrals->getSize() }); + core::smart_refctd_ptr marginalCdfBuffer = core::make_smart_refctd_ptr(conditionalIntegrals->getSize()); memset(marginalCdfBuffer->getPointer(), 0, marginalCdfBuffer->getSize()); auto marginalCdfImageParams = inImage->getCreationParameters(); @@ -502,12 +502,12 @@ class ImportanceSamplingEnvMaps : public ApplicationBase const uint32_t phiPdfLUTChannelCount = 2u; // phi and pdf const size_t phiPdfLUTBufferSize = pdfDomainExtent.X * pdfDomainExtent.Y * phiPdfLUTChannelCount * sizeof(float); - core::smart_refctd_ptr phiPdfLUTBuffer = ICPUBuffer::create({ phiPdfLUTBufferSize }); + core::smart_refctd_ptr phiPdfLUTBuffer = core::make_smart_refctd_ptr(phiPdfLUTBufferSize); memset(phiPdfLUTBuffer->getPointer(), 0, phiPdfLUTBufferSize); const uint32_t thetaLUTChannelCount = 1u; // theta const size_t thetaLUTBufferSize = pdfDomainExtent.Y * thetaLUTChannelCount * sizeof(float); - core::smart_refctd_ptr thetaLUTBuffer = ICPUBuffer::create({ thetaLUTBufferSize }); + core::smart_refctd_ptr thetaLUTBuffer = core::make_smart_refctd_ptr(thetaLUTBufferSize); memset(thetaLUTBuffer->getPointer(), 0, thetaLUTBufferSize); float* phiPdfLUTPixel = (float*)phiPdfLUTBuffer->getPointer(); @@ -553,7 +553,7 @@ class ImportanceSamplingEnvMaps : public ApplicationBase const uint32_t MaxDimensions = 3u << kShaderParameters.MaxDepthLog2; const uint32_t MaxSamples = 1u << kShaderParameters.MaxSamplesLog2; - auto sampleSequence = core::make_smart_refctd_ptr(sizeof(uint32_t) * MaxDimensions * MaxSamples); core::OwenSampler sampler(MaxDimensions, 0xdeadbeefu); //core::SobolSampler sampler(MaxDimensions); @@ -640,7 +640,7 @@ class ImportanceSamplingEnvMaps : public ApplicationBase { auto cpuFragmentSpecializedShader = core::smart_refctd_ptr_static_cast(assetManager->getAsset(pathToShader, {}).getContents().begin()[0]); ISpecializedShader::SInfo info = cpuFragmentSpecializedShader->getSpecializationInfo(); - info.m_backingBuffer = ICPUBuffer::create({ sizeof(ShaderParameters) }); + info.m_backingBuffer = core::make_smart_refctd_ptr(sizeof(ShaderParameters)); memcpy(info.m_backingBuffer->getPointer(), &kShaderParameters, sizeof(ShaderParameters)); info.m_entries = core::make_refctd_dynamic_array>(2u); for (uint32_t i = 0; i < 2; i++) diff --git a/10_CountingSort/CMakeLists.txt b/10_CountingSort/CMakeLists.txt index 1c23744fe..b7cad41da 100644 --- a/10_CountingSort/CMakeLists.txt +++ b/10_CountingSort/CMakeLists.txt @@ -22,62 +22,3 @@ if(NBL_EMBED_BUILTIN_RESOURCES) LINK_BUILTIN_RESOURCES_TO_TARGET(${EXECUTABLE_NAME} ${_BR_TARGET_}) endif() - -set(OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/auto-gen") - -set(SM 6_8) -set(REQUIRED_CAPS [=[ - { - "kind": "limits", - "name": "maxComputeWorkGroupInvocations", - "type": "uint32_t", - "values": [256,512,1024] - }, - { - "kind": "limits", - "name": "maxComputeSharedMemorySize", - "type": "uint32_t", - "values": [16384, 32768, 65536] - } -]=]) - -set(JSON [=[ -[ - { - "INPUT": "app_resources/prefix_sum_shader.comp.hlsl", - "KEY": "prefix_sum_shader", - "CAPS": [${REQUIRED_CAPS}] - }, - { - "INPUT": "app_resources/scatter_shader.comp.hlsl", - "KEY": "scatter_shader", - "CAPS": [${REQUIRED_CAPS}] - } -] -]=]) -string(CONFIGURE "${JSON}" JSON) - -set(COMPILE_OPTIONS - -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} -) diff --git a/10_CountingSort/app_resources/common.hlsl b/10_CountingSort/app_resources/common.hlsl index 1074432b0..bcbf01727 100644 --- a/10_CountingSort/app_resources/common.hlsl +++ b/10_CountingSort/app_resources/common.hlsl @@ -22,10 +22,6 @@ using namespace nbl::hlsl; #ifdef __HLSL_VERSION #include "nbl/builtin/hlsl/bda/bda_accessor.hlsl" -static const uint32_t WorkgroupSize = DeviceConfigCaps::maxComputeWorkGroupInvocations; -static const uint32_t MaxBucketCount = (DeviceConfigCaps::maxComputeSharedMemorySize / sizeof(uint32_t)) / 2; -static const uint32_t BucketCount = (MaxBucketCount > 3000) ? 3000 : MaxBucketCount; - using Ptr = bda::__ptr; using PtrAccessor = BdaAccessor; @@ -58,8 +54,6 @@ uint32_t3 glsl::gl_WorkGroupSize() { return uint32_t3(WorkgroupSize, 1, 1); } - - #endif #endif \ No newline at end of file diff --git a/10_CountingSort/app_resources/prefix_sum_shader.comp.hlsl b/10_CountingSort/app_resources/prefix_sum_shader.comp.hlsl index b0301fc3f..1e5d2510e 100644 --- a/10_CountingSort/app_resources/prefix_sum_shader.comp.hlsl +++ b/10_CountingSort/app_resources/prefix_sum_shader.comp.hlsl @@ -4,7 +4,6 @@ [[vk::push_constant]] CountingPushData pushData; [numthreads(WorkgroupSize,1,1)] -[shader("compute")] void main(uint32_t3 ID : SV_GroupThreadID, uint32_t3 GroupID : SV_GroupID) { sort::CountingParameters < uint32_t > params; diff --git a/10_CountingSort/app_resources/scatter_shader.comp.hlsl b/10_CountingSort/app_resources/scatter_shader.comp.hlsl index ddecfca2b..fa502726f 100644 --- a/10_CountingSort/app_resources/scatter_shader.comp.hlsl +++ b/10_CountingSort/app_resources/scatter_shader.comp.hlsl @@ -6,7 +6,6 @@ using DoublePtrAccessor = DoubleBdaAccessor; [numthreads(WorkgroupSize, 1, 1)] -[shader("compute")] void main(uint32_t3 ID : SV_GroupThreadID, uint32_t3 GroupID : SV_GroupID) { sort::CountingParameters params; diff --git a/10_CountingSort/main.cpp b/10_CountingSort/main.cpp index a22647750..4d0c93516 100644 --- a/10_CountingSort/main.cpp +++ b/10_CountingSort/main.cpp @@ -1,22 +1,20 @@ -#include "nbl/examples/examples.hpp" -#include "nbl/this_example/builtin/build/spirv/keys.hpp" +#include "nbl/application_templates/MonoDeviceApplication.hpp" +#include "nbl/application_templates/MonoAssetManagerAndBuiltinResourceApplication.hpp" +#include "CommonPCH/PCH.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; +using namespace core; +using namespace system; +using namespace asset; +using namespace video; #include "app_resources/common.hlsl" #include "nbl/builtin/hlsl/bit.hlsl" -class CountingSortApp final : public application_templates::MonoDeviceApplication, public BuiltinResourcesApplication +class CountingSortApp final : public application_templates::MonoDeviceApplication, public application_templates::MonoAssetManagerAndBuiltinResourceApplication { using device_base_t = application_templates::MonoDeviceApplication; - using asset_base_t = BuiltinResourcesApplication; + using asset_base_t = application_templates::MonoAssetManagerAndBuiltinResourceApplication; public: // Yay thanks to multiple inheritance we cannot forward ctors anymore @@ -33,34 +31,19 @@ class CountingSortApp final : public application_templates::MonoDeviceApplicatio return false; auto limits = m_physicalDevice->getLimits(); - constexpr std::array AllowedMaxComputeSharedMemorySizes = { - 16384, 32768, 65536 - }; - - auto upperBoundSharedMemSize = std::upper_bound(AllowedMaxComputeSharedMemorySizes.begin(), AllowedMaxComputeSharedMemorySizes.end(), limits.maxComputeSharedMemorySize); - // devices which support less than 16KB of max compute shared memory size are not supported - if (upperBoundSharedMemSize == AllowedMaxComputeSharedMemorySizes.begin()) - { - m_logger->log("maxComputeSharedMemorySize is too low (%u)", ILogger::E_LOG_LEVEL::ELL_ERROR, limits.maxComputeSharedMemorySize); - exit(0); - } - - limits.maxComputeSharedMemorySize = *(upperBoundSharedMemSize - 1); - const uint32_t WorkgroupSize = limits.maxComputeWorkGroupInvocations; const uint32_t MaxBucketCount = (limits.maxComputeSharedMemorySize / sizeof(uint32_t)) / 2; constexpr uint32_t element_count = 100000; const uint32_t bucket_count = std::min((uint32_t)3000, MaxBucketCount); const uint32_t elements_per_thread = ceil((float)ceil((float)element_count / limits.computeUnits) / WorkgroupSize); - auto loadPrecompiledShader = [&]() -> smart_refctd_ptr + auto prepShader = [&](const core::string& path) -> smart_refctd_ptr { // this time we load a shader directly from a file IAssetLoader::SAssetLoadParams lp = {}; lp.logger = m_logger.get(); - lp.workingDirectory = "app_resources"; // virtual root - auto key = nbl::this_example::builtin::build::get_spirv_key(limits, m_physicalDevice->getFeatures()); - auto assetBundle = m_assetMgr->getAsset(key.data(), lp); + lp.workingDirectory = ""; // virtual root + auto assetBundle = m_assetMgr->getAsset(path,lp); const auto assets = assetBundle.getContents(); if (assets.empty()) { @@ -68,24 +51,29 @@ class CountingSortApp final : public application_templates::MonoDeviceApplicatio return nullptr; } - auto shader = IAsset::castDown(assets[0]); + auto source = IAsset::castDown(assets[0]); // The down-cast should not fail! - assert(shader); + assert(source); // There's two ways of doing stuff like this: // 1. this - modifying the asset after load // 2. creating a short shader source file that includes the asset you would have wanted to load - // - //auto overrideSource = CHLSLCompiler::createOverridenCopy( - // source.get(), "#define WorkgroupSize %d\n#define BucketCount %d\n", - // WorkgroupSize, bucket_count - //); - - // this time we skip the use of the asset converter since the IShader->IGPUShader path is quick and simple + auto overrideSource = CHLSLCompiler::createOverridenCopy( + source.get(), "#define WorkgroupSize %d\n#define BucketCount %d\n", + WorkgroupSize, bucket_count + ); + + // this time we skip the use of the asset converter since the ICPUShader->IGPUShader path is quick and simple + auto shader = m_device->createShader(overrideSource.get()); + if (!shader) + { + logFail("Creation of Prefix Sum Shader from CPU Shader source failed!"); + return nullptr; + } return shader; }; - auto prefixSumShader = loadPrecompiledShader.operator()<"prefix_sum_shader">(); // "app_resources/prefix_sum_shader.comp.hlsl" - auto scatterShader = loadPrecompiledShader.operator()<"scatter_shader">(); // "app_resources/scatter_shader.comp.hlsl" + auto prefixSumShader = prepShader("app_resources/prefix_sum_shader.comp.hlsl"); + auto scatterShader = prepShader("app_resources/scatter_shader.comp.hlsl"); // People love Reflection but I prefer Shader Sources instead! const nbl::asset::SPushConstantRange pcRange = { .stageFlags = IShader::E_SHADER_STAGE::ESS_COMPUTE,.offset = 0,.size = sizeof(CountingPushData) }; @@ -105,8 +93,8 @@ class CountingSortApp final : public application_templates::MonoDeviceApplicatio params.shader.shader = prefixSumShader.get(); params.shader.entryPoint = "main"; params.shader.entries = nullptr; - params.shader.requiredSubgroupSize = static_cast(5); - params.cached.requireFullSubgroups = true; + params.shader.requireFullSubgroups = true; + params.shader.requiredSubgroupSize = static_cast(5); if (!m_device->createComputePipelines(nullptr, { ¶ms,1 }, &prefixSumPipeline)) return logFail("Failed to create compute pipeline!\n"); params.shader.shader = scatterShader.get(); diff --git a/11_FFT/CMakeLists.txt b/11_FFT/CMakeLists.txt deleted file mode 100644 index 6b6304ed8..000000000 --- a/11_FFT/CMakeLists.txt +++ /dev/null @@ -1,62 +0,0 @@ -include(common RESULT_VARIABLE RES) -if(NOT RES) - message(FATAL_ERROR "common.cmake not found. Should be in {repo_root}/cmake directory") -endif() - -nbl_create_executable_project("" "" "" "" "${NBL_EXECUTABLE_PROJECT_CREATION_PCH_TARGET}") - -if(NBL_EMBED_BUILTIN_RESOURCES) - set(_BR_TARGET_ ${EXECUTABLE_NAME}_builtinResourceData) - set(RESOURCE_DIR "app_resources") - - get_filename_component(_SEARCH_DIRECTORIES_ "${CMAKE_CURRENT_SOURCE_DIR}" ABSOLUTE) - get_filename_component(_OUTPUT_DIRECTORY_SOURCE_ "${CMAKE_CURRENT_BINARY_DIR}/src" ABSOLUTE) - get_filename_component(_OUTPUT_DIRECTORY_HEADER_ "${CMAKE_CURRENT_BINARY_DIR}/include" ABSOLUTE) - - file(GLOB_RECURSE BUILTIN_RESOURCE_FILES RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}/${RESOURCE_DIR}" CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/${RESOURCE_DIR}/*") - foreach(RES_FILE ${BUILTIN_RESOURCE_FILES}) - LIST_BUILTIN_RESOURCE(RESOURCES_TO_EMBED "${RES_FILE}") - endforeach() - - ADD_CUSTOM_BUILTIN_RESOURCES(${_BR_TARGET_} RESOURCES_TO_EMBED "${_SEARCH_DIRECTORIES_}" "${RESOURCE_DIR}" "nbl::this_example::builtin" "${_OUTPUT_DIRECTORY_HEADER_}" "${_OUTPUT_DIRECTORY_SOURCE_}") - - LINK_BUILTIN_RESOURCES_TO_TARGET(${EXECUTABLE_NAME} ${_BR_TARGET_}) -endif() - -set(OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/auto-gen") - -set(SM 6_8) -set(JSON [=[ -[ - { - "INPUT": "app_resources/shader.comp.hlsl", - "KEY": "shader", - } -] -]=]) -string(CONFIGURE "${JSON}" JSON) - -set(COMPILE_OPTIONS - -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} -) diff --git a/11_FFT/app_resources/common.hlsl b/11_FFT/app_resources/common.hlsl deleted file mode 100644 index dc6b96e71..000000000 --- a/11_FFT/app_resources/common.hlsl +++ /dev/null @@ -1,12 +0,0 @@ -#include "nbl/builtin/hlsl/cpp_compat.hlsl" - -using scalar_t = nbl::hlsl::float32_t; - -struct PushConstantData -{ - uint64_t deviceBufferAddress; -}; - -NBL_CONSTEXPR uint32_t WorkgroupSizeLog2 = 6; -NBL_CONSTEXPR uint32_t ElementsPerThreadLog2 = 3; -NBL_CONSTEXPR uint32_t complexElementCount = uint32_t(1) << (WorkgroupSizeLog2 + ElementsPerThreadLog2); \ No newline at end of file diff --git a/11_FFT/app_resources/shader.comp.hlsl b/11_FFT/app_resources/shader.comp.hlsl deleted file mode 100644 index 7c86f50b4..000000000 --- a/11_FFT/app_resources/shader.comp.hlsl +++ /dev/null @@ -1,74 +0,0 @@ -#include "common.hlsl" -#include "nbl/builtin/hlsl/workgroup/fft.hlsl" - -[[vk::push_constant]] PushConstantData pushConstants; - -using namespace nbl::hlsl; - -using ConstevalParameters = workgroup::fft::ConstevalParameters; - -groupshared uint32_t sharedmem[ ConstevalParameters::SharedMemoryDWORDs]; - -// Users MUST define this method for FFT to work -uint32_t3 glsl::gl_WorkGroupSize() { return uint32_t3(uint32_t(ConstevalParameters::WorkgroupSize), 1, 1); } - -struct SharedMemoryAccessor -{ - template - void set(IndexType idx, AccessType value) - { - sharedmem[idx] = value; - } - - template - void get(IndexType idx, NBL_REF_ARG(AccessType) value) - { - value = sharedmem[idx]; - } - - void workgroupExecutionAndMemoryBarrier() - { - glsl::barrier(); - } - -}; - -// Almost a LegacyBdaAccessor, but since we need `uint32_t index` getter and setter it's the same as writing one ourselves -struct Accessor -{ - static Accessor create(const uint64_t address) - { - Accessor accessor; - accessor.address = address; - return accessor; - } - - // TODO: can't use our own BDA yet, because it doesn't support the types `workgroup::FFT` will invoke these templates with - template - void get(const IndexType index, NBL_REF_ARG(AccessType) value) - { - value = vk::RawBufferLoad(address + index * sizeof(AccessType)); - } - - template - void set(const IndexType index, const AccessType value) - { - vk::RawBufferStore(address + index * sizeof(AccessType), value); - } - - uint64_t address; -}; - -[numthreads(ConstevalParameters::WorkgroupSize,1,1)] -[shader("compute")] -void main(uint32_t3 ID : SV_DispatchThreadID) -{ - Accessor accessor = Accessor::create(pushConstants.deviceBufferAddress); - SharedMemoryAccessor sharedmemAccessor; - - // FFT - - workgroup::FFT::template __call(accessor, sharedmemAccessor); - sharedmemAccessor.workgroupExecutionAndMemoryBarrier(); - workgroup::FFT::template __call(accessor, sharedmemAccessor); -} \ No newline at end of file diff --git a/11_FFT/main.cpp b/11_FFT/main.cpp deleted file mode 100644 index 49d157a38..000000000 --- a/11_FFT/main.cpp +++ /dev/null @@ -1,322 +0,0 @@ -// Copyright (C) 2018-2024 - 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 "nbl/this_example/builtin/build/spirv/keys.hpp" - -#include "nbl/examples/examples.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; - -#include "app_resources/common.hlsl" -#include "nbl/builtin/hlsl/bit.hlsl" -#include "nbl/builtin/hlsl/random/xoroshiro.hlsl" - - -// Simple showcase of how to run FFT on a 1D array -class FFT_Test final : public application_templates::MonoDeviceApplication, public BuiltinResourcesApplication -{ - using device_base_t = application_templates::MonoDeviceApplication; - using asset_base_t = BuiltinResourcesApplication; - - smart_refctd_ptr m_pipeline; - - smart_refctd_ptr m_utils; - - nbl::video::StreamingTransientDataBufferMT<>* m_upStreamingBuffer; - StreamingTransientDataBufferMT<>* m_downStreamingBuffer; - smart_refctd_ptr m_deviceLocalBuffer; - - // These are Buffer Device Addresses - uint64_t m_upStreamingBufferAddress; - uint64_t m_downStreamingBufferAddress; - uint64_t m_deviceLocalBufferAddress; - - // You can ask the `nbl::core::GeneralpurposeAddressAllocator` used internally by the Streaming Buffers give out offsets aligned to a certain multiple (not only Power of Two!) - uint32_t m_alignment; - - // This example really lets the advantages of a timeline semaphore shine through! - smart_refctd_ptr m_timeline; - uint64_t semaphorValue = 0; - -public: - // Yay thanks to multiple inheritance we cannot forward ctors anymore - FFT_Test(const path& _localInputCWD, const path& _localOutputCWD, const path& _sharedInputCWD, const path& _sharedOutputCWD) : - system::IApplicationFramework(_localInputCWD, _localOutputCWD, _sharedInputCWD, _sharedOutputCWD) {} - - // we stuff all our work here because its a "single shot" app - bool onAppInitialized(smart_refctd_ptr&& system) override - { - // Remember to call the base class initialization! - if (!device_base_t::onAppInitialized(smart_refctd_ptr(system))) - return false; - if (!asset_base_t::onAppInitialized(std::move(system))) - return false; - - smart_refctd_ptr shader; - { - IAssetLoader::SAssetLoadParams lp = {}; - lp.logger = m_logger.get(); - lp.workingDirectory = "app_resources"; // virtual root - auto key = nbl::this_example::builtin::build::get_spirv_key<"shader">(m_device.get()); - auto assetBundle = m_assetMgr->getAsset(key.data(), lp); - const auto assets = assetBundle.getContents(); - if (assets.empty()) - return logFail("Could not load shader!"); - - // Cast down the asset to its proper type - shader = IAsset::castDown(assets[0]); - - if (!shader) - return logFail("Invalid shader!"); - } - - // Create massive upload/download buffers - constexpr uint32_t DownstreamBufferSize = sizeof(scalar_t) << 23; - constexpr uint32_t UpstreamBufferSize = sizeof(scalar_t) << 23; - - m_utils = IUtilities::create(smart_refctd_ptr(m_device), smart_refctd_ptr(m_logger), DownstreamBufferSize, UpstreamBufferSize); - if (!m_utils) - return logFail("Failed to create Utilities!"); - m_upStreamingBuffer = m_utils->getDefaultUpStreamingBuffer(); - m_downStreamingBuffer = m_utils->getDefaultDownStreamingBuffer(); - m_upStreamingBufferAddress = m_upStreamingBuffer->getBuffer()->getDeviceAddress(); - m_downStreamingBufferAddress = m_downStreamingBuffer->getBuffer()->getDeviceAddress(); - - // Create device-local buffer - { - const uint32_t scalarElementCount = 2 * complexElementCount; - IGPUBuffer::SCreationParams deviceLocalBufferParams = {}; - - IQueue* const queue = getComputeQueue(); - uint32_t queueFamilyIndex = queue->getFamilyIndex(); - - deviceLocalBufferParams.queueFamilyIndexCount = 1; - deviceLocalBufferParams.queueFamilyIndices = &queueFamilyIndex; - deviceLocalBufferParams.size = sizeof(scalar_t) * scalarElementCount; - deviceLocalBufferParams.usage = nbl::asset::IBuffer::E_USAGE_FLAGS::EUF_TRANSFER_SRC_BIT | nbl::asset::IBuffer::E_USAGE_FLAGS::EUF_TRANSFER_DST_BIT | nbl::asset::IBuffer::E_USAGE_FLAGS::EUF_SHADER_DEVICE_ADDRESS_BIT; - - m_deviceLocalBuffer = m_device->createBuffer(std::move(deviceLocalBufferParams)); - auto mreqs = m_deviceLocalBuffer->getMemoryReqs(); - mreqs.memoryTypeBits &= m_device->getPhysicalDevice()->getDeviceLocalMemoryTypeBits(); - auto gpubufMem = m_device->allocate(mreqs, m_deviceLocalBuffer.get(), IDeviceMemoryAllocation::E_MEMORY_ALLOCATE_FLAGS::EMAF_DEVICE_ADDRESS_BIT); - - m_deviceLocalBufferAddress = m_deviceLocalBuffer.get()->getDeviceAddress(); - } - - const nbl::asset::SPushConstantRange pcRange = { .stageFlags = IShader::E_SHADER_STAGE::ESS_COMPUTE,.offset = 0,.size = sizeof(PushConstantData) }; - - { - auto layout = m_device->createPipelineLayout({ &pcRange,1 }); - IGPUComputePipeline::SCreationParams params = {}; - params.layout = layout.get(); - params.shader.shader = shader.get(); - params.shader.entryPoint = "main"; - params.shader.requiredSubgroupSize = static_cast(hlsl::findMSB(m_physicalDevice->getLimits().maxSubgroupSize)); - params.cached.requireFullSubgroups = true; - if (!m_device->createComputePipelines(nullptr, { ¶ms,1 }, &m_pipeline)) - return logFail("Failed to create compute pipeline!\n"); - } - - const auto& deviceLimits = m_device->getPhysicalDevice()->getLimits(); - // The ranges of non-coherent mapped memory you flush or invalidate need to be aligned. You'll often see a value of 64 reported by devices - // which just happens to coincide with a CPU cache line size. So we ask our streaming buffers during allocation to give us properly aligned offsets. - // Sidenote: For SSBOs, UBOs, BufferViews, Vertex Buffer Bindings, Acceleration Structure BDAs, Shader Binding Tables, Descriptor Buffers, etc. - // there is also a requirement to bind buffers at offsets which have a certain alignment. Memory binding to Buffers and Images also has those. - // We'll align to max of coherent atom size even if the memory is coherent, - // and we also need to take into account BDA shader loads need to be aligned to the type being loaded. - m_alignment = core::max(deviceLimits.nonCoherentAtomSize, alignof(float)); - - // Semaphor used here to know the FFT is done before download - m_timeline = m_device->createSemaphore(semaphorValue); - - IQueue* const queue = getComputeQueue(); - - // Note that I'm using the sample struct with methods that have identical code which compiles as both C++ and HLSL - auto rng = nbl::hlsl::Xoroshiro64StarStar::construct({ semaphorValue ^ 0xdeadbeefu,std::hash()(_NBL_APP_NAME_) }); - - const uint32_t scalarElementCount = 2 * complexElementCount; - const uint32_t inputSize = sizeof(scalar_t) * scalarElementCount; - - // Just need a single suballocation in this example - const uint32_t AllocationCount = 1; - - // It comes with a certain drawback that you need to remember to initialize your "yet unallocated" offsets to the Invalid value - // this is to allow a set of allocations to fail, and you to re-try after doing something to free up space without repacking args. - auto inputOffset = m_upStreamingBuffer->invalid_value; - - // We always just wait till an allocation becomes possible (during allocation previous "latched" frees get their latch conditions polled) - // Freeing of Streaming Buffer Allocations can and should be deferred until an associated polled event signals done (more on that later). - std::chrono::steady_clock::time_point waitTill(std::chrono::years(45)); - // note that the API takes a time-point not a duration, because there are multiple waits and preemptions possible, so the durations wouldn't add up properly - m_upStreamingBuffer->multi_allocate(waitTill, AllocationCount, &inputOffset, &inputSize, &m_alignment); - - // Generate our data in-place on the allocated staging buffer. Packing is interleaved in this example! - { - auto* const inputPtr = reinterpret_cast(reinterpret_cast(m_upStreamingBuffer->getBufferPointer()) + inputOffset); - std::cout << "Begin array CPU\n"; - for (auto j = 0; j < complexElementCount; j++) - { - //Random array - - //scalar_t x = rng() / scalar_t(nbl::hlsl::numeric_limits::max), y = rng() / scalar_t(nbl::hlsl::numeric_limits::max); - - // FFT( (1,0), (0,0), (0,0),... ) = (1,0), (1,0), (1,0),... - - - scalar_t x = j > 0 ? 0.f : 1.f; - scalar_t y = 0; - - - // FFT( (c,0), (c,0), (c,0),... ) = (Nc,0), (0,0), (0,0),... - - /* - scalar_t x = 1.f; - scalar_t y = 0.f; - */ - - inputPtr[2 * j] = x; - inputPtr[2 * j + 1] = y; - std::cout << "(" << x << ", " << y << "), "; - } - std::cout << "\nEnd array CPU\n"; - // Always remember to flush! - if (m_upStreamingBuffer->needsManualFlushOrInvalidate()) - { - const auto bound = m_upStreamingBuffer->getBuffer()->getBoundMemory(); - const ILogicalDevice::MappedMemoryRange range(bound.memory, bound.offset + inputOffset, inputSize); - m_device->flushMappedMemoryRanges(1, &range); - } - } - - // finally allocate our output range - const uint32_t outputSize = inputSize; - - auto outputOffset = m_downStreamingBuffer->invalid_value; - m_downStreamingBuffer->multi_allocate(waitTill, AllocationCount, &outputOffset, &outputSize, &m_alignment); - - smart_refctd_ptr cmdbuf; - { - smart_refctd_ptr cmdpool = m_device->createCommandPool(queue->getFamilyIndex(), IGPUCommandPool::CREATE_FLAGS::TRANSIENT_BIT); - if (!cmdpool->createCommandBuffers(IGPUCommandPool::BUFFER_LEVEL::PRIMARY, 1u, &cmdbuf)) { - return logFail("Failed to create Command Buffers!\n"); - } - cmdpool->createCommandBuffers(IGPUCommandPool::BUFFER_LEVEL::PRIMARY, { &cmdbuf,1 }, core::smart_refctd_ptr(m_logger)); - cmdbuf->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT); - cmdbuf->bindComputePipeline(m_pipeline.get()); - // This is the new fun part, pushing constants - const PushConstantData pc = {.deviceBufferAddress = m_deviceLocalBufferAddress}; - IGPUCommandBuffer::SBufferCopy copyInfo = {}; - copyInfo.srcOffset = 0; - copyInfo.dstOffset = 0; - copyInfo.size = m_deviceLocalBuffer->getSize(); - cmdbuf->copyBuffer(m_upStreamingBuffer->getBuffer(), m_deviceLocalBuffer.get(), 1, ©Info); - cmdbuf->pushConstants(m_pipeline->getLayout(), IShader::E_SHADER_STAGE::ESS_COMPUTE, 0u, sizeof(pc), &pc); - // Remember we do a single workgroup per 1D array in these parts - cmdbuf->dispatch(1, 1, 1); - - // Pipeline barrier: wait for FFT shader to be done before copying to downstream buffer - IGPUCommandBuffer::SPipelineBarrierDependencyInfo pipelineBarrierInfo = {}; - - decltype(pipelineBarrierInfo)::buffer_barrier_t barrier = {}; - pipelineBarrierInfo.bufBarriers = { &barrier, 1u }; - - barrier.range.buffer = m_deviceLocalBuffer; - - barrier.barrier.dep.srcStageMask = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT; - barrier.barrier.dep.srcAccessMask = ACCESS_FLAGS::MEMORY_WRITE_BITS; - barrier.barrier.dep.dstStageMask = PIPELINE_STAGE_FLAGS::COPY_BIT; - barrier.barrier.dep.dstAccessMask = ACCESS_FLAGS::MEMORY_READ_BITS; - - cmdbuf->pipelineBarrier(asset::E_DEPENDENCY_FLAGS(0), pipelineBarrierInfo); - cmdbuf->copyBuffer(m_deviceLocalBuffer.get(), m_downStreamingBuffer->getBuffer(), 1, ©Info); - cmdbuf->end(); - } - - semaphorValue++; - { - const IQueue::SSubmitInfo::SCommandBufferInfo cmdbufInfo = - { - .cmdbuf = cmdbuf.get() - }; - const IQueue::SSubmitInfo::SSemaphoreInfo signalInfo = - { - .semaphore = m_timeline.get(), - .value = semaphorValue, - .stageMask = asset::PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT - }; - - const IQueue::SSubmitInfo submitInfo = { - .waitSemaphores = {}, - .commandBuffers = {&cmdbufInfo,1}, - .signalSemaphores = {&signalInfo,1} - }; - - m_api->startCapture(); - queue->submit({ &submitInfo,1 }); - m_api->endCapture(); - } - - // We let all latches know what semaphore and counter value has to be passed for the functors to execute - const ISemaphore::SWaitInfo futureWait = { m_timeline.get(),semaphorValue }; - - // As promised, we can defer an upstreaming buffer deallocation until a fence is signalled - // You can also attach an additional optional IReferenceCounted derived object to hold onto until deallocation. - m_upStreamingBuffer->multi_deallocate(AllocationCount, &inputOffset, &inputSize, futureWait); - - // Now a new and even more advanced usage of the latched events, we make our own refcounted object with a custom destructor and latch that like we did the commandbuffer. - // Instead of making our own and duplicating logic, we'll use one from IUtilities meant for down-staging memory. - // Its nice because it will also remember to invalidate our memory mapping if its not coherent. - auto latchedConsumer = make_smart_refctd_ptr( - IDeviceMemoryAllocation::MemoryRange(outputOffset, outputSize), - // Note the use of capture by-value [=] and not by-reference [&] because this lambda will be called asynchronously whenever the event signals - [=](const size_t dstOffset, const void* bufSrc, const size_t size)->void - { - // The unused variable is used for letting the consumer know the subsection of the output we've managed to download - // But here we're sure we can get the whole thing in one go because we allocated the whole range ourselves. - assert(dstOffset == 0 && size == outputSize); - - std::cout << "Begin array GPU\n"; - scalar_t* const data = reinterpret_cast(const_cast(bufSrc)); - for (auto i = 0u; i < complexElementCount; i++) { - std::cout << "(" << data[2 * i] << ", " << data[2 * i + 1] << "), "; - } - - std::cout << "\nEnd array GPU\n"; - }, - // Its also necessary to hold onto the commandbuffer, even though we take care to not reset the parent pool, because if it - // hits its destructor, our automated reference counting will drop all references to objects used in the recorded commands. - // It could also be latched in the upstreaming deallocate, because its the same fence. - std::move(cmdbuf), m_downStreamingBuffer - ); - // We put a function we want to execute - m_downStreamingBuffer->multi_deallocate(AllocationCount, &outputOffset, &outputSize, futureWait, &latchedConsumer.get()); - - return true; - } - - // One-shot App - bool keepRunning() override { return false; } - - // One-shot App - void workLoopBody() override{} - - // Cleanup - bool onAppTerminated() override - { - // Need to make sure that there are no events outstanding if we want all lambdas to eventually execute before `onAppTerminated` - // (the destructors of the Command Pool Cache and Streaming buffers will still wait for all lambda events to drain) - while (m_downStreamingBuffer->cull_frees()) {} - return device_base_t::onAppTerminated(); - } -}; - - -NBL_MAIN_FUNC(FFT_Test) \ No newline at end of file diff --git a/11_FFT/pipeline.groovy b/11_FFT/pipeline.groovy deleted file mode 100644 index 1a7b043a4..000000000 --- a/11_FFT/pipeline.groovy +++ /dev/null @@ -1,50 +0,0 @@ -import org.DevshGraphicsProgramming.Agent -import org.DevshGraphicsProgramming.BuilderInfo -import org.DevshGraphicsProgramming.IBuilder - -class CStreamingAndBufferDeviceAddressBuilder extends IBuilder -{ - public CStreamingAndBufferDeviceAddressBuilder(Agent _agent, _info) - { - super(_agent, _info) - } - - @Override - public boolean prepare(Map axisMapping) - { - return true - } - - @Override - public boolean build(Map axisMapping) - { - IBuilder.CONFIGURATION config = axisMapping.get("CONFIGURATION") - IBuilder.BUILD_TYPE buildType = axisMapping.get("BUILD_TYPE") - - def nameOfBuildDirectory = getNameOfBuildDirectory(buildType) - def nameOfConfig = getNameOfConfig(config) - - agent.execute("cmake --build ${info.rootProjectPath}/${nameOfBuildDirectory}/${info.targetProjectPathRelativeToRoot} --target ${info.targetBaseName} --config ${nameOfConfig} -j12 -v") - - return true - } - - @Override - public boolean test(Map axisMapping) - { - return true - } - - @Override - public boolean install(Map axisMapping) - { - return true - } -} - -def create(Agent _agent, _info) -{ - return new CStreamingAndBufferDeviceAddressBuilder(_agent, _info) -} - -return this \ No newline at end of file diff --git a/12_MeshLoaders/CMakeLists.txt b/12_MeshLoaders/CMakeLists.txt deleted file mode 100644 index 709b7d40b..000000000 --- a/12_MeshLoaders/CMakeLists.txt +++ /dev/null @@ -1,26 +0,0 @@ -set(NBL_INCLUDE_SERACH_DIRECTORIES - "${CMAKE_CURRENT_SOURCE_DIR}/include" -) -set(NBL_LIBRARIES) - -if (NBL_BUILD_MITSUBA_LOADER) - list(APPEND NBL_INCLUDE_SERACH_DIRECTORIES - "${NBL_EXT_MITSUBA_LOADER_INCLUDE_DIRS}" - ) - list(APPEND NBL_LIBRARIES - "${NBL_EXT_MITSUBA_LOADER_LIB}" - ) -endif() - - # 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_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) - -if (NBL_BUILD_DEBUG_DRAW) - target_link_libraries(${EXECUTABLE_NAME} PRIVATE Nabla::ext::DebugDraw) -endif() - - -add_dependencies(${EXECUTABLE_NAME} argparse) -target_include_directories(${EXECUTABLE_NAME} PUBLIC $) \ No newline at end of file diff --git a/12_MeshLoaders/README.md b/12_MeshLoaders/README.md deleted file mode 100644 index 6330f4673..000000000 --- a/12_MeshLoaders/README.md +++ /dev/null @@ -1,2 +0,0 @@ -https://github.com/user-attachments/assets/6f779700-e6d4-4e11-95fb-7a7fddc47255 - diff --git a/12_MeshLoaders/include/common.hpp b/12_MeshLoaders/include/common.hpp deleted file mode 100644 index 84cd8118a..000000000 --- a/12_MeshLoaders/include/common.hpp +++ /dev/null @@ -1,18 +0,0 @@ -#ifndef _NBL_THIS_EXAMPLE_COMMON_H_INCLUDED_ -#define _NBL_THIS_EXAMPLE_COMMON_H_INCLUDED_ - - -#include "nbl/examples/examples.hpp" - -using namespace nbl; -using namespace core; -using namespace hlsl; -using namespace system; -using namespace asset; -using namespace ui; -using namespace video; -using namespace scene; -using namespace nbl::examples; - - -#endif // __NBL_THIS_EXAMPLE_COMMON_H_INCLUDED__ \ No newline at end of file diff --git a/12_MeshLoaders/main.cpp b/12_MeshLoaders/main.cpp deleted file mode 100644 index e27ed4be0..000000000 --- a/12_MeshLoaders/main.cpp +++ /dev/null @@ -1,581 +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 -#include "argparse/argparse.hpp" -#include "common.hpp" - -#include "../3rdparty/portable-file-dialogs/portable-file-dialogs.h" -#include - -#ifdef NBL_BUILD_MITSUBA_LOADER -#include "nbl/ext/MitsubaLoader/CSerializedLoader.h" -#endif - -#ifdef NBL_BUILD_DEBUG_DRAW -#include "nbl/ext/DebugDraw/CDrawAABB.h" -#endif - -class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourcesApplication -{ - using device_base_t = MonoWindowApplication; - using asset_base_t = BuiltinResourcesApplication; - - enum DrawBoundingBoxMode - { - DBBM_NONE, - DBBM_AABB, - DBBM_OBB, - DBBM_COUNT - }; - - public: - inline MeshLoadersApp(const path& _localInputCWD, const path& _localOutputCWD, const path& _sharedInputCWD, const path& _sharedOutputCWD) - : IApplicationFramework(_localInputCWD, _localOutputCWD, _sharedInputCWD, _sharedOutputCWD), - device_base_t({1280,720}, EF_D32_SFLOAT, _localInputCWD, _localOutputCWD, _sharedInputCWD, _sharedOutputCWD) {} - - inline bool onAppInitialized(smart_refctd_ptr&& system) override - { - if (!asset_base_t::onAppInitialized(smart_refctd_ptr(system))) - return false; -#ifdef NBL_BUILD_MITSUBA_LOADER - m_assetMgr->addAssetLoader(make_smart_refctd_ptr()); -#endif - if (!device_base_t::onAppInitialized(smart_refctd_ptr(system))) - return false; - - m_saveGeomPrefixPath = localOutputCWD / "saved"; - - // parse args - argparse::ArgumentParser parser("12_meshloaders"); - parser.add_argument("--savegeometry") - .help("Save the mesh on exit or reload") - .flag(); - - parser.add_argument("--savepath") - .nargs(1) - .help("Specify the file to which the mesh will be saved"); - - try - { - parser.parse_args({ argv.data(), argv.data() + argv.size() }); - } - catch (const std::exception& e) - { - return logFail(e.what()); - } - - if (parser["--savegeometry"] == true) - m_saveGeom = true; - - if (parser.present("--savepath")) - { - auto tmp = path(parser.get("--savepath")); - - if (tmp.empty() || !tmp.has_filename()) - return logFail("Invalid path has been specified in --savepath argument"); - - if (!std::filesystem::exists(tmp.parent_path())) - return logFail("Path specified in --savepath argument doesn't exist"); - - m_specifiedGeomSavePath.emplace(std::move(tmp.generic_string())); - } - - 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; i < MaxFramesInFlight; i++) - { - if (!pool) - return logFail("Couldn't create Command Pool!"); - if (!pool->createCommandBuffers(IGPUCommandPool::BUFFER_LEVEL::PRIMARY, { m_cmdBufs.data() + i,1 })) - return logFail("Couldn't create Command Buffer!"); - } - - - auto scRes = static_cast(m_surface->getSwapchainResources()); - m_renderer = CSimpleDebugRenderer::create(m_assetMgr.get(), scRes->getRenderpass(), 0, {}); - if (!m_renderer) - return logFail("Failed to create renderer!"); - -#ifdef NBL_BUILD_DEBUG_DRAW - { - auto* renderpass = scRes->getRenderpass(); - ext::debug_draw::DrawAABB::SCreationParameters params = {}; - params.assetManager = m_assetMgr; - params.transfer = getTransferUpQueue(); - params.drawMode = ext::debug_draw::DrawAABB::ADM_DRAW_BATCH; - params.batchPipelineLayout = ext::debug_draw::DrawAABB::createDefaultPipelineLayout(m_device.get()); - params.renderpass = smart_refctd_ptr(renderpass); - params.utilities = m_utils; - m_drawAABB = ext::debug_draw::DrawAABB::create(std::move(params)); - } -#endif - - // - if (!reloadModel()) - return false; - - camera.mapKeysToArrows(); - - onAppInitializedFinish(); - return true; - } - - inline IQueue::SSubmitInfo::SSemaphoreInfo renderFrame(const std::chrono::microseconds nextPresentationTimestamp) override - { - m_inputSystem->getDefaultMouse(&mouse); - m_inputSystem->getDefaultKeyboard(&keyboard); - - // - 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 - { - // begin renderpass - { - auto scRes = static_cast(m_surface->getSwapchainResources()); - auto* framebuffer = scRes->getFramebuffer(device_base_t::getCurrentAcquire().imageIndex); - const IGPUCommandBuffer::SClearColorValue clearValue = { .float32 = {1.f,0.f,1.f,1.f} }; - const IGPUCommandBuffer::SClearDepthStencilValue depthValue = { .depth = 0.f }; - const VkRect2D currentRenderArea = - { - .offset = {0,0}, - .extent = {framebuffer->getCreationParameters().width,framebuffer->getCreationParameters().height} - }; - const IGPUCommandBuffer::SRenderpassBeginInfo info = - { - .framebuffer = framebuffer, - .colorClearValues = &clearValue, - .depthStencilClearValues = &depthValue, - .renderArea = currentRenderArea - }; - cb->beginRenderPass(info, IGPUCommandBuffer::SUBPASS_CONTENTS::INLINE); - - const SViewport viewport = { - .x = static_cast(currentRenderArea.offset.x), - .y = static_cast(currentRenderArea.offset.y), - .width = static_cast(currentRenderArea.extent.width), - .height = static_cast(currentRenderArea.extent.height) - }; - cb->setViewport(0u,1u,&viewport); - - cb->setScissor(0u,1u,¤tRenderArea); - } - // late latch input - { - bool reload = false; - 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 - { - for (const auto& event : events) - { - if (event.keyCode == E_KEY_CODE::EKC_R && event.action == SKeyboardEvent::ECA_RELEASED) - reload = true; - if (event.keyCode == E_KEY_CODE::EKC_B && event.action == SKeyboardEvent::ECA_RELEASED) - { - m_drawBBMode = DrawBoundingBoxMode((m_drawBBMode + 1) % DBBM_COUNT); - } - } - camera.keyboardProcess(events); - }, - m_logger.get() - ); - camera.endInputProcessing(nextPresentationTimestamp); - if (reload) - reloadModel(); - } - // draw scene - float32_t3x4 viewMatrix = camera.getViewMatrix(); - float32_t4x4 viewProjMatrix = camera.getConcatenatedMatrix(); - m_renderer->render(cb,CSimpleDebugRenderer::SViewParams(viewMatrix,viewProjMatrix)); -#ifdef NBL_BUILD_DEBUG_DRAW - if (m_drawBBMode != DBBM_NONE) - { - const ISemaphore::SWaitInfo drawFinished = { .semaphore = m_semaphore.get(),.value = m_realFrameIx + 1u }; - ext::debug_draw::DrawAABB::DrawParameters drawParams; - drawParams.commandBuffer = cb; - drawParams.cameraMat = viewProjMatrix; - m_drawAABB->render(drawParams, drawFinished, m_drawBBMode == DBBM_OBB ? m_obbInstances : m_aabbInstances); - } -#endif - cb->endRenderPass(); - } - cb->end(); - - 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--; - } - - std::string caption = "[Nabla Engine] Mesh Loaders"; - { - caption += ", displaying ["; - caption += m_modelPath; - caption += "]"; - m_window->setCaption(caption); - } - return retval; - } - - inline bool onAppTerminated() override - { - if (m_saveGeomTaskFuture.valid()) - { - m_logger->log("Waiting for geometry writer to finish writing...", ILogger::ELL_INFO); - m_saveGeomTaskFuture.wait(); - } - - return device_base_t::onAppTerminated(); - } - -protected: - const video::IGPURenderpass::SCreationParams::SSubpassDependency* getDefaultSubpassDependencies() const override - { - // Subsequent submits don't wait for each other, hence its important to have External Dependencies which prevent users of the depth attachment overlapping. - 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 - .srcStageMask = PIPELINE_STAGE_FLAGS::LATE_FRAGMENT_TESTS_BIT, - // don't want any writes to be available, we'll clear - .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 - }, - // color from ATTACHMENT_OPTIMAL to PRESENT_SRC - { - .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: - // TODO: standardise this across examples, and take from `argv` - bool m_nonInteractiveTest = false; - - bool reloadModel() - { - if (m_nonInteractiveTest) // TODO: maybe also take from argv and argc - m_modelPath = (sharedInputCWD / "ply/Spanner-ply.ply").string(); - else - { - pfd::open_file file("Choose a supported Model File", sharedInputCWD.string(), - { - "All Supported Formats", "*.ply *.stl *.serialized *.obj", - "TODO (.ply)", "*.ply", - "TODO (.stl)", "*.stl", - "Mitsuba 0.6 Serialized (.serialized)", "*.serialized", - "Wavefront Object (.obj)", "*.obj" - }, - false - ); - if (file.result().empty()) - return false; - m_modelPath = file.result()[0]; - } - - // free up - m_renderer->m_instances.clear(); - m_renderer->clearGeometries({ .semaphore = m_semaphore.get(),.value = m_realFrameIx }); - m_assetMgr->clearAllAssetCache(); - - //! load the geometry - IAssetLoader::SAssetLoadParams params = {}; - params.logger = m_logger.get(); - auto asset = m_assetMgr->getAsset(m_modelPath, params); - if (asset.getContents().empty()) - return false; - - // - core::vector> geometries; - switch (asset.getAssetType()) - { - case IAsset::E_TYPE::ET_GEOMETRY: - for (const auto& item : asset.getContents()) - if (auto polyGeo = IAsset::castDown(item); polyGeo) - geometries.push_back(polyGeo); - break; - default: - m_logger->log("Asset loaded but not a supported type (ET_GEOMETRY,ET_GEOMETRY_COLLECTION)", ILogger::ELL_ERROR); - break; - } - if (geometries.empty()) - return false; - - if (m_saveGeom) - { - if (m_saveGeomTaskFuture.valid()) - { - m_logger->log("Waiting for previous geometry saving task to complete...", ILogger::ELL_INFO); - m_saveGeomTaskFuture.wait(); - } - - std::string currentGeomSavePath = m_specifiedGeomSavePath.value_or((m_saveGeomPrefixPath / path(m_modelPath).filename()).generic_string()); - m_saveGeomTaskFuture = std::async( - std::launch::async, - [this, geometries, currentGeomSavePath] { writeGeometry( - geometries[0], - currentGeomSavePath - ); } - ); - } - - using aabb_t = hlsl::shapes::AABB<3, double>; - auto printAABB = [&](const aabb_t& aabb, const char* extraMsg = "")->void - { - m_logger->log("%s AABB is (%f,%f,%f) -> (%f,%f,%f)", ILogger::ELL_INFO, extraMsg, aabb.minVx.x, aabb.minVx.y, aabb.minVx.z, aabb.maxVx.x, aabb.maxVx.y, aabb.maxVx.z); - }; - auto bound = aabb_t::create(); - // convert the geometries - { - smart_refctd_ptr converter = CAssetConverter::create({ .device = m_device.get() }); - - const auto transferFamily = getTransferUpQueue()->getFamilyIndex(); - - struct SInputs : CAssetConverter::SInputs - { - virtual inline std::span getSharedOwnershipQueueFamilies(const size_t groupCopyID, const asset::ICPUBuffer* buffer, const CAssetConverter::patch_t& patch) const - { - return sharedBufferOwnership; - } - - core::vector sharedBufferOwnership; - } inputs = {}; - core::vector> patches(geometries.size(), CSimpleDebugRenderer::DefaultPolygonGeometryPatch); - { - inputs.logger = m_logger.get(); - std::get>(inputs.assets) = { &geometries.front().get(),geometries.size() }; - std::get>(inputs.patches) = patches; - // set up shared ownership so we don't have to - core::unordered_set families; - families.insert(transferFamily); - families.insert(getGraphicsQueue()->getFamilyIndex()); - if (families.size() > 1) - for (const auto fam : families) - inputs.sharedBufferOwnership.push_back(fam); - } - - // reserve - auto reservation = converter->reserve(inputs); - if (!reservation) - { - m_logger->log("Failed to reserve GPU objects for CPU->GPU conversion!", ILogger::ELL_ERROR); - return false; - } - - // convert - { - auto semaphore = m_device->createSemaphore(0u); - - constexpr auto MultiBuffering = 2; - std::array, MultiBuffering> commandBuffers = {}; - { - auto pool = m_device->createCommandPool(transferFamily, IGPUCommandPool::CREATE_FLAGS::RESET_COMMAND_BUFFER_BIT | IGPUCommandPool::CREATE_FLAGS::TRANSIENT_BIT); - pool->createCommandBuffers(IGPUCommandPool::BUFFER_LEVEL::PRIMARY, commandBuffers, smart_refctd_ptr(m_logger)); - } - commandBuffers.front()->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT); - - std::array commandBufferSubmits; - for (auto i = 0; i < MultiBuffering; i++) - commandBufferSubmits[i].cmdbuf = commandBuffers[i].get(); - - SIntendedSubmitInfo transfer = {}; - transfer.queue = getTransferUpQueue(); - transfer.scratchCommandBuffers = commandBufferSubmits; - transfer.scratchSemaphore = { - .semaphore = semaphore.get(), - .value = 0u, - .stageMask = PIPELINE_STAGE_FLAGS::ALL_TRANSFER_BITS - }; - - CAssetConverter::SConvertParams cpar = {}; - cpar.utilities = m_utils.get(); - cpar.transfer = &transfer; - - // basically it records all data uploads and submits them right away - auto future = reservation.convert(cpar); - if (future.copy()!=IQueue::RESULT::SUCCESS) - { - m_logger->log("Failed to await submission feature!", ILogger::ELL_ERROR); - return false; - } - } - - auto tmp = hlsl::float32_t4x3( - hlsl::float32_t3(1,0,0), - hlsl::float32_t3(0,1,0), - hlsl::float32_t3(0,0,1), - hlsl::float32_t3(0,0,0) - ); - core::vector worldTforms; - const auto& converted = reservation.getGPUObjects(); - m_aabbInstances.resize(converted.size()); - m_obbInstances.resize(converted.size()); - for (uint32_t i = 0; i < converted.size(); i++) - { - const auto& geom = converted[i]; - const auto promoted = geom.value->getAABB(); - printAABB(promoted,"Geometry"); - tmp[3].x += promoted.getExtent().x; - const auto promotedWorld = hlsl::float64_t3x4(worldTforms.emplace_back(hlsl::transpose(tmp))); - const auto transformed = hlsl::shapes::util::transform(promotedWorld,promoted); - printAABB(transformed,"Transformed"); - bound = hlsl::shapes::util::union_(transformed,bound); - -#ifdef NBL_BUILD_DEBUG_DRAW - - auto& aabbInst = m_aabbInstances[i]; - const auto tmpAabb = shapes::AABB<3,float>(promoted.minVx, promoted.maxVx); - - hlsl::float32_t3x4 aabbTransform = ext::debug_draw::DrawAABB::getTransformFromAABB(tmpAabb); - const auto tmpWorld = hlsl::float32_t3x4(promotedWorld); - const auto world4x4 = float32_t4x4{ - tmpWorld[0], - tmpWorld[1], - tmpWorld[2], - float32_t4(0, 0, 0, 1) - }; - - aabbInst.color = { 1,1,1,1 }; - aabbInst.transform = math::linalg::promoted_mul(world4x4, aabbTransform); - - auto& obbInst = m_obbInstances[i]; - const auto& cpuGeom = geometries[i].get(); - const auto obb = CPolygonGeometryManipulator::calculateOBB( - cpuGeom->getPositionView().getElementCount(), - [geo = cpuGeom, &world4x4](size_t vertex_i) { - hlsl::float32_t3 pt; - geo->getPositionView().decodeElement(vertex_i, pt); - return pt; - }); - obbInst.color = { 0, 0, 1, 1 }; - obbInst.transform = math::linalg::promoted_mul(world4x4, obb.transform); -#endif - } - - printAABB(bound,"Total"); - if (!m_renderer->addGeometries({ &converted.front().get(),converted.size() })) - return false; - - auto worlTformsIt = worldTforms.begin(); - for (const auto& geo : m_renderer->getGeometries()) - m_renderer->m_instances.push_back({ - .world = *(worlTformsIt++), - .packedGeo = &geo - }); - } - - // get scene bounds and reset camera - { - const double distance = 0.05; - const auto diagonal = bound.getExtent(); - { - 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); - } - 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 - - return true; - } - - void writeGeometry(smart_refctd_ptr geometry, const std::string& savePath) - { - IAsset* assetPtr = const_cast(static_cast(geometry.get())); - IAssetWriter::SAssetWriteParams params{ assetPtr }; - m_logger->log("Saving mesh to %s", ILogger::ELL_INFO, savePath.c_str()); - if (!m_assetMgr->writeAsset(savePath, params)) - m_logger->log("Failed to save %s", ILogger::ELL_ERROR, savePath.c_str()); - m_logger->log("Mesh successfully saved!", ILogger::ELL_INFO); - } - - // 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; - // - smart_refctd_ptr m_renderer; - // - smart_refctd_ptr m_semaphore; - uint64_t m_realFrameIx = 0; - std::array, MaxFramesInFlight> m_cmdBufs; - // - InputSystem::ChannelReader mouse; - InputSystem::ChannelReader keyboard; - // - Camera camera = Camera(core::vectorSIMDf(0, 0, 0), core::vectorSIMDf(0, 0, 0), hlsl::float32_t4x4()); - // mutables - std::string m_modelPath; - - DrawBoundingBoxMode m_drawBBMode; -#ifdef NBL_BUILD_DEBUG_DRAW - smart_refctd_ptr m_drawAABB; - std::vector m_aabbInstances; - std::vector m_obbInstances; - -#endif - - bool m_saveGeom = false; - std::future m_saveGeomTaskFuture; - std::optional m_specifiedGeomSavePath; - nbl::system::path m_saveGeomPrefixPath; -}; - -NBL_MAIN_FUNC(MeshLoadersApp) \ No newline at end of file diff --git a/13_MaterialCompilerTest/CMakeLists.txt b/13_MaterialCompilerTest/CMakeLists.txt deleted file mode 100644 index 81f9630fb..000000000 --- a/13_MaterialCompilerTest/CMakeLists.txt +++ /dev/null @@ -1,29 +0,0 @@ -include(common RESULT_VARIABLE RES) -if(NOT RES) - message(FATAL_ERROR "common.cmake not found. Should be in {repo_root}/cmake directory") -endif() - -# keep the sources here for a while so we can quickly iterate -set(NBL_EXTRA_SOURCES -# "${NBL_ROOT_PATH}/src/nbl/asset/material_compiler3/CFrontendIR.cpp" -) - -nbl_create_executable_project("${NBL_EXTRA_SOURCES}" "" "" "" "${NBL_EXECUTABLE_PROJECT_CREATION_PCH_TARGET}") - -if(NBL_EMBED_BUILTIN_RESOURCES) - set(_BR_TARGET_ ${EXECUTABLE_NAME}_builtinResourceData) - set(RESOURCE_DIR "app_resources") - - get_filename_component(_SEARCH_DIRECTORIES_ "${CMAKE_CURRENT_SOURCE_DIR}" ABSOLUTE) - get_filename_component(_OUTPUT_DIRECTORY_SOURCE_ "${CMAKE_CURRENT_BINARY_DIR}/src" ABSOLUTE) - get_filename_component(_OUTPUT_DIRECTORY_HEADER_ "${CMAKE_CURRENT_BINARY_DIR}/include" ABSOLUTE) - - file(GLOB_RECURSE BUILTIN_RESOURCE_FILES RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}/${RESOURCE_DIR}" CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/${RESOURCE_DIR}/*") - foreach(RES_FILE ${BUILTIN_RESOURCE_FILES}) - LIST_BUILTIN_RESOURCE(RESOURCES_TO_EMBED "${RES_FILE}") - endforeach() - - ADD_CUSTOM_BUILTIN_RESOURCES(${_BR_TARGET_} RESOURCES_TO_EMBED "${_SEARCH_DIRECTORIES_}" "${RESOURCE_DIR}" "nbl::this_example::builtin" "${_OUTPUT_DIRECTORY_HEADER_}" "${_OUTPUT_DIRECTORY_SOURCE_}") - - LINK_BUILTIN_RESOURCES_TO_TARGET(${EXECUTABLE_NAME} ${_BR_TARGET_}) -endif() \ No newline at end of file diff --git a/13_MaterialCompilerTest/main.cpp b/13_MaterialCompilerTest/main.cpp deleted file mode 100644 index bdba96d29..000000000 --- a/13_MaterialCompilerTest/main.cpp +++ /dev/null @@ -1,646 +0,0 @@ -// Copyright (C) 2018-2024 - 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 "nbl/examples/examples.hpp" - - - -using namespace nbl; -using namespace nbl::core; -using namespace nbl::hlsl; -using namespace nbl::system; -using namespace nbl::asset; -using namespace nbl::asset::material_compiler3; -using namespace nbl::ui; -using namespace nbl::video; -using namespace nbl::examples; - - -// Testing our material compiler -class MaterialCompilerTest final : public application_templates::MonoDeviceApplication, public BuiltinResourcesApplication -{ - using device_base_t = application_templates::MonoDeviceApplication; - using asset_base_t = BuiltinResourcesApplication; - - public: - // Yay thanks to multiple inheritance we cannot forward ctors anymore - MaterialCompilerTest(const path& _localInputCWD, const path& _localOutputCWD, const path& _sharedInputCWD, const path& _sharedOutputCWD) : - system::IApplicationFramework(_localInputCWD, _localOutputCWD, _sharedInputCWD, _sharedOutputCWD) {} - - // we stuff all our work here because its a "single shot" app - bool onAppInitialized(smart_refctd_ptr&& system) override - { - // Remember to call the base class initialization! - if (!device_base_t::onAppInitialized(smart_refctd_ptr(system))) - return false; - if (!asset_base_t::onAppInitialized(std::move(system))) - return false; - - auto forest = CFrontendIR::create({.composed={.blockSizeKBLog2=4}}); - auto& forestPool = forest->getObjectPool(); - - auto logger = m_logger.get(); - - { - // dummy image views - smart_refctd_ptr monochromeImageView, rgbImageView; - { - constexpr auto format = EF_R16_SFLOAT; - auto image = ICPUImage::create({ - .type = IImage::E_TYPE::ET_2D, - .samples = IImage::E_SAMPLE_COUNT_FLAGS::ESCF_1_BIT, - .format = format, - .extent = {32,32,1}, - .mipLevels = 1, - .arrayLayers = 1 - }); - monochromeImageView = ICPUImageView::create({.image=std::move(image),.viewType=ICPUImageView::ET_2D,.format=format}); - } - { - constexpr auto format = EF_R8G8B8A8_SRGB; - auto image = ICPUImage::create({ - .type = IImage::E_TYPE::ET_2D, - .samples = IImage::E_SAMPLE_COUNT_FLAGS::ESCF_1_BIT, - .format = format, - .extent = {1024,1024,1}, - .mipLevels = 11, - .arrayLayers = 72 // fur teh lulz - }); - rgbImageView = ICPUImageView::create({.image=std::move(image),.viewType=ICPUImageView::ET_2D_ARRAY,.format=format}); - } - - // TODO: use std::source_info - #define ASSERT_VALUE(WHAT,VALUE,MSG) if (WHAT!=VALUE) \ - return logFail("%s:%d test doesn't match expected value. %s",__FILE__,__LINE__,MSG); \ - else if (!VALUE) \ - if constexpr (std::is_same_v) \ - m_logger->log("Disregard the error above, its expected.",system::ILogger::ELL_INFO) - - - using spectral_var_t = CFrontendIR::CSpectralVariable; - // simple white furnace testing materials - { - // transmission - { - const auto layerH = forestPool.emplace(); - auto* layer = forestPool.deref(layerH); - layer->debugInfo = forestPool.emplace("MyWeirdInvisibleMaterial"); - layer->btdf = forestPool.emplace(); - ASSERT_VALUE(forest->addMaterial(layerH,logger),true,"Add Material"); - } - - // creating a node and changing our mind - { - - spectral_var_t::SCreationParams<1> params = {}; - params.knots.params[0].scale = 4.5f; - params.knots.params[0].view = monochromeImageView; - - ASSERT_VALUE(monochromeImageView->getReferenceCount(),2,"initial reference count"); - - const auto handle = forestPool.emplace(std::move(params)); - ASSERT_VALUE(monochromeImageView->getReferenceCount(),2,"transferred reference count"); - - // cleaning it up right away should run the destructor immediately and drop the image view refcount - forestPool._delete(handle); - ASSERT_VALUE(monochromeImageView->getReferenceCount(),1,"after deletion reference count"); - } - - // delta reflection - { - const auto layerH = forestPool.emplace(); - auto* layer = forestPool.deref(layerH); - layer->debugInfo = forestPool.emplace("PerfectMirror"); - - { - const auto ctH = forestPool.emplace(); - auto* ct = forestPool.deref(ctH); - ct->debugInfo = forestPool.emplace("Smooth NDF"); - ASSERT_VALUE(ct->ndParams.getRougness()[0].scale,0.f,"Initial NDF Params must be Smooth"); - ASSERT_VALUE(ct->ndParams.getRougness()[1].scale,0.f,"Initial NDF Params must be Smooth"); - ASSERT_VALUE(ct->ndParams.getDerivMap()[0].scale,0.f,"Initial NDF Params must be Flat"); - ASSERT_VALUE(ct->ndParams.getDerivMap()[1].scale,0.f,"Initial NDF Params must be Flat"); - layer->brdfTop = ctH; - } - - // test layer cycle detection - layer->coated = layerH; - ASSERT_VALUE(forest->addMaterial(layerH,logger),false,"Layer Cycle Detection"); - layer->coated = {}; - - ASSERT_VALUE(forest->addMaterial(layerH,logger),true,"Add Material"); - } - - // two-sided diffuse - { - const auto layerH = forestPool.emplace(); - auto* layer = forestPool.deref(layerH); - layer->debugInfo = forestPool.emplace("Twosided Diffuse"); - const auto orenNayarH = forestPool.emplace(); - auto* orenNayar = forestPool.deref(orenNayarH); - orenNayar->debugInfo = forestPool.emplace("Actually Lambertian"); - // TODO: add a derivative map for testing the printing and compilation - layer->brdfTop = orenNayarH; - layer->brdfBottom = orenNayarH; - ASSERT_VALUE(forest->addMaterial(layerH,logger),true,"Add Material"); - } - - // diffuse isotropic rough transmissive - { - const auto layerH = forestPool.emplace(); - auto* layer = forestPool.deref(layerH); - layer->debugInfo = forestPool.emplace("Rough Diffuse Transmitter"); - // The material compiler can't handle the BRDF vs. BTDF normalization and energy conservation for you. - // Given a BRDF expression we simply can't tell if the missing energy was supposed - // to be transferred to the BTDF or absorbed by the BRDF itself. - // Hence the BTDF expression must contain the BRDF coating term (how much energy is "taken" by the BRDF). - const auto mulH = forestPool.emplace(); - layer->brdfTop = mulH; - layer->btdf = mulH; - layer->brdfBottom = mulH; - - auto* mul = forestPool.deref(mulH); - // regular BRDF will normalize to 100% over a hemisphere, if we allow a BTDF term we must split it half/half - { - spectral_var_t::SCreationParams<1> params = {}; - params.knots.params[0].scale = 0.5f; - mul->rhs = forestPool.emplace(std::move(params)); - } - - // test expression cycle detection - mul->lhs = mulH; - ASSERT_VALUE(forest->addMaterial(layerH,logger),false,"Expression Cycle Detection"); - - // create the BxDF as we'd do for a single BRDF or BTDF - { - const auto orenNayarH = forestPool.emplace(); - auto* orenNayar = forestPool.deref(orenNayarH); - orenNayar->debugInfo = forestPool.emplace("BxDF Normalized For Whole Sphere"); - auto roughness = orenNayar->ndParams.getRougness(); - roughness[1].scale = roughness[0].scale = 0.8f; - mul->lhs = orenNayarH; - } - // TODO: add a derivative map for testing the printing and compilation - ASSERT_VALUE(forest->addMaterial(layerH,logger),true,"Add Material"); - } - } - - // emitter without IES profile - { - const auto layerH = forestPool.emplace(); - auto* layer = forestPool.deref(layerH); - layer->debugInfo = forestPool.emplace("Twosided Constant Emitter"); - { - const auto mulH = forestPool.emplace(); - auto* mul = forestPool.deref(mulH); - { - const auto emitterH = forestPool.emplace(); - // no profile, unit emission - mul->lhs = emitterH; - } - // we multiply the unit emitter by the value we actually want - { - spectral_var_t::SCreationParams<3> params = {}; - params.getSemantics() = spectral_var_t::Semantics::Fixed3_SRGB; - params.knots.params[0].scale = 3.f; - params.knots.params[1].scale = 7.f; - params.knots.params[2].scale = 15.f; - mul->rhs = forestPool.emplace(std::move(params)); - } - layer->brdfTop = mulH; - layer->brdfBottom = mulH; - } - ASSERT_VALUE(forest->addMaterial(layerH,logger),true,"Add Material"); - } - - // emitter with IES profile - { - const auto layerH = forestPool.emplace(); - auto* layer = forestPool.deref(layerH); - layer->debugInfo = forestPool.emplace("IES Profile Emitter"); - { - const auto mulH = forestPool.emplace(); - auto* mul = forestPool.deref(mulH); - { - const auto emitterH = forestPool.emplace(); - auto* emitter = forestPool.deref(emitterH); - // you should use this to normalize the profile to unit emission over the hemisphere - // so the light gets picked "fairly" - emitter->profile.scale = 0.01f; - emitter->profile.viewChannel = 0; - emitter->profile.view = monochromeImageView; - // these are defaults but going to set them - emitter->profile.sampler.TextureWrapU = ISampler::E_TEXTURE_CLAMP::ETC_REPEAT; - emitter->profile.sampler.TextureWrapV = ISampler::E_TEXTURE_CLAMP::ETC_REPEAT; - // TODO: set transform after merging the OBB PR - //emitter->profileTransform = ; - mul->lhs = emitterH; - } - // we multiply the unit emitter by the emission color value we actually want - { - spectral_var_t::SCreationParams<3> params = {}; - params.getSemantics() = spectral_var_t::Semantics::Fixed3_SRGB; - params.knots.params[0].scale = 60.f; - params.knots.params[1].scale = 90.f; - params.knots.params[2].scale = 45.f; - mul->rhs = forestPool.emplace(std::move(params)); - } - layer->brdfTop = mulH; - } - ASSERT_VALUE(forest->addMaterial(layerH,logger),true,"Add Material"); - } - - // onesided emitter with spatially varying emission from the backside - { - const auto layerH = forestPool.emplace(); - auto* layer = forestPool.deref(layerH); - layer->debugInfo = forestPool.emplace("Spatially Varying Emitter"); - { - const auto mulH = forestPool.emplace(); - auto* mul = forestPool.deref(mulH); - { - const auto emitterH = forestPool.emplace(); - // no profile, unit emission - mul->lhs = emitterH; - } - // we multiply the unit emitter by the value we actually want - { - spectral_var_t::SCreationParams<3> params = {}; - params.getSemantics() = spectral_var_t::Semantics::Fixed3_SRGB; - for (auto c=0; c<3; c++) - { - params.knots.params[c].scale = 4.9f; - params.knots.params[c].viewChannel = c; - params.knots.params[c].view = rgbImageView; - params.knots.params[c].sampler.TextureWrapU = ISampler::E_TEXTURE_CLAMP::ETC_CLAMP_TO_BORDER; - params.knots.params[c].sampler.TextureWrapV = ISampler::E_TEXTURE_CLAMP::ETC_CLAMP_TO_BORDER; - params.knots.params[c].sampler.BorderColor = ISampler::E_TEXTURE_BORDER_COLOR::ETBC_FLOAT_OPAQUE_BLACK; - } - mul->rhs = forestPool.emplace(std::move(params)); - } - layer->brdfBottom = mulH; - } - ASSERT_VALUE(forest->addMaterial(layerH,logger),true,"Add Material"); - } - - // spatially varying emission but with a profile (think classroom projector) - { - const auto layerH = forestPool.emplace(); - auto* layer = forestPool.deref(layerH); - layer->debugInfo = forestPool.emplace("Spatially Varying Emitter with IES profile e.g. Digital Projector"); - { - const auto mulH = forestPool.emplace(); - auto* mul = forestPool.deref(mulH); - { - const auto emitterH = forestPool.emplace(); - auto* emitter = forestPool.deref(emitterH); - emitter->profile.scale = 67.f; - emitter->profile.viewChannel = 0; - emitter->profile.view = monochromeImageView; - // lets try some other samplers - emitter->profile.sampler.TextureWrapU = ISampler::E_TEXTURE_CLAMP::ETC_CLAMP_TO_EDGE; - emitter->profile.sampler.TextureWrapV = ISampler::E_TEXTURE_CLAMP::ETC_CLAMP_TO_EDGE; - // try with default transform - mul->lhs = emitterH; - } - // we multiply the unit emitter by the value we actually want - { - spectral_var_t::SCreationParams<3> params = {}; - params.getSemantics() = spectral_var_t::Semantics::Fixed3_SRGB; - for (auto c=0; c<3; c++) - { - params.knots.params[c].scale = 900.f; // super bright cause its probably small - params.knots.params[c].viewChannel = c; - params.knots.params[c].view = rgbImageView; - params.knots.params[c].sampler.TextureWrapU = ISampler::E_TEXTURE_CLAMP::ETC_CLAMP_TO_BORDER; - params.knots.params[c].sampler.TextureWrapV = ISampler::E_TEXTURE_CLAMP::ETC_CLAMP_TO_BORDER; - params.knots.params[c].sampler.BorderColor = ISampler::E_TEXTURE_BORDER_COLOR::ETBC_FLOAT_OPAQUE_BLACK; - } - mul->rhs = forestPool.emplace(std::move(params)); - } - layer->brdfTop = mulH; - } - ASSERT_VALUE(forest->addMaterial(layerH,logger),true,"Add Material"); - } - - // anisotropic cook torrance GGX with Conductor Fresnel - { - const auto layerH = forestPool.emplace(); - auto* layer = forestPool.deref(layerH); - layer->debugInfo = forestPool.emplace("Anisotropic Aluminium"); - - const auto mulH = forestPool.emplace(); - auto* mul = forestPool.deref(mulH); - // BxDF always goes in left hand side of Mul - { - const auto ctH = forestPool.emplace(); - auto* ct = forestPool.deref(ctH); - ct->debugInfo = forestPool.emplace("First Anisotropic GGX"); - ct->ndParams.getRougness()[0].scale = 0.2f; - ct->ndParams.getRougness()[1].scale = 0.01f; - mul->lhs = ctH; - } - // other multipliers in not-left subtrees - mul->rhs = forest->createNamedFresnel("Al"); - layer->brdfTop = mulH; - - // test that our bad subtree checks by swapping lhs with rhs - std::swap(mul->lhs,mul->rhs); - ASSERT_VALUE(forest->addMaterial(layerH,logger),false,"Contributor not in left subtree check failed"); - - // should work now - std::swap(mul->lhs,mul->rhs); - ASSERT_VALUE(forest->addMaterial(layerH,logger),true,"Contributor in left subtree check failed"); - } - - // dielectric - const auto dielectricH = forestPool.emplace(); - { - auto* mul = forestPool.deref(dielectricH); - // do fresnel first - const auto fresnelH = forest->createNamedFresnel("ThF4"); - auto* fresnel = forestPool.deref(fresnelH); - mul->rhs = fresnelH; - // BxDF always goes in left hand side of Mul - { - const auto ctH = forestPool.emplace(); - auto* ct = forestPool.deref(ctH); - ct->debugInfo = forestPool.emplace("First Isotropic GGX"); - ct->ndParams.getRougness()[0].scale = ct->ndParams.getRougness()[1].scale = 0.05f; - // ignored for BRDFs, needed for BTDFs - ct->orientedRealEta = fresnel->orientedRealEta; - mul->lhs = ctH; - } - } - { - const auto layerH = forestPool.emplace(); - auto* layer = forestPool.deref(layerH); - layer->debugInfo = forestPool.emplace("Glass"); - - // use same BxDF for all parts of a layer - layer->brdfTop = dielectricH; - layer->btdf = dielectricH; - layer->brdfBottom = dielectricH; - - ASSERT_VALUE(forest->addMaterial(layerH,logger),true,"Dielectric"); - } - - // correlated thindielectric (exit through a microfacet with identical normal on the other side - no refraction possible) - { - const auto layerH = forestPool.emplace(); - auto* layer = forestPool.deref(layerH); - layer->debugInfo = forestPool.emplace("Correlated Single Pane"); - - // do fresnel first for all to have the same one - const auto fresnelH = forest->createNamedFresnel("ThF4"); - const auto* fresnel = forestPool.deref(fresnelH); - - const auto ctH = forestPool.emplace(); - { - auto* ct = forestPool.deref(ctH); - ct->ndParams.getRougness()[0].scale = ct->ndParams.getRougness()[1].scale = 0.1f; - // ignored for BRDFs, needed for BTDFs - ct->orientedRealEta = fresnel->orientedRealEta; - layer->brdfTop = forest->createMul(ctH,fresnelH); - } - // the BTDF - { - const auto thinInfiniteScatterH = forestPool.emplace(); - { - auto* thinInfiniteScatter = forestPool.deref(thinInfiniteScatterH); - thinInfiniteScatter->reflectanceTop = fresnelH; - thinInfiniteScatter->reflectanceBottom = fresnelH; - // without extinction - } - layer->btdf = forest->createMul(forestPool.emplace(),thinInfiniteScatterH); - } - // the interface on top as Air->ThF4, we need interface on bottom to be ThF4->Air so reciprocate te Eta - layer->brdfBottom = forest->createMul(forest->reciprocate(ctH)._const_cast(),fresnelH); - - { - auto* imagEta = forestPool.deref(fresnel->orientedImagEta); - imagEta->getParam(0)->scale = std::numeric_limits::min(); - imagEta->getParam(1)->scale = -std::numeric_limits::max(); - imagEta->getParam(2)->scale = 0.5f; - ASSERT_VALUE(forest->addMaterial(layerH,logger),false,"Imaginary Fresnel disallowed"); - for (uint8_t i=0; i<3; i++) - imagEta->getParam(i)->scale = 0.f; - } - - ASSERT_VALUE(forest->addMaterial(layerH,logger),true,"ThinDielectric"); - } - - // complex materials with coatings with IOR 1.5 - { - // make the nodes everyone shares - const auto roughDiffuseH = forestPool.emplace(); - { - auto* mul = forestPool.deref(roughDiffuseH); - { - const auto orenNayarH = forestPool.emplace(); - auto* orenNayar = forestPool.deref(orenNayarH); - orenNayar->ndParams.getRougness()[0].scale = orenNayar->ndParams.getRougness()[1].scale = 0.2f; - mul->lhs = orenNayarH; - } - { - spectral_var_t::SCreationParams<3> params = {}; - params.getSemantics() = spectral_var_t::Semantics::Fixed3_SRGB; - params.knots.params[0].scale = 0.9f; - params.knots.params[1].scale = 0.6f; - params.knots.params[2].scale = 0.01f; - const auto albedoH = forestPool.emplace(std::move(params)); - forestPool.deref(albedoH)->debugInfo = forestPool.emplace("Albedo"); - mul->rhs = albedoH; - } - } - const auto fresnelH = forestPool.emplace(); - { - auto* fresnel = forestPool.deref(fresnelH); - spectral_var_t::SCreationParams<1> params = {}; - params.knots.params[0].scale = 1.5f; - fresnel->orientedRealEta = forestPool.emplace(std::move(params)); - } - // the delta layering should optimize out nicely due to the sampling property - const auto transH = forest->createMul(forestPool.emplace(),fresnelH); - // can't attach a copy of the top layer because we'll have a cycle, also the BRDF needs to be on the other side - const auto bottomH = forestPool.emplace(); - { - auto* bottomLayer = forestPool.deref(bottomH); - bottomLayer->debugInfo = forestPool.emplace("Rough Coating Copy"); - // no brdf on the top of last layer, kill multiscattering - bottomLayer->btdf = transH; - // need the interface to be Air on the bottom, not Glass - bottomLayer->brdfBottom = forest->reciprocate(dielectricH)._const_cast(); - } - - // twosided rough plastic - { - const auto rootH = forestPool.emplace(); - auto* topLayer = forestPool.deref(rootH); - topLayer->debugInfo = forestPool.emplace("Twosided Rough Plastic"); - - topLayer->brdfTop = dielectricH; - topLayer->btdf = transH; - // no brdf on the bottom of first layer, kill multiscattering - - const auto diffuseH = forestPool.emplace(); - topLayer->coated = diffuseH; - { - auto* midLayer = forestPool.deref(diffuseH); - midLayer->brdfTop = roughDiffuseH; - // no transmission in the mid-layer, so the backend needs to decompose into separate front/back materials - midLayer->brdfBottom = roughDiffuseH; - midLayer->coated = bottomH; - } - - ASSERT_VALUE(forest->addMaterial(rootH,logger),true,"Twosided Rough Plastic"); - } - - // Diffuse transmitter normalized to whole sphere - const auto roughDiffTransH = forestPool.emplace(); - { - // normalize the Oren Nayar over the full sphere - auto* mul = forestPool.deref(roughDiffTransH); - mul->lhs = roughDiffuseH; - { - spectral_var_t::SCreationParams<1> params = {}; - params.knots.params[0].scale = 0.5f; - mul->rhs = forestPool.emplace(std::move(params)); - } - } - - // coated diffuse transmitter - { - const auto rootH = forestPool.emplace(); - auto* topLayer = forestPool.deref(rootH); - topLayer->debugInfo = forestPool.emplace("Coated Diffuse Transmitter"); - - topLayer->brdfTop = dielectricH; - topLayer->btdf = transH; - // no brdf on the bottom of first layer, kill multiscattering - - const auto midH = forestPool.emplace(); - auto* midLayer = forestPool.deref(midH); - { - midLayer->brdfTop = roughDiffTransH; - midLayer->btdf = roughDiffTransH; - midLayer->brdfBottom = roughDiffTransH; - - // we could even have a BSDF with a different Roughness on the bottom layer! - midLayer->coated = bottomH; - } - topLayer->coated = midH; - - ASSERT_VALUE(forest->addMaterial(rootH,logger),true,"Coated Diffuse Transmitter"); - } - - // same thing but with subsurface beer absorption - { - const auto rootH = forestPool.emplace(); - auto* topLayer = forestPool.deref(rootH); - topLayer->debugInfo = forestPool.emplace("Coated Diffuse Extinction Transmitter"); -// TODO: triple check this example Material - // we have a choice of where to stick the Beer Absorption: - // - on the BTDF of the outside layer, means that it will be applied to the transmission so twice according to VdotN and LdotN - // (but delta transmission makes special weight nodes behave in a special and only once because `L=-V` is forced in a single scattering) - // - inner layer BRDF or BTDF but thats intractable for most compiler backends because the `L` and `V` in the internal layers are not trivially known - // unless the previous layers are delta distributions (in which case we can equivalently hoist beer to the previous layer). - const auto beerH = forestPool.emplace(); - auto* beer = forestPool.deref(beerH); - { - spectral_var_t::SCreationParams<3> params = {}; - params.getSemantics() = spectral_var_t::Semantics::Fixed3_SRGB; - params.knots.params[0].scale = 0.3f; - params.knots.params[1].scale = 0.9f; - params.knots.params[2].scale = 0.7f; - beer->perpTransmittance = forestPool.emplace(std::move(params)); - } - { - spectral_var_t::SCreationParams<1> params = {}; - params.knots.params[0].scale = 1.f; - beer->thickness = forestPool.emplace(std::move(params)); - } - - topLayer->brdfTop = dielectricH; - // simplest/recommended - { - const auto transAbsorbH = forestPool.emplace(); - auto* transAbsorb = forestPool.deref(transAbsorbH); - transAbsorb->lhs = transH; - { - transAbsorb->rhs = beerH; - } - topLayer->btdf = transAbsorbH; - } - - const auto midH = forestPool.emplace(); - auto* midLayer = forestPool.deref(midH); - { - midLayer->brdfTop = roughDiffTransH; - midLayer->btdf = roughDiffTransH; - // making extra work for our canonicalizer - { - midLayer->brdfBottom = forest->createMul(roughDiffTransH,beerH); - } - - // we could even have a BSDF with a different Fresnel and Roughness on the bottom layer! - midLayer->coated = bottomH; - } - topLayer->coated = midH; - - ASSERT_VALUE(forest->addMaterial(rootH,logger),true,"Coated Diffuse Extinction Transmitter"); - } - } - - smart_refctd_ptr file; - { - m_system->deleteFile(localOutputCWD/"frontend.dot"); - ISystem::future_t> future; - m_system->createFile(future,localOutputCWD/"frontend.dot",IFileBase::E_CREATE_FLAGS::ECF_WRITE); - if (!future.wait()) - return logFail("Failed to Open file for writing"); - future.acquire().move_into(file); - } - if (file) - { - CFrontendIR::SDotPrinter printer = {forest.get(),forest->getMaterials()}; - auto visualization = printer(); - // file write does not take an internal copy of pointer given, need to keep source alive till end - IFile::success_t succ; - file->write(succ,visualization.c_str(),0,visualization.size()); - succ.getBytesProcessed(); - } - } - - // Frontend AST -> IR compilation - { - } - - // Reference Backend Codegen - { - } - - // Compilation from HLSL to SPIR-V just to make sure it works - { - } - - return true; - } - - // One-shot App - bool keepRunning() override { return false; } - - // One-shot App - void workLoopBody() override{} - - // Cleanup - bool onAppTerminated() override - { - return device_base_t::onAppTerminated(); - } -}; - - -NBL_MAIN_FUNC(MaterialCompilerTest) \ No newline at end of file diff --git a/14_Mortons/CMakeLists.txt b/14_Mortons/CMakeLists.txt deleted file mode 100644 index 8229b36b5..000000000 --- a/14_Mortons/CMakeLists.txt +++ /dev/null @@ -1,72 +0,0 @@ -include(common RESULT_VARIABLE RES) -if(NOT RES) - message(FATAL_ERROR "common.cmake not found. Should be in {repo_root}/cmake directory") -endif() - -nbl_create_executable_project("" "" "" "" "${NBL_EXECUTABLE_PROJECT_CREATION_PCH_TARGET}") - -if(NBL_EMBED_BUILTIN_RESOURCES) - set(_BR_TARGET_ ${EXECUTABLE_NAME}_builtinResourceData) - set(RESOURCE_DIR "app_resources") - - get_filename_component(_SEARCH_DIRECTORIES_ "${CMAKE_CURRENT_SOURCE_DIR}" ABSOLUTE) - get_filename_component(_OUTPUT_DIRECTORY_SOURCE_ "${CMAKE_CURRENT_BINARY_DIR}/src" ABSOLUTE) - get_filename_component(_OUTPUT_DIRECTORY_HEADER_ "${CMAKE_CURRENT_BINARY_DIR}/include" ABSOLUTE) - - file(GLOB_RECURSE BUILTIN_RESOURCE_FILES RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}/${RESOURCE_DIR}" CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/${RESOURCE_DIR}/*") - foreach(RES_FILE ${BUILTIN_RESOURCE_FILES}) - LIST_BUILTIN_RESOURCE(RESOURCES_TO_EMBED "${RES_FILE}") - endforeach() - - ADD_CUSTOM_BUILTIN_RESOURCES(${_BR_TARGET_} RESOURCES_TO_EMBED "${_SEARCH_DIRECTORIES_}" "${RESOURCE_DIR}" "nbl::this_example::builtin" "${_OUTPUT_DIRECTORY_HEADER_}" "${_OUTPUT_DIRECTORY_SOURCE_}") - - LINK_BUILTIN_RESOURCES_TO_TARGET(${EXECUTABLE_NAME} ${_BR_TARGET_}) -endif() - -if(MSVC) - target_compile_options("${EXECUTABLE_NAME}" PUBLIC "/fp:strict") -else() - target_compile_options("${EXECUTABLE_NAME}" PUBLIC -ffloat-store -frounding-math -fsignaling-nans -ftrapping-math) -endif() - -set(OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/auto-gen") - -set(SM 6_8) -set(JSON [=[ -[ - { - "INPUT": "app_resources/test.comp.hlsl", - "KEY": "test", - }, - { - "INPUT": "app_resources/test2.comp.hlsl", - "KEY": "test2", - }, -] -]=]) -string(CONFIGURE "${JSON}" JSON) - -set(COMPILE_OPTIONS - -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} -) diff --git a/14_Mortons/CTester.h b/14_Mortons/CTester.h deleted file mode 100644 index 2e81ef564..000000000 --- a/14_Mortons/CTester.h +++ /dev/null @@ -1,493 +0,0 @@ -#ifndef _NBL_EXAMPLES_TESTS_12_MORTON_C_TESTER_INCLUDED_ -#define _NBL_EXAMPLES_TESTS_12_MORTON_C_TESTER_INCLUDED_ - -#include -#include "app_resources/testCommon.hlsl" -#include "app_resources/testCommon2.hlsl" -#include "nbl/examples/Tester/ITester.h" - -using namespace nbl; - -class CTester final : public ITester -{ - using base_t = ITester; - -public: - CTester(const uint32_t testBatchCount) - : base_t(testBatchCount) {}; - -private: - InputTestValues generateInputTestValues() override - { - std::uniform_int_distribution intDistribution(uint32_t(0), std::numeric_limits::max()); - std::uniform_int_distribution longDistribution(uint64_t(0), std::numeric_limits::max()); - - // Set input thest values that will be used in both CPU and GPU tests - InputTestValues testInput; - - testInput.generatedA = longDistribution(getRandomEngine()); - testInput.generatedB = longDistribution(getRandomEngine()); - - uint32_t generatedShift = intDistribution(getRandomEngine()) & uint32_t(63); - testInput.shift = generatedShift; - - testInput.coordX = longDistribution(getRandomEngine()); - testInput.coordY = longDistribution(getRandomEngine()); - testInput.coordZ = longDistribution(getRandomEngine()); - testInput.coordW = longDistribution(getRandomEngine()); - - return testInput; - } - - TestValues determineExpectedResults(const InputTestValues& testInput) override - { - // use std library or glm functions to determine expected test values, the output of functions from intrinsics.hlsl will be verified against these values - TestValues expected; - - { - const uint64_t generatedA = testInput.generatedA; - const uint64_t generatedB = testInput.generatedB; - const uint32_t generatedShift = testInput.shift; - - expected.emulatedAnd = _static_cast(generatedA & generatedB); - expected.emulatedOr = _static_cast(generatedA | generatedB); - expected.emulatedXor = _static_cast(generatedA ^ generatedB); - expected.emulatedNot = _static_cast(~generatedA); - expected.emulatedPlus = _static_cast(generatedA + generatedB); - expected.emulatedMinus = _static_cast(generatedA - generatedB); - expected.emulatedUnaryMinus = _static_cast(-generatedA); - expected.emulatedLess = uint32_t(generatedA < generatedB); - expected.emulatedLessEqual = uint32_t(generatedA <= generatedB); - expected.emulatedGreater = uint32_t(generatedA > generatedB); - expected.emulatedGreaterEqual = uint32_t(generatedA >= generatedB); - - expected.emulatedLeftShifted = _static_cast(generatedA << generatedShift); - expected.emulatedUnsignedRightShifted = _static_cast(generatedA >> generatedShift); - expected.emulatedSignedRightShifted = _static_cast(static_cast(generatedA) >> generatedShift); - } - { - uint64_t2 Vec2A = { testInput.coordX, testInput.coordY }; - uint64_t2 Vec2B = { testInput.coordZ, testInput.coordW }; - - uint16_t2 Vec2ASmall = createAnyBitIntegerVecFromU64Vec(Vec2A); - uint16_t2 Vec2BSmall = createAnyBitIntegerVecFromU64Vec(Vec2B); - uint16_t2 Vec2AMedium = createAnyBitIntegerVecFromU64Vec(Vec2A); - uint16_t2 Vec2BMedium = createAnyBitIntegerVecFromU64Vec(Vec2B); - uint32_t2 Vec2AFull = createAnyBitIntegerVecFromU64Vec(Vec2A); - uint32_t2 Vec2BFull = createAnyBitIntegerVecFromU64Vec(Vec2B); - - uint64_t3 Vec3A = { testInput.coordX, testInput.coordY, testInput.coordZ }; - uint64_t3 Vec3B = { testInput.coordY, testInput.coordZ, testInput.coordW }; - - uint16_t3 Vec3ASmall = createAnyBitIntegerVecFromU64Vec(Vec3A); - uint16_t3 Vec3BSmall = createAnyBitIntegerVecFromU64Vec(Vec3B); - uint16_t3 Vec3AMedium = createAnyBitIntegerVecFromU64Vec(Vec3A); - uint16_t3 Vec3BMedium = createAnyBitIntegerVecFromU64Vec(Vec3B); - uint32_t3 Vec3AFull = createAnyBitIntegerVecFromU64Vec(Vec3A); - uint32_t3 Vec3BFull = createAnyBitIntegerVecFromU64Vec(Vec3B); - - uint64_t4 Vec4A = { testInput.coordX, testInput.coordY, testInput.coordZ, testInput.coordW }; - uint64_t4 Vec4B = { testInput.coordY, testInput.coordZ, testInput.coordW, testInput.coordX }; - - uint16_t4 Vec4ASmall = createAnyBitIntegerVecFromU64Vec(Vec4A); - uint16_t4 Vec4BSmall = createAnyBitIntegerVecFromU64Vec(Vec4B); - uint16_t4 Vec4AMedium = createAnyBitIntegerVecFromU64Vec(Vec4A); - uint16_t4 Vec4BMedium = createAnyBitIntegerVecFromU64Vec(Vec4B); - uint16_t4 Vec4AFull = createAnyBitIntegerVecFromU64Vec(Vec4A); - uint16_t4 Vec4BFull = createAnyBitIntegerVecFromU64Vec(Vec4B); - - // Signed vectors can't just have their highest bits masked off, for them to preserve sign we also need to left shift then right shift them - // so their highest bits are all 0s or 1s depending on the sign of the number they encode - - int16_t2 Vec2ASignedSmall = createAnyBitIntegerVecFromU64Vec(Vec2A); - int16_t2 Vec2BSignedSmall = createAnyBitIntegerVecFromU64Vec(Vec2B); - int16_t2 Vec2ASignedMedium = createAnyBitIntegerVecFromU64Vec(Vec2A); - int16_t2 Vec2BSignedMedium = createAnyBitIntegerVecFromU64Vec(Vec2B); - int32_t2 Vec2ASignedFull = createAnyBitIntegerVecFromU64Vec(Vec2A); - int32_t2 Vec2BSignedFull = createAnyBitIntegerVecFromU64Vec(Vec2B); - - int16_t3 Vec3ASignedSmall = createAnyBitIntegerVecFromU64Vec(Vec3A); - int16_t3 Vec3BSignedSmall = createAnyBitIntegerVecFromU64Vec(Vec3B); - int16_t3 Vec3ASignedMedium = createAnyBitIntegerVecFromU64Vec(Vec3A); - int16_t3 Vec3BSignedMedium = createAnyBitIntegerVecFromU64Vec(Vec3B); - int32_t3 Vec3ASignedFull = createAnyBitIntegerVecFromU64Vec(Vec3A); - int32_t3 Vec3BSignedFull = createAnyBitIntegerVecFromU64Vec(Vec3B); - - int16_t4 Vec4ASignedSmall = createAnyBitIntegerVecFromU64Vec(Vec4A); - int16_t4 Vec4BSignedSmall = createAnyBitIntegerVecFromU64Vec(Vec4B); - int16_t4 Vec4ASignedMedium = createAnyBitIntegerVecFromU64Vec(Vec4A); - int16_t4 Vec4BSignedMedium = createAnyBitIntegerVecFromU64Vec(Vec4B); - int16_t4 Vec4ASignedFull = createAnyBitIntegerVecFromU64Vec(Vec4A); - int16_t4 Vec4BSignedFull = createAnyBitIntegerVecFromU64Vec(Vec4B); - - // Plus - expected.mortonPlus_small_2 = createMortonFromU64Vec(Vec2ASmall + Vec2BSmall); - expected.mortonPlus_medium_2 = createMortonFromU64Vec(Vec2AMedium + Vec2BMedium); - expected.mortonPlus_full_2 = createMortonFromU64Vec(Vec2AFull + Vec2BFull); - expected.mortonPlus_emulated_2 = createMortonFromU64Vec(Vec2AFull + Vec2BFull); - - expected.mortonPlus_small_3 = createMortonFromU64Vec(Vec3ASmall + Vec3BSmall); - expected.mortonPlus_medium_3 = createMortonFromU64Vec(Vec3AMedium + Vec3BMedium); - expected.mortonPlus_full_3 = createMortonFromU64Vec(Vec3AFull + Vec3BFull); - expected.mortonPlus_emulated_3 = createMortonFromU64Vec(Vec3AFull + Vec3BFull); - - expected.mortonPlus_small_4 = createMortonFromU64Vec(Vec4ASmall + Vec4BSmall); - expected.mortonPlus_medium_4 = createMortonFromU64Vec(Vec4AMedium + Vec4BMedium); - expected.mortonPlus_full_4 = createMortonFromU64Vec(Vec4AFull + Vec4BFull); - expected.mortonPlus_emulated_4 = createMortonFromU64Vec(Vec4AFull + Vec4BFull); - - // Minus - expected.mortonMinus_small_2 = createMortonFromU64Vec(Vec2ASmall - Vec2BSmall); - expected.mortonMinus_medium_2 = createMortonFromU64Vec(Vec2AMedium - Vec2BMedium); - expected.mortonMinus_full_2 = createMortonFromU64Vec(Vec2AFull - Vec2BFull); - expected.mortonMinus_emulated_2 = createMortonFromU64Vec(Vec2AFull - Vec2BFull); - - expected.mortonMinus_small_3 = createMortonFromU64Vec(Vec3ASmall - Vec3BSmall); - expected.mortonMinus_medium_3 = createMortonFromU64Vec(Vec3AMedium - Vec3BMedium); - expected.mortonMinus_full_3 = createMortonFromU64Vec(Vec3AFull - Vec3BFull); - expected.mortonMinus_emulated_3 = createMortonFromU64Vec(Vec3AFull - Vec3BFull); - - expected.mortonMinus_small_4 = createMortonFromU64Vec(Vec4ASmall - Vec4BSmall); - expected.mortonMinus_medium_4 = createMortonFromU64Vec(Vec4AMedium - Vec4BMedium); - expected.mortonMinus_full_4 = createMortonFromU64Vec(Vec4AFull - Vec4BFull); - expected.mortonMinus_emulated_4 = createMortonFromU64Vec(Vec4AFull - Vec4BFull); - - // Coordinate-wise equality - expected.mortonEqual_small_2 = uint32_t2(glm::equal(Vec2ASmall, Vec2BSmall)); - expected.mortonEqual_medium_2 = uint32_t2(glm::equal(Vec2AMedium, Vec2BMedium)); - expected.mortonEqual_full_2 = uint32_t2(glm::equal(Vec2AFull, Vec2BFull)); - expected.mortonEqual_emulated_2 = uint32_t2(glm::equal(Vec2AFull, Vec2BFull)); - - expected.mortonEqual_small_3 = uint32_t3(glm::equal(Vec3ASmall, Vec3BSmall)); - expected.mortonEqual_medium_3 = uint32_t3(glm::equal(Vec3AMedium, Vec3BMedium)); - expected.mortonEqual_full_3 = uint32_t3(glm::equal(Vec3AFull, Vec3BFull)); - expected.mortonEqual_emulated_3 = uint32_t3(glm::equal(Vec3AFull, Vec3BFull)); - - expected.mortonEqual_small_4 = uint32_t4(glm::equal(Vec4ASmall, Vec4BSmall)); - expected.mortonEqual_medium_4 = uint32_t4(glm::equal(Vec4AMedium, Vec4BMedium)); - expected.mortonEqual_full_4 = uint32_t4(glm::equal(Vec4AFull, Vec4BFull)); - expected.mortonEqual_emulated_4 = uint32_t4(glm::equal(Vec4AFull, Vec4BFull)); - - // Coordinate-wise unsigned inequality (just testing with less) - expected.mortonUnsignedLess_small_2 = uint32_t2(glm::lessThan(Vec2ASmall, Vec2BSmall)); - expected.mortonUnsignedLess_medium_2 = uint32_t2(glm::lessThan(Vec2AMedium, Vec2BMedium)); - expected.mortonUnsignedLess_full_2 = uint32_t2(glm::lessThan(Vec2AFull, Vec2BFull)); - expected.mortonUnsignedLess_emulated_2 = uint32_t2(glm::lessThan(Vec2AFull, Vec2BFull)); - - expected.mortonUnsignedLess_small_3 = uint32_t3(glm::lessThan(Vec3ASmall, Vec3BSmall)); - expected.mortonUnsignedLess_medium_3 = uint32_t3(glm::lessThan(Vec3AMedium, Vec3BMedium)); - expected.mortonUnsignedLess_full_3 = uint32_t3(glm::lessThan(Vec3AFull, Vec3BFull)); - expected.mortonUnsignedLess_emulated_3 = uint32_t3(glm::lessThan(Vec3AFull, Vec3BFull)); - - expected.mortonUnsignedLess_small_4 = uint32_t4(glm::lessThan(Vec4ASmall, Vec4BSmall)); - expected.mortonUnsignedLess_medium_4 = uint32_t4(glm::lessThan(Vec4AMedium, Vec4BMedium)); - expected.mortonUnsignedLess_full_4 = uint32_t4(glm::lessThan(Vec4AFull, Vec4BFull)); - expected.mortonUnsignedLess_emulated_4 = uint32_t4(glm::lessThan(Vec4AFull, Vec4BFull)); - - // Coordinate-wise signed inequality - expected.mortonSignedLess_small_2 = uint32_t2(glm::lessThan(Vec2ASignedSmall, Vec2BSignedSmall)); - expected.mortonSignedLess_medium_2 = uint32_t2(glm::lessThan(Vec2ASignedMedium, Vec2BSignedMedium)); - expected.mortonSignedLess_full_2 = uint32_t2(glm::lessThan(Vec2ASignedFull, Vec2BSignedFull)); - expected.mortonSignedLess_emulated_2 = uint32_t2(glm::lessThan(Vec2ASignedFull, Vec2BSignedFull)); - - expected.mortonSignedLess_small_3 = uint32_t3(glm::lessThan(Vec3ASignedSmall, Vec3BSignedSmall)); - expected.mortonSignedLess_medium_3 = uint32_t3(glm::lessThan(Vec3ASignedMedium, Vec3BSignedMedium)); - expected.mortonSignedLess_full_3 = uint32_t3(glm::lessThan(Vec3ASignedFull, Vec3BSignedFull)); - expected.mortonSignedLess_emulated_3 = uint32_t3(glm::lessThan(Vec3ASignedFull, Vec3BSignedFull)); - - expected.mortonSignedLess_small_4 = uint32_t4(glm::lessThan(Vec4ASignedSmall, Vec4BSignedSmall)); - expected.mortonSignedLess_medium_4 = uint32_t4(glm::lessThan(Vec4ASignedMedium, Vec4BSignedMedium)); - expected.mortonSignedLess_full_4 = uint32_t4(glm::lessThan(Vec4ASignedFull, Vec4BSignedFull)); - expected.mortonSignedLess_emulated_4 = uint32_t4(glm::lessThan(Vec4ASignedFull, Vec4BSignedFull)); - - uint16_t castedShift = uint16_t(testInput.shift); - // Left-shift - expected.mortonLeftShift_small_2 = createMortonFromU64Vec(Vec2ASmall << uint16_t(castedShift % smallBits_2)); - expected.mortonLeftShift_medium_2 = createMortonFromU64Vec(Vec2AMedium << uint16_t(castedShift % mediumBits_2)); - expected.mortonLeftShift_full_2 = createMortonFromU64Vec(Vec2AFull << uint32_t(castedShift % fullBits_2)); - expected.mortonLeftShift_emulated_2 = createMortonFromU64Vec(Vec2AFull << uint32_t(castedShift % fullBits_2)); - - expected.mortonLeftShift_small_3 = createMortonFromU64Vec(Vec3ASmall << uint16_t(castedShift % smallBits_3)); - expected.mortonLeftShift_medium_3 = createMortonFromU64Vec(Vec3AMedium << uint16_t(castedShift % mediumBits_3)); - expected.mortonLeftShift_full_3 = createMortonFromU64Vec(Vec3AFull << uint32_t(castedShift % fullBits_3)); - expected.mortonLeftShift_emulated_3 = createMortonFromU64Vec(Vec3AFull << uint32_t(castedShift % fullBits_3)); - - expected.mortonLeftShift_small_4 = createMortonFromU64Vec(Vec4ASmall << uint16_t(castedShift % smallBits_4)); - expected.mortonLeftShift_medium_4 = createMortonFromU64Vec(Vec4AMedium << uint16_t(castedShift % mediumBits_4)); - expected.mortonLeftShift_full_4 = createMortonFromU64Vec(Vec4AFull << uint16_t(castedShift % fullBits_4)); - expected.mortonLeftShift_emulated_4 = createMortonFromU64Vec(Vec4AFull << uint16_t(castedShift % fullBits_4)); - - // Unsigned right-shift - expected.mortonUnsignedRightShift_small_2 = morton::code::create(Vec2ASmall >> uint16_t(castedShift % smallBits_2)); - expected.mortonUnsignedRightShift_medium_2 = morton::code::create(Vec2AMedium >> uint16_t(castedShift % mediumBits_2)); - expected.mortonUnsignedRightShift_full_2 = morton::code::create(Vec2AFull >> uint32_t(castedShift % fullBits_2)); - expected.mortonUnsignedRightShift_emulated_2 = morton::code::create(Vec2AFull >> uint32_t(castedShift % fullBits_2)); - - expected.mortonUnsignedRightShift_small_3 = morton::code::create(Vec3ASmall >> uint16_t(castedShift % smallBits_3)); - expected.mortonUnsignedRightShift_medium_3 = morton::code::create(Vec3AMedium >> uint16_t(castedShift % mediumBits_3)); - expected.mortonUnsignedRightShift_full_3 = morton::code::create(Vec3AFull >> uint32_t(castedShift % fullBits_3)); - expected.mortonUnsignedRightShift_emulated_3 = morton::code::create(Vec3AFull >> uint32_t(castedShift % fullBits_3)); - - expected.mortonUnsignedRightShift_small_4 = morton::code::create(Vec4ASmall >> uint16_t(castedShift % smallBits_4)); - expected.mortonUnsignedRightShift_medium_4 = morton::code::create(Vec4AMedium >> uint16_t(castedShift % mediumBits_4)); - expected.mortonUnsignedRightShift_full_4 = morton::code::create(Vec4AFull >> uint16_t(castedShift % fullBits_4)); - expected.mortonUnsignedRightShift_emulated_4 = morton::code::create(Vec4AFull >> uint16_t(castedShift % fullBits_4)); - - // Signed right-shift - expected.mortonSignedRightShift_small_2 = morton::code::create(Vec2ASignedSmall >> int16_t(castedShift % smallBits_2)); - expected.mortonSignedRightShift_medium_2 = morton::code::create(Vec2ASignedMedium >> int16_t(castedShift % mediumBits_2)); - expected.mortonSignedRightShift_full_2 = morton::code::create(Vec2ASignedFull >> int32_t(castedShift % fullBits_2)); - expected.mortonSignedRightShift_emulated_2 = createMortonFromU64Vec(Vec2ASignedFull >> int32_t(castedShift % fullBits_2)); - - expected.mortonSignedRightShift_small_3 = morton::code::create(Vec3ASignedSmall >> int16_t(castedShift % smallBits_3)); - expected.mortonSignedRightShift_medium_3 = morton::code::create(Vec3ASignedMedium >> int16_t(castedShift % mediumBits_3)); - expected.mortonSignedRightShift_full_3 = morton::code::create(Vec3ASignedFull >> int32_t(castedShift % fullBits_3)); - expected.mortonSignedRightShift_emulated_3 = createMortonFromU64Vec(Vec3ASignedFull >> int32_t(castedShift % fullBits_3)); - - expected.mortonSignedRightShift_small_4 = morton::code::create(Vec4ASignedSmall >> int16_t(castedShift % smallBits_4)); - expected.mortonSignedRightShift_medium_4 = morton::code::create(Vec4ASignedMedium >> int16_t(castedShift % mediumBits_4)); - expected.mortonSignedRightShift_full_4 = morton::code::create(Vec4ASignedFull >> int16_t(castedShift % fullBits_4)); - expected.mortonSignedRightShift_emulated_4 = createMortonFromU64Vec(Vec4ASignedFull >> int16_t(castedShift % fullBits_4)); - } - - return expected; - } - - bool verifyTestResults(const TestValues& expectedTestValues, const TestValues& testValues, const size_t testIteration, const uint32_t seed, ITester::TestType testType) override - { - bool pass = true; - // Some verification is commented out and moved to CTester2 due to bug in dxc. Uncomment them when the bug is fixed. - pass &= verifyTestValue("emulatedAnd", expectedTestValues.emulatedAnd, testValues.emulatedAnd, testIteration, seed, testType); - pass &= verifyTestValue("emulatedOr", expectedTestValues.emulatedOr, testValues.emulatedOr, testIteration, seed, testType); - pass &= verifyTestValue("emulatedXor", expectedTestValues.emulatedXor, testValues.emulatedXor, testIteration, seed, testType); - pass &= verifyTestValue("emulatedNot", expectedTestValues.emulatedNot, testValues.emulatedNot, testIteration, seed, testType); - pass &= verifyTestValue("emulatedPlus", expectedTestValues.emulatedPlus, testValues.emulatedPlus, testIteration, seed, testType); - pass &= verifyTestValue("emulatedMinus", expectedTestValues.emulatedMinus, testValues.emulatedMinus, testIteration, seed, testType); - pass &= verifyTestValue("emulatedLess", expectedTestValues.emulatedLess, testValues.emulatedLess, testIteration, seed, testType); - pass &= verifyTestValue("emulatedLessEqual", expectedTestValues.emulatedLessEqual, testValues.emulatedLessEqual, testIteration, seed, testType); - pass &= verifyTestValue("emulatedGreater", expectedTestValues.emulatedGreater, testValues.emulatedGreater, testIteration, seed, testType); - pass &= verifyTestValue("emulatedGreaterEqual", expectedTestValues.emulatedGreaterEqual, testValues.emulatedGreaterEqual, testIteration, seed, testType); - pass &= verifyTestValue("emulatedLeftShifted", expectedTestValues.emulatedLeftShifted, testValues.emulatedLeftShifted, testIteration, seed, testType); - pass &= verifyTestValue("emulatedUnsignedRightShifted", expectedTestValues.emulatedUnsignedRightShifted, testValues.emulatedUnsignedRightShifted, testIteration, seed, testType); - pass &= verifyTestValue("emulatedSignedRightShifted", expectedTestValues.emulatedSignedRightShifted, testValues.emulatedSignedRightShifted, testIteration, seed, testType); - pass &= verifyTestValue("emulatedUnaryMinus", expectedTestValues.emulatedUnaryMinus, testValues.emulatedUnaryMinus, testIteration, seed, testType); - - // Morton Plus - pass &= verifyTestValue("mortonPlus_small_2", expectedTestValues.mortonPlus_small_2, testValues.mortonPlus_small_2, testIteration, seed, testType); - pass &= verifyTestValue("mortonPlus_medium_2", expectedTestValues.mortonPlus_medium_2, testValues.mortonPlus_medium_2, testIteration, seed, testType); - pass &= verifyTestValue("mortonPlus_full_2", expectedTestValues.mortonPlus_full_2, testValues.mortonPlus_full_2, testIteration, seed, testType); - pass &= verifyTestValue("mortonPlus_emulated_2", expectedTestValues.mortonPlus_emulated_2, testValues.mortonPlus_emulated_2, testIteration, seed, testType); - - pass &= verifyTestValue("mortonPlus_small_3", expectedTestValues.mortonPlus_small_3, testValues.mortonPlus_small_3, testIteration, seed, testType); - pass &= verifyTestValue("mortonPlus_medium_3", expectedTestValues.mortonPlus_medium_3, testValues.mortonPlus_medium_3, testIteration, seed, testType); - pass &= verifyTestValue("mortonPlus_full_3", expectedTestValues.mortonPlus_full_3, testValues.mortonPlus_full_3, testIteration, seed, testType); - pass &= verifyTestValue("mortonPlus_emulated_3", expectedTestValues.mortonPlus_emulated_3, testValues.mortonPlus_emulated_3, testIteration, seed, testType); - - pass &= verifyTestValue("mortonPlus_small_4", expectedTestValues.mortonPlus_small_4, testValues.mortonPlus_small_4, testIteration, seed, testType); - pass &= verifyTestValue("mortonPlus_medium_4", expectedTestValues.mortonPlus_medium_4, testValues.mortonPlus_medium_4, testIteration, seed, testType); - pass &= verifyTestValue("mortonPlus_full_4", expectedTestValues.mortonPlus_full_4, testValues.mortonPlus_full_4, testIteration, seed, testType); - pass &= verifyTestValue("mortonPlus_emulated_4", expectedTestValues.mortonPlus_emulated_4, testValues.mortonPlus_emulated_4, testIteration, seed, testType); - - // Morton Minus - pass &= verifyTestValue("mortonMinus_small_2", expectedTestValues.mortonMinus_small_2, testValues.mortonMinus_small_2, testIteration, seed, testType); - pass &= verifyTestValue("mortonMinus_medium_2", expectedTestValues.mortonMinus_medium_2, testValues.mortonMinus_medium_2, testIteration, seed, testType); - pass &= verifyTestValue("mortonMinus_full_2", expectedTestValues.mortonMinus_full_2, testValues.mortonMinus_full_2, testIteration, seed, testType); - pass &= verifyTestValue("mortonMinus_emulated_2", expectedTestValues.mortonMinus_emulated_2, testValues.mortonMinus_emulated_2, testIteration, seed, testType); - - pass &= verifyTestValue("mortonMinus_small_3", expectedTestValues.mortonMinus_small_3, testValues.mortonMinus_small_3, testIteration, seed, testType); - pass &= verifyTestValue("mortonMinus_medium_3", expectedTestValues.mortonMinus_medium_3, testValues.mortonMinus_medium_3, testIteration, seed, testType); - pass &= verifyTestValue("mortonMinus_full_3", expectedTestValues.mortonMinus_full_3, testValues.mortonMinus_full_3, testIteration, seed, testType); - pass &= verifyTestValue("mortonMinus_emulated_3", expectedTestValues.mortonMinus_emulated_3, testValues.mortonMinus_emulated_3, testIteration, seed, testType); - - pass &= verifyTestValue("mortonMinus_small_4", expectedTestValues.mortonMinus_small_4, testValues.mortonMinus_small_4, testIteration, seed, testType); - pass &= verifyTestValue("mortonMinus_medium_4", expectedTestValues.mortonMinus_medium_4, testValues.mortonMinus_medium_4, testIteration, seed, testType); - pass &= verifyTestValue("mortonMinus_full_4", expectedTestValues.mortonMinus_full_4, testValues.mortonMinus_full_4, testIteration, seed, testType); - pass &= verifyTestValue("mortonMinus_emulated_4", expectedTestValues.mortonMinus_emulated_4, testValues.mortonMinus_emulated_4, testIteration, seed, testType); - - // Morton coordinate-wise equality - pass &= verifyTestValue("mortonEqual_small_2", expectedTestValues.mortonEqual_small_2, testValues.mortonEqual_small_2, testIteration, seed, testType); - pass &= verifyTestValue("mortonEqual_medium_2", expectedTestValues.mortonEqual_medium_2, testValues.mortonEqual_medium_2, testIteration, seed, testType); - pass &= verifyTestValue("mortonEqual_full_2", expectedTestValues.mortonEqual_full_2, testValues.mortonEqual_full_2, testIteration, seed, testType); - pass &= verifyTestValue("mortonEqual_emulated_2", expectedTestValues.mortonEqual_emulated_2, testValues.mortonEqual_emulated_2, testIteration, seed, testType); - - pass &= verifyTestValue("mortonEqual_small_3", expectedTestValues.mortonEqual_small_3, testValues.mortonEqual_small_3, testIteration, seed, testType); - pass &= verifyTestValue("mortonEqual_medium_3", expectedTestValues.mortonEqual_medium_3, testValues.mortonEqual_medium_3, testIteration, seed, testType); - pass &= verifyTestValue("mortonEqual_full_3", expectedTestValues.mortonEqual_full_3, testValues.mortonEqual_full_3, testIteration, seed, testType); - pass &= verifyTestValue("mortonEqual_emulated_3", expectedTestValues.mortonEqual_emulated_3, testValues.mortonEqual_emulated_3, testIteration, seed, testType); - - pass &= verifyTestValue("mortonEqual_small_4", expectedTestValues.mortonEqual_small_4, testValues.mortonEqual_small_4, testIteration, seed, testType); - pass &= verifyTestValue("mortonEqual_medium_4", expectedTestValues.mortonEqual_medium_4, testValues.mortonEqual_medium_4, testIteration, seed, testType); - pass &= verifyTestValue("mortonEqual_full_4", expectedTestValues.mortonEqual_full_4, testValues.mortonEqual_full_4, testIteration, seed, testType); - pass &= verifyTestValue("mortonEqual_emulated_4", expectedTestValues.mortonEqual_emulated_4, testValues.mortonEqual_emulated_4, testIteration, seed, testType); - - // Morton coordinate-wise unsigned inequality - pass &= verifyTestValue("mortonUnsignedLess_small_2", expectedTestValues.mortonUnsignedLess_small_2, testValues.mortonUnsignedLess_small_2, testIteration, seed, testType); - pass &= verifyTestValue("mortonUnsignedLess_medium_2", expectedTestValues.mortonUnsignedLess_medium_2, testValues.mortonUnsignedLess_medium_2, testIteration, seed, testType); - pass &= verifyTestValue("mortonUnsignedLess_full_2", expectedTestValues.mortonUnsignedLess_full_2, testValues.mortonUnsignedLess_full_2, testIteration, seed, testType); - pass &= verifyTestValue("mortonUnsignedLess_emulated_2", expectedTestValues.mortonUnsignedLess_emulated_2, testValues.mortonUnsignedLess_emulated_2, testIteration, seed, testType); - - pass &= verifyTestValue("mortonUnsignedLess_small_3", expectedTestValues.mortonUnsignedLess_small_3, testValues.mortonUnsignedLess_small_3, testIteration, seed, testType); - pass &= verifyTestValue("mortonUnsignedLess_medium_3", expectedTestValues.mortonUnsignedLess_medium_3, testValues.mortonUnsignedLess_medium_3, testIteration, seed, testType); - pass &= verifyTestValue("mortonUnsignedLess_full_3", expectedTestValues.mortonUnsignedLess_full_3, testValues.mortonUnsignedLess_full_3, testIteration, seed, testType); - pass &= verifyTestValue("mortonUnsignedLess_emulated_3", expectedTestValues.mortonUnsignedLess_emulated_3, testValues.mortonUnsignedLess_emulated_3, testIteration, seed, testType); - - pass &= verifyTestValue("mortonUnsignedLess_small_4", expectedTestValues.mortonUnsignedLess_small_4, testValues.mortonUnsignedLess_small_4, testIteration, seed, testType); - pass &= verifyTestValue("mortonUnsignedLess_medium_4", expectedTestValues.mortonUnsignedLess_medium_4, testValues.mortonUnsignedLess_medium_4, testIteration, seed, testType); - pass &= verifyTestValue("mortonUnsignedLess_full_4", expectedTestValues.mortonUnsignedLess_full_4, testValues.mortonUnsignedLess_full_4, testIteration, seed, testType); - // verifyTestValue("mortonUnsignedLess_emulated_4", expectedTestValues.mortonUnsignedLess_emulated_4, testValues.mortonUnsignedLess_emulated_4, testIteration, seed, testType); - - // Morton coordinate-wise signed inequality - pass &= verifyTestValue("mortonSignedLess_small_2", expectedTestValues.mortonSignedLess_small_2, testValues.mortonSignedLess_small_2, testIteration, seed, testType); - pass &= verifyTestValue("mortonSignedLess_medium_2", expectedTestValues.mortonSignedLess_medium_2, testValues.mortonSignedLess_medium_2, testIteration, seed, testType); - pass &= verifyTestValue("mortonSignedLess_full_2", expectedTestValues.mortonSignedLess_full_2, testValues.mortonSignedLess_full_2, testIteration, seed, testType); - // verifyTestValue("mortonSignedLess_emulated_2", expectedTestValues.mortonSignedLess_emulated_2, testValues.mortonSignedLess_emulated_2, testIteration, seed, testType); - - pass &= verifyTestValue("mortonSignedLess_small_3", expectedTestValues.mortonSignedLess_small_3, testValues.mortonSignedLess_small_3, testIteration, seed, testType); - pass &= verifyTestValue("mortonSignedLess_medium_3", expectedTestValues.mortonSignedLess_medium_3, testValues.mortonSignedLess_medium_3, testIteration, seed, testType); - pass &= verifyTestValue("mortonSignedLess_full_3", expectedTestValues.mortonSignedLess_full_3, testValues.mortonSignedLess_full_3, testIteration, seed, testType); - // verifyTestValue("mortonSignedLess_emulated_3", expectedTestValues.mortonSignedLess_emulated_3, testValues.mortonSignedLess_emulated_3, testIteration, seed, testType); - - pass &= verifyTestValue("mortonSignedLess_small_4", expectedTestValues.mortonSignedLess_small_4, testValues.mortonSignedLess_small_4, testIteration, seed, testType); - pass &= verifyTestValue("mortonSignedLess_medium_4", expectedTestValues.mortonSignedLess_medium_4, testValues.mortonSignedLess_medium_4, testIteration, seed, testType); - pass &= verifyTestValue("mortonSignedLess_full_4", expectedTestValues.mortonSignedLess_full_4, testValues.mortonSignedLess_full_4, testIteration, seed, testType); - // verifyTestValue("mortonSignedLess_emulated_4", expectedTestValues.mortonSignedLess_emulated_4, testValues.mortonSignedLess_emulated_4, testIteration, seed, testType); - - // Morton left-shift - pass &= verifyTestValue("mortonLeftShift_small_2", expectedTestValues.mortonLeftShift_small_2, testValues.mortonLeftShift_small_2, testIteration, seed, testType); - pass &= verifyTestValue("mortonLeftShift_medium_2", expectedTestValues.mortonLeftShift_medium_2, testValues.mortonLeftShift_medium_2, testIteration, seed, testType); - pass &= verifyTestValue("mortonLeftShift_full_2", expectedTestValues.mortonLeftShift_full_2, testValues.mortonLeftShift_full_2, testIteration, seed, testType); - pass &= verifyTestValue("mortonLeftShift_emulated_2", expectedTestValues.mortonLeftShift_emulated_2, testValues.mortonLeftShift_emulated_2, testIteration, seed, testType); - - pass &= verifyTestValue("mortonLeftShift_small_3", expectedTestValues.mortonLeftShift_small_3, testValues.mortonLeftShift_small_3, testIteration, seed, testType); - pass &= verifyTestValue("mortonLeftShift_medium_3", expectedTestValues.mortonLeftShift_medium_3, testValues.mortonLeftShift_medium_3, testIteration, seed, testType); - pass &= verifyTestValue("mortonLeftShift_full_3", expectedTestValues.mortonLeftShift_full_3, testValues.mortonLeftShift_full_3, testIteration, seed, testType); - pass &= verifyTestValue("mortonLeftShift_emulated_3", expectedTestValues.mortonLeftShift_emulated_3, testValues.mortonLeftShift_emulated_3, testIteration, seed, testType); - - pass &= verifyTestValue("mortonLeftShift_small_4", expectedTestValues.mortonLeftShift_small_4, testValues.mortonLeftShift_small_4, testIteration, seed, testType); - pass &= verifyTestValue("mortonLeftShift_medium_4", expectedTestValues.mortonLeftShift_medium_4, testValues.mortonLeftShift_medium_4, testIteration, seed, testType); - pass &= verifyTestValue("mortonLeftShift_full_4", expectedTestValues.mortonLeftShift_full_4, testValues.mortonLeftShift_full_4, testIteration, seed, testType); - pass &= verifyTestValue("mortonLeftShift_emulated_4", expectedTestValues.mortonLeftShift_emulated_4, testValues.mortonLeftShift_emulated_4, testIteration, seed, testType); - - // Morton unsigned right-shift - pass &= verifyTestValue("mortonUnsignedRightShift_small_2", expectedTestValues.mortonUnsignedRightShift_small_2, testValues.mortonUnsignedRightShift_small_2, testIteration, seed, testType); - pass &= verifyTestValue("mortonUnsignedRightShift_medium_2", expectedTestValues.mortonUnsignedRightShift_medium_2, testValues.mortonUnsignedRightShift_medium_2, testIteration, seed, testType); - pass &= verifyTestValue("mortonUnsignedRightShift_full_2", expectedTestValues.mortonUnsignedRightShift_full_2, testValues.mortonUnsignedRightShift_full_2, testIteration, seed, testType); - pass &= verifyTestValue("mortonUnsignedRightShift_emulated_2", expectedTestValues.mortonUnsignedRightShift_emulated_2, testValues.mortonUnsignedRightShift_emulated_2, testIteration, seed, testType); - - pass &= verifyTestValue("mortonUnsignedRightShift_small_3", expectedTestValues.mortonUnsignedRightShift_small_3, testValues.mortonUnsignedRightShift_small_3, testIteration, seed, testType); - pass &= verifyTestValue("mortonUnsignedRightShift_medium_3", expectedTestValues.mortonUnsignedRightShift_medium_3, testValues.mortonUnsignedRightShift_medium_3, testIteration, seed, testType); - pass &= verifyTestValue("mortonUnsignedRightShift_full_3", expectedTestValues.mortonUnsignedRightShift_full_3, testValues.mortonUnsignedRightShift_full_3, testIteration, seed, testType); - pass &= verifyTestValue("mortonUnsignedRightShift_emulated_3", expectedTestValues.mortonUnsignedRightShift_emulated_3, testValues.mortonUnsignedRightShift_emulated_3, testIteration, seed, testType); - - pass &= verifyTestValue("mortonUnsignedRightShift_small_4", expectedTestValues.mortonUnsignedRightShift_small_4, testValues.mortonUnsignedRightShift_small_4, testIteration, seed, testType); - pass &= verifyTestValue("mortonUnsignedRightShift_medium_4", expectedTestValues.mortonUnsignedRightShift_medium_4, testValues.mortonUnsignedRightShift_medium_4, testIteration, seed, testType); - pass &= verifyTestValue("mortonUnsignedRightShift_full_4", expectedTestValues.mortonUnsignedRightShift_full_4, testValues.mortonUnsignedRightShift_full_4, testIteration, seed, testType); - pass &= verifyTestValue("mortonUnsignedRightShift_emulated_4", expectedTestValues.mortonUnsignedRightShift_emulated_4, testValues.mortonUnsignedRightShift_emulated_4, testIteration, seed, testType); - - // Morton signed right-shift - pass &= verifyTestValue("mortonSignedRightShift_small_2", expectedTestValues.mortonSignedRightShift_small_2, testValues.mortonSignedRightShift_small_2, testIteration, seed, testType); - pass &= verifyTestValue("mortonSignedRightShift_medium_2", expectedTestValues.mortonSignedRightShift_medium_2, testValues.mortonSignedRightShift_medium_2, testIteration, seed, testType); - pass &= verifyTestValue("mortonSignedRightShift_full_2", expectedTestValues.mortonSignedRightShift_full_2, testValues.mortonSignedRightShift_full_2, testIteration, seed, testType); - // verifyTestValue("mortonSignedRightShift_emulated_2", expectedTestValues.mortonSignedRightShift_emulated_2, testValues.mortonSignedRightShift_emulated_2, testIteration, seed, testType); - - pass &= verifyTestValue("mortonSignedRightShift_small_3", expectedTestValues.mortonSignedRightShift_small_3, testValues.mortonSignedRightShift_small_3, testIteration, seed, testType); - pass &= verifyTestValue("mortonSignedRightShift_medium_3", expectedTestValues.mortonSignedRightShift_medium_3, testValues.mortonSignedRightShift_medium_3, testIteration, seed, testType); - pass &= verifyTestValue("mortonSignedRightShift_full_3", expectedTestValues.mortonSignedRightShift_full_3, testValues.mortonSignedRightShift_full_3, testIteration, seed, testType); - //verifyTestValue("mortonSignedRightShift_emulated_3", expectedTestValues.mortonSignedRightShift_emulated_3, testValues.mortonSignedRightShift_emulated_3, testIteration, seed, testType); - - pass &= verifyTestValue("mortonSignedRightShift_small_4", expectedTestValues.mortonSignedRightShift_small_4, testValues.mortonSignedRightShift_small_4, testIteration, seed, testType); - pass &= verifyTestValue("mortonSignedRightShift_medium_4", expectedTestValues.mortonSignedRightShift_medium_4, testValues.mortonSignedRightShift_medium_4, testIteration, seed, testType); - pass &= verifyTestValue("mortonSignedRightShift_full_4", expectedTestValues.mortonSignedRightShift_full_4, testValues.mortonSignedRightShift_full_4, testIteration, seed, testType); - // verifyTestValue("mortonSignedRightShift_emulated_4", expectedTestValues.mortonSignedRightShift_emulated_4, testValues.mortonSignedRightShift_emulated_4, testIteration, seed, testType); - return pass; - } -}; - -// Some hlsl code will result in compilation error if mixed together due to some bug in dxc. So we separate them into multiple shader compilation and test. -class CTester2 final : public ITester -{ - using base_t = ITester; -public: - CTester2(const uint32_t testBatchCount) - : base_t(testBatchCount) {}; - -private: - InputTestValues generateInputTestValues() override - { - std::uniform_int_distribution intDistribution(uint32_t(0), std::numeric_limits::max()); - std::uniform_int_distribution longDistribution(uint64_t(0), std::numeric_limits::max()); - - // Set input thest values that will be used in both CPU and GPU tests - InputTestValues testInput; - - testInput.generatedA = longDistribution(getRandomEngine()); - testInput.generatedB = longDistribution(getRandomEngine()); - - uint32_t generatedShift = intDistribution(getRandomEngine()) & uint32_t(63); - testInput.shift = generatedShift; - - testInput.coordX = longDistribution(getRandomEngine()); - testInput.coordY = longDistribution(getRandomEngine()); - testInput.coordZ = longDistribution(getRandomEngine()); - testInput.coordW = longDistribution(getRandomEngine()); - - return testInput; - } - - TestValues determineExpectedResults(const InputTestValues& testInput) override - { - // use std library or glm functions to determine expected test values, the output of functions from intrinsics.hlsl will be verified against these values - TestValues expected; - - const uint32_t generatedShift = testInput.shift; - uint64_t2 Vec2A = { testInput.coordX, testInput.coordY }; - uint64_t2 Vec2B = { testInput.coordZ, testInput.coordW }; - - uint64_t3 Vec3A = { testInput.coordX, testInput.coordY, testInput.coordZ }; - uint64_t3 Vec3B = { testInput.coordY, testInput.coordZ, testInput.coordW }; - - uint64_t4 Vec4A = { testInput.coordX, testInput.coordY, testInput.coordZ, testInput.coordW }; - uint64_t4 Vec4B = { testInput.coordY, testInput.coordZ, testInput.coordW, testInput.coordX }; - - uint16_t4 Vec4AFull = createAnyBitIntegerVecFromU64Vec(Vec4A); - uint16_t4 Vec4BFull = createAnyBitIntegerVecFromU64Vec(Vec4B); - - int32_t2 Vec2ASignedFull = createAnyBitIntegerVecFromU64Vec(Vec2A); - int32_t2 Vec2BSignedFull = createAnyBitIntegerVecFromU64Vec(Vec2B); - - int32_t3 Vec3ASignedFull = createAnyBitIntegerVecFromU64Vec(Vec3A); - int32_t3 Vec3BSignedFull = createAnyBitIntegerVecFromU64Vec(Vec3B); - - int16_t4 Vec4ASignedFull = createAnyBitIntegerVecFromU64Vec(Vec4A); - int16_t4 Vec4BSignedFull = createAnyBitIntegerVecFromU64Vec(Vec4B); - - expected.mortonUnsignedLess_emulated_4 = uint32_t4(glm::lessThan(Vec4AFull, Vec4BFull)); - - expected.mortonSignedLess_emulated_2 = uint32_t2(glm::lessThan(Vec2ASignedFull, Vec2BSignedFull)); - expected.mortonSignedLess_emulated_3 = uint32_t3(glm::lessThan(Vec3ASignedFull, Vec3BSignedFull)); - expected.mortonSignedLess_emulated_4 = uint32_t4(glm::lessThan(Vec4ASignedFull, Vec4BSignedFull)); - - uint16_t castedShift = uint16_t(generatedShift); - expected.mortonSignedRightShift_emulated_2 = createMortonFromU64Vec(Vec2ASignedFull >> int32_t(castedShift % fullBits_2)); - expected.mortonSignedRightShift_emulated_3 = createMortonFromU64Vec(Vec3ASignedFull >> int32_t(castedShift % fullBits_3)); - expected.mortonSignedRightShift_emulated_4 = createMortonFromU64Vec(Vec4ASignedFull >> int16_t(castedShift % fullBits_4)); - - return expected; - } - - bool verifyTestResults(const TestValues& expectedTestValues, const TestValues& testValues, const size_t testIteration, const uint32_t seed, ITester::TestType testType) override - { - bool pass = true; - pass &= verifyTestValue("mortonUnsignedLess_emulated_4", expectedTestValues.mortonUnsignedLess_emulated_4, testValues.mortonUnsignedLess_emulated_4, testIteration, seed, testType); - - pass &= verifyTestValue("mortonSignedLess_emulated_2", expectedTestValues.mortonSignedLess_emulated_2, testValues.mortonSignedLess_emulated_2, testIteration, seed, testType); - pass &= verifyTestValue("mortonSignedLess_emulated_3", expectedTestValues.mortonSignedLess_emulated_3, testValues.mortonSignedLess_emulated_3, testIteration, seed, testType); - pass &= verifyTestValue("mortonSignedLess_emulated_4", expectedTestValues.mortonSignedLess_emulated_4, testValues.mortonSignedLess_emulated_4, testIteration, seed, testType); - - pass &= verifyTestValue("mortonSignedRightShift_emulated_2", expectedTestValues.mortonSignedRightShift_emulated_2, testValues.mortonSignedRightShift_emulated_2, testIteration, seed, testType); - pass &= verifyTestValue("mortonSignedRightShift_emulated_3", expectedTestValues.mortonSignedRightShift_emulated_3, testValues.mortonSignedRightShift_emulated_3, testIteration, seed, testType); - pass &= verifyTestValue("mortonSignedRightShift_emulated_4", expectedTestValues.mortonSignedRightShift_emulated_4, testValues.mortonSignedRightShift_emulated_4, testIteration, seed, testType); - return pass; - } -}; -#endif \ No newline at end of file diff --git a/14_Mortons/app_resources/common.hlsl b/14_Mortons/app_resources/common.hlsl deleted file mode 100644 index 98e5e1342..000000000 --- a/14_Mortons/app_resources/common.hlsl +++ /dev/null @@ -1,233 +0,0 @@ -//// Copyright (C) 2023-2024 - 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_TESTS_12_MORTON_COMMON_INCLUDED_ -#define _NBL_EXAMPLES_TESTS_12_MORTON_COMMON_INCLUDED_ - -#include - -#include - -NBL_CONSTEXPR uint16_t smallBits_2 = 8; -NBL_CONSTEXPR uint16_t mediumBits_2 = 16; -NBL_CONSTEXPR uint16_t fullBits_2 = 32; -NBL_CONSTEXPR uint16_t smallBits_3 = 5; -NBL_CONSTEXPR uint16_t mediumBits_3 = 10; -NBL_CONSTEXPR uint16_t fullBits_3 = 21; -NBL_CONSTEXPR uint16_t smallBits_4 = 4; -NBL_CONSTEXPR uint16_t mediumBits_4 = 8; -NBL_CONSTEXPR uint16_t fullBits_4 = 16; - -using namespace nbl::hlsl; -template -NBL_CONSTEXPR_INLINE_FUNC T createAnyBitIntegerFromU64(uint64_t val) -{ - if(Signed) - { - NBL_CONSTEXPR_FUNC_SCOPE_VAR uint64_t mask = (uint64_t(1) << (Bits - 1)) - 1; - // fill excess bit with one - if (_static_cast(val) < 0) - return _static_cast(val | ~mask); - else - return _static_cast(val & mask); - } else - { - NBL_CONSTEXPR_FUNC_SCOPE_VAR uint64_t mask = (uint64_t(1) << Bits) - 1; - return _static_cast(val & mask); - } -} - -template -NBL_CONSTEXPR_INLINE_FUNC vector createAnyBitIntegerVecFromU64Vec(vector val) -{ - array_get, uint64_t> getter; - array_set, T> setter; - vector output; - NBL_UNROLL - for (uint16_t i = 0; i < D; i++) - { - setter(output, i, createAnyBitIntegerFromU64(getter(val, i))); - } - return output; -} - -template -NBL_CONSTEXPR_INLINE_FUNC morton::code createMortonFromU64Vec(const vector vec) -{ - using morton_code_t = morton::code; - using decode_component_t = typename morton_code_t::decode_component_t; - return morton_code_t::create(createAnyBitIntegerVecFromU64Vec(vec)); -} - -struct InputTestValues -{ - // Both tests - uint32_t shift; - - // Emulated int tests - uint64_t generatedA; - uint64_t generatedB; - - // Morton tests - uint64_t coordX; - uint64_t coordY; - uint64_t coordZ; - uint64_t coordW; -}; - -struct TestValues -{ - // Emulated int tests - emulated_uint64_t emulatedAnd; - emulated_uint64_t emulatedOr; - emulated_uint64_t emulatedXor; - emulated_uint64_t emulatedNot; - emulated_uint64_t emulatedPlus; - emulated_uint64_t emulatedMinus; - emulated_int64_t emulatedUnaryMinus; - // These are bools but stored as uint because you can't store bools, causes a SPIR-V issue - uint32_t emulatedLess; - uint32_t emulatedLessEqual; - uint32_t emulatedGreater; - uint32_t emulatedGreaterEqual; - emulated_uint64_t emulatedLeftShifted; - emulated_uint64_t emulatedUnsignedRightShifted; - emulated_int64_t emulatedSignedRightShifted; - - // Morton tests - for each dimension let's do one small, medium and full-szied (max bits possible) test to cover representation with - // 16, 32 and 64-bit types. Could make it more exhaustive with macros (test all possible bitwidths) - // For emulated mortons, we store only the emulated uint64 representing it, because DXC complains about bitcasts otherwise - - // Plus - morton::code mortonPlus_small_2; - morton::code mortonPlus_medium_2; - morton::code mortonPlus_full_2; - morton::code mortonPlus_emulated_2; - - morton::code mortonPlus_small_3; - morton::code mortonPlus_medium_3; - morton::code mortonPlus_full_3; - morton::code mortonPlus_emulated_3; - - morton::code mortonPlus_small_4; - morton::code mortonPlus_medium_4; - morton::code mortonPlus_full_4; - morton::code mortonPlus_emulated_4; - - // Minus - morton::code mortonMinus_small_2; - morton::code mortonMinus_medium_2; - morton::code mortonMinus_full_2; - morton::code mortonMinus_emulated_2; - - morton::code mortonMinus_small_3; - morton::code mortonMinus_medium_3; - morton::code mortonMinus_full_3; - morton::code mortonMinus_emulated_3; - - morton::code mortonMinus_small_4; - morton::code mortonMinus_medium_4; - morton::code mortonMinus_full_4; - morton::code mortonMinus_emulated_4; - - // Coordinate-wise equality (these are bools) - uint32_t2 mortonEqual_small_2; - uint32_t2 mortonEqual_medium_2; - uint32_t2 mortonEqual_full_2; - uint32_t2 mortonEqual_emulated_2; - - uint32_t3 mortonEqual_small_3; - uint32_t3 mortonEqual_medium_3; - uint32_t3 mortonEqual_full_3; - uint32_t3 mortonEqual_emulated_3; - - uint32_t4 mortonEqual_small_4; - uint32_t4 mortonEqual_medium_4; - uint32_t4 mortonEqual_full_4; - uint32_t4 mortonEqual_emulated_4; - - // Coordinate-wise unsigned inequality (just testing with less, again these are bools) - uint32_t2 mortonUnsignedLess_small_2; - uint32_t2 mortonUnsignedLess_medium_2; - uint32_t2 mortonUnsignedLess_full_2; - uint32_t2 mortonUnsignedLess_emulated_2; - - uint32_t3 mortonUnsignedLess_small_3; - uint32_t3 mortonUnsignedLess_medium_3; - uint32_t3 mortonUnsignedLess_full_3; - uint32_t3 mortonUnsignedLess_emulated_3; - - uint32_t4 mortonUnsignedLess_small_4; - uint32_t4 mortonUnsignedLess_medium_4; - uint32_t4 mortonUnsignedLess_full_4; - uint32_t4 mortonUnsignedLess_emulated_4; - - // Coordinate-wise signed inequality (bools) - uint32_t2 mortonSignedLess_small_2; - uint32_t2 mortonSignedLess_medium_2; - uint32_t2 mortonSignedLess_full_2; - uint32_t2 mortonSignedLess_emulated_2; - - uint32_t3 mortonSignedLess_small_3; - uint32_t3 mortonSignedLess_medium_3; - uint32_t3 mortonSignedLess_full_3; - uint32_t3 mortonSignedLess_emulated_3; - - uint32_t4 mortonSignedLess_small_4; - uint32_t4 mortonSignedLess_medium_4; - uint32_t4 mortonSignedLess_full_4; - uint32_t4 mortonSignedLess_emulated_4; - - // Left-shift - morton::code mortonLeftShift_small_2; - morton::code mortonLeftShift_medium_2; - morton::code mortonLeftShift_full_2; - morton::code mortonLeftShift_emulated_2; - - morton::code mortonLeftShift_small_3; - morton::code mortonLeftShift_medium_3; - morton::code mortonLeftShift_full_3; - morton::code mortonLeftShift_emulated_3; - - morton::code mortonLeftShift_small_4; - morton::code mortonLeftShift_medium_4; - morton::code mortonLeftShift_full_4; - morton::code mortonLeftShift_emulated_4; - - // Unsigned right-shift - morton::code mortonUnsignedRightShift_small_2; - morton::code mortonUnsignedRightShift_medium_2; - morton::code mortonUnsignedRightShift_full_2; - morton::code mortonUnsignedRightShift_emulated_2; - - morton::code mortonUnsignedRightShift_small_3; - morton::code mortonUnsignedRightShift_medium_3; - morton::code mortonUnsignedRightShift_full_3; - morton::code mortonUnsignedRightShift_emulated_3; - - morton::code mortonUnsignedRightShift_small_4; - morton::code mortonUnsignedRightShift_medium_4; - morton::code mortonUnsignedRightShift_full_4; - morton::code mortonUnsignedRightShift_emulated_4; - - // Signed right-shift - morton::code mortonSignedRightShift_small_2; - morton::code mortonSignedRightShift_medium_2; - morton::code mortonSignedRightShift_full_2; - morton::code mortonSignedRightShift_emulated_2; - - morton::code mortonSignedRightShift_small_3; - morton::code mortonSignedRightShift_medium_3; - morton::code mortonSignedRightShift_full_3; - morton::code mortonSignedRightShift_emulated_3; - - morton::code mortonSignedRightShift_small_4; - morton::code mortonSignedRightShift_medium_4; - morton::code mortonSignedRightShift_full_4; - morton::code mortonSignedRightShift_emulated_4; - - -}; - -#endif diff --git a/14_Mortons/app_resources/test.comp.hlsl b/14_Mortons/app_resources/test.comp.hlsl deleted file mode 100644 index 2a2c465f4..000000000 --- a/14_Mortons/app_resources/test.comp.hlsl +++ /dev/null @@ -1,20 +0,0 @@ -//// Copyright (C) 2023-2024 - 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 shader_stage(compute) - -#include "testCommon.hlsl" -#include - -[[vk::binding(0, 0)]] RWStructuredBuffer inputTestValues; -[[vk::binding(1, 0)]] RWStructuredBuffer outputTestValues; - -[numthreads(256, 1, 1)] -[shader("compute")] -void main() -{ - const uint invID = nbl::hlsl::glsl::gl_GlobalInvocationID().x; - TestExecutor executor; - executor(inputTestValues[invID], outputTestValues[invID]); -} - diff --git a/14_Mortons/app_resources/test2.comp.hlsl b/14_Mortons/app_resources/test2.comp.hlsl deleted file mode 100644 index 8561faf83..000000000 --- a/14_Mortons/app_resources/test2.comp.hlsl +++ /dev/null @@ -1,20 +0,0 @@ -//// Copyright (C) 2023-2024 - 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 shader_stage(compute) - -#include "testCommon2.hlsl" -#include - -[[vk::binding(0, 0)]] RWStructuredBuffer inputTestValues; -[[vk::binding(1, 0)]] RWStructuredBuffer outputTestValues; - -[numthreads(256, 1, 1)] -[shader("compute")] -void main() -{ - const uint invID = nbl::hlsl::glsl::gl_GlobalInvocationID().x; - TestExecutor2 executor; - executor(inputTestValues[invID], outputTestValues[invID]); -} - diff --git a/14_Mortons/app_resources/testCommon.hlsl b/14_Mortons/app_resources/testCommon.hlsl deleted file mode 100644 index b285bd8cd..000000000 --- a/14_Mortons/app_resources/testCommon.hlsl +++ /dev/null @@ -1,297 +0,0 @@ -#include "common.hlsl" - -struct TestExecutor -{ - void operator()(NBL_CONST_REF_ARG(InputTestValues) input, NBL_REF_ARG(TestValues) output) - { - emulated_uint64_t emulatedA = _static_cast(input.generatedA); - emulated_uint64_t emulatedB = _static_cast(input.generatedB); - emulated_int64_t signedEmulatedA = _static_cast(input.generatedA); - - // Emulated int tests - output.emulatedAnd = emulatedA & emulatedB; - output.emulatedOr = emulatedA | emulatedB; - output.emulatedXor = emulatedA ^ emulatedB; - output.emulatedNot = emulatedA.operator~(); - output.emulatedPlus = emulatedA + emulatedB; - output.emulatedMinus = emulatedA - emulatedB; - output.emulatedLess = uint32_t(emulatedA < emulatedB); - output.emulatedLessEqual = uint32_t(emulatedA <= emulatedB); - output.emulatedGreater = uint32_t(emulatedA > emulatedB); - output.emulatedGreaterEqual = uint32_t(emulatedA >= emulatedB); - - left_shift_operator leftShift; - output.emulatedLeftShifted = leftShift(emulatedA, input.shift); - - arithmetic_right_shift_operator unsignedRightShift; - output.emulatedUnsignedRightShifted = unsignedRightShift(emulatedA, input.shift); - - arithmetic_right_shift_operator signedRightShift; - output.emulatedSignedRightShifted = signedRightShift(signedEmulatedA, input.shift); - - output.emulatedUnaryMinus = signedEmulatedA.operator-(); - - // Morton tests - uint64_t2 Vec2A = { input.coordX, input.coordY }; - uint64_t2 Vec2B = { input.coordZ, input.coordW }; - - uint64_t3 Vec3A = { input.coordX, input.coordY, input.coordZ }; - uint64_t3 Vec3B = { input.coordY, input.coordZ, input.coordW }; - - uint64_t4 Vec4A = { input.coordX, input.coordY, input.coordZ, input.coordW }; - uint64_t4 Vec4B = { input.coordY, input.coordZ, input.coordW, input.coordX }; - - uint16_t2 Vec2ASmall = createAnyBitIntegerVecFromU64Vec(Vec2A); - uint16_t2 Vec2BSmall = createAnyBitIntegerVecFromU64Vec(Vec2B); - uint16_t2 Vec2AMedium = createAnyBitIntegerVecFromU64Vec(Vec2A); - uint16_t2 Vec2BMedium = createAnyBitIntegerVecFromU64Vec(Vec2B); - uint32_t2 Vec2AFull = createAnyBitIntegerVecFromU64Vec(Vec2A); - uint32_t2 Vec2BFull = createAnyBitIntegerVecFromU64Vec(Vec2B); - - uint16_t3 Vec3ASmall = createAnyBitIntegerVecFromU64Vec(Vec3A); - uint16_t3 Vec3BSmall = createAnyBitIntegerVecFromU64Vec(Vec3B); - uint16_t3 Vec3AMedium = createAnyBitIntegerVecFromU64Vec(Vec3A); - uint16_t3 Vec3BMedium = createAnyBitIntegerVecFromU64Vec(Vec3B); - uint32_t3 Vec3AFull = createAnyBitIntegerVecFromU64Vec(Vec3A); - uint32_t3 Vec3BFull = createAnyBitIntegerVecFromU64Vec(Vec3B); - - uint16_t4 Vec4ASmall = createAnyBitIntegerVecFromU64Vec(Vec4A); - uint16_t4 Vec4BSmall = createAnyBitIntegerVecFromU64Vec(Vec4B); - uint16_t4 Vec4AMedium = createAnyBitIntegerVecFromU64Vec(Vec4A); - uint16_t4 Vec4BMedium = createAnyBitIntegerVecFromU64Vec(Vec4B); - uint16_t4 Vec4AFull = createAnyBitIntegerVecFromU64Vec(Vec4A); - uint16_t4 Vec4BFull = createAnyBitIntegerVecFromU64Vec(Vec4B); - - int16_t2 Vec2ASignedSmall = createAnyBitIntegerVecFromU64Vec(Vec2A); - int16_t2 Vec2BSignedSmall = createAnyBitIntegerVecFromU64Vec(Vec2B); - int16_t2 Vec2ASignedMedium = createAnyBitIntegerVecFromU64Vec(Vec2A); - int16_t2 Vec2BSignedMedium = createAnyBitIntegerVecFromU64Vec(Vec2B); - int32_t2 Vec2ASignedFull = createAnyBitIntegerVecFromU64Vec(Vec2A); - int32_t2 Vec2BSignedFull = createAnyBitIntegerVecFromU64Vec(Vec2B); - - int16_t3 Vec3ASignedSmall = createAnyBitIntegerVecFromU64Vec(Vec3A); - int16_t3 Vec3BSignedSmall = createAnyBitIntegerVecFromU64Vec(Vec3B); - int16_t3 Vec3ASignedMedium = createAnyBitIntegerVecFromU64Vec(Vec3A); - int16_t3 Vec3BSignedMedium = createAnyBitIntegerVecFromU64Vec(Vec3B); - int32_t3 Vec3ASignedFull = createAnyBitIntegerVecFromU64Vec(Vec3A); - int32_t3 Vec3BSignedFull = createAnyBitIntegerVecFromU64Vec(Vec3B); - - int16_t4 Vec4ASignedSmall = createAnyBitIntegerVecFromU64Vec(Vec4A); - int16_t4 Vec4BSignedSmall = createAnyBitIntegerVecFromU64Vec(Vec4B); - int16_t4 Vec4ASignedMedium = createAnyBitIntegerVecFromU64Vec(Vec4A); - int16_t4 Vec4BSignedMedium = createAnyBitIntegerVecFromU64Vec(Vec4B); - int16_t4 Vec4ASignedFull = createAnyBitIntegerVecFromU64Vec(Vec4A); - int16_t4 Vec4BSignedFull = createAnyBitIntegerVecFromU64Vec(Vec4B); - - morton::code morton_small_2A = createMortonFromU64Vec(Vec2A); - morton::code morton_medium_2A = createMortonFromU64Vec(Vec2A); - morton::code morton_full_2A = createMortonFromU64Vec(Vec2A); - morton::code morton_emulated_2A = createMortonFromU64Vec(Vec2A); - morton::code morton_small_2B = createMortonFromU64Vec(Vec2B); - morton::code morton_medium_2B = createMortonFromU64Vec(Vec2B); - morton::code morton_full_2B = createMortonFromU64Vec(Vec2B); - morton::code morton_emulated_2B = createMortonFromU64Vec(Vec2B); - - morton::code morton_small_3A = createMortonFromU64Vec(Vec3A); - morton::code morton_medium_3A = createMortonFromU64Vec(Vec3A); - morton::code morton_full_3A = createMortonFromU64Vec(Vec3A); - morton::code morton_emulated_3A = createMortonFromU64Vec(Vec3A); - morton::code morton_small_3B = createMortonFromU64Vec(Vec3B); - morton::code morton_medium_3B = createMortonFromU64Vec(Vec3B); - morton::code morton_full_3B = createMortonFromU64Vec(Vec3B); - morton::code morton_emulated_3B = createMortonFromU64Vec(Vec3B); - - morton::code morton_small_4A = createMortonFromU64Vec(Vec4A); - morton::code morton_medium_4A = createMortonFromU64Vec(Vec4A); - morton::code morton_full_4A = createMortonFromU64Vec(Vec4A); - morton::code morton_emulated_4A = createMortonFromU64Vec(Vec4A); - morton::code morton_small_4B = createMortonFromU64Vec(Vec4B); - morton::code morton_medium_4B = createMortonFromU64Vec(Vec4B); - morton::code morton_full_4B = createMortonFromU64Vec(Vec4B); - morton::code morton_emulated_4B = createMortonFromU64Vec(Vec4B); - - morton::code morton_small_2_signed = createMortonFromU64Vec(Vec2A); - morton::code morton_medium_2_signed = createMortonFromU64Vec(Vec2A); - morton::code morton_full_2_signed = createMortonFromU64Vec(Vec2A); - morton::code morton_emulated_2_signed = createMortonFromU64Vec(Vec2A); - - morton::code morton_small_3_signed = createMortonFromU64Vec(Vec3A); - morton::code morton_medium_3_signed = createMortonFromU64Vec(Vec3A); - morton::code morton_full_3_signed = createMortonFromU64Vec(Vec3A); - morton::code morton_emulated_3_signed = createMortonFromU64Vec(Vec3A); - - morton::code morton_small_4_signed = createMortonFromU64Vec(Vec4A); - morton::code morton_medium_4_signed = createMortonFromU64Vec(Vec4A); - morton::code morton_full_4_signed = createMortonFromU64Vec(Vec4A); - morton::code morton_emulated_4_signed = createMortonFromU64Vec(Vec4A); - - // Some test and operation is moved to testCommon2.hlsl due to dxc bug that cause compilation failure. Uncomment when the bug is fixed. - // Plus - output.mortonPlus_small_2 = morton_small_2A + morton_small_2B; - output.mortonPlus_medium_2 = morton_medium_2A + morton_medium_2B; - output.mortonPlus_full_2 = morton_full_2A + morton_full_2B; - output.mortonPlus_emulated_2 = morton_emulated_2A + morton_emulated_2B; - - output.mortonPlus_small_3 = morton_small_3A + morton_small_3B; - output.mortonPlus_medium_3 = morton_medium_3A + morton_medium_3B; - output.mortonPlus_full_3 = morton_full_3A + morton_full_3B; - output.mortonPlus_emulated_3 = morton_emulated_3A + morton_emulated_3B; - - output.mortonPlus_small_4 = morton_small_4A + morton_small_4B; - output.mortonPlus_medium_4 = morton_medium_4A + morton_medium_4B; - output.mortonPlus_full_4 = morton_full_4A + morton_full_4B; - output.mortonPlus_emulated_4 = morton_emulated_4A + morton_emulated_4B; - - // Minus - output.mortonMinus_small_2 = morton_small_2A - morton_small_2B; - output.mortonMinus_medium_2 = morton_medium_2A - morton_medium_2B; - output.mortonMinus_full_2 = morton_full_2A - morton_full_2B; - output.mortonMinus_emulated_2 = morton_emulated_2A - morton_emulated_2B; - - output.mortonMinus_small_3 = morton_small_3A - morton_small_3B; - output.mortonMinus_medium_3 = morton_medium_3A - morton_medium_3B; - output.mortonMinus_full_3 = morton_full_3A - morton_full_3B; - output.mortonMinus_emulated_3 = morton_emulated_3A - morton_emulated_3B; - - output.mortonMinus_small_4 = morton_small_4A - morton_small_4B; - output.mortonMinus_medium_4 = morton_medium_4A - morton_medium_4B; - output.mortonMinus_full_4 = morton_full_4A - morton_full_4B; - output.mortonMinus_emulated_4 = morton_emulated_4A - morton_emulated_4B; - - // Coordinate-wise equality - output.mortonEqual_small_2 = uint32_t2(morton_small_2A.equal(Vec2BSmall)); - output.mortonEqual_medium_2 = uint32_t2(morton_medium_2A.equal(Vec2BMedium)); - output.mortonEqual_full_2 = uint32_t2(morton_full_2A.equal(Vec2BFull)); - output.mortonEqual_emulated_2 = uint32_t2(morton_emulated_2A.equal(Vec2BFull)); - - output.mortonEqual_small_3 = uint32_t3(morton_small_3A.equal(Vec3BSmall)); - output.mortonEqual_medium_3 = uint32_t3(morton_medium_3A.equal(Vec3BMedium)); - output.mortonEqual_full_3 = uint32_t3(morton_full_3A.equal(Vec3BFull)); - output.mortonEqual_emulated_3 = uint32_t3(morton_emulated_3A.equal(Vec3BFull)); - - output.mortonEqual_small_4 = uint32_t4(morton_small_4A.equal(Vec4BSmall)); - output.mortonEqual_medium_4 = uint32_t4(morton_medium_4A.equal(Vec4BMedium)); - output.mortonEqual_full_4 = uint32_t4(morton_full_4A.equal(Vec4BFull)); - output.mortonEqual_emulated_4 = uint32_t4(morton_emulated_4A.equal(Vec4BFull)); - - // Coordinate-wise unsigned inequality (just testing with less) - output.mortonUnsignedLess_small_2 = uint32_t2(morton_small_2A.lessThan(Vec2BSmall)); - output.mortonUnsignedLess_medium_2 = uint32_t2(morton_medium_2A.lessThan(Vec2BMedium)); - output.mortonUnsignedLess_full_2 = uint32_t2(morton_full_2A.lessThan(Vec2BFull)); - output.mortonUnsignedLess_emulated_2 = uint32_t2(morton_emulated_2A.lessThan(Vec2BFull)); - - output.mortonUnsignedLess_small_3 = uint32_t3(morton_small_3A.lessThan(Vec3BSmall)); - output.mortonUnsignedLess_medium_3 = uint32_t3(morton_medium_3A.lessThan(Vec3BMedium)); - output.mortonUnsignedLess_full_3 = uint32_t3(morton_full_3A.lessThan(Vec3BFull)); - output.mortonUnsignedLess_emulated_3 = uint32_t3(morton_emulated_3A.lessThan(Vec3BFull)); - - output.mortonUnsignedLess_small_4 = uint32_t4(morton_small_4A.lessThan(Vec4BSmall)); - output.mortonUnsignedLess_medium_4 = uint32_t4(morton_medium_4A.lessThan(Vec4BMedium)); - output.mortonUnsignedLess_full_4 = uint32_t4(morton_full_4A.lessThan(Vec4BFull)); - // output.mortonUnsignedLess_emulated_4 = uint32_t4(morton_emulated_4A.lessThan(Vec4BFull)); - - // Coordinate-wise signed inequality - output.mortonSignedLess_small_2 = uint32_t2(morton_small_2_signed.lessThan(Vec2BSignedSmall)); - output.mortonSignedLess_medium_2 = uint32_t2(morton_medium_2_signed.lessThan(Vec2BSignedMedium)); - output.mortonSignedLess_full_2 = uint32_t2(morton_full_2_signed.lessThan(Vec2BSignedFull)); - // output.mortonSignedLess_emulated_2 = uint32_t2(morton_emulated_2_signed.lessThan(Vec2BSignedFull)); - - output.mortonSignedLess_small_3 = uint32_t3(morton_small_3_signed.lessThan(Vec3BSignedSmall)); - output.mortonSignedLess_medium_3 = uint32_t3(morton_medium_3_signed.lessThan(Vec3BSignedMedium)); - output.mortonSignedLess_full_3 = uint32_t3(morton_full_3_signed.lessThan(Vec3BSignedFull)); - // output.mortonSignedLess_emulated_3 = uint32_t3(morton_emulated_3_signed.lessThan(Vec3BSignedFull)); - - output.mortonSignedLess_small_4 = uint32_t4(morton_small_4_signed.lessThan(Vec4BSignedSmall)); - output.mortonSignedLess_medium_4 = uint32_t4(morton_medium_4_signed.lessThan(Vec4BSignedMedium)); - output.mortonSignedLess_full_4 = uint32_t4(morton_full_4_signed.lessThan(Vec4BSignedFull)); - // output.mortonSignedLess_emulated_4 = uint32_t4(morton_emulated_4_signed.lessThan(Vec4BSignedFull)); - - // Cast to uint16_t which is what left shift for Mortons expect - uint16_t castedShift = uint16_t(input.shift); - // Each left shift clamps to correct bits so the result kinda makes sense - // Left-shift - left_shift_operator > leftShiftSmall2; - output.mortonLeftShift_small_2 = leftShiftSmall2(morton_small_2A, castedShift % smallBits_2); - left_shift_operator > leftShiftMedium2; - output.mortonLeftShift_medium_2 = leftShiftMedium2(morton_medium_2A, castedShift % mediumBits_2); - left_shift_operator > leftShiftFull2; - output.mortonLeftShift_full_2 = leftShiftFull2(morton_full_2A, castedShift % fullBits_2); - left_shift_operator > leftShiftEmulated2; - output.mortonLeftShift_emulated_2 = leftShiftEmulated2(morton_emulated_2A, castedShift % fullBits_2); - - left_shift_operator > leftShiftSmall3; - output.mortonLeftShift_small_3 = leftShiftSmall3(morton_small_3A, castedShift % smallBits_3); - left_shift_operator > leftShiftMedium3; - output.mortonLeftShift_medium_3 = leftShiftMedium3(morton_medium_3A, castedShift % mediumBits_3); - left_shift_operator > leftShiftFull3; - output.mortonLeftShift_full_3 = leftShiftFull3(morton_full_3A, castedShift % fullBits_3); - left_shift_operator > leftShiftEmulated3; - output.mortonLeftShift_emulated_3 = leftShiftEmulated3(morton_emulated_3A, castedShift % fullBits_3); - - left_shift_operator > leftShiftSmall4; - output.mortonLeftShift_small_4 = leftShiftSmall4(morton_small_4A, castedShift % smallBits_4); - left_shift_operator > leftShiftMedium4; - output.mortonLeftShift_medium_4 = leftShiftMedium4(morton_medium_4A, castedShift % mediumBits_4); - left_shift_operator > leftShiftFull4; - output.mortonLeftShift_full_4 = leftShiftFull4(morton_full_4A, castedShift % fullBits_4); - left_shift_operator > leftShiftEmulated4; - output.mortonLeftShift_emulated_4 = leftShiftEmulated4(morton_emulated_4A, castedShift % fullBits_4); - - // Unsigned right-shift - arithmetic_right_shift_operator > rightShiftSmall2; - output.mortonUnsignedRightShift_small_2 = rightShiftSmall2(morton_small_2A, castedShift % smallBits_2); - arithmetic_right_shift_operator > rightShiftMedium2; - output.mortonUnsignedRightShift_medium_2 = rightShiftMedium2(morton_medium_2A, castedShift % mediumBits_2); - arithmetic_right_shift_operator > rightShiftFull2; - output.mortonUnsignedRightShift_full_2 = rightShiftFull2(morton_full_2A, castedShift % fullBits_2); - arithmetic_right_shift_operator > rightShiftEmulated2; - output.mortonUnsignedRightShift_emulated_2 = rightShiftEmulated2(morton_emulated_2A, castedShift % fullBits_2); - - arithmetic_right_shift_operator > rightShiftSmall3; - output.mortonUnsignedRightShift_small_3 = rightShiftSmall3(morton_small_3A, castedShift % smallBits_3); - arithmetic_right_shift_operator > rightShiftMedium3; - output.mortonUnsignedRightShift_medium_3 = rightShiftMedium3(morton_medium_3A, castedShift % mediumBits_3); - arithmetic_right_shift_operator > rightShiftFull3; - output.mortonUnsignedRightShift_full_3 = rightShiftFull3(morton_full_3A, castedShift % fullBits_3); - arithmetic_right_shift_operator > rightShiftEmulated3; - output.mortonUnsignedRightShift_emulated_3 = rightShiftEmulated3(morton_emulated_3A, castedShift % fullBits_3); - - arithmetic_right_shift_operator > rightShiftSmall4; - output.mortonUnsignedRightShift_small_4 = rightShiftSmall4(morton_small_4A, castedShift % smallBits_4); - arithmetic_right_shift_operator > rightShiftMedium4; - output.mortonUnsignedRightShift_medium_4 = rightShiftMedium4(morton_medium_4A, castedShift % mediumBits_4); - arithmetic_right_shift_operator > rightShiftFull4; - output.mortonUnsignedRightShift_full_4 = rightShiftFull4(morton_full_4A, castedShift % fullBits_4); - arithmetic_right_shift_operator > rightShiftEmulated4; - output.mortonUnsignedRightShift_emulated_4 = rightShiftEmulated4(morton_emulated_4A, castedShift % fullBits_4); - - // Signed right-shift - arithmetic_right_shift_operator > rightShiftSignedSmall2; - output.mortonSignedRightShift_small_2 = rightShiftSignedSmall2(morton_small_2_signed, castedShift % smallBits_2); - arithmetic_right_shift_operator > rightShiftSignedMedium2; - output.mortonSignedRightShift_medium_2 = rightShiftSignedMedium2(morton_medium_2_signed, castedShift % mediumBits_2); - arithmetic_right_shift_operator > rightShiftSignedFull2; - output.mortonSignedRightShift_full_2 = rightShiftSignedFull2(morton_full_2_signed, castedShift % fullBits_2); - // arithmetic_right_shift_operator > rightShiftSignedEmulated2; - // output.mortonSignedRightShift_emulated_2 = rightShiftSignedEmulated2(morton_emulated_2_signed, castedShift % fullBits_2); - - arithmetic_right_shift_operator > rightShiftSignedSmall3; - output.mortonSignedRightShift_small_3 = rightShiftSignedSmall3(morton_small_3_signed, castedShift % smallBits_3); - arithmetic_right_shift_operator > rightShiftSignedMedium3; - output.mortonSignedRightShift_medium_3 = rightShiftSignedMedium3(morton_medium_3_signed, castedShift % mediumBits_3); - arithmetic_right_shift_operator > rightShiftSignedFull3; - output.mortonSignedRightShift_full_3 = rightShiftSignedFull3(morton_full_3_signed, castedShift % fullBits_3); - // arithmetic_right_shift_operator > rightShiftSignedEmulated3; - // output.mortonSignedRightShift_emulated_3 = rightShiftSignedEmulated3(morton_emulated_3_signed, castedShift % fullBits_3); - - arithmetic_right_shift_operator > rightShiftSignedSmall4; - output.mortonSignedRightShift_small_4 = rightShiftSignedSmall4(morton_small_4_signed, castedShift % smallBits_4); - arithmetic_right_shift_operator > rightShiftSignedMedium4; - output.mortonSignedRightShift_medium_4 = rightShiftSignedMedium4(morton_medium_4_signed, castedShift % mediumBits_4); - arithmetic_right_shift_operator > rightShiftSignedFull4; - output.mortonSignedRightShift_full_4 = rightShiftSignedFull4(morton_full_4_signed, castedShift % fullBits_4); - // arithmetic_right_shift_operator > rightShiftSignedEmulated4; - // output.mortonSignedRightShift_emulated_4 = rightShiftSignedEmulated4(morton_emulated_4_signed, castedShift % fullBits_4); - - } -}; diff --git a/14_Mortons/app_resources/testCommon2.hlsl b/14_Mortons/app_resources/testCommon2.hlsl deleted file mode 100644 index 5c2a953ac..000000000 --- a/14_Mortons/app_resources/testCommon2.hlsl +++ /dev/null @@ -1,42 +0,0 @@ -#include "common.hlsl" - -struct TestExecutor2 -{ - void operator()(NBL_CONST_REF_ARG(InputTestValues) input, NBL_REF_ARG(TestValues) output) - { - uint64_t2 Vec2A = { input.coordX, input.coordY }; - uint64_t2 Vec2B = { input.coordZ, input.coordW }; - - uint64_t3 Vec3A = { input.coordX, input.coordY, input.coordZ }; - uint64_t3 Vec3B = { input.coordY, input.coordZ, input.coordW }; - - uint64_t4 Vec4A = { input.coordX, input.coordY, input.coordZ, input.coordW }; - uint64_t4 Vec4B = { input.coordY, input.coordZ, input.coordW, input.coordX }; - - uint16_t4 Vec4BFull = createAnyBitIntegerVecFromU64Vec(Vec4B); - int32_t2 Vec2BSignedFull = createAnyBitIntegerVecFromU64Vec(Vec2B); - int32_t3 Vec3BSignedFull = createAnyBitIntegerVecFromU64Vec(Vec3B); - int16_t4 Vec4BSignedFull = createAnyBitIntegerVecFromU64Vec(Vec4B); - - morton::code morton_emulated_4A = createMortonFromU64Vec(Vec4A); - morton::code morton_emulated_2_signed = createMortonFromU64Vec(Vec2A); - morton::code morton_emulated_3_signed = createMortonFromU64Vec(Vec3A); - morton::code morton_emulated_4_signed = createMortonFromU64Vec(Vec4A); - - - output.mortonUnsignedLess_emulated_4 = uint32_t4(morton_emulated_4A.lessThan(Vec4BFull)); - - output.mortonSignedLess_emulated_2 = uint32_t2(morton_emulated_2_signed.lessThan(Vec2BSignedFull)); - output.mortonSignedLess_emulated_3 = uint32_t3(morton_emulated_3_signed.lessThan(Vec3BSignedFull)); - output.mortonSignedLess_emulated_4 = uint32_t4(morton_emulated_4_signed.lessThan(Vec4BSignedFull)); - - uint16_t castedShift = uint16_t(input.shift); - - arithmetic_right_shift_operator > rightShiftSignedEmulated2; - output.mortonSignedRightShift_emulated_2 = rightShiftSignedEmulated2(morton_emulated_2_signed, castedShift % fullBits_2); - arithmetic_right_shift_operator > rightShiftSignedEmulated3; - output.mortonSignedRightShift_emulated_3 = rightShiftSignedEmulated3(morton_emulated_3_signed, castedShift % fullBits_3); - arithmetic_right_shift_operator > rightShiftSignedEmulated4; - output.mortonSignedRightShift_emulated_4 = rightShiftSignedEmulated4(morton_emulated_4_signed, castedShift % fullBits_4); - } -}; diff --git a/14_Mortons/config.json.template b/14_Mortons/config.json.template deleted file mode 100644 index 717d05d53..000000000 --- a/14_Mortons/config.json.template +++ /dev/null @@ -1,28 +0,0 @@ -{ - "enableParallelBuild": true, - "threadsPerBuildProcess" : 2, - "isExecuted": false, - "scriptPath": "", - "cmake": { - "configurations": [ "Release", "Debug", "RelWithDebInfo" ], - "buildModes": [], - "requiredOptions": [] - }, - "profiles": [ - { - "backend": "vulkan", // should be none - "platform": "windows", - "buildModes": [], - "runConfiguration": "Release", // we also need to run in Debug nad RWDI because foundational example - "gpuArchitectures": [] - } - ], - "dependencies": [], - "data": [ - { - "dependencies": [], - "command": [""], - "outputs": [] - } - ] -} \ No newline at end of file diff --git a/14_Mortons/main.cpp b/14_Mortons/main.cpp deleted file mode 100644 index d995b8109..000000000 --- a/14_Mortons/main.cpp +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright (C) 2018-2024 - 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 "nbl/this_example/builtin/build/spirv/keys.hpp" - -#include "nbl/application_templates/MonoDeviceApplication.hpp" -#include "nbl/examples/common/BuiltinResourcesApplication.hpp" - -#include "app_resources/common.hlsl" -#include "CTester.h" - -using namespace nbl::core; -using namespace nbl::hlsl; -using namespace nbl::system; -using namespace nbl::asset; -using namespace nbl::video; -using namespace nbl::examples; -using namespace nbl::application_templates; - -class MortonTest final : public MonoDeviceApplication, public BuiltinResourcesApplication -{ - using device_base_t = MonoDeviceApplication; - using asset_base_t = BuiltinResourcesApplication; -public: - MortonTest(const path& _localInputCWD, const path& _localOutputCWD, const path& _sharedInputCWD, const path& _sharedOutputCWD) : - IApplicationFramework(_localInputCWD, _localOutputCWD, _sharedInputCWD, _sharedOutputCWD) { - } - - bool onAppInitialized(smart_refctd_ptr&& system) override - { - // Remember to call the base class initialization! - if (!device_base_t::onAppInitialized(smart_refctd_ptr(system))) - return false; - if (!asset_base_t::onAppInitialized(std::move(system))) - return false; - // Some tests with mortons with emulated uint storage were cut off, it should be fine since each tested on their own produces correct results for each operator - // Blocked by https://github.com/KhronosGroup/SPIRV-Tools/issues/6104 - bool pass = true; - { - CTester::PipelineSetupData pplnSetupData; - pplnSetupData.device = m_device; - pplnSetupData.api = m_api; - pplnSetupData.assetMgr = m_assetMgr; - pplnSetupData.logger = m_logger; - pplnSetupData.physicalDevice = m_physicalDevice; - pplnSetupData.computeFamilyIndex = getComputeQueue()->getFamilyIndex(); - pplnSetupData.shaderKey = nbl::this_example::builtin::build::get_spirv_key<"test">(m_device.get()); - - CTester mortonTester(4); // 4 * 128 = 512 tests - mortonTester.setupPipeline(pplnSetupData); - pass &= mortonTester.performTestsAndVerifyResults("MortonTestLog.txt"); - } - { - CTester2::PipelineSetupData pplnSetupData; - pplnSetupData.device = m_device; - pplnSetupData.api = m_api; - pplnSetupData.assetMgr = m_assetMgr; - pplnSetupData.logger = m_logger; - pplnSetupData.physicalDevice = m_physicalDevice; - pplnSetupData.computeFamilyIndex = getComputeQueue()->getFamilyIndex(); - pplnSetupData.shaderKey = nbl::this_example::builtin::build::get_spirv_key<"test2">(m_device.get()); - - CTester2 mortonTester2(4); - mortonTester2.setupPipeline(reinterpret_cast(pplnSetupData)); - pass &= mortonTester2.performTestsAndVerifyResults("MortonTestLog2.txt"); - } - - return pass; - } - - void onAppTerminated_impl() override - { - m_device->waitIdle(); - } - - void workLoopBody() override - { - m_keepRunning = false; - } - - bool keepRunning() override - { - return m_keepRunning; - } - - -private: - bool m_keepRunning = true; -}; - -NBL_MAIN_FUNC(MortonTest) \ No newline at end of file diff --git a/15_MitsubaLoader/CMakeLists.txt b/15_MitsubaLoader/CMakeLists.txt deleted file mode 100644 index 3921c61d9..000000000 --- a/15_MitsubaLoader/CMakeLists.txt +++ /dev/null @@ -1,9 +0,0 @@ -list(APPEND NBL_INCLUDE_SERACH_DIRECTORIES - "${NBL_EXT_MITSUBA_LOADER_INCLUDE_DIRS}" -) -list(APPEND NBL_LIBRARIES - "${NBL_EXT_MITSUBA_LOADER_LIB}" -) - - -nbl_create_executable_project("" "" "${NBL_INCLUDE_SERACH_DIRECTORIES}" "${NBL_LIBRARIES}") diff --git a/15_MitsubaLoader/main.cpp b/15_MitsubaLoader/main.cpp deleted file mode 100644 index 2bd96ce16..000000000 --- a/15_MitsubaLoader/main.cpp +++ /dev/null @@ -1,131 +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 - -#include "nbl/examples/examples.hpp" -#include "nbl/ext/MitsubaLoader/CMitsubaLoader.h" -#include "nbl/ext/MitsubaLoader/CSerializedLoader.h" - -#include -#include - - -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; - -// Testing our Mitsuba Loader -class MitsubaLoaderTest final : public BuiltinResourcesApplication -{ - using base_t = BuiltinResourcesApplication; - - bool test(const system::path& listPath) - { - smart_refctd_ptr file; - { - ISystem::future_t> future; - using create_flags_t = IFileBase::E_CREATE_FLAGS; - m_system->createFile(future,listPath,create_flags_t::ECF_READ|create_flags_t::ECF_MAPPABLE); - if (!future.wait()) - return logFail("Failed to list of scenes to test with path %s",listPath.string().c_str()); - smart_refctd_ptr tmp; - future.acquire().move_into(tmp); - file = std::move(tmp); - } - if (!file) - return logFail("Failed to open list of scenes to test with path %s",listPath.string().c_str()); - - const auto base = file->getFileName().parent_path(); - const void* const ptr = file->getMappedPointer(); - const auto end = reinterpret_cast(ptr)+file->getSize(); - for (auto cursor=reinterpret_cast(ptr); cursor(std::isspace)); - if (cursor==end) - break; - auto nextLine = [&]()->const char* - { - constexpr std::array newlines = {'\r','\n'}; - auto retval = std::find_first_of(cursor,end,newlines.begin(),newlines.end()); - while (++retvalgetAsset(relPath,params); - if (asset.getContents().empty() || asset.getAssetType()!=IAsset::E_TYPE::ET_SCENE) - return logFail("Failed To Load %s",relPath.c_str()); - m_logger->log("Loaded %s",ILogger::ELL_INFO,relPath.c_str()); - // TODO: print True Material IR - // so we don't run out of RAM during testing - m_assetMgr->clearAllAssetCache(); - } - else if (*cursor!=';') - { - const char chr[2] = {*cursor,0}; - cursor = std::find(cursor,cursorEnd,'\"'); - if (cursor==cursorEnd) - return logFail("Parser Error, encountered unsupprted character %s near line start",chr); - } - cursor = cursorEnd; - } - return true; - } - - public: - // Yay thanks to multiple inheritance we cannot forward ctors anymore - MitsubaLoaderTest(const path& _localInputCWD, const path& _localOutputCWD, const path& _sharedInputCWD, const path& _sharedOutputCWD) : - system::IApplicationFramework(_localInputCWD, _localOutputCWD, _sharedInputCWD, _sharedOutputCWD) {} - - // we stuff all our work here because its a "single shot" app - bool onAppInitialized(smart_refctd_ptr&& system) override - { - if (!base_t::onAppInitialized(std::move(system))) - return false; - - m_assetMgr->addAssetLoader(make_smart_refctd_ptr(core::smart_refctd_ptr(m_system))); - // some of our test scenes won't load without the `.serialized` support - m_assetMgr->addAssetLoader(make_smart_refctd_ptr()); - - // public batch - if (!test(localInputCWD/"test_scenes.txt")) - return false; - if (!test(sharedInputCWD/"Ditt-Reference-Scenes/private_test_scenes.txt")) - return false; - - return true; - } - - // One-shot App - bool keepRunning() override { return false; } - - // One-shot App - void workLoopBody() override {} - - // Cleanup - bool onAppTerminated() override - { - return base_t::onAppTerminated(); - } -}; - - -NBL_MAIN_FUNC(MitsubaLoaderTest) \ No newline at end of file diff --git a/15_MitsubaLoader/test_scenes.txt b/15_MitsubaLoader/test_scenes.txt deleted file mode 100644 index 7a5938f86..000000000 --- a/15_MitsubaLoader/test_scenes.txt +++ /dev/null @@ -1,30 +0,0 @@ -; Here is my Commented line that batch file will skip (started with semicolons) -"../media/mitsuba/shapetest.xml" -"../media/mitsuba/daily_pt.xml" -"../media/mitsuba/brdf_eval_test.xml" -"../media/mitsuba/brdf_eval_test_as.xml" -"../media/mitsuba/brdf_eval_test_diffuse.xml" -"../media/mitsuba/brdf_eval_test_lambert.xml" -"../media/mitsuba/aniso_ies/72_render_0_2.xml" -;"../media/mitsuba/bathroom/scene.xml" ; pending OBJ loader merge -;"../media/mitsuba/bathroom2/scene.xml" ; pending OBJ loader merge -;"../media/mitsuba/bedroom/scene.xml" ; we'd need to commit uncompressed 100MB OBJ, and this example doesn't load from ZIP -;"../media/mitsuba/car2/scene.xml" ; pending OBJ loader merge -;"../media/mitsuba/coffee/scene.xml" ; pending OBJ loader merge -;"../media/mitsuba/classroom/scene.xml" ; skip because is not supported -"../media/mitsuba/ditt/render_720p.xml" -"../media/mitsuba/ditt/render_2160p.xml" -"../media/mitsuba/ditt/render_cube_lh.xml" -"../media/mitsuba/ditt/render_cube_rh.xml" -;"../media/mitsuba/glass-of-water/scene.xml" ; pending OBJ loader merge -;"../media/mitsuba/kitchen/scene.xml" ; pending OBJ loader merge -;"../media/mitsuba/lamp/scene.xml" ; skip because is not supported -;"../media/mitsuba/living-room/scene.xml" ; pending OBJ loader merge -;"../media/mitsuba/living-room-2/scene.xml" ; pending OBJ loader merge -"../media/mitsuba/iso_ies/71_render_0_2.xml" -"../media/mitsuba/messed_up_uvs/31_scene_0_1.xml" -"../media/mitsuba/normalmap_test/render_withnormalmap.xml" -"../media/mitsuba/normalmap_test/render_withoutnormalmap.xml" -;"../media/mitsuba/spaceship/scene.xml" ; pending OBJ loader merge -;"../media/mitsuba/staircase/scene.xml" ; pending OBJ loader merge -;"../media/mitsuba/staircase2/scene.xml" ; skip because is not supported diff --git a/16_ZipArchiveLoaderTest/CMakeLists.txt b/16_ZipArchiveLoaderTest/CMakeLists.txt deleted file mode 100644 index f60757aad..000000000 --- a/16_ZipArchiveLoaderTest/CMakeLists.txt +++ /dev/null @@ -1,32 +0,0 @@ -include(common RESULT_VARIABLE RES) -if(NOT RES) - message(FATAL_ERROR "common.cmake not found. Should be in {repo_root}/cmake directory") -endif() - -nbl_create_executable_project("" "" "" "" "${NBL_EXECUTABLE_PROJECT_CREATION_PCH_TARGET}") - -if(NBL_EMBED_BUILTIN_RESOURCES) - set(_BR_TARGET_ ${EXECUTABLE_NAME}_builtinResourceData) - set(RESOURCE_DIR "app_resources") - - get_filename_component(_SEARCH_DIRECTORIES_ "${CMAKE_CURRENT_SOURCE_DIR}" ABSOLUTE) - get_filename_component(_OUTPUT_DIRECTORY_SOURCE_ "${CMAKE_CURRENT_BINARY_DIR}/src" ABSOLUTE) - get_filename_component(_OUTPUT_DIRECTORY_HEADER_ "${CMAKE_CURRENT_BINARY_DIR}/include" ABSOLUTE) - - file(GLOB_RECURSE BUILTIN_RESOURCE_FILES RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}/${RESOURCE_DIR}" CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/${RESOURCE_DIR}/*") - foreach(RES_FILE ${BUILTIN_RESOURCE_FILES}) - LIST_BUILTIN_RESOURCE(RESOURCES_TO_EMBED "${RES_FILE}") - endforeach() - - ADD_CUSTOM_BUILTIN_RESOURCES(${_BR_TARGET_} RESOURCES_TO_EMBED "${_SEARCH_DIRECTORIES_}" "${RESOURCE_DIR}" "nbl::this_example::builtin" "${_OUTPUT_DIRECTORY_HEADER_}" "${_OUTPUT_DIRECTORY_SOURCE_}") - - LINK_BUILTIN_RESOURCES_TO_TARGET(${EXECUTABLE_NAME} ${_BR_TARGET_}) -endif() - -enable_testing() - -add_test(NAME NBL_ZIP_ARCHIVE_LOADER_TEST - COMMAND "$" - WORKING_DIRECTORY "$" - COMMAND_EXPAND_LISTS -) diff --git a/16_ZipArchiveLoaderTest/main.cpp b/16_ZipArchiveLoaderTest/main.cpp deleted file mode 100644 index ccdef7a06..000000000 --- a/16_ZipArchiveLoaderTest/main.cpp +++ /dev/null @@ -1,266 +0,0 @@ -// Copyright (C) 2018-2024 - 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 - -using namespace nbl; -using namespace nbl::core; -using namespace nbl::system; -using namespace nbl::asset; -using namespace nbl::examples; - -class ZipArchiveLoaderTest final : public BuiltinResourcesApplication -{ - using asset_base_t = BuiltinResourcesApplication; - -public: - ZipArchiveLoaderTest(const path& _localInputCWD, const path& _localOutputCWD, const path& _sharedInputCWD, const path& _sharedOutputCWD) : - IApplicationFramework(_localInputCWD, _localOutputCWD, _sharedInputCWD, _sharedOutputCWD) { - } - - bool onAppInitialized(smart_refctd_ptr&& system) override - { - if (!asset_base_t::onAppInitialized(std::move(system))) - return false; - - const path zipPath = sharedInputCWD / "mitsuba/bedroom.zip"; - auto archive = m_system->openFileArchive(zipPath); - if (!archive) - { - m_logger->log("Failed to open zip archive: %s", ILogger::ELL_ERROR, zipPath.string().c_str()); - return false; - } - - auto archiveFiles = IFileArchive::SFileList::span_t(archive->listAssets()); - if (archiveFiles.empty()) - { - m_logger->log("Zip archive is empty: %s", ILogger::ELL_ERROR, zipPath.string().c_str()); - return false; - } - - const path scenePath = "scene.xml"; - auto sceneIt = std::find_if(archiveFiles.begin(), archiveFiles.end(), [&scenePath](const auto& entry) - { - return entry.pathRelativeToArchive == scenePath; - }); - if (sceneIt == archiveFiles.end()) - { - m_logger->log("Zip archive missing scene.xml: %s", ILogger::ELL_ERROR, zipPath.string().c_str()); - return false; - } - - auto sceneFile = archive->getFile(scenePath, IFileBase::ECF_READ, ""); - if (!sceneFile) - { - m_logger->log("Failed to open scene.xml from zip: %s", ILogger::ELL_ERROR, zipPath.string().c_str()); - return false; - } - - if (sceneIt->size == 0 || sceneFile->getSize() != sceneIt->size) - { - m_logger->log("scene.xml size mismatch in zip: %s", ILogger::ELL_ERROR, zipPath.string().c_str()); - return false; - } - - const size_t probeSize = std::min(sceneIt->size, 64u); - std::array probe{}; - IFile::success_t probeRead; - sceneFile->read(probeRead, probe.data(), 0, probeSize); - if (!probeRead) - { - m_logger->log("Failed to read scene.xml from zip: %s", ILogger::ELL_ERROR, zipPath.string().c_str()); - return false; - } - - const std::string_view probeView(probe.data(), probeSize); - if (probeView.find("log("scene.xml header is unexpected in zip: %s", ILogger::ELL_ERROR, zipPath.string().c_str()); - return false; - } - - const size_t linesToPrint = 6u; - const char* mapped = static_cast(sceneFile->getMappedPointer()); - if (mapped) - { - std::vector headLines; - headLines.reserve(linesToPrint); - std::deque tailLines; - - size_t lineStart = 0; - for (size_t i = 0; i < sceneIt->size; ++i) - { - if (mapped[i] != '\n') - continue; - - size_t lineLen = i - lineStart; - if (lineLen && mapped[i - 1] == '\r') - --lineLen; - - const std::string_view line(mapped + lineStart, lineLen); - if (headLines.size() < linesToPrint) - headLines.push_back(line); - if (tailLines.size() == linesToPrint) - tailLines.pop_front(); - tailLines.push_back(line); - lineStart = i + 1; - } - if (lineStart < sceneIt->size) - { - size_t lineLen = sceneIt->size - lineStart; - if (lineLen && mapped[sceneIt->size - 1] == '\r') - --lineLen; - const std::string_view line(mapped + lineStart, lineLen); - if (headLines.size() < linesToPrint) - headLines.push_back(line); - if (tailLines.size() == linesToPrint) - tailLines.pop_front(); - tailLines.push_back(line); - } - - std::string head; - for (const auto& line : headLines) - { - head.append(line); - head.push_back('\n'); - } - std::string tail; - for (const auto& line : tailLines) - { - tail.append(line); - tail.push_back('\n'); - } - - m_logger->log("scene.xml head (%u lines):\n%s", ILogger::ELL_INFO, static_cast(headLines.size()), head.c_str()); - m_logger->log("scene.xml tail (%u lines):\n%s", ILogger::ELL_INFO, static_cast(tailLines.size()), tail.c_str()); - } - else - { - std::vector headLines; - headLines.reserve(linesToPrint); - std::deque tailLines; - std::string carry; - const size_t chunkSize = 64u * 1024u; - std::string buffer(chunkSize, '\0'); - size_t offset = 0; - while (offset < sceneIt->size) - { - const size_t toRead = std::min(chunkSize, sceneIt->size - offset); - IFile::success_t chunkRead; - sceneFile->read(chunkRead, buffer.data(), offset, toRead); - if (!chunkRead) - { - m_logger->log("Failed to read scene.xml from zip: %s", ILogger::ELL_ERROR, zipPath.string().c_str()); - return false; - } - - size_t lineStart = 0; - for (size_t i = 0; i < toRead; ++i) - { - if (buffer[i] != '\n') - continue; - - size_t lineEnd = i; - if (lineEnd > lineStart && buffer[lineEnd - 1] == '\r') - --lineEnd; - - std::string line; - if (!carry.empty()) - { - line = carry; - if (lineEnd > lineStart) - line.append(buffer.data() + lineStart, lineEnd - lineStart); - if (!line.empty() && line.back() == '\r') - line.pop_back(); - carry.clear(); - } - else - { - line.assign(buffer.data() + lineStart, lineEnd - lineStart); - } - - if (headLines.size() < linesToPrint) - headLines.push_back(line); - if (tailLines.size() == linesToPrint) - tailLines.pop_front(); - tailLines.push_back(std::move(line)); - lineStart = i + 1; - } - - if (lineStart < toRead) - { - const size_t tailSize = toRead - lineStart; - if (carry.empty()) - carry.assign(buffer.data() + lineStart, tailSize); - else - carry.append(buffer.data() + lineStart, tailSize); - } - - offset += toRead; - } - if (!carry.empty()) - { - if (!carry.empty() && carry.back() == '\r') - carry.pop_back(); - if (headLines.size() < linesToPrint) - headLines.push_back(carry); - if (tailLines.size() == linesToPrint) - tailLines.pop_front(); - tailLines.push_back(carry); - carry.clear(); - } - - std::string head; - for (const auto& line : headLines) - { - head.append(line); - head.push_back('\n'); - } - std::string tail; - for (const auto& line : tailLines) - { - tail.append(line); - tail.push_back('\n'); - } - - m_logger->log("scene.xml head (%u lines):\n%s", ILogger::ELL_INFO, static_cast(headLines.size()), head.c_str()); - m_logger->log("scene.xml tail (%u lines):\n%s", ILogger::ELL_INFO, static_cast(tailLines.size()), tail.c_str()); - } - - std::stringstream ss; - for (const auto& file : archiveFiles) - { - ss << "ID: " << file.ID; - ss << " offset: " << file.offset; - ss << " path relative od archive: " << file.pathRelativeToArchive; - ss << " size: " << file.size << '\n'; - } - - m_logger->log(ss.str().c_str(), ILogger::ELL_PERFORMANCE); - - return true; - } - - void onAppTerminated_impl() override - { - } - - void workLoopBody() override - { - } - - bool keepRunning() override - { - return false; - } -}; - -NBL_MAIN_FUNC(ZipArchiveLoaderTest) diff --git a/20_AllocatorTest/main.cpp b/20_AllocatorTest/main.cpp index e8292ca9f..c6f55e2d3 100644 --- a/20_AllocatorTest/main.cpp +++ b/20_AllocatorTest/main.cpp @@ -309,40 +309,6 @@ class AllocatorTestApp final : public nbl::application_templates::MonoSystemMono if (!base_t::onAppInitialized(std::move(system))) return false; - // Special Test (repro by Erfan) - { - using AddressAllocator = nbl::core::GeneralpurposeAddressAllocator; - using ReservedAllocator = nbl::core::allocator; - - static constexpr uint64_t TotalSize = 14280 * 1024u; // safe choice based on hardware reports - static constexpr uint64_t MaxMemoryAlignment = 4096u; // safe choice based on hardware reports - static constexpr uint64_t MinAllocSize = 128 * 1024u; // 128KB, the larger this is the better - - auto m_reservedAllocSize = AddressAllocator::reserved_size(MaxMemoryAlignment, TotalSize, MinAllocSize); - auto m_reservedAllocator = std::unique_ptr(new ReservedAllocator()); - auto m_reservedAlloc = m_reservedAllocator->allocate(m_reservedAllocSize, _NBL_SIMD_ALIGNMENT); - auto m_addressAllocator = std::unique_ptr(new AddressAllocator( - m_reservedAlloc, 0u, 0u, MaxMemoryAlignment, TotalSize, MinAllocSize - )); - - // 1. Allocate with `14622720` successful, Free Size = 0 - auto offset1 = m_addressAllocator->alloc_addr(TotalSize, 1024u); assert(offset1 != AddressAllocator::invalid_address); - // 2. Allocate with `9240576` fails (as expected), Free Size = 0 - auto offset2 = m_addressAllocator->alloc_addr(9240576, 1024u); assert(offset2 == AddressAllocator::invalid_address); - // 3. Free Initial Allocation (size=14622720, offset=0) ==> Allocator Free Size=14622720 - m_addressAllocator->free_addr(offset1, TotalSize); - - // 4. Allocate with `9240576` successful (as expected), Free Size = 5382144 - auto offset3 = m_addressAllocator->alloc_addr(9240576, 1024u); assert(offset3 != AddressAllocator::invalid_address); - // 5. Allocate with `14622720` fails (as expected), Free Size = 5382144 - auto offset4 = m_addressAllocator->alloc_addr(TotalSize, 1024u); assert(offset4 == AddressAllocator::invalid_address); - // 6. Free Second Allocation (size=9240576, offset=0) ==> Allocator Free Size=14622720 - m_addressAllocator->free_addr(offset3, 9240576); - - // 7. Allocate with `14622720` fails (UNEXPECTED), Free Size = 0 - auto offset5 = m_addressAllocator->alloc_addr(TotalSize, 1024u); assert(offset5 != AddressAllocator::invalid_address); - } - // Allocator test { { diff --git a/21_LRUCacheUnitTest/main.cpp b/21_LRUCacheUnitTest/main.cpp index 467c6d4e4..6fb7b7066 100644 --- a/21_LRUCacheUnitTest/main.cpp +++ b/21_LRUCacheUnitTest/main.cpp @@ -5,7 +5,6 @@ // I've moved out a tiny part of this example into a shared header for reuse, please open and read it. #include "nbl/application_templates/MonoSystemMonoLoggerApplication.hpp" -#include using namespace nbl; using namespace core; @@ -34,7 +33,7 @@ class LRUCacheTestApp final : public nbl::application_templates::MonoSystemMonoL inline TextureReference& operator=(uint64_t semamphoreVal) { lastUsedSemaphoreValue = semamphoreVal; return *this; } }; - using TextureLRUCache = core::ResizableLRUCache; + using TextureLRUCache = core::LRUCache; // we stuff all our work here because its a "single shot" app bool onAppInitialized(smart_refctd_ptr&& system) override @@ -45,18 +44,12 @@ class LRUCacheTestApp final : public nbl::application_templates::MonoSystemMonoL m_logger->log("LRU cache unit test"); m_logger->log("Testing large cache..."); - ResizableLRUCache hugeCache(50000000u); + LRUCache hugeCache(50000000u); hugeCache.insert(0, '0'); hugeCache.print(m_logger); - // Use this to ensure the disposal function is properly called on every element of the first cache - ResizableLRUCache::disposal_func_t df([&](std::pair a) - { - std::ostringstream tmp; - tmp << "Disposal function called on element (" << a.first << ", " << a.second << ")"; - m_logger->log(tmp.str()); - }); - ResizableLRUCache cache(5u, std::move(df)); + + LRUCache cache(5u); m_logger->log("Testing insert with const key, const val..."); //const, const @@ -87,11 +80,8 @@ class LRUCacheTestApp final : public nbl::application_templates::MonoSystemMonoL //non const, const int i = 0; cache.insert(++i, '1'); - assert(cache.getState() == "{12, e}, {10, c}, {11, d}, {13, f}, {1, 1}"); cache.insert(++i, '2'); - assert(cache.getState() == "{10, c}, {11, d}, {13, f}, {1, 1}, {2, 2}"); cache.insert(++i, '3'); - assert(cache.getState() == "{11, d}, {13, f}, {1, 1}, {2, 2}, {3, 3}"); returned = *(cache.get(1)); assert(returned == '1'); @@ -121,17 +111,7 @@ class LRUCacheTestApp final : public nbl::application_templates::MonoSystemMonoL auto peekedNullptr = cache.peek(5); assert(peekedNullptr == nullptr); - // Try clearing the cache - m_logger->log("Clearing test"); - m_logger->log("Print contents before clearing"); - cache.print(m_logger); - assert(cache.getState() == "{2, 2}, {3, 3}, {1, 1}, {4, N}, {6, Y}"); - cache.clear(); - m_logger->log("Print contents after clearing"); - cache.print(m_logger); - assert(cache.getState() == ""); - - ResizableLRUCache cache2(5u); + core::LRUCache cache2(5u); cache2.insert(500, "five hundred"); //inserts at addr = 0 cache2.insert(510, "five hundred and ten"); //inserts at addr = 472 @@ -144,112 +124,6 @@ class LRUCacheTestApp final : public nbl::application_templates::MonoSystemMonoL cache2.print(m_logger); cache2.insert(++i, "key is 112"); - // Grow test - try growing the cache - m_logger->log("Growing test"); - auto previousState = cache2.getState(); - // Grow cache - assert(cache2.grow(10)); - // Cache state should be the same - assert(cache2.getState() == previousState); - cache2.print(m_logger); - cache2.insert(++i, "key is 113"); - cache2.insert(++i, "key is 114"); - cache2.insert(++i, "key is 115"); - cache2.insert(++i, "key is 116"); - cache2.insert(++i, "key is 117"); - cache2.print(m_logger); - // Should evict key 52 - cache2.insert(++i, "key is 118"); - cache2.print(m_logger); - const auto latestState = cache2.getState(); - assert(latestState == "{21, key is 21}, {22, key is 22}, {23, key is 23}, {112, key is 112}, {113, key is 113}, {114, key is 114}, {115, key is 115}, {116, key is 116}, {117, key is 117}, {118, key is 118}"); - // Invalid grow should fail - assert(!cache2.grow(5)); - assert(!cache2.grow(10)); - // Call a bunch of grows that shouldn't fail and some others that should - for (auto i = 1u; i < 50; i++) - { - assert(cache2.grow(50 * i)); - assert(!cache2.grow(25 * i)); - assert(!cache2.grow(50 * i)); - assert(cache2.getState() == latestState); - } - - // Single element cache test - checking for edge cases - ResizableLRUCache cache3(1u); - cache3.insert(0, "foo"); - cache3.insert(1, "bar"); - cache3.clear(); - - // Cache iterator test - constexpr uint32_t cache4Size = 10; - ResizableLRUCache cache4(cache4Size); - for (auto i = 0u; i < cache4Size; i++) - { - cache4.insert(i, i); - } - // Default iterator is MRU -> LRU - uint32_t counter = cache4Size - 1; - for (auto& pair : cache4) - { - assert(pair.first == counter && pair.second == counter); - counter--; - } - // Reverse LRU -> MRU traversal - counter = 0u; - for (auto it = cache4.crbegin(); it != cache4.crend(); it++) - { - assert(it->first == counter && it->second == counter); - counter++; - } - - // Cache copy test - ResizableLRUCache cache4Copy(cache4); - for (auto it = cache4.cbegin(), itCopy = cache4Copy.cbegin(); it != cache4.cend(); it++, itCopy++) - { - assert(*it == *itCopy); - // Assert deep copy - assert(it.operator->() != itCopy.operator->()); - - } - - // Besides the disposal function that gets called when evicting, we need to check that the Cache properly destroys all resident `Key,Value` pairs when destroyed - struct Foo - { - int* destroyCounter; - - Foo(int* _destroyCounter) : destroyCounter(_destroyCounter){} - - void operator=(Foo&& other) - { - destroyCounter = other.destroyCounter; - other.destroyCounter = nullptr; - } - - Foo(Foo&& other) - { - operator=(std::move(other)); - } - - ~Foo() - { - // Only count destructions of objects resident in Cache and not ones that happen right after moving out of - if (destroyCounter) - (*destroyCounter)++; - } - }; - - int destroyCounter = 0; - { - ResizableLRUCache cache5(10u); - for (int i = 0; i < 10; i++) - cache5.insert(i, Foo(&destroyCounter)); - int x = 0; - } - assert(destroyCounter == 10); - - m_logger->log("all good"); - m_textureLRUCache = std::unique_ptr(new TextureLRUCache(1024u)); { SIntendedSubmitInfo intendedNextSubmit = {}; @@ -268,6 +142,11 @@ class LRUCacheTestApp final : public nbl::application_templates::MonoSystemMonoL TextureReference* inserted = m_textureLRUCache->insert(69420, nextSemaSignal.value, evictionCallback); } + #ifdef _NBL_DEBUG + cache2.print(m_logger); + #endif + m_logger->log("all good"); + constexpr uint32_t InvalidIdx = ~0u; struct TextureReference { @@ -289,7 +168,7 @@ class LRUCacheTestApp final : public nbl::application_templates::MonoSystemMonoL // In LRU Cache `insert` function, in case of cache hit, we need to assign semaphore value to TextureReference without changing `alloc_idx` inline TextureReference& operator=(uint64_t semamphoreVal) { lastUsedSemaphoreValue = semamphoreVal; return *this; } }; - using TextureLRUCache = ResizableLRUCache; + using TextureLRUCache = LRUCache; TextureLRUCache textureCache = TextureLRUCache(3u); diff --git a/22_CppCompat/CIntrinsicsTester.h b/22_CppCompat/CIntrinsicsTester.h deleted file mode 100644 index 49597b4d8..000000000 --- a/22_CppCompat/CIntrinsicsTester.h +++ /dev/null @@ -1,287 +0,0 @@ -#ifndef _NBL_EXAMPLES_TESTS_22_CPP_COMPAT_C_INTRINSICS_TESTER_INCLUDED_ -#define _NBL_EXAMPLES_TESTS_22_CPP_COMPAT_C_INTRINSICS_TESTER_INCLUDED_ - - -#include "nbl/examples/examples.hpp" - -#include "app_resources/common.hlsl" - - -using namespace nbl; - -class CIntrinsicsTester final : public ITester -{ - using base_t = ITester; - -public: - CIntrinsicsTester(const uint32_t testBatchCount) - : base_t(testBatchCount) {}; - -private: - IntrinsicsIntputTestValues generateInputTestValues() override - { - std::uniform_real_distribution realDistributionNeg(-50.0f, -1.0f); - std::uniform_real_distribution realDistributionPos(1.0f, 50.0f); - std::uniform_real_distribution realDistributionZeroToOne(0.0f, 1.0f); - std::uniform_real_distribution realDistribution(-100.0f, 100.0f); - std::uniform_real_distribution realDistributionSmall(1.0f, 4.0f); - std::uniform_int_distribution intDistribution(-100, 100); - std::uniform_int_distribution uintDistribution(0, 100); - - IntrinsicsIntputTestValues testInput; - testInput.bitCount = intDistribution(getRandomEngine()); - testInput.crossLhs = float32_t3(realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), realDistribution(getRandomEngine())); - testInput.crossRhs = float32_t3(realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), realDistribution(getRandomEngine())); - testInput.clampVal = realDistribution(getRandomEngine()); - testInput.clampMin = realDistributionNeg(getRandomEngine()); - testInput.clampMax = realDistributionPos(getRandomEngine()); - testInput.length = float32_t3(realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), realDistribution(getRandomEngine())); - testInput.normalize = float32_t3(realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), realDistribution(getRandomEngine())); - testInput.dotLhs = float32_t3(realDistributionSmall(getRandomEngine()), realDistributionSmall(getRandomEngine()), realDistributionSmall(getRandomEngine())); - testInput.dotRhs = float32_t3(realDistributionSmall(getRandomEngine()), realDistributionSmall(getRandomEngine()), realDistributionSmall(getRandomEngine())); - testInput.determinant = float32_t3x3( - realDistributionSmall(getRandomEngine()), realDistributionSmall(getRandomEngine()), realDistributionSmall(getRandomEngine()), - realDistributionSmall(getRandomEngine()), realDistributionSmall(getRandomEngine()), realDistributionSmall(getRandomEngine()), - realDistributionSmall(getRandomEngine()), realDistributionSmall(getRandomEngine()), realDistributionSmall(getRandomEngine()) - ); - testInput.findMSB = realDistribution(getRandomEngine()); - testInput.findLSB = realDistribution(getRandomEngine()); - testInput.inverse = float32_t3x3( - realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), - realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), - realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), realDistribution(getRandomEngine()) - ); - testInput.transpose = float32_t3x3( - realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), - realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), - realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), realDistribution(getRandomEngine()) - ); - testInput.mulLhs = float32_t3x3( - realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), - realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), - realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), realDistribution(getRandomEngine()) - ); - testInput.mulRhs = float32_t3x3( - realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), - realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), - realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), realDistribution(getRandomEngine()) - ); - testInput.pseudoMat3x4 = float32_t4x4( - realDistribution(getRandomEngine()), 0, 0, realDistribution(getRandomEngine()), - 0, realDistribution(getRandomEngine()), 0, realDistribution(getRandomEngine()), - 0, 0, realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), - 0,0,0,1 - ); - testInput.minA = realDistribution(getRandomEngine()); - testInput.minB = realDistribution(getRandomEngine()); - testInput.maxA = realDistribution(getRandomEngine()); - testInput.maxB = realDistribution(getRandomEngine()); - testInput.rsqrt = realDistributionPos(getRandomEngine()); - testInput.bitReverse = realDistribution(getRandomEngine()); - testInput.frac = realDistribution(getRandomEngine()); - testInput.mixX = realDistributionNeg(getRandomEngine()); - testInput.mixY = realDistributionPos(getRandomEngine()); - testInput.mixA = realDistributionZeroToOne(getRandomEngine()); - testInput.sign = realDistribution(getRandomEngine()); - testInput.radians = realDistribution(getRandomEngine()); - testInput.degrees = realDistribution(getRandomEngine()); - testInput.stepEdge = realDistribution(getRandomEngine()); - testInput.stepX = realDistribution(getRandomEngine()); - testInput.smoothStepEdge0 = realDistributionNeg(getRandomEngine()); - testInput.smoothStepEdge1 = realDistributionPos(getRandomEngine()); - testInput.smoothStepX = realDistribution(getRandomEngine()); - - testInput.bitCountVec = int32_t3(intDistribution(getRandomEngine()), intDistribution(getRandomEngine()), intDistribution(getRandomEngine())); - testInput.clampValVec = float32_t3(realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), realDistribution(getRandomEngine())); - testInput.clampMinVec = float32_t3(realDistributionNeg(getRandomEngine()), realDistributionNeg(getRandomEngine()), realDistributionNeg(getRandomEngine())); - testInput.clampMaxVec = float32_t3(realDistributionPos(getRandomEngine()), realDistributionPos(getRandomEngine()), realDistributionPos(getRandomEngine())); - testInput.findMSBVec = uint32_t3(uintDistribution(getRandomEngine()), uintDistribution(getRandomEngine()), uintDistribution(getRandomEngine())); - testInput.findLSBVec = uint32_t3(uintDistribution(getRandomEngine()), uintDistribution(getRandomEngine()), uintDistribution(getRandomEngine())); - testInput.minAVec = float32_t3(realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), realDistribution(getRandomEngine())); - testInput.minBVec = float32_t3(realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), realDistribution(getRandomEngine())); - testInput.maxAVec = float32_t3(realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), realDistribution(getRandomEngine())); - testInput.maxBVec = float32_t3(realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), realDistribution(getRandomEngine())); - testInput.rsqrtVec = float32_t3(realDistributionPos(getRandomEngine()), realDistributionPos(getRandomEngine()), realDistributionPos(getRandomEngine())); - testInput.bitReverseVec = uint32_t3(uintDistribution(getRandomEngine()), uintDistribution(getRandomEngine()), uintDistribution(getRandomEngine())); - testInput.fracVec = float32_t3(realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), realDistribution(getRandomEngine())); - testInput.mixXVec = float32_t3(realDistributionNeg(getRandomEngine()), realDistributionNeg(getRandomEngine()), realDistributionNeg(getRandomEngine())); - testInput.mixYVec = float32_t3(realDistributionPos(getRandomEngine()), realDistributionPos(getRandomEngine()), realDistributionPos(getRandomEngine())); - testInput.mixAVec = float32_t3(realDistributionZeroToOne(getRandomEngine()), realDistributionZeroToOne(getRandomEngine()), realDistributionZeroToOne(getRandomEngine())); - - testInput.signVec = float32_t3(realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), realDistribution(getRandomEngine())); - testInput.radiansVec = float32_t3(realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), realDistribution(getRandomEngine())); - testInput.degreesVec = float32_t3(realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), realDistribution(getRandomEngine())); - testInput.stepEdgeVec = float32_t3(realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), realDistribution(getRandomEngine())); - testInput.stepXVec = float32_t3(realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), realDistribution(getRandomEngine())); - testInput.smoothStepEdge0Vec = float32_t3(realDistributionNeg(getRandomEngine()), realDistributionNeg(getRandomEngine()), realDistributionNeg(getRandomEngine())); - testInput.smoothStepEdge1Vec = float32_t3(realDistributionPos(getRandomEngine()), realDistributionPos(getRandomEngine()), realDistributionPos(getRandomEngine())); - testInput.smoothStepXVec = float32_t3(realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), realDistribution(getRandomEngine())); - testInput.faceForwardN = float32_t3(realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), realDistribution(getRandomEngine())); - testInput.faceForwardI = float32_t3(realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), realDistribution(getRandomEngine())); - testInput.faceForwardNref = float32_t3(realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), realDistribution(getRandomEngine())); - testInput.reflectI = float32_t3(realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), realDistribution(getRandomEngine())); - testInput.reflectN = glm::normalize(float32_t3(realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), realDistribution(getRandomEngine()))); - testInput.refractI = float32_t3(realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), realDistribution(getRandomEngine())); - testInput.refractN = glm::normalize(float32_t3(realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), realDistribution(getRandomEngine()))); - testInput.refractEta = realDistribution(getRandomEngine()); - - return testInput; - } - - IntrinsicsTestValues determineExpectedResults(const IntrinsicsIntputTestValues& testInput) override - { - IntrinsicsTestValues expected; - expected.bitCount = glm::bitCount(testInput.bitCount); - expected.clamp = glm::clamp(testInput.clampVal, testInput.clampMin, testInput.clampMax); - expected.length = glm::length(testInput.length); - expected.dot = glm::dot(testInput.dotLhs, testInput.dotRhs); - expected.determinant = glm::determinant(reinterpret_cast(testInput.determinant)); - expected.findMSB = glm::findMSB(testInput.findMSB); - expected.findLSB = glm::findLSB(testInput.findLSB); - expected.min = glm::min(testInput.minA, testInput.minB); - expected.max = glm::max(testInput.maxA, testInput.maxB); - expected.rsqrt = (1.0f / std::sqrt(testInput.rsqrt)); - expected.mix = std::lerp(testInput.mixX, testInput.mixY, testInput.mixA); - expected.sign = glm::sign(testInput.sign); - expected.radians = glm::radians(testInput.radians); - expected.degrees = glm::degrees(testInput.degrees); - expected.step = glm::step(testInput.stepEdge, testInput.stepX); - expected.smoothStep = glm::smoothstep(testInput.smoothStepEdge0, testInput.smoothStepEdge1, testInput.smoothStepX); - - expected.addCarry.result = glm::uaddCarry(testInput.addCarryA, testInput.addCarryB, expected.addCarry.carry); - expected.subBorrow.result = glm::usubBorrow(testInput.subBorrowA, testInput.subBorrowB, expected.subBorrow.borrow); - - expected.frac = testInput.frac - std::floor(testInput.frac); - expected.bitReverse = glm::bitfieldReverse(testInput.bitReverse); - - expected.normalize = glm::normalize(testInput.normalize); - expected.cross = glm::cross(testInput.crossLhs, testInput.crossRhs); - expected.bitCountVec = int32_t3(glm::bitCount(testInput.bitCountVec.x), glm::bitCount(testInput.bitCountVec.y), glm::bitCount(testInput.bitCountVec.z)); - expected.clampVec = float32_t3( - glm::clamp(testInput.clampValVec.x, testInput.clampMinVec.x, testInput.clampMaxVec.x), - glm::clamp(testInput.clampValVec.y, testInput.clampMinVec.y, testInput.clampMaxVec.y), - glm::clamp(testInput.clampValVec.z, testInput.clampMinVec.z, testInput.clampMaxVec.z) - ); - expected.findMSBVec = glm::findMSB(testInput.findMSBVec); - expected.findLSBVec = glm::findLSB(testInput.findLSBVec); - expected.minVec = float32_t3( - glm::min(testInput.minAVec.x, testInput.minBVec.x), - glm::min(testInput.minAVec.y, testInput.minBVec.y), - glm::min(testInput.minAVec.z, testInput.minBVec.z) - ); - expected.maxVec = float32_t3( - glm::max(testInput.maxAVec.x, testInput.maxBVec.x), - glm::max(testInput.maxAVec.y, testInput.maxBVec.y), - glm::max(testInput.maxAVec.z, testInput.maxBVec.z) - ); - expected.rsqrtVec = float32_t3(1.0f / std::sqrt(testInput.rsqrtVec.x), 1.0f / std::sqrt(testInput.rsqrtVec.y), 1.0f / std::sqrt(testInput.rsqrtVec.z)); - expected.bitReverseVec = glm::bitfieldReverse(testInput.bitReverseVec); - expected.fracVec = float32_t3( - testInput.fracVec.x - std::floor(testInput.fracVec.x), - testInput.fracVec.y - std::floor(testInput.fracVec.y), - testInput.fracVec.z - std::floor(testInput.fracVec.z)); - expected.mixVec.x = std::lerp(testInput.mixXVec.x, testInput.mixYVec.x, testInput.mixAVec.x); - expected.mixVec.y = std::lerp(testInput.mixXVec.y, testInput.mixYVec.y, testInput.mixAVec.y); - expected.mixVec.z = std::lerp(testInput.mixXVec.z, testInput.mixYVec.z, testInput.mixAVec.z); - - expected.signVec = glm::sign(testInput.signVec); - expected.radiansVec = glm::radians(testInput.radiansVec); - expected.degreesVec = glm::degrees(testInput.degreesVec); - expected.stepVec = glm::step(testInput.stepEdgeVec, testInput.stepXVec); - expected.smoothStepVec = glm::smoothstep(testInput.smoothStepEdge0Vec, testInput.smoothStepEdge1Vec, testInput.smoothStepXVec); - expected.faceForward = glm::faceforward(testInput.faceForwardN, testInput.faceForwardI, testInput.faceForwardNref); - expected.reflect = glm::reflect(testInput.reflectI, testInput.reflectN); - expected.refract = glm::refract(testInput.refractI, testInput.refractN, testInput.refractEta); - - expected.addCarryVec.result = glm::uaddCarry(testInput.addCarryAVec, testInput.addCarryBVec, expected.addCarryVec.carry); - expected.subBorrowVec.result = glm::usubBorrow(testInput.subBorrowAVec, testInput.subBorrowBVec, expected.subBorrowVec.borrow); - - auto mulGlm = nbl::hlsl::mul(testInput.mulLhs, testInput.mulRhs); - expected.mul = reinterpret_cast(mulGlm); - auto transposeGlm = glm::transpose(reinterpret_cast(testInput.transpose)); - expected.transpose = reinterpret_cast(transposeGlm); - auto inverseGlm = glm::inverse(reinterpret_cast(testInput.inverse)); - expected.inverse = reinterpret_cast(inverseGlm); - - auto inverse3x4Glm = hlsl::inverse(testInput.pseudoMat3x4); - expected.pseudoInverse3x4 = nbl::hlsl::mul(inverse3x4Glm, float32_t4(testInput.length, 1)); - - return expected; - } - - bool verifyTestResults(const IntrinsicsTestValues& expectedTestValues, const IntrinsicsTestValues& testValues, const size_t testIteration, const uint32_t seed, TestType testType) override - { - volatile float lengthToleranace = 0.00001; - volatile float dotToleranace = 0.00001; - volatile float determinantToleranace = 0.000212669; - volatile float rsqrtTolerance = 1.1922e-07; - volatile float mixTolerance = 0.00001; // for now - volatile float radiansToleranace = 0.000001; - volatile float degreesToleranace = 0.000001; - volatile float smoothstepToleranace = 3.57628e-07; - volatile float normalizeToleranace = 0.0000001; - volatile float reflectToleranace = 0.0001; - volatile float refractToleranace = 0.001; - volatile float matrixMulToleranace = 0.00001; - volatile float inverseToleranace = 0.0001; - volatile float pseudoInverseToleranace = 0.0001; - - bool pass = true; - pass &= verifyTestValue("bitCount", expectedTestValues.bitCount, testValues.bitCount, testIteration, seed, testType); - pass &= verifyTestValue("clamp", expectedTestValues.clamp, testValues.clamp, testIteration, seed, testType); - pass &= verifyTestValue("length", expectedTestValues.length, testValues.length, testIteration, seed, testType, lengthToleranace); - pass &= verifyTestValue("dot", expectedTestValues.dot, testValues.dot, testIteration, seed, testType, dotToleranace); - pass &= verifyTestValue("determinant", expectedTestValues.determinant, testValues.determinant, testIteration, seed, testType, determinantToleranace); - pass &= verifyTestValue("findMSB", expectedTestValues.findMSB, testValues.findMSB, testIteration, seed, testType); - pass &= verifyTestValue("findLSB", expectedTestValues.findLSB, testValues.findLSB, testIteration, seed, testType); - pass &= verifyTestValue("min", expectedTestValues.min, testValues.min, testIteration, seed, testType); - pass &= verifyTestValue("max", expectedTestValues.max, testValues.max, testIteration, seed, testType); - pass &= verifyTestValue("rsqrt", expectedTestValues.rsqrt, testValues.rsqrt, testIteration, seed, testType, rsqrtTolerance); - pass &= verifyTestValue("frac", expectedTestValues.frac, testValues.frac, testIteration, seed, testType); - pass &= verifyTestValue("bitReverse", expectedTestValues.bitReverse, testValues.bitReverse, testIteration, seed, testType); - pass &= verifyTestValue("mix", expectedTestValues.mix, testValues.mix, testIteration, seed, testType, mixTolerance); - pass &= verifyTestValue("sign", expectedTestValues.sign, testValues.sign, testIteration, seed, testType); - pass &= verifyTestValue("radians", expectedTestValues.radians, testValues.radians, testIteration, seed, testType, radiansToleranace); - pass &= verifyTestValue("degrees", expectedTestValues.degrees, testValues.degrees, testIteration, seed, testType, degreesToleranace); - pass &= verifyTestValue("step", expectedTestValues.step, testValues.step, testIteration, seed, testType); - pass &= verifyTestValue("smoothStep", expectedTestValues.smoothStep, testValues.smoothStep, testIteration, seed, testType, smoothstepToleranace); - pass &= verifyTestValue("addCarryResult", expectedTestValues.addCarry.result, testValues.addCarry.result, testIteration, seed, testType); - pass &= verifyTestValue("addCarryCarry", expectedTestValues.addCarry.carry, testValues.addCarry.carry, testIteration, seed, testType); - pass &= verifyTestValue("subBorrowResult", expectedTestValues.subBorrow.result, testValues.subBorrow.result, testIteration, seed, testType); - pass &= verifyTestValue("subBorrowBorrow", expectedTestValues.subBorrow.borrow, testValues.subBorrow.borrow, testIteration, seed, testType); - - pass &= verifyTestValue("normalize", expectedTestValues.normalize, testValues.normalize, testIteration, seed, testType, normalizeToleranace); - pass &= verifyTestValue("cross", expectedTestValues.cross, testValues.cross, testIteration, seed, testType); - pass &= verifyTestValue("bitCountVec", expectedTestValues.bitCountVec, testValues.bitCountVec, testIteration, seed, testType); - pass &= verifyTestValue("clampVec", expectedTestValues.clampVec, testValues.clampVec, testIteration, seed, testType); - pass &= verifyTestValue("findMSBVec", expectedTestValues.findMSBVec, testValues.findMSBVec, testIteration, seed, testType); - pass &= verifyTestValue("findLSBVec", expectedTestValues.findLSBVec, testValues.findLSBVec, testIteration, seed, testType); - pass &= verifyTestValue("minVec", expectedTestValues.minVec, testValues.minVec, testIteration, seed, testType); - pass &= verifyTestValue("maxVec", expectedTestValues.maxVec, testValues.maxVec, testIteration, seed, testType); - pass &= verifyTestValue("rsqrtVec", expectedTestValues.rsqrtVec, testValues.rsqrtVec, testIteration, seed, testType); - pass &= verifyTestValue("bitReverseVec", expectedTestValues.bitReverseVec, testValues.bitReverseVec, testIteration, seed, testType); - pass &= verifyTestValue("fracVec", expectedTestValues.fracVec, testValues.fracVec, testIteration, seed, testType); - pass &= verifyTestValue("mixVec", expectedTestValues.mixVec, testValues.mixVec, testIteration, seed, testType); - - pass &= verifyTestValue("signVec", expectedTestValues.signVec, testValues.signVec, testIteration, seed, testType); - pass &= verifyTestValue("radiansVec", expectedTestValues.radiansVec, testValues.radiansVec, testIteration, seed, testType, radiansToleranace); - pass &= verifyTestValue("degreesVec", expectedTestValues.degreesVec, testValues.degreesVec, testIteration, seed, testType, degreesToleranace); - pass &= verifyTestValue("stepVec", expectedTestValues.stepVec, testValues.stepVec, testIteration, seed, testType); - pass &= verifyTestValue("smoothStepVec", expectedTestValues.smoothStepVec, testValues.smoothStepVec, testIteration, seed, testType, smoothstepToleranace); - pass &= verifyTestValue("faceForward", expectedTestValues.faceForward, testValues.faceForward, testIteration, seed, testType); - pass &= verifyTestValue("reflect", expectedTestValues.reflect, testValues.reflect, testIteration, seed, testType, reflectToleranace); - pass &= verifyTestValue("refract", expectedTestValues.refract, testValues.refract, testIteration, seed, testType, refractToleranace); - pass &= verifyTestValue("addCarryVecResult", expectedTestValues.addCarryVec.result, testValues.addCarryVec.result, testIteration, seed, testType); - pass &= verifyTestValue("addCarryVecCarry", expectedTestValues.addCarryVec.carry, testValues.addCarryVec.carry, testIteration, seed, testType); - pass &= verifyTestValue("subBorrowVecResult", expectedTestValues.subBorrowVec.result, testValues.subBorrowVec.result, testIteration, seed, testType); - pass &= verifyTestValue("subBorrowVecBorrow", expectedTestValues.subBorrowVec.borrow, testValues.subBorrowVec.borrow, testIteration, seed, testType); - - pass &= verifyTestValue("mul", expectedTestValues.mul, testValues.mul, testIteration, seed, testType, matrixMulToleranace); - pass &= verifyTestValue("transpose", expectedTestValues.transpose, testValues.transpose, testIteration, seed, testType, 0.0); - pass &= verifyTestValue("inverse", expectedTestValues.inverse, testValues.inverse, testIteration, seed, testType, inverseToleranace); - pass &= verifyTestValue("pseudoInverse3x4", expectedTestValues.pseudoInverse3x4, testValues.pseudoInverse3x4, testIteration, seed, testType, pseudoInverseToleranace); - return pass; - } -}; - -#endif \ No newline at end of file diff --git a/22_CppCompat/CMakeLists.txt b/22_CppCompat/CMakeLists.txt index d7a203d2d..b7e52875d 100644 --- a/22_CppCompat/CMakeLists.txt +++ b/22_CppCompat/CMakeLists.txt @@ -21,56 +21,4 @@ if(NBL_EMBED_BUILTIN_RESOURCES) ADD_CUSTOM_BUILTIN_RESOURCES(${_BR_TARGET_} RESOURCES_TO_EMBED "${_SEARCH_DIRECTORIES_}" "${RESOURCE_DIR}" "nbl::this_example::builtin" "${_OUTPUT_DIRECTORY_HEADER_}" "${_OUTPUT_DIRECTORY_SOURCE_}") LINK_BUILTIN_RESOURCES_TO_TARGET(${EXECUTABLE_NAME} ${_BR_TARGET_}) -endif() - -if(MSVC) - target_compile_options("${EXECUTABLE_NAME}" PUBLIC "/fp:strict") -else() - target_compile_options("${EXECUTABLE_NAME}" PUBLIC -ffloat-store -frounding-math -fsignaling-nans -ftrapping-math) -endif() - -set(OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/auto-gen") - -set(SM 6_8) -set(JSON [=[ -[ - { - "INPUT": "app_resources/test.comp.hlsl", - "KEY": "test", - }, - { - "INPUT": "app_resources/intrinsicsTest.comp.hlsl", - "KEY": "intrinsicsTest", - }, - { - "INPUT": "app_resources/tgmathTest.comp.hlsl", - "KEY": "tgmathTest", - } -] -]=]) -string(CONFIGURE "${JSON}" JSON) - -set(COMPILE_OPTIONS - -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} -) +endif() \ No newline at end of file diff --git a/22_CppCompat/CTgmathTester.h b/22_CppCompat/CTgmathTester.h deleted file mode 100644 index 15b8305c6..000000000 --- a/22_CppCompat/CTgmathTester.h +++ /dev/null @@ -1,361 +0,0 @@ -#ifndef _NBL_EXAMPLES_TESTS_22_CPP_COMPAT_C_TGMATH_TESTER_INCLUDED_ -#define _NBL_EXAMPLES_TESTS_22_CPP_COMPAT_C_TGMATH_TESTER_INCLUDED_ - - -#include "nbl/examples/examples.hpp" -#include "app_resources/common.hlsl" -#include "nbl/examples/Tester/ITester.h" - -using namespace nbl; - -class CTgmathTester final : public ITester -{ - using base_t = ITester; - -public: - CTgmathTester(const uint32_t testBatchCount) - : base_t(testBatchCount) {}; - -private: - TgmathIntputTestValues generateInputTestValues() override - { - std::uniform_real_distribution realDistribution(-100.0f, 100.0f); - std::uniform_real_distribution realDistributionSmall(1.0f, 4.0f); - std::uniform_int_distribution intDistribution(-100, 100); - std::uniform_int_distribution coinFlipDistribution(0, 1); - - TgmathIntputTestValues testInput; - testInput.floor = realDistribution(getRandomEngine()); - testInput.isnan = coinFlipDistribution(getRandomEngine()) ? realDistribution(getRandomEngine()) : std::numeric_limits::quiet_NaN(); - testInput.isinf = coinFlipDistribution(getRandomEngine()) ? realDistribution(getRandomEngine()) : std::numeric_limits::infinity(); - testInput.powX = realDistributionSmall(getRandomEngine()); - testInput.powY = realDistributionSmall(getRandomEngine()); - testInput.exp = realDistributionSmall(getRandomEngine()); - testInput.exp2 = realDistributionSmall(getRandomEngine()); - testInput.log = realDistribution(getRandomEngine()); - testInput.log2 = realDistribution(getRandomEngine()); - testInput.absF = realDistribution(getRandomEngine()); - testInput.absI = intDistribution(getRandomEngine()); - testInput.sqrt = realDistribution(getRandomEngine()); - testInput.sin = realDistribution(getRandomEngine()); - testInput.cos = realDistribution(getRandomEngine()); - testInput.tan = realDistribution(getRandomEngine()); - testInput.asin = realDistribution(getRandomEngine()); - testInput.atan = realDistribution(getRandomEngine()); - testInput.sinh = realDistribution(getRandomEngine()); - testInput.cosh = realDistribution(getRandomEngine()); - testInput.tanh = realDistribution(getRandomEngine()); - testInput.asinh = realDistribution(getRandomEngine()); - testInput.acosh = realDistribution(getRandomEngine()); - testInput.atanh = realDistribution(getRandomEngine()); - testInput.atan2X = realDistribution(getRandomEngine()); - testInput.atan2Y = realDistribution(getRandomEngine()); - testInput.acos = realDistribution(getRandomEngine()); - testInput.modf = realDistribution(getRandomEngine()); - testInput.round = realDistribution(getRandomEngine()); - testInput.roundEven = coinFlipDistribution(getRandomEngine()) ? realDistributionSmall(getRandomEngine()) : (static_cast(intDistribution(getRandomEngine()) / 2) + 0.5f); - testInput.trunc = realDistribution(getRandomEngine()); - testInput.ceil = realDistribution(getRandomEngine()); - testInput.fmaX = realDistribution(getRandomEngine()); - testInput.fmaY = realDistribution(getRandomEngine()); - testInput.fmaZ = realDistribution(getRandomEngine()); - testInput.ldexpArg = realDistributionSmall(getRandomEngine()); - testInput.ldexpExp = intDistribution(getRandomEngine()); - testInput.erf = realDistribution(getRandomEngine()); - testInput.erfInv = realDistribution(getRandomEngine()); - - testInput.floorVec = float32_t3(realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), realDistribution(getRandomEngine())); - testInput.isnanVec = float32_t3(realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), realDistribution(getRandomEngine())); - testInput.isinfVec = float32_t3(realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), realDistribution(getRandomEngine())); - testInput.powXVec = float32_t3(realDistributionSmall(getRandomEngine()), realDistributionSmall(getRandomEngine()), realDistributionSmall(getRandomEngine())); - testInput.powYVec = float32_t3(realDistributionSmall(getRandomEngine()), realDistributionSmall(getRandomEngine()), realDistributionSmall(getRandomEngine())); - testInput.expVec = float32_t3(realDistributionSmall(getRandomEngine()), realDistributionSmall(getRandomEngine()), realDistributionSmall(getRandomEngine())); - testInput.exp2Vec = float32_t3(realDistributionSmall(getRandomEngine()), realDistributionSmall(getRandomEngine()), realDistributionSmall(getRandomEngine())); - testInput.logVec = float32_t3(realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), realDistribution(getRandomEngine())); - testInput.log2Vec = float32_t3(realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), realDistribution(getRandomEngine())); - testInput.absFVec = float32_t3(realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), realDistribution(getRandomEngine())); - testInput.absIVec = int32_t3(intDistribution(getRandomEngine()), intDistribution(getRandomEngine()), intDistribution(getRandomEngine())); - testInput.sqrtVec = float32_t3(realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), realDistribution(getRandomEngine())); - testInput.sinVec = float32_t3(realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), realDistribution(getRandomEngine())); - testInput.cosVec = float32_t3(realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), realDistribution(getRandomEngine())); - testInput.tanVec = float32_t3(realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), realDistribution(getRandomEngine())); - testInput.asinVec = float32_t3(realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), realDistribution(getRandomEngine())); - testInput.atanVec = float32_t3(realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), realDistribution(getRandomEngine())); - testInput.sinhVec = float32_t3(realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), realDistribution(getRandomEngine())); - testInput.coshVec = float32_t3(realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), realDistribution(getRandomEngine())); - testInput.tanhVec = float32_t3(realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), realDistribution(getRandomEngine())); - testInput.asinhVec = float32_t3(realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), realDistribution(getRandomEngine())); - testInput.acoshVec = float32_t3(realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), realDistribution(getRandomEngine())); - testInput.atanhVec = float32_t3(realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), realDistribution(getRandomEngine())); - testInput.atan2XVec = float32_t3(realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), realDistribution(getRandomEngine())); - testInput.atan2YVec = float32_t3(realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), realDistribution(getRandomEngine())); - testInput.acosVec = float32_t3(realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), realDistribution(getRandomEngine())); - testInput.modfVec = float32_t3(realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), realDistribution(getRandomEngine())); - testInput.ldexpArgVec = float32_t3(realDistributionSmall(getRandomEngine()), realDistributionSmall(getRandomEngine()), realDistributionSmall(getRandomEngine())); - testInput.ldexpExpVec = float32_t3(intDistribution(getRandomEngine()), intDistribution(getRandomEngine()), intDistribution(getRandomEngine())); - testInput.erfVec = float32_t3(realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), realDistribution(getRandomEngine())); - testInput.erfInvVec = float32_t3(realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), realDistribution(getRandomEngine())); - - testInput.modfStruct = realDistribution(getRandomEngine()); - testInput.modfStructVec = float32_t3(realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), realDistribution(getRandomEngine())); - testInput.frexpStruct = realDistribution(getRandomEngine()); - testInput.frexpStructVec = float32_t3(realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), realDistribution(getRandomEngine())); - - return testInput; - } - - TgmathTestValues determineExpectedResults(const TgmathIntputTestValues& testInput) override - { - // use std library functions to determine expected test values, the output of functions from tgmath.hlsl will be verified against these values - TgmathTestValues expected; - expected.floor = std::floor(testInput.floor); - expected.isnan = std::isnan(testInput.isnan); - expected.isinf = std::isinf(testInput.isinf); - expected.pow = std::pow(testInput.powX, testInput.powY); - expected.exp = std::exp(testInput.exp); - expected.exp2 = std::exp2(testInput.exp2); - expected.log = std::log(testInput.log); - expected.log2 = std::log2(testInput.log2); - expected.absF = std::abs(testInput.absF); - expected.absI = std::abs(testInput.absI); - expected.sqrt = std::sqrt(testInput.sqrt); - expected.sin = std::sin(testInput.sin); - expected.cos = std::cos(testInput.cos); - expected.acos = std::acos(testInput.acos); - expected.tan = std::tan(testInput.tan); - expected.asin = std::asin(testInput.asin); - expected.atan = std::atan(testInput.atan); - expected.sinh = std::sinh(testInput.sinh); - expected.cosh = std::cosh(testInput.cosh); - expected.tanh = std::tanh(testInput.tanh); - expected.asinh = std::asinh(testInput.asinh); - expected.acosh = std::acosh(testInput.acosh); - expected.atanh = std::atanh(testInput.atanh); - expected.atan2 = std::atan2(testInput.atan2Y, testInput.atan2X); - expected.erf = std::erf(testInput.erf); - { - float tmp; - expected.modf = std::modf(testInput.modf, &tmp); - } - expected.round = std::round(testInput.round); - // TODO: uncomment when C++23 - //expected.roundEven = std::roundeven(testInput.roundEven); - // TODO: remove when C++23 - auto roundeven = [](const float& val) -> float - { - float tmp; - if (std::abs(std::modf(val, &tmp)) == 0.5f) - { - int32_t result = static_cast(val); - if (result % 2 != 0) - result >= 0 ? ++result : --result; - return result; - } - - return std::round(val); - }; - expected.roundEven = roundeven(testInput.roundEven); - - expected.trunc = std::trunc(testInput.trunc); - expected.ceil = std::ceil(testInput.ceil); - expected.fma = std::fma(testInput.fmaX, testInput.fmaY, testInput.fmaZ); - expected.ldexp = std::ldexp(testInput.ldexpArg, testInput.ldexpExp); - - expected.floorVec = float32_t3(std::floor(testInput.floorVec.x), std::floor(testInput.floorVec.y), std::floor(testInput.floorVec.z)); - - expected.isnanVec = float32_t3(std::isnan(testInput.isnanVec.x), std::isnan(testInput.isnanVec.y), std::isnan(testInput.isnanVec.z)); - expected.isinfVec = float32_t3(std::isinf(testInput.isinfVec.x), std::isinf(testInput.isinfVec.y), std::isinf(testInput.isinfVec.z)); - - expected.powVec.x = std::pow(testInput.powXVec.x, testInput.powYVec.x); - expected.powVec.y = std::pow(testInput.powXVec.y, testInput.powYVec.y); - expected.powVec.z = std::pow(testInput.powXVec.z, testInput.powYVec.z); - - expected.expVec = float32_t3(std::exp(testInput.expVec.x), std::exp(testInput.expVec.y), std::exp(testInput.expVec.z)); - expected.exp2Vec = float32_t3(std::exp2(testInput.exp2Vec.x), std::exp2(testInput.exp2Vec.y), std::exp2(testInput.exp2Vec.z)); - expected.logVec = float32_t3(std::log(testInput.logVec.x), std::log(testInput.logVec.y), std::log(testInput.logVec.z)); - expected.log2Vec = float32_t3(std::log2(testInput.log2Vec.x), std::log2(testInput.log2Vec.y), std::log2(testInput.log2Vec.z)); - expected.absFVec = float32_t3(std::abs(testInput.absFVec.x), std::abs(testInput.absFVec.y), std::abs(testInput.absFVec.z)); - expected.absIVec = float32_t3(std::abs(testInput.absIVec.x), std::abs(testInput.absIVec.y), std::abs(testInput.absIVec.z)); - expected.sqrtVec = float32_t3(std::sqrt(testInput.sqrtVec.x), std::sqrt(testInput.sqrtVec.y), std::sqrt(testInput.sqrtVec.z)); - expected.cosVec = float32_t3(std::cos(testInput.cosVec.x), std::cos(testInput.cosVec.y), std::cos(testInput.cosVec.z)); - expected.sinVec = float32_t3(std::sin(testInput.sinVec.x), std::sin(testInput.sinVec.y), std::sin(testInput.sinVec.z)); - expected.tanVec = float32_t3(std::tan(testInput.tanVec.x), std::tan(testInput.tanVec.y), std::tan(testInput.tanVec.z)); - expected.asinVec = float32_t3(std::asin(testInput.asinVec.x), std::asin(testInput.asinVec.y), std::asin(testInput.asinVec.z)); - expected.atanVec = float32_t3(std::atan(testInput.atanVec.x), std::atan(testInput.atanVec.y), std::atan(testInput.atanVec.z)); - expected.sinhVec = float32_t3(std::sinh(testInput.sinhVec.x), std::sinh(testInput.sinhVec.y), std::sinh(testInput.sinhVec.z)); - expected.coshVec = float32_t3(std::cosh(testInput.coshVec.x), std::cosh(testInput.coshVec.y), std::cosh(testInput.coshVec.z)); - expected.tanhVec = float32_t3(std::tanh(testInput.tanhVec.x), std::tanh(testInput.tanhVec.y), std::tanh(testInput.tanhVec.z)); - expected.asinhVec = float32_t3(std::asinh(testInput.asinhVec.x), std::asinh(testInput.asinhVec.y), std::asinh(testInput.asinhVec.z)); - expected.acoshVec = float32_t3(std::acosh(testInput.acoshVec.x), std::acosh(testInput.acoshVec.y), std::acosh(testInput.acoshVec.z)); - expected.atanhVec = float32_t3(std::atanh(testInput.atanhVec.x), std::atanh(testInput.atanhVec.y), std::atanh(testInput.atanhVec.z)); - expected.atan2Vec = float32_t3(std::atan2(testInput.atan2YVec.x, testInput.atan2XVec.x), std::atan2(testInput.atan2YVec.y, testInput.atan2XVec.y), std::atan2(testInput.atan2YVec.z, testInput.atan2XVec.z)); - expected.acosVec = float32_t3(std::acos(testInput.acosVec.x), std::acos(testInput.acosVec.y), std::acos(testInput.acosVec.z)); - expected.erfVec = float32_t3(std::erf(testInput.erfVec.x), std::erf(testInput.erfVec.y), std::erf(testInput.erfVec.z)); - { - float tmp; - expected.modfVec = float32_t3(std::modf(testInput.modfVec.x, &tmp), std::modf(testInput.modfVec.y, &tmp), std::modf(testInput.modfVec.z, &tmp)); - } - expected.roundVec = float32_t3( - std::round(testInput.roundVec.x), - std::round(testInput.roundVec.y), - std::round(testInput.roundVec.z) - ); - // TODO: uncomment when C++23 - //expected.roundEven = float32_t( - // std::roundeven(testInput.roundEvenVec.x), - // std::roundeven(testInput.roundEvenVec.y), - // std::roundeven(testInput.roundEvenVec.z) - // ); - // TODO: remove when C++23 - expected.roundEvenVec = float32_t3( - roundeven(testInput.roundEvenVec.x), - roundeven(testInput.roundEvenVec.y), - roundeven(testInput.roundEvenVec.z) - ); - - expected.truncVec = float32_t3(std::trunc(testInput.truncVec.x), std::trunc(testInput.truncVec.y), std::trunc(testInput.truncVec.z)); - expected.ceilVec = float32_t3(std::ceil(testInput.ceilVec.x), std::ceil(testInput.ceilVec.y), std::ceil(testInput.ceilVec.z)); - expected.fmaVec = float32_t3( - std::fma(testInput.fmaXVec.x, testInput.fmaYVec.x, testInput.fmaZVec.x), - std::fma(testInput.fmaXVec.y, testInput.fmaYVec.y, testInput.fmaZVec.y), - std::fma(testInput.fmaXVec.z, testInput.fmaYVec.z, testInput.fmaZVec.z) - ); - expected.ldexpVec = float32_t3( - std::ldexp(testInput.ldexpArgVec.x, testInput.ldexpExpVec.x), - std::ldexp(testInput.ldexpArgVec.y, testInput.ldexpExpVec.y), - std::ldexp(testInput.ldexpArgVec.z, testInput.ldexpExpVec.z) - ); - - { - ModfOutput expectedModfStructOutput; - expectedModfStructOutput.fractionalPart = std::modf(testInput.modfStruct, &expectedModfStructOutput.wholeNumberPart); - expected.modfStruct = expectedModfStructOutput; - - ModfOutput expectedModfStructOutputVec; - for (int i = 0; i < 3; ++i) - expectedModfStructOutputVec.fractionalPart[i] = std::modf(testInput.modfStructVec[i], &expectedModfStructOutputVec.wholeNumberPart[i]); - expected.modfStructVec = expectedModfStructOutputVec; - } - - { - FrexpOutput expectedFrexpStructOutput; - expectedFrexpStructOutput.significand = std::frexp(testInput.frexpStruct, &expectedFrexpStructOutput.exponent); - expected.frexpStruct = expectedFrexpStructOutput; - - FrexpOutput expectedFrexpStructOutputVec; - for (int i = 0; i < 3; ++i) - expectedFrexpStructOutputVec.significand[i] = std::frexp(testInput.frexpStructVec[i], &expectedFrexpStructOutputVec.exponent[i]); - expected.frexpStructVec = expectedFrexpStructOutputVec; - } - - return expected; - } - - bool verifyTestResults(const TgmathTestValues& expectedTestValues, const TgmathTestValues& testValues, const size_t testIteration, const uint32_t seed, TestType testType) override - { - // TODO: figure out input for functions: sinh, cosh so output isn't a crazy low number - // very low numbers generate comparison errors - - // made them volatile so can change numbers in debugger without recompile - volatile float powTolerance = 1.00000012; - volatile float expTolerance = 2.38419e-07; - volatile float exp2Tolerance = 1.19209290e-07; - volatile float logTolerance = 5.96046e-06; - volatile float log2Tolerance = 6.19888e-06; - volatile float sqrtTolerance = 1.1921e-07; - volatile float sinCosTolerance = 0.0303608; - volatile float acosTolerance = 1.1921e-07; - volatile float tanTolerance = 0.195737; - volatile float asinTolerance = 1.1921e-07; - volatile float atanTolerance = 1.19209290e-07; - volatile float tanhTolerance = 1.1921e-07; - volatile float asinhTolerance = 1.1920929e-07; - volatile float acoshTolerance = 1.1920929e-07; - volatile float atanhTolerance = 2.38419e-07; - volatile float atan2Tolerance = 2.3842e-07; - volatile float erfTolerance = 0.000118017; - - bool pass = true; - pass &= verifyTestValue("floor", expectedTestValues.floor, testValues.floor, testIteration, seed, testType); - pass &= verifyTestValue("isnan", expectedTestValues.isnan, testValues.isnan, testIteration, seed, testType); - pass &= verifyTestValue("isinf", expectedTestValues.isinf, testValues.isinf, testIteration, seed, testType); - pass &= verifyTestValue("pow", expectedTestValues.pow, testValues.pow, testIteration, seed, testType, powTolerance); - pass &= verifyTestValue("exp", expectedTestValues.exp, testValues.exp, testIteration, seed, testType, expTolerance); - pass &= verifyTestValue("exp2", expectedTestValues.exp2, testValues.exp2, testIteration, seed, testType, exp2Tolerance); - pass &= verifyTestValue("log", expectedTestValues.log, testValues.log, testIteration, seed, testType, logTolerance); - pass &= verifyTestValue("log2", expectedTestValues.log2, testValues.log2, testIteration, seed, testType, log2Tolerance); - pass &= verifyTestValue("absF", expectedTestValues.absF, testValues.absF, testIteration, seed, testType); - pass &= verifyTestValue("absI", expectedTestValues.absI, testValues.absI, testIteration, seed, testType); - pass &= verifyTestValue("sqrt", expectedTestValues.sqrt, testValues.sqrt, testIteration, seed, testType, sqrtTolerance); - pass &= verifyTestValue("sin", expectedTestValues.sin, testValues.sin, testIteration, seed, testType, sinCosTolerance); - pass &= verifyTestValue("cos", expectedTestValues.cos, testValues.cos, testIteration, seed, testType, sinCosTolerance); - pass &= verifyTestValue("acos", expectedTestValues.acos, testValues.acos, testIteration, seed, testType, acosTolerance); - pass &= verifyTestValue("tan", expectedTestValues.tan, testValues.tan, testIteration, seed, testType, tanTolerance); - pass &= verifyTestValue("asin", expectedTestValues.asin, testValues.asin, testIteration, seed, testType, asinTolerance); - pass &= verifyTestValue("atan", expectedTestValues.atan, testValues.atan, testIteration, seed, testType, atanTolerance); - //pass &= verifyTestValue("sinh", expectedTestValues.sinh, testValues.sinh, testIteration, seed, testType); - //pass &= verifyTestValue("cosh", expectedTestValues.cosh, testValues.cosh, testIteration, seed, testType); - pass &= verifyTestValue("tanh", expectedTestValues.tanh, testValues.tanh, testIteration, seed, testType, tanhTolerance); - pass &= verifyTestValue("asinh", expectedTestValues.asinh, testValues.asinh, testIteration, seed, testType, asinhTolerance); - pass &= verifyTestValue("acosh", expectedTestValues.acosh, testValues.acosh, testIteration, seed, testType, acoshTolerance); - pass &= verifyTestValue("atanh", expectedTestValues.atanh, testValues.atanh, testIteration, seed, testType, atanhTolerance); - pass &= verifyTestValue("atan2", expectedTestValues.atan2, testValues.atan2, testIteration, seed, testType, atan2Tolerance); - pass &= verifyTestValue("modf", expectedTestValues.modf, testValues.modf, testIteration, seed, testType); - pass &= verifyTestValue("round", expectedTestValues.round, testValues.round, testIteration, seed, testType); - pass &= verifyTestValue("roundEven", expectedTestValues.roundEven, testValues.roundEven, testIteration, seed, testType); - pass &= verifyTestValue("trunc", expectedTestValues.trunc, testValues.trunc, testIteration, seed, testType); - pass &= verifyTestValue("ceil", expectedTestValues.ceil, testValues.ceil, testIteration, seed, testType); - pass &= verifyTestValue("fma", expectedTestValues.fma, testValues.fma, testIteration, seed, testType); - pass &= verifyTestValue("ldexp", expectedTestValues.ldexp, testValues.ldexp, testIteration, seed, testType); - pass &= verifyTestValue("erf", expectedTestValues.erf, testValues.erf, testIteration, seed, testType, erfTolerance); - //pass &= verifyTestValue("erfInv", expectedTestValues.erfInv, testValues.erfInv, testIteration, seed, testType); - - pass &= verifyTestValue("floorVec", expectedTestValues.floorVec, testValues.floorVec, testIteration, seed, testType); - pass &= verifyTestValue("isnanVec", expectedTestValues.isnanVec, testValues.isnanVec, testIteration, seed, testType); - pass &= verifyTestValue("isinfVec", expectedTestValues.isinfVec, testValues.isinfVec, testIteration, seed, testType); - pass &= verifyTestValue("powVec", expectedTestValues.powVec, testValues.powVec, testIteration, seed, testType, powTolerance); - pass &= verifyTestValue("expVec", expectedTestValues.expVec, testValues.expVec, testIteration, seed, testType, expTolerance); - pass &= verifyTestValue("exp2Vec", expectedTestValues.exp2Vec, testValues.exp2Vec, testIteration, seed, testType, exp2Tolerance); - pass &= verifyTestValue("logVec", expectedTestValues.logVec, testValues.logVec, testIteration, seed, testType, logTolerance); - pass &= verifyTestValue("log2Vec", expectedTestValues.log2Vec, testValues.log2Vec, testIteration, seed, testType, log2Tolerance); - pass &= verifyTestValue("absFVec", expectedTestValues.absFVec, testValues.absFVec, testIteration, seed, testType); - pass &= verifyTestValue("absIVec", expectedTestValues.absIVec, testValues.absIVec, testIteration, seed, testType); - pass &= verifyTestValue("sqrtVec", expectedTestValues.sqrtVec, testValues.sqrtVec, testIteration, seed, testType, sqrtTolerance); - pass &= verifyTestValue("sinVec", expectedTestValues.sinVec, testValues.sinVec, testIteration, seed, testType, sinCosTolerance); - pass &= verifyTestValue("cosVec", expectedTestValues.cosVec, testValues.cosVec, testIteration, seed, testType, sinCosTolerance); - pass &= verifyTestValue("acosVec", expectedTestValues.acosVec, testValues.acosVec, testIteration, seed, testType); - pass &= verifyTestValue("modfVec", expectedTestValues.modfVec, testValues.modfVec, testIteration, seed, testType); - pass &= verifyTestValue("roundVec", expectedTestValues.roundVec, testValues.roundVec, testIteration, seed, testType); - pass &= verifyTestValue("roundEvenVec", expectedTestValues.roundEvenVec, testValues.roundEvenVec, testIteration, seed, testType); - pass &= verifyTestValue("truncVec", expectedTestValues.truncVec, testValues.truncVec, testIteration, seed, testType); - pass &= verifyTestValue("ceilVec", expectedTestValues.ceilVec, testValues.ceilVec, testIteration, seed, testType); - pass &= verifyTestValue("fmaVec", expectedTestValues.fmaVec, testValues.fmaVec, testIteration, seed, testType); - pass &= verifyTestValue("ldexp", expectedTestValues.ldexpVec, testValues.ldexpVec, testIteration, seed, testType); - pass &= verifyTestValue("tanVec", expectedTestValues.tanVec, testValues.tanVec, testIteration, seed, testType, tanTolerance); - pass &= verifyTestValue("asinVec", expectedTestValues.asinVec, testValues.asinVec, testIteration, seed, testType); - pass &= verifyTestValue("atanVec", expectedTestValues.atanVec, testValues.atanVec, testIteration, seed, testType, atanTolerance); - //pass &= verifyTestValue("sinhVec", expectedTestValues.sinhVec, testValues.sinhVec, testIteration, seed, testType); - //pass &= verifyTestValue("coshVec", expectedTestValues.coshVec, testValues.coshVec, testIteration, seed, testType); - pass &= verifyTestValue("tanhVec", expectedTestValues.tanhVec, testValues.tanhVec, testIteration, seed, testType, tanhTolerance); - pass &= verifyTestValue("asinhVec", expectedTestValues.asinhVec, testValues.asinhVec, testIteration, seed, testType, asinhTolerance); - pass &= verifyTestValue("acoshVec", expectedTestValues.acoshVec, testValues.acoshVec, testIteration, seed, testType); - pass &= verifyTestValue("atanhVec", expectedTestValues.atanhVec, testValues.atanhVec, testIteration, seed, testType, atanhTolerance); - pass &= verifyTestValue("atan2Vec", expectedTestValues.atan2Vec, testValues.atan2Vec, testIteration, seed, testType); - pass &= verifyTestValue("erfVec", expectedTestValues.erfVec, testValues.erfVec, testIteration, seed, testType, erfTolerance); - //pass &= verifyTestValue("erfInvVec", expectedTestValues.erfInvVec, testValues.erfInvVec, testIteration, seed, testType); - - // verify output of struct producing functions - pass &= verifyTestValue("modfStruct", expectedTestValues.modfStruct.fractionalPart, testValues.modfStruct.fractionalPart, testIteration, seed, testType); - pass &= verifyTestValue("modfStruct", expectedTestValues.modfStruct.wholeNumberPart, testValues.modfStruct.wholeNumberPart, testIteration, seed, testType); - pass &= verifyTestValue("modfStructVec", expectedTestValues.modfStructVec.fractionalPart, testValues.modfStructVec.fractionalPart, testIteration, seed, testType); - pass &= verifyTestValue("modfStructVec", expectedTestValues.modfStructVec.wholeNumberPart, testValues.modfStructVec.wholeNumberPart, testIteration, seed, testType); - - pass &= verifyTestValue("frexpStruct", expectedTestValues.frexpStruct.significand, testValues.frexpStruct.significand, testIteration, seed, testType); - pass &= verifyTestValue("frexpStruct", expectedTestValues.frexpStruct.exponent, testValues.frexpStruct.exponent, testIteration, seed, testType); - pass &= verifyTestValue("frexpStructVec", expectedTestValues.frexpStructVec.significand, testValues.frexpStructVec.significand, testIteration, seed, testType); - pass &= verifyTestValue("frexpStructVec", expectedTestValues.frexpStructVec.exponent, testValues.frexpStructVec.exponent, testIteration, seed, testType); - return pass; - } -}; - -#endif \ No newline at end of file diff --git a/22_CppCompat/app_resources/common.hlsl b/22_CppCompat/app_resources/common.hlsl index b09f84a6f..ca43b5afd 100644 --- a/22_CppCompat/app_resources/common.hlsl +++ b/22_CppCompat/app_resources/common.hlsl @@ -1,486 +1,35 @@ -//// Copyright (C) 2023-2024 - 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_TESTS_22_CPP_COMPAT_COMMON_INCLUDED_ -#define _NBL_EXAMPLES_TESTS_22_CPP_COMPAT_COMMON_INCLUDED_ - -// because DXC doesn't properly support `_Static_assert` -// TODO: add a message, and move to macros.h or cpp_compat -#define STATIC_ASSERT(...) { nbl::hlsl::conditional<__VA_ARGS__, int, void>::type a = 0; } - -#include - -#include -#include - -#include -#include - -#include -#include -#include -#include - -#include - -#include -#include - -#include - - -#include -#include -#include - -#include -#include -#include - -// tgmath.hlsl and intrinsics.hlsl tests - -using namespace nbl::hlsl; -struct TgmathIntputTestValues -{ - float floor; - float isnan; - float isinf; - float powX; - float powY; - float exp; - float exp2; - float log; - float log2; - float absF; - int absI; - float sqrt; - float sin; - float cos; - float acos; - float modf; - float round; - float roundEven; - float trunc; - float ceil; - float fmaX; - float fmaY; - float fmaZ; - float ldexpArg; - int ldexpExp; - float modfStruct; - float frexpStruct; - float tan; - float asin; - float atan; - float sinh; - float cosh; - float tanh; - float asinh; - float acosh; - float atanh; - float atan2X; - float atan2Y; - float erf; - float erfInv; - - float32_t3 floorVec; - float32_t3 isnanVec; - float32_t3 isinfVec; - float32_t3 powXVec; - float32_t3 powYVec; - float32_t3 expVec; - float32_t3 exp2Vec; - float32_t3 logVec; - float32_t3 log2Vec; - float32_t3 absFVec; - int32_t3 absIVec; - float32_t3 sqrtVec; - float32_t3 sinVec; - float32_t3 cosVec; - float32_t3 acosVec; - float32_t3 modfVec; - float32_t3 roundVec; - float32_t3 roundEvenVec; - float32_t3 truncVec; - float32_t3 ceilVec; - float32_t3 fmaXVec; - float32_t3 fmaYVec; - float32_t3 fmaZVec; - float32_t3 ldexpArgVec; - int32_t3 ldexpExpVec; - float32_t3 modfStructVec; - float32_t3 frexpStructVec; - float32_t3 tanVec; - float32_t3 asinVec; - float32_t3 atanVec; - float32_t3 sinhVec; - float32_t3 coshVec; - float32_t3 tanhVec; - float32_t3 asinhVec; - float32_t3 acoshVec; - float32_t3 atanhVec; - float32_t3 atan2XVec; - float32_t3 atan2YVec; - float32_t3 erfVec; - float32_t3 erfInvVec; -}; - -struct TgmathTestValues -{ - float floor; - int isnan; - int isinf; - float pow; - float exp; - float exp2; - float log; - float log2; - float absF; - int absI; - float sqrt; - float sin; - float cos; - float acos; - float modf; - float round; - float roundEven; - float trunc; - float ceil; - float fma; - float ldexp; - float tan; - float asin; - float atan; - float sinh; - float cosh; - float tanh; - float asinh; - float acosh; - float atanh; - float atan2; - float erf; - float erfInv; - - float32_t3 floorVec; - - // we can't fix this because using namespace nbl::hlsl would cause ambiguous math functions below - // and we can't add a nbl::hlsl alias for the builtin hLSL vector type because of https://github.com/microsoft/DirectXShaderCompiler/issues/7035 -#ifndef __HLSL_VERSION - nbl::hlsl::vector isnanVec; - nbl::hlsl::vector isinfVec; -#else - vector isnanVec; - vector isinfVec; -#endif - - float32_t3 powVec; - float32_t3 expVec; - float32_t3 exp2Vec; - float32_t3 logVec; - float32_t3 log2Vec; - float32_t3 absFVec; - int32_t3 absIVec; - float32_t3 sqrtVec; - float32_t3 cosVec; - float32_t3 sinVec; - float32_t3 acosVec; - float32_t3 modfVec; - float32_t3 roundVec; - float32_t3 roundEvenVec; - float32_t3 truncVec; - float32_t3 ceilVec; - float32_t3 fmaVec; - float32_t3 ldexpVec; - float32_t3 tanVec; - float32_t3 asinVec; - float32_t3 atanVec; - float32_t3 sinhVec; - float32_t3 coshVec; - float32_t3 tanhVec; - float32_t3 asinhVec; - float32_t3 acoshVec; - float32_t3 atanhVec; - float32_t3 atan2Vec; - float32_t3 erfVec; - float32_t3 erfInvVec; - - ModfOutput modfStruct; - ModfOutput modfStructVec; - FrexpOutput frexpStruct; - FrexpOutput frexpStructVec; -}; - -struct IntrinsicsIntputTestValues -{ - int bitCount; - float32_t3 crossLhs; - float32_t3 crossRhs; - float clampVal; - float clampMin; - float clampMax; - float32_t3 length; - float32_t3 normalize; - float32_t3 dotLhs; - float32_t3 dotRhs; - float32_t3x3 determinant; - uint32_t findMSB; - uint32_t findLSB; - float32_t3x3 inverse; - float32_t3x3 transpose; - float32_t3x3 mulLhs; - float32_t3x3 mulRhs; - float32_t4x4 pseudoMat3x4; - float minA; - float minB; - float maxA; - float maxB; - float rsqrt; - uint32_t bitReverse; - float frac; - float mixX; - float mixY; - float mixA; - float sign; - float radians; - float degrees; - float stepEdge; - float stepX; - float smoothStepEdge0; - float smoothStepEdge1; - float smoothStepX; - uint32_t addCarryA; - uint32_t addCarryB; - uint32_t subBorrowA; - uint32_t subBorrowB; - - int32_t3 bitCountVec; - float32_t3 clampValVec; - float32_t3 clampMinVec; - float32_t3 clampMaxVec; - uint32_t3 findMSBVec; - uint32_t3 findLSBVec; - float32_t3 minAVec; - float32_t3 minBVec; - float32_t3 maxAVec; - float32_t3 maxBVec; - float32_t3 rsqrtVec; - uint32_t3 bitReverseVec; - float32_t3 fracVec; - float32_t3 mixXVec; - float32_t3 mixYVec; - float32_t3 mixAVec; - float32_t3 signVec; - float32_t3 radiansVec; - float32_t3 degreesVec; - float32_t3 stepEdgeVec; - float32_t3 stepXVec; - float32_t3 smoothStepEdge0Vec; - float32_t3 smoothStepEdge1Vec; - float32_t3 smoothStepXVec; - float32_t3 faceForwardN; - float32_t3 faceForwardI; - float32_t3 faceForwardNref; - float32_t3 reflectI; - float32_t3 reflectN; - float32_t3 refractI; - float32_t3 refractN; - float refractEta; - uint32_t3 addCarryAVec; - uint32_t3 addCarryBVec; - uint32_t3 subBorrowAVec; - uint32_t3 subBorrowBVec; -}; - -struct IntrinsicsTestValues -{ - int bitCount; - float clamp; - float length; - float dot; - float determinant; - int findMSB; - int findLSB; - float min; - float max; - float rsqrt; - float frac; - uint32_t bitReverse; - float mix; - float sign; - float radians; - float degrees; - float step; - float smoothStep; - - float32_t3 normalize; - float32_t3 cross; - int32_t3 bitCountVec; - float32_t3 clampVec; - uint32_t3 findMSBVec; - uint32_t3 findLSBVec; - float32_t3 minVec; - float32_t3 maxVec; - float32_t3 rsqrtVec; - uint32_t3 bitReverseVec; - float32_t3 fracVec; - float32_t3 mixVec; - float32_t3 signVec; - float32_t3 radiansVec; - float32_t3 degreesVec; - float32_t3 stepVec; - float32_t3 smoothStepVec; - float32_t3 faceForward; - float32_t3 reflect; - float32_t3 refract; - - float32_t3x3 mul; - float32_t3x3 transpose; - float32_t3x3 inverse; - float32_t3 pseudoInverse3x4; - - spirv::AddCarryOutput addCarry; - spirv::SubBorrowOutput subBorrow; - spirv::AddCarryOutput addCarryVec; - spirv::SubBorrowOutput subBorrowVec; -}; - -struct IntrinsicsTestExecutor -{ - void operator()(NBL_CONST_REF_ARG(IntrinsicsIntputTestValues) input, NBL_REF_ARG(IntrinsicsTestValues) output) - { - output.bitCount = nbl::hlsl::bitCount(input.bitCount); - output.cross = nbl::hlsl::cross(input.crossLhs, input.crossRhs); - output.clamp = nbl::hlsl::clamp(input.clampVal, input.clampMin, input.clampMax); - output.length = nbl::hlsl::length(input.length); - output.normalize = nbl::hlsl::normalize(input.normalize); - output.dot = nbl::hlsl::dot(input.dotLhs, input.dotRhs); - output.determinant = nbl::hlsl::determinant(input.determinant); - output.findMSB = nbl::hlsl::findMSB(input.findMSB); - output.findLSB = nbl::hlsl::findLSB(input.findLSB); - output.inverse = nbl::hlsl::inverse(input.inverse); - output.transpose = nbl::hlsl::transpose(input.transpose); - output.mul = nbl::hlsl::mul(input.mulLhs, input.mulRhs); - float32_t3x4 mat3x4 = nbl::hlsl::math::linalg::truncate<3,4,4,4,float>(input.pseudoMat3x4); - float32_t3x4 invMat3x4 = nbl::hlsl::math::linalg::pseudoInverse3x4(mat3x4); - output.pseudoInverse3x4 = nbl::hlsl::mul(invMat3x4, float32_t4(input.length, 1)); - // TODO: fix min and max - output.min = nbl::hlsl::min(input.minA, input.minB); - output.max = nbl::hlsl::max(input.maxA, input.maxB); - output.rsqrt = nbl::hlsl::rsqrt(input.rsqrt); - output.bitReverse = nbl::hlsl::bitReverse(input.bitReverse); - output.frac = nbl::hlsl::fract(input.frac); - output.mix = nbl::hlsl::mix(input.mixX, input.mixY, input.mixA); - output.sign = nbl::hlsl::sign(input.sign); - output.radians = nbl::hlsl::radians(input.radians); - output.degrees = nbl::hlsl::degrees(input.degrees); - output.step = nbl::hlsl::step(input.stepEdge, input.stepX); - output.smoothStep = nbl::hlsl::smoothStep(input.smoothStepEdge0, input.smoothStepEdge1, input.smoothStepX); - - output.bitCountVec = nbl::hlsl::bitCount(input.bitCountVec); - output.clampVec = nbl::hlsl::clamp(input.clampValVec, input.clampMinVec, input.clampMaxVec); - output.findMSBVec = nbl::hlsl::findMSB(input.findMSBVec); - output.findLSBVec = nbl::hlsl::findLSB(input.findLSBVec); - // TODO: fix min and max - output.minVec = nbl::hlsl::min(input.minAVec, input.minBVec); - output.maxVec = nbl::hlsl::max(input.maxAVec, input.maxBVec); - output.rsqrtVec = nbl::hlsl::rsqrt(input.rsqrtVec); - output.bitReverseVec = nbl::hlsl::bitReverse(input.bitReverseVec); - output.fracVec = nbl::hlsl::fract(input.fracVec); - output.mixVec = nbl::hlsl::mix(input.mixXVec, input.mixYVec, input.mixAVec); - - output.signVec = nbl::hlsl::sign(input.signVec); - output.radiansVec = nbl::hlsl::radians(input.radiansVec); - output.degreesVec = nbl::hlsl::degrees(input.degreesVec); - output.stepVec = nbl::hlsl::step(input.stepEdgeVec, input.stepXVec); - output.smoothStepVec = nbl::hlsl::smoothStep(input.smoothStepEdge0Vec, input.smoothStepEdge1Vec, input.smoothStepXVec); - output.faceForward = nbl::hlsl::faceForward(input.faceForwardN, input.faceForwardI, input.faceForwardNref); - output.reflect = nbl::hlsl::reflect(input.reflectI, input.reflectN); - output.refract = nbl::hlsl::refract(input.refractI, input.refractN, input.refractEta); - output.addCarry = nbl::hlsl::addCarry(input.addCarryA, input.addCarryB); - output.subBorrow = nbl::hlsl::subBorrow(input.subBorrowA, input.subBorrowB); - output.addCarryVec = nbl::hlsl::addCarry(input.addCarryAVec, input.addCarryBVec); - output.subBorrowVec = nbl::hlsl::subBorrow(input.subBorrowAVec, input.subBorrowBVec); - } -}; - -struct TgmathTestExecutor -{ - void operator()(NBL_CONST_REF_ARG(TgmathIntputTestValues) input, NBL_REF_ARG(TgmathTestValues) output) - { - output.floor = nbl::hlsl::floor(input.floor); - output.isnan = nbl::hlsl::isnan(input.isnan); - output.isinf = nbl::hlsl::isinf(input.isinf); - output.pow = nbl::hlsl::pow(input.powX, input.powY); - output.exp = nbl::hlsl::exp(input.exp); - output.exp2 = nbl::hlsl::exp2(input.exp2); - output.log = nbl::hlsl::log(input.log); - output.log2 = nbl::hlsl::log2(input.log2); - output.absF = nbl::hlsl::abs(input.absF); - output.absI = nbl::hlsl::abs(input.absI); - output.sqrt = nbl::hlsl::sqrt(input.sqrt); - output.sin = nbl::hlsl::sin(input.sin); - output.cos = nbl::hlsl::cos(input.cos); - output.tan = nbl::hlsl::tan(input.tan); - output.asin = nbl::hlsl::asin(input.asin); - output.atan = nbl::hlsl::atan(input.atan); - output.sinh = nbl::hlsl::sinh(input.sinh); - output.cosh = nbl::hlsl::cosh(input.cosh); - output.tanh = nbl::hlsl::tanh(input.tanh); - output.asinh = nbl::hlsl::asinh(input.asinh); - output.acosh = nbl::hlsl::acosh(input.acosh); - output.atanh = nbl::hlsl::atanh(input.atanh); - output.atan2 = nbl::hlsl::atan2(input.atan2Y, input.atan2X); - output.erf = nbl::hlsl::erf(input.erf); - output.erfInv = nbl::hlsl::erfInv(input.erfInv); - output.acos = nbl::hlsl::acos(input.acos); - output.modf = nbl::hlsl::modf(input.modf); - output.round = nbl::hlsl::round(input.round); - output.roundEven = nbl::hlsl::roundEven(input.roundEven); - output.trunc = nbl::hlsl::trunc(input.trunc); - output.ceil = nbl::hlsl::ceil(input.ceil); - output.fma = nbl::hlsl::fma(input.fmaX, input.fmaY, input.fmaZ); - output.ldexp = nbl::hlsl::ldexp(input.ldexpArg, input.ldexpExp); - - output.floorVec = nbl::hlsl::floor(input.floorVec); - output.isnanVec = nbl::hlsl::isnan(input.isnanVec); - output.isinfVec = nbl::hlsl::isinf(input.isinfVec); - output.powVec = nbl::hlsl::pow(input.powXVec, input.powYVec); - output.expVec = nbl::hlsl::exp(input.expVec); - output.exp2Vec = nbl::hlsl::exp2(input.exp2Vec); - output.logVec = nbl::hlsl::log(input.logVec); - output.log2Vec = nbl::hlsl::log2(input.log2Vec); - output.absFVec = nbl::hlsl::abs(input.absFVec); - output.absIVec = nbl::hlsl::abs(input.absIVec); - output.sqrtVec = nbl::hlsl::sqrt(input.sqrtVec); - output.sinVec = nbl::hlsl::sin(input.sinVec); - output.cosVec = nbl::hlsl::cos(input.cosVec); - output.tanVec = nbl::hlsl::tan(input.tanVec); - output.asinVec = nbl::hlsl::asin(input.asinVec); - output.atanVec = nbl::hlsl::atan(input.atanVec); - output.sinhVec = nbl::hlsl::sinh(input.sinhVec); - output.coshVec = nbl::hlsl::cosh(input.coshVec); - output.tanhVec = nbl::hlsl::tanh(input.tanhVec); - output.asinhVec = nbl::hlsl::asinh(input.asinhVec); - output.acoshVec = nbl::hlsl::acosh(input.acoshVec); - output.atanhVec = nbl::hlsl::atanh(input.atanhVec); - output.atan2Vec = nbl::hlsl::atan2(input.atan2YVec, input.atan2XVec); - output.acosVec = nbl::hlsl::acos(input.acosVec); - output.modfVec = nbl::hlsl::modf(input.modfVec); - output.roundVec = nbl::hlsl::round(input.roundVec); - output.roundEvenVec = nbl::hlsl::roundEven(input.roundEvenVec); - output.truncVec = nbl::hlsl::trunc(input.truncVec); - output.ceilVec = nbl::hlsl::ceil(input.ceilVec); - output.fmaVec = nbl::hlsl::fma(input.fmaXVec, input.fmaYVec, input.fmaZVec); - output.ldexpVec = nbl::hlsl::ldexp(input.ldexpArgVec, input.ldexpExpVec); - output.erfVec = nbl::hlsl::erf(input.erfVec); - output.erfInvVec = nbl::hlsl::erfInv(input.erfInvVec); - - output.modfStruct = nbl::hlsl::modfStruct(input.modfStruct); - output.modfStructVec = nbl::hlsl::modfStruct(input.modfStructVec); - output.frexpStruct = nbl::hlsl::frexpStruct(input.frexpStruct); - output.frexpStructVec = nbl::hlsl::frexpStruct(input.frexpStructVec); - } -}; - -#endif +//// Copyright (C) 2023-2024 - 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 + +// because DXC doesn't properly support `_Static_assert` +#define STATIC_ASSERT(...) { nbl::hlsl::conditional<__VA_ARGS__, int, void>::type a = 0; } + +#include + +#include +#include + +#include +#include + +#include +#include +#include +#include + +#include + +#include +#include + +#include + +#define DISABLE_TGMATH_TESTS +#ifndef DISABLE_TGMATH_TESTS +#include +#endif + +#include +#include +#include diff --git a/22_CppCompat/app_resources/intrinsicsTest.comp.hlsl b/22_CppCompat/app_resources/intrinsicsTest.comp.hlsl deleted file mode 100644 index 23579cd09..000000000 --- a/22_CppCompat/app_resources/intrinsicsTest.comp.hlsl +++ /dev/null @@ -1,19 +0,0 @@ -//// Copyright (C) 2023-2024 - 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 shader_stage(compute) - -#include "common.hlsl" -#include - -[[vk::binding(0, 0)]] RWStructuredBuffer inputTestValues; -[[vk::binding(1, 0)]] RWStructuredBuffer outputTestValues; - -[numthreads(256, 1, 1)] -[shader("compute")] -void main() -{ - const uint invID = nbl::hlsl::glsl::gl_GlobalInvocationID().x; - IntrinsicsTestExecutor executor; - executor(inputTestValues[invID], outputTestValues[invID]); -} \ No newline at end of file diff --git a/22_CppCompat/app_resources/test.comp.hlsl b/22_CppCompat/app_resources/test.comp.hlsl index 9a817e021..0960c5c07 100644 --- a/22_CppCompat/app_resources/test.comp.hlsl +++ b/22_CppCompat/app_resources/test.comp.hlsl @@ -1,10 +1,20 @@ //// Copyright (C) 2023-2024 - 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 shader_stage(compute) - #include "app_resources/common.hlsl" +#define SHADER_CRASHING_ASSERT(expr) \ +{ \ + bool con = (expr); \ + do { \ + [branch] if (!con) \ + vk::RawBufferStore(0xdeadbeefBADC0FFbull,0x45u,4u); \ + } while(!con); \ +} + +template +const static bool is_same_v = nbl::hlsl::is_same_v; + struct PushConstants { @@ -87,7 +97,6 @@ struct device_capabilities2 }; [numthreads(8, 8, 1)] -[shader("compute")] void main(uint3 invocationID : SV_DispatchThreadID) { fill(invocationID, 1); @@ -146,10 +155,10 @@ void main(uint3 invocationID : SV_DispatchThreadID) // uint32_t rotrResult0 = nbl::hlsl::mpl::rotr::value; // uint32_t rotrResult1 = nbl::hlsl::mpl::rotr::value; - // assert(rotlResult0 == mplRotlResult0); - // assert(rotlResult1 == mplRotlResult1); - // assert(rotrResult0 == mplRotrResult0); - // assert(rotrResult1 == mplRotrResult1); + // SHADER_CRASHING_ASSERT(rotlResult0 == mplRotlResult0); + // SHADER_CRASHING_ASSERT(rotlResult1 == mplRotlResult1); + // SHADER_CRASHING_ASSERT(rotrResult0 == mplRotrResult0); + // SHADER_CRASHING_ASSERT(rotrResult1 == mplRotrResult1); // TODO: more tests and compare with cpp version as well fill(invocationID, 5); @@ -157,36 +166,36 @@ void main(uint3 invocationID : SV_DispatchThreadID) { static const uint16_t TEST_VALUE_0 = 5; static const uint32_t TEST_VALUE_1 = 0x80000000u; - static const uint32_t TEST_VALUE_2 = 0x8000000000000000u; // TODO: Przmek is this intended? it warns because its too big from uint32_t + static const uint32_t TEST_VALUE_2 = 0x8000000000000000u; static const uint32_t TEST_VALUE_3 = 0x00000001u; - static const uint32_t TEST_VALUE_4 = 0x0000000000000001u; // TODO: Przmek is this intended? it warns because its too big from uint32_t + static const uint32_t TEST_VALUE_4 = 0x0000000000000001u; fill(invocationID, 5.01); uint16_t compileTimeCountLZero = nbl::hlsl::mpl::countl_zero::value; uint16_t runTimeCountLZero = nbl::hlsl::countl_zero(TEST_VALUE_0); fill(invocationID, float4(5.1, compileTimeCountLZero, runTimeCountLZero, 0)); - assert(compileTimeCountLZero == runTimeCountLZero); + SHADER_CRASHING_ASSERT(compileTimeCountLZero == runTimeCountLZero); compileTimeCountLZero = nbl::hlsl::mpl::countl_zero::value; runTimeCountLZero = nbl::hlsl::countl_zero(TEST_VALUE_1); fill(invocationID, float4(5.2, compileTimeCountLZero, runTimeCountLZero, 0)); - assert(compileTimeCountLZero == runTimeCountLZero); + SHADER_CRASHING_ASSERT(compileTimeCountLZero == runTimeCountLZero); compileTimeCountLZero = nbl::hlsl::mpl::countl_zero::value; runTimeCountLZero = nbl::hlsl::countl_zero(TEST_VALUE_2); fill(invocationID, float4(5.3, compileTimeCountLZero, runTimeCountLZero, 0)); - assert(compileTimeCountLZero == runTimeCountLZero); + SHADER_CRASHING_ASSERT(compileTimeCountLZero == runTimeCountLZero); compileTimeCountLZero = nbl::hlsl::mpl::countl_zero::value; runTimeCountLZero = nbl::hlsl::countl_zero(TEST_VALUE_3); fill(invocationID, float4(5.4, compileTimeCountLZero, runTimeCountLZero, 0)); - assert(compileTimeCountLZero == runTimeCountLZero); + SHADER_CRASHING_ASSERT(compileTimeCountLZero == runTimeCountLZero); compileTimeCountLZero = nbl::hlsl::mpl::countl_zero::value; runTimeCountLZero = nbl::hlsl::countl_zero(TEST_VALUE_4); fill(invocationID, float4(5.5, compileTimeCountLZero, runTimeCountLZero, 0)); - assert(compileTimeCountLZero == runTimeCountLZero); + SHADER_CRASHING_ASSERT(compileTimeCountLZero == runTimeCountLZero); } { @@ -202,7 +211,7 @@ void main(uint3 invocationID : SV_DispatchThreadID) { float4 v; fill(invocationID, float4(alignof(v.x), alignof(v), 0, 0)); - assert(alignof(v.x) == alignof(v)); + SHADER_CRASHING_ASSERT(alignof(v.x) == alignof(v)); } { diff --git a/22_CppCompat/app_resources/tgmathTest.comp.hlsl b/22_CppCompat/app_resources/tgmathTest.comp.hlsl deleted file mode 100644 index 4aeecb91d..000000000 --- a/22_CppCompat/app_resources/tgmathTest.comp.hlsl +++ /dev/null @@ -1,19 +0,0 @@ -//// Copyright (C) 2023-2024 - 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 shader_stage(compute) - -#include "common.hlsl" -#include - -[[vk::binding(0, 0)]] RWStructuredBuffer inputTestValues; -[[vk::binding(1, 0)]] RWStructuredBuffer outputTestValues; - -[numthreads(256, 1, 1)] -[shader("compute")] -void main() -{ - const uint invID = nbl::hlsl::glsl::gl_GlobalInvocationID().x; - TgmathTestExecutor executor; - executor(inputTestValues[invID], outputTestValues[invID]); -} \ No newline at end of file diff --git a/22_CppCompat/main.cpp b/22_CppCompat/main.cpp index f55e9506f..0959c9bd3 100644 --- a/22_CppCompat/main.cpp +++ b/22_CppCompat/main.cpp @@ -1,26 +1,24 @@ // Copyright (C) 2018-2024 - 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 "nbl/this_example/builtin/build/spirv/keys.hpp" - -#include "app_resources/common.hlsl" - -#include "CTgmathTester.h" -#include "CIntrinsicsTester.h" - +#include #include #include #include +#include "nbl/application_templates/MonoDeviceApplication.hpp" +#include "nbl/application_templates/MonoAssetManagerAndBuiltinResourceApplication.hpp" + +#include "app_resources/common.hlsl" + -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; +using namespace nbl::application_templates; + //using namespace glm; @@ -43,10 +41,10 @@ struct T float32_t4 h; }; -class CompatibilityTest final : public application_templates::MonoDeviceApplication, public BuiltinResourcesApplication +class CompatibilityTest final : public MonoDeviceApplication, public MonoAssetManagerAndBuiltinResourceApplication { - using device_base_t = application_templates::MonoDeviceApplication; - using asset_base_t = BuiltinResourcesApplication; + using device_base_t = MonoDeviceApplication; + using asset_base_t = MonoAssetManagerAndBuiltinResourceApplication; public: CompatibilityTest(const path& _localInputCWD, const path& _localOutputCWD, const path& _sharedInputCWD, const path& _sharedOutputCWD) : IApplicationFramework(_localInputCWD, _localOutputCWD, _sharedInputCWD, _sharedOutputCWD) {} @@ -58,60 +56,30 @@ class CompatibilityTest final : public application_templates::MonoDeviceApplicat return false; if (!asset_base_t::onAppInitialized(std::move(system))) return false; - - bool pass = true; - { - CTgmathTester::PipelineSetupData pplnSetupData; - pplnSetupData.device = m_device; - pplnSetupData.api = m_api; - pplnSetupData.assetMgr = m_assetMgr; - pplnSetupData.logger = m_logger; - pplnSetupData.physicalDevice = m_physicalDevice; - pplnSetupData.computeFamilyIndex = getComputeQueue()->getFamilyIndex(); - pplnSetupData.shaderKey = nbl::this_example::builtin::build::get_spirv_key<"tgmathTest">(m_device.get()); - - CTgmathTester tgmathTester(8); - tgmathTester.setupPipeline(pplnSetupData); - pass &= tgmathTester.performTestsAndVerifyResults("TgmathTestLog.txt"); - } - { - CIntrinsicsTester::PipelineSetupData pplnSetupData; - pplnSetupData.device = m_device; - pplnSetupData.api = m_api; - pplnSetupData.assetMgr = m_assetMgr; - pplnSetupData.logger = m_logger; - pplnSetupData.physicalDevice = m_physicalDevice; - pplnSetupData.computeFamilyIndex = getComputeQueue()->getFamilyIndex(); - pplnSetupData.shaderKey = nbl::this_example::builtin::build::get_spirv_key<"intrinsicsTest">(m_device.get()); - - CIntrinsicsTester intrinsicsTester(8); - intrinsicsTester.setupPipeline(pplnSetupData); - pass &= intrinsicsTester.performTestsAndVerifyResults("IntrinsicsTestLog.txt"); - } - if (!pass) - return false; - + m_queue = m_device->getQueue(0, 0); m_commandPool = m_device->createCommandPool(m_queue->getFamilyIndex(), IGPUCommandPool::CREATE_FLAGS::RESET_COMMAND_BUFFER_BIT); m_commandPool->createCommandBuffers(IGPUCommandPool::BUFFER_LEVEL::PRIMARY, { &m_cmdbuf,1 }, smart_refctd_ptr(m_logger)); + - smart_refctd_ptr shader; + smart_refctd_ptr shader; { IAssetLoader::SAssetLoadParams lp = {}; lp.logger = m_logger.get(); - lp.workingDirectory = "app_resources"; // virtual root - auto key = nbl::this_example::builtin::build::get_spirv_key<"test">(m_device.get()); - auto assetBundle = m_assetMgr->getAsset(key.data(), lp); + lp.workingDirectory = ""; // virtual root + auto assetBundle = m_assetMgr->getAsset("app_resources/test.comp.hlsl", lp); const auto assets = assetBundle.getContents(); if (assets.empty()) return logFail("Could not load shader!"); - auto source = IAsset::castDown(assets[0]); + // lets go straight from ICPUSpecializedShader to IGPUSpecializedShader + auto source = IAsset::castDown(assets[0]); // The down-cast should not fail! assert(source); + assert(source->getStage() == IShader::E_SHADER_STAGE::ESS_COMPUTE); // this time we skip the use of the asset converter since the ICPUShader->IGPUShader path is quick and simple - shader = m_device->compileShader({ source.get() }); + shader = m_device->createShader(source.get()); if (!shader) return logFail("Creation of a GPU Shader to from CPU Shader source failed!"); } @@ -139,11 +107,11 @@ class CompatibilityTest final : public application_templates::MonoDeviceApplicat IGPUComputePipeline::SCreationParams params = {}; params.layout = layout.get(); params.shader.shader = shader.get(); - params.shader.entryPoint = "main"; if (!m_device->createComputePipelines(nullptr, { ¶ms,1 }, &m_pipeline)) return logFail("Failed to create compute pipeline!\n"); } + for (int i = 0; i < 2; ++i) { m_images[i] = m_device->createImage(IGPUImage::SCreationParams { @@ -243,6 +211,7 @@ class CompatibilityTest final : public application_templates::MonoDeviceApplicat constexpr auto StartedValue = 0; smart_refctd_ptr progress = m_device->createSemaphore(StartedValue); + m_cmdbuf->reset(IGPUCommandBuffer::RESET_FLAGS::RELEASE_RESOURCES_BIT); m_cmdbuf->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT); @@ -593,7 +562,7 @@ void cpu_tests() auto zero = cross(x,x); auto lenX2 = dot(x,x); //auto z_inv = inverse(z); //busted return type conversion - auto mid = nbl::hlsl::mix(x,x,float32_t3(0.5f)); + auto mid = lerp(x,x,0.5f); //auto w = transpose(y); //also busted @@ -696,7 +665,6 @@ void cpu_tests() // countl_zero test mpl::countl_zero::value; - // TODO: fix warning about nodiscard std::countl_zero(5u); nbl::hlsl::countl_zero(5u); @@ -793,8 +761,8 @@ void cpu_tests() TEST_CMATH(fdim, 2, type) \ - //TEST_CMATH_FOR_TYPE(float32_t) - //TEST_CMATH_FOR_TYPE(float64_t) + TEST_CMATH_FOR_TYPE(float32_t) + TEST_CMATH_FOR_TYPE(float64_t) #endif std::cout << "cpu tests done\n"; } diff --git a/22_CppCompat/test.hlsl.orig b/22_CppCompat/test.hlsl.orig index a8ff6ce40..7f59cb228 100644 --- a/22_CppCompat/test.hlsl.orig +++ b/22_CppCompat/test.hlsl.orig @@ -7,6 +7,14 @@ #define STATIC_ASSERT(C) { nbl::hlsl::conditional::type a = 0; } #define IS_SAME(L,R) nbl::hlsl::is_same::value +#define SHADER_CRASHING_ASSERT(expr) \ +{ \ + bool con = (expr); \ + do { \ + [branch] if (!con) \ + vk::RawBufferStore(0xdeadbeefBADC0FFbull,0x45u,4u); \ + } while(!con); \ +} #include @@ -137,10 +145,10 @@ void main(uint3 invocationID : SV_DispatchThreadID) // uint32_t rotrResult0 = nbl::hlsl::mpl::rotr::value; // uint32_t rotrResult1 = nbl::hlsl::mpl::rotr::value; - // assert(rotlResult0 == mplRotlResult0); - // assert(rotlResult1 == mplRotlResult1); - // assert(rotrResult0 == mplRotrResult0); - // assert(rotrResult1 == mplRotrResult1); + // SHADER_CRASHING_ASSERT(rotlResult0 == mplRotlResult0); + // SHADER_CRASHING_ASSERT(rotlResult1 == mplRotlResult1); + // SHADER_CRASHING_ASSERT(rotrResult0 == mplRotrResult0); + // SHADER_CRASHING_ASSERT(rotrResult1 == mplRotrResult1); // TODO: more tests and compare with cpp version as well fill(invocationID, 5); @@ -157,27 +165,27 @@ void main(uint3 invocationID : SV_DispatchThreadID) uint16_t compileTimeCountLZero = nbl::hlsl::mpl::countl_zero::value; uint16_t runTimeCountLZero = nbl::hlsl::countl_zero(TEST_VALUE_0); fill(invocationID, float4(5.1, compileTimeCountLZero, runTimeCountLZero, 0)); - assert(compileTimeCountLZero == runTimeCountLZero); + SHADER_CRASHING_ASSERT(compileTimeCountLZero == runTimeCountLZero); compileTimeCountLZero = nbl::hlsl::mpl::countl_zero::value; runTimeCountLZero = nbl::hlsl::countl_zero(TEST_VALUE_1); fill(invocationID, float4(5.2, compileTimeCountLZero, runTimeCountLZero, 0)); - assert(compileTimeCountLZero == runTimeCountLZero); + SHADER_CRASHING_ASSERT(compileTimeCountLZero == runTimeCountLZero); compileTimeCountLZero = nbl::hlsl::mpl::countl_zero::value; runTimeCountLZero = nbl::hlsl::countl_zero(TEST_VALUE_2); fill(invocationID, float4(5.3, compileTimeCountLZero, runTimeCountLZero, 0)); - assert(compileTimeCountLZero == runTimeCountLZero); + SHADER_CRASHING_ASSERT(compileTimeCountLZero == runTimeCountLZero); compileTimeCountLZero = nbl::hlsl::mpl::countl_zero::value; runTimeCountLZero = nbl::hlsl::countl_zero(TEST_VALUE_3); fill(invocationID, float4(5.4, compileTimeCountLZero, runTimeCountLZero, 0)); - assert(compileTimeCountLZero == runTimeCountLZero); + SHADER_CRASHING_ASSERT(compileTimeCountLZero == runTimeCountLZero); compileTimeCountLZero = nbl::hlsl::mpl::countl_zero::value; runTimeCountLZero = nbl::hlsl::countl_zero(TEST_VALUE_4); fill(invocationID, float4(5.5, compileTimeCountLZero, runTimeCountLZero, 0)); - assert(compileTimeCountLZero == runTimeCountLZero); + SHADER_CRASHING_ASSERT(compileTimeCountLZero == runTimeCountLZero); } { @@ -193,7 +201,7 @@ void main(uint3 invocationID : SV_DispatchThreadID) { float4 v; fill(invocationID, float4(alignof(v.x), alignof(v), 0, 0)); - assert(alignof(v.x) == alignof(v)); + SHADER_CRASHING_ASSERT(alignof(v.x) == alignof(v)); } { @@ -258,7 +266,7 @@ void main(uint3 invocationID : SV_DispatchThreadID) decltype(fn(__VA_ARGS__)) lhs = nbl::hlsl::fn(__VA_ARGS__); \ decltype(lhs) rhs = fn(__VA_ARGS__); \ fill(invocationID, float4(idx, lhs, rhs)); \ - assert(abs(lhs - rhs) < nbl::hlsl::numeric_limits::epsilon) \ + SHADER_CRASHING_ASSERT(abs(lhs - rhs) < nbl::hlsl::numeric_limits::epsilon) \ } TEST_FN(float2(10, 1), cos, 1.44f) TEST_FN(float2(10, 2), sin, 1.44f) diff --git a/22_RaytracedAO/CMakeLists.txt b/22_RaytracedAO/CMakeLists.txt index 04c7b7401..06484ce41 100644 --- a/22_RaytracedAO/CMakeLists.txt +++ b/22_RaytracedAO/CMakeLists.txt @@ -31,10 +31,8 @@ endif() set(EXTRA_SOURCES ../../src/nbl/ext/DebugDraw/CDraw3DLine.cpp - ../../src/nbl/ext/EnvmapImportanceSampling/EnvmapImportanceSampling.cpp Renderer.cpp CommandLineHandler.cpp - SimpleJson.cpp ) nbl_create_executable_project( @@ -42,11 +40,5 @@ nbl_create_executable_project( "" "${RAY_TRACED_AO_EXAMPLE_INCLUDE_DIRS}" "${RAY_TRACED_AO_EXAMPLE_LIBS}" -) - -# TODO: Abstract this stuff away inside `nbl_create_executable_project` -if(NBL_RUN_TESTS) - get_target_property(_RTAO_OUTPUT_RUNTIME_RELEASE_ ${EXECUTABLE_NAME} RUNTIME_OUTPUT_DIRECTORY_RELEASE) - set(_NBL_PATHTRACER_EXE_ "${_RTAO_OUTPUT_RUNTIME_RELEASE_}/${EXECUTABLE_NAME}.exe" CACHE FILEPATH "") - configure_file("${NBL_ROOT_PATH}/tests/22.RaytracedAO/test.py" "${NBL_BINARY_PATH}/examples_tests/22.RaytracedAO/test.py") -endif() \ No newline at end of file + "${NBL_EXECUTABLE_PROJECT_CREATION_PCH_TARGET}" +) \ No newline at end of file diff --git a/22_RaytracedAO/CommandLineHandler.cpp b/22_RaytracedAO/CommandLineHandler.cpp index 12c20436c..a073da5c9 100644 --- a/22_RaytracedAO/CommandLineHandler.cpp +++ b/22_RaytracedAO/CommandLineHandler.cpp @@ -95,14 +95,7 @@ CommandLineHandler::CommandLineHandler(const std::vector& argv) else { const auto offset = firstHyphen + 1; - bool hasArguments = true; - auto endOfFetchedVariableName = rawFetchedCmdArgument.find_first_of("="); - if (endOfFetchedVariableName == std::string::npos) - { - hasArguments = false; - endOfFetchedVariableName = std::distance(rawFetchedCmdArgument.begin(), rawFetchedCmdArgument.end()); - } - + const auto endOfFetchedVariableName = rawFetchedCmdArgument.find_first_of("="); const auto count = endOfFetchedVariableName - offset; const auto cmdFetchedVariable = rawFetchedCmdArgument.substr(offset, count); std::string variable = cmdFetchedVariable; @@ -122,10 +115,9 @@ CommandLineHandler::CommandLineHandler(const std::vector& argv) break; } - if(hasArguments) + if(endOfFetchedVariableName != std::string::npos) { auto value = rawFetchedCmdArgument.substr(endOfFetchedVariableName + 1); - auto zipExtensionPos = value.find(".zip"); if(zipExtensionPos == std::string::npos) zipExtensionPos = value.find(".ZIP"); @@ -152,6 +144,7 @@ CommandLineHandler::CommandLineHandler(const std::vector& argv) toAdd.push_back(value); rawVariables[arg].emplace(toAdd); } + } else { @@ -161,6 +154,7 @@ CommandLineHandler::CommandLineHandler(const std::vector& argv) previousArg = arg; } + } if (!validateParameters() || !success) diff --git a/22_RaytracedAO/CommandLineHandler.hpp b/22_RaytracedAO/CommandLineHandler.hpp index 4330147e6..b65e2f1b6 100644 --- a/22_RaytracedAO/CommandLineHandler.hpp +++ b/22_RaytracedAO/CommandLineHandler.hpp @@ -16,93 +16,69 @@ constexpr std::string_view helpMessage = R"( Parameters: -SCENE=sceneMitsubaXMLPathOrZipAndXML +-TERMINATE Description and usage: -SCENE: some/path extra/path which will make it skip the file choose dialog --PROCESS_SENSORS ID: - It will control the behaviour of sensors in the app as detailed above. - If the option is not passed, then it defaults to RenderAllThenInteractive. - If the ID is not passed, then it defaults to 0. +-TERMINATE: + which will make the app stop when the required amount of samples has been renderered (its in the Mitsuba Scene metadata) and obviously take screenshot when quitting Example Usages : - raytracedao.exe -SCENE=../../media/kitchen.zip scene.xml - raytracedao.exe -SCENE=../../media/kitchen.zip scene.xml -PROCESS_SENSORS RenderAllThenInteractive - raytracedao.exe -SCENE="../../media/my good kitchen.zip" scene.xml -PROCESS_SENSORS RenderAllThenTerminate 0 - raytracedao.exe -SCENE="../../media/my good kitchen.zip scene.xml" -PROCESS_SENSORS RenderSensorThenInteractive 1 - raytracedao.exe -SCENE="../../media/extraced folder/scene.xml" -PROCESS_SENSORS InteractiveAtSensor 2 + raytracedao.exe -SCENE=../../media/kitchen.zip scene.xml -TERMINATE + raytracedao.exe -SCENE="../../media/my good kitchen.zip" scene.xml -TERMINATE + raytracedao.exe -SCENE="../../media/my good kitchen.zip scene.xml" -TERMINATE + raytracedao.exe -SCENE="../../media/extraced folder/scene.xml" -TERMINATE )"; constexpr std::string_view SCENE_VAR_NAME = "SCENE"; constexpr std::string_view SCREENSHOT_OUTPUT_FOLDER_VAR_NAME = "SCREENSHOT_OUTPUT_FOLDER"; -constexpr std::string_view PROCESS_SENSORS_VAR_NAME = "PROCESS_SENSORS"; -constexpr std::string_view DEFER_DENOISE_VAR_NAME = "DEFER_DENOISE"; +constexpr std::string_view TERMINATE_VAR_NAME = "TERMINATE"; constexpr uint32_t MaxRayTracerCommandLineArgs = 8; enum RaytracerExampleArguments { REA_SCENE, - REA_PROCESS_SENSORS, - REA_DEFER_DENOISE, + REA_TERMINATE, REA_COUNT, }; -enum class ProcessSensorsBehaviour -{ - PSB_RENDER_ALL_THEN_INTERACTIVE, - PSB_RENDER_ALL_THEN_TERMINATE, - PSB_RENDER_SENSOR_THEN_INTERACTIVE, - PSB_INTERACTIVE_AT_SENSOR, - PSB_COUNT -}; - using variablesType = std::unordered_map>>; class CommandLineHandler { public: + CommandLineHandler(const std::vector& argv); - inline auto& getSceneDirectory() const + auto& getSceneDirectory() const { return sceneDirectory; } - inline auto& getProcessSensorsBehaviour() const - { - return processSensorsBehaviour; - } - - inline auto& getSensorID() const + auto& getTerminate() const { - return sensorID; - } - - inline bool getDeferredDenoiseFlag() const - { - return isDenoiseDeferred; + return terminate; } private: + void initializeMatchingMap() { rawVariables[REA_SCENE]; - rawVariables[REA_PROCESS_SENSORS]; - rawVariables[REA_DEFER_DENOISE]; + rawVariables[REA_TERMINATE]; } RaytracerExampleArguments getMatchedVariableMapID(const std::string& variableName) { if (variableName == SCENE_VAR_NAME) return REA_SCENE; - else if (variableName == PROCESS_SENSORS_VAR_NAME) - return REA_PROCESS_SENSORS; - else if (variableName == DEFER_DENOISE_VAR_NAME) - return REA_DEFER_DENOISE; + else if (variableName == TERMINATE_VAR_NAME) + return REA_TERMINATE; else return REA_COUNT; } @@ -113,43 +89,8 @@ class CommandLineHandler { if(rawVariables[REA_SCENE].has_value()) sceneDirectory = rawVariables[REA_SCENE].value(); - if (rawVariables[REA_PROCESS_SENSORS].has_value()) - { - const auto& values = rawVariables[REA_PROCESS_SENSORS].value(); - for (uint32_t i = 0; i < values.size(); ++i) - { - if (i == 0) - { - const char* behaviour = values[0].c_str(); - if (strcmp(behaviour, "RenderAllThenInteractive") == 0) - { - processSensorsBehaviour = ProcessSensorsBehaviour::PSB_RENDER_ALL_THEN_INTERACTIVE; - } - else if (strcmp(behaviour, "RenderAllThenTerminate") == 0) - { - processSensorsBehaviour = ProcessSensorsBehaviour::PSB_RENDER_ALL_THEN_TERMINATE; - } - else if (strcmp(behaviour, "RenderSensorThenInteractive") == 0) - { - processSensorsBehaviour = ProcessSensorsBehaviour::PSB_RENDER_SENSOR_THEN_INTERACTIVE; - } - else if (strcmp(behaviour, "InteractiveAtSensor") == 0) - { - processSensorsBehaviour = ProcessSensorsBehaviour::PSB_INTERACTIVE_AT_SENSOR; - } - else - { - printf("[ERROR]: Invalid option for '%s'. Using RenderAllThenInteractive.\n", PROCESS_SENSORS_VAR_NAME.data()); - } - } - else if (i == 1) - { - sensorID = std::stoi(values[1]); - } - } - } - - isDenoiseDeferred = rawVariables[REA_DEFER_DENOISE].has_value(); + if(rawVariables[REA_TERMINATE].has_value()) + terminate = true; } variablesType rawVariables; @@ -157,13 +98,7 @@ class CommandLineHandler // Loaded from CMD std::vector sceneDirectory; // [0] zip [1] optional xml in zip std::string outputScreenshotsFolderPath; - bool isDenoiseDeferred; - struct - { - ProcessSensorsBehaviour processSensorsBehaviour = ProcessSensorsBehaviour::PSB_RENDER_ALL_THEN_INTERACTIVE; - uint32_t sensorID = 0; - }; - + bool terminate = false; }; #endif // _DENOISER_TONEMAPPER_COMMAND_LINE_HANDLER_ \ No newline at end of file diff --git a/22_RaytracedAO/README.md b/22_RaytracedAO/README.md index 2b6947322..8ab38d73d 100644 --- a/22_RaytracedAO/README.md +++ b/22_RaytracedAO/README.md @@ -2,26 +2,19 @@ ## How the Renderer works -* You have control over how the renderer behaves via the following options passed to the `-PROCESS_SENSORS` command line option: - - -| Option | Behaviour | -|-----------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `RenderAllThenInteractive ID` | It starts by rendering the scene with each sensor starting at the given ID, and will stop when enough samples are taken. To skip this part, press the `END` Key (more detail below). Then you'll have full control of the camera, you can take snapshots, move around and have fun :)| -| `RenderAllThenTerminate ID` | Same as above but it will exit after rendering with each sensor. | -| `RenderSensorThenInteractive ID` | This will render only with the passed sensor ID. If the sensor ID is not valid, then it defaults to the first one in Mitsuba metadata. After the rendering you'll be in interactive mode. | -| `InteractiveAtSensor ID` | This will skip all rendering and you'll go straight to the interactive mode with the given sensor ID. | - - -* For all the above options, if the ID is not passed, then it defaults to 0. +* It starts by rendering the scene with each sensor and will stop when enough samples is taken. + * To skip this part, press the `END` Key. (more detail below) +--- +* Then you'll have full control of the camera, you can take snapshots, move around and have fun :) + * To skip this part pass `-TERMINATE` as a cmd argument when executing the renderer (more detail below). +--- * Before Exiting from the Renderer, the very last view will be rendered and denoised to files named like `LastView_spaceship_Sensor_0` ## CommandLine Help ``` Parameters: -SCENE=sceneMitsubaXMLPathOrZipAndXML --PROCESS_SENSORS ID --DEFER_DENOISE +-TERMINATE Description and usage: @@ -30,83 +23,45 @@ Description and usage: NOTE: If the scene path contains space, put it between quotation marks --PROCESS_SENSORS ID: - It will control the behaviour of sensors in the app as detailed above. - If the option is not passed, then it defaults to RenderAllThenInteractive. - If the ID is not passed, then it defaults to 0. - --DEFER_DENOISE: - Defer all denoise operations until application is terminated. +-TERMINATE: + It will make the app stop when the required amount of samples has been renderered (its in the Mitsuba Scene metadata) and obviously take screenshot when quitting + Example Usages : - raytracedao.exe -SCENE=../../media/kitchen.zip scene.xml - raytracedao.exe -SCENE=../../media/kitchen.zip scene.xml -PROCESS_SENSORS RenderAllThenInteractive - raytracedao.exe -SCENE="../../media/my good kitchen.zip" scene.xml -PROCESS_SENSORS RenderAllThenTerminate 0 - raytracedao.exe -SCENE="../../media/my good kitchen.zip scene.xml" -PROCESS_SENSORS RenderSensorThenInteractive 1 - raytracedao.exe -SCENE="../../media/extraced folder/scene.xml" -PROCESS_SENSORS InteractiveAtSensor 2 -DEFER_DENOISE + raytracedao.exe -SCENE=../../media/kitchen.zip scene.xml -TERMINATE + raytracedao.exe -SCENE="../../media/my good kitchen.zip" scene.xml -TERMINATE + raytracedao.exe -SCENE="../../media/my good kitchen.zip scene.xml" -TERMINATE + raytracedao.exe -SCENE="../../media/extraced folder/scene.xml" -TERMINATE ``` ## New mitsuba properties and tags - Multiple Sensor tags in mitsuba XML's is now supported. This feature helps you have multiple views with different camera and film parameters without needing to execute the renderer and load again. You can switch between those sensors using `PAGE UP/DOWN` Keys defined in more detail below. -### Properties added to \: - -| Property Name | Description | Type | Default Value | -|-----------------|-------------------------------------------|---------|----------------| -| hideEnvironment | Replace bakcground with Transparent Alpha | boolean | false | - -Note that we don't support Mitsuba's `hideEmitters` - ### Properties added to \: -| Property Name | Description | Type | Default Value | -|---------------|-------------------------------------------------------------------------------------|---------|------------------------------------------| -| up | Up Vector to determine roll around view axis and the north pole to rotate around | vector | 0.0, 1.0, 0.0 | -| moveSpeed | Camera Movement Speed | float | NaN -> Will be deduced from scene bounds | -| zoomSpeed | Camera Zoom Speed | float | NaN -> Will be deduced from scene bounds | -| rotateSpeed | Camera Rotation Speed | float | 300.0 | -| clipPlaneN\* | Worldspace coefficients for a plane equation of the form `a*x + b*y + c*z + w >= 0` | vector | 0.0, 0.0, 0.0, 0.0 (disabled) | - -\* N ranges from 0 to 5 - -#### Properties added to \: - -| Property Name | Description | Type | Default Value | -|---------------|--------------------------------------------------------------------------|-------|---------------| -| shiftX | Right/Left Lens-Shift in NDC units, 1.0 moves screen center to the edge. | float | 0.0 | -| shiftY | Up/Down Lens-Shift in NDC units, 1.0 moves screen center to the edge. | float | 0.0 | - -### Properties added to \ -| Property Name | Description | Type | Default Value | -|----------------|-----------------------------------------------------------------------------------------------------------------------------------------|---------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| outputFilePath | Final Render Output Path;
Denoised Render will have "_denoised" suffix added to it. | string | Render_{SceneName}_Sensor_{SensorIdx}.exr
{SceneName} is the filename of the xml or zip loaded.
{SensorIdx} is the index of the Sensor in xml used for the render. | -| cascadeCount | The number of Luminance Cascades to use for the Re-Weighting Monte Carlo 2018 paper, 6 was the paper's default. | integer | 1 (disable RWMC) | -| cascadeLuminanceStart | The Luminance value of the first Cascade, 1.0 was the paper's default. | float | NaN (gets replaced by rfilter's Emin) | -| cascadeLuminanceBase | The magnitude factor between subsequent Luminance Cascades, 8.0 was the paper's default. | float | NaN (gets replaced by the N-th root of the ratio of Maximum Emitter Radiance's luminance over cascadeLuminanceStart, where N is `cascadeCount-1`) | -| bloomScale | Denoiser Bloom Scale | float | 0.1 | -| bloomIntensity | Denoiser Bloom Intensity | float | 0.1 | -| bloomFilePath | Lens Flare File Path | string | "../../media/kernels/physical_flare_512.exr" | -| tonemapper | Tonemapper Settings for Denoiser | string | "ACES=0.4,0.8" | -| cropOffsetX, cropOffsetY | Used to control the offset for cropping cubemap renders (instead of highQualityEdges) | int | 0 | -| cropWidth, cropHeight | Used to control the size for cropping cubemap renders (instead of highQualityEdges) | int | width-cropOffsetX, height-cropOffsetY | -|envmapRegularizationFactor| Fractional blend between guiding paths based on just the BxDF (0.0) or the product of the BxDF and the Environment Map (1.0)
Valid parameter ranges are between 0.0 and 0.8 as guiding fully by the product produces extreme fireflies from indirect light or local lights. | float | 0.5 | - +| Property Name | Description | Type | Default Value | +|---------------|-----------------------|-------|------------------------------------------| +| moveSpeed | Camera Movement Speed | float | NaN -> Will be deduced from scene bounds | +| zoomSpeed | Camera Zoom Speed | float | NaN -> Will be deduced from scene bounds | +| rotateSpeed | Camera Rotation Speed | float | 300.0 | ### Properties added to \ -| Property Name | Description | Type | Default Value | -|---------------|------------------------------------------------------------------------------------------------------------------|-------|---------------| -| kappa | Parameter from Re-weighting Monte Carlo 2018 paper where its an integer, high values reject more aggressively | float | 0.0 (disable) | -| Emin | Threshold of absolute luminance below which a sample is always considered reliable. Default taken from the paper.| float | 0.05 | +| Property Name | Description | Type | Default Value | +|----------------|----------------------------------------------------------------------------------------|--------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| outputFilePath | Final Render Output Path;
Denoised Render will have "_denoised" suffix added to it. | string | Render_{SceneName}_Sensor_{SensorIdx}.exr
{SceneName} is the filename of the xml or zip loaded.
{SensorIdx} is the index of the Sensor in xml used for the render. | +| bloomScale | Denoiser Bloom Scale | float | 0.1 | +| bloomIntensity | Denoiser Bloom Intensity | float | 0.1 | +| bloomFilePath | Lens Flare File Path | string | "../../media/kernels/physical_flare_512.exr" | +| tonemapper | Tonemapper Settings for Denoiser | string | "ACES=0.4,0.8" | +| highQualityEdges | Number in pixels (prevously was bool) to add to borders for more accurate denoising | integer| 0 | ### Example of a sensor using all new properties described above. ```xml - @@ -124,83 +79,14 @@ Note that we don't support Mitsuba's `hideEmitters` - - - - - - - + ``` -### Example of Cubemap Render -```xml - - - - - - - - - - - - - - - - - - - - - - - - - - -``` - -Example above renders to 1024x1024 cubemap sides and uses 64 offset pixels for higher quality. -So the full width, height are 1152x1152 (64+1024+64=1152) - -### Tags added to \: - -#### \ (IES emission profile) - -**\** tag can be used to attach IES light profile to area emitter. - -| Property Name | Description | Type | Default Value | -|---------------|------------------------------------------------------------------------------------------------------------------|-------|---------------| -| normalization | Parameter to normalize the intensity of emission profile.
1) If `normalization` is `NONE` or invalid/none of the below, it will not perform any normalization.
2) If `normalization` is `UNIT_MAX`, it will normalize the intensity by dividing out the maximum intensity. (normalization by max)
3) If `normalization` is `UNIT_AVERAGE_OVER_IMPLIED_DOMAIN`, it will integrate the profile over the hemisphere as well as the solid angles where the profile has emission above 0. This has an advantage over a plain average as you don't need to care whether the light is a sphere, hemisphere, or a spotlight of a given aperture. (normalization by energy)
4) If `normalization` is `UNIT_AVERAGE_OVER_FULL_DOMAIN` we behave like `UNIT_AVERAGE` but presume the soild angle of the domain is `(CIESProfile::vAngles.front()-CIESProfile::vAngles.back())*4.f` | string | ""
(no normalization) | -| flatten | Optional "blend" of the original profile value with the average value, if negative we use the average as if for `UNIT_AVERAGE_OVER_FULL_DOMAIN` if positive we use the average as-if for `UNIT_AVERAGE_OVER_IMPLIED_DOMAIN`.
This is useful when the emitter appears "not bright enough" when observing from directions outside the main power-lobes.
Valid range is 0.0 to 1.0, value gets treated with `min(abs(flatten),1.f)` to make it conform.
A value equal to 1.0 or -1.0 will render your IES profile uniform, so its not something you should use and a warning will be emitted. | float | 0.0 | -| filename | The filename of the IES profile. | string | "" | - - -NOTE: **\** tag of emitter node can be used to orient the emission direction of IES light. - -#### Example of Area Light with IES Profile which flattens its profile against a full Sphere or Hemisphere average - -```xml - - - - - - - - - - - -``` ## Mouse @@ -222,7 +108,6 @@ NOTE: **\** tag of emitter node can be used to orient the emission d | P | Press to take a snapshot when moving around (will be denoised) | | L | Press to log the current progress percentage and samples rendered. | | B | Toggle between Path Tracing and Albedo preview, allows you to position the camera more responsively in complex scenes. | -| F5 | Press to reload the application. | ## Denoiser Hook `denoiser_hook.bat` is a script that you can call to denoise your rendered images. @@ -255,4 +140,4 @@ Here is an example of `test_scenes.txt`: ; Here is my Commented line that batch file will skip (started with semicolons) ; "relative/dir/from/bin/folder/to/scene.zip something.xml ``` -lines with semicolons will be skipped. +lines with semicolons will be skipped. \ No newline at end of file diff --git a/22_RaytracedAO/Renderer.cpp b/22_RaytracedAO/Renderer.cpp index 589baba74..cdaff9fca 100644 --- a/22_RaytracedAO/Renderer.cpp +++ b/22_RaytracedAO/Renderer.cpp @@ -21,6 +21,18 @@ using namespace nbl::video; constexpr uint32_t kOptiXPixelSize = sizeof(uint16_t)*3u; +core::smart_refctd_ptr specializedShaderFromFile(IAssetManager* assetManager, const char* path) +{ + auto bundle = assetManager->getAsset(path, {}); + return core::smart_refctd_ptr_static_cast(*bundle.getContents().begin()); +} +core::smart_refctd_ptr gpuSpecializedShaderFromFile(IAssetManager* assetManager, IVideoDriver* driver, const char* path) +{ + auto shader = specializedShaderFromFile(assetManager,path); + // TODO: @Crisspl find a way to stop the user from such insanity as moving from the bundle's dynamic array + //return std::move(driver->getGPUObjectsFromAssets(&shader,&shader+1u)->operator[](0)); + return driver->getGPUObjectsFromAssets(&shader,&shader+1u)->operator[](0); +} // TODO: make these util function in `IDescriptorSetLayout` -> Assign: @Vib auto fillIotaDescriptorBindingDeclarations = [](auto* outBindings, uint32_t accessFlags, uint32_t count, asset::E_DESCRIPTOR_TYPE descType=asset::EDT_INVALID, uint32_t startIndex=0u) -> void { @@ -34,15 +46,14 @@ auto fillIotaDescriptorBindingDeclarations = [](auto* outBindings, uint32_t acce } }; -Renderer::Renderer(IVideoDriver* _driver, IAssetManager* _assetManager, scene::ISceneManager* _smgr, bool deferDenoise, bool useDenoiser) : - m_useDenoiser(useDenoiser), m_deferDenoise(deferDenoise), m_driver(_driver), m_smgr(_smgr), m_assetManager(_assetManager), +Renderer::Renderer(IVideoDriver* _driver, IAssetManager* _assetManager, scene::ISceneManager* _smgr, bool useDenoiser) : + m_useDenoiser(useDenoiser), m_driver(_driver), m_smgr(_smgr), m_assetManager(_assetManager), m_rrManager(ext::RadeonRays::Manager::create(m_driver)), - m_prevView(), m_prevCamTform(), m_sceneBound(FLT_MAX,FLT_MAX,FLT_MAX,-FLT_MAX,-FLT_MAX,-FLT_MAX), m_maxAreaLightLuma(0.f), + m_prevView(), m_prevCamTform(), m_sceneBound(FLT_MAX,FLT_MAX,FLT_MAX,-FLT_MAX,-FLT_MAX,-FLT_MAX), m_framesDispatched(0u), m_rcpPixelSize{0.f,0.f}, - m_staticViewData{ {0u,0u},0u,0u,0u,0u,false,core::infinity(),{}}, m_raytraceCommonData{0.f,0u,0u,0u,core::matrix3x4SIMD()}, + m_staticViewData{{0u,0u},0u,0u}, m_raytraceCommonData{core::matrix4SIMD(), vec3(),0.f,0u,0u,0u,0.f}, m_indirectDrawBuffers{nullptr},m_cullPushConstants{core::matrix4SIMD(),1.f,0u,0u,0u},m_cullWorkGroups(0u), - m_raygenWorkGroups{0u,0u},m_colorBuffer(nullptr), - m_envMapImportanceSampling(_driver) + m_raygenWorkGroups{0u,0u},m_visibilityBuffer(nullptr),m_colorBuffer(nullptr) { // TODO: reimplement m_useDenoiser = false; @@ -50,35 +61,32 @@ Renderer::Renderer(IVideoDriver* _driver, IAssetManager* _assetManager, scene::I // set up raycount buffers { const uint32_t zeros[RAYCOUNT_N_BUFFERING] = { 0u }; - m_rayCountBuffer = m_driver->createFilledDeviceLocalGPUBufferOnDedMem(sizeof(uint32_t)*RAYCOUNT_N_BUFFERING,zeros); - IDriverMemoryBacked::SDriverMemoryRequirements reqs; + m_rayCountBuffer = m_driver->createFilledDeviceLocalBufferOnDedMem(sizeof(uint32_t)*RAYCOUNT_N_BUFFERING,zeros); + IDeviceMemoryBacked::SDeviceMemoryRequirements reqs; reqs.vulkanReqs.size = sizeof(uint32_t); reqs.vulkanReqs.alignment = alignof(uint32_t); reqs.vulkanReqs.memoryTypeBits = ~0u; - reqs.memoryHeapLocation = IDriverMemoryAllocation::ESMT_NOT_DEVICE_LOCAL; - reqs.mappingCapability = IDriverMemoryAllocation::EMCF_COHERENT|IDriverMemoryAllocation::EMCF_CAN_MAP_FOR_READ; + reqs.memoryHeapLocation = IDeviceMemoryAllocation::ESMT_NOT_DEVICE_LOCAL; + reqs.mappingCapability = IDeviceMemoryAllocation::EMCF_COHERENT|IDeviceMemoryAllocation::EMCF_CAN_MAP_FOR_READ; reqs.prefersDedicatedAllocation = 0u; reqs.requiresDedicatedAllocation = 0u; m_littleDownloadBuffer = m_driver->createGPUBufferOnDedMem(reqs); - m_littleDownloadBuffer->getBoundMemory()->mapMemoryRange(IDriverMemoryAllocation::EMCAF_READ,{0,sizeof(uint32_t)}); + m_littleDownloadBuffer->getBoundMemory()->mapMemoryRange(IDeviceMemoryAllocation::EMCAF_READ,{0,sizeof(uint32_t)}); } - // no deferral for now - m_fragGPUShader = gpuSpecializedShaderFromFile(m_assetManager,m_driver,"../fillVisBuffer.frag"); - // set up Visibility Buffer pipeline { IGPUDescriptorSetLayout::SBinding binding; fillIotaDescriptorBindingDeclarations(&binding,ISpecializedShader::ESS_VERTEX|ISpecializedShader::ESS_FRAGMENT,1u,asset::EDT_STORAGE_BUFFER); - m_rasterInstanceDataDSLayout = m_driver->createGPUDescriptorSetLayout(&binding,&binding+1u); + m_rasterInstanceDataDSLayout = m_driver->createDescriptorSetLayout(&binding,&binding+1u); } { constexpr auto additionalGlobalDescriptorCount = 5u; IGPUDescriptorSetLayout::SBinding bindings[additionalGlobalDescriptorCount]; fillIotaDescriptorBindingDeclarations(bindings,ISpecializedShader::ESS_COMPUTE|ISpecializedShader::ESS_VERTEX|ISpecializedShader::ESS_FRAGMENT,additionalGlobalDescriptorCount,asset::EDT_STORAGE_BUFFER); - m_additionalGlobalDSLayout = m_driver->createGPUDescriptorSetLayout(bindings,bindings+additionalGlobalDescriptorCount); + m_additionalGlobalDSLayout = m_driver->createDescriptorSetLayout(bindings,bindings+additionalGlobalDescriptorCount); } { constexpr auto cullingDescriptorCount = 3u; @@ -86,12 +94,29 @@ Renderer::Renderer(IVideoDriver* _driver, IAssetManager* _assetManager, scene::I fillIotaDescriptorBindingDeclarations(bindings,ISpecializedShader::ESS_COMPUTE|ISpecializedShader::ESS_VERTEX,cullingDescriptorCount,asset::EDT_STORAGE_BUFFER); bindings[2u].count = 2u; - m_cullDSLayout = m_driver->createGPUDescriptorSetLayout(bindings,bindings+cullingDescriptorCount); + m_cullDSLayout = m_driver->createDescriptorSetLayout(bindings,bindings+cullingDescriptorCount); } m_perCameraRasterDSLayout = core::smart_refctd_ptr(m_cullDSLayout); + { + core::smart_refctd_ptr shaders[] = {gpuSpecializedShaderFromFile(m_assetManager,m_driver,"../fillVisBuffer.vert"),gpuSpecializedShaderFromFile(m_assetManager,m_driver,"../fillVisBuffer.frag")}; + SPrimitiveAssemblyParams primitiveAssembly; + primitiveAssembly.primitiveType = EPT_TRIANGLE_LIST; + SRasterizationParams raster; + raster.faceCullingMode = EFCM_NONE; + auto _visibilityBufferFillPipelineLayout = m_driver->createPipelineLayout( + nullptr,nullptr, + core::smart_refctd_ptr(m_rasterInstanceDataDSLayout), + core::smart_refctd_ptr(m_additionalGlobalDSLayout), + core::smart_refctd_ptr(m_cullDSLayout) + ); + m_visibilityBufferFillPipeline = m_driver->createGPURenderpassIndependentPipeline( + nullptr,std::move(_visibilityBufferFillPipelineLayout),&shaders->get(),&shaders->get()+2u, + SVertexInputParams{},SBlendParams{},primitiveAssembly,raster + ); + } { - constexpr auto raytracingCommonDescriptorCount = 11u; + constexpr auto raytracingCommonDescriptorCount = 8u; IGPUDescriptorSetLayout::SBinding bindings[raytracingCommonDescriptorCount]; fillIotaDescriptorBindingDeclarations(bindings,ISpecializedShader::ESS_COMPUTE,raytracingCommonDescriptorCount); bindings[0].type = asset::EDT_UNIFORM_BUFFER; @@ -101,11 +126,9 @@ Renderer::Renderer(IVideoDriver* _driver, IAssetManager* _assetManager, scene::I bindings[4].type = asset::EDT_STORAGE_BUFFER; bindings[5].type = asset::EDT_STORAGE_IMAGE; bindings[6].type = asset::EDT_STORAGE_IMAGE; - bindings[7].type = asset::EDT_STORAGE_IMAGE; - bindings[8].type = asset::EDT_COMBINED_IMAGE_SAMPLER; - bindings[9].type = asset::EDT_COMBINED_IMAGE_SAMPLER; - bindings[10].type = asset::EDT_COMBINED_IMAGE_SAMPLER; - m_commonRaytracingDSLayout = m_driver->createGPUDescriptorSetLayout(bindings,bindings+raytracingCommonDescriptorCount); + bindings[7].type = asset::EDT_COMBINED_IMAGE_SAMPLER; + + m_commonRaytracingDSLayout = m_driver->createDescriptorSetLayout(bindings,bindings+raytracingCommonDescriptorCount); } ISampler::SParams samplerParams; @@ -114,7 +137,7 @@ Renderer::Renderer(IVideoDriver* _driver, IAssetManager* _assetManager, scene::I samplerParams.MipmapMode = ISampler::ESMM_NEAREST; samplerParams.AnisotropicFilter = 0u; samplerParams.CompareEnable = false; - auto sampler = m_driver->createGPUSampler(samplerParams); + auto sampler = m_driver->createSampler(samplerParams); { constexpr auto raygenDescriptorCount = 3u; IGPUDescriptorSetLayout::SBinding bindings[raygenDescriptorCount]; @@ -123,17 +146,17 @@ Renderer::Renderer(IVideoDriver* _driver, IAssetManager* _assetManager, scene::I bindings[1].samplers = &sampler; bindings[2].type = asset::EDT_STORAGE_IMAGE; - m_raygenDSLayout = m_driver->createGPUDescriptorSetLayout(bindings,bindings+raygenDescriptorCount); + m_raygenDSLayout = m_driver->createDescriptorSetLayout(bindings,bindings+raygenDescriptorCount); } { constexpr auto closestHitDescriptorCount = 2u; IGPUDescriptorSetLayout::SBinding bindings[2]; fillIotaDescriptorBindingDeclarations(bindings,ISpecializedShader::ESS_COMPUTE,closestHitDescriptorCount,EDT_STORAGE_BUFFER); - m_closestHitDSLayout = m_driver->createGPUDescriptorSetLayout(bindings,bindings+closestHitDescriptorCount); + m_closestHitDSLayout = m_driver->createDescriptorSetLayout(bindings,bindings+closestHitDescriptorCount); } { - constexpr auto resolveDescriptorCount = 8u; + constexpr auto resolveDescriptorCount = 7u; IGPUDescriptorSetLayout::SBinding bindings[resolveDescriptorCount]; fillIotaDescriptorBindingDeclarations(bindings,ISpecializedShader::ESS_COMPUTE,resolveDescriptorCount); bindings[0].type = asset::EDT_UNIFORM_BUFFER; @@ -143,24 +166,17 @@ Renderer::Renderer(IVideoDriver* _driver, IAssetManager* _assetManager, scene::I bindings[2].samplers = &sampler; bindings[3].type = asset::EDT_COMBINED_IMAGE_SAMPLER; bindings[3].samplers = &sampler; - bindings[4].type = asset::EDT_COMBINED_IMAGE_SAMPLER; - bindings[4].samplers = &sampler; + bindings[4].type = asset::EDT_STORAGE_IMAGE; bindings[5].type = asset::EDT_STORAGE_IMAGE; bindings[6].type = asset::EDT_STORAGE_IMAGE; - bindings[7].type = asset::EDT_STORAGE_IMAGE; - m_resolveDSLayout = m_driver->createGPUDescriptorSetLayout(bindings,bindings+resolveDescriptorCount); + m_resolveDSLayout = m_driver->createDescriptorSetLayout(bindings,bindings+resolveDescriptorCount); } - - if(m_deferDenoise) - m_deferDenoiseFile.open(DEFER_DENOISE_HOOK_FILE_NAME, std::ios_base::in | std::ios_base::out | std::ios_base::trunc); } Renderer::~Renderer() { deinitSceneResources(); - deinitRenderer(); - finalizeDeferredDenoise(); } @@ -171,9 +187,9 @@ Renderer::InitializationData Renderer::initSceneObjects(const SAssetBundle& mesh using GPUMeshPacker = CGPUMeshPackerV2; // get primary (texture and material) global DS - InitializationData retval = {}; + InitializationData retval; m_globalMeta = meshes.getMetadata()->selfCast(); - assert(m_globalMeta); + assert(m_globalMeta ); // { @@ -188,16 +204,13 @@ Renderer::InitializationData Renderer::initSceneObjects(const SAssetBundle& mesh switch (integrator->type) { case Enum::DIRECT: - maxPathDepth = 2u; - hideEnvironment = integrator->direct.hideEnvironment; + pathDepth = 2u; break; case Enum::PATH: - hideEnvironment = integrator->path.hideEnvironment; - [[fallthrough]]; case Enum::VOL_PATH_SIMPLE: case Enum::VOL_PATH: case Enum::BDPT: - maxPathDepth = integrator->bdpt.maxPathDepth; + pathDepth = integrator->bdpt.maxPathDepth; noRussianRouletteDepth = integrator->bdpt.russianRouletteDepth-1u; break; case Enum::ADAPTIVE: @@ -225,10 +238,9 @@ Renderer::InitializationData Renderer::initSceneObjects(const SAssetBundle& mesh } // - auto* _globalBackendDataDS = m_globalMeta->m_global.m_ds0.get(); + auto* _globalBackendDataDS = m_globalMeta ->m_global.m_ds0.get(); - constexpr auto kInstanceDataDescriptorBinding = 5u; - auto* instanceDataDescPtr = _globalBackendDataDS->getDescriptors(kInstanceDataDescriptorBinding).begin(); + auto* instanceDataDescPtr = _globalBackendDataDS->getDescriptors(5u).begin(); assert(instanceDataDescPtr->desc->getTypeCategory()==IDescriptor::EC_BUFFER); auto* origInstanceData = reinterpret_cast(static_cast(instanceDataDescPtr->desc.get())->getPointer()); @@ -255,7 +267,7 @@ Renderer::InitializationData Renderer::initSceneObjects(const SAssetBundle& mesh writes[i].dstSet = dstSet; }; // make secondary (geometry) DS - m_additionalGlobalDS = m_driver->createGPUDescriptorSet(core::smart_refctd_ptr(m_additionalGlobalDSLayout)); + m_additionalGlobalDS = m_driver->createDescriptorSet(core::smart_refctd_ptr(m_additionalGlobalDSLayout)); // one cull data per instace of a batch core::vector cullData; @@ -264,8 +276,6 @@ Renderer::InitializationData Renderer::initSceneObjects(const SAssetBundle& mesh // set up batches/meshlets, lights and culling data { auto contents = meshes.getContents(); - const auto meshCount = contents.size(); - printf("[INFO] Mesh Count: %d\n",meshCount); core::vector pmbd; // split into packed batches @@ -273,7 +283,7 @@ Renderer::InitializationData Renderer::initSceneObjects(const SAssetBundle& mesh // one instance data per instance of a batch core::smart_refctd_ptr newInstanceDataBuffer; - constexpr uint16_t minTrisBatch = MAX_TRIANGLES_IN_BATCH>>3u; // allow small allocations to fight fragmentation + constexpr uint16_t minTrisBatch = MAX_TRIANGLES_IN_BATCH>>1u; constexpr uint16_t maxTrisBatch = MAX_TRIANGLES_IN_BATCH; constexpr uint8_t minVertexSize = asset::getTexelOrBlockBytesize()+ @@ -284,8 +294,8 @@ Renderer::InitializationData Renderer::initSceneObjects(const SAssetBundle& mesh constexpr uint16_t minIndicesBatch = minTrisBatch*kIndicesPerTriangle; CPUMeshPacker::AllocationParams allocParams; - allocParams.vertexBuffSupportedByteSize = (1u<<31u)-1; // RTX cards - allocParams.vertexBufferMinAllocByteSize = minTrisBatch*minVertexSize; // under max vertex reuse + allocParams.vertexBuffSupportedByteSize = 1u<<31u; + allocParams.vertexBufferMinAllocByteSize = minTrisBatch*minVertexSize; allocParams.indexBuffSupportedCnt = (allocParams.vertexBuffSupportedByteSize/allocParams.vertexBufferMinAllocByteSize)*minIndicesBatch; allocParams.indexBufferMinAllocCnt = minIndicesBatch; allocParams.MDIDataBuffSupportedCnt = allocParams.indexBuffSupportedCnt/minIndicesBatch; @@ -298,7 +308,7 @@ Renderer::InitializationData Renderer::initSceneObjects(const SAssetBundle& mesh IMeshPackerV2Base::SupportedFormatsContainer formats; formats.insert(EF_R32G32B32_SFLOAT); - formats.insert(EF_R32G32B32_UINT); + formats.insert(EF_R32G32_UINT); auto cpump = core::make_smart_refctd_ptr>(allocParams,formats,minTrisBatch,maxTrisBatch); uint32_t mdiBoundMax=0u,batchInstanceBoundTotal=0u; core::vector allocData; @@ -309,9 +319,6 @@ Renderer::InitializationData Renderer::initSceneObjects(const SAssetBundle& mesh // TODO: Optimize! Check which triangles need normals, bin into two separate meshbuffers, dont have normals for meshbuffers where all(abs(transpose(normals)*cross(pos1-pos0,pos2-pos0))~=1.f) // TODO: Optimize! Check which materials use any textures, if meshbuffer doens't use any textures, its pipeline doesn't need UV coordinates // TODO: separate pipeline for stuff without UVs and separate out the barycentric derivative FBO attachment - // stats - uint32_t totalInstanceCount = 0; - size_t totalInstancedTriangleCount = 0u; for (const auto& asset : contents) { auto cpumesh = static_cast(asset.get()); @@ -319,31 +326,26 @@ Renderer::InitializationData Renderer::initSceneObjects(const SAssetBundle& mesh assert(!meshBuffers.empty()); const uint32_t instanceCount = (*meshBuffers.begin())->getInstanceCount(); - totalInstanceCount += instanceCount; for (auto mbIt=meshBuffers.begin(); mbIt!=meshBuffers.end(); mbIt++) { auto meshBuffer = *mbIt; - totalInstancedTriangleCount += (meshBuffer->getIndexCount()/3)*instanceCount; assert(meshBuffer->getInstanceCount()==instanceCount); // We'll disable certain attributes to ensure we only copy position, normal and uv attribute SVertexInputParams& vertexInput = meshBuffer->getPipeline()->getVertexInputParams(); - // but we'll pack normals and UVs together to save one SSBO binding, but no quantization of UVs to keep accurate floating point precision for baricentrics + // but we'll pack normals and UVs together to save one SSBO binding (and quantize UVs to half floats) constexpr auto freeBinding = 15u; vertexInput.attributes[combinedNormalUVAttributeIx].binding = freeBinding; - vertexInput.attributes[combinedNormalUVAttributeIx].format = EF_R32G32B32_UINT; + vertexInput.attributes[combinedNormalUVAttributeIx].format = EF_R32G32_UINT; vertexInput.attributes[combinedNormalUVAttributeIx].relativeOffset = 0u; vertexInput.enabledBindingFlags |= 0x1u<getBaseVertex(); - struct CombinedNormalUV { - uint32_t normal; - float u, v; + uint32_t nml; + uint16_t u,v; }; - static_assert(sizeof(CombinedNormalUV) == sizeof(float) * 3u); - auto newBuff = core::make_smart_refctd_ptr(sizeof(CombinedNormalUV)*approxVxCount); auto* dst = reinterpret_cast(newBuff->getPointer())+meshBuffer->getBaseVertex(); meshBuffer->setVertexBufferBinding({0u,newBuff},freeBinding); @@ -352,11 +354,11 @@ Renderer::InitializationData Renderer::initSceneObjects(const SAssetBundle& mesh vertexInput.attributes[normalAttr].format = EF_R32_UINT; for (auto i=0u; igetAttribute(&dst[i].normal,normalAttr,i); + meshBuffer->getAttribute(&dst[i].nml,normalAttr,i); core::vectorSIMDf uv; meshBuffer->getAttribute(uv,2u,i); - dst[i].u = uv.x; - dst[i].v = uv.y; + dst[i].u = core::Float16Compressor::compress(uv.x); + dst[i].v = core::Float16Compressor::compress(uv.y); } } @@ -366,29 +368,12 @@ Renderer::InitializationData Renderer::initSceneObjects(const SAssetBundle& mesh meshBuffersToProcess.insert(meshBuffersToProcess.end(),meshBuffers.begin(),meshBuffers.end()); } - if (totalInstancedTriangleCount >= ~0x0u) - printf("[ERROR] Over 2^32-1 Triangles, WILL CRASH!\n"); - printf("[INFO] Total Instanced Triangles in the Scene: %d\n",totalInstancedTriangleCount); - { - // Radeon Rays uses 64 byte nodes in BVH2 - const auto bvh_size = size_t(totalInstancedTriangleCount)*64u*2u; - printf("[INFO] BVH Size: At Least %d MB\n",bvh_size>>20u); - if ((0x1ull<<31)(meshBuffer)->getPipeline()->getVertexInputParams().enabledAttribFlags = newEnabledAttributeMask; allocData.resize(meshBuffersToProcess.size()); - if (!cpump->alloc(allocData.data(),meshBuffersToProcess.begin(),meshBuffersToProcess.end())) - { - printf("[ERROR] Failed to Allocate Mesh data in SSBOs, quitting!\n"); - exit(-42); - } + cpump->alloc(allocData.data(),meshBuffersToProcess.begin(),meshBuffersToProcess.end()); cpump->shrinkOutputBuffersSize(); cpump->instantiateDataStorage(); @@ -397,13 +382,6 @@ Renderer::InitializationData Renderer::initSceneObjects(const SAssetBundle& mesh newInstanceDataBuffer = core::make_smart_refctd_ptr(sizeof(ext::MitsubaLoader::instance_data_t)*batchInstanceBoundTotal); } - // - { - const auto totalIndexBytes = cpump->getIndexAllocator().get_allocated_size(); - printf("[INFO] Total Index Memory Consumed: %d MB\n",((totalIndexBytes-1)>>20)+1); - const auto totalVertexBytes = cpump->getVertexAllocator().get_allocated_size(); - printf("[INFO] Total Vertex Memory Consumed: %d MB\n",((totalVertexBytes-1)>>20)+1); - } // actually commit the physical memory, compute batches and set up instance data { auto allocDataIt = allocData.begin(); @@ -512,12 +490,9 @@ Renderer::InitializationData Renderer::initSceneObjects(const SAssetBundle& mesh SLight newLight(aabbMesh,newInstanceData->tform); // TODO: should be an OBB - const float luma = newLight.computeLuma(emitter.area.radiance); - const float weight = newLight.computeFluxBound(luma)*emitter.area.samplingWeight; + const float weight = newLight.computeFluxBound(emitter.area.radiance)*emitter.area.samplingWeight; if (weight<=FLT_MIN) continue; - if (m_maxAreaLightLuma < luma) - m_maxAreaLightLuma = luma; retval.lights.emplace_back(std::move(newLight)); retval.lightPDF.push_back(weight); @@ -544,7 +519,7 @@ Renderer::InitializationData Renderer::initSceneObjects(const SAssetBundle& mesh } } } - printf("[INFO] Scene Bound: %f,%f,%f -> %f,%f,%f\n", + printf("Scene Bound: %f,%f,%f -> %f,%f,%f\n", m_sceneBound.MinEdge.X, m_sceneBound.MinEdge.Y, m_sceneBound.MinEdge.Z, @@ -595,12 +570,12 @@ Renderer::InitializationData Renderer::initSceneObjects(const SAssetBundle& mesh m_cullPushConstants.currentCommandBufferIx = 0x0u; m_cullWorkGroups = (m_cullPushConstants.maxGlobalInstanceCount-1u)/WORKGROUP_SIZE+1u; - m_cullDS = m_driver->createGPUDescriptorSet(core::smart_refctd_ptr(m_cullDSLayout)); - m_perCameraRasterDS = m_driver->createGPUDescriptorSet(core::smart_refctd_ptr(m_perCameraRasterDSLayout)); + m_cullDS = m_driver->createDescriptorSet(core::smart_refctd_ptr(m_cullDSLayout)); + m_perCameraRasterDS = m_driver->createDescriptorSet(core::smart_refctd_ptr(m_perCameraRasterDSLayout)); { recordInfoBuffer(infos[3],core::smart_refctd_ptr(m_indirectDrawBuffers[1])); recordInfoBuffer(infos[2],core::smart_refctd_ptr(m_indirectDrawBuffers[0])); - recordInfoBuffer(infos[1],m_driver->createFilledDeviceLocalGPUBufferOnDedMem(m_cullPushConstants.maxGlobalInstanceCount*sizeof(CullData_t),cullData.data())); + recordInfoBuffer(infos[1],m_driver->createFilledDeviceLocalBufferOnDedMem(m_cullPushConstants.maxGlobalInstanceCount*sizeof(CullData_t),cullData.data())); cullData.clear(); recordInfoBuffer(infos[0],m_driver->createDeviceLocalGPUBufferOnDedMem(m_cullPushConstants.maxGlobalInstanceCount*sizeof(DrawData_t))); @@ -617,7 +592,7 @@ Renderer::InitializationData Renderer::initSceneObjects(const SAssetBundle& mesh // TODO: after port to new API, use a converter which does not generate mip maps m_globalBackendDataDS = m_driver->getGPUObjectsFromAssets(&_globalBackendDataDS,&_globalBackendDataDS+1)->front(); // make a shortened version of the globalBackendDataDS - m_rasterInstanceDataDS = m_driver->createGPUDescriptorSet(core::smart_refctd_ptr(m_rasterInstanceDataDSLayout)); + m_rasterInstanceDataDS = m_driver->createDescriptorSet(core::smart_refctd_ptr(m_rasterInstanceDataDSLayout)); { IGPUDescriptorSet::SCopyDescriptorSet copy = {}; copy.dstSet = m_rasterInstanceDataDS.get(); @@ -634,90 +609,116 @@ Renderer::InitializationData Renderer::initSceneObjects(const SAssetBundle& mesh void Renderer::initSceneNonAreaLights(Renderer::InitializationData& initData) { - core::smart_refctd_ptr paramsBuffer; + core::vectorSIMDf _envmapBaseColor; + _envmapBaseColor.set(0.0f,0.0f,0.0f,1.f); + + for (const auto& emitter : m_globalMeta->m_global.m_emitters) { - core::vector params({core::vectorSIMDf(0.0f,0.0f,0.0f,1.f)}); - for (const auto& emitter : m_globalMeta->m_global.m_emitters) + float weight = 0.f; + switch (emitter.type) { - float weight = 0.f; - switch (emitter.type) + case ext::MitsubaLoader::CElementEmitter::Type::CONSTANT: { - case ext::MitsubaLoader::CElementEmitter::Type::CONSTANT: - { - params.front() += emitter.constant.radiance; - break; - } - case ext::MitsubaLoader::CElementEmitter::Type::ENVMAP: - { - std::cout << "ENVMAP FOUND = " << std::endl; - std::cout << "\tScale = " << emitter.envmap.scale << std::endl; - std::cout << "\tGamma = " << emitter.envmap.gamma << std::endl; - std::cout << "\tSamplingWeight = " << emitter.envmap.samplingWeight << std::endl; - // LOAD file relative to the XML - std::cout << "\tFileName = " << emitter.envmap.filename.svalue << std::endl; - core::matrix3x4SIMD invTform; - emitter.transform.matrix.extractSub3x4().getInverse(invTform); - params.push_back(invTform.rows[0]); - params.push_back(invTform.rows[1]); - params.push_back(invTform.rows[2]); - break; - } - case ext::MitsubaLoader::CElementEmitter::Type::INVALID: - break; - default: - #ifdef _DEBUG - assert(false); - #endif - // let's implement a new emitter type! - //weight = emitter.unionType.samplingWeight; - break; + _envmapBaseColor += emitter.constant.radiance; } - if (weight==0.f) - continue; + break; + case ext::MitsubaLoader::CElementEmitter::Type::ENVMAP: + { + std::cout << "ENVMAP FOUND = " << std::endl; + std::cout << "\tScale = " << emitter.envmap.scale << std::endl; + std::cout << "\tGamma = " << emitter.envmap.gamma << std::endl; + std::cout << "\tSamplingWeight = " << emitter.envmap.samplingWeight << std::endl; + std::cout << "\tFileName = " << emitter.envmap.filename.svalue << std::endl; + // LOAD file relative to the XML + } + break; + case ext::MitsubaLoader::CElementEmitter::Type::INVALID: + break; + default: + #ifdef _DEBUG + assert(false); + #endif + // let's implement a new emitter type! + //weight = emitter.unionType.samplingWeight; + break; + } + if (weight==0.f) + continue; - //weight *= light.computeFlux(NAN); - if (weight <= FLT_MIN) - continue; + //weight *= light.computeFlux(NAN); + if (weight <= FLT_MIN) + continue; - //initData.lightPDF.push_back(weight); - //initData.lights.push_back(light); - } - reinterpret_cast(params.front().w) = params.size()/3; - paramsBuffer = m_driver->createFilledDeviceLocalGPUBufferOnDedMem(sizeof(core::vectorSIMDf)*params.size(),params.data()); + //initData.lightPDF.push_back(weight); + //initData.lights.push_back(light); } - const auto& envMapCPUImages = m_globalMeta->m_global.m_envMapImages; - - // don't touch this, 4x2 envmap is absolute minimum to have everything working - uint32_t newWidth = 4; - // create image - { - const auto colorFormat = asset::EF_E5B9G9R9_UFLOAT_PACK32; - for (const auto& envmapCpuImage : envMapCPUImages) - { - const auto& extent = envmapCpuImage->getCreationParameters().extent; - // I could upscale 2x2 if detecting a rotation, but should have used Cubemaps or Octahedral mapping instead - newWidth = core::max(core::max(extent.width,extent.height<<1u),newWidth); - } - constexpr uint32_t kMaxEnvMapSize = 16u << 10u; - newWidth = core::roundUpToPoT(core::min(newWidth,kMaxEnvMapSize)); + // Initialize Pipeline and Resources for EnvMap Blending + auto fullScreenTriangle = ext::FullScreenTriangle::createFullScreenTriangle(m_assetManager, m_driver); - // full mipchain would be `MSB+1` but we want it to stop at 4x2 - const auto mipLevels = core::findMSB(newWidth)-1; + IGPUDescriptorSetLayout::SBinding binding{ 0u, EDT_COMBINED_IMAGE_SAMPLER, 1u, IGPUSpecializedShader::ESS_FRAGMENT, nullptr }; + auto blendEnvDescriptorSetLayout = m_driver->createDescriptorSetLayout(&binding, &binding + 1u); + + IAssetLoader::SAssetLoadParams lp; + auto fs_bundle = m_assetManager->getAsset("nbl/builtin/material/lambertian/singletexture/specialized_shader.frag",lp); + auto fs_contents = fs_bundle.getContents(); + assert(!fs_contents.empty()); + + ICPUSpecializedShader* fs = static_cast(fs_contents.begin()->get()); + + auto fragShader = m_driver->getGPUObjectsFromAssets(&fs, &fs + 1)->front(); + if (!fragShader) + std::cout << "[ERROR] Couldn't get fragShader." << std::endl; + + IGPUSpecializedShader* shaders[2] = { std::get<0>(fullScreenTriangle).get(), fragShader.get() }; + SBlendParams blendParams = {}; + blendParams.logicOpEnable = false; + blendParams.logicOp = ELO_NO_OP; + blendParams.blendParams[0].blendEnable = true; + blendParams.blendParams[0].srcColorFactor = asset::EBF_ONE; + blendParams.blendParams[0].dstColorFactor = asset::EBF_ONE; + blendParams.blendParams[0].colorBlendOp = asset::EBO_ADD; + blendParams.blendParams[0].srcAlphaFactor = asset::EBF_ONE; + blendParams.blendParams[0].dstAlphaFactor = asset::EBF_ONE; + blendParams.blendParams[0].alphaBlendOp = asset::EBO_ADD; + blendParams.blendParams[0].colorWriteMask = (1u << 0u) | (1u << 1u) | (1u << 2u) | (1u << 3u); + + SRasterizationParams rasterParams = {}; + rasterParams.faceCullingMode = EFCM_NONE; + rasterParams.depthCompareOp = ECO_ALWAYS; + rasterParams.minSampleShading = 1.f; + rasterParams.depthWriteEnable = false; + rasterParams.depthTestEnable = false; + + auto gpuPipelineLayout = m_driver->createPipelineLayout(nullptr, nullptr, nullptr, nullptr, nullptr, core::smart_refctd_ptr(blendEnvDescriptorSetLayout)); + + blendEnvPipeline = m_driver->createGPURenderpassIndependentPipeline(nullptr, std::move(gpuPipelineLayout), shaders, shaders + 2, + std::get(fullScreenTriangle), blendParams, + std::get(fullScreenTriangle), rasterParams); + + SBufferBinding idxBinding{ 0ull, nullptr }; + blendEnvMeshBuffer = core::make_smart_refctd_ptr(nullptr, nullptr, nullptr, std::move(idxBinding)); + blendEnvMeshBuffer->setIndexCount(3u); + blendEnvMeshBuffer->setInstanceCount(1u); + + video::IFrameBuffer* finalEnvFramebuffer = nullptr; + { + const auto colorFormat = asset::EF_R16G16B16A16_SFLOAT; + const auto mipCount = 13u; + const auto resolution = 0x1u<<(mipCount-1u); IGPUImage::SCreationParams imgInfo; imgInfo.format = colorFormat; imgInfo.type = IGPUImage::ET_2D; - imgInfo.extent.width = newWidth; - imgInfo.extent.height = newWidth>>1u; + imgInfo.extent.width = resolution; + imgInfo.extent.height = resolution/2; imgInfo.extent.depth = 1u; - - imgInfo.mipLevels = mipLevels; + imgInfo.mipLevels = mipCount; imgInfo.arrayLayers = 1u; imgInfo.samples = asset::ICPUImage::ESCF_1_BIT; imgInfo.flags = static_cast(0u); - auto image = m_driver->createGPUImageOnDedMem(std::move(imgInfo),m_driver->getDeviceLocalGPUMemoryReqs()); + auto image = m_driver->createGPUImageOnDedMem(std::move(imgInfo), m_driver->getDeviceLocalGPUMemoryReqs()); IGPUImageView::SCreationParams imgViewInfo; imgViewInfo.image = std::move(image); @@ -727,118 +728,70 @@ void Renderer::initSceneNonAreaLights(Renderer::InitializationData& initData) imgViewInfo.subresourceRange.baseArrayLayer = 0u; imgViewInfo.subresourceRange.baseMipLevel = 0u; imgViewInfo.subresourceRange.layerCount = 1u; - imgViewInfo.subresourceRange.levelCount = mipLevels; + imgViewInfo.subresourceRange.levelCount = mipCount; - m_finalEnvmap = m_driver->createGPUImageView(std::move(imgViewInfo)); + m_finalEnvmap = m_driver->createImageView(std::move(imgViewInfo)); + + finalEnvFramebuffer = m_driver->addFrameBuffer(); + finalEnvFramebuffer->attach(video::EFAP_COLOR_ATTACHMENT0, std::move(m_finalEnvmap)); } + auto blendToFinalEnvMap = [&](nbl::core::smart_refctd_ptr gpuImageView) -> void { - // we don't have DLT on this branch - auto sampler = m_driver->createGPUSampler(IGPUSampler::SParams{ - IGPUSampler::ETC_REPEAT, - IGPUSampler::ETC_REPEAT, - IGPUSampler::ETC_REPEAT, - IGPUSampler::ETBC_FLOAT_OPAQUE_BLACK, - IGPUSampler::ETF_LINEAR, - IGPUSampler::ETF_LINEAR, - IGPUSampler::ESMM_LINEAR, - 5, - false, - IGPUSampler::ECO_ALWAYS - }); - core::vector> retainedViews; - - constexpr auto BindingCount = 3; - // Initialize Pipeline and Resources for EnvMap Blending - core::smart_refctd_ptr descriptorSet; - core::smart_refctd_ptr pipeline; - // - { - core::smart_refctd_ptr samplers[MAX_SAMPLERS_COMPUTE]; - std::fill_n(samplers,MAX_SAMPLERS_COMPUTE,sampler); - const IGPUDescriptorSetLayout::SBinding bindings[BindingCount] = { - {0,EDT_STORAGE_BUFFER,1,ISpecializedShader::ESS_COMPUTE,nullptr}, - {1,EDT_STORAGE_IMAGE,1,ISpecializedShader::ESS_COMPUTE,nullptr}, - {2,EDT_COMBINED_IMAGE_SAMPLER,MAX_SAMPLERS_COMPUTE,ISpecializedShader::ESS_COMPUTE,samplers} - }; - auto descriptorSetLayout = m_driver->createGPUDescriptorSetLayout(bindings,bindings+BindingCount); - - pipeline = m_driver->createGPUComputePipeline(nullptr, - m_driver->createGPUPipelineLayout(nullptr,nullptr,core::smart_refctd_ptr(descriptorSetLayout)), - gpuSpecializedShaderFromFile(m_assetManager,m_driver,"../addEnvironmentEmitters.comp") - ); - - descriptorSet = m_driver->createGPUDescriptorSet(std::move(descriptorSetLayout)); - } - // + auto blendEnvDescriptorSet = m_driver->createDescriptorSet(std::move(blendEnvDescriptorSetLayout)); + + IGPUDescriptorSet::SDescriptorInfo info; { - std::unique_ptr infos(new IGPUDescriptorSet::SDescriptorInfo[envMapCPUImages.size()+BindingCount-1u]); - - IGPUDescriptorSet::SWriteDescriptorSet writes[BindingCount]; - for (auto i=0; igetSize()}; - infos[1].desc = m_driver->createGPUImageView({ - static_cast(0u), - m_finalEnvmap->getCreationParameters().image, - IGPUImageView::ET_2D, - asset::EF_R32_UINT, // need to view the RGB9E5 as R32UI to write to it - {},{IGPUImage::EAF_COLOR_BIT,0u,1u,0u,1u} - }); - infos[1].image = {nullptr,EIL_GENERAL}; - auto pInfoEnvmap = writes[2].info; - auto envMapGPUImages = m_driver->getGPUObjectsFromAssets(envMapCPUImages.data(),envMapCPUImages.data()+envMapCPUImages.size()); - for(auto& image : *envMapGPUImages) - { - pInfoEnvmap->desc = retainedViews.emplace_back(m_driver->createGPUImageView({ - static_cast(0u), - core::smart_refctd_ptr(image), - IGPUImageView::ET_2D, - image->getCreationParameters().format, - {},{IGPUImage::EAF_COLOR_BIT,0u,1u,0u,1u} - })); - pInfoEnvmap->image = {nullptr,asset::EIL_SHADER_READ_ONLY_OPTIMAL}; - pInfoEnvmap++; - } - - auto bindingCount = BindingCount; - if (envMapCPUImages.empty()) - bindingCount--; - m_driver->updateDescriptorSets(bindingCount,writes,0u,nullptr); + info.desc = gpuImageView; + ISampler::SParams samplerParams = { ISampler::ETC_CLAMP_TO_EDGE, ISampler::ETC_CLAMP_TO_EDGE, ISampler::ETC_CLAMP_TO_EDGE, ISampler::ETBC_FLOAT_OPAQUE_BLACK, ISampler::ETF_LINEAR, ISampler::ETF_LINEAR, ISampler::ESMM_LINEAR, 0u, false, ECO_ALWAYS }; + info.image.sampler = m_driver->createSampler(samplerParams); + info.image.imageLayout = EIL_SHADER_READ_ONLY_OPTIMAL; } - m_driver->bindComputePipeline(pipeline.get()); - m_driver->bindDescriptorSets(EPBP_COMPUTE,pipeline->getLayout(),0u,1u,&descriptorSet.get(),nullptr); - const auto xGroups = (newWidth-1u)/WORKGROUP_DIM+1u; - m_driver->dispatch(xGroups,core::max(xGroups>>1u,1u),1u); - - // always needs doing after rendering - // TODO: better filter and GPU accelerated - COpenGLExtensionHandler::extGlMemoryBarrier(GL_TEXTURE_FETCH_BARRIER_BIT|GL_SHADER_IMAGE_ACCESS_BARRIER_BIT); + IGPUDescriptorSet::SWriteDescriptorSet write; + write.dstSet = blendEnvDescriptorSet.get(); + write.binding = 0u; + write.arrayElement = 0u; + write.count = 1u; + write.descriptorType = EDT_COMBINED_IMAGE_SAMPLER; + write.info = &info; + + m_driver->updateDescriptorSets(1u, &write, 0u, nullptr); + m_driver->bindGraphicsPipeline(blendEnvPipeline.get()); + m_driver->bindDescriptorSets(EPBP_GRAPHICS, blendEnvPipeline->getLayout(), 3u, 1u, &blendEnvDescriptorSet.get(), nullptr); + m_driver->drawMeshBuffer(blendEnvMeshBuffer.get()); + }; + + m_driver->setRenderTarget(finalEnvFramebuffer, true); + float colorClearValues[] = { _envmapBaseColor.x, _envmapBaseColor.y, _envmapBaseColor.z, _envmapBaseColor.w }; + m_driver->clearColorBuffer(video::EFAP_COLOR_ATTACHMENT0, colorClearValues); + for(uint32_t i = 0u; i < m_globalMeta->m_global.m_envMapImages.size(); ++i) + { + auto envmapCpuImage = m_globalMeta->m_global.m_envMapImages[i]; + ICPUImageView::SCreationParams viewParams; + viewParams.flags = static_cast(0u); + viewParams.image = envmapCpuImage; + viewParams.format = viewParams.image->getCreationParameters().format; + viewParams.viewType = IImageView::ET_2D; + viewParams.subresourceRange.baseArrayLayer = 0u; + viewParams.subresourceRange.layerCount = 1u; + viewParams.subresourceRange.baseMipLevel = 0u; + viewParams.subresourceRange.levelCount = 1u; + auto cpuEnvmapImageView = ICPUImageView::create(std::move(viewParams)); + auto envMapImageView = m_driver->getGPUObjectsFromAssets(&cpuEnvmapImageView.get(), &cpuEnvmapImageView.get() + 1u)->front(); + blendToFinalEnvMap(envMapImageView); } - + m_driver->setRenderTarget(nullptr, true); + // always needs doing after rendering + // TODO: better filter and GPU accelerated m_finalEnvmap->regenerateMipMapLevels(); - m_envMapImportanceSampling.initResources(m_finalEnvmap); } void Renderer::finalizeScene(Renderer::InitializationData& initData) { if (initData.lights.empty()) return; -// TODO: later -// m_staticViewData.lightCount = initData.lights.size(); + m_staticViewData.lightCount = initData.lights.size(); const double weightSum = std::accumulate(initData.lightPDF.begin(),initData.lightPDF.end(),0.0); assert(weightSum>FLT_MIN); @@ -872,16 +825,16 @@ void Renderer::finalizeScene(Renderer::InitializationData& initData) } } -core::smart_refctd_ptr Renderer::createTexture(uint32_t width, uint32_t height, E_FORMAT format, uint32_t mipLevels, uint32_t layers) +core::smart_refctd_ptr Renderer::createScreenSizedTexture(E_FORMAT format, uint32_t layers) { const auto real_layers = layers ? layers:1u; IGPUImage::SCreationParams imgparams; - imgparams.extent = {width, height, 1u}; + imgparams.extent = {m_staticViewData.imageDimensions.x,m_staticViewData.imageDimensions.y,1u}; imgparams.arrayLayers = real_layers; imgparams.flags = static_cast(0); imgparams.format = format; - imgparams.mipLevels = mipLevels; + imgparams.mipLevels = 1u; imgparams.samples = IImage::ESCF_1_BIT; imgparams.type = IImage::ET_2D; @@ -894,15 +847,11 @@ core::smart_refctd_ptr Renderer::createTexture(uint32_t width, ui viewparams.subresourceRange.baseArrayLayer = 0u; viewparams.subresourceRange.layerCount = real_layers; viewparams.subresourceRange.baseMipLevel = 0u; - viewparams.subresourceRange.levelCount = mipLevels; + viewparams.subresourceRange.levelCount = 1u; - return m_driver->createGPUImageView(std::move(viewparams)); + return m_driver->createImageView(std::move(viewparams)); } -core::smart_refctd_ptr Renderer::createScreenSizedTexture(E_FORMAT format, uint32_t layers) -{ - return createTexture(m_staticViewData.imageDimensions[0], m_staticViewData.imageDimensions[1], format, 1u, layers); -} core::smart_refctd_ptr Renderer::SampleSequence::createCPUBuffer(uint32_t quantizedDimensions, uint32_t sampleCount) { @@ -914,8 +863,8 @@ core::smart_refctd_ptr Renderer::SampleSequence::createCPUBuf } void Renderer::SampleSequence::createBufferView(IVideoDriver* driver, core::smart_refctd_ptr&& buff) { - auto gpubuf = driver->createFilledDeviceLocalGPUBufferOnDedMem(buff->getSize(),buff->getPointer()); - bufferView = driver->createGPUBufferView(gpubuf.get(),asset::EF_R32G32_UINT); + auto gpubuf = driver->createFilledDeviceLocalBufferOnDedMem(buff->getSize(),buff->getPointer()); + bufferView = driver->createBufferView(gpubuf.get(),asset::EF_R32G32_UINT); } core::smart_refctd_ptr Renderer::SampleSequence::createBufferView(IVideoDriver* driver, uint32_t quantizedDimensions, uint32_t sampleCount) { @@ -926,7 +875,7 @@ core::smart_refctd_ptr Renderer::SampleSequence::createBufferView(IV // Memory Order: 3 Dimensions, then multiple of sampling stragies per vertex, then depth, then sample ID auto buff = createCPUBuffer(quantizedDimensions,sampleCount); uint32_t(&pout)[][2] = *reinterpret_cast(buff->getPointer()); - // the horrible locality of iteration over output memory is caused by the fact that certain samplers like the + // the horrible order of iteration over output memory is caused by the fact that certain samplers like the // Owen Scramble sampler, have a large cache which needs to be generated separately for each dimension. for (auto metadim=0u; metadim Renderer::SampleSequence::createBufferView(IV return buff; } +// + // TODO: be able to fail void Renderer::initSceneResources(SAssetBundle& meshes, nbl::io::path&& _sampleSequenceCachePath) { @@ -975,13 +926,13 @@ void Renderer::initSceneResources(SAssetBundle& meshes, nbl::io::path&& _sampleS // cull { SPushConstantRange range{ISpecializedShader::ESS_COMPUTE,0u,sizeof(CullShaderData_t)}; - m_cullPipelineLayout = m_driver->createGPUPipelineLayout(&range,&range+1u,core::smart_refctd_ptr(globalBackendDataDSLayout),core::smart_refctd_ptr(m_cullDSLayout),nullptr,nullptr); + m_cullPipelineLayout = m_driver->createPipelineLayout(&range,&range+1u,core::smart_refctd_ptr(globalBackendDataDSLayout),core::smart_refctd_ptr(m_cullDSLayout),nullptr,nullptr); } SPushConstantRange raytracingCommonPCRange{ISpecializedShader::ESS_COMPUTE,0u,sizeof(RaytraceShaderCommonData_t)}; // raygen { - m_raygenPipelineLayout = m_driver->createGPUPipelineLayout( + m_raygenPipelineLayout = m_driver->createPipelineLayout( &raytracingCommonPCRange,&raytracingCommonPCRange+1u, core::smart_refctd_ptr(globalBackendDataDSLayout), core::smart_refctd_ptr(m_additionalGlobalDSLayout), @@ -989,12 +940,12 @@ void Renderer::initSceneResources(SAssetBundle& meshes, nbl::io::path&& _sampleS core::smart_refctd_ptr(m_raygenDSLayout) ); - m_raygenDS = m_driver->createGPUDescriptorSet(core::smart_refctd_ptr(m_raygenDSLayout)); + m_raygenDS = m_driver->createDescriptorSet(core::smart_refctd_ptr(m_raygenDSLayout)); } // closest hit { - m_closestHitPipelineLayout = m_driver->createGPUPipelineLayout( + m_closestHitPipelineLayout = m_driver->createPipelineLayout( &raytracingCommonPCRange,&raytracingCommonPCRange+1u, core::smart_refctd_ptr(globalBackendDataDSLayout), core::smart_refctd_ptr(m_additionalGlobalDSLayout), @@ -1005,9 +956,8 @@ void Renderer::initSceneResources(SAssetBundle& meshes, nbl::io::path&& _sampleS // resolve { - SPushConstantRange range{ISpecializedShader::ESS_COMPUTE,0u,sizeof(core::matrix3x4SIMD)+sizeof(nbl_glsl_RWMC_ReweightingParameters)}; - m_resolvePipelineLayout = m_driver->createGPUPipelineLayout(&range,&range+1,core::smart_refctd_ptr(m_resolveDSLayout)); - m_resolveDS = m_driver->createGPUDescriptorSet(core::smart_refctd_ptr(m_resolveDSLayout)); + m_resolvePipelineLayout = m_driver->createPipelineLayout(nullptr,nullptr,core::smart_refctd_ptr(m_resolveDSLayout)); + m_resolveDS = m_driver->createDescriptorSet(core::smart_refctd_ptr(m_resolveDSLayout)); } // @@ -1019,7 +969,7 @@ void Renderer::initSceneResources(SAssetBundle& meshes, nbl::io::path&& _sampleS }; auto createFilledBufferAndSetUpInfo = [&](IGPUDescriptorSet::SDescriptorInfo* info, size_t size, const void* data) { - auto buf = m_driver->createFilledDeviceLocalGPUBufferOnDedMem(size,data); + auto buf = m_driver->createFilledDeviceLocalBufferOnDedMem(size,data); setBufferInfo(info,core::smart_refctd_ptr(buf)); return buf; }; @@ -1093,22 +1043,22 @@ void Renderer::initSceneResources(SAssetBundle& meshes, nbl::io::path&& _sampleS } // lets keep path length within bounds of sanity constexpr auto MaxPathDepth = 255u; - if (maxPathDepth==0) + if (pathDepth==0) { printf("[ERROR] No suppoerted Integrator found in the Mitsuba XML, setting default.\n"); - maxPathDepth = DefaultPathDepth; + pathDepth = DefaultPathDepth; } - else if (maxPathDepth>MaxPathDepth) + else if (pathDepth>MaxPathDepth) { - printf("[WARNING] Path Depth %d greater than maximum supported, clamping to %d\n",maxPathDepth,MaxPathDepth); - maxPathDepth = MaxPathDepth; + printf("[WARNING] Path Depth %d greater than maximum supported, clamping to %d\n",pathDepth,MaxPathDepth); + pathDepth = MaxPathDepth; } - const uint32_t quantizedDimensions = SampleSequence::computeQuantizedDimensions(maxPathDepth); + const uint32_t quantizedDimensions = SampleSequence::computeQuantizedDimensions(pathDepth); // The primary limiting factor is the precision of turning a fixed point grid sample to IEEE754 32bit float in the [0,1] range. // Mantissa is only 23 bits, and primary sample space low discrepancy sequence will start to produce duplicates // near 1.0 with exponent -1 after the sample count passes 2^24 elements. // Another limiting factor is our encoding of sample sequences, we only use 21bits per channel, so no duplicates till 2^21 samples. - maxSensorSamples = core::min(0x1u<<21u,maxSensorSamples); + maxSensorSamples = core::min(0x1<<21,maxSensorSamples); if (cachedQuantizedDimensions>=quantizedDimensions && cachedSampleCount>=maxSensorSamples) sampleSequence.createBufferView(m_driver,std::move(cachebuff)); else @@ -1124,9 +1074,8 @@ void Renderer::initSceneResources(SAssetBundle& meshes, nbl::io::path&& _sampleS cacheFile->drop(); } } - std::cout << "\tmaxPathDepth = " << maxPathDepth << std::endl; + std::cout << "\tpathDepth = " << pathDepth << std::endl; std::cout << "\tnoRussianRouletteDepth = " << noRussianRouletteDepth << std::endl; - std::cout << "\thideEnvironment = " << hideEnvironment << std::endl; std::cout << "\tmaxSamples = " << maxSensorSamples << std::endl; } } @@ -1160,13 +1109,11 @@ void Renderer::deinitSceneResources() m_indirectDrawBuffers[1] = m_indirectDrawBuffers[0] = nullptr; m_indexBuffer = nullptr; - m_raytraceCommonData = { 0.f,0u,0u,0u,core::matrix3x4SIMD() }; + m_raytraceCommonData = {core::matrix4SIMD(),vec3(),0.f,0,0,0,0.f}; m_sceneBound = core::aabbox3df(FLT_MAX, FLT_MAX, FLT_MAX, -FLT_MAX, -FLT_MAX, -FLT_MAX); - m_maxAreaLightLuma = 0.f; m_finalEnvmap = nullptr; - m_envMapImportanceSampling.deinitResources(); - m_staticViewData = {{0u,0u},0u,0u,0u,0u,false,core::infinity(),{}}; + m_staticViewData = {{0u,0u},0u,0u}; auto rr = m_rrManager->getRadeonRaysAPI(); rr->DetachAll(); @@ -1180,97 +1127,32 @@ void Renderer::deinitSceneResources() rr->DeleteShape(shape); rrShapes.clear(); - maxPathDepth = DefaultPathDepth; + pathDepth = DefaultPathDepth; noRussianRouletteDepth = 5u; - hideEnvironment = false; maxSensorSamples = MaxFreeviewSamples; } -void Renderer::deinitRenderer() +void Renderer::initScreenSizedResources(uint32_t width, uint32_t height) { - m_driver = nullptr; - m_smgr = nullptr; - m_assetManager = nullptr; - m_rrManager = nullptr; -} - -void Renderer::finalizeDeferredDenoise() -{ - if (!m_deferDenoise) - return; - - m_deferDenoiseFile.close(); - std::cout << "\n---[DENOISER_BEGIN]---" << std::endl; - std::system(DEFER_DENOISE_HOOK_FILE_NAME); - std::cout << "\n---[DENOISER_END]---" << std::endl; - std::remove(DEFER_DENOISE_HOOK_FILE_NAME); -} - -void Renderer::initScreenSizedResources( - const uint32_t width, const uint32_t height, - const float envMapRegularizationFactor, - int32_t cascadeCount, - float cascadeLuminanceBase, - float cascadeLuminanceStart, - const float Emin, - const nbl::core::vector& clipPlanes -) -{ - float maxEmitterRadianceLuma; - bool enableRIS = m_envMapImportanceSampling.computeWarpMap(envMapRegularizationFactor,m_staticViewData.envMapPDFNormalizationFactor,maxEmitterRadianceLuma); - if (maxEmitterRadianceLumastd::numeric_limits::min(); - if (core::isnan(cascadeLuminanceStart)) - cascadeLuminanceStart = baseIsKnown ? (maxEmitterRadianceLuma*std::pow(cascadeLuminanceBase,-cascadeSegmentCount)):Emin; - // rationale, we don't have NEE and BRDF importance sampling samples with throughput <= 1.0 - // However we have RIS, and that can complicate this assumption a bit - if (!baseIsKnown) - cascadeLuminanceBase = core::max(std::pow(maxEmitterRadianceLuma/cascadeLuminanceStart,1.f/cascadeSegmentCount),1.0625f); - std::cout << "Re-Weighting Monte Carlo = ENABLED [cascadeCount: "< void { - m_staticViewData.samplesPerPixelPerDispatch = sampleMultiplier; + m_staticViewData.samplesPerPixelPerDispatch = SAMPLING_STRATEGY_COUNT*sampleMultiplier; const size_t minimumSampleCountPerDispatch = static_cast(renderPixelCount)*getSamplesPerPixelPerDispatch(); _maxRaysPerDispatch = static_cast(minimumSampleCountPerDispatch); @@ -1290,39 +1172,21 @@ void Renderer::initScreenSizedResources( printf("[INFO] Using %d samples (per pixel) per dispatch\n",getSamplesPerPixelPerDispatch()); } } - m_staticViewData.sampleSequenceStride = SampleSequence::computeQuantizedDimensions(maxPathDepth); - auto stream = std::ofstream("runtime_defines.glsl"); - - for (auto i=0; im_global.getVTStorageViewCount() << "\n" + + (std::ofstream("runtime_defines.glsl") + << "#define _NBL_EXT_MITSUBA_LOADER_VT_STORAGE_VIEW_COUNT " << m_globalMeta->m_global.getVTStorageViewCount() << "\n" << m_globalMeta->m_global.m_materialCompilerGLSL_declarations + << "#define SAMPLE_SEQUENCE_STRIDE " << SampleSequence::computeQuantizedDimensions(pathDepth) << "\n" << "#ifndef MAX_RAYS_GENERATED\n" << "# define MAX_RAYS_GENERATED " << getSamplesPerPixelPerDispatch() << "\n" - << "#endif\n"; - - if(!enableRIS) - stream << "#define ONLY_BXDF_SAMPLING\n"; - - stream.close(); + << "#endif\n" + ).close(); compileShadersFuture = std::async(std::launch::async, [&]() { // cull m_cullGPUShader = gpuSpecializedShaderFromFile(m_assetManager,m_driver,"../cull.comp"); - // visbuffer - m_vertGPUShader = gpuSpecializedShaderFromFile(m_assetManager, m_driver, "../fillVisBuffer.vert"); - // raygen m_raygenGPUShader = gpuSpecializedShaderFromFile(m_assetManager,m_driver,"../raygen.comp"); @@ -1345,7 +1209,7 @@ void Renderer::initScreenSizedResources( auto createFilledBufferAndSetUpInfo = [&](IGPUDescriptorSet::SDescriptorInfo* info, size_t size, const void* data) { - auto buf = m_driver->createFilledDeviceLocalGPUBufferOnDedMem(size,data); + auto buf = m_driver->createFilledDeviceLocalBufferOnDedMem(size,data); setBufferInfo(info,core::smart_refctd_ptr(buf)); return buf; }; @@ -1368,7 +1232,7 @@ void Renderer::initScreenSizedResources( if (static_cast(m_driver)->runningInRenderdoc()) // makes Renderdoc capture the modifications done by OpenCL { interopBuffer.buffer = m_driver->createUpStreamingGPUBufferOnDedMem(size); -// interopBuffer.buffer->getBoundMemory()->mapMemoryRange(IDriverMemoryAllocation::EMCAF_READ_AND_WRITE,{0u,size}); + //interopBuffer.buffer->getBoundMemory()->mapMemoryRange(IDeviceMemoryAllocation::EMCAF_WRITE,{0u,size}) } else interopBuffer.buffer = m_driver->createDeviceLocalGPUBufferOnDedMem(size); @@ -1393,21 +1257,17 @@ void Renderer::initScreenSizedResources( }; // create out screen-sized textures - m_accumulation = createScreenSizedTexture(EF_R32G32_UINT,(cascadeCount+1u)*m_staticViewData.samplesPerPixelPerDispatch); // one more (first) layer because of accumulation metadata for a path + m_accumulation = createScreenSizedTexture(EF_R32G32_UINT,m_staticViewData.samplesPerPixelPerDispatch); m_albedoAcc = createScreenSizedTexture(EF_R32_UINT,m_staticViewData.samplesPerPixelPerDispatch); m_normalAcc = createScreenSizedTexture(EF_R32_UINT,m_staticViewData.samplesPerPixelPerDispatch); - m_maskAcc = createScreenSizedTexture(EF_R16_UNORM,m_staticViewData.samplesPerPixelPerDispatch); m_tonemapOutput = createScreenSizedTexture(EF_R16G16B16A16_SFLOAT); m_albedoRslv = createScreenSizedTexture(EF_A2B10G10R10_UNORM_PACK32); m_normalRslv = createScreenSizedTexture(EF_R16G16B16A16_SFLOAT); - constexpr uint32_t MaxDescritorUpdates = 11u; + constexpr uint32_t MaxDescritorUpdates = 8u; IGPUDescriptorSet::SDescriptorInfo infos[MaxDescritorUpdates]; IGPUDescriptorSet::SWriteDescriptorSet writes[MaxDescritorUpdates]; - - auto warpMap = m_envMapImportanceSampling.getWarpMapImageView(); - auto lumaMap = m_envMapImportanceSampling.getLuminanceImageView(); - + // set up m_commonRaytracingDS core::smart_refctd_ptr _staticViewDataBuffer; size_t staticViewDataBufferSize=0u; @@ -1418,41 +1278,20 @@ void Renderer::initScreenSizedResources( setImageInfo(infos+2,asset::EIL_GENERAL,core::smart_refctd_ptr(m_accumulation)); setImageInfo(infos+5,asset::EIL_GENERAL,core::smart_refctd_ptr(m_albedoAcc)); setImageInfo(infos+6,asset::EIL_GENERAL,core::smart_refctd_ptr(m_normalAcc)); - setImageInfo(infos+7,asset::EIL_GENERAL,core::smart_refctd_ptr(m_maskAcc)); // envmap - { - setImageInfo(infos+8,asset::EIL_GENERAL,core::smart_refctd_ptr(m_finalEnvmap)); - ISampler::SParams samplerParams = { ISampler::ETC_REPEAT, ISampler::ETC_CLAMP_TO_EDGE, ISampler::ETC_CLAMP_TO_EDGE, ISampler::ETBC_FLOAT_OPAQUE_BLACK, ISampler::ETF_LINEAR, ISampler::ETF_LINEAR, ISampler::ESMM_LINEAR, 0u, false, ECO_ALWAYS }; - infos[8].image.sampler = m_driver->createGPUSampler(samplerParams); - infos[8].image.imageLayout = EIL_SHADER_READ_ONLY_OPTIMAL; - } - // warpmap - { - setImageInfo(infos+9,asset::EIL_GENERAL,core::smart_refctd_ptr(warpMap)); - ISampler::SParams samplerParams = { ISampler::ETC_REPEAT, ISampler::ETC_CLAMP_TO_EDGE, ISampler::ETC_CLAMP_TO_EDGE, ISampler::ETBC_FLOAT_OPAQUE_BLACK, ISampler::ETF_LINEAR, ISampler::ETF_LINEAR, ISampler::ESMM_LINEAR, 0u, false, ECO_ALWAYS }; - infos[9].image.sampler = m_driver->createGPUSampler(samplerParams); - infos[9].image.imageLayout = EIL_SHADER_READ_ONLY_OPTIMAL; - } - - IGPUDescriptorSet::SDescriptorInfo luminanceDescriptorInfo = {}; - // luminance mip maps - { - ISampler::SParams samplerParams = { ISampler::ETC_CLAMP_TO_BORDER, ISampler::ETC_CLAMP_TO_EDGE, ISampler::ETC_CLAMP_TO_EDGE, ISampler::ETBC_FLOAT_OPAQUE_BLACK, ISampler::ETF_NEAREST, ISampler::ETF_NEAREST, ISampler::ETF_NEAREST, 0u, false, ECO_ALWAYS }; - auto sampler = m_driver->createGPUSampler(samplerParams); - - luminanceDescriptorInfo.desc = lumaMap; - luminanceDescriptorInfo.image.sampler = sampler; - luminanceDescriptorInfo.image.imageLayout = asset::EIL_SHADER_READ_ONLY_OPTIMAL; - } + setImageInfo(infos+7,asset::EIL_GENERAL,core::smart_refctd_ptr(m_finalEnvmap)); + ISampler::SParams samplerParams = { ISampler::ETC_REPEAT, ISampler::ETC_CLAMP_TO_EDGE, ISampler::ETC_CLAMP_TO_EDGE, ISampler::ETBC_FLOAT_OPAQUE_BLACK, ISampler::ETF_LINEAR, ISampler::ETF_LINEAR, ISampler::ESMM_LINEAR, 0u, false, ECO_ALWAYS }; + infos[7].image.sampler = m_driver->createSampler(samplerParams); + infos[7].image.imageLayout = EIL_SHADER_READ_ONLY_OPTIMAL; createEmptyInteropBufferAndSetUpInfo(infos+3,m_rayBuffer[0],raygenBufferSize); setBufferInfo(infos+4,m_rayCountBuffer); for (auto i=0u; i<2u; i++) - m_commonRaytracingDS[i] = m_driver->createGPUDescriptorSet(core::smart_refctd_ptr(m_commonRaytracingDSLayout)); + m_commonRaytracingDS[i] = m_driver->createDescriptorSet(core::smart_refctd_ptr(m_commonRaytracingDSLayout)); - constexpr auto descriptorUpdateCount = 11u; + constexpr auto descriptorUpdateCount = 8u; setDstSetAndDescTypesOnWrites(m_commonRaytracingDS[0].get(),writes,infos,{ EDT_UNIFORM_BUFFER, EDT_UNIFORM_TEXEL_BUFFER, @@ -1461,19 +1300,8 @@ void Renderer::initScreenSizedResources( EDT_STORAGE_BUFFER, EDT_STORAGE_IMAGE, EDT_STORAGE_IMAGE, - EDT_STORAGE_IMAGE, - EDT_COMBINED_IMAGE_SAMPLER, EDT_COMBINED_IMAGE_SAMPLER, }); - - // Set last write - writes[10].binding = 10u; - writes[10].arrayElement = 0u; - writes[10].count = 1u; - writes[10].descriptorType = EDT_COMBINED_IMAGE_SAMPLER; - writes[10].dstSet = m_commonRaytracingDS[0].get(); - writes[10].info = &luminanceDescriptorInfo; - m_driver->updateDescriptorSets(descriptorUpdateCount,writes,0u,nullptr); // set up second DS createEmptyInteropBufferAndSetUpInfo(infos+3,m_rayBuffer[1],raygenBufferSize); @@ -1483,6 +1311,8 @@ void Renderer::initScreenSizedResources( } // set up m_raygenDS + core::smart_refctd_ptr visibilityBuffer = createScreenSizedTexture(EF_R32G32B32A32_UINT); + { { constexpr auto ScrambleStateChannels = 2u; auto tmpBuff = m_driver->createCPUSideGPUVisibleGPUBufferOnDedMem(sizeof(uint32_t)*ScrambleStateChannels*renderPixelCount); @@ -1490,8 +1320,8 @@ void Renderer::initScreenSizedResources( { core::RandomSampler rng(0xbadc0ffeu); auto it = reinterpret_cast(tmpBuff->getBoundMemory()->mapMemoryRange( - IDriverMemoryAllocation::EMCAF_WRITE, - IDriverMemoryAllocation::MemoryRange(0u,tmpBuff->getSize()) + IDeviceMemoryAllocation::EMCAF_WRITE, + IDeviceMemoryAllocation::MemoryRange(0u,tmpBuff->getSize()) )); for (auto end=it+ScrambleStateChannels*renderPixelCount; it!=end; it++) *it = rng.nextSample(); @@ -1503,11 +1333,12 @@ void Renderer::initScreenSizedResources( //region.imageSubresource.aspectMask = ; region.imageSubresource.baseArrayLayer = 0u; region.imageSubresource.layerCount = 1u; - region.imageExtent = {m_staticViewData.imageDimensions[0],m_staticViewData.imageDimensions[1],1u}; + region.imageExtent = {m_staticViewData.imageDimensions.x,m_staticViewData.imageDimensions.y,0u}; auto scrambleKeys = createScreenSizedTexture(EF_R32G32_UINT); m_driver->copyBufferToImage(tmpBuff.get(),scrambleKeys->getCreationParameters().image.get(),1u,®ion); setImageInfo(infos+0,asset::EIL_SHADER_READ_ONLY_OPTIMAL,std::move(scrambleKeys)); } + setImageInfo(infos+1,asset::EIL_SHADER_READ_ONLY_OPTIMAL,core::smart_refctd_ptr(visibilityBuffer)); setImageInfo(infos+2,asset::EIL_GENERAL,core::smart_refctd_ptr(m_tonemapOutput)); setDstSetAndDescTypesOnWrites(m_raygenDS.get(),writes,infos,{ @@ -1515,7 +1346,8 @@ void Renderer::initScreenSizedResources( EDT_COMBINED_IMAGE_SAMPLER, EDT_STORAGE_IMAGE }); - m_driver->updateDescriptorSets(3u,writes,0u,nullptr); + } + m_driver->updateDescriptorSets(3u,writes,0u,nullptr); // set up m_closestHitDS for (auto i=0u; i<2u; i++) @@ -1526,7 +1358,7 @@ void Renderer::initScreenSizedResources( infos[0u].buffer.size = m_rayBuffer[other].buffer->getSize(); createEmptyInteropBufferAndSetUpInfo(infos+1,m_intersectionBuffer[other],intersectionBufferSize); - m_closestHitDS[i] = m_driver->createGPUDescriptorSet(core::smart_refctd_ptr(m_closestHitDSLayout)); + m_closestHitDS[i] = m_driver->createDescriptorSet(core::smart_refctd_ptr(m_closestHitDSLayout)); setDstSetAndDescTypesOnWrites(m_closestHitDS[i].get(),writes,infos,{EDT_STORAGE_BUFFER,EDT_STORAGE_BUFFER}); m_driver->updateDescriptorSets(2u,writes,0u,nullptr); @@ -1541,29 +1373,31 @@ void Renderer::initScreenSizedResources( { IGPUImageView::SCreationParams viewparams = m_albedoAcc->getCreationParameters(); viewparams.format = EF_A2B10G10R10_UNORM_PACK32; - albedoSamplerView = m_driver->createGPUImageView(std::move(viewparams)); + albedoSamplerView = m_driver->createImageView(std::move(viewparams)); } setImageInfo(infos+2,asset::EIL_GENERAL,std::move(albedoSamplerView)); setImageInfo(infos+3,asset::EIL_GENERAL,core::smart_refctd_ptr(m_normalAcc)); - setImageInfo(infos+4,asset::EIL_GENERAL,core::smart_refctd_ptr(m_maskAcc)); - setImageInfo(infos+5,asset::EIL_GENERAL,core::smart_refctd_ptr(m_tonemapOutput)); + setImageInfo(infos+4,asset::EIL_GENERAL,core::smart_refctd_ptr(m_tonemapOutput)); core::smart_refctd_ptr albedoStorageView; { IGPUImageView::SCreationParams viewparams = m_albedoRslv->getCreationParameters(); viewparams.format = EF_R32_UINT; - albedoStorageView = m_driver->createGPUImageView(std::move(viewparams)); + albedoStorageView = m_driver->createImageView(std::move(viewparams)); } - setImageInfo(infos+6,asset::EIL_GENERAL,std::move(albedoStorageView)); - setImageInfo(infos+7,asset::EIL_GENERAL,core::smart_refctd_ptr(m_normalRslv)); + setImageInfo(infos+5,asset::EIL_GENERAL,std::move(albedoStorageView)); + setImageInfo(infos+6,asset::EIL_GENERAL,core::smart_refctd_ptr(m_normalRslv)); setDstSetAndDescTypesOnWrites(m_resolveDS.get(),writes,infos,{ EDT_UNIFORM_BUFFER, - EDT_COMBINED_IMAGE_SAMPLER,EDT_COMBINED_IMAGE_SAMPLER,EDT_COMBINED_IMAGE_SAMPLER,EDT_COMBINED_IMAGE_SAMPLER, + EDT_COMBINED_IMAGE_SAMPLER,EDT_COMBINED_IMAGE_SAMPLER,EDT_COMBINED_IMAGE_SAMPLER, EDT_STORAGE_IMAGE,EDT_STORAGE_IMAGE,EDT_STORAGE_IMAGE }); } - m_driver->updateDescriptorSets(8u,writes,0u,nullptr); + m_driver->updateDescriptorSets(7u,writes,0u,nullptr); + m_visibilityBuffer = m_driver->addFrameBuffer(); + m_visibilityBuffer->attach(EFAP_DEPTH_ATTACHMENT,createScreenSizedTexture(EF_D32_SFLOAT)); + m_visibilityBuffer->attach(EFAP_COLOR_ATTACHMENT0,std::move(visibilityBuffer)); m_colorBuffer = m_driver->addFrameBuffer(); m_colorBuffer->attach(EFAP_COLOR_ATTACHMENT0, core::smart_refctd_ptr(m_tonemapOutput)); @@ -1580,6 +1414,76 @@ void Renderer::initScreenSizedResources( std::cout << std::endl; } +void Renderer::deinitScreenSizedResources() +{ + auto commandQueue = m_rrManager->getCLCommandQueue(); + ocl::COpenCLHandler::ocl.pclFinish(commandQueue); + + glFinish(); + + // make sure descriptor sets dont dangle + //m_driver->bindDescriptorSets(video::EPBP_COMPUTE,nullptr,0u,4u,nullptr); + m_closestHitDS[0] = m_closestHitDS[1] = nullptr; + m_commonRaytracingDS[0] = m_commonRaytracingDS[1] = nullptr; + + // unset the framebuffer (dangling smartpointer in state cache can prevent the framebuffer from being dropped until the next framebuffer set) + m_driver->setRenderTarget(nullptr,false); + if (m_visibilityBuffer) + { + m_driver->removeFrameBuffer(m_visibilityBuffer); + m_visibilityBuffer = nullptr; + } + if (m_colorBuffer) + { + m_driver->removeFrameBuffer(m_colorBuffer); + m_colorBuffer = nullptr; + } + m_accumulation = m_tonemapOutput = nullptr; + m_albedoAcc = m_albedoRslv = nullptr; + m_normalAcc = m_normalRslv = nullptr; + + glFinish(); + + // wait for OpenCL to finish + ocl::COpenCLHandler::ocl.pclFlush(commandQueue); + ocl::COpenCLHandler::ocl.pclFinish(commandQueue); + for (auto i=0; i<2u; i++) + { + auto deleteInteropBuffer = [&](InteropBuffer& buffer) -> void + { + m_rrManager->unlinkBuffer(std::move(buffer.asRRBuffer)); + buffer = {}; + }; + deleteInteropBuffer(m_intersectionBuffer[i]); + deleteInteropBuffer(m_rayBuffer[i]); + } + + m_raygenWorkGroups[0] = m_raygenWorkGroups[1] = 0u; + + m_cullPipeline = nullptr; + m_raygenPipeline = nullptr; + m_closestHitPipeline = nullptr; + m_resolvePipeline = nullptr; + + m_staticViewData.imageDimensions = {0u, 0u}; + m_staticViewData.pathDepth = DefaultPathDepth; + m_staticViewData.noRussianRouletteDepth = 5u; + m_staticViewData.samplesPerPixelPerDispatch = 1u; + m_totalRaysCast = 0ull; + m_rcpPixelSize = {0.f,0.f}; + m_framesDispatched = 0u; + std::fill_n(m_prevView.pointer(),12u,0.f); + m_prevCamTform = nbl::core::matrix4x3(); +} + +void Renderer::resetSampleAndFrameCounters() +{ + m_totalRaysCast = 0ull; + m_framesDispatched = 0u; + std::fill_n(m_prevView.pointer(),12u,0.f); + m_prevCamTform = nbl::core::matrix4x3(); +} + void Renderer::takeAndSaveScreenShot(const std::filesystem::path& screenshotFilePath, bool denoise, const DenoiserArgs& denoiserArgs) { auto commandQueue = m_rrManager->getCLCommandQueue(); @@ -1600,53 +1504,39 @@ void Renderer::takeAndSaveScreenShot(const std::filesystem::path& screenshotFile if (m_normalRslv) ext::ScreenShot::createScreenShot(m_driver,m_assetManager,m_normalRslv.get(),filename_wo_ext.string()+"_normal.exr",format); - if (!denoise) - return; - - const std::string defaultBloomFile = "../../media/kernels/physical_flare_512.exr"; - const std::string defaultTonemapperArgs = "ACES=0.4,0.8"; - constexpr auto defaultBloomScale = 0.1f; - constexpr auto defaultBloomIntensity = 0.1f; - auto bloomFilePathStr = (denoiserArgs.bloomFilePath.string().empty()) ? defaultBloomFile : denoiserArgs.bloomFilePath.string(); - auto bloomScale = (denoiserArgs.bloomScale == 0.0f) ? defaultBloomScale : denoiserArgs.bloomScale; - auto bloomIntensity = (denoiserArgs.bloomIntensity == 0.0f) ? defaultBloomIntensity : denoiserArgs.bloomIntensity; - auto tonemapperArgs = (denoiserArgs.tonemapperArgs.empty()) ? defaultTonemapperArgs : denoiserArgs.tonemapperArgs; - - std::ostringstream denoiserCmd; - // 1.ColorFile 2.AlbedoFile 3.NormalFile 4.BloomPsfFilePath(STRING) 5.BloomScale(FLOAT) 6.BloomIntensity(FLOAT) 7.TonemapperArgs(STRING) - denoiserCmd << "call ../denoiser_hook.bat"; - denoiserCmd << " \"" << filename_wo_ext.string() << ".exr" << "\""; - denoiserCmd << " \"" << filename_wo_ext.string() << "_albedo.exr" << "\""; - denoiserCmd << " \"" << filename_wo_ext.string() << "_normal.exr" << "\""; - denoiserCmd << " \"" << bloomFilePathStr << "\""; - denoiserCmd << " " << bloomScale; - denoiserCmd << " " << bloomIntensity; - denoiserCmd << " " << "\"" << tonemapperArgs << "\""; - - if (m_deferDenoise) - { - // TODO[Przemek]: what to do when m_deferDenoiseFile is not open? crash or log error? - const bool isDeferDenoiseFileOpen = m_deferDenoiseFile.is_open(); - assert(isDeferDenoiseFileOpen); - - if (isDeferDenoiseFileOpen) - m_deferDenoiseFile << denoiserCmd.str() << std::endl; - } - else + if(denoise) { + const std::string defaultBloomFile = "../../media/kernels/physical_flare_512.exr"; + const std::string defaultTonemapperArgs = "ACES=0.4,0.8"; + constexpr auto defaultBloomScale = 0.1f; + constexpr auto defaultBloomIntensity = 0.1f; + auto bloomFilePathStr = (denoiserArgs.bloomFilePath.string().empty()) ? defaultBloomFile : denoiserArgs.bloomFilePath.string(); + auto bloomScale = (denoiserArgs.bloomScale == 0.0f) ? defaultBloomScale : denoiserArgs.bloomScale; + auto bloomIntensity = (denoiserArgs.bloomIntensity == 0.0f) ? defaultBloomIntensity : denoiserArgs.bloomIntensity; + auto tonemapperArgs = (denoiserArgs.tonemapperArgs.empty()) ? defaultTonemapperArgs : denoiserArgs.tonemapperArgs; + + std::ostringstream denoiserCmd; + // 1.ColorFile 2.AlbedoFile 3.NormalFile 4.BloomPsfFilePath(STRING) 5.BloomScale(FLOAT) 6.BloomIntensity(FLOAT) 7.TonemapperArgs(STRING) + denoiserCmd << "call ../denoiser_hook.bat"; + denoiserCmd << " \"" << filename_wo_ext.string() << ".exr" << "\""; + denoiserCmd << " \"" << filename_wo_ext.string() << "_albedo.exr" << "\""; + denoiserCmd << " \"" << filename_wo_ext.string() << "_normal.exr" << "\""; + denoiserCmd << " \"" << bloomFilePathStr << "\""; + denoiserCmd << " " << bloomScale; + denoiserCmd << " " << bloomIntensity; + denoiserCmd << " " << "\"" << tonemapperArgs << "\""; // NOTE/TODO/FIXME : Do as I say, not as I do // https://wiki.sei.cmu.edu/confluence/pages/viewpage.action?pageId=87152177 std::cout << "\n---[DENOISER_BEGIN]---" << std::endl; std::system(denoiserCmd.str().c_str()); std::cout << "\n---[DENOISER_END]---" << std::endl; } - } void Renderer::denoiseCubemapFaces( std::filesystem::path filePaths[6], const std::string& mergedFileName, - int32_t cropOffsetX, int32_t cropOffsetY, int32_t cropWidth, int32_t cropHeight, + int borderPixels, const DenoiserArgs& denoiserArgs) { auto commandQueue = m_rrManager->getCLCommandQueue(); @@ -1664,32 +1554,30 @@ void Renderer::denoiseCubemapFaces( for(uint32_t i = 0; i < 6; ++i) normalFilePaths[i] = filePaths[i].replace_extension().string() + "_normal.exr"; - std::filesystem::path mergedCubeMapOutputPath = filePaths[0].parent_path(); - - std::filesystem::path mergedRenderFilePath = mergedCubeMapOutputPath / std::filesystem::path(mergedFileName + ".exr"); - std::filesystem::path mergedAlbedoFilePath = mergedCubeMapOutputPath / std::filesystem::path(mergedFileName + "_albedo.exr"); - std::filesystem::path mergedNormalFilePath = mergedCubeMapOutputPath / std::filesystem::path(mergedFileName + "_normal.exr"); - std::filesystem::path mergedDenoisedFilePath = mergedCubeMapOutputPath / std::filesystem::path(mergedFileName + "_denoised.exr"); + std::string mergedRenderFilePath = mergedFileName + ".exr"; + std::string mergedAlbedoFilePath = mergedFileName + "_albedo.exr"; + std::string mergedNormalFilePath = mergedFileName + "_normal.exr"; + std::string mergedDenoisedFilePath = mergedFileName + "_denoised.exr"; std::ostringstream mergeRendersCmd; mergeRendersCmd << "call ../mergeCubemap.bat"; for(uint32_t i = 0; i < 6; ++i) mergeRendersCmd << " " << renderFilePaths[i]; - mergeRendersCmd << " " << mergedRenderFilePath.string(); + mergeRendersCmd << " " << mergedRenderFilePath; std::system(mergeRendersCmd.str().c_str()); std::ostringstream mergeAlbedosCmd; mergeAlbedosCmd << "call ../mergeCubemap.bat "; for(uint32_t i = 0; i < 6; ++i) mergeAlbedosCmd << " " << albedoFilePaths[i]; - mergeAlbedosCmd << " " << mergedAlbedoFilePath.string(); + mergeAlbedosCmd << " " << mergedAlbedoFilePath; std::system(mergeAlbedosCmd.str().c_str()); std::ostringstream mergeNormalsCmd; mergeNormalsCmd << "call ../mergeCubemap.bat "; for(uint32_t i = 0; i < 6; ++i) mergeNormalsCmd << " " << normalFilePaths[i]; - mergeNormalsCmd << " " << mergedNormalFilePath.string(); + mergeNormalsCmd << " " << mergedNormalFilePath; std::system(mergeNormalsCmd.str().c_str()); const std::string defaultBloomFile = "../../media/kernels/physical_flare_512.exr"; @@ -1704,9 +1592,9 @@ void Renderer::denoiseCubemapFaces( std::ostringstream denoiserCmd; // 1.ColorFile 2.AlbedoFile 3.NormalFile 4.BloomPsfFilePath(STRING) 5.BloomScale(FLOAT) 6.BloomIntensity(FLOAT) 7.TonemapperArgs(STRING) denoiserCmd << "call ../denoiser_hook.bat"; - denoiserCmd << " \"" << mergedRenderFilePath.string() << "\""; - denoiserCmd << " \"" << mergedAlbedoFilePath.string() << "\""; - denoiserCmd << " \"" << mergedNormalFilePath.string() << "\""; + denoiserCmd << " \"" << mergedRenderFilePath << "\""; + denoiserCmd << " \"" << mergedAlbedoFilePath << "\""; + denoiserCmd << " \"" << mergedNormalFilePath << "\""; denoiserCmd << " \"" << bloomFilePathStr << "\""; denoiserCmd << " " << bloomScale; denoiserCmd << " " << bloomIntensity; @@ -1722,10 +1610,13 @@ void Renderer::denoiseCubemapFaces( std::ostringstream extractImagesCmd; auto mergedDenoisedWithoutExtension = std::filesystem::path(mergedDenoisedFilePath).replace_extension().string(); extractImagesCmd << "call ../extractCubemap.bat "; - extractImagesCmd << " " << std::to_string(cropOffsetX); - extractImagesCmd << " " << std::to_string(cropOffsetY); + extractImagesCmd << " " << std::to_string(borderPixels); extractImagesCmd << " " << mergedDenoisedWithoutExtension + extension; - extractImagesCmd << " " << mergedDenoisedWithoutExtension + "_stripe" + extension; + for(uint32_t i = 0; i < 6; ++i) + { + auto renderFilePathWithoutExtension = std::filesystem::path(renderFilePaths[i]).replace_extension().string(); + extractImagesCmd << " " << renderFilePathWithoutExtension + "_denoised" + extension; + } std::system(extractImagesCmd.str().c_str()); }; @@ -1737,7 +1628,7 @@ void Renderer::denoiseCubemapFaces( // one day it will just work like that //#include -bool Renderer::render(nbl::ITimer* timer, const float kappa, const float Emin, const bool transformNormals, const bool beauty) +bool Renderer::render(nbl::ITimer* timer, const bool transformNormals, const bool beauty) { if (m_cullPushConstants.maxGlobalInstanceCount==0u) return true; @@ -1783,27 +1674,10 @@ bool Renderer::render(nbl::ITimer* timer, const float kappa, const float Emin, c bool compiledShaders = compileShadersFuture.get(); if(compiledShaders) { - m_cullPipeline = m_driver->createGPUComputePipeline(nullptr,core::smart_refctd_ptr(m_cullPipelineLayout), core::smart_refctd_ptr(m_cullGPUShader)); - { - IGPUSpecializedShader* shaders[] = {m_vertGPUShader.get(),m_fragGPUShader.get()}; - SPrimitiveAssemblyParams primitiveAssembly; - primitiveAssembly.primitiveType = EPT_TRIANGLE_LIST; - SRasterizationParams raster; - raster.faceCullingMode = EFCM_NONE; - auto _visibilityBufferFillPipelineLayout = m_driver->createGPUPipelineLayout( - nullptr,nullptr, - core::smart_refctd_ptr(m_rasterInstanceDataDSLayout), - core::smart_refctd_ptr(m_additionalGlobalDSLayout), - core::smart_refctd_ptr(m_cullDSLayout) - ); - m_visibilityBufferFillPipeline = m_driver->createGPURenderpassIndependentPipeline( - nullptr,std::move(_visibilityBufferFillPipelineLayout),shaders,shaders+2u, - SVertexInputParams{},SBlendParams{},primitiveAssembly,raster - ); - } - m_raygenPipeline = m_driver->createGPUComputePipeline(nullptr,core::smart_refctd_ptr(m_raygenPipelineLayout), core::smart_refctd_ptr(m_raygenGPUShader)); - m_closestHitPipeline = m_driver->createGPUComputePipeline(nullptr,core::smart_refctd_ptr(m_closestHitPipelineLayout), core::smart_refctd_ptr(m_closestHitGPUShader)); - m_resolvePipeline = m_driver->createGPUComputePipeline(nullptr,core::smart_refctd_ptr(m_resolvePipelineLayout), core::smart_refctd_ptr(m_resolveGPUShader)); + m_cullPipeline = m_driver->createComputePipeline(nullptr,core::smart_refctd_ptr(m_cullPipelineLayout), core::smart_refctd_ptr(m_cullGPUShader)); + m_raygenPipeline = m_driver->createComputePipeline(nullptr,core::smart_refctd_ptr(m_raygenPipelineLayout), core::smart_refctd_ptr(m_raygenGPUShader)); + m_closestHitPipeline = m_driver->createComputePipeline(nullptr,core::smart_refctd_ptr(m_closestHitPipelineLayout), core::smart_refctd_ptr(m_closestHitGPUShader)); + m_resolvePipeline = m_driver->createComputePipeline(nullptr,core::smart_refctd_ptr(m_resolvePipelineLayout), core::smart_refctd_ptr(m_resolveGPUShader)); bool createPipelinesSuceess = m_cullPipeline && m_raygenPipeline && m_closestHitPipeline && m_resolvePipeline; if(!createPipelinesSuceess) std::cout << "Pipeline Compilation Failed." << std::endl; @@ -1818,10 +1692,8 @@ bool Renderer::render(nbl::ITimer* timer, const float kappa, const float Emin, c // raster jittered frame { - const auto projMat = camera->getProjectionMatrix(); - const bool isOrtho = (projMat.rows[3]==core::vectorSIMDf(0.f,0.f,0.f,1.f)).all(); // jitter with AA AntiAliasingSequence - const auto modifiedProj = [&](uint32_t frameID) + const auto modifiedViewProj = [&](uint32_t frameID) { const float stddev = 0.5f; const float* sample = AntiAliasingSequence[frameID%AntiAliasingSequenceLength]; @@ -1833,38 +1705,14 @@ bool Renderer::render(nbl::ITimer* timer, const float kappa, const float Emin, c core::matrix4SIMD jitterMatrix; jitterMatrix.rows[0][3] = cosPhi*r*m_rcpPixelSize.x; jitterMatrix.rows[1][3] = sinPhi*r*m_rcpPixelSize.y; - return core::concatenateBFollowedByA(jitterMatrix,projMat); + return core::concatenateBFollowedByA(jitterMatrix,core::concatenateBFollowedByA(camera->getProjectionMatrix(),m_prevView)); }(m_framesDispatched); m_raytraceCommonData.rcpFramesDispatched = 1.f/float(m_framesDispatched); - m_raytraceCommonData.textureFootprintFactor = core::inversesqrt(core::min(m_framesDispatched ? m_framesDispatched:1u,Renderer::AntiAliasingSequenceLength)); - - // work out the inverse of the Rotation component of the View applied before Projection - core::matrix4SIMD viewDirReconFactorsT; - { - core::matrix4SIMD viewRotProjInvT; - { - core::matrix4SIMD viewRotProj(m_prevView); - viewRotProj.setTranslation(core::vectorSIMDf(0.f)); - if (!core::concatenateBFollowedByA(modifiedProj,viewRotProj).getInverseTransform(viewRotProjInvT)) - std::cout << "Couldn't calculate viewProjection matrix's inverse. something is wrong." << std::endl; - viewRotProjInvT = core::transpose(viewRotProjInvT); - } - if (isOrtho) // normalizedV = -viewRotProjInv - { - viewDirReconFactorsT.rows[0].set(0,0,0,0); - viewDirReconFactorsT.rows[1].set(0,0,0,0); - viewDirReconFactorsT.rows[2] = -viewRotProjInvT.rows[2]; - } - else - { - // note that we don't care about W coordinate & last row, W-divide, etc. because later normalization murders any scale on the vector - // normalizedV = normalize(-viewRotProjInv*vec3(NDC*vec2(0.5,-0.5)+vec2(-0.5,0.5),1)) - viewDirReconFactorsT.rows[0] = viewRotProjInvT.rows[0]*(-2.f); - viewDirReconFactorsT.rows[1] = viewRotProjInvT.rows[1]*(+2.f); - viewDirReconFactorsT.rows[2] = viewRotProjInvT.rows[0]-viewRotProjInvT.rows[1]-viewRotProjInvT.rows[2]-viewRotProjInvT.rows[3]; - } - } - + m_raytraceCommonData.textureFootprintFactor = core::inversesqrt(core::min(m_framesDispatched,Renderer::AntiAliasingSequenceLength)); + if(!modifiedViewProj.getInverseTransform(m_raytraceCommonData.viewProjMatrixInverse)) + std::cout << "Couldn't calculate viewProjection matrix's inverse. something is wrong." << std::endl; + // for (auto i=0u; i<3u; i++) + // m_raytraceCommonData.ndcToV.rows[i] = inverseMVP.rows[3]*cameraPosition[i]-inverseMVP.rows[i]; // cull batches m_driver->bindComputePipeline(m_cullPipeline.get()); { @@ -1873,8 +1721,8 @@ bool Renderer::render(nbl::ITimer* timer, const float kappa, const float Emin, c IGPUDescriptorSet* descriptorSets[] = { m_globalBackendDataDS.get(),m_cullDS.get() }; m_driver->bindDescriptorSets(EPBP_COMPUTE,_cullPipelineLayout,0u,2u,descriptorSets,nullptr); - m_cullPushConstants.viewProjMatrix = core::concatenateBFollowedByA(modifiedProj,m_prevView); - m_cullPushConstants.viewProjDeterminant = core::determinant(m_cullPushConstants.viewProjMatrix); + m_cullPushConstants.viewProjMatrix = modifiedViewProj; + m_cullPushConstants.viewProjDeterminant = core::determinant(modifiedViewProj); m_driver->pushConstants(_cullPipelineLayout,ISpecializedShader::ESS_COMPUTE,0u,sizeof(CullShaderData_t),&m_cullPushConstants); } // TODO: Occlusion Culling against HiZ Buffer @@ -1906,13 +1754,15 @@ bool Renderer::render(nbl::ITimer* timer, const float kappa, const float Emin, c m_cullPushConstants.currentCommandBufferIx ^= 0x01u; // prepare camera data for raytracing - viewDirReconFactorsT.rows[3] = core::vectorSIMDf().set(camera->getAbsolutePosition()); - m_raytraceCommonData.viewDirReconFactors = core::transpose(viewDirReconFactorsT).extractSub3x4(); + const auto cameraPosition = core::vectorSIMDf().set(camera->getAbsolutePosition()); + m_raytraceCommonData.camPos.x = cameraPosition.x; + m_raytraceCommonData.camPos.y = cameraPosition.y; + m_raytraceCommonData.camPos.z = cameraPosition.z; } // raygen { // vertex 0 is camera - m_raytraceCommonData.setPathDepth(beauty ? 0u:(~0u)); + m_raytraceCommonData.depth = beauty ? 0u:(~0u); // video::IGPUDescriptorSet* sameDS[2] = {m_raygenDS.get(),m_raygenDS.get()}; @@ -1925,7 +1775,7 @@ bool Renderer::render(nbl::ITimer* timer, const float kappa, const float Emin, c // path trace if (beauty) { - while (m_raytraceCommonData.getPathDepth()!=m_staticViewData.maxPathDepth) + while (m_raytraceCommonData.depth!=m_staticViewData.pathDepth) { uint32_t raycount; if(!traceBounce(raycount)) @@ -1941,8 +1791,6 @@ bool Renderer::render(nbl::ITimer* timer, const float kappa, const float Emin, c // resolve pseudo-MSAA if (beauty) { - m_raytraceCommonData.frameLowDiscrepancySequenceShift = (m_raytraceCommonData.frameLowDiscrepancySequenceShift+getSamplesPerPixelPerDispatch())%maxSensorSamples; - m_driver->bindDescriptorSets(EPBP_COMPUTE,m_resolvePipeline->getLayout(),0u,1u,&m_resolveDS.get(),nullptr); m_driver->bindComputePipeline(m_resolvePipeline.get()); if (transformNormals) @@ -1952,20 +1800,12 @@ bool Renderer::render(nbl::ITimer* timer, const float kappa, const float Emin, c decltype(m_prevView) identity; m_driver->pushConstants(m_resolvePipeline->getLayout(),ICPUSpecializedShader::ESS_COMPUTE,0u,sizeof(identity),&identity); } - { - const auto reweightingParams = nbl_glsl_RWMC_computeReweightingParameters( - m_staticViewData.cascadeParams.penultimateCascadeIx+2u, - m_staticViewData.cascadeParams.base, - m_framesDispatched*m_staticViewData.samplesPerPixelPerDispatch, - Emin,kappa - ); - m_driver->pushConstants(m_resolvePipeline->getLayout(),ICPUSpecializedShader::ESS_COMPUTE,sizeof(m_prevView),sizeof(reweightingParams),&reweightingParams); - } m_driver->dispatch(m_raygenWorkGroups[0],m_raygenWorkGroups[1],1); COpenGLExtensionHandler::pGlMemoryBarrier(GL_TEXTURE_FETCH_BARRIER_BIT|GL_SHADER_IMAGE_ACCESS_BARRIER_BIT // because of direct to screen resolve |GL_FRAMEBUFFER_BARRIER_BIT|GL_TEXTURE_UPDATE_BARRIER_BIT ); + m_raytraceCommonData.samplesComputed = (m_raytraceCommonData.samplesComputed+getSamplesPerPixelPerDispatch())%maxSensorSamples; } // TODO: autoexpose properly @@ -1975,13 +1815,12 @@ bool Renderer::render(nbl::ITimer* timer, const float kappa, const float Emin, c void Renderer::preDispatch(const video::IGPUPipelineLayout* pipelineLayout, video::IGPUDescriptorSet*const *const lastDS) { // increment depth - const auto depth = m_raytraceCommonData.getPathDepth()+1; - m_raytraceCommonData.setPathDepth(depth); - const uint32_t descSetIx = depth&0x1u; + const uint32_t descSetIx = (++m_raytraceCommonData.depth)&0x1u; m_driver->pushConstants(pipelineLayout,ISpecializedShader::ESS_COMPUTE,0u,sizeof(RaytraceShaderCommonData_t),&m_raytraceCommonData); - // advance rayCountWriteIx - m_raytraceCommonData.advanceWriteIndex(); + // advance + static_assert(core::isPoT(RAYCOUNT_N_BUFFERING),"Raycount Buffer needs to be PoT sized!"); + m_raytraceCommonData.rayCountWriteIx = (++m_raytraceCommonData.rayCountWriteIx)&RAYCOUNT_N_BUFFERING_MASK; IGPUDescriptorSet* descriptorSets[4] = {m_globalBackendDataDS.get(),m_additionalGlobalDS.get(),m_commonRaytracingDS[descSetIx].get(),lastDS[descSetIx]}; m_driver->bindDescriptorSets(EPBP_COMPUTE,pipelineLayout,0u,4u,descriptorSets,nullptr); @@ -1991,7 +1830,10 @@ bool Renderer::traceBounce(uint32_t& raycount) { // probably wise to flush all caches (in the future can optimize to texture_fetch|shader_image_access|shader_storage_buffer|blit|texture_download|...) COpenGLExtensionHandler::pGlMemoryBarrier(GL_ALL_BARRIER_BITS); - m_driver->copyBuffer(m_rayCountBuffer.get(),m_littleDownloadBuffer.get(),sizeof(uint32_t)*m_raytraceCommonData.getReadIndex(),0u,sizeof(uint32_t)); + { + const auto rayCountReadIx = (m_raytraceCommonData.rayCountWriteIx-1u)&RAYCOUNT_N_BUFFERING_MASK; + m_driver->copyBuffer(m_rayCountBuffer.get(),m_littleDownloadBuffer.get(),sizeof(uint32_t)*rayCountReadIx,0u,sizeof(uint32_t)); + } glFinish(); // sync CPU to GL raycount = *reinterpret_cast(m_littleDownloadBuffer->getBoundMemory()->getMappedPointer()); @@ -2000,7 +1842,7 @@ bool Renderer::traceBounce(uint32_t& raycount) // trace rays m_totalRaysCast += raycount; { - const uint32_t descSetIx = m_raytraceCommonData.getPathDepth()&0x1u; + const uint32_t descSetIx = m_raytraceCommonData.depth&0x1u; auto commandQueue = m_rrManager->getCLCommandQueue(); const cl_mem clObjects[] = {m_rayBuffer[descSetIx].asRRBuffer.second,m_intersectionBuffer[descSetIx].asRRBuffer.second}; @@ -2044,17 +1886,7 @@ bool Renderer::traceBounce(uint32_t& raycount) std::cout << "[ERROR] RadeonRays Timed Out" << std::endl; return false; } - - if (static_cast(m_driver)->runningInRenderdoc()) - { - auto touchAllBytes = [](IGPUBuffer* buf)->void - { - auto ptr = reinterpret_cast(buf->getBoundMemory()->getMappedPointer()); - }; - touchAllBytes(m_intersectionBuffer[descSetIx].buffer.get()); - } } - // compute bounce (accumulate contributions and optionally generate rays) { @@ -2066,4 +1898,1033 @@ bool Renderer::traceBounce(uint32_t& raycount) } return true; -} \ No newline at end of file +} + + +const float Renderer::AntiAliasingSequence[Renderer::AntiAliasingSequenceLength][2] = +{ +{0.229027962000000, 0.100901043000000}, +{0.934988661250000, 0.900492937500000}, +{0.693936740750000, 0.477888665000000}, +{0.396013875250000, 0.867381653000000}, +{0.151208663250000, 0.331649132250000}, +{0.919338615000000, 0.306386117750000}, +{0.454737456500000, 0.597940860250000}, +{0.911951413000000, 0.584874565000000}, +{0.471331207500000, 0.117509299250000}, +{0.724981748000000, 0.988645892000000}, +{0.227727943750000, 0.553082892250000}, +{0.927148254750000, 0.059077206250000}, +{0.170420940250000, 0.853803466500000}, +{0.369496963250000, 0.372492160250000}, +{0.709055501500000, 0.719526612750000}, +{0.708593019750000, 0.236308825250000}, +{0.053515783250000, 0.244794542562500}, +{0.759417624125000, 0.846532545187500}, +{0.572365454937500, 0.341559262437500}, +{0.269128942562500, 0.962581831375000}, +{0.246508261687500, 0.286661635812500}, +{0.819542439062500, 0.459099133812500}, +{0.411348913687500, 0.737420359250000}, +{0.896647944437500, 0.717554343125000}, +{0.358057598000000, 0.050206801437500}, +{0.605871046250000, 0.779868041500000}, +{0.036816445812500, 0.506511135625000}, +{0.806931985937500, 0.138270723062500}, +{0.045020470000000, 0.818334270875000}, +{0.433264399500000, 0.254739200375000}, +{0.556258709500000, 0.559776624000000}, +{0.611048395312500, 0.162518625750000}, +{0.028918631812500, 0.053438072375000}, +{0.856252533125000, 0.916712681500000}, +{0.580344816187500, 0.463534157062500}, +{0.291334488000000, 0.774756179000000}, +{0.157847279187500, 0.464948199125000}, +{0.775478249937500, 0.320623736250000}, +{0.306258709500000, 0.653526624000000}, +{0.798533046937500, 0.552896543187500}, +{0.349953270437500, 0.123764825500000}, +{0.534027961437500, 0.969931745937500}, +{0.122488661312500, 0.681742937625000}, +{0.849003468812500, 0.216845413250000}, +{0.145343900750000, 0.962506045625000}, +{0.395929912437500, 0.488477370312500}, +{0.675219736437500, 0.601237158875000}, +{0.728921568625000, 0.053308823500000}, +{0.153721825125000, 0.145597505062500}, +{0.852763510375000, 0.797682223125000}, +{0.644595719312500, 0.367380713687500}, +{0.475934665312500, 0.787623234375000}, +{0.037670496437500, 0.386130180750000}, +{0.916111850937500, 0.403604173437500}, +{0.307256453062500, 0.518207928812500}, +{0.836158139312500, 0.677526975812500}, +{0.291525812500000, 0.197831715312500}, +{0.632543215125000, 0.896220934750000}, +{0.039235045687500, 0.629605464812500}, +{0.927263875375000, 0.179881653187500}, +{0.036335975187500, 0.990626511375000}, +{0.458406617875000, 0.372877193062500}, +{0.545614665812500, 0.676662283062500}, +{0.606815968812500, 0.044970413250000}, +{0.031533697125000, 0.184836288625000}, +{0.943869562500000, 0.830155934062500}, +{0.607026984312500, 0.286243495000000}, +{0.385468447812500, 0.923477959062500}, +{0.211591778000000, 0.432717372437500}, +{0.959561740812500, 0.477888665062500}, +{0.340921091062500, 0.599871303750000}, +{0.770926812125000, 0.740443845937500}, +{0.492972183312500, 0.243769330562500}, +{0.520086204062500, 0.865883539250000}, +{0.194132187625000, 0.711586172812500}, +{0.867832801875000, 0.029377324812500}, +{0.018898352500000, 0.755166315812500}, +{0.294110519250000, 0.340476317312500}, +{0.645436781125000, 0.669120978187500}, +{0.537010584750000, 0.070669853500000}, +{0.161951413000000, 0.209874565062500}, +{0.786335975187500, 0.990626511375000}, +{0.525681985937500, 0.419520723062500}, +{0.287619562500000, 0.834550465312500}, +{0.100299557750000, 0.367542953000000}, +{0.787670496437500, 0.386130180750000}, +{0.425010132750000, 0.666850725937500}, +{0.959417841312500, 0.712724761625000}, +{0.259027114250000, 0.027505482375000}, +{0.706747124500000, 0.863983912687500}, +{0.118758709500000, 0.559776624000000}, +{0.979834653750000, 0.076596529437500}, +{0.076814113250000, 0.879551982187500}, +{0.458038607062500, 0.495297691687500}, +{0.676899749875000, 0.533654791000000}, +{0.739509651750000, 0.162886922875000}, +{0.130635833000000, 0.032884578937500}, +{0.995486845875000, 0.879726983937500}, +{0.681683761437500, 0.415213866187500}, +{0.471888733500000, 0.975077322375000}, +{0.002080578437500, 0.292317740812500}, +{0.982026984312500, 0.286243495000000}, +{0.291525812500000, 0.713456715312500}, +{0.803515783250000, 0.619794542562500}, +{0.363736251000000, 0.241491573500000}, +{0.581375603187500, 0.850024182625000}, +{0.126134788437500, 0.739345154625000}, +{0.807256990625000, 0.025225260812500}, +{0.214063133312500, 0.979178170312500}, +{0.279120068187500, 0.455460706437500}, +{0.521614411125000, 0.748128257250000}, +{0.541375661500000, 0.191916865812500}, +{0.092374240812500, 0.093123040062500}, +{0.819780017000000, 0.863865176562500}, +{0.723535390937500, 0.290673655562500}, +{0.333626471625000, 0.991508772375000}, +{0.180081879937500, 0.273337083437500}, +{0.884853249937500, 0.353826861250000}, +{0.486489450437500, 0.649922456187500}, +{0.970355124125000, 0.588720045187500}, +{0.411054041562500, 0.190728892687500}, +{0.670557598000000, 0.782628676437500}, +{0.176686781125000, 0.590995978187500}, +{0.923484185187500, 0.119472166250000}, +{0.229834653750000, 0.826596529437500}, +{0.402229645500000, 0.427815757250000}, +{0.614887300500000, 0.582390020187500}, +{0.721331207625000, 0.117509299250000}, +{0.221780261562500, 0.160787322875000}, +{0.980871046250000, 0.779868041500000}, +{0.521614411125000, 0.498128257250000}, +{0.462698109750000, 0.855009158437500}, +{0.102148981812500, 0.485181351500000}, +{0.790505847500000, 0.272359588500000}, +{0.357263913000000, 0.553624565062500}, +{0.852875617687500, 0.518271589687500}, +{0.412788910312500, 0.072860258937500}, +{0.739509651750000, 0.912886922875000}, +{0.244715387500000, 0.610882883562500}, +{0.931245437000000, 0.247161473000000}, +{0.118495619500000, 0.827404835625000}, +{0.356241537562500, 0.307793951312500}, +{0.739954645312500, 0.601971750750000}, +{0.652229645500000, 0.240315757250000}, +{0.085230272750000, 0.149967825937500}, +{0.790487853250000, 0.802468641250000}, +{0.742972183312500, 0.431269330562500}, +{0.338023546687500, 0.864140358375000}, +{0.161359195250000, 0.386030244500000}, +{0.979622565375000, 0.415764143437500}, +{0.344324410875000, 0.743490102812500}, +{0.850000234687500, 0.588036936812500}, +{0.478921568625000, 0.053308823500000}, +{0.575878945812500, 0.904948635625000}, +{0.066809364125000, 0.711985215062500}, +{0.842374240812500, 0.093123040062500}, +{0.072833622000000, 0.943689044750000}, +{0.473982478062500, 0.309619342562500}, +{0.643468702500000, 0.727011596187500}, +{0.661784804062500, 0.096504548000000}, +{0.075593410125000, 0.020665263437500}, +{0.846367111937500, 0.980869489750000}, +{0.584417841312500, 0.402177886625000}, +{0.419264650000000, 0.807665176000000}, +{0.108911798812500, 0.274823750687500}, +{0.842214949125000, 0.395649388625000}, +{0.424460011250000, 0.541515061937500}, +{0.914875915625000, 0.525088448500000}, +{0.276815978250000, 0.138406141250000}, +{0.682946765937500, 0.941192325375000}, +{0.243922631125000, 0.674414353500000}, +{0.983747165312500, 0.225123234375000}, +{0.209039534812500, 0.919381743937500}, +{0.317979460312500, 0.396931190375000}, +{0.595789254625000, 0.645833852812500}, +{0.589063133312500, 0.229178170312500}, +{0.201732996437500, 0.034567680750000}, +{0.911951413000000, 0.959874565062500}, +{0.669465614812500, 0.307270670687500}, +{0.442773254937500, 0.918452206312500}, +{0.228659227750000, 0.498372525687500}, +{0.864786425062500, 0.258916160937500}, +{0.366015783250000, 0.682294542562500}, +{0.832368054687500, 0.749523853312500}, +{0.475148582250000, 0.180782790250000}, +{0.543804934062500, 0.806559289687500}, +{0.041831345187500, 0.574164114062500}, +{0.981787063687500, 0.014769301562500}, +{0.167325380812500, 0.796656456375000}, +{0.305883847687500, 0.260168413875000}, +{0.736593646187500, 0.544303438875000}, +{0.595631980687500, 0.113338942562500}, +{0.233747165312500, 0.225123234375000}, +{0.881420496437500, 0.854880180750000}, +{0.514120916562500, 0.361726452125000}, +{0.262088408375000, 0.897305108937500}, +{0.040764352812500, 0.448613484812500}, +{0.882527922875000, 0.453355770312500}, +{0.486593646187500, 0.544303438875000}, +{0.944193581437500, 0.650074503000000}, +{0.403764392000000, 0.003513614062500}, +{0.647805652187500, 0.839498260375000}, +{0.004346402437500, 0.700568695812500}, +{0.863684364125000, 0.149485215062500}, +{0.075593410125000, 0.770665263437500}, +{0.260573230937500, 0.378374937125000}, +{0.606947383687500, 0.518907935937500}, +{0.522543332062500, 0.131538315687500}, +{0.115674527437500, 0.213752289562500}, +{0.978861550187500, 0.943531534250000}, +{0.716222608750000, 0.357993041500000}, +{0.396123640562500, 0.988911053812500}, +{0.116417439062500, 0.427849133812500}, +{0.960375986437500, 0.355143408875000}, +{0.396123640562500, 0.613911053812500}, +{0.771872079375000, 0.679610301562500}, +{0.407651999187500, 0.129979780250000}, +{0.610967390562500, 0.957538983500000}, +{0.099030910125000, 0.622227763437500}, +{0.792605235062500, 0.213450866625000}, +{0.231787063687500, 0.764769301562500}, +{0.345102996500000, 0.451097146437500}, +{0.537639116937500, 0.609792796562500}, +{0.670557598000000, 0.032628676437500}, +{0.161148929812500, 0.091845178375000}, +{0.915446201000000, 0.774126136937500}, +{0.542495165812500, 0.275647345250000}, +{0.316935423562500, 0.923529210750000}, +{0.209068937250000, 0.340693571625000}, +{0.770926812125000, 0.490443845937500}, +{0.462732614000000, 0.712224844000000}, +{0.887121046250000, 0.643149291500000}, +{0.302373640562500, 0.082661053812500}, +{0.728921568625000, 0.803308823500000}, +{0.181258709500000, 0.653526624000000}, +{0.977987853250000, 0.146218641250000}, +{0.016702341062500, 0.927996303750000}, +{0.467374240812500, 0.431013665062500}, +{0.706158315437500, 0.659556033875000}, +{0.669301523312500, 0.186625825562500}, +{0.039461819812500, 0.116237049750000}, +{0.798533046937500, 0.927896543187500}, +{0.636245165812500, 0.463147345250000}, +{0.358057598000000, 0.800206801437500}, +{0.057953992187500, 0.323742603312500}, +{0.838196808062500, 0.318240323625000}, +{0.288441098000000, 0.563378403500000}, +{0.981947383687500, 0.518907935937500}, +{0.348181288187500, 0.183212688250000}, +{0.506420496437500, 0.917380180750000}, +{0.164875915625000, 0.525088448500000}, +{0.787802165812500, 0.082912283062500}, +{0.134409779187500, 0.902448199125000}, +{0.408376747562500, 0.326245734125000}, +{0.584561740812500, 0.727888665062500}, +{0.534266910125000, 0.009900787187500}, +{0.192731703031250, 0.122610961484375}, +{0.886403666453125, 0.919165570765625}, +{0.740362459437500, 0.479082682703125}, +{0.381143881328125, 0.825372076343750}, +{0.177058600328125, 0.355660703968750}, +{0.880411986109375, 0.304388585781250}, +{0.489954645359375, 0.601971750750000}, +{0.931245437015625, 0.622161472968750}, +{0.495691442609375, 0.089534956375000}, +{0.748617226093750, 0.964032599359375}, +{0.200716102781250, 0.538594564312500}, +{0.902762098828125, 0.040629656437500}, +{0.167366403703125, 0.826817667671875}, +{0.326986691484375, 0.360298081343750}, +{0.725793624375000, 0.689620323765625}, +{0.725003755000000, 0.197653489734375}, +{0.019203528312500, 0.219887995546875}, +{0.789461819796875, 0.866237049781250}, +{0.602987853250000, 0.364968641265625}, +{0.286530910140625, 0.997227763453125}, +{0.197691088203125, 0.299653371203125}, +{0.864509651765625, 0.475386922921875}, +{0.409509265921875, 0.695098575390625}, +{0.924966150343750, 0.731175805390625}, +{0.366549151562500, 0.016182260046875}, +{0.620446765921875, 0.753692325390625}, +{0.004126035234375, 0.512365679515625}, +{0.759204111453125, 0.126627783906250}, +{0.039461819796875, 0.866237049781250}, +{0.385324830531250, 0.282537197156250}, +{0.529500805046875, 0.539659192578125}, +{0.620486845921875, 0.129726983984375}, +{0.040287232453125, 0.022961294593750}, +{0.871648411546875, 0.886075859718750}, +{0.567731703031250, 0.497610961484375}, +{0.271219649968750, 0.785940731265625}, +{0.149813081953125, 0.495059302156250}, +{0.752354406031250, 0.336633136296875}, +{0.267950605953125, 0.630717656421875}, +{0.763064970921875, 0.526211358984375}, +{0.350302165859375, 0.082912283093750}, +{0.541375661546875, 0.941916865828125}, +{0.067888875390625, 0.648631653203125}, +{0.817282235640625, 0.240645457843750}, +{0.176686781125000, 0.965995978171875}, +{0.414407018781250, 0.458335411421875}, +{0.635381359671875, 0.622395865796875}, +{0.696580598671875, 0.010305563015625}, +{0.146140435203125, 0.181972166265625}, +{0.853197227578125, 0.768215064734375}, +{0.631158461921875, 0.330667103468750}, +{0.443098689046875, 0.770526325000000}, +{0.008860189078125, 0.404241883828125}, +{0.920499240812500, 0.436873040078125}, +{0.274931749687500, 0.517395920968750}, +{0.872488661328125, 0.681742937687500}, +{0.273658139312500, 0.240026975812500}, +{0.686178846875000, 0.902720720890625}, +{0.022328994562500, 0.659601535312500}, +{0.889064677375000, 0.139944156000000}, +{0.041831345203125, 0.949164114093750}, +{0.443262299140625, 0.313206182765625}, +{0.553107937421875, 0.637161480234375}, +{0.576361266343750, 0.010049207671875}, +{0.024757727531250, 0.155556940859375}, +{0.954885609765625, 0.864774783453125}, +{0.576988498046875, 0.268435650828125}, +{0.378272153750000, 0.889096529468750}, +{0.243922631171875, 0.424414353515625}, +{0.993449504812500, 0.487462829328125}, +{0.315047772046875, 0.590538342781250}, +{0.757436479140625, 0.715431613031250}, +{0.454737456671875, 0.222940860375000}, +{0.506538910328125, 0.822860258968750}, +{0.223798019234375, 0.699851317375000}, +{0.839514399500000, 0.012551700359375}, +{0.013378945812500, 0.811198635640625}, +{0.259404890625000, 0.333637616328125}, +{0.674460011265625, 0.635265061968750}, +{0.552179912484375, 0.113477370312500}, +{0.133506990359375, 0.242482936484375}, +{0.792605235078125, 0.963450866640625}, +{0.556245437015625, 0.434661472968750}, +{0.302640369765625, 0.866357903265625}, +{0.104025812484375, 0.322831715312500}, +{0.788292439093750, 0.420036633796875}, +{0.383947288453125, 0.645595154640625}, +{0.987679025703125, 0.720586141078125}, +{0.310166960328125, 0.049274940406250}, +{0.692634651765625, 0.826949422921875}, +{0.066739180750000, 0.551367061812500}, +{0.954885609765625, 0.114774783453125}, +{0.106252533187500, 0.916712681484375}, +{0.490362459437500, 0.479082682703125}, +{0.646028602781250, 0.509297689312500}, +{0.696508847734375, 0.182043413890625}, +{0.167639399500000, 0.008157169109375}, +{0.942731703031250, 0.935110961484375}, +{0.682010510203125, 0.383364961312500}, +{0.444750986453125, 0.993815283906250}, +{0.012293182515625, 0.265019109265625}, +{0.943520700468750, 0.285664643703125}, +{0.256436740812500, 0.727888665078125}, +{0.792605235078125, 0.588450866640625}, +{0.323828669343750, 0.228345414000000}, +{0.589727949703125, 0.818705937671875}, +{0.146647944500000, 0.717554343109375}, +{0.763378945812500, 0.061198635640625}, +{0.245217761562500, 0.944967010375000}, +{0.302009651765625, 0.475386922921875}, +{0.508157018781250, 0.708335411421875}, +{0.552058600328125, 0.230660703968750}, +{0.076470961921875, 0.065042103468750}, +{0.839060384390625, 0.826948487828125}, +{0.743383847734375, 0.260168413890625}, +{0.361729406031250, 0.961633136296875}, +{0.130411986109375, 0.304388585781250}, +{0.913057411375000, 0.372578939312500}, +{0.450791960328125, 0.635700721656250}, +{0.994715387546875, 0.610882883562500}, +{0.396123640625000, 0.238911053828125}, +{0.635171568625000, 0.803308823531250}, +{0.134436274500000, 0.588235294109375}, +{0.893091363734375, 0.085389815609375}, +{0.204885609765625, 0.864774783453125}, +{0.419763913046875, 0.397374565093750}, +{0.589063133281250, 0.604178170375000}, +{0.692634651765625, 0.076949422921875}, +{0.192731703031250, 0.185110961484375}, +{0.951814247656250, 0.756306315203125}, +{0.506689186515625, 0.439765218421875}, +{0.456461826015625, 0.821001137000000}, +{0.083707248625000, 0.461775383828125}, +{0.764249240812500, 0.280623040078125}, +{0.323579912671875, 0.557221730578125}, +{0.818079645359375, 0.508221750750000}, +{0.435674328421875, 0.095052115796875}, +{0.725148582281250, 0.930782790234375}, +{0.211591777984375, 0.620217372437500}, +{0.901467761562500, 0.194967010375000}, +{0.114509651765625, 0.873824422921875}, +{0.350302165859375, 0.270412283093750}, +{0.713196808109375, 0.568240323640625}, +{0.631158461921875, 0.205667103468750}, +{0.121648411546875, 0.136075859718750}, +{0.807256990687500, 0.775225260828125}, +{0.748441255000000, 0.385153489734375}, +{0.322881990687500, 0.822100260828125}, +{0.170499240812500, 0.436873040078125}, +{0.976735045687500, 0.379605464812500}, +{0.326988498046875, 0.705935650828125}, +{0.849030910140625, 0.622227763453125}, +{0.456628942609375, 0.025081831375000}, +{0.603718978906250, 0.881272112125000}, +{0.087671365468750, 0.733711546609375}, +{0.858316099875000, 0.063684800093750}, +{0.079233855890625, 0.980882302687500}, +{0.498831558375000, 0.275019330593750}, +{0.683006746078125, 0.696560873750000}, +{0.634421685203125, 0.070155760015625}, +{0.100941098000000, 0.000878403515625}, +{0.868983666328125, 0.946905808593750}, +{0.622823333015625, 0.407884578921875}, +{0.380701413046875, 0.772374565093750}, +{0.070966777984375, 0.276467372437500}, +{0.869730392156250, 0.411764705875000}, +{0.390973727828125, 0.533716984406250}, +{0.934919978109375, 0.561328326953125}, +{0.267725152796875, 0.170350753109375}, +{0.650556467859375, 0.939171186375000}, +{0.208092013671875, 0.656130963328125}, +{0.939854406031250, 0.211633136296875}, +{0.227987853250000, 0.896218641265625}, +{0.362037827187500, 0.411778179156250}, +{0.575564970921875, 0.682461358984375}, +{0.603861550250000, 0.193531534234375}, +{0.245936791328125, 0.056280808593750}, +{0.931245437015625, 0.997161472968750}, +{0.674341363734375, 0.272889815609375}, +{0.475148582281250, 0.930782790234375}, +{0.196690920375000, 0.490305748390625}, +{0.823073230953125, 0.290484312156250}, +{0.349801672015625, 0.643614082703125}, +{0.816809364156250, 0.711985215093750}, +{0.442773254953125, 0.168452206343750}, +{0.559175124515625, 0.768645847968750}, +{0.012608312281250, 0.564660480046875}, +{0.951732996484375, 0.034567680765625}, +{0.130635833015625, 0.782884578921875}, +{0.295859197828125, 0.295320202140625}, +{0.712431749687500, 0.517395920968750}, +{0.572626461875000, 0.068089897125000}, +{0.211591777984375, 0.245217372437500}, +{0.901756746078125, 0.821560873750000}, +{0.512364601359375, 0.315328221531250}, +{0.275838593406250, 0.932598880093750}, +{0.007956102718750, 0.451497525703125}, +{0.924966150343750, 0.481175805390625}, +{0.454495282953125, 0.559257955593750}, +{0.978187903312500, 0.673257136171875}, +{0.416103249937500, 0.041326861265625}, +{0.664447403859375, 0.864416693968750}, +{0.033521988046875, 0.696631179015625}, +{0.852837228421875, 0.184355089812500}, +{0.090934062750000, 0.810372893375000}, +{0.275746963312500, 0.411554660312500}, +{0.588037245453125, 0.558795337875000}, +{0.554922654015625, 0.160357524531250}, +{0.072833622000000, 0.193689044750000}, +{0.964063133281250, 0.979178170375000}, +{0.708517234312500, 0.319548392906250}, +{0.432256990687500, 0.962725260828125}, +{0.068079645359375, 0.414471750750000}, +{0.963190877593750, 0.324420555781250}, +{0.411502470921875, 0.573086358984375}, +{0.800162124781250, 0.669611615750000}, +{0.387554934109375, 0.150309289718750}, +{0.579945004250000, 0.965966294140625}, +{0.065522102093750, 0.599326277234375}, +{0.761255117500000, 0.204583567718750}, +{0.196950541453125, 0.770728070765625}, +{0.344324410937500, 0.493490102843750}, +{0.510111550250000, 0.568531534234375}, +{0.636389399500000, 0.008157169109375}, +{0.128530229140625, 0.090431613031250}, +{0.883566727531250, 0.752475196796875}, +{0.552206093281250, 0.309003302484375}, +{0.348181288187500, 0.933212688265625}, +{0.227987853250000, 0.364968641265625}, +{0.771924527437500, 0.448127289609375}, +{0.489679912484375, 0.745313307812500}, +{0.927148254953125, 0.684077206343750}, +{0.264066255000000, 0.103903489734375}, +{0.740057568187500, 0.764054456484375}, +{0.148947313656250, 0.630208463203125}, +{0.974161986109375, 0.179388585781250}, +{0.010457836296875, 0.893541028515625}, +{0.498441255000000, 0.385153489734375}, +{0.744468904421875, 0.637380397421875}, +{0.678301531546875, 0.136423458078125}, +{0.010191088203125, 0.112153371203125}, +{0.774757727531250, 0.905556940859375}, +{0.674229406031250, 0.446008136296875}, +{0.319922419343750, 0.784986039000000}, +{0.011042923812500, 0.349437248625000}, +{0.821379264859375, 0.354136628500000}, +{0.257237357453125, 0.579287821171875}, +{0.948151308765625, 0.522112716656250}, +{0.318520700468750, 0.160664643703125}, +{0.543804934109375, 0.900309289718750}, +{0.130607996843750, 0.519919121093750}, +{0.811627065421875, 0.071665408953125}, +{0.160867175625000, 0.931752899046875}, +{0.428297719390625, 0.362355138953125}, +{0.609505036140625, 0.690144858781250}, +{0.504804041609375, 0.003228892687500}, +{0.216196606265625, 0.064729040234375}, +{0.901736845921875, 0.879726983984375}, +{0.708719649968750, 0.453909481265625}, +{0.415337952218750, 0.849024716109375}, +{0.134853249937500, 0.353826861265625}, +{0.903787097500000, 0.267080047484375}, +{0.479025812484375, 0.572831715312500}, +{0.876605124109375, 0.604345045187500}, +{0.456461826015625, 0.071001137000000}, +{0.709494562484375, 0.955644215312500}, +{0.231947383703125, 0.518907935984375}, +{0.932230392156250, 0.013327205875000}, +{0.145086204046875, 0.865883539265625}, +{0.350930456281250, 0.348899376265625}, +{0.725034838203125, 0.739106496203125}, +{0.739954645359375, 0.226971750750000}, +{0.042605235078125, 0.213450866640625}, +{0.811627065421875, 0.821665408953125}, +{0.587735274500000, 0.312719600875000}, +{0.307230392156250, 0.950827205875000}, +{0.217487057734375, 0.273792953031250}, +{0.854401308765625, 0.440081466656250}, +{0.384747995484375, 0.706561433531250}, +{0.899206688062500, 0.699456330843750}, +{0.334068937265625, 0.028193571656250}, +{0.576864665859375, 0.801662283093750}, +{0.041360180171875, 0.539240991515625}, +{0.780622165328125, 0.170435734406250}, +{0.025074889437500, 0.841885738250000}, +{0.412686740812500, 0.274763665078125}, +{0.551686781125000, 0.512870978171875}, +{0.574633261734375, 0.138224135796875}, +{0.058436791328125, 0.056280808593750}, +{0.822110274500000, 0.890844600875000}, +{0.618449504812500, 0.487462829328125}, +{0.264066255000000, 0.807028489734375}, +{0.132527922859375, 0.453355770328125}, +{0.807953992203125, 0.323742603312500}, +{0.302148254953125, 0.684077206343750}, +{0.794171695281250, 0.522748994546875}, +{0.372686140625000, 0.110004803828125}, +{0.507741981953125, 0.951245734125000}, +{0.107097304093750, 0.651192048015625}, +{0.822833622000000, 0.193689044750000}, +{0.181245437015625, 0.997161472968750}, +{0.384747995484375, 0.456561433531250}, +{0.662226998781250, 0.569583683906250}, +{0.727197083078125, 0.021632137984375}, +{0.184988661328125, 0.150492937687500}, +{0.873243045828125, 0.810942332640625}, +{0.684963615078125, 0.357045297593750}, +{0.461525611203125, 0.759528012437500}, +{0.025718904421875, 0.410817897421875}, +{0.897009311359375, 0.420948834468750}, +{0.263037063734375, 0.546019301578125}, +{0.857097304093750, 0.651192048015625}, +{0.252866199843750, 0.205957512640625}, +{0.665602115484375, 0.895166775859375}, +{0.056996963312500, 0.684992160312500}, +{0.918804934109375, 0.150309289718750}, +{0.019203528312500, 0.969887995546875}, +{0.485853633703125, 0.339220435984375}, +{0.509352115484375, 0.676416775859375}, +{0.564952190359375, 0.039350341546875}, +{0.061178846875000, 0.152720720890625}, +{0.979027962734375, 0.850901043359375}, +{0.618986693593750, 0.251067502968750}, +{0.416788412578125, 0.890801763843750}, +{0.191935423609375, 0.392279210781250}, +{0.959424527437500, 0.448127289609375}, +{0.360967390625000, 0.582538983515625}, +{0.802390435203125, 0.744472166265625}, +{0.498617226093750, 0.214032599359375}, +{0.542495165859375, 0.838147345281250}, +{0.211457644609375, 0.742513028953125}, +{0.837488317609375, 0.030941206375000}, +{0.038430456281250, 0.786399376265625}, +{0.290324504812500, 0.370275329328125}, +{0.678301531546875, 0.667673458078125}, +{0.510613942796875, 0.106955928328125}, +{0.170736691484375, 0.235298081343750}, +{0.759083993796875, 0.997656627843750}, +{0.539572313656250, 0.380208463203125}, +{0.255388875390625, 0.867381653203125}, +{0.071379264859375, 0.354136628500000}, +{0.758860189078125, 0.404241883828125}, +{0.420321786390625, 0.636298924375000}, +{0.978659227718750, 0.748372525703125}, +{0.287503755000000, 0.010153489734375}, +{0.739679912484375, 0.870313307812500}, +{0.089315978250000, 0.513406141265625}, +{0.943869562484375, 0.080155934062500}, +{0.076564677375000, 0.913381656000000}, +{0.444542439093750, 0.459099133796875}, +{0.633162999796875, 0.540307445062500}, +{0.709818581500000, 0.157887002984375}, +{0.167325380828125, 0.046656456390625}, +{0.977987853250000, 0.896218641265625}, +{0.652229645515625, 0.427815757218750}, +{0.492972183375000, 0.993769330593750}, +{0.040505847500000, 0.272359588500000}, +{0.962978249937500, 0.260076861265625}, +{0.302640369765625, 0.741357903265625}, +{0.768208405500000, 0.610922261187500}, +{0.318273390046875, 0.193320190000000}, +{0.619905641343750, 0.853941035859375}, +{0.181258709546875, 0.747276624031250}, +{0.757229657953125, 0.013359518093750}, +{0.244715387546875, 0.985882883562500}, +{0.259008847734375, 0.494543413890625}, +{0.554055652187500, 0.714498260375000}, +{0.534027961468750, 0.219931745984375}, +{0.069780017046875, 0.113865176609375}, +{0.848982478109375, 0.872119342578125}, +{0.713196808109375, 0.271365323640625}, +{0.363736251062500, 0.991491573531250}, +{0.150433761421875, 0.282401366203125}, +{0.901208663437500, 0.331649132359375}, +{0.463675308375000, 0.681269330593750}, +{0.949633261734375, 0.606974135796875}, +{0.384851754687500, 0.208333852843750}, +{0.668614601359375, 0.752828221531250}, +{0.181245437015625, 0.622161472968750}, +{0.895086204046875, 0.115883539265625}, +{0.193869562484375, 0.830155934062500}, +{0.387335340921875, 0.396347453890625}, +{0.564854406031250, 0.586633136296875}, +{0.745691442609375, 0.089534956375000}, +{0.245486845921875, 0.129726983984375}, +{0.982811359250000, 0.811790368250000}, +{0.536503468843750, 0.474657913296875}, +{0.489679912484375, 0.870313307812500}, +{0.067888875390625, 0.492381653203125}, +{0.751398662484375, 0.307813307812500}, +{0.352987853250000, 0.521218641265625}, +{0.869468904421875, 0.543630397421875}, +{0.400000938734375, 0.102147747421875}, +{0.716287097500000, 0.892080047484375}, +{0.204945004250000, 0.590966294140625}, +{0.883506990359375, 0.242482936484375}, +{0.065143307734375, 0.844593734281250}, +{0.327001686515625, 0.299140218421875}, +{0.748446786390625, 0.573798924375000}, +{0.665686274500000, 0.213235294109375}, +{0.115683153500000, 0.178056211000000}, +{0.757883424281250, 0.796209072156250}, +{0.713222390562500, 0.397222857359375}, +{0.362679025703125, 0.845586141078125}, +{0.147009311359375, 0.420948834468750}, +{0.954472218843750, 0.404345413296875}, +{0.363836204046875, 0.740883539265625}, +{0.821904890625000, 0.583637616328125}, +{0.492832801906250, 0.029377324812500}, +{0.599161986109375, 0.929388585781250}, +{0.096331207625000, 0.695634299281250}, +{0.848982478109375, 0.122119342578125}, +{0.099003468843750, 0.966845413296875}, +{0.462431749687500, 0.267395920968750}, +{0.677354460328125, 0.725544471656250}, +{0.650008709546875, 0.122276624031250}, +{0.123243045828125, 0.060942332640625}, +{0.822833622000000, 0.943689044750000}, +{0.586591777984375, 0.432717372437500}, +{0.408903485687500, 0.776738982078125}, +{0.080479406031250, 0.305383136296875}, +{0.818079645359375, 0.414471750750000}, +{0.431931985968750, 0.513270723078125}, +{0.903169756421875, 0.555146535265625}, +{0.285517059468750, 0.166546514046875}, +{0.675219736453125, 0.976237158906250}, +{0.238942076937500, 0.643155269500000}, +{0.964063133281250, 0.229178170375000}, +{0.192731703031250, 0.935110961484375}, +{0.348181288187500, 0.401962688265625}, +{0.618003220203125, 0.658636770343750}, +{0.588190877593750, 0.199420555781250}, +{0.216958200468750, 0.007344331203125}, +{0.883506990359375, 0.992482936484375}, +{0.636042923812500, 0.294749748625000}, +{0.481304934109375, 0.900309289718750}, +{0.237679025703125, 0.470586141078125}, +{0.831750421625000, 0.262285054609375}, +{0.341264392046875, 0.675388614109375}, +{0.864509651765625, 0.725386922921875}, +{0.481304934109375, 0.150309289718750}, +{0.507180456281250, 0.786399376265625}, +{0.033602444796875, 0.600612049781250}, +{0.962250867203125, 0.054211353312500}, +{0.151703992203125, 0.761242603312500}, +{0.261464799453125, 0.261330050531250}, +{0.740667841375000, 0.511552886640625}, +{0.615093996609375, 0.088785852218750}, +{0.228861550250000, 0.193531534234375}, +{0.920420940359375, 0.853803466546875}, +{0.541111850968750, 0.345010423484375}, +{0.305472183375000, 0.900019330593750}, +{0.044319650703125, 0.478886922328125}, +{0.892725152796875, 0.482850753109375}, +{0.490667841375000, 0.511552886640625}, +{0.971780261562500, 0.629537322875000}, +{0.387554934109375, 0.056559289718750}, +{0.662939186515625, 0.814765218421875}, +{0.052390435203125, 0.744472166265625}, +{0.826564677375000, 0.163381656000000}, +{0.103197227578125, 0.768215064734375}, +{0.306265020015625, 0.415613958984375}, +{0.575564970921875, 0.526211358984375}, +{0.505411986109375, 0.183294835781250}, +{0.079233855890625, 0.230882302687500}, +{0.939854406031250, 0.961633136296875}, +{0.747488661328125, 0.369242937687500}, +{0.399926992500000, 0.954583567718750}, +{0.122446606265625, 0.392854040234375}, +{0.987719736453125, 0.351237158906250}, +{0.425219736453125, 0.601237158906250}, +{0.774082801906250, 0.642658574812500}, +{0.416299322109375, 0.161398788656250}, +{0.602987853250000, 0.989968641265625}, +{0.089514399500000, 0.575051700359375}, +{0.786335975187500, 0.240626511406250}, +{0.231815968843750, 0.794970413296875}, +{0.318750014656250, 0.443002308546875}, +{0.505566445812500, 0.623698635640625}, +{0.637067640531250, 0.039603148984375}, +{0.180883847734375, 0.072668413890625}, +{0.917325380828125, 0.796656456390625}, +{0.526756746078125, 0.259060873750000}, +{0.334878599875000, 0.893762925093750}, +{0.225425998046875, 0.315310650828125}, +{0.794319650703125, 0.478886922328125}, +{0.445957801906250, 0.724689824812500}, +{0.915440720703125, 0.656963966125000}, +{0.302640369765625, 0.116357903265625}, +{0.698976641187500, 0.774455388406250}, +{0.177148254953125, 0.684077206343750}, +{0.949633261734375, 0.138224135796875}, +{0.045314677375000, 0.913381656000000}, +{0.458038607125000, 0.401547691687500}, +{0.713980484156250, 0.625879926296875}, +{0.643255797593750, 0.155068738921875}, +{0.025074889437500, 0.091885738250000}, +{0.766702341031250, 0.927996303765625}, +{0.664447403859375, 0.489416693968750}, +{0.352787232453125, 0.772961294593750}, +{0.025478249937500, 0.320623736265625}, +{0.854074830531250, 0.352849697156250}, +{0.286530910140625, 0.622227763453125}, +{0.977727943875000, 0.553082892328125}, +{0.350685461906250, 0.153596186453125}, +{0.557230392156250, 0.880514705875000}, +{0.153169756421875, 0.555146535265625}, +{0.789461819796875, 0.116237049781250}, +{0.168804934109375, 0.900309289718750}, +{0.377355421296875, 0.330038940000000}, +{0.612679025703125, 0.720586141078125}, +{0.508506990359375, 0.054982936484375}, +{0.196917624109375, 0.096532545187500}, +{0.910867175625000, 0.931752899046875}, +{0.728171685203125, 0.443690916265625}, +{0.435674328421875, 0.845052115796875}, +{0.182230392156250, 0.325827205875000}, +{0.884087952218750, 0.286524716109375}, +{0.448834987281250, 0.579381097156250}, +{0.884436274500000, 0.588235294109375}, +{0.439752211921875, 0.123635853468750}, +{0.688559795171875, 0.981591765453125}, +{0.198151308765625, 0.522112716656250}, +{0.901703992203125, 0.011242603312500}, +{0.128530229140625, 0.840431613031250}, +{0.317826892046875, 0.321872989109375}, +{0.696508847734375, 0.744543413890625}, +{0.688847218843750, 0.216845413296875}, +{0.004724588125000, 0.188791578953125}, +{0.787802165859375, 0.832912283093750}, +{0.567052101359375, 0.371480565281250}, +{0.302148254953125, 0.996577206343750}, +{0.243986693593750, 0.251067502968750}, +{0.825795788375000, 0.474201552750000}, +{0.430456688062500, 0.699456330843750}, +{0.931258709546875, 0.747276624031250}, +{0.325564970921875, 0.057461358984375}, +{0.575724486109375, 0.777044835781250}, +{0.058436791328125, 0.525030808593750}, +{0.759808761421875, 0.157401366203125}, +{0.010191088203125, 0.862153371203125}, +{0.411476654468750, 0.296344298265625}, +{0.505607996843750, 0.519919121093750}, +{0.563898662484375, 0.182813307812500}, +{0.007229657953125, 0.013359518093750}, +{0.826564677375000, 0.913381656000000}, +{0.615744562484375, 0.440019215312500}, +{0.273049196593750, 0.752824731078125}, +{0.126134788453125, 0.489345154640625}, +{0.776528750687500, 0.346344691359375}, +{0.271093019843750, 0.673808825390625}, +{0.766504140937500, 0.549462718109375}, +{0.321690920375000, 0.115305748390625}, +{0.552058600328125, 0.980660703968750}, +{0.086158139312500, 0.677526975812500}, +{0.868983666328125, 0.196905808593750}, +{0.133506990359375, 0.992482936484375}, +{0.428389216390625, 0.479768192062500}, +{0.642183234343750, 0.594837245046875}, +{0.693414534828125, 0.060006743953125}, +{0.177263875390625, 0.179881653203125}, +{0.822881453125000, 0.799457928828125}, +{0.657602996515625, 0.342698708937500}, +{0.447833622000000, 0.795251544750000}, +{0.049908315421875, 0.432993533953125}, +{0.884744019140625, 0.384203634359375}, +{0.303495121031250, 0.531469981562500}, +{0.829156508796875, 0.635903919875000}, +{0.279003945812500, 0.225261135640625}, +{0.626154293906250, 0.912625724750000}, +{0.009546365468750, 0.639961546609375}, +{0.886403666453125, 0.169165570765625}, +{0.052259883703125, 0.979845435984375}, +{0.459494562484375, 0.330644215312500}, +{0.524813081953125, 0.651309302156250}, +{0.606787063734375, 0.014769301578125}, +{0.010457836296875, 0.143541028515625}, +{0.947626461875000, 0.818089897125000}, +{0.571917624109375, 0.284032545187500}, +{0.416299322109375, 0.911398788656250}, +{0.236033046906250, 0.396646543203125}, +{0.990797719390625, 0.456105138953125}, +{0.333626471656250, 0.616508772390625}, +{0.787209695250000, 0.716482502812500}, +{0.439756778562500, 0.194376370593750}, +{0.552179912484375, 0.863477370312500}, +{0.243449504812500, 0.737462829328125}, +{0.822881453125000, 0.049457928828125}, +{0.058436791328125, 0.806280808593750}, +{0.267237456671875, 0.347940860375000}, +{0.659100916609375, 0.628228892687500}, +{0.525109796625000, 0.082597554609375}, +{0.135951233515625, 0.201639822421875}, +{0.761255117500000, 0.954583567718750}, +{0.556490320328125, 0.399823343937500}, +{0.258466777984375, 0.838967372437500}, +{0.104074830531250, 0.352849697156250}, +{0.803389216390625, 0.386018192062500}, +{0.392068237890625, 0.666186056234375}, +{0.984505036140625, 0.690144858781250}, +{0.277932351500000, 0.052908284062500}, +{0.713196808109375, 0.833865323640625}, +{0.102915496515625, 0.537034646437500}, +{0.979027962734375, 0.100901043359375}, +{0.106115002812500, 0.885378765484375}, +{0.473302101359375, 0.455953221531250}, +{0.685199429843750, 0.551526284734375}, +{0.706158315421875, 0.128306033953125}, +{0.147265783328125, 0.057294542578125}, +{0.959039534828125, 0.919381743953125}, +{0.646013875390625, 0.398631653203125}, +{0.458517234312500, 0.944548392906250}, +{0.056758487734375, 0.295253452109375}, +{0.988836204046875, 0.303383539265625}, +{0.254309364156250, 0.711985215093750}, +{0.775799306890625, 0.567053766171875}, +{0.340921091031250, 0.224871303765625}, +{0.620506746078125, 0.821560873750000}, +{0.160012464359375, 0.690720392781250}, +{0.788430456281250, 0.036399376265625}, +{0.220355124109375, 0.963720045187500}, +{0.287648582281250, 0.493282790234375}, +{0.536503468843750, 0.724657913296875}, +{0.552148254953125, 0.246577206343750}, +{0.116843560203125, 0.107753416265625}, +{0.842374240812500, 0.843123040078125}, +{0.696912145562500, 0.310820697562500}, +{0.330881907437500, 0.958779769828125}, +{0.169338615078125, 0.306386117906250}, +{0.927058600328125, 0.355660703968750}, +{0.477915496515625, 0.630784646437500}, +{0.947107614062500, 0.571599844062500}, +{0.411298019234375, 0.231101317375000}, +{0.634196201015625, 0.774126137000000}, +{0.133506990359375, 0.617482936484375}, +{0.911148929828125, 0.091845178421875}, +{0.229027962734375, 0.850901043359375}, +{0.433436791328125, 0.431280808593750}, +{0.570900611203125, 0.618903012437500}, +{0.691994851500000, 0.099783284062500}, +{0.212500058671875, 0.147009234203125}, +{0.951732996484375, 0.784567680765625}, +{0.538479240078125, 0.442006235093750}, +{0.495691442609375, 0.839534956375000}, +{0.092295940359375, 0.445600341546875}, +{0.806758487734375, 0.295253452109375}, +{0.374367956281250, 0.505149376265625}, +{0.822584307093750, 0.538276272453125}, +{0.381143881328125, 0.075372076343750}, +{0.746648411546875, 0.886075859718750}, +{0.225690524703125, 0.572657414093750}, +{0.908135803781250, 0.224055233687500}, +{0.100557411375000, 0.856953939312500}, +{0.326749240812500, 0.257185540078125}, +{0.703382877203125, 0.594522854968750}, +{0.635381359671875, 0.247395865796875}, +{0.091503945812500, 0.170573635640625}, +{0.773093560203125, 0.773280760015625}, +{0.699864601359375, 0.424703221531250}, +{0.337978249937500, 0.822576861265625}, +{0.177278602781250, 0.415547689312500}, +{0.950716102781250, 0.382344564312500}, +{0.345102996515625, 0.701097146437500}, +{0.850941098000000, 0.563378403515625}, +{0.447833622000000, 0.045251544750000}, +{0.584417841375000, 0.933427886640625}, +{0.117581880000000, 0.710837083484375}, +{0.864601654468750, 0.093219298265625}, +{0.115674527437500, 0.963752289609375}, +{0.438901999203125, 0.285253217765625}, +{0.641702341031250, 0.693621303765625}, +{0.662939186515625, 0.064765218421875}, +{0.102763510390625, 0.047682223171875}, +{0.821904890625000, 0.958637616328125}, +{0.617697313656250, 0.380208463203125}, +{0.387554934109375, 0.806559289718750}, +{0.096212938656250, 0.263020963203125}, +{0.867422654015625, 0.379107524531250}, +{0.383386910937500, 0.555990102843750}, +{0.880607996843750, 0.519919121093750}, +{0.252252211921875, 0.162698353468750}, +{0.642183234343750, 0.969837245046875}, +{0.196583993796875, 0.685156627843750}, +{0.963190877593750, 0.199420555781250}, +{0.199633261734375, 0.888224135796875}, +{0.350685461906250, 0.434846186453125}, +{0.614399749906250, 0.627404791062500}, +{0.608747165328125, 0.225123234406250}, +{0.244958663437500, 0.019149132359375}, +{0.880021551015625, 0.966470884812500}, +{0.686627065421875, 0.259165408953125}, +{0.463246963312500, 0.880304660312500}, +{0.209424527437500, 0.448127289609375}, +{0.850148582281250, 0.305782790234375}, +{0.321802423796875, 0.647870625703125}, +{0.853921568625000, 0.709558823531250}, +{0.496648411546875, 0.136075859718750}, +{0.534266910125000, 0.759900787234375}, +{0.018208405500000, 0.610922261187500}, +{0.981815968843750, 0.044970413296875}, +{0.147265783328125, 0.807294542578125}, +{0.278764594718750, 0.293912076453125}, +{0.697168203437500, 0.508447093109375}, +{0.569936479140625, 0.090431613031250}, +{0.199633261734375, 0.231974135796875}, +{0.878530229140625, 0.840431613031250}, +{0.530111691484375, 0.313423081343750}, +{0.287503755000000, 0.877340989734375}, +{0.020926812156250, 0.490443845953125}, +{0.925118529859375, 0.463551966546875}, +{0.460281853234375, 0.514086682671875}, +{0.946583993796875, 0.685156627843750}, +{0.432953992203125, 0.011242603312500}, +{0.681996963312500, 0.841242160312500}, +{0.035440756328125, 0.741967535328125}, +{0.826814113265625, 0.129551982203125}, +{0.102763510390625, 0.797682223171875}, +{0.257250986453125, 0.431315283906250}, +{0.601735045687500, 0.535855464812500}, +{0.523947313656250, 0.161458463203125}, +{0.089514399500000, 0.200051700359375}, +{0.998871711468750, 0.969931745984375}, +{0.725557411375000, 0.325703939312500}, +{0.411054041609375, 0.940728892687500}, +{0.092214949156250, 0.395649388656250}, +{0.947571808109375, 0.318240323640625}, +{0.385150528859375, 0.598791693968750}, +{0.760340045031250, 0.666060247875000}, +{0.385468447859375, 0.173477959078125}, +{0.619715387546875, 0.985882883562500}, +{0.110693313734375, 0.604613051578125}, +{0.759083993796875, 0.247656627843750}, +{0.198151308765625, 0.803362716656250}, +{0.368295322046875, 0.475490672062500}, +{0.522431987421875, 0.579676484406250}, +{0.668614601359375, 0.002828221531250}, +{0.159061291453125, 0.115786836312500}, +{0.885343915375000, 0.797979216453125}, +{0.502049350968750, 0.290322923484375}, +{0.361960011265625, 0.916515061968750}, +{0.212257727531250, 0.374306940859375}, +{0.808006746078125, 0.446560873750000}, +{0.489786425109375, 0.696416160921875}, +{0.914775229140625, 0.625807223171875}, +{0.272394756234375, 0.082105300000000}, +{0.697833622000000, 0.795251544750000}, +{0.146614411140625, 0.654378257218750}, +{0.959039534828125, 0.169381743953125}, +{0.056931985968750, 0.888270723078125}, +{0.493765020015625, 0.415613958984375}, +{0.727875617718750, 0.674521589734375}, +{0.646232996843750, 0.172262871093750}, +{0.052267234312500, 0.085173392906250}, +{0.794171695281250, 0.897748994546875}, +{0.664407018781250, 0.458335411421875}, +{0.318273390046875, 0.755820190000000}, +{0.046247165328125, 0.350123234406250}, +{0.865799202015625, 0.333466330906250}, +{0.285498339406250, 0.583855022421875}, +{0.956147102093750, 0.505576277234375}, +{0.318949826718750, 0.141763441546875}, +{0.510429611953125, 0.887119489765625}, +{0.184919978109375, 0.561328326953125}, +{0.759417624109375, 0.096532545187500}, +{0.130403602781250, 0.937032064312500}, +{0.394595719296875, 0.367380713734375}, +{0.567731703031250, 0.747610961484375}, +{0.538716806515625, 0.039836274843750} +}; \ No newline at end of file diff --git a/22_RaytracedAO/Renderer.h b/22_RaytracedAO/Renderer.h index 81e38ac7f..61190bedf 100644 --- a/22_RaytracedAO/Renderer.h +++ b/22_RaytracedAO/Renderer.h @@ -7,7 +7,6 @@ #undef PI #include "nbl/ext/MitsubaLoader/CMitsubaLoader.h" -#include "nbl/ext/EnvmapImportanceSampling/EnvmapImportanceSampling.h" #include @@ -24,39 +23,38 @@ class Renderer : public nbl::core::IReferenceCounted, public nbl::core::Interfac public: #include "rasterizationCommon.h" #include "raytraceCommon.h" + #ifdef __cplusplus + #undef uint + #undef vec4 + #undef mat4 + #undef mat4x3 + #endif + struct DenoiserArgs + { + std::filesystem::path bloomFilePath; + float bloomScale = 0.0f; + float bloomIntensity = 0.0f; + std::string tonemapperArgs = ""; + }; - Renderer(nbl::video::IVideoDriver* _driver, nbl::asset::IAssetManager* _assetManager, nbl::scene::ISceneManager* _smgr, bool deferDenoise, bool useDenoiser = true); + Renderer(nbl::video::IVideoDriver* _driver, nbl::asset::IAssetManager* _assetManager, nbl::scene::ISceneManager* _smgr, bool useDenoiser = true); void initSceneResources(nbl::asset::SAssetBundle& meshes, nbl::io::path&& _sampleSequenceCachePath=""); void deinitSceneResources(); - - void deinitRenderer(); - - void finalizeDeferredDenoise(); - void initScreenSizedResources( - const uint32_t width, const uint32_t height, - const float envMapRegularizationFactor, - int32_t cascadeCount, - float cascadeLuminanceBase, - float cascadeLuminanceStart, - const float Emin, - const nbl::core::vector& clipPlanes={} - ); + void initScreenSizedResources(uint32_t width, uint32_t height); + + void deinitScreenSizedResources(); void resetSampleAndFrameCounters(); - void takeAndSaveScreenShot(const std::filesystem::path& screenshotFilePath, bool denoise, const DenoiserArgs& denoiserArgs = {}); + void takeAndSaveScreenShot(const std::filesystem::path& screenshotFilePath, bool denoise = false, const DenoiserArgs& denoiserArgs = {}); - void denoiseCubemapFaces( - std::filesystem::path filePaths[6], - const std::string& mergedFileName, - int32_t cropOffsetX, int32_t cropOffsetY, int32_t cropWidth, int32_t cropHeight, - const DenoiserArgs& denoiserArgs = {}); + void denoiseCubemapFaces(std::filesystem::path filePaths[6], const std::string& mergedFileName, int borderPixels, const DenoiserArgs& denoiserArgs = {}); - bool render(nbl::ITimer* timer, const float kappa, const float Emin, const bool transformNormals, const bool beauty=true); + bool render(nbl::ITimer* timer, const bool transformNormals, const bool beauty=true); auto* getColorBuffer() { return m_colorBuffer; } @@ -73,7 +71,7 @@ class Renderer : public nbl::core::IReferenceCounted, public nbl::core::Interfac } uint64_t getTotalSamplesComputed() const { - const auto samplesPerDispatch = static_cast(getSamplesPerPixelPerDispatch()*m_staticViewData.imageDimensions[1])*m_staticViewData.imageDimensions[0]; + const auto samplesPerDispatch = getSamplesPerPixelPerDispatch()*static_cast(m_staticViewData.imageDimensions.x*m_staticViewData.imageDimensions.y); const auto framesDispatched = static_cast(m_framesDispatched); return framesDispatched*samplesPerDispatch; } @@ -126,7 +124,6 @@ class Renderer : public nbl::core::IReferenceCounted, public nbl::core::Interfac void finalizeScene(InitializationData& initData); // - nbl::core::smart_refctd_ptr createTexture(uint32_t width, uint32_t height, nbl::asset::E_FORMAT format, uint32_t mipLevels=1u, uint32_t layers=0u); nbl::core::smart_refctd_ptr createScreenSizedTexture(nbl::asset::E_FORMAT format, uint32_t layers=0u); // @@ -138,8 +135,6 @@ class Renderer : public nbl::core::IReferenceCounted, public nbl::core::Interfac // "constants" bool m_useDenoiser; - const bool m_deferDenoise; - static constexpr char* DEFER_DENOISE_HOOK_FILE_NAME = "defer_denoise_hook.bat"; // managers nbl::video::IVideoDriver* m_driver; @@ -149,8 +144,6 @@ class Renderer : public nbl::core::IReferenceCounted, public nbl::core::Interfac nbl::core::smart_refctd_ptr m_rrManager; - // - std::fstream m_deferDenoiseFile; // persistent (intialized in constructor nbl::core::smart_refctd_ptr m_rayCountBuffer,m_littleDownloadBuffer; @@ -158,6 +151,7 @@ class Renderer : public nbl::core::IReferenceCounted, public nbl::core::Interfac nbl::core::smart_refctd_ptr m_perCameraRasterDSLayout; nbl::core::smart_refctd_ptr m_rasterInstanceDataDSLayout,m_additionalGlobalDSLayout,m_commonRaytracingDSLayout; nbl::core::smart_refctd_ptr m_raygenDSLayout,m_closestHitDSLayout,m_resolveDSLayout; + nbl::core::smart_refctd_ptr m_visibilityBufferFillPipeline; nbl::core::smart_refctd_ptr m_cullPipelineLayout; nbl::core::smart_refctd_ptr m_raygenPipelineLayout; @@ -165,11 +159,10 @@ class Renderer : public nbl::core::IReferenceCounted, public nbl::core::Interfac nbl::core::smart_refctd_ptr m_resolvePipelineLayout; nbl::core::smart_refctd_ptr m_cullGPUShader; - nbl::core::smart_refctd_ptr m_vertGPUShader,m_fragGPUShader; nbl::core::smart_refctd_ptr m_raygenGPUShader; nbl::core::smart_refctd_ptr m_closestHitGPUShader; nbl::core::smart_refctd_ptr m_resolveGPUShader; - + // semi persistent data nbl::io::path sampleSequenceCachePath; struct SampleSequence @@ -192,12 +185,18 @@ class Renderer : public nbl::core::IReferenceCounted, public nbl::core::Interfac private: nbl::core::smart_refctd_ptr bufferView; } sampleSequence; + uint16_t pathDepth; + uint16_t noRussianRouletteDepth; + uint32_t maxSensorSamples; + + // scene specific data + nbl::core::vector<::RadeonRays::Shape*> rrShapes; + nbl::core::vector<::RadeonRays::Shape*> rrInstances; nbl::core::matrix3x4SIMD m_prevView; nbl::core::matrix4x3 m_prevCamTform; nbl::core::aabbox3df m_sceneBound; uint32_t m_framesDispatched; - float m_maxAreaLightLuma; vec2 m_rcpPixelSize; uint64_t m_totalRaysCast; StaticViewData_t m_staticViewData; @@ -221,12 +220,26 @@ class Renderer : public nbl::core::IReferenceCounted, public nbl::core::Interfac nbl::core::smart_refctd_ptr m_commonRaytracingDS[2]; nbl::core::smart_refctd_ptr m_rasterInstanceDataDS,m_raygenDS,m_resolveDS; nbl::core::smart_refctd_ptr m_closestHitDS[2]; + uint32_t m_raygenWorkGroups[2]; - nbl::video::IFrameBuffer* m_colorBuffer; + struct InteropBuffer + { + nbl::core::smart_refctd_ptr buffer; + std::pair<::RadeonRays::Buffer*, cl_mem> asRRBuffer = { nullptr,0u }; + }; + InteropBuffer m_rayBuffer[2]; + InteropBuffer m_intersectionBuffer[2]; + nbl::core::smart_refctd_ptr m_accumulation,m_tonemapOutput; + nbl::core::smart_refctd_ptr m_albedoAcc,m_albedoRslv; + nbl::core::smart_refctd_ptr m_normalAcc,m_normalRslv; + nbl::video::IFrameBuffer* m_visibilityBuffer,* m_colorBuffer; - // Resources used for envmap sampling - nbl::core::smart_refctd_ptr m_finalEnvmap; - nbl::ext::EnvmapImportanceSampling::EnvmapImportanceSampling m_envMapImportanceSampling; + // Resources used for blending environmental maps + nbl::core::smart_refctd_ptr blendEnvPipeline; + nbl::core::smart_refctd_ptr blendEnvDescriptorSet; + nbl::core::smart_refctd_ptr blendEnvMeshBuffer; + + nbl::core::smart_refctd_ptr m_finalEnvmap; std::future compileShadersFuture; }; diff --git a/22_RaytracedAO/addEnvironmentEmitters.comp b/22_RaytracedAO/addEnvironmentEmitters.comp deleted file mode 100644 index dc303c5a7..000000000 --- a/22_RaytracedAO/addEnvironmentEmitters.comp +++ /dev/null @@ -1,51 +0,0 @@ -#version 430 core - -#include "common.h" -layout(local_size_x = WORKGROUP_DIM, local_size_y = WORKGROUP_DIM) in; - -layout(set = 0, binding = 0, row_major) readonly buffer Params -{ - vec3 baseColor; - uint inputCount; - mat4x3 reorientations[]; -}; -layout(set = 0, binding = 1, r32ui) restrict uniform uimage2D outEnvMap; -layout(set = 0, binding = 2) uniform sampler2D inEnvMaps[MAX_SAMPLERS_COMPUTE]; - -#include -#include -#include - -vec3 directionFromMitsubaUVCoord(in vec2 uv) -{ - float dummy; - return mitsubaEnvmapToWorldSpace(nbl_glsl_sampling_envmap_directionFromUVCoord(uv,dummy)); -} - -void main() -{ - const ivec2 pixelCoord = ivec2(gl_GlobalInvocationID.xy); - const ivec2 dstSize = imageSize(outEnvMap); - if (all(lessThan(pixelCoord,dstSize))) - { - vec3 result = baseColor; - - const vec2 origUV = vec2(pixelCoord)/vec2(dstSize); - const mat3 dir = mat3( - directionFromMitsubaUVCoord(origUV+vec2(0.05,0.5)/vec2(dstSize)), - directionFromMitsubaUVCoord(origUV+vec2(0.5,0.05)/vec2(dstSize)), - directionFromMitsubaUVCoord(origUV+vec2(0.5)/vec2(dstSize)) - ); - for (uint i=0; i - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/22_RaytracedAO/closestHit.comp b/22_RaytracedAO/closestHit.comp index 85d746b54..4eaea39f9 100644 --- a/22_RaytracedAO/closestHit.comp +++ b/22_RaytracedAO/closestHit.comp @@ -10,7 +10,7 @@ layout(local_size_x = WORKGROUP_SIZE) in; uint get_path_vertex_depth() { - return bitfieldExtract(pc.cummon.pathDepth_rayCountWriteIx,0,RAYCOUNT_SHIFT); + return pc.cummon.depth; } #include @@ -23,16 +23,15 @@ layout(set = 3, binding = 1, std430) restrict buffer Queries nbl_glsl_ext_RadeonRays_Intersection intersections[]; }; + bool get_sample_job() { - return gl_GlobalInvocationID.x>RAYCOUNT_SHIFT]; + return gl_GlobalInvocationID.x=0.f; + vec3 geomNormal; + const vec3 lastVxPos = load_positions(geomNormal,batchInstanceData,indices); + + const bool frontfacing = bool((batchInstanceData.determinantSignBit^floatBitsToUint(dot(normalizedV,geomNormal)))&0x80000000u); // get material const nbl_glsl_MC_oriented_material_t material = nbl_glsl_MC_material_data_t_getOriented(batchInstanceData.material,frontfacing); - contrib.color = contrib.albedo = nbl_glsl_MC_oriented_material_t_getEmissive(material, normalizedV); + contrib.color = contrib.albedo = nbl_glsl_MC_oriented_material_t_getEmissive(material); - const uint maxPathDepth = bitfieldExtract(staticViewData.maxPathDepth_noRussianRouletteDepth_samplesPerPixelPerDispatch,0,8); - const bool _continue = vertex_depth!=maxPathDepth && material.genchoice_count!=0u && ray.maxT==nbl_glsl_FLT_MAX; // not last vertex and has a BxDF and not NEE path + const uint pathDepth = bitfieldExtract(staticViewData.pathDepth_noRussianRouletteDepth_samplesPerPixelPerDispatch,0,8); + const bool _continue = vertex_depth!=pathDepth && material.genchoice_count!=0u && ray.maxT==nbl_glsl_FLT_MAX; // not last vertex and has a BxDF and not NEE path if (_continue) { // if we ever support spatially varying emissive, we'll need to hoist barycentric computation and UV fetching to the position fetching const vec2 compactBary = vec2(1.f-intersection.uv.x-intersection.uv.y,intersection.uv.x); // radeon rays is a special boy and does its barycentrics weird - + // const nbl_glsl_xoroshiro64star_state_t scramble_start_state = nbl_glsl_xoroshiro64star_state_t(ray.mask,ray._active); - + // normalizedN = load_normal_and_prefetch_textures( - batchInstanceData,indices,compactBary,material - #ifdef TEX_PREFETCH_STREAM + batchInstanceData,indices,compactBary,geomNormal,material +#ifdef TEX_PREFETCH_STREAM ,mat2(0.0) // TODO: Covariance Rendering - #endif +#endif ); - + const vec3 origin = dPdBary*compactBary+lastVxPos; - rayMask = generate_next_rays( + generate_next_rays( MAX_RAYS_GENERATED,material,frontfacing,vertex_depth, scramble_start_state,sampleID,outPixelLocation,origin, throughput,aovThroughputScale,contrib.albedo,contrib.worldspaceNormal ); } else - contrib.worldspaceNormal = normalizedG*nbl_glsl_MC_colorToScalar(contrib.albedo); + contrib.worldspaceNormal = geomNormal*nbl_glsl_MC_colorToScalar(contrib.albedo); } else Contribution_initMiss(contrib,aovThroughputScale); Contribution_normalizeAoV(contrib); - const uint samplesPerPixelPerDispatch = bitfieldExtract(staticViewData.maxPathDepth_noRussianRouletteDepth_samplesPerPixelPerDispatch,16,16); + const uint samplesPerPixelPerDispatch = bitfieldExtract(staticViewData.pathDepth_noRussianRouletteDepth_samplesPerPixelPerDispatch,16,16); const uvec3 accumulationLocation = uvec3(outPixelLocation,sampleID%samplesPerPixelPerDispatch); + const vec3 acc_emissive = fetchAccumulation(accumulationLocation); + const vec3 acc_albedo = fetchAlbedo(accumulationLocation); + const vec3 acc_worldspaceNormal = fetchWorldspaceNormal(accumulationLocation); // TODO: finish MIS - contrib.color *= throughput; + storeAccumulation(acc_emissive,contrib.color*throughput,accumulationLocation); const vec3 aovThroughput = throughput*aovThroughputScale; - // - if (isRWMCEnabled()) - { - const bool pathToBeContinued = bool(rayMask); - if (pathToBeContinued) - addAccumulation(contrib.color,accumulationLocation); - else - { - // need whole path throughput when splatting - contrib.color += fetchAccumulation(accumulationLocation); - const nbl_glsl_RWMC_SplattingParameters splat = nbl_glsl_RWMC_getCascade(staticViewData.cascadeParams,nbl_glsl_MC_colorToScalar(contrib.color)/pc.cummon.rcpFramesDispatched); - for (uint j=0u; j<2u; j++) - addAccumulationCascade( - contrib.color*splat.cascadeWeights[j],accumulationLocation, - samplesPerPixelPerDispatch,splat.lowerCascade+j - ); - } - } - else - addAccumulation(contrib.color,accumulationLocation); - // - addAlbedo(contrib.albedo*aovThroughput,accumulationLocation); - addWorldspaceNormal(contrib.worldspaceNormal*nbl_glsl_MC_colorToScalar(aovThroughput),accumulationLocation); - // only misses contribute to transparency - if (bool(staticViewData.sampleSequenceStride_hideEnvmap>>31)) - { - float mask = 0.f; - if (!hit) - { - // make the luma of throughput dictate transparency - mask = dot(aovThroughput,transpose(nbl_glsl_sRGBtoXYZ)[1]); - // only count transmissions - const vec2 texCoordUV = (vec2(accumulationLocation.xy)+vec2(0.5))/vec2(getImageDimensions(staticViewData)); - const vec3 seeThroughDir = normalize(mat3(pc.cummon.viewDirReconFactors)*vec3(texCoordUV,1.f)); - mask *= pow(max(dot(normalizedV,seeThroughDir),0.f),1024.f); - } - addMask(mask,accumulationLocation); - } + storeAlbedo(acc_albedo,contrib.albedo*aovThroughput,accumulationLocation); + storeWorldspaceNormal(acc_worldspaceNormal,contrib.worldspaceNormal*nbl_glsl_MC_colorToScalar(aovThroughput),accumulationLocation); } } \ No newline at end of file diff --git a/22_RaytracedAO/common.h b/22_RaytracedAO/common.h index da7528213..600d2d8a0 100644 --- a/22_RaytracedAO/common.h +++ b/22_RaytracedAO/common.h @@ -2,15 +2,35 @@ #define _COMMON_INCLUDED_ -// for some reason Mitsuba uses Left-Handed coordinate system with Y-up for Envmaps -vec3 worldSpaceToMitsubaEnvmap(in vec3 worldSpace) -{ - return vec3(-worldSpace.z,worldSpace.xy); -} -vec3 mitsubaEnvmapToWorldSpace(in vec3 mitsubaEnvmaSpace) -{ - return vec3(mitsubaEnvmaSpace.yz,-mitsubaEnvmaSpace.x); -} +#define RAYCOUNT_N_BUFFERING 4 +#define RAYCOUNT_N_BUFFERING_MASK (RAYCOUNT_N_BUFFERING-1) + +#define MAX_TRIANGLES_IN_BATCH 16384 + +// need to bump to 2 in case of NEE + MIS, 3 in case of Path Guiding +#define SAMPLING_STRATEGY_COUNT 1 + + +#define WORKGROUP_SIZE 256 + + +#ifdef __cplusplus + #define uint uint32_t + struct uvec2 + { + uint x,y; + }; + struct vec2 + { + float x,y; + }; + struct vec3 + { + float x,y,z; + }; + #define vec4 nbl::core::vectorSIMDf + #define mat4 nbl::core::matrix4SIMD + #define mat4x3 nbl::core::matrix3x4SIMD #endif diff --git a/22_RaytracedAO/config.json.template b/22_RaytracedAO/config.json.template new file mode 100644 index 000000000..abfc8e387 --- /dev/null +++ b/22_RaytracedAO/config.json.template @@ -0,0 +1,28 @@ +{ + "enableParallelBuild": true, + "threadsPerBuildProcess" : 2, + "isExecuted": false, + "scriptPath": "", + "cmake": { + "configurations": [ "Release", "Debug", "RelWithDebInfo" ], + "buildModes": [], + "requiredOptions": [ "NBL_BUILD_MITSUBA_LOADER", "NBL_BUILD_RADEON_RAYS" ] + }, + "profiles": [ + { + "backend": "vulkan", + "platform": "windows", + "buildModes": [], + "runConfiguration": "Release", + "gpuArchitectures": [] + } + ], + "dependencies": [], + "data": [ + { + "dependencies": [], + "command": [""], + "outputs": [] + } + ] +} \ No newline at end of file diff --git a/22_RaytracedAO/cull.comp b/22_RaytracedAO/cull.comp new file mode 100644 index 000000000..7e214e555 --- /dev/null +++ b/22_RaytracedAO/cull.comp @@ -0,0 +1,74 @@ +#version 430 core + +#include "rasterizationCommon.h" +layout(local_size_x = WORKGROUP_SIZE) in; + +#include + +#include +layout(set=1, binding=0, row_major) writeonly restrict buffer PerInstancePerCamera +{ + DrawData_t data[]; +} instanceDataPerCamera; +layout(set=1, binding=1, std430, row_major) restrict readonly buffer PerInstanceCull +{ + CullData_t cullData[]; +}; +layout(set=1, binding=2, std430) restrict coherent buffer IndirectDraws +{ + nbl_glsl_DrawElementsIndirectCommand_t draws[]; +} commandBuff[2]; + + + +layout(push_constant, row_major) uniform PushConstants +{ + CullShaderData_t data; +} pc; + + + +#include +#include + + +// base instance remains unchanged +// we just do atomic add on the instance count +void main() +{ + for (uint drawCommandGUID=gl_GlobalInvocationID.x; drawCommandGUID=pc.data.maxGlobalInstanceCount) + return; + + // fetch instance data + const CullData_t batchInstanceData = cullData[batchInstanceID]; + const uint batchInstanceGUID = batchInstanceData.batchInstanceGUID; + + const nbl_glsl_ext_Mitsuba_Loader_instance_data_t instanceData = InstData.data[batchInstanceGUID]; + const mat4x3 worldMatrix = instanceData.tform; + const mat4 MVP = nbl_glsl_pseudoMul4x4with4x3(pc.data.viewProjMatrix,worldMatrix); + + // cull + bool notCulled = true; + if (false) + { + const mat2x3 bbox = mat2x3(batchInstanceData.aabbMinEdge,batchInstanceData.aabbMaxEdge); + notCulled = nbl_glsl_couldBeVisible(MVP,bbox); + } + + // set up MDI + if (notCulled) + { + const uint drawCommandGUID = batchInstanceData.drawCommandGUID; + const uint drawInstanceID = commandBuff[pc.data.currentCommandBufferIx].draws[drawCommandGUID].baseInstance+ + atomicAdd(commandBuff[pc.data.currentCommandBufferIx].draws[drawCommandGUID].instanceCount,1u); + + instanceDataPerCamera.data[drawInstanceID].MVP = MVP; + // use the MSB to denote if face orientation should be flipped + instanceDataPerCamera.data[drawInstanceID].backfacingBit_batchInstanceGUID = batchInstanceGUID|((instanceData.determinantSignBit^floatBitsToUint(pc.data.viewProjDeterminant))&0x80000000u); + instanceDataPerCamera.data[drawInstanceID].firstIndex = commandBuff[pc.data.currentCommandBufferIx].draws[drawCommandGUID].firstIndex; + } +} \ No newline at end of file diff --git a/22_RaytracedAO/denoiser_hook.bat b/22_RaytracedAO/denoiser_hook.bat index e6aefb328..b16cc08df 100644 --- a/22_RaytracedAO/denoiser_hook.bat +++ b/22_RaytracedAO/denoiser_hook.bat @@ -1,6 +1,6 @@ @echo off -set denoiser_dir="%~dp0../39.DenoiserTonemapper/bin" +set denoiser_dir="%~dp0../39_DenoiserTonemapper/bin" if NOT EXIST %denoiser_dir%/denoisertonemapper.exe ( echo BatchScriptError: Denoiser Executable does not exist. ^(at %denoiser%^) REM Don't Exit when denoiser is not found diff --git a/22_RaytracedAO/extractCubemap.bat b/22_RaytracedAO/extractCubemap.bat new file mode 100644 index 000000000..2719a8aff --- /dev/null +++ b/22_RaytracedAO/extractCubemap.bat @@ -0,0 +1,33 @@ +@echo off + +REM examplary usage: +REM mergeCubemap.bat 50 mergedImage.png right.png left.png top.png bottom.png front.png back.png + +set borderSz=%1 + +set img=%2 + +set right=%3 +set left=%4 +set top=%5 +set bottom=%6 +set front=%7 +set back=%8 + +REM set extracted image size +for /f "tokens=*" %%s in ('magick identify -format "%%w" %img%') do set sz=%%s +set /a imgSz=sz/3 +set /a extractedImgSz=imgSz-2*borderSz + +set /a x0 = borderSz +set /a x1 = imgSz+borderSz +set /a x2 = 2*imgSz+borderSz +set /a y0 = borderSz +set /a y1 = imgSz+borderSz + +magick convert %img% -crop %extractedImgSz%x%extractedImgSz%+%x0%+%y0% %right% +magick convert %img% -crop %extractedImgSz%x%extractedImgSz%+%x1%+%y0% %left% +magick convert %img% -crop %extractedImgSz%x%extractedImgSz%+%x2%+%y0% %top% +magick convert %img% -crop %extractedImgSz%x%extractedImgSz%+%x0%+%y1% %bottom% +magick convert %img% -crop %extractedImgSz%x%extractedImgSz%+%x1%+%y1% %front% +magick convert %img% -crop %extractedImgSz%x%extractedImgSz%+%x2%+%y1% %back% \ No newline at end of file diff --git a/22_RaytracedAO/fillVisBuffer.frag b/22_RaytracedAO/fillVisBuffer.frag new file mode 100644 index 000000000..9bce3dc26 --- /dev/null +++ b/22_RaytracedAO/fillVisBuffer.frag @@ -0,0 +1,36 @@ +// 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 +#version 460 core +#extension GL_EXT_shader_16bit_storage : require +#include + + +#define _NBL_GLSL_EXT_MITSUBA_LOADER_INSTANCE_DATA_BINDING_ 0 +#include "virtualGeometry.glsl" + +#include +layout(location = 2) flat in uint BackfacingBit_BatchInstanceGUID; +layout(location = 3) flat in uint drawCmdFirstIndex; + +uint nbl_glsl_barycentric_frag_getDrawID() {return BackfacingBit_BatchInstanceGUID&0x7fffffffu;} +vec3 nbl_glsl_barycentric_frag_getVertexPos(in uint batchInstanceGUID, in uint primID, in uint primsVx) +{ + const uint ix = nbl_glsl_VG_fetchTriangleVertexIndex(primID*3u+drawCmdFirstIndex,primsVx); + return nbl_glsl_fetchVtxPos(ix,InstData.data[batchInstanceGUID]); +} + + +layout(location = 0) out uvec4 frontFacingTriangleIDDrawID_unorm16Bary_dBarydScreenHalf2x2; // should it be called backfacing or frontfacing? + + +void main() +{ + vec2 bary = nbl_glsl_barycentric_frag_get(); + + const int triangleIDBitcount = findMSB(MAX_TRIANGLES_IN_BATCH-1)+1; + frontFacingTriangleIDDrawID_unorm16Bary_dBarydScreenHalf2x2[0] = bitfieldInsert(BackfacingBit_BatchInstanceGUID,gl_PrimitiveID,31-triangleIDBitcount,triangleIDBitcount)^(gl_FrontFacing ? 0x0u:0x80000000u); + frontFacingTriangleIDDrawID_unorm16Bary_dBarydScreenHalf2x2[1] = packUnorm2x16(bary); + frontFacingTriangleIDDrawID_unorm16Bary_dBarydScreenHalf2x2[2] = packHalf2x16(dFdx(bary)); + frontFacingTriangleIDDrawID_unorm16Bary_dBarydScreenHalf2x2[3] = packHalf2x16(dFdy(bary)); +} diff --git a/22_RaytracedAO/fillVisBuffer.vert b/22_RaytracedAO/fillVisBuffer.vert index eca6aa925..6c9279e02 100644 --- a/22_RaytracedAO/fillVisBuffer.vert +++ b/22_RaytracedAO/fillVisBuffer.vert @@ -10,8 +10,6 @@ #define _NBL_GLSL_EXT_MITSUBA_LOADER_INSTANCE_DATA_BINDING_ 0 #include "virtualGeometry.glsl" -#include "runtime_defines.glsl" - layout(set=2, binding=0, row_major) readonly restrict buffer PerInstancePerCamera { DrawData_t data[]; @@ -33,25 +31,4 @@ void main() const vec3 modelPos = nbl_glsl_fetchVtxPos(gl_VertexIndex,InstData.data[batchInstanceGUID]); nbl_glsl_barycentric_vert_set(modelPos); gl_Position = nbl_glsl_pseudoMul4x4with3x1(self.MVP,modelPos); - - // clipping -#ifdef CLIP_PLANE_0 - const vec4 worldPos = vec4(nbl_glsl_pseudoMul3x4with3x1(InstData.data[batchInstanceGUID].tform,modelPos),1.0); - gl_ClipDistance[0] = dot(CLIP_PLANE_0,worldPos); -#ifdef CLIP_PLANE_1 - gl_ClipDistance[1] = dot(CLIP_PLANE_1,worldPos); -#ifdef CLIP_PLANE_2 - gl_ClipDistance[2] = dot(CLIP_PLANE_2,worldPos); -#ifdef CLIP_PLANE_3 - gl_ClipDistance[3] = dot(CLIP_PLANE_3,worldPos); -#ifdef CLIP_PLANE_4 - gl_ClipDistance[4] = dot(CLIP_PLANE_4,worldPos); -#ifdef CLIP_PLANE_5 - gl_ClipDistance[5] = dot(CLIP_PLANE_5,worldPos); -#endif -#endif -#endif -#endif -#endif -#endif } diff --git a/22_RaytracedAO/main.cpp b/22_RaytracedAO/main.cpp index 45db9f715..850f5b245 100644 --- a/22_RaytracedAO/main.cpp +++ b/22_RaytracedAO/main.cpp @@ -7,7 +7,6 @@ #include #include -#include #include "../common/QToQuitEventReceiver.h" @@ -56,12 +55,6 @@ class RaytracerExampleEventReceiver : public nbl::IEventReceiver case BeautyKey: renderingBeauty = !renderingBeauty; break; - case ReloadKey: - reloadKeyPressed = true; - break; - case OverloadCameraKey: - overloadCameraKeyPressed = true; - break; case QuitKey: running = false; return true; @@ -89,10 +82,6 @@ class RaytracerExampleEventReceiver : public nbl::IEventReceiver inline bool isRenderingBeauty() const { return renderingBeauty; } - inline bool isReloadKeyPressed() const { return reloadKeyPressed; } - - inline bool isOverloadCameraKeyPressed() const { return overloadCameraKeyPressed; } - inline void resetKeys() { skipKeyPressed = false; @@ -101,8 +90,6 @@ class RaytracerExampleEventReceiver : public nbl::IEventReceiver previousKeyPressed = false; screenshotKeyPressed = false; logProgressKeyPressed = false; - reloadKeyPressed = false; - overloadCameraKeyPressed = false; } private: @@ -114,8 +101,6 @@ class RaytracerExampleEventReceiver : public nbl::IEventReceiver static constexpr nbl::EKEY_CODE ScreenshotKey = nbl::KEY_KEY_P; static constexpr nbl::EKEY_CODE LogProgressKey = nbl::KEY_KEY_L; static constexpr nbl::EKEY_CODE BeautyKey = nbl::KEY_KEY_B; - static constexpr nbl::EKEY_CODE ReloadKey = nbl::KEY_F5; - static constexpr nbl::EKEY_CODE OverloadCameraKey = nbl::KEY_KEY_C; bool running; bool renderingBeauty; @@ -126,11 +111,8 @@ class RaytracerExampleEventReceiver : public nbl::IEventReceiver bool previousKeyPressed; bool screenshotKeyPressed; bool logProgressKeyPressed; - bool reloadKeyPressed; - bool overloadCameraKeyPressed; }; - int main(int argc, char** argv) { std::vector arguments; @@ -140,71 +122,86 @@ int main(int argc, char** argv) arguments.emplace_back(argv[i]); } - { - CommandLineHandler cmdHandler = CommandLineHandler(arguments); - - applicationState.processSensorsBehaviour = cmdHandler.getProcessSensorsBehaviour(); - applicationState.startSensorID = cmdHandler.getSensorID(); - applicationState.isInteractiveMode = (applicationState.processSensorsBehaviour == ProcessSensorsBehaviour::PSB_INTERACTIVE_AT_SENSOR); - applicationState.isDenoiseDeferred = cmdHandler.getDeferredDenoiseFlag(); - - auto sceneDir = cmdHandler.getSceneDirectory(); - - std::string filePath = (sceneDir.size() >= 1) ? sceneDir[0] : ""; // zip or xml - std::string extraPath = (sceneDir.size() >= 2) ? sceneDir[1] : "";; // xml in zip - if (core::hasFileExtension(io::path(filePath.c_str()), "zip", "ZIP")) - { - applicationState.zipPath = filePath; - applicationState.xmlPath = extraPath; - } - else - { - applicationState.xmlPath = filePath; - } - // After this, there could be 4 cases: - // 1. zipPath filled, xmlPath filled -> load xml from zip - // 2. zipPath empty, xmlPath filled -> directly load xml - // 3. zipPath filled, xmlPath empty -> load chosen (or default to first) xml from zip - // 4. zipPath empty, xmlPath empty -> try to restore state after asking for the user to choose files - } - +#ifdef TEST_ARGS + arguments = std::vector { + "-SCENE", + "../../media/mitsuba/staircase2.zip", + "scene.xml", + "-TERMINATE", + "-SCREENSHOT_OUTPUT_FOLDER", + "\"C:\\Nabla-Screen-Shots\"" + }; +#endif + + CommandLineHandler cmdHandler = CommandLineHandler(arguments); + + auto sceneDir = cmdHandler.getSceneDirectory(); + std::string filePath = (sceneDir.size() >= 1) ? sceneDir[0] : ""; // zip or xml + std::string extraPath = (sceneDir.size() >= 2) ? sceneDir[1] : "";; // xml in zip + bool shouldTerminateAfterRenders = cmdHandler.getTerminate(); // skip interaction with window and take screenshots only bool takeScreenShots = true; - -// DEVICE CREATION EMITTED + std::string mainFileName; // std::filesystem::path(filePath).filename().string(); + + // create device with full flexibility over creation parameters + // you can add more parameters if desired, check nbl::SIrrlichtCreationParameters + nbl::SIrrlichtCreationParameters params; + params.Bits = 24; //may have to set to 32bit for some platforms + params.ZBufferBits = 24; + params.DriverType = video::EDT_OPENGL; + params.Fullscreen = false; + params.Vsync = false; + params.Doublebuffer = true; + params.Stencilbuffer = false; //! This will not even be a choice soon + params.WindowSize = dimension2d(1920, 1080); + auto device = createDeviceEx(params); + if (!device) + return 1; // could not create selected driver. + + // will leak it because there's no cross platform input! + std::thread cin_thread; // - asset::SAssetBundle meshes = {}; + asset::SAssetBundle meshes; core::smart_refctd_ptr globalMeta; { - -// LOADER ADDITION EMITTED - - if (applicationState.zipPath.empty() && applicationState.xmlPath.empty() && !applicationIsReloaded) + io::IFileSystem* fs = device->getFileSystem(); + asset::IAssetManager* am = device->getAssetManager(); + + auto serializedLoader = core::make_smart_refctd_ptr(am); + auto mitsubaLoader = core::make_smart_refctd_ptr(am,fs); + serializedLoader->initialize(); + mitsubaLoader->initialize(); + am->addAssetLoader(std::move(serializedLoader)); + am->addAssetLoader(std::move(mitsubaLoader)); + + if(filePath.empty()) { - pfd::message("Choose file to load", "Choose mitsuba XML file to load or ZIP containing an XML. If you cancel or choosen file fails to load, previous state of the application will be restored, if available.", pfd::choice::ok); - pfd::open_file file("Choose XML or ZIP file", "../../media/mitsuba", { "All Supported Formats", "*.xml *.zip", "ZIP files (.zip)", "*.zip", "XML files (.xml)", "*.xml" }); + pfd::message("Choose file to load", "Choose mitsuba XML file to load or ZIP containing an XML. \nIf you cancel or choosen file fails to load, simple scene will be loaded.", pfd::choice::ok); + pfd::open_file file("Choose XML or ZIP file", "../../media/mitsuba", { "ZIP files (.zip)", "*.zip", "XML files (.xml)", "*.xml"}); if (!file.result().empty()) - { - if (core::hasFileExtension(io::path(file.result()[0].c_str()), "zip", "ZIP")) - applicationState.zipPath = file.result()[0]; - else - applicationState.xmlPath = file.result()[0]; - } + filePath = file.result()[0]; } - auto loadScene = [&device, &am, &fs](const std::string& _zipPath, std::string& _xmlPath, std::string& _mainFileName) -> asset::SAssetBundle - { - asset::SAssetBundle result = {}; - -// ADD ARCHIVE AND VALIDATION EMITTED - + if(filePath.empty()) + filePath = "../../media/mitsuba/staircase2.zip"; + + mainFileName = std::filesystem::path(filePath).filename().string(); + mainFileName = mainFileName.substr(0u, mainFileName.find_first_of('.')); + + std::cout << "\nSelected File = " << filePath << "\n" << std::endl; + if (core::hasFileExtension(io::path(filePath.c_str()), "zip", "ZIP")) + { + io::IFileArchive* arch = nullptr; + device->getFileSystem()->addFileArchive(filePath.c_str(),io::EFAT_ZIP,"",&arch); + if (arch) + { auto flist = arch->getFileList(); if (!flist) - return {}; - + return 3; auto files = flist->getFiles(); - for (auto it = files.begin(); it != files.end(); ) + + for (auto it=files.begin(); it!=files.end(); ) { if (core::hasFileExtension(it->FullName, "xml", "XML")) it++; @@ -212,94 +209,114 @@ int main(int argc, char** argv) it = files.erase(it); } if (files.size() == 0u) - { - printf("[ERROR]: No XML files found in the ZIP archive!\n"); - return result; - } + return 4; - if (_xmlPath.empty()) + if(extraPath.empty()) { uint32_t chosen = 0xffffffffu; - // Only ask for choosing a file when there are multiple of them. - if (files.size() > 1) + // Don't ask for choosing file when there is only 1 available + if(files.size() > 1) { - printf("Choose File (0-%llu):\n", files.size() - 1ull); + std::cout << "Choose File (0-" << files.size() - 1ull << "):" << std::endl; for (auto i = 0u; i < files.size(); i++) - printf("%u: %s\n", i, files[i].FullName.c_str()); + std::cout << i << ": " << files[i].FullName.c_str() << std::endl; // std::cin with timeout { std::atomic started = false; - std::thread cin_thread([&chosen, &started]() + cin_thread = std::thread([&chosen,&started]() { started = true; std::cin >> chosen; }); - cin_thread.detach(); - const auto end = std::chrono::steady_clock::now() + std::chrono::seconds(10u); - while (!started || chosen == 0xffffffffu && std::chrono::steady_clock::now() < end) {} + const auto end = std::chrono::steady_clock::now()+std::chrono::seconds(10u); + while (!started || chosen==0xffffffffu && std::chrono::steady_clock::now()= 0) { - printf("[INFO]: The only available XML in the ZIP is selected.\n"); + std::cout << "The only available XML in zip Selected." << std::endl; } - + if (chosen >= files.size()) chosen = 0u; - _xmlPath = std::string(files[chosen].FullName.c_str()); + filePath = files[chosen].FullName.c_str(); + std::cout << "Selected XML File: "<< files[chosen].Name.c_str() << std::endl; + mainFileName += std::string("_") + std::filesystem::path(files[chosen].Name.c_str()).replace_extension().string(); } else { - // Verify that the passed XML path is in the ZIP archive. bool found = false; - for (auto it = files.begin(); it != files.end(); it++) + for (auto it=files.begin(); it!=files.end(); it++) { - // In `PersistentState` we save the full XML name `_xmlPath` could also be that. - if ((_xmlPath == std::string(it->Name.c_str())) || (_xmlPath == std::string(it->FullName.c_str()))) + if(extraPath == std::string(it->Name.c_str())) { - _xmlPath = std::string(it->FullName.c_str()); found = true; + filePath = it->FullName.c_str(); + mainFileName += std::string("_") + std::filesystem::path(it->Name.c_str()).replace_extension().string(); break; } } - if (!found) - { - printf("[ERROR]: Cannot find requested XML file (%s) in the ZIP (%s)\n", _xmlPath.c_str(), _zipPath.c_str()); - return result; + if(!found) { + std::cout << "Cannot find requested file (" << extraPath.c_str() << ") in zip (" << filePath << ")" << std::endl; + return 4; } } + } + } + + asset::CQuantNormalCache* qnc = am->getMeshManipulator()->getQuantNormalCache(); + + //! read cache results -- speeds up mesh generation + qnc->loadCacheFromFile(fs, "../../tmp/normalCache101010.sse"); + //! load the mitsuba scene + meshes = am->getAsset(filePath, {}); + //! cache results -- speeds up mesh generation on second run + qnc->saveCacheToFile(fs, "../../tmp/normalCache101010.sse"); + + auto contents = meshes.getContents(); + if (!contents.size()) { + std::cout << "[ERROR] Failed loading asset in " << filePath << "."; + return 2; + } - _mainFileName += std::string("_") + std::filesystem::path(_xmlPath.c_str()).filename().replace_extension().string(); - - - printf("[INFO]: Loading XML file: %s\n", _xmlPath.c_str()); - - asset::CQuantNormalCache* qnc = am->getMeshManipulator()->getQuantNormalCache(); - - //! read cache results -- speeds up mesh generation - qnc->loadCacheFromFile(fs, "../../tmp/normalCache101010.sse"); - //! load the mitsuba scene - result = am->getAsset(_xmlPath, {}); - //! cache results -- speeds up mesh generation on second run - qnc->saveCacheToFile(fs, "../../tmp/normalCache101010.sse"); - - return result; - }; - - meshes = loadScene(applicationState.zipPath, applicationState.xmlPath, mainFileName); - -// APPLICATION RESTORE OMITTED - + globalMeta = core::smart_refctd_ptr(meshes.getMetadata()->selfCast()); + if (!globalMeta) { + std::cout << "[ERROR] Couldn't get global Meta"; + return 3; + } } + constexpr float DefaultRotateSpeed = 300.0f; + constexpr float DefaultZoomSpeed = 1.0f; + constexpr float DefaultMoveSpeed = 100.0f; + constexpr float DefaultSceneDiagonal = 50.0f; // reference for default zoom and move speed; struct SensorData { -// ... + int32_t width = 0u; + int32_t height = 0u; + bool rightHandedCamera = true; + uint32_t samplesNeeded = 0u; + float moveSpeed = core::nan(); + float stepZoomSpeed = core::nan(); + float rotateSpeed = core::nan(); + scene::ICameraSceneNode * staticCamera; + scene::ICameraSceneNode * interactiveCamera; + std::filesystem::path outputFilePath; + ext::MitsubaLoader::CElementSensor::Type type; + ext::MitsubaLoader::CElementFilm::FileFormat fileFormat; + Renderer::DenoiserArgs denoiserInfo = {}; + int32_t highQualityEdges = 0u; + bool envmap = false; + + scene::CSceneNodeAnimatorCameraModifiedMaya* getInteractiveCameraAnimator() + { + return reinterpret_cast(interactiveCamera->getAnimators()[0]); + } void resetInteractiveCamera() { @@ -317,38 +334,226 @@ int main(int argc, char** argv) modifiedMayaAnim->setZoomAndRotationBasedOnTargetAndPosition(cameraPos, cameraTarget); } }; + + struct CubemapRender + { + uint32_t sensorIdx = 0u; + uint32_t getSensorsBeginIdx() const { return sensorIdx; } + uint32_t getSensorsEndIdx() const { return sensorIdx + 5; } + }; + auto smgr = device->getSceneManager(); + + // When outputFilePath isn't set in Film Element in Mitsuba, use this to find the extension string. + auto getFileExtensionFromFormat= [](ext::MitsubaLoader::CElementFilm::FileFormat format) -> std::string + { + std::string ret = ""; + using FileFormat = ext::MitsubaLoader::CElementFilm::FileFormat; + switch (format) { + case FileFormat::PNG: + ret = ".png"; + break; + case FileFormat::OPENEXR: + ret = ".exr"; + break; + case FileFormat::JPEG: + ret = ".jpg"; + break; + default: // TODO? + break; + } + return ret; + }; -// ... + auto isFileExtensionCompatibleWithFormat = [](std::string extension, ext::MitsubaLoader::CElementFilm::FileFormat format) -> bool + { + if(extension.empty()) + return false; + + if(extension[0] == '.') + extension = extension.substr(1, extension.size()); + + // TODO: get the supported extensions from loaders(?) + using FileFormat = ext::MitsubaLoader::CElementFilm::FileFormat; + switch (format) { + case FileFormat::PNG: + return extension == "png"; + case FileFormat::OPENEXR: + return extension == "exr"; + case FileFormat::JPEG: + return extension == "jpg" || extension == "jpeg" || extension == "jpe" || extension == "jif" || extension == "jfif" || extension == "jfi"; + default: + return false; + } + }; + + std::cout << "Total number of Sensors = " << globalMeta->m_global.m_sensors.size() << std::endl; + + if(globalMeta->m_global.m_sensors.empty()) + { + std::cout << "[ERROR] No Sensors found." << std::endl; + assert(false); + return 5; // return code? + } + + const bool shouldHaveSensorIdxInFileName = globalMeta->m_global.m_sensors.size() > 1; + std::vector sensors = std::vector(); + std::vector cubemapRenders = std::vector(); auto extractAndAddToSensorData = [&](const ext::MitsubaLoader::CElementSensor& sensor, uint32_t idx) -> bool { SensorData mainSensorData = {}; + const auto& film = sensor.film; + mainSensorData.denoiserInfo.bloomFilePath = std::filesystem::path(film.denoiserBloomFilePath); + mainSensorData.denoiserInfo.bloomScale = film.denoiserBloomScale; + mainSensorData.denoiserInfo.bloomIntensity = film.denoiserBloomIntensity; + mainSensorData.denoiserInfo.tonemapperArgs = std::string(film.denoiserTonemapperArgs); + mainSensorData.fileFormat = film.fileFormat; + mainSensorData.highQualityEdges = film.highQualityEdges; + mainSensorData.outputFilePath = std::filesystem::path(film.outputFilePath); + if(!isFileExtensionCompatibleWithFormat(mainSensorData.outputFilePath.extension().string(), mainSensorData.fileFormat)) + { + std::cout << "[ERROR] film.outputFilePath's extension is not compatible with film.fileFormat" << std::endl; + } + // handle missing output path + if (mainSensorData.outputFilePath.empty()) + { + auto extensionStr = getFileExtensionFromFormat(mainSensorData.fileFormat); + if(shouldHaveSensorIdxInFileName) + mainSensorData.outputFilePath = std::filesystem::path("Render_" + mainFileName + "_Sensor_" + std::to_string(idx) + extensionStr); + else + mainSensorData.outputFilePath = std::filesystem::path("Render_" + mainFileName + extensionStr); + } -// ... + mainSensorData.samplesNeeded = sensor.sampler.sampleCount; + std::cout << "\t SamplesPerPixelNeeded = " << mainSensorData.samplesNeeded << std::endl; + const ext::MitsubaLoader::CElementSensor::PerspectivePinhole* persp = nullptr; + const ext::MitsubaLoader::CElementSensor::Orthographic* ortho = nullptr; + const ext::MitsubaLoader::CElementSensor::CameraBase* cameraBase = nullptr; + switch (sensor.type) + { + case ext::MitsubaLoader::CElementSensor::Type::PERSPECTIVE: + persp = &sensor.perspective; + cameraBase = persp; + std::cout << "\t Type = PERSPECTIVE" << std::endl; + break; + case ext::MitsubaLoader::CElementSensor::Type::THINLENS: + persp = &sensor.thinlens; + cameraBase = persp; + std::cout << "\t Type = THINLENS" << std::endl; + break; + case ext::MitsubaLoader::CElementSensor::Type::ORTHOGRAPHIC: + ortho = &sensor.orthographic; + cameraBase = ortho; + std::cout << "\t Type = ORTHOGRAPHIC" << std::endl; + break; + case ext::MitsubaLoader::CElementSensor::Type::TELECENTRIC: + ortho = &sensor.telecentric; + cameraBase = ortho; + std::cout << "\t Type = TELECENTRIC" << std::endl; + break; + case ext::MitsubaLoader::CElementSensor::Type::SPHERICAL: + cameraBase = &sensor.spherical; + std::cout << "\t Type = SPHERICAL" << std::endl; + break; + default: + std::cout << "\tSensor Type is not valid" << std::endl; + return false; + } + mainSensorData.type = sensor.type; + mainSensorData.rotateSpeed = cameraBase->rotateSpeed; + mainSensorData.stepZoomSpeed = cameraBase->zoomSpeed; + mainSensorData.moveSpeed = cameraBase->moveSpeed; + + if(core::isnan(mainSensorData.rotateSpeed)) + { + mainSensorData.rotateSpeed = DefaultRotateSpeed; + std::cout << "\t Camera Rotate Speed = " << mainSensorData.rotateSpeed << " = [Default Value]" << std::endl; + } + else + std::cout << "\t Camera Rotate Speed = " << mainSensorData.rotateSpeed << std::endl; + if(core::isnan(mainSensorData.stepZoomSpeed)) + std::cout << "\t Camera Step Zoom Speed [Linear] = " << "[Value will be deduced from Scene Bounds] " << std::endl; + else + std::cout << "\t Camera Step Zoom Speed [Linear] = " << mainSensorData.stepZoomSpeed << std::endl; + + if(core::isnan(mainSensorData.moveSpeed)) + std::cout << "\t Camera Move Speed = " << "[Value will be deduced from Scene Bounds] " << std::endl; + else + std::cout << "\t Camera Move Speed = " << mainSensorData.moveSpeed << std::endl; + + float defaultZoomSpeedMultiplier = std::pow(DefaultSceneDiagonal, DefaultZoomSpeed / DefaultSceneDiagonal); + mainSensorData.interactiveCamera = smgr->addCameraSceneNodeModifiedMaya(nullptr, -1.0f * mainSensorData.rotateSpeed, 50.0f, mainSensorData.moveSpeed, -1, 2.0f, defaultZoomSpeedMultiplier, false, true); + + nbl::core::vectorSIMDf mainCamPos; + nbl::core::vectorSIMDf mainCamUp; + nbl::core::vectorSIMDf mainCamView; + // need to extract individual components from matrix to camera + { + auto relativeTransform = sensor.transform.matrix.extractSub3x4(); + if (relativeTransform.getPseudoDeterminant().x < 0.f) + mainSensorData.rightHandedCamera = false; + else + mainSensorData.rightHandedCamera = true; + + std::cout << "\t IsRightHanded=" << ((mainSensorData.rightHandedCamera) ? "TRUE" : "FALSE") << std::endl; + + mainCamPos = relativeTransform.getTranslation(); + + std::cout << "\t Camera Position = <" << mainCamPos.x << "," << mainCamPos.y << "," << mainCamPos.z << ">" << std::endl; + + auto tpose = core::transpose(sensor.transform.matrix); + mainCamUp = tpose.rows[1]; + mainCamView = tpose.rows[2]; + } + + float realFoVDegrees; + auto width = film.cropWidth; + auto height = film.cropHeight; + mainSensorData.width = width; + mainSensorData.height = height; + float aspectRatio = float(width) / float(height); + auto convertFromXFoV = [=](float fov) -> float + { + float aspectX = tan(core::radians(fov)*0.5f); + return core::degrees(atan(aspectX/aspectRatio)*2.f); + }; + + // TODO: apply the crop offset + assert(film.cropOffsetX==0 && film.cropOffsetY==0); + + float nearClip = cameraBase->nearClip; + float farClip = cameraBase->farClip; + if(farClip > nearClip * 10'000.0f) + std::cout << "[WARN] Depth Range is too big: nearClip = " << nearClip << ", farClip = " << farClip << std::endl; if (mainSensorData.type == ext::MitsubaLoader::CElementSensor::Type::SPHERICAL) { -#ifdef 0 // camera setup cubemap nbl::core::vectorSIMDf camViews[6] = { - nbl::core::vectorSIMDf(-1, 0, 0, 0), // -X nbl::core::vectorSIMDf(+1, 0, 0, 0), // +X - nbl::core::vectorSIMDf(0, -1, 0, 0), // -Y + nbl::core::vectorSIMDf(-1, 0, 0, 0), // -X nbl::core::vectorSIMDf(0, +1, 0, 0), // +Y - nbl::core::vectorSIMDf(0, 0, -1, 0), // -Z + nbl::core::vectorSIMDf(0, -1, 0, 0), // -Y nbl::core::vectorSIMDf(0, 0, +1, 0), // +Z + nbl::core::vectorSIMDf(0, 0, -1, 0), // -Z }; + if(!mainSensorData.rightHandedCamera) + { + camViews[0] *= -1; + camViews[1] *= -1; + } + const nbl::core::vectorSIMDf upVectors[6] = { nbl::core::vectorSIMDf(0, +1, 0, 0), // +Y nbl::core::vectorSIMDf(0, +1, 0, 0), // +Y - nbl::core::vectorSIMDf(0, 0, -1, 0), // -Z - nbl::core::vectorSIMDf(0, 0, +1, 0), // +Z + nbl::core::vectorSIMDf(0, 0, +1, 0), // -Z + nbl::core::vectorSIMDf(0, 0, -1, 0), // +Z nbl::core::vectorSIMDf(0, +1, 0, 0), // +Y nbl::core::vectorSIMDf(0, +1, 0, 0), // +Y }; @@ -359,240 +564,307 @@ int main(int argc, char** argv) for(uint32_t i = 0; i < 6; ++i) { + SensorData cubemapFaceSensorData = mainSensorData; + cubemapFaceSensorData.envmap = true; + + if(mainSensorData.width != mainSensorData.height) + { + std::cout << "[ERROR] Cannot generate cubemap faces where film.width and film.height are not equal. (Aspect Ration must be 1)" << std::endl; + assert(false); + } + const auto baseResolution = core::max(mainSensorData.width,mainSensorData.height); + cubemapFaceSensorData.width = baseResolution + mainSensorData.highQualityEdges * 2; + cubemapFaceSensorData.height = baseResolution + mainSensorData.highQualityEdges * 2; + // FIXME: suffix added after extension cubemapFaceSensorData.outputFilePath.replace_extension(); constexpr const char* suffixes[6] = { - "_x-.exr", "_x+.exr", - "_y-.exr", + "_x-.exr", "_y+.exr", - "_z-.exr", + "_y-.exr", "_z+.exr", + "_z-.exr", }; cubemapFaceSensorData.outputFilePath += suffixes[i]; - staticCamera->setTarget((mainCamPos + camViews[i]).getAsVector3df()); - staticCamera->setUpVector(upVectors[i]); + cubemapFaceSensorData.staticCamera = smgr->addCameraSceneNode(nullptr); + auto& staticCamera = cubemapFaceSensorData.staticCamera; + + const auto& camView = camViews[i]; + const auto& upVector = upVectors[i]; + + staticCamera->setPosition(mainCamPos.getAsVector3df()); + staticCamera->setTarget((mainCamPos + camView).getAsVector3df()); + staticCamera->setUpVector(upVector); - const float w = float(cubemapFaceSensorData.width)/float(cubemapFaceSensorData.cropWidth); - const float h = float(cubemapFaceSensorData.height)/float(cubemapFaceSensorData.cropHeight); + // auto fov = core::radians(90.0f); + auto fov = atanf(float(cubemapFaceSensorData.width) / float(mainSensorData.width)) * 2.0f; + auto aspectRatio = 1.0f; - const auto fov = 45 degree nondiag; - const auto aspectRatio = 1.f; if (mainSensorData.rightHandedCamera) - staticCamera->setProjectionMatrix(core::matrix4SIMD::buildProjectionMatrixPerspectiveFovRH(fov, aspectRatio, nearClip, farClip)); + staticCamera->setProjectionMatrix(core::matrix4SIMD::buildProjectionMatrixPerspectiveFovRH(fov, 1.0f, nearClip, farClip)); else - staticCamera->setProjectionMatrix(core::matrix4SIMD::buildProjectionMatrixPerspectiveFovLH(fov, aspectRatio, nearClip, farClip)); + staticCamera->setProjectionMatrix(core::matrix4SIMD::buildProjectionMatrixPerspectiveFovLH(fov, 1.0f, nearClip, farClip)); + + cubemapFaceSensorData.interactiveCamera = smgr->addCameraSceneNodeModifiedMaya(nullptr, -1.0f * mainSensorData.rotateSpeed, 50.0f, mainSensorData.moveSpeed, -1, 2.0f, defaultZoomSpeedMultiplier, false, true); + cubemapFaceSensorData.resetInteractiveCamera(); + sensors.push_back(cubemapFaceSensorData); } -#endif } else { -// camera setup non spherical + mainSensorData.staticCamera = smgr->addCameraSceneNode(nullptr); + auto& staticCamera = mainSensorData.staticCamera; + + staticCamera->setPosition(mainCamPos.getAsVector3df()); + + { + auto target = mainCamView+mainCamPos; + std::cout << "\t Camera Target = <" << target.x << "," << target.y << "," << target.z << ">" << std::endl; + staticCamera->setTarget(target.getAsVector3df()); + } + + if (core::dot(core::normalize(core::cross(staticCamera->getUpVector(),mainCamView)),core::cross(mainCamUp,mainCamView)).x<0.99f) + staticCamera->setUpVector(mainCamUp); + + // + if (ortho) + { + const auto scale = sensor.transform.matrix.extractSub3x4().getScale(); + const float volumeX = 2.f*scale.x; + const float volumeY = (2.f/aspectRatio)*scale.y; + if (mainSensorData.rightHandedCamera) + staticCamera->setProjectionMatrix(core::matrix4SIMD::buildProjectionMatrixOrthoRH(volumeX, volumeY, nearClip, farClip)); + else + staticCamera->setProjectionMatrix(core::matrix4SIMD::buildProjectionMatrixOrthoLH(volumeX, volumeY, nearClip, farClip)); + } + else if (persp) + { + switch (persp->fovAxis) + { + case ext::MitsubaLoader::CElementSensor::PerspectivePinhole::FOVAxis::X: + realFoVDegrees = convertFromXFoV(persp->fov); + break; + case ext::MitsubaLoader::CElementSensor::PerspectivePinhole::FOVAxis::Y: + realFoVDegrees = persp->fov; + break; + case ext::MitsubaLoader::CElementSensor::PerspectivePinhole::FOVAxis::DIAGONAL: + { + float aspectDiag = tan(core::radians(persp->fov)*0.5f); + float aspectY = aspectDiag/core::sqrt(1.f+aspectRatio*aspectRatio); + realFoVDegrees = core::degrees(atan(aspectY)*2.f); + } + break; + case ext::MitsubaLoader::CElementSensor::PerspectivePinhole::FOVAxis::SMALLER: + if (width < height) + realFoVDegrees = convertFromXFoV(persp->fov); + else + realFoVDegrees = persp->fov; + break; + case ext::MitsubaLoader::CElementSensor::PerspectivePinhole::FOVAxis::LARGER: + if (width < height) + realFoVDegrees = persp->fov; + else + realFoVDegrees = convertFromXFoV(persp->fov); + break; + default: + realFoVDegrees = NAN; + assert(false); + break; + } + + if (mainSensorData.rightHandedCamera) + staticCamera->setProjectionMatrix(core::matrix4SIMD::buildProjectionMatrixPerspectiveFovRH(core::radians(realFoVDegrees), aspectRatio, nearClip, farClip)); + else + staticCamera->setProjectionMatrix(core::matrix4SIMD::buildProjectionMatrixPerspectiveFovLH(core::radians(realFoVDegrees), aspectRatio, nearClip, farClip)); + } + else + { + assert(false); + } + + mainSensorData.resetInteractiveCamera(); + sensors.push_back(mainSensorData); } return true; }; + for(uint32_t s = 0u; s < globalMeta->m_global.m_sensors.size(); ++s) + { + std::cout << "Sensors[" << s << "] = " << std::endl; + const auto& sensor = globalMeta->m_global.m_sensors[s]; + extractAndAddToSensorData(sensor, s); + } + auto driver = device->getVideoDriver(); - core::smart_refctd_ptr renderer = core::make_smart_refctd_ptr(driver,device->getAssetManager(),smgr,applicationState.isDenoiseDeferred); + core::smart_refctd_ptr renderer = core::make_smart_refctd_ptr(driver,device->getAssetManager(),smgr); renderer->initSceneResources(meshes,"LowDiscrepancySequenceCache.bin"); + meshes = {}; // free memory + + RaytracerExampleEventReceiver receiver; + device->setEventReceiver(&receiver); -// free memory -meshes = {}; -device->getAssetManager()->clearAllGPUObjects(); - + // Deduce Move and Zoom Speeds if it is nan + auto sceneBoundsExtent = renderer->getSceneBound().getExtent(); + auto sceneDiagonal = sceneBoundsExtent.getLength(); - core::SRange nonInteractiveSensors = { nullptr, nullptr }; - if (!applicationState.isInteractiveMode) + for(uint32_t s = 0u; s < sensors.size(); ++s) { - assert(applicationState.startSensorID < globalMeta->m_global.m_sensors.size() && "startSensorID should've been a valid value by now."); - - uint32_t onePastLastSensorID = applicationState.startSensorID; - if ((applicationState.processSensorsBehaviour == ProcessSensorsBehaviour::PSB_RENDER_ALL_THEN_INTERACTIVE) || (applicationState.processSensorsBehaviour == ProcessSensorsBehaviour::PSB_RENDER_ALL_THEN_TERMINATE)) - onePastLastSensorID = sensors.size(); - else if (applicationState.processSensorsBehaviour == ProcessSensorsBehaviour::PSB_RENDER_SENSOR_THEN_INTERACTIVE) - onePastLastSensorID = applicationState.startSensorID + 1; - nonInteractiveSensors = { sensors.data() + applicationState.startSensorID, sensors.data() + onePastLastSensorID }; + auto& sensorData = sensors[s]; + + float linearStepZoomSpeed = sensorData.stepZoomSpeed; + if(core::isnan(sensorData.stepZoomSpeed)) + { + linearStepZoomSpeed = sceneDiagonal * (DefaultZoomSpeed / DefaultSceneDiagonal); + } + + // Set Zoom Multiplier + { + float logarithmicZoomSpeed = std::pow(sceneDiagonal, linearStepZoomSpeed / sceneDiagonal); + sensorData.stepZoomSpeed = logarithmicZoomSpeed; + sensorData.getInteractiveCameraAnimator()->setStepZoomMultiplier(logarithmicZoomSpeed); + printf("[INFO] Sensor[%d] Camera Step Zoom Speed deduced from scene bounds = %f [Linear], %f [Logarithmic] \n", s, linearStepZoomSpeed, logarithmicZoomSpeed); + } + + if(core::isnan(sensorData.moveSpeed)) + { + float newMoveSpeed = DefaultMoveSpeed * (sceneDiagonal / DefaultSceneDiagonal); + sensorData.moveSpeed = newMoveSpeed; + sensorData.getInteractiveCameraAnimator()->setMoveSpeed(newMoveSpeed); + printf("[INFO] Sensor[%d] Camera Move Speed deduced from scene bounds = %f\n", s, newMoveSpeed); + } + + assert(!core::isnan(sensorData.getInteractiveCameraAnimator()->getRotateSpeed())); + //assert(!core::isnan(sensorData.getInteractiveCameraAnimator()->getStepZoomSpeed())); + assert(!core::isnan(sensorData.getInteractiveCameraAnimator()->getMoveSpeed())); } - assert(nonInteractiveSensors.size() <= sensors.size()); -// ... // Render To file int32_t prevWidth = 0; int32_t prevHeight = 0; - int32_t prevCascadeCount = 0; - float prevRegFactor = 0.0f; - if (!nonInteractiveSensors.empty()) + for(uint32_t s = 0u; s < sensors.size(); ++s) { - for (const auto& sensor : nonInteractiveSensors) - { - if(!receiver.keepOpen()) - break; + if(!receiver.keepOpen()) + break; - const uint32_t s = &sensor - sensors.data(); - - // Save application's current persistent state to disk. - { - applicationState.isBeauty = receiver.isRenderingBeauty(); - applicationState.isInteractiveMode = false; - applicationState.startSensorID = s; - - if (!applicationState.writeToDisk()) - printf("[ERROR]: Cannot write application state to disk\n"); - } + const auto& sensorData = sensors[s]; - printf("[INFO] Rendering %s - Sensor(%d) to file.\n", applicationState.xmlPath.c_str(), s); + printf("[INFO] Rendering %s - Sensor(%d) to file.\n", filePath.c_str(), s); - bool needsReinit = prevWidth!=sensor.width || prevHeight!=sensor.height || prevCascadeCount!=sensor.cascadeCount || prevRegFactor!=sensor.envmapRegFactor; - prevWidth = sensor.width; - prevHeight = sensor.height; - prevCascadeCount = sensor.cascadeCount; - prevRegFactor = sensor.envmapRegFactor; + bool needsReinit = (prevWidth != sensorData.width) || (prevHeight != sensorData.height); // >= or != + prevWidth = sensorData.width; + prevHeight = sensorData.height; - renderer->resetSampleAndFrameCounters(); // so that renderer->getTotalSamplesPerPixelComputed is 0 at the very beginning - if(needsReinit) - { - renderer->deinitScreenSizedResources(); - renderer->initScreenSizedResources(sensor.width,sensor.height,sensor.envmapRegFactor,sensor.cascadeCount,sensor.cascadeLuminanceBase,sensor.cascadeLuminanceStart,sensor.Emin,sensor.clipPlanes); - } + renderer->resetSampleAndFrameCounters(); // so that renderer->getTotalSamplesPerPixelComputed is 0 at the very beginning + if(needsReinit) + { + renderer->deinitScreenSizedResources(); + renderer->initScreenSizedResources(sensorData.width,sensorData.height); + } - smgr->setActiveCamera(sensor.staticCamera); + smgr->setActiveCamera(sensorData.staticCamera); - const uint32_t samplesPerPixelPerDispatch = renderer->getSamplesPerPixelPerDispatch(); - const uint32_t maxNeededIterations = (sensor.samplesNeeded + samplesPerPixelPerDispatch - 1) / samplesPerPixelPerDispatch; + const uint32_t samplesPerPixelPerDispatch = renderer->getSamplesPerPixelPerDispatch(); + const uint32_t maxNeededIterations = (sensorData.samplesNeeded + samplesPerPixelPerDispatch - 1) / samplesPerPixelPerDispatch; - uint32_t itr = 0u; - bool takenEnoughSamples = false; - bool renderFailed = false; - auto lastTimeLoggedProgress = std::chrono::steady_clock::now(); - while(!takenEnoughSamples && (device->run() && !receiver.isSkipKeyPressed() && receiver.keepOpen())) - { - if(itr >= maxNeededIterations) - std::cout << "[ERROR] Samples taken (" << renderer->getTotalSamplesPerPixelComputed() << ") must've exceeded samples needed for Sensor (" << sensor.samplesNeeded << ") by now; something is wrong." << std::endl; - - // Handle Inputs - { - if(receiver.isLogProgressKeyPressed() || (std::chrono::steady_clock::now()-lastTimeLoggedProgress)>std::chrono::seconds(3)) - { - int progress = float(renderer->getTotalSamplesPerPixelComputed())/float(sensor.samplesNeeded) * 100; - printf("[INFO] Rendering in progress - %d%% Progress = %u/%u SamplesPerPixel. \n", progress, renderer->getTotalSamplesPerPixelComputed(), sensor.samplesNeeded); - lastTimeLoggedProgress = std::chrono::steady_clock::now(); - } - if (receiver.isReloadKeyPressed()) - { - printf("[INFO]: Reloading..\n"); - reloadApplication(); - } - receiver.resetKeys(); - } - - driver->beginScene(false, false); + uint32_t itr = 0u; + bool takenEnoughSamples = false; + bool renderFailed = false; + while(!takenEnoughSamples && (device->run() && !receiver.isSkipKeyPressed() && receiver.keepOpen())) + { + if(itr >= maxNeededIterations) + std::cout << "[ERROR] Samples taken (" << renderer->getTotalSamplesPerPixelComputed() << ") must've exceeded samples needed for Sensor (" << sensorData.samplesNeeded << ") by now; something is wrong." << std::endl; - if(!renderer->render(device->getTimer(),sensor.kappa,sensor.Emin,!sensor.envmap)) + // Handle Inputs + { + if(receiver.isLogProgressKeyPressed()) { - renderFailed = true; - driver->endScene(); - break; + int progress = float(renderer->getTotalSamplesPerPixelComputed())/float(sensorData.samplesNeeded) * 100; + printf("[INFO] Rendering in progress - %d%% Progress = %u/%u SamplesPerPixel. \n", progress, renderer->getTotalSamplesPerPixelComputed(), sensorData.samplesNeeded); } + receiver.resetKeys(); + } - auto oldVP = driver->getViewPort(); - driver->blitRenderTargets(renderer->getColorBuffer(),nullptr,false,false,{},{},true); - driver->setViewPort(oldVP); - driver->endScene(); - - if(renderer->getTotalSamplesPerPixelComputed() >= sensor.samplesNeeded) - takenEnoughSamples = true; - - itr++; - } + driver->beginScene(false, false); - auto screenshotFilePath = sensor.outputFilePath; - - if(renderFailed) + if(!renderer->render(device->getTimer(),!sensorData.envmap)) { - std::cout << "[ERROR] Render Failed." << std::endl; + renderFailed = true; + driver->endScene(); + break; } - else - { - const bool shouldDenoise = sensor.type != ext::MitsubaLoader::CElementSensor::Type::SPHERICAL; - renderer->takeAndSaveScreenShot(screenshotFilePath, shouldDenoise, sensor.denoiserInfo); - int progress = float(renderer->getTotalSamplesPerPixelComputed())/float(sensor.samplesNeeded) * 100; - printf("[INFO] Rendered Successfully - %d%% Progress = %u/%u SamplesPerPixel - FileName = %s. \n", progress, renderer->getTotalSamplesPerPixelComputed(), sensor.samplesNeeded, screenshotFilePath.filename().string().c_str()); - auto filename_wo_ext = screenshotFilePath; - filename_wo_ext.replace_extension(); - auto stream = std::make_shared(); - stream->begin_json_object(); - stream->emit_json_key_value("output_tonemap", filename_wo_ext.string() +".exr"); - stream->emit_json_key_value("output_albedo", filename_wo_ext.string() + "_albedo.exr"); - stream->emit_json_key_value("output_normal", filename_wo_ext.string() + "_normal.exr"); - if(shouldDenoise) - stream->emit_json_key_value("output_denoised", filename_wo_ext.string() + "_denoised.exr"); - stream->end_json_object(); - std::cout << "\n[JSON] " << stream->str() << "\n[ENDJSON]" << std::endl; - - } + auto oldVP = driver->getViewPort(); + driver->blitRenderTargets(renderer->getColorBuffer(),nullptr,false,false,{},{},true); + driver->setViewPort(oldVP); - receiver.resetKeys(); + driver->endScene(); + + if(renderer->getTotalSamplesPerPixelComputed() >= sensorData.samplesNeeded) + takenEnoughSamples = true; + + itr++; } - // Denoise Cubemaps that weren't denoised seperately - for(uint32_t i = 0; i < cubemapRenders.size(); ++i) + auto screenshotFilePath = sensorData.outputFilePath; + + if(renderFailed) + { + std::cout << "[ERROR] Render Failed." << std::endl; + } + else { - uint32_t beginIdx = cubemapRenders[i].getSensorsBeginIdx(); - assert(beginIdx + 6 <= sensors.size()); + bool shouldDenoise = sensorData.type != ext::MitsubaLoader::CElementSensor::Type::SPHERICAL; + renderer->takeAndSaveScreenShot(screenshotFilePath, shouldDenoise, sensorData.denoiserInfo); + int progress = float(renderer->getTotalSamplesPerPixelComputed())/float(sensorData.samplesNeeded) * 100; + printf("[INFO] Rendered Successfully - %d%% Progress = %u/%u SamplesPerPixel - FileName = %s. \n", progress, renderer->getTotalSamplesPerPixelComputed(), sensorData.samplesNeeded, screenshotFilePath.filename().string().c_str()); + } - std::filesystem::path filePaths[6] = {}; + receiver.resetKeys(); + } - for(uint32_t f = beginIdx; f < beginIdx + 6; ++f) - { - const auto & sensor = sensors[f]; - filePaths[f] = sensor.outputFilePath; - } + // Denoise Cubemaps that weren't denoised seperately + for(uint32_t i = 0; i < cubemapRenders.size(); ++i) + { + uint32_t beginIdx = cubemapRenders[i].getSensorsBeginIdx(); + assert(beginIdx + 6 <= sensors.size()); + auto borderPixels = sensors[beginIdx].highQualityEdges; - std::string mergedFileName = "Merge_CubeMap_" + mainFileName; - renderer->denoiseCubemapFaces( - filePaths, - mergedFileName, - sensors[beginIdx].cropOffsetX, sensors[beginIdx].cropOffsetY, sensors[beginIdx].cropWidth, sensors[beginIdx].cropHeight, - sensors[beginIdx].denoiserInfo); + std::filesystem::path filePaths[6] = {}; + + for(uint32_t f = beginIdx; f < beginIdx + 6; ++f) + { + const auto & sensor = sensors[f]; + filePaths[f] = sensor.outputFilePath; } + + std::string mergedFileName = "Merge_CubeMap_" + mainFileName; + renderer->denoiseCubemapFaces(filePaths, mergedFileName, borderPixels, sensors[beginIdx].denoiserInfo); } // Interactive - if((applicationState.processSensorsBehaviour != ProcessSensorsBehaviour::PSB_RENDER_ALL_THEN_TERMINATE) && receiver.keepOpen()) + if(!shouldTerminateAfterRenders && receiver.keepOpen()) { - int activeSensor = -1; + int activeSensor = -1; // that outputs to current window when not in TERMIANTE mode. auto setActiveSensor = [&](int index) { if(index >= 0 && index < sensors.size()) { - bool needsReinit; - if (activeSensor != -1) - { - const auto& activeSensorData = sensors[activeSensor]; - const auto& nextSensorData = sensors[index]; - needsReinit = activeSensorData.width!=nextSensorData.width || - activeSensorData.height!=nextSensorData.height || - activeSensorData.cascadeCount!=nextSensorData.cascadeCount || - activeSensorData.envmapRegFactor!=nextSensorData.envmapRegFactor; - } - else - needsReinit = true; + bool needsReinit = (activeSensor == -1) || (sensors[activeSensor].width != sensors[index].width) || (sensors[activeSensor].height != sensors[index].height); // should be >= or != ? activeSensor = index; renderer->resetSampleAndFrameCounters(); if(needsReinit) { renderer->deinitScreenSizedResources(); - const auto& sensorData = sensors[activeSensor]; - renderer->initScreenSizedResources(sensorData.width,sensorData.height,sensorData.envmapRegFactor,sensorData.cascadeCount,sensorData.cascadeLuminanceBase,sensorData.cascadeLuminanceStart,sensorData.Emin,sensorData.clipPlanes); + renderer->initScreenSizedResources(sensors[activeSensor].width,sensors[activeSensor].height); } smgr->setActiveCamera(sensors[activeSensor].interactiveCamera); @@ -600,9 +872,8 @@ device->getAssetManager()->clearAllGPUObjects(); } }; - setActiveSensor(applicationState.startSensorID); + setActiveSensor(0); - bool writeLastRunState = false; uint64_t lastFPSTime = 0; auto start = std::chrono::steady_clock::now(); bool renderFailed = false; @@ -615,42 +886,13 @@ device->getAssetManager()->clearAllGPUObjects(); sensors[activeSensor].resetInteractiveCamera(); std::cout << "Interactive Camera Position and Target has been Reset." << std::endl; } - else if(receiver.isOverloadCameraKeyPressed()) - { - pfd::open_file file("Choose XML file to overload camera with (only first sensor overrides)", "../../media/mitsuba", { "XML files (.xml)", "*.xml" }); - if (!file.result().empty()) - { - const auto filePath = file.result()[0]; - using namespace nbl::asset; - smart_refctd_ptr mitsubaMetadata; - { - static const IAssetLoader::SAssetLoadParams mitsubaLoaderParams = { 0, nullptr, IAssetLoader::ECF_DONT_CACHE_REFERENCES, nullptr, IAssetLoader::ELPF_LOAD_METADATA_ONLY }; - auto meshes_bundle = device->getAssetManager()->getAsset(filePath.data(),mitsubaLoaderParams); - if (!meshes_bundle.getContents().empty()) - mitsubaMetadata = smart_refctd_ptr(static_cast(meshes_bundle.getMetadata())); - } - if (!mitsubaMetadata || mitsubaMetadata->m_global.m_sensors.empty()) - os::Printer::log("ERROR (" + std::to_string(__LINE__) + " line): The xml file is invalid/cannot be loaded! File path: " + filePath, ELL_ERROR); - else - { - const uint32_t originalSensorCount = sensors.size(); - uint32_t idx = originalSensorCount; - for (const auto& sensor : mitsubaMetadata->m_global.m_sensors) - extractAndAddToSensorData(sensor,idx++); - setActiveSensor(originalSensorCount); - } - writeLastRunState = true; - } - } - else if(receiver.isNextPressed()) + if(receiver.isNextPressed()) { setActiveSensor(activeSensor + 1); - writeLastRunState = true; } - else if(receiver.isPreviousPressed()) + if(receiver.isPreviousPressed()) { setActiveSensor(activeSensor - 1); - writeLastRunState = true; } if(receiver.isScreenshotKeyPressed()) { @@ -686,53 +928,19 @@ device->getAssetManager()->clearAllGPUObjects(); } if(receiver.isLogProgressKeyPressed()) { - printf("[INFO] Rendering in progress - %d Total SamplesPerPixel Computed.\n", renderer->getTotalSamplesPerPixelComputed()); + printf("[INFO] Rendering in progress - %d Total SamplesPerPixel Computed. \n", renderer->getTotalSamplesPerPixelComputed()); } + receiver.resetKeys(); } driver->beginScene(false, false); - if(!renderer->render( - device->getTimer(), - activeSensor!=-1 ? sensors[activeSensor].kappa:0.f, - activeSensor!=-1 ? sensors[activeSensor].Emin:0.f, - true,receiver.isRenderingBeauty() - )) + if(!renderer->render(device->getTimer(),true,receiver.isRenderingBeauty())) { renderFailed = true; driver->endScene(); break; } - if (writeLastRunState) - { - applicationState.isBeauty = receiver.isRenderingBeauty(); - applicationState.isInteractiveMode = true; - applicationState.startSensorID = activeSensor; - applicationState.isInteractiveViewMatrixLH = !sensors[activeSensor].rightHandedCamera; - applicationState.interactiveCameraViewMatrix = sensors[activeSensor].interactiveCamera->getViewMatrix(); - - applicationState.writeToDisk(); - - writeLastRunState = false; - } - - // Post-frame input handling - { - if (receiver.isReloadKeyPressed()) - { - applicationState.isBeauty = receiver.isRenderingBeauty(); - applicationState.isInteractiveMode = true; - applicationState.startSensorID = activeSensor; - applicationState.isInteractiveViewMatrixLH = !sensors[activeSensor].rightHandedCamera; - applicationState.interactiveCameraViewMatrix = sensors[activeSensor].interactiveCamera->getViewMatrix(); - - applicationState.writeToDisk(); - - reloadApplication(); - } - receiver.resetKeys(); - } - auto oldVP = driver->getViewPort(); driver->blitRenderTargets(renderer->getColorBuffer(),nullptr,false,false,{},{},true); driver->setViewPort(oldVP); @@ -747,8 +955,7 @@ device->getAssetManager()->clearAllGPUObjects(); auto samples = renderer->getTotalSamplesComputed(); auto rays = renderer->getTotalRaysCast(); const double microsecondsElapsed = std::chrono::duration_cast(std::chrono::steady_clock::now()-start).count(); - str << L"Nabla Path Tracer: " << applicationState.zipPath.c_str() << "\\" << applicationState.xmlPath.c_str() - << " MegaSamples: " << samples/1000000ull + str << L"Raytraced Shadows Demo - Nabla Engine MegaSamples: " << samples/1000000ull << " MSample/s: " << double(samples)/microsecondsElapsed << " MRay/s: " << double(rays)/microsecondsElapsed; @@ -776,6 +983,4 @@ device->getAssetManager()->clearAllGPUObjects(); // will leak thread because there's no cross platform input! std::exit(0); return 0; -} - -extern "C" { _declspec(dllexport) DWORD NvOptimusEnablement = 0x00000001; } \ No newline at end of file +} \ No newline at end of file diff --git a/22_RaytracedAO/mergeCubemap.bat b/22_RaytracedAO/mergeCubemap.bat new file mode 100644 index 000000000..3efc58ceb --- /dev/null +++ b/22_RaytracedAO/mergeCubemap.bat @@ -0,0 +1,31 @@ +@echo off + +set right=%1 +set left=%2 +set top=%3 +set bottom=%4 +set front=%5 +set back=%6 +set output=%~dpn7 + +REM examplary usage: +REM mergeCubemap.bat right.png left.png top.png bottom.png front.png back.png outputImageName + +REM set image size +for /f "tokens=*" %%s in ('magick identify -format "%%w" %right%') do set sz=%%s + +REM set image fromat +for /f "tokens=*" %%s in ('magick identify -format "%%m" %right%') do set format=%%s + +set /a szx2=2*sz +set /a outputWidth=3*sz +set /a outputHeight=2*sz + +magick convert -size %outputwidth%x%outputHeight% canvas:none ^ +-draw "image over 0,0 0,0 '%right%'" ^ +-draw "image over %sz%,0 0,0 '%left%'" ^ +-draw "image over %szx2%,0 0,0 '%top%'" ^ +-draw "image over 0,%sz% 0,0 '%bottom%'" ^ +-draw "image over %sz%,%sz% 0,0 '%front%'" ^ +-draw "image over %szx2%,%sz% 0,0 '%back%'" ^ +%output%.%format% \ No newline at end of file diff --git a/31_HLSLPathTracer/pipeline.groovy b/22_RaytracedAO/pipeline.groovy similarity index 85% rename from 31_HLSLPathTracer/pipeline.groovy rename to 22_RaytracedAO/pipeline.groovy index 955e77cec..04729bc71 100644 --- a/31_HLSLPathTracer/pipeline.groovy +++ b/22_RaytracedAO/pipeline.groovy @@ -2,9 +2,9 @@ import org.DevshGraphicsProgramming.Agent import org.DevshGraphicsProgramming.BuilderInfo import org.DevshGraphicsProgramming.IBuilder -class CHLSLPathTracerBuilder extends IBuilder +class CRaytracedAOBuilder extends IBuilder { - public CHLSLPathTracerBuilder(Agent _agent, _info) + public CRaytracedAOBuilder(Agent _agent, _info) { super(_agent, _info) } @@ -44,7 +44,7 @@ class CHLSLPathTracerBuilder extends IBuilder def create(Agent _agent, _info) { - return new CHLSLPathTracerBuilder(_agent, _info) + return new CRaytracedAOBuilder(_agent, _info) } -return this +return this \ No newline at end of file diff --git a/22_RaytracedAO/raygen.comp b/22_RaytracedAO/raygen.comp index 2b45dec4d..c4d9f0d51 100644 --- a/22_RaytracedAO/raygen.comp +++ b/22_RaytracedAO/raygen.comp @@ -12,7 +12,7 @@ layout(set = 3, binding = 2, rgba16f) restrict uniform image2D framebuffer; bool get_sample_job() { - return all(lessThan(gl_GlobalInvocationID.xy,getImageDimensions(staticViewData))); + return all(lessThan(gl_GlobalInvocationID.xy,staticViewData.imageDimensions)); } vec3 unpack_barycentrics(in uint data) @@ -24,27 +24,49 @@ vec3 unpack_barycentrics(in uint data) void main() { clear_raycount(); - - uint rayMask = 0u; if (get_sample_job()) { // vis buffer read const uvec2 outPixelLocation = gl_GlobalInvocationID.xy; const uvec4 visBuffer = texelFetch(frontFacingTriangleIDDrawID_unorm16Bary_dBarydScreenHalf2x2,ivec2(outPixelLocation),0); - const vec2 texCoordUV = (vec2(outPixelLocation)+vec2(0.5))/vec2(getImageDimensions(staticViewData)); - normalizedV = normalize(mat3(pc.cummon.viewDirReconFactors)*vec3(texCoordUV,1.f)); + const vec2 texCoordUV = (vec2(outPixelLocation)+vec2(0.5)) / vec2(staticViewData.imageDimensions); + const vec2 NDC = texCoordUV * vec2(2.0,-2.0) + vec2(-1.0,1.0); + /* + mat3 ndcToV = mat3( + pc.cummon.viewProjMatrixInverse[0].xyz*pc.cummon.camPos.x, + pc.cummon.viewProjMatrixInverse[1].xyz*pc.cummon.camPos.y, + pc.cummon.viewProjMatrixInverse[2].xyz*pc.cummon.camPos.z + )-mat3(pc.cummon.viewProjMatrixInverse); + */ + // NDC -> ViewSpace + // (invProj*NDC).xyz/(invProj*NDC).www + // ViewSpace -> WorldSpace + // invView*viewSpace + // ViewSpace Dir -> WorldSpace Dir + // inverse(mat3(view))*viewDir + // NDC dir -> View Space Dir + // (invProj*NDC).xyz/(invProj*NDC).www-camPos + // camPos = (invProj[3]).xyz/invPorj[3].www + // inverse(mat3(view))*viewDir + // NDC dir -> World Space Dir + // (invViewProj*NDC).xyz/(invViewProj*NDC).www-camPos + // camPos = (invViewProj[3]).xyz/invViewProj[3].www + const vec4 tmpA = pc.cummon.viewProjMatrixInverse*vec4(NDC,1.f,1.f); + const vec4 tmpB = pc.cummon.viewProjMatrixInverse[3].xyzw; + normalizedV = normalize(pc.cummon.camPos-tmpA.xyz/tmpA.www); - const uint samplesPerPixelPerDispatch = bitfieldExtract(staticViewData.maxPathDepth_noRussianRouletteDepth_samplesPerPixelPerDispatch,16,16); + const uint samplesPerPixelPerDispatch = bitfieldExtract(staticViewData.pathDepth_noRussianRouletteDepth_samplesPerPixelPerDispatch,16,16); Contribution contrib; const bool hit = visBuffer[0]!=0xffffffffu; if (hit) { // vis buffer decode + const bool frontfacing = !bool(visBuffer[0]&0x80000000u); const int triangleIDBitcount = findMSB(MAX_TRIANGLES_IN_BATCH-1)+1; const uint triangleID = bitfieldExtract(visBuffer[0],31-triangleIDBitcount,triangleIDBitcount); const uint batchInstanceGUID = bitfieldExtract(visBuffer[0],0,31-triangleIDBitcount); -//const vec2 compactBary = unpackUnorm2x16(visBuffer[1]); + const vec2 compactBary = unpackUnorm2x16(visBuffer[1]); #ifdef TEX_PREFETCH_STREAM // TODO: separate pipeline and separate out the barycentric derivative FBO attachment, only write if need to, only fetch if `needs_texture_prefetch` const mat2 dBarydScreen = mat2(unpackHalf2x16(visBuffer[2]),unpackHalf2x16(visBuffer[3])); @@ -53,134 +75,75 @@ void main() // const nbl_glsl_ext_Mitsuba_Loader_instance_data_t batchInstanceData = InstData.data[batchInstanceGUID]; const uvec3 indices = get_triangle_indices(batchInstanceData,triangleID); - - // load vertex data - const vec3 lastVxPos = load_positions(batchInstanceData,indices); - if (!bool(batchInstanceData.determinantSignBit&0x80000000u)) - normalizedG = -normalizedG; - const float VdotG = dot(normalizedV,normalizedG); - const bool frontfacing = VdotG>=0.f; - - // get material + + // get material while waiting for indices const nbl_glsl_MC_oriented_material_t material = nbl_glsl_MC_material_data_t_getOriented(batchInstanceData.material,frontfacing); - contrib.color = contrib.albedo = nbl_glsl_MC_oriented_material_t_getEmissive(material,normalizedV); + contrib.color = contrib.albedo = nbl_glsl_MC_oriented_material_t_getEmissive(material); + + // load vertex data + vec3 geomNormal; + const vec3 lastVxPos = load_positions(geomNormal,batchInstanceData,indices); // little optimization for non-twosided materials if (material.genchoice_count!=0u) - { - // get initial scramble key + { + // get initial scramble key while waiting for vertex positions const nbl_glsl_xoroshiro64star_state_t scramble_start_state = texelFetch(scramblebuf,ivec2(outPixelLocation),0).rg; - vec3 origin; - #if RECOMPUTE_BARY - // we know the ray will intersect the triangle - vec2 compactBary; - { - // reversed order of arguments for each cross cause V is negative - const vec3 ray_cross_e2 = cross(dPdBary[1],normalizedV); - const float detRcp = 1.f/dot(dPdBary[0],ray_cross_e2); - // assert(!isinf(detRcp)); - const vec3 s = (pc.cummon.viewDirReconFactors[3]-lastVxPos)*detRcp; - const float u = dot(s,ray_cross_e2); - // assert(0.f<=u && u<=1.f) - const vec3 s_cross_e1 = cross(s,dPdBary[0]); - const float v = -dot(normalizedV,s_cross_e1); - // assert(0.f<=v && v<=1.f) - compactBary = vec2(u,v); - // - const float t = dot(dPdBary[1],s_cross_e1); - //assert(t>0.f); - origin = pc.cummon.viewDirReconFactors[3]-normalizedV*t; - } - #else - const vec2 compactBary = unpackUnorm2x16(visBuffer[1]); - #endif - origin = dPdBary*compactBary+lastVxPos; - // normalizedN = load_normal_and_prefetch_textures( - batchInstanceData,indices,compactBary,material + batchInstanceData,indices,compactBary,geomNormal,material #ifdef TEX_PREFETCH_STREAM ,dBarydScreen #endif ); + + const vec3 origin = dPdBary*compactBary+lastVxPos; + normalizedV = normalize(pc.cummon.camPos-origin); // generate rays const uint vertex_depth = 1u; - rayMask = generate_next_rays( + generate_next_rays( samplesPerPixelPerDispatch,material,frontfacing,vertex_depth, - scramble_start_state,pc.cummon.frameLowDiscrepancySequenceShift,outPixelLocation,origin, + scramble_start_state,pc.cummon.samplesComputed,outPixelLocation,origin, vec3(pc.cummon.rcpFramesDispatched),1.f,contrib.albedo,contrib.worldspaceNormal ); } else - contrib.worldspaceNormal = normalizedG*nbl_glsl_MC_colorToScalar(contrib.albedo); + contrib.worldspaceNormal = geomNormal*nbl_glsl_MC_colorToScalar(contrib.albedo); } else Contribution_initMiss(contrib,1.f); - if (bool(bitfieldExtract(pc.cummon.pathDepth_rayCountWriteIx,0,RAYCOUNT_SHIFT))) + if (bool(pc.cummon.depth)) { Contribution_normalizeAoV(contrib); // we could optimize this, but its pointless before using KHR_ray_query const bool firstFrame = pc.cummon.rcpFramesDispatched==1.f; - const float luma = nbl_glsl_MC_colorToScalar(contrib.color); for (uint i=0u; i>i)&0x1u); - if (pathToBeContinued) - { - storeAccumulation(contrib.color*pc.cummon.rcpFramesDispatched,coord); - splat.cascadeWeights = vec2(0.f,0.f); - } - - const uint higherCascade = splat.lowerCascade+1u; - const uint cascadeCount = staticViewData.cascadeParams.penultimateCascadeIx+2u; - for (uint cascadeIx=0u; cascadeIx>31); // clear accumulations totally if beginning a new frame if (firstFrame) { - if (!isRWMCEnabled()) - storeAccumulation(contrib.color,coord); + storeAccumulation(contrib.color,coord); storeAlbedo(contrib.albedo,coord); storeWorldspaceNormal(contrib.worldspaceNormal,coord); - storeMask(hideEnvmap&&(!hit) ? 1.f:0.f,coord); } else { - if (!isRWMCEnabled()) - { - const vec3 prev = fetchAccumulation(coord); - const vec3 delta = (contrib.color-prev)*pc.cummon.rcpFramesDispatched; - if (any(greaterThan(abs(delta),vec3(exp2(-19.f))))) - storeAccumulation(prev+delta,coord); - } - addAlbedo(contrib.albedo,coord,pc.cummon.rcpFramesDispatched); - addWorldspaceNormal(contrib.worldspaceNormal,coord,pc.cummon.rcpFramesDispatched); - if (hideEnvmap) - addMask(hit ? 0.f:1.f,coord,pc.cummon.rcpFramesDispatched); + const vec3 acc_emissive = fetchAccumulation(coord); + const vec3 acc_albedo = fetchAlbedo(coord); + const vec3 acc_worldspaceNormal = fetchWorldspaceNormal(coord); + + const vec3 emissive_delta = (contrib.color-acc_emissive)*pc.cummon.rcpFramesDispatched; + const vec3 albedo_delta = (contrib.albedo-acc_albedo)*pc.cummon.rcpFramesDispatched; + const vec3 worldspaceNormal_delta = (contrib.worldspaceNormal-acc_worldspaceNormal)*pc.cummon.rcpFramesDispatched; + + storeAccumulation(acc_emissive,emissive_delta,coord); + storeAlbedo(acc_albedo,albedo_delta,coord); + storeWorldspaceNormal(acc_worldspaceNormal,worldspaceNormal_delta,coord); } } } diff --git a/22_RaytracedAO/raytraceCommon.glsl b/22_RaytracedAO/raytraceCommon.glsl index 10f49273f..7ad45c135 100644 --- a/22_RaytracedAO/raytraceCommon.glsl +++ b/22_RaytracedAO/raytraceCommon.glsl @@ -2,9 +2,7 @@ #define _RAYTRACE_COMMON_GLSL_INCLUDED_ #include "virtualGeometry.glsl" -#include -#include layout(push_constant, row_major) uniform PushConstants { @@ -25,7 +23,6 @@ layout(set = 2, binding = 0, row_major) uniform StaticViewData { StaticViewData_t staticViewData; }; - // rng layout(set = 2, binding = 1) uniform usamplerBuffer quantizedSampleSequence; // accumulation @@ -44,16 +41,13 @@ layout(set = 2, binding = 4) restrict coherent buffer RayCount // maybe remove c // aovs layout(set = 2, binding = 5, r32ui) restrict uniform uimage2DArray albedoAOV; layout(set = 2, binding = 6, r32ui) restrict uniform uimage2DArray normalAOV; -layout(set = 2, binding = 7, r16) restrict uniform image2DArray maskAOV; // environment emitter -layout(set = 2, binding = 8) uniform sampler2D envMap; -layout(set = 2, binding = 9) uniform sampler2D warpMap; -layout(set = 2, binding = 10) uniform sampler2D luminance; +layout(set = 2, binding = 7) uniform sampler2D envMap; void clear_raycount() { if (all(equal(uvec3(0u),gl_GlobalInvocationID))) - rayCount[(pc.cummon.pathDepth_rayCountWriteIx+(0x1u<>RAYCOUNT_SHIFT] = 0u; + rayCount[(pc.cummon.rayCountWriteIx+1u)&uint(RAYCOUNT_N_BUFFERING_MASK)] = 0u; } // @@ -73,12 +67,6 @@ uvec3 get_triangle_indices(in nbl_glsl_ext_Mitsuba_Loader_instance_data_t batchI #include #include - -bool isRWMCEnabled() -{ - return staticViewData.cascadeParams.penultimateCascadeIx!=uint(-2); -} - vec3 fetchAccumulation(in uvec3 coord) { const uvec2 data = imageLoad(accumulation,ivec3(coord)).rg; @@ -89,117 +77,45 @@ void storeAccumulation(in vec3 color, in uvec3 coord) const uvec2 data = nbl_glsl_encodeRGB19E7(color); imageStore(accumulation,ivec3(coord),uvec4(data,0u,0u)); } -void addAccumulation(in vec3 delta, in uvec3 coord) +void storeAccumulation(in vec3 prev, in vec3 delta, in uvec3 coord) { - if (any(greaterThan(abs(delta),vec3(exp2(-19.f))))) - { - const vec3 prev = fetchAccumulation(coord); - const vec3 newVal = prev+delta; - // TODO: do a better check, compare actually encoded values for difference - const uvec3 diff = floatBitsToUint(newVal)^floatBitsToUint(prev); - if (bool((diff.x|diff.y|diff.z)&0x7ffffff0u)) - storeAccumulation(newVal,coord); - } + const vec3 newVal = prev+delta; + const uvec3 diff = floatBitsToUint(newVal)^floatBitsToUint(prev); + if (bool((diff.x|diff.y|diff.z)&0x7ffffff0u)) + storeAccumulation(newVal,coord); } -// TODO: use a R17G17B17_UNORM format matched to cascade range, then use 13 bits to store last spp count (max 8k spp renders) -// This way we can avoid writing every cascade every path storage -void nextSampleAccumulationCascade(in bool firstFrame, in vec3 weightedDelta, uvec3 coord, in uint samplesPerPixelPerDispatch, in uint cascadeIndex, in float rcpN) +vec3 fetchAlbedo(in uvec3 coord) { - // but leave first index in the array for the ray accumulation metadata, hence the +1 - coord.z += (cascadeIndex+1u)*samplesPerPixelPerDispatch; - const vec3 prev = firstFrame ? vec3(0.0):fetchAccumulation(coord); - const vec3 newVal = prev+(weightedDelta-prev)*rcpN; - // always store, cause we need to reset the value - storeAccumulation(newVal,coord); -} -void addAccumulationCascade(in vec3 weightedDelta, uvec3 coord, in uint samplesPerPixelPerDispatch, in uint cascadeIndex) -{ - if (any(greaterThan(abs(weightedDelta),vec3(exp2(-19.f))))) - { - // but leave first index in the array for the ray accumulation metadata, hence the +1 - coord.z += (cascadeIndex+1u)*samplesPerPixelPerDispatch; - const vec3 prev = fetchAccumulation(coord); - const vec3 newVal = prev+weightedDelta; - // TODO: do a better check, compare actually encoded values for difference - const uvec3 diff = floatBitsToUint(newVal)^floatBitsToUint(prev); - if (bool((diff.x|diff.y|diff.z)&0x7ffffff0u)) - storeAccumulation(newVal,coord); - } + const uint data = imageLoad(albedoAOV,ivec3(coord)).r; + return nbl_glsl_decodeRGB10A2_UNORM(data).rgb; } - void storeAlbedo(in vec3 color, in uvec3 coord) { const uint data = nbl_glsl_encodeRGB10A2_UNORM(vec4(color,1.f)); imageStore(albedoAOV,ivec3(coord),uvec4(data,0u,0u,0u)); } -void impl_addAlbedo(vec3 delta, in uvec3 coord, in float rcpN, in bool newSample) +void storeAlbedo(in vec3 prev, in vec3 delta, in uvec3 coord) { - const uint data = imageLoad(albedoAOV,ivec3(coord)).r; - const vec3 prev = nbl_glsl_decodeRGB10A2_UNORM(data).rgb; - if (newSample) - delta = (delta-prev)*rcpN; if (any(greaterThan(abs(delta),vec3(1.f/1024.f)))) storeAlbedo(prev+delta,coord); } -// for starting a new sample -void addAlbedo(vec3 delta, in uvec3 coord, in float rcpN) -{ - impl_addAlbedo(delta,coord,rcpN,true); -} -// for adding to the last sample -void addAlbedo(vec3 delta, in uvec3 coord) + +vec3 fetchWorldspaceNormal(in uvec3 coord) { - impl_addAlbedo(delta,coord,0.f,false); + const uint data = imageLoad(normalAOV,ivec3(coord)).r; + return nbl_glsl_decodeRGB10A2_SNORM(data).xyz; } - void storeWorldspaceNormal(in vec3 normal, in uvec3 coord) { const uint data = nbl_glsl_encodeRGB10A2_SNORM(vec4(normal,1.f)); imageStore(normalAOV,ivec3(coord),uvec4(data,0u,0u,0u)); } -void impl_addWorldspaceNormal(vec3 delta, in uvec3 coord, in float rcpN, in bool newSample) +void storeWorldspaceNormal(in vec3 prev, in vec3 delta, in uvec3 coord) { - const uint data = imageLoad(normalAOV,ivec3(coord)).r; - const vec3 prev = nbl_glsl_decodeRGB10A2_SNORM(data).rgb; - if (newSample) - delta = (delta-prev)*rcpN; if (any(greaterThan(abs(delta),vec3(1.f/512.f)))) storeWorldspaceNormal(prev+delta,coord); } -// for starting a new sample -void addWorldspaceNormal(vec3 delta, in uvec3 coord, in float rcpN) -{ - impl_addWorldspaceNormal(delta,coord,rcpN,true); -} -// for adding to the last sample -void addWorldspaceNormal(vec3 delta, in uvec3 coord) -{ - impl_addWorldspaceNormal(delta,coord,0.f,false); -} - -void storeMask(in float mask, in uvec3 coord) -{ - imageStore(maskAOV,ivec3(coord),vec4(mask,0.f,0.f,0.f)); -} -void impl_addMask(float delta, in uvec3 coord, in float rcpN, in bool newSample) -{ - const float prev = imageLoad(maskAOV,ivec3(coord)).r; - if (newSample) - delta = (delta-prev)*rcpN; - if (abs(delta)>1.f/65536.f) - storeMask(prev+delta,coord); -} -// for starting a new sample -void addMask(float delta, in uvec3 coord, in float rcpN) -{ - impl_addMask(delta,coord,rcpN,true); -} -// for adding to the last sample -void addMask(float delta, in uvec3 coord) -{ - impl_addMask(delta,coord,0.f,false); -} // due to memory limitations we can only do 6k renders // so that's 13 bits for width, 12 bits for height, which leaves us with 7 bits for throughput @@ -219,11 +135,6 @@ void unpackOutPixelLocationAndAoVThroughputFactor(in float val, out uvec2 outPix #include "bin/runtime_defines.glsl" #include -vec3 normalizedG; -vec3 nbl_glsl_MC_getNormalizedWorldSpaceG() -{ - return normalizedG; -} vec3 normalizedV; vec3 nbl_glsl_MC_getNormalizedWorldSpaceV() { @@ -243,7 +154,7 @@ bool has_world_transform(in nbl_glsl_ext_Mitsuba_Loader_instance_data_t batchIns #include mat2x3 dPdBary; -vec3 load_positions(in nbl_glsl_ext_Mitsuba_Loader_instance_data_t batchInstanceData, in uvec3 indices) +vec3 load_positions(out vec3 geomNormal, in nbl_glsl_ext_Mitsuba_Loader_instance_data_t batchInstanceData, in uvec3 indices) { mat3 positions = mat3( nbl_glsl_fetchVtxPos(indices[0],batchInstanceData), @@ -256,7 +167,7 @@ vec3 load_positions(in nbl_glsl_ext_Mitsuba_Loader_instance_data_t batchInstance // for (int i=0; i<2; i++) dPdBary[i] = positions[i]-positions[2]; - normalizedG = normalize(cross(dPdBary[0],dPdBary[1])); + geomNormal = normalize(cross(dPdBary[0],dPdBary[1])); // if (tform) positions[2] += batchInstanceData.tform[3]; @@ -285,7 +196,7 @@ bool needs_texture_prefetch(in nbl_glsl_ext_Mitsuba_Loader_instance_data_t batch vec3 load_normal_and_prefetch_textures( in nbl_glsl_ext_Mitsuba_Loader_instance_data_t batchInstanceData, - in uvec3 indices, in vec2 compactBary, + in uvec3 indices, in vec2 compactBary, in vec3 geomNormal, in nbl_glsl_MC_oriented_material_t material #ifdef TEX_PREFETCH_STREAM ,in mat2 dBarydScreen @@ -306,10 +217,6 @@ vec3 load_normal_and_prefetch_textures( dUVdBary = mat2(uvs[0]-uvs[2],uvs[1]-uvs[2]); const vec2 UV = dUVdBary*compactBary+uvs[2]; - // flip the tangent frame if mesh got flipped to undo Left Handed tangent frame - if (!bool(batchInstanceData.determinantSignBit&0x80000000u)) - dUVdBary = -dUVdBary; - // the direction/winding of the UV-space parallelogram doesn't matter for texture filtering const mat2 dUVdScreen = nbl_glsl_applyChainRule2D(dUVdBary,dBarydScreen); nbl_glsl_MC_runTexPrefetchStream(tps,UV,dUVdScreen*pc.cummon.textureFootprintFactor); } @@ -340,150 +247,52 @@ vec3 load_normal_and_prefetch_textures( } // TODO: this check wouldn't be needed if we had `needsSmoothNormals` implemented if (!isnan(smoothNormal.x)) - return nbl_glsl_MC_pullUpNormal(normalize(smoothNormal),normalizedV,normalizedG); + return normalize(smoothNormal); } - return normalizedG; + return geomNormal; } #include -mat2x3 rand6d(in uvec3 scramble_keys[2], in int _sample, int depth) +vec3 rand3d(in uvec3 scramble_key, in int _sample, int depth) { - mat2x3 retVal; // decrement depth because first vertex is rasterized and picked with a different sample sequence --depth; // - const int offset = _sample*int(staticViewData.sampleSequenceStride_hideEnvmap&0x7fFFffFFu)+depth*SAMPLING_STRATEGY_COUNT; - - const nbl_glsl_sampling_quantized3D quant1 = texelFetch(quantizedSampleSequence, offset).xy; - const nbl_glsl_sampling_quantized3D quant2 = texelFetch(quantizedSampleSequence, offset+1).xy; - retVal[0] = nbl_glsl_sampling_decodeSample3Dimensions(quant1,scramble_keys[0]); - retVal[1] = nbl_glsl_sampling_decodeSample3Dimensions(quant2,scramble_keys[1]); - return retVal; + const nbl_glsl_sampling_quantized3D quant = texelFetch(quantizedSampleSequence,int(_sample)*SAMPLE_SEQUENCE_STRIDE+depth).xy; + return nbl_glsl_sampling_decodeSample3Dimensions(quant,scramble_key); } -#include - - nbl_glsl_MC_quot_pdf_aov_t gen_sample_ray( out vec3 direction, - in uvec3 scramble_keys[2], + in uvec3 scramble_key, in uint sampleID, in uint depth, in nbl_glsl_MC_precomputed_t precomp, in nbl_glsl_MC_instr_stream_t gcs, in nbl_glsl_MC_instr_stream_t rnps ) { - mat2x3 rand = rand6d(scramble_keys,int(sampleID),int(depth)); - - // (1) BXDF Sample and Weight - nbl_glsl_LightSample bxdfSample; - nbl_glsl_MC_quot_pdf_aov_t bxdfCosThroughput = nbl_glsl_MC_runGenerateAndRemainderStream(precomp,gcs,rnps,rand[0],bxdfSample); - - nbl_glsl_LightSample outSample; - nbl_glsl_MC_quot_pdf_aov_t result; - -#ifndef ONLY_BXDF_SAMPLING - float bxdfWeight = 0; - - float p_bxdf_bxdf = bxdfCosThroughput.pdf; // BxDF PDF evaluated with BxDF sample (returned from BxDF sampling) - // Envmap PDF evaluated with BxDF sample (returned by manual tap of the envmap PDF texture) - float p_env_bxdf = nbl_glsl_ext_HierarchicalWarp_deferred_pdf(worldSpaceToMitsubaEnvmap(bxdfSample.L), luminance, staticViewData.envMapPDFNormalizationFactor); - //assert(p_env_bxdf>FLT_MIN && p_env_bxdfnoRussianRouletteDepth) { const float rrContinuationFactor = 0.25f; const float survivalProb = min(nbl_glsl_MC_colorToScalar(result.quotient)/rrContinuationFactor,1.f); result.pdf *= survivalProb; float dummy; // not going to use it, because we can optimize out better - const bool kill = nbl_glsl_partitionRandVariable(survivalProb,rand[0].z,dummy); + const bool kill = nbl_glsl_partitionRandVariable(survivalProb,rand.z,dummy); result.quotient *= kill ? 0.f:(1.f/survivalProb); } - direction = outSample.L; + direction = s.L; return result; } -uint generate_next_rays( +void generate_next_rays( in uint maxRaysToGen, in nbl_glsl_MC_oriented_material_t material, in bool frontfacing, in uint vertex_depth, nbl_glsl_xoroshiro64star_state_t scramble_state, in uint sampleID, in uvec2 outPixelLocation, in vec3 origin, in vec3 prevThroughput, in float prevAoVThroughputScale, inout vec3 albedo, out vec3 worldspaceNormal) @@ -508,16 +317,12 @@ uint generate_next_rays( vec3 nextThroughput[MAX_RAYS_GENERATED]; float nextAoVThroughputScale[MAX_RAYS_GENERATED]; { - const uvec3 scramble_keys[2] = { - uvec3(nbl_glsl_xoroshiro64star(scramble_state),nbl_glsl_xoroshiro64star(scramble_state),nbl_glsl_xoroshiro64star(scramble_state)), - uvec3(nbl_glsl_xoroshiro64star(scramble_state),nbl_glsl_xoroshiro64star(scramble_state),nbl_glsl_xoroshiro64star(scramble_state)) - }; - + const uvec3 scramble_key = uvec3(nbl_glsl_xoroshiro64star(scramble_state),nbl_glsl_xoroshiro64star(scramble_state),nbl_glsl_xoroshiro64star(scramble_state)); for (uint i=0u; i>RAYCOUNT_SHIFT],raysToAllocate); - - // set to 1 if ray generated - uint rayMask = 0u; + const uint baseOutputID = atomicAdd(rayCount[pc.cummon.rayCountWriteIx],raysToAllocate); // the 1.03125f adjusts for the fact that the normal might be too short (inversesqrt precision) const float inversesqrt_precision = 1.03125f; - const vec3 ray_offset_vector = normalizedG*inversesqrt_precision; - float origin_offset = nbl_glsl_numeric_limits_float_epsilon(120u); // I pulled the constants out of my @$$ - origin_offset += dot(abs(ray_offset_vector),abs(origin))*nbl_glsl_numeric_limits_float_epsilon(128u); - + // TODO: investigate why we can't use `normalizedN` here + const vec3 ray_offset_vector = normalize(cross(dPdBary[0],dPdBary[1]))*inversesqrt_precision; + float origin_offset = nbl_glsl_numeric_limits_float_epsilon(44u); // I pulled the constants out of my @$$ + origin_offset += dot(abs(ray_offset_vector),abs(origin))*nbl_glsl_numeric_limits_float_epsilon(32u); // TODO: in the future run backward error analysis of // dot(mat3(WorldToObj)*(origin+offset*geomNormal/length(geomNormal))+(WorldToObj-vx_pos[1]),geomNormal) // where @@ -553,7 +355,6 @@ uint generate_next_rays( //const vec3 geomNormal = cross(dPdBary[0],dPdBary[1]); //float ray_offset = ?; //ray_offset = nbl_glsl_ieee754_next_ulp_away_from_zero(ray_offset); - const vec3 ray_offset = ray_offset_vector*origin_offset; const vec3 ray_origin[2] = {origin+ray_offset,origin-ray_offset}; uint offset = 0u; @@ -574,12 +375,10 @@ uint generate_next_rays( newRay.useless_padding[1] = bitfieldInsert(packHalf2x16(nextThroughput[i].bb),sampleID+i,16,16); const uint outputID = baseOutputID+(offset++); sinkRays[outputID] = newRay; - - rayMask |= 0x1u<(); const auto unitBoxScale = obb.getScale(); const float obbArea = 2.f * (unitBoxScale.x * unitBoxScale.y + unitBoxScale.x * unitBoxScale.z + unitBoxScale.y * unitBoxScale.z); - return luma * unitHemisphereArea * obbArea; + return nbl::core::dot(radiance, rec709LumaCoeffs).x * unitHemisphereArea * obbArea; } #endif @@ -83,5 +81,31 @@ struct SLight **/ }; -#include + + +// +struct StaticViewData_t +{ + uvec2 imageDimensions; +#ifdef __cplusplus + uint8_t pathDepth; + uint8_t noRussianRouletteDepth; + uint16_t samplesPerPixelPerDispatch; +#else + uint pathDepth_noRussianRouletteDepth_samplesPerPixelPerDispatch; +#endif + uint lightCount; +}; + +struct RaytraceShaderCommonData_t +{ + mat4 viewProjMatrixInverse; + vec3 camPos; + float rcpFramesDispatched; + uint samplesComputed; + uint depth; // 0 if path tracing disabled + uint rayCountWriteIx; + float textureFootprintFactor; +}; + #endif \ No newline at end of file diff --git a/22_RaytracedAO/resolve.comp b/22_RaytracedAO/resolve.comp index 33541d08a..033b77b00 100644 --- a/22_RaytracedAO/resolve.comp +++ b/22_RaytracedAO/resolve.comp @@ -1,8 +1,4 @@ #version 430 core -#extension GL_EXT_shader_integer_mix : require - -#define NBL_GLSL_RWMC_LUMA_DEFINED -#define NBL_GLSL_RWMC_SAMPLE_CASCADE_TEXEL_DEFINED #include "raytraceCommon.h" layout(local_size_x = WORKGROUP_DIM, local_size_y = WORKGROUP_DIM) in; @@ -13,90 +9,47 @@ layout(set = 0, binding = 0, row_major) uniform StaticViewData layout(set = 0, binding = 1) uniform usampler2DArray colorSamples; layout(set = 0, binding = 2) uniform sampler2DArray albedoSamples; layout(set = 0, binding = 3) uniform usampler2DArray normalSamples; -layout(set = 0, binding = 4) uniform sampler2DArray maskSamples; -layout(set = 0, binding = 5, rgba16f) restrict uniform image2D framebuffer; -layout(set = 0, binding = 6, r32ui) restrict uniform uimage2D albedo; -layout(set = 0, binding = 7, rgba16f) restrict uniform image2D normals; +layout(set = 0, binding = 4, rgba16f) restrict uniform image2D framebuffer; +layout(set = 0, binding = 5, r32ui) restrict uniform uimage2D albedo; +layout(set = 0, binding = 6, rgba16f) restrict uniform image2D normals; layout(push_constant, row_major) uniform PushConstants { mat4x3 viewMatrix; - nbl_glsl_RWMC_ReweightingParameters rwmcReweightingParams; } pc; #include #include -vec3 fetchAccumulation(in uvec3 coord) +vec3 fetchAccumulation(in uvec2 coord, in uint subsample) { - const uvec2 data = texelFetch(colorSamples,ivec3(coord),0).rg; + const uvec2 data = texelFetch(colorSamples,ivec3(coord,subsample),0).rg; return nbl_glsl_decodeRGB19E7(data); } - -#include -float nbl_glsl_RWMC_luma(in vec3 val) -{ - return dot(val,transpose(nbl_glsl_sRGBtoXYZ)[1]); -} - - -uint samplesPerPixelPerDispatch; - - -vec3 nbl_glsl_RWMC_sampleCascadeTexel(ivec2 coord, in ivec2 offset, in uint cascadeIndex) -{ - coord += offset; - // weird mirror-like wraps but not quite exactly - coord = mix(coord,-coord,greaterThan(ivec2(0,0),coord)); - const ivec2 imgSz = ivec2(getImageDimensions(staticViewData)); - coord = mix(imgSz*2-ivec2(1),coord,lessThan(coord,imgSz)); - - uvec3 sampleCoord = uvec3(coord,cascadeIndex*samplesPerPixelPerDispatch+samplesPerPixelPerDispatch); - vec3 value = fetchAccumulation(sampleCoord); - for (uint i=1u; i + + +#include + + +vec3 nbl_glsl_fetchVtxPos(in uint vtxID, in nbl_glsl_ext_Mitsuba_Loader_instance_data_t batchInstanceData) +{ + nbl_glsl_VG_VirtualAttributePacked_t va = batchInstanceData.padding1; + return nbl_glsl_VG_attribFetch_RGB32_SFLOAT(va,vtxID); +} + +vec3 nbl_glsl_fetchVtxNormal(in uint vtxID, in nbl_glsl_ext_Mitsuba_Loader_instance_data_t batchInstanceData) +{ + nbl_glsl_VG_VirtualAttributePacked_t va = batchInstanceData.determinantSignBit; + const uint codedNormal = nbl_glsl_VG_attribFetch2u(va,vtxID)[0]; + return normalize(nbl_glsl_decodeRGB10A2_SNORM(codedNormal).xyz); +} + +vec2 nbl_glsl_fetchVtxUV(in uint vtxID, in nbl_glsl_ext_Mitsuba_Loader_instance_data_t batchInstanceData) +{ + nbl_glsl_VG_VirtualAttributePacked_t va = batchInstanceData.determinantSignBit; + const uint codedUV = nbl_glsl_VG_attribFetch2u(va,vtxID)[1]; + return unpackHalf2x16(codedUV).xy; +} + + +#endif diff --git a/23_Arithmetic2UnitTest/CMakeLists.txt b/23_Arithmetic2UnitTest/CMakeLists.txt deleted file mode 100644 index a18b7a8c0..000000000 --- a/23_Arithmetic2UnitTest/CMakeLists.txt +++ /dev/null @@ -1,15 +0,0 @@ -include(common) - -nbl_create_executable_project("" "" "" "") - -NBL_CREATE_RESOURCE_ARCHIVE( - NAMESPACE nbl::this_example::builtin - TARGET ${EXECUTABLE_NAME}_builtins - LINK_TO ${EXECUTABLE_NAME} - BIND app_resources - BUILTINS - common.hlsl - shaderCommon.hlsl - testSubgroup.comp.hlsl - testWorkgroup.comp.hlsl -) \ No newline at end of file diff --git a/23_Arithmetic2UnitTest/app_resources/shaderCommon.hlsl b/23_Arithmetic2UnitTest/app_resources/shaderCommon.hlsl deleted file mode 100644 index 5baf9a28d..000000000 --- a/23_Arithmetic2UnitTest/app_resources/shaderCommon.hlsl +++ /dev/null @@ -1,19 +0,0 @@ -#include "app_resources/common.hlsl" - -using namespace nbl; -using namespace hlsl; - -[[vk::push_constant]] PushConstantData pc; - -struct device_capabilities -{ -#ifdef TEST_NATIVE - NBL_CONSTEXPR_STATIC_INLINE bool shaderSubgroupArithmetic = true; -#else - NBL_CONSTEXPR_STATIC_INLINE bool shaderSubgroupArithmetic = false; -#endif -}; - -#ifndef OPERATION -#error "Define OPERATION!" -#endif diff --git a/23_Arithmetic2UnitTest/app_resources/testSubgroup.comp.hlsl b/23_Arithmetic2UnitTest/app_resources/testSubgroup.comp.hlsl deleted file mode 100644 index de1e813f1..000000000 --- a/23_Arithmetic2UnitTest/app_resources/testSubgroup.comp.hlsl +++ /dev/null @@ -1,55 +0,0 @@ -#pragma shader_stage(compute) - -#define operation_t nbl::hlsl::OPERATION - -#include "nbl/builtin/hlsl/glsl_compat/core.hlsl" -#include "nbl/builtin/hlsl/glsl_compat/subgroup_basic.hlsl" -#include "nbl/builtin/hlsl/subgroup2/arithmetic_portability.hlsl" -#include "nbl/builtin/hlsl/subgroup2/arithmetic_params.hlsl" - -#include "app_resources/shaderCommon.hlsl" -#include "nbl/builtin/hlsl/workgroup2/basic.hlsl" - -template -using params_t = SUBGROUP_CONFIG_T; - -typedef vector::base_t, device_capabilities>::ItemsPerInvocation> type_t; - -uint32_t globalIndex() -{ - return glsl::gl_WorkGroupID().x*WORKGROUP_SIZE+workgroup::SubgroupContiguousIndex(); -} - -template -static void subtest(NBL_CONST_REF_ARG(type_t) sourceVal) -{ - const uint64_t outputBufAddr = pc.pOutputBuf[Binop::BindingIndex]; - - assert(glsl::gl_SubgroupSize() == params_t::config_t::Size) - - operation_t > func; - type_t val = func(sourceVal); - - vk::RawBufferStore(outputBufAddr + sizeof(type_t) * globalIndex(), val, sizeof(uint32_t)); -} - -type_t test() -{ - const uint32_t idx = globalIndex(); - type_t sourceVal = vk::RawBufferLoad(pc.pInputBuf + idx * sizeof(type_t)); - - subtest >(sourceVal); - subtest >(sourceVal); - subtest >(sourceVal); - subtest >(sourceVal); - subtest >(sourceVal); - subtest >(sourceVal); - subtest >(sourceVal); - return sourceVal; -} - -[numthreads(WORKGROUP_SIZE,1,1)] -void main() -{ - test(); -} diff --git a/23_Arithmetic2UnitTest/app_resources/testWorkgroup.comp.hlsl b/23_Arithmetic2UnitTest/app_resources/testWorkgroup.comp.hlsl deleted file mode 100644 index 664e2f472..000000000 --- a/23_Arithmetic2UnitTest/app_resources/testWorkgroup.comp.hlsl +++ /dev/null @@ -1,75 +0,0 @@ -#pragma shader_stage(compute) - -#include "nbl/builtin/hlsl/glsl_compat/core.hlsl" -#include "nbl/builtin/hlsl/glsl_compat/subgroup_basic.hlsl" -#include "nbl/builtin/hlsl/subgroup2/arithmetic_portability.hlsl" -#include "nbl/builtin/hlsl/workgroup2/arithmetic.hlsl" - -using config_t = WORKGROUP_CONFIG_T; - -#include "app_resources/shaderCommon.hlsl" - -typedef vector type_t; - -// final (level 1/2) scan needs to fit in one subgroup exactly -groupshared uint32_t scratch[mpl::max_v]; - -#include "nbl/examples/workgroup/DataAccessors.hlsl" -using namespace nbl::hlsl::examples::workgroup; - -static ScratchProxy arithmeticAccessor; - -template -struct operation_t -{ - using binop_base_t = typename Binop::base_t; - using otype_t = typename Binop::type_t; - - // workgroup reduction returns the value of the reduction - // workgroup scans do no return anything, but use the data accessor to do the storing directly - void operator()() - { - using data_proxy_t = PreloadedDataProxy; - data_proxy_t dataAccessor = data_proxy_t::create(pc.pInputBuf, pc.pOutputBuf[Binop::BindingIndex]); - dataAccessor.preload(); -#if IS_REDUCTION - otype_t value = -#endif - OPERATION::template __call(dataAccessor,arithmeticAccessor); - // we barrier before because we alias the accessors for Binop - arithmeticAccessor.workgroupExecutionAndMemoryBarrier(); -#if IS_REDUCTION - [unroll] - for (uint32_t i = 0; i < data_proxy_t::PreloadedDataCount; i++) - dataAccessor.preloaded[i] = value; -#endif - dataAccessor.unload(); - } -}; - - -template -static void subtest() -{ - assert(glsl::gl_SubgroupSize() == config_t::SubgroupSize) - - operation_t func; - func(); -} - -void test() -{ - subtest >(); - subtest >(); - subtest >(); - subtest >(); - subtest >(); - subtest >(); - subtest >(); -} - -[numthreads(config_t::WorkgroupSize,1,1)] -void main() -{ - test(); -} \ No newline at end of file diff --git a/23_Arithmetic2UnitTest/main.cpp b/23_Arithmetic2UnitTest/main.cpp deleted file mode 100644 index 8d70547bc..000000000 --- a/23_Arithmetic2UnitTest/main.cpp +++ /dev/null @@ -1,509 +0,0 @@ -// TODO: copyright notice - - -#include "nbl/examples/examples.hpp" - -#include "app_resources/common.hlsl" -#include "nbl/builtin/hlsl/workgroup2/arithmetic_config.hlsl" -#include "nbl/builtin/hlsl/subgroup2/arithmetic_params.hlsl" - - -using namespace nbl; -using namespace core; -using namespace asset; -using namespace system; -using namespace video; - -// method emulations on the CPU, to verify the results of the GPU methods -template -struct emulatedReduction -{ - using type_t = typename Binop::type_t; - - static inline void impl(type_t* out, const type_t* in, const uint32_t itemCount) - { - const type_t red = std::reduce(in,in+itemCount,Binop::identity,Binop()); - std::fill(out,out+itemCount,red); - } - - static inline constexpr const char* name = "reduction"; -}; -template -struct emulatedScanInclusive -{ - using type_t = typename Binop::type_t; - - static inline void impl(type_t* out, const type_t* in, const uint32_t itemCount) - { - std::inclusive_scan(in,in+itemCount,out,Binop()); - } - static inline constexpr const char* name = "inclusive_scan"; -}; -template -struct emulatedScanExclusive -{ - using type_t = typename Binop::type_t; - - static inline void impl(type_t* out, const type_t* in, const uint32_t itemCount) - { - std::exclusive_scan(in,in+itemCount,out,Binop::identity,Binop()); - } - static inline constexpr const char* name = "exclusive_scan"; -}; - -class Workgroup2ScanTestApp final : public application_templates::BasicMultiQueueApplication, public examples::BuiltinResourcesApplication -{ - using device_base_t = application_templates::BasicMultiQueueApplication; - using asset_base_t = examples::BuiltinResourcesApplication; - -public: - Workgroup2ScanTestApp(const path& _localInputCWD, const path& _localOutputCWD, const path& _sharedInputCWD, const path& _sharedOutputCWD) : - system::IApplicationFramework(_localInputCWD, _localOutputCWD, _sharedInputCWD, _sharedOutputCWD) {} - - bool onAppInitialized(smart_refctd_ptr&& system) override - { - if (!device_base_t::onAppInitialized(std::move(system))) - return false; - if (!asset_base_t::onAppInitialized(std::move(system))) - return false; - - transferDownQueue = getTransferDownQueue(); - computeQueue = getComputeQueue(); - - // TODO: get the element count from argv - const uint32_t elementCount = 1024 * 1024; - // populate our random data buffer on the CPU and create a GPU copy - inputData = new uint32_t[elementCount]; - smart_refctd_ptr gpuinputDataBuffer; - { - std::mt19937 randGenerator(0xdeadbeefu); - for (uint32_t i = 0u; i < elementCount; i++) - inputData[i] = randGenerator(); // TODO: change to using xoroshiro, then we can skip having the input buffer at all - - IGPUBuffer::SCreationParams inputDataBufferCreationParams = {}; - inputDataBufferCreationParams.size = sizeof(uint32_t) * elementCount; - inputDataBufferCreationParams.usage = IGPUBuffer::EUF_STORAGE_BUFFER_BIT | IGPUBuffer::EUF_TRANSFER_DST_BIT | IGPUBuffer::EUF_SHADER_DEVICE_ADDRESS_BIT; - m_utils->createFilledDeviceLocalBufferOnDedMem( - SIntendedSubmitInfo{.queue=getTransferUpQueue()}, - std::move(inputDataBufferCreationParams), - inputData - ).move_into(gpuinputDataBuffer); - } - - // create 8 buffers for 8 operations - for (auto i=0u; igetSize(); - params.usage = bitflag(IGPUBuffer::EUF_STORAGE_BUFFER_BIT) | IGPUBuffer::EUF_TRANSFER_SRC_BIT | IGPUBuffer::EUF_SHADER_DEVICE_ADDRESS_BIT; - - outputBuffers[i] = m_device->createBuffer(std::move(params)); - auto mreq = outputBuffers[i]->getMemoryReqs(); - mreq.memoryTypeBits &= m_physicalDevice->getDeviceLocalMemoryTypeBits(); - assert(mreq.memoryTypeBits); - - auto bufferMem = m_device->allocate(mreq, outputBuffers[i].get(), IDeviceMemoryAllocation::EMAF_DEVICE_ADDRESS_BIT); - assert(bufferMem.isValid()); - } - pc.pInputBuf = gpuinputDataBuffer->getDeviceAddress(); - for (uint32_t i = 0; i < OutputBufferCount; i++) - pc.pOutputBuf[i] = outputBuffers[i]->getDeviceAddress(); - - // create Pipeline Layout - { - SPushConstantRange pcRange = { .stageFlags = IShader::E_SHADER_STAGE::ESS_COMPUTE, .offset = 0,.size = sizeof(PushConstantData) }; - pipelineLayout = m_device->createPipelineLayout({&pcRange, 1}); - } - - const auto spirv_isa_cache_path = localOutputCWD / "spirv_isa_cache.bin"; - // enclose to make sure file goes out of scope and we can reopen it - { - smart_refctd_ptr spirv_isa_cache_input; - // try to load SPIR-V to ISA cache - { - ISystem::future_t> fileCreate; - m_system->createFile(fileCreate, spirv_isa_cache_path, IFile::ECF_READ | IFile::ECF_MAPPABLE | IFile::ECF_COHERENT); - if (auto lock = fileCreate.acquire()) - spirv_isa_cache_input = *lock; - } - // create the cache - { - std::span spirv_isa_cache_data = {}; - if (spirv_isa_cache_input) - spirv_isa_cache_data = { reinterpret_cast(spirv_isa_cache_input->getMappedPointer()),spirv_isa_cache_input->getSize() }; - else - m_logger->log("Failed to load SPIR-V 2 ISA cache!", ILogger::ELL_PERFORMANCE); - // Normally we'd deserialize a `ICPUPipelineCache` properly and pass that instead - m_spirv_isa_cache = m_device->createPipelineCache(spirv_isa_cache_data); - } - } - { - // TODO: rename `deleteDirectory` to just `delete`? and a `IFile::setSize()` ? - m_system->deleteDirectory(spirv_isa_cache_path); - ISystem::future_t> fileCreate; - m_system->createFile(fileCreate, spirv_isa_cache_path, IFile::ECF_WRITE); - // I can be relatively sure I'll succeed to acquire the future, the pointer to created file might be null though. - m_spirv_isa_cache_output = *fileCreate.acquire(); - if (!m_spirv_isa_cache_output) - logFail("Failed to Create SPIR-V to ISA cache file."); - } - - // load shader source from file - auto getShaderSource = [&](const char* filePath) -> auto - { - IAssetLoader::SAssetLoadParams lparams = {}; - lparams.logger = m_logger.get(); - lparams.workingDirectory = ""; - auto bundle = m_assetMgr->getAsset(filePath, lparams); - if (bundle.getContents().empty() || bundle.getAssetType()!=IAsset::ET_SHADER) - { - m_logger->log("Shader %s not found!", ILogger::ELL_ERROR, filePath); - exit(-1); - } - auto firstAssetInBundle = bundle.getContents()[0]; - return smart_refctd_ptr_static_cast(firstAssetInBundle); - }; - - auto subgroupTestSource = getShaderSource("app_resources/testSubgroup.comp.hlsl"); - auto workgroupTestSource = getShaderSource("app_resources/testWorkgroup.comp.hlsl"); - // now create or retrieve final resources to run our tests - sema = m_device->createSemaphore(timelineValue); - resultsBuffer = ICPUBuffer::create({ outputBuffers[0]->getSize() }); - { - smart_refctd_ptr cmdpool = m_device->createCommandPool(computeQueue->getFamilyIndex(),IGPUCommandPool::CREATE_FLAGS::RESET_COMMAND_BUFFER_BIT); - if (!cmdpool->createCommandBuffers(IGPUCommandPool::BUFFER_LEVEL::PRIMARY,{&cmdbuf,1})) - { - logFail("Failed to create Command Buffers!\n"); - return false; - } - } - - const auto MaxWorkgroupSize = m_physicalDevice->getLimits().maxComputeWorkGroupInvocations; - const auto MinSubgroupSize = m_physicalDevice->getLimits().minSubgroupSize; - const auto MaxSubgroupSize = m_physicalDevice->getLimits().maxSubgroupSize; - for (uint32_t useNative = 0; useNative <= uint32_t(m_physicalDevice->getProperties().limits.shaderSubgroupArithmetic); useNative++) - { - if (useNative) - m_logger->log("Testing with native subgroup arithmetic", ILogger::ELL_INFO); - else - m_logger->log("Testing with emulated subgroup arithmetic", ILogger::ELL_INFO); - - for (auto subgroupSize = MinSubgroupSize; subgroupSize <= MaxSubgroupSize; subgroupSize *= 2u) - { - const uint8_t subgroupSizeLog2 = hlsl::findMSB(subgroupSize); - for (uint32_t workgroupSize = subgroupSize; workgroupSize <= MaxWorkgroupSize; workgroupSize *= 2u) - { - // make sure renderdoc captures everything for debugging - m_api->startCapture(); - m_logger->log("Testing Workgroup Size %u with Subgroup Size %u", ILogger::ELL_INFO, workgroupSize, subgroupSize); - - for (uint32_t j = 0; j < ItemsPerInvocations.size(); j++) - { - const uint32_t itemsPerInvocation = ItemsPerInvocations[j]; - uint32_t itemsPerWG = workgroupSize * itemsPerInvocation; - m_logger->log("Testing Items per Invocation %u", ILogger::ELL_INFO, itemsPerInvocation); - bool passed = true; - passed = runTest(subgroupTestSource, elementCount, subgroupSizeLog2, workgroupSize, bool(useNative), itemsPerWG, itemsPerInvocation) && passed; - logTestOutcome(passed, itemsPerWG); - passed = runTest(subgroupTestSource, elementCount, subgroupSizeLog2, workgroupSize, bool(useNative), itemsPerWG, itemsPerInvocation) && passed; - logTestOutcome(passed, itemsPerWG); - passed = runTest(subgroupTestSource, elementCount, subgroupSizeLog2, workgroupSize, bool(useNative), itemsPerWG, itemsPerInvocation) && passed; - logTestOutcome(passed, itemsPerWG); - - hlsl::workgroup2::SArithmeticConfiguration wgConfig; - wgConfig.init(hlsl::findMSB(workgroupSize), subgroupSizeLog2, itemsPerInvocation); - itemsPerWG = wgConfig.VirtualWorkgroupSize * wgConfig.ItemsPerInvocation_0; - m_logger->log("Testing Item Count %u", ILogger::ELL_INFO, itemsPerWG); - passed = runTest(workgroupTestSource, elementCount, subgroupSizeLog2, workgroupSize, bool(useNative), itemsPerWG, itemsPerInvocation) && passed; - logTestOutcome(passed, itemsPerWG); - passed = runTest(workgroupTestSource, elementCount, subgroupSizeLog2, workgroupSize, bool(useNative), itemsPerWG, itemsPerInvocation) && passed; - logTestOutcome(passed, itemsPerWG); - passed = runTest(workgroupTestSource, elementCount, subgroupSizeLog2, workgroupSize, bool(useNative), itemsPerWG, itemsPerInvocation) && passed; - logTestOutcome(passed, itemsPerWG); - } - m_api->endCapture(); - - // save cache every now and then - { - auto cpu = m_spirv_isa_cache->convertToCPUCache(); - // Normally we'd beautifully JSON serialize the thing, allow multiple devices & drivers + metadata - auto bin = cpu->getEntries().begin()->second.bin; - IFile::success_t success; - m_spirv_isa_cache_output->write(success, bin->data(), 0ull, bin->size()); - if (!success) - logFail("Could not write Create SPIR-V to ISA cache to disk!"); - } - } - } - } - - return true; - } - - virtual bool onAppTerminated() override - { - m_logger->log("==========Result==========", ILogger::ELL_INFO); - m_logger->log("Fail Count: %u", ILogger::ELL_INFO, totalFailCount); - delete[] inputData; - return true; - } - - // the unit test is carried out on init - void workLoopBody() override {} - - // - bool keepRunning() override { return false; } - -private: - void logTestOutcome(bool passed, uint32_t workgroupSize) - { - if (passed) - m_logger->log("Passed test #%u", ILogger::ELL_INFO, workgroupSize); - else - { - totalFailCount++; - m_logger->log("Failed test #%u", ILogger::ELL_ERROR, workgroupSize); - } - } - - // create pipeline (specialized every test) [TODO: turn into a future/async] - smart_refctd_ptr createPipeline(const IShader* overridenUnspecialized, const uint8_t subgroupSizeLog2) - { - auto shader = m_device->compileShader({ overridenUnspecialized }); - IGPUComputePipeline::SCreationParams params = {}; - params.layout = pipelineLayout.get(); - params.shader = { - .shader = shader.get(), - .entryPoint = "main", - .requiredSubgroupSize = static_cast(subgroupSizeLog2), - .entries = nullptr, - }; - params.cached.requireFullSubgroups = true; - core::smart_refctd_ptr pipeline; - if (!m_device->createComputePipelines(m_spirv_isa_cache.get(),{¶ms,1},&pipeline)) - return nullptr; - return pipeline; - } - - template class Arithmetic, bool WorkgroupTest> - bool runTest(const smart_refctd_ptr& source, const uint32_t elementCount, const uint8_t subgroupSizeLog2, const uint32_t workgroupSize, bool useNative, uint32_t itemsPerWG, uint32_t itemsPerInvoc = 1u) - { - std::string arith_name = Arithmetic>::name; - const uint32_t workgroupSizeLog2 = hlsl::findMSB(workgroupSize); - - auto compiler = make_smart_refctd_ptr(smart_refctd_ptr(m_system)); - CHLSLCompiler::SOptions options = {}; - options.stage = IShader::E_SHADER_STAGE::ESS_COMPUTE; - options.preprocessorOptions.targetSpirvVersion = m_device->getPhysicalDevice()->getLimits().spirvVersion; - options.spirvOptimizer = nullptr; -#ifndef _NBL_DEBUG - ISPIRVOptimizer::E_OPTIMIZER_PASS optPasses = ISPIRVOptimizer::EOP_STRIP_DEBUG_INFO; - auto opt = make_smart_refctd_ptr(std::span(&optPasses, 1)); - options.spirvOptimizer = opt.get(); -#else - options.debugInfoFlags |= IShaderCompiler::E_DEBUG_INFO_FLAGS::EDIF_LINE_BIT; -#endif - options.preprocessorOptions.sourceIdentifier = source->getFilepathHint(); - options.preprocessorOptions.logger = m_logger.get(); - - auto* includeFinder = compiler->getDefaultIncludeFinder(); - options.preprocessorOptions.includeFinder = includeFinder; - - smart_refctd_ptr overriddenUnspecialized; - if constexpr (WorkgroupTest) - { - hlsl::workgroup2::SArithmeticConfiguration wgConfig; - wgConfig.init(hlsl::findMSB(workgroupSize), subgroupSizeLog2, itemsPerInvoc); - - const std::string definitions[3] = { - "workgroup2::" + arith_name, - wgConfig.getConfigTemplateStructString(), - std::to_string(arith_name=="reduction") - }; - - const IShaderCompiler::SMacroDefinition defines[4] = { - { "OPERATION", definitions[0] }, - { "WORKGROUP_CONFIG_T", definitions[1] }, - { "IS_REDUCTION", definitions[2] }, - { "TEST_NATIVE", "1" } - }; - if (useNative) - options.preprocessorOptions.extraDefines = { defines, defines + 4 }; - else - options.preprocessorOptions.extraDefines = { defines, defines + 3 }; - - overriddenUnspecialized = compiler->compileToSPIRV((const char*)source->getContent()->getPointer(), options); - } - else - { - hlsl::subgroup2::SArithmeticParams sgParams; - sgParams.init(subgroupSizeLog2, itemsPerInvoc); - - const std::string definitions[3] = { - "subgroup2::" + arith_name, - std::to_string(workgroupSize), - sgParams.getParamTemplateStructString() - }; - - const IShaderCompiler::SMacroDefinition defines[4] = { - { "OPERATION", definitions[0] }, - { "WORKGROUP_SIZE", definitions[1] }, - { "SUBGROUP_CONFIG_T", definitions[2] }, - { "TEST_NATIVE", "1" } - }; - if (useNative) - options.preprocessorOptions.extraDefines = { defines, defines + 4 }; - else - options.preprocessorOptions.extraDefines = { defines, defines + 3 }; - - overriddenUnspecialized = compiler->compileToSPIRV((const char*)source->getContent()->getPointer(), options); - } - - auto pipeline = createPipeline(overriddenUnspecialized.get(),subgroupSizeLog2); - - // TODO: overlap dispatches with memory readbacks (requires multiple copies of `buffers`) - uint32_t workgroupCount = 1;// min(elementCount / itemsPerWG, m_physicalDevice->getLimits().maxComputeWorkGroupCount[0]); - - cmdbuf->begin(IGPUCommandBuffer::USAGE::NONE); - cmdbuf->bindComputePipeline(pipeline.get()); - cmdbuf->pushConstants(pipelineLayout.get(), IShader::E_SHADER_STAGE::ESS_COMPUTE, 0, sizeof(PushConstantData), &pc); - cmdbuf->dispatch(workgroupCount, 1, 1); - { - IGPUCommandBuffer::SPipelineBarrierDependencyInfo::buffer_barrier_t memoryBarrier[OutputBufferCount]; - for (auto i=0u; igetSize(),outputBuffers[i]} - }; - } - IGPUCommandBuffer::SPipelineBarrierDependencyInfo info = {.memBarriers={},.bufBarriers=memoryBarrier}; - cmdbuf->pipelineBarrier(asset::E_DEPENDENCY_FLAGS::EDF_NONE,info); - } - cmdbuf->end(); - - const IQueue::SSubmitInfo::SSemaphoreInfo signal[1] = {{.semaphore=sema.get(),.value=++timelineValue}}; - const IQueue::SSubmitInfo::SCommandBufferInfo cmdbufs[1] = {{.cmdbuf=cmdbuf.get()}}; - const IQueue::SSubmitInfo submits[1] = {{.commandBuffers=cmdbufs,.signalSemaphores=signal}}; - computeQueue->submit(submits); - const ISemaphore::SWaitInfo wait[1] = {{.semaphore=sema.get(),.value=timelineValue}}; - m_device->blockForSemaphores(wait); - - const uint32_t subgroupSize = 1u << subgroupSizeLog2; - // check results - bool passed = validateResults, WorkgroupTest>(itemsPerWG, workgroupCount, subgroupSize, itemsPerInvoc); - passed = validateResults, WorkgroupTest>(itemsPerWG, workgroupCount, subgroupSize, itemsPerInvoc) && passed; - passed = validateResults, WorkgroupTest>(itemsPerWG, workgroupCount, subgroupSize, itemsPerInvoc) && passed; - passed = validateResults, WorkgroupTest>(itemsPerWG, workgroupCount, subgroupSize, itemsPerInvoc) && passed; - passed = validateResults, WorkgroupTest>(itemsPerWG, workgroupCount, subgroupSize, itemsPerInvoc) && passed; - passed = validateResults, WorkgroupTest>(itemsPerWG, workgroupCount, subgroupSize, itemsPerInvoc) && passed; - passed = validateResults, WorkgroupTest>(itemsPerWG, workgroupCount, subgroupSize, itemsPerInvoc) && passed; - - return passed; - } - - //returns true if result matches - template class Arithmetic, class Binop, bool WorkgroupTest> - bool validateResults(const uint32_t itemsPerWG, const uint32_t workgroupCount, const uint32_t subgroupSize, const uint32_t itemsPerInvoc) - { - bool success = true; - - // download data - const SBufferRange bufferRange = {0u, resultsBuffer->getSize(), outputBuffers[Binop::BindingIndex]}; - m_utils->downloadBufferRangeViaStagingBufferAutoSubmit(SIntendedSubmitInfo{.queue=transferDownQueue},bufferRange,resultsBuffer->getPointer()); - - using type_t = typename Binop::type_t; - const auto testData = reinterpret_cast(resultsBuffer->getPointer()); - - // TODO: parallel for (the temporary values need to be threadlocal or what?) - // now check if the data obtained has valid values - type_t* tmp = new type_t[itemsPerWG]; - for (uint32_t workgroupID = 0u; success && workgroupID < workgroupCount; workgroupID++) - { - if constexpr (WorkgroupTest) - { - const auto workgroupOffset = workgroupID * itemsPerWG; - Arithmetic::impl(tmp, inputData + workgroupOffset, itemsPerWG); - - for (uint32_t localInvocationIndex = 0u; localInvocationIndex < itemsPerWG; localInvocationIndex++) - { - const auto globalInvocationIndex = workgroupOffset + localInvocationIndex; - const auto cpuVal = tmp[localInvocationIndex]; - const auto gpuVal = testData[globalInvocationIndex]; - if (cpuVal != gpuVal) - { - m_logger->log( - "Failed test #%d (%s) (%s) Expected %u got %u for workgroup %d and localinvoc %d", - ILogger::ELL_ERROR, itemsPerWG, WorkgroupTest ? "workgroup" : "subgroup", Binop::name, - cpuVal, gpuVal, workgroupID, localInvocationIndex - ); - success = false; - break; - } - } - } - else - { - const auto workgroupOffset = workgroupID * itemsPerWG; - const auto workgroupSize = itemsPerWG / itemsPerInvoc; - for (uint32_t pseudoSubgroupID = 0u; pseudoSubgroupID < workgroupSize; pseudoSubgroupID += subgroupSize) - Arithmetic::impl(tmp + pseudoSubgroupID * itemsPerInvoc, inputData + workgroupOffset + pseudoSubgroupID * itemsPerInvoc, subgroupSize * itemsPerInvoc); - - for (uint32_t localInvocationIndex = 0u; localInvocationIndex < workgroupSize; localInvocationIndex++) - { - const auto localOffset = localInvocationIndex * itemsPerInvoc; - const auto globalInvocationIndex = workgroupOffset + localOffset; - - for (uint32_t itemInvocationIndex = 0u; itemInvocationIndex < itemsPerInvoc; itemInvocationIndex++) - { - const auto cpuVal = tmp[localOffset + itemInvocationIndex]; - const auto gpuVal = testData[globalInvocationIndex + itemInvocationIndex]; - if (cpuVal != gpuVal) - { - m_logger->log( - "Failed test #%d (%s) (%s) Expected %u got %u for workgroup %d and localinvoc %d and iteminvoc %d", - ILogger::ELL_ERROR, itemsPerWG, WorkgroupTest ? "workgroup" : "subgroup", Binop::name, - cpuVal, gpuVal, workgroupID, localInvocationIndex, itemInvocationIndex - ); - success = false; - break; - } - } - } - } - } - delete[] tmp; - - return success; - } - - IQueue* transferDownQueue; - IQueue* computeQueue; - smart_refctd_ptr m_spirv_isa_cache; - smart_refctd_ptr m_spirv_isa_cache_output; - - uint32_t* inputData = nullptr; - constexpr static inline uint32_t OutputBufferCount = 8u; - smart_refctd_ptr outputBuffers[OutputBufferCount]; - smart_refctd_ptr pipelineLayout; - PushConstantData pc; - - smart_refctd_ptr sema; - uint64_t timelineValue = 0; - smart_refctd_ptr cmdbuf; - smart_refctd_ptr resultsBuffer; - - uint32_t totalFailCount = 0; - - constexpr static inline std::array ItemsPerInvocations = { 1, 2, 3, 4 }; -}; - -NBL_MAIN_FUNC(Workgroup2ScanTestApp) \ No newline at end of file diff --git a/28_FFTBloom/CMakeLists.txt b/23_ArithmeticUnitTest/CMakeLists.txt similarity index 99% rename from 28_FFTBloom/CMakeLists.txt rename to 23_ArithmeticUnitTest/CMakeLists.txt index a434ff32a..0724366c9 100644 --- a/28_FFTBloom/CMakeLists.txt +++ b/23_ArithmeticUnitTest/CMakeLists.txt @@ -1,3 +1,4 @@ + include(common RESULT_VARIABLE RES) if(NOT RES) message(FATAL_ERROR "common.cmake not found. Should be in {repo_root}/cmake directory") diff --git a/23_Arithmetic2UnitTest/app_resources/common.hlsl b/23_ArithmeticUnitTest/app_resources/common.hlsl similarity index 89% rename from 23_Arithmetic2UnitTest/app_resources/common.hlsl rename to 23_ArithmeticUnitTest/app_resources/common.hlsl index 6654645cf..10892a2b9 100644 --- a/23_Arithmetic2UnitTest/app_resources/common.hlsl +++ b/23_ArithmeticUnitTest/app_resources/common.hlsl @@ -1,14 +1,15 @@ #include "nbl/builtin/hlsl/cpp_compat.hlsl" #include "nbl/builtin/hlsl/functional.hlsl" -struct PushConstantData +template +struct Output { - uint64_t pInputBuf; - uint64_t pOutputBuf[8]; + NBL_CONSTEXPR_STATIC_INLINE uint32_t ScanElementCount = kScanElementCount; + + uint32_t subgroupSize; + uint32_t data[ScanElementCount]; }; -namespace arithmetic -{ // Thanks to our unified HLSL/C++ STD lib we're able to remove a whole load of code template struct bit_and : nbl::hlsl::bit_and @@ -91,6 +92,5 @@ struct ballot : nbl::hlsl::plus static inline constexpr const char* name = "bitcount"; #endif }; -} -#include "nbl/builtin/hlsl/glsl_compat/subgroup_basic.hlsl" +#include "nbl/builtin/hlsl/subgroup/basic.hlsl" \ No newline at end of file diff --git a/23_ArithmeticUnitTest/app_resources/shaderCommon.hlsl b/23_ArithmeticUnitTest/app_resources/shaderCommon.hlsl new file mode 100644 index 000000000..13ee8d21e --- /dev/null +++ b/23_ArithmeticUnitTest/app_resources/shaderCommon.hlsl @@ -0,0 +1,55 @@ +#include "common.hlsl" + +#include "nbl/builtin/hlsl/glsl_compat/core.hlsl" +#include "nbl/builtin/hlsl/subgroup/basic.hlsl" +#include "nbl/builtin/hlsl/subgroup/arithmetic_portability.hlsl" + +#include "nbl/builtin/hlsl/jit/device_capabilities.hlsl" + +// https://github.com/microsoft/DirectXShaderCompiler/issues/6144 +uint32_t3 nbl::hlsl::glsl::gl_WorkGroupSize() {return uint32_t3(WORKGROUP_SIZE,1,1);} + +// unfortunately DXC chokes on descriptors as static members +// https://github.com/microsoft/DirectXShaderCompiler/issues/5940 +[[vk::binding(0, 0)]] StructuredBuffer inputValue; +[[vk::binding(1, 0)]] RWByteAddressBuffer output[8]; + +// because subgroups don't match `gl_LocalInvocationIndex` snake curve addressing, we also can't load inputs that way +uint32_t globalIndex(); +// since we test ITEMS_PER_WG class binop> +static void subtest(NBL_CONST_REF_ARG(type_t) sourceVal) +{ + if (globalIndex()==0u) + output[binop::BindingIndex].template Store(0,nbl::hlsl::glsl::gl_SubgroupSize()); + + operation_t::base_t,nbl::hlsl::jit::device_capabilities> func; + if (canStore()) + output[binop::BindingIndex].template Store(sizeof(uint32_t)+sizeof(type_t)*globalIndex(),func(sourceVal)); +} + + +type_t test() +{ + const type_t sourceVal = inputValue[globalIndex()]; + + subtest(sourceVal); + subtest(sourceVal); + subtest(sourceVal); + subtest(sourceVal); + subtest(sourceVal); + subtest(sourceVal); + subtest(sourceVal); + return sourceVal; +} + +#include "nbl/builtin/hlsl/workgroup/basic.hlsl" \ No newline at end of file diff --git a/23_ArithmeticUnitTest/app_resources/testSubgroup.comp.hlsl b/23_ArithmeticUnitTest/app_resources/testSubgroup.comp.hlsl new file mode 100644 index 000000000..479265d73 --- /dev/null +++ b/23_ArithmeticUnitTest/app_resources/testSubgroup.comp.hlsl @@ -0,0 +1,18 @@ +#pragma shader_stage(compute) + +#define operation_t nbl::hlsl::OPERATION + +#include "shaderCommon.hlsl" + +uint32_t globalIndex() +{ + return nbl::hlsl::glsl::gl_WorkGroupID().x*WORKGROUP_SIZE+nbl::hlsl::workgroup::SubgroupContiguousIndex(); +} + +bool canStore() {return true;} + +[numthreads(WORKGROUP_SIZE,1,1)] +void main() +{ + test(); +} \ No newline at end of file diff --git a/23_ArithmeticUnitTest/app_resources/testWorkgroup.comp.hlsl b/23_ArithmeticUnitTest/app_resources/testWorkgroup.comp.hlsl new file mode 100644 index 000000000..9bafae47f --- /dev/null +++ b/23_ArithmeticUnitTest/app_resources/testWorkgroup.comp.hlsl @@ -0,0 +1,107 @@ +#pragma shader_stage(compute) + + +#include "nbl/builtin/hlsl/workgroup/scratch_size.hlsl" + +static const uint32_t ArithmeticSz = nbl::hlsl::workgroup::scratch_size_arithmetic::value; +static const uint32_t BallotSz = nbl::hlsl::workgroup::scratch_size_ballot::value; +static const uint32_t ScratchSz = ArithmeticSz+BallotSz; + +// TODO: Can we make it a static variable in the ScratchProxy struct? +groupshared uint32_t scratch[ScratchSz]; + + +#include "nbl/builtin/hlsl/workgroup/arithmetic.hlsl" + + +template +struct ScratchProxy +{ + void get(const uint32_t ix, NBL_REF_ARG(uint32_t) value) + { + value = scratch[ix+offset]; + } + void set(const uint32_t ix, const uint32_t value) + { + scratch[ix+offset] = value; + } + + uint32_t atomicOr(const uint32_t ix, const uint32_t value) + { + return nbl::hlsl::glsl::atomicOr(scratch[ix],value); + } + + void workgroupExecutionAndMemoryBarrier() + { + nbl::hlsl::glsl::barrier(); + //nbl::hlsl::glsl::memoryBarrierShared(); implied by the above + } +}; + +static ScratchProxy<0> arithmeticAccessor; + + +#include "nbl/builtin/hlsl/workgroup/broadcast.hlsl" + + +template +struct operation_t +{ + using type_t = typename Binop::type_t; + + type_t operator()(type_t value) + { + type_t retval = nbl::hlsl::OPERATION::template __call >(value,arithmeticAccessor); + // we barrier before because we alias the accessors for Binop + arithmeticAccessor.workgroupExecutionAndMemoryBarrier(); + return retval; + } +}; + + +#include "shaderCommon.hlsl" + +static ScratchProxy ballotAccessor; + + +uint32_t globalIndex() +{ + return nbl::hlsl::glsl::gl_WorkGroupID().x*ITEMS_PER_WG+nbl::hlsl::workgroup::SubgroupContiguousIndex(); +} + +bool canStore() +{ + return nbl::hlsl::workgroup::SubgroupContiguousIndex()::BindingIndex].template Store(0,nbl::hlsl::glsl::gl_SubgroupSize()); + + // we can only ballot booleans, so low bit + nbl::hlsl::workgroup::ballot >(bool(sourceVal & 0x1u), ballotAccessor); + // need to barrier between ballot and usages of a ballot by myself + ballotAccessor.workgroupExecutionAndMemoryBarrier(); + + uint32_t destVal = 0xdeadbeefu; +#define CONSTEXPR_OP_TYPE_TEST(IS_OP) nbl::hlsl::is_same,0x45>,nbl::hlsl::workgroup::IS_OP,0x45> >::value +#define BALLOT_TEMPLATE_ARGS ITEMS_PER_WG,decltype(ballotAccessor),decltype(arithmeticAccessor),nbl::hlsl::jit::device_capabilities + if (CONSTEXPR_OP_TYPE_TEST(reduction)) + destVal = nbl::hlsl::workgroup::ballotBitCount(ballotAccessor,arithmeticAccessor); + else if (CONSTEXPR_OP_TYPE_TEST(inclusive_scan)) + destVal = nbl::hlsl::workgroup::ballotInclusiveBitCount(ballotAccessor,arithmeticAccessor); + else if (CONSTEXPR_OP_TYPE_TEST(exclusive_scan)) + destVal = nbl::hlsl::workgroup::ballotExclusiveBitCount(ballotAccessor,arithmeticAccessor); + else + { + assert(false); + } +#undef BALLOT_TEMPLATE_ARGS +#undef CONSTEXPR_OP_TYPE_TEST + + if (canStore()) + output[ballot::BindingIndex].template Store(sizeof(uint32_t)+sizeof(type_t)*globalIndex(),destVal); +} \ No newline at end of file diff --git a/29_Arithmetic2Bench/config.json.template b/23_ArithmeticUnitTest/config.json.template similarity index 100% rename from 29_Arithmetic2Bench/config.json.template rename to 23_ArithmeticUnitTest/config.json.template diff --git a/23_ArithmeticUnitTest/main.cpp b/23_ArithmeticUnitTest/main.cpp new file mode 100644 index 000000000..f4b682183 --- /dev/null +++ b/23_ArithmeticUnitTest/main.cpp @@ -0,0 +1,462 @@ +#include "nbl/application_templates/BasicMultiQueueApplication.hpp" +#include "nbl/application_templates/MonoAssetManagerAndBuiltinResourceApplication.hpp" +#include "app_resources/common.hlsl" + +using namespace nbl; +using namespace core; +using namespace asset; +using namespace system; +using namespace video; + +// method emulations on the CPU, to verify the results of the GPU methods +template +struct emulatedReduction +{ + using type_t = typename Binop::type_t; + + static inline void impl(type_t* out, const type_t* in, const uint32_t itemCount) + { + const type_t red = std::reduce(in,in+itemCount,Binop::identity,Binop()); + std::fill(out,out+itemCount,red); + } + + static inline constexpr const char* name = "reduction"; +}; +template +struct emulatedScanInclusive +{ + using type_t = typename Binop::type_t; + + static inline void impl(type_t* out, const type_t* in, const uint32_t itemCount) + { + std::inclusive_scan(in,in+itemCount,out,Binop()); + } + static inline constexpr const char* name = "inclusive_scan"; +}; +template +struct emulatedScanExclusive +{ + using type_t = typename Binop::type_t; + + static inline void impl(type_t* out, const type_t* in, const uint32_t itemCount) + { + std::exclusive_scan(in,in+itemCount,out,Binop::identity,Binop()); + } + static inline constexpr const char* name = "exclusive_scan"; +}; + +class ArithmeticUnitTestApp final : public application_templates::BasicMultiQueueApplication, public application_templates::MonoAssetManagerAndBuiltinResourceApplication +{ + using device_base_t = application_templates::BasicMultiQueueApplication; + using asset_base_t = application_templates::MonoAssetManagerAndBuiltinResourceApplication; + +public: + ArithmeticUnitTestApp(const path& _localInputCWD, const path& _localOutputCWD, const path& _sharedInputCWD, const path& _sharedOutputCWD) : + system::IApplicationFramework(_localInputCWD, _localOutputCWD, _sharedInputCWD, _sharedOutputCWD) {} + + bool onAppInitialized(smart_refctd_ptr&& system) override + { + if (!device_base_t::onAppInitialized(std::move(system))) + return false; + if (!asset_base_t::onAppInitialized(std::move(system))) + return false; + + transferDownQueue = getTransferDownQueue(); + computeQueue = getComputeQueue(); + + // TODO: get the element count from argv + const uint32_t elementCount = Output<>::ScanElementCount; + // populate our random data buffer on the CPU and create a GPU copy + inputData = new uint32_t[elementCount]; + smart_refctd_ptr gpuinputDataBuffer; + { + std::mt19937 randGenerator(0xdeadbeefu); + for (uint32_t i = 0u; i < elementCount; i++) + inputData[i] = randGenerator(); // TODO: change to using xoroshiro, then we can skip having the input buffer at all + + IGPUBuffer::SCreationParams inputDataBufferCreationParams = {}; + inputDataBufferCreationParams.size = sizeof(Output<>::data[0]) * elementCount; + inputDataBufferCreationParams.usage = IGPUBuffer::EUF_STORAGE_BUFFER_BIT | IGPUBuffer::EUF_TRANSFER_DST_BIT; + m_utils->createFilledDeviceLocalBufferOnDedMem( + SIntendedSubmitInfo{.queue=getTransferUpQueue()}, + std::move(inputDataBufferCreationParams), + inputData + ).move_into(gpuinputDataBuffer); + } + + // create 8 buffers for 8 operations + for (auto i=0u; igetSize(); + params.usage = bitflag(IGPUBuffer::EUF_STORAGE_BUFFER_BIT) | IGPUBuffer::EUF_TRANSFER_SRC_BIT; + + outputBuffers[i] = m_device->createBuffer(std::move(params)); + auto mreq = outputBuffers[i]->getMemoryReqs(); + mreq.memoryTypeBits &= m_physicalDevice->getDeviceLocalMemoryTypeBits(); + assert(mreq.memoryTypeBits); + + auto bufferMem = m_device->allocate(mreq, outputBuffers[i].get()); + assert(bufferMem.isValid()); + } + + // create Descriptor Set and Pipeline Layout + { + // create Descriptor Set Layout + smart_refctd_ptr dsLayout; + { + IGPUDescriptorSetLayout::SBinding binding[2]; + for (uint32_t i = 0u; i < 2; i++) + binding[i] = {{},i,IDescriptor::E_TYPE::ET_STORAGE_BUFFER,IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE,IShader::E_SHADER_STAGE::ESS_COMPUTE,1u,nullptr }; + binding[1].count = OutputBufferCount; + dsLayout = m_device->createDescriptorSetLayout(binding); + } + + // set and transient pool + auto descPool = m_device->createDescriptorPoolForDSLayouts(IDescriptorPool::ECF_NONE,{&dsLayout.get(),1}); + descriptorSet = descPool->createDescriptorSet(smart_refctd_ptr(dsLayout)); + { + IGPUDescriptorSet::SDescriptorInfo infos[1+OutputBufferCount]; + infos[0].desc = gpuinputDataBuffer; + infos[0].info.buffer = { 0u,gpuinputDataBuffer->getSize() }; + for (uint32_t i = 1u; i <= OutputBufferCount; i++) + { + auto buff = outputBuffers[i - 1]; + infos[i].info.buffer = { 0u,buff->getSize() }; + infos[i].desc = std::move(buff); // save an atomic in the refcount + + } + + IGPUDescriptorSet::SWriteDescriptorSet writes[2]; + for (uint32_t i=0u; i<2; i++) + writes[i] = {descriptorSet.get(),i,0u,1u,infos+i}; + writes[1].count = OutputBufferCount; + + m_device->updateDescriptorSets(2, writes, 0u, nullptr); + } + + pipelineLayout = m_device->createPipelineLayout({},std::move(dsLayout)); + } + + const auto spirv_isa_cache_path = localOutputCWD/"spirv_isa_cache.bin"; + // enclose to make sure file goes out of scope and we can reopen it + { + smart_refctd_ptr spirv_isa_cache_input; + // try to load SPIR-V to ISA cache + { + ISystem::future_t> fileCreate; + m_system->createFile(fileCreate,spirv_isa_cache_path,IFile::ECF_READ|IFile::ECF_MAPPABLE|IFile::ECF_COHERENT); + if (auto lock=fileCreate.acquire()) + spirv_isa_cache_input = *lock; + } + // create the cache + { + std::span spirv_isa_cache_data = {}; + if (spirv_isa_cache_input) + spirv_isa_cache_data = {reinterpret_cast(spirv_isa_cache_input->getMappedPointer()),spirv_isa_cache_input->getSize()}; + else + m_logger->log("Failed to load SPIR-V 2 ISA cache!",ILogger::ELL_PERFORMANCE); + // Normally we'd deserialize a `ICPUPipelineCache` properly and pass that instead + m_spirv_isa_cache = m_device->createPipelineCache(spirv_isa_cache_data); + } + } + { + // TODO: rename `deleteDirectory` to just `delete`? and a `IFile::setSize()` ? + m_system->deleteDirectory(spirv_isa_cache_path); + ISystem::future_t> fileCreate; + m_system->createFile(fileCreate,spirv_isa_cache_path,IFile::ECF_WRITE); + // I can be relatively sure I'll succeed to acquire the future, the pointer to created file might be null though. + m_spirv_isa_cache_output=*fileCreate.acquire(); + if (!m_spirv_isa_cache_output) + logFail("Failed to Create SPIR-V to ISA cache file."); + } + + // load shader source from file + auto getShaderSource = [&](const char* filePath) -> auto + { + IAssetLoader::SAssetLoadParams lparams = {}; + lparams.logger = m_logger.get(); + lparams.workingDirectory = ""; + auto bundle = m_assetMgr->getAsset(filePath, lparams); + if (bundle.getContents().empty() || bundle.getAssetType()!=IAsset::ET_SHADER) + { + m_logger->log("Shader %s not found!", ILogger::ELL_ERROR, filePath); + exit(-1); + } + auto firstAssetInBundle = bundle.getContents()[0]; + return smart_refctd_ptr_static_cast(firstAssetInBundle); + }; + + auto subgroupTestSource = getShaderSource("app_resources/testSubgroup.comp.hlsl"); + auto workgroupTestSource = getShaderSource("app_resources/testWorkgroup.comp.hlsl"); + // now create or retrieve final resources to run our tests + sema = m_device->createSemaphore(timelineValue); + resultsBuffer = make_smart_refctd_ptr(outputBuffers[0]->getSize()); + { + smart_refctd_ptr cmdpool = m_device->createCommandPool(computeQueue->getFamilyIndex(),IGPUCommandPool::CREATE_FLAGS::RESET_COMMAND_BUFFER_BIT); + if (!cmdpool->createCommandBuffers(IGPUCommandPool::BUFFER_LEVEL::PRIMARY,{&cmdbuf,1})) + { + logFail("Failed to create Command Buffers!\n"); + return false; + } + } + + const auto MaxWorkgroupSize = m_physicalDevice->getLimits().maxComputeWorkGroupInvocations; + const auto MinSubgroupSize = m_physicalDevice->getLimits().minSubgroupSize; + const auto MaxSubgroupSize = m_physicalDevice->getLimits().maxSubgroupSize; + for (auto subgroupSize=MinSubgroupSize; subgroupSize <= MaxSubgroupSize; subgroupSize *= 2u) + { + const uint8_t subgroupSizeLog2 = hlsl::findMSB(subgroupSize); + for (uint32_t workgroupSize = subgroupSize; workgroupSize <= MaxWorkgroupSize; workgroupSize += subgroupSize) + { + // make sure renderdoc captures everything for debugging + m_api->startCapture(); + m_logger->log("Testing Workgroup Size %u with Subgroup Size %u", ILogger::ELL_INFO, workgroupSize, subgroupSize); + + bool passed = true; + // TODO async the testing + passed = runTest(subgroupTestSource, elementCount, subgroupSizeLog2, workgroupSize) && passed; + logTestOutcome(passed, workgroupSize); + passed = runTest(subgroupTestSource, elementCount, subgroupSizeLog2, workgroupSize) && passed; + logTestOutcome(passed, workgroupSize); + passed = runTest(subgroupTestSource, elementCount, subgroupSizeLog2, workgroupSize) && passed; + logTestOutcome(passed, workgroupSize); + for (uint32_t itemsPerWG = workgroupSize; itemsPerWG > workgroupSize - subgroupSize; itemsPerWG--) + { + m_logger->log("Testing Item Count %u", ILogger::ELL_INFO, itemsPerWG); + passed = runTest(workgroupTestSource, elementCount, subgroupSizeLog2, workgroupSize, itemsPerWG) && passed; + logTestOutcome(passed, itemsPerWG); + passed = runTest(workgroupTestSource, elementCount, subgroupSizeLog2, workgroupSize, itemsPerWG) && passed; + logTestOutcome(passed, itemsPerWG); + passed = runTest(workgroupTestSource, elementCount, subgroupSizeLog2, workgroupSize, itemsPerWG) && passed; + logTestOutcome(passed, itemsPerWG); + } + m_api->endCapture(); + + // save cache every now and then + { + auto cpu = m_spirv_isa_cache->convertToCPUCache(); + // Normally we'd beautifully JSON serialize the thing, allow multiple devices & drivers + metadata + auto bin = cpu->getEntries().begin()->second.bin; + IFile::success_t success; + m_spirv_isa_cache_output->write(success,bin->data(),0ull,bin->size()); + if (!success) + logFail("Could not write Create SPIR-V to ISA cache to disk!"); + } + } + } + + return true; + } + + virtual bool onAppTerminated() override + { + m_logger->log("==========Result==========", ILogger::ELL_INFO); + m_logger->log("Fail Count: %u", ILogger::ELL_INFO, totalFailCount); + delete[] inputData; + return true; + } + + // the unit test is carried out on init + void workLoopBody() override {} + + // + bool keepRunning() override { return false; } + +private: + void logTestOutcome(bool passed, uint32_t workgroupSize) + { + if (passed) + m_logger->log("Passed test #%u", ILogger::ELL_INFO, workgroupSize); + else + { + totalFailCount++; + m_logger->log("Failed test #%u", ILogger::ELL_ERROR, workgroupSize); + } + } + + // create pipeline (specialized every test) [TODO: turn into a future/async] + smart_refctd_ptr createPipeline(const ICPUShader* overridenUnspecialized, const uint8_t subgroupSizeLog2) + { + auto shader = m_device->createShader(overridenUnspecialized); + IGPUComputePipeline::SCreationParams params = {}; + params.layout = pipelineLayout.get(); + params.shader = { + .entryPoint = "main", + .shader = shader.get(), + .entries = nullptr, + .requiredSubgroupSize = static_cast(subgroupSizeLog2), + .requireFullSubgroups = true + }; + core::smart_refctd_ptr pipeline; + if (!m_device->createComputePipelines(m_spirv_isa_cache.get(),{¶ms,1},&pipeline)) + return nullptr; + return pipeline; + } + + /*template class Arithmetic, bool WorkgroupTest> + bool runTest(const smart_refctd_ptr& source, const uint32_t elementCount, const uint32_t workgroupSize, uint32_t itemsPerWG = ~0u) + { + return true; + }*/ + + template class Arithmetic, bool WorkgroupTest> + bool runTest(const smart_refctd_ptr& source, const uint32_t elementCount, const uint8_t subgroupSizeLog2, const uint32_t workgroupSize, uint32_t itemsPerWG = ~0u) + { + std::string arith_name = Arithmetic>::name; + + smart_refctd_ptr overridenUnspecialized; + if constexpr (WorkgroupTest) + { + overridenUnspecialized = CHLSLCompiler::createOverridenCopy( + source.get(), "#define OPERATION %s\n#define WORKGROUP_SIZE %d\n#define ITEMS_PER_WG %d\n", + (("workgroup::") + arith_name).c_str(), workgroupSize, itemsPerWG + ); + } + else + { + itemsPerWG = workgroupSize; + overridenUnspecialized = CHLSLCompiler::createOverridenCopy( + source.get(), "#define OPERATION %s\n#define WORKGROUP_SIZE %d\n", + (("subgroup::") + arith_name).c_str(), workgroupSize + ); + } + auto pipeline = createPipeline(overridenUnspecialized.get(),subgroupSizeLog2); + + // TODO: overlap dispatches with memory readbacks (requires multiple copies of `buffers`) + const uint32_t workgroupCount = elementCount / itemsPerWG; + cmdbuf->begin(IGPUCommandBuffer::USAGE::NONE); + cmdbuf->bindComputePipeline(pipeline.get()); + cmdbuf->bindDescriptorSets(EPBP_COMPUTE, pipeline->getLayout(), 0u, 1u, &descriptorSet.get()); + cmdbuf->dispatch(workgroupCount, 1, 1); + { + IGPUCommandBuffer::SPipelineBarrierDependencyInfo::buffer_barrier_t memoryBarrier[OutputBufferCount]; + for (auto i=0u; igetSize(),outputBuffers[i]} + }; + } + IGPUCommandBuffer::SPipelineBarrierDependencyInfo info = {.memBarriers={},.bufBarriers=memoryBarrier}; + cmdbuf->pipelineBarrier(asset::E_DEPENDENCY_FLAGS::EDF_NONE,info); + } + cmdbuf->end(); + + const IQueue::SSubmitInfo::SSemaphoreInfo signal[1] = {{.semaphore=sema.get(),.value=++timelineValue}}; + const IQueue::SSubmitInfo::SCommandBufferInfo cmdbufs[1] = {{.cmdbuf=cmdbuf.get()}}; + const IQueue::SSubmitInfo submits[1] = {{.commandBuffers=cmdbufs,.signalSemaphores=signal}}; + computeQueue->submit(submits); + const ISemaphore::SWaitInfo wait[1] = {{.semaphore=sema.get(),.value=timelineValue}}; + m_device->blockForSemaphores(wait); + + // check results + bool passed = validateResults, WorkgroupTest>(itemsPerWG, workgroupCount); + passed = validateResults, WorkgroupTest>(itemsPerWG, workgroupCount) && passed; + passed = validateResults, WorkgroupTest>(itemsPerWG, workgroupCount) && passed; + passed = validateResults, WorkgroupTest>(itemsPerWG, workgroupCount) && passed; + passed = validateResults, WorkgroupTest>(itemsPerWG, workgroupCount) && passed; + passed = validateResults, WorkgroupTest>(itemsPerWG, workgroupCount) && passed; + passed = validateResults, WorkgroupTest>(itemsPerWG, workgroupCount) && passed; + if constexpr (WorkgroupTest) + passed = validateResults, WorkgroupTest>(itemsPerWG, workgroupCount) && passed; + + return passed; + } + + //returns true if result matches + template class Arithmetic, class Binop, bool WorkgroupTest> + bool validateResults(const uint32_t itemsPerWG, const uint32_t workgroupCount) + { + bool success = true; + + // download data + const SBufferRange bufferRange = {0u, resultsBuffer->getSize(), outputBuffers[Binop::BindingIndex]}; + m_utils->downloadBufferRangeViaStagingBufferAutoSubmit(SIntendedSubmitInfo{.queue=transferDownQueue},bufferRange,resultsBuffer->getPointer()); + + using type_t = typename Binop::type_t; + const auto dataFromBuffer = reinterpret_cast(resultsBuffer->getPointer()); + const auto subgroupSize = dataFromBuffer[0]; + if (subgroupSizenbl::hlsl::subgroup::MaxSubgroupSize) + { + m_logger->log("Unexpected Subgroup Size %u", ILogger::ELL_ERROR, subgroupSize); + return false; + } + + const auto testData = reinterpret_cast(dataFromBuffer + 1); + // TODO: parallel for (the temporary values need to be threadlocal or what?) + // now check if the data obtained has valid values + type_t* tmp = new type_t[itemsPerWG]; + type_t* ballotInput = new type_t[itemsPerWG]; + for (uint32_t workgroupID = 0u; success && workgroupID < workgroupCount; workgroupID++) + { + const auto workgroupOffset = workgroupID * itemsPerWG; + + if constexpr (WorkgroupTest) + { + if constexpr (std::is_same_v, Binop>) + { + for (auto i = 0u; i < itemsPerWG; i++) + ballotInput[i] = inputData[i + workgroupOffset] & 0x1u; + Arithmetic::impl(tmp, ballotInput, itemsPerWG); + } + else + Arithmetic::impl(tmp, inputData + workgroupOffset, itemsPerWG); + } + else + { + for (uint32_t pseudoSubgroupID = 0u; pseudoSubgroupID < itemsPerWG; pseudoSubgroupID += subgroupSize) + Arithmetic::impl(tmp + pseudoSubgroupID, inputData + workgroupOffset + pseudoSubgroupID, subgroupSize); + } + + for (uint32_t localInvocationIndex = 0u; localInvocationIndex < itemsPerWG; localInvocationIndex++) + { + const auto globalInvocationIndex = workgroupOffset + localInvocationIndex; + const auto cpuVal = tmp[localInvocationIndex]; + const auto gpuVal = testData[globalInvocationIndex]; + if (cpuVal != gpuVal) + { + m_logger->log( + "Failed test #%d (%s) (%s) Expected %u got %u for workgroup %d and localinvoc %d", + ILogger::ELL_ERROR, itemsPerWG, WorkgroupTest ? "workgroup" : "subgroup", Binop::name, + cpuVal, gpuVal, workgroupID, localInvocationIndex + ); + success = false; + break; + } + } + } + delete[] ballotInput; + delete[] tmp; + + return success; + } + + IQueue* transferDownQueue; + IQueue* computeQueue; + smart_refctd_ptr m_spirv_isa_cache; + smart_refctd_ptr m_spirv_isa_cache_output; + + uint32_t* inputData = nullptr; + constexpr static inline uint32_t OutputBufferCount = 8u; + smart_refctd_ptr outputBuffers[OutputBufferCount]; + smart_refctd_ptr descriptorSet; + smart_refctd_ptr pipelineLayout; + + smart_refctd_ptr sema; + uint64_t timelineValue = 0; + smart_refctd_ptr cmdbuf; + smart_refctd_ptr resultsBuffer; + + uint32_t totalFailCount = 0; +}; + +NBL_MAIN_FUNC(ArithmeticUnitTestApp) \ No newline at end of file diff --git a/23_Arithmetic2UnitTest/pipeline.groovy b/23_ArithmeticUnitTest/pipeline.groovy similarity index 100% rename from 23_Arithmetic2UnitTest/pipeline.groovy rename to 23_ArithmeticUnitTest/pipeline.groovy diff --git a/24_ColorSpaceTest/CMakeLists.txt b/24_ColorSpaceTest/CMakeLists.txt index 71b1dde16..026add505 100644 --- a/24_ColorSpaceTest/CMakeLists.txt +++ b/24_ColorSpaceTest/CMakeLists.txt @@ -4,7 +4,6 @@ if(NOT RES) endif() nbl_create_executable_project("" "" "" "" "${NBL_EXECUTABLE_PROJECT_CREATION_PCH_TARGET}") -target_link_libraries(${EXECUTABLE_NAME} PRIVATE Nabla::ext::FullScreenTriangle) if(NBL_EMBED_BUILTIN_RESOURCES) set(_BR_TARGET_ ${EXECUTABLE_NAME}_builtinResourceData) @@ -33,42 +32,4 @@ add_test(NAME NBL_IMAGE_HASH_RUN_TESTS COMMAND "$" --test hash WORKING_DIRECTORY "$" COMMAND_EXPAND_LISTS -) - -set(OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/auto-gen") - -set(SM 6_8) -set(JSON [=[ -[ - { - "INPUT": "app_resources/present.frag.hlsl", - "KEY": "present", - } -] -]=]) -string(CONFIGURE "${JSON}" JSON) - -set(COMPILE_OPTIONS - -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} -) +) \ No newline at end of file diff --git a/24_ColorSpaceTest/main.cpp b/24_ColorSpaceTest/main.cpp index e347df6e7..c3ad19c6c 100644 --- a/24_ColorSpaceTest/main.cpp +++ b/24_ColorSpaceTest/main.cpp @@ -1,9 +1,10 @@ // Copyright (C) 2018-2024 - 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 "nbl/this_example/builtin/build/spirv/keys.hpp" -#include "nbl/examples/examples.hpp" +#include "nbl/application_templates/MonoAssetManagerAndBuiltinResourceApplication.hpp" +#include "SimpleWindowedApplication.hpp" +#include "nbl/video/surface/CSurfaceVulkan.h" #include "nbl/ext/FullScreenTriangle/FullScreenTriangle.h" #include "nlohmann/json.hpp" @@ -18,15 +19,14 @@ using namespace system; using namespace asset; using namespace ui; using namespace video; -using namespace nbl::examples; // defines for sampler tests can be found in the file below #include "app_resources/push_constants.hlsl" -class ColorSpaceTestSampleApp final : public SimpleWindowedApplication, public BuiltinResourcesApplication +class ColorSpaceTestSampleApp final : public examples::SimpleWindowedApplication, public application_templates::MonoAssetManagerAndBuiltinResourceApplication { - using device_base_t = SimpleWindowedApplication; - using asset_base_t = BuiltinResourcesApplication; + using device_base_t = examples::SimpleWindowedApplication; + using asset_base_t = application_templates::MonoAssetManagerAndBuiltinResourceApplication; using clock_t = std::chrono::steady_clock; using perf_clock_resolution_t = std::chrono::milliseconds; @@ -161,24 +161,26 @@ class ColorSpaceTestSampleApp final : public SimpleWindowedApplication, public B return logFail("Failed to create Full Screen Triangle protopipeline or load its vertex shader!"); // Load Custom Shader - auto loadPrecompiledShader = [&]() -> smart_refctd_ptr - { - IAssetLoader::SAssetLoadParams lp = {}; - lp.logger = m_logger.get(); - lp.workingDirectory = "app_resources"; - - auto key = nbl::this_example::builtin::build::get_spirv_key(m_device.get()); - auto assetBundle = m_assetMgr->getAsset(key.data(), lp); - const auto assets = assetBundle.getContents(); - if (assets.empty()) - return nullptr; + auto loadCompileAndCreateShader = [&](const std::string& relPath) -> smart_refctd_ptr + { + IAssetLoader::SAssetLoadParams lp = {}; + lp.logger = m_logger.get(); + lp.workingDirectory = ""; // virtual root + auto assetBundle = m_assetMgr->getAsset(relPath, lp); + const auto assets = assetBundle.getContents(); + if (assets.empty()) + return nullptr; - auto shader = IAsset::castDown(assets[0]); - return shader; - }; - auto fragmentShader = loadPrecompiledShader.operator()<"present">(); // "app_resources/present.frag.hlsl" + // lets go straight from ICPUSpecializedShader to IGPUSpecializedShader + auto source = IAsset::castDown(assets[0]); + if (!source) + return nullptr; + + return m_device->createShader(source.get()); + }; + auto fragmentShader = loadCompileAndCreateShader("app_resources/present.frag.hlsl"); if (!fragmentShader) - return logFail("Failed to load precompiled fragment shader!"); + return logFail("Failed to Load and Compile Fragment Shader!"); // Now surface indep resources m_semaphore = m_device->createSemaphore(m_submitIx); @@ -253,14 +255,14 @@ class ColorSpaceTestSampleApp final : public SimpleWindowedApplication, public B // Now create the pipeline { const asset::SPushConstantRange range = { - .stageFlags = ESS_FRAGMENT, + .stageFlags = IShader::E_SHADER_STAGE::ESS_FRAGMENT, .offset = 0, .size = sizeof(push_constants_t) }; auto layout = m_device->createPipelineLayout({ &range,1 }, nullptr, nullptr, nullptr, core::smart_refctd_ptr(dsLayout)); - const IGPUPipelineBase::SShaderSpecInfo fragSpec = { - .shader = fragmentShader.get(), + const IGPUShader::SSpecInfo fragSpec = { .entryPoint = "main", + .shader = fragmentShader.get() }; m_pipeline = fsTriProtoPPln.createPipeline(fragSpec, layout.get(), scResources->getRenderpass()/*,default is subpass 0*/); if (!m_pipeline) @@ -271,15 +273,16 @@ class ColorSpaceTestSampleApp final : public SimpleWindowedApplication, public B // Let's just use the same queue since there's no need for async present if (!m_surface || !m_surface->init(queue, std::move(scResources), swapchainParams.sharedParams)) return logFail("Could not create Window & Surface or initialize the Surface!"); + m_maxFramesInFlight = m_surface->getMaxFramesInFlight(); // create the descriptor sets, 1 per FIF and with enough room for one image sampler { - const uint32_t setCount = MaxFramesInFlight; + const uint32_t setCount = m_maxFramesInFlight; auto pool = m_device->createDescriptorPoolForDSLayouts(IDescriptorPool::E_CREATE_FLAGS::ECF_NONE, { &dsLayout.get(),1 }, &setCount); if (!pool) return logFail("Failed to Create Descriptor Pool"); - for (auto i = 0u; i < MaxFramesInFlight; i++) + for (auto i = 0u; i < m_maxFramesInFlight; i++) { m_descriptorSets[i] = pool->createDescriptorSet(core::smart_refctd_ptr(dsLayout)); if (!m_descriptorSets[i]) @@ -290,7 +293,7 @@ class ColorSpaceTestSampleApp final : public SimpleWindowedApplication, public B // need resetttable commandbuffers for the upload utility m_cmdPool = m_device->createCommandPool(queue->getFamilyIndex(), IGPUCommandPool::CREATE_FLAGS::RESET_COMMAND_BUFFER_BIT); // create the commandbuffers - for (auto i = 0u; i < MaxFramesInFlight; i++) + for (auto i = 0u; i < m_maxFramesInFlight; i++) { if (!m_cmdPool) return logFail("Couldn't create Command Pool!"); @@ -370,7 +373,7 @@ class ColorSpaceTestSampleApp final : public SimpleWindowedApplication, public B const auto* inImage = outViewParams.image.get(); const auto inImageParams = inImage->getCreationParameters(); - smart_refctd_ptr inBuffer = asset::ICPUBuffer::create({ { inImage->getBuffer()->getSize() }, const_cast(inImage->getBuffer()->getPointer()), core::getNullMemoryResource() }, core::adopt_memory); // adopt memory & don't free it on exit + smart_refctd_ptr inBuffer = core::make_smart_refctd_ptr, true> >(inImage->getBuffer()->getSize(), (uint8_t*)inImage->getBuffer()->getPointer(), core::adopt_memory); // adopt memory & don't free it on exit const auto inRegions = inImage->getRegionArray(); const auto inAmountOfRegions = inRegions->size(); @@ -541,7 +544,7 @@ class ColorSpaceTestSampleApp final : public SimpleWindowedApplication, public B struct { std::string path; - ::json data; + json data; } current, reference; current.path = (localOutputCWD / filename).make_preferred().string() + "_" + modeAsString + extension.string() + ".json"; @@ -552,7 +555,7 @@ class ColorSpaceTestSampleApp final : public SimpleWindowedApplication, public B m_logger->log("Mode: \"%s\"", ILogger::ELL_INFO, modeAsString.c_str()); m_logger->log("Writing \"%ls\"'s image hash to \"%s\"", ILogger::ELL_INFO, filename.c_str(), current.path.c_str()); - current.data["image"] = ::json::array(); + current.data["image"] = json::array(); for (const auto& it : hash) current.data["image"].push_back(it); @@ -561,7 +564,7 @@ class ColorSpaceTestSampleApp final : public SimpleWindowedApplication, public B const std::string prettyJson = current.data.dump(4); if (options.verbose) - m_logger->log("%s", ILogger::ELL_INFO, prettyJson); + m_logger->log(prettyJson, ILogger::ELL_INFO); system::ISystem::future_t> future; m_system->createFile(future, current.path, system::IFileBase::ECF_WRITE); @@ -629,25 +632,19 @@ class ColorSpaceTestSampleApp final : public SimpleWindowedApplication, public B } else { - // framesInFlight: ensuring safe execution of command buffers and acquires, `framesInFlight` only affect semaphore waits, don't use this to index your resources because it can change with swapchain recreation. - const uint32_t framesInFlight = core::min(MaxFramesInFlight, m_surface->getMaxAcquiresInFlight()); - // We block for semaphores for 2 reasons here: - // A) Resource: Can't use resource like a command buffer BEFORE previous use is finished! [MaxFramesInFlight] - // B) Acquire: Can't have more acquires in flight than a certain threshold returned by swapchain or your surface helper class. [MaxAcquiresInFlight] - if (m_submitIx >= framesInFlight) + // Can't reset a cmdbuffer before the previous use of commandbuffer is finished! + if (m_submitIx>=m_maxFramesInFlight) { - const ISemaphore::SWaitInfo cbDonePending[] = - { - { + const ISemaphore::SWaitInfo cmdbufDonePending[] = { + { .semaphore = m_semaphore.get(), - .value = m_submitIx + 1 - framesInFlight + .value = m_submitIx+1-m_maxFramesInFlight } }; - if (m_device->blockForSemaphores(cbDonePending) != ISemaphore::WAIT_RESULT::SUCCESS) + if (m_device->blockForSemaphores(cmdbufDonePending)!=ISemaphore::WAIT_RESULT::SUCCESS) return false; } - - const auto resourceIx = m_submitIx%MaxFramesInFlight; + const auto resourceIx = m_submitIx%m_maxFramesInFlight; // we don't want to overcomplicate the example with multi-queue auto queue = getGraphicsQueue(); @@ -793,9 +790,8 @@ class ColorSpaceTestSampleApp final : public SimpleWindowedApplication, public B }; cmdbuf->beginRenderPass(info,IGPUCommandBuffer::SUBPASS_CONTENTS::INLINE); } - cmdbuf->bindGraphicsPipeline(m_pipeline.get()); - cmdbuf->pushConstants(m_pipeline->getLayout(),hlsl::ShaderStage::ESS_FRAGMENT,0,sizeof(push_constants_t),&pc); + cmdbuf->pushConstants(m_pipeline->getLayout(),IGPUShader::E_SHADER_STAGE::ESS_FRAGMENT,0,sizeof(push_constants_t),&pc); cmdbuf->bindDescriptorSets(nbl::asset::EPBP_GRAPHICS,m_pipeline->getLayout(),3,1,&ds); ext::FullScreenTriangle::recordDrawCall(cmdbuf); cmdbuf->endRenderPass(); @@ -968,12 +964,12 @@ class ColorSpaceTestSampleApp final : public SimpleWindowedApplication, public B smart_refctd_ptr m_scratchSemaphore; SIntendedSubmitInfo m_intendedSubmit; // Use a separate counter to cycle through our resources for clarity - uint64_t m_submitIx = 0; - // 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; + uint64_t m_submitIx : 59 = 0; + // Maximum frames which can be simultaneously rendered + uint64_t m_maxFramesInFlight : 5; // Enough Command Buffers and other resources for all frames in flight! - std::array,MaxFramesInFlight> m_descriptorSets; - std::array,MaxFramesInFlight> m_cmdBufs; + std::array,ISwapchain::MaxImages> m_descriptorSets; + std::array,ISwapchain::MaxImages> m_cmdBufs; private: diff --git a/25_FilterTest/main.cpp b/25_FilterTest/main.cpp index 4ce68d66c..690985417 100644 --- a/25_FilterTest/main.cpp +++ b/25_FilterTest/main.cpp @@ -2,7 +2,7 @@ // This file is part of the "Nabla Engine". // For conditions of distribution and use, see copyright notice in nabla.h -#include "nbl/application_templates/BasicMultiQueueApplication.hpp" +#include "nbl/application_templates/MonoDeviceApplication.hpp" // TODO: these should come from and nbl/asset/asset.h find out why they don't #include "nbl/asset/filters/CRegionBlockFunctorFilter.h" @@ -11,1386 +11,1284 @@ #include "nbl/ext/ScreenShot/ScreenShot.h" using namespace nbl; -using namespace nbl::core; -using namespace nbl::system; using namespace nbl::asset; +using namespace nbl::core; using namespace nbl::video; +// TODO: move inside some class +static inline asset::IImageView::E_TYPE getImageViewTypeFromImageType_CPU(const asset::IImage::E_TYPE type) +{ + switch (type) + { + case asset::IImage::ET_1D: + return asset::ICPUImageView::ET_1D; + case asset::IImage::ET_2D: + return asset::ICPUImageView::ET_2D; + case asset::IImage::ET_3D: + return asset::ICPUImageView::ET_3D; + default: + assert(!"Invalid code path."); + return static_cast::E_TYPE>(0u); + } +} + +// TODO: move inside some class +static inline video::IGPUImageView::E_TYPE getImageViewTypeFromImageType_GPU(const video::IGPUImage::E_TYPE type) +{ + switch (type) + { + case video::IGPUImage::ET_1D: + return video::IGPUImageView::ET_1D_ARRAY; + case video::IGPUImage::ET_2D: + return video::IGPUImageView::ET_2D_ARRAY; + case video::IGPUImage::ET_3D: + return video::IGPUImageView::ET_3D; + default: + assert(!"Invalid code path."); + return static_cast(0u); + } +} + // TODO: inherit from BasicMultiQueue app -class BlitFilterTestApp final : public virtual application_templates::BasicMultiQueueApplication +class BlitFilterTestApp final : public virtual application_templates::MonoDeviceApplication { - static smart_refctd_ptr createCPUImage(const hlsl::uint32_t3 extent, const uint32_t layers, const IImage::E_TYPE imageType, const E_FORMAT format, const bool fillWithTestData = false) + using base_t = nbl::application_templates::MonoDeviceApplication; + + constexpr static uint32_t SC_IMG_COUNT = 3u; + + class ITest + { + public: + virtual bool run() = 0; + + protected: + ITest(core::smart_refctd_ptr&& inImage, BlitFilterTestApp* parentApp) : m_inImage(std::move(inImage)), m_parentApp(parentApp) { assert(m_parentApp); } + + core::smart_refctd_ptr m_inImage = nullptr; + BlitFilterTestApp* m_parentApp = nullptr; + + void writeImage(core::smart_refctd_ptr&& image, const char* path) { - IImage::SCreationParams imageParams = {}; - imageParams.flags = static_cast(asset::IImage::ECF_MUTABLE_FORMAT_BIT | asset::IImage::ECF_EXTENDED_USAGE_BIT); - imageParams.type = imageType; - imageParams.format = format; - imageParams.extent = { extent[0], extent[1], extent[2] }; - imageParams.mipLevels = 1; - imageParams.arrayLayers = layers; - imageParams.samples = ICPUImage::ESCF_1_BIT; - imageParams.usage = IImage::EUF_SAMPLED_BIT; - - smart_refctd_ptr image = ICPUImage::create(std::move(imageParams)); - assert(image); - - const size_t bufferSize = (((static_cast(layers) * extent[0]) * extent[1]) * extent[2]) * getTexelOrBlockBytesize(format); + asset::ICPUImageView::SCreationParams viewParams = {}; + viewParams.flags = static_cast(0u); + viewParams.image = std::move(image); + viewParams.format = viewParams.image->getCreationParameters().format; + viewParams.viewType = asset::ICPUImageView::ET_2D; + viewParams.subresourceRange.aspectMask = asset::IImage::EAF_COLOR_BIT; + viewParams.subresourceRange.baseArrayLayer = 0u; + viewParams.subresourceRange.layerCount = viewParams.image->getCreationParameters().arrayLayers; + viewParams.subresourceRange.baseMipLevel = 0u; + viewParams.subresourceRange.levelCount = viewParams.image->getCreationParameters().mipLevels; + + auto imageViewToWrite = asset::ICPUImageView::create(std::move(viewParams)); + if (!imageViewToWrite) { - auto imageRegions = make_refctd_dynamic_array>(1ull); - auto& region = imageRegions->front(); - region.bufferImageHeight = 0u; - region.bufferOffset = 0ull; - region.bufferRowLength = extent[0]; - region.imageExtent = { extent[0], extent[1], extent[2] }; - region.imageOffset = { 0u, 0u, 0u }; - region.imageSubresource.aspectMask = IImage::EAF_COLOR_BIT; - region.imageSubresource.baseArrayLayer = 0u; - region.imageSubresource.layerCount = layers; - region.imageSubresource.mipLevel = 0; - - image->setBufferAndRegions(ICPUBuffer::create({ bufferSize }),std::move(imageRegions)); + m_parentApp->m_logger->log("Failed to create image view for the output image to write it to disk.", system::ILogger::ELL_ERROR); + return; } - if (fillWithTestData) + asset::IAssetWriter::SAssetWriteParams writeParams(imageViewToWrite.get()); + if (!m_parentApp->assetManager->writeAsset(path, writeParams)) { - double pixelValueUpperBound = 20.0; - if (asset::isNormalizedFormat(format) || format == asset::EF_B10G11R11_UFLOAT_PACK32) - pixelValueUpperBound = 1.00000000001; + m_parentApp->m_logger->log("Failed to write the output image.", system::ILogger::ELL_ERROR); + return; + } + } + }; - std::uniform_real_distribution dist(0.0, pixelValueUpperBound); - std::mt19937 prng; + template + class CBlitImageFilterTest : public ITest + { + using blit_utils_t = BlitUtilities; - uint8_t* bytePtr = reinterpret_cast(image->getBuffer()->getPointer()); - const auto layerSize = bufferSize / imageParams.arrayLayers; + public: + CBlitImageFilterTest( + core::smart_refctd_ptr&& inImage, + BlitFilterTestApp* parentApp, + const core::vectorSIMDu32& outImageDim, + const asset::E_FORMAT outImageFormat, + const char* writeImagePath, + const typename blit_utils_t::convolution_kernels_t& convolutionKernels, + const IBlitUtilities::E_ALPHA_SEMANTIC alphaSemantic = asset::IBlitUtilities::EAS_NONE_OR_PREMULTIPLIED, + const float referenceAlpha = 0.5f, + const uint32_t alphaBinCount = asset::IBlitUtilities::DefaultAlphaBinCount) + : ITest(std::move(inImage), parentApp), m_outImageDim(outImageDim), m_outImageFormat(outImageFormat), + m_convolutionKernels(convolutionKernels), m_writeImagePath(writeImagePath), + m_alphaSemantic(alphaSemantic), m_referenceAlpha(referenceAlpha), m_alphaBinCount(alphaBinCount) + {} + + bool run() override + { + const auto& inImageExtent = m_inImage->getCreationParameters().extent; + const auto& inImageFormat = m_inImage->getCreationParameters().format; - double dummyVal = 1.0; - for (auto layer = 0; layer < layers; ++layer) - for (uint64_t k = 0u; k < extent[2]; ++k) - for (uint64_t j = 0u; j < extent[1]; ++j) - for (uint64_t i = 0; i < extent[0]; ++i) - { - double decodedPixel[4] = { 0 }; - for (uint32_t ch = 0u; ch < asset::getFormatChannelCount(format); ++ch) - decodedPixel[ch] = dist(prng); + assert(m_outImageDim.w == m_inImage->getCreationParameters().arrayLayers); - const uint64_t pixelIndex = (k * extent[1] * extent[0]) + (j * extent[0]) + i; - asset::encodePixelsRuntime(format, bytePtr + layer * layerSize + pixelIndex * asset::getTexelOrBlockBytesize(format), decodedPixel); - } + auto outImage = m_parentApp->createCPUImage(m_outImageDim, m_inImage->getCreationParameters().type, m_outImageFormat); + if (!outImage) + { + m_parentApp->m_logger->log("Failed to create CPU image for output.", system::ILogger::ELL_ERROR); + return false; } - return image; - } + // enabled clamping so the test outputs don't look weird on Kaiser filters which ring + using BlitFilter = asset::CBlitImageFilter; + typename BlitFilter::state_type blitFilterState(m_convolutionKernels); - using base_t = application_templates::BasicMultiQueueApplication; + blitFilterState.inOffsetBaseLayer = core::vectorSIMDu32(); + blitFilterState.inExtentLayerCount = core::vectorSIMDu32(0u, 0u, 0u, m_inImage->getCreationParameters().arrayLayers) + m_inImage->getMipSize(); + blitFilterState.inImage = m_inImage.get(); - constexpr static uint32_t SC_IMG_COUNT = 3u; + blitFilterState.outImage = outImage.get(); - // Class to unify test inputs and writing out of the result - class ITest - { - public: - virtual bool run() = 0; + blitFilterState.outOffsetBaseLayer = core::vectorSIMDu32(); + blitFilterState.outExtentLayerCount = m_outImageDim; - protected: - ITest(smart_refctd_ptr&& inImage, BlitFilterTestApp* parentApp) : m_inImage(std::move(inImage)), m_parentApp(parentApp) { assert(m_parentApp); } + blitFilterState.alphaSemantic = m_alphaSemantic; + blitFilterState.alphaBinCount = m_alphaBinCount; + blitFilterState.alphaRefValue = m_referenceAlpha; - smart_refctd_ptr m_inImage = nullptr; - BlitFilterTestApp* m_parentApp = nullptr; + blitFilterState.scratchMemoryByteSize = BlitFilter::getRequiredScratchByteSize(&blitFilterState); + blitFilterState.scratchMemory = reinterpret_cast(_NBL_ALIGNED_MALLOC(blitFilterState.scratchMemoryByteSize, 32)); - void writeImage(const ICPUImage* image, const std::string_view path) - { - const auto& params = image->getCreationParameters(); + if (!blit_utils_t::computeScaledKernelPhasedLUT(blitFilterState.scratchMemory + BlitFilter::getScratchOffset(&blitFilterState, BlitFilter::ESU_SCALED_KERNEL_PHASED_LUT), blitFilterState.inExtentLayerCount, blitFilterState.outExtentLayerCount, blitFilterState.inImage->getCreationParameters().type, m_convolutionKernels)) + { + m_parentApp->m_logger->log("Failed to compute the LUT for blitting", system::ILogger::ELL_ERROR); + return false; + } - ICPUImageView::SCreationParams viewParams = {}; - viewParams.flags = static_cast(0u); - viewParams.image = core::smart_refctd_ptr(const_cast(image)); - viewParams.format = params.format; - switch (params.type) - { - case ICPUImage::ET_1D: - viewParams.viewType = params.arrayLayers>1 ? ICPUImageView::ET_1D_ARRAY:ICPUImageView::ET_1D; - break; - case ICPUImage::ET_2D: - viewParams.viewType = params.arrayLayers>1 ? ICPUImageView::ET_2D_ARRAY:ICPUImageView::ET_2D; - break; - default: - viewParams.viewType = ICPUImageView::ET_3D; - break; - } - viewParams.subresourceRange.aspectMask = asset::IImage::EAF_COLOR_BIT; - viewParams.subresourceRange.baseArrayLayer = 0u; - viewParams.subresourceRange.layerCount = params.arrayLayers; - viewParams.subresourceRange.baseMipLevel = 0u; - viewParams.subresourceRange.levelCount = params.mipLevels; - - auto imageViewToWrite = ICPUImageView::create(std::move(viewParams)); - if (!imageViewToWrite) - { - m_parentApp->m_logger->log("Failed to create image view for the output image to write it to disk.", system::ILogger::ELL_ERROR); - return; - } + if (!BlitFilter::execute(core::execution::par_unseq, &blitFilterState)) + { + m_parentApp->m_logger->log("Failed to blit", system::ILogger::ELL_ERROR); + return false; + } - asset::IAssetWriter::SAssetWriteParams writeParams(imageViewToWrite.get()); - if (!m_parentApp->assetManager->writeAsset(core::string(path),writeParams)) - { - m_parentApp->m_logger->log("Failed to write the output image.", system::ILogger::ELL_ERROR); - return; - } - } - }; + _NBL_ALIGNED_FREE(blitFilterState.scratchMemory); + + writeImage(std::move(outImage), m_writeImagePath); + + return true; + } - // CPU Blit test - template requires std::is_base_of_v - class CBlitImageFilterTest : public ITest + private: + const core::vectorSIMDu32 m_outImageDim; + const asset::E_FORMAT m_outImageFormat; + const typename blit_utils_t::convolution_kernels_t m_convolutionKernels; + const char* m_writeImagePath; + const IBlitUtilities::E_ALPHA_SEMANTIC m_alphaSemantic; + const float m_referenceAlpha; + const uint32_t m_alphaBinCount; + }; + + class CFlattenRegionsImageFilterTest : public ITest + { + public: + CFlattenRegionsImageFilterTest( + core::smart_refctd_ptr&& inImage, + BlitFilterTestApp* parentApp, + const bool enablePrefill, + const asset::IImageFilter::IState::ColorValue& fillColor, + const char* writeImagePath) + : ITest(std::move(inImage), parentApp), m_enablePrefill(enablePrefill), m_writeImagePath(writeImagePath) { - using blit_utils_t = BlitUtilities; - using convolution_kernels_t = typename blit_utils_t::convolution_kernels_t; - - public: - CBlitImageFilterTest( - BlitFilterTestApp* parentApp, - smart_refctd_ptr&& inImage, - const hlsl::uint32_t3& outImageDim, - const uint32_t& outImageLayers, - const E_FORMAT outImageFormat, - const char* writeImagePath, - const convolution_kernels_t& convolutionKernels, - const IBlitUtilities::E_ALPHA_SEMANTIC alphaSemantic = IBlitUtilities::EAS_NONE_OR_PREMULTIPLIED, - const float referenceAlpha = 0.5f, - const uint32_t alphaBinCount = IBlitUtilities::DefaultAlphaBinCount) - : ITest(std::move(inImage), parentApp), m_convolutionKernels(convolutionKernels), m_writeImagePath(writeImagePath), - m_outImageDim(outImageDim), m_outImageLayers(outImageLayers), m_outImageFormat(outImageFormat), - m_alphaSemantic(alphaSemantic), m_referenceAlpha(referenceAlpha), m_alphaBinCount(alphaBinCount) - {} - - bool run() override - { - const auto& inImageExtent = m_inImage->getCreationParameters().extent; - const auto& inImageFormat = m_inImage->getCreationParameters().format; + memcpy(&m_fillColorValue, &fillColor, sizeof(asset::IImageFilter::IState::ColorValue)); + } - auto outImage = createCPUImage(m_outImageDim, m_outImageLayers, m_inImage->getCreationParameters().type, m_outImageFormat); - if (!outImage) - { - m_parentApp->m_logger->log("Failed to create CPU image for output.", system::ILogger::ELL_ERROR); - return false; - } + bool run() override + { + const auto& inImageExtent = m_inImage->getCreationParameters().extent; + const auto& inImageFormat = m_inImage->getCreationParameters().format; + const uint32_t inImageMipCount = m_inImage->getCreationParameters().mipLevels; - // enabled clamping so the test outputs don't look weird on Kaiser filters which ring - using BlitFilter = asset::CBlitImageFilter; - typename BlitFilter::state_type blitFilterState(m_convolutionKernels); - - const auto mipSize = m_inImage->getMipSize(); - - blitFilterState.inOffsetBaseLayer = hlsl::uint32_t4(0,0,0,0); - blitFilterState.inExtentLayerCount = hlsl::uint32_t4(mipSize.x,mipSize.y,mipSize.z,m_outImageLayers); - blitFilterState.inImage = m_inImage.get(); - - blitFilterState.outImage = outImage.get(); - - blitFilterState.outOffsetBaseLayer = hlsl::uint32_t4(); - blitFilterState.outExtentLayerCount = hlsl::uint32_t4(m_outImageDim[0],m_outImageDim[1],m_outImageDim[2],m_outImageLayers); - - blitFilterState.alphaSemantic = m_alphaSemantic; - blitFilterState.alphaBinCount = m_alphaBinCount; - blitFilterState.alphaRefValue = m_referenceAlpha; - - blitFilterState.scratchMemoryByteSize = BlitFilter::getRequiredScratchByteSize(&blitFilterState); - auto scratch = std::make_unique(blitFilterState.scratchMemoryByteSize); - blitFilterState.scratchMemory = scratch.get(); - - const auto lutOffsetInScratch = BlitFilter::getScratchOffset(&blitFilterState,BlitFilter::ESU_SCALED_KERNEL_PHASED_LUT); - if (!blit_utils_t::computeScaledKernelPhasedLUT( - blitFilterState.scratchMemory+lutOffsetInScratch, - blitFilterState.inExtentLayerCount, - blitFilterState.outExtentLayerCount, - blitFilterState.inImage->getCreationParameters().type, - m_convolutionKernels - )) - { - m_parentApp->m_logger->log("Failed to compute the LUT for blitting",ILogger::ELL_ERROR); - return false; - } + core::smart_refctd_ptr flattenInImage; + { + const uint64_t bufferSizeNeeded = (inImageExtent.width * inImageExtent.height * inImageExtent.depth * asset::getTexelOrBlockBytesize(inImageFormat)) / 2ull; + + IImage::SCreationParams imageParams = {}; + imageParams.type = asset::ICPUImage::ET_2D; + imageParams.format = inImageFormat; + imageParams.extent = { inImageExtent.width, inImageExtent.height, inImageExtent.depth }; + imageParams.mipLevels = 1u; + imageParams.arrayLayers = 1u; + imageParams.samples = asset::ICPUImage::ESCF_1_BIT; + + auto imageRegions = core::make_refctd_dynamic_array>(2ull); + std::fill(imageRegions->begin(),imageRegions->end(),*m_inImage->getRegion(0,core::vectorSIMDu32(0,0,0))); + { + auto& region = (*imageRegions)[0]; + region.bufferOffset = 0ull; + if (!region.bufferRowLength) + region.bufferRowLength = region.imageExtent.width; + region.imageExtent = { imageParams.extent.width / 2, imageParams.extent.height / 2, core::max(imageParams.extent.depth / 2, 1) }; + region.imageOffset = { 0u, 0u, 0u }; + } + { + auto& region = (*imageRegions)[1]; + if (!region.bufferRowLength) + region.bufferRowLength = region.imageExtent.width; + region.imageExtent = { imageParams.extent.width / 2, imageParams.extent.height / 2, core::max(imageParams.extent.depth / 2, 1) }; + region.imageOffset = { imageParams.extent.width / 2, imageParams.extent.height / 2, 0u }; + const auto blockSize = asset::getBlockDimensions(inImageFormat); + region.bufferOffset += ((region.bufferRowLength/blockSize[0])*(region.imageOffset.y/blockSize[1])+region.imageOffset.x/blockSize[0])*asset::getTexelOrBlockBytesize(inImageFormat); + } - if (!BlitFilter::execute(core::execution::par_unseq,&blitFilterState)) - { - m_parentApp->m_logger->log("Failed to blit",ILogger::ELL_ERROR); - return false; - } + flattenInImage = ICPUImage::create(std::move(imageParams)); + if (!flattenInImage) + { + m_parentApp->m_logger->log("Failed to create the flatten input image.", system::ILogger::ELL_ERROR); + return false; + } + + flattenInImage->setBufferAndRegions(core::smart_refctd_ptr(m_inImage->getBuffer()), imageRegions); + } - writeImage(outImage.get(),m_writeImagePath); + asset::CFlattenRegionsImageFilter::CState filterState; + filterState.inImage = flattenInImage.get(); + filterState.outImage = nullptr; + filterState.preFill = m_enablePrefill; - return true; + if (filterState.preFill) + { + const auto inImageFormat = filterState.inImage->getCreationParameters().format; + if (asset::isBlockCompressionFormat(inImageFormat)) + memcpy(filterState.fillValue.asCompressedBlock, m_fillColorValue.asCompressedBlock, asset::getTexelOrBlockBytesize(inImageFormat)); + else if (asset::isFloatingPointFormat(inImageFormat)) + filterState.fillValue.asFloat = m_fillColorValue.asFloat; + else + _NBL_TODO(); + } + + if (!asset::CFlattenRegionsImageFilter::execute(&filterState)) + { + m_parentApp->m_logger->log("CFlattenRegionsImageFilter failed.", system::ILogger::ELL_ERROR); + return false; } - private: - const convolution_kernels_t m_convolutionKernels; - const char* m_writeImagePath; - const hlsl::uint32_t3 m_outImageDim; - const uint32_t m_outImageLayers; - const E_FORMAT m_outImageFormat; - const IBlitUtilities::E_ALPHA_SEMANTIC m_alphaSemantic; - const float m_referenceAlpha; - const uint32_t m_alphaBinCount; - }; + writeImage(core::smart_refctd_ptr(filterState.outImage), m_writeImagePath); + + return true; + } - template - class CSwizzleAndConvertTest : public ITest + private: + const bool m_enablePrefill; + asset::IImageFilter::IState::ColorValue m_fillColorValue; + const char* m_writeImagePath; + }; + + template + class CSwizzleAndConvertTest : public ITest + { + public: + CSwizzleAndConvertTest(core::smart_refctd_ptr&& inImage, + BlitFilterTestApp* parentApp, + const asset::E_FORMAT outFormat, + core::vectorSIMDu32 inOffsetBaseLayer, + core::vectorSIMDu32 outOffsetBaseLayer, + asset::ICPUImageView::SComponentMapping swizzle, + const char* writeImagePath) + : ITest(std::move(inImage), parentApp), m_outFormat(outFormat), m_inOffsetBaseLayer(inOffsetBaseLayer), m_outOffsetBaseLayer(outOffsetBaseLayer), m_swizzle(swizzle), m_writeImagePath(writeImagePath) + {} + + bool run() override { - public: - CSwizzleAndConvertTest(core::smart_refctd_ptr&& inImage, - BlitFilterTestApp* parentApp, - const asset::E_FORMAT outFormat, - hlsl::uint32_t4 inOffsetBaseLayer, - hlsl::uint32_t4 outOffsetBaseLayer, - asset::ICPUImageView::SComponentMapping swizzle, - const char* writeImagePath) - : ITest(std::move(inImage), parentApp), m_outFormat(outFormat), m_inOffsetBaseLayer(inOffsetBaseLayer), m_outOffsetBaseLayer(outOffsetBaseLayer), m_swizzle(swizzle), m_writeImagePath(writeImagePath) - {} - - bool run() override - { - if (!m_inImage) - return false; + if (!m_inImage) + return false; - const auto& inImageExtent = m_inImage->getCreationParameters().extent; - const auto& inImageFormat = m_inImage->getCreationParameters().format; + const auto& inImageExtent = m_inImage->getCreationParameters().extent; + const auto& inImageFormat = m_inImage->getCreationParameters().format; - auto outImage = createCPUImage({inImageExtent.width,inImageExtent.height,inImageExtent.depth}, m_inImage->getCreationParameters().arrayLayers, m_inImage->getCreationParameters().type, m_outFormat); - if (!outImage) - return false; + auto outImage = m_parentApp->createCPUImage(core::vectorSIMDu32(inImageExtent.width, inImageExtent.height, inImageExtent.depth, m_inImage->getCreationParameters().arrayLayers), m_inImage->getCreationParameters().type, m_outFormat); + if (!outImage) + return false; - using convert_filter_t = asset::CSwizzleAndConvertImageFilter; + using convert_filter_t = asset::CSwizzleAndConvertImageFilter; - typename convert_filter_t::state_type filterState = {}; - filterState.extentLayerCount = core::vectorSIMDu32( - inImageExtent.width-m_inOffsetBaseLayer.x, - inImageExtent.height-m_inOffsetBaseLayer.y, - inImageExtent.depth- m_inOffsetBaseLayer.z, - m_inImage->getCreationParameters().arrayLayers-m_inOffsetBaseLayer.w - ); - assert((static_cast(filterState.extentLayerCount) > core::vectorSIMDi32(0)).all()); + typename convert_filter_t::state_type filterState = {}; + filterState.extentLayerCount = core::vectorSIMDu32(inImageExtent.width, inImageExtent.height, inImageExtent.depth, m_inImage->getCreationParameters().arrayLayers) - m_inOffsetBaseLayer; + assert((static_cast(filterState.extentLayerCount) > core::vectorSIMDi32(0)).all()); - filterState.inOffsetBaseLayer = reinterpret_cast(m_inOffsetBaseLayer); - filterState.outOffsetBaseLayer = reinterpret_cast(m_outOffsetBaseLayer); - filterState.inMipLevel = 0; - filterState.outMipLevel = 0; - filterState.inImage = m_inImage.get(); - filterState.outImage = outImage.get(); - filterState.swizzle = m_swizzle; + filterState.inOffsetBaseLayer = m_inOffsetBaseLayer; + filterState.outOffsetBaseLayer = m_outOffsetBaseLayer; + filterState.inMipLevel = 0; + filterState.outMipLevel = 0; + filterState.inImage = m_inImage.get(); + filterState.outImage = outImage.get(); + filterState.swizzle = m_swizzle; - if constexpr (std::is_same_v) - { - asset::CWhiteNoiseDither::CState ditherState; - ditherState.texelRange.offset = filterState.inOffset; - ditherState.texelRange.extent = filterState.extent; + if constexpr (std::is_same_v) + { + asset::CWhiteNoiseDither::CState ditherState; + ditherState.texelRange.offset = filterState.inOffset; + ditherState.texelRange.extent = filterState.extent; - filterState.dither = asset::CWhiteNoiseDither(); - filterState.ditherState = &ditherState; - } + filterState.dither = asset::CWhiteNoiseDither(); + filterState.ditherState = &ditherState; + } - if (!convert_filter_t::execute(&filterState)) - return false; + if (!convert_filter_t::execute(&filterState)) + return false; - writeImage(outImage.get(),m_writeImagePath); + writeImage(std::move(outImage), m_writeImagePath); - return true; - } + return true; + } - private: - asset::E_FORMAT m_outFormat = asset::EF_UNKNOWN; - hlsl::uint32_t4 m_inOffsetBaseLayer; - hlsl::uint32_t4 m_outOffsetBaseLayer; - asset::ICPUImageView::SComponentMapping m_swizzle; - const char* m_writeImagePath; - }; + private: + asset::E_FORMAT m_outFormat = asset::EF_UNKNOWN; + core::vectorSIMDu32 m_inOffsetBaseLayer; + core::vectorSIMDu32 m_outOffsetBaseLayer; + asset::ICPUImageView::SComponentMapping m_swizzle; + const char* m_writeImagePath; + }; + + template + class CComputeBlitTest : public ITest + { + using blit_utils_t = BlitUtilities; - template - class CComputeBlitTest : public ITest + public: + CComputeBlitTest( + core::smart_refctd_ptr&& inImage, + BlitFilterTestApp* parentApp, + const core::vectorSIMDu32& outImageDim, + const typename blit_utils_t::convolution_kernels_t& convolutionKernels, + const IBlitUtilities::E_ALPHA_SEMANTIC alphaSemantic = asset::IBlitUtilities::EAS_NONE_OR_PREMULTIPLIED, + const float referenceAlpha = 0.f, + const uint32_t alphaBinCount = asset::IBlitUtilities::DefaultAlphaBinCount) + : ITest(std::move(inImage), parentApp), m_outImageDim(outImageDim), m_convolutionKernels(convolutionKernels), + m_alphaSemantic(alphaSemantic), m_referenceAlpha(referenceAlpha), m_alphaBinCount(alphaBinCount) + {} + + bool run() override { - using blit_utils_t = BlitUtilities; - using convolution_kernels_t = typename blit_utils_t::convolution_kernels_t; - - public: - CComputeBlitTest( - BlitFilterTestApp* parentApp, - const char* outputName, - smart_refctd_ptr&& inImage, - const hlsl::uint32_t3& outImageDim, - const convolution_kernels_t& convolutionKernels, - const IBlitUtilities::E_ALPHA_SEMANTIC alphaSemantic = IBlitUtilities::EAS_NONE_OR_PREMULTIPLIED, - const float referenceAlpha = 0.f, - const uint32_t alphaBinCount = IBlitUtilities::DefaultAlphaBinCount - ) : ITest(std::move(inImage), parentApp), m_outputName(outputName), m_convolutionKernels(convolutionKernels), - m_outImageDim(outImageDim), m_alphaSemantic(alphaSemantic), m_referenceAlpha(referenceAlpha), m_alphaBinCount(alphaBinCount) - { - } + assert(m_inImage->getCreationParameters().mipLevels == 1); + // GPU clamps when storing to a texture, so the CPU needs to as well + using BlitFilter = asset::CBlitImageFilter; - bool run() override - { - assert(m_inImage->getCreationParameters().mipLevels == 1); + const asset::E_FORMAT inImageFormat = m_inImage->getCreationParameters().format; + const asset::E_FORMAT outImageFormat = inImageFormat; + const auto layerCount = m_inImage->getCreationParameters().arrayLayers; + assert(m_outImageDim.w == layerCount); - // GPU clamps when storing to a texture, so the CPU needs to as well - using BlitFilter = CBlitImageFilter; + auto computeAlphaCoverage = [this](const double referenceAlpha, asset::ICPUImage* image) -> float + { + const uint32_t mipLevel = 0u; - const auto inCreationParams = m_inImage->getCreationParameters(); - const auto layerCount = inCreationParams.arrayLayers; + uint32_t alphaTestPassCount = 0u; - auto* logger = m_parentApp->m_logger.get(); + const auto& extent = image->getCreationParameters().extent; + const auto layerCount = image->getCreationParameters().arrayLayers; - auto computeAlphaCoverage = [&](ICPUImage* image) -> void + for (auto layer = 0; layer < layerCount; ++layer) + { + for (uint32_t z = 0u; z < extent.depth; ++z) { - constexpr uint32_t mipLevel = 0u; - - const auto format = image->getCreationParameters().format; - const auto extent = image->getCreationParameters().extent; - for (auto layer=0; layergetTexelBlockData(mipLevel,texCoord,dummy); + const void* encodedPixel = image->getTexelBlockData(mipLevel, texCoord, dummy); double decodedPixel[4]; - asset::decodePixelsRuntime(format, &encodedPixel, decodedPixel, dummy.x, dummy.y); + asset::decodePixelsRuntime(image->getCreationParameters().format, &encodedPixel, decodedPixel, dummy.x, dummy.y); - if (decodedPixel[3] > m_referenceAlpha) + if (decodedPixel[3] > referenceAlpha) ++alphaTestPassCount; } - const float alphaCoverage = float(alphaTestPassCount) / float(extent.width * extent.height * extent.depth); - logger->log("CPU alpha coverage: %f with reference value %f", ILogger::ELL_INFO, alphaCoverage, m_referenceAlpha); } - }; + } + } - const auto type = inCreationParams.type; - const E_FORMAT inImageFormat = inCreationParams.format; - const E_FORMAT outImageFormat = inImageFormat; + const float alphaCoverage = float(alphaTestPassCount) / float(extent.width * extent.height * extent.depth * layerCount); + return alphaCoverage; + }; - // CPU - { - auto outImageCPU = createCPUImage(m_outImageDim,layerCount,type,outImageFormat); - if (!outImageCPU) - return false; + // CPU + core::vector cpuOutput(static_cast(m_outImageDim[0]) * m_outImageDim[1] * m_outImageDim[2] * asset::getTexelOrBlockBytesize(outImageFormat) * layerCount); + { + auto outImageCPU = m_parentApp->createCPUImage(m_outImageDim, m_inImage->getCreationParameters().type, outImageFormat, 1); + if (!outImageCPU) + return false; - typename BlitFilter::state_type blitFilterState = typename BlitFilter::state_type(m_convolutionKernels); + typename BlitFilter::state_type blitFilterState = typename BlitFilter::state_type(m_convolutionKernels); - const auto mipSize = m_inImage->getMipSize(); + blitFilterState.inOffsetBaseLayer = core::vectorSIMDu32(); + blitFilterState.inExtentLayerCount = core::vectorSIMDu32(0u, 0u, 0u, layerCount) + m_inImage->getMipSize(); + blitFilterState.inImage = m_inImage.get(); + blitFilterState.outImage = outImageCPU.get(); - blitFilterState.inOffsetBaseLayer = hlsl::uint32_t4(0,0,0,0); - blitFilterState.inExtentLayerCount = hlsl::uint32_t4(mipSize.x,mipSize.y,mipSize.z,layerCount); - blitFilterState.inImage = m_inImage.get(); - blitFilterState.outImage = outImageCPU.get(); + blitFilterState.outOffsetBaseLayer = core::vectorSIMDu32(); + blitFilterState.outExtentLayerCount = m_outImageDim; - blitFilterState.outOffsetBaseLayer = hlsl::uint32_t4(); - blitFilterState.outExtentLayerCount = hlsl::uint32_t4(m_outImageDim[0],m_outImageDim[1],m_outImageDim[2],layerCount); + blitFilterState.axisWraps[0] = asset::ISampler::ETC_CLAMP_TO_EDGE; + blitFilterState.axisWraps[1] = asset::ISampler::ETC_CLAMP_TO_EDGE; + blitFilterState.axisWraps[2] = asset::ISampler::ETC_CLAMP_TO_EDGE; + blitFilterState.borderColor = asset::ISampler::E_TEXTURE_BORDER_COLOR::ETBC_FLOAT_OPAQUE_WHITE; - blitFilterState.axisWraps[0] = asset::ISampler::E_TEXTURE_CLAMP::ETC_CLAMP_TO_EDGE; - blitFilterState.axisWraps[1] = asset::ISampler::E_TEXTURE_CLAMP::ETC_CLAMP_TO_EDGE; - blitFilterState.axisWraps[2] = asset::ISampler::E_TEXTURE_CLAMP::ETC_CLAMP_TO_EDGE; - blitFilterState.borderColor = asset::ISampler::E_TEXTURE_BORDER_COLOR::ETBC_FLOAT_OPAQUE_WHITE; + blitFilterState.alphaSemantic = m_alphaSemantic; + blitFilterState.alphaBinCount = m_alphaBinCount; + blitFilterState.alphaRefValue = m_referenceAlpha; - blitFilterState.alphaSemantic = m_alphaSemantic; - blitFilterState.alphaBinCount = m_alphaBinCount; - blitFilterState.alphaRefValue = m_referenceAlpha; + blitFilterState.scratchMemoryByteSize = BlitFilter::getRequiredScratchByteSize(&blitFilterState); + blitFilterState.scratchMemory = reinterpret_cast(_NBL_ALIGNED_MALLOC(blitFilterState.scratchMemoryByteSize, 32)); - blitFilterState.scratchMemoryByteSize = BlitFilter::getRequiredScratchByteSize(&blitFilterState); - blitFilterState.scratchMemory = reinterpret_cast(_NBL_ALIGNED_MALLOC(blitFilterState.scratchMemoryByteSize, 32)); + if (!BlitFilter::blit_utils_t::computeScaledKernelPhasedLUT(blitFilterState.scratchMemory + BlitFilter::getScratchOffset(&blitFilterState, BlitFilter::ESU_SCALED_KERNEL_PHASED_LUT), blitFilterState.inExtentLayerCount, blitFilterState.outExtentLayerCount, blitFilterState.inImage->getCreationParameters().type, m_convolutionKernels)) + m_parentApp->m_logger->log("Failed to compute the LUT for blitting\n", system::ILogger::ELL_ERROR); - const auto lutOffsetInScratch = BlitFilter::getScratchOffset(&blitFilterState, BlitFilter::ESU_SCALED_KERNEL_PHASED_LUT); - if (!BlitFilter::blit_utils_t::computeScaledKernelPhasedLUT( - blitFilterState.scratchMemory+lutOffsetInScratch, - blitFilterState.inExtentLayerCount, - blitFilterState.outExtentLayerCount, - type, - m_convolutionKernels - )) - logger->log("Failed to compute the LUT for blitting\n", ILogger::ELL_ERROR); + m_parentApp->m_logger->log("CPU begin.."); + if (!BlitFilter::execute(core::execution::par_unseq, &blitFilterState)) + m_parentApp->m_logger->log("Failed to blit\n", system::ILogger::ELL_ERROR); + m_parentApp->m_logger->log("CPU end.."); - logger->log("CPU begin.."); - if (!BlitFilter::execute(core::execution::par_unseq, &blitFilterState)) - logger->log("Failed to blit\n", ILogger::ELL_ERROR); - logger->log("CPU end.."); + if (m_alphaSemantic == IBlitUtilities::EAS_REFERENCE_OR_COVERAGE) + m_parentApp->m_logger->log("CPU alpha coverage: %f", system::ILogger::ELL_DEBUG, computeAlphaCoverage(m_referenceAlpha, outImageCPU.get())); - if (m_alphaSemantic == IBlitUtilities::EAS_REFERENCE_OR_COVERAGE) - computeAlphaCoverage(outImageCPU.get()); + // TODO: this is silly and just slows us down + memcpy(cpuOutput.data(), outImageCPU->getBuffer()->getPointer(), cpuOutput.size()); - _NBL_ALIGNED_FREE(blitFilterState.scratchMemory); + _NBL_ALIGNED_FREE(blitFilterState.scratchMemory); + } - writeImage(outImageCPU.get(),"cpu_blit_ref_"+m_outputName+".dds"); - } - // GPU - { - auto* device = m_parentApp->m_device.get(); - auto* computeQueue = m_parentApp->getComputeQueue(); + // GPU + core::vector gpuOutput(static_cast(m_outImageDim[0]) * m_outImageDim[1] * m_outImageDim[2] * asset::getTexelOrBlockBytesize(outImageFormat) * layerCount); + { + constexpr auto BlitWorkgroupSize = video::CComputeBlit::DefaultBlitWorkgroupSize; - auto* utils = m_parentApp->m_utils.get(); - // timeline semaphore - auto semaphore = device->createSemaphore(0); + assert(m_inImage->getCreationParameters().mipLevels == 1); - assert(m_inImage->getCreationParameters().mipLevels==1); + auto transitionImageLayout = [this](core::smart_refctd_ptr&& image, const asset::IImage::E_LAYOUT finalLayout) + { + core::smart_refctd_ptr cmdbuf = nullptr; + m_parentApp->m_device->createCommandBuffers(m_parentApp->commandPool.get(), video::IGPUCommandBuffer::EL_PRIMARY, 1u, &cmdbuf); + + auto fence = m_parentApp->m_device->createFence(video::IGPUFence::ECF_UNSIGNALED); + + video::IGPUCommandBuffer::SImageMemoryBarrier barrier = {}; + barrier.oldLayout = asset::IImage::EL_UNDEFINED; + barrier.newLayout = finalLayout; + barrier.srcQueueFamilyIndex = ~0u; + barrier.dstQueueFamilyIndex = ~0u; + barrier.image = image; + barrier.subresourceRange.aspectMask = video::IGPUImage::EAF_COLOR_BIT; + barrier.subresourceRange.levelCount = image->getCreationParameters().mipLevels; + barrier.subresourceRange.layerCount = image->getCreationParameters().arrayLayers; + + cmdbuf->begin(video::IGPUCommandBuffer::EU_ONE_TIME_SUBMIT_BIT); + cmdbuf->pipelineBarrier(asset::EPSF_TOP_OF_PIPE_BIT, asset::EPSF_BOTTOM_OF_PIPE_BIT, asset::EDF_NONE, 0u, nullptr, 0u, nullptr, 1u, &barrier); + cmdbuf->end(); + + video::IGPUQueue::SSubmitInfo submitInfo = {}; + submitInfo.commandBufferCount = 1u; + submitInfo.commandBuffers = &cmdbuf.get(); + m_parentApp->queue->submit(1u, &submitInfo, fence.get()); + m_parentApp->m_device->blockForFences(1u, &fence.get()); + }; + + core::smart_refctd_ptr inImageGPU = nullptr; + { + m_parentApp->cpu2gpuParams.beginCommandBuffers(); + auto gpuArray = m_parentApp->cpu2gpu.getGPUObjectsFromAssets(&m_inImage, &m_inImage+ 1ull, m_parentApp->cpu2gpuParams); + m_parentApp->cpu2gpuParams.waitForCreationToComplete(); + if (!gpuArray || gpuArray->size() < 1ull || (!(*gpuArray)[0])) + { + m_parentApp->m_logger->log("Cannot convert the inpute CPU image to GPU image", system::ILogger::ELL_ERROR); + return false; + } - // - IImageViewBase::E_TYPE viewType; - switch (type) - { - case IImage::E_TYPE::ET_1D: - viewType = IImageViewBase::E_TYPE::ET_1D_ARRAY; - break; - case IImage::E_TYPE::ET_3D: - viewType = IImageViewBase::E_TYPE::ET_3D; - break; - default: - viewType = IImageViewBase::E_TYPE::ET_2D_ARRAY; - break; - } - - // Create resources needed to do the blit - auto blitFilter = m_parentApp->m_blitFilter.get(); - - // - auto converter = CAssetConverter::create({.device=device}); - - // - using binding_t = ICPUDescriptorSetLayout::SBinding; - using binding_create_f = binding_t::E_CREATE_FLAGS; - const core::bitflag BindingFlags = binding_create_f::ECF_PARTIALLY_BOUND_BIT|binding_create_f::ECF_UPDATE_AFTER_BIND_BIT|binding_create_f::ECF_UPDATE_UNUSED_WHILE_PENDING_BIT; - const binding_t kernelBinding = {{},0u,IDescriptor::E_TYPE::ET_UNIFORM_TEXEL_BUFFER,BindingFlags,IShader::E_SHADER_STAGE::ESS_COMPUTE,2,nullptr}; - const binding_t inputBinding = {{},1u,IDescriptor::E_TYPE::ET_SAMPLED_IMAGE,BindingFlags,IShader::E_SHADER_STAGE::ESS_COMPUTE,2,nullptr}; - const binding_t samplerBinding = {{},2u,IDescriptor::E_TYPE::ET_SAMPLER,BindingFlags,IShader::E_SHADER_STAGE::ESS_COMPUTE,2,nullptr}; - const binding_t outputBinding = {{},3u,IDescriptor::E_TYPE::ET_STORAGE_IMAGE,BindingFlags,IShader::E_SHADER_STAGE::ESS_COMPUTE,2,nullptr}; - - // - CComputeBlit::SPipelines pipelines = {}; - { - const binding_t bindings[] = {kernelBinding,inputBinding,samplerBinding,outputBinding}; - auto layout = make_smart_refctd_ptr(CComputeBlit::DefaultPushConstantRanges,make_smart_refctd_ptr(bindings),nullptr,nullptr,nullptr); - // - const CComputeBlit::SPipelinesCreateInfo info = { - .converter = converter.get(), - .layout = layout.get(), - .kernelWeights = {.binding=kernelBinding.binding,.set=0}, - .inputs = {.binding=inputBinding.binding,.set=0}, - .samplers = {.binding=samplerBinding.binding,.set=0}, - .outputs = {.binding=outputBinding.binding,.set=0} - }; - pipelines = blitFilter->createAndCachePipelines(info); - } + inImageGPU = gpuArray->begin()[0]; - // start capturing - m_parentApp->m_api->startCapture(); + // Do layout transition to SHADER_READ_ONLY_OPTIMAL + // I think it might be a good idea to allow the user to change asset::ICPUImage's initialLayout and have the asset converter + // do the layout transition for them. + transitionImageLayout(core::smart_refctd_ptr(inImageGPU), asset::IImage::EL_SHADER_READ_ONLY_OPTIMAL); + } - // just use the asset converter to make the image view as well - auto* uploadQueue = m_parentApp->getTransferUpQueue(); - smart_refctd_ptr inImageView; - { - // intialize command buffers - constexpr auto MultiBuffering = 2; - std::array, MultiBuffering> commandBuffers; - std::array commandBufferInfos; - { - auto pool = device->createCommandPool(uploadQueue->getFamilyIndex(), IGPUCommandPool::CREATE_FLAGS::RESET_COMMAND_BUFFER_BIT); - pool->createCommandBuffers(IGPUCommandPool::BUFFER_LEVEL::PRIMARY,{commandBuffers.data(),MultiBuffering},smart_refctd_ptr(logger)); - // - for (uint32_t i = 0u; i < MultiBuffering; ++i) - { - commandBuffers[i]->setObjectDebugName(("Command Buffer #" + std::to_string(i)).c_str()); - commandBufferInfos[i].cmdbuf = commandBuffers[i].get(); - } - } + core::smart_refctd_ptr outImageGPU = nullptr; + { + video::IGPUImage::SCreationParams creationParams = {}; + creationParams.flags = video::IGPUImage::ECF_MUTABLE_FORMAT_BIT; + creationParams.type = inImageGPU->getCreationParameters().type; + creationParams.format = outImageFormat; + creationParams.extent = { m_outImageDim.x, m_outImageDim.y, m_outImageDim.z }; + creationParams.mipLevels = m_inImage->getCreationParameters().mipLevels; // Asset converter will make the mip levels 10 for inImage, so use the original value of m_inImage + creationParams.arrayLayers = layerCount; + creationParams.samples = video::IGPUImage::ESCF_1_BIT; + creationParams.tiling = video::IGPUImage::ET_OPTIMAL; + creationParams.usage = static_cast(video::IGPUImage::EUF_STORAGE_BIT | video::IGPUImage::EUF_TRANSFER_SRC_BIT | video::IGPUImage::EUF_SAMPLED_BIT); + + outImageGPU = m_parentApp->m_device->createImage(std::move(creationParams)); + auto memReqs = outImageGPU->getMemoryReqs(); + memReqs.memoryTypeBits &= m_parentApp->m_device->getPhysicalDevice()->getDeviceLocalMemoryTypeBits(); + m_parentApp->m_device->allocate(memReqs, outImageGPU.get()); + + transitionImageLayout(core::smart_refctd_ptr(outImageGPU), asset::IImage::EL_GENERAL); + } - // test creation of image views, also it seems more ergonomic this way - smart_refctd_ptr cpuImageView; - { - ICPUImageView::SCreationParams params = {}; - params.image = m_inImage; - params.viewType = viewType; - params.format = inImageFormat; - cpuImageView = ICPUImageView::create(std::move(params)); - } - video::IGPUImageView::SCreationParams creationParams = {}; + // Create resources needed to do the blit + auto blitFilter = video::CComputeBlit::create(core::smart_refctd_ptr(m_parentApp->m_device)); - // We don't want to generate mip-maps for these images, because compute blitting is what does it and we want to test it here! - struct SInputs final : CAssetConverter::SInputs - { - inline uint8_t getMipLevelCount(const size_t groupCopyID, const ICPUImage* image, const CAssetConverter::patch_t& patch) const override - { - return 1; - } - inline uint16_t needToRecomputeMips(const size_t groupCopyID, const ICPUImage* image, const CAssetConverter::patch_t& patch) const override - { - return 0b0u; - } - } inputs = {}; - inputs.readCache = converter.get(); - inputs.logger = logger; - std::get>(inputs.assets) = { &cpuImageView.get(),1 }; - auto reservation = converter->reserve(inputs); - // the `.value` is just a funny way to make the `smart_refctd_ptr` copyable - inImageView = reservation.getGPUObjects().front().value; - if (!inImageView) - { - logger->log("Cannot create a GPU image for the input CPU images!",ILogger::ELL_ERROR); - return false; - } - inImageView->setObjectDebugName((m_outputName+" Input View").c_str()); + const asset::E_FORMAT outImageViewFormat = blitFilter->getOutImageViewFormat(outImageFormat); - // scratch command buffers for asset converter transfer commands - SIntendedSubmitInfo transfer = - { - .queue = uploadQueue, - .waitSemaphores = {}, - .prevCommandBuffers = {}, - .scratchCommandBuffers = commandBufferInfos, - .scratchSemaphore = { - .semaphore = semaphore.get(), - .value = 0, - // because of layout transitions - .stageMask = PIPELINE_STAGE_FLAGS::ALL_COMMANDS_BITS - } - }; - // as per the `SIntendedSubmitInfo` one commandbuffer must be begun - commandBuffers[0]->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT); - // make sure we - struct SConvertParams : CAssetConverter::SConvertParams - { - inline uint32_t getFinalOwnerQueueFamily(const IGPUImage* image, const core::blake3_hash_t& createdFrom, const uint8_t mipLevel) override - { - return m_finalFamily; - } - // keep the default final layout deduction rules - - uint32_t m_finalFamily; - }; - SConvertParams params = {}; - params.transfer = &transfer; - params.utilities = utils; - params.m_finalFamily = computeQueue->getFamilyIndex(); - auto result = reservation.convert(params); - if (!result.blocking() && result.copy()!=IQueue::RESULT::SUCCESS) - { - logger->log("Failed to upload CPU image data to GPU!",ILogger::ELL_ERROR); - return false; - } - } + const auto layersToBlit = layerCount; + core::smart_refctd_ptr inImageView = nullptr; + core::smart_refctd_ptr outImageView = nullptr; + { + video::IGPUImageView::SCreationParams creationParams = {}; + creationParams.image = inImageGPU; + creationParams.viewType = getImageViewTypeFromImageType_GPU(inImageGPU->getCreationParameters().type); + creationParams.format = inImageGPU->getCreationParameters().format; + creationParams.subresourceRange.aspectMask = video::IGPUImage::EAF_COLOR_BIT; + creationParams.subresourceRange.baseMipLevel = 0; + creationParams.subresourceRange.levelCount = 1; + creationParams.subresourceRange.baseArrayLayer = 0; + creationParams.subresourceRange.layerCount = layersToBlit; + + video::IGPUImageView::SCreationParams outCreationParams = creationParams; + outCreationParams.image = outImageGPU; + outCreationParams.format = outImageViewFormat; + + inImageView = m_parentApp->m_device->createImageView(std::move(creationParams)); + outImageView = m_parentApp->m_device->createImageView(std::move(outCreationParams)); + } + core::smart_refctd_ptr normalizationInImageView = outImageView; + core::smart_refctd_ptr normalizationInImage = outImageGPU; + auto normalizationInFormat = outImageFormat; + if (m_alphaSemantic == IBlitUtilities::EAS_REFERENCE_OR_COVERAGE) + { + normalizationInFormat = video::CComputeBlit::getCoverageAdjustmentIntermediateFormat(outImageFormat); - // create the outputs - uint32_t normalizationScratchSize = 0; - smart_refctd_ptr outImageView, intermediateAlphaView; - { - const auto outImageViewFormat = blitFilter->getOutputViewFormat(outImageFormat); - if (outImageViewFormat==EF_UNKNOWN) - { - logger->log("Cannot encode into this format, even manually!",ILogger::ELL_ERROR); - return false; - } - const bool manualEncoding = outImageViewFormat!=outImageFormat; + if (normalizationInFormat != outImageFormat) + { + video::IGPUImage::SCreationParams creationParams; + creationParams = outImageGPU->getCreationParameters(); + creationParams.format = normalizationInFormat; + creationParams.usage = static_cast(video::IGPUImage::EUF_STORAGE_BIT | video::IGPUImage::EUF_SAMPLED_BIT); + normalizationInImage = m_parentApp->m_device->createImage(std::move(creationParams)); + auto memReqs = normalizationInImage->getMemoryReqs(); + memReqs.memoryTypeBits &= m_parentApp->m_device->getPhysicalDevice()->getDeviceLocalMemoryTypeBits(); + m_parentApp->m_device->allocate(memReqs, normalizationInImage.get()); + transitionImageLayout(core::smart_refctd_ptr(normalizationInImage), asset::IImage::EL_GENERAL); // First we do the blit which requires storage image so starting layout is GENERAL + + video::IGPUImageView::SCreationParams viewCreationParams = {}; + viewCreationParams.image = normalizationInImage; + viewCreationParams.viewType = getImageViewTypeFromImageType_GPU(inImageGPU->getCreationParameters().type); + viewCreationParams.format = normalizationInImage->getCreationParameters().format; + viewCreationParams.subresourceRange.aspectMask = video::IGPUImage::EAF_COLOR_BIT; + viewCreationParams.subresourceRange.baseMipLevel = 0; + viewCreationParams.subresourceRange.levelCount = 1; + viewCreationParams.subresourceRange.baseArrayLayer = 0; + viewCreationParams.subresourceRange.layerCount = layersToBlit; + + normalizationInImageView = m_parentApp->m_device->createImageView(std::move(viewCreationParams)); + } + } - smart_refctd_ptr outImage; - { - IGPUImage::SCreationParams creationParams = {}; - creationParams.type = type; - creationParams.samples = IGPUImage::E_SAMPLE_COUNT_FLAGS::ESCF_1_BIT; - creationParams.format = outImageFormat; - creationParams.extent = { m_outImageDim.x, m_outImageDim.y, m_outImageDim.z }; - creationParams.mipLevels = inCreationParams.mipLevels; - creationParams.arrayLayers = layerCount; - if (manualEncoding) - creationParams.flags = core::bitflag(IGPUImage::ECF_MUTABLE_FORMAT_BIT)|IGPUImage::ECF_EXTENDED_USAGE_BIT; - creationParams.usage = IGPUImage::EUF_STORAGE_BIT|video::IGPUImage::EUF_TRANSFER_SRC_BIT; - creationParams.viewFormats.set(outImageFormat,true); - creationParams.viewFormats.set(outImageViewFormat,true); - outImage = device->createImage(std::move(creationParams)); - if (!outImage || !device->allocate(outImage->getMemoryReqs(),outImage.get()).isValid()) - { - logger->log("Failed to create output GPU image!",ILogger::ELL_ERROR); - return false; - } - outImage->setObjectDebugName((m_outputName + " Output").c_str()); - } + const core::vectorSIMDu32 inExtent(inImageGPU->getCreationParameters().extent.width, inImageGPU->getCreationParameters().extent.height, inImageGPU->getCreationParameters().extent.depth, 1); + const auto inImageType = inImageGPU->getCreationParameters().type; - IGPUImageView::SCreationParams params = {}; - params.image = std::move(outImage); - params.viewType = viewType; - params.format = outImageViewFormat; - outImageView = device->createImageView(std::move(params)); - - if (m_alphaSemantic==IBlitUtilities::EAS_REFERENCE_OR_COVERAGE) - { - const auto format = CComputeBlit::getCoverageAdjustmentIntermediateFormat(outImageFormat); - - IGPUImage::SCreationParams creationParams = {}; - creationParams = outImageView->getCreationParameters().image->getCreationParameters(); - creationParams.format = format; - creationParams.usage = IGPUImage::EUF_STORAGE_BIT; - creationParams.viewFormats.reset(); - creationParams.viewFormats.set(format,true); - auto image = device->createImage(std::move(creationParams)); - if (!image || !device->allocate(image->getMemoryReqs(), image.get()).isValid()) - { - logger->log("Failed to create intermediate alpha GPU image!",ILogger::ELL_ERROR); - return false; - } - - IGPUImageView::SCreationParams viewCreationParams = {}; - viewCreationParams.image = std::move(image); - viewCreationParams.viewType = outImageView->getCreationParameters().viewType; - viewCreationParams.format = format; - intermediateAlphaView = device->createImageView(std::move(viewCreationParams)); - - normalizationScratchSize = core::roundUp( - CComputeBlit::getNormalizationByteSize(pipelines,format,layerCount), - device->getPhysicalDevice()->getLimits().bufferViewAlignment - ); - } - } + // create scratch buffer + core::smart_refctd_ptr coverageAdjustmentScratchBuffer = nullptr; + { + const size_t scratchSize = blitFilter->getCoverageAdjustmentScratchSize(m_alphaSemantic, inImageType, m_alphaBinCount, layersToBlit); + if (scratchSize > 0) + { + video::IGPUBuffer::SCreationParams creationParams = {}; + creationParams.size = scratchSize; + creationParams.usage = static_cast(video::IGPUBuffer::EUF_TRANSFER_DST_BIT | video::IGPUBuffer::EUF_STORAGE_BUFFER_BIT); + + coverageAdjustmentScratchBuffer = m_parentApp->m_device->createBuffer(std::move(creationParams)); + auto memReqs = coverageAdjustmentScratchBuffer->getMemoryReqs(); + memReqs.memoryTypeBits &= m_parentApp->m_physicalDevice->getDeviceLocalMemoryTypeBits(); + m_parentApp->m_device->allocate(memReqs, coverageAdjustmentScratchBuffer.get()); + + asset::SBufferRange bufferRange = {}; + bufferRange.offset = 0ull; + bufferRange.size = coverageAdjustmentScratchBuffer->getSize(); + bufferRange.buffer = coverageAdjustmentScratchBuffer; + + core::vector fillValues(scratchSize / sizeof(uint32_t), 0u); + m_parentApp->utilities->updateBufferRangeViaStagingBufferAutoSubmit(bufferRange, fillValues.data(), m_parentApp->queue); + } + } - const hlsl::uint32_t3 inExtent(inCreationParams.extent.width,inCreationParams.extent.height,inCreationParams.extent.depth); - - // create scaledKernelPhasedLUT and its view - smart_refctd_ptr scratchAndScaledKernelPhasedLUT; - smart_refctd_ptr scaledKernelPhasedLUTView; - { - const auto lutOffset = normalizationScratchSize; - const auto lutSize = blit_utils_t::getScaledKernelPhasedLUTSize(inExtent,m_outImageDim,type,m_convolutionKernels); + // create scaledKernelPhasedLUT and its view + core::smart_refctd_ptr scaledKernelPhasedLUTView = nullptr; + { + const auto lutSize = blit_utils_t::getScaledKernelPhasedLUTSize(inExtent, m_outImageDim, inImageType, m_convolutionKernels); - // TODO: repack & use R and RG formats if we can - auto lutMemory = std::make_unique(lutSize); - if (!blit_utils_t::computeScaledKernelPhasedLUT(lutMemory.get(),inExtent,m_outImageDim,type,m_convolutionKernels)) - { - logger->log("Failed to compute scaled kernel phased LUT for the GPU case!",ILogger::ELL_ERROR); - return false; - } + uint8_t* lutMemory = reinterpret_cast(_NBL_ALIGNED_MALLOC(lutSize, 32)); + if (!blit_utils_t::computeScaledKernelPhasedLUT(lutMemory, inExtent, m_outImageDim, inImageType, m_convolutionKernels)) + { + m_parentApp->m_logger->log("Failed to compute scaled kernel phased LUT for the GPU case!", system::ILogger::ELL_ERROR); + return false; + } - IGPUBuffer::SCreationParams creationParams = {}; - // `samplerBuffer`, lut upload and scratch clear command, BDA - creationParams.usage = IGPUBuffer::EUF_UNIFORM_TEXEL_BUFFER_BIT|IGPUBuffer::EUF_TRANSFER_DST_BIT|IGPUBuffer::EUF_SHADER_DEVICE_ADDRESS_BIT; - creationParams.size = normalizationScratchSize+lutSize; - scratchAndScaledKernelPhasedLUT = device->createBuffer(std::move(creationParams)); - if (!device->allocate(scratchAndScaledKernelPhasedLUT->getMemoryReqs(),scratchAndScaledKernelPhasedLUT.get(),IDeviceMemoryAllocation::EMAF_DEVICE_ADDRESS_BIT).isValid()) - { - logger->log("Failed to create the Phase LUT and coverage buffer!",ILogger::ELL_ERROR); - return false; - } + video::IGPUBuffer::SCreationParams creationParams = {}; + creationParams.usage = static_cast(video::IGPUBuffer::EUF_STORAGE_BUFFER_BIT | video::IGPUBuffer::EUF_UNIFORM_TEXEL_BUFFER_BIT | video::IGPUBuffer::EUF_TRANSFER_DST_BIT); + creationParams.size = lutSize; + auto scaledKernelPhasedLUT = m_parentApp->m_device->createBuffer(std::move(creationParams)); + auto memReqs = scaledKernelPhasedLUT->getMemoryReqs(); + memReqs.memoryTypeBits &= m_parentApp->m_physicalDevice->getDeviceLocalMemoryTypeBits(); + m_parentApp->m_device->allocate(memReqs, scaledKernelPhasedLUT.get()); + + // fill it up with data + asset::SBufferRange bufferRange = {}; + bufferRange.offset = 0ull; + bufferRange.size = lutSize; + bufferRange.buffer = scaledKernelPhasedLUT; + m_parentApp->utilities->updateBufferRangeViaStagingBufferAutoSubmit(bufferRange, lutMemory, m_parentApp->queue); + + asset::E_FORMAT bufferViewFormat; + if constexpr (std::is_same_v) + bufferViewFormat = asset::EF_R16G16B16A16_SFLOAT; + else if constexpr (std::is_same_v) + bufferViewFormat = asset::EF_R32G32B32A32_SFLOAT; + else + assert(false); - // fill it up with data - SBufferRange bufferRange = {}; - bufferRange.offset = lutOffset; - bufferRange.size = lutSize; - bufferRange.buffer = scratchAndScaledKernelPhasedLUT; - { - // "wrong" queue just so that we don't need to do ownership transfers - SIntendedSubmitInfo intended = {.queue=computeQueue}; - auto transferred = utils->autoSubmit(intended,[&](auto& info)->bool - { - return utils->updateBufferRangeViaStagingBuffer(info,bufferRange,lutMemory.get()); - } - ); - if (transferred.copy()!=IQueue::RESULT::SUCCESS) - { - logger->log("Failed to upload Convolution Weights to GPU!",ILogger::ELL_ERROR); - return false; - } - } + scaledKernelPhasedLUTView = m_parentApp->m_device->createBufferView(scaledKernelPhasedLUT.get(), bufferViewFormat, 0ull, scaledKernelPhasedLUT->getSize()); - E_FORMAT bufferViewFormat; - if constexpr (std::is_same_v) - bufferViewFormat = asset::EF_R16G16B16A16_SFLOAT; - else if constexpr (std::is_same_v) - bufferViewFormat = asset::EF_R32G32B32A32_SFLOAT; - else - { - assert(false); - } - scaledKernelPhasedLUTView = device->createBufferView(bufferRange,bufferViewFormat); - } + _NBL_ALIGNED_FREE(lutMemory); + } - // will need this later - auto layout = pipelines.blit->getLayout(); - assert(pipelines.coverage->getLayout()==layout); + auto blitDSLayout = blitFilter->getDefaultBlitDescriptorSetLayout(m_alphaSemantic); + auto kernelWeightsDSLayout = blitFilter->getDefaultKernelWeightsDescriptorSetLayout(); + auto blitPipelineLayout = blitFilter->getDefaultBlitPipelineLayout(m_alphaSemantic); - smart_refctd_ptr ds; - { - auto descriptorPool = device->createDescriptorPoolForDSLayouts(IDescriptorPool::ECF_UPDATE_AFTER_BIND_BIT,layout->getDescriptorSetLayouts()); - ds = descriptorPool->createDescriptorSet(smart_refctd_ptr(layout->getDescriptorSetLayout(0))); - } + video::IGPUDescriptorSetLayout* blitDSLayouts_raw[] = { blitDSLayout.get(), kernelWeightsDSLayout.get() }; + uint32_t dsCounts[] = { 2, 1 }; + auto descriptorPool = m_parentApp->m_device->createDescriptorPoolForDSLayouts(video::IDescriptorPool::ECF_NONE, blitDSLayouts_raw, blitDSLayouts_raw + 2ull, dsCounts); - using layout_t = IGPUImage::LAYOUT; - { - constexpr auto WriteCount = 5u; - IGPUDescriptorSet::SDescriptorInfo infos[WriteCount]; - IGPUDescriptorSet::SWriteDescriptorSet writes[WriteCount]; - for (auto i=0u; icreateSampler({ - .TextureWrapU = wrap_t::ETC_CLAMP_TO_EDGE, - .TextureWrapV = wrap_t::ETC_CLAMP_TO_EDGE, - .TextureWrapW = wrap_t::ETC_CLAMP_TO_EDGE, - .BorderColor = IGPUSampler::E_TEXTURE_BORDER_COLOR::ETBC_FLOAT_OPAQUE_BLACK - }); - writes[3].binding = outputBinding.binding; - infos[3].desc = core::smart_refctd_ptr(outImageView); - infos[3].info.image.imageLayout = layout_t::GENERAL; - std::span writeSpan; - if (intermediateAlphaView) - { - writes[4].binding = outputBinding.binding; - writes[4].arrayElement = 1; - infos[4].desc = core::smart_refctd_ptr(intermediateAlphaView); - infos[4].info.image.imageLayout = layout_t::GENERAL; - writeSpan = writes; - } - else - writeSpan = {writes,WriteCount-1}; - device->updateDescriptorSets(writeSpan,{}); - } + core::smart_refctd_ptr blitPipeline = nullptr; + core::smart_refctd_ptr blitDS = nullptr; + core::smart_refctd_ptr blitWeightsDS = nullptr; - { - smart_refctd_ptr cmdbuf; - auto pool = device->createCommandPool(computeQueue->getFamilyIndex(),IGPUCommandPool::CREATE_FLAGS::TRANSIENT_BIT); - pool->createCommandBuffers(IGPUCommandPool::BUFFER_LEVEL::PRIMARY,{&cmdbuf,1},smart_refctd_ptr(logger)); + core::smart_refctd_ptr alphaTestPipeline = nullptr; + core::smart_refctd_ptr normalizationPipeline = nullptr; + core::smart_refctd_ptr normalizationDS = nullptr; - struct SMemoryUsage - { - core::bitflag stageMask = PIPELINE_STAGE_FLAGS::NONE; - core::bitflag accessMask = ACCESS_FLAGS::NONE; - }; - - using buffer_barrier_t = IGPUCommandBuffer::SBufferMemoryBarrier; - using image_barrier_t = IGPUCommandBuffer::SImageMemoryBarrier; - auto imageBarrierFromView = [layerCount]( - const auto& imageView, const SMemoryUsage& src, const SMemoryUsage& dst, - const layout_t oldLayout=layout_t::UNDEFINED, const layout_t newLayout=layout_t::UNDEFINED, - const uint32_t acquireFromFamilyIndex=IQueue::FamilyIgnored - )->image_barrier_t - { - if (!imageView) - return {}; - return image_barrier_t{ - .barrier = { - .dep = { - .srcStageMask = src.stageMask, - .srcAccessMask = src.accessMask, - .dstStageMask = dst.stageMask, - .dstAccessMask = dst.accessMask - }, - .ownershipOp = IGPUCommandBuffer::SOwnershipTransferBarrier::OWNERSHIP_OP::ACQUIRE, - .otherQueueFamilyIndex = acquireFromFamilyIndex - }, - .image = imageView->getCreationParameters().image.get(), - // whole image view - //.subresourceRange = imageView->getCreationParameters().subresourceRange, - // https://github.com/KhronosGroup/Vulkan-ValidationLayers/issues/8823 - .subresourceRange = { - .aspectMask = IGPUImage::E_ASPECT_FLAGS::EAF_COLOR_BIT, - .baseMipLevel = 0, - .levelCount = 1, - .baseArrayLayer = 0, - .layerCount = layerCount - }, - .oldLayout = oldLayout, - .newLayout = newLayout - }; - }; - - cmdbuf->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT); - // if doing coverage, clear the buffer to 0 - if (normalizationScratchSize) - cmdbuf->fillBuffer({.offset=0,.size=normalizationScratchSize,.buffer=scratchAndScaledKernelPhasedLUT},0); - // Acquire ownership of input and split layout transition from transfer - { - const buffer_barrier_t bufBarrier = { - .barrier = { - .dep = { - .srcStageMask = PIPELINE_STAGE_FLAGS::CLEAR_BIT|PIPELINE_STAGE_FLAGS::COPY_BIT, - .srcAccessMask = ACCESS_FLAGS::TRANSFER_WRITE_BIT, - .dstStageMask = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT, - .dstAccessMask = ACCESS_FLAGS::SHADER_READ_BITS|ACCESS_FLAGS::SHADER_WRITE_BITS - } // no ownership transfers, etc. - }, - // whole buffer because we transferred the contents into it - .range = {.offset=0,.size=scratchAndScaledKernelPhasedLUT->getSize(),.buffer=scratchAndScaledKernelPhasedLUT} - }; - // we're synchronised by a semaphore signal op or first usage, no stages or masks needed - const SMemoryUsage src = {PIPELINE_STAGE_FLAGS::NONE,ACCESS_FLAGS::NONE}; - const SMemoryUsage dstWrite = {PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT,ACCESS_FLAGS::SHADER_WRITE_BITS}; - // split transition during an ownership transfer, needs to match - const bool splitLayoutXsition = computeQueue->getFamilyIndex()!=uploadQueue->getFamilyIndex(); - const SMemoryUsage dstRead = {PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT,ACCESS_FLAGS::SAMPLED_READ_BIT}; - const image_barrier_t imgBarriers[] = { - imageBarrierFromView( - inImageView,src,dstRead, - splitLayoutXsition ? layout_t::TRANSFER_DST_OPTIMAL:layout_t::UNDEFINED, - splitLayoutXsition ? layout_t::READ_ONLY_OPTIMAL:layout_t::UNDEFINED, - uploadQueue->getFamilyIndex() - ), - imageBarrierFromView(outImageView,src,dstWrite,layout_t::UNDEFINED,layout_t::GENERAL), - imageBarrierFromView(intermediateAlphaView,src,dstWrite,layout_t::UNDEFINED,layout_t::GENERAL) - }; - cmdbuf->pipelineBarrier(E_DEPENDENCY_FLAGS::EDF_NONE,{ - .memBarriers = {}, - .bufBarriers = {&bufBarrier,1}, - .imgBarriers = {imgBarriers,intermediateAlphaView ? 3ull:2ull} - }); - } - cmdbuf->bindDescriptorSets(E_PIPELINE_BIND_POINT::EPBP_COMPUTE,layout,0,1,&ds.get()); - cmdbuf->bindComputePipeline(pipelines.blit.get()); - { - const hlsl::uint16_t3 outExtent16(m_outImageDim); - const hlsl::blit::Parameters params = { - .perWG = CComputeBlit::computePerWorkGroup(pipelines.sharedMemorySize,m_convolutionKernels,type,hlsl::uint16_t3(inExtent),outExtent16), - .inputDescIx = 0, - .samplerDescIx = 0, - .unused0 = 0, - .outputDescIx = 0 - }; - if (!params) - { - logger->log("Failed to fit the preload region in shared memory even for 1x1x1 workgroup!",ILogger::ELL_ERROR); - return false; - } - cmdbuf->pushConstants(layout,hlsl::ShaderStage::ESS_COMPUTE,0,sizeof(params),¶ms); - cmdbuf->dispatch(params.perWG.getWorkgroupCount(outExtent16)); - if (m_alphaSemantic==IBlitUtilities::EAS_REFERENCE_OR_COVERAGE) - { - // alpha histogram, color output and intermediate alpha - { - const buffer_barrier_t bufBarrier = { - .barrier = { - .dep = { - .srcStageMask = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT, - .srcAccessMask = ACCESS_FLAGS::SHADER_READ_BITS|ACCESS_FLAGS::SHADER_WRITE_BITS, - .dstStageMask = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT, - .dstAccessMask = ACCESS_FLAGS::SHADER_READ_BITS - } // no ownership transfers, etc. - }, - .range = {.offset=0,.size=normalizationScratchSize,.buffer=scratchAndScaledKernelPhasedLUT} - }; - const SMemoryUsage src = {PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT,ACCESS_FLAGS::SHADER_WRITE_BITS}; - const SMemoryUsage dst = {PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT,ACCESS_FLAGS::SHADER_READ_BITS}; - const image_barrier_t imgBarriers[] = { - imageBarrierFromView(outImageView,src,dst), - imageBarrierFromView(intermediateAlphaView,src,dst) - }; - cmdbuf->pipelineBarrier(E_DEPENDENCY_FLAGS::EDF_NONE,{ - .memBarriers = {}, - .bufBarriers = {&bufBarrier,normalizationScratchSize ? 1ull:0ull}, - .imgBarriers = {imgBarriers,intermediateAlphaView ? 2ull:1ull} - }); - } - cmdbuf->bindComputePipeline(pipelines.coverage.get()); - // cmdbuf->pushConstants(); - // cmdbuf->dispatch(); - } - } - cmdbuf->end(); + if (m_alphaSemantic == IBlitUtilities::EAS_REFERENCE_OR_COVERAGE) + { + alphaTestPipeline = blitFilter->getAlphaTestPipeline(m_alphaBinCount, inImageType); + normalizationPipeline = blitFilter->getNormalizationPipeline(normalizationInImage->getCreationParameters().type, outImageFormat, m_alphaBinCount); - { - // I can do this because I've already awaited the semaphore on host and I have no pending signals - const auto semaphoreValue = semaphore->getCounterValue(); - const IQueue::SSubmitInfo::SSemaphoreInfo waitSemaphores[1] = {{ - .semaphore = semaphore.get(), - .value = semaphoreValue, - .stageMask = PIPELINE_STAGE_FLAGS::ALL_COMMANDS_BITS - }}; - const IQueue::SSubmitInfo::SCommandBufferInfo cmbBufInfos[1] = {{.cmdbuf=cmdbuf.get()}}; - const IQueue::SSubmitInfo::SSemaphoreInfo signalSemaphores[1] = {{ - .semaphore = semaphore.get(), - .value = semaphoreValue+1, - .stageMask = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT - }}; - const IQueue::SSubmitInfo info = { - .waitSemaphores = waitSemaphores, - .commandBuffers = cmbBufInfos, - .signalSemaphores = signalSemaphores - }; - computeQueue->submit({&info,1}); - // wait right away because we want to start using the downloaded data - { - const ISemaphore::SWaitInfo waitInfos[] = {{.semaphore=signalSemaphores->semaphore,.value=signalSemaphores->value}}; - device->blockForSemaphores(waitInfos); - } - } - } -#if 0 + normalizationDS = descriptorPool->createDescriptorSet(core::smart_refctd_ptr(blitDSLayout)); + blitFilter->updateDescriptorSet(normalizationDS.get(), nullptr, normalizationInImageView, outImageView, coverageAdjustmentScratchBuffer, nullptr); + } + + blitPipeline = blitFilter->getBlitPipeline(outImageFormat, inImageType, inExtent, m_outImageDim, m_alphaSemantic, m_convolutionKernels, BlitWorkgroupSize, m_alphaBinCount); + blitDS = descriptorPool->createDescriptorSet(core::smart_refctd_ptr(blitDSLayout)); + blitWeightsDS = descriptorPool->createDescriptorSet(core::smart_refctd_ptr(kernelWeightsDSLayout)); + + blitFilter->updateDescriptorSet(blitDS.get(), blitWeightsDS.get(), inImageView, normalizationInImageView, coverageAdjustmentScratchBuffer, scaledKernelPhasedLUTView); + + m_parentApp->m_logger->log("GPU begin.."); + m_parentApp->m_api->startCapture(); + blitFilter->blit( + m_parentApp->queue, m_alphaSemantic, + blitDS.get(), alphaTestPipeline.get(), + blitDS.get(), blitWeightsDS.get(), blitPipeline.get(), + normalizationDS.get(), normalizationPipeline.get(), + inExtent, inImageType, inImageFormat, normalizationInImage, m_convolutionKernels, + layersToBlit, + coverageAdjustmentScratchBuffer, m_referenceAlpha, + m_alphaBinCount, BlitWorkgroupSize); + m_parentApp->m_api->endCapture(); + m_parentApp->m_logger->log("GPU end.."); + + if (m_alphaSemantic == IBlitUtilities::EAS_REFERENCE_OR_COVERAGE) + if (outImageGPU->getCreationParameters().type == asset::IImage::ET_2D) // TODO: why alpha coverage only for 2D ? + { + if (layerCount > 1) + { + // This can be removed once ext::ScreenShot::createScreenShot works for multiple layers. + m_parentApp->m_logger->log("Layer count (%d) is greater than 1 for a 2D image, not calculating GPU alpha coverage..", system::ILogger::ELL_WARNING, layerCount); + } + else + { auto outCPUImageView = ext::ScreenShot::createScreenShot( - device.get(), - m_parentApp->getTransferQueue(), + m_parentApp->m_device.get(), + m_parentApp->queue, nullptr, outImageView.get(), asset::EAF_NONE, - IImage::EL_GENERAL - ); + asset::IImage::EL_GENERAL); - // TODO: also save the gpu image to disk! - - logger.log("GPU alpha coverage: %f", system::ILogger::ELL_DEBUG, computeAlphaCoverage(m_referenceAlpha, outCPUImageView->getCreationParameters().image.get())); + m_parentApp->m_logger->log("GPU alpha coverage: %f", system::ILogger::ELL_DEBUG, computeAlphaCoverage(m_referenceAlpha, outCPUImageView->getCreationParameters().image.get())); + } + } - // download results to check - { - const size_t downloadSize = gpuOutput.size(); + // download results to check + { + const size_t downloadSize = gpuOutput.size(); - video::IGPUBuffer::SCreationParams creationParams = {}; - creationParams.usage = video::IGPUBuffer::EUF_TRANSFER_DST_BIT; - creationParams.size = downloadSize; - core::smart_refctd_ptr downloadBuffer = m_parentApp->m_device->createBuffer(std::move(creationParams)); + video::IGPUBuffer::SCreationParams creationParams = {}; + creationParams.usage = video::IGPUBuffer::EUF_TRANSFER_DST_BIT; + creationParams.size = downloadSize; + core::smart_refctd_ptr downloadBuffer = m_parentApp->m_device->createBuffer(std::move(creationParams)); - auto memReqs = downloadBuffer->getMemoryReqs(); - memReqs.memoryTypeBits &= m_parentApp->m_physicalDevice->getDownStreamingMemoryTypeBits(); - m_parentApp->m_device->allocate(memReqs, downloadBuffer.get()); + auto memReqs = downloadBuffer->getMemoryReqs(); + memReqs.memoryTypeBits &= m_parentApp->m_physicalDevice->getDownStreamingMemoryTypeBits(); + m_parentApp->m_device->allocate(memReqs, downloadBuffer.get()); - core::smart_refctd_ptr cmdbuf = nullptr; - m_parentApp->m_device->createCommandBuffers(m_parentApp->commandPool.get(), video::IGPUCommandBuffer::EL_PRIMARY, 1u, &cmdbuf); - auto fence = m_parentApp->m_device->createFence(video::IGPUFence::ECF_UNSIGNALED); + core::smart_refctd_ptr cmdbuf = nullptr; + m_parentApp->m_device->createCommandBuffers(m_parentApp->commandPool.get(), video::IGPUCommandBuffer::EL_PRIMARY, 1u, &cmdbuf); + auto fence = m_parentApp->m_device->createFence(video::IGPUFence::ECF_UNSIGNALED); - cmdbuf->begin(video::IGPUCommandBuffer::EU_ONE_TIME_SUBMIT_BIT); + cmdbuf->begin(video::IGPUCommandBuffer::EU_ONE_TIME_SUBMIT_BIT); - asset::ICPUImage::SBufferCopy downloadRegion = {}; - downloadRegion.imageSubresource.aspectMask = video::IGPUImage::EAF_COLOR_BIT; - downloadRegion.imageSubresource.layerCount = layerCount; - downloadRegion.imageExtent = outImageGPU->getCreationParameters().extent; + asset::ICPUImage::SBufferCopy downloadRegion = {}; + downloadRegion.imageSubresource.aspectMask = video::IGPUImage::EAF_COLOR_BIT; + downloadRegion.imageSubresource.layerCount = layerCount; + downloadRegion.imageExtent = outImageGPU->getCreationParameters().extent; - // Todo(achal): Transition layout to TRANSFER_SRC_OPTIMAL - cmdbuf->copyImageToBuffer(outImageGPU.get(), asset::IImage::EL_GENERAL, downloadBuffer.get(), 1u, &downloadRegion); + // Todo(achal): Transition layout to TRANSFER_SRC_OPTIMAL + cmdbuf->copyImageToBuffer(outImageGPU.get(), asset::IImage::EL_GENERAL, downloadBuffer.get(), 1u, &downloadRegion); - cmdbuf->end(); + cmdbuf->end(); - video::IGPUQueue::SSubmitInfo submitInfo = {}; - submitInfo.commandBufferCount = 1u; - submitInfo.commandBuffers = &cmdbuf.get(); - m_parentApp->queue->submit(1u, &submitInfo, fence.get()); + video::IGPUQueue::SSubmitInfo submitInfo = {}; + submitInfo.commandBufferCount = 1u; + submitInfo.commandBuffers = &cmdbuf.get(); + m_parentApp->queue->submit(1u, &submitInfo, fence.get()); - m_parentApp->m_device->blockForFences(1u, &fence.get()); + m_parentApp->m_device->blockForFences(1u, &fence.get()); - video::IDeviceMemoryAllocation::MappedMemoryRange memoryRange = {}; - memoryRange.memory = downloadBuffer->getBoundMemory(); - memoryRange.length = downloadSize; - uint8_t* mappedGPUData = reinterpret_cast(m_parentApp->m_device->mapMemory(memoryRange)); + video::IDeviceMemoryAllocation::MappedMemoryRange memoryRange = {}; + memoryRange.memory = downloadBuffer->getBoundMemory(); + memoryRange.length = downloadSize; + uint8_t* mappedGPUData = reinterpret_cast(m_parentApp->m_device->mapMemory(memoryRange)); - memcpy(gpuOutput.data(), mappedGPUData, gpuOutput.size()); - m_parentApp->m_device->unmapMemory(downloadBuffer->getBoundMemory()); - } -#endif - } + memcpy(gpuOutput.data(), mappedGPUData, gpuOutput.size()); + m_parentApp->m_device->unmapMemory(downloadBuffer->getBoundMemory()); - m_parentApp->m_api->endCapture(); -#if 0 -// core::vector gpuOutput(static_cast(m_outImageDim[0]) * m_outImageDim[1] * m_outImageDim[2] * asset::getTexelOrBlockBytesize(outImageFormat) * layerCount); + // TODO: also save the gpu image to disk! + } + } - assert(gpuOutput.size() == cpuOutput.size()); + assert(gpuOutput.size() == cpuOutput.size()); - const uint32_t outChannelCount = asset::getFormatChannelCount(outImageFormat); + const uint32_t outChannelCount = asset::getFormatChannelCount(outImageFormat); - double sqErr = 0.0; - uint8_t* cpuBytePtr = cpuOutput.data(); - uint8_t* gpuBytePtr = gpuOutput.data(); - const auto layerSize = m_outImageDim[2] * m_outImageDim[1] * m_outImageDim[0] * asset::getTexelOrBlockBytesize(outImageFormat); + double sqErr = 0.0; + uint8_t* cpuBytePtr = cpuOutput.data(); + uint8_t* gpuBytePtr = gpuOutput.data(); + const auto layerSize = m_outImageDim[2] * m_outImageDim[1] * m_outImageDim[0] * asset::getTexelOrBlockBytesize(outImageFormat); - for (auto layer = 0; layer < layerCount; ++layer) + for (auto layer = 0; layer < layerCount; ++layer) + { + for (uint64_t k = 0u; k < m_outImageDim[2]; ++k) + { + for (uint64_t j = 0u; j < m_outImageDim[1]; ++j) { - for (uint64_t k = 0u; k < m_outImageDim[2]; ++k) + for (uint64_t i = 0; i < m_outImageDim[0]; ++i) { - for (uint64_t j = 0u; j < m_outImageDim[1]; ++j) + const uint64_t pixelIndex = (k * m_outImageDim[1] * m_outImageDim[0]) + (j * m_outImageDim[0]) + i; + core::vectorSIMDu32 dummy; + + const void* cpuEncodedPixel = cpuBytePtr + (layer * layerSize) + pixelIndex * asset::getTexelOrBlockBytesize(outImageFormat); + const void* gpuEncodedPixel = gpuBytePtr + (layer * layerSize) + pixelIndex * asset::getTexelOrBlockBytesize(outImageFormat); + + double cpuDecodedPixel[4]; + asset::decodePixelsRuntime(outImageFormat, &cpuEncodedPixel, cpuDecodedPixel, dummy.x, dummy.y); + + double gpuDecodedPixel[4]; + asset::decodePixelsRuntime(outImageFormat, &gpuEncodedPixel, gpuDecodedPixel, dummy.x, dummy.y); + + for (uint32_t ch = 0u; ch < outChannelCount; ++ch) { - for (uint64_t i = 0; i < m_outImageDim[0]; ++i) - { - const uint64_t pixelIndex = (k * m_outImageDim[1] * m_outImageDim[0]) + (j * m_outImageDim[0]) + i; - core::vectorSIMDu32 dummy; - - const void* cpuEncodedPixel = cpuBytePtr + (layer * layerSize) + pixelIndex * asset::getTexelOrBlockBytesize(outImageFormat); - const void* gpuEncodedPixel = gpuBytePtr + (layer * layerSize) + pixelIndex * asset::getTexelOrBlockBytesize(outImageFormat); - - double cpuDecodedPixel[4]; - asset::decodePixelsRuntime(outImageFormat, &cpuEncodedPixel, cpuDecodedPixel, dummy.x, dummy.y); - - double gpuDecodedPixel[4]; - asset::decodePixelsRuntime(outImageFormat, &gpuEncodedPixel, gpuDecodedPixel, dummy.x, dummy.y); - - for (uint32_t ch = 0u; ch < outChannelCount; ++ch) - { - // TODO: change to logs - #if 1 - if (std::isnan(cpuDecodedPixel[ch]) || std::isinf(cpuDecodedPixel[ch])) - __debugbreak(); - - if (std::isnan(gpuDecodedPixel[ch]) || std::isinf(gpuDecodedPixel[ch])) - __debugbreak(); - - const auto diff = std::abs(cpuDecodedPixel[ch]-gpuDecodedPixel[ch]) / core::max(core::max(core::abs(cpuDecodedPixel[ch]),core::abs(gpuDecodedPixel[ch])),exp2(-16.f)); - if (diff>0.01f) - __debugbreak(); - #endif - - sqErr += (cpuDecodedPixel[ch] - gpuDecodedPixel[ch]) * (cpuDecodedPixel[ch] - gpuDecodedPixel[ch]); - } - } + // TODO: change to logs +#if 1 + if (std::isnan(cpuDecodedPixel[ch]) || std::isinf(cpuDecodedPixel[ch])) + __debugbreak(); + + if (std::isnan(gpuDecodedPixel[ch]) || std::isinf(gpuDecodedPixel[ch])) + __debugbreak(); + + const auto diff = std::abs(cpuDecodedPixel[ch]-gpuDecodedPixel[ch]) / core::max(core::max(core::abs(cpuDecodedPixel[ch]),core::abs(gpuDecodedPixel[ch])),exp2(-16.f)); + if (diff>0.01f) + __debugbreak(); +#endif + + sqErr += (cpuDecodedPixel[ch] - gpuDecodedPixel[ch]) * (cpuDecodedPixel[ch] - gpuDecodedPixel[ch]); } } } + } + } - // compute alpha coverage - const uint64_t totalPixelCount = static_cast(m_outImageDim[2]) * m_outImageDim[1] * m_outImageDim[0] * layerCount; - const double RMSE = core::sqrt(sqErr / totalPixelCount); - m_parentApp->m_logger->log("RMSE: %f", system::ILogger::ELL_INFO, RMSE); + // compute alpha coverage + const uint64_t totalPixelCount = static_cast(m_outImageDim[2]) * m_outImageDim[1] * m_outImageDim[0] * layerCount; + const double RMSE = core::sqrt(sqErr / totalPixelCount); + m_parentApp->m_logger->log("RMSE: %f", system::ILogger::ELL_INFO, RMSE); - constexpr double MaxAllowedRMSE = 0.0046; // arbitrary + constexpr double MaxAllowedRMSE = 0.0046; // arbitrary - return (RMSE <= MaxAllowedRMSE) && !std::isnan(RMSE); -#endif - return true; - } + return (RMSE <= MaxAllowedRMSE) && !std::isnan(RMSE); + } - private: - const std::string m_outputName; - const typename blit_utils_t::convolution_kernels_t m_convolutionKernels; - const hlsl::uint32_t3 m_outImageDim; - const IBlitUtilities::E_ALPHA_SEMANTIC m_alphaSemantic; - const float m_referenceAlpha = 0.f; - const uint32_t m_alphaBinCount = asset::IBlitUtilities::DefaultAlphaBinCount; - }; + private: + const core::vectorSIMDu32 m_outImageDim; + const IBlitUtilities::E_ALPHA_SEMANTIC m_alphaSemantic; + const typename blit_utils_t::convolution_kernels_t m_convolutionKernels; + const float m_referenceAlpha = 0.f; + const uint32_t m_alphaBinCount = asset::IBlitUtilities::DefaultAlphaBinCount; + }; + + class CRegionBlockFunctorFilterTest : public ITest + { + public: + CRegionBlockFunctorFilterTest(core::smart_refctd_ptr&& inImage, BlitFilterTestApp* parentApp, const char* writeImagePath) + : ITest(std::move(inImage), parentApp), m_writeImagePath(writeImagePath) + {} - class CRegionBlockFunctorFilterTest : public ITest + bool run() override { - public: - CRegionBlockFunctorFilterTest(smart_refctd_ptr&& inImage, BlitFilterTestApp* parentApp, const char* writeImagePath) - : ITest(std::move(inImage), parentApp), m_writeImagePath(writeImagePath) - {} + auto outImage = core::smart_refctd_ptr_static_cast(m_inImage->clone()); + if (!outImage) + return false; - bool run() override - { - auto outImage = smart_refctd_ptr_static_cast(m_inImage->clone()); - if (!outImage) - return false; + const auto format = m_inImage->getCreationParameters().format; + TexelBlockInfo blockInfo(format); - // this is our per-block function - const auto format = m_inImage->getCreationParameters().format; - const auto regions = m_inImage->getRegions(); - const auto region = regions.begin(); - TexelBlockInfo blockInfo(format); - const auto strides = region->getByteStrides(blockInfo); - uint8_t* src = reinterpret_cast(m_inImage->getBuffer()->getPointer()); - uint8_t* dst = reinterpret_cast(outImage->getBuffer()->getPointer()); - auto copyFromLevel0 = [src, dst, &blockInfo, region, strides](uint64_t dstByteOffset, vectorSIMDu32 coord) - { - const uint64_t srcByteOffset = region->getByteOffset(coord, strides); - memcpy(dst+dstByteOffset, src+srcByteOffset, blockInfo.getBlockByteSize()); - }; + const auto strides = m_inImage->getRegions().begin()->getByteStrides(blockInfo); - using region_block_filter_t = asset::CRegionBlockFunctorFilter; + auto copyFromLevel0 = [this, &outImage, format, &blockInfo, &strides](uint64_t dstByteOffset, core::vectorSIMDu32 coord) + { + const uint64_t srcByteOffset = m_inImage->getRegions().begin()->getByteOffset(coord, strides); - region_block_filter_t::CState filterState(copyFromLevel0,outImage.get(),regions.data()+1); + uint8_t* src = reinterpret_cast(m_inImage->getBuffer()->getPointer()) + srcByteOffset; + uint8_t* dst = reinterpret_cast(outImage->getBuffer()->getPointer()) + dstByteOffset; + memcpy(dst, src, asset::getTexelOrBlockBytesize(format)); + }; - for (uint32_t i=1; igetCreationParameters().mipLevels; ++i) - { - filterState.regionIterator = outImage->getRegions().data()+i; - if (!region_block_filter_t::execute(&filterState)) - { - m_parentApp->m_logger->log("CRegionBlockFunctorFilter failed for mip level %u", system::ILogger::ELL_ERROR, i); - return false; - } - } + using region_block_filter_t = asset::CRegionBlockFunctorFilter; + + region_block_filter_t::CState filterState(copyFromLevel0, outImage.get(), outImage->getRegions().begin() + 1); - writeImage(outImage.get(), m_writeImagePath); + for (uint32_t i = 1; i < outImage->getCreationParameters().mipLevels; ++i) + { + filterState.regionIterator = outImage->getRegions().begin() + i; - return true; + if (!region_block_filter_t::execute(&filterState)) + { + m_parentApp->m_logger->log("CRegionBlockFunctorFilter failed for mip level %u", system::ILogger::ELL_ERROR, i); + return false; } + } - private: - const char* m_writeImagePath; - }; + writeImage(std::move(outImage), m_writeImagePath); - public: - using base_t::base_t; - BlitFilterTestApp() = default; + return true; + } - virtual bool onAppInitialized(core::smart_refctd_ptr&& system) override - { - if (!base_t::onAppInitialized(std::move(system))) - return false; + private: + const char* m_writeImagePath; + }; + +public: + using base_t::base_t; + BlitFilterTestApp() = default; + + virtual bool onAppInitialized(core::smart_refctd_ptr&& system) override + { + if (!base_t::onAppInitialized(std::move(system))) + return false; + + queue = getComputeQueue(); + commandPool = m_device->createCommandPool(queue->getFamilyIndex(), IGPUCommandPool::ECF_RESET_COMMAND_BUFFER_BIT); + assetManager = make_smart_refctd_ptr(smart_refctd_ptr(m_system)); + utilities = make_smart_refctd_ptr(smart_refctd_ptr(m_device)); + + core::smart_refctd_ptr transferCmdBuffer; + core::smart_refctd_ptr computeCmdBuffer; + + m_device->createCommandBuffers(commandPool.get(), IGPUCommandBuffer::EL_PRIMARY, 1u, &transferCmdBuffer); + m_device->createCommandBuffers(commandPool.get(), IGPUCommandBuffer::EL_PRIMARY, 1u, &computeCmdBuffer); - assetManager = make_smart_refctd_ptr(smart_refctd_ptr(m_system)); + cpu2gpuParams.assetManager = assetManager.get(); + cpu2gpuParams.device = m_device.get(); + cpu2gpuParams.finalQueueFamIx = queue->getFamilyIndex(); + cpu2gpuParams.pipelineCache = nullptr; + cpu2gpuParams.utilities = utilities.get(); + cpu2gpuParams.perQueue[video::IGPUObjectFromAssetConverter::EQU_TRANSFER].queue = queue; + cpu2gpuParams.perQueue[video::IGPUObjectFromAssetConverter::EQU_COMPUTE].queue = queue; + cpu2gpuParams.perQueue[IGPUObjectFromAssetConverter::EQU_TRANSFER].cmdbuf = transferCmdBuffer; + cpu2gpuParams.perQueue[IGPUObjectFromAssetConverter::EQU_COMPUTE].cmdbuf = computeCmdBuffer; - constexpr bool TestCPUBlitFilter = true; - constexpr bool TestSwizzleAndConvertFilter = false; - constexpr bool TestGPUBlitFilter = true; - constexpr bool TestRegionBlockFunctorFilter = false; + constexpr bool TestCPUBlitFilter = true; + constexpr bool TestFlattenFilter = true; + constexpr bool TestSwizzleAndConvertFilter = true; + constexpr bool TestGPUBlitFilter = true; + constexpr bool TestRegionBlockFunctorFilter = true; - auto loadImage = [this](const char* path) -> smart_refctd_ptr + auto loadImage = [this](const char* path) -> core::smart_refctd_ptr + { + constexpr auto cachingFlags = static_cast(asset::IAssetLoader::ECF_DONT_CACHE_REFERENCES & asset::IAssetLoader::ECF_DONT_CACHE_TOP_LEVEL); + asset::IAssetLoader::SAssetLoadParams loadParams(0ull, nullptr, cachingFlags); + auto imageBundle = assetManager->getAsset(path, loadParams); + auto imageContents = imageBundle.getContents(); + + if (imageContents.empty()) { - // to prevent the images hanging around in the cache and taking up RAM - constexpr auto cachingFlags = static_cast(IAssetLoader::ECF_DONT_CACHE_REFERENCES & IAssetLoader::ECF_DONT_CACHE_TOP_LEVEL); - IAssetLoader::SAssetLoadParams loadParams(0ull, nullptr, cachingFlags); - auto imageBundle = assetManager->getAsset(path, loadParams); - auto imageContents = imageBundle.getContents(); + m_logger->log("Failed to load image at path %s", system::ILogger::ELL_ERROR, path); + return nullptr; + } - if (imageContents.empty()) - { - m_logger->log("Failed to load image at path %s", system::ILogger::ELL_ERROR, path); - return nullptr; - } + auto asset = *imageContents.begin(); - auto asset = *imageContents.begin(); + core::smart_refctd_ptr result; + { + if (asset->getAssetType() == asset::IAsset::ET_IMAGE_VIEW) + result = core::smart_refctd_ptr_static_cast(asset)->getCreationParameters().image; + else if (asset->getAssetType() == asset::IAsset::ET_IMAGE) + result = std::move(core::smart_refctd_ptr_static_cast(asset)); + else + assert(!"Invalid code path."); + } - smart_refctd_ptr result; - { - if (asset->getAssetType() == IAsset::ET_IMAGE_VIEW) - result = smart_refctd_ptr_static_cast(asset)->getCreationParameters().image; - else if (asset->getAssetType() == IAsset::ET_IMAGE) - result = std::move(smart_refctd_ptr_static_cast(asset)); - else - assert(!"Invalid code path."); - } + result->addImageUsageFlags(asset::IImage::EUF_SAMPLED_BIT); - return result; - }; + return result; + }; - auto runTests = [this](const std::span> tests) + auto runTests = [this](const uint32_t count, std::unique_ptr* tests) + { + assert(tests); + + for (uint32_t i = 0; i < count; ++i) { - auto i = 0; - for (auto& test : tests) + if (tests[i]) { - assert(test); - if (!test->run()) + if (!tests[i]->run()) m_logger->log("Test #%u failed.", system::ILogger::ELL_ERROR, i); else m_logger->log("Test #%u passed.", system::ILogger::ELL_INFO, i); - i++; } - }; + } + }; - if (TestCPUBlitFilter) - { - using namespace asset; + if (TestCPUBlitFilter) + { + using namespace asset; + + m_logger->log("CBlitImageFilter", system::ILogger::ELL_INFO); - m_logger->log("CBlitImageFilter", system::ILogger::ELL_INFO); + constexpr uint32_t TestCount = 2; + std::unique_ptr tests[TestCount] = { nullptr }; - constexpr uint32_t TestCount = 2; - std::unique_ptr tests[TestCount] = { nullptr }; + // Test 0: Non-uniform downscale 2D BC format image with Mitchell + { + const char* path = "../../media/GLI/kueken7_rgba_dxt1_unorm.dds"; + auto inImage = loadImage(path); - // Test 0: Non-uniform downscale 2D BC format image with Mitchell + if (inImage) { - const char* path = "../../media/GLI/kueken7_rgba_dxt1_unorm.dds"; - auto inImage = loadImage(path); + const auto& inExtent = inImage->getCreationParameters().extent; + const auto outImageDim = core::vectorSIMDu32(inExtent.width/2, inExtent.height/4, 1, 1); + const auto outImageFormat = asset::EF_R8G8B8A8_SRGB; - if (inImage) - { - const auto& inExtent = inImage->getCreationParameters().extent; - const hlsl::uint32_t3 outImageDim(inExtent.width/2,inExtent.height/4,1); - const auto outImageFormat = asset::EF_R8G8B8A8_SRGB; - - using BlitUtilities = CBlitUtilities>, CWeightFunction1D>>>>; - - auto convolutionKernels = BlitUtilities::getConvolutionKernels>>({inExtent.width,inExtent.height,inExtent.depth},outImageDim); - - tests[0] = std::make_unique> - ( - this, - std::move(inImage), - outImageDim, - 1, - outImageFormat, - "CBlitImageFilter_0.png", - convolutionKernels - ); - } + using BlitUtilities = CBlitUtilities>, CWeightFunction1D>>>>; + + auto convolutionKernels = BlitUtilities::getConvolutionKernels>>(core::vectorSIMDu32(inExtent.width, inExtent.height, inExtent.depth, 1), outImageDim); + + tests[0] = std::make_unique> + ( + std::move(inImage), + this, + outImageDim, + outImageFormat, + "CBlitImageFilter_0.png", + convolutionKernels + ); } + } + + // Test 1: Non-uniform upscale 2D BC format image with Kaiser + { + const char* path = "../../media/GLI/kueken7_rgba_dxt5_unorm.dds"; + auto inImage = loadImage(path); - // Test 1: Non-uniform upscale 2D BC format image with Kaiser + if (inImage) { - const char* path = "../../media/GLI/kueken7_rgba_dxt5_unorm.dds"; - auto inImage = loadImage(path); + const auto& inExtent = inImage->getCreationParameters().extent; + const auto outImageDim = core::vectorSIMDu32(inExtent.width*2, inExtent.height*4, 1, 1); + const auto outImageFormat = asset::EF_R32G32B32A32_SFLOAT; - if (inImage) - { + using BlitUtilities = CBlitUtilities, CWeightFunction1D>>>; - using BlitUtilities = CBlitUtilities, CWeightFunction1D>>>; - - const auto& inExtent = inImage->getCreationParameters().extent; - const hlsl::uint32_t3 outImageDim(inExtent.width*2, inExtent.height*4, 1); - auto convolutionKernels = BlitUtilities::getConvolutionKernels>({inExtent.width,inExtent.height,inExtent.depth},outImageDim); - - const auto outImageFormat = asset::EF_R32G32B32A32_SFLOAT; - tests[1] = std::make_unique> - ( - this, - std::move(inImage), - outImageDim, - 1, - outImageFormat, - "CBlitImageFilter_1.exr", - convolutionKernels - ); - } - } + auto convolutionKernels = BlitUtilities::getConvolutionKernels>(core::vectorSIMDu32(inExtent.width, inExtent.height, inExtent.depth, 1), outImageDim); - runTests(tests); + tests[1] = std::make_unique> + ( + std::move(inImage), + this, + outImageDim, + outImageFormat, + "CBlitImageFilter_1.exr", + convolutionKernels + ); + } } - if (TestSwizzleAndConvertFilter) + runTests(TestCount, tests); + } + + if (TestFlattenFilter) + { + asset::IImageFilter::IState::ColorValue fillColorValue; + auto getFillValueAsFirstBlockOrTexel = [&fillColorValue](asset::ICPUImage* image) { - m_logger->log("CSwizzleAndConvertImageFilter",ILogger::ELL_INFO); + const auto format = image->getCreationParameters().format; + if (asset::isBlockCompressionFormat(format)) + memcpy(fillColorValue.asCompressedBlock, image->getBuffer()->getPointer(), asset::getTexelOrBlockBytesize(format)); + else if (asset::isFloatingPointFormat(format)) + fillColorValue.asFloat.set(reinterpret_cast(image->getBuffer()->getPointer())); + else + _NBL_TODO(); + }; - constexpr uint32_t TestCount = 6; - std::unique_ptr tests[TestCount] = { nullptr }; + m_logger->log("CFlattenRegionsImageFilter", system::ILogger::ELL_INFO); - // Test 0: Simple format conversion - { - const char* path = "../../media/GLI/kueken7_rgba_dxt1_unorm.dds"; - auto inImage = loadImage(path); + constexpr uint32_t TestCount = 4; + std::unique_ptr tests[TestCount] = { nullptr }; - if (inImage) - { - tests[0] = std::make_unique> - ( - std::move(inImage), - this, - asset::EF_R8G8B8A8_SRGB, - hlsl::uint32_t4(0, 0, 0, 0), - hlsl::uint32_t4(0, 0, 0, 0), - asset::ICPUImageView::SComponentMapping(), - "CSwizzleAndConvertImageFilter_0.png" - ); - } - } + // Test 0: BC format with prefill + { + const char* path = "../../media/GLI/kueken7_rgba_dxt1_unorm.dds"; + auto inImage = loadImage(path); - // Test 1: Non-trivial offsets + if (inImage) { - const char* path = "../../media/GLI/kueken7_rgba_dxt5_unorm.dds"; - auto inImage = loadImage(path); + const auto inImageFormat = inImage->getCreationParameters().format; + getFillValueAsFirstBlockOrTexel(inImage.get()); - if (inImage) - { - tests[1] = std::make_unique> - ( - std::move(inImage), - this, - asset::EF_R32G32B32A32_SFLOAT, - hlsl::uint32_t4(64, 64, 0, 0), - hlsl::uint32_t4(64, 0, 0, 0), - asset::ICPUImageView::SComponentMapping(), - "CSwizzleAndConvertImageFilter_1.exr" - ); - } + tests[0] = std::make_unique + ( + std::move(inImage), + this, + true, + fillColorValue, + "CFlattenRegionsImageFilter_0.dds" + ); } + } + + // Test 1: Non BC format with prefill + { + const char* path = "../../media/colorexr.exr"; + auto inImage = loadImage(path); - // Test 2: Non-trivial swizzle + if (inImage) { - const char* path = "../../media/GLI/dice_bc3.dds"; - auto inImage = loadImage(path); + const auto inImageFormat = inImage->getCreationParameters().format; + getFillValueAsFirstBlockOrTexel(inImage.get()); - if (inImage) - { - tests[2] = std::make_unique> - ( - std::move(inImage), - this, - asset::EF_R32G32B32A32_SFLOAT, - hlsl::uint32_t4(0, 0, 0, 0), - hlsl::uint32_t4(0, 0, 0, 0), - asset::ICPUImageView::SComponentMapping(asset::ICPUImageView::SComponentMapping::ES_G, asset::ICPUImageView::SComponentMapping::ES_B, asset::ICPUImageView::SComponentMapping::ES_R, asset::ICPUImageView::SComponentMapping::ES_A), - "CSwizzleAndConvertImageFilter_2.exr" - ); - } + tests[1] = std::make_unique + ( + std::move(inImage), + this, + true, + fillColorValue, + "CFlattenRegionsImageFilter_1.exr" + ); } + } - // Test 3: Non-trivial dithering + // Test 2: BC format without prefill + { + const char* path = "../../media/GLI/kueken7_rgba_dxt5_unorm.dds"; + auto inImage = loadImage(path); + + if (inImage) { - const char* path = "../../media/GLI/kueken7_rgb_dxt1_unorm.ktx"; - auto inImage = loadImage(path); + tests[2] = std::make_unique + ( + std::move(inImage), + this, + false, + asset::IImageFilter::IState::ColorValue(), + "CFlattenRegionsImageFilter_2.dds" + ); + } + } - if (inImage) - { - tests[3] = std::make_unique> - ( - std::move(inImage), - this, - asset::EF_R8G8B8_SRGB, - hlsl::uint32_t4(0, 0, 0, 0), - hlsl::uint32_t4(0, 0, 0, 0), - asset::ICPUImageView::SComponentMapping(), - "CSwizzleAndConvertImageFilter_3.jpg" - ); - } + // Test 3: Non BC format without prefill + { + const char* path = "../../media/color_space_test/R8G8B8_2.jpg"; + auto inImage = loadImage(path); + + if (inImage) + { + tests[3] = std::make_unique + ( + std::move(inImage), + this, + false, + asset::IImageFilter::IState::ColorValue(), + "CFlattenRegionsImageFilter_3.jpg" + ); } + } + + runTests(TestCount, tests); + } - // Test 4: Non-trivial normalization (warning, supposed to look like crap) + if (TestSwizzleAndConvertFilter) + { + m_logger->log("CSwizzleAndConvertImageFilter", system::ILogger::ELL_INFO); + + constexpr uint32_t TestCount = 6; + std::unique_ptr tests[TestCount] = { nullptr }; + + // Test 0: Simple format conversion + { + const char* path = "../../media/GLI/kueken7_rgba_dxt1_unorm.dds"; + auto inImage = loadImage(path); + + if (inImage) { - const char* path = "../../media/envmap/envmap_0.exr"; - auto inImage = loadImage(path); + tests[0] = std::make_unique> + ( + std::move(inImage), + this, + asset::EF_R8G8B8A8_SRGB, + core::vectorSIMDu32(0, 0, 0, 0), + core::vectorSIMDu32(0, 0, 0, 0), + asset::ICPUImageView::SComponentMapping(), + "CSwizzleAndConvertImageFilter_0.png" + ); + } + } - if (inImage) - { - tests[4] = std::make_unique> - ( - std::move(inImage), - this, - asset::EF_R32G32B32A32_SFLOAT, - hlsl::uint32_t4(0, 0, 0, 0), - hlsl::uint32_t4(0, 0, 0, 0), - asset::ICPUImageView::SComponentMapping(), - "CSwizzleAndConvertImageFilter_4.exr" - ); - } + // Test 1: Non-trivial offsets + { + const char* path = "../../media/GLI/kueken7_rgba_dxt5_unorm.dds"; + auto inImage = loadImage(path); + + if (inImage) + { + tests[1] = std::make_unique> + ( + std::move(inImage), + this, + asset::EF_R32G32B32A32_SFLOAT, + core::vectorSIMDu32(64, 64, 0, 0), + core::vectorSIMDu32(64, 0, 0, 0), + asset::ICPUImageView::SComponentMapping(), + "CSwizzleAndConvertImageFilter_1.exr" + ); } + } + + // Test 2: Non-trivial swizzle + { + const char* path = "../../media/GLI/dice_bc3.dds"; + auto inImage = loadImage(path); - // Test 5: Non-trivial clamping + if (inImage) { - const char* path = "../../media/envmap/envmap_1.exr"; - auto inImage = loadImage(path); + tests[2] = std::make_unique> + ( + std::move(inImage), + this, + asset::EF_R32G32B32A32_SFLOAT, + core::vectorSIMDu32(0, 0, 0, 0), + core::vectorSIMDu32(0, 0, 0, 0), + asset::ICPUImageView::SComponentMapping(asset::ICPUImageView::SComponentMapping::ES_G, asset::ICPUImageView::SComponentMapping::ES_B, asset::ICPUImageView::SComponentMapping::ES_R, asset::ICPUImageView::SComponentMapping::ES_A), + "CSwizzleAndConvertImageFilter_2.exr" + ); + } + } - if (inImage) - { - tests[5] = std::make_unique> - ( - std::move(inImage), - this, - asset::EF_R8G8B8A8_SRGB, - hlsl::uint32_t4(0, 0, 0, 0), - hlsl::uint32_t4(0, 0, 0, 0), - asset::ICPUImageView::SComponentMapping(), - "CSwizzleAndConvertImageFilter_5.png" - ); - } + // Test 3: Non-trivial dithering + { + const char* path = "../../media/GLI/kueken7_rgb_dxt1_unorm.ktx"; + auto inImage = loadImage(path); + + if (inImage) + { + tests[3] = std::make_unique> + ( + std::move(inImage), + this, + asset::EF_R8G8B8_SRGB, + core::vectorSIMDu32(0, 0, 0, 0), + core::vectorSIMDu32(0, 0, 0, 0), + asset::ICPUImageView::SComponentMapping(), + "CSwizzleAndConvertImageFilter_3.jpg" + ); } + } + + // Test 4: Non-trivial normalization (warning, supposed to look like crap) + { + const char* path = "../../media/envmap/envmap_0.exr"; + auto inImage = loadImage(path); - runTests(tests); + if (inImage) + { + tests[4] = std::make_unique> + ( + std::move(inImage), + this, + asset::EF_R32G32B32A32_SFLOAT, + core::vectorSIMDu32(0, 0, 0, 0), + core::vectorSIMDu32(0, 0, 0, 0), + asset::ICPUImageView::SComponentMapping(), + "CSwizzleAndConvertImageFilter_4.exr" + ); + } } - if (TestGPUBlitFilter) + // Test 5: Non-trivial clamping { - m_logger->log("CComputeBlit", system::ILogger::ELL_INFO); + const char* path = "../../media/envmap/envmap_1.exr"; + auto inImage = loadImage(path); - m_blitFilter = make_smart_refctd_ptr(smart_refctd_ptr(m_device)); + if (inImage) + { + tests[5] = std::make_unique> + ( + std::move(inImage), + this, + asset::EF_R8G8B8A8_SRGB, + core::vectorSIMDu32(0, 0, 0, 0), + core::vectorSIMDu32(0, 0, 0, 0), + asset::ICPUImageView::SComponentMapping(), + "CSwizzleAndConvertImageFilter_5.png" + ); + } + } + + runTests(TestCount, tests); + } + + if (TestGPUBlitFilter) + { + using namespace asset; - constexpr uint32_t TestCount = 6; - std::unique_ptr tests[TestCount] = { nullptr }; + m_logger->log("CComputeBlit", system::ILogger::ELL_INFO); - // Test 0: Resize 1D image with Mitchell + constexpr uint32_t TestCount = 6; + std::unique_ptr tests[TestCount] = { nullptr }; + + // Test 0: Resize 1D image with Mitchell + { + const auto layerCount = 10; + const core::vectorSIMDu32 inImageDim(59u, 1u, 1u, layerCount); + const asset::IImage::E_TYPE inImageType = asset::IImage::ET_1D; + const asset::E_FORMAT inImageFormat = asset::EF_R32_SFLOAT; + auto inImage = createCPUImage(inImageDim, inImageType, inImageFormat, true); + + if (inImage) { - const hlsl::uint32_t3 inImageDim(59u,1u,1u); - const auto layerCount = 10; - auto inImage = createCPUImage(inImageDim,layerCount,IImage::ET_1D,EF_R32_SFLOAT,true); - assert(inImage); + const core::vectorSIMDu32 outImageDim(800u, 1u, 1u, layerCount); auto reconstructionX = asset::CWeightFunction1D>(); reconstructionX.stretchAndScale(0.35f); @@ -1398,63 +1296,66 @@ class BlitFilterTestApp final : public virtual application_templates::BasicMulti auto resamplingX = asset::CWeightFunction1D>(); resamplingX.stretchAndScale(0.35f); - using LutDataType = hlsl::float16_t; + using LutDataType = uint16_t; using BlitUtilities = CBlitUtilities< CDefaultChannelIndependentWeightFunction1D>, CWeightFunction1D>>>, CDefaultChannelIndependentWeightFunction1D>, CWeightFunction1D>>>, CDefaultChannelIndependentWeightFunction1D>, CWeightFunction1D>>>, LutDataType>; - const hlsl::uint32_t3 outImageDim(800u, 1u, 1u); auto convolutionKernels = BlitUtilities::getConvolutionKernels>>(inImageDim, outImageDim, std::move(reconstructionX), std::move(resamplingX)); tests[0] = std::make_unique> ( - this, - "mitchell_1d", std::move(inImage), + this, outImageDim, convolutionKernels ); } + } - // Test 1: Resize 2D image with Kaiser + // Test 1: Resize 2D image with Kaiser + { + const char* path = "../../media/colorexr.exr"; + auto inImage = loadImage(path); + if (inImage) { - const char* path = "../../media/colorexr.exr"; - auto inImage = loadImage(path); - if (inImage) - { - using LutDataType = float; - using BlitUtilities = CBlitUtilities< - CDefaultChannelIndependentWeightFunction1D, CWeightFunction1D>>, - CDefaultChannelIndependentWeightFunction1D, CWeightFunction1D>>, - CDefaultChannelIndependentWeightFunction1D, CWeightFunction1D>>, - LutDataType - >; - - const auto& inExtent = inImage->getCreationParameters().extent; - const hlsl::uint32_t3 outImageDim(inExtent.width / 3u, inExtent.height / 7u, inExtent.depth); - auto convolutionKernels = BlitUtilities::getConvolutionKernels>({inExtent.width,inExtent.height,inExtent.depth},outImageDim); - - tests[1] = std::make_unique> - ( - this, - "kaiser_2d", - std::move(inImage), - outImageDim, - convolutionKernels - ); - } + const auto& inExtent = inImage->getCreationParameters().extent; + const auto layerCount = inImage->getCreationParameters().arrayLayers; + const core::vectorSIMDu32 outImageDim(inExtent.width / 3u, inExtent.height / 7u, inExtent.depth, layerCount); + + using LutDataType = float; + using BlitUtilities = CBlitUtilities< + CDefaultChannelIndependentWeightFunction1D, CWeightFunction1D>>, + CDefaultChannelIndependentWeightFunction1D, CWeightFunction1D>>, + CDefaultChannelIndependentWeightFunction1D, CWeightFunction1D>>, + LutDataType + >; + + auto convolutionKernels = BlitUtilities::getConvolutionKernels>(core::vectorSIMDu32(inExtent.width, inExtent.height, inExtent.depth, 1), outImageDim); + + tests[1] = std::make_unique> + ( + std::move(inImage), + this, + outImageDim, + convolutionKernels + ); } + } + + // Test 2: Resize 3D image with Box + { + const auto layerCount = 1u; + const core::vectorSIMDu32 inImageDim(2u, 3u, 4u, layerCount); + const asset::IImage::E_TYPE inImageType = asset::IImage::ET_3D; + const asset::E_FORMAT inImageFormat = asset::EF_R32G32B32A32_SFLOAT; + auto inImage = createCPUImage(inImageDim, inImageType, inImageFormat, true); - // Test 2: Resize 3D image with Box + if (inImage) { - const uint32_t layerCount = 1; - const hlsl::uint32_t3 inImageDim(2,3,4); - const IImage::E_TYPE inImageType = IImage::ET_3D; - const E_FORMAT inImageFormat = EF_R32G32B32A32_SFLOAT; - auto inImage = createCPUImage(inImageDim,layerCount,inImageType,inImageFormat,true); - assert(inImage); + const core::vectorSIMDu32 outImageDim(3u, 4u, 2u, layerCount); auto reconstructionX = asset::CWeightFunction1D(); reconstructionX.stretchAndScale(0.35f); @@ -1466,7 +1367,7 @@ class BlitFilterTestApp final : public virtual application_templates::BasicMulti auto resamplingY = asset::CWeightFunction1D(); resamplingY.stretchAndScale(9.f/16.f); - using LutDataType = hlsl::float16_t; + using LutDataType = uint16_t; using BlitUtilities = CBlitUtilities< CDefaultChannelIndependentWeightFunction1D, CWeightFunction1D>>, CDefaultChannelIndependentWeightFunction1D, CWeightFunction1D>>, @@ -1474,68 +1375,68 @@ class BlitFilterTestApp final : public virtual application_templates::BasicMulti LutDataType >; - const hlsl::uint32_t3 outImageDim(3, 4, 2); auto convolutionKernels = BlitUtilities::getConvolutionKernels>(inImageDim, outImageDim, std::move(reconstructionX), std::move(resamplingX), std::move(reconstructionY), std::move(resamplingY)); tests[2] = std::make_unique> ( - this, - "box_3d", std::move(inImage), + this, outImageDim, convolutionKernels ); } + } - // Test 3: Resize 2D image with alpha coverage adjustment + // Test 3: Resize 2D image with alpha coverage adjustment + { + // We should find a better image for testing coverage adjustment + const char* path = "../../media/colorexr.exr"; + auto inImage = loadImage(path); + if (inImage) { - // We should find a better image for testing coverage adjustment - // WARNING: The output of this will turn pixels with Alpha 1.0 to pixels with Alpha slightly > referenceAlpha !!!! - // This is simply how coverage adjustment works! - const char* path = "../../media/colorexr.exr"; - auto inImage = loadImage(path); - if (inImage) - { - const auto& inExtent = inImage->getCreationParameters().extent; - const auto alphaSemantic = IBlitUtilities::EAS_REFERENCE_OR_COVERAGE; - const float referenceAlpha = 0.5f; - const auto alphaBinCount = 1024; - - using LutDataType = float; - using BlitUtilities = CBlitUtilities< - CDefaultChannelIndependentWeightFunction1D>, CWeightFunction1D>>>, - CDefaultChannelIndependentWeightFunction1D>, CWeightFunction1D>>>, - CDefaultChannelIndependentWeightFunction1D>, CWeightFunction1D>>>, - LutDataType - >; - - const hlsl::uint32_t3 outImageDim(inExtent.width/3,inExtent.height/7,inExtent.depth); - auto convolutionKernels = BlitUtilities::getConvolutionKernels>>({inExtent.width,inExtent.height,inExtent.depth},outImageDim); - - tests[3] = std::make_unique> - ( - this, - "coverage_2d", - std::move(inImage), - outImageDim, - convolutionKernels, - alphaSemantic, - referenceAlpha, - alphaBinCount - ); - } + const auto& inExtent = inImage->getCreationParameters().extent; + const auto layerCount = inImage->getCreationParameters().arrayLayers; + const core::vectorSIMDu32 outImageDim(inExtent.width / 3u, inExtent.height / 7u, inExtent.depth, layerCount); + const auto alphaSemantic = IBlitUtilities::EAS_REFERENCE_OR_COVERAGE; + const float referenceAlpha = 0.5f; + const auto alphaBinCount = 1024; + + using LutDataType = float; + using BlitUtilities = CBlitUtilities< + CDefaultChannelIndependentWeightFunction1D>, CWeightFunction1D>>>, + CDefaultChannelIndependentWeightFunction1D>, CWeightFunction1D>>>, + CDefaultChannelIndependentWeightFunction1D>, CWeightFunction1D>>>, + LutDataType + >; + + auto convolutionKernels = BlitUtilities::getConvolutionKernels>>(core::vectorSIMDu32(inExtent.width, inExtent.height, inExtent.depth, 1), outImageDim); + + tests[3] = std::make_unique> + ( + std::move(inImage), + this, + outImageDim, + convolutionKernels, + alphaSemantic, + referenceAlpha, + alphaBinCount + ); } + } - // Test 4: A larger 3D image with an atypical format + // Test 4: A larger 3D image with an atypical format + { + const auto layerCount = 1; + const core::vectorSIMDu32 inImageDim(257u, 129u, 63u, layerCount); + const asset::IImage::E_TYPE inImageType = asset::IImage::ET_3D; + const asset::E_FORMAT inImageFormat = asset::EF_B10G11R11_UFLOAT_PACK32; + auto inImage = createCPUImage(inImageDim, inImageType, inImageFormat, true); + + if (inImage) { - const auto layerCount = 1; - const hlsl::uint32_t3 inImageDim(257,129,63); - const IImage::E_TYPE inImageType = IImage::ET_3D; - const E_FORMAT inImageFormat = EF_B10G11R11_UFLOAT_PACK32; - auto inImage = createCPUImage(inImageDim, layerCount, inImageType, inImageFormat, true); - assert(inImage); - - using LutDataType = hlsl::float16_t; + const core::vectorSIMDu32 outImageDim(256u, 128u, 64u, layerCount); + + using LutDataType = uint16_t; using BlitUtilities = CBlitUtilities< CDefaultChannelIndependentWeightFunction1D>, CWeightFunction1D>>>, CDefaultChannelIndependentWeightFunction1D>, CWeightFunction1D>>>, @@ -1543,27 +1444,32 @@ class BlitFilterTestApp final : public virtual application_templates::BasicMulti LutDataType >; - const hlsl::uint32_t3 outImageDim(256,128,64); auto convolutionKernels = BlitUtilities::getConvolutionKernels>>(inImageDim, outImageDim); tests[4] = std::make_unique> ( - this, - "b10g11r11_3d", std::move(inImage), + this, outImageDim, convolutionKernels ); } + } + + // Test 5: A 2D image with atypical dimensions and alpha coverage adjustment + { + const auto layerCount = 7; + const core::vectorSIMDu32 inImageDim(511u, 1024u, 1u, layerCount); + const asset::IImage::E_TYPE inImageType = asset::IImage::ET_2D; + const asset::E_FORMAT inImageFormat = EF_R16G16B16A16_SNORM; + auto inImage = createCPUImage(inImageDim, inImageType, inImageFormat, true); - // Test 5: A 2D image with atypical dimensions and alpha coverage adjustment + if (inImage) { - const auto layerCount = 7; - const hlsl::uint32_t3 inImageDim(511,1024,1); - const IImage::E_TYPE inImageType = IImage::ET_2D; - const E_FORMAT inImageFormat = EF_R16G16B16A16_SNORM; - auto inImage = createCPUImage(inImageDim, layerCount, inImageType, inImageFormat, true); - assert(inImage); + const core::vectorSIMDu32 outImageDim(512u, 257u, 1u, layerCount); + const IBlitUtilities::E_ALPHA_SEMANTIC alphaSemantic = IBlitUtilities::EAS_REFERENCE_OR_COVERAGE; + const float referenceAlpha = 0.5f; + const auto alphaBinCount = 4096; using LutDataType = float; using BlitUtilities = CBlitUtilities< @@ -1573,17 +1479,12 @@ class BlitFilterTestApp final : public virtual application_templates::BasicMulti LutDataType >; - const hlsl::uint32_t3 outImageDim(512, 257, 1); auto convolutionKernels = BlitUtilities::getConvolutionKernels>>(inImageDim, outImageDim); - const IBlitUtilities::E_ALPHA_SEMANTIC alphaSemantic = IBlitUtilities::EAS_REFERENCE_OR_COVERAGE; - const float referenceAlpha = 0.5f; - const auto alphaBinCount = 4096; tests[5] = std::make_unique> ( - this, - "rand_coverage_2d", std::move(inImage), + this, outImageDim, convolutionKernels, alphaSemantic, @@ -1591,66 +1492,144 @@ class BlitFilterTestApp final : public virtual application_templates::BasicMulti alphaBinCount ); } - - runTests(tests); } - if (TestRegionBlockFunctorFilter) - { - m_logger->log("CRegionBlockFunctorFilter",ILogger::ELL_INFO); + runTests(TestCount, tests); + } - constexpr uint32_t TestCount = 1; - std::unique_ptr tests[TestCount] = { nullptr }; + if (TestRegionBlockFunctorFilter) + { + m_logger->log("CRegionBlockFunctorFilter", system::ILogger::ELL_INFO); - // Test 0: Copy the first NxM texels of the 0th mip level to the ith mip level, where N and M are dimensions of the ith mip level. - { - auto inImage = loadImage("../../media/GLI/kueken7_rgba_dxt5_unorm.dds"); - if (inImage) - tests[0] = std::make_unique(std::move(inImage), this, "CRegionBlockFunctorFilter_0.dds"); - } + constexpr uint32_t TestCount = 1; + std::unique_ptr tests[TestCount] = { nullptr }; - runTests(tests); + // Test 0: Copy the first NxM texels of the 0th mip level to the ith mip level, where N and M are dimensions of the ith mip level. + { + auto inImage = loadImage("../../media/GLI/kueken7_rgba_dxt5_unorm.dds"); + if (inImage) + tests[0] = std::make_unique(std::move(inImage), this, "CRegionBlockFunctorFilter_0.dds"); } - return true; + runTests(TestCount, tests); } - - bool onAppTerminated() override + } + + bool onAppTerminated() override + { + m_device->waitIdle(); + return base_t::onAppTerminated(); + } + + void workLoopBody() override + { + } + + bool keepRunning() override + { + return false; + } + + core::vector getQueueRequirements() const override + { + core::vector retval; + + using flags_t = video::IPhysicalDevice::E_QUEUE_FLAGS; + // IGPUObjectFromAssetConverter requires the queue to support graphics as well. + retval.push_back({ .requiredFlags = flags_t::EQF_COMPUTE_BIT | flags_t::EQF_TRANSFER_BIT | flags_t::EQF_GRAPHICS_BIT,.disallowedFlags = flags_t::EQF_NONE,.queueCount = 1,.maxImageTransferGranularity = {1,1,1} }); + + return retval; + } + +private: + // dims[3] is layer count + core::smart_refctd_ptr createCPUImage(const core::vectorSIMDu32& dims, const asset::IImage::E_TYPE imageType, const asset::E_FORMAT format, const bool fillWithTestData = false) + { + IImage::SCreationParams imageParams = {}; + imageParams.flags = static_cast(asset::IImage::ECF_MUTABLE_FORMAT_BIT | asset::IImage::ECF_EXTENDED_USAGE_BIT); + imageParams.type = imageType; + imageParams.format = format; + imageParams.extent = { dims[0], dims[1], dims[2] }; + imageParams.mipLevels = 1; + imageParams.arrayLayers = dims[3]; + imageParams.samples = asset::ICPUImage::ESCF_1_BIT; + imageParams.usage = asset::IImage::EUF_SAMPLED_BIT; + + auto imageRegions = core::make_refctd_dynamic_array>(1ull); + auto& region = (*imageRegions)[0]; + region.bufferImageHeight = 0u; + region.bufferOffset = 0ull; + region.bufferRowLength = dims[0]; + region.imageExtent = { dims[0], dims[1], dims[2] }; + region.imageOffset = { 0u, 0u, 0u }; + region.imageSubresource.aspectMask = asset::IImage::EAF_COLOR_BIT; + region.imageSubresource.baseArrayLayer = 0u; + region.imageSubresource.layerCount = imageParams.arrayLayers; + region.imageSubresource.mipLevel = 0; + + size_t bufferSize = imageParams.arrayLayers * asset::getTexelOrBlockBytesize(imageParams.format) * static_cast(region.imageExtent.width) * region.imageExtent.height * region.imageExtent.depth; + auto imageBuffer = core::make_smart_refctd_ptr(bufferSize); + + core::smart_refctd_ptr image = ICPUImage::create(std::move(imageParams)); + if (!image) { - m_device->waitIdle(); - return base_t::onAppTerminated(); + m_logger->log("Failed to create a CPU image", system::ILogger::ELL_ERROR); + return nullptr; } - void workLoopBody() override - { - } + image->setBufferAndRegions(core::smart_refctd_ptr(imageBuffer), imageRegions); - bool keepRunning() override + if (fillWithTestData) { - return false; - } + double pixelValueUpperBound = 20.0; + if (asset::isNormalizedFormat(format) || format == asset::EF_B10G11R11_UFLOAT_PACK32) + pixelValueUpperBound = 1.00000000001; - core::vector getQueueRequirements() const override - { - core::vector retval; - - using flags_t = IQueue::FAMILY_FLAGS; - // IGPUObjectFromAssetConverter requires the queue to support graphics as well. - retval.push_back({ - .requiredFlags = flags_t::COMPUTE_BIT|flags_t::TRANSFER_BIT|flags_t::GRAPHICS_BIT, - .disallowedFlags = flags_t::NONE, - .queueCount = 1, - .maxImageTransferGranularity = {1,1,1} - }); - - return retval; + std::uniform_real_distribution dist(0.0, pixelValueUpperBound); + std::mt19937 prng; + + uint8_t* bytePtr = reinterpret_cast(image->getBuffer()->getPointer()); + const auto layerSize = bufferSize / imageParams.arrayLayers; + + double dummyVal = 1.0; + for (auto layer = 0; layer < image->getCreationParameters().arrayLayers; ++layer) + { + for (uint64_t k = 0u; k < dims[2]; ++k) + { + for (uint64_t j = 0u; j < dims[1]; ++j) + { + for (uint64_t i = 0; i < dims[0]; ++i) + { + double decodedPixel[4] = { 0 }; + for (uint32_t ch = 0u; ch < asset::getFormatChannelCount(format); ++ch) + decodedPixel[ch] = dist(prng); + + const uint64_t pixelIndex = (k * dims[1] * dims[0]) + (j * dims[0]) + i; + asset::encodePixelsRuntime(format, bytePtr + layer * layerSize + pixelIndex * asset::getTexelOrBlockBytesize(format), decodedPixel); + } + } + } + } } - smart_refctd_ptr m_blitFilter; + return image; + } - private: - smart_refctd_ptr assetManager; - IQueue* queue; + video::SPhysicalDeviceFeatures getRequiredDeviceFeatures() const override + { + auto retval = base_t::getRequiredDeviceFeatures(); + + retval.vulkanMemoryModelDeviceScope = true; + + return retval; + } + + core::smart_refctd_ptr assetManager; + video::IGPUQueue* queue; + core::smart_refctd_ptr commandPool; + core::smart_refctd_ptr utilities; + video::IGPUObjectFromAssetConverter::SParams cpu2gpuParams; + video::IGPUObjectFromAssetConverter cpu2gpu; }; NBL_MAIN_FUNC(BlitFilterTestApp) \ No newline at end of file diff --git a/26_Blur/CMakeLists.txt b/26_Blur/CMakeLists.txt deleted file mode 100644 index a434ff32a..000000000 --- a/26_Blur/CMakeLists.txt +++ /dev/null @@ -1,24 +0,0 @@ -include(common RESULT_VARIABLE RES) -if(NOT RES) - message(FATAL_ERROR "common.cmake not found. Should be in {repo_root}/cmake directory") -endif() - -nbl_create_executable_project("" "" "" "" "${NBL_EXECUTABLE_PROJECT_CREATION_PCH_TARGET}") - -if(NBL_EMBED_BUILTIN_RESOURCES) - set(_BR_TARGET_ ${EXECUTABLE_NAME}_builtinResourceData) - set(RESOURCE_DIR "app_resources") - - get_filename_component(_SEARCH_DIRECTORIES_ "${CMAKE_CURRENT_SOURCE_DIR}" ABSOLUTE) - get_filename_component(_OUTPUT_DIRECTORY_SOURCE_ "${CMAKE_CURRENT_BINARY_DIR}/src" ABSOLUTE) - get_filename_component(_OUTPUT_DIRECTORY_HEADER_ "${CMAKE_CURRENT_BINARY_DIR}/include" ABSOLUTE) - - file(GLOB_RECURSE BUILTIN_RESOURCE_FILES RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}/${RESOURCE_DIR}" CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/${RESOURCE_DIR}/*") - foreach(RES_FILE ${BUILTIN_RESOURCE_FILES}) - LIST_BUILTIN_RESOURCE(RESOURCES_TO_EMBED "${RES_FILE}") - endforeach() - - ADD_CUSTOM_BUILTIN_RESOURCES(${_BR_TARGET_} RESOURCES_TO_EMBED "${_SEARCH_DIRECTORIES_}" "${RESOURCE_DIR}" "nbl::this_example::builtin" "${_OUTPUT_DIRECTORY_HEADER_}" "${_OUTPUT_DIRECTORY_SOURCE_}") - - LINK_BUILTIN_RESOURCES_TO_TARGET(${EXECUTABLE_NAME} ${_BR_TARGET_}) -endif() \ No newline at end of file diff --git a/26_Blur/app_resources/common.hlsl b/26_Blur/app_resources/common.hlsl deleted file mode 100644 index e3d76ca03..000000000 --- a/26_Blur/app_resources/common.hlsl +++ /dev/null @@ -1,11 +0,0 @@ -#include "nbl/builtin/hlsl/cpp_compat.hlsl" -#include "nbl/builtin/hlsl/type_traits.hlsl" - -static const uint16_t PASSES = 2; - -struct PushConstants -{ - nbl::hlsl::float32_t radius; - uint32_t activeAxis : 2; - uint32_t edgeWrapMode : 6; -}; \ No newline at end of file diff --git a/26_Blur/app_resources/shader.comp.hlsl b/26_Blur/app_resources/shader.comp.hlsl deleted file mode 100644 index 99e876ccc..000000000 --- a/26_Blur/app_resources/shader.comp.hlsl +++ /dev/null @@ -1,161 +0,0 @@ -#include "nbl/builtin/hlsl/prefix_sum_blur/blur.hlsl" -#include "nbl/builtin/hlsl/prefix_sum_blur/box_sampler.hlsl" -#include "nbl/builtin/hlsl/workgroup/scratch_size.hlsl" -#include "nbl/builtin/hlsl/jit/device_capabilities.hlsl" -#include "nbl/builtin/hlsl/colorspace/OETF.hlsl" -#include "common.hlsl" - -using namespace nbl::hlsl; - -uint32_t3 glsl::gl_WorkGroupSize() { return uint32_t3(WORKGROUP_SIZE, 1, 1); } - -[[vk::binding(0)]] -Texture2D input; -[[vk::binding(1)]] -RWTexture2D output; - -[[vk::push_constant]] PushConstants pc; - -template -struct TextureProxy -{ - NBL_CONSTEXPR uint16_t Channels = Chnls; - using texel_t = vector; - - // divisions by PoT constant will optimize out nicely - template - T get(const uint16_t channel, const uint16_t uv) - { - return spill[uv / WORKGROUP_SIZE][channel]; - } - - template - void set(const uint16_t channel, const uint16_t uv, T value) - { - spill[uv / WORKGROUP_SIZE][channel] = value; - } - - void load() - { - const uint16_t end = linearSize(); - uint16_t ix = workgroup::SubgroupContiguousIndex(); - // because workgroups do scans cooperatively all spill values need sane defaults - for (uint16_t i=0; i < SpillSize; ix += WORKGROUP_SIZE) - spill[i++] = ix < end ? (texel_t)input[position(ix)] : promote(0.f); - } - - void store() - { - const uint16_t end = linearSize(); - uint16_t i = 0; - // making sure that we don't store out of range - for (uint16_t ix = workgroup::SubgroupContiguousIndex(); ix < end; ix += WORKGROUP_SIZE) - { - float32_t4 tmp = float32_t4(0, 0, 0, 1); - for (uint16_t ch=0; ch < Channels; ch++) - tmp[ch] = spill[i][ch]; - i++; - output[position(ix)] = tmp; - } - } - - uint16_t linearSize() - { - uint32_t3 dims; - input.GetDimensions(0, dims.x, dims.y, dims.z); - return _static_cast(dims[activeAxis]); - } - - uint16_t2 position(uint16_t ix) - { - uint16_t2 pos; - pos[activeAxis] = ix; - pos[activeAxis ^ 0x1] = _static_cast(glsl::gl_WorkGroupID().x); - return pos; - } - - // whether we pas along X or Y - uint16_t activeAxis; - NBL_CONSTEXPR uint16_t SpillSize = (MAX_SCANLINE_SIZE - 1) / WORKGROUP_SIZE + 1; - texel_t spill[SpillSize]; -}; - -static const uint16_t MAX_SCAN_SCRATCH_SIZE = workgroup::scratch_size_arithmetic::value + 2; - -// we always use `uint32_t` -groupshared uint32_t smem[MAX_SCANLINE_SIZE]; -groupshared uint32_t prefix_smem[MAX_SCAN_SCRATCH_SIZE]; - -struct SharedMemoryProxy -{ - NBL_CONSTEXPR uint16_t Size = MAX_SCANLINE_SIZE; - - template - enable_if_t get(const I idx) - { - return bit_cast(smem[idx]); - } - - template - enable_if_t set(const I idx, T value) - { - smem[idx] = bit_cast(value); - } - - void workgroupExecutionAndMemoryBarrier() - { - glsl::barrier(); - } -}; - -struct ScanSharedMemoryProxy -{ - NBL_CONSTEXPR uint16_t Size = MAX_SCAN_SCRATCH_SIZE; - - template - enable_if_t get(const uint16_t idx, NBL_REF_ARG(T) val) - { - val = bit_cast(prefix_smem[idx]); - } - - template - enable_if_t set(const I idx, T value) - { - prefix_smem[idx] = bit_cast(value); - } - - void workgroupExecutionAndMemoryBarrier() - { - glsl::barrier(); - } -}; - -[numthreads(WORKGROUP_SIZE, 1, 1)] -[shader("compute")] -void main() -{ - ScanSharedMemoryProxy scanSmemAccessor; - - TextureProxy texAccessor; - texAccessor.activeAxis = (uint16_t)pc.activeAxis; - texAccessor.load(); - - prefix_sum_blur::BoxSampler boxSampler; - boxSampler.wrapMode = uint16_t(pc.edgeWrapMode); - boxSampler.linearSize = texAccessor.linearSize(); - - prefix_sum_blur::Blur1D blur; - blur.radius = pc.radius; - blur.borderColor = float32_t4(0, 1, 0, 1); - - for (uint16_t ch=0; ch < CHANNELS; ch++) - for (uint16_t pass=0; pass < PASSES; pass++) - { - // its the `SharedMemoryProxy` that gets aliased and reused so we need to barrier on its memory - if (ch != 0 && pass != 0) - boxSampler.prefixSumAccessor.workgroupExecutionAndMemoryBarrier(); - blur(texAccessor, scanSmemAccessor, boxSampler, ch); - } - - texAccessor.store(); -} diff --git a/26_Blur/main.cpp b/26_Blur/main.cpp deleted file mode 100644 index 83cf140d6..000000000 --- a/26_Blur/main.cpp +++ /dev/null @@ -1,775 +0,0 @@ -// Copyright (C) 2024-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 - - -#include "nbl/examples/examples.hpp" - -#include -#include - -using namespace nbl; -using namespace nbl::core; -using namespace nbl::system; -using namespace nbl::asset; -using namespace nbl::ui; -using namespace nbl::video; -using namespace nbl::examples; - -#include "app_resources/common.hlsl" - - - -class BlurApp final : public SimpleWindowedApplication, public BuiltinResourcesApplication -{ - using device_base_t = SimpleWindowedApplication; - using asset_base_t = BuiltinResourcesApplication; - using clock_t = std::chrono::steady_clock; - - public: - inline BlurApp(const path& _localInputCWD, const path& _localOutputCWD, const path& _sharedInputCWD, const path& _sharedOutputCWD) - : IApplicationFramework(_localInputCWD, _localOutputCWD, _sharedInputCWD, _sharedOutputCWD) {} - - inline bool isComputeOnly() const override { return false; } - - // tired of packing and unpacking from float16_t, let some Junior do device traits / manual pack - virtual video::SPhysicalDeviceLimits getRequiredDeviceLimits() const override - { - auto retval = device_base_t::getRequiredDeviceLimits(); - retval.shaderSubgroupArithmetic = true; - retval.shaderFloat16 = true; - return retval; - } - - inline core::vector getSurfaces() const override - { - if (!m_surface) - { - { - IWindow::SCreationParams params = {}; - params.callback = core::make_smart_refctd_ptr(smart_refctd_ptr(m_inputSystem), smart_refctd_ptr(m_logger)); - // We resize the window later - params.width = 0; - params.height = 0; - params.x = 32; - params.y = 32; - params.flags = IWindow::ECF_BORDERLESS | IWindow::ECF_HIDDEN | IWindow::ECF_CAN_RESIZE; - params.windowCaption = "BlurApp"; - 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) = CSimpleResizeSurface::create(std::move(surface)); - } - - if (m_surface) - return { {m_surface->getSurface()/*,EQF_NONE*/} }; - - return {}; - } - - inline bool onAppInitialized(smart_refctd_ptr&& system) override - { - m_inputSystem = make_smart_refctd_ptr(logger_opt_smart_ptr(smart_refctd_ptr(m_logger))); - - if (!device_base_t::onAppInitialized(smart_refctd_ptr(system))) - return false; - if (!asset_base_t::onAppInitialized(std::move(system))) - return false; - - smart_refctd_ptr dsLayout; - { - const IGPUDescriptorSetLayout::SBinding bindings[2] = { - { - .binding = 0, - .type = IDescriptor::E_TYPE::ET_COMBINED_IMAGE_SAMPLER, - .createFlags = IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - .stageFlags = IShader::E_SHADER_STAGE::ESS_COMPUTE, - .count = 1 - }, - { - .binding = 1, - .type = IDescriptor::E_TYPE::ET_STORAGE_IMAGE, - .createFlags = IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - .stageFlags = IShader::E_SHADER_STAGE::ESS_COMPUTE, - .count = 1 - } - }; - dsLayout = m_device->createDescriptorSetLayout(bindings); - if (!dsLayout) - return logFail("Failed to Create Descriptor Layout"); - } - - core::smart_refctd_ptr cmdbuf; - auto queue = getGraphicsQueue(); - { - core::bitflag flags = static_cast(IGPUCommandPool::CREATE_FLAGS::TRANSIENT_BIT | IGPUCommandPool::CREATE_FLAGS::RESET_COMMAND_BUFFER_BIT); - smart_refctd_ptr cmdpool = m_device->createCommandPool(queue->getFamilyIndex(), flags); - if (!cmdpool->createCommandBuffers(IGPUCommandPool::BUFFER_LEVEL::PRIMARY, 1, &cmdbuf)) - return false; - } - - cmdbuf->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT); - auto converter = CAssetConverter::create({ .device = m_device.get() }); - - IQueue::SSubmitInfo::SCommandBufferInfo commandBufferInfo; - commandBufferInfo.cmdbuf = cmdbuf.get(); - - core::smart_refctd_ptr imgFillSemaphore = m_device->createSemaphore(0); - imgFillSemaphore->setObjectDebugName("Image Fill Semaphore"); - // scratch command buffers for asset converter transfer commands - SIntendedSubmitInfo transfer = { - .queue = queue, - .waitSemaphores = {}, - .prevCommandBuffers = {}, - .scratchCommandBuffers = { &commandBufferInfo, 1 }, - .scratchSemaphore = { - .semaphore = imgFillSemaphore.get(), - .value = 0, - // because of layout transitions - .stageMask = PIPELINE_STAGE_FLAGS::ALL_COMMANDS_BITS - } - }; - CAssetConverter::SConvertParams params = {}; - params.transfer = &transfer; - params.utilities = m_utils.get(); - - IAssetLoader::SAssetLoadParams lp; - SAssetBundle bundle = m_assetMgr->getAsset("../../media/color_space_test/R8G8B8_2.jpg", lp); - if (bundle.getContents().empty()) - logFail("Couldn't load an asset.", ILogger::ELL_ERROR); - - auto cpu_image = IAsset::castDown(bundle.getContents()[0]); - cpu_image->addImageUsageFlags(ICPUImage::E_USAGE_FLAGS::EUF_SAMPLED_BIT); - if (!cpu_image) - logFail("Failed to load image", ILogger::ELL_ERROR); - struct SInputs final : CAssetConverter::SInputs - { - // we also need to override this to have concurrent sharing - inline std::span getSharedOwnershipQueueFamilies(const size_t groupCopyID, const asset::ICPUImage* buffer, const CAssetConverter::patch_t& patch) const override - { - if (familyIndices.size() > 1) - return familyIndices; - return {}; - } - - inline uint8_t getMipLevelCount(const size_t groupCopyID, const ICPUImage* image, const CAssetConverter::patch_t& patch) const override - { - return image->getCreationParameters().mipLevels; - } - inline uint16_t needToRecomputeMips(const size_t groupCopyID, const ICPUImage* image, const CAssetConverter::patch_t& patch) const override - { - return 0b0u; - } - - std::vector familyIndices; - } inputs = {}; - CAssetConverter::patch_t patch(cpu_image.get()); - patch.mutableFormat = true; - std::get>(inputs.assets) = { &cpu_image.get(),1 }; - std::get>(inputs.patches) = { &patch, 1 }; - inputs.readCache = converter.get(); - inputs.logger = m_logger.get(); - { - const core::set uniqueFamilyIndices = { getTransferUpQueue()->getFamilyIndex(), getGraphicsQueue()->getFamilyIndex(), getComputeQueue()->getFamilyIndex() }; - inputs.familyIndices = { uniqueFamilyIndices.begin(),uniqueFamilyIndices.end() }; - } - // assert that we don't need to provide patches - assert(cpu_image->getImageUsageFlags().hasFlags(ICPUImage::E_USAGE_FLAGS::EUF_SAMPLED_BIT)); - auto reservation = converter->reserve(inputs); - // the `.value` is just a funny way to make the `smart_refctd_ptr` copyable - m_inputImg = reservation.getGPUObjects().front().value; - if (!m_inputImg) - logFail("Failed to convert image into an IGPUImage handle", ILogger::ELL_ERROR); - - // debug log about overflows - transfer.overflowCallback = [&](const ISemaphore::SWaitInfo&)->void - { - m_logger->log("Overflown when uploading image!\n", ILogger::ELL_PERFORMANCE); - }; - // and launch the conversions - auto result = reservation.convert(params); - if (!result.blocking() && result.copy() != IQueue::RESULT::SUCCESS) - logFail("Failed to record or submit conversions"); - - auto image_params = m_inputImg->getCreationParameters(); - - m_horzImg = m_device->createImage({ - { - .type = IGPUImage::E_TYPE::ET_2D, - .samples = IGPUImage::E_SAMPLE_COUNT_FLAGS::ESCF_1_BIT, - .format = E_FORMAT::EF_R8G8B8A8_UNORM, - .extent = image_params.extent, - .mipLevels = 1, - .arrayLayers = 1, - .usage = IGPUImage::E_USAGE_FLAGS::EUF_SAMPLED_BIT | IGPUImage::E_USAGE_FLAGS::EUF_TRANSFER_DST_BIT | IGPUImage::E_USAGE_FLAGS::EUF_TRANSFER_SRC_BIT | IGPUImage::E_USAGE_FLAGS::EUF_STORAGE_BIT - } - }); - // make sure we're always allocating from VRAM - auto reqs = m_horzImg->getMemoryReqs(); - reqs.memoryTypeBits &= m_physicalDevice->getDeviceLocalMemoryTypeBits(); - if (!m_horzImg || !m_device->allocate(reqs, m_horzImg.get()).isValid()) - return logFail("Could not create HDR Image"); - - m_vertImg = m_device->createImage({ - { - .type = IGPUImage::E_TYPE::ET_2D, - .samples = IGPUImage::E_SAMPLE_COUNT_FLAGS::ESCF_1_BIT, - .format = E_FORMAT::EF_R8G8B8A8_UNORM, - .extent = image_params.extent, - .mipLevels = 1, - .arrayLayers = 1, - .usage = IGPUImage::E_USAGE_FLAGS::EUF_TRANSFER_DST_BIT | IGPUImage::E_USAGE_FLAGS::EUF_TRANSFER_SRC_BIT | IGPUImage::E_USAGE_FLAGS::EUF_STORAGE_BIT - } - }); - reqs = m_horzImg->getMemoryReqs(); - reqs.memoryTypeBits &= m_physicalDevice->getDeviceLocalMemoryTypeBits(); - if (!m_vertImg || !m_device->allocate(reqs, m_vertImg.get()).isValid()) - return logFail("Could not create HDR Image"); - - smart_refctd_ptr shader; - { - IAssetLoader::SAssetLoadParams lp = {}; - lp.logger = m_logger.get(); - lp.workingDirectory = ""; // virtual root - auto assetBundle = m_assetMgr->getAsset("app_resources/shader.comp.hlsl", lp); - const auto assets = assetBundle.getContents(); - if (assets.empty()) - return logFail("Failed to load shader from disk"); - - // lets go straight from ICPUSpecializedShader to IGPUSpecializedShader - auto sourceRaw = IAsset::castDown(assets[0]); - if (!sourceRaw) - return logFail("Failed to load shader from disk"); - smart_refctd_ptr source = CHLSLCompiler::createOverridenCopy( - sourceRaw.get(), - "static const uint16_t WORKGROUP_SIZE = %d;\n" - "static const uint16_t MAX_SCANLINE_SIZE = %d;\n" - "static const uint16_t MAX_SUBGROUP_SIZE = %d;\n" - "static const uint16_t CHANNELS = %d;\n", - m_physicalDevice->getLimits().maxOptimallyResidentWorkgroupInvocations, - max(image_params.extent.width, image_params.extent.height), - m_physicalDevice->getLimits().maxSubgroupSize, - asset::getFormatChannelCount(image_params.format) - ); - -#ifndef _NBL_DEBUG - const ISPIRVOptimizer::E_OPTIMIZER_PASS optPasses[] = { - ISPIRVOptimizer::EOP_STRIP_DEBUG_INFO, - ISPIRVOptimizer::EOP_INLINE, - ISPIRVOptimizer::EOP_DEAD_BRANCH_ELIM, - ISPIRVOptimizer::EOP_DEAD_INSERT_ELIM, - ISPIRVOptimizer::EOP_IF_CONVERSION, - ISPIRVOptimizer::EOP_LOOP_INVARIANT_CODE_MOTION, - ISPIRVOptimizer::EOP_LOCAL_MULTI_STORE_ELIM - }; - auto opt = make_smart_refctd_ptr(optPasses); - shader = m_device->compileShader({ source.get(),opt.get() }); -#else - shader = m_device->compileShader({ source.get() }); -#endif - if (!shader) - return false; - } - - { - const asset::SPushConstantRange ranges[] = { { - .stageFlags = hlsl::ShaderStage::ESS_COMPUTE, - .offset = 0, - .size = sizeof(PushConstants) - } }; - auto layout = m_device->createPipelineLayout(ranges, smart_refctd_ptr(dsLayout)); - - IGPUComputePipeline::SCreationParams params = {}; - params.layout = layout.get(); - params.shader.shader = shader.get(); - params.shader.entryPoint = "main"; - params.cached.requireFullSubgroups = true; - if (!m_device->createComputePipelines(nullptr, { ¶ms, 1 }, &m_ppln)) - return logFail("Failed to create Pipeline"); - } - - smart_refctd_ptr progress = m_device->createSemaphore(0); - const IQueue::SSubmitInfo::SSemaphoreInfo signals[] = { { - .semaphore = progress.get(), - .value = 1, - // wait for the Copy Image to Buffer to finish before we signal - .stageMask = asset::PIPELINE_STAGE_FLAGS::COPY_BIT - } }; - - IQueue::SSubmitInfo submitInfos[1]; - IQueue::SSubmitInfo::SCommandBufferInfo cmdbufInfos[1] = { cmdbuf.get() }; - submitInfos[0].commandBuffers = cmdbufInfos; - submitInfos[0].signalSemaphores = signals; - - const IGPUImage::SSubresourceRange whole2DColorImage = - { - .aspectMask = IGPUImage::E_ASPECT_FLAGS::EAF_COLOR_BIT, - .baseMipLevel = 0, - .levelCount = 1, - .baseArrayLayer = 0, - .layerCount = 1 - }; - - using image_memory_barrier_t = IGPUCommandBuffer::SImageMemoryBarrier; - image_memory_barrier_t imgBarriers[] = { - { - .barrier = { - .dep = { - .srcStageMask = PIPELINE_STAGE_FLAGS::ALL_COMMANDS_BITS, - .srcAccessMask = ACCESS_FLAGS::MEMORY_WRITE_BITS, - .dstStageMask = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT, - .dstAccessMask = ACCESS_FLAGS::MEMORY_READ_BITS - } - }, - .image = m_horzImg.get(), - .subresourceRange = whole2DColorImage, - .oldLayout = IImage::LAYOUT::UNDEFINED, - .newLayout = IImage::LAYOUT::GENERAL - }, - { - .barrier = { - .dep = { - .srcStageMask = PIPELINE_STAGE_FLAGS::ALL_COMMANDS_BITS, - .srcAccessMask = ACCESS_FLAGS::MEMORY_WRITE_BITS, - .dstStageMask = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT, - .dstAccessMask = ACCESS_FLAGS::MEMORY_READ_BITS - } - }, - .image = m_vertImg.get(), - .subresourceRange = whole2DColorImage, - .oldLayout = IImage::LAYOUT::UNDEFINED, - .newLayout = IImage::LAYOUT::GENERAL - }, - { - .barrier = { - .dep = { - .srcStageMask = PIPELINE_STAGE_FLAGS::ALL_COMMANDS_BITS, - .srcAccessMask = ACCESS_FLAGS::MEMORY_WRITE_BITS, - .dstStageMask = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT, - .dstAccessMask = ACCESS_FLAGS::MEMORY_READ_BITS - } - }, - .image = m_inputImg.get(), - .subresourceRange = whole2DColorImage, - .oldLayout = IImage::LAYOUT::UNDEFINED, - .newLayout = IImage::LAYOUT::GENERAL - } - }; - - // clear the image - cmdbuf->reset({}); - cmdbuf->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT); - cmdbuf->pipelineBarrier(E_DEPENDENCY_FLAGS::EDF_NONE, { .memBarriers = {},.bufBarriers = {},.imgBarriers = {imgBarriers,3} }); - cmdbuf->end(); - queue->submit(submitInfos); - const ISemaphore::SWaitInfo waitInfos[1] = { { - .semaphore = progress.get(), - .value = 1 - } }; - m_device->blockForSemaphores(waitInfos); - - { - IGPUDescriptorSetLayout* const dsLayouts[] = {dsLayout.get(), dsLayout.get()}; - auto pool = m_device->createDescriptorPoolForDSLayouts(IDescriptorPool::E_CREATE_FLAGS::ECF_NONE, { dsLayouts, 2 }); - if (!pool) - return logFail("Could not create Descriptor Pool"); - - m_ds0 = pool->createDescriptorSet(smart_refctd_ptr(dsLayout)); - if (!m_ds0) - return logFail("Could not create Descriptor Set"); - m_ds1 = pool->createDescriptorSet(std::move(dsLayout)); - if (!m_ds1) - return logFail("Could not create Descriptor Set"); - - auto image_params = m_inputImg->getCreationParameters(); - IGPUDescriptorSet::SDescriptorInfo inputImgSampledInfo = {}; - { - inputImgSampledInfo.desc = m_device->createImageView({ - .flags = IGPUImageView::ECF_NONE, - .subUsages = IGPUImage::E_USAGE_FLAGS::EUF_SAMPLED_BIT, - .image = m_inputImg, - .viewType = IGPUImageView::E_TYPE::ET_2D, - .format = image_params.format, - }); - if (!inputImgSampledInfo.desc) - return logFail("Failed to create image view"); - inputImgSampledInfo.info.combinedImageSampler.imageLayout = IGPUImage::LAYOUT::GENERAL; - inputImgSampledInfo.info.combinedImageSampler.sampler = m_device->createSampler({}); - } - IGPUDescriptorSet::SDescriptorInfo horzImgStorageInfo = {}; - { - horzImgStorageInfo.desc = m_device->createImageView({ - .flags = IGPUImageView::ECF_NONE, - .subUsages = IGPUImage::E_USAGE_FLAGS::EUF_STORAGE_BIT, - .image = m_horzImg, - .viewType = IGPUImageView::E_TYPE::ET_2D, - .format = E_FORMAT::EF_R8G8B8A8_UNORM - }); - if (!horzImgStorageInfo.desc) - return logFail("Failed to create image view"); - horzImgStorageInfo.info.image.imageLayout = IGPUImage::LAYOUT::GENERAL; - } - IGPUDescriptorSet::SDescriptorInfo horzImgSampledInfo = {}; - { - horzImgSampledInfo.desc = m_device->createImageView({ - .flags = IGPUImageView::ECF_NONE, - .subUsages = IGPUImage::E_USAGE_FLAGS::EUF_SAMPLED_BIT, - .image = m_horzImg, - .viewType = IGPUImageView::E_TYPE::ET_2D, - .format = E_FORMAT::EF_R8G8B8A8_UNORM - }); - if (!horzImgSampledInfo.desc) - return logFail("Failed to create image view"); - horzImgSampledInfo.info.combinedImageSampler.imageLayout = IGPUImage::LAYOUT::GENERAL; - horzImgSampledInfo.info.combinedImageSampler.sampler = m_device->createSampler({}); - } - IGPUDescriptorSet::SDescriptorInfo vertImgStorageInfo = {}; - { - vertImgStorageInfo.desc = m_device->createImageView({ - .flags = IGPUImageView::ECF_NONE, - .subUsages = IGPUImage::E_USAGE_FLAGS::EUF_STORAGE_BIT, - .image = m_vertImg, - .viewType = IGPUImageView::E_TYPE::ET_2D, - .format = E_FORMAT::EF_R8G8B8A8_UNORM - }); - if (!vertImgStorageInfo.desc) - return logFail("Failed to create image view"); - vertImgStorageInfo.info.image.imageLayout = IGPUImage::LAYOUT::GENERAL; - } - - const IGPUDescriptorSet::SWriteDescriptorSet writes[] = { - { - .dstSet = m_ds0.get(), - .binding = 0, - .arrayElement = 0, - .count = 1, - .info = &inputImgSampledInfo - }, - { - .dstSet = m_ds0.get(), - .binding = 1, - .arrayElement = 0, - .count = 1, - .info = &horzImgStorageInfo - }, - { - .dstSet = m_ds1.get(), - .binding = 0, - .arrayElement = 0, - .count = 1, - .info = &horzImgSampledInfo - }, - { - .dstSet = m_ds1.get(), - .binding = 1, - .arrayElement = 0, - .count = 1, - .info = &vertImgStorageInfo - } - }; - if (!m_device->updateDescriptorSets(writes,{})) - return logFail("Failed to write descriptor set"); - } - - m_semaphore = m_device->createSemaphore(m_realFrameIx); - if (!m_semaphore) - return logFail("Failed to Create a Semaphore!"); - - ISwapchain::SCreationParams swapchainParams = { .surface = m_surface->getSurface() }; - if (!swapchainParams.deduceFormat(m_physicalDevice)) - return logFail("Could not choose a Surface Format for the Swapchain!"); - - auto gQueue = getGraphicsQueue(); - if (!m_surface || !m_surface->init(gQueue, std::make_unique(), swapchainParams.sharedParams)) - return logFail("Could not create Window & Surface or initialize the Surface!"); - - auto pool = m_device->createCommandPool(gQueue->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!"); - } - - m_winMgr->setWindowSize(m_window.get(), image_params.extent.width, image_params.extent.height); - m_surface->recreateSwapchain(); - - auto assetManager = make_smart_refctd_ptr(smart_refctd_ptr(system)); - - m_winMgr->show(m_window.get()); - - return true; - } - - inline void workLoopBody() override - { - // framesInFlight: ensuring safe execution of command buffers and acquires, `framesInFlight` only affect semaphore waits, don't use this to index your resources because it can change with swapchain recreation. - const uint32_t framesInFlight = core::min(MaxFramesInFlight, m_surface->getMaxAcquiresInFlight()); - // We block for semaphores for 2 reasons here: - // A) Resource: Can't use resource like a command buffer BEFORE previous use is finished! [MaxFramesInFlight] - // B) Acquire: Can't have more acquires in flight than a certain threshold returned by swapchain or your surface helper class. [MaxAcquiresInFlight] - if (m_realFrameIx >= framesInFlight) - { - const ISemaphore::SWaitInfo cbDonePending[] = - { - { - .semaphore = m_semaphore.get(), - .value = m_realFrameIx + 1 - framesInFlight - } - }; - if (m_device->blockForSemaphores(cbDonePending) != ISemaphore::WAIT_RESULT::SUCCESS) - return; - } - - const auto resourceIx = m_realFrameIx % MaxFramesInFlight; - - auto image_params = m_inputImg->getCreationParameters(); - m_inputSystem->getDefaultMouse(&mouse); - mouse.consumeEvents([&](const IMouseEventChannel::range_t& events) -> void { - for (auto eventIt = events.begin(); eventIt != events.end(); eventIt++) - { - auto ev = *eventIt; - if (ev.type == nbl::ui::SMouseEvent::EET_SCROLL) - { - blurRadius = std::clamp(blurRadius + 5 * core::sign(ev.scrollEvent.verticalScroll), - std::numeric_limits::min(), - max(image_params.extent.width, image_params.extent.height)); - m_logger->log("Radius changed: %f", ILogger::ELL_DEBUG, blurRadius); - } - 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_RELEASED) - { - blurEdgeWrapMode = (blurEdgeWrapMode + 1) % ISampler::E_TEXTURE_CLAMP::ETC_COUNT; - m_logger->log("Edge wrapping mode changed: %d", ILogger::ELL_DEBUG, blurEdgeWrapMode); - } - } - } - }, m_logger.get()); - - m_currentImageAcquire = m_surface->acquireNextImage(); - if (!m_currentImageAcquire) - return; - - auto* const cb = m_cmdBufs.data()[resourceIx].get(); - cb->reset(IGPUCommandBuffer::RESET_FLAGS::RELEASE_RESOURCES_BIT); - cb->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT); - - const IGPUImage::SSubresourceRange whole2DColorImage = - { - .aspectMask = IGPUImage::E_ASPECT_FLAGS::EAF_COLOR_BIT, - .baseMipLevel = 0, - .levelCount = 1, - .baseArrayLayer = 0, - .layerCount = 1 - }; - - using image_memory_barrier_t = IGPUCommandBuffer::SImageMemoryBarrier; - image_memory_barrier_t vertImgBarrier = { - .barrier = { - .dep = { - .srcStageMask = PIPELINE_STAGE_FLAGS::ALL_COMMANDS_BITS, - .srcAccessMask = ACCESS_FLAGS::STORAGE_WRITE_BIT, - .dstStageMask = PIPELINE_STAGE_FLAGS::CLEAR_BIT, - .dstAccessMask = ACCESS_FLAGS::TRANSFER_WRITE_BIT - } - }, - .image = m_vertImg.get(), - .subresourceRange = whole2DColorImage, - .oldLayout = IImage::LAYOUT::GENERAL, - .newLayout = IImage::LAYOUT::GENERAL - }; - - // clear the image - cb->pipelineBarrier(E_DEPENDENCY_FLAGS::EDF_NONE,{.memBarriers={},.bufBarriers={},.imgBarriers={&vertImgBarrier,1}}); - { - const IGPUCommandBuffer::SClearColorValue color = { - .float32 = {0,0,0,1} - }; - const IGPUImage::SSubresourceRange range = { - .aspectMask = IGPUImage::E_ASPECT_FLAGS::EAF_COLOR_BIT, - .baseMipLevel = 0, - .levelCount = 1, - .baseArrayLayer = 0, - .layerCount = 1 - }; - cb->clearColorImage(m_vertImg.get(),IGPUImage::LAYOUT::GENERAL,&color,1,&range); - // now we stay in same layout for remainder of the frame - vertImgBarrier.oldLayout = IImage::LAYOUT::GENERAL; - } - - const SMemoryBarrier computeToBlit = { - .srcStageMask = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT, - .srcAccessMask = ACCESS_FLAGS::STORAGE_WRITE_BIT|ACCESS_FLAGS::STORAGE_READ_BIT, - .dstStageMask = PIPELINE_STAGE_FLAGS::BLIT_BIT, - .dstAccessMask = ACCESS_FLAGS::TRANSFER_WRITE_BIT - }; - - auto& imgDep = vertImgBarrier.barrier.dep; - // use the "generate a barrier between the one before and after" API - imgDep = imgDep.nextBarrier(computeToBlit); - cb->pipelineBarrier(E_DEPENDENCY_FLAGS::EDF_NONE,{.memBarriers={},.bufBarriers={},.imgBarriers={&vertImgBarrier,1}}); - - // write the image - { - cb->bindComputePipeline(m_ppln.get()); - auto* layout = m_ppln->getLayout(); - - cb->pipelineBarrier(E_DEPENDENCY_FLAGS::EDF_NONE, { .memBarriers = {}, .bufBarriers = {},.imgBarriers = {&vertImgBarrier,1} }); - cb->bindDescriptorSets(E_PIPELINE_BIND_POINT::EPBP_COMPUTE, layout, 0, 1, &m_ds0.get()); - PushConstants pc = { .radius = blurRadius, .activeAxis = 0, .edgeWrapMode = blurEdgeWrapMode }; - cb->pushConstants(layout, hlsl::ShaderStage::ESS_COMPUTE, 0, sizeof(pc), &pc); - cb->dispatch(image_params.extent.height, 1, 1); - - image_memory_barrier_t horzImgBarrier = { - .barrier = { - .dep = { - .srcStageMask = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT, - .srcAccessMask = ACCESS_FLAGS::STORAGE_READ_BIT | ACCESS_FLAGS::STORAGE_WRITE_BIT, - .dstStageMask = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT, - .dstAccessMask = ACCESS_FLAGS::STORAGE_READ_BIT | ACCESS_FLAGS::STORAGE_WRITE_BIT, - } - }, - .image = m_horzImg.get(), - .subresourceRange = whole2DColorImage, - .oldLayout = IImage::LAYOUT::GENERAL, - .newLayout = IImage::LAYOUT::GENERAL - }; - cb->pipelineBarrier(E_DEPENDENCY_FLAGS::EDF_NONE, { .memBarriers = {}, .bufBarriers = {},.imgBarriers = {&horzImgBarrier,1} }); - cb->bindDescriptorSets(E_PIPELINE_BIND_POINT::EPBP_COMPUTE, layout, 0, 1, &m_ds1.get()); - pc.activeAxis = 1; - cb->pushConstants(layout, hlsl::ShaderStage::ESS_COMPUTE, 0, sizeof(pc), &pc); - cb->dispatch(image_params.extent.width, 1, 1); - } - - { - auto swapImg = m_surface->getSwapchainResources()->getImage(m_currentImageAcquire.imageIndex); - auto swapImgParams = swapImg->getCreationParameters(); - imgDep = computeToBlit; - // special case, the swapchain is a NONE stage with NONE accesses - image_memory_barrier_t imgBarriers[] = { - vertImgBarrier, - { - .barrier = { - .dep = { - .srcStageMask = PIPELINE_STAGE_FLAGS::NONE, - .srcAccessMask = ACCESS_FLAGS::NONE, - .dstStageMask = PIPELINE_STAGE_FLAGS::BLIT_BIT, - .dstAccessMask = ACCESS_FLAGS::TRANSFER_WRITE_BIT - } - }, - .image = swapImg, - .subresourceRange = whole2DColorImage, - .oldLayout = IImage::LAYOUT::UNDEFINED, // don't care about old contents - .newLayout = IImage::LAYOUT::TRANSFER_DST_OPTIMAL - } - }; - cb->pipelineBarrier(E_DEPENDENCY_FLAGS::EDF_NONE,{.memBarriers={},.bufBarriers={},.imgBarriers=imgBarriers}); - - const IGPUCommandBuffer::SImageBlit regions[] = {{ - .srcMinCoord = {0,0,0}, - .srcMaxCoord = {image_params.extent.width, image_params.extent.height,1}, - .dstMinCoord = {0,0,0}, - .dstMaxCoord = {swapImgParams.extent.width, swapImgParams.extent.height,1}, - .layerCount = 1, - .srcBaseLayer = 0, - .dstBaseLayer = 0, - .srcMipLevel = 0, - .dstMipLevel = 0, - .aspectMask = IGPUImage::E_ASPECT_FLAGS::EAF_COLOR_BIT - }}; - cb->blitImage(m_vertImg.get(),IGPUImage::LAYOUT::GENERAL,swapImg,IGPUImage::LAYOUT::TRANSFER_DST_OPTIMAL,regions,IGPUSampler::ETF_NEAREST); - - auto& swapImageBarrier = imgBarriers[1]; - swapImageBarrier.barrier.dep = swapImageBarrier.barrier.dep.nextBarrier(PIPELINE_STAGE_FLAGS::NONE,ACCESS_FLAGS::NONE); - swapImageBarrier.oldLayout = imgBarriers[1].newLayout; - swapImageBarrier.newLayout = IGPUImage::LAYOUT::PRESENT_SRC; - cb->pipelineBarrier(E_DEPENDENCY_FLAGS::EDF_NONE,{.memBarriers={},.bufBarriers={},.imgBarriers={&swapImageBarrier,1}}); - } - - cb->end(); - - { - const IQueue::SSubmitInfo::SSemaphoreInfo rendered[] = - { - { - .semaphore = m_semaphore.get(), - .value = ++m_realFrameIx, - .stageMask = PIPELINE_STAGE_FLAGS::ALL_COMMANDS_BITS // because of the layout transition of the swapchain image - } - }; - { - { - const IQueue::SSubmitInfo::SCommandBufferInfo commandBuffers[] = {{.cmdbuf = cb }}; - - const IQueue::SSubmitInfo::SSemaphoreInfo acquired[] = - { - { - .semaphore = m_currentImageAcquire.semaphore, - .value = m_currentImageAcquire.acquireCount, - .stageMask = PIPELINE_STAGE_FLAGS::NONE - } - }; - const IQueue::SSubmitInfo infos[] = - { - { - .waitSemaphores = acquired, - .commandBuffers = commandBuffers, - .signalSemaphores = rendered - } - }; - - if (getGraphicsQueue()->submit(infos) == IQueue::RESULT::SUCCESS) - { - const ISemaphore::SWaitInfo waitInfos[] = {{ .semaphore = m_semaphore.get(), .value = m_realFrameIx }}; - m_device->blockForSemaphores(waitInfos); // this is not solution, quick wa to not throw validation errors - } - else - --m_realFrameIx; - } - } - - m_surface->present(m_currentImageAcquire.imageIndex,rendered); - } - } - - inline bool keepRunning() override - { - if (m_surface->irrecoverable()) - return false; - - return true; - } - - inline bool onAppTerminated() override - { - return device_base_t::onAppTerminated(); - } - - private: - // 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; - smart_refctd_ptr m_window; - smart_refctd_ptr> m_surface; - - core::smart_refctd_ptr m_inputSystem; - InputSystem::ChannelReader mouse; - - hlsl::float32_t blurRadius = 6; - uint16_t blurEdgeWrapMode = ISampler::E_TEXTURE_CLAMP::ETC_REPEAT; - - smart_refctd_ptr m_ppln; - smart_refctd_ptr m_ds0; - smart_refctd_ptr m_ds1; - smart_refctd_ptr m_inputImg; - smart_refctd_ptr m_horzImg; - smart_refctd_ptr m_vertImg; - smart_refctd_ptr m_semaphore; - - uint64_t m_realFrameIx = 0; - std::array,MaxFramesInFlight> m_cmdBufs; - ISimpleManagedSurface::SAcquireResult m_currentImageAcquire = {}; -}; - -NBL_MAIN_FUNC(BlurApp) \ No newline at end of file diff --git a/27_MPMCScheduler/CMakeLists.txt b/27_MPMCScheduler/CMakeLists.txt index 7d7cfd71c..a434ff32a 100644 --- a/27_MPMCScheduler/CMakeLists.txt +++ b/27_MPMCScheduler/CMakeLists.txt @@ -1,36 +1,24 @@ -include(common) +include(common RESULT_VARIABLE RES) +if(NOT RES) + message(FATAL_ERROR "common.cmake not found. Should be in {repo_root}/cmake directory") +endif() -nbl_create_executable_project("" "" "" "") +nbl_create_executable_project("" "" "" "" "${NBL_EXECUTABLE_PROJECT_CREATION_PCH_TARGET}") -set(OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/auto-gen") +if(NBL_EMBED_BUILTIN_RESOURCES) + set(_BR_TARGET_ ${EXECUTABLE_NAME}_builtinResourceData) + set(RESOURCE_DIR "app_resources") -set(JSON [=[ -[ - { - "INPUT": "app_resources/shader.comp.hlsl", - "KEY": "shader", - "COMPILE_OPTIONS": ["-T", "cs_6_8"], - "CAPS": [] - } -] -]=]) + get_filename_component(_SEARCH_DIRECTORIES_ "${CMAKE_CURRENT_SOURCE_DIR}" ABSOLUTE) + get_filename_component(_OUTPUT_DIRECTORY_SOURCE_ "${CMAKE_CURRENT_BINARY_DIR}/src" ABSOLUTE) + get_filename_component(_OUTPUT_DIRECTORY_HEADER_ "${CMAKE_CURRENT_BINARY_DIR}/include" ABSOLUTE) -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 -I ${CMAKE_CURRENT_SOURCE_DIR} - OUTPUT_VAR KEYS - INCLUDE nbl/this_example/builtin/build/spirv/keys.hpp - NAMESPACE nbl::this_example::builtin::build - INPUTS ${JSON} -) + file(GLOB_RECURSE BUILTIN_RESOURCE_FILES RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}/${RESOURCE_DIR}" CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/${RESOURCE_DIR}/*") + foreach(RES_FILE ${BUILTIN_RESOURCE_FILES}) + LIST_BUILTIN_RESOURCE(RESOURCES_TO_EMBED "${RES_FILE}") + endforeach() -NBL_CREATE_RESOURCE_ARCHIVE( - NAMESPACE nbl::this_example::builtin::build - TARGET ${EXECUTABLE_NAME}_builtinsBuild - LINK_TO ${EXECUTABLE_NAME} - BIND ${OUTPUT_DIRECTORY} - BUILTINS ${KEYS} -) + ADD_CUSTOM_BUILTIN_RESOURCES(${_BR_TARGET_} RESOURCES_TO_EMBED "${_SEARCH_DIRECTORIES_}" "${RESOURCE_DIR}" "nbl::this_example::builtin" "${_OUTPUT_DIRECTORY_HEADER_}" "${_OUTPUT_DIRECTORY_SOURCE_}") + + LINK_BUILTIN_RESOURCES_TO_TARGET(${EXECUTABLE_NAME} ${_BR_TARGET_}) +endif() \ No newline at end of file diff --git a/27_MPMCScheduler/app_resources/common.hlsl b/27_MPMCScheduler/app_resources/common.hlsl index 2783f13a2..259d5069d 100644 --- a/27_MPMCScheduler/app_resources/common.hlsl +++ b/27_MPMCScheduler/app_resources/common.hlsl @@ -1,11 +1,22 @@ #include "nbl/builtin/hlsl/cpp_compat.hlsl" -NBL_CONSTEXPR_INLINE_NSPC_SCOPE_VAR uint32_t WorkgroupSizeX = 8; -NBL_CONSTEXPR_INLINE_NSPC_SCOPE_VAR uint32_t WorkgroupSizeY = 8; -NBL_CONSTEXPR_INLINE_NSPC_SCOPE_VAR uint32_t WorkgroupSize = WorkgroupSizeX*WorkgroupSizeY; +NBL_CONSTEXPR uint32_t WorkgroupSizeX = 16; +NBL_CONSTEXPR uint32_t WorkgroupSizeY = 16; +NBL_CONSTEXPR uint32_t WorkgroupSize = WorkgroupSizeX*WorkgroupSizeY; + +static const uint32_t FRAMES_IN_FLIGHT = 3u; + +static const uint32_t RED_OFFSET = 0u; +static const uint32_t GREEN_OFFSET = 256u; +static const uint32_t BLUE_OFFSET = 256u * 2u; + +static const uint32_t CHANEL_CNT = 3; +static const uint32_t VAL_PER_CHANEL_CNT = 256; +static const uint32_t HISTOGRAM_SIZE = CHANEL_CNT * VAL_PER_CHANEL_CNT; +static const uint32_t HISTOGRAM_BYTE_SIZE = HISTOGRAM_SIZE * sizeof(uint32_t); +static const uint32_t COMBINED_HISTOGRAM_BUFFER_BYTE_SIZE = HISTOGRAM_BYTE_SIZE * FRAMES_IN_FLIGHT; struct PushConstants { - uint32_t sharedAcceptableIdleCount : 10; - uint32_t globalAcceptableIdleCount : 10; + uint32_t histogramBufferOffset; }; \ No newline at end of file diff --git a/27_MPMCScheduler/app_resources/schedulers/mpmc.hlsl b/27_MPMCScheduler/app_resources/schedulers/mpmc.hlsl index 836c91576..2e345bd3e 100644 --- a/27_MPMCScheduler/app_resources/schedulers/mpmc.hlsl +++ b/27_MPMCScheduler/app_resources/schedulers/mpmc.hlsl @@ -1,8 +1,8 @@ -#ifndef _NBL_HLSL_SCHEDULERS_MPMC_HLSL_ -#define _NBL_HLSL_SCHEDULERS_MPMC_HLSL_ +#ifndef _NBL_HLSL_MPMC_HLSL_ +#define _NBL_HLSL_MPMC_HLSL_ -//#include "app_resources/workgroup/stack.hlsl" -//#include "app_resources/mpmc_queue.hlsl" +#include "workgroup/stack.hlsl" +#include "mpmc_queue.hlsl" #include "nbl/builtin/hlsl/workgroup/scratch_size.hlsl" #include "nbl/builtin/hlsl/workgroup/arithmetic.hlsl" @@ -16,7 +16,7 @@ namespace schedulers { // TODO: improve and use a Global Pool Allocator and stop moving whole payloads around in VRAM -template +template struct MPMC { // TODO: static asset that the signature of the `Task::operator()` is `void()` @@ -29,20 +29,17 @@ struct MPMC // already stole some work, need to spill if (nextValid) { -#if 0 // if the shared memory stack will overflow if (!sStack.push(payload)) { // spill to a global queue gQueue.push(payload); } -#endif } else next = payload; } -#if 0 // returns if there's any invocation at all that wants to pop uint16_t popCountInclusive_impl(out uint16_t reduction) { @@ -64,7 +61,6 @@ struct MPMC sStack.accessor.get(PopCountOffset,reduction); return retval; } -#endif void operator()() { @@ -79,7 +75,6 @@ struct MPMC nextValid = false; tmp(); } -#if 0 // everyone sync up here so we can count how many invocations won't have jobs glsl::barrier(); uint16_t popCountInclusive = popCountInclusive_impl(popCount); @@ -96,14 +91,11 @@ struct MPMC gQueue.pop(sStack.accessor,!nextValid,next,popCountInclusive,lastInvocationInGroup,0); } } -#else - popCount = 0; -#endif } } -// MPMCQueue gQueue; -// workgroup::Stack sStack; + MPMCQueue gQueue; + workgroup::Stack sStack; Task next; // popping work from the stack and queue might be expensive, expensive enough to not justify doing all the legwork to just pull a few items of work uint16_t sharedAcceptableIdleCount; @@ -111,6 +103,9 @@ struct MPMC bool nextValid; }; +} +} + } } } diff --git a/27_MPMCScheduler/app_resources/shader.comp.hlsl b/27_MPMCScheduler/app_resources/shader.comp.hlsl index 3055ad618..a58cef874 100644 --- a/27_MPMCScheduler/app_resources/shader.comp.hlsl +++ b/27_MPMCScheduler/app_resources/shader.comp.hlsl @@ -1,14 +1,11 @@ //#include "nbl/builtin/hlsl/memory_accessor.hlsl" +//#include "nbl/builtin/hlsl/type_traits.hlsl" -#include "app_resources/common.hlsl" +#include "schedulers/mpmc.hlsl" #include "nbl/builtin/hlsl/limits.hlsl" #include "nbl/builtin/hlsl/numbers.hlsl" - -using namespace nbl::hlsl; - -// Scene enum Material : uint32_t { Emission = 0, @@ -19,32 +16,18 @@ struct Sphere { static const uint32_t MaxColorValue = 1023; - float16_t3 getColor() - { - return float16_t3(R,G,B)/float16_t3(MaxColorValue,MaxColorValue,MaxColorValue); - } - - float32_t intersect(const float32_t3 rayOrigin, const float32_t3 rayDir) + float32_t3 getColor() { - float32_t3 relOrigin = rayOrigin - position; - float32_t relOriginLen2 = dot(relOrigin,relOrigin); - - float32_t dirDotRelOrigin = dot(rayDir,relOrigin); - float32_t det = radius2 - relOriginLen2 + dirDotRelOrigin * dirDotRelOrigin; - - // do some speculative math here - float32_t detsqrt = sqrt(det); - return -dirDotRelOrigin + (relOriginLen2 > radius2 ? (-detsqrt) : detsqrt); + return float32_t3(R,G,B)/float32_t3(MaxColorValue,MaxColorValue,MaxColorValue); } float32_t3 position; - float32_t radius2; + float32_t radius; uint32_t R : 10; uint32_t G : 10; uint32_t B : 10; - uint32_t material : 2; + Material material : 2; }; - const static uint32_t SphereCount = 5; const static Sphere spheres[5] = { { @@ -56,7 +39,7 @@ const static Sphere spheres[5] = { Material::Emission }, { - float32_t3(-1,1,0), + float32_t3(-2,3,0), 0.6f, Sphere::MaxColorValue, Sphere::MaxColorValue, @@ -64,8 +47,8 @@ const static Sphere spheres[5] = { Material::Metal }, { - float32_t3(1,1,0), - 0.8f, + float32_t3(2,3,0), + 0.4f, 0, Sphere::MaxColorValue, Sphere::MaxColorValue, @@ -73,7 +56,7 @@ const static Sphere spheres[5] = { }, // Glass balls need to be monochromatic, cause I didn't do RGB in my task payload { - float32_t3(-2,3,0), + float32_t3(-1,1,0), 0.7f, Sphere::MaxColorValue, Sphere::MaxColorValue, @@ -81,7 +64,7 @@ const static Sphere spheres[5] = { Material::Glass }, { - float32_t3(2,3,0), + float32_t3(-1,1,0), 0.7f, 0, Sphere::MaxColorValue/2, @@ -90,42 +73,62 @@ const static Sphere spheres[5] = { } }; -#include "nbl/builtin/hlsl/format/octahedral.hlsl" - -// Payload and Executor for our Task-Graph struct WhittedTask { - static const uint32_t MaxDepth = (1<<5)-1; - - using dir_t = format::octahedral; + static const uint32_t MaxDepth = (1<<2)-1; + static const uint32_t MaxTheta = (1<<19)-1; + static const uint32_t MaxPhi = (1<<20)-1; float32_t3 origin; - dir_t dir; - float16_t3 throughput; - float16_t3 contribution; + uint32_t throughputR : 11; + uint32_t throughputG : 11; + uint32_t throughputB : 10; // - uint32_t outputX : 14; - uint32_t outputY : 13; - uint32_t depth : 5; + uint64_t outputX : 12; + uint64_t outputY : 11; + uint64_t dirTheta : 19; + uint64_t dirPhi : 20; + uint64_t depth : 2; - void setRayDir(float32_t3 _dir) + void setThroughput(const float32_t3 col) { - dir = _static_cast(_dir); + throughputR = uint32_t(col.r*2047.f+0.4f); + throughputG = uint32_t(col.g*2047.f+0.4f); + throughputB = uint32_t(col.b*1023.f+0.4f); } - float32_t3 getRayDir() + float32_t3 getThroughput() { - return _static_cast(dir); + return float32_t3(throughputR,throughputG,throughputB)/float32_t3(2047,2047,1023); } - // yay workaround for https://github.com/microsoft/DirectXShaderCompiler/issues/6973 - void __impl_call(); + void setRayDir(float32_t3 dir) + { + const float32_t pi = nbl::hlsl::numbers::pi; + dirTheta = acos(dir.z)*float32_t(MaxTheta)/pi+0.5f; + // rely on integer wraparound to map (-pi,0) to [UINT_MAX,INT_MAX) + dirPhi = uint32_t(floor(atan2(dir.x,dir.y)*float32_t(MaxPhi)/pi+0.5f)); + } + float32_t3 getRayDir() + { + float32_t3 dir; + const float32_t pi = nbl::hlsl::numbers::pi; + dir.z = cos(float32_t(dirTheta)*pi/float32_t(MaxTheta)); + // shtuff + { + const float32_t phi = float32_t(dirPhi)/float32_t(MaxPhi); + dir.xy = float32_t2(cos(phi),sin(phi)); + } + return dir; + } - void operator()() {__impl_call();} + void operator()(); }; -//NBL_REGISTER_OBJ_TYPE(WhittedTask,8); +NBL_REGISTER_OBJ_TYPE(WhittedTask,8); +struct GlobalAccessor +{ +}; // something something, Nvidia can do 32 bytes of smem per invocation -#include "nbl/builtin/hlsl/glsl_compat/core.hlsl" groupshared uint32_t sdata[512]; struct SharedAccessor { @@ -145,23 +148,18 @@ struct SharedAccessor return nbl::hlsl::glsl::atomicAdd(sdata[ix],val); } - void set(const uint32_t ix, const in uint32_t val) + template + void set(const uint32_t ix, const in T val) { - sdata[ix] = val; +// sdata[ix] = val; } - void get(const uint32_t ix, out uint32_t val) + template + void get(const uint32_t ix, out T val) { - val = sdata[ix]; +// sdata[ix] = val; } }; - -// -#include "app_resources/schedulers/mpmc.hlsl" -struct SubgroupCaps -{ - NBL_CONSTEXPR_STATIC_INLINE bool shaderSubgroupArithmetic = true; -}; -static nbl::hlsl::schedulers::MPMC scheduler; +static nbl::hlsl::MPMCScheduler scheduler; // stolen from Nabla GLSL bool nbl_glsl_getOrientedEtas(out float orientedEta, out float rcpOrientedEta, in float NdotI, in float eta) @@ -194,14 +192,12 @@ float32_t3 nbl_glsl_refract(in float32_t3 I, in float32_t3 N, in bool backside, return N*(NdotI*rcpOrientedEta + NdotT) - rcpOrientedEta*I; } - - -#include "nbl/builtin/hlsl/format/shared_exp.hlsl" -[[vk::binding(0,0)]] RWTexture2D framebuffer; - -void WhittedTask::__impl_call() +void WhittedTask::operator()() { + using namespace nbl::hlsl; + const float32_t3 rayDir = getRayDir(); + const float32_t3 throughput = getThroughput(); // intersect with spheres uint32_t closestIx = SphereCount; @@ -211,22 +207,18 @@ void WhittedTask::__impl_call() for (uint32_t i=0; i0 && d0.f ? float32_t3(0.1,0.7,0.03):float32_t3(0.05,0.25,1.0)); if (contribution.r+contribution.g+contribution.b<1.f/2047.f) return; - const uint32_t2 output = uint32_t2(outputX,outputY); - // CAS loops on R32_UINT view of RGB9E5 because there's no atomic add (only NV has vector half float atomics, but RGB9E5 not storable) - using rgb9e5_t = format::shared_exp; - rgb9e5_t actual,expected; - // assume image is empty, good assumption always true once - actual.storage = 0; - do - { - expected = actual; - rgb9e5_t newVal = _static_cast(_static_cast(expected)+float32_t3(contribution)); - InterlockedCompareExchange(framebuffer[output],expected.storage,newVal.storage,actual.storage); - } while (expected!=actual); + // Use device traits to do CAS loops on R32_UINT view of RGB9E5 when no VK_NV_shader_atomic_float16_vector +// spirv::atomicAdd(spirv::addrof(framebuffer),contribution); + framebuffer[uint32_t2(outputX,outputY)] = float32_t4(contribution,1.f); } -[[vk::push_constant]] PushConstants pc; - -// have to do weird stuff with workgroup size because of subgroup full spec namespace nbl { namespace hlsl { namespace glsl { -uint32_t3 gl_WorkGroupSize() {return uint32_t3(WorkgroupSizeX*WorkgroupSizeY,1,1);} +uint32_t3 gl_WorkGroupSize() {return uint32_t3(8,8,1);} } } } -[numthreads(WorkgroupSizeX*WorkgroupSizeY,1,1)] -[shader("compute")] -void main() +[numthreads(8,8,1)] +void main(uint32_t3 gl_GlobalInvocationID : SV_DispatchThreadID) { // manually push an explicit workload { - // reconstruct the actual XY coordinate we want - const uint32_t2 VirtualWorkgroupSize = uint32_t2(WorkgroupSizeX,WorkgroupSizeY); - uint32_t2 GlobalInvocationID = glsl::gl_WorkGroupID().xy*VirtualWorkgroupSize; - // TODO: morton code - { - const uint32_t linearIx = glsl::gl_LocalInvocationIndex(); - GlobalInvocationID.x += linearIx%WorkgroupSizeX; - GlobalInvocationID.y += linearIx/WorkgroupSizeX; - } - scheduler.next.origin = float32_t3(0,2.5,6); - scheduler.next.throughput = float16_t3(1,1,1); - scheduler.next.contribution = float16_t3(0,0,0); - scheduler.next.outputX = GlobalInvocationID.x; - scheduler.next.outputY = GlobalInvocationID.y; + scheduler.next.origin = float32_t3(0,0,-5); + scheduler.next.setThroughput(float32_t3(1,1,1)); + scheduler.next.outputX = gl_GlobalInvocationID.x; + scheduler.next.outputY = gl_GlobalInvocationID.y; { using namespace nbl::hlsl; float32_t3 ndc; { - const float32_t2 totalInvocations = glsl::gl_NumWorkGroups().xy*VirtualWorkgroupSize; - ndc.xy = float32_t2(GlobalInvocationID.xy)*float32_t2(2,-2)/totalInvocations+float32_t2(-1.f,1.f)+float32_t2(1,1)/totalInvocations; + const float32_t2 totalInvocations = glsl::gl_NumWorkGroups().xy*8.f; + ndc.xy = (float32_t2(gl_GlobalInvocationID.xy)+float32_t2(0.5,0.5))*2.f/totalInvocations-float32_t2(1,1); ndc.y *= totalInvocations.y/totalInvocations.x; // aspect raio } - ndc.z = -1.f; // FOV of 90 degrees - scheduler.next.setRayDir(ndc); + ndc.z = 1.f; // FOV of 90 degrees + scheduler.next.setRayDir(normalize(ndc)); } scheduler.next.depth = 0; - scheduler.sharedAcceptableIdleCount = 0; - scheduler.globalAcceptableIdleCount = 0; scheduler.nextValid = true; } // excute implcit as scheduled scheduler(); +#ifdef DEBUG + printf("Workgroup Quit"); +#endif } diff --git a/27_MPMCScheduler/app_resources/workgroup/pool_allocator.hlsl b/27_MPMCScheduler/app_resources/workgroup/pool_allocator.hlsl index e1532f945..6685fd5fc 100644 --- a/27_MPMCScheduler/app_resources/workgroup/pool_allocator.hlsl +++ b/27_MPMCScheduler/app_resources/workgroup/pool_allocator.hlsl @@ -1,7 +1,7 @@ #ifndef _NBL_HLSL_WORKGROUP_POOL_ALLOCATOR_HLSL_ #define _NBL_HLSL_WORKGROUP_POOL_ALLOCATOR_HLSL_ -#include "app_resources/workgroup/stack.hlsl" +#include "workgroup/stack.hlsl" namespace nbl { diff --git a/27_MPMCScheduler/main.cpp b/27_MPMCScheduler/main.cpp index 0963f86e5..f2a14b452 100644 --- a/27_MPMCScheduler/main.cpp +++ b/27_MPMCScheduler/main.cpp @@ -1,10 +1,9 @@ // Copyright (C) 2024-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 - - -#include "nbl/examples/examples.hpp" -#include "nbl/this_example/builtin/build/spirv/keys.hpp" +#include "nabla.h" +#include "nbl/application_templates/MonoAssetManagerAndBuiltinResourceApplication.hpp" +#include "SimpleWindowedApplication.hpp" using namespace nbl; using namespace nbl::core; @@ -12,18 +11,16 @@ using namespace nbl::system; using namespace nbl::asset; using namespace nbl::ui; using namespace nbl::video; -using namespace nbl::examples; -#include "app_resources/common.hlsl" - -class MPMCSchedulerApp final : public SimpleWindowedApplication, public BuiltinResourcesApplication +class MPMCSchedulerApp final : public examples::SimpleWindowedApplication, public application_templates::MonoAssetManagerAndBuiltinResourceApplication { - using device_base_t = SimpleWindowedApplication; - using asset_base_t = BuiltinResourcesApplication; + using device_base_t = examples::SimpleWindowedApplication; + using asset_base_t = application_templates::MonoAssetManagerAndBuiltinResourceApplication; using clock_t = std::chrono::steady_clock; - constexpr static inline uint32_t WIN_W = 1280, WIN_H = 720; + constexpr static inline uint32_t WIN_W = 1280, WIN_H = 720, SC_IMG_COUNT = 3u, FRAMES_IN_FLIGHT = 5u; + static_assert(FRAMES_IN_FLIGHT > SC_IMG_COUNT); public: inline MPMCSchedulerApp(const path& _localInputCWD, const path& _localOutputCWD, const path& _sharedInputCWD, const path& _sharedOutputCWD) @@ -31,15 +28,6 @@ class MPMCSchedulerApp final : public SimpleWindowedApplication, public BuiltinR inline bool isComputeOnly() const override { return false; } - // tired of packing and unpacking from float16_t, let some Junior do device traits / manual pack - virtual video::SPhysicalDeviceLimits getRequiredDeviceLimits() const override - { - auto retval = device_base_t::getRequiredDeviceLimits(); - retval.shaderSubgroupArithmetic = true; - retval.shaderFloat16 = true; - return retval; - } - inline core::vector getSurfaces() const override { if (!m_surface) @@ -71,34 +59,27 @@ class MPMCSchedulerApp final : public SimpleWindowedApplication, public BuiltinR return false; if (!asset_base_t::onAppInitialized(std::move(system))) return false; - - smart_refctd_ptr shader; +/* + smart_refctd_ptr shader; { - // load shader - { - IAssetLoader::SAssetLoadParams lp = {}; - lp.logger = m_logger.get(); - lp.workingDirectory = ""; - - auto key = "app_resources/" + nbl::this_example::builtin::build::get_spirv_key<"shader">(m_device.get()); - const auto bundle = m_assetMgr->getAsset(key.data(), lp); - - const auto contents = bundle.getContents(); - - if (contents.empty()) - return logFail("Failed to load shader from disk"); - - if (bundle.getAssetType() != IAsset::ET_SHADER) - return logFail("Loaded asset has wrong type!"); - - shader = IAsset::castDown(contents[0]); - - if (!shader) - false; - } - + IAssetLoader::SAssetLoadParams lp = {}; + lp.logger = m_logger.get(); + lp.workingDirectory = ""; // virtual root + auto assetBundle = m_assetMgr->getAsset("app_resources/shader.comp.hlsl", lp); + const auto assets = assetBundle.getContents(); + if (assets.empty()) + return logFail("Failed to load shader from disk"); + + // lets go straight from ICPUSpecializedShader to IGPUSpecializedShader + auto source = IAsset::castDown(assets[0]); + if (!source) + return logFail("Failed to load shader from disk"); + + shader = m_device->createShader(source.get()); + if (!shader) + return false; } - +*/ smart_refctd_ptr dsLayout; { const IGPUDescriptorSetLayout::SBinding bindings[1] = { { @@ -113,23 +94,27 @@ class MPMCSchedulerApp final : public SimpleWindowedApplication, public BuiltinR if (!dsLayout) return logFail("Failed to Create Descriptor Layout"); } - +/* { - const asset::SPushConstantRange ranges[] = {{ - .stageFlags = IShader::E_SHADER_STAGE::ESS_COMPUTE, - .offset = 0, - .size = sizeof(PushConstants) + auto layout = m_device->createPipelineLayout({},smart_refctd_ptr(dsLayout)); + const IGPUComputePipeline::SCreationParams params[] = { { + { + .layout = layout.get() + }, + {}, + IGPUComputePipeline::SCreationParams::FLAGS::NONE, + { + .entryPoint = "main", + .shader = shader.get(), + .entries = nullptr, + .requiredSubgroupSize = IGPUShader::SSpecInfo::SUBGROUP_SIZE::UNKNOWN, + .requireFullSubgroups = true + } }}; - auto layout = m_device->createPipelineLayout(ranges,smart_refctd_ptr(dsLayout)); - IGPUComputePipeline::SCreationParams params; - params.layout = layout.get(); - params.shader.shader = shader.get(); - params.shader.entryPoint = "main"; - params.cached.requireFullSubgroups = true; - if (!m_device->createComputePipelines(nullptr, { ¶ms, 1 }, &m_ppln)) + if (!m_device->createComputePipelines(nullptr,params,&m_ppln)) return logFail("Failed to create Pipeline"); } - +*/ m_hdr = m_device->createImage({ { .type = IGPUImage::E_TYPE::ET_2D, @@ -149,8 +134,8 @@ class MPMCSchedulerApp final : public SimpleWindowedApplication, public BuiltinR auto pool = m_device->createDescriptorPoolForDSLayouts(IDescriptorPool::E_CREATE_FLAGS::ECF_NONE,{&dsLayout.get(),1}); if (!pool) return logFail("Could not create Descriptor Pool"); - m_ds = pool->createDescriptorSet(std::move(dsLayout)); - if (!m_ds) + auto ds = pool->createDescriptorSet(std::move(dsLayout)); + if (!ds) return logFail("Could not create Descriptor Set"); IGPUDescriptorSet::SDescriptorInfo info = {}; { @@ -167,7 +152,7 @@ class MPMCSchedulerApp final : public SimpleWindowedApplication, public BuiltinR info.info.image.imageLayout = IGPUImage::LAYOUT::GENERAL; } const IGPUDescriptorSet::SWriteDescriptorSet writes[] = {{ - .dstSet = m_ds.get(), + .dstSet = ds.get(), .binding = 0, .arrayElement = 0, .count = 1, @@ -189,8 +174,15 @@ class MPMCSchedulerApp final : public SimpleWindowedApplication, public BuiltinR if (!m_surface || !m_surface->init(gQueue, std::make_unique(), swapchainParams.sharedParams)) return logFail("Could not create Window & Surface or initialize the Surface!"); + m_maxFramesInFlight = m_surface->getMaxFramesInFlight(); + if (FRAMES_IN_FLIGHT < m_maxFramesInFlight) + { + m_logger->log("Lowering frames in flight!", ILogger::ELL_WARNING); + m_maxFramesInFlight = FRAMES_IN_FLIGHT; + } + auto pool = m_device->createCommandPool(gQueue->getFamilyIndex(),IGPUCommandPool::CREATE_FLAGS::RESET_COMMAND_BUFFER_BIT); - for (auto i=0u; igetMaxAcquiresInFlight()); - // We block for semaphores for 2 reasons here: - // A) Resource: Can't use resource like a command buffer BEFORE previous use is finished! [MaxFramesInFlight] - // B) Acquire: Can't have more acquires in flight than a certain threshold returned by swapchain or your surface helper class. [MaxAcquiresInFlight] - if (m_realFrameIx >= framesInFlight) + const auto resourceIx = m_realFrameIx % m_maxFramesInFlight; + + if (m_realFrameIx >= m_maxFramesInFlight) { const ISemaphore::SWaitInfo cbDonePending[] = { { .semaphore = m_semaphore.get(), - .value = m_realFrameIx + 1 - framesInFlight + .value = m_realFrameIx + 1 - m_maxFramesInFlight } }; if (m_device->blockForSemaphores(cbDonePending) != ISemaphore::WAIT_RESULT::SUCCESS) return; } - const auto resourceIx = m_realFrameIx % MaxFramesInFlight; - m_currentImageAcquire = m_surface->acquireNextImage(); if (!m_currentImageAcquire) return; @@ -300,15 +287,9 @@ class MPMCSchedulerApp final : public SimpleWindowedApplication, public BuiltinR // write the image { - cb->bindComputePipeline(m_ppln.get()); - auto* layout = m_ppln->getLayout(); - cb->bindDescriptorSets(E_PIPELINE_BIND_POINT::EPBP_COMPUTE,layout,0,1,&m_ds.get()); - const PushConstants pc = { - .sharedAcceptableIdleCount = 0, - .globalAcceptableIdleCount = 0 - }; - cb->pushConstants(layout,hlsl::ShaderStage::ESS_COMPUTE,0,sizeof(pc),&pc); - cb->dispatch(WIN_W/WorkgroupSizeX,WIN_H/WorkgroupSizeY,1); + // + // cb->bindComputePipeline(rawPipeline); + // push constants } { @@ -426,16 +407,14 @@ class MPMCSchedulerApp final : public SimpleWindowedApplication, public BuiltinR } private: - // 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; smart_refctd_ptr m_window; smart_refctd_ptr> m_surface; - smart_refctd_ptr m_ppln; - smart_refctd_ptr m_ds; smart_refctd_ptr m_hdr; + smart_refctd_ptr m_ppln; smart_refctd_ptr m_semaphore; - uint64_t m_realFrameIx = 0; - std::array,MaxFramesInFlight> m_cmdBufs; + uint64_t m_realFrameIx : 59 = 0; + uint64_t m_maxFramesInFlight : 5; + std::array,ISwapchain::MaxImages> m_cmdBufs; ISimpleManagedSurface::SAcquireResult m_currentImageAcquire = {}; }; diff --git a/27_PLYSTLDemo/CMakeLists.txt b/27_PLYSTLDemo/CMakeLists.txt new file mode 100644 index 000000000..a476b6203 --- /dev/null +++ b/27_PLYSTLDemo/CMakeLists.txt @@ -0,0 +1,7 @@ + +include(common RESULT_VARIABLE RES) +if(NOT RES) + message(FATAL_ERROR "common.cmake not found. Should be in {repo_root}/cmake directory") +endif() + +nbl_create_executable_project("" "" "" "" "${NBL_EXECUTABLE_PROJECT_CREATION_PCH_TARGET}") \ No newline at end of file diff --git a/27_PLYSTLDemo/config.json.template b/27_PLYSTLDemo/config.json.template new file mode 100644 index 000000000..cb1b3b7a7 --- /dev/null +++ b/27_PLYSTLDemo/config.json.template @@ -0,0 +1,28 @@ +{ + "enableParallelBuild": true, + "threadsPerBuildProcess" : 2, + "isExecuted": false, + "scriptPath": "", + "cmake": { + "configurations": [ "Release", "Debug", "RelWithDebInfo" ], + "buildModes": [], + "requiredOptions": [ "NBL_BUILD_MITSUBA_LOADER", "NBL_BUILD_OPTIX" ] + }, + "profiles": [ + { + "backend": "vulkan", + "platform": "windows", + "buildModes": [], + "runConfiguration": "Release", + "gpuArchitectures": [] + } + ], + "dependencies": [], + "data": [ + { + "dependencies": [], + "command": [""], + "outputs": [] + } + ] +} \ No newline at end of file diff --git a/27_PLYSTLDemo/main.cpp b/27_PLYSTLDemo/main.cpp new file mode 100644 index 000000000..1e6d470e2 --- /dev/null +++ b/27_PLYSTLDemo/main.cpp @@ -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 + +#define _NBL_STATIC_LIB_ +#include +#include +#include + +#include "CCamera.hpp" +#include "../common/CommonAPI.h" +#include "nbl/ext/ScreenShot/ScreenShot.h" + +using namespace nbl; +using namespace core; + +/* + Uncomment for more detailed logging +*/ + +// #define NBL_MORE_LOGS + +/* + Uncomment for writing assets +*/ + +#define WRITE_ASSETS + +class PLYSTLDemo : public ApplicationBase +{ + static constexpr uint32_t WIN_W = 1280; + static constexpr uint32_t WIN_H = 720; + static constexpr uint32_t SC_IMG_COUNT = 3u; + static constexpr uint32_t FRAMES_IN_FLIGHT = 5u; + static constexpr uint64_t MAX_TIMEOUT = 99999999999999ull; + static constexpr size_t NBL_FRAMES_TO_AVERAGE = 100ull; + static_assert(FRAMES_IN_FLIGHT > SC_IMG_COUNT); + + using RENDERPASS_INDEPENDENT_PIPELINE_ADRESS = size_t; + using GPU_PIPELINE_HASH_CONTAINER = std::map>; + using DependentDrawData = std::tuple, core::smart_refctd_ptr, core::smart_refctd_ptr, uint32_t, const asset::IRenderpassIndependentPipelineMetadata*>; + +public: + nbl::core::smart_refctd_ptr windowManager; + nbl::core::smart_refctd_ptr window; + nbl::core::smart_refctd_ptr windowCallback; + nbl::core::smart_refctd_ptr gl; + nbl::core::smart_refctd_ptr surface; + nbl::core::smart_refctd_ptr utilities; + nbl::core::smart_refctd_ptr logicalDevice; + nbl::video::IPhysicalDevice* gpuPhysicalDevice; + std::array queues = { nullptr, nullptr, nullptr, nullptr }; + nbl::core::smart_refctd_ptr swapchain; + nbl::core::smart_refctd_ptr renderpass; + nbl::core::smart_refctd_dynamic_array> fbos; + std::array, CommonAPI::InitOutput::MaxFramesInFlight>, CommonAPI::InitOutput::MaxQueuesCount> commandPools; + nbl::core::smart_refctd_ptr system; + nbl::core::smart_refctd_ptr assetManager; + nbl::video::IGPUObjectFromAssetConverter::SParams cpu2gpuParams; + nbl::core::smart_refctd_ptr logger; + nbl::core::smart_refctd_ptr inputSystem; + + nbl::core::smart_refctd_ptr gpuTransferFence; + nbl::core::smart_refctd_ptr gpuComputeFence; + nbl::video::IGPUObjectFromAssetConverter cpu2gpu; + + uint32_t acquiredNextFBO = {}; + int resourceIx = -1; + + core::smart_refctd_ptr commandBuffers[FRAMES_IN_FLIGHT]; + + core::smart_refctd_ptr frameComplete[FRAMES_IN_FLIGHT] = { nullptr }; + core::smart_refctd_ptr imageAcquire[FRAMES_IN_FLIGHT] = { nullptr }; + core::smart_refctd_ptr renderFinished[FRAMES_IN_FLIGHT] = { nullptr }; + + nbl::video::ISwapchain::SCreationParams m_swapchainCreationParams; + + std::chrono::system_clock::time_point lastTime; + bool frameDataFilled = false; + size_t frame_count = 0ull; + double time_sum = 0; + double dtList[NBL_FRAMES_TO_AVERAGE] = {}; + + CommonAPI::InputSystem::ChannelReader mouse; + CommonAPI::InputSystem::ChannelReader keyboard; + + Camera camera = Camera(core::vectorSIMDf(0, 0, 0), core::vectorSIMDf(0, 0, 0), core::matrix4SIMD()); + + GPU_PIPELINE_HASH_CONTAINER gpuPipelinesPly; + GPU_PIPELINE_HASH_CONTAINER gpuPipelinesStl; + + DependentDrawData plyDrawData; + DependentDrawData stlDrawData; + + void setWindow(core::smart_refctd_ptr&& wnd) override + { + window = std::move(wnd); + } + nbl::ui::IWindow* getWindow() override + { + return window.get(); + } + void setSystem(core::smart_refctd_ptr&& s) override + { + system = std::move(s); + } + video::IAPIConnection* getAPIConnection() override + { + return gl.get(); + } + video::ILogicalDevice* getLogicalDevice() override + { + return logicalDevice.get(); + } + video::IGPURenderpass* getRenderpass() override + { + return renderpass.get(); + } + void setSurface(core::smart_refctd_ptr&& s) override + { + surface = std::move(s); + } + void setFBOs(std::vector>& f) override + { + for (int i = 0; i < f.size(); i++) + { + fbos->begin()[i] = core::smart_refctd_ptr(f[i]); + } + } + void setSwapchain(core::smart_refctd_ptr&& s) override + { + swapchain = std::move(s); + } + uint32_t getSwapchainImageCount() override + { + return swapchain->getImageCount(); + } + virtual nbl::asset::E_FORMAT getDepthFormat() override + { + return nbl::asset::EF_D32_SFLOAT; + } + +APP_CONSTRUCTOR(PLYSTLDemo) + + void onAppInitialized_impl() override + { + const auto swapchainImageUsage = static_cast(asset::IImage::EUF_COLOR_ATTACHMENT_BIT); + CommonAPI::InitParams initParams; + initParams.window = core::smart_refctd_ptr(window); + initParams.apiType = video::EAT_VULKAN; + initParams.appName = { _NBL_APP_NAME_ }; + initParams.framesInFlight = FRAMES_IN_FLIGHT; + initParams.windowWidth = WIN_W; + initParams.windowHeight = WIN_H; + initParams.swapchainImageCount = SC_IMG_COUNT; + initParams.swapchainImageUsage = swapchainImageUsage; + initParams.depthFormat = nbl::asset::EF_D32_SFLOAT; + auto initOutput = CommonAPI::InitWithDefaultExt(std::move(initParams)); + + window = std::move(initParams.window); + gl = std::move(initOutput.apiConnection); + surface = std::move(initOutput.surface); + gpuPhysicalDevice = std::move(initOutput.physicalDevice); + logicalDevice = std::move(initOutput.logicalDevice); + queues = std::move(initOutput.queues); + renderpass = std::move(initOutput.renderToSwapchainRenderpass); + commandPools = std::move(initOutput.commandPools); + assetManager = std::move(initOutput.assetManager); + logger = std::move(initOutput.logger); + inputSystem = std::move(initOutput.inputSystem); + system = std::move(initOutput.system); + windowCallback = std::move(initParams.windowCb); + utilities = std::move(initOutput.utilities); + m_swapchainCreationParams = std::move(initOutput.swapchainCreationParams); + + CommonAPI::createSwapchain(std::move(logicalDevice), m_swapchainCreationParams, WIN_W, WIN_H, swapchain); + assert(swapchain); + fbos = CommonAPI::createFBOWithSwapchainImages( + swapchain->getImageCount(), WIN_W, WIN_H, + logicalDevice, swapchain, renderpass, + nbl::asset::EF_D32_SFLOAT + ); + + auto defaultComputeCommandPool = commandPools[CommonAPI::InitOutput::EQT_COMPUTE][0]; + auto defaultTransferUpCommandPool = commandPools[CommonAPI::InitOutput::EQT_TRANSFER_UP][0]; + + nbl::video::IGPUObjectFromAssetConverter cpu2gpu; + nbl::video::IGPUObjectFromAssetConverter::SParams cpu2gpuParams; + + nbl::core::smart_refctd_ptr gpuTransferFence; + nbl::core::smart_refctd_ptr gpuTransferSemaphore; + + nbl::core::smart_refctd_ptr gpuComputeFence; + nbl::core::smart_refctd_ptr gpuComputeSemaphore; + + { + gpuTransferFence = logicalDevice->createFence(static_cast(0)); + gpuTransferSemaphore = logicalDevice->createSemaphore(); + + gpuComputeFence = logicalDevice->createFence(static_cast(0)); + gpuComputeSemaphore = logicalDevice->createSemaphore(); + + cpu2gpuParams.utilities = utilities.get(); + cpu2gpuParams.device = logicalDevice.get(); + cpu2gpuParams.assetManager = assetManager.get(); + cpu2gpuParams.pipelineCache = nullptr; + cpu2gpuParams.limits = gpuPhysicalDevice->getLimits(); + cpu2gpuParams.finalQueueFamIx = queues[decltype(initOutput)::EQT_GRAPHICS]->getFamilyIndex(); + + logicalDevice->createCommandBuffers(defaultTransferUpCommandPool.get(),video::IGPUCommandBuffer::EL_PRIMARY,1u,&cpu2gpuParams.perQueue[nbl::video::IGPUObjectFromAssetConverter::EQU_TRANSFER].cmdbuf); + cpu2gpuParams.perQueue[nbl::video::IGPUObjectFromAssetConverter::EQU_TRANSFER].queue = queues[decltype(initOutput)::EQT_TRANSFER_UP]; + cpu2gpuParams.perQueue[nbl::video::IGPUObjectFromAssetConverter::EQU_TRANSFER].semaphore = &gpuTransferSemaphore; + + logicalDevice->createCommandBuffers(defaultComputeCommandPool.get(),video::IGPUCommandBuffer::EL_PRIMARY,1u,&cpu2gpuParams.perQueue[nbl::video::IGPUObjectFromAssetConverter::EQU_COMPUTE].cmdbuf); + cpu2gpuParams.perQueue[nbl::video::IGPUObjectFromAssetConverter::EQU_COMPUTE].queue = queues[decltype(initOutput)::EQT_COMPUTE]; + cpu2gpuParams.perQueue[nbl::video::IGPUObjectFromAssetConverter::EQU_COMPUTE].semaphore = &gpuComputeSemaphore; + + cpu2gpuParams.beginCommandBuffers(); + } + + auto loadAndGetCpuMesh = [&](system::path path) -> std::pair, const asset::IAssetMetadata*> + { + auto meshes_bundle = assetManager->getAsset(path.string(), {}); + { + bool status = !meshes_bundle.getContents().empty(); + assert(status); + } + + auto mesh = core::smart_refctd_ptr_static_cast(meshes_bundle.getContents().begin()[0]); + auto metadata = meshes_bundle.getMetadata(); + return std::make_pair(mesh, metadata); + //return std::make_pair(core::smart_refctd_ptr_static_cast(meshes_bundle.getContents().begin()[0]), meshes_bundle.getMetadata()); + }; + + auto cpuBundlePLYData = loadAndGetCpuMesh(sharedInputCWD / "ply/Spanner-ply.ply"); + auto cpuBundleSTLData = loadAndGetCpuMesh(sharedInputCWD / "extrusionLogo_TEST_fixed.stl"); + + core::smart_refctd_ptr cpuMeshPly = cpuBundlePLYData.first; + auto metadataPly = cpuBundlePLYData.second->selfCast(); + + core::smart_refctd_ptr cpuMeshStl = cpuBundleSTLData.first; + auto metadataStl = cpuBundleSTLData.second->selfCast(); + +#ifdef WRITE_ASSETS + { + asset::IAssetWriter::SAssetWriteParams wp(cpuMeshPly.get()); + bool status = assetManager->writeAsset("Spanner_ply.ply", wp); + assert(status); + } + + { + asset::IAssetWriter::SAssetWriteParams wp(cpuMeshStl.get()); + bool status = assetManager->writeAsset("extrusionLogo_TEST_fixedTest.stl", wp); + assert(status); + } +#endif // WRITE_ASSETS + + /* + For the testing puposes we can safely assume all meshbuffers within mesh loaded from PLY & STL has same DS1 layout (used for camera-specific data) + */ + + auto getMeshDependentDrawData = [&](core::smart_refctd_ptr cpuMesh, bool isPLY) -> DependentDrawData + { + const asset::ICPUMeshBuffer* const firstMeshBuffer = cpuMesh->getMeshBuffers().begin()[0]; + const asset::ICPUDescriptorSetLayout* ds1layout = firstMeshBuffer->getPipeline()->getLayout()->getDescriptorSetLayout(1u); //! DS1 + const asset::IRenderpassIndependentPipelineMetadata* pipelineMetadata; + { + if (isPLY) + pipelineMetadata = metadataPly->getAssetSpecificMetadata(firstMeshBuffer->getPipeline()); + else + pipelineMetadata = metadataStl->getAssetSpecificMetadata(firstMeshBuffer->getPipeline()); + } + + /* + So we can create just one DescriptorSet + */ + + const uint32_t ds1UboBinding = ds1layout->getDescriptorRedirect(asset::IDescriptor::E_TYPE::ET_UNIFORM_BUFFER).getBinding(asset::ICPUDescriptorSetLayout::CBindingRedirect::storage_range_index_t{ 0 }).data; + + auto getNeededDS1UboByteSize = [&]() + { + size_t neededDS1UboSize = 0ull; + { + for (const auto& shaderInputs : pipelineMetadata->m_inputSemantics) + if (shaderInputs.descriptorSection.type == asset::IRenderpassIndependentPipelineMetadata::ShaderInput::E_TYPE::ET_UNIFORM_BUFFER && shaderInputs.descriptorSection.uniformBufferObject.set == 1u && shaderInputs.descriptorSection.uniformBufferObject.binding == ds1UboBinding) + neededDS1UboSize = std::max(neededDS1UboSize, shaderInputs.descriptorSection.uniformBufferObject.relByteoffset + shaderInputs.descriptorSection.uniformBufferObject.bytesize); + } + return neededDS1UboSize; + }; + + const uint64_t uboDS1ByteSize = getNeededDS1UboByteSize(); + + core::smart_refctd_ptr gpuds1layout; + { + auto gpu_array = cpu2gpu.getGPUObjectsFromAssets(&ds1layout, &ds1layout + 1, cpu2gpuParams); + if (!gpu_array || gpu_array->size() < 1u || !(*gpu_array)[0]) + assert(false); + + gpuds1layout = (*gpu_array)[0]; + } + + const uint32_t setCount = 1; + auto gpuUBODescriptorPool = logicalDevice->createDescriptorPoolForDSLayouts(video::IDescriptorPool::ECF_NONE, &gpuds1layout.get(), &gpuds1layout.get()+1ull, &setCount); + + video::IGPUBuffer::SCreationParams creationParams; + creationParams.usage = asset::IBuffer::E_USAGE_FLAGS(asset::IBuffer::EUF_UNIFORM_BUFFER_BIT | asset::IBuffer::EUF_INLINE_UPDATE_VIA_CMDBUF); + creationParams.queueFamilyIndices = 0u; + creationParams.queueFamilyIndices = nullptr; + creationParams.size = uboDS1ByteSize; + + auto gpuubo = logicalDevice->createBuffer(std::move(creationParams)); + auto gpuuboMemReqs = gpuubo->getMemoryReqs(); + gpuuboMemReqs.memoryTypeBits &= logicalDevice->getPhysicalDevice()->getDeviceLocalMemoryTypeBits(); + logicalDevice->allocate(gpuuboMemReqs, gpuubo.get()); + + auto gpuds1 = gpuUBODescriptorPool->createDescriptorSet(std::move(gpuds1layout)); + { + video::IGPUDescriptorSet::SWriteDescriptorSet write; + write.dstSet = gpuds1.get(); + write.binding = ds1UboBinding; + write.count = 1u; + write.arrayElement = 0u; + write.descriptorType = asset::IDescriptor::E_TYPE::ET_UNIFORM_BUFFER; + video::IGPUDescriptorSet::SDescriptorInfo info; + { + info.desc = gpuubo; + info.info.buffer.offset = 0ull; + info.info.buffer.size = uboDS1ByteSize; + } + write.info = &info; + logicalDevice->updateDescriptorSets(1u, &write, 0u, nullptr); + } + + core::smart_refctd_ptr gpumesh; + { + auto gpu_array = cpu2gpu.getGPUObjectsFromAssets(&cpuMesh.get(), &cpuMesh.get() + 1, cpu2gpuParams); + cpu2gpuParams.waitForCreationToComplete(true); + cpu2gpuParams.beginCommandBuffers(); + if (!gpu_array || gpu_array->size() < 1u || !(*gpu_array)[0]) + assert(false); + + gpumesh = (*gpu_array)[0]; + } + + return std::make_tuple(gpumesh, gpuubo, gpuds1, ds1UboBinding, pipelineMetadata); + }; + + plyDrawData = getMeshDependentDrawData(cpuMeshPly, true); + stlDrawData = getMeshDependentDrawData(cpuMeshStl, false); + + { + auto fillGpuPipeline = [&](GPU_PIPELINE_HASH_CONTAINER& container, video::IGPUMesh* gpuMesh) + { + for (size_t i = 0; i < gpuMesh->getMeshBuffers().size(); ++i) + { + auto gpuIndependentPipeline = gpuMesh->getMeshBuffers().begin()[i]->getPipeline(); + + nbl::video::IGPUGraphicsPipeline::SCreationParams graphicsPipelineParams; + graphicsPipelineParams.renderpassIndependent = core::smart_refctd_ptr(const_cast(gpuIndependentPipeline)); + graphicsPipelineParams.renderpass = core::smart_refctd_ptr(renderpass); + + const RENDERPASS_INDEPENDENT_PIPELINE_ADRESS adress = reinterpret_cast(graphicsPipelineParams.renderpassIndependent.get()); + container[adress] = logicalDevice->createGraphicsPipeline(nullptr, std::move(graphicsPipelineParams)); + } + }; + + fillGpuPipeline(gpuPipelinesPly, std::get>(plyDrawData).get()); + fillGpuPipeline(gpuPipelinesStl, std::get>(stlDrawData).get()); + } + + core::vectorSIMDf cameraPosition(0, 5, -10); + matrix4SIMD projectionMatrix = matrix4SIMD::buildProjectionMatrixPerspectiveFovLH(core::radians(60.0f), video::ISurface::getTransformedAspectRatio(swapchain->getPreTransform(), WIN_W, WIN_H), 0.001, 1000); + camera = Camera(cameraPosition, core::vectorSIMDf(0, 0, 0), projectionMatrix, 0.01f, 1.f); + lastTime = std::chrono::system_clock::now(); + + for (size_t i = 0ull; i < NBL_FRAMES_TO_AVERAGE; ++i) + dtList[i] = 0.0; + + const auto& graphicsCommandPools = commandPools[CommonAPI::InitOutput::EQT_GRAPHICS]; + for (uint32_t i = 0u; i < FRAMES_IN_FLIGHT; i++) + { + logicalDevice->createCommandBuffers(graphicsCommandPools[i].get(), video::IGPUCommandBuffer::EL_PRIMARY, 1, commandBuffers+i); + imageAcquire[i] = logicalDevice->createSemaphore(); + renderFinished[i] = logicalDevice->createSemaphore(); + } + } + + void onAppTerminated_impl() override + { + const auto& fboCreationParams = fbos->begin()[acquiredNextFBO]->getCreationParameters(); + auto gpuSourceImageView = fboCreationParams.attachments[0]; + + //TODO: + bool status = ext::ScreenShot::createScreenShot( + logicalDevice.get(), + queues[CommonAPI::InitOutput::EQT_TRANSFER_UP], + renderFinished[resourceIx].get(), + gpuSourceImageView.get(), + assetManager.get(), + "ScreenShot.png", + asset::IImage::EL_PRESENT_SRC, + asset::EAF_NONE); + assert(status); + } + + void workLoopBody() override + { + ++resourceIx; + if (resourceIx >= FRAMES_IN_FLIGHT) + resourceIx = 0; + + auto& commandBuffer = commandBuffers[resourceIx]; + auto& fence = frameComplete[resourceIx]; + + if (fence) + while (logicalDevice->waitForFences(1u, &fence.get(), false, MAX_TIMEOUT) == video::IGPUFence::ES_TIMEOUT) {} + else + fence = logicalDevice->createFence(static_cast(0)); + + auto renderStart = std::chrono::system_clock::now(); + const auto renderDt = std::chrono::duration_cast(renderStart - lastTime).count(); + lastTime = renderStart; + { // Calculate Simple Moving Average for FrameTime + time_sum -= dtList[frame_count]; + time_sum += renderDt; + dtList[frame_count] = renderDt; + frame_count++; + if (frame_count >= NBL_FRAMES_TO_AVERAGE) + { + frameDataFilled = true; + frame_count = 0; + } + + } + const double averageFrameTime = frameDataFilled ? (time_sum / (double)NBL_FRAMES_TO_AVERAGE) : (time_sum / frame_count); + +#ifdef NBL_MORE_LOGS + logger->log("renderDt = %f ------ averageFrameTime = %f", system::ILogger::ELL_INFO, renderDt, averageFrameTime); +#endif // NBL_MORE_LOGS + + auto averageFrameTimeDuration = std::chrono::duration(averageFrameTime); + auto nextPresentationTime = renderStart + averageFrameTimeDuration; + auto nextPresentationTimeStamp = std::chrono::duration_cast(nextPresentationTime.time_since_epoch()); + + inputSystem->getDefaultMouse(&mouse); + inputSystem->getDefaultKeyboard(&keyboard); + + camera.beginInputProcessing(nextPresentationTimeStamp); + mouse.consumeEvents([&](const ui::IMouseEventChannel::range_t& events) -> void { camera.mouseProcess(events); }, logger.get()); + keyboard.consumeEvents([&](const ui::IKeyboardEventChannel::range_t& events) -> void { camera.keyboardProcess(events); }, logger.get()); + camera.endInputProcessing(nextPresentationTimeStamp); + + const auto& viewMatrix = camera.getViewMatrix(); + const auto& viewProjectionMatrix = matrix4SIMD::concatenateBFollowedByAPrecisely( + video::ISurface::getSurfaceTransformationMatrix(swapchain->getPreTransform()), + camera.getConcatenatedMatrix() + ); + + commandBuffer->reset(nbl::video::IGPUCommandBuffer::ERF_RELEASE_RESOURCES_BIT); + commandBuffer->begin(video::IGPUCommandBuffer::EU_ONE_TIME_SUBMIT_BIT); // TODO: Reset Frame's CommandPool + + asset::SViewport viewport; + viewport.minDepth = 1.f; + viewport.maxDepth = 0.f; + viewport.x = 0u; + viewport.y = 0u; + viewport.width = WIN_W; + viewport.height = WIN_H; + commandBuffer->setViewport(0u, 1u, &viewport); + + swapchain->acquireNextImage(MAX_TIMEOUT, imageAcquire[resourceIx].get(), nullptr, &acquiredNextFBO); + + nbl::video::IGPUCommandBuffer::SRenderpassBeginInfo beginInfo; + { + VkRect2D area; + area.offset = { 0,0 }; + area.extent = { WIN_W, WIN_H }; + asset::SClearValue clear[2] = {}; + clear[0].color.float32[0] = 1.f; + clear[0].color.float32[1] = 1.f; + clear[0].color.float32[2] = 1.f; + clear[0].color.float32[3] = 1.f; + clear[1].depthStencil.depth = 0.f; + + beginInfo.clearValueCount = 2u; + beginInfo.framebuffer = fbos->begin()[acquiredNextFBO]; + beginInfo.renderpass = renderpass; + beginInfo.renderArea = area; + beginInfo.clearValues = clear; + } + + commandBuffer->beginRenderPass(&beginInfo, nbl::asset::ESC_INLINE); + + auto renderMesh = [&](GPU_PIPELINE_HASH_CONTAINER& gpuPipelines, DependentDrawData& drawData, uint32_t index) + { + auto gpuMesh = std::get>(drawData); + auto gpuubo = std::get>(drawData); + auto gpuds1 = std::get>(drawData); + auto ds1UboBinding = std::get(drawData); + const auto* pipelineMetadata = std::get(drawData); + + core::matrix3x4SIMD modelMatrix; + + if (index == 1) + modelMatrix.setScale(core::vectorSIMDf(10, 10, 10)); + modelMatrix.setTranslation(nbl::core::vectorSIMDf(index * 150, 0, 0, 0)); + + core::matrix4SIMD mvp = core::concatenateBFollowedByA(viewProjectionMatrix, modelMatrix); + + core::vector uboData(gpuubo->getSize()); + for (const auto& shaderInputs : pipelineMetadata->m_inputSemantics) + { + if (shaderInputs.descriptorSection.type == asset::IRenderpassIndependentPipelineMetadata::ShaderInput::E_TYPE::ET_UNIFORM_BUFFER && shaderInputs.descriptorSection.uniformBufferObject.set == 1u && shaderInputs.descriptorSection.uniformBufferObject.binding == ds1UboBinding) + { + switch (shaderInputs.type) + { + case asset::IRenderpassIndependentPipelineMetadata::ECSI_WORLD_VIEW_PROJ: + { + memcpy(uboData.data() + shaderInputs.descriptorSection.uniformBufferObject.relByteoffset, mvp.pointer(), shaderInputs.descriptorSection.uniformBufferObject.bytesize); + } break; + + case asset::IRenderpassIndependentPipelineMetadata::ECSI_WORLD_VIEW: + { + memcpy(uboData.data() + shaderInputs.descriptorSection.uniformBufferObject.relByteoffset, viewMatrix.pointer(), shaderInputs.descriptorSection.uniformBufferObject.bytesize); + } break; + + case asset::IRenderpassIndependentPipelineMetadata::ECSI_WORLD_VIEW_INVERSE_TRANSPOSE: + { + memcpy(uboData.data() + shaderInputs.descriptorSection.uniformBufferObject.relByteoffset, viewMatrix.pointer(), shaderInputs.descriptorSection.uniformBufferObject.bytesize); + } break; + } + } + } + + commandBuffer->updateBuffer(gpuubo.get(), 0ull, gpuubo->getSize(), uboData.data()); + + for (auto gpuMeshBuffer : gpuMesh->getMeshBuffers()) + { + auto gpuGraphicsPipeline = gpuPipelines[reinterpret_cast(gpuMeshBuffer->getPipeline())]; + + const video::IGPURenderpassIndependentPipeline* gpuRenderpassIndependentPipeline = gpuMeshBuffer->getPipeline(); + const video::IGPUDescriptorSet* ds3 = gpuMeshBuffer->getAttachedDescriptorSet(); + + commandBuffer->bindGraphicsPipeline(gpuGraphicsPipeline.get()); + + const video::IGPUDescriptorSet* gpuds1_ptr = gpuds1.get(); + commandBuffer->bindDescriptorSets(asset::EPBP_GRAPHICS, gpuRenderpassIndependentPipeline->getLayout(), 1u, 1u, &gpuds1_ptr, 0u); + const video::IGPUDescriptorSet* gpuds3_ptr = gpuMeshBuffer->getAttachedDescriptorSet(); + + if (gpuds3_ptr) + commandBuffer->bindDescriptorSets(asset::EPBP_GRAPHICS, gpuRenderpassIndependentPipeline->getLayout(), 3u, 1u, &gpuds3_ptr, 0u); + if (gpuRenderpassIndependentPipeline->getLayout()->m_pushConstantRanges) + commandBuffer->pushConstants(gpuRenderpassIndependentPipeline->getLayout(), video::IGPUShader::ESS_FRAGMENT, 0u, gpuMeshBuffer->MAX_PUSH_CONSTANT_BYTESIZE, gpuMeshBuffer->getPushConstantsDataPtr()); + + commandBuffer->drawMeshBuffer(gpuMeshBuffer); + } + }; + + /* + Record PLY and STL rendering commands + */ + + renderMesh(gpuPipelinesPly, plyDrawData, 0); + renderMesh(gpuPipelinesStl, stlDrawData, 1); + + commandBuffer->endRenderPass(); + commandBuffer->end(); + + CommonAPI::Submit(logicalDevice.get(), commandBuffer.get(), queues[CommonAPI::InitOutput::EQT_GRAPHICS], imageAcquire[resourceIx].get(), renderFinished[resourceIx].get(), fence.get()); + CommonAPI::Present(logicalDevice.get(), swapchain.get(), queues[CommonAPI::InitOutput::EQT_GRAPHICS], renderFinished[resourceIx].get(), acquiredNextFBO); + } + + bool keepRunning() override + { + return windowCallback->isWindowOpen(); + } +}; + +NBL_COMMON_API_MAIN(PLYSTLDemo) \ No newline at end of file diff --git a/14_Mortons/pipeline.groovy b/27_PLYSTLDemo/pipeline.groovy similarity index 82% rename from 14_Mortons/pipeline.groovy rename to 27_PLYSTLDemo/pipeline.groovy index 1a7b043a4..9a89cc786 100644 --- a/14_Mortons/pipeline.groovy +++ b/27_PLYSTLDemo/pipeline.groovy @@ -2,9 +2,9 @@ import org.DevshGraphicsProgramming.Agent import org.DevshGraphicsProgramming.BuilderInfo import org.DevshGraphicsProgramming.IBuilder -class CStreamingAndBufferDeviceAddressBuilder extends IBuilder +class CPLYSTLDemoBuilder extends IBuilder { - public CStreamingAndBufferDeviceAddressBuilder(Agent _agent, _info) + public CPLYSTLDemoBuilder(Agent _agent, _info) { super(_agent, _info) } @@ -44,7 +44,7 @@ class CStreamingAndBufferDeviceAddressBuilder extends IBuilder def create(Agent _agent, _info) { - return new CStreamingAndBufferDeviceAddressBuilder(_agent, _info) + return new CPLYSTLDemoBuilder(_agent, _info) } return this \ No newline at end of file diff --git a/28_FFTBloom/app_resources/common.hlsl b/28_FFTBloom/app_resources/common.hlsl deleted file mode 100644 index 0f71391eb..000000000 --- a/28_FFTBloom/app_resources/common.hlsl +++ /dev/null @@ -1,51 +0,0 @@ -#include "nbl/builtin/hlsl/cpp_compat.hlsl" -#include "nbl/builtin/hlsl/workgroup/fft.hlsl" - -NBL_CONSTEXPR_STATIC_INLINE uint32_t Channels = 3; - -using namespace nbl::hlsl; - -// All packed bitfields of int32_t are uints being passed as ints so we don't litter the code with int32 casts -struct PushConstantData -{ - // After running FFT along a column, we want to store the result in column major order for coalesced writes, and similarly after running an FFT in row major order - // All FFTs that read from a buffer and write to a buffer (kernel second FFT, image second FFT + conv + IFFT) read in one order and write in another. - // Since workgroups start and finish at arbitrary times it's not possible to pickup the data from a buffer in one layout and write it back in another layout due to - // possibility of writing over data that still hasn't been read by some workgroups. - uint64_t colMajorBufferAddress; - uint64_t rowMajorBufferAddress; - uint64_t channelStrideBytes; - // To save some work, we don't mirror the image along both directions when doing the FFT. This means that when doing the FFT along the second axis, we do an FFT of length - // `RoundUpToPoT(imageRowLength + kernelPadding)` where `imageRowLength` is the actual length of the image along the second axis. We need it to keep track of the image's original dimension. - // The following three fields being push constants allow dynamic resizing of the image without recompiling shaders (limited by the FFT length) - int32_t imageRowLength : 16; - int32_t imageHalfRowLength : 16; - // We don't pack it into a bitfield so we can use offsetof and update only this field from CPP side - // Alternatively, we could do the packing/unpacking manually to save 32 bits - int32_t padding; - // Used by IFFT to tell if an index belongs to an image or is in the padding - int32_t imageColumnLength : 19; - int32_t halfPadding : 13; - float32_t2 imageHalfPixelSize; - float32_t2 imagePixelSize; - float32_t imageTwoPixelSize_x; - float32_t imageWorkgroupSizePixelSize_y; - float32_t interpolatingFactor; -}; - -#ifdef __HLSL_VERSION - -#include "nbl/builtin/hlsl/bda/legacy_bda_accessor.hlsl" - -[[vk::push_constant]] PushConstantData pushConstants; - -using scalar_t = ShaderConstevalParameters::scalar_t; -using FFTParameters = workgroup::fft::ConstevalParameters; - -using FFTIndexingUtils = workgroup::fft::FFTIndexingUtils; -using FFTMirrorTradeUtils = workgroup::fft::FFTMirrorTradeUtils; - -// Users MUST define this method for FFT to work - workgroup::SubgroupContiguousIndex also needs it defined -uint32_t3 glsl::gl_WorkGroupSize() { return uint32_t3(uint32_t(FFTParameters::WorkgroupSize), 1, 1); } - -#endif diff --git a/28_FFTBloom/app_resources/fft_common.hlsl b/28_FFTBloom/app_resources/fft_common.hlsl deleted file mode 100644 index 9f2be1432..000000000 --- a/28_FFTBloom/app_resources/fft_common.hlsl +++ /dev/null @@ -1,71 +0,0 @@ -#include "common.hlsl" -#include "nbl/builtin/hlsl/workgroup/fft.hlsl" - -groupshared uint32_t sharedmem[FFTParameters::SharedMemoryDWORDs]; - -struct SharedMemoryAccessor -{ - template - void set(IndexType idx, AccessType value) - { - sharedmem[idx] = value; - } - - template - void get(IndexType idx, NBL_REF_ARG(AccessType) value) - { - value = sharedmem[idx]; - } - - void workgroupExecutionAndMemoryBarrier() - { - glsl::barrier(); - } - -}; - -struct PreloadedAccessorCommonBase -{ - NBL_CONSTEXPR_STATIC_INLINE uint16_t ElementsPerInvocationLog2 = FFTParameters::ElementsPerInvocationLog2; - NBL_CONSTEXPR_STATIC_INLINE uint16_t WorkgroupSizeLog2 = FFTParameters::WorkgroupSizeLog2; - - NBL_CONSTEXPR_STATIC_INLINE uint16_t ElementsPerInvocation = FFTParameters::ElementsPerInvocation; - NBL_CONSTEXPR_STATIC_INLINE uint16_t WorkgroupSize = FFTParameters::WorkgroupSize; - NBL_CONSTEXPR_STATIC_INLINE uint16_t TotalSize = FFTParameters::TotalSize; -}; - -struct PreloadedAccessorBase : PreloadedAccessorCommonBase -{ - template - void set(IndexType idx, AccessType value) - { - preloaded[idx >> WorkgroupSizeLog2] = value; - } - - template - void get(IndexType idx, NBL_REF_ARG(AccessType) value) - { - value = preloaded[idx >> WorkgroupSizeLog2]; - } - - complex_t preloaded[ElementsPerInvocation]; -}; - -// In the case for preloading all channels at once we make it stateful so we track which channel we're running FFT on -struct MultiChannelPreloadedAccessorBase : PreloadedAccessorCommonBase -{ - template - void set(IndexType idx, AccessType value) - { - preloaded[currentChannel][idx >> WorkgroupSizeLog2] = value; - } - - template - void get(IndexType idx, NBL_REF_ARG(AccessType) value) - { - value = preloaded[currentChannel][idx >> WorkgroupSizeLog2]; - } - - complex_t preloaded[Channels][ElementsPerInvocation]; - uint16_t currentChannel; -}; \ No newline at end of file diff --git a/28_FFTBloom/app_resources/fft_convolve_ifft.hlsl b/28_FFTBloom/app_resources/fft_convolve_ifft.hlsl deleted file mode 100644 index 02ae4ff40..000000000 --- a/28_FFTBloom/app_resources/fft_convolve_ifft.hlsl +++ /dev/null @@ -1,245 +0,0 @@ -#include "fft_mirror_common.hlsl" - -[[vk::binding(3, 0)]] Texture2DArray kernelChannels; -[[vk::binding(1, 0)]] SamplerState samplerState; - -// ------------------------------------------ SECOND AXIS FFT + CONVOLUTION + IFFT ------------------------------------------------------------- - -// This is done for the channel specified in pushConstants. - -// This time each Workgroup will compute the FFT along a horizontal line (fixed y for the whole Workgroup). We get the y coordinate for the -// row a workgroup is working on via `gl_WorkGroupID().x`. We have to keep this in mind: What's stored as the first row is actually `Z + iN`, -// where `Z` is the actual 0th row and `N` is the Nyquist row (the one with index TotalSize / 2). Those are packed together -// so they need to be unpacked properly after FFT like we did earlier. - -// Reordering for unpacking on load is used with info from the previous pass FFT -using PreviousPassFFTIndexingUtils = workgroup::fft::FFTIndexingUtils; - -struct PreloadedSecondAxisAccessor : PreloadedAccessorMirrorTradeBase -{ - NBL_CONSTEXPR_STATIC_INLINE uint16_t NumWorkgroupsLog2 = ShaderConstevalParameters::NumWorkgroupsLog2; - NBL_CONSTEXPR_STATIC_INLINE uint32_t NumWorkgroups = ShaderConstevalParameters::NumWorkgroups; - NBL_CONSTEXPR_STATIC_INLINE uint32_t PreviousWorkgroupSize = uint32_t(ShaderConstevalParameters::PreviousWorkgroupSize); - NBL_CONSTEXPR_STATIC_INLINE float32_t TotalSizeReciprocal = ShaderConstevalParameters::TotalSizeReciprocal; - NBL_CONSTEXPR_STATIC_INLINE uint16_t KernelSideLength = ShaderConstevalParameters::KernelSideLength; - NBL_CONSTEXPR_STATIC_INLINE float32_t2 KernelHalfPixelSize; - - // When sampling u/v coordinates along the first axis we did an FFT on, workgroup `w` samples at normalized position `SampleSlope * w + KernelHalfPixelSize` - NBL_CONSTEXPR_STATIC_INLINE float32_t SampleSlope = float32_t(KernelSideLength) / float32_t(NumWorkgroups * uint32_t(KernelSideLength + 2)); - - NBL_CONSTEXPR_STATIC_INLINE vector One; - - // ---------------------------------------------------- Utils --------------------------------------------------------- - uint32_t colMajorOffset(uint32_t x, uint32_t y) - { - return x * (NumWorkgroups << 1) | y; // can sum with | here because NumWorkGroups is still PoT (has to match half the TotalSize of previous pass) - } - - uint32_t rowMajorOffset(uint32_t x, uint32_t y) - { - return y * pushConstants.imageRowLength + x; // can no longer sum with | since there's no guarantees on row length - } - - // No channel as parameter since workgroup runs on a single channel - uint64_t getChannelStartOffsetBytes() - { - return uint64_t(glsl::gl_WorkGroupID().y) * pushConstants.channelStrideBytes; - } - // ---------------------------------------------------- End Utils --------------------------------------------------------- - - // Unpacking on load: Has no workgroup shuffles (which become execution barriers) which would be necessary for unpacking on store - - // The lower half of the DFT is retrieved (in bit-reversed order as an N/2 bit number, N being the length of the whole DFT) as the even elements of the Nabla-ordered FFT. - // That is, for the threads in the previous pass you take all `preloaded[0]` elements in thread-ascending order (so preloaded[0] for the 0th thread, then 1st thread etc). - // Then you do the same for the next even index of `preloaded` (`prealoded[2]`, `preloaded[4]`, etc). - // Every two consecutive threads here have to get the value for the row given by `gl_WorkGroupID().x`, for two consecutive columns which were stored as one column holding the packed FFT. - // Except for the special case `gl_WorkGroupID().x = 0`, to unpack the values for the current two columns at `y = NablaFFT[gl_WorkGroupID().x] = DFT[T]` we need the value at - // `NablaFFT[getDFTMirrorIndex(gl_WorkGroupID().x)] = DFT[-T]`. To achieve this, we make the thread with an even ID load the former element and the one with an odd ID load the latter. - // Then, we do a subgroup shuffle (`xor`ing with 1) so each even thread shares its value with their corresponding odd thread. Then they perform a computation to retrieve the value - // corresponding to the column they correspond to. - // The `gl_WorkGroupID().x = 0` case is special because instead of getting the mirror we need to get both zero and nyquist frequencies for the columns, which doesn't happen just by mirror - // indexing. - void preload() - { - // Set up accessor to read in data - const uint64_t channelStartOffsetBytes = getChannelStartOffsetBytes(); - const LegacyBdaAccessor > colMajorAccessor = LegacyBdaAccessor >::create(pushConstants.colMajorBufferAddress + channelStartOffsetBytes); - - // This one shows up a lot so we give it a name - const bool oddThread = glsl::gl_SubgroupInvocationID() & 1u; - - // Since every two consecutive columns are stored as one packed column, we divide the index by 2 to get the index of that packed column - const uint32_t firstIndex = workgroup::SubgroupContiguousIndex() / 2; - int32_t paddedIndex = int32_t(firstIndex) - pushConstants.halfPadding; - const uint32_t evenRow = glsl::gl_WorkGroupID().x + ((glsl::gl_WorkGroupID().x / PreviousWorkgroupSize) * PreviousWorkgroupSize); - const uint32_t y = glsl::gl_WorkGroupID().x ? (oddThread ? PreviousPassFFTIndexingUtils::getNablaMirrorIndex(evenRow) : evenRow) - : (oddThread ? PreviousWorkgroupSize : 0); - - [unroll] - for (uint32_t localElementIndex = 0; localElementIndex < ElementsPerInvocation; localElementIndex++) - { - // If mirrored, we need to invert which thread is loading lo and which is loading hi - // If using zero-padding, useful to find out if we're outside of [0,1) bounds - bool inPadding = paddedIndex < 0 || paddedIndex >= pushConstants.imageHalfRowLength; - int32_t wrappedIndex = paddedIndex < 0 ? ~paddedIndex : paddedIndex; // ~x = - x - 1 in two's complement (except maybe at the borders of representable range) - wrappedIndex = paddedIndex < pushConstants.imageHalfRowLength ? wrappedIndex : pushConstants.imageRowLength + ~paddedIndex; - const complex_t loOrHi = colMajorAccessor.get(colMajorOffset(wrappedIndex, y)); - // Make it a vector so it can be subgroup-shuffled - const vector loOrHiVector = vector (loOrHi.real(), loOrHi.imag()); - const vector otherThreadloOrHiVector = glsl::subgroupShuffleXor< vector >(loOrHiVector, 1u); - const complex_t otherThreadLoOrHi = { otherThreadloOrHiVector.x, otherThreadloOrHiVector.y }; - - if (glsl::gl_WorkGroupID().x) - { - complex_t lo = nbl::hlsl::select(oddThread, otherThreadLoOrHi, loOrHi); - complex_t hi = nbl::hlsl::select(oddThread, loOrHi, otherThreadLoOrHi); - fft::unpack(lo, hi); - - // --------------------------------------------------- MIRROR PADDING ------------------------------------------------------------------------------------------- - #ifdef MIRROR_PADDING - preloaded[localElementIndex] = nbl::hlsl::select(oddThread != inPadding, hi, lo); - // ----------------------------------------------------- ZERO PADDING ------------------------------------------------------------------------------------------- - #else - const complex_t Zero = { scalar_t(0), scalar_t(0) }; - preloaded[localElementIndex] = nbl::hlsl::select(inPadding, Zero, nbl::hlsl::select(oddThread, hi, lo)); - #endif - // ------------------------------------------------ END PADDING DIVERGENCE ---------------------------------------------------------------------------------------- - } - else - { - // `lo` holds `Z0 + iZ1` and `hi` holds `N0 + iN1`. We want at the end for `lo` to hold the packed `Z0 + iN0` and `hi` to hold `Z1 + iN1` - // For the even thread (`oddThread == false`) `lo = loOrHi` and `hi = otherThreadLoOrHi`. For the odd thread the opposite is true - - // Even thread writes `lo = Z0 + iN0` - const complex_t evenThreadLo = { loOrHi.real(), otherThreadLoOrHi.real() }; - // Odd thread writes `hi = Z1 + iN1` - const complex_t oddThreadHi = { otherThreadLoOrHi.imag(), loOrHi.imag() }; - preloaded[localElementIndex] = nbl::hlsl::select(oddThread != inPadding, oddThreadHi, evenThreadLo); - } - paddedIndex += WorkgroupSize / 2; - } - - } - - // Each element on this row is Nabla-ordered. So the element at `x' = index, y' = gl_WorkGroupID().x` that we're operating on is actually the element at - // `x = F(index), y = bitreverse(gl_WorkGroupID().x)` (with the bitreversal done as an N-1 bit number, for `N = log2(TotalSize)` *of the first axist FFT*) - template - void convolve(NBL_REF_ARG(sharedmem_adaptor_t) adaptorForSharedMemory) - { - if (glsl::gl_WorkGroupID().x) - { - const uint32_t y = bitReverseAs(glsl::gl_WorkGroupID().x, NumWorkgroupsLog2); - uint32_t globalElementIndex = workgroup::SubgroupContiguousIndex(); - [unroll] - for (uint32_t localElementIndex = 0; localElementIndex < ElementsPerInvocation; localElementIndex++) - { - const uint32_t indexDFT = FFTIndexingUtils::getDFTIndex(globalElementIndex); - const uint32_t2 texCoords = uint32_t2(indexDFT, y); - const float32_t2 uv = texCoords * float32_t2(TotalSizeReciprocal, SampleSlope) + KernelHalfPixelSize; - const vector sampledKernelVector = vector(kernelChannels.SampleLevel(samplerState, float32_t3(uv, float32_t(glsl::gl_WorkGroupID().y)), 0)); - const vector sampledKernelInterpolatedVector = lerp(sampledKernelVector, One, promote, float32_t>(pushConstants.interpolatingFactor)); - const complex_t sampledKernelInterpolated = { sampledKernelInterpolatedVector.x, sampledKernelInterpolatedVector.y }; - preloaded[localElementIndex] = preloaded[localElementIndex] * sampledKernelInterpolated; - - globalElementIndex += WorkgroupSize; - } - } - // Remember first row holds Z + iN - else - { - uint32_t globalElementIndex = workgroup::SubgroupContiguousIndex(); - [unroll] - for (uint32_t localElementIndex = 0; localElementIndex < ElementsPerInvocation; localElementIndex += 2) - { - complex_t zero = preloaded[localElementIndex]; - // Either wait on FFT pass or previous iteration's workgroup shuffle - adaptorForSharedMemory.workgroupExecutionAndMemoryBarrier(); - complex_t nyquist = getDFTMirror(globalElementIndex, adaptorForSharedMemory); - - fft::unpack(zero, nyquist); - - // We now have zero and Nyquist frequencies at NFFT[index], so we must use `getDFTIndex(index)` to get the actual index into the DFT - const float32_t indexDFT = float32_t(FFTIndexingUtils::getDFTIndex(globalElementIndex)); - - float32_t2 uv = float32_t2(indexDFT * TotalSizeReciprocal, float32_t(0)) + KernelHalfPixelSize; - const vector zeroKernelVector = vector(kernelChannels.SampleLevel(samplerState, float32_t3(uv, float32_t(glsl::gl_WorkGroupID().y)), 0)); - const vector zeroKernelInterpolatedVector = lerp(zeroKernelVector, One, promote, float32_t>(pushConstants.interpolatingFactor)); - const complex_t zeroKernelInterpolated = { zeroKernelInterpolatedVector.x, zeroKernelInterpolatedVector.y }; - zero = zero * zeroKernelInterpolated; - - // Do the same for the nyquist coord - uv.y = 1.f - KernelHalfPixelSize.y; - const vector nyquistKernelVector = vector(kernelChannels.SampleLevel(samplerState, float32_t3(uv, float32_t(glsl::gl_WorkGroupID().y)), 0)); - const vector nyquistKernelInterpolatedVector = lerp(nyquistKernelVector, One, promote, float32_t>(pushConstants.interpolatingFactor)); - const complex_t nyquistKernelInterpolated = { nyquistKernelInterpolatedVector.x, nyquistKernelInterpolatedVector.y }; - nyquist = nyquist * nyquistKernelInterpolated; - - // Since their IFFT is going to be real, we can pack them back as Z + iN, do a single IFFT and recover them afterwards - preloaded[localElementIndex] = zero + rotateLeft(nyquist); - - // We have set Z + iN for an even element (lower half of the DFT). We must now set conj(Z) + i * conj(N) for an odd element (upper half of DFT) - // The logic here is very similar to that in `getDFTMirror`: we figure out which of our odd elements corresponds to the other thread's - // current even element (current even element is `localElementIndex` and our local odd element that's the mirror of the other thread's even element is - // `elementToTradeLocalIdx`. Then we get conj(Z) + i * conj(N) from that thread and send our own via a shuffle. - // Unlike `getDFTMirror` however, the logic is inverted, in the sense that we don't send `preloaded[mirrorLocalIndex]` but rather we receive a value to store there - const complex_t mirrored = conj(zero) + rotateLeft(conj(nyquist)); - vector mirroredVector = { mirrored.real(), mirrored.imag() }; - const FFTMirrorTradeUtils::NablaMirrorLocalInfo info = FFTMirrorTradeUtils::getNablaMirrorLocalInfo(globalElementIndex); - const uint32_t mirrorLocalIndex = info.mirrorLocalIndex; - const uint32_t otherThreadID = info.otherThreadID; - // Make sure the `getDFTMirror` at the top is done - adaptorForSharedMemory.workgroupExecutionAndMemoryBarrier(); - workgroup::Shuffle >::__call(mirroredVector, otherThreadID, adaptorForSharedMemory); - preloaded[mirrorLocalIndex].real(mirroredVector.x); - preloaded[mirrorLocalIndex].imag(mirroredVector.y); - - globalElementIndex += 2 * WorkgroupSize; - } - } - } - - // Save a row back in row major order. Remember that the first row (one with `gl_WorkGroupID().x == 0`) will actually hold the packed IFFT of Zero and Nyquist rows. - void unload() - { - // Set up accessor to write out data - const uint64_t channelStartOffsetBytes = getChannelStartOffsetBytes(); - const LegacyBdaAccessor > rowMajorAccessor = LegacyBdaAccessor >::create(pushConstants.rowMajorBufferAddress + channelStartOffsetBytes); - - const uint32_t firstIndex = workgroup::SubgroupContiguousIndex(); - int32_t paddedIndex = int32_t(firstIndex) - pushConstants.padding; - [unroll] - for (uint32_t localElementIndex = 0; localElementIndex < ElementsPerInvocation; localElementIndex++) - { - if (paddedIndex >= 0 && paddedIndex < pushConstants.imageRowLength) - rowMajorAccessor.set(rowMajorOffset(paddedIndex, glsl::gl_WorkGroupID().x), preloaded[localElementIndex]); - - paddedIndex += WorkgroupSize; - } - } -}; -NBL_CONSTEXPR_STATIC_INLINE float32_t2 PreloadedSecondAxisAccessor::KernelHalfPixelSize = ShaderConstevalParameters::KernelHalfPixelSize; -NBL_CONSTEXPR_STATIC_INLINE vector PreloadedSecondAxisAccessor::One = {1.0f, 0.f}; - -[numthreads(FFTParameters::WorkgroupSize, 1, 1)] -[shader("compute")] -void main(uint32_t3 ID : SV_DispatchThreadID) -{ - SharedMemoryAccessor sharedmemAccessor; - // Set up the memory adaptor - using sharedmem_adaptor_t = accessor_adaptors::StructureOfArrays; - sharedmem_adaptor_t adaptorForSharedMemory; - - PreloadedSecondAxisAccessor preloadedAccessor; - - preloadedAccessor.preload(); - workgroup::FFT::template __call(preloadedAccessor, sharedmemAccessor); - // Update state after FFT run - adaptorForSharedMemory.accessor = sharedmemAccessor; - preloadedAccessor.convolve(adaptorForSharedMemory); - // Remember to update the accessor's state - sharedmemAccessor = adaptorForSharedMemory.accessor; - // Either wait on first FFT (all workgroups but 0) or convolution (only 0th workgroup actually uses sharedmem for convolution) - sharedmemAccessor.workgroupExecutionAndMemoryBarrier(); - workgroup::FFT::template __call(preloadedAccessor, sharedmemAccessor); - preloadedAccessor.unload(); -} \ No newline at end of file diff --git a/28_FFTBloom/app_resources/fft_mirror_common.hlsl b/28_FFTBloom/app_resources/fft_mirror_common.hlsl deleted file mode 100644 index 7a38ec0bb..000000000 --- a/28_FFTBloom/app_resources/fft_mirror_common.hlsl +++ /dev/null @@ -1,46 +0,0 @@ -#include "fft_common.hlsl" - -struct PreloadedAccessorMirrorTradeBase : PreloadedAccessorBase -{ - // Some operations require a thread to have both elements `DFT[T]` and `DFT[-T]` (where the latter is the mirror around Nyquist, or the "negative frequency" of T). For example, - // this is needed when unpacking two different FFTs of real sequences `x, y` from the FFT of a single packed sequence `z = x + iy`. - // Suppose we are on a particular thread, we have an index `globalElementIdx` for a particular element (this index is an index into the Nabla-ordered array, not the proper DFT-ordered one) - // If `NablaFFT[globalElementIdx] = DFT[T]` for some T, first we must find the `otherElementIdx` such that `NablaFFT[otherElementIdx] = DFT[-T]`. This way we know which is the other - // element to get to do something such as unpacking. This is achieved via `FFTIndexingUtils::getNablaMirrorIndex`. - // Once we have the otherElementIdx, we must know which thread holds that other element. Thankfully, this is just the lower bits of the index. - // Now, either for our thread or for the other thread, we need to send an element to the other thread and receive one from them (there is a proof somewhere, might add it to - // readme, that at each step a pair of threads always trades two values between them, there's no long cycles of trades or anything like that. Known exception is the 0th - // element of the first two threads: threads indexed 0 and 1 actually trade with themselves). - // The question is which element. This part is still unproven, but it turns out that at each step the local element index `elementToTradeLocalIdx` of the element - // we need to send is a function of just `localElementIndex`, and it turns out to be the higher bits of `otherElementIdx`. - template - complex_t getDFTMirror(uint32_t globalElementIndex, NBL_REF_ARG(sharedmem_adaptor_t) adaptorForSharedMemory) - { - const FFTMirrorTradeUtils::NablaMirrorLocalInfo info = FFTMirrorTradeUtils::getNablaMirrorLocalInfo(globalElementIndex); - const uint32_t mirrorLocalIndex = info.mirrorLocalIndex; - complex_t toTrade = preloaded[mirrorLocalIndex]; - vector toTradeVector = { toTrade.real(), toTrade.imag() }; - const uint32_t otherThreadID = info.otherThreadID; - workgroup::Shuffle >::__call(toTradeVector, otherThreadID, adaptorForSharedMemory); - toTrade.real(toTradeVector.x); - toTrade.imag(toTradeVector.y); - return toTrade; - } -}; - -struct MultiChannelPreloadedAccessorMirrorTradeBase : MultiChannelPreloadedAccessorBase -{ - template - complex_t getDFTMirror(uint32_t globalElementIndex, uint16_t channel, NBL_REF_ARG(sharedmem_adaptor_t) adaptorForSharedMemory) - { - const FFTMirrorTradeUtils::NablaMirrorLocalInfo info = FFTMirrorTradeUtils::getNablaMirrorLocalInfo(globalElementIndex); - const uint32_t mirrorLocalIndex = info.mirrorLocalIndex; - complex_t toTrade = preloaded[channel][mirrorLocalIndex]; - vector toTradeVector = { toTrade.real(), toTrade.imag() }; - const uint32_t otherThreadID = info.otherThreadID; - workgroup::Shuffle >::__call(toTradeVector, otherThreadID, adaptorForSharedMemory); - toTrade.real(toTradeVector.x); - toTrade.imag(toTradeVector.y); - return toTrade; - } -}; \ No newline at end of file diff --git a/28_FFTBloom/app_resources/image_fft_first_axis.hlsl b/28_FFTBloom/app_resources/image_fft_first_axis.hlsl deleted file mode 100644 index f1478a8d6..000000000 --- a/28_FFTBloom/app_resources/image_fft_first_axis.hlsl +++ /dev/null @@ -1,96 +0,0 @@ -#include "fft_common.hlsl" - -[[vk::binding(0, 0)]] Texture2D texture; -[[vk::binding(4, 0)]] SamplerState samplerState; -// -------------------------------------------- FIRST AXIS FFT ------------------------------------------------------------------ - -// Each Workgroup computes the FFT along two consecutive vertical scanlines (fixed x for the whole Workgroup) so we use `2 * gl_WorkGroupID().x, 2 * gl_WorkGroupID().x + 1` -// to get the x coordinates for each of the consecutive lines. This time we launch `inputImageSize.x / 2` workgroups - -// After first axis FFT we store results packed. This is because unpacking on load is faster: Unpacking on store would require many workgroup shuffles, which incur a barrier -struct PreloadedFirstAxisAccessor : MultiChannelPreloadedAccessorBase -{ - // ---------------------------------------------------- Utils --------------------------------------------------------- - - // After first FFT we only store half of a column, so the offset per column is half the image side length - uint32_t colMajorOffset(uint32_t x, uint32_t y) - { - return x * TotalSize | y; - } - - // Each channel after first FFT will be stored as half the image (every two columns have been packed into one column of complex numbers) in col-major order. - // We launch one workgroup every two columns in the image, so there are `glsl::gl_NumWorkGroups().x` columns (of complex elements) per channel, - // each of size `FFTParameters::TotalSize` - uint64_t getChannelStartOffsetBytes(uint16_t channel) - { - return uint64_t(channel) * glsl::gl_NumWorkGroups().x * TotalSize * sizeof(complex_t); - } - - // ---------------------------------------------------- End Utils --------------------------------------------------------- - - void preload() - { - float32_t2 normalizedCoordsFirstLine, normalizedCoordsSecondLine; - normalizedCoordsFirstLine.x = float32_t(glsl::gl_WorkGroupID().x) * pushConstants.imageTwoPixelSize_x + pushConstants.imageHalfPixelSize.x; - normalizedCoordsSecondLine.x = normalizedCoordsFirstLine.x + pushConstants.imagePixelSize.x; - normalizedCoordsFirstLine.y = (int32_t(workgroup::SubgroupContiguousIndex()) - pushConstants.padding) * pushConstants.imagePixelSize.y + pushConstants.imageHalfPixelSize.y; - - [unroll] - for (uint32_t localElementIndex = 0; localElementIndex < ElementsPerInvocation; localElementIndex++) - { - const float32_t4 firstLineTexValue = texture.SampleLevel(samplerState, normalizedCoordsFirstLine, 0); - - [unroll] - for (uint16_t channel = 0; channel < Channels; channel++) - preloaded[channel][localElementIndex].real(scalar_t(firstLineTexValue[channel])); - - normalizedCoordsSecondLine.y = normalizedCoordsFirstLine.y; - const float32_t4 secondLineTexValue = texture.SampleLevel(samplerState, normalizedCoordsSecondLine, 0); - - [unroll] - for (uint16_t channel = 0; channel < Channels; channel++) - preloaded[channel][localElementIndex].imag(scalar_t(secondLineTexValue[channel])); - - normalizedCoordsFirstLine.y += pushConstants.imageWorkgroupSizePixelSize_y; - } - } - - // Once the FFT is done, each thread should write its elements back. Storage is in column-major order because this avoids cache misses when writing. - // Channels will be contiguous in buffer memory. - void unload() - { - for (uint16_t channel = 0; channel < Channels; channel++) - { - const uint64_t channelStartOffsetBytes = getChannelStartOffsetBytes(channel); - const LegacyBdaAccessor > colMajorAccessor = LegacyBdaAccessor >::create(pushConstants.colMajorBufferAddress + channelStartOffsetBytes); - - uint32_t globalElementIndex = workgroup::SubgroupContiguousIndex(); - [unroll] - for (uint32_t localElementIndex = 0; localElementIndex < ElementsPerInvocation; localElementIndex++) - { - colMajorAccessor.set(colMajorOffset(glsl::gl_WorkGroupID().x, globalElementIndex), preloaded[channel][localElementIndex]); - globalElementIndex += WorkgroupSize; - } - } - } -}; - -[numthreads(FFTParameters::WorkgroupSize, 1, 1)] -[shader("compute")] -void main(uint32_t3 ID : SV_DispatchThreadID) -{ - SharedMemoryAccessor sharedmemAccessor; - PreloadedFirstAxisAccessor preloadedAccessor; - - preloadedAccessor.preload(); - - for (uint16_t channel = 0; channel < Channels; channel++) - { - preloadedAccessor.currentChannel = channel; - // Wait on previous FFT pass to ensure no thread is on previous FFT trying to read from sharedmem - if (channel) - sharedmemAccessor.workgroupExecutionAndMemoryBarrier(); - workgroup::FFT::template __call(preloadedAccessor, sharedmemAccessor); - } - preloadedAccessor.unload(); -} \ No newline at end of file diff --git a/28_FFTBloom/app_resources/image_ifft_first_axis.hlsl b/28_FFTBloom/app_resources/image_ifft_first_axis.hlsl deleted file mode 100644 index b3bef3510..000000000 --- a/28_FFTBloom/app_resources/image_ifft_first_axis.hlsl +++ /dev/null @@ -1,161 +0,0 @@ -#include "fft_mirror_common.hlsl" - -[[vk::binding(2, 0)]] RWTexture2D convolvedImage; - - - -// -------------------------------------------- FIRST AXIS IFFT ------------------------------------------------------------------ - -// Previous shader stored results unpacked, and we re-pack them before IFFT here. Although re-packing on store achieves a much higher L2 cache hit ratio and depending on the implementation it can be done -// with no barriers (and reads that all over the place) OR half the amount of barriers done here with the same accesses to memory, the SM throughput inexplicably falls by about 5%. -// So re-pack on load it is. - -struct PreloadedFirstAxisAccessor : MultiChannelPreloadedAccessorMirrorTradeBase -{ - // ---------------------------------------------------- Utils --------------------------------------------------------- - uint32_t rowMajorOffset(uint32_t x, uint32_t y) - { - return y * pushConstants.imageRowLength + x; // can no longer sum with | since there's no guarantees on row length - } - - // Same numbers as forward FFT - uint64_t getChannelStartOffsetBytes(uint16_t channel) - { - return uint64_t(channel) * glsl::gl_NumWorkGroups().x * TotalSize * sizeof(complex_t); - } - - // ---------------------------------------------------- End Utils --------------------------------------------------------- - - // Each column of the data currently stored in the rowMajorBuffer corresponds to (half) a column of the DFT of a column of the convolved image. With this in mind, knowing that the IFFT will yield - // a real result, we can pack two consecutive columns as Z = C1 + iC2 and by linearity of DFT we get IFFT(C1) = Re(IFFT(Z)), IFFT(C2) = Im(IFFT(Z)). This is the inverse of the packing trick - // in the forward FFT, with a much easier expression. - // When we wrote the columns after the forward FFT, each thread wrote its even elements to the buffer. So it stands to reason that if we load the elements from each column in the same way, - // we can load each thread's even elements. Their odd elements, however, are the conjugates of even elements of some other threads - which element of which thread follows the same logic we used to - // unpack the FFT in the forward step. - // Since complex conjugation is not linear, we cannot simply store two columns and pass around their conjugates. We load one, trade, then load the other, trade again. - template - void preload(NBL_REF_ARG(sharedmem_adaptor_t) adaptorForSharedMemory) - { - for (uint16_t channel = 0; channel < Channels; channel++) - { - const uint64_t channelStartOffsetBytes = getChannelStartOffsetBytes(channel); - // Set LegacyBdaAccessor for reading - const LegacyBdaAccessor > rowMajorAccessor = LegacyBdaAccessor >::create(pushConstants.rowMajorBufferAddress + channelStartOffsetBytes); - - uint32_t globalElementIndex = workgroup::SubgroupContiguousIndex(); - // Load all even elements of first column - [unroll] - for (uint32_t localElementIndex = 0; localElementIndex < (ElementsPerInvocation / 2); localElementIndex++) - { - preloaded[channel][localElementIndex << 1] = rowMajorAccessor.get(rowMajorOffset(2 * glsl::gl_WorkGroupID().x, globalElementIndex)); - globalElementIndex += WorkgroupSize; - } - // Get all odd elements by trading - // Reset globalElementIndex - Add WorkgroupSize to account for `localElementIndex` starting at 1 - globalElementIndex = WorkgroupSize | workgroup::SubgroupContiguousIndex(); - [unroll] - for (uint32_t localElementIndex = 1; localElementIndex < ElementsPerInvocation; localElementIndex += 2) - { - preloaded[channel][localElementIndex] = conj(getDFTMirror(globalElementIndex, channel, adaptorForSharedMemory)); - // Add 2 * WorkgroupSize since `localElementIndex` moves in strides of 2 - globalElementIndex += 2 * WorkgroupSize; - adaptorForSharedMemory.workgroupExecutionAndMemoryBarrier(); - } - // Load even elements of second column, multiply them by i and add them to even positions - // This makes even positions hold C1 + iC2 - // Reset globalElementIndex - globalElementIndex = workgroup::SubgroupContiguousIndex(); - [unroll] - for (uint32_t localElementIndex = 0; localElementIndex < (ElementsPerInvocation / 2); localElementIndex++) - { - preloaded[channel][localElementIndex << 1] = preloaded[channel][localElementIndex << 1] + rotateLeft(rowMajorAccessor.get(rowMajorOffset(2 * glsl::gl_WorkGroupID().x + 1, globalElementIndex))); - globalElementIndex += WorkgroupSize; - } - // Finally, trade to get odd elements of second column. Note that by trading we receive an element of the form C1 + iC2 for an even position. The current odd position holds conj(C1) and we - // want it to hold conj(C1) + i*conj(C2). So we first do conj(C1 + iC2) to yield conj(C1) - i*conj(C2). Then we subtract conj(C1) to get -i*conj(C2), negate that to get i * conj(C2), and finally - // add conj(C1) back to have conj(C1) + i * conj(C2). - // Reset globalElementIndex - Add WorkgroupSize to account for `localElementIndex` starting at 1 - globalElementIndex = WorkgroupSize | workgroup::SubgroupContiguousIndex(); - [unroll] - for (uint32_t localElementIndex = 1; localElementIndex < ElementsPerInvocation; localElementIndex += 2) - { - complex_t otherThreadEven = conj(getDFTMirror(globalElementIndex, channel, adaptorForSharedMemory)); - if (workgroup::SubgroupContiguousIndex() || localElementIndex != 1) - { - otherThreadEven = otherThreadEven - preloaded[channel][localElementIndex]; - otherThreadEven = otherThreadEven * scalar_t(-1); - preloaded[channel][localElementIndex] = preloaded[channel][localElementIndex] + otherThreadEven; - } - // Thread 0's first odd element is Nyquist, which was packed alongside Zero - this means that what was said above breaks in this particular case and needs special treatment - else - { - // preloaded[channel][1] currently holds trash - this is because 0 and Nyquist are the only fixed points of T -> -T. - // preloaded[channel][0] currently holds (C1(Z) - C2(N)) + i * (C1(N) + C2(Z)). This is because of how we loaded the even elements of both columns. - // We want preloaded[channel][0] to hold C1(Z) + i * C2(Z) and preloaded[channel][1] to hold C1(N) + i * C2(N). - // We can re-load C2(Z) + i * C2(N) and use it to unpack the values - complex_t c2 = rowMajorAccessor.get(rowMajorOffset(2 * glsl::gl_WorkGroupID().x + 1, 0)); - complex_t p1 = { preloaded[channel][0].imag() - c2.real(), c2.imag() }; - preloaded[channel][1] = p1; - complex_t p0 = { preloaded[channel][0].real() + c2.imag() , c2.real() }; - preloaded[channel][0] = p0; - } - // Add 2 * WorkgroupSize since `localElementIndex` moves in strides of 2 - globalElementIndex += 2 * WorkgroupSize; - adaptorForSharedMemory.workgroupExecutionAndMemoryBarrier(); - } - } - } - - void unload() - { - const uint32_t firstIndex = workgroup::SubgroupContiguousIndex(); - int32_t paddedIndex = int32_t(firstIndex) - pushConstants.padding; - - [unroll] - for (uint32_t localElementIndex = 0; localElementIndex < ElementsPerInvocation; localElementIndex++) - { - if (paddedIndex >= 0 && paddedIndex < pushConstants.imageColumnLength) - { - vector firstLineTexValue, secondLineTexValue; - // In case we're not using alpha - firstLineTexValue.a = 1.f; - secondLineTexValue.a = 1.f; - - [unroll] - for (uint16_t channel = 0; channel < Channels; channel++) - { - firstLineTexValue[channel] = scalar_t(preloaded[channel][localElementIndex].real()); - secondLineTexValue[channel] = scalar_t(preloaded[channel][localElementIndex].imag()); - } - convolvedImage[uint32_t2(2 * glsl::gl_WorkGroupID().x, paddedIndex)] = firstLineTexValue; - convolvedImage[uint32_t2(2 * glsl::gl_WorkGroupID().x + 1, paddedIndex)] = secondLineTexValue; - } - paddedIndex += WorkgroupSize; - } - } -}; - -[numthreads(FFTParameters::WorkgroupSize, 1, 1)] -[shader("compute")] -void main(uint32_t3 ID : SV_DispatchThreadID) -{ - SharedMemoryAccessor sharedmemAccessor; - // Set up the memory adaptor - using sharedmem_adaptor_t = accessor_adaptors::StructureOfArrays; - sharedmem_adaptor_t adaptorForSharedMemory; - adaptorForSharedMemory.accessor = sharedmemAccessor; - - PreloadedFirstAxisAccessor preloadedAccessor; - preloadedAccessor.preload(adaptorForSharedMemory); - // Update state after preload - sharedmemAccessor = adaptorForSharedMemory.accessor; - - for (uint16_t channel = 0; channel < Channels; channel++) - { - preloadedAccessor.currentChannel = channel; - if (channel) - sharedmemAccessor.workgroupExecutionAndMemoryBarrier(); - workgroup::FFT::template __call(preloadedAccessor, sharedmemAccessor); - } - preloadedAccessor.unload(); -} diff --git a/28_FFTBloom/app_resources/kernel_fft_first_axis.hlsl b/28_FFTBloom/app_resources/kernel_fft_first_axis.hlsl deleted file mode 100644 index 741bac7db..000000000 --- a/28_FFTBloom/app_resources/kernel_fft_first_axis.hlsl +++ /dev/null @@ -1,87 +0,0 @@ -#include "fft_common.hlsl" - -[[vk::binding(0, 0)]] Texture2D texture; - -// -------------------------------------------- FIRST AXIS FFT ------------------------------------------------------------------ - -// Each Workgroup computes the FFT along two consecutive vertical scanlines (fixed x for the whole Workgroup) so we use `2 * gl_WorkGroupID().x, 2 * gl_WorkGroupID().x + 1` -// to get the x coordinates for each of the consecutive lines -// Since the output images (one per channel) are square of size TotalSize (defined above) we will be launching half that amount of workgroups - -struct PreloadedFirstAxisAccessor : MultiChannelPreloadedAccessorBase -{ - // ---------------------------------------------------- Utils --------------------------------------------------------- - - uint32_t colMajorOffset(uint32_t x, uint32_t y) - { - return x * TotalSize | y; - } - - // Each channel after first FFT will be stored as half the image (every two columns have been packed into one column of complex numbers) in col-major order, and the whole size of the image is N^2, - // for N = TotalSize - uint64_t getChannelStartOffsetBytes(uint16_t channel) - { - return uint64_t(channel) * TotalSize * TotalSize / 2 * sizeof(complex_t); - } - - // ---------------------------------------------------- End Utils --------------------------------------------------------- - - void preload() - { - uint32_t globalElementIndex = workgroup::SubgroupContiguousIndex(); - [unroll] - for (uint32_t localElementIndex = 0; localElementIndex < ElementsPerInvocation; localElementIndex++) - { - const float32_t4 firstLineTexValue = texture[uint32_t2(2 * glsl::gl_WorkGroupID().x, globalElementIndex)]; - [unroll] - for (uint16_t channel = 0; channel < Channels; channel++) - preloaded[channel][localElementIndex].real(scalar_t(firstLineTexValue[channel])); - - const float32_t4 secondLineTexValue = texture[uint32_t2(2 * glsl::gl_WorkGroupID().x + 1, globalElementIndex)]; - [unroll] - for (uint16_t channel = 0; channel < Channels; channel++) - preloaded[channel][localElementIndex].imag(scalar_t(secondLineTexValue[channel])); - - globalElementIndex += WorkgroupSize; - } - } - - // Once the FFT is done, each thread should write its elements back. Storage is in column-major order because this avoids cache misses when writing. - // Channels will be contiguous in buffer memory. - void unload() - { - [unroll] - for (uint16_t channel = 0; channel < Channels; channel++) - { - const uint64_t channelStartOffsetBytes = getChannelStartOffsetBytes(channel); - const LegacyBdaAccessor > colMajorAccessor = LegacyBdaAccessor >::create(pushConstants.colMajorBufferAddress + channelStartOffsetBytes); - - uint32_t globalElementIndex = workgroup::SubgroupContiguousIndex(); - [unroll] - for (uint32_t localElementIndex = 0; localElementIndex < ElementsPerInvocation; localElementIndex++) - { - colMajorAccessor.set(colMajorOffset(glsl::gl_WorkGroupID().x, globalElementIndex), preloaded[channel][localElementIndex]); - globalElementIndex += WorkgroupSize; - } - } - } -}; - -[numthreads(FFTParameters::WorkgroupSize, 1, 1)] -[shader("compute")] -void main(uint32_t3 ID : SV_DispatchThreadID) -{ - SharedMemoryAccessor sharedmemAccessor; - PreloadedFirstAxisAccessor preloadedAccessor; - - preloadedAccessor.preload(); - [unroll] - for (uint16_t channel = 0; channel < Channels; channel++) - { - preloadedAccessor.currentChannel = channel; - if (channel) - sharedmemAccessor.workgroupExecutionAndMemoryBarrier(); - workgroup::FFT::template __call(preloadedAccessor, sharedmemAccessor); - } - preloadedAccessor.unload(); -} \ No newline at end of file diff --git a/28_FFTBloom/app_resources/kernel_fft_second_axis.hlsl b/28_FFTBloom/app_resources/kernel_fft_second_axis.hlsl deleted file mode 100644 index eca81e859..000000000 --- a/28_FFTBloom/app_resources/kernel_fft_second_axis.hlsl +++ /dev/null @@ -1,222 +0,0 @@ -#include "fft_mirror_common.hlsl" -#include "nbl/builtin/hlsl/colorspace/encodeCIEXYZ.hlsl" - -[[vk::binding(2, 0)]] RWTexture2DArray kernelChannels; - -// ------------------------------------------ SECOND AXIS FFT ------------------------------------------------------------- -// This time each Workgroup will compute the FFT along a horizontal line (fixed y for the whole Workgroup). We get the y coordinate for the row a workgroup is working on via `gl_WorkGroupID().x`. -// After first axis FFT, for every two columns we have saved one whole column with the packed FFT of both columns. Since they're real signals, the FFT of each is conjugate-mirrored -// (meaning we only need to retrieve the lower half of the DFT after unpacking, since the result of the FFT for any row after Nyquist can be obtained as a conjugate-inverse: see wikipedia -// for more info on how `DFT(conj(Z))` relates to `DFT(Z)`). -// We have thus launched FFTParameters::TotalSize / 2 workgroups, as there are exactly that amount of rows to be computed. -// Since Z and N are real signals (where `Z` is the actual 0th row and `N` is the Nyquist row, as in the one with index FFTParameters::TotalSize / 2) we can pack those together again for an FFT. -// We unpack values on load - -struct PreloadedSecondAxisAccessor : MultiChannelPreloadedAccessorMirrorTradeBase -{ - NBL_CONSTEXPR_STATIC_INLINE uint32_t PreviousWorkgroupSize = uint32_t(ShaderConstevalParameters::PreviousWorkgroupSize); - - // ---------------------------------------------------- Utils --------------------------------------------------------- - uint32_t colMajorOffset(uint32_t x, uint32_t y) - { - return x * TotalSize | y; - } - - uint64_t getChannelStartOffsetBytes(uint16_t channel) - { - return uint64_t(channel) * TotalSize * TotalSize / 2 * sizeof(complex_t); - } - - // ---------------------------------------------------- End Utils --------------------------------------------------------- - - // Unpacking on load: Has no workgroup shuffles (which become execution barriers) which would be necessary for unpacking on store - - // The lower half of the DFT is retrieved (in bit-reversed order as an N/2 bit number, N being the length of the whole DFT) as the even elements of the Nabla-ordered FFT. - // That is, for the threads in the previous pass you take all `preloaded[0]` elements in thread-ascending order (so preloaded[0] for the 0th thread, then 1st thread etc). - // Then you do the same for the next even index of `preloaded` (`prealoded[2]`, `preloaded[4]`, etc). - // Every two consecutive threads here have to get the value for the row given by `gl_WorkGroupID().x`, for two consecutive columns which were stored as one column holding the packed FFT. - // Except for the special case `gl_WorkGroupID().x = 0`, to unpack the values for the current two columns at `y = NablaFFT[gl_WorkGroupID().x] = DFT[T]` we need the value at - // `NablaFFT[getDFTMirrorIndex(gl_WorkGroupID().x)] = DFT[-T]`. To achieve this, we make the thread with an even ID load the former element and the one with an odd ID load the latter. - // Then, we do a subgroup shuffle (`xor`ing with 1) so each even thread shares its value with their corresponding odd thread. Then they perform a computation to retrieve the value - // corresponding to the column they correspond to. - // The `gl_WorkGroupID().x = 0` case is special because instead of getting the mirror we need to get both zero and nyquist frequencies for the columns, which doesn't happen just by mirror - // indexing. - void preload() - { - // This one shows up a lot so we give it a name - const bool oddThread = glsl::gl_SubgroupInvocationID() & 1u; - - if (glsl::gl_WorkGroupID().x) - { - // Even thread must index a y corresponding to an even element of the previous FFT pass, and the odd thread must index its DFT Mirror - // The math here essentially ensues we enumerate all even elements in order: we alternate `PreviousWorkgroupSize` even elements (all `preloaded[0]` elements of - // the previous pass' threads), then `PreviousWorkgroupSize` odd elements (`preloaded[1]`) and so on - const uint32_t evenRow = glsl::gl_WorkGroupID().x + ((glsl::gl_WorkGroupID().x / PreviousWorkgroupSize) * PreviousWorkgroupSize); - const uint32_t y = oddThread ? FFTIndexingUtils::getNablaMirrorIndex(evenRow) : evenRow; - [unroll] - for (uint16_t channel = 0; channel < Channels; channel++) - { - const uint64_t channelStartOffsetBytes = getChannelStartOffsetBytes(channel); - // Set LegacyBdaAccessor for reading - const LegacyBdaAccessor > colMajorAccessor = LegacyBdaAccessor >::create(pushConstants.colMajorBufferAddress + channelStartOffsetBytes); - - // Since every two consecutive columns are stored as one packed column, we divide the index by 2 to get the index of that packed column - uint32_t packedColumnIndex = workgroup::SubgroupContiguousIndex() / 2; - [unroll] - for (uint32_t localElementIndex = 0; localElementIndex < ElementsPerInvocation; localElementIndex++) - { - const complex_t loOrHi = colMajorAccessor.get(colMajorOffset(packedColumnIndex, y)); - // Make it a vector so it can be subgroup-shuffled - const vector loOrHiVector = vector (loOrHi.real(), loOrHi.imag()); - const vector otherThreadloOrHiVector = glsl::subgroupShuffleXor< vector >(loOrHiVector, 1u); - const complex_t otherThreadLoOrHi = { otherThreadloOrHiVector.x, otherThreadloOrHiVector.y }; - complex_t lo = nbl::hlsl::select(oddThread, otherThreadLoOrHi, loOrHi); - complex_t hi = nbl::hlsl::select(oddThread, loOrHi, otherThreadLoOrHi); - fft::unpack(lo, hi); - preloaded[channel][localElementIndex] = nbl::hlsl::select(oddThread, hi, lo); - - packedColumnIndex += WorkgroupSize / 2; - } - } - } - // Special case where we retrieve 0 and Nyquist - else - { - // Even thread retrieves Zero, odd thread retrieves Nyquist. Zero is always `preloaded[0]` of the previous FFT's 0th thread, while Nyquist is always `preloaded[1]` of that same thread. - // Therefore we know Nyquist ends up exactly at y = PreviousWorkgroupSize - const uint32_t y = oddThread ? PreviousWorkgroupSize : 0; - [unroll] - for (uint16_t channel = 0; channel < Channels; channel++) - { - const uint64_t channelStartOffsetBytes = getChannelStartOffsetBytes(channel); - // Set LegacyBdaAccessor for reading - const LegacyBdaAccessor > colMajorAccessor = LegacyBdaAccessor >::create(pushConstants.colMajorBufferAddress + channelStartOffsetBytes); - - // Since every two consecutive columns are stored as one packed column, we divide the index by 2 to get the index of that packed column - uint32_t packedColumnIndex = workgroup::SubgroupContiguousIndex() / 2; - [unroll] - for (uint32_t localElementIndex = 0; localElementIndex < ElementsPerInvocation; localElementIndex++) - { - const complex_t loOrHi = colMajorAccessor.get(colMajorOffset(packedColumnIndex, y)); - // Make it a vector so it can be subgroup-shuffled - const vector loOrHiVector = vector (loOrHi.real(), loOrHi.imag()); - const vector otherThreadloOrHiVector = glsl::subgroupShuffleXor< vector >(loOrHiVector, 1u); - const complex_t otherThreadLoOrHi = { otherThreadloOrHiVector.x, otherThreadloOrHiVector.y }; - - // `lo` holds `Z0 + iZ1` and `hi` holds `N0 + iN1`. We want at the end for `lo` to hold the packed `Z0 + iN0` and `hi` to hold `Z1 + iN1` - // For the even thread (`oddThread == false`) `lo = loOrHi` and `hi = otherThreadLoOrHi`. For the odd thread the opposite is true - - // Even thread writes `lo = Z0 + iN0` - const complex_t evenThreadLo = { loOrHi.real(), otherThreadLoOrHi.real() }; - // Odd thread writes `hi = Z1 + iN1` - const complex_t oddThreadHi = { otherThreadLoOrHi.imag(), loOrHi.imag() }; - preloaded[channel][localElementIndex] = nbl::hlsl::select(oddThread, oddThreadHi, evenThreadLo); - - packedColumnIndex += WorkgroupSize / 2; - } - } - } - } - - // Write spectra in their right positions - template - void unload(NBL_REF_ARG(sharedmem_adaptor_t) adaptorForSharedMemory) - { - NBL_CONSTEXPR_STATIC_INLINE uint16_t NumWorkgroupsLog2 = ShaderConstevalParameters::NumWorkgroupsLog2; - - // Most rows have just have to reflect their values along the Nyquist row. - // If you'll remember, however, the first axis FFT stored the lower half of the DFT, bit-reversed not as a `log2(FFTSize)` bit number but in the range of the lower half - // (so as a `log2(FFTSize / 2) bit number)`. Which means that whenever we get an element at `x' = index`, `y' = gl_WorkGroupID().x` we must get the actual coordinates of - // the element in the DFT with `x = F(x')` and `y = bitreverse(y')` - if (glsl::gl_WorkGroupID().x) - { - const uint32_t y = bitReverseAs(glsl::gl_WorkGroupID().x, NumWorkgroupsLog2); - [unroll] - for (uint16_t channel = 0; channel < Channels; channel++) - { - uint32_t globalElementIndex = workgroup::SubgroupContiguousIndex(); - [unroll] - for (uint32_t localElementIndex = 0; localElementIndex < ElementsPerInvocation; localElementIndex++) - { - // Get actual x,y coordinates for the element we wish to write - const uint32_t x = FFTIndexingUtils::getDFTIndex(globalElementIndex); - - const vector toStoreVector = { preloaded[channel][localElementIndex].real(), preloaded[channel][localElementIndex].imag() }; - kernelChannels[uint32_t3(x, y, channel)] = toStoreVector; - - globalElementIndex += WorkgroupSize; - } - } - } - // Remember that the first row has packed `Z + iN` so it has to unpack those - else - { - [unroll] - for (uint16_t channel = 0; channel < Channels; channel++) - { - uint32_t globalElementIndex = workgroup::SubgroupContiguousIndex(); - // FFT[Z + iN] was stored in the Nabla order - [unroll] - for (uint32_t localElementIndex = 0; localElementIndex < ElementsPerInvocation; localElementIndex++) - { - complex_t zero = preloaded[channel][localElementIndex]; - // If one shuffle has already occurred, need to wait on every thread having read sharedmem before doing another - // Otherwise, we still need to wait for the last FFT to be done with sharedmem - adaptorForSharedMemory.workgroupExecutionAndMemoryBarrier(); - complex_t nyquist = getDFTMirror(globalElementIndex, channel, adaptorForSharedMemory); - fft::unpack(zero, nyquist); - - // We now have zero and Nyquist frequencies at NFFT[globalElementIndex], so we must use `getDFTIndex(index)` to get the actual index into the DFT - const uint32_t globalElementIndexDFT = FFTIndexingUtils::getDFTIndex(globalElementIndex); - - // Store zeroth element - const uint32_t2 zeroCoord = uint32_t2(globalElementIndexDFT, 0); - const vector zeroVector = { zero.real(), zero.imag() }; - kernelChannels[uint32_t3(zeroCoord, channel)] = zeroVector; - - // Store nyquist element - const uint32_t2 nyquistCoord = uint32_t2(globalElementIndexDFT, TotalSize / 2); - const vector nyquistVector = { nyquist.real(), nyquist.imag() }; - kernelChannels[uint32_t3(nyquistCoord, channel)] = nyquistVector; - - globalElementIndex += WorkgroupSize; - - // Also save the result of unpacking for later in case it's the channelWiseSum - real part of element at (0,0) of a channel - if (!workgroup::SubgroupContiguousIndex() && !localElementIndex) - preloaded[channel][localElementIndex] = zero; - } - } - // Before leaving, store the reciprocal of power to the row major buffer start since we have that available - if (!workgroup::SubgroupContiguousIndex()) - { - const vector channelWiseSums = { preloaded[0][0].real(), preloaded[1][0].real(), preloaded[2][0].real() }; - const scalar_t power = mul(vector(colorspace::scRGBtoXYZ._m10_m11_m12), channelWiseSums); - vk::RawBufferStore(pushConstants.rowMajorBufferAddress, scalar_t(1) / power); - } - } - } -}; - -[numthreads(FFTParameters::WorkgroupSize, 1, 1)] -[shader("compute")] -void main(uint32_t3 ID : SV_DispatchThreadID) -{ - SharedMemoryAccessor sharedmemAccessor; - PreloadedSecondAxisAccessor preloadedAccessor; - - preloadedAccessor.preload(); - [unroll] - for (uint16_t channel = 0; channel < Channels; channel++) - { - preloadedAccessor.currentChannel = channel; - // Except for the first FFT, wait on the previous one to be done to ensure sharedmem memory usage does not overlap - if (channel) - sharedmemAccessor.workgroupExecutionAndMemoryBarrier(); - workgroup::FFT::template __call(preloadedAccessor, sharedmemAccessor); - } - // Set up the memory adaptor - using sharedmem_adaptor_t = accessor_adaptors::StructureOfArrays; - sharedmem_adaptor_t adaptorForSharedMemory; - adaptorForSharedMemory.accessor = sharedmemAccessor; - preloadedAccessor.unload(adaptorForSharedMemory); -} \ No newline at end of file diff --git a/28_FFTBloom/app_resources/kernel_spectrum_normalize.hlsl b/28_FFTBloom/app_resources/kernel_spectrum_normalize.hlsl deleted file mode 100644 index efe406301..000000000 --- a/28_FFTBloom/app_resources/kernel_spectrum_normalize.hlsl +++ /dev/null @@ -1,21 +0,0 @@ -#include "common.hlsl" -[[vk::binding(2, 0)]] RWTexture2DArray kernelChannels; - -[numthreads(8, 8, 1)] -[shader("compute")] -void main(uint32_t3 ID : SV_DispatchThreadID) -{ - const scalar_t powerReciprocal = vk::RawBufferLoad(pushConstants.rowMajorBufferAddress); - const uint32_t2 texCoord = uint32_t2(glsl::gl_GlobalInvocationID().x, glsl::gl_GlobalInvocationID().y); - const scalar_t shift = ((texCoord.x + texCoord.y) & 1) ? scalar_t(-1) : scalar_t(1); - const scalar_t shiftOverPower = shift * powerReciprocal; - // Kernel spectrum image conserves the original width but height is half + 1 (we don't need the other half since it's redundant) - if (all(texCoord < uint32_t2(FFTParameters::TotalSize, FFTParameters::TotalSize / 2 + 1))) - { - [unroll] - for (uint32_t channel = 0; channel < Channels; channel++) - { - kernelChannels[uint32_t3(texCoord, channel)] *= shiftOverPower; - } - } -} \ No newline at end of file diff --git a/28_FFTBloom/config.json.template b/28_FFTBloom/config.json.template deleted file mode 100644 index 717d05d53..000000000 --- a/28_FFTBloom/config.json.template +++ /dev/null @@ -1,28 +0,0 @@ -{ - "enableParallelBuild": true, - "threadsPerBuildProcess" : 2, - "isExecuted": false, - "scriptPath": "", - "cmake": { - "configurations": [ "Release", "Debug", "RelWithDebInfo" ], - "buildModes": [], - "requiredOptions": [] - }, - "profiles": [ - { - "backend": "vulkan", // should be none - "platform": "windows", - "buildModes": [], - "runConfiguration": "Release", // we also need to run in Debug nad RWDI because foundational example - "gpuArchitectures": [] - } - ], - "dependencies": [], - "data": [ - { - "dependencies": [], - "command": [""], - "outputs": [] - } - ] -} \ No newline at end of file diff --git a/28_FFTBloom/main.cpp b/28_FFTBloom/main.cpp deleted file mode 100644 index e8ec015e3..000000000 --- a/28_FFTBloom/main.cpp +++ /dev/null @@ -1,1418 +0,0 @@ -// Copyright (C) 2018-2024 - 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 "nbl/examples/examples.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; - -#include "app_resources/common.hlsl" -#include "nbl/builtin/hlsl/bit.hlsl" - - - -// Defaults that match this example's image -constexpr uint32_t WIN_W = 1280; -constexpr uint32_t WIN_H = 720; - -class FFTBloomApp final : public SimpleWindowedApplication, public BuiltinResourcesApplication -{ - using device_base_t = SimpleWindowedApplication; - using asset_base_t = BuiltinResourcesApplication; - using clock_t = std::chrono::steady_clock; - - // Windowed App members - constexpr static inline uint32_t MaxFramesInFlight = 3u; - smart_refctd_ptr m_window; - smart_refctd_ptr> m_surface; - smart_refctd_ptr m_timeline; - uint64_t m_realFrameIx = 0; - std::array, MaxFramesInFlight> m_cmdBufs; - ISimpleManagedSurface::SAcquireResult m_currentImageAcquire = {}; - - // Persistent compute Pipelines - smart_refctd_ptr m_firstAxisFFTPipeline; - smart_refctd_ptr m_lastAxisFFT_convolution_lastAxisIFFTPipeline; - smart_refctd_ptr m_firstAxisIFFTPipeline; - - // Universal descriptor set - smart_refctd_ptr m_descriptorSet; - - // Utils - smart_refctd_ptr m_utils; - - // Resources - smart_refctd_ptr m_srcImageView; - smart_refctd_ptr m_kerImageView; - smart_refctd_ptr m_outImgView; - smart_refctd_ptr m_kernelNormalizedSpectrums; - - // Used to store intermediate results - smart_refctd_ptr m_rowMajorBuffer[MaxFramesInFlight]; - smart_refctd_ptr m_colMajorBuffer[MaxFramesInFlight]; - - // These are Buffer Device Addresses - uint64_t m_rowMajorBufferAddress[MaxFramesInFlight]; - uint64_t m_colMajorBufferAddress[MaxFramesInFlight]; - - bool m_useHalfFloats = false; - // Controls whether source image gets sampled with mirror padding or zero padding and along which axis - // First controls the first axis of the FFT, so sampling done in `image_fft_first_axis.hlsl` - // Second controls the second axis of the FFT, so sampling done in `fft_convolve_ifft.hlsl` - bool m_useMirrorPadding_first = false; - bool m_useMirrorPadding_second = false; - - // Other parameter-dependent variables - asset::VkExtent3D m_marginSrcDim; - uint16_t m_imageFirstAxisFFTWorkgroupSize; - uint32_t m_imageSecondAxisFFTNumWorkgroups; - - // Shader Cache - smart_refctd_ptr m_readCache; - smart_refctd_ptr m_writeCache; - - // Only use one queue - IQueue* m_queue; - - // -------------------------------- WINDOWED APP OVERRIDES --------------------------------------------------- - inline bool isComputeOnly() const override { return false; } - - virtual video::SPhysicalDeviceLimits getRequiredDeviceLimits() const override - { - auto retval = device_base_t::getRequiredDeviceLimits(); - retval.shaderFloat16 = m_useHalfFloats; - return retval; - } - - inline core::vector getSurfaces() const override - { - if (m_surface) - return { {m_surface->getSurface()/*,EQF_NONE*/} }; - - return {}; - } - - // -------------------------------- END WINDOWED APP OVERRIDES --------------------------------------------------- - - inline void updateDescriptorSet(smart_refctd_ptr imageDescriptor, smart_refctd_ptr storageImageDescriptor, smart_refctd_ptr textureArrayDescriptor = nullptr) - { - IGPUDescriptorSet::SDescriptorInfo infos[3] = {}; - IGPUDescriptorSet::SWriteDescriptorSet writes[3] = {}; - - for (auto i = 0u; i < 3; i++) { - writes[i].dstSet = m_descriptorSet.get(); - writes[i].arrayElement = 0u; - writes[i].count = 1u; - writes[i].info = &infos[i]; - } - - infos[0].desc = imageDescriptor; - infos[0].info.image.imageLayout = IImage::LAYOUT::READ_ONLY_OPTIMAL; - // Read image at binding 0 - writes[0].binding = 0u; - - // Binding 2 skipped since it's the sampler which we never need to change - - infos[1].desc = storageImageDescriptor; - infos[1].info.image.imageLayout = IImage::LAYOUT::GENERAL; - // Storage image at binding 1 - writes[1].binding = 2u; - - // If nullptr give it SOME value so validation layer doesn't complain even though we don't use it - infos[2].desc = textureArrayDescriptor ? textureArrayDescriptor : imageDescriptor; - infos[2].info.image.imageLayout = IImage::LAYOUT::READ_ONLY_OPTIMAL; - // Texture array for reading at binding 3 - writes[2].binding = 3u; - - m_device->updateDescriptorSets(writes, std::span()); - } - - - struct SShaderConstevalParameters - { - struct SShaderConstevalParametersCreateInfo - { - bool useHalfFloats = false; - uint16_t elementsPerInvocationLog2 = 0; - uint16_t workgroupSizeLog2 = 0; - uint16_t numWorkgroupsLog2 = 0; - uint16_t previousElementsPerInvocationLog2 = 0; - uint16_t previousWorkgroupSizeLog2 = 0; - uint16_t kernelSideLength = 0; - float32_t2 kernelHalfPixelSize = { 0.5f, 0.5f }; - }; - - SShaderConstevalParameters(SShaderConstevalParametersCreateInfo& info) : - scalar_t(info.useHalfFloats ? "float16_t" : "float32_t"), elementsPerInvocationLog2(info.elementsPerInvocationLog2), - workgroupSizeLog2(info.workgroupSizeLog2), numWorkgroupsLog2(info.numWorkgroupsLog2), numWorkgroups(uint32_t(1) << info.numWorkgroupsLog2), - previousElementsPerInvocationLog2(info.previousElementsPerInvocationLog2), previousWorkgroupSizeLog2(info.previousWorkgroupSizeLog2), - previousWorkgroupSize(uint16_t(1) << info.previousWorkgroupSizeLog2), kernelSideLength(info.kernelSideLength), kernelHalfPixelSize(info.kernelHalfPixelSize) - { - const uint32_t totalSize = uint32_t(1) << (elementsPerInvocationLog2 + workgroupSizeLog2); - totalSizeReciprocal = 1.f / float32_t(totalSize); - } - - std::string scalar_t; - uint16_t elementsPerInvocationLog2; - uint16_t workgroupSizeLog2; - uint16_t numWorkgroupsLog2; - uint32_t numWorkgroups; - uint16_t previousElementsPerInvocationLog2; - uint16_t previousWorkgroupSizeLog2; - uint16_t previousWorkgroupSize; - uint16_t kernelSideLength; - float32_t2 kernelHalfPixelSize; - float32_t totalSizeReciprocal; - }; - - inline core::smart_refctd_ptr createShader(const char* includeMainName, const SShaderConstevalParameters& shaderConstants) - { - // The annoying "const static member field must be initialized outside of struct" bug strikes again - std::ostringstream kernelHalfPixelSizeStream; - kernelHalfPixelSizeStream << "{" << shaderConstants.kernelHalfPixelSize.x << "," << shaderConstants.kernelHalfPixelSize.y << "}"; - std::string kernelHalfPixelSizeString = kernelHalfPixelSizeStream.str(); - - const auto prelude = [&]()->std::string - { - std::ostringstream tmp; - tmp << R"===( - #include "nbl/builtin/hlsl/workgroup/fft.hlsl" - )===" << (m_useMirrorPadding_second ? "#define MIRROR_PADDING\n" : "") << R"===( - struct ShaderConstevalParameters - { - using scalar_t = )===" << shaderConstants.scalar_t << R"===(; - - NBL_CONSTEXPR_STATIC_INLINE uint16_t ElementsPerInvocationLog2 = )===" << shaderConstants.elementsPerInvocationLog2 << R"===(; - NBL_CONSTEXPR_STATIC_INLINE uint16_t WorkgroupSizeLog2 = )===" << shaderConstants.workgroupSizeLog2 << R"===(; - NBL_CONSTEXPR_STATIC_INLINE uint16_t NumWorkgroupsLog2 = )===" << shaderConstants.numWorkgroupsLog2 << R"===(; - NBL_CONSTEXPR_STATIC_INLINE uint32_t NumWorkgroups = )===" << shaderConstants.numWorkgroups << R"===(; - NBL_CONSTEXPR_STATIC_INLINE uint16_t PreviousElementsPerInvocationLog2 = )===" << shaderConstants.previousElementsPerInvocationLog2 << R"===(; - NBL_CONSTEXPR_STATIC_INLINE uint16_t PreviousWorkgroupSizeLog2 = )===" << shaderConstants.previousWorkgroupSizeLog2 << R"===(; - NBL_CONSTEXPR_STATIC_INLINE uint16_t PreviousWorkgroupSize = )===" << shaderConstants.previousWorkgroupSize << R"===(; - NBL_CONSTEXPR_STATIC_INLINE uint16_t KernelSideLength = )===" << shaderConstants.kernelSideLength << R"===(; - NBL_CONSTEXPR_STATIC_INLINE float32_t2 KernelHalfPixelSize; - NBL_CONSTEXPR_STATIC_INLINE float32_t TotalSizeReciprocal = )===" << shaderConstants.totalSizeReciprocal << R"===(; - }; - NBL_CONSTEXPR_STATIC_INLINE float32_t2 ShaderConstevalParameters::KernelHalfPixelSize = )===" << kernelHalfPixelSizeString << R"===(; - )==="; - return tmp.str(); - }(); - - - - auto HLSLShader = core::make_smart_refctd_ptr((prelude+"\n#include \"" + includeMainName + "\"\n").c_str(), - IShader::E_CONTENT_TYPE::ECT_HLSL, - includeMainName); - assert(HLSLShader); - - #ifndef _NBL_DEBUG - ISPIRVOptimizer::E_OPTIMIZER_PASS optPasses = ISPIRVOptimizer::EOP_STRIP_DEBUG_INFO; - auto opt = make_smart_refctd_ptr(std::span(&optPasses, 1)); - return m_device->compileShader({ HLSLShader.get(), opt.get(), m_readCache.get(), m_writeCache.get()}); - #else - return m_device->compileShader({ HLSLShader.get(), nullptr, m_readCache.get(), m_writeCache.get() }); - #endif - } - -public: - // Yay thanks to multiple inheritance we cannot forward ctors anymore - FFTBloomApp(const path& _localInputCWD, const path& _localOutputCWD, const path& _sharedInputCWD, const path& _sharedOutputCWD) : - system::IApplicationFramework(_localInputCWD, _localOutputCWD, _sharedInputCWD, _sharedOutputCWD) {} - - virtual SPhysicalDeviceFeatures getPreferredDeviceFeatures() const override - { - auto retval = device_base_t::getPreferredDeviceFeatures(); - retval.pipelineExecutableInfo = true; - return retval; - } - - bool onAppInitialized(smart_refctd_ptr&& system) override - { - // Remember to call the base class initialization! - if (!device_base_t::onAppInitialized(smart_refctd_ptr(system))) - return false; - if (!asset_base_t::onAppInitialized(std::move(system))) - return false; - - - // Setup semaphores - m_timeline = m_device->createSemaphore(m_realFrameIx); - // We can't use the same sepahore for uploads so we signal a different semaphore - smart_refctd_ptr scratchSemaphore = m_device->createSemaphore(0); - - // Get graphics queue - these queues can do compute + blit - // In the real world you might do queue ownership transfers and have compute-dedicated queues - but here we KISS - m_queue = getGraphicsQueue(); - uint32_t queueFamilyIndex = m_queue->getFamilyIndex(); - - // Create command buffers for managing frames in flight + 1 extra - smart_refctd_ptr assConvCmdBuf; - { - std::array, MaxFramesInFlight + 1> commandBuffers; - - smart_refctd_ptr cmdpool = m_device->createCommandPool(queueFamilyIndex, IGPUCommandPool::CREATE_FLAGS::RESET_COMMAND_BUFFER_BIT); - if (!cmdpool->createCommandBuffers(IGPUCommandPool::BUFFER_LEVEL::PRIMARY, commandBuffers)) - return logFail("Failed to create Command Buffers!\n"); - - for (auto i = 0u; i < MaxFramesInFlight; i++) - m_cmdBufs[i] = std::move(commandBuffers[i]); - - assConvCmdBuf = std::move(commandBuffers[MaxFramesInFlight]); - } - - // Use asset converter to upload images to GPU, while at the same time creating our universal descriptor set and pipeline layout - smart_refctd_ptr pipelineLayout; - { - // Load source and kernel images - IAssetLoader::SAssetLoadParams lp = {}; - lp.logger = m_logger.get(); - lp.workingDirectory = ""; // virtual root - auto srcImageBundle = m_assetMgr->getAsset("../../media/colorexr.exr", lp); - auto kerImageBundle = m_assetMgr->getAsset("../../media/kernels/physical_flare_256.exr", lp); - const auto srcImages = srcImageBundle.getContents(); - const auto kerImages = kerImageBundle.getContents(); - if (srcImages.empty() or kerImages.empty()) - return logFail("Could not load image or kernel!"); - auto srcImageCPU = IAsset::castDown(srcImages[0]); - auto kerImageCPU = IAsset::castDown(kerImages[0]); - { - auto kerImageExtent = kerImageCPU->getCreationParameters().extent; - if (kerImageExtent.width != kerImageExtent.height || (kerImageExtent.width & (kerImageExtent.width - 1))) - return logFail("Kernel Image must be square, with side length a power of two!"); - } - - const auto srcImageFormat = srcImageCPU->getCreationParameters().format; - const auto kerImageFormat = kerImageCPU->getCreationParameters().format; - - // Create views for these images - ICPUImageView::SCreationParams viewParams[2] = - { - { - .flags = ICPUImageView::E_CREATE_FLAGS::ECF_NONE, - .image = std::move(srcImageCPU), - .viewType = IImageView::E_TYPE::ET_2D, - .format = srcImageFormat, - }, - { - .flags = ICPUImageView::E_CREATE_FLAGS::ECF_NONE, - .image = std::move(kerImageCPU), - .viewType = IImageView::E_TYPE::ET_2D, - .format = kerImageFormat, - } - }; - const auto srcImageViewCPU = ICPUImageView::create(std::move(viewParams[0])); - const auto kerImageViewCPU = ICPUImageView::create(std::move(viewParams[1])); - - // Create a CPU Descriptor Set - ICPUDescriptorSetLayout::SBinding bnd[5] = - { - // Kernel FFT and Image FFT read from a single Image - { - IDescriptorSetLayoutBase::SBindingBase(), - 0u, - IDescriptor::E_TYPE::ET_SAMPLED_IMAGE, - IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - IShader::E_SHADER_STAGE::ESS_COMPUTE, - 1u, - nullptr - }, - // Sampler: First Axis FFT (kernel) and convolution use a mirror-sampler - // Could be static for each pipeline but since it's shared it implies no descriptor changes between pipelines - { - IDescriptorSetLayoutBase::SBindingBase(), - 1u, - IDescriptor::E_TYPE::ET_SAMPLER, - IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - IShader::E_SHADER_STAGE::ESS_COMPUTE, - 1u, - nullptr - }, - // Storage Image: Normalization binds a texture array, First Axis IFFT binds a single image - { - IDescriptorSetLayoutBase::SBindingBase(), - 2u, - IDescriptor::E_TYPE::ET_STORAGE_IMAGE, - IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - IShader::E_SHADER_STAGE::ESS_COMPUTE, - 1, - nullptr - }, - // Convolution binds a texture array. Trying to have this in same binding slot as image would be cool but we lose the ability to write the descriptor set only once - { - IDescriptorSetLayoutBase::SBindingBase(), - 3u, - IDescriptor::E_TYPE::ET_SAMPLED_IMAGE, - IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - IShader::E_SHADER_STAGE::ESS_COMPUTE, - 1, - nullptr - }, - // Sampler: First Axis FFT (Image) uses a clamp to border sampler - // Could be static for each pipeline but since it's shared it implies no descriptor changes between pipelines - { - IDescriptorSetLayoutBase::SBindingBase(), - 4u, - IDescriptor::E_TYPE::ET_SAMPLER, - IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - IShader::E_SHADER_STAGE::ESS_COMPUTE, - 1u, - nullptr - } - }; - auto descriptorSetLayoutCPU = make_smart_refctd_ptr(bnd); - // Create a CPU pipeline layout so we also create that one here - const asset::SPushConstantRange pcRange[1] = { {IShader::E_SHADER_STAGE::ESS_COMPUTE, 0u, sizeof(PushConstantData)} }; - auto pipelineLayoutCPU = make_smart_refctd_ptr (pcRange, std::move(descriptorSetLayoutCPU), nullptr, nullptr, nullptr); - - // Create a Descriptor Set and fill it out - // Reassigning because it's been moved out of - descriptorSetLayoutCPU = smart_refctd_ptr(pipelineLayoutCPU->getDescriptorSetLayout(0)); - auto descriptorSetCPU = make_smart_refctd_ptr(std::move(descriptorSetLayoutCPU)); - - // Create samplers - ICPUSampler::SParams samplerCreationParams = - { - ISampler::E_TEXTURE_CLAMP::ETC_REPEAT, - ISampler::E_TEXTURE_CLAMP::ETC_REPEAT, - ISampler::E_TEXTURE_CLAMP::ETC_REPEAT, - ISampler::ETBC_FLOAT_OPAQUE_BLACK, - ISampler::ETF_LINEAR, - ISampler::ETF_LINEAR, - ISampler::ESMM_LINEAR, - 3u, - 0u, - ISampler::ECO_ALWAYS - }; - - auto repeatSamplerCPU = make_smart_refctd_ptr(samplerCreationParams); - - smart_refctd_ptr imageSamplerCPU; - if (!m_useMirrorPadding_first) - { - samplerCreationParams.TextureWrapU = ISampler::E_TEXTURE_CLAMP::ETC_CLAMP_TO_BORDER; - samplerCreationParams.TextureWrapV = ISampler::E_TEXTURE_CLAMP::ETC_CLAMP_TO_BORDER; - samplerCreationParams.TextureWrapW = ISampler::E_TEXTURE_CLAMP::ETC_CLAMP_TO_BORDER; - } - else - { - samplerCreationParams.TextureWrapU = ISampler::E_TEXTURE_CLAMP::ETC_MIRROR; - samplerCreationParams.TextureWrapV = ISampler::E_TEXTURE_CLAMP::ETC_MIRROR; - samplerCreationParams.TextureWrapW = ISampler::E_TEXTURE_CLAMP::ETC_MIRROR; - } - imageSamplerCPU = make_smart_refctd_ptr(samplerCreationParams); - - // Set descriptor set values for automatic upload - - auto& firstSampledImageDescriptorInfo = descriptorSetCPU->getDescriptorInfos(ICPUDescriptorSetLayout::CBindingRedirect::binding_number_t(0u), IDescriptor::E_TYPE::ET_SAMPLED_IMAGE).front(); - auto& secondSampledImageDescriptorInfo = descriptorSetCPU->getDescriptorInfos(ICPUDescriptorSetLayout::CBindingRedirect::binding_number_t(3u), IDescriptor::E_TYPE::ET_SAMPLED_IMAGE).front(); - - firstSampledImageDescriptorInfo.desc = kerImageViewCPU; - firstSampledImageDescriptorInfo.info.image.imageLayout = IImage::LAYOUT::READ_ONLY_OPTIMAL; - - secondSampledImageDescriptorInfo.desc = srcImageViewCPU; - secondSampledImageDescriptorInfo.info.image.imageLayout = IImage::LAYOUT::READ_ONLY_OPTIMAL; - - auto& mirrorSamplerDescriptorInfo = descriptorSetCPU->getDescriptorInfos(ICPUDescriptorSetLayout::CBindingRedirect::binding_number_t(1u), IDescriptor::E_TYPE::ET_SAMPLER).front(); - mirrorSamplerDescriptorInfo.desc = repeatSamplerCPU; - - auto& imageSamplerDescriptorInfo = descriptorSetCPU->getDescriptorInfos(ICPUDescriptorSetLayout::CBindingRedirect::binding_number_t(4u), IDescriptor::E_TYPE::ET_SAMPLER).front(); - imageSamplerDescriptorInfo.desc = imageSamplerCPU; - - // Using asset converter - smart_refctd_ptr converter = video::CAssetConverter::create({ .device = m_device.get(),.optimizer = {} }); - // We don't want to generate mip-maps for these images (YET), to ensure that we must override the default callbacks. - struct SInputs final : CAssetConverter::SInputs - { - inline uint8_t getMipLevelCount(const size_t groupCopyID, const ICPUImage* image, const CAssetConverter::patch_t& patch) const override - { - return image->getCreationParameters().mipLevels; - } - inline uint16_t needToRecomputeMips(const size_t groupCopyID, const ICPUImage* image, const CAssetConverter::patch_t& patch) const override - { - return 0b0u; - } - } inputs = {}; - inputs.logger = m_logger.get(); - asset::ICPUImageView* CPUImageViews[2] = { kerImageViewCPU.get(), srcImageViewCPU.get() }; - - std::get>(inputs.assets) = { &pipelineLayoutCPU.get(), 1}; - std::get>(inputs.assets) = { CPUImageViews, 2 }; - std::get>(inputs.assets) = { &descriptorSetCPU.get(), 1 }; - - auto reservation = converter->reserve(inputs); - - // Retrieve GPU uploads - const auto pipelineLayoutGPU = reservation.getGPUObjects(); - pipelineLayout = pipelineLayoutGPU.front().value; - - const auto imagesGPU = reservation.getGPUObjects(); - m_kerImageView = imagesGPU[0].value; - m_srcImageView = imagesGPU[1].value; - - const auto descriptorSetGPU = reservation.getGPUObjects(); - m_descriptorSet = descriptorSetGPU.front().value; - - // Give them debug names - m_srcImageView->setObjectDebugName("Source image view"); - m_srcImageView->getCreationParameters().image->setObjectDebugName("Source Image"); - m_kerImageView->setObjectDebugName("Bloom kernel image view"); - m_kerImageView->getCreationParameters().image->setObjectDebugName("Bloom kernel Image"); - - // The down-cast should not fail! - assert(m_srcImageView); - assert(m_kerImageView); - - // Going to need an IUtils to perform uploads/downloads - m_utils = IUtilities::create(smart_refctd_ptr(m_device), smart_refctd_ptr(m_logger)); - - // Now convert uploads - // Get graphics queue for image transfer - // For image uploads - SIntendedSubmitInfo intendedSubmit; - - intendedSubmit.queue = m_queue; - // Set up submit for image transfers - // wait for nothing before upload - intendedSubmit.waitSemaphores = {}; - intendedSubmit.prevCommandBuffers = {}; - // fill later - intendedSubmit.scratchCommandBuffers = {}; - intendedSubmit.scratchSemaphore = { - .semaphore = scratchSemaphore.get(), - .value = 0, - // because of layout transitions - .stageMask = PIPELINE_STAGE_FLAGS::ALL_COMMANDS_BITS - }; - - // Needs to be open for utilities - assConvCmdBuf->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT); - - IQueue::SSubmitInfo::SCommandBufferInfo assConvCmdBufInfo = { assConvCmdBuf.get() }; - intendedSubmit.scratchCommandBuffers = { &assConvCmdBufInfo,1 }; - - CAssetConverter::SConvertParams params = {}; - params.transfer = &intendedSubmit; - params.utilities = m_utils.get(); - auto result = reservation.convert(params); - // block immediately - if (result.copy() != IQueue::RESULT::SUCCESS) - return false; - } - - // Create and initialize surface and swapchain - { - // First create the surface, sized the same as the input image - auto srcImgExtent = m_srcImageView->getCreationParameters().image->getCreationParameters().extent; - { - IWindow::SCreationParams params = {}; - params.callback = core::make_smart_refctd_ptr(); - params.width = srcImgExtent.width; - params.height = srcImgExtent.height; - params.x = 32; - params.y = 32; - params.flags = IWindow::ECF_BORDERLESS | IWindow::ECF_HIDDEN; - params.windowCaption = "FFT Bloom Demo"; - 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) = CSimpleResizeSurface::create(std::move(surface)); - - // Set up swapchain creation parameters - ISwapchain::SCreationParams swapchainParams = { .surface = m_surface->getSurface(),.sharedParams = {.presentMode = ISurface::EPM_IMMEDIATE} }; - if (!swapchainParams.deduceFormat(m_physicalDevice)) - return logFail("Could not choose a Surface Format for the Swapchain!"); - - // Initialize the surface - auto graphicsQueue = getGraphicsQueue(); - if (!m_surface || !m_surface->init(graphicsQueue, std::make_unique(), swapchainParams.sharedParams)) - return logFail("Could not create Window & Surface or initialize the Surface!"); - - // Set window size to match input image - m_winMgr->setWindowSize(m_window.get(), srcImgExtent.width, srcImgExtent.height); - - // Create the swapchain - m_surface->recreateSwapchain(); - - m_winMgr->show(m_window.get()); - } - - // Create Out Image - { - auto dstImgViewInfo = m_srcImageView->getCreationParameters(); - - IGPUImage::SCreationParams dstImgInfo(dstImgViewInfo.image->getCreationParameters()); - // Specify we want this to be a storage image, + transfer for readback (blit when we have swapchain up) - dstImgInfo.usage = IImage::EUF_STORAGE_BIT | IImage::EUF_TRANSFER_SRC_BIT; - dstImgInfo.format = m_useHalfFloats ? EF_R16G16B16A16_SFLOAT : EF_R32G32B32A32_SFLOAT; - auto outImg = m_device->createImage(std::move(dstImgInfo)); - - outImg->setObjectDebugName("Convolved Image"); - - auto memReqs = outImg->getMemoryReqs(); - memReqs.memoryTypeBits &= m_device->getPhysicalDevice()->getDeviceLocalMemoryTypeBits(); - auto gpuMem = m_device->allocate(memReqs, outImg.get()); - - dstImgViewInfo.image = outImg; - dstImgViewInfo.subUsages = IImage::EUF_STORAGE_BIT | IImage::EUF_TRANSFER_SRC_BIT; - dstImgViewInfo.format = m_useHalfFloats ? EF_R16G16B16A16_SFLOAT : EF_R32G32B32A32_SFLOAT; - m_outImgView = m_device->createImageView(IGPUImageView::SCreationParams(dstImgViewInfo)); - - m_outImgView->setObjectDebugName("Convolved Image View"); - } - - // agree on formats - const E_FORMAT srcFormat = m_srcImageView->getCreationParameters().format; - - //! OVERRIDE (we dont need alpha) - uint32_t srcNumChannels = Channels; - uint32_t kerNumChannels = Channels; - - // Kernel pixel to image pixel conversion ratio - const float bloomRelativeScale = 0.25f; - const auto kerDim = m_kerImageView->getCreationParameters().image->getCreationParameters().extent; - const auto srcDim = m_srcImageView->getCreationParameters().image->getCreationParameters().extent; - auto bloomScale = core::min(float(srcDim.width) / float(kerDim.width), float(srcDim.height) / float(kerDim.height)) * bloomRelativeScale; - assert(bloomScale <= 1.f); - - m_marginSrcDim = srcDim; - - // Add padding to m_marginSrcDim - for (auto i = 0u; i < 3u; i++) - { - const auto coord = (&kerDim.width)[i]; - (&m_marginSrcDim.width)[i] += core::max(coord, 1u) - 1u; - } - - - // Create intermediate buffers - { - IGPUBuffer::SCreationParams deviceLocalBufferParams = {}; - - deviceLocalBufferParams.queueFamilyIndexCount = 1; - deviceLocalBufferParams.queueFamilyIndices = &queueFamilyIndex; - uint32_t2 imageDimensions = { srcDim.width, srcDim.height }; - uint32_t2 kernelDimensions = { kerDim.width, kerDim.height }; - // Y-axis goes first in the FFT - hlsl::vector axisPassOrder = { 1, 0 }; - deviceLocalBufferParams.size = fft::getOutputBufferSizeConvolution<2>(Channels, imageDimensions, kernelDimensions, 0, axisPassOrder, true, m_useHalfFloats); - deviceLocalBufferParams.usage = asset::IBuffer::E_USAGE_FLAGS::EUF_STORAGE_BUFFER_BIT | asset::IBuffer::E_USAGE_FLAGS::EUF_SHADER_DEVICE_ADDRESS_BIT; - - for (auto i = 0u; i < MaxFramesInFlight; i++) - { - m_rowMajorBuffer[i] = m_device->createBuffer(std::move(deviceLocalBufferParams)); - deviceLocalBufferParams = m_rowMajorBuffer[i]->getCreationParams(); - m_colMajorBuffer[i] = m_device->createBuffer(std::move(deviceLocalBufferParams)); - deviceLocalBufferParams = m_rowMajorBuffer[i]->getCreationParams(); - - auto rowMemReqs = m_rowMajorBuffer[i]->getMemoryReqs(); - auto colMemReqs = m_colMajorBuffer[i]->getMemoryReqs(); - rowMemReqs.memoryTypeBits &= m_device->getPhysicalDevice()->getDeviceLocalMemoryTypeBits(); - colMemReqs.memoryTypeBits &= m_device->getPhysicalDevice()->getDeviceLocalMemoryTypeBits(); - auto gpuRowMem = m_device->allocate(rowMemReqs, m_rowMajorBuffer[i].get(), IDeviceMemoryAllocation::E_MEMORY_ALLOCATE_FLAGS::EMAF_DEVICE_ADDRESS_BIT); - auto gpuColMem = m_device->allocate(colMemReqs, m_colMajorBuffer[i].get(), IDeviceMemoryAllocation::E_MEMORY_ALLOCATE_FLAGS::EMAF_DEVICE_ADDRESS_BIT); - - m_rowMajorBufferAddress[i] = m_rowMajorBuffer[i].get()->getDeviceAddress(); - m_colMajorBufferAddress[i] = m_colMajorBuffer[i].get()->getDeviceAddress(); - } - } - // Create cache for shader compilation - - m_readCache = nullptr; - m_writeCache = core::make_smart_refctd_ptr(); - // Keep caches separate for debug runs - #ifndef _NBL_DEBUG - auto shaderCachePath = localOutputCWD / "cache.bin"; - #else - auto shaderCachePath = localOutputCWD / "cache_d.bin"; - #endif - - { - core::smart_refctd_ptr shaderReadCacheFile; - { - system::ISystem::future_t> future; - m_system->createFile(future, shaderCachePath.c_str(), system::IFile::ECF_READ); - if (future.wait()) - { - future.acquire().move_into(shaderReadCacheFile); - if (shaderReadCacheFile) - { - const size_t size = shaderReadCacheFile->getSize(); - if (size > 0ull) - { - std::vector contents(size); - system::IFile::success_t succ; - shaderReadCacheFile->read(succ, contents.data(), 0, size); - if (succ) - m_readCache = IShaderCompiler::CCache::deserialize(contents); - } - } - } - else - m_logger->log("Failed Opening Shader Cache File.", ILogger::ELL_ERROR); - } - - } - - // Kernel second axis FFT has no descriptor sets so we just create another pipeline with the same layout - // TODO: To avoid duplicated layouts we could make samplers dynamic in the first axis FFT. Also if we don't hardcode (by #defining) some stuff in the first axis FFT - // (once FFT ext is back) we can also avoid having duplicated pipelines (like the old Bloom example, which had a single pipeline for forward FFT along an axis) - // and setting stuff via shader push constants (such as which axis to perform FFT on and the size of output image). - - // -------------------------------------- KERNEL FFT PRECOMP ---------------------------------------------------------------- - const auto& deviceLimits = m_device->getPhysicalDevice()->getLimits(); - { - // create kernel spectrums - auto createKernelSpectrum = [&]() -> auto - { - video::IGPUImage::SCreationParams imageParams; - imageParams.flags = static_cast(0u); - imageParams.type = asset::IImage::ET_2D; - imageParams.format = m_useHalfFloats ? EF_R16G16_SFLOAT : EF_R32G32_SFLOAT; - imageParams.extent = { kerDim.width,kerDim.height / 2 + 1, 1u }; - imageParams.mipLevels = 1u; - imageParams.arrayLayers = Channels; - imageParams.samples = asset::IImage::ESCF_1_BIT; - imageParams.usage = IImage::EUF_STORAGE_BIT | IImage::EUF_SAMPLED_BIT; - - auto kernelImg = m_device->createImage(std::move(imageParams)); - - auto memReqs = kernelImg->getMemoryReqs(); - memReqs.memoryTypeBits &= m_device->getPhysicalDevice()->getDeviceLocalMemoryTypeBits(); - auto gpuMem = m_device->allocate(memReqs, kernelImg.get()); - - video::IGPUImageView::SCreationParams viewParams; - viewParams.flags = static_cast(0u); - viewParams.image = kernelImg; - viewParams.viewType = video::IGPUImageView::ET_2D_ARRAY; - viewParams.format = m_useHalfFloats ? EF_R16G16_SFLOAT : EF_R32G32_SFLOAT; - viewParams.subresourceRange.layerCount = Channels; - return m_device->createImageView(std::move(viewParams)); - }; - - m_kernelNormalizedSpectrums = createKernelSpectrum(); - - // Give them names - m_kernelNormalizedSpectrums->setObjectDebugName("Kernel spectrum array view"); - m_kernelNormalizedSpectrums->getCreationParameters().image->setObjectDebugName("Kernel spectrum array"); - - // Provide sampler so it's bound already - updateDescriptorSet(m_kerImageView, m_kernelNormalizedSpectrums); - - // Invoke a workgroup per two vertical scanlines. Kernel is square and runs first in the y-direction. - // That means we have to create a shader that does an FFT of size `kerDim.height = kerDim.width` (length of each column, already padded to PoT), - // and call `kerDim.width / 2` workgroups to run it. We also have to keep in mind `kerDim.y = WorkgroupSize * ElementsPerInvocation`. - // We prefer to go with 2 elements per invocation and max out WorkgroupSize when possible. - // This is because we use PreloadedAccessors which reduce global memory accesses at the cost of decreasing occupancy with increasing ElementsPerInvocation - - // Compute required WorkgroupSize and ElementsPerThread for FFT - // Remember we assume kernel is square! - - auto [elementsPerInvocationLog2, workgroupSizeLog2] = workgroup::fft::optimalFFTParameters(deviceLimits.maxOptimallyResidentWorkgroupInvocations, kerDim.width, deviceLimits.maxSubgroupSize); - // Normalization shader needs this info - uint16_t secondAxisFFTHalfLengthLog2 = elementsPerInvocationLog2 + workgroupSizeLog2 - 1; - // Create shaders - smart_refctd_ptr shaders[3]; - uint16_t2 kernelDimensions = { kerDim.width, kerDim.height }; - SShaderConstevalParameters::SShaderConstevalParametersCreateInfo shaderConstevalInfo = { .useHalfFloats = m_useHalfFloats, .elementsPerInvocationLog2 = elementsPerInvocationLog2, .workgroupSizeLog2 = workgroupSizeLog2, .numWorkgroupsLog2 = secondAxisFFTHalfLengthLog2, .previousWorkgroupSizeLog2 = workgroupSizeLog2 }; - SShaderConstevalParameters shaderConstevalParameters(shaderConstevalInfo); - shaders[0] = createShader("app_resources/kernel_fft_first_axis.hlsl", shaderConstevalParameters); - shaders[1] = createShader("app_resources/kernel_fft_second_axis.hlsl", shaderConstevalParameters); - shaders[2] = createShader("app_resources/kernel_spectrum_normalize.hlsl", shaderConstevalParameters); - - // Create compute pipelines - First axis FFT -> Second axis FFT -> Normalization - IGPUComputePipeline::SCreationParams params[3] = {}; - for (auto i = 0u; i < 3; i++) - { - params[i].layout = pipelineLayout.get(); - params[i].shader.shader = shaders[i].get(); - params[i].shader.entryPoint = "main"; - // Normalization doesn't require full subgroups - params[i].cached.requireFullSubgroups = bool(2-i); - params[i].shader.requiredSubgroupSize = static_cast(hlsl::findMSB(deviceLimits.maxSubgroupSize)); - if (m_device->getEnabledFeatures().pipelineExecutableInfo) - { - params[i].flags |= IGPUComputePipeline::SCreationParams::FLAGS::CAPTURE_STATISTICS; - params[i].flags |= IGPUComputePipeline::SCreationParams::FLAGS::CAPTURE_INTERNAL_REPRESENTATIONS; - } - } - - smart_refctd_ptr pipelines[3]; - if(!m_device->createComputePipelines(nullptr, { params, 3 }, pipelines)) - return logFail("Failed to create Compute Pipelines!\n"); - - if (m_device->getEnabledFeatures().pipelineExecutableInfo) - { - const char* kernelNames[] = {"Kernel First Axis FFT", "Kernel Second Axis FFT", "Kernel Spectrum Normalize"}; - for (auto i = 0u; i < 3; i++) - { - auto report = system::to_string(pipelines[i]->getExecutableInfo()); - m_logger->log("%s Pipeline Executable Report:\n%s", ILogger::ELL_PERFORMANCE, kernelNames[i], report.c_str()); - } - } - - // Push Constants - only need to specify BDAs here - PushConstantData pushConstants; - pushConstants.colMajorBufferAddress = m_colMajorBufferAddress[0]; - pushConstants.rowMajorBufferAddress = m_rowMajorBufferAddress[0]; - - // Create a command buffer for this submit only - smart_refctd_ptr kernelPrecompCmdBuf; - { - smart_refctd_ptr cmdpool = m_device->createCommandPool(queueFamilyIndex, IGPUCommandPool::CREATE_FLAGS::TRANSIENT_BIT); - if (!cmdpool->createCommandBuffers(IGPUCommandPool::BUFFER_LEVEL::PRIMARY, {&kernelPrecompCmdBuf, 1u})) - return logFail("Failed to create Command Buffers!\n"); - } - - kernelPrecompCmdBuf->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT); - - // First Axis FFT - kernelPrecompCmdBuf->bindComputePipeline(pipelines[0].get()); - kernelPrecompCmdBuf->bindDescriptorSets(asset::EPBP_COMPUTE, pipelines[0]->getLayout(), 0, 1, &m_descriptorSet.get()); - kernelPrecompCmdBuf->pushConstants(pipelines[0]->getLayout(), IShader::E_SHADER_STAGE::ESS_COMPUTE, 0u, sizeof(pushConstants), &pushConstants); - // One workgroup per 2 columns - kernelPrecompCmdBuf->dispatch(kerDim.width / 2, 1, 1); - - // Pipeline barrier: wait for first axis FFT before second axis can begin - IGPUCommandBuffer::SPipelineBarrierDependencyInfo pipelineBarrierInfo = {}; - decltype(pipelineBarrierInfo)::buffer_barrier_t bufBarrier = {}; - pipelineBarrierInfo.bufBarriers = { &bufBarrier, 1u }; - - // First axis FFT writes to colMajorBuffer - bufBarrier.range.buffer = m_colMajorBuffer[0]; - - // Wait for first compute write (first axis FFT) before next compute read (second axis FFT) - bufBarrier.barrier.dep.srcStageMask = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT; - bufBarrier.barrier.dep.srcAccessMask = ACCESS_FLAGS::SHADER_WRITE_BITS; - bufBarrier.barrier.dep.dstStageMask = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT; - bufBarrier.barrier.dep.dstAccessMask = ACCESS_FLAGS::SHADER_READ_BITS; - - // Also set kernel channel image array to GENERAL for writing - decltype(pipelineBarrierInfo)::image_barrier_t imgBarrier = {}; - pipelineBarrierInfo.imgBarriers = { &imgBarrier, 1 }; - - imgBarrier.image = m_kernelNormalizedSpectrums->getCreationParameters().image.get(); - imgBarrier.subresourceRange.aspectMask = IImage::EAF_COLOR_BIT; - imgBarrier.subresourceRange.levelCount = 1u; - imgBarrier.subresourceRange.layerCount = Channels; - imgBarrier.barrier.dep.srcStageMask = PIPELINE_STAGE_FLAGS::NONE; - imgBarrier.barrier.dep.srcAccessMask = ACCESS_FLAGS::NONE; - imgBarrier.barrier.dep.dstStageMask = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT; - imgBarrier.barrier.dep.dstAccessMask = ACCESS_FLAGS::SHADER_WRITE_BITS; - imgBarrier.oldLayout = IImage::LAYOUT::UNDEFINED; - imgBarrier.newLayout = IImage::LAYOUT::GENERAL; - - kernelPrecompCmdBuf->pipelineBarrier(asset::E_DEPENDENCY_FLAGS(0), pipelineBarrierInfo); - - // Now do second axis FFT - kernelPrecompCmdBuf->bindComputePipeline(pipelines[1].get()); - // Same number of workgroups - this time because we only saved half the rows - kernelPrecompCmdBuf->dispatch(kerDim.width / 2, 1, 1); - - // Wait on second axis FFT to write the kernel image before running normalization step - // Normalization needs to access the power value stored in the rowmajorbuffer - bufBarrier.range.buffer = m_rowMajorBuffer[0]; - - // No layout transition now - imgBarrier.oldLayout = IImage::LAYOUT::UNDEFINED; - imgBarrier.newLayout = IImage::LAYOUT::UNDEFINED; - - // Wait on second axis FFT write ... - imgBarrier.barrier.dep.srcStageMask = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT; - imgBarrier.barrier.dep.srcAccessMask = ACCESS_FLAGS::SHADER_WRITE_BITS; - // ... before normalization read - imgBarrier.barrier.dep.dstStageMask = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT; - imgBarrier.barrier.dep.dstAccessMask = ACCESS_FLAGS::SHADER_READ_BITS; - - kernelPrecompCmdBuf->pipelineBarrier(asset::E_DEPENDENCY_FLAGS(0), pipelineBarrierInfo); - - //Finally, normalize kernel Image - same number of workgroups - kernelPrecompCmdBuf->bindComputePipeline(pipelines[2].get()); - // Hardcoded 8x8 workgroup seems to be optimal for tex access - const auto& kernelSpectraExtent = m_kernelNormalizedSpectrums->getCreationParameters().image->getCreationParameters().extent; - // Assumed PoT. +1 in Y dispatch to account for Nyquist row - kernelPrecompCmdBuf->dispatch(kernelSpectraExtent.width / 8, kernelSpectraExtent.height / 8 + 1, 1); - - // Pipeline barrier: transition kernel spectrum images into read only, and outImage into general - IGPUCommandBuffer::SPipelineBarrierDependencyInfo imagePipelineBarrierInfo = {}; - decltype(imagePipelineBarrierInfo)::image_barrier_t imgBarriers[2] = {}; - imagePipelineBarrierInfo.imgBarriers = { imgBarriers, 2 }; - - // outImage just needs a layout transition before it can be written to - // Masks left empty because we will wait on device idle at the end of app initialization anyway - imgBarriers[0].image = m_outImgView->getCreationParameters().image.get(); - imgBarriers[0].barrier.dep.srcStageMask = PIPELINE_STAGE_FLAGS::NONE; - imgBarriers[0].barrier.dep.srcAccessMask = ACCESS_FLAGS::NONE; - imgBarriers[0].barrier.dep.dstStageMask = PIPELINE_STAGE_FLAGS::NONE; - imgBarriers[0].barrier.dep.dstAccessMask = ACCESS_FLAGS::NONE; - imgBarriers[0].oldLayout = IImage::LAYOUT::UNDEFINED; - imgBarriers[0].newLayout = IImage::LAYOUT::GENERAL; - imgBarriers[0].subresourceRange = { IGPUImage::EAF_COLOR_BIT, 0u, 1u, 0u, 1 }; - - // Transition kernel spectrums so that convolution shader can access them later - imgBarriers[1].image = m_kernelNormalizedSpectrums->getCreationParameters().image.get(); - imgBarriers[1].barrier.dep.srcStageMask = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT; - imgBarriers[1].barrier.dep.srcAccessMask = ACCESS_FLAGS::SHADER_WRITE_BITS; - imgBarriers[1].barrier.dep.dstStageMask = PIPELINE_STAGE_FLAGS::NONE; - imgBarriers[1].barrier.dep.dstAccessMask = ACCESS_FLAGS::NONE; - imgBarriers[1].oldLayout = IImage::LAYOUT::GENERAL; - imgBarriers[1].newLayout = IImage::LAYOUT::READ_ONLY_OPTIMAL; - imgBarriers[1].subresourceRange = { IGPUImage::EAF_COLOR_BIT, 0u, 1u, 0u, Channels }; - - kernelPrecompCmdBuf->pipelineBarrier(asset::E_DEPENDENCY_FLAGS(0), imagePipelineBarrierInfo); - - kernelPrecompCmdBuf->end(); - - // Submit to queue and add sync point - { - const IQueue::SSubmitInfo::SCommandBufferInfo cmdbufInfo = - { - .cmdbuf = kernelPrecompCmdBuf.get() - }; - const IQueue::SSubmitInfo::SSemaphoreInfo signalInfo = - { - .semaphore = m_timeline.get(), - .value = 1, - .stageMask = asset::PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT - }; - - // Could check whether queue used for upload is different than the compute one, but oh well - IQueue::SSubmitInfo::SSemaphoreInfo transferSemaphore = { - .semaphore = scratchSemaphore.get(), - .value = 1, - // because of layout transitions - . stageMask = PIPELINE_STAGE_FLAGS::ALL_COMMANDS_BITS - }; - const IQueue::SSubmitInfo submitInfo = { - .waitSemaphores = {&transferSemaphore, 1}, - .commandBuffers = {&cmdbufInfo,1}, - .signalSemaphores = {&signalInfo,1} - }; - - m_api->startCapture(); - m_queue->submit({ &submitInfo,1 }); - m_api->endCapture(); - } - } - // ----------------------------------------- KERNEL PRECOMP END ------------------------------------------------- - - // Now create the pipelines for the image FFT - - // Second axis FFT launches an amount of workgroups equal to half of the length of the first axis FFT. Second pass FFT needs the log2 of this number baked in as a constant. - uint16_t firstAxisFFTHalfLengthLog2; - uint16_t firstAxisFFTElementsPerInvocationLog2; - uint16_t firstAxisFFTWorkgroupSizeLog2; - smart_refctd_ptr shaders[3]; - { - auto [elementsPerInvocationLog2, workgroupSizeLog2] = workgroup::fft::optimalFFTParameters(deviceLimits.maxOptimallyResidentWorkgroupInvocations, m_marginSrcDim.height, deviceLimits.maxSubgroupSize); - SShaderConstevalParameters::SShaderConstevalParametersCreateInfo shaderConstevalInfo = { .useHalfFloats = m_useHalfFloats, .elementsPerInvocationLog2 = elementsPerInvocationLog2, .workgroupSizeLog2 = workgroupSizeLog2 }; - SShaderConstevalParameters shaderConstevalParameters(shaderConstevalInfo); - shaders[0] = createShader("app_resources/image_fft_first_axis.hlsl", shaderConstevalParameters); - // IFFT along first axis has same dimensions as FFT - shaders[2] = createShader("app_resources/image_ifft_first_axis.hlsl", shaderConstevalParameters); - firstAxisFFTHalfLengthLog2 = elementsPerInvocationLog2 + workgroupSizeLog2 - 1; - firstAxisFFTElementsPerInvocationLog2 = elementsPerInvocationLog2; - firstAxisFFTWorkgroupSizeLog2 = workgroupSizeLog2; - m_imageFirstAxisFFTWorkgroupSize = uint16_t(1) << workgroupSizeLog2; - m_imageSecondAxisFFTNumWorkgroups = uint32_t(1) << firstAxisFFTHalfLengthLog2; - } - - // Second axis FFT might have different dimensions - { - auto [elementsPerInvocationLog2, workgroupSizeLog2] = workgroup::fft::optimalFFTParameters(deviceLimits.maxOptimallyResidentWorkgroupInvocations, m_marginSrcDim.width, deviceLimits.maxSubgroupSize); - // Compute kernel half pixel size - const auto& kernelSpectraExtent = m_kernelNormalizedSpectrums->getCreationParameters().image->getCreationParameters().extent; - float32_t2 kernelHalfPixelSize{ 0.5f,0.5f }; - kernelHalfPixelSize.x /= kernelSpectraExtent.width; - kernelHalfPixelSize.y /= kernelSpectraExtent.height; - SShaderConstevalParameters::SShaderConstevalParametersCreateInfo shaderConstevalInfo = - { - .useHalfFloats = m_useHalfFloats, - .elementsPerInvocationLog2 = elementsPerInvocationLog2, - .workgroupSizeLog2 = workgroupSizeLog2, - .numWorkgroupsLog2 = firstAxisFFTHalfLengthLog2, - .previousElementsPerInvocationLog2 = firstAxisFFTElementsPerInvocationLog2, - .previousWorkgroupSizeLog2 = firstAxisFFTWorkgroupSizeLog2, - .kernelSideLength = uint16_t(kerDim.height), - .kernelHalfPixelSize = kernelHalfPixelSize - }; - SShaderConstevalParameters shaderConstevalParameters(shaderConstevalInfo); - shaders[1] = createShader("app_resources/fft_convolve_ifft.hlsl", shaderConstevalParameters); - } - - // Create compute pipelines - First axis FFT -> Second axis FFT -> Normalization - IGPUComputePipeline::SCreationParams params[3] = {}; - for (auto i = 0u; i < 3; i++) { - params[i].layout = pipelineLayout.get(); - params[i].shader.shader = shaders[i].get(); - params[i].shader.entryPoint = "main"; - params[i].shader.requiredSubgroupSize = static_cast(hlsl::findMSB(deviceLimits.maxSubgroupSize)); - params[i].cached.requireFullSubgroups = true; - if (m_device->getEnabledFeatures().pipelineExecutableInfo) - { - params[i].flags |= IGPUComputePipeline::SCreationParams::FLAGS::CAPTURE_STATISTICS; - params[i].flags |= IGPUComputePipeline::SCreationParams::FLAGS::CAPTURE_INTERNAL_REPRESENTATIONS; - } - } - - smart_refctd_ptr pipelines[3]; - if (!m_device->createComputePipelines(nullptr, { params, 3 }, pipelines)) - return logFail("Failed to create Compute Pipelines!\n"); - - if (m_device->getEnabledFeatures().pipelineExecutableInfo) - { - const char* imageNames[] = {"Image First Axis FFT", "FFT Convolve IFFT", "Image First Axis IFFT"}; - for (auto i = 0u; i < 3; i++) - { - auto report = system::to_string(pipelines[i]->getExecutableInfo()); - m_logger->log("%s Pipeline Executable Report:\n%s", ILogger::ELL_PERFORMANCE, imageNames[i], report.c_str()); - } - } - - m_firstAxisFFTPipeline = pipelines[0]; - m_lastAxisFFT_convolution_lastAxisIFFTPipeline = pipelines[1]; - m_firstAxisIFFTPipeline = pipelines[2]; - - // Dump cache to disk since we won't be doing any more compilations - for now - { - core::smart_refctd_ptr shaderWriteCacheFile; - { - system::ISystem::future_t> future; - m_system->deleteFile(shaderCachePath); // temp solution instead of trimming, to make sure we won't have corrupted json - m_system->createFile(future, shaderCachePath.c_str(), system::IFile::ECF_WRITE); - if (future.wait()) - { - future.acquire().move_into(shaderWriteCacheFile); - if (shaderWriteCacheFile) - { - auto serializedCache = m_writeCache->serialize(); - if (shaderWriteCacheFile) - { - system::IFile::success_t succ; - shaderWriteCacheFile->write(succ, serializedCache->getPointer(), 0, serializedCache->getSize()); - if (!succ) - m_logger->log("Failed Writing To Shader Cache File.", ILogger::ELL_ERROR); - } - } - else - m_logger->log("Failed Creating Shader Cache File.", ILogger::ELL_ERROR); - } - else - m_logger->log("Failed Creating Shader Cache File.", ILogger::ELL_ERROR); - } - - } - // Block and wait until kernel FFT is done before we drop the pipelines. - // One could instead opt to create a latch that does nothing but capture the pipelines and gets called when semaphore is signalled. - // IMPORTANT: This wait offsets our frames in flight math by 1, so it's important to remember it - const ISemaphore::SWaitInfo waitInfo = { m_timeline.get(), 1 }; - - m_device->blockForSemaphores({ &waitInfo, 1 }); - - // Before leaving, update descriptor set with values needed by image transform - // Write descriptor set for kernel FFT computation - updateDescriptorSet(m_srcImageView, m_outImgView, m_kernelNormalizedSpectrums); - - return true; - } - - bool keepRunning() override - { - if (m_surface->irrecoverable()) - return false; - - return true; - } - - // Right now it's one shot app, but I put this code here for easy refactoring when it refactors into it being a live app - void workLoopBody() override - { - // framesInFlight: ensuring safe execution of command buffers and acquires, `framesInFlight` only affect semaphore waits, don't use this to index your resources because it can change with swapchain recreation. - const uint32_t framesInFlight = core::min(MaxFramesInFlight, m_surface->getMaxAcquiresInFlight()); - // We block for semaphores for 2 reasons here: - // A) Resource: Can't use resource like a command buffer BEFORE previous use is finished! [MaxFramesInFlight] - // B) Acquire: Can't have more acquires in flight than a certain threshold returned by swapchain or your surface helper class. [MaxAcquiresInFlight] - if (m_realFrameIx >= framesInFlight) - { - const ISemaphore::SWaitInfo cbDonePending[] = - { - { - .semaphore = m_timeline.get(), - .value = m_realFrameIx + 2 - framesInFlight // There is a +2 here instead of +1 to account for the kernel precomp value increase - } - }; - if (m_device->blockForSemaphores(cbDonePending) != ISemaphore::WAIT_RESULT::SUCCESS) - return; - } - - const auto resourceIx = m_realFrameIx % MaxFramesInFlight; - - m_currentImageAcquire = m_surface->acquireNextImage(); - if (!m_currentImageAcquire) - return; - - // Acquire and reset command buffer - auto* const cmdBuf = m_cmdBufs[resourceIx].get(); - cmdBuf->reset(IGPUCommandBuffer::RESET_FLAGS::RELEASE_RESOURCES_BIT); - cmdBuf->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT); - - // Compute to blit barrier: Ensure compute pass is done before blitting from image - const IGPUImage::SSubresourceRange whole2DColorImage = - { - .aspectMask = IGPUImage::E_ASPECT_FLAGS::EAF_COLOR_BIT, - .baseMipLevel = 0, - .levelCount = 1, - .baseArrayLayer = 0, - .layerCount = 1 - }; - - // -------------------------------------- DRAW BEGIN ------------------------------------------- - - // Prepare for first axis FFT - // Push Constants - only need to specify BDAs here - const auto& imageExtent = m_srcImageView->getCreationParameters().image->getCreationParameters().extent; - const int32_t paddingAlongColumns = int32_t(core::roundUpToPoT(m_marginSrcDim.height) - imageExtent.height) / 2; - const int32_t paddingAlongRows = int32_t(core::roundUpToPoT(m_marginSrcDim.width) - imageExtent.width) / 2; - const int32_t halfPaddingAlongRows = paddingAlongRows / 2; - - PushConstantData pushConstants; - pushConstants.colMajorBufferAddress = m_colMajorBufferAddress[resourceIx]; - pushConstants.rowMajorBufferAddress = m_rowMajorBufferAddress[resourceIx]; - pushConstants.imageRowLength = int32_t(imageExtent.width); - pushConstants.imageHalfRowLength = int32_t(imageExtent.width) / 2; - pushConstants.imageColumnLength = int32_t(imageExtent.height); - pushConstants.padding = paddingAlongColumns; - pushConstants.halfPadding = halfPaddingAlongRows; - - float32_t2 imageHalfPixelSize = { 0.5f, 0.5f }; - imageHalfPixelSize.x /= imageExtent.width; - imageHalfPixelSize.y /= imageExtent.height; - pushConstants.imageHalfPixelSize = imageHalfPixelSize; - pushConstants.imagePixelSize = 2.f * imageHalfPixelSize; - pushConstants.imageTwoPixelSize_x = 4.f * imageHalfPixelSize.x; - pushConstants.imageWorkgroupSizePixelSize_y = m_imageFirstAxisFFTWorkgroupSize * pushConstants.imagePixelSize.y; - - // Interpolate between dirac delta and kernel based on current time - auto epochNanoseconds = clock_t::now().time_since_epoch().count(); - pushConstants.interpolatingFactor = cos(epochNanoseconds / 1000000000.f) * cos(epochNanoseconds / 1000000000.f); - - // Get size required to store FFT of a single channel - uint64_t channelStrideBytes = m_imageSecondAxisFFTNumWorkgroups * imageExtent.width * (m_useHalfFloats ? sizeof(complex_t) : sizeof(complex_t)); - pushConstants.channelStrideBytes = channelStrideBytes; - - cmdBuf->bindComputePipeline(m_firstAxisFFTPipeline.get()); - cmdBuf->bindDescriptorSets(asset::EPBP_COMPUTE, m_firstAxisFFTPipeline->getLayout(), 0, 1, &m_descriptorSet.get()); - cmdBuf->pushConstants(m_firstAxisFFTPipeline->getLayout(), IShader::E_SHADER_STAGE::ESS_COMPUTE, 0u, sizeof(pushConstants), &pushConstants); - // One workgroup per 2 columns - auto srcDim = m_srcImageView->getCreationParameters().image->getCreationParameters().extent; - cmdBuf->dispatch(srcDim.width / 2, 1, 1); - - // Pipeline Barrier: Wait for colMajorBuffer to be written to before reading it from next shader - IGPUCommandBuffer::SPipelineBarrierDependencyInfo bufferPipelineBarrierInfo = {}; - decltype(bufferPipelineBarrierInfo)::buffer_barrier_t bufBarrier = {}; - bufferPipelineBarrierInfo.bufBarriers = { &bufBarrier, 1u }; - - // First axis FFT writes to colMajorBuffer - bufBarrier.range.buffer = m_colMajorBuffer[resourceIx]; - - // Wait for first compute write (first axis FFT) before next compute read (second axis FFT) - bufBarrier.barrier.dep.srcStageMask = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT; - bufBarrier.barrier.dep.srcAccessMask = ACCESS_FLAGS::SHADER_WRITE_BITS; - bufBarrier.barrier.dep.dstStageMask = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT; - bufBarrier.barrier.dep.dstAccessMask = ACCESS_FLAGS::SHADER_READ_BITS; - - cmdBuf->pipelineBarrier(asset::E_DEPENDENCY_FLAGS(0), bufferPipelineBarrierInfo); - // Now comes Second axis FFT + Conv + IFFT - cmdBuf->bindComputePipeline(m_lastAxisFFT_convolution_lastAxisIFFTPipeline.get()); - - // Update padding for run along rows - pushConstants.padding = paddingAlongRows; - cmdBuf->pushConstants(m_firstAxisFFTPipeline->getLayout(), IShader::E_SHADER_STAGE::ESS_COMPUTE, offsetof(PushConstantData, padding), sizeof(pushConstants.padding), &pushConstants.padding); - - // One workgroup on X per row in the lower half of the DFT - // One workgroup on Y per channel - cmdBuf->dispatch(core::roundUpToPoT(m_marginSrcDim.height) / 2, Channels, 1); - - // Recycle pipeline barrier, only have to change which buffer we need to wait to be written to - bufBarrier.range.buffer = m_rowMajorBuffer[resourceIx]; - cmdBuf->pipelineBarrier(asset::E_DEPENDENCY_FLAGS(0), bufferPipelineBarrierInfo); - - // Finally run the IFFT on the first axis - cmdBuf->bindComputePipeline(m_firstAxisIFFTPipeline.get()); - // Update padding for run along columns - pushConstants.padding = paddingAlongColumns; - cmdBuf->pushConstants(m_firstAxisFFTPipeline->getLayout(), IShader::E_SHADER_STAGE::ESS_COMPUTE, offsetof(PushConstantData, padding), sizeof(pushConstants.padding), &pushConstants.padding); - // One workgroup per 2 columns - cmdBuf->dispatch(srcDim.width / 2, 1, 1); - - // -------------------------------------- DRAW END ---------------------------------------- - - // BLIT - { - auto swapImg = m_surface->getSwapchainResources()->getImage(m_currentImageAcquire.imageIndex); - - using image_memory_barrier_t = IGPUCommandBuffer::SImageMemoryBarrier; - image_memory_barrier_t imgComputeToBlitBarrier = { - .barrier = { - .dep = { - .srcStageMask = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT, - .srcAccessMask = ACCESS_FLAGS::STORAGE_WRITE_BIT | ACCESS_FLAGS::STORAGE_READ_BIT, - .dstStageMask = PIPELINE_STAGE_FLAGS::BLIT_BIT, - .dstAccessMask = ACCESS_FLAGS::TRANSFER_WRITE_BIT - } - }, - .image = m_outImgView->getCreationParameters().image.get(), - .subresourceRange = whole2DColorImage, - .oldLayout = IImage::LAYOUT::UNDEFINED, - .newLayout = IImage::LAYOUT::UNDEFINED - }; - - // special case, the swapchain is a NONE stage with NONE accesses - image_memory_barrier_t swapchainAcquireToBlitBarrier = { - .barrier = { - .dep = { - .srcStageMask = PIPELINE_STAGE_FLAGS::NONE, - .srcAccessMask = ACCESS_FLAGS::NONE, - .dstStageMask = PIPELINE_STAGE_FLAGS::BLIT_BIT, - .dstAccessMask = ACCESS_FLAGS::TRANSFER_WRITE_BIT - } - // no ownership transfer and don't care about contents - }, - .image = swapImg, - .subresourceRange = whole2DColorImage, - .oldLayout = IImage::LAYOUT::UNDEFINED, // don't care about old contents - .newLayout = IImage::LAYOUT::TRANSFER_DST_OPTIMAL - }; - - image_memory_barrier_t imgBarriers[] = {imgComputeToBlitBarrier, swapchainAcquireToBlitBarrier}; - cmdBuf->pipelineBarrier(E_DEPENDENCY_FLAGS::EDF_NONE, { .memBarriers = {},.bufBarriers = {},.imgBarriers = imgBarriers }); - - auto outImg = m_outImgView->getCreationParameters().image.get(); - auto outImgExtent = outImg->getCreationParameters().extent; - - const IGPUCommandBuffer::SImageBlit regions[] = { { - .srcMinCoord = {0,0,0}, - .srcMaxCoord = {outImgExtent.width,outImgExtent.height,1}, - .dstMinCoord = {0,0,0}, - .dstMaxCoord = {outImgExtent.width,outImgExtent.height,1}, - .layerCount = 1, - .srcBaseLayer = 0, - .dstBaseLayer = 0, - .srcMipLevel = 0, - .dstMipLevel = 0, - .aspectMask = IGPUImage::E_ASPECT_FLAGS::EAF_COLOR_BIT - } }; - cmdBuf->blitImage(outImg, IGPUImage::LAYOUT::GENERAL, swapImg, IGPUImage::LAYOUT::TRANSFER_DST_OPTIMAL, regions, IGPUSampler::ETF_NEAREST); - - auto& swapImageBarrier = imgBarriers[1]; - swapImageBarrier.barrier.dep = swapImageBarrier.barrier.dep.nextBarrier(PIPELINE_STAGE_FLAGS::NONE, ACCESS_FLAGS::NONE); - swapImageBarrier.oldLayout = imgBarriers[1].newLayout; - swapImageBarrier.newLayout = IGPUImage::LAYOUT::PRESENT_SRC; - cmdBuf->pipelineBarrier(E_DEPENDENCY_FLAGS::EDF_NONE, { .memBarriers = {},.bufBarriers = {},.imgBarriers = {&swapImageBarrier,1} }); - } - - cmdBuf->end(); - - // Submit to queue and add sync point - { - const IQueue::SSubmitInfo::SSemaphoreInfo rendered[] = - { - { - .semaphore = m_timeline.get(), - .value = ++m_realFrameIx + 1, // The +1 here is to account for the first submit on this semaphore on kernel precomp - .stageMask = PIPELINE_STAGE_FLAGS::ALL_COMMANDS_BITS // because of the layout transition of the swapchain image - } - }; - { - { - const IQueue::SSubmitInfo::SCommandBufferInfo commandBuffers[] = - { - {.cmdbuf = cmdBuf } - }; - - const IQueue::SSubmitInfo::SSemaphoreInfo acquired[] = - { - { - .semaphore = m_currentImageAcquire.semaphore, - .value = m_currentImageAcquire.acquireCount, - .stageMask = PIPELINE_STAGE_FLAGS::NONE - } - }; - const IQueue::SSubmitInfo infos[] = - { - { - .waitSemaphores = acquired, - .commandBuffers = commandBuffers, - .signalSemaphores = rendered - } - }; - - m_api->startCapture(); - if (m_queue->submit(infos) != IQueue::RESULT::SUCCESS) - --m_realFrameIx; - m_api->endCapture(); - } - } - - m_surface->present(m_currentImageAcquire.imageIndex, rendered); - } - } - - bool onAppTerminated() override - { - // Wait for all work to be done - m_device->waitIdle(); - - // Copied from 64_FFT which I think copies from ex 07 - // Create a buffer for download - const auto& deviceLimits = m_device->getPhysicalDevice()->getLimits(); - uint32_t alignment = core::max(deviceLimits.nonCoherentAtomSize, alignof(float)); - auto srcImageDims = m_srcImageView->getCreationParameters().image->getCreationParameters().extent; - const uint32_t srcImageSize = srcImageDims.height * srcImageDims.width * srcImageDims.depth * (Channels + 1) * sizeof(float32_t); - - auto downStreamingBuffer = m_utils->getDefaultDownStreamingBuffer(); - - auto outputOffset = downStreamingBuffer->invalid_value; - std::chrono::steady_clock::time_point waitTill(std::chrono::years(45)); - const uint32_t AllocationCount = 1; - downStreamingBuffer->multi_allocate(waitTill, AllocationCount, &outputOffset, &srcImageSize, &alignment); - - // Since all work is done grab any command buffer - auto cmdBuf = m_cmdBufs[0]; - - // Send download commands to GPU - { - cmdBuf->reset(IGPUCommandBuffer::RESET_FLAGS::RELEASE_RESOURCES_BIT); - cmdBuf->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT); - - // Pipeline barrier: transition outImg to transfer source optimal - IGPUCommandBuffer::SPipelineBarrierDependencyInfo imagePipelineBarrierInfo = {}; - decltype(imagePipelineBarrierInfo)::image_barrier_t imgBarrier; - imagePipelineBarrierInfo.imgBarriers = { &imgBarrier, 1 }; - - imgBarrier.image = m_outImgView->getCreationParameters().image.get(); - imgBarrier.barrier.dep.srcStageMask = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT; - imgBarrier.barrier.dep.srcAccessMask = ACCESS_FLAGS::SHADER_WRITE_BITS; - imgBarrier.barrier.dep.dstStageMask = PIPELINE_STAGE_FLAGS::COPY_BIT; - imgBarrier.barrier.dep.dstAccessMask = ACCESS_FLAGS::TRANSFER_READ_BIT; - imgBarrier.oldLayout = IImage::LAYOUT::GENERAL; - imgBarrier.newLayout = IImage::LAYOUT::TRANSFER_SRC_OPTIMAL; - imgBarrier.subresourceRange = { IGPUImage::EAF_COLOR_BIT, 0u, 1u, 0u, 1 }; - - cmdBuf->pipelineBarrier(asset::E_DEPENDENCY_FLAGS(0), imagePipelineBarrierInfo); - IImage::SBufferCopy copy; - copy.imageExtent = m_outImgView->getCreationParameters().image->getCreationParameters().extent; - copy.imageSubresource = { IImage::EAF_COLOR_BIT, 0u, 0u, 1u }; - cmdBuf->copyImageToBuffer(m_outImgView->getCreationParameters().image.get(), IImage::LAYOUT::TRANSFER_SRC_OPTIMAL, downStreamingBuffer->getBuffer(), 1, ©); - cmdBuf->end(); - } - // Submit - { - const IQueue::SSubmitInfo::SCommandBufferInfo cmdbufInfo = - { - .cmdbuf = cmdBuf.get() - }; - const IQueue::SSubmitInfo::SSemaphoreInfo signalInfo = - { - .semaphore = m_timeline.get(), - .value = m_realFrameIx + 1, - .stageMask = asset::PIPELINE_STAGE_FLAGS::COPY_BIT - }; - - const IQueue::SSubmitInfo submitInfo = { - .commandBuffers = {&cmdbufInfo,1}, - .signalSemaphores = {&signalInfo,1} - }; - - m_queue->submit({ &submitInfo,1 }); - } - - // We let all latches know what semaphore and counter value has to be passed for the functors to execute - const ISemaphore::SWaitInfo futureWait = { m_timeline.get(),m_realFrameIx + 1 }; - - // Now a new and even more advanced usage of the latched events, we make our own refcounted object with a custom destructor and latch that like we did the commandbuffer. - // Instead of making our own and duplicating logic, we'll use one from IUtilities meant for down-staging memory. - // Its nice because it will also remember to invalidate our memory mapping if its not coherent. - auto latchedConsumer = make_smart_refctd_ptr( - IDeviceMemoryAllocation::MemoryRange(outputOffset, srcImageSize), - // Note the use of capture by-value [=] and not by-reference [&] because this lambda will be called asynchronously whenever the event signals - [=](const size_t dstOffset, const void* bufSrc, const size_t size)->void - { - // image view - core::smart_refctd_ptr imageView; - { - // create image - ICPUImage::SCreationParams imgParams; - imgParams.flags = static_cast(0u); // no flags - imgParams.type = ICPUImage::ET_2D; - imgParams.format = m_outImgView->getCreationParameters().image->getCreationParameters().format; - imgParams.extent = m_outImgView->getCreationParameters().image->getCreationParameters().extent; - imgParams.mipLevels = 1u; - imgParams.arrayLayers = 1u; - imgParams.samples = ICPUImage::ESCF_1_BIT; - - auto image = ICPUImage::create(std::move(imgParams)); - { - // set up regions - auto regions = core::make_refctd_dynamic_array >(1u); - { - auto& region = regions->front(); - region.bufferOffset = 0u; - region.bufferRowLength = 0; - region.bufferImageHeight = 0; - region.imageSubresource.aspectMask = IImage::EAF_COLOR_BIT; - region.imageSubresource.mipLevel = 0u; - region.imageSubresource.baseArrayLayer = 0u; - region.imageSubresource.layerCount = 1u; - region.imageOffset = { 0u,0u,0u }; - region.imageExtent = imgParams.extent; - } - // the cpu is not touching the data yet because the custom CPUBuffer is adopting the memory (no copy) - auto* data = reinterpret_cast(downStreamingBuffer->getBufferPointer()) + outputOffset; - //.size = srcImageSize, .data = data, .memoryResource = core::getNullMemoryResource() - ICPUBuffer::SCreationParams cpuBufferAliasCreationParams = { .data = data, .memoryResource = core::getNullMemoryResource()}; // Don't free on exit, we're not taking ownership - cpuBufferAliasCreationParams.size = srcImageSize; - auto cpuBufferAlias = ICPUBuffer::create(std::move(cpuBufferAliasCreationParams), core::adopt_memory); - image->setBufferAndRegions(std::move(cpuBufferAlias), regions); - } - - // create image view - ICPUImageView::SCreationParams imgViewParams; - imgViewParams.flags = static_cast(0u); - imgViewParams.format = image->getCreationParameters().format; - imgViewParams.image = std::move(image); - imgViewParams.viewType = ICPUImageView::ET_2D; - imgViewParams.subresourceRange = { IImage::EAF_COLOR_BIT,0u,1u,0u,1u }; - imageView = ICPUImageView::create(std::move(imgViewParams)); - } - - // save as .EXR image - { - IAssetWriter::SAssetWriteParams wp(imageView.get()); - m_assetMgr->writeAsset((localOutputCWD / "convolved.exr").string(), wp); - } - }, - // Its also necessary to hold onto the commandbuffer, even though we take care to not reset the parent pool, because if it - // hits its destructor, our automated reference counting will drop all references to objects used in the recorded commands. - // It could also be latched in the upstreaming deallocate, because its the same fence. - std::move(cmdBuf), downStreamingBuffer - ); - // We put a function we want to execute - downStreamingBuffer->multi_deallocate(AllocationCount, &outputOffset, &srcImageSize, futureWait, &latchedConsumer.get()); - - // Need to make sure that there are no events outstanding if we want all lambdas to eventually execute before `onAppTerminated` - // (the destructors of the Command Pool Cache and Streaming buffers will still wait for all lambda events to drain) - while (downStreamingBuffer->cull_frees()) {} - return device_base_t::onAppTerminated(); - } -}; - - -NBL_MAIN_FUNC(FFTBloomApp) \ No newline at end of file diff --git a/28_FFTBloom/pipeline.groovy b/28_FFTBloom/pipeline.groovy deleted file mode 100644 index 1a7b043a4..000000000 --- a/28_FFTBloom/pipeline.groovy +++ /dev/null @@ -1,50 +0,0 @@ -import org.DevshGraphicsProgramming.Agent -import org.DevshGraphicsProgramming.BuilderInfo -import org.DevshGraphicsProgramming.IBuilder - -class CStreamingAndBufferDeviceAddressBuilder extends IBuilder -{ - public CStreamingAndBufferDeviceAddressBuilder(Agent _agent, _info) - { - super(_agent, _info) - } - - @Override - public boolean prepare(Map axisMapping) - { - return true - } - - @Override - public boolean build(Map axisMapping) - { - IBuilder.CONFIGURATION config = axisMapping.get("CONFIGURATION") - IBuilder.BUILD_TYPE buildType = axisMapping.get("BUILD_TYPE") - - def nameOfBuildDirectory = getNameOfBuildDirectory(buildType) - def nameOfConfig = getNameOfConfig(config) - - agent.execute("cmake --build ${info.rootProjectPath}/${nameOfBuildDirectory}/${info.targetProjectPathRelativeToRoot} --target ${info.targetBaseName} --config ${nameOfConfig} -j12 -v") - - return true - } - - @Override - public boolean test(Map axisMapping) - { - return true - } - - @Override - public boolean install(Map axisMapping) - { - return true - } -} - -def create(Agent _agent, _info) -{ - return new CStreamingAndBufferDeviceAddressBuilder(_agent, _info) -} - -return this \ No newline at end of file diff --git a/29_Arithmetic2Bench/CMakeLists.txt b/29_Arithmetic2Bench/CMakeLists.txt deleted file mode 100644 index 99c51769c..000000000 --- a/29_Arithmetic2Bench/CMakeLists.txt +++ /dev/null @@ -1,15 +0,0 @@ -include(common) - -nbl_create_executable_project("" "" "" "") - -NBL_CREATE_RESOURCE_ARCHIVE( - NAMESPACE nbl::this_example::builtin - TARGET ${EXECUTABLE_NAME}_builtins - LINK_TO ${EXECUTABLE_NAME} - BIND app_resources - BUILTINS - benchmarkSubgroup.comp.hlsl - benchmarkWorkgroup.comp.hlsl - common.hlsl - shaderCommon.hlsl -) \ No newline at end of file diff --git a/29_Arithmetic2Bench/app_resources/benchmarkSubgroup.comp.hlsl b/29_Arithmetic2Bench/app_resources/benchmarkSubgroup.comp.hlsl deleted file mode 100644 index 018672386..000000000 --- a/29_Arithmetic2Bench/app_resources/benchmarkSubgroup.comp.hlsl +++ /dev/null @@ -1,57 +0,0 @@ -#pragma shader_stage(compute) - -#define operation_t nbl::hlsl::OPERATION - -#include "nbl/builtin/hlsl/glsl_compat/core.hlsl" -#include "nbl/builtin/hlsl/glsl_compat/subgroup_basic.hlsl" -#include "nbl/builtin/hlsl/subgroup2/arithmetic_params.hlsl" -#include "nbl/builtin/hlsl/subgroup2/arithmetic_portability.hlsl" -#include "nbl/builtin/hlsl/random/xoroshiro.hlsl" - -#include "app_resources/shaderCommon.hlsl" -#include "nbl/builtin/hlsl/workgroup2/basic.hlsl" - -template -using params_t = SUBGROUP_CONFIG_T; - -NBL_CONSTEXPR_STATIC_INLINE uint32_t ItemsPerInvocation = params_t::base_t, device_capabilities>::ItemsPerInvocation; - -typedef vector type_t; - -uint32_t globalIndex() -{ - return glsl::gl_WorkGroupID().x*WORKGROUP_SIZE+workgroup::SubgroupContiguousIndex(); -} - -template -static void subbench(NBL_CONST_REF_ARG(type_t) sourceVal) -{ - type_t value = sourceVal; - - const uint64_t outputBufAddr = pc.pOutputBuf[Binop::BindingIndex]; - - operation_t > func; - // [unroll] - for (uint32_t i = 0; i < NUM_LOOPS; i++) - value = func(value); - - vk::RawBufferStore(outputBufAddr + sizeof(type_t) * globalIndex(), value, sizeof(uint32_t)); -} - -void benchmark() -{ - const uint32_t invocationIndex = globalIndex(); - type_t sourceVal; - Xoroshiro64Star xoroshiro = Xoroshiro64Star::construct(uint32_t2(invocationIndex,invocationIndex+1)); - [unroll] - for (uint16_t i = 0; i < ItemsPerInvocation; i++) - sourceVal[i] = xoroshiro(); - - subbench >(sourceVal); -} - -[numthreads(WORKGROUP_SIZE,1,1)] -void main() -{ - benchmark(); -} diff --git a/29_Arithmetic2Bench/app_resources/benchmarkWorkgroup.comp.hlsl b/29_Arithmetic2Bench/app_resources/benchmarkWorkgroup.comp.hlsl deleted file mode 100644 index 8442ecc38..000000000 --- a/29_Arithmetic2Bench/app_resources/benchmarkWorkgroup.comp.hlsl +++ /dev/null @@ -1,125 +0,0 @@ -#pragma shader_stage(compute) - -#include "nbl/builtin/hlsl/glsl_compat/core.hlsl" -#include "nbl/builtin/hlsl/glsl_compat/subgroup_basic.hlsl" -#include "nbl/builtin/hlsl/subgroup2/arithmetic_portability.hlsl" -#include "nbl/builtin/hlsl/workgroup2/arithmetic.hlsl" -#include "nbl/builtin/hlsl/random/xoroshiro.hlsl" - -using config_t = WORKGROUP_CONFIG_T; - -#include "app_resources/shaderCommon.hlsl" - -typedef vector type_t; - -// final (level 1/2) scan needs to fit in one subgroup exactly -groupshared uint32_t scratch[mpl::max_v]; - -#include "nbl/examples/workgroup/DataAccessors.hlsl" -using namespace nbl::hlsl::examples::workgroup; - -template -struct RandomizedInputDataProxy -{ - using dtype_t = vector; - - NBL_CONSTEXPR_STATIC_INLINE uint16_t WorkgroupSize = uint16_t(1u) << WorkgroupSizeLog2; - NBL_CONSTEXPR_STATIC_INLINE uint16_t PreloadedDataCount = VirtualWorkgroupSize / WorkgroupSize; - - static RandomizedInputDataProxy create(uint64_t inputBuf, uint64_t outputBuf) - { - RandomizedInputDataProxy retval; - retval.data = DataProxy::create(inputBuf, outputBuf); - return retval; - } - - template - void get(const IndexType ix, NBL_REF_ARG(AccessType) value) - { - value = preloaded[ix>>WorkgroupSizeLog2]; - } - template - void set(const IndexType ix, const AccessType value) - { - preloaded[ix>>WorkgroupSizeLog2] = value; - } - - void preload() - { - const uint16_t invocationIndex = workgroup::SubgroupContiguousIndex(); - Xoroshiro64Star xoroshiro = Xoroshiro64Star::construct(uint32_t2(invocationIndex,invocationIndex+1)); - [unroll] - for (uint16_t idx = 0; idx < PreloadedDataCount; idx++) - [unroll] - for (uint16_t i = 0; i < ItemsPerInvocation; i++) - preloaded[idx][i] = xoroshiro(); - } - void unload() - { - const uint16_t invocationIndex = workgroup::SubgroupContiguousIndex(); - [unroll] - for (uint16_t idx = 0; idx < PreloadedDataCount; idx++) - data.template set(idx * WorkgroupSize + invocationIndex, preloaded[idx]); - } - - void workgroupExecutionAndMemoryBarrier() - { - glsl::barrier(); - //glsl::memoryBarrierShared(); implied by the above - } - - DataProxy data; - dtype_t preloaded[PreloadedDataCount]; -}; - -static ScratchProxy arithmeticAccessor; - -using data_proxy_t = RandomizedInputDataProxy; - -template -struct operation_t -{ - using binop_base_t = typename Binop::base_t; - using otype_t = typename Binop::type_t; - - void operator()(data_proxy_t dataAccessor) - { -#if IS_REDUCTION - otype_t value = -#endif - OPERATION::template __call(dataAccessor,arithmeticAccessor); - // we barrier before because we alias the accessors for Binop - arithmeticAccessor.workgroupExecutionAndMemoryBarrier(); -#if IS_REDUCTION - [unroll] - for (uint32_t i = 0; i < data_proxy_t::PreloadedDataCount; i++) - dataAccessor.preloaded[i] = value; -#endif - } -}; - -template -static void subbench() -{ - data_proxy_t dataAccessor = data_proxy_t::create(0, pc.pOutputBuf[Binop::BindingIndex]); - dataAccessor.preload(); - - operation_t func; - for (uint32_t i = 0; i < NUM_LOOPS; i++) - func(dataAccessor); - - dataAccessor.unload(); -} - -void benchmark() -{ - // only benchmark plus op - subbench >(); -} - - -[numthreads(config_t::WorkgroupSize,1,1)] -void main() -{ - benchmark(); -} diff --git a/29_Arithmetic2Bench/app_resources/common.hlsl b/29_Arithmetic2Bench/app_resources/common.hlsl deleted file mode 100644 index cca5af987..000000000 --- a/29_Arithmetic2Bench/app_resources/common.hlsl +++ /dev/null @@ -1,34 +0,0 @@ -#include "nbl/builtin/hlsl/cpp_compat.hlsl" -#include "nbl/builtin/hlsl/functional.hlsl" - -struct PushConstantData -{ - uint64_t pOutputBuf[2]; -}; - -namespace arithmetic -{ -template -struct plus : nbl::hlsl::plus -{ - using base_t = nbl::hlsl::plus; - - NBL_CONSTEXPR_STATIC_INLINE uint16_t BindingIndex = 0; -#ifndef __HLSL_VERSION - static inline constexpr const char* name = "plus"; -#endif -}; - -template -struct ballot : nbl::hlsl::plus -{ - using base_t = nbl::hlsl::plus; - - NBL_CONSTEXPR_STATIC_INLINE uint16_t BindingIndex = 1; -#ifndef __HLSL_VERSION - static inline constexpr const char* name = "bitcount"; -#endif -}; -} - -#include "nbl/builtin/hlsl/glsl_compat/subgroup_basic.hlsl" diff --git a/29_Arithmetic2Bench/app_resources/shaderCommon.hlsl b/29_Arithmetic2Bench/app_resources/shaderCommon.hlsl deleted file mode 100644 index ec5824a21..000000000 --- a/29_Arithmetic2Bench/app_resources/shaderCommon.hlsl +++ /dev/null @@ -1,26 +0,0 @@ -#include "app_resources/common.hlsl" - -using namespace nbl; -using namespace hlsl; - -[[vk::push_constant]] PushConstantData pc; - -struct device_capabilities -{ -#ifdef TEST_NATIVE - NBL_CONSTEXPR_STATIC_INLINE bool shaderSubgroupArithmetic = true; -#else - NBL_CONSTEXPR_STATIC_INLINE bool shaderSubgroupArithmetic = false; -#endif -}; - -#ifndef OPERATION -#error "Define OPERATION!" -#endif - -#ifndef NUM_LOOPS -#error "Define NUM_LOOPS!" -#endif - -// NOTE added dummy output image to be able to profile with Nsight, which still doesn't support profiling headless compute shaders -[[vk::binding(2, 0)]] RWTexture2D outImage; // dummy diff --git a/29_Arithmetic2Bench/main.cpp b/29_Arithmetic2Bench/main.cpp deleted file mode 100644 index 889401d3d..000000000 --- a/29_Arithmetic2Bench/main.cpp +++ /dev/null @@ -1,707 +0,0 @@ -#include "nbl/examples/examples.hpp" -#include "app_resources/common.hlsl" -#include "nbl/builtin/hlsl/workgroup2/arithmetic_config.hlsl" -#include "nbl/builtin/hlsl/subgroup2/arithmetic_params.hlsl" - -using namespace nbl; -using namespace nbl::core; -using namespace nbl::system; -using namespace nbl::asset; -using namespace nbl::ui; -using namespace nbl::video; -using namespace nbl::examples; - - -template requires std::is_base_of_v -class CExplicitSurfaceFormatResizeSurface final : public ISimpleManagedSurface -{ -public: - using this_t = CExplicitSurfaceFormatResizeSurface; - - // Factory method so we can fail, requires a `_surface` created from a window and with a callback that inherits from `ICallback` declared just above - template requires std::is_base_of_v, Surface> - static inline core::smart_refctd_ptr create(core::smart_refctd_ptr&& _surface) - { - if (!_surface) - return nullptr; - - auto _window = _surface->getWindow(); - ICallback* cb = nullptr; - if (_window) - cb = dynamic_cast(_window->getEventCallback()); - - return core::smart_refctd_ptr(new this_t(std::move(_surface), cb), core::dont_grab); - } - - // Factory method so we can fail, requires a `_surface` created from a native surface - template requires std::is_base_of_v, Surface> - static inline core::smart_refctd_ptr create(core::smart_refctd_ptr&& _surface, ICallback* cb) - { - if (!_surface) - return nullptr; - - return core::smart_refctd_ptr(new this_t(std::move(_surface), cb), core::dont_grab); - } - - // - inline bool init(CThreadSafeQueueAdapter* queue, std::unique_ptr&& scResources, const ISwapchain::SSharedCreationParams& sharedParams = {}) - { - if (!scResources || !base_init(queue)) - return init_fail(); - - m_sharedParams = sharedParams; - if (!m_sharedParams.deduce(queue->getOriginDevice()->getPhysicalDevice(), getSurface())) - return init_fail(); - - m_swapchainResources = std::move(scResources); - return true; - } - - // Can be public because we don't need to worry about mutexes unlike the Smooth Resize class - inline ISwapchainResources* getSwapchainResources() override { return m_swapchainResources.get(); } - - // need to see if the swapchain is invalidated (e.g. because we're starting from 0-area old Swapchain) and try to recreate the swapchain - inline SAcquireResult acquireNextImage() - { - if (!isWindowOpen()) - { - becomeIrrecoverable(); - return {}; - } - - if (!m_swapchainResources || (m_swapchainResources->getStatus() != ISwapchainResources::STATUS::USABLE && !recreateSwapchain(m_surfaceFormat))) - return {}; - - return ISimpleManagedSurface::acquireNextImage(); - } - - // its enough to just foward though - inline bool present(const uint8_t imageIndex, const std::span waitSemaphores) - { - return ISimpleManagedSurface::present(imageIndex, waitSemaphores); - } - - // - inline bool recreateSwapchain(const ISurface::SFormat& explicitSurfaceFormat) - { - assert(m_swapchainResources); - // dont assign straight to `m_swapchainResources` because of complex refcounting and cycles - core::smart_refctd_ptr newSwapchain; - // TODO: This block of code could be rolled up into `ISimpleManagedSurface::ISwapchainResources` eventually - { - auto* surface = getSurface(); - auto device = const_cast(getAssignedQueue()->getOriginDevice()); - // 0s are invalid values, so they indicate we want them deduced - m_sharedParams.width = 0; - m_sharedParams.height = 0; - // Question: should we re-query the supported queues, formats, present modes, etc. just-in-time?? - auto* swapchain = m_swapchainResources->getSwapchain(); - if (swapchain ? swapchain->deduceRecreationParams(m_sharedParams) : m_sharedParams.deduce(device->getPhysicalDevice(), surface)) - { - // super special case, we can't re-create the swapchain but its possible to recover later on - if (m_sharedParams.width == 0 || m_sharedParams.height == 0) - { - // we need to keep the old-swapchain around, but can drop the rest - m_swapchainResources->invalidate(); - return false; - } - // now lets try to create a new swapchain - if (swapchain) - newSwapchain = swapchain->recreate(m_sharedParams); - else - { - ISwapchain::SCreationParams params = { - .surface = core::smart_refctd_ptr(surface), - .surfaceFormat = explicitSurfaceFormat, - .sharedParams = m_sharedParams - // we're not going to support concurrent sharing in this simple class - }; - m_surfaceFormat = explicitSurfaceFormat; - newSwapchain = CVulkanSwapchain::create(core::smart_refctd_ptr(device), std::move(params)); - } - } - else // parameter deduction failed - return false; - } - - if (newSwapchain) - { - m_swapchainResources->invalidate(); - return m_swapchainResources->onCreateSwapchain(getAssignedQueue()->getFamilyIndex(), std::move(newSwapchain)); - } - else - becomeIrrecoverable(); - - return false; - } - -protected: - using ISimpleManagedSurface::ISimpleManagedSurface; - - // - inline void deinit_impl() override final - { - becomeIrrecoverable(); - } - - // - inline void becomeIrrecoverable() override { m_swapchainResources = nullptr; } - - // gets called when OUT_OF_DATE upon an acquire - inline SAcquireResult handleOutOfDate() override final - { - // recreate swapchain and try to acquire again - if (recreateSwapchain(m_surfaceFormat)) - return ISimpleManagedSurface::acquireNextImage(); - return {}; - } - -private: - // Because the surface can start minimized (extent={0,0}) we might not be able to create the swapchain right away, so store creation parameters until we can create it. - ISwapchain::SSharedCreationParams m_sharedParams = {}; - // The swapchain might not be possible to create or recreate right away, so this might be - // either nullptr before the first successful acquire or the old to-be-retired swapchain. - std::unique_ptr m_swapchainResources = {}; - - ISurface::SFormat m_surfaceFormat = {}; -}; - -// NOTE added swapchain + drawing frames to be able to profile with Nsight, which still doesn't support profiling headless compute shaders -class ArithmeticBenchApp final : public examples::SimpleWindowedApplication, public examples::BuiltinResourcesApplication -{ - using device_base_t = examples::SimpleWindowedApplication; - using asset_base_t = examples::BuiltinResourcesApplication; - - constexpr static inline uint32_t WIN_W = 1280; - constexpr static inline uint32_t WIN_H = 720; - constexpr static inline uint32_t MaxFramesInFlight = 5; - -public: - ArithmeticBenchApp(const path& _localInputCWD, const path& _localOutputCWD, const path& _sharedInputCWD, const path& _sharedOutputCWD) : - system::IApplicationFramework(_localInputCWD, _localOutputCWD, _sharedInputCWD, _sharedOutputCWD) {} - - virtual SPhysicalDeviceFeatures getPreferredDeviceFeatures() const override - { - auto retval = device_base_t::getPreferredDeviceFeatures(); - retval.pipelineExecutableInfo = true; - return retval; - } - - inline core::vector getSurfaces() const override - { - if (!m_surface) - { - { - auto windowCallback = core::make_smart_refctd_ptr(smart_refctd_ptr(m_inputSystem), smart_refctd_ptr(m_logger)); - IWindow::SCreationParams params = {}; - params.callback = core::make_smart_refctd_ptr(); - params.width = WIN_W; - params.height = WIN_H; - params.x = 32; - params.y = 32; - params.flags = ui::IWindow::ECF_HIDDEN | IWindow::ECF_BORDERLESS | IWindow::ECF_RESIZABLE; - params.windowCaption = "ArithmeticBenchApp"; - params.callback = windowCallback; - 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) = CExplicitSurfaceFormatResizeSurface::create(std::move(surface)); - } - - if (m_surface) - return { {m_surface->getSurface()/*,EQF_NONE*/} }; - - return {}; - } - - bool onAppInitialized(smart_refctd_ptr&& system) override - { - m_inputSystem = make_smart_refctd_ptr(logger_opt_smart_ptr(smart_refctd_ptr(m_logger))); - - if (!device_base_t::onAppInitialized(std::move(system))) - return false; - if (!asset_base_t::onAppInitialized(std::move(system))) - return false; - - m_semaphore = m_device->createSemaphore(m_realFrameIx); - if (!m_semaphore) - return logFail("Failed to Create a Semaphore!"); - - ISwapchain::SCreationParams swapchainParams = { .surface = m_surface->getSurface() }; - asset::E_FORMAT preferredFormats[] = { asset::EF_R8G8B8A8_UNORM }; - if (!swapchainParams.deduceFormat(m_physicalDevice, preferredFormats)) - return logFail("Could not choose a Surface Format for the Swapchain!"); - - swapchainParams.sharedParams.imageUsage = IGPUImage::E_USAGE_FLAGS::EUF_RENDER_ATTACHMENT_BIT | IGPUImage::E_USAGE_FLAGS::EUF_STORAGE_BIT; - - auto graphicsQueue = getGraphicsQueue(); - if (!m_surface || !m_surface->init(graphicsQueue, std::make_unique(), swapchainParams.sharedParams)) - return logFail("Could not create Window & Surface or initialize the Surface!"); - - auto pool = m_device->createCommandPool(graphicsQueue->getFamilyIndex(), IGPUCommandPool::CREATE_FLAGS::RESET_COMMAND_BUFFER_BIT); - - for (auto i = 0u; i < MaxFramesInFlight; i++) - { - if (!pool) - return logFail("Couldn't create Command Pool!"); - if (!pool->createCommandBuffers(IGPUCommandPool::BUFFER_LEVEL::PRIMARY, { m_cmdBufs.data() + i, 1 })) - return logFail("Couldn't create Command Buffer!"); - } - - m_winMgr->setWindowSize(m_window.get(), WIN_W, WIN_H); - m_surface->recreateSwapchain(swapchainParams.surfaceFormat); - - transferDownQueue = getTransferDownQueue(); - computeQueue = getComputeQueue(); - - // create 2 buffers for 2 operations - for (auto i=0u; icreateBuffer(std::move(params)); - auto mreq = outputBuffers[i]->getMemoryReqs(); - mreq.memoryTypeBits &= m_physicalDevice->getDeviceLocalMemoryTypeBits(); - assert(mreq.memoryTypeBits); - - auto bufferMem = m_device->allocate(mreq, outputBuffers[i].get(), IDeviceMemoryAllocation::EMAF_DEVICE_ADDRESS_BIT); - assert(bufferMem.isValid()); - } - for (auto i = 0u; i < OutputBufferCount; i++) - pc.pOutputBuf[i] = outputBuffers[i]->getDeviceAddress(); - - // create image views for swapchain images - for (uint32_t i = 0; i < ISwapchain::MaxImages; i++) - { - IGPUImage* scImg = m_surface->getSwapchainResources()->getImage(i); - if (scImg == nullptr) - continue; - IGPUImageView::SCreationParams viewParams = { - .flags = IGPUImageView::ECF_NONE, - .subUsages = IGPUImage::E_USAGE_FLAGS::EUF_STORAGE_BIT, - .image = smart_refctd_ptr(scImg), - .viewType = IGPUImageView::ET_2D, - .format = scImg->getCreationParameters().format - }; - swapchainImageViews[i] = m_device->createImageView(std::move(viewParams)); - } - - // create Descriptor Sets and Pipeline Layouts - smart_refctd_ptr benchPplnLayout; - { - // set and transient pool - smart_refctd_ptr benchLayout; - { - IGPUDescriptorSetLayout::SBinding binding[1]; - binding[0] = { {},2,IDescriptor::E_TYPE::ET_STORAGE_IMAGE,IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_UPDATE_AFTER_BIND_BIT,IShader::E_SHADER_STAGE::ESS_COMPUTE,1u,nullptr }; - benchLayout = m_device->createDescriptorSetLayout(binding); - } - - const uint32_t setCount = ISwapchain::MaxImages; - benchPool = m_device->createDescriptorPoolForDSLayouts(IDescriptorPool::ECF_UPDATE_AFTER_BIND_BIT, { &benchLayout.get(),1 }, &setCount); - for (auto i = 0u; i < ISwapchain::MaxImages; i++) - { - benchDs[i] = benchPool->createDescriptorSet(smart_refctd_ptr(benchLayout)); - if (!benchDs[i]) - return logFail("Could not create Descriptor Set!"); - } - - SPushConstantRange pcRange = { .stageFlags = IShader::E_SHADER_STAGE::ESS_COMPUTE, .offset = 0,.size = sizeof(PushConstantData) }; - benchPplnLayout = m_device->createPipelineLayout({ &pcRange, 1 }, std::move(benchLayout)); - } - if (UseNativeArithmetic && !m_physicalDevice->getProperties().limits.shaderSubgroupArithmetic) - { - logFail("UseNativeArithmetic is true but device does not support shaderSubgroupArithmetic!"); - return false; - } - - IGPUDescriptorSet::SWriteDescriptorSet dsWrites[ISwapchain::MaxImages]; - for (auto i = 0u; i < ISwapchain::MaxImages; i++) - { - if (swapchainImageViews[i].get() == nullptr) - continue; - - video::IGPUDescriptorSet::SDescriptorInfo dsInfo; - dsInfo.info.image.imageLayout = IImage::LAYOUT::GENERAL; - dsInfo.desc = swapchainImageViews[i]; - - dsWrites[i] = - { - .dstSet = benchDs[i].get(), - .binding = 2u, - .arrayElement = 0u, - .count = 1u, - .info = &dsInfo, - }; - m_device->updateDescriptorSets(1u, &dsWrites[i], 0u, nullptr); - } - - - // load shader source from file - auto getShaderSource = [&](const char* filePath) -> auto - { - IAssetLoader::SAssetLoadParams lparams = {}; - lparams.logger = m_logger.get(); - lparams.workingDirectory = ""; - auto bundle = m_assetMgr->getAsset(filePath, lparams); - if (bundle.getContents().empty() || bundle.getAssetType()!=IAsset::ET_SHADER) - { - m_logger->log("Shader %s not found!", ILogger::ELL_ERROR, filePath); - exit(-1); - } - auto firstAssetInBundle = bundle.getContents()[0]; - return smart_refctd_ptr_static_cast(firstAssetInBundle); - }; - - // for each workgroup size (manually adjust items per invoc, operation else uses up a lot of ram) - const auto MaxSubgroupSize = m_physicalDevice->getLimits().maxSubgroupSize; - smart_refctd_ptr shaderSource; - if constexpr (DoWorkgroupBenchmarks) - shaderSource = getShaderSource("app_resources/benchmarkWorkgroup.comp.hlsl"); - else - shaderSource = getShaderSource("app_resources/benchmarkSubgroup.comp.hlsl"); - - for (uint32_t op = 0; op < arithmeticOperations.size(); op++) - for (uint32_t i = 0; i < workgroupSizes.size(); i++) - benchSets[op*workgroupSizes.size()+i] = createBenchmarkPipelines(shaderSource, benchPplnLayout.get(), ElementCount, arithmeticOperations[op], hlsl::findMSB(MaxSubgroupSize), workgroupSizes[i], ItemsPerInvocation, NumLoops); - - m_winMgr->show(m_window.get()); - - return true; - } - - virtual bool onAppTerminated() override - { - return true; - } - - // the unit test is carried out on init - void workLoopBody() override - { - const auto resourceIx = m_realFrameIx % MaxFramesInFlight; - - const uint32_t framesInFlight = core::min(MaxFramesInFlight, m_surface->getMaxAcquiresInFlight()); - - if (m_realFrameIx >= framesInFlight) - { - const ISemaphore::SWaitInfo cbDonePending[] = - { - { - .semaphore = m_semaphore.get(), - .value = m_realFrameIx + 1 - framesInFlight - } - }; - if (m_device->blockForSemaphores(cbDonePending) != ISemaphore::WAIT_RESULT::SUCCESS) - return; - } - - m_currentImageAcquire = m_surface->acquireNextImage(); - if (!m_currentImageAcquire) - return; - - auto* const cmdbuf = m_cmdBufs.data()[resourceIx].get(); - cmdbuf->reset(IGPUCommandBuffer::RESET_FLAGS::RELEASE_RESOURCES_BIT); - cmdbuf->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT); - - const auto MaxSubgroupSize = m_physicalDevice->getLimits().maxSubgroupSize; - const auto SubgroupSizeLog2 = hlsl::findMSB(MaxSubgroupSize); - - cmdbuf->bindDescriptorSets(EPBP_COMPUTE, benchSets[0].pipeline->getLayout(), 0u, 1u, &benchDs[m_currentImageAcquire.imageIndex].get()); - cmdbuf->pushConstants(benchSets[0].pipeline->getLayout(), IShader::E_SHADER_STAGE::ESS_COMPUTE, 0, sizeof(PushConstantData), &pc); - - for (uint32_t i = 0; i < benchSets.size(); i++) - runBenchmark(cmdbuf, benchSets[i], ElementCount, SubgroupSizeLog2); - - // barrier transition to PRESENT - { - IGPUCommandBuffer::SPipelineBarrierDependencyInfo::image_barrier_t imageBarriers[1]; - imageBarriers[0].barrier = { - .dep = { - .srcStageMask = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT, - .srcAccessMask = ACCESS_FLAGS::SHADER_WRITE_BITS, - .dstStageMask = PIPELINE_STAGE_FLAGS::NONE, - .dstAccessMask = ACCESS_FLAGS::NONE - } - }; - imageBarriers[0].image = m_surface->getSwapchainResources()->getImage(m_currentImageAcquire.imageIndex); - imageBarriers[0].subresourceRange = { - .aspectMask = IImage::EAF_COLOR_BIT, - .baseMipLevel = 0u, - .levelCount = 1u, - .baseArrayLayer = 0u, - .layerCount = 1u - }; - imageBarriers[0].oldLayout = IImage::LAYOUT::UNDEFINED; - imageBarriers[0].newLayout = IImage::LAYOUT::PRESENT_SRC; - - cmdbuf->pipelineBarrier(E_DEPENDENCY_FLAGS::EDF_NONE, { .imgBarriers = imageBarriers }); - } - - cmdbuf->end(); - - // submit - { - auto* queue = getGraphicsQueue(); - const IQueue::SSubmitInfo::SSemaphoreInfo rendered[] = - { - { - .semaphore = m_semaphore.get(), - .value = ++m_realFrameIx, - .stageMask = PIPELINE_STAGE_FLAGS::ALL_TRANSFER_BITS - } - }; - { - { - const IQueue::SSubmitInfo::SCommandBufferInfo commandBuffers[] = - { - {.cmdbuf = cmdbuf } - }; - - const IQueue::SSubmitInfo::SSemaphoreInfo acquired[] = - { - { - .semaphore = m_currentImageAcquire.semaphore, - .value = m_currentImageAcquire.acquireCount, - .stageMask = PIPELINE_STAGE_FLAGS::NONE - } - }; - const IQueue::SSubmitInfo infos[] = - { - { - .waitSemaphores = acquired, - .commandBuffers = commandBuffers, - .signalSemaphores = rendered - } - }; - - if (queue->submit(infos) == IQueue::RESULT::SUCCESS) - { - const nbl::video::ISemaphore::SWaitInfo waitInfos[] = - { { - .semaphore = m_semaphore.get(), - .value = m_realFrameIx - } }; - - m_device->blockForSemaphores(waitInfos); // this is not solution, quick wa to not throw validation errors - } - else - --m_realFrameIx; - } - } - - m_surface->present(m_currentImageAcquire.imageIndex, rendered); - } - - numSubmits++; - } - - // - bool keepRunning() override { return numSubmits < MaxNumSubmits; } - -private: - // create pipeline (specialized every test) [TODO: turn into a future/async] - smart_refctd_ptr createPipeline(const IShader* overridenUnspecialized, const IGPUPipelineLayout* layout, const uint8_t subgroupSizeLog2) - { - auto shader = m_device->compileShader({ overridenUnspecialized }); - IGPUComputePipeline::SCreationParams params = {}; - params.layout = layout; - params.shader = { - .shader = shader.get(), - .entryPoint = "main", - .requiredSubgroupSize = static_cast(subgroupSizeLog2), - .entries = nullptr, - }; - params.cached.requireFullSubgroups = true; - if (m_device->getEnabledFeatures().pipelineExecutableInfo) - { - params.flags |= IGPUComputePipeline::SCreationParams::FLAGS::CAPTURE_STATISTICS; - params.flags |= IGPUComputePipeline::SCreationParams::FLAGS::CAPTURE_INTERNAL_REPRESENTATIONS; - } - core::smart_refctd_ptr pipeline; - if (!m_device->createComputePipelines(nullptr,{¶ms,1},&pipeline)) - return nullptr; - - if (m_device->getEnabledFeatures().pipelineExecutableInfo) - { - auto report = system::to_string(pipeline->getExecutableInfo()); - m_logger->log("Arithmetic Bench Pipeline Executable Report:\n%s", ILogger::ELL_PERFORMANCE, report.c_str()); - } - return pipeline; - } - - struct BenchmarkSet - { - smart_refctd_ptr pipeline; - uint32_t workgroupSize; - uint32_t itemsPerInvocation; - }; - - template - BenchmarkSet createBenchmarkPipelines(const smart_refctd_ptr&source, const IGPUPipelineLayout* layout, const uint32_t elementCount, const std::string& arith_name, const uint8_t subgroupSizeLog2, const uint32_t workgroupSize, uint32_t itemsPerInvoc = 1u, uint32_t numLoops = 8u) - { - auto compiler = make_smart_refctd_ptr(smart_refctd_ptr(m_system)); - CHLSLCompiler::SOptions options = {}; - options.stage = IShader::E_SHADER_STAGE::ESS_COMPUTE; - options.preprocessorOptions.targetSpirvVersion = m_device->getPhysicalDevice()->getLimits().spirvVersion; - options.spirvOptimizer = nullptr; -#ifndef _NBL_DEBUG - ISPIRVOptimizer::E_OPTIMIZER_PASS optPasses = ISPIRVOptimizer::EOP_STRIP_DEBUG_INFO; - auto opt = make_smart_refctd_ptr(std::span(&optPasses, 1)); - options.spirvOptimizer = opt.get(); -#else - options.debugInfoFlags |= IShaderCompiler::E_DEBUG_INFO_FLAGS::EDIF_LINE_BIT; -#endif - options.preprocessorOptions.sourceIdentifier = source->getFilepathHint(); - options.preprocessorOptions.logger = m_logger.get(); - - auto* includeFinder = compiler->getDefaultIncludeFinder(); - options.preprocessorOptions.includeFinder = includeFinder; - - const uint32_t subgroupSize = 0x1u << subgroupSizeLog2; - const uint32_t workgroupSizeLog2 = hlsl::findMSB(workgroupSize); - hlsl::workgroup2::SArithmeticConfiguration wgConfig; - wgConfig.init(workgroupSizeLog2, subgroupSizeLog2, itemsPerInvoc); - const uint32_t itemsPerWG = wgConfig.VirtualWorkgroupSize * wgConfig.ItemsPerInvocation_0; - smart_refctd_ptr overriddenUnspecialized; - if constexpr (WorkgroupBench) - { - const std::string definitions[4] = { - "workgroup2::" + arith_name, - wgConfig.getConfigTemplateStructString(), - std::to_string(numLoops), - std::to_string(arith_name=="reduction") - }; - - const IShaderCompiler::SMacroDefinition defines[5] = { - { "OPERATION", definitions[0] }, - { "WORKGROUP_CONFIG_T", definitions[1] }, - { "NUM_LOOPS", definitions[2] }, - { "IS_REDUCTION", definitions[3] }, - { "TEST_NATIVE", "1" } - }; - if (UseNativeArithmetic) - options.preprocessorOptions.extraDefines = { defines, defines + 5 }; - else - options.preprocessorOptions.extraDefines = { defines, defines + 4 }; - - overriddenUnspecialized = compiler->compileToSPIRV((const char*)source->getContent()->getPointer(), options); - } - else - { - hlsl::subgroup2::SArithmeticParams sgParams; - sgParams.init(subgroupSizeLog2, itemsPerInvoc); - - const std::string definitions[4] = { - "subgroup2::" + arith_name, - std::to_string(workgroupSize), - sgParams.getParamTemplateStructString(), - std::to_string(numLoops) - }; - - const IShaderCompiler::SMacroDefinition defines[5] = { - { "OPERATION", definitions[0] }, - { "WORKGROUP_SIZE", definitions[1] }, - { "SUBGROUP_CONFIG_T", definitions[2] }, - { "NUM_LOOPS", definitions[3] }, - { "TEST_NATIVE", "1" } - }; - if (UseNativeArithmetic) - options.preprocessorOptions.extraDefines = { defines, defines + 5 }; - else - options.preprocessorOptions.extraDefines = { defines, defines + 4 }; - - overriddenUnspecialized = compiler->compileToSPIRV((const char*)source->getContent()->getPointer(), options); - } - - BenchmarkSet set; - set.pipeline = createPipeline(overriddenUnspecialized.get(), layout, subgroupSizeLog2); - if constexpr (WorkgroupBench) - { - set.workgroupSize = itemsPerWG; - } - else - { - set.workgroupSize = workgroupSize; - } - set.itemsPerInvocation = itemsPerInvoc; - - return set; - }; - - template - void runBenchmark(IGPUCommandBuffer* cmdbuf, const BenchmarkSet& set, const uint32_t elementCount, const uint8_t subgroupSizeLog2) - { - uint32_t workgroupCount; - if constexpr (WorkgroupBench) - workgroupCount = elementCount / set.workgroupSize; - else - workgroupCount = elementCount / (set.workgroupSize * set.itemsPerInvocation); - - cmdbuf->bindComputePipeline(set.pipeline.get()); - cmdbuf->dispatch(workgroupCount, 1, 1); - { - IGPUCommandBuffer::SPipelineBarrierDependencyInfo::buffer_barrier_t memoryBarrier[OutputBufferCount]; - for (auto i = 0u; i < OutputBufferCount; i++) - { - memoryBarrier[i] = { - .barrier = { - .dep = { - .srcStageMask = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT, - .srcAccessMask = ACCESS_FLAGS::SHADER_WRITE_BITS, - // in theory we don't need the HOST BITS cause we block on a semaphore but might as well add them - .dstStageMask = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT | PIPELINE_STAGE_FLAGS::HOST_BIT, - .dstAccessMask = ACCESS_FLAGS::SHADER_WRITE_BITS | ACCESS_FLAGS::HOST_READ_BIT - } - }, - .range = {0ull,outputBuffers[i]->getSize(),outputBuffers[i]} - }; - } - IGPUCommandBuffer::SPipelineBarrierDependencyInfo info = { .memBarriers = {},.bufBarriers = memoryBarrier }; - cmdbuf->pipelineBarrier(asset::E_DEPENDENCY_FLAGS::EDF_NONE, info); - } - } - - IQueue* transferDownQueue; - IQueue* computeQueue; - - smart_refctd_ptr m_window; - smart_refctd_ptr> m_surface; - smart_refctd_ptr m_semaphore; - uint64_t m_realFrameIx = 0; - std::array, MaxFramesInFlight> m_cmdBufs; - ISimpleManagedSurface::SAcquireResult m_currentImageAcquire = {}; - - smart_refctd_ptr m_inputSystem; - - std::array, ISwapchain::MaxImages> swapchainImageViews; - - constexpr static inline uint32_t MaxNumSubmits = 30; - uint32_t numSubmits = 0; - constexpr static inline uint32_t ElementCount = 1024 * 1024; - - /* PARAMETERS TO CHANGE FOR DIFFERENT BENCHMARKS */ - constexpr static inline bool DoWorkgroupBenchmarks = true; - constexpr static inline bool UseNativeArithmetic = true; - uint32_t ItemsPerInvocation = 4u; - constexpr static inline uint32_t NumLoops = 1000u; - constexpr static inline uint32_t NumBenchmarks = 6u; - std::array workgroupSizes = { 32, 64, 128, 256, 512, 1024 }; - std::array arithmeticOperations = { "reduction", "inclusive_scan", "exclusive_scan" }; - - - std::array benchSets; - smart_refctd_ptr benchPool; - std::array, ISwapchain::MaxImages> benchDs; - - constexpr static inline uint32_t OutputBufferCount = 2u; - smart_refctd_ptr outputBuffers[OutputBufferCount]; - smart_refctd_ptr gpuOutputAddressesBuffer; - PushConstantData pc; - - uint64_t timelineValue = 0; -}; - -NBL_MAIN_FUNC(ArithmeticBenchApp) \ No newline at end of file diff --git a/29_SpecializationConstants/CMakeLists.txt b/29_SpecializationConstants/CMakeLists.txt new file mode 100644 index 000000000..a476b6203 --- /dev/null +++ b/29_SpecializationConstants/CMakeLists.txt @@ -0,0 +1,7 @@ + +include(common RESULT_VARIABLE RES) +if(NOT RES) + message(FATAL_ERROR "common.cmake not found. Should be in {repo_root}/cmake directory") +endif() + +nbl_create_executable_project("" "" "" "" "${NBL_EXECUTABLE_PROJECT_CREATION_PCH_TARGET}") \ No newline at end of file diff --git a/16_ZipArchiveLoaderTest/config.json.template b/29_SpecializationConstants/config.json.template similarity index 99% rename from 16_ZipArchiveLoaderTest/config.json.template rename to 29_SpecializationConstants/config.json.template index 24adf54fb..f961745c1 100644 --- a/16_ZipArchiveLoaderTest/config.json.template +++ b/29_SpecializationConstants/config.json.template @@ -25,4 +25,4 @@ "outputs": [] } ] -} +} \ No newline at end of file diff --git a/29_SpecializationConstants/main.cpp b/29_SpecializationConstants/main.cpp new file mode 100644 index 000000000..970225b7f --- /dev/null +++ b/29_SpecializationConstants/main.cpp @@ -0,0 +1,566 @@ +// 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 + +#define _NBL_STATIC_LIB_ +#include + +#include "../common/CommonAPI.h" +using namespace nbl; +using namespace core; +using namespace ui; + +struct UBOCompute +{ + //xyz - gravity point, w - dt + core::vectorSIMDf gravPointAndDt; +}; + +class SpecializationConstantsSampleApp : public ApplicationBase +{ + constexpr static uint32_t WIN_W = 1280u; + constexpr static uint32_t WIN_H = 720u; + constexpr static uint32_t SC_IMG_COUNT = 3u; + constexpr static uint32_t FRAMES_IN_FLIGHT = 5u; + static constexpr uint64_t MAX_TIMEOUT = 99999999999999ull; + static_assert(FRAMES_IN_FLIGHT > SC_IMG_COUNT); + + core::smart_refctd_ptr window; + core::smart_refctd_ptr system; + core::smart_refctd_ptr windowCb; + core::smart_refctd_ptr api; + core::smart_refctd_ptr surface; + core::smart_refctd_ptr utils; + core::smart_refctd_ptr device; + video::IPhysicalDevice* gpu; + std::array queues; + core::smart_refctd_ptr swapchain; + core::smart_refctd_ptr renderpass; + nbl::core::smart_refctd_dynamic_array> fbo; + std::array, CommonAPI::InitOutput::MaxFramesInFlight>, CommonAPI::InitOutput::MaxQueuesCount> commandPools; + core::smart_refctd_ptr filesystem; + core::smart_refctd_ptr assetManager; + video::IGPUObjectFromAssetConverter::SParams cpu2gpuParams; + core::smart_refctd_ptr logger; + core::smart_refctd_ptr inputSystem; + video::IGPUObjectFromAssetConverter cpu2gpu; + + constexpr static uint32_t COMPUTE_SET = 0u; + constexpr static uint32_t PARTICLE_BUF_BINDING = 0u; + constexpr static uint32_t COMPUTE_DATA_UBO_BINDING = 1u; + constexpr static uint32_t WORKGROUP_SIZE = 256u; + constexpr static uint32_t PARTICLE_COUNT = 1u << 21; + constexpr static uint32_t PARTICLE_COUNT_PER_AXIS = 1u << 7; + constexpr static uint32_t POS_BUF_IX = 0u; + constexpr static uint32_t VEL_BUF_IX = 1u; + constexpr static uint32_t BUF_COUNT = 2u; + constexpr static uint32_t GRAPHICS_SET = 0u; + constexpr static uint32_t GRAPHICS_DATA_UBO_BINDING = 0u; + + std::chrono::high_resolution_clock::time_point m_lastTime; + int32_t m_resourceIx = -1; + core::smart_refctd_ptr m_cmdbuf[FRAMES_IN_FLIGHT]; + core::smart_refctd_ptr m_frameComplete[FRAMES_IN_FLIGHT] = { nullptr }; + core::smart_refctd_ptr m_imageAcquire[FRAMES_IN_FLIGHT] = { nullptr }; + core::smart_refctd_ptr m_renderFinished[FRAMES_IN_FLIGHT] = { nullptr }; + core::vectorSIMDf m_cameraPosition; + core::vectorSIMDf m_camFront; + UBOCompute m_uboComputeData; + asset::SBufferRange m_computeUBORange; + asset::SBufferRange m_graphicsUBORange; + core::smart_refctd_ptr m_gpuComputePipeline; + core::smart_refctd_ptr m_graphicsPipeline; + core::smart_refctd_ptr m_gpuds0Compute; + core::smart_refctd_ptr m_gpuds0Graphics; + asset::SBasicViewParameters m_viewParams; + core::matrix4SIMD m_viewProj; + core::smart_refctd_ptr m_gpuParticleBuf; + core::smart_refctd_ptr m_rpIndependentPipeline; + nbl::video::ISwapchain::SCreationParams m_swapchainCreationParams; + +public: + + void setWindow(core::smart_refctd_ptr&& wnd) override + { + window = std::move(wnd); + } + void setSystem(core::smart_refctd_ptr&& s) override + { + system = std::move(s); + } + nbl::ui::IWindow* getWindow() override + { + return window.get(); + } + video::IAPIConnection* getAPIConnection() override + { + return api.get(); + } + video::ILogicalDevice* getLogicalDevice() override + { + return device.get(); + } + video::IGPURenderpass* getRenderpass() override + { + return renderpass.get(); + } + void setSurface(core::smart_refctd_ptr&& s) override + { + surface = std::move(s); + } + void setFBOs(std::vector>& f) override + { + for (int i = 0; i < f.size(); i++) + { + fbo->begin()[i] = core::smart_refctd_ptr(f[i]); + } + } + void setSwapchain(core::smart_refctd_ptr&& s) override + { + swapchain = std::move(s); + } + uint32_t getSwapchainImageCount() override + { + return swapchain->getImageCount(); + } + virtual nbl::asset::E_FORMAT getDepthFormat() override + { + return nbl::asset::EF_UNKNOWN; + } + + APP_CONSTRUCTOR(SpecializationConstantsSampleApp); + + void onAppInitialized_impl() override + { + const auto swapchainImageUsage = static_cast(asset::IImage::EUF_COLOR_ATTACHMENT_BIT | asset::IImage::EUF_STORAGE_BIT); + const asset::E_FORMAT depthFormat = asset::EF_UNKNOWN; + CommonAPI::InitParams initParams; + initParams.window = core::smart_refctd_ptr(window); + initParams.apiType = video::EAT_VULKAN; + initParams.appName = { _NBL_APP_NAME_ }; + initParams.framesInFlight = FRAMES_IN_FLIGHT; + initParams.windowWidth = WIN_W; + initParams.windowHeight = WIN_H; + initParams.swapchainImageCount = SC_IMG_COUNT; + initParams.swapchainImageUsage = swapchainImageUsage; + initParams.depthFormat = depthFormat; + initParams.physicalDeviceFilter.minimumLimits.workgroupSizeFromSpecConstant = true; + auto initOutp = CommonAPI::InitWithDefaultExt(std::move(initParams)); + + window = std::move(initParams.window); + system = std::move(initOutp.system); + windowCb = std::move(initParams.windowCb); + api = std::move(initOutp.apiConnection); + surface = std::move(initOutp.surface); + device = std::move(initOutp.logicalDevice); + gpu = std::move(initOutp.physicalDevice); + queues = std::move(initOutp.queues); + renderpass = std::move(initOutp.renderToSwapchainRenderpass); + commandPools = std::move(initOutp.commandPools); + assetManager = std::move(initOutp.assetManager); + filesystem = std::move(initOutp.system); + cpu2gpuParams = std::move(initOutp.cpu2gpuParams); + utils = std::move(initOutp.utilities); + m_swapchainCreationParams = std::move(initOutp.swapchainCreationParams); + + CommonAPI::createSwapchain(std::move(device), m_swapchainCreationParams, WIN_W, WIN_H, swapchain); + assert(swapchain); + fbo = CommonAPI::createFBOWithSwapchainImages( + swapchain->getImageCount(), WIN_W, WIN_H, + device, swapchain, renderpass, + depthFormat + ); + + video::IGPUObjectFromAssetConverter CPU2GPU; + m_cameraPosition = core::vectorSIMDf(0, 0, -10); + matrix4SIMD proj = matrix4SIMD::buildProjectionMatrixPerspectiveFovRH(core::radians(90.0f), video::ISurface::getTransformedAspectRatio(swapchain->getPreTransform(), WIN_W, WIN_H), 0.01, 100); + matrix3x4SIMD view = matrix3x4SIMD::buildCameraLookAtMatrixRH(m_cameraPosition, core::vectorSIMDf(0, 0, 0), core::vectorSIMDf(0, 1, 0)); + m_viewProj = matrix4SIMD::concatenateBFollowedByAPrecisely( + video::ISurface::getSurfaceTransformationMatrix(swapchain->getPreTransform()), + matrix4SIMD::concatenateBFollowedByA(proj, matrix4SIMD(view)) + ); + m_camFront = view[2]; + + // auto glslExts = device->getSupportedGLSLExtensions(); + asset::CSPIRVIntrospector introspector; + + const char* pathToCompShader = "../particles.comp"; + auto compilerSet = assetManager->getCompilerSet(); + core::smart_refctd_ptr computeUnspec = nullptr; + core::smart_refctd_ptr computeUnspecSPIRV = nullptr; + { + auto csBundle = assetManager->getAsset(pathToCompShader, {}); + auto csContents = csBundle.getContents(); + if (csContents.empty()) + assert(false); + + asset::ICPUSpecializedShader* csSpec = static_cast(csContents.begin()->get()); + computeUnspec = core::smart_refctd_ptr(csSpec->getUnspecialized()); + + auto compiler = compilerSet->getShaderCompiler(computeUnspec->getContentType()); + + asset::IShaderCompiler::SPreprocessorOptions preprocessOptions = {}; + preprocessOptions.sourceIdentifier = pathToCompShader; + preprocessOptions.includeFinder = compiler->getDefaultIncludeFinder(); + computeUnspec = compilerSet->preprocessShader(computeUnspec.get(), preprocessOptions); + } + + core::smart_refctd_ptr introspection = nullptr; + { + //! This example first preprocesses and then compiles the shader, although it could've been done by calling compileToSPIRV with setting compilerOptions.preprocessorOptions + asset::IShaderCompiler::SCompilerOptions compilerOptions = {}; + // compilerOptions.entryPoint = "main"; + compilerOptions.stage = computeUnspec->getStage(); + compilerOptions.debugInfoFlags = asset::IShaderCompiler::E_DEBUG_INFO_FLAGS::EDIF_SOURCE_BIT; // should be DIF_SOURCE_BIT for introspection + compilerOptions.preprocessorOptions.sourceIdentifier = computeUnspec->getFilepathHint(); // already preprocessed but for logging it's best to fill sourceIdentifier + computeUnspecSPIRV = compilerSet->compileToSPIRV(computeUnspec.get(), compilerOptions); + + asset::CSPIRVIntrospector::SIntrospectionParams params = { "main", computeUnspecSPIRV }; + introspection = introspector.introspect(params); + } + + asset::ISpecializedShader::SInfo specInfo; + { + struct SpecConstants + { + int32_t wg_size; + int32_t particle_count; + int32_t pos_buf_ix; + int32_t vel_buf_ix; + int32_t buf_count; + }; + SpecConstants swapchain{ WORKGROUP_SIZE, PARTICLE_COUNT, POS_BUF_IX, VEL_BUF_IX, BUF_COUNT }; + + auto it_particleBufDescIntro = std::find_if(introspection->descriptorSetBindings[COMPUTE_SET].begin(), introspection->descriptorSetBindings[COMPUTE_SET].end(), + [=](auto b) { return b.binding == PARTICLE_BUF_BINDING; } + ); + assert(it_particleBufDescIntro->descCountIsSpecConstant); + const uint32_t buf_count_specID = it_particleBufDescIntro->count_specID; + auto& particleDataArrayIntro = it_particleBufDescIntro->get().members.array[0]; + assert(particleDataArrayIntro.countIsSpecConstant); + const uint32_t particle_count_specID = particleDataArrayIntro.count_specID; + + auto backbuf = core::make_smart_refctd_ptr(sizeof(swapchain)); + memcpy(backbuf->getPointer(), &swapchain, sizeof(swapchain)); + auto entries = core::make_refctd_dynamic_array>(5u); + (*entries)[0] = { 0u,offsetof(SpecConstants,wg_size),sizeof(int32_t) };//currently local_size_{x|y|z}_id is not queryable via introspection API + (*entries)[1] = { particle_count_specID,offsetof(SpecConstants,particle_count),sizeof(int32_t) }; + (*entries)[2] = { 2u,offsetof(SpecConstants,pos_buf_ix),sizeof(int32_t) }; + (*entries)[3] = { 3u,offsetof(SpecConstants,vel_buf_ix),sizeof(int32_t) }; + (*entries)[4] = { buf_count_specID,offsetof(SpecConstants,buf_count),sizeof(int32_t) }; + + specInfo = asset::ISpecializedShader::SInfo(std::move(entries), std::move(backbuf), "main"); + } + + auto compute = core::make_smart_refctd_ptr(std::move(computeUnspecSPIRV), std::move(specInfo)); + + auto computePipeline = introspector.createApproximateComputePipelineFromIntrospection(compute.get()); + auto computeLayout = core::make_smart_refctd_ptr(nullptr, nullptr, core::smart_refctd_ptr(computePipeline->getLayout()->getDescriptorSetLayout(0))); + computePipeline->setLayout(core::smart_refctd_ptr(computeLayout)); + + // These conversions don't require command buffers + m_gpuComputePipeline = CPU2GPU.getGPUObjectsFromAssets(&computePipeline.get(), &computePipeline.get() + 1, cpu2gpuParams)->front(); + auto* ds0layoutCompute = computeLayout->getDescriptorSetLayout(0); + core::smart_refctd_ptr gpuDs0layoutCompute = CPU2GPU.getGPUObjectsFromAssets(&ds0layoutCompute, &ds0layoutCompute + 1, cpu2gpuParams)->front(); + + core::vector particlePosAndVel; + particlePosAndVel.reserve(PARTICLE_COUNT * 2); + for (int32_t i = 0; i < PARTICLE_COUNT_PER_AXIS; ++i) + for (int32_t j = 0; j < PARTICLE_COUNT_PER_AXIS; ++j) + for (int32_t k = 0; k < PARTICLE_COUNT_PER_AXIS; ++k) + particlePosAndVel.push_back(core::vector3df_SIMD(i, j, k) * 0.5f); + + for (int32_t i = 0; i < PARTICLE_COUNT; ++i) + particlePosAndVel.push_back(core::vector3df_SIMD(0.0f)); + + constexpr size_t BUF_SZ = 4ull * sizeof(float) * PARTICLE_COUNT; + video::IGPUBuffer::SCreationParams bufferCreationParams = {}; + bufferCreationParams.usage = static_cast(asset::IBuffer::EUF_TRANSFER_DST_BIT | asset::IBuffer::EUF_STORAGE_BUFFER_BIT | asset::IBuffer::EUF_VERTEX_BUFFER_BIT); + bufferCreationParams.size = 2ull * BUF_SZ; + m_gpuParticleBuf = device->createBuffer(std::move(bufferCreationParams)); + m_gpuParticleBuf->setObjectDebugName("m_gpuParticleBuf"); + auto particleBufMemReqs = m_gpuParticleBuf->getMemoryReqs(); + particleBufMemReqs.memoryTypeBits &= device->getPhysicalDevice()->getDeviceLocalMemoryTypeBits(); + device->allocate(particleBufMemReqs, m_gpuParticleBuf.get()); + asset::SBufferRange range; + range.buffer = m_gpuParticleBuf; + range.offset = 0ull; + range.size = BUF_SZ * 2ull; + utils->updateBufferRangeViaStagingBufferAutoSubmit(range, particlePosAndVel.data(), queues[CommonAPI::InitOutput::EQT_GRAPHICS]); + particlePosAndVel.clear(); + + video::IGPUBuffer::SCreationParams uboComputeCreationParams = {}; + uboComputeCreationParams.usage = static_cast(asset::IBuffer::EUF_UNIFORM_BUFFER_BIT | asset::IBuffer::EUF_TRANSFER_DST_BIT | asset::IBuffer::EUF_INLINE_UPDATE_VIA_CMDBUF); + uboComputeCreationParams.size = core::roundUp(sizeof(UBOCompute), 64ull); + auto gpuUboCompute = device->createBuffer(std::move(uboComputeCreationParams)); + auto gpuUboComputeMemReqs = gpuUboCompute->getMemoryReqs(); + gpuUboComputeMemReqs.memoryTypeBits &= device->getPhysicalDevice()->getDeviceLocalMemoryTypeBits(); + device->allocate(gpuUboComputeMemReqs, gpuUboCompute.get()); + + asset::SBufferBinding vtxBindings[video::IGPUMeshBuffer::MAX_ATTR_BUF_BINDING_COUNT]; + vtxBindings[0].buffer = m_gpuParticleBuf; + vtxBindings[0].offset = 0u; + //auto meshbuffer = core::make_smart_refctd_ptr(nullptr, nullptr, vtxBindings, asset::SBufferBinding{}); + //meshbuffer->setIndexCount(PARTICLE_COUNT); + //meshbuffer->setIndexType(asset::EIT_UNKNOWN); + + + auto createSpecShader = [&](const char* filepath, asset::IShader::E_SHADER_STAGE stage) + { + auto shaderBundle = assetManager->getAsset(filepath, {}); + auto shaderContents = shaderBundle.getContents(); + if (shaderContents.empty()) + assert(false); + + auto specializedShader = static_cast(shaderContents.begin()->get()); + auto unspecShader = specializedShader->getUnspecialized(); + + auto compiler = compilerSet->getShaderCompiler(computeUnspec->getContentType()); + asset::IShaderCompiler::SCompilerOptions compilerOptions = {}; + // compilerOptions.entryPoint = specializedShader->getSpecializationInfo().entryPoint; + compilerOptions.stage = unspecShader->getStage(); + compilerOptions.debugInfoFlags = asset::IShaderCompiler::E_DEBUG_INFO_FLAGS::EDIF_SOURCE_BIT; + compilerOptions.preprocessorOptions.sourceIdentifier = unspecShader->getFilepathHint(); // already preprocessed but for logging it's best to fill sourceIdentifier + compilerOptions.preprocessorOptions.includeFinder = compiler->getDefaultIncludeFinder(); + auto unspecSPIRV = compilerSet->compileToSPIRV(unspecShader, compilerOptions); + + return core::make_smart_refctd_ptr(std::move(unspecSPIRV), asset::ISpecializedShader::SInfo(specializedShader->getSpecializationInfo())); + }; + auto vs = createSpecShader("../particles.vert", asset::IShader::ESS_VERTEX); + auto fs = createSpecShader("../particles.frag", asset::IShader::ESS_FRAGMENT); + + asset::ICPUSpecializedShader* shaders[2] = { vs.get(),fs.get() }; + auto pipeline = introspector.createApproximateRenderpassIndependentPipelineFromIntrospection({ shaders, shaders + 2 }); + { + auto& vtxParams = pipeline->getVertexInputParams(); + vtxParams.attributes[0].binding = 0u; + vtxParams.attributes[0].format = asset::EF_R32G32B32_SFLOAT; + vtxParams.attributes[0].relativeOffset = 0u; + vtxParams.bindings[0].inputRate = asset::EVIR_PER_VERTEX; + vtxParams.bindings[0].stride = 4u * sizeof(float); + + pipeline->getPrimitiveAssemblyParams().primitiveType = asset::EPT_POINT_LIST; + + auto& blendParams = pipeline->getBlendParams(); + blendParams.logicOpEnable = false; + blendParams.logicOp = nbl::asset::ELO_NO_OP; + } + auto gfxLayout = core::make_smart_refctd_ptr(nullptr, nullptr, core::smart_refctd_ptr(pipeline->getLayout()->getDescriptorSetLayout(0))); + pipeline->setLayout(core::smart_refctd_ptr(gfxLayout)); + + m_rpIndependentPipeline = CPU2GPU.getGPUObjectsFromAssets(&pipeline.get(), &pipeline.get() + 1, cpu2gpuParams)->front(); + auto* ds0layoutGraphics = gfxLayout->getDescriptorSetLayout(0); + core::smart_refctd_ptr gpuDs0layoutGraphics = CPU2GPU.getGPUObjectsFromAssets(&ds0layoutGraphics, &ds0layoutGraphics + 1, cpu2gpuParams)->front(); + + video::IGPUDescriptorSetLayout* gpuDSLayouts_raw[2] = { gpuDs0layoutCompute.get(), gpuDs0layoutGraphics.get() }; + const uint32_t setCount[2] = { 1u, 1u }; + auto dscPool = device->createDescriptorPoolForDSLayouts(video::IDescriptorPool::ECF_NONE, gpuDSLayouts_raw, gpuDSLayouts_raw + 2ull, setCount); + + m_gpuds0Compute = dscPool->createDescriptorSet(std::move(gpuDs0layoutCompute)); + { + video::IGPUDescriptorSet::SDescriptorInfo i[3]; + video::IGPUDescriptorSet::SWriteDescriptorSet w[2]; + w[0].arrayElement = 0u; + w[0].binding = PARTICLE_BUF_BINDING; + w[0].count = BUF_COUNT; + w[0].descriptorType = asset::IDescriptor::E_TYPE::ET_STORAGE_BUFFER; + w[0].dstSet = m_gpuds0Compute.get(); + w[0].info = i; + w[1].arrayElement = 0u; + w[1].binding = COMPUTE_DATA_UBO_BINDING; + w[1].count = 1u; + w[1].descriptorType = asset::IDescriptor::E_TYPE::ET_UNIFORM_BUFFER; + w[1].dstSet = m_gpuds0Compute.get(); + w[1].info = i + 2u; + i[0].desc = m_gpuParticleBuf; + i[0].info.buffer.offset = 0ull; + i[0].info.buffer.size = BUF_SZ; + i[1].desc = m_gpuParticleBuf; + i[1].info.buffer.offset = BUF_SZ; + i[1].info.buffer.size = BUF_SZ; + i[2].desc = gpuUboCompute; + i[2].info.buffer.offset = 0ull; + i[2].info.buffer.size = gpuUboCompute->getSize(); + + device->updateDescriptorSets(2u, w, 0u, nullptr); + } + + + m_gpuds0Graphics = dscPool->createDescriptorSet(std::move(gpuDs0layoutGraphics)); + + video::IGPUGraphicsPipeline::SCreationParams gp_params; + gp_params.rasterizationSamples = asset::IImage::ESCF_1_BIT; + gp_params.renderpass = core::smart_refctd_ptr(renderpass); + gp_params.renderpassIndependent = core::smart_refctd_ptr(m_rpIndependentPipeline); + gp_params.subpassIx = 0u; + + m_graphicsPipeline = device->createGraphicsPipeline(nullptr, std::move(gp_params)); + + video::IGPUBuffer::SCreationParams gfxUboCreationParams = {}; + gfxUboCreationParams.usage = static_cast(asset::IBuffer::EUF_UNIFORM_BUFFER_BIT | asset::IBuffer::EUF_TRANSFER_DST_BIT | asset::IBuffer::EUF_INLINE_UPDATE_VIA_CMDBUF); + gfxUboCreationParams.size = sizeof(m_viewParams); + auto gpuUboGraphics = device->createBuffer(std::move(gfxUboCreationParams)); + auto gpuUboGraphicsMemReqs = gpuUboGraphics->getMemoryReqs(); + gpuUboGraphicsMemReqs.memoryTypeBits &= device->getPhysicalDevice()->getDeviceLocalMemoryTypeBits(); + + device->allocate(gpuUboGraphicsMemReqs, gpuUboGraphics.get()); + { + video::IGPUDescriptorSet::SWriteDescriptorSet w; + video::IGPUDescriptorSet::SDescriptorInfo i; + w.arrayElement = 0u; + w.binding = GRAPHICS_DATA_UBO_BINDING; + w.count = 1u; + w.descriptorType = asset::IDescriptor::E_TYPE::ET_UNIFORM_BUFFER; + w.dstSet = m_gpuds0Graphics.get(); + w.info = &i; + i.desc = gpuUboGraphics; + i.info.buffer.offset = 0u; + i.info.buffer.size = gpuUboGraphics->getSize(); // gpuUboGraphics->getSize(); + + device->updateDescriptorSets(1u, &w, 0u, nullptr); + } + + m_lastTime = std::chrono::high_resolution_clock::now(); + constexpr uint32_t FRAME_COUNT = 500000u; + constexpr uint64_t MAX_TIMEOUT = 99999999999999ull; + m_computeUBORange = { 0, gpuUboCompute->getSize(), gpuUboCompute }; + m_graphicsUBORange = { 0, gpuUboGraphics->getSize(), gpuUboGraphics }; + + const auto& graphicsCommandPools = commandPools[CommonAPI::InitOutput::EQT_GRAPHICS]; + for (uint32_t i = 0u; i < FRAMES_IN_FLIGHT; i++) + { + device->createCommandBuffers(graphicsCommandPools[i].get(), video::IGPUCommandBuffer::EL_PRIMARY, 1, m_cmdbuf+i); + m_imageAcquire[i] = device->createSemaphore(); + m_renderFinished[i] = device->createSemaphore(); + } + } + + void onAppTerminated_impl() override + { + device->waitIdle(); + } + + void workLoopBody() override + { + m_resourceIx++; + if (m_resourceIx >= FRAMES_IN_FLIGHT) + m_resourceIx = 0; + + auto& cb = m_cmdbuf[m_resourceIx]; + auto& fence = m_frameComplete[m_resourceIx]; + if (fence) + { + auto retval = device->waitForFences(1u, &fence.get(), false, MAX_TIMEOUT); + assert(retval == video::IGPUFence::ES_TIMEOUT || retval == video::IGPUFence::ES_SUCCESS); + device->resetFences(1u, &fence.get()); + } + else + { + fence = device->createFence(static_cast(0)); + } + + // safe to proceed + cb->begin(video::IGPUCommandBuffer::EU_ONE_TIME_SUBMIT_BIT); // TODO: Reset Frame's CommandPool + + { + auto time = std::chrono::high_resolution_clock::now(); + core::vector3df_SIMD gravPoint = m_cameraPosition + m_camFront * 250.f; + m_uboComputeData.gravPointAndDt = gravPoint; + m_uboComputeData.gravPointAndDt.w = std::chrono::duration_cast(time - m_lastTime).count() * 1e-4; + + m_lastTime = time; + cb->updateBuffer(m_computeUBORange.buffer.get(), m_computeUBORange.offset, m_computeUBORange.size, &m_uboComputeData); + } + cb->bindComputePipeline(m_gpuComputePipeline.get()); + cb->bindDescriptorSets(asset::EPBP_COMPUTE, + m_gpuComputePipeline->getLayout(), + COMPUTE_SET, + 1u, + &m_gpuds0Compute.get(), + 0u); + cb->dispatch(PARTICLE_COUNT / WORKGROUP_SIZE, 1u, 1u); + + asset::SMemoryBarrier memBarrier; + memBarrier.srcAccessMask = asset::EAF_SHADER_WRITE_BIT; + memBarrier.dstAccessMask = asset::EAF_VERTEX_ATTRIBUTE_READ_BIT; + cb->pipelineBarrier( + asset::EPSF_COMPUTE_SHADER_BIT, + asset::EPSF_VERTEX_INPUT_BIT, + static_cast(0u), + 1, &memBarrier, + 0, nullptr, + 0, nullptr); + + { + memcpy(m_viewParams.MVP, &m_viewProj, sizeof(m_viewProj)); + cb->updateBuffer(m_graphicsUBORange.buffer.get(), m_graphicsUBORange.offset, m_graphicsUBORange.size, &m_viewParams); + } + { + asset::SViewport vp; + vp.minDepth = 1.f; + vp.maxDepth = 0.f; + vp.x = 0u; + vp.y = 0u; + vp.width = WIN_W; + vp.height = WIN_H; + cb->setViewport(0u, 1u, &vp); + + VkRect2D scissor; + scissor.offset = { 0, 0 }; + scissor.extent = { WIN_W, WIN_H }; + cb->setScissor(0u, 1u, &scissor); + } + // renderpass + uint32_t imgnum = 0u; + swapchain->acquireNextImage(MAX_TIMEOUT, m_imageAcquire[m_resourceIx].get(), nullptr, &imgnum); + { + video::IGPUCommandBuffer::SRenderpassBeginInfo info; + asset::SClearValue clear; + clear.color.float32[0] = 0.f; + clear.color.float32[1] = 0.f; + clear.color.float32[2] = 0.f; + clear.color.float32[3] = 1.f; + info.renderpass = renderpass; + info.framebuffer = fbo->begin()[imgnum]; + info.clearValueCount = 1u; + info.clearValues = &clear; + info.renderArea.offset = { 0, 0 }; + info.renderArea.extent = { WIN_W, WIN_H }; + cb->beginRenderPass(&info, asset::ESC_INLINE); + } + // individual draw + { + cb->bindGraphicsPipeline(m_graphicsPipeline.get()); + size_t vbOffset = 0; + cb->bindVertexBuffers(0, 1, &m_gpuParticleBuf.get(), &vbOffset); + cb->bindDescriptorSets(asset::EPBP_GRAPHICS, m_rpIndependentPipeline->getLayout(), GRAPHICS_SET, 1u, &m_gpuds0Graphics.get(), 0u); + cb->draw(PARTICLE_COUNT, 1, 0, 0); + } + cb->endRenderPass(); + cb->end(); + + CommonAPI::Submit( + device.get(), + cb.get(), + queues[CommonAPI::InitOutput::EQT_GRAPHICS], + m_imageAcquire[m_resourceIx].get(), + m_renderFinished[m_resourceIx].get(), + fence.get()); + + CommonAPI::Present( + device.get(), + swapchain.get(), + queues[CommonAPI::InitOutput::EQT_GRAPHICS], + m_renderFinished[m_resourceIx].get(), + imgnum); + } + + bool keepRunning() override + { + return windowCb->isWindowOpen(); + } +}; + +NBL_COMMON_API_MAIN(SpecializationConstantsSampleApp) + +extern "C" { _declspec(dllexport) DWORD NvOptimusEnablement = 0x00000001; } \ No newline at end of file diff --git a/29_SpecializationConstants/particles.comp b/29_SpecializationConstants/particles.comp new file mode 100644 index 000000000..5889af74c --- /dev/null +++ b/29_SpecializationConstants/particles.comp @@ -0,0 +1,39 @@ +// 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 + +#version 430 core + +layout (constant_id = 1) const int PARTICLE_COUNT = 256; +layout (constant_id = 2) const int POS_BUF_IX = 0; +layout (constant_id = 3) const int VEL_BUF_IX = 1; +layout (constant_id = 4) const int BUF_COUNT = 2; + +layout (local_size_x_id = 0) in; + +layout (set = 0, binding = 0, std430) restrict buffer PARTICLE_DATA +{ + vec3 p[PARTICLE_COUNT]; +} data[BUF_COUNT]; +layout (set = 0, binding = 1, std140) uniform UBO +{ + vec3 gravP; + float dt; +} ubo; + +void main() +{ + uint GID = gl_GlobalInvocationID.x; + + vec3 p = data[POS_BUF_IX].p[GID]; + vec3 v = data[VEL_BUF_IX].p[GID]; + + v *= 1.0 - 0.99*ubo.dt; + float d = distance(ubo.gravP,p); + float a = 10000.0 / max(1.0, 0.01*pow(d,1.5)); + v += (ubo.gravP-p)/d * a * ubo.dt; + p += v*ubo.dt; + + data[POS_BUF_IX].p[GID] = p; + data[VEL_BUF_IX].p[GID] = v; +} \ No newline at end of file diff --git a/29_SpecializationConstants/particles.frag b/29_SpecializationConstants/particles.frag new file mode 100644 index 000000000..c03ba9afc --- /dev/null +++ b/29_SpecializationConstants/particles.frag @@ -0,0 +1,12 @@ +// 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 + +#version 430 core + +layout (location = 0) out vec4 Color; + +void main() +{ + Color = vec4(1.0); +} \ No newline at end of file diff --git a/29_SpecializationConstants/particles.vert b/29_SpecializationConstants/particles.vert new file mode 100644 index 000000000..f87486cac --- /dev/null +++ b/29_SpecializationConstants/particles.vert @@ -0,0 +1,21 @@ +// 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 + +#version 430 core + +layout (location = 0) in vec3 vPos; + +#include +#include + +layout (set = 0, binding = 0, row_major, std140) uniform UBO +{ + nbl_glsl_SBasicViewParameters params; +} CamData; + +void main() +{ + gl_PointSize = 1; + gl_Position = nbl_glsl_pseudoMul4x4with3x1(CamData.params.MVP, vPos); +} \ No newline at end of file diff --git a/29_SpecializationConstants/pipeline.groovy b/29_SpecializationConstants/pipeline.groovy new file mode 100644 index 000000000..d61a3c808 --- /dev/null +++ b/29_SpecializationConstants/pipeline.groovy @@ -0,0 +1,50 @@ +import org.DevshGraphicsProgramming.Agent +import org.DevshGraphicsProgramming.BuilderInfo +import org.DevshGraphicsProgramming.IBuilder + +class CSpecializationConstantsBuilder extends IBuilder +{ + public CSpecializationConstantsBuilder(Agent _agent, _info) + { + super(_agent, _info) + } + + @Override + public boolean prepare(Map axisMapping) + { + return true + } + + @Override + public boolean build(Map axisMapping) + { + IBuilder.CONFIGURATION config = axisMapping.get("CONFIGURATION") + IBuilder.BUILD_TYPE buildType = axisMapping.get("BUILD_TYPE") + + def nameOfBuildDirectory = getNameOfBuildDirectory(buildType) + def nameOfConfig = getNameOfConfig(config) + + agent.execute("cmake --build ${info.rootProjectPath}/${nameOfBuildDirectory}/${info.targetProjectPathRelativeToRoot} --target ${info.targetBaseName} --config ${nameOfConfig} -j12 -v") + + return true + } + + @Override + public boolean test(Map axisMapping) + { + return true + } + + @Override + public boolean install(Map axisMapping) + { + return true + } +} + +def create(Agent _agent, _info) +{ + return new CSpecializationConstantsBuilder(_agent, _info) +} + +return this \ No newline at end of file diff --git a/31_HLSLPathTracer/CMakeLists.txt b/31_HLSLPathTracer/CMakeLists.txt deleted file mode 100644 index 8990714aa..000000000 --- a/31_HLSLPathTracer/CMakeLists.txt +++ /dev/null @@ -1,93 +0,0 @@ -include(common) -include("${CMAKE_CURRENT_SOURCE_DIR}/pt.cmake") -include("${CMAKE_CURRENT_SOURCE_DIR}/pt.variant_ids.cmake") - -if(NBL_BUILD_IMGUI) - # EX31 keeps triangle polygon-method variants as separate precompiled persistent entrypoints. - # This keeps polygon-method choice compile-time and avoids runtime shader switching on this axis. - # On AMD Ryzen 5 5600G with Radeon Graphics (6C/12T), - # a Visual Studio Debug x64 full rebuild of the SPIR-V project completed in about 19.789 s. - set(PT_CACHE_ROOT "pipeline/cache" CACHE STRING - "Relative cache root written to path_tracer.runtime.json in the common bin directory. The runtime resolves this path relative to the JSON file location. Empty disables the generated dev-mode JSON and falls back to --pipeline-cache-dir or LocalAppData." - ) - if(IS_ABSOLUTE "${PT_CACHE_ROOT}") - message(FATAL_ERROR "PT_CACHE_ROOT must stay relative because the runtime resolves it against path_tracer.runtime.json") - endif() - - set(NBL_INCLUDE_SERACH_DIRECTORIES - "${CMAKE_CURRENT_SOURCE_DIR}/include" - ) - - list(APPEND NBL_LIBRARIES - imguizmo - "${NBL_EXT_IMGUI_UI_LIB}" - Nabla::ext::FullScreenTriangle - ) - - nbl_create_executable_project("" "" "${NBL_INCLUDE_SERACH_DIRECTORIES}" "${NBL_LIBRARIES}" "${NBL_EXECUTABLE_PROJECT_CREATION_PCH_TARGET}") - add_dependencies(${EXECUTABLE_NAME} argparse) - target_include_directories(${EXECUTABLE_NAME} PUBLIC $) - target_compile_definitions(${EXECUTABLE_NAME} PRIVATE PATH_TRACER_BUILD_CONFIG_NAME=\"$\") - if(NOT PT_CACHE_ROOT STREQUAL "") - string(REPLACE "\\" "/" PT_CACHE_ROOT_JSON "${PT_CACHE_ROOT}") - configure_file( - "${CMAKE_CURRENT_SOURCE_DIR}/path_tracer.runtime.json.in" - "${CMAKE_CURRENT_BINARY_DIR}/path_tracer.runtime.json" - @ONLY - ) - file(GENERATE - OUTPUT "$/path_tracer.runtime.json" - INPUT "${CMAKE_CURRENT_BINARY_DIR}/path_tracer.runtime.json" - ) - endif() - - set(SM 6_8) - set(OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/auto-gen") - set(COMPILE_OPTIONS - -I "${CMAKE_CURRENT_SOURCE_DIR}/../common/include" - -I "${CMAKE_CURRENT_SOURCE_DIR}/include" - -I "${CMAKE_CURRENT_SOURCE_DIR}/app_resources/hlsl" - -isystem "${NBL_ROOT_PATH}/include" # workaround, the same thing like in IES I will address this issue later - $<$:-O1experimental> - $<$:-O1experimental> - -T "lib_${SM}" - -Wno-conversion - -Wno-sign-conversion - -Wno-float-conversion - -Wno-shorten-64-to-32 - -Wno-shadow - -Wno-literal-range - ) - - # Keep the payload flat and explicit here. Once Nabla PR #988 lands, these per-rule compile axes should move to first-class packaged-variant support there. - PT_APPEND_SPIRV_RULE(VAR JSON INPUT "app_resources/hlsl/spirv/pt.compute.sphere.proxy.hlsl" KEY "pt.compute.sphere" COMPILE_OPTIONS "-DPT_VARIANT_USE_RWMC=0" "-DPT_VARIANT_SCENE_KIND=${PT_SCENE_SPHERE}") - PT_APPEND_SPIRV_RULE(VAR JSON INPUT "app_resources/hlsl/spirv/pt.compute.sphere.rwmc.proxy.hlsl" KEY "pt.compute.sphere.rwmc" COMPILE_OPTIONS "-DPT_VARIANT_USE_RWMC=1" "-DPT_VARIANT_SCENE_KIND=${PT_SCENE_SPHERE}") - PT_APPEND_SPIRV_RULE(VAR JSON INPUT "app_resources/hlsl/spirv/pt.compute.triangle.persistent.proxy.hlsl" KEY "pt.compute.triangle.persistent" COMPILE_OPTIONS "-DPT_VARIANT_USE_RWMC=0" "-DPT_VARIANT_ENTRYPOINT_KIND=${PT_ENTRYPOINT_PERSISTENT}") - PT_APPEND_SPIRV_RULE(VAR JSON INPUT "app_resources/hlsl/spirv/pt.compute.triangle.rwmc.persistent.proxy.hlsl" KEY "pt.compute.triangle.rwmc.persistent" COMPILE_OPTIONS "-DPT_VARIANT_USE_RWMC=1" "-DPT_VARIANT_ENTRYPOINT_KIND=${PT_ENTRYPOINT_PERSISTENT}") - PT_APPEND_SPIRV_RULE(VAR JSON INPUT "app_resources/hlsl/spirv/pt.compute.rectangle.rwmc.persistent.proxy.hlsl" KEY "pt.compute.rectangle.rwmc.persistent" COMPILE_OPTIONS "-DPT_VARIANT_USE_RWMC=1" "-DPT_VARIANT_SCENE_KIND=${PT_SCENE_RECTANGLE}") - PT_APPEND_SPIRV_RULE(VAR JSON INPUT "app_resources/hlsl/spirv/pt.compute.rectangle.proxy.hlsl" KEY "pt.compute.rectangle" COMPILE_OPTIONS "-DPT_VARIANT_USE_RWMC=0" "-DPT_VARIANT_SCENE_KIND=${PT_SCENE_RECTANGLE}") - PT_APPEND_SPIRV_RULE(VAR JSON INPUT "app_resources/hlsl/resolve.comp.hlsl" KEY "pt.compute.resolve") - PT_APPEND_SPIRV_RULE(VAR JSON INPUT "app_resources/hlsl/spirv/pt.misc.proxy.hlsl" KEY "pt.misc") - PT_FINALIZE_JSON_PAYLOAD(INOUT JSON) - - NBL_CREATE_NSC_COMPILE_RULES( - TARGET ${EXECUTABLE_NAME}SPIRV - LINK_TO ${EXECUTABLE_NAME} - DEPENDS ${DEPENDS} - 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} - ) -endif() diff --git a/31_HLSLPathTracer/app_resources/hlsl/compute.render.common.hlsl b/31_HLSLPathTracer/app_resources/hlsl/compute.render.common.hlsl deleted file mode 100644 index bc8af3c38..000000000 --- a/31_HLSLPathTracer/app_resources/hlsl/compute.render.common.hlsl +++ /dev/null @@ -1,66 +0,0 @@ -#ifndef PATH_TRACER_USE_RWMC -#error PATH_TRACER_USE_RWMC must be defined before including compute.render.common.hlsl -#endif - -#ifndef PATH_TRACER_ENABLE_LINEAR -#define PATH_TRACER_ENABLE_LINEAR 1 -#endif - -#ifndef PATH_TRACER_ENABLE_PERSISTENT -#define PATH_TRACER_ENABLE_PERSISTENT 1 -#endif - -#if !PATH_TRACER_ENABLE_LINEAR && !PATH_TRACER_ENABLE_PERSISTENT -#error At least one path tracer entrypoint mode must be enabled -#endif - -#include "nbl/builtin/hlsl/cpp_compat.hlsl" -#include "nbl/builtin/hlsl/glsl_compat/core.hlsl" -// -#include "nbl/builtin/hlsl/random/pcg.hlsl" -#include "nbl/builtin/hlsl/random/xoroshiro.hlsl" -// -#include "nbl/builtin/hlsl/morton.hlsl" -// -#include "nbl/builtin/hlsl/bxdf/reflection.hlsl" -#include "nbl/builtin/hlsl/bxdf/transmission.hlsl" -// -#include "nbl/builtin/hlsl/path_tracing/basic_ray_gen.hlsl" -#include "nbl/builtin/hlsl/path_tracing/unidirectional.hlsl" - -#include "nbl/examples/common/KeyedQuantizedSequence.hlsl" - -#include "render_common.hlsl" - -#if PATH_TRACER_USE_RWMC -#include "nbl/builtin/hlsl/rwmc/CascadeAccumulator.hlsl" -#include "render_rwmc_common.hlsl" -#else -#include "nbl/builtin/hlsl/path_tracing/default_accumulator.hlsl" -#endif - -#if PATH_TRACER_USE_RWMC -[[vk::push_constant]] RenderRWMCPushConstants pc; -#else -[[vk::push_constant]] RenderPushConstants pc; -#endif - -[[vk::combinedImageSampler]] [[vk::binding(0, 0)]] Texture2D envMap; -[[vk::combinedImageSampler]] [[vk::binding(0, 0)]] SamplerState envSampler; - -[[vk::combinedImageSampler]] [[vk::binding(1, 0)]] Texture2D scramblebuf; -[[vk::combinedImageSampler]] [[vk::binding(1, 0)]] SamplerState scrambleSampler; - -[[vk::image_format("rgba16f")]] [[vk::binding(2, 0)]] RWTexture2DArray outImage; - -#if PATH_TRACER_USE_RWMC -[[vk::image_format("rgba16f")]] [[vk::binding(3, 0)]] RWTexture2DArray cascade; -#endif - -#include "example_common.hlsl" -#include "intersector.hlsl" -#include "material_system.hlsl" -#include "next_event_estimator.hlsl" - -using namespace nbl; -using namespace hlsl; diff --git a/31_HLSLPathTracer/app_resources/hlsl/compute.render.linear.entrypoints.hlsl b/31_HLSLPathTracer/app_resources/hlsl/compute.render.linear.entrypoints.hlsl deleted file mode 100644 index 0660cbead..000000000 --- a/31_HLSLPathTracer/app_resources/hlsl/compute.render.linear.entrypoints.hlsl +++ /dev/null @@ -1,21 +0,0 @@ -#ifndef PATH_TRACER_ENTRYPOINT_NAME -#define PATH_TRACER_ENTRYPOINT_NAME main -#endif - -#ifndef PATH_TRACER_ENTRYPOINT_POLYGON_METHOD -#define PATH_TRACER_ENTRYPOINT_POLYGON_METHOD pathtracer_variant_config::EntryPointPolygonMethod -#endif - -#if !PATH_TRACER_ENABLE_LINEAR -#error Linear entrypoint requested while PATH_TRACER_ENABLE_LINEAR is disabled -#endif - -[numthreads(RenderWorkgroupSize, 1, 1)] -[shader("compute")] -void PATH_TRACER_ENTRYPOINT_NAME(uint32_t3 threadID : SV_DispatchThreadID) -{ - pathtracer_render_variant::runLinear(threadID); -} - -#undef PATH_TRACER_ENTRYPOINT_POLYGON_METHOD -#undef PATH_TRACER_ENTRYPOINT_NAME diff --git a/31_HLSLPathTracer/app_resources/hlsl/compute.render.persistent.entrypoints.hlsl b/31_HLSLPathTracer/app_resources/hlsl/compute.render.persistent.entrypoints.hlsl deleted file mode 100644 index 2327e5b04..000000000 --- a/31_HLSLPathTracer/app_resources/hlsl/compute.render.persistent.entrypoints.hlsl +++ /dev/null @@ -1,21 +0,0 @@ -#ifndef PATH_TRACER_ENTRYPOINT_NAME -#define PATH_TRACER_ENTRYPOINT_NAME mainPersistent -#endif - -#ifndef PATH_TRACER_ENTRYPOINT_POLYGON_METHOD -#define PATH_TRACER_ENTRYPOINT_POLYGON_METHOD pathtracer_variant_config::EntryPointPolygonMethod -#endif - -#if !PATH_TRACER_ENABLE_PERSISTENT -#error Persistent entrypoint requested while PATH_TRACER_ENABLE_PERSISTENT is disabled -#endif - -[numthreads(RenderWorkgroupSize, 1, 1)] -[shader("compute")] -void PATH_TRACER_ENTRYPOINT_NAME(uint32_t3 threadID : SV_DispatchThreadID) -{ - pathtracer_render_variant::runPersistent(); -} - -#undef PATH_TRACER_ENTRYPOINT_POLYGON_METHOD -#undef PATH_TRACER_ENTRYPOINT_NAME diff --git a/31_HLSLPathTracer/app_resources/hlsl/compute_render_scene_impl.hlsl b/31_HLSLPathTracer/app_resources/hlsl/compute_render_scene_impl.hlsl deleted file mode 100644 index 8706a8917..000000000 --- a/31_HLSLPathTracer/app_resources/hlsl/compute_render_scene_impl.hlsl +++ /dev/null @@ -1,160 +0,0 @@ -#ifndef PATH_TRACER_USE_RWMC -#error PATH_TRACER_USE_RWMC must be defined before including compute_render_scene_impl.hlsl -#endif - -namespace pathtracer_render_variant -{ -using namespace nbl; -using namespace hlsl; - -using ray_dir_info_t = bxdf::ray_dir_info::SBasic; -using iso_interaction = PTIsotropicInteraction; -using aniso_interaction = PTAnisotropicInteraction; -using sample_t = bxdf::SLightSample; -using iso_cache = bxdf::SIsotropicMicrofacetCache; -using aniso_cache = bxdf::SAnisotropicMicrofacetCache; - -using iso_config_t = PTIsoConfiguration; -using iso_microfacet_config_t = PTIsoMicrofacetConfiguration; - -using diffuse_bxdf_type = bxdf::reflection::SOrenNayar; -using conductor_bxdf_type = bxdf::reflection::SGGXIsotropic; -using dielectric_bxdf_type = bxdf::transmission::SGGXDielectricIsotropic; -using iri_conductor_bxdf_type = bxdf::reflection::SIridescent; -using iri_dielectric_bxdf_type = bxdf::transmission::SIridescent; - -using payload_type = Payload; -using randgen_type = examples::KeyedQuantizedSequence; -using material_system_type = MaterialSystem; - -#if PATH_TRACER_USE_RWMC -using accumulator_type = rwmc::CascadeAccumulator >; -#else -using accumulator_type = path_tracing::DefaultAccumulator; -#endif - -template -struct SVariantTypes -{ - using ray_type = Ray; - using raygen_type = path_tracing::BasicRayGenerator; - using intersector_type = Intersector; - using nee_type = NextEventEstimator; - using pathtracer_type = path_tracing::Unidirectional; -}; - -RenderPushConstants getRenderPushConstants() -{ -#if PATH_TRACER_USE_RWMC - return ::pc.renderPushConstants; -#else - return ::pc; -#endif -} - -template -void tracePixel(int32_t2 coords) -{ - const RenderPushConstants renderPushConstants = getRenderPushConstants(); - using variant_types = SVariantTypes; - - uint32_t width, height, imageArraySize; - ::outImage.GetDimensions(width, height, imageArraySize); - if (any(coords < int32_t2(0, 0)) || any(coords >= int32_t2(width, height))) - return; - - float32_t2 texCoord = float32_t2(coords) / float32_t2(width, height); - texCoord.y = 1.0 - texCoord.y; - - if (((renderPushConstants.depth - 1) >> MaxDepthLog2) > 0 || ((renderPushConstants.sampleCount - 1) >> MaxSamplesLog2) > 0) - { - ::outImage[uint3(coords.x, coords.y, 0)] = float32_t4(1.0, 0.0, 0.0, 1.0); - return; - } - - typename variant_types::pathtracer_type pathtracer; - - uint2 scrambleDim; - ::scramblebuf.GetDimensions(scrambleDim.x, scrambleDim.y); - const float32_t2 pixOffsetParam = float32_t2(1.0, 1.0) / float32_t2(scrambleDim); - - float32_t4 NDC = float32_t4(texCoord * float32_t2(2.0, -2.0) + float32_t2(-1.0, 1.0), 0.0, 1.0); - float32_t3 camPos; - { - float32_t4 tmp = mul(renderPushConstants.invMVP, NDC); - camPos = tmp.xyz / tmp.w; - NDC.z = 1.0; - } - - scene_type scene; - scene.updateLight(renderPushConstants.getLightMatrix()); - - typename variant_types::raygen_type rayGen; - rayGen.pixOffsetParam = pixOffsetParam; - rayGen.camPos = camPos; - rayGen.NDC = NDC; - rayGen.invMVP = renderPushConstants.invMVP; - - pathtracer.scene = scene; - pathtracer.randGen.pSampleBuffer = renderPushConstants.pSampleSequence; - pathtracer.randGen.rng = Xoroshiro64Star::construct(scramblebuf[coords].rg); - pathtracer.randGen.sequenceSamplesLog2 = renderPushConstants.sequenceSampleCountLog2; - pathtracer.nee.lights = lights; - pathtracer.materialSystem.bxdfs = bxdfs; - pathtracer.bxdfPdfThreshold = 0.0001; - pathtracer.lumaContributionThreshold = hlsl::dot(colorspace::scRGBtoXYZ[1], colorspace::eotf::sRGB(hlsl::promote(1.0 / 255.0))); - pathtracer.spectralTypeToLumaCoeffs = colorspace::scRGBtoXYZ[1]; - -#if PATH_TRACER_USE_RWMC - accumulator_type accumulator = accumulator_type::create(::pc.splattingParameters); -#else - accumulator_type accumulator = accumulator_type::create(); -#endif - - for (uint32_t i = 0u; i < renderPushConstants.sampleCount; ++i) - { - const float32_t3 uvw = pathtracer.randGen(0u, i); - typename variant_types::ray_type ray = rayGen.generate(uvw); - ray.initPayload(); - pathtracer.sampleMeasure(ray, i, renderPushConstants.depth, accumulator); - } - -#if PATH_TRACER_USE_RWMC - for (uint32_t i = 0; i < CascadeCount; ++i) - ::cascade[uint3(coords.x, coords.y, i)] = float32_t4(accumulator.accumulation.__data[i], 1.0f); -#else - ::outImage[uint3(coords.x, coords.y, 0)] = float32_t4(accumulator.accumulation, 1.0); -#endif -} - -#if PATH_TRACER_ENABLE_LINEAR -template -void runLinear(uint32_t3 threadID) -{ - uint32_t width, height, imageArraySize; - ::outImage.GetDimensions(width, height, imageArraySize); - tracePixel(int32_t2(threadID.x % width, threadID.x / width)); -} -#endif - -#if PATH_TRACER_ENABLE_PERSISTENT -template -void runPersistent() -{ - uint32_t width, height, imageArraySize; - ::outImage.GetDimensions(width, height, imageArraySize); - const uint32_t numWorkgroupsX = width / RenderWorkgroupSizeSqrt; - const uint32_t numWorkgroupsY = height / RenderWorkgroupSizeSqrt; - - [loop] - for (uint32_t wgBase = glsl::gl_WorkGroupID().x; wgBase < numWorkgroupsX * numWorkgroupsY; wgBase += glsl::gl_NumWorkGroups().x) - { - const int32_t2 wgCoords = int32_t2(wgBase % numWorkgroupsX, wgBase / numWorkgroupsX); - morton::code mc; - mc.value = glsl::gl_LocalInvocationIndex().x; - const int32_t2 localCoords = _static_cast(mc); - tracePixel(wgCoords * int32_t2(RenderWorkgroupSizeSqrt, RenderWorkgroupSizeSqrt) + localCoords); - } -} -#endif -} diff --git a/31_HLSLPathTracer/app_resources/hlsl/example_common.hlsl b/31_HLSLPathTracer/app_resources/hlsl/example_common.hlsl deleted file mode 100644 index 4e21f0309..000000000 --- a/31_HLSLPathTracer/app_resources/hlsl/example_common.hlsl +++ /dev/null @@ -1,742 +0,0 @@ -#ifndef _PATHTRACER_EXAMPLE_EXAMPLE_COMMON_INCLUDED_ -#define _PATHTRACER_EXAMPLE_EXAMPLE_COMMON_INCLUDED_ - -#include "nbl/builtin/hlsl/cpp_compat.hlsl" -#include "nbl/builtin/hlsl/shapes/spherical_triangle.hlsl" -#include "nbl/builtin/hlsl/shapes/spherical_rectangle.hlsl" -#include "nbl/builtin/hlsl/sampling/spherical_triangle.hlsl" -#include "nbl/builtin/hlsl/sampling/projected_spherical_triangle.hlsl" -#include "nbl/builtin/hlsl/sampling/spherical_rectangle.hlsl" - -using namespace nbl; -using namespace hlsl; - -enum ProceduralShapeType : uint16_t -{ - PST_NONE = 0, - PST_SPHERE, - PST_TRIANGLE, - PST_RECTANGLE -}; - -template -struct Payload -{ - using this_t = Payload; - using scalar_type = T; - using spectral_type = vector; - - spectral_type accumulation; - scalar_type otherTechniqueHeuristic; - spectral_type throughput; - // #ifdef KILL_DIFFUSE_SPECULAR_PATHS - // bool hasDiffuse; - // #endif -}; - -enum NEEPolygonMethod : uint16_t -{ - PPM_AREA, - PPM_SOLID_ANGLE, - PPM_APPROX_PROJECTED_SOLID_ANGLE -}; - -struct ObjectID -{ - static ObjectID create(uint16_t id, ProceduralShapeType shapeType) - { - ObjectID retval; - retval.id = id; - retval.shapeType = shapeType; - return retval; - } - - NBL_CONSTEXPR_STATIC_INLINE uint16_t INVALID_ID = 0x3fffu; - - uint16_t id : 14u; - ProceduralShapeType shapeType : 2u; -}; - -struct LightID -{ - NBL_CONSTEXPR_STATIC_INLINE uint16_t INVALID_ID = 0xffffu; - - uint16_t id; -}; - -struct MaterialID -{ - NBL_CONSTEXPR_STATIC_INLINE uint16_t INVALID_ID = 0xffffu; - - uint16_t id; -}; - -template -struct Ray -{ - using this_t = Ray; - using payload_type = Payload; - using scalar_type = typename payload_type::scalar_type; - using vector3_type = vector; - - // immutable - vector3_type origin; - vector3_type direction; - - // mutable - scalar_type intersectionT; - - payload_type payload; - using spectral_type = typename payload_type::spectral_type; - - void init(const vector3_type _origin, const vector3_type _direction) - { - origin = _origin; - direction = _direction; - } - - template - void setInteraction(NBL_CONST_REF_ARG(Interaction) interaction) - { - // empty, only for projected solid angle - } - - void initPayload() - { - payload.accumulation = hlsl::promote(0.0); - payload.otherTechniqueHeuristic = scalar_type(0.0); // needed for direct eye-light paths - payload.throughput = hlsl::promote(1.0); - } - - bool shouldDoMIS() - { - return payload.otherTechniqueHeuristic > numeric_limits::min; - } - - scalar_type foundEmissiveMIS(scalar_type pdfSq) - { - return scalar_type(1.0) / (scalar_type(1.0) + pdfSq * payload.otherTechniqueHeuristic); - } - - void addPayloadContribution(const spectral_type contribution) - { - payload.accumulation += contribution; - } - spectral_type getPayloadAccumulatiion() { return payload.accumulation; } - - void updateThroughputAndMISWeights(const spectral_type throughput, const scalar_type otherTechniqueHeuristic) - { - payload.throughput = throughput; - payload.otherTechniqueHeuristic = otherTechniqueHeuristic; - } - - void setT(scalar_type t) { intersectionT = t; } - scalar_type getT() NBL_CONST_MEMBER_FUNC { return intersectionT; } - - spectral_type getPayloadThroughput() NBL_CONST_MEMBER_FUNC { return payload.throughput; } -}; - -template -struct Ray -{ - using this_t = Ray; - using payload_type = Payload; - using scalar_type = typename payload_type::scalar_type; - using vector3_type = vector; - - // immutable - vector3_type origin; - vector3_type direction; - - vector3_type normalAtOrigin; - bool wasBSDFAtOrigin; - - // mutable - scalar_type intersectionT; - - payload_type payload; - using spectral_type = typename payload_type::spectral_type; - - void init(const vector3_type _origin, const vector3_type _direction) - { - origin = _origin; - direction = _direction; - } - - template - void setInteraction(NBL_CONST_REF_ARG(Interaction) interaction) - { - normalAtOrigin = interaction.getN(); - wasBSDFAtOrigin = interaction.isMaterialBSDF(); - } - - void initPayload() - { - payload.accumulation = hlsl::promote(0.0); - payload.otherTechniqueHeuristic = scalar_type(0.0); // needed for direct eye-light paths - payload.throughput = hlsl::promote(1.0); - } - - bool shouldDoMIS() - { - return payload.otherTechniqueHeuristic > numeric_limits::min; - } - - scalar_type foundEmissiveMIS(scalar_type pdfSq) - { - return scalar_type(1.0) / (scalar_type(1.0) + pdfSq * payload.otherTechniqueHeuristic); - } - - void addPayloadContribution(const vector3_type contribution) - { - payload.accumulation += contribution; - } - vector3_type getPayloadAccumulatiion() { return payload.accumulation; } - - void updateThroughputAndMISWeights(const vector3_type throughput, const scalar_type otherTechniqueHeuristic) - { - payload.throughput = throughput; - payload.otherTechniqueHeuristic = otherTechniqueHeuristic; - } - - void setT(scalar_type t) { intersectionT = t; } - scalar_type getT() NBL_CONST_MEMBER_FUNC { return intersectionT; } - - vector3_type getPayloadThroughput() NBL_CONST_MEMBER_FUNC { return payload.throughput; } -}; - -template -struct Light -{ - using spectral_type = Spectrum; - - static Light create(uint32_t emissiveMatID, uint32_t objId, ProceduralShapeType shapeType) - { - Light retval; - retval.emissiveMatID.id = uint16_t(emissiveMatID); - retval.objectID = ObjectID::create(uint16_t(objId), shapeType); - return retval; - } - - static Light create(uint32_t emissiveMatID, NBL_CONST_REF_ARG(ObjectID) objectID) - { - Light retval; - retval.emissiveMatID.id = uint16_t(emissiveMatID); - retval.objectID = objectID; - return retval; - } - - MaterialID emissiveMatID; - ObjectID objectID; -}; - -template -struct Tolerance -{ - NBL_CONSTEXPR_STATIC_INLINE T INTERSECTION_ERROR_BOUND_LOG2 = -8.0; - - static T __common(uint16_t depth) - { - T depthRcp = 1.0 / T(depth); - return INTERSECTION_ERROR_BOUND_LOG2; - } - - static T getStart(uint16_t depth) - { - return nbl::hlsl::exp2(__common(depth)); - } - - static T getEnd(uint16_t depth) - { - return 1.0 - nbl::hlsl::exp2(__common(depth) + 1.0); - } - - template - static void adjust(NBL_REF_ARG(Ray) ray, const vector adjDirection, uint16_t depth) - { - ray.origin += adjDirection * ray.intersectionT * getStart(depth); - } -}; - -enum MaterialType : uint32_t -{ - DIFFUSE = 0u, - CONDUCTOR, - DIELECTRIC, - IRIDESCENT_CONDUCTOR, - IRIDESCENT_DIELECTRIC, - EMISSIVE -}; - -template) -struct SBxDFCreationParams -{ - bool is_aniso; - vector A; // roughness - Spectrum ior0; // source ior - Spectrum ior1; // destination ior - Spectrum iork; // destination iork (for iridescent only) - Scalar eta; // in most cases, eta will be calculated from ior0 and ior1; see monochromeEta in path_tracing/unidirectional.hlsl -}; - -template -struct BxDFNode -{ - using spectral_type = Spectrum; - using scalar_type = typename vector_traits::scalar_type; - using vector2_type = vector; - using params_type = SBxDFCreationParams; - - // for diffuse bxdfs - static BxDFNode create(uint32_t materialType, bool isAniso, NBL_CONST_REF_ARG(vector2_type) A, NBL_CONST_REF_ARG(spectral_type) albedo) - { - BxDFNode retval; - retval.albedo = albedo; - retval.materialType = materialType; - retval.params.is_aniso = isAniso; - retval.params.A = hlsl::max(A, hlsl::promote(1e-3)); - retval.params.ior0 = hlsl::promote(1.0); - retval.params.ior1 = hlsl::promote(1.0); - return retval; - } - - // for conductor, ior0 = eta, ior1 = etak - // for dielectric, eta = ior1/ior0 - static BxDFNode create(uint32_t materialType, bool isAniso, NBL_CONST_REF_ARG(vector2_type) A, NBL_CONST_REF_ARG(spectral_type) ior0, NBL_CONST_REF_ARG(spectral_type) ior1) - { - BxDFNode retval; - retval.albedo = hlsl::promote(1.0); - retval.materialType = materialType; - retval.params.is_aniso = isAniso; - retval.params.A = hlsl::max(A, hlsl::promote(1e-3)); - retval.params.ior0 = ior0; - retval.params.ior1 = ior1; - return retval; - } - - // for iridescent bxdfs, ior0 = thin film ior, ior1+iork1 = base mat ior (k for conductor base) - static BxDFNode create(uint32_t materialType, bool isAniso, scalar_type A, scalar_type Dinc, NBL_CONST_REF_ARG(spectral_type) ior0, NBL_CONST_REF_ARG(spectral_type) ior1, NBL_CONST_REF_ARG(spectral_type) iork1) - { - BxDFNode retval; - retval.albedo = hlsl::promote(1.0); - retval.materialType = materialType; - retval.params.is_aniso = isAniso; - retval.params.A = vector2_type(hlsl::max(A, 1e-3), Dinc); - retval.params.ior0 = ior0; - retval.params.ior1 = ior1; - retval.params.iork = iork1; - return retval; - } - - // for emissive materials - static BxDFNode create(uint32_t materialType, NBL_CONST_REF_ARG(spectral_type) radiance) - { - BxDFNode retval; - retval.albedo = radiance; - retval.materialType = materialType; - return retval; - } - - scalar_type getNEEProb() - { - const scalar_type alpha = materialType != MaterialType::DIFFUSE ? params.A[0] : 1.0; - return hlsl::min(8.0 * alpha, 1.0); - } - - spectral_type albedo; // also stores radiance for emissive - uint32_t materialType; - params_type params; -}; - - -template -struct Shape; - -template -struct Shape -{ - using scalar_type = T; - using vector3_type = vector; - - static Shape create(NBL_CONST_REF_ARG(vector3_type) position, float32_t radius2, uint32_t bsdfLightIDs) - { - Shape retval; - retval.position = position; - retval.radius2 = radius2; - retval.bsdfLightIDs = bsdfLightIDs; - return retval; - } - - static Shape create(NBL_CONST_REF_ARG(vector3_type) position, scalar_type radius, uint32_t bsdfID, uint32_t lightID) - { - uint32_t bsdfLightIDs = glsl::bitfieldInsert(bsdfID, lightID, 16, 16); - return create(position, radius * radius, bsdfLightIDs); - } - - void updateTransform(NBL_CONST_REF_ARG(float32_t3x4) m) - { - position = float3(m[0].w, m[1].w, m[2].w); - radius2 = m[0].x * m[0].x; - } - - // return intersection distance if found, nan otherwise - scalar_type intersect(NBL_CONST_REF_ARG(vector3_type) origin, NBL_CONST_REF_ARG(vector3_type) direction) - { - vector3_type relOrigin = origin - position; - scalar_type relOriginLen2 = hlsl::dot(relOrigin, relOrigin); - - scalar_type dirDotRelOrigin = hlsl::dot(direction, relOrigin); - scalar_type det = radius2 - relOriginLen2 + dirDotRelOrigin * dirDotRelOrigin; - - // do some speculative math here - scalar_type detsqrt = hlsl::sqrt(det); - return -dirDotRelOrigin + (relOriginLen2 > radius2 ? (-detsqrt) : detsqrt); - } - - vector3_type getNormal(NBL_CONST_REF_ARG(vector3_type) hitPosition) - { - const scalar_type radiusRcp = hlsl::rsqrt(radius2); - return (hitPosition - position) * radiusRcp; - } - - scalar_type getSolidAngle(NBL_CONST_REF_ARG(vector3_type) origin) - { - vector3_type dist = position - origin; - scalar_type cosThetaMax = hlsl::sqrt(1.0 - radius2 / hlsl::dot(dist, dist)); - return 2.0 * numbers::pi * (1.0 - cosThetaMax); - } - - NBL_CONSTEXPR_STATIC_INLINE uint32_t ObjSize = 5; - - vector3_type position; - float32_t radius2; - uint32_t bsdfLightIDs; -}; - -template -struct Shape -{ - using scalar_type = T; - using vector3_type = vector; - - static Shape create(NBL_CONST_REF_ARG(vector3_type) vertex0, NBL_CONST_REF_ARG(vector3_type) vertex1, NBL_CONST_REF_ARG(vector3_type) vertex2, uint32_t bsdfLightIDs) - { - Shape retval; - retval.vertex0 = vertex0; - retval.vertex1 = vertex1; - retval.vertex2 = vertex2; - retval.bsdfLightIDs = bsdfLightIDs; - return retval; - } - - static Shape create(NBL_CONST_REF_ARG(vector3_type) vertex0, NBL_CONST_REF_ARG(vector3_type) vertex1, NBL_CONST_REF_ARG(vector3_type) vertex2, uint32_t bsdfID, uint32_t lightID) - { - uint32_t bsdfLightIDs = glsl::bitfieldInsert(bsdfID, lightID, 16, 16); - return create(vertex0, vertex1, vertex2, bsdfLightIDs); - } - - void updateTransform(NBL_CONST_REF_ARG(float32_t3x4) m) - { - // Define triangle in local space - float3 localVertex0 = float3(0.0, 0.0, 0.0); - float3 localVertex1 = float3(1.0, 0.0, 0.0); - float3 localVertex2 = float3(0.0, 1.0, 0.0); - - // Transform each vertex - vertex0 = mul(m, float4(localVertex0, 1.0)).xyz; - vertex1 = mul(m, float4(localVertex1, 1.0)).xyz; - vertex2 = mul(m, float4(localVertex2, 1.0)).xyz; - } - - scalar_type intersect(NBL_CONST_REF_ARG(vector3_type) origin, NBL_CONST_REF_ARG(vector3_type) direction) - { - const vector3_type edges[2] = { vertex1 - vertex0, vertex2 - vertex0 }; - - const vector3_type h = hlsl::cross(direction, edges[1]); - const scalar_type a = hlsl::dot(edges[0], h); - - const vector3_type relOrigin = origin - vertex0; - - const scalar_type u = hlsl::dot(relOrigin, h) / a; - - const vector3_type q = hlsl::cross(relOrigin, edges[0]); - const scalar_type v = hlsl::dot(direction, q) / a; - - const scalar_type t = hlsl::dot(edges[1], q) / a; - - const bool intersection = t > 0.f && u >= 0.f && v >= 0.f && (u + v) <= 1.f; - return intersection ? t : bit_cast(numeric_limits::infinity); - } - - vector3_type getNormalTimesArea() - { - const vector3_type edges[2] = { vertex1 - vertex0, vertex2 - vertex0 }; - return hlsl::cross(edges[0], edges[1]) * 0.5f; - } - - NBL_CONSTEXPR_STATIC_INLINE uint32_t ObjSize = 10; - - vector3_type vertex0; - vector3_type vertex1; - vector3_type vertex2; - uint32_t bsdfLightIDs; -}; - -template -struct Shape -{ - using scalar_type = T; - using vector3_type = vector; - - static Shape create(NBL_CONST_REF_ARG(vector3_type) offset, NBL_CONST_REF_ARG(vector3_type) edge0, NBL_CONST_REF_ARG(vector3_type) edge1, uint32_t bsdfLightIDs) - { - Shape retval; - retval.offset = offset; - retval.edge0 = edge0; - retval.edge1 = edge1; - retval.bsdfLightIDs = bsdfLightIDs; - return retval; - } - - static Shape create(NBL_CONST_REF_ARG(vector3_type) offset, NBL_CONST_REF_ARG(vector3_type) edge0, NBL_CONST_REF_ARG(vector3_type) edge1, uint32_t bsdfID, uint32_t lightID) - { - uint32_t bsdfLightIDs = glsl::bitfieldInsert(bsdfID, lightID, 16, 16); - return create(offset, edge0, edge1, bsdfLightIDs); - } - - void updateTransform(NBL_CONST_REF_ARG(float32_t3x4) m) - { - // Define rectangle in local space - float3 localVertex0 = float3(0.0, 0.0, 0.0); - float3 localVertex1 = float3(1.0, 0.0, 0.0); - float3 localVertex2 = float3(0.0, 1.0, 0.0); - - // Transform each vertex - float3 vertex0 = mul(m, float4(localVertex0, 1.0)).xyz; - float3 vertex1 = mul(m, float4(localVertex1, 1.0)).xyz; - float3 vertex2 = mul(m, float4(localVertex2, 1.0)).xyz; - - // Extract offset and edges from transformed vertices - offset = vertex0; - edge0 = vertex1 - vertex0; - edge1 = vertex2 - vertex0; - } - - scalar_type intersect(NBL_CONST_REF_ARG(vector3_type) origin, NBL_CONST_REF_ARG(vector3_type) direction) - { - const vector3_type h = hlsl::cross(direction, edge1); - const scalar_type a = hlsl::dot(edge0, h); - - const vector3_type relOrigin = origin - offset; - - const scalar_type u = hlsl::dot(relOrigin,h)/a; - - const vector3_type q = hlsl::cross(relOrigin, edge0); - const scalar_type v = hlsl::dot(direction, q) / a; - - const scalar_type t = hlsl::dot(edge1, q) / a; - - const bool intersection = t > 0.f && u >= 0.f && v >= 0.f && u <= 1.f && v <= 1.f; - return intersection ? t : bit_cast(numeric_limits::infinity); - } - - vector3_type getNormalTimesArea() - { - return hlsl::cross(edge0, edge1); - } - - void getNormalBasis(NBL_REF_ARG(matrix) basis, NBL_REF_ARG(vector) extents) - { - extents = vector(nbl::hlsl::length(edge0), nbl::hlsl::length(edge1)); - basis[0] = edge0 / extents[0]; - basis[1] = edge1 / extents[1]; - basis[2] = nbl::hlsl::normalize(nbl::hlsl::cross(basis[0],basis[1])); - } - - NBL_CONSTEXPR_STATIC_INLINE uint32_t ObjSize = 10; - - vector3_type offset; - vector3_type edge0; - vector3_type edge1; - uint32_t bsdfLightIDs; -}; - -template && concepts::FloatingPointLikeVectorial) -struct PTIsotropicInteraction -{ - using this_t = PTIsotropicInteraction; - using ray_dir_info_type = RayDirInfo; - using scalar_type = typename RayDirInfo::scalar_type; - using vector3_type = typename RayDirInfo::vector3_type; - using spectral_type = vector3_type; - - // WARNING: Changed since GLSL, now arguments need to be normalized! - static this_t create(NBL_CONST_REF_ARG(RayDirInfo) normalizedV, const vector3_type normalizedN) - { - this_t retval; - retval.V = normalizedV; - retval.N = normalizedN; - retval.NdotV = nbl::hlsl::dot(retval.N, retval.V.getDirection()); - retval.NdotV2 = retval.NdotV * retval.NdotV; - retval.luminosityContributionHint = hlsl::promote(1.0); - - return retval; - } - - RayDirInfo getV() NBL_CONST_MEMBER_FUNC { return V; } - vector3_type getN() NBL_CONST_MEMBER_FUNC { return N; } - scalar_type getNdotV(bxdf::BxDFClampMode _clamp = bxdf::BxDFClampMode::BCM_NONE) NBL_CONST_MEMBER_FUNC - { - return bxdf::conditionalAbsOrMax(NdotV, _clamp); - } - scalar_type getNdotV2() NBL_CONST_MEMBER_FUNC { return NdotV2; } - - bxdf::PathOrigin getPathOrigin() NBL_CONST_MEMBER_FUNC { return bxdf::PathOrigin::PO_SENSOR; } - spectral_type getLuminosityContributionHint() NBL_CONST_MEMBER_FUNC { return luminosityContributionHint; } - bool isMaterialBSDF() NBL_CONST_MEMBER_FUNC { return b_isMaterialBSDF; } - - RayDirInfo V; - vector3_type N; - scalar_type NdotV; - scalar_type NdotV2; - bool b_isMaterialBSDF; - - spectral_type luminosityContributionHint; -}; - -template) -struct PTAnisotropicInteraction -{ - using this_t = PTAnisotropicInteraction; - using isotropic_interaction_type = IsotropicInteraction; - using ray_dir_info_type = typename isotropic_interaction_type::ray_dir_info_type; - using scalar_type = typename ray_dir_info_type::scalar_type; - using vector3_type = typename ray_dir_info_type::vector3_type; - using matrix3x3_type = matrix; - using spectral_type = typename isotropic_interaction_type::spectral_type; - - // WARNING: Changed since GLSL, now arguments need to be normalized! - static this_t create( - NBL_CONST_REF_ARG(isotropic_interaction_type) isotropic, - const vector3_type normalizedT, - const vector3_type normalizedB - ) - { - this_t retval; - retval.isotropic = isotropic; - - retval.T = normalizedT; - retval.B = normalizedB; - - retval.TdotV = nbl::hlsl::dot(retval.isotropic.getV().getDirection(), retval.T); - retval.BdotV = nbl::hlsl::dot(retval.isotropic.getV().getDirection(), retval.B); - - return retval; - } - static this_t create(NBL_CONST_REF_ARG(isotropic_interaction_type) isotropic, const vector3_type normalizedT) - { - return create(isotropic, normalizedT, cross(isotropic.getN(), normalizedT)); - } - static this_t create(NBL_CONST_REF_ARG(isotropic_interaction_type) isotropic) - { - vector3_type T, B; - math::frisvad(isotropic.getN(), T, B); - return create(isotropic, nbl::hlsl::normalize(T), nbl::hlsl::normalize(B)); - } - - static this_t create(NBL_CONST_REF_ARG(ray_dir_info_type) normalizedV, const vector3_type normalizedN) - { - isotropic_interaction_type isotropic = isotropic_interaction_type::create(normalizedV, normalizedN); - return create(isotropic); - } - - ray_dir_info_type getV() NBL_CONST_MEMBER_FUNC { return isotropic.getV(); } - vector3_type getN() NBL_CONST_MEMBER_FUNC { return isotropic.getN(); } - scalar_type getNdotV(bxdf::BxDFClampMode _clamp = bxdf::BxDFClampMode::BCM_NONE) NBL_CONST_MEMBER_FUNC { return isotropic.getNdotV(_clamp); } - scalar_type getNdotV2() NBL_CONST_MEMBER_FUNC { return isotropic.getNdotV2(); } - bxdf::PathOrigin getPathOrigin() NBL_CONST_MEMBER_FUNC { return isotropic.getPathOrigin(); } - spectral_type getLuminosityContributionHint() NBL_CONST_MEMBER_FUNC { return isotropic.getLuminosityContributionHint(); } - bool isMaterialBSDF() NBL_CONST_MEMBER_FUNC { return isotropic.isMaterialBSDF(); } - isotropic_interaction_type getIsotropic() NBL_CONST_MEMBER_FUNC { return isotropic; } - - vector3_type getT() NBL_CONST_MEMBER_FUNC { return T; } - vector3_type getB() NBL_CONST_MEMBER_FUNC { return B; } - scalar_type getTdotV() NBL_CONST_MEMBER_FUNC { return TdotV; } - scalar_type getTdotV2() NBL_CONST_MEMBER_FUNC { const scalar_type t = getTdotV(); return t*t; } - scalar_type getBdotV() NBL_CONST_MEMBER_FUNC { return BdotV; } - scalar_type getBdotV2() NBL_CONST_MEMBER_FUNC { const scalar_type t = getBdotV(); return t*t; } - - vector3_type getTangentSpaceV() NBL_CONST_MEMBER_FUNC { return vector3_type(TdotV, BdotV, isotropic.getNdotV()); } - matrix3x3_type getToTangentSpace() NBL_CONST_MEMBER_FUNC { return matrix3x3_type(T, B, isotropic.getN()); } - matrix3x3_type getFromTangentSpace() NBL_CONST_MEMBER_FUNC { return nbl::hlsl::transpose(matrix3x3_type(T, B, isotropic.getN())); } - - isotropic_interaction_type isotropic; - vector3_type T; - vector3_type B; - scalar_type TdotV; - scalar_type BdotV; -}; - -template -struct PTIsoConfiguration; - -#define CONF_ISO bxdf::LightSample && bxdf::surface_interactions::Isotropic && !bxdf::surface_interactions::Anisotropic && concepts::FloatingPointLikeVectorial - -template -NBL_PARTIAL_REQ_TOP(CONF_ISO) -struct PTIsoConfiguration -#undef CONF_ISO -{ - NBL_CONSTEXPR_STATIC_INLINE bool IsAnisotropic = false; - - using scalar_type = typename LS::scalar_type; - using ray_dir_info_type = typename LS::ray_dir_info_type; - using vector2_type = vector; - using vector3_type = vector; - using monochrome_type = vector; - - using isotropic_interaction_type = Interaction; - using anisotropic_interaction_type = PTAnisotropicInteraction; - using sample_type = LS; - using spectral_type = Spectrum; - using quotient_pdf_type = sampling::quotient_and_pdf; -}; - -template -struct PTIsoMicrofacetConfiguration; - -#define MICROFACET_CONF_ISO bxdf::LightSample && bxdf::surface_interactions::Isotropic && !bxdf::surface_interactions::Anisotropic && bxdf::CreatableIsotropicMicrofacetCache && !bxdf::AnisotropicMicrofacetCache && concepts::FloatingPointLikeVectorial - -template -NBL_PARTIAL_REQ_TOP(MICROFACET_CONF_ISO) -struct PTIsoMicrofacetConfiguration : PTIsoConfiguration -#undef MICROFACET_CONF_ISO -{ - NBL_CONSTEXPR_STATIC_INLINE bool IsAnisotropic = false; - - using base_type = PTIsoConfiguration; - - using matrix3x3_type = matrix; - - using isocache_type = MicrofacetCache; - using anisocache_type = bxdf::SAnisotropicMicrofacetCache; -}; - -template -struct PTMaterialSystemCache -{ - using this_t = PTMaterialSystemCache; - using anisocache_type = AnisoCache; - using isocache_type = IsoCache; - - anisocache_type aniso_cache; - - // TODO: union or serialize somehow? - DiffuseBxDF diffuseBxDF; - ConductorBxDF conductorBxDF; - DielectricBxDF dielectricBxDF; - IridescentConductorBxDF iridescentConductorBxDF; - IridescentDielectricBxDF iridescentDielectricBxDF; -}; - -#endif diff --git a/31_HLSLPathTracer/app_resources/hlsl/imgui.unified.hlsl b/31_HLSLPathTracer/app_resources/hlsl/imgui.unified.hlsl deleted file mode 100644 index f36d98144..000000000 --- a/31_HLSLPathTracer/app_resources/hlsl/imgui.unified.hlsl +++ /dev/null @@ -1,9 +0,0 @@ -#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 1 -#define NBL_SAMPLERS_COUNT 2 - -#include "nbl/ext/ImGui/builtin/hlsl/fragment.hlsl" -#include "nbl/ext/ImGui/builtin/hlsl/vertex.hlsl" diff --git a/31_HLSLPathTracer/app_resources/hlsl/intersector.hlsl b/31_HLSLPathTracer/app_resources/hlsl/intersector.hlsl deleted file mode 100644 index 1ed93f098..000000000 --- a/31_HLSLPathTracer/app_resources/hlsl/intersector.hlsl +++ /dev/null @@ -1,131 +0,0 @@ -#ifndef _PATHTRACER_EXAMPLE_INTERSECTOR_INCLUDED_ -#define _PATHTRACER_EXAMPLE_INTERSECTOR_INCLUDED_ - -#include "example_common.hlsl" -#include - -using namespace nbl; -using namespace hlsl; - -template -struct Intersector -{ - using scalar_type = typename Ray::scalar_type; - using vector3_type = vector; - using ray_type = Ray; - using scene_type = Scene; - using object_handle_type = ObjectID; - - using anisotropic_interaction_type = AnisoInteraction; - using isotropic_interaction_type = typename anisotropic_interaction_type::isotropic_interaction_type; - using ray_dir_info_type = typename anisotropic_interaction_type::ray_dir_info_type; - - struct SIntersectData - { - using object_handle_type = object_handle_type; - using vector3_type = vector3_type; - using interaction_type = anisotropic_interaction_type; - - object_handle_type objectID; - vector3_type position; - interaction_type aniso_interaction; - vector3_type geometricNormal; - - bool foundHit() NBL_CONST_MEMBER_FUNC { return !hlsl::isnan(position.x); } - object_handle_type getObjectID() NBL_CONST_MEMBER_FUNC { return objectID; } - vector3_type getPosition() NBL_CONST_MEMBER_FUNC { return position; } - interaction_type getInteraction() NBL_CONST_MEMBER_FUNC { return aniso_interaction; } - vector3_type getGeometricNormal() NBL_CONST_MEMBER_FUNC { return geometricNormal; } - }; - using closest_hit_type = SIntersectData; - - static closest_hit_type traceClosestHit(NBL_CONST_REF_ARG(scene_type) scene, NBL_REF_ARG(ray_type) ray) - { - object_handle_type objectID; - objectID.id = object_handle_type::INVALID_ID; - - // prodedural shapes - NBL_UNROLL for (int i = 0; i < scene_type::SphereCount; i++) - { - float t = scene.getSphere(i).intersect(ray.origin, ray.direction); - - bool closerIntersection = t > 0.0 && t < ray.intersectionT; - - if (closerIntersection) - { - ray.intersectionT = t; - objectID.id = uint16_t(i); - objectID.shapeType = PST_SPHERE; - } - } - NBL_UNROLL for (int i = 0; i < scene_type::TriangleCount; i++) - { - float t = scene.getTriangle(i).intersect(ray.origin, ray.direction); - - bool closerIntersection = t > 0.0 && t < ray.intersectionT; - - if (closerIntersection) - { - ray.intersectionT = t; - objectID.id = uint16_t(i); - objectID.shapeType = PST_TRIANGLE; - } - } - NBL_UNROLL for (int i = 0; i < scene_type::RectangleCount; i++) - { - float t = scene.getRectangle(i).intersect(ray.origin, ray.direction); - - bool closerIntersection = t > 0.0 && t < ray.intersectionT; - - if (closerIntersection) - { - ray.intersectionT = t; - objectID.id = uint16_t(i); - objectID.shapeType = PST_RECTANGLE; - } - } - - closest_hit_type retval; - retval.objectID = objectID; - retval.position = hlsl::promote(bit_cast(numeric_limits::quiet_NaN)); - - bool foundHit = objectID.id != object_handle_type::INVALID_ID; - if (foundHit) - retval = scene.template getIntersection(objectID, ray); - - return retval; - } - - static scalar_type traceShadowRay(NBL_CONST_REF_ARG(scene_type) scene, NBL_REF_ARG(ray_type) ray, NBL_CONST_REF_ARG(object_handle_type) objectID) - { - // prodedural shapes - NBL_UNROLL for (int i = 0; i < scene_type::SphereCount; i++) - { - float t = scene.getSphere(i).intersect(ray.origin, ray.direction); - bool closerIntersection = t > 0.0 && t < ray.intersectionT; - - if (closerIntersection) - return 0.0; - } - NBL_UNROLL for (int i = 0; i < scene_type::TriangleCount; i++) - { - float t = scene.getTriangle(i).intersect(ray.origin, ray.direction); - bool closerIntersection = t > 0.0 && t < ray.intersectionT; - - if (closerIntersection) - return 0.0; - } - NBL_UNROLL for (int i = 0; i < scene_type::RectangleCount; i++) - { - float t = scene.getRectangle(i).intersect(ray.origin, ray.direction); - bool closerIntersection = t > 0.0 && t < ray.intersectionT; - - if (closerIntersection) - return 0.0; - } - - return 1.0; - } -}; - -#endif diff --git a/31_HLSLPathTracer/app_resources/hlsl/material_system.hlsl b/31_HLSLPathTracer/app_resources/hlsl/material_system.hlsl deleted file mode 100644 index 5aedd7a83..000000000 --- a/31_HLSLPathTracer/app_resources/hlsl/material_system.hlsl +++ /dev/null @@ -1,286 +0,0 @@ -#ifndef _PATHTRACER_EXAMPLE_MATERIAL_SYSTEM_INCLUDED_ -#define _PATHTRACER_EXAMPLE_MATERIAL_SYSTEM_INCLUDED_ - -#include -#include -#include - -#include "example_common.hlsl" - -using namespace nbl; -using namespace hlsl; - -template // NOTE: these bxdfs should match the ones in Scene BxDFNode -struct MaterialSystem -{ - using this_t = MaterialSystem; - using scalar_type = typename DiffuseBxDF::scalar_type; // types should be same across all 3 bxdfs - using vector2_type = vector; - using vector3_type = vector; - using material_id_type = MaterialID; - using measure_type = typename DiffuseBxDF::spectral_type; - using sample_type = typename DiffuseBxDF::sample_type; - using ray_dir_info_type = typename sample_type::ray_dir_info_type; - using quotient_pdf_type = typename DiffuseBxDF::quotient_pdf_type; - using anisotropic_interaction_type = typename DiffuseBxDF::anisotropic_interaction_type; - using isotropic_interaction_type = typename anisotropic_interaction_type::isotropic_interaction_type; - using anisocache_type = typename ConductorBxDF::anisocache_type; - using isocache_type = typename anisocache_type::isocache_type; - using cache_type = PTMaterialSystemCache; - using create_params_t = SBxDFCreationParams; - - using bxdfnode_type = BxDFNode; - using diffuse_op_type = DiffuseBxDF; - using conductor_op_type = ConductorBxDF; - using dielectric_op_type = DielectricBxDF; - using iri_conductor_op_type = IridescentConductorBxDF; - using iri_dielectric_op_type = IridescentDielectricBxDF; - - NBL_CONSTEXPR_STATIC_INLINE uint32_t IsBSDFPacked = uint32_t(bxdf::traits::type == bxdf::BT_BSDF) << uint32_t(MaterialType::DIFFUSE) | - uint32_t(bxdf::traits::type == bxdf::BT_BSDF) << uint32_t(MaterialType::CONDUCTOR) | - uint32_t(bxdf::traits::type == bxdf::BT_BSDF) << uint32_t(MaterialType::DIELECTRIC) | - uint32_t(bxdf::traits::type == bxdf::BT_BSDF) << uint32_t(MaterialType::IRIDESCENT_CONDUCTOR) | - uint32_t(bxdf::traits::type == bxdf::BT_BSDF) << uint32_t(MaterialType::IRIDESCENT_DIELECTRIC); - - bool isBSDF(material_id_type matID) - { - MaterialType matType = (MaterialType)bxdfs[matID.id].materialType; - return bool(IsBSDFPacked & (1u << matID.id)); - } - - bxdfnode_type getBxDFNode(material_id_type matID, NBL_REF_ARG(anisotropic_interaction_type) interaction) NBL_CONST_MEMBER_FUNC - { - interaction.isotropic.b_isMaterialBSDF = isBSDF(matID); - return bxdfs[matID.id]; - } - - scalar_type setMonochromeEta(material_id_type matID, measure_type throughputCIE_Y) - { - bxdfnode_type bxdf = bxdfs[matID.id]; - const measure_type eta = bxdf.params.ior1 / bxdf.params.ior0; - const scalar_type monochromeEta = hlsl::dot(throughputCIE_Y, eta) / (throughputCIE_Y.r + throughputCIE_Y.g + throughputCIE_Y.b); // TODO: imaginary eta? - bxdfs[matID.id].params.eta = monochromeEta; - return monochromeEta; - } - - cache_type getCacheFromSampleInteraction(material_id_type matID, NBL_CONST_REF_ARG(sample_type) _sample, NBL_CONST_REF_ARG(anisotropic_interaction_type) interaction) - { - const scalar_type monochromeEta = setMonochromeEta(matID, interaction.getLuminosityContributionHint()); - using monochrome_type = typename dielectric_op_type::monochrome_type; - bxdf::fresnel::OrientedEtas orientedEta = bxdf::fresnel::OrientedEtas::create(interaction.getNdotV(), hlsl::promote(monochromeEta)); - cache_type _cache; - _cache.aniso_cache = anisocache_type::template create(interaction, _sample, orientedEta); - fillBxdfParams(matID, _cache); - return _cache; - } - - // these are specific for the bxdfs used for this example - void fillBxdfParams(material_id_type matID, NBL_REF_ARG(cache_type) _cache) - { - create_params_t cparams = bxdfs[matID.id].params; - MaterialType matType = (MaterialType)bxdfs[matID.id].materialType; - switch(matType) - { - case MaterialType::DIFFUSE: - { - using creation_t = typename diffuse_op_type::creation_type; - creation_t params; - params.A = cparams.A.x; - _cache.diffuseBxDF = diffuse_op_type::create(params); - } - break; - case MaterialType::CONDUCTOR: - { - _cache.conductorBxDF.ndf = conductor_op_type::ndf_type::create(cparams.A.x); - _cache.conductorBxDF.fresnel = conductor_op_type::fresnel_type::create(cparams.ior0,cparams.ior1); - } - break; - case MaterialType::DIELECTRIC: - { - using oriented_eta_t = bxdf::fresnel::OrientedEtas; - oriented_eta_t orientedEta = oriented_eta_t::create(1.0, hlsl::promote(cparams.eta)); - _cache.dielectricBxDF.ndf = dielectric_op_type::ndf_type::create(cparams.A.x); - _cache.dielectricBxDF.fresnel = dielectric_op_type::fresnel_type::create(orientedEta); - } - break; - case MaterialType::IRIDESCENT_CONDUCTOR: - { - _cache.iridescentConductorBxDF.ndf = iri_conductor_op_type::ndf_type::create(cparams.A.x); - using creation_params_t = typename iri_conductor_op_type::fresnel_type::creation_params_type; - creation_params_t params; - params.Dinc = cparams.A.y; - params.ior1 = hlsl::promote(1.0); - params.ior2 = cparams.ior0; - params.ior3 = cparams.ior1; - params.iork3 = cparams.iork; - _cache.iridescentConductorBxDF.fresnel = iri_conductor_op_type::fresnel_type::create(params); - } - break; - case MaterialType::IRIDESCENT_DIELECTRIC: - { - _cache.iridescentDielectricBxDF.ndf = iri_dielectric_op_type::ndf_type::create(cparams.A.x); - using creation_params_t = typename iri_dielectric_op_type::fresnel_type::creation_params_type; - creation_params_t params; - params.Dinc = cparams.A.y; - params.ior1 = hlsl::promote(1.0); - params.ior2 = cparams.ior0; - params.ior3 = cparams.ior1; - _cache.iridescentDielectricBxDF.fresnel = iri_dielectric_op_type::fresnel_type::create(params); - } - break; - default: - return; - } - } - - measure_type eval(material_id_type matID, NBL_CONST_REF_ARG(sample_type) _sample, NBL_CONST_REF_ARG(anisotropic_interaction_type) interaction) - { - cache_type _cache = getCacheFromSampleInteraction(matID, _sample, interaction); - MaterialType matType = (MaterialType)bxdfs[matID.id].materialType; - switch(matType) - { - case MaterialType::DIFFUSE: - { - return bxdfs[matID.id].albedo * _cache.diffuseBxDF.eval(_sample, interaction.isotropic); - } - case MaterialType::CONDUCTOR: - { - return _cache.conductorBxDF.eval(_sample, interaction.isotropic, _cache.aniso_cache.iso_cache); - } - case MaterialType::DIELECTRIC: - { - return _cache.dielectricBxDF.eval(_sample, interaction.isotropic, _cache.aniso_cache.iso_cache); - } - case MaterialType::IRIDESCENT_CONDUCTOR: - { - return _cache.iridescentConductorBxDF.eval(_sample, interaction.isotropic, _cache.aniso_cache.iso_cache); - } - case MaterialType::IRIDESCENT_DIELECTRIC: - { - return _cache.iridescentDielectricBxDF.eval(_sample, interaction.isotropic, _cache.aniso_cache.iso_cache); - } - default: - return hlsl::promote(0.0); - } - } - - sample_type generate(material_id_type matID, NBL_CONST_REF_ARG(anisotropic_interaction_type) interaction, NBL_CONST_REF_ARG(vector3_type) u, NBL_REF_ARG(cache_type) _cache) - { - fillBxdfParams(matID, _cache); - MaterialType matType = (MaterialType)bxdfs[matID.id].materialType; - switch(matType) - { - case MaterialType::DIFFUSE: - { - return _cache.diffuseBxDF.generate(interaction, u.xy); - } - case MaterialType::CONDUCTOR: - { - return _cache.conductorBxDF.generate(interaction, u.xy, _cache.aniso_cache); - } - case MaterialType::DIELECTRIC: - { - return _cache.dielectricBxDF.generate(interaction, u, _cache.aniso_cache); - } - case MaterialType::IRIDESCENT_CONDUCTOR: - { - return _cache.iridescentConductorBxDF.generate(interaction, u.xy, _cache.aniso_cache); - } - case MaterialType::IRIDESCENT_DIELECTRIC: - { - return _cache.iridescentDielectricBxDF.generate(interaction, u, _cache.aniso_cache); - } - default: - { - ray_dir_info_type L; - L.makeInvalid(); - return sample_type::create(L, hlsl::promote(0.0)); - } - } - } - - scalar_type pdf(material_id_type matID, NBL_CONST_REF_ARG(sample_type) _sample, NBL_CONST_REF_ARG(anisotropic_interaction_type) interaction) - { - cache_type _cache = getCacheFromSampleInteraction(matID, _sample, interaction); - MaterialType matType = (MaterialType)bxdfs[matID.id].materialType; - switch(matType) - { - case MaterialType::DIFFUSE: - { - return _cache.diffuseBxDF.pdf(_sample, interaction.isotropic); - } - case MaterialType::CONDUCTOR: - { - return _cache.conductorBxDF.pdf(_sample, interaction.isotropic, _cache.aniso_cache.iso_cache); - } - case MaterialType::DIELECTRIC: - { - return _cache.dielectricBxDF.pdf(_sample, interaction.isotropic, _cache.aniso_cache.iso_cache); - } - case MaterialType::IRIDESCENT_CONDUCTOR: - { - return _cache.iridescentConductorBxDF.pdf(_sample, interaction.isotropic, _cache.aniso_cache.iso_cache); - } - case MaterialType::IRIDESCENT_DIELECTRIC: - { - return _cache.iridescentDielectricBxDF.pdf(_sample, interaction.isotropic, _cache.aniso_cache.iso_cache); - } - default: - return scalar_type(0.0); - } - } - - quotient_pdf_type quotient_and_pdf(material_id_type matID, NBL_CONST_REF_ARG(sample_type) _sample, NBL_CONST_REF_ARG(anisotropic_interaction_type) interaction, NBL_REF_ARG(cache_type) _cache) - { - const float minimumProjVectorLen = 0.00000001; // TODO: still need this check? - if (interaction.getNdotV(bxdf::BxDFClampMode::BCM_ABS) > minimumProjVectorLen && _sample.getNdotL(bxdf::BxDFClampMode::BCM_ABS) > minimumProjVectorLen) - { - MaterialType matType = (MaterialType)bxdfs[matID.id].materialType; - switch(matType) - { - case MaterialType::DIFFUSE: - { - quotient_pdf_type ret = _cache.diffuseBxDF.quotient_and_pdf(_sample, interaction.isotropic); - ret.quotient *= bxdfs[matID.id].albedo; - return ret; - } - case MaterialType::CONDUCTOR: - { - return _cache.conductorBxDF.quotient_and_pdf(_sample, interaction.isotropic, _cache.aniso_cache.iso_cache); - } - case MaterialType::DIELECTRIC: - { - return _cache.dielectricBxDF.quotient_and_pdf(_sample, interaction.isotropic, _cache.aniso_cache.iso_cache); - } - case MaterialType::IRIDESCENT_CONDUCTOR: - { - return _cache.iridescentConductorBxDF.quotient_and_pdf(_sample, interaction.isotropic, _cache.aniso_cache.iso_cache); - } - case MaterialType::IRIDESCENT_DIELECTRIC: - { - return _cache.iridescentDielectricBxDF.quotient_and_pdf(_sample, interaction.isotropic, _cache.aniso_cache.iso_cache); - } - default: - break; - } - } - return quotient_pdf_type::create(hlsl::promote(0.0), 0.0); - } - - bool hasEmission(material_id_type matID) - { - MaterialType matType = (MaterialType)bxdfs[matID.id].materialType; - return matType == MaterialType::EMISSIVE; - } - - measure_type getEmission(material_id_type matID, NBL_CONST_REF_ARG(anisotropic_interaction_type) interaction) - { - if (hasEmission(matID)) - return bxdfs[matID.id].albedo; - return hlsl::promote(0.0); - } - - bxdfnode_type bxdfs[Scene::SCENE_BXDF_COUNT]; -}; - -#endif diff --git a/31_HLSLPathTracer/app_resources/hlsl/next_event_estimator.hlsl b/31_HLSLPathTracer/app_resources/hlsl/next_event_estimator.hlsl deleted file mode 100644 index cd4be9929..000000000 --- a/31_HLSLPathTracer/app_resources/hlsl/next_event_estimator.hlsl +++ /dev/null @@ -1,455 +0,0 @@ -#ifndef _PATHTRACER_EXAMPLE_NEXT_EVENT_ESTIMATOR_INCLUDED_ -#define _PATHTRACER_EXAMPLE_NEXT_EVENT_ESTIMATOR_INCLUDED_ - -#include "example_common.hlsl" - -using namespace nbl; -using namespace hlsl; - -template -struct ShapeSampling; - -template -struct ShapeSampling -{ - using scalar_type = T; - using vector3_type = vector; - - static ShapeSampling create(NBL_CONST_REF_ARG(Shape) sphere) - { - ShapeSampling retval; - retval.sphere = sphere; - return retval; - } - - template - scalar_type deferredPdf(NBL_CONST_REF_ARG(Ray) ray) - { - return 1.0 / sphere.getSolidAngle(ray.origin); - } - - template - vector3_type generate_and_pdf(NBL_REF_ARG(scalar_type) pdf, NBL_REF_ARG(scalar_type) newRayMaxT, NBL_CONST_REF_ARG(vector3_type) origin, NBL_CONST_REF_ARG(Aniso) interaction, NBL_CONST_REF_ARG(vector3_type) xi) - { - vector3_type Z = sphere.position - origin; - const scalar_type distanceSQ = hlsl::dot(Z,Z); - const scalar_type cosThetaMax2 = 1.0 - sphere.radius2 / distanceSQ; - if (cosThetaMax2 > 0.0) - { - const scalar_type rcpDistance = 1.0 / hlsl::sqrt(distanceSQ); - Z *= rcpDistance; - - const scalar_type cosThetaMax = hlsl::sqrt(cosThetaMax2); - const scalar_type cosTheta = hlsl::mix(1.0f, cosThetaMax, xi.x); - - vector3_type L = Z * cosTheta; - - const scalar_type cosTheta2 = cosTheta * cosTheta; - const scalar_type sinTheta = hlsl::sqrt(1.0 - cosTheta2); - scalar_type sinPhi, cosPhi; - math::sincos(2.0 * numbers::pi * xi.y - numbers::pi, sinPhi, cosPhi); - vector3_type X, Y; - math::frisvad(Z, X, Y); - - L += (X * cosPhi + Y * sinPhi) * sinTheta; - - newRayMaxT = (cosTheta - hlsl::sqrt(cosTheta2 - cosThetaMax2)) / rcpDistance; - pdf = 1.0 / (2.0 * numbers::pi * (1.0 - cosThetaMax)); - return L; - } - pdf = 0.0; - return vector3_type(0.0,0.0,0.0); - } - - Shape sphere; -}; - -// make Area sampling default spec for everything -template -struct ShapeSampling -{ - using scalar_type = T; - using vector3_type = vector; - - static ShapeSampling create(NBL_CONST_REF_ARG(Shape) tri) - { - ShapeSampling retval; - retval.tri = tri; - return retval; - } - - template - scalar_type deferredPdf(NBL_CONST_REF_ARG(Ray) ray) - { - const scalar_type dist = ray.intersectionT; - const vector3_type L = ray.direction; - return dist * dist / hlsl::abs(hlsl::dot(tri.getNormalTimesArea(), L)); - } - - template - vector3_type generate_and_pdf(NBL_REF_ARG(scalar_type) pdf, NBL_REF_ARG(scalar_type) newRayMaxT, NBL_CONST_REF_ARG(vector3_type) origin, NBL_CONST_REF_ARG(Aniso) interaction, NBL_CONST_REF_ARG(vector3_type) xi) - { - const vector3_type edge0 = tri.vertex1 - tri.vertex0; - const vector3_type edge1 = tri.vertex2 - tri.vertex0; - const scalar_type sqrtU = hlsl::sqrt(xi.x); - vector3_type pnt = tri.vertex0 + edge0 * (1.0 - sqrtU) + edge1 * sqrtU * xi.y; - vector3_type L = pnt - origin; - - const scalar_type distanceSq = hlsl::dot(L,L); - const scalar_type rcpDistance = 1.0 / hlsl::sqrt(distanceSq); - L *= rcpDistance; - - pdf = distanceSq / hlsl::abs(hlsl::dot(hlsl::cross(edge0, edge1) * 0.5f, L)); - newRayMaxT = 1.0 / rcpDistance; - return L; - } - - Shape tri; -}; - -template -struct ShapeSampling -{ - using scalar_type = T; - using vector3_type = vector; - - static ShapeSampling create(NBL_CONST_REF_ARG(Shape) tri) - { - ShapeSampling retval; - retval.tri = tri; - return retval; - } - - template - scalar_type deferredPdf(NBL_CONST_REF_ARG(Ray) ray) - { - const vector3_type tri_vertices[3] = {tri.vertex0, tri.vertex1, tri.vertex2}; - shapes::SphericalTriangle st = shapes::SphericalTriangle::create(tri_vertices, ray.origin); - const scalar_type rcpProb = st.solidAngle(); - // if `rcpProb` is NAN then the triangle's solid angle was close to 0.0 - return rcpProb > numeric_limits::min ? (1.0 / rcpProb) : numeric_limits::max; - } - - template - vector3_type generate_and_pdf(NBL_REF_ARG(scalar_type) pdf, NBL_REF_ARG(scalar_type) newRayMaxT, NBL_CONST_REF_ARG(vector3_type) origin, NBL_CONST_REF_ARG(Aniso) interaction, NBL_CONST_REF_ARG(vector3_type) xi) - { - scalar_type rcpPdf; - const vector3_type tri_vertices[3] = {tri.vertex0, tri.vertex1, tri.vertex2}; - shapes::SphericalTriangle st = shapes::SphericalTriangle::create(tri_vertices, origin); - sampling::SphericalTriangle sst = sampling::SphericalTriangle::create(st); - - const vector3_type L = sst.generate(rcpPdf, xi.xy); - - pdf = rcpPdf > numeric_limits::min ? (1.0 / rcpPdf) : numeric_limits::max; - - const vector3_type N = tri.getNormalTimesArea(); - newRayMaxT = hlsl::dot(N, tri.vertex0 - origin) / hlsl::dot(N, L); - return L; - } - - Shape tri; -}; - -template -struct ShapeSampling -{ - using scalar_type = T; - using vector3_type = vector; - - static ShapeSampling create(NBL_CONST_REF_ARG(Shape) tri) - { - ShapeSampling retval; - retval.tri = tri; - return retval; - } - - template - scalar_type deferredPdf(NBL_CONST_REF_ARG(Ray) ray) - { - const vector3_type L = ray.direction; - const vector3_type tri_vertices[3] = {tri.vertex0, tri.vertex1, tri.vertex2}; - shapes::SphericalTriangle st = shapes::SphericalTriangle::create(tri_vertices, ray.origin); - sampling::ProjectedSphericalTriangle pst = sampling::ProjectedSphericalTriangle::create(st); - const scalar_type pdf = pst.backwardPdf(ray.normalAtOrigin, ray.wasBSDFAtOrigin, L); - // if `pdf` is NAN then the triangle's projected solid angle was close to 0.0, if its close to INF then the triangle was very small - return pdf < numeric_limits::max ? pdf : numeric_limits::max; - } - - template - vector3_type generate_and_pdf(NBL_REF_ARG(scalar_type) pdf, NBL_REF_ARG(scalar_type) newRayMaxT, NBL_CONST_REF_ARG(vector3_type) origin, NBL_CONST_REF_ARG(Aniso) interaction, NBL_CONST_REF_ARG(vector3_type) xi) - { - scalar_type rcpPdf; - const vector3_type tri_vertices[3] = {tri.vertex0, tri.vertex1, tri.vertex2}; - shapes::SphericalTriangle st = shapes::SphericalTriangle::create(tri_vertices, origin); - sampling::ProjectedSphericalTriangle pst = sampling::ProjectedSphericalTriangle::create(st); - - const vector3_type L = pst.generate(rcpPdf, interaction.getN(), interaction.isMaterialBSDF(), xi.xy); - - pdf = rcpPdf > numeric_limits::min ? (1.0 / rcpPdf) : numeric_limits::max; - - const vector3_type N = tri.getNormalTimesArea(); - newRayMaxT = hlsl::dot(N, tri.vertex0 - origin) / hlsl::dot(N, L); - return L; - } - - Shape tri; -}; - -// make Area sampling default spec for everything -template -struct ShapeSampling -{ - using scalar_type = T; - using vector3_type = vector; - - static ShapeSampling create(NBL_CONST_REF_ARG(Shape) rect) - { - ShapeSampling retval; - retval.rect = rect; - return retval; - } - - template - scalar_type deferredPdf(NBL_CONST_REF_ARG(Ray) ray) - { - const scalar_type dist = ray.intersectionT; - const vector3_type L = ray.direction; - return dist * dist / hlsl::abs(hlsl::dot(rect.getNormalTimesArea(), L)); - } - - template - vector3_type generate_and_pdf(NBL_REF_ARG(scalar_type) pdf, NBL_REF_ARG(scalar_type) newRayMaxT, NBL_CONST_REF_ARG(vector3_type) origin, NBL_CONST_REF_ARG(Aniso) interaction, NBL_CONST_REF_ARG(vector3_type) xi) - { - const vector3_type N = rect.getNormalTimesArea(); - const vector3_type origin2origin = rect.offset - origin; - - vector3_type L = origin2origin + rect.edge0 * xi.x + rect.edge1 * xi.y; - const scalar_type distSq = hlsl::dot(L, L); - const scalar_type rcpDist = 1.0 / hlsl::sqrt(distSq); - L *= rcpDist; - pdf = distSq / hlsl::abs(hlsl::dot(N, L)); - newRayMaxT = 1.0 / rcpDist; - return L; - } - - Shape rect; -}; - -template -struct ShapeSampling -{ - using scalar_type = T; - using vector3_type = vector; - - static ShapeSampling create(NBL_CONST_REF_ARG(Shape) rect) - { - ShapeSampling retval; - retval.rect = rect; - return retval; - } - - template - scalar_type deferredPdf(NBL_CONST_REF_ARG(Ray) ray) - { - scalar_type pdf; - matrix rectNormalBasis; - vector rectExtents; - rect.getNormalBasis(rectNormalBasis, rectExtents); - shapes::SphericalRectangle sphR0; - sphR0.origin = rect.offset; - sphR0.extents = rectExtents; - sphR0.basis = rectNormalBasis; - scalar_type solidAngle = sphR0.solidAngle(ray.origin); - if (solidAngle > numeric_limits::min) - pdf = 1.f / solidAngle; - else - pdf = bit_cast(numeric_limits::infinity); - return pdf; - } - - template - vector3_type generate_and_pdf(NBL_REF_ARG(scalar_type) pdf, NBL_REF_ARG(scalar_type) newRayMaxT, NBL_CONST_REF_ARG(vector3_type) origin, NBL_CONST_REF_ARG(Aniso) interaction, NBL_CONST_REF_ARG(vector3_type) xi) - { - const vector3_type N = rect.getNormalTimesArea(); - const vector3_type origin2origin = rect.offset - origin; - - matrix rectNormalBasis; - vector rectExtents; - rect.getNormalBasis(rectNormalBasis, rectExtents); - shapes::SphericalRectangle sphR0; - sphR0.origin = rect.offset; - sphR0.extents = rectExtents; - sphR0.basis = rectNormalBasis; - vector3_type L = hlsl::promote(0.0); - scalar_type solidAngle = sphR0.solidAngle(origin); - - sampling::SphericalRectangle ssph = sampling::SphericalRectangle::create(sphR0); - vector sphUv = ssph.generate(origin, xi.xy, solidAngle); - if (solidAngle > numeric_limits::min) - { - vector3_type sph_sample = sphUv.x * rect.edge0 + sphUv.y * rect.edge1 + rect.offset; - L = sph_sample - origin; - const bool invalid = hlsl::all(hlsl::abs(L) < hlsl::promote(numeric_limits::min)); - L = hlsl::mix(hlsl::normalize(L), hlsl::promote(0.0), invalid); - pdf = hlsl::mix(1.f / solidAngle, bit_cast(numeric_limits::infinity), invalid); - } - else - pdf = bit_cast(numeric_limits::infinity); - - newRayMaxT = hlsl::dot(N, origin2origin) / hlsl::dot(N, L); - return L; - } - - Shape rect; -}; - -template -struct EffectivePolygonMethod -{ - NBL_CONSTEXPR_STATIC_INLINE NEEPolygonMethod value = PPM; -}; - -template -struct EffectivePolygonMethod -{ - NBL_CONSTEXPR_STATIC_INLINE NEEPolygonMethod value = PPM_SOLID_ANGLE; -}; - -template<> -struct EffectivePolygonMethod -{ - NBL_CONSTEXPR_STATIC_INLINE NEEPolygonMethod value = PPM_SOLID_ANGLE; -}; - - -template -struct NextEventEstimator -{ - using scalar_type = typename Ray::scalar_type; - using vector3_type = vector; - using ray_type = Ray; - using scene_type = Scene; - using light_type = Light; - using light_id_type = LightID; - using spectral_type = typename light_type::spectral_type; - using interaction_type = Aniso; - using quotient_pdf_type = sampling::quotient_and_pdf; - using sample_type = LightSample; - using ray_dir_info_type = typename sample_type::ray_dir_info_type; - using tolerance_method_type = Tolerance; - - using shape_type = Shape; - - struct SampleQuotientReturn - { - using sample_type = sample_type; - using quotient_pdf_type = quotient_pdf_type; - using scalar_type = scalar_type; - using object_handle_type = ObjectID; - - sample_type sample_; - quotient_pdf_type quotient_pdf; - scalar_type newRayMaxT; - object_handle_type lightObjectID; - - sample_type getSample() NBL_CONST_MEMBER_FUNC { return sample_; } - quotient_pdf_type getQuotientPdf() NBL_CONST_MEMBER_FUNC { return quotient_pdf; } - scalar_type getT() NBL_CONST_MEMBER_FUNC { return newRayMaxT; } - object_handle_type getLightObjectID() NBL_CONST_MEMBER_FUNC { return lightObjectID; } - }; - using sample_quotient_return_type = SampleQuotientReturn; - NBL_CONSTEXPR_STATIC_INLINE NEEPolygonMethod EffectivePPM = EffectivePolygonMethod::value; - using shape_sampling_type = ShapeSampling; - - template NBL_FUNC_REQUIRES(C::value && PST==PST_SPHERE) - shape_type __getShape(uint32_t lightObjectID, NBL_CONST_REF_ARG(scene_type) scene) - { - return scene.getSphere(lightObjectID); - } - template NBL_FUNC_REQUIRES(C::value && PST==PST_TRIANGLE) - shape_type __getShape(uint32_t lightObjectID, NBL_CONST_REF_ARG(scene_type) scene) - { - return scene.getTriangle(lightObjectID); - } - template NBL_FUNC_REQUIRES(C::value && PST==PST_RECTANGLE) - shape_type __getShape(uint32_t lightObjectID, NBL_CONST_REF_ARG(scene_type) scene) - { - return scene.getRectangle(lightObjectID); - } - - scalar_type deferred_pdf(NBL_CONST_REF_ARG(scene_type) scene, light_id_type lightID, NBL_CONST_REF_ARG(ray_type) ray) - { - if (lightID.id == 0u) - return scalar_type(0.0); // env light pdf=0 - const light_type light = lights[0u]; - const shape_type shape = __getShape(light.objectID.id, scene); - const shape_sampling_type sampling = shape_sampling_type::create(shape); - return sampling.template deferredPdf(ray) / scalar_type(scene_type::SCENE_LIGHT_COUNT); - } - - template - sample_quotient_return_type generate_and_quotient_and_pdf(NBL_CONST_REF_ARG(scene_type) scene, NBL_CONST_REF_ARG(MaterialSystem) materialSystem, const vector3_type origin, NBL_CONST_REF_ARG(interaction_type) interaction, const vector3_type xi, uint16_t depth) - { - // light id 0 is reserved for env light - // however, we start indexing light array without env light, so index 0 is first shape light - // use constant indices because with variables, driver (at least nvidia) seemed to nuke the light array and propagated constants throughout the code - // which caused frame times to increase from 16ms to 85ms - const light_type light = lights[0u]; - const shape_type shape = __getShape(light.objectID.id, scene); - - sample_quotient_return_type retval; - scalar_type pdf, newRayMaxT; - const shape_sampling_type sampling = shape_sampling_type::create(shape); - const vector3_type sampleL = sampling.template generate_and_pdf(pdf, newRayMaxT, origin, interaction, xi); - - const vector3_type N = interaction.getN(); - const scalar_type NdotL = nbl::hlsl::dot(N, sampleL); - - // returned pdf is for MIS weight only - // normally, pdf=inf indicates a point light - // but here pdf=inf when solidAngle=0, so quotient of finite area emission =0 due to division by inf - // also for NdotL, normally would have to check conditionalMaxOrAbs(NdotL,0.0f,isBSDF) > min - // because BSDFs should receive light from the backside - // however, unnecessary for this example because scene has only watertight geometry - if (pdf > numeric_limits::min && !hlsl::isinf(pdf) && NdotL > numeric_limits::min) - { - ray_dir_info_type rayL; - rayL.setDirection(sampleL); - retval.sample_ = sample_type::create(rayL,interaction.getT(),interaction.getB(),NdotL); - - newRayMaxT *= tolerance_method_type::getEnd(depth); - pdf *= 1.0 / scalar_type(scene_type::SCENE_LIGHT_COUNT); - const spectral_type radiance = materialSystem.getEmission(light.emissiveMatID, interaction); - spectral_type quo = radiance / pdf; - retval.quotient_pdf = quotient_pdf_type::create(quo, pdf); - retval.newRayMaxT = newRayMaxT; - retval.lightObjectID = light.objectID; - } - else - { - retval.quotient_pdf = quotient_pdf_type::create(0.0, 0.0); - ray_dir_info_type rayL; - rayL.makeInvalid(); - retval.sample_ = sample_type::create(rayL,hlsl::promote(0.0)); - } - - return retval; - } - - light_id_type get_env_light_id() - { - light_id_type env_light_id; - env_light_id.id = 0u; - return env_light_id; - } - - spectral_type get_environment_radiance(NBL_CONST_REF_ARG(ray_type) ray) - { - // can also sample environment map using ray direction - return vector3_type(0.15, 0.21, 0.3); - } - - light_type lights[scene_type::SCENE_LIGHT_COUNT]; -}; - -#endif diff --git a/31_HLSLPathTracer/app_resources/hlsl/present.frag.hlsl b/31_HLSLPathTracer/app_resources/hlsl/present.frag.hlsl deleted file mode 100644 index d69815fba..000000000 --- a/31_HLSLPathTracer/app_resources/hlsl/present.frag.hlsl +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (C) 2024-2024 - 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 - -// vertex shader is provided by the fullScreenTriangle extension -#include -using namespace nbl::hlsl; -using namespace ext::FullScreenTriangle; - -// binding 0 set 0 -[[vk::combinedImageSampler]] [[vk::binding(0, 0)]] Texture2DArray texture; -[[vk::combinedImageSampler]] [[vk::binding(0, 0)]] SamplerState samplerState; - -[shader("pixel")] -float32_t4 main(SVertexAttributes vxAttr) : SV_Target0 -{ - return float32_t4(texture.Sample(samplerState, float3(vxAttr.uv, 0)).rgb, 1.0f); -} diff --git a/31_HLSLPathTracer/app_resources/hlsl/render_common.hlsl b/31_HLSLPathTracer/app_resources/hlsl/render_common.hlsl deleted file mode 100644 index 95c3e2c3d..000000000 --- a/31_HLSLPathTracer/app_resources/hlsl/render_common.hlsl +++ /dev/null @@ -1,58 +0,0 @@ -#ifndef _PATHTRACER_EXAMPLE_RENDER_COMMON_INCLUDED_ -#define _PATHTRACER_EXAMPLE_RENDER_COMMON_INCLUDED_ -#include "nbl/builtin/hlsl/cpp_compat.hlsl" - -using namespace nbl; -using namespace hlsl; - -#define MAX_DEPTH_LOG2 4 -#define MAX_SAMPLES_LOG2 10 -NBL_CONSTEXPR uint32_t MaxDepthLog2 = MAX_DEPTH_LOG2; -NBL_CONSTEXPR uint32_t MaxSamplesLog2 = MAX_SAMPLES_LOG2; - -struct RenderPushConstants -{ - // we set using a transpose matrix - void setLightMatrix(const float32_t4x3 matT) - { - lightX = matT[0]; - lightY = matT[1]; - // Z had a length and a direction, can point colinear or opposite cross product - const float32_t3 recon = hlsl::cross(matT[0],matT[1]); - lightZscale = hlsl::sign(hlsl::dot(recon,matT[2]))*hlsl::length(matT[2])/hlsl::length(recon); - lightPos = matT[3]; - assert(lightMatrix()==hlsl::transpose(matT)); - } - - float32_t3x4 getLightMatrix() - { - float32_t4x3 retval; - retval[0] = lightX; - retval[1] = lightY; - retval[2] = cross(lightX, lightY) * lightZscale; - retval[3] = lightPos; - return hlsl::transpose(retval); - } - - uint64_t pSampleSequence; - float32_t4x4 invMVP; - float32_t3 lightX; - float32_t3 lightY; - float32_t lightZscale; - float32_t3 lightPos; - // TODO: compact a bit and refactor - uint32_t sampleCount : MAX_SAMPLES_LOG2; - uint32_t depth : MAX_DEPTH_LOG2; - uint32_t sequenceSampleCountLog2 : 5; - uint32_t unused : 13; -}; -#undef MAX_SAMPLES_LOG2 -#undef MAX_DEPTH_LOG2 - -NBL_CONSTEXPR float32_t3 LightEminence = float32_t3(30.0f, 25.0f, 15.0f); -NBL_CONSTEXPR uint32_t RenderWorkgroupSizeSqrt = 8u; -NBL_CONSTEXPR uint32_t RenderWorkgroupSize = RenderWorkgroupSizeSqrt*RenderWorkgroupSizeSqrt; -NBL_CONSTEXPR uint32_t MaxDescriptorCount = 256u; -NBL_CONSTEXPR uint16_t MaxUITextureCount = 1u; - -#endif diff --git a/31_HLSLPathTracer/app_resources/hlsl/render_rwmc_common.hlsl b/31_HLSLPathTracer/app_resources/hlsl/render_rwmc_common.hlsl deleted file mode 100644 index 65104ecf2..000000000 --- a/31_HLSLPathTracer/app_resources/hlsl/render_rwmc_common.hlsl +++ /dev/null @@ -1,17 +0,0 @@ -#ifndef _PATHTRACER_EXAMPLE_RENDER_RWMC_COMMON_INCLUDED_ -#define _PATHTRACER_EXAMPLE_RENDER_RWMC_COMMON_INCLUDED_ -#include "nbl/builtin/hlsl/cpp_compat.hlsl" -#include "rwmc_common.hlsl" -#include "nbl/builtin/hlsl/rwmc/SplattingParameters.hlsl" -#include "render_common.hlsl" - -struct RenderRWMCPushConstants -{ - RenderPushConstants renderPushConstants; - nbl::hlsl::rwmc::SPackedSplattingParameters splattingParameters; -}; -#ifndef __HLSL_VERSION -static_assert(sizeof(RenderRWMCPushConstants)<=128,"Nabla Core Profile Guarantees only minimum of 128 bytes"); -#endif - -#endif diff --git a/31_HLSLPathTracer/app_resources/hlsl/resolve.comp.hlsl b/31_HLSLPathTracer/app_resources/hlsl/resolve.comp.hlsl deleted file mode 100644 index 346ff7322..000000000 --- a/31_HLSLPathTracer/app_resources/hlsl/resolve.comp.hlsl +++ /dev/null @@ -1,66 +0,0 @@ -#include -#include "resolve_common.hlsl" - -[[vk::image_format("rgba16f")]] [[vk::binding(2, 0)]] RWTexture2DArray outImage; -[[vk::image_format("rgba16f")]] [[vk::binding(3, 0)]] RWTexture2DArray cascade; -[[vk::push_constant]] ResolvePushConstants pc; - -using namespace nbl; -using namespace hlsl; - -struct SCascadeAccessor -{ - using output_scalar_t = float32_t; - NBL_CONSTEXPR_STATIC_INLINE int32_t Components = 3; - using output_t = vector; - NBL_CONSTEXPR_STATIC_INLINE int32_t image_dimension = 2; - - static SCascadeAccessor create() - { - SCascadeAccessor retval; - uint32_t imgWidth, imgHeight, layers; - cascade.GetDimensions(imgWidth, imgHeight, layers); - retval.cascadeImageDimension = int16_t2(imgWidth, imgHeight); - return retval; - } - - template - void get(NBL_REF_ARG(output_t) value, vector uv, uint16_t layer, uint16_t level) - { - if (any(uv < int16_t2(0, 0)) || any(uv >= cascadeImageDimension)) - { - value = promote(0); - return; - } - - value = cascade.Load(int32_t3(uv, int32_t(layer))).rgb; - } - - int16_t2 cascadeImageDimension; -}; - -int32_t2 getImageExtents() -{ - uint32_t width, height, imageArraySize; - outImage.GetDimensions(width, height, imageArraySize); - return int32_t2(width, height); -} - -[numthreads(ResolveWorkgroupSizeX, ResolveWorkgroupSizeY, 1)] -[shader("compute")] -void resolve(uint32_t3 threadID : SV_DispatchThreadID) -{ - const int32_t2 coords = int32_t2(threadID.x, threadID.y); - const int32_t2 imageExtents = getImageExtents(); - if (coords.x >= imageExtents.x || coords.y >= imageExtents.y) - return; - - using SResolveAccessorAdaptorType = rwmc::SResolveAccessorAdaptor; - using SResolverType = rwmc::SResolver; - SResolveAccessorAdaptorType accessor = { SCascadeAccessor::create() }; - SResolverType resolve = SResolverType::create(pc.resolveParameters); - - float32_t3 color = resolve(accessor, int16_t2(coords.x, coords.y)); - - outImage[uint3(coords.x, coords.y, 0)] = float32_t4(color, 1.0f); -} diff --git a/31_HLSLPathTracer/app_resources/hlsl/resolve_common.hlsl b/31_HLSLPathTracer/app_resources/hlsl/resolve_common.hlsl deleted file mode 100644 index ec13c0080..000000000 --- a/31_HLSLPathTracer/app_resources/hlsl/resolve_common.hlsl +++ /dev/null @@ -1,14 +0,0 @@ -#ifndef _PATHTRACER_EXAMPLE_RESOLVE_COMMON_INCLUDED_ -#define _PATHTRACER_EXAMPLE_RESOLVE_COMMON_INCLUDED_ -#include "nbl/builtin/hlsl/cpp_compat.hlsl" -#include "rwmc_common.hlsl" -#include "nbl/builtin/hlsl/rwmc/ResolveParameters.hlsl" - -struct ResolvePushConstants -{ - nbl::hlsl::rwmc::SResolveParameters resolveParameters; -}; - -NBL_CONSTEXPR uint32_t ResolveWorkgroupSizeX = 32u; -NBL_CONSTEXPR uint32_t ResolveWorkgroupSizeY = 16u; -#endif diff --git a/31_HLSLPathTracer/app_resources/hlsl/rwmc_common.hlsl b/31_HLSLPathTracer/app_resources/hlsl/rwmc_common.hlsl deleted file mode 100644 index 77020ce17..000000000 --- a/31_HLSLPathTracer/app_resources/hlsl/rwmc_common.hlsl +++ /dev/null @@ -1,8 +0,0 @@ -#ifndef _PATHTRACER_EXAMPLE_RWMC_COMMON_INCLUDED_ -#define _PATHTRACER_EXAMPLE_RWMC_COMMON_INCLUDED_ - -#include "nbl/builtin/hlsl/cpp_compat.hlsl" - -NBL_CONSTEXPR uint32_t CascadeCount = 6u; - -#endif diff --git a/31_HLSLPathTracer/app_resources/hlsl/scene_base.hlsl b/31_HLSLPathTracer/app_resources/hlsl/scene_base.hlsl deleted file mode 100644 index 59a9f3c57..000000000 --- a/31_HLSLPathTracer/app_resources/hlsl/scene_base.hlsl +++ /dev/null @@ -1,83 +0,0 @@ -#if !defined(PATHTRACER_SCENE_BASE_MULTI_INCLUDE) - #ifndef _PATHTRACER_EXAMPLE_SCENE_BASE_INCLUDED_ - #define _PATHTRACER_EXAMPLE_SCENE_BASE_INCLUDED_ - #define PATHTRACER_SCENE_BASE_EMIT_BODY 1 - #endif -#else - #define PATHTRACER_SCENE_BASE_EMIT_BODY 1 -#endif - -#if PATHTRACER_SCENE_BASE_EMIT_BODY - -using namespace nbl; -using namespace hlsl; - -struct SceneBase -{ - using scalar_type = float; - using vector3_type = vector; - using light_type = Light; - using light_id_type = LightID; - - NBL_CONSTEXPR_STATIC_INLINE uint32_t SCENE_SPHERE_COUNT = 10u; - NBL_CONSTEXPR_STATIC_INLINE uint32_t SCENE_LIGHT_COUNT = 1u; - NBL_CONSTEXPR_STATIC_INLINE uint32_t SCENE_BXDF_COUNT = 10u; - - static const Shape scene_spheres[SCENE_SPHERE_COUNT]; - - struct MatLightID - { - using light_id_type = LightID; - using material_id_type = MaterialID; - - light_id_type lightID; - material_id_type matID; - - static MatLightID createFromPacked(uint32_t packedID) - { - MatLightID retval; - retval.lightID.id = uint16_t(glsl::bitfieldExtract(packedID, 16, 16)); - retval.matID.id = uint16_t(glsl::bitfieldExtract(packedID, 0, 16)); - return retval; - } - - light_id_type getLightID() NBL_CONST_MEMBER_FUNC { return lightID; } - material_id_type getMaterialID() NBL_CONST_MEMBER_FUNC { return matID; } - - bool isLight() NBL_CONST_MEMBER_FUNC { return lightID.id != light_id_type::INVALID_ID; } - bool canContinuePath() NBL_CONST_MEMBER_FUNC { return matID.id != material_id_type::INVALID_ID; } - }; - using mat_light_id_type = MatLightID; -}; - -const Shape SceneBase::scene_spheres[SCENE_SPHERE_COUNT] = { - Shape::create(float3(0.0, -100.5, -1.0), 100.0, 0u, SceneBase::light_id_type::INVALID_ID), - Shape::create(float3(2.0, 0.0, -1.0), 0.5, 1u, SceneBase::light_id_type::INVALID_ID), - Shape::create(float3(0.0, 0.0, -1.0), 0.5, 2u, SceneBase::light_id_type::INVALID_ID), - Shape::create(float3(-2.0, 0.0, -1.0), 0.5, 3u, SceneBase::light_id_type::INVALID_ID), - Shape::create(float3(2.0, 0.0, 1.0), 0.5, 4u, SceneBase::light_id_type::INVALID_ID), - Shape::create(float3(0.0, 0.0, 1.0), 0.5, 4u, SceneBase::light_id_type::INVALID_ID), - Shape::create(float3(-2.0, 0.0, 1.0), 0.5, 5u, SceneBase::light_id_type::INVALID_ID), - Shape::create(float3(0.5, 1.0, 0.5), 0.5, 6u, SceneBase::light_id_type::INVALID_ID), - Shape::create(float3(-4.0, 0.0, 1.0), 0.5, 7u, SceneBase::light_id_type::INVALID_ID), - Shape::create(float3(-4.0, 0.0, -1.0), 0.5, 8u, SceneBase::light_id_type::INVALID_ID) -}; - -using spectral_t = vector; -using bxdfnode_type = BxDFNode; - -static const bxdfnode_type bxdfs[SceneBase::SCENE_BXDF_COUNT] = { - bxdfnode_type::create(MaterialType::DIFFUSE, false, float2(0,0), spectral_t(0.8,0.8,0.8)), - bxdfnode_type::create(MaterialType::DIFFUSE, false, float2(0,0), spectral_t(0.8,0.4,0.4)), - bxdfnode_type::create(MaterialType::DIFFUSE, false, float2(0,0), spectral_t(0.4,0.8,0.4)), - bxdfnode_type::create(MaterialType::CONDUCTOR, false, float2(0,0), spectral_t(1.02,1.02,1.3), spectral_t(1.0,1.0,2.0)), - bxdfnode_type::create(MaterialType::CONDUCTOR, false, float2(0,0), spectral_t(1.02,1.3,1.02), spectral_t(1.0,2.0,1.0)), - bxdfnode_type::create(MaterialType::CONDUCTOR, false, float2(0.15,0.15), spectral_t(1.02,1.3,1.02), spectral_t(1.0,2.0,1.0)), - bxdfnode_type::create(MaterialType::DIELECTRIC, false, float2(0.0625,0.0625), spectral_t(1,1,1), spectral_t(1.4,1.45,1.5)), - bxdfnode_type::create(MaterialType::IRIDESCENT_CONDUCTOR, false, 0.0, 505.0, spectral_t(1.39,1.39,1.39), spectral_t(1.2,1.2,1.2), spectral_t(0.5,0.5,0.5)), - bxdfnode_type::create(MaterialType::IRIDESCENT_DIELECTRIC, false, 0.0, 400.0, spectral_t(1.7,1.7,1.7), spectral_t(1.0,1.0,1.0), spectral_t(0,0,0)), - bxdfnode_type::create(MaterialType::EMISSIVE, LightEminence) -}; - -#undef PATHTRACER_SCENE_BASE_EMIT_BODY -#endif diff --git a/31_HLSLPathTracer/app_resources/hlsl/scene_rectangle_light.hlsl b/31_HLSLPathTracer/app_resources/hlsl/scene_rectangle_light.hlsl deleted file mode 100644 index 4dd821a94..000000000 --- a/31_HLSLPathTracer/app_resources/hlsl/scene_rectangle_light.hlsl +++ /dev/null @@ -1,97 +0,0 @@ -#ifndef _PATHTRACER_EXAMPLE_SCENE_RECTANGLE_LIGHT_INCLUDED_ -#define _PATHTRACER_EXAMPLE_SCENE_RECTANGLE_LIGHT_INCLUDED_ - -#include "scene_base.hlsl" - -using namespace nbl; -using namespace hlsl; - -struct SceneRectangleLight : SceneBase -{ - using scalar_type = float; - using vector3_type = vector; - using this_t = SceneRectangleLight; - using base_t = SceneBase; - using object_handle_type = ObjectID; - using mat_light_id_type = base_t::mat_light_id_type; - - using ray_dir_info_t = bxdf::ray_dir_info::SBasic; - using interaction_type = PTIsotropicInteraction; - - NBL_CONSTEXPR_STATIC_INLINE uint32_t SphereCount = base_t::SCENE_SPHERE_COUNT; - NBL_CONSTEXPR_STATIC_INLINE uint32_t TriangleCount = 0u; - NBL_CONSTEXPR_STATIC_INLINE uint32_t RectangleCount = base_t::SCENE_LIGHT_COUNT; - - static const Shape light_rectangles[1]; - - Shape getSphere(uint32_t idx) - { - assert(idx < SphereCount); - return base_t::scene_spheres[idx]; - } - Shape getTriangle(uint32_t idx) - { - assert(false); - Shape dummy; - return dummy; - } - Shape getRectangle(uint32_t idx) - { - assert(idx < RectangleCount); - return light_rectangles[idx]; - } - - void updateLight(NBL_CONST_REF_ARG(float32_t3x4) generalPurposeLightMatrix) - { - light_rectangles[0].updateTransform(generalPurposeLightMatrix); - } - - mat_light_id_type getMatLightIDs(NBL_CONST_REF_ARG(object_handle_type) objectID) - { - assert(objectID.shapeType == PST_SPHERE || objectID.shapeType == PST_RECTANGLE); - if (objectID.shapeType == PST_SPHERE) - return mat_light_id_type::createFromPacked(getSphere(objectID.id).bsdfLightIDs); - else - return mat_light_id_type::createFromPacked(getRectangle(objectID.id).bsdfLightIDs); - } - - template - Intersection getIntersection(NBL_CONST_REF_ARG(object_handle_type) objectID, NBL_CONST_REF_ARG(Ray) rayIntersected) - { - assert(objectID.shapeType == PST_SPHERE || objectID.shapeType == PST_RECTANGLE); - Intersection intersection; - intersection.objectID = objectID; - intersection.position = rayIntersected.origin + rayIntersected.direction * rayIntersected.intersectionT; - - vector3_type N = objectID.shapeType == PST_SPHERE ? getSphere(objectID.id).getNormal(intersection.position) : getRectangle(objectID.id).getNormalTimesArea(); - N = hlsl::normalize(N); - intersection.geometricNormal = N; - ray_dir_info_t V; - V.setDirection(-rayIntersected.direction); - interaction_type interaction = interaction_type::create(V, N); - interaction.luminosityContributionHint = colorspace::scRGBtoXYZ[1] * rayIntersected.getPayloadThroughput(); - interaction.luminosityContributionHint /= interaction.luminosityContributionHint.r + interaction.luminosityContributionHint.g + interaction.luminosityContributionHint.b; - intersection.aniso_interaction = Intersection::interaction_type::create(interaction); - return intersection; - } -}; - -const Shape SceneRectangleLight::light_rectangles[1] = { - Shape::create(float3(-3.8,0.35,1.3), normalize(float3(2,0,-1))*7.0, normalize(float3(2,-5,4))*0.1, SceneBase::SCENE_BXDF_COUNT-1u, 1u) -}; - -using scene_type = SceneRectangleLight; - -NBL_CONSTEXPR ProceduralShapeType LIGHT_TYPE = PST_RECTANGLE; -using light_type = Light; - -// light id 0 is reserved for env light -// however, we start indexing light array without env light, so index 0 is first shape light -// use constant indices because with variables, driver (at least nvidia) seemed to nuke the light array and propagated constants throughout the code -// which caused frame times to increase from 16ms to 85ms -static const light_type lights[scene_type::SCENE_LIGHT_COUNT] = { - // imaginary index env light 0 here, - light_type::create(SceneBase::SCENE_BXDF_COUNT-1u, 0u, LIGHT_TYPE) -}; - -#endif diff --git a/31_HLSLPathTracer/app_resources/hlsl/scene_sphere_light.hlsl b/31_HLSLPathTracer/app_resources/hlsl/scene_sphere_light.hlsl deleted file mode 100644 index 17ea519c6..000000000 --- a/31_HLSLPathTracer/app_resources/hlsl/scene_sphere_light.hlsl +++ /dev/null @@ -1,100 +0,0 @@ -#ifndef _PATHTRACER_EXAMPLE_SCENE_SPHERE_LIGHT_INCLUDED_ -#define _PATHTRACER_EXAMPLE_SCENE_SPHERE_LIGHT_INCLUDED_ - -#include "scene_base.hlsl" - -using namespace nbl; -using namespace hlsl; - -struct SceneSphereLight : SceneBase -{ - using scalar_type = float; - using vector3_type = vector; - using this_t = SceneSphereLight; - using base_t = SceneBase; - using object_handle_type = ObjectID; - using mat_light_id_type = base_t::mat_light_id_type; - - using ray_dir_info_t = bxdf::ray_dir_info::SBasic; - using interaction_type = PTIsotropicInteraction; - - NBL_CONSTEXPR_STATIC_INLINE uint32_t SphereCount = base_t::SCENE_SPHERE_COUNT + base_t::SCENE_LIGHT_COUNT; - NBL_CONSTEXPR_STATIC_INLINE uint32_t TriangleCount = 0u; - NBL_CONSTEXPR_STATIC_INLINE uint32_t RectangleCount = 0u; - - static const Shape light_spheres[1]; - - Shape getSphere(uint32_t idx) - { - assert(idx < SphereCount); - if (idx < base_t::SCENE_SPHERE_COUNT) - return base_t::scene_spheres[idx]; - else - return light_spheres[idx-base_t::SCENE_SPHERE_COUNT]; - } - - Shape getTriangle(uint32_t idx) - { - assert(false); - Shape dummy; - return dummy; - } - - Shape getRectangle(uint32_t idx) - { - assert(false); - Shape dummy; - return dummy; - } - - void updateLight(NBL_CONST_REF_ARG(float32_t3x4) generalPurposeLightMatrix) - { - light_spheres[0].updateTransform(generalPurposeLightMatrix); - } - - mat_light_id_type getMatLightIDs(NBL_CONST_REF_ARG(object_handle_type) objectID) - { - assert(objectID.shapeType == PST_SPHERE); - return mat_light_id_type::createFromPacked(getSphere(objectID.id).bsdfLightIDs); - } - - template - Intersection getIntersection(NBL_CONST_REF_ARG(object_handle_type) objectID, NBL_CONST_REF_ARG(Ray) rayIntersected) - { - assert(objectID.shapeType == PST_SPHERE); - Intersection intersection; - intersection.objectID = objectID; - intersection.position = rayIntersected.origin + rayIntersected.direction * rayIntersected.intersectionT; - - vector3_type N = getSphere(objectID.id).getNormal(intersection.position); - N = hlsl::normalize(N); - intersection.geometricNormal = N; - ray_dir_info_t V; - V.setDirection(-rayIntersected.direction); - interaction_type interaction = interaction_type::create(V, N); - interaction.luminosityContributionHint = colorspace::scRGBtoXYZ[1] * rayIntersected.getPayloadThroughput(); - interaction.luminosityContributionHint /= interaction.luminosityContributionHint.r + interaction.luminosityContributionHint.g + interaction.luminosityContributionHint.b; - intersection.aniso_interaction = Intersection::interaction_type::create(interaction); - return intersection; - } -}; - -const Shape SceneSphereLight::light_spheres[1] = { - Shape::create(float3(-1.5, 1.5, 0.0), 0.3, SceneBase::SCENE_BXDF_COUNT-1u/*last in mat arr*/, 1u) -}; - -using scene_type = SceneSphereLight; - -NBL_CONSTEXPR ProceduralShapeType LIGHT_TYPE = PST_SPHERE; -using light_type = Light; - -// light id 0 is reserved for env light -// however, we start indexing light array without env light, so index 0 is first shape light -// use constant indices because with variables, driver (at least nvidia) seemed to nuke the light array and propagated constants throughout the code -// which caused frame times to increase from 16ms to 85ms -static const light_type lights[scene_type::SCENE_LIGHT_COUNT] = { - // imaginary index env light 0 here, - light_type::create(SceneBase::SCENE_BXDF_COUNT-1u/*last in mat arr*/, scene_type::SCENE_SPHERE_COUNT, LIGHT_TYPE) -}; - -#endif diff --git a/31_HLSLPathTracer/app_resources/hlsl/scene_triangle_light.hlsl b/31_HLSLPathTracer/app_resources/hlsl/scene_triangle_light.hlsl deleted file mode 100644 index 92edfe5cd..000000000 --- a/31_HLSLPathTracer/app_resources/hlsl/scene_triangle_light.hlsl +++ /dev/null @@ -1,97 +0,0 @@ -#ifndef _PATHTRACER_EXAMPLE_SCENE_TRIANGLE_LIGHT_INCLUDED_ -#define _PATHTRACER_EXAMPLE_SCENE_TRIANGLE_LIGHT_INCLUDED_ - -#include "scene_base.hlsl" - -using namespace nbl; -using namespace hlsl; - -struct SceneTriangleLight : SceneBase -{ - using scalar_type = float; - using vector3_type = vector; - using this_t = SceneTriangleLight; - using base_t = SceneBase; - using object_handle_type = ObjectID; - using mat_light_id_type = base_t::mat_light_id_type; - - using ray_dir_info_t = bxdf::ray_dir_info::SBasic; - using interaction_type = PTIsotropicInteraction; - - NBL_CONSTEXPR_STATIC_INLINE uint32_t SphereCount = base_t::SCENE_SPHERE_COUNT; - NBL_CONSTEXPR_STATIC_INLINE uint32_t TriangleCount = base_t::SCENE_LIGHT_COUNT; - NBL_CONSTEXPR_STATIC_INLINE uint32_t RectangleCount = 0u; - - static const Shape light_triangles[1]; - - Shape getSphere(uint32_t idx) - { - assert(idx < SphereCount); - return base_t::scene_spheres[idx]; - } - Shape getTriangle(uint32_t idx) - { - assert(idx < TriangleCount); - return light_triangles[idx]; - } - Shape getRectangle(uint32_t idx) - { - assert(false); - Shape dummy; - return dummy; - } - - void updateLight(NBL_CONST_REF_ARG(float32_t3x4) generalPurposeLightMatrix) - { - light_triangles[0].updateTransform(generalPurposeLightMatrix); - } - - mat_light_id_type getMatLightIDs(NBL_CONST_REF_ARG(object_handle_type) objectID) - { - assert(objectID.shapeType == PST_SPHERE || objectID.shapeType == PST_TRIANGLE); - if (objectID.shapeType == PST_SPHERE) - return mat_light_id_type::createFromPacked(getSphere(objectID.id).bsdfLightIDs); - else - return mat_light_id_type::createFromPacked(getTriangle(objectID.id).bsdfLightIDs); - } - - template - Intersection getIntersection(NBL_CONST_REF_ARG(object_handle_type) objectID, NBL_CONST_REF_ARG(Ray) rayIntersected) - { - assert(objectID.shapeType == PST_SPHERE || objectID.shapeType == PST_TRIANGLE); - Intersection intersection; - intersection.objectID = objectID; - intersection.position = rayIntersected.origin + rayIntersected.direction * rayIntersected.intersectionT; - - vector3_type N = objectID.shapeType == PST_SPHERE ? getSphere(objectID.id).getNormal(intersection.position) : getTriangle(objectID.id).getNormalTimesArea(); - N = hlsl::normalize(N); - intersection.geometricNormal = N; - ray_dir_info_t V; - V.setDirection(-rayIntersected.direction); - interaction_type interaction = interaction_type::create(V, N); - interaction.luminosityContributionHint = colorspace::scRGBtoXYZ[1] * rayIntersected.getPayloadThroughput(); - interaction.luminosityContributionHint /= interaction.luminosityContributionHint.r + interaction.luminosityContributionHint.g + interaction.luminosityContributionHint.b; - intersection.aniso_interaction = Intersection::interaction_type::create(interaction); - return intersection; - } -}; - -const Shape SceneTriangleLight::light_triangles[1] = { - Shape::create(float3(-1.8,0.35,0.3) * 10.0, float3(-1.2,0.35,0.0) * 10.0, float3(-1.5,0.8,-0.3) * 10.0, SceneBase::SCENE_BXDF_COUNT-1u, 1u) -}; - -using scene_type = SceneTriangleLight; - -NBL_CONSTEXPR ProceduralShapeType LIGHT_TYPE = PST_TRIANGLE; -using light_type = Light; - -// light id 0 is reserved for env light -// however, we start indexing light array without env light, so index 0 is first shape light -// use constant indices because with variables, driver (at least nvidia) seemed to nuke the light array and propagated constants throughout the code -// which caused frame times to increase from 16ms to 85ms -static const light_type lights[scene_type::SCENE_LIGHT_COUNT] = { - // imaginary index env light 0 here, - light_type::create(SceneBase::SCENE_BXDF_COUNT-1u, 0u, LIGHT_TYPE) -}; - -#endif diff --git a/31_HLSLPathTracer/app_resources/hlsl/spirv/pt.compute.rectangle.proxy.hlsl b/31_HLSLPathTracer/app_resources/hlsl/spirv/pt.compute.rectangle.proxy.hlsl deleted file mode 100644 index 8e04bcfc1..000000000 --- a/31_HLSLPathTracer/app_resources/hlsl/spirv/pt.compute.rectangle.proxy.hlsl +++ /dev/null @@ -1 +0,0 @@ -#include "pt.compute.variant.shared.hlsl" diff --git a/31_HLSLPathTracer/app_resources/hlsl/spirv/pt.compute.rectangle.rwmc.persistent.proxy.hlsl b/31_HLSLPathTracer/app_resources/hlsl/spirv/pt.compute.rectangle.rwmc.persistent.proxy.hlsl deleted file mode 100644 index 8e04bcfc1..000000000 --- a/31_HLSLPathTracer/app_resources/hlsl/spirv/pt.compute.rectangle.rwmc.persistent.proxy.hlsl +++ /dev/null @@ -1 +0,0 @@ -#include "pt.compute.variant.shared.hlsl" diff --git a/31_HLSLPathTracer/app_resources/hlsl/spirv/pt.compute.sphere.proxy.hlsl b/31_HLSLPathTracer/app_resources/hlsl/spirv/pt.compute.sphere.proxy.hlsl deleted file mode 100644 index 8e04bcfc1..000000000 --- a/31_HLSLPathTracer/app_resources/hlsl/spirv/pt.compute.sphere.proxy.hlsl +++ /dev/null @@ -1 +0,0 @@ -#include "pt.compute.variant.shared.hlsl" diff --git a/31_HLSLPathTracer/app_resources/hlsl/spirv/pt.compute.sphere.rwmc.proxy.hlsl b/31_HLSLPathTracer/app_resources/hlsl/spirv/pt.compute.sphere.rwmc.proxy.hlsl deleted file mode 100644 index 8e04bcfc1..000000000 --- a/31_HLSLPathTracer/app_resources/hlsl/spirv/pt.compute.sphere.rwmc.proxy.hlsl +++ /dev/null @@ -1 +0,0 @@ -#include "pt.compute.variant.shared.hlsl" diff --git a/31_HLSLPathTracer/app_resources/hlsl/spirv/pt.compute.triangle.methods.shared.hlsl b/31_HLSLPathTracer/app_resources/hlsl/spirv/pt.compute.triangle.methods.shared.hlsl deleted file mode 100644 index 5bb76a3e1..000000000 --- a/31_HLSLPathTracer/app_resources/hlsl/spirv/pt.compute.triangle.methods.shared.hlsl +++ /dev/null @@ -1,71 +0,0 @@ -#pragma once - -#if !defined(PT_VARIANT_USE_RWMC) || !defined(PT_VARIANT_ENTRYPOINT_KIND) -#error Missing triangle method compile options -#endif - -#define PT_VARIANT_ENTRYPOINT_LINEAR 1 -#define PT_VARIANT_ENTRYPOINT_PERSISTENT 2 -#if PT_VARIANT_ENTRYPOINT_KIND == PT_VARIANT_ENTRYPOINT_LINEAR -#define PATH_TRACER_ENABLE_LINEAR 1 -#define PATH_TRACER_ENABLE_PERSISTENT 0 -#elif PT_VARIANT_ENTRYPOINT_KIND == PT_VARIANT_ENTRYPOINT_PERSISTENT -#define PATH_TRACER_ENABLE_LINEAR 0 -#define PATH_TRACER_ENABLE_PERSISTENT 1 -#else -#error Unsupported PT_VARIANT_ENTRYPOINT_KIND -#endif - -#if PT_VARIANT_USE_RWMC -#define PATH_TRACER_VARIANT_USE_RWMC true -#else -#define PATH_TRACER_VARIANT_USE_RWMC false -#endif - -#define PATH_TRACER_VARIANT_GEOMETRY ELG_TRIANGLE -#define PATH_TRACER_VARIANT_POLYGON_METHOD EPM_PROJECTED_SOLID_ANGLE -#define PATH_TRACER_VARIANT_ENTRYPOINT_POLYGON_METHOD PPM_APPROX_PROJECTED_SOLID_ANGLE -#define PATH_TRACER_VARIANT_ENABLE_LINEAR PATH_TRACER_ENABLE_LINEAR -#define PATH_TRACER_VARIANT_ENABLE_PERSISTENT PATH_TRACER_ENABLE_PERSISTENT -#define PATH_TRACER_USE_RWMC PT_VARIANT_USE_RWMC - -#include "compute.render.common.hlsl" -#include "nbl/this_example/render_variant_config.hlsl" -#include "scene_triangle_light.hlsl" -#include "compute_render_scene_impl.hlsl" - -#if PT_VARIANT_ENTRYPOINT_KIND == PT_VARIANT_ENTRYPOINT_LINEAR -#define PATH_TRACER_ENTRYPOINT_NAME main -#define PATH_TRACER_ENTRYPOINT_POLYGON_METHOD PPM_APPROX_PROJECTED_SOLID_ANGLE -#include "compute.render.linear.entrypoints.hlsl" - -#define PATH_TRACER_ENTRYPOINT_NAME mainArea -#define PATH_TRACER_ENTRYPOINT_POLYGON_METHOD PPM_AREA -#include "compute.render.linear.entrypoints.hlsl" - -#define PATH_TRACER_ENTRYPOINT_NAME mainSolidAngle -#define PATH_TRACER_ENTRYPOINT_POLYGON_METHOD PPM_SOLID_ANGLE -#include "compute.render.linear.entrypoints.hlsl" -#endif - -#if PT_VARIANT_ENTRYPOINT_KIND == PT_VARIANT_ENTRYPOINT_PERSISTENT -#define PATH_TRACER_ENTRYPOINT_NAME mainPersistent -#define PATH_TRACER_ENTRYPOINT_POLYGON_METHOD PPM_APPROX_PROJECTED_SOLID_ANGLE -#include "compute.render.persistent.entrypoints.hlsl" - -#define PATH_TRACER_ENTRYPOINT_NAME mainPersistentArea -#define PATH_TRACER_ENTRYPOINT_POLYGON_METHOD PPM_AREA -#include "compute.render.persistent.entrypoints.hlsl" - -#define PATH_TRACER_ENTRYPOINT_NAME mainPersistentSolidAngle -#define PATH_TRACER_ENTRYPOINT_POLYGON_METHOD PPM_SOLID_ANGLE -#include "compute.render.persistent.entrypoints.hlsl" -#endif - -#undef PATH_TRACER_VARIANT_ENABLE_PERSISTENT -#undef PATH_TRACER_VARIANT_ENABLE_LINEAR -#undef PATH_TRACER_VARIANT_ENTRYPOINT_POLYGON_METHOD -#undef PATH_TRACER_VARIANT_POLYGON_METHOD -#undef PATH_TRACER_VARIANT_GEOMETRY -#undef PATH_TRACER_VARIANT_USE_RWMC -#undef PATH_TRACER_USE_RWMC diff --git a/31_HLSLPathTracer/app_resources/hlsl/spirv/pt.compute.triangle.persistent.proxy.hlsl b/31_HLSLPathTracer/app_resources/hlsl/spirv/pt.compute.triangle.persistent.proxy.hlsl deleted file mode 100644 index 5035e960d..000000000 --- a/31_HLSLPathTracer/app_resources/hlsl/spirv/pt.compute.triangle.persistent.proxy.hlsl +++ /dev/null @@ -1 +0,0 @@ -#include "pt.compute.triangle.methods.shared.hlsl" diff --git a/31_HLSLPathTracer/app_resources/hlsl/spirv/pt.compute.triangle.rwmc.persistent.proxy.hlsl b/31_HLSLPathTracer/app_resources/hlsl/spirv/pt.compute.triangle.rwmc.persistent.proxy.hlsl deleted file mode 100644 index 5035e960d..000000000 --- a/31_HLSLPathTracer/app_resources/hlsl/spirv/pt.compute.triangle.rwmc.persistent.proxy.hlsl +++ /dev/null @@ -1 +0,0 @@ -#include "pt.compute.triangle.methods.shared.hlsl" diff --git a/31_HLSLPathTracer/app_resources/hlsl/spirv/pt.compute.variant.shared.hlsl b/31_HLSLPathTracer/app_resources/hlsl/spirv/pt.compute.variant.shared.hlsl deleted file mode 100644 index c1062391a..000000000 --- a/31_HLSLPathTracer/app_resources/hlsl/spirv/pt.compute.variant.shared.hlsl +++ /dev/null @@ -1,55 +0,0 @@ -#pragma once - -#if !defined(PT_VARIANT_USE_RWMC) || !defined(PT_VARIANT_SCENE_KIND) -#error Missing path tracer variant compile options -#endif - -#define PT_VARIANT_SCENE_SPHERE 0 -#define PT_VARIANT_SCENE_RECTANGLE 2 -#define PATH_TRACER_ENABLE_LINEAR 0 -#define PATH_TRACER_ENABLE_PERSISTENT 1 - -#if PT_VARIANT_USE_RWMC -#define PATH_TRACER_VARIANT_USE_RWMC true -#else -#define PATH_TRACER_VARIANT_USE_RWMC false -#endif - -#define PATH_TRACER_VARIANT_ENABLE_LINEAR PATH_TRACER_ENABLE_LINEAR -#define PATH_TRACER_VARIANT_ENABLE_PERSISTENT PATH_TRACER_ENABLE_PERSISTENT -#define PATH_TRACER_USE_RWMC PT_VARIANT_USE_RWMC - -#include "compute.render.common.hlsl" -#if PT_VARIANT_SCENE_KIND == PT_VARIANT_SCENE_SPHERE -#define PATH_TRACER_VARIANT_GEOMETRY ELG_SPHERE -#define PATH_TRACER_VARIANT_POLYGON_METHOD EPM_SOLID_ANGLE -#define PATH_TRACER_VARIANT_ENTRYPOINT_POLYGON_METHOD PPM_SOLID_ANGLE -#define PATH_TRACER_ENTRYPOINT_POLYGON_METHOD PPM_SOLID_ANGLE -#include "scene_sphere_light.hlsl" -#elif PT_VARIANT_SCENE_KIND == PT_VARIANT_SCENE_RECTANGLE -#define PATH_TRACER_VARIANT_GEOMETRY ELG_RECTANGLE -#define PATH_TRACER_VARIANT_POLYGON_METHOD EPM_SOLID_ANGLE -#define PATH_TRACER_VARIANT_ENTRYPOINT_POLYGON_METHOD PPM_SOLID_ANGLE -#define PATH_TRACER_ENTRYPOINT_POLYGON_METHOD PPM_SOLID_ANGLE -#include "scene_rectangle_light.hlsl" -#else -#error Unsupported PT_VARIANT_SCENE_KIND -#endif -#include "nbl/this_example/render_variant_config.hlsl" -#include "compute_render_scene_impl.hlsl" - -#include "compute.render.persistent.entrypoints.hlsl" -#if PT_VARIANT_SCENE_KIND == PT_VARIANT_SCENE_RECTANGLE -#define PATH_TRACER_ENTRYPOINT_NAME mainPersistentArea -#define PATH_TRACER_ENTRYPOINT_POLYGON_METHOD PPM_AREA -#include "compute.render.persistent.entrypoints.hlsl" -#endif - -#undef PATH_TRACER_ENTRYPOINT_POLYGON_METHOD -#undef PATH_TRACER_USE_RWMC -#undef PATH_TRACER_VARIANT_ENTRYPOINT_POLYGON_METHOD -#undef PATH_TRACER_VARIANT_POLYGON_METHOD -#undef PATH_TRACER_VARIANT_GEOMETRY -#undef PATH_TRACER_VARIANT_ENABLE_PERSISTENT -#undef PATH_TRACER_VARIANT_ENABLE_LINEAR -#undef PATH_TRACER_VARIANT_USE_RWMC diff --git a/31_HLSLPathTracer/app_resources/hlsl/spirv/pt.misc.proxy.hlsl b/31_HLSLPathTracer/app_resources/hlsl/spirv/pt.misc.proxy.hlsl deleted file mode 100644 index 8a2882574..000000000 --- a/31_HLSLPathTracer/app_resources/hlsl/spirv/pt.misc.proxy.hlsl +++ /dev/null @@ -1,5 +0,0 @@ -#include "present.frag.hlsl" - -#define pc ex31_imgui_pc -#include "imgui.unified.hlsl" -#undef pc diff --git a/31_HLSLPathTracer/include/nbl/this_example/common.hpp b/31_HLSLPathTracer/include/nbl/this_example/common.hpp deleted file mode 100644 index db051bb3e..000000000 --- a/31_HLSLPathTracer/include/nbl/this_example/common.hpp +++ /dev/null @@ -1,17 +0,0 @@ -#ifndef __NBL_THIS_EXAMPLE_COMMON_H_INCLUDED__ -#define __NBL_THIS_EXAMPLE_COMMON_H_INCLUDED__ - -#include - -// common api -#include "nbl/examples/common/SimpleWindowedApplication.hpp" -#include "nbl/examples/examples.hpp" -#include "nbl/examples/cameras/CCamera.hpp" -#include "nbl/examples/common/CEventCallback.hpp" - -// example's own headers -#include "nbl/ui/ICursorControl.h" -#include "nbl/ext/ImGui/ImGui.h" -#include "imgui/imgui_internal.h" - -#endif // __NBL_THIS_EXAMPLE_COMMON_H_INCLUDED__ \ No newline at end of file diff --git a/31_HLSLPathTracer/include/nbl/this_example/path_tracer_pipeline_state.hpp b/31_HLSLPathTracer/include/nbl/this_example/path_tracer_pipeline_state.hpp deleted file mode 100644 index 83186f46a..000000000 --- a/31_HLSLPathTracer/include/nbl/this_example/path_tracer_pipeline_state.hpp +++ /dev/null @@ -1,26 +0,0 @@ -#ifndef __NBL_THIS_EXAMPLE_PATH_TRACER_PIPELINE_STATE_HPP_INCLUDED__ -#define __NBL_THIS_EXAMPLE_PATH_TRACER_PIPELINE_STATE_HPP_INCLUDED__ - -#include "nbl/examples/common/CachedPipelineState.hpp" -#include "nbl/this_example/render_variant_enums.hlsl" - -namespace nbl::this_example -{ -using pipeline_future_t = examples::common::pipeline_future_t; -using cached_pipeline_storage_traits_t = examples::common::SRenderPipelineStorage; -using shader_array_t = typename cached_pipeline_storage_traits_t::shader_array_t; -using pipeline_method_array_t = typename cached_pipeline_storage_traits_t::pipeline_method_array_t; -using pipeline_future_method_array_t = typename cached_pipeline_storage_traits_t::pipeline_future_method_array_t; -using pipeline_array_t = typename cached_pipeline_storage_traits_t::pipeline_array_t; -using pipeline_future_array_t = typename cached_pipeline_storage_traits_t::pipeline_future_array_t; - -template -using SRenderPipelineStorage = examples::common::SRenderPipelineStorage; - -using SResolvePipelineState = examples::common::SResolvePipelineState; -using SWarmupJob = examples::common::SWarmupJob; -using SPipelineCacheState = examples::common::SPipelineCacheState; -using SStartupLogState = examples::common::SStartupLogState; -} - -#endif diff --git a/31_HLSLPathTracer/include/nbl/this_example/path_tracer_ui.hpp b/31_HLSLPathTracer/include/nbl/this_example/path_tracer_ui.hpp deleted file mode 100644 index a9cc6c084..000000000 --- a/31_HLSLPathTracer/include/nbl/this_example/path_tracer_ui.hpp +++ /dev/null @@ -1,137 +0,0 @@ -#ifndef __NBL_THIS_EXAMPLE_PATH_TRACER_UI_HPP_INCLUDED__ -#define __NBL_THIS_EXAMPLE_PATH_TRACER_UI_HPP_INCLUDED__ - -#include "nbl/this_example/common.hpp" - -#include -#include -#include -#include - -namespace nbl::this_example::pt_ui -{ -struct SFloatSliderRow -{ - const char* label; - float* value; - float min; - float max; - const char* format; -}; - -struct SIntSliderRow -{ - const char* label; - int* value; - int min; - int max; -}; - -struct SCheckboxRow -{ - const char* label; - bool* value; -}; - -struct SComboRow -{ - const char* label; - int* value; - const char* const* items; - int count; -}; - -struct STextRow -{ - const char* label; - std::string value; -}; - -template -inline float calcMaxTextWidth(const Range& items, ToText&& toText) -{ - float width = 0.f; - for (const auto& item : items) - width = std::max(width, ImGui::CalcTextSize(toText(item)).x); - return width; -} - -inline std::string makeReadyText(const size_t ready, const size_t total) -{ - return std::to_string(ready) + "/" + std::to_string(total); -} - -inline std::string makeRunQueueText(const size_t running, const size_t queued) -{ - return std::to_string(running) + " / " + std::to_string(queued); -} - -inline bool beginSectionTable(const char* id) -{ - return ImGui::BeginTable(id, 2, ImGuiTableFlags_SizingFixedFit); -} - -inline void setupSectionTable(const float tableLabelColumnWidth) -{ - ImGui::TableSetupColumn("label", ImGuiTableColumnFlags_WidthFixed, tableLabelColumnWidth); - ImGui::TableSetupColumn("value", ImGuiTableColumnFlags_WidthStretch); -} - -inline void sliderFloatRow(const SFloatSliderRow& row) -{ - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::TextUnformatted(row.label); - ImGui::TableSetColumnIndex(1); - ImGui::SetNextItemWidth(-FLT_MIN); - ImGui::PushID(row.label); - ImGui::SliderFloat("##value", row.value, row.min, row.max, row.format, ImGuiSliderFlags_AlwaysClamp); - ImGui::PopID(); -} - -inline void sliderIntRow(const SIntSliderRow& row) -{ - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::TextUnformatted(row.label); - ImGui::TableSetColumnIndex(1); - ImGui::SetNextItemWidth(-FLT_MIN); - ImGui::PushID(row.label); - ImGui::SliderInt("##value", row.value, row.min, row.max); - ImGui::PopID(); -} - -inline void comboRow(const SComboRow& row) -{ - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::TextUnformatted(row.label); - ImGui::TableSetColumnIndex(1); - ImGui::SetNextItemWidth(-FLT_MIN); - ImGui::PushID(row.label); - ImGui::Combo("##value", row.value, row.items, row.count); - ImGui::PopID(); -} - -inline void checkboxRow(const SCheckboxRow& row) -{ - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::TextUnformatted(row.label); - ImGui::TableSetColumnIndex(1); - ImGui::PushID(row.label); - ImGui::Checkbox("##value", row.value); - ImGui::PopID(); -} - -inline void textRow(const STextRow& row) -{ - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::TextUnformatted(row.label); - ImGui::TableSetColumnIndex(1); - ImGui::TextUnformatted(row.value.c_str()); -} -} - -#endif diff --git a/31_HLSLPathTracer/include/nbl/this_example/render_variant_config.hlsl b/31_HLSLPathTracer/include/nbl/this_example/render_variant_config.hlsl deleted file mode 100644 index 8d7bd2dfa..000000000 --- a/31_HLSLPathTracer/include/nbl/this_example/render_variant_config.hlsl +++ /dev/null @@ -1,40 +0,0 @@ -#ifndef __NBL_THIS_EXAMPLE_RENDER_VARIANT_CONFIG_HLSL_INCLUDED__ -#define __NBL_THIS_EXAMPLE_RENDER_VARIANT_CONFIG_HLSL_INCLUDED__ - -#include "nbl/this_example/render_variant_enums.hlsl" - -#ifndef PATH_TRACER_VARIANT_GEOMETRY -#error PATH_TRACER_VARIANT_GEOMETRY must be defined before including render_variant_config.hlsl -#endif - -#ifndef PATH_TRACER_VARIANT_POLYGON_METHOD -#error PATH_TRACER_VARIANT_POLYGON_METHOD must be defined before including render_variant_config.hlsl -#endif - -#ifndef PATH_TRACER_VARIANT_ENTRYPOINT_POLYGON_METHOD -#error PATH_TRACER_VARIANT_ENTRYPOINT_POLYGON_METHOD must be defined before including render_variant_config.hlsl -#endif - -#ifndef PATH_TRACER_VARIANT_USE_RWMC -#error PATH_TRACER_VARIANT_USE_RWMC must be defined before including render_variant_config.hlsl -#endif - -#ifndef PATH_TRACER_VARIANT_ENABLE_LINEAR -#error PATH_TRACER_VARIANT_ENABLE_LINEAR must be defined before including render_variant_config.hlsl -#endif - -#ifndef PATH_TRACER_VARIANT_ENABLE_PERSISTENT -#error PATH_TRACER_VARIANT_ENABLE_PERSISTENT must be defined before including render_variant_config.hlsl -#endif - -struct pathtracer_variant_config -{ - NBL_CONSTEXPR_STATIC_INLINE E_LIGHT_GEOMETRY Geometry = PATH_TRACER_VARIANT_GEOMETRY; - NBL_CONSTEXPR_STATIC_INLINE E_POLYGON_METHOD PolygonMethod = PATH_TRACER_VARIANT_POLYGON_METHOD; - NBL_CONSTEXPR_STATIC_INLINE NEEPolygonMethod EntryPointPolygonMethod = PATH_TRACER_VARIANT_ENTRYPOINT_POLYGON_METHOD; - NBL_CONSTEXPR_STATIC_INLINE bool UseRWMC = PATH_TRACER_VARIANT_USE_RWMC; - NBL_CONSTEXPR_STATIC_INLINE bool EnableLinear = PATH_TRACER_VARIANT_ENABLE_LINEAR; - NBL_CONSTEXPR_STATIC_INLINE bool EnablePersistent = PATH_TRACER_VARIANT_ENABLE_PERSISTENT; -}; - -#endif diff --git a/31_HLSLPathTracer/include/nbl/this_example/render_variant_enums.hlsl b/31_HLSLPathTracer/include/nbl/this_example/render_variant_enums.hlsl deleted file mode 100644 index dc5fc52db..000000000 --- a/31_HLSLPathTracer/include/nbl/this_example/render_variant_enums.hlsl +++ /dev/null @@ -1,28 +0,0 @@ -#ifndef __NBL_THIS_EXAMPLE_RENDER_VARIANT_ENUMS_HLSL_INCLUDED__ -#define __NBL_THIS_EXAMPLE_RENDER_VARIANT_ENUMS_HLSL_INCLUDED__ - -#ifndef __HLSL_VERSION -enum E_LIGHT_GEOMETRY : uint8_t -#else -enum E_LIGHT_GEOMETRY -#endif -{ - ELG_SPHERE, - ELG_TRIANGLE, - ELG_RECTANGLE, - ELG_COUNT -}; - -#ifndef __HLSL_VERSION -enum E_POLYGON_METHOD : uint8_t -#else -enum E_POLYGON_METHOD -#endif -{ - EPM_AREA, - EPM_SOLID_ANGLE, - EPM_PROJECTED_SOLID_ANGLE, - EPM_COUNT -}; - -#endif diff --git a/31_HLSLPathTracer/include/nbl/this_example/render_variant_info.hpp b/31_HLSLPathTracer/include/nbl/this_example/render_variant_info.hpp deleted file mode 100644 index 1bc2893ef..000000000 --- a/31_HLSLPathTracer/include/nbl/this_example/render_variant_info.hpp +++ /dev/null @@ -1,55 +0,0 @@ -#ifndef __NBL_THIS_EXAMPLE_RENDER_VARIANT_INFO_HPP_INCLUDED__ -#define __NBL_THIS_EXAMPLE_RENDER_VARIANT_INFO_HPP_INCLUDED__ - -#include "nbl/this_example/render_variant_enums.hlsl" - -namespace nbl::this_example -{ -struct SRenderVariantInfo -{ - E_POLYGON_METHOD effectiveMethod; - E_POLYGON_METHOD pipelineMethod; - const char* entryPoint; -}; - -static constexpr const char* getDefaultRenderEntryPointName(const bool) -{ - return "mainPersistent"; -} - -static constexpr SRenderVariantInfo getRenderVariantInfo(const E_LIGHT_GEOMETRY geometry, const bool persistentWorkGroups, const E_POLYGON_METHOD requestedMethod) -{ - const char* const defaultEntryPoint = getDefaultRenderEntryPointName(persistentWorkGroups); - switch (geometry) - { - case ELG_SPHERE: - return { EPM_SOLID_ANGLE, EPM_SOLID_ANGLE, defaultEntryPoint }; - case ELG_TRIANGLE: - switch (requestedMethod) - { - case EPM_AREA: - return { EPM_AREA, EPM_AREA, "mainPersistentArea" }; - case EPM_SOLID_ANGLE: - return { EPM_SOLID_ANGLE, EPM_SOLID_ANGLE, "mainPersistentSolidAngle" }; - case EPM_PROJECTED_SOLID_ANGLE: - default: - return { EPM_PROJECTED_SOLID_ANGLE, EPM_PROJECTED_SOLID_ANGLE, defaultEntryPoint }; - } - case ELG_RECTANGLE: - switch (requestedMethod) - { - case EPM_AREA: - return { EPM_AREA, EPM_AREA, "mainPersistentArea" }; - case EPM_SOLID_ANGLE: - return { EPM_SOLID_ANGLE, EPM_SOLID_ANGLE, defaultEntryPoint }; - case EPM_PROJECTED_SOLID_ANGLE: - default: - return { EPM_SOLID_ANGLE, EPM_SOLID_ANGLE, defaultEntryPoint }; - } - default: - return { EPM_PROJECTED_SOLID_ANGLE, EPM_PROJECTED_SOLID_ANGLE, defaultEntryPoint }; - } -} -} - -#endif diff --git a/31_HLSLPathTracer/include/nbl/this_example/render_variant_strings.hpp b/31_HLSLPathTracer/include/nbl/this_example/render_variant_strings.hpp deleted file mode 100644 index 0d7434758..000000000 --- a/31_HLSLPathTracer/include/nbl/this_example/render_variant_strings.hpp +++ /dev/null @@ -1,98 +0,0 @@ -#ifndef __NBL_THIS_EXAMPLE_RENDER_VARIANT_STRINGS_HPP_INCLUDED__ -#define __NBL_THIS_EXAMPLE_RENDER_VARIANT_STRINGS_HPP_INCLUDED__ - -#include -#include - -#include "nbl/system/to_string.h" -#include "nbl/this_example/render_variant_enums.hlsl" - -namespace nbl::system::impl -{ -template<> -struct to_string_helper -{ - static inline std::string __call(const E_LIGHT_GEOMETRY value) - { - switch (value) - { - case ELG_SPHERE: - return "ELG_SPHERE"; - case ELG_TRIANGLE: - return "ELG_TRIANGLE"; - case ELG_RECTANGLE: - return "ELG_RECTANGLE"; - default: - return "ERROR (geometry)"; - } - } -}; - -template<> -struct to_string_helper -{ - static inline std::string __call(const E_POLYGON_METHOD value) - { - switch (value) - { - case EPM_AREA: - return "Area"; - case EPM_SOLID_ANGLE: - return "Solid Angle"; - case EPM_PROJECTED_SOLID_ANGLE: - return "Projected Solid Angle"; - default: - return "ERROR (method)"; - } - } -}; -} - -namespace nbl::this_example -{ -inline const auto& getLightGeometryNameStorage() -{ - static const auto names = std::to_array({ - system::to_string(ELG_SPHERE), - system::to_string(ELG_TRIANGLE), - system::to_string(ELG_RECTANGLE) - }); - return names; -} - -inline const auto& getLightGeometryNamePointers() -{ - static const auto ptrs = [] { - std::array retval = {}; - const auto& names = getLightGeometryNameStorage(); - for (size_t i = 0u; i < names.size(); ++i) - retval[i] = names[i].c_str(); - return retval; - }(); - return ptrs; -} - -inline const auto& getPolygonMethodNameStorage() -{ - static const auto names = std::to_array({ - system::to_string(EPM_AREA), - system::to_string(EPM_SOLID_ANGLE), - system::to_string(EPM_PROJECTED_SOLID_ANGLE) - }); - return names; -} - -inline const auto& getPolygonMethodNamePointers() -{ - static const auto ptrs = [] { - std::array retval = {}; - const auto& names = getPolygonMethodNameStorage(); - for (size_t i = 0u; i < names.size(); ++i) - retval[i] = names[i].c_str(); - return retval; - }(); - return ptrs; -} -} - -#endif diff --git a/31_HLSLPathTracer/include/nbl/this_example/transform.hpp b/31_HLSLPathTracer/include/nbl/this_example/transform.hpp deleted file mode 100644 index dd6368ca1..000000000 --- a/31_HLSLPathTracer/include/nbl/this_example/transform.hpp +++ /dev/null @@ -1,167 +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; - bool isSphere = false; - ImGuizmo::OPERATION allowedOp; - uint8_t sceneTexDescIx = ~0; - bool useWindow = false, 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)) // Always translate - mCurrentGizmoOperation = ImGuizmo::TRANSLATE; - if (ImGui::IsKeyPressed(ImGuiKey_R) && params.allowedOp & ImGuizmo::OPERATION::ROTATE) - mCurrentGizmoOperation = ImGuizmo::ROTATE; - if (ImGui::IsKeyPressed(ImGuiKey_S) && params.allowedOp & ImGuizmo::OPERATION::SCALEU) // for sphere - mCurrentGizmoOperation = ImGuizmo::SCALEU; - if (ImGui::IsKeyPressed(ImGuiKey_S) && params.allowedOp & ImGuizmo::OPERATION::SCALE) // for triangle/rectangle - mCurrentGizmoOperation = ImGuizmo::SCALE_X | ImGuizmo::SCALE_Y; - -#if 0 - 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(); - } -#endif - } - - 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__ diff --git a/31_HLSLPathTracer/main.cpp b/31_HLSLPathTracer/main.cpp deleted file mode 100644 index d7569e4b8..000000000 --- a/31_HLSLPathTracer/main.cpp +++ /dev/null @@ -1,2946 +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 - -#include "argparse/argparse.hpp" -#include "nbl/examples/examples.hpp" -#include "nbl/examples/common/CCachedOwenScrambledSequence.hpp" -#include "nbl/this_example/path_tracer_pipeline_state.hpp" -#include "nbl/this_example/path_tracer_ui.hpp" -#include "nbl/this_example/render_variant_info.hpp" -#include "nbl/this_example/transform.hpp" -#include "nbl/this_example/render_variant_strings.hpp" -#include "nbl/ext/FullScreenTriangle/FullScreenTriangle.h" -#include "nbl/ext/ScreenShot/ScreenShot.h" - -#include "nbl/builtin/hlsl/math/thin_lens_projection.hlsl" - -#include "nbl/this_example/common.hpp" -#include "nbl/this_example/builtin/build/spirv/keys.hpp" -#include "nbl/builtin/hlsl/colorspace/encodeCIEXYZ.hlsl" -#include "nbl/builtin/hlsl/sampling/quantized_sequence.hlsl" -#include "nbl/asset/utils/ISPIRVEntryPointTrimmer.h" -#include "nbl/system/ModuleLookupUtils.h" -#include "app_resources/hlsl/render_common.hlsl" -#include "app_resources/hlsl/render_rwmc_common.hlsl" -#include "app_resources/hlsl/resolve_common.hlsl" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "nlohmann/json.hpp" - - -using namespace nbl; -using namespace core; -using namespace hlsl; -using namespace system; -using namespace asset; -using namespace ui; -using namespace video; -using namespace nbl::examples; -using namespace nbl::this_example; -namespace cached_pipeline_state = nbl::examples::common; - -// TODO: Add a QueryPool for timestamping once its ready -// TODO: Do buffer creation using assConv -class HLSLComputePathtracer final : public SimpleWindowedApplication, public BuiltinResourcesApplication -{ - using device_base_t = SimpleWindowedApplication; - using asset_base_t = BuiltinResourcesApplication; - using clock_t = std::chrono::steady_clock; - - constexpr static inline uint32_t2 WindowDimensions = { 1280, 720 }; - constexpr static inline uint32_t MaxFramesInFlight = 5; - static constexpr size_t BinaryToggleCount = 2ull; - static constexpr std::string_view BuildConfigName = PATH_TRACER_BUILD_CONFIG_NAME; - static constexpr bool UsePersistentWorkGroups = true; - static constexpr uint32_t CiFramesBeforeCapture = 3u; - static constexpr std::string_view RuntimeConfigFilename = "path_tracer.runtime.json"; - static inline std::string DefaultImagePathsFile = "envmap/envmap_0.exr"; - - public: - inline HLSLComputePathtracer(const path& _localInputCWD, const path& _localOutputCWD, const path& _sharedInputCWD, const path& _sharedOutputCWD) - : IApplicationFramework(_localInputCWD, _localOutputCWD, _sharedInputCWD, _sharedOutputCWD) {} - - inline bool isComputeOnly() const override { return false; } - - inline core::bitflag getLogLevelMask() override - { - return core::bitflag(system::ILogger::ELL_INFO) | system::ILogger::ELL_WARNING | system::ILogger::ELL_PERFORMANCE | system::ILogger::ELL_ERROR; - } - - inline video::SPhysicalDeviceFeatures getPreferredDeviceFeatures() const override - { - auto retval = device_base_t::getPreferredDeviceFeatures(); - retval.pipelineExecutableInfo = true; - return retval; - } - - inline core::vector getSurfaces() const override - { - if (!m_surface) - { - { - auto windowCallback = core::make_smart_refctd_ptr(smart_refctd_ptr(m_inputSystem), smart_refctd_ptr(m_logger)); - IWindow::SCreationParams params = {}; - params.callback = core::make_smart_refctd_ptr(); - params.width = WindowDimensions.x; - params.height = WindowDimensions.y; - params.x = 32; - params.y = 32; - params.flags = ui::IWindow::ECF_HIDDEN | IWindow::ECF_BORDERLESS | IWindow::ECF_RESIZABLE; - params.windowCaption = "ComputeShaderPathtracer"; - params.callback = windowCallback; - 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) = nbl::video::CSimpleResizeSurface::create(std::move(surface)); - } - - if (m_surface) - return { {m_surface->getSurface()/*,EQF_NONE*/} }; - - return {}; - } - - inline bool onAppInitialized(smart_refctd_ptr&& system) override - { - m_startupBeganAt = clock_t::now(); - - // Init systems - { - m_inputSystem = make_smart_refctd_ptr(logger_opt_smart_ptr(smart_refctd_ptr(m_logger))); - - // Remember to call the base class initialization! - if (!device_base_t::onAppInitialized(smart_refctd_ptr(system))) - return false; - if (!asset_base_t::onAppInitialized(std::move(system))) - return false; - - m_semaphore = m_device->createSemaphore(m_realFrameIx); - - if (!m_semaphore) - return logFail("Failed to create semaphore!"); - } - - auto sequenceFuture = std::async(std::launch::async,[this]()->auto - { - return CCachedOwenScrambledSequence::create({ - .cachePath = (sharedOutputCWD/CCachedOwenScrambledSequence::SCreationParams::DefaultFilename).string(), - .assMan = m_assetMgr.get(), - .header = {.maxSamplesLog2 = MaxSamplesLog2,.maxDimensions = 0x6u<(m_surface->getSurface()) }; - if (!swapchainParams.deduceFormat(m_physicalDevice)) - return logFail("Could not choose a Surface Format for the Swapchain!"); - - const static IGPURenderpass::SCreationParams::SSubpassDependency dependencies[] = - { - { - .srcSubpass = IGPURenderpass::SCreationParams::SSubpassDependency::External, - .dstSubpass = 0, - .memoryBarrier = - { - .srcStageMask = asset::PIPELINE_STAGE_FLAGS::COPY_BIT, - .srcAccessMask = asset::ACCESS_FLAGS::TRANSFER_WRITE_BIT, - .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 - } - }, - IGPURenderpass::SCreationParams::DependenciesEnd - }; - - auto scResources = std::make_unique(m_device.get(), swapchainParams.surfaceFormat.format, dependencies); - renderpass = scResources->getRenderpass(); - - if (!renderpass) - return logFail("Failed to create Renderpass!"); - - auto gQueue = getGraphicsQueue(); - if (!m_surface || !m_surface->init(gQueue, std::move(scResources), swapchainParams.sharedParams)) - return logFail("Could not create Window & Surface or initialize the Surface!"); - } - // Create command pool and buffers - { - auto gQueue = getGraphicsQueue(); - m_cmdPool = m_device->createCommandPool(gQueue->getFamilyIndex(), IGPUCommandPool::CREATE_FLAGS::RESET_COMMAND_BUFFER_BIT); - if (!m_cmdPool) - return logFail("Couldn't create Command Pool!"); - - if (!m_cmdPool->createCommandBuffers(IGPUCommandPool::BUFFER_LEVEL::PRIMARY, { m_cmdBufs.data(), MaxFramesInFlight })) - return logFail("Couldn't create Command Buffer!"); - } - { - m_scratchSemaphore = m_device->createSemaphore(0); - if (!m_scratchSemaphore) - return logFail("Could not create Scratch Semaphore"); - m_scratchSemaphore->setObjectDebugName("Scratch Semaphore"); - m_intendedSubmit.queue = getGraphicsQueue(); - m_intendedSubmit.waitSemaphores = {}; - m_intendedSubmit.scratchCommandBuffers = {}; - m_intendedSubmit.scratchSemaphore = { - .semaphore = m_scratchSemaphore.get(), - .value = 0, - .stageMask = PIPELINE_STAGE_FLAGS::ALL_TRANSFER_BITS - }; - } - initializePipelineCache(); - ISampler::SParams samplerParams = { - .AnisotropicFilter = 0 - }; - auto defaultSampler = m_device->createSampler(samplerParams); - - // Create descriptors and pipeline for the pathtracer - { - auto convertDSLayoutCPU2GPU = [&](smart_refctd_ptr cpuLayout) { - auto converter = CAssetConverter::create({ .device = m_device.get() }); - CAssetConverter::SInputs inputs = {}; - inputs.readCache = converter.get(); - inputs.logger = m_logger.get(); - CAssetConverter::SConvertParams params = {}; - params.utilities = m_utils.get(); - - std::get>(inputs.assets) = { &cpuLayout.get(),1 }; - // don't need to assert that we don't need to provide patches since layouts are not patchable - //assert(true); - auto reservation = converter->reserve(inputs); - // the `.value` is just a funny way to make the `smart_refctd_ptr` copyable - auto gpuLayout = reservation.getGPUObjects().front().value; - if (!gpuLayout) { - m_logger->log("Failed to convert %s into an IGPUDescriptorSetLayout handle", ILogger::ELL_ERROR); - std::exit(-1); - } - - return gpuLayout; - }; - auto convertDSCPU2GPU = [&](smart_refctd_ptr cpuDS) { - auto converter = CAssetConverter::create({ .device = m_device.get() }); - CAssetConverter::SInputs inputs = {}; - inputs.readCache = converter.get(); - inputs.logger = m_logger.get(); - CAssetConverter::SConvertParams params = {}; - params.utilities = m_utils.get(); - - std::get>(inputs.assets) = { &cpuDS.get(), 1 }; - // don't need to assert that we don't need to provide patches since layouts are not patchable - //assert(true); - auto reservation = converter->reserve(inputs); - // the `.value` is just a funny way to make the `smart_refctd_ptr` copyable - auto gpuDS = reservation.getGPUObjects().front().value; - if (!gpuDS) { - m_logger->log("Failed to convert %s into an IGPUDescriptorSet handle", ILogger::ELL_ERROR); - std::exit(-1); - } - - return gpuDS; - }; - - std::array descriptorSetBindings = {}; - std::array presentDescriptorSetBindings; - - descriptorSetBindings[0] = { - .binding = 0u, - .type = nbl::asset::IDescriptor::E_TYPE::ET_COMBINED_IMAGE_SAMPLER, - .createFlags = ICPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - .stageFlags = IShader::E_SHADER_STAGE::ESS_COMPUTE, - .count = 1u, - .immutableSamplers = nullptr - }; - descriptorSetBindings[1] = { - .binding = 1u, - .type = nbl::asset::IDescriptor::E_TYPE::ET_COMBINED_IMAGE_SAMPLER, - .createFlags = ICPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - .stageFlags = IShader::E_SHADER_STAGE::ESS_COMPUTE, - .count = 1u, - .immutableSamplers = nullptr - }; - descriptorSetBindings[2] = { - .binding = 2u, - .type = nbl::asset::IDescriptor::E_TYPE::ET_STORAGE_IMAGE, - .createFlags = ICPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - .stageFlags = IShader::E_SHADER_STAGE::ESS_COMPUTE, - .count = 1u, - .immutableSamplers = nullptr - }; - descriptorSetBindings[3] = { - .binding = 3u, - .type = nbl::asset::IDescriptor::E_TYPE::ET_STORAGE_IMAGE, - .createFlags = ICPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - .stageFlags = IShader::E_SHADER_STAGE::ESS_COMPUTE, - .count = 1u, - .immutableSamplers = nullptr - }; - - presentDescriptorSetBindings[0] = { - .binding = 0u, - .type = nbl::asset::IDescriptor::E_TYPE::ET_COMBINED_IMAGE_SAMPLER, - .createFlags = ICPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - .stageFlags = IShader::E_SHADER_STAGE::ESS_FRAGMENT, - .count = 1u, - .immutableSamplers = &defaultSampler - }; - - auto cpuDescriptorSetLayout = make_smart_refctd_ptr(descriptorSetBindings); - - auto gpuDescriptorSetLayout = convertDSLayoutCPU2GPU(cpuDescriptorSetLayout); - auto gpuPresentDescriptorSetLayout = m_device->createDescriptorSetLayout(presentDescriptorSetBindings); - - auto cpuDescriptorSet = make_smart_refctd_ptr(std::move(cpuDescriptorSetLayout)); - - m_descriptorSet = convertDSCPU2GPU(cpuDescriptorSet); - - smart_refctd_ptr presentDSPool; - { - const video::IGPUDescriptorSetLayout* const layouts[] = { gpuPresentDescriptorSetLayout.get() }; - const uint32_t setCounts[] = { 1u }; - presentDSPool = m_device->createDescriptorPoolForDSLayouts(IDescriptorPool::E_CREATE_FLAGS::ECF_NONE, layouts, setCounts); - } - m_presentDescriptorSet = presentDSPool->createDescriptorSet(gpuPresentDescriptorSetLayout); - - const uint32_t deviceMinSubgroupSize = m_device->getPhysicalDevice()->getLimits().minSubgroupSize; - m_requiredSubgroupSize = static_cast(hlsl::log2(float(deviceMinSubgroupSize))); - - { - const nbl::asset::SPushConstantRange pcRange = { - .stageFlags = IShader::E_SHADER_STAGE::ESS_COMPUTE, - .offset = 0, - .size = sizeof(RenderPushConstants) - }; - m_renderPipelineLayout = m_device->createPipelineLayout( - { &pcRange, 1 }, - core::smart_refctd_ptr(gpuDescriptorSetLayout), - nullptr, - nullptr, - nullptr - ); - if (!m_renderPipelineLayout) - return logFail("Failed to create Pathtracing pipeline layout"); - } - - { - const nbl::asset::SPushConstantRange pcRange = { - .stageFlags = IShader::E_SHADER_STAGE::ESS_COMPUTE, - .offset = 0, - .size = sizeof(RenderRWMCPushConstants) - }; - m_rwmcRenderPipelineLayout = m_device->createPipelineLayout( - { &pcRange, 1 }, - core::smart_refctd_ptr(gpuDescriptorSetLayout), - nullptr, - nullptr, - nullptr - ); - if (!m_rwmcRenderPipelineLayout) - return logFail("Failed to create RWMC Pathtracing pipeline layout"); - } - - { - const nbl::asset::SPushConstantRange pcRange = { - .stageFlags = IShader::E_SHADER_STAGE::ESS_COMPUTE, - .offset = 0u, - .size = sizeof(ResolvePushConstants) - }; - m_resolvePipelineState.layout = m_device->createPipelineLayout( - { &pcRange, 1 }, - core::smart_refctd_ptr(gpuDescriptorSetLayout) - ); - if (!m_resolvePipelineState.layout) - return logFail("Failed to create resolve pipeline layout"); - } - - const auto ensureRenderShaderLoaded = [this](const E_LIGHT_GEOMETRY geometry, const bool persistentWorkGroups, const bool rwmc) -> bool - { - auto& shaderSlot = m_renderPipelines.getShaders(persistentWorkGroups, rwmc)[geometry]; - if (shaderSlot) - return true; - shaderSlot = loadRenderShader(geometry, persistentWorkGroups, rwmc); - return static_cast(shaderSlot); - }; - const auto ensureResolveShaderLoaded = [this]() -> bool - { - if (m_resolvePipelineState.shader) - return true; - m_resolvePipelineState.shader = loadPrecompiledShader(); - return static_cast(m_resolvePipelineState.shader); - }; - - const auto startupGeometry = static_cast(guiControlled.PTPipeline); - if (!ensureRenderShaderLoaded(startupGeometry, UsePersistentWorkGroups, guiControlled.useRWMC)) - return logFail("Failed to load current precompiled compute shader variant"); - if (guiControlled.useRWMC && !ensureResolveShaderLoaded()) - return logFail("Failed to load precompiled resolve compute shader"); - - ensureRenderPipeline( - startupGeometry, - UsePersistentWorkGroups, - guiControlled.useRWMC, - static_cast(guiControlled.polygonMethod) - ); - if (guiControlled.useRWMC) - ensureResolvePipeline(); - - for (auto geometry = 0u; geometry < ELG_COUNT; ++geometry) - { - for (const auto rwmc : { false, true }) - { - if (!ensureRenderShaderLoaded(static_cast(geometry), UsePersistentWorkGroups, rwmc)) - return logFail("Failed to load precompiled compute shader variant"); - } - } - if (!ensureResolveShaderLoaded()) - return logFail("Failed to load precompiled resolve compute shader"); - - // Create graphics pipeline - { - auto scRes = static_cast(m_surface->getSwapchainResources()); - ext::FullScreenTriangle::ProtoPipeline fsTriProtoPPln(m_assetMgr.get(), m_device.get(), m_logger.get()); - if (!fsTriProtoPPln) - return logFail("Failed to create Full Screen Triangle protopipeline or load its vertex shader!"); - - auto fragmentShader = loadPrecompiledShader(); - if (!fragmentShader) - return logFail("Failed to Load and Compile Fragment Shader: lumaMeterShader!"); - - const IGPUPipelineBase::SShaderSpecInfo fragSpec = { - .shader = fragmentShader.get(), - .entryPoint = "main" - }; - - auto presentLayout = m_device->createPipelineLayout( - {}, - core::smart_refctd_ptr(gpuPresentDescriptorSetLayout), - nullptr, - nullptr, - nullptr - ); - m_presentPipeline = fsTriProtoPPln.createPipeline(fragSpec, presentLayout.get(), scRes->getRenderpass(), 0u, {}, hlsl::SurfaceTransform::FLAG_BITS::IDENTITY_BIT, m_pipelineCache.object.get()); - if (!m_presentPipeline) - return logFail("Could not create Graphics Pipeline!"); - m_pipelineCache.dirty = true; - - } - } - - // load CPUImages and convert to GPUImages - smart_refctd_ptr envMap, scrambleMap; - { - auto convertImgCPU2GPU = [&](std::span cpuImgs) { - auto queue = getGraphicsQueue(); - auto cmdbuf = m_cmdBufs[0].get(); - cmdbuf->reset(IGPUCommandBuffer::RESET_FLAGS::NONE); - std::array commandBufferInfo = { cmdbuf }; - core::smart_refctd_ptr imgFillSemaphore = m_device->createSemaphore(0); - imgFillSemaphore->setObjectDebugName("Image Fill Semaphore"); - - auto converter = CAssetConverter::create({ .device = m_device.get() }); - // We don't want to generate mip-maps for these images, to ensure that we must override the default callbacks. - struct SInputs final : CAssetConverter::SInputs - { - // we also need to override this to have concurrent sharing - inline std::span getSharedOwnershipQueueFamilies(const size_t groupCopyID, const asset::ICPUImage* buffer, const CAssetConverter::patch_t& patch) const override - { - if (familyIndices.size() > 1) - return familyIndices; - return {}; - } - - inline uint8_t getMipLevelCount(const size_t groupCopyID, const ICPUImage* image, const CAssetConverter::patch_t& patch) const override - { - return image->getCreationParameters().mipLevels; - } - inline uint16_t needToRecomputeMips(const size_t groupCopyID, const ICPUImage* image, const CAssetConverter::patch_t& patch) const override - { - return 0b0u; - } - - std::vector familyIndices; - } inputs = {}; - inputs.readCache = converter.get(); - inputs.logger = m_logger.get(); - { - const core::set uniqueFamilyIndices = { queue->getFamilyIndex(), queue->getFamilyIndex() }; - inputs.familyIndices = { uniqueFamilyIndices.begin(),uniqueFamilyIndices.end() }; - } - // scratch command buffers for asset converter transfer commands - SIntendedSubmitInfo transfer = { - .queue = queue, - .waitSemaphores = {}, - .prevCommandBuffers = {}, - .scratchCommandBuffers = commandBufferInfo, - .scratchSemaphore = { - .semaphore = imgFillSemaphore.get(), - .value = 0, - // because of layout transitions - .stageMask = PIPELINE_STAGE_FLAGS::ALL_COMMANDS_BITS - } - }; - // as per the `SIntendedSubmitInfo` one commandbuffer must be begun - cmdbuf->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT); - // Normally we'd have to inherit and override the `getFinalOwnerQueueFamily` callback to ensure that the - // compute queue becomes the owner of the buffers and images post-transfer, but in this example we use concurrent sharing - CAssetConverter::SConvertParams params = {}; - params.transfer = &transfer; - params.utilities = m_utils.get(); - - std::get>(inputs.assets) = cpuImgs; - // assert that we don't need to provide patches - assert(cpuImgs[0]->getImageUsageFlags().hasFlags(ICPUImage::E_USAGE_FLAGS::EUF_SAMPLED_BIT)); - auto reservation = converter->reserve(inputs); - // the `.value` is just a funny way to make the `smart_refctd_ptr` copyable - auto gpuImgs = reservation.getGPUObjects(); - for (auto& gpuImg : gpuImgs) { - if (!gpuImg) { - m_logger->log("Failed to convert %s into an IGPUImage handle", ILogger::ELL_ERROR, DefaultImagePathsFile); - std::exit(-1); - } - } - - // and launch the conversions - m_api->startCapture(); - auto result = reservation.convert(params); - m_api->endCapture(); - if (!result.blocking() && result.copy() != IQueue::RESULT::SUCCESS) { - m_logger->log("Failed to record or submit conversions", ILogger::ELL_ERROR); - std::exit(-1); - } - - envMap = gpuImgs[0].value; - scrambleMap = gpuImgs[1].value; - }; - - smart_refctd_ptr envMapCPU, scrambleMapCPU; - { - IAssetLoader::SAssetLoadParams lp; - lp.workingDirectory = this->sharedInputCWD; - SAssetBundle bundle = m_assetMgr->getAsset(DefaultImagePathsFile, lp); - if (bundle.getContents().empty()) { - m_logger->log("Couldn't load an asset.", ILogger::ELL_ERROR); - std::exit(-1); - } - - envMapCPU = IAsset::castDown(bundle.getContents()[0]); - if (!envMapCPU) { - m_logger->log("Couldn't load an asset.", ILogger::ELL_ERROR); - std::exit(-1); - } - }; - { - asset::ICPUImage::SCreationParams info; - info.format = asset::E_FORMAT::EF_R32G32_UINT; - info.type = asset::ICPUImage::ET_2D; - auto extent = envMapCPU->getCreationParameters().extent; - info.extent.width = extent.width; - info.extent.height = extent.height; - info.extent.depth = 1u; - info.mipLevels = 1u; - info.arrayLayers = 1u; - info.samples = asset::ICPUImage::E_SAMPLE_COUNT_FLAGS::ESCF_1_BIT; - info.flags = static_cast(0u); - info.usage = asset::IImage::EUF_TRANSFER_SRC_BIT | asset::IImage::EUF_SAMPLED_BIT; - - scrambleMapCPU = ICPUImage::create(std::move(info)); - const uint32_t texelFormatByteSize = getTexelOrBlockBytesize(scrambleMapCPU->getCreationParameters().format); - const uint32_t texelBufferSize = scrambleMapCPU->getImageDataSizeInBytes(); - auto texelBuffer = ICPUBuffer::create({ texelBufferSize }); - - core::RandomSampler rng(0xbadc0ffeu); - auto out = reinterpret_cast(texelBuffer->getPointer()); - for (auto index = 0u; index < texelBufferSize / 4; index++) { - out[index] = rng.nextSample(); - } - - auto regions = core::make_refctd_dynamic_array>(1u); - ICPUImage::SBufferCopy& region = regions->front(); - region.imageSubresource.aspectMask = IImage::E_ASPECT_FLAGS::EAF_COLOR_BIT; - region.imageSubresource.mipLevel = 0u; - region.imageSubresource.baseArrayLayer = 0u; - region.imageSubresource.layerCount = 1u; - region.bufferOffset = 0u; - region.bufferRowLength = IImageAssetHandlerBase::calcPitchInBlocks(extent.width, texelFormatByteSize); - region.bufferImageHeight = 0u; - region.imageOffset = { 0u, 0u, 0u }; - region.imageExtent = scrambleMapCPU->getCreationParameters().extent; - - scrambleMapCPU->setBufferAndRegions(std::move(texelBuffer), regions); - - // programmatically user-created IPreHashed need to have their hash computed (loaders do it while loading) - scrambleMapCPU->setContentHash(scrambleMapCPU->computeContentHash()); - } - - std::array cpuImgs = { envMapCPU.get(), scrambleMapCPU.get() }; - convertImgCPU2GPU(cpuImgs); - } - - // create views for textures - { - auto createHDRIImage = [this](const asset::E_FORMAT colorFormat, const uint32_t width, const uint32_t height, const bool useCascadeCreationParameters = false) -> smart_refctd_ptr { - IGPUImage::SCreationParams imgInfo; - imgInfo.format = colorFormat; - imgInfo.type = IGPUImage::ET_2D; - imgInfo.extent.width = width; - imgInfo.extent.height = height; - imgInfo.extent.depth = 1u; - imgInfo.mipLevels = 1u; - imgInfo.samples = IGPUImage::ESCF_1_BIT; - imgInfo.flags = static_cast(0u); - - if (!useCascadeCreationParameters) - { - imgInfo.arrayLayers = 1u; - imgInfo.usage = asset::IImage::EUF_STORAGE_BIT | asset::IImage::EUF_TRANSFER_DST_BIT | asset::IImage::EUF_TRANSFER_SRC_BIT | asset::IImage::EUF_SAMPLED_BIT; - } - else - { - imgInfo.arrayLayers = CascadeCount; - imgInfo.usage = asset::IImage::EUF_STORAGE_BIT; - } - - auto image = m_device->createImage(std::move(imgInfo)); - auto imageMemReqs = image->getMemoryReqs(); - imageMemReqs.memoryTypeBits &= m_device->getPhysicalDevice()->getDeviceLocalMemoryTypeBits(); - m_device->allocate(imageMemReqs, image.get()); - - return image; - }; - auto createHDRIImageView = [this](smart_refctd_ptr img, const uint32_t imageArraySize = 1u, const IGPUImageView::E_TYPE imageViewType = IGPUImageView::ET_2D) -> smart_refctd_ptr - { - auto format = img->getCreationParameters().format; - IGPUImageView::SCreationParams imgViewInfo; - imgViewInfo.image = std::move(img); - imgViewInfo.format = format; - imgViewInfo.flags = static_cast(0u); - imgViewInfo.subresourceRange.aspectMask = IImage::E_ASPECT_FLAGS::EAF_COLOR_BIT; - imgViewInfo.subresourceRange.baseArrayLayer = 0u; - imgViewInfo.subresourceRange.baseMipLevel = 0u; - imgViewInfo.subresourceRange.levelCount = 1u; - imgViewInfo.viewType = imageViewType; - - imgViewInfo.subresourceRange.layerCount = imageArraySize; - - return m_device->createImageView(std::move(imgViewInfo)); - }; - - auto params = envMap->getCreationParameters(); - auto extent = params.extent; - - envMap->setObjectDebugName("Env Map"); - m_envMapView = createHDRIImageView(envMap); - m_envMapView->setObjectDebugName("Env Map View"); - - scrambleMap->setObjectDebugName("Scramble Map"); - m_scrambleView = createHDRIImageView(scrambleMap); - m_scrambleView->setObjectDebugName("Scramble Map View"); - - auto outImg = createHDRIImage(asset::E_FORMAT::EF_R16G16B16A16_SFLOAT, WindowDimensions.x, WindowDimensions.y); - outImg->setObjectDebugName("Output Image"); - m_outImgView = createHDRIImageView(outImg, 1, IGPUImageView::ET_2D_ARRAY); - m_outImgView->setObjectDebugName("Output Image View"); - - auto cascade = createHDRIImage(asset::E_FORMAT::EF_R16G16B16A16_SFLOAT, WindowDimensions.x, WindowDimensions.y, true); - cascade->setObjectDebugName("Cascade"); - m_cascadeView = createHDRIImageView(cascade, CascadeCount, IGPUImageView::ET_2D_ARRAY); - m_cascadeView->setObjectDebugName("Cascade View"); - } - - // Update Descriptors - { - ISampler::SParams samplerParams0 = { - ISampler::E_TEXTURE_CLAMP::ETC_CLAMP_TO_EDGE, - ISampler::E_TEXTURE_CLAMP::ETC_CLAMP_TO_EDGE, - ISampler::E_TEXTURE_CLAMP::ETC_CLAMP_TO_EDGE, - ISampler::ETBC_FLOAT_OPAQUE_BLACK, - ISampler::ETF_LINEAR, - ISampler::ETF_LINEAR, - ISampler::ESMM_LINEAR, - 0u, - false, - ECO_ALWAYS - }; - auto sampler0 = m_device->createSampler(samplerParams0); - ISampler::SParams samplerParams1 = { - ISampler::E_TEXTURE_CLAMP::ETC_CLAMP_TO_EDGE, - ISampler::E_TEXTURE_CLAMP::ETC_CLAMP_TO_EDGE, - ISampler::E_TEXTURE_CLAMP::ETC_CLAMP_TO_EDGE, - ISampler::ETBC_INT_OPAQUE_BLACK, - ISampler::ETF_NEAREST, - ISampler::ETF_NEAREST, - ISampler::ESMM_NEAREST, - 0u, - false, - ECO_ALWAYS - }; - auto sampler1 = m_device->createSampler(samplerParams1); - - std::array writeDSInfos = {}; - writeDSInfos[0].desc = m_outImgView; - writeDSInfos[0].info.image.imageLayout = IImage::LAYOUT::GENERAL; - writeDSInfos[1].desc = m_cascadeView; - writeDSInfos[1].info.image.imageLayout = IImage::LAYOUT::GENERAL; - writeDSInfos[2].desc = m_envMapView; - // ISampler::SParams samplerParams = { ISampler::ETC_CLAMP_TO_EDGE, ISampler::ETC_CLAMP_TO_EDGE, ISampler::ETC_CLAMP_TO_EDGE, ISampler::ETBC_FLOAT_OPAQUE_BLACK, ISampler::ETF_LINEAR, ISampler::ETF_LINEAR, ISampler::ESMM_LINEAR, 0u, false, ECO_ALWAYS }; - writeDSInfos[2].info.combinedImageSampler.sampler = sampler0; - writeDSInfos[2].info.combinedImageSampler.imageLayout = asset::IImage::LAYOUT::READ_ONLY_OPTIMAL; - writeDSInfos[3].desc = m_scrambleView; - // ISampler::SParams samplerParams = { ISampler::ETC_CLAMP_TO_EDGE, ISampler::ETC_CLAMP_TO_EDGE, ISampler::ETC_CLAMP_TO_EDGE, ISampler::ETBC_INT_OPAQUE_BLACK, ISampler::ETF_NEAREST, ISampler::ETF_NEAREST, ISampler::ESMM_NEAREST, 0u, false, ECO_ALWAYS }; - writeDSInfos[3].info.combinedImageSampler.sampler = sampler1; - writeDSInfos[3].info.combinedImageSampler.imageLayout = asset::IImage::LAYOUT::READ_ONLY_OPTIMAL; - writeDSInfos[4].desc = m_outImgView; - writeDSInfos[4].info.image.imageLayout = IImage::LAYOUT::READ_ONLY_OPTIMAL; - - std::array writeDescriptorSets = {}; - writeDescriptorSets[0] = { - .dstSet = m_descriptorSet.get(), - .binding = 2, - .arrayElement = 0u, - .count = 1u, - .info = &writeDSInfos[0] - }; - writeDescriptorSets[1] = { - .dstSet = m_descriptorSet.get(), - .binding = 3, - .arrayElement = 0u, - .count = 1u, - .info = &writeDSInfos[1] - }; - writeDescriptorSets[2] = { - .dstSet = m_descriptorSet.get(), - .binding = 0, - .arrayElement = 0u, - .count = 1u, - .info = &writeDSInfos[2] - }; - writeDescriptorSets[3] = { - .dstSet = m_descriptorSet.get(), - .binding = 1, - .arrayElement = 0u, - .count = 1u, - .info = &writeDSInfos[3] - }; - writeDescriptorSets[4] = { - .dstSet = m_presentDescriptorSet.get(), - .binding = 0, - .arrayElement = 0u, - .count = 1u, - .info = &writeDSInfos[4] - }; - - m_device->updateDescriptorSets(writeDescriptorSets, {}); - } - - // Create ui descriptors - { - using binding_flags_t = IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS; - { - IGPUSampler::SParams params; - params.AnisotropicFilter = 1u; - params.TextureWrapU = ISampler::E_TEXTURE_CLAMP::ETC_REPEAT; - params.TextureWrapV = ISampler::E_TEXTURE_CLAMP::ETC_REPEAT; - params.TextureWrapW = ISampler::E_TEXTURE_CLAMP::ETC_REPEAT; - - m_ui.samplers.gui = m_device->createSampler(params); - m_ui.samplers.gui->setObjectDebugName("Nabla IMGUI UI Sampler"); - } - - std::array, 69u> immutableSamplers; - for (auto& it : immutableSamplers) - it = smart_refctd_ptr(m_ui.samplers.scene); - - immutableSamplers[nbl::ext::imgui::UI::FontAtlasTexId] = smart_refctd_ptr(m_ui.samplers.gui); - - 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 = m_pipelineCache.object; - params.pipelineLayout = nbl::ext::imgui::UI::createDefaultPipelineLayout(m_utils->getLogicalDevice(), params.resources.texturesInfo, params.resources.samplersInfo, MaxUITextureCount); - params.renderpass = smart_refctd_ptr(renderpass); - params.streamingBuffer = nullptr; - params.subpassIx = 0u; - params.transfer = getTransferUpQueue(); - params.utilities = m_utils; - params.spirv = nbl::ext::imgui::UI::SCreationParameters::PrecompiledShaders{ - .vertex = loadPrecompiledShader(), - .fragment = loadPrecompiledShader() - }; - if (!params.spirv->vertex || !params.spirv->fragment) - return logFail("Failed to load precompiled ImGui shaders"); - { - m_ui.manager = ext::imgui::UI::create(std::move(params)); - if (m_ui.manager) - m_pipelineCache.dirty = true; - - // note that we use default layout provided by our extension, but you are free to create your own by filling nbl::ext::imgui::UI::S_CREATION_PARAMETERS::resources - const auto* descriptorSetLayout = m_ui.manager->getPipeline()->getLayout()->getDescriptorSetLayout(0u); - const auto& params = m_ui.manager->getCreationParameters(); - - IDescriptorPool::SCreateInfo descriptorPoolInfo = {}; - descriptorPoolInfo.maxDescriptorCount[static_cast(asset::IDescriptor::E_TYPE::ET_SAMPLER)] = (uint32_t)nbl::ext::imgui::UI::DefaultSamplerIx::COUNT; - descriptorPoolInfo.maxDescriptorCount[static_cast(asset::IDescriptor::E_TYPE::ET_SAMPLED_IMAGE)] = MaxUITextureCount; - descriptorPoolInfo.maxSets = 1u; - descriptorPoolInfo.flags = IDescriptorPool::E_CREATE_FLAGS::ECF_UPDATE_AFTER_BIND_BIT; - - m_guiDescriptorSetPool = m_device->createDescriptorPool(std::move(descriptorPoolInfo)); - assert(m_guiDescriptorSetPool); - - m_guiDescriptorSetPool->createDescriptorSets(1u, &descriptorSetLayout, &m_ui.descriptorSet); - assert(m_ui.descriptorSet); - } - } - m_ui.manager->registerListener( - [this]() -> void { - ImGuiIO& io = ImGui::GetIO(); - ImGuizmo::SetOrthographic(false); - ImGuizmo::BeginFrame(); - 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)); - - const ImGuiViewport* viewport = ImGui::GetMainViewport(); - const ImVec2 viewportPos = viewport->Pos; - const ImVec2 viewportSize = viewport->Size; - const ImGuiStyle& style = ImGui::GetStyle(); - const float panelMargin = 10.f; - const auto currentGeometry = static_cast(guiControlled.PTPipeline); - const auto requestedMethod = static_cast(guiControlled.polygonMethod); - const auto currentVariant = getRenderVariantInfo(currentGeometry, UsePersistentWorkGroups, requestedMethod); - const size_t readyRenderPipelines = getReadyRenderPipelineCount(); - const size_t totalRenderPipelines = getKnownRenderPipelineCount(); - const size_t readyTotalPipelines = readyRenderPipelines + (m_resolvePipelineState.pipeline ? 1ull : 0ull); - const size_t totalKnownPipelines = totalRenderPipelines + 1ull; - const size_t runningPipelineBuilds = getRunningPipelineBuildCount(); - const size_t queuedPipelineBuilds = m_pipelineCache.warmup.queue.size(); - const bool warmupInProgress = m_startupLog.hasPathtraceOutput && !m_pipelineCache.warmup.loggedComplete; - const char* const effectiveEntryPoint = currentVariant.entryPoint; - const auto& shaderNames = this_example::getLightGeometryNamePointers(); - const auto& polygonMethodNames = this_example::getPolygonMethodNamePointers(); - const std::string pipelineStatusText = !m_startupLog.hasPathtraceOutput ? - "Building pipeline..." : - (warmupInProgress ? - ("Warmup " + std::to_string(readyTotalPipelines) + "/" + std::to_string(totalKnownPipelines)) : - "All pipelines ready"); - const std::string cacheStateText = m_pipelineCache.loadedFromDisk ? "loaded from disk" : "cold start"; - const std::string trimCacheText = std::to_string(m_pipelineCache.trimmedShaders.loadedFromDiskCount + m_pipelineCache.trimmedShaders.generatedCount) + " ready"; - const std::string parallelismText = std::to_string(m_pipelineCache.warmup.budget); - const std::string renderStateText = this_example::pt_ui::makeReadyText(readyTotalPipelines, totalKnownPipelines); - const std::string warmupStateText = this_example::pt_ui::makeRunQueueText(runningPipelineBuilds, queuedPipelineBuilds); - const std::string cursorText = "cursor " + std::to_string(static_cast(io.MousePos.x)) + " " + std::to_string(static_cast(io.MousePos.y)); - const this_example::pt_ui::SFloatSliderRow cameraFloatRows[] = { - { "move", &guiControlled.moveSpeed, 0.1f, 10.f, "%.2f" }, - { "rotate", &guiControlled.rotateSpeed, 0.1f, 10.f, "%.2f" }, - { "fov", &guiControlled.fov, 20.f, 150.f, "%.0f" }, - { "zNear", &guiControlled.zNear, 0.1f, 100.f, "%.2f" }, - { "zFar", &guiControlled.zFar, 110.f, 10000.f, "%.0f" }, - }; - const this_example::pt_ui::SComboRow renderComboRows[] = { - { "shader", &guiControlled.PTPipeline, shaderNames.data(), static_cast(shaderNames.size()) }, - { "method", &guiControlled.polygonMethod, polygonMethodNames.data(), static_cast(polygonMethodNames.size()) }, - }; - const this_example::pt_ui::SIntSliderRow renderIntRows[] = { - { "spp", &guiControlled.spp, 1, (0x1u<registerListener( - [this]() -> void { - static struct - { - hlsl::float32_t4x4 view, projection; - } imguizmoM16InOut; - - 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.projection[1][1] *= -1.f; // https://johannesugb.github.io/gpu-programming/why-do-opengl-proj-matrices-fail-in-vulkan/ - - m_transformParams.editTransformDecomposition = true; - m_transformParams.sceneTexDescIx = 1u; - - if (ImGui::IsKeyPressed(ImGuiKey_End)) - { - m_lightModelMatrix = hlsl::float32_t4x4( - 0.3f, 0.0f, 0.0f, 0.0f, - 0.0f, 0.3f, 0.0f, 0.0f, - 0.0f, 0.0f, 0.3f, 0.0f, - -1.0f, 1.5f, 0.0f, 1.0f - ); - } - - if (E_LIGHT_GEOMETRY::ELG_SPHERE == guiControlled.PTPipeline) - { - m_transformParams.allowedOp = ImGuizmo::OPERATION::TRANSLATE | ImGuizmo::OPERATION::SCALEU; - m_transformParams.isSphere = true; - } - else - { - m_transformParams.allowedOp = ImGuizmo::OPERATION::TRANSLATE | ImGuizmo::OPERATION::ROTATE | ImGuizmo::OPERATION::SCALE; - m_transformParams.isSphere = false; - } - EditTransform(&imguizmoM16InOut.view[0][0], &imguizmoM16InOut.projection[0][0], &m_lightModelMatrix[0][0], m_transformParams); - - if (E_LIGHT_GEOMETRY::ELG_SPHERE == guiControlled.PTPipeline) - { - // keep uniform scale for sphere - float32_t uniformScale = (m_lightModelMatrix[0][0] + m_lightModelMatrix[1][1] + m_lightModelMatrix[2][2]) / 3.0f; - m_lightModelMatrix[0][0] = uniformScale; - m_lightModelMatrix[1][1] = uniformScale; // Doesn't affect sphere but will affect rectangle/triangle if switching shapes - m_lightModelMatrix[2][2] = uniformScale; - } - - } - ); - - // Set Camera - { - core::vectorSIMDf cameraPosition(0, 5, -10); - 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_showUI = true; - if (m_commandLine.ciMode) - m_showUI = false; - - m_winMgr->setWindowSize(m_window.get(), WindowDimensions.x, WindowDimensions.y); - m_surface->recreateSwapchain(); - m_winMgr->show(m_window.get()); - m_oracle.reportBeginFrameRecord(); - m_camera.mapKeysToArrows(); - - // set initial rwmc settings - resetRWMCParamsToDefaults(); - applyLateCommandLineOverrides(); - - // do this as late as possible - { - auto sequence = sequenceFuture.get(); - m_sequenceSamplesLog2 = sequence->getHeader().maxSamplesLog2; - auto* const seqBufferCPU = sequence->getBuffer(); - m_utils->createFilledDeviceLocalBufferOnDedMem(SIntendedSubmitInfo{.queue=getGraphicsQueue()},IGPUBuffer::SCreationParams{seqBufferCPU->getCreationParams()},seqBufferCPU->getPointer()).move_into(m_sequenceBuffer); - m_sequenceBuffer->setObjectDebugName("Low Discrepancy Sequence"); - } - - return true; - } - - bool updateGUIDescriptorSet() - { - // texture atlas, note we don't create info & write pair for the font sampler because UI extension's is immutable and baked into DS layout - static std::array descriptorInfo; - static IGPUDescriptorSet::SWriteDescriptorSet writes[MaxUITextureCount]; - - descriptorInfo[nbl::ext::imgui::UI::FontAtlasTexId].info.image.imageLayout = IImage::LAYOUT::READ_ONLY_OPTIMAL; - descriptorInfo[nbl::ext::imgui::UI::FontAtlasTexId].desc = smart_refctd_ptr(m_ui.manager->getFontAtlasView()); - - for (uint32_t i = 0; i < descriptorInfo.size(); ++i) - { - writes[i].dstSet = m_ui.descriptorSet.get(); - writes[i].binding = 0u; - writes[i].arrayElement = i; - writes[i].count = 1u; - } - writes[nbl::ext::imgui::UI::FontAtlasTexId].info = descriptorInfo.data() + nbl::ext::imgui::UI::FontAtlasTexId; - - return m_device->updateDescriptorSets(writes, {}); - } - - inline void workLoopBody() override - { - pollPendingPipelines(); - pumpPipelineWarmup(); - if (!m_startupLog.loggedFirstFrameLoop) - { - logStartupEvent("first_frame_loop"); - m_startupLog.loggedFirstFrameLoop = true; - } - - // framesInFlight: ensuring safe execution of command buffers and acquires, `framesInFlight` only affect semaphore waits, don't use this to index your resources because it can change with swapchain recreation. - const uint32_t framesInFlight = core::min(MaxFramesInFlight, m_surface->getMaxAcquiresInFlight()); - // We block for semaphores for 2 reasons here: - // A) Resource: Can't use resource like a command buffer BEFORE previous use is finished! [MaxFramesInFlight] - // B) Acquire: Can't have more acquires in flight than a certain threshold returned by swapchain or your surface helper class. [MaxAcquiresInFlight] - if (m_realFrameIx >= framesInFlight) - { - const ISemaphore::SWaitInfo cbDonePending[] = - { - { - .semaphore = m_semaphore.get(), - .value = m_realFrameIx + 1 - framesInFlight - } - }; - if (m_device->blockForSemaphores(cbDonePending) != ISemaphore::WAIT_RESULT::SUCCESS) - return; - } - const auto resourceIx = m_realFrameIx % MaxFramesInFlight; - - // CPU events - update(); - - auto queue = getGraphicsQueue(); - auto cmdbuf = m_cmdBufs[resourceIx].get(); - - if (!keepRunning()) - return; - - cmdbuf->reset(IGPUCommandBuffer::RESET_FLAGS::NONE); - - // safe to proceed - // upload buffer data - cmdbuf->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT); - cmdbuf->beginDebugMarker("ComputeShaderPathtracer IMGUI Frame"); - - RenderRWMCPushConstants rwmcPushConstants; - ResolvePushConstants resolvePushConstants; - RenderPushConstants pc; - auto updatePathtracerPushConstants = [&]() -> void { - // disregard surface/swapchain transformation for now - const float32_t4x4 viewProjectionMatrix = m_camera.getConcatenatedMatrix(); - const float32_t3x4 modelMatrix = hlsl::math::linalg::identity(); - - const float32_t4x4 modelViewProjectionMatrix = nbl::hlsl::math::linalg::promoted_mul(viewProjectionMatrix, modelMatrix); - const float32_t4x4 invMVP = hlsl::inverse(modelViewProjectionMatrix); - - pc.pSampleSequence = m_sequenceBuffer->getDeviceAddress(); - pc.invMVP = invMVP; - pc.setLightMatrix(hlsl::float32_t4x3(m_lightModelMatrix)); - pc.sampleCount = guiControlled.spp; - guiControlled.rwmcParams.sampleCount = guiControlled.spp; - pc.depth = guiControlled.depth; - pc.sequenceSampleCountLog2 = m_sequenceSamplesLog2; - if (guiControlled.useRWMC) - { - rwmcPushConstants.renderPushConstants = pc; - rwmcPushConstants.splattingParameters = rwmc::SPackedSplattingParameters::create(guiControlled.rwmcParams.base, guiControlled.rwmcParams.start, CascadeCount); - } - }; - updatePathtracerPushConstants(); - bool producedRenderableOutput = false; - bool dispatchedRwmcPathTrace = false; - - // TRANSITION m_outImgView to GENERAL (because of descriptorSets0 -> ComputeShader Writes into the image) - { - const IGPUCommandBuffer::SImageMemoryBarrier imgBarriers[] = { - { - .barrier = { - .dep = { - .srcStageMask = PIPELINE_STAGE_FLAGS::ALL_TRANSFER_BITS, - .srcAccessMask = ACCESS_FLAGS::TRANSFER_WRITE_BIT, - .dstStageMask = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT, - .dstAccessMask = ACCESS_FLAGS::SHADER_WRITE_BITS - } - }, - .image = m_outImgView->getCreationParameters().image.get(), - .subresourceRange = { - .aspectMask = IImage::EAF_COLOR_BIT, - .baseMipLevel = 0u, - .levelCount = 1u, - .baseArrayLayer = 0u, - .layerCount = 1u - }, - .oldLayout = IImage::LAYOUT::UNDEFINED, - .newLayout = IImage::LAYOUT::GENERAL - } - }; - cmdbuf->pipelineBarrier(E_DEPENDENCY_FLAGS::EDF_NONE, { .imgBarriers = imgBarriers }); - } - - // transit m_cascadeView layout to GENERAL, block until previous shader is done with reading from the cascade - if(guiControlled.useRWMC) - { - const IGPUCommandBuffer::SImageMemoryBarrier cascadeBarrier[] = { - { - .barrier = { - .dep = { - .srcStageMask = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT, - .srcAccessMask = ACCESS_FLAGS::NONE, - .dstStageMask = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT, - .dstAccessMask = ACCESS_FLAGS::NONE - } - }, - .image = m_cascadeView->getCreationParameters().image.get(), - .subresourceRange = { - .aspectMask = IImage::EAF_COLOR_BIT, - .baseMipLevel = 0u, - .levelCount = 1u, - .baseArrayLayer = 0u, - .layerCount = CascadeCount - }, - .oldLayout = IImage::LAYOUT::UNDEFINED, - .newLayout = IImage::LAYOUT::GENERAL - } - }; - cmdbuf->pipelineBarrier(E_DEPENDENCY_FLAGS::EDF_NONE, { .imgBarriers = cascadeBarrier }); - } - - { - // TODO: shouldn't it be computed only at initialization stage and on window resize? - const uint32_t dispatchSize = - m_physicalDevice->getLimits().computeOptimalPersistentWorkgroupDispatchSize(WindowDimensions.x * WindowDimensions.y, RenderWorkgroupSize); - - IGPUComputePipeline* pipeline = pickPTPipeline(); - if (pipeline) - { - cmdbuf->bindComputePipeline(pipeline); - cmdbuf->bindDescriptorSets(EPBP_COMPUTE, pipeline->getLayout(), 0u, 1u, &m_descriptorSet.get()); - - const uint32_t pushConstantsSize = guiControlled.useRWMC ? sizeof(RenderRWMCPushConstants) : sizeof(RenderPushConstants); - const void* pushConstantsPtr = guiControlled.useRWMC ? reinterpret_cast(&rwmcPushConstants) : reinterpret_cast(&pc); - cmdbuf->pushConstants(pipeline->getLayout(), IShader::E_SHADER_STAGE::ESS_COMPUTE, 0, pushConstantsSize, pushConstantsPtr); - - cmdbuf->dispatch(dispatchSize, 1u, 1u); - dispatchedRwmcPathTrace = guiControlled.useRWMC; - producedRenderableOutput = !guiControlled.useRWMC; - } - } - - // m_cascadeView synchronization - wait for previous compute shader to write into the cascade - // TODO: create this and every other barrier once outside of the loop? - if(guiControlled.useRWMC && dispatchedRwmcPathTrace) - { - const IGPUCommandBuffer::SImageMemoryBarrier cascadeBarrier[] = { - { - .barrier = { - .dep = { - .srcStageMask = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT, - .srcAccessMask = ACCESS_FLAGS::SHADER_WRITE_BITS, - .dstStageMask = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT, - .dstAccessMask = ACCESS_FLAGS::SHADER_READ_BITS - } - }, - .image = m_cascadeView->getCreationParameters().image.get(), - .subresourceRange = { - .aspectMask = IImage::EAF_COLOR_BIT, - .baseMipLevel = 0u, - .levelCount = 1u, - .baseArrayLayer = 0u, - .layerCount = CascadeCount - } - } - }; - cmdbuf->pipelineBarrier(E_DEPENDENCY_FLAGS::EDF_NONE, { .imgBarriers = cascadeBarrier }); - - // resolve - const uint32_t2 dispatchSize = uint32_t2( // Round up division - (m_window->getWidth() + ResolveWorkgroupSizeX - 1) / ResolveWorkgroupSizeX, - (m_window->getHeight() + ResolveWorkgroupSizeY - 1) / ResolveWorkgroupSizeY - ); - - IGPUComputePipeline* pipeline = ensureResolvePipeline(); - if (pipeline) - { - resolvePushConstants.resolveParameters = rwmc::SResolveParameters::create(guiControlled.rwmcParams); - - cmdbuf->bindComputePipeline(pipeline); - cmdbuf->bindDescriptorSets(EPBP_COMPUTE, pipeline->getLayout(), 0u, 1u, &m_descriptorSet.get()); - cmdbuf->pushConstants(pipeline->getLayout(), IShader::E_SHADER_STAGE::ESS_COMPUTE, 0u, sizeof(ResolvePushConstants), &resolvePushConstants); - - cmdbuf->dispatch(dispatchSize.x, dispatchSize.y, 1u); - producedRenderableOutput = true; - } - } - - if (producedRenderableOutput) - { - m_startupLog.hasPathtraceOutput = true; - if (!m_startupLog.loggedFirstRenderDispatch) - { - logStartupEvent("first_render_dispatch"); - m_startupLog.loggedFirstRenderDispatch = true; - } - } - maybeQueueCiScreenshotRequest(); - - // TRANSITION m_outImgView to READ (because of descriptorSets0 -> ComputeShader Writes into the image) - { - const IGPUCommandBuffer::SImageMemoryBarrier imgBarriers[] = { - { - .barrier = { - .dep = { - .srcStageMask = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT, - .srcAccessMask = ACCESS_FLAGS::SHADER_WRITE_BITS, - .dstStageMask = PIPELINE_STAGE_FLAGS::FRAGMENT_SHADER_BIT, - .dstAccessMask = ACCESS_FLAGS::SHADER_READ_BITS - } - }, - .image = m_outImgView->getCreationParameters().image.get(), - .subresourceRange = { - .aspectMask = IImage::EAF_COLOR_BIT, - .baseMipLevel = 0u, - .levelCount = 1u, - .baseArrayLayer = 0u, - .layerCount = 1u - }, - .oldLayout = IImage::LAYOUT::GENERAL, - .newLayout = IImage::LAYOUT::READ_ONLY_OPTIMAL - } - }; - cmdbuf->pipelineBarrier(E_DEPENDENCY_FLAGS::EDF_NONE, { .imgBarriers = imgBarriers }); - } - - // TODO: tone mapping and stuff - - asset::SViewport viewport; - { - viewport.minDepth = 1.f; - viewport.maxDepth = 0.f; - viewport.x = 0u; - viewport.y = 0u; - viewport.width = WindowDimensions.x; - viewport.height = WindowDimensions.y; - } - cmdbuf->setViewport(0u, 1u, &viewport); - - - VkRect2D defaultScisors[] = { {.offset = {(int32_t)viewport.x, (int32_t)viewport.y}, .extent = {(uint32_t)viewport.width, (uint32_t)viewport.height}} }; - cmdbuf->setScissor(defaultScisors); - - const VkRect2D currentRenderArea = - { - .offset = {0,0}, - .extent = {m_window->getWidth(),m_window->getHeight()} - }; - auto scRes = static_cast(m_surface->getSwapchainResources()); - - // Upload m_outImg to swapchain + UI - { - const IGPUCommandBuffer::SRenderpassBeginInfo info = - { - .framebuffer = scRes->getFramebuffer(m_currentImageAcquire.imageIndex), - .colorClearValues = &clearColor, - .depthStencilClearValues = nullptr, - .renderArea = currentRenderArea - }; - nbl::video::ISemaphore::SWaitInfo waitInfo = { .semaphore = m_semaphore.get(), .value = m_realFrameIx + 1u }; - - cmdbuf->beginRenderPass(info, IGPUCommandBuffer::SUBPASS_CONTENTS::INLINE); - - if (m_startupLog.hasPathtraceOutput) - { - cmdbuf->bindGraphicsPipeline(m_presentPipeline.get()); - cmdbuf->bindDescriptorSets(EPBP_GRAPHICS, m_presentPipeline->getLayout(), 0, 1u, &m_presentDescriptorSet.get()); - ext::FullScreenTriangle::recordDrawCall(cmdbuf); - } - - if (m_showUI) - { - const auto uiParams = m_ui.manager->getCreationParameters(); - auto* uiPipeline = m_ui.manager->getPipeline(); - cmdbuf->bindGraphicsPipeline(uiPipeline); - cmdbuf->bindDescriptorSets(EPBP_GRAPHICS, uiPipeline->getLayout(), uiParams.resources.texturesInfo.setIx, 1u, &m_ui.descriptorSet.get()); - m_ui.manager->render(cmdbuf, waitInfo); - } - - cmdbuf->endRenderPass(); - } - - cmdbuf->end(); - { - const IQueue::SSubmitInfo::SSemaphoreInfo rendered[] = - { - { - .semaphore = m_semaphore.get(), - .value = ++m_realFrameIx, - .stageMask = PIPELINE_STAGE_FLAGS::COLOR_ATTACHMENT_OUTPUT_BIT - } - }; - { - { - const IQueue::SSubmitInfo::SCommandBufferInfo commandBuffers[] = - { - {.cmdbuf = cmdbuf } - }; - - const IQueue::SSubmitInfo::SSemaphoreInfo acquired[] = - { - { - .semaphore = m_currentImageAcquire.semaphore, - .value = m_currentImageAcquire.acquireCount, - .stageMask = PIPELINE_STAGE_FLAGS::NONE - } - }; - const IQueue::SSubmitInfo infos[] = - { - { - .waitSemaphores = acquired, - .commandBuffers = commandBuffers, - .signalSemaphores = rendered - } - }; - - updateGUIDescriptorSet(); - - bool submitSucceeded = false; - m_api->startCapture(); - if (queue->submit(infos) != IQueue::RESULT::SUCCESS) - m_realFrameIx--; - else - submitSucceeded = true; - m_api->endCapture(); - - if (submitSucceeded && rendered[0].semaphore) - maybeSaveSceneScreenshot(queue, rendered[0]); - } - } - - if (producedRenderableOutput && !m_startupLog.loggedFirstRenderSubmit) - { - logStartupEvent("first_render_submit"); - m_startupLog.loggedFirstRenderSubmit = true; - } - if (!m_commandLine.ciMode && m_startupLog.hasPathtraceOutput && !m_pipelineCache.warmup.started) - { - kickoffPipelineWarmup(); - } - maybeCheckpointPipelineCache(); - - m_window->setCaption("[Nabla Engine] HLSL Compute Path Tracer"); - m_surface->present(m_currentImageAcquire.imageIndex, rendered); - } - } - - inline bool keepRunning() override - { - if (m_exitRequested) - return false; - if (m_surface->irrecoverable()) - return false; - - return true; - } - - inline bool onAppTerminated() override - { - waitForPendingPipelines(); - savePipelineCache(); - return device_base_t::onAppTerminated(); - } - - inline void update() - { - m_camera.setMoveSpeed(guiControlled.moveSpeed); - m_camera.setRotateSpeed(guiControlled.rotateSpeed); - - static std::chrono::microseconds previousEventTimestamp{}; - - m_inputSystem->getDefaultMouse(&mouse); - m_inputSystem->getDefaultKeyboard(&keyboard); - - auto updatePresentationTimestamp = [&]() - { - m_currentImageAcquire = m_surface->acquireNextImage(); - - m_oracle.reportEndFrameRecord(); - const auto timestamp = m_oracle.getNextPresentationTimeStamp(); - m_oracle.reportBeginFrameRecord(); - - return timestamp; - }; - - const auto nextPresentationTimestamp = updatePresentationTimestamp(); - - struct - { - std::vector mouse{}; - std::vector keyboard{}; - } capturedEvents; - - m_camera.beginInputProcessing(nextPresentationTimestamp); - { - if (!m_commandLine.ciMode) - { - const auto& io = ImGui::GetIO(); - 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 - { - if (e.timeStamp < previousEventTimestamp) - continue; - - previousEventTimestamp = e.timeStamp; - capturedEvents.mouse.emplace_back(e); - - if (e.type == nbl::ui::SMouseEvent::EET_SCROLL) - gcIndex = std::clamp(int16_t(gcIndex) + int16_t(core::sign(e.scrollEvent.verticalScroll)), int64_t(0), int64_t(ELG_COUNT - (uint8_t)1u)); - } - }, m_logger.get()); - - 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 - { - if (e.timeStamp < previousEventTimestamp) - continue; - - if (e.keyCode == ui::EKC_F12) - if (e.action == ui::SKeyboardEvent::ECA_RELEASED) - requestSceneScreenshot(getNextSceneScreenshotPath(), false); - - if (e.keyCode == ui::EKC_H) - if (e.action == ui::SKeyboardEvent::ECA_RELEASED) - m_showUI = !m_showUI; - - previousEventTimestamp = e.timeStamp; - capturedEvents.keyboard.emplace_back(e); - } - }, m_logger.get()); - } - else - { - mouse.consumeEvents([&](const IMouseEventChannel::range_t&) -> void {}, m_logger.get()); - keyboard.consumeEvents([&](const IKeyboardEventChannel::range_t&) -> void {}, m_logger.get()); - } - } - m_camera.endInputProcessing(nextPresentationTimestamp); - - 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()); - const auto cursorPosition = m_window->getCursorControl()->getPosition(); - const auto mousePosition = float32_t2(cursorPosition.x, cursorPosition.y) - float32_t2(m_window->getX(), m_window->getY()); - - const ext::imgui::UI::SUpdateParameters params = - { - .mousePosition = mousePosition, - .displaySize = { m_window->getWidth(), m_window->getHeight() }, - .mouseEvents = mouseEvents, - .keyboardEvents = keyboardEvents - }; - - if (m_showUI) - m_ui.manager->update(params); - } - - private: - template - smart_refctd_ptr loadPrecompiledShader() - { - IAssetLoader::SAssetLoadParams lp = {}; - lp.logger = m_logger.get(); - lp.workingDirectory = "app_resources"; - - const auto key = nbl::this_example::builtin::build::get_spirv_key(m_device.get()); - auto assetBundle = m_assetMgr->getAsset(key, lp); - const auto assets = assetBundle.getContents(); - if (assets.empty()) - { - m_logger->log("Could not load precompiled shader: %s", ILogger::ELL_ERROR, key.c_str()); - return nullptr; - } - - auto shader = IAsset::castDown(assets[0]); - if (!shader) - { - m_logger->log("Failed to cast %s asset to IShader!", ILogger::ELL_ERROR, key.c_str()); - return nullptr; - } - - shader->setFilePathHint(std::string(std::string_view(ShaderKey.value))); - return shader; - } - - void logStartupEvent(const char* const eventName) - { - const auto elapsedMs = std::chrono::duration_cast(clock_t::now() - m_startupBeganAt).count(); - m_logger->log("PATH_TRACER_STARTUP %s_ms=%lld", ILogger::ELL_INFO, eventName, static_cast(elapsedMs)); - } - - static std::string normalizeCliToken(std::string value) - { - std::string normalized; - normalized.reserve(value.size()); - for (const auto ch : value) - { - if (ch == '-' || ch == '_' || std::isspace(static_cast(ch))) - continue; - normalized.push_back(static_cast(std::tolower(static_cast(ch)))); - } - return normalized; - } - - static std::optional parseGeometryOverride(const std::string& value) - { - const auto normalized = normalizeCliToken(value); - if (normalized == "sphere" || normalized == "elgsphere") - return ELG_SPHERE; - if (normalized == "triangle" || normalized == "elgtriangle") - return ELG_TRIANGLE; - if (normalized == "rectangle" || normalized == "quad" || normalized == "elgrectangle") - return ELG_RECTANGLE; - return std::nullopt; - } - - static std::optional parseMethodOverride(const std::string& value) - { - const auto normalized = normalizeCliToken(value); - if (normalized == "area" || normalized == "epmarea") - return EPM_AREA; - if (normalized == "solidangle" || normalized == "epmsolidangle") - return EPM_SOLID_ANGLE; - if (normalized == "projectedsolidangle" || normalized == "projected" || normalized == "epmprojectedsolidangle") - return EPM_PROJECTED_SOLID_ANGLE; - return std::nullopt; - } - - void applyEarlyCommandLineOverrides() - { - if (m_commandLine.geometryOverride.has_value()) - guiControlled.PTPipeline = static_cast(m_commandLine.geometryOverride.value()); - if (m_commandLine.methodOverride.has_value()) - guiControlled.polygonMethod = static_cast(m_commandLine.methodOverride.value()); - if (m_commandLine.sppOverride.has_value()) - guiControlled.spp = m_commandLine.sppOverride.value(); - if (m_commandLine.depthOverride.has_value()) - guiControlled.depth = m_commandLine.depthOverride.value(); - if (m_commandLine.rwmcOverride.has_value()) - guiControlled.useRWMC = m_commandLine.rwmcOverride.value(); - } - - void applyLateCommandLineOverrides() - { - if (m_commandLine.rwmcStartOverride.has_value()) - guiControlled.rwmcParams.start = m_commandLine.rwmcStartOverride.value(); - if (m_commandLine.rwmcBaseOverride.has_value()) - guiControlled.rwmcParams.base = m_commandLine.rwmcBaseOverride.value(); - if (m_commandLine.rwmcMinReliableLumaOverride.has_value()) - guiControlled.rwmcParams.minReliableLuma = m_commandLine.rwmcMinReliableLumaOverride.value(); - if (m_commandLine.rwmcKappaOverride.has_value()) - guiControlled.rwmcParams.kappa = m_commandLine.rwmcKappaOverride.value(); - guiControlled.rwmcParams.sampleCount = guiControlled.spp; - } - - void resetRWMCParamsToDefaults() - { - guiControlled.rwmcParams.start = hlsl::dot(hlsl::transpose(colorspace::scRGBtoXYZ)[1], LightEminence); - guiControlled.rwmcParams.base = 8.0f; - guiControlled.rwmcParams.minReliableLuma = 1.0f; - guiControlled.rwmcParams.kappa = 1.0f; - guiControlled.rwmcParams.sampleCount = guiControlled.spp; - } - - bool parseCommandLine() - { - argparse::ArgumentParser parser("31_hlslpathtracer"); - parser.add_argument("--ci") - .help("Run in CI mode: save a scene screenshot and exit.") - .default_value(false) - .implicit_value(true); - parser.add_argument("--ci-screenshot") - .nargs(1) - .help("Override the CI scene screenshot output path"); - parser.add_argument("--pipeline-cache-dir") - .nargs(1) - .help("Override the PATH_TRACER pipeline cache root directory"); - parser.add_argument("--clear-pipeline-cache") - .help("Clear the PATH_TRACER cache root before startup") - .flag(); - parser.add_argument("--shader") - .nargs(1) - .help("Select startup geometry: sphere, triangle, rectangle"); - parser.add_argument("--method") - .nargs(1) - .help("Select startup method: area, solid-angle, projected-solid-angle"); - parser.add_argument("--spp") - .scan<'i', int>() - .help("Override startup samples per pixel"); - parser.add_argument("--depth") - .scan<'i', int>() - .help("Override startup path depth"); - parser.add_argument("--rwmc") - .help("Enable RWMC at startup") - .default_value(false) - .implicit_value(true); - parser.add_argument("--rwmc-start") - .scan<'g', float>() - .help("Override RWMC start threshold"); - parser.add_argument("--rwmc-base") - .scan<'g', float>() - .help("Override RWMC base"); - parser.add_argument("--rwmc-min-reliable") - .scan<'g', float>() - .help("Override RWMC minimum reliable luma"); - parser.add_argument("--rwmc-kappa") - .scan<'g', float>() - .help("Override RWMC kappa"); - - try - { - parser.parse_args({ argv.data(), argv.data() + argv.size() }); - } - catch (const std::exception& e) - { - m_logger->log("Failed to parse arguments: %s", ILogger::ELL_ERROR, e.what()); - return false; - } - - m_commandLine.ciMode = parser.get("--ci"); - m_commandLine.ciScreenshotPath = localOutputCWD / "31_hlslpathtracer_ci.png"; - if (parser.present("--ci-screenshot")) - m_commandLine.ciScreenshotPath = path(parser.get("--ci-screenshot")); - m_commandLine.pipelineCacheDirOverride.reset(); - if (parser.present("--pipeline-cache-dir")) - m_commandLine.pipelineCacheDirOverride = path(parser.get("--pipeline-cache-dir")); - m_commandLine.clearPipelineCache = parser.get("--clear-pipeline-cache"); - m_commandLine.geometryOverride.reset(); - if (parser.present("--shader")) - { - const auto geometryValue = parser.get("--shader"); - m_commandLine.geometryOverride = parseGeometryOverride(geometryValue); - if (!m_commandLine.geometryOverride.has_value()) - { - m_logger->log("Unknown --shader value: %s", ILogger::ELL_ERROR, geometryValue.c_str()); - return false; - } - } - m_commandLine.methodOverride.reset(); - if (parser.present("--method")) - { - const auto methodValue = parser.get("--method"); - m_commandLine.methodOverride = parseMethodOverride(methodValue); - if (!m_commandLine.methodOverride.has_value()) - { - m_logger->log("Unknown --method value: %s", ILogger::ELL_ERROR, methodValue.c_str()); - return false; - } - } - m_commandLine.sppOverride.reset(); - if (parser.present("--spp")) - { - const auto spp = parser.get("--spp"); - if (spp < 1 || spp > static_cast((0x1u << MaxSamplesLog2) - 1u)) - { - m_logger->log("Invalid --spp value: %d", ILogger::ELL_ERROR, spp); - return false; - } - m_commandLine.sppOverride = spp; - } - m_commandLine.depthOverride.reset(); - if (parser.present("--depth")) - { - const auto depth = parser.get("--depth"); - if (depth < 1 || depth > static_cast((0x1u << MaxDepthLog2) - 1u)) - { - m_logger->log("Invalid --depth value: %d", ILogger::ELL_ERROR, depth); - return false; - } - m_commandLine.depthOverride = depth; - } - m_commandLine.rwmcOverride.reset(); - if (parser.is_used("--rwmc")) - m_commandLine.rwmcOverride = parser.get("--rwmc"); - m_commandLine.rwmcStartOverride.reset(); - if (parser.present("--rwmc-start")) - m_commandLine.rwmcStartOverride = parser.get("--rwmc-start"); - m_commandLine.rwmcBaseOverride.reset(); - if (parser.present("--rwmc-base")) - m_commandLine.rwmcBaseOverride = parser.get("--rwmc-base"); - m_commandLine.rwmcMinReliableLumaOverride.reset(); - if (parser.present("--rwmc-min-reliable")) - m_commandLine.rwmcMinReliableLumaOverride = parser.get("--rwmc-min-reliable"); - m_commandLine.rwmcKappaOverride.reset(); - if (parser.present("--rwmc-kappa")) - m_commandLine.rwmcKappaOverride = parser.get("--rwmc-kappa"); - return true; - } - - void requestExit() - { - m_exitRequested = true; - } - - void requestSceneScreenshot(path outputPath, const bool exitAfterCapture) - { - m_sceneScreenshotRequested = true; - m_sceneScreenshotExitAfterCapture = exitAfterCapture; - m_pendingSceneScreenshotPath = std::move(outputPath); - } - - path getNextSceneScreenshotPath() - { - return localOutputCWD / ("31_hlslpathtracer_scene_" + std::to_string(m_sceneScreenshotCounter++) + ".png"); - } - - void maybeQueueCiScreenshotRequest() - { - if (!m_commandLine.ciMode || m_ciScreenshotCaptured || m_sceneScreenshotRequested || !m_startupLog.hasPathtraceOutput) - return; - - ++m_ciRenderableFrameCounter; - if (m_ciRenderableFrameCounter < CiFramesBeforeCapture) - return; - - requestSceneScreenshot(m_commandLine.ciScreenshotPath, true); - } - - void maybeSaveSceneScreenshot(IQueue* const queue, const IQueue::SSubmitInfo::SSemaphoreInfo& rendered) - { - if (!m_sceneScreenshotRequested || !m_pendingSceneScreenshotPath.has_value() || !rendered.semaphore) - return; - - const ISemaphore::SWaitInfo waitInfo[] = - { - { - .semaphore = rendered.semaphore, - .value = rendered.value - } - }; - if (m_device->blockForSemaphores(waitInfo) != ISemaphore::WAIT_RESULT::SUCCESS) - { - m_logger->log("Scene screenshot failed: could not wait for rendered frame.", ILogger::ELL_ERROR); - m_sceneScreenshotRequested = false; - m_pendingSceneScreenshotPath.reset(); - if (m_sceneScreenshotExitAfterCapture) - requestExit(); - return; - } - - const auto screenshotPath = std::move(*m_pendingSceneScreenshotPath); - m_pendingSceneScreenshotPath.reset(); - m_sceneScreenshotRequested = false; - - const bool ok = ext::ScreenShot::createScreenShot( - m_device.get(), - queue, - nullptr, - m_outImgView.get(), - m_assetMgr.get(), - screenshotPath, - asset::IImage::LAYOUT::READ_ONLY_OPTIMAL, - asset::ACCESS_FLAGS::SHADER_READ_BITS); - - if (ok) - m_logger->log("Scene screenshot saved to \"%s\".", ILogger::ELL_INFO, screenshotPath.string().c_str()); - else - m_logger->log("Scene screenshot failed to save.", ILogger::ELL_ERROR); - - if (m_sceneScreenshotExitAfterCapture) - { - m_ciScreenshotCaptured = true; - requestExit(); - } - m_sceneScreenshotExitAfterCapture = false; - } - - static std::string hashToHex(const core::blake3_hash_t& hash) - { - static constexpr char digits[] = "0123456789abcdef"; - static constexpr size_t HexCharsPerByte = 2ull; - static constexpr uint32_t HighNibbleBitOffset = 4u; - static constexpr uint8_t NibbleMask = 0xfu; - const auto hashByteCount = sizeof(hash.data); - std::string retval; - retval.resize(hashByteCount * HexCharsPerByte); - for (size_t i = 0ull; i < hashByteCount; ++i) - { - const auto hexOffset = i * HexCharsPerByte; - retval[hexOffset] = digits[(hash.data[i] >> HighNibbleBitOffset) & NibbleMask]; - retval[hexOffset + 1ull] = digits[hash.data[i] & NibbleMask]; - } - return retval; - } - - path getDefaultPipelineCacheDir() const - { - if (const auto* localAppData = std::getenv("LOCALAPPDATA"); localAppData && localAppData[0] != '\0') - return path(localAppData) / "nabla/examples/31_HLSLPathTracer/pipeline/cache"; - return localOutputCWD / "pipeline/cache"; - } - - path getRuntimeConfigPath() const - { - return system::executableDirectory() / RuntimeConfigFilename; - } - - std::optional tryGetPipelineCacheDirFromRuntimeConfig() const - { - const auto configPath = getRuntimeConfigPath(); - if (!m_system->exists(configPath, IFile::ECF_READ)) - return std::nullopt; - - std::ifstream input(configPath); - if (!input.is_open()) - return std::nullopt; - - nlohmann::json json; - try - { - input >> json; - } - catch (const std::exception& e) - { - m_logger->log("Failed to parse PATH_TRACER runtime config %s: %s", ILogger::ELL_WARNING, configPath.string().c_str(), e.what()); - return std::nullopt; - } - - const auto cacheRootIt = json.find("cache_root"); - if (cacheRootIt == json.end() || !cacheRootIt->is_string()) - return std::nullopt; - - const auto cacheRoot = cacheRootIt->get(); - if (cacheRoot.empty()) - return std::nullopt; - - const path relativeRoot(cacheRoot); - if (relativeRoot.is_absolute()) - { - m_logger->log("Ignoring absolute cache_root in %s", ILogger::ELL_WARNING, configPath.string().c_str()); - return std::nullopt; - } - - return (configPath.parent_path() / relativeRoot).lexically_normal(); - } - - path getPipelineCacheRootDir() const - { - if (m_commandLine.pipelineCacheDirOverride.has_value()) - return m_commandLine.pipelineCacheDirOverride.value(); - if (const auto runtimeConfigDir = tryGetPipelineCacheDirFromRuntimeConfig(); runtimeConfigDir.has_value()) - return runtimeConfigDir.value(); - return getDefaultPipelineCacheDir(); - } - - path getPipelineCacheBlobPath() const - { - const auto key = m_device->getPipelineCacheKey(); - return getPipelineCacheRootDir() / "blob" / BuildConfigName / (std::string(key.deviceAndDriverUUID) + ".bin"); - } - - path getSpirvCacheDir() const - { - return getPipelineCacheRootDir() / "spirv" / BuildConfigName; - } - - path getTrimmedShaderCachePath(const IShader* shader, const char* const entryPoint) const - { - core::blake3_hasher hasher; - hasher << std::string_view(shader ? shader->getFilepathHint() : std::string_view{}); - hasher << std::string_view(entryPoint); - if (shader) - { - if (const auto* const content = shader->getContent()) - { - auto contentHash = content->getContentHash(); - if (contentHash == ICPUBuffer::INVALID_HASH) - contentHash = content->computeContentHash(); - hasher << contentHash; - } - } - return getSpirvCacheDir() / (hashToHex(static_cast(hasher)) + ".spv"); - } - - path getValidatedSpirvMarkerPath(const ICPUBuffer* spirvBuffer) const - { - auto contentHash = spirvBuffer->getContentHash(); - if (contentHash == ICPUBuffer::INVALID_HASH) - contentHash = spirvBuffer->computeContentHash(); - return getSpirvCacheDir() / (hashToHex(contentHash) + ".hash"); - } - - size_t getBackgroundPipelineBuildBudget() const - { - static constexpr uint32_t ReservedForegroundThreadCount = 1u; - const auto concurrency = std::thread::hardware_concurrency(); - if (concurrency > ReservedForegroundThreadCount) - return static_cast(concurrency - ReservedForegroundThreadCount); - return ReservedForegroundThreadCount; - } - - bool ensureCacheDirectoryExists(const path& dir, const char* const description) - { - if (dir.empty() || m_system->isDirectory(dir)) - return true; - - if (m_system->createDirectory(dir) || m_system->isDirectory(dir)) - return true; - - m_logger->log("Failed to create %s %s", ILogger::ELL_WARNING, description, dir.string().c_str()); - return false; - } - - bool finalizeCacheFile(const path& tempPath, const path& finalPath, const char* const description) - { - m_system->deleteFile(finalPath); - const auto ec = m_system->moveFileOrDirectory(tempPath, finalPath); - if (!ec) - return true; - - m_system->deleteFile(tempPath); - m_logger->log("Failed to finalize %s %s", ILogger::ELL_WARNING, description, finalPath.string().c_str()); - return false; - } - - void initializePipelineCache() - { - m_pipelineCache.blobPath = getPipelineCacheBlobPath(); - m_pipelineCache.trimmedShaders.rootDir = getSpirvCacheDir(); - m_pipelineCache.trimmedShaders.validationDir = getSpirvCacheDir(); - if (!m_pipelineCache.trimmedShaders.trimmer) - m_pipelineCache.trimmedShaders.trimmer = core::make_smart_refctd_ptr(); - const auto pipelineCacheRootDir = getPipelineCacheRootDir(); - std::error_code ec; - m_pipelineCache.loadedBytes = 0ull; - m_pipelineCache.loadedFromDisk = false; - m_pipelineCache.clearedOnStartup = m_commandLine.clearPipelineCache; - m_pipelineCache.newlyReadyPipelinesSinceLastSave = 0ull; - m_pipelineCache.checkpointedAfterFirstSubmit = false; - m_pipelineCache.lastSaveAt = clock_t::now(); - if (m_commandLine.clearPipelineCache) - { - if (m_system->isDirectory(pipelineCacheRootDir) && !m_system->deleteDirectory(pipelineCacheRootDir)) - m_logger->log("Failed to clear pipeline cache directory %s", ILogger::ELL_WARNING, pipelineCacheRootDir.string().c_str()); - else - m_logger->log("PATH_TRACER_PIPELINE_CACHE clear root=%s", ILogger::ELL_INFO, pipelineCacheRootDir.string().c_str()); - } - ensureCacheDirectoryExists(m_pipelineCache.blobPath.parent_path(), "pipeline cache directory"); - ensureCacheDirectoryExists(m_pipelineCache.trimmedShaders.rootDir, "trimmed shader cache directory"); - ensureCacheDirectoryExists(m_pipelineCache.trimmedShaders.validationDir, "validated shader cache directory"); - - std::vector initialData; - { - std::ifstream input(m_pipelineCache.blobPath, std::ios::binary | std::ios::ate); - if (input.is_open()) - { - const auto size = input.tellg(); - if (size > 0) - { - initialData.resize(static_cast(size)); - input.seekg(0, std::ios::beg); - input.read(reinterpret_cast(initialData.data()), static_cast(initialData.size())); - if (!input) - initialData.clear(); - } - } - } - - std::span initialDataSpan = {}; - if (!initialData.empty()) - { - initialDataSpan = { initialData.data(), initialData.size() }; - m_pipelineCache.loadedBytes = initialData.size(); - m_pipelineCache.loadedFromDisk = true; - } - - m_pipelineCache.object = m_device->createPipelineCache(initialDataSpan); - if (!m_pipelineCache.object && !initialData.empty()) - { - m_logger->log("Pipeline cache blob at %s was rejected. Falling back to empty cache.", ILogger::ELL_WARNING, m_pipelineCache.blobPath.string().c_str()); - m_pipelineCache.object = m_device->createPipelineCache(std::span{}); - } - if (!m_pipelineCache.object) - { - m_logger->log("Failed to create PATH_TRACER pipeline cache.", ILogger::ELL_WARNING); - return; - } - - m_pipelineCache.object->setObjectDebugName("PATH_TRACER Pipeline Cache"); - m_logger->log("PATH_TRACER pipeline cache path: %s", ILogger::ELL_INFO, m_pipelineCache.blobPath.string().c_str()); - m_logger->log("PATH_TRACER trimmed shader cache path: %s", ILogger::ELL_INFO, m_pipelineCache.trimmedShaders.rootDir.string().c_str()); - m_logger->log("PATH_TRACER validated shader cache path: %s", ILogger::ELL_INFO, m_pipelineCache.trimmedShaders.validationDir.string().c_str()); - m_logger->log( - "PATH_TRACER_PIPELINE_CACHE init clear=%u loaded_from_disk=%u loaded_bytes=%zu path=%s", - ILogger::ELL_INFO, - m_pipelineCache.clearedOnStartup ? 1u : 0u, - m_pipelineCache.loadedFromDisk ? 1u : 0u, - m_pipelineCache.loadedBytes, - m_pipelineCache.blobPath.string().c_str() - ); - if (!initialData.empty()) - m_logger->log("Loaded PATH_TRACER pipeline cache blob: %s", ILogger::ELL_INFO, m_pipelineCache.blobPath.string().c_str()); - } - - smart_refctd_ptr tryLoadTrimmedShaderFromDisk(const IShader* sourceShader, const char* const entryPoint) - { - const auto cachePath = getTrimmedShaderCachePath(sourceShader, entryPoint); - std::ifstream input(cachePath, std::ios::binary | std::ios::ate); - if (!input.is_open()) - return nullptr; - - const auto size = input.tellg(); - if (size <= 0) - return nullptr; - - std::vector bytes(static_cast(size)); - input.seekg(0, std::ios::beg); - input.read(reinterpret_cast(bytes.data()), static_cast(bytes.size())); - if (!input) - return nullptr; - - auto buffer = ICPUBuffer::create({ { bytes.size() }, bytes.data() }); - if (!buffer) - return nullptr; - buffer->setContentHash(buffer->computeContentHash()); - { - std::lock_guard lock(m_pipelineCache.trimmedShaders.mutex); - m_pipelineCache.trimmedShaders.loadedBytes += bytes.size(); - ++m_pipelineCache.trimmedShaders.loadedFromDiskCount; - } - m_logger->log( - "PATH_TRACER_SHADER_CACHE load entrypoint=%s bytes=%zu path=%s", - ILogger::ELL_INFO, - entryPoint, - bytes.size(), - cachePath.string().c_str() - ); - return core::make_smart_refctd_ptr(std::move(buffer), IShader::E_CONTENT_TYPE::ECT_SPIRV, std::string(sourceShader->getFilepathHint())); - } - - bool hasValidatedSpirvMarker(const ICPUBuffer* spirvBuffer) const - { - return m_system->exists(getValidatedSpirvMarkerPath(spirvBuffer), IFile::ECF_READ); - } - - void saveValidatedSpirvMarker(const ICPUBuffer* spirvBuffer) - { - const auto markerPath = getValidatedSpirvMarkerPath(spirvBuffer); - if (!ensureCacheDirectoryExists(markerPath.parent_path(), "validated shader cache directory")) - return; - - auto tempPath = markerPath; - tempPath += ".tmp"; - { - std::ofstream output(tempPath, std::ios::binary | std::ios::trunc); - if (!output.is_open()) - { - m_logger->log("Failed to open validated shader marker temp file %s", ILogger::ELL_WARNING, tempPath.string().c_str()); - return; - } - output << "ok\n"; - output.flush(); - if (!output) - { - output.close(); - m_system->deleteFile(tempPath); - m_logger->log("Failed to write validated shader marker %s", ILogger::ELL_WARNING, tempPath.string().c_str()); - return; - } - } - - finalizeCacheFile(tempPath, markerPath, "validated shader marker"); - } - - bool ensurePreparedShaderValidated(const smart_refctd_ptr& preparedShader) - { - if (!preparedShader) - return false; - - auto* const content = preparedShader->getContent(); - if (!content) - return false; - - if (hasValidatedSpirvMarker(content)) - { - { - std::lock_guard lock(m_pipelineCache.trimmedShaders.mutex); - m_pipelineCache.trimmedShaders.trimmer->markValidated(content); - } - return true; - } - - { - std::lock_guard lock(m_pipelineCache.trimmedShaders.mutex); - if (!m_pipelineCache.trimmedShaders.trimmer->ensureValidated(content, m_logger.get())) - return false; - } - - saveValidatedSpirvMarker(content); - return true; - } - - void saveTrimmedShaderToDisk(const IShader* shader, const char* const entryPoint, const path& cachePath) - { - const auto* content = shader->getContent(); - if (!content || !content->getPointer() || cachePath.empty()) - return; - - if (!ensureCacheDirectoryExists(cachePath.parent_path(), "trimmed shader cache directory")) - return; - - auto tempPath = cachePath; - tempPath += ".tmp"; - { - std::ofstream output(tempPath, std::ios::binary | std::ios::trunc); - if (!output.is_open()) - { - m_logger->log("Failed to open trimmed shader cache temp file %s", ILogger::ELL_WARNING, tempPath.string().c_str()); - return; - } - output.write(reinterpret_cast(content->getPointer()), static_cast(content->getSize())); - output.flush(); - if (!output) - { - output.close(); - m_system->deleteFile(tempPath); - m_logger->log("Failed to write trimmed shader cache blob to %s", ILogger::ELL_WARNING, tempPath.string().c_str()); - return; - } - } - - if (!finalizeCacheFile(tempPath, cachePath, "trimmed shader cache blob")) - return; - - { - std::lock_guard lock(m_pipelineCache.trimmedShaders.mutex); - m_pipelineCache.trimmedShaders.savedBytes += content->getSize(); - ++m_pipelineCache.trimmedShaders.savedToDiskCount; - } - m_logger->log( - "PATH_TRACER_SHADER_CACHE save entrypoint=%s bytes=%zu path=%s", - ILogger::ELL_INFO, - entryPoint, - content->getSize(), - cachePath.string().c_str() - ); - } - - smart_refctd_ptr getPreparedShaderForEntryPoint(const smart_refctd_ptr& shaderModule, const char* const entryPoint) - { - if (!shaderModule || shaderModule->getContentType() != IShader::E_CONTENT_TYPE::ECT_SPIRV) - return shaderModule; - - const auto cachePath = getTrimmedShaderCachePath(shaderModule.get(), entryPoint); - const auto cacheKey = cachePath.string(); - { - std::lock_guard lock(m_pipelineCache.trimmedShaders.mutex); - const auto found = m_pipelineCache.trimmedShaders.runtimeShaders.find(cacheKey); - if (found != m_pipelineCache.trimmedShaders.runtimeShaders.end()) - return found->second; - } - - const auto startedAt = clock_t::now(); - auto preparedShader = tryLoadTrimmedShaderFromDisk(shaderModule.get(), entryPoint); - bool cameFromDisk = static_cast(preparedShader); - bool wasTrimmed = false; - if (!preparedShader) - { - const core::set entryPoints = { asset::ISPIRVEntryPointTrimmer::EntryPoint{ .name = entryPoint, .stage = hlsl::ShaderStage::ESS_COMPUTE } }; - const auto result = [&]() - { - std::lock_guard lock(m_pipelineCache.trimmedShaders.mutex); - return m_pipelineCache.trimmedShaders.trimmer->trim(shaderModule->getContent(), entryPoints, nullptr); - }(); - if (!result) - { - m_logger->log("Failed to prepare trimmed PATH_TRACER shader for %s. Falling back to the original module.", ILogger::ELL_WARNING, entryPoint); - return shaderModule; - } - if (result.spirv) - { - result.spirv->setContentHash(result.spirv->computeContentHash()); - preparedShader = core::make_smart_refctd_ptr(core::smart_refctd_ptr(result.spirv), IShader::E_CONTENT_TYPE::ECT_SPIRV, std::string(shaderModule->getFilepathHint())); - } - else - preparedShader = shaderModule; - - saveTrimmedShaderToDisk(preparedShader.get(), entryPoint, cachePath); - { - std::lock_guard lock(m_pipelineCache.trimmedShaders.mutex); - ++m_pipelineCache.trimmedShaders.generatedCount; - } - wasTrimmed = (preparedShader != shaderModule); - } - - if (!ensurePreparedShaderValidated(preparedShader)) - { - m_logger->log("Prepared PATH_TRACER shader for %s is not valid SPIR-V", ILogger::ELL_ERROR, entryPoint); - return nullptr; - } - - { - std::lock_guard lock(m_pipelineCache.trimmedShaders.mutex); - const auto [it, inserted] = m_pipelineCache.trimmedShaders.runtimeShaders.emplace(cacheKey, preparedShader); - if (!inserted) - preparedShader = it->second; - } - - const auto wallMs = std::chrono::duration_cast(clock_t::now() - startedAt).count(); - m_logger->log( - "PATH_TRACER_SHADER_CACHE ready entrypoint=%s wall_ms=%lld from_disk=%u trimmed=%u", - ILogger::ELL_INFO, - entryPoint, - static_cast(wallMs), - cameFromDisk ? 1u : 0u, - wasTrimmed ? 1u : 0u - ); - return preparedShader; - } - - void savePipelineCache() - { - if (!m_pipelineCache.object || !m_pipelineCache.dirty || m_pipelineCache.blobPath.empty()) - return; - - const auto saveStartedAt = clock_t::now(); - auto cpuCache = m_pipelineCache.object->convertToCPUCache(); - if (!cpuCache) - return; - - const auto& entries = cpuCache->getEntries(); - const auto found = entries.find(m_device->getPipelineCacheKey()); - if (found == entries.end() || !found->second.bin || found->second.bin->empty()) - return; - - if (!ensureCacheDirectoryExists(m_pipelineCache.blobPath.parent_path(), "pipeline cache directory")) - return; - - auto tempPath = m_pipelineCache.blobPath; - tempPath += ".tmp"; - { - std::ofstream output(tempPath, std::ios::binary | std::ios::trunc); - if (!output.is_open()) - { - m_logger->log("Failed to open pipeline cache temp file %s", ILogger::ELL_WARNING, tempPath.string().c_str()); - return; - } - output.write(reinterpret_cast(found->second.bin->data()), static_cast(found->second.bin->size())); - output.flush(); - if (!output) - { - output.close(); - m_system->deleteFile(tempPath); - m_logger->log("Failed to write pipeline cache blob to %s", ILogger::ELL_WARNING, tempPath.string().c_str()); - return; - } - } - - if (!finalizeCacheFile(tempPath, m_pipelineCache.blobPath, "pipeline cache blob")) - return; - - m_pipelineCache.dirty = false; - m_pipelineCache.savedBytes = found->second.bin->size(); - m_pipelineCache.newlyReadyPipelinesSinceLastSave = 0ull; - m_pipelineCache.lastSaveAt = clock_t::now(); - const auto saveElapsedMs = std::chrono::duration_cast(clock_t::now() - saveStartedAt).count(); - m_logger->log( - "PATH_TRACER_PIPELINE_CACHE save bytes=%zu wall_ms=%lld path=%s", - ILogger::ELL_INFO, - m_pipelineCache.savedBytes, - static_cast(saveElapsedMs), - m_pipelineCache.blobPath.string().c_str() - ); - m_logger->log("Saved PATH_TRACER pipeline cache blob: %s", ILogger::ELL_INFO, m_pipelineCache.blobPath.string().c_str()); - } - - void maybeCheckpointPipelineCache() - { - if (!m_pipelineCache.object || !m_pipelineCache.dirty) - return; - - if (m_startupLog.loggedFirstRenderSubmit && !m_pipelineCache.checkpointedAfterFirstSubmit) - { - savePipelineCache(); - m_pipelineCache.checkpointedAfterFirstSubmit = true; - return; - } - - if (!m_pipelineCache.warmup.started || m_pipelineCache.warmup.loggedComplete) - return; - - static constexpr size_t WarmupCheckpointThreshold = 4ull; - if (m_pipelineCache.newlyReadyPipelinesSinceLastSave < WarmupCheckpointThreshold) - return; - - const auto elapsedSinceLastSave = std::chrono::duration_cast(clock_t::now() - m_pipelineCache.lastSaveAt).count(); - if (elapsedSinceLastSave < 1000ll) - return; - - savePipelineCache(); - } - - smart_refctd_ptr loadRenderShader(const E_LIGHT_GEOMETRY geometry, const bool persistentWorkGroups, const bool rwmc) - { - (void)persistentWorkGroups; - switch (geometry) - { - case ELG_SPHERE: - if (rwmc) - return loadPrecompiledShader(); - return loadPrecompiledShader(); - case ELG_TRIANGLE: - if (rwmc) - return loadPrecompiledShader(); - return loadPrecompiledShader(); - case ELG_RECTANGLE: - if (rwmc) - return loadPrecompiledShader(); - return loadPrecompiledShader(); - default: - return nullptr; - } - } - - struct SCommandLineOptions - { - bool ciMode = false; - path ciScreenshotPath; - std::optional pipelineCacheDirOverride; - bool clearPipelineCache = false; - std::optional geometryOverride; - std::optional methodOverride; - std::optional sppOverride; - std::optional depthOverride; - std::optional rwmcOverride; - std::optional rwmcStartOverride; - std::optional rwmcBaseOverride; - std::optional rwmcMinReliableLumaOverride; - std::optional rwmcKappaOverride; - }; - - size_t getRunningPipelineBuildCount() const - { - return cached_pipeline_state::getRunningPipelineBuildCount(m_renderPipelines, m_resolvePipelineState); - } - - size_t getKnownRenderPipelineCount() const - { - size_t count = 0ull; - bool seen[ELG_COUNT][BinaryToggleCount][BinaryToggleCount][EPM_COUNT] = {}; - for (auto geometry = 0u; geometry < ELG_COUNT; ++geometry) - { - for (const auto persistentWorkGroups : { UsePersistentWorkGroups }) - { - for (auto rwmc = 0u; rwmc < BinaryToggleCount; ++rwmc) - { - for (auto method = 0u; method < EPM_COUNT; ++method) - { - const auto pipelineMethod = static_cast(getRenderVariantInfo( - static_cast(geometry), - static_cast(persistentWorkGroups), - static_cast(method) - ).pipelineMethod); - if (seen[geometry][persistentWorkGroups][rwmc][pipelineMethod]) - continue; - seen[geometry][persistentWorkGroups][rwmc][pipelineMethod] = true; - ++count; - } - } - } - } - return count; - } - - size_t getReadyRenderPipelineCount() const - { - return cached_pipeline_state::getReadyRenderPipelineCount(m_renderPipelines); - } - - void enqueueWarmupJob(const SWarmupJob& job) - { - for (const auto& existing : m_pipelineCache.warmup.queue) - { - if (existing.type != job.type) - continue; - if (existing.type == SWarmupJob::E_TYPE::Resolve) - return; - if ( - existing.geometry == job.geometry && - existing.persistentWorkGroups == job.persistentWorkGroups && - existing.rwmc == job.rwmc && - getRenderVariantInfo(existing.geometry, existing.persistentWorkGroups, existing.method).pipelineMethod == - getRenderVariantInfo(job.geometry, job.persistentWorkGroups, job.method).pipelineMethod - ) - return; - } - m_pipelineCache.warmup.queue.push_back(job); - } - - bool launchWarmupJobIfNeeded(const SWarmupJob& job) - { - if (job.type == SWarmupJob::E_TYPE::Resolve) - { - if (m_resolvePipelineState.pipeline || m_resolvePipelineState.pendingPipeline.valid()) - return false; - ensureResolvePipeline(); - return m_resolvePipelineState.pendingPipeline.valid(); - } - - auto& pipelines = m_renderPipelines.getPipelines(job.persistentWorkGroups, job.rwmc); - auto& pendingPipelines = m_renderPipelines.getPendingPipelines(job.persistentWorkGroups, job.rwmc); - const auto methodIx = static_cast(getRenderVariantInfo(job.geometry, job.persistentWorkGroups, job.method).pipelineMethod); - if (pipelines[job.geometry][methodIx] || pendingPipelines[job.geometry][methodIx].valid()) - return false; - - ensureRenderPipeline(job.geometry, job.persistentWorkGroups, job.rwmc, job.method); - return pendingPipelines[job.geometry][methodIx].valid(); - } - - void pumpPipelineWarmup() - { - if (!m_pipelineCache.warmup.started) - return; - - while (!m_pipelineCache.warmup.queue.empty() && getRunningPipelineBuildCount() < m_pipelineCache.warmup.budget) - { - const auto job = m_pipelineCache.warmup.queue.front(); - m_pipelineCache.warmup.queue.pop_front(); - if (launchWarmupJobIfNeeded(job)) - ++m_pipelineCache.warmup.launchedJobs; - else - ++m_pipelineCache.warmup.skippedJobs; - } - - if (!m_pipelineCache.warmup.loggedComplete && m_pipelineCache.warmup.queue.empty() && getRunningPipelineBuildCount() == 0ull) - { - m_pipelineCache.warmup.loggedComplete = true; - const auto warmupElapsedMs = std::chrono::duration_cast(clock_t::now() - m_pipelineCache.warmup.beganAt).count(); - const auto readyRenderPipelines = getReadyRenderPipelineCount(); - const auto totalRenderPipelines = getKnownRenderPipelineCount(); - m_logger->log( - "PATH_TRACER_PIPELINE_CACHE warmup_complete wall_ms=%lld queued_jobs=%zu launched_jobs=%zu skipped_jobs=%zu max_parallel=%zu ready_render=%zu total_render=%zu resolve_ready=%u", - ILogger::ELL_INFO, - static_cast(warmupElapsedMs), - m_pipelineCache.warmup.queuedJobs, - m_pipelineCache.warmup.launchedJobs, - m_pipelineCache.warmup.skippedJobs, - m_pipelineCache.warmup.budget, - readyRenderPipelines, - totalRenderPipelines, - m_resolvePipelineState.pipeline ? 1u : 0u - ); - logStartupEvent("pipeline_warmup_complete"); - savePipelineCache(); - } - } - - pipeline_future_t requestComputePipelineBuild(smart_refctd_ptr shaderModule, IGPUPipelineLayout* const pipelineLayout, const char* const entryPoint) - { - if (!shaderModule) - return {}; - - return std::async( - std::launch::async, - [ - this, - device = m_device, - pipelineCache = m_pipelineCache.object, - shader = std::move(shaderModule), - layout = smart_refctd_ptr(pipelineLayout), - requiredSubgroupSize = m_requiredSubgroupSize, - logger = m_logger.get(), - entryPointName = std::string(entryPoint), - cacheLoadedFromDisk = m_pipelineCache.loadedFromDisk - ]() -> smart_refctd_ptr - { - const auto startedAt = clock_t::now(); - auto preparedShader = getPreparedShaderForEntryPoint(shader, entryPointName.c_str()); - if (!preparedShader) - return nullptr; - smart_refctd_ptr pipeline; - IGPUComputePipeline::SCreationParams params = {}; - params.layout = layout.get(); - params.shader.shader = preparedShader.get(); - params.shader.entryPoint = entryPointName.c_str(); - params.shader.entries = nullptr; - params.cached.requireFullSubgroups = true; - params.shader.requiredSubgroupSize = requiredSubgroupSize; - if (!device->createComputePipelines(pipelineCache.get(), { ¶ms, 1 }, &pipeline)) - { - if (logger) - logger->log("Failed to create precompiled path tracing pipeline for %s", ILogger::ELL_ERROR, entryPointName.c_str()); - return nullptr; - } - if (logger) - { - const auto wallMs = std::chrono::duration_cast(clock_t::now() - startedAt).count(); - logger->log( - "PATH_TRACER_PIPELINE_BUILD entrypoint=%s wall_ms=%lld cache_loaded_from_disk=%u", - ILogger::ELL_INFO, - entryPointName.c_str(), - static_cast(wallMs), - cacheLoadedFromDisk ? 1u : 0u - ); - } - return pipeline; - } - ); - } - - void pollPendingPipeline(pipeline_future_t& future, smart_refctd_ptr& pipeline) - { - if (cached_pipeline_state::pollPendingPipeline(future, pipeline)) - { - m_pipelineCache.dirty = true; - ++m_pipelineCache.newlyReadyPipelinesSinceLastSave; - } - } - - void pollPendingPipelines() - { - cached_pipeline_state::pollPendingPipelines( - m_renderPipelines, - m_resolvePipelineState, - m_pipelineCache.dirty, - m_pipelineCache.newlyReadyPipelinesSinceLastSave - ); - } - - void waitForPendingPipelines() - { - cached_pipeline_state::waitForPendingPipelines( - m_renderPipelines, - m_resolvePipelineState, - m_pipelineCache.dirty, - m_pipelineCache.newlyReadyPipelinesSinceLastSave - ); - } - - IGPUComputePipeline* ensureRenderPipeline(const E_LIGHT_GEOMETRY geometry, const bool persistentWorkGroups, const bool rwmc, const E_POLYGON_METHOD polygonMethod) - { - auto& pipelines = m_renderPipelines.getPipelines(persistentWorkGroups, rwmc); - auto& pendingPipelines = m_renderPipelines.getPendingPipelines(persistentWorkGroups, rwmc); - const auto variantInfo = getRenderVariantInfo(geometry, persistentWorkGroups, polygonMethod); - const auto methodIx = static_cast(variantInfo.pipelineMethod); - auto& pipeline = pipelines[geometry][methodIx]; - auto& future = pendingPipelines[geometry][methodIx]; - - pollPendingPipeline(future, pipeline); - if (pipeline) - return pipeline.get(); - - if (!future.valid()) - { - const auto& shaders = m_renderPipelines.getShaders(persistentWorkGroups, rwmc); - auto* const layout = rwmc ? m_rwmcRenderPipelineLayout.get() : m_renderPipelineLayout.get(); - future = requestComputePipelineBuild(shaders[geometry], layout, variantInfo.entryPoint); - } - - return nullptr; - } - - IGPUComputePipeline* ensureResolvePipeline() - { - pollPendingPipeline(m_resolvePipelineState.pendingPipeline, m_resolvePipelineState.pipeline); - if (m_resolvePipelineState.pipeline) - return m_resolvePipelineState.pipeline.get(); - - if (!m_resolvePipelineState.pendingPipeline.valid()) - m_resolvePipelineState.pendingPipeline = requestComputePipelineBuild(m_resolvePipelineState.shader, m_resolvePipelineState.layout.get(), "resolve"); - - return nullptr; - } - - void kickoffPipelineWarmup() - { - m_pipelineCache.warmup.started = true; - m_pipelineCache.warmup.queue.clear(); - m_pipelineCache.warmup.loggedComplete = false; - m_pipelineCache.warmup.beganAt = clock_t::now(); - m_pipelineCache.warmup.budget = getBackgroundPipelineBuildBudget(); - m_pipelineCache.warmup.queuedJobs = 0ull; - m_pipelineCache.warmup.launchedJobs = 0ull; - m_pipelineCache.warmup.skippedJobs = 0ull; - const auto currentGeometry = static_cast(guiControlled.PTPipeline); - const auto currentMethod = static_cast(guiControlled.polygonMethod); - const auto enqueueRenderVariants = [this, currentGeometry](const E_LIGHT_GEOMETRY geometry, const E_POLYGON_METHOD preferredMethod) -> void - { - const auto enqueueForMethods = [this, geometry](const std::initializer_list methods, const bool preferRWMC) -> void - { - const bool rwmcOrder[2] = { preferRWMC, !preferRWMC }; - for (const auto method : methods) - { - for (const auto rwmc : rwmcOrder) - { - enqueueWarmupJob({ - .type = SWarmupJob::E_TYPE::Render, - .geometry = geometry, - .persistentWorkGroups = UsePersistentWorkGroups, - .rwmc = rwmc, - .method = method - }); - } - } - }; - - const bool preferRWMC = geometry == currentGeometry ? guiControlled.useRWMC : false; - switch (geometry) - { - case ELG_SPHERE: - enqueueForMethods({ EPM_SOLID_ANGLE }, preferRWMC); - break; - case ELG_TRIANGLE: - { - switch (preferredMethod) - { - case EPM_AREA: - enqueueForMethods({ EPM_AREA, EPM_SOLID_ANGLE, EPM_PROJECTED_SOLID_ANGLE }, preferRWMC); - break; - case EPM_SOLID_ANGLE: - enqueueForMethods({ EPM_SOLID_ANGLE, EPM_AREA, EPM_PROJECTED_SOLID_ANGLE }, preferRWMC); - break; - case EPM_PROJECTED_SOLID_ANGLE: - default: - enqueueForMethods({ EPM_PROJECTED_SOLID_ANGLE, EPM_AREA, EPM_SOLID_ANGLE }, preferRWMC); - break; - } - break; - } - case ELG_RECTANGLE: - switch (preferredMethod) - { - case EPM_AREA: - enqueueForMethods({ EPM_AREA, EPM_SOLID_ANGLE, EPM_PROJECTED_SOLID_ANGLE }, preferRWMC); - break; - case EPM_SOLID_ANGLE: - enqueueForMethods({ EPM_SOLID_ANGLE, EPM_AREA, EPM_PROJECTED_SOLID_ANGLE }, preferRWMC); - break; - case EPM_PROJECTED_SOLID_ANGLE: - default: - enqueueForMethods({ EPM_PROJECTED_SOLID_ANGLE, EPM_SOLID_ANGLE, EPM_AREA }, preferRWMC); - break; - } - break; - default: - break; - } - }; - - enqueueRenderVariants(currentGeometry, currentMethod); - for (auto geometry = 0u; geometry < ELG_COUNT; ++geometry) - { - const auto geometryEnum = static_cast(geometry); - if (geometryEnum == currentGeometry) - continue; - enqueueRenderVariants(geometryEnum, currentMethod); - } - enqueueWarmupJob({ .type = SWarmupJob::E_TYPE::Resolve }); - m_pipelineCache.warmup.queuedJobs = m_pipelineCache.warmup.queue.size(); - const auto logicalConcurrency = std::thread::hardware_concurrency(); - m_logger->log( - "PATH_TRACER_PIPELINE_CACHE warmup_start queued_jobs=%zu max_parallel=%zu logical_threads=%u current_geometry=%u current_method=%u", - ILogger::ELL_INFO, - m_pipelineCache.warmup.queuedJobs, - m_pipelineCache.warmup.budget, - logicalConcurrency, - static_cast(currentGeometry), - static_cast(currentMethod) - ); - pumpPipelineWarmup(); - } - - IGPUComputePipeline* pickPTPipeline() - { - return ensureRenderPipeline( - static_cast(guiControlled.PTPipeline), - UsePersistentWorkGroups, - guiControlled.useRWMC, - static_cast(guiControlled.polygonMethod) - ); - } - - private: - smart_refctd_ptr m_window; - smart_refctd_ptr> m_surface; - - // gpu resources - smart_refctd_ptr m_cmdPool; - SRenderPipelineStorage m_renderPipelines; - smart_refctd_ptr m_renderPipelineLayout; - smart_refctd_ptr m_rwmcRenderPipelineLayout; - SResolvePipelineState m_resolvePipelineState; - smart_refctd_ptr m_presentPipeline; - IPipelineBase::SUBGROUP_SIZE m_requiredSubgroupSize = IPipelineBase::SUBGROUP_SIZE::UNKNOWN; - uint64_t m_realFrameIx = 0; - std::array, MaxFramesInFlight> m_cmdBufs; - ISimpleManagedSurface::SAcquireResult m_currentImageAcquire = {}; - smart_refctd_ptr m_descriptorSet, m_presentDescriptorSet; - - core::smart_refctd_ptr m_guiDescriptorSetPool; - - // system resources - core::smart_refctd_ptr m_inputSystem; - InputSystem::ChannelReader mouse; - InputSystem::ChannelReader keyboard; - - // pathtracer resources - smart_refctd_ptr m_envMapView, m_scrambleView; - smart_refctd_ptr m_sequenceBuffer; - smart_refctd_ptr m_outImgView; - smart_refctd_ptr m_cascadeView; - uint8_t m_sequenceSamplesLog2; - - // sync - smart_refctd_ptr m_semaphore; - - // image upload resources - smart_refctd_ptr m_scratchSemaphore; - SIntendedSubmitInfo m_intendedSubmit; - - struct C_UI - { - nbl::core::smart_refctd_ptr manager; - - struct - { - core::smart_refctd_ptr gui, scene; - } samplers; - - core::smart_refctd_ptr descriptorSet; - } m_ui; - - Camera m_camera; - bool m_showUI; - bool m_exitRequested = false; - bool m_sceneScreenshotRequested = false; - bool m_sceneScreenshotExitAfterCapture = false; - bool m_ciScreenshotCaptured = false; - uint32_t m_ciRenderableFrameCounter = 0u; - uint64_t m_sceneScreenshotCounter = 0ull; - std::optional m_pendingSceneScreenshotPath; - - video::CDumbPresentationOracle m_oracle; - - uint16_t gcIndex = {}; - - struct GUIControllables - { - 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; - int PTPipeline = E_LIGHT_GEOMETRY::ELG_SPHERE; - int polygonMethod = EPM_PROJECTED_SOLID_ANGLE; - int spp = 32; - int depth = 3; - rwmc::SResolveParameters::SCreateParams rwmcParams; - bool useRWMC = false; - }; - GUIControllables guiControlled; - - hlsl::float32_t4x4 m_lightModelMatrix = { - 0.3f, 0.0f, 0.0f, 0.0f, - 0.0f, 0.3f, 0.0f, 0.0f, - 0.0f, 0.0f, 0.3f, 0.0f, - -1.0f, 1.5f, 0.0f, 1.0f, - }; - TransformRequestParams m_transformParams; - - clock_t::time_point m_startupBeganAt = clock_t::now(); - SCommandLineOptions m_commandLine; - SStartupLogState m_startupLog; - SPipelineCacheState m_pipelineCache; - IGPUCommandBuffer::SClearColorValue clearColor = { .float32 = {0.f,0.f,0.f,1.f} }; -}; - -NBL_MAIN_FUNC(HLSLComputePathtracer) diff --git a/31_HLSLPathTracer/path_tracer.runtime.json.in b/31_HLSLPathTracer/path_tracer.runtime.json.in deleted file mode 100644 index 29177c49f..000000000 --- a/31_HLSLPathTracer/path_tracer.runtime.json.in +++ /dev/null @@ -1,3 +0,0 @@ -{ - "cache_root": "@PT_CACHE_ROOT_JSON@" -} diff --git a/31_HLSLPathTracer/pt.cmake b/31_HLSLPathTracer/pt.cmake deleted file mode 100644 index a8927acdb..000000000 --- a/31_HLSLPathTracer/pt.cmake +++ /dev/null @@ -1,35 +0,0 @@ -macro(PT_APPEND_SPIRV_RULE) - set(options) - set(oneValueArgs VAR INPUT KEY) - set(multiValueArgs COMPILE_OPTIONS) - cmake_parse_arguments(PT_RULE "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) - if(PT_RULE_INPUT STREQUAL "" OR PT_RULE_KEY STREQUAL "") - message(FATAL_ERROR "PT_APPEND_SPIRV_RULE requires INPUT and KEY") - endif() - set(PT_RULE_JSON "{\"INPUT\":\"${PT_RULE_INPUT}\",\"KEY\":\"${PT_RULE_KEY}\"") - if(PT_RULE_COMPILE_OPTIONS) - set(PT_RULE_COMPILE_OPTIONS_JSON "") - foreach(PT_RULE_COMPILE_OPTION IN LISTS PT_RULE_COMPILE_OPTIONS) - string(APPEND PT_RULE_COMPILE_OPTIONS_JSON "\"${PT_RULE_COMPILE_OPTION}\",") - endforeach() - string(REGEX REPLACE ",$" "" PT_RULE_COMPILE_OPTIONS_JSON "${PT_RULE_COMPILE_OPTIONS_JSON}") - string(APPEND PT_RULE_JSON ",\"COMPILE_OPTIONS\":[${PT_RULE_COMPILE_OPTIONS_JSON}]") - endif() - string(APPEND PT_RULE_JSON "}") - list(APPEND ${PT_RULE_VAR} "${PT_RULE_JSON}") -endmacro() - -macro(PT_FINALIZE_JSON_PAYLOAD) - set(oneValueArgs INOUT) - cmake_parse_arguments(PT_PAYLOAD "" "${oneValueArgs}" "" ${ARGN}) - if(PT_PAYLOAD_INOUT STREQUAL "") - message(FATAL_ERROR "PT_FINALIZE_JSON_PAYLOAD requires INOUT") - endif() - string(JOIN ",\n" PT_RULES_BODY ${${PT_PAYLOAD_INOUT}}) - set(PT_PAYLOAD_TEMPLATE [=[ -[ -@PT_RULES_BODY@ -] -]=]) - string(CONFIGURE "${PT_PAYLOAD_TEMPLATE}" ${PT_PAYLOAD_INOUT} @ONLY) -endmacro() diff --git a/31_HLSLPathTracer/pt.variant_ids.cmake b/31_HLSLPathTracer/pt.variant_ids.cmake deleted file mode 100644 index 544b58a4e..000000000 --- a/31_HLSLPathTracer/pt.variant_ids.cmake +++ /dev/null @@ -1,4 +0,0 @@ -set(PT_SCENE_SPHERE 0) -set(PT_SCENE_TRIANGLE 1) -set(PT_SCENE_RECTANGLE 2) -set(PT_ENTRYPOINT_PERSISTENT 2) diff --git a/34_DebugDraw/CMakeLists.txt b/34_DebugDraw/CMakeLists.txt deleted file mode 100644 index 8d78f3de9..000000000 --- a/34_DebugDraw/CMakeLists.txt +++ /dev/null @@ -1,9 +0,0 @@ -if(NBL_BUILD_DEBUG_DRAW) - set(NBL_INCLUDE_SERACH_DIRECTORIES - "${CMAKE_CURRENT_SOURCE_DIR}/include" - ) - - nbl_create_executable_project("${NBL_EXTRA_SOURCES}" "" "${NBL_INCLUDE_SERACH_DIRECTORIES}" "" "${NBL_EXECUTABLE_PROJECT_CREATION_PCH_TARGET}") - - target_link_libraries(${EXECUTABLE_NAME} PRIVATE Nabla::ext::DebugDraw) -endif() diff --git a/34_DebugDraw/include/common.hpp b/34_DebugDraw/include/common.hpp deleted file mode 100644 index aad9bdb1d..000000000 --- a/34_DebugDraw/include/common.hpp +++ /dev/null @@ -1,22 +0,0 @@ -#ifndef __NBL_THIS_EXAMPLE_COMMON_H_INCLUDED__ -#define __NBL_THIS_EXAMPLE_COMMON_H_INCLUDED__ - -#include - -#include "nbl/examples/cameras/CCamera.hpp" -#include "nbl/examples/common/SimpleWindowedApplication.hpp" -#include "nbl/examples/common/CEventCallback.hpp" -#include "nbl/examples/examples.hpp" - -#include "nbl/ext/DebugDraw/CDrawAABB.h" - -using namespace nbl; -using namespace core; -using namespace hlsl; -using namespace system; -using namespace asset; -using namespace ui; -using namespace video; -using namespace nbl::examples; - -#endif // __NBL_THIS_EXAMPLE_COMMON_H_INCLUDED__ \ No newline at end of file diff --git a/34_DebugDraw/main.cpp b/34_DebugDraw/main.cpp deleted file mode 100644 index f2dd6210d..000000000 --- a/34_DebugDraw/main.cpp +++ /dev/null @@ -1,367 +0,0 @@ -// 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 - -#include "common.hpp" -#include - -class DebugDrawSampleApp final : public SimpleWindowedApplication, public BuiltinResourcesApplication -{ - using device_base_t = SimpleWindowedApplication; - using asset_base_t = BuiltinResourcesApplication; - - _NBL_STATIC_INLINE_CONSTEXPR uint32_t WIN_W = 1280, WIN_H = 720; - -public: - inline DebugDrawSampleApp(const path& _localInputCWD, const path& _localOutputCWD, const path& _sharedInputCWD, const path& _sharedOutputCWD) - : IApplicationFramework(_localInputCWD, _localOutputCWD, _sharedInputCWD, _sharedOutputCWD) {} - - inline core::vector getSurfaces() const override - { - if (!m_surface) - { - { - auto windowCallback = core::make_smart_refctd_ptr(smart_refctd_ptr(m_inputSystem), smart_refctd_ptr(m_logger)); - IWindow::SCreationParams params = {}; - params.callback = core::make_smart_refctd_ptr(); - params.width = WIN_W; - params.height = WIN_H; - params.x = 32; - params.y = 32; - params.flags = ui::IWindow::ECF_HIDDEN | IWindow::ECF_BORDERLESS | IWindow::ECF_RESIZABLE; - params.windowCaption = "DebugDrawSampleApp"; - params.callback = windowCallback; - 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) = nbl::video::CSimpleResizeSurface::create(std::move(surface)); - } - - if (m_surface) - return { {m_surface->getSurface()/*,EQF_NONE*/} }; - - return {}; - } - - inline bool onAppInitialized(smart_refctd_ptr&& system) override - { - m_inputSystem = make_smart_refctd_ptr(logger_opt_smart_ptr(smart_refctd_ptr(m_logger))); - - if (!device_base_t::onAppInitialized(smart_refctd_ptr(system))) - return false; - if (!asset_base_t::onAppInitialized(smart_refctd_ptr(system))) - return false; - - { - 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); - } - - m_semaphore = m_device->createSemaphore(m_realFrameIx); - if (!m_semaphore) - return logFail("Failed to Create a Semaphore!"); - - ISwapchain::SCreationParams swapchainParams = { .surface = m_surface->getSurface() }; - if (!swapchainParams.deduceFormat(m_physicalDevice)) - return logFail("Could not choose a Surface Format for the Swapchain!"); - - const static IGPURenderpass::SCreationParams::SSubpassDependency dependencies[] = - { - { - .srcSubpass = IGPURenderpass::SCreationParams::SSubpassDependency::External, - .dstSubpass = 0, - .memoryBarrier = - { - .srcStageMask = asset::PIPELINE_STAGE_FLAGS::COPY_BIT, - .srcAccessMask = asset::ACCESS_FLAGS::TRANSFER_WRITE_BIT, - .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 - } - }, - IGPURenderpass::SCreationParams::DependenciesEnd - }; - - auto scResources = std::make_unique(m_device.get(), swapchainParams.surfaceFormat.format, dependencies); - auto* renderpass = scResources->getRenderpass(); - - if (!renderpass) - return logFail("Failed to create Renderpass!"); - - auto gQueue = getGraphicsQueue(); - if (!m_surface || !m_surface->init(gQueue, std::move(scResources), swapchainParams.sharedParams)) - return logFail("Could not create Window & Surface or initialize the Surface!"); - - m_cmdPool = m_device->createCommandPool(gQueue->getFamilyIndex(), IGPUCommandPool::CREATE_FLAGS::RESET_COMMAND_BUFFER_BIT); - - for (auto i = 0u; i < MaxFramesInFlight; i++) - { - if (!m_cmdPool) - return logFail("Couldn't create Command Pool!"); - if (!m_cmdPool->createCommandBuffers(IGPUCommandPool::BUFFER_LEVEL::PRIMARY, { m_cmdBufs.data() + i, 1 })) - return logFail("Couldn't create Command Buffer!"); - } - - m_winMgr->setWindowSize(m_window.get(), WIN_W, WIN_H); - m_surface->recreateSwapchain(); - - SPushConstantRange simplePcRange = { - .stageFlags = IShader::E_SHADER_STAGE::ESS_VERTEX, - .offset = offsetof(ext::debug_draw::PushConstants, spc), - .size = sizeof(ext::debug_draw::SSinglePC) - }; - { - ext::debug_draw::DrawAABB::SCreationParameters params = {}; - params.transfer = getTransferUpQueue(); - params.assetManager = m_assetMgr; - params.drawMode = ext::debug_draw::DrawAABB::ADM_DRAW_BOTH; - params.singlePipelineLayout = ext::debug_draw::DrawAABB::createPipelineLayoutFromPCRange(m_device.get(), simplePcRange); - params.batchPipelineLayout = ext::debug_draw::DrawAABB::createDefaultPipelineLayout(m_device.get()); - params.renderpass = smart_refctd_ptr(renderpass); - params.utilities = m_utils; - drawAABB = ext::debug_draw::DrawAABB::create(std::move(params)); - } - - m_window->setCaption("[Nabla Engine] Debug Draw App Test Demo"); - m_winMgr->show(m_window.get()); - oracle.reportBeginFrameRecord(); - - return true; - } - - inline void workLoopBody() override - { - // framesInFlight: ensuring safe execution of command buffers and acquires, `framesInFlight` only affect semaphore waits, don't use this to index your resources because it can change with swapchain recreation. - const uint32_t framesInFlight = core::min(MaxFramesInFlight, m_surface->getMaxAcquiresInFlight()); - // We block for semaphores for 2 reasons here: - // A) Resource: Can't use resource like a command buffer BEFORE previous use is finished! [MaxFramesInFlight] - // B) Acquire: Can't have more acquires in flight than a certain threshold returned by swapchain or your surface helper class. [MaxAcquiresInFlight] - if (m_realFrameIx >= framesInFlight) - { - const ISemaphore::SWaitInfo cbDonePending[] = - { - { - .semaphore = m_semaphore.get(), - .value = m_realFrameIx + 1 - framesInFlight - } - }; - if (m_device->blockForSemaphores(cbDonePending) != ISemaphore::WAIT_RESULT::SUCCESS) - return; - } - - const auto resourceIx = m_realFrameIx % MaxFramesInFlight; - - m_inputSystem->getDefaultMouse(&mouse); - m_inputSystem->getDefaultKeyboard(&keyboard); - - auto updatePresentationTimestamp = [&]() - { - m_currentImageAcquire = m_surface->acquireNextImage(); - - oracle.reportEndFrameRecord(); - const auto timestamp = oracle.getNextPresentationTimeStamp(); - oracle.reportBeginFrameRecord(); - - return timestamp; - }; - - const auto nextPresentationTimestamp = updatePresentationTimestamp(); - - if (!m_currentImageAcquire) - return; - - // render whole scene to offline frame buffer & submit - - auto* const cmdbuf = m_cmdBufs.data()[resourceIx].get(); - cmdbuf->reset(IGPUCommandBuffer::RESET_FLAGS::RELEASE_RESOURCES_BIT); - cmdbuf->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT); - 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); - } - - auto* queue = getGraphicsQueue(); - - asset::SViewport viewport; - { - viewport.minDepth = 1.f; - viewport.maxDepth = 0.f; - viewport.x = 0u; - viewport.y = 0u; - viewport.width = WIN_W; - viewport.height = WIN_H; - } - cmdbuf->setViewport(0u, 1u, &viewport); - - VkRect2D scissor{ - .offset = { 0, 0 }, - .extent = { m_window->getWidth(), m_window->getHeight() } - }; - cmdbuf->setScissor(0u, 1u, &scissor); - - const VkRect2D currentRenderArea = - { - .offset = {0,0}, - .extent = {m_window->getWidth(),m_window->getHeight()} - }; - - { - auto scRes = static_cast(m_surface->getSwapchainResources()); - const IGPUCommandBuffer::SClearColorValue clearValue = { .float32 = {0.f,0.f,0.f,1.f} }; - const IGPUCommandBuffer::SRenderpassBeginInfo beginInfo = - { - .framebuffer = scRes->getFramebuffer(m_currentImageAcquire.imageIndex), - .colorClearValues = &clearValue, - .depthStencilClearValues = nullptr, - .renderArea = currentRenderArea - }; - - cmdbuf->beginRenderPass(beginInfo, IGPUCommandBuffer::SUBPASS_CONTENTS::INLINE); - - ext::debug_draw::DrawAABB::DrawParameters drawParams; - drawParams.commandBuffer = cmdbuf; - drawParams.cameraMat = camera.getConcatenatedMatrix(); - - 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); - { - using aabb_t = hlsl::shapes::AABB<3, float>; - using point_t = aabb_t::point_t; - - std::mt19937 gen(42); - std::uniform_real_distribution translate_dis(-50.f, 50.f); - std::uniform_real_distribution scale_dis(1.f, 10.f); - std::uniform_real_distribution color_dis(0.f, 1.f); - const uint32_t aabbCount = 200u; - - std::array aabbInstances; - for (auto i = 0u; i < aabbCount; i++) - { - point_t pmin = { translate_dis(gen), translate_dis(gen), translate_dis(gen) }; - point_t pmax = pmin + point_t{ scale_dis(gen), scale_dis(gen), scale_dis(gen) }; - aabb_t aabb = { pmin, pmax }; - - auto& instance = aabbInstances[i]; - instance.color = { color_dis(gen),color_dis(gen),color_dis(gen),1 }; - - hlsl::float32_t3x4 instanceTransform = ext::debug_draw::DrawAABB::getTransformFromAABB(aabb); - instance.transform = math::linalg::promoted_mul(float32_t4x4(1), instanceTransform); - } - - const ISemaphore::SWaitInfo drawFinished = { .semaphore = m_semaphore.get(),.value = m_realFrameIx + 1u }; - if (!drawAABB->render(drawParams, drawFinished, aabbInstances)) - m_logger->log("Unable to draw AABBs with instanced draw pipeline!", ILogger::ELL_ERROR); - } - - cmdbuf->endRenderPass(); - } - cmdbuf->endDebugMarker(); - cmdbuf->end(); - - { - const IQueue::SSubmitInfo::SSemaphoreInfo rendered[] = - { - { - .semaphore = m_semaphore.get(), - .value = ++m_realFrameIx, - .stageMask = PIPELINE_STAGE_FLAGS::COLOR_ATTACHMENT_OUTPUT_BIT - } - }; - - { - { - const IQueue::SSubmitInfo::SCommandBufferInfo commandBuffers[] = - { - {.cmdbuf = cmdbuf } - }; - - const IQueue::SSubmitInfo::SSemaphoreInfo acquired[] = - { - { - .semaphore = m_currentImageAcquire.semaphore, - .value = m_currentImageAcquire.acquireCount, - .stageMask = PIPELINE_STAGE_FLAGS::NONE - } - }; - - const IQueue::SSubmitInfo infos[] = - { - { - .waitSemaphores = acquired, - .commandBuffers = commandBuffers, - .signalSemaphores = rendered - } - }; - - if (queue->submit(infos) == IQueue::RESULT::SUCCESS) - { - const nbl::video::ISemaphore::SWaitInfo waitInfos[] = - { { - .semaphore = m_semaphore.get(), - .value = m_realFrameIx - } }; - - m_device->blockForSemaphores(waitInfos); // this is not solution, quick wa to not throw validation errors - } - else - --m_realFrameIx; - } - } - - m_surface->present(m_currentImageAcquire.imageIndex, rendered); - } - } - - inline bool keepRunning() override - { - if (m_surface->irrecoverable()) - return false; - - return true; - } - - inline bool onAppTerminated() override - { - return device_base_t::onAppTerminated(); - } - -private: - // 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; - - smart_refctd_ptr m_window; - smart_refctd_ptr> m_surface; - smart_refctd_ptr m_semaphore; - smart_refctd_ptr m_cmdPool; - uint64_t m_realFrameIx = 0; - std::array, MaxFramesInFlight> m_cmdBufs; - ISimpleManagedSurface::SAcquireResult m_currentImageAcquire = {}; - - core::smart_refctd_ptr m_inputSystem; - InputSystem::ChannelReader mouse; - InputSystem::ChannelReader keyboard; - - Camera camera; - 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 diff --git a/36_CUDAInterop/main.cpp b/36_CUDAInterop/main.cpp index c577ff852..07dad3512 100644 --- a/36_CUDAInterop/main.cpp +++ b/36_CUDAInterop/main.cpp @@ -141,8 +141,8 @@ int main() video::IVideoDriver* driver = device->getVideoDriver(); - core::smart_refctd_ptr cpubuffers[2] = { asset::ICPUBuffer::create({ _size }), - asset::ICPUBuffer::create({ _size })}; + core::smart_refctd_ptr cpubuffers[2] = { core::make_smart_refctd_ptr(_size), + core::make_smart_refctd_ptr(_size)}; for (auto j=0; j<2; j++) for (auto i=0; i(cpubuffers[j]->getPointer())[i] = rand(); diff --git a/39_DenoiserTonemapper/CMakeLists.txt b/39_DenoiserTonemapper/CMakeLists.txt index 06cfd87d0..f5c6e8017 100644 --- a/39_DenoiserTonemapper/CMakeLists.txt +++ b/39_DenoiserTonemapper/CMakeLists.txt @@ -27,4 +27,5 @@ nbl_create_executable_project( "" "${DENOISER_TONEMAPPER_EXAMPLE_INCLUDE_DIRS}" "${DENOISER_TONEMAPPER_EXAMPLE_LIBS}" + "${NBL_EXECUTABLE_PROJECT_CREATION_PCH_TARGET}" ) \ No newline at end of file diff --git a/39_DenoiserTonemapper/CommonPushConstants.h b/39_DenoiserTonemapper/CommonPushConstants.h index cedc1b4f6..349fb1668 100644 --- a/39_DenoiserTonemapper/CommonPushConstants.h +++ b/39_DenoiserTonemapper/CommonPushConstants.h @@ -13,8 +13,7 @@ struct CommonPushConstants uint inImageTexelPitch[3]; uint imageWidth; uint imageHeight; - float denoiseBlendFactor; - + uint padding; vec2 kernel_half_pixel_size; // luma meter and tonemapping var but also for denoiser diff --git a/39_DenoiserTonemapper/ShaderCommon.glsl b/39_DenoiserTonemapper/ShaderCommon.glsl index dd4bae7ba..da7e08f1e 100644 --- a/39_DenoiserTonemapper/ShaderCommon.glsl +++ b/39_DenoiserTonemapper/ShaderCommon.glsl @@ -37,7 +37,13 @@ uint nbl_glsl_ext_FFT_Parameters_t_getPaddingType() #define _NBL_GLSL_EXT_FFT_MAX_DIM_SIZE_ 16384 -//#define SHARED_CHANNELS 3 +#define SHARED_CHANNELS 3 +struct f16vec3_packed +{ + float16_t x; + float16_t y; + float16_t z; +}; // luma metering stuff @@ -85,10 +91,10 @@ uint nbl_glsl_ext_FFT_Parameters_t_getPaddingType() return int((~pc.data.flags)&0x1u); } - vec4 globalPixelData; + vec3 globalPixelData; vec3 nbl_glsl_ext_LumaMeter_getColor(bool wgExecutionMask) { - return globalPixelData.rgb; + return globalPixelData; } #else #include "nbl/builtin/glsl/ext/LumaMeter/common.glsl" diff --git a/39_DenoiserTonemapper/main.cpp b/39_DenoiserTonemapper/main.cpp index bcb3fe319..5155f9989 100644 --- a/39_DenoiserTonemapper/main.cpp +++ b/39_DenoiserTonemapper/main.cpp @@ -80,7 +80,7 @@ int main(int argc, char* argv[]) params.Doublebuffer = true; params.Stencilbuffer = false; // TODO: this is a temporary fix for a problem solved in the Vulkan Branch - params.StreamingUploadBufferSize = (1024+512)*1024*1024; // for Color + 2 AoV of 8k images + params.StreamingUploadBufferSize = 1024*1024*1024; // for Color + 2 AoV of 8k images params.StreamingDownloadBufferSize = core::roundUp(params.StreamingUploadBufferSize/3u,256u); // for output image auto device = createDeviceEx(params); @@ -130,10 +130,30 @@ int main(int argc, char* argv[]) if (check_error(!m_optixContext, "Could not create Optix Context!")) return error_code; - // TODO: make more denoisers with formats - constexpr OptixPixelFormat forcedOptiXFormats[] = {OPTIX_PIXEL_FORMAT_HALF4,OPTIX_PIXEL_FORMAT_HALF3,OPTIX_PIXEL_FORMAT_HALF3}; - const uint32_t forcedOptiXFormatPixelStrides[] = {8,6,6}; - const uint32_t forcedOptiXFormatPixelCumExclSizes[] = {0,8,14,20}; + constexpr auto forcedOptiXFormat = OPTIX_PIXEL_FORMAT_HALF3; // TODO: make more denoisers with formats + E_FORMAT nblFmtRequired = EF_UNKNOWN; + switch (forcedOptiXFormat) + { + case OPTIX_PIXEL_FORMAT_UCHAR3: + nblFmtRequired = EF_R8G8B8_SRGB; + break; + case OPTIX_PIXEL_FORMAT_UCHAR4: + nblFmtRequired = EF_R8G8B8A8_SRGB; + break; + case OPTIX_PIXEL_FORMAT_HALF3: + nblFmtRequired = EF_R16G16B16_SFLOAT; + break; + case OPTIX_PIXEL_FORMAT_HALF4: + nblFmtRequired = EF_R16G16B16A16_SFLOAT; + break; + case OPTIX_PIXEL_FORMAT_FLOAT3: + nblFmtRequired = EF_R32G32B32_SFLOAT; + break; + case OPTIX_PIXEL_FORMAT_FLOAT4: + nblFmtRequired = EF_R32G32B32A32_SFLOAT; + break; + } + constexpr auto forcedOptiXFormatPixelStride = 6u; DenoiserToUse denoisers[EII_COUNT]; { OptixDenoiserOptions opts = { OPTIX_DENOISER_INPUT_RGB }; @@ -155,7 +175,6 @@ int main(int argc, char* argv[]) using ToneMapperClass = ext::ToneMapper::CToneMapper; constexpr uint32_t kComputeWGSize = FFTClass::DEFAULT_WORK_GROUP_SIZE; // if it changes, maybe it breaks stuff - constexpr uint32_t allChannelsFFT = 4u; constexpr uint32_t colorChannelsFFT = 3u; constexpr bool usingHalfFloatFFTStorage = false; @@ -184,7 +203,7 @@ int main(int argc, char* argv[]) float bloomIntensity; }; { - auto firstKernelFFTShader = driver->createGPUShader(core::make_smart_refctd_ptr(R"===( + auto firstKernelFFTShader = driver->createShader(core::make_smart_refctd_ptr(R"===( #version 450 core #define _NBL_GLSL_WORKGROUP_SIZE_ 256 layout(local_size_x=_NBL_GLSL_WORKGROUP_SIZE_, local_size_y=1, local_size_z=1) in; @@ -213,7 +232,7 @@ nbl_glsl_complex nbl_glsl_ext_FFT_getPaddedData(in ivec3 coordinate, in uint cha #include "nbl/builtin/glsl/ext/FFT/default_compute_fft.comp" )===")); - auto lastKernelFFTShader = driver->createGPUShader(core::make_smart_refctd_ptr(R"===( + auto lastKernelFFTShader = driver->createShader(core::make_smart_refctd_ptr(R"===( #version 450 core #define _NBL_GLSL_WORKGROUP_SIZE_ 256 layout(local_size_x=_NBL_GLSL_WORKGROUP_SIZE_, local_size_y=1, local_size_z=1) in; @@ -236,7 +255,7 @@ layout(set=0, binding=2) writeonly restrict buffer OutputBuffer #include "nbl/builtin/glsl/ext/FFT/default_compute_fft.comp" )===")); - auto kernelNormalizationShader = driver->createGPUShader(core::make_smart_refctd_ptr(R"===( + auto kernelNormalizationShader = driver->createShader(core::make_smart_refctd_ptr(R"===( #version 450 core layout(local_size_x=16, local_size_y=16, local_size_z=1) in; @@ -274,9 +293,9 @@ void main() imageStore(NormalizedKernel[gl_WorkGroupID.z],ivec2(coord),vec4(value,0.0,0.0)); } )===")); - auto firstKernelFFTSpecializedShader = driver->createGPUSpecializedShader(firstKernelFFTShader.get(),IGPUSpecializedShader::SInfo(nullptr,nullptr,"main",ISpecializedShader::ESS_COMPUTE)); - auto lastKernelFFTSpecializedShader = driver->createGPUSpecializedShader(lastKernelFFTShader.get(),IGPUSpecializedShader::SInfo(nullptr,nullptr,"main",ISpecializedShader::ESS_COMPUTE)); - auto kernelNormalizationSpecializedShader = driver->createGPUSpecializedShader(kernelNormalizationShader.get(),IGPUSpecializedShader::SInfo(nullptr,nullptr,"main",ISpecializedShader::ESS_COMPUTE)); + auto firstKernelFFTSpecializedShader = driver->createSpecializedShader(firstKernelFFTShader.get(),IGPUSpecializedShader::SInfo(nullptr,nullptr,"main",ISpecializedShader::ESS_COMPUTE)); + auto lastKernelFFTSpecializedShader = driver->createSpecializedShader(lastKernelFFTShader.get(),IGPUSpecializedShader::SInfo(nullptr,nullptr,"main",ISpecializedShader::ESS_COMPUTE)); + auto kernelNormalizationSpecializedShader = driver->createSpecializedShader(kernelNormalizationShader.get(),IGPUSpecializedShader::SInfo(nullptr,nullptr,"main",ISpecializedShader::ESS_COMPUTE)); { IGPUSampler::SParams params = @@ -294,27 +313,27 @@ void main() ISampler::ECO_ALWAYS } }; - auto sampler = driver->createGPUSampler(std::move(params)); + auto sampler = driver->createSampler(std::move(params)); IGPUDescriptorSetLayout::SBinding binding[kernelSetDescCount] = { {0u,EDT_COMBINED_IMAGE_SAMPLER,1u,IGPUSpecializedShader::ESS_COMPUTE,&sampler}, {1u,EDT_STORAGE_BUFFER,1u,IGPUSpecializedShader::ESS_COMPUTE,nullptr}, {2u,EDT_STORAGE_BUFFER,1u,IGPUSpecializedShader::ESS_COMPUTE,nullptr}, {3u,EDT_STORAGE_IMAGE,colorChannelsFFT,IGPUSpecializedShader::ESS_COMPUTE,nullptr}, }; - kernelDescriptorSetLayout = driver->createGPUDescriptorSetLayout(binding,binding+kernelSetDescCount); + kernelDescriptorSetLayout = driver->createDescriptorSetLayout(binding,binding+kernelSetDescCount); } { SPushConstantRange pcRange[1] = {IGPUSpecializedShader::ESS_COMPUTE,0u,core::max(sizeof(FFTClass::Parameters_t),sizeof(NormalizationPushConstants))}; - kernelPipelineLayout = driver->createGPUPipelineLayout(pcRange,pcRange+1u,core::smart_refctd_ptr(kernelDescriptorSetLayout)); + kernelPipelineLayout = driver->createPipelineLayout(pcRange,pcRange+1u,core::smart_refctd_ptr(kernelDescriptorSetLayout)); } - firstKernelFFTPipeline = driver->createGPUComputePipeline(nullptr,core::smart_refctd_ptr(kernelPipelineLayout),std::move(firstKernelFFTSpecializedShader)); - lastKernelFFTPipeline = driver->createGPUComputePipeline(nullptr,core::smart_refctd_ptr(kernelPipelineLayout),std::move(lastKernelFFTSpecializedShader)); - kernelNormalizationPipeline = driver->createGPUComputePipeline(nullptr,core::smart_refctd_ptr(kernelPipelineLayout),std::move(kernelNormalizationSpecializedShader)); + firstKernelFFTPipeline = driver->createComputePipeline(nullptr,core::smart_refctd_ptr(kernelPipelineLayout),std::move(firstKernelFFTSpecializedShader)); + lastKernelFFTPipeline = driver->createComputePipeline(nullptr,core::smart_refctd_ptr(kernelPipelineLayout),std::move(lastKernelFFTSpecializedShader)); + kernelNormalizationPipeline = driver->createComputePipeline(nullptr,core::smart_refctd_ptr(kernelPipelineLayout),std::move(kernelNormalizationSpecializedShader)); - auto deinterleaveShader = driver->createGPUShader(core::make_smart_refctd_ptr(R"===( + auto deinterleaveShader = driver->createShader(core::make_smart_refctd_ptr(R"===( #version 450 core #extension GL_EXT_shader_16bit_storage : require #define _NBL_GLSL_EXT_LUMA_METER_FIRST_PASS_DEFINED_ @@ -325,16 +344,18 @@ layout(binding = 0, std430) restrict readonly buffer ImageInputBuffer } inBuffers[EII_COUNT]; layout(binding = 1, std430) restrict writeonly buffer ImageOutputBuffer { - float16_t data[]; + f16vec3_packed data[]; } outBuffers[EII_COUNT]; -vec4 fetchData(in uvec3 texCoord) +vec3 fetchData(in uvec3 texCoord) { - vec4 data = vec4(inBuffers[texCoord.z].data[texCoord.y*pc.data.inImageTexelPitch[texCoord.z]+texCoord.x]); - const bool invalid = any(isnan(data.rgb))||any(isinf(abs(data.rgb))); + vec3 data = vec4(inBuffers[texCoord.z].data[texCoord.y*pc.data.inImageTexelPitch[texCoord.z]+texCoord.x]).xyz; + bool invalid = any(isnan(data))||any(isinf(abs(data))); if (texCoord.z==EII_ALBEDO) - data.rgb = invalid ? vec3(1.0):data.rgb; + data = invalid ? vec3(1.0):data; else if (texCoord.z==EII_NORMAL) - data.xyz = invalid||length(data.xyz)<0.000000001 ? vec3(0.0,0.0,1.0):normalize(pc.data.normalMatrix*data.xyz); + { + data = invalid||length(data)<0.000000001 ? vec3(0.0,0.0,1.0):normalize(pc.data.normalMatrix*data); + } return data; } void main() @@ -346,15 +367,13 @@ void main() nbl_glsl_ext_LumaMeter(colorLayer && gl_GlobalInvocationID.xcreateGPUShader(core::make_smart_refctd_ptr(R"===( + auto intensityShader = driver->createShader(core::make_smart_refctd_ptr(R"===( #version 450 core #extension GL_EXT_shader_16bit_storage : require #include "../ShaderCommon.glsl" @@ -402,20 +421,20 @@ void main() intensity[pc.data.intensityBufferDWORDOffset] = optixIntensity; } )===")); - auto secondLumaMeterAndFirstFFTShader = driver->createGPUShader(core::make_smart_refctd_ptr(R"===( + auto secondLumaMeterAndFirstFFTShader = driver->createShader(core::make_smart_refctd_ptr(R"===( #version 450 core #extension GL_EXT_shader_16bit_storage : require #define _NBL_GLSL_EXT_LUMA_METER_FIRST_PASS_DEFINED_ #include "../ShaderCommon.glsl" -layout(binding = 0, std430) restrict readonly buffer DenoisedImageInputBuffer +layout(binding = 0, std430) restrict readonly buffer ImageInputBuffer { - uvec2 inDenoisedBuffer[]; + f16vec3_packed inBuffer[]; }; #define _NBL_GLSL_EXT_FFT_INPUT_DESCRIPTOR_DEFINED_ -layout(binding = 1, std430) restrict buffer NoisyImageInputBufferAndSpectrumOutputBuffer +layout(binding = 1, std430) restrict writeonly buffer SpectrumOutputBuffer { - uvec2 data[]; -} aliasedBuffer[2]; + vec2 outSpectrum[]; +}; #define _NBL_GLSL_EXT_FFT_OUTPUT_DESCRIPTOR_DEFINED_ @@ -447,7 +466,7 @@ uint nbl_glsl_ext_FFT_Parameters_t_getDirection() void nbl_glsl_ext_FFT_setData(in uvec3 coordinate, in uint channel, in nbl_glsl_complex complex_value) { const uint index = ((channel<createGPUShader(core::make_smart_refctd_ptr(R"===( + auto convolveShader = driver->createShader(core::make_smart_refctd_ptr(R"===( #version 450 core #extension GL_EXT_shader_16bit_storage : require @@ -523,8 +551,8 @@ shared uint _NBL_GLSL_SCRATCH_SHARED_DEFINED_[_NBL_GLSL_SCRATCH_SHARED_SIZE_DEFI #include "../ShaderCommon.glsl" layout(binding = 1, std430) restrict buffer SpectrumBuffer { - vec2 data[]; -} spectrumBuffer[2]; + vec2 spectrum[]; +}; #define _NBL_GLSL_EXT_FFT_INPUT_DESCRIPTOR_DEFINED_ #define _NBL_GLSL_EXT_FFT_OUTPUT_DESCRIPTOR_DEFINED_ @@ -559,7 +587,7 @@ nbl_glsl_complex nbl_glsl_ext_FFT_getPaddedData(ivec3 coordinate, in uint channe void nbl_glsl_ext_FFT_setData(in uvec3 coordinate, in uint channel, in nbl_glsl_complex complex_value) { const uint index = ((channel<createGPUShader(core::make_smart_refctd_ptr(R"===( + auto interleaveAndLastFFTShader = driver->createShader(core::make_smart_refctd_ptr(R"===( #version 450 core #extension GL_EXT_shader_16bit_storage : require @@ -656,8 +675,8 @@ layout(binding = 0, std430) restrict buffer ImageOutputBuffer #define _NBL_GLSL_EXT_FFT_OUTPUT_DESCRIPTOR_DEFINED_ layout(binding = 1, std430) restrict readonly buffer SpectrumInputBuffer { - vec2 data[]; -} inSpectrumBuffer[2]; + vec2 inSpectrum[]; +}; #define _NBL_GLSL_EXT_FFT_INPUT_DESCRIPTOR_DEFINED_ layout(binding = 3, std430) restrict readonly buffer IntensityBuffer { @@ -697,12 +716,12 @@ void nbl_glsl_ext_FFT_setData(in uvec3 coordinate, in uint channel, in nbl_glsl_ return; uint dataOffset = coords.y*pc.data.inImageTexelPitch[EII_COLOR]+coords.x; - vec4 color = vec4(outBuffer[dataOffset]); + vec3 color = vec4(outBuffer[dataOffset]).xyz; color[channel] = complex_value.x; - if (channel==3) + if (channel==nbl_glsl_ext_FFT_Parameters_t_getMaxChannel()) { - color.rgb = _NBL_GLSL_EXT_LUMA_METER_XYZ_CONVERSION_MATRIX_DEFINED_*color.rgb; - color.rgb *= intensity[pc.data.intensityBufferDWORDOffset]; // *= 0.18/AvgLuma + color = _NBL_GLSL_EXT_LUMA_METER_XYZ_CONVERSION_MATRIX_DEFINED_*color; + color *= intensity[pc.data.intensityBufferDWORDOffset]; // *= 0.18/AvgLuma switch (pc.data.tonemappingOperator) { case _NBL_GLSL_EXT_TONE_MAPPER_REINHARD_OPERATOR: @@ -710,7 +729,7 @@ void nbl_glsl_ext_FFT_setData(in uvec3 coordinate, in uint channel, in nbl_glsl_ nbl_glsl_ext_ToneMapper_ReinhardParams_t tonemapParams; tonemapParams.keyAndManualLinearExposure = pc.data.tonemapperParams[0]; tonemapParams.rcpWhite2 = pc.data.tonemapperParams[1]; - color.rgb = nbl_glsl_ext_ToneMapper_Reinhard(tonemapParams,color.rgb); + color = nbl_glsl_ext_ToneMapper_Reinhard(tonemapParams,color); break; } case _NBL_GLSL_EXT_TONE_MAPPER_ACES_OPERATOR: @@ -718,19 +737,18 @@ void nbl_glsl_ext_FFT_setData(in uvec3 coordinate, in uint channel, in nbl_glsl_ nbl_glsl_ext_ToneMapper_ACESParams_t tonemapParams; tonemapParams.gamma = pc.data.tonemapperParams[0]; tonemapParams.exposure = pc.data.tonemapperParams[1]; - color.rgb = nbl_glsl_ext_ToneMapper_ACES(tonemapParams,color.rgb); + color = nbl_glsl_ext_ToneMapper_ACES(tonemapParams,color); break; } default: { - color.rgb *= pc.data.tonemapperParams[0]; + color *= pc.data.tonemapperParams[0]; break; } } - color.rgb = nbl_glsl_XYZtosRGB*color.rgb; - color.a = clamp(color.a,0.f,1.f); + color = nbl_glsl_XYZtosRGB*color; } - outBuffer[dataOffset] = f16vec4(color); + outBuffer[dataOffset] = f16vec4(vec4(color,1.f)); } #define _NBL_GLSL_EXT_FFT_SET_DATA_DEFINED_ @@ -744,7 +762,7 @@ void main() // Virtual Threads Calculation const uint log2FFTSize = nbl_glsl_ext_FFT_Parameters_t_getLog2FFTSize(); const uint item_per_thread_count = 0x1u<<(log2FFTSize-_NBL_GLSL_WORKGROUP_SIZE_LOG2_); - for(uint channel=0u; channel<4u; channel++) + for(uint channel=0u; channel<3u; channel++) { // Load Values into local memory for(uint t=0u; tcreateGPUSpecializedShader(deinterleaveShader.get(),specInfo); - auto intensitySpecializedShader = driver->createGPUSpecializedShader(intensityShader.get(),specInfo); - auto secondLumaMeterAndFirstFFTSpecializedShader = driver->createGPUSpecializedShader(secondLumaMeterAndFirstFFTShader.get(),specInfo); - auto convolveSpecializedShader = driver->createGPUSpecializedShader(convolveShader.get(),specInfo); - auto interleaveAndLastFFTSpecializedShader = driver->createGPUSpecializedShader(interleaveAndLastFFTShader.get(),specInfo); + auto deinterleaveSpecializedShader = driver->createSpecializedShader(deinterleaveShader.get(),specInfo); + auto intensitySpecializedShader = driver->createSpecializedShader(intensityShader.get(),specInfo); + auto secondLumaMeterAndFirstFFTSpecializedShader = driver->createSpecializedShader(secondLumaMeterAndFirstFFTShader.get(),specInfo); + auto convolveSpecializedShader = driver->createSpecializedShader(convolveShader.get(),specInfo); + auto interleaveAndLastFFTSpecializedShader = driver->createSpecializedShader(interleaveAndLastFFTShader.get(),specInfo); { core::smart_refctd_ptr samplers[colorChannelsFFT]; @@ -818,7 +836,7 @@ nbl_glsl_complex nbl_glsl_ext_FFT_getPaddedData(ivec3 coordinate, in uint channe ISampler::ECO_ALWAYS } }; - auto sampler = driver->createGPUSampler(std::move(params)); + auto sampler = driver->createSampler(std::move(params)); std::fill_n(samplers,colorChannelsFFT,sampler); } IGPUDescriptorSetLayout::SBinding binding[SharedDescriptorSetDescCount] = { @@ -828,19 +846,19 @@ nbl_glsl_complex nbl_glsl_ext_FFT_getPaddedData(ivec3 coordinate, in uint channe {3u,EDT_STORAGE_BUFFER,1u,IGPUSpecializedShader::ESS_COMPUTE,nullptr}, {4u,EDT_COMBINED_IMAGE_SAMPLER,colorChannelsFFT,IGPUSpecializedShader::ESS_COMPUTE,samplers} }; - sharedDescriptorSetLayout = driver->createGPUDescriptorSetLayout(binding,binding+SharedDescriptorSetDescCount); + sharedDescriptorSetLayout = driver->createDescriptorSetLayout(binding,binding+SharedDescriptorSetDescCount); } { SPushConstantRange pcRange[1] = {IGPUSpecializedShader::ESS_COMPUTE,0u,sizeof(CommonPushConstants)}; - sharedPipelineLayout = driver->createGPUPipelineLayout(pcRange,pcRange+sizeof(pcRange)/sizeof(SPushConstantRange),core::smart_refctd_ptr(sharedDescriptorSetLayout)); + sharedPipelineLayout = driver->createPipelineLayout(pcRange,pcRange+sizeof(pcRange)/sizeof(SPushConstantRange),core::smart_refctd_ptr(sharedDescriptorSetLayout)); } - deinterleavePipeline = driver->createGPUComputePipeline(nullptr,core::smart_refctd_ptr(sharedPipelineLayout),std::move(deinterleaveSpecializedShader)); - intensityPipeline = driver->createGPUComputePipeline(nullptr,core::smart_refctd_ptr(sharedPipelineLayout),std::move(intensitySpecializedShader)); - secondLumaMeterAndFirstFFTPipeline = driver->createGPUComputePipeline(nullptr,core::smart_refctd_ptr(sharedPipelineLayout),std::move(secondLumaMeterAndFirstFFTSpecializedShader)); - convolvePipeline = driver->createGPUComputePipeline(nullptr,core::smart_refctd_ptr(sharedPipelineLayout),std::move(convolveSpecializedShader)); - interleaveAndLastFFTPipeline = driver->createGPUComputePipeline(nullptr,core::smart_refctd_ptr(sharedPipelineLayout),std::move(interleaveAndLastFFTSpecializedShader)); + deinterleavePipeline = driver->createComputePipeline(nullptr,core::smart_refctd_ptr(sharedPipelineLayout),std::move(deinterleaveSpecializedShader)); + intensityPipeline = driver->createComputePipeline(nullptr,core::smart_refctd_ptr(sharedPipelineLayout),std::move(intensitySpecializedShader)); + secondLumaMeterAndFirstFFTPipeline = driver->createComputePipeline(nullptr,core::smart_refctd_ptr(sharedPipelineLayout),std::move(secondLumaMeterAndFirstFFTSpecializedShader)); + convolvePipeline = driver->createComputePipeline(nullptr,core::smart_refctd_ptr(sharedPipelineLayout),std::move(convolveSpecializedShader)); + interleaveAndLastFFTPipeline = driver->createComputePipeline(nullptr,core::smart_refctd_ptr(sharedPipelineLayout),std::move(interleaveAndLastFFTSpecializedShader)); } const auto inputFilesAmount = cmdHandler.getInputFilesAmount(); @@ -881,7 +899,7 @@ nbl_glsl_complex nbl_glsl_ext_FFT_getPaddedData(ivec3 coordinate, in uint channe asset::IAssetLoader::SAssetLoadParams lp(0ull,nullptr); auto default_kernel_image_bundle = am->getAsset("../../media/kernels/physical_flare_512.exr",lp); // TODO: make it a builtins? - for (size_t i=0; igetRegions(); - // no mip chain, etc. assert(regions.begin()+1u==regions.end()); const auto& region = regions.begin()[0]; - // there is an explicit buffer row length assert(region.bufferRowLength); outParam.colorTexelSize = asset::getTexelOrBlockBytesize(colorCreationParams.format); } @@ -1020,8 +1036,6 @@ nbl_glsl_complex nbl_glsl_ext_FFT_getPaddedData(ivec3 coordinate, in uint channe { auto kerDim = outParam.kernel->getCreationParameters().extent; float kernelScale,minKernelScale; - // portrait vs landscape, get smallest dimension - // the kernelScale makes sure that resampled kernel resolution will match the image to be blurred scaled by `bloomRelativeScale` if (extent.width1.f) os::Printer::log(imageIDString + "Bloom Kernel loose sharpness, increase resolution of bloom kernel or reduce its relative scale!", ELL_WARNING); - // kernel cannot be smaller than 2x2 else if (kernelScale auto { auto tmp = extent; @@ -1054,16 +1066,14 @@ nbl_glsl_complex nbl_glsl_ext_FFT_getPaddedData(ivec3 coordinate, in uint channe } return tmp; }(); - // we abuse the same buffer as temporary storage for the Kernel FFT (two spans needed) fftScratchSize = core::max(FFTClass::getOutputBufferSize(usingHalfFloatFFTStorage,outParam.scaledKernelExtent,colorChannelsFFT)*2u,fftScratchSize); - // and for the main image FFT (alpha included) - fftScratchSize = core::max(FFTClass::getOutputBufferSize(usingHalfFloatFFTStorage,marginSrcDim,allChannelsFFT),fftScratchSize); + fftScratchSize = core::max(FFTClass::getOutputBufferSize(usingHalfFloatFFTStorage,marginSrcDim,colorChannelsFFT),fftScratchSize); // TODO: maybe move them to nested loop and compute JIT { auto* fftPushConstants = outParam.fftPushConstants; auto* fftDispatchInfo = outParam.fftDispatchInfo; const ISampler::E_TEXTURE_CLAMP fftPadding[2] = {ISampler::ETC_MIRROR,ISampler::ETC_MIRROR}; - const auto passes = FFTClass::buildParameters(false,allChannelsFFT,extent,fftPushConstants,fftDispatchInfo,fftPadding,marginSrcDim); + const auto passes = FFTClass::buildParameters(false,colorChannelsFFT,extent,fftPushConstants,fftDispatchInfo,fftPadding,marginSrcDim); { // override for less work and storage (dont need to store the extra padding of the last axis after iFFT) fftPushConstants[1].output_strides.x = fftPushConstants[0].input_strides.x; @@ -1079,7 +1089,6 @@ nbl_glsl_complex nbl_glsl_ext_FFT_getPaddedData(ivec3 coordinate, in uint channe } fftDispatchInfo[2] = fftDispatchInfo[0]; } - // only a 2D FFT assert(passes==2); } @@ -1102,7 +1111,6 @@ nbl_glsl_complex nbl_glsl_ext_FFT_getPaddedData(ivec3 coordinate, in uint channe { os::Printer::log(imageIDString + "Image extent of the Albedo Channel does not match the Color Channel, Albedo Channel will not be used!", ELL_ERROR); albedoImage = nullptr; - continue; } else outParam.denoiserType = EII_ALBEDO; @@ -1144,7 +1152,7 @@ nbl_glsl_complex nbl_glsl_ext_FFT_getPaddedData(ivec3 coordinate, in uint channe size_t denoiserStateBufferSize = 0ull; { size_t scratchBufferSize = fftScratchSize; - size_t tempBufferSize = forcedOptiXFormatPixelCumExclSizes[EII_COUNT]*maxResolution[0]*maxResolution[1]; + size_t tempBufferSize = fftScratchSize; for (uint32_t i=0u; igetCreationParameters(); + assert(asset::getTexelOrBlockBytesize(creationParameters.format)==param.colorTexelSize); // set up some image pitch and offset info shaderConstants.inImageTexelPitch[j] = image->getRegions().begin()[0].bufferRowLength; inImageByteOffset[j] = offsetPair->getOffset(); @@ -1338,7 +1344,7 @@ nbl_glsl_complex nbl_glsl_ext_FFT_getPaddedData(ivec3 coordinate, in uint channe kerImgViewInfo.subresourceRange.levelCount = kerImgViewInfo.image->getCreationParameters().mipLevels; kerImgViewInfo.subresourceRange.baseArrayLayer = 0; kerImgViewInfo.subresourceRange.layerCount = 1; - kerImageView = driver->createGPUImageView(std::move(kerImgViewInfo)); + kerImageView = driver->createImageView(std::move(kerImgViewInfo)); } // kernel outputs @@ -1363,7 +1369,7 @@ nbl_glsl_complex nbl_glsl_ext_FFT_getPaddedData(ivec3 coordinate, in uint channe viewParams.subresourceRange = {}; viewParams.subresourceRange.levelCount = 1u; viewParams.subresourceRange.layerCount = 1u; - kernelNormalizedSpectrums[i] = driver->createGPUImageView(std::move(viewParams)); + kernelNormalizedSpectrums[i] = driver->createImageView(std::move(viewParams)); } // @@ -1374,7 +1380,7 @@ nbl_glsl_complex nbl_glsl_ext_FFT_getPaddedData(ivec3 coordinate, in uint channe // the kernel's FFTs { - auto kernelDescriptorSet = driver->createGPUDescriptorSet(core::smart_refctd_ptr(kernelDescriptorSetLayout)); + auto kernelDescriptorSet = driver->createDescriptorSet(core::smart_refctd_ptr(kernelDescriptorSetLayout)); { IGPUDescriptorSet::SDescriptorInfo infos[kernelSetDescCount+colorChannelsFFT-1u]; infos[0].desc = kerImageView; @@ -1428,7 +1434,7 @@ nbl_glsl_complex nbl_glsl_ext_FFT_getPaddedData(ivec3 coordinate, in uint channe // bind shader resources { // create descriptor set - auto descriptorSet = driver->createGPUDescriptorSet(core::smart_refctd_ptr(sharedDescriptorSetLayout)); + auto descriptorSet = driver->createDescriptorSet(core::smart_refctd_ptr(sharedDescriptorSetLayout)); // write descriptor set { IGPUDescriptorSet::SDescriptorInfo infos[SharedDescriptorSetDescCount+EII_COUNT*2u-2u+colorChannelsFFT]; @@ -1456,14 +1462,13 @@ nbl_glsl_complex nbl_glsl_ext_FFT_getPaddedData(ivec3 coordinate, in uint channe attachBufferImageRange(writes[0].info+EII_ALBEDO,albedoPixelBuffer.getObject(),inImageByteOffset[EII_ALBEDO],interleavedPixelBytesize); if (denoiserInputCount>EII_NORMAL) attachBufferImageRange(writes[0].info+EII_NORMAL,normalPixelBuffer.getObject(),inImageByteOffset[EII_NORMAL],interleavedPixelBytesize); - // always need at least two input noisy buffers due to having to keep noisy colour around - for (uint32_t j=0u; jtileAndInvoke( @@ -1617,7 +1622,6 @@ nbl_glsl_complex nbl_glsl_ext_FFT_getPaddedData(ivec3 coordinate, in uint channe // image view core::smart_refctd_ptr imageView; - // size needed to download denoised, bloomed and tonemapped image const uint32_t colorBufferBytesize = param.height*param.width*param.colorTexelSize; { // create image @@ -1763,7 +1767,7 @@ nbl_glsl_complex nbl_glsl_ext_FFT_getPaddedData(ivec3 coordinate, in uint channe state.inMipLevel = region->imageSubresource.mipLevel; state.outMipLevel = region->imageSubresource.mipLevel; - if (!convertFilter.execute(std::execution::par_unseq,&state)) + if (!convertFilter.execute(core::execution::par_unseq,&state)) os::Printer::log("WARNING (" + std::to_string(__LINE__) + " line): Something went wrong while converting the image!", ELL_WARNING); _NBL_DELETE(state.ditherState); @@ -1783,7 +1787,7 @@ nbl_glsl_complex nbl_glsl_ext_FFT_getPaddedData(ivec3 coordinate, in uint channe // convert to EF_R8G8B8_SRGB and save it as .png and .jpg { - auto newImageView = getConvertedImageView(imageView->getCreationParameters().image, EF_R8G8B8A8_SRGB); + auto newImageView = getConvertedImageView(imageView->getCreationParameters().image, EF_R8G8B8_SRGB); IAssetWriter::SAssetWriteParams wp(newImageView.get()); std::string fileName = outputFileBundle[i].value().c_str(); diff --git a/40_PathTracer/CMakeLists.txt b/40_PathTracer/CMakeLists.txt deleted file mode 100644 index cfdee9493..000000000 --- a/40_PathTracer/CMakeLists.txt +++ /dev/null @@ -1,76 +0,0 @@ -include(common) - -set(NBL_INCLUDE_SERACH_DIRECTORIES - "${NBL_EXT_MITSUBA_LOADER_INCLUDE_DIRS}" - "${CMAKE_CURRENT_SOURCE_DIR}/include" - "${CMAKE_CURRENT_SOURCE_DIR}/src" - "${CMAKE_CURRENT_SOURCE_DIR}/app_resources" -) -set(NBL_LIBRARIES - "${NBL_EXT_MITSUBA_LOADER_LIB}" - Nabla::ext::FullScreenTriangle - imguizmo - "${NBL_EXT_IMGUI_UI_LIB}" -) -set(NBL_EXAMPLE_SOURCES - "${CMAKE_CURRENT_SOURCE_DIR}/src/io/CSceneLoader.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/src/renderer/CSession.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/src/renderer/CScene.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/src/renderer/CRenderer.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/src/renderer/resolve/CBasicRWMCResolver.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/src/renderer/present/CWindowPresenter.cpp" -) -nbl_create_executable_project("${NBL_EXAMPLE_SOURCES}" "" "${NBL_INCLUDE_SERACH_DIRECTORIES}" "${NBL_LIBRARIES}") - -set(OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/auto-gen") - -set(SM 6_8) -set(JSON [=[ -[ - { - "INPUT": "app_resources/pathtrace/previs.hlsl", - "KEY": "pathtrace_previs", - }, - { - "INPUT": "app_resources/pathtrace/beauty.hlsl", - "KEY": "pathtrace_beauty", - }, - { - "INPUT": "app_resources/pathtrace/debug.hlsl", - "KEY": "pathtrace_debug", - }, - { - "INPUT": "app_resources/present/default.hlsl", - "KEY": "present_default", - } -] -]=]) -string(CONFIGURE "${JSON}" JSON) - -set(COMPILE_OPTIONS - -I "${CMAKE_CURRENT_SOURCE_DIR}/../common/include" - -I "${CMAKE_CURRENT_SOURCE_DIR}/include" - -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} -) - - diff --git a/40_PathTracer/app_resources/pathtrace/beauty.hlsl b/40_PathTracer/app_resources/pathtrace/beauty.hlsl deleted file mode 100644 index 39d17af3b..000000000 --- a/40_PathTracer/app_resources/pathtrace/beauty.hlsl +++ /dev/null @@ -1,473 +0,0 @@ -#include "nbl/builtin/hlsl/rwmc/CascadeAccumulator.hlsl" - -#include "common.hlsl" - - -struct CCascades -{ - using layer_type = float16_t3; - using sample_count_type = uint16_t; - using weight_t = float16_t; - - inline uint16_t getLastCascade() {return gSensor.lastCascadeIndex;} - - inline void clear() - { - for (uint16_t i=0u; i<=getLastCascade(); ++i) - gRWMCCascades[__getCoord(i)] = uint32_t2(0,0); - } - - inline void addSampleIntoCascadeEntry(const layer_type _sample, const uint16_t lowerCascadeIndex, const weight_t lowerCascadeLevelWeight, const weight_t higherCascadeLevelWeight, const sample_count_type sampleCount) - { - const weight_t reciprocalSampleCount = weight_t(1) / weight_t(sampleCount); - uint16_t3 coord = __getCoord(lowerCascadeIndex); - __splatToLayer(coord,_sample*lowerCascadeLevelWeight,sampleCount,reciprocalSampleCount); - if (higherCascadeLevelWeight>weight_t(0)) - { - coord.z++; - __splatToLayer(coord,_sample*higherCascadeLevelWeight,sampleCount,reciprocalSampleCount); - } - } - - inline uint16_t3 __getCoord(const uint16_t cascadeIx) - { - uint16_t3 coord = _static_cast(spirv::LaunchIdKHR); - coord.z = coord.z*uint16_t(6)+cascadeIx; - return coord; - } - - inline void __splatToLayer(const uint16_t3 coord, const layer_type weightedSample, const sample_count_type sampleCount, const weight_t reciprocalSampleCount) - { - uint16_t4 data = uint16_t4(0,0,0,0); - if (sampleCount>1) - data = bit_cast(gRWMCCascades[coord]); - layer_type value = bit_cast(data.xyz); - const sample_count_type oldSampleCount = data.w; - value += (weightedSample - value*weight_t(sampleCount - oldSampleCount)) * reciprocalSampleCount; - data = uint16_t4(bit_cast(value),sampleCount); - gRWMCCascades[coord] = bit_cast(data); - } -}; - - -// There's actually a huge problem with doing any throughput or accumulation modification in AnyHit shaders, they run out of order (BVH order) and a hit behind your eventual closest hit can invoke the anyhit stage. -// -// Most examples which multiply alpha in anyhit are super misleading, because: -// - for shadow / anyhit rays you either eventually hit an opaque (leading to a mul/replacement of transparency by 0) or you hit all opaques along the ray -// - for NEE rays you often have a finite tMax and this stops you accumulating translucency behind the emitter -// - multiplicative operations are order independent, so accumulating the visibility function can happen out of order (basis of many OIT techniques) as long as you know tMax of the closest hit -// - stochastic transparency cancels out the alpha weighting on the throughput, so there's no multiplication to perform, the throughput stays constant no matter what you do. -// Which means it doesn't matter if you perform the test for occluded transparent geometries, you will never know, the alpha on the opaque also cancels out (shouldn't use premultiplied to shade). -// -// However the minute you want to do stochastic RGB translucency the pdf no longer cancels out the RGB weight coefficients. While the application of `opacity/luma(opacity)` from a hit accepted as the closest, -// can be delayed until the closest hit if found, you'll start accumulating the wrong visibility from all the ignored hits. You literally have to use stochastic monochrome transparency. -// -// Furthermore the minute you wish to add emission to the accumulation in the payload you run into Order Dependent Transparency because it requires a blend over operator. -// -// The solutions are then as follows: -// 1. Only use Anyhit to employ stochastic transparency when the translucency weight is monochrome -// 2. Re-trace rays, find closest hit as with (1), then launch anyhit rays with known tMax - this only gets you correct RGB translucency -// 3. Use OIT techniques (A-Buffer, MLAB, WBOIT) to estimate the visibility function but without re-tracing need a robust technique which can handle "opaque transparents" -// RGB translucency can be accumulated without sorting an A-Buffer in a O(1) pass over all intersections, also self-balancing tree and MLAB can throw out entries beyond current tMin. -// Note that within a TLAS instances are likely to be traversed approximately in-order, and within a BLAS the primitives are too (see CWBVH8 paper with children visit order depending on ray direction signs). -// Therefore a two tier linked list + insertion sort are a viable alternative to a self-balancing tree. To allow for emittance to be contributed by anyhit stage, it would need to be deferred to be performant, -// the hit attributes would need to be stored alongside the translucency, so at least instance ID (possibly material ID or SBT offset), primitive ID, and the barycentrics. -// 4. Decompose the Complex Mixture Material into a Scalar Delta Transmission plus the rest of the BxDF. The motivation is simple, for monochrome materials we have -// DeltaTransmission*(1-alpha) + alpha*(Rest of BxDF Nodes with their Weights) -// Where the thing getting factored is a blackbox sum of contributors, but we can reformulate any BxDF as -// DeltaTransmission*Factor + (Rest of BxDF Nodes with their Weights) -// Then we can simply break down the transmissive part into a monochrome part and a coloured residual, if we're unwilling to get into negative weights only option is `Transparency = min_element(Factor[0],...)` -// DeltaTransmission * Transparency + (DeltaTransmission * (Factor-Transparency) + Rest of BxDF Nodes with their Weights) -// We can still use stochastic transparency! Its just that whenever we accept a hit, we need pass `transparency` at the point of acceptance to the closest hit shader as to compute this -// (DeltaTransmission * (Factor-Transparency) + Rest of BxDF Nodes with their Weights)/(1-Transparency) -// Since Transparency can be just an approximation of the `Factor` in a monochrome form (luma) or its minimum, already computed or fetched data could be passed in payload for accepted hit -// -// MOST IMPORTANT THING: AFTER ANYHIT ACCEPTS, ANOTHER MAY ACCEPT THATS CLOSER! -// This is very important to keep in mind when we do our Solid Angle Sampling. -// -// Anyhit needs to pass the transparency probability to any closest hit it accepts and which then becomes the final anyhit -struct[raypayload] SAnyHitRetval -{ - // before sending the ray by the caller - inline void init(float32_t tMax = hlsl::numeric_limits::max) - { - rayT = tMax; - } - // call in AnyHit instead of AcceptHit - inline void acceptHit(const float16_t _transparency) - { - // need to read the spec if an anyhit is possible that the last anyhit to run and accept a hit candidate for a ray is not the last one to - if (rayT>spirv::RayTmaxKHR) - { - rayT = spirv::RayTmaxKHR; - transparency = _transparency; - } - // TODO: call accept Hit intrinsic - } - // - - // opacity russian roulette requires this for Discrete Probability Sampling - float32_t xi : read(anyhit) : write(caller,anyhit); - // need to store the t value at which the anyhit was executed, so we know whether the current closest hit comes from a confirmed anyhit - float32_t rayT : read(caller,anyhit) : write(caller,anyhit); - // essentially the probability of transmission - float16_t transparency : read(caller) : write(anyhit); - // can use additional `float16` to store BxDF mixture weights or other things so they don't need recomputing/re-fetching during shading -}; - - - -// Because SER based on Material ID will probably greatly benefit us, the shading needs to happen in Raygen Shader or ClosestHit executed directly by Raygen -// Lets examine what happens in the 3 options of Shading with SER Hit Objects: -// 1. Fused hitObjectTraceReorderExecuteEXT -> shading in Closest Hit -// Miss and Closest hit still called immediately, Shading happens in both of them, only need payload to store anyhit + random number state (depth and optionally the seed) -// but `SClosestHitRetval` gets passed to a shading function. Use NO_NULL_MISS_SHADERS definitely, and NO_NULL_CLOSEST_HIT_SHADERS if there's no blackhole materials. -// 2. hitObjectTraceRayEXT && Shading in Closest Hit with hitObjectExecuteShaderEXT -// Only Anyhit payload needed, separate `SClosestHitRetval` payload is made in raygen and passed to the hitObjectExecuteShaderEXT, miss shader is not used. -// Can use NO_NULL_CLOSEST_HIT and NO_NULL_MISS_SHADERS and then never invoke an invalid ClosestHit -// 3. hitObjectTraceRayEXT && Shading in Raygen -// Only Anyhit payload needed, separate `SClosestHitRetval` is made and passed to traceRay, no closest hit shaders at all. -// Should use NO_NULL_CLOSEST_HIT and NO_NULL_MISS_SHADERS -struct SClosestHitRetval -{ - float32_t3 hitPos; - // to interpolate our vertex attributes - float32_t2 barycentrics; - // to get our material and geometry data back - uint32_t instancedGeometryID; - // to get particular Triangle's indices - uint32_t primitiveID; - // - float32_t3 geometricNormal; -}; - - -// This payload will eventually not be needed with SER, and only the one below will be used -struct [raypayload] BeautyPayload -{ - inline void markAsMissed() - { - closestRet.geometricNormal = 45.f; - } - inline bool hasMissed() {return closestRet.geometricNormal[0]>1.f;} - - SClosestHitRetval closestRet : read(caller) : write(caller,closesthit); -}; - - -enum E_SBT_OFFSETS : uint16_t -{ - ESBTO_PATH, - ESBTO_NEE -}; - - -[[vk::push_constant]] SBeautyPushConstants pc; - -// TODO: do a function with MIS to do envmap lighting - - -[shader("raygeneration")] -void raygen() -{ - const uint16_t3 launchID = uint16_t3(spirv::LaunchIdKHR); - - const SBeautyPushConstants::S16BitData unpacked16BitPC = pc.get16BitData(); - - // Take n samples per frame - // TODO: establish min/max - adaptive sampling - SPixelSamplingInfo samplingInfo = advanceSampleCount(launchID,unpacked16BitPC.maxSppPerDispatch,uint16_t(pc.sensorDynamics.keepAccumulating),pc.sensorDynamics.maxSPP); - // took max samples - const uint16_t endSample = samplingInfo.newSampleCount; - const uint16_t samplesThisFrame = endSample-samplingInfo.firstSample; - if (samplesThisFrame==0) - return; - // weight for non RWMC contribution - const float16_t newSamplesOverTotal = _static_cast(_static_cast(samplesThisFrame)*samplingInfo.rcpNewSampleCount); - const float16_t rcpSamplesThisFrame = float16_t(1)/_static_cast(samplesThisFrame); - - float16_t transparency = 0.f; - SArbitraryOutputValues aovs; - aovs.clear(); - // some weird DXC and SPIR-V Tools Bug, lets try to move stuff out to temporaries and only use those - decltype(samplingInfo.randgen) randgen = samplingInfo.randgen; - const bool keepAccumulating = samplingInfo.firstSample; - [[loop]] for (uint16_t sampleIndex=samplingInfo.firstSample; sampleIndex!=endSample; ) - { - // For RWMC to work, every sample must be splatted individually - spectral_t color; - - using namespace nbl::hlsl::bxdf; - using namespace nbl::hlsl::material_compiler3::backends::default_upt; - using bxdf_config_t = BxDFConfig; - using ray_dir_info_t = bxdf_config_t::ray_dir_info_type; - using isotropic_interaction_t = bxdf_config_t::isotropic_interaction_type; - using light_sample_t = bxdf_config_t::sample_type; - using quotient_pdf_type = bxdf_config_t::quotient_pdf_type; - // trace primary ray - float32_t3 rayOrigin,rayDir; - // - bool missed; - SClosestHitRetval closestInfo; - { - // fetch random variable from memory - const float32_t3 randVec = randgen(0u,sampleIndex); - // TODO: motion blur and lens DOF triplet - - // get our NDC coordinates and ray - const float32_t2 pixelSizeNDC = promote(2.f)/float32_t2(spirv::LaunchSizeKHR.xy); - const float32_t2 NDC = float32_t2(launchID.xy)*pixelSizeNDC - promote(1.f); - const SRay ray = SRay::create(pc.sensorDynamics,pixelSizeNDC,NDC,float16_t2(randVec.xy)); - // TODO: possible SER point if doing variable spp - - // TODO: when doing anyhit opacity pass `randVec.z` into the payload - [[vk::ext_storage_class(spv::StorageClassRayPayloadKHR)]] BeautyPayload payload; - payload.markAsMissed(); - spirv::traceRayKHR(gTLASes[0],spv::RayFlagsMaskNone,0xff,ESBTO_PATH,0u,ESBTO_PATH,ray.origin,ray.tMin,ray.direction,ray.tMax,payload); - - // - missed = payload.hasMissed(); - if (missed) - { - const SEnvSample _sample = sampleEnv(ray.direction); - color = _sample.color; - aovs = aovs + _sample.aov * rcpSamplesThisFrame; - transparency += rcpSamplesThisFrame; - } - else // TODO: erase the `missed` variable and setup the struct in "wasAHit" - { - closestInfo = payload.closestRet; - rayOrigin = ray.origin; - rayDir = ray.direction; - } - } - // trace further rays - if (!missed) - { - // - MaxContributionEstimator contribEstimator = MaxContributionEstimator::create(unpacked16BitPC.rrThroughputWeights); - const uint16_t lastPathDepth = gSensor.lastPathDepth; - const uint16_t lastNoRussianRouletteDepth = gSensor.lastNoRussianRouletteDepth; - // - color = spectral_t(0,0,0); - spectral_t throughput = spectral_t(1,1,1); - float32_t otherTechniqueHeuristic = 0.f; - SAOVThroughputs aovThroughput; - aovThroughput.clear(rcpSamplesThisFrame); - [[loop]] for (uint16_t depth=1; true; depth++) // ideally peel this loop once - { - // TODO: get the material ID and UVs - - // TODO: only for twosided materials - closestInfo.geometricNormal *= -sign(hlsl::dot(rayDir,closestInfo.geometricNormal)); - - float32_t3 shadingNormal = closestInfo.geometricNormal; - - // TODO: possible SER point based on NEE status, and material flags - - // TODO: get AoVs from material and emission - SAOVThroughputs nextThroughput; - nextThroughput.clear(0.f); - SArbitraryOutputValues aovContrib; - aovContrib.albedo = float16_t3(1,1,1); - aovContrib.normal = float16_t3(shadingNormal); - // obtain full next - nextThroughput = aovThroughput * nextThroughput; - // already premultiplied by next throughput complement - aovs = aovs + aovContrib * (aovThroughput - nextThroughput); - aovThroughput = nextThroughput; - - // TODO: handle emission and do NEE MIS for any emission found on current hit - if (false) - { - // get emission stream - float16_t3 emission = float16_t3(0,0,0); - // compute emission - const float32_t WeightThreshold = hlsl::numeric_limits::min; - if (otherTechniqueHeuristic>WeightThreshold) - { - // compute NEE MIS backward weight on the contribution color - // assert not inf - // apply emissive weight - } - // add emissive to the contribution - color += emission*throughput; - } - - // to keep path depths equal for NEE and BxDF sampling, we can't continue and do NEE - if (depth==lastPathDepth) - break; - - // get next random number, compensate for the triplets ray generation used - const uint16_t sequenceProtoDim = (depth-uint16_t(1))*RandDimTriplesPerDepth+PrimaryRayRandTripletsUsed; - float32_t3 randVec = randgen(sequenceProtoDim,sampleIndex); - - // TODO: start at 0 or numeric_limits::min? - const float32_t tMin = 0.f; - - // perform NEE - const float32_t neeProb = 1.f; - if (false) // whether to perform NEE at all for this material - { - float32_t3 randNEE = randgen(sequenceProtoDim+uint16_t(1),sampleIndex); - // choose regular lights or envmap - - // TODO: SER point, top bits are NEE kind (none, regular light, envmap, then use bits of NEE random number and current position) - - // perform the NEE sampling - float32_t3 L;//light_sample_t L; - float32_t pdf; - float32_t tMax; - { - const float32_t cosTheta = hlsl::mix(1.0f, sunConeHalfAngleCos, randNEE.x); - const float32_t cosTheta2 = cosTheta * cosTheta; - const float32_t sinTheta = hlsl::sqrt(1.0 - cosTheta2); - - L = sunDir * cosTheta; - - float32_t phi = 2.0 * numbers::pi *randNEE.y; - float32_t3 X, Y; - math::frisvad(sunDir, X, Y); - - L += (X * hlsl::cos(phi) + Y * hlsl::sin(phi)) * sinTheta; - - pdf = 1.0 / (2.0 * numbers::pi * (1.0 - sunConeHalfAngleCos)); - tMax = hlsl::numeric_limits::max; - } - - // compute BxDF eval value, another layer of culling - - // trace shadow rays only for contributing samples - { - // TODO: another possible SER point before casting shadow rays - [[vk::ext_storage_class(spv::StorageClassRayPayloadKHR)]] SAnyHitRetval payload; - payload.init(tMax); - // TODO: change to ESBTO_NEE when ready - spirv::traceRayKHR(gTLASes[0],spv::RayFlagsTerminateOnFirstHitKHRMask|spv::RayFlagsSkipClosestHitShaderKHRMask,0xff,ESBTO_PATH,0u,ESBTO_PATH,rayOrigin,tMin,L,tMax,payload); - - if (false) - { - // apply everything - } - } - } - - // TODO: perform shading - { - - ray_dir_info_t tmpV; - tmpV.direction = -rayDir; - // TODO: embed a bit in the material stream whether: - // 1. anisotropic interaction is needed - // 2. whether luma contribution hint is needed - isotropic_interaction_t interaction = isotropic_interaction_t::create(tmpV,shadingNormal,throughput); - - // TODO: SER point, top bits are material Flags and ID geting executed - - using brdf_t = reflection::SOrenNayar; - brdf_t::SCreationParams cParams; - cParams.A = 0.f; - const brdf_t diffuse = brdf_t::create(cParams); - - // - const light_sample_t _sample = diffuse.generate(interaction,randVec.xy); - // Do I need to check `_sample.isValid()` myself before calling `forwardWeight`? - const quotient_pdf_type qAp = diffuse.quotient_and_pdf(_sample,interaction); - const float forwardWeight = qAp.pdf; - if (forwardWeight<0.00000001f) - break; - tmpV = _sample.getL(); - rayDir = tmpV.getDirection(); - - const float32_t3 albedo = float32_t3(0.8,0.7,0.5); - throughput = throughput * qAp.quotient * albedo; - - // TODO: include neeProb here - otherTechniqueHeuristic = 1.f/forwardWeight; - } - - // to keep path depths equal for NEE and BxDF sampling, we - if (contribEstimator.notCulled(throughput,depth<=lastNoRussianRouletteDepth,randVec.z)) - { - // advance ray origin - const float32_t3 originMagnitude = max(abs(closestInfo.hitPos),abs(rayOrigin)); - // TODO: should probably also take `tMax` of found hit into account - rayOrigin = closestInfo.hitPos + closestInfo.geometricNormal*max(max(exp2(8.f),originMagnitude.x),max(originMagnitude.y,originMagnitude.z))*exp2(-20.f); - - // continue the path - { - // TODO: when doing anyhit opacity pass `randVec.z` into the payload - [[vk::ext_storage_class(spv::StorageClassRayPayloadKHR)]] BeautyPayload payload; - payload.markAsMissed(); - spirv::traceRayKHR(gTLASes[0],spv::RayFlagsMaskNone,0xff,ESBTO_PATH,0u,ESBTO_PATH,rayOrigin,tMin,rayDir,hlsl::numeric_limits::max,payload); - if (payload.hasMissed()) - { - SEnvSample _sample = sampleEnv(rayDir); - if (otherTechniqueHeuristic>0.f) - { - // compute NEE MIS backward weight - // assert not inf - // apply MIS to adjust _sample.color - } - color += _sample.color*throughput; - aovs = aovs + _sample.aov*aovThroughput; - transparency += aovThroughput.transparency; - break; - } - } - } - } - } - // can't use pc.keepAccumulating because of variable sampling we want to do later, so just have first sample clear the RWMC - const bool doClear = (sampleIndex++)==0; - // color output, don't precompute `rwmc::CascadeAccumulator::create(gSensor.splatting)` and keep it as live state, it will spill anyway - rwmc::CascadeAccumulator colorAcc = rwmc::CascadeAccumulator::create(gSensor.splatting,doClear); - colorAcc.addSample(sampleIndex,accum_t(color)); - } - // albedo - Accumulator albedoAcc; - albedoAcc.accumulate(launchID.xy,launchID.z,aovs.albedo,newSamplesOverTotal,keepAccumulating); - // normal - Accumulator normalAcc; - normalAcc.accumulate(launchID.xy,launchID.z,correctSNorm10WhenStoringToUnorm(hlsl::normalize(aovs.normal)),newSamplesOverTotal,keepAccumulating); - // TODO: motion - // mask (TODO: do a separate pipeline for this with removed transparency calculations) - if (gSensor.hideEnvironment) - { - Accumulator maskAcc; - vector opacity = float16_t(1)-transparency; - maskAcc.accumulate(launchID.xy,launchID.z,opacity,newSamplesOverTotal,keepAccumulating); - } -} - - -[shader("closesthit")] -void closestHit(inout BeautyPayload payload, in BuiltInTriangleIntersectionAttributes attribs) -{ - SClosestHitRetval closestHitReturn; - // Which method of barycentric interpolation is more precise? Pick your poison! -#define POSITION_RECON_METHOD 0 -#if POSITION_RECON_METHOD!=0 - // compute worldspace hit position - const float32_t3 vertices[3] = spirv::HitTriangleVertexPositionsKHR; -#if POSITION_RECON_METHOD!=2 - // This way at least we stay within the triangle, and compiler can do CSE with the geometric normal calculation - const float32_t3 modelSpacePos = vertices[0] + (vertices[1]-vertices[0]) * attribs.barycentrics[0] + (vertices[2] - vertices[0]) * attribs.barycentrics[1]; -#else - // This way we get less catastrophic cancellation by adding and computing the edges, but can end up outside the triangle - const float32_t modelSpacePos = vertices[0] * (1.f-attribs.barycentrics.u-attribs.barycentrics.v) + vertices[1] * attribs.barycentrics.u + vertices[2] * attribs.barycentrics.v; -#endif - closestHitReturn.hitPos = math::linalg::promoted_mul(spirv::ObjectToWorldKHR,modelSpacePos); -#else - // the way that raytracers have done this before SPV_KHR_ray_tracing_position_fetch - closestHitReturn.hitPos = spirv::WorldRayOriginKHR + spirv::WorldRayDirectionKHR * spirv::RayTmaxKHR; -#endif -#undef POSITION_RECON_METHOD - closestHitReturn.barycentrics = attribs.barycentrics; - closestHitReturn.instancedGeometryID = spirv::InstanceCustomIndexKHR + spirv::RayGeometryIndexKHR; - closestHitReturn.primitiveID = spirv::PrimitiveId; - closestHitReturn.geometricNormal = reconstructGeometricNormal(); - payload.closestRet = closestHitReturn; -} - -// TODO: Anyhit transparency diff --git a/40_PathTracer/app_resources/pathtrace/common.hlsl b/40_PathTracer/app_resources/pathtrace/common.hlsl deleted file mode 100644 index e76942e9c..000000000 --- a/40_PathTracer/app_resources/pathtrace/common.hlsl +++ /dev/null @@ -1,505 +0,0 @@ -#include "renderer/shaders/pathtrace/common.hlsl" - -#include "nbl/builtin/hlsl/spirv_intrinsics/raytracing.hlsl" - -#include "nbl/examples/common/KeyedQuantizedSequence.hlsl" - -// TODO: move to material compiler -#include "nbl/builtin/hlsl/bxdf/common.hlsl" -#include "nbl/builtin/hlsl/bxdf/reflection/oren_nayar.hlsl" -namespace nbl -{ -namespace hlsl -{ -namespace material_compiler3 -{ -namespace backends -{ -namespace default_upt // unidirectional path tracing -{ -NBL_CONSTEXPR_STATIC_INLINE float32_t3 LumaConversionCoeffs = hlsl::transpose(hlsl::colorspace::scRGBtoXYZ)[1]; - -template) -struct SIsotropicInteraction -{ - using this_t = SIsotropicInteraction; - using spectral_type = SpectralType; - // TODO: experiment with float16 - using ray_dir_info_type = bxdf::ray_dir_info::SBasic; - using scalar_type = typename ray_dir_info_type::scalar_type; - using vector3_type = typename ray_dir_info_type::vector3_type; - - - // WARNING: Changed since GLSL, now arguments need to be normalized! - static this_t create(NBL_CONST_REF_ARG(ray_dir_info_type) normalizedV, const vector3_type normalizedN) - { - this_t retval; - retval.V = normalizedV; - retval.N = normalizedN; - retval.NdotV = hlsl::dot(retval.N,retval.V.getDirection()); - retval.NdotV2 = retval.NdotV * retval.NdotV; - retval.luminosityContributionHint = spectral_type(LumaConversionCoeffs); - - return retval; - } - static this_t create(NBL_CONST_REF_ARG(ray_dir_info_type) normalizedV, const vector3_type normalizedN, const spectral_type throughputSoFar) - { - this_t retval = create(normalizedV,normalizedN); - retval.luminosityContributionHint *= throughputSoFar; - retval.luminosityContributionHint /= math::lpNorm(retval.luminosityContributionHint); - return retval; - } - - ray_dir_info_type getV() NBL_CONST_MEMBER_FUNC { return V; } - vector3_type getN() NBL_CONST_MEMBER_FUNC { return N; } - scalar_type getNdotV(bxdf::BxDFClampMode _clamp = bxdf::BxDFClampMode::BCM_NONE) NBL_CONST_MEMBER_FUNC - { - return bxdf::conditionalAbsOrMax(NdotV,_clamp); - } - scalar_type getNdotV2() NBL_CONST_MEMBER_FUNC { return NdotV2; } - - bxdf::PathOrigin getPathOrigin() NBL_CONST_MEMBER_FUNC { return bxdf::PathOrigin::PO_SENSOR; } - spectral_type getLuminosityContributionHint() NBL_CONST_MEMBER_FUNC { return luminosityContributionHint; } - - ray_dir_info_type V; - vector3_type N; - scalar_type NdotV; - spectral_type luminosityContributionHint; - // TODO: experiment, precompute vs not - scalar_type NdotV2; -}; - - -template) -struct SAnisotropicInteraction -{ - using this_t = SAnisotropicInteraction; - using isotropic_interaction_type = IsotropicInteraction; - using ray_dir_info_type = typename isotropic_interaction_type::ray_dir_info_type; - using scalar_type = typename ray_dir_info_type::scalar_type; - using vector3_type = typename ray_dir_info_type::vector3_type; - using matrix3x3_type = matrix; - using spectral_type = typename isotropic_interaction_type::spectral_type; - - // WARNING: Changed since GLSL, now arguments need to be normalized! - static this_t create(NBL_CONST_REF_ARG(isotropic_interaction_type) isotropic, const vector3_type normalizedT, const vector3_type normalizedB) - { - this_t retval; - retval.isotropic = isotropic; - - retval.T = normalizedT; - retval.B = normalizedB; - - retval.TdotV = hlsl::dot(retval.isotropic.getV().getDirection(), retval.T); - retval.BdotV = hlsl::dot(retval.isotropic.getV().getDirection(), retval.B); - - return retval; - } - static this_t create(NBL_CONST_REF_ARG(isotropic_interaction_type) isotropic, const vector3_type normalizedT) - { - return create(isotropic, normalizedT, cross(isotropic.getN(), normalizedT)); - } - static this_t create(NBL_CONST_REF_ARG(isotropic_interaction_type) isotropic) - { - vector3_type T, B; - math::frisvad(isotropic.getN(), T, B); - return create(isotropic, nbl::hlsl::normalize(T), nbl::hlsl::normalize(B)); - } - - static this_t create(NBL_CONST_REF_ARG(ray_dir_info_type) normalizedV, const vector3_type normalizedN) - { - isotropic_interaction_type isotropic = isotropic_interaction_type::create(normalizedV, normalizedN); - return create(isotropic); - } - - ray_dir_info_type getV() NBL_CONST_MEMBER_FUNC { return isotropic.getV(); } - vector3_type getN() NBL_CONST_MEMBER_FUNC { return isotropic.getN(); } - scalar_type getNdotV(bxdf::BxDFClampMode _clamp = bxdf::BxDFClampMode::BCM_NONE) NBL_CONST_MEMBER_FUNC { return isotropic.getNdotV(_clamp); } - scalar_type getNdotV2() NBL_CONST_MEMBER_FUNC { return isotropic.getNdotV2(); } - bxdf::PathOrigin getPathOrigin() NBL_CONST_MEMBER_FUNC { return isotropic.getPathOrigin(); } - spectral_type getLuminosityContributionHint() NBL_CONST_MEMBER_FUNC { return isotropic.getLuminosityContributionHint(); } - bool isMaterialBSDF() NBL_CONST_MEMBER_FUNC { return isotropic.isMaterialBSDF(); } - isotropic_interaction_type getIsotropic() NBL_CONST_MEMBER_FUNC { return isotropic; } - - vector3_type getT() NBL_CONST_MEMBER_FUNC { return T; } - vector3_type getB() NBL_CONST_MEMBER_FUNC { return B; } - scalar_type getTdotV() NBL_CONST_MEMBER_FUNC { return TdotV; } - scalar_type getTdotV2() NBL_CONST_MEMBER_FUNC { const scalar_type t = getTdotV(); return t*t; } - scalar_type getBdotV() NBL_CONST_MEMBER_FUNC { return BdotV; } - scalar_type getBdotV2() NBL_CONST_MEMBER_FUNC { const scalar_type t = getBdotV(); return t*t; } - - vector3_type getTangentSpaceV() NBL_CONST_MEMBER_FUNC { return vector3_type(TdotV, BdotV, isotropic.getNdotV()); } - matrix3x3_type getToTangentSpace() NBL_CONST_MEMBER_FUNC { return matrix3x3_type(T, B, isotropic.getN()); } - matrix3x3_type getFromTangentSpace() NBL_CONST_MEMBER_FUNC { return nbl::hlsl::transpose(matrix3x3_type(T, B, isotropic.getN())); } - - isotropic_interaction_type isotropic; - vector3_type T; - vector3_type B; - scalar_type TdotV; - scalar_type BdotV; -}; - -struct BxDFConfig -{ - NBL_CONSTEXPR_STATIC_INLINE bool IsAnisotropic = false; - - // TODO: experiment with float16_t - using spectral_type = float32_t3; - using isotropic_interaction_type = SIsotropicInteraction; - - using scalar_type = typename isotropic_interaction_type::scalar_type; - using vector2_type = vector; - using vector3_type = typename isotropic_interaction_type::vector3_type; - using anisotropic_interaction_type = SAnisotropicInteraction; - // TODO: experiment with spectral_type's scalar type - using monochrome_type = vector; - - using ray_dir_info_type = typename isotropic_interaction_type::ray_dir_info_type; - using sample_type = bxdf::SLightSample; - - // TODO: change to conform to PR 1001 later - using quotient_pdf_type = sampling::quotient_and_pdf; -}; -} -} -} -} -} - - -namespace nbl -{ -namespace this_example -{ - -// There's different ways to accumulate with on-line averaging: -// - one Sample every Frame: `avg + (sample-avg)/N` -// - variable Samples every Frame without skipping: `avg + (sampleSum-avg*sampleCount)/N` or `avg + (sampleAvg-avg)*sampleCount/N` -// the second option has 1 MUL extra compared to regular accumulation, whereas first does 1 MUL extra but it requires cheap averaging -// - variable Samples every Frame with skipping: `avg + (sampleSum-avg*(sampleCount+skippedSamples))/N` equivalently `avg+(sampleSum-avg*(N-oldSamples))/N` -// pre-averaged variant is then `avg + (sampleAvg - avg*(N-oldSamples)/sampleCount)*sampleCount/N` - -template// NBL_PRIMARY_REQUIRES( -// hlsl::concepts::accessors::LoadableImage && -// hlsl::concepts::accessors::StorableImage -//) -struct Accumulator -{ - NBL_CONSTEXPR_STATIC_INLINE uint32_t Dimension = LoadStoreImageAccessor::Dimension; - using coded_type = typename LoadStoreImageAccessor::coded_type; - - // TODO: some check that `T` is same integral type and sign - template coord, const uint16_t layer, const vector data, const T rcpNewSampleCount, const bool keepAccumulating) - { - coded_type val; - - if (keepAccumulating) - { - composed.get(val,coord,layer); - NBL_UNROLL for (uint16_t i=0; i(hlsl::abs(delta) < LoadStoreImageAccessor::QuantizationThreshold))) - // return; - } - else // clear path - NBL_UNROLL for (uint16_t i=0; i -vector correctSNorm10WhenStoringToUnorm(const vector input) -{ - using vec_t = vector; - const T factor = _static_cast(0.499512); - return hlsl::mix(input*factor+hlsl::promote(_static_cast(0.999022)),input*factor,hlsl::promote(_static_cast(0)); - -// sample count incrementing function -struct SPixelSamplingInfo -{ - randgen_t randgen; - float32_t rcpNewSampleCount; - uint16_t newSampleCount; - uint16_t firstSample; -}; -SPixelSamplingInfo advanceSampleCount(const uint16_t3 coord, const uint16_t newSamplesThisPixel, const uint16_t dontClear, const uint16_t maxSamples) -{ - SPixelSamplingInfo retval; - // - const uint16_t oldSampleCount = _static_cast(gSampleCount[coord]); - retval.firstSample = oldSampleCount*dontClear; - // setup randgen - { - retval.randgen.pSampleBuffer = gScene.init.pSampleSequence; - // TODO: experiment with storing every dimension scramble in the texture to not pollute the ray payload - // TODO: experiment with truncating the sequence to a fixed number of dimensions, and using the scramble, sampleIndex and rank keys as xoroshiro64 seed and use simple RNG for last dimensions - retval.randgen.rng = scramble_state_t::construct(gScrambleKey[uint16_t3(coord.xy & uint16_t(511), 0)]); - retval.randgen.sequenceSamplesLog2 = gScene.init.sequenceSamplesLog2; // TODO: make this compile time constant - Spec Constant? - } - // - retval.newSampleCount = hlsl::min(retval.firstSample+newSamplesThisPixel,maxSamples); - if (retval.newSampleCount!=oldSampleCount) // whether this pays off depends on ratio of pixels with 0 spp in a dispatch - gSampleCount[coord] = retval.newSampleCount; - // handle overflow properly - retval.rcpNewSampleCount = 1.f/float32_t(retval.newSampleCount); - return retval; -} - -// TODO: split into RayDir -// raygen functions -struct SRay -{ - static SRay create(const SSensorDynamics sensor, const float32_t2 pixelSizeNDC, const float32_t2 ndc, const float16_t2 xi) - { - using namespace nbl::hlsl; - using namespace nbl::hlsl::math::linalg; - - // stochastic reconstruction filter - const float16_t stddev = _static_cast(1.2); - const float32_t3 adjNDC = float32_t3(path_tracing::GaussianFilter::create(stddev,stddev).sample(xi)*pixelSizeNDC+ndc,-1.f); - // unproject - const float32_t3 direction = hlsl::normalize(float32_t3(hlsl::mul(sensor.ndcToRay,adjNDC), -1.0)); - const float32_t3 origin = -float32_t3(direction.xy/direction.z,sensor.nearClip); - // rotate with camera - SRay retval; - retval.origin = promoted_mul(sensor.invView,origin); - retval.tMin = sensor.nearClip; - // normalization is extremely important for tMax to have correct units and also so rest of BxDF code can assume normalized V=-dir - retval.direction = hlsl::normalize(hlsl::mul(truncate<3,3,3,4>(sensor.invView),direction)); - retval.tMax = sensor.tMax; - return retval; - } - - float32_t3 origin; - float32_t tMin; - float32_t3 direction; - float32_t tMax; - // TODO: ray differentials or covariance -}; - -// variables that multiply together -struct SAOVThroughputs -{ - inline void clear(const float16_t weight) - { - albedo = hlsl::promote(transparency = weight); - } - - inline SAOVThroughputs operator-(const SAOVThroughputs factor) - { - SAOVThroughputs retval = this; - retval.albedo -= factor.albedo; - retval.transparency -= factor.transparency; - return retval; - } - - inline SAOVThroughputs operator*(const float16_t factor) - { - SAOVThroughputs retval = this; - retval.albedo *= factor; - retval.transparency *= factor; - return retval; - } - inline SAOVThroughputs operator*(const SAOVThroughputs factor) - { - SAOVThroughputs retval = this; - retval.albedo *= factor.albedo; - retval.transparency *= factor.transparency; - return retval; - } - - inline SAOVThroughputs operator/(const float16_t factor) - { - SAOVThroughputs retval = this; - retval.albedo /= factor; - retval.transparency /= factor; - return retval; - } - inline SAOVThroughputs operator/(const SAOVThroughputs factor) - { - SAOVThroughputs retval = this; - retval.albedo /= factor.albedo; - retval.transparency /= factor.transparency; - return retval; - } - - - // RGB transparency of smooth reflections and refractions, used for modulating albedo and most AOVs - float16_t3 albedo; - // Motion is special because Real Time defines it as a mapping of where current pixel was last frame. - // True motion output would require us to implement differentiable rendering and formulate motion as an integral of `Throughput dScreenPos/dTime` which is super tricky because: - // - A turning mirror imparts motion on the reflection of a static object - // - whats the motion vector for a disoccluded part of a reflection? How to even know about a disocclusion/our reflection's motion vector reprojecting badly? - // - its not generally a function, the current pixel could be in multiple places at in the last frame (think about the flow of the reflection of your face in a concave spoon) - // - Non-differentiability, hard edges of triangles and Breps - // - lighting imparts is own motion vectors, e.g. shadows move across static surfaces - // - how to weigh contributions? luma of RGB effect? inidividually, etc. - // TL;DR you can't just blend motions and get something useful (even less than normals), only directly tranmissive paths should be allowed to accumulate their motion vectors (easy to calculate) - // as we pass through surfaces we need to know how much of the outgoing ray distribution is focused around the directly transmissive direction. This can modulate both our masking and motion vectors. - // Albeit for smooth but refractive surfaces we could experiment with accepting transparent masking even though ray direction won't match in a simple Photoshop composting, - // but would you rather have an opaque swimming pool, round glass vase, or water droplet OR composted with no refraction? But then we'd need a motion throughput and track some more metadata. - float16_t transparency; -}; - -using spectral_t = float32_t3; - - -struct SArbitraryOutputValues -{ - inline void clear() - { - normal = albedo = float16_t3(0,0,0); - // TODO: motion - } - - inline SArbitraryOutputValues operator+(const SArbitraryOutputValues other) - { - SArbitraryOutputValues retval; - retval.albedo = albedo+other.albedo; - retval.normal = normal+other.normal; - return retval; - } - - inline SArbitraryOutputValues operator*(const float16_t factor) - { - SArbitraryOutputValues retval; - retval.albedo = albedo*factor; - retval.normal = normal*factor; - return retval; - } - - inline SArbitraryOutputValues operator*(const SAOVThroughputs throughput) - { - SArbitraryOutputValues retval; - retval.albedo = albedo*throughput.albedo; - retval.normal = normal*hlsl::dot(throughput.albedo,_static_cast(hlsl::material_compiler3::backends::default_upt::LumaConversionCoeffs)); - return retval; - } - - // AoVs are handled as "special emission", basically the contribution of albedo is same as the material illuminated in a White Furnace - // so for transparent (anyhit) to impart its albedo or normal into the AoV it can add it same way it would add any color emission - float16_t3 albedo; - // One would think that normals can't be blended, but yes they can! Just make sure you weigh then using the Luma of the RGB aovThroughput. - // Here's the problem with dealing with reflections & refractions, the reflection of a wall with an X- normal in the X+ window should have an apparent X+ normal. - // This means that one would need to track the surfaces through which we reflect in a stack along the path, eg: - // `originalNormal - 2 dot(originalNormal, reflectorNormal) * reflectorNormal == (Identity - 2 outerProductMatrix(reflectorNormal)) originalNormal` - // To follow through 2 or more reflections we'd need to multiply these 3x3 matrices together along the ray like so - // `(I - 2 n_0 n_0^T) (I - 2 n_1 n_1^T) = I + 4 n_0 (n_0^T n_1) n_1^T - 2 (n_0 n_0^T + n_1 n_1^T)` - // Theoretically because every series of reflections is just one reflection and a rotation, it could be possible to store this in 3 floats, due to the properties of SO(3) - // "The orthogonal group, consisting of all proper and improper rotations, is generated by reflections. Every proper rotation is the composition of two reflections, a special case of the Cartan–Dieudonné theorem." - // I'm not sure how we could extend that for refractions but probably a similar form is possible - virtual object corresponding under transmission to what's seen under refraction. - // The question is.. is it worth it? Do er really need objects warped by in a labyrynth of wonky mirrors to have warped normals? Or a ceiling reflected in a choppy swimming pool to inherit the pool's wave normals ? - // NO because this is an input to a denoiser to stop it blurring lighting across surfaces oriented in different directions! Doesn't matter what the reflection and refraction normals are as long as they're consistent. - // For the example of a flat building wall reflected in a wavy but smooth reflector, that would actually be a massive self-own and leave behind a lot of noise! - float16_t3 normal; - // TODO: motion (RG vector to past location, B or BA as a measure of spread, e.g. spherical gaussian, direction and its variance, Polar Harmonics - Laplace on a Circle) - //float16_t3or4 motion; -}; - -// accumulated color -using accum_t = float16_t3; - -// only callable from closestHit -inline float32_t3 reconstructGeometricNormal() -{ - using namespace nbl::hlsl; - - // Do diffs in high precision, edges can be very long and dot products can easily overflow 64k max float16_t value and normalizing one extra time makes no sense - const float32_t3 geometricNormal = hlsl::cross( - spirv::HitTriangleVertexPositionsKHR[1]-spirv::HitTriangleVertexPositionsKHR[0], - spirv::HitTriangleVertexPositionsKHR[2]-spirv::HitTriangleVertexPositionsKHR[0] - ); - - // Scales can be absolutely huge, we'd need special per-instance pre-scaled 3x3 matrices and also guarantee `geometricNormal` isn't huge - // this would require a normalization before the matrix multiplication, making everything slower/ - const float32_t3x3 normalMatrix = hlsl::math::linalg::truncate<3,3,3,4>(hlsl::transpose(float32_t4x3(spirv::WorldToObjectKHR))); - // normalization also needs to be done in full floats because length squared can easily be over 64k - return hlsl::normalize(hlsl::mul(normalMatrix,geometricNormal)); -} - - -// This is not only used for Russian Roulette but also for culling low throughput paths early (adds bias but keeps the critical path of the path tracer - pun intended - manageable) -struct MaxContributionEstimator -{ - // TODO: apply inverse exposure so we're sensitive to screen output (previous beauty), but don't go overkill and apply toonemapped luma derivative based on current inverse tonemapping of color accumulation - static inline MaxContributionEstimator create(const float16_t3 constantThroughputWeights) - { - MaxContributionEstimator retval; - // essentially how much can we move the accumulation needle - retval.throughputWeights = constantThroughputWeights; - return retval; - } - - // notCulled instead of culled because of NaN handling - inline bool notCulled(NBL_REF_ARG(float32_t3) throughput, bool skipRussianRoulette, NBL_REF_ARG(float32_t) xi) - { - // recompute after previous hit - const float16_t surviveProb = hlsl::dot(float16_t3(throughput),throughputWeights); - // TODO: prevent "fireflies in AoVs" because AoV targets are not HDR - don't do RR if that will overshoot our albedo and normal contributions - // skipRussianRoulette = skipRussianRoulette && ...; - // cull really low throughput paths (adds bias) - const float16_t RelativeLumaThroughputThreshold = hlsl::numeric_limits::min; - // < instead of <= very important for handling zero probability, note that nextULP correction doesn't need to be applied because we use unclamped probability here - if (surviveProb>RelativeLumaThroughputThreshold && (skipRussianRoulette || xisunConeHalfAngleCos) -// retval.color = sunColor; - // TODO: apply some tonemapping operator with exposure (first envmap's avg luma, then our own) - retval.aov.albedo = hlsl::min(retval.color,float16_t3(1,1,1)); - retval.aov.normal = -hlsl::normalize(float16_t3(raydir)); - return retval; -} - -} -} - - -// TODO: should this be here? -using namespace nbl; -using namespace nbl::hlsl; -using namespace nbl::this_example; -using namespace nbl::hlsl::path_tracing; \ No newline at end of file diff --git a/40_PathTracer/app_resources/pathtrace/debug.hlsl b/40_PathTracer/app_resources/pathtrace/debug.hlsl deleted file mode 100644 index 9d25e86ed..000000000 --- a/40_PathTracer/app_resources/pathtrace/debug.hlsl +++ /dev/null @@ -1,70 +0,0 @@ -#include "common.hlsl" - - -[[vk::push_constant]] SDebugPushConstants pc; - - -struct [raypayload] DebugPayload -{ - uint32_t instanceID : read(caller) : write(closesthit); - uint32_t primitiveID : read(caller) : write(closesthit); - SArbitraryOutputValues aov : read(caller) : write(closesthit,miss); -}; - -[shader("raygeneration")] -void raygen() -{ - const uint16_t3 launchID = uint16_t3(spirv::LaunchIdKHR); - - SPixelSamplingInfo samplingInfo = advanceSampleCount(launchID,1,uint16_t(pc.sensorDynamics.keepAccumulating),256); - // took 64k-1 spp - if (samplingInfo.rcpNewSampleCount==0.f) - return; - - // - const float32_t2 pixelSizeNDC = promote(2.f) / float32_t2(spirv::LaunchSizeKHR.xy); - const float32_t2 NDC = float32_t2(launchID.xy) * pixelSizeNDC - promote(1.f); - - [[vk::ext_storage_class(spv::StorageClassRayPayloadKHR)]] - DebugPayload payload; - // take just one sample per dispatch - { - const float16_t2 randVec = float16_t2(samplingInfo.randgen(0u,samplingInfo.firstSample).xy); - const SRay ray = SRay::create(pc.sensorDynamics,pixelSizeNDC,NDC,randVec); - - payload.aov.clear(); - spirv::traceRayKHR(gTLASes[0], spv::RayFlagsMaskNone, 0xff, 0u, 0u, 0u, ray.origin, ray.tMin, ray.direction, ray.tMax, payload); - } - - // simple overwrite without accumulation - gRWMCCascades[launchID] = uint32_t2(payload.instanceID,payload.primitiveID); - // can also shove some stuff in `gBeauty`, `gMotion` and `gMask` - - const bool keepAccumulating = samplingInfo.firstSample; - // albedo - Accumulator albedoAcc; - albedoAcc.accumulate(launchID.xy,launchID.z,float32_t3(payload.aov.albedo),samplingInfo.rcpNewSampleCount,keepAccumulating); - // normal - Accumulator normalAcc; - normalAcc.accumulate(launchID.xy,launchID.z,float32_t3(correctSNorm10WhenStoringToUnorm(payload.aov.normal)),samplingInfo.rcpNewSampleCount,keepAccumulating); -} - -[shader("closesthit")] -void closestHit(inout DebugPayload payload, in BuiltInTriangleIntersectionAttributes attribs) -{ - const uint32_t instanceCustomIndex = spirv::InstanceCustomIndexKHR; - const uint32_t geometryIndex = spirv::RayGeometryIndexKHR; - payload.instanceID = instanceCustomIndex;// TODO: can we get geometry count in instance and "linearize" our geometry into an UUID ? - payload.primitiveID = spirv::PrimitiveId; - - payload.aov.albedo = accum_t(1,1,1); - payload.aov.normal = accum_t(reconstructGeometricNormal()); -} - -[shader("miss")] -void miss(inout DebugPayload payload) -{ - const SEnvSample _sample = sampleEnv(spirv::WorldRayDirectionKHR); - //_sample.color; - payload.aov = _sample.aov; -} \ No newline at end of file diff --git a/40_PathTracer/app_resources/pathtrace/previs.hlsl b/40_PathTracer/app_resources/pathtrace/previs.hlsl deleted file mode 100644 index 53197b7d7..000000000 --- a/40_PathTracer/app_resources/pathtrace/previs.hlsl +++ /dev/null @@ -1,30 +0,0 @@ -#include "renderer/shaders/pathtrace/common.hlsl" -using namespace nbl::hlsl; -using namespace nbl::this_example; - -[[vk::push_constant]] SPrevisPushConstants pc; - - -struct[raypayload] PrevisPayload -{ - uint16_t materialID : read(caller):write(closesthit); -}; - -[shader("raygeneration")] -void raygen() -{ - const uint32_t3 launchID = spirv::LaunchIdKHR; - const uint32_t3 launchSize = spirv::LaunchSizeKHR; - - gAlbedo[launchID] = float32_t4(float32_t3(launchID)/float32_t3(launchSize),1.f); -} - -[shader("closesthit")] -void closestHit(inout PrevisPayload payload, in BuiltInTriangleIntersectionAttributes attribs) -{ -} - -[shader("miss")] -void miss(inout PrevisPayload payload) -{ -} diff --git a/40_PathTracer/app_resources/present/default.hlsl b/40_PathTracer/app_resources/present/default.hlsl deleted file mode 100644 index dc857fb2d..000000000 --- a/40_PathTracer/app_resources/present/default.hlsl +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (C) 2024-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 "renderer/shaders/present/push_constants.hlsl" -// vertex shader is provided by the fullScreenTriangle extension -#include -NBL_CONSTEXPR_INLINE_NSPC_SCOPE_VAR uint32_t SessionDSIndex = 0; -#include "renderer/shaders/session.hlsl" - -using namespace nbl::hlsl; -using namespace nbl::this_example; -using namespace ext::FullScreenTriangle; - - -[[vk::push_constant]] SDefaultResolvePushConstants pc; - -[shader("pixel")] -float32_t4 present_default(SVertexAttributes vxAttr) : SV_Target0 -{ - float32_t3 tint = promote(1.f); - float32_t3 uv; - if (pc.isCubemap) - { - const float32_t4 ndc = float32_t4(vxAttr.uv*2.f-float32_t2(1,1),1.f,1.f); - float32_t4 tmp = mul(pc.cubemap().invProjView,ndc); - float32_t3 dir = tmp.xyz/tmp.www; - // TODO: convert dir to cubemap face, and the UV coord - tint = float32_t3(1,0,1); // right now go magenta error colour - } - else - { - const SDefaultResolvePushConstants::Regular regular = pc.regular(); - uv.xy = vxAttr.uv*regular.scale; - if (any(uv.xy>float32_t2(1,1))) - return promote(0.f); - uv.z = pc.layer; - if (any(regular._min>uv.xy) || any(regular._max data; - Material material; - core::matrix3x4SIMD transform; - -}; - -} - -#endif // __NBL_THIS_EXAMPLE_COMMON_H_INCLUDED__ diff --git a/40_PathTracer/include/io/CSceneLoader.h b/40_PathTracer/include/io/CSceneLoader.h deleted file mode 100644 index be69afda0..000000000 --- a/40_PathTracer/include/io/CSceneLoader.h +++ /dev/null @@ -1,278 +0,0 @@ -// Copyright (C) 2025-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 -#ifndef _NBL_THIS_EXAMPLE_C_SCENE_LOADER_H_INCLUDED_ -#define _NBL_THIS_EXAMPLE_C_SCENE_LOADER_H_INCLUDED_ - - -#include "nabla.h" -#include "nbl/builtin/hlsl/cpp_compat/promote.hlsl" - -#include "nbl/ext/MitsubaLoader/CMitsubaMetadata.h" - - -namespace nbl::this_example -{ - -class CSceneLoader : public core::IReferenceCounted, public core::InterfaceUnmovable -{ - public: - struct SCachedCreationParams - { - core::smart_refctd_ptr assMan = nullptr; - system::logger_opt_smart_ptr logger = nullptr; - }; - struct SCreationParams : SCachedCreationParams - { - explicit inline operator bool() const - { - if (!assMan) - return false; - return true; - } - }; - static core::smart_refctd_ptr create(SCreationParams&& params); - - // When outputFilePath isn't set in Film Element in Mitsuba, use this to find the extension string. - static inline std::string_view fileExtensionFromFormat(ext::MitsubaLoader::CElementFilm::FileFormat format) - { - using FileFormat = ext::MitsubaLoader::CElementFilm::FileFormat; - switch (format) - { - case FileFormat::PNG: - return ".png"; - case FileFormat::OPENEXR: - return ".exr"; - case FileFormat::JPEG: - return ".jpg"; - default: - break; - } - return ""; - } - - struct SLoadResult - { - struct SSensor - { - using type_e = ext::MitsubaLoader::CElementSensor::Type; - - inline SSensor() = default; - inline SSensor(const SSensor&) = default; - inline SSensor(SSensor&&) = default; - inline SSensor& operator=(const SSensor&) = default; - inline SSensor& operator=(SSensor&&) = default; - - explicit inline operator bool() const - { - return bool(constants) && mutableDefaults.valid(constants) && bool(dynamicDefaults); - } - - struct SConstants - { - constexpr static inline uint32_t MaxWidth = 0x1u<<(sizeof(uint16_t)*8-2); - constexpr static inline uint32_t MaxHeight = MaxWidth; - constexpr static inline uint32_t MaxCascadeCount = 15; - - explicit inline operator bool() const - { - if (width <= 0 || width >= MaxWidth) - return false; - if (height <= 0 || height >= MaxHeight) - return false; - if (type != type_e::INVALID) - return false; - if (cascadeCount <= 0 || cascadeCount >= MaxCascadeCount) - return false; - return true; - } - - // where the FFT bloom kernel is - system::path bloomFilePath = {}; - // - uint32_t width = 0u; - uint32_t height = 0u; - // - type_e type = type_e::INVALID; - // - uint8_t cascadeCount : 4 = 1; - } constants = {}; - // these could theoretically change without recreating session resources - struct SMutable - { - constexpr static inline uint8_t MaxClipPlanes = 6; - - inline uint8_t getClipPlaneCount() const - { - using namespace nbl::hlsl; - for (uint8_t i=0; i(0.f); - const auto& rhs = clipPlanes[i].xyz; - if (any(glsl::notEqual(lhs,rhs))) - continue; - return i; - } - return MaxClipPlanes; - } - - inline bool valid(const SConstants& cst) const - { - // TODO more checks - return true; - } - - // inverse of view matrix, can include SCALE ! - hlsl::float32_t3x4 absoluteTransform; - // TODO: thin lens and telecentric support - struct Raygen - { - public: - enum class Type : uint8_t - { - Persp = 0, - Ortho = 1, - Env = 2 - }; - - // - inline Type getType() const - { - // note that actual matrix always requires columns to have Y- directions - if (encoded[1][1]<0.f) - return Type::Persp; - if (encoded[1][1]>0.f) - return Type::Ortho; - return Type::Env; - } - - // for a raygen shader to transform the [0,1]^2 NDC coord into a ray (without tMin/tStart) - // PERSP `dir = normalize(float3(pseudo_mul(mat,ndc),-1)); - // origin = -float32_t3(dir.xy/dir.z,nearClip);` - // ORTHO `origin = float32_t3(pseudo_mul(mat,ndc),-nearClip); - // dir = float32_t(0,0,-1)` - inline explicit operator hlsl::float32_t2x3() const - { - auto retval = encoded; - // y-axis column shall always be negative - if (encoded[1][1]>0.f) - { - retval[0][1] = -encoded[0][1]; - retval[1][1] = -encoded[1][1]; - } - return retval; - } - - // Whether Z+ or Z- is forward,and X- or X+ is right for the camera - inline bool isRightHanded() const {return encoded[0][0]>0.f;} - - private: - friend class CSceneLoader; - - hlsl::float32_t2x3 encoded = {}; - } raygen; - // - std::array clipPlanes = {}; - // denoiser and bloom require rendering with a "skirt" this controls the skirt size - int32_t cropWidth = 0u; - int32_t cropHeight = 0u; - int32_t cropOffsetX = 0u; - int32_t cropOffsetY = 0u; - // - float nearClip; - float farClip; - // - float cascadeLuminanceBase = core::nan(); - float cascadeLuminanceStart = core::nan(); - // - uint16_t hideEnvironment : 1 = false; - uint16_t russianRouletteDepth : 15 = 0x7fffu; - uint16_t maxPathDepth = 0; - } mutableDefaults = {}; - // these can change without having to reset accumulations, etc. - struct SDynamic - { - // For a legacy `smgr->addCameraSceneNodeModifiedMaya(nullptr, -1.0f * mainSensorData.rotateSpeed, 50.0f, mainSensorData.moveSpeed, -1, 2.0f, defaultZoomSpeedMultiplier, false, true)` - constexpr static inline float DefaultRotateSpeed = 300.0f; - constexpr static inline float DefaultZoomSpeed = 1.0f; - constexpr static inline float DefaultMoveSpeed = 100.0f; - constexpr static inline float DefaultSceneSize = 50.0f; // reference for default zoom and move speed; - // no constexpr std::pow - //constexpr static inline float DefaultZoomSpeedMultiplier = std::pow(DefaultSceneSize,DefaultZoomSpeed/DefaultSceneSize); - - struct SPostProcess - { - std::filesystem::path bloomFilePath; - float bloomScale = 0.0f; - float bloomIntensity = 0.0f; - std::string tonemapperArgs = ""; - }; - - // - explicit inline operator bool() const - { - // TODO more checks - return !hlsl::isnan(moveSpeed); - } - - // members - system::path outputFilePath = {}; - SPostProcess postProc = {}; - // even though spherical can't rotate, the preview camera can - hlsl::float32_t3 up = {}; - float rotateSpeed = core::nan(); - union - { - struct SZoomable // spherical can't zoom - { - float speed = core::nan(); - } zoomable = {}; - }; - // - float moveSpeed = core::nan(); - // - uint32_t samplesNeeded = 0u; - float kappa = 0.f; - float Emin = 0.05f; - } dynamicDefaults = {}; - - }; - - explicit inline operator bool() const - { - if (!scene || sensors.empty()) - return false; - return true; - } - - // - core::smart_refctd_ptr scene = {}; - // - core::vector sensors; - // TODO: for Material Compiler - //std::future compileShadersFuture = {}; - }; - struct SLoadParams - { - system::path relPath = ""; - system::path workingDirectory = ""; - }; - SLoadResult load(SLoadParams&& _params); - - protected: - struct SConstructorParams : SCachedCreationParams - { - }; - inline CSceneLoader(SConstructorParams&& _params) : m_params(std::move(_params)) {} - virtual inline ~CSceneLoader() {} - - SConstructorParams m_params; -}; - -} - -#ifndef _NBL_THIS_EXAMPLE_C_SCENE_LOADER_CPP_ -extern template struct nbl::system::impl::to_string_helper; -#endif - -#endif diff --git a/40_PathTracer/include/renderer/CRenderer.h b/40_PathTracer/include/renderer/CRenderer.h deleted file mode 100644 index 62995369d..000000000 --- a/40_PathTracer/include/renderer/CRenderer.h +++ /dev/null @@ -1,180 +0,0 @@ -// Copyright (C) 2025-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 -#ifndef _NBL_THIS_EXAMPLE_C_RENDERER_H_INCLUDED_ -#define _NBL_THIS_EXAMPLE_C_RENDERER_H_INCLUDED_ - - -#include "nbl/examples/common/CCachedOwenScrambledSequence.hpp" - -#include "renderer/CScene.h" -#include "renderer/CSession.h" - -#include "renderer/shaders/pathtrace/push_constants.hlsl" -#include "nbl/this_example/builtin/build/spirv/keys.hpp" - - -namespace nbl::this_example -{ - -class CRenderer : public core::IReferenceCounted, public core::InterfaceUnmovable -{ - friend struct SSubmitInfo; - public: - // - constexpr static video::SPhysicalDeviceFeatures RequiredDeviceFeatures() - { - video::SPhysicalDeviceFeatures retval = {}; - retval.rayTracingPipeline = true; - retval.accelerationStructure = true; - return retval; - } - // - constexpr static video::SPhysicalDeviceFeatures PreferredDeviceFeatures() - { - auto retval = RequiredDeviceFeatures(); - retval.accelerationStructureHostCommands = true; - return retval; - } -#if 0 // see TODO in main.cpp - constexpr static video::SPhysicalDeviceLimits RequiredDeviceLimits() - { - video::SPhysicalDeviceLimits retval = {}; - retval.shaderStorageImageReadWithoutFormat = true; - return retval; - } -#endif - // - template - static inline core::smart_refctd_ptr loadPrecompiledShader( - asset::IAssetManager* assMan, video::ILogicalDevice* device, system::logger_opt_ptr logger={} - ) - { - return loadPrecompiledShader_impl(assMan,builtin::build::get_spirv_key(device),logger); - } - - struct SCachedCreationParams - { - inline operator bool() const - { - if (!graphicsQueue || !computeQueue || !uploadQueue) - return false; - if (!utilities) - return false; - if (graphicsQueue->getOriginDevice()!=utilities->getLogicalDevice()) - return false; - if (computeQueue->getOriginDevice()!=utilities->getLogicalDevice()) - return false; - if (uploadQueue->getOriginDevice()!=utilities->getLogicalDevice()) - return false; - return true; - } - - video::CThreadSafeQueueAdapter* graphicsQueue = nullptr; - video::CThreadSafeQueueAdapter* computeQueue = nullptr; - video::CThreadSafeQueueAdapter* uploadQueue = nullptr; - // - core::smart_refctd_ptr utilities = nullptr; - // can be null - system::logger_opt_smart_ptr logger = nullptr; - }; - struct SCreationParams : SCachedCreationParams - { - asset::IAssetManager* assMan; - std::string sequenceCachePath; - }; - static core::smart_refctd_ptr create(SCreationParams&& _params); - - // - inline const SCachedCreationParams& getCreationParams() const { return m_creation; } - - // - inline system::logger_opt_ptr getLogger() const {return m_creation.logger.get().get();} - - // - inline video::ILogicalDevice* getDevice() const {return m_creation.utilities->getLogicalDevice();} - - struct SCachedConstructionParams - { - constexpr static inline uint8_t FramesInFlight = 3; - - // TODO: Some Constant to Tell us how many dimensions each path vertex consumes - inline auto getSequenceMaxPathDepth() const {return sequenceHeader.maxDimensions/3;} - - - core::smart_refctd_ptr semaphore; - - // per pipeline UBO for other pipelines - core::smart_refctd_ptr uboDSLayout; - // descriptor set for a scene shall contain sampled textures and compiled materials - core::smart_refctd_ptr sceneDSLayout; - // descriptor set for sensors - core::smart_refctd_ptr sensorDSLayout; - - // temporary - std::array,uint8_t(CSession::RenderMode::Count)> shaders; - std::array,uint8_t(CSession::RenderMode::Count)> renderingLayouts; - // TODO -// std::array,uint8_t(CSession::RenderMode::Count)> genericPipelines; - - // - core::smart_refctd_ptr commandBuffers[FramesInFlight]; - - // - core::smart_refctd_ptr sobolSequence; - //! Brief guideline to good path depth limits - // Want to see stuff with indirect lighting on the other side of a pane of glass - // 5 = glass frontface->glass backface->diffuse surface->diffuse surface->light - // Want to see through a glass box, vase, or office - // 7 = glass frontface->glass backface->glass frontface->glass backface->diffuse surface->diffuse surface->light - // pick higher numbers for better GI and less bias - // TODO: Upload only a subsection of the sample sequence to the GPU, so we can use more samples without trashing VRAM - examples::CCachedOwenScrambledSequence::SCacheHeader sequenceHeader = {}; - }; - // - inline const SCachedConstructionParams& getConstructionParams() const {return m_construction;} - - // - core::smart_refctd_ptr createScene(CScene::SCreationParams&& _params); - - // - struct SSubmit final : core::Uncopyable - { - public: - inline SSubmit() {} - inline SSubmit(CRenderer* _renderer, video::IGPUCommandBuffer* _cb) : renderer(_renderer), cb(_cb) {assert(operator bool());} - - inline operator bool() const {return cb;} - inline operator video::IGPUCommandBuffer*() const {return cb;} - - // returns semaphore signalled by submit - video::IQueue::SSubmitInfo::SSemaphoreInfo operator()(std::span extraWaits); - - asset::PIPELINE_STAGE_FLAGS stageMask = asset::PIPELINE_STAGE_FLAGS::RAY_TRACING_SHADER_BIT; - private: - CRenderer* renderer = nullptr; - video::IGPUCommandBuffer* cb = nullptr; - }; - SSubmit render(CSession* session); - - protected: - struct SConstructorParams : SCachedCreationParams, SCachedConstructionParams - { -#if 0 - // Resources used for envmap sampling - nbl::ext::EnvmapImportanceSampling::EnvmapImportanceSampling m_envMapImportanceSampling; -#endif - }; - inline CRenderer(SConstructorParams&& _params) : m_creation(std::move(_params)), m_construction(std::move(_params)), - m_frameIx(m_construction.semaphore->getCounterValue()) {} - virtual inline ~CRenderer() {} - - static core::smart_refctd_ptr loadPrecompiledShader_impl(asset::IAssetManager* assMan, const core::string& key, system::logger_opt_ptr logger); - - SCachedCreationParams m_creation; - SCachedConstructionParams m_construction; - uint64_t m_frameIx; -}; - -} -#endif diff --git a/40_PathTracer/include/renderer/CScene.h b/40_PathTracer/include/renderer/CScene.h deleted file mode 100644 index babcd14cf..000000000 --- a/40_PathTracer/include/renderer/CScene.h +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright (C) 2025-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 -#ifndef _NBL_THIS_EXAMPLE_C_SCENE_H_INCLUDED_ -#define _NBL_THIS_EXAMPLE_C_SCENE_H_INCLUDED_ - - -#include "io/CSceneLoader.h" -#include "renderer/CSession.h" -#include "renderer/shaders/scene.hlsl" - - -namespace nbl::this_example -{ -class CRenderer; - -class CScene : public core::IReferenceCounted, public core::InterfaceUnmovable -{ - public: - struct SCachedCreationParams - { - }; - struct SCreationParams : SCachedCreationParams - { - CSceneLoader::SLoadResult load = {}; - video::CAssetConverter* converter = nullptr; - - inline operator bool() const - { - if (!load) - return false; - // converter can be null, we can make a new one - return true; - } - }; - - // - inline CRenderer* getRenderer() const {return m_construction.renderer.get();} - - // - inline video::IGPURayTracingPipeline* getPipeline(const CSession::RenderMode mode) const - { - return m_construction.pipelines[static_cast(mode)].get(); - } - - // - inline const auto& getSBT(const CSession::RenderMode mode) const {return m_construction.sbts[static_cast(mode)];} - - // - inline const video::IGPUDescriptorSet* getDescriptorSet() const {return m_construction.sceneDS->getDescriptorSet();} - - using sensor_t = CSceneLoader::SLoadResult::SSensor; - // - inline std::span getSensors() const {return m_construction.sensors;} - - // - core::smart_refctd_ptr createSession(const CSession::SCreationParams& sensor); - - protected: - friend class CRenderer; - struct SCachedConstructorParams - { - // - hlsl::shapes::AABB<> sceneBound; - // - core::vector sensors; - // backward link for reference counting - core::smart_refctd_ptr renderer; - // specialized per-scene pipelines - core::smart_refctd_ptr pipelines[uint8_t(CSession::RenderMode::Count)]; - // - video::IGPURayTracingPipeline::SShaderBindingTable sbts[uint8_t(CSession::RenderMode::Count)]; - // descriptor set for a scene shall contain sampled textures and compiled materials - core::smart_refctd_ptr sceneDS; - // main TLAS - core::smart_refctd_ptr TLAS; - }; - struct SConstructorParams : SCachedCreationParams, SCachedConstructorParams - { - // sensor list can be empty, we can just make one up as we go along - inline operator bool() const - { - for (uint8_t i=0; i(CSession::RenderMode::Count); i++) - if (const auto* pipeline=pipelines[i].get(); !pipeline || !sbts[i].valid(pipeline->getCreationFlags())) - return false; - return renderer && sceneDS; - } - }; - inline CScene(SConstructorParams&& _params) : m_creation(std::move(_params)), m_construction(std::move(_params)) {} - virtual inline ~CScene() {} - - SCachedCreationParams m_creation; - SCachedConstructorParams m_construction; -}; - -} -#endif diff --git a/40_PathTracer/include/renderer/CSession.h b/40_PathTracer/include/renderer/CSession.h deleted file mode 100644 index 993c3ff19..000000000 --- a/40_PathTracer/include/renderer/CSession.h +++ /dev/null @@ -1,158 +0,0 @@ -// Copyright (C) 2025-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 -#ifndef _NBL_THIS_EXAMPLE_C_SESSION_H_INCLUDED_ -#define _NBL_THIS_EXAMPLE_C_SESSION_H_INCLUDED_ - - -#include "io/CSceneLoader.h" - -#include "renderer/shaders/session.hlsl" -#include "renderer/shaders/pathtrace/push_constants.hlsl" - - -namespace nbl::this_example -{ -class CScene; - -class CSession final : public core::IReferenceCounted -{ - public: - using sensor_t = CSceneLoader::SLoadResult::SSensor; - using sensor_type_e = sensor_t::SMutable::Raygen::Type; - enum class RenderMode : uint8_t - { - Previs, - Beauty, - Debug, - Count - }; - struct SCachedCreationParams - { - RenderMode mode = RenderMode::Beauty; - }; - struct SCreationParams : SCachedCreationParams - { - inline operator bool() const {return sensor;} - - const sensor_t* sensor; - }; - - // - bool init(video::SIntendedSubmitInfo& info); - - // - inline bool isInitialized() const {return bool(m_active.immutables);} - - // heavy VRAM data and data only needed during an active session - struct SImageWithViews - { - inline operator bool() const - { - return image && !views.empty() && views.begin()->second; - } - - inline video::IGPUImageView* getView(const asset::E_FORMAT format) const - { - if (const auto found=views.find(format); found!=views.end()) - return found->second.get(); - return nullptr; - } - - core::smart_refctd_ptr image = {}; - core::unordered_map> views = {}; - }; - struct SActiveResources - { - struct SImmutables - { - inline operator bool() const - { - return bool(scrambleKey) && sampleCount && rwmcCascades && albedo && normal && motion && mask && ds; - } - - // QUESTION: No idea how to marry RWMC with Temporal Denoise, do we denoise separately per cascade? - // ANSWER: RWMC relies on many spp, can use denoised/reprojected to confidence measures from other cascades. - // Shouldn't touch the previous frame, denoiser needs to know what was on screen last frame, only touch current. - // QUESTION: with temporal denoise do we turn the `sampleCount` into a `sequenceOffset` texutre? - SImageWithViews scrambleKey = {}, sampleCount = {}, beauty = {}, rwmcCascades = {}, albedo = {}, normal = {}, motion = {}, mask = {}; - // stores all the sensor data required - core::smart_refctd_ptr ds = {}; - }; - SImmutables immutables = {}; - SSensorDynamics currentSensorState = {}, prevSensorState = {}; - }; - - // - inline const SActiveResources& getActiveResources() const {return m_active;} - - // - bool reset(const SSensorDynamics& newVal, video::SIntendedSubmitInfo& info); - - // - bool update(const SSensorDynamics& newVal); - - // TODO: figure this out - inline float getProgress() const - { - return 0.f; - } - - // - inline void deinit() - { - m_active = {}; - } - - // - struct SConstructionParams : SCachedCreationParams - { - core::string name = "TODO from `sensor`"; - core::smart_refctd_ptr scene; - SResolveConstants initResolveConstants; - SSensorUniforms uniforms; - SSensorDynamics initDynamics; - hlsl::uint16_t2 cropOffsets; - hlsl::uint16_t2 cropResolution; - sensor_type_e type; - }; - inline const SConstructionParams& getConstructionParams() const {return m_params;} - - private: - friend class CScene; - inline CSession(SConstructionParams&& _params) : m_params(std::move(_params)) {} - - const SConstructionParams m_params; - SActiveResources m_active = {}; -}; - -} - -// -namespace nbl::system::impl -{ -template<> -struct to_string_helper -{ - private: - using enum_t = nbl::this_example::CSession::RenderMode; - - public: - static inline std::string __call(const enum_t value) - { - switch (value) - { - case enum_t::Beauty: - return "Beauty"; - case enum_t::Previs: - return "Previs"; - case enum_t::Debug: - return "Debug"; - default: - break; - } - return ""; - } -}; -} -#endif diff --git a/40_PathTracer/include/renderer/SAASequence.h b/40_PathTracer/include/renderer/SAASequence.h deleted file mode 100644 index 460e9ee69..000000000 --- a/40_PathTracer/include/renderer/SAASequence.h +++ /dev/null @@ -1,1046 +0,0 @@ -// Copyright (C) 2025-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 "nbl/builtin/hlsl/cpp_compat.hlsl" - -namespace nbl::this_example -{ - -// -struct SAASequence -{ - using type_t = nbl::hlsl::float32_t2; - - inline operator std::span() const {return {Data,Size};} - - static constexpr inline uint32_t Size = 1024; - constexpr static inline type_t Data[Size] = - { - {0.229027962000000, 0.100901043000000}, - {0.934988661250000, 0.900492937500000}, - {0.693936740750000, 0.477888665000000}, - {0.396013875250000, 0.867381653000000}, - {0.151208663250000, 0.331649132250000}, - {0.919338615000000, 0.306386117750000}, - {0.454737456500000, 0.597940860250000}, - {0.911951413000000, 0.584874565000000}, - {0.471331207500000, 0.117509299250000}, - {0.724981748000000, 0.988645892000000}, - {0.227727943750000, 0.553082892250000}, - {0.927148254750000, 0.059077206250000}, - {0.170420940250000, 0.853803466500000}, - {0.369496963250000, 0.372492160250000}, - {0.709055501500000, 0.719526612750000}, - {0.708593019750000, 0.236308825250000}, - {0.053515783250000, 0.244794542562500}, - {0.759417624125000, 0.846532545187500}, - {0.572365454937500, 0.341559262437500}, - {0.269128942562500, 0.962581831375000}, - {0.246508261687500, 0.286661635812500}, - {0.819542439062500, 0.459099133812500}, - {0.411348913687500, 0.737420359250000}, - {0.896647944437500, 0.717554343125000}, - {0.358057598000000, 0.050206801437500}, - {0.605871046250000, 0.779868041500000}, - {0.036816445812500, 0.506511135625000}, - {0.806931985937500, 0.138270723062500}, - {0.045020470000000, 0.818334270875000}, - {0.433264399500000, 0.254739200375000}, - {0.556258709500000, 0.559776624000000}, - {0.611048395312500, 0.162518625750000}, - {0.028918631812500, 0.053438072375000}, - {0.856252533125000, 0.916712681500000}, - {0.580344816187500, 0.463534157062500}, - {0.291334488000000, 0.774756179000000}, - {0.157847279187500, 0.464948199125000}, - {0.775478249937500, 0.320623736250000}, - {0.306258709500000, 0.653526624000000}, - {0.798533046937500, 0.552896543187500}, - {0.349953270437500, 0.123764825500000}, - {0.534027961437500, 0.969931745937500}, - {0.122488661312500, 0.681742937625000}, - {0.849003468812500, 0.216845413250000}, - {0.145343900750000, 0.962506045625000}, - {0.395929912437500, 0.488477370312500}, - {0.675219736437500, 0.601237158875000}, - {0.728921568625000, 0.053308823500000}, - {0.153721825125000, 0.145597505062500}, - {0.852763510375000, 0.797682223125000}, - {0.644595719312500, 0.367380713687500}, - {0.475934665312500, 0.787623234375000}, - {0.037670496437500, 0.386130180750000}, - {0.916111850937500, 0.403604173437500}, - {0.307256453062500, 0.518207928812500}, - {0.836158139312500, 0.677526975812500}, - {0.291525812500000, 0.197831715312500}, - {0.632543215125000, 0.896220934750000}, - {0.039235045687500, 0.629605464812500}, - {0.927263875375000, 0.179881653187500}, - {0.036335975187500, 0.990626511375000}, - {0.458406617875000, 0.372877193062500}, - {0.545614665812500, 0.676662283062500}, - {0.606815968812500, 0.044970413250000}, - {0.031533697125000, 0.184836288625000}, - {0.943869562500000, 0.830155934062500}, - {0.607026984312500, 0.286243495000000}, - {0.385468447812500, 0.923477959062500}, - {0.211591778000000, 0.432717372437500}, - {0.959561740812500, 0.477888665062500}, - {0.340921091062500, 0.599871303750000}, - {0.770926812125000, 0.740443845937500}, - {0.492972183312500, 0.243769330562500}, - {0.520086204062500, 0.865883539250000}, - {0.194132187625000, 0.711586172812500}, - {0.867832801875000, 0.029377324812500}, - {0.018898352500000, 0.755166315812500}, - {0.294110519250000, 0.340476317312500}, - {0.645436781125000, 0.669120978187500}, - {0.537010584750000, 0.070669853500000}, - {0.161951413000000, 0.209874565062500}, - {0.786335975187500, 0.990626511375000}, - {0.525681985937500, 0.419520723062500}, - {0.287619562500000, 0.834550465312500}, - {0.100299557750000, 0.367542953000000}, - {0.787670496437500, 0.386130180750000}, - {0.425010132750000, 0.666850725937500}, - {0.959417841312500, 0.712724761625000}, - {0.259027114250000, 0.027505482375000}, - {0.706747124500000, 0.863983912687500}, - {0.118758709500000, 0.559776624000000}, - {0.979834653750000, 0.076596529437500}, - {0.076814113250000, 0.879551982187500}, - {0.458038607062500, 0.495297691687500}, - {0.676899749875000, 0.533654791000000}, - {0.739509651750000, 0.162886922875000}, - {0.130635833000000, 0.032884578937500}, - {0.995486845875000, 0.879726983937500}, - {0.681683761437500, 0.415213866187500}, - {0.471888733500000, 0.975077322375000}, - {0.002080578437500, 0.292317740812500}, - {0.982026984312500, 0.286243495000000}, - {0.291525812500000, 0.713456715312500}, - {0.803515783250000, 0.619794542562500}, - {0.363736251000000, 0.241491573500000}, - {0.581375603187500, 0.850024182625000}, - {0.126134788437500, 0.739345154625000}, - {0.807256990625000, 0.025225260812500}, - {0.214063133312500, 0.979178170312500}, - {0.279120068187500, 0.455460706437500}, - {0.521614411125000, 0.748128257250000}, - {0.541375661500000, 0.191916865812500}, - {0.092374240812500, 0.093123040062500}, - {0.819780017000000, 0.863865176562500}, - {0.723535390937500, 0.290673655562500}, - {0.333626471625000, 0.991508772375000}, - {0.180081879937500, 0.273337083437500}, - {0.884853249937500, 0.353826861250000}, - {0.486489450437500, 0.649922456187500}, - {0.970355124125000, 0.588720045187500}, - {0.411054041562500, 0.190728892687500}, - {0.670557598000000, 0.782628676437500}, - {0.176686781125000, 0.590995978187500}, - {0.923484185187500, 0.119472166250000}, - {0.229834653750000, 0.826596529437500}, - {0.402229645500000, 0.427815757250000}, - {0.614887300500000, 0.582390020187500}, - {0.721331207625000, 0.117509299250000}, - {0.221780261562500, 0.160787322875000}, - {0.980871046250000, 0.779868041500000}, - {0.521614411125000, 0.498128257250000}, - {0.462698109750000, 0.855009158437500}, - {0.102148981812500, 0.485181351500000}, - {0.790505847500000, 0.272359588500000}, - {0.357263913000000, 0.553624565062500}, - {0.852875617687500, 0.518271589687500}, - {0.412788910312500, 0.072860258937500}, - {0.739509651750000, 0.912886922875000}, - {0.244715387500000, 0.610882883562500}, - {0.931245437000000, 0.247161473000000}, - {0.118495619500000, 0.827404835625000}, - {0.356241537562500, 0.307793951312500}, - {0.739954645312500, 0.601971750750000}, - {0.652229645500000, 0.240315757250000}, - {0.085230272750000, 0.149967825937500}, - {0.790487853250000, 0.802468641250000}, - {0.742972183312500, 0.431269330562500}, - {0.338023546687500, 0.864140358375000}, - {0.161359195250000, 0.386030244500000}, - {0.979622565375000, 0.415764143437500}, - {0.344324410875000, 0.743490102812500}, - {0.850000234687500, 0.588036936812500}, - {0.478921568625000, 0.053308823500000}, - {0.575878945812500, 0.904948635625000}, - {0.066809364125000, 0.711985215062500}, - {0.842374240812500, 0.093123040062500}, - {0.072833622000000, 0.943689044750000}, - {0.473982478062500, 0.309619342562500}, - {0.643468702500000, 0.727011596187500}, - {0.661784804062500, 0.096504548000000}, - {0.075593410125000, 0.020665263437500}, - {0.846367111937500, 0.980869489750000}, - {0.584417841312500, 0.402177886625000}, - {0.419264650000000, 0.807665176000000}, - {0.108911798812500, 0.274823750687500}, - {0.842214949125000, 0.395649388625000}, - {0.424460011250000, 0.541515061937500}, - {0.914875915625000, 0.525088448500000}, - {0.276815978250000, 0.138406141250000}, - {0.682946765937500, 0.941192325375000}, - {0.243922631125000, 0.674414353500000}, - {0.983747165312500, 0.225123234375000}, - {0.209039534812500, 0.919381743937500}, - {0.317979460312500, 0.396931190375000}, - {0.595789254625000, 0.645833852812500}, - {0.589063133312500, 0.229178170312500}, - {0.201732996437500, 0.034567680750000}, - {0.911951413000000, 0.959874565062500}, - {0.669465614812500, 0.307270670687500}, - {0.442773254937500, 0.918452206312500}, - {0.228659227750000, 0.498372525687500}, - {0.864786425062500, 0.258916160937500}, - {0.366015783250000, 0.682294542562500}, - {0.832368054687500, 0.749523853312500}, - {0.475148582250000, 0.180782790250000}, - {0.543804934062500, 0.806559289687500}, - {0.041831345187500, 0.574164114062500}, - {0.981787063687500, 0.014769301562500}, - {0.167325380812500, 0.796656456375000}, - {0.305883847687500, 0.260168413875000}, - {0.736593646187500, 0.544303438875000}, - {0.595631980687500, 0.113338942562500}, - {0.233747165312500, 0.225123234375000}, - {0.881420496437500, 0.854880180750000}, - {0.514120916562500, 0.361726452125000}, - {0.262088408375000, 0.897305108937500}, - {0.040764352812500, 0.448613484812500}, - {0.882527922875000, 0.453355770312500}, - {0.486593646187500, 0.544303438875000}, - {0.944193581437500, 0.650074503000000}, - {0.403764392000000, 0.003513614062500}, - {0.647805652187500, 0.839498260375000}, - {0.004346402437500, 0.700568695812500}, - {0.863684364125000, 0.149485215062500}, - {0.075593410125000, 0.770665263437500}, - {0.260573230937500, 0.378374937125000}, - {0.606947383687500, 0.518907935937500}, - {0.522543332062500, 0.131538315687500}, - {0.115674527437500, 0.213752289562500}, - {0.978861550187500, 0.943531534250000}, - {0.716222608750000, 0.357993041500000}, - {0.396123640562500, 0.988911053812500}, - {0.116417439062500, 0.427849133812500}, - {0.960375986437500, 0.355143408875000}, - {0.396123640562500, 0.613911053812500}, - {0.771872079375000, 0.679610301562500}, - {0.407651999187500, 0.129979780250000}, - {0.610967390562500, 0.957538983500000}, - {0.099030910125000, 0.622227763437500}, - {0.792605235062500, 0.213450866625000}, - {0.231787063687500, 0.764769301562500}, - {0.345102996500000, 0.451097146437500}, - {0.537639116937500, 0.609792796562500}, - {0.670557598000000, 0.032628676437500}, - {0.161148929812500, 0.091845178375000}, - {0.915446201000000, 0.774126136937500}, - {0.542495165812500, 0.275647345250000}, - {0.316935423562500, 0.923529210750000}, - {0.209068937250000, 0.340693571625000}, - {0.770926812125000, 0.490443845937500}, - {0.462732614000000, 0.712224844000000}, - {0.887121046250000, 0.643149291500000}, - {0.302373640562500, 0.082661053812500}, - {0.728921568625000, 0.803308823500000}, - {0.181258709500000, 0.653526624000000}, - {0.977987853250000, 0.146218641250000}, - {0.016702341062500, 0.927996303750000}, - {0.467374240812500, 0.431013665062500}, - {0.706158315437500, 0.659556033875000}, - {0.669301523312500, 0.186625825562500}, - {0.039461819812500, 0.116237049750000}, - {0.798533046937500, 0.927896543187500}, - {0.636245165812500, 0.463147345250000}, - {0.358057598000000, 0.800206801437500}, - {0.057953992187500, 0.323742603312500}, - {0.838196808062500, 0.318240323625000}, - {0.288441098000000, 0.563378403500000}, - {0.981947383687500, 0.518907935937500}, - {0.348181288187500, 0.183212688250000}, - {0.506420496437500, 0.917380180750000}, - {0.164875915625000, 0.525088448500000}, - {0.787802165812500, 0.082912283062500}, - {0.134409779187500, 0.902448199125000}, - {0.408376747562500, 0.326245734125000}, - {0.584561740812500, 0.727888665062500}, - {0.534266910125000, 0.009900787187500}, - {0.192731703031250, 0.122610961484375}, - {0.886403666453125, 0.919165570765625}, - {0.740362459437500, 0.479082682703125}, - {0.381143881328125, 0.825372076343750}, - {0.177058600328125, 0.355660703968750}, - {0.880411986109375, 0.304388585781250}, - {0.489954645359375, 0.601971750750000}, - {0.931245437015625, 0.622161472968750}, - {0.495691442609375, 0.089534956375000}, - {0.748617226093750, 0.964032599359375}, - {0.200716102781250, 0.538594564312500}, - {0.902762098828125, 0.040629656437500}, - {0.167366403703125, 0.826817667671875}, - {0.326986691484375, 0.360298081343750}, - {0.725793624375000, 0.689620323765625}, - {0.725003755000000, 0.197653489734375}, - {0.019203528312500, 0.219887995546875}, - {0.789461819796875, 0.866237049781250}, - {0.602987853250000, 0.364968641265625}, - {0.286530910140625, 0.997227763453125}, - {0.197691088203125, 0.299653371203125}, - {0.864509651765625, 0.475386922921875}, - {0.409509265921875, 0.695098575390625}, - {0.924966150343750, 0.731175805390625}, - {0.366549151562500, 0.016182260046875}, - {0.620446765921875, 0.753692325390625}, - {0.004126035234375, 0.512365679515625}, - {0.759204111453125, 0.126627783906250}, - {0.039461819796875, 0.866237049781250}, - {0.385324830531250, 0.282537197156250}, - {0.529500805046875, 0.539659192578125}, - {0.620486845921875, 0.129726983984375}, - {0.040287232453125, 0.022961294593750}, - {0.871648411546875, 0.886075859718750}, - {0.567731703031250, 0.497610961484375}, - {0.271219649968750, 0.785940731265625}, - {0.149813081953125, 0.495059302156250}, - {0.752354406031250, 0.336633136296875}, - {0.267950605953125, 0.630717656421875}, - {0.763064970921875, 0.526211358984375}, - {0.350302165859375, 0.082912283093750}, - {0.541375661546875, 0.941916865828125}, - {0.067888875390625, 0.648631653203125}, - {0.817282235640625, 0.240645457843750}, - {0.176686781125000, 0.965995978171875}, - {0.414407018781250, 0.458335411421875}, - {0.635381359671875, 0.622395865796875}, - {0.696580598671875, 0.010305563015625}, - {0.146140435203125, 0.181972166265625}, - {0.853197227578125, 0.768215064734375}, - {0.631158461921875, 0.330667103468750}, - {0.443098689046875, 0.770526325000000}, - {0.008860189078125, 0.404241883828125}, - {0.920499240812500, 0.436873040078125}, - {0.274931749687500, 0.517395920968750}, - {0.872488661328125, 0.681742937687500}, - {0.273658139312500, 0.240026975812500}, - {0.686178846875000, 0.902720720890625}, - {0.022328994562500, 0.659601535312500}, - {0.889064677375000, 0.139944156000000}, - {0.041831345203125, 0.949164114093750}, - {0.443262299140625, 0.313206182765625}, - {0.553107937421875, 0.637161480234375}, - {0.576361266343750, 0.010049207671875}, - {0.024757727531250, 0.155556940859375}, - {0.954885609765625, 0.864774783453125}, - {0.576988498046875, 0.268435650828125}, - {0.378272153750000, 0.889096529468750}, - {0.243922631171875, 0.424414353515625}, - {0.993449504812500, 0.487462829328125}, - {0.315047772046875, 0.590538342781250}, - {0.757436479140625, 0.715431613031250}, - {0.454737456671875, 0.222940860375000}, - {0.506538910328125, 0.822860258968750}, - {0.223798019234375, 0.699851317375000}, - {0.839514399500000, 0.012551700359375}, - {0.013378945812500, 0.811198635640625}, - {0.259404890625000, 0.333637616328125}, - {0.674460011265625, 0.635265061968750}, - {0.552179912484375, 0.113477370312500}, - {0.133506990359375, 0.242482936484375}, - {0.792605235078125, 0.963450866640625}, - {0.556245437015625, 0.434661472968750}, - {0.302640369765625, 0.866357903265625}, - {0.104025812484375, 0.322831715312500}, - {0.788292439093750, 0.420036633796875}, - {0.383947288453125, 0.645595154640625}, - {0.987679025703125, 0.720586141078125}, - {0.310166960328125, 0.049274940406250}, - {0.692634651765625, 0.826949422921875}, - {0.066739180750000, 0.551367061812500}, - {0.954885609765625, 0.114774783453125}, - {0.106252533187500, 0.916712681484375}, - {0.490362459437500, 0.479082682703125}, - {0.646028602781250, 0.509297689312500}, - {0.696508847734375, 0.182043413890625}, - {0.167639399500000, 0.008157169109375}, - {0.942731703031250, 0.935110961484375}, - {0.682010510203125, 0.383364961312500}, - {0.444750986453125, 0.993815283906250}, - {0.012293182515625, 0.265019109265625}, - {0.943520700468750, 0.285664643703125}, - {0.256436740812500, 0.727888665078125}, - {0.792605235078125, 0.588450866640625}, - {0.323828669343750, 0.228345414000000}, - {0.589727949703125, 0.818705937671875}, - {0.146647944500000, 0.717554343109375}, - {0.763378945812500, 0.061198635640625}, - {0.245217761562500, 0.944967010375000}, - {0.302009651765625, 0.475386922921875}, - {0.508157018781250, 0.708335411421875}, - {0.552058600328125, 0.230660703968750}, - {0.076470961921875, 0.065042103468750}, - {0.839060384390625, 0.826948487828125}, - {0.743383847734375, 0.260168413890625}, - {0.361729406031250, 0.961633136296875}, - {0.130411986109375, 0.304388585781250}, - {0.913057411375000, 0.372578939312500}, - {0.450791960328125, 0.635700721656250}, - {0.994715387546875, 0.610882883562500}, - {0.396123640625000, 0.238911053828125}, - {0.635171568625000, 0.803308823531250}, - {0.134436274500000, 0.588235294109375}, - {0.893091363734375, 0.085389815609375}, - {0.204885609765625, 0.864774783453125}, - {0.419763913046875, 0.397374565093750}, - {0.589063133281250, 0.604178170375000}, - {0.692634651765625, 0.076949422921875}, - {0.192731703031250, 0.185110961484375}, - {0.951814247656250, 0.756306315203125}, - {0.506689186515625, 0.439765218421875}, - {0.456461826015625, 0.821001137000000}, - {0.083707248625000, 0.461775383828125}, - {0.764249240812500, 0.280623040078125}, - {0.323579912671875, 0.557221730578125}, - {0.818079645359375, 0.508221750750000}, - {0.435674328421875, 0.095052115796875}, - {0.725148582281250, 0.930782790234375}, - {0.211591777984375, 0.620217372437500}, - {0.901467761562500, 0.194967010375000}, - {0.114509651765625, 0.873824422921875}, - {0.350302165859375, 0.270412283093750}, - {0.713196808109375, 0.568240323640625}, - {0.631158461921875, 0.205667103468750}, - {0.121648411546875, 0.136075859718750}, - {0.807256990687500, 0.775225260828125}, - {0.748441255000000, 0.385153489734375}, - {0.322881990687500, 0.822100260828125}, - {0.170499240812500, 0.436873040078125}, - {0.976735045687500, 0.379605464812500}, - {0.326988498046875, 0.705935650828125}, - {0.849030910140625, 0.622227763453125}, - {0.456628942609375, 0.025081831375000}, - {0.603718978906250, 0.881272112125000}, - {0.087671365468750, 0.733711546609375}, - {0.858316099875000, 0.063684800093750}, - {0.079233855890625, 0.980882302687500}, - {0.498831558375000, 0.275019330593750}, - {0.683006746078125, 0.696560873750000}, - {0.634421685203125, 0.070155760015625}, - {0.100941098000000, 0.000878403515625}, - {0.868983666328125, 0.946905808593750}, - {0.622823333015625, 0.407884578921875}, - {0.380701413046875, 0.772374565093750}, - {0.070966777984375, 0.276467372437500}, - {0.869730392156250, 0.411764705875000}, - {0.390973727828125, 0.533716984406250}, - {0.934919978109375, 0.561328326953125}, - {0.267725152796875, 0.170350753109375}, - {0.650556467859375, 0.939171186375000}, - {0.208092013671875, 0.656130963328125}, - {0.939854406031250, 0.211633136296875}, - {0.227987853250000, 0.896218641265625}, - {0.362037827187500, 0.411778179156250}, - {0.575564970921875, 0.682461358984375}, - {0.603861550250000, 0.193531534234375}, - {0.245936791328125, 0.056280808593750}, - {0.931245437015625, 0.997161472968750}, - {0.674341363734375, 0.272889815609375}, - {0.475148582281250, 0.930782790234375}, - {0.196690920375000, 0.490305748390625}, - {0.823073230953125, 0.290484312156250}, - {0.349801672015625, 0.643614082703125}, - {0.816809364156250, 0.711985215093750}, - {0.442773254953125, 0.168452206343750}, - {0.559175124515625, 0.768645847968750}, - {0.012608312281250, 0.564660480046875}, - {0.951732996484375, 0.034567680765625}, - {0.130635833015625, 0.782884578921875}, - {0.295859197828125, 0.295320202140625}, - {0.712431749687500, 0.517395920968750}, - {0.572626461875000, 0.068089897125000}, - {0.211591777984375, 0.245217372437500}, - {0.901756746078125, 0.821560873750000}, - {0.512364601359375, 0.315328221531250}, - {0.275838593406250, 0.932598880093750}, - {0.007956102718750, 0.451497525703125}, - {0.924966150343750, 0.481175805390625}, - {0.454495282953125, 0.559257955593750}, - {0.978187903312500, 0.673257136171875}, - {0.416103249937500, 0.041326861265625}, - {0.664447403859375, 0.864416693968750}, - {0.033521988046875, 0.696631179015625}, - {0.852837228421875, 0.184355089812500}, - {0.090934062750000, 0.810372893375000}, - {0.275746963312500, 0.411554660312500}, - {0.588037245453125, 0.558795337875000}, - {0.554922654015625, 0.160357524531250}, - {0.072833622000000, 0.193689044750000}, - {0.964063133281250, 0.979178170375000}, - {0.708517234312500, 0.319548392906250}, - {0.432256990687500, 0.962725260828125}, - {0.068079645359375, 0.414471750750000}, - {0.963190877593750, 0.324420555781250}, - {0.411502470921875, 0.573086358984375}, - {0.800162124781250, 0.669611615750000}, - {0.387554934109375, 0.150309289718750}, - {0.579945004250000, 0.965966294140625}, - {0.065522102093750, 0.599326277234375}, - {0.761255117500000, 0.204583567718750}, - {0.196950541453125, 0.770728070765625}, - {0.344324410937500, 0.493490102843750}, - {0.510111550250000, 0.568531534234375}, - {0.636389399500000, 0.008157169109375}, - {0.128530229140625, 0.090431613031250}, - {0.883566727531250, 0.752475196796875}, - {0.552206093281250, 0.309003302484375}, - {0.348181288187500, 0.933212688265625}, - {0.227987853250000, 0.364968641265625}, - {0.771924527437500, 0.448127289609375}, - {0.489679912484375, 0.745313307812500}, - {0.927148254953125, 0.684077206343750}, - {0.264066255000000, 0.103903489734375}, - {0.740057568187500, 0.764054456484375}, - {0.148947313656250, 0.630208463203125}, - {0.974161986109375, 0.179388585781250}, - {0.010457836296875, 0.893541028515625}, - {0.498441255000000, 0.385153489734375}, - {0.744468904421875, 0.637380397421875}, - {0.678301531546875, 0.136423458078125}, - {0.010191088203125, 0.112153371203125}, - {0.774757727531250, 0.905556940859375}, - {0.674229406031250, 0.446008136296875}, - {0.319922419343750, 0.784986039000000}, - {0.011042923812500, 0.349437248625000}, - {0.821379264859375, 0.354136628500000}, - {0.257237357453125, 0.579287821171875}, - {0.948151308765625, 0.522112716656250}, - {0.318520700468750, 0.160664643703125}, - {0.543804934109375, 0.900309289718750}, - {0.130607996843750, 0.519919121093750}, - {0.811627065421875, 0.071665408953125}, - {0.160867175625000, 0.931752899046875}, - {0.428297719390625, 0.362355138953125}, - {0.609505036140625, 0.690144858781250}, - {0.504804041609375, 0.003228892687500}, - {0.216196606265625, 0.064729040234375}, - {0.901736845921875, 0.879726983984375}, - {0.708719649968750, 0.453909481265625}, - {0.415337952218750, 0.849024716109375}, - {0.134853249937500, 0.353826861265625}, - {0.903787097500000, 0.267080047484375}, - {0.479025812484375, 0.572831715312500}, - {0.876605124109375, 0.604345045187500}, - {0.456461826015625, 0.071001137000000}, - {0.709494562484375, 0.955644215312500}, - {0.231947383703125, 0.518907935984375}, - {0.932230392156250, 0.013327205875000}, - {0.145086204046875, 0.865883539265625}, - {0.350930456281250, 0.348899376265625}, - {0.725034838203125, 0.739106496203125}, - {0.739954645359375, 0.226971750750000}, - {0.042605235078125, 0.213450866640625}, - {0.811627065421875, 0.821665408953125}, - {0.587735274500000, 0.312719600875000}, - {0.307230392156250, 0.950827205875000}, - {0.217487057734375, 0.273792953031250}, - {0.854401308765625, 0.440081466656250}, - {0.384747995484375, 0.706561433531250}, - {0.899206688062500, 0.699456330843750}, - {0.334068937265625, 0.028193571656250}, - {0.576864665859375, 0.801662283093750}, - {0.041360180171875, 0.539240991515625}, - {0.780622165328125, 0.170435734406250}, - {0.025074889437500, 0.841885738250000}, - {0.412686740812500, 0.274763665078125}, - {0.551686781125000, 0.512870978171875}, - {0.574633261734375, 0.138224135796875}, - {0.058436791328125, 0.056280808593750}, - {0.822110274500000, 0.890844600875000}, - {0.618449504812500, 0.487462829328125}, - {0.264066255000000, 0.807028489734375}, - {0.132527922859375, 0.453355770328125}, - {0.807953992203125, 0.323742603312500}, - {0.302148254953125, 0.684077206343750}, - {0.794171695281250, 0.522748994546875}, - {0.372686140625000, 0.110004803828125}, - {0.507741981953125, 0.951245734125000}, - {0.107097304093750, 0.651192048015625}, - {0.822833622000000, 0.193689044750000}, - {0.181245437015625, 0.997161472968750}, - {0.384747995484375, 0.456561433531250}, - {0.662226998781250, 0.569583683906250}, - {0.727197083078125, 0.021632137984375}, - {0.184988661328125, 0.150492937687500}, - {0.873243045828125, 0.810942332640625}, - {0.684963615078125, 0.357045297593750}, - {0.461525611203125, 0.759528012437500}, - {0.025718904421875, 0.410817897421875}, - {0.897009311359375, 0.420948834468750}, - {0.263037063734375, 0.546019301578125}, - {0.857097304093750, 0.651192048015625}, - {0.252866199843750, 0.205957512640625}, - {0.665602115484375, 0.895166775859375}, - {0.056996963312500, 0.684992160312500}, - {0.918804934109375, 0.150309289718750}, - {0.019203528312500, 0.969887995546875}, - {0.485853633703125, 0.339220435984375}, - {0.509352115484375, 0.676416775859375}, - {0.564952190359375, 0.039350341546875}, - {0.061178846875000, 0.152720720890625}, - {0.979027962734375, 0.850901043359375}, - {0.618986693593750, 0.251067502968750}, - {0.416788412578125, 0.890801763843750}, - {0.191935423609375, 0.392279210781250}, - {0.959424527437500, 0.448127289609375}, - {0.360967390625000, 0.582538983515625}, - {0.802390435203125, 0.744472166265625}, - {0.498617226093750, 0.214032599359375}, - {0.542495165859375, 0.838147345281250}, - {0.211457644609375, 0.742513028953125}, - {0.837488317609375, 0.030941206375000}, - {0.038430456281250, 0.786399376265625}, - {0.290324504812500, 0.370275329328125}, - {0.678301531546875, 0.667673458078125}, - {0.510613942796875, 0.106955928328125}, - {0.170736691484375, 0.235298081343750}, - {0.759083993796875, 0.997656627843750}, - {0.539572313656250, 0.380208463203125}, - {0.255388875390625, 0.867381653203125}, - {0.071379264859375, 0.354136628500000}, - {0.758860189078125, 0.404241883828125}, - {0.420321786390625, 0.636298924375000}, - {0.978659227718750, 0.748372525703125}, - {0.287503755000000, 0.010153489734375}, - {0.739679912484375, 0.870313307812500}, - {0.089315978250000, 0.513406141265625}, - {0.943869562484375, 0.080155934062500}, - {0.076564677375000, 0.913381656000000}, - {0.444542439093750, 0.459099133796875}, - {0.633162999796875, 0.540307445062500}, - {0.709818581500000, 0.157887002984375}, - {0.167325380828125, 0.046656456390625}, - {0.977987853250000, 0.896218641265625}, - {0.652229645515625, 0.427815757218750}, - {0.492972183375000, 0.993769330593750}, - {0.040505847500000, 0.272359588500000}, - {0.962978249937500, 0.260076861265625}, - {0.302640369765625, 0.741357903265625}, - {0.768208405500000, 0.610922261187500}, - {0.318273390046875, 0.193320190000000}, - {0.619905641343750, 0.853941035859375}, - {0.181258709546875, 0.747276624031250}, - {0.757229657953125, 0.013359518093750}, - {0.244715387546875, 0.985882883562500}, - {0.259008847734375, 0.494543413890625}, - {0.554055652187500, 0.714498260375000}, - {0.534027961468750, 0.219931745984375}, - {0.069780017046875, 0.113865176609375}, - {0.848982478109375, 0.872119342578125}, - {0.713196808109375, 0.271365323640625}, - {0.363736251062500, 0.991491573531250}, - {0.150433761421875, 0.282401366203125}, - {0.901208663437500, 0.331649132359375}, - {0.463675308375000, 0.681269330593750}, - {0.949633261734375, 0.606974135796875}, - {0.384851754687500, 0.208333852843750}, - {0.668614601359375, 0.752828221531250}, - {0.181245437015625, 0.622161472968750}, - {0.895086204046875, 0.115883539265625}, - {0.193869562484375, 0.830155934062500}, - {0.387335340921875, 0.396347453890625}, - {0.564854406031250, 0.586633136296875}, - {0.745691442609375, 0.089534956375000}, - {0.245486845921875, 0.129726983984375}, - {0.982811359250000, 0.811790368250000}, - {0.536503468843750, 0.474657913296875}, - {0.489679912484375, 0.870313307812500}, - {0.067888875390625, 0.492381653203125}, - {0.751398662484375, 0.307813307812500}, - {0.352987853250000, 0.521218641265625}, - {0.869468904421875, 0.543630397421875}, - {0.400000938734375, 0.102147747421875}, - {0.716287097500000, 0.892080047484375}, - {0.204945004250000, 0.590966294140625}, - {0.883506990359375, 0.242482936484375}, - {0.065143307734375, 0.844593734281250}, - {0.327001686515625, 0.299140218421875}, - {0.748446786390625, 0.573798924375000}, - {0.665686274500000, 0.213235294109375}, - {0.115683153500000, 0.178056211000000}, - {0.757883424281250, 0.796209072156250}, - {0.713222390562500, 0.397222857359375}, - {0.362679025703125, 0.845586141078125}, - {0.147009311359375, 0.420948834468750}, - {0.954472218843750, 0.404345413296875}, - {0.363836204046875, 0.740883539265625}, - {0.821904890625000, 0.583637616328125}, - {0.492832801906250, 0.029377324812500}, - {0.599161986109375, 0.929388585781250}, - {0.096331207625000, 0.695634299281250}, - {0.848982478109375, 0.122119342578125}, - {0.099003468843750, 0.966845413296875}, - {0.462431749687500, 0.267395920968750}, - {0.677354460328125, 0.725544471656250}, - {0.650008709546875, 0.122276624031250}, - {0.123243045828125, 0.060942332640625}, - {0.822833622000000, 0.943689044750000}, - {0.586591777984375, 0.432717372437500}, - {0.408903485687500, 0.776738982078125}, - {0.080479406031250, 0.305383136296875}, - {0.818079645359375, 0.414471750750000}, - {0.431931985968750, 0.513270723078125}, - {0.903169756421875, 0.555146535265625}, - {0.285517059468750, 0.166546514046875}, - {0.675219736453125, 0.976237158906250}, - {0.238942076937500, 0.643155269500000}, - {0.964063133281250, 0.229178170375000}, - {0.192731703031250, 0.935110961484375}, - {0.348181288187500, 0.401962688265625}, - {0.618003220203125, 0.658636770343750}, - {0.588190877593750, 0.199420555781250}, - {0.216958200468750, 0.007344331203125}, - {0.883506990359375, 0.992482936484375}, - {0.636042923812500, 0.294749748625000}, - {0.481304934109375, 0.900309289718750}, - {0.237679025703125, 0.470586141078125}, - {0.831750421625000, 0.262285054609375}, - {0.341264392046875, 0.675388614109375}, - {0.864509651765625, 0.725386922921875}, - {0.481304934109375, 0.150309289718750}, - {0.507180456281250, 0.786399376265625}, - {0.033602444796875, 0.600612049781250}, - {0.962250867203125, 0.054211353312500}, - {0.151703992203125, 0.761242603312500}, - {0.261464799453125, 0.261330050531250}, - {0.740667841375000, 0.511552886640625}, - {0.615093996609375, 0.088785852218750}, - {0.228861550250000, 0.193531534234375}, - {0.920420940359375, 0.853803466546875}, - {0.541111850968750, 0.345010423484375}, - {0.305472183375000, 0.900019330593750}, - {0.044319650703125, 0.478886922328125}, - {0.892725152796875, 0.482850753109375}, - {0.490667841375000, 0.511552886640625}, - {0.971780261562500, 0.629537322875000}, - {0.387554934109375, 0.056559289718750}, - {0.662939186515625, 0.814765218421875}, - {0.052390435203125, 0.744472166265625}, - {0.826564677375000, 0.163381656000000}, - {0.103197227578125, 0.768215064734375}, - {0.306265020015625, 0.415613958984375}, - {0.575564970921875, 0.526211358984375}, - {0.505411986109375, 0.183294835781250}, - {0.079233855890625, 0.230882302687500}, - {0.939854406031250, 0.961633136296875}, - {0.747488661328125, 0.369242937687500}, - {0.399926992500000, 0.954583567718750}, - {0.122446606265625, 0.392854040234375}, - {0.987719736453125, 0.351237158906250}, - {0.425219736453125, 0.601237158906250}, - {0.774082801906250, 0.642658574812500}, - {0.416299322109375, 0.161398788656250}, - {0.602987853250000, 0.989968641265625}, - {0.089514399500000, 0.575051700359375}, - {0.786335975187500, 0.240626511406250}, - {0.231815968843750, 0.794970413296875}, - {0.318750014656250, 0.443002308546875}, - {0.505566445812500, 0.623698635640625}, - {0.637067640531250, 0.039603148984375}, - {0.180883847734375, 0.072668413890625}, - {0.917325380828125, 0.796656456390625}, - {0.526756746078125, 0.259060873750000}, - {0.334878599875000, 0.893762925093750}, - {0.225425998046875, 0.315310650828125}, - {0.794319650703125, 0.478886922328125}, - {0.445957801906250, 0.724689824812500}, - {0.915440720703125, 0.656963966125000}, - {0.302640369765625, 0.116357903265625}, - {0.698976641187500, 0.774455388406250}, - {0.177148254953125, 0.684077206343750}, - {0.949633261734375, 0.138224135796875}, - {0.045314677375000, 0.913381656000000}, - {0.458038607125000, 0.401547691687500}, - {0.713980484156250, 0.625879926296875}, - {0.643255797593750, 0.155068738921875}, - {0.025074889437500, 0.091885738250000}, - {0.766702341031250, 0.927996303765625}, - {0.664447403859375, 0.489416693968750}, - {0.352787232453125, 0.772961294593750}, - {0.025478249937500, 0.320623736265625}, - {0.854074830531250, 0.352849697156250}, - {0.286530910140625, 0.622227763453125}, - {0.977727943875000, 0.553082892328125}, - {0.350685461906250, 0.153596186453125}, - {0.557230392156250, 0.880514705875000}, - {0.153169756421875, 0.555146535265625}, - {0.789461819796875, 0.116237049781250}, - {0.168804934109375, 0.900309289718750}, - {0.377355421296875, 0.330038940000000}, - {0.612679025703125, 0.720586141078125}, - {0.508506990359375, 0.054982936484375}, - {0.196917624109375, 0.096532545187500}, - {0.910867175625000, 0.931752899046875}, - {0.728171685203125, 0.443690916265625}, - {0.435674328421875, 0.845052115796875}, - {0.182230392156250, 0.325827205875000}, - {0.884087952218750, 0.286524716109375}, - {0.448834987281250, 0.579381097156250}, - {0.884436274500000, 0.588235294109375}, - {0.439752211921875, 0.123635853468750}, - {0.688559795171875, 0.981591765453125}, - {0.198151308765625, 0.522112716656250}, - {0.901703992203125, 0.011242603312500}, - {0.128530229140625, 0.840431613031250}, - {0.317826892046875, 0.321872989109375}, - {0.696508847734375, 0.744543413890625}, - {0.688847218843750, 0.216845413296875}, - {0.004724588125000, 0.188791578953125}, - {0.787802165859375, 0.832912283093750}, - {0.567052101359375, 0.371480565281250}, - {0.302148254953125, 0.996577206343750}, - {0.243986693593750, 0.251067502968750}, - {0.825795788375000, 0.474201552750000}, - {0.430456688062500, 0.699456330843750}, - {0.931258709546875, 0.747276624031250}, - {0.325564970921875, 0.057461358984375}, - {0.575724486109375, 0.777044835781250}, - {0.058436791328125, 0.525030808593750}, - {0.759808761421875, 0.157401366203125}, - {0.010191088203125, 0.862153371203125}, - {0.411476654468750, 0.296344298265625}, - {0.505607996843750, 0.519919121093750}, - {0.563898662484375, 0.182813307812500}, - {0.007229657953125, 0.013359518093750}, - {0.826564677375000, 0.913381656000000}, - {0.615744562484375, 0.440019215312500}, - {0.273049196593750, 0.752824731078125}, - {0.126134788453125, 0.489345154640625}, - {0.776528750687500, 0.346344691359375}, - {0.271093019843750, 0.673808825390625}, - {0.766504140937500, 0.549462718109375}, - {0.321690920375000, 0.115305748390625}, - {0.552058600328125, 0.980660703968750}, - {0.086158139312500, 0.677526975812500}, - {0.868983666328125, 0.196905808593750}, - {0.133506990359375, 0.992482936484375}, - {0.428389216390625, 0.479768192062500}, - {0.642183234343750, 0.594837245046875}, - {0.693414534828125, 0.060006743953125}, - {0.177263875390625, 0.179881653203125}, - {0.822881453125000, 0.799457928828125}, - {0.657602996515625, 0.342698708937500}, - {0.447833622000000, 0.795251544750000}, - {0.049908315421875, 0.432993533953125}, - {0.884744019140625, 0.384203634359375}, - {0.303495121031250, 0.531469981562500}, - {0.829156508796875, 0.635903919875000}, - {0.279003945812500, 0.225261135640625}, - {0.626154293906250, 0.912625724750000}, - {0.009546365468750, 0.639961546609375}, - {0.886403666453125, 0.169165570765625}, - {0.052259883703125, 0.979845435984375}, - {0.459494562484375, 0.330644215312500}, - {0.524813081953125, 0.651309302156250}, - {0.606787063734375, 0.014769301578125}, - {0.010457836296875, 0.143541028515625}, - {0.947626461875000, 0.818089897125000}, - {0.571917624109375, 0.284032545187500}, - {0.416299322109375, 0.911398788656250}, - {0.236033046906250, 0.396646543203125}, - {0.990797719390625, 0.456105138953125}, - {0.333626471656250, 0.616508772390625}, - {0.787209695250000, 0.716482502812500}, - {0.439756778562500, 0.194376370593750}, - {0.552179912484375, 0.863477370312500}, - {0.243449504812500, 0.737462829328125}, - {0.822881453125000, 0.049457928828125}, - {0.058436791328125, 0.806280808593750}, - {0.267237456671875, 0.347940860375000}, - {0.659100916609375, 0.628228892687500}, - {0.525109796625000, 0.082597554609375}, - {0.135951233515625, 0.201639822421875}, - {0.761255117500000, 0.954583567718750}, - {0.556490320328125, 0.399823343937500}, - {0.258466777984375, 0.838967372437500}, - {0.104074830531250, 0.352849697156250}, - {0.803389216390625, 0.386018192062500}, - {0.392068237890625, 0.666186056234375}, - {0.984505036140625, 0.690144858781250}, - {0.277932351500000, 0.052908284062500}, - {0.713196808109375, 0.833865323640625}, - {0.102915496515625, 0.537034646437500}, - {0.979027962734375, 0.100901043359375}, - {0.106115002812500, 0.885378765484375}, - {0.473302101359375, 0.455953221531250}, - {0.685199429843750, 0.551526284734375}, - {0.706158315421875, 0.128306033953125}, - {0.147265783328125, 0.057294542578125}, - {0.959039534828125, 0.919381743953125}, - {0.646013875390625, 0.398631653203125}, - {0.458517234312500, 0.944548392906250}, - {0.056758487734375, 0.295253452109375}, - {0.988836204046875, 0.303383539265625}, - {0.254309364156250, 0.711985215093750}, - {0.775799306890625, 0.567053766171875}, - {0.340921091031250, 0.224871303765625}, - {0.620506746078125, 0.821560873750000}, - {0.160012464359375, 0.690720392781250}, - {0.788430456281250, 0.036399376265625}, - {0.220355124109375, 0.963720045187500}, - {0.287648582281250, 0.493282790234375}, - {0.536503468843750, 0.724657913296875}, - {0.552148254953125, 0.246577206343750}, - {0.116843560203125, 0.107753416265625}, - {0.842374240812500, 0.843123040078125}, - {0.696912145562500, 0.310820697562500}, - {0.330881907437500, 0.958779769828125}, - {0.169338615078125, 0.306386117906250}, - {0.927058600328125, 0.355660703968750}, - {0.477915496515625, 0.630784646437500}, - {0.947107614062500, 0.571599844062500}, - {0.411298019234375, 0.231101317375000}, - {0.634196201015625, 0.774126137000000}, - {0.133506990359375, 0.617482936484375}, - {0.911148929828125, 0.091845178421875}, - {0.229027962734375, 0.850901043359375}, - {0.433436791328125, 0.431280808593750}, - {0.570900611203125, 0.618903012437500}, - {0.691994851500000, 0.099783284062500}, - {0.212500058671875, 0.147009234203125}, - {0.951732996484375, 0.784567680765625}, - {0.538479240078125, 0.442006235093750}, - {0.495691442609375, 0.839534956375000}, - {0.092295940359375, 0.445600341546875}, - {0.806758487734375, 0.295253452109375}, - {0.374367956281250, 0.505149376265625}, - {0.822584307093750, 0.538276272453125}, - {0.381143881328125, 0.075372076343750}, - {0.746648411546875, 0.886075859718750}, - {0.225690524703125, 0.572657414093750}, - {0.908135803781250, 0.224055233687500}, - {0.100557411375000, 0.856953939312500}, - {0.326749240812500, 0.257185540078125}, - {0.703382877203125, 0.594522854968750}, - {0.635381359671875, 0.247395865796875}, - {0.091503945812500, 0.170573635640625}, - {0.773093560203125, 0.773280760015625}, - {0.699864601359375, 0.424703221531250}, - {0.337978249937500, 0.822576861265625}, - {0.177278602781250, 0.415547689312500}, - {0.950716102781250, 0.382344564312500}, - {0.345102996515625, 0.701097146437500}, - {0.850941098000000, 0.563378403515625}, - {0.447833622000000, 0.045251544750000}, - {0.584417841375000, 0.933427886640625}, - {0.117581880000000, 0.710837083484375}, - {0.864601654468750, 0.093219298265625}, - {0.115674527437500, 0.963752289609375}, - {0.438901999203125, 0.285253217765625}, - {0.641702341031250, 0.693621303765625}, - {0.662939186515625, 0.064765218421875}, - {0.102763510390625, 0.047682223171875}, - {0.821904890625000, 0.958637616328125}, - {0.617697313656250, 0.380208463203125}, - {0.387554934109375, 0.806559289718750}, - {0.096212938656250, 0.263020963203125}, - {0.867422654015625, 0.379107524531250}, - {0.383386910937500, 0.555990102843750}, - {0.880607996843750, 0.519919121093750}, - {0.252252211921875, 0.162698353468750}, - {0.642183234343750, 0.969837245046875}, - {0.196583993796875, 0.685156627843750}, - {0.963190877593750, 0.199420555781250}, - {0.199633261734375, 0.888224135796875}, - {0.350685461906250, 0.434846186453125}, - {0.614399749906250, 0.627404791062500}, - {0.608747165328125, 0.225123234406250}, - {0.244958663437500, 0.019149132359375}, - {0.880021551015625, 0.966470884812500}, - {0.686627065421875, 0.259165408953125}, - {0.463246963312500, 0.880304660312500}, - {0.209424527437500, 0.448127289609375}, - {0.850148582281250, 0.305782790234375}, - {0.321802423796875, 0.647870625703125}, - {0.853921568625000, 0.709558823531250}, - {0.496648411546875, 0.136075859718750}, - {0.534266910125000, 0.759900787234375}, - {0.018208405500000, 0.610922261187500}, - {0.981815968843750, 0.044970413296875}, - {0.147265783328125, 0.807294542578125}, - {0.278764594718750, 0.293912076453125}, - {0.697168203437500, 0.508447093109375}, - {0.569936479140625, 0.090431613031250}, - {0.199633261734375, 0.231974135796875}, - {0.878530229140625, 0.840431613031250}, - {0.530111691484375, 0.313423081343750}, - {0.287503755000000, 0.877340989734375}, - {0.020926812156250, 0.490443845953125}, - {0.925118529859375, 0.463551966546875}, - {0.460281853234375, 0.514086682671875}, - {0.946583993796875, 0.685156627843750}, - {0.432953992203125, 0.011242603312500}, - {0.681996963312500, 0.841242160312500}, - {0.035440756328125, 0.741967535328125}, - {0.826814113265625, 0.129551982203125}, - {0.102763510390625, 0.797682223171875}, - {0.257250986453125, 0.431315283906250}, - {0.601735045687500, 0.535855464812500}, - {0.523947313656250, 0.161458463203125}, - {0.089514399500000, 0.200051700359375}, - {0.998871711468750, 0.969931745984375}, - {0.725557411375000, 0.325703939312500}, - {0.411054041609375, 0.940728892687500}, - {0.092214949156250, 0.395649388656250}, - {0.947571808109375, 0.318240323640625}, - {0.385150528859375, 0.598791693968750}, - {0.760340045031250, 0.666060247875000}, - {0.385468447859375, 0.173477959078125}, - {0.619715387546875, 0.985882883562500}, - {0.110693313734375, 0.604613051578125}, - {0.759083993796875, 0.247656627843750}, - {0.198151308765625, 0.803362716656250}, - {0.368295322046875, 0.475490672062500}, - {0.522431987421875, 0.579676484406250}, - {0.668614601359375, 0.002828221531250}, - {0.159061291453125, 0.115786836312500}, - {0.885343915375000, 0.797979216453125}, - {0.502049350968750, 0.290322923484375}, - {0.361960011265625, 0.916515061968750}, - {0.212257727531250, 0.374306940859375}, - {0.808006746078125, 0.446560873750000}, - {0.489786425109375, 0.696416160921875}, - {0.914775229140625, 0.625807223171875}, - {0.272394756234375, 0.082105300000000}, - {0.697833622000000, 0.795251544750000}, - {0.146614411140625, 0.654378257218750}, - {0.959039534828125, 0.169381743953125}, - {0.056931985968750, 0.888270723078125}, - {0.493765020015625, 0.415613958984375}, - {0.727875617718750, 0.674521589734375}, - {0.646232996843750, 0.172262871093750}, - {0.052267234312500, 0.085173392906250}, - {0.794171695281250, 0.897748994546875}, - {0.664407018781250, 0.458335411421875}, - {0.318273390046875, 0.755820190000000}, - {0.046247165328125, 0.350123234406250}, - {0.865799202015625, 0.333466330906250}, - {0.285498339406250, 0.583855022421875}, - {0.956147102093750, 0.505576277234375}, - {0.318949826718750, 0.141763441546875}, - {0.510429611953125, 0.887119489765625}, - {0.184919978109375, 0.561328326953125}, - {0.759417624109375, 0.096532545187500}, - {0.130403602781250, 0.937032064312500}, - {0.394595719296875, 0.367380713734375}, - {0.567731703031250, 0.747610961484375}, - {0.538716806515625, 0.039836274843750} - }; -}; - -} \ No newline at end of file diff --git a/40_PathTracer/include/renderer/present/CWindowPresenter.h b/40_PathTracer/include/renderer/present/CWindowPresenter.h deleted file mode 100644 index e936e1ae5..000000000 --- a/40_PathTracer/include/renderer/present/CWindowPresenter.h +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright (C) 2025-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 -#ifndef _NBL_THIS_EXAMPLE_C_WINDOW_PRESENTER_H_INCLUDED_ -#define _NBL_THIS_EXAMPLE_C_BASIC_RWMC_RESOLVER_H_INCLUDED_ - - -#include "nbl/ext/FullScreenTriangle/FullScreenTriangle.h" - -#include "renderer/CRenderer.h" -#include "renderer/present/IPresenter.h" - -#include "renderer/shaders/present/push_constants.hlsl" - - -namespace nbl::this_example -{ - -class CWindowPresenter : public IPresenter -{ - public: - using swapchain_resources_t = video::CDefaultSwapchainFramebuffers; - static const video::IGPURenderpass::SCreationParams::SSubpassDependency Dependencies[3]; - - struct SCachedCreationParams - { - core::smart_refctd_ptr winMgr = nullptr; - // for the UI, 1080p with 50% scaling - hlsl::uint16_t2 minResolution = {1248,688}; - }; - struct SCreationParams : IPresenter::SCachedCreationParams, SCachedCreationParams - { - explicit inline operator bool() const {return assMan && winMgr && api && callback;} - - core::smart_refctd_ptr api = {}; - core::smart_refctd_ptr callback = {}; - std::string_view initialWindowCaption = ""; - }; - static core::smart_refctd_ptr create(SCreationParams&& _params); - - // - inline const video::ISurface* getSurface() const {return m_construction.surface->getSurface();} - - // - inline const SCachedCreationParams& getCreationParams() const {return m_creation;} - - // - inline ui::ICursorControl* getCursorControl() const {return m_construction.cursorControl;} - - // - inline const video::IGPURenderpass* getRenderpass() const {return getSwapchainResources()->getRenderpass();} - - // - bool irrecoverable() const {return m_construction.surface->irrecoverable() || !m_construction.surface->isWindowOpen();} - - protected: - using surface_t = video::CSimpleResizeSurface; - struct SCachedConstructionParams - { - core::smart_refctd_ptr surface; - ui::IWindow* window; - ui::ICursorControl* cursorControl; - hlsl::float64_t2 aspectRatioRange; - hlsl::uint16_t2 maxResolution; - }; - struct SConstructorParams : IPresenter::SCachedCreationParams, SCachedCreationParams, SCachedConstructionParams - { - }; - inline CWindowPresenter(SConstructorParams&& _params) : IPresenter(std::move(_params)), m_creation(std::move(_params)), m_construction(std::move(_params)), m_pushConstants({}) {} - // - bool init_impl(CRenderer* renderer) override; - - // - clock_t::time_point acquire_impl(const CSession* session, video::ISemaphore::SWaitInfo* p_currentImageAcquire) override; - bool beginRenderpass_impl() override; - inline bool present(const video::IQueue::SSubmitInfo::SSemaphoreInfo& readyToPresent) override - { - return m_construction.surface->present(m_currentImageIndex,{&readyToPresent,1}); - } - - inline video::ISurface* getSurface() {return m_construction.surface->getSurface();} - - inline swapchain_resources_t* getSwapchainResources() {return static_cast(m_construction.surface->getSwapchainResources());} - inline const swapchain_resources_t* getSwapchainResources() const {return static_cast(m_construction.surface->getSwapchainResources());} - - SCachedCreationParams m_creation; - SCachedConstructionParams m_construction; - core::smart_refctd_ptr m_present; - SDefaultResolvePushConstants m_pushConstants; - uint8_t m_currentImageIndex = ~0u; -}; - -} -#endif diff --git a/40_PathTracer/include/renderer/present/IPresenter.h b/40_PathTracer/include/renderer/present/IPresenter.h deleted file mode 100644 index 761d5e9a1..000000000 --- a/40_PathTracer/include/renderer/present/IPresenter.h +++ /dev/null @@ -1,205 +0,0 @@ -// Copyright (C) 2025-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 -#ifndef _NBL_THIS_EXAMPLE_I_PRESENTER_H_INCLUDED_ -#define _NBL_THIS_EXAMPLE_I_PRESENTER_H_INCLUDED_ - - -#include "renderer/CScene.h" -#include "renderer/CSession.h" - -#include "renderer/shaders/pathtrace/push_constants.hlsl" - - -namespace nbl::this_example -{ - -class IPresenter : public core::IReferenceCounted, public core::InterfaceUnmovable -{ - public: - constexpr static inline uint8_t CircularBufferSize = 4; - - struct SCachedCreationParams - { - core::smart_refctd_ptr assMan = nullptr; - system::logger_opt_smart_ptr logger = nullptr; - }; - // - inline const SCachedCreationParams& getCreationParams() const {return m_creation;} - - // - inline bool init(CRenderer* renderer) - { - if (m_queue) - return isInitialized(); - - auto& logger = m_creation.logger; - auto* device = renderer->getDevice(); - m_queue = renderer->getCreationParams().graphicsQueue; - - bool success = false; - auto deinit = core::makeRAIIExiter([&]()->void{ - if (success) - return; - m_semaphore = nullptr; - std::fill(m_cmdbufs.begin(),m_cmdbufs.end(),nullptr); - }); - - using namespace nbl::system; - if (!(m_semaphore=device->createSemaphore(m_presentCount))) - { - logger.log("`IPresenter::init` failed to create a semaphore!",ILogger::ELL_ERROR); - return false; - } - - for (auto& cmdbuf : m_cmdbufs) - { - using namespace nbl::video; - auto pool=device->createCommandPool(m_queue->getFamilyIndex(),IGPUCommandPool::CREATE_FLAGS::TRANSIENT_BIT); - if (!pool || !pool->createCommandBuffers(IGPUCommandPool::BUFFER_LEVEL::PRIMARY,{&cmdbuf,1},core::smart_refctd_ptr(logger.get()))) - { - logger.log("`IPresenter::init` failed to create Command Buffer!",ILogger::ELL_ERROR); - return false; - } - } - - return success = init_impl(renderer); - } - inline bool isInitialized() const {return bool(m_semaphore);} - - // - inline video::IQueue* getQueue() const {return m_queue;} - // - inline video::ILogicalDevice* getDevice() const {return const_cast(m_semaphore->getOriginDevice());} - - // - virtual bool irrecoverable() const {return false;} - - // returns expected presentation time for frame pacing - using clock_t = std::chrono::steady_clock; - inline clock_t::time_point acquire(const CSession* background) - { - auto expectedPresent = clock_t::time_point::min(); // invalid value - m_currentImageAcquire = {}; - if (!background) - { - m_currentSessionDS = nullptr; - return expectedPresent; - } - m_currentSessionDS = background->getActiveResources().immutables.ds; - return acquire_impl(background,&m_currentImageAcquire); - } - - // - inline video::IGPUCommandBuffer* beginRenderpass() - { - if (!isInitialized() || !m_currentImageAcquire.semaphore) - return nullptr; - - using namespace nbl::video; - if (m_presentCount>=CircularBufferSize) - { - const ISemaphore::SWaitInfo cbDonePending[] = - { - { - .semaphore = m_semaphore.get(), - .value = m_presentCount+1-CircularBufferSize - } - }; - if (getDevice()->blockForSemaphores(cbDonePending) != ISemaphore::WAIT_RESULT::SUCCESS) - return {}; - } - - auto* const cb = getCurrentCmdBuffer(); - cb->getPool()->reset(); - if (!cb->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT)) - return nullptr; - - if (!beginRenderpass_impl()) - { - cb->end(); - return nullptr; - } - return cb; - } - - // - inline bool endRenderpassAndPresent(const video::IQueue::SSubmitInfo::SSemaphoreInfo& extraSubmitWait) - { - using namespace nbl::asset; - using namespace nbl::video; - auto* const cb = getCurrentCmdBuffer(); - if (cb->getState()!=IGPUCommandBuffer::STATE::RECORDING) - return false; - - if (!endRenderpass() || !cb->end()) - return false; - - const IQueue::SSubmitInfo::SSemaphoreInfo rendered[] = - { - { - .semaphore = m_semaphore.get(), - .value = ++m_presentCount, - .stageMask = PIPELINE_STAGE_FLAGS::COLOR_ATTACHMENT_OUTPUT_BIT - } - }; - const IQueue::SSubmitInfo::SCommandBufferInfo commandBuffers[] = - { - {.cmdbuf = cb} - }; - const IQueue::SSubmitInfo::SSemaphoreInfo wait[] = - { - { - .semaphore = const_cast(m_currentImageAcquire.semaphore), - .value = m_currentImageAcquire.value, - // the subpass from-external dependency has some bits, so seems I don't need anything here to sync with the timeline sema signalled by acquire - .stageMask = PIPELINE_STAGE_FLAGS::NONE - }, - extraSubmitWait - }; - IQueue::SSubmitInfo infos[] = - { - { - .waitSemaphores = wait, - .commandBuffers = commandBuffers, - .signalSemaphores = rendered - } - }; - if (!extraSubmitWait.semaphore) - infos->waitSemaphores = {wait,1}; - - if (m_queue->submit(infos)!=IQueue::RESULT::SUCCESS) - { - m_presentCount--; - return false; - } - return present(*rendered); - } - - protected: - inline IPresenter(SCachedCreationParams&& _params) : m_creation(std::move(_params)) {} - virtual bool init_impl(CRenderer* renderer) = 0; - - virtual clock_t::time_point acquire_impl(const CSession* background, video::ISemaphore::SWaitInfo* p_currentImageAcquire) = 0; - virtual bool beginRenderpass_impl() = 0; - virtual bool endRenderpass() - { - return getCurrentCmdBuffer()->endRenderPass(); - } - virtual bool present(const video::IQueue::SSubmitInfo::SSemaphoreInfo& readyToPresent) = 0; - - inline video::IGPUDescriptorSet* getCurrentSessionDS() const {return m_currentSessionDS.get();} - inline video::IGPUCommandBuffer* getCurrentCmdBuffer() const {return m_cmdbufs[m_presentCount % CircularBufferSize].get();} - - private: - SCachedCreationParams m_creation; - video::CThreadSafeQueueAdapter* m_queue = nullptr; - core::smart_refctd_ptr m_semaphore = {}; - std::array, CircularBufferSize> m_cmdbufs = {}; - video::ISemaphore::SWaitInfo m_currentImageAcquire = {}; - core::smart_refctd_ptr m_currentSessionDS = {}; - uint64_t m_presentCount = 0; -}; - -} -#endif diff --git a/40_PathTracer/include/renderer/resolve/CBasicRWMCResolver.h b/40_PathTracer/include/renderer/resolve/CBasicRWMCResolver.h deleted file mode 100644 index 339e7cb7c..000000000 --- a/40_PathTracer/include/renderer/resolve/CBasicRWMCResolver.h +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright (C) 2025-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 -#ifndef _NBL_THIS_EXAMPLE_C_BASIC_RWMC_RESOLVER_H_INCLUDED_ -#define _NBL_THIS_EXAMPLE_C_BASIC_RWMC_RESOLVER_H_INCLUDED_ - - -#include "renderer/CRenderer.h" -#include "renderer/resolve/IResolver.h" -#include "renderer/shaders/resolve/rwmc.hlsl" - - -namespace nbl::this_example -{ - -class CBasicRWMCResolver : public IResolver -{ - public: - enum class AutoExposure : uint8_t - { - GeometricAverage, - Median, - Count - }; - enum class Tonemapping : uint8_t - { - Reinhard, - ACES, - Count - }; - - // - struct SCachedCreationParams - { - }; - struct SCreationParams : SCachedCreationParams - { - inline operator bool() const {return renderer;} - - CRenderer* renderer; - }; - static core::smart_refctd_ptr create(SCreationParams&& _params); - - // - inline const SCachedCreationParams& getCreationParams() const { return m_creation; } - - struct SCachedConstructionParams - { - core::smart_refctd_ptr layout; - // TODO: autoexposure - core::smart_refctd_ptr lumaMeasure; - // TODO: motion vector stuff - // rwmc resolve, apply exposure, interleave into OptiX input formats - core::smart_refctd_ptr rwmcResolve; - // TODO: OIDN denoise - // deinterlave from OptiX output format, perform first axis of FFT - core::smart_refctd_ptr postDenoise; // TODO - // second axis FFT, spectrum multiply and iFFT - core::smart_refctd_ptr secondAxisBloom; // TODO - // first axis iFFT, tonemap, encode into final EXR format - core::smart_refctd_ptr secondAxisFFTTonemap; // TODO - // - core::smart_refctd_ptr persistentExposureArgs; - // - core::smart_refctd_ptr bloomKernelSpectrum; - }; - // - inline const SCachedConstructionParams& getConstructionParams() const {return m_construction;} - - // - inline uint64_t computeScratchSize(const CSession* session) const override - { - if (!session) - return 0ull; - switch (session->getConstructionParams().mode) - { - case CSession::RenderMode::Previs: [[fallthrough]]; - case CSession::RenderMode::Debug: - return 0ull; - case CSession::RenderMode::Beauty: - return 0ull; // for now, as long as we blit - default: - break; - } - assert(false); // unimplemented - return ~0ull; - } - // - bool resolve(video::IGPUCommandBuffer* cb, video::IGPUBuffer* scratch) override; - - protected: - struct SConstructorParams : SCachedCreationParams, SCachedConstructionParams - { - }; - inline CBasicRWMCResolver(SConstructorParams&& _params) : m_creation(std::move(_params)), m_construction(std::move(_params)) {} - - bool changeSession_impl() override; - - SCachedCreationParams m_creation; - SCachedConstructionParams m_construction; -}; - -} -#endif diff --git a/40_PathTracer/include/renderer/resolve/IResolver.h b/40_PathTracer/include/renderer/resolve/IResolver.h deleted file mode 100644 index 74e708edf..000000000 --- a/40_PathTracer/include/renderer/resolve/IResolver.h +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (C) 2025-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 -#ifndef _NBL_THIS_EXAMPLE_I_RESOLVER_H_INCLUDED_ -#define _NBL_THIS_EXAMPLE_I_RESOLVER_H_INCLUDED_ - - -#include "renderer/CSession.h" - - -namespace nbl::this_example -{ - -class IResolver : public core::IReferenceCounted, public core::InterfaceUnmovable -{ - public: - // - inline CSession* getActiveSession() {return m_activeSession.get();} - inline const CSession* getActiveSession() const {return m_activeSession.get();} - - // - virtual uint64_t computeScratchSize(const CSession* session) const = 0; - inline uint64_t computeScratchSize() const {return computeScratchSize(m_activeSession.get());} - - // - inline bool changeSession(core::smart_refctd_ptr&& session) - { - m_activeSession = std::move(session); - if (!m_activeSession || !m_activeSession->isInitialized() || !changeSession_impl()) - { - m_activeSession = {}; - return false; - } - return true; - } - - // - virtual bool resolve(video::IGPUCommandBuffer* cv, video::IGPUBuffer* scratch) = 0; - - protected: - virtual bool changeSession_impl() = 0; - - core::smart_refctd_ptr m_activeSession; -}; - -} -#endif diff --git a/40_PathTracer/include/renderer/shaders/common.hlsl b/40_PathTracer/include/renderer/shaders/common.hlsl deleted file mode 100644 index e086f19fc..000000000 --- a/40_PathTracer/include/renderer/shaders/common.hlsl +++ /dev/null @@ -1,79 +0,0 @@ -#ifndef _NBL_THIS_EXAMPLE_COMMON_HLSL_INCLUDED_ -#define _NBL_THIS_EXAMPLE_COMMON_HLSL_INCLUDED_ - - -#include "nbl/builtin/hlsl/cpp_compat.hlsl" -#include "nbl/builtin/hlsl/concepts/accessors/loadable_image.hlsl" -#include "nbl/builtin/hlsl/concepts/accessors/storable_image.hlsl" -// -#include "nbl/builtin/hlsl/math/linalg/fast_affine.hlsl" -// -#include "nbl/builtin/hlsl/path_tracing/gaussian_filter.hlsl" - - -// TODO: move to type_traits? -namespace nbl -{ -namespace hlsl -{ -#ifdef __HLSL_VERSION -template -struct texture_traits; - -template -struct texture_traits > > -{ - NBL_CONSTEXPR_STATIC_INLINE int32_t Dimension = 2; - using coded_type = vector; -}; -//special case -template NBL_PARTIAL_REQ_TOP(is_scalar_v) -struct texture_traits NBL_PARTIAL_REQ_BOT(is_scalar_v)> -{ - NBL_CONSTEXPR_STATIC_INLINE int32_t Dimension = 1; - using coded_type = vector; -}; - -template -struct texture_traits > > -{ - NBL_CONSTEXPR_STATIC_INLINE int32_t Dimension = 2; - using coded_type = vector; -}; -//special case -template NBL_PARTIAL_REQ_TOP(is_scalar_v) -struct texture_traits NBL_PARTIAL_REQ_BOT(is_scalar_v)> -{ - NBL_CONSTEXPR_STATIC_INLINE int32_t Dimension = 1; - using coded_type = vector; -}; -#endif -} -} - -// -#define DEFINE_TEXTURE_ACCESSOR(TEX_NAME) struct ImageAccessor_ ## TEX_NAME \ -{ \ - using texture_traits_t = ::nbl::hlsl::texture_traits; \ - NBL_CONSTEXPR_STATIC_INLINE uint32_t Dimension = texture_traits_t::Dimension; \ - using coded_type = typename texture_traits_t::coded_type; \ - using coded_type_traits = ::nbl::hlsl::vector_traits; \ - using scalar_type = typename coded_type_traits::scalar_type; \ - NBL_CONSTEXPR_STATIC_INLINE uint32_t Components = coded_type_traits::Dimension; \ -\ - template) value, const vector coord, const uint16_t layer) \ - { \ - value = TEX_NAME[vector(coord,layer)]; \ - } \ - template coord, const uint16_t layer, const vector value) \ - { \ - TEX_NAME[vector(coord,layer)] = value; \ - } \ -} - -// -#define MAX_PATH_DEPTH_LOG2 8 - -#endif // _NBL_THIS_EXAMPLE_COMMON_HLSL_INCLUDED_ diff --git a/40_PathTracer/include/renderer/shaders/pathtrace/common.hlsl b/40_PathTracer/include/renderer/shaders/pathtrace/common.hlsl deleted file mode 100644 index 6b85d534a..000000000 --- a/40_PathTracer/include/renderer/shaders/pathtrace/common.hlsl +++ /dev/null @@ -1,23 +0,0 @@ -#ifndef _NBL_THIS_EXAMPLE_PATHTRACE_COMMON_HLSL_INCLUDED_ -#define _NBL_THIS_EXAMPLE_PATHTRACE_COMMON_HLSL_INCLUDED_ - - -#include "nbl/builtin/hlsl/random/xoroshiro.hlsl" - -#include "nbl/builtin/hlsl/glsl_compat/core.hlsl" -#include "nbl/builtin/hlsl/spirv_intrinsics/raytracing.hlsl" - -namespace nbl -{ -namespace this_example -{ -NBL_CONSTEXPR_INLINE_NSPC_SCOPE_VAR uint32_t SceneDSIndex = 0; -NBL_CONSTEXPR_INLINE_NSPC_SCOPE_VAR uint32_t SessionDSIndex = 1; -} -} -#include "renderer/shaders/scene.hlsl" -#include "renderer/shaders/session.hlsl" -#include "renderer/shaders/pathtrace/push_constants.hlsl" - - -#endif // _NBL_THIS_EXAMPLE_PATHTRACE_COMMON_HLSL_INCLUDED_ diff --git a/40_PathTracer/include/renderer/shaders/pathtrace/push_constants.hlsl b/40_PathTracer/include/renderer/shaders/pathtrace/push_constants.hlsl deleted file mode 100644 index 1b6d130dd..000000000 --- a/40_PathTracer/include/renderer/shaders/pathtrace/push_constants.hlsl +++ /dev/null @@ -1,85 +0,0 @@ -#ifndef _NBL_THIS_EXAMPLE_PATHTRACE_PUSH_CONSTANTS_HLSL_INCLUDED_ -#define _NBL_THIS_EXAMPLE_PATHTRACE_PUSH_CONSTANTS_HLSL_INCLUDED_ - - -#include "renderer/shaders/session.hlsl" - -#include - - -// no uint16_t to be used because its going to be a push constant -namespace nbl -{ -namespace this_example -{ - -#define MAX_SPP_LOG2 15 -NBL_CONSTEXPR_STATIC_INLINE uint16_t MaxSPPLog2 = MAX_SPP_LOG2; -// need to be able to count (represent) both 0 and Max, but the count needs to be PoT because Sobol Sequence -NBL_CONSTEXPR_STATIC_INLINE uint32_t MaxSPP = 0x1u << (MaxSPPLog2 - 1); -struct SSensorDynamics -{ - // assuming input will be ndc = [-1,1]^2 x {-1} - hlsl::float32_t3x4 invView; - hlsl::float32_t2x3 ndcToRay; - hlsl::float32_t nearClip; - hlsl::float32_t tMax; - // we can adaptively sample per-pixel, but some bounds need to be kept - uint32_t minSPP : MAX_SPP_LOG2; - uint32_t maxSPP : MAX_SPP_LOG2; - uint32_t unused : 1; - uint32_t keepAccumulating : 1; -}; -#undef MAX_SPP_LOG2 - -struct SPrevisPushConstants -{ - SSensorDynamics sensorDynamics; -}; - -// We do it so weirdly because https://github.com/microsoft/DirectXShaderCompiler/issues/7131 -#define MAX_SPP_PER_DISPATCH_LOG2 5 -struct SBeautyPushConstants -{ - NBL_CONSTEXPR_STATIC_INLINE uint32_t MaxSppPerDispatchLog2 = MAX_SPP_PER_DISPATCH_LOG2; - - // PushConstant16bit access feature isn't ubiquitous - struct S16BitData - { - // Luma conversion coefficients scaled by something proportional to the brightest light in the scene -#ifndef __HLSL_VERSION - hlsl:: -#endif - float16_t3 rrThroughputWeights; - // For a foveated render - uint16_t maxSppPerDispatch; - }; - - - SSensorDynamics sensorDynamics; -#ifdef __HLSL_VERSION - uint32_t2 __16BitData; - static_assert(sizeof(uint32_t2)==sizeof(S16BitData)); - // - S16BitData get16BitData() - { - S16BitData retval; - retval.rrThroughputWeights = hlsl::bit_cast(__16BitData).xyz; - retval.maxSppPerDispatch = hlsl::bit_cast(__16BitData).w; - return retval; - } -#else - S16BitData __16BitData; -#endif -}; -#undef MAX_SPP_PER_DISPATCH_LOG2 - -struct SDebugPushConstants -{ - SSensorDynamics sensorDynamics; - // some enum/choice of what to debug -}; - -} -} -#endif // _NBL_THIS_EXAMPLE_PATHTRACE_PUSH_CONSTANTS_HLSL_INCLUDED_ diff --git a/40_PathTracer/include/renderer/shaders/present/push_constants.hlsl b/40_PathTracer/include/renderer/shaders/present/push_constants.hlsl deleted file mode 100644 index 6ffcd74f0..000000000 --- a/40_PathTracer/include/renderer/shaders/present/push_constants.hlsl +++ /dev/null @@ -1,60 +0,0 @@ -#ifndef _NBL_THIS_EXAMPLE_PRESENT_PUSH_CONSTANTS_HLSL_INCLUDED_ -#define _NBL_THIS_EXAMPLE_PRESENT_PUSH_CONSTANTS_HLSL_INCLUDED_ - - -#include "renderer/shaders/resolve/rwmc.hlsl" - - -// no uint16_t to be used because its going to be a push constant -namespace nbl -{ -namespace this_example -{ -using namespace nbl::hlsl; - -struct SDefaultResolvePushConstants -{ - NBL_CONSTEXPR_STATIC_INLINE uint32_t ImageCount = 16; - - struct Regular - { - // if more than 1.f - float32_t2 scale; - // to visualize what will get cropped out - float32_t2 _min,_max; - }; - struct Cubemap - { - // theoretically we only need inverse of product of 3x3 view with very sparse 4x4 - float32_t4x4 invProjView; - }; -#ifndef __HLSL_VERSION - union - { - Regular regular; - Cubemap cubemap; - }; -#else - // note how this is a conversion to a copy, and not handing out of a reference - // Ergo, its not a true "union" - inline Regular regular() - { - Regular retval; - retval.scale = __union.invProjView[0].xy; - retval._min = __union.invProjView[0].zw; - retval._max = __union.invProjView[1].xy; - return retval; - } - inline Cubemap cubemap() {return __union;} - - Cubemap __union; -#endif - // 3 extra bits for cube layer - uint32_t isCubemap : 1; - uint32_t layer : MAX_CASCADE_COUNT_LOG2; - uint32_t imageIndex : BOOST_PP_SUB(31,MAX_CASCADE_COUNT_LOG2); -}; - -} -} -#endif // _NBL_THIS_EXAMPLE_PRESENT_PUSH_CONSTANTS_HLSL_INCLUDED_ diff --git a/40_PathTracer/include/renderer/shaders/resolve/rwmc.hlsl b/40_PathTracer/include/renderer/shaders/resolve/rwmc.hlsl deleted file mode 100644 index 692a6fb16..000000000 --- a/40_PathTracer/include/renderer/shaders/resolve/rwmc.hlsl +++ /dev/null @@ -1,35 +0,0 @@ -#ifndef _NBL_THIS_EXAMPLE_RWMC_HLSL_INCLUDED_ -#define _NBL_THIS_EXAMPLE_RWMC_HLSL_INCLUDED_ - - -#include "renderer/shaders/common.hlsl" -#include "nbl/builtin/hlsl/rwmc/SplattingParameters.hlsl" -#include "nbl/builtin/hlsl/rwmc/ResolveParameters.hlsl" - -#include -#include - -namespace nbl -{ -namespace this_example -{ -// We do it so weirdly because https://github.com/microsoft/DirectXShaderCompiler/issues/7131 -#define MAX_CASCADE_COUNT_LOG2 3 - -// no uint16_t to be used because its going to be a push constant -struct SResolveConstants // TODO: move somewhere -{ - struct SProtoRWMC - { - hlsl::float32_t initialEmin; - hlsl::float32_t reciprocalBase; - hlsl::float32_t reciprocalKappa; - hlsl::float32_t colorReliabilityFactor; - } rwmc; - uint64_t cascadeCount : BOOST_PP_ADD(MAX_CASCADE_COUNT_LOG2,1); - uint64_t scratchBDA : BOOST_PP_SUB(63,MAX_CASCADE_COUNT_LOG2); -}; - -} -} -#endif // _NBL_THIS_EXAMPLE_RWMC_HLSL_INCLUDED_ diff --git a/40_PathTracer/include/renderer/shaders/scene.hlsl b/40_PathTracer/include/renderer/shaders/scene.hlsl deleted file mode 100644 index 130d55ad8..000000000 --- a/40_PathTracer/include/renderer/shaders/scene.hlsl +++ /dev/null @@ -1,74 +0,0 @@ -#ifndef _NBL_THIS_EXAMPLE_SCENE_HLSL_INCLUDED_ -#define _NBL_THIS_EXAMPLE_SCENE_HLSL_INCLUDED_ - - -#include "renderer/shaders/common.hlsl" - -namespace nbl -{ -namespace this_example -{ -struct SSceneUniforms -{ - struct SInit - { - NBL_CONSTEXPR_STATIC_INLINE uint16_t MaxPathDepthLog2 = MAX_PATH_DEPTH_LOG2; - - // - uint64_t pSampleSequence; - uint16_t sequenceSamplesLog2 : 5; // TODO: make this compile time constant - Spec Constant? - uint16_t lastSequencePathDepth : 11; // TODO: what do we even need this for ? Also coult be a spec constant - static_assert(MaxPathDepthLog2<=11); - uint16_t unused1[3]; - //hlsl::float16_t envmapScale; - // TODO: later when we can save the envmap values to a buffer - // because the PDF is rescaled to log2(luma)/log2(Max)*255 - // and you get it out as `exp2(texValue)*factor` - //hlsl::float32_t envmapPDFNormalizationFactor; - } init; -}; - -struct SceneDSBindings -{ - NBL_CONSTEXPR_STATIC_INLINE uint32_t UBO = 0; - // RGB9E5 post multiplied by a max value - NBL_CONSTEXPR_STATIC_INLINE uint32_t Envmap = 1; - NBL_CONSTEXPR_STATIC_INLINE uint32_t TLASes = 2; - NBL_CONSTEXPR_STATIC_INLINE uint32_t Samplers = 3; - NBL_CONSTEXPR_STATIC_INLINE uint32_t SampledImages = 4; - // UINT8 log2(luma) meant for stochastic descent or querying the PDF of the Warp Map - NBL_CONSTEXPR_STATIC_INLINE uint32_t EnvmapPDF = 5; - // R16G16_UNORM or R32G32_SFLOAT (depending on envmap resolution) meant for skipping stochastic descent - NBL_CONSTEXPR_STATIC_INLINE uint32_t EnvmapWarpMap = 6; -}; - -struct SceneDSBindingCounts -{ - // Mostly held back by Intel ARC, important to not have more than this many light geometries, can increase to - // https://vulkan.gpuinfo.org/displayextensionproperty.php?extensionname=VK_KHR_acceleration_structure&extensionproperty=maxDescriptorSetUpdateAfterBindAccelerationStructures&platform=all - // https://vulkan.gpuinfo.org/displayextensionproperty.php?extensionname=VK_KHR_acceleration_structure&extensionproperty=maxPerStageDescriptorUpdateAfterBindAccelerationStructures&platform=all - NBL_CONSTEXPR_STATIC_INLINE uint32_t TLASes = 65535; - // Reasonable combo (esp if we implement a cache over the DS) - NBL_CONSTEXPR_STATIC_INLINE uint32_t Samplers = 128; - // Spec mandated minimum - NBL_CONSTEXPR_STATIC_INLINE uint32_t SampledImages = 500000; -}; - -#ifdef __HLSL_VERSION -[[vk::binding(SceneDSBindings::UBO,SceneDSIndex)]] ConstantBuffer gScene; -// could be float32_t3 -[[vk::binding(SceneDSBindings::Envmap,SceneDSIndex)]] [[vk::combinedImageSampler]] Texture2D gEnvmap; -[[vk::binding(SceneDSBindings::Envmap,SceneDSIndex)]] [[vk::combinedImageSampler]] SamplerState gEnvmapSampler; -[[vk::binding(SceneDSBindings::TLASes,SceneDSIndex)]] RaytracingAccelerationStructure gTLASes[SceneDSBindingCounts::TLASes]; -[[vk::binding(SceneDSBindings::Samplers,SceneDSIndex)]] SamplerState gSamplers[SceneDSBindingCounts::Samplers]; -[[vk::binding(SceneDSBindings::SampledImages,SceneDSIndex)]] Texture2DArray gSampledImages[SceneDSBindingCounts::SampledImages]; -// could be float32_t -[[vk::binding(SceneDSBindings::EnvmapPDF,SceneDSIndex)]] [[vk::combinedImageSampler]] Texture2D gEnvmapPDF; -[[vk::binding(SceneDSBindings::EnvmapPDF,SceneDSIndex)]] [[vk::combinedImageSampler]] SamplerState gEnvmapPDFSampler; -// could be float32_t2 -[[vk::binding(SceneDSBindings::EnvmapWarpMap,SceneDSIndex)]] [[vk::combinedImageSampler]] Texture2D gEnvmapWarpMap; -[[vk::binding(SceneDSBindings::EnvmapWarpMap,SceneDSIndex)]] [[vk::combinedImageSampler]] SamplerState gEnvmapWarpMapSampler; -#endif -} -} -#endif // _NBL_THIS_EXAMPLE_SCENE_HLSL_INCLUDED_ diff --git a/40_PathTracer/include/renderer/shaders/session.hlsl b/40_PathTracer/include/renderer/shaders/session.hlsl deleted file mode 100644 index 439f17e6c..000000000 --- a/40_PathTracer/include/renderer/shaders/session.hlsl +++ /dev/null @@ -1,110 +0,0 @@ -#ifndef _NBL_THIS_EXAMPLE_SESSION_HLSL_INCLUDED_ -#define _NBL_THIS_EXAMPLE_SESSION_HLSL_INCLUDED_ - - -#include "renderer/shaders/common.hlsl" -#include "renderer/shaders/resolve/rwmc.hlsl" - - -namespace nbl -{ -namespace this_example -{ -// [0].xyz for subpixel jitter and stochastic opacity anyhit, [1].xy for DoF and motion Blur -NBL_CONSTEXPR_INLINE_NSPC_SCOPE_VAR uint16_t PrimaryRayRandTripletsUsed = 2; -// [0].xyz for BRDF Lobe sampling, then reuse [0].z for Russian Roulette, [1].xyz for BTDF Lobe sampling and [1].z for RIS lobe resampling, [2].xyz for NEE -NBL_CONSTEXPR_INLINE_NSPC_SCOPE_VAR uint16_t RandDimTriplesPerDepth = 3; - -struct SSensorUniforms -{ - NBL_CONSTEXPR_STATIC_INLINE uint16_t ScrambleKeyTextureSize = 512; - NBL_CONSTEXPR_STATIC_INLINE uint16_t MaxCascadeCountLog2 = MAX_CASCADE_COUNT_LOG2; - - hlsl::float32_t2 rcpPixelSize; - hlsl::rwmc::SPackedSplattingParameters splatting; - hlsl::uint16_t2 renderSize; - // bitfield - uint16_t lastPathDepth : MAX_PATH_DEPTH_LOG2; - uint16_t lastNoRussianRouletteDepth : MAX_PATH_DEPTH_LOG2; - uint16_t lastCascadeIndex : MAX_CASCADE_COUNT_LOG2; - uint16_t unused0 : 12; //BOOST_PP_SUB(15, BOOST_PP_ADD(BOOST_PP_MUL(MAX_PATH_DEPTH_LOG2, 2), MAX_CASCADE_COUNT_LOG2)); - uint16_t hideEnvironment : 1; -}; - -struct SensorDSBindings -{ - NBL_CONSTEXPR_STATIC_INLINE uint32_t UBO = 0; - // R32G32_UINT storage texture (can get animated/rearranged) - NBL_CONSTEXPR_STATIC_INLINE uint32_t ScrambleKey = 1; - // R16_UINT Per Pixel Sample Count (so don't need to read all RWMC cascades) - NBL_CONSTEXPR_STATIC_INLINE uint32_t SampleCount = 2; - // R64_UINT with packing RGB14E6 or RGB14E7 and using rest for spp in the cascade - NBL_CONSTEXPR_STATIC_INLINE uint32_t RWMCCascades = 3; - // RGB5E9 - NBL_CONSTEXPR_STATIC_INLINE uint32_t Beauty = 4; - // R10G10B10_UNORM - NBL_CONSTEXPR_STATIC_INLINE uint32_t Albedo = 5; - // modified R10G10B10_UNORM - NBL_CONSTEXPR_STATIC_INLINE uint32_t Normal = 6; - // modified R10G10B10_UNORM - NBL_CONSTEXPR_STATIC_INLINE uint32_t Motion = 7; - // R16_UNORM - NBL_CONSTEXPR_STATIC_INLINE uint32_t Mask = 8; - // - NBL_CONSTEXPR_STATIC_INLINE uint32_t Samplers = 9; - // - NBL_CONSTEXPR_STATIC_INLINE uint32_t AsSampledImages = 10; - - enum class SampledImageIndex : uint16_t - { - ScrambleKey = ScrambleKey-ScrambleKey, - SampleCount = SampleCount-ScrambleKey, - RWMCCascades = RWMCCascades-ScrambleKey, - Beauty = Beauty-ScrambleKey, - Albedo = Albedo-ScrambleKey, - Normal = Normal-ScrambleKey, - Motion = Motion-ScrambleKey, - Mask = Mask-ScrambleKey, - Count - }; -}; - -struct SensorDSBindingCounts -{ - // - NBL_CONSTEXPR_STATIC_INLINE uint32_t Samplers = 1; - NBL_CONSTEXPR_STATIC_INLINE uint32_t AsSampledImages = SensorDSBindings::Samplers-SensorDSBindings::ScrambleKey; -}; - - -#ifdef __HLSL_VERSION -[[vk::binding(SensorDSBindings::UBO,SessionDSIndex)]] ConstantBuffer gSensor; -[[vk::binding(SensorDSBindings::ScrambleKey,SessionDSIndex)]] RWTexture2DArray gScrambleKey; -// could be uint16_t were it not for "Expected Sampled Type to be a 32-bit int, 64-bit int or 32-bit float scalar type for Vulkan environment" -[[vk::binding(SensorDSBindings::SampleCount,SessionDSIndex)]] RWTexture2DArray gSampleCount; -[[vk::binding(SensorDSBindings::RWMCCascades,SessionDSIndex)]] RWTexture2DArray gRWMCCascades; -[[vk::binding(SensorDSBindings::Beauty,SessionDSIndex)]] RWTexture2DArray gBeauty; -[[vk::binding(SensorDSBindings::Albedo,SessionDSIndex)]] RWTexture2DArray gAlbedo; -// thse two are snorm but stored as unorm, care needs to be taken to map: -// [-1,1] <-> [0,1] but with 0 being exactly representable, so really [-1,1] <-> [1/1023,1] -// Requires x*1022.f/2046.f+1024.f/2046.f shift/adjust for accumulation and storage -// Then to decode back into [-1,1] need max(y*2046.f/1022.f-1024.f/1022.f,-1) = x -[[vk::binding(SensorDSBindings::Normal,SessionDSIndex)]] RWTexture2DArray gNormal; -// TODO: motion confidence mask -[[vk::binding(SensorDSBindings::Motion,SessionDSIndex)]] RWTexture2DArray gMotion; -[[vk::binding(SensorDSBindings::Mask,SessionDSIndex)]] RWTexture2DArray gMask; -// -[[vk::binding(SensorDSBindings::Samplers,SessionDSIndex)]] SamplerState gSensorSamplers[SensorDSBindingCounts::Samplers]; -// -[[vk::binding(SensorDSBindings::AsSampledImages,SessionDSIndex)]] Texture2DArray gSensorTextures[SensorDSBindingCounts::AsSampledImages]; - -// For our generic accumulators -// RWMC cascades and Beauty need special treatment -DEFINE_TEXTURE_ACCESSOR(gAlbedo); -DEFINE_TEXTURE_ACCESSOR(gNormal); -DEFINE_TEXTURE_ACCESSOR(gMotion); -DEFINE_TEXTURE_ACCESSOR(gMask); -#endif -} -} -#endif // _NBL_THIS_EXAMPLE_SESSION_HLSL_INCLUDED_ diff --git a/40_PathTracer/main.cpp b/40_PathTracer/main.cpp deleted file mode 100644 index 9d4cb6b65..000000000 --- a/40_PathTracer/main.cpp +++ /dev/null @@ -1,606 +0,0 @@ -// Copyright (C) 2025-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 "nbl/examples/common/BuiltinResourcesApplication.hpp" - -#include "nbl/examples/examples.hpp" - -#include "renderer/CRenderer.h" -#include "renderer/resolve/CBasicRWMCResolver.h" -#include "renderer/present/CWindowPresenter.h" - -#include "nlohmann/json.hpp" - - -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::application_templates; -using namespace nbl::examples; -using namespace nbl::this_example; - -// TODO: move to argument parsing class -struct AppArguments -{ - bool headless = false; -}; - - -class PathTracingApp final : public SimpleWindowedApplication, public BuiltinResourcesApplication -{ - using device_base_t = SimpleWindowedApplication; - using asset_base_t = BuiltinResourcesApplication; - - // TODO: move to Nabla proper - static inline void jsonizeGitInfo(nlohmann::json& target, const nbl::gtml::GitInfo& info) - { - target["isPopulated"] = info.isPopulated; - if (info.hasUncommittedChanges.has_value()) - target["hasUncommittedChanges"] = info.hasUncommittedChanges.value(); - else - target["hasUncommittedChanges"] = "UNKNOWN, BUILT WITHOUT DIRTY-CHANGES CAPTURE"; - - target["commitAuthorName"] = info.commitAuthorName; - target["commitAuthorEmail"] = info.commitAuthorEmail; - target["commitHash"] = info.commitHash; - target["commitShortHash"] = info.commitShortHash; - target["commitDate"] = info.commitDate; - target["commitSubject"] = info.commitSubject; - target["commitBody"] = info.commitBody; - target["describe"] = info.describe; - target["branchName"] = info.branchName; - target["latestTag"] = info.latestTag; - target["latestTagName"] = info.latestTagName; - } - - inline void printGitInfos() const - { - nlohmann::json j; - - auto& modules = j["modules"]; - jsonizeGitInfo(modules["nabla"],nbl::gtml::nabla_git_info); - jsonizeGitInfo(modules["dxc"],nbl::gtml::dxc_git_info); - - m_logger->log("Build Info:\n%s",ILogger::ELL_INFO,j.dump(4).c_str()); - } - - - public: - inline PathTracingApp(const path& _localInputCWD, const path& _localOutputCWD, const path& _sharedInputCWD, const path& _sharedOutputCWD) - : IApplicationFramework(_localInputCWD, _localOutputCWD, _sharedInputCWD, _sharedOutputCWD) {} - - inline IAPIConnection::SFeatures getAPIFeaturesToEnable() override - { - auto retval = device_base_t::getAPIFeaturesToEnable(); - if (m_args.headless) - retval.swapchainMode = E_SWAPCHAIN_MODE::ESM_NONE; - return retval; - } - - inline SPhysicalDeviceFeatures getRequiredDeviceFeatures() const override - { - auto retval = device_base_t::getRequiredDeviceFeatures(); - return retval.unionWith(CRenderer::RequiredDeviceFeatures()); - } - - inline SPhysicalDeviceFeatures getPreferredDeviceFeatures() const override - { - auto retval = device_base_t::getPreferredDeviceFeatures(); - if (m_args.headless) - retval.swapchainMode = E_SWAPCHAIN_MODE::ESM_NONE; - return retval.unionWith(CRenderer::PreferredDeviceFeatures()); - } - - inline SPhysicalDeviceLimits getRequiredDeviceLimits() const override - { - auto retval = device_base_t::getRequiredDeviceLimits(); - // TODO: need union/superset - retval.rayTracingPositionFetch = true; - retval.shaderStorageImageReadWithoutFormat = true; - return retval; - } - - inline void filterDevices(nbl::core::set& physicalDevices) const override - { - device_base_t::filterDevices(physicalDevices); - std::erase_if(physicalDevices,[&](const IPhysicalDevice* device)->bool - { - const auto& props = device->getMemoryProperties(); - uint64_t largestVRAMHeap = 0; - using heap_flags_e = IDeviceMemoryAllocation::E_MEMORY_HEAP_FLAGS; - for (uint32_t h=0; hgetDirectVRAMAccessMemoryTypeBits(); - for (uint32_t t=0; t>t)&0x1u) && props.memoryHeaps[props.memoryTypes[t].heapIndex].size==largestVRAMHeap) - return false; - m_logger->log("Filtering out Device %p (%s) due to lack of ReBAR",ILogger::ELL_WARNING,device,device->getProperties().deviceName); - return true; - } - ); - } - - inline nbl::core::vector getSurfaces() const override - { - if (m_args.headless) - return {}; - - if (!m_presenter) - { - const_cast&>(m_presenter) = CWindowPresenter::create({ - { - .assMan = m_assetMgr, - .logger = smart_refctd_ptr(m_logger) - }, - { - .winMgr = m_winMgr - }, - m_api, - make_smart_refctd_ptr(smart_refctd_ptr(m_inputSystem),smart_refctd_ptr(m_logger)), - "Path Tracer" - }); - } - - if (m_presenter) - { - const auto* presenter = m_presenter.get(); - return { {presenter->getSurface()/*,EQF_NONE*/} }; - } - - return {}; - } - - inline bool onAppInitialized(smart_refctd_ptr&& system) override - { - // TODO: parse the arguments - m_args.headless = false; - - if (!m_args.headless) - m_inputSystem = make_smart_refctd_ptr(logger_opt_smart_ptr(smart_refctd_ptr(m_logger))); - - if (!asset_base_t::onAppInitialized(smart_refctd_ptr(system))) - return false; - - if (m_args.headless) - { - if (!BasicMultiQueueApplication::onAppInitialized(smart_refctd_ptr(system))) - return false; - } - else if (!device_base_t::onAppInitialized(smart_refctd_ptr(system))) - return false; - - printGitInfos(); - - // - if (!m_args.headless && !m_presenter) - return logFail("Failed to create CWindowPresenter"); - - // - m_renderer = CRenderer::create({ - { - .graphicsQueue = getGraphicsQueue(), - .computeQueue = getComputeQueue(), - .uploadQueue = getTransferUpQueue(), - .utilities = smart_refctd_ptr(m_utils) - }, - m_assetMgr.get(), - (sharedOutputCWD/nbl::examples::CCachedOwenScrambledSequence::SCreationParams::DefaultFilename).string() - }); - if (!m_renderer) - return logFail("Failed to create CRenderer"); - - // - if (!m_args.headless && !m_presenter->init(m_renderer.get())) - return logFail("Failed to initialize CWindowPresenter"); - - // - m_resolver = CBasicRWMCResolver::create({ - {}, - m_renderer.get() - }); - if (!m_resolver) - return logFail("Failed to create CBasicRWMCResolver"); - - // set up the scene loader - m_sceneLoader = CSceneLoader::create({ - { - .assMan = smart_refctd_ptr(m_assetMgr), - .logger = smart_refctd_ptr(m_logger) - } - }); - - // TODO: tmp code - { - m_api->startCapture(); - auto scene_daily_pt = m_renderer->createScene({ - .load = m_sceneLoader->load({ - .relPath = sharedInputCWD/"mitsuba/ditt/render_2160p.xml", - .workingDirectory = localOutputCWD - }), - .converter = nullptr - }); - // the UI would have you load the zip first, then present a dropdown of what to load - // but still need to support archive mount for cmdline load - #if 0 // this particular zip goes down an unsupported path in our zip loader - auto scene_bedroom = m_sceneLoader->load({ - .relPath = sharedInputCWD/"mitsuba/bedroom.zip/scene.xml", - .workingDirectory = localOutputCWD - }); - #endif - m_api->endCapture(); - - if (!scene_daily_pt) - return logFail("Could not create scene"); - - // quick test code - nbl::core::vector sensors(3,scene_daily_pt->getSensors().front()); - { - sensors[1].mutableDefaults.cropWidth = 640; - sensors[1].mutableDefaults.cropHeight = 360; - sensors[1].mutableDefaults.cropOffsetX = 0; - sensors[1].mutableDefaults.cropOffsetY = 0; - } - { - sensors[2].mutableDefaults.cropWidth = 5120; - sensors[2].mutableDefaults.cropHeight = 2880; - sensors[2].mutableDefaults.cropOffsetX = 128; - sensors[2].mutableDefaults.cropOffsetY = 128; - } - for (auto i=1; i<3; i++) - { - sensors[i].constants.width = sensors[i].mutableDefaults.cropWidth+2*sensors[i].mutableDefaults.cropOffsetX; - sensors[i].constants.height = sensors[i].mutableDefaults.cropHeight+2*sensors[i].mutableDefaults.cropOffsetY; - } -// sensors.erase(sensors.begin()); - for (const auto& sensor : sensors) - m_sessionQueue.push( - scene_daily_pt->createSession({ - {.mode=CSession::RenderMode::Beauty},&sensor - }) - ); - } - - return true; - -#if 0 // ui - // gui descriptor setup - { - using binding_flags_t = IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS; - { - IGPUSampler::SParams params; - params.AnisotropicFilter = 1u; - params.TextureWrapU = ETC_REPEAT; - params.TextureWrapV = ETC_REPEAT; - params.TextureWrapW = ETC_REPEAT; - - m_ui.samplers.gui = m_device->createSampler(params); - m_ui.samplers.gui->setObjectDebugName("Nabla IMGUI UI Sampler"); - } - - std::array, 69u> immutableSamplers; - for (auto& it : immutableSamplers) - it = smart_refctd_ptr(m_ui.samplers.scene); - - immutableSamplers[nbl::ext::imgui::UI::FontAtlasTexId] = smart_refctd_ptr(m_ui.samplers.gui); - - 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, MaxUITextureCount); - params.renderpass = smart_refctd_ptr(renderpass); - params.streamingBuffer = nullptr; - params.subpassIx = 0u; - params.transfer = getGraphicsQueue(); - params.utilities = m_utils; - { - m_ui.manager = ext::imgui::UI::create(std::move(params)); - - // note that we use default layout provided by our extension, but you are free to create your own by filling nbl::ext::imgui::UI::S_CREATION_PARAMETERS::resources - const auto* descriptorSetLayout = m_ui.manager->getPipeline()->getLayout()->getDescriptorSetLayout(0u); - const auto& params = m_ui.manager->getCreationParameters(); - - IDescriptorPool::SCreateInfo descriptorPoolInfo = {}; - descriptorPoolInfo.maxDescriptorCount[static_cast(asset::IDescriptor::E_TYPE::ET_SAMPLER)] = (uint32_t)nbl::ext::imgui::UI::DefaultSamplerIx::COUNT; - descriptorPoolInfo.maxDescriptorCount[static_cast(asset::IDescriptor::E_TYPE::ET_SAMPLED_IMAGE)] = MaxUITextureCount; - descriptorPoolInfo.maxSets = 1u; - descriptorPoolInfo.flags = IDescriptorPool::E_CREATE_FLAGS::ECF_UPDATE_AFTER_BIND_BIT; - - m_guiDescriptorSetPool = m_device->createDescriptorPool(std::move(descriptorPoolInfo)); - assert(m_guiDescriptorSetPool); - - m_guiDescriptorSetPool->createDescriptorSets(1u, &descriptorSetLayout, &m_ui.descriptorSet); - assert(m_ui.descriptorSet); - } - } - - m_ui.manager->registerListener( - [this]() -> void { - ImGuiIO& io = ImGui::GetIO(); - - m_camera.setProjectionMatrix([&]() - { - static matrix4SIMD projection; - - projection = matrix4SIMD::buildProjectionMatrixPerspectiveFovRH( - core::radians(m_cameraSetting.fov), - io.DisplaySize.x / io.DisplaySize.y, - m_cameraSetting.zNear, - m_cameraSetting.zFar); - - return projection; - }()); - - 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("Controls"); - - ImGui::SameLine(); - - ImGui::Text("Camera"); - - ImGui::SliderFloat("Move speed", &m_cameraSetting.moveSpeed, 0.1f, 10.f); - ImGui::SliderFloat("Rotate speed", &m_cameraSetting.rotateSpeed, 0.1f, 10.f); - ImGui::SliderFloat("Fov", &m_cameraSetting.fov, 20.f, 150.f); - ImGui::SliderFloat("zNear", &m_cameraSetting.zNear, 0.1f, 100.f); - ImGui::SliderFloat("zFar", &m_cameraSetting.zFar, 110.f, 10000.f); - Light m_oldLight = m_light; - int light_type = m_light.type; - ImGui::ListBox("LightType", &light_type, s_lightTypeNames, ELT_COUNT); - m_light.type = static_cast(light_type); - if (m_light.type == ELT_DIRECTIONAL) - { - ImGui::SliderFloat3("Light Direction", &m_light.direction.x, -1.f, 1.f); - } - else if (m_light.type == ELT_POINT) - { - ImGui::SliderFloat3("Light Position", &m_light.position.x, -20.f, 20.f); - } - else if (m_light.type == ELT_SPOT) - { - ImGui::SliderFloat3("Light Direction", &m_light.direction.x, -1.f, 1.f); - ImGui::SliderFloat3("Light Position", &m_light.position.x, -20.f, 20.f); - - float32_t dOuterCutoff = hlsl::degrees(acos(m_light.outerCutoff)); - if (ImGui::SliderFloat("Light Outer Cutoff", &dOuterCutoff, 0.0f, 45.0f)) - { - m_light.outerCutoff = cos(hlsl::radians(dOuterCutoff)); - } - } - ImGui::Checkbox("Use Indirect Command", &m_useIndirectCommand); - if (m_light != m_oldLight) - { - m_frameAccumulationCounter = 0; - } - - ImGui::Text("X: %f Y: %f", io.MousePos.x, io.MousePos.y); - - ImGui::End(); - } - ); -#endif - } - -#if 0 // gui - bool updateGUIDescriptorSet() - { - // texture atlas, note we don't create info & write pair for the font sampler because UI extension's is immutable and baked into DS layout - static std::array descriptorInfo; - static IGPUDescriptorSet::SWriteDescriptorSet writes[MaxUITextureCount]; - - descriptorInfo[nbl::ext::imgui::UI::FontAtlasTexId].info.image.imageLayout = IImage::LAYOUT::READ_ONLY_OPTIMAL; - descriptorInfo[nbl::ext::imgui::UI::FontAtlasTexId].desc = smart_refctd_ptr(m_ui.manager->getFontAtlasView()); - - for (uint32_t i = 0; i < descriptorInfo.size(); ++i) - { - writes[i].dstSet = m_ui.descriptorSet.get(); - writes[i].binding = 0u; - writes[i].arrayElement = i; - writes[i].count = 1u; - } - writes[nbl::ext::imgui::UI::FontAtlasTexId].info = descriptorInfo.data() + nbl::ext::imgui::UI::FontAtlasTexId; - - return m_device->updateDescriptorSets(writes, {}); - } -#endif - - inline void workLoopBody() override - { - CSession* session; - { - bool sameSession = true; - volatile bool skip = false; // skip using the debugger - for (session=m_resolver->getActiveSession(); !session || session->getProgress()>=1.f || skip;) - { - skip = false; - if (m_sessionQueue.empty()) - { - if (!m_args.headless) - handleInputs(); - return; - } - session = m_sessionQueue.front().get(); - // init - m_utils->autoSubmit({.queue=getGraphicsQueue()},[&session,this](SIntendedSubmitInfo& info)->bool - { - return session->init(info); - } - ); - m_resolver->changeSession(std::move(m_sessionQueue.front())); - sameSession = false; - m_sessionQueue.pop(); - } - // TODO: camera movement and UI update - if (sameSession) - { - // no update right now - session->update(session->getActiveResources().prevSensorState); - } - } - - m_api->startCapture(); - IQueue::SSubmitInfo::SSemaphoreInfo rendered = {}; - { - auto deferredSubmit = m_renderer->render(session); - if (deferredSubmit) - { - IGPUCommandBuffer* const cb = deferredSubmit; - if (!m_args.headless || session->getProgress()>=1.f) - { - m_resolver->resolve(cb,nullptr); - } - rendered = deferredSubmit({}); - } - } - m_api->endCapture(); - - if (m_args.headless) - return; - handleInputs(); - if (!keepRunning()) - return; - - m_presenter->acquire(session); - auto* const cb = m_presenter->beginRenderpass(); - { - // can do additional stuff like ImGUI work here - } - m_presenter->endRenderpassAndPresent(rendered); -#if 0 // gui - -// ... - const auto uiParams = m_ui.manager->getCreationParameters(); - auto* uiPipeline = m_ui.manager->getPipeline(); - cmdbuf->bindGraphicsPipeline(uiPipeline); - cmdbuf->bindDescriptorSets(EPBP_GRAPHICS, uiPipeline->getLayout(), uiParams.resources.texturesInfo.setIx, 1u, &m_ui.descriptorSet.get()); - ISemaphore::SWaitInfo waitInfo = { .semaphore = m_semaphore.get(), .value = m_realFrameIx + 1u }; - m_ui.manager->render(cmdbuf, waitInfo); - - { - { - - updateGUIDescriptorSet(); - - } - } -#endif - } - - inline void handleInputs() - { - if (m_args.headless) - return; - - m_inputSystem->getDefaultMouse(&m_mouse); - m_inputSystem->getDefaultKeyboard(&m_keyboard); - - struct - { - std::vector mouse{}; - std::vector keyboard{}; - } capturedEvents; - -// const auto& io = ImGui::GetIO(); - static std::chrono::microseconds previousEventTimestamp{}; - m_mouse.consumeEvents([&](const IMouseEventChannel::range_t& events) -> void - { - for (const auto& e : events) // here capture - { - if (e.timeStamp < previousEventTimestamp) - continue; - - previousEventTimestamp = e.timeStamp; - capturedEvents.mouse.emplace_back(e); - - } - }, m_logger.get() - ); - m_keyboard.consumeEvents([&](const IKeyboardEventChannel::range_t& events) -> void - { - for (const auto& e : events) // here capture - { - if (e.timeStamp < previousEventTimestamp) - continue; - - previousEventTimestamp = e.timeStamp; - capturedEvents.keyboard.emplace_back(e); - } - }, m_logger.get() - ); -#if 0 // ui - const SRange mouseEvents(capturedEvents.mouse.data(), capturedEvents.mouse.data() + capturedEvents.mouse.size()); - const SRange keyboardEvents(capturedEvents.keyboard.data(), capturedEvents.keyboard.data() + capturedEvents.keyboard.size()); - const auto cursorPosition = m_window->getCursorControl()->getPosition(); - const auto mousePosition = float32_t2(cursorPosition.x, cursorPosition.y) - float32_t2(m_window->getX(), m_window->getY()); - - const nbl::ext::imgui::UI::SUpdateParameters params = - { - .mousePosition = mousePosition, - .displaySize = { m_window->getWidth(), m_window->getHeight() }, - .mouseEvents = mouseEvents, - .keyboardEvents = keyboardEvents - }; - m_ui.manager->update(params); -#endif - } - - inline bool keepRunning() override - { - if (m_args.headless) - { - if (auto* const currentSession=m_resolver->getActiveSession(); m_sessionQueue.empty() && (!currentSession || currentSession->getProgress()>=1.f)) - return false; - return true; - } - else - return !m_presenter->irrecoverable(); - } - - inline bool onAppTerminated() override - { - return device_base_t::onAppTerminated(); - } - - private: - AppArguments m_args = {}; - // - smart_refctd_ptr m_inputSystem; - InputSystem::ChannelReader m_mouse; - InputSystem::ChannelReader m_keyboard; - // - smart_refctd_ptr m_presenter; - // - smart_refctd_ptr m_renderer; - smart_refctd_ptr m_resolver; - // - smart_refctd_ptr m_sceneLoader; - // - nbl::core::queue> m_sessionQueue; - -#if 0 // gui - struct C_UI - { - nbl::core::smart_refctd_ptr manager; - - struct - { - core::smart_refctd_ptr gui, scene; - } samplers; - - core::smart_refctd_ptr descriptorSet; - } m_ui; - core::smart_refctd_ptr m_guiDescriptorSetPool; -#endif - -}; -NBL_MAIN_FUNC(PathTracingApp) \ No newline at end of file diff --git a/40_PathTracer/src/io/CSceneLoader.cpp b/40_PathTracer/src/io/CSceneLoader.cpp deleted file mode 100644 index 5f782f8e4..000000000 --- a/40_PathTracer/src/io/CSceneLoader.cpp +++ /dev/null @@ -1,542 +0,0 @@ -// Copyright (C) 2025-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_THIS_EXAMPLE_C_SCENE_LOADER_CPP_ -#include "io/CSceneLoader.h" - -#include "nbl/builtin/hlsl/testing/relative_approx_compare.hlsl" - -#include "nbl/ext/MitsubaLoader/CMitsubaLoader.h" -#include "nbl/ext/MitsubaLoader/CSerializedLoader.h" - -#include "nlohmann/json.hpp" - - -// -namespace nbl::system::impl -{ -template<> -struct to_string_helper -{ - public: - static inline std::string __call(const nbl::this_example::CSceneLoader::SLoadResult::SSensor& value) - { - nlohmann::json j; - j["valid"] = bool(value); - - auto& constants = j["constants"]; - { - auto& mutableDefaults = j["mutableDefaults"]; - const auto& _mutableDefaults = value.mutableDefaults; - mutableDefaults["absoluteTransform"] = system::to_string(_mutableDefaults.absoluteTransform); - { - auto& raygen = mutableDefaults["raygen"]; - const auto& _raygen = _mutableDefaults.raygen; - raygen["virtualPlaneFromNDC"] = system::to_string(hlsl::float32_t2x3(_raygen)); - { - auto& clipPlanes = mutableDefaults["clipPlanes"]; - for (uint8_t i=0,count=_mutableDefaults.getClipPlaneCount(); i CSceneLoader::create(SCreationParams&& _params) -{ - if (!_params) - return nullptr; - SConstructorParams params = {std::move(_params)}; - - // add the loaders - { - auto* const assMan = params.assMan.get(); - auto* const system = assMan->getSystem(); - - bool success = true; - success = success && assMan->addAssetLoader(make_smart_refctd_ptr(smart_refctd_ptr(system)))!=0xdeadbeefu; - // some of our test scenes won't load without the `.serialized` support - success = success && assMan->addAssetLoader(make_smart_refctd_ptr()) != 0xdeadbeefu; - - if (!success) - { - params.logger.log("Could not add Mitsuba Asset Loaders", ILogger::ELL_ERROR); - return nullptr; - } - } - - return core::smart_refctd_ptr(new CSceneLoader(std::move(params)),core::dont_grab); -} - -auto CSceneLoader::load(SLoadParams&& _params) -> SLoadResult -{ - IAssetLoader::SAssetLoadParams params = {}; - params.workingDirectory = _params.workingDirectory; - auto logger = params.logger = m_params.logger.get().get(); - - auto* const assMan = m_params.assMan.get(); - // handle archive stuff - const auto relPath = _params.relPath.lexically_normal(); - auto* const system = assMan->getSystem(); - core::stack archiveStack; - for (auto it=relPath.begin(); it!=relPath.end();) - { - const auto ext = (it++)->extension().string(); - if (strcmpi(ext.c_str(),".zip")==0) - { - // some N4950 defect makes it impossible - //const auto archPath = system::path(relPath.begin(),it); - const auto archPath = std::accumulate(relPath.begin(),it,system::path(),[](const system::path& lhs, const system::path& rhs)->system::path - { - return lhs/rhs; - } - ); - auto archive = system->openFileArchive(archPath); - archiveStack.push(archive.get()); - system->mount(std::move(archive)); - } - } - - const auto relPathStr = relPath.string(); - auto asset = assMan->getAsset(relPathStr,params); - if (asset.getContents().empty()) - { - logger.log( - "Failed to Load Mitsuba scene from \"%s\" with working directory \"%s\"", - ILogger::ELL_ERROR,relPathStr.c_str(),_params.workingDirectory.lexically_normal().string().c_str() - ); - return {}; - } - logger.log("Loaded %s",ILogger::ELL_INFO,relPathStr.c_str()); - - // now unmount the archives - for (; !archiveStack.empty(); archiveStack.pop()) - system->unmount(archiveStack.top()); - - const auto type = asset.getAssetType(); - if (type!=IAsset::E_TYPE::ET_SCENE) - { - logger.log("But did not load an `ICPUScene` type is %S",ILogger::ELL_ERROR,system::to_string(type)); - return {}; - } - - const auto* const untypedMeta = asset.getMetadata(); - if (!untypedMeta || strcmpi(untypedMeta->getLoaderName(),CMitsubaMetadata::LoaderName)!=0) - { - logger.log("Loaded an ICPUScene but without `CMistubaMetadata`",ILogger::ELL_ERROR); - return {}; - } - const auto* const meta = static_cast(untypedMeta); - - // - auto& integrator = meta->m_global.m_integrator; - - // TODO: compute/get this from minumum extent of scene - float sceneSize = 50.f; - - // - core::vector sensors; - auto& _sensors = meta->m_global.m_sensors; - if (_sensors.empty()) - { - logger.log("The `CMistubaMetadata` contains no sensors",ILogger::ELL_ERROR); - return {}; - } - else - { - sensors.resize(_sensors.size()); - logger.log("Total number of Sensors = %d",ILogger::ELL_INFO,sensors.size()); - const bool shouldHaveSensorIdxInFileName = sensors.size()>1; - const auto mainFileName = relPath.filename(); - for (auto i=0; i::min) - { - logger.log("Sensor %s (%d-th in XML) has non invertible singular transformation!",ILogger::ELL_ERROR,id,i); - constants = {}; - continue; - } - // extract and remove scale, also make the transform right-handed - { - scaleRcp = rsqrt({ - dot(orientationT[0],orientationT[0]), - dot(orientationT[1],orientationT[1]), - dot(orientationT[2],orientationT[2]) - }); - // unflip X if left handed - if (det<0.f) - scaleRcp.x = -scaleRcp.x; - // Old Code View Matrix: - // LH X+ = Left, Y+ = Up, Z+ = Backward - // RH X+ = Right, Y+ = Up, Z+ = Forward - // Basically RH view matrix used to make the Forward direction Z-, so LH projection matrix flupped it to have Z+ and W+ (cancel out) - // The only thing that stayed was the flipping of the X direction. - // ------------------------------------------ - // If we're using our animators, then we can't have negative scales on odd number of axes - // the animators will re-create the camera from forward and up axes with right handed matrix - // New Sensor code should take a look at inverse Projection Matrix to determine the dNDC/dView directions - // nearPlaneCenter = mul(invProj,float(0,0,0,1)) = invProj.column[3] - // ndcXDir = normalize(invProj.column[0].xyz*nearPlaneCenter.w-nearPlaneCenter.xyz*invProj.column[3].w) = if regular matrix = normalize(invProj.column[0].xyz) - // ndcYDir = normalize(invProj.column[1].xyz*nearPlaneCenter.w-nearPlaneCenter.xyz*invProj.column[3].w) = if regular matrix = normalize(invProj.column[1].xyz) - for (auto r=0; r<3; r++) - { - orientationT[r] *= scaleRcp[r]; - absoluteTransform[r].xyz *= scaleRcp; - } - } - } - mutableDefaults.absoluteTransform = absoluteTransform; - } - // raygen - auto& ndc = mutableDefaults.raygen.encoded; - switch (_sensor.type) - { - case mts_sensor_t::Type::THINLENS: - logger.log("Sensor %s (%d-th in XML) is THINLENS, Depth of Field not implemented yet, demoting to PERSPECTIVE!",ILogger::ELL_WARNING,id,i); - [[fallthrough]]; - case mts_sensor_t::Type::PERSPECTIVE: - { - const auto& persp = _sensor.perspective; - // calculations for the projection plane behind the aperture (or in-front if thinking virtual) - const float halfFoVRad = hlsl::radians(persp.fov)*0.5f; - const auto halfSize = hlsl::tan(halfFoVRad); - // by default FoV is y-axis - float halfHeight = halfSize; - float halfWidth = halfSize; - // - const float aspectRatio = float(constants.width)/float(constants.height); - using fov_axis_e = mts_sensor_t::PerspectivePinhole::FOVAxis; - switch (persp.fovAxis) - { - case fov_axis_e::X: - halfHeight /= aspectRatio; - break; - case fov_axis_e::Y: - halfWidth *= aspectRatio; - break; - case fov_axis_e::DIAGONAL: - { - // halfSize^2 == halfWidth^2+halfHeight^2 == (1+aspectRatio^2)*halfHeight^2 - halfHeight /= hlsl::sqrt(1.f+aspectRatio*aspectRatio); - halfWidth = halfHeight*aspectRatio; - } - break; - case fov_axis_e::SMALLER: - if (aspectRatio<1.f) - halfHeight /= aspectRatio; - else - halfWidth *= aspectRatio; - break; - case fov_axis_e::LARGER: - if (aspectRatio<1.f) - halfWidth *= aspectRatio; - else - halfHeight /= aspectRatio; - break; - default: - break; - } - // max 1/4 circle - if (!(halfWidth>0.f && halfHeight>0.f)) - { - ndc[1][1] = core::nan(); - logger.log("Sensor %s (%d-th in XML) had a Field of View of %f degrees!",ILogger::ELL_ERROR,id,i,persp.fov); - break; - } - // elongating camera along Z will shrink the effective FOV - ndc[0] = float32_t3(scaleRcp.z/scaleRcp.x,0.f,hlsl::sign(scaleRcp.x)*persp.shiftX); - // column gets negated because in Vulkan NDC.y runs downwards - ndc[1] = -float32_t3(0.f,scaleRcp.z/scaleRcp.y,persp.shiftY)*halfHeight; - } - break; - case mts_sensor_t::Type::TELECENTRIC: - logger.log("Sensor %s (%d-th in XML) is TELECENTRIC, Depth of Field not implemented yet, demoting to ORTHOGRAPHIC!",ILogger::ELL_WARNING,id,i); - [[fallthrough]]; - case mts_sensor_t::Type::ORTHOGRAPHIC: - { - const auto& ortho = _sensor.orthographic; - // extract and negate the scale from the - ndc[0] = float32_t3(scaleRcp.x,0.f,0.f); - ndc[1] = float32_t3(0.f,scaleRcp.y*float(constants.height)/float(constants.width),0.f); - } - break; - case mts_sensor_t::Type::SPHERICAL: - // irrelevant for spherical cameras, we send rays everywhere - ndc[0] = promote(0); - ndc[1] = promote(0); - break; - default: - ndc[0][0] = core::nan(); - break; - } - if (hlsl::isnan(ndc[0][0])) - { - logger.log("Sensor %s (%d-th in XML) has invalid projection, had type %s!",ILogger::ELL_ERROR,id,i,system::to_string(_sensor.type).c_str()); - constants = {}; - continue; - } - // clip planes - auto outClipPlane = mutableDefaults.clipPlanes.begin(); - for (auto i=0; i(0.f); - if (any(glsl::notEqual(plane,rhs))) - { - if (outClipPlane>mutableDefaults.clipPlanes.end()) - { - logger.log("Sensor %s (%d-th in XML) has more than %d clip planes, ignoreing the rest!",ILogger::ELL_ERROR,id,i); - break; - } - *(outClipPlane++) = plane; - } - } - // ignore crops for spherical cameras - if (!isSpherical) - { - mutableDefaults.cropWidth = film.cropWidth; - mutableDefaults.cropHeight = film.cropHeight; - mutableDefaults.cropOffsetX = film.cropOffsetX; - mutableDefaults.cropOffsetY = film.cropOffsetY; - } - // - mutableDefaults.nearClip = base.nearClip; - mutableDefaults.farClip = base.farClip; - // - mutableDefaults.cascadeLuminanceBase = film.cascadeLuminanceBase; - mutableDefaults.cascadeLuminanceStart = film.cascadeLuminanceStart; - // - integrator.visit([&mutableDefaults](auto& var)->void - { - if constexpr (std::is_base_of_v>) - mutableDefaults.hideEnvironment = var.hideEnvironment; - } - ); - integrator.visit([&mutableDefaults](auto& var)->void - { - if constexpr (std::is_base_of_v>) - { - mutableDefaults.maxPathDepth = var.maxPathDepth; - mutableDefaults.russianRouletteDepth = var.russianRouletteDepth; - } - } - ); - } - { - using dyn_t = SLoadResult::SSensor::SDynamic; - dyn_t& dynamicDefaults = sensors[i].dynamicDefaults; - // output file settings - { - std::filesystem::path outputFilePath = film.outputFilePath; - // handle missing output path - if (outputFilePath.empty()) - { - const auto extensionStr = fileExtensionFromFormat(film.fileFormat); - core::string filename = "Render_" + mainFileName.stem().string(); - if(shouldHaveSensorIdxInFileName) - filename += "_Sensor_" + system::to_string(i) + extensionStr.data(); - else - filename += extensionStr; - logger.log("Sensor %s (%d-th in XML) has no output path, deduced to \"%s\"",ILogger::ELL_WARNING,id,i,filename.c_str()); - outputFilePath = filename; - } - std::string_view extension = ""; - bool invalid = false; - if (auto ext=outputFilePath.extension().string(); ext.size()>2) - { - extension = {ext.begin()+1,ext.end()}; - using format_e = ext::MitsubaLoader::CElementFilm::FileFormat; - switch (film.fileFormat) - { - case format_e::PNG: - invalid = strcmpi(extension.data(),"png")!=0; - break; - case format_e::OPENEXR: - invalid = strcmpi(extension.data(),"exr")!=0; - break; - case format_e::JPEG: - invalid = strcmpi(extension.data(),"jpg")!=0 && strcmpi(extension.data(),"jpe")!=0 && strcmpi(extension.data(),"jpeg")!=0 && - strcmpi(extension.data(),"jif")!=0 && strcmpi(extension.data(),"jfif")!=0 && strcmpi(extension.data(),"jfi")!=0; - break; - default: - break; - } - } - if (invalid) - { - logger.log("Sensor %s (%d-th in XML) has invalid format %d or extension \"%s\"",ILogger::ELL_ERROR,id,i,system::to_string(film.fileFormat),extension.data()); - dynamicDefaults = {}; - continue; - } - dynamicDefaults.outputFilePath = std::move(outputFilePath); -#if 0 // not part of the loader, do somewhere else - // - if (outputFilePath.is_relative()) - { - logger.log("Film output path is relative: \"%s\"",ILogger::ELL_INFO,outputFilePath.c_str()); - // output relative to output dir - // or the XML if so wished (walk backward and determine which directories are read only) - } -#endif - } - // post process - { - dynamicDefaults.postProc.bloomFilePath = film.denoiserBloomFilePath; - dynamicDefaults.postProc.bloomScale = film.denoiserBloomScale; - dynamicDefaults.postProc.bloomIntensity = film.denoiserBloomIntensity; - dynamicDefaults.postProc.tonemapperArgs = std::string(film.denoiserTonemapperArgs); - } - // up vector - { - // true forward may be Z+ or Z- - const auto viewSpaceZ = orientationT[2]; - // our "right" will only be X+ if forward is Z- - const auto reconstructedRight = cross(base.up,viewSpaceZ); - const auto actualRight = cross(orientationT[1],viewSpaceZ); - // but it doesn't matter here for this check (both will be flipped, dot product identical) - const float dp = dot(reconstructedRight,actualRight); - const float pb = dot(base.up,viewSpaceZ); - // special formulation avoiding multiple sqrt and inversesqrt to preserve precision - const auto reconstructedLen = hlsl::length(reconstructedRight); - logger.log("Camera Reconstructed Up Vector match score = %f",system::ILogger::ELL_INFO,dp/reconstructedLen); - const float64_t threshold = 0.9996*hlsl::length(base.up); - if (testing::relativeApproxCompare(dp,reconstructedLen,0.03f) && hlsl::abs(pb)setStepZoomMultiplier(logarithmicZoomSpeed); - } - else if (!hlsl::isnan(base.zoomSpeed)) - logger.log("Sensor %s (%d-th in XML) is SPHERICAL, zoom speed gets ignored!",ILogger::ELL_WARNING,id,i); - dynamicDefaults.samplesNeeded = _sensor.sampler.sampleCount; - dynamicDefaults.kappa = constants.cascadeCount<2 ? 0.f:film.rfilter.kappa; - dynamicDefaults.Emin = film.rfilter.Emin; - if (film.envmapRegularizationFactor>0.f) - logger.log("Sensor %s (%d-th in XML) `envmapRegularizationFactor=%f` is deprecated and ignored, we do MIS now",ILogger::ELL_WARNING,id,i,film.envmapRegularizationFactor); - } - } - // log - for (auto i=0; iclearAllAssetCache(); - // return - return { - .scene = IAsset::castDown(asset.getContents()[0]), - .sensors = std::move(sensors) - }; -} - -} \ No newline at end of file diff --git a/40_PathTracer/src/renderer/CRenderer.cpp b/40_PathTracer/src/renderer/CRenderer.cpp deleted file mode 100644 index 7d38ee5a1..000000000 --- a/40_PathTracer/src/renderer/CRenderer.cpp +++ /dev/null @@ -1,894 +0,0 @@ -// Copyright (C) 2025-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 "renderer/CRenderer.h" -#include "renderer/SAASequence.h" - -#include "nbl/ext/FullScreenTriangle/FullScreenTriangle.h" - -#include -#include -#include -#include - - -namespace nbl::this_example -{ -using namespace nbl::core; -using namespace nbl::asset; -using namespace nbl::system; -using namespace nbl::video; - - -smart_refctd_ptr CRenderer::loadPrecompiledShader_impl(IAssetManager* assMan, const core::string& key, logger_opt_ptr logger) -{ - IAssetLoader::SAssetLoadParams lp = {}; - lp.logger = logger; - lp.workingDirectory = "app_resources"; // virtual root - auto assetBundle = assMan->getAsset(key,lp); - const auto assets = assetBundle.getContents(); - if (!assets.empty()) - if (auto shader = IAsset::castDown(*assets.begin()); shader) - return shader; - - logger.log("Failed to load precompiled shader %s", ILogger::ELL_ERROR, key.c_str()); - return nullptr; -} - -// -smart_refctd_ptr CRenderer::create(SCreationParams&& _params) -{ - if (!_params) - return nullptr; - - // get started with the sequence ASAP - auto* const assMan = _params.assMan; - auto sequenceFuture = std::async(std::launch::async,[assMan](std::string&& cachePath)->auto - { - // TODO: resize the Sample Sequence after every scene load, smaller sequence has better caching properties - return nbl::examples::CCachedOwenScrambledSequence::create({ - .cachePath=std::move(cachePath),.assMan=assMan,.header={ - .maxSamplesLog2=MaxSPPLog2,.maxDimensions=(RandDimTriplesPerDepth*((0x1u<(params.utilities->getLogger()); - logger_opt_ptr logger = params.logger.get().get(); - - // - auto checkNullObject = [¶ms,logger](auto& obj, const std::string_view debugName)->bool - { - if (!obj) - { - logger.log("Failed to Create %s Object!",ILogger::ELL_ERROR,debugName.data()); - return true; - } - obj->setObjectDebugName(debugName.data()); - return false; - }; - - // - ILogicalDevice* device = params.utilities->getLogicalDevice(); - - // - params.semaphore = device->createSemaphore(0); - if (checkNullObject(params.semaphore,"CRenderer Semaphore")) - return nullptr; - - // basic samplers - const auto samplerDefaultRepeat = device->createSampler({}); - - using render_mode_e = CSession::RenderMode; - // create the layouts - { - constexpr auto RTStages = hlsl::ShaderStage::ESS_ALL_RAY_TRACING;// | hlsl::ShaderStage::ESS_COMPUTE; - constexpr auto RenderingStages = RTStages | hlsl::ShaderStage::ESS_COMPUTE; - // descriptor - { - using binding_create_flags_t = IDescriptorSetLayoutBase::SBindingBase::E_CREATE_FLAGS; - constexpr IGPUDescriptorSetLayout::SBinding UBOBinding = { - .binding = SensorDSBindings::UBO, - .type = IDescriptor::E_TYPE::ET_UNIFORM_BUFFER, - .createFlags = binding_create_flags_t::ECF_NONE, - .stageFlags = RenderingStages, - .count = 1 - }; - // the generic single-UBO - { - params.uboDSLayout = device->createDescriptorSetLayout({&UBOBinding,1}); - if (checkNullObject(params.uboDSLayout,"Generic Single UBO Layout")) - return nullptr; - } - constexpr auto DescriptorIndexingFlags = binding_create_flags_t::ECF_UPDATE_AFTER_BIND_BIT | binding_create_flags_t::ECF_UPDATE_UNUSED_WHILE_PENDING_BIT | binding_create_flags_t::ECF_PARTIALLY_BOUND_BIT; - // - auto singleStorageImage = [](const uint32_t binding)->IGPUDescriptorSetLayout::SBinding - { - return { - .binding = binding, - .type = IDescriptor::E_TYPE::ET_STORAGE_IMAGE, - .createFlags = binding_create_flags_t::ECF_PARTIALLY_BOUND_BIT, - .stageFlags = RenderingStages, - .count = 1 - }; - }; - // TODO: provide these two samplers from Envmap Importance sampling extension - const auto samplerNearestRepeat = device->createSampler({ - { - .MinFilter = ISampler::E_TEXTURE_FILTER::ETF_NEAREST, - .MaxFilter = ISampler::E_TEXTURE_FILTER::ETF_NEAREST, - .MipmapMode = ISampler::E_SAMPLER_MIPMAP_MODE::ESMM_NEAREST, - .AnisotropicFilter = 0, - }, - 0.f, - 0.f, - 0.f - }); - // bindless everything - { - // TODO: provide these two samplers from Envmap Importance sampling extension - const auto samplerEnvmapPDF = samplerNearestRepeat; - const auto samplerEnvmapWarpmap = device->createSampler({ - { - .MinFilter = ISampler::E_TEXTURE_FILTER::ETF_LINEAR, - .MaxFilter = ISampler::E_TEXTURE_FILTER::ETF_LINEAR, - .MipmapMode = ISampler::E_SAMPLER_MIPMAP_MODE::ESMM_NEAREST, - .AnisotropicFilter = 0, - }, - 0.f, - 0.f, - 0.f - }); - std::initializer_list bindings = { - UBOBinding, - { - .binding = SceneDSBindings::Envmap, - .type = IDescriptor::E_TYPE::ET_SAMPLED_IMAGE, - .createFlags = binding_create_flags_t::ECF_NONE, - .stageFlags = RTStages, - .count = 1, - .immutableSamplers = &samplerDefaultRepeat - }, - { - .binding = SceneDSBindings::TLASes, - .type = IDescriptor::E_TYPE::ET_ACCELERATION_STRUCTURE, - .createFlags = DescriptorIndexingFlags, - .stageFlags = RTStages, - .count = SceneDSBindingCounts::TLASes - }, - { - .binding = SceneDSBindings::Samplers, - .type = IDescriptor::E_TYPE::ET_SAMPLER, - .createFlags = DescriptorIndexingFlags, - .stageFlags = RTStages, - .count = SceneDSBindingCounts::Samplers - }, - { - .binding = SceneDSBindings::SampledImages, - .type = IDescriptor::E_TYPE::ET_SAMPLED_IMAGE, - .createFlags = DescriptorIndexingFlags, - .stageFlags = RTStages, - .count = SceneDSBindingCounts::SampledImages - }, - { - .binding = SceneDSBindings::EnvmapPDF, - .type = IDescriptor::E_TYPE::ET_SAMPLED_IMAGE, - .createFlags = DescriptorIndexingFlags, - .stageFlags = RTStages, - .count = 1, - .immutableSamplers = &samplerEnvmapPDF - }, - { - .binding = SceneDSBindings::EnvmapWarpMap, - .type = IDescriptor::E_TYPE::ET_SAMPLED_IMAGE, - .createFlags = DescriptorIndexingFlags, - .stageFlags = RTStages, - .count = 1, - .immutableSamplers = &samplerEnvmapWarpmap - } - }; - params.sceneDSLayout = device->createDescriptorSetLayout(bindings); - if (checkNullObject(params.sceneDSLayout,"Scene Descriptor Layout")) - return nullptr; - } - // the sensor layout - { - constexpr auto ResolveAndPresentStages = hlsl::ShaderStage::ESS_COMPUTE | hlsl::ShaderStage::ESS_FRAGMENT; - const auto defaultSampler = device->createSampler({ - { - .AnisotropicFilter = 0 - }, - 0.f, - 0.f, - 0.f - }); - std::initializer_list bindings = { - UBOBinding, - singleStorageImage(SensorDSBindings::ScrambleKey), - singleStorageImage(SensorDSBindings::SampleCount), - singleStorageImage(SensorDSBindings::Beauty), - singleStorageImage(SensorDSBindings::RWMCCascades), - singleStorageImage(SensorDSBindings::Albedo), - singleStorageImage(SensorDSBindings::Normal), - singleStorageImage(SensorDSBindings::Motion), - singleStorageImage(SensorDSBindings::Mask), - { - .binding = SensorDSBindings::Samplers, - .type = IDescriptor::E_TYPE::ET_SAMPLER, - .createFlags = binding_create_flags_t::ECF_NONE, - .stageFlags = ResolveAndPresentStages, - .count = SensorDSBindingCounts::Samplers, - .immutableSamplers = &defaultSampler - }, - { - .binding = SensorDSBindings::AsSampledImages, - .type = IDescriptor::E_TYPE::ET_SAMPLED_IMAGE, - .createFlags = binding_create_flags_t::ECF_PARTIALLY_BOUND_BIT, - .stageFlags = ResolveAndPresentStages, - .count = SensorDSBindingCounts::AsSampledImages - } - }; - params.sensorDSLayout = device->createDescriptorSetLayout(bindings); - if (checkNullObject(params.sensorDSLayout,"Sensor Descriptor Layout")) - return nullptr; - } - } - - // but many push constant ranges - SPushConstantRange pcRanges[uint8_t(render_mode_e::Count)]; - auto setPCRange = [&pcRanges](const render_mode_e mode)->void - { - pcRanges[uint8_t(mode)] = {.stageFlags=RTStages,.offset=0,.size=sizeof(T)}; - }; - setPCRange.operator()(render_mode_e::Previs); - setPCRange.operator()(render_mode_e::Beauty); - setPCRange.operator()(render_mode_e::Debug); - for (uint8_t t=0; tcreatePipelineLayout({pcRanges+t,1},params.sceneDSLayout,params.sensorDSLayout); - string debugName = to_string(static_cast(t))+"Rendering Pipeline Layout"; - if (checkNullObject(params.renderingLayouts[t],debugName)) - return nullptr; - } - } - - // TODO: create the generic pipelines - params.shaders[uint8_t(render_mode_e::Previs)] = loadPrecompiledShader<"pathtrace_previs">(_params.assMan,device,logger); - params.shaders[uint8_t(render_mode_e::Beauty)] = loadPrecompiledShader<"pathtrace_beauty">(_params.assMan,device,logger); - params.shaders[uint8_t(render_mode_e::Debug)] = loadPrecompiledShader<"pathtrace_debug">(_params.assMan,device,logger); - for (auto i=0; i(i))); - return nullptr; - } - - // command buffers - for (uint8_t i=0; icreateCommandPool(params.graphicsQueue->getFamilyIndex(),IGPUCommandPool::CREATE_FLAGS::RESET_COMMAND_BUFFER_BIT); - if (pool) - pool->createCommandBuffers(IGPUCommandPool::BUFFER_LEVEL::PRIMARY,1,params.commandBuffers+i,smart_refctd_ptr(params.logger.get())); - if (checkNullObject(params.commandBuffers[i],"Graphics Command Buffer "+to_string(i))) - return nullptr; - } - - // upload quantized LDS sequence buffer - { - auto sequence = sequenceFuture.get(); - params.sequenceHeader = sequence->getHeader(); - auto* const seqBufferCPU = sequence->getBuffer(); - params.utilities->createFilledDeviceLocalBufferOnDedMem(SIntendedSubmitInfo{.queue=params.graphicsQueue},IGPUBuffer::SCreationParams{seqBufferCPU->getCreationParams()},seqBufferCPU->getPointer()).move_into(params.sobolSequence); - params.sobolSequence->setObjectDebugName("Low Discrepancy Sequence"); - } - - return core::smart_refctd_ptr(new CRenderer(std::move(params)),core::dont_grab); -} - -core::smart_refctd_ptr CRenderer::createScene(CScene::SCreationParams&& _params) -{ - if (!_params) - return nullptr; - - auto* const device = getDevice(); - auto converter = core::smart_refctd_ptr(_params.converter); - - CScene::SConstructorParams params = {std::move(_params)}; -// params.sceneBound = ; - params.sensors = std::move(_params.load.sensors); - params.renderer = smart_refctd_ptr(this); - { - auto pool = device->createDescriptorPoolForDSLayouts(IDescriptorPool::E_CREATE_FLAGS::ECF_UPDATE_AFTER_BIND_BIT,{&m_construction.sceneDSLayout.get(),1}); - auto ds = pool->createDescriptorSet(smart_refctd_ptr(m_construction.sceneDSLayout)); - if (!ds) - { - m_creation.logger.log("Failed to create a scene - failed descriptor set allocation!",ILogger::ELL_ERROR); - return nullptr; - } - params.sceneDS = make_smart_refctd_ptr(std::move(ds)); - } - - auto* const cpuScene = _params.load.scene.get(); - - constexpr auto RenderModeCount = uint8_t(CSession::RenderMode::Count); - // create the pipelines - { - IGPURayTracingPipeline::SCreationParams creationParams[RenderModeCount] = {}; - using creation_flags_e = IGPURayTracingPipeline::SCreationParams::FLAGS; - IGPURayTracingPipeline::SShaderSpecInfo missShaders[RenderModeCount] = {}; - IGPURayTracingPipeline::SHitGroup hitShaders[RenderModeCount] = {}; - { - for (uint8_t m=0; m flags = creation_flags_e::NONE; // NO_NULL_INTERSECTION_SHADERS ? - if (!isBeauty) // TODO: With SER hit objdects will that still be true? - flags |= creation_flags_e::NO_NULL_MISS_SHADERS; - creationParams[m] = { - .layout = m_construction.renderingLayouts[m].get(), - .shaderGroups = { - .raygen = {.shader=shader,.entryPoint="raygen"}, - .misses = {missShaders+m,isBeauty ? 0ull:1ull}, - .hits = {hitShaders+m,1} - // TODO: use Material Compiler to get callables for us - }, - .cached = { - .flags = flags - } - }; - } - } - if (!device->createRayTracingPipelines(nullptr,creationParams,params.pipelines)) - { - m_creation.logger.log("Failed to create Path Tracing Pipelines",ILogger::ELL_ERROR); - return nullptr; - } - } - - // TODO: make this configurable - constexpr bool movableInstances = false; - - smart_refctd_ptr ubo; - core::vector> TLASes; - { - // construct the TLASes - core::vector> tmpTLASes; - // main TLAS - { - using tlas_build_f = ICPUTopLevelAccelerationStructure::BUILD_FLAGS; - constexpr auto baseTLASFlags = tlas_build_f::PREFER_FAST_TRACE_BIT | tlas_build_f::ALLOW_COMPACTION_BIT; - - auto& main = tmpTLASes.emplace_back(); - main = make_smart_refctd_ptr(); - { - auto& cpuInstances = cpuScene->getInstances(); - // need to convert weird topologies to lists - for (auto i=0u; igetTargets()) - { - auto* const collection = target.geoCollection.get(); - for (auto& ref : *collection->getGeometries()) - { - const auto* geo = ref.geometry.get(); - if (geo->getPrimitiveType()!=IGeometryBase::EPrimitiveType::Polygon) - continue; - ref.geometry = CPolygonGeometryManipulator::createTriangleListIndexing(static_cast(geo)); - } - } - ICPUScene::CDefaultTLASExporter exporter(cpuInstances); - auto exported = exporter(); - if (!exported) - { - m_creation.logger.log("Failed to convert TLAS instances!",ILogger::ELL_ERROR); - return nullptr; - } - if (!exported.allInstancesValid) - m_creation.logger.log("Some instances in the scene are invisible!",ILogger::ELL_ERROR); - for (auto& pair : exporter.m_blasCache) - { - using blas_build_f = ICPUBottomLevelAccelerationStructure::BUILD_FLAGS; - auto flags = pair.second->getBuildFlags(); - flags |= blas_build_f::ALLOW_DATA_ACCESS; - pair.second->setBuildFlags(flags); - } - for (auto& instance : *exported.instances) - { - // TODO: for now, we need material compiler and knowing how we'll orgarnise our SBT - instance.getBase().instanceShaderBindingTableRecordOffset = 0; - } - main->setInstances(std::move(exported.instances)); - } - // de-instancing and welding BLASes - { - // TODO: de-instancing step, need AS memory budget and a heuristic of which instances to eliminate (out of the ones we don't want to be movable) - // probably need OOBs of Geometry Collections, best heuristic would be "total surface of all intersections with other instances" divided by build memory. - // In the meantime can do own OBB/AABB surface area divded by build size. - // NOTE: can only "weld" BLASes which have the same build flags - } - { - auto flags = baseTLASFlags; - if (movableInstances) - flags |= tlas_build_f::ALLOW_UPDATE_BIT; - main->setBuildFlags(flags); - } - } - - struct Buffers final - { - using render_mode_e = CSession::RenderMode; - inline operator std::span() const {return {&ubo.get(),1+RenderModeCount};} - - smart_refctd_ptr ubo; - smart_refctd_ptr sbts[RenderModeCount]; - } tmpBuffers; - // - using buffer_usage_e = IGPUBuffer::E_USAGE_FLAGS; - constexpr auto BasicBufferUsages = buffer_usage_e::EUF_SHADER_DEVICE_ADDRESS_BIT; - { - tmpBuffers.ubo = ICPUBuffer::create({{.size=sizeof(SSceneUniforms),.usage=BasicBufferUsages|buffer_usage_e::EUF_UNIFORM_BUFFER_BIT},nullptr}); - auto& uniforms = *reinterpret_cast(tmpBuffers.ubo->getPointer()); - uniforms.init = {}; - uniforms.init.pSampleSequence = m_construction.sobolSequence->getDeviceAddress(); - uniforms.init.sequenceSamplesLog2 = m_construction.sequenceHeader.maxSamplesLog2; - // TODO: Some Constant to Tell us how many dimensions each path vertex consumes - uniforms.init.lastSequencePathDepth = m_construction.getSequenceMaxPathDepth(); - tmpBuffers.ubo->setContentHash(tmpBuffers.ubo->computeContentHash()); - } - // SBT - const auto& limits = device->getPhysicalDevice()->getLimits(); - assert(limits.shaderGroupBaseAlignment>=limits.shaderGroupHandleAlignment); - constexpr auto HandleSize = SPhysicalDeviceLimits::ShaderGroupHandleSize; - const auto handleSizeAligned = nbl::core::alignUp(HandleSize,limits.shaderGroupHandleAlignment); - for (uint8_t i=0; igetHitHandles(); - const auto missHandles = pipeline->getMissHandles(); - const auto callableHandles = pipeline->getCallableHandles(); - // - { - class CVectorBacked final : public core::refctd_memory_resource - { - public: - inline CVectorBacked(const size_t reservation) - { - storage.reserve(reservation*HandleSize); - } - - inline void* allocate(size_t bytes, size_t alignment) override - { - assert(bytes==storage.size()); - return storage.data(); - } - inline void deallocate(void* p, size_t bytes, size_t alignment) override {storage = {};} - - core::vector storage; - }; - auto memRsc = core::make_smart_refctd_ptr(hitHandles.size()+missHandles.size()+callableHandles.size()+1); - { - core::LinearAddressAllocatorST allocator(nullptr,0,0,limits.shaderGroupBaseAlignment,0x7fff0000u); - // Without SER, the arrays for Miss and Closest Hit cannot be null, unless you can guarantee a Ray will never hit geometry or miss - auto copyShaderHandles = [&](const std::span handles, const size_t minCount=1)->SBufferRange - { - SBufferRange range = {.size=hlsl::max(handles.size(),minCount)*handleSizeAligned}; - range.offset = allocator.alloc_addr(range.size,limits.shaderGroupBaseAlignment); - memRsc->storage.resize(core::alignUp(allocator.get_allocated_size(),limits.shaderGroupBaseAlignment)); - uint8_t* out = memRsc->storage.data()+range.offset; - for (const auto& handle : handles) - { - memcpy(out,&handle,HandleSize); - out += handleSizeAligned; - } - for (auto i=minCount; igetRaygen(),1}); - sbt.miss.range = copyShaderHandles(pipeline->getMissHandles()); - // TODO: the material compiler with an RT pipeline backend should give 3 or 4 hitgroups depending on opacity and other funny things - // problem is that due to how TLAS instances and their Geometries call into hitgroups, we need to spam duplicates around the SBT - // also de-dup stuff that has the same hash (array of hitgroups) so two instances can happily point at the same material - sbt.hit.range = copyShaderHandles(pipeline->getHitHandles()); - // TODO: material compiler will give us callables and we need to turn those into materials - sbt.callable.range = copyShaderHandles(pipeline->getCallableHandles(),0); - // TODO: futhermore different rays (NEE vs BxDF) should use different SBTs using big offsets so it becomes a really funny mess - sbt.miss.stride = sbt.hit.stride = sbt.callable.stride = handleSizeAligned; - } - auto& sbtBuff = tmpBuffers.sbts[i]; - sbtBuff = ICPUBuffer::create({ - { - .size=memRsc->storage.size(),.usage=BasicBufferUsages|buffer_usage_e::EUF_SHADER_BINDING_TABLE_BIT - }, - /*.data = */memRsc->storage.data(), - /*.memoryResource = */memRsc - },core::adopt_memory); - sbtBuff->setContentHash(sbtBuff->computeContentHash()); - } - } - - // new cache if none provided - if (!converter) - converter = CAssetConverter::create({.device=device,.optimizer={}}); - - // customized setup - struct MyInputs : CAssetConverter::SInputs - { - // For the GPU Buffers to be directly writeable and so that we don't need a Transfer Queue submit at all - inline uint32_t constrainMemoryTypeBits(const size_t groupCopyID, const IAsset* canonicalAsset, const blake3_hash_t& contentHash, const IDeviceMemoryBacked* memoryBacked) const override - { - assert(memoryBacked); - return memoryBacked->getObjectType()!=IDeviceMemoryBacked::EOT_BUFFER ? (~0u):rebarMemoryTypes; - } - - uint32_t rebarMemoryTypes; - } inputs = {}; - inputs.logger = m_creation.logger.get().get(); - inputs.rebarMemoryTypes = device->getPhysicalDevice()->getDirectVRAMAccessMemoryTypeBits(); - // the allocator needs to be overriden to hand out memory ranges which have already been mapped so that the ReBAR fast-path can kick in - // (multiple buffers can be bound to same memory, but memory can only be mapped once at one place, so Asset Converter can't do it) - struct MyAllocator final : public IDeviceMemoryAllocator - { - ILogicalDevice* getDeviceForAllocations() const override {return device;} - - SAllocation allocate(const SAllocateInfo& info) override - { - auto retval = device->allocate(info); - // map what is mappable by default so ReBAR checks succeed - if (retval.isValid() && retval.memory->isMappable()) - retval.memory->map({.offset=0,.length=info.size}); - return retval; - } - - ILogicalDevice* device; - } myalloc; - myalloc.device = device; - inputs.allocator = &myalloc; - - // assign inputs - { - std::get>(inputs.assets) = tmpBuffers; - std::get>(inputs.assets) = {&tmpTLASes.front().get(),tmpTLASes.size()}; - } - CAssetConverter::SReserveResult reservation = converter->reserve(inputs); - { - bool success = true; - auto check = [&](const CAssetConverter::SInputs::asset_span_t references)->void - { - auto objects = reservation.getGPUObjects(); - auto referenceIt = references.begin(); - for (auto& object : objects) - { - auto* reference = *(referenceIt++); - if (!reference) - continue; - - success = bool(object.value); - if (!success) - { - inputs.logger.log("Failed to convert a CPU object to GPU of type %s!",ILogger::ELL_ERROR,system::to_string(reference->getAssetType())); - return; - } - } - }; - check.template operator()(tmpBuffers); - check.template operator()({&tmpTLASes.front().get(),tmpTLASes.size()}); - if (!success) - return nullptr; - } - - // convert - { - smart_refctd_ptr scratchAlloc; - { - constexpr auto scratchUsages = IGPUBuffer::EUF_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT|IGPUBuffer::EUF_SHADER_DEVICE_ADDRESS_BIT|IGPUBuffer::EUF_STORAGE_BUFFER_BIT; - - constexpr uint16_t MaxAlignment = 256; - constexpr uint64_t MinAllocationSize = 1024; - const auto scratchSize = core::alignUp(hlsl::max(reservation.getMaxASBuildScratchSize(false),MinAllocationSize),MaxAlignment); - - auto scratchBuffer = device->createBuffer({{.size=scratchSize,.usage=scratchUsages}}); - - auto reqs = scratchBuffer->getMemoryReqs(); - reqs.memoryTypeBits &= device->getPhysicalDevice()->getDirectVRAMAccessMemoryTypeBits(); - - auto allocation = device->allocate(reqs,scratchBuffer.get(),IDeviceMemoryAllocation::EMAF_DEVICE_ADDRESS_BIT); - allocation.memory->map({.offset=0,.length=reqs.size}); - - scratchAlloc = make_smart_refctd_ptr( - SBufferRange{0ull,scratchSize,std::move(scratchBuffer)}, - core::allocator(), MaxAlignment, MinAllocationSize - ); - } - - constexpr auto CompBufferCount = 2; - - std::array,CompBufferCount> compBufs = {}; - std::array compBufInfos = {}; - { - constexpr auto RequiredFlags = IGPUCommandPool::CREATE_FLAGS::RESET_COMMAND_BUFFER_BIT|IGPUCommandPool::CREATE_FLAGS::TRANSIENT_BIT; - auto pool = device->createCommandPool(m_creation.computeQueue->getFamilyIndex(),RequiredFlags); - if (!pool || !pool->createCommandBuffers(IGPUCommandPool::BUFFER_LEVEL::PRIMARY, compBufs)) - { - inputs.logger.log("Failed to create Command Buffers for the Compute Queue!",ILogger::ELL_ERROR); - return nullptr; - } - compBufs.front()->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT); - for (auto i=0; icreateSemaphore(0u); - - // TODO: `SIntendedSubmitInfo transfer` as well, because of images - SIntendedSubmitInfo compute = {}; - compute.queue = m_creation.computeQueue; - compute.scratchCommandBuffers = compBufInfos; - compute.scratchSemaphore = { - .semaphore = compSema.get(), - .value = 0u, - .stageMask = PIPELINE_STAGE_FLAGS::ACCELERATION_STRUCTURE_BUILD_BIT|PIPELINE_STAGE_FLAGS::ACCELERATION_STRUCTURE_COPY_BIT|PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT - }; - struct MyParams final : CAssetConverter::SConvertParams - { - inline uint32_t getFinalOwnerQueueFamily(const IGPUBuffer* buffer, const core::blake3_hash_t& createdFrom) override - { - return finalUser; - } - inline uint32_t getFinalOwnerQueueFamily(const IGPUAccelerationStructure* image, const core::blake3_hash_t& createdFrom) override - { - return finalUser; - } - - uint8_t finalUser; - } cvtParam = {}; - cvtParam.utilities = m_creation.utilities.get(); - cvtParam.compute = &compute; - cvtParam.scratchForDeviceASBuild = scratchAlloc.get(); - cvtParam.finalUser = m_creation.graphicsQueue->getFamilyIndex(); - - auto future = reservation.convert(cvtParam); - // release the memory - { - for (auto& tmpTLAS : tmpTLASes) - { - IAsset* const asAsset = tmpTLAS.get(); - IPreHashed::discardDependantsContents({&asAsset,1}); - } - tmpTLASes.clear(); - tmpBuffers = {}; - } - if (future.copy()!=IQueue::RESULT::SUCCESS) - { - inputs.logger.log("Failed to await `CAssetConverter::SReserveResult::convert(...)` submission semaphore!",ILogger::ELL_ERROR); - return nullptr; - } - - - const auto buffers = reservation.getGPUObjects(); - ubo = buffers[0].value; - for (uint8_t i=0; i& stRange)->void - { - stRange.range.buffer = stRange.range.size ? buffer:nullptr; - }; - params.sbts[i].raygen.buffer = buffer; - setSBTBuffer(params.sbts[i].miss); - setSBTBuffer(params.sbts[i].hit); - setSBTBuffer(params.sbts[i].callable); - } - - const bool success = reservation.moveGPUObjects(TLASes); - assert(success); - params.TLAS = TLASes[0].value; - } - } - - // write into DS - { - vector infos; - vector writes; - auto* const ds = params.sceneDS->getDescriptorSet(); - auto addWrite = [&](const uint32_t binding)->void - { - writes.emplace_back() = { - .dstSet = ds, - .binding = binding, - .arrayElement = 0, - .count = 1, - .info = reinterpret_cast(infos.size()) - }; - }; - addWrite(SceneDSBindings::UBO); - infos.push_back(SBufferRange{.offset=0,.size=sizeof(SSceneUniforms),.buffer=std::move(ubo)}); - // TODO: Envmap - { - addWrite(SceneDSBindings::TLASes); - infos.reserve(infos.size()+TLASes.size()); - for (auto& tlas : TLASes) - infos.emplace_back().desc = tlas.value; - } - // TODO: Samplers - // TODO: Sampled Images - // TODO: Envmap PDF - // TODO: Envmap Warp Map - for (auto& write : writes) - write.info = infos.data()+reinterpret_cast(write.info); - device->updateDescriptorSets(writes,{}); - } - -#if 0 - float m_maxAreaLightLuma; - // Resources used for envmap sampling - nbl::core::smart_refctd_ptr m_finalEnvmap; -#endif - - // - if (!params) - { - m_creation.logger.log("Failed to create a scene!",ILogger::ELL_ERROR); - return nullptr; - } - return core::smart_refctd_ptr(new CScene(std::move(params)),core::dont_grab); -} - - -auto CRenderer::render(CSession* session) -> SSubmit -{ - if (!session || !session->isInitialized()) - return {}; - const auto& sessionParams = session->getConstructionParams(); - auto* const device = getDevice(); - - if (m_frameIx>=SCachedConstructionParams::FramesInFlight) - { - const ISemaphore::SWaitInfo cbDonePending[] = - { - { - .semaphore = m_construction.semaphore.get(), - .value = m_frameIx+1-SCachedConstructionParams::FramesInFlight - } - }; - if (device->blockForSemaphores(cbDonePending) != ISemaphore::WAIT_RESULT::SUCCESS) - return {}; - } - const auto resourceIx = m_frameIx % SCachedConstructionParams::FramesInFlight; - - auto* const cb = m_construction.commandBuffers[resourceIx].get(); - cb->getPool()->reset(); - if (!cb->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT)) - return {}; - - const auto* const scene = session->getConstructionParams().scene.get(); - const auto mode = sessionParams.mode; - const auto& sessionResources = session->getActiveResources(); - const auto* const pipeline = scene->getPipeline(mode); - - bool success; - // push constants - { - switch (mode) - { - case CSession::RenderMode::Debug: - { - SDebugPushConstants pc = {sessionResources.currentSensorState}; - success = cb->pushConstants(pipeline->getLayout(),hlsl::ShaderStage::ESS_ALL_RAY_TRACING,0,sizeof(pc),&pc); - break; - } - case CSession::RenderMode::Beauty: - { - SBeautyPushConstants pc = {.sensorDynamics=sessionResources.currentSensorState}; - // TODOs - pc.__16BitData.rrThroughputWeights = hlsl::promote(hlsl::numeric_limits::max); // always pass RR, later LumaConversionCoeffs - pc.__16BitData.maxSppPerDispatch = 3; - success = cb->pushConstants(pipeline->getLayout(),hlsl::ShaderStage::ESS_ALL_RAY_TRACING,0,sizeof(pc),&pc); - break; - } - default: - getLogger().log("Unimplemented RenderMode::%s !",ILogger::ELL_ERROR,system::to_string(mode).c_str()); - return {}; - } - } - - const auto& sessionImmutables = sessionResources.immutables; - // bind pipelines - success = success && cb->bindRayTracingPipeline(pipeline); - { - const IGPUDescriptorSet* sets[2] = {sessionParams.scene->getDescriptorSet(),sessionImmutables.ds.get()}; - success = success && cb->bindDescriptorSets(EPBP_RAY_TRACING,pipeline->getLayout(),0,2,sets); - } - - // barrier against previous usages of accumulation targets (so that RMW cycles sync up properly) - { - constexpr auto raytracingStages = PIPELINE_STAGE_FLAGS::RAY_TRACING_SHADER_BIT; - using image_barrier_t = IGPUCommandBuffer::SPipelineBarrierDependencyInfo::image_barrier_t; - core::vector barr; - { - constexpr image_barrier_t base = { - .barrier = { - .dep = { - // Any of the images can be read by Debug/Presenter, ideally we should be aware of that and inject it here via a Command Graph - // but to keep code decoupled we'll have those subsystems use one more pipeline barrier after their own dispatch - .srcStageMask = raytracingStages, - .srcAccessMask = ACCESS_FLAGS::SHADER_WRITE_BITS, - .dstStageMask = raytracingStages, - .dstAccessMask = ACCESS_FLAGS::SHADER_READ_BITS|ACCESS_FLAGS::SHADER_WRITE_BITS - } - }, - .subresourceRange = {} - }; - barr.reserve(SensorDSBindingCounts::AsSampledImages); - - auto enqueueBarrier = [&barr,base](const CSession::SImageWithViews& img)->void - { - auto& out = barr.emplace_back(base); - out.image = img.image.get(); - out.subresourceRange = { - .aspectMask = IGPUImage::E_ASPECT_FLAGS::EAF_COLOR_BIT, - .levelCount = 1, - .layerCount = out.image->getCreationParameters().arrayLayers - }; - }; - enqueueBarrier(sessionImmutables.sampleCount); - enqueueBarrier(sessionImmutables.rwmcCascades); - enqueueBarrier(sessionImmutables.albedo); - enqueueBarrier(sessionImmutables.normal); - enqueueBarrier(sessionImmutables.motion); - enqueueBarrier(sessionImmutables.mask); - } - success = cb->pipelineBarrier(asset::EDF_NONE,{.imgBarriers=barr}); - } - - const auto renderSize = sessionParams.uniforms.renderSize; - success = success && cb->traceRays(scene->getSBT(mode),renderSize.x,renderSize.y,sessionParams.type!=CSession::sensor_type_e::Env ? 1:6); - - if (success) - return SSubmit(this,cb); - else - return {}; -} - -IQueue::SSubmitInfo::SSemaphoreInfo CRenderer::SSubmit::operator()(std::span extraWaits) -{ - if (!cb || !cb->end()) - return {}; - - const IQueue::SSubmitInfo::SSemaphoreInfo rendered[] = - { - { - .semaphore = renderer->m_construction.semaphore.get(), - .value = ++renderer->m_frameIx, - .stageMask = stageMask - } - }; - const IQueue::SSubmitInfo::SCommandBufferInfo commandBuffers[] = {{.cmdbuf=cb}}; - const IQueue::SSubmitInfo infos[] = - { - { - .waitSemaphores = extraWaits, - .commandBuffers = commandBuffers, - .signalSemaphores = rendered - } - }; - if (renderer->getCreationParams().graphicsQueue->submit(infos)!=IQueue::RESULT::SUCCESS) - { - renderer->m_frameIx--; - return {}; - } - return rendered[0]; -} - -} \ No newline at end of file diff --git a/40_PathTracer/src/renderer/CScene.cpp b/40_PathTracer/src/renderer/CScene.cpp deleted file mode 100644 index 39e58493e..000000000 --- a/40_PathTracer/src/renderer/CScene.cpp +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright (C) 2025-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 "nbl/builtin/hlsl/limits.hlsl" - -#include "renderer/CRenderer.h" - -namespace nbl::this_example -{ -using namespace nbl::core; -using namespace nbl::system; -using namespace nbl::asset; -using namespace nbl::hlsl; -using namespace nbl::video; - -// -smart_refctd_ptr CScene::createSession(const CSession::SCreationParams& _params) -{ - if (!_params) - return nullptr; - - const auto& constants = _params.sensor->constants; - const auto& dynDefaults = _params.sensor->dynamicDefaults; - const auto& mutDefaults = _params.sensor->mutableDefaults; - const auto& raygen = mutDefaults.raygen; - - CSession::SConstructionParams params = {std::move(_params)}; - params.scene = smart_refctd_ptr(this); - params.cropOffsets = {mutDefaults.cropOffsetX,mutDefaults.cropOffsetY}; - params.cropResolution = {mutDefaults.cropWidth,mutDefaults.cropHeight}; - params.type = raygen.getType(); - - const uint16_t2 renderSize(constants.width,constants.height); - assert(all(params.cropOffsets(mutDefaults.maxPathDepth,1,m_construction.renderer->getConstructionParams().getSequenceMaxPathDepth()); - const uint16_t russianRouletteDepth = hlsl::clamp(mutDefaults.russianRouletteDepth,1,maxPathDepth); - params.uniforms = { - .rcpPixelSize = promote(1.f)/float32_t2(renderSize), - .splatting = hlsl::rwmc::SPackedSplattingParameters::create(mutDefaults.cascadeLuminanceBase,mutDefaults.cascadeLuminanceStart,constants.cascadeCount), - .renderSize = renderSize, - .lastPathDepth = static_cast(maxPathDepth-1), - .lastNoRussianRouletteDepth = static_cast(russianRouletteDepth-1), - .lastCascadeIndex = static_cast(constants.cascadeCount-1), - .hideEnvironment = mutDefaults.hideEnvironment - }; - } - - // - params.initDynamics = { - .invView = mutDefaults.absoluteTransform, - .ndcToRay = float32_t2x3(mutDefaults.raygen), - .nearClip = mutDefaults.nearClip, - .tMax = mutDefaults.farClip, - .minSPP = core::min(dynDefaults.samplesNeeded,16), // for later enhancement - .maxSPP = dynDefaults.samplesNeeded - }; - - // - { - const auto reciprocalKappa = 1.f/dynDefaults.kappa; - params.initResolveConstants = { - .rwmc = { - .initialEmin = dynDefaults.Emin, - .reciprocalBase = 1.f/mutDefaults.cascadeLuminanceBase, - .reciprocalKappa = reciprocalKappa, - .colorReliabilityFactor = hlsl::mix(mutDefaults.cascadeLuminanceBase,1.f,reciprocalKappa) - }, - .cascadeCount = constants.cascadeCount - }; - } - - return smart_refctd_ptr(new CSession(std::move(params)),dont_grab); -} - -} \ No newline at end of file diff --git a/40_PathTracer/src/renderer/CSession.cpp b/40_PathTracer/src/renderer/CSession.cpp deleted file mode 100644 index 144d3a495..000000000 --- a/40_PathTracer/src/renderer/CSession.cpp +++ /dev/null @@ -1,375 +0,0 @@ -// Copyright (C) 2025-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 "renderer/CRenderer.h" - -namespace nbl::this_example -{ -using namespace nbl::core; -using namespace nbl::system; -using namespace nbl::asset; -using namespace nbl::hlsl; -using namespace nbl::video; - -// -bool CSession::init(SIntendedSubmitInfo& info) -{ - auto renderer = m_params.scene->getRenderer(); - auto& logger = renderer->getCreationParams().logger; - auto device = renderer->getDevice(); - - auto& immutables = m_active.immutables; - - // create the descriptors - core::vector infos; - core::vector writes; - { - auto addWrite = [&](const uint32_t binding, IGPUDescriptorSet::SDescriptorInfo&& info)->void - { - writes.emplace_back() = { - .binding = binding, - .arrayElement = 0, - .count = 1, - .info = reinterpret_cast(infos.size()) - }; - infos.push_back(std::move(info)); - }; - - // - auto dedicatedAllocate = [&](IDeviceMemoryBacked* memBacked, const std::string_view debugName)->bool - { - if (!memBacked) - { - logger.log("Failed to create Sensor \"%s\"'s \"%s\" in CSession::init()",ILogger::ELL_ERROR,m_params.name.c_str(),debugName.data()); - return false; - } - memBacked->setObjectDebugName(debugName.data()); - - auto mreqs = memBacked->getMemoryReqs(); - mreqs.memoryTypeBits &= device->getPhysicalDevice()->getDeviceLocalMemoryTypeBits(); - using flags_e = IDeviceMemoryAllocation::E_MEMORY_ALLOCATE_FLAGS; - core::bitflag flags = flags_e::EMAF_NONE; - if (memBacked->getObjectType()==IDeviceMemoryBacked::E_OBJECT_TYPE::EOT_BUFFER && - static_cast(memBacked)->getCreationParams().usage.hasFlags(IGPUBuffer::E_USAGE_FLAGS::EUF_SHADER_DEVICE_ADDRESS_BIT)) - flags |= flags_e::EMAF_DEVICE_ADDRESS_BIT; - if (!device->allocate(mreqs,memBacked,flags).isValid()) - { - logger.log("Could not allocate memory for Sensor \"%s\"'s \"%s\" in CSession::init()",ILogger::ELL_ERROR,m_params.name.c_str(),debugName.data()); - return false; - } - return true; - }; - - // create UBO - { - IGPUBuffer::SCreationParams params = {}; - params.size = sizeof(m_params.uniforms); - using usage_flags_e = IGPUBuffer::E_USAGE_FLAGS; - params.usage = usage_flags_e::EUF_UNIFORM_BUFFER_BIT | usage_flags_e::EUF_TRANSFER_DST_BIT | usage_flags_e::EUF_INLINE_UPDATE_VIA_CMDBUF; - auto ubo = device->createBuffer(std::move(params)); - if (!dedicatedAllocate(ubo.get(),"Sensor UBO")) - return false; - // pipeline barrier in `reset` will take care of sync for this - info.getCommandBufferForRecording()->cmdbuf->updateBuffer({.size=sizeof(m_params.uniforms),.buffer=ubo},&m_params.uniforms); - addWrite(SensorDSBindings::UBO,SBufferRange{.offset=0,.size=sizeof(m_params.uniforms),.buffer=ubo}); - } - - const auto allowedFormatUsages = device->getPhysicalDevice()->getImageFormatUsagesOptimalTiling(); - auto createImage = [&]( - const std::string_view debugName, const E_FORMAT format, const uint16_t2 resolution, const uint16_t layers, - const IGPUImage::E_CREATE_FLAGS extraFlags=IGPUImage::E_CREATE_FLAGS::ECF_NONE, std::bitset viewFormats={}, - const IGPUImage::E_USAGE_FLAGS extraUsages=IGPUImage::E_USAGE_FLAGS::EUF_STORAGE_BIT|IGPUImage::E_USAGE_FLAGS::EUF_SAMPLED_BIT - ) -> SImageWithViews - { - SImageWithViews retval = {}; - { - { - IGPUImage::SCreationParams params = {}; - params.type = IGPUImage::E_TYPE::ET_2D; - params.samples = IGPUImage::E_SAMPLE_COUNT_FLAGS::ESCF_1_BIT; - params.format = format; - params.extent.width = resolution[0]; - params.extent.height = resolution[1]; - params.extent.depth = 1; - params.mipLevels = 1; - params.arrayLayers = layers; - params.flags |= extraFlags; - using image_usage_e = IGPUImage::E_USAGE_FLAGS; - params.usage = image_usage_e::EUF_TRANSFER_DST_BIT|extraUsages; - viewFormats.set(format); - if (viewFormats.count()>1) - { - params.flags |= IGPUImage::E_CREATE_FLAGS::ECF_MUTABLE_FORMAT_BIT; - params.flags |= IGPUImage::E_CREATE_FLAGS::ECF_EXTENDED_USAGE_BIT; - } - params.viewFormats = viewFormats; - retval.image = device->createImage(std::move(params)); - if (!dedicatedAllocate(retval.image.get(),debugName)) - return {}; - } - const auto& params = retval.image->getCreationParameters(); - for (uint8_t f=0; f(f); - const auto thisFormatUsages = static_cast>(allowedFormatUsages[viewFormat]); - auto view = device->createImageView({ - .subUsages = retval.image->getCreationParameters().usage & thisFormatUsages, - .image = retval.image, - .viewType = IGPUImageView::E_TYPE::ET_2D_ARRAY, - .format = viewFormat - }); - string viewDebugName = string(debugName)+" "+to_string(viewFormat)+" View"; - if (!view) - { - logger.log("Failed to create Sensor \"%s\"'s \"%s\" in CSession::init()",ILogger::ELL_ERROR,m_params.name.c_str(),viewDebugName.c_str()); - return {}; - } - view->setObjectDebugName(viewDebugName.c_str()); - retval.views[viewFormat] = std::move(view); - } - } - return retval; - }; - auto addImageWrite = [&](const uint32_t binding, const smart_refctd_ptr& view)->void - { - IGPUDescriptorSet::SDescriptorInfo info = {}; - info.desc = view; - info.info.image.imageLayout = IGPUImage::LAYOUT::GENERAL; - addWrite(binding,std::move(info)); - }; - - // create Scramble Key image - { - const auto layers = 1u; // for now, until the crazy Heitz 2019 thing, or if we choose to save 8 bytes in ray payload and read a premade scramble at every depth - immutables.scrambleKey = createImage("Scramble Dimension Keys",E_FORMAT::EF_R32G32_UINT,hlsl::promote(SSensorUniforms::ScrambleKeyTextureSize),layers); - } - // - auto scrambleKeyView = immutables.scrambleKey.views[E_FORMAT::EF_R32G32_UINT]; - addImageWrite(SensorDSBindings::ScrambleKey,scrambleKeyView); - - // create the render-sized images - auto createScreenSizedImage = [&](const std::string_view debugName, const E_FORMAT format, uint16_t layers=1, Args&&... args)->SImageWithViews - { - using create_flags_e = IGPUImage::E_CREATE_FLAGS; - create_flags_e flags = create_flags_e::ECF_NONE; - if (m_params.type==sensor_type_e::Env) - { - layers *= 6; - flags = IGPUImage::E_CREATE_FLAGS::ECF_CUBE_COMPATIBLE_BIT; - } - return createImage(debugName,format,m_params.uniforms.renderSize,layers,flags,std::forward(args)...); - }; - immutables.sampleCount = createScreenSizedImage("Current Sample Count",E_FORMAT::EF_R16_UINT); - auto sampleCountView = immutables.sampleCount.views[E_FORMAT::EF_R16_UINT]; - addImageWrite(SensorDSBindings::SampleCount,sampleCountView); - immutables.rwmcCascades = createScreenSizedImage("RWMC Cascades",E_FORMAT::EF_R32G32_UINT,m_params.uniforms.lastCascadeIndex+1,std::bitset().set(E_FORMAT::EF_R16G16B16A16_SFLOAT)); - addImageWrite(SensorDSBindings::RWMCCascades,immutables.rwmcCascades.views[E_FORMAT::EF_R32G32_UINT]); - immutables.beauty = createScreenSizedImage("Beauty",E_FORMAT::EF_E5B9G9R9_UFLOAT_PACK32,1,std::bitset().set(E_FORMAT::EF_R32_UINT)); - addImageWrite(SensorDSBindings::Beauty,immutables.beauty.views[E_FORMAT::EF_R32_UINT]); - immutables.albedo = createScreenSizedImage("Albedo",E_FORMAT::EF_A2B10G10R10_UNORM_PACK32); - auto albedoView = immutables.albedo.views[E_FORMAT::EF_A2B10G10R10_UNORM_PACK32]; - addImageWrite(SensorDSBindings::Albedo,albedoView); - // Normal and Albedo should have used `EF_A2B10G10R10_SNORM_PACK32` but Nvidia doesn't support - immutables.normal = createScreenSizedImage("Normal",E_FORMAT::EF_A2B10G10R10_UNORM_PACK32); - auto normalView = immutables.normal.views[E_FORMAT::EF_A2B10G10R10_UNORM_PACK32]; - addImageWrite(SensorDSBindings::Normal,normalView); - immutables.motion = createScreenSizedImage("Motion",E_FORMAT::EF_A2B10G10R10_UNORM_PACK32); - auto motionView = immutables.motion.views[E_FORMAT::EF_A2B10G10R10_UNORM_PACK32]; - addImageWrite(SensorDSBindings::Motion,motionView); - immutables.mask = createScreenSizedImage("Mask",E_FORMAT::EF_R16_UNORM); - auto maskView = immutables.mask.views[E_FORMAT::EF_R16_UNORM]; - addImageWrite(SensorDSBindings::Mask,maskView); - // shorthand a little bit - addImageWrite(SensorDSBindings::AsSampledImages,scrambleKeyView); - writes.back().count = SensorDSBindingCounts::AsSampledImages; - { - const auto oldSize = infos.size(); - infos.resize(oldSize +SensorDSBindingCounts::AsSampledImages,infos.back()); - const auto viewInfos = infos.data()+oldSize-1; - using index_e = SensorDSBindings::SampledImageIndex; - viewInfos[uint8_t(index_e::ScrambleKey)].desc = scrambleKeyView; - viewInfos[uint8_t(index_e::SampleCount)].desc = sampleCountView; - viewInfos[uint8_t(index_e::RWMCCascades)].desc = immutables.rwmcCascades.views[E_FORMAT::EF_R16G16B16A16_SFLOAT]; - viewInfos[uint8_t(index_e::Beauty)].desc = immutables.beauty.views[E_FORMAT::EF_E5B9G9R9_UFLOAT_PACK32]; - viewInfos[uint8_t(index_e::Albedo)].desc = albedoView; - viewInfos[uint8_t(index_e::Normal)].desc = normalView; - viewInfos[uint8_t(index_e::Motion)].desc = motionView; - viewInfos[uint8_t(index_e::Mask)].desc = maskView; - } - } - - // create descriptor set - { - auto layout = renderer->getConstructionParams().sensorDSLayout; - auto pool = device->createDescriptorPoolForDSLayouts(IDescriptorPool::E_CREATE_FLAGS::ECF_UPDATE_AFTER_BIND_BIT,{&layout.get(),1}); - immutables.ds = pool->createDescriptorSet(std::move(layout)); - const char* DebugName = "Sensor Descriptor Set"; - if (!immutables.ds) - { - logger.log("Failed to create Sensor \"%s\"'s \"%s\" in CSession::init()",ILogger::ELL_ERROR,m_params.name.c_str(),DebugName); - return false; - } - immutables.ds->setObjectDebugName(DebugName); - for (auto& write : writes) - { - write.dstSet = immutables.ds.get(); - write.info = infos.data()+reinterpret_cast(write.info); - } - if (!device->updateDescriptorSets(writes,{})) - { - logger.log("Failed to write Sensor \"%s\"'s \"%s\" in CSession::init()",ILogger::ELL_ERROR,m_params.name.c_str(),DebugName); - return false; - } - } - - bool success = immutables; - - // transition image layouts instead of barriering in Reset - if (success) - { - // slam the barriers as big as possible, it wont happen frequently - using image_barrier_t = IGPUCommandBuffer::SPipelineBarrierDependencyInfo::image_barrier_t; - core::vector barr; - { - constexpr image_barrier_t base = { - .barrier = { - .dep = { - .dstStageMask = PIPELINE_STAGE_FLAGS::RAY_TRACING_SHADER_BIT, - .dstAccessMask = ACCESS_FLAGS::SHADER_READ_BITS|ACCESS_FLAGS::SHADER_WRITE_BITS - } - }, - .subresourceRange = {}, - .newLayout = IGPUImage::LAYOUT::GENERAL - }; - barr.reserve(SensorDSBindingCounts::AsSampledImages); - - auto enqueueBarrier = [&barr,base](const SImageWithViews& img)->void - { - auto& out = barr.emplace_back(base); - out.image = img.image.get(); - out.subresourceRange = { - .aspectMask = IGPUImage::E_ASPECT_FLAGS::EAF_COLOR_BIT, - .levelCount = 1, - .layerCount = out.image->getCreationParameters().arrayLayers - }; - }; - enqueueBarrier(immutables.sampleCount); - enqueueBarrier(immutables.beauty); // TODO: who will clear this? Resolver or denoiser? - enqueueBarrier(immutables.rwmcCascades); - enqueueBarrier(immutables.albedo); - enqueueBarrier(immutables.normal); - enqueueBarrier(immutables.motion); - enqueueBarrier(immutables.mask); - } - success = info.getCommandBufferForRecording()->cmdbuf->pipelineBarrier(asset::EDF_NONE,{.imgBarriers=barr}); - } - - if (!success || !reset(m_params.initDynamics,info)) - { - logger.log("Could not Init Session for sensor \"%s\" failed to reset!",ILogger::ELL_ERROR,m_params.name.c_str()); - deinit(); - return false; - } - - return true; -} - -bool CSession::reset(const SSensorDynamics& newVal, video::SIntendedSubmitInfo& info) -{ - if (!isInitialized()) - return false; - - auto* const renderer = m_params.scene->getRenderer(); - auto* const device = renderer->getDevice(); - const auto& scrambleImage = m_active.immutables.scrambleKey.image; - const auto& params = scrambleImage->getCreationParameters(); - const IGPUImage::SSubresourceRange subresources = { - .aspectMask = IGPUImage::E_ASPECT_FLAGS::EAF_COLOR_BIT, - .levelCount = 1, - .layerCount = params.arrayLayers - }; - - bool success = true; - constexpr auto RegularScrambleAccesses = PIPELINE_STAGE_FLAGS::RAY_TRACING_SHADER_BIT|PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT; - // slam the barriers as big as possible, it wont happen frequently - using image_barrier_t = IGPUCommandBuffer::SPipelineBarrierDependencyInfo::image_barrier_t; - { - const image_barrier_t before = { - .barrier = { - .dep = { - .srcStageMask = RegularScrambleAccesses, - .srcAccessMask = ACCESS_FLAGS::NONE, // because we don't care about reading previously written values - .dstStageMask = PIPELINE_STAGE_FLAGS::COPY_BIT, - .dstAccessMask = ACCESS_FLAGS::TRANSFER_WRITE_BIT - } - }, - .image = scrambleImage.get(), - .subresourceRange = subresources, - .newLayout = IGPUImage::LAYOUT::GENERAL - }; - success = success && info.getCommandBufferForRecording()->cmdbuf->pipelineBarrier(asset::EDF_NONE,{.imgBarriers={&before,1}}); - } - - // fill scramble with noise - { - auto* const utils = renderer->getCreationParams().utilities.get(); - core::vector data(params.extent.width*params.extent.height*params.arrayLayers); - { - core::RandomSampler rng(0xbadc0ffeu); - for (auto& el : data) - el = {rng.nextSample(),rng.nextSample()}; - } - const ICPUImage::SBufferCopy region = { - .bufferRowLength = params.extent.width, - .bufferImageHeight = params.extent.height, - .imageSubresource = { - .aspectMask = IImage::E_ASPECT_FLAGS::EAF_COLOR_BIT, - .mipLevel = 0u, - .baseArrayLayer = 0u, - .layerCount = params.arrayLayers - }, - .imageExtent = params.extent - }; - utils->updateImageViaStagingBuffer(info,data.data(),params.format,scrambleImage.get(),IGPUImage::LAYOUT::GENERAL,{®ion,1}); - } - - { - const image_barrier_t after = { - .barrier = { - .dep = { - .srcStageMask = PIPELINE_STAGE_FLAGS::COPY_BIT, - .srcAccessMask = ACCESS_FLAGS::TRANSFER_WRITE_BIT, - .dstStageMask = RegularScrambleAccesses, - .dstAccessMask = ACCESS_FLAGS::SHADER_READ_BITS|ACCESS_FLAGS::SHADER_WRITE_BITS - } - }, - .image = scrambleImage.get(), - .subresourceRange = subresources, - .newLayout = IGPUImage::LAYOUT::GENERAL - }; - success = success && info.getCommandBufferForRecording()->cmdbuf->pipelineBarrier(asset::EDF_NONE,{.imgBarriers={&after,1}}); - } - - if (success) - { - m_active.currentSensorState = newVal; - m_active.currentSensorState.keepAccumulating = false; - m_active.prevSensorState = m_active.currentSensorState; - } - return success; -} - -bool CSession::update(const SSensorDynamics& newVal) -{ - if (!isInitialized()) - return false; - - m_active.prevSensorState = m_active.currentSensorState; - m_active.currentSensorState = newVal; - // TODO: reset m_framesDispatched to 0 every time camera moves considerable amount - m_active.currentSensorState.keepAccumulating = true; - return true; -} - -} \ No newline at end of file diff --git a/40_PathTracer/src/renderer/present/CWindowPresenter.cpp b/40_PathTracer/src/renderer/present/CWindowPresenter.cpp deleted file mode 100644 index 274f3ae35..000000000 --- a/40_PathTracer/src/renderer/present/CWindowPresenter.cpp +++ /dev/null @@ -1,301 +0,0 @@ -// Copyright (C) 2025-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 "renderer/present/CWindowPresenter.h" -#include "renderer/shaders/session.hlsl" - -namespace nbl::this_example -{ -using namespace nbl::core; -using namespace nbl::system; -using namespace nbl::asset; -using namespace nbl::hlsl; -using namespace nbl::ui; -using namespace nbl::video; - -constexpr auto SessionImageWritingStages = PIPELINE_STAGE_FLAGS::COPY_BIT|PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT|PIPELINE_STAGE_FLAGS::RAY_TRACING_SHADER_BIT; - -constexpr IGPURenderpass::SCreationParams::SSubpassDependency CWindowPresenter::Dependencies[3] = -{ - { - .srcSubpass = IGPURenderpass::SCreationParams::SSubpassDependency::External, - .dstSubpass = 0, - .memoryBarrier = - { - // would we need some bits here to sync against the acquire? https://github.com/KhronosGroup/Vulkan-ValidationLayers/issues/12066#issuecomment-4222646404 - .srcStageMask = SessionImageWritingStages, - .srcAccessMask = ACCESS_FLAGS::TRANSFER_WRITE_BIT|ACCESS_FLAGS::SHADER_WRITE_BITS, - // fragment shader that draws them, also barrier the CLEAR (or potential CLEAR with DONT_CARE) against the layout transition - .dstStageMask = PIPELINE_STAGE_FLAGS::FRAGMENT_SHADER_BIT|PIPELINE_STAGE_FLAGS::COLOR_ATTACHMENT_OUTPUT_BIT, - .dstAccessMask = ACCESS_FLAGS::SAMPLED_READ_BIT|ACCESS_FLAGS::COLOR_ATTACHMENT_WRITE_BIT - } - }, - { - .srcSubpass = 0, - .dstSubpass = IGPURenderpass::SCreationParams::SSubpassDependency::External, - .memoryBarrier = - { - // the output to swapchain image - .srcStageMask = PIPELINE_STAGE_FLAGS::COLOR_ATTACHMENT_OUTPUT_BIT, - .srcAccessMask = ACCESS_FLAGS::COLOR_ATTACHMENT_WRITE_BIT, - // we only worry about next compute dispatch not overwriting the image we're presenting - .dstStageMask = SessionImageWritingStages, - // but there are no writes from present to make available to it - .dstAccessMask = ACCESS_FLAGS::NONE - // swapchain present of image index I synchronises with the next acquire of image I so no need to worry about the reuse of that - // note that there's no extra destination stages or accesses because they're not needed for a swapchain present - } - }, - IGPURenderpass::SCreationParams::DependenciesEnd -}; - -// -smart_refctd_ptr CWindowPresenter::create(SCreationParams&& _params) -{ - if (!_params) - { - _params.logger.log("`CWindowPresenter::SCreationParams` are invalidl!",ILogger::ELL_ERROR); - return nullptr; - } - CWindowPresenter::SConstructorParams params = {std::move(_params),std::move(_params)}; - - { - const auto& primDpyInfo = params.winMgr->getPrimaryDisplayInfo(); - // subtract window border/decoration elements - params.maxResolution = hlsl::max(int32_t2(primDpyInfo.resX,primDpyInfo.resY)-int32_t2(32,32),int32_t2(0,0)); - // we add an additional constraint that any dimension of maxResolution cannot be less than any dimension of minResolution - // e.g. max resolution Height cannot be less than min resolution width - if (hlsl::any(hlsl::less()(params.maxResolution.xxyy,params.minResolution.xyxy))) - { - params.logger.log( - "`CWindowPresenter::create` desktop resolution must allow for at least a %d x %d window!", - ILogger::ELL_ERROR,params.minResolution.x,params.minResolution.y - ); - return nullptr; - } - params.aspectRatioRange[0] = float64_t(params.minResolution.x)/float64_t(params.maxResolution.y); - params.aspectRatioRange[1] = float64_t(params.maxResolution.x)/float64_t(params.minResolution.y); - } - - // create the window - smart_refctd_ptr window; - { - IWindow::SCreationParams winParams = {}; - winParams.width = 64; - winParams.height = 64; - winParams.x = 32; - winParams.y = 32; - winParams.flags = IWindow::ECF_HIDDEN|IWindow::ECF_BORDERLESS|IWindow::ECF_RESIZABLE; - winParams.windowCaption = _params.initialWindowCaption; - winParams.callback = std::move(_params.callback); - window = params.winMgr->createWindow(std::move(winParams)); - } - if (!window) - { - params.logger.log("`CWindowPresenter::create` failed to create a window!",ILogger::ELL_ERROR); - return nullptr; - } - params.window = window.get(); - params.cursorControl = window->getCursorControl(); - - // create surface - { - auto surface = CSurfaceVulkanWin32::create(std::move(_params.api),move_and_static_cast(window)); - params.surface = surface_t::create(std::move(surface)); - } - if (!params.surface) - { - params.logger.log("`CWindowPresenter::create` failed to create a surface!",ILogger::ELL_ERROR); - return nullptr; - } - - return smart_refctd_ptr(new CWindowPresenter(std::move(params)),dont_grab); -} - -bool CWindowPresenter::init_impl(CRenderer* renderer) -{ - auto& logger = IPresenter::getCreationParams().logger; - auto* device = renderer->getDevice(); - - // create swapchain and its resources (renderpass, etc.) - { - ISurface* const tmp = getSurface(); - ISwapchain::SCreationParams swapchainParams = {.surface=smart_refctd_ptr(tmp)}; - if (!swapchainParams.deduceFormat(device->getPhysicalDevice())) - { - logger.log("Could not choose a Surface Format for the Swapchain!",ILogger::ELL_ERROR); - return false; - } - - auto scResources = std::make_unique(device,swapchainParams.surfaceFormat.format,Dependencies,IGPURenderpass::LOAD_OP::DONT_CARE); - if (!scResources || !scResources->getRenderpass()) - { - logger.log("Failed to create Renderpass!",ILogger::ELL_ERROR); - return false; - } - - if (!m_construction.surface->init(renderer->getCreationParams().graphicsQueue,std::move(scResources),swapchainParams.sharedParams)) - { - logger.log("Could not create Window & Surface or initialize the Surface!",ILogger::ELL_ERROR); - return false; - } - } - - // - auto* const assMan = IPresenter::getCreationParams().assMan.get(); - - // present pipeline layout - smart_refctd_ptr layout; - { - const SPushConstantRange pcRange[] = { - {.stageFlags=ShaderStage::ESS_FRAGMENT,.offset=0,.size=sizeof(m_pushConstants)} - }; - if (!(layout=device->createPipelineLayout(pcRange,renderer->getConstructionParams().sensorDSLayout))) - { - logger.log("`CWindowPresenter::create` failed to create Pipeline Layout!",ILogger::ELL_ERROR); - return false; - } - } - - // present pipeline - if (auto shader=renderer->loadPrecompiledShader<"present_default">(assMan,device,logger.get().get()); shader) - { - const IGPUPipelineBase::SShaderSpecInfo fragSpec = { - .shader = shader.get(), - .entryPoint = "present_default" - }; - - ext::FullScreenTriangle::ProtoPipeline fsTriProtoPln(assMan, device, logger.get().get()); - if (!fsTriProtoPln) { logger.log("`CWindowPresenter::create` failed to create Full Screen Triangle protopipeline or load its vertex shader!",ILogger::ELL_ERROR); return false; } - m_present = fsTriProtoPln.createPipeline(fragSpec, layout.get(), getRenderpass()); - - if (!m_present) - logger.log("`CWindowPresenter::create` failed to create Graphics Pipeline!",ILogger::ELL_ERROR); - } - else - { - logger.log("`CWindowPresenter::create` failed to load shader!",ILogger::ELL_ERROR); - return false; - } - - return bool(m_present); -} - -auto CWindowPresenter::acquire_impl(const CSession* session, ISemaphore::SWaitInfo* p_currentImageAcquire) -> clock_t::time_point -{ - auto expectedPresent = clock_t::time_point::min(); // invalid value - if (!session) - return expectedPresent; - const auto& sessionParams = session->getConstructionParams(); - m_pushConstants.isCubemap = sessionParams.type==CSession::sensor_type_e::Env; - - const auto maxResolution = m_construction.maxResolution; - uint16_t2 targetResolution = m_pushConstants.isCubemap ? maxResolution:sessionParams.uniforms.renderSize; - if (m_pushConstants.isCubemap) - { - // TODO: build default perspective projection matrix given aspect ratio and smaller axis (or diagonal) FOV of the viewer -// m_pushConstants.cubemap.invProjView = ; - } - else - { - m_pushConstants.regular._min = float32_t2(sessionParams.cropOffsets)*sessionParams.uniforms.rcpPixelSize; - m_pushConstants.regular._max = float32_t2(sessionParams.cropResolution+sessionParams.cropOffsets)*sessionParams.uniforms.rcpPixelSize; - const double originalAspectRatio = float64_t(targetResolution.x)/float64_t(targetResolution.y); - // prevent extreme window size - const auto minResolution = m_creation.minResolution; - double scaleDown = 1.0; - for (uint8_t i=0; i<2; i++) - scaleDown = hlsl::min(float64_t(maxResolution[i])/float64_t(targetResolution[i]),scaleDown); - targetResolution = float64_t2(targetResolution)*scaleDown; - // pad artificially - m_pushConstants.regular.scale = {1,1}; - for (uint8_t i=0; i<2; i++) - { - const auto tmp = float64_t(minResolution[i])/float64_t(targetResolution[i]); - if (tmp>1.0) - targetResolution[i] = minResolution[i]; - } - // pad with darkness on the dimension thats too big - const double newAspectRatio = float64_t(targetResolution.x)/float64_t(targetResolution.y); - if (newAspectRatio>originalAspectRatio) - m_pushConstants.regular.scale[1] *= newAspectRatio/originalAspectRatio; - else - m_pushConstants.regular.scale[0] *= originalAspectRatio/newAspectRatio; - // `CWindowPresenter::create` aspect ratio ranges and min/max relationships help us stay valid - assert(all(minResolution<=targetResolution)&&all(targetResolution<=maxResolution)); - } - - // handle session resolution change - auto& winMgr = m_creation.winMgr; - auto* const window = m_construction.window; - if (const uint16_t2 currentResolution={window->getWidth(),window->getHeight()}; currentResolution!=targetResolution) - { - if (!winMgr->setWindowSize(window,targetResolution.x,targetResolution.y)) - return expectedPresent; - m_construction.surface->recreateSwapchain(); - } - if (window->isHidden()) - winMgr->show(window); - - m_pushConstants.layer = 0; // TODO: cubemaps and RWMC debug - m_pushConstants.imageIndex = uint8_t(SensorDSBindings::SampledImageIndex::RWMCCascades); - - auto acquireResult = m_construction.surface->acquireNextImage(); - *p_currentImageAcquire = {.semaphore=acquireResult.semaphore,.value=acquireResult.acquireCount}; - m_currentImageIndex = acquireResult.imageIndex; - if (!acquireResult) - return expectedPresent; - - // TODO: Do this properly with present timing extension and a better oracle - expectedPresent = clock_t::now() + std::chrono::microseconds(16666); - - return expectedPresent; -} - -bool CWindowPresenter::beginRenderpass_impl() -{ - auto* const scRes = getSwapchainResources(); - auto* const framebuffer = scRes->getFramebuffer(m_currentImageIndex); - const uint16_t2 resolution = { framebuffer->getCreationParameters().width,framebuffer->getCreationParameters().height}; - - auto* const cb = getCurrentCmdBuffer(); - bool success = cb->beginDebugMarker("Present"); - const SViewport viewport[] = {{ - .x = 0u, .y = 0u, - .width = static_cast(resolution.x), - .height = static_cast(resolution.y), - .minDepth = 1.f, .maxDepth = 0.f - }}; - success = success && cb->setViewport(viewport,0); - { - const VkRect2D defaultScisors[] = {{ - .offset = {static_cast(viewport->x), static_cast(viewport->y)}, - .extent = {resolution.x,resolution.y} - }}; - success = success && cb->setScissor(defaultScisors); - const VkRect2D currentRenderArea = {.offset = {0,0}, .extent = defaultScisors->extent}; - const IGPUCommandBuffer::SRenderpassBeginInfo info = - { - .framebuffer = framebuffer, - .colorClearValues = nullptr, - .depthStencilClearValues = nullptr, - .renderArea = currentRenderArea - }; - success = success && cb->beginRenderPass(info,IGPUCommandBuffer::SUBPASS_CONTENTS::INLINE); - } - - success = success && cb->bindGraphicsPipeline(m_present.get()); - - const auto* layout = m_present->getLayout(); - { - const auto* ds = getCurrentSessionDS(); - success = success && cb->bindDescriptorSets(EPBP_GRAPHICS,layout,0,1u,&ds); - } - success = success && cb->pushConstants(layout,ShaderStage::ESS_FRAGMENT,0,sizeof(m_pushConstants),&m_pushConstants); - ext::FullScreenTriangle::recordDrawCall(cb); - - success = success && cb->endDebugMarker(); - return success; -} - -} diff --git a/40_PathTracer/src/renderer/resolve/CBasicRWMCResolver.cpp b/40_PathTracer/src/renderer/resolve/CBasicRWMCResolver.cpp deleted file mode 100644 index 299013c0a..000000000 --- a/40_PathTracer/src/renderer/resolve/CBasicRWMCResolver.cpp +++ /dev/null @@ -1,120 +0,0 @@ -// Copyright (C) 2025-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 "renderer/resolve/CBasicRWMCResolver.h" - -namespace nbl::this_example -{ -using namespace nbl::core; -using namespace nbl::system; -using namespace nbl::asset; -using namespace nbl::hlsl; -using namespace nbl::ui; -using namespace nbl::video; - -// -smart_refctd_ptr CBasicRWMCResolver::create(SCreationParams&& _params) -{ - auto logger = _params.renderer->getLogger(); - if (!_params) - { - logger.log("`CBasicRWMCResolver::SCreationParams` are invalid!",ILogger::ELL_ERROR); - return nullptr; - } - CBasicRWMCResolver::SConstructorParams params = {std::move(_params)}; - - auto* const device = _params.renderer->getDevice(); - { - const SPushConstantRange pcRange[] = { - {.stageFlags=ShaderStage::ESS_COMPUTE,.offset=0,.size=sizeof(SResolveConstants)} - }; - if (!(params.layout=device->createPipelineLayout(pcRange,_params.renderer->getConstructionParams().sensorDSLayout))) - { - logger.log("`CBasicRWMCResolver::create` failed to create Pipeline Layout!",ILogger::ELL_ERROR); - return nullptr; - } - } - - // TODO: create all the pipelines! - - return smart_refctd_ptr(new CBasicRWMCResolver(std::move(params)),dont_grab); -} - -bool CBasicRWMCResolver::changeSession_impl() -{ - return true; -} - -bool CBasicRWMCResolver::resolve(video::IGPUCommandBuffer* cb, video::IGPUBuffer* scratch) -{ - if (!m_activeSession || !cb) - return false; - - switch (m_activeSession->getConstructionParams().mode) - { - case CSession::RenderMode::Previs: [[fallthrough]]; - case CSession::RenderMode::Debug: - return true; // do nothing - case CSession::RenderMode::Beauty: - break; - default: - return false; - } - - bool success = true; - if (success) - { - constexpr auto raytracingStages = PIPELINE_STAGE_FLAGS::RAY_TRACING_SHADER_BIT; - constexpr auto firstResolveStage = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT; - - using image_barrier_t = IGPUCommandBuffer::SPipelineBarrierDependencyInfo::image_barrier_t; - core::vector barr; - { - constexpr image_barrier_t base = { - .barrier = { - .dep = { - .srcStageMask = raytracingStages, - .srcAccessMask = ACCESS_FLAGS::SHADER_WRITE_BITS, - .dstStageMask = firstResolveStage, - .dstAccessMask = ACCESS_FLAGS::SHADER_READ_BITS - } - }, - .subresourceRange = {}, - .newLayout = IGPUImage::LAYOUT::GENERAL - }; - barr.reserve(4); - - auto enqueueBarrier = [&barr,base](const CSession::SImageWithViews& img)->void - { - auto& out = barr.emplace_back(base); - out.image = img.image.get(); - out.subresourceRange = { - .aspectMask = IGPUImage::E_ASPECT_FLAGS::EAF_COLOR_BIT, - .levelCount = 1, - .layerCount = out.image->getCreationParameters().arrayLayers - }; - }; - const auto& immutables = m_activeSession->getActiveResources().immutables; - enqueueBarrier(immutables.rwmcCascades); - enqueueBarrier(immutables.albedo); - enqueueBarrier(immutables.normal); - enqueueBarrier(immutables.motion); - enqueueBarrier(immutables.mask); - // this one is slightly different, we barrier against ourselves, and we'll also be writing to it - enqueueBarrier(immutables.beauty); - barr.back().barrier.dep.srcStageMask = firstResolveStage; - barr.back().barrier.dep.dstAccessMask |= ACCESS_FLAGS::SHADER_WRITE_BITS; - - } - success = cb->pipelineBarrier(asset::EDF_NONE,{.imgBarriers=barr}); - } - - const auto* const layout = m_construction.layout.get(); - // TODO: uimplemented yet - - // compute passes - - return success; -} - -} \ No newline at end of file diff --git a/41_VisibilityBuffer/CMakeLists.txt b/41_VisibilityBuffer/CMakeLists.txt new file mode 100644 index 000000000..28aaf663f --- /dev/null +++ b/41_VisibilityBuffer/CMakeLists.txt @@ -0,0 +1,13 @@ + +include(common RESULT_VARIABLE RES) +if(NOT RES) + message(FATAL_ERROR "common.cmake not found. Should be in {repo_root}/cmake directory") +endif() + +set(EXT_SOURCE_FILES + ../../src/nbl/ext/DebugDraw/CDraw3DLine.cpp + ../../src/nbl/ext/DepthPyramidGenerator/DepthPyramidGenerator.cpp +) + + +nbl_create_executable_project("${EXT_SOURCE_FILES}" "" "" "" "${NBL_EXECUTABLE_PROJECT_CREATION_PCH_TARGET}") \ No newline at end of file diff --git a/41_VisibilityBuffer/common.glsl b/41_VisibilityBuffer/common.glsl new file mode 100644 index 000000000..12324fb83 --- /dev/null +++ b/41_VisibilityBuffer/common.glsl @@ -0,0 +1,68 @@ +#ifndef _COMMON_GLSL_INCLUDED_ +#define _COMMON_GLSL_INCLUDED_ + +#extension GL_EXT_shader_16bit_storage: require + +#include "common.h" + +// defines for buffer fill pipeline +#define NBL_GLSL_BARYCENTRIC_VERT_POS_OUTPUT_LOC 1 +#define NBL_GLSL_BARYCENTRIC_VERT_PROVOKINGPOS_OUTPUT_LOC 2 +#define NBL_GLSL_BARYCENTRIC_FRAG_POS_INPUT_LOC NBL_GLSL_BARYCENTRIC_VERT_POS_OUTPUT_LOC +#define NBL_GLSL_BARYCENTRIC_FRAG_PROVOKINGPOS_INPUT_LOC NBL_GLSL_BARYCENTRIC_VERT_PROVOKINGPOS_OUTPUT_LOC + +#include +struct BatchInstanceData +{ + vec3 Ka; + uint firstIndex; + vec3 Kd; + nbl_glsl_VG_VirtualAttributePacked_t vAttrPos; + vec3 Ks; + nbl_glsl_VG_VirtualAttributePacked_t vAttrUV; + vec3 Ke; + nbl_glsl_VG_VirtualAttributePacked_t vAttrNormal; + uvec2 map_Ka_data; + uvec2 map_Kd_data; + uvec2 map_Ks_data; + uvec2 map_Ns_data; + uvec2 map_d_data; + uvec2 map_bump_data; + float Ns; + float d; + float Ni; + uint extra; //flags copied from MTL metadata +}; + +layout(set = 1, binding = 0, std430) readonly buffer BatchInstanceBuffer +{ + BatchInstanceData batchInstanceData[]; +}; + +// non-global descriptors +#include +layout(set = 2, binding = 0, row_major, std140) uniform UBO +{ + nbl_glsl_SBasicViewParameters params; +} CamData; + +// functions +#include +vec3 nbl_glsl_fetchVtxPos(in uint vtxID, in uint drawGUID) +{ + nbl_glsl_VG_VirtualAttributePacked_t va = batchInstanceData[drawGUID].vAttrPos; + return nbl_glsl_VG_attribFetch_RGB32_SFLOAT(va,vtxID); +} + +vec2 nbl_glsl_fetchVtxUV(in uint vtxID, in uint drawGUID) +{ + nbl_glsl_VG_VirtualAttributePacked_t va = batchInstanceData[drawGUID].vAttrUV; + return nbl_glsl_VG_attribFetch_RG32_SFLOAT(va,vtxID); +} + +vec3 nbl_glsl_fetchVtxNormal(in uint vtxID, in uint drawGUID) +{ + nbl_glsl_VG_VirtualAttributePacked_t va = batchInstanceData[drawGUID].vAttrNormal; + return normalize(nbl_glsl_VG_attribFetch_RGB10A2_SNORM(va,vtxID).xyz); +} +#endif \ No newline at end of file diff --git a/41_VisibilityBuffer/common.h b/41_VisibilityBuffer/common.h new file mode 100644 index 000000000..85fabddd8 --- /dev/null +++ b/41_VisibilityBuffer/common.h @@ -0,0 +1,20 @@ +#ifndef _COMMON_H_INCLUDED_ +#define _COMMON_H_INCLUDED_ + + +#define PAGE_SZ_LOG2 7 +#define PAGE_PADDING 8 + +#define _NBL_VT_PAGE_TABLE_BINDING 0 +#define _NBL_VT_FLOAT_VIEWS_BINDING 1 + + +#define USED_ATTRIBUTES 3 +#define MAX_TRIANGLES_IN_BATCH 1024 + + +#define SHADING_WG_SIZE_X 16 +#define SHADING_WG_SIZE_Y 16 + + +#endif \ No newline at end of file diff --git a/31_HLSLPathTracer/config.json.template b/41_VisibilityBuffer/config.json.template similarity index 99% rename from 31_HLSLPathTracer/config.json.template rename to 41_VisibilityBuffer/config.json.template index 24adf54fb..f961745c1 100644 --- a/31_HLSLPathTracer/config.json.template +++ b/41_VisibilityBuffer/config.json.template @@ -25,4 +25,4 @@ "outputs": [] } ] -} +} \ No newline at end of file diff --git a/41_VisibilityBuffer/cull.comp b/41_VisibilityBuffer/cull.comp new file mode 100644 index 000000000..f17496dab --- /dev/null +++ b/41_VisibilityBuffer/cull.comp @@ -0,0 +1,102 @@ +#version 430 core +#extension GL_EXT_shader_16bit_storage : require + +#include "rasterizationCommon.h" +layout(local_size_x = WORKGROUP_SIZE) in; + +layout(set=0, binding=0, std430, row_major) restrict readonly buffer PerInstanceCull +{ + CullData_t cullData[]; +}; +layout(set=0, binding=1, std430) restrict buffer MVPs +{ + mat4 mvps[]; +} mvpBuff; + +layout(set=0, binding=2, std430, column_major) restrict buffer CubeMVPs +{ + mat4 cubeMVPs[]; +} cubeMvpBuff; + +#define CUBE_COMMAND_BUFF_SET 0 +#define CUBE_COMMAND_BUFF_BINDING 3 +#define FRUSTUM_CULLED_COMMAND_BUFF_SET 0 +#define FRUSTUM_CULLED_COMMAND_BUFF_BINDING 4 +#define OCCLUSION_CULLED_COMMAND_BUFF_SET 0 +#define OCCLUSION_CULLED_COMMAND_BUFF_BINDING 5 +#define CUBE_DRAW_GUID_BUFF_SET 0 +#define CUBE_DRAW_GUID_BUFF_BINDING 6 +#define OCCLUSION_DISPATCH_INDIRECT_BUFF_SET 0 +#define OCCLUSION_DISPATCH_INDIRECT_BUFF_BINDING 7 +#define VISIBLE_BUFF_SET 0 +#define VISIBLE_BUFF_BINDING 8 +#include "occlusionCullingShaderCommon.glsl" + +layout(push_constant, row_major) uniform PushConstants +{ + CullShaderData_t data; +} pc; + +#include +#include + +bool unpackFreezeCullFlag(in uint packedVal) +{ + return bool(packedVal >> 16u); +} + +uint unpackMaxBatchCount(in uint packedVal) +{ + return packedVal & 0x0000FFFFu; +} + +void main() +{ + if (gl_GlobalInvocationID.x >= unpackMaxBatchCount(pc.data.freezeCullingAndMaxBatchCountPacked)) + return; + + mvpBuff.mvps[gl_GlobalInvocationID.x] = pc.data.viewProjMatrix; // no model matrices + + const CullData_t batchCullData = cullData[gl_GlobalInvocationID.x]; + const uint drawCommandGUID = batchCullData.drawCommandGUID; + occlusionCommandBuff.draws[drawCommandGUID].instanceCount = 0; + + if (unpackFreezeCullFlag(pc.data.freezeCullingAndMaxBatchCountPacked)) + return; + + + const mat2x3 bbox = mat2x3(batchCullData.aabbMinEdge,batchCullData.aabbMaxEdge); + bool couldBeVisible = nbl_glsl_couldBeVisible(pc.data.viewProjMatrix,bbox); + + if (couldBeVisible) + { + const vec3 localCameraPos = pc.data.worldCamPos; // true in this case + const bool cameraInsideAABB = all(greaterThanEqual(localCameraPos, batchCullData.aabbMinEdge)) && all(lessThanEqual(localCameraPos, batchCullData.aabbMaxEdge)); + const bool assumedVisible = uint(visibleBuff.visible[gl_GlobalInvocationID.x]) == 1u || cameraInsideAABB; + frustumCommandBuff.draws[drawCommandGUID].instanceCount = assumedVisible ? 1u : 0u; + // if not frustum culled and batch was not visible in the last frame, and it makes sense to test + if(!assumedVisible) + { + const uint currCubeIdx = atomicAdd(cubeIndirectDraw.draw.instanceCount, 1); + + if(currCubeIdx % WORKGROUP_SIZE == 0) + atomicAdd(occlusionDispatchIndirect.di.num_groups_x, 1); + + // only works for a source geometry box which is [0,1]^2, the geometry creator box is [-0.5,0.5]^2, so either make your own box, or work out the math for this + vec3 aabbExtent = batchCullData.aabbMaxEdge - batchCullData.aabbMinEdge; + cubeMvpBuff.cubeMVPs[currCubeIdx] = mat4( + pc.data.viewProjMatrix[0]*aabbExtent.x, + pc.data.viewProjMatrix[1]*aabbExtent.y, + pc.data.viewProjMatrix[2]*aabbExtent.z, + pc.data.viewProjMatrix*vec4(batchCullData.aabbMinEdge,1) + ); + + cubeDrawGUIDBuffer.drawGUID[currCubeIdx] = drawCommandGUID; + } + } + else + frustumCommandBuff.draws[drawCommandGUID].instanceCount = 0; + + // does `freezeCulling` affect this negatively? + visibleBuff.visible[gl_GlobalInvocationID.x] = uint16_t(0u); +} \ No newline at end of file diff --git a/41_VisibilityBuffer/cullShaderCommon.h b/41_VisibilityBuffer/cullShaderCommon.h new file mode 100644 index 000000000..7588c0170 --- /dev/null +++ b/41_VisibilityBuffer/cullShaderCommon.h @@ -0,0 +1,32 @@ +#ifndef _COMMON_INCLUDED_ +#define _COMMON_INCLUDED_ + + +#define MAX_TRIANGLES_IN_BATCH 512 +#define MAX_ACCUMULATED_SAMPLES 0x10000 + + +#define WORKGROUP_SIZE 256 + + +#ifdef __cplusplus + #define uint uint32_t + struct uvec2 + { + uint x,y; + }; + struct vec2 + { + float x,y; + }; + struct vec3 + { + float x,y,z; + }; + #define vec4 nbl::core::vectorSIMDf + #define mat4 nbl::core::matrix4SIMD + #define mat4x3 nbl::core::matrix3x4SIMD +#endif + + +#endif diff --git a/41_VisibilityBuffer/fillVBuffer.frag b/41_VisibilityBuffer/fillVBuffer.frag new file mode 100644 index 000000000..a85cd2476 --- /dev/null +++ b/41_VisibilityBuffer/fillVBuffer.frag @@ -0,0 +1,28 @@ +#version 460 core +#include + +#include "common.glsl" + +layout(location = 0) flat in uint drawGUID; + +// TODO: investigate using snorm16 for the derivatives +layout(location = 0) out uvec4 triangleIDdrawID_unorm16Bary_dBarydScreenHalf2x2; // 32bit triangleID, 2x16bit barycentrics, 4x16bit barycentric derivatives +#include + +uint nbl_glsl_barycentric_frag_getDrawID() {return drawGUID;} +vec3 nbl_glsl_barycentric_frag_getVertexPos(in uint drawID, in uint primID, in uint primsVx) +{ + const uint ix = nbl_glsl_VG_fetchTriangleVertexIndex(primID*3u+batchInstanceData[drawID].firstIndex,primsVx); + return nbl_glsl_fetchVtxPos(ix,drawID); +} + +void main() +{ + vec2 bary = nbl_glsl_barycentric_frag_get(); + + const int triangleIDBitcount = findMSB(MAX_TRIANGLES_IN_BATCH-1)+1; + triangleIDdrawID_unorm16Bary_dBarydScreenHalf2x2[0] = bitfieldInsert(gl_PrimitiveID,drawGUID,triangleIDBitcount,32-triangleIDBitcount); + triangleIDdrawID_unorm16Bary_dBarydScreenHalf2x2[1] = packUnorm2x16(bary); + triangleIDdrawID_unorm16Bary_dBarydScreenHalf2x2[2] = packHalf2x16(dFdx(bary)); + triangleIDdrawID_unorm16Bary_dBarydScreenHalf2x2[3] = packHalf2x16(dFdy(bary)); +} \ No newline at end of file diff --git a/41_VisibilityBuffer/fillVBuffer.vert b/41_VisibilityBuffer/fillVBuffer.vert new file mode 100644 index 000000000..49db1d22b --- /dev/null +++ b/41_VisibilityBuffer/fillVBuffer.vert @@ -0,0 +1,20 @@ +#version 460 core + +#include "common.glsl" + +layout (push_constant) uniform Block +{ + uint dataBufferOffset; +} pc; + +layout(location = 0) flat out uint drawGUID; +#include + +#include +void main() +{ + drawGUID = gl_DrawID+pc.dataBufferOffset; + vec3 modelPos = nbl_glsl_fetchVtxPos(gl_VertexIndex,drawGUID); + nbl_glsl_barycentric_vert_set(modelPos); + gl_Position = nbl_glsl_pseudoMul4x4with3x1(CamData.params.MVP,modelPos); +} \ No newline at end of file diff --git a/41_VisibilityBuffer/main.cpp b/41_VisibilityBuffer/main.cpp new file mode 100644 index 000000000..d5e355055 --- /dev/null +++ b/41_VisibilityBuffer/main.cpp @@ -0,0 +1,1333 @@ +// 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 + +#define _NBL_STATIC_LIB_ +#include +#include +#include + +//! I advise to check out this file, its a basic input handler +#include "../common/QToQuitEventReceiver.h" + +#ifdef DEBUG_AABBS +#include "nbl/ext/DebugDraw/CDraw3DLine.h" +#include "nbl/ext/DepthPyramidGenerator/DepthPyramidGenerator.h" + +using namespace nbl; +using namespace nbl::core; +using namespace nbl::asset; +using namespace nbl::video; + +bool freezeCulling = false; + +class MyEventReceiver : public QToQuitEventReceiver +{ +public: + + MyEventReceiver() + { + } + + bool OnEvent(const SEvent& event) + { + if (event.EventType == nbl::EET_KEY_INPUT_EVENT && !event.KeyInput.PressedDown) + { + switch (event.KeyInput.Key) + { + case nbl::KEY_KEY_Q: // so we can quit + return QToQuitEventReceiver::OnEvent(event); + case nbl::KEY_KEY_C: // freeze culling + freezeCulling = !freezeCulling; // Not enabled/necessary yet + return true; + default: + break; + } + } + + return false; + } +}; + +#include "common.h" +#include "rasterizationCommon.h" + +//vt stuff +using STextureData = asset::ICPUVirtualTexture::SMasterTextureData; + +constexpr uint32_t TILES_PER_DIM_LOG2 = 4u; +constexpr uint32_t MAX_ALLOCATABLE_TEX_SZ_LOG2 = 12u; //4096 + +struct commit_t +{ + STextureData addr; + core::smart_refctd_ptr texture; + asset::ICPUImage::SSubresourceRange subresource; + asset::ICPUSampler::E_TEXTURE_CLAMP uwrap; + asset::ICPUSampler::E_TEXTURE_CLAMP vwrap; + asset::ICPUSampler::E_TEXTURE_BORDER_COLOR border; + + core::vector cullData; +}; + +constexpr uint32_t TEX_OF_INTEREST_CNT = 6u; +constexpr uint32_t texturesOfInterest[TEX_OF_INTEREST_CNT] = +{ + asset::CMTLMetadata::CRenderpassIndependentPipeline::EMP_AMBIENT, + asset::CMTLMetadata::CRenderpassIndependentPipeline::EMP_DIFFUSE, + asset::CMTLMetadata::CRenderpassIndependentPipeline::EMP_SPECULAR, + asset::CMTLMetadata::CRenderpassIndependentPipeline::EMP_SHININESS, + asset::CMTLMetadata::CRenderpassIndependentPipeline::EMP_OPACITY, + asset::CMTLMetadata::CRenderpassIndependentPipeline::EMP_BUMP +}; + +#include "nbl/nblpack.h" +struct BatchInstanceData +{ + BatchInstanceData() : shininess(32.f), opacity(1.f), IoR(1.6f), extra(0u) + { + ambient = vector3df_SIMD(0.f); + diffuse = vector3df_SIMD(1.f); + specular = vector3df_SIMD(0.f); + emissive = vector3df_SIMD(0.f); + const auto invalid_tex = STextureData::invalid(); + std::fill_n(map_data,TEX_OF_INTEREST_CNT,reinterpret_cast(invalid_tex)); + } + BatchInstanceData(const BatchInstanceData& other) : BatchInstanceData() + { + this->operator=(other); + } + ~BatchInstanceData() + { + } + + BatchInstanceData& operator=(const BatchInstanceData& other) + { + ambient = other.ambient; + diffuse = other.diffuse; + specular = other.specular; + emissive = other.emissive; + std::copy_n(other.map_data,TEX_OF_INTEREST_CNT,map_data); + shininess = other.shininess; + opacity = other.opacity; + IoR = other.IoR; + extra = other.extra; + return *this; + } + + union + { + //Ka + vector3df_SIMD ambient; + struct + { + uint32_t invalid_0[3]; + uint32_t firstIndex; + }; + }; + union + { + //Kd + vector3df_SIMD diffuse; + struct + { + uint32_t invalid_1[3]; + uint32_t vAttrPos; + }; + }; + union + { + //Ks + vector3df_SIMD specular; + struct + { + uint32_t invalid_2[3]; + uint32_t vAttrUV; + }; + }; + union + { + //Ke + vector3df_SIMD emissive; + struct + { + uint32_t invalid_3[3]; + uint32_t vAttrNormal; + }; + }; + uint64_t map_data[TEX_OF_INTEREST_CNT]; + //Ns, specular exponent in phong model + float shininess; + //d + float opacity; + //Ni, index of refraction + float IoR; + uint32_t extra; +} PACK_STRUCT; +#include "nbl/nblunpack.h" +static_assert(sizeof(BatchInstanceData) <= asset::ICPUMeshBuffer::MAX_PUSH_CONSTANT_BYTESIZE, "doesnt fit in push constants"); + +//mesh packing stuff +struct DrawIndexedIndirectInput +{ + size_t offset = 0u; + size_t maxCount = 0u; + + static constexpr asset::E_PRIMITIVE_TOPOLOGY mode = asset::EPT_TRIANGLE_LIST; + static constexpr asset::E_INDEX_TYPE indexType = asset::EIT_16BIT; +}; + + +struct SceneData +{ + smart_refctd_ptr fillVBufferPpln; + smart_refctd_ptr shadeVBufferPpln; + + smart_refctd_ptr idxBuffer; + //TODO: mdi buffers should be in the `cullShaderData` struct? + smart_refctd_ptr frustumCulledMdiBuffer; + smart_refctd_ptr occlusionCulledMdiBuffer; + smart_refctd_ptr vtDS,vgDS,perFrameDS,shadingDS; + + core::vector drawIndirectInput; + core::vector pushConstantsData; + + smart_refctd_ptr ubo; + +}; + +//TODO: split into two structs (one for frustrum culling, one for occlusion) +struct CullShaderData +{ + core::smart_refctd_ptr perBatchCull; + core::smart_refctd_ptr mvpBuffer; + + // frustum culling + core::smart_refctd_ptr cullPipeline; + core::smart_refctd_ptr cullDSLayout; + core::smart_refctd_ptr cullDS; + + // occlusion culling + core::smart_refctd_ptr occlusionCullPipeline; + core::smart_refctd_ptr occlusionCullDSLayout; + core::smart_refctd_ptr occlusionCullDS; + + // batch ID to MDI offset mapping + core::smart_refctd_ptr mapPipeline; + core::smart_refctd_ptr mapDSLayout; + core::smart_refctd_ptr mapDS; + + SBufferBinding cubeIdxBuffer; + SBufferBinding cubeVertexBuffers[SVertexInputParams::MAX_ATTR_BUF_BINDING_COUNT]; + core::smart_refctd_ptr visible; + core::smart_refctd_ptr cubeMVPs; + core::smart_refctd_ptr cubeDrawGUIDs; + core::smart_refctd_ptr cubeCommandBuffer; + core::smart_refctd_ptr dispatchIndirect; + + uint32_t maxBatchCount; +}; + +using MeshPacker = CCPUMeshPackerV2; +using GPUMeshPacker = CGPUMeshPackerV2; + +STextureData getTextureData(core::vector& _out_commits, const asset::ICPUImage* _img, asset::ICPUVirtualTexture* _vt, asset::ISampler::E_TEXTURE_CLAMP _uwrap, asset::ISampler::E_TEXTURE_CLAMP _vwrap, asset::ISampler::E_TEXTURE_BORDER_COLOR _borderColor) +{ + const auto& extent = _img->getCreationParameters().extent; + + auto imgAndOrigSz = asset::ICPUVirtualTexture::createPoTPaddedSquareImageWithMipLevels(_img, _uwrap, _vwrap, _borderColor); + + asset::IImage::SSubresourceRange subres; + subres.baseMipLevel = 0u; + subres.levelCount = core::findLSB(core::roundDownToPoT(std::max(extent.width, extent.height))) + 1; + subres.baseArrayLayer = 0u; + subres.layerCount = 1u; + + auto addr = _vt->alloc(_img->getCreationParameters().format, imgAndOrigSz.second, subres, _uwrap, _vwrap); + commit_t cm{ addr, std::move(imgAndOrigSz.first), subres, _uwrap, _vwrap, _borderColor }; + + _out_commits.push_back(cm); + + return addr; +} + +constexpr bool useSSBO = true; + +int main() +{ + // create device with full flexibility over creation parameters + // you can add more parameters if desired, check irr::SIrrlichtCreationParameters + nbl::SIrrlichtCreationParameters params; + params.Bits = 24; //may have to set to 32bit for some platforms + params.ZBufferBits = 24; //we'd like 32bit here + params.DriverType = video::EDT_OPENGL; //! Only Well functioning driver, software renderer left for sake of 2D image drawing + //params.WindowSize = dimension2d(3000, 2100); + params.WindowSize = dimension2d(1280, 720); + params.Fullscreen = false; + params.Vsync = true; //! If supported by target platform + params.Doublebuffer = true; + params.Stencilbuffer = false; //! This will not even be a choice soon + auto device = createDeviceEx(params); + + if (!device) + return 1; // could not create selected driver. + + //! disable mouse cursor, since camera will force it to the middle + //! and we don't want a jittery cursor in the middle distracting us + device->getCursorControl()->setVisible(false); + + //! Since our cursor will be enslaved, there will be no way to close the window + //! So we listen for the "Q" key being pressed and exit the application + MyEventReceiver receiver; + device->setEventReceiver(&receiver); + + auto* driver = device->getVideoDriver(); + auto* smgr = device->getSceneManager(); + auto* am = device->getAssetManager(); + auto* fs = am->getFileSystem(); + +#ifdef DEBUG_AABBS + auto draw3DLine = ext::DebugDraw::CDraw3DLine::create(driver); +#endif + + // + auto createScreenSizedImage = [driver,¶ms](const E_FORMAT format) -> auto + { + IGPUImage::SCreationParams param; + param.flags = static_cast(0u); + param.type = IImage::ET_2D; + param.format = format; + param.extent = {params.WindowSize.Width,params.WindowSize.Height,1u}; + param.mipLevels = 1u; + param.arrayLayers = 1u; + param.samples = IImage::ESCF_1_BIT; + return driver->createDeviceLocalGPUImageOnDedMem(std::move(param)); + }; + auto framebuffer = createScreenSizedImage(EF_R8G8B8A8_SRGB); + auto createImageView = [driver,¶ms](smart_refctd_ptr&& image, const E_FORMAT format=EF_UNKNOWN) -> auto + { + IGPUImageView::SCreationParams params; + params.flags = static_cast(0u); + params.image = std::move(image); + params.viewType = IGPUImageView::ET_2D; + params.format = format!=EF_UNKNOWN ? format:params.image->getCreationParameters().format; + params.components = {}; + params.subresourceRange = {}; + params.subresourceRange.levelCount = 1u; + params.subresourceRange.layerCount = 1u; + return driver->createImageView(std::move(params)); + }; + auto depthBufferView = createImageView(createScreenSizedImage(EF_D32_SFLOAT)); + auto visBufferView = createImageView(createScreenSizedImage(EF_R32G32B32A32_UINT)); + + auto visBuffer = driver->addFrameBuffer(); + visBuffer->attach(EFAP_DEPTH_ATTACHMENT,smart_refctd_ptr(depthBufferView)); + visBuffer->attach(EFAP_COLOR_ATTACHMENT0,smart_refctd_ptr(visBufferView)); + auto fb = driver->addFrameBuffer(); + fb->attach(EFAP_COLOR_ATTACHMENT0,createImageView(smart_refctd_ptr(framebuffer))); + + auto zBuffOnlyFrameBuffer = driver->addFrameBuffer(); + zBuffOnlyFrameBuffer->attach(EFAP_DEPTH_ATTACHMENT, std::move(depthBufferView)); + + // depth pyramid generator setup + using DPG = ext::DepthPyramidGenerator::DepthPyramidGenerator; + + DPG::Config config; + config.op = DPG::E_MIPMAP_GENERATION_OPERATOR::EMGO_MAX; + config.outputFormat = EF_R32_SFLOAT; + config.workGroupSize = DPG::E_WORK_GROUP_SIZE::EWGS_32x32x1; + //config.roundUpToPoTWithPadding = true; + //config.lvlLimit = 5u; + DPG dpg(driver, am, depthBufferView, config); + + const uint32_t mipCnt = DPG::getMaxMipCntFromImage(depthBufferView, config); + + core::vector> mipImages(mipCnt); + core::vector> mips(mipCnt); + DPG::createMipMapImages(driver, depthBufferView, mipImages.data(), config); + DPG::createMipMapImageViews(driver, depthBufferView, mipImages.data(), mips.data(), config); + + + core::smart_refctd_ptr dpgDsLayout; + dpgDsLayout = DPG::createDescriptorSetLayout(driver, config); + const uint32_t dpgDsCnt = DPG::createDescriptorSets(driver, depthBufferView, mips.data(), dpgDsLayout, nullptr, nullptr, config); + core::vector> dpgDs(dpgDsCnt); + core::vector dpgDispatchData(dpgDsCnt); + DPG::createDescriptorSets(driver, depthBufferView, mips.data(), dpgDsLayout, dpgDs.data(), dpgDispatchData.data(), config); + + core::smart_refctd_ptr dpgPpln; + dpg.createPipeline(driver, dpgDsLayout, dpgPpln); + + // + SceneData sceneData; + CullShaderData cullShaderData; +#ifdef DEBUG_AABBS + core::vector> dbgLines; +#endif + { + // + auto* qnc = am->getMeshManipulator()->getQuantNormalCache(); + //loading cache from file + qnc->loadCacheFromFile(fs, "../../tmp/normalCache101010.sse", true); + + // register the zip + device->getFileSystem()->addFileArchive("../../media/sponza.zip"); + asset::IAssetLoader::SAssetLoadParams lp; + auto meshes_bundle = am->getAsset("sponza.obj", lp); + assert(!meshes_bundle.getContents().empty()); + auto mesh_raw = static_cast(meshes_bundle.getContents().begin()->get()); + + // ensure memory will be freed as soon as CPU assets are dropped + am->clearAllAssetCache(); + //saving cache to file + qnc->saveCacheToFile(fs, "../../tmp/normalCache101010.sse"); + //qnc->clearCache(); // TODO + + // + auto meshBuffers = mesh_raw->getMeshBufferVector(); + + auto pipelineMeshBufferRanges = [&meshBuffers]() -> auto + { + core::vector*> output; + if (!meshBuffers.empty()) + { + // sort meshbuffers by pipeline + std::sort(meshBuffers.begin(),meshBuffers.end(),[](const auto& lhs, const auto& rhs) + { + auto lPpln = lhs->getPipeline(); + auto rPpln = rhs->getPipeline(); + // render non-transparent things first + if (lPpln->getBlendParams().blendParams[0].blendEnable < rPpln->getBlendParams().blendParams[0].blendEnable) + return true; + if (lPpln->getBlendParams().blendParams[0].blendEnable == rPpln->getBlendParams().blendParams[0].blendEnable) + return lPpln < rPpln; + return false; + } + ); + + const ICPURenderpassIndependentPipeline* mbPipeline = nullptr; + for (const auto& mb : meshBuffers) + if (mb->getPipeline()!=mbPipeline) + { + mbPipeline = mb->getPipeline(); + output.push_back(&mb); + } + output.push_back(meshBuffers.data()+meshBuffers.size()); + } + return output; + }(); + + // the texture packing + smart_refctd_ptr gpuvt; + { + smart_refctd_ptr vt = core::make_smart_refctd_ptr([](asset::E_FORMAT_CLASS) -> uint32_t { return TILES_PER_DIM_LOG2; }, PAGE_SZ_LOG2, PAGE_PADDING, MAX_ALLOCATABLE_TEX_SZ_LOG2); + { + core::vector vt_commits; + + core::unordered_map,STextureData> VTtexDataMap; + //modifying push constants and default fragment shader for VT + for (const auto& meshbuffer : meshBuffers) + { + BatchInstanceData pushConsts; + + auto* ds = meshbuffer->getAttachedDescriptorSet(); + if (!ds) + continue; + + std::for_each_n(texturesOfInterest,TEX_OF_INTEREST_CNT,[&](auto textureOfInterest) + { + const auto* view = static_cast(ds->getDescriptors(textureOfInterest).begin()->desc.get()); + auto img = view->getCreationParameters().image; + auto extent = img->getCreationParameters().extent; + if (extent.width<=2u||extent.height<=2u) //dummy 2x2, TODO: compare by finding it in the cache! + return; + + auto& texData = reinterpret_cast(pushConsts.map_data[textureOfInterest]); + static_assert(sizeof(STextureData)==sizeof(pushConsts.map_data[0]),"wrong reinterpret_cast"); + + auto found = VTtexDataMap.find(img); + if (found!=VTtexDataMap.end()) + texData = found->second; + else + { + const auto* smplr = ds->getLayout()->getBindings().begin()[textureOfInterest].samplers[0].get(); + const auto uwrap = static_cast(smplr->getParams().TextureWrapU); + const auto vwrap = static_cast(smplr->getParams().TextureWrapV); + const auto borderColor = static_cast(smplr->getParams().BorderColor); + texData = getTextureData(vt_commits,img.get(),vt.get(),uwrap,vwrap,borderColor); + VTtexDataMap.insert({img,texData}); + // get rid of pixel storage + img->convertToDummyObject(~0ull); + } + }); + + // all pipelines will have the same metadata + const asset::CMTLMetadata::CRenderpassIndependentPipeline* pipelineMetadata = meshes_bundle.getMetadata()->selfCast()->getAssetSpecificMetadata(meshbuffer->getPipeline()); + assert(pipelineMetadata); + + //copy texture presence flags + pushConsts.extra = pipelineMetadata->m_materialParams.extra; + pushConsts.ambient = pipelineMetadata->m_materialParams.ambient; + pushConsts.diffuse = pipelineMetadata->m_materialParams.diffuse; + pushConsts.emissive = pipelineMetadata->m_materialParams.emissive; + pushConsts.specular = pipelineMetadata->m_materialParams.specular; + pushConsts.IoR = pipelineMetadata->m_materialParams.IoR; + pushConsts.opacity = pipelineMetadata->m_materialParams.opacity; + pushConsts.shininess = pipelineMetadata->m_materialParams.shininess; + memcpy(meshbuffer->getPushConstantsDataPtr(),&pushConsts,sizeof(pushConsts)); + + //we dont want this DS to be converted into GPU DS, so set to nullptr + //dont worry about deletion of textures (invalidation of pointers), they're grabbed in VTtexDataMap + meshbuffer->setAttachedDescriptorSet(nullptr); + } + + vt->shrink(); + for (const auto& cm : vt_commits) + { + vt->commit(cm.addr, cm.texture.get(), cm.subresource, cm.uwrap, cm.vwrap, cm.border); + } + } + + gpuvt = core::make_smart_refctd_ptr(driver, vt.get()); + } + + // the vertex packing + smart_refctd_ptr gpump; + smart_refctd_ptr batchDataSSBO; + { + constexpr uint16_t minTrisBatch = 256u; + constexpr uint16_t maxTrisBatch = MAX_TRIANGLES_IN_BATCH; + + constexpr uint32_t kVerticesPerTriangle = 3u; + MeshPacker::AllocationParams allocParams; + allocParams.indexBuffSupportedCnt = 32u * 1024u * 1024u; + allocParams.indexBufferMinAllocCnt = minTrisBatch*kVerticesPerTriangle; + allocParams.vertexBuffSupportedByteSize = 128u * 1024u * 1024u; + allocParams.vertexBufferMinAllocByteSize = minTrisBatch; + allocParams.MDIDataBuffSupportedCnt = 8192u; + allocParams.MDIDataBuffMinAllocCnt = 16u; + + auto wholeMbRangeBegin = pipelineMeshBufferRanges.front(); + auto wholeMbRangeEnd = pipelineMeshBufferRanges.back(); + + IMeshPackerV2Base::SupportedFormatsContainer formats; + formats.insertFormatsFromMeshBufferRange(wholeMbRangeBegin, wholeMbRangeEnd); + + auto mp = core::make_smart_refctd_ptr>(allocParams,formats,minTrisBatch,maxTrisBatch); + + const uint32_t mdiCntBound = mp->calcMDIStructMaxCount(wholeMbRangeBegin,wholeMbRangeEnd); + + auto allocData = core::make_refctd_dynamic_array>(mdiCntBound); + auto allocDataIt = allocData->begin(); + for (auto it=pipelineMeshBufferRanges.begin(); it!=pipelineMeshBufferRanges.end()-1u; ) + { + auto mbRangeBegin = &(*it)->get(); + auto mbRangeEnd = &(*(++it))->get(); + + bool allocSuccessfull = mp->alloc(&*allocDataIt,mbRangeBegin,mbRangeEnd); + if (!allocSuccessfull) + { + std::cout << "Alloc failed \n"; + _NBL_DEBUG_BREAK_IF(true); + } + allocDataIt += mp->calcMDIStructMaxCount(mbRangeBegin,mbRangeEnd); + } + + mp->shrinkOutputBuffersSize(); + mp->instantiateDataStorage(); + + core::vector batchData; + batchData.reserve(mdiCntBound); + + core::vector batchCullData(mdiCntBound); + auto batchCullDataEnd = batchCullData.begin(); + + allocDataIt = allocData->begin(); + uint32_t mdiListOffset = 0u; + for (auto it=pipelineMeshBufferRanges.begin(); it!=pipelineMeshBufferRanges.end()-1u; ) + { + auto mbRangeBegin = &(*it)->get(); + auto mbRangeEnd = &(*(++it))->get(); + + const uint32_t meshMdiBound = mp->calcMDIStructMaxCount(mbRangeBegin,mbRangeEnd); + core::vector pmbd(std::distance(mbRangeBegin,mbRangeEnd)); + core::vector cdot(meshMdiBound); + core::vector aabbs(meshMdiBound); + uint32_t actualMdiCnt = mp->commit(pmbd.data(),cdot.data(),aabbs.data(),&*allocDataIt,mbRangeBegin,mbRangeEnd); + allocDataIt += meshMdiBound; + + if (actualMdiCnt==0u) + { + std::cout << "Commit failed \n"; + _NBL_DEBUG_BREAK_IF(true); + } + + uint32_t aabbIdx = 0u; + for (auto packedMeshBufferData : pmbd) + { + for (uint32_t i = 0u; i < packedMeshBufferData.mdiParameterCount; i++) + { + batchCullDataEnd->aabbMinEdge.x = aabbs[aabbIdx].MinEdge.X; + batchCullDataEnd->aabbMinEdge.y = aabbs[aabbIdx].MinEdge.Y; + batchCullDataEnd->aabbMinEdge.z = aabbs[aabbIdx].MinEdge.Z; + + batchCullDataEnd->aabbMaxEdge.x = aabbs[aabbIdx].MaxEdge.X; + batchCullDataEnd->aabbMaxEdge.y = aabbs[aabbIdx].MaxEdge.Y; + batchCullDataEnd->aabbMaxEdge.z = aabbs[aabbIdx].MaxEdge.Z; + + batchCullDataEnd->drawCommandGUID = packedMeshBufferData.mdiParameterOffset + i; + +#ifdef DEBUG_AABBS + draw3DLine->enqueueBox(dbgLines, aabbs[aabbIdx], 0.0f, 0.0f, 0.0f, 1.0f, core::matrix3x4SIMD()); +#endif + + batchCullDataEnd++; + aabbIdx++; + } + } + + sceneData.pushConstantsData.push_back(mdiListOffset); + mdiListOffset += actualMdiCnt; + + DrawIndexedIndirectInput& mdiCallInput = sceneData.drawIndirectInput.emplace_back(); + mdiCallInput.maxCount = actualMdiCnt; + mdiCallInput.offset = pmbd.front().mdiParameterOffset*sizeof(DrawElementsIndirectCommand_t); + + auto pmbdIt = pmbd.begin(); + auto cdotIt = cdot.begin(); + for (auto mbIt=mbRangeBegin; mbIt!=mbRangeEnd; mbIt++) + { + const auto& material = *reinterpret_cast((*mbIt)->getPushConstantsDataPtr()); + const IMeshPackerBase::PackedMeshBufferData& packedData = *(pmbdIt++); + for (uint32_t mdi=0u; mdi(mp->getPackerDataStore().MDIDataBuffer->getPointer())[packedData.mdiParameterOffset+mdi].firstIndex; + + MeshPacker::CombinedDataOffsetTable& virtualAttribTable = *(cdotIt++); + constexpr auto UVAttributeIx = 2; + batch.vAttrPos = reinterpret_cast(virtualAttribTable.attribInfo[(*mbIt)->getPositionAttributeIx()]); + batch.vAttrUV = reinterpret_cast(virtualAttribTable.attribInfo[UVAttributeIx]); + batch.vAttrNormal = reinterpret_cast(virtualAttribTable.attribInfo[(*mbIt)->getNormalAttributeIx()]); + } + } + } + + const uint32_t totalActualMdiCnt = mdiListOffset; + + batchDataSSBO = driver->createFilledDeviceLocalBufferOnDedMem(batchData.size()*sizeof(BatchInstanceData),batchData.data()); + + gpump = core::make_smart_refctd_ptr>(driver,mp.get()); + sceneData.idxBuffer = gpump->getPackerDataStore().indexBuffer; + + sceneData.frustumCulledMdiBuffer = gpump->getPackerDataStore().MDIDataBuffer; + sceneData.occlusionCulledMdiBuffer = driver->createDeviceLocalGPUBufferOnDedMem(sceneData.frustumCulledMdiBuffer->getSize()); + driver->copyBuffer(sceneData.frustumCulledMdiBuffer.get(), sceneData.occlusionCulledMdiBuffer.get(), 0u, 0u, sceneData.frustumCulledMdiBuffer->getSize()); + + cullShaderData.cubeMVPs = driver->createDeviceLocalGPUBufferOnDedMem(totalActualMdiCnt * sizeof(core::matrix4SIMD)); + cullShaderData.cubeDrawGUIDs = driver->createDeviceLocalGPUBufferOnDedMem(totalActualMdiCnt * sizeof(uint32_t)); + + cullShaderData.maxBatchCount = std::distance(batchCullData.begin(), batchCullDataEnd); + cullShaderData.perBatchCull = driver->createFilledDeviceLocalBufferOnDedMem(cullShaderData.maxBatchCount * sizeof(CullData_t), batchCullData.data()); + cullShaderData.mvpBuffer = driver->createDeviceLocalGPUBufferOnDedMem(cullShaderData.maxBatchCount * sizeof(core::matrix4SIMD)); + cullShaderData.visible = driver->createDeviceLocalGPUBufferOnDedMem(totalActualMdiCnt * sizeof(uint16_t)); + driver->fillBuffer(cullShaderData.visible.get(), 0u, cullShaderData.visible->getSize(), 0u); + } + mesh_raw->convertToDummyObject(~0u); + + // + { + constexpr uint32_t cubeIdxCnt = 36u; + + DrawElementsIndirectCommand_t cubeIndirectDrawCommand; + cubeIndirectDrawCommand.baseInstance = 0u; + cubeIndirectDrawCommand.baseVertex = 0u; + cubeIndirectDrawCommand.count = cubeIdxCnt; + cubeIndirectDrawCommand.firstIndex = 0u; + cubeIndirectDrawCommand.instanceCount = 0u; + cullShaderData.cubeCommandBuffer = driver->createFilledDeviceLocalBufferOnDedMem(sizeof(DrawElementsIndirectCommand_t), &cubeIndirectDrawCommand); + + DispatchIndirectCommand_t dispatchIndirect; + dispatchIndirect.num_groups_x = 0u; + dispatchIndirect.num_groups_y = 1u; + dispatchIndirect.num_groups_z = 1u; + cullShaderData.dispatchIndirect = driver->createFilledDeviceLocalBufferOnDedMem(sizeof(DispatchIndirectCommand_t), &dispatchIndirect); + } + + // + smart_refctd_ptr perFrameDSLayout, shadingDSLayout; + { + { + IGPUDescriptorSetLayout::SBinding bindings[1]; + bindings[0].binding = 0u; + bindings[0].count = 1u; + bindings[0].samplers = nullptr; + bindings[0].stageFlags = ISpecializedShader::ESS_VERTEX; + bindings[0].type = EDT_UNIFORM_BUFFER; + + perFrameDSLayout = driver->createDescriptorSetLayout(bindings, bindings + sizeof(bindings) / sizeof(IGPUDescriptorSetLayout::SBinding)); + } + { + sceneData.ubo = driver->createDeviceLocalGPUBufferOnDedMem(sizeof(SBasicViewParameters)); + IGPUDescriptorSet::SDescriptorInfo infos[1]; + infos[0].desc = core::smart_refctd_ptr(sceneData.ubo); + infos[0].buffer.offset = 0u; + infos[0].buffer.size = sceneData.ubo->getSize(); + + sceneData.perFrameDS = driver->createDescriptorSet(smart_refctd_ptr(perFrameDSLayout)); + IGPUDescriptorSet::SWriteDescriptorSet writes[1]; + writes[0].dstSet = sceneData.perFrameDS.get(); + writes[0].binding = 0u; + writes[0].arrayElement = 0u; + writes[0].count = 1u; + writes[0].descriptorType = EDT_UNIFORM_BUFFER; + writes[0].info = infos + 0u; + driver->updateDescriptorSets(sizeof(writes) / sizeof(IGPUDescriptorSet::SWriteDescriptorSet), writes, 0u, nullptr); + } + + { + IGPUSampler::SParams params; + params.TextureWrapU = ISampler::ETC_MIRROR; + params.TextureWrapV = ISampler::ETC_MIRROR; + params.TextureWrapW = ISampler::ETC_MIRROR; + params.BorderColor = ISampler::ETBC_FLOAT_OPAQUE_BLACK; + params.MinFilter = ISampler::ETF_NEAREST; + params.MaxFilter = ISampler::ETF_NEAREST; + params.MipmapMode = ISampler::ESMM_NEAREST; + params.AnisotropicFilter = 0; + params.CompareEnable = 0; + auto sampler = driver->createSampler(params); + + IGPUDescriptorSetLayout::SBinding bindings[5]; + bindings[0].binding = 0u; + bindings[0].count = 1u; + bindings[0].samplers = &sampler; + bindings[0].stageFlags = ISpecializedShader::ESS_COMPUTE; + bindings[0].type = EDT_COMBINED_IMAGE_SAMPLER; + + + + bindings[1].binding = 1u; + bindings[1].count = 1u; + bindings[1].samplers = nullptr; + bindings[1].stageFlags = ISpecializedShader::ESS_COMPUTE; + bindings[1].type = EDT_STORAGE_IMAGE; + + for (uint32_t i = 2u; i < 5u; i++) + { + bindings[i].binding = i; + bindings[i].count = 1u; + bindings[i].samplers = nullptr; + bindings[i].stageFlags = ISpecializedShader::ESS_COMPUTE; + bindings[i].type = EDT_STORAGE_BUFFER; + } + + shadingDSLayout = driver->createDescriptorSetLayout(bindings, bindings + sizeof(bindings) / sizeof(IGPUDescriptorSetLayout::SBinding)); + } + { + IGPUDescriptorSet::SDescriptorInfo infos[5]; + infos[0].desc = core::smart_refctd_ptr(visBufferView); + //infos[0].image.imageLayout = ?; + infos[0].image.sampler = nullptr; // used immutable in the layout + infos[1].desc = createImageView(std::move(framebuffer), EF_R8G8B8A8_UNORM); + //infos[0].image.imageLayout = ?; + infos[1].image.sampler = nullptr; // storage image + + infos[2].buffer.offset = 0u; + infos[2].buffer.size = cullShaderData.visible->getSize(); + infos[2].desc = cullShaderData.visible; + + infos[3].buffer.offset = 0u; + infos[3].buffer.size = cullShaderData.cubeCommandBuffer->getSize(); + infos[3].desc = cullShaderData.cubeCommandBuffer; + + infos[4].buffer.offset = 0u; + infos[4].buffer.size = cullShaderData.dispatchIndirect->getSize(); + infos[4].desc = cullShaderData.dispatchIndirect; + + sceneData.shadingDS = driver->createDescriptorSet(smart_refctd_ptr(shadingDSLayout)); + IGPUDescriptorSet::SWriteDescriptorSet writes[5]; + for (auto i = 0u; i < 5u; i++) + { + writes[i].dstSet = sceneData.shadingDS.get(); + writes[i].binding = i; + writes[i].arrayElement = 0u; + writes[i].count = 1u; + writes[i].info = infos + i; + } + writes[0].descriptorType = EDT_COMBINED_IMAGE_SAMPLER; + writes[1].descriptorType = EDT_STORAGE_IMAGE; + writes[2].descriptorType = EDT_STORAGE_BUFFER; + writes[3].descriptorType = EDT_STORAGE_BUFFER; + writes[4].descriptorType = EDT_STORAGE_BUFFER; + + driver->updateDescriptorSets(sizeof(writes) / sizeof(IGPUDescriptorSet::SWriteDescriptorSet), writes, 0u, nullptr); + } + } + + // + smart_refctd_ptr vtDSLayout; + { + // layout + auto binding_and_sampler_count = gpuvt->getDSlayoutBindings(nullptr,nullptr); + core::vector vtBindings(binding_and_sampler_count.first); + core::vector> vtSamplers(binding_and_sampler_count.second); + gpuvt->getDSlayoutBindings(vtBindings.data(),vtSamplers.data(),_NBL_VT_PAGE_TABLE_BINDING,_NBL_VT_FLOAT_VIEWS_BINDING); + auto& precomputedBinding = vtBindings.emplace_back(); + precomputedBinding.binding = 2u; + precomputedBinding.type = EDT_STORAGE_BUFFER; + precomputedBinding.count = 1u; + precomputedBinding.samplers = nullptr; // ssbo + for (auto& binding : vtBindings) + binding.stageFlags = static_cast(ISpecializedShader::ESS_COMPUTE|ISpecializedShader::ESS_FRAGMENT); + + vtDSLayout = driver->createDescriptorSetLayout(vtBindings.data(),vtBindings.data()+vtBindings.size()); + + // write + sceneData.vtDS = driver->createDescriptorSet(smart_refctd_ptr(vtDSLayout)); + + const auto sizesVT = gpuvt->getDescriptorSetWrites(nullptr,nullptr,nullptr); + core::vector writesVT(sizesVT.first+1u); + core::vector infoVT(sizesVT.second+1u); + gpuvt->getDescriptorSetWrites(writesVT.data(),infoVT.data(),sceneData.vtDS.get(),_NBL_VT_PAGE_TABLE_BINDING,_NBL_VT_FLOAT_VIEWS_BINDING); + + auto& precomp = gpuvt->getPrecomputedData(); + infoVT.back().desc = driver->createFilledDeviceLocalBufferOnDedMem(sizeof(precomp),&precomp); + infoVT.back().buffer.offset = 0u; + infoVT.back().buffer.size = sizeof(video::IGPUVirtualTexture::SPrecomputedData); + writesVT.back().dstSet = sceneData.vtDS.get(); + writesVT.back().binding = 2u; + writesVT.back().arrayElement = 0u; + writesVT.back().count = 1u; + writesVT.back().descriptorType = EDT_STORAGE_BUFFER; + writesVT.back().info = &infoVT.back(); + + driver->updateDescriptorSets(writesVT.size(),writesVT.data(),0u,nullptr); + } + smart_refctd_ptr vgDSLayout; + std::string extraCode; + // msvc is incredibly dumb and complains about type mismatches in code sections guarded by ifconstexpr + std::conditional_t tmp; + [&](auto& layoutParams) -> void + { + // layout + core::vector vgBindings((useSSBO ? gpump->getDSlayoutBindingsForSSBO(nullptr):gpump->getDSlayoutBindingsForUTB(nullptr))+1u); + auto& materialDataBinding = vgBindings.front(); + materialDataBinding.binding = 0u; + materialDataBinding.type = EDT_STORAGE_BUFFER; + materialDataBinding.count = 1u; + materialDataBinding.samplers = nullptr; // not sampler interpolated + auto* actualVGBindings = vgBindings.data()+1u; + if constexpr (useSSBO) + { + layoutParams.uintBufferBinding = 1u; + layoutParams.uvec2BufferBinding = 2u; + layoutParams.uvec3BufferBinding = 3u; + layoutParams.uvec4BufferBinding = 4u; + layoutParams.indexBufferBinding = 5u; + gpump->getDSlayoutBindingsForSSBO(actualVGBindings,layoutParams); + } + else + { + layoutParams.usamplersBinding = 1u; + layoutParams.fsamplersBinding = 2u; + gpump->getDSlayoutBindingsForUTB(actualVGBindings,layoutParams); + } + for (auto& binding : vgBindings) + binding.stageFlags = static_cast(ISpecializedShader::ESS_VERTEX|ISpecializedShader::ESS_COMPUTE|ISpecializedShader::ESS_FRAGMENT); + + vgDSLayout = driver->createDescriptorSetLayout(vgBindings.data(),vgBindings.data()+vgBindings.size()); + + // write + sceneData.vgDS = driver->createDescriptorSet(smart_refctd_ptr(vgDSLayout)); + + uint32_t writeCount,infoCount; + if constexpr (useSSBO) + { + writeCount = gpump->getDescriptorSetWritesForSSBO(nullptr, nullptr, nullptr); + infoCount = 2u; + } + else + std::tie(writeCount, infoCount) = gpump->getDescriptorSetWritesForUTB(nullptr, nullptr, nullptr); + vector writesVG(++writeCount); + vector infosVG(++infoCount); + + auto writes = writesVG.data(); + auto infos = infosVG.data(); + writes->dstSet = sceneData.vgDS.get(); + writes->binding = 0u; + writes->arrayElement = 0u; + writes->count = 1u; + writes->descriptorType = EDT_STORAGE_BUFFER; + writes->info = infos; + writes++; + infos->buffer.offset = 0u; + infos->buffer.size = batchDataSSBO->getSize(); + infos->desc = std::move(batchDataSSBO); + infos++; + + constexpr uint32_t vgDescriptorSetIx = 1u; + if constexpr (useSSBO) + { + extraCode = gpump->getGLSLForSSBO(vgDescriptorSetIx, layoutParams); + gpump->getDescriptorSetWritesForSSBO(writes, infos, sceneData.vgDS.get(), layoutParams); + } + else + { + extraCode = gpump->getGLSLForUTB(vgDescriptorSetIx, layoutParams); + gpump->getDescriptorSetWritesForUTB(writes, infos, sceneData.vgDS.get(), layoutParams); + } + driver->updateDescriptorSets(writeCount, writesVG.data(), 0u, nullptr); + }(tmp); + auto overrideShaderJustAfterVersionDirective = [am, driver, &extraCode](const char* path) + { + asset::IAssetLoader::SAssetLoadParams lp; + auto _specShader = IAsset::castDown(*am->getAsset(path, lp).getContents().begin()); + assert(_specShader); + const asset::ICPUShader* unspec = _specShader->getUnspecialized(); + assert(unspec->containsGLSL()); + + auto begin = reinterpret_cast(unspec->getContent()->getPointer()); + const std::string_view origSource(begin, unspec->getContent()->getSize()); + + const size_t firstNewlineAfterVersion = origSource.find("\n", origSource.find("#version ")); + assert(firstNewlineAfterVersion != std::string_view::npos); + const std::string_view sourceWithoutVersion(begin + firstNewlineAfterVersion, origSource.size() - firstNewlineAfterVersion); + + std::string newSource("#version 460 core\n"); + newSource += extraCode; + newSource += sourceWithoutVersion; + + auto unspecNew = core::make_smart_refctd_ptr(newSource.c_str()); + auto specinfo = _specShader->getSpecializationInfo(); + + auto newSpecShader = core::make_smart_refctd_ptr(std::move(unspecNew), std::move(specinfo)); + auto gpuSpecShaders = driver->getGPUObjectsFromAssets(&newSpecShader.get(), &newSpecShader.get() + 1u); + return std::move(gpuSpecShaders->begin()[0]); + }; + // + { + SPushConstantRange pcRange; + pcRange.size = sizeof(uint32_t); + pcRange.offset = 0u; + pcRange.stageFlags = ISpecializedShader::ESS_VERTEX; + + smart_refctd_ptr fillShaders[2] = { + overrideShaderJustAfterVersionDirective("../fillVBuffer.vert"), + overrideShaderJustAfterVersionDirective("../fillVBuffer.frag") + }; + + sceneData.fillVBufferPpln = driver->createRenderpassIndependentPipeline( + nullptr, driver->createPipelineLayout(&pcRange, &pcRange + 1, smart_refctd_ptr(vtDSLayout), smart_refctd_ptr(vgDSLayout), smart_refctd_ptr(perFrameDSLayout)), + &fillShaders[0].get(), &fillShaders[0].get() + 2u, + SVertexInputParams{}, + SBlendParams{}, + SPrimitiveAssemblyParams{}, + SRasterizationParams{} + ); + } + { + extraCode += "#define _NBL_VT_FLOAT_VIEWS_COUNT " + std::to_string(gpuvt->getFloatViews().size()) + "\n"; + std::cout << gpuvt->getGLSLFunctionsIncludePath().c_str() << std::endl; + + SPushConstantRange pcRange; + pcRange.size = sizeof(core::vector3df); + pcRange.offset = 0u; + pcRange.stageFlags = ISpecializedShader::ESS_COMPUTE; + + sceneData.shadeVBufferPpln = driver->createComputePipeline( + nullptr, driver->createPipelineLayout(&pcRange, &pcRange + 1, std::move(vtDSLayout), std::move(vgDSLayout), std::move(perFrameDSLayout), std::move(shadingDSLayout)), + overrideShaderJustAfterVersionDirective("../shadeVBuffer.comp") + ); + } + } + + // occlusion cull shader pipeline setup + { + cullShaderData.cubeVertexBuffers[0].offset = 0ull; + cullShaderData.cubeVertexBuffers[0].buffer = cullShaderData.cubeMVPs; + + const uint16_t indices[36] = { + 0, 2, 1, + 0, 3, 2, + 0, 4, 3, + 3, 4, 7, + 4, 5, 7, + 5, 6, 7, + 5, 1, 6, + 1, 2, 6, + 7, 6, 3, + 6, 2, 3, + 4, 0, 5, + 0, 1, 5 + }; + + cullShaderData.cubeIdxBuffer.offset = 0ull; + cullShaderData.cubeIdxBuffer.buffer = driver->createFilledDeviceLocalBufferOnDedMem(sizeof(indices), indices); + + SVertexInputParams vtxParams; + vtxParams.enabledAttribFlags = 0b1111u; + vtxParams.enabledBindingFlags = 0b1u; + + vtxParams.bindings[0].inputRate = EVIR_PER_INSTANCE; + vtxParams.bindings[0].stride = sizeof(core::vectorSIMDf) * 4u; + + for (uint32_t i = 0u; i < 4u; i++) + { + vtxParams.attributes[i].binding = 0u; + vtxParams.attributes[i].format = EF_R32G32B32A32_SFLOAT; + vtxParams.attributes[i].relativeOffset = sizeof(core::vectorSIMDf) * i; + } + + { + IGPUDescriptorSetLayout::SBinding bindings[1]; + + bindings[0].binding = 0u; + bindings[0].count = 1u; + bindings[0].samplers = nullptr; + bindings[0].stageFlags = ISpecializedShader::ESS_FRAGMENT; + bindings[0].type = EDT_STORAGE_BUFFER; + + cullShaderData.occlusionCullDSLayout = driver->createDescriptorSetLayout(bindings, bindings + sizeof(bindings) / sizeof(IGPUDescriptorSetLayout::SBinding)); + } + + { + cullShaderData.occlusionCullDS = driver->createDescriptorSet(smart_refctd_ptr(cullShaderData.occlusionCullDSLayout)); + + IGPUDescriptorSet::SDescriptorInfo info[1]; + info[0].desc = core::smart_refctd_ptr(cullShaderData.visible); + info[0].buffer.offset = 0u; + info[0].buffer.size = cullShaderData.visible->getSize(); + + + IGPUDescriptorSet::SWriteDescriptorSet write[1]; + write[0].dstSet = cullShaderData.occlusionCullDS.get(); + write[0].binding = 0; + write[0].arrayElement = 0u; + write[0].count = 1u; + write[0].descriptorType = EDT_STORAGE_BUFFER; + write[0].info = info; + + driver->updateDescriptorSets(sizeof(write) / sizeof(IGPUDescriptorSet::SWriteDescriptorSet), write, 0u, nullptr); + } + + asset::IAssetLoader::SAssetLoadParams lp; + auto vtxOcclusionShader = IAsset::castDown(*am->getAsset("../occlusionCull.vert", lp).getContents().begin()); + auto fragOcclusionShader = IAsset::castDown(*am->getAsset("../occlusionCull.frag", lp).getContents().begin()); + assert(vtxOcclusionShader); + assert(fragOcclusionShader); + assert(vtxOcclusionShader->getUnspecialized()->containsGLSL()); + assert(fragOcclusionShader->getUnspecialized()->containsGLSL()); + + auto gpuVtxOcclusionShader = driver->getGPUObjectsFromAssets(&vtxOcclusionShader, &vtxOcclusionShader + 1u)->begin()[0]; + auto gpuFragOcclusionShader = driver->getGPUObjectsFromAssets(&fragOcclusionShader, &fragOcclusionShader + 1u)->begin()[0]; + + IGPUSpecializedShader* occlusionShaders[2] = { + gpuVtxOcclusionShader.get(), gpuFragOcclusionShader.get() + }; + + SRasterizationParams rasterizationParams; + rasterizationParams.depthWriteEnable = false; + + cullShaderData.occlusionCullPipeline = driver->createRenderpassIndependentPipeline( + nullptr, driver->createPipelineLayout(nullptr, nullptr, smart_refctd_ptr(cullShaderData.occlusionCullDSLayout)), + occlusionShaders, occlusionShaders + 2u, + vtxParams, + SBlendParams{}, + SPrimitiveAssemblyParams{}, + rasterizationParams + ); + } + + // map shader pipeline setup + { + SPushConstantRange range{ ISpecializedShader::ESS_COMPUTE,0u,sizeof(uint32_t) }; + + { + IGPUDescriptorSetLayout::SBinding bindings[4]; + for (uint32_t i = 0u; i < 4; i++) + { + bindings[i].binding = i; + bindings[i].count = 1u; + bindings[i].samplers = nullptr; + bindings[i].stageFlags = ISpecializedShader::ESS_COMPUTE; + bindings[i].type = EDT_STORAGE_BUFFER; + } + + cullShaderData.mapDSLayout = driver->createDescriptorSetLayout(bindings, bindings + sizeof(bindings) / sizeof(IGPUDescriptorSetLayout::SBinding)); + } + + { + IGPUDescriptorSet::SDescriptorInfo infos[4]; + + infos[0].desc = sceneData.occlusionCulledMdiBuffer; + infos[0].buffer.offset = 0u; + infos[0].buffer.size = sceneData.occlusionCulledMdiBuffer->getSize(); + + infos[1].desc = cullShaderData.visible; + infos[1].buffer.offset = 0u; + infos[1].buffer.size = cullShaderData.visible->getSize(); + + infos[2].desc = cullShaderData.cubeDrawGUIDs; + infos[2].buffer.offset = 0u; + infos[2].buffer.size = cullShaderData.cubeDrawGUIDs->getSize(); + + infos[3].desc = cullShaderData.cubeCommandBuffer; + infos[3].buffer.offset = 0u; + infos[3].buffer.size = cullShaderData.cubeCommandBuffer->getSize(); + + cullShaderData.mapDS = driver->createDescriptorSet(smart_refctd_ptr(cullShaderData.mapDSLayout)); + + IGPUDescriptorSet::SWriteDescriptorSet writes[4]; + for (uint32_t i = 0u; i < 4; i++) + { + writes[i].dstSet = cullShaderData.mapDS.get(); + writes[i].binding = i; + writes[i].arrayElement = 0u; + writes[i].count = 1u; + writes[i].descriptorType = EDT_STORAGE_BUFFER; + writes[i].info = infos + i; + } + + driver->updateDescriptorSets(sizeof(writes) / sizeof(IGPUDescriptorSet::SWriteDescriptorSet), writes, 0u, nullptr); + } + + asset::IAssetLoader::SAssetLoadParams lp; + auto mapShader = IAsset::castDown(*am->getAsset("../occlusionCullMap.comp", lp).getContents().begin()); + assert(mapShader); + const asset::ICPUShader* unspec = mapShader->getUnspecialized(); + assert(unspec->containsGLSL()); + + auto gpuMapShader = driver->getGPUObjectsFromAssets(&mapShader, &mapShader + 1u)->begin()[0]; + + auto mapPipelineLayout = driver->createPipelineLayout(&range, &range + 1u, core::smart_refctd_ptr(cullShaderData.mapDSLayout)); + cullShaderData.mapPipeline = driver->createComputePipeline(nullptr, std::move(mapPipelineLayout), std::move(gpuMapShader)); + } + + // frustum cull shader pipeline setup + { + SPushConstantRange range{ ISpecializedShader::ESS_COMPUTE,0u,sizeof(CullShaderData_t) }; + + { + IGPUDescriptorSetLayout::SBinding bindings[9]; + for (uint32_t i = 0u; i < 9; i++) + { + bindings[i].binding = i; + bindings[i].count = 1u; + bindings[i].samplers = nullptr; + bindings[i].stageFlags = ISpecializedShader::ESS_COMPUTE; + bindings[i].type = EDT_STORAGE_BUFFER; + } + + cullShaderData.cullDSLayout = driver->createDescriptorSetLayout(bindings, bindings + sizeof(bindings) / sizeof(IGPUDescriptorSetLayout::SBinding)); + } + + { + IGPUDescriptorSet::SDescriptorInfo infos[9]; + + infos[0].desc = core::smart_refctd_ptr(cullShaderData.perBatchCull); + infos[0].buffer.offset = 0u; + infos[0].buffer.size = cullShaderData.perBatchCull->getSize(); + + infos[1].desc = core::smart_refctd_ptr(cullShaderData.mvpBuffer); + infos[1].buffer.offset = 0u; + infos[1].buffer.size = cullShaderData.mvpBuffer->getSize(); + + infos[2].desc = cullShaderData.cubeVertexBuffers[0].buffer; + infos[2].buffer.offset = 0u; + infos[2].buffer.size = cullShaderData.cubeVertexBuffers[0].buffer->getSize(); + + infos[3].desc = cullShaderData.cubeCommandBuffer; + infos[3].buffer.offset = 0u; + infos[3].buffer.size = cullShaderData.cubeCommandBuffer->getSize(); + + infos[4].desc = sceneData.frustumCulledMdiBuffer; + infos[4].buffer.offset = 0u; + infos[4].buffer.size = sceneData.frustumCulledMdiBuffer->getSize(); + + infos[5].desc = sceneData.occlusionCulledMdiBuffer; + infos[5].buffer.offset = 0u; + infos[5].buffer.size = sceneData.occlusionCulledMdiBuffer->getSize(); + + infos[6].desc = cullShaderData.cubeDrawGUIDs; + infos[6].buffer.offset = 0u; + infos[6].buffer.size = cullShaderData.cubeDrawGUIDs->getSize(); + + infos[7].desc = cullShaderData.dispatchIndirect; + infos[7].buffer.offset = 0u; + infos[7].buffer.size = cullShaderData.dispatchIndirect->getSize(); + + infos[8].desc = cullShaderData.visible; + infos[8].buffer.offset = 0u; + infos[8].buffer.size = cullShaderData.visible->getSize(); + + cullShaderData.cullDS = driver->createDescriptorSet(smart_refctd_ptr(cullShaderData.cullDSLayout)); + + IGPUDescriptorSet::SWriteDescriptorSet writes[9]; + for (uint32_t i = 0u; i < 9; i++) + { + writes[i].dstSet = cullShaderData.cullDS.get(); + writes[i].binding = i; + writes[i].arrayElement = 0u; + writes[i].count = 1u; + writes[i].descriptorType = EDT_STORAGE_BUFFER; + writes[i].info = infos + i; + } + + driver->updateDescriptorSets(sizeof(writes) / sizeof(IGPUDescriptorSet::SWriteDescriptorSet), writes, 0u, nullptr); + } + + asset::IAssetLoader::SAssetLoadParams lp; + auto cullShader = IAsset::castDown(*am->getAsset("../cull.comp", lp).getContents().begin()); + assert(cullShader); + const asset::ICPUShader* unspec = cullShader->getUnspecialized(); + assert(unspec->containsGLSL()); + + auto gpuCullShader = driver->getGPUObjectsFromAssets(&cullShader, &cullShader + 1u)->begin()[0]; + + auto cullPipelineLayout = driver->createPipelineLayout(&range, &range + 1u, core::smart_refctd_ptr(cullShaderData.cullDSLayout)); + cullShaderData.cullPipeline = driver->createComputePipeline(nullptr, std::move(cullPipelineLayout), std::move(gpuCullShader)); + } + + auto cullBatches = [&driver, &cullShaderData, &sceneData](const core::matrix4SIMD& vp, const core::vector3df& camPos, bool freezeCulling) + { + driver->bindDescriptorSets(EPBP_COMPUTE, cullShaderData.cullPipeline->getLayout(), 0u, 1u, &cullShaderData.cullDS.get(), nullptr); + driver->bindComputePipeline(cullShaderData.cullPipeline.get()); + + CullShaderData_t cullPushConstants; + cullPushConstants.viewProjMatrix = vp; + cullPushConstants.worldCamPos.x = camPos.X; + cullPushConstants.worldCamPos.y = camPos.Y; + cullPushConstants.worldCamPos.z = camPos.Z; + cullPushConstants.freezeCullingAndMaxBatchCountPacked = cullShaderData.maxBatchCount | (static_cast(freezeCulling) << 16u); + + driver->pushConstants(cullShaderData.cullPipeline->getLayout(), ISpecializedShader::ESS_COMPUTE, 0u, sizeof(CullShaderData_t), &cullPushConstants); + + const uint32_t cullWorkGroups = (cullShaderData.maxBatchCount - 1u) / WORKGROUP_SIZE + 1u; + + driver->dispatch(cullWorkGroups, 1u, 1u); + }; + + const IGPUDescriptorSet* vBuffDs[4] = { sceneData.vtDS.get(),sceneData.vgDS.get(),sceneData.perFrameDS.get(),sceneData.shadingDS.get() }; + auto fillVBuffer = [&driver, &sceneData, &vBuffDs](core::smart_refctd_ptr mdiBuffer) + { + driver->bindDescriptorSets(video::EPBP_GRAPHICS, sceneData.fillVBufferPpln->getLayout(), 0u, 3u, vBuffDs, nullptr); + driver->bindGraphicsPipeline(sceneData.fillVBufferPpln.get()); + + for (auto i = 0u; i < sceneData.pushConstantsData.size(); i++) + { + driver->pushConstants(sceneData.fillVBufferPpln->getLayout(), IGPUSpecializedShader::ESS_ALL, 0u, sizeof(uint32_t), &sceneData.pushConstantsData[i]); + + const asset::SBufferBinding noVtxBindings[IGPUMeshBuffer::MAX_ATTR_BUF_BINDING_COUNT] = {}; + driver->drawIndexedIndirect( + noVtxBindings, DrawIndexedIndirectInput::mode, DrawIndexedIndirectInput::indexType, + sceneData.idxBuffer.get(), mdiBuffer.get(), + sceneData.drawIndirectInput[i].offset, sceneData.drawIndirectInput[i].maxCount, + sizeof(DrawElementsIndirectCommand_t) + ); + } + }; + + //! we want to move around the scene and view it from different angles + scene::ICameraSceneNode* camera = smgr->addCameraSceneNodeFPS(0, 100.0f, 0.5f); + + camera->setPosition(core::vector3df(-4, 0, 0)); + camera->setTarget(core::vector3df(0, 0, 0)); + camera->setNearValue(1.f); + camera->setFarValue(5000.0f); + + smgr->setActiveCamera(camera); + + bool asdf = true; + uint64_t lastFPSTime = 0; + while (device->run() && receiver.keepOpen()) + { + driver->beginScene(true, true, video::SColor(255, 0, 0, 255)); + + //! This animates (moves) the camera and sets the transforms + camera->OnAnimate(std::chrono::duration_cast(device->getTimer()->getTime()).count()); + camera->render(); + + SBasicViewParameters uboData; + memcpy(uboData.MVP, camera->getConcatenatedMatrix().pointer(), sizeof(core::matrix4SIMD)); + memcpy(uboData.MV, camera->getViewMatrix().pointer(), sizeof(core::matrix3x4SIMD)); + memcpy(uboData.NormalMat, camera->getViewMatrix().pointer(), sizeof(core::matrix3x4SIMD)); + driver->updateBufferRangeViaStagingBuffer(sceneData.ubo.get(), 0u, sizeof(SBasicViewParameters), &uboData); + + // frustum cull + // TODO: fill instanceCounts of frustumCulledMdiBuffer with zeros + cullBatches(camera->getConcatenatedMatrix(), camera->getPosition(), freezeCulling); + COpenGLExtensionHandler::pGlMemoryBarrier(GL_VERTEX_ATTRIB_ARRAY_BARRIER_BIT | GL_COMMAND_BARRIER_BIT | GL_SHADER_STORAGE_BARRIER_BIT); + + // first fill visibility buffer pass + driver->setRenderTarget(visBuffer); + driver->clearZBuffer(); + const uint32_t invalidObjectCode[4] = { ~0u,0u,0u,0u }; + driver->clearColorBuffer(EFAP_COLOR_ATTACHMENT0, invalidObjectCode); + fillVBuffer(sceneData.frustumCulledMdiBuffer); + + // create depth pyramid + for (uint32_t i = 0u; i < dpgDsCnt; i++) + dpg.generateMipMaps(depthBufferView, dpgPpln, dpgDs[i], dpgDispatchData[i]); + + // occlusion cull (against partially filled new Z-buffer) + driver->setRenderTarget(zBuffOnlyFrameBuffer); + driver->bindDescriptorSets(video::EPBP_GRAPHICS, cullShaderData.occlusionCullPipeline->getLayout(), 0u, 1u, &cullShaderData.occlusionCullDS.get(), nullptr); + driver->bindGraphicsPipeline(cullShaderData.occlusionCullPipeline.get()); + + driver->drawIndexedIndirect( + cullShaderData.cubeVertexBuffers, EPT_TRIANGLE_LIST, EIT_16BIT, + cullShaderData.cubeIdxBuffer.buffer.get(), cullShaderData.cubeCommandBuffer.get(), + 0u, 1u, + sizeof(DrawElementsIndirectCommand_t) + ); + COpenGLExtensionHandler::pGlMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT); + + // map batchIDs of batches that passed occlusion test to the `occlusionCulledMdiBuffer` + driver->bindDescriptorSets(video::EPBP_COMPUTE, cullShaderData.mapPipeline->getLayout(), 0u, 1u, &cullShaderData.mapDS.get(), nullptr); + driver->bindComputePipeline(cullShaderData.mapPipeline.get()); + + driver->dispatchIndirect(cullShaderData.dispatchIndirect.get(), 0u); + COpenGLExtensionHandler::pGlMemoryBarrier(GL_COMMAND_BARRIER_BIT | GL_SHADER_STORAGE_BARRIER_BIT); + + // second fill visibility buffer pass + driver->setRenderTarget(visBuffer); + fillVBuffer(sceneData.occlusionCulledMdiBuffer); + COpenGLExtensionHandler::extGlMemoryBarrier(GL_FRAMEBUFFER_BARRIER_BIT); + +#ifdef DEBUG_AABBS + //draw aabbs + draw3DLine->draw(camera->getConcatenatedMatrix(), dbgLines); +#endif + + // shade visibility buffer + driver->bindDescriptorSets(video::EPBP_COMPUTE,sceneData.shadeVBufferPpln->getLayout(),0u,4u,vBuffDs,nullptr); + driver->bindComputePipeline(sceneData.shadeVBufferPpln.get()); + { + auto camPos = camera->getAbsolutePosition(); + + driver->pushConstants(sceneData.shadeVBufferPpln->getLayout(),IGPUSpecializedShader::ESS_COMPUTE,0u,sizeof(core::vector3df),&camPos.X); + } + driver->dispatch((params.WindowSize.Width-1u)/SHADING_WG_SIZE_X+1u,(params.WindowSize.Height-1u)/SHADING_WG_SIZE_Y+1u,1u); + COpenGLExtensionHandler::extGlMemoryBarrier(GL_COMMAND_BARRIER_BIT | GL_SHADER_STORAGE_BARRIER_BIT | GL_FRAMEBUFFER_BARRIER_BIT); // is GL_FRAMEBUFFER_BARRIER_BIT needed? + + // blit + driver->blitRenderTargets(fb,0); + + driver->endScene(); + + // display frames per second in window title + uint64_t time = device->getTimer()->getRealTime(); + if (time - lastFPSTime > 1000) + { + std::wostringstream str; + str << L"Visibility Buffer - Nabla Engine [" << driver->getName() << "] FPS:" << driver->getFPS() << " PrimitvesDrawn:" << driver->getPrimitiveCountDrawn(); + + device->setWindowCaption(str.str().c_str()); + lastFPSTime = time; + } + + } + driver->removeAllFrameBuffers(); +} \ No newline at end of file diff --git a/41_VisibilityBuffer/occlusionCull.frag b/41_VisibilityBuffer/occlusionCull.frag new file mode 100644 index 000000000..109de0103 --- /dev/null +++ b/41_VisibilityBuffer/occlusionCull.frag @@ -0,0 +1,15 @@ +#version 460 core +#extension GL_EXT_shader_16bit_storage : require + +layout(early_fragment_tests) in; + +layout(location = 0) in flat uint instanceID; + +#define VISIBLE_BUFF_SET 0 +#define VISIBLE_BUFF_BINDING 0 +#include "occlusionCullingShaderCommon.glsl" + +void main() +{ + visibleBuff.visible[instanceID] = uint16_t(1u); +} \ No newline at end of file diff --git a/41_VisibilityBuffer/occlusionCull.vert b/41_VisibilityBuffer/occlusionCull.vert new file mode 100644 index 000000000..6a1ac9123 --- /dev/null +++ b/41_VisibilityBuffer/occlusionCull.vert @@ -0,0 +1,28 @@ +#version 460 core + +layout(location = 0) in vec4 vMatRow0; +layout(location = 1) in vec4 vMatRow1; +layout(location = 2) in vec4 vMatRow2; +layout(location = 3) in vec4 vMatRow3; + +layout(location = 0) out flat uint instanceID; + +void main() +{ + const vec3 pos[8] = { + vec3(0.0, 0.0, 0.0), + vec3(1.0, 0.0, 0.0), + vec3(1.0, 1.0, 0.0), + vec3(0.0, 1.0, 0.0), + + vec3(0.0, 0.0, 1.0), + vec3(1.0, 0.0, 1.0), + vec3(1.0, 1.0, 1.0), + vec3(0.0, 1.0, 1.0), + }; + + mat4 mvp = mat4(vMatRow0, vMatRow1, vMatRow2, vMatRow3); + + gl_Position = mvp * vec4(pos[gl_VertexIndex], 1.0); + instanceID = gl_InstanceIndex; +} \ No newline at end of file diff --git a/41_VisibilityBuffer/occlusionCullMap.comp b/41_VisibilityBuffer/occlusionCullMap.comp new file mode 100644 index 000000000..97e8ffc45 --- /dev/null +++ b/41_VisibilityBuffer/occlusionCullMap.comp @@ -0,0 +1,30 @@ +#version 460 core + +#extension GL_EXT_shader_16bit_storage : require + +#include "cullShaderCommon.h" +layout(local_size_x = WORKGROUP_SIZE) in; + +#define OCCLUSION_CULLED_COMMAND_BUFF_SET 0 +#define OCCLUSION_CULLED_COMMAND_BUFF_BINDING 0 +#define VISIBLE_BUFF_SET 0 +#define VISIBLE_BUFF_BINDING 1 +#define CUBE_DRAW_GUID_BUFF_SET 0 +#define CUBE_DRAW_GUID_BUFF_BINDING 2 +#define CUBE_COMMAND_BUFF_SET 0 +#define CUBE_COMMAND_BUFF_BINDING 3 +#include "occlusionCullingShaderCommon.glsl" + +void main() +{ + if(gl_GlobalInvocationID.x > cubeIndirectDraw.draw.instanceCount) + return; + + if(uint(visibleBuff.visible[gl_GlobalInvocationID.x]) == 0) + return; + + const uint batchMdiOffset = cubeDrawGUIDBuffer.drawGUID[gl_GlobalInvocationID.x]; + occlusionCommandBuff.draws[batchMdiOffset].instanceCount = 1; + + visibleBuff.visible[gl_GlobalInvocationID.x] = uint16_t(0u); +} \ No newline at end of file diff --git a/41_VisibilityBuffer/occlusionCullingShaderCommon.glsl b/41_VisibilityBuffer/occlusionCullingShaderCommon.glsl new file mode 100644 index 000000000..de84a3525 --- /dev/null +++ b/41_VisibilityBuffer/occlusionCullingShaderCommon.glsl @@ -0,0 +1,58 @@ +//TODO: rename +//TODO: move buffers, that are not used by multiple shaders + +#include + +#ifdef CUBE_COMMAND_BUFF_SET + +layout(set=CUBE_COMMAND_BUFF_SET, binding=CUBE_COMMAND_BUFF_BINDING, std430) restrict coherent buffer CubeIndirectDraw +{ + nbl_glsl_DrawElementsIndirectCommand_t draw; +} cubeIndirectDraw; + +#endif + +#ifdef FRUSTUM_CULLED_COMMAND_BUFF_SET + +layout(set=FRUSTUM_CULLED_COMMAND_BUFF_SET, binding=FRUSTUM_CULLED_COMMAND_BUFF_BINDING, std430) restrict buffer FrustumIndirectDraws +{ + nbl_glsl_DrawElementsIndirectCommand_t draws[]; +} frustumCommandBuff; + +#endif + +#ifdef OCCLUSION_CULLED_COMMAND_BUFF_SET + +layout(set=OCCLUSION_CULLED_COMMAND_BUFF_SET, binding=OCCLUSION_CULLED_COMMAND_BUFF_BINDING, std430) restrict buffer OcclusionIndirectDraws +{ + nbl_glsl_DrawElementsIndirectCommand_t draws[]; +} occlusionCommandBuff; + +#endif + +#ifdef VISIBLE_BUFF_SET + +layout(set = VISIBLE_BUFF_SET, binding = VISIBLE_BUFF_BINDING, std430) restrict buffer VisibleBuff +{ + uint16_t visible[]; +} visibleBuff; + +#endif + +#ifdef CUBE_DRAW_GUID_BUFF_SET + +layout(set=CUBE_DRAW_GUID_BUFF_SET, binding=CUBE_DRAW_GUID_BUFF_BINDING, std430) restrict buffer CubeDrawGUID +{ + uint drawGUID[]; +} cubeDrawGUIDBuffer; + +#endif + +#ifdef OCCLUSION_DISPATCH_INDIRECT_BUFF_SET + +layout(set=OCCLUSION_DISPATCH_INDIRECT_BUFF_SET, binding=OCCLUSION_DISPATCH_INDIRECT_BUFF_BINDING, std430) restrict coherent buffer OcclusionDispatchIndirectBuffer +{ + nbl_glsl_DispatchIndirectCommand_t di; +} occlusionDispatchIndirect; + +#endif \ No newline at end of file diff --git a/41_VisibilityBuffer/pipeline.groovy b/41_VisibilityBuffer/pipeline.groovy new file mode 100644 index 000000000..de86c9cba --- /dev/null +++ b/41_VisibilityBuffer/pipeline.groovy @@ -0,0 +1,50 @@ +import org.DevshGraphicsProgramming.Agent +import org.DevshGraphicsProgramming.BuilderInfo +import org.DevshGraphicsProgramming.IBuilder + +class CVisibilityBufferBuilder extends IBuilder +{ + public CVisibilityBufferBuilder(Agent _agent, _info) + { + super(_agent, _info) + } + + @Override + public boolean prepare(Map axisMapping) + { + return true + } + + @Override + public boolean build(Map axisMapping) + { + IBuilder.CONFIGURATION config = axisMapping.get("CONFIGURATION") + IBuilder.BUILD_TYPE buildType = axisMapping.get("BUILD_TYPE") + + def nameOfBuildDirectory = getNameOfBuildDirectory(buildType) + def nameOfConfig = getNameOfConfig(config) + + agent.execute("cmake --build ${info.rootProjectPath}/${nameOfBuildDirectory}/${info.targetProjectPathRelativeToRoot} --target ${info.targetBaseName} --config ${nameOfConfig} -j12 -v") + + return true + } + + @Override + public boolean test(Map axisMapping) + { + return true + } + + @Override + public boolean install(Map axisMapping) + { + return true + } +} + +def create(Agent _agent, _info) +{ + return new CVisibilityBufferBuilder(_agent, _info) +} + +return this \ No newline at end of file diff --git a/41_VisibilityBuffer/rasterizationCommon.h b/41_VisibilityBuffer/rasterizationCommon.h new file mode 100644 index 000000000..0090093e2 --- /dev/null +++ b/41_VisibilityBuffer/rasterizationCommon.h @@ -0,0 +1,21 @@ +#ifndef _RASTERIZATION_COMMON_H_INCLUDED_ +#define _RASTERIZATION_COMMON_H_INCLUDED_ + +#include "cullShaderCommon.h" + +struct CullShaderData_t +{ + mat4 viewProjMatrix; + vec3 worldCamPos; + uint freezeCullingAndMaxBatchCountPacked; +}; + +struct CullData_t +{ + vec3 aabbMinEdge; + uint padding; + vec3 aabbMaxEdge; + uint drawCommandGUID; // offset into mdi buffer +}; + +#endif diff --git a/41_VisibilityBuffer/shadeVBuffer.comp b/41_VisibilityBuffer/shadeVBuffer.comp new file mode 100644 index 000000000..559d40d81 --- /dev/null +++ b/41_VisibilityBuffer/shadeVBuffer.comp @@ -0,0 +1,199 @@ +#version 430 core +#extension GL_EXT_shader_16bit_storage : require + +#line 4 // so we get good line numbers for errors +#include "common.glsl" +layout (local_size_x = SHADING_WG_SIZE_X, local_size_y = SHADING_WG_SIZE_Y) in; + +#include +#define _NBL_VT_DESCRIPTOR_SET 0 +#define _NBL_VT_FLOAT_VIEWS +#define _NBL_VT_INT_VIEWS_COUNT 0 +#define _NBL_VT_INT_VIEWS +#define _NBL_VT_UINT_VIEWS_COUNT 0 +#define _NBL_VT_UINT_VIEWS +#include +layout (set = 0, binding = 2, std430) restrict readonly buffer PrecomputedStuffSSBO +{ + uint pgtab_sz_log2; + float vtex_sz_rcp; + float phys_pg_tex_sz_rcp[_NBL_VT_MAX_PAGE_TABLE_LAYERS]; + uint layer_to_sampler_ix[_NBL_VT_MAX_PAGE_TABLE_LAYERS]; +} precomputed; + +layout(set = 3, binding = 0) uniform usampler2D vBuffer; +layout(set = 3, binding = 1, rgba8) uniform image2D frameBuffer; + +#include +BatchInstanceData gBatchInstance; +nbl_glsl_MTLMaterialParameters nbl_glsl_getMaterialParameters() // this function is for MTL's shader only +{ + nbl_glsl_MTLMaterialParameters mtl_params; + mtl_params.Ka = gBatchInstance.Ka; + mtl_params.Kd = gBatchInstance.Kd; + mtl_params.Ks = gBatchInstance.Ks; + mtl_params.Ke = gBatchInstance.Ke; + mtl_params.Ns = gBatchInstance.Ns; + mtl_params.d = gBatchInstance.d; + mtl_params.Ni = gBatchInstance.Ni; + mtl_params.extra = gBatchInstance.extra; + return mtl_params; +} +#define _NBL_FRAG_GET_MATERIAL_PARAMETERS_FUNCTION_DEFINED_ + + +uint nbl_glsl_VT_layer2pid(in uint layer) +{ + return precomputed.layer_to_sampler_ix[layer]; +} +uint nbl_glsl_VT_getPgTabSzLog2() +{ + return precomputed.pgtab_sz_log2; +} +float nbl_glsl_VT_getPhysPgTexSzRcp(in uint layer) +{ + return precomputed.phys_pg_tex_sz_rcp[layer]; +} +float nbl_glsl_VT_getVTexSzRcp() +{ + return precomputed.vtex_sz_rcp; +} +#define _NBL_USER_PROVIDED_VIRTUAL_TEXTURING_FUNCTIONS_ +#if PAGE_SZ_LOG2!=7||PAGE_PADDING!=8 + #error "Adjust the include path for the VT functions!" +#endif +#include + +vec4 nbl_sample_Ka(in vec2 uv, in mat2 dUV) { return nbl_glsl_vTextureGrad(gBatchInstance.map_Ka_data, uv, dUV); } +vec4 nbl_sample_Kd(in vec2 uv, in mat2 dUV) { return nbl_glsl_vTextureGrad(gBatchInstance.map_Kd_data, uv, dUV); } +vec4 nbl_sample_Ks(in vec2 uv, in mat2 dUV) { return nbl_glsl_vTextureGrad(gBatchInstance.map_Ks_data, uv, dUV); } +vec4 nbl_sample_Ns(in vec2 uv, in mat2 dUV) { return nbl_glsl_vTextureGrad(gBatchInstance.map_Ns_data, uv, dUV); } +vec4 nbl_sample_d(in vec2 uv, in mat2 dUV) { return nbl_glsl_vTextureGrad(gBatchInstance.map_d_data, uv, dUV); } +vec4 nbl_sample_bump(in vec2 uv, in mat2 dUV) { return nbl_glsl_vTextureGrad(gBatchInstance.map_bump_data, uv, dUV); } +#define _NBL_TEXTURE_SAMPLE_FUNCTIONS_DEFINED_ + + +mat2x3 dPdBary; +mat2x3 nbl_glsl_perturbNormal_dPdSomething() +{ + return dPdBary; +} +mat2 dUVdBary; +mat2 nbl_glsl_perturbNormal_dUVdSomething() +{ + return dUVdBary; +} +#define _NBL_BUILTIN_GLSL_BUMP_MAPPING_DERIVATIVES_DECLARED_ + +layout (push_constant) uniform Block +{ + vec3 camPos; +} pc; +#define _NBL_FRAG_PUSH_CONSTANTS_DEFINED_ + +#define _NBL_FRAG_INPUTS_DEFINED_ +#define _NBL_FRAG_OUTPUTS_DEFINED_ +#define _NBL_FRAG_SET3_BINDINGS_DEFINED_ +#define _NBL_FRAG_MAIN_DEFINED_ +vec2 UV; +mat2 dUVdScreen; +mat2 nbl_glsl_dUVdScreen() +{ + return dUVdScreen; +} +#define _NBL_FRAG_GET_UV_DERIVATIVES_FUNCTION_DEFINED_ +// weird globals for the overriden MTL fragment shader +vec3 ViewPos; +#include + + +#include + +vec3 uintIDToColor(uint id) +{ + return vec3(bitfieldExtract(id,0,4),bitfieldExtract(id,4,4),bitfieldExtract(id,8,4))/15.0; +} + +#define VISIBLE_BUFF_SET 3 +#define VISIBLE_BUFF_BINDING 2 +#define CUBE_COMMAND_BUFF_SET 3 +#define CUBE_COMMAND_BUFF_BINDING 3 +#define OCCLUSION_DISPATCH_INDIRECT_BUFF_SET 3 +#define OCCLUSION_DISPATCH_INDIRECT_BUFF_BINDING 4 + + +#include "occlusionCullingShaderCommon.glsl" + +void main() +{ + const ivec2 fragCoord = ivec2(gl_GlobalInvocationID); + if (any(greaterThanEqual(fragCoord,textureSize(vBuffer,0)))) + return; + + vec4 color = vec4(0,0,0,1); + + const uvec4 triangleIDdrawID_unorm16Bary_dBarydScreenHalf2x2 = texelFetch(vBuffer,fragCoord,0); + if (triangleIDdrawID_unorm16Bary_dBarydScreenHalf2x2[0] != 0xffFFffFFu) + { + const int triangleIDBitcount = findMSB(MAX_TRIANGLES_IN_BATCH-1)+1; + const uint drawGUID = bitfieldExtract(triangleIDdrawID_unorm16Bary_dBarydScreenHalf2x2[0],triangleIDBitcount,32-triangleIDBitcount); + const uint triangleID = bitfieldExtract(triangleIDdrawID_unorm16Bary_dBarydScreenHalf2x2[0],0,triangleIDBitcount); + const vec2 bary = unpackUnorm2x16(triangleIDdrawID_unorm16Bary_dBarydScreenHalf2x2[1]); + const mat2 dBary = mat2( + unpackHalf2x16(triangleIDdrawID_unorm16Bary_dBarydScreenHalf2x2[2]), + unpackHalf2x16(triangleIDdrawID_unorm16Bary_dBarydScreenHalf2x2[3]) + ); + + gBatchInstance = batchInstanceData[drawGUID]; + uvec3 vertexIDs; + const uint baseTriangleVertex = triangleID*3u+gBatchInstance.firstIndex; + for (uint i=0u; i<3u; i++) + vertexIDs[i] = nbl_glsl_VG_fetchTriangleVertexIndex(baseTriangleVertex,i); + + vec3 pos,normal; + { + vec3 positions[3]; + vec2 uvs[3]; + // wrap this up into a template macro? + for (int i=0; i<3; i++) + { + positions[i] = nbl_glsl_fetchVtxPos(vertexIDs[i],drawGUID); + uvs[i] = nbl_glsl_fetchVtxUV(vertexIDs[i],drawGUID); + } + + // maybe could wrap this up into a function + dPdBary[0] = positions[0]-positions[2]; + dPdBary[1] = positions[1]-positions[2]; + pos = dPdBary*bary+positions[2]; // TODO: compute from depth buffer or from barycentric? [Need Vib benchmark] + + dUVdBary[0] = uvs[0]-uvs[2]; + dUVdBary[1] = uvs[1]-uvs[2]; + UV = dUVdBary*bary+uvs[2]; + + dUVdScreen = nbl_glsl_applyChainRule2D(dUVdBary,dBary); + + + const float lastBary = 1.f-bary.x-bary.y; + + normal = nbl_glsl_fetchVtxNormal(vertexIDs[0],drawGUID)*bary.x; + normal += nbl_glsl_fetchVtxNormal(vertexIDs[1],drawGUID)*bary.y; + normal += nbl_glsl_fetchVtxNormal(vertexIDs[2],drawGUID)*lastBary; + normal = normalize(normal); + } + + // the lighting is completely meh (note: we assume there's no modelspace to world transform) + ViewPos = pos-pc.camPos; // weird global for MTL shader + nbl_glsl_IsotropicViewSurfaceInteraction interaction = nbl_glsl_calcSurfaceInteraction(pc.camPos,pos,normal); + color.rgb = nbl_computeLighting(interaction); + + visibleBuff.visible[drawGUID] = uint16_t(1u); + + if(gl_GlobalInvocationID.x == 0) + { + cubeIndirectDraw.draw.instanceCount = 0; + occlusionDispatchIndirect.di.num_groups_x = 0; + } + } + + imageStore(frameBuffer,fragCoord,vec4(nbl_glsl_oetf_sRGB(color.rgb),color.a)); +} \ No newline at end of file diff --git a/42_FragmentShaderPathTracer/CMakeLists.txt b/42_FragmentShaderPathTracer/CMakeLists.txt new file mode 100644 index 000000000..a476b6203 --- /dev/null +++ b/42_FragmentShaderPathTracer/CMakeLists.txt @@ -0,0 +1,7 @@ + +include(common RESULT_VARIABLE RES) +if(NOT RES) + message(FATAL_ERROR "common.cmake not found. Should be in {repo_root}/cmake directory") +endif() + +nbl_create_executable_project("" "" "" "" "${NBL_EXECUTABLE_PROJECT_CREATION_PCH_TARGET}") \ No newline at end of file diff --git a/31_HLSLPathTracer/app_resources/glsl/common.glsl b/42_FragmentShaderPathTracer/common.glsl similarity index 93% rename from 31_HLSLPathTracer/app_resources/glsl/common.glsl rename to 42_FragmentShaderPathTracer/common.glsl index 6b6e96710..20f7a7359 100644 --- a/31_HLSLPathTracer/app_resources/glsl/common.glsl +++ b/42_FragmentShaderPathTracer/common.glsl @@ -2,27 +2,27 @@ // This file is part of the "Nabla Engine". // For conditions of distribution and use, see copyright notice in nabla.h +// basic settings +#define MAX_DEPTH 3 +#define SAMPLES 128 + // firefly and variance reduction techniques //#define KILL_DIFFUSE_SPECULAR_PATHS //#define VISUALIZE_HIGH_VARIANCE -// debug -//#define NEE_ONLY - -layout(set = 2, binding = 0) uniform sampler2D envMap; +layout(set = 2, binding = 0) uniform sampler2D envMap; layout(set = 2, binding = 1) uniform usamplerBuffer sampleSequence; layout(set = 2, binding = 2) uniform usampler2D scramblebuf; layout(set=0, binding=0, rgba16f) uniform image2D outImage; #ifndef _NBL_GLSL_WORKGROUP_SIZE_ -#define _NBL_GLSL_WORKGROUP_SIZE_ 512 -layout(local_size_x=_NBL_GLSL_WORKGROUP_SIZE_, local_size_y=1, local_size_z=1) in; +#define _NBL_GLSL_WORKGROUP_SIZE_ 16 +layout(local_size_x=_NBL_GLSL_WORKGROUP_SIZE_, local_size_y=_NBL_GLSL_WORKGROUP_SIZE_, local_size_z=1) in; #endif ivec2 getCoordinates() { - ivec2 imageSize = imageSize(outImage); - return ivec2(gl_GlobalInvocationID.x % imageSize.x, gl_GlobalInvocationID.x / imageSize.x); + return ivec2(gl_GlobalInvocationID.xy); } vec2 getTexCoords() { @@ -35,18 +35,15 @@ vec2 getTexCoords() { #include #include #include -#ifdef PERSISTENT_WORKGROUPS -#include -#endif +#include #include -layout(push_constant, row_major) uniform constants +layout(set = 1, binding = 0, row_major, std140) uniform UBO { - mat4 invMVP; - int sampleCount; - int depth; -} PTPushConstant; + nbl_glsl_SBasicViewParameters params; +} cameraData; + #define INVALID_ID_16BIT 0xffffu struct Sphere @@ -54,7 +51,7 @@ struct Sphere vec3 position; float radius2; uint bsdfLightIDs; -}; +}; Sphere Sphere_Sphere(in vec3 position, in float radius, in uint bsdfID, in uint lightID) { @@ -191,7 +188,7 @@ void Rectangle_getNormalBasis(in Rectangle rect, out mat3 basis, out vec2 extent basis[0] = rect.edge0/extents[0]; basis[1] = rect.edge1/extents[1]; basis[2] = normalize(cross(basis[0],basis[1])); -} +} // return intersection distance if found, nbl_glsl_FLT_NAN otherwise float Rectangle_intersect(in Rectangle rect, in vec3 origin, in vec3 direction) @@ -225,7 +222,7 @@ vec3 Rectangle_getNormalTimesArea(in Rectangle rect) #define OP_BITS_OFFSET 0 #define OP_BITS_SIZE 2 struct BSDFNode -{ +{ uvec4 data[2]; }; @@ -389,13 +386,13 @@ vec2 SampleSphericalMap(vec3 v) { vec2 uv = vec2(atan(v.z, v.x), asin(v.y)); uv *= nbl_glsl_RECIPROCAL_PI*0.5; - uv += 0.5; + uv += 0.5; return uv; } void missProgram(in ImmutableRay_t _immutable, inout Payload_t _payload) { - vec3 finalContribution = _payload.throughput; + vec3 finalContribution = _payload.throughput; // #define USE_ENVMAP #ifdef USE_ENVMAP vec2 uv = SampleSphericalMap(_immutable.direction); @@ -418,7 +415,7 @@ nbl_glsl_LightSample nbl_glsl_bsdf_cos_generate(in nbl_glsl_AnisotropicViewSurfa { const float a = BSDFNode_getRoughness(bsdf); const mat2x3 ior = BSDFNode_getEta(bsdf); - + // fresnel stuff for dielectrics float orientedEta, rcpOrientedEta; const bool viewerInsideMedium = nbl_glsl_getOrientedEtas(orientedEta,rcpOrientedEta,interaction.isotropic.NdotV,monochromeEta); @@ -522,7 +519,7 @@ int traceRay(inout float intersectionT, in vec3 origin, in vec3 direction) intersectionT = closerIntersection ? t : intersectionT; objectID = closerIntersection ? i:objectID; - + // allowing early out results in a performance regression, WTF!? //if (anyHit && closerIntersection) //break; @@ -546,7 +543,7 @@ nbl_glsl_LightSample nbl_glsl_light_generate_and_remainder_and_pdf(out vec3 rema { // normally we'd pick from set of lights, using `xi.z` const Light light = lights[0]; - + vec3 L = nbl_glsl_light_generate_and_pdf(pdf,newRayMaxT,origin,interaction,isBSDF,xi,Light_getObjectID(light)); newRayMaxT *= getEndTolerance(depth); @@ -643,7 +640,7 @@ bool closestHitProgram(in uint depth, in uint _sample, inout Ray_t ray, inout nb float bsdfPdf; neeContrib *= nbl_glsl_bsdf_cos_remainder_and_pdf(bsdfPdf,nee_sample,interaction,bsdf,monochromeEta,_cache)*throughput; const float otherGenOverChoice = bsdfPdf*rcpChoiceProb; -#ifndef NEE_ONLY +#if 0 const float otherGenOverLightAndChoice = otherGenOverChoice/lightPdf; neeContrib *= otherGenOverChoice/(1.f+otherGenOverLightAndChoice*otherGenOverLightAndChoice); // MIS weight #else @@ -653,7 +650,7 @@ bool closestHitProgram(in uint depth, in uint _sample, inout Ray_t ray, inout nb ray._payload.accumulation += neeContrib; }} } -#if NEE_ONLY +#if 1 return false; #endif // sample BSDF @@ -666,7 +663,7 @@ bool closestHitProgram(in uint depth, in uint _sample, inout Ray_t ray, inout nb // bsdfSampleL = bsdf_sample.L; } - + // additional threshold const float lumaThroughputThreshold = lumaContributionThreshold; if (bsdfPdf>bsdfPdfThreshold && getLuma(throughput)>lumaThroughputThreshold) @@ -674,7 +671,7 @@ bool closestHitProgram(in uint depth, in uint _sample, inout Ray_t ray, inout nb ray._payload.throughput = throughput; ray._payload.otherTechniqueHeuristic = neeProbability/bsdfPdf; // numerically stable, don't touch ray._payload.otherTechniqueHeuristic *= ray._payload.otherTechniqueHeuristic; - + // trace new ray ray._immutable.origin = intersection+bsdfSampleL*(1.0/*kSceneSize*/)*getStartTolerance(depth); ray._immutable.direction = bsdfSampleL; @@ -691,45 +688,27 @@ bool closestHitProgram(in uint depth, in uint _sample, inout Ray_t ray, inout nb void main() { const ivec2 imageExtents = imageSize(outImage); - -#ifdef PERSISTENT_WORKGROUPS - uint virtualThreadIndex; - for (uint virtualThreadBase = gl_WorkGroupID.x * _NBL_GLSL_WORKGROUP_SIZE_; virtualThreadBase < 1920*1080; virtualThreadBase += gl_NumWorkGroups.x * _NBL_GLSL_WORKGROUP_SIZE_) // not sure why 1280*720 doesn't cover draw surface - { - virtualThreadIndex = virtualThreadBase + gl_LocalInvocationIndex.x; - const ivec2 coords = ivec2(nbl_glsl_morton_decode2d32b(virtualThreadIndex)); -#else const ivec2 coords = getCoordinates(); -#endif - vec2 texCoord = vec2(coords) / vec2(imageExtents); texCoord.y = 1.0 - texCoord.y; if (false == (all(lessThanEqual(ivec2(0),coords)) && all(greaterThan(imageExtents,coords)))) { -#ifdef PERSISTENT_WORKGROUPS - continue; -#else return; -#endif } - if (((PTPushConstant.depth-1)>>MAX_DEPTH_LOG2)>0 || ((PTPushConstant.sampleCount-1)>>MAX_SAMPLES_LOG2)>0) + if (((MAX_DEPTH-1)>>MAX_DEPTH_LOG2)>0 || ((SAMPLES-1)>>MAX_SAMPLES_LOG2)>0) { vec4 pixelCol = vec4(1.0,0.0,0.0,1.0); imageStore(outImage, coords, pixelCol); -#ifdef PERSISTENT_WORKGROUPS - continue; -#else return; -#endif } - nbl_glsl_xoroshiro64star_state_t scramble_start_state = texelFetch(scramblebuf,coords,0).rg; + nbl_glsl_xoroshiro64star_state_t scramble_start_state = texelFetch(scramblebuf,coords,0).rg; const vec2 pixOffsetParam = vec2(1.0)/vec2(textureSize(scramblebuf,0)); - const mat4 invMVP = PTPushConstant.invMVP; - + const mat4 invMVP = inverse(cameraData.params.MVP); + vec4 NDC = vec4(texCoord*vec2(2.0,-2.0)+vec2(-1.0,1.0),0.0,1.0); vec3 camPos; { @@ -740,8 +719,8 @@ void main() vec3 color = vec3(0.0); float meanLumaSquared = 0.0; - // TODO: if we collapse the nested for loop, then all GPUs will get `PTPushConstant.depth` factor speedup, not just NV with separate PC - for (int i=0; i + +#include "../common/CommonAPI.h" +#include "CCamera.hpp" +#include "nbl/ext/ScreenShot/ScreenShot.h" +#include "nbl/video/utilities/CDumbPresentationOracle.h" + +using namespace nbl; +using namespace core; +using namespace ui; + + +using namespace nbl; +using namespace core; +using namespace asset; +using namespace video; + +smart_refctd_ptr createHDRImageView(nbl::core::smart_refctd_ptr device, asset::E_FORMAT colorFormat, uint32_t width, uint32_t height) +{ + smart_refctd_ptr gpuImageViewColorBuffer; + { + IGPUImage::SCreationParams imgInfo; + imgInfo.format = colorFormat; + imgInfo.type = IGPUImage::ET_2D; + imgInfo.extent.width = width; + imgInfo.extent.height = height; + imgInfo.extent.depth = 1u; + imgInfo.mipLevels = 1u; + imgInfo.arrayLayers = 1u; + imgInfo.samples = asset::ICPUImage::ESCF_1_BIT; + imgInfo.flags = static_cast(0u); + imgInfo.usage = core::bitflag(asset::IImage::EUF_STORAGE_BIT) | asset::IImage::EUF_TRANSFER_SRC_BIT; + + auto image = device->createImage(std::move(imgInfo)); + auto imageMemReqs = image->getMemoryReqs(); + imageMemReqs.memoryTypeBits &= device->getPhysicalDevice()->getDeviceLocalMemoryTypeBits(); + device->allocate(imageMemReqs, image.get()); + + IGPUImageView::SCreationParams imgViewInfo; + imgViewInfo.image = std::move(image); + imgViewInfo.format = colorFormat; + imgViewInfo.viewType = IGPUImageView::ET_2D; + imgViewInfo.flags = static_cast(0u); + imgViewInfo.subresourceRange.aspectMask = IImage::E_ASPECT_FLAGS::EAF_COLOR_BIT; + imgViewInfo.subresourceRange.baseArrayLayer = 0u; + imgViewInfo.subresourceRange.baseMipLevel = 0u; + imgViewInfo.subresourceRange.layerCount = 1u; + imgViewInfo.subresourceRange.levelCount = 1u; + + gpuImageViewColorBuffer = device->createImageView(std::move(imgViewInfo)); + } + + return gpuImageViewColorBuffer; +} + +struct ShaderParameters +{ + const uint32_t MaxDepthLog2 = 4; //5 + const uint32_t MaxSamplesLog2 = 10; //18 +} kShaderParameters; + +enum E_LIGHT_GEOMETRY +{ + ELG_SPHERE, + ELG_TRIANGLE, + ELG_RECTANGLE +}; + +struct DispatchInfo_t +{ + uint32_t workGroupCount[3]; +}; + +_NBL_STATIC_INLINE_CONSTEXPR uint32_t DEFAULT_WORK_GROUP_SIZE = 16u; + +DispatchInfo_t getDispatchInfo(uint32_t imgWidth, uint32_t imgHeight) { + DispatchInfo_t ret = {}; + ret.workGroupCount[0] = (uint32_t)core::ceil((float)imgWidth / (float)DEFAULT_WORK_GROUP_SIZE); + ret.workGroupCount[1] = (uint32_t)core::ceil((float)imgHeight / (float)DEFAULT_WORK_GROUP_SIZE); + ret.workGroupCount[2] = 1; + return ret; +} + +int main() +{ + system::IApplicationFramework::GlobalsInit(); + + constexpr uint32_t WIN_W = 1280; + constexpr uint32_t WIN_H = 720; + constexpr uint32_t FBO_COUNT = 2u; + constexpr uint32_t FRAMES_IN_FLIGHT = 5u; + constexpr bool LOG_TIMESTAMP = false; + static_assert(FRAMES_IN_FLIGHT>FBO_COUNT); + + const auto swapchainImageUsage = static_cast(asset::IImage::EUF_COLOR_ATTACHMENT_BIT | asset::IImage::EUF_TRANSFER_DST_BIT); + CommonAPI::InitParams initParams; + initParams.apiType = video::EAT_VULKAN; + initParams.appName = { "Compute Shader PathTracer" }; + initParams.framesInFlight = FRAMES_IN_FLIGHT; + initParams.windowWidth = WIN_W; + initParams.windowHeight = WIN_H; + initParams.swapchainImageCount = FBO_COUNT; + initParams.swapchainImageUsage = swapchainImageUsage; + initParams.depthFormat = asset::EF_D32_SFLOAT; + auto initOutput = CommonAPI::InitWithDefaultExt(std::move(initParams)); + + auto system = std::move(initOutput.system); + auto window = std::move(initParams.window); + auto windowCb = std::move(initParams.windowCb); + auto gl = std::move(initOutput.apiConnection); + auto surface = std::move(initOutput.surface); + auto gpuPhysicalDevice = std::move(initOutput.physicalDevice); + auto device = std::move(initOutput.logicalDevice); + auto queues = std::move(initOutput.queues); + auto graphicsQueue = queues[CommonAPI::InitOutput::EQT_GRAPHICS]; + auto transferUpQueue = queues[CommonAPI::InitOutput::EQT_TRANSFER_UP]; + auto computeQueue = queues[CommonAPI::InitOutput::EQT_COMPUTE]; + auto renderpass = std::move(initOutput.renderToSwapchainRenderpass); + auto assetManager = std::move(initOutput.assetManager); + auto cpu2gpuParams = std::move(initOutput.cpu2gpuParams); + auto logger = std::move(initOutput.logger); + auto inputSystem = std::move(initOutput.inputSystem); + auto utilities = std::move(initOutput.utilities); + auto graphicsCommandPools = std::move(initOutput.commandPools[CommonAPI::InitOutput::EQT_GRAPHICS]); + auto computeCommandPools = std::move(initOutput.commandPools[CommonAPI::InitOutput::EQT_COMPUTE]); + auto swapchainCreationParams = std::move(initOutput.swapchainCreationParams); + + core::smart_refctd_ptr swapchain = nullptr; + CommonAPI::createSwapchain(std::move(device), swapchainCreationParams, WIN_W, WIN_H, swapchain); + assert(swapchain); + auto fbo = CommonAPI::createFBOWithSwapchainImages( + swapchain->getImageCount(), WIN_W, WIN_H, + device, swapchain, renderpass, + asset::EF_D32_SFLOAT + ); + + auto graphicsCmdPoolQueueFamIdx = graphicsQueue->getFamilyIndex(); + + nbl::video::IGPUObjectFromAssetConverter CPU2GPU; + + core::smart_refctd_ptr cmdbuf[FRAMES_IN_FLIGHT]; + for (uint32_t i = 0u; i < FRAMES_IN_FLIGHT; i++) + device->createCommandBuffers(graphicsCommandPools[i].get(), video::IGPUCommandBuffer::EL_PRIMARY, 1, cmdbuf+i); + + constexpr uint32_t maxDescriptorCount = 256u; + constexpr uint32_t PoolSizesCount = 5u; + + nbl::video::IDescriptorPool::SCreateInfo createInfo; + createInfo.maxDescriptorCount[static_cast(nbl::asset::IDescriptor::E_TYPE::ET_STORAGE_BUFFER)] = maxDescriptorCount * 1; + createInfo.maxDescriptorCount[static_cast(nbl::asset::IDescriptor::E_TYPE::ET_STORAGE_IMAGE)] = maxDescriptorCount * 8; + createInfo.maxDescriptorCount[static_cast(nbl::asset::IDescriptor::E_TYPE::ET_COMBINED_IMAGE_SAMPLER)] = maxDescriptorCount * 2; + createInfo.maxDescriptorCount[static_cast(nbl::asset::IDescriptor::E_TYPE::ET_UNIFORM_TEXEL_BUFFER)] = maxDescriptorCount * 1; + createInfo.maxDescriptorCount[static_cast(nbl::asset::IDescriptor::E_TYPE::ET_UNIFORM_BUFFER)] = maxDescriptorCount * 1; + createInfo.maxSets = maxDescriptorCount; + + auto descriptorPool = device->createDescriptorPool(std::move(createInfo)); + + const auto timestampQueryPool = device->createQueryPool({ + .queryType = video::IQueryPool::EQT_TIMESTAMP, + .queryCount = 2u + }); + + // Camera + core::vectorSIMDf cameraPosition(0, 5, -10); + matrix4SIMD proj = matrix4SIMD::buildProjectionMatrixPerspectiveFovRH(core::radians(60.0f), video::ISurface::getTransformedAspectRatio(swapchain->getPreTransform(), WIN_W, WIN_H), 0.01f, 500.0f); + Camera cam = Camera(cameraPosition, core::vectorSIMDf(0, 0, 0), proj); + + IGPUDescriptorSetLayout::SBinding descriptorSet0Bindings[] = { + { 0u, nbl::asset::IDescriptor::E_TYPE::ET_STORAGE_IMAGE, IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, IShader::ESS_COMPUTE, 1u, nullptr }, + }; + IGPUDescriptorSetLayout::SBinding uboBinding + { 0u, nbl::asset::IDescriptor::E_TYPE::ET_UNIFORM_BUFFER, IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, IShader::ESS_COMPUTE, 1u, nullptr }; + IGPUDescriptorSetLayout::SBinding descriptorSet3Bindings[] = { + { 0u, nbl::asset::IDescriptor::E_TYPE::ET_COMBINED_IMAGE_SAMPLER, IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, IShader::ESS_COMPUTE, 1u, nullptr }, + { 1u, nbl::asset::IDescriptor::E_TYPE::ET_UNIFORM_TEXEL_BUFFER, IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, IShader::ESS_COMPUTE, 1u, nullptr }, + { 2u, nbl::asset::IDescriptor::E_TYPE::ET_COMBINED_IMAGE_SAMPLER, IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, IShader::ESS_COMPUTE, 1u, nullptr }, + }; + + auto gpuDescriptorSetLayout0 = device->createDescriptorSetLayout(descriptorSet0Bindings, descriptorSet0Bindings + 1u); + auto gpuDescriptorSetLayout1 = device->createDescriptorSetLayout(&uboBinding, &uboBinding + 1u); + auto gpuDescriptorSetLayout2 = device->createDescriptorSetLayout(descriptorSet3Bindings, descriptorSet3Bindings+3u); + + auto createGpuResources = [&](std::string pathToShader) -> core::smart_refctd_ptr + { + asset::IAssetLoader::SAssetLoadParams params{}; + params.logger = logger.get(); + //params.relativeDir = tmp.c_str(); + auto spec = assetManager->getAsset(pathToShader,params).getContents(); + + if (spec.empty()) + assert(false); + + auto cpuComputeSpecializedShader = core::smart_refctd_ptr_static_cast(*spec.begin()); + + ISpecializedShader::SInfo info = cpuComputeSpecializedShader->getSpecializationInfo(); + info.m_backingBuffer = core::make_smart_refctd_ptr(sizeof(ShaderParameters)); + memcpy(info.m_backingBuffer->getPointer(),&kShaderParameters,sizeof(ShaderParameters)); + info.m_entries = core::make_refctd_dynamic_array>(2u); + for (uint32_t i=0; i<2; i++) + info.m_entries->operator[](i) = {i,(uint32_t)(i*sizeof(uint32_t)),sizeof(uint32_t)}; + + + cpuComputeSpecializedShader->setSpecializationInfo(std::move(info)); + + auto gpuComputeSpecializedShader = CPU2GPU.getGPUObjectsFromAssets(&cpuComputeSpecializedShader, &cpuComputeSpecializedShader + 1, cpu2gpuParams)->front(); + + auto gpuPipelineLayout = device->createPipelineLayout(nullptr, nullptr, core::smart_refctd_ptr(gpuDescriptorSetLayout0), core::smart_refctd_ptr(gpuDescriptorSetLayout1), core::smart_refctd_ptr(gpuDescriptorSetLayout2), nullptr); + + auto gpuPipeline = device->createComputePipeline(nullptr, std::move(gpuPipelineLayout), std::move(gpuComputeSpecializedShader)); + + return gpuPipeline; + }; + + E_LIGHT_GEOMETRY lightGeom = ELG_SPHERE; + constexpr const char* shaderPaths[] = {"../litBySphere.comp","../litByTriangle.comp","../litByRectangle.comp"}; + auto gpuComputePipeline = createGpuResources(shaderPaths[lightGeom]); + + DispatchInfo_t dispatchInfo = getDispatchInfo(WIN_W, WIN_H); + + auto createImageView = [&](std::string pathToOpenEXRHDRIImage) + { +#ifndef _NBL_COMPILE_WITH_OPENEXR_LOADER_ + assert(false); +#endif + + auto pathToTexture = pathToOpenEXRHDRIImage; + IAssetLoader::SAssetLoadParams lp(0ull, nullptr, IAssetLoader::ECF_DONT_CACHE_REFERENCES); + auto cpuTexture = assetManager->getAsset(pathToTexture, lp); + auto cpuTextureContents = cpuTexture.getContents(); + assert(!cpuTextureContents.empty()); + auto cpuImage = core::smart_refctd_ptr_static_cast(*cpuTextureContents.begin()); + cpuImage->setImageUsageFlags(IImage::E_USAGE_FLAGS::EUF_SAMPLED_BIT); + + ICPUImageView::SCreationParams viewParams; + viewParams.flags = static_cast(0u); + viewParams.image = cpuImage; + viewParams.format = viewParams.image->getCreationParameters().format; + viewParams.viewType = IImageView::ET_2D; + viewParams.subresourceRange.aspectMask = IImage::E_ASPECT_FLAGS::EAF_COLOR_BIT; + viewParams.subresourceRange.baseArrayLayer = 0u; + viewParams.subresourceRange.layerCount = 1u; + viewParams.subresourceRange.baseMipLevel = 0u; + viewParams.subresourceRange.levelCount = 1u; + + auto cpuImageView = ICPUImageView::create(std::move(viewParams)); + + cpu2gpuParams.beginCommandBuffers(); + auto gpuImageView = CPU2GPU.getGPUObjectsFromAssets(&cpuImageView, &cpuImageView + 1u, cpu2gpuParams)->front(); + cpu2gpuParams.waitForCreationToComplete(false); + + return gpuImageView; + }; + + auto gpuEnvmapImageView = createImageView("../../media/envmap/envmap_0.exr"); + + smart_refctd_ptr gpuSequenceBufferView; + { + const uint32_t MaxDimensions = 3u<(sizeof(uint32_t)*MaxDimensions*MaxSamples); + + core::OwenSampler sampler(MaxDimensions, 0xdeadbeefu); + //core::SobolSampler sampler(MaxDimensions); + + auto out = reinterpret_cast(sampleSequence->getPointer()); + for (auto dim=0u; dimcreateFilledDeviceLocalBufferOnDedMem(graphicsQueue, sampleSequence->getSize(), sampleSequence->getPointer()); + core::smart_refctd_ptr gpuSequenceBuffer; + { + IGPUBuffer::SCreationParams params = {}; + const size_t size = sampleSequence->getSize(); + params.usage = core::bitflag(asset::IBuffer::EUF_TRANSFER_DST_BIT) | asset::IBuffer::EUF_UNIFORM_TEXEL_BUFFER_BIT; + params.size = size; + gpuSequenceBuffer = device->createBuffer(std::move(params)); + auto gpuSequenceBufferMemReqs = gpuSequenceBuffer->getMemoryReqs(); + gpuSequenceBufferMemReqs.memoryTypeBits &= device->getPhysicalDevice()->getDeviceLocalMemoryTypeBits(); + device->allocate(gpuSequenceBufferMemReqs, gpuSequenceBuffer.get()); + utilities->updateBufferRangeViaStagingBufferAutoSubmit(asset::SBufferRange{0u,size,gpuSequenceBuffer},sampleSequence->getPointer(), graphicsQueue); + } + gpuSequenceBufferView = device->createBufferView(gpuSequenceBuffer.get(), asset::EF_R32G32B32_UINT); + } + + smart_refctd_ptr gpuScrambleImageView; + { + IGPUImage::SCreationParams imgParams; + imgParams.flags = static_cast(0u); + imgParams.type = IImage::ET_2D; + imgParams.format = EF_R32G32_UINT; + imgParams.extent = {WIN_W, WIN_H,1u}; + imgParams.mipLevels = 1u; + imgParams.arrayLayers = 1u; + imgParams.samples = IImage::ESCF_1_BIT; + imgParams.usage = core::bitflag(IImage::EUF_SAMPLED_BIT) | IImage::EUF_TRANSFER_DST_BIT; + imgParams.initialLayout = asset::IImage::EL_UNDEFINED; + + IGPUImage::SBufferCopy region = {}; + region.bufferOffset = 0u; + region.bufferRowLength = 0u; + region.bufferImageHeight = 0u; + region.imageExtent = imgParams.extent; + region.imageOffset = {0u,0u,0u}; + region.imageSubresource.layerCount = 1u; + region.imageSubresource.aspectMask = IImage::E_ASPECT_FLAGS::EAF_COLOR_BIT; + + constexpr auto ScrambleStateChannels = 2u; + const auto renderPixelCount = imgParams.extent.width*imgParams.extent.height; + core::vector random(renderPixelCount*ScrambleStateChannels); + { + core::RandomSampler rng(0xbadc0ffeu); + for (auto& pixel : random) + pixel = rng.nextSample(); + } + + // TODO: Temp Fix because createFilledDeviceLocalBufferOnDedMem doesn't take in params + // auto buffer = utilities->createFilledDeviceLocalBufferOnDedMem(graphicsQueue, random.size()*sizeof(uint32_t), random.data()); + core::smart_refctd_ptr buffer; + { + IGPUBuffer::SCreationParams params = {}; + const size_t size = random.size() * sizeof(uint32_t); + params.usage = core::bitflag(asset::IBuffer::EUF_TRANSFER_DST_BIT) | asset::IBuffer::EUF_TRANSFER_SRC_BIT; + params.size = size; + buffer = device->createBuffer(std::move(params)); + auto bufferMemReqs = buffer->getMemoryReqs(); + bufferMemReqs.memoryTypeBits &= device->getPhysicalDevice()->getDeviceLocalMemoryTypeBits(); + device->allocate(bufferMemReqs, buffer.get()); + utilities->updateBufferRangeViaStagingBufferAutoSubmit(asset::SBufferRange{0u,size,buffer},random.data(),graphicsQueue); + } + + IGPUImageView::SCreationParams viewParams; + viewParams.flags = static_cast(0u); + // TODO: Replace this IGPUBuffer -> IGPUImage to using image upload utility + viewParams.image = utilities->createFilledDeviceLocalImageOnDedMem(std::move(imgParams), buffer.get(), 1u, ®ion, graphicsQueue); + viewParams.viewType = IGPUImageView::ET_2D; + viewParams.format = EF_R32G32_UINT; + viewParams.subresourceRange.aspectMask = IImage::E_ASPECT_FLAGS::EAF_COLOR_BIT; + viewParams.subresourceRange.levelCount = 1u; + viewParams.subresourceRange.layerCount = 1u; + gpuScrambleImageView = device->createImageView(std::move(viewParams)); + } + + // Create Out Image TODO + constexpr uint32_t MAX_FBO_COUNT = 4u; + smart_refctd_ptr outHDRImageViews[MAX_FBO_COUNT] = {}; + assert(MAX_FBO_COUNT >= swapchain->getImageCount()); + for(uint32_t i = 0; i < swapchain->getImageCount(); ++i) { + outHDRImageViews[i] = createHDRImageView(device, asset::EF_R16G16B16A16_SFLOAT, WIN_W, WIN_H); + } + + core::smart_refctd_ptr descriptorSets0[FBO_COUNT] = {}; + for(uint32_t i = 0; i < FBO_COUNT; ++i) + { + auto & descSet = descriptorSets0[i]; + descSet = descriptorPool->createDescriptorSet(core::smart_refctd_ptr(gpuDescriptorSetLayout0)); + video::IGPUDescriptorSet::SWriteDescriptorSet writeDescriptorSet; + writeDescriptorSet.dstSet = descSet.get(); + writeDescriptorSet.binding = 0; + writeDescriptorSet.count = 1u; + writeDescriptorSet.arrayElement = 0u; + writeDescriptorSet.descriptorType = asset::IDescriptor::E_TYPE::ET_STORAGE_IMAGE; + video::IGPUDescriptorSet::SDescriptorInfo info; + { + info.desc = outHDRImageViews[i]; + info.info.image.sampler = nullptr; + info.info.image.imageLayout = asset::IImage::EL_GENERAL; + } + writeDescriptorSet.info = &info; + device->updateDescriptorSets(1u, &writeDescriptorSet, 0u, nullptr); + } + + struct SBasicViewParametersAligned + { + SBasicViewParameters uboData; + }; + + IGPUBuffer::SCreationParams gpuuboParams = {}; + gpuuboParams.usage = core::bitflag(IGPUBuffer::EUF_UNIFORM_BUFFER_BIT) | IGPUBuffer::EUF_TRANSFER_DST_BIT; + gpuuboParams.size = sizeof(SBasicViewParametersAligned); + auto gpuubo = device->createBuffer(std::move(gpuuboParams)); + auto gpuuboMemReqs = gpuubo->getMemoryReqs(); + gpuuboMemReqs.memoryTypeBits &= device->getPhysicalDevice()->getDeviceLocalMemoryTypeBits(); + device->allocate(gpuuboMemReqs, gpuubo.get()); + + auto uboDescriptorSet1 = descriptorPool->createDescriptorSet(core::smart_refctd_ptr(gpuDescriptorSetLayout1)); + { + video::IGPUDescriptorSet::SWriteDescriptorSet uboWriteDescriptorSet; + uboWriteDescriptorSet.dstSet = uboDescriptorSet1.get(); + uboWriteDescriptorSet.binding = 0; + uboWriteDescriptorSet.count = 1u; + uboWriteDescriptorSet.arrayElement = 0u; + uboWriteDescriptorSet.descriptorType = asset::IDescriptor::E_TYPE::ET_UNIFORM_BUFFER; + video::IGPUDescriptorSet::SDescriptorInfo info; + { + info.desc = gpuubo; + info.info.buffer.offset = 0ull; + info.info.buffer.size = sizeof(SBasicViewParametersAligned); + } + uboWriteDescriptorSet.info = &info; + device->updateDescriptorSets(1u, &uboWriteDescriptorSet, 0u, nullptr); + } + + ISampler::SParams samplerParams0 = { ISampler::ETC_CLAMP_TO_EDGE, ISampler::ETC_CLAMP_TO_EDGE, ISampler::ETC_CLAMP_TO_EDGE, ISampler::ETBC_FLOAT_OPAQUE_BLACK, ISampler::ETF_LINEAR, ISampler::ETF_LINEAR, ISampler::ESMM_LINEAR, 0u, false, ECO_ALWAYS }; + auto sampler0 = device->createSampler(samplerParams0); + ISampler::SParams samplerParams1 = { ISampler::ETC_CLAMP_TO_EDGE, ISampler::ETC_CLAMP_TO_EDGE, ISampler::ETC_CLAMP_TO_EDGE, ISampler::ETBC_INT_OPAQUE_BLACK, ISampler::ETF_NEAREST, ISampler::ETF_NEAREST, ISampler::ESMM_NEAREST, 0u, false, ECO_ALWAYS }; + auto sampler1 = device->createSampler(samplerParams1); + + auto descriptorSet2 = descriptorPool->createDescriptorSet(core::smart_refctd_ptr(gpuDescriptorSetLayout2)); + { + constexpr auto kDescriptorCount = 3; + IGPUDescriptorSet::SWriteDescriptorSet samplerWriteDescriptorSet[kDescriptorCount]; + IGPUDescriptorSet::SDescriptorInfo samplerDescriptorInfo[kDescriptorCount]; + for (auto i=0; iupdateDescriptorSets(kDescriptorCount, samplerWriteDescriptorSet, 0u, nullptr); + } + + constexpr uint32_t FRAME_COUNT = 500000u; + + core::smart_refctd_ptr frameComplete[FRAMES_IN_FLIGHT] = { nullptr }; + core::smart_refctd_ptr imageAcquire[FRAMES_IN_FLIGHT] = { nullptr }; + core::smart_refctd_ptr renderFinished[FRAMES_IN_FLIGHT] = { nullptr }; + for (uint32_t i=0u; icreateSemaphore(); + renderFinished[i] = device->createSemaphore(); + } + + CDumbPresentationOracle oracle; + oracle.reportBeginFrameRecord(); + constexpr uint64_t MAX_TIMEOUT = 99999999999999ull; + + // polling for events! + CommonAPI::InputSystem::ChannelReader mouse; + CommonAPI::InputSystem::ChannelReader keyboard; + + uint32_t resourceIx = 0; + while(windowCb->isWindowOpen()) + { + resourceIx++; + if(resourceIx >= FRAMES_IN_FLIGHT) { + resourceIx = 0; + } + + oracle.reportEndFrameRecord(); + double dt = oracle.getDeltaTimeInMicroSeconds() / 1000.0; + auto nextPresentationTimeStamp = oracle.getNextPresentationTimeStamp(); + oracle.reportBeginFrameRecord(); + + // Input + inputSystem->getDefaultMouse(&mouse); + inputSystem->getDefaultKeyboard(&keyboard); + + cam.beginInputProcessing(nextPresentationTimeStamp); + mouse.consumeEvents([&](const IMouseEventChannel::range_t& events) -> void { cam.mouseProcess(events); }, logger.get()); + keyboard.consumeEvents([&](const IKeyboardEventChannel::range_t& events) -> void { cam.keyboardProcess(events); }, logger.get()); + cam.endInputProcessing(nextPresentationTimeStamp); + + auto& cb = cmdbuf[resourceIx]; + auto& fence = frameComplete[resourceIx]; + if (fence) + while (device->waitForFences(1u,&fence.get(),false,MAX_TIMEOUT)==video::IGPUFence::ES_TIMEOUT) + { + } + else + fence = device->createFence(static_cast(0)); + + const auto viewMatrix = cam.getViewMatrix(); + const auto viewProjectionMatrix = matrix4SIMD::concatenateBFollowedByAPrecisely( + video::ISurface::getSurfaceTransformationMatrix(swapchain->getPreTransform()), + cam.getConcatenatedMatrix() + ); + + // safe to proceed + cb->begin(IGPUCommandBuffer::EU_NONE); + cb->resetQueryPool(timestampQueryPool.get(), 0u, 2u); + + // renderpass + uint32_t imgnum = 0u; + swapchain->acquireNextImage(MAX_TIMEOUT,imageAcquire[resourceIx].get(),nullptr,&imgnum); + { + auto mv = viewMatrix; + auto mvp = viewProjectionMatrix; + core::matrix3x4SIMD normalMat; + mv.getSub3x3InverseTranspose(normalMat); + + SBasicViewParametersAligned viewParams; + memcpy(viewParams.uboData.MV, mv.pointer(), sizeof(mv)); + memcpy(viewParams.uboData.MVP, mvp.pointer(), sizeof(mvp)); + memcpy(viewParams.uboData.NormalMat, normalMat.pointer(), sizeof(normalMat)); + + asset::SBufferRange range; + range.buffer = gpuubo; + range.offset = 0ull; + range.size = sizeof(viewParams); + utilities->updateBufferRangeViaStagingBufferAutoSubmit(range, &viewParams, graphicsQueue); + } + + // TRANSITION outHDRImageViews[imgnum] to EIL_GENERAL (because of descriptorSets0 -> ComputeShader Writes into the image) + { + IGPUCommandBuffer::SImageMemoryBarrier imageBarriers[3u] = {}; + imageBarriers[0].barrier.srcAccessMask = asset::EAF_NONE; + imageBarriers[0].barrier.dstAccessMask = static_cast(asset::EAF_SHADER_WRITE_BIT); + imageBarriers[0].oldLayout = asset::IImage::EL_UNDEFINED; + imageBarriers[0].newLayout = asset::IImage::EL_GENERAL; + imageBarriers[0].srcQueueFamilyIndex = graphicsCmdPoolQueueFamIdx; + imageBarriers[0].dstQueueFamilyIndex = graphicsCmdPoolQueueFamIdx; + imageBarriers[0].image = outHDRImageViews[imgnum]->getCreationParameters().image; + imageBarriers[0].subresourceRange.aspectMask = asset::IImage::EAF_COLOR_BIT; + imageBarriers[0].subresourceRange.baseMipLevel = 0u; + imageBarriers[0].subresourceRange.levelCount = 1; + imageBarriers[0].subresourceRange.baseArrayLayer = 0u; + imageBarriers[0].subresourceRange.layerCount = 1; + + imageBarriers[1].barrier.srcAccessMask = asset::EAF_NONE; + imageBarriers[1].barrier.dstAccessMask = static_cast(asset::EAF_SHADER_READ_BIT); + imageBarriers[1].oldLayout = asset::IImage::EL_UNDEFINED; + imageBarriers[1].newLayout = asset::IImage::EL_SHADER_READ_ONLY_OPTIMAL; + imageBarriers[1].srcQueueFamilyIndex = graphicsCmdPoolQueueFamIdx; + imageBarriers[1].dstQueueFamilyIndex = graphicsCmdPoolQueueFamIdx; + imageBarriers[1].image = gpuScrambleImageView->getCreationParameters().image; + imageBarriers[1].subresourceRange.aspectMask = asset::IImage::EAF_COLOR_BIT; + imageBarriers[1].subresourceRange.baseMipLevel = 0u; + imageBarriers[1].subresourceRange.levelCount = 1; + imageBarriers[1].subresourceRange.baseArrayLayer = 0u; + imageBarriers[1].subresourceRange.layerCount = 1; + + imageBarriers[2].barrier.srcAccessMask = asset::EAF_NONE; + imageBarriers[2].barrier.dstAccessMask = static_cast(asset::EAF_SHADER_READ_BIT); + imageBarriers[2].oldLayout = asset::IImage::EL_UNDEFINED; + imageBarriers[2].newLayout = asset::IImage::EL_SHADER_READ_ONLY_OPTIMAL; + imageBarriers[2].srcQueueFamilyIndex = graphicsCmdPoolQueueFamIdx; + imageBarriers[2].dstQueueFamilyIndex = graphicsCmdPoolQueueFamIdx; + imageBarriers[2].image = gpuEnvmapImageView->getCreationParameters().image; + imageBarriers[2].subresourceRange.aspectMask = asset::IImage::EAF_COLOR_BIT; + imageBarriers[2].subresourceRange.baseMipLevel = 0u; + imageBarriers[2].subresourceRange.levelCount = gpuEnvmapImageView->getCreationParameters().subresourceRange.levelCount; + imageBarriers[2].subresourceRange.baseArrayLayer = 0u; + imageBarriers[2].subresourceRange.layerCount = gpuEnvmapImageView->getCreationParameters().subresourceRange.layerCount; + + cb->pipelineBarrier(asset::EPSF_TOP_OF_PIPE_BIT, asset::EPSF_COMPUTE_SHADER_BIT, asset::EDF_NONE, 0u, nullptr, 0u, nullptr, 3u, imageBarriers); + } + + // cube envmap handle + { + cb->writeTimestamp(asset::E_PIPELINE_STAGE_FLAGS::EPSF_TOP_OF_PIPE_BIT, timestampQueryPool.get(), 0u); + cb->bindComputePipeline(gpuComputePipeline.get()); + cb->bindDescriptorSets(EPBP_COMPUTE, gpuComputePipeline->getLayout(), 0u, 1u, &descriptorSets0[imgnum].get()); + cb->bindDescriptorSets(EPBP_COMPUTE, gpuComputePipeline->getLayout(), 1u, 1u, &uboDescriptorSet1.get()); + cb->bindDescriptorSets(EPBP_COMPUTE, gpuComputePipeline->getLayout(), 2u, 1u, &descriptorSet2.get()); + cb->dispatch(dispatchInfo.workGroupCount[0], dispatchInfo.workGroupCount[1], dispatchInfo.workGroupCount[2]); + cb->writeTimestamp(asset::E_PIPELINE_STAGE_FLAGS::EPSF_BOTTOM_OF_PIPE_BIT, timestampQueryPool.get(), 1u); + } + // TODO: tone mapping and stuff + + // Copy HDR Image to SwapChain + auto srcImgViewCreationParams = outHDRImageViews[imgnum]->getCreationParameters(); + auto dstImgViewCreationParams = fbo->begin()[imgnum]->getCreationParameters().attachments[0]->getCreationParameters(); + + // Getting Ready for Blit + // TRANSITION outHDRImageViews[imgnum] to EIL_TRANSFER_SRC_OPTIMAL + // TRANSITION `fbo[imgnum]->getCreationParameters().attachments[0]` to EIL_TRANSFER_DST_OPTIMAL + { + IGPUCommandBuffer::SImageMemoryBarrier imageBarriers[2u] = {}; + imageBarriers[0].barrier.srcAccessMask = asset::EAF_NONE; + imageBarriers[0].barrier.dstAccessMask = asset::EAF_TRANSFER_WRITE_BIT; + imageBarriers[0].oldLayout = asset::IImage::EL_UNDEFINED; + imageBarriers[0].newLayout = asset::IImage::EL_TRANSFER_SRC_OPTIMAL; + imageBarriers[0].srcQueueFamilyIndex = graphicsCmdPoolQueueFamIdx; + imageBarriers[0].dstQueueFamilyIndex = graphicsCmdPoolQueueFamIdx; + imageBarriers[0].image = srcImgViewCreationParams.image; + imageBarriers[0].subresourceRange.aspectMask = asset::IImage::EAF_COLOR_BIT; + imageBarriers[0].subresourceRange.baseMipLevel = 0u; + imageBarriers[0].subresourceRange.levelCount = 1; + imageBarriers[0].subresourceRange.baseArrayLayer = 0u; + imageBarriers[0].subresourceRange.layerCount = 1; + + imageBarriers[1].barrier.srcAccessMask = asset::EAF_NONE; + imageBarriers[1].barrier.dstAccessMask = asset::EAF_TRANSFER_WRITE_BIT; + imageBarriers[1].oldLayout = asset::IImage::EL_UNDEFINED; + imageBarriers[1].newLayout = asset::IImage::EL_TRANSFER_DST_OPTIMAL; + imageBarriers[1].srcQueueFamilyIndex = graphicsCmdPoolQueueFamIdx; + imageBarriers[1].dstQueueFamilyIndex = graphicsCmdPoolQueueFamIdx; + imageBarriers[1].image = dstImgViewCreationParams.image; + imageBarriers[1].subresourceRange.aspectMask = asset::IImage::EAF_COLOR_BIT; + imageBarriers[1].subresourceRange.baseMipLevel = 0u; + imageBarriers[1].subresourceRange.levelCount = 1; + imageBarriers[1].subresourceRange.baseArrayLayer = 0u; + imageBarriers[1].subresourceRange.layerCount = 1; + cb->pipelineBarrier(asset::EPSF_TRANSFER_BIT, asset::EPSF_TRANSFER_BIT, asset::EDF_NONE, 0u, nullptr, 0u, nullptr, 2u, imageBarriers); + } + + // Blit Image + { + SImageBlit blit = {}; + blit.srcOffsets[0] = {0, 0, 0}; + blit.srcOffsets[1] = {WIN_W, WIN_H, 1}; + + blit.srcSubresource.aspectMask = srcImgViewCreationParams.subresourceRange.aspectMask; + blit.srcSubresource.mipLevel = srcImgViewCreationParams.subresourceRange.baseMipLevel; + blit.srcSubresource.baseArrayLayer = srcImgViewCreationParams.subresourceRange.baseArrayLayer; + blit.srcSubresource.layerCount = srcImgViewCreationParams.subresourceRange.layerCount; + blit.dstOffsets[0] = {0, 0, 0}; + blit.dstOffsets[1] = {WIN_W, WIN_H, 1}; + blit.dstSubresource.aspectMask = dstImgViewCreationParams.subresourceRange.aspectMask; + blit.dstSubresource.mipLevel = dstImgViewCreationParams.subresourceRange.baseMipLevel; + blit.dstSubresource.baseArrayLayer = dstImgViewCreationParams.subresourceRange.baseArrayLayer; + blit.dstSubresource.layerCount = dstImgViewCreationParams.subresourceRange.layerCount; + + auto srcImg = srcImgViewCreationParams.image; + auto dstImg = dstImgViewCreationParams.image; + + cb->blitImage(srcImg.get(), asset::IImage::EL_TRANSFER_SRC_OPTIMAL, dstImg.get(), asset::IImage::EL_TRANSFER_DST_OPTIMAL, 1u, &blit , ISampler::ETF_NEAREST); + } + + // TRANSITION `fbo[imgnum]->getCreationParameters().attachments[0]` to EIL_PRESENT + { + IGPUCommandBuffer::SImageMemoryBarrier imageBarriers[1u] = {}; + imageBarriers[0].barrier.srcAccessMask = asset::EAF_TRANSFER_WRITE_BIT; + imageBarriers[0].barrier.dstAccessMask = asset::EAF_NONE; + imageBarriers[0].oldLayout = asset::IImage::EL_TRANSFER_DST_OPTIMAL; + imageBarriers[0].newLayout = asset::IImage::EL_PRESENT_SRC; + imageBarriers[0].srcQueueFamilyIndex = graphicsCmdPoolQueueFamIdx; + imageBarriers[0].dstQueueFamilyIndex = graphicsCmdPoolQueueFamIdx; + imageBarriers[0].image = dstImgViewCreationParams.image; + imageBarriers[0].subresourceRange.aspectMask = asset::IImage::EAF_COLOR_BIT; + imageBarriers[0].subresourceRange.baseMipLevel = 0u; + imageBarriers[0].subresourceRange.levelCount = 1; + imageBarriers[0].subresourceRange.baseArrayLayer = 0u; + imageBarriers[0].subresourceRange.layerCount = 1; + cb->pipelineBarrier(asset::EPSF_TRANSFER_BIT, asset::EPSF_TOP_OF_PIPE_BIT, asset::EDF_NONE, 0u, nullptr, 0u, nullptr, 1u, imageBarriers); + } + + cb->end(); + device->resetFences(1, &fence.get()); + CommonAPI::Submit(device.get(), cb.get(), graphicsQueue, imageAcquire[resourceIx].get(), renderFinished[resourceIx].get(), fence.get()); + CommonAPI::Present(device.get(), swapchain.get(), graphicsQueue, renderFinished[resourceIx].get(), imgnum); + + if (LOG_TIMESTAMP) + { + std::array timestamps{}; + auto queryResultFlags = core::bitflag(video::IQueryPool::EQRF_WAIT_BIT) | video::IQueryPool::EQRF_WITH_AVAILABILITY_BIT | video::IQueryPool::EQRF_64_BIT; + device->getQueryPoolResults(timestampQueryPool.get(), 0u, 2u, sizeof(timestamps), timestamps.data(), sizeof(uint64_t) * 2ull, queryResultFlags); + const float timePassed = (timestamps[2] - timestamps[0]) * device->getPhysicalDevice()->getLimits().timestampPeriodInNanoSeconds; + logger->log("Time Passed (Seconds) = %f", system::ILogger::ELL_INFO, (timePassed * 1e-9)); + logger->log("Timestamps availablity: %d, %d", system::ILogger::ELL_INFO, timestamps[1], timestamps[3]); + } + } + + const auto& fboCreationParams = fbo->begin()[0]->getCreationParameters(); + auto gpuSourceImageView = fboCreationParams.attachments[0]; + + device->waitIdle(); + + // bool status = ext::ScreenShot::createScreenShot(device.get(), queues[decltype(initOutput)::EQT_TRANSFER_UP], renderFinished[0].get(), gpuSourceImageView.get(), assetManager.get(), "ScreenShot.png"); + // assert(status); + + return 0; +} diff --git a/42_FragmentShaderPathTracer/pipeline.groovy b/42_FragmentShaderPathTracer/pipeline.groovy new file mode 100644 index 000000000..9e3a71cf3 --- /dev/null +++ b/42_FragmentShaderPathTracer/pipeline.groovy @@ -0,0 +1,50 @@ +import org.DevshGraphicsProgramming.Agent +import org.DevshGraphicsProgramming.BuilderInfo +import org.DevshGraphicsProgramming.IBuilder + +class CFragmentShaderPathTracerBuilder extends IBuilder +{ + public CFragmentShaderPathTracerBuilder(Agent _agent, _info) + { + super(_agent, _info) + } + + @Override + public boolean prepare(Map axisMapping) + { + return true + } + + @Override + public boolean build(Map axisMapping) + { + IBuilder.CONFIGURATION config = axisMapping.get("CONFIGURATION") + IBuilder.BUILD_TYPE buildType = axisMapping.get("BUILD_TYPE") + + def nameOfBuildDirectory = getNameOfBuildDirectory(buildType) + def nameOfConfig = getNameOfConfig(config) + + agent.execute("cmake --build ${info.rootProjectPath}/${nameOfBuildDirectory}/${info.targetProjectPathRelativeToRoot} --target ${info.targetBaseName} --config ${nameOfConfig} -j12 -v") + + return true + } + + @Override + public boolean test(Map axisMapping) + { + return true + } + + @Override + public boolean install(Map axisMapping) + { + return true + } +} + +def create(Agent _agent, _info) +{ + return new CFragmentShaderPathTracerBuilder(_agent, _info) +} + +return this \ No newline at end of file diff --git a/43_SumAndCDFFilters/CMakeLists.txt b/43_SumAndCDFFilters/CMakeLists.txt new file mode 100644 index 000000000..a476b6203 --- /dev/null +++ b/43_SumAndCDFFilters/CMakeLists.txt @@ -0,0 +1,7 @@ + +include(common RESULT_VARIABLE RES) +if(NOT RES) + message(FATAL_ERROR "common.cmake not found. Should be in {repo_root}/cmake directory") +endif() + +nbl_create_executable_project("" "" "" "" "${NBL_EXECUTABLE_PROJECT_CREATION_PCH_TARGET}") \ No newline at end of file diff --git a/11_FFT/config.json.template b/43_SumAndCDFFilters/config.json.template similarity index 75% rename from 11_FFT/config.json.template rename to 43_SumAndCDFFilters/config.json.template index 717d05d53..f961745c1 100644 --- a/11_FFT/config.json.template +++ b/43_SumAndCDFFilters/config.json.template @@ -10,10 +10,10 @@ }, "profiles": [ { - "backend": "vulkan", // should be none + "backend": "vulkan", "platform": "windows", "buildModes": [], - "runConfiguration": "Release", // we also need to run in Debug nad RWDI because foundational example + "runConfiguration": "Release", "gpuArchitectures": [] } ], diff --git a/43_SumAndCDFFilters/main.cpp b/43_SumAndCDFFilters/main.cpp new file mode 100644 index 000000000..aae9eda8e --- /dev/null +++ b/43_SumAndCDFFilters/main.cpp @@ -0,0 +1,369 @@ +// 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 "nbl/asset/filters/CSummedAreaTableImageFilter.h" +#include "nbl/ext/ScreenShot/ScreenShot.h" +#include "../common/CommonAPI.h" + +using namespace nbl; +using namespace core; +using namespace asset; +using namespace video; + +/* + Comment IMAGE_VIEW define to use ordinary cpu image. + You can view the results in Renderdoc. + + When using ordinary IMAGE you can use OVERLAPPING_REGIONS + to choose whether to use extra overlapping region on output image + with a custom in-offset and extent + + You can also specify whether to perform sum in + exclusive mode by EXCLUSIVE_SUM, + otherwise in inclusive mode +*/ + +// #define IMAGE_VIEW +// #define OVERLAPPING_REGIONS +constexpr bool EXCLUSIVE_SUM = true; +constexpr auto MIPMAP_IMAGE_VIEW = 2u; // feel free to change the mipmap +constexpr auto MIPMAP_IMAGE = 0u; // ordinary image used in the example has only 0-th mipmap + +/* + Discrete convolution for getting input image after SAT calculations + + - support [-1.5,1.5] + - (weight = -1) in [-1.5,-0.5] + - (weight = 1) in [-0.5,0.5] + - (weight = 0) in [0.5,1.5] and in range over the support +*/ + +using CDiscreteConvolutionRatioForSupport = std::ratio<3, 2>; //!< 1.5 +class CDiscreteConvolutionFilterKernel : public CFloatingPointSeparableImageFilterKernelBase +{ + using Base = CFloatingPointSeparableImageFilterKernelBase; + + public: + CDiscreteConvolutionFilterKernel() : Base(1.5f,0.5f) {} + + inline float weight(float x, int32_t channel) const + { + if (x >= -1.5f && x <= -0.5f) + return -1.0f; + else if (x >= -0.5f && x <= 0.5f) + return 1.0f; + else + return 0.0f; + } +}; + +class SumAndCDFFilterSampleApp : public NonGraphicalApplicationBase +{ + core::smart_refctd_ptr system; + core::smart_refctd_ptr assetManager; + core::smart_refctd_ptr logger; + +public: + + void setSystem(core::smart_refctd_ptr&& _system) override + { + system = std::move(_system); + } + + NON_GRAPHICAL_APP_CONSTRUCTOR(SumAndCDFFilterSampleApp); + + void onAppInitialized_impl() override + { + CommonAPI::InitParams initParams; + initParams.apiType = video::EAT_VULKAN; + initParams.appName = { "43.SumAndCDFFilter" }; + auto initOutput = CommonAPI::Init(std::move(initParams)); + + system = std::move(initOutput.system); + assetManager = std::move(initOutput.assetManager); + logger = std::move(initOutput.logger); + + auto getSummedImage = [&](const core::smart_refctd_ptr image) -> core::smart_refctd_ptr + { + using SUM_FILTER = CSummedAreaTableImageFilter; + + core::smart_refctd_ptr newSumImage; + { + const auto referenceImageParams = image->getCreationParameters(); + const auto referenceBuffer = image->getBuffer(); + const auto referenceRegions = image->getRegions(); + const auto* referenceRegion = referenceRegions.begin(); + + auto newImageParams = referenceImageParams; + core::smart_refctd_ptr newCpuBuffer; + + #ifdef IMAGE_VIEW + newImageParams.flags = IImage::ECF_CUBE_COMPATIBLE_BIT; + newImageParams.format = EF_R16G16B16A16_UNORM; + #else + newImageParams.format = EF_R32G32B32A32_SFLOAT; + #endif // IMAGE_VIEW + + auto newRegions = core::make_refctd_dynamic_array> + ( + #ifdef IMAGE_VIEW + referenceRegions.size() + #else + + #ifdef OVERLAPPING_REGIONS + 2u + #else + referenceRegions.size() // one region at all + #endif // OVERLAPPING_REGIONS + + #endif // IMAGE_VIEW + ); + + size_t regionOffsets = {}; + + #ifdef IMAGE_VIEW + for (auto newRegion = newRegions->begin(); newRegion != newRegions->end(); ++newRegion) + { + /* + Regions pulled directly from a loader doesn't overlap, so each following is a certain single mipmap + */ + + auto idOffset = newRegion - newRegions->begin(); + *newRegion = *(referenceRegion++); + newRegion->bufferOffset = regionOffsets; + + const auto fullMipMapExtent = image->getMipSize(idOffset); + + regionOffsets += fullMipMapExtent.x * fullMipMapExtent.y * fullMipMapExtent.z * newImageParams.arrayLayers * asset::getTexelOrBlockBytesize(newImageParams.format); + } + newCpuBuffer = core::make_smart_refctd_ptr(regionOffsets); + #else + + /* + 2 overlapping regions if OVERLAPPING_REGIONS is defined + */ + + const auto fullMipMapExtent = image->getMipSize(MIPMAP_IMAGE); + const auto info = image->getTexelBlockInfo(); + const auto fullMipMapExtentInBlocks = info.convertTexelsToBlocks(fullMipMapExtent); + const size_t bufferByteSize = fullMipMapExtentInBlocks.x * fullMipMapExtentInBlocks.y * fullMipMapExtentInBlocks.z * newImageParams.arrayLayers * asset::getTexelOrBlockBytesize(newImageParams.format); + newCpuBuffer = core::make_smart_refctd_ptr(bufferByteSize); + + auto newFirstRegion = newRegions->begin(); + *newFirstRegion = *(referenceRegion++); + newFirstRegion->bufferOffset = regionOffsets; + + #ifdef OVERLAPPING_REGIONS + auto newSecondRegion = newRegions->begin() + 1; + *newSecondRegion = *newFirstRegion; + + newSecondRegion->bufferRowLength = fullMipMapExtent.x; + newSecondRegion->bufferImageHeight = fullMipMapExtent.y; + + auto simdImageOffset = fullMipMapExtent / 4; + newSecondRegion->imageOffset = { simdImageOffset.x, simdImageOffset.y, simdImageOffset.z }; + + auto simdImageExtent = fullMipMapExtent / 2; + newSecondRegion->imageExtent = { simdImageExtent.x, simdImageExtent.y, 1 }; + + newSecondRegion->bufferOffset = newFirstRegion->getByteOffset(simdImageOffset,newFirstRegion->getByteStrides(TexelBlockInfo(newImageParams.format))); + #endif // OVERLAPPING_REGIONS + + #endif // IMAGE_VIEW + + newSumImage = ICPUImage::create(std::move(newImageParams)); + newSumImage->setBufferAndRegions(std::move(newCpuBuffer), newRegions); + + SUM_FILTER sumFilter; + SUM_FILTER::state_type state; + + state.inImage = image.get(); + state.outImage = newSumImage.get(); + state.inOffset = { 0, 0, 0 }; + state.inBaseLayer = 0; + state.outOffset = { 0, 0, 0 }; + state.outBaseLayer = 0; + + #ifdef IMAGE_VIEW + const auto fullMipMapExtent = image->getMipSize(MIPMAP_IMAGE_VIEW); + state.extent = { fullMipMapExtent.x, fullMipMapExtent.y, fullMipMapExtent.z }; + #else + state.extent = { referenceImageParams.extent.width, referenceImageParams.extent.height, referenceImageParams.extent.depth }; + #endif // IMAGE_VIEW + + state.layerCount = newSumImage->getCreationParameters().arrayLayers; + + state.scratchMemoryByteSize = state.getRequiredScratchByteSize(state.inImage, state.extent); + state.scratchMemory = reinterpret_cast(_NBL_ALIGNED_MALLOC(state.scratchMemoryByteSize, 32)); + #ifdef IMAGE_VIEW + state.inMipLevel = MIPMAP_IMAGE_VIEW; + state.outMipLevel = MIPMAP_IMAGE_VIEW; + state.normalizeImageByTotalSATValues = true; // pay attention that we may force normalizing output values (but it will do it anyway if input is normalized) + #else + state.inMipLevel = MIPMAP_IMAGE; + state.outMipLevel = MIPMAP_IMAGE; + #endif // IMAGE_VIEW + state.axesToSum = (1 << 2) | (1 << 1) | (1 << 0); // ZYX + + if (!sumFilter.execute(core::execution::par_unseq,&state)) + logger->log("Something went wrong while performing sum operation!", nbl::system::ILogger::ELL_WARNING); + + _NBL_ALIGNED_FREE(state.scratchMemory); + } + return newSumImage; + }; + + asset::IAssetLoader::SAssetLoadParams loadParams; + loadParams.logger = logger.get(); + + #ifdef IMAGE_VIEW + auto imgPath = sharedInputCWD / "GLI/earth-cubemap3.dds"; + auto bundle = assetManager->getAsset(imgPath.string(), loadParams); + auto cpuImageViewFetched = core::smart_refctd_ptr_static_cast(bundle.getContents()[0]); + auto cpuImage = getSummedImage(cpuImageViewFetched->getCreationParameters().image); + #else + auto imgPath = sharedInputCWD / "colorexr.exr"; + auto bundle = assetManager->getAsset(imgPath.string(), loadParams); + auto cpuImage = getSummedImage(core::smart_refctd_ptr_static_cast(bundle.getContents()[0])); + #endif // IMAGE_VIEW + + ICPUImageView::SCreationParams viewParams; + viewParams.flags = static_cast(0u); + viewParams.image = cpuImage; + viewParams.format = viewParams.image->getCreationParameters().format; + + #ifdef IMAGE_VIEW + viewParams.components = cpuImageViewFetched->getComponents(); + viewParams.viewType = IImageView::ET_2D_ARRAY; + #else + viewParams.viewType = IImageView::ET_2D; + #endif // IMAGE_VIEW + + viewParams.subresourceRange.baseArrayLayer = 0u; + viewParams.subresourceRange.layerCount = cpuImage->getCreationParameters().arrayLayers; + viewParams.subresourceRange.baseMipLevel = 0u; + viewParams.subresourceRange.levelCount = cpuImage->getCreationParameters().mipLevels; + + auto cpuImageView = ICPUImageView::create(std::move(viewParams)); + assert(cpuImageView.get()); // The imageView didn't pass creation validation! + + auto writeSATandGetItsOutputName = [&]() -> std::string //! Writes asset and returns file name without full path + { + std::string outputFileName; + + constexpr std::string_view MODE = [&]() constexpr + { + if constexpr (EXCLUSIVE_SUM) + return "EXCLUSIVE_SAT_"; + else + return "INCLUSIVE_SAT_"; + } + (); + + asset::IAssetWriter::SAssetWriteParams wparams(cpuImageView.get()); + wparams.logger = logger.get(); + + #ifdef IMAGE_VIEW + outputFileName = std::string(MODE.data()) + "IMG_VIEW.dds"; + #else + #ifdef OVERLAPPING_REGIONS + outputFileName = std::string(MODE.data()) + "IMG_OVERLAPPING_REGIONS.exr"; + #else + outputFileName = std::string(MODE.data()) + "IMG.exr"; + #endif // OVERLAPPING_REGIONS + #endif // IMAGE_VIEW + + auto outputFilePath = localOutputCWD / outputFileName; + assetManager->writeAsset(outputFilePath.string(), wparams); + return outputFileName; + }; + + auto getDisConvolutedImage = [&](const core::smart_refctd_ptr inImage) -> core::smart_refctd_ptr + { + auto outImage = core::move_and_static_cast(inImage->clone()); + + using DISCRETE_CONVOLUTION_BLIT_FILTER = asset::CBlitImageFilter; + DISCRETE_CONVOLUTION_BLIT_FILTER blitImageFilter; + DISCRETE_CONVOLUTION_BLIT_FILTER::state_type state; + + core::vectorSIMDu32 extentLayerCount; + #ifdef IMAGE_VIEW + state.inMipLevel = MIPMAP_IMAGE_VIEW; + state.outMipLevel = MIPMAP_IMAGE_VIEW; + extentLayerCount = core::vectorSIMDu32(0, 0, 0, inImage->getCreationParameters().arrayLayers) + inImage->getMipSize(MIPMAP_IMAGE_VIEW); + #else + state.inMipLevel = MIPMAP_IMAGE; + state.outMipLevel = MIPMAP_IMAGE; + extentLayerCount = core::vectorSIMDu32(0, 0, 0, inImage->getCreationParameters().arrayLayers) + inImage->getMipSize(MIPMAP_IMAGE); + #endif // IMAGE_VIEW + + state.inOffsetBaseLayer = core::vectorSIMDu32(); + state.inExtentLayerCount = extentLayerCount; + state.inImage = inImage.get(); + + state.outOffsetBaseLayer = core::vectorSIMDu32(); + state.outExtentLayerCount = extentLayerCount; + state.outImage = outImage.get(); + + state.swizzle = {}; + + state.ditherState = _NBL_NEW(std::remove_pointer::type); + state.scratchMemoryByteSize = blitImageFilter.getRequiredScratchByteSize(&state); + state.scratchMemory = reinterpret_cast(_NBL_ALIGNED_MALLOC(state.scratchMemoryByteSize, 32)); + + const bool succ = state.recomputeScaledKernelPhasedLUT(); + assert(succ); + + if (!blitImageFilter.execute(core::execution::par_unseq,&state)) + logger->log("Something went wrong while performing discrete convolution operation!", nbl::system::ILogger::ELL_WARNING); + + _NBL_DELETE(state.ditherState); + _NBL_ALIGNED_FREE(state.scratchMemory); + + return outImage; + }; + + { + const std::string satFileName = writeSATandGetItsOutputName(); + const auto satFilePath = localOutputCWD / satFileName; + const std::string convolutedSatFileName = "CONVOLUTED_" + satFileName; + const auto convolutedSatFilePath = localOutputCWD / convolutedSatFileName; + + auto bundle = assetManager->getAsset(satFilePath.string(), loadParams); + #ifdef IMAGE_VIEW + auto cpuImageViewFetched = core::smart_refctd_ptr_static_cast(bundle.getContents().begin()[0]); + auto cpuImage = getDisConvolutedImage(cpuImageViewFetched->getCreationParameters().image); + #else + auto cpuImage = getDisConvolutedImage(core::smart_refctd_ptr_static_cast(bundle.getContents().begin()[0])); + #endif // IMAGE_VIEW + + viewParams.image = cpuImage; + viewParams.format = cpuImage->getCreationParameters().format; + viewParams.components = {}; + + auto cpuImageView = ICPUImageView::create(std::move(viewParams)); + assert(cpuImageView.get()); // The imageView didn't pass creation validation! + + asset::IAssetWriter::SAssetWriteParams wparams(cpuImageView.get()); + assetManager->writeAsset(convolutedSatFilePath.string(), wparams); + } + } + + void onAppTerminated_impl() override + { + } + + void workLoopBody() override + { + } + + bool keepRunning() override + { + return false; + } +}; + +NBL_COMMON_API_MAIN(SumAndCDFFilterSampleApp) \ No newline at end of file diff --git a/43_SumAndCDFFilters/pipeline.groovy b/43_SumAndCDFFilters/pipeline.groovy new file mode 100644 index 000000000..db34c48c6 --- /dev/null +++ b/43_SumAndCDFFilters/pipeline.groovy @@ -0,0 +1,50 @@ +import org.DevshGraphicsProgramming.Agent +import org.DevshGraphicsProgramming.BuilderInfo +import org.DevshGraphicsProgramming.IBuilder + +class CSumAndCDFFiltersBuilder extends IBuilder +{ + public CSumAndCDFFiltersBuilder(Agent _agent, _info) + { + super(_agent, _info) + } + + @Override + public boolean prepare(Map axisMapping) + { + return true + } + + @Override + public boolean build(Map axisMapping) + { + IBuilder.CONFIGURATION config = axisMapping.get("CONFIGURATION") + IBuilder.BUILD_TYPE buildType = axisMapping.get("BUILD_TYPE") + + def nameOfBuildDirectory = getNameOfBuildDirectory(buildType) + def nameOfConfig = getNameOfConfig(config) + + agent.execute("cmake --build ${info.rootProjectPath}/${nameOfBuildDirectory}/${info.targetProjectPathRelativeToRoot} --target ${info.targetBaseName} --config ${nameOfConfig} -j12 -v") + + return true + } + + @Override + public boolean test(Map axisMapping) + { + return true + } + + @Override + public boolean install(Map axisMapping) + { + return true + } +} + +def create(Agent _agent, _info) +{ + return new CSumAndCDFFiltersBuilder(_agent, _info) +} + +return this \ No newline at end of file diff --git a/47_DerivMapTest/main.cpp b/47_DerivMapTest/main.cpp index f74d15fb3..f7577d996 100644 --- a/47_DerivMapTest/main.cpp +++ b/47_DerivMapTest/main.cpp @@ -199,7 +199,7 @@ static core::smart_refctd_ptr createDerivMapFromHeightMap(asse auto outParams = inParams; outParams.format = getRGformat(outParams.format); const uint32_t pitch = IImageAssetHandlerBase::calcPitchInBlocks(outParams.extent.width, asset::getTexelOrBlockBytesize(outParams.format)); - auto buffer = asset::ICPUBuffer::create({ asset::getTexelOrBlockBytesize(outParams.format) * pitch * outParams.extent.height }); + auto buffer = core::make_smart_refctd_ptr(asset::getTexelOrBlockBytesize(outParams.format) * pitch * outParams.extent.height); asset::ICPUImage::SBufferCopy region; region.imageOffset = { 0,0,0 }; region.imageExtent = outParams.extent; diff --git a/50.IESViewer/App.hpp b/50.IESViewer/App.hpp deleted file mode 100644 index 4912b4b2c..000000000 --- a/50.IESViewer/App.hpp +++ /dev/null @@ -1,328 +0,0 @@ -#ifndef _THIS_EXAMPLE_APP_HPP_ -#define _THIS_EXAMPLE_APP_HPP_ - -// 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 - -#include "nbl/examples/examples.hpp" -#include "nbl/examples/common/SimpleWindowedApplication.hpp" -#include "nbl/examples/common/CSwapchainFramebuffersAndDepth.hpp" -#include "nbl/examples/common/CEventCallback.hpp" -#include "nbl/examples/common/InputSystem.hpp" -#include "nbl/ui/ICursorControl.h" -#include "nbl/ext/FullScreenTriangle/FullScreenTriangle.h" -#include "nbl/builtin/hlsl/cpp_compat.hlsl" -#include "IES.hpp" -#include "CSimpleIESRenderer.hpp" -#include - - -NBL_EXPOSE_NAMESPACES - -namespace nbl::ext::imgui -{ - class UI; -} - -template -concept AppIESByteCount = std::unsigned_integral; - -template -concept AppIESContainer = std::ranges::sized_range && - (std::same_as, float> || - std::same_as, hlsl::ies::IESTextureInfo>); -static_assert(alignof(hlsl::ies::IESTextureInfo) == 4u, "IESTextureInfo must be 4 byte aligned"); - -template -concept AppIESBufferCreationAllowed = AppIESByteCount || AppIESContainer; - -class IESWindowedApplication : public virtual SimpleWindowedApplication -{ - using base_t = SimpleWindowedApplication; - -public: - constexpr static inline uint8_t MaxFramesInFlight = 3; - - template - IESWindowedApplication(const hlsl::uint16_t2 _initialResolution, const asset::E_FORMAT _depthFormat, Args&&... args) : - base_t(std::forward(args)...), m_initialResolution(_initialResolution), m_depthFormat(_depthFormat) {} - - using surface_list_t = decltype(std::declval().getSurfaces()); - - inline surface_list_t getSurfaces() const override - { - if (!m_surface) - { - auto windowCallback = make_smart_refctd_ptr(smart_refctd_ptr(m_inputSystem), smart_refctd_ptr(m_logger)); - IWindow::SCreationParams params = {}; - params.callback = make_smart_refctd_ptr(); - params.width = m_initialResolution[0]; - params.height = m_initialResolution[1]; - params.x = 32; - params.y = 32; - params.flags = ui::IWindow::ECF_HIDDEN | IWindow::ECF_CAN_MINIMIZE | IWindow::ECF_CAN_MAXIMIZE | IWindow::ECF_CAN_RESIZE; - params.windowCaption = "IESViewer"; - params.callback = windowCallback; - 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) = CSimpleResizeSurface::create(std::move(surface)); - - if (m_surface) - return { {m_surface->getSurface()} }; - - return {}; - } - - inline bool onAppInitialized(core::smart_refctd_ptr&& system) override - { - using namespace nbl::core; - using namespace nbl::video; - if (!MonoSystemMonoLoggerApplication::onAppInitialized(std::move(system))) - return false; - - m_inputSystem = make_smart_refctd_ptr(system::logger_opt_smart_ptr(smart_refctd_ptr(m_logger))); - if (!base_t::onAppInitialized(std::move(system))) - return false; - - ISwapchain::SCreationParams swapchainParams = { .surface = smart_refctd_ptr(m_surface->getSurface()) }; - swapchainParams.sharedParams.imageUsage |= IGPUImage::E_USAGE_FLAGS::EUF_TRANSFER_SRC_BIT; - if (!swapchainParams.deduceFormat(m_physicalDevice)) - return logFail("Could not choose a Surface Format for the Swapchain!"); - - auto scResources = std::make_unique(m_device.get(), m_depthFormat, swapchainParams.surfaceFormat.format, getDefaultSubpassDependencies()); - auto* renderpass = scResources->getRenderpass(); - - if (!renderpass) - return logFail("Failed to create Renderpass!"); - - auto gQueue = getGraphicsQueue(); - if (!m_surface || !m_surface->init(gQueue, std::move(scResources), swapchainParams.sharedParams)) - return logFail("Could not create Window & Surface or initialize the Surface!"); - - return true; - } - - inline void workLoopBody() override final - { - using namespace nbl::core; - using namespace nbl::video; - if (m_window && m_surface && !m_window->isMinimized()) - { - if (auto* scRes = m_surface->getSwapchainResources()) - { - if (auto* sc = scRes->getSwapchain()) - { - const auto& params = sc->getCreationParameters().sharedParams; - if (params.width != m_window->getWidth() || params.height != m_window->getHeight()) - { - m_surface->recreateSwapchain(); - return; - } - } - } - } - - const uint32_t framesInFlightCount = hlsl::min(MaxFramesInFlight, m_surface->getMaxAcquiresInFlight()); - if (m_framesInFlight.size() >= framesInFlightCount) - { - const ISemaphore::SWaitInfo framesDone[] = - { - { - .semaphore = m_framesInFlight.front().semaphore.get(), - .value = m_framesInFlight.front().value - } - }; - if (m_device->blockForSemaphores(framesDone) != ISemaphore::WAIT_RESULT::SUCCESS) - return; - m_framesInFlight.pop_front(); - } - - auto updatePresentationTimestamp = [&]() - { - m_currentImageAcquire = m_surface->acquireNextImage(); - - oracle.reportEndFrameRecord(); - const auto timestamp = oracle.getNextPresentationTimeStamp(); - oracle.reportBeginFrameRecord(); - - return timestamp; - }; - - const auto nextPresentationTimestamp = updatePresentationTimestamp(); - - if (!m_currentImageAcquire) - return; - - const IQueue::SSubmitInfo::SSemaphoreInfo rendered[] = { renderFrame(nextPresentationTimestamp) }; - onPostRenderFrame(rendered[0]); - m_surface->present(m_currentImageAcquire.imageIndex, rendered); - if (rendered->semaphore) - m_framesInFlight.emplace_back(smart_refctd_ptr(rendered->semaphore), rendered->value); - } - - inline bool keepRunning() override final - { - if (m_exitRequested) - return false; - if (m_surface->irrecoverable()) - return false; - - return true; - } - - inline bool onAppTerminated() override - { - m_inputSystem = nullptr; - m_device->waitIdle(); - m_framesInFlight.clear(); - m_surface = nullptr; - m_window = nullptr; - return base_t::onAppTerminated(); - } - -protected: - inline void onAppInitializedFinish() - { - m_winMgr->show(m_window.get()); - oracle.reportBeginFrameRecord(); - } - inline const auto& getCurrentAcquire() const { return m_currentImageAcquire; } - inline void requestExit() { m_exitRequested = true; } - - virtual const video::IGPURenderpass::SCreationParams::SSubpassDependency* getDefaultSubpassDependencies() const = 0; - virtual video::IQueue::SSubmitInfo::SSemaphoreInfo renderFrame(const std::chrono::microseconds nextPresentationTimestamp) = 0; - virtual void onPostRenderFrame(const video::IQueue::SSubmitInfo::SSemaphoreInfo& rendered) {} - - const hlsl::uint16_t2 m_initialResolution; - const asset::E_FORMAT m_depthFormat; - core::smart_refctd_ptr m_inputSystem; - core::smart_refctd_ptr m_window; - core::smart_refctd_ptr> m_surface; - -private: - struct SSubmittedFrame - { - core::smart_refctd_ptr semaphore; - uint64_t value; - }; - core::deque m_framesInFlight; - video::ISimpleManagedSurface::SAcquireResult m_currentImageAcquire = {}; - video::CDumbPresentationOracle oracle; - bool m_exitRequested = false; -}; - -class IESViewer final : public IESWindowedApplication, public BuiltinResourcesApplication -{ - using device_base_t = IESWindowedApplication; - using asset_base_t = BuiltinResourcesApplication; - -public: - static constexpr inline uint32_t AppWindowWidth = 669u * 2u; - static constexpr inline uint32_t AppWindowHeight = AppWindowWidth; - static constexpr inline asset::E_FORMAT AppDepthBufferFormat = asset::EF_UNKNOWN; - static constexpr inline const char* MediaEntry = "../../media"; - static constexpr inline const char* InputJsonFile = "../inputs.json"; - - IESViewer(const path& _localInputCWD, const path& _localOutputCWD, const path& _sharedInputCWD, const path& _sharedOutputCWD); - - bool onAppInitialized(smart_refctd_ptr&& system) override; - IQueue::SSubmitInfo::SSemaphoreInfo renderFrame(const std::chrono::microseconds nextPresentationTimestamp) override; - -protected: - const IGPURenderpass::SCreationParams::SSubpassDependency* getDefaultSubpassDependencies() const override; - void onPostRenderFrame(const video::IQueue::SSubmitInfo::SSemaphoreInfo& rendered) override; - -private: - smart_refctd_ptr m_graphicsPipeline; - smart_refctd_ptr m_computePipeline; - std::array, IGPUPipelineLayout::DESCRIPTOR_SET_COUNT> m_descriptors; - - bool m_running = true; - std::vector m_assets; - - size_t m_realFrameIx = 0; - smart_refctd_ptr m_semaphore; - std::array, device_base_t::MaxFramesInFlight> m_cmdBuffers; - - std::array, device_base_t::MaxFramesInFlight> m_frameBuffers2D, m_frameBuffers3D; - - smart_refctd_ptr m_scene; - smart_refctd_ptr m_renderer; - Camera camera; - uint32_t m_plot3DWidth = 640u; - uint32_t m_plot3DHeight = 640u; - float m_plotRadius = 100.0f; - bool m_ciMode = false; - bool m_ciScreenshotDone = false; - uint32_t m_ciFrameCounter = 0u; - static constexpr uint32_t CiFramesBeforeCapture = 10u; - system::path m_ciScreenshotPath; - std::vector m_assetLabels; - std::vector m_candelaDirty; - - InputSystem::ChannelReader mouse; - InputSystem::ChannelReader keyboard; - - struct { - smart_refctd_ptr it; - smart_refctd_ptr descriptor; - } ui; - - struct UIState - { - size_t activeAssetIx = 0; - float cameraMoveSpeed = 1.0f; - float cameraRotateSpeed = 1.0f; - float cameraFovDeg = 60.0f; - bool cameraControlEnabled = false; - bool cameraControlApplied = false; - bool wireframeEnabled = false; - bool showOctaMapPreview = true; - bool showHints = true; - bool plot2DRectValid = false; - hlsl::float32_t2 plot2DRectMin = hlsl::float32_t2(0.f, 0.f); - hlsl::float32_t2 plot2DRectMax = hlsl::float32_t2(0.f, 0.f); - - struct - { - IES::E_MODE view = IES::EM_CDC; - bitflag sphere = - bitflag(hlsl::this_example::ies::ESM_OCTAHEDRAL_UV_INTERPOLATE) | hlsl::this_example::ies::ESM_FALSE_COLOR; - } mode; - } uiState; - - void processMouse(const IMouseEventChannel::range_t& events); - void processKeyboard(const IKeyboardEventChannel::range_t& events); - - smart_refctd_ptr createImageView(const size_t width, const size_t height, E_FORMAT format, std::string name, - bitflag usage = bitflag(IImage::EUF_SAMPLED_BIT) | IImage::EUF_STORAGE_BIT, - bitflag aspectFlags = bitflag(IImage::EAF_COLOR_BIT)); - bool recreate3DPlotFramebuffers(uint32_t width, uint32_t height); - void applyWindowMode(); - bool parseCommandLine(); - - template - requires AppIESBufferCreationAllowed - smart_refctd_ptr createBuffer(const T& in, std::string name, bool unmap = true) - { - const void* src = nullptr; size_t bytes = {}; - if constexpr (AppIESByteCount) - bytes = static_cast(in); - else if (AppIESContainer) - { - using element_t = std::ranges::range_value_t; - static_assert(alignof(element_t) == 4u, "IESViewer::createBuffer: AppIESContainer's \"T\" must be 4 byte aligned"); - bytes = sizeof(element_t) * in.size(); - src = static_cast(std::data(in)); - } - return implCreateBuffer(src, bytes, name, unmap); - } - smart_refctd_ptr implCreateBuffer(const void* src, size_t bytes, const std::string& name, bool unmap); - - void uiListener(); -}; - -#endif // _THIS_EXAMPLE_APP_HPP_ diff --git a/50.IESViewer/AppEvent.cpp b/50.IESViewer/AppEvent.cpp deleted file mode 100644 index cbd5ba042..000000000 --- a/50.IESViewer/AppEvent.cpp +++ /dev/null @@ -1,62 +0,0 @@ -// 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 - -#include "App.hpp" - -void IESViewer::processMouse(const nbl::ui::IMouseEventChannel::range_t& events) -{ - for (auto it = events.begin(); it != events.end(); it++) - { - auto ev = *it; - - if (ev.type == nbl::ui::SMouseEvent::EET_SCROLL) - { - auto* cursorControl = m_window ? m_window->getCursorControl() : nullptr; - if (!cursorControl || !uiState.plot2DRectValid) - continue; - const auto cursor = cursorControl->getPosition(); - const float cursorX = static_cast(cursor.x); - const float cursorY = static_cast(cursor.y); - if (cursorX < uiState.plot2DRectMin.x || cursorX > uiState.plot2DRectMax.x || - cursorY < uiState.plot2DRectMin.y || cursorY > uiState.plot2DRectMax.y) - continue; - - auto& ies = m_assets[uiState.activeAssetIx]; - const auto& accessor = ies.getProfile()->getAccessor(); - - auto impulse = ev.scrollEvent.verticalScroll * 0.02f; - ies.zDegree = std::clamp(ies.zDegree + impulse, accessor.hAngles.front(), accessor.hAngles.back()); - } - } -} - -void IESViewer::processKeyboard(const nbl::ui::IKeyboardEventChannel::range_t& events) -{ - for (auto it = events.begin(); it != events.end(); it++) - { - const auto ev = *it; - - if (ev.action == nbl::ui::SKeyboardEvent::ECA_RELEASED) - { - if (ev.keyCode == nbl::ui::EKC_UP_ARROW) - uiState.activeAssetIx = std::clamp(uiState.activeAssetIx + 1, 0, m_assets.size() - 1u); - else if (ev.keyCode == nbl::ui::EKC_DOWN_ARROW) - uiState.activeAssetIx = std::clamp(uiState.activeAssetIx - 1, 0, m_assets.size() - 1u); - - auto& ies = m_assets[uiState.activeAssetIx]; - - if (ev.keyCode == nbl::ui::EKC_C) - uiState.mode.view = IES::EM_CDC; - else if (ev.keyCode == nbl::ui::EKC_V) - uiState.mode.view = IES::EM_OCTAHEDRAL_MAP; - else if (ev.keyCode == nbl::ui::EKC_ESCAPE && uiState.cameraControlEnabled) - uiState.cameraControlEnabled = false; - else if (ev.keyCode == nbl::ui::EKC_SPACE) - uiState.cameraControlEnabled = !uiState.cameraControlEnabled; - - if (ev.keyCode == nbl::ui::EKC_Q) - requestExit(); - } - } -} diff --git a/50.IESViewer/AppGPU.cpp b/50.IESViewer/AppGPU.cpp deleted file mode 100644 index c9fa20e76..000000000 --- a/50.IESViewer/AppGPU.cpp +++ /dev/null @@ -1,102 +0,0 @@ -// 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 - -#include "App.hpp" - -core::smart_refctd_ptr IESViewer::createImageView(const size_t width, const size_t height, E_FORMAT format, std::string name, bitflag usage, bitflag aspectFlags) -{ - IGPUImage::SCreationParams imageParams{}; - imageParams.type = IImage::E_TYPE::ET_2D; - imageParams.extent.height = height; - imageParams.extent.width = width; - imageParams.extent.depth = 1u; - imageParams.format = format; - imageParams.mipLevels = 1u; - imageParams.flags = IImage::ECF_NONE; - imageParams.arrayLayers = 1u; - imageParams.samples = IImage::E_SAMPLE_COUNT_FLAGS::ESCF_1_BIT; - imageParams.usage = usage; - - auto image = m_device->createImage(std::move(imageParams)); - image->setObjectDebugName(name.c_str()); - - if (!image) - { - m_logger->log("Failed to create \"%s\" image!", system::ILogger::ELL_ERROR, name.c_str()); - return nullptr; - } - - auto allocation = m_device->allocate(image->getMemoryReqs(), image.get(), nbl::video::IDeviceMemoryAllocation::EMAF_NONE); - if (!allocation.isValid()) - { - m_logger->log("Failed to allocate device memory for \"%s\" image!", system::ILogger::ELL_ERROR, name.c_str()); - return nullptr; - } - - IGPUImageView::SCreationParams viewParams{}; - viewParams.image = std::move(image); - viewParams.format = format; - viewParams.viewType = IGPUImageView::ET_2D; - viewParams.flags = IImageViewBase::ECF_NONE; - viewParams.subresourceRange.baseArrayLayer = 0u; - viewParams.subresourceRange.baseMipLevel = 0u; - viewParams.subresourceRange.layerCount = 1u; - viewParams.subresourceRange.levelCount = 1u; - viewParams.subresourceRange.aspectMask = aspectFlags; - - auto imageView = m_device->createImageView(std::move(viewParams)); - - if (not imageView) - m_logger->log("Failed to create image view for \"%s\" image!", system::ILogger::ELL_ERROR, name.c_str()); - - return imageView; -} - -core::smart_refctd_ptr IESViewer::implCreateBuffer(const void* src, size_t bytes, const std::string& name, bool unmap) -{ - IGPUBuffer::SCreationParams bufferParams = {}; - bufferParams.usage = core::bitflag(asset::IBuffer::EUF_SHADER_DEVICE_ADDRESS_BIT) | IGPUBuffer::EUF_TRANSFER_DST_BIT; - bufferParams.size = bytes; - - auto buffer = m_device->createBuffer(std::move(bufferParams)); - buffer->setObjectDebugName(name.c_str()); - - if (not buffer) - { - m_logger->log("Failed to create \"%s\" buffer!", ILogger::ELL_ERROR, name.c_str()); - return nullptr; - } - - auto memoryReqs = buffer->getMemoryReqs(); - - if (m_utils) - memoryReqs.memoryTypeBits &= m_utils->getLogicalDevice()->getPhysicalDevice()->getUpStreamingMemoryTypeBits(); - - auto allocation = m_device->allocate(memoryReqs, buffer.get(), core::bitflag(video::IDeviceMemoryAllocation::EMAF_DEVICE_ADDRESS_BIT)); - if (not allocation.isValid()) - { - m_logger->log("Failed to allocate \"%s\" buffer!", ILogger::ELL_ERROR, name.c_str()); - return nullptr; - } - - auto* mappedPointer = allocation.memory->map({ 0ull, memoryReqs.size }, IDeviceMemoryAllocation::EMCAF_READ_AND_WRITE); - - if (not mappedPointer) - { - m_logger->log("Failed to map device memory for \"%s\" buffer!", ILogger::ELL_ERROR, name.c_str()); - return nullptr; - } - - if(src) - memcpy(mappedPointer, src, buffer->getSize()); - - if(unmap) - if (not allocation.memory->unmap()) - { - m_logger->log("Failed to unmap device memory for \"%s\" buffer!", ILogger::ELL_ERROR, name.c_str()); - return nullptr; - } - - return buffer; -} \ No newline at end of file diff --git a/50.IESViewer/AppInit.cpp b/50.IESViewer/AppInit.cpp deleted file mode 100644 index 79338e8ec..000000000 --- a/50.IESViewer/AppInit.cpp +++ /dev/null @@ -1,654 +0,0 @@ -// 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 - -#include "argparse/argparse.hpp" -#include "App.hpp" -#include -#include -#include -#include -#include "AppInputParser.hpp" -#include "app_resources/common.hlsl" -#include "app_resources/imgui.opts.hlsl" -#include "nbl/ext/ImGui/ImGui.h" -#include "nbl/builtin/hlsl/math/thin_lens_projection.hlsl" -#include "nbl/this_example/builtin/build/spirv/keys.hpp" - -bool IESViewer::parseCommandLine() -{ - argparse::ArgumentParser parser("IESViewer"); - parser.add_argument("--ci") - .help("Run in CI mode: capture a screenshot after a few frames and exit.") - .default_value(false) - .implicit_value(true); - - try - { - parser.parse_args({ argv.data(), argv.data() + argv.size() }); - } - catch (const std::exception& e) - { - if (m_logger) - m_logger->log("Failed to parse arguments: %s", system::ILogger::ELL_ERROR, e.what()); - return false; - } - - m_ciMode = parser.get("--ci"); - if (m_ciMode) - m_ciScreenshotPath = localOutputCWD / "iesviewer_ci.png"; - return true; -} - -bool IESViewer::onAppInitialized(smart_refctd_ptr&& system) -{ - if (!parseCommandLine()) - return false; - if (!asset_base_t::onAppInitialized(smart_refctd_ptr(system))) - return false; - if (!device_base_t::onAppInitialized(smart_refctd_ptr(system))) - return false; - - const auto media = absolute(path(MediaEntry)); - - AppInputParser::Output out; - AppInputParser parser(system::logger_opt_ptr(m_logger.get())); - if (!parser.parse(out, InputJsonFile, media.string())) - return false; - - m_logger->log("Loading IES m_assets..", system::ILogger::ELL_INFO); - { - auto start = std::chrono::high_resolution_clock::now(); - size_t loaded = {}, total = out.inputList.size(); - IAssetLoader::SAssetLoadParams lp = {}; - lp.loaderFlags = IAssetLoader::E_LOADER_PARAMETER_FLAGS::ELPF_LOAD_METADATA_ONLY; - lp.logger = system::logger_opt_ptr(m_logger.get()); - - for (const auto& in : out.inputList) - { - auto asset = m_assetMgr->getAsset(in.c_str(), lp); - - if (asset.getMetadata()) - { - auto& ies = m_assets.emplace_back(); - ies.bundle = std::move(asset); - ies.key = path(in).lexically_relative(media).string(); - ++loaded; - - m_logger->log("Loaded \"%s\".", system::ILogger::ELL_INFO, in.c_str()); - } - else - m_logger->log("Failed to load metadata for \"%s\"! Skipping..", system::ILogger::ELL_WARNING, in.c_str()); - } - const auto sl = std::to_string(loaded), st = std::to_string(total); - const bool passed = loaded == total; - - if (not passed) - { - auto diff = std::to_string(total - loaded); - m_logger->log("Failed to load [%s/%s] IES m_assets!", system::ILogger::ELL_ERROR, diff.c_str(), st.c_str()); - } - auto elapsed = std::chrono::duration(std::chrono::high_resolution_clock::now() - start); - auto took = std::to_string(elapsed.count()); - m_logger->log("Finished loading IES m_assets, took %s seconds.", system::ILogger::ELL_PERFORMANCE, took.c_str()); - } - { - m_assetLabels.clear(); - m_assetLabels.reserve(m_assets.size()); - for (const auto& ies : m_assets) - m_assetLabels.emplace_back(path(ies.key).filename().string()); - } - m_candelaDirty.assign(m_assets.size(), true); - - m_logger->log("Creating GPU IES resources..", system::ILogger::ELL_INFO); - { - auto start = std::chrono::high_resolution_clock::now(); - - auto textureInfos = createBuffer(m_assets.size() * sizeof(hlsl::ies::IESTextureInfo), "IES Textures Info", false); - if(!textureInfos) return false; - auto* textureInfosMapped = static_cast(textureInfos->getBoundMemory().memory->getMappedPointer()); - - for (size_t i = 0u; i < m_assets.size(); ++i) - { - auto& ies = m_assets[i]; - const auto* profile = ies.getProfile(); - const auto& accessor = profile->getAccessor(); - const auto& resolution = accessor.properties.optimalIESResolution; - textureInfosMapped[i] = CIESProfile::texture_t::create(accessor.properties.maxCandelaValue, resolution).info; - ies.buffers.textureInfo.buffer = textureInfos; - ies.buffers.textureInfo.offset = i * sizeof(hlsl::ies::IESTextureInfo); - - #define CREATE_VIEW(VIEW, FORMAT, NAME) \ - if (!(VIEW = createImageView(resolution.x, resolution.y, FORMAT, NAME + ies.key) )) return false; - - // Filled later by the compute pass (CdcCS) when candela data is marked dirty. - CREATE_VIEW(ies.views.candelaOctahedralMap, asset::EF_R16_UNORM, "IES Candela Octahedral Map Image: ") - - #define CREATE_BUFFER(BUFFER, DATA, NAME) \ - if (!(BUFFER = createBuffer(DATA, NAME + ies.key) )) return false; - - CREATE_BUFFER(ies.buffers.vAngles, accessor.vAngles, "IES Vertical Angles Buffer: ") - CREATE_BUFFER(ies.buffers.hAngles, accessor.hAngles, "IES Horizontal Angles Buffer: ") - CREATE_BUFFER(ies.buffers.data, accessor.data, "IES Data Buffer: ") - } - auto elapsed = std::chrono::duration(std::chrono::high_resolution_clock::now() - start); - auto took = std::to_string(elapsed.count()); - m_logger->log("Finished creating GPU IES resources, took %s seconds.", system::ILogger::ELL_PERFORMANCE, took.c_str()); - } - - auto createShader = [&]() -> smart_refctd_ptr - { - IAssetLoader::SAssetLoadParams lp = {}; - lp.logger = system::logger_opt_ptr(m_logger.get()); - lp.workingDirectory = "app_resources"; - - auto key = nbl::this_example::builtin::build::get_spirv_key(m_device.get()); - auto assetBundle = m_assetMgr->getAsset(key, lp); - const auto m_assets = assetBundle.getContents(); - - if (m_assets.empty()) - { - m_logger->log("Failed to load \"%s\" shader!", system::ILogger::ELL_ERROR, key.data()); - return nullptr; - } - - auto spirvShader = IAsset::castDown(m_assets[0]); - - if (spirvShader) - m_logger->log("Loaded \"%s\".", system::ILogger::ELL_INFO, key.data()); - else - m_logger->log("Failed to cast \"%s\" asset to IShader!", system::ILogger::ELL_ERROR, key.data()); - - return spirvShader; - }; - - #define CREATE_SHADER(SHADER, PATH) \ - if (!(SHADER = createShader.template operator()() )) return false; - - m_logger->log("Loading GPU shaders..", system::ILogger::ELL_INFO); - - struct - { - smart_refctd_ptr ies, imgui, fullScreenTriangleVS; - } shaders; - { - auto start = std::chrono::high_resolution_clock::now(); - CREATE_SHADER(shaders.ies, "ies.unified") - CREATE_SHADER(shaders.imgui, "imgui.unified") - shaders.fullScreenTriangleVS = ext::FullScreenTriangle::ProtoPipeline::createDefaultVertexShader(m_assetMgr.get(), m_device.get(), m_logger.get()); - if (!shaders.fullScreenTriangleVS) - return logFail("Failed to create FullScreenTriangle vertex shader!"); - auto elapsed = std::chrono::duration(std::chrono::high_resolution_clock::now() - start); - auto took = std::to_string(elapsed.count()); - m_logger->log("Finished loading GPU shaders, took %s seconds!", system::ILogger::ELL_PERFORMANCE, took.c_str()); - } - - // Pipelines & Descriptor Sets - { - using binding_flags_t = video::IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS; - using stage_flags_t = asset::IShader::E_SHADER_STAGE; - static constexpr auto TexturesCreateFlags = core::bitflag(binding_flags_t::ECF_UPDATE_AFTER_BIND_BIT) | binding_flags_t::ECF_PARTIALLY_BOUND_BIT | binding_flags_t::ECF_UPDATE_UNUSED_WHILE_PENDING_BIT; - static constexpr auto SamplersCreateFlags = core::bitflag(binding_flags_t::ECF_UPDATE_AFTER_BIND_BIT); - static constexpr auto StageFlags = core::bitflag(stage_flags_t::ESS_FRAGMENT) | stage_flags_t::ESS_VERTEX | stage_flags_t::ESS_COMPUTE; - - //! single descriptor for both compute & graphics, we will only need to trasition images' layout with a barrier - #define BINDING_TEXTURE(IX, TYPE) { .binding = IX, .type = TYPE, .createFlags = TexturesCreateFlags, .stageFlags = StageFlags, .count = hlsl::this_example::MaxIesImages, .immutableSamplers = nullptr } - #define BINDING_SAMPLER(IX) { .binding = IX, .type = IDescriptor::E_TYPE::ET_SAMPLER, .createFlags = SamplersCreateFlags, .stageFlags = StageFlags, .count = 1u, .immutableSamplers = nullptr } - static constexpr auto bindings = std::to_array - ({ - BINDING_TEXTURE(0u, IDescriptor::E_TYPE::ET_SAMPLED_IMAGE), BINDING_TEXTURE(0u + 10u, IDescriptor::E_TYPE::ET_STORAGE_IMAGE), // candela octahedral map - BINDING_SAMPLER(0u + 100u) - }); - - const uint32_t texturesCount = m_assets.size(); - smart_refctd_ptr generalSampler; - { - IGPUSampler::SParams params; - params.AnisotropicFilter = 1u; - params.TextureWrapU = ISampler::E_TEXTURE_CLAMP::ETC_CLAMP_TO_EDGE; - params.TextureWrapV = ISampler::E_TEXTURE_CLAMP::ETC_CLAMP_TO_EDGE; - params.TextureWrapW = ISampler::E_TEXTURE_CLAMP::ETC_CLAMP_TO_EDGE; - params.BorderColor = ISampler::ETBC_FLOAT_OPAQUE_BLACK; - params.MinFilter = ISampler::ETF_LINEAR; - params.MaxFilter = ISampler::ETF_LINEAR; - params.MipmapMode = ISampler::ESMM_LINEAR; - params.AnisotropicFilter = 0u; - params.CompareEnable = false; - params.CompareFunc = ISampler::ECO_ALWAYS; - - generalSampler = m_device->createSampler(params); - - if (not generalSampler) - { - m_logger->log("Failed to create sampler!", system::ILogger::ELL_ERROR); - return false; - } - - generalSampler->setObjectDebugName("General IES sampler"); - } - - auto scRes = static_cast(m_surface->getSwapchainResources()); - scRes->getRenderpass(); // note it also creates rp if nulled - { - auto descriptorSetLayout = m_device->createDescriptorSetLayout(bindings); - - if (not descriptorSetLayout) - return logFail("Failed to create descriptor set layout!"); - - auto range = std::to_array({ {StageFlags.value, offsetof(hlsl::this_example::ies::PushConstants, cdc), sizeof(hlsl::this_example::ies::CdcPC)} }); - auto pipelineLayout = m_device->createPipelineLayout(range, core::smart_refctd_ptr(descriptorSetLayout), nullptr, nullptr, nullptr); - - if (not pipelineLayout) - return logFail("Failed to create pipeline layout!"); - - // Compute Pipeline - { - auto params = std::to_array({ {} });; - params[0].layout = pipelineLayout.get(); - params[0].shader.shader = shaders.ies.get(); - params[0].shader.entryPoint = "CdcCS"; - - if (!m_device->createComputePipelines(nullptr, params, &m_computePipeline)) - return logFail("Failed to create compute pipeline!"); - } - - // Graphics Pipeline - { - IGPUPipelineBase::SShaderEntryMap specConstants; - const auto orientationAsUint32 = static_cast(SurfaceTransform::FLAG_BITS::IDENTITY_BIT); - specConstants[0] = std::span{ reinterpret_cast(&orientationAsUint32), sizeof(orientationAsUint32) }; - - video::IGPUPipelineBase::SShaderSpecInfo specInfo[] = - { - {.shader = shaders.fullScreenTriangleVS.get(), .entryPoint = "__nbl__hlsl__ext__FullScreenTriangle__vertex_main", .entries = &specConstants }, - {.shader = shaders.ies.get(), .entryPoint = "CdcPS" } - }; - - auto params = std::to_array({ {} }); - params[0].renderpass = scRes->getRenderpass(); - params[0].vertexShader = specInfo[0]; - params[0].fragmentShader = specInfo[1]; - params[0].layout = pipelineLayout.get(); - params[0].cached = - { - .vertexInput = {}, // full screen tri ext, no inputs - .primitiveAssembly = {}, - .rasterization = { - .polygonMode = EPM_FILL, - .faceCullingMode = EFCM_NONE, - .depthWriteEnable = false, - }, - .blend = {}, - .subpassIx = 0u - }; - - if (!m_device->createGraphicsPipelines(nullptr, params, &m_graphicsPipeline)) - return logFail("Failed to create graphics pipeline!"); - } - - const auto dscLayoutPtrs = m_graphicsPipeline->getLayout()->getDescriptorSetLayouts(); - auto pool = m_device->createDescriptorPoolForDSLayouts(IDescriptorPool::ECF_UPDATE_AFTER_BIND_BIT, dscLayoutPtrs); - pool->createDescriptorSets(dscLayoutPtrs.size(), dscLayoutPtrs.data(), m_descriptors.data()); - { - constexpr auto ViewsCount = 1u; // used to be 4u with debug maps (counted x2 for RO & RW binding but one descriptor) - std::array, ViewsCount * 2u + 1u> infos; - auto addInfo = [](auto& list, auto desc, IImage::LAYOUT layout) - { - auto& info = list.emplace_back(); - info.desc = desc; - info.info.image.imageLayout = layout; - }; - - for (uint32_t i = 0; i < m_assets.size(); ++i) - { - auto& ies = m_assets[i]; - addInfo(infos[0u], ies.views.candelaOctahedralMap, IImage::LAYOUT::READ_ONLY_OPTIMAL); - addInfo(infos[1u], ies.views.candelaOctahedralMap, IImage::LAYOUT::GENERAL); - } - addInfo(infos.back(), generalSampler, IImage::LAYOUT::READ_ONLY_OPTIMAL); - auto* samplerInfo = infos.back().data(); - - std::array writes = {}; - auto& sampledWrite = writes[0u]; - sampledWrite.count = m_assets.size(); - sampledWrite.info = infos[0u].data(); - sampledWrite.dstSet = m_descriptors[0u].get(); - sampledWrite.arrayElement = 0u; - sampledWrite.binding = 0u; - - auto& storageWrite = writes[1u]; - storageWrite.count = m_assets.size(); - storageWrite.info = infos[1u].data(); - storageWrite.dstSet = m_descriptors[0u].get(); - storageWrite.arrayElement = 0u; - storageWrite.binding = 10u; - - auto& write = writes.back(); - write.count = 1u; - write.info = samplerInfo; - write.dstSet = m_descriptors[0u].get(); - write.arrayElement = 0u; - write.binding = 0u + 100u; - - if (!m_device->updateDescriptorSets(writes, {})) - return logFail("Failed to write descriptor sets"); - } - } - } - - // frame buffers - { - // TODO: I will create my own - auto renderpass = smart_refctd_ptr(static_cast(m_surface->getSwapchainResources())->getRenderpass()); - - for (uint32_t i = 0u; i < m_frameBuffers2D.size(); ++i) - { - auto& fb2D = m_frameBuffers2D[i]; - auto& fb3D = m_frameBuffers3D[i]; - auto ixs = std::to_string(i); - - // TODO: may actually change it, temporary hardcoding - constexpr auto WIDTH = 640; - constexpr auto HEIGHT_2D = WIDTH * 2; - constexpr auto HEIGHT_3D = WIDTH; - - { - auto color = createImageView(WIDTH, HEIGHT_2D, EF_R8G8B8A8_SRGB, "[2D Plot]: framebuffer[" + ixs + "].color attachement", IGPUImage::EUF_RENDER_ATTACHMENT_BIT | IGPUImage::EUF_SAMPLED_BIT, IImage::EAF_COLOR_BIT); - fb2D = m_device->createFramebuffer - ( - { { - .renderpass = renderpass, - .depthStencilAttachments = nullptr, - .colorAttachments = &color.get(), - .width = WIDTH, - .height = HEIGHT_2D - } } - ); - } - - { - auto color = createImageView(WIDTH, HEIGHT_3D, EF_R8G8B8A8_SRGB, "[3D Plot]: framebuffer[" + ixs + "].color attachement", IGPUImage::EUF_RENDER_ATTACHMENT_BIT | IGPUImage::EUF_SAMPLED_BIT, IImage::EAF_COLOR_BIT); - auto depth = createImageView(WIDTH, HEIGHT_3D, EF_D16_UNORM, "[3D Plot]: framebuffer[" + ixs + "].depth attachement", IGPUImage::EUF_RENDER_ATTACHMENT_BIT | IGPUImage::EUF_SAMPLED_BIT, IGPUImage::EAF_DEPTH_BIT); - - fb3D = m_device->createFramebuffer - ( - { { - .renderpass = renderpass, - .depthStencilAttachments = &depth.get(), - .colorAttachments = &color.get(), - .width = WIDTH, - .height = HEIGHT_3D - } } - ); - } - } - } - auto scRes = static_cast(m_surface->getSwapchainResources()); - - // geometries for 3D scene - { - struct IESGeometryScene final : public CGeometryCreatorScene - { - explicit IESGeometryScene(const std::vector& assets) : m_assets(&assets) {} - - protected: - core::vector addGeometries(asset::CGeometryCreator* creator) const override - { - core::vector entries; - if (!m_assets) - return entries; - - std::set> seen; - for (const auto& ies : *m_assets) - { - const auto& resolution = ies.getProfile()->getAccessor().properties.optimalIESResolution; - std::pair key{ resolution.x, resolution.y }; - if (!seen.insert(key).second) - continue; - - std::string name = "Grid (" + std::to_string(resolution.x) + " x " + std::to_string(resolution.y) + ")"; // (**) used to assign polygons! - entries.push_back({ std::move(name), creator->createGrid({ resolution.x, resolution.y }) }); - } - - return entries; - } - - private: - const std::vector* m_assets = nullptr; - }; - - const uint32_t addtionalBufferOwnershipFamilies[] = { getGraphicsQueue()->getFamilyIndex() }; - m_scene = CGeometryCreatorScene::create( - { - .transferQueue = getTransferUpQueue(), - .utilities = m_utils.get(), - .logger = m_logger.get(), - .addtionalBufferOwnershipFamilies = addtionalBufferOwnershipFamilies - }, - CSimpleIESRenderer::DefaultPolygonGeometryPatch, - m_assets - ); - - const auto& geoParams = m_scene->getInitParams(); - std::vector> polygons(m_assets.size()); - for (uint32_t i = 0u; i < m_assets.size(); ++i) - { - const auto& resolution = m_assets[i].getProfile()->getAccessor().properties.optimalIESResolution; - - for (uint32_t g = 0u; g < geoParams.geometryNames.size(); ++g) - { - uint32_t w = 0u, h = 0u; - std::sscanf(geoParams.geometryNames[g].c_str(), "Grid (%u x %u)", &w, &h); // (**) - - if (w == resolution.x && h == resolution.y) - { - polygons[i] = geoParams.geometries[g]; - break; - } - } - assert(polygons[i]); - } - - m_renderer = CSimpleIESRenderer::create(shaders.ies, core::smart_refctd_ptr(m_descriptors[0u]->getLayout()), scRes->getRenderpass(), 0, { &polygons.front().get(),polygons.size() }); - if (!m_renderer || m_renderer->getGeometries().size() != polygons.size()) - return logFail("Could not create 3D Plot Renderer!"); - - m_renderer->m_instances.resize(1); - m_renderer->m_instances[0].world = float32_t3x4( - float32_t4(1, 0, 0, 0), - float32_t4(0, 1, 0, 0), - 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; - cameraPosition = cameraTarget + cameraOffset * 1.5f; - - 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(); - uiState.cameraControlApplied = !uiState.cameraControlEnabled; - } - - // imGUI - { - ext::imgui::UI::SCreationParameters params = {}; - params.resources.texturesInfo = { .setIx = NBL_TEXTURES_SET_IX, .bindingIx = NBL_TEXTURES_BINDING_IX }; - params.resources.samplersInfo = { .setIx = NBL_SAMPLER_STATES_SET_IX, .bindingIx = NBL_SAMPLER_STATES_BINDING_IX }; - params.utilities = m_utils; - params.transfer = getTransferUpQueue(); - params.pipelineLayout = ext::imgui::UI::createDefaultPipelineLayout(m_utils->getLogicalDevice(), params.resources.texturesInfo, params.resources.samplersInfo, NBL_TEXTURES_COUNT); - params.assetManager = make_smart_refctd_ptr(smart_refctd_ptr(m_system)); - params.renderpass = smart_refctd_ptr(scRes->getRenderpass()); - params.subpassIx = 0u; - params.pipelineCache = nullptr; - - using imgui_precompiled_spirv_t = ext::imgui::UI::SCreationParameters::PrecompiledShaders; - params.spirv = std::make_optional(imgui_precompiled_spirv_t{ .vertex = shaders.imgui, .fragment = shaders.imgui }); - - auto imguiPtr = ext::imgui::UI::create(std::move(params)); - auto* imgui = imguiPtr.get(); - ui.it = smart_refctd_ptr_static_cast(imguiPtr); - if (not imgui) - return logFail("Failed to create `nbl::ext::imgui::UI` class"); - - { - 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)); - ui.descriptor = make_smart_refctd_ptr(std::move(ds)); - if (!ui.descriptor) - return logFail("Failed to create the descriptor set"); - - { - std::array addresses; - addresses.fill(SubAllocatedDescriptorSet::invalid_value); - ui.descriptor->multi_allocate(0, addresses.size(), addresses.data()); - - bool ok = true; - ok &= addresses.front() == ext::imgui::UI::FontAtlasTexId; - for (auto i = ext::imgui::UI::FontAtlasTexId; i < addresses.size(); ++i) - ok &= addresses[i] == i; - - assert(ok); - - std::array infos; - for (auto& it : infos) it.info.image.imageLayout = IImage::LAYOUT::READ_ONLY_OPTIMAL; - - auto* ix = addresses.data(); - infos[*ix].desc = smart_refctd_ptr(imgui->getFontAtlasView()); ++ix; - - for (uint8_t i = 0u; i < MaxFramesInFlight; ++i, ++ix) infos[*ix].desc = m_frameBuffers2D[i]->getCreationParameters().colorAttachments[0u]; - for (uint8_t i = 0u; i < MaxFramesInFlight; ++i, ++ix) infos[*ix].desc = m_frameBuffers3D[i]->getCreationParameters().colorAttachments[0u]; - - auto writes = std::to_array({ IGPUDescriptorSet::SWriteDescriptorSet{ - .dstSet = ui.descriptor->getDescriptorSet(), - .binding = NBL_TEXTURES_BINDING_IX, - .arrayElement = 0u, - .count = infos.size(), - .info = infos.data() - }}); - - if (!m_device->updateDescriptorSets(writes, {})) - return logFail("Failed to write the descriptor set"); - } - } - - imgui->registerListener([this]() - { - uiListener(); - }); - } - - m_semaphore = m_device->createSemaphore(m_realFrameIx); - if (!m_semaphore) - return logFail("Failed to Create a Semaphore!"); - - using pool_flags_t = IGPUCommandPool::CREATE_FLAGS; - - auto createCommandBuffers = [&](auto* queue, const std::span> out, pool_flags_t flags) -> bool - { - auto pool = m_device->createCommandPool(queue->getFamilyIndex(), flags); - if (!pool) - return logFail("Couldn't create command pool!"); - if (!pool->createCommandBuffers(IGPUCommandPool::BUFFER_LEVEL::PRIMARY, out)) - return logFail("Couldn't create command buffer!"); - return true; - }; - - // render loop command buffers - if (not createCommandBuffers(getGraphicsQueue(), m_cmdBuffers, pool_flags_t::RESET_COMMAND_BUFFER_BIT)) - return false; - - // transient command buffer - { - auto* queue = getGraphicsQueue(); - auto cbs = std::to_array({ smart_refctd_ptr() }); - if (not createCommandBuffers(queue, cbs, pool_flags_t::RESET_COMMAND_BUFFER_BIT | pool_flags_t::TRANSIENT_BIT)) - return false; - - std::vector images; - for (uint32_t i = 0; i < m_assets.size(); ++i) - { - auto& ies = m_assets[i]; - - images.emplace_back() = ies.views.candelaOctahedralMap->getCreationParameters().image.get(); - } - - auto* cb = cbs.front().get(); - cb->setObjectDebugName("Transient Command Buffer"); - - if (not cb->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT)) - return logFail("Couldn't begin command buffer!"); - - if (not IES::barrier(cb, images)) - return logFail("Failed to record pipeline barriers!"); - - if (not cb->end()) - return logFail("Couldn't end command buffer!"); - - core::smart_refctd_ptr semaphore = m_device->createSemaphore(0); - semaphore->setObjectDebugName("Scratch Semaphore"); - { - IQueue::SSubmitInfo::SSemaphoreInfo signal = - { - .semaphore = semaphore.get(), - .value = 1u, - .stageMask = PIPELINE_STAGE_FLAGS::ALL_TRANSFER_BITS - }; - - const IQueue::SSubmitInfo::SCommandBufferInfo cmds[] = { {.cmdbuf = cb } }; - - const IQueue::SSubmitInfo infos[] = - { { - .waitSemaphores = {}, - .commandBuffers = cmds, - .signalSemaphores = {&signal,1} - } }; - - if (queue->submit(infos) != IQueue::RESULT::SUCCESS) - return logFail("Failed to submit queue!"); - } - - { - const ISemaphore::SWaitInfo infos[] = - { { - .semaphore = semaphore.get(), - .value = 1u - } }; - - if (m_device->blockForSemaphores(infos) != ISemaphore::WAIT_RESULT::SUCCESS) - return logFail("Couldn't block for scratch semaphore!"); - } - } - - onAppInitializedFinish(); - if (m_window && m_winMgr) - applyWindowMode(); - - return true; -} - -void IESViewer::applyWindowMode() -{ - if (!m_window || !m_winMgr) - return; - - m_winMgr->maximize(m_window.get()); - - if (m_surface) - { - m_surface->recreateSwapchain(); - } -} - diff --git a/50.IESViewer/AppInputParser.cpp b/50.IESViewer/AppInputParser.cpp deleted file mode 100644 index 42e6e5cd8..000000000 --- a/50.IESViewer/AppInputParser.cpp +++ /dev/null @@ -1,108 +0,0 @@ -// 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 - -#include "AppInputParser.hpp" -#include "nlohmann/json.hpp" - -NBL_EXPOSE_NAMESPACES -using json = ::nlohmann::json; - -bool AppInputParser::parse(Output& out, const std::string input, const std::string cwd) -{ - const auto jInputFile = std::filesystem::absolute(input); - const auto sjInputFile = jInputFile.string(); - - std::ifstream file(sjInputFile.c_str()); - if (!file.is_open()) { - - logger.log("Could not open \"%s\" file.", system::ILogger::ELL_ERROR, sjInputFile.c_str()); - return false; - } - - std::stringstream buffer; - buffer << file.rdbuf(); - const auto jsonBuffer = buffer.str(); - - if (jsonBuffer.empty()) - { - logger.log("\"%s\" file is empty!", system::ILogger::ELL_ERROR, sjInputFile.c_str()); - return false; - } - - const auto jsonMap = ::json::parse(jsonBuffer.c_str()); - - if (!jsonMap["directories"].is_array()) - { - logger.log("\"%s\" file is empty!", system::ILogger::ELL_ERROR, sjInputFile.c_str()); - return false; - } - - if (!jsonMap["files"].is_array()) - { - logger.log("\"%s\" file's field \"files\" is not an array!", system::ILogger::ELL_ERROR, sjInputFile.c_str()); - return false; - } - - if (!jsonMap["writeAssets"].is_boolean()) - { - logger.log("\"%s\" file's field \"writeAssets\" is not a boolean!", system::ILogger::ELL_ERROR, sjInputFile.c_str()); - return false; - } - - auto addFile = [&](const std::string_view in) -> bool - { - auto path = std::filesystem::absolute(cwd / std::filesystem::path(in)); - - if (std::filesystem::exists(path) && std::filesystem::is_regular_file(path) && path.extension() == ".ies") - out.inputList.push_back(path.string()); - else - { - logger.log("Invalid \"%s\" input!", system::ILogger::ELL_ERROR, path.string().c_str()); - return false; - } - - return true; - }; - - auto addFiles = [&](const std::string_view directoryPath) -> bool - { - auto directory(std::filesystem::absolute(cwd / std::filesystem::path(directoryPath))); - if (!std::filesystem::exists(directory) || !std::filesystem::is_directory(directory)) - { - logger.log("Invalid \"%s\" directory!", system::ILogger::ELL_ERROR, directory.string().c_str()); - return false; - } - - for (const auto& entry : std::filesystem::directory_iterator(directory)) - if (!addFile(entry.path().string().c_str())) - return false; - - return true; - }; - - // parse json - { - std::vector jDirectories; - jsonMap["directories"].get_to(jDirectories); - - for (const auto& it : jDirectories) - if (!addFiles(it)) - return false; - - std::vector jFiles; - jsonMap["files"].get_to(jFiles); - - for (const auto& it : jFiles) - if (!addFile(it)) - return false; - } - - out.withGUI = false; - jsonMap["gui"].get_to(out.withGUI); - - out.writeAssets = false; - jsonMap["writeAssets"].get_to(out.writeAssets); - - return true; -} \ No newline at end of file diff --git a/50.IESViewer/AppInputParser.hpp b/50.IESViewer/AppInputParser.hpp deleted file mode 100644 index 18b5e4fe3..000000000 --- a/50.IESViewer/AppInputParser.hpp +++ /dev/null @@ -1,27 +0,0 @@ -#ifndef _THIS_EXAMPLE_APP_INPUT_PARSER_HPP_ -#define _THIS_EXAMPLE_APP_INPUT_PARSER_HPP_ - -// 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 - -#include "nbl/examples/examples.hpp" - -struct AppInputParser -{ -public: - struct Output - { - std::vector inputList; - bool withGUI; - bool writeAssets; - }; - - AppInputParser(nbl::system::logger_opt_ptr _logger = nullptr) : logger(_logger) {} - bool parse(Output& out, const std::string jFilePath, const std::string cwd = "."); - -private: - nbl::system::logger_opt_ptr logger; -}; - -#endif // _THIS_EXAMPLE_APP_INPUT_PARSER_HPP_ \ No newline at end of file diff --git a/50.IESViewer/AppRender.cpp b/50.IESViewer/AppRender.cpp deleted file mode 100644 index a06b2702a..000000000 --- a/50.IESViewer/AppRender.cpp +++ /dev/null @@ -1,557 +0,0 @@ -// 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 - -#include "App.hpp" -#include -#include -#include "nbl/ext/ImGui/ImGui.h" -#include "nbl/ext/ScreenShot/ScreenShot.h" -#include "app_resources/common.hlsl" -#include "nbl/builtin/hlsl/math/thin_lens_projection.hlsl" - -bool IESViewer::recreate3DPlotFramebuffers(uint32_t width, uint32_t height) -{ - if (width == 0u || height == 0u) - return false; - - if (width == m_plot3DWidth && height == m_plot3DHeight) - return true; - - m_device->waitIdle(); - m_plot3DWidth = width; - m_plot3DHeight = height; - - auto* scRes = static_cast(m_surface->getSwapchainResources()); - auto renderpass = smart_refctd_ptr(scRes->getRenderpass()); - - for (uint32_t i = 0u; i < m_frameBuffers3D.size(); ++i) - { - auto& fb3D = m_frameBuffers3D[i]; - auto ixs = std::to_string(i); - - auto color = createImageView(width, height, EF_R8G8B8A8_SRGB, "[3D Plot]: framebuffer[" + ixs + "].color attachement", IGPUImage::EUF_RENDER_ATTACHMENT_BIT | IGPUImage::EUF_SAMPLED_BIT, IImage::EAF_COLOR_BIT); - if (!color) - return false; - - auto depth = createImageView(width, height, EF_D16_UNORM, "[3D Plot]: framebuffer[" + ixs + "].depth attachement", IGPUImage::EUF_RENDER_ATTACHMENT_BIT | IGPUImage::EUF_SAMPLED_BIT, IGPUImage::EAF_DEPTH_BIT); - if (!depth) - return false; - - fb3D = m_device->createFramebuffer - ( - { { - .renderpass = renderpass, - .depthStencilAttachments = &depth.get(), - .colorAttachments = &color.get(), - .width = width, - .height = height - } } - ); - if (!fb3D) - return false; - } - - auto* imgui = static_cast(ui.it.get()); - if (imgui && ui.descriptor) - { - std::array infos; - for (auto& it : infos) - it.info.image.imageLayout = IImage::LAYOUT::READ_ONLY_OPTIMAL; - - auto* ix = infos.data(); - ix->desc = smart_refctd_ptr(imgui->getFontAtlasView()); - ++ix; - for (uint8_t i = 0u; i < device_base_t::MaxFramesInFlight; ++i, ++ix) - ix->desc = m_frameBuffers2D[i]->getCreationParameters().colorAttachments[0u]; - for (uint8_t i = 0u; i < device_base_t::MaxFramesInFlight; ++i, ++ix) - ix->desc = m_frameBuffers3D[i]->getCreationParameters().colorAttachments[0u]; - - const auto texturesBinding = imgui->getCreationParameters().resources.texturesInfo.bindingIx; - auto writes = std::to_array({ IGPUDescriptorSet::SWriteDescriptorSet{ - .dstSet = ui.descriptor->getDescriptorSet(), - .binding = texturesBinding, - .arrayElement = ext::imgui::UI::FontAtlasTexId, - .count = static_cast(infos.size()), - .info = infos.data() - } }); - - if (!m_device->updateDescriptorSets(writes, {})) - return false; - } - - const float aspect = float(width) / float(height); - const auto projectionMatrix = buildProjectionMatrixPerspectiveFovLH(hlsl::radians(uiState.cameraFovDeg), aspect, 0.1f, 10000.0f); - camera.setProjectionMatrix(projectionMatrix); - - return true; -} - -IQueue::SSubmitInfo::SSemaphoreInfo IESViewer::renderFrame(const std::chrono::microseconds nextPresentationTimestamp) -{ - const auto resourceIx = m_realFrameIx % device_base_t::MaxFramesInFlight; - auto* const cb = m_cmdBuffers.data()[resourceIx].get(); - - auto scRes = static_cast(m_surface->getSwapchainResources()); - auto* imgui = static_cast(ui.it.get()); - - const bool windowFocused = m_window->hasInputFocus() || m_window->hasMouseFocus(); - if (!windowFocused && uiState.cameraControlEnabled) - uiState.cameraControlEnabled = false; - const bool wantCameraControl = uiState.cameraControlEnabled && windowFocused; - - uint32_t renderWidth = m_window->getWidth(); - uint32_t renderHeight = m_window->getHeight(); - if (auto* sc = scRes->getSwapchain()) - { - const auto& params = sc->getCreationParameters().sharedParams; - if (params.width && params.height) - { - renderWidth = params.width; - renderHeight = params.height; - } - } - if (renderWidth == 0u || renderHeight == 0u || m_window->isMinimized()) - return {}; - - if (uiState.cameraControlApplied != wantCameraControl) - { - 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); - } - - - - const uint32_t desired3DWidth = renderWidth; - const uint32_t desired3DHeight = renderHeight; - if (!recreate3DPlotFramebuffers(desired3DWidth, desired3DHeight)) - return {}; - - auto* const fb2D = m_frameBuffers2D[resourceIx].get(); - auto* const fb3D = m_frameBuffers3D[resourceIx].get(); - - cb->reset(IGPUCommandBuffer::RESET_FLAGS::RELEASE_RESOURCES_BIT); - cb->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT); - - m_inputSystem->getDefaultMouse(&mouse); - m_inputSystem->getDefaultKeyboard(&keyboard); - { - struct - { - 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); - }, m_logger.get()); - } - else - { - mouse.consumeEvents([&](const IMouseEventChannel::range_t&) -> void {}, m_logger.get()); - keyboard.consumeEvents([&](const IKeyboardEventChannel::range_t&) -> void {}, m_logger.get()); - } - camera.endInputProcessing(nextPresentationTimestamp); - - { - 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()); - const float dist = length(pos); - if (dist > maxRadius) - { - const auto target = toHlslVec3(camera.getTarget()); - const auto forward = target - pos; - pos = normalize(pos) * clampRadius; - camera.setPosition(toCoreVec3(pos)); - camera.setTarget(toCoreVec3(pos + forward)); - } - } - - auto* cursorControl = m_window->getCursorControl(); - const auto cursorPosition = cursorControl->getPosition(); - const int32_t windowX = m_window->getX(); - const int32_t windowY = m_window->getY(); - const int32_t windowW = static_cast(m_window->getWidth()); - const int32_t windowH = static_cast(m_window->getHeight()); - const bool cursorInsideWindow = - cursorPosition.x >= windowX && cursorPosition.x < windowX + windowW && - cursorPosition.y >= windowY && cursorPosition.y < windowY + windowH; - cursorControl->setVisible(!(cursorInsideWindow || uiState.cameraControlApplied)); - ext::imgui::UI::SUpdateParameters params = - { - .mousePosition = float32_t2(cursorPosition.x,cursorPosition.y) - float32_t2(m_window->getX(),m_window->getY()), - .displaySize = {renderWidth,renderHeight}, - .mouseEvents = captured.mouse, - .keyboardEvents = captured.keyboard - }; - - if (imgui) - imgui->update(params); - } - - if (uiState.cameraControlApplied) - { - if (auto* cursor = m_window->getCursorControl()) - cursor->setRelativePosition(m_window.get(), {0.5f, 0.5f}); - } - - auto& ies = m_assets[uiState.activeAssetIx]; - const auto* profile = ies.getProfile(); - const auto& accessor = profile->getAccessor(); - const auto hCount = accessor.hAnglesCount(); - const auto vCount = accessor.vAnglesCount(); - const auto pc = hlsl::this_example::ies::CdcPC - { - .hAnglesBDA = ies.buffers.hAngles->getDeviceAddress(), - .vAnglesBDA = ies.buffers.vAngles->getDeviceAddress(), - .dataBDA = ies.buffers.data->getDeviceAddress(), - .txtInfoBDA = ies.buffers.textureInfo.buffer->getDeviceAddress(), - .mode = uiState.mode.view, - .texIx = static_cast(uiState.activeAssetIx), - .hAnglesCount = hCount, - .vAnglesCount = vCount, - .zAngleDegreeRotation = ies.zDegree, - .properties = accessor.getProperties() - }; - - for (auto& buffer : { ies.buffers.data, ies.buffers.hAngles, ies.buffers.vAngles }) // flush request for sanity - { - auto bound = buffer->getBoundMemory(); - if (bound.memory->haveToMakeVisible()) - { - const ILogicalDevice::MappedMemoryRange range(bound.memory, bound.offset, buffer->getSize()); - m_device->flushMappedMemoryRanges(1, &range); - } - } - - auto* const descriptor = m_descriptors[0].get(); - auto* image = ies.getActiveImage(IES::EM_OCTAHEDRAL_MAP); - - bool needCompute = true; - if (uiState.activeAssetIx < m_candelaDirty.size()) - needCompute = m_candelaDirty[uiState.activeAssetIx]; - - if (needCompute) - { - cb->beginDebugMarker("IES::compute"); - IES::barrier(cb, image); - auto* layout = m_computePipeline->getLayout(); - cb->bindComputePipeline(m_computePipeline.get()); - cb->bindDescriptorSets(E_PIPELINE_BIND_POINT::EPBP_COMPUTE, layout, 0, 1, &descriptor); - cb->pushConstants(layout, layout->getPushConstantRanges().begin()->stageFlags, offsetof(hlsl::this_example::ies::PushConstants, cdc), sizeof(pc), &pc); - const auto xGroups = (ies.getProfile()->getAccessor().properties.optimalIESResolution.x - 1u) / hlsl::this_example::WorkgroupDimension + 1u; - cb->dispatch(xGroups, xGroups, 1); - IES::barrier(cb, image); - cb->endDebugMarker(); - if (uiState.activeAssetIx < m_candelaDirty.size()) - m_candelaDirty[uiState.activeAssetIx] = false; - } - - // Graphics - { - auto extent = fb2D->getCreationParameters().colorAttachments[0u]->getCreationParameters().image->getCreationParameters().extent; - const uint32_t plotHeight = extent.height / 2u; - - asset::SViewport viewport; - { - viewport.minDepth = 1.f; - viewport.maxDepth = 0.f; - viewport.x = 0u; - viewport.y = 0u; - viewport.width = extent.width; - viewport.height = extent.height; - } - cb->setViewport(0u, 1u, &viewport); - - VkRect2D scissor = - { - .offset = { 0, 0 }, - .extent = { extent.width, extent.height }, - }; - cb->setScissor(0u, 1u, &scissor); - - VkRect2D currentRenderArea = - { - .offset = {0,0}, - .extent = {extent.width,extent.height} - }; - - const IGPUCommandBuffer::SClearColorValue clearValue = { .float32 = {0.f,0.f,0.f,1.f} }; - const IGPUCommandBuffer::SClearDepthStencilValue depthValue = { .depth = 0.f }; - IGPUCommandBuffer::SRenderpassBeginInfo info = - { - .framebuffer = fb2D, - .colorClearValues = &clearValue, - .depthStencilClearValues = &depthValue, - .renderArea = currentRenderArea - }; - - cb->beginDebugMarker("IES::graphics 2D plot"); - cb->beginRenderPass(info, IGPUCommandBuffer::SUBPASS_CONTENTS::INLINE); - { - auto* layout = m_graphicsPipeline->getLayout(); - cb->bindGraphicsPipeline(m_graphicsPipeline.get()); - cb->bindDescriptorSets(EPBP_GRAPHICS, layout, 0, 1, &descriptor); - asset::SViewport viewport2D = viewport; - viewport2D.width = static_cast(extent.width); - viewport2D.height = static_cast(plotHeight); - VkRect2D scissor2D = scissor; - scissor2D.extent = { extent.width, plotHeight }; - - auto pc2D = pc; - pc2D.mode = uiState.mode.view; - cb->setViewport(0u, 1u, &viewport2D); - cb->setScissor(0u, 1u, &scissor2D); - cb->pushConstants(layout, layout->getPushConstantRanges().begin()->stageFlags, 0, sizeof(pc2D), &pc2D); - ext::FullScreenTriangle::recordDrawCall(cb); - - if (uiState.showOctaMapPreview) - { - viewport2D.y = static_cast(plotHeight); - scissor2D.offset.y = static_cast(plotHeight); - pc2D.mode = IES::EM_OCTAHEDRAL_MAP; - cb->setViewport(0u, 1u, &viewport2D); - cb->setScissor(0u, 1u, &scissor2D); - cb->pushConstants(layout, layout->getPushConstantRanges().begin()->stageFlags, 0, sizeof(pc2D), &pc2D); - ext::FullScreenTriangle::recordDrawCall(cb); - } - } - cb->endRenderPass(); - cb->endDebugMarker(); - - const IGPUCommandBuffer::SClearColorValue d3clearValue = { .float32 = {1.f,0.f,1.f,1.f} }; - auto info3D = info; - info3D.colorClearValues = &d3clearValue; // tmp - info3D.depthStencilClearValues = &depthValue; - info3D.framebuffer = fb3D; - auto extent3D = fb3D->getCreationParameters().colorAttachments[0u]->getCreationParameters().image->getCreationParameters().extent; - viewport.width = extent3D.width; - viewport.height = extent3D.height; - cb->setViewport(0u, 1u, &viewport); - scissor.extent = { extent3D.width, extent3D.height }; - cb->setScissor(0u, 1u, &scissor); - currentRenderArea.extent = { extent3D.width, extent3D.height }; - info3D.renderArea = currentRenderArea; - 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 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 }); - - // tear down scene every frame - m_renderer->m_instances[0].packedGeo = m_renderer->getGeometries().data() + uiState.activeAssetIx; - m_renderer->render(cb, viewParams, iesParams); - } - cb->endRenderPass(); - cb->endDebugMarker(); - - cb->beginDebugMarker("IES::graphics ImGUI"); - - viewport.width = renderWidth; - viewport.height = renderHeight; - cb->setViewport(0u, 1u, &viewport); - scissor.extent = { renderWidth, renderHeight }; - cb->setScissor(0u, 1u, &scissor); - currentRenderArea.extent = { renderWidth, renderHeight }; - - info.framebuffer = scRes->getFramebuffer(device_base_t::getCurrentAcquire().imageIndex); - info.renderArea = currentRenderArea; - - cb->beginRenderPass(info, IGPUCommandBuffer::SUBPASS_CONTENTS::INLINE); - if (imgui) - { - auto* pipeline = imgui->getPipeline(); - cb->bindGraphicsPipeline(pipeline); - const auto* ds = ui.descriptor->getDescriptorSet(); - cb->bindDescriptorSets(EPBP_GRAPHICS, pipeline->getLayout(), imgui->getCreationParameters().resources.texturesInfo.setIx, 1u, &ds); - const ISemaphore::SWaitInfo wait = { .semaphore = m_semaphore.get(),.value = m_realFrameIx + 1u }; - if (!imgui->render(cb, wait)) - { - 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(); - } - - 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--; - } - - std::string caption = "[Nabla Engine] IES Viewer"; - { - m_window->setCaption(caption); - } - return retval; -} - -void IESViewer::onPostRenderFrame(const video::IQueue::SSubmitInfo::SSemaphoreInfo& rendered) -{ - if (!m_ciMode || m_ciScreenshotDone) - return; - - ++m_ciFrameCounter; - if (m_ciFrameCounter < CiFramesBeforeCapture) - return; - - m_ciScreenshotDone = true; - - if (!m_device || !m_surface || !m_assetMgr) - { - requestExit(); - return; - } - - // Ensure the last submitted frame is finished before we read back. - m_device->waitIdle(); - - auto* scRes = static_cast(m_surface->getSwapchainResources()); - auto* fb = scRes->getFramebuffer(device_base_t::getCurrentAcquire().imageIndex); - if (!fb) - { - m_logger->log("CI screenshot failed: missing swapchain framebuffer.", system::ILogger::ELL_ERROR); - requestExit(); - return; - } - - auto colorView = fb->getCreationParameters().colorAttachments[0u]; - if (!colorView) - { - m_logger->log("CI screenshot failed: missing swapchain color attachment.", system::ILogger::ELL_ERROR); - requestExit(); - return; - } - - { - const auto usage = colorView->getCreationParameters().image->getCreationParameters().usage; - const bool hasTransferSrc = usage.hasFlags(asset::IImage::EUF_TRANSFER_SRC_BIT); - m_logger->log( - "CI screenshot source usage: 0x%llx (transfer_src=%s).", - system::ILogger::ELL_INFO, - static_cast(usage.value), - hasTransferSrc ? "yes" : "no"); - } - - const bool ok = ext::ScreenShot::createScreenShot( - m_device.get(), - getGraphicsQueue(), - nullptr, - colorView.get(), - m_assetMgr.get(), - m_ciScreenshotPath, - asset::IImage::LAYOUT::PRESENT_SRC, - asset::ACCESS_FLAGS::COLOR_ATTACHMENT_WRITE_BIT); - - if (ok) - m_logger->log("CI screenshot saved to \"%s\".", system::ILogger::ELL_INFO, m_ciScreenshotPath.string().c_str()); - else - m_logger->log("CI screenshot failed to save.", system::ILogger::ELL_ERROR); - - requestExit(); -} - -const video::IGPURenderpass::SCreationParams::SSubpassDependency* IESViewer::getDefaultSubpassDependencies() const -{ - // Subsequent submits don't wait for each other, hence its important to have External Dependencies which prevent users of the depth attachment overlapping. - 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 - .srcStageMask = PIPELINE_STAGE_FLAGS::LATE_FRAGMENT_TESTS_BIT, - // don't want any writes to be available, we'll clear - .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 - }, - // color from ATTACHMENT_OPTIMAL to PRESENT_SRC - { - .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; -} - diff --git a/50.IESViewer/AppUI.cpp b/50.IESViewer/AppUI.cpp deleted file mode 100644 index c376f7730..000000000 --- a/50.IESViewer/AppUI.cpp +++ /dev/null @@ -1,651 +0,0 @@ -// 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 - -#include "App.hpp" -#include -#include -#include -#include -#include "imgui/imgui.h" -#include "imgui/imgui_internal.h" -#include "nbl/ext/ImGui/ImGui.h" -#include "app_resources/common.hlsl" -#include "app_resources/false_color.hlsl" -#include "app_resources/imgui.opts.hlsl" -#include "nbl/builtin/hlsl/math/thin_lens_projection.hlsl" -#include "nbl/builtin/hlsl/math/linalg/fast_affine.hlsl" -#include "nbl/builtin/hlsl/math/octahedral.hlsl" - -void IESViewer::uiListener() -{ - const auto resourceIx = m_realFrameIx % device_base_t::MaxFramesInFlight; - - SImResourceInfo info; - info.textureID = ext::imgui::UI::FontAtlasTexId + resourceIx + 1u; - info.samplerIx = (uint16_t)ext::imgui::UI::DefaultSamplerIx::USER; - - const ImGuiViewport* vp = ImGui::GetMainViewport(); - const ImVec2 viewportPos = vp->Pos; - const ImVec2 viewportSize = vp->Size; - auto* cursorControl = m_window->getCursorControl(); - const auto cursorPosition = cursorControl ? cursorControl->getPosition() : ICursorControl::SPosition{}; - const int32_t windowX = m_window->getX(); - const int32_t windowY = m_window->getY(); - const int32_t windowW = static_cast(m_window->getWidth()); - const int32_t windowH = static_cast(m_window->getHeight()); - const bool cursorInsideWindow = cursorControl && - cursorPosition.x >= windowX && cursorPosition.x < windowX + windowW && - cursorPosition.y >= windowY && cursorPosition.y < windowY + windowH; - ImGui::GetIO().MouseDrawCursor = cursorInsideWindow && !uiState.cameraControlEnabled; - const ImVec2 bottomSize(viewportSize.x, viewportSize.y); - const ImVec2 bottomPos(viewportPos.x, viewportPos.y); - const auto legendColor = [&](float v, bool useFalseColor) -> ImU32 - { - const float clamped = ImClamp(v, 0.0f, 1.0f); - if (useFalseColor) - { - const auto col = hlsl::this_example::ies::falseColor(clamped); - return ImGui::ColorConvertFloat4ToU32(ImVec4(col.x, col.y, col.z, 1.0f)); - } - return ImGui::ColorConvertFloat4ToU32(ImVec4(clamped, clamped, clamped, 1.0f)); - }; - const auto showHint = [&](const char* text) - { - if (!uiState.showHints || !text || text[0] == '\0') - return; - if (!ImGui::IsItemHovered()) - return; - ImGui::BeginTooltip(); - ImGui::TextUnformatted(text); - ImGui::EndTooltip(); - }; - std::vector assetLabelPtrs; - assetLabelPtrs.reserve(m_assetLabels.size()); - for (const auto& label : m_assetLabels) - assetLabelPtrs.push_back(label.c_str()); - - size_t activeIx = uiState.activeAssetIx; - if (activeIx >= m_assets.size()) - activeIx = 0u; - int activeIxUi = static_cast(activeIx); - float candelaValue = 0.0f; - bool candelaValid = false; - ImVec2 plotRectMin(0.f, 0.f); - ImVec2 plotRectMax(0.f, 0.f); - bool plotRectValid = false; - bool plotHovered = false; - uiState.plot2DRectValid = false; - - auto& ies = m_assets[activeIx]; - auto* profile = ies.getProfile(); - const auto& accessor = profile->getAccessor(); - const auto& properties = accessor.getProperties(); - - const float lowerBound = accessor.hAngles.front(); - const float upperBound = accessor.hAngles.back(); - const bool singleAngle = (upperBound == lowerBound); - - constexpr size_t kSmallBufSize = 32; - auto angle = ImClamp(ies.zDegree, lowerBound, upperBound); - - auto updateCameraProjection = [&]() - { - if (m_plot3DWidth == 0u || m_plot3DHeight == 0u) - 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); - }; - - auto draw3DControls = [&]() - { - bool interpolateCandela = uiState.mode.sphere.hasFlags(hlsl::this_example::ies::ESM_OCTAHEDRAL_UV_INTERPOLATE); - - if (ImGui::Checkbox("interpolate candelas", &interpolateCandela)) - { - if (interpolateCandela) - uiState.mode.sphere |= hlsl::this_example::ies::E_SPHERE_MODE::ESM_OCTAHEDRAL_UV_INTERPOLATE; - else - uiState.mode.sphere &= static_cast( - ~hlsl::this_example::ies::E_SPHERE_MODE::ESM_OCTAHEDRAL_UV_INTERPOLATE - ); - } - showHint("Interpolate candela values in the octahedral map."); - - bool falseColor = uiState.mode.sphere.hasFlags(hlsl::this_example::ies::ESM_FALSE_COLOR); - - if (ImGui::Checkbox("false color", &falseColor)) - { - if (falseColor) - uiState.mode.sphere |= hlsl::this_example::ies::E_SPHERE_MODE::ESM_FALSE_COLOR; - else - uiState.mode.sphere &= static_cast( - ~hlsl::this_example::ies::E_SPHERE_MODE::ESM_FALSE_COLOR - ); - } - showHint("Use false color palette for the 3D plot."); - - bool showOctaMap = uiState.showOctaMapPreview; - if (ImGui::Checkbox("octahedral map", &showOctaMap)) - uiState.showOctaMapPreview = showOctaMap; - showHint("Show octahedral map preview under the 2D plot."); - - bool showHints = uiState.showHints; - if (ImGui::Checkbox("show hints", &showHints)) - uiState.showHints = showHints; - showHint("Toggle help tooltips."); - - bool cubePlot = uiState.mode.sphere.hasFlags(hlsl::this_example::ies::ESM_CUBE); - - if (ImGui::Checkbox("cube plot", &cubePlot)) - { - if (cubePlot) - uiState.mode.sphere |= hlsl::this_example::ies::E_SPHERE_MODE::ESM_CUBE; - else - uiState.mode.sphere &= static_cast( - ~hlsl::this_example::ies::E_SPHERE_MODE::ESM_CUBE - ); - } - showHint("Render the plot on a cube instead of a sphere."); - - bool wireframe = uiState.wireframeEnabled; - if (ImGui::Checkbox("wireframe", &wireframe)) - uiState.wireframeEnabled = wireframe; - showHint("Show wireframe topology in the 3D plot."); - - bool cameraControl = uiState.cameraControlEnabled; - if (ImGui::Checkbox("camera control (space)", &cameraControl)) - uiState.cameraControlEnabled = cameraControl; - showHint("Enable camera movement with mouse and keyboard."); - - bool speedChanged = false; - bool fovChanged = false; - if (ImGui::BeginTable("##camera_controls", 2, ImGuiTableFlags_SizingStretchProp)) - { - float labelWidth = 0.0f; - labelWidth = ImMax(labelWidth, ImGui::CalcTextSize("move speed").x); - labelWidth = ImMax(labelWidth, ImGui::CalcTextSize("rotate speed").x); - labelWidth = ImMax(labelWidth, ImGui::CalcTextSize("fov").x); - labelWidth += ImGui::GetStyle().CellPadding.x * 2.0f; - labelWidth = ImMin(labelWidth, ImGui::GetContentRegionAvail().x * 0.6f); - ImGui::TableSetupColumn("label", ImGuiTableColumnFlags_WidthFixed, labelWidth); - ImGui::TableSetupColumn("value", ImGuiTableColumnFlags_WidthStretch); - auto sliderRow = [&](const char* label, float* value, float min, float max, const char* fmt, const char* hint) - { - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::AlignTextToFramePadding(); - ImGui::TextUnformatted(label); - showHint(hint); - ImGui::TableSetColumnIndex(1); - ImGui::SetNextItemWidth(-FLT_MIN); - ImGui::PushID(label); - const bool changed = ImGui::SliderFloat("##value", value, min, max, fmt, ImGuiSliderFlags_AlwaysClamp); - ImGui::PopID(); - showHint(hint); - return changed; - }; - - speedChanged |= sliderRow("move speed", &uiState.cameraMoveSpeed, 0.1f, 10.0f, "%.2f", "Camera movement speed."); - speedChanged |= sliderRow("rotate speed", &uiState.cameraRotateSpeed, 0.1f, 5.0f, "%.2f", "Camera rotation speed."); - fovChanged |= sliderRow("fov", &uiState.cameraFovDeg, 30.0f, 120.0f, "%.0f", "Camera field of view."); - - ImGui::EndTable(); - } - - if (speedChanged && uiState.cameraControlEnabled) - { - camera.setMoveSpeed(uiState.cameraMoveSpeed); - camera.setRotateSpeed(uiState.cameraRotateSpeed); - } - - if (fovChanged) - updateCameraProjection(); - - }; - - const float panelMargin = 8.f; - const float panelWidth = ImClamp(viewportSize.x * 0.25f, 260.0f, 420.0f); - const float panelMaxHeight = ImMax(240.0f, viewportSize.y * 0.9f); - ImGui::SetNextWindowPos(ImVec2(viewportPos.x + panelMargin, viewportPos.y + panelMargin), ImGuiCond_Always); - ImGui::SetNextWindowSizeConstraints(ImVec2(panelWidth, 0.0f), ImVec2(panelWidth, panelMaxHeight)); - ImGui::SetNextWindowBgAlpha(0.7f); - ImGuiWindowFlags panelFlags = - ImGuiWindowFlags_NoDecoration | - ImGuiWindowFlags_NoMove | - ImGuiWindowFlags_NoSavedSettings | - ImGuiWindowFlags_NoNav | - ImGuiWindowFlags_AlwaysAutoResize | - ImGuiWindowFlags_NoResize; - - if (ImGui::Begin("IES Panel", nullptr, panelFlags)) - { - const auto& resolution = accessor.properties.optimalIESResolution; - - constexpr size_t kInfoBufSize = 64; - std::array bAngle{}; - std::array bAngles{}; - std::array bRes{}; - std::array bMax{}; - std::array bAvg{}; - std::array bAvgFull{}; - const auto hCount = accessor.hAnglesCount(); - const auto vCount = accessor.vAnglesCount(); - std::snprintf(bAngle.data(), bAngle.size(), "%.3f deg", angle); - std::snprintf(bAngles.data(), bAngles.size(), "angles: %u x %u", hCount, vCount); - std::snprintf(bRes.data(), bRes.size(), "resolution: %u x %u", resolution.x, resolution.y); - std::snprintf(bMax.data(), bMax.size(), "max cd: %.3f", properties.maxCandelaValue); - std::snprintf(bAvg.data(), bAvg.size(), "avg: %.3f", properties.avgEmmision); - std::snprintf(bAvgFull.data(), bAvgFull.size(), "avg full: %.3f", properties.fullDomainAvgEmission); - const std::string symmetryLabel = nbl::system::to_string(properties.getSymmetry()); - const std::string typeLabel = nbl::system::to_string(properties.getType()); - const std::string versionLabel = nbl::system::to_string(properties.getVersion()); - float leftWidth = 0.0f; - leftWidth = ImMax(leftWidth, ImGui::CalcTextSize(symmetryLabel.c_str()).x); - leftWidth = ImMax(leftWidth, ImGui::CalcTextSize(versionLabel.c_str()).x); - leftWidth = ImMax(leftWidth, ImGui::CalcTextSize(bAngles.data()).x); - leftWidth = ImMax(leftWidth, ImGui::CalcTextSize(bMax.data()).x); - leftWidth = ImMax(leftWidth, ImGui::CalcTextSize(bAvgFull.data()).x); - leftWidth += ImGui::GetStyle().CellPadding.x * 2.0f; - leftWidth = ImMin(leftWidth, ImGui::GetContentRegionAvail().x * 0.6f); - if (ImGui::BeginTable("##profile_info", 2, ImGuiTableFlags_SizingFixedFit)) - { - ImGui::TableSetupColumn("left", ImGuiTableColumnFlags_WidthFixed, leftWidth); - ImGui::TableSetupColumn("right", ImGuiTableColumnFlags_WidthStretch); - auto rightText = [&](const char* text, const char* hint) - { - const float avail = ImGui::GetContentRegionAvail().x; - const float textWidth = ImGui::CalcTextSize(text).x; - const char* displayText = text; - std::string clipped; - if (textWidth > avail && avail > 0.0f) - { - const char* ell = "..."; - const float ellW = ImGui::CalcTextSize(ell).x; - const float target = ImMax(0.0f, avail - ellW); - const int len = static_cast(std::strlen(text)); - int lo = 0; - int hi = len; - while (lo < hi) - { - int mid = (lo + hi + 1) / 2; - float w = ImGui::CalcTextSize(text, text + mid).x; - if (w <= target) - lo = mid; - else - hi = mid - 1; - } - clipped.assign(text, text + lo); - clipped.append(ell); - displayText = clipped.c_str(); - } - const float displayWidth = ImGui::CalcTextSize(displayText).x; - if (displayWidth < avail) - ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (avail - displayWidth)); - ImGui::TextUnformatted(displayText); - showHint(hint); - }; - auto row = [&](const char* left, const char* right, const char* leftHint, const char* rightHint) - { - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::TextUnformatted(left); - showHint(leftHint); - ImGui::TableSetColumnIndex(1); - rightText(right, rightHint); - }; - - row(symmetryLabel.c_str(), typeLabel.c_str(), "IES symmetry mode.", "IES photometric type."); - row(versionLabel.c_str(), assetLabelPtrs.empty() ? ies.key.c_str() : assetLabelPtrs[activeIx], "IES standard/version.", "Active IES profile file."); - row(bAngles.data(), bRes.data(), "Horizontal and vertical angle count.", "Octahedral map resolution."); - row(bMax.data(), bAvg.data(), "Maximum candela value.", "Average candela value."); - row(bAvgFull.data(), bAngle.data(), "Average candela over full domain.", "Current horizontal angle."); - - ImGui::EndTable(); - } - - ImGui::Separator(); - - const ImVec2 avail = ImGui::GetContentRegionAvail(); - ImVec2 plotSize(0.f, 0.f); - float plotSide = ImMax(0.0f, avail.x); - if (plotSide > 0.0f) - { - plotSize = ImVec2(plotSide, plotSide); - ImVec2 plotPos = ImGui::GetCursorScreenPos(); - { - const std::string modeLabel = nbl::system::to_string(uiState.mode.view); - const char* title = modeLabel.c_str(); - const ImVec2 titleSize = ImGui::CalcTextSize(title); - const float titleX = ImMax(0.0f, (ImGui::GetContentRegionAvail().x - titleSize.x) * 0.5f); - ImGui::SetCursorPosX(ImGui::GetCursorPosX() + titleX); - ImGui::TextUnformatted(title); - showHint("2D candlepower distribution curve."); - } - - plotPos = ImGui::GetCursorScreenPos(); - ImGui::Image(info, plotSize, ImVec2(0.f, 0.f), ImVec2(1.f, 0.5f)); - const ImVec2 itemMin = ImGui::GetItemRectMin(); - const ImVec2 itemMax = ImGui::GetItemRectMax(); - uiState.plot2DRectMin = float32_t2(itemMin.x, itemMin.y); - uiState.plot2DRectMax = float32_t2(itemMax.x, itemMax.y); - uiState.plot2DRectValid = true; - showHint("2D candlepower distribution curve."); - - ImDrawList* dl = ImGui::GetWindowDrawList(); - - const float pad = 6.f; - const float barWidth = 16.f; - const float sliderH = ImMax(0.f, plotSize.y - pad * 2.f); - const float sliderX = plotPos.x + plotSize.x - barWidth - pad; - const float sliderY = plotPos.y + pad; - - if (sliderH > 0.0f) - { - ImGui::SetCursorScreenPos(ImVec2(sliderX, sliderY)); - ImGui::InvisibleButton("##angle_slider", ImVec2(barWidth, sliderH)); - showHint("Adjust horizontal angle."); - ImVec2 rmin = ImGui::GetItemRectMin(); - ImVec2 rmax = ImGui::GetItemRectMax(); - ImU32 col = IM_COL32(220, 60, 60, 255); - - float knobR = 7.f; - float trackX = rmax.x - barWidth * 0.5f; - float y0 = rmin.y + knobR + 1.f; - float y1 = rmax.y - knobR - 1.f; - - dl->AddLine(ImVec2(trackX, y0), ImVec2(trackX, y1), col, 3.f); - - if (singleAngle) - { - float y = (y0 + y1) * 0.5f; - dl->AddLine(ImVec2(trackX - 22.f, y), ImVec2(trackX - 8.f, y), ImGui::GetColorU32(ImGuiCol_Text)); - std::array tb{}; - std::snprintf(tb.data(), tb.size(), "%.0f", lowerBound); - ImVec2 ts = ImGui::CalcTextSize(tb.data()); - dl->AddText(ImVec2(trackX - 24.f - ts.x, y - ts.y * 0.5f), ImGui::GetColorU32(ImGuiCol_Text), tb.data()); - } - else - { - for (int i = 0; i < 5; ++i) - { - float v = lowerBound + (upperBound - lowerBound) * (float(i) / 4.f); - float t = (v - lowerBound) / (upperBound - lowerBound); - float y = y1 - t * (y1 - y0); - dl->AddLine(ImVec2(trackX - 22.f, y), ImVec2(trackX - 8.f, y), ImGui::GetColorU32(ImGuiCol_Text)); - std::array tb{}; - std::snprintf(tb.data(), tb.size(), "%.0f", v); - ImVec2 ts = ImGui::CalcTextSize(tb.data()); - dl->AddText(ImVec2(trackX - 24.f - ts.x, y - ts.y * 0.5f), ImGui::GetColorU32(ImGuiCol_Text), tb.data()); - } - } - - float t = singleAngle ? 0.5f : (angle - lowerBound) / (upperBound - lowerBound); - float knobY = y1 - t * (y1 - y0); - dl->AddCircleFilled(ImVec2(trackX, knobY), knobR, col); - dl->AddCircle(ImVec2(trackX, knobY), knobR, ImGui::GetColorU32(ImGuiCol_Border)); - - if (!singleAngle && (ImGui::IsItemHovered() || ImGui::IsItemActive()) && ImGui::IsMouseDown(0)) - { - float my = ImClamp(ImGui::GetIO().MousePos.y, y0, y1); - float nt = (y1 - my) / (y1 - y0); - angle = lowerBound + nt * (upperBound - lowerBound); - } - } - } - - if (plotSize.x > 0.0f && plotSize.y > 0.0f && uiState.showOctaMapPreview) - { - ImGui::Spacing(); - { - const char* title = "Octahedral Map"; - const ImVec2 titleSize = ImGui::CalcTextSize(title); - const float titleX = ImMax(0.0f, (ImGui::GetContentRegionAvail().x - titleSize.x) * 0.5f); - ImGui::SetCursorPosX(ImGui::GetCursorPosX() + titleX); - ImGui::TextUnformatted(title); - showHint("Octahedral map preview."); - } - ImGui::Image(info, plotSize, ImVec2(0.f, 0.5f), ImVec2(1.f, 1.f)); - showHint("Octahedral map preview."); - } - - ImGui::Separator(); - draw3DControls(); - ImGui::Separator(); - - if (!assetLabelPtrs.empty()) - { - ImGui::TextUnformatted("profile"); - ImGui::SameLine(); - if (ImGui::ArrowButton("##profile_prev", ImGuiDir_Up)) - { - activeIx = (activeIx + assetLabelPtrs.size() - 1u) % assetLabelPtrs.size(); - activeIxUi = static_cast(activeIx); - } - ImGui::SameLine(); - if (ImGui::ArrowButton("##profile_next", ImGuiDir_Down)) - { - activeIx = (activeIx + 1u) % assetLabelPtrs.size(); - activeIxUi = static_cast(activeIx); - } - showHint("Select active IES profile. Use up/down arrows."); - ImGui::NewLine(); - ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); - if (ImGui::Combo("##profile", &activeIxUi, assetLabelPtrs.data(), static_cast(assetLabelPtrs.size()))) - activeIx = static_cast(activeIxUi); - showHint("Select active IES profile."); - } - } - ImGui::End(); - - ies.zDegree = angle; - uiState.activeAssetIx = activeIx; - // 3D plot - { - info.textureID += device_base_t::MaxFramesInFlight; - - { - ImGui::SetNextWindowPos(bottomPos, ImGuiCond_Always); - ImGui::SetNextWindowSize(bottomSize, ImGuiCond_Always); - - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.f, 0.f)); - ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.f); - - ImGuiWindowFlags imgFlags = - ImGuiWindowFlags_NoDecoration | - ImGuiWindowFlags_NoMove | - ImGuiWindowFlags_NoSavedSettings | - ImGuiWindowFlags_NoBringToFrontOnFocus | - ImGuiWindowFlags_NoNav | - ImGuiWindowFlags_NoScrollbar | - ImGuiWindowFlags_NoScrollWithMouse; - - if (ImGui::Begin("3D Plot", nullptr, imgFlags)) - { - const ImVec2 avail = ImGui::GetContentRegionAvail(); - const ImVec2 plotSize(ImMax(0.0f, avail.x), ImMax(0.0f, avail.y)); - ImVec2 imgPos = ImGui::GetCursorScreenPos(); - ImGui::Image(info, plotSize); - plotRectMin = ImGui::GetItemRectMin(); - plotRectMax = ImGui::GetItemRectMax(); - plotRectValid = true; - plotHovered = ImGui::IsItemHovered(); - - const float margin = 8.0f; - const float barWidth = 16.0f; - const float barHeight = ImMax(80.0f, plotSize.y - margin * 2.0f); - if (plotSize.x > barWidth + margin * 2.0f && plotSize.y > margin * 2.0f) - { - const bool useFalseColorLegend = uiState.mode.sphere.hasFlags(hlsl::this_example::ies::ESM_FALSE_COLOR); - ImVec2 barMin(imgPos.x + plotSize.x - barWidth - margin, imgPos.y + margin); - ImVec2 barMax(barMin.x + barWidth, barMin.y + barHeight); - - ImDrawList* dl = ImGui::GetWindowDrawList(); - const int steps = 64; - for (int i = 0; i < steps; ++i) - { - const float t0 = float(i) / float(steps); - const float t1 = float(i + 1) / float(steps); - const float y0 = barMin.y + (1.0f - t1) * barHeight; - const float y1 = barMin.y + (1.0f - t0) * barHeight; - const float v = (t0 + t1) * 0.5f; - const ImU32 col = legendColor(v, useFalseColorLegend); - dl->AddRectFilled(ImVec2(barMin.x, y0), ImVec2(barMax.x, y1), col); - } - dl->AddRect(barMin, barMax, ImGui::GetColorU32(ImGuiCol_Border)); - - const ImU32 textCol = ImGui::GetColorU32(ImGuiCol_Text); - for (uint32_t i = 0u; i < hlsl::this_example::ies::FalseColorStopCount; ++i) - { - const float stop = hlsl::this_example::ies::falseColorStop(i); - const float y = barMin.y + (1.0f - stop) * barHeight; - dl->AddLine(ImVec2(barMin.x - 4.0f, y), ImVec2(barMin.x, y), textCol); - const float cdValue = stop * properties.maxCandelaValue; - std::array label{}; - std::snprintf(label.data(), label.size(), "%.0f cd", cdValue); - ImVec2 labelSize = ImGui::CalcTextSize(label.data()); - dl->AddText(ImVec2(barMin.x - labelSize.x - 6.0f, y - labelSize.y * 0.5f), textCol, label.data()); - } - } - } - ImGui::End(); - - ImGui::PopStyleVar(2); - } - } - - if (plotRectValid && plotHovered && activeIx < m_assets.size()) - { - const float plotW = plotRectMax.x - plotRectMin.x; - const float plotH = plotRectMax.y - plotRectMin.y; - const ImVec2 mousePos = ImGui::GetIO().MousePos; - if (plotW > 1.0f && plotH > 1.0f && - mousePos.x >= plotRectMin.x && mousePos.x <= plotRectMax.x && - mousePos.y >= plotRectMin.y && mousePos.y <= plotRectMax.y) - { - const auto& iesCandela = m_assets[activeIx]; - const auto* profileCandela = iesCandela.getProfile(); - const auto& accessorCandela = profileCandela->getAccessor(); - const auto& resolutionCandela = accessorCandela.properties.optimalIESResolution; - - const float u = (mousePos.x - plotRectMin.x) / plotW; - const float v = (mousePos.y - plotRectMin.y) / plotH; - const float ndcX = u * 2.0f - 1.0f; - const float ndcY = v * 2.0f - 1.0f; - - float32_t4x4 viewProj = camera.getConcatenatedMatrix(); - const auto invViewProj = inverse(viewProj); - - const float32_t4 nearPoint(ndcX, ndcY, 0.0f, 1.0f); - const float32_t4 farPoint(ndcX, ndcY, 1.0f, 1.0f); - auto nearWorld = mul(invViewProj, nearPoint); - auto farWorld = mul(invViewProj, farPoint); - 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 farPos = float32_t3(farWorld); - float32_t3 direction = normalize(farPos - origin); - - float32_t3 hitPos(0.f); - bool hit = false; - const bool cubePlot = uiState.mode.sphere.hasFlags(hlsl::this_example::ies::ESM_CUBE); - if (cubePlot) - { - float tmin = -1.0e20f; - float tmax = 1.0e20f; - auto update = [&](float originAxis, float dirAxis) -> bool - { - const float eps = 1.0e-6f; - if (abs(dirAxis) < eps) - { - if (originAxis < -m_plotRadius || originAxis > m_plotRadius) - return false; - return true; - } - float t1 = (-m_plotRadius - originAxis) / dirAxis; - float t2 = (m_plotRadius - originAxis) / dirAxis; - if (t1 > t2) - { - float tmp = t1; - t1 = t2; - t2 = tmp; - } - tmin = hlsl::max(tmin, t1); - tmax = hlsl::min(tmax, t2); - return tmin <= tmax; - }; - - if (update(origin.x, direction.x) && update(origin.y, direction.y) && update(origin.z, direction.z)) - { - const float t = (tmax < 0.0f) ? tmin : tmax; - if (t >= 0.0f) - { - hitPos = origin + direction * t; - hit = true; - } - } - } - else - { - const float b = dot(origin, direction); - const float c = dot(origin, origin) - m_plotRadius * m_plotRadius; - const float disc = b * b - c; - if (disc >= 0.0f) - { - const float sqrtDisc = sqrt(disc); - const float tFar = -b + sqrtDisc; - const float tNear = -b - sqrtDisc; - const float t = (tFar < 0.0f) ? tNear : tFar; - if (t >= 0.0f) - { - hitPos = origin + direction * t; - hit = true; - } - } - } - - if (hit) - { - using octahedral_t = math::OctahedralTransform; - const float32_t3 dir = normalize(hitPos); - const uint32_t resX = resolutionCandela.x; - const uint32_t resY = resolutionCandela.y; - if (resX > 0u && resY > 0u) - { - const float32_t2 res(static_cast(resX), static_cast(resY)); - const float32_t2 halfMinusHalfPixel = float32_t2(0.5f, 0.5f) - float32_t2(0.5f, 0.5f) / res; - float32_t2 uv = octahedral_t::dirToUV(dir, halfMinusHalfPixel); - const bool interpolateCandela = uiState.mode.sphere.hasFlags(hlsl::this_example::ies::ESM_OCTAHEDRAL_UV_INTERPOLATE); - if (!interpolateCandela) - { - const auto pixel = floor(uv * res); - uv = (pixel + float32_t2(0.5f, 0.5f)) / res; - } - - const auto texture = CIESProfile::texture_t::create(accessorCandela.properties.maxCandelaValue, resolutionCandela); - const float normalized = texture.__call(accessorCandela, uv); - candelaValue = texture.info.maxValueRecip > 0.0f ? (normalized / texture.info.maxValueRecip) : 0.0f; - candelaValid = true; - } - } - } - } - - if (candelaValid && !uiState.cameraControlEnabled) - { - ImGui::BeginTooltip(); - ImGui::Text("candela: %.3f cd", candelaValue); - ImGui::EndTooltip(); - } -} - - - diff --git a/50.IESViewer/CMakeLists.txt b/50.IESViewer/CMakeLists.txt deleted file mode 100644 index ed6bd3668..000000000 --- a/50.IESViewer/CMakeLists.txt +++ /dev/null @@ -1,75 +0,0 @@ -if(NBL_BUILD_IMGUI) -set(SRCs - AppInit.cpp AppRender.cpp AppGPU.cpp AppUI.cpp AppEvent.cpp AppInputParser.cpp - App.hpp AppInputParser.hpp - IES.cpp IES.hpp - CSimpleIESRenderer.hpp - inputs.json -) - -set(LIBs - imtestengine - imguizmo - "${NBL_EXT_IMGUI_UI_LIB}" - Nabla::ext::FullScreenTriangle -) - -nbl_create_executable_project("${SRCs}" "" "" "${LIBs}") -target_link_libraries(${EXECUTABLE_NAME} PRIVATE nlohmann_json::nlohmann_json) -add_dependencies(${EXECUTABLE_NAME} argparse) -target_include_directories(${EXECUTABLE_NAME} PUBLIC $) - -set(OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/auto-gen") -set(DEPENDS - app_resources/common.hlsl - app_resources/false_color.hlsl - app_resources/imgui.opts.hlsl - app_resources/ies.unified.hlsl - app_resources/imgui.unified.hlsl -) -target_sources(${EXECUTABLE_NAME} PRIVATE ${DEPENDS}) -set_source_files_properties(${DEPENDS} PROPERTIES HEADER_FILE_ONLY ON) - -set(SM 6_8) -set(JSON [=[ -[ - { - "INPUT": "app_resources/ies.unified.hlsl", - "KEY": "ies.unified" - }, - { - "INPUT": "app_resources/imgui.unified.hlsl", - "KEY": "imgui.unified" - } -] -]=]) -string(CONFIGURE "${JSON}" JSON) - -set(COMPILE_OPTIONS - -isystem "${NBL_ROOT_PATH}/include" # a workaround due to imgui ext headers which are not part of Nabla builtin archive - -I "${CMAKE_CURRENT_SOURCE_DIR}" - -O3 - -T lib_${SM} -) - -NBL_CREATE_NSC_COMPILE_RULES( - TARGET ${EXECUTABLE_NAME}SPIRV - LINK_TO ${EXECUTABLE_NAME} - DEPENDS ${DEPENDS} - 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} -) -endif() diff --git a/50.IESViewer/CSimpleIESRenderer.hpp b/50.IESViewer/CSimpleIESRenderer.hpp deleted file mode 100644 index d5614aa7a..000000000 --- a/50.IESViewer/CSimpleIESRenderer.hpp +++ /dev/null @@ -1,427 +0,0 @@ -#ifndef _NBL_EXAMPLES_C_SIMPLE_IES_RENDERER_H_INCLUDED_ -#define _NBL_EXAMPLES_C_SIMPLE_IES_RENDERER_H_INCLUDED_ - -// NOTE: this is CSimpleDebugRenderer with dirty updates, not meant to be used outside the example - -#include "nbl/examples/examples.hpp" -#include "nbl/builtin/hlsl/math/linalg/fast_affine.hlsl" -#include "app_resources/common.hlsl" -#include - -namespace nbl::examples -{ - -class CSimpleIESRenderer final : public core::IReferenceCounted -{ -#define EXPOSE_NABLA_NAMESPACES \ - using namespace nbl::core; \ - using namespace nbl::system; \ - using namespace nbl::asset; \ - using namespace nbl::video - - public: - // - constexpr static inline uint16_t VertexAttrubUTBDescBinding = 0; - // - struct SViewParams - { - inline SViewParams(const hlsl::float32_t3x4& _view, const hlsl::float32_t4x4& _viewProj) - { - view = _view; - viewProj = _viewProj; - using namespace nbl::hlsl; - normal = transpose(inverse(float32_t3x3(view))); - } - - inline auto computeForInstance(hlsl::float32_t3x4 world) const - { - using namespace nbl::hlsl; - hlsl::this_example::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)); - retval.normal = float32_t3x3(transpose(inverse(sub3x3))); - return retval; - } - - hlsl::float32_t3x4 view; - hlsl::float32_t4x4 viewProj; - hlsl::float32_t3x3 normal; - }; - - struct SIESParams - { - hlsl::float32_t radius = 1.f; - IGPUDescriptorSet* ds = nullptr; - uint16_t texID = 0u; - uint16_t mode = hlsl::this_example::ies::ESM_NONE; - bool wireframe = false; - }; - // - struct SPackedGeometry - { - core::smart_refctd_ptr pipeline = {}; - asset::SBufferBinding indexBuffer = {}; - uint32_t elementCount = 0; - // indices into the descriptor set - constexpr static inline auto MissingView = hlsl::this_example::ies::SpherePC::DescriptorCount; - uint16_t positionView = MissingView; - uint16_t normalView = MissingView; - asset::E_INDEX_TYPE indexType = asset::EIT_UNKNOWN; - }; - // - struct SInstance - { - using SPushConstants = hlsl::this_example::ies::SpherePC; - inline SPushConstants computePushConstants(const SViewParams& viewParams, const SIESParams& iesParams) const - { - using namespace hlsl; - return { - .matrices = viewParams.computeForInstance(world), - .positionView = packedGeo->positionView, - .normalView = packedGeo->normalView, - .radius = iesParams.radius, - .mode = iesParams.mode, - .texIx = iesParams.texID - }; - } - - hlsl::float32_t3x4 world; - const SPackedGeometry* packedGeo; - }; - - // - constexpr static inline auto DefaultPolygonGeometryPatch = []()->video::CAssetConverter::patch_t - { - // we want to use the vertex data through UTBs - using usage_f = video::IGPUBuffer::E_USAGE_FLAGS; - video::CAssetConverter::patch_t patch = {}; - patch.positionBufferUsages = usage_f::EUF_UNIFORM_TEXEL_BUFFER_BIT; - patch.indexBufferUsages = usage_f::EUF_INDEX_BUFFER_BIT; - patch.otherBufferUsages = usage_f::EUF_UNIFORM_TEXEL_BUFFER_BIT; - return patch; - }(); - - // - static inline core::smart_refctd_ptr create(core::smart_refctd_ptr precompiled, core::smart_refctd_ptr iesDSLayout, video::IGPURenderpass* renderpass, const uint32_t subpassIX) - { - EXPOSE_NABLA_NAMESPACES; - - if (!renderpass) - return nullptr; - auto device = const_cast(renderpass->getOriginDevice()); - auto logger = device->getLogger(); - - if (not precompiled) - return nullptr; - smart_refctd_ptr shader = precompiled; - - SInitParams init; - - // create descriptor set - { - // create Descriptor Set Layout - smart_refctd_ptr dsLayout; - { - using binding_flags_t = IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS; - const IGPUDescriptorSetLayout::SBinding bindings[] = - { - { - .binding = VertexAttrubUTBDescBinding, - .type = IDescriptor::E_TYPE::ET_UNIFORM_TEXEL_BUFFER, - // need this trifecta of flags for `SubAllocatedDescriptorSet` to accept the binding as suballocatable - .createFlags = binding_flags_t::ECF_UPDATE_AFTER_BIND_BIT|binding_flags_t::ECF_UPDATE_UNUSED_WHILE_PENDING_BIT |binding_flags_t::ECF_PARTIALLY_BOUND_BIT, - .stageFlags = IShader::E_SHADER_STAGE::ESS_VERTEX|IShader::E_SHADER_STAGE::ESS_FRAGMENT, - .count = SPackedGeometry::MissingView - } - }; - dsLayout = device->createDescriptorSetLayout(bindings); - if (!dsLayout) - { - logger->log("Could not create descriptor set layout!",ILogger::ELL_ERROR); - return nullptr; - } - } - - // create Descriptor Set - auto pool = device->createDescriptorPoolForDSLayouts(IDescriptorPool::ECF_UPDATE_AFTER_BIND_BIT,{&dsLayout.get(),1}); - auto ds = pool->createDescriptorSet(std::move(dsLayout)); - if (!ds) - { - logger->log("Could not descriptor set!",ILogger::ELL_ERROR); - return nullptr; - } - init.subAllocDS = make_smart_refctd_ptr(std::move(ds)); - } - - // create pipeline layout - const SPushConstantRange ranges[] = {{ - .stageFlags = hlsl::ShaderStage::ESS_VERTEX|hlsl::ShaderStage::ESS_FRAGMENT, - .offset = offsetof(hlsl::this_example::ies::PushConstants, sphere), - .size = sizeof(SInstance::SPushConstants), - }}; - init.layout = device->createPipelineLayout(ranges, smart_refctd_ptr(iesDSLayout), smart_refctd_ptr(init.subAllocDS->getDescriptorSet()->getLayout())); - - // create pipelines - using pipeline_e = SInitParams::PipelineType; - { - IGPUGraphicsPipeline::SCreationParams params[pipeline_e::Count] = {}; - params[pipeline_e::SphereTriangleStrip].vertexShader = { .shader = shader.get(),.entryPoint = "SphereVS" }; - params[pipeline_e::SphereTriangleStrip].fragmentShader = { .shader = shader.get(),.entryPoint = "SpherePS" }; - params[pipeline_e::SphereTriangleStripWire].vertexShader = { .shader = shader.get(),.entryPoint = "SphereVS" }; - params[pipeline_e::SphereTriangleStripWire].fragmentShader = { .shader = shader.get(),.entryPoint = "SpherePS" }; - for (auto i=0; i(i); - switch (type) - { - case pipeline_e::SphereTriangleStrip: - primitiveAssembly.primitiveType = E_PRIMITIVE_TOPOLOGY::EPT_TRIANGLE_STRIP; - break; - case pipeline_e::SphereTriangleStripWire: - primitiveAssembly.primitiveType = E_PRIMITIVE_TOPOLOGY::EPT_TRIANGLE_STRIP; - rasterization.polygonMode = EPM_LINE; - break; - default: - assert(false); - break; - } - primitiveAssembly.primitiveRestartEnable = false; - rasterization.faceCullingMode = EFCM_NONE; - rasterization.depthWriteEnable = true; - rasterization.depthCompareOp = ECO_GREATER; - params[i].cached.subpassIx = subpassIX; - params[i].renderpass = renderpass; - } - if (!device->createGraphicsPipelines(nullptr,params,init.pipelines)) - { - logger->log("Could not create Graphics Pipelines!",ILogger::ELL_ERROR); - return nullptr; - } - } - - return smart_refctd_ptr(new CSimpleIESRenderer(std::move(init)),dont_grab); - } - - // - static inline core::smart_refctd_ptr create(core::smart_refctd_ptr precompiled, core::smart_refctd_ptr iesDSLayout, video::IGPURenderpass* renderpass, const uint32_t subpassIX, const std::span geometries) - { - auto retval = create(precompiled, iesDSLayout, renderpass, subpassIX); - if (retval) - retval->addGeometries(geometries); - return retval; - } - - // - struct SInitParams - { - enum PipelineType : uint8_t - { - SphereTriangleStrip, - SphereTriangleStripWire, - Count - }; - - core::smart_refctd_ptr subAllocDS; - core::smart_refctd_ptr layout; - core::smart_refctd_ptr pipelines[PipelineType::Count]; - }; - inline const SInitParams& getInitParams() const {return m_params;} - - // - inline bool addGeometries(const std::span geometries) - { - EXPOSE_NABLA_NAMESPACES; - if (geometries.empty()) - return false; - auto device = const_cast(m_params.layout->getOriginDevice()); - - std::vector writes; - std::vector infos; - bool anyFailed = false; - auto allocateUTB = [&](const IGeometry::SDataView& view)->decltype(SubAllocatedDescriptorSet::invalid_value) - { - if (!view) - return SPackedGeometry::MissingView; - auto index = SubAllocatedDescriptorSet::invalid_value; - if (m_params.subAllocDS->multi_allocate(VertexAttrubUTBDescBinding,1,&index)!=0) - { - anyFailed = true; - return SPackedGeometry::MissingView; - } - const auto infosOffset = infos.size(); - infos.emplace_back().desc = device->createBufferView(view.src,view.composed.format); - writes.emplace_back() = { - .dstSet = m_params.subAllocDS->getDescriptorSet(), - .binding = VertexAttrubUTBDescBinding, - .arrayElement = index, - .count = 1, - .info = reinterpret_cast(infosOffset) - }; - return index; - }; - if (anyFailed) - device->getLogger()->log("Failed to allocate a UTB for some geometries, probably ran out of space in Descriptor Set!",system::ILogger::ELL_ERROR); - - auto sizeToSet = m_geoms.size(); - auto resetGeoms = core::makeRAIIExiter([&]()->void - { - for (auto& write : writes) - immediateDealloc(write.arrayElement); - m_geoms.resize(sizeToSet); - } - ); - for (const auto geom : geometries) - { - // could also check device origin on all buffers - if (!geom->valid()) - return false; - auto& out = m_geoms.emplace_back(); - using pipeline_e = SInitParams::PipelineType; - switch (geom->getIndexingCallback()->knownTopology()) - { - case E_PRIMITIVE_TOPOLOGY::EPT_TRIANGLE_STRIP: - out.pipeline = m_params.pipelines[pipeline_e::SphereTriangleStrip]; - break; - default: - assert(false); - break; - } - if (const auto& view=geom->getIndexView(); view) - { - out.indexBuffer.offset = view.src.offset; - out.indexBuffer.buffer = view.src.buffer; - switch (view.composed.format) - { - case E_FORMAT::EF_R16_UINT: - out.indexType = EIT_16BIT; - break; - case E_FORMAT::EF_R32_UINT: - out.indexType = EIT_32BIT; - break; - default: - return false; - } - } - out.elementCount = geom->getVertexReferenceCount(); - out.positionView = allocateUTB(geom->getPositionView()); - out.normalView = allocateUTB(geom->getNormalView()); - } - - // no geometry - if (infos.empty()) - return false; - - // unbase our pointers - for (auto& write : writes) - write.info = infos.data()+reinterpret_cast(write.info); - if (!device->updateDescriptorSets(writes,{})) - return false; - - // retain - writes.clear(); - sizeToSet = m_geoms.size(); - return true; - } - - // - inline void removeGeometry(const uint32_t ix, const video::ISemaphore::SWaitInfo& info) - { - EXPOSE_NABLA_NAMESPACES; - if (ix>=m_geoms.size()) - return; - - std::vector deferredFree; - deferredFree.reserve(3); - auto deallocate = [&](SubAllocatedDescriptorSet::value_type index)->void - { - if (index>=SPackedGeometry::MissingView) - return; - if (info.semaphore) - deferredFree.push_back(index); - else - immediateDealloc(index); - }; - auto geo = m_geoms.begin() + ix; - deallocate(geo->positionView); - deallocate(geo->normalView); - m_geoms.erase(geo); - - if (deferredFree.empty()) - return; - m_params.subAllocDS->multi_deallocate(VertexAttrubUTBDescBinding,deferredFree.size(),deferredFree.data(),info); - } - - // - inline void clearGeometries(const video::ISemaphore::SWaitInfo& info) - { - // back to front to avoid O(n^2) resize - while (!m_geoms.empty()) - removeGeometry(m_geoms.size()-1,info); - } - - // - inline const auto& getGeometries() const {return m_geoms;} - inline auto& getGeometry(const uint32_t ix) {return m_geoms[ix];} - - // - inline void render(video::IGPUCommandBuffer* cmdbuf, const SViewParams& viewParams, const SIESParams& iesParams) const - { - EXPOSE_NABLA_NAMESPACES; - - cmdbuf->beginDebugMarker("CSimpleIESRenderer::render"); - - const auto* layout = m_params.layout.get(); - - IGPUDescriptorSet* descriptors[] = { iesParams.ds, m_params.subAllocDS->getDescriptorSet() }; - cmdbuf->bindDescriptorSets(E_PIPELINE_BIND_POINT::EPBP_GRAPHICS,layout,0,2, descriptors); - - for (const auto& instance : m_instances) - { - const auto* geo = instance.packedGeo; - auto pipeline = geo->pipeline; - if (iesParams.wireframe) - pipeline = m_params.pipelines[SInitParams::PipelineType::SphereTriangleStripWire]; - cmdbuf->bindGraphicsPipeline(pipeline.get()); - const auto pc = instance.computePushConstants(viewParams, iesParams); - cmdbuf->pushConstants(layout,hlsl::ShaderStage::ESS_VERTEX|hlsl::ShaderStage::ESS_FRAGMENT,offsetof(hlsl::this_example::ies::PushConstants, sphere),sizeof(pc),&pc); - if (geo->indexBuffer) - { - cmdbuf->bindIndexBuffer(geo->indexBuffer,geo->indexType); - cmdbuf->drawIndexed(geo->elementCount,1,0,0,0); - } - else - cmdbuf->draw(geo->elementCount,1,0,0); - } - cmdbuf->endDebugMarker(); - } - - std::vector m_instances; - - protected: - inline CSimpleIESRenderer(SInitParams&& _params) : m_params(std::move(_params)) {} - inline ~CSimpleIESRenderer() - { - // clean shutdown, can also make SubAllocatedDescriptorSet resillient against that, and issue `device->waitIdle` if not everything is freed - const_cast(m_params.layout->getOriginDevice())->waitIdle(); - clearGeometries({}); - } - - inline void immediateDealloc(video::SubAllocatedDescriptorSet::value_type index) - { - video::IGPUDescriptorSet::SDropDescriptorSet dummy[1]; - m_params.subAllocDS->multi_deallocate(dummy,VertexAttrubUTBDescBinding,1,&index); - } - - SInitParams m_params; - std::vector m_geoms; -#undef EXPOSE_NABLA_NAMESPACES -}; - -} -#endif // _NBL_EXAMPLES_C_SIMPLE_IES_RENDERER_H_INCLUDED_ diff --git a/50.IESViewer/IES.cpp b/50.IESViewer/IES.cpp deleted file mode 100644 index 3c1df172c..000000000 --- a/50.IESViewer/IES.cpp +++ /dev/null @@ -1,27 +0,0 @@ -// 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 - -#include "IES.hpp" - -const asset::CIESProfile* IES::getProfile() const -{ - auto* meta = bundle.getMetadata(); - if (meta) - return &meta->selfCast()->profile; - - return nullptr; -} - -video::IGPUImage* IES::getActiveImage(E_MODE mode) const -{ - switch (mode) - { - case EM_OCTAHEDRAL_MAP: - return views.candelaOctahedralMap->getCreationParameters().image.get(); - - case EM_CDC: - default: - return nullptr; - } -} diff --git a/50.IESViewer/IES.hpp b/50.IESViewer/IES.hpp deleted file mode 100644 index da9f98b3f..000000000 --- a/50.IESViewer/IES.hpp +++ /dev/null @@ -1,193 +0,0 @@ -#ifndef _THIS_EXAMPLE_IES_HPP_ -#define _THIS_EXAMPLE_IES_HPP_ - -// 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 - -#include "nbl/examples/examples.hpp" -#include "nbl/system/to_string.h" - -NBL_EXPOSE_NAMESPACES - -struct IES -{ - enum E_MODE : uint32_t - { - EM_CDC, //! Candlepower Distribution Curve - EM_OCTAHEDRAL_MAP, //! Candela Octahedral Map - - EM_SIZE - }; - - struct - { - smart_refctd_ptr candelaOctahedralMap = nullptr; - } views; - - struct - { - smart_refctd_ptr vAngles = nullptr, hAngles = nullptr, data = nullptr; // allocation per ies - SBufferBinding textureInfo; // shared allocation for all ies - } buffers; - - SAssetBundle bundle; - std::string key; - - float zDegree = 0.f; - - const asset::CIESProfile* getProfile() const; - video::IGPUImage* getActiveImage(E_MODE mode) const; - - template - requires(newLayout == IImage::LAYOUT::GENERAL or newLayout == IImage::LAYOUT::READ_ONLY_OPTIMAL) - static inline bool barrier(IGPUCommandBuffer* const cb, const std::span images) - { - if (images.empty()) - return false; - - if (not cb) - return false; - - using image_memory_barrier_t = IGPUCommandBuffer::SImageMemoryBarrier; - const IGPUImage::SSubresourceRange range = - { - .aspectMask = IGPUImage::E_ASPECT_FLAGS::EAF_COLOR_BIT, - .baseMipLevel = 0u, - .levelCount = 1u, - .baseArrayLayer = 0u, - .layerCount = 1u - }; - - std::vector imageBarriers(images.size()); - - for (uint32_t i = 0; i < imageBarriers.size(); ++i) - { - auto& it = imageBarriers[i] = - { - .barrier = {.dep = {}}, - .image = images[i], - .subresourceRange = range, - .oldLayout = IImage::LAYOUT::UNDEFINED, - .newLayout = newLayout - }; - - if constexpr (newLayout == IImage::LAYOUT::GENERAL) - { - // READ_ONLY_OPTIMAL -> GENERAL, RW - it.barrier.dep.srcStageMask = PIPELINE_STAGE_FLAGS::ALL_GRAPHICS_BITS; - it.barrier.dep.srcAccessMask = ACCESS_FLAGS::SHADER_READ_BITS; - it.barrier.dep.dstStageMask = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT; - it.barrier.dep.dstAccessMask = ACCESS_FLAGS::STORAGE_WRITE_BIT; - it.oldLayout = IImage::LAYOUT::READ_ONLY_OPTIMAL; - } - else if (newLayout == IImage::LAYOUT::READ_ONLY_OPTIMAL) - { - // GENERAL -> READ_ONLY_OPTIMAL, RO - it.barrier.dep.srcStageMask = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT; - it.barrier.dep.srcAccessMask = ACCESS_FLAGS::STORAGE_WRITE_BIT; - it.barrier.dep.dstStageMask = PIPELINE_STAGE_FLAGS::ALL_GRAPHICS_BITS; - it.barrier.dep.dstAccessMask = ACCESS_FLAGS::SHADER_READ_BITS; - it.oldLayout = IImage::LAYOUT::GENERAL; - } - - if constexpr (undefined) - it.oldLayout = IImage::LAYOUT::UNDEFINED; // transition for init - } - - return cb->pipelineBarrier(E_DEPENDENCY_FLAGS::EDF_NONE, { .memBarriers = {}, .bufBarriers = {}, .imgBarriers = imageBarriers }); - } - - template - requires(newLayout == IImage::LAYOUT::GENERAL or newLayout == IImage::LAYOUT::READ_ONLY_OPTIMAL) - static inline bool barrier(IGPUCommandBuffer* const cb, video::IGPUImage* image) - { - if (not image) - return false; - - auto in = std::to_array({ image }); - return barrier(cb, in); - } -}; - -namespace nbl::system::impl -{ -template<> -struct to_string_helper -{ - static std::string __call(const IES::E_MODE mode) - { - switch (mode) - { - case IES::EM_CDC: - return "Candlepower Distribution Curve"; - case IES::EM_OCTAHEDRAL_MAP: - return "Candela Octahedral Map"; - default: - return "ERROR (mode)"; - } - } -}; - -template<> -struct to_string_helper -{ - static std::string __call(const nbl::asset::CIESProfile::properties_t::LuminairePlanesSymmetry symmetry) - { - switch (symmetry) - { - case nbl::asset::CIESProfile::properties_t::ISOTROPIC: - return "ISOTROPIC"; - case nbl::asset::CIESProfile::properties_t::QUAD_SYMETRIC: - return "QUAD_SYMETRIC"; - case nbl::asset::CIESProfile::properties_t::HALF_SYMETRIC: - return "HALF_SYMETRIC"; - case nbl::asset::CIESProfile::properties_t::OTHER_HALF_SYMMETRIC: - return "OTHER_HALF_SYMMETRIC"; - case nbl::asset::CIESProfile::properties_t::NO_LATERAL_SYMMET: - return "NO_LATERAL_SYMMET"; - default: - return "ERROR (symmetry)"; - } - } -}; - -template<> -struct to_string_helper -{ - static std::string __call(const nbl::asset::CIESProfile::properties_t::PhotometricType type) - { - switch (type) - { - case nbl::asset::CIESProfile::properties_t::TYPE_C: - return "TYPE_C"; - case nbl::asset::CIESProfile::properties_t::TYPE_B: - return "TYPE_B"; - case nbl::asset::CIESProfile::properties_t::TYPE_A: - return "TYPE_A"; - case nbl::asset::CIESProfile::properties_t::TYPE_NONE: - default: - return "TYPE_NONE"; - } - } -}; - -template<> -struct to_string_helper -{ - static std::string __call(const nbl::asset::CIESProfile::properties_t::Version version) - { - switch (version) - { - case nbl::asset::CIESProfile::properties_t::V_1995: - return "V_1995"; - case nbl::asset::CIESProfile::properties_t::V_2002: - return "V_2002"; - default: - return "V_UNKNOWN"; - } - } -}; -} - -#endif // _THIS_EXAMPLE_IES_HPP_ diff --git a/50.IESViewer/app_resources/common.hlsl b/50.IESViewer/app_resources/common.hlsl deleted file mode 100644 index 54a95b9d0..000000000 --- a/50.IESViewer/app_resources/common.hlsl +++ /dev/null @@ -1,73 +0,0 @@ -#ifndef _THIS_EXAMPLE_COMMON_HLSL_INCLUDED_ -#define _THIS_EXAMPLE_COMMON_HLSL_INCLUDED_ - -#include "nbl/builtin/hlsl/cpp_compat.hlsl" -#include "nbl/builtin/hlsl/ies/profile.hlsl" - -namespace nbl -{ -namespace hlsl -{ -namespace this_example -{ - -NBL_CONSTEXPR_INLINE_NSPC_SCOPE_VAR float QuantErrorAdmissible = 1.0f / 1024.0f; -NBL_CONSTEXPR_INLINE_NSPC_SCOPE_VAR uint32_t WorkgroupSize = 256u; -NBL_CONSTEXPR_INLINE_NSPC_SCOPE_VAR uint32_t WorkgroupDimension = 16u; -NBL_CONSTEXPR_INLINE_NSPC_SCOPE_VAR uint32_t MaxIesImages = 6969u; - -struct SInstanceMatrices -{ - float32_t4x4 worldViewProj; - float32_t3x3 normal; -}; - -namespace ies -{ - -struct CdcPC -{ - uint64_t hAnglesBDA; - uint64_t vAnglesBDA; - uint64_t dataBDA; - uint64_t txtInfoBDA; - uint32_t mode : 8; - uint32_t texIx : 24; - uint32_t hAnglesCount; - uint32_t vAnglesCount; - float32_t zAngleDegreeRotation; - nbl::hlsl::ies::ProfileProperties properties; - - float32_t pad; -}; - -enum E_SPHERE_MODE : uint16_t -{ - ESM_NONE = 0, - ESM_OCTAHEDRAL_UV_INTERPOLATE = 1u << 0, - ESM_FALSE_COLOR = 1u << 1, - ESM_CUBE = 1u << 2 -}; - -struct SpherePC -{ - NBL_CONSTEXPR_STATIC_INLINE uint32_t DescriptorCount = (0x1<<16)-1; - this_example::SInstanceMatrices matrices; - uint32_t positionView : 16; - uint32_t normalView : 16; - float32_t radius; - uint32_t mode : 8; - uint32_t texIx : 24; -}; - -struct PushConstants -{ - CdcPC cdc; - SpherePC sphere; -}; - -} -} -} -} -#endif // _THIS_EXAMPLE_COMMON_HLSL_INCLUDED_ diff --git a/50.IESViewer/app_resources/false_color.hlsl b/50.IESViewer/app_resources/false_color.hlsl deleted file mode 100644 index ffc830ec2..000000000 --- a/50.IESViewer/app_resources/false_color.hlsl +++ /dev/null @@ -1,74 +0,0 @@ -#ifndef _THIS_EXAMPLE_FALSE_COLOR_HLSL_INCLUDED_ -#define _THIS_EXAMPLE_FALSE_COLOR_HLSL_INCLUDED_ - -#include "nbl/builtin/hlsl/tgmath.hlsl" - -namespace nbl -{ -namespace hlsl -{ -namespace this_example -{ -namespace ies -{ - -NBL_CONSTEXPR_STATIC_INLINE uint32_t FalseColorStopCount = 6u; - -inline float32_t falseColorStop(uint32_t idx) -{ - switch (idx) - { - case 0u: return 0.0f; - case 1u: return 0.15f; - case 2u: return 0.35f; - case 3u: return 0.55f; - case 4u: return 0.75f; - default: return 1.0f; - } -} - -inline float32_t3 falseColor(float32_t v) -{ - v = nbl::hlsl::clamp(v, float32_t(0.0f), float32_t(1.0f)); - v = nbl::hlsl::pow(v, float32_t(0.8f)); - - const float32_t3 c0 = float32_t3(0.0f, 0.0f, 0.0f); - const float32_t3 c1 = float32_t3(0.0f, 0.0f, 0.35f); - const float32_t3 c2 = float32_t3(0.10f, 0.20f, 0.90f); - const float32_t3 c3 = float32_t3(0.70f, 0.05f, 0.80f); - const float32_t3 c4 = float32_t3(1.00f, 0.30f, 1.00f); - const float32_t3 c5 = float32_t3(1.00f, 1.00f, 1.00f); - - if (v < 0.15f) - { - const float32_t t = v / 0.15f; - return c0 + (c1 - c0) * t; - } - else if (v < 0.35f) - { - const float32_t t = (v - 0.15f) / (0.35f - 0.15f); - return c1 + (c2 - c1) * t; - } - else if (v < 0.55f) - { - const float32_t t = (v - 0.35f) / (0.55f - 0.35f); - return c2 + (c3 - c2) * t; - } - else if (v < 0.75f) - { - const float32_t t = (v - 0.55f) / (0.75f - 0.55f); - return c3 + (c4 - c3) * t; - } - else - { - const float32_t t = (v - 0.75f) / (1.0f - 0.75f); - return c4 + (c5 - c4) * t; - } -} - -} -} -} -} - -#endif diff --git a/50.IESViewer/app_resources/ies.unified.hlsl b/50.IESViewer/app_resources/ies.unified.hlsl deleted file mode 100644 index fb89b2ed5..000000000 --- a/50.IESViewer/app_resources/ies.unified.hlsl +++ /dev/null @@ -1,165 +0,0 @@ -#include "common.hlsl" -#include "nbl/builtin/hlsl/bda/__ptr.hlsl" -#include "nbl/builtin/hlsl/math/linalg/fast_affine.hlsl" -#include "nbl/builtin/hlsl/ext/FullScreenTriangle/SVertexAttributes.hlsl" -#include "false_color.hlsl" - -using namespace nbl::hlsl; -using namespace nbl::hlsl::this_example; -using namespace nbl::hlsl::this_example::ies; -using namespace nbl::hlsl::ext::FullScreenTriangle; - -[[vk::binding(0, 0)]] Texture2D inIESCandelaImage[MaxIesImages]; -[[vk::binding(0 + 10, 0)]] RWTexture2D outIESCandelaImage[MaxIesImages]; -[[vk::binding(0 + 100, 0)]] SamplerState generalSampler; - -[[vk::binding(0, 1)]] Buffer utbs[SpherePC::DescriptorCount]; -[[vk::push_constant]] PushConstants pc; - -struct Accessor -{ - using angle_t = float32_t; - using candela_t = float32_t; - - candela_t value(const uint32_t2 ij) { return (nbl::hlsl::bda::__ptr::create(pc.cdc.dataBDA) + pc.cdc.vAnglesCount * ij.x + ij.y).deref().load(); } - angle_t vAngle(const uint32_t idx) { return (nbl::hlsl::bda::__ptr::create(pc.cdc.vAnglesBDA) + idx).deref().load(); } - angle_t hAngle(const uint32_t idx) { return (nbl::hlsl::bda::__ptr::create(pc.cdc.hAnglesBDA) + idx).deref().load(); } - uint32_t vAnglesCount() { return pc.cdc.vAnglesCount; } - uint32_t hAnglesCount() { return pc.cdc.hAnglesCount; } - - nbl::hlsl::ies::ProfileProperties getProperties() { return pc.cdc.properties; } -}; - -#include "nbl/builtin/hlsl/ies/texture.hlsl" - -struct SInterpolants -{ - float32_t4 ndc : SV_Position; - float32_t3 latDir : COLOR1; - float32_t2 uv : TEXCOORD0; -}; - -using octahedral_t = math::OctahedralTransform; -using texture_t = nbl::hlsl::ies::SProceduralTexture; - -[shader("vertex")] -SInterpolants SphereVS(uint32_t vIx : SV_VertexID) -{ - uint32_t2 res; - inIESCandelaImage[pc.sphere.texIx].GetDimensions(res.x, res.y); - - const float32_t2 resF = float32_t2(res); - const float32_t2 uv = (float32_t2(vIx % res.x, vIx / res.x) + float32_t2(0.5f, 0.5f)) / resF; - const float32_t2 halfMinusHalfPixel = float32_t2(0.5f, 0.5f) - float32_t2(0.5f, 0.5f) / resF; - - const float32_t3 dir = octahedral_t::uvToDir(uv, halfMinusHalfPixel); - float32_t3 pos = dir; - const bool useCube = (pc.sphere.mode & ESM_CUBE) != 0; - if (useCube) - { - const float32_t3 ad = abs(dir); - const float32_t maxAxis = max(ad.x, max(ad.y, ad.z)); - pos = dir / maxAxis; - } - pos *= pc.sphere.radius; - - SInterpolants o; - o.ndc = math::linalg::promoted_mul(pc.sphere.matrices.worldViewProj, pos); - o.latDir = dir; - o.uv = uv; - - return o; -} - -[shader("pixel")] -float32_t4 SpherePS(SInterpolants input) : SV_Target0 -{ - uint32_t2 res; - inIESCandelaImage[pc.sphere.texIx].GetDimensions(res.x, res.y); - float32_t2 uv = input.uv; - - const bool dontInterpolateUV = (pc.sphere.mode & ESM_OCTAHEDRAL_UV_INTERPOLATE) == 0; - if (dontInterpolateUV) - { - float32_t2 pixel = floor(uv * float32_t2(res)); - uv = (pixel + float32_t2(0.5f, 0.5f)) / float32_t2(res); - } - - float32_t I = inIESCandelaImage[pc.sphere.texIx].SampleLevel(generalSampler, uv, 0.0f).r; - const bool useFalseColor = (pc.sphere.mode & ESM_FALSE_COLOR) != 0; - float32_t3 col = useFalseColor ? falseColor(I) : float32_t3(I, I, I); - - return float32_t4(col, 1.0f); -} - -[numthreads(WorkgroupDimension, WorkgroupDimension, 1)] -[shader("compute")] -void CdcCS(uint32_t3 ID : SV_DispatchThreadID) -{ - uint32_t2 destinationSize; - outIESCandelaImage[pc.cdc.texIx].GetDimensions(destinationSize.x, destinationSize.y); - const uint32_t2 pixelCoordinates = uint32_t2(glsl::gl_GlobalInvocationID().x, glsl::gl_GlobalInvocationID().y); - if (all(pixelCoordinates < destinationSize)) - { - Accessor accessor; - texture_t txt; - nbl::hlsl::ies::IESTextureInfo info = (nbl::hlsl::bda::__ptr::create(pc.cdc.txtInfoBDA) + pc.cdc.texIx).deref().load(); - txt.info = info; - outIESCandelaImage[pc.cdc.texIx][pixelCoordinates] = txt.__call(accessor, pixelCoordinates); - } -} - -float32_t plot(float32_t cand, float32_t pct, float32_t bold) -{ - return smoothstep(pct - 0.005f * bold, pct, cand) - smoothstep(pct, pct + 0.005f * bold, cand); -} - -// vertical cut of IES (i.e. cut by plane x = 0) -float32_t f(float32_t2 uv) -{ - float32_t3 dir = normalize(float32_t3(uv.x, 0.001f, uv.y)); - if (pc.cdc.zAngleDegreeRotation != 0.f) - { - float32_t rad = radians(pc.cdc.zAngleDegreeRotation); - float32_t s = sin(rad); - float32_t c = cos(rad); - - dir = float32_t3( - c * dir.x - s * dir.y, - s * dir.x + c * dir.y, - dir.z - ); - } - - uint32_t2 res; - inIESCandelaImage[pc.cdc.texIx].GetDimensions(res.x, res.y); - float32_t2 halfMinusHalfPixel = 0.5f - 0.5f / float32_t2(res); - float32_t2 uvOcta = octahedral_t::dirToUV(dir, halfMinusHalfPixel); - - return inIESCandelaImage[pc.cdc.texIx].SampleLevel(generalSampler, uvOcta, 0u).x; -} - -[shader("pixel")] -float32_t4 CdcPS(SVertexAttributes input) : SV_Target0 -{ - switch (pc.cdc.mode) - { - case 0: - { - float32_t2 ndc = input.uv * 2.f - 1.f; - float32_t dist = length(ndc) * 1.015625f; - float32_t p = plot(dist, 1.0f, 0.75f); - float32_t3 col = float32_t3(p, p, p); - - float32_t normalizedStrength = f(ndc); - if (dist < normalizedStrength) - col += float32_t3(1.0f, 0.0f, 0.0f); - - return float32_t4(col, 1.0f); - } - case 1: - return float32_t4(inIESCandelaImage[pc.cdc.texIx].Sample(generalSampler, input.uv).x, 0.f, 0.f, 1.f); - default: - return float32_t4(0.f, 0.f, 0.f, 0.f); - } -} diff --git a/50.IESViewer/app_resources/imgui.opts.hlsl b/50.IESViewer/app_resources/imgui.opts.hlsl deleted file mode 100644 index fc5cf0fb0..000000000 --- a/50.IESViewer/app_resources/imgui.opts.hlsl +++ /dev/null @@ -1,16 +0,0 @@ -#ifndef _THIS_EXAMPLE_IMGUI_OPTS_HLSL_INCLUDED_ -#define _THIS_EXAMPLE_IMGUI_OPTS_HLSL_INCLUDED_ - -// 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 - -#define NBL_TEXTURES_BINDING_IX 0u -#define NBL_SAMPLER_STATES_BINDING_IX 1u -#define NBL_TEXTURES_SET_IX 0u -#define NBL_SAMPLER_STATES_SET_IX 0u -#define NBL_TEXTURES_COUNT 10u -#define NBL_SAMPLERS_COUNT 2u - -#endif // _THIS_EXAMPLE_IMGUI_OPTS_HLSL_INCLUDED_ - diff --git a/50.IESViewer/app_resources/imgui.unified.hlsl b/50.IESViewer/app_resources/imgui.unified.hlsl deleted file mode 100644 index 03e5624b0..000000000 --- a/50.IESViewer/app_resources/imgui.unified.hlsl +++ /dev/null @@ -1,7 +0,0 @@ -// 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 - -#include "imgui.opts.hlsl" // bindings info -#include "nbl/ext/ImGui/builtin/hlsl/fragment.hlsl" // pixel entry point -#include "nbl/ext/ImGui/builtin/hlsl/vertex.hlsl" // vertex entry point diff --git a/50.IESViewer/inputs.json b/50.IESViewer/inputs.json deleted file mode 100644 index fbb833112..000000000 --- a/50.IESViewer/inputs.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "directories": [ - "mitsuba/ies/packages/leomoon-dot-com_ies-lights-pack/ies-lights-pack" - ], - "files": [ - "mitsuba/ies/ISOTROPIC/007cfb11e343e2f42e3b476be4ab684e.ies", - "mitsuba/ies/ANIISOTROPIC/QUAD_SYMMETRY/0275171fb664c1b3f024d1e442a68d22.ies", - "mitsuba/ies/ANIISOTROPIC/HALF_SYMMETRY/1392a1ba55b67d3e0ae7fd63527f3e78.ies", - "mitsuba/ies/ANIISOTROPIC/OTHER_HALF_SYMMETRY/028e97564391140b1476695ae7a46fa4.ies", - "mitsuba/ies/NO_LATERAL_SYMMET/4b88bf886b39cfa63094e70e1afa680e.ies" - ], - "gui": true, - "writeAssets": false -} \ No newline at end of file diff --git a/50.IESViewer/main.cpp b/50.IESViewer/main.cpp deleted file mode 100644 index ca44888ef..000000000 --- a/50.IESViewer/main.cpp +++ /dev/null @@ -1,14 +0,0 @@ -// 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 - -#include "App.hpp" - -IESViewer::IESViewer(const path& _localInputCWD, const path& _localOutputCWD, const path& _sharedInputCWD, const path& _sharedOutputCWD) - : IApplicationFramework(_localInputCWD, _localOutputCWD, _sharedInputCWD, _sharedOutputCWD), - device_base_t({ AppWindowWidth, AppWindowHeight }, AppDepthBufferFormat, _localInputCWD, _localOutputCWD, _sharedInputCWD, _sharedOutputCWD) -{ - -} - -NBL_MAIN_FUNC(IESViewer) diff --git a/53_ComputeShaders/CMakeLists.txt b/53_ComputeShaders/CMakeLists.txt new file mode 100644 index 000000000..2f9218f93 --- /dev/null +++ b/53_ComputeShaders/CMakeLists.txt @@ -0,0 +1,6 @@ +include(common RESULT_VARIABLE RES) +if(NOT RES) + message(FATAL_ERROR "common.cmake not found. Should be in {repo_root}/cmake directory") +endif() + +nbl_create_executable_project("" "" "" "" "${NBL_EXECUTABLE_PROJECT_CREATION_PCH_TARGET}") \ No newline at end of file diff --git a/53_ComputeShaders/computeShader.comp b/53_ComputeShaders/computeShader.comp new file mode 100644 index 000000000..033a6aabb --- /dev/null +++ b/53_ComputeShaders/computeShader.comp @@ -0,0 +1,95 @@ +#version 450 core +#extension GL_EXT_shader_16bit_storage : require + +#include "shaderCommon.glsl" + +layout(set = 0, binding = 0, std430) buffer Position +{ + vec4 positions[]; +}; + +layout(set = 0, binding = 1, std430) buffer Velocity +{ + vec4 velocities[]; +}; + +layout(set = 0, binding = 2, std430) buffer Color +{ + vec4 colors[]; +}; + +layout(set = 0, binding = 3, std430) buffer ColorRisingFlag +{ + bvec4 colorsRisingFlag[]; +}; + +layout(local_size_x = 128, local_size_y = 1, local_size_z = 1) in; + +void manageColorAxieState(float colorAxie, inout bool colorIntensityRisingAxieFlag) +{ + if(colorAxie <= 0) + colorIntensityRisingAxieFlag = true; + else if(colorAxie >= 1) + colorIntensityRisingAxieFlag = false; +} + +void manageColorState(vec3 color) +{ + uint globalInvocationID = gl_GlobalInvocationID.x; // the .y and .z are both 1 in this case + bvec4 isColorIntensityRising = colorsRisingFlag[globalInvocationID]; + + manageColorAxieState(color.x, isColorIntensityRising.x); + manageColorAxieState(color.y, isColorIntensityRising.y); + manageColorAxieState(color.z, isColorIntensityRising.z); + + colorsRisingFlag[globalInvocationID] = isColorIntensityRising; +} + +float getNewAxieColor(float colorAxie, bool colorIntensityRisingAxieFlag) +{ + const float colorDelta = 0.04; + + if(colorIntensityRisingAxieFlag) + colorAxie += colorDelta; + else + colorAxie -= colorDelta; + + return colorAxie; +} + +vec3 getNewColor(vec3 color) +{ + uint globalInvocationID = gl_GlobalInvocationID.x; // the .y and .z are both 1 in this case + bvec4 isColorIntensityRising = colorsRisingFlag[globalInvocationID]; + + return vec3(getNewAxieColor(color.x, isColorIntensityRising.x), getNewAxieColor(color.y, isColorIntensityRising.y), getNewAxieColor(color.z, isColorIntensityRising.z)); +} + +void main() +{ + const float deltaTime = 0.004; + + uint globalInvocationID = gl_GlobalInvocationID.x; // the .y and .z are both 1 in this case + + vec3 position = positions[globalInvocationID].xyz; + vec3 velocity = velocities[globalInvocationID].xyz; + vec3 color = colors[globalInvocationID].xyz; + + if(!pushConstants.isXPressed) + { + /* + if(pushConstants.isZPressed) + { + // TODO gravity to force a particle's velocity towards the user + } + */ + position += velocity * deltaTime; + } + + vec3 newComputedColor = getNewColor(color); + manageColorState(newComputedColor); + + positions[globalInvocationID].xyz = position; + velocities[globalInvocationID].xyz = velocity; + colors[globalInvocationID].xyz = newComputedColor; +} \ No newline at end of file diff --git a/53_ComputeShaders/config.json.template b/53_ComputeShaders/config.json.template new file mode 100644 index 000000000..f961745c1 --- /dev/null +++ b/53_ComputeShaders/config.json.template @@ -0,0 +1,28 @@ +{ + "enableParallelBuild": true, + "threadsPerBuildProcess" : 2, + "isExecuted": false, + "scriptPath": "", + "cmake": { + "configurations": [ "Release", "Debug", "RelWithDebInfo" ], + "buildModes": [], + "requiredOptions": [] + }, + "profiles": [ + { + "backend": "vulkan", + "platform": "windows", + "buildModes": [], + "runConfiguration": "Release", + "gpuArchitectures": [] + } + ], + "dependencies": [], + "data": [ + { + "dependencies": [], + "command": [""], + "outputs": [] + } + ] +} \ No newline at end of file diff --git a/53_ComputeShaders/fragmentShader.frag b/53_ComputeShaders/fragmentShader.frag new file mode 100644 index 000000000..9fe445b2b --- /dev/null +++ b/53_ComputeShaders/fragmentShader.frag @@ -0,0 +1,12 @@ +#version 430 core + +layout(location = 0) in vec4 inFFullyProjectedVelocity; +layout(location = 1) in vec4 inFColor; + +layout(location = 0) out vec4 outColor; + +void main() +{ + outColor = inFColor; +} + \ No newline at end of file diff --git a/53_ComputeShaders/geometryShader.geom b/53_ComputeShaders/geometryShader.geom new file mode 100644 index 000000000..4a8bf36f0 --- /dev/null +++ b/53_ComputeShaders/geometryShader.geom @@ -0,0 +1,27 @@ +#version 450 core + +#include "shaderCommon.glsl" + +layout(location = 0) in vec4 gFullyProjectedVelocity[]; +layout(location = 1) in vec4 gColor[]; + +layout(location = 0) out vec4 outFVelocity; +layout(location = 1) out vec4 outFColor; + +layout (points) in; +layout (line_strip, max_vertices = 2) out; + +void main() +{ + if(pushConstants.isCPressed) + { + outFColor = vec4(0.0, 1.0, 0.0, 0.0); + gl_Position = gl_in[0].gl_Position; + EmitVertex(); + gl_Position = gl_in[0].gl_Position + gFullyProjectedVelocity[0]; + EmitVertex(); + + EndPrimitive(); + } +} + \ No newline at end of file diff --git a/53_ComputeShaders/main.cpp b/53_ComputeShaders/main.cpp new file mode 100644 index 000000000..e06d4f296 --- /dev/null +++ b/53_ComputeShaders/main.cpp @@ -0,0 +1,694 @@ +// 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 + +#define _NBL_STATIC_LIB_ +#include +#include +#include + +#include "CCamera.hpp" +#include "../common/CommonAPI.h" +#include "nbl/ext/ScreenShot/ScreenShot.h" + +using namespace nbl; +using namespace asset; +using namespace core; + +/* + Uncomment for more detailed logging +*/ + +// #define NBL_MORE_LOGS + +class CEventReceiver +{ +public: + CEventReceiver() : particlesVectorChangeFlag(false), forceChangeVelocityFlag(false), visualizeVelocityVectorsFlag(false) {} + + void process(const ui::IKeyboardEventChannel::range_t& events) + { + particlesVectorChangeFlag = false; + forceChangeVelocityFlag = false; + visualizeVelocityVectorsFlag = false; + + for (auto eventIterator = events.begin(); eventIterator != events.end(); eventIterator++) + { + auto event = *eventIterator; + + if (event.keyCode == nbl::ui::EKC_X) + particlesVectorChangeFlag = true; + + if (event.keyCode == nbl::ui::EKC_Z) + forceChangeVelocityFlag = true; + + if (event.keyCode == nbl::ui::EKC_C) + visualizeVelocityVectorsFlag = true; + + if (event.keyCode == nbl::ui::EKC_V) + visualizeVelocityVectorsFlag = false; + } + } + + inline bool isXPressed() const { return particlesVectorChangeFlag; } + inline bool isZPressed() const { return forceChangeVelocityFlag; } + inline bool isCPressed() const { return visualizeVelocityVectorsFlag; } + +private: + bool particlesVectorChangeFlag; + bool forceChangeVelocityFlag; + bool visualizeVelocityVectorsFlag; +}; + +_NBL_STATIC_INLINE_CONSTEXPR size_t NUMBER_OF_PARTICLES = 1024 * 1024; // total number of particles to move +_NBL_STATIC_INLINE_CONSTEXPR size_t WORK_GROUP_SIZE = 128; // work-items per work-group + +enum E_ENTRIES +{ + EE_POSITIONS, + EE_VELOCITIES, + EE_COLORS, + EE_COLORS_RISING_FLAG, + EE_COUNT +}; + +#include "nbl/nblpack.h" +struct alignas(16) SShaderStorageBufferObject +{ + core::vector4df_SIMD positions[NUMBER_OF_PARTICLES]; + core::vector4df_SIMD velocities[NUMBER_OF_PARTICLES]; + core::vector4df_SIMD colors[NUMBER_OF_PARTICLES]; + bool isColorIntensityRising[NUMBER_OF_PARTICLES][4]; +} PACK_STRUCT; +#include "nbl/nblunpack.h" + +static_assert(sizeof(SShaderStorageBufferObject) == sizeof(SShaderStorageBufferObject::positions) + sizeof(SShaderStorageBufferObject::velocities) + sizeof(SShaderStorageBufferObject::colors) + sizeof(SShaderStorageBufferObject::isColorIntensityRising), "There will be inproper alignment!"); + +#include "nbl/nblpack.h" +struct alignas(32) SPushConstants +{ + uint32_t isXPressed = false; + uint32_t isZPressed = false; + uint32_t isCPressed = false; + core::vector3df currentUserAbsolutePosition; +} PACK_STRUCT; +#include "nbl/nblunpack.h" + +void triggerRandomSetup(SShaderStorageBufferObject* ssbo) +{ + _NBL_STATIC_INLINE_CONSTEXPR float POSITION_EACH_AXIE_MIN = -10.f; + _NBL_STATIC_INLINE_CONSTEXPR float POSITION_EACH_AXIE_MAX = 10.f; + + _NBL_STATIC_INLINE_CONSTEXPR float VELOCITY_EACH_AXIE_MIN = 0.f; + _NBL_STATIC_INLINE_CONSTEXPR float VELOCITY_EACH_AXIE_MAX = 0.001f; + + _NBL_STATIC_INLINE_CONSTEXPR float COLOR_EACH_AXIE_MIN = 0.f; + _NBL_STATIC_INLINE_CONSTEXPR float COLOR_EACH_AXIE_MAX = 1.f; + + auto get_random = [&](const float& min, const float& max) + { + static std::default_random_engine engine; + static std::uniform_real_distribution<> distribution(min, max); + return distribution(engine); + }; + + for (size_t i = 0; i < NUMBER_OF_PARTICLES; ++i) + { + ssbo->positions[i] = core::vector4df_SIMD(get_random(POSITION_EACH_AXIE_MIN, POSITION_EACH_AXIE_MAX), get_random(POSITION_EACH_AXIE_MIN, POSITION_EACH_AXIE_MAX), get_random(POSITION_EACH_AXIE_MIN, POSITION_EACH_AXIE_MAX), get_random(POSITION_EACH_AXIE_MIN, POSITION_EACH_AXIE_MAX)); + ssbo->velocities[i] = core::vector4df_SIMD(get_random(VELOCITY_EACH_AXIE_MIN, VELOCITY_EACH_AXIE_MAX), get_random(VELOCITY_EACH_AXIE_MIN, VELOCITY_EACH_AXIE_MAX), get_random(VELOCITY_EACH_AXIE_MIN, VELOCITY_EACH_AXIE_MAX), get_random(VELOCITY_EACH_AXIE_MIN, VELOCITY_EACH_AXIE_MAX)); + ssbo->colors[i] = core::vector4df_SIMD(get_random(COLOR_EACH_AXIE_MIN, COLOR_EACH_AXIE_MAX), get_random(COLOR_EACH_AXIE_MIN, COLOR_EACH_AXIE_MAX), get_random(COLOR_EACH_AXIE_MIN, COLOR_EACH_AXIE_MAX), get_random(COLOR_EACH_AXIE_MIN, COLOR_EACH_AXIE_MAX)); + + for (uint8_t b = 0; b < 4; ++b) + ssbo->isColorIntensityRising[i][b] = true; + } +} + +class MeshLoadersApp : public ApplicationBase +{ + static constexpr uint32_t WIN_W = 1280; + static constexpr uint32_t WIN_H = 720; + static constexpr uint32_t FBO_COUNT = 2u; + static constexpr uint32_t FRAMES_IN_FLIGHT = 1u; + static constexpr size_t NBL_FRAMES_TO_AVERAGE = 100ull; + +public: + nbl::core::smart_refctd_ptr windowManager; + nbl::core::smart_refctd_ptr window; + nbl::core::smart_refctd_ptr windowCallback; + nbl::core::smart_refctd_ptr gl; + nbl::core::smart_refctd_ptr surface; + nbl::core::smart_refctd_ptr utilities; + nbl::core::smart_refctd_ptr logicalDevice; + nbl::video::IPhysicalDevice* gpuPhysicalDevice; + std::array queues = { nullptr, nullptr, nullptr, nullptr }; + nbl::core::smart_refctd_ptr swapchain; + nbl::core::smart_refctd_ptr renderpass; + nbl::core::smart_refctd_dynamic_array> fbo; + std::array, CommonAPI::InitOutput::MaxFramesInFlight>, CommonAPI::InitOutput::MaxQueuesCount> commandPools; + nbl::core::smart_refctd_ptr system; + nbl::core::smart_refctd_ptr assetManager; + nbl::video::IGPUObjectFromAssetConverter::SParams cpu2gpuParams; + nbl::core::smart_refctd_ptr logger; + nbl::core::smart_refctd_ptr inputSystem; + + nbl::core::smart_refctd_ptr gpuTransferFence; + nbl::core::smart_refctd_ptr gpuComputeFence; + nbl::video::IGPUObjectFromAssetConverter cpu2gpu; + + core::smart_refctd_ptr commandBuffers[1]; + + CEventReceiver eventReceiver; + CommonAPI::InputSystem::ChannelReader mouse; + CommonAPI::InputSystem::ChannelReader keyboard; + + Camera camera = Camera(core::vectorSIMDf(0, 0, 0), core::vectorSIMDf(0, 0, 0), core::matrix4SIMD()); + std::chrono::system_clock::time_point lastTime; + size_t frame_count = 0ull; + double time_sum = 0; + double dtList[NBL_FRAMES_TO_AVERAGE] = {}; + + SPushConstants pushConstants; + nbl::core::smart_refctd_ptr gpuComputePipeline; + nbl::core::smart_refctd_ptr gpuCDescriptorSet; + nbl::core::smart_refctd_ptr gpuUBO; + nbl::core::smart_refctd_ptr gpuGraphicsPipeline; + nbl::core::smart_refctd_ptr gpuGraphicsPipeline2; + nbl::core::smart_refctd_ptr gpuMeshBuffer; + nbl::core::smart_refctd_ptr gpuMeshBuffer2; + core::smart_refctd_ptr gpuGDescriptorSet1; + nbl::core::smart_refctd_ptr render_finished_sem; + nbl::video::ISwapchain::SCreationParams m_swapchainCreationParams; + + void setWindow(core::smart_refctd_ptr&& wnd) override + { + window = std::move(wnd); + } + void setSystem(core::smart_refctd_ptr&& s) override + { + system = std::move(s); + } + nbl::ui::IWindow* getWindow() override + { + return window.get(); + } + video::IAPIConnection* getAPIConnection() override + { + return gl.get(); + } + video::ILogicalDevice* getLogicalDevice() override + { + return logicalDevice.get(); + } + video::IGPURenderpass* getRenderpass() override + { + return renderpass.get(); + } + void setSurface(core::smart_refctd_ptr&& s) override + { + surface = std::move(s); + } + void setFBOs(std::vector>& f) override + { + for (int i = 0; i < f.size(); i++) + { + fbo->begin()[i] = core::smart_refctd_ptr(f[i]); + } + } + void setSwapchain(core::smart_refctd_ptr&& s) override + { + swapchain = std::move(s); + } + uint32_t getSwapchainImageCount() override + { + return swapchain->getImageCount(); + } + virtual nbl::asset::E_FORMAT getDepthFormat() override + { + return nbl::asset::EF_D32_SFLOAT; + } + +APP_CONSTRUCTOR(MeshLoadersApp) + + void onAppInitialized_impl() override + { + const auto swapchainImageUsage = static_cast(asset::IImage::EUF_COLOR_ATTACHMENT_BIT); + CommonAPI::InitParams initParams; + initParams.window = core::smart_refctd_ptr(window); + initParams.apiType = video::EAT_VULKAN; + initParams.appName = { _NBL_APP_NAME_ }; + initParams.framesInFlight = FRAMES_IN_FLIGHT; + initParams.windowWidth = WIN_W; + initParams.windowHeight = WIN_H; + initParams.swapchainImageCount = FBO_COUNT; + initParams.swapchainImageUsage = swapchainImageUsage; + initParams.depthFormat = nbl::asset::EF_D32_SFLOAT; + auto initOutput = CommonAPI::InitWithDefaultExt(std::move(initParams)); + + window = std::move(initParams.window); + gl = std::move(initOutput.apiConnection); + surface = std::move(initOutput.surface); + gpuPhysicalDevice = std::move(initOutput.physicalDevice); + logicalDevice = std::move(initOutput.logicalDevice); + queues = std::move(initOutput.queues); + renderpass = std::move(initOutput.renderToSwapchainRenderpass); + commandPools = std::move(initOutput.commandPools); + assetManager = std::move(initOutput.assetManager); + logger = std::move(initOutput.logger); + inputSystem = std::move(initOutput.inputSystem); + windowCallback = std::move(initParams.windowCb); + cpu2gpuParams = std::move(initOutput.cpu2gpuParams); + m_swapchainCreationParams = std::move(initOutput.swapchainCreationParams); + auto defaultGraphicsCommandPool = commandPools[CommonAPI::InitOutput::EQT_GRAPHICS][0]; + + CommonAPI::createSwapchain(std::move(logicalDevice), m_swapchainCreationParams, WIN_W, WIN_H, swapchain); + assert(swapchain); + fbo = CommonAPI::createFBOWithSwapchainImages( + swapchain->getImageCount(), WIN_W, WIN_H, + logicalDevice, swapchain, renderpass, + nbl::asset::EF_D32_SFLOAT + ); + + logicalDevice->createCommandBuffers(defaultGraphicsCommandPool.get(), nbl::video::IGPUCommandBuffer::EL_PRIMARY, 1, commandBuffers); + auto commandBuffer = commandBuffers[0]; + + auto createDescriptorPool = [&](const uint32_t itemCount, E_DESCRIPTOR_TYPE descriptorType) + { + constexpr uint32_t maxItemCount = 256u; + { + nbl::video::IDescriptorPool::SDescriptorPoolSize poolSize; + poolSize.count = itemCount; + poolSize.type = descriptorType; + return logicalDevice->createDescriptorPool(static_cast(0), maxItemCount, 1u, &poolSize); + } + }; + + /* + Compute pipeline + */ + + auto computeShaderBundle = assetManager->getAsset("../computeShader.comp", {}); + { + bool status = !computeShaderBundle.getContents().empty(); + assert(status); + } + + auto cpuComputeShader = core::smart_refctd_ptr_static_cast(computeShaderBundle.getContents().begin()[0]); + smart_refctd_ptr gpuComputeShader; + { + auto gpu_array = cpu2gpu.getGPUObjectsFromAssets(&cpuComputeShader, &cpuComputeShader + 1, cpu2gpuParams); + if (!gpu_array || gpu_array->size() < 1u || !(*gpu_array)[0]) + assert(false); + + gpuComputeShader = (*gpu_array)[0]; + } + + auto cpuSSBOBuffer = core::make_smart_refctd_ptr(sizeof(SShaderStorageBufferObject)); + cpuSSBOBuffer->addUsageFlags(asset::IBuffer::EUF_STORAGE_BUFFER_BIT); + triggerRandomSetup(reinterpret_cast(cpuSSBOBuffer->getPointer())); + core::smart_refctd_ptr gpuSSBOBuffer; + { + cpu2gpuParams.beginCommandBuffers(); + + auto gpu_array = cpu2gpu.getGPUObjectsFromAssets(&cpuSSBOBuffer, &cpuSSBOBuffer + 1, cpu2gpuParams); + if (!gpu_array || gpu_array->size() < 1u || !(*gpu_array)[0]) + assert(false); + + cpu2gpuParams.waitForCreationToComplete(false); + + auto gpuSSBOOffsetBufferPair = (*gpu_array)[0]; + gpuSSBOBuffer = core::smart_refctd_ptr(gpuSSBOOffsetBufferPair->getBuffer()); + } + + video::IGPUDescriptorSetLayout::SBinding gpuBindingsLayout[EE_COUNT] = + { + {EE_POSITIONS, EDT_STORAGE_BUFFER, 1u, video::IGPUShader::ESS_COMPUTE, nullptr}, + {EE_VELOCITIES, EDT_STORAGE_BUFFER, 1u, video::IGPUShader::ESS_COMPUTE, nullptr}, + {EE_COLORS, EDT_STORAGE_BUFFER, 1u, video::IGPUShader::ESS_COMPUTE, nullptr}, + {EE_COLORS_RISING_FLAG, EDT_STORAGE_BUFFER, 1u, video::IGPUShader::ESS_COMPUTE, nullptr} + }; + + auto gpuCDescriptorPool = createDescriptorPool(EE_COUNT, EDT_STORAGE_BUFFER); + auto gpuCDescriptorSetLayout = logicalDevice->createDescriptorSetLayout(gpuBindingsLayout, gpuBindingsLayout + EE_COUNT); + gpuCDescriptorSet = logicalDevice->createDescriptorSet(gpuCDescriptorPool.get(), core::smart_refctd_ptr(gpuCDescriptorSetLayout)); + { + video::IGPUDescriptorSet::SDescriptorInfo gpuDescriptorSetInfos[EE_COUNT]; + + gpuDescriptorSetInfos[EE_POSITIONS].desc = gpuSSBOBuffer; + gpuDescriptorSetInfos[EE_POSITIONS].buffer.size = sizeof(SShaderStorageBufferObject::positions); + gpuDescriptorSetInfos[EE_POSITIONS].buffer.offset = 0; + + gpuDescriptorSetInfos[EE_VELOCITIES].desc = gpuSSBOBuffer; + gpuDescriptorSetInfos[EE_VELOCITIES].buffer.size = sizeof(SShaderStorageBufferObject::velocities); + gpuDescriptorSetInfos[EE_VELOCITIES].buffer.offset = sizeof(SShaderStorageBufferObject::positions); + + gpuDescriptorSetInfos[EE_COLORS].desc = gpuSSBOBuffer; + gpuDescriptorSetInfos[EE_COLORS].buffer.size = sizeof(SShaderStorageBufferObject::colors); + gpuDescriptorSetInfos[EE_COLORS].buffer.offset = gpuDescriptorSetInfos[EE_VELOCITIES].buffer.offset + sizeof(SShaderStorageBufferObject::velocities); + + gpuDescriptorSetInfos[EE_COLORS_RISING_FLAG].desc = gpuSSBOBuffer; + gpuDescriptorSetInfos[EE_COLORS_RISING_FLAG].buffer.size = sizeof(SShaderStorageBufferObject::isColorIntensityRising); + gpuDescriptorSetInfos[EE_COLORS_RISING_FLAG].buffer.offset = gpuDescriptorSetInfos[EE_COLORS].buffer.offset + sizeof(SShaderStorageBufferObject::colors); + + video::IGPUDescriptorSet::SWriteDescriptorSet gpuWrites[EE_COUNT]; + { + for (uint32_t binding = 0u; binding < EE_COUNT; binding++) + gpuWrites[binding] = { gpuCDescriptorSet.get(), binding, 0u, 1u, EDT_STORAGE_BUFFER, gpuDescriptorSetInfos + binding }; + logicalDevice->updateDescriptorSets(EE_COUNT, gpuWrites, 0u, nullptr); + } + } + + asset::SPushConstantRange pushConstantRange; + { + pushConstantRange.stageFlags = (asset::IShader::E_SHADER_STAGE)(asset::IShader::ESS_COMPUTE | asset::IShader::ESS_GEOMETRY); + pushConstantRange.offset = 0; + pushConstantRange.size = sizeof(SPushConstants); + } + + auto gpuCPipelineLayout = logicalDevice->createPipelineLayout(&pushConstantRange, &pushConstantRange + 1, std::move(gpuCDescriptorSetLayout), nullptr, nullptr, nullptr); + gpuComputePipeline = logicalDevice->createComputePipeline(nullptr, std::move(gpuCPipelineLayout), std::move(gpuComputeShader)); + + /* + Graphics Pipeline + */ + + asset::SVertexInputParams inputVertexParams; + inputVertexParams.enabledAttribFlags = core::createBitmask({ EE_POSITIONS, EE_VELOCITIES, EE_COLORS, EE_COLORS_RISING_FLAG }); + inputVertexParams.enabledBindingFlags = core::createBitmask({ EE_POSITIONS, EE_VELOCITIES, EE_COLORS, EE_COLORS_RISING_FLAG }); + + for (uint8_t i = 0; i < EE_COUNT; ++i) + { + inputVertexParams.bindings[i].stride = (i == EE_COLORS_RISING_FLAG ? getTexelOrBlockBytesize(EF_R8G8B8A8_UINT) : getTexelOrBlockBytesize(EF_R32G32B32A32_SFLOAT)); + inputVertexParams.bindings[i].inputRate = asset::EVIR_PER_VERTEX; + + inputVertexParams.attributes[i].binding = i; + inputVertexParams.attributes[i].format = (i == EE_COLORS_RISING_FLAG ? EF_R8G8B8A8_UINT : asset::EF_R32G32B32A32_SFLOAT); + inputVertexParams.attributes[i].relativeOffset = 0; + } + + asset::SBlendParams blendParams; + asset::SPrimitiveAssemblyParams primitiveAssemblyParams; + primitiveAssemblyParams.primitiveType = EPT_POINT_LIST; + asset::SRasterizationParams rasterizationParams; + + video::IGPUDescriptorSetLayout::SBinding gpuUboBinding = {}; + gpuUboBinding.count = 1u; + gpuUboBinding.binding = 0; + gpuUboBinding.stageFlags = static_cast(asset::ICPUShader::ESS_VERTEX | asset::ICPUShader::ESS_FRAGMENT); + gpuUboBinding.type = asset::EDT_UNIFORM_BUFFER; + + auto gpuGDescriptorPool = createDescriptorPool(1, EDT_UNIFORM_BUFFER); + auto gpuGDs1Layout = logicalDevice->createDescriptorSetLayout(&gpuUboBinding, &gpuUboBinding + 1); + + video::IGPUBuffer::SCreationParams gpuUBOCreationParams; + //gpuUBOCreationParams.size = sizeof(SBasicViewParameters); + gpuUBOCreationParams.usage = asset::IBuffer::E_USAGE_FLAGS(asset::IBuffer::EUF_UNIFORM_BUFFER_BIT | asset::IBuffer::EUF_INLINE_UPDATE_VIA_CMDBUF); + gpuUBOCreationParams.queueFamilyIndexCount = 0u; + gpuUBOCreationParams.queueFamilyIndices = nullptr; + gpuUBOCreationParams.size = sizeof(SBasicViewParameters); + + gpuUBO = logicalDevice->createBuffer(std::move(gpuUBOCreationParams)); + auto gpuUBOmemreqs = gpuUBO->getMemoryReqs(); + gpuUBOmemreqs.memoryTypeBits &= gpuPhysicalDevice->getDeviceLocalMemoryTypeBits(); + logicalDevice->allocate(gpuUBOmemreqs, gpuUBO.get()); + + gpuGDescriptorSet1 = logicalDevice->createDescriptorSet(gpuGDescriptorPool.get(), gpuGDs1Layout); + { + video::IGPUDescriptorSet::SWriteDescriptorSet write; + write.dstSet = gpuGDescriptorSet1.get(); + write.binding = 0; + write.count = 1u; + write.arrayElement = 0u; + write.descriptorType = asset::EDT_UNIFORM_BUFFER; + video::IGPUDescriptorSet::SDescriptorInfo info; + { + info.desc = gpuUBO; + info.buffer.offset = 0ull; + info.buffer.size = sizeof(SBasicViewParameters); + } + write.info = &info; + logicalDevice->updateDescriptorSets(1u, &write, 0u, nullptr); + } + + auto vertexShaderBundle = assetManager->getAsset("../vertexShader.vert", {}); + { + bool status = !vertexShaderBundle.getContents().empty(); + assert(status); + } + + auto cpuVertexShader = core::smart_refctd_ptr_static_cast(vertexShaderBundle.getContents().begin()[0]); + smart_refctd_ptr gpuVertexShader; + { + auto gpu_array = cpu2gpu.getGPUObjectsFromAssets(&cpuVertexShader, &cpuVertexShader + 1, cpu2gpuParams); + if (!gpu_array || gpu_array->size() < 1u || !(*gpu_array)[0]) + assert(false); + + gpuVertexShader = (*gpu_array)[0]; + } + + auto fragmentShaderBundle = assetManager->getAsset("../fragmentShader.frag", {}); + { + bool status = !fragmentShaderBundle.getContents().empty(); + assert(status); + } + + auto cpuFragmentShader = core::smart_refctd_ptr_static_cast(fragmentShaderBundle.getContents().begin()[0]); + smart_refctd_ptr gpuFragmentShader; + { + auto gpu_array = cpu2gpu.getGPUObjectsFromAssets(&cpuFragmentShader, &cpuFragmentShader + 1, cpu2gpuParams); + if (!gpu_array || gpu_array->size() < 1u || !(*gpu_array)[0]) + assert(false); + + gpuFragmentShader = (*gpu_array)[0]; + } + + auto geometryShaderBundle = assetManager->getAsset("../geometryShader.geom", {}); + { + bool status = !geometryShaderBundle.getContents().empty(); + assert(status); + } + + auto cpuGeometryShader = core::smart_refctd_ptr_static_cast(geometryShaderBundle.getContents().begin()[0]); + smart_refctd_ptr gpuGeometryShader; + { + auto gpu_array = cpu2gpu.getGPUObjectsFromAssets(&cpuGeometryShader, &cpuGeometryShader + 1, cpu2gpuParams); + if (!gpu_array || gpu_array->size() < 1u || !(*gpu_array)[0]) + assert(false); + + gpuGeometryShader = (*gpu_array)[0]; + } + + core::smart_refctd_ptr gpuGShaders[] = { gpuVertexShader, gpuFragmentShader, gpuGeometryShader }; + auto gpuGShadersPointer = reinterpret_cast(gpuGShaders); + + auto gpuGPipelineLayout = logicalDevice->createPipelineLayout(&pushConstantRange, &pushConstantRange + 1, nullptr, std::move(gpuGDs1Layout), nullptr, nullptr); + auto gpuRenderpassIndependentPipeline = logicalDevice->createRenderpassIndependentPipeline(nullptr, core::smart_refctd_ptr(gpuGPipelineLayout), gpuGShadersPointer, gpuGShadersPointer + 2 /* discard geometry shader*/, inputVertexParams, blendParams, primitiveAssemblyParams, rasterizationParams); + auto gpuRenderpassIndependentPipeline2 = logicalDevice->createRenderpassIndependentPipeline(nullptr, core::smart_refctd_ptr(gpuGPipelineLayout), gpuGShadersPointer, gpuGShadersPointer + 3, inputVertexParams, blendParams, primitiveAssemblyParams, rasterizationParams); + + asset::SBufferBinding gpuGbindings[video::IGPUMeshBuffer::MAX_ATTR_BUF_BINDING_COUNT]; + + gpuGbindings[EE_POSITIONS].buffer = gpuSSBOBuffer; + gpuGbindings[EE_POSITIONS].offset = 0; + + gpuGbindings[EE_VELOCITIES].buffer = gpuSSBOBuffer; + gpuGbindings[EE_VELOCITIES].offset = sizeof(SShaderStorageBufferObject::positions); + + gpuGbindings[EE_COLORS].buffer = gpuSSBOBuffer; + gpuGbindings[EE_COLORS].offset = gpuGbindings[EE_VELOCITIES].offset + sizeof(SShaderStorageBufferObject::velocities); + + gpuGbindings[EE_COLORS_RISING_FLAG].buffer = gpuSSBOBuffer; + gpuGbindings[EE_COLORS_RISING_FLAG].offset = gpuGbindings[EE_COLORS].offset + sizeof(SShaderStorageBufferObject::colors); + + gpuMeshBuffer = core::make_smart_refctd_ptr(std::move(gpuRenderpassIndependentPipeline), nullptr, gpuGbindings, asset::SBufferBinding()); + { + gpuMeshBuffer->setIndexType(asset::EIT_UNKNOWN); + gpuMeshBuffer->setIndexCount(NUMBER_OF_PARTICLES); + } + + { + nbl::video::IGPUGraphicsPipeline::SCreationParams graphicsPipelineParams; + graphicsPipelineParams.renderpassIndependent = core::smart_refctd_ptr(const_cast(gpuMeshBuffer->getPipeline())); + graphicsPipelineParams.renderpass = core::smart_refctd_ptr(renderpass); + gpuGraphicsPipeline = logicalDevice->createGraphicsPipeline(nullptr, std::move(graphicsPipelineParams)); + } + + gpuMeshBuffer2 = core::make_smart_refctd_ptr(std::move(gpuRenderpassIndependentPipeline2), nullptr, gpuGbindings, asset::SBufferBinding()); + { + gpuMeshBuffer2->setIndexType(asset::EIT_UNKNOWN); + gpuMeshBuffer2->setIndexCount(NUMBER_OF_PARTICLES); + } + + { + nbl::video::IGPUGraphicsPipeline::SCreationParams graphicsPipelineParams; + graphicsPipelineParams.renderpassIndependent = core::smart_refctd_ptr(const_cast(gpuMeshBuffer2->getPipeline())); + graphicsPipelineParams.renderpass = core::smart_refctd_ptr(renderpass); + gpuGraphicsPipeline2 = logicalDevice->createGraphicsPipeline(nullptr, std::move(graphicsPipelineParams)); + } + + const std::string captionData = "[Nabla Engine] Compute Shaders"; + window->setCaption(captionData); + + core::vectorSIMDf cameraPosition(0, 0, 0); + matrix4SIMD projectionMatrix = matrix4SIMD::buildProjectionMatrixPerspectiveFovLH(core::radians(60.0f), video::ISurface::getTransformedAspectRatio(swapchain->getPreTransform(), WIN_W, WIN_H), 0.001, 1000); + camera = Camera(cameraPosition, core::vectorSIMDf(0, 0, -1), projectionMatrix, 10.f, 1.f); + lastTime = std::chrono::system_clock::now(); + for (size_t i = 0ull; i < NBL_FRAMES_TO_AVERAGE; ++i) + dtList[i] = 0.0; + } + + void onAppTerminated_impl() override + { + const auto& fboCreationParams = fbo->begin()[0]->getCreationParameters(); + auto gpuSourceImageView = fboCreationParams.attachments[0]; + + bool status = ext::ScreenShot::createScreenShot(logicalDevice.get(), + queues[CommonAPI::InitOutput::EQT_TRANSFER_UP], + render_finished_sem.get(), + gpuSourceImageView.get(), + assetManager.get(), + "ScreenShot.png", + asset::IImage::EL_PRESENT_SRC, + asset::EAF_NONE); + + assert(status); + } + + void workLoopBody() override + { + auto renderStart = std::chrono::system_clock::now(); + const auto renderDt = std::chrono::duration_cast(renderStart - lastTime).count(); + lastTime = renderStart; + { // Calculate Simple Moving Average for FrameTime + time_sum -= dtList[frame_count]; + time_sum += renderDt; + dtList[frame_count] = renderDt; + frame_count++; + if (frame_count >= NBL_FRAMES_TO_AVERAGE) + frame_count = 0; + } + const double averageFrameTime = time_sum / (double)NBL_FRAMES_TO_AVERAGE; + +#ifdef NBL_MORE_LOGS + logger->log("renderDt = %f ------ averageFrameTime = %f", system::ILogger::ELL_INFO, renderDt, averageFrameTime); +#endif // NBL_MORE_LOGS + + auto averageFrameTimeDuration = std::chrono::duration(averageFrameTime); + auto nextPresentationTime = renderStart + averageFrameTimeDuration; + auto nextPresentationTimeStamp = std::chrono::duration_cast(nextPresentationTime.time_since_epoch()); + + inputSystem->getDefaultMouse(&mouse); + inputSystem->getDefaultKeyboard(&keyboard); + + camera.beginInputProcessing(nextPresentationTimeStamp); + mouse.consumeEvents([&](const ui::IMouseEventChannel::range_t& events) -> void { camera.mouseProcess(events); }, logger.get()); + keyboard.consumeEvents([&](const ui::IKeyboardEventChannel::range_t& events) -> void { camera.keyboardProcess(events); eventReceiver.process(events); }, logger.get()); + camera.endInputProcessing(nextPresentationTimeStamp); + + const auto& viewMatrix = camera.getViewMatrix(); + const auto& viewProjectionMatrix = matrix4SIMD::concatenateBFollowedByAPrecisely( + video::ISurface::getSurfaceTransformationMatrix(swapchain->getPreTransform()), + camera.getConcatenatedMatrix() + ); + + auto& commandBuffer = commandBuffers[0]; + commandBuffer->reset(nbl::video::IGPUCommandBuffer::ERF_RELEASE_RESOURCES_BIT); + commandBuffer->begin(video::IGPUCommandBuffer::EU_ONE_TIME_SUBMIT_BIT); // TODO: Reset Frame's CommandPool + + asset::SViewport viewport; + viewport.minDepth = 1.f; + viewport.maxDepth = 0.f; + viewport.x = 0u; + viewport.y = 0u; + viewport.width = WIN_W; + viewport.height = WIN_H; + commandBuffer->setViewport(0u, 1u, &viewport); + + nbl::video::IGPUCommandBuffer::SRenderpassBeginInfo beginInfo; + VkRect2D area; + area.offset = { 0,0 }; + area.extent = { WIN_W, WIN_H }; + nbl::asset::SClearValue clear[2]; + clear[0].color.float32[0] = 0.f; + clear[0].color.float32[1] = 0.f; + clear[0].color.float32[2] = 0.f; + clear[0].color.float32[3] = 0.f; + clear[1].depthStencil.depth = 0.f; + + beginInfo.clearValueCount = 2u; + beginInfo.framebuffer = fbo->begin()[0]; + beginInfo.renderpass = renderpass; + beginInfo.renderArea = area; + beginInfo.clearValues = clear; + + commandBuffer->beginRenderPass(&beginInfo, nbl::asset::ESC_INLINE); + + pushConstants.isXPressed = eventReceiver.isXPressed(); + pushConstants.isZPressed = eventReceiver.isZPressed(); + pushConstants.isCPressed = eventReceiver.isCPressed(); + pushConstants.currentUserAbsolutePosition = camera.getPosition().getAsVector3df(); + + /* + Calculation of particle postitions takes place here + */ + + commandBuffer->bindComputePipeline(gpuComputePipeline.get()); + commandBuffer->pushConstants(gpuComputePipeline->getLayout(), asset::IShader::ESS_COMPUTE, 0, sizeof(SPushConstants), &pushConstants); + commandBuffer->bindDescriptorSets(EPBP_COMPUTE, gpuComputePipeline->getLayout(), 0, 1, &gpuCDescriptorSet.get(), 0u); + + static_assert(NUMBER_OF_PARTICLES % WORK_GROUP_SIZE == 0, "Inccorect amount!"); + _NBL_STATIC_INLINE_CONSTEXPR size_t groupCountX = NUMBER_OF_PARTICLES / WORK_GROUP_SIZE; + + commandBuffer->dispatch(groupCountX, 1, 1); + + /* + After calculation of positions each particle gets displayed + */ + + core::matrix3x4SIMD modelMatrix; + modelMatrix.setTranslation(nbl::core::vectorSIMDf(0, 0, 0, 0)); + + core::matrix4SIMD mvp = core::concatenateBFollowedByA(viewProjectionMatrix, modelMatrix); + + SBasicViewParameters uboData; + memcpy(uboData.MV, viewMatrix.pointer(), sizeof(uboData.MV)); + memcpy(uboData.MVP, mvp.pointer(), sizeof(uboData.MVP)); + memcpy(uboData.NormalMat, viewMatrix.pointer(), sizeof(uboData.NormalMat)); + commandBuffer->updateBuffer(gpuUBO.get(), 0ull, sizeof(uboData), &uboData); + + /* + Draw particles + */ + + commandBuffer->bindGraphicsPipeline(gpuGraphicsPipeline.get()); + commandBuffer->bindDescriptorSets(asset::EPBP_GRAPHICS, gpuMeshBuffer->getPipeline()->getLayout(), 1u, 1u, &gpuGDescriptorSet1.get(), 0u); + commandBuffer->drawMeshBuffer(gpuMeshBuffer.get()); + + /* + Draw extras with geometry usage under key c and v conditions + */ + + commandBuffer->bindGraphicsPipeline(gpuGraphicsPipeline2.get()); + commandBuffer->pushConstants(gpuMeshBuffer2->getPipeline()->getLayout(), asset::IShader::ESS_GEOMETRY, 0, sizeof(SPushConstants), &pushConstants); + commandBuffer->bindDescriptorSets(asset::EPBP_GRAPHICS, gpuMeshBuffer2->getPipeline()->getLayout(), 1u, 1u, &gpuGDescriptorSet1.get(), 0u); + commandBuffer->drawMeshBuffer(gpuMeshBuffer2.get()); + + commandBuffer->endRenderPass(); + commandBuffer->end(); + + auto img_acq_sem = logicalDevice->createSemaphore(); + render_finished_sem = logicalDevice->createSemaphore(); + + uint32_t imgnum = 0u; + constexpr uint64_t MAX_TIMEOUT = 99999999999999ull; // ns + swapchain->acquireNextImage(MAX_TIMEOUT, img_acq_sem.get(), nullptr, &imgnum); + + CommonAPI::Submit(logicalDevice.get(), commandBuffer.get(), queues[CommonAPI::InitOutput::EQT_GRAPHICS], img_acq_sem.get(), render_finished_sem.get()); + CommonAPI::Present(logicalDevice.get(), swapchain.get(), queues[CommonAPI::InitOutput::EQT_GRAPHICS], render_finished_sem.get(), imgnum); + } + + bool keepRunning() override + { + return windowCallback->isWindowOpen(); + } +}; + +NBL_COMMON_API_MAIN(MeshLoadersApp, MeshLoadersApp::Nabla) diff --git a/53_ComputeShaders/pipeline.groovy b/53_ComputeShaders/pipeline.groovy new file mode 100644 index 000000000..e8eb74b5b --- /dev/null +++ b/53_ComputeShaders/pipeline.groovy @@ -0,0 +1,50 @@ +import org.DevshGraphicsProgramming.Agent +import org.DevshGraphicsProgramming.BuilderInfo +import org.DevshGraphicsProgramming.IBuilder + +class CComputeShadersBuilder extends IBuilder +{ + public CComputeShadersBuilder(Agent _agent, _info) + { + super(_agent, _info) + } + + @Override + public boolean prepare(Map axisMapping) + { + return true + } + + @Override + public boolean build(Map axisMapping) + { + IBuilder.CONFIGURATION config = axisMapping.get("CONFIGURATION") + IBuilder.BUILD_TYPE buildType = axisMapping.get("BUILD_TYPE") + + def nameOfBuildDirectory = getNameOfBuildDirectory(buildType) + def nameOfConfig = getNameOfConfig(config) + + agent.execute("cmake --build ${info.rootProjectPath}/${nameOfBuildDirectory}/${info.targetProjectPathRelativeToRoot} --target ${info.targetBaseName} --config ${nameOfConfig} -j12 -v") + + return true + } + + @Override + public boolean test(Map axisMapping) + { + return true + } + + @Override + public boolean install(Map axisMapping) + { + return true + } +} + +def create(Agent _agent, _info) +{ + return new CComputeShadersBuilder(_agent, _info) +} + +return this \ No newline at end of file diff --git a/53_ComputeShaders/shaderCommon.glsl b/53_ComputeShaders/shaderCommon.glsl new file mode 100644 index 000000000..972a8789a --- /dev/null +++ b/53_ComputeShaders/shaderCommon.glsl @@ -0,0 +1,6 @@ +layout(push_constant, row_major) uniform Block{ + bool isXPressed; + bool isZPressed; + bool isCPressed; + vec3 currentUserAbsolutePostion; +} pushConstants; \ No newline at end of file diff --git a/53_ComputeShaders/vertexShader.vert b/53_ComputeShaders/vertexShader.vert new file mode 100644 index 000000000..6b14d97c8 --- /dev/null +++ b/53_ComputeShaders/vertexShader.vert @@ -0,0 +1,23 @@ +#version 430 core + +layout(location = 0) in vec4 vPosition; +layout(location = 1) in vec4 vVelocity; +layout(location = 2) in vec4 vColor; + +#include +#include + +layout (set = 1, binding = 0, row_major, std140) uniform UBO +{ + nbl_glsl_SBasicViewParameters params; +} cameraData; + +layout(location = 0) flat out vec4 outGOrFFullyProjectedVelocity; +layout(location = 1) flat out vec4 outGorFColor; + +void main() +{ + gl_Position = (cameraData.params.MVP) * vPosition; + outGOrFFullyProjectedVelocity = (cameraData.params.MVP) * vVelocity * 0.0001; + outGorFColor = vColor; +} \ No newline at end of file diff --git a/56_RayQuery/CMakeLists.txt b/56_RayQuery/CMakeLists.txt new file mode 100644 index 000000000..a476b6203 --- /dev/null +++ b/56_RayQuery/CMakeLists.txt @@ -0,0 +1,7 @@ + +include(common RESULT_VARIABLE RES) +if(NOT RES) + message(FATAL_ERROR "common.cmake not found. Should be in {repo_root}/cmake directory") +endif() + +nbl_create_executable_project("" "" "" "" "${NBL_EXECUTABLE_PROJECT_CREATION_PCH_TARGET}") \ No newline at end of file diff --git a/56_RayQuery/common.glsl b/56_RayQuery/common.glsl new file mode 100644 index 000000000..ad88789f8 --- /dev/null +++ b/56_RayQuery/common.glsl @@ -0,0 +1,793 @@ +// 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 + +// basic settings +#define MAX_DEPTH 15 +#define SAMPLES 32 + +// firefly and variance reduction techniques +//#define KILL_DIFFUSE_SPECULAR_PATHS +//#define VISUALIZE_HIGH_VARIANCE + +#define INVALID_ID_16BIT 0xffffu +struct Sphere +{ + vec3 position; + float radius2; + uint bsdfLightIDs; +}; + +layout(set=0, binding=0, rgba16f) uniform image2D outImage; + +layout(set = 2, binding = 0) uniform sampler2D envMap; +layout(set = 2, binding = 1) uniform usamplerBuffer sampleSequence; +layout(set = 2, binding = 2) uniform usampler2D scramblebuf; +layout(set = 2, binding = 3) uniform accelerationStructureEXT topLevelAS; +layout(set = 2, binding = 4) readonly restrict buffer InputBuffer +{ + Sphere spheres[]; +}; + +#ifndef _NBL_GLSL_WORKGROUP_SIZE_ +#define _NBL_GLSL_WORKGROUP_SIZE_ 16 +layout(local_size_x=_NBL_GLSL_WORKGROUP_SIZE_, local_size_y=_NBL_GLSL_WORKGROUP_SIZE_, local_size_z=1) in; +#endif + +ivec2 getCoordinates() { + return ivec2(gl_GlobalInvocationID.xy); +} + +vec2 getTexCoords() { + ivec2 imageSize = imageSize(outImage); + ivec2 iCoords = getCoordinates(); + return vec2(float(iCoords.x) / imageSize.x, 1.0 - float(iCoords.y) / imageSize.y); +} + + +#include +#include +#include + +#include + +layout(set = 1, binding = 0, row_major, std140) uniform UBO +{ + nbl_glsl_SBasicViewParameters params; +} cameraData; + +Sphere Sphere_Sphere(in vec3 position, in float radius, in uint bsdfID, in uint lightID) +{ + Sphere sphere; + sphere.position = position; + sphere.radius2 = radius*radius; + sphere.bsdfLightIDs = bitfieldInsert(bsdfID,lightID,16,16); + return sphere; +} + +// return intersection distance if found, FLT_NAN otherwise +float Sphere_intersect(in Sphere sphere, in vec3 origin, in vec3 direction) +{ + vec3 relOrigin = origin-sphere.position; + float relOriginLen2 = dot(relOrigin,relOrigin); + const float radius2 = sphere.radius2; + + float dirDotRelOrigin = dot(direction,relOrigin); + float det = radius2-relOriginLen2+dirDotRelOrigin*dirDotRelOrigin; + + // do some speculative math here + float detsqrt = sqrt(det); + return -dirDotRelOrigin+(relOriginLen2>radius2 ? (-detsqrt):detsqrt); +} + +vec3 Sphere_getNormal(in Sphere sphere, in vec3 position) +{ + const float radiusRcp = inversesqrt(sphere.radius2); + return (position-sphere.position)*radiusRcp; +} + +float Sphere_getSolidAngle_impl(in float cosThetaMax) +{ + return 2.0*nbl_glsl_PI*(1.0-cosThetaMax); +} +float Sphere_getSolidAngle(in Sphere sphere, in vec3 origin) +{ + float cosThetaMax = sqrt(1.0-sphere.radius2/nbl_glsl_lengthSq(sphere.position-origin)); + return Sphere_getSolidAngle_impl(cosThetaMax); +} + +struct Triangle +{ + vec3 vertex0; + uint bsdfLightIDs; + vec3 vertex1; + uint padding0; + vec3 vertex2; + uint padding1; +}; + +Triangle Triangle_Triangle(in mat3 vertices, in uint bsdfID, in uint lightID) +{ + Triangle tri; + tri.vertex0 = vertices[0]; + tri.vertex1 = vertices[1]; + tri.vertex2 = vertices[2]; + // + tri.bsdfLightIDs = bitfieldInsert(bsdfID, lightID, 16, 16); + return tri; +} + +// return intersection distance if found, FLT_NAN otherwise +float Triangle_intersect(in Triangle tri, in vec3 origin, in vec3 direction) +{ + const vec3 edges[2] = vec3[2](tri.vertex1-tri.vertex0,tri.vertex2-tri.vertex0); + + const vec3 h = cross(direction,edges[1]); + const float a = dot(edges[0],h); + + const vec3 relOrigin = origin-tri.vertex0; + + const float u = dot(relOrigin,h)/a; + + const vec3 q = cross(relOrigin,edges[0]); + const float v = dot(direction,q)/a; + + const float t = dot(edges[1],q)/a; + + return t>0.f&&u>=0.f&&v>=0.f&&(u+v)<=1.f ? t:nbl_glsl_FLT_NAN; +} + +vec3 Triangle_getNormalTimesArea_impl(in mat2x3 edges) +{ + return cross(edges[0],edges[1])*0.5; +} +vec3 Triangle_getNormalTimesArea(in Triangle tri) +{ + return Triangle_getNormalTimesArea_impl(mat2x3(tri.vertex1-tri.vertex0,tri.vertex2-tri.vertex0)); +} + + + +struct Rectangle +{ + vec3 offset; + uint bsdfLightIDs; + vec3 edge0; + uint padding0; + vec3 edge1; + uint padding1; +}; + +Rectangle Rectangle_Rectangle(in vec3 offset, in vec3 edge0, in vec3 edge1, in uint bsdfID, in uint lightID) +{ + Rectangle rect; + rect.offset = offset; + rect.edge0 = edge0; + rect.edge1 = edge1; + // + rect.bsdfLightIDs = bitfieldInsert(bsdfID, lightID, 16, 16); + return rect; +} + +// return intersection distance if found, FLT_NAN otherwise +float Rectangle_intersect(in Rectangle rect, in vec3 origin, in vec3 direction) +{ + const vec3 h = cross(direction,rect.edge1); + const float a = dot(rect.edge0,h); + + const vec3 relOrigin = origin-rect.offset; + + const float u = dot(relOrigin,h)/a; + + const vec3 q = cross(relOrigin,rect.edge0); + const float v = dot(direction,q)/a; + + const float t = dot(rect.edge1,q)/a; + + const bool intersection = t>0.f&&u>=0.f&&v>=0.f&&u<=1.f&&v<=1.f; + return intersection ? t:nbl_glsl_FLT_NAN; +} + +vec3 Rectangle_getNormalTimesArea(in Rectangle rect) +{ + return cross(rect.edge0,rect.edge1); +} + + + +#define DIFFUSE_OP 0u +#define CONDUCTOR_OP 1u +#define DIELECTRIC_OP 2u +#define OP_BITS_OFFSET 0 +#define OP_BITS_SIZE 2 +struct BSDFNode +{ + uvec4 data[2]; +}; + +uint BSDFNode_getType(in BSDFNode node) +{ + return bitfieldExtract(node.data[0].w,OP_BITS_OFFSET,OP_BITS_SIZE); +} +bool BSDFNode_isBSDF(in BSDFNode node) +{ + return BSDFNode_getType(node)==DIELECTRIC_OP; +} +bool BSDFNode_isNotDiffuse(in BSDFNode node) +{ + return BSDFNode_getType(node)!=DIFFUSE_OP; +} +float BSDFNode_getRoughness(in BSDFNode node) +{ + return uintBitsToFloat(node.data[1].w); +} +vec3 BSDFNode_getRealEta(in BSDFNode node) +{ + return uintBitsToFloat(node.data[0].rgb); +} +vec3 BSDFNode_getImaginaryEta(in BSDFNode node) +{ + return uintBitsToFloat(node.data[1].rgb); +} +mat2x3 BSDFNode_getEta(in BSDFNode node) +{ + return mat2x3(BSDFNode_getRealEta(node),BSDFNode_getImaginaryEta(node)); +} +#include +vec3 BSDFNode_getReflectance(in BSDFNode node, in float VdotH) +{ + const vec3 albedoOrRealIoR = uintBitsToFloat(node.data[0].rgb); + if (BSDFNode_isNotDiffuse(node)) + return nbl_glsl_fresnel_conductor(albedoOrRealIoR, BSDFNode_getImaginaryEta(node), VdotH); + else + return albedoOrRealIoR; +} + +float BSDFNode_getNEEProb(in BSDFNode bsdf) +{ + const float alpha = BSDFNode_isNotDiffuse(bsdf) ? BSDFNode_getRoughness(bsdf):1.0; + return min(8.0*alpha,1.0); +} + +#include +#include +float getLuma(in vec3 col) +{ + return dot(transpose(nbl_glsl_scRGBtoXYZ)[1],col); +} + +#define BSDF_COUNT 7 +BSDFNode bsdfs[BSDF_COUNT] = { + {{uvec4(floatBitsToUint(vec3(0.8,0.8,0.8)),DIFFUSE_OP),floatBitsToUint(vec4(0.0,0.0,0.0,0.0))}}, + {{uvec4(floatBitsToUint(vec3(0.8,0.4,0.4)),DIFFUSE_OP),floatBitsToUint(vec4(0.0,0.0,0.0,0.0))}}, + {{uvec4(floatBitsToUint(vec3(0.4,0.8,0.4)),DIFFUSE_OP),floatBitsToUint(vec4(0.0,0.0,0.0,0.0))}}, + {{uvec4(floatBitsToUint(vec3(1.02,1.02,1.3)),CONDUCTOR_OP),floatBitsToUint(vec4(1.0,1.0,2.0,0.0))}}, + {{uvec4(floatBitsToUint(vec3(1.02,1.3,1.02)),CONDUCTOR_OP),floatBitsToUint(vec4(1.0,2.0,1.0,0.0))}}, + {{uvec4(floatBitsToUint(vec3(1.02,1.3,1.02)),CONDUCTOR_OP),floatBitsToUint(vec4(1.0,2.0,1.0,0.15))}}, + {{uvec4(floatBitsToUint(vec3(1.4,1.45,1.5)),DIELECTRIC_OP),floatBitsToUint(vec4(0.0,0.0,0.0,0.0625))}} +}; + + +struct Light +{ + vec3 radiance; + uint objectID; +}; + +vec3 Light_getRadiance(in Light light) +{ + return light.radiance; +} +uint Light_getObjectID(in Light light) +{ + return light.objectID; +} + + +#define LIGHT_COUNT 1 +float scene_getLightChoicePdf(in Light light) +{ + return 1.0/float(LIGHT_COUNT); +} + + +#define LIGHT_COUNT 1 +Light lights[LIGHT_COUNT] = +{ + { + vec3(30.0,25.0,15.0), +#ifdef POLYGON_METHOD + 0u +#else + 8u +#endif + } +}; + + + +#define ANY_HIT_FLAG (-2147483648) +#define DEPTH_BITS_COUNT 8 +#define DEPTH_BITS_OFFSET (31-DEPTH_BITS_COUNT) +struct ImmutableRay_t +{ + vec3 origin; + vec3 direction; +#if POLYGON_METHOD==2 + vec3 normalAtOrigin; + bool wasBSDFAtOrigin; +#endif +}; +struct MutableRay_t +{ + float intersectionT; + uint objectID; + /* irrelevant here + uint triangleID; + vec2 barycentrics; + */ +}; +struct Payload_t +{ + vec3 accumulation; + float otherTechniqueHeuristic; + vec3 throughput; + #ifdef KILL_DIFFUSE_SPECULAR_PATHS + bool hasDiffuse; + #endif +}; + +struct Ray_t +{ + ImmutableRay_t _immutable; + MutableRay_t _mutable; + Payload_t _payload; +}; + + +#define INTERSECTION_ERROR_BOUND_LOG2 (-8.0) +float getTolerance_common(in uint depth) +{ + float depthRcp = 1.0/float(depth); + return INTERSECTION_ERROR_BOUND_LOG2;// *depthRcp*depthRcp; +} +float getStartTolerance(in uint depth) +{ + return exp2(getTolerance_common(depth)); +} +float getEndTolerance(in uint depth) +{ + return 1.0-exp2(getTolerance_common(depth)+1.0); +} + + +vec2 SampleSphericalMap(vec3 v) +{ + vec2 uv = vec2(atan(v.z, v.x), asin(v.y)); + uv *= nbl_glsl_RECIPROCAL_PI*0.5; + uv += 0.5; + return uv; +} + +void missProgram(in ImmutableRay_t _immutable, inout Payload_t _payload) +{ + vec3 finalContribution = _payload.throughput; + // #define USE_ENVMAP +#ifdef USE_ENVMAP + vec2 uv = SampleSphericalMap(_immutable.direction); + finalContribution *= textureLod(envMap, uv, 0.0).rgb; +#else + const vec3 kConstantEnvLightRadiance = vec3(0.15, 0.21, 0.3); + finalContribution *= kConstantEnvLightRadiance; +#endif + _payload.accumulation += finalContribution; +} + +#include +#include +#include +#include +#include +#include +#include +nbl_glsl_LightSample nbl_glsl_bsdf_cos_generate(in nbl_glsl_AnisotropicViewSurfaceInteraction interaction, in vec3 u, in BSDFNode bsdf, in float monochromeEta, out nbl_glsl_AnisotropicMicrofacetCache _cache) +{ + const float a = BSDFNode_getRoughness(bsdf); + const mat2x3 ior = BSDFNode_getEta(bsdf); + + // fresnel stuff for dielectrics + float orientedEta, rcpOrientedEta; + const bool viewerInsideMedium = nbl_glsl_getOrientedEtas(orientedEta,rcpOrientedEta,interaction.isotropic.NdotV,monochromeEta); + + nbl_glsl_LightSample smpl; + nbl_glsl_AnisotropicMicrofacetCache dummy; + switch (BSDFNode_getType(bsdf)) + { + case DIFFUSE_OP: + smpl = nbl_glsl_oren_nayar_cos_generate(interaction,u.xy,a*a); + break; + case CONDUCTOR_OP: + smpl = nbl_glsl_ggx_cos_generate(interaction,u.xy,a,a,_cache); + break; + default: + smpl = nbl_glsl_ggx_dielectric_cos_generate(interaction,u,a,a,monochromeEta,_cache); + break; + } + return smpl; +} + +vec3 nbl_glsl_bsdf_cos_remainder_and_pdf(out float pdf, in nbl_glsl_LightSample _sample, in nbl_glsl_AnisotropicViewSurfaceInteraction interaction, in BSDFNode bsdf, in float monochromeEta, in nbl_glsl_AnisotropicMicrofacetCache _cache) +{ + // are V and L on opposite sides of the surface? + const bool transmitted = nbl_glsl_isTransmissionPath(interaction.isotropic.NdotV,_sample.NdotL); + + // is the BSDF or BRDF, if it is then we make the dot products `abs` before `max(,0.0)` + const bool transmissive = BSDFNode_isBSDF(bsdf); + const float clampedNdotL = nbl_glsl_conditionalAbsOrMax(transmissive,_sample.NdotL,0.0); + const float clampedNdotV = nbl_glsl_conditionalAbsOrMax(transmissive,interaction.isotropic.NdotV,0.0); + + vec3 remainder; + + const float minimumProjVectorLen = 0.00000001; + if (clampedNdotV>minimumProjVectorLen && clampedNdotL>minimumProjVectorLen) + { + // fresnel stuff for conductors (but reflectance also doubles as albedo) + const mat2x3 ior = BSDFNode_getEta(bsdf); + const vec3 reflectance = BSDFNode_getReflectance(bsdf,_cache.isotropic.VdotH); + + // fresnel stuff for dielectrics + float orientedEta, rcpOrientedEta; + const bool viewerInsideMedium = nbl_glsl_getOrientedEtas(orientedEta,rcpOrientedEta,interaction.isotropic.NdotV,monochromeEta); + + // + const float VdotL = dot(interaction.isotropic.V.dir,_sample.L); + + // + const float a = max(BSDFNode_getRoughness(bsdf),0.0001); // TODO: @Crisspl 0-roughness still doesn't work! Also Beckmann has a weird dark rim instead as fresnel!? + const float a2 = a*a; + + // TODO: refactor into Material Compiler-esque thing + switch (BSDFNode_getType(bsdf)) + { + case DIFFUSE_OP: + remainder = reflectance*nbl_glsl_oren_nayar_cos_remainder_and_pdf_wo_clamps(pdf,a*a,VdotL,clampedNdotL,clampedNdotV); + break; + case CONDUCTOR_OP: + remainder = nbl_glsl_ggx_cos_remainder_and_pdf_wo_clamps(pdf,nbl_glsl_ggx_trowbridge_reitz(a2,_cache.isotropic.NdotH2),clampedNdotL,_sample.NdotL2,clampedNdotV,interaction.isotropic.NdotV_squared,reflectance,a2); + break; + default: + remainder = vec3(nbl_glsl_ggx_dielectric_cos_remainder_and_pdf(pdf, _sample, interaction.isotropic, _cache.isotropic, monochromeEta, a*a)); + break; + } + } + else + remainder = vec3(0.0); + return remainder; +} + +layout (constant_id = 0) const int MAX_DEPTH_LOG2 = 4; +layout (constant_id = 1) const int MAX_SAMPLES_LOG2 = 10; + + +#include + +mat2x3 rand3d(in uint protoDimension, in uint _sample, inout nbl_glsl_xoroshiro64star_state_t scramble_state) +{ + mat2x3 retval; + uint address = bitfieldInsert(protoDimension,_sample,MAX_DEPTH_LOG2,MAX_SAMPLES_LOG2); + for (int i=0; i<2u; i++) + { + uvec3 seqVal = texelFetch(sampleSequence,int(address)+i).xyz; + seqVal ^= uvec3(nbl_glsl_xoroshiro64star(scramble_state),nbl_glsl_xoroshiro64star(scramble_state),nbl_glsl_xoroshiro64star(scramble_state)); + retval[i] = vec3(seqVal)*uintBitsToFloat(0x2f800004u); + } + return retval; +} + + +void traceRay_extraShape(inout int objectID, inout float intersectionT, in vec3 origin, in vec3 direction); +int traceRay(inout float intersectionT, in vec3 origin, in vec3 direction) +{ + int objectID = -1; + +#define USE_RAY_QUERY +#ifdef USE_RAY_QUERY + rayQueryEXT rayQuery; + rayQueryInitializeEXT(rayQuery, topLevelAS, gl_RayFlagsNoneEXT, 0xFF, origin, 0.0, direction, 1000.0); + + // Start traversal: return false if traversal is complete + while(rayQueryProceedEXT(rayQuery)) + { + if(rayQueryGetIntersectionTypeEXT(rayQuery, false) == gl_RayQueryCandidateIntersectionAABBEXT) + { + int id = rayQueryGetIntersectionPrimitiveIndexEXT(rayQuery, false); + float t = Sphere_intersect(spheres[id],origin,direction); + bool reportIntersection = (t != nbl_glsl_FLT_NAN && t > 0 && t < intersectionT); + if(reportIntersection) + { + intersectionT = t; + objectID = id; + rayQueryGenerateIntersectionEXT(rayQuery, t); + } + } + } +#else + for (int i=0; i0.0 && t0.0; + // but if we allowed non-watertight transmitters (single water surface), it would make sense just to apply this line by itself + nbl_glsl_AnisotropicMicrofacetCache _cache; + validPath = validPath && nbl_glsl_calcAnisotropicMicrofacetCache(_cache, interaction, nee_sample, monochromeEta); + if (validPath) + { + float bsdfPdf; + neeContrib *= nbl_glsl_bsdf_cos_remainder_and_pdf(bsdfPdf,nee_sample,interaction,bsdf,monochromeEta,_cache)*throughput; + const float oc = bsdfPdf*rcpChoiceProb; + neeContrib /= 1.0/oc+oc/(lightPdf*lightPdf); // MIS weight + if (bsdfPdflumaContributionThreshold && traceRay(t,intersection+nee_sample.L*t*getStartTolerance(depth),nee_sample.L)==-1) + ray._payload.accumulation += neeContrib; + } + } + + // sample BSDF + float bsdfPdf; vec3 bsdfSampleL; + { + nbl_glsl_AnisotropicMicrofacetCache _cache; + nbl_glsl_LightSample bsdf_sample = nbl_glsl_bsdf_cos_generate(interaction,epsilon[1],bsdf,monochromeEta,_cache); + // the value of the bsdf divided by the probability of the sample being generated + throughput *= nbl_glsl_bsdf_cos_remainder_and_pdf(bsdfPdf,bsdf_sample,interaction,bsdf,monochromeEta,_cache); + // + bsdfSampleL = bsdf_sample.L; + } + + // additional threshold + const float lumaThroughputThreshold = lumaContributionThreshold; + if (bsdfPdf>bsdfPdfThreshold && getLuma(throughput)>lumaThroughputThreshold) + { + ray._payload.throughput = throughput; + ray._payload.otherTechniqueHeuristic = neeProbability/bsdfPdf; // numerically stable, don't touch + ray._payload.otherTechniqueHeuristic *= ray._payload.otherTechniqueHeuristic; + + // trace new ray + ray._immutable.origin = intersection+bsdfSampleL*(1.0/*kSceneSize*/)*getStartTolerance(depth); + ray._immutable.direction = bsdfSampleL; + #if POLYGON_METHOD==2 + ray._immutable.normalAtOrigin = interaction.isotropic.N; + ray._immutable.wasBSDFAtOrigin = isBSDF; + #endif + return true; + } + } + return false; +} + +void main() +{ + const ivec2 coords = getCoordinates(); + const vec2 texCoord = getTexCoords(); + + if (false == (all(lessThanEqual(ivec2(0),coords)) && all(greaterThan(imageSize(outImage),coords)))) { + return; + } + + if (((MAX_DEPTH-1)>>MAX_DEPTH_LOG2)>0 || ((SAMPLES-1)>>MAX_SAMPLES_LOG2)>0) + { + vec4 pixelCol = vec4(1.0,0.0,0.0,1.0); + imageStore(outImage, coords, pixelCol); + return; + } + + nbl_glsl_xoroshiro64star_state_t scramble_start_state = texelFetch(scramblebuf,coords,0).rg; + const vec2 pixOffsetParam = vec2(1.0)/vec2(textureSize(scramblebuf,0)); + + + const mat4 invMVP = inverse(cameraData.params.MVP); + + vec4 NDC = vec4(texCoord*vec2(2.0,-2.0)+vec2(-1.0,1.0),0.0,1.0); + vec3 camPos; + { + vec4 tmp = invMVP*NDC; + camPos = tmp.xyz/tmp.w; + NDC.z = 1.0; + } + + vec3 color = vec3(0.0); + float meanLumaSquared = 0.0; + // TODO: if we collapse the nested for loop, then all GPUs will get `MAX_DEPTH` factor speedup, not just NV with separate PC + for (int i=0; i5.0) + color = vec3(1.0,0.0,0.0); + #endif + + vec4 pixelCol = vec4(color, 1.0); + imageStore(outImage, coords, pixelCol); +} +/** TODO: Improving Rendering + +Now: +- Always MIS (path correlated reuse) +- Test MIS alpha (roughness) scheme + +Many Lights: +- Path Guiding +- Light Importance Lists/Classification +- Spatio-Temporal Reservoir Sampling + +Indirect Light: +- Bidirectional Path Tracing +- Uniform Path Sampling / Vertex Connection and Merging / Path Space Regularization + +Animations: +- A-SVGF / BMFR +**/ \ No newline at end of file diff --git a/56_RayQuery/config.json.template b/56_RayQuery/config.json.template new file mode 100644 index 000000000..f961745c1 --- /dev/null +++ b/56_RayQuery/config.json.template @@ -0,0 +1,28 @@ +{ + "enableParallelBuild": true, + "threadsPerBuildProcess" : 2, + "isExecuted": false, + "scriptPath": "", + "cmake": { + "configurations": [ "Release", "Debug", "RelWithDebInfo" ], + "buildModes": [], + "requiredOptions": [] + }, + "profiles": [ + { + "backend": "vulkan", + "platform": "windows", + "buildModes": [], + "runConfiguration": "Release", + "gpuArchitectures": [] + } + ], + "dependencies": [], + "data": [ + { + "dependencies": [], + "command": [""], + "outputs": [] + } + ] +} \ No newline at end of file diff --git a/56_RayQuery/litByRectangle.comp b/56_RayQuery/litByRectangle.comp new file mode 100644 index 000000000..829d03398 --- /dev/null +++ b/56_RayQuery/litByRectangle.comp @@ -0,0 +1,106 @@ +// 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 + +#version 460 core +#extension GL_GOOGLE_include_directive : require +#extension GL_EXT_ray_query : enable + +#define SPHERE_COUNT 8 +#define POLYGON_METHOD 0 // 0 area sampling, 1 solid angle sampling, 2 approximate projected solid angle sampling +#include "common.glsl" + + +#define RECTANGLE_COUNT 1 +const vec3 edge0 = normalize(vec3(2,0,-1)); +const vec3 edge1 = normalize(vec3(2,-5,4)); +Rectangle rectangles[RECTANGLE_COUNT] = { + Rectangle_Rectangle(vec3(-3.8,0.35,1.3),edge0*7.0,edge1*0.1,INVALID_ID_16BIT,0u) +}; + + +void traceRay_extraShape(inout int objectID, inout float intersectionT, in vec3 origin, in vec3 direction) +{ + for (int i=0; i0.0 && t +float nbl_glsl_light_deferred_pdf(in Light light, in Ray_t ray) +{ + const Rectangle rect = rectangles[Light_getObjectID(light)]; + + const vec3 L = ray._immutable.direction; +#if POLYGON_METHOD==0 + const float dist = ray._mutable.intersectionT; + return dist*dist/abs(dot(Rectangle_getNormalTimesArea(rect),L)); +#else + const ImmutableRay_t _immutable = ray._immutable; + const mat3 sphericalVertices = nbl_glsl_shapes_getSphericalTriangle(mat3(tri.vertex0,tri.vertex1,tri.vertex2),_immutable.origin); + #if POLYGON_METHOD==1 + const float rcpProb = nbl_glsl_shapes_SolidAngleOfTriangle(sphericalVertices); + // if `rcpProb` is NAN then the triangle's solid angle was close to 0.0 + return rcpProb>FLT_MIN ? (1.0/rcpProb):nbl_glsl_FLT_MAX; + #elif POLYGON_METHOD==2 + const float pdf = nbl_glsl_sampling_probProjectedSphericalTriangleSample(sphericalVertices,_immutable.normalAtOrigin,_immutable.wasBSDFAtOrigin,L); + // if `pdf` is NAN then the triangle's projected solid angle was close to 0.0, if its close to INF then the triangle was very small + return pdfFLT_MIN ? (1.0/rcpPdf):0.0; + + const vec3 N = Triangle_getNormalTimesArea(tri); + newRayMaxT = dot(N,tri.vertex0-origin)/dot(N,L); + return L; +#endif +} + + +uint getBSDFLightIDAndDetermineNormal(out vec3 normal, in uint objectID, in vec3 intersection) +{ + if (objectID0.0) + { + const float rcpDistance = inversesqrt(distanceSQ); + Z *= rcpDistance; + + const float cosThetaMax = sqrt(cosThetaMax2); + const float cosTheta = mix(1.0,cosThetaMax,xi.x); + + vec3 L = Z*cosTheta; + + const float cosTheta2 = cosTheta*cosTheta; + const float sinTheta = sqrt(1.0-cosTheta2); + float sinPhi,cosPhi; + nbl_glsl_sincos(2.0*nbl_glsl_PI*xi.y-nbl_glsl_PI,sinPhi,cosPhi); + mat2x3 XY = nbl_glsl_frisvad(Z); + + L += (XY[0]*cosPhi+XY[1]*sinPhi)*sinTheta; + + newRayMaxT = (cosTheta-sqrt(cosTheta2-cosThetaMax2))/rcpDistance; + pdf = 1.0/Sphere_getSolidAngle_impl(cosThetaMax); + return L; + } + pdf = 0.0; + return vec3(0.0,0.0,0.0); +} + +uint getBSDFLightIDAndDetermineNormal(out vec3 normal, in uint objectID, in vec3 intersection) +{ + Sphere sphere = spheres[objectID]; + normal = Sphere_getNormal(sphere,intersection); + return sphere.bsdfLightIDs; +} \ No newline at end of file diff --git a/56_RayQuery/litByTriangle.comp b/56_RayQuery/litByTriangle.comp new file mode 100644 index 000000000..1cd1d3ee3 --- /dev/null +++ b/56_RayQuery/litByTriangle.comp @@ -0,0 +1,105 @@ +// 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 + +#version 460 core +#extension GL_GOOGLE_include_directive : require +#extension GL_EXT_ray_query : enable + +#define SPHERE_COUNT 8 +#define POLYGON_METHOD 0 // 0 area sampling, 1 solid angle sampling, 2 approximate projected solid angle sampling +#include "common.glsl" + +#define TRIANGLE_COUNT 1 +Triangle triangles[TRIANGLE_COUNT] = { + Triangle_Triangle(mat3(vec3(-1.8,0.35,0.3),vec3(-1.2,0.35,0.0),vec3(-1.5,0.8,-0.3)),INVALID_ID_16BIT,0u) +}; + +void traceRay_extraShape(inout int objectID, inout float intersectionT, in vec3 origin, in vec3 direction) +{ + for (int i=0; i0.0 && t +float nbl_glsl_light_deferred_pdf(in Light light, in Ray_t ray) +{ + const Triangle tri = triangles[Light_getObjectID(light)]; + + const vec3 L = ray._immutable.direction; +#if POLYGON_METHOD==0 + const float dist = ray._mutable.intersectionT; + return dist*dist/abs(dot(Triangle_getNormalTimesArea(tri),L)); +#else + const ImmutableRay_t _immutable = ray._immutable; + const mat3 sphericalVertices = nbl_glsl_shapes_getSphericalTriangle(mat3(tri.vertex0,tri.vertex1,tri.vertex2),_immutable.origin); + #if POLYGON_METHOD==1 + const float rcpProb = nbl_glsl_shapes_SolidAngleOfTriangle(sphericalVertices); + // if `rcpProb` is NAN then the triangle's solid angle was close to 0.0 + return rcpProb>FLT_MIN ? (1.0/rcpProb):nbl_glsl_FLT_MAX; + #elif POLYGON_METHOD==2 + const float pdf = nbl_glsl_sampling_probProjectedSphericalTriangleSample(sphericalVertices,_immutable.normalAtOrigin,_immutable.wasBSDFAtOrigin,L); + // if `pdf` is NAN then the triangle's projected solid angle was close to 0.0, if its close to INF then the triangle was very small + return pdfFLT_MIN ? (1.0/rcpPdf):0.0; + + const vec3 N = Triangle_getNormalTimesArea(tri); + newRayMaxT = dot(N,tri.vertex0-origin)/dot(N,L); + return L; +#endif +} + + +uint getBSDFLightIDAndDetermineNormal(out vec3 normal, in uint objectID, in vec3 intersection) +{ + if (objectID + +#include "../common/CommonAPI.h" +#include "CCamera.hpp" +#include "nbl/ext/ScreenShot/ScreenShot.h" +#include "nbl/video/utilities/CDumbPresentationOracle.h" + +using namespace nbl; +using namespace core; +using namespace ui; + + +using namespace nbl; +using namespace core; +using namespace asset; +using namespace video; + +smart_refctd_ptr createHDRImageView(nbl::core::smart_refctd_ptr device, asset::E_FORMAT colorFormat, uint32_t width, uint32_t height) +{ + smart_refctd_ptr gpuImageViewColorBuffer; + { + IGPUImage::SCreationParams imgInfo; + imgInfo.format = colorFormat; + imgInfo.type = IGPUImage::ET_2D; + imgInfo.extent.width = width; + imgInfo.extent.height = height; + imgInfo.extent.depth = 1u; + imgInfo.mipLevels = 1u; + imgInfo.arrayLayers = 1u; + imgInfo.samples = asset::ICPUImage::ESCF_1_BIT; + imgInfo.flags = static_cast(0u); + imgInfo.usage = core::bitflag(asset::IImage::EUF_STORAGE_BIT) | asset::IImage::EUF_TRANSFER_SRC_BIT; + + // (Erfan -> Cyprian) + // auto image = device->createGPUImageOnDedMem(std::move(imgInfo),device->getDeviceLocalGPUMemoryReqs()); + auto image = device->createImage(std::move(imgInfo)); + auto imageMemoryReqs = image->getMemoryReqs(); + imageMemoryReqs.memoryTypeBits &= device->getPhysicalDevice()->getDeviceLocalMemoryTypeBits(); // getDeviceLocalMemoryTypeBits because of previous code getDeviceLocalGPUMemoryReqs + auto imageMem = device->allocate(imageMemoryReqs, image.get()); + + IGPUImageView::SCreationParams imgViewInfo; + imgViewInfo.image = std::move(image); + imgViewInfo.format = colorFormat; + imgViewInfo.viewType = IGPUImageView::ET_2D; + imgViewInfo.flags = static_cast(0u); + imgViewInfo.subresourceRange.aspectMask = IImage::E_ASPECT_FLAGS::EAF_COLOR_BIT; + imgViewInfo.subresourceRange.baseArrayLayer = 0u; + imgViewInfo.subresourceRange.baseMipLevel = 0u; + imgViewInfo.subresourceRange.layerCount = 1u; + imgViewInfo.subresourceRange.levelCount = 1u; + + gpuImageViewColorBuffer = device->createImageView(std::move(imgViewInfo)); + } + + return gpuImageViewColorBuffer; +} + +struct ShaderParameters +{ + const uint32_t MaxDepthLog2 = 4; //5 + const uint32_t MaxSamplesLog2 = 10; //18 +} kShaderParameters; + +enum E_LIGHT_GEOMETRY +{ + ELG_SPHERE, + ELG_TRIANGLE, + ELG_RECTANGLE +}; + +struct DispatchInfo_t +{ + uint32_t workGroupCount[3]; +}; + +_NBL_STATIC_INLINE_CONSTEXPR uint32_t DEFAULT_WORK_GROUP_SIZE = 16u; + +DispatchInfo_t getDispatchInfo(uint32_t imgWidth, uint32_t imgHeight) { + DispatchInfo_t ret = {}; + ret.workGroupCount[0] = (uint32_t)core::ceil((float)imgWidth / (float)DEFAULT_WORK_GROUP_SIZE); + ret.workGroupCount[1] = (uint32_t)core::ceil((float)imgHeight / (float)DEFAULT_WORK_GROUP_SIZE); + ret.workGroupCount[2] = 1; + return ret; +} + +class RayQuerySampleApp : public ApplicationBase +{ + constexpr static uint32_t WIN_W = 1280u; + constexpr static uint32_t WIN_H = 720u; + constexpr static uint32_t FRAMES_IN_FLIGHT = 5u; + static constexpr uint64_t MAX_TIMEOUT = 99999999999999ull; + + core::smart_refctd_ptr windowManager; + core::smart_refctd_ptr window; + core::smart_refctd_ptr windowCb; + core::smart_refctd_ptr apiConnection; + core::smart_refctd_ptr surface; + core::smart_refctd_ptr utilities; + core::smart_refctd_ptr logicalDevice; + video::IPhysicalDevice* physicalDevice; + std::array queues; + core::smart_refctd_ptr swapchain; + core::smart_refctd_ptr renderpass; + core::smart_refctd_dynamic_array> fbos; + std::array, CommonAPI::InitOutput::MaxFramesInFlight>, CommonAPI::InitOutput::MaxQueuesCount> commandPools; + core::smart_refctd_ptr system; + core::smart_refctd_ptr assetManager; + video::IGPUObjectFromAssetConverter::SParams cpu2gpuParams; + core::smart_refctd_ptr logger; + core::smart_refctd_ptr inputSystem; + video::IGPUObjectFromAssetConverter cpu2gpu; + + int32_t m_resourceIx = -1; + uint32_t m_acquiredNextFBO = {}; + + CDumbPresentationOracle oracle; + + // polling for events! + CommonAPI::InputSystem::ChannelReader mouse; + CommonAPI::InputSystem::ChannelReader keyboard; + + core::smart_refctd_ptr frameUploadDataCompleteFence[FRAMES_IN_FLIGHT] = { nullptr }; + core::smart_refctd_ptr frameComplete[FRAMES_IN_FLIGHT] = { nullptr }; + core::smart_refctd_ptr imageAcquire[FRAMES_IN_FLIGHT] = { nullptr }; + core::smart_refctd_ptr renderFinished[FRAMES_IN_FLIGHT] = { nullptr }; + core::smart_refctd_ptr frameUploadDataCompleteSemaphore[FRAMES_IN_FLIGHT] = { nullptr }; + + core::smart_refctd_ptr cmdbuf[FRAMES_IN_FLIGHT]; // from graphics + + Camera cam; + + core::smart_refctd_ptr gpuubo = nullptr; + core::smart_refctd_ptr gpuEnvmapImageView = nullptr; + core::smart_refctd_ptr gpuScrambleImageView; + + core::smart_refctd_ptr gpuComputePipeline = nullptr; + DispatchInfo_t dispatchInfo = {}; + + core::smart_refctd_ptr outHDRImageViews[CommonAPI::InitOutput::MaxSwapChainImageCount] = {}; + + core::smart_refctd_ptr descriptorSets0[CommonAPI::InitOutput::MaxSwapChainImageCount] = {}; + core::smart_refctd_ptr descriptorSet2 = nullptr; + core::smart_refctd_ptr uboDescriptorSet1 = nullptr; + + core::smart_refctd_ptr aabbsBuffer = nullptr; + core::smart_refctd_ptr gpuBlas = nullptr; + core::smart_refctd_ptr gpuBlas2 = nullptr; // Built via CPUObject To GPUObject operations and utility + core::smart_refctd_ptr gpuTlas = nullptr; + core::smart_refctd_ptr instancesBuffer = nullptr; + + core::smart_refctd_ptr gpuSequenceBufferView = nullptr; + + core::smart_refctd_ptr sampler0 = nullptr; + core::smart_refctd_ptr sampler1 = nullptr; + + core::smart_refctd_ptr gpuSequenceBuffer = nullptr; + + core::smart_refctd_ptr spheresBuffer = nullptr; + + struct SBasicViewParametersAligned + { + SBasicViewParameters uboData; + }; + +public: + void setWindow(core::smart_refctd_ptr&& wnd) override + { + window = std::move(wnd); + } + nbl::ui::IWindow* getWindow() override + { + return window.get(); + } + void setSystem(core::smart_refctd_ptr&& system) override + { + system = std::move(system); + } + + APP_CONSTRUCTOR(RayQuerySampleApp); + + void onAppInitialized_impl() override + { + const auto swapchainImageUsage = static_cast(asset::IImage::EUF_COLOR_ATTACHMENT_BIT | asset::IImage::EUF_TRANSFER_DST_BIT | asset::IImage::EUF_TRANSFER_SRC_BIT); + + CommonAPI::InitParams initParams; + initParams.window = core::smart_refctd_ptr(window); + initParams.apiType = video::EAT_VULKAN; + initParams.appName = { _NBL_APP_NAME_ }; + initParams.framesInFlight = FRAMES_IN_FLIGHT; + initParams.windowWidth = WIN_W; + initParams.windowHeight = WIN_H; + initParams.swapchainImageCount = 2u; + initParams.swapchainImageUsage = swapchainImageUsage; + initParams.depthFormat = asset::EF_D32_SFLOAT; + auto initOutput = CommonAPI::InitWithRaytracingExt(std::move(initParams)); + + system = std::move(initOutput.system); + window = std::move(initParams.window); + windowCb = std::move(initParams.windowCb); + apiConnection = std::move(initOutput.apiConnection); + surface = std::move(initOutput.surface); + physicalDevice = std::move(initOutput.physicalDevice); + logicalDevice = std::move(initOutput.logicalDevice); + utilities = std::move(initOutput.utilities); + queues = std::move(initOutput.queues); + renderpass = std::move(initOutput.renderToSwapchainRenderpass); + commandPools = std::move(initOutput.commandPools); + assetManager = std::move(initOutput.assetManager); + cpu2gpuParams = std::move(initOutput.cpu2gpuParams); + logger = std::move(initOutput.logger); + inputSystem = std::move(initOutput.inputSystem); + + CommonAPI::createSwapchain(std::move(logicalDevice), initOutput.swapchainCreationParams, WIN_W, WIN_H, swapchain); + assert(swapchain); + fbos = CommonAPI::createFBOWithSwapchainImages( + swapchain->getImageCount(), WIN_W, WIN_H, + logicalDevice, swapchain, renderpass, + asset::EF_D32_SFLOAT + ); + auto graphicsQueue = queues[CommonAPI::InitOutput::EQT_GRAPHICS]; + auto computeQueue = queues[CommonAPI::InitOutput::EQT_GRAPHICS]; + auto graphicsCommandPools = commandPools[CommonAPI::InitOutput::EQT_GRAPHICS]; + auto computeCommandPools = commandPools[CommonAPI::InitOutput::EQT_COMPUTE]; + + video::IGPUObjectFromAssetConverter cpu2gpu; + for (uint32_t i = 0u; i < FRAMES_IN_FLIGHT; i++) + logicalDevice->createCommandBuffers(graphicsCommandPools[i].get(), video::IGPUCommandBuffer::EL_PRIMARY, 1, cmdbuf+i); + + core::smart_refctd_ptr descriptorPool = nullptr; + { + video::IDescriptorPool::SCreateInfo createInfo = {}; + createInfo.maxSets = CommonAPI::InitOutput::MaxSwapChainImageCount+2; + createInfo.maxDescriptorCount[static_cast(asset::IDescriptor::E_TYPE::ET_STORAGE_BUFFER)] = 1; + createInfo.maxDescriptorCount[static_cast(asset::IDescriptor::E_TYPE::ET_STORAGE_IMAGE)] = CommonAPI::InitOutput::MaxSwapChainImageCount; + createInfo.maxDescriptorCount[static_cast(asset::IDescriptor::E_TYPE::ET_COMBINED_IMAGE_SAMPLER)] = 2; + createInfo.maxDescriptorCount[static_cast(asset::IDescriptor::E_TYPE::ET_UNIFORM_TEXEL_BUFFER)] = 1; + createInfo.maxDescriptorCount[static_cast(asset::IDescriptor::E_TYPE::ET_UNIFORM_BUFFER)] = 1; + createInfo.maxDescriptorCount[static_cast(asset::IDescriptor::E_TYPE::ET_ACCELERATION_STRUCTURE)] = 1; + + descriptorPool = logicalDevice->createDescriptorPool(std::move(createInfo)); + } + + // Initialize Spheres + constexpr uint32_t SphereCount = 9u; + constexpr uint32_t INVALID_ID_16BIT = 0xffffu; + + struct alignas(16) Sphere + { + Sphere() + : position(0.0f, 0.0f, 0.0f) + , radius2(0.0f) + { + bsdfLightIDs = core::bitfieldInsert(0u,INVALID_ID_16BIT,16,16); + } + + Sphere(core::vector3df _position, float _radius, uint32_t _bsdfID, uint32_t _lightID) + { + position = _position; + radius2 = _radius*_radius; + bsdfLightIDs = core::bitfieldInsert(_bsdfID,_lightID,16,16); + } + + IGPUAccelerationStructure::AABB_Position getAABB() const + { + float radius = core::sqrt(radius2); + return IGPUAccelerationStructure::AABB_Position(position-core::vector3df(radius, radius, radius), position+core::vector3df(radius, radius, radius)); + } + + core::vector3df position; + float radius2; + uint32_t bsdfLightIDs; + }; + + Sphere spheres[SphereCount] = {}; + spheres[0] = Sphere(core::vector3df(0.0,-100.5,-1.0), 100.0, 0u, INVALID_ID_16BIT); + spheres[1] = Sphere(core::vector3df(3.0,0.0,-1.0), 0.5, 1u, INVALID_ID_16BIT); + spheres[2] = Sphere(core::vector3df(0.0,0.0,-1.0), 0.5, 2u, INVALID_ID_16BIT); + spheres[3] = Sphere(core::vector3df(-3.0,0.0,-1.0), 0.5, 3u, INVALID_ID_16BIT); + spheres[4] = Sphere(core::vector3df(3.0,0.0,1.0), 0.5, 4u, INVALID_ID_16BIT); + spheres[5] = Sphere(core::vector3df(0.0,0.0,1.0), 0.5, 4u, INVALID_ID_16BIT); + spheres[6] = Sphere(core::vector3df(-3.0,0.0,1.0), 0.5, 5u, INVALID_ID_16BIT); + spheres[7] = Sphere(core::vector3df(0.5,1.0,0.5), 0.5, 6u, INVALID_ID_16BIT); + spheres[8] = Sphere(core::vector3df(-1.5,1.5,0.0), 0.3, INVALID_ID_16BIT, 0u); + + // Create Spheres Buffer + uint32_t spheresBufferSize = sizeof(Sphere) * SphereCount; + + { + IGPUBuffer::SCreationParams params = {}; + params.size = spheresBufferSize; // (Erfan->Cyprian) See How I moved "createDeviceLocalGPUBufferOnDedMem" second parameter to params.size? IGPUBuffer::SCreationParams::size is very important to be filled unlike before + params.usage = core::bitflag(asset::IBuffer::EUF_STORAGE_BUFFER_BIT) | asset::IBuffer::EUF_TRANSFER_DST_BIT; + spheresBuffer = logicalDevice->createBuffer(std::move(params)); + auto bufferReqs = spheresBuffer->getMemoryReqs(); + bufferReqs.memoryTypeBits &= logicalDevice->getPhysicalDevice()->getDeviceLocalMemoryTypeBits(); // (Erfan->Cyprian) I used `getDeviceLocalMemoryTypeBits` because of previous createDeviceLocalGPUBufferOnDedMem (Focus on DeviceLocal Part) + auto spheresBufferMem = logicalDevice->allocate(bufferReqs, spheresBuffer.get()); + utilities->updateBufferRangeViaStagingBufferAutoSubmit(asset::SBufferRange{0u,spheresBufferSize,spheresBuffer}, spheres, graphicsQueue); + } + +#define TEST_CPU_2_GPU_BLAS +#ifdef TEST_CPU_2_GPU_BLAS + // Acceleration Structure Test + // Create + Build BLAS (CPU2GPU Version) + { + struct AABB { + IGPUAccelerationStructure::AABB_Position aabb; + }; + const uint32_t aabbsCount = SphereCount / 2u; + uint32_t aabbsBufferSize = sizeof(AABB) * aabbsCount; + + AABB aabbs[aabbsCount] = {}; + for(uint32_t i = 0; i < aabbsCount; ++i) + { + aabbs[i].aabb = spheres[i].getAABB(); + } + + // auto raytracingFlags = core::bitflag(asset::IBuffer::EUF_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT) | asset::IBuffer::EUF_STORAGE_BUFFER_BIT; + // | asset::IBuffer::EUF_TRANSFER_DST_BIT | asset::IBuffer::EUF_SHADER_DEVICE_ADDRESS_BIT + core::smart_refctd_ptr aabbsBuffer = core::make_smart_refctd_ptr(aabbsBufferSize); + memcpy(aabbsBuffer->getPointer(), aabbs, aabbsBufferSize); + + ICPUAccelerationStructure::SCreationParams asCreateParams; + asCreateParams.type = ICPUAccelerationStructure::ET_BOTTOM_LEVEL; + asCreateParams.flags = ICPUAccelerationStructure::ECF_NONE; + core::smart_refctd_ptr cpuBlas = ICPUAccelerationStructure::create(std::move(asCreateParams)); + + using HostGeom = ICPUAccelerationStructure::HostBuildGeometryInfo::Geom; + core::smart_refctd_dynamic_array geometries = core::make_refctd_dynamic_array>(1u); + + HostGeom & simpleGeom = geometries->operator[](0u); + simpleGeom.type = IAccelerationStructure::EGT_AABBS; + simpleGeom.flags = IAccelerationStructure::EGF_OPAQUE_BIT; + simpleGeom.data.aabbs.data.offset = 0u; + simpleGeom.data.aabbs.data.buffer = aabbsBuffer; + simpleGeom.data.aabbs.stride = sizeof(AABB); + + ICPUAccelerationStructure::HostBuildGeometryInfo buildInfo; + buildInfo.type = asCreateParams.type; + buildInfo.buildFlags = ICPUAccelerationStructure::EBF_PREFER_FAST_TRACE_BIT; + buildInfo.buildMode = ICPUAccelerationStructure::EBM_BUILD; + buildInfo.geometries = geometries; + + core::smart_refctd_dynamic_array buildRangeInfos = core::make_refctd_dynamic_array>(1u); + ICPUAccelerationStructure::BuildRangeInfo & firstBuildRangeInfo = buildRangeInfos->operator[](0u); + firstBuildRangeInfo.primitiveCount = aabbsCount; + firstBuildRangeInfo.primitiveOffset = 0u; + firstBuildRangeInfo.firstVertex = 0u; + firstBuildRangeInfo.transformOffset = 0u; + + cpuBlas->setBuildInfoAndRanges(std::move(buildInfo), buildRangeInfos); + + // Build BLAS + { + cpu2gpuParams.beginCommandBuffers(); + gpuBlas2 = cpu2gpu.getGPUObjectsFromAssets(&cpuBlas, &cpuBlas + 1u, cpu2gpuParams)->front(); + cpu2gpuParams.waitForCreationToComplete(); + } + } +#endif + + // Create + Build BLAS + { + // Build BLAS with AABBS + const uint32_t aabbsCount = SphereCount; + + struct AABB { + IGPUAccelerationStructure::AABB_Position aabb; + }; + + AABB aabbs[aabbsCount] = {}; + for(uint32_t i = 0; i < aabbsCount; ++i) + { + aabbs[i].aabb = spheres[i].getAABB(); + } + auto raytracingFlags = core::bitflag(asset::IBuffer::EUF_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT) | asset::IBuffer::EUF_STORAGE_BUFFER_BIT; + uint32_t aabbsBufferSize = sizeof(AABB) * aabbsCount; + + { + IGPUBuffer::SCreationParams params = {}; + params.size = aabbsBufferSize; + params.usage = raytracingFlags | asset::IBuffer::EUF_TRANSFER_DST_BIT | asset::IBuffer::EUF_SHADER_DEVICE_ADDRESS_BIT; + aabbsBuffer = logicalDevice->createBuffer(std::move(params)); + auto bufferReqs = aabbsBuffer->getMemoryReqs(); + bufferReqs.memoryTypeBits &= logicalDevice->getPhysicalDevice()->getDeviceLocalMemoryTypeBits(); + auto aabbBufferMem = logicalDevice->allocate(bufferReqs, aabbsBuffer.get(), IDeviceMemoryAllocation::EMAF_DEVICE_ADDRESS_BIT); + // (Erfan->Cyprian) -> I passed `IDeviceMemoryAllocation::EMAF_DEVICE_ADDRESS_BIT` as a third parameter to the allocate function because the buffer needs the usage `EUF_SHADER_DEVICE_ADDRESS_BIT` + // You don't have to worry about it, it's only used in this example + utilities->updateBufferRangeViaStagingBufferAutoSubmit(asset::SBufferRange{0u,aabbsBufferSize,aabbsBuffer}, aabbs, graphicsQueue); + } + + using DeviceGeom = IGPUAccelerationStructure::DeviceBuildGeometryInfo::Geometry; + + DeviceGeom simpleGeom = {}; + simpleGeom.type = IAccelerationStructure::EGT_AABBS; + simpleGeom.flags = IAccelerationStructure::EGF_OPAQUE_BIT; + simpleGeom.data.aabbs.data.offset = 0u; + simpleGeom.data.aabbs.data.buffer = aabbsBuffer; + simpleGeom.data.aabbs.stride = sizeof(AABB); + + IGPUAccelerationStructure::DeviceBuildGeometryInfo blasBuildInfo = {}; + blasBuildInfo.type = IGPUAccelerationStructure::ET_BOTTOM_LEVEL; + blasBuildInfo.buildFlags = IGPUAccelerationStructure::EBF_PREFER_FAST_TRACE_BIT; + blasBuildInfo.buildMode = IGPUAccelerationStructure::EBM_BUILD; + blasBuildInfo.srcAS = nullptr; + blasBuildInfo.dstAS = nullptr; + blasBuildInfo.geometries = core::SRange(&simpleGeom, &simpleGeom + 1u); + blasBuildInfo.scratchAddr = {}; + + // Get BuildSizes + IGPUAccelerationStructure::BuildSizes buildSizes = {}; + { + std::vector maxPrimCount(1u); + maxPrimCount[0] = aabbsCount; + buildSizes = logicalDevice->getAccelerationStructureBuildSizes(blasBuildInfo, maxPrimCount.data()); + } + + { + core::smart_refctd_ptr asBuffer; + IGPUBuffer::SCreationParams params = {}; + params.size = buildSizes.accelerationStructureSize; + params.usage = core::bitflag(asset::IBuffer::EUF_SHADER_DEVICE_ADDRESS_BIT) | asset::IBuffer::EUF_ACCELERATION_STRUCTURE_STORAGE_BIT; + asBuffer = logicalDevice->createBuffer(std::move(params)); + auto bufferReqs = asBuffer->getMemoryReqs(); + bufferReqs.memoryTypeBits &= logicalDevice->getPhysicalDevice()->getDeviceLocalMemoryTypeBits(); + auto asBufferMem = logicalDevice->allocate(bufferReqs, asBuffer.get(), IDeviceMemoryAllocation::EMAF_DEVICE_ADDRESS_BIT); + + IGPUAccelerationStructure::SCreationParams blasParams = {}; + blasParams.type = IGPUAccelerationStructure::ET_BOTTOM_LEVEL; + blasParams.flags = IGPUAccelerationStructure::ECF_NONE; + blasParams.bufferRange.buffer = asBuffer; + blasParams.bufferRange.offset = 0u; + blasParams.bufferRange.size = buildSizes.accelerationStructureSize; + gpuBlas = logicalDevice->createAccelerationStructure(std::move(blasParams)); + } + + // Allocate ScratchBuffer + core::smart_refctd_ptr scratchBuffer; + { + IGPUBuffer::SCreationParams params = {}; + params.size = buildSizes.buildScratchSize; + params.usage = core::bitflag(asset::IBuffer::EUF_SHADER_DEVICE_ADDRESS_BIT) | asset::IBuffer::EUF_STORAGE_BUFFER_BIT; + scratchBuffer = logicalDevice->createBuffer(std::move(params)); + auto bufferReqs = scratchBuffer->getMemoryReqs(); + bufferReqs.memoryTypeBits &= logicalDevice->getPhysicalDevice()->getDeviceLocalMemoryTypeBits(); + auto scratchBufferMem = logicalDevice->allocate(bufferReqs, scratchBuffer.get(), IDeviceMemoryAllocation::EMAF_DEVICE_ADDRESS_BIT); + } + + // Complete BLAS Build Info + { + blasBuildInfo.dstAS = gpuBlas.get(); + blasBuildInfo.scratchAddr.buffer = scratchBuffer; + blasBuildInfo.scratchAddr.offset = 0u; + } + + IGPUAccelerationStructure::BuildRangeInfo firstBuildRangeInfos[1u]; + firstBuildRangeInfos[0].primitiveCount = aabbsCount; + firstBuildRangeInfos[0].primitiveOffset = 0u; + firstBuildRangeInfos[0].firstVertex = 0u; + firstBuildRangeInfos[0].transformOffset = 0u; + IGPUAccelerationStructure::BuildRangeInfo* pRangeInfos[1u]; + pRangeInfos[0] = firstBuildRangeInfos; + // pRangeInfos[1] = &secondBuildRangeInfos; + + // Build BLAS + { + utilities->buildAccelerationStructures(computeQueue, core::SRange(&blasBuildInfo, &blasBuildInfo + 1u), pRangeInfos); + } + } + + // Create + Build TLAS + { + struct Instance { + IGPUAccelerationStructure::Instance instance; + }; + + const uint32_t instancesCount = 1u; + Instance instances[instancesCount] = {}; + core::matrix3x4SIMD identity; + instances[0].instance.mat = identity; + instances[0].instance.instanceCustomIndex = 0u; + instances[0].instance.mask = 0xFF; + instances[0].instance.instanceShaderBindingTableRecordOffset = 0u; + instances[0].instance.flags = IAccelerationStructure::EIF_TRIANGLE_FACING_CULL_DISABLE_BIT; +#ifdef TEST_CPU_2_GPU_BLAS + instances[0].instance.accelerationStructureReference = gpuBlas2->getReferenceForDeviceOperations(); +#else + instances[0].instance.accelerationStructureReference = gpuBlas->getReferenceForDeviceOperations(); +#endif + auto raytracingFlags = core::bitflag(asset::IBuffer::EUF_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT) | asset::IBuffer::EUF_STORAGE_BUFFER_BIT; + + uint32_t instancesBufferSize = sizeof(Instance); + { + IGPUBuffer::SCreationParams params = {}; + params.size = instancesBufferSize; + params.usage = raytracingFlags | asset::IBuffer::EUF_TRANSFER_DST_BIT | asset::IBuffer::EUF_SHADER_DEVICE_ADDRESS_BIT; + instancesBuffer = logicalDevice->createBuffer(std::move(params)); + auto bufferReqs = instancesBuffer->getMemoryReqs(); + bufferReqs.memoryTypeBits &= logicalDevice->getPhysicalDevice()->getDeviceLocalMemoryTypeBits(); + auto instancesBufferMem = logicalDevice->allocate(bufferReqs, instancesBuffer.get(), IDeviceMemoryAllocation::EMAF_DEVICE_ADDRESS_BIT); + utilities->updateBufferRangeViaStagingBufferAutoSubmit(asset::SBufferRange{0u,instancesBufferSize,instancesBuffer}, instances, graphicsQueue); + } + + using DeviceGeom = IGPUAccelerationStructure::DeviceBuildGeometryInfo::Geometry; + + DeviceGeom blasInstancesGeom = {}; + blasInstancesGeom.type = IAccelerationStructure::EGT_INSTANCES; + blasInstancesGeom.flags = IAccelerationStructure::EGF_NONE; + blasInstancesGeom.data.instances.data.offset = 0u; + blasInstancesGeom.data.instances.data.buffer = instancesBuffer; + + IGPUAccelerationStructure::DeviceBuildGeometryInfo tlasBuildInfo = {}; + tlasBuildInfo.type = IGPUAccelerationStructure::ET_TOP_LEVEL; + tlasBuildInfo.buildFlags = IGPUAccelerationStructure::EBF_PREFER_FAST_TRACE_BIT; + tlasBuildInfo.buildMode = IGPUAccelerationStructure::EBM_BUILD; + tlasBuildInfo.srcAS = nullptr; + tlasBuildInfo.dstAS = nullptr; + tlasBuildInfo.geometries = core::SRange(&blasInstancesGeom, &blasInstancesGeom + 1u); + tlasBuildInfo.scratchAddr = {}; + + // Get BuildSizes + IGPUAccelerationStructure::BuildSizes buildSizes = {}; + { + std::vector maxPrimCount(1u); + maxPrimCount[0] = instancesCount; + buildSizes = logicalDevice->getAccelerationStructureBuildSizes(tlasBuildInfo, maxPrimCount.data()); + } + + { + core::smart_refctd_ptr asBuffer; + IGPUBuffer::SCreationParams params = {}; + params.size = buildSizes.accelerationStructureSize; + params.usage = core::bitflag(asset::IBuffer::EUF_SHADER_DEVICE_ADDRESS_BIT) | asset::IBuffer::EUF_ACCELERATION_STRUCTURE_STORAGE_BIT; + asBuffer = logicalDevice->createBuffer(std::move(params)); + auto bufferReqs = asBuffer->getMemoryReqs(); + bufferReqs.memoryTypeBits &= logicalDevice->getPhysicalDevice()->getDeviceLocalMemoryTypeBits(); + auto asBufferMem = logicalDevice->allocate(bufferReqs, asBuffer.get(), IDeviceMemoryAllocation::EMAF_DEVICE_ADDRESS_BIT); + + IGPUAccelerationStructure::SCreationParams tlasParams = {}; + tlasParams.type = IGPUAccelerationStructure::ET_TOP_LEVEL; + tlasParams.flags = IGPUAccelerationStructure::ECF_NONE; + tlasParams.bufferRange.buffer = asBuffer; + tlasParams.bufferRange.offset = 0u; + tlasParams.bufferRange.size = buildSizes.accelerationStructureSize; + gpuTlas = logicalDevice->createAccelerationStructure(std::move(tlasParams)); + } + + // Allocate ScratchBuffer + core::smart_refctd_ptr scratchBuffer; + { + IGPUBuffer::SCreationParams params = {}; + params.size = buildSizes.buildScratchSize; + params.usage = core::bitflag(asset::IBuffer::EUF_SHADER_DEVICE_ADDRESS_BIT) | asset::IBuffer::EUF_STORAGE_BUFFER_BIT; + scratchBuffer = logicalDevice->createBuffer(std::move(params)); + auto bufferReqs = scratchBuffer->getMemoryReqs(); + bufferReqs.memoryTypeBits &= logicalDevice->getPhysicalDevice()->getDeviceLocalMemoryTypeBits(); + auto scratchBufferMem = logicalDevice->allocate(bufferReqs, scratchBuffer.get(), IDeviceMemoryAllocation::EMAF_DEVICE_ADDRESS_BIT); + } + + // Complete BLAS Build Info + { + tlasBuildInfo.dstAS = gpuTlas.get(); + tlasBuildInfo.scratchAddr.buffer = scratchBuffer; + tlasBuildInfo.scratchAddr.offset = 0u; + } + + IGPUAccelerationStructure::BuildRangeInfo firstBuildRangeInfos[1u]; + firstBuildRangeInfos[0].primitiveCount = instancesCount; + firstBuildRangeInfos[0].primitiveOffset = 0u; + firstBuildRangeInfos[0].firstVertex = 0u; + firstBuildRangeInfos[0].transformOffset = 0u; + IGPUAccelerationStructure::BuildRangeInfo* pRangeInfos[1u]; + pRangeInfos[0] = firstBuildRangeInfos; + + // Build TLAS + { + utilities->buildAccelerationStructures(computeQueue, core::SRange(&tlasBuildInfo, &tlasBuildInfo + 1u), pRangeInfos); + } + } + + + // Camera + core::vectorSIMDf cameraPosition(0, 5, -10); + matrix4SIMD proj = matrix4SIMD::buildProjectionMatrixPerspectiveFovRH(core::radians(60.0f), video::ISurface::getTransformedAspectRatio(swapchain->getPreTransform(), WIN_W, WIN_H), 0.01f, 500.0f); + cam = Camera(cameraPosition, core::vectorSIMDf(0, 0, 0), proj); + + IGPUDescriptorSetLayout::SBinding descriptorSet0Bindings[] = + { + { 0u, asset::IDescriptor::E_TYPE::ET_STORAGE_IMAGE, video::IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, IShader::ESS_COMPUTE, 1u, nullptr }, + }; + IGPUDescriptorSetLayout::SBinding uboBinding {0, asset::IDescriptor::E_TYPE::ET_UNIFORM_BUFFER, video::IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, IShader::ESS_COMPUTE, 1u, nullptr}; + IGPUDescriptorSetLayout::SBinding descriptorSet3Bindings[] = { + { 0u, asset::IDescriptor::E_TYPE::ET_COMBINED_IMAGE_SAMPLER, video::IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, IShader::ESS_COMPUTE, 1u, nullptr }, + { 1u, asset::IDescriptor::E_TYPE::ET_UNIFORM_TEXEL_BUFFER, video::IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, IShader::ESS_COMPUTE, 1u, nullptr }, + { 2u, asset::IDescriptor::E_TYPE::ET_COMBINED_IMAGE_SAMPLER, video::IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, IShader::ESS_COMPUTE, 1u, nullptr }, + { 3u, asset::IDescriptor::E_TYPE::ET_ACCELERATION_STRUCTURE, video::IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, IShader::ESS_COMPUTE, 1u, nullptr }, + { 4u, asset::IDescriptor::E_TYPE::ET_STORAGE_BUFFER, video::IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, IShader::ESS_COMPUTE, 1u, nullptr } + }; + + auto gpuDescriptorSetLayout0 = logicalDevice->createDescriptorSetLayout(descriptorSet0Bindings, descriptorSet0Bindings + 1u); + auto gpuDescriptorSetLayout1 = logicalDevice->createDescriptorSetLayout(&uboBinding, &uboBinding + 1u); + auto gpuDescriptorSetLayout2 = logicalDevice->createDescriptorSetLayout(descriptorSet3Bindings, descriptorSet3Bindings+5u); + + auto createGpuResources = [&](std::string pathToShader) -> core::smart_refctd_ptr + { + asset::IAssetLoader::SAssetLoadParams params{}; + params.logger = logger.get(); + //params.relativeDir = tmp.c_str(); + auto spec = assetManager->getAsset(pathToShader,params).getContents(); + + if (spec.empty()) + assert(false); + + auto cpuComputeSpecializedShader = core::smart_refctd_ptr_static_cast(*spec.begin()); + + ISpecializedShader::SInfo info = cpuComputeSpecializedShader->getSpecializationInfo(); + info.m_backingBuffer = core::make_smart_refctd_ptr(sizeof(ShaderParameters)); + memcpy(info.m_backingBuffer->getPointer(),&kShaderParameters,sizeof(ShaderParameters)); + info.m_entries = core::make_refctd_dynamic_array>(2u); + for (uint32_t i=0; i<2; i++) + info.m_entries->operator[](i) = {i,i*sizeof(uint32_t),sizeof(uint32_t)}; + + + cpuComputeSpecializedShader->setSpecializationInfo(std::move(info)); + + auto gpuComputeSpecializedShader = cpu2gpu.getGPUObjectsFromAssets(&cpuComputeSpecializedShader, &cpuComputeSpecializedShader + 1, cpu2gpuParams)->front(); + + auto gpuPipelineLayout = logicalDevice->createPipelineLayout(nullptr, nullptr, core::smart_refctd_ptr(gpuDescriptorSetLayout0), core::smart_refctd_ptr(gpuDescriptorSetLayout1), core::smart_refctd_ptr(gpuDescriptorSetLayout2), nullptr); + + auto gpuPipeline = logicalDevice->createComputePipeline(nullptr, std::move(gpuPipelineLayout), std::move(gpuComputeSpecializedShader)); + + return gpuPipeline; + }; + + E_LIGHT_GEOMETRY lightGeom = ELG_SPHERE; + constexpr const char* shaderPaths[] = {"../litBySphere.comp","../litByTriangle.comp","../litByRectangle.comp"}; + gpuComputePipeline = createGpuResources(shaderPaths[lightGeom]); + + dispatchInfo = getDispatchInfo(WIN_W, WIN_H); + + auto createImageView = [&](std::string pathToOpenEXRHDRIImage) + { + auto pathToTexture = pathToOpenEXRHDRIImage; + IAssetLoader::SAssetLoadParams lp(0ull, nullptr, IAssetLoader::ECF_DONT_CACHE_REFERENCES); + auto cpuTexture = assetManager->getAsset(pathToTexture, lp); + auto cpuTextureContents = cpuTexture.getContents(); + assert(!cpuTextureContents.empty()); + auto cpuImage = core::smart_refctd_ptr_static_cast(*cpuTextureContents.begin()); + cpuImage->setImageUsageFlags(IImage::E_USAGE_FLAGS::EUF_SAMPLED_BIT); + + ICPUImageView::SCreationParams viewParams; + viewParams.flags = static_cast(0u); + viewParams.image = cpuImage; + viewParams.format = viewParams.image->getCreationParameters().format; + viewParams.viewType = IImageView::ET_2D; + viewParams.subresourceRange.aspectMask = IImage::E_ASPECT_FLAGS::EAF_COLOR_BIT; + viewParams.subresourceRange.baseArrayLayer = 0u; + viewParams.subresourceRange.layerCount = 1u; + viewParams.subresourceRange.baseMipLevel = 0u; + viewParams.subresourceRange.levelCount = 1u; + + auto cpuImageView = ICPUImageView::create(std::move(viewParams)); + + cpu2gpuParams.beginCommandBuffers(); + auto gpuImageView = cpu2gpu.getGPUObjectsFromAssets(&cpuImageView, &cpuImageView + 1u, cpu2gpuParams)->front(); + cpu2gpuParams.waitForCreationToComplete(); + + return gpuImageView; + }; + + gpuEnvmapImageView = createImageView("../../media/envmap/envmap_0.exr"); + + { + const uint32_t MaxDimensions = 3u<(sizeof(uint32_t)*MaxDimensions*MaxSamples); + + core::OwenSampler sampler(MaxDimensions, 0xdeadbeefu); + //core::SobolSampler sampler(MaxDimensions); + + auto out = reinterpret_cast(sampleSequence->getPointer()); + for (auto dim=0u; dimgetSize(); + IGPUBuffer::SCreationParams params = {}; + params.size = bufferSize; + params.usage = core::bitflag(asset::IBuffer::EUF_TRANSFER_DST_BIT) | asset::IBuffer::EUF_UNIFORM_TEXEL_BUFFER_BIT; + gpuSequenceBuffer = logicalDevice->createBuffer(std::move(params)); + auto bufferReqs = gpuSequenceBuffer->getMemoryReqs(); + bufferReqs.memoryTypeBits &= logicalDevice->getPhysicalDevice()->getDeviceLocalMemoryTypeBits(); + auto gpuSequenceBufferMem = logicalDevice->allocate(bufferReqs, gpuSequenceBuffer.get()); + utilities->updateBufferRangeViaStagingBufferAutoSubmit(asset::SBufferRange{0u,bufferSize,gpuSequenceBuffer},sampleSequence->getPointer(), graphicsQueue); + } + gpuSequenceBufferView = logicalDevice->createBufferView(gpuSequenceBuffer.get(), asset::EF_R32G32B32_UINT); + } + + { + IGPUImage::SCreationParams imgParams; + imgParams.flags = static_cast(0u); + imgParams.type = IImage::ET_2D; + imgParams.format = EF_R32G32_UINT; + imgParams.extent = {WIN_W, WIN_H,1u}; + imgParams.mipLevels = 1u; + imgParams.arrayLayers = 1u; + imgParams.samples = IImage::ESCF_1_BIT; + imgParams.usage = core::bitflag(IImage::EUF_SAMPLED_BIT) | IImage::EUF_TRANSFER_DST_BIT; + imgParams.initialLayout = asset::IImage::EL_UNDEFINED; + + IGPUImage::SBufferCopy region = {}; + region.bufferOffset = 0u; + region.bufferRowLength = 0u; + region.bufferImageHeight = 0u; + region.imageExtent = imgParams.extent; + region.imageOffset = {0u,0u,0u}; + region.imageSubresource.layerCount = 1u; + region.imageSubresource.aspectMask = IImage::E_ASPECT_FLAGS::EAF_COLOR_BIT; + + constexpr auto ScrambleStateChannels = 2u; + const auto renderPixelCount = imgParams.extent.width*imgParams.extent.height; + core::vector random(renderPixelCount*ScrambleStateChannels); + { + core::RandomSampler rng(0xbadc0ffeu); + for (auto& pixel : random) + pixel = rng.nextSample(); + } + + core::smart_refctd_ptr scrambleImageBuffer; + { + const auto bufferSize = random.size() * sizeof(uint32_t); + IGPUBuffer::SCreationParams params = {}; + params.size = bufferSize; + params.usage = core::bitflag(asset::IBuffer::EUF_TRANSFER_DST_BIT) | asset::IBuffer::EUF_TRANSFER_SRC_BIT; + scrambleImageBuffer = logicalDevice->createBuffer(std::move(params)); + auto bufferReqs = scrambleImageBuffer->getMemoryReqs(); + bufferReqs.memoryTypeBits &= logicalDevice->getPhysicalDevice()->getDeviceLocalMemoryTypeBits(); + auto bufferMem = logicalDevice->allocate(bufferReqs, scrambleImageBuffer.get()); + utilities->updateBufferRangeViaStagingBufferAutoSubmit(asset::SBufferRange{0u,bufferSize,scrambleImageBuffer},random.data(),graphicsQueue); + } + + IGPUImageView::SCreationParams viewParams; + viewParams.flags = static_cast(0u); + // TODO: Replace this IGPUBuffer -> IGPUImage to using image upload utility + viewParams.image = utilities->createFilledDeviceLocalImageOnDedMem(std::move(imgParams), scrambleImageBuffer.get(), 1u, ®ion, graphicsQueue); + viewParams.viewType = IGPUImageView::ET_2D; + viewParams.format = EF_R32G32_UINT; + viewParams.subresourceRange.aspectMask = IImage::E_ASPECT_FLAGS::EAF_COLOR_BIT; + viewParams.subresourceRange.levelCount = 1u; + viewParams.subresourceRange.layerCount = 1u; + gpuScrambleImageView = logicalDevice->createImageView(std::move(viewParams)); + } + + // Create Out Image + for(uint32_t i = 0; i < swapchain->getImageCount(); ++i) { + outHDRImageViews[i] = createHDRImageView(logicalDevice, asset::EF_R16G16B16A16_SFLOAT, WIN_W, WIN_H); + } + + for(uint32_t i = 0; i < swapchain->getImageCount(); ++i) + { + auto & descSet = descriptorSets0[i]; + descSet = descriptorPool->createDescriptorSet(core::smart_refctd_ptr(gpuDescriptorSetLayout0)); + video::IGPUDescriptorSet::SWriteDescriptorSet writeDescriptorSet; + writeDescriptorSet.dstSet = descSet.get(); + writeDescriptorSet.binding = 0; + writeDescriptorSet.count = 1u; + writeDescriptorSet.arrayElement = 0u; + writeDescriptorSet.descriptorType = asset::IDescriptor::E_TYPE::ET_STORAGE_IMAGE; + video::IGPUDescriptorSet::SDescriptorInfo info; + { + info.desc = outHDRImageViews[i]; + info.info.image.sampler = nullptr; + info.info.image.imageLayout = asset::IImage::EL_GENERAL; + } + writeDescriptorSet.info = &info; + logicalDevice->updateDescriptorSets(1u, &writeDescriptorSet, 0u, nullptr); + } + + IGPUBuffer::SCreationParams gpuuboParams = {}; + gpuuboParams.size = sizeof(SBasicViewParametersAligned); + gpuuboParams.usage = core::bitflag(IGPUBuffer::EUF_UNIFORM_BUFFER_BIT) | IGPUBuffer::EUF_TRANSFER_DST_BIT; + gpuubo = logicalDevice->createBuffer(std::move(gpuuboParams)); + auto gpuUboMemReqs = gpuubo->getMemoryReqs(); + gpuUboMemReqs.memoryTypeBits &= logicalDevice->getPhysicalDevice()->getDeviceLocalMemoryTypeBits(); + auto gpuUboMem = logicalDevice->allocate(gpuUboMemReqs, gpuubo.get()); + + uboDescriptorSet1 = descriptorPool->createDescriptorSet(core::smart_refctd_ptr(gpuDescriptorSetLayout1)); + { + video::IGPUDescriptorSet::SWriteDescriptorSet uboWriteDescriptorSet; + uboWriteDescriptorSet.dstSet = uboDescriptorSet1.get(); + uboWriteDescriptorSet.binding = 0; + uboWriteDescriptorSet.count = 1u; + uboWriteDescriptorSet.arrayElement = 0u; + uboWriteDescriptorSet.descriptorType = asset::IDescriptor::E_TYPE::ET_UNIFORM_BUFFER; + video::IGPUDescriptorSet::SDescriptorInfo info; + { + info.desc = gpuubo; + info.info.buffer.offset = 0ull; + info.info.buffer.size = sizeof(SBasicViewParametersAligned); + } + uboWriteDescriptorSet.info = &info; + logicalDevice->updateDescriptorSets(1u, &uboWriteDescriptorSet, 0u, nullptr); + } + + ISampler::SParams samplerParams0 = { ISampler::ETC_CLAMP_TO_EDGE, ISampler::ETC_CLAMP_TO_EDGE, ISampler::ETC_CLAMP_TO_EDGE, ISampler::ETBC_FLOAT_OPAQUE_BLACK, ISampler::ETF_LINEAR, ISampler::ETF_LINEAR, ISampler::ESMM_LINEAR, 0u, false, ECO_ALWAYS }; + sampler0 = logicalDevice->createSampler(samplerParams0); + ISampler::SParams samplerParams1 = { ISampler::ETC_CLAMP_TO_EDGE, ISampler::ETC_CLAMP_TO_EDGE, ISampler::ETC_CLAMP_TO_EDGE, ISampler::ETBC_INT_OPAQUE_BLACK, ISampler::ETF_NEAREST, ISampler::ETF_NEAREST, ISampler::ESMM_NEAREST, 0u, false, ECO_ALWAYS }; + sampler1 = logicalDevice->createSampler(samplerParams1); + + descriptorSet2 = descriptorPool->createDescriptorSet(core::smart_refctd_ptr(gpuDescriptorSetLayout2)); + { + constexpr auto kDescriptorCount = 5; + IGPUDescriptorSet::SWriteDescriptorSet writeDescriptorSet2[kDescriptorCount]; + IGPUDescriptorSet::SDescriptorInfo writeDescriptorInfo[kDescriptorCount]; + for (auto i=0; iupdateDescriptorSets(kDescriptorCount, writeDescriptorSet2, 0u, nullptr); + } + + constexpr uint32_t FRAME_COUNT = 500000u; + + for (uint32_t i=0u; icreateSemaphore(); + renderFinished[i] = logicalDevice->createSemaphore(); + frameComplete[i] = logicalDevice->createFence(video::IGPUFence::ECF_SIGNALED_BIT); + frameUploadDataCompleteSemaphore[i] = logicalDevice->createSemaphore(); + frameUploadDataCompleteFence[i] = logicalDevice->createFence(video::IGPUFence::ECF_UNSIGNALED); + } + + oracle.reportBeginFrameRecord(); + } + + void onAppTerminated_impl() override + { + const auto& fboCreationParams = fbos->begin()[m_acquiredNextFBO]->getCreationParameters(); + auto gpuSourceImageView = fboCreationParams.attachments[0]; + logicalDevice->waitIdle(); + + bool status = ext::ScreenShot::createScreenShot( + logicalDevice.get(), + queues[CommonAPI::InitOutput::EQT_TRANSFER_UP], + renderFinished[m_resourceIx].get(), + gpuSourceImageView.get(), + assetManager.get(), + "ScreenShot.png", + asset::IImage::EL_PRESENT_SRC, + asset::EAF_NONE); + + assert(status); + } + + void workLoopBody() override + { + auto& graphicsQueue = queues[CommonAPI::InitOutput::EQT_GRAPHICS]; + + m_resourceIx++; + if(m_resourceIx >= FRAMES_IN_FLIGHT) { + m_resourceIx = 0; + } + + oracle.reportEndFrameRecord(); + double dt = oracle.getDeltaTimeInMicroSeconds() / 1000.0; + auto nextPresentationTimeStamp = oracle.getNextPresentationTimeStamp(); + oracle.reportBeginFrameRecord(); + + // Input + inputSystem->getDefaultMouse(&mouse); + inputSystem->getDefaultKeyboard(&keyboard); + + cam.beginInputProcessing(nextPresentationTimeStamp); + mouse.consumeEvents([&](const IMouseEventChannel::range_t& events) -> void { cam.mouseProcess(events); }, logger.get()); + keyboard.consumeEvents([&](const IKeyboardEventChannel::range_t& events) -> void { cam.keyboardProcess(events); }, logger.get()); + cam.endInputProcessing(nextPresentationTimeStamp); + + auto& cb = cmdbuf[m_resourceIx]; + auto& fence = frameComplete[m_resourceIx]; + while (logicalDevice->waitForFences(1u,&fence.get(),false,MAX_TIMEOUT)==video::IGPUFence::ES_TIMEOUT) + { + } + + const auto viewMatrix = cam.getViewMatrix(); + const auto viewProjectionMatrix = matrix4SIMD::concatenateBFollowedByAPrecisely( + video::ISurface::getSurfaceTransformationMatrix(swapchain->getPreTransform()), + cam.getConcatenatedMatrix() + ); + + // safe to proceed + cb->begin(IGPUCommandBuffer::EU_NONE); + + // renderpass + swapchain->acquireNextImage(MAX_TIMEOUT,imageAcquire[m_resourceIx].get(),nullptr,&m_acquiredNextFBO); + { + auto mv = viewMatrix; + auto mvp = viewProjectionMatrix; + core::matrix3x4SIMD normalMat; + mv.getSub3x3InverseTranspose(normalMat); + + SBasicViewParametersAligned viewParams; + memcpy(viewParams.uboData.MV, mv.pointer(), sizeof(mv)); + memcpy(viewParams.uboData.MVP, mvp.pointer(), sizeof(mvp)); + memcpy(viewParams.uboData.NormalMat, normalMat.pointer(), sizeof(normalMat)); + + asset::SBufferRange range; + range.buffer = gpuubo; + range.offset = 0ull; + range.size = sizeof(viewParams); + + video::IGPUQueue::SSubmitInfo uploadImageSubmit; + uploadImageSubmit.pSignalSemaphores = &frameUploadDataCompleteSemaphore[m_resourceIx].get(); + uploadImageSubmit.signalSemaphoreCount = 1u; + + // We know the fence is already signal because of how we structured our execution -> frameUploadDataCompleteSemaphore -> signals to Render Frame -> wait for frameComplete fence to finish -> then we know frameUploadCompleteFence is signalled + utilities->getDefaultUpStreamingBuffer()->cull_frees(); // need to cull_frees after fence signalled and before fence is reset again + logicalDevice->resetFences(1, &frameUploadDataCompleteFence[m_resourceIx].get()); + + utilities->updateBufferRangeViaStagingBufferAutoSubmit(range, &viewParams, graphicsQueue, frameUploadDataCompleteFence[m_resourceIx].get(), uploadImageSubmit); + // No need to wait for frameUploadDataCompleteFence in CPU, we'll use semaphores to singal the next stage the upload is complete. + } + + auto graphicsCmdQueueFamIdx = queues[CommonAPI::InitOutput::EQT_GRAPHICS]->getFamilyIndex(); + // TRANSITION outHDRImageViews[m_acquiredNextFBO] to EIL_GENERAL (because of descriptorSets0 -> ComputeShader Writes into the image) + { + IGPUCommandBuffer::SImageMemoryBarrier imageBarriers[3u] = {}; + imageBarriers[0].barrier.srcAccessMask = asset::EAF_NONE; + imageBarriers[0].barrier.dstAccessMask = static_cast(asset::EAF_SHADER_WRITE_BIT); + imageBarriers[0].oldLayout = asset::IImage::EL_UNDEFINED; + imageBarriers[0].newLayout = asset::IImage::EL_GENERAL; + imageBarriers[0].srcQueueFamilyIndex = graphicsCmdQueueFamIdx; + imageBarriers[0].dstQueueFamilyIndex = graphicsCmdQueueFamIdx; + imageBarriers[0].image = outHDRImageViews[m_acquiredNextFBO]->getCreationParameters().image; + imageBarriers[0].subresourceRange.aspectMask = asset::IImage::EAF_COLOR_BIT; + imageBarriers[0].subresourceRange.baseMipLevel = 0u; + imageBarriers[0].subresourceRange.levelCount = 1; + imageBarriers[0].subresourceRange.baseArrayLayer = 0u; + imageBarriers[0].subresourceRange.layerCount = 1; + + imageBarriers[1].barrier.srcAccessMask = asset::EAF_NONE; + imageBarriers[1].barrier.dstAccessMask = static_cast(asset::EAF_SHADER_READ_BIT); + imageBarriers[1].oldLayout = asset::IImage::EL_UNDEFINED; + imageBarriers[1].newLayout = asset::IImage::EL_SHADER_READ_ONLY_OPTIMAL; + imageBarriers[1].srcQueueFamilyIndex = graphicsCmdQueueFamIdx; + imageBarriers[1].dstQueueFamilyIndex = graphicsCmdQueueFamIdx; + imageBarriers[1].image = gpuScrambleImageView->getCreationParameters().image; + imageBarriers[1].subresourceRange.aspectMask = asset::IImage::EAF_COLOR_BIT; + imageBarriers[1].subresourceRange.baseMipLevel = 0u; + imageBarriers[1].subresourceRange.levelCount = 1; + imageBarriers[1].subresourceRange.baseArrayLayer = 0u; + imageBarriers[1].subresourceRange.layerCount = 1; + + imageBarriers[2].barrier.srcAccessMask = asset::EAF_NONE; + imageBarriers[2].barrier.dstAccessMask = static_cast(asset::EAF_SHADER_READ_BIT); + imageBarriers[2].oldLayout = asset::IImage::EL_UNDEFINED; + imageBarriers[2].newLayout = asset::IImage::EL_SHADER_READ_ONLY_OPTIMAL; + imageBarriers[2].srcQueueFamilyIndex = graphicsCmdQueueFamIdx; + imageBarriers[2].dstQueueFamilyIndex = graphicsCmdQueueFamIdx; + imageBarriers[2].image = gpuEnvmapImageView->getCreationParameters().image; + imageBarriers[2].subresourceRange.aspectMask = asset::IImage::EAF_COLOR_BIT; + imageBarriers[2].subresourceRange.baseMipLevel = 0u; + imageBarriers[2].subresourceRange.levelCount = gpuEnvmapImageView->getCreationParameters().subresourceRange.levelCount; + imageBarriers[2].subresourceRange.baseArrayLayer = 0u; + imageBarriers[2].subresourceRange.layerCount = gpuEnvmapImageView->getCreationParameters().subresourceRange.layerCount; + + cb->pipelineBarrier(asset::EPSF_TOP_OF_PIPE_BIT, asset::EPSF_COMPUTE_SHADER_BIT, asset::EDF_NONE, 0u, nullptr, 0u, nullptr, 3u, imageBarriers); + } + + // cube envmap handle + { + cb->bindComputePipeline(gpuComputePipeline.get()); + cb->bindDescriptorSets(EPBP_COMPUTE, gpuComputePipeline->getLayout(), 0u, 1u, &descriptorSets0[m_acquiredNextFBO].get()); + cb->bindDescriptorSets(EPBP_COMPUTE, gpuComputePipeline->getLayout(), 1u, 1u, &uboDescriptorSet1.get()); + cb->bindDescriptorSets(EPBP_COMPUTE, gpuComputePipeline->getLayout(), 2u, 1u, &descriptorSet2.get()); + cb->dispatch(dispatchInfo.workGroupCount[0], dispatchInfo.workGroupCount[1], dispatchInfo.workGroupCount[2]); + } + // TODO: tone mapping and stuff + + // Copy HDR Image to SwapChain + auto srcImgViewCreationParams = outHDRImageViews[m_acquiredNextFBO]->getCreationParameters(); + auto dstImgViewCreationParams = fbos->begin()[m_acquiredNextFBO]->getCreationParameters().attachments[0]->getCreationParameters(); + + // Getting Ready for Blit + // TRANSITION outHDRImageViews[m_acquiredNextFBO] to EIL_TRANSFER_SRC_OPTIMAL + // TRANSITION `fbos[m_acquiredNextFBO]->getCreationParameters().attachments[0]` to EIL_TRANSFER_DST_OPTIMAL + { + IGPUCommandBuffer::SImageMemoryBarrier imageBarriers[2u] = {}; + imageBarriers[0].barrier.srcAccessMask = asset::EAF_NONE; + imageBarriers[0].barrier.dstAccessMask = asset::EAF_TRANSFER_WRITE_BIT; + imageBarriers[0].oldLayout = asset::IImage::EL_UNDEFINED; + imageBarriers[0].newLayout = asset::IImage::EL_TRANSFER_SRC_OPTIMAL; + imageBarriers[0].srcQueueFamilyIndex = graphicsCmdQueueFamIdx; + imageBarriers[0].dstQueueFamilyIndex = graphicsCmdQueueFamIdx; + imageBarriers[0].image = srcImgViewCreationParams.image; + imageBarriers[0].subresourceRange.aspectMask = asset::IImage::EAF_COLOR_BIT; + imageBarriers[0].subresourceRange.baseMipLevel = 0u; + imageBarriers[0].subresourceRange.levelCount = 1; + imageBarriers[0].subresourceRange.baseArrayLayer = 0u; + imageBarriers[0].subresourceRange.layerCount = 1; + + imageBarriers[1].barrier.srcAccessMask = asset::EAF_NONE; + imageBarriers[1].barrier.dstAccessMask = asset::EAF_TRANSFER_WRITE_BIT; + imageBarriers[1].oldLayout = asset::IImage::EL_UNDEFINED; + imageBarriers[1].newLayout = asset::IImage::EL_TRANSFER_DST_OPTIMAL; + imageBarriers[1].srcQueueFamilyIndex = graphicsCmdQueueFamIdx; + imageBarriers[1].dstQueueFamilyIndex = graphicsCmdQueueFamIdx; + imageBarriers[1].image = dstImgViewCreationParams.image; + imageBarriers[1].subresourceRange.aspectMask = asset::IImage::EAF_COLOR_BIT; + imageBarriers[1].subresourceRange.baseMipLevel = 0u; + imageBarriers[1].subresourceRange.levelCount = 1; + imageBarriers[1].subresourceRange.baseArrayLayer = 0u; + imageBarriers[1].subresourceRange.layerCount = 1; + cb->pipelineBarrier(asset::EPSF_TRANSFER_BIT, asset::EPSF_TRANSFER_BIT, asset::EDF_NONE, 0u, nullptr, 0u, nullptr, 2u, imageBarriers); + } + + // Blit Image + { + SImageBlit blit = {}; + blit.srcOffsets[0] = {0, 0, 0}; + blit.srcOffsets[1] = {WIN_W, WIN_H, 1}; + + blit.srcSubresource.aspectMask = srcImgViewCreationParams.subresourceRange.aspectMask; + blit.srcSubresource.mipLevel = srcImgViewCreationParams.subresourceRange.baseMipLevel; + blit.srcSubresource.baseArrayLayer = srcImgViewCreationParams.subresourceRange.baseArrayLayer; + blit.srcSubresource.layerCount = srcImgViewCreationParams.subresourceRange.layerCount; + blit.dstOffsets[0] = {0, 0, 0}; + blit.dstOffsets[1] = {WIN_W, WIN_H, 1}; + blit.dstSubresource.aspectMask = dstImgViewCreationParams.subresourceRange.aspectMask; + blit.dstSubresource.mipLevel = dstImgViewCreationParams.subresourceRange.baseMipLevel; + blit.dstSubresource.baseArrayLayer = dstImgViewCreationParams.subresourceRange.baseArrayLayer; + blit.dstSubresource.layerCount = dstImgViewCreationParams.subresourceRange.layerCount; + + auto srcImg = srcImgViewCreationParams.image; + auto dstImg = dstImgViewCreationParams.image; + + cb->blitImage(srcImg.get(), asset::IImage::EL_TRANSFER_SRC_OPTIMAL, dstImg.get(), asset::IImage::EL_TRANSFER_DST_OPTIMAL, 1u, &blit , ISampler::ETF_NEAREST); + } + + // TRANSITION `fbos[m_acquiredNextFBO]->getCreationParameters().attachments[0]` to EIL_PRESENT + { + IGPUCommandBuffer::SImageMemoryBarrier imageBarriers[1u] = {}; + imageBarriers[0].barrier.srcAccessMask = asset::EAF_TRANSFER_WRITE_BIT; + imageBarriers[0].barrier.dstAccessMask = asset::EAF_NONE; + imageBarriers[0].oldLayout = asset::IImage::EL_TRANSFER_DST_OPTIMAL; + imageBarriers[0].newLayout = asset::IImage::EL_PRESENT_SRC; + imageBarriers[0].srcQueueFamilyIndex = graphicsCmdQueueFamIdx; + imageBarriers[0].dstQueueFamilyIndex = graphicsCmdQueueFamIdx; + imageBarriers[0].image = dstImgViewCreationParams.image; + imageBarriers[0].subresourceRange.aspectMask = asset::IImage::EAF_COLOR_BIT; + imageBarriers[0].subresourceRange.baseMipLevel = 0u; + imageBarriers[0].subresourceRange.levelCount = 1; + imageBarriers[0].subresourceRange.baseArrayLayer = 0u; + imageBarriers[0].subresourceRange.layerCount = 1; + cb->pipelineBarrier(asset::EPSF_TRANSFER_BIT, asset::EPSF_TOP_OF_PIPE_BIT, asset::EDF_NONE, 0u, nullptr, 0u, nullptr, 1u, imageBarriers); + } + + cb->end(); + logicalDevice->resetFences(1, &fence.get()); + + nbl::video::IGPUQueue::SSubmitInfo submit; + submit.commandBufferCount = 1u; + submit.commandBuffers = &cb.get(); + submit.signalSemaphoreCount = 1u; + submit.pSignalSemaphores = &renderFinished[m_resourceIx].get(); + nbl::video::IGPUSemaphore* waitSemaphores[2u] = { imageAcquire[m_resourceIx].get(), frameUploadDataCompleteSemaphore[m_resourceIx].get() }; + asset::E_PIPELINE_STAGE_FLAGS waitStages[2u] = { nbl::asset::EPSF_COLOR_ATTACHMENT_OUTPUT_BIT, nbl::asset::EPSF_RAY_TRACING_SHADER_BIT_KHR} ; + submit.waitSemaphoreCount = 2u; + submit.pWaitSemaphores = waitSemaphores; + submit.pWaitDstStageMask = waitStages; + + graphicsQueue->submit(1u,&submit,fence.get()); + + CommonAPI::Present(logicalDevice.get(), swapchain.get(), queues[CommonAPI::InitOutput::EQT_GRAPHICS], renderFinished[m_resourceIx].get(), m_acquiredNextFBO); + } + + bool keepRunning() override + { + return windowCb->isWindowOpen(); + } + + video::IAPIConnection* getAPIConnection() override + { + return apiConnection.get(); + } + video::ILogicalDevice* getLogicalDevice() override + { + return logicalDevice.get(); + } + video::IGPURenderpass* getRenderpass() override + { + return renderpass.get(); + } + void setSurface(core::smart_refctd_ptr&& s) override + { + surface = std::move(s); + } + void setFBOs(std::vector>& f) override + { + for (int i = 0; i < f.size(); i++) + { + fbos->begin()[i] = core::smart_refctd_ptr(f[i]); + } + } + void setSwapchain(core::smart_refctd_ptr&& s) override + { + swapchain = std::move(s); + } + uint32_t getSwapchainImageCount() override + { + return swapchain->getImageCount(); + } + virtual nbl::asset::E_FORMAT getDepthFormat() override + { + return nbl::asset::EF_D32_SFLOAT; + } +}; + +NBL_COMMON_API_MAIN(RayQuerySampleApp) diff --git a/56_RayQuery/pipeline.groovy b/56_RayQuery/pipeline.groovy new file mode 100644 index 000000000..beba797c3 --- /dev/null +++ b/56_RayQuery/pipeline.groovy @@ -0,0 +1,50 @@ +import org.DevshGraphicsProgramming.Agent +import org.DevshGraphicsProgramming.BuilderInfo +import org.DevshGraphicsProgramming.IBuilder + +class CRayQueryBuilder extends IBuilder +{ + public CRayQueryBuilder(Agent _agent, _info) + { + super(_agent, _info) + } + + @Override + public boolean prepare(Map axisMapping) + { + return true + } + + @Override + public boolean build(Map axisMapping) + { + IBuilder.CONFIGURATION config = axisMapping.get("CONFIGURATION") + IBuilder.BUILD_TYPE buildType = axisMapping.get("BUILD_TYPE") + + def nameOfBuildDirectory = getNameOfBuildDirectory(buildType) + def nameOfConfig = getNameOfConfig(config) + + agent.execute("cmake --build ${info.rootProjectPath}/${nameOfBuildDirectory}/${info.targetProjectPathRelativeToRoot} --target ${info.targetBaseName} --config ${nameOfConfig} -j12 -v") + + return true + } + + @Override + public boolean test(Map axisMapping) + { + return true + } + + @Override + public boolean install(Map axisMapping) + { + return true + } +} + +def create(Agent _agent, _info) +{ + return new CRayQueryBuilder(_agent, _info) +} + +return this \ No newline at end of file diff --git a/59_QuaternionTests/CMakeLists.txt b/59_QuaternionTests/CMakeLists.txt deleted file mode 100644 index 84152c9b8..000000000 --- a/59_QuaternionTests/CMakeLists.txt +++ /dev/null @@ -1,68 +0,0 @@ -include(common RESULT_VARIABLE RES) -if(NOT RES) - message(FATAL_ERROR "common.cmake not found. Should be in {repo_root}/cmake directory") -endif() - -nbl_create_executable_project("" "" "" "" "${NBL_EXECUTABLE_PROJECT_CREATION_PCH_TARGET}") - -if(NBL_EMBED_BUILTIN_RESOURCES) - set(_BR_TARGET_ ${EXECUTABLE_NAME}_builtinResourceData) - set(RESOURCE_DIR "app_resources") - - get_filename_component(_SEARCH_DIRECTORIES_ "${CMAKE_CURRENT_SOURCE_DIR}" ABSOLUTE) - get_filename_component(_OUTPUT_DIRECTORY_SOURCE_ "${CMAKE_CURRENT_BINARY_DIR}/src" ABSOLUTE) - get_filename_component(_OUTPUT_DIRECTORY_HEADER_ "${CMAKE_CURRENT_BINARY_DIR}/include" ABSOLUTE) - - file(GLOB_RECURSE BUILTIN_RESOURCE_FILES RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}/${RESOURCE_DIR}" CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/${RESOURCE_DIR}/*") - foreach(RES_FILE ${BUILTIN_RESOURCE_FILES}) - LIST_BUILTIN_RESOURCE(RESOURCES_TO_EMBED "${RES_FILE}") - endforeach() - - ADD_CUSTOM_BUILTIN_RESOURCES(${_BR_TARGET_} RESOURCES_TO_EMBED "${_SEARCH_DIRECTORIES_}" "${RESOURCE_DIR}" "nbl::this_example::builtin" "${_OUTPUT_DIRECTORY_HEADER_}" "${_OUTPUT_DIRECTORY_SOURCE_}") - - LINK_BUILTIN_RESOURCES_TO_TARGET(${EXECUTABLE_NAME} ${_BR_TARGET_}) -endif() - -if(MSVC) - target_compile_options("${EXECUTABLE_NAME}" PUBLIC "/fp:strict") -else() - target_compile_options("${EXECUTABLE_NAME}" PUBLIC -ffloat-store -frounding-math -fsignaling-nans -ftrapping-math) -endif() - -set(OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/auto-gen") - -set(SM 6_8) -set(JSON [=[ -[ - { - "INPUT": "app_resources/quaternionTest.comp.hlsl", - "KEY": "quaternionTest", - } -] -]=]) -string(CONFIGURE "${JSON}" JSON) - -set(COMPILE_OPTIONS - -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} -) diff --git a/59_QuaternionTests/CQuaternionTester.h b/59_QuaternionTests/CQuaternionTester.h deleted file mode 100644 index 89478d1ad..000000000 --- a/59_QuaternionTests/CQuaternionTester.h +++ /dev/null @@ -1,186 +0,0 @@ -#ifndef _NBL_EXAMPLES_TESTS_59_QUATERNION_TESTER_INCLUDED_ -#define _NBL_EXAMPLES_TESTS_59_QUATERNION_TESTER_INCLUDED_ - -#define GLM_FORCE_RADIANS -#include -#include -#define GLM_ENABLE_EXPERIMENTAL -#include -#include - -#include "nbl/examples/examples.hpp" -#include "app_resources/common.hlsl" -#include "nbl/examples/Tester/ITester.h" -#include -#include - -using namespace nbl; - -class CQuaternionTester final : public ITester -{ - using base_t = ITester; - -public: - CQuaternionTester(const uint32_t testBatchCount) - : base_t(testBatchCount) {}; - -private: - QuaternionInputTestValues generateInputTestValues() override - { - std::uniform_real_distribution realDistribution(-1.0f, 1.0f); - std::uniform_real_distribution realDistribution01(0.0f, 1.0f); - std::uniform_real_distribution realDistributionRad(-numbers::pi, numbers::pi); - - QuaternionInputTestValues testInput; - testInput.axis = hlsl::normalize(float32_t3(realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), realDistribution(getRandomEngine()))); - testInput.angle = realDistributionRad(getRandomEngine()); - testInput.quat0 = math::quaternion::create(float32_t3(realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), realDistribution(getRandomEngine())), realDistribution(getRandomEngine())); - testInput.quat0 = hlsl::normalize(testInput.quat0); - testInput.quat1 = math::quaternion::create(float32_t3(realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), realDistribution(getRandomEngine())), realDistribution(getRandomEngine())); - testInput.quat1 = hlsl::normalize(testInput.quat1); - testInput.quat2 = testInput.quat0 * realDistribution(getRandomEngine()) * 1000.f; - testInput.quat3 = testInput.quat1 * realDistribution(getRandomEngine()) * 1000.f; - testInput.pitch = realDistributionRad(getRandomEngine()); - testInput.yaw = realDistributionRad(getRandomEngine()); - testInput.roll = realDistributionRad(getRandomEngine()); - testInput.rotationMat = float32_t3x3(glm::rotate(realDistributionRad(getRandomEngine()), hlsl::normalize(float32_t3(realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), realDistribution(getRandomEngine()))))); - testInput.scaleFactor = realDistribution01(getRandomEngine()) * 1000.f; - - testInput.scaleRotationMat = testInput.rotationMat; - testInput.scaleRotationMat *= testInput.scaleFactor; - - testInput.interpolationFactor = realDistribution01(getRandomEngine()); - testInput.someVec = float32_t3(realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), realDistribution(getRandomEngine())); - - return testInput; - } - - QuaternionTestValues determineExpectedResults(const QuaternionInputTestValues& testInput) override - { - const auto glmquat0 = glm::quat(testInput.quat0.data.w, testInput.quat0.data.x, testInput.quat0.data.y, testInput.quat0.data.z); - const auto glmquat1 = glm::quat(testInput.quat1.data.w, testInput.quat1.data.x, testInput.quat1.data.y, testInput.quat1.data.z); - const auto glmquat2 = glm::quat(testInput.quat2.data.w, testInput.quat2.data.x, testInput.quat2.data.y, testInput.quat2.data.z); - const auto glmquat3 = glm::quat(testInput.quat3.data.w, testInput.quat3.data.x, testInput.quat3.data.y, testInput.quat3.data.z); - - QuaternionTestValues expected; - { - const auto glmquat = glm::angleAxis(testInput.angle, testInput.axis); - expected.quatFromAngleAxis.data.x = glmquat.data.data[0]; - expected.quatFromAngleAxis.data.y = glmquat.data.data[1]; - expected.quatFromAngleAxis.data.z = glmquat.data.data[2]; - expected.quatFromAngleAxis.data.w = glmquat.data.data[3]; - } - { - const auto rotmat = glm::yawPitchRoll(testInput.yaw, testInput.pitch, testInput.roll); - const auto glmquat = glm::quat_cast(rotmat); - expected.quatFromEulerAngles.data.x = glmquat.data.data[0]; - expected.quatFromEulerAngles.data.y = glmquat.data.data[1]; - expected.quatFromEulerAngles.data.z = glmquat.data.data[2]; - expected.quatFromEulerAngles.data.w = glmquat.data.data[3]; - } - { - glm::mat3x3 rotmat; - rotmat[0] = testInput.rotationMat[0]; - rotmat[1] = testInput.rotationMat[1]; - rotmat[2] = testInput.rotationMat[2]; - const auto glmquat = glm::quat_cast(glm::transpose(rotmat)); - expected.quatFromMat.data.x = glmquat.data.data[0]; - expected.quatFromMat.data.y = glmquat.data.data[1]; - expected.quatFromMat.data.z = glmquat.data.data[2]; - expected.quatFromMat.data.w = glmquat.data.data[3]; - - expected.quatFromScaledMat.data = hlsl::normalize(expected.quatFromMat.data) * testInput.scaleFactor; - } - { - const auto rotmat = glm::transpose(glm::mat3_cast(glmquat0)); - expected.rotationMat[0] = rotmat[0]; - expected.rotationMat[1] = rotmat[1]; - expected.rotationMat[2] = rotmat[2]; - } - { - const auto rotmat = transpose(glm::mat3_cast(glmquat2)); - expected.scaleRotationMat[0] = rotmat[0]; - expected.scaleRotationMat[1] = rotmat[1]; - expected.scaleRotationMat[2] = rotmat[2]; - } - { - const auto mult = glmquat0 * glmquat1; - expected.quatMult.data.x = mult.data.data[0]; - expected.quatMult.data.y = mult.data.data[1]; - expected.quatMult.data.z = mult.data.data[2]; - expected.quatMult.data.w = mult.data.data[3]; - } - { - const auto slerped = glm::slerp(glmquat0, glmquat1, testInput.interpolationFactor); - expected.quatSlerp.data.x = slerped.data.data[0]; - expected.quatSlerp.data.y = slerped.data.data[1]; - expected.quatSlerp.data.z = slerped.data.data[2]; - expected.quatSlerp.data.w = slerped.data.data[3]; - - expected.quatFlerp.data = expected.quatSlerp.data; - } - { - const auto mult = glmquat2 * glmquat3; - expected.quatScaledMult.data.x = mult.data.data[0]; - expected.quatScaledMult.data.y = mult.data.data[1]; - expected.quatScaledMult.data.z = mult.data.data[2]; - expected.quatScaledMult.data.w = mult.data.data[3]; - } - expected.transformedVec = glmquat0 * testInput.someVec; - - return expected; - } - - bool verifyTestResults(const QuaternionTestValues& expectedTestValues, const QuaternionTestValues& testValues, const size_t testIteration, const uint32_t seed, TestType testType) override - { - bool pass = true; - pass &= verifyVectorTestValue("create from axis angle", expectedTestValues.quatFromAngleAxis.data, testValues.quatFromAngleAxis.data, testIteration, seed, testType, 1e-2, true); - pass &= verifyVectorTestValue("create from Euler angles", expectedTestValues.quatFromEulerAngles.data, testValues.quatFromEulerAngles.data, testIteration, seed, testType, 1e-2, true); - pass &= verifyVectorTestValue("create from rotation matrix", expectedTestValues.quatFromMat.data, testValues.quatFromMat.data, testIteration, seed, testType, 1e-2, true); - pass &= verifyScaledVectorTestValue("create from scale rotation matrix", expectedTestValues.quatFromScaledMat.data, testValues.quatFromScaledMat.data, testIteration, seed, testType, 1e-4, 1e-2); - - pass &= verifyTestValue("construct matrix", expectedTestValues.rotationMat, testValues.rotationMat, testIteration, seed, testType, 1e-2); - pass &= verifyTestValue("construct matrix (scaled)", expectedTestValues.scaleRotationMat, testValues.scaleRotationMat, testIteration, seed, testType, 1e-2); - - pass &= verifyVectorTestValue("multiply quat", expectedTestValues.quatMult.data, testValues.quatMult.data, testIteration, seed, testType, 1e-2, true); - pass &= verifyVectorTestValue("slerp quat", expectedTestValues.quatSlerp.data, testValues.quatSlerp.data, testIteration, seed, testType, 1e-2, true); - pass &= verifyVectorTestValue("flerp quat", expectedTestValues.quatFlerp.data, testValues.quatFlerp.data, testIteration, seed, testType, 1e-1, true); - pass &= verifyTestValue("transform vector", expectedTestValues.transformedVec, testValues.transformedVec, testIteration, seed, testType, 1e-2); - - pass &= verifyScaledVectorTestValue("multiply scaled quat", expectedTestValues.quatScaledMult.data, testValues.quatScaledMult.data, testIteration, seed, testType, 1e-4, 1e-2); - return pass; - } - - template - bool verifyScaledVectorTestValue(const std::string& memberName, const T& expectedVal, const T& testVal, - const size_t testIteration, const uint32_t seed, const TestType testType, const float64_t maxAbsoluteDifference, const float64_t maxRelativeDifference) - { - if (nbl::hlsl::testing::orientationCompare(expectedVal, testVal, maxRelativeDifference) && - nbl::hlsl::testing::vectorLengthCompare(expectedVal, testVal, maxAbsoluteDifference, maxRelativeDifference)) - return true; - - printTestFail(memberName, expectedVal, testVal, testIteration, seed, testType); - return false; - } - - template - bool verifyVectorTestValue(const std::string& memberName, const T& expectedVal, const T& testVal, - const size_t testIteration, const uint32_t seed, const TestType testType, const float64_t maxAllowedDifference, const bool testOrientation) - { - if (compareVectorTestValues(expectedVal, testVal, maxAllowedDifference, testOrientation)) - return true; - - printTestFail(memberName, expectedVal, testVal, testIteration, seed, testType); - return false; - } - - template requires concepts::FloatingPointLikeVectorial - bool compareVectorTestValues(const T& lhs, const T& rhs, const float64_t maxAllowedDifference, const bool testOrientation) - { - if (testOrientation) - return nbl::hlsl::testing::orientationCompare(lhs, rhs, maxAllowedDifference); - return nbl::hlsl::testing::relativeApproxCompare(lhs, rhs, maxAllowedDifference); - } -}; - -#endif diff --git a/59_QuaternionTests/app_resources/common.hlsl b/59_QuaternionTests/app_resources/common.hlsl deleted file mode 100644 index 312b037f4..000000000 --- a/59_QuaternionTests/app_resources/common.hlsl +++ /dev/null @@ -1,65 +0,0 @@ -//// Copyright (C) 2023-2024 - 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_TESTS_59_QUATERNION_COMMON_INCLUDED_ -#define _NBL_EXAMPLES_TESTS_59_QUATERNION_COMMON_INCLUDED_ - -#include - -using namespace nbl::hlsl; -struct QuaternionInputTestValues -{ - math::quaternion quat0; - math::quaternion quat1; - math::quaternion quat2; - math::quaternion quat3; - float32_t3 axis; - float angle; - float pitch; - float yaw; - float roll; - float32_t3x3 rotationMat; - float scaleFactor; - float32_t3x3 scaleRotationMat; - float interpolationFactor; - float32_t3 someVec; -}; - -struct QuaternionTestValues -{ - math::quaternion quatFromAngleAxis; - math::quaternion quatFromEulerAngles; - math::quaternion quatFromMat; - math::quaternion quatFromScaledMat; - float32_t3x3 rotationMat; - float32_t3x3 scaleRotationMat; - math::quaternion quatMult; - math::quaternion quatSlerp; - math::quaternion quatFlerp; - math::quaternion quatScaledMult; - float32_t3 transformedVec; -}; - -struct QuaternionTestExecutor -{ - void operator()(NBL_CONST_REF_ARG(QuaternionInputTestValues) input, NBL_REF_ARG(QuaternionTestValues) output) - { - output.quatFromAngleAxis = math::quaternion::create(input.axis, input.angle); - output.quatFromEulerAngles = math::quaternion::create(input.pitch, input.yaw, input.roll); - output.quatFromMat = math::quaternion::create(input.rotationMat); - output.quatFromScaledMat = math::quaternion::create(input.scaleRotationMat); - - output.rotationMat = _static_cast(input.quat0); - output.scaleRotationMat = _static_cast(input.quat2); - - output.quatMult = input.quat0 * input.quat1; - output.quatSlerp = math::quaternion::slerp(input.quat0, input.quat1, input.interpolationFactor); - output.quatFlerp = math::quaternion::flerp(input.quat0, input.quat1, input.interpolationFactor); - output.transformedVec = input.quat0.transformVector(input.someVec, true); - - output.quatScaledMult = input.quat2 * input.quat3; - } -}; - -#endif diff --git a/59_QuaternionTests/app_resources/quaternionTest.comp.hlsl b/59_QuaternionTests/app_resources/quaternionTest.comp.hlsl deleted file mode 100644 index 5d3e6577a..000000000 --- a/59_QuaternionTests/app_resources/quaternionTest.comp.hlsl +++ /dev/null @@ -1,19 +0,0 @@ -//// Copyright (C) 2023-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 shader_stage(compute) - -#include "common.hlsl" -#include - -[[vk::binding(0, 0)]] RWStructuredBuffer inputTestValues; -[[vk::binding(1, 0)]] RWStructuredBuffer outputTestValues; - -[numthreads(256, 1, 1)] -[shader("compute")] -void main() -{ - const uint invID = nbl::hlsl::glsl::gl_GlobalInvocationID().x; - QuaternionTestExecutor executor; - executor(inputTestValues[invID], outputTestValues[invID]); -} \ No newline at end of file diff --git a/59_QuaternionTests/main.cpp b/59_QuaternionTests/main.cpp deleted file mode 100644 index 00a60aef8..000000000 --- a/59_QuaternionTests/main.cpp +++ /dev/null @@ -1,72 +0,0 @@ -// 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 "nbl/this_example/builtin/build/spirv/keys.hpp" - -#include "app_resources/common.hlsl" - -#include "CQuaternionTester.h" - -#include -#include -#include - - -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; - -//using namespace glm; - -class QuaternionTest final : public application_templates::MonoDeviceApplication, public BuiltinResourcesApplication -{ - using device_base_t = application_templates::MonoDeviceApplication; - using asset_base_t = BuiltinResourcesApplication; -public: - QuaternionTest(const path& _localInputCWD, const path& _localOutputCWD, const path& _sharedInputCWD, const path& _sharedOutputCWD) : - IApplicationFramework(_localInputCWD, _localOutputCWD, _sharedInputCWD, _sharedOutputCWD) {} - - bool onAppInitialized(smart_refctd_ptr&& system) override - { - // Remember to call the base class initialization! - if (!device_base_t::onAppInitialized(smart_refctd_ptr(system))) - return false; - if (!asset_base_t::onAppInitialized(std::move(system))) - return false; - - { - CQuaternionTester::PipelineSetupData pplnSetupData; - pplnSetupData.device = m_device; - pplnSetupData.api = m_api; - pplnSetupData.assetMgr = m_assetMgr; - pplnSetupData.logger = m_logger; - pplnSetupData.physicalDevice = m_physicalDevice; - pplnSetupData.computeFamilyIndex = getComputeQueue()->getFamilyIndex(); - pplnSetupData.shaderKey = nbl::this_example::builtin::build::get_spirv_key<"quaternionTest">(m_device.get()); - - CQuaternionTester quaternionTester(8); - quaternionTester.setupPipeline(pplnSetupData); - if (!quaternionTester.performTestsAndVerifyResults("QuaternionTestLog.txt")) - return false; - } - - // In contrast to fences, we just need one semaphore to rule all dispatches - return true; - } - - void onAppTerminated_impl() override - { - m_device->waitIdle(); - } - - void workLoopBody() override {} - - bool keepRunning() override { return false; } -}; - -NBL_MAIN_FUNC(QuaternionTest) diff --git a/old_to_refactor/60_ClusteredRendering/CMakeLists.txt b/60_ClusteredRendering/CMakeLists.txt similarity index 100% rename from old_to_refactor/60_ClusteredRendering/CMakeLists.txt rename to 60_ClusteredRendering/CMakeLists.txt diff --git a/60_ClusteredRendering/config.json.template b/60_ClusteredRendering/config.json.template new file mode 100644 index 000000000..f961745c1 --- /dev/null +++ b/60_ClusteredRendering/config.json.template @@ -0,0 +1,28 @@ +{ + "enableParallelBuild": true, + "threadsPerBuildProcess" : 2, + "isExecuted": false, + "scriptPath": "", + "cmake": { + "configurations": [ "Release", "Debug", "RelWithDebInfo" ], + "buildModes": [], + "requiredOptions": [] + }, + "profiles": [ + { + "backend": "vulkan", + "platform": "windows", + "buildModes": [], + "runConfiguration": "Release", + "gpuArchitectures": [] + } + ], + "dependencies": [], + "data": [ + { + "dependencies": [], + "command": [""], + "outputs": [] + } + ] +} \ No newline at end of file diff --git a/old_to_refactor/60_ClusteredRendering/main.cpp b/60_ClusteredRendering/main.cpp similarity index 100% rename from old_to_refactor/60_ClusteredRendering/main.cpp rename to 60_ClusteredRendering/main.cpp diff --git a/old_to_refactor/60_ClusteredRendering/pipeline.groovy b/60_ClusteredRendering/pipeline.groovy similarity index 100% rename from old_to_refactor/60_ClusteredRendering/pipeline.groovy rename to 60_ClusteredRendering/pipeline.groovy diff --git a/61_UI/CMakeLists.txt b/61_UI/CMakeLists.txt index 0e3248fdb..a34e46ce6 100644 --- a/61_UI/CMakeLists.txt +++ b/61_UI/CMakeLists.txt @@ -1,4 +1,4 @@ -if(NBL_BUILD_IMGUI AND NBL_BUILD_FRUSTUM) +if(NBL_BUILD_IMGUI) set(NBL_EXTRA_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/src/transform.cpp" ) @@ -7,15 +7,12 @@ if(NBL_BUILD_IMGUI AND NBL_BUILD_FRUSTUM) "${CMAKE_CURRENT_SOURCE_DIR}/include" ) - list(APPEND NBL_LIBRARIES + list(APPEND NBL_LIBRARIES imtestengine imguizmo "${NBL_EXT_IMGUI_UI_LIB}" - "${NBL_EXT_FRUSTUM_LIB}" ) - - # 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) + + nbl_create_executable_project("${NBL_EXTRA_SOURCES}" "" "${NBL_INCLUDE_SERACH_DIRECTORIES}" "${NBL_LIBRARIES}" "${NBL_EXECUTABLE_PROJECT_CREATION_PCH_TARGET}") + LINK_BUILTIN_RESOURCES_TO_TARGET(${EXECUTABLE_NAME} geometryCreatorSpirvBRD) endif() \ No newline at end of file diff --git a/61_UI/include/common.hpp b/61_UI/include/common.hpp index f79948e95..a5def7551 100644 --- a/61_UI/include/common.hpp +++ b/61_UI/include/common.hpp @@ -1,22 +1,25 @@ -#ifndef _NBL_THIS_EXAMPLE_COMMON_H_INCLUDED_ -#define _NBL_THIS_EXAMPLE_COMMON_H_INCLUDED_ +#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 "CCamera.hpp" +#include "SimpleWindowedApplication.hpp" +#include "CEventCallback.hpp" // the example's headers #include "transform.hpp" +#include "CGeomtryCreatorScene.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; +using namespace core; +using namespace hlsl; +using namespace system; +using namespace asset; +using namespace ui; +using namespace video; +using namespace scene; +using namespace geometrycreator; -#endif // _NBL_THIS_EXAMPLE_COMMON_H_INCLUDED_ \ No newline at end of file +#endif // __NBL_THIS_EXAMPLE_COMMON_H_INCLUDED__ \ No newline at end of file diff --git a/61_UI/include/transform.hpp b/61_UI/include/transform.hpp index fb1672c2f..88a78f751 100644 --- a/61_UI/include/transform.hpp +++ b/61_UI/include/transform.hpp @@ -1,23 +1,20 @@ -#ifndef _NBL_THIS_EXAMPLE_TRANSFORM_H_INCLUDED_ -#define _NBL_THIS_EXAMPLE_TRANSFORM_H_INCLUDED_ - +#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" +static constexpr inline auto OfflineSceneTextureIx = 1u; struct TransformRequestParams { - float camDistance = 8.f; - uint8_t sceneTexDescIx = ~0; bool useWindow = true, editTransformDecomposition = false, enableViewManipulate = false; + float camDistance = 8.f; }; -nbl::hlsl::uint16_t2 EditTransform(float* cameraView, const float* cameraProjection, float* matrix, const TransformRequestParams& params) +void EditTransform(float* cameraView, const float* cameraProjection, float* matrix, const TransformRequestParams& params) { static ImGuizmo::OPERATION mCurrentGizmoOperation(ImGuizmo::TRANSLATE); static ImGuizmo::MODE mCurrentGizmoMode(ImGuizmo::LOCAL); @@ -102,12 +99,11 @@ nbl::hlsl::uint16_t2 EditTransform(float* cameraView, const float* cameraProject 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.textureID = OfflineSceneTextureIx; 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); @@ -122,7 +118,6 @@ nbl::hlsl::uint16_t2 EditTransform(float* cameraView, const float* cameraProject 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; @@ -142,7 +137,6 @@ nbl::hlsl::uint16_t2 EditTransform(float* cameraView, const float* cameraProject 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; @@ -155,8 +149,6 @@ nbl::hlsl::uint16_t2 EditTransform(float* cameraView, const float* cameraProject 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..ef03d88d6 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -1,972 +1,799 @@ -// Copyright (C) 2018-2026 DevSH Graphics Programming Sp. z O.O. +// 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 "common.hpp" -#include /* -Renders scene texture to an offscreen framebuffer whose color attachment is then sampled into a imgui window. + Renders scene texture to an offline + framebuffer which 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. + Written with Nabla, it's UI extension + and got integrated with ImGuizmo to + handle scene's object translations. */ -class UISampleApp final : public MonoWindowApplication, public BuiltinResourcesApplication + +class UISampleApp final : public examples::SimpleWindowedApplication { - using device_base_t = MonoWindowApplication; - using asset_base_t = BuiltinResourcesApplication; + using device_base_t = examples::SimpleWindowedApplication; + using clock_t = std::chrono::steady_clock; + + _NBL_STATIC_INLINE_CONSTEXPR uint32_t WIN_W = 1280, WIN_H = 720, SC_IMG_COUNT = 3u, FRAMES_IN_FLIGHT = 5u; + static_assert(FRAMES_IN_FLIGHT > SC_IMG_COUNT); + + constexpr static inline clock_t::duration DisplayImageDuration = std::chrono::milliseconds(900); 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) {} + : IApplicationFramework(_localInputCWD, _localOutputCWD, _sharedInputCWD, _sharedOutputCWD) {} + + inline core::vector getSurfaces() const override + { + if (!m_surface) + { + { + auto windowCallback = core::make_smart_refctd_ptr(smart_refctd_ptr(m_inputSystem), smart_refctd_ptr(m_logger)); + IWindow::SCreationParams params = {}; + params.callback = core::make_smart_refctd_ptr(); + params.width = WIN_W; + params.height = WIN_H; + params.x = 32; + params.y = 32; + params.flags = ui::IWindow::ECF_HIDDEN | IWindow::ECF_BORDERLESS | IWindow::ECF_RESIZABLE; + params.windowCaption = "UISampleApp"; + params.callback = windowCallback; + 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) = nbl::video::CSimpleResizeSurface::create(std::move(surface)); + } + + if (m_surface) + return { {m_surface->getSurface()/*,EQF_NONE*/} }; + + return {}; + } inline bool onAppInitialized(smart_refctd_ptr&& system) override { - if (!asset_base_t::onAppInitialized(smart_refctd_ptr(system))) - return false; + m_inputSystem = make_smart_refctd_ptr(logger_opt_smart_ptr(smart_refctd_ptr(m_logger))); + if (!device_base_t::onAppInitialized(smart_refctd_ptr(system))) return false; + m_assetManager = make_smart_refctd_ptr(smart_refctd_ptr(m_system)); + auto* geometry = m_assetManager->getGeometryCreator(); + 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; igetSurface() }; + if (!swapchainParams.deduceFormat(m_physicalDevice)) + return logFail("Could not choose a Surface Format for the Swapchain!"); + + const static IGPURenderpass::SCreationParams::SSubpassDependency dependencies[] = { - if (!pool) - return logFail("Couldn't create Command Pool!"); - if (!pool->createCommandBuffers(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 = IGPURenderpass::SCreationParams::SSubpassDependency::External, + .dstSubpass = 0, + .memoryBarrier = { - .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) + .srcStageMask = asset::PIPELINE_STAGE_FLAGS::COPY_BIT, + .srcAccessMask = asset::ACCESS_FLAGS::TRANSFER_WRITE_BIT, + .dstStageMask = asset::PIPELINE_STAGE_FLAGS::COLOR_ATTACHMENT_OUTPUT_BIT, + .dstAccessMask = asset::ACCESS_FLAGS::COLOR_ATTACHMENT_WRITE_BIT + } + }, { - 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); + .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 + } + }, + IGPURenderpass::SCreationParams::DependenciesEnd + }; + + auto scResources = std::make_unique(m_device.get(), swapchainParams.surfaceFormat.format, dependencies); + auto* renderpass = scResources->getRenderpass(); + + if (!renderpass) + return logFail("Failed to create Renderpass!"); - // Create Frustum Drawer + auto gQueue = getGraphicsQueue(); + if (!m_surface || !m_surface->init(gQueue, std::move(scResources), swapchainParams.sharedParams)) + return logFail("Could not create Window & Surface or initialize the Surface!"); + + m_maxFramesInFlight = m_surface->getMaxFramesInFlight(); + if (FRAMES_IN_FLIGHT < m_maxFramesInFlight) { - 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!"); + m_logger->log("Lowering frames in flight!", ILogger::ELL_WARNING); + m_maxFramesInFlight = FRAMES_IN_FLIGHT; } - // Create ImGUI + m_cmdPool = m_device->createCommandPool(gQueue->getFamilyIndex(), IGPUCommandPool::CREATE_FLAGS::RESET_COMMAND_BUFFER_BIT); + + for (auto i = 0u; i < m_maxFramesInFlight; i++) { - 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"); + if (!m_cmdPool) + return logFail("Couldn't create Command Pool!"); + if (!m_cmdPool->createCommandBuffers(IGPUCommandPool::BUFFER_LEVEL::PRIMARY, { m_cmdBufs.data() + i, 1 })) + return logFail("Couldn't create Command Buffer!"); } - - // create rest of User Interface + + //pass.scene = CScene::create(smart_refctd_ptr(m_utils), smart_refctd_ptr(m_logger), gQueue, geometry); + pass.scene = CScene::create(smart_refctd_ptr(m_utils), smart_refctd_ptr(m_logger), gQueue, geometry); + + nbl::ext::imgui::UI::SCreationParameters params; + + params.resources.texturesInfo = { .setIx = 0u, .bindingIx = 0u }; + params.resources.samplersInfo = { .setIx = 0u, .bindingIx = 1u }; + params.assetManager = m_assetManager; + params.pipelineCache = nullptr; + params.pipelineLayout = nbl::ext::imgui::UI::createDefaultPipelineLayout(m_utils->getLogicalDevice(), params.resources.texturesInfo, params.resources.samplersInfo, TexturesAmount); + params.renderpass = smart_refctd_ptr(renderpass); + params.streamingBuffer = nullptr; + params.subpassIx = 0u; + params.transfer = getTransferUpQueue(); + params.utilities = m_utils; { - auto* imgui = interface.imGUI.get(); - // create the suballocated descriptor set + pass.ui.manager = nbl::ext::imgui::UI::create(std::move(params)); + + if (!pass.ui.manager) + return false; + + // note that we use default layout provided by our extension, but you are free to create your own by filling nbl::ext::imgui::UI::S_CREATION_PARAMETERS::resources + const auto* descriptorSetLayout = pass.ui.manager->getPipeline()->getLayout()->getDescriptorSetLayout(0u); + const auto& params = pass.ui.manager->getCreationParameters(); + + IDescriptorPool::SCreateInfo descriptorPoolInfo = {}; + descriptorPoolInfo.maxDescriptorCount[static_cast(asset::IDescriptor::E_TYPE::ET_SAMPLER)] = (uint32_t)nbl::ext::imgui::UI::DefaultSamplerIx::COUNT; + descriptorPoolInfo.maxDescriptorCount[static_cast(asset::IDescriptor::E_TYPE::ET_SAMPLED_IMAGE)] = TexturesAmount; + descriptorPoolInfo.maxSets = 1u; + descriptorPoolInfo.flags = IDescriptorPool::E_CREATE_FLAGS::ECF_UPDATE_AFTER_BIND_BIT; + + m_descriptorSetPool = m_device->createDescriptorPool(std::move(descriptorPoolInfo)); + assert(m_descriptorSetPool); + + m_descriptorSetPool->createDescriptorSets(1u, &descriptorSetLayout, &pass.ui.descriptorSet); + assert(pass.ui.descriptorSet); + } + pass.ui.manager->registerListener([this]() -> void { - // 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 + ImGuiIO& io = ImGui::GetIO(); + + camera.setProjectionMatrix([&]() { - 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();}); - } + static matrix4SIMD projection; - interface.camera.mapKeysToArrows(); + if (isPerspective) + if(isLH) + projection = matrix4SIMD::buildProjectionMatrixPerspectiveFovLH(core::radians(fov), io.DisplaySize.x / io.DisplaySize.y, zNear, zFar); + else + projection = matrix4SIMD::buildProjectionMatrixPerspectiveFovRH(core::radians(fov), io.DisplaySize.x / io.DisplaySize.y, zNear, zFar); + else + { + float viewHeight = viewWidth * io.DisplaySize.y / io.DisplaySize.x; - onAppInitializedFinish(); - return true; - } + if(isLH) + projection = matrix4SIMD::buildProjectionMatrixOrthoLH(viewWidth, viewHeight, zNear, zFar); + else + projection = matrix4SIMD::buildProjectionMatrixOrthoRH(viewWidth, viewHeight, zNear, zFar); + } - // - 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(); - } + return projection; + }()); - inline IQueue::SSubmitInfo::SSemaphoreInfo renderFrame(const std::chrono::microseconds nextPresentationTimestamp) override - { - // CPU events - update(nextPresentationTimestamp); + ImGuizmo::SetOrthographic(false); + ImGuizmo::BeginFrame(); - const auto& virtualWindowRes = interface.sceneResolution; - if (!m_framebuffer || m_framebuffer->getCreationParameters().width!=virtualWindowRes[0] || m_framebuffer->getCreationParameters().height!=virtualWindowRes[1]) - recreateFramebuffer(virtualWindowRes); + ImGui::SetNextWindowPos(ImVec2(1024, 100), ImGuiCond_Appearing); + ImGui::SetNextWindowSize(ImVec2(256, 256), ImGuiCond_Appearing); - // - const auto resourceIx = m_realFrameIx % MaxFramesInFlight; + // create a window and insert the inspector + ImGui::SetNextWindowPos(ImVec2(10, 10), ImGuiCond_Appearing); + ImGui::SetNextWindowSize(ImVec2(320, 340), ImGuiCond_Appearing); + ImGui::Begin("Editor"); - 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; + if (ImGui::RadioButton("Full view", !transformParams.useWindow)) + transformParams.useWindow = false; - ext::frustum::CDrawFrustum::DrawParameters drawParams; - drawParams.commandBuffer = cb; - drawParams.viewProjectionMatrix = viewCamera.getConcatenatedMatrix(); - drawParams.lineWidth = 1.0f; + ImGui::SameLine(); - hlsl::float32_t4x4 frustumCameraViewProj = interface.debugCamera.getConcatenatedMatrix(); - hlsl::float32_t4x4 frustumTransform = hlsl::inverse(frustumCameraViewProj); + if (ImGui::RadioButton("Window", transformParams.useWindow)) + transformParams.useWindow = true; - hlsl::float32_t4 color = {1.0f, 1.0f, 1.0f, 1.0f}; + ImGui::Text("Camera"); + bool viewDirty = false; - 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(); + if (ImGui::RadioButton("LH", isLH)) + isLH = true; - //updateGUIDescriptorSet(); + ImGui::SameLine(); - 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--; - } + if (ImGui::RadioButton("RH", !isLH)) + isLH = false; + if (ImGui::RadioButton("Perspective", isPerspective)) + isPerspective = true; - m_window->setCaption("[Nabla Engine] UI App Test Demo"); - return retval; - } + ImGui::SameLine(); - 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; - } + if (ImGui::RadioButton("Orthographic", !isPerspective)) + isPerspective = false; - private: - inline void update(const std::chrono::microseconds nextPresentationTimestamp) - { - auto& camera = interface.camera; - camera.setMoveSpeed(interface.moveSpeed); - camera.setRotateSpeed(interface.rotateSpeed); + 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 - m_inputSystem->getDefaultMouse(&mouse); - m_inputSystem->getDefaultKeyboard(&keyboard); + if (isPerspective) + ImGui::SliderFloat("Fov", &fov, 20.f, 150.f); + else + ImGui::SliderFloat("Ortho width", &viewWidth, 1, 20); - struct - { - std::vector mouse{}; - std::vector keyboard{}; - } uiEvents; + ImGui::SliderFloat("zNear", &zNear, 0.1f, 100.f); + ImGui::SliderFloat("zFar", &zFar, 110.f, 10000.f); - // TODO: should be a member really - static std::chrono::microseconds previousEventTimestamp{}; + viewDirty |= ImGui::SliderFloat("Distance", &transformParams.camDistance, 1.f, 69.f); - // 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 (viewDirty || firstFrame) { - if (interface.move) - camera.mouseProcess(events); // don't capture the events, only let camera handle them with its impl + 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); - for (const auto& e : events) // here capture - { - if (e.timeStamp < previousEventTimestamp) - continue; + camera.setPosition(cameraPosition); + camera.setTarget(cameraTarget); + camera.setBackupUpVector(up); - previousEventTimestamp = e.timeStamp; - uiEvents.mouse.emplace_back(e); + camera.recomputeViewMatrix(); - 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 + firstFrame = false; + } + + ImGui::Text("X: %f Y: %f", io.MousePos.x, io.MousePos.y); + if (ImGuizmo::IsUsing()) { - if (interface.move) - camera.keyboardProcess(events); // don't capture the events, only let camera handle them with its impl + 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(); - for (const auto& e : events) // here capture - { - if (e.timeStamp < previousEventTimestamp) - continue; + /* + * 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 - previousEventTimestamp = e.timeStamp; - uiEvents.keyboard.emplace_back(e); - } - }, - m_logger.get() - ); - } - camera.endInputProcessing(nextPresentationTimestamp); + - VIEW: - const auto cursorPosition = m_window->getCursorControl()->getPosition(); + ImGuizmo - 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 - }; + | 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 | - interface.objectName = m_scene->getInitParams().geometryNames[interface.gcIndex]; - interface.imGUI->update(params); - } + Nabla - 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; + | 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) | - // 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; - } + = transpose(nbl::core::matrix4SIMD()) - 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); - } + - PERSPECTIVE [PROJECTION CASE]: - // 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(); + ImGuizmo - // TODO: why is this a lambda and not just an assignment in a scope ? - camera.setProjectionMatrix([&]() - { - hlsl::float32_t4x4 projection; + | (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) | - 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 + 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 { - float viewHeight = viewWidth * io.DisplaySize.y / io.DisplaySize.x; + core::matrix4SIMD view, projection, model; + } imguizmoM16InOut; - if(isLH) - projection = hlsl::math::thin_lens::lhPerspectiveFovMatrix(viewWidth, viewHeight, zNear, zFar); - else - projection = hlsl::math::thin_lens::rhPerspectiveFovMatrix(viewWidth, viewHeight, zNear, zFar); + ImGuizmo::SetID(0u); + + imguizmoM16InOut.view = core::transpose(matrix4SIMD(camera.getViewMatrix())); + imguizmoM16InOut.projection = core::transpose(camera.getProjectionMatrix()); + imguizmoM16InOut.model = core::transpose(core::matrix4SIMD(pass.scene->object.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; + EditTransform(imguizmoM16InOut.view.pointer(), imguizmoM16InOut.projection.pointer(), imguizmoM16InOut.model.pointer(), transformParams); } - return projection; - }()); + // to Nabla + update camera & model matrices + const auto& view = camera.getViewMatrix(); + const auto& projection = camera.getProjectionMatrix(); - // 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 + // TODO: make it more nicely + const_cast(view) = core::transpose(imguizmoM16InOut.view).extractSub3x4(); // 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) + camera.setProjectionMatrix(projection); // update concatanated matrix { - 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); + static nbl::core::matrix3x4SIMD modelView, normal; + static nbl::core::matrix4SIMD modelViewProjection; + + auto& hook = pass.scene->object; + hook.model = core::transpose(imguizmoM16InOut.model).extractSub3x4(); + { + const auto& references = pass.scene->getResources().objects; + const auto type = static_cast(gcIndex); + + const auto& [gpu, meta] = references[type]; + hook.meta.type = type; + hook.meta.name = meta.name; + } + + auto& ubo = hook.viewParameters; + + modelView = nbl::core::concatenateBFollowedByA(view, hook.model); + modelView.getSub3x3InverseTranspose(normal); + modelViewProjection = nbl::core::concatenateBFollowedByA(camera.getConcatenatedMatrix(), hook.model); + + memcpy(ubo.MVP, modelViewProjection.pointer(), sizeof(ubo.MVP)); + memcpy(ubo.MV, modelView.pointer(), sizeof(ubo.MV)); + memcpy(ubo.NormalMat, normal.pointer(), sizeof(ubo.NormalMat)); + + // object meta display + { + ImGui::Begin("Object"); + ImGui::Text("type: \"%s\"", hook.meta.name.data()); + ImGui::End(); + } } - return projection; - }()); + + // view matrices editor + { + ImGui::Begin("Matrices"); - ImGuizmo::SetOrthographic(false); - ImGuizmo::BeginFrame(); + 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(); + } - ImGui::SetNextWindowPos(ImVec2(1024, 100), ImGuiCond_Appearing); - ImGui::SetNextWindowSize(ImVec2(256, 256), ImGuiCond_Appearing); + if (withSeparator) + ImGui::Separator(); + }; - // create a window and insert the inspector - ImGui::SetNextWindowPos(ImVec2(10, 10), ImGuiCond_Appearing); - ImGui::SetNextWindowSize(ImVec2(320, 340), ImGuiCond_Appearing); - ImGui::Begin("Editor"); + addMatrixTable("Model Matrix", "ModelMatrixTable", 3, 4, pass.scene->object.model.pointer()); + addMatrixTable("Camera View Matrix", "ViewMatrixTable", 3, 4, view.pointer()); + addMatrixTable("Camera View Projection Matrix", "ViewProjectionMatrixTable", 4, 4, projection.pointer(), false); - if (ImGui::RadioButton("Full view", !transformParams.useWindow)) - transformParams.useWindow = false; + ImGui::End(); + } - ImGui::SameLine(); + // 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 = pass.ui.manager->getStreamingBuffer(); - if (ImGui::RadioButton("Window", transformParams.useWindow)) - transformParams.useWindow = true; + 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 - ImGui::Text("Camera"); - bool viewDirty = false; + float freePercentage = 100.0f * (float)(freeSize) / (float)total; + float allocatedPercentage = (float)(consumedMemory) / (float)total; - if (ImGui::RadioButton("LH", isLH)) - isLH = true; + ImVec2 barSize = ImVec2(400, 30); + float windowPadding = 10.0f; + float verticalPadding = ImGui::GetStyle().FramePadding.y; - ImGui::SameLine(); + ImGui::SetNextWindowSize(ImVec2(barSize.x + 2 * windowPadding, 110 + verticalPadding), ImGuiCond_Always); + ImGui::Begin("Nabla Imgui MDI Buffer Info", nullptr, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoScrollbar); - if (ImGui::RadioButton("RH", !isLH)) - isLH = false; + ImGui::Text("Total Allocated Size: %zu bytes", total); + ImGui::Text("In use: %zu bytes", consumedMemory); + ImGui::Text("Buffer Usage:"); - if (ImGui::RadioButton("Perspective", isPerspective)) - isPerspective = true; + ImGui::SetCursorPosX(windowPadding); - ImGui::SameLine(); + 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 - if (ImGui::RadioButton("Orthographic", !isPerspective)) - isPerspective = false; + ImGui::ProgressBar(allocatedPercentage, barSize, ""); - 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::PopStyleColor(); - // ImGui::Checkbox("Flip Gizmo's Y axis", &flipGizmoY); // let's not expose it to be changed in UI but keep the logic in case + ImDrawList* drawList = ImGui::GetWindowDrawList(); - if (isPerspective) - ImGui::SliderFloat("Fov", &fov, 20.f, 150.f); - else - ImGui::SliderFloat("Ortho width", &viewWidth, 1, 20); + ImVec2 progressBarPos = ImGui::GetItemRectMin(); + ImVec2 progressBarSize = ImGui::GetItemRectSize(); - ImGui::SliderFloat("zNear", &zNear, 0.1f, 100.f); - ImGui::SliderFloat("zFar", &zFar, 110.f, 10000.f); + const char* text = "%.2f%% free"; + char textBuffer[64]; + snprintf(textBuffer, sizeof(textBuffer), text, freePercentage); - viewDirty |= ImGui::SliderFloat("Distance", &transformParams.camDistance, 1.f, 69.f); + ImVec2 textSize = ImGui::CalcTextSize(textBuffer); + ImVec2 textPos = ImVec2 + ( + progressBarPos.x + (progressBarSize.x - textSize.x) * 0.5f, + progressBarPos.y + (progressBarSize.y - textSize.y) * 0.5f + ); - // 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); - } + 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) + ); - 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); + ImGui::SetCursorScreenPos(textPos); + ImGui::Text("%s", textBuffer); - camera.setPosition(cameraPosition); - camera.setTarget(cameraTarget); - camera.setBackupUpVector(up); + ImGui::Dummy(ImVec2(0.0f, verticalPadding)); - camera.recomputeViewMatrix(); - } - firstFrame = false; + ImGui::End(); + } - 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::End(); } - 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 + m_winMgr->setWindowSize(m_window.get(), WIN_W, WIN_H); + m_surface->recreateSwapchain(); + m_winMgr->show(m_window.get()); + oracle.reportBeginFrameRecord(); + camera.mapKeysToArrows(); - - VIEW: + return true; + } - ImGuizmo + bool updateGUIDescriptorSet() + { + // texture atlas + our scene texture, note we don't create info & write pair for the font sampler because UI extension's is immutable and baked into DS layout + static std::array descriptorInfo; + static IGPUDescriptorSet::SWriteDescriptorSet writes[TexturesAmount]; - | 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 | + descriptorInfo[nbl::ext::imgui::UI::FontAtlasTexId].info.image.imageLayout = IImage::LAYOUT::READ_ONLY_OPTIMAL; + descriptorInfo[nbl::ext::imgui::UI::FontAtlasTexId].desc = core::smart_refctd_ptr(pass.ui.manager->getFontAtlasView()); - Nabla + descriptorInfo[OfflineSceneTextureIx].info.image.imageLayout = IImage::LAYOUT::READ_ONLY_OPTIMAL; + descriptorInfo[OfflineSceneTextureIx].desc = pass.scene->getResources().attachments.color; - | 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) | + for (uint32_t i = 0; i < descriptorInfo.size(); ++i) + { + writes[i].dstSet = pass.ui.descriptorSet.get(); + writes[i].binding = 0u; + writes[i].arrayElement = i; + writes[i].count = 1u; + } + writes[nbl::ext::imgui::UI::FontAtlasTexId].info = descriptorInfo.data() + nbl::ext::imgui::UI::FontAtlasTexId; + writes[OfflineSceneTextureIx].info = descriptorInfo.data() + OfflineSceneTextureIx; - = transpose(nbl::core::matrix4SIMD()) + return m_device->updateDescriptorSets(writes, {}); + } - - PERSPECTIVE [PROJECTION CASE]: + inline void workLoopBody() override + { + const auto resourceIx = m_realFrameIx % m_maxFramesInFlight; - ImGuizmo + if (m_realFrameIx >= m_maxFramesInFlight) + { + const ISemaphore::SWaitInfo cbDonePending[] = + { + { + .semaphore = m_semaphore.get(), + .value = m_realFrameIx + 1 - m_maxFramesInFlight + } + }; + if (m_device->blockForSemaphores(cbDonePending) != ISemaphore::WAIT_RESULT::SUCCESS) + return; + } - | (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) | + // CPU events + update(); - Nabla + // render whole scene to offline frame buffer & submit + pass.scene->begin(); + { + pass.scene->update(); + pass.scene->record(); + pass.scene->end(); + } + pass.scene->submit(); - | 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) | + auto* const cb = m_cmdBufs.data()[resourceIx].get(); + cb->reset(IGPUCommandBuffer::RESET_FLAGS::RELEASE_RESOURCES_BIT); + cb->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT); + cb->beginDebugMarker("UISampleApp IMGUI Frame"); - = transpose() + auto* queue = getGraphicsQueue(); - * - * 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 - */ + asset::SViewport viewport; + { + viewport.minDepth = 1.f; + viewport.maxDepth = 0.f; + viewport.x = 0u; + viewport.y = 0u; + viewport.width = WIN_W; + viewport.height = WIN_H; + } + cb->setViewport(0u, 1u, &viewport); - static struct - { - hlsl::float32_t4x4 view, projection, model; - } imguizmoM16InOut; + const VkRect2D currentRenderArea = + { + .offset = {0,0}, + .extent = {m_window->getWidth(),m_window->getHeight()} + }; - ImGuizmo::SetID(0u); + IQueue::SSubmitInfo::SCommandBufferInfo commandBuffersInfo[] = {{.cmdbuf = cb }}; - 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)); + // UI render pass + { + auto scRes = static_cast(m_surface->getSwapchainResources()); + const IGPUCommandBuffer::SRenderpassBeginInfo renderpassInfo = { - 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); - } + .framebuffer = scRes->getFramebuffer(m_currentImageAcquire.imageIndex), + .colorClearValues = &clear.color, + .depthStencilClearValues = nullptr, + .renderArea = currentRenderArea + }; + nbl::video::ISemaphore::SWaitInfo waitInfo = { .semaphore = m_semaphore.get(), .value = m_realFrameIx + 1u }; - 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 + cb->beginRenderPass(renderpassInfo, IGPUCommandBuffer::SUBPASS_CONTENTS::INLINE); + const auto uiParams = pass.ui.manager->getCreationParameters(); + auto* pipeline = pass.ui.manager->getPipeline(); + cb->bindGraphicsPipeline(pipeline); + cb->bindDescriptorSets(EPBP_GRAPHICS, pipeline->getLayout(), uiParams.resources.texturesInfo.setIx, 1u, &pass.ui.descriptorSet.get()); // note that we use default UI pipeline layout where uiParams.resources.textures.setIx == uiParams.resources.samplers.setIx + + if (!keepRunning()) + return; + + if (!pass.ui.manager->render(cb,waitInfo)) { - ImGui::Begin("Object"); - ImGui::Text("type: \"%s\"", objectName.data()); - ImGui::End(); + // TODO: need to present acquired image before bailing because its already acquired + return; } - - // view matrices editor - { - ImGui::Begin("Matrices"); + cb->endRenderPass(); + } + cb->end(); + { + const IQueue::SSubmitInfo::SSemaphoreInfo rendered[] = + { + { + .semaphore = m_semaphore.get(), + .value = ++m_realFrameIx, + .stageMask = PIPELINE_STAGE_FLAGS::COLOR_ATTACHMENT_OUTPUT_BIT + } + }; - 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) + const IQueue::SSubmitInfo::SSemaphoreInfo acquired[] = + { { - ImGui::TableNextRow(); - for (int x = 0; x < columns; ++x) - { - ImGui::TableSetColumnIndex(x); - ImGui::Text("%.3f", *(pointer + (y * columns) + x)); - } - } - ImGui::EndTable(); - } + .semaphore = m_currentImageAcquire.semaphore, + .value = m_currentImageAcquire.acquireCount, + .stageMask = PIPELINE_STAGE_FLAGS::NONE + } + }; + + const IQueue::SSubmitInfo infos[] = + { + { + .waitSemaphores = acquired, + .commandBuffers = commandBuffersInfo, + .signalSemaphores = rendered + } + }; + + const nbl::video::ISemaphore::SWaitInfo waitInfos[] = + { { + .semaphore = pass.scene->semaphore.progress.get(), + .value = pass.scene->semaphore.finishedValue + } }; + + m_device->blockForSemaphores(waitInfos); + + updateGUIDescriptorSet(); + + if (queue->submit(infos) != IQueue::RESULT::SUCCESS) + m_realFrameIx--; + } + } - if (withSeparator) - ImGui::Separator(); - }; + m_window->setCaption("[Nabla Engine] UI App Test Demo"); + m_surface->present(m_currentImageAcquire.imageIndex, rendered); + } + } - 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); + inline bool keepRunning() override + { + if (m_surface->irrecoverable()) + return false; - ImGui::End(); - } + return true; + } - // 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. + inline bool onAppTerminated() override + { + return device_base_t::onAppTerminated(); + } + + inline void update() + { + camera.setMoveSpeed(moveSpeed); + camera.setRotateSpeed(rotateSpeed); + + static std::chrono::microseconds previousEventTimestamp{}; + + m_inputSystem->getDefaultMouse(&mouse); + m_inputSystem->getDefaultKeyboard(&keyboard); + + auto updatePresentationTimestamp = [&]() + { + m_currentImageAcquire = m_surface->acquireNextImage(); + + oracle.reportEndFrameRecord(); + const auto timestamp = oracle.getNextPresentationTimeStamp(); + oracle.reportBeginFrameRecord(); + + return timestamp; + }; + + const auto nextPresentationTimestamp = updatePresentationTimestamp(); + + struct + { + std::vector mouse{}; + std::vector keyboard{}; + } capturedEvents; + + if (move) camera.beginInputProcessing(nextPresentationTimestamp); + { + mouse.consumeEvents([&](const IMouseEventChannel::range_t& events) -> void { - auto* streaminingBuffer = imGUI->getStreamingBuffer(); + if (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; - 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 + previousEventTimestamp = e.timeStamp; + capturedEvents.mouse.emplace_back(e); - float freePercentage = 100.0f * (float)(freeSize) / (float)total; - float allocatedPercentage = (float)(consumedMemory) / (float)total; + if (e.type == nbl::ui::SMouseEvent::EET_SCROLL) + gcIndex = std::clamp(int16_t(gcIndex) + int16_t(core::sign(e.scrollEvent.verticalScroll)), int64_t(0), int64_t(OT_COUNT - (uint8_t)1u)); + } + }, m_logger.get()); - ImVec2 barSize = ImVec2(400, 30); - float windowPadding = 10.0f; - float verticalPadding = ImGui::GetStyle().FramePadding.y; + keyboard.consumeEvents([&](const IKeyboardEventChannel::range_t& events) -> void + { + if (move) + camera.keyboardProcess(events); // don't capture the events, only let camera handle them with its impl - ImGui::SetNextWindowSize(ImVec2(barSize.x + 2 * windowPadding, 110 + verticalPadding), ImGuiCond_Always); - ImGui::Begin("Nabla Imgui MDI Buffer Info", nullptr, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoScrollbar); + for (const auto& e : events) // here capture + { + if (e.timeStamp < previousEventTimestamp) + continue; - ImGui::Text("Total Allocated Size: %zu bytes", total); - ImGui::Text("In use: %zu bytes", consumedMemory); - ImGui::Text("Buffer Usage:"); + previousEventTimestamp = e.timeStamp; + capturedEvents.keyboard.emplace_back(e); + } + }, m_logger.get()); + } + if (move) camera.endInputProcessing(nextPresentationTimestamp); - ImGui::SetCursorPosX(windowPadding); + const auto cursorPosition = m_window->getCursorControl()->getPosition(); - 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 + nbl::ext::imgui::UI::SUpdateParameters params = + { + .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() } + }; - ImGui::ProgressBar(allocatedPercentage, barSize, ""); + pass.ui.manager->update(params); + } - ImGui::PopStyleColor(); + private: + smart_refctd_ptr m_window; + smart_refctd_ptr> m_surface; + smart_refctd_ptr m_pipeline; + smart_refctd_ptr m_semaphore; + smart_refctd_ptr m_cmdPool; + uint64_t m_realFrameIx : 59 = 0; + uint64_t m_maxFramesInFlight : 5; + std::array, ISwapchain::MaxImages> m_cmdBufs; + ISimpleManagedSurface::SAcquireResult m_currentImageAcquire = {}; + + smart_refctd_ptr m_assetManager; + core::smart_refctd_ptr m_inputSystem; + InputSystem::ChannelReader mouse; + InputSystem::ChannelReader keyboard; - ImDrawList* drawList = ImGui::GetWindowDrawList(); + constexpr static inline auto TexturesAmount = 2u; - ImVec2 progressBarPos = ImGui::GetItemRectMin(); - ImVec2 progressBarSize = ImGui::GetItemRectSize(); + core::smart_refctd_ptr m_descriptorSetPool; - const char* text = "%.2f%% free"; - char textBuffer[64]; - snprintf(textBuffer, sizeof(textBuffer), text, freePercentage); + struct C_UI + { + nbl::core::smart_refctd_ptr manager; - ImVec2 textSize = ImGui::CalcTextSize(textBuffer); - ImVec2 textPos = ImVec2 - ( - progressBarPos.x + (progressBarSize.x - textSize.x) * 0.5f, - progressBarPos.y + (progressBarSize.y - textSize.y) * 0.5f - ); + struct + { + core::smart_refctd_ptr gui, scene; + } samplers; - 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) - ); + core::smart_refctd_ptr descriptorSet; + }; - ImGui::SetCursorScreenPos(textPos); - ImGui::Text("%s", textBuffer); + struct E_APP_PASS + { + nbl::core::smart_refctd_ptr scene; + C_UI ui; + } pass; - ImGui::Dummy(ImVec2(0.0f, verticalPadding)); + Camera camera = Camera(core::vectorSIMDf(0, 0, 0), core::vectorSIMDf(0, 0, 0), core::matrix4SIMD()); + video::CDumbPresentationOracle oracle; - ImGui::End(); - } + 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 - ImGui::End(); - } + TransformRequestParams transformParams; + bool isPerspective = true, isLH = true, flipGizmoY = true, move = false; + 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; - 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; + bool firstFrame = true; }; -NBL_MAIN_FUNC(UISampleApp) +NBL_MAIN_FUNC(UISampleApp) \ No newline at end of file diff --git a/62_CAD/CMakeLists.txt b/62_CAD/CMakeLists.txt index 128e236ba..38ef95f63 100644 --- a/62_CAD/CMakeLists.txt +++ b/62_CAD/CMakeLists.txt @@ -8,19 +8,34 @@ set(EXAMPLE_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/Curves.h" "${CMAKE_CURRENT_SOURCE_DIR}/Hatch.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/Hatch.h" - "${CMAKE_CURRENT_SOURCE_DIR}/Polyline.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/Polyline.h" "${CMAKE_CURRENT_SOURCE_DIR}/DrawResourcesFiller.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/DrawResourcesFiller.h" "${CMAKE_CURRENT_SOURCE_DIR}/SingleLineText.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/SingleLineText.h" - "${CMAKE_CURRENT_SOURCE_DIR}/Images.cpp" "../../src/nbl/ext/TextRendering/TextRendering.cpp" # TODO: this one will be a part of dedicated Nabla ext called "TextRendering" later on which uses MSDF + Freetype ) set(EXAMPLE_INCLUDES "${CMAKE_CURRENT_SOURCE_DIR}/../../3rdparty/boost/superproject/libs/math/include") nbl_create_executable_project("${EXAMPLE_SOURCES}" "" "${EXAMPLE_INCLUDES}" "" "${NBL_EXECUTABLE_PROJECT_CREATION_PCH_TARGET}") -target_link_libraries(${EXECUTABLE_NAME} PRIVATE Nabla::ext::FullScreenTriangle) + +if(NBL_EMBED_BUILTIN_RESOURCES) + set(_BR_TARGET_ ${EXECUTABLE_NAME}_builtinResourceData) + set(RESOURCE_DIR "app_resources") + + get_filename_component(_SEARCH_DIRECTORIES_ "${CMAKE_CURRENT_SOURCE_DIR}" ABSOLUTE) + get_filename_component(_OUTPUT_DIRECTORY_SOURCE_ "${CMAKE_CURRENT_BINARY_DIR}/src" ABSOLUTE) + get_filename_component(_OUTPUT_DIRECTORY_HEADER_ "${CMAKE_CURRENT_BINARY_DIR}/include" ABSOLUTE) + + file(GLOB_RECURSE BUILTIN_RESOURCE_FILES RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}/${RESOURCE_DIR}" CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/${RESOURCE_DIR}/*") + foreach(RES_FILE ${BUILTIN_RESOURCE_FILES}) + LIST_BUILTIN_RESOURCE(RESOURCES_TO_EMBED "${RES_FILE}") + endforeach() + + ADD_CUSTOM_BUILTIN_RESOURCES(${_BR_TARGET_} RESOURCES_TO_EMBED "${_SEARCH_DIRECTORIES_}" "${RESOURCE_DIR}" "nbl::this_example::builtin" "${_OUTPUT_DIRECTORY_HEADER_}" "${_OUTPUT_DIRECTORY_SOURCE_}") + + LINK_BUILTIN_RESOURCES_TO_TARGET(${EXECUTABLE_NAME} ${_BR_TARGET_}) +endif() # if enabled then try use Nabla "Text Rendering" extension # with an implemented interface using the 3rdparty deps @@ -43,57 +58,4 @@ else() foreach(NBL_TARGET IN LISTS NBL_MSDFGEN_TARGETS) target_include_directories(${EXECUTABLE_NAME} PUBLIC $) endforeach() -endif() - -set(OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/auto-gen") - -set(SM 6_8) -set(REQUIRED_CAPS [=[ -{ - "kind": "features", - "name": "fragmentShaderPixelInterlock", - "type": "bool", - "values": [1] -} -]=]) - -set(JSON [=[ -[ - { - "INPUT": "shaders/main_pipeline/vertex_shader.hlsl", - "KEY": "main_pipeline_vertex_shader", - "CAPS": [${REQUIRED_CAPS}] - }, - { - "INPUT": "shaders/main_pipeline/all_fragment_shaders.hlsl", - "KEY": "main_pipeline_fragment_shader", - "CAPS": [${REQUIRED_CAPS}] - } -] -]=]) -string(CONFIGURE "${JSON}" JSON) - -set(COMPILE_OPTIONS - -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} -) +endif() \ No newline at end of file diff --git a/62_CAD/CTriangleMesh.cpp b/62_CAD/CTriangleMesh.cpp deleted file mode 100644 index 5564c0a51..000000000 --- a/62_CAD/CTriangleMesh.cpp +++ /dev/null @@ -1 +0,0 @@ -#include "CTriangleMesh.h" \ No newline at end of file diff --git a/62_CAD/CTriangleMesh.h b/62_CAD/CTriangleMesh.h deleted file mode 100644 index 78f7dd99f..000000000 --- a/62_CAD/CTriangleMesh.h +++ /dev/null @@ -1,137 +0,0 @@ -#pragma once - -#include -#include -#include "shaders/globals.hlsl" - -using namespace nbl; - -struct DTMHeightShadingSettingsInfo -{ - // Height Shading Mode - E_HEIGHT_SHADING_MODE heightShadingMode; - - // Used as fixed interval length for "DISCRETE_FIXED_LENGTH_INTERVALS" shading mode - float intervalLength; - - // Converts an interval index to its corresponding height value - // For example, if this value is 10.0, then an interval index of 2 corresponds to a height of 20.0. - // This computed height is later used to determine the interpolated color for shading. - // It makes sense for this variable to be always equal to `intervalLength` but sometimes it's a different scaling so that last index corresponds to largestHeight - float intervalIndexToHeightMultiplier; - - // Used for "DISCRETE_FIXED_LENGTH_INTERVALS" shading mode - // If `isCenteredShading` is true, the intervals are centered around `minHeight`, meaning the - // first interval spans [minHeight - intervalLength / 2.0, minHeight + intervalLength / 2.0]. - // Otherwise, intervals are aligned from `minHeight` upward, so the first interval spans - // [minHeight, minHeight + intervalLength]. - bool isCenteredShading; - - void addHeightColorMapEntry(float height, float32_t4 color) - { - heightColorSet.emplace(height, color); - } - - bool fillShaderDTMSettingsHeightColorMap(DTMSettings& dtmSettings) const - { - const uint32_t mapSize = heightColorSet.size(); - if (mapSize > DTMHeightShadingSettings::HeightColorMapMaxEntries) - return false; - dtmSettings.heightShadingSettings.heightColorEntryCount = mapSize; - - int index = 0; - for (auto it = heightColorSet.begin(); it != heightColorSet.end(); ++it) - { - dtmSettings.heightShadingSettings.heightColorMapHeights[index] = it->height; - dtmSettings.heightShadingSettings.heightColorMapColors[index] = it->color; - ++index; - } - - return true; - } - -private: - struct HeightColor - { - float height; - float32_t4 color; - - bool operator<(const HeightColor& other) const - { - return height < other.height; - } - }; - - std::set heightColorSet; -}; - -struct DTMContourSettingsInfo -{ - LineStyleInfo lineStyleInfo; - - float startHeight; - float endHeight; - float heightInterval; -}; - -struct DTMSettingsInfo -{ - static constexpr uint32_t MaxContourSettings = DTMSettings::MaxContourSettings; - - uint32_t mode = 0u; // related to E_DTM_MODE - - // outline - LineStyleInfo outlineStyleInfo; - // contours - uint32_t contourSettingsCount = 0u; - DTMContourSettingsInfo contourSettings[MaxContourSettings]; - // height shading - DTMHeightShadingSettingsInfo heightShadingInfo; -}; - -class CTriangleMesh final -{ -public: - using index_t = uint32_t; - using vertex_t = TriangleMeshVertex; - - inline void setVertices(core::vector&& vertices) - { - m_vertices = std::move(vertices); - } - inline void setIndices(core::vector&& indices) - { - m_indices = std::move(indices); - } - - inline const core::vector& getVertices() const - { - return m_vertices; - } - inline const core::vector& getIndices() const - { - return m_indices; - } - - inline size_t getVertexBuffByteSize() const - { - return sizeof(vertex_t) * m_vertices.size(); - } - inline size_t getIndexBuffByteSize() const - { - return sizeof(index_t) * m_indices.size(); - } - inline size_t getIndexCount() const - { - return m_indices.size(); - } - - inline void clear() - { - m_vertices.clear(); - m_indices.clear(); - } - - core::vector m_vertices; - core::vector m_indices; -}; \ No newline at end of file diff --git a/62_CAD/DrawResourcesFiller.cpp b/62_CAD/DrawResourcesFiller.cpp index 97ae6621b..eafed6101 100644 --- a/62_CAD/DrawResourcesFiller.cpp +++ b/62_CAD/DrawResourcesFiller.cpp @@ -1,19 +1,12 @@ #include "DrawResourcesFiller.h" -using namespace nbl; - DrawResourcesFiller::DrawResourcesFiller() {} -DrawResourcesFiller::DrawResourcesFiller(smart_refctd_ptr&& device, smart_refctd_ptr&& bufferUploadUtils, smart_refctd_ptr&& imageUploadUtils, IQueue* copyQueue, core::smart_refctd_ptr&& logger) : - m_device(std::move(device)), - m_bufferUploadUtils(std::move(bufferUploadUtils)), - m_imageUploadUtils(std::move(imageUploadUtils)), - m_copyQueue(copyQueue), - m_logger(std::move(logger)) -{ - imagesCache = std::unique_ptr(new ImagesCache(ImagesBindingArraySize)); -} +DrawResourcesFiller::DrawResourcesFiller(smart_refctd_ptr&& utils, IQueue* copyQueue) : + m_utilities(utils), + m_copyQueue(copyQueue) +{} // function is called when buffer is filled and we should submit draws and clear the buffers and continue filling @@ -22,149 +15,116 @@ void DrawResourcesFiller::setSubmitDrawsFunction(const SubmitFunc& func) submitDraws = func; } -// DrawResourcesFiller needs to access these in order to allocate GPUImages and write the to their correct descriptor set binding -void DrawResourcesFiller::setTexturesDescriptorSetAndBinding(core::smart_refctd_ptr&& descriptorSet, uint32_t binding) +void DrawResourcesFiller::allocateIndexBuffer(ILogicalDevice* logicalDevice, uint32_t maxIndices) { - imagesArrayBinding = binding; - imagesDescriptorIndexAllocator = core::make_smart_refctd_ptr(std::move(descriptorSet)); -} + maxIndexCount = maxIndices; + const size_t indexBufferSize = maxIndices * sizeof(index_buffer_type); + auto indexBuffer = make_smart_refctd_ptr(indexBufferSize); -bool DrawResourcesFiller::allocateDrawResources(ILogicalDevice* logicalDevice, size_t requiredImageMemorySize, size_t requiredBufferMemorySize, std::span memoryTypeIndexTryOrder) -{ - // requiredImageMemorySize = core::alignUp(50'399'744 * 2, 1024); - // single memory allocation sectioned into images+buffers (images start at offset=0) - const size_t adjustedImagesMemorySize = core::alignUp(requiredImageMemorySize, GPUStructsMaxNaturalAlignment); - const size_t adjustedBuffersMemorySize = core::max(requiredBufferMemorySize, getMinimumRequiredResourcesBufferSize()); - const size_t totalResourcesSize = adjustedImagesMemorySize + adjustedBuffersMemorySize; - - IGPUBuffer::SCreationParams resourcesBufferCreationParams = {}; - resourcesBufferCreationParams.size = adjustedBuffersMemorySize; - resourcesBufferCreationParams.usage = bitflag(IGPUBuffer::EUF_SHADER_DEVICE_ADDRESS_BIT) | IGPUBuffer::EUF_TRANSFER_DST_BIT | IGPUBuffer::EUF_INDEX_BUFFER_BIT; - resourcesGPUBuffer = logicalDevice->createBuffer(std::move(resourcesBufferCreationParams)); - - if (!resourcesGPUBuffer) + index_buffer_type* indices = reinterpret_cast(indexBuffer->getPointer()); + for (uint32_t i = 0u; i < maxIndices / 6u; ++i) { - m_logger.log("Failed to create resourcesGPUBuffer.", nbl::system::ILogger::ELL_ERROR); - return false; - } + index_buffer_type objIndex = i; + indices[i * 6] = objIndex * 4u + 1u; + indices[i * 6 + 1u] = objIndex * 4u + 0u; + indices[i * 6 + 2u] = objIndex * 4u + 2u; - resourcesGPUBuffer->setObjectDebugName("drawResourcesBuffer"); + indices[i * 6 + 3u] = objIndex * 4u + 1u; + indices[i * 6 + 4u] = objIndex * 4u + 2u; + indices[i * 6 + 5u] = objIndex * 4u + 3u; + } - IDeviceMemoryBacked::SDeviceMemoryRequirements memReq = resourcesGPUBuffer->getMemoryReqs(); - - nbl::video::IDeviceMemoryBacked::SDeviceMemoryRequirements gpuBufferMemoryReqs = resourcesGPUBuffer->getMemoryReqs(); - const bool memoryRequirementsMatch = - (logicalDevice->getPhysicalDevice()->getDeviceLocalMemoryTypeBits() & gpuBufferMemoryReqs.memoryTypeBits) != 0 && // should have device local memory compatible - (gpuBufferMemoryReqs.requiresDedicatedAllocation == false); // should not require dedicated allocation + IGPUBuffer::SCreationParams indexBufferCreationParams = {}; + indexBufferCreationParams.size = indexBufferSize; + indexBufferCreationParams.usage = IGPUBuffer::EUF_INDEX_BUFFER_BIT | IGPUBuffer::EUF_TRANSFER_DST_BIT; - if (!memoryRequirementsMatch) - { - m_logger.log("Shouldn't happen: Buffer Memory Requires Dedicated Allocation or can't biind to device local memory.", nbl::system::ILogger::ELL_ERROR); - return false; - } - - const auto& memoryProperties = logicalDevice->getPhysicalDevice()->getMemoryProperties(); + m_utilities->createFilledDeviceLocalBufferOnDedMem(SIntendedSubmitInfo{.queue=m_copyQueue}, std::move(indexBufferCreationParams), indices).move_into(gpuDrawBuffers.indexBuffer); + gpuDrawBuffers.indexBuffer->setObjectDebugName("indexBuffer"); +} - video::IDeviceMemoryAllocator::SAllocation allocation = {}; - for (const auto& memoryTypeIdx : memoryTypeIndexTryOrder) - { - IDeviceMemoryAllocator::SAllocateInfo allocationInfo = - { - .size = totalResourcesSize, - .flags = IDeviceMemoryAllocation::E_MEMORY_ALLOCATE_FLAGS::EMAF_DEVICE_ADDRESS_BIT, // for the buffers - .memoryTypeIndex = memoryTypeIdx, - .dedication = nullptr, - }; - - allocation = logicalDevice->allocate(allocationInfo); - - if (allocation.isValid()) - break; - } +void DrawResourcesFiller::allocateMainObjectsBuffer(ILogicalDevice* logicalDevice, uint32_t mainObjects) +{ + maxMainObjects = mainObjects; + size_t mainObjectsBufferSize = maxMainObjects * sizeof(MainObject); - if (!allocation.isValid()) - { - m_logger.log("Failed Allocation for draw resources!", nbl::system::ILogger::ELL_ERROR); - return false; - } + IGPUBuffer::SCreationParams mainObjectsCreationParams = {}; + mainObjectsCreationParams.size = mainObjectsBufferSize; + mainObjectsCreationParams.usage = IGPUBuffer::EUF_STORAGE_BUFFER_BIT | IGPUBuffer::EUF_TRANSFER_DST_BIT; + gpuDrawBuffers.mainObjectsBuffer = logicalDevice->createBuffer(std::move(mainObjectsCreationParams)); + gpuDrawBuffers.mainObjectsBuffer->setObjectDebugName("mainObjectsBuffer"); - imagesMemoryArena = { - .memory = allocation.memory, - .offset = allocation.offset, - }; + IDeviceMemoryBacked::SDeviceMemoryRequirements memReq = gpuDrawBuffers.mainObjectsBuffer->getMemoryReqs(); + memReq.memoryTypeBits &= logicalDevice->getPhysicalDevice()->getDeviceLocalMemoryTypeBits(); + auto mainObjectsBufferMem = logicalDevice->allocate(memReq, gpuDrawBuffers.mainObjectsBuffer.get()); - buffersMemoryArena = { - .memory = allocation.memory, - .offset = core::alignUp(allocation.offset + adjustedImagesMemorySize, GPUStructsMaxNaturalAlignment), // first natural alignment after images section of the memory allocation - }; + cpuDrawBuffers.mainObjectsBuffer = make_smart_refctd_ptr(mainObjectsBufferSize); +} - imagesMemorySubAllocator = core::make_smart_refctd_ptr(adjustedImagesMemorySize); +void DrawResourcesFiller::allocateDrawObjectsBuffer(ILogicalDevice* logicalDevice, uint32_t drawObjects) +{ + maxDrawObjects = drawObjects; + size_t drawObjectsBufferSize = maxDrawObjects * sizeof(DrawObject); - video::ILogicalDevice::SBindBufferMemoryInfo bindBufferMemory = { - .buffer = resourcesGPUBuffer.get(), - .binding = { - .memory = buffersMemoryArena.memory.get(), - .offset = buffersMemoryArena.offset, - } - }; + IGPUBuffer::SCreationParams drawObjectsCreationParams = {}; + drawObjectsCreationParams.size = drawObjectsBufferSize; + drawObjectsCreationParams.usage = IGPUBuffer::EUF_STORAGE_BUFFER_BIT | IGPUBuffer::EUF_TRANSFER_DST_BIT; + gpuDrawBuffers.drawObjectsBuffer = logicalDevice->createBuffer(std::move(drawObjectsCreationParams)); + gpuDrawBuffers.drawObjectsBuffer->setObjectDebugName("drawObjectsBuffer"); - if (!logicalDevice->bindBufferMemory(1, &bindBufferMemory)) - { - m_logger.log("DrawResourcesFiller::allocateDrawResources, bindBufferMemory failed.", nbl::system::ILogger::ELL_ERROR); - return false; - } + IDeviceMemoryBacked::SDeviceMemoryRequirements memReq = gpuDrawBuffers.drawObjectsBuffer->getMemoryReqs(); + memReq.memoryTypeBits &= logicalDevice->getPhysicalDevice()->getDeviceLocalMemoryTypeBits(); + auto drawObjectsBufferMem = logicalDevice->allocate(memReq, gpuDrawBuffers.drawObjectsBuffer.get()); - return true; + cpuDrawBuffers.drawObjectsBuffer = make_smart_refctd_ptr(drawObjectsBufferSize); } -bool DrawResourcesFiller::allocateDrawResourcesWithinAvailableVRAM(ILogicalDevice* logicalDevice, size_t maxImageMemorySize, size_t maxBufferMemorySize, std::span memoryTypeIndexTryOrder, uint32_t reductionPercent, uint32_t maxTries) +void DrawResourcesFiller::allocateGeometryBuffer(ILogicalDevice* logicalDevice, size_t size) { - const size_t minimumAcceptableSize = core::max(MinimumDrawResourcesMemorySize, getMinimumRequiredResourcesBufferSize()); + maxGeometryBufferSize = size; - size_t currentBufferSize = maxBufferMemorySize; - size_t currentImageSize = maxImageMemorySize; - const size_t totalInitialSize = currentBufferSize + currentImageSize; + IGPUBuffer::SCreationParams geometryCreationParams = {}; + geometryCreationParams.size = size; + geometryCreationParams.usage = bitflag(IGPUBuffer::EUF_STORAGE_BUFFER_BIT) | IGPUBuffer::EUF_SHADER_DEVICE_ADDRESS_BIT | IGPUBuffer::EUF_TRANSFER_DST_BIT; + gpuDrawBuffers.geometryBuffer = logicalDevice->createBuffer(std::move(geometryCreationParams)); + gpuDrawBuffers.geometryBuffer->setObjectDebugName("geometryBuffer"); - // If initial size is less than minimum acceptable then increase the buffer and image size to sum up to minimumAcceptableSize with image:buffer ratios preserved - if (totalInitialSize < minimumAcceptableSize) - { - // Preserve ratio: R = buffer / (buffer + image) - // scaleFactor = minimumAcceptableSize / totalInitialSize; - const double scaleFactor = static_cast(minimumAcceptableSize) / totalInitialSize; - currentBufferSize = static_cast(currentBufferSize * scaleFactor); - currentImageSize = minimumAcceptableSize - currentBufferSize; // ensures exact sum - } + IDeviceMemoryBacked::SDeviceMemoryRequirements memReq = gpuDrawBuffers.geometryBuffer->getMemoryReqs(); + memReq.memoryTypeBits &= logicalDevice->getPhysicalDevice()->getDeviceLocalMemoryTypeBits(); + auto geometryBufferMem = logicalDevice->allocate(memReq, gpuDrawBuffers.geometryBuffer.get(), IDeviceMemoryAllocation::EMAF_DEVICE_ADDRESS_BIT); + geometryBufferAddress = gpuDrawBuffers.geometryBuffer->getDeviceAddress(); + + cpuDrawBuffers.geometryBuffer = make_smart_refctd_ptr(size); +} - uint32_t numTries = 0u; - while ((currentBufferSize + currentImageSize) >= minimumAcceptableSize && numTries < maxTries) +void DrawResourcesFiller::allocateStylesBuffer(ILogicalDevice* logicalDevice, uint32_t lineStylesCount) +{ { - if (allocateDrawResources(logicalDevice, currentImageSize, currentBufferSize, memoryTypeIndexTryOrder)) - { - m_logger.log("Successfully allocated memory for images (%zu) and buffers (%zu).", system::ILogger::ELL_INFO, currentImageSize, currentBufferSize); - return true; - } + maxLineStyles = lineStylesCount; + size_t lineStylesBufferSize = lineStylesCount * sizeof(LineStyle); - m_logger.log("Allocation of memory for images(%zu) and buffers(%zu) failed; Reducing allocation size by %u%% and retrying...", system::ILogger::ELL_WARNING, currentImageSize, currentBufferSize, reductionPercent); - currentBufferSize = (currentBufferSize * (100 - reductionPercent)) / 100; - currentImageSize = (currentImageSize * (100 - reductionPercent)) / 100; - numTries++; - } + IGPUBuffer::SCreationParams lineStylesCreationParams = {}; + lineStylesCreationParams.size = lineStylesBufferSize; + lineStylesCreationParams.usage = IGPUBuffer::EUF_STORAGE_BUFFER_BIT | IGPUBuffer::EUF_TRANSFER_DST_BIT; + gpuDrawBuffers.lineStylesBuffer = logicalDevice->createBuffer(std::move(lineStylesCreationParams)); + gpuDrawBuffers.lineStylesBuffer->setObjectDebugName("lineStylesBuffer"); + + IDeviceMemoryBacked::SDeviceMemoryRequirements memReq = gpuDrawBuffers.lineStylesBuffer->getMemoryReqs(); + memReq.memoryTypeBits &= logicalDevice->getPhysicalDevice()->getDeviceLocalMemoryTypeBits(); + auto stylesBufferMem = logicalDevice->allocate(memReq, gpuDrawBuffers.lineStylesBuffer.get()); - m_logger.log("All attempts to allocate memory for images(%zu) and buffers(%zu) failed.", system::ILogger::ELL_ERROR, currentImageSize, currentBufferSize); - return false; + cpuDrawBuffers.lineStylesBuffer = make_smart_refctd_ptr(lineStylesBufferSize); + } } -bool DrawResourcesFiller::allocateMSDFTextures(ILogicalDevice* logicalDevice, uint32_t maxMSDFs, uint32_t2 msdfsExtent) +void DrawResourcesFiller::allocateMSDFTextures(ILogicalDevice* logicalDevice, uint32_t maxMSDFs, uint32_t2 msdfsExtent) { - // TODO: Make this function failable and report insufficient memory + textureLRUCache = std::unique_ptr(new MSDFsLRUCache(maxMSDFs)); + msdfTextureArrayIndexAllocator = core::make_smart_refctd_ptr(core::smart_refctd_ptr(logicalDevice), maxMSDFs); + asset::E_FORMAT msdfFormat = MSDFTextureFormat; asset::VkExtent3D MSDFsExtent = { msdfsExtent.x, msdfsExtent.y, 1u }; - if (maxMSDFs > logicalDevice->getPhysicalDevice()->getLimits().maxImageArrayLayers) - { - m_logger.log("requested maxMSDFs is greater than maxImageArrayLayers. lowering the limit...", nbl::system::ILogger::ELL_WARNING); - maxMSDFs = logicalDevice->getPhysicalDevice()->getLimits().maxImageArrayLayers; - } - + assert(maxMSDFs <= logicalDevice->getPhysicalDevice()->getLimits().maxImageArrayLayers); + IPhysicalDevice::SImageFormatPromotionRequest promotionRequest = {}; promotionRequest.originalFormat = msdfFormat; promotionRequest.usages = {}; @@ -186,10 +146,7 @@ bool DrawResourcesFiller::allocateMSDFTextures(ILogicalDevice* logicalDevice, ui auto image = logicalDevice->createImage(std::move(imgInfo)); auto imageMemReqs = image->getMemoryReqs(); imageMemReqs.memoryTypeBits &= logicalDevice->getPhysicalDevice()->getDeviceLocalMemoryTypeBits(); - const auto allocation = logicalDevice->allocate(imageMemReqs, image.get()); - - if (!allocation.isValid()) - return false; + logicalDevice->allocate(imageMemReqs, image.get()); image->setObjectDebugName("MSDFs Texture Array"); @@ -206,14 +163,6 @@ bool DrawResourcesFiller::allocateMSDFTextures(ILogicalDevice* logicalDevice, ui msdfTextureArray = logicalDevice->createImageView(std::move(imgViewInfo)); } - - if (!msdfTextureArray) - return false; - - msdfLRUCache = std::unique_ptr(new MSDFsLRUCache(maxMSDFs)); - msdfTextureArrayIndexAllocator = core::make_smart_refctd_ptr(core::smart_refctd_ptr(logicalDevice), maxMSDFs); - msdfImagesState.resize(maxMSDFs); - return true; } void DrawResourcesFiller::drawPolyline(const CPolylineBase& polyline, const LineStyleInfo& lineStyleInfo, SIntendedSubmitInfo& intendedNextSubmit) @@ -221,33 +170,18 @@ void DrawResourcesFiller::drawPolyline(const CPolylineBase& polyline, const Line if (!lineStyleInfo.isVisible()) return; - setActiveLineStyle(lineStyleInfo); - - beginMainObject(MainObjectType::POLYLINE, TransformationType::TT_NORMAL); - drawPolyline(polyline, intendedNextSubmit); - endMainObject(); -} + uint32_t styleIdx = addLineStyle_SubmitIfNeeded(lineStyleInfo, intendedNextSubmit); -void DrawResourcesFiller::drawFixedGeometryPolyline(const CPolylineBase& polyline, const LineStyleInfo& lineStyleInfo, const float64_t3x3& transformation, TransformationType transformationType, SIntendedSubmitInfo& intendedNextSubmit) -{ - if (!lineStyleInfo.isVisible()) - return; + uint32_t mainObjIdx = addMainObject_SubmitIfNeeded(styleIdx, intendedNextSubmit); - setActiveLineStyle(lineStyleInfo); - - pushCustomProjection(getFixedGeometryFinalTransformationMatrix(transformation, transformationType)); - beginMainObject(MainObjectType::POLYLINE, transformationType); - drawPolyline(polyline, intendedNextSubmit); - endMainObject(); - popCustomProjection(); + drawPolyline(polyline, mainObjIdx, intendedNextSubmit); } -void DrawResourcesFiller::drawPolyline(const CPolylineBase& polyline, SIntendedSubmitInfo& intendedNextSubmit) +void DrawResourcesFiller::drawPolyline(const CPolylineBase& polyline, uint32_t polylineMainObjIdx, SIntendedSubmitInfo& intendedNextSubmit) { - uint32_t mainObjectIdx = acquireActiveMainObjectIndex_SubmitIfNeeded(intendedNextSubmit); - if (mainObjectIdx == InvalidMainObjectIdx) + if (polylineMainObjIdx == InvalidMainObjectIdx) { - m_logger.log("drawPolyline: acquireActiveMainObjectIndex returned invalid index", nbl::system::ILogger::ELL_ERROR); + // TODO: assert or log error here assert(false); return; } @@ -260,7 +194,7 @@ void DrawResourcesFiller::drawPolyline(const CPolylineBase& polyline, SIntendedS while (currentSectionIdx < sectionsCount) { const auto& currentSection = polyline.getSectionInfoAt(currentSectionIdx); - addPolylineObjects_Internal(polyline, currentSection, currentObjectInSection, mainObjectIdx); + addPolylineObjects_Internal(polyline, currentSection, currentObjectInSection, polylineMainObjIdx); if (currentObjectInSection >= currentSection.count) { @@ -268,7 +202,7 @@ void DrawResourcesFiller::drawPolyline(const CPolylineBase& polyline, SIntendedS currentObjectInSection = 0u; } else - submitCurrentDrawObjectsAndReset(intendedNextSubmit, mainObjectIdx); + submitCurrentObjectsAndReset(intendedNextSubmit, polylineMainObjIdx); } if (!polyline.getConnectors().empty()) @@ -276,93 +210,14 @@ void DrawResourcesFiller::drawPolyline(const CPolylineBase& polyline, SIntendedS uint32_t currentConnectorPolylineObject = 0u; while (currentConnectorPolylineObject < polyline.getConnectors().size()) { - addPolylineConnectors_Internal(polyline, currentConnectorPolylineObject, mainObjectIdx); + addPolylineConnectors_Internal(polyline, currentConnectorPolylineObject, polylineMainObjIdx); if (currentConnectorPolylineObject < polyline.getConnectors().size()) - submitCurrentDrawObjectsAndReset(intendedNextSubmit, mainObjectIdx); + submitCurrentObjectsAndReset(intendedNextSubmit, polylineMainObjIdx); } } } -void DrawResourcesFiller::drawTriangleMesh( - const CTriangleMesh& mesh, - const DTMSettingsInfo& dtmSettingsInfo, - SIntendedSubmitInfo& intendedNextSubmit) -{ - flushDrawObjects(); // flushes draw call construction of any possible draw objects before dtm, because currently we're sepaerating dtm draw calls from drawObj draw calls - - setActiveDTMSettings(dtmSettingsInfo); - beginMainObject(MainObjectType::DTM); - - uint32_t mainObjectIdx = acquireActiveMainObjectIndex_SubmitIfNeeded(intendedNextSubmit); - if (mainObjectIdx == InvalidMainObjectIdx) - { - m_logger.log("drawTriangleMesh: acquireActiveMainObjectIndex returned invalid index", nbl::system::ILogger::ELL_ERROR); - assert(false); - return; - } - - DrawCallData drawCallData = {}; - drawCallData.isDTMRendering = true; - - ICPUBuffer::SCreationParams geometryBuffParams; - - // concatenate the index and vertex buffer into the geometry buffer - const auto& indexBuffer = mesh.getIndices(); - const auto& vertexBuffer = mesh.getVertices(); - assert(indexBuffer.size() == vertexBuffer.size()); // We don't have any vertex re-use due to other limitations at the moemnt. - - - const uint32_t numTriangles = indexBuffer.size() / 3u; - uint32_t trianglesUploaded = 0; - while (trianglesUploaded < numTriangles) - { - const size_t remainingResourcesSize = calculateRemainingResourcesSize(); - const uint32_t maxUploadableVertices = remainingResourcesSize / (sizeof(CTriangleMesh::vertex_t) + sizeof(CTriangleMesh::index_t)); - const uint32_t maxUploadableTriangles = maxUploadableVertices / 3u; - const uint32_t remainingTrianglesToUpload = numTriangles - trianglesUploaded; - const uint32_t trianglesToUpload = core::min(remainingTrianglesToUpload, maxUploadableTriangles); - const size_t vtxBuffByteSize = trianglesToUpload * 3u * sizeof(CTriangleMesh::vertex_t); - const size_t indexBuffByteSize = trianglesToUpload * 3u * sizeof(CTriangleMesh::index_t); - const size_t trianglesToUploadByteSize = vtxBuffByteSize + indexBuffByteSize; - - // Copy VertexBuffer - size_t geometryBufferOffset = resourcesCollection.geometryInfo.increaseSizeAndGetOffset(trianglesToUploadByteSize, alignof(CTriangleMesh::vertex_t)); - void* dst = resourcesCollection.geometryInfo.data() + geometryBufferOffset; - // the actual bda address will be determined only after all copies are finalized, later we will do += `baseBDAAddress + geometryInfo.bufferOffset` - // the - is a small hack because index buffer grows but vertex buffer needs to start from 0, remove that once we either get rid of the index buffer or implement an algorithm that can have vertex reuse - drawCallData.dtm.triangleMeshVerticesBaseAddress = geometryBufferOffset - (sizeof(CTriangleMesh::vertex_t) * trianglesUploaded * 3); - memcpy(dst, &vertexBuffer[trianglesUploaded * 3u], vtxBuffByteSize); - geometryBufferOffset += vtxBuffByteSize; - - // Copy IndexBuffer - dst = resourcesCollection.geometryInfo.data() + geometryBufferOffset; - drawCallData.dtm.indexBufferOffset = geometryBufferOffset; - memcpy(dst, &indexBuffer[trianglesUploaded * 3u], indexBuffByteSize); - geometryBufferOffset += indexBuffByteSize; - - trianglesUploaded += trianglesToUpload; - - drawCallData.dtm.triangleMeshMainObjectIndex = mainObjectIdx; - drawCallData.dtm.indexCount = trianglesToUpload * 3u; - drawCalls.push_back(drawCallData); - - //if (trianglesUploaded == 0u) - //{ - // m_logger.log("drawTriangleMesh: not enough vram allocation for a single triangle!", nbl::system::ILogger::ELL_ERROR); - // assert(false); - // break; - //} - - // Requires Auto-Submit If All Triangles of the Mesh couldn't fit into Memory - if (trianglesUploaded < numTriangles) - submitCurrentDrawObjectsAndReset(intendedNextSubmit, mainObjectIdx); - } - - endMainObject(); -} - -// TODO[Erfan]: Makes more sense if parameters are: solidColor + fillPattern + patternColor void DrawResourcesFiller::drawHatch( const Hatch& hatch, const float32_t4& foregroundColor, @@ -370,8 +225,10 @@ void DrawResourcesFiller::drawHatch( const HatchFillPattern fillPattern, SIntendedSubmitInfo& intendedNextSubmit) { - // TODO[Optimization Idea]: don't draw hatch twice, we now have color storage buffer and we can treat rendering hatches like a procedural texture (requires 2 colors so no more abusing of linestyle for hatches) - + // TODO[Optimization Idea]: don't draw hatch twice if both colors are visible: instead do the msdf inside the alpha resolve by detecting mainObj being a hatch + // https://discord.com/channels/593902898015109131/856835291712716820/1228337893366300743 + // TODO: Come back to this idea when doing color resolve for ecws (they don't have mainObj/style Index, instead they have uv into a texture + // if backgroundColor is visible drawHatch(hatch, backgroundColor, intendedNextSubmit); // if foregroundColor is visible @@ -384,91 +241,35 @@ void DrawResourcesFiller::drawHatch( const HatchFillPattern fillPattern, SIntendedSubmitInfo& intendedNextSubmit) { - drawHatch_impl(hatch, color, fillPattern, intendedNextSubmit); -} - -void DrawResourcesFiller::drawHatch(const Hatch& hatch, const float32_t4& color, SIntendedSubmitInfo& intendedNextSubmit) -{ - drawHatch(hatch, color, HatchFillPattern::SOLID_FILL, intendedNextSubmit); -} - -void DrawResourcesFiller::drawFixedGeometryHatch( - const Hatch& hatch, - const float32_t4& foregroundColor, - const float32_t4& backgroundColor, - const HatchFillPattern fillPattern, - const float64_t3x3& transformation, - TransformationType transformationType, - SIntendedSubmitInfo& intendedNextSubmit) -{ - // TODO[Optimization Idea]: don't draw hatch twice, we now have color storage buffer and we can treat rendering hatches like a procedural texture (requires 2 colors so no more abusing of linestyle for hatches) - - // if backgroundColor is visible - drawFixedGeometryHatch(hatch, backgroundColor, transformation, transformationType, intendedNextSubmit); - // if foregroundColor is visible - drawFixedGeometryHatch(hatch, foregroundColor, fillPattern, transformation, transformationType, intendedNextSubmit); -} - -void DrawResourcesFiller::drawFixedGeometryHatch( - const Hatch& hatch, - const float32_t4& color, - const HatchFillPattern fillPattern, - const float64_t3x3& transformation, - TransformationType transformationType, - SIntendedSubmitInfo& intendedNextSubmit) -{ - pushCustomProjection(getFixedGeometryFinalTransformationMatrix(transformation, transformationType)); - drawHatch_impl(hatch, color, fillPattern, intendedNextSubmit, transformationType); - popCustomProjection(); -} - -void DrawResourcesFiller::drawFixedGeometryHatch( - const Hatch& hatch, - const float32_t4& color, - const float64_t3x3& transformation, - TransformationType transformationType, - SIntendedSubmitInfo& intendedNextSubmit) -{ - drawFixedGeometryHatch(hatch, color, HatchFillPattern::SOLID_FILL, transformation, transformationType, intendedNextSubmit); -} - -void DrawResourcesFiller::drawHatch_impl( - const Hatch& hatch, - const float32_t4& color, - const HatchFillPattern fillPattern, - SIntendedSubmitInfo& intendedNextSubmit, - TransformationType transformationType) -{ - if (color.a == 0.0f) // not visible - return; - - uint32_t textureIdx = InvalidTextureIndex; + uint32_t textureIdx = InvalidTextureIdx; if (fillPattern != HatchFillPattern::SOLID_FILL) { - MSDFInputInfo msdfInfo = MSDFInputInfo(fillPattern); - textureIdx = getMSDFIndexFromInputInfo(msdfInfo, intendedNextSubmit); - if (textureIdx == InvalidTextureIndex) - textureIdx = addMSDFTexture(msdfInfo, getHatchFillPatternMSDF(fillPattern), intendedNextSubmit); - _NBL_DEBUG_BREAK_IF(textureIdx == InvalidTextureIndex); // probably getHatchFillPatternMSDF returned nullptr + const msdf_hash msdfHash = hashFillPattern(fillPattern); + textureIdx = getTextureIndexFromHash(msdfHash, intendedNextSubmit); + if (textureIdx == InvalidTextureIdx) + textureIdx = addMSDFTexture(getHatchFillPatternMSDF(fillPattern), msdfHash, intendedNextSubmit); + assert(textureIdx != InvalidTextureIdx); } LineStyleInfo lineStyle = {}; lineStyle.color = color; lineStyle.screenSpaceLineWidth = nbl::hlsl::bit_cast(textureIdx); + const uint32_t styleIdx = addLineStyle_SubmitIfNeeded(lineStyle, intendedNextSubmit); - setActiveLineStyle(lineStyle); - beginMainObject(MainObjectType::HATCH, transformationType); + uint32_t mainObjIdx = addMainObject_SubmitIfNeeded(styleIdx, intendedNextSubmit); - uint32_t mainObjectIdx = acquireActiveMainObjectIndex_SubmitIfNeeded(intendedNextSubmit); - uint32_t currentObjectInSection = 0u; // Object here refers to DrawObject. You can think of it as a Cage. + uint32_t currentObjectInSection = 0u; // Object here refers to DrawObject used in vertex shader. You can think of it as a Cage. while (currentObjectInSection < hatch.getHatchBoxCount()) { - addHatch_Internal(hatch, currentObjectInSection, mainObjectIdx); + addHatch_Internal(hatch, currentObjectInSection, mainObjIdx); if (currentObjectInSection < hatch.getHatchBoxCount()) - submitCurrentDrawObjectsAndReset(intendedNextSubmit, mainObjectIdx); + submitCurrentObjectsAndReset(intendedNextSubmit, mainObjIdx); } +} - endMainObject(); +void DrawResourcesFiller::drawHatch(const Hatch& hatch, const float32_t4& color, SIntendedSubmitInfo& intendedNextSubmit) +{ + drawHatch(hatch, color, HatchFillPattern::SOLID_FILL, intendedNextSubmit); } void DrawResourcesFiller::drawFontGlyph( @@ -478,1874 +279,339 @@ void DrawResourcesFiller::drawFontGlyph( float32_t2 dirU, float32_t aspectRatio, float32_t2 minUV, + uint32_t mainObjIdx, SIntendedSubmitInfo& intendedNextSubmit) { - uint32_t textureIdx = InvalidTextureIndex; - const MSDFInputInfo msdfInput = MSDFInputInfo(fontFace->getHash(), glyphIdx); - textureIdx = getMSDFIndexFromInputInfo(msdfInput, intendedNextSubmit); - if (textureIdx == InvalidTextureIndex) - textureIdx = addMSDFTexture(msdfInput, getGlyphMSDF(fontFace, glyphIdx), intendedNextSubmit); - - uint32_t mainObjIdx = acquireActiveMainObjectIndex_SubmitIfNeeded(intendedNextSubmit); - if (mainObjIdx == InvalidMainObjectIdx) - { - m_logger.log("drawFontGlyph: acquireActiveMainObjectIndex returned invalid index", nbl::system::ILogger::ELL_ERROR); - assert(false); - return; - } + uint32_t textureIdx = InvalidTextureIdx; + const msdf_hash msdfHash = hashFontGlyph(fontFace->getHash(), glyphIdx); + textureIdx = getTextureIndexFromHash(msdfHash, intendedNextSubmit); + if (textureIdx == InvalidTextureIdx) + textureIdx = addMSDFTexture(getGlyphMSDF(fontFace, glyphIdx), msdfHash, intendedNextSubmit); + assert(textureIdx != InvalidTextureIdx); - if (textureIdx != InvalidTextureIndex) - { - GlyphInfo glyphInfo = GlyphInfo(topLeft, dirU, aspectRatio, textureIdx, minUV); - if (!addFontGlyph_Internal(glyphInfo, mainObjIdx)) - { - // single font glyph couldn't fit into memory to push to gpu, so we submit rendering current objects and reset geometry buffer and draw objects - submitCurrentDrawObjectsAndReset(intendedNextSubmit, mainObjIdx); - const bool success = addFontGlyph_Internal(glyphInfo, mainObjIdx); - if (!success) - { - m_logger.log("addFontGlyph_Internal failed, even after overflow-submission, this is irrecoverable.", nbl::system::ILogger::ELL_ERROR); - assert(false); - } - } - } - else + GlyphInfo glyphInfo = GlyphInfo(topLeft, dirU, aspectRatio, textureIdx, minUV); + if (!addFontGlyph_Internal(glyphInfo, mainObjIdx)) { - m_logger.log("drawFontGlyph: textureIdx is invalid.", nbl::system::ILogger::ELL_ERROR); - _NBL_DEBUG_BREAK_IF(true); + // single font glyph couldn't fit into memory to push to gpu, so we submit rendering current objects and reset geometry buffer and draw objects + submitCurrentObjectsAndReset(intendedNextSubmit, mainObjIdx); + bool success = addFontGlyph_Internal(glyphInfo, mainObjIdx); + assert(success); // this should always be true, otherwise it's either bug in code or not enough memory allocated to hold a single GlyphInfo } } -bool DrawResourcesFiller::ensureStaticImageAvailability(const StaticImageInfo& staticImage, SIntendedSubmitInfo& intendedNextSubmit) +void DrawResourcesFiller::finalizeAllCopiesToGPU(SIntendedSubmitInfo& intendedNextSubmit) { - // imagesCache->logState(m_logger); - - // Check if image already exists and requires force update. We do this before insertion and updating `lastUsedFrameIndex` to get correct overflow-submit behaviour - // otherwise we'd always overflow submit, even if not needed and image was not queued/intended to use in the next submit. - CachedImageRecord* cachedImageRecord = imagesCache->get(staticImage.imageID); - - if (cachedImageRecord && cachedImageRecord->arrayIndex != InvalidTextureIndex && staticImage.forceUpdate) + finalizeMainObjectCopiesToGPU(intendedNextSubmit); + finalizeGeometryCopiesToGPU(intendedNextSubmit); + finalizeLineStyleCopiesToGPU(intendedNextSubmit); + finalizeTextureCopies(intendedNextSubmit); +} + +uint32_t DrawResourcesFiller::addLineStyle_SubmitIfNeeded(const LineStyleInfo& lineStyle, SIntendedSubmitInfo& intendedNextSubmit) +{ + uint32_t outLineStyleIdx = addLineStyle_Internal(lineStyle); + if (outLineStyleIdx == InvalidStyleIdx) { - // found in cache, and we want to force new data into the image - if (cachedImageRecord->staticCPUImage) - { - const auto cachedImageParams = cachedImageRecord->staticCPUImage->getCreationParameters(); - const auto newImageParams = staticImage.cpuImage->getCreationParameters(); - const bool needsRecreation = newImageParams != cachedImageParams; - if (needsRecreation) - { - // call the eviction callback so the currently cached imageID gets eventually deallocated from memory arena along with it's allocated array slot from the suballocated descriptor set - evictImage_SubmitIfNeeded(staticImage.imageID, *cachedImageRecord, intendedNextSubmit); - - // Instead of erasing and inserting the imageID into the cache, we just reset it, so the next block of code goes into array index allocation + creating our new image - // imagesCache->erase(imageID); - // cachedImageRecord = imagesCache->insert(imageID, intendedNextSubmit.getFutureScratchSemaphore().value, evictCallback); - *cachedImageRecord = CachedImageRecord(currentFrameIndex); - } - else - { - // Doesn't need image recreation, we'll use the same array index in descriptor set + the same bound memory. - // reset it's state + update the cpu image used for copying. - cachedImageRecord->state = ImageState::CREATED_AND_MEMORY_BOUND; - cachedImageRecord->staticCPUImage = staticImage.cpuImage; - } - } - else - { - m_logger.log("found static image has empty cpu image, shouldn't happen", nbl::system::ILogger::ELL_ERROR); - } + finalizeAllCopiesToGPU(intendedNextSubmit); + submitDraws(intendedNextSubmit); + resetGeometryCounters(); + resetMainObjectCounters(); + resetLineStyleCounters(); + outLineStyleIdx = addLineStyle_Internal(lineStyle); + assert(outLineStyleIdx != InvalidStyleIdx); } + return outLineStyleIdx; +} - // Try inserting or updating the image usage in the cache. - // If the image is already present, updates its semaphore value. - auto evictCallback = [&](image_id imageID, const CachedImageRecord& evicted) { evictImage_SubmitIfNeeded(imageID, evicted, intendedNextSubmit); }; - cachedImageRecord = imagesCache->insert(staticImage.imageID, currentFrameIndex, evictCallback); - cachedImageRecord->lastUsedFrameIndex = currentFrameIndex; // in case there was an eviction + auto-submit, we need to update AGAIN - - - // if cachedImageRecord->index was not InvalidTextureIndex then it means we had a cache hit and updated the value of our sema - // in which case we don't queue anything for upload, and return the idx - if (cachedImageRecord->arrayIndex == InvalidTextureIndex) +uint32_t DrawResourcesFiller::addMainObject_SubmitIfNeeded(uint32_t styleIdx, SIntendedSubmitInfo& intendedNextSubmit) +{ + MainObject mainObject = {}; + mainObject.styleIdx = styleIdx; + mainObject.clipProjectionAddress = acquireCurrentClipProjectionAddress(intendedNextSubmit); + uint32_t outMainObjectIdx = addMainObject_Internal(mainObject); + if (outMainObjectIdx == InvalidMainObjectIdx) { - // This is a new image (cache miss). Allocate a descriptor index for it. - cachedImageRecord->arrayIndex = video::SubAllocatedDescriptorSet::AddressAllocator::invalid_address; - // Blocking allocation attempt; if the descriptor pool is exhausted, this may stall. - imagesDescriptorIndexAllocator->multi_allocate(std::chrono::time_point::max(), imagesArrayBinding, 1u, &cachedImageRecord->arrayIndex); // if the prev submit causes DEVICE_LOST then we'll get a deadlock here since we're using max timepoint - cachedImageRecord->arrayIndexAllocatedUsingImageDescriptorIndexAllocator = true; + finalizeAllCopiesToGPU(intendedNextSubmit); + submitDraws(intendedNextSubmit); - if (cachedImageRecord->arrayIndex != video::SubAllocatedDescriptorSet::AddressAllocator::invalid_address) - { - auto* physDev = m_device->getPhysicalDevice(); - - IGPUImage::SCreationParams imageParams = {}; - imageParams = staticImage.cpuImage->getCreationParameters(); - imageParams.usage |= IGPUImage::EUF_TRANSFER_DST_BIT|IGPUImage::EUF_SAMPLED_BIT; - // promote format because RGB8 and friends don't actually exist in HW - { - const IPhysicalDevice::SImageFormatPromotionRequest request = { - .originalFormat = imageParams.format, - .usages = IPhysicalDevice::SFormatImageUsages::SUsage(imageParams.usage) - }; - imageParams.format = physDev->promoteImageFormat(request,imageParams.tiling); - } - - // Attempt to create a GPU image and image view for this texture. - ImageAllocateResults allocResults = tryCreateAndAllocateImage_SubmitIfNeeded(imageParams, staticImage.imageViewFormatOverride, intendedNextSubmit, std::to_string(staticImage.imageID)); - - if (allocResults.isValid()) - { - cachedImageRecord->type = ImageType::STATIC; - cachedImageRecord->state = ImageState::CREATED_AND_MEMORY_BOUND; - cachedImageRecord->currentLayout = nbl::asset::IImage::LAYOUT::UNDEFINED; - cachedImageRecord->lastUsedFrameIndex = currentFrameIndex; // there was an eviction + auto-submit, we need to update AGAIN - cachedImageRecord->allocationOffset = allocResults.allocationOffset; - cachedImageRecord->allocationSize = allocResults.allocationSize; - cachedImageRecord->gpuImageView = allocResults.gpuImageView; - cachedImageRecord->staticCPUImage = staticImage.cpuImage; - cachedImageRecord->georeferencedImageState = nullptr; - evictConflictingImagesInCache_SubmitIfNeeded(staticImage.imageID, *cachedImageRecord, intendedNextSubmit); - } - else - { - // All attempts to try create the GPU image and its corresponding view have failed. - // Most likely cause: insufficient GPU memory or unsupported image parameters. - m_logger.log("ensureStaticImageAvailability failed, likely due to low VRAM.", nbl::system::ILogger::ELL_ERROR); - _NBL_DEBUG_BREAK_IF(true); - - if (cachedImageRecord->allocationOffset != ImagesMemorySubAllocator::InvalidAddress) - { - // We previously successfully create and allocated memory for the Image - // but failed to bind and create image view - // It's crucial to deallocate the offset+size form our images memory suballocator - imagesMemorySubAllocator->deallocate(cachedImageRecord->allocationOffset, cachedImageRecord->allocationSize); - } - - if (cachedImageRecord->arrayIndex != InvalidTextureIndex) - { - // We previously allocated a descriptor index, but failed to create a usable GPU image. - // It's crucial to deallocate this index to avoid leaks and preserve descriptor pool space. - // No semaphore wait needed here, as the GPU never got to use this slot. - imagesDescriptorIndexAllocator->multi_deallocate(imagesArrayBinding, 1u, &cachedImageRecord->arrayIndex, {}); - cachedImageRecord->arrayIndex = InvalidTextureIndex; - } - - // erase the entry we failed to allocate an image for, no need for `evictImage_SubmitIfNeeded`, because it didn't get to be used in any submit to defer it's memory and index deallocation - imagesCache->erase(staticImage.imageID); - } - } - else - { - m_logger.log("ensureStaticImageAvailability failed index allocation. shouldn't have happened.", nbl::system::ILogger::ELL_ERROR); - cachedImageRecord->arrayIndex = InvalidTextureIndex; - } + // geometries needs to be reset because they reference draw objects and draw objects reference main objects that are now unavailable and reset + resetGeometryCounters(); + // mainObjects needs to be reset because we submitted every previous main object + resetMainObjectCounters(); + // we shouldn't reset linestyles and clip projections here because it was possibly requested to push to mem before addMainObjects + // but clip projections are reset due to geometry/bda buffer being reset so we need to push again + + // acquireCurrentClipProjectionAddress again here because clip projection should exist in the geometry buffer, and reseting geometry counters will invalidate the current clip proj and requires repush + mainObject.clipProjectionAddress = acquireCurrentClipProjectionAddress(intendedNextSubmit); + outMainObjectIdx = addMainObject_Internal(mainObject); + assert(outMainObjectIdx != InvalidMainObjectIdx); } - - // cached or just inserted, we update the lastUsedFrameIndex - cachedImageRecord->lastUsedFrameIndex = currentFrameIndex; - - assert(cachedImageRecord->arrayIndex != InvalidTextureIndex); // shouldn't happen, because we're using LRU cache, so worst case eviction will happen + multi-deallocate and next next multi_allocate should definitely succeed - return cachedImageRecord->arrayIndex != InvalidTextureIndex; + return outMainObjectIdx; } -bool DrawResourcesFiller::ensureMultipleStaticImagesAvailability(std::span staticImages, SIntendedSubmitInfo& intendedNextSubmit) +void DrawResourcesFiller::pushClipProjectionData(const ClipProjectionData& clipProjectionData) { - if (staticImages.size() > ImagesBindingArraySize) - return false; - - for (auto& staticImage : staticImages) - { - if (!ensureStaticImageAvailability(staticImage, intendedNextSubmit)) - return false; // failed ensuring a single staticImage is available, shouldn't happen unless the image is larger than the memory arena allocated for images. - } - for (auto& staticImage : staticImages) - { - if (imagesCache->peek(staticImage.imageID) == nullptr) - return false; // this means one of the images evicted another, most likely due to VRAM limitations not all images can be resident all at once. - } - return true; + clipProjections.push_back(clipProjectionData); + clipProjectionAddresses.push_back(InvalidClipProjectionAddress); } -// TODO[Przemek]: similar to other drawXXX and drawXXX_internal functions that create mainobjects, drawObjects and push additional info in geometry buffer, input to function would be a GridDTMInfo -// We don't have an allocator or memory management for texture updates yet, see how `_test_addImageObject` is being temporarily used (Descriptor updates and pipeline barriers) to upload an image into gpu and update a descriptor slot (it will become more sophisticated but doesn't block you) -void DrawResourcesFiller::drawGridDTM( - const float64_t2& topLeft, - float64_t2 worldSpaceExtents, - float gridCellWidth, - uint64_t textureID, - const DTMSettingsInfo& dtmSettingsInfo, - SIntendedSubmitInfo& intendedNextSubmit) +void DrawResourcesFiller::popClipProjectionData() { - if (dtmSettingsInfo.mode == 0u) + if (clipProjections.empty()) return; - GridDTMInfo gridDTMInfo; - gridDTMInfo.topLeft = topLeft; - gridDTMInfo.worldSpaceExtents = worldSpaceExtents; - gridDTMInfo.gridCellWidth = gridCellWidth; - if (textureID != InvalidTextureIndex) - gridDTMInfo.textureID = getImageIndexFromID(textureID, intendedNextSubmit); // for this to be valid and safe, this function needs to be called immediately after `addStaticImage` function to make sure image is in memory - else - gridDTMInfo.textureID = InvalidTextureIndex; - - // determine the thickes line - float thickestLineThickness = 0.0f; - if (dtmSettingsInfo.mode & E_DTM_MODE::OUTLINE) - { - thickestLineThickness = dtmSettingsInfo.outlineStyleInfo.worldSpaceLineWidth + dtmSettingsInfo.outlineStyleInfo.screenSpaceLineWidth; - } - else if (dtmSettingsInfo.mode & E_DTM_MODE::CONTOUR) - { - for (int i = 0; i < dtmSettingsInfo.contourSettingsCount; ++i) - { - const auto& contourLineStyle = dtmSettingsInfo.contourSettings[i].lineStyleInfo; - const float contourLineThickness = contourLineStyle.worldSpaceLineWidth + contourLineStyle.screenSpaceLineWidth; - thickestLineThickness = std::max(thickestLineThickness, contourLineThickness); - } - } - gridDTMInfo.thicknessOfTheThickestLine = thickestLineThickness; + clipProjections.pop_back(); + clipProjectionAddresses.pop_back(); +} - setActiveDTMSettings(dtmSettingsInfo); - beginMainObject(MainObjectType::GRID_DTM); +void DrawResourcesFiller::finalizeMainObjectCopiesToGPU(SIntendedSubmitInfo& intendedNextSubmit) +{ + // Copy MainObjects + uint32_t remainingMainObjects = currentMainObjectCount - inMemMainObjectCount; + SBufferRange mainObjectsRange = { sizeof(MainObject) * inMemMainObjectCount, sizeof(MainObject) * remainingMainObjects, gpuDrawBuffers.mainObjectsBuffer }; + const MainObject* srcMainObjData = reinterpret_cast(cpuDrawBuffers.mainObjectsBuffer->getPointer()) + inMemMainObjectCount; + if (mainObjectsRange.size > 0u) + m_utilities->updateBufferRangeViaStagingBuffer(intendedNextSubmit, mainObjectsRange, srcMainObjData); + inMemMainObjectCount = currentMainObjectCount; +} - uint32_t mainObjectIdx = acquireActiveMainObjectIndex_SubmitIfNeeded(intendedNextSubmit); - if (mainObjectIdx == InvalidMainObjectIdx) - { - m_logger.log("drawGridDTM: acquireActiveMainObjectIndex returned invalid index", nbl::system::ILogger::ELL_ERROR); - assert(false); - return; - } +void DrawResourcesFiller::finalizeGeometryCopiesToGPU(SIntendedSubmitInfo& intendedNextSubmit) +{ + // Copy DrawObjects + uint32_t remainingDrawObjects = currentDrawObjectCount - inMemDrawObjectCount; + SBufferRange drawObjectsRange = { sizeof(DrawObject) * inMemDrawObjectCount, sizeof(DrawObject) * remainingDrawObjects, gpuDrawBuffers.drawObjectsBuffer }; + const DrawObject* srcDrawObjData = reinterpret_cast(cpuDrawBuffers.drawObjectsBuffer->getPointer()) + inMemDrawObjectCount; + if (drawObjectsRange.size > 0u) + m_utilities->updateBufferRangeViaStagingBuffer(intendedNextSubmit, drawObjectsRange, srcDrawObjData); + inMemDrawObjectCount = currentDrawObjectCount; - if (!addGridDTM_Internal(gridDTMInfo, mainObjectIdx)) - { - // single grid DTM couldn't fit into memory to push to gpu, so we submit rendering current objects and reset geometry buffer and draw objects - submitCurrentDrawObjectsAndReset(intendedNextSubmit, mainObjectIdx); - const bool success = addGridDTM_Internal(gridDTMInfo, mainObjectIdx); - if (!success) - { - m_logger.log("addGridDTM_Internal failed, even after overflow-submission, this is irrecoverable.", nbl::system::ILogger::ELL_ERROR); - assert(false); - } - } + // Copy GeometryBuffer + uint64_t remainingGeometrySize = currentGeometryBufferSize - inMemGeometryBufferSize; + SBufferRange geomRange = { inMemGeometryBufferSize, remainingGeometrySize, gpuDrawBuffers.geometryBuffer }; + const uint8_t* srcGeomData = reinterpret_cast(cpuDrawBuffers.geometryBuffer->getPointer()) + inMemGeometryBufferSize; + if (geomRange.size > 0u) + m_utilities->updateBufferRangeViaStagingBuffer(intendedNextSubmit, geomRange, srcGeomData); + inMemGeometryBufferSize = currentGeometryBufferSize; +} - endMainObject(); +void DrawResourcesFiller::finalizeLineStyleCopiesToGPU(SIntendedSubmitInfo& intendedNextSubmit) +{ + // Copy LineStyles + uint32_t remainingLineStyles = currentLineStylesCount - inMemLineStylesCount; + SBufferRange stylesRange = { sizeof(LineStyle) * inMemLineStylesCount, sizeof(LineStyle) * remainingLineStyles, gpuDrawBuffers.lineStylesBuffer }; + const LineStyle* srcLineStylesData = reinterpret_cast(cpuDrawBuffers.lineStylesBuffer->getPointer()) + inMemLineStylesCount; + if (stylesRange.size > 0u) + m_utilities->updateBufferRangeViaStagingBuffer(intendedNextSubmit, stylesRange, srcLineStylesData); + inMemLineStylesCount = currentLineStylesCount; } -void DrawResourcesFiller::addImageObject(image_id imageID, const OrientedBoundingBox2D& obb, SIntendedSubmitInfo& intendedNextSubmit) +void DrawResourcesFiller::finalizeTextureCopies(SIntendedSubmitInfo& intendedNextSubmit) { - beginMainObject(MainObjectType::STATIC_IMAGE); + auto cmdBuff = intendedNextSubmit.getScratchCommandBuffer(); + + auto msdfImage = msdfTextureArray->getCreationParameters().image; + + msdfTextureArrayIndicesUsed.clear(); - uint32_t mainObjIdx = acquireActiveMainObjectIndex_SubmitIfNeeded(intendedNextSubmit); - if (mainObjIdx == InvalidMainObjectIdx) + // if (!textureCopies.size()) + // return; + + // preparing images for copy + using image_barrier_t = IGPUCommandBuffer::SPipelineBarrierDependencyInfo::image_barrier_t; + std::vector barriers; + barriers.reserve(textureCopies.size()); { - m_logger.log("addImageObject: acquireActiveMainObjectIndex returned invalid index", nbl::system::ILogger::ELL_ERROR); - assert(false); - return; + barriers.push_back({ + .barrier = { + .dep = { + .srcStageMask = PIPELINE_STAGE_FLAGS::ALL_COMMANDS_BITS, + .srcAccessMask = ACCESS_FLAGS::TRANSFER_WRITE_BIT, + .dstStageMask = PIPELINE_STAGE_FLAGS::COPY_BIT, + .dstAccessMask = ACCESS_FLAGS::MEMORY_WRITE_BITS, + } + // .ownershipOp. No queueFam ownership transfer + }, + .image = msdfImage.get(), + .subresourceRange = { + .aspectMask = IImage::EAF_COLOR_BIT, + .baseMipLevel = 0u, + .levelCount = msdfImage->getCreationParameters().mipLevels, + .baseArrayLayer = 0u, + .layerCount = msdfTextureArray->getCreationParameters().image->getCreationParameters().arrayLayers, + }, + .oldLayout = m_hasInitializedMSDFTextureArrays ? IImage::LAYOUT::READ_ONLY_OPTIMAL : IImage::LAYOUT::UNDEFINED, + .newLayout = IImage::LAYOUT::TRANSFER_DST_OPTIMAL, + }); + video::IGPUCommandBuffer::SPipelineBarrierDependencyInfo barrierInfo = { .imgBarriers = barriers }; + cmdBuff->pipelineBarrier( + static_cast(0u), + barrierInfo); + + if (!m_hasInitializedMSDFTextureArrays) + m_hasInitializedMSDFTextureArrays = true; } - ImageObjectInfo info = {}; - info.topLeft = obb.topLeft; - info.dirU = obb.dirU; - info.aspectRatio = obb.aspectRatio; - info.textureID = getImageIndexFromID(imageID, intendedNextSubmit); // for this to be valid and safe, this function needs to be called immediately after `addStaticImage` function to make sure image is in memory - if (!addImageObject_Internal(info, mainObjIdx)) + for (uint32_t i = 0; i < textureCopies.size(); i++) { - // single image object couldn't fit into memory to push to gpu, so we submit rendering current objects and reset geometry buffer and draw objects - submitCurrentDrawObjectsAndReset(intendedNextSubmit, mainObjIdx); - const bool success = addImageObject_Internal(info, mainObjIdx); - if (!success) + auto& textureCopy = textureCopies[i]; + for (uint32_t mip = 0; mip < textureCopy.image->getCreationParameters().mipLevels; mip++) { - m_logger.log("addImageObject_Internal failed, even after overflow-submission, this is irrecoverable.", nbl::system::ILogger::ELL_ERROR); - assert(false); + auto mipImageRegion = textureCopy.image->getRegion(mip, core::vectorSIMDu32(0u, 0u)); + + asset::IImage::SBufferCopy region = {}; + region.imageSubresource.aspectMask = asset::IImage::EAF_COLOR_BIT; + region.imageSubresource.mipLevel = mipImageRegion->imageSubresource.mipLevel; + region.imageSubresource.baseArrayLayer = textureCopy.index; + region.imageSubresource.layerCount = 1u; + region.bufferOffset = 0u; + region.bufferRowLength = mipImageRegion->getExtent().width; + region.bufferImageHeight = 0u; + region.imageExtent = mipImageRegion->imageExtent; + region.imageOffset = { 0u, 0u, 0u }; + + auto buffer = reinterpret_cast(textureCopy.image->getBuffer()->getPointer()); + auto bufferOffset = mipImageRegion->bufferOffset; + + m_utilities->updateImageViaStagingBuffer( + intendedNextSubmit, + buffer + bufferOffset, nbl::ext::TextRendering::TextRenderer::MSDFTextureFormat, + msdfImage.get(), IImage::LAYOUT::TRANSFER_DST_OPTIMAL, + { ®ion, ®ion + 1 }); } } + + // preparing images for use + { + barriers.clear(); + barriers.push_back({ + .barrier = { + .dep = { + .srcStageMask = PIPELINE_STAGE_FLAGS::COPY_BIT, + .srcAccessMask = ACCESS_FLAGS::MEMORY_WRITE_BITS, + .dstStageMask = PIPELINE_STAGE_FLAGS::FRAGMENT_SHADER_BIT, // we READ/SAMPLE on FRAG_SHADER + .dstAccessMask = ACCESS_FLAGS::SHADER_READ_BITS, + } + // .ownershipOp. No queueFam ownership transfer + }, + .image = msdfImage.get(), + .subresourceRange = { + .aspectMask = IImage::EAF_COLOR_BIT, + .baseMipLevel = 0u, + .levelCount = msdfImage->getCreationParameters().mipLevels, + .baseArrayLayer = 0u, + .layerCount = msdfTextureArray->getCreationParameters().image->getCreationParameters().arrayLayers, + }, + .oldLayout = IImage::LAYOUT::TRANSFER_DST_OPTIMAL, + .newLayout = IImage::LAYOUT::READ_ONLY_OPTIMAL, + }); + video::IGPUCommandBuffer::SPipelineBarrierDependencyInfo barrierInfo = { .imgBarriers = barriers }; + cmdBuff->pipelineBarrier( + static_cast(0u), + barrierInfo); + } - endMainObject(); + // done copying textures + textureCopies.clear(); } -uint32_t2 DrawResourcesFiller::computeStreamingImageExtentsForViewportCoverage(const uint32_t2 viewportExtents) +void DrawResourcesFiller::submitCurrentObjectsAndReset(SIntendedSubmitInfo& intendedNextSubmit, uint32_t mainObjectIndex) { - const uint32_t diagonal = static_cast(nbl::hlsl::ceil( - nbl::hlsl::sqrt(static_cast( - viewportExtents.x * viewportExtents.x + viewportExtents.y * viewportExtents.y)) - )); + finalizeAllCopiesToGPU(intendedNextSubmit); + submitDraws(intendedNextSubmit); - const uint32_t gpuImageSidelength = - 2 * core::roundUp(diagonal, GeoreferencedImageTileSize) + - GeoreferencedImagePaddingTiles * GeoreferencedImageTileSize; + // We reset Geometry Counters (drawObj+geometryInfos) because we're done rendering previous geometry + // We don't reset counters for styles because we will be reusing them + resetGeometryCounters(); - return { gpuImageSidelength, gpuImageSidelength }; + uint64_t newClipProjectionAddress = acquireCurrentClipProjectionAddress(intendedNextSubmit); + // If the clip projection stack is non-empty, then it means we need to re-push the clipProjectionData (because it exists in geometry data and it was reset) + if (newClipProjectionAddress != InvalidClipProjectionAddress) + { + // then modify the mainObject data + getMainObject(mainObjectIndex)->clipProjectionAddress = newClipProjectionAddress; + // we need to rewind back inMemMainObjectCount to this mainObjIndex so it re-uploads the current mainObject (because we modified it) + inMemMainObjectCount = min(inMemMainObjectCount, mainObjectIndex); + } } -nbl::core::smart_refctd_ptr DrawResourcesFiller::ensureGeoreferencedImageEntry(image_id imageID, const OrientedBoundingBox2D& worldSpaceOBB, const uint32_t2 currentViewportExtents, const float64_t3x3& ndcToWorldMat, const std::filesystem::path& storagePath) +uint32_t DrawResourcesFiller::addMainObject_Internal(const MainObject& mainObject) { - nbl::core::smart_refctd_ptr ret = nullptr; + MainObject* mainObjsArray = reinterpret_cast(cpuDrawBuffers.mainObjectsBuffer->getPointer()); + + if (currentMainObjectCount >= MaxIndexableMainObjects) + return InvalidMainObjectIdx; + if (currentMainObjectCount >= maxMainObjects) + return InvalidMainObjectIdx; - auto* physDev = m_device->getPhysicalDevice(); + void* dst = mainObjsArray + currentMainObjectCount; + memcpy(dst, &mainObject, sizeof(MainObject)); + uint32_t ret = currentMainObjectCount; + currentMainObjectCount++; + return ret; +} - if (!imageLoader) +uint32_t DrawResourcesFiller::addLineStyle_Internal(const LineStyleInfo& lineStyleInfo) +{ + LineStyle gpuLineStyle = lineStyleInfo.getAsGPUData(); + _NBL_DEBUG_BREAK_IF(gpuLineStyle.stipplePatternSize > LineStyle::StipplePatternMaxSize); // Oops, even after style normalization the style is too long to be in gpu mem :( + LineStyle* stylesArray = reinterpret_cast(cpuDrawBuffers.lineStylesBuffer->getPointer()); + for (uint32_t i = 0u; i < currentLineStylesCount; ++i) { - m_logger.log("imageLoader is null/empty. make sure to register your loader!", nbl::system::ILogger::ELL_ERROR); - return nullptr; + const LineStyle& itr = stylesArray[i]; + + if (itr == gpuLineStyle) + return i; } - uint32_t2 fullResImageExtents = imageLoader->getExtents(storagePath); - asset::E_FORMAT format = imageLoader->getFormat(storagePath); + if (currentLineStylesCount >= maxLineStyles) + return InvalidStyleIdx; - uint32_t2 gpuImageExtents = computeStreamingImageExtentsForViewportCoverage(currentViewportExtents); + void* dst = stylesArray + currentLineStylesCount; + memcpy(dst, &gpuLineStyle, sizeof(LineStyle)); + return currentLineStylesCount++; +} - IGPUImage::SCreationParams gpuImageCreationParams = {}; - gpuImageCreationParams.type = asset::IImage::ET_2D; - gpuImageCreationParams.samples = asset::IImage::ESCF_1_BIT; - gpuImageCreationParams.format = format; - gpuImageCreationParams.extent = { .width = gpuImageExtents.x, .height = gpuImageExtents.y, .depth = 1u }; - gpuImageCreationParams.mipLevels = 2u; - gpuImageCreationParams.arrayLayers = 1u; +uint64_t DrawResourcesFiller::acquireCurrentClipProjectionAddress(SIntendedSubmitInfo& intendedNextSubmit) +{ + if (clipProjectionAddresses.empty()) + return InvalidClipProjectionAddress; - gpuImageCreationParams.usage |= IGPUImage::EUF_TRANSFER_DST_BIT|IGPUImage::EUF_SAMPLED_BIT; - // promote format because RGB8 and friends don't actually exist in HW - { - const IPhysicalDevice::SImageFormatPromotionRequest request = { - .originalFormat = gpuImageCreationParams.format, - .usages = IPhysicalDevice::SFormatImageUsages::SUsage(gpuImageCreationParams.usage) - }; - gpuImageCreationParams.format = physDev->promoteImageFormat(request,gpuImageCreationParams.tiling); - } + if (clipProjectionAddresses.back() == InvalidClipProjectionAddress) + clipProjectionAddresses.back() = addClipProjectionData_SubmitIfNeeded(clipProjections.back(), intendedNextSubmit); - CachedImageRecord* cachedImageRecord = imagesCache->get(imageID); - if (!cachedImageRecord) + return clipProjectionAddresses.back(); +} + +uint64_t DrawResourcesFiller::addClipProjectionData_SubmitIfNeeded(const ClipProjectionData& clipProjectionData, SIntendedSubmitInfo& intendedNextSubmit) +{ + uint64_t outClipProjectionAddress = addClipProjectionData_Internal(clipProjectionData); + if (outClipProjectionAddress == InvalidClipProjectionAddress) { - ret = nbl::core::make_smart_refctd_ptr(); - const bool initSuccess = ret->init(worldSpaceOBB, fullResImageExtents, format, storagePath); - if (!initSuccess) - m_logger.log("Failed to init GeoreferencedImageStreamingState!", nbl::system::ILogger::ELL_ERROR); + finalizeAllCopiesToGPU(intendedNextSubmit); + submitDraws(intendedNextSubmit); + + resetGeometryCounters(); + resetMainObjectCounters(); + + outClipProjectionAddress = addClipProjectionData_Internal(clipProjectionData); + assert(outClipProjectionAddress != InvalidClipProjectionAddress); } - else - { - // StreamingState already in cache, we return it; - if (!cachedImageRecord->georeferencedImageState) - m_logger.log("image had entry in the cache but cachedImageRecord->georeferencedImageState was nullptr, this shouldn't happen!", nbl::system::ILogger::ELL_ERROR); - ret = cachedImageRecord->georeferencedImageState; - } - - // Update GeoreferencedImageState with new viewport width/height and requirements - - // width only because gpu image is square - const uint32_t newGPUImageSideLengthTiles = gpuImageCreationParams.extent.width / GeoreferencedImageTileSize; - - // This will reset the residency state after a resize. it makes sense because when gpu image is resized, it's recreated and no previous tile is resident anymore - // We don't copy tiles between prev/next resized image, we're more focused on optimizing pan/zoom with a fixed window size. - if (ret->gpuImageSideLengthTiles != newGPUImageSideLengthTiles) - { - ret->gpuImageSideLengthTiles = newGPUImageSideLengthTiles; - ret->ResetTileOccupancyState(); - ret->currentMappedRegionTileRange = { .baseMipLevel = std::numeric_limits::max() }; - } - - ret->gpuImageCreationParams = std::move(gpuImageCreationParams); - // Update with current viewport - ret->updateStreamingStateForViewport(currentViewportExtents, ndcToWorldMat); - - return ret; -} - -bool DrawResourcesFiller::launchGeoreferencedImageTileLoads(image_id imageID, GeoreferencedImageStreamingState* imageStreamingState, const WorldClipRect clipRect) -{ - if (!imageStreamingState) - { - m_logger.log("imageStreamingState is null/empty, make sure `ensureGeoreferencedImageEntry` was called beforehand!", nbl::system::ILogger::ELL_ERROR); - assert(false); - return false; - } - - auto& thisImageQueuedCopies = streamedImageCopies[imageID]; - - const auto& viewportTileRange = imageStreamingState->currentViewportTileRange; - const uint32_t2 lastTileIndex = imageStreamingState->getLastTileIndex(viewportTileRange.baseMipLevel); - - // We need to make every tile that covers the viewport resident. We reserve the amount of tiles needed for upload. - auto tilesToLoad = imageStreamingState->tilesToLoad(); - - - // m_logger.log(std::format("Tiles to Load = {}.", tilesToLoad.size()).c_str(), nbl::system::ILogger::ELL_INFO); - - - const uint32_t2 imageExtents = imageStreamingState->fullResImageExtents; - const std::filesystem::path imageStoragePath = imageStreamingState->storagePath; - - // Figure out worldspace coordinates for each of the tile's corners - these are used if there's a clip rect - const float64_t2 imageTopLeft = imageStreamingState->worldspaceOBB.topLeft; - const float64_t2 dirU = float64_t2(imageStreamingState->worldspaceOBB.dirU); - const float64_t2 dirV = float64_t2(dirU.y, -dirU.x) * float64_t(imageStreamingState->worldspaceOBB.aspectRatio); - const uint32_t tileMipLevel = imageStreamingState->currentViewportTileRange.baseMipLevel; - - uint32_t ignored = 0; - for (auto [imageTileIndex, gpuImageTileIndex] : tilesToLoad) - { - // clip against current rect, if valid - if (clipRect.minClip.x != std::numeric_limits::signaling_NaN()) - { - float64_t2 topLeftWorld = imageTopLeft + dirU * (float64_t(GeoreferencedImageTileSize * imageTileIndex.x << tileMipLevel) / float64_t(imageExtents.x)) + dirV * (float64_t(GeoreferencedImageTileSize * imageTileIndex.y << tileMipLevel) / float64_t(imageExtents.y)); - float64_t2 topRightWorld = imageTopLeft + dirU * (float64_t(GeoreferencedImageTileSize * (imageTileIndex.x + 1) << tileMipLevel) / float64_t(imageExtents.x)) + dirV * (float64_t(GeoreferencedImageTileSize * imageTileIndex.y << tileMipLevel) / float64_t(imageExtents.y)); - float64_t2 bottomLeftWorld = imageTopLeft + dirU * (float64_t(GeoreferencedImageTileSize * imageTileIndex.x << tileMipLevel) / float64_t(imageExtents.x)) + dirV * (float64_t(GeoreferencedImageTileSize * (imageTileIndex.y + 1) << tileMipLevel) / float64_t(imageExtents.y)); - float64_t2 bottomRightWorld = imageTopLeft + dirU * (float64_t(GeoreferencedImageTileSize * (imageTileIndex.x + 1) << tileMipLevel) / float64_t(imageExtents.x)) + dirV * (float64_t(GeoreferencedImageTileSize * (imageTileIndex.y + 1) << tileMipLevel) / float64_t(imageExtents.y)); - - float64_t minX = std::min({ topLeftWorld.x, topRightWorld.x, bottomLeftWorld.x, bottomRightWorld.x }); - float64_t minY = std::min({ topLeftWorld.y, topRightWorld.y, bottomLeftWorld.y, bottomRightWorld.y }); - float64_t maxX = std::max({ topLeftWorld.x, topRightWorld.x, bottomLeftWorld.x, bottomRightWorld.x }); - float64_t maxY = std::max({ topLeftWorld.y, topRightWorld.y, bottomLeftWorld.y, bottomRightWorld.y }); - - // Check if the tile intersects clip rect at all. Note that y clips are inverted - if (maxX < clipRect.minClip.x || minX > clipRect.maxClip.x || maxY < clipRect.maxClip.y || minY > clipRect.minClip.y) - continue; - } - - uint32_t2 targetExtentMip0(GeoreferencedImageTileSize, GeoreferencedImageTileSize); - std::future> gpuMip0Tile; - std::future> gpuMip1Tile; - - { - uint32_t2 samplingExtentMip0 = uint32_t2(GeoreferencedImageTileSize, GeoreferencedImageTileSize) << viewportTileRange.baseMipLevel; - const uint32_t2 samplingOffsetMip0 = (imageTileIndex * GeoreferencedImageTileSize) << viewportTileRange.baseMipLevel; - - // If on the last tile, we might not load a full `GeoreferencedImageTileSize x GeoreferencedImageTileSize` tile, so we figure out how many pixels to load in this case to have - // minimal artifacts and no stretching - if (imageTileIndex.x == lastTileIndex.x) - { - samplingExtentMip0.x = imageStreamingState->lastTileSamplingExtent.x; - targetExtentMip0.x = imageStreamingState->lastTileTargetExtent.x; - // If the last tile is too small just ignore it - if (targetExtentMip0.x == 0u) - continue; - } - if (imageTileIndex.y == lastTileIndex.y) - { - samplingExtentMip0.y = imageStreamingState->lastTileSamplingExtent.y; - targetExtentMip0.y = imageStreamingState->lastTileTargetExtent.y; - // If the last tile is too small just ignore it - if (targetExtentMip0.y == 0u) - continue; - } - - if (!imageLoader->hasPrecomputedMips(imageStoragePath)) - { - gpuMip0Tile = std::async(std::launch::async, [=, this]() { - return imageLoader->load(imageStoragePath, samplingOffsetMip0, samplingExtentMip0, targetExtentMip0); - }); - gpuMip1Tile = std::async(std::launch::async, [=, this]() { - return imageLoader->load(imageStoragePath, samplingOffsetMip0, samplingExtentMip0, targetExtentMip0 / 2u); - }); - } - else - { - gpuMip0Tile = std::async(std::launch::async, [=, this]() { - return imageLoader->load(imageStoragePath, imageTileIndex * GeoreferencedImageTileSize, targetExtentMip0, imageStreamingState->currentMappedRegionTileRange.baseMipLevel, false); - }); - gpuMip1Tile = std::async(std::launch::async, [=, this]() { - return imageLoader->load(imageStoragePath, imageTileIndex * GeoreferencedImageTileSizeMip1, targetExtentMip0 / 2u, imageStreamingState->currentMappedRegionTileRange.baseMipLevel, true); - }); - } - } - - asset::IImage::SBufferCopy bufCopy; - bufCopy.bufferOffset = 0; - bufCopy.bufferRowLength = targetExtentMip0.x; - bufCopy.bufferImageHeight = 0; - bufCopy.imageSubresource.aspectMask = IImage::EAF_COLOR_BIT; - bufCopy.imageSubresource.mipLevel = 0u; - bufCopy.imageSubresource.baseArrayLayer = 0u; - bufCopy.imageSubresource.layerCount = 1u; - uint32_t2 gpuImageOffset = gpuImageTileIndex * GeoreferencedImageTileSize; - bufCopy.imageOffset = { gpuImageOffset.x, gpuImageOffset.y, 0u }; - bufCopy.imageExtent.width = targetExtentMip0.x; - bufCopy.imageExtent.height = targetExtentMip0.y; - bufCopy.imageExtent.depth = 1; - - thisImageQueuedCopies.emplace_back(imageStreamingState->sourceImageFormat, std::move(gpuMip0Tile), std::move(bufCopy)); - - // Upload the smaller tile to mip 1 - bufCopy = {}; - - bufCopy.bufferOffset = 0; - bufCopy.bufferRowLength = targetExtentMip0.x / 2; - bufCopy.bufferImageHeight = 0; - bufCopy.imageSubresource.aspectMask = IImage::EAF_COLOR_BIT; - bufCopy.imageSubresource.mipLevel = 1u; - bufCopy.imageSubresource.baseArrayLayer = 0u; - bufCopy.imageSubresource.layerCount = 1u; - gpuImageOffset /= 2; // Half tile size! - bufCopy.imageOffset = { gpuImageOffset.x, gpuImageOffset.y, 0u }; - bufCopy.imageExtent.width = targetExtentMip0.x / 2; - bufCopy.imageExtent.height = targetExtentMip0.y / 2; - bufCopy.imageExtent.depth = 1; - - thisImageQueuedCopies.emplace_back(imageStreamingState->sourceImageFormat, std::move(gpuMip1Tile), std::move(bufCopy)); - - // Mark tile as resident - imageStreamingState->currentMappedRegionOccupancy[gpuImageTileIndex.x][gpuImageTileIndex.y] = true; - } - - return true; -} - -bool DrawResourcesFiller::cancelGeoreferencedImageTileLoads(image_id imageID) -{ - auto it = streamedImageCopies.find(imageID); - if (it != streamedImageCopies.end()) - it->second.clear(); // clear the vector of copies for this image - - return true; -} - -void DrawResourcesFiller::drawGeoreferencedImage(image_id imageID, nbl::core::smart_refctd_ptr&& imageStreamingState, SIntendedSubmitInfo& intendedNextSubmit) -{ - // OutputDebugStringA(std::format("Image Cache Size = {} ", imagesCache->size()).c_str()); - - const bool resourcesEnsured = ensureGeoreferencedImageResources_AllocateIfNeeded(imageID, std::move(imageStreamingState), intendedNextSubmit); - if (resourcesEnsured) - { - // Georefernced Image Data in the cache was already pre-transformed from local to main worldspace coordinates for tile calculation purposes - // Because of this reason, the pre-transformed obb in the cache doesn't need to be transformed by custom projection again anymore. - // we push the identity transform to prevent any more tranformation on the obb which is already in worldspace units. - float64_t3x3 identity = float64_t3x3(1, 0, 0, 0, 1, 0, 0, 0, 1); - pushCustomProjection(identity); - - beginMainObject(MainObjectType::STREAMED_IMAGE); - - uint32_t mainObjIdx = acquireActiveMainObjectIndex_SubmitIfNeeded(intendedNextSubmit); - if (mainObjIdx != InvalidMainObjectIdx) - { - // Query imageType - auto cachedImageRecord = imagesCache->peek(imageID); - if (cachedImageRecord) - { - GeoreferencedImageInfo info = cachedImageRecord->georeferencedImageState->computeGeoreferencedImageAddressingAndPositioningInfo(); - info.textureID = getImageIndexFromID(imageID, intendedNextSubmit); // for this to be valid and safe, this function needs to be called immediately after `addStaticImage` function to make sure image is in memory - if (!addGeoreferencedImageInfo_Internal(info, mainObjIdx)) - { - // single image object couldn't fit into memory to push to gpu, so we submit rendering current objects and reset geometry buffer and draw objects - submitCurrentDrawObjectsAndReset(intendedNextSubmit, mainObjIdx); - const bool success = addGeoreferencedImageInfo_Internal(info, mainObjIdx); - if (!success) - { - m_logger.log("addGeoreferencedImageInfo_Internal failed, even after overflow-submission, this is irrecoverable.", nbl::system::ILogger::ELL_ERROR); - assert(false); - } - } - } - else - { - m_logger.log("drawGeoreferencedImage was not called immediately after enforceGeoreferencedImageAvailability!", nbl::system::ILogger::ELL_ERROR); - assert(false); - } - } - else - { - m_logger.log("drawGeoreferencedImage: acquireActiveMainObjectIndex returned invalid index", nbl::system::ILogger::ELL_ERROR); - assert(false); - } - - endMainObject(); - - popCustomProjection(); - } - else - { - m_logger.log("Failed to ensure resources (memory and descriptorIndex) for georeferencedImage", nbl::system::ILogger::ELL_ERROR); - } -} - -bool DrawResourcesFiller::finalizeGeoreferencedImageTileLoads(SIntendedSubmitInfo& intendedNextSubmit) -{ - bool success = true; - - if (streamedImageCopies.size() > 0ull) - { - auto* cmdBuffInfo = intendedNextSubmit.getCommandBufferForRecording(); - - if (cmdBuffInfo) - { - std::vector validCopies; - validCopies.reserve(streamedImageCopies.size()); - - // Step 1: collect valid image iters - for (auto it = streamedImageCopies.begin(); it != streamedImageCopies.end(); ++it) - { - const auto& imageID = it->first; - auto* imageRecord = imagesCache->peek(imageID); - - if (it->second.size() > 0u) - { - if (imageRecord && imageRecord->gpuImageView && imageRecord->georeferencedImageState) - validCopies.push_back(it); - else - m_logger.log(std::format("Can't upload to imageId {} yet. (no gpu record yet).", imageID).c_str(), nbl::system::ILogger::ELL_INFO); - } - } - - // m_logger.log(std::format("{} Valid Copies, Frame Idx = {}.", validCopies.size(), currentFrameIndex).c_str(), nbl::system::ILogger::ELL_INFO); - - if (validCopies.size() > 0u) - { - IGPUCommandBuffer* commandBuffer = cmdBuffInfo->cmdbuf; - std::vector beforeCopyImageBarriers; - beforeCopyImageBarriers.reserve(streamedImageCopies.size()); - - // Pipeline Barriers before imageCopy - for (auto it : validCopies) - { - auto& [imageID, imageCopies] = *it; - // OutputDebugStringA(std::format("Copying {} copies for Id = {} \n", imageCopies.size(), imageID).c_str()); - - auto* imageRecord = imagesCache->peek(imageID); - if (imageRecord == nullptr) - { - m_logger.log(std::format("`pushStreamedImagesUploads` failed, no image record found for image id {}.", imageID).c_str(), nbl::system::ILogger::ELL_ERROR); - continue; - } - - const auto& gpuImg = imageRecord->gpuImageView->getCreationParameters().image; - - IImage::LAYOUT newLayout = IImage::LAYOUT::GENERAL; - - beforeCopyImageBarriers.push_back( - { - .barrier = { - .dep = { - .srcStageMask = PIPELINE_STAGE_FLAGS::NONE, // previous top of pipe -> top_of_pipe in first scope = none - .srcAccessMask = ACCESS_FLAGS::NONE, - .dstStageMask = PIPELINE_STAGE_FLAGS::COPY_BIT, - .dstAccessMask = ACCESS_FLAGS::TRANSFER_WRITE_BIT, - } - // .ownershipOp. No queueFam ownership transfer - }, - .image = gpuImg.get(), - .subresourceRange = { - .aspectMask = IImage::E_ASPECT_FLAGS::EAF_COLOR_BIT, - .baseMipLevel = 0u, - .levelCount = ICPUImageView::remaining_mip_levels, - .baseArrayLayer = 0u, - .layerCount = ICPUImageView::remaining_array_layers - }, - .oldLayout = imageRecord->currentLayout, - .newLayout = newLayout, - }); - imageRecord->currentLayout = newLayout; - } - success &= commandBuffer->pipelineBarrier(E_DEPENDENCY_FLAGS::EDF_NONE, { .imgBarriers = beforeCopyImageBarriers }); - - for (auto it : validCopies) - { - auto& [imageID, imageCopies] = *it; - auto* imageRecord = imagesCache->peek(imageID); - if (imageRecord == nullptr) - continue; - - const auto& gpuImg = imageRecord->gpuImageView->getCreationParameters().image; - - for (auto& imageCopy : imageCopies) - { - auto srcBuffer = imageCopy.srcBufferFuture.get(); - if (srcBuffer) - { - const bool copySuccess = m_imageUploadUtils->updateImageViaStagingBuffer( - intendedNextSubmit, - srcBuffer->getPointer(), imageCopy.srcFormat, - gpuImg.get(), IImage::LAYOUT::GENERAL, - { &imageCopy.region, 1u }); - success &= copySuccess; - if (!copySuccess) - { - m_logger.log(std::format("updateImageViaStagingBuffer failed. region offset = ({}, {}), region size = ({}, {}), gpu image size = ({}, {})", - imageCopy.region.imageOffset.x,imageCopy.region.imageOffset.y, - imageCopy.region.imageExtent.width, imageCopy.region.imageExtent.height, - gpuImg->getCreationParameters().extent.width, gpuImg->getCreationParameters().extent.height).c_str(), nbl::system::ILogger::ELL_ERROR); - } - } - else - m_logger.log(std::format("srcBuffer was invalid for image id {}.", imageID).c_str(), nbl::system::ILogger::ELL_ERROR); - } - } - - commandBuffer = intendedNextSubmit.getCommandBufferForRecording()->cmdbuf; // overflow-submit in utilities calls might've cause current recording command buffer to change - - std::vector afterCopyImageBarriers; - afterCopyImageBarriers.reserve(streamedImageCopies.size()); - - // Pipeline Barriers after imageCopy - for (auto it : validCopies) - { - auto& [imageID, imageCopies] = *it; - auto* imageRecord = imagesCache->peek(imageID); - if (imageRecord == nullptr) - { - m_logger.log(std::format("`pushStreamedImagesUploads` failed, no image record found for image id {}.", imageID).c_str(), nbl::system::ILogger::ELL_ERROR); - continue; - } - - const auto& gpuImg = imageRecord->gpuImageView->getCreationParameters().image; - - IImage::LAYOUT newLayout = IImage::LAYOUT::GENERAL; - - afterCopyImageBarriers.push_back ( - { - .barrier = { - .dep = { - .srcStageMask = PIPELINE_STAGE_FLAGS::COPY_BIT, // previous top of pipe -> top_of_pipe in first scope = none - .srcAccessMask = ACCESS_FLAGS::TRANSFER_WRITE_BIT, - .dstStageMask = PIPELINE_STAGE_FLAGS::FRAGMENT_SHADER_BIT, - .dstAccessMask = ACCESS_FLAGS::SHADER_READ_BITS, - } - // .ownershipOp. No queueFam ownership transfer - }, - .image = gpuImg.get(), - .subresourceRange = { - .aspectMask = IImage::E_ASPECT_FLAGS::EAF_COLOR_BIT, - .baseMipLevel = 0u, - .levelCount = ICPUImageView::remaining_mip_levels, - .baseArrayLayer = 0u, - .layerCount = ICPUImageView::remaining_array_layers - }, - .oldLayout = imageRecord->currentLayout, - .newLayout = newLayout, - }); - imageRecord->currentLayout = newLayout; - } - success &= commandBuffer->pipelineBarrier(E_DEPENDENCY_FLAGS::EDF_NONE, { .imgBarriers = afterCopyImageBarriers }); - // Remove the processed valid ones, keep invalids for later retries - for (auto it : validCopies) - streamedImageCopies.erase(it); - } - } - else - { - _NBL_DEBUG_BREAK_IF(true); - success = false; - } - } - - if (!success) - { - m_logger.log("Failure in `pushStreamedImagesUploads`.", nbl::system::ILogger::ELL_ERROR); - _NBL_DEBUG_BREAK_IF(true); - } - return success; -} - -bool DrawResourcesFiller::pushAllUploads(SIntendedSubmitInfo& intendedNextSubmit) -{ - if (!intendedNextSubmit.valid()) - { - // It is a caching submit without command buffer, just for the purpose of accumulation of staging resources - // In that case we don't push any uploads (i.e. we don't record any imageRecord commmand in active command buffer, because there is no active command buffer) - return false; - } - - bool success = true; - - if (currentReplayCache) - { - // In rare cases, we need to wait for the previous frame's submit to ensure all GPU usage of the any images has completed. - nbl::video::ISemaphore::SWaitInfo previousSubmitWaitInfo = { .semaphore = intendedNextSubmit.scratchSemaphore.semaphore, .value = intendedNextSubmit.scratchSemaphore.value }; - - // This means we're in a replay cache scope, use the replay cache to push to GPU instead of internal accumulation - success &= pushBufferUploads(intendedNextSubmit, currentReplayCache->resourcesCollection); - success &= pushMSDFImagesUploads(intendedNextSubmit, currentReplayCache->msdfImagesState); - - bool evictedAnotherImage = false; - - // Push Static Images Uploads from replay cache, all the work below is necessary to detect whether our image to replay is already in the cache in the exact form OR we need to create new image + bind memory and set array index - for (auto& [toReplayImageID, toReplayRecord] : *currentReplayCache->imagesCache) - { - if (toReplayRecord.type != ImageType::STATIC) // non-static images (Georeferenced) won't be replayed like this - continue; - - auto* cachedRecord = imagesCache->peek(toReplayImageID); - bool alreadyResident = false; - - // compare with existing state, and check whether image id is already resident. - if (cachedRecord != nullptr) - { - const bool allocationMatches = - cachedRecord->allocationOffset == toReplayRecord.allocationOffset && - cachedRecord->allocationSize == toReplayRecord.allocationSize; - - const bool arrayIndexMatches = cachedRecord->arrayIndex == toReplayRecord.arrayIndex; - - alreadyResident = allocationMatches && arrayIndexMatches && cachedRecord->state != ImageState::INVALID; - } - - // if already resident, ignore, no need to insert into cache anymore - if (alreadyResident) - { - cachedRecord->lastUsedFrameIndex = currentFrameIndex; - } - else - { - // make sure to evict any cache entry that conflicts with the new entry (either in memory allocation or descriptor index) - if (evictConflictingImagesInCache_SubmitIfNeeded(toReplayImageID, toReplayRecord, intendedNextSubmit)) - evictedAnotherImage = true; - - // creating and inserting new entry - bool successCreateNewImage = false; - { - // Not already resident, we need to recreate the image and bind the image memory to correct location again, and update the descriptor set and push the uploads - auto existingGPUImageViewParams = toReplayRecord.gpuImageView->getCreationParameters(); - IGPUImage::SCreationParams imageParams = {}; - imageParams = existingGPUImageViewParams.image->getCreationParameters(); - - auto newGPUImage = m_device->createImage(std::move(imageParams)); - if (newGPUImage) - { - nbl::video::ILogicalDevice::SBindImageMemoryInfo bindImageMemoryInfo = - { - .image = newGPUImage.get(), - .binding = {.memory = imagesMemoryArena.memory.get(), .offset = imagesMemoryArena.offset + toReplayRecord.allocationOffset } - }; - - const bool boundToMemorySuccessfully = m_device->bindImageMemory({ &bindImageMemoryInfo, 1u }); - if (boundToMemorySuccessfully) - { - newGPUImage->setObjectDebugName((std::to_string(toReplayImageID) + " Static Image 2D").c_str()); - IGPUImageView::SCreationParams viewParams = existingGPUImageViewParams; - viewParams.image = newGPUImage; - - auto newGPUImageView = m_device->createImageView(std::move(viewParams)); - if (newGPUImageView) - { - successCreateNewImage = true; - toReplayRecord.arrayIndexAllocatedUsingImageDescriptorIndexAllocator = false; // array index wasn't allocated useing desc set suballocator. it's being replayed - toReplayRecord.gpuImageView = newGPUImageView; - toReplayRecord.state = ImageState::CREATED_AND_MEMORY_BOUND; - toReplayRecord.currentLayout = nbl::asset::IImage::LAYOUT::UNDEFINED; - toReplayRecord.lastUsedFrameIndex = currentFrameIndex; - newGPUImageView->setObjectDebugName((std::to_string(toReplayImageID) + " Static Image View 2D").c_str()); - } - - } - } - } - - if (successCreateNewImage) - { - // inserting the new entry into the cache (With new image and memory binding) - imagesCache->base_t::insert(toReplayImageID, toReplayRecord); - } - else - { - m_logger.log("Couldn't create new gpu image in pushAllUploads: cache and replay mode.", nbl::system::ILogger::ELL_ERROR); - _NBL_DEBUG_BREAK_IF(true); - success = false; - } - - } - } - - success &= pushStaticImagesUploads(intendedNextSubmit, *imagesCache); - - if (evictedAnotherImage) - { - // We're about to update the descriptor set binding using the replay's array indices. - // Normally, descriptor-set allocation and updates are synchronized to ensure the GPU - // isn't still using the same descriptor indices we're about to overwrite. - // - // However, in this case we bypassed the descriptor-set allocator (imagesDescriptorIndexAllocator) and are writing directly into the set. - // This means proper synchronization is not guaranteed. - // - // Since evicting another image can happen due to array index conflicts, - // we must ensure that any prior GPU work using those descriptor indices has finished before we update them. - // Therefore, wait for the previous frame (and any usage of these indices) to complete before proceeding to bind/write our images to their descriptor - m_device->blockForSemaphores({ &previousSubmitWaitInfo, 1u }); - } - - success &= updateDescriptorSetImageBindings(*imagesCache); - } - else - { - flushDrawObjects(); - success &= pushBufferUploads(intendedNextSubmit, resourcesCollection); - success &= pushMSDFImagesUploads(intendedNextSubmit, msdfImagesState); - success &= pushStaticImagesUploads(intendedNextSubmit, *imagesCache); - success &= updateDescriptorSetImageBindings(*imagesCache); - } - - - return success; -} - -const DrawResourcesFiller::ResourcesCollection& DrawResourcesFiller::getResourcesCollection() const -{ - if (currentReplayCache) - return currentReplayCache->resourcesCollection; - else - return resourcesCollection; -} - -void DrawResourcesFiller::setActiveLineStyle(const LineStyleInfo& lineStyle) -{ - activeLineStyle = lineStyle; - activeLineStyleIndex = InvalidStyleIdx; -} - -void DrawResourcesFiller::setActiveDTMSettings(const DTMSettingsInfo& dtmSettingsInfo) -{ - activeDTMSettings = dtmSettingsInfo; - activeDTMSettingsIndex = InvalidDTMSettingsIdx; -} - -void DrawResourcesFiller::beginMainObject(MainObjectType type, TransformationType transformationType) -{ - activeMainObjectType = type; - activeMainObjectTransformationType = transformationType; - activeMainObjectIndex = InvalidMainObjectIdx; -} - -void DrawResourcesFiller::endMainObject() -{ - activeMainObjectType = MainObjectType::NONE; - activeMainObjectTransformationType = TransformationType::TT_NORMAL; - activeMainObjectIndex = InvalidMainObjectIdx; -} - -void DrawResourcesFiller::pushCustomProjection(const float64_t3x3& projection) -{ - activeProjections.push_back(projection); - activeProjectionIndices.push_back(InvalidCustomProjectionIndex); -} - -void DrawResourcesFiller::popCustomProjection() -{ - if (activeProjections.empty()) - return; - - activeProjections.pop_back(); - activeProjectionIndices.pop_back(); -} - -void DrawResourcesFiller::pushCustomClipRect(const WorldClipRect& clipRect) -{ - activeClipRects.push_back(clipRect); - activeClipRectIndices.push_back(InvalidCustomClipRectIndex); -} - -void DrawResourcesFiller::popCustomClipRect() -{ if (activeClipRects.empty()) - return; - - activeClipRects.pop_back(); - activeClipRectIndices.pop_back(); -} - -/// For advanced use only, (passed to shaders for them to know if we overflow-submitted in the middle if a main obj -uint32_t DrawResourcesFiller::getActiveMainObjectIndex() const -{ - if (currentReplayCache) - return currentReplayCache->activeMainObjectIndex; - else - return activeMainObjectIndex; -} - -const std::vector& DrawResourcesFiller::getDrawCalls() const -{ - if (currentReplayCache) - return currentReplayCache->drawCallsData; - else - return drawCalls; -} - -std::unique_ptr DrawResourcesFiller::createReplayCache() -{ - flushDrawObjects(); - std::unique_ptr ret = std::unique_ptr(new ReplayCache); - ret->resourcesCollection = resourcesCollection; - ret->msdfImagesState = msdfImagesState; - for (auto& stagedMSDF : ret->msdfImagesState) - stagedMSDF.uploadedToGPU = false; // to trigger upload for all msdf functions again. - ret->drawCallsData = drawCalls; - ret->activeMainObjectIndex = activeMainObjectIndex; - ret->imagesCache = std::unique_ptr(new ImagesCache(ImagesBindingArraySize)); - - // m_logger.log(std::format("== createReplayCache, currentFrameIndex = {} ==", currentFrameIndex).c_str(), nbl::system::ILogger::ELL_INFO); - // imagesCache->logState(m_logger); - - for (auto& [imageID, record] : *imagesCache) - { - // Only return images in the cache used within the last frame - if (record.lastUsedFrameIndex == currentFrameIndex) - ret->imagesCache->base_t::insert(imageID, record); - } - - return ret; -} - -void DrawResourcesFiller::setReplayCache(ReplayCache* cache) -{ - currentReplayCache = cache; - // currentReplayCache->imagesCache->logState(m_logger); -} - -void DrawResourcesFiller::unsetReplayCache() -{ - currentReplayCache = nullptr; -} - -uint64_t DrawResourcesFiller::getImagesMemoryConsumption() const -{ - uint64_t ret = 0ull; - for (auto& [imageID, record] : *imagesCache) - ret += record.allocationSize; - return ret; -} - -DrawResourcesFiller::UsageData DrawResourcesFiller::getCurrentUsageData() -{ - UsageData ret = {}; - const auto& resources = getResourcesCollection(); - ret.lineStyleCount = resources.lineStyles.getCount(); - ret.dtmSettingsCount = resources.dtmSettings.getCount(); - ret.customProjectionsCount = resources.customProjections.getCount(); - ret.mainObjectCount = resources.mainObjects.getCount(); - ret.drawObjectCount = resources.drawObjects.getCount(); - ret.geometryBufferSize = resources.geometryInfo.getStorageSize(); - ret.bufferMemoryConsumption = resources.calculateTotalConsumption(); - ret.imageMemoryConsumption = getImagesMemoryConsumption(); - return ret; -} - -bool DrawResourcesFiller::pushBufferUploads(SIntendedSubmitInfo& intendedNextSubmit, ResourcesCollection& resources) -{ - copiedResourcesSize = 0ull; - - if (resourcesCollection.calculateTotalConsumption() > resourcesGPUBuffer->getSize()) - { - m_logger.log("some bug has caused the resourcesCollection to consume more memory than available in resourcesGPUBuffer without overflow submit", nbl::system::ILogger::ELL_ERROR); - assert(false); - return false; - } - - auto copyCPUFilledDrawBuffer = [&](auto& drawBuffer) -> bool - { - // drawBuffer must be of type CPUGeneratedResource - SBufferRange copyRange = { copiedResourcesSize, drawBuffer.getStorageSize(), resourcesGPUBuffer}; - - if (copyRange.offset + copyRange.size > resourcesGPUBuffer->getSize()) - { - m_logger.log("`copyRange.offset + copyRange.size > resourcesGPUBuffer->getSize()` is true in `copyCPUFilledDrawBuffer`, this shouldn't happen with correct auto-submission mechanism.", nbl::system::ILogger::ELL_ERROR); - assert(false); - return false; - } - - drawBuffer.bufferOffset = copyRange.offset; - if (copyRange.size > 0ull) - { - if (!m_bufferUploadUtils->updateBufferRangeViaStagingBuffer(intendedNextSubmit, copyRange, drawBuffer.vector.data())) - return false; - copiedResourcesSize += drawBuffer.getAlignedStorageSize(); - } - return true; - }; - - auto addComputeReservedFilledDrawBuffer = [&](auto& drawBuffer) -> bool - { - // drawBuffer must be of type ReservedComputeResource - SBufferRange copyRange = { copiedResourcesSize, drawBuffer.getStorageSize(), resourcesGPUBuffer}; - - if (copyRange.offset + copyRange.size > resourcesGPUBuffer->getSize()) - { - m_logger.log("`copyRange.offset + copyRange.size > resourcesGPUBuffer->getSize()` is true in `addComputeReservedFilledDrawBuffer`, this shouldn't happen with correct auto-submission mechanism.", nbl::system::ILogger::ELL_ERROR); - assert(false); - return false; - } - - drawBuffer.bufferOffset = copyRange.offset; - copiedResourcesSize += drawBuffer.getAlignedStorageSize(); - }; - - copyCPUFilledDrawBuffer(resources.lineStyles); - copyCPUFilledDrawBuffer(resources.dtmSettings); - copyCPUFilledDrawBuffer(resources.customProjections); - copyCPUFilledDrawBuffer(resources.customClipRects); - copyCPUFilledDrawBuffer(resources.mainObjects); - copyCPUFilledDrawBuffer(resources.drawObjects); - copyCPUFilledDrawBuffer(resources.indexBuffer); - copyCPUFilledDrawBuffer(resources.geometryInfo); - - return true; -} - -bool DrawResourcesFiller::pushMSDFImagesUploads(SIntendedSubmitInfo& intendedNextSubmit, std::vector& stagedMSDFCPUImages) -{ - auto* cmdBuffInfo = intendedNextSubmit.getCommandBufferForRecording(); - - if (cmdBuffInfo) - { - IGPUCommandBuffer* commandBuffer = cmdBuffInfo->cmdbuf; - - auto msdfImage = msdfTextureArray->getCreationParameters().image; - - // preparing msdfs for imageRecord - using image_barrier_t = IGPUCommandBuffer::SPipelineBarrierDependencyInfo::image_barrier_t; - image_barrier_t beforeTransferImageBarrier[] = - { - { - .barrier = { - .dep = { - .srcStageMask = PIPELINE_STAGE_FLAGS::ALL_COMMANDS_BITS, - .srcAccessMask = ACCESS_FLAGS::TRANSFER_WRITE_BIT, - .dstStageMask = PIPELINE_STAGE_FLAGS::COPY_BIT, - .dstAccessMask = ACCESS_FLAGS::MEMORY_WRITE_BITS, - } - // .ownershipOp. No queueFam ownership transfer - }, - .image = msdfImage.get(), - .subresourceRange = { - .aspectMask = IImage::EAF_COLOR_BIT, - .baseMipLevel = 0u, - .levelCount = msdfImage->getCreationParameters().mipLevels, - .baseArrayLayer = 0u, - .layerCount = msdfTextureArray->getCreationParameters().image->getCreationParameters().arrayLayers, - }, - .oldLayout = m_hasInitializedMSDFTextureArrays ? IImage::LAYOUT::READ_ONLY_OPTIMAL : IImage::LAYOUT::UNDEFINED, - .newLayout = IImage::LAYOUT::TRANSFER_DST_OPTIMAL, - } - }; - commandBuffer->pipelineBarrier(E_DEPENDENCY_FLAGS::EDF_NONE, { .imgBarriers = beforeTransferImageBarrier }); - - // Do the copies and advance the iterator. - // this is the pattern we use for iterating when entries will get erased if processed successfully, but may get skipped for later. - for (uint32_t i = 0u; i < stagedMSDFCPUImages.size(); ++i) - { - auto& stagedMSDF = stagedMSDFCPUImages[i]; - if (stagedMSDF.image && i < msdfImage->getCreationParameters().arrayLayers) - { - for (uint32_t mip = 0; mip < stagedMSDF.image->getCreationParameters().mipLevels; mip++) - { - auto mipImageRegion = stagedMSDF.image->getRegion(mip, core::vectorSIMDu32(0u, 0u)); - if (mipImageRegion) - { - asset::IImage::SBufferCopy region = {}; - region.imageSubresource.aspectMask = asset::IImage::EAF_COLOR_BIT; - region.imageSubresource.mipLevel = mipImageRegion->imageSubresource.mipLevel; - region.imageSubresource.baseArrayLayer = i; - region.imageSubresource.layerCount = 1u; - region.bufferOffset = 0u; - region.bufferRowLength = mipImageRegion->getExtent().width; - region.bufferImageHeight = 0u; - region.imageExtent = mipImageRegion->imageExtent; - region.imageOffset = { 0u, 0u, 0u }; - - auto buffer = reinterpret_cast(stagedMSDF.image->getBuffer()->getPointer()); - auto bufferOffset = mipImageRegion->bufferOffset; - - stagedMSDF.uploadedToGPU = m_bufferUploadUtils->updateImageViaStagingBuffer( - intendedNextSubmit, - buffer + bufferOffset, - nbl::ext::TextRendering::TextRenderer::MSDFTextureFormat, - msdfImage.get(), - IImage::LAYOUT::TRANSFER_DST_OPTIMAL, - { ®ion, ®ion + 1 }); - } - else - { - assert(false); - stagedMSDF.uploadedToGPU = false; - } - } - } - else - { - stagedMSDF.uploadedToGPU = false; - } - } - - commandBuffer = intendedNextSubmit.getCommandBufferForRecording()->cmdbuf; // overflow-submit in utilities calls might've cause current recording command buffer to change - - // preparing msdfs for use - image_barrier_t afterTransferImageBarrier[] = - { - { - .barrier = { - .dep = { - .srcStageMask = PIPELINE_STAGE_FLAGS::COPY_BIT, - .srcAccessMask = ACCESS_FLAGS::MEMORY_WRITE_BITS, - .dstStageMask = PIPELINE_STAGE_FLAGS::FRAGMENT_SHADER_BIT, // we READ/SAMPLE on FRAG_SHADER - .dstAccessMask = ACCESS_FLAGS::SHADER_READ_BITS, - } - // .ownershipOp. No queueFam ownership transfer - }, - .image = msdfImage.get(), - .subresourceRange = { - .aspectMask = IImage::EAF_COLOR_BIT, - .baseMipLevel = 0u, - .levelCount = msdfImage->getCreationParameters().mipLevels, - .baseArrayLayer = 0u, - .layerCount = msdfTextureArray->getCreationParameters().image->getCreationParameters().arrayLayers, - }, - .oldLayout = IImage::LAYOUT::TRANSFER_DST_OPTIMAL, - .newLayout = IImage::LAYOUT::READ_ONLY_OPTIMAL, - } - }; - commandBuffer->pipelineBarrier(E_DEPENDENCY_FLAGS::EDF_NONE, { .imgBarriers = afterTransferImageBarrier }); - - if (!m_hasInitializedMSDFTextureArrays) - m_hasInitializedMSDFTextureArrays = true; - - return true; - } - else - { - m_logger.log("`copyRange.offset + copyRange.size > resourcesGPUBuffer->getSize()` is true in `addComputeReservedFilledDrawBuffer`, this shouldn't happen with correct auto-submission mechanism.", nbl::system::ILogger::ELL_ERROR); - return false; - } -} - -bool DrawResourcesFiller::updateDescriptorSetImageBindings(ImagesCache& imagesCache) -{ - bool success = true; - - auto* descriptorSet = imagesDescriptorIndexAllocator->getDescriptorSet(); - - // DescriptorSet Updates - std::vector descriptorInfos; - std::vector descriptorWrites; - descriptorInfos.resize(imagesCache.size()); - descriptorWrites.resize(imagesCache.size()); - - // Potential GPU waits before writing to descriptor bindings that were previously deallocated manually (bypassing the imagesDescriptorIndexAllocator). - // The allocator normally guarantees safe reuse of array indices by synchronizing allocations and deallocations internally. - // but since these bindings were queued for deferred deallocation, we must ensure their previous GPU usage has completed before writing new data into those slots. - std::vector waitInfos; - waitInfos.reserve(deferredDescriptorIndexDeallocations.size()); - - uint32_t descriptorWriteCount = 0u; - for (auto& [id, record] : imagesCache) - { - if (record.state >= ImageState::BOUND_TO_DESCRIPTOR_SET || !record.gpuImageView) - continue; - - // Check if this writing to this array index has a deferred deallocation pending - if (auto it = deferredDescriptorIndexDeallocations.find(record.arrayIndex); it != deferredDescriptorIndexDeallocations.end()) - { - // TODO: Assert we're not waiting for a value which hasn't been submitted yet. - waitInfos.push_back(it->second); - // erase -> it's a one-time wait: - deferredDescriptorIndexDeallocations.erase(it); - } - - // Bind gpu image view to descriptor set - video::IGPUDescriptorSet::SDescriptorInfo descriptorInfo = {}; - descriptorInfo.info.image.imageLayout = (record.type == ImageType::STATIC) ? IImage::LAYOUT::READ_ONLY_OPTIMAL : IImage::LAYOUT::GENERAL; // WARN: don't use `record.currentLayout`, it's the layout "At the time" the image is going to be accessed - descriptorInfo.desc = record.gpuImageView; - descriptorInfos[descriptorWriteCount] = descriptorInfo; - - // consider batching contiguous writes, if descriptor set updating was a hotspot - IGPUDescriptorSet::SWriteDescriptorSet descriptorWrite = {}; - descriptorWrite.dstSet = descriptorSet; - descriptorWrite.binding = imagesArrayBinding; - descriptorWrite.arrayElement = record.arrayIndex; - descriptorWrite.count = 1u; - descriptorWrite.info = &descriptorInfos[descriptorWriteCount]; - descriptorWrites[descriptorWriteCount] = descriptorWrite; - - - record.state = ImageState::BOUND_TO_DESCRIPTOR_SET; - descriptorWriteCount++; - } - - if (!waitInfos.empty()) - m_device->blockForSemaphores(waitInfos, /*waitAll=*/true); - - if (descriptorWriteCount > 0u) - success &= m_device->updateDescriptorSets(descriptorWriteCount, descriptorWrites.data(), 0u, nullptr); - - return success; -} - -bool DrawResourcesFiller::pushStaticImagesUploads(SIntendedSubmitInfo& intendedNextSubmit, ImagesCache& imagesCache) -{ - bool success = true; - - // Push Static Images Uploads, only those who are not gpu resident - // TODO: remove this vector and check state in each for loop below? - std::vector nonResidentImageRecords; - for (auto& [id, record] : imagesCache) - { - if (record.staticCPUImage && record.type == ImageType::STATIC && record.state < ImageState::GPU_RESIDENT_WITH_VALID_STATIC_DATA) - nonResidentImageRecords.push_back(&record); - } - - if (nonResidentImageRecords.size() > 0ull) - { - auto* cmdBuffInfo = intendedNextSubmit.getCommandBufferForRecording(); - - if (cmdBuffInfo) - { - IGPUCommandBuffer* commandBuffer = cmdBuffInfo->cmdbuf; - - std::vector beforeCopyImageBarriers; - beforeCopyImageBarriers.resize(nonResidentImageRecords.size()); - - // Pipeline Barriers before imageRecord - for (uint32_t i = 0u; i < nonResidentImageRecords.size(); ++i) - { - auto& imageRecord = *nonResidentImageRecords[i]; - const auto& gpuImg = imageRecord.gpuImageView->getCreationParameters().image; - beforeCopyImageBarriers[i] = - { - .barrier = { - .dep = { - .srcStageMask = PIPELINE_STAGE_FLAGS::NONE, // previous top of pipe -> top_of_pipe in first scope = none - .srcAccessMask = ACCESS_FLAGS::NONE, - .dstStageMask = PIPELINE_STAGE_FLAGS::COPY_BIT, - .dstAccessMask = ACCESS_FLAGS::TRANSFER_WRITE_BIT, - } - // .ownershipOp. No queueFam ownership transfer - }, - .image = gpuImg.get(), - .subresourceRange = { - .aspectMask = IImage::E_ASPECT_FLAGS::EAF_COLOR_BIT, - .baseMipLevel = 0u, - .levelCount = ICPUImageView::remaining_mip_levels, - .baseArrayLayer = 0u, - .layerCount = ICPUImageView::remaining_array_layers - }, - .oldLayout = imageRecord.currentLayout, - .newLayout = IImage::LAYOUT::TRANSFER_DST_OPTIMAL, - }; - imageRecord.currentLayout = beforeCopyImageBarriers[i].newLayout; - } - success &= commandBuffer->pipelineBarrier(E_DEPENDENCY_FLAGS::EDF_NONE, { .imgBarriers = beforeCopyImageBarriers }); - - for (uint32_t i = 0u; i < nonResidentImageRecords.size(); ++i) - { - auto& imageRecord = *nonResidentImageRecords[i]; - auto& gpuImg = imageRecord.gpuImageView->getCreationParameters().image; - success &= m_imageUploadUtils->updateImageViaStagingBuffer( - intendedNextSubmit, - imageRecord.staticCPUImage->getBuffer()->getPointer(), imageRecord.staticCPUImage->getCreationParameters().format, - gpuImg.get(), IImage::LAYOUT::TRANSFER_DST_OPTIMAL, - imageRecord.staticCPUImage->getRegions()); - - if (success) - imageRecord.state = ImageState::GPU_RESIDENT_WITH_VALID_STATIC_DATA; - else - { - m_logger.log("Failed `updateImageViaStagingBuffer` in pushStaticImagesUploads.", nbl::system::ILogger::ELL_ERROR); - } - } - - commandBuffer = intendedNextSubmit.getCommandBufferForRecording()->cmdbuf; // overflow-submit in utilities calls might've cause current recording command buffer to change - - std::vector afterCopyImageBarriers; - afterCopyImageBarriers.resize(nonResidentImageRecords.size()); - - // Pipeline Barriers before imageRecord - for (uint32_t i = 0u; i < nonResidentImageRecords.size(); ++i) - { - auto& imageRecord = *nonResidentImageRecords[i]; - const auto& gpuImg = imageRecord.gpuImageView->getCreationParameters().image; - afterCopyImageBarriers[i] = - { - .barrier = { - .dep = { - .srcStageMask = PIPELINE_STAGE_FLAGS::COPY_BIT, // previous top of pipe -> top_of_pipe in first scope = none - .srcAccessMask = ACCESS_FLAGS::TRANSFER_WRITE_BIT, - .dstStageMask = PIPELINE_STAGE_FLAGS::FRAGMENT_SHADER_BIT, - .dstAccessMask = ACCESS_FLAGS::SHADER_READ_BITS, - } - // .ownershipOp. No queueFam ownership transfer - }, - .image = gpuImg.get(), - .subresourceRange = { - .aspectMask = IImage::E_ASPECT_FLAGS::EAF_COLOR_BIT, - .baseMipLevel = 0u, - .levelCount = ICPUImageView::remaining_mip_levels, - .baseArrayLayer = 0u, - .layerCount = ICPUImageView::remaining_array_layers - }, - .oldLayout = imageRecord.currentLayout, - .newLayout = IImage::LAYOUT::READ_ONLY_OPTIMAL, - }; - imageRecord.currentLayout = afterCopyImageBarriers[i].newLayout; - } - success &= commandBuffer->pipelineBarrier(E_DEPENDENCY_FLAGS::EDF_NONE, { .imgBarriers = afterCopyImageBarriers }); - } - else - { - _NBL_DEBUG_BREAK_IF(true); - success = false; - } - } - - if (!success) - { - m_logger.log("Failure in `pushStaticImagesUploads`.", nbl::system::ILogger::ELL_ERROR); - _NBL_DEBUG_BREAK_IF(true); - } - return success; -} - -bool DrawResourcesFiller::evictConflictingImagesInCache_SubmitIfNeeded(image_id toInsertImageID, const CachedImageRecord& toInsertRecord, nbl::video::SIntendedSubmitInfo& intendedNextSubmit) -{ - bool evictedSomething = false; - for (auto& [cachedImageID, cachedRecord] : *imagesCache) - { - bool cachedImageConflictsWithImageToReplay = false; - - // Case 1: Same imageID, but params differ (offset/size/arrayIndex mismatch) conflict - if (cachedImageID == toInsertImageID) - { - const bool allocationMatches = - cachedRecord.allocationOffset == toInsertRecord.allocationOffset && - cachedRecord.allocationSize == toInsertRecord.allocationSize; - const bool arrayIndexMatches = cachedRecord.arrayIndex == toInsertRecord.arrayIndex; - const bool exactSameImage = allocationMatches && arrayIndexMatches; - if (!exactSameImage) - cachedImageConflictsWithImageToReplay = true; - } - else - { - // Different Image ID: - // Conflicted if: 1. same array index or 2. conflict in allocation/mem - const bool sameArrayIndex = cachedRecord.arrayIndex == toInsertRecord.arrayIndex; - const bool conflictingMemory = - (cachedRecord.allocationOffset < toInsertRecord.allocationOffset + toInsertRecord.allocationSize) && - (toInsertRecord.allocationOffset < cachedRecord.allocationOffset + cachedRecord.allocationSize); - - if (sameArrayIndex || conflictingMemory) - cachedImageConflictsWithImageToReplay = true; - } - - if (cachedImageConflictsWithImageToReplay) - { - evictImage_SubmitIfNeeded(cachedImageID, cachedRecord, intendedNextSubmit); - imagesCache->erase(cachedImageID); - evictedSomething = true; - } - } - return evictedSomething; -} - -bool DrawResourcesFiller::ensureGeoreferencedImageResources_AllocateIfNeeded(image_id imageID, nbl::core::smart_refctd_ptr&& imageStreamingState, SIntendedSubmitInfo& intendedNextSubmit) -{ - auto* physDev = m_device->getPhysicalDevice(); - - // Check if image already exists and requires resize. We do this before insertion and updating `lastUsedFrameIndex` to get correct overflow-submit behaviour - // otherwise we'd always overflow submit, even if not needed and image was not queued/intended to use in the next submit. - CachedImageRecord* cachedImageRecord = imagesCache->get(imageID); - - // if cachedImageRecord->index was not InvalidTextureIndex then it means we had a cache hit and updated the value of our sema - // But we need to check if the cached image needs resizing/recreation. - if (cachedImageRecord && cachedImageRecord->arrayIndex != InvalidTextureIndex) - { - // found in cache, but does it require resize? recreation? - if (cachedImageRecord->gpuImageView) - { - auto imgViewParams = cachedImageRecord->gpuImageView->getCreationParameters(); - if (imgViewParams.image) - { - const auto cachedParams = static_cast(imgViewParams.image->getCreationParameters()); - // image type and creation params (most importantly extent and format) should match, otherwise we evict, recreate and re-pus - const auto toCreateParams = static_cast(imageStreamingState->gpuImageCreationParams); - const bool needsRecreation = cachedParams != toCreateParams; - if (needsRecreation) - { - // call the eviction callback so the currently cached imageID gets eventually deallocated from memory arena. - // note: it doesn't remove the entry from lru cache. - evictImage_SubmitIfNeeded(imageID, *cachedImageRecord, intendedNextSubmit); - - // instead of erasing and inserting the imageID into the cache, we just reset it, so the next block of code goes into array index allocation + creating our new image - CachedImageRecord newRecord = CachedImageRecord(currentFrameIndex); //reset everything except image streaming state - *cachedImageRecord = std::move(newRecord); - } - } - else - { - m_logger.log("Cached georeferenced image has invalid gpu image.", nbl::system::ILogger::ELL_ERROR); - } - } - else - { - m_logger.log("Cached georeferenced image has invalid gpu image view.", nbl::system::ILogger::ELL_ERROR); - } - } - - - // Try inserting or updating the image usage in the cache. - // If the image is already present, updates its semaphore value. - auto evictCallback = [&](image_id imageID, const CachedImageRecord& evicted) { evictImage_SubmitIfNeeded(imageID, evicted, intendedNextSubmit); }; - cachedImageRecord = imagesCache->insert(imageID, currentFrameIndex, evictCallback); - cachedImageRecord->lastUsedFrameIndex = currentFrameIndex; // in case there was an eviction + auto-submit, we need to update AGAIN - - // Setting the image streaming state returned in `ensureGeoreferencedImageEntry` which was either creating anew or gotten from this very own cache - cachedImageRecord->georeferencedImageState = std::move(imageStreamingState); - cachedImageRecord->georeferencedImageState->outOfDate = false; - - if (cachedImageRecord == nullptr) - { - m_logger.log("Couldn't insert image in cache; make sure you called `ensureGeoreferencedImageEntry` before anything else.", nbl::system::ILogger::ELL_ERROR); - return false; - } - - // in which case we don't queue anything for upload, and return the idx - if (cachedImageRecord->arrayIndex == InvalidTextureIndex) - { - // This is a new image (cache miss). Allocate a descriptor index for it. - cachedImageRecord->arrayIndex = video::SubAllocatedDescriptorSet::AddressAllocator::invalid_address; - // Blocking allocation attempt; if the descriptor pool is exhausted, this may stall. - imagesDescriptorIndexAllocator->multi_allocate(std::chrono::time_point::max(), imagesArrayBinding, 1u, &cachedImageRecord->arrayIndex); // if the prev submit causes DEVICE_LOST then we'll get a deadlock here since we're using max timepoint - cachedImageRecord->arrayIndexAllocatedUsingImageDescriptorIndexAllocator = true; - - if (cachedImageRecord->arrayIndex != video::SubAllocatedDescriptorSet::AddressAllocator::invalid_address) - { - const auto& imageCreationParams = cachedImageRecord->georeferencedImageState->gpuImageCreationParams; - - std::string debugName = cachedImageRecord->georeferencedImageState->storagePath.string(); - - // Attempt to create a GPU image and image view for this texture. - ImageAllocateResults allocResults = tryCreateAndAllocateImage_SubmitIfNeeded(imageCreationParams, asset::E_FORMAT::EF_COUNT, intendedNextSubmit, debugName); - if (allocResults.isValid()) - { - cachedImageRecord->type = ImageType::GEOREFERENCED_STREAMED; - cachedImageRecord->state = ImageState::CREATED_AND_MEMORY_BOUND; - cachedImageRecord->currentLayout = nbl::asset::IImage::LAYOUT::UNDEFINED; - cachedImageRecord->lastUsedFrameIndex = currentFrameIndex; // there was an eviction + auto-submit, we need to update AGAIN - cachedImageRecord->allocationOffset = allocResults.allocationOffset; - cachedImageRecord->allocationSize = allocResults.allocationSize; - cachedImageRecord->gpuImageView = allocResults.gpuImageView; - cachedImageRecord->staticCPUImage = nullptr; - evictConflictingImagesInCache_SubmitIfNeeded(imageID, *cachedImageRecord, intendedNextSubmit); - } - else - { - // All attempts to try create the GPU image and its corresponding view have failed. - // Most likely cause: insufficient GPU memory or unsupported image parameters. - - m_logger.log("ensureGeoreferencedImageAvailability_AllocateIfNeeded failed, likely due to low VRAM.", nbl::system::ILogger::ELL_ERROR); - _NBL_DEBUG_BREAK_IF(true); - - if (cachedImageRecord->allocationOffset != ImagesMemorySubAllocator::InvalidAddress) - { - // We previously successfully create and allocated memory for the Image - // but failed to bind and create image view - // It's crucial to deallocate the offset+size form our images memory suballocator - imagesMemorySubAllocator->deallocate(cachedImageRecord->allocationOffset, cachedImageRecord->allocationSize); - } - - if (cachedImageRecord->arrayIndex != InvalidTextureIndex) - { - // We previously allocated a descriptor index, but failed to create a usable GPU image. - // It's crucial to deallocate this index to avoid leaks and preserve descriptor pool space. - // No semaphore wait needed here, as the GPU never got to use this slot. - imagesDescriptorIndexAllocator->multi_deallocate(imagesArrayBinding, 1u, &cachedImageRecord->arrayIndex, {}); - cachedImageRecord->arrayIndex = InvalidTextureIndex; - } - - // erase the entry we failed to fill, no need for `evictImage_SubmitIfNeeded`, because it didn't get to be used in any submit to defer it's memory and index deallocation - imagesCache->erase(imageID); - } - } - else - { - m_logger.log("ensureGeoreferencedImageAvailability_AllocateIfNeeded failed index allocation. shouldn't have happened.", nbl::system::ILogger::ELL_ERROR); - cachedImageRecord->arrayIndex = InvalidTextureIndex; - } - } - - // cached or just inserted, we update the lastUsedFrameIndex - cachedImageRecord->lastUsedFrameIndex = currentFrameIndex; - - assert(cachedImageRecord->arrayIndex != InvalidTextureIndex); // shouldn't happen, because we're using LRU cache, so worst case eviction will happen + multi-deallocate and next next multi_allocate should definitely succeed - return (cachedImageRecord->arrayIndex != InvalidTextureIndex); -} - -const size_t DrawResourcesFiller::calculateRemainingResourcesSize() const -{ - assert(resourcesGPUBuffer->getSize() >= resourcesCollection.calculateTotalConsumption()); - return resourcesGPUBuffer->getSize() - resourcesCollection.calculateTotalConsumption(); -} - -void DrawResourcesFiller::submitCurrentDrawObjectsAndReset(SIntendedSubmitInfo& intendedNextSubmit, uint32_t& mainObjectIndex) -{ - submitDraws(intendedNextSubmit); - reset(); // resets everything, things referenced through mainObj and other shit will be pushed again through acquireXXX_SubmitIfNeeded - mainObjectIndex = acquireActiveMainObjectIndex_SubmitIfNeeded(intendedNextSubmit); // it will be 0 because it's first mainObjectIndex after reset and invalidation -} - -uint32_t DrawResourcesFiller::addLineStyle_Internal(const LineStyleInfo& lineStyleInfo) -{ - const size_t remainingResourcesSize = calculateRemainingResourcesSize(); - const bool enoughMem = remainingResourcesSize >= sizeof(LineStyle); // enough remaining memory for 1 more linestyle? - if (!enoughMem) - return InvalidStyleIdx; - // TODO: Maybe constraint by a max size? and return InvalidIdx if it would exceed - - LineStyle gpuLineStyle = lineStyleInfo.getAsGPUData(); - _NBL_DEBUG_BREAK_IF(gpuLineStyle.stipplePatternSize > LineStyle::StipplePatternMaxSize); // Oops, even after style normalization the style is too long to be in gpu mem :( - for (uint32_t i = 0u; i < resourcesCollection.lineStyles.vector.size(); ++i) - { - const LineStyle& itr = resourcesCollection.lineStyles.vector[i]; - if (itr == gpuLineStyle) - return i; - } - - return resourcesCollection.lineStyles.addAndGetOffset(gpuLineStyle); // this will implicitly increase total resource consumption and reduce remaining size --> no need for mem size trackers -} - -uint32_t DrawResourcesFiller::addDTMSettings_Internal(const DTMSettingsInfo& dtmSettingsInfo, SIntendedSubmitInfo& intendedNextSubmit) -{ - const size_t remainingResourcesSize = calculateRemainingResourcesSize(); - const size_t noOfLineStylesRequired = ((dtmSettingsInfo.mode & E_DTM_MODE::OUTLINE) ? 1u : 0u) + dtmSettingsInfo.contourSettingsCount; - const size_t maxMemRequired = sizeof(DTMSettings) + noOfLineStylesRequired * sizeof(LineStyle); - const bool enoughMem = remainingResourcesSize >= maxMemRequired; // enough remaining memory for 1 more dtm settings with 2 referenced line styles? - - if (!enoughMem) - return InvalidDTMSettingsIdx; - // TODO: Maybe constraint by a max size? and return InvalidIdx if it would exceed - - DTMSettings dtmSettings; - - ////dtmSettingsInfo.mode = E_DTM_MODE::HEIGHT_SHADING | E_DTM_MODE::CONTOUR | E_DTM_MODE::OUTLINE; - - dtmSettings.mode = dtmSettingsInfo.mode; - if (dtmSettings.mode & E_DTM_MODE::HEIGHT_SHADING) - { - switch (dtmSettingsInfo.heightShadingInfo.heightShadingMode) - { - case E_HEIGHT_SHADING_MODE::DISCRETE_VARIABLE_LENGTH_INTERVALS: - dtmSettings.heightShadingSettings.intervalLength = std::numeric_limits::infinity(); - break; - case E_HEIGHT_SHADING_MODE::DISCRETE_FIXED_LENGTH_INTERVALS: - dtmSettings.heightShadingSettings.intervalLength = dtmSettingsInfo.heightShadingInfo.intervalLength; - break; - case E_HEIGHT_SHADING_MODE::CONTINOUS_INTERVALS: - dtmSettings.heightShadingSettings.intervalLength = 0.0f; - break; - } - dtmSettings.heightShadingSettings.intervalIndexToHeightMultiplier = dtmSettingsInfo.heightShadingInfo.intervalIndexToHeightMultiplier; - dtmSettings.heightShadingSettings.isCenteredShading = static_cast(dtmSettingsInfo.heightShadingInfo.isCenteredShading); - dtmSettingsInfo.heightShadingInfo.fillShaderDTMSettingsHeightColorMap(dtmSettings); - } - if (dtmSettings.mode & E_DTM_MODE::CONTOUR) - { - dtmSettings.contourSettingsCount = dtmSettingsInfo.contourSettingsCount; - for (uint32_t i = 0u; i < dtmSettings.contourSettingsCount; ++i) - { - dtmSettings.contourSettings[i].contourLinesStartHeight = dtmSettingsInfo.contourSettings[i].startHeight; - dtmSettings.contourSettings[i].contourLinesEndHeight = dtmSettingsInfo.contourSettings[i].endHeight; - dtmSettings.contourSettings[i].contourLinesHeightInterval = dtmSettingsInfo.contourSettings[i].heightInterval; - dtmSettings.contourSettings[i].contourLineStyleIdx = addLineStyle_Internal(dtmSettingsInfo.contourSettings[i].lineStyleInfo); - } - } - if (dtmSettings.mode & E_DTM_MODE::OUTLINE) - { - dtmSettings.outlineLineStyleIdx = addLineStyle_Internal(dtmSettingsInfo.outlineStyleInfo); - } - - for (uint32_t i = 0u; i < resourcesCollection.dtmSettings.vector.size(); ++i) - { - const DTMSettings& itr = resourcesCollection.dtmSettings.vector[i]; - if (itr == dtmSettings) - return i; - } - - return resourcesCollection.dtmSettings.addAndGetOffset(dtmSettings); // this will implicitly increase total resource consumption and reduce remaining size --> no need for mem size trackers -} - -float64_t3x3 DrawResourcesFiller::getFixedGeometryFinalTransformationMatrix(const float64_t3x3& transformation, TransformationType transformationType) const -{ - if (!activeProjections.empty()) - { - float64_t3x3 newTransformation = nbl::hlsl::mul(activeProjections.back(), transformation); - - if (transformationType == TransformationType::TT_NORMAL) - { - return newTransformation; - } - else if (transformationType == TransformationType::TT_FIXED_SCREENSPACE_SIZE) - { - // Extract normalized rotation columns - float64_t2 column0 = nbl::hlsl::normalize(float64_t2(newTransformation[0][0], newTransformation[1][0])); - float64_t2 column1 = nbl::hlsl::normalize(float64_t2(newTransformation[0][1], newTransformation[1][1])); - - // Extract fixed screen-space scale from the original transformation - float64_t2 fixedScale = float64_t2( - nbl::hlsl::length(float64_t2(transformation[0][0], transformation[1][0])), - nbl::hlsl::length(float64_t2(transformation[0][1], transformation[1][1]))); - - // Apply fixed scale to normalized directions - column0 *= fixedScale.x; - column1 *= fixedScale.y; - - // Compose final matrix with adjusted columns - newTransformation[0][0] = column0[0]; - newTransformation[1][0] = column0[1]; - newTransformation[0][1] = column1[0]; - newTransformation[1][1] = column1[1]; - - return newTransformation; - } - else - { - // Fallback if transformationType is unrecognized, shouldn't happen - return newTransformation; - } - } - else - { - // Within no active projection scope, return transformation directly - return transformation; - } -} - -uint32_t DrawResourcesFiller::acquireActiveLineStyleIndex_SubmitIfNeeded(SIntendedSubmitInfo& intendedNextSubmit) -{ - if (activeLineStyleIndex == InvalidStyleIdx) - activeLineStyleIndex = addLineStyle_SubmitIfNeeded(activeLineStyle, intendedNextSubmit); - - return activeLineStyleIndex; -} - -uint32_t DrawResourcesFiller::acquireActiveDTMSettingsIndex_SubmitIfNeeded(SIntendedSubmitInfo& intendedNextSubmit) -{ - if (activeDTMSettingsIndex == InvalidDTMSettingsIdx) - activeDTMSettingsIndex = addDTMSettings_SubmitIfNeeded(activeDTMSettings, intendedNextSubmit); - - return activeDTMSettingsIndex; -} - -uint32_t DrawResourcesFiller::acquireActiveCustomProjectionIndex_SubmitIfNeeded(SIntendedSubmitInfo& intendedNextSubmit) -{ - if (activeProjectionIndices.empty()) - return InvalidCustomProjectionIndex; - - if (activeProjectionIndices.back() == InvalidCustomProjectionIndex) - activeProjectionIndices.back() = addCustomProjection_SubmitIfNeeded(activeProjections.back(), intendedNextSubmit); - - return activeProjectionIndices.back(); -} - -uint32_t DrawResourcesFiller::acquireActiveCustomClipRectIndex_SubmitIfNeeded(SIntendedSubmitInfo& intendedNextSubmit) -{ - if (activeClipRectIndices.empty()) - return InvalidCustomClipRectIndex; - - if (activeClipRectIndices.back() == InvalidCustomClipRectIndex) - activeClipRectIndices.back() = addCustomClipRect_SubmitIfNeeded(activeClipRects.back(), intendedNextSubmit); - - return activeClipRectIndices.back(); -} - -uint32_t DrawResourcesFiller::acquireActiveMainObjectIndex_SubmitIfNeeded(SIntendedSubmitInfo& intendedNextSubmit) -{ - if (activeMainObjectIndex != InvalidMainObjectIdx) - return activeMainObjectIndex; - if (activeMainObjectType == MainObjectType::NONE) - { - assert(false); // You're probably trying to acquire mainObjectIndex outside of startMainObject, endMainObject scope - return InvalidMainObjectIdx; - } - - const bool needsLineStyle = - (activeMainObjectType == MainObjectType::POLYLINE) || - (activeMainObjectType == MainObjectType::HATCH) || - (activeMainObjectType == MainObjectType::TEXT); - const bool needsDTMSettings = (activeMainObjectType == MainObjectType::DTM || activeMainObjectType == MainObjectType::GRID_DTM); - const bool needsCustomProjection = (!activeProjectionIndices.empty()); - const bool needsCustomClipRect = (!activeClipRectIndices.empty()); - - const size_t remainingResourcesSize = calculateRemainingResourcesSize(); - // making sure MainObject and everything it references fits into remaining resources mem - size_t memRequired = sizeof(MainObject); - if (needsLineStyle) memRequired += sizeof(LineStyle); - if (needsDTMSettings) memRequired += sizeof(DTMSettings); - if (needsCustomProjection) memRequired += sizeof(float64_t3x3); - if (needsCustomClipRect) memRequired += sizeof(WorldClipRect); - - const bool enoughMem = remainingResourcesSize >= memRequired; // enough remaining memory for 1 more dtm settings with 2 referenced line styles? - const bool needToOverflowSubmit = (!enoughMem) || (resourcesCollection.mainObjects.vector.size() >= MaxIndexableMainObjects); - - if (needToOverflowSubmit) - { - // failed to fit into remaining resources mem or exceeded max indexable mainobj - submitDraws(intendedNextSubmit); - reset(); // resets everything! be careful! - } - - MainObject mainObject = {}; - // These 3 calls below shouldn't need to Submit because we made sure there is enough memory for all of them. - // if something here triggers a auto-submit it's a possible bug with calculating `memRequired` above, TODO: assert that somehow? - mainObject.styleIdx = (needsLineStyle) ? acquireActiveLineStyleIndex_SubmitIfNeeded(intendedNextSubmit) : InvalidStyleIdx; - mainObject.dtmSettingsIdx = (needsDTMSettings) ? acquireActiveDTMSettingsIndex_SubmitIfNeeded(intendedNextSubmit) : InvalidDTMSettingsIdx; - mainObject.customProjectionIndex = (needsCustomProjection) ? acquireActiveCustomProjectionIndex_SubmitIfNeeded(intendedNextSubmit) : InvalidCustomProjectionIndex; - mainObject.customClipRectIndex = (needsCustomClipRect) ? acquireActiveCustomClipRectIndex_SubmitIfNeeded(intendedNextSubmit) : InvalidCustomClipRectIndex; - mainObject.transformationType = (uint32_t)activeMainObjectTransformationType; - activeMainObjectIndex = resourcesCollection.mainObjects.addAndGetOffset(mainObject); - return activeMainObjectIndex; -} - -uint32_t DrawResourcesFiller::addLineStyle_SubmitIfNeeded(const LineStyleInfo& lineStyle, SIntendedSubmitInfo& intendedNextSubmit) -{ - uint32_t outLineStyleIdx = addLineStyle_Internal(lineStyle); - if (outLineStyleIdx == InvalidStyleIdx) - { - // There wasn't enough resource memory remaining to fit a single LineStyle - submitDraws(intendedNextSubmit); - reset(); // resets everything! be careful! - - outLineStyleIdx = addLineStyle_Internal(lineStyle); - assert(outLineStyleIdx != InvalidStyleIdx); - } - - return outLineStyleIdx; -} - -uint32_t DrawResourcesFiller::addDTMSettings_SubmitIfNeeded(const DTMSettingsInfo& dtmSettings, SIntendedSubmitInfo& intendedNextSubmit) -{ - // before calling `addDTMSettings_Internal` we have made sute we have enough mem for - uint32_t outDTMSettingIdx = addDTMSettings_Internal(dtmSettings, intendedNextSubmit); - if (outDTMSettingIdx == InvalidDTMSettingsIdx) - { - // There wasn't enough resource memory remaining to fit dtmsettings struct + 2 linestyles structs. - submitDraws(intendedNextSubmit); - reset(); // resets everything! be careful! - - outDTMSettingIdx = addDTMSettings_Internal(dtmSettings, intendedNextSubmit); - assert(outDTMSettingIdx != InvalidDTMSettingsIdx); - } - return outDTMSettingIdx; + return outClipProjectionAddress; } -uint32_t DrawResourcesFiller::addCustomProjection_SubmitIfNeeded(const float64_t3x3& projection, SIntendedSubmitInfo& intendedNextSubmit) +uint64_t DrawResourcesFiller::addClipProjectionData_Internal(const ClipProjectionData& clipProjectionData) { - const size_t remainingResourcesSize = calculateRemainingResourcesSize(); - const size_t memRequired = sizeof(float64_t3x3); - const bool enoughMem = remainingResourcesSize >= memRequired; // enough remaining memory for 1 more dtm settings with 2 referenced line styles? - - if (!enoughMem) - { - submitDraws(intendedNextSubmit); - reset(); // resets everything! be careful! - } + const uint64_t maxGeometryBufferClipProjData = (maxGeometryBufferSize - currentGeometryBufferSize) / sizeof(ClipProjectionData); + if (maxGeometryBufferClipProjData <= 0) + return InvalidClipProjectionAddress; - resourcesCollection.customProjections.vector.push_back(projection); // this will implicitly increase total resource consumption and reduce remaining size --> no need for mem size trackers - return resourcesCollection.customProjections.vector.size() - 1u; -} - -uint32_t DrawResourcesFiller::addCustomClipRect_SubmitIfNeeded(const WorldClipRect& clipRect, SIntendedSubmitInfo& intendedNextSubmit) -{ - const size_t remainingResourcesSize = calculateRemainingResourcesSize(); - const size_t memRequired = sizeof(WorldClipRect); - const bool enoughMem = remainingResourcesSize >= memRequired; // enough remaining memory for 1 more dtm settings with 2 referenced line styles? + void* dst = reinterpret_cast(cpuDrawBuffers.geometryBuffer->getPointer()) + currentGeometryBufferSize; + memcpy(dst, &clipProjectionData, sizeof(ClipProjectionData)); - if (!enoughMem) - { - submitDraws(intendedNextSubmit); - reset(); // resets everything! be careful! - } - - resourcesCollection.customClipRects.vector.push_back(clipRect); // this will implicitly increase total resource consumption and reduce remaining size --> no need for mem size trackers - return resourcesCollection.customClipRects.vector.size() - 1u; + const uint64_t ret = currentGeometryBufferSize + geometryBufferAddress; + currentGeometryBufferSize += sizeof(ClipProjectionData); + return ret; } void DrawResourcesFiller::addPolylineObjects_Internal(const CPolylineBase& polyline, const CPolylineBase::SectionInfo& section, uint32_t& currentObjectInSection, uint32_t mainObjIdx) @@ -2360,49 +626,39 @@ void DrawResourcesFiller::addPolylineObjects_Internal(const CPolylineBase& polyl void DrawResourcesFiller::addPolylineConnectors_Internal(const CPolylineBase& polyline, uint32_t& currentPolylineConnectorObj, uint32_t mainObjIdx) { - const size_t remainingResourcesSize = calculateRemainingResourcesSize(); + const auto maxGeometryBufferConnectors = (maxGeometryBufferSize - currentGeometryBufferSize) / sizeof(PolylineConnector); - const uint32_t uploadableObjects = (remainingResourcesSize) / (sizeof(PolylineConnector) + sizeof(DrawObject) + sizeof(uint32_t) * 6u); - // TODO[ERFAN]: later take into account: our maximum indexable vertex - - const uint32_t connectorCount = static_cast(polyline.getConnectors().size()); - const uint32_t remainingObjects = connectorCount - currentPolylineConnectorObj; - const uint32_t objectsToUpload = core::min(uploadableObjects, remainingObjects); + uint32_t uploadableObjects = (maxIndexCount / 6u) - currentDrawObjectCount; + uploadableObjects = min(uploadableObjects, maxGeometryBufferConnectors); + uploadableObjects = min(uploadableObjects, maxDrawObjects - currentDrawObjectCount); - if (objectsToUpload <= 0u) - return; + const auto connectorCount = polyline.getConnectors().size(); + const auto remainingObjects = connectorCount - currentPolylineConnectorObj; - // Add Geometry - const auto connectorsByteSize = sizeof(PolylineConnector) * objectsToUpload; - size_t geometryBufferOffset = resourcesCollection.geometryInfo.increaseSizeAndGetOffset(connectorsByteSize, alignof(PolylineConnector)); - void* dst = resourcesCollection.geometryInfo.data() + geometryBufferOffset; - const PolylineConnector& connector = polyline.getConnectors()[currentPolylineConnectorObj]; - memcpy(dst, &connector, connectorsByteSize); - - // Push Indices, remove later when compute fills this - uint32_t* indexBufferToBeFilled = resourcesCollection.indexBuffer.increaseCountAndGetPtr(6u * objectsToUpload); - const uint32_t startObj = resourcesCollection.drawObjects.getCount(); - for (uint32_t i = 0u; i < objectsToUpload; ++i) - { - indexBufferToBeFilled[i*6] = (startObj+i)*4u + 1u; - indexBufferToBeFilled[i*6 + 1u] = (startObj+i)*4u + 0u; - indexBufferToBeFilled[i*6 + 2u] = (startObj+i)*4u + 2u; - indexBufferToBeFilled[i*6 + 3u] = (startObj+i)*4u + 1u; - indexBufferToBeFilled[i*6 + 4u] = (startObj+i)*4u + 2u; - indexBufferToBeFilled[i*6 + 5u] = (startObj+i)*4u + 3u; - } + const uint32_t objectsToUpload = min(uploadableObjects, remainingObjects); // Add DrawObjs - DrawObject* drawObjectsToBeFilled = resourcesCollection.drawObjects.increaseCountAndGetPtr(objectsToUpload); DrawObject drawObj = {}; drawObj.mainObjIndex = mainObjIdx; drawObj.type_subsectionIdx = uint32_t(static_cast(ObjectType::POLYLINE_CONNECTOR) | 0 << 16); - drawObj.geometryAddress = geometryBufferOffset; + drawObj.geometryAddress = geometryBufferAddress + currentGeometryBufferSize; for (uint32_t i = 0u; i < objectsToUpload; ++i) { - drawObjectsToBeFilled[i] = drawObj; + void* dst = reinterpret_cast(cpuDrawBuffers.drawObjectsBuffer->getPointer()) + currentDrawObjectCount; + memcpy(dst, &drawObj, sizeof(DrawObject)); + currentDrawObjectCount += 1u; drawObj.geometryAddress += sizeof(PolylineConnector); - } + } + + // Add Geometry + if (objectsToUpload > 0u) + { + const auto connectorsByteSize = sizeof(PolylineConnector) * objectsToUpload; + void* dst = reinterpret_cast(cpuDrawBuffers.geometryBuffer->getPointer()) + currentGeometryBufferSize; + auto& connector = polyline.getConnectors()[currentPolylineConnectorObj]; + memcpy(dst, &connector, connectorsByteSize); + currentGeometryBufferSize += connectorsByteSize; + } currentPolylineConnectorObj += objectsToUpload; } @@ -2412,547 +668,169 @@ void DrawResourcesFiller::addLines_Internal(const CPolylineBase& polyline, const assert(section.count >= 1u); assert(section.type == ObjectType::LINE); + const auto maxGeometryBufferPoints = (maxGeometryBufferSize - currentGeometryBufferSize) / sizeof(LinePointInfo); + const auto maxGeometryBufferLines = (maxGeometryBufferPoints <= 1u) ? 0u : maxGeometryBufferPoints - 1u; - const size_t remainingResourcesSize = calculateRemainingResourcesSize(); - if (remainingResourcesSize < sizeof(LinePointInfo)) - return; - - // how many lines fit into mem? --> memConsumption = sizeof(LinePointInfo) + sizeof(LinePointInfo)*lineCount + sizeof(DrawObject)*lineCount + sizeof(uint32_t) * 6u * lineCount - const uint32_t uploadableObjects = (remainingResourcesSize - sizeof(LinePointInfo)) / (sizeof(LinePointInfo) + sizeof(DrawObject) + sizeof(uint32_t) * 6u); - // TODO[ERFAN]: later take into account: our maximum indexable vertex - - const uint32_t lineCount = section.count; - const uint32_t remainingObjects = lineCount - currentObjectInSection; - const uint32_t objectsToUpload = core::min(uploadableObjects, remainingObjects); + uint32_t uploadableObjects = (maxIndexCount / 6u) - currentDrawObjectCount; + uploadableObjects = min(uploadableObjects, maxGeometryBufferLines); + uploadableObjects = min(uploadableObjects, maxDrawObjects - currentDrawObjectCount); - if (objectsToUpload <= 0u) - return; - - // Add Geometry - const auto pointsByteSize = sizeof(LinePointInfo) * (objectsToUpload + 1u); - size_t geometryBufferOffset = resourcesCollection.geometryInfo.increaseSizeAndGetOffset(pointsByteSize, alignof(LinePointInfo)); - void* dst = resourcesCollection.geometryInfo.data() + geometryBufferOffset; - const LinePointInfo& linePoint = polyline.getLinePointAt(section.index + currentObjectInSection); - memcpy(dst, &linePoint, pointsByteSize); - - // Push Indices, remove later when compute fills this - uint32_t* indexBufferToBeFilled = resourcesCollection.indexBuffer.increaseCountAndGetPtr(6u * objectsToUpload); - const uint32_t startObj = resourcesCollection.drawObjects.getCount(); - for (uint32_t i = 0u; i < objectsToUpload; ++i) - { - indexBufferToBeFilled[i*6] = (startObj+i)*4u + 1u; - indexBufferToBeFilled[i*6 + 1u] = (startObj+i)*4u + 0u; - indexBufferToBeFilled[i*6 + 2u] = (startObj+i)*4u + 2u; - indexBufferToBeFilled[i*6 + 3u] = (startObj+i)*4u + 1u; - indexBufferToBeFilled[i*6 + 4u] = (startObj+i)*4u + 2u; - indexBufferToBeFilled[i*6 + 5u] = (startObj+i)*4u + 3u; - } + const auto lineCount = section.count; + const auto remainingObjects = lineCount - currentObjectInSection; + uint32_t objectsToUpload = min(uploadableObjects, remainingObjects); // Add DrawObjs - DrawObject* drawObjectsToBeFilled = resourcesCollection.drawObjects.increaseCountAndGetPtr(objectsToUpload); DrawObject drawObj = {}; drawObj.mainObjIndex = mainObjIdx; drawObj.type_subsectionIdx = uint32_t(static_cast(ObjectType::LINE) | 0 << 16); - drawObj.geometryAddress = geometryBufferOffset; + drawObj.geometryAddress = geometryBufferAddress + currentGeometryBufferSize; for (uint32_t i = 0u; i < objectsToUpload; ++i) { - drawObjectsToBeFilled[i] = drawObj; + void* dst = reinterpret_cast(cpuDrawBuffers.drawObjectsBuffer->getPointer()) + currentDrawObjectCount; + memcpy(dst, &drawObj, sizeof(DrawObject)); + currentDrawObjectCount += 1u; drawObj.geometryAddress += sizeof(LinePointInfo); - } + } + + // Add Geometry + if (objectsToUpload > 0u) + { + const auto pointsByteSize = sizeof(LinePointInfo) * (objectsToUpload + 1u); + void* dst = reinterpret_cast(cpuDrawBuffers.geometryBuffer->getPointer()) + currentGeometryBufferSize; + auto& linePoint = polyline.getLinePointAt(section.index + currentObjectInSection); + memcpy(dst, &linePoint, pointsByteSize); + currentGeometryBufferSize += pointsByteSize; + } currentObjectInSection += objectsToUpload; } void DrawResourcesFiller::addQuadBeziers_Internal(const CPolylineBase& polyline, const CPolylineBase::SectionInfo& section, uint32_t& currentObjectInSection, uint32_t mainObjIdx) { - constexpr uint32_t CagesPerQuadBezier = 3u; // TODO: Break into 3 beziers in compute shader. - + constexpr uint32_t CagesPerQuadBezier = getCageCountPerPolylineObject(ObjectType::QUAD_BEZIER); assert(section.type == ObjectType::QUAD_BEZIER); - const size_t remainingResourcesSize = calculateRemainingResourcesSize(); - // how many quad bezier objects fit into mem? - // memConsumption = quadBezCount * (sizeof(QuadraticBezierInfo) + 3*(sizeof(DrawObject)+6u*sizeof(uint32_t)) - const uint32_t uploadableObjects = (remainingResourcesSize) / (sizeof(QuadraticBezierInfo) + (sizeof(DrawObject) + 6u * sizeof(uint32_t)) * CagesPerQuadBezier); - // TODO[ERFAN]: later take into account: our maximum indexable vertex + const auto maxGeometryBufferBeziers = (maxGeometryBufferSize - currentGeometryBufferSize) / sizeof(QuadraticBezierInfo); - const uint32_t beziersCount = section.count; - const uint32_t remainingObjects = beziersCount - currentObjectInSection; - const uint32_t objectsToUpload = core::min(uploadableObjects, remainingObjects); - const uint32_t cagesCount = objectsToUpload * CagesPerQuadBezier; - - if (objectsToUpload <= 0u) - return; - - // Add Geometry - const auto beziersByteSize = sizeof(QuadraticBezierInfo) * (objectsToUpload); - size_t geometryBufferOffset = resourcesCollection.geometryInfo.increaseSizeAndGetOffset(beziersByteSize, alignof(QuadraticBezierInfo)); - void* dst = resourcesCollection.geometryInfo.data() + geometryBufferOffset; - const QuadraticBezierInfo& quadBezier = polyline.getQuadBezierInfoAt(section.index + currentObjectInSection); - memcpy(dst, &quadBezier, beziersByteSize); + uint32_t uploadableObjects = (maxIndexCount / 6u) - currentDrawObjectCount; + uploadableObjects = min(uploadableObjects, maxGeometryBufferBeziers); + uploadableObjects = min(uploadableObjects, maxDrawObjects - currentDrawObjectCount); + uploadableObjects /= CagesPerQuadBezier; + const auto beziersCount = section.count; + const auto remainingObjects = beziersCount - currentObjectInSection; + uint32_t objectsToUpload = min(uploadableObjects, remainingObjects); - - // Push Indices, remove later when compute fills this - uint32_t* indexBufferToBeFilled = resourcesCollection.indexBuffer.increaseCountAndGetPtr(6u*cagesCount); - const uint32_t startObj = resourcesCollection.drawObjects.getCount(); - for (uint32_t i = 0u; i < cagesCount; ++i) - { - indexBufferToBeFilled[i*6] = (startObj+i)*4u + 1u; - indexBufferToBeFilled[i*6 + 1u] = (startObj+i)*4u + 0u; - indexBufferToBeFilled[i*6 + 2u] = (startObj+i)*4u + 2u; - indexBufferToBeFilled[i*6 + 3u] = (startObj+i)*4u + 1u; - indexBufferToBeFilled[i*6 + 4u] = (startObj+i)*4u + 2u; - indexBufferToBeFilled[i*6 + 5u] = (startObj+i)*4u + 3u; - } - // Add DrawObjs - DrawObject* drawObjectsToBeFilled = resourcesCollection.drawObjects.increaseCountAndGetPtr(cagesCount); DrawObject drawObj = {}; drawObj.mainObjIndex = mainObjIdx; - drawObj.geometryAddress = geometryBufferOffset; + drawObj.geometryAddress = geometryBufferAddress + currentGeometryBufferSize; for (uint32_t i = 0u; i < objectsToUpload; ++i) { for (uint16_t subObject = 0; subObject < CagesPerQuadBezier; subObject++) { drawObj.type_subsectionIdx = uint32_t(static_cast(ObjectType::QUAD_BEZIER) | (subObject << 16)); - drawObjectsToBeFilled[i * CagesPerQuadBezier + subObject] = drawObj; + void* dst = reinterpret_cast(cpuDrawBuffers.drawObjectsBuffer->getPointer()) + currentDrawObjectCount; + memcpy(dst, &drawObj, sizeof(DrawObject)); + currentDrawObjectCount += 1u; } drawObj.geometryAddress += sizeof(QuadraticBezierInfo); } + // Add Geometry + if (objectsToUpload > 0u) + { + const auto beziersByteSize = sizeof(QuadraticBezierInfo) * (objectsToUpload); + void* dst = reinterpret_cast(cpuDrawBuffers.geometryBuffer->getPointer()) + currentGeometryBufferSize; + auto& quadBezier = polyline.getQuadBezierInfoAt(section.index + currentObjectInSection); + memcpy(dst, &quadBezier, beziersByteSize); + currentGeometryBufferSize += beziersByteSize; + } currentObjectInSection += objectsToUpload; } void DrawResourcesFiller::addHatch_Internal(const Hatch& hatch, uint32_t& currentObjectInSection, uint32_t mainObjIndex) { - const size_t remainingResourcesSize = calculateRemainingResourcesSize(); - - const uint32_t uploadableObjects = (remainingResourcesSize) / (sizeof(Hatch::CurveHatchBox) + sizeof(DrawObject) + sizeof(uint32_t) * 6u); - // TODO[ERFAN]: later take into account: our maximum indexable vertex + const auto maxGeometryBufferHatchBoxes = (maxGeometryBufferSize - currentGeometryBufferSize) / sizeof(Hatch::CurveHatchBox); - uint32_t remainingObjects = hatch.getHatchBoxCount() - currentObjectInSection; - const uint32_t objectsToUpload = core::min(uploadableObjects, remainingObjects); + uint32_t uploadableObjects = (maxIndexCount / 6u) - currentDrawObjectCount; + uploadableObjects = min(uploadableObjects, maxDrawObjects - currentDrawObjectCount); + uploadableObjects = min(uploadableObjects, maxGeometryBufferHatchBoxes); - if (objectsToUpload <= 0u) - return; + uint32_t remainingObjects = hatch.getHatchBoxCount() - currentObjectInSection; + uploadableObjects = min(uploadableObjects, remainingObjects); - // Add Geometry - static_assert(sizeof(CurveBox) == sizeof(Hatch::CurveHatchBox)); - const auto curveBoxesByteSize = sizeof(Hatch::CurveHatchBox) * objectsToUpload; - size_t geometryBufferOffset = resourcesCollection.geometryInfo.increaseSizeAndGetOffset(curveBoxesByteSize, alignof(Hatch::CurveHatchBox)); - void* dst = resourcesCollection.geometryInfo.data() + geometryBufferOffset; - const Hatch::CurveHatchBox& hatchBox = hatch.getHatchBox(currentObjectInSection); // WARNING: This is assuming hatch boxes are contigous in memory, TODO: maybe make that more obvious through Hatch interface - memcpy(dst, &hatchBox, curveBoxesByteSize); - - // Push Indices, remove later when compute fills this - uint32_t* indexBufferToBeFilled = resourcesCollection.indexBuffer.increaseCountAndGetPtr(6u * objectsToUpload); - const uint32_t startObj = resourcesCollection.drawObjects.getCount(); - for (uint32_t i = 0u; i < objectsToUpload; ++i) - { - indexBufferToBeFilled[i*6] = (startObj+i)*4u + 1u; - indexBufferToBeFilled[i*6 + 1u] = (startObj+i)*4u + 0u; - indexBufferToBeFilled[i*6 + 2u] = (startObj+i)*4u + 2u; - indexBufferToBeFilled[i*6 + 3u] = (startObj+i)*4u + 1u; - indexBufferToBeFilled[i*6 + 4u] = (startObj+i)*4u + 2u; - indexBufferToBeFilled[i*6 + 5u] = (startObj+i)*4u + 3u; - } - - // Add DrawObjs - DrawObject* drawObjectsToBeFilled = resourcesCollection.drawObjects.increaseCountAndGetPtr(objectsToUpload); - DrawObject drawObj = {}; - drawObj.mainObjIndex = mainObjIndex; - drawObj.type_subsectionIdx = uint32_t(static_cast(ObjectType::CURVE_BOX) | (0 << 16)); - drawObj.geometryAddress = geometryBufferOffset; - for (uint32_t i = 0u; i < objectsToUpload; ++i) + for (uint32_t i = 0; i < uploadableObjects; i++) { - drawObjectsToBeFilled[i] = drawObj; - drawObj.geometryAddress += sizeof(Hatch::CurveHatchBox); + const Hatch::CurveHatchBox& hatchBox = hatch.getHatchBox(i + currentObjectInSection); + + uint64_t hatchBoxAddress; + { + static_assert(sizeof(CurveBox) == sizeof(Hatch::CurveHatchBox)); + void* dst = reinterpret_cast(cpuDrawBuffers.geometryBuffer->getPointer()) + currentGeometryBufferSize; + memcpy(dst, &hatchBox, sizeof(CurveBox)); + hatchBoxAddress = geometryBufferAddress + currentGeometryBufferSize; + currentGeometryBufferSize += sizeof(CurveBox); + } + + DrawObject drawObj = {}; + drawObj.type_subsectionIdx = uint32_t(static_cast(ObjectType::CURVE_BOX) | (0 << 16)); + drawObj.mainObjIndex = mainObjIndex; + drawObj.geometryAddress = hatchBoxAddress; + void* dst = reinterpret_cast(cpuDrawBuffers.drawObjectsBuffer->getPointer()) + currentDrawObjectCount + i; + memcpy(dst, &drawObj, sizeof(DrawObject)); } // Add Indices + currentDrawObjectCount += uploadableObjects; currentObjectInSection += uploadableObjects; } bool DrawResourcesFiller::addFontGlyph_Internal(const GlyphInfo& glyphInfo, uint32_t mainObjIdx) { - const size_t remainingResourcesSize = calculateRemainingResourcesSize(); - - const uint32_t uploadableObjects = (remainingResourcesSize) / (sizeof(GlyphInfo) + sizeof(DrawObject) + sizeof(uint32_t) * 6u); - // TODO[ERFAN]: later take into account: our maximum indexable vertex - - if (uploadableObjects <= 0u) - return false; - - // Add Geometry - size_t geometryBufferOffset = resourcesCollection.geometryInfo.increaseSizeAndGetOffset(sizeof(GlyphInfo), alignof(GlyphInfo)); - void* dst = resourcesCollection.geometryInfo.data() + geometryBufferOffset; - memcpy(dst, &glyphInfo, sizeof(GlyphInfo)); - - // Push Indices, remove later when compute fills this - uint32_t* indexBufferToBeFilled = resourcesCollection.indexBuffer.increaseCountAndGetPtr(6u * 1u); - const uint32_t startObj = resourcesCollection.drawObjects.getCount(); - uint32_t i = 0u; - indexBufferToBeFilled[i*6] = (startObj+i)*4u + 1u; - indexBufferToBeFilled[i*6 + 1u] = (startObj+i)*4u + 0u; - indexBufferToBeFilled[i*6 + 2u] = (startObj+i)*4u + 2u; - indexBufferToBeFilled[i*6 + 3u] = (startObj+i)*4u + 1u; - indexBufferToBeFilled[i*6 + 4u] = (startObj+i)*4u + 2u; - indexBufferToBeFilled[i*6 + 5u] = (startObj+i)*4u + 3u; - - // Add DrawObjs - DrawObject* drawObjectsToBeFilled = resourcesCollection.drawObjects.increaseCountAndGetPtr(1u); - DrawObject drawObj = {}; - drawObj.mainObjIndex = mainObjIdx; - drawObj.type_subsectionIdx = uint32_t(static_cast(ObjectType::FONT_GLYPH) | (0 << 16)); - drawObj.geometryAddress = geometryBufferOffset; - drawObjectsToBeFilled[0u] = drawObj; - - return true; -} - -bool DrawResourcesFiller::addGridDTM_Internal(const GridDTMInfo& gridDTMInfo, uint32_t mainObjIdx) -{ - const size_t remainingResourcesSize = calculateRemainingResourcesSize(); - - const uint32_t uploadableObjects = (remainingResourcesSize) / (sizeof(GridDTMInfo) + sizeof(DrawObject) + sizeof(uint32_t) * 6u); - // TODO[ERFAN]: later take into account: our maximum indexable vertex - - if (uploadableObjects <= 0u) - return false; - - // Add Geometry - size_t geometryBufferOffset = resourcesCollection.geometryInfo.increaseSizeAndGetOffset(sizeof(GridDTMInfo), alignof(GridDTMInfo)); - void* dst = resourcesCollection.geometryInfo.data() + geometryBufferOffset; - memcpy(dst, &gridDTMInfo, sizeof(GridDTMInfo)); - - // Push Indices, remove later when compute fills this - uint32_t* indexBufferToBeFilled = resourcesCollection.indexBuffer.increaseCountAndGetPtr(6u); - const uint32_t startObj = resourcesCollection.drawObjects.getCount(); - uint32_t i = 0u; - indexBufferToBeFilled[i * 6] = (startObj + i) * 4u + 1u; - indexBufferToBeFilled[i * 6 + 1u] = (startObj + i) * 4u + 0u; - indexBufferToBeFilled[i * 6 + 2u] = (startObj + i) * 4u + 2u; - indexBufferToBeFilled[i * 6 + 3u] = (startObj + i) * 4u + 1u; - indexBufferToBeFilled[i * 6 + 4u] = (startObj + i) * 4u + 2u; - indexBufferToBeFilled[i * 6 + 5u] = (startObj + i) * 4u + 3u; - - // Add DrawObjs - DrawObject* drawObjectsToBeFilled = resourcesCollection.drawObjects.increaseCountAndGetPtr(1u); - DrawObject drawObj = {}; - drawObj.mainObjIndex = mainObjIdx; - drawObj.type_subsectionIdx = uint32_t(static_cast(ObjectType::GRID_DTM) | (0 << 16)); - drawObj.geometryAddress = geometryBufferOffset; - drawObjectsToBeFilled[0u] = drawObj; - - return true; -} - -bool DrawResourcesFiller::addImageObject_Internal(const ImageObjectInfo& imageObjectInfo, uint32_t mainObjIdx) -{ - const size_t remainingResourcesSize = calculateRemainingResourcesSize(); - - const uint32_t uploadableObjects = (remainingResourcesSize) / (sizeof(ImageObjectInfo) + sizeof(DrawObject) + sizeof(uint32_t) * 6u); - // TODO[ERFAN]: later take into account: our maximum indexable vertex - - if (uploadableObjects <= 0u) - return false; - - // Add Geometry - size_t geometryBufferOffset = resourcesCollection.geometryInfo.increaseSizeAndGetOffset(sizeof(ImageObjectInfo), alignof(ImageObjectInfo)); - void* dst = resourcesCollection.geometryInfo.data() + geometryBufferOffset; - memcpy(dst, &imageObjectInfo, sizeof(ImageObjectInfo)); - - // Push Indices, remove later when compute fills this - uint32_t* indexBufferToBeFilled = resourcesCollection.indexBuffer.increaseCountAndGetPtr(6u * 1u); - const uint32_t startObj = resourcesCollection.drawObjects.getCount(); - uint32_t i = 0u; - indexBufferToBeFilled[i * 6] = (startObj + i) * 4u + 1u; - indexBufferToBeFilled[i * 6 + 1u] = (startObj + i) * 4u + 0u; - indexBufferToBeFilled[i * 6 + 2u] = (startObj + i) * 4u + 2u; - indexBufferToBeFilled[i * 6 + 3u] = (startObj + i) * 4u + 1u; - indexBufferToBeFilled[i * 6 + 4u] = (startObj + i) * 4u + 2u; - indexBufferToBeFilled[i * 6 + 5u] = (startObj + i) * 4u + 3u; - - // Add DrawObjs - DrawObject* drawObjectsToBeFilled = resourcesCollection.drawObjects.increaseCountAndGetPtr(1u); - DrawObject drawObj = {}; - drawObj.mainObjIndex = mainObjIdx; - drawObj.type_subsectionIdx = uint32_t(static_cast(ObjectType::STATIC_IMAGE) | (0 << 16)); // TODO: use custom pack/unpack function - drawObj.geometryAddress = geometryBufferOffset; - drawObjectsToBeFilled[0u] = drawObj; - - return true; -} - -bool DrawResourcesFiller::addGeoreferencedImageInfo_Internal(const GeoreferencedImageInfo& georeferencedImageInfo, uint32_t mainObjIdx) -{ - const size_t remainingResourcesSize = calculateRemainingResourcesSize(); - - const uint32_t uploadableObjects = (remainingResourcesSize) / (sizeof(GeoreferencedImageInfo) + sizeof(DrawObject) + sizeof(uint32_t) * 6u); - // TODO[ERFAN]: later take into account: our maximum indexable vertex - - if (uploadableObjects <= 0u) - return false; - - // Add Geometry - size_t geometryBufferOffset = resourcesCollection.geometryInfo.increaseSizeAndGetOffset(sizeof(GeoreferencedImageInfo), alignof(GeoreferencedImageInfo)); - void* dst = resourcesCollection.geometryInfo.data() + geometryBufferOffset; - memcpy(dst, &georeferencedImageInfo, sizeof(GeoreferencedImageInfo)); - - // Push Indices, remove later when compute fills this - uint32_t* indexBufferToBeFilled = resourcesCollection.indexBuffer.increaseCountAndGetPtr(6u * 1u); - const uint32_t startObj = resourcesCollection.drawObjects.getCount(); - uint32_t i = 0u; - indexBufferToBeFilled[i * 6] = (startObj + i) * 4u + 1u; - indexBufferToBeFilled[i * 6 + 1u] = (startObj + i) * 4u + 0u; - indexBufferToBeFilled[i * 6 + 2u] = (startObj + i) * 4u + 2u; - indexBufferToBeFilled[i * 6 + 3u] = (startObj + i) * 4u + 1u; - indexBufferToBeFilled[i * 6 + 4u] = (startObj + i) * 4u + 2u; - indexBufferToBeFilled[i * 6 + 5u] = (startObj + i) * 4u + 3u; - - // Add DrawObjs - DrawObject* drawObjectsToBeFilled = resourcesCollection.drawObjects.increaseCountAndGetPtr(1u); - DrawObject drawObj = {}; - drawObj.mainObjIndex = mainObjIdx; - drawObj.type_subsectionIdx = uint32_t(static_cast(ObjectType::STREAMED_IMAGE) | (0 << 16)); // TODO: use custom pack/unpack function - drawObj.geometryAddress = geometryBufferOffset; - drawObjectsToBeFilled[0u] = drawObj; - - return true; -} - -uint32_t DrawResourcesFiller::getImageIndexFromID(image_id imageID, const SIntendedSubmitInfo& intendedNextSubmit) -{ - uint32_t textureIdx = InvalidTextureIndex; - CachedImageRecord* imageRef = imagesCache->get(imageID); - if (imageRef) - { - textureIdx = imageRef->arrayIndex; - imageRef->lastUsedFrameIndex = currentFrameIndex; // update this because the texture will get used on the next frane - } - return textureIdx; -} - -void DrawResourcesFiller::evictImage_SubmitIfNeeded(image_id imageID, const CachedImageRecord& evicted, SIntendedSubmitInfo& intendedNextSubmit) -{ - if (evicted.arrayIndex == InvalidTextureIndex) - { - m_logger.log("evictImage_SubmitIfNeeded: `evicted.arrayIndex == InvalidTextureIndex` is true, shouldn't happen under normal circumstances.", nbl::system::ILogger::ELL_WARNING); - _NBL_DEBUG_BREAK_IF(true); - return; - } - -#if 0 - m_logger.log(("Evicting Image: \n" + evicted.toString(imageID)).c_str(), nbl::system::ILogger::ELL_INFO); -#endif + const auto maxGeometryBufferFontGlyphs = (maxGeometryBufferSize - currentGeometryBufferSize) / sizeof(GlyphInfo); - const bool imageUsedForNextIntendedSubmit = (evicted.lastUsedFrameIndex == currentFrameIndex); + uint32_t uploadableObjects = (maxIndexCount / 6u) - currentDrawObjectCount; + uploadableObjects = min(uploadableObjects, maxDrawObjects - currentDrawObjectCount); + uploadableObjects = min(uploadableObjects, maxGeometryBufferFontGlyphs); - if (evicted.arrayIndexAllocatedUsingImageDescriptorIndexAllocator) + if (uploadableObjects >= 1u) { - // Image being evicted was allocated using image descriptor set allocator - // Later used to release the image's memory range. - core::smart_refctd_ptr cleanupObject = core::make_smart_refctd_ptr(); - cleanupObject->imagesMemorySuballocator = imagesMemorySubAllocator; - cleanupObject->addr = evicted.allocationOffset; - cleanupObject->size = evicted.allocationSize; - - if (evicted.type == ImageType::GEOREFERENCED_STREAMED) - { - // Important to mark this as out of date. - // because any other place still holding on to the state (which is possible) need to know the image associated with the state has been evicted and the state is no longer valid and needs to "ensure"d again. - evicted.georeferencedImageState->outOfDate = true; - // cancelGeoreferencedImageTileLoads(imageID); // clear any of the pending loads/futures requested for the image - } + void* geomDst = reinterpret_cast(cpuDrawBuffers.geometryBuffer->getPointer()) + currentGeometryBufferSize; + memcpy(geomDst, &glyphInfo, sizeof(GlyphInfo)); + uint64_t fontGlyphAddr = geometryBufferAddress + currentGeometryBufferSize; + currentGeometryBufferSize += sizeof(GlyphInfo); - // NOTE: `deallocationWaitInfo` is crucial for both paths, we need to make sure we'll write to a descriptor arrayIndex when it's 100% done with previous usages. - if (imageUsedForNextIntendedSubmit) - { - // The evicted image is scheduled for use in the upcoming submit. - // To avoid rendering artifacts, we must flush the current draw queue now. - // After submission, we reset state so that data referencing the evicted slot can be re-uploaded. - submitDraws(intendedNextSubmit); - reset(); // resets everything, things referenced through mainObj and other shit will be pushed again through acquireXXX_SubmitIfNeeded + DrawObject drawObj = {}; + drawObj.type_subsectionIdx = uint32_t(static_cast(ObjectType::FONT_GLYPH) | (0 << 16)); + drawObj.mainObjIndex = mainObjIdx; + drawObj.geometryAddress = fontGlyphAddr; + void* drawObjDst = reinterpret_cast(cpuDrawBuffers.drawObjectsBuffer->getPointer()) + currentDrawObjectCount; + memcpy(drawObjDst, &drawObj, sizeof(DrawObject)); + currentDrawObjectCount += 1u; - // Prepare wait info to defer index deallocation until the GPU has finished using the resource. - // we wait on the signal semaphore for the submit we just did above. - ISemaphore::SWaitInfo deallocationWaitInfo = { .semaphore = intendedNextSubmit.scratchSemaphore.semaphore, .value = intendedNextSubmit.scratchSemaphore.value }; - imagesDescriptorIndexAllocator->multi_deallocate(imagesArrayBinding, 1u, &evicted.arrayIndex, deallocationWaitInfo, &cleanupObject.get()); - } - else - { - // The image is not used in the current frame, so we can deallocate without submitting any draws. - // Still wait on the semaphore to ensure past GPU usage is complete. - // TODO: We don't know which semaphore value the frame with `evicted.lastUsedFrameIndex` index was submitted with, so we wait for the worst case value conservatively, which is the immediate prev submit. - ISemaphore::SWaitInfo deallocationWaitInfo = { .semaphore = intendedNextSubmit.scratchSemaphore.semaphore, .value = intendedNextSubmit.scratchSemaphore.value }; - imagesDescriptorIndexAllocator->multi_deallocate(imagesArrayBinding, 1u, &evicted.arrayIndex, deallocationWaitInfo, &cleanupObject.get()); - } + return true; } else { - // Less often case: index wasn't allocated using imageDescriptorSetAllocator, like replayed images which skip the allocator to write to the set directly. - // we won't cleanup + multi_dealloc in this case, instead we queue the deallocations and wait for them before any next image writes into the same index. - if (!imageUsedForNextIntendedSubmit) - deferredDescriptorIndexDeallocations[evicted.arrayIndex] = ISemaphore::SWaitInfo{ .semaphore = intendedNextSubmit.scratchSemaphore.semaphore, .value = intendedNextSubmit.scratchSemaphore.value }; - else - { - m_logger.log(std::format("Image which is being evicted and had skipped descriptor set allocator requires overflow submit; This shouldn't happen. Image Info = {}", evicted.toString(imageID)).c_str(), nbl::system::ILogger::ELL_ERROR); - imagesCache->logState(m_logger); - } - + return false; } } -DrawResourcesFiller::ImageAllocateResults DrawResourcesFiller::tryCreateAndAllocateImage_SubmitIfNeeded( - const nbl::asset::IImage::SCreationParams& imageParams, - const asset::E_FORMAT imageViewFormatOverride, - nbl::video::SIntendedSubmitInfo& intendedNextSubmit, - std::string imageDebugName) +DrawResourcesFiller::msdf_hash DrawResourcesFiller::hashFillPattern(HatchFillPattern fillPattern) { - ImageAllocateResults ret = {}; - - auto* physDev = m_device->getPhysicalDevice(); - - bool alreadyBlockedForDeferredFrees = false; - - // Attempt to create a GPU image and corresponding image view for this texture. - // If creation or memory allocation fails (likely due to VRAM exhaustion), - // we'll evict another texture from the LRU cache and retry until successful, or until only the currently-cachedImageRecord image remains. - while (imagesCache->size() > 0u) - { - // Try creating the image and allocating memory for it: - nbl::video::IGPUImage::SCreationParams params = {}; - params = imageParams; - - if (imageViewFormatOverride != asset::E_FORMAT::EF_COUNT && imageViewFormatOverride != imageParams.format) - { - params.viewFormats.set(static_cast(imageViewFormatOverride), true); - params.flags |= asset::IImage::E_CREATE_FLAGS::ECF_MUTABLE_FORMAT_BIT; - } - auto gpuImage = m_device->createImage(std::move(params)); - - if (gpuImage) - { - nbl::video::IDeviceMemoryBacked::SDeviceMemoryRequirements gpuImageMemoryRequirements = gpuImage->getMemoryReqs(); - uint32_t actualAlignment = 1u << gpuImageMemoryRequirements.alignmentLog2; - const bool imageMemoryRequirementsMatch = - (physDev->getDeviceLocalMemoryTypeBits() & gpuImageMemoryRequirements.memoryTypeBits) != 0 && // should have device local memory compatible - (gpuImageMemoryRequirements.requiresDedicatedAllocation == false) && // should not require dedicated allocation - ((ImagesMemorySubAllocator::MaxMemoryAlignment % actualAlignment) == 0u); // should be consistent with our suballocator's max alignment - - if (imageMemoryRequirementsMatch) - { - // OutputDebugStringA(std::format("ALlocating {} !!!!\n", gpuImageMemoryRequirements.size).c_str()); - // m_logger.log(std::format(" [BEFORE] Allocator Free Size={} \n",imagesMemorySubAllocator->getFreeSize()).c_str(), nbl::system::ILogger::ELL_INFO); - ret.allocationOffset = imagesMemorySubAllocator->allocate(gpuImageMemoryRequirements.size, 1u << gpuImageMemoryRequirements.alignmentLog2); - // m_logger.log(std::format(" [AFTER] Alloc Size = {}, Alloc Offset = {}, Alignment = {} \n",gpuImageMemoryRequirements.size, ret.allocationOffset, 1u << gpuImageMemoryRequirements.alignmentLog2).c_str(), nbl::system::ILogger::ELL_INFO); - // m_logger.log(std::format(" [AFTER] Allocator Free Size={} \n",imagesMemorySubAllocator->getFreeSize()).c_str(), nbl::system::ILogger::ELL_INFO); - const bool allocationFromImagesMemoryArenaSuccessfull = ret.allocationOffset != ImagesMemorySubAllocator::InvalidAddress; - if (allocationFromImagesMemoryArenaSuccessfull) - { - ret.allocationSize = gpuImageMemoryRequirements.size; - nbl::video::ILogicalDevice::SBindImageMemoryInfo bindImageMemoryInfo = - { - .image = gpuImage.get(), - .binding = { .memory = imagesMemoryArena.memory.get(), .offset = imagesMemoryArena.offset + ret.allocationOffset } - }; - const bool boundToMemorySuccessfully = m_device->bindImageMemory({ &bindImageMemoryInfo, 1u }); - if (boundToMemorySuccessfully) - { - gpuImage->setObjectDebugName(imageDebugName.c_str()); - IGPUImageView::SCreationParams viewParams = { - .image = gpuImage, - .viewType = IGPUImageView::ET_2D, - .format = (imageViewFormatOverride == asset::E_FORMAT::EF_COUNT) ? gpuImage->getCreationParameters().format : imageViewFormatOverride - }; - - const uint32_t channelCount = nbl::asset::getFormatChannelCount(viewParams.format); - if (channelCount == 1u) - { - // for rendering grayscale: - viewParams.components.r = nbl::asset::IImageViewBase::SComponentMapping::E_SWIZZLE::ES_R; - viewParams.components.g = nbl::asset::IImageViewBase::SComponentMapping::E_SWIZZLE::ES_R; - viewParams.components.b = nbl::asset::IImageViewBase::SComponentMapping::E_SWIZZLE::ES_R; - viewParams.components.a = nbl::asset::IImageViewBase::SComponentMapping::E_SWIZZLE::ES_ONE; - } - - ret.gpuImageView = m_device->createImageView(std::move(viewParams)); - if (ret.gpuImageView) - { - // SUCCESS! - ret.gpuImageView->setObjectDebugName((imageDebugName + " View").c_str()); - } - else - { - // irrecoverable error if simple image creation fails. - m_logger.log("tryCreateAndAllocateImage_SubmitIfNeeded: gpuImageView creation failed, that's rare and irrecoverable when adding a new image.", nbl::system::ILogger::ELL_ERROR); - _NBL_DEBUG_BREAK_IF(true); - } - - // succcessful with everything, just break and get out of this retry loop - break; - } - else - { - // irrecoverable error if simple bindImageMemory fails. - m_logger.log("tryCreateAndAllocateImage_SubmitIfNeeded: bindImageMemory failed, that's irrecoverable when adding a new image.", nbl::system::ILogger::ELL_ERROR); - _NBL_DEBUG_BREAK_IF(true); - break; - } - } - else - { - m_logger.log(std::format("Retrying Allocation after failure with Allocation Size={}, Allocator Free Size={} \n", gpuImageMemoryRequirements.size, imagesMemorySubAllocator->getFreeSize()).c_str(), nbl::system::ILogger::ELL_INFO); - // recoverable error when allocation fails, we don't log anything, next code will try evicting other images and retry - } - } - else - { - m_logger.log("tryCreateAndAllocateImage_SubmitIfNeeded: memory requirements of the gpu image doesn't match our preallocated device memory, that's irrecoverable when adding a new image.", nbl::system::ILogger::ELL_ERROR); - _NBL_DEBUG_BREAK_IF(true); - break; - } - } - else - { - m_logger.log("tryCreateAndAllocateImage_SubmitIfNeeded: gpuImage creation failed, that's irrecoverable when adding a new image.", nbl::system::ILogger::ELL_ERROR); - _NBL_DEBUG_BREAK_IF(true); - break; - } - - // Getting here means we failed creating or allocating the image, evict and retry. - - - // If imageCache size is 1 it means there is nothing else to evict, but there may still be already evicts/frees queued up. - // `cull_frees` will make sure all pending deallocations will be blocked for. - if (imagesCache->size() == 1u && alreadyBlockedForDeferredFrees) - { - // We give up, it's really nothing we can do, no image to evict (alreadyBlockedForDeferredFrees==1) and no more memory to free up (alreadyBlockedForDeferredFrees). - // We probably have evicted almost every other texture except the one we just allocated an index for. - // This is most likely due to current image memory requirement being greater than the whole memory allocated for all images - m_logger.log("tryCreateAndAllocateImage_SubmitIfNeeded: failed allocating an image, there is nothing more from mcache to evict, the current memory requirement is simply greater than the whole memory allocated for all images.", nbl::system::ILogger::ELL_ERROR); - _NBL_DEBUG_BREAK_IF(true); - break; - } - - if (imagesCache->size() > 1u) - { - const image_id evictionCandidate = imagesCache->select_eviction_candidate(); - CachedImageRecord* imageRef = imagesCache->peek(evictionCandidate); - if (imageRef) - evictImage_SubmitIfNeeded(evictionCandidate, *imageRef, intendedNextSubmit); - imagesCache->erase(evictionCandidate); - } - - while (imagesDescriptorIndexAllocator->cull_frees()) {}; // to make sure deallocation requests in eviction callback are blocked for. - alreadyBlockedForDeferredFrees = true; - - // we don't hold any references to the GPUImageView or GPUImage so descriptor binding will be the last reference - // hopefully by here the suballocated descriptor set freed some VRAM by dropping the image last ref and it's dedicated allocation. - } + std::size_t hash = std::hash{}(uint32_t(MSDFType::HATCH_FILL_PATTERN)); + nbl::core::hash_combine(hash, std::hash{}(uint32_t(fillPattern))); + return hash; +} - return ret; +DrawResourcesFiller::msdf_hash DrawResourcesFiller::hashFontGlyph(size_t fontHash, uint32_t glyphIndex) +{ + std::size_t hash = std::hash{}(uint32_t(MSDFType::FONT_GLYPH)); + nbl::core::hash_combine(hash, std::hash{}(fontHash)); + nbl::core::hash_combine(hash, std::hash{}(glyphIndex)); + return hash; } void DrawResourcesFiller::setGlyphMSDFTextureFunction(const GetGlyphMSDFTextureFunc& func) @@ -2965,127 +843,74 @@ void DrawResourcesFiller::setHatchFillMSDFTextureFunction(const GetHatchFillPatt getHatchFillPatternMSDF = func; } -void DrawResourcesFiller::markFrameUsageComplete(uint64_t drawSubmitWaitValue) -{ - // m_logger.log(std::format("Finished Frame Idx = {}", currentFrameIndex).c_str(), nbl::system::ILogger::ELL_INFO); - currentFrameIndex++; - // TODO[LATER]: take into account that currentFrameIndex was submitted with drawSubmitWaitValue; Use that value when deallocating the resources marked with this frame index - // Currently, for evictions the worst case value will be waited for, as there is no way yet to know which semaphoroe value will signal the completion of the (to be evicted) resource's usage -} - -uint32_t DrawResourcesFiller::getMSDFIndexFromInputInfo(const MSDFInputInfo& msdfInfo, const SIntendedSubmitInfo& intendedNextSubmit) +uint32_t DrawResourcesFiller::getMSDFTextureIndex(msdf_hash hash) { - uint32_t textureIdx = InvalidTextureIndex; - MSDFReference* tRef = msdfLRUCache->get(msdfInfo); - if (tRef) - { - textureIdx = tRef->alloc_idx; - tRef->lastUsedFrameIndex = currentFrameIndex; // update this because the texture will get used on the next frame - } - return textureIdx; + auto ptr = textureLRUCache->get(hash); + if (ptr) return ptr->alloc_idx; + else return InvalidMSDFHash; } -uint32_t DrawResourcesFiller::addMSDFTexture(const MSDFInputInfo& msdfInput, core::smart_refctd_ptr&& cpuImage, SIntendedSubmitInfo& intendedNextSubmit) +uint32_t DrawResourcesFiller::addMSDFTexture(std::function()> createResourceIfEmpty, msdf_hash hash, SIntendedSubmitInfo& intendedNextSubmit) { - if (!cpuImage) - { - m_logger.log("addMSDFTexture: cpuImage is nullptr.", nbl::system::ILogger::ELL_ERROR); - return InvalidTextureIndex; - } - - const auto cpuImageSize = cpuImage->getMipSize(0); - const bool sizeMatch = cpuImageSize.x == getMSDFResolution().x && cpuImageSize.y == getMSDFResolution().y && cpuImageSize.z == 1u; - if (!sizeMatch) - { - m_logger.log("addMSDFTexture: cpuImage size doesn't match with msdf array image.", nbl::system::ILogger::ELL_ERROR); - return InvalidTextureIndex; - } + // TextureReferences hold the semaValue related to the "scratch semaphore" in IntendedSubmitInfo + // Every single submit increases this value by 1 + // The reason for hiolding on to the lastUsedSema is deferred dealloc, which we call in the case of eviction, making sure we get rid of the entry inside the allocator only when the texture is done being used + const auto nextSemaSignal = intendedNextSubmit.getFutureScratchSemaphore(); - /* - * The `msdfTextureArrayIndexAllocator` manages indices (slots) into a texture array for MSDF images. - * When all slots are occupied, the least recently used entry is evicted via `msdfLRUCache`. - * This callback is invoked on eviction, and must: - * - Ensure safe deallocation of the slot. - * - Submit any pending draw calls if the evicted MSDF was scheduled to be used in the upcoming submission. - */ auto evictionCallback = [&](const MSDFReference& evicted) { - // `deallocationWaitInfo` is used to prepare wait info to defer index deallocation until the GPU has finished using the resource. - // NOTE: `deallocationWaitInfo` is currently *not* required for correctness because: - // - Both the image upload (msdfImagesState) and usage occur within the same timeline (`intendedNextSubmit`). - // - timeline semaphores guarantee proper ordering: the next submit's msdfImagesState will wait on the prior usage. - // - Therefore, we can safely overwrite or reallocate the slot without waiting for explicit GPU completion. - // - // However, this `deallocationWaitInfo` *will* become essential if we start interacting with MSDF images - // outside the `intendedNextSubmit` timeline for example, issuing uploads via a transfer queue or using a separate command buffer and timeline. - - const bool imageUsedForNextIntendedSubmit = (evicted.lastUsedFrameIndex == currentFrameIndex); - - if (imageUsedForNextIntendedSubmit) + if (msdfTextureArrayIndicesUsed.contains(evicted.alloc_idx)) { - // The evicted image is scheduled for use in the upcoming submit. - // To avoid rendering artifacts, we must flush the current draw queue now. - // After submission, we reset state so that data referencing the evicted slot can be re-uploaded. - submitDraws(intendedNextSubmit); - reset(); // resets everything, things referenced through mainObj and other shit will be pushed again through acquireXXX_SubmitIfNeeded + // Dealloc once submission is finished + msdfTextureArrayIndexAllocator->multi_deallocate(1u, &evicted.alloc_idx, nextSemaSignal); - // Prepare wait info to defer index deallocation until the GPU has finished using the resource. - // we wait on the signal semaphore for the submit we just did above. - ISemaphore::SWaitInfo deallocationWaitInfo = { .semaphore = intendedNextSubmit.scratchSemaphore.semaphore, .value = intendedNextSubmit.scratchSemaphore.value }; - msdfTextureArrayIndexAllocator->multi_deallocate(1u, &evicted.alloc_idx, deallocationWaitInfo); - } - else - { - // The image is not used in the current frame, so we can deallocate without submitting any draws. - // Still wait on the semaphore to ensure past GPU usage is complete. - // TODO: We don't know which semaphore value the frame with `evicted.lastUsedFrameIndex` index was submitted with, so we wait for the worst case value which is the immediate prev submit (scratchSemaphore.value). - ISemaphore::SWaitInfo deallocationWaitInfo = { .semaphore = intendedNextSubmit.scratchSemaphore.semaphore, .value = intendedNextSubmit.scratchSemaphore.value }; - msdfTextureArrayIndexAllocator->multi_deallocate(1u, &evicted.alloc_idx, deallocationWaitInfo); + // Submit + finalizeAllCopiesToGPU(intendedNextSubmit); + submitDraws(intendedNextSubmit); + // Importatn: We don't reset anything because the auto submit wasn't due to lack of any of the buffers such as geometry, drawObjs or mainObjs + // If we reset it will cause an auto submission bug, where adding an msdf texture while constructing glyphs will invalidate geometries and main objects + // resetGeometryCounters(); + // resetMainObjectCounters(); + } else { + // We didn't use it this frame, so it's safe to dealloc now + msdfTextureArrayIndexAllocator->multi_deallocate(1u, &evicted.alloc_idx); } - - // Clear CPU-side metadata associated with the evicted slot. - msdfImagesState[evicted.alloc_idx].evict(); }; // We pass nextSemaValue instead of constructing a new MSDFReference and passing it into `insert` that's because we might get a cache hit and only update the value of the nextSema - MSDFReference* inserted = msdfLRUCache->insert(msdfInput, currentFrameIndex, evictionCallback); + MSDFReference* inserted = textureLRUCache->insert(hash, nextSemaSignal.value, evictionCallback); - inserted->lastUsedFrameIndex = currentFrameIndex; // in case there was an eviction + auto-submit, we need to update AGAIN - - // if cachedImageRecord->alloc_idx was not InvalidTextureIndex then it means we had a cache hit and updated the value of our sema, in which case we don't queue anything for upload, and return the idx - if (inserted->alloc_idx == InvalidTextureIndex) + // if inserted->alloc_idx was not InvalidTextureIdx then it means we had a cache hit and updated the value of our sema, in which case we don't queue anything for upload, and return the idx + if (inserted->alloc_idx == InvalidTextureIdx) { - // New insertion == cache miss happened and insertion was successfull - inserted->alloc_idx = IndexAllocator::AddressAllocator::invalid_address; - msdfTextureArrayIndexAllocator->multi_allocate(std::chrono::time_point::max(), 1u, &inserted->alloc_idx); // if the prev submit causes DEVICE_LOST then we'll get a deadlock here since we're using max timepoint + auto cpuImage = createResourceIfEmpty(); - if (inserted->alloc_idx != IndexAllocator::AddressAllocator::invalid_address) - { - // We stage msdfImagesState, pushMSDFImagesUploads will push it into GPU - msdfImagesState[inserted->alloc_idx].image = std::move(cpuImage); - msdfImagesState[inserted->alloc_idx].uploadedToGPU = false; - } - else + const auto cpuImageSize = cpuImage->getMipSize(0); + const bool sizeMatch = cpuImageSize.x == getMSDFResolution().x && cpuImageSize.y == getMSDFResolution().y && cpuImageSize.z == 1u; + + if (sizeMatch) { - m_logger.log("addMSDFTexture: index allocation failed.", nbl::system::ILogger::ELL_ERROR); - inserted->alloc_idx = InvalidTextureIndex; + // New insertion == cache miss happened and insertion was successfull + inserted->alloc_idx = IndexAllocator::AddressAllocator::invalid_address; + msdfTextureArrayIndexAllocator->multi_allocate(1u, &inserted->alloc_idx); + + // We queue copy and finalize all on `finalizeTextureCopies` function called before draw calls to make sure it's in mem + textureCopies.push_back({ .image = std::move(cpuImage), .index = inserted->alloc_idx }); } } - - assert(inserted->alloc_idx != InvalidTextureIndex); // shouldn't happen, because we're using LRU cache, so worst case eviction will happen + multi-deallocate and next next multi_allocate should definitely succeed + + assert(inserted->alloc_idx != InvalidTextureIdx); + if (inserted->alloc_idx != InvalidTextureIdx) + msdfTextureArrayIndicesUsed.emplace(inserted->alloc_idx); return inserted->alloc_idx; } -void DrawResourcesFiller::flushDrawObjects() +uint32_t DrawResourcesFiller::addMSDFTexture(core::smart_refctd_ptr textureBuffer, msdf_hash hash, SIntendedSubmitInfo& intendedNextSubmit) { - if (resourcesCollection.drawObjects.getCount() > drawObjectsFlushedToDrawCalls) - { - DrawCallData drawCall = {}; - drawCall.isDTMRendering = false; - drawCall.drawObj.drawObjectStart = drawObjectsFlushedToDrawCalls; - drawCall.drawObj.drawObjectCount = resourcesCollection.drawObjects.getCount() - drawObjectsFlushedToDrawCalls; - drawCalls.push_back(drawCall); - drawObjectsFlushedToDrawCalls = resourcesCollection.drawObjects.getCount(); - } + return addMSDFTexture( + [textureBuffer] { return std::move(textureBuffer); }, + hash, + intendedNextSubmit + ); } \ No newline at end of file diff --git a/62_CAD/DrawResourcesFiller.h b/62_CAD/DrawResourcesFiller.h index 3b0e0c4bb..b5786b716 100644 --- a/62_CAD/DrawResourcesFiller.h +++ b/62_CAD/DrawResourcesFiller.h @@ -1,37 +1,32 @@ -/******************************************************************************/ -/* DrawResourcesFiller: This class provides important functionality to manage resources needed for a draw. -/******************************************************************************/ #pragma once - -#if __has_include("glm/glm/glm.hpp") // legacy -#include "glm/glm/glm.hpp" -#else -#include "glm/glm.hpp" // new build system -#endif -#include -#include -#include -#include -#include #include "Polyline.h" #include "Hatch.h" #include "IndexAllocator.h" #include -#include "CTriangleMesh.h" -#include "Shaders/globals.hlsl" -#include "Images.h" - -//#include +#include #include using namespace nbl; using namespace nbl::video; using namespace nbl::core; using namespace nbl::asset; +using namespace nbl::ext::TextRendering; static_assert(sizeof(DrawObject) == 16u); -static_assert(sizeof(MainObject) == 20u); -static_assert(sizeof(LineStyle) == 88u); +static_assert(sizeof(MainObject) == 16u); +static_assert(sizeof(Globals) == 128u); +static_assert(sizeof(LineStyle) == 96u); +static_assert(sizeof(ClipProjectionData) == 88u); + +template +struct DrawBuffers +{ + smart_refctd_ptr indexBuffer; // only is valid for IGPUBuffer because it's filled at allocation time and never touched again + smart_refctd_ptr mainObjectsBuffer; + smart_refctd_ptr drawObjectsBuffer; + smart_refctd_ptr geometryBuffer; + smart_refctd_ptr lineStylesBuffer; +}; // ! DrawResourcesFiller // ! This class provides important functionality to manage resources needed for a draw. @@ -42,168 +37,29 @@ static_assert(sizeof(LineStyle) == 88u); struct DrawResourcesFiller { public: - - // We pack multiple data types in a single buffer, we need to makes sure each offset starts aligned to avoid mis-aligned accesses - static constexpr size_t GPUStructsMaxNaturalAlignment = 8u; - static constexpr size_t MinimumDrawResourcesMemorySize = 512u * 1 << 20u; // 512MB - - /// @brief general parent struct for 1.ReservedCompute and 2.CPUGenerated Resources - struct ResourceBase - { - static constexpr size_t InvalidBufferOffset = ~0u; - size_t bufferOffset = InvalidBufferOffset; // set when copy to gpu buffer is issued - virtual size_t getCount() const = 0; - virtual size_t getStorageSize() const = 0; - virtual size_t getAlignedStorageSize() const { return core::alignUp(getStorageSize(), GPUStructsMaxNaturalAlignment); } - }; - - /// @brief ResourceBase reserved for compute shader stages input/output - template - struct ReservedComputeResource : ResourceBase - { - size_t count = 0ull; - size_t getCount() const override { return count; } - size_t getStorageSize() const override { return count * sizeof(T); } - }; - - /// @brief ResourceBase which is filled by CPU, packed and sent to GPU - template - struct CPUGeneratedResource : ResourceBase - { - core::vector vector; - size_t getCount() const { return vector.size(); } - size_t getStorageSize() const { return vector.size() * sizeof(T); } - - /// @return pointer to start of the data to be filled, up to additionalCount - T* increaseCountAndGetPtr(size_t additionalCount) - { - size_t offset = vector.size(); - vector.resize(offset + additionalCount); - return &vector[offset]; - } - - /// @brief increases size of general-purpose resources that hold bytes - /// @param alignment: Alignment of the pointer returned to be filled, should be PoT and <= GPUStructsMaxNaturalAlignment, only use this if storing raw bytes in vector - /// @return pointer to start of the data to be filled, up to additional size - size_t increaseSizeAndGetOffset(size_t additionalSize, size_t alignment) - { - assert(core::isPoT(alignment) && alignment <= GPUStructsMaxNaturalAlignment); - size_t offset = core::alignUp(vector.size(), alignment); - vector.resize(offset + additionalSize); - return offset; - } - - uint32_t addAndGetOffset(const T& val) - { - vector.push_back(val); - return vector.size() - 1u; - } - - T* data() { return vector.data(); } - }; - /// @brief struct to hold all resources - // TODO: rename to staged resources buffers or something like that - struct ResourcesCollection - { - // auto-submission level 0 resources (settings that mainObj references) - CPUGeneratedResource lineStyles; - CPUGeneratedResource dtmSettings; - CPUGeneratedResource customProjections; - CPUGeneratedResource customClipRects; - - // auto-submission level 1 buffers (mainObj that drawObjs references, if all drawObjs+idxBuffer+geometryInfo doesn't fit into mem this will be broken down into many) - CPUGeneratedResource mainObjects; - - // auto-submission level 2 buffers - CPUGeneratedResource drawObjects; - CPUGeneratedResource indexBuffer; // TODO: this is going to change to ReservedComputeResource where index buffer gets filled by compute shaders - CPUGeneratedResource geometryInfo; // general purpose byte buffer for custom data for geometries (eg. line points, bezier definitions, aabbs) - - // Get Total memory consumption, If all ResourcesCollection get packed together with GPUStructsMaxNaturalAlignment - // used to decide the remaining memory and when to overflow - size_t calculateTotalConsumption() const - { - return - lineStyles.getAlignedStorageSize() + - dtmSettings.getAlignedStorageSize() + - customProjections.getAlignedStorageSize() + - customClipRects.getAlignedStorageSize() + - mainObjects.getAlignedStorageSize() + - drawObjects.getAlignedStorageSize() + - indexBuffer.getAlignedStorageSize() + - geometryInfo.getAlignedStorageSize(); - } - }; - - // @brief Register a loader - void setGeoreferencedImageLoader(core::smart_refctd_ptr&& _imageLoader) - { - imageLoader = _imageLoader; - } + typedef uint32_t index_buffer_type; - uint32_t2 queryGeoreferencedImageExtents(std::filesystem::path imagePath) - { - return imageLoader->getExtents(imagePath); - } + using msdf_hash = std::size_t; - asset::E_FORMAT queryGeoreferencedImageFormat(std::filesystem::path imagePath) - { - return imageLoader->getFormat(imagePath); - } - DrawResourcesFiller(); - DrawResourcesFiller(smart_refctd_ptr&& device, smart_refctd_ptr&& bufferUploadUtils, smart_refctd_ptr&& imageUploadUtils, IQueue* copyQueue, core::smart_refctd_ptr&& logger); + DrawResourcesFiller(smart_refctd_ptr&& utils, IQueue* copyQueue); typedef std::function SubmitFunc; void setSubmitDrawsFunction(const SubmitFunc& func); - - // DrawResourcesFiller needs to access these in order to allocate GPUImages and write the to their correct descriptor set binding - void setTexturesDescriptorSetAndBinding(core::smart_refctd_ptr&& descriptorSet, uint32_t binding); + void allocateIndexBuffer(ILogicalDevice* logicalDevice, uint32_t indices); - /// @brief Get minimum required size for resources buffer (containing objects and geometry info and their settings) - static constexpr size_t getMinimumRequiredResourcesBufferSize() - { - // for auto-submission to work correctly, memory needs to serve at least 2 linestyle, 1 dtm settings, 1 clip proj, 1 main obj, 1 draw obj and 512 bytes of additional mem for geometries and index buffer - // this is the ABSOLUTE MINIMUM (if this value is used rendering will probably be as slow as CPU drawing :D) - return core::alignUp(sizeof(LineStyle) + sizeof(LineStyle) * DTMSettings::MaxContourSettings + sizeof(DTMSettings) + sizeof(WorldClipRect) + sizeof(float64_t3x3) + sizeof(MainObject) + sizeof(DrawObject) + 512ull, GPUStructsMaxNaturalAlignment); - } + void allocateMainObjectsBuffer(ILogicalDevice* logicalDevice, uint32_t mainObjects); - /** - * @brief Attempts to allocate a single contiguous device-local memory block for draw resources, divided into image and buffer sections. - * - * The function allocates a single memory block and splits it into image and buffer arenas. - * - * @param logicalDevice Pointer to the logical device used for memory allocation and resource creation. - * @param requiredImageMemorySize The size in bytes of the memory required for images. - * @param requiredBufferMemorySize The size in bytes of the memory required for buffers. - * @param memoryTypeIndexTryOrder Ordered list of memory type indices to attempt allocation with, in the order they should be tried. - * - * @return true if the memory allocation and resource setup succeeded; false otherwise. - */ - bool allocateDrawResources(ILogicalDevice* logicalDevice, size_t requiredImageMemorySize, size_t requiredBufferMemorySize, std::span memoryTypeIndexTryOrder); + void allocateDrawObjectsBuffer(ILogicalDevice* logicalDevice, uint32_t drawObjects); + + void allocateGeometryBuffer(ILogicalDevice* logicalDevice, size_t size); + + void allocateStylesBuffer(ILogicalDevice* logicalDevice, uint32_t lineStylesCount); - /** - * @brief Attempts to allocate draw resources within a given VRAM budget, retrying with progressively smaller sizes on failure. - * - * This function preserves the initial image-to-buffer memory ratio. If the initial sizes are too small, - * it scales them up to meet a minimum required threshold. On allocation failure, it reduces the memory - * sizes by a specified percentage and retries, until it either succeeds or the number of attempts exceeds `maxTries`. - * - * @param logicalDevice Pointer to the logical device used for allocation. - * @param maxImageMemorySize Initial image memory size (in bytes) to attempt allocation with. - * @param maxBufferMemorySize Initial buffer memory size (in bytes) to attempt allocation with. - * @param memoryTypeIndexTryOrder Ordered list of memory type indices to attempt allocation with, in the order they should be tried. - * @param reductionPercent The percentage by which to reduce the memory sizes after each failed attempt (e.g., 10 means reduce by 10%). - * @param maxTries Maximum number of attempts to try reducing and allocating memory. - * - * @return true if the allocation succeeded at any iteration; false if all attempts failed. - */ - bool allocateDrawResourcesWithinAvailableVRAM(ILogicalDevice* logicalDevice, size_t maxImageMemorySize, size_t maxBufferMemorySize, std::span memoryTypeIndexTryOrder, uint32_t reductionPercent = 10u, uint32_t maxTries = 32u); - - bool allocateMSDFTextures(ILogicalDevice* logicalDevice, uint32_t maxMSDFs, uint32_t2 msdfsExtent); + void allocateMSDFTextures(ILogicalDevice* logicalDevice, uint32_t maxMSDFs, uint32_t2 msdfsExtent); // functions that user should set to get MSDF texture if it's not available in cache. // it's up to user to return cached or generate on the fly. @@ -212,34 +68,11 @@ struct DrawResourcesFiller void setGlyphMSDFTextureFunction(const GetGlyphMSDFTextureFunc& func); void setHatchFillMSDFTextureFunction(const GetHatchFillPatternMSDFTextureFunc& func); - // Must be called at the end of each frame. - // right before submitting the main draw that uses the currently queued geometry, images, or other objects/resources. - // Registers the semaphore/value that will signal completion of this frame�s draw, - // This allows future frames to safely deallocate or evict resources used in the current frame by waiting on this signal before reuse or destruction. - // `drawSubmitWaitValue` should reference the wait value of the draw submission finishing this frame using the `intendedNextSubmit`; - void markFrameUsageComplete(uint64_t drawSubmitWaitValue); - - // TODO[Przemek]: try to draft up a `CTriangleMesh` Class in it's own header (like CPolyline), simplest form is basically two cpu buffers (1 array of uint index buffer, 1 array of float64_t3 vertexBuffer) - // TODO[Przemek]: Then have a `drawMesh` function here similar to drawXXX's below, this will fit both vertex and index buffer in the `geometryBuffer`. - // take a `SIntendedSubmitInfo` like others, but don't use it as I don't want you to handle anything regarding autoSubmit - // somehow retrieve or calculate the geometry buffer offsets of your vertex and index buffer to be used outside for binding purposes - //! this function fills buffers required for drawing a polyline and submits a draw through provided callback when there is not enough memory. void drawPolyline(const CPolylineBase& polyline, const LineStyleInfo& lineStyleInfo, SIntendedSubmitInfo& intendedNextSubmit); - //! Draws a fixed-geometry polyline using a custom transformation. - //! TODO: Change `polyline` input to an ID referencing a possibly cached instance in our buffers, allowing reuse and avoiding redundant uploads. - void drawFixedGeometryPolyline(const CPolylineBase& polyline, const LineStyleInfo& lineStyleInfo, const float64_t3x3& transformation, TransformationType transformationType, SIntendedSubmitInfo& intendedNextSubmit); - - /// Use this in a begin/endMainObject scope when you want to draw different polylines that should essentially be a single main object (no self-blending between components of a single main object) - /// WARNING: make sure this function is called within begin/endMainObject scope - void drawPolyline(const CPolylineBase& polyline, SIntendedSubmitInfo& intendedNextSubmit); + void drawPolyline(const CPolylineBase& polyline, uint32_t polylineMainObjIdx, SIntendedSubmitInfo& intendedNextSubmit); - void drawTriangleMesh( - const CTriangleMesh& mesh, - const DTMSettingsInfo& dtmSettingsInfo, - SIntendedSubmitInfo& intendedNextSubmit); - // ! Convinience function for Hatch with MSDF Pattern and a solid background void drawHatch( const Hatch& hatch, @@ -260,36 +93,8 @@ struct DrawResourcesFiller const Hatch& hatch, const float32_t4& color, SIntendedSubmitInfo& intendedNextSubmit); - - //! Convinience function for fixed-geometry Hatch with MSDF Pattern and a solid background - void drawFixedGeometryHatch( - const Hatch& hatch, - const float32_t4& foregroundColor, - const float32_t4& backgroundColor, - const HatchFillPattern fillPattern, - const float64_t3x3& transformation, - TransformationType transformationType, - SIntendedSubmitInfo& intendedNextSubmit); - - // ! Fixed-geometry Hatch with MSDF Pattern - void drawFixedGeometryHatch( - const Hatch& hatch, - const float32_t4& color, - const HatchFillPattern fillPattern, - const float64_t3x3& transformation, - TransformationType transformationType, - SIntendedSubmitInfo& intendedNextSubmit); - // ! Solid Fill Fixed-geometry Hatch - void drawFixedGeometryHatch( - const Hatch& hatch, - const float32_t4& color, - const float64_t3x3& transformation, - TransformationType transformationType, - SIntendedSubmitInfo& intendedNextSubmit); - - /// Used by SingleLineText, Issue drawing a font glyph - /// WARNING: make sure this function is called within begin/endMainObject scope + // ! Draw Font Glyph, will auto submit if there is no space void drawFontGlyph( nbl::ext::TextRendering::FontFace* fontFace, uint32_t glyphIdx, @@ -297,207 +102,104 @@ struct DrawResourcesFiller float32_t2 dirU, float32_t aspectRatio, float32_t2 minUV, + uint32_t mainObjIdx, SIntendedSubmitInfo& intendedNextSubmit); + + void _test_addImageObject( + float64_t2 topLeftPos, + float32_t2 size, + float32_t rotation, + SIntendedSubmitInfo& intendedNextSubmit) + { + auto addImageObject_Internal = [&](const ImageObjectInfo& imageObjectInfo, uint32_t mainObjIdx) -> bool + { + const auto maxGeometryBufferImageObjects = (maxGeometryBufferSize - currentGeometryBufferSize) / sizeof(ImageObjectInfo); + uint32_t uploadableObjects = (maxIndexCount / 6u) - currentDrawObjectCount; + uploadableObjects = min(uploadableObjects, maxDrawObjects - currentDrawObjectCount); + uploadableObjects = min(uploadableObjects, maxGeometryBufferImageObjects); + + if (uploadableObjects >= 1u) + { + void* dstGeom = reinterpret_cast(cpuDrawBuffers.geometryBuffer->getPointer()) + currentGeometryBufferSize; + memcpy(dstGeom, &imageObjectInfo, sizeof(ImageObjectInfo)); + uint64_t geomBufferAddr = geometryBufferAddress + currentGeometryBufferSize; + currentGeometryBufferSize += sizeof(ImageObjectInfo); + + DrawObject drawObj = {}; + drawObj.type_subsectionIdx = uint32_t(static_cast(ObjectType::IMAGE) | (0 << 16)); // TODO: use custom pack/unpack function + drawObj.mainObjIndex = mainObjIdx; + drawObj.geometryAddress = geomBufferAddr; + void* dstDrawObj = reinterpret_cast(cpuDrawBuffers.drawObjectsBuffer->getPointer()) + currentDrawObjectCount; + memcpy(dstDrawObj, &drawObj, sizeof(DrawObject)); + currentDrawObjectCount += 1u; + + return true; + } + else + return false; + }; + + uint32_t mainObjIdx = addMainObject_SubmitIfNeeded(InvalidStyleIdx, intendedNextSubmit); + + ImageObjectInfo info = {}; + info.topLeft = topLeftPos; + info.dirU = float32_t2(size.x * cos(rotation), size.x * sin(rotation)); // + info.aspectRatio = size.y / size.x; + info.textureID = 0u; + if (!addImageObject_Internal(info, mainObjIdx)) + { + // single image object couldn't fit into memory to push to gpu, so we submit rendering current objects and reset geometry buffer and draw objects + submitCurrentObjectsAndReset(intendedNextSubmit, mainObjIdx); + bool success = addImageObject_Internal(info, mainObjIdx); + assert(success); // this should always be true, otherwise it's either bug in code or not enough memory allocated to hold a single image object + } + } - void drawGridDTM(const float64_t2& topLeft, - float64_t2 worldSpaceExtents, - float gridCellWidth, - uint64_t textureID, - const DTMSettingsInfo& dtmSettingsInfo, - SIntendedSubmitInfo& intendedNextSubmit); + void finalizeAllCopiesToGPU(SIntendedSubmitInfo& intendedNextSubmit); - /** - * @brief Adds a static 2D image to the draw resource set for rendering. - * - * This function ensures that a given image is available as a GPU-resident texture for future draw submissions. - * It uses an LRU cache to manage descriptor set slots and evicts old images if necessary to make room for new ones. - * - * If the image is already cached and its slot is valid, it returns true; - * Otherwise, it performs the following: - * - Allocates a new descriptor set slot. - * - Promotes the image format to be GPU-compatible. - * - Creates a GPU image and GPU image view. - * - Queues the image for uploading via staging in the next submit. - * - If memory is constrained, attempts to evict other images to free up space. - * - * @param staticImage Unique identifier for the image resource plus the CPU-side image resource to (possibly) upload. - * @param staticImage::forceUpdate If true, bypasses the existing GPU-side cache and forces an update of the image data; Useful when replacing the contents of a static image that may already be resident. - * @param intendedNextSubmit Struct representing the upcoming submission, including a semaphore for safe scheduling. - * - * @note This function ensures that the descriptor slot is not reused while the GPU may still be reading from it. - * If an eviction is required and the evicted image is scheduled to be used in the next submit, it triggers - * a flush of pending draws to preserve correctness. - * - * @note The function uses the `imagesCache` LRU cache to track usage and validity of texture slots. - * If an insertion leads to an eviction, a callback ensures proper deallocation and synchronization. - * @return true if the image was successfully cached and is ready for use; false if allocation failed most likely due to the image being larger than the memory arena allocated for all images. - */ - bool ensureStaticImageAvailability(const StaticImageInfo& staticImage, SIntendedSubmitInfo& intendedNextSubmit); - - /** - * @brief Ensures that multiple static 2D images are resident and ready for rendering. - * - * Attempts to make all provided static images GPU-resident by calling `ensureStaticImageAvailability` - * for each. Afterward, it verifies that none of the newly ensured images have been evicted, - * which could happen due to limited VRAM or memory fragmentation. - * - * This function is expected to succeed if: - * - The number of images does not exceed `ImagesBindingArraySize`. - * - Each image individually fits into the image memory arena. - * - There is enough VRAM to hold all images simultaneously. - * - * @param staticImages A span of StaticImageInfo structures describing the images to be ensured. - * @param intendedNextSubmit Struct representing the upcoming submission, including a semaphore for safe scheduling. - * - * @return true If all images were successfully made resident and none were evicted during the process. - * @return false If: - * - The number of images exceeds the descriptor binding array size. - * - Any individual image could not be made resident (e.g., larger than the allocator can support). - * - Some images were evicted due to VRAM pressure or allocator fragmentation, in which case Clearing the image cache and retrying MIGHT be a success (TODO: handle internally) - */ - bool ensureMultipleStaticImagesAvailability(std::span staticImages, SIntendedSubmitInfo& intendedNextSubmit); - - // This function must be called immediately after `addStaticImage` for the same imageID. - void addImageObject(image_id imageID, const OrientedBoundingBox2D& obb, SIntendedSubmitInfo& intendedNextSubmit); - - /* - Georeferenced Image Functions: - */ - - /** - * @brief Computes the recommended GPU image extents for streamed (georeferenced) imagery. - * - * This function estimates the required GPU-side image size to safely cover the current viewport, accounting for: - * - Full coverage of twice the viewport at mip 0 - * - Arbitrary rotation (by considering the diagonal) - * - Padding - * - * The resulting size is always rounded up to a multiple of the georeferenced tile size. - * - * @param viewportExtents The width and height of the viewport in pixels. - * @return A uint32_t2 representing the GPU image width and height for streamed imagery. - */ - static uint32_t2 computeStreamingImageExtentsForViewportCoverage(const uint32_t2 viewportExtents); - - /** - * @brief Creates a streaming state for a georeferenced image. - * - * This function prepares the required state for streaming and rendering a georeferenced image. - * - * WARNING: User should make sure to: - * - Transforms the OBB into world space if custom projections (such as dwg/symbols) are active. - * - * Specifically, this function: - * - Builds a new GeoreferencedImageStreamingState for the given image ID, OBB, and storage path. - * - Looks up image info such as format and extents from the registered loader and the storage path - * - Updates the returned state with current viewport. - * - * @note The returned state is not managed by the cache. The caller is responsible for - * storing it and passing the same state to subsequent streaming and draw functions. - * - * this function does **not** insert the image into the internal cache, because doing so could lead to - * premature eviction (either of this image or of another resource) before the draw call is made. - * - * @param imageID Unique identifier of the image. - * @param worldspaceOBB Oriented bounding box of the image in world space. - * @param viewportExtent Extent of the current viewport in pixels. - * @param ndcToWorldMat 3x3 matrix transforming NDC coordinates to world coordinates. - * @param storagePath Filesystem path where the image data is stored. - * @return A GeoreferencedImageStreamingState object initialized for this image. - */ - nbl::core::smart_refctd_ptr ensureGeoreferencedImageEntry(image_id imageID, const OrientedBoundingBox2D& worldSpaceOBB, const uint32_t2 currentViewportExtents, const float64_t3x3& ndcToWorldMat, const std::filesystem::path& storagePath); - - /** - * @brief Launches tile loading for a cached georeferenced image. - * - * Queues all tiles visible in the current viewport for GPU upload. - * - * The work includes: - * - Calculating visible tile coverage from the OBB and viewport. - * - Loading the necessary tiles from disk via the registered `imageLoader`. - * - Preparing staging buffers and `IImage::SBufferCopy` upload regions for GPU transfer. - * - Appending the upload commands into `streamedImageCopies` for later execution. - * - Updating the state's tile occupancy map to reflect newly resident tiles. - * - * Context: this function is dedicated to streaming tiles for georeferenced images only. - * This function should be called anywhere between `ensureGeoreferencedImageEntry` and `finalizeGeoreferencedImageTileLoads` - * But It's prefered to start loading as soon as possible to hide the latency of loading tiles from disk. - * - * @note The `imageStreamingState` passed in must be exactly the one returned by `ensureGeoreferencedImageEntry` with same image_id. Passing a stale or unrelated state is undefined. - * @note This function only queues uploads; GPU transfer happens later when queued copies are executed. - * - * @param imageID Unique identifier of the image. - * @param imageStreamingState Reference to the GeoreferencedImageStreamingState created or returned by `ensureGeoreferencedImageEntry` with same image_id. - */ - bool launchGeoreferencedImageTileLoads(image_id imageID, GeoreferencedImageStreamingState* imageStreamingState, const WorldClipRect clipRect); - - bool cancelGeoreferencedImageTileLoads(image_id imageID); - - /** - * @brief Issue Drawing a GeoreferencedImage - * - * Ensures streaming resources are allocated, computes addressing and positioning info (OBB and min/max UV), and pushes the image info to the geometry buffer for rendering. - * - * This function should be called anywhere between `ensureGeoreferencedImageEntry` and `finalizeGeoreferencedImageTileLoads` - * - * @note The `imageStreamingState` must be the one returned by `ensureGeoreferencedImageEntry`. - * - * @param imageID Unique identifier of the image. - * @param imageStreamingState Reference to the GeoreferencedImageStreamingState created or returned by `ensureGeoreferencedImageEntry` with same image_id. - * @param intendedNextSubmit Submission info describing synchronization and barriers for the next batch. - */ - void drawGeoreferencedImage(image_id imageID, nbl::core::smart_refctd_ptr&& imageStreamingState, SIntendedSubmitInfo& intendedNextSubmit); - - /** - * @brief copies the queued up streamed copies. - * @note call this function after `drawGeoreferencedImage` to make sure there is a gpu resource to copy to. - * @because`drawGeoreferencedImage` internally calls `ensureGeoreferencedImageResources_AllocateIfNeeded` - */ - bool finalizeGeoreferencedImageTileLoads(SIntendedSubmitInfo& intendedNextSubmit); - - /// @brief call this function before submitting to ensure all buffer and textures resourcesCollection requested via drawing calls are copied to GPU - /// records copy command into intendedNextSubmit's active command buffer and might possibly submits if fails allocation on staging upload memory. - bool pushAllUploads(SIntendedSubmitInfo& intendedNextSubmit); - - /// @brief resets staging buffers and images - void reset() + inline uint32_t getLineStyleCount() const { return currentLineStylesCount; } + + inline uint32_t getDrawObjectCount() const { return currentDrawObjectCount; } + + inline uint32_t getMainObjectCount() const { return currentMainObjectCount; } + + inline size_t getCurrentMainObjectsBufferSize() const { - resetDrawObjects(); - resetMainObjects(); - resetCustomProjections(); - resetCustomClipRects(); - resetLineStyles(); - resetDTMSettings(); - - drawObjectsFlushedToDrawCalls = 0ull; - drawCalls.clear(); + return sizeof(MainObject) * currentMainObjectCount; } - /// @brief collection of all the resources that will eventually be reserved or copied to in the resourcesGPUBuffer, will be accessed via individual BDA pointers in shaders - const ResourcesCollection& getResourcesCollection() const; + inline size_t getCurrentDrawObjectsBufferSize() const + { + return sizeof(DrawObject) * currentDrawObjectCount; + } - /// @brief buffer containing all non-texture type resources - nbl::core::smart_refctd_ptr getResourcesGPUBuffer() const { return resourcesGPUBuffer; } + inline size_t getCurrentGeometryBufferSize() const + { + return currentGeometryBufferSize; + } - /// @return how far resourcesGPUBuffer was copied to by `finalizeAllCopiesToGPU` in `resourcesCollection` - const size_t getCopiedResourcesSize() { return copiedResourcesSize; } + inline size_t getCurrentLineStylesBufferSize() const + { + return sizeof(LineStyle) * currentLineStylesCount; + } - // Setting Active Resources: - void setActiveLineStyle(const LineStyleInfo& lineStyle); - - void setActiveDTMSettings(const DTMSettingsInfo& dtmSettingsInfo); + void reset() + { + resetGeometryCounters(); + resetMainObjectCounters(); + resetLineStyleCounters(); + } - void beginMainObject(MainObjectType type, TransformationType transformationType = TransformationType::TT_NORMAL); - void endMainObject(); + DrawBuffers cpuDrawBuffers; + DrawBuffers gpuDrawBuffers; - void pushCustomProjection(const float64_t3x3& projection); - void popCustomProjection(); + uint32_t addLineStyle_SubmitIfNeeded(const LineStyleInfo& lineStyle, SIntendedSubmitInfo& intendedNextSubmit); - void pushCustomClipRect(const WorldClipRect& clipRect); - void popCustomClipRect(); + uint32_t addMainObject_SubmitIfNeeded(uint32_t styleIdx, SIntendedSubmitInfo& intendedNextSubmit); - const std::deque& getCustomProjectionStack() const { return activeProjections; } - const std::deque& getCustomClipRectsStack() const { return activeClipRects; } + // we need to store the clip projection stack to make sure the front is always available in memory + void pushClipProjectionData(const ClipProjectionData& clipProjectionData); + void popClipProjectionData(); smart_refctd_ptr getMSDFsTextureArray() { return msdfTextureArray; } @@ -509,531 +211,180 @@ struct DrawResourcesFiller return msdfTextureArray->getCreationParameters().image->getCreationParameters().mipLevels; } - /// For advanced use only, (passed to shaders for them to know if we overflow-submitted in the middle if a main obj - uint32_t getActiveMainObjectIndex() const; + // TODO: Return to protected after testing + uint32_t addMSDFTexture(std::function()> createResourceIfEmpty, msdf_hash hash, SIntendedSubmitInfo& intendedNextSubmit); - struct MSDFImageState +protected: + + struct TextureCopy { core::smart_refctd_ptr image; - bool uploadedToGPU : 1u; - - bool isValid() const { return image.get() != nullptr; } - void evict() - { - image = nullptr; - uploadedToGPU = false; - } - }; - - // NOTE: Most probably Going to get removed soon with a single draw call in GPU-driven rendering - struct DrawCallData - { - union - { - struct Dtm - { - uint64_t indexBufferOffset; - uint64_t indexCount; - uint64_t triangleMeshVerticesBaseAddress; - uint32_t triangleMeshMainObjectIndex; - } dtm; - struct DrawObj - { - uint64_t drawObjectStart = 0ull; - uint64_t drawObjectCount = 0ull; - } drawObj; - }; - bool isDTMRendering; - }; - - const std::vector& getDrawCalls() const; - - /// @brief Stores all CPU-side resources that were staged and prepared for a single GPU submission. - /// - /// *** This cache includes anything used or referenced from DrawResourcesFiller in the Draw Submit: - /// - Buffer data (geometry, indices, etc.) - /// - MSDF CPU images - /// - Draw call metadata - /// - Active MainObject Index --> this is another state of the submit that we need to store - /// - /// The data is fully preprocessed and ready to be pushed to the GPU with no further transformation. - /// This enables efficient replays without traversing or re-generating scene content. - struct ReplayCache - { - std::vector drawCallsData; - ResourcesCollection resourcesCollection; - std::vector msdfImagesState; - std::unique_ptr imagesCache; - uint32_t activeMainObjectIndex = InvalidMainObjectIdx; - // TODO: non msdf general CPU Images - // TODO: Get total memory consumption for logging? + uint32_t index; }; - /// @brief Creates a snapshot of all currently staged CPU-side resourcesCollection for future replay or deferred submission. - /// - /// @warning This cache corresponds to a **single intended GPU submit**. - /// If your frame submission overflows into multiple submits due to staging memory limits or batching, - /// you are responsible for creating **multiple ReplayCache instances**, one per submit. - /// - /// @return A heap-allocated ReplayCache containing a copy of all staged CPU-side resourcesCollection and draw call data. - std::unique_ptr createReplayCache(); - - /// @brief Redirects all subsequent resource upload and getters to use an external ReplayCache. - /// - /// After calling this function, staging, resource getters, and upload mechanisms will pull data from the given ReplayCache - /// instead of the internal accumulation cache. - /// - /// User is responsible for management of cache and making sure it's alive in the ReplayCache scope - void setReplayCache(ReplayCache* cache); + SubmitFunc submitDraws; - /// @brief Reverts internal logic to use the default internal staging and resource accumulation cache. - /// Must be called once per corresponding `pushReplayCacheUse()`. - void unsetReplayCache(); - - uint64_t getImagesMemoryConsumption() const; + void finalizeMainObjectCopiesToGPU(SIntendedSubmitInfo& intendedNextSubmit); - struct UsageData - { - uint32_t lineStyleCount = 0u; - uint32_t dtmSettingsCount = 0u; - uint32_t customProjectionsCount = 0u; - uint32_t mainObjectCount = 0u; - uint32_t drawObjectCount = 0u; - uint32_t geometryBufferSize = 0u; - uint64_t bufferMemoryConsumption = 0ull; - uint64_t imageMemoryConsumption = 0ull; - - void add(const UsageData& other) - { - lineStyleCount += other.lineStyleCount; - dtmSettingsCount += other.dtmSettingsCount; - customProjectionsCount += other.customProjectionsCount; - mainObjectCount += other.mainObjectCount; - drawObjectCount += other.drawObjectCount; - geometryBufferSize += other.geometryBufferSize; - bufferMemoryConsumption = nbl::hlsl::max(bufferMemoryConsumption, other.bufferMemoryConsumption); - imageMemoryConsumption = nbl::hlsl::max(imageMemoryConsumption, other.imageMemoryConsumption); - } + void finalizeGeometryCopiesToGPU(SIntendedSubmitInfo& intendedNextSubmit); - std::string toString() const - { - std::ostringstream oss; - oss << "Usage Data:\n"; - oss << " lineStyles (Count): " << lineStyleCount << "\n"; - oss << " dtmSettings (Count): " << dtmSettingsCount << "\n"; - oss << " customProjections (Count): " << customProjectionsCount << "\n"; - oss << " mainObject (Count): " << mainObjectCount << "\n"; - oss << " drawObject (Count): " << drawObjectCount << "\n"; - oss << " geometryBufferSize (Bytes): " << geometryBufferSize << "\n"; - oss << " Max Buffer Memory Consumption (Bytes): " << bufferMemoryConsumption << "\n"; - oss << " Max Image Memory Consumption (Bytes):" << imageMemoryConsumption; - return oss.str(); - } - }; + void finalizeLineStyleCopiesToGPU(SIntendedSubmitInfo& intendedNextSubmit); + + void finalizeCustomClipProjectionCopiesToGPU(SIntendedSubmitInfo& intendedNextSubmit); + + void finalizeTextureCopies(SIntendedSubmitInfo& intendedNextSubmit); - UsageData getCurrentUsageData(); + // A hatch and a polyline are considered a "Main Object" which consists of smaller geometries such as beziers, lines, connectors, hatchBoxes + // If the whole polyline can't fit into memory for draw, then we submit the render of smaller geometries midway and continue + void submitCurrentObjectsAndReset(SIntendedSubmitInfo& intendedNextSubmit, uint32_t mainObjectIndex); -protected: + uint32_t addMainObject_Internal(const MainObject& mainObject); - SubmitFunc submitDraws; + uint32_t addLineStyle_Internal(const LineStyleInfo& lineStyleInfo); - /// @brief Records GPU copy commands for all staged buffer resourcesCollection into the active command buffer. - bool pushBufferUploads(SIntendedSubmitInfo& intendedNextSubmit, ResourcesCollection& resourcesCollection); + // Gets the current clip projection data (the top of stack) gpu addreess inside the geometryBuffer + // If it's been invalidated then it will request to upload again with a possible auto-submit on low geometry buffer memory. + uint64_t acquireCurrentClipProjectionAddress(SIntendedSubmitInfo& intendedNextSubmit); - /// @brief Records GPU copy commands for all staged msdf images into the active command buffer. - bool pushMSDFImagesUploads(SIntendedSubmitInfo& intendedNextSubmit, std::vector& msdfImagesState); - - /// @brief binds cached images into their correct descriptor set slot if not already resident. - bool updateDescriptorSetImageBindings(ImagesCache& imagesCache); + uint64_t addClipProjectionData_SubmitIfNeeded(const ClipProjectionData& clipProjectionData, SIntendedSubmitInfo& intendedNextSubmit); - /// @brief Records GPU copy commands for all staged images into the active command buffer. - bool pushStaticImagesUploads(SIntendedSubmitInfo& intendedNextSubmit, ImagesCache& imagesCache); - - /// @brief Handles eviction of images with conflicting memory regions or array indices in cache & replay mode. - /// - /// In cache & replay mode, image allocations bypass the standard arena allocator and are rebound - /// to their original GPU memory locations. Since we can't depend on the allocator to avoid conflicting memory location, - /// this function scans the image cache for potential overlaps with the given image and evicts any conflicting entries, submitting work if necessary. - /// - /// @param toInsertImageID Identifier of the image being inserted. - /// @param toInsertRecord Record describing the image and its intended memory placement. - /// @param intendedNextSubmit Reference to the intended GPU submit info; may be used if eviction requires submission. - /// @return true if something was evicted, false otherwise - bool evictConflictingImagesInCache_SubmitIfNeeded(image_id toInsertImageID, const CachedImageRecord& toInsertRecord, nbl::video::SIntendedSubmitInfo& intendedNextSubmit); - - /* - GeoreferencesImage Protected Functions: - */ - - /** - * @brief Ensures a GPU-resident georeferenced image exists in the cache, allocating resources if necessary. - * - * If the specified image ID is not already present in the cache, or if the cached version is incompatible - * with the requested parameters (e.g. extent, format, or type), this function allocates GPU memory, - * creates the image and its view, to be bound to a descriptor binding in the future. - * - * If the image already exists and matches the requested parameters, its usage metadata is updated. - * In either case, the cache is updated to reflect usage in the current frame. - * - * This function also handles automatic eviction of old images via an LRU policy when space is limited. - * - * @param imageID Unique identifier of the image to add or reuse. - * @param imageStreamingState Reference to the GeoreferencedImageStreamingState created or returned by `ensureGeoreferencedImageEntry` with same image_id. - * @param intendedNextSubmit Submit info object used to track resources pending GPU submission. - * - * @return true if the image was successfully cached and is ready for use; false if allocation failed. - */ - bool ensureGeoreferencedImageResources_AllocateIfNeeded(image_id imageID, nbl::core::smart_refctd_ptr&& imageStreamingState, SIntendedSubmitInfo& intendedNextSubmit); - - const size_t calculateRemainingResourcesSize() const; - - /// @brief Internal Function to call whenever we overflow when we can't fill all of mainObject's drawObjects - /// @param intendedNextSubmit - /// @param mainObjectIndex: function updates mainObjectIndex after submitting, clearing everything and acquiring mainObjectIndex again. - void submitCurrentDrawObjectsAndReset(SIntendedSubmitInfo& intendedNextSubmit, uint32_t& mainObjectIndex); - - // Gets resource index to the active linestyle data from the top of stack - // If it's been invalidated then it will request to add to resources again ( auto-submission happens If there is not enough memory to add again) - uint32_t acquireActiveLineStyleIndex_SubmitIfNeeded(SIntendedSubmitInfo& intendedNextSubmit); - - // Gets resource index to the active linestyle data from the top of stack - // If it's been invalidated then it will request to add to resources again ( auto-submission happens If there is not enough memory to add again) - uint32_t acquireActiveDTMSettingsIndex_SubmitIfNeeded(SIntendedSubmitInfo& intendedNextSubmit); + uint64_t addClipProjectionData_Internal(const ClipProjectionData& clipProjectionData); - // Gets resource index to the active projection data from the top of stack - // If it's been invalidated then it will request to add to resources again ( auto-submission happens If there is not enough memory to add again) - uint32_t acquireActiveCustomProjectionIndex_SubmitIfNeeded(SIntendedSubmitInfo& intendedNextSubmit); - - // Gets resource index to the active clip data from the top of stack - // If it's been invalidated then it will request to add to resources again ( auto-submission happens If there is not enough memory to add again) - uint32_t acquireActiveCustomClipRectIndex_SubmitIfNeeded(SIntendedSubmitInfo& intendedNextSubmit); - - // Gets resource index to the active main object data - // If it's been invalidated then it will request to add to resources again ( auto-submission happens If there is not enough memory to add again) - uint32_t acquireActiveMainObjectIndex_SubmitIfNeeded(SIntendedSubmitInfo& intendedNextSubmit); + static constexpr uint32_t getCageCountPerPolylineObject(ObjectType type) + { + if (type == ObjectType::LINE) + return 1u; + else if (type == ObjectType::QUAD_BEZIER) + return 3u; + return 0u; + }; - /// Attempts to add lineStyle to resources. If it fails to do, due to resource limitations, auto-submits and tries again. - uint32_t addLineStyle_SubmitIfNeeded(const LineStyleInfo& lineStyle, SIntendedSubmitInfo& intendedNextSubmit); - - /// Attempts to add dtmSettings to resources. If it fails to do, due to resource limitations, auto-submits and tries again. - uint32_t addDTMSettings_SubmitIfNeeded(const DTMSettingsInfo& dtmSettings, SIntendedSubmitInfo& intendedNextSubmit); - - /// Attempts to add custom projection to gpu resources. If it fails to do, due to resource limitations, auto-submits and tries again. - uint32_t addCustomProjection_SubmitIfNeeded(const float64_t3x3& projection, SIntendedSubmitInfo& intendedNextSubmit); - - /// Attempts to add custom clip to gpu resources. If it fails to do, due to resource limitations, auto-submits and tries again. - uint32_t addCustomClipRect_SubmitIfNeeded(const WorldClipRect& clipRect, SIntendedSubmitInfo& intendedNextSubmit); - - /// returns index to added LineStyleInfo, returns Invalid index if it exceeds resource limitations - uint32_t addLineStyle_Internal(const LineStyleInfo& lineStyleInfo); - - /// returns index to added DTMSettingsInfo, returns Invalid index if it exceeds resource limitations - uint32_t addDTMSettings_Internal(const DTMSettingsInfo& dtmSettings, SIntendedSubmitInfo& intendedNextSubmit); - - /** - * @brief Computes the final transformation matrix for fixed geometry rendering, - * considering any active custom projections and the transformation type. - * - * This function handles how a given transformation should be applied depending on the - * current transformation type and the presence of any active projection matrices. - * - * - If no active projection exists, the input transformation is returned unmodified. - * - * - If an active projection exists: - * - For TT_NORMAL, the input transformation is simply multiplied by the top of the projection stack. - * - For TT_FIXED_SCREENSPACE_SIZE, the input transformation is multiplied by the top of the projection stack, - * but the resulting scale is replaced with the screen-space scale from the original input `transformation`. - * - * @param transformation The input 3x3 transformation matrix to apply. - * @param transformationType The type of transformation to apply (e.g., TT_NORMAL or TT_FIXED_SCREENSPACE_SIZE). - * - */ - float64_t3x3 getFixedGeometryFinalTransformationMatrix(const float64_t3x3& transformation, TransformationType transformationType) const; - - /// Attempts to upload as many draw objects as possible within the given polyline section considering resource limitations void addPolylineObjects_Internal(const CPolylineBase& polyline, const CPolylineBase::SectionInfo& section, uint32_t& currentObjectInSection, uint32_t mainObjIdx); - - /// Attempts to upload as many draw objects as possible within the given polyline connectors considering resource limitations + void addPolylineConnectors_Internal(const CPolylineBase& polyline, uint32_t& currentPolylineConnectorObj, uint32_t mainObjIdx); - - /// Attempts to upload as many draw objects as possible within the given polyline section considering resource limitations + void addLines_Internal(const CPolylineBase& polyline, const CPolylineBase::SectionInfo& section, uint32_t& currentObjectInSection, uint32_t mainObjIdx); - - /// Attempts to upload as many draw objects as possible within the given polyline section considering resource limitations + void addQuadBeziers_Internal(const CPolylineBase& polyline, const CPolylineBase::SectionInfo& section, uint32_t& currentObjectInSection, uint32_t mainObjIdx); - - /// Attempts to upload as many draw objects as possible within the given hatch considering resource limitations + void addHatch_Internal(const Hatch& hatch, uint32_t& currentObjectInSection, uint32_t mainObjIndex); - /// Attempts to upload a single GlyphInfo considering resource limitations bool addFontGlyph_Internal(const GlyphInfo& glyphInfo, uint32_t mainObjIdx); - /// Attempts to upload a single GridDTMInfo considering resource limitations - bool addGridDTM_Internal(const GridDTMInfo& gridDTMInfo, uint32_t mainObjIdx); - /// Attempts to upload a single image object considering resource limitations (not accounting for the resource image added using ensureStaticImageAvailability function) - bool addImageObject_Internal(const ImageObjectInfo& imageObjectInfo, uint32_t mainObjIdx);; - - /// Attempts to upload a georeferenced image info considering resource limitations (not accounting for the resource image added using ensureStaticImageAvailability function) - bool addGeoreferencedImageInfo_Internal(const GeoreferencedImageInfo& georeferencedImageInfo, uint32_t mainObjIdx); - - uint32_t getImageIndexFromID(image_id imageID, const SIntendedSubmitInfo& intendedNextSubmit); - - /** - * @brief Evicts a GPU image and deallocates its associated descriptor and memory, flushing draws if needed. - * - * This function is called when an image must be removed from GPU memory (typically due to VRAM pressure). - * If the evicted image is scheduled to be used in the next draw submission, a flush is performed to avoid - * use-after-free issues. Otherwise, it proceeds with deallocation immediately. - * - * It prepares a cleanup object that ensures the memory range used by the image will be returned to the suballocator - * only after the GPU has finished using it, guarded by a semaphore wait. - * - * @param imageID The unique ID of the image being evicted. - * @param evicted A reference to the evicted image, containing metadata such as allocation offset, size, usage frame, etc. - * @param intendedNextSubmit Reference to the intended submit information. Used for synchronizing draw submission and safe deallocation. - * - * @warning Deallocation may use a conservative semaphore wait value if exact usage information is unavailable. [future todo: fix] - */ - void evictImage_SubmitIfNeeded(image_id imageID, const CachedImageRecord& evicted, SIntendedSubmitInfo& intendedNextSubmit); - - struct ImageAllocateResults - { - nbl::core::smart_refctd_ptr gpuImageView = nullptr; - uint64_t allocationOffset = ImagesMemorySubAllocator::InvalidAddress; - uint64_t allocationSize = 0ull; - bool isValid() const { return (gpuImageView && (allocationOffset != ImagesMemorySubAllocator::InvalidAddress)); } - }; - - /** - * @brief Attempts to create and allocate a GPU image and its view, with fallback eviction on failure. - * - * This function tries to create a GPU image using the specified creation parameters, allocate memory - * from the shared image memory arena, bind it to device-local memory, and create an associated image view. - * If memory allocation fails (e.g. due to VRAM exhaustion), the function will evict textures from the internal - * LRU cache and retry the operation until successful, or until only the currently-inserted image remains. - * - * This is primarily used by the draw resource filler to manage GPU image memory for streamed or cached images. - * - * @param imageParams Creation parameters for the image. Should match `nbl::asset::IImage::SCreationParams`. - * @param imageViewFormatOverride Specifies whether the image view format should differ from the image format. If set to asset::E_FORMAT_ET_COUNT, the image view uses the same format as the image - * @param intendedNextSubmit Reference to the current intended submit info. Used for synchronizing evictions. - * @param imageDebugName Debug name assigned to the image and its view for easier profiling/debugging. - * - * @return ImageAllocateResults A struct containing: - * - `allocationOffset`: Offset into the memory arena (or InvalidAddress on failure). - * - `allocationSize`: Size of the allocated memory region. - * - `gpuImageView`: The created GPU image view (nullptr if creation failed). - */ - ImageAllocateResults tryCreateAndAllocateImage_SubmitIfNeeded(const nbl::asset::IImage::SCreationParams& imageParams, - const asset::E_FORMAT imageViewFormatOverride, - nbl::video::SIntendedSubmitInfo& intendedNextSubmit, - std::string imageDebugName); - - /** - * @brief Used to implement both `drawHatch` and `drawFixedGeometryHatch` without exposing the transformation type parameter - */ - void drawHatch_impl( - const Hatch& hatch, - const float32_t4& color, - const HatchFillPattern fillPattern, - SIntendedSubmitInfo& intendedNextSubmit, - TransformationType transformationType = TransformationType::TT_NORMAL); - - void resetMainObjects() + void resetMainObjectCounters() { - resourcesCollection.mainObjects.vector.clear(); - activeMainObjectIndex = InvalidMainObjectIdx; + inMemMainObjectCount = 0u; + currentMainObjectCount = 0u; } - // these resources are data related to chunks of a whole mainObject - void resetDrawObjects() + void resetGeometryCounters() { - resourcesCollection.drawObjects.vector.clear(); - resourcesCollection.indexBuffer.vector.clear(); - resourcesCollection.geometryInfo.vector.clear(); - } + inMemDrawObjectCount = 0u; + currentDrawObjectCount = 0u; - void resetCustomProjections() - { - resourcesCollection.customProjections.vector.clear(); - - // Invalidate all the clip projection addresses because activeProjections buffer got reset - for (auto& addr : activeProjectionIndices) - addr = InvalidCustomProjectionIndex; - } + inMemGeometryBufferSize = 0u; + currentGeometryBufferSize = 0u; - void resetCustomClipRects() - { - resourcesCollection.customClipRects.vector.clear(); - - // Invalidate all the clip projection addresses because activeProjections buffer got reset - for (auto& addr : activeClipRectIndices) - addr = InvalidCustomClipRectIndex; + // Invalidate all the clip projection addresses because geometry buffer got reset + for (auto& clipProjAddr : clipProjectionAddresses) + clipProjAddr = InvalidClipProjectionAddress; } - void resetLineStyles() + void resetLineStyleCounters() { - resourcesCollection.lineStyles.vector.clear(); - activeLineStyleIndex = InvalidStyleIdx; + currentLineStylesCount = 0u; + inMemLineStylesCount = 0u; } - void resetDTMSettings() + MainObject* getMainObject(uint32_t idx) { - resourcesCollection.dtmSettings.vector.clear(); - activeDTMSettingsIndex = InvalidDTMSettingsIdx; + MainObject* mainObjsArray = reinterpret_cast(cpuDrawBuffers.mainObjectsBuffer->getPointer()); + return &mainObjsArray[idx]; } - + // MSDF Hashing and Caching Internal Functions + static constexpr uint64_t InvalidMSDFHash = std::numeric_limits::max(); enum class MSDFType : uint8_t { HATCH_FILL_PATTERN, FONT_GLYPH, }; - struct MSDFInputInfo - { - // It's a font glyph - MSDFInputInfo(core::blake3_hash_t fontFaceHash, uint32_t glyphIdx) - : type(MSDFType::FONT_GLYPH) - , faceHash(fontFaceHash) - , glyphIndex(glyphIdx) - { - computeBlake3Hash(); - } - - // It's a hatch fill pattern - MSDFInputInfo(HatchFillPattern fillPattern) - : type(MSDFType::HATCH_FILL_PATTERN) - , faceHash({}) - , fillPattern(fillPattern) - { - computeBlake3Hash(); - } - bool operator==(const MSDFInputInfo& rhs) const - { - return hash == rhs.hash && glyphIndex == rhs.glyphIndex && type == rhs.type; - } - - MSDFType type; - uint8_t pad[3u]; // 3 bytes pad - union - { - uint32_t glyphIndex; - HatchFillPattern fillPattern; - }; - static_assert(sizeof(uint32_t) == sizeof(HatchFillPattern)); - - core::blake3_hash_t faceHash = {}; - core::blake3_hash_t hash = {}; // actual hash, we will check in == operator - size_t lookupHash = 0ull; // for containers expecting size_t hash - - private: - - void computeBlake3Hash() - { - core::blake3_hasher hasher; - hasher.update(&type, sizeof(MSDFType)); - hasher.update(&glyphIndex, sizeof(uint32_t)); - hasher.update(&faceHash, sizeof(core::blake3_hash_t)); - hash = static_cast(hasher); - lookupHash = std::hash{}(hash); // hashing the hash :D - } + static msdf_hash hashFillPattern(HatchFillPattern fillPattern); - }; + static msdf_hash hashFontGlyph(size_t fontHash, uint32_t glyphIndex); - struct MSDFInputInfoHash { std::size_t operator()(const MSDFInputInfo& info) const { return info.lookupHash; } }; - struct MSDFReference { uint32_t alloc_idx; - uint64_t lastUsedFrameIndex; + uint64_t lastUsedSemaphoreValue; - MSDFReference(uint32_t alloc_idx, uint64_t semaphoreVal) : alloc_idx(alloc_idx), lastUsedFrameIndex(semaphoreVal) {} - MSDFReference(uint64_t currentFrameIndex) : MSDFReference(InvalidTextureIndex, currentFrameIndex) {} - MSDFReference() : MSDFReference(InvalidTextureIndex, ~0ull) {} + MSDFReference(uint32_t alloc_idx, uint64_t semaphoreVal) : alloc_idx(alloc_idx), lastUsedSemaphoreValue(semaphoreVal) {} + MSDFReference(uint64_t semaphoreVal) : MSDFReference(InvalidTextureIdx, semaphoreVal) {} + MSDFReference() : MSDFReference(InvalidTextureIdx, ~0ull) {} // In LRU Cache `insert` function, in case of cache hit, we need to assign semaphore value to MSDFReference without changing `alloc_idx` - inline MSDFReference& operator=(uint64_t currentFrameIndex) { lastUsedFrameIndex = currentFrameIndex; return *this; } + inline MSDFReference& operator=(uint64_t semamphoreVal) { lastUsedSemaphoreValue = semamphoreVal; return *this; } }; - uint32_t getMSDFIndexFromInputInfo(const MSDFInputInfo& msdfInfo, const SIntendedSubmitInfo& intendedNextSubmit); + uint32_t getMSDFTextureIndex(msdf_hash hash); - uint32_t addMSDFTexture(const MSDFInputInfo& msdfInput, core::smart_refctd_ptr&& cpuImage, SIntendedSubmitInfo& intendedNextSubmit); - - // Flushes Current Draw Call and adds to drawCalls - void flushDrawObjects(); - - // Logger - nbl::system::logger_opt_smart_ptr m_logger = nullptr; - - // FrameIndex used as a criteria for resource/image eviction in case of limitations - uint32_t currentFrameIndex = 0u; - - // Replay Cache override - ReplayCache* currentReplayCache = nullptr; - - // DrawCalls Data - uint64_t drawObjectsFlushedToDrawCalls = 0ull; - std::vector drawCalls; // either dtms or objects - - // ResourcesCollection and packed into GPUBuffer - ResourcesCollection resourcesCollection; - IDeviceMemoryAllocator::SAllocation buffersMemoryArena; - nbl::core::smart_refctd_ptr resourcesGPUBuffer; - size_t copiedResourcesSize; + uint32_t getTextureIndexFromHash(const msdf_hash msdfTexture, SIntendedSubmitInfo& intendedNextSubmit) + { + uint32_t textureIdx = InvalidTextureIdx; + if (msdfTexture != InvalidMSDFHash) + { + MSDFReference* tRef = textureLRUCache->get(msdfTexture); + if (tRef) + { + textureIdx = tRef->alloc_idx; + tRef->lastUsedSemaphoreValue = intendedNextSubmit.getFutureScratchSemaphore().value; // update this because the texture will get used on the next submit + } + } + return textureIdx; + } + uint32_t addMSDFTexture(core::smart_refctd_ptr textureBuffer, msdf_hash hash, SIntendedSubmitInfo& intendedNextSubmit); + + // Members + smart_refctd_ptr m_utilities; + IQueue* m_copyQueue; - smart_refctd_ptr m_device; - core::smart_refctd_ptr m_bufferUploadUtils; - core::smart_refctd_ptr m_imageUploadUtils; + uint32_t maxIndexCount; - IQueue* m_copyQueue; + uint32_t inMemMainObjectCount = 0u; + uint32_t currentMainObjectCount = 0u; + uint32_t maxMainObjects = 0u; - // Active Resources we need to keep track of and push to resources buffer if needed. - LineStyleInfo activeLineStyle; - uint32_t activeLineStyleIndex = InvalidStyleIdx; + uint32_t inMemDrawObjectCount = 0u; + uint32_t currentDrawObjectCount = 0u; + uint32_t maxDrawObjects = 0u; - DTMSettingsInfo activeDTMSettings; - uint32_t activeDTMSettingsIndex = InvalidDTMSettingsIdx; + uint64_t inMemGeometryBufferSize = 0u; + uint64_t currentGeometryBufferSize = 0u; + uint64_t maxGeometryBufferSize = 0u; - MainObjectType activeMainObjectType; - TransformationType activeMainObjectTransformationType; + uint32_t inMemLineStylesCount = 0u; + uint32_t currentLineStylesCount = 0u; + uint32_t maxLineStyles = 0u; - uint32_t activeMainObjectIndex = InvalidMainObjectIdx; + uint64_t geometryBufferAddress = 0u; // Actual BDA offset 0 of the gpu buffer - // The ClipRects & Projections are stack, because user can push/pop ClipRects & Projections in any order - std::deque activeProjections; // stack of projections stored so we can resubmit them if geometry buffer got reset. - std::deque activeProjectionIndices; // stack of projection gpu addresses in geometry buffer. to keep track of them in push/pops - - std::deque activeClipRects; // stack of clips stored so we can resubmit them if geometry buffer got reset. - std::deque activeClipRectIndices; // stack of clips gpu addresses in geometry buffer. to keep track of them in push/pops + std::deque clipProjections; // stack of clip projectios stored so we can resubmit them if geometry buffer got reset. + std::deque clipProjectionAddresses; // stack of clip projection gpu addresses in geometry buffer. to keep track of them in push/pops + // MSDF GetGlyphMSDFTextureFunc getGlyphMSDF; GetHatchFillPatternMSDFTextureFunc getHatchFillPatternMSDF; - using MSDFsLRUCache = core::ResizableLRUCache; + using MSDFsLRUCache = core::LRUCache; smart_refctd_ptr msdfTextureArray; // view to the resource holding all the msdfs in it's layers smart_refctd_ptr msdfTextureArrayIndexAllocator; - std::unique_ptr msdfLRUCache; // LRU Cache to evict Least Recently Used in case of overflow + std::set msdfTextureArrayIndicesUsed = {}; // indices in the msdf texture array allocator that have been used in the current frame // TODO: make this a dynamic bitset + std::vector textureCopies = {}; // queued up texture copies, @Lucas change to deque if possible + std::unique_ptr textureLRUCache; // LRU Cache to evict Least Recently Used in case of overflow + static constexpr asset::E_FORMAT MSDFTextureFormat = asset::E_FORMAT::EF_R8G8B8_SNORM; - std::vector msdfImagesState = {}; // cached cpu imaged + their status, size equals to LRUCache size - static constexpr asset::E_FORMAT MSDFTextureFormat = asset::E_FORMAT::EF_R8G8B8A8_SNORM; bool m_hasInitializedMSDFTextureArrays = false; - - // Images: - core::smart_refctd_ptr imageLoader; - // A. Image Cache - std::unique_ptr imagesCache; - // B. GPUImages Memory Arena + AddressAllocator - IDeviceMemoryAllocator::SAllocation imagesMemoryArena; - smart_refctd_ptr imagesMemorySubAllocator; - // C. Images Descriptor Set Allocation/Deallocation - uint32_t imagesArrayBinding = 0u; - smart_refctd_ptr imagesDescriptorIndexAllocator; - // Tracks descriptor array indices that have been logically deallocated independant of the `imagesDescriptorSetAllocator` but may still be in use by the GPU. - // Notes: If `imagesDescriptorIndexAllocator` could give us functionality to force allocate and exact index, that would allow us to replay the cache perfectly - // remove the variable below and only rely on the `imagesDescriptorIndexAllocator` to synchronize accesses to descriptor sets for us. but unfortuantely it doesn't have that functionality yet. - std::unordered_map deferredDescriptorIndexDeallocations; - // D. Queued Up Copies/Futures for Streamed Images - std::unordered_map> streamedImageCopies; -}; \ No newline at end of file +}; + diff --git a/62_CAD/Hatch.cpp b/62_CAD/Hatch.cpp index b383e7d81..8bd0b8d30 100644 --- a/62_CAD/Hatch.cpp +++ b/62_CAD/Hatch.cpp @@ -16,7 +16,7 @@ bool Hatch::Segment::isStraightLineConstantMajor() const p1 = originalBezier->P1[major], p2 = originalBezier->P2[major]; //assert(p0 <= p1 && p1 <= p2); (PRECISION ISSUES ARISE ONCE MORE) - return abs(p1 - p0) <= core::exp2(-24.0) && abs(p2 - p0) <= hlsl::exp(-24.0f); + return abs(p1 - p0) <= core::exp2(-24.0) && abs(p2 - p0) <= exp(-24); } std::array Hatch::Segment::intersect(const Segment& other) const @@ -133,8 +133,9 @@ std::array Hatch::Segment::intersect(const Segment& other) const return result; } -Hatch::Hatch(std::span lines, const MajorAxis majorAxis, nbl::system::logger_opt_smart_ptr logger, int32_t* debugStepPtr, const std::function& debugOutput) +Hatch::Hatch(std::span lines, const MajorAxis majorAxis, int32_t* debugStepPtr, const std::function& debugOutput) { + intersectionAmounts = std::vector(); // this threshsold is used to decide when to consider minor position to be // the same and check tangents because intersection algorithms has rounding // errors @@ -274,7 +275,6 @@ Hatch::Hatch(std::span lines, const MajorAxis majorAxis, nbl::system: } } } - for (uint32_t bezierIdx = 0; bezierIdx < beziers.size(); bezierIdx++) { auto hatchBezier = &beziers[bezierIdx]; @@ -284,12 +284,6 @@ Hatch::Hatch(std::span lines, const MajorAxis majorAxis, nbl::system: segment.t_end = 1.0; segments.push_back(segment); } - - if (segments.empty()) - { - logger.log("Empty Polylines with no segments were fed into the Hatch construction.", nbl::system::ILogger::ELL_WARNING); - return; - } std::sort(segments.begin(), segments.end(), [&](const Segment& a, const Segment& b) { return a.originalBezier->P0[major] > b.originalBezier->P0[major]; }); for (Segment& segment : segments) @@ -430,6 +424,7 @@ Hatch::Hatch(std::span lines, const MajorAxis majorAxis, nbl::system: { if (entry.isStraightLineConstantMajor()) return; + intersectionAmounts.push_back(activeCandidates.size()); // Look for intersections among active candidates // this is a little O(n^2) but only in the `n=candidates.size()` for (const auto& segment : activeCandidates) @@ -464,6 +459,7 @@ Hatch::Hatch(std::span lines, const MajorAxis majorAxis, nbl::system: activeCandidates.push_back(entry); }; + double lastMajor = starts.top().originalBezier->evaluate(starts.top().t_start)[major]; while (lastMajor!=maxMajor) { @@ -472,16 +468,10 @@ Hatch::Hatch(std::span lines, const MajorAxis majorAxis, nbl::system: break; bool isCurrentDebugStep = step == debugStep; #endif - + double newMajor; bool addStartSegmentToCandidates = false; - if (ends.empty()) - { - logger.log("Hatch Creation Failure: `ends` stack is empty in the main loop", nbl::system::ILogger::ELL_ERROR); - _NBL_DEBUG_BREAK_IF(true); // This shouldn't happen, TODO: LOG - break; - } const double maxMajorEnds = ends.top(); const Segment nextStartEvent = starts.empty() ? Segment() : starts.top(); @@ -489,6 +479,7 @@ Hatch::Hatch(std::span lines, const MajorAxis majorAxis, nbl::system: // We check which event, within start, end and intersection events have the smallest // major coordinate at this point + auto intersectionVisit = [&]() { const double newMajor = intersections.top(); @@ -514,6 +505,7 @@ Hatch::Hatch(std::span lines, const MajorAxis majorAxis, nbl::system: if (debugOutput && isCurrentDebugStep) drawDebugLine(float64_t2(-1000.0, newMajor), float64_t2(1000.0, newMajor), float32_t4(0.0, 0.8, 0.0, 1.0)); #endif + //std::cout << "Start event at " << newMajor << "\n"; } // (intersection event) else newMajor = intersectionVisit(); @@ -522,9 +514,9 @@ Hatch::Hatch(std::span lines, const MajorAxis majorAxis, nbl::system: // (intersection event) else if (!intersections.empty() && intersections.top() < maxMajorEnds) newMajor = intersectionVisit(); + // (end event) else { - // (end event) newMajor = maxMajorEnds; ends.pop(); #ifdef DEBUG_HATCH_VISUALLY @@ -534,111 +526,105 @@ Hatch::Hatch(std::span lines, const MajorAxis majorAxis, nbl::system: //std::cout << "End event at " << newMajor << "\n"; } // spawn quads for the previous iterations if we advanced - + // printf(std::format("New major: {} Last major: {}\n", newMajor, lastMajor).c_str()); if (newMajor > lastMajor) { - const auto candidatesSize = std::distance(activeCandidates.begin(),activeCandidates.end()); - // Because n4ce works on loops, this must be `true` in almost every case, but can fail at times, because we skip adding beziers (lines) almost constant in major direction - if (candidatesSize % 2u == 0u) - { #ifdef DEBUG_HATCH_VISUALLY - if (debugOutput && isCurrentDebugStep) - drawDebugLine(float64_t2(-1000.0, lastMajor), float64_t2(1000.0, lastMajor), float32_t4(0.1, 0.1, 0.0, 0.5)); + if (debugOutput && isCurrentDebugStep) + drawDebugLine(float64_t2(-1000.0, lastMajor), float64_t2(1000.0, lastMajor), float32_t4(0.1, 0.1, 0.0, 0.5)); #endif - // trim - if ((candidatesSize % 2u) != 0u) - { - logger.log("Hatch Creation Failure: candidatesSize is odd", nbl::system::ILogger::ELL_ERROR); - _NBL_DEBUG_BREAK_IF(true); // input polyline/polygon - } + // trim + const auto candidatesSize = std::distance(activeCandidates.begin(),activeCandidates.end()); + //std::cout << "Candidates size: " << candidatesSize << "\n"; + // because n4ce works on loops, this must be true + _NBL_DEBUG_BREAK_IF((candidatesSize % 2u)!=0u); // input polyline/polygon does not meet requirements to construct and XOR hatch #ifdef DEBUG_HATCH_VISUALLY - if (candidatesSize % 2u == 1u) + if (candidatesSize % 2u == 1u) + { + for (uint32_t i = 0u; i < candidatesSize; i++) { - for (uint32_t i = 0u; i < candidatesSize; i++) - { - const Segment& item = activeCandidates[i]; - auto curveMinEnd = intersectOrtho(*item.originalBezier, newMajor, major); - auto splitCurveMin = *item.originalBezier; - splitCurveMin.splitCurveFromMinToMax(item.t_start, core::isnan(curveMinEnd) ? 1.0 : curveMinEnd); + const Segment& item = activeCandidates[i]; + auto curveMinEnd = intersectOrtho(*item.originalBezier, newMajor, major); + auto splitCurveMin = *item.originalBezier; + splitCurveMin.splitCurveFromMinToMax(item.t_start, core::isnan(curveMinEnd) ? 1.0 : curveMinEnd); - drawDebugBezier(splitCurveMin, (i == candidatesSize - 1) ? float32_t4(0.0, 0.0, 1.0, 1.0) : float32_t4(1.0, 0.0, 0.0, 1.0)); - if (i == candidatesSize - 1) - { - printf(std::format("problematic guy: ({}, {}), ({}, {}), ({}, {})", - splitCurveMin.P0.x, splitCurveMin.P0.y, - splitCurveMin.P1.x, splitCurveMin.P1.y, - splitCurveMin.P2.x, splitCurveMin.P2.y - ).c_str()); - } + drawDebugBezier(splitCurveMin, (i == candidatesSize - 1) ? float32_t4(0.0, 0.0, 1.0, 1.0) : float32_t4(1.0, 0.0, 0.0, 1.0)); + if (i == candidatesSize - 1) + { + printf(std::format("problematic guy: ({}, {}), ({}, {}), ({}, {})", + splitCurveMin.P0.x, splitCurveMin.P0.y, + splitCurveMin.P1.x, splitCurveMin.P1.y, + splitCurveMin.P2.x, splitCurveMin.P2.y + ).c_str()); } } + } #endif - for (auto i = 0u; i < (candidatesSize / 2) * 2;) - { - const Segment& left = activeCandidates[i++]; - const Segment& right = activeCandidates[i++]; + for (auto i=0u; i< (candidatesSize / 2) * 2;) + { + const Segment& left = activeCandidates[i++]; + const Segment& right = activeCandidates[i++]; - CurveHatchBox curveBox; + CurveHatchBox curveBox; - // Due to precision, if the curve is right at the end, intersectOrtho may return nan - auto curveMinEnd = intersectOrtho(*left.originalBezier, newMajor, major); - auto curveMaxEnd = intersectOrtho(*right.originalBezier, newMajor, major); + // Due to precision, if the curve is right at the end, intersectOrtho may return nan + auto curveMinEnd = intersectOrtho(*left.originalBezier, newMajor, major); + auto curveMaxEnd = intersectOrtho(*right.originalBezier, newMajor, major); - auto splitCurveMin = *left.originalBezier; - splitCurveMin.splitFromMinToMax(left.t_start, core::isnan(curveMinEnd) ? 1.0 : curveMinEnd); - auto splitCurveMax = *right.originalBezier; - splitCurveMax.splitFromMinToMax(right.t_start, core::isnan(curveMaxEnd) ? 1.0 : curveMaxEnd); + auto splitCurveMin = *left.originalBezier; + splitCurveMin.splitFromMinToMax(left.t_start, core::isnan(curveMinEnd) ? 1.0 : curveMinEnd); + auto splitCurveMax = *right.originalBezier; + splitCurveMax.splitFromMinToMax(right.t_start, core::isnan(curveMaxEnd) ? 1.0 : curveMaxEnd); - assert(splitCurveMin.evaluate(0.0)[major] <= splitCurveMin.evaluate(1.0)[major]); - assert(splitCurveMax.evaluate(0.0)[major] <= splitCurveMax.evaluate(1.0)[major]); + assert(splitCurveMin.evaluate(0.0)[major] <= splitCurveMin.evaluate(1.0)[major]); + assert(splitCurveMax.evaluate(0.0)[major] <= splitCurveMax.evaluate(1.0)[major]); - auto curveMinAabb = getBezierBoundingBoxMinor(splitCurveMin); - auto curveMaxAabb = getBezierBoundingBoxMinor(splitCurveMax); - curveBox.aabbMin = float64_t2(std::min(curveMinAabb.first.x, curveMaxAabb.first.x), lastMajor); - curveBox.aabbMax = float64_t2(std::max(curveMinAabb.second.x, curveMaxAabb.second.x), newMajor); + auto curveMinAabb = getBezierBoundingBoxMinor(splitCurveMin); + auto curveMaxAabb = getBezierBoundingBoxMinor(splitCurveMax); + curveBox.aabbMin = float64_t2(std::min(curveMinAabb.first.x, curveMaxAabb.first.x), lastMajor); + curveBox.aabbMax = float64_t2(std::max(curveMinAabb.second.x, curveMaxAabb.second.x), newMajor); #ifdef DEBUG_HATCH_VISUALLY - if (isCurrentDebugStep) - { - drawDebugBezier(splitCurveMin, float64_t4(1.0, 0.0, 0.0, 1.0)); - drawDebugBezier(splitCurveMax, float64_t4(0.0, 1.0, 0.0, 1.0)); - - printf(std::format("AABB min: {}, {} max: {}, {} curve min: ({}, {}), ({}, {}), ({}, {}) curve max ({}, {}), ({}, {}), ({}, {})\n", - curveBox.aabbMin.x, curveBox.aabbMin.y, curveBox.aabbMax.x, curveBox.aabbMax.y, - - splitCurveMin.P0.x, splitCurveMin.P0.y, - splitCurveMin.P1.x, splitCurveMin.P1.y, - splitCurveMin.P2.x, splitCurveMin.P2.y, - splitCurveMax.P0.x, splitCurveMax.P0.y, - splitCurveMax.P1.x, splitCurveMax.P1.y, - splitCurveMax.P2.x, splitCurveMax.P2.y - ).c_str()); - } + if (isCurrentDebugStep) + { + drawDebugBezier(splitCurveMin, float64_t4(1.0, 0.0, 0.0, 1.0)); + drawDebugBezier(splitCurveMax, float64_t4(0.0, 1.0, 0.0, 1.0)); + + printf(std::format("AABB min: {}, {} max: {}, {} curve min: ({}, {}), ({}, {}), ({}, {}) curve max ({}, {}), ({}, {}), ({}, {})\n", + curveBox.aabbMin.x, curveBox.aabbMin.y, curveBox.aabbMax.x, curveBox.aabbMax.y, + + splitCurveMin.P0.x, splitCurveMin.P0.y, + splitCurveMin.P1.x, splitCurveMin.P1.y, + splitCurveMin.P2.x, splitCurveMin.P2.y, + splitCurveMax.P0.x, splitCurveMax.P0.y, + splitCurveMax.P1.x, splitCurveMax.P1.y, + splitCurveMax.P2.x, splitCurveMax.P2.y + ).c_str()); + } #endif - // Transform curves into AABB UV space and turn them into quadratic coefficients - // so we wont need to convert here - auto transformCurves = [](Hatch::QuadraticBezier bezier, float64_t2 aabbMin, float64_t2 aabbMax, float32_t2* output) { - auto rcpAabbExtents = float64_t2(1.0, 1.0) / (aabbMax - aabbMin); - auto transformedBezier = QuadraticBezier::construct( - (bezier.P0 - aabbMin) * rcpAabbExtents, - (bezier.P1 - aabbMin) * rcpAabbExtents, - (bezier.P2 - aabbMin) * rcpAabbExtents - ); - auto quadratic = QuadraticCurve::constructFromBezier(transformedBezier); - - if (isLineSegment(transformedBezier)) - quadratic.A = float64_t2(0.0); - - output[0] = (quadratic.A); - output[1] = (quadratic.B); - output[2] = (quadratic.C); - }; - transformCurves(splitCurveMin, curveBox.aabbMin, curveBox.aabbMax, &curveBox.curveMin[0]); - transformCurves(splitCurveMax, curveBox.aabbMin, curveBox.aabbMax, &curveBox.curveMax[0]); + // Transform curves into AABB UV space and turn them into quadratic coefficients + // so we wont need to convert here + auto transformCurves = [](Hatch::QuadraticBezier bezier, float64_t2 aabbMin, float64_t2 aabbMax, float32_t2* output) { + auto rcpAabbExtents = float64_t2(1.0, 1.0) / (aabbMax - aabbMin); + auto transformedBezier = QuadraticBezier::construct( + (bezier.P0 - aabbMin) * rcpAabbExtents, + (bezier.P1 - aabbMin) * rcpAabbExtents, + (bezier.P2 - aabbMin) * rcpAabbExtents + ); + auto quadratic = QuadraticCurve::constructFromBezier(transformedBezier); + + if (isLineSegment(transformedBezier)) + quadratic.A = float64_t2(0.0); + + output[0] = (quadratic.A); + output[1] = (quadratic.B); + output[2] = (quadratic.C); + }; + transformCurves(splitCurveMin, curveBox.aabbMin, curveBox.aabbMax, &curveBox.curveMin[0]); + transformCurves(splitCurveMax, curveBox.aabbMin, curveBox.aabbMax, &curveBox.curveMax[0]); - hatchBoxes.push_back(curveBox); - } + hatchBoxes.push_back(curveBox); } // advance and trim all of the beziers in the candidate set @@ -667,23 +653,20 @@ Hatch::Hatch(std::span lines, const MajorAxis majorAxis, nbl::system: const auto newSize = std::distance(activeCandidates.begin(), oit); activeCandidates.resize(newSize); } - // If we had a start event, we need to add the candidate if (addStartSegmentToCandidates) { addToCandidateSet(nextStartEvent); } - // We'll need to sort if we had a start event and added to the candidate set // or if we have advanced our candidate set if (addStartSegmentToCandidates || newMajor > lastMajor) { std::sort(activeCandidates.begin(), activeCandidates.end(), candidateComparator); + if (newMajor > lastMajor) + lastMajor = newMajor; } - if (newMajor > lastMajor) - lastMajor = newMajor; - #ifdef DEBUG_HATCH_VISUALLY step++; #endif @@ -812,7 +795,7 @@ static constexpr float64_t FillPatternShapeExtent = 32.0; void line(std::vector& polylines, float64_t2 begin, float64_t2 end) { - std::array points = { + std::vector points = { begin, end }; CPolyline polyline; @@ -846,23 +829,15 @@ void checkered(std::vector& polylines, const float64_t2& offset) float64_t2(0.0, 1.0), }; { - std::array points; - auto i = 0u; - for (const auto& p : squarePointsCW) - { - points[i] = p * FillPatternShapeExtent + offset; - i++; - } + std::vector points; + points.reserve(squarePointsCW.size()); + for (const auto& p : squarePointsCW) points.push_back(p * FillPatternShapeExtent + offset); polyline.addLinePoints(points); } { - std::array points; - auto i = 0u; - for (const auto& p : squarePointsCW) - { - points[i] = (p + float64_t2(0.5, -0.5)) * FillPatternShapeExtent + offset; - i++; - } + std::vector points; + points.reserve(squarePointsCW.size()); + for (const auto& p : squarePointsCW) points.push_back((p + float64_t2(0.5, -0.5)) * FillPatternShapeExtent + offset); polyline.addLinePoints(points); } polylines.push_back(std::move(polyline)); @@ -893,24 +868,16 @@ void diamonds(std::vector& polylines, const float64_t2& offset) // Outer { - std::array points; - auto i = 0u; - for (const auto& p : diamondPointsCW) - { - points[i] = p * outerSize + origin; - i++; - } + std::vector points; + points.reserve(diamondPointsCW.size()); + for (const auto& p : diamondPointsCW) points.push_back(p * outerSize + origin); polyline.addLinePoints(points); } // Inner { - std::array points; - auto i = 0u; - for (const auto& p : diamondPointsCCW) - { - points[i] = p * innerSize + origin; - i++; - } + std::vector points; + points.reserve(diamondPointsCCW.size()); + for (const auto& p : diamondPointsCCW) points.push_back(p * innerSize + origin); polyline.addLinePoints(points); } polylines.push_back(std::move(polyline)); @@ -931,13 +898,9 @@ void crossHatch(std::vector& polylines, const float64_t2& offset) float64_t2(0.375, 0.0), }; { - std::array points; - auto i = 0u; - for (const auto& p : outerPointsCW) - { - points[i] = p * FillPatternShapeExtent + offset; - i++; - } + std::vector points; + points.reserve(outerPointsCW.size()); + for (const auto& p : outerPointsCW) points.push_back(p * FillPatternShapeExtent + offset); polyline.addLinePoints(points); } @@ -950,13 +913,9 @@ void crossHatch(std::vector& polylines, const float64_t2& offset) }; { float64_t2 origin = float64_t2(FillPatternShapeExtent/2.0, FillPatternShapeExtent/2.0) + offset; - std::array points; - auto i = 0u; - for (const auto& p : diamondPointsCCW) - { - points[i] = p * 0.75 * FillPatternShapeExtent + origin; - i++; - } + std::vector points; + points.reserve(diamondPointsCCW.size()); + for (const auto& p : diamondPointsCCW) points.push_back(p * 0.75 * FillPatternShapeExtent + origin); polyline.addLinePoints(points); } polylines.push_back(std::move(polyline)); @@ -972,7 +931,7 @@ void hatch(std::vector& polylines, const float64_t2& offset) { float64_t2 radiusOffsetTL = float64_t2(+lineDiameter / 2.0, +lineDiameter / 2.0) * FillPatternShapeExtent / 8.0; float64_t2 radiusOffsetBL = float64_t2(-lineDiameter / 2.0, -lineDiameter / 2.0) * FillPatternShapeExtent / 8.0; - std::array points = { + std::vector points = { basePt0 + radiusOffsetTL, basePt0 + radiusOffsetBL, // 0 basePt1 + radiusOffsetBL, // 1 @@ -1076,7 +1035,7 @@ void reverseHatch(std::vector& polylines, const float64_t2& offset) { float64_t2 radiusOffsetTL = float64_t2(-lineDiameter / 2.0, +lineDiameter / 2.0) * FillPatternShapeExtent / 8.0; float64_t2 radiusOffsetBL = float64_t2(+lineDiameter / 2.0, -lineDiameter / 2.0) * FillPatternShapeExtent / 8.0; - std::array points = { + std::vector points = { basePt0 + radiusOffsetTL, basePt1 + radiusOffsetTL, // 0 basePt1 + radiusOffsetBL, // 1 @@ -1180,13 +1139,12 @@ void lightShaded(std::vector& polylines, const float64_t2& offset) void shaded(std::vector& polylines, const float64_t2& offset) { - float64_t2 size = float64_t2(1.0, 1.0)/8.0 * FillPatternShapeExtent; for (uint32_t x = 0; x < 8; x++) { for (uint32_t y = 0; y < 8; y++) { if (x % 2 != y % 2) - square(polylines, float64_t2((double)x, (double)y)/8.0 * FillPatternShapeExtent + offset, size); + square(polylines, float64_t2((double)x, (double)y)/8.0 * FillPatternShapeExtent + offset); } } @@ -1284,12 +1242,7 @@ core::smart_refctd_ptr Hatch::generateHatchFillPatternMSDF(nbl float scaleY = (1.0 / float(FillPatternShapeExtent)) * float(msdfExtents.y); auto bufferSize = msdfExtents.x * msdfExtents.y * sizeof(uint8_t) * 4; - - ICPUBuffer::SCreationParams bparams; - bparams.size = bufferSize; - - auto buffer = ICPUBuffer::create(std::move(bparams)); - + auto buffer = core::make_smart_refctd_ptr(bufferSize); size_t bufferOffset = 0ull; textRenderer->generateShapeMSDF(buffer.get(), &bufferOffset, glyph, MSDFPixelRange, msdfExtents, float32_t2(scaleX, scaleY), float32_t2(0, 0)); assert(bufferOffset == bufferSize); diff --git a/62_CAD/Hatch.h b/62_CAD/Hatch.h index 37d3c5ed1..3bcd72962 100644 --- a/62_CAD/Hatch.h +++ b/62_CAD/Hatch.h @@ -76,9 +76,7 @@ class Hatch bool isStraightLineConstantMajor() const; }; - Hatch() = default; - - Hatch(std::span lines, const MajorAxis majorAxis, nbl::system::logger_opt_smart_ptr logger = nullptr, int32_t* debugStep = nullptr, const std::function& debugOutput = {}); + Hatch(std::span lines, const MajorAxis majorAxis, int32_t* debugStep = nullptr, const std::function& debugOutput = {}); // (temporary) Hatch(std::vector&& in_hatchBoxes) : @@ -89,10 +87,12 @@ class Hatch const CurveHatchBox& getHatchBox(uint32_t idx) const { return hatchBoxes[idx]; } uint32_t getHatchBoxCount() const { return hatchBoxes.size(); } + std::vector intersectionAmounts; + // Generate Fill Pattern static core::smart_refctd_ptr generateHatchFillPatternMSDF(nbl::ext::TextRendering::TextRenderer* textRenderer, HatchFillPattern fillPattern, uint32_t2 msdfExtents); private: - std::vector hatchBoxes = {}; + std::vector hatchBoxes; }; diff --git a/62_CAD/Images.cpp b/62_CAD/Images.cpp deleted file mode 100644 index c444bd453..000000000 --- a/62_CAD/Images.cpp +++ /dev/null @@ -1,398 +0,0 @@ -#include "Images.h" - -using namespace nbl::hlsl; - -ImageCleanup::ImageCleanup() - : imagesMemorySuballocator(nullptr) - , addr(ImagesMemorySubAllocator::InvalidAddress) - , size(0ull) -{ -} - -ImageCleanup::~ImageCleanup() -{ - // printf(std::format("Actual Eviction size={}, offset={} \n", size, addr).c_str()); - if (imagesMemorySuballocator && addr != ImagesMemorySubAllocator::InvalidAddress) - imagesMemorySuballocator->deallocate(addr, size); -} - -bool GeoreferencedImageStreamingState::init(const OrientedBoundingBox2D& worldspaceOBB, const uint32_t2 fullResImageExtents, const asset::E_FORMAT sourceImageFormat, const std::filesystem::path& storagePath) -{ - this->worldspaceOBB = std::move(worldspaceOBB); - this->fullResImageExtents = fullResImageExtents; - this->sourceImageFormat = sourceImageFormat; - this->storagePath = storagePath; - // 1. Get the displacement (will be an offset vector in world coords and world units) from the `topLeft` corner of the image to the point - // 2. Transform this displacement vector into the coordinates in the basis {dirU, dirV} (worldspace vectors that span the sides of the image). - // The composition of these matrices therefore transforms any point in worldspace into uv coordinates in imagespace - // To reduce code complexity, instead of computing the product of these matrices, since the first is a pure displacement matrix - // (non-homogenous 2x2 upper left is identity matrix) and the other is a pure rotation matrix (2x2) we can just put them together - // by putting the rotation in the upper left 2x2 of the result and the post-rotated displacement in the upper right 2x1. - // The result is also 2x3 and not 3x3 because we can drop he homogenous since the displacement yields a vector - - // 2. Change of Basis. Since {dirU, dirV} are orthogonal, the matrix to change from world coords to `span{dirU, dirV}` coords has a quite nice expression - // Non-uniform scaling doesn't affect this, but this has to change if we allow for shearing (basis vectors stop being orthogonal) - const float64_t2 dirU = this->worldspaceOBB.dirU; - const float64_t2 dirV = float64_t2(dirU.y, -dirU.x) * float64_t(this->worldspaceOBB.aspectRatio); - const float64_t dirULengthSquared = nbl::hlsl::dot(dirU, dirU); - const float64_t dirVLengthSquared = nbl::hlsl::dot(dirV, dirV); - const float64_t2 firstRow = dirU / dirULengthSquared; - const float64_t2 secondRow = dirV / dirVLengthSquared; - - const float64_t2 displacement = -(this->worldspaceOBB.topLeft); - // This is the same as multiplying the change of basis matrix by the displacement vector - const float64_t postRotatedShiftX = nbl::hlsl::dot(firstRow, displacement); - const float64_t postRotatedShiftY = nbl::hlsl::dot(secondRow, displacement); - - // Put them all together - this->worldToUV = float64_t2x3(firstRow.x, firstRow.y, postRotatedShiftX, secondRow.x, secondRow.y, postRotatedShiftY); - - // Also set the maxMipLevel - to keep stuff simple, we don't consider having less than one tile per dimension - // If you're zoomed out enough then at that point the whole image is just sampled as one tile along that dimension - // In pathological cases, such as images that are way bigger on one side than the other, this could cause aliasing and slow down sampling if zoomed out too much. - // If we were ever to observe such pathological cases, then maybe we should consider doing something else here. For example, making the loader able to handle different tile lengths per dimension - // (so for example a 128x64 tile) but again for now it should be left as-is. - uint32_t2 maxMipLevels = nbl::hlsl::findMSB(nbl::hlsl::roundUpToPoT(this->fullResImageExtents / GeoreferencedImageTileSize)); - this->maxMipLevel = nbl::hlsl::min(maxMipLevels.x, maxMipLevels.y); - - this->fullImageTileLength = (this->fullResImageExtents - 1u) / GeoreferencedImageTileSize + 1u; - - return true; -} - -void GeoreferencedImageStreamingState::updateStreamingStateForViewport(const uint32_t2 viewportExtent, const float64_t3x3& ndcToWorldMat) -{ - currentViewportTileRange = computeViewportTileRange(viewportExtent, ndcToWorldMat); - // Slide or remap the current mapped region to ensure the viewport falls inside it - ensureMappedRegionCoversViewport(currentViewportTileRange); - - const uint32_t2 lastTileIndex = getLastTileIndex(currentViewportTileRange.baseMipLevel); - const uint32_t2 lastTileSampligOffsetMip0 = (lastTileIndex * GeoreferencedImageTileSize) << currentViewportTileRange.baseMipLevel; - lastTileSamplingExtent = fullResImageExtents - lastTileSampligOffsetMip0; - const uint32_t2 lastTileTargetExtentMip1 = lastTileSamplingExtent >> (currentViewportTileRange.baseMipLevel + 1); - lastTileTargetExtent = lastTileTargetExtentMip1 << 1u; -} - -core::vector GeoreferencedImageStreamingState::tilesToLoad() const -{ - core::vector retVal; - for (uint32_t tileY = currentViewportTileRange.topLeftTile.y; tileY <= currentViewportTileRange.bottomRightTile.y; tileY++) - for (uint32_t tileX = currentViewportTileRange.topLeftTile.x; tileX <= currentViewportTileRange.bottomRightTile.x; tileX++) - { - uint32_t2 imageTileIndex = uint32_t2(tileX, tileY); - // Toroidal shift to find which gpu tile the image tile corresponds to - uint32_t2 gpuImageTileIndex = ((imageTileIndex - currentMappedRegionTileRange.topLeftTile) + gpuImageTopLeft) % gpuImageSideLengthTiles; - // Don't bother scheduling an upload if the tile is already resident - if (!currentMappedRegionOccupancy[gpuImageTileIndex.x][gpuImageTileIndex.y]) - retVal.push_back({ imageTileIndex , gpuImageTileIndex }); - } - return retVal; -} - -GeoreferencedImageInfo GeoreferencedImageStreamingState::computeGeoreferencedImageAddressingAndPositioningInfo() -{ - GeoreferencedImageInfo ret = {}; - - // Figure out an obb that covers only the currently loaded tiles - OrientedBoundingBox2D viewportEncompassingOBB = worldspaceOBB; - // The image's worldspace dirU corresponds to `fullResImageExtents.x` texels of the image, therefore one image texel in the U direction has a worldspace span of `dirU / fullResImageExtents.x`. - // One mip 0 tiles therefore spans `dirU * GeoreferencedImageTileSize/ fullResImageExtents.x`. A mip `n` tile spans `2^n` this amount, since each texel at that mip level spans - // `2^n` mip texels. Therefore the dirU offset from the image wordlspace's topLeft of the tile of index `currentViewportTileRange.topLeftTile.x` at mip level `currentMappedRegion.baseMipLevel` can be calculated as - const uint32_t oneTileTexelSpan = GeoreferencedImageTileSize << currentMappedRegionTileRange.baseMipLevel; - viewportEncompassingOBB.topLeft += worldspaceOBB.dirU * float32_t(currentViewportTileRange.topLeftTile.x * oneTileTexelSpan) / float32_t(fullResImageExtents.x); - // Same reasoning for offset in v direction - const float32_t2 dirV = float32_t2(worldspaceOBB.dirU.y, -worldspaceOBB.dirU.x) * worldspaceOBB.aspectRatio; - viewportEncompassingOBB.topLeft += dirV * float32_t(currentViewportTileRange.topLeftTile.y * oneTileTexelSpan) / float32_t(fullResImageExtents.y); - - const uint32_t2 viewportTileLength = currentViewportTileRange.bottomRightTile - currentViewportTileRange.topLeftTile + uint32_t2(1, 1); - // If the last tile is visible, we use the fractional span for the last tile. Otherwise it's just a normal tile - const bool2 lastTileVisible = isLastTileVisible(currentViewportTileRange.bottomRightTile); - const uint32_t2 lastSampledImageTileTexels = { lastTileVisible.x ? lastTileSamplingExtent.x : oneTileTexelSpan, lastTileVisible.y ? lastTileSamplingExtent.y : oneTileTexelSpan }; - const uint32_t2 lastGPUImageTileTexels = { lastTileVisible.x ? lastTileTargetExtent.x : GeoreferencedImageTileSize, lastTileVisible.y ? lastTileTargetExtent.y : GeoreferencedImageTileSize }; - - // Instead of grouping per tile like in the offset case, we group per texel: the same reasoning leads to a single texel at current mip level having a span of `dirU * 2^(currentMappedRegionTileRange.baseMipLevel)/ fullResImageExtents.x` - // in the U direction. Therefore the span in worldspace of the OBB we construct is just this number multiplied by the number of image texels spanned to draw. - // The number of texels is just `GeoreferencedImageTileSize * 2^{mipLevel}` times the number of full tiles (all but the last) + the number of texels of the last tile, which might not be a full tile if near the right boundary - const uint32_t2 sampledImageTexels = oneTileTexelSpan * (viewportTileLength - 1u) + lastSampledImageTileTexels; - viewportEncompassingOBB.dirU = worldspaceOBB.dirU * float32_t(sampledImageTexels.x) / float32_t(fullResImageExtents.x); - // Simply number of image texels in the y direction divided by number of texels in the x direction. - viewportEncompassingOBB.aspectRatio = float32_t(sampledImageTexels.y) / float32_t(sampledImageTexels.x); - - // GPU tile corresponding to the real image tile containing the viewport top left - we can let it be negative since wrapping mode is repeat, negative tiles are correct modulo `gpuImageSideLengthTiles` - const uint32_t2 viewportTopLeftGPUTile = currentViewportTileRange.topLeftTile - currentMappedRegionTileRange.topLeftTile + gpuImageTopLeft; - // To get the uv corresponding to the above, simply divide the tile index by the number of tiles in the GPU image. - // However to consider a one-texel shift inward (to prevent color bleeding at the edges) we map both numerator and denominator to texel units (by multiplying with `GeoreferencedImageTileSize`) and add - // a single texel to the numerator - const float32_t2 minUV = float32_t2(GeoreferencedImageTileSize * viewportTopLeftGPUTile + 1u) / float32_t(GeoreferencedImageTileSize * gpuImageSideLengthTiles); - // If the image was perfectly partitioned into tiles, we could get the maxUV in a similar fashion to minUV: Just compute `bottomRightTile - currentMappedRegionTileRange.topLeftTile` to get a tile - // then divide by `gpuImageSideLengthTiles` to get a coord in `(0,1)` (correct modulo `gpuImageSideLengthTiles`) - // However the last tile might not have all `GeoreferencedImageTileSize` texels in it. Therefore maxUV computation can be separated into a UV contribution by all full tiles (all but the last) + a contribution from the last tile - // UV contribution from full tiles will therefore be `(bottomRightTile - currentMappedRegionTileRange.topLeftTile) / gpuImageSideLengthTiles` while last tile contribution will be - // `lastGPUImageTileTexels / (gpuImageSideLengthTiles * GeoreferencedImageTileSize)`. We group terms below to reduce number of float ops. - // Again we first map to texel units then subtract one to add a single texel uv shift. - const uint32_t2 viewportBottomRightGPUTile = currentViewportTileRange.bottomRightTile - currentMappedRegionTileRange.topLeftTile + gpuImageTopLeft; - const float32_t2 maxUV = float32_t2(GeoreferencedImageTileSize * viewportBottomRightGPUTile + lastGPUImageTileTexels - 1u) / float32_t(GeoreferencedImageTileSize * gpuImageSideLengthTiles); - - ret.minUV = minUV; - ret.maxUV = maxUV; - ret.topLeft = viewportEncompassingOBB.topLeft; - ret.dirU = viewportEncompassingOBB.dirU; - ret.aspectRatio = viewportEncompassingOBB.aspectRatio; - - return ret; -} - -GeoreferencedImageTileRange GeoreferencedImageStreamingState::computeViewportTileRange(const uint32_t2 viewportExtent, const float64_t3x3& ndcToWorldMat) -{ - // These are vulkan standard, might be different in n4ce! - constexpr static float64_t3 topLeftViewportNDC = float64_t3(-1.0, -1.0, 1.0); - constexpr static float64_t3 topRightViewportNDC = float64_t3(1.0, -1.0, 1.0); - constexpr static float64_t3 bottomLeftViewportNDC = float64_t3(-1.0, 1.0, 1.0); - constexpr static float64_t3 bottomRightViewportNDC = float64_t3(1.0, 1.0, 1.0); - - // First get world coordinates for each of the viewport's corners - const float64_t3 topLeftViewportWorld = nbl::hlsl::mul(ndcToWorldMat, topLeftViewportNDC); - const float64_t3 topRightViewportWorld = nbl::hlsl::mul(ndcToWorldMat, topRightViewportNDC); - const float64_t3 bottomLeftViewportWorld = nbl::hlsl::mul(ndcToWorldMat, bottomLeftViewportNDC); - const float64_t3 bottomRightViewportWorld = nbl::hlsl::mul(ndcToWorldMat, bottomRightViewportNDC); - - // Then we get mip 0 tiles coordinates for each of them, into the image - const float64_t2 topLeftTileLattice = transformWorldCoordsToTileCoords(topLeftViewportWorld); - const float64_t2 topRightTileLattice = transformWorldCoordsToTileCoords(topRightViewportWorld); - const float64_t2 bottomLeftTileLattice = transformWorldCoordsToTileCoords(bottomLeftViewportWorld); - const float64_t2 bottomRightTileLattice = transformWorldCoordsToTileCoords(bottomRightViewportWorld); - - // Get the min and max of each lattice coordinate to get a bounding rectangle - const float64_t2 minTop = nbl::hlsl::min(topLeftTileLattice, topRightTileLattice); - const float64_t2 minBottom = nbl::hlsl::min(bottomLeftTileLattice, bottomRightTileLattice); - const float64_t2 minAll = nbl::hlsl::min(minTop, minBottom); - - const float64_t2 maxTop = nbl::hlsl::max(topLeftTileLattice, topRightTileLattice); - const float64_t2 maxBottom = nbl::hlsl::max(bottomLeftTileLattice, bottomRightTileLattice); - const float64_t2 maxAll = nbl::hlsl::max(maxTop, maxBottom); - - // Floor them to get an integer coordinate (index) for the tiles they fall in - int32_t2 minAllFloored = nbl::hlsl::floor(minAll); - int32_t2 maxAllFloored = nbl::hlsl::floor(maxAll); - - // We're undoing a previous division. Could be avoided but won't restructure the code atp. - // Here we compute how many image pixels each side of the viewport spans - const float64_t2 viewportSideUImageTexelsVector = float64_t(GeoreferencedImageTileSize) * (topRightTileLattice - topLeftTileLattice); - const float64_t2 viewportSideVImageTexelsVector = float64_t(GeoreferencedImageTileSize) * (bottomLeftTileLattice - topLeftTileLattice); - - // WARNING: This assumes pixels in the image are the same size along each axis. If the image is nonuniformly scaled or sheared, I *think* it should not matter - // (since the pixel span takes that transformation into account), BUT we have to check if we plan on allowing those - // Compute the side vectors of the viewport in image pixel(texel) space. - // These vectors represent how many image pixels each side of the viewport spans. - // They correspond to the local axes of the mapped OBB (not the mapped region one, the viewport one) in texel coordinates. - const float64_t viewportSideUImageTexels = nbl::hlsl::length(viewportSideUImageTexelsVector); - const float64_t viewportSideVImageTexels = nbl::hlsl::length(viewportSideVImageTexelsVector); - - // Mip is decided based on max of these - float64_t pixelRatio = nbl::hlsl::max(viewportSideUImageTexels / viewportExtent.x, viewportSideVImageTexels / viewportExtent.y); - pixelRatio = pixelRatio < 1.0 ? 1.0 : pixelRatio; - - GeoreferencedImageTileRange retVal = {}; - // Clamp mip level so we don't consider tiles that are too small along one dimension - // If on a pathological case this gets too expensive because the GPU starts sampling a lot, we can consider changing this, but I doubt that will happen - retVal.baseMipLevel = nbl::hlsl::min(nbl::hlsl::findMSB(uint32_t(nbl::hlsl::floor(pixelRatio))), int32_t(maxMipLevel)); - - // Current tiles are measured in mip 0. We want the result to measure mip `retVal.baseMipLevel` tiles. Each next mip level divides by 2. - minAllFloored >>= retVal.baseMipLevel; - maxAllFloored >>= retVal.baseMipLevel; - - - // Clamp them to reasonable tile indices - int32_t2 lastTileIndex = getLastTileIndex(retVal.baseMipLevel); - retVal.topLeftTile = nbl::hlsl::clamp(minAllFloored, int32_t2(0, 0), lastTileIndex); - retVal.bottomRightTile = nbl::hlsl::clamp(maxAllFloored, int32_t2(0, 0), lastTileIndex); - - return retVal; -} - -void GeoreferencedImageStreamingState::ensureMappedRegionCoversViewport(const GeoreferencedImageTileRange& viewportTileRange) -{ - // A base mip level of x in the current mapped region means we can handle the viewport having mip level y, with x <= y < x + 1.0 - // without needing to remap the region. When the user starts zooming in or out and the mip level of the viewport falls outside this range, we have to remap - // the mapped region. - const bool mipBoundaryCrossed = viewportTileRange.baseMipLevel != currentMappedRegionTileRange.baseMipLevel; - - // If we moved a huge amount in any direction, no tiles will remain resident, so we simply reset state - // This only need be evaluated if the mip boundary was not already crossed - const bool relativeShiftTooBig = !mipBoundaryCrossed && - nbl::hlsl::any - ( - nbl::hlsl::abs(int32_t2(viewportTileRange.topLeftTile) - int32_t2(currentMappedRegionTileRange.topLeftTile)) >= int32_t2(gpuImageSideLengthTiles, gpuImageSideLengthTiles) - ) - || nbl::hlsl::any - ( - nbl::hlsl::abs(int32_t2(viewportTileRange.bottomRightTile) - int32_t2(currentMappedRegionTileRange.bottomRightTile)) >= int32_t2(gpuImageSideLengthTiles, gpuImageSideLengthTiles) - ); - - // If there is no overlap between previous mapped region and the next, just reset everything - if (mipBoundaryCrossed || relativeShiftTooBig) - remapCurrentRegion(viewportTileRange); - // Otherwise we can get away with (at worst) sliding the mapped region along the real image, preserving the residency of the tiles that overlap between previous mapped region and the next - else - slideCurrentRegion(viewportTileRange); -} - -void GeoreferencedImageStreamingState::remapCurrentRegion(const GeoreferencedImageTileRange& viewportTileRange) -{ - // Zoomed out - if (viewportTileRange.baseMipLevel > currentMappedRegionTileRange.baseMipLevel) - { - // TODO: Here we would move some mip 1 tiles to mip 0 image to save the work of reuploading them, reflect that in the tracked tiles - } - // Zoomed in - else if (viewportTileRange.baseMipLevel < currentMappedRegionTileRange.baseMipLevel) - { - // TODO: Here we would move some mip 0 tiles to mip 1 image to save the work of reuploading them, reflect that in the tracked tiles - } - currentMappedRegionTileRange = viewportTileRange; - // We can expand the currentMappedRegionTileRange to make it as big as possible, at no extra cost since we only upload tiles on demand - // Since we use toroidal updating it's kinda the same which way we expand the region. We first try to make the extent be `gpuImageSideLengthTiles` - currentMappedRegionTileRange.bottomRightTile = currentMappedRegionTileRange.topLeftTile + uint32_t2(gpuImageSideLengthTiles, gpuImageSideLengthTiles) - uint32_t2(1, 1); - // This extension can cause the mapped region to fall out of bounds on border cases, therefore we clamp it and extend it in the other direction - // by the amount of tiles we removed during clamping - const uint32_t2 excessTiles = uint32_t2(nbl::hlsl::max(int32_t2(0, 0), int32_t2(currentMappedRegionTileRange.bottomRightTile) - int32_t2(getLastTileIndex(currentMappedRegionTileRange.baseMipLevel)))); - currentMappedRegionTileRange.bottomRightTile -= excessTiles; - // Shifting of the topLeftTile could fall out of bounds in pathological cases or at very high mip levels (zooming out too much), so we shift if possible, otherwise set it to 0 - currentMappedRegionTileRange.topLeftTile = uint32_t2(nbl::hlsl::max(int32_t2(0, 0), int32_t2(currentMappedRegionTileRange.topLeftTile) - int32_t2(excessTiles))); - - ResetTileOccupancyState(); - // Reset state for gpu image so that it starts loading tiles at top left. Not really necessary. - gpuImageTopLeft = uint32_t2(0, 0); -} - -void GeoreferencedImageStreamingState::ResetTileOccupancyState() -{ - // Mark all gpu tiles as dirty - currentMappedRegionOccupancy.assign(gpuImageSideLengthTiles, std::vector(gpuImageSideLengthTiles, false)); -} - -void GeoreferencedImageStreamingState::slideCurrentRegion(const GeoreferencedImageTileRange& viewportTileRange) -{ - // `topLeftShift` represents how many tiles up and to the left we have to move the mapped region to fit the viewport. - // First we compute a vector from the current mapped region's topleft to the viewport's topleft. If this vector is positive along a dimension it means - // the viewport's topleft is to the right or below the current mapped region's topleft, so we don't have to shift the mapped region to the left/up in that case - const int32_t2 topLeftShift = nbl::hlsl::min(int32_t2(0, 0), int32_t2(viewportTileRange.topLeftTile) - int32_t2(currentMappedRegionTileRange.topLeftTile)); - // `bottomRightShift` represents the same as above but in the other direction. - const int32_t2 bottomRightShift = nbl::hlsl::max(int32_t2(0, 0), int32_t2(viewportTileRange.bottomRightTile) - int32_t2(currentMappedRegionTileRange.bottomRightTile)); - - // The following is not necessarily equal to `gpuImageSideLengthTiles` since there can be pathological cases, as explained in the remapping method - const uint32_t2 mappedRegionDimensions = currentMappedRegionTileRange.bottomRightTile - currentMappedRegionTileRange.topLeftTile + 1u; - const uint32_t2 gpuImageBottomRight = (gpuImageTopLeft + mappedRegionDimensions - 1u) % gpuImageSideLengthTiles; - - // Mark dropped tiles as dirty/non-resident - if (topLeftShift.x < 0) - { - // Shift left - const uint32_t tilesToFit = -topLeftShift.x; - for (uint32_t tile = 0; tile < tilesToFit; tile++) - { - // Get actual tile index with wraparound - uint32_t tileIdx = (gpuImageBottomRight.x + (gpuImageSideLengthTiles - tile)) % gpuImageSideLengthTiles; - currentMappedRegionOccupancy[tileIdx].clear(); - currentMappedRegionOccupancy[tileIdx].resize(gpuImageSideLengthTiles, false); - } - } - else if (bottomRightShift.x > 0) - { - //Shift right - const uint32_t tilesToFit = bottomRightShift.x; - for (uint32_t tile = 0; tile < tilesToFit; tile++) - { - // Get actual tile index with wraparound - uint32_t tileIdx = (tile + gpuImageTopLeft.x) % gpuImageSideLengthTiles; - currentMappedRegionOccupancy[tileIdx].clear(); - currentMappedRegionOccupancy[tileIdx].resize(gpuImageSideLengthTiles, false); - } - } - - if (topLeftShift.y < 0) - { - // Shift up - const uint32_t tilesToFit = -topLeftShift.y; - for (uint32_t tile = 0; tile < tilesToFit; tile++) - { - // Get actual tile index with wraparound - uint32_t tileIdx = (gpuImageBottomRight.y + (gpuImageSideLengthTiles - tile)) % gpuImageSideLengthTiles; - for (uint32_t i = 0u; i < gpuImageSideLengthTiles; i++) - currentMappedRegionOccupancy[i][tileIdx] = false; - } - } - else if (bottomRightShift.y > 0) - { - //Shift down - const uint32_t tilesToFit = bottomRightShift.y; - for (uint32_t tile = 0; tile < tilesToFit; tile++) - { - // Get actual tile index with wraparound - uint32_t tileIdx = (tile + gpuImageTopLeft.y) % gpuImageSideLengthTiles; - for (uint32_t i = 0u; i < gpuImageSideLengthTiles; i++) - currentMappedRegionOccupancy[i][tileIdx] = false; - } - } - - // Shift the mapped region accordingly - // A nice consequence of the mapped region being always maximally - sized is that - // along any dimension, only a shift in one direction is necessary, so we can simply add up the shifts - currentMappedRegionTileRange.topLeftTile = uint32_t2(int32_t2(currentMappedRegionTileRange.topLeftTile) + topLeftShift + bottomRightShift); - currentMappedRegionTileRange.bottomRightTile = uint32_t2(int32_t2(currentMappedRegionTileRange.bottomRightTile) + topLeftShift + bottomRightShift); - - // Toroidal shift for the gpu image top left - gpuImageTopLeft = (gpuImageTopLeft + uint32_t2(topLeftShift + bottomRightShift + int32_t(gpuImageSideLengthTiles))) % gpuImageSideLengthTiles; -} - -std::string CachedImageRecord::toString(uint64_t imageID) const -{ - auto stringifyImageState = [](ImageState state) -> std::string { - switch (state) - { - case ImageState::INVALID: return "INVALID"; - case ImageState::CREATED_AND_MEMORY_BOUND: return "CREATED_AND_MEMORY_BOUND"; - case ImageState::BOUND_TO_DESCRIPTOR_SET: return "BOUND_TO_DESCRIPTOR_SET"; - case ImageState::GPU_RESIDENT_WITH_VALID_STATIC_DATA: return "GPU_RESIDENT_WITH_VALID_STATIC_DATA"; - default: return "UNKNOWN_STATE"; - } - }; - - auto stringifyImageType = [](ImageType type) -> std::string { - switch (type) - { - case ImageType::INVALID: return "INVALID"; - case ImageType::STATIC: return "STATIC"; - case ImageType::GEOREFERENCED_STREAMED: return "GEOREFERENCED_STREAMED"; - default: return "UNKNOWN_TYPE"; - } - }; - - std::string result; - if (imageID != std::numeric_limits::max()) - result += std::format(" ImageID: {}\n", imageID); - - result += std::format( - " Type: {}\n" - " State: {}\n" - " Array Index: {}\n" - " Allocation Offset: {}\n" - " Allocation Size: {}\n" - " Current Layout: {}\n" - " Last Used Frame Index: {}\n" - " GPU ImageView: {}\n" - " CPU Image: {}\n" - " Georeferenced Image State: {}\n", - stringifyImageType(type), - stringifyImageState(state), - arrayIndex, - allocationOffset, - allocationSize, - static_cast(currentLayout), - lastUsedFrameIndex, - gpuImageView ? "VALID" : "NULL", - staticCPUImage ? "VALID" : "NULL", - georeferencedImageState ? "VALID" : "NULL" - ); - return result; -} \ No newline at end of file diff --git a/62_CAD/Images.h b/62_CAD/Images.h deleted file mode 100644 index d397141d1..000000000 --- a/62_CAD/Images.h +++ /dev/null @@ -1,476 +0,0 @@ -/* DrawResourcesFiller: This class provides important functionality to manage resources needed for a draw. -/******************************************************************************/ -#pragma once - -#include "shaders/globals.hlsl" -#include - -using namespace nbl; -using namespace nbl::video; -using namespace nbl::core; -using namespace nbl::asset; - -using image_id = uint64_t; // Could later be templated or replaced with a stronger type or hash key. - -// These are mip 0 pixels per tile, also size of each physical tile into the gpu resident image -constexpr static uint32_t GeoreferencedImageTileSize = 128u; -// Mip 1 tiles are naturally half the size -constexpr static uint32_t GeoreferencedImageTileSizeMip1 = GeoreferencedImageTileSize / 2; -// How many tiles of extra padding we give to the gpu image holding the tiles for a georeferenced image -constexpr static uint32_t GeoreferencedImagePaddingTiles = 2; - -enum class ImageState : uint8_t -{ - INVALID = 0, - CREATED_AND_MEMORY_BOUND, // GPU image created, not bound to descriptor set yet - GPU_RESIDENT_WITH_VALID_STATIC_DATA, // When data for static images gets issued for upload successfully, may not be bound to it's descriptor binding array index yet - BOUND_TO_DESCRIPTOR_SET, // Bound to descriptor set, GPU resident -}; - -enum class ImageType : uint8_t -{ - INVALID = 0, - STATIC, // Regular non-georeferenced image, fully loaded once - GEOREFERENCED_STREAMED, // Streamed image, resolution depends on camera/view // TODO[DEVSH]: Probably best to rename this to STREAMED image -}; - -/** - * @class ImagesMemorySubAllocator - * @brief A memory sub-allocator designed for managing sub-allocations within a pre-allocated GPU memory arena for images. - * - * This class wraps around `nbl::core::GeneralpurposeAddressAllocator` to provide offset-based memory allocation - * for image resources within a contiguous block of GPU memory. - * - * @note This class only manages address offsets. The actual memory must be bound separately. - */ -class ImagesMemorySubAllocator : public core::IReferenceCounted -{ -public: - using AddressAllocator = nbl::core::GeneralpurposeAddressAllocator; - using ReservedAllocator = nbl::core::allocator; - static constexpr uint64_t InvalidAddress = AddressAllocator::invalid_address; - static constexpr uint64_t MaxMemoryAlignment = 4096u; // safe choice based on hardware reports - static constexpr uint64_t MinAllocSize = 128 * 1024u; // 128KB, the larger this is the better - - ImagesMemorySubAllocator(uint64_t memoryArenaSize) - { - m_reservedAllocSize = AddressAllocator::reserved_size(MaxMemoryAlignment, memoryArenaSize, MinAllocSize); - m_reservedAllocator = std::unique_ptr(new ReservedAllocator()); - m_reservedAlloc = m_reservedAllocator->allocate(m_reservedAllocSize, _NBL_SIMD_ALIGNMENT); - m_addressAllocator = std::unique_ptr(new AddressAllocator( - m_reservedAlloc, 0u, 0u, MaxMemoryAlignment, memoryArenaSize, MinAllocSize - )); - } - - // return offset, will return InvalidAddress if failed - uint64_t allocate(uint64_t size, uint64_t alignment) - { - return m_addressAllocator->alloc_addr(size, alignment); - } - - void deallocate(uint64_t addr, uint64_t size) - { - m_addressAllocator->free_addr(addr, size); - } - - uint64_t getFreeSize() const - { - return m_addressAllocator->get_free_size(); - } - - ~ImagesMemorySubAllocator() - { - if (m_reservedAlloc) - m_reservedAllocator->deallocate(reinterpret_cast(m_reservedAlloc), m_reservedAllocSize); - } - -private: - std::unique_ptr m_addressAllocator = nullptr; - - // Memory Allocation Required for the AddressAllocator - std::unique_ptr m_reservedAllocator = nullptr; - void* m_reservedAlloc = nullptr; - size_t m_reservedAllocSize = 0; - -}; - -// This will be dropped when the descriptor gets dropped from SuballocatedDescriptorSet. -// Destructor will then deallocate from GeneralPurposeAllocator, making the previously allocated range of the image available/free again. -struct ImageCleanup : public core::IReferenceCounted -{ - ImageCleanup(); - - ~ImageCleanup() override; - - smart_refctd_ptr imagesMemorySuballocator; - uint64_t addr; - uint64_t size; - -}; - -// Measures a range of mip `baseMipLevel` tiles in the georeferenced image, starting at `topLeftTile` that is `nTiles` long -struct GeoreferencedImageTileRange -{ - uint32_t2 topLeftTile; - uint32_t2 bottomRightTile; - uint32_t baseMipLevel; -}; - -// @brief Used to load tiles into VRAM, keep track of loaded tiles, determine how they get sampled etc. -struct GeoreferencedImageStreamingState : public IReferenceCounted -{ -public: - - GeoreferencedImageStreamingState() - { } - - //! Creates a new streaming state for a georeferenced image - /* - Initializes CPU-side state for image streaming. - Sets up world-to-UV transform, computes mip hierarchy parameters, - and stores metadata about the image. - - @param worldspaceOBB Oriented bounding box of the image in world space - @param fullResImageExtents Full resolution image size in pixels (width, height) - @param format Pixel format of the image - @param storagePath Filesystem path for image tiles - */ - bool init(const OrientedBoundingBox2D& worldSpaceOBB, const uint32_t2 fullResImageExtents, const asset::E_FORMAT format, const std::filesystem::path& storagePath); - - /** - * @brief Update the mapped region to cover the current viewport. - * - * Computes the required tile range from the viewport and updates - * `currentMappedRegion` by remapping or sliding as needed. - * - * @param currentViewportExtents Viewport size in pixels. - * @param ndcToWorldMat NDC to world space mattix. - * - * @see tilesToLoad - */ - void updateStreamingStateForViewport(const uint32_t2 viewportExtent, const float64_t3x3& ndcToWorldMat); - - // @brief Info to match a gpu tile to the tile in the real image it should hold image data for - struct ImageTileToGPUTileCorrespondence - { - uint32_t2 imageTileIndex; - uint32_t2 gpuImageTileIndex; - }; - - /* - * @brief Get the tiles required for rendering the current viewport. - * Uses the region set by `updateStreamingStateForViewport()` to return - * which image tiles need loading and their target GPU tile indices. - */ - core::vector tilesToLoad() const; - - // @brief Returns the index of the last tile when covering the image with `mipLevel` tiles - inline uint32_t2 getLastTileIndex(uint32_t mipLevel) const - { - return (fullImageTileLength - 1u) >> mipLevel; - } - - // @brief Returns whether the last tile in the image (along each dimension) is visible from the current viewport - inline bool2 isLastTileVisible(const uint32_t2 viewportBottomRightTile) const - { - const uint32_t2 lastTileIndex = getLastTileIndex(currentMappedRegionTileRange.baseMipLevel); - return bool2(lastTileIndex.x == viewportBottomRightTile.x, lastTileIndex.y == viewportBottomRightTile.y); - } - - /** - * @brief Compute viewport positioning and UV addressing for a georeferenced image. - * - * Returns a `GeoreferencedImageInfo` filled with: - * - `topLeft`, `dirU`, `aspectRatio` (world-space OBB) - * - `minUV`, `maxUV` (UV addressing for the viewport) - * - * Leaves `textureID` unmodified. - * - * @note Make sure to call `updateStreamingStateForViewport()` first so that - * the OBB and UVs reflect the latest viewport. - * - * @param imageStreamingState The streaming state of the georeferenced image. - * @return GeoreferencedImageInfo containing viewport positioning and UV info. - */ - GeoreferencedImageInfo computeGeoreferencedImageAddressingAndPositioningInfo(); - - bool isOutOfDate() const { return outOfDate; } - -private: - // These are NOT UV, pixel or tile coords into the mapped image region, rather into the real, huge image - // Tile coords are always in mip 0 tile size. Translating to other mips levels is trivial - - // @brief Transform worldspace coordinates into UV coordinates into the image - float64_t2 transformWorldCoordsToUV(const float64_t3 worldCoords) const { return nbl::hlsl::mul(worldToUV, worldCoords); } - // @brief Transform worldspace coordinates into texel coordinates into the image - float64_t2 transformWorldCoordsToTexelCoords(const float64_t3 worldCoords) const { return float64_t2(fullResImageExtents) * transformWorldCoordsToUV(worldCoords); } - // @brief Transform worldspace coordinates into tile coordinates into the image, where the image is broken up into tiles of size `GeoreferencedImageTileSize` - float64_t2 transformWorldCoordsToTileCoords(const float64_t3 worldCoords) const { return (1.0 / GeoreferencedImageTileSize) * transformWorldCoordsToTexelCoords(worldCoords); } - - /** - * @brief Compute the tile range and mip level needed to cover the viewport. - * - * Calculates which portion of the source image is visible through the given - * viewport and chooses the optimal mip level based on zoom (viewport size - * relative to the image). The returned range is always a subset of - * `currentMappedRegion` and covers only the visible tiles. - * - * @param currentViewportExtents Size of the viewport in pixels. - * @param ndcToWorldMat Transform from NDC to world space, used to project - * the viewport onto the image. - * - * @return A tile range (`GeoreferencedImageTileRange`) representing the - * visible region at the chosen mip level. - */ - GeoreferencedImageTileRange computeViewportTileRange(const uint32_t2 viewportExtent, const float64_t3x3& ndcToWorldMat); - - /* - * @brief The GPU image backs a mapped region which is a rectangular sub-region of the original image. Note that a region being mapped does NOT imply it's currently resident in GPU memory. - * To display the iomage on the screen, before even checking that the tiles needed to render the portion of the image currently visible are resident in GPU memory, we first must ensure that - * said region is included (as a sub-rectangle) in the mapped region. - * - * @param viewportTileRange Range of tiles + mip level indicating what sub-rectangle (and at which mip level) of the image is going to be visible from the viewport - */ - void ensureMappedRegionCoversViewport(const GeoreferencedImageTileRange& viewportTileRange); - - /* - * @brief Sets the mapped region into the image so it at least covers the sub-rectangle currently visible from the viewport. Also marks all gpu tiles dirty since none can be recycled - * - * @param viewportTileRange Range of tiles + mip level indicating a sub-rectangle of the image (visible from viewport) that the mapped region needs to cover - */ - void remapCurrentRegion(const GeoreferencedImageTileRange& viewportTileRange); - - /** - * @brief Resets the streaming state's GPU tile occupancy map. - * - Clears all previously marked resident tiles. - * - After this call, every entry in `currentMappedRegionOccupancy` is `false`, - * meaning the GPU image is considered completely dirty (no tiles mapped). - */ - void ResetTileOccupancyState(); - - /* - * @brief Slides the mapped region along the image, marking the tiles dropped as dirty but preserving the residency for tiles that are inside both the previous and new mapped regions. - * Note that the checks for whether this is valid to do happen outside of this function. - * - * @param viewportTileRange Range of tiles + mip level indicating a sub-rectangle of the image (visible from viewport) that the mapped region needs to cover - */ - void slideCurrentRegion(const GeoreferencedImageTileRange& viewportTileRange); - -protected: - friend class DrawResourcesFiller; - - // Oriented bounding box of the original image in world space (position + orientation) - OrientedBoundingBox2D worldspaceOBB = {}; - // Full resolution original image size in pixels (width, height) - uint32_t2 fullResImageExtents = {}; - // Pixel format of the image as provided by storage/loader (may differ from GPU format) - asset::E_FORMAT sourceImageFormat = {}; - // Filesystem path where image tiles are stored - std::filesystem::path storagePath = {}; - // GPU Image Params for the image to be created with - IGPUImage::SCreationParams gpuImageCreationParams = {}; - // 2D bool set for tile validity of the currentMappedRegionTileRange - std::vector> currentMappedRegionOccupancy = {}; - // Sidelength of the gpu image, in mip 0 tiles that are `TileSize` (creation parameter) texels wide - uint32_t gpuImageSideLengthTiles = {}; - // We establish a max mipLevel for the image, which is the mip level at which any of width, height fit in a single tile - uint32_t maxMipLevel = {}; - // Number of mip 0 tiles needed to cover the whole image, counting the last tile that might be fractional if the image size is not perfectly divisible by TileSize - uint32_t2 fullImageTileLength = {}; - // Indicates on which tile of the gpu image the current mapped region's `topLeft` resides - uint32_t2 gpuImageTopLeft = {}; - // Converts a point (z = 1) in worldspace to UV coordinates in image space (origin shifted to topleft of the image) - float64_t2x3 worldToUV = {}; - // The GPU-mapped region covering a subrectangle of the source image - GeoreferencedImageTileRange currentMappedRegionTileRange = { .baseMipLevel = std::numeric_limits::max() }; - // Tile range covering only the tiles currently visible in the viewport - GeoreferencedImageTileRange currentViewportTileRange = { .baseMipLevel = std::numeric_limits::max() }; - // Extents used for sampling the last tile (handles partial tiles / NPOT images); gets updated with `updateStreamingStateForViewport` - uint32_t2 lastTileSamplingExtent; - // Extents used when writing/updating the last tile in GPU memory (handles partial tiles / NPOT images); gets updated with `updateStreamingStateForViewport` - uint32_t2 lastTileTargetExtent; - // We set this to true when image is evicted from cache, hinting at other places holding a smart_refctd_ptr to this objet that the GeoreferencedImageStreamingState isn't valid anymore and needs recreation/update - bool outOfDate = false; -}; - -struct CachedImageRecord -{ - static constexpr uint32_t InvalidTextureIndex = nbl::hlsl::numeric_limits::max; - - uint32_t arrayIndex = InvalidTextureIndex; // index in our array of textures binding - bool arrayIndexAllocatedUsingImageDescriptorIndexAllocator; // whether the index of this cache entry was allocated using suballocated descriptor set which ensures correct synchronized access to a set index. (if not extra synchro is needed) - ImageType type = ImageType::INVALID; - ImageState state = ImageState::INVALID; - nbl::asset::IImage::LAYOUT currentLayout = nbl::asset::IImage::LAYOUT::UNDEFINED; - uint64_t lastUsedFrameIndex = 0ull; // last used semaphore value on this image - uint64_t allocationOffset = ImagesMemorySubAllocator::InvalidAddress; - uint64_t allocationSize = 0ull; - core::smart_refctd_ptr gpuImageView = nullptr; - core::smart_refctd_ptr staticCPUImage = nullptr; // cached cpu image for uploading to gpuImageView when needed. - core::smart_refctd_ptr georeferencedImageState = nullptr; // Used to track tile residency for georeferenced images - - // In LRU Cache `insert` function, in case of cache miss, we need to construct the refereence with semaphore value - CachedImageRecord(uint64_t currentFrameIndex) - : arrayIndex(InvalidTextureIndex) - , arrayIndexAllocatedUsingImageDescriptorIndexAllocator(false) - , type(ImageType::INVALID) - , state(ImageState::INVALID) - , lastUsedFrameIndex(currentFrameIndex) - , allocationOffset(ImagesMemorySubAllocator::InvalidAddress) - , allocationSize(0ull) - , gpuImageView(nullptr) - , staticCPUImage(nullptr) - {} - - CachedImageRecord() - : CachedImageRecord(0ull) - {} - - std::string toString(uint64_t imageID = std::numeric_limits::max()) const; - - // In LRU Cache `insert` function, in case of cache hit, we need to assign semaphore value without changing `index` - inline CachedImageRecord& operator=(uint64_t currentFrameIndex) { lastUsedFrameIndex = currentFrameIndex; return *this; } -}; - -// A resource-aware image cache with an LRU eviction policy. -// This cache tracks image usage by ID and provides hooks for eviction logic (such as releasing descriptor slots and deallocating GPU memory done by user of this class) -// Currently, eviction is purely LRU-based. In the future, eviction decisions may incorporate additional factors: -// - memory usage per image. -// - lastUsedFrameIndex. -// This class helps coordinate images' lifetimes in sync with GPU usage via eviction callbacks. -class ImagesCache : public core::ResizableLRUCache -{ -public: - using base_t = core::ResizableLRUCache; - - ImagesCache(size_t capacity) - : base_t(capacity) - {} - - // Attempts to insert a new image into the cache. - // If the cache is full, invokes the provided `evictCallback` to evict an image. - // Returns a pointer to the inserted or existing ImageReference. - template EvictionCallback> - inline CachedImageRecord* insert(image_id imageID, uint64_t lastUsedSema, EvictionCallback&& evictCallback) - { - return base_t::insert(imageID, lastUsedSema, evictCallback); - } - - // Retrieves the image associated with `imageID`, updating its LRU position. - inline CachedImageRecord* get(image_id imageID) - { - return base_t::get(imageID); - } - - // Retrieves the ImageReference without updating LRU order. - inline CachedImageRecord* peek(image_id imageID) - { - return base_t::peek(imageID); - } - - inline size_t size() const { return base_t::size(); } - - // Selects an eviction candidate based on LRU policy. - // In the future, this could factor in memory pressure or semaphore sync requirements. - inline image_id select_eviction_candidate() - { - const image_id* lru = base_t::get_least_recently_used(); - if (lru) - return *lru; - else - { - // we shouldn't select eviction candidate if lruCache is empty - _NBL_DEBUG_BREAK_IF(true); - return ~0ull; - } - } - - inline void logState(nbl::system::logger_opt_smart_ptr logger) - { - logger.log("=== Image Cache Status ===", nbl::system::ILogger::ELL_INFO); - for (const auto& [imageID, record] : *this) - { - logger.log(("\n" + record.toString(imageID)).c_str(), nbl::system::ILogger::ELL_INFO); - } - logger.log("=== End of Image Cache ===", nbl::system::ILogger::ELL_INFO); - } - - // Removes a specific image from the cache (manual eviction). - inline void erase(image_id imageID) - { - base_t::erase(imageID); - } -}; - -struct StreamedImageCopy -{ - asset::E_FORMAT srcFormat; - std::future> srcBufferFuture; - asset::IImage::SBufferCopy region; -}; - -// TODO: Rename to StaticImageAvailabilityRequest? -struct StaticImageInfo -{ - image_id imageID = ~0ull; - core::smart_refctd_ptr cpuImage = nullptr; - bool forceUpdate = false; // If true, bypasses the existing GPU-side cache and forces an update of the image data; Useful when replacing the contents of a static image that may already be resident. - asset::E_FORMAT imageViewFormatOverride = asset::E_FORMAT::EF_COUNT; // if asset::E_FORMAT::EF_COUNT then image view will have the same format as `cpuImage` -}; - -/// @brief Abstract class with two overridable methods to load a region of an image, either by requesting a region at a target extent (like the loaders in n4ce do) or to request a specific region from a mip level -// (like precomputed mips solution would use). -struct IImageRegionLoader : IReferenceCounted -{ - /** - * @brief Load a region from an image - used to load from images with precomputed mips - * - * @param imagePath Path to file holding the image data - * @param offset Offset into the image (at requested mipLevel!) at which the region begins - * @param extent Extent of the region to load (at requested mipLevel!) - * @param mipLevel From which mip level image to retrieve the data from - * @param downsample True if this request is supposed to go into GPU mip level 1, false otherwise - * - * @return ICPUBuffer with the requested image data - */ - core::smart_refctd_ptr load(std::filesystem::path imagePath, uint32_t2 offset, uint32_t2 extent, uint32_t mipLevel, bool downsample) - { - assert(hasPrecomputedMips(imagePath)); - return load_impl(imagePath, offset, extent, mipLevel, downsample); - } - - /** - * @brief Load a region from an image - used to load from images using the n4ce loaders. Loads a region given by `offset, extent` as an image of size `targetExtent` - * where `targetExtent <= extent` so the loader is in charge of downsampling. - * - * @param imagePath Path to file holding the image data - * @param offset Offset into the image at which the region begins - * @param extent Extent of the region to load - * @param targetExtent Extent of the resulting image. Should NEVER be bigger than `extent` - * - * @return ICPUBuffer with the requested image data - */ - core::smart_refctd_ptr load(std::filesystem::path imagePath, uint32_t2 offset, uint32_t2 extent, uint32_t2 targetExtent) - { - assert(!hasPrecomputedMips(imagePath)); - return load_impl(imagePath, offset, extent, targetExtent); - } - - // @brief Get the extents (in texels) of an image. - virtual uint32_t2 getExtents(std::filesystem::path imagePath) = 0; - - /** - * @brief Get the texel format for an image. - */ - virtual asset::E_FORMAT getFormat(std::filesystem::path imagePath) = 0; - - // @brief Returns whether the image should be loaded with the precomputed mip method or the n4ce loader method. - virtual bool hasPrecomputedMips(std::filesystem::path imagePath) const = 0; -private: - - // @brief Override to support loading with precomputed mips - virtual core::smart_refctd_ptr load_impl(std::filesystem::path imagePath, uint32_t2 offset, uint32_t2 extent, uint32_t mipLevel, bool downsample) { return nullptr; } - - // @brief Override to support loading with n4ce-style loaders - virtual core::smart_refctd_ptr load_impl(std::filesystem::path imagePath, uint32_t2 offset, uint32_t2 extent, uint32_t2 targetExtent) { return nullptr; } -}; \ No newline at end of file diff --git a/62_CAD/Polyline.cpp b/62_CAD/Polyline.cpp deleted file mode 100644 index 4149942c7..000000000 --- a/62_CAD/Polyline.cpp +++ /dev/null @@ -1,702 +0,0 @@ -#include "Polyline.h" - -void CPolyline::preprocessPolylineWithStyle(const LineStyleInfo& lineStyle, float64_t discontinuityErrorTolerance, const AddShapeFunc& addShape) -{ - if (lineStyle.skipPreprocess()) - return; - const float64_t2 DiscontinuityErrorTolerance = float64_t2(discontinuityErrorTolerance, discontinuityErrorTolerance); - // We allow for discontinuity now, so no need to enable this unless testing. - // DISCONNECTION DETECTED, will break styling and offsetting the polyline, if you don't care about those then ignore discontinuity. - // _NBL_DEBUG_BREAK_IF(!checkSectionsContinuity()); - - // Check if it's truly closedPolygon - bool actuallyClosed = false; - if (m_closedPolygon && m_sections.size() > 0u) - actuallyClosed = checkSectionsActuallyClosed(discontinuityErrorTolerance); - - const bool shouldAddShapes = (lineStyle.hasShape() && addShape.operator bool()); - // When stretchToFit is true, the curve section and individual lines should start from the beginning of the pattern (phaseShift = lineStyle.phaseShift) - float currentPhaseShift = lineStyle.phaseShift; - - m_polylineConnector.clear(); - // to detect gap/discontinuity and reset phase shift - float64_t2 prevPoint = float64_t2(nbl::hlsl::numeric_limits::infinity, nbl::hlsl::numeric_limits::infinity); - static constexpr float64_t2 InvalidNormal = float64_t2(nbl::hlsl::numeric_limits::infinity, nbl::hlsl::numeric_limits::infinity); - float64_t2 prevNormal = InvalidNormal; - - for (uint32_t sectionIdx = 0u; sectionIdx < m_sections.size(); sectionIdx++) - { - const auto& section = m_sections[sectionIdx]; - - if (section.count == 0u) - { - assert(false); // shouldn't happen in any scenario - continue; - } - - // if gap detected - if (sectionIdx > 0u && glm::any(glm::greaterThan(glm::abs(getSectionFirstPoint(section) - prevPoint), DiscontinuityErrorTolerance))) - { - if constexpr (PolylineSettings::ResetLineStyleOnDiscontinuity) - currentPhaseShift = lineStyle.phaseShift; // reset phase shift - prevNormal = InvalidNormal; // to avoid construction of polyline connector after a gap - } - - if (section.type == ObjectType::LINE) - { - // calculate phase shift at each point of each line in section - const uint32_t lineCount = section.count; - - for (uint32_t i = 0u; i < lineCount; i++) - { - const uint32_t currIdx = section.index + i; - auto& linePoint = m_linePoints[currIdx]; - const auto& nextLinePoint = m_linePoints[currIdx + 1u]; - const float64_t2 lineVector = nextLinePoint.p - linePoint.p; - const float64_t lineLen = glm::length(lineVector); - const float32_t stretchValue = lineStyle.calculateStretchValue(lineLen); - const float rcpStretchedPatternLen = (lineStyle.reciprocalStipplePatternLen) / stretchValue; - - if (lineStyle.stretchToFit) - currentPhaseShift = lineStyle.getStretchedPhaseShift(stretchValue); - - linePoint.phaseShift = currentPhaseShift; - linePoint.stretchValue = stretchValue; - - if (lineStyle.isRoadStyleFlag) - { - float64_t2 lineNormal = float64_t2(-lineVector.y, lineVector.x) / lineLen; - if (prevNormal != InvalidNormal && checkIfInDrawSection(lineStyle, currentPhaseShift)) - addMiterIfVisible(prevNormal, lineNormal, linePoint.p); - prevNormal = lineNormal; - } - - if (shouldAddShapes) - { - float shapeOffsetNormalized = lineStyle.getStretchedShapeNormalizedPlaceInPattern(stretchValue); - // next shape Offset from start of line/curve - float nextShapeOffset = shapeOffsetNormalized - currentPhaseShift; - if (nextShapeOffset < 0.0f) - nextShapeOffset += 1.0f; - - int32_t numberOfShapes = static_cast(std::ceil(lineLen * rcpStretchedPatternLen - nextShapeOffset)); // numberOfShapes = (ArcLen - nextShapeOffset*PatternLen)/PatternLen + 1 - - float64_t stretchedPatternLen = 1.0 / (float64_t)rcpStretchedPatternLen; - float64_t currentWorldSpaceOffset = nextShapeOffset * stretchedPatternLen; - float64_t2 direction = lineVector / lineLen; - for (int32_t s = 0; s < numberOfShapes; ++s) - { - const float64_t2 position = linePoint.p + direction * currentWorldSpaceOffset; - addShape(position, direction, stretchValue); - currentWorldSpaceOffset += stretchedPatternLen; - } - } - - if (!lineStyle.stretchToFit) - { - // setting next phase shift based on current arc length - const double changeInPhaseShift = glm::fract(lineLen * rcpStretchedPatternLen); - currentPhaseShift = static_cast(glm::fract(currentPhaseShift + changeInPhaseShift)); - } - } - } - else if (section.type == ObjectType::QUAD_BEZIER) - { - const uint32_t quadBezierCount = section.count; - - - // when stretchToFit is true, we need to calculate the whole section arc length to figure out the stretch value needed for stippling phaseshift - float stretchValue = 1.0; - if (lineStyle.stretchToFit) - { - float64_t sectionArcLen = 0.0; - for (uint32_t i = 0u; i < quadBezierCount; i++) - { - const QuadraticBezierInfo& quadBezierInfo = m_quadBeziers[section.index + i]; - nbl::hlsl::shapes::Quadratic quadratic = nbl::hlsl::shapes::Quadratic::constructFromBezier(quadBezierInfo.shape); - nbl::hlsl::shapes::Quadratic::ArcLengthCalculator arcLenCalc = nbl::hlsl::shapes::Quadratic::ArcLengthCalculator::construct(quadratic); - sectionArcLen += arcLenCalc.calcArcLen(1.0); - } - stretchValue = lineStyle.calculateStretchValue(sectionArcLen); - } - - if (lineStyle.stretchToFit) - currentPhaseShift = lineStyle.getStretchedPhaseShift(stretchValue); - - const float rcpStretchedPatternLen = (lineStyle.reciprocalStipplePatternLen) / stretchValue; - - // calculate phase shift at point P0 of each bezier - for (uint32_t i = 0u; i < quadBezierCount; i++) - { - const uint32_t currIdx = section.index + i; - - QuadraticBezierInfo& quadBezierInfo = m_quadBeziers[currIdx]; - quadBezierInfo.phaseShift = currentPhaseShift; - quadBezierInfo.stretchValue = stretchValue; - - if (lineStyle.isRoadStyleFlag) - { - const float32_t2 tangentAtP0 = glm::normalize(quadBezierInfo.shape.derivative(0.0)); - const float32_t2 tangentAtP2 = glm::normalize(quadBezierInfo.shape.derivative(1.0)); - const float64_t2 normalAtP0 = float32_t2(-tangentAtP0.y, tangentAtP0.x); - const float64_t2 normalAtP2 = float32_t2(-tangentAtP2.y, tangentAtP2.x); - if (prevNormal != InvalidNormal && checkIfInDrawSection(lineStyle, currentPhaseShift)) - addMiterIfVisible(prevNormal, normalAtP0, quadBezierInfo.shape.P0); - prevNormal = normalAtP2; - } - - // setting next phase shift based on current arc length - nbl::hlsl::shapes::Quadratic quadratic = nbl::hlsl::shapes::Quadratic::constructFromBezier(quadBezierInfo.shape); - nbl::hlsl::shapes::Quadratic::ArcLengthCalculator arcLenCalc = nbl::hlsl::shapes::Quadratic::ArcLengthCalculator::construct(quadratic); - const double bezierLen = arcLenCalc.calcArcLen(1.0); - - if (shouldAddShapes) - { - float shapeOffsetNormalized = lineStyle.getStretchedShapeNormalizedPlaceInPattern(stretchValue); - - // next shape Offset from start of line/curve - float nextShapeOffset = shapeOffsetNormalized - currentPhaseShift; - if (nextShapeOffset < 0.0f) - nextShapeOffset += 1.0f; - - int32_t numberOfShapes = static_cast(std::ceil(bezierLen * rcpStretchedPatternLen - nextShapeOffset)); // numberOfShapes = (ArcLen - nextShapeOffset*PatternLen)/PatternLen + 1 - - float64_t stretchedPatternLen = 1.0 / (float64_t)rcpStretchedPatternLen; - float64_t currentWorldSpaceOffset = nextShapeOffset * stretchedPatternLen; - for (int32_t s = 0; s < numberOfShapes; ++s) - { - float64_t t = arcLenCalc.calcArcLenInverse(quadratic, 0.0, 1.0, currentWorldSpaceOffset, 1e-5, 0.5); // todo: use discontinuityErrorTolerance here instead of 1e-5? same order? - addShape(quadratic.evaluate(t), quadratic.derivative(t), stretchValue); - currentWorldSpaceOffset += stretchedPatternLen; - } - } - - const double changeInPhaseShift = glm::fract(bezierLen * rcpStretchedPatternLen); - currentPhaseShift = static_cast(glm::fract(currentPhaseShift + changeInPhaseShift)); - } - } - - prevPoint = getSectionLastPoint(section); - } - - if (lineStyle.isRoadStyleFlag && actuallyClosed) - { - const auto& firstSection = m_sections.front(); - - const float32_t2 firstTangent = glm::normalize(getSectionFirstTangent(firstSection)); - const float64_t2 firstNormal = float32_t2(-firstTangent.y, firstTangent.x); - - if (checkIfInDrawSection(lineStyle, lineStyle.phaseShift) && checkIfInDrawSection(lineStyle, currentPhaseShift)) - addMiterIfVisible(prevNormal, firstNormal, prevPoint); - } -} - -// outputs two offsets to the polyline and connects the ends if not closed -CPolyline CPolyline::generateParallelPolyline(float64_t offset, const float64_t maxError) const -{ - // DISCONNECTION DETECTED, will break styling and offsetting the polyline, if you don't care about those then ignore discontinuity. - _NBL_DEBUG_BREAK_IF(!checkSectionsContinuity()); - - // TODO: move to nbl::hlsl later on as convenience function, correctly templated - auto safe_normalize = [](nbl::hlsl::float64_t2 vec) -> nbl::hlsl::float64_t2 - { - const float64_t dirLen = glm::length(vec); - if (dirLen == 0.0) - return nbl::hlsl::float64_t2(0.0, 0.0); - return vec / dirLen; - }; - - CPolyline parallelPolyline = {}; - parallelPolyline.setClosed(m_closedPolygon); - - // The next two lamda functions connect offsetted beziers and lines - // This function adds a bezier section and connects it to the previous section - auto& newSections = parallelPolyline.m_sections; - - constexpr uint32_t InvalidSectionIdx = ~0u; - uint32_t previousLineSectionIdx = InvalidSectionIdx; - constexpr float64_t CROSS_PRODUCT_LINEARITY_EPSILON = 1e-5; - auto connectTwoSections = [&](uint32_t prevSectionIdx, uint32_t nextSectionIdx) - { - auto& prevSection = newSections[prevSectionIdx]; - auto& nextSection = newSections[nextSectionIdx]; - - float64_t2 prevTangent = parallelPolyline.getSectionLastTangent(prevSection); - float64_t2 nextTangent = parallelPolyline.getSectionFirstTangent(nextSection); - const float64_t crossProduct = nbl::hlsl::cross2D(prevTangent, nextTangent); - - if (abs(crossProduct) > CROSS_PRODUCT_LINEARITY_EPSILON) - { - if (crossProduct * offset > 0u) // Outward, needs connection - { - float64_t2 prevSectionEndPos = parallelPolyline.getSectionLastPoint(prevSection); - float64_t2 nextSectionStartPos = parallelPolyline.getSectionFirstPoint(nextSection); - float64_t2 intersection = nbl::hlsl::shapes::util::LineLineIntersection(prevSectionEndPos, prevTangent, nextSectionStartPos, nextTangent); - - if (nextSection.type == ObjectType::LINE) - { - if (prevSection.type == ObjectType::LINE) - { - // Change last point of left segment and first point of right segment to be equal to their intersection. - parallelPolyline.m_linePoints[prevSection.index + prevSection.count].p = intersection; - parallelPolyline.m_linePoints[nextSection.index].p = intersection; - } - else if (prevSection.type == ObjectType::QUAD_BEZIER) - { - // Add QuadBez Position + Intersection to start of right segment - float64_t2 newLinePoints[2u] = { prevSectionEndPos, intersection }; - parallelPolyline.insertLinePointsToSection(nextSectionIdx, 0u, newLinePoints); - } - } - else if (nextSection.type == ObjectType::QUAD_BEZIER) - { - if (prevSection.type == ObjectType::LINE) - { - // Add Intersection + right Segment first line position to end of leftSegment - float64_t2 newLinePoints[2u] = { intersection, nextSectionStartPos }; - parallelPolyline.insertLinePointsToSection(prevSectionIdx, prevSection.count + 1u, newLinePoints); - } - else if (prevSection.type == ObjectType::QUAD_BEZIER) - { - // Add Intersection + right Segment first line position to end of leftSegment - float64_t2 newLinePoints[3u] = { prevSectionEndPos, intersection, nextSectionStartPos }; - - uint32_t linePointsInsertion = 0u; - if (previousLineSectionIdx != InvalidSectionIdx) - linePointsInsertion = newSections[previousLineSectionIdx].index + newSections[previousLineSectionIdx].count + 1u; - - const uint32_t newSectionIdx = prevSectionIdx + 1u; - SectionInfo newSection = {}; - newSection.type = ObjectType::LINE; - newSection.index = linePointsInsertion; - newSection.count = 0u; - newSections.insert(newSections.begin() + newSectionIdx, newSection); - parallelPolyline.insertLinePointsToSection(newSectionIdx, 0u, newLinePoints); - previousLineSectionIdx = newSectionIdx; - } - } - } - else // Inward Needs Trim and Prune - { - SectionIntersectResult sectionIntersectResult = parallelPolyline.intersectTwoSections(prevSection, nextSection); - - if (sectionIntersectResult.valid()) - { - const bool sameSectionIntersection = (prevSectionIdx == nextSectionIdx); - if (sameSectionIntersection) - std::swap(sectionIntersectResult.prevObjIndex, sectionIntersectResult.nextObjIndex); // because `intersectionTwoSections` function prioritizes the largest distance intersection, it will get the indices swapped to get the largest indices distance - - assert(sectionIntersectResult.prevObjIndex < prevSection.count); - assert(sectionIntersectResult.nextObjIndex < nextSection.count); - - // we want to first delete from (idx to end) and then (begin to idx) - parallelPolyline.removeSectionObjectsFromIdxToEnd(prevSectionIdx, sectionIntersectResult.prevObjIndex + 1u); - parallelPolyline.removeSectionObjectsFromBeginToIdx(nextSectionIdx, sectionIntersectResult.nextObjIndex); - const bool removePrevSection = (prevSection.count == 0u); - const bool removeNextSection = (nextSection.count == 0u); - - if (nextSection.type == ObjectType::LINE) - { - if (prevSection.type == ObjectType::LINE) - { - if (!removePrevSection) - parallelPolyline.m_linePoints[prevSection.index + prevSection.count].p = sectionIntersectResult.intersection; - if (!removeNextSection) - parallelPolyline.m_linePoints[nextSection.index].p = sectionIntersectResult.intersection; - } - else if (prevSection.type == ObjectType::QUAD_BEZIER) - { - if (!removePrevSection) - parallelPolyline.m_quadBeziers[prevSection.index + prevSection.count - 1u].shape.splitFromStart(sectionIntersectResult.prevT); - if (!removeNextSection) - parallelPolyline.m_linePoints[nextSection.index].p = sectionIntersectResult.intersection; - } - } - else if (nextSection.type == ObjectType::QUAD_BEZIER) - { - if (prevSection.type == ObjectType::LINE) - { - if (!removePrevSection) - parallelPolyline.m_linePoints[prevSection.index + prevSection.count].p = sectionIntersectResult.intersection; - if (!removeNextSection) - parallelPolyline.m_quadBeziers[nextSection.index].shape.splitToEnd(sectionIntersectResult.nextT); - } - else if (prevSection.type == ObjectType::QUAD_BEZIER) - { - if (!removePrevSection) - parallelPolyline.m_quadBeziers[prevSection.index + prevSection.count - 1u].shape.splitFromStart(sectionIntersectResult.prevT); - if (!removeNextSection) - parallelPolyline.m_quadBeziers[nextSection.index].shape.splitToEnd(sectionIntersectResult.nextT); - } - } - - // Remove Sections that got their whole objects removed - if (nextSectionIdx >= prevSectionIdx) - { - // we want to first delete the higher idx - if (removeNextSection) - parallelPolyline.m_sections.erase(parallelPolyline.m_sections.begin() + nextSectionIdx); - if (removePrevSection && !sameSectionIntersection) - parallelPolyline.m_sections.erase(parallelPolyline.m_sections.begin() + prevSectionIdx); - } - else - { - if (removePrevSection) - parallelPolyline.m_sections.erase(parallelPolyline.m_sections.begin() + prevSectionIdx); - if (removeNextSection) - parallelPolyline.m_sections.erase(parallelPolyline.m_sections.begin() + nextSectionIdx); - } - } - else - { - // TODO: If Polyline is continuous, and the tangents are reported correctly this shouldn't happen - } - } - } - }; - auto connectBezierSection = [&](std::vector>&& beziers) - { - parallelPolyline.addQuadBeziers(beziers); - // If there is a previous section, connect to that - if (newSections.size() > 1u) - { - const uint32_t prevSectionIdx = newSections.size() - 2u; - const uint32_t nextSectionIdx = newSections.size() - 1u; - connectTwoSections(prevSectionIdx, nextSectionIdx); - } - }; - auto connectLinesSection = [&](std::vector&& linePoints) - { - parallelPolyline.addLinePoints(linePoints); - // If there is a previous section, connect to that - if (newSections.size() > 1u) - { - const uint32_t prevSectionIdx = newSections.size() - 2u; - const uint32_t nextSectionIdx = newSections.size() - 1u; - connectTwoSections(prevSectionIdx, nextSectionIdx); - } - previousLineSectionIdx = newSections.size() - 1u; - }; - - // This loop Generates Mitered Line Sections and Offseted Beziers -> will still have breaks and disconnections - // then we call addBezierSection and it connects it to the previous section - for (uint32_t i = 0; i < m_sections.size(); ++i) - { - const auto& section = m_sections[i]; - if (section.type == ObjectType::LINE) - { - // TODO: try merging lines if they have same tangent (resultin in less points) - std::vector newLinePoints; - newLinePoints.reserve(section.count + 1); - for (uint32_t j = 0; j < section.count + 1; ++j) - { - const uint32_t linePointIdx = section.index + j; - float64_t2 offsetVector; - if (j == 0) - { - const float64_t2 tangent = safe_normalize(m_linePoints[linePointIdx + 1].p - m_linePoints[linePointIdx].p); - offsetVector = float64_t2(tangent.y, -tangent.x); - } - else if (j == section.count) - { - const float64_t2 tangent = safe_normalize(m_linePoints[linePointIdx].p - m_linePoints[linePointIdx - 1].p); - offsetVector = float64_t2(tangent.y, -tangent.x); - } - else - { - const float64_t2 tangentPrevLine = safe_normalize(m_linePoints[linePointIdx].p - m_linePoints[linePointIdx - 1].p); - const float64_t2 normalPrevLine = float64_t2(tangentPrevLine.y, -tangentPrevLine.x); - const float64_t2 tangentNextLine = safe_normalize(m_linePoints[linePointIdx + 1].p - m_linePoints[linePointIdx].p); - const float64_t2 normalNextLine = float64_t2(tangentNextLine.y, -tangentNextLine.x); - - const float64_t2 intersectionDirection = safe_normalize(normalPrevLine + normalNextLine); - const float64_t cosAngleBetweenNormals = glm::dot(normalPrevLine, normalNextLine); - offsetVector = intersectionDirection * sqrt(2.0 / (1.0 + cosAngleBetweenNormals)); - } - newLinePoints.push_back(m_linePoints[linePointIdx].p + offsetVector * offset); - } - connectLinesSection(std::move(newLinePoints)); - } - else if (section.type == ObjectType::QUAD_BEZIER) - { - std::vector> newBeziers; - curves::Subdivision::AddBezierFunc addToBezier = [&](nbl::hlsl::shapes::QuadraticBezier&& info) -> void - { - newBeziers.push_back(info); - }; - for (uint32_t j = 0; j < section.count; ++j) - { - const uint32_t bezierIdx = section.index + j; - curves::OffsettedBezier offsettedBezier(m_quadBeziers[bezierIdx].shape, offset); - curves::Subdivision::adaptive(offsettedBezier, maxError, addToBezier, 10u); - } - connectBezierSection(std::move(newBeziers)); - } - } - - if (parallelPolyline.m_closedPolygon) - { - const uint32_t prevSectionIdx = newSections.size() - 1u; - const uint32_t nextSectionIdx = 0u; - connectTwoSections(prevSectionIdx, nextSectionIdx); - } - - return parallelPolyline; -} - -void CPolyline::makeWideWhole(CPolyline& outOffset1, CPolyline& outOffset2, float64_t offset, const float64_t maxError) const -{ - outOffset1 = generateParallelPolyline(offset, maxError); - outOffset2 = generateParallelPolyline(-1.0 * offset, maxError); - - if (!m_closedPolygon) - { - if (outOffset1.getSectionsCount() == 0u || outOffset2.getSectionsCount() == 0u) - return; - - nbl::hlsl::float64_t2 beginToBeginConnector[2u]; - beginToBeginConnector[0u] = outOffset1.getSectionFirstPoint(outOffset1.getSectionInfoAt(0u)); - beginToBeginConnector[1u] = outOffset2.getSectionFirstPoint(outOffset2.getSectionInfoAt(0u)); - nbl::hlsl::float64_t2 endToEndConnector[2u]; - endToEndConnector[0u] = outOffset1.getSectionLastPoint(outOffset1.getSectionInfoAt(outOffset1.getSectionsCount() - 1u)); - endToEndConnector[1u] = outOffset2.getSectionLastPoint(outOffset2.getSectionInfoAt(outOffset2.getSectionsCount() - 1u)); - outOffset2.addLinePoints({ beginToBeginConnector, beginToBeginConnector + 2 }); - outOffset2.addLinePoints({ endToEndConnector, endToEndConnector + 2 }); - } -} - -void CPolyline::stippleBreakDown(const LineStyleInfo& lineStyle, const OutputPolylineFunc& addPolyline, float64_t discontinuityErrorTolerance) const -{ - if (!lineStyle.isVisible()) - return; - - // currently only works for road styles with only 2 stipple values (1 draw, 1 gap) - assert(lineStyle.stipplePatternSize <= 1); - - const float64_t patternLen = 1.0 / lineStyle.reciprocalStipplePatternLen; - const float32_t drawSectionNormalizedLen = lineStyle.stipplePattern[0]; - const float32_t gapSectionNormalizedLen = 1.0 - lineStyle.stipplePattern[0]; - const float32_t drawSectionLen = drawSectionNormalizedLen * patternLen; - const bool allSolid = drawSectionNormalizedLen == 1.0f; - const bool continous = checkSectionsContinuity(discontinuityErrorTolerance); - - if (allSolid && continous) - { - addPolyline(*this); // optimization to avoid copying and processing each individual line and bezier again. - return; - } - - // To detect gaps and flush - const float64_t2 DiscontinuityErrorTolerance = float64_t2(discontinuityErrorTolerance, discontinuityErrorTolerance); - float64_t2 prevPoint = float64_t2(nbl::hlsl::numeric_limits::infinity, nbl::hlsl::numeric_limits::infinity); - - CPolyline currentPolyline; - std::vector linePoints; - std::vector> beziers; - auto flushCurrentPolyline = [&]() - { - if (linePoints.size() > 1u) - { - currentPolyline.addLinePoints({ linePoints.data(), linePoints.data() + linePoints.size() }); - linePoints.clear(); - } - if (beziers.size() > 0u) - { - currentPolyline.addQuadBeziers({ beziers.data(), beziers.data() + beziers.size() }); - beziers.clear(); - } - if (currentPolyline.getSectionsCount() > 0u) - addPolyline(currentPolyline); - currentPolyline.clearEverything(); - }; - auto pushBackToLinePoints = [&](const float64_t2& point) - { - if (linePoints.empty()) - linePoints.push_back(point); - else if (linePoints.back() != point) - linePoints.push_back(point); - }; - - float currentPhaseShift = lineStyle.phaseShift; - for (uint32_t sectionIdx = 0u; sectionIdx < m_sections.size(); sectionIdx++) - { - const auto& section = m_sections[sectionIdx]; - - // if gap detected - if (sectionIdx > 0u && glm::any(glm::greaterThan(glm::abs(getSectionFirstPoint(section) - prevPoint), DiscontinuityErrorTolerance))) - flushCurrentPolyline(); - - if (section.type == ObjectType::LINE) - { - // calculate phase shift at each point of each line in section - const uint32_t lineCount = section.count; - for (uint32_t i = 0u; i < lineCount; i++) - { - const uint32_t currIdx = section.index + i; - const auto& currlinePoint = m_linePoints[currIdx]; - const auto& nextLinePoint = m_linePoints[currIdx + 1u]; - - if (allSolid) - { - pushBackToLinePoints(currlinePoint.p); - continue; - } - - const float64_t2 lineVector = nextLinePoint.p - currlinePoint.p; - const float64_t lineLen = glm::length(lineVector); - const float64_t2 lineVectorNormalized = lineVector / lineLen; - - float64_t currentTracedLen = 0.0; - const float32_t differenceToNextDrawSectionEnd = drawSectionNormalizedLen - currentPhaseShift; - const bool insideDrawSection = differenceToNextDrawSectionEnd > 0.0f; - - // Handle beginning of the line if it's inside a draw section and draw it partially based on currentPhaseShift - if (insideDrawSection) - { - const float64_t nextDrawSectionEnd = differenceToNextDrawSectionEnd * patternLen; - const bool finishesOnThisShape = nextDrawSectionEnd <= lineLen; - - pushBackToLinePoints(currlinePoint.p); - - if (finishesOnThisShape) - { - pushBackToLinePoints(currlinePoint.p + nextDrawSectionEnd * lineVectorNormalized); - flushCurrentPolyline(); - } - else - { - pushBackToLinePoints(nextLinePoint.p); - } - currentTracedLen = nbl::core::min(nextDrawSectionEnd, lineLen); - } - - const float32_t currentTracedLenPlaceInPattern = glm::fract(currentPhaseShift + currentTracedLen * lineStyle.reciprocalStipplePatternLen); - const float32_t differenceToNextDrawSectionBegin = glm::fract(1.0 - currentTracedLenPlaceInPattern); // 0.0 gives 0.0, and 1.0 gives 0.0 - const float64_t lenToNextDrawBegin = differenceToNextDrawSectionBegin * patternLen; - currentTracedLen = nbl::core::min(currentTracedLen + lenToNextDrawBegin, lineLen); - const float64_t remainingLen = lineLen - currentTracedLen; - - // Handle the rest of the line and draw full patterns fitting inside this line, last one may partially belongs to the next line or section. - if (remainingLen > 0.0) - { - float64_t remainingLenNormalized = remainingLen * lineStyle.reciprocalStipplePatternLen; - int32_t fullPatternsFitNumber = static_cast(std::ceil(remainingLenNormalized)); - linePoints.reserve(linePoints.size() + fullPatternsFitNumber * 2u); - - for (int32_t s = 0; s < fullPatternsFitNumber; ++s) - { - const bool completelyInsideShape = (currentTracedLen + drawSectionLen) <= lineLen; - const float64_t nextDrawSectionEnd = (completelyInsideShape) ? (currentTracedLen + drawSectionLen) : lineLen; - - pushBackToLinePoints(currlinePoint.p + currentTracedLen * lineVectorNormalized); - pushBackToLinePoints(currlinePoint.p + nextDrawSectionEnd * lineVectorNormalized); - - if (completelyInsideShape) - flushCurrentPolyline(); - - currentTracedLen = nbl::core::min(currentTracedLen + patternLen, lineLen); - } - } - - const double changeInPhaseShift = glm::fract(lineLen * lineStyle.reciprocalStipplePatternLen); - currentPhaseShift = static_cast(glm::fract(currentPhaseShift + changeInPhaseShift)); - } - - if (allSolid) - pushBackToLinePoints(m_linePoints[section.index + lineCount].p); - - currentPolyline.addLinePoints({ linePoints.data(), linePoints.data() + linePoints.size() }); - linePoints.clear(); - } - else if (section.type == ObjectType::QUAD_BEZIER) - { - const uint32_t quadBezierCount = section.count; - - // calculate phase shift at point P0 of each bezier - for (uint32_t i = 0u; i < quadBezierCount; i++) - { - const uint32_t currIdx = section.index + i; - const QuadraticBezierInfo& quadBezierInfo = m_quadBeziers[currIdx]; - - if (allSolid) - { - beziers.push_back(quadBezierInfo.shape); - continue; - } - - // setting next phase shift based on current arc length - nbl::hlsl::shapes::Quadratic quadratic = nbl::hlsl::shapes::Quadratic::constructFromBezier(quadBezierInfo.shape); - nbl::hlsl::shapes::Quadratic::ArcLengthCalculator arcLenCalc = nbl::hlsl::shapes::Quadratic::ArcLengthCalculator::construct(quadratic); - const double bezierLen = arcLenCalc.calcArcLen(1.0); - - float64_t currentTracedLen = 0.0; - const float32_t differenceToNextDrawSectionEnd = drawSectionNormalizedLen - currentPhaseShift; - const bool insideDrawSection = differenceToNextDrawSectionEnd > 0.0f; - if (insideDrawSection) - { - const float64_t nextDrawSectionEnd = differenceToNextDrawSectionEnd * patternLen; - const bool finishesOnThisShape = nextDrawSectionEnd <= bezierLen; - - auto newBezier = quadBezierInfo.shape; - if (finishesOnThisShape) - { - float64_t t = arcLenCalc.calcArcLenInverse(quadratic, 0.0, 1.0, nextDrawSectionEnd, 1e-5, 0.5); - newBezier.splitFromStart(t); - beziers.push_back(std::move(newBezier)); - flushCurrentPolyline(); - } - else - { - // draw section covers entire bezier, no need for clipping - beziers.push_back(std::move(newBezier)); - } - - currentTracedLen = nbl::core::min(nextDrawSectionEnd, bezierLen); - } - - const float32_t currentTracedLenPlaceInPattern = glm::fract(currentPhaseShift + currentTracedLen * lineStyle.reciprocalStipplePatternLen); - const float32_t differenceToNextDrawSectionBegin = glm::fract(1.0 - currentTracedLenPlaceInPattern); // 0.0 gives 0.0, and 1.0 gives 0.0 - const float64_t lenToNextDrawBegin = differenceToNextDrawSectionBegin * patternLen; - currentTracedLen = nbl::core::min(currentTracedLen + lenToNextDrawBegin, bezierLen); - const float64_t remainingLen = bezierLen - currentTracedLen; - - if (remainingLen > 0.0) - { - float64_t remainingLenNormalized = remainingLen * lineStyle.reciprocalStipplePatternLen; - int32_t fullPatternsFitNumber = static_cast(std::ceil(remainingLenNormalized)); - beziers.reserve(beziers.size() + fullPatternsFitNumber); - - for (int32_t s = 0; s < fullPatternsFitNumber; ++s) - { - const bool completelyInsideShape = (currentTracedLen + drawSectionLen) <= bezierLen; - const float64_t nextDrawSectionEnd = (completelyInsideShape) ? (currentTracedLen + drawSectionLen) : bezierLen; - float64_t tStart = arcLenCalc.calcArcLenInverse(quadratic, 0.0, 1.0, currentTracedLen, 1e-5, 0.5); - float64_t tEnd = arcLenCalc.calcArcLenInverse(quadratic, 0.0, 1.0, nextDrawSectionEnd, 1e-5, 0.5); - - auto newBezier = quadBezierInfo.shape; - newBezier.splitFromMinToMax(tStart, tEnd); - beziers.push_back(std::move(newBezier)); - - if (completelyInsideShape) - flushCurrentPolyline(); - - currentTracedLen = nbl::core::min(currentTracedLen + patternLen, bezierLen); - } - } - - const double changeInPhaseShift = glm::fract(bezierLen * lineStyle.reciprocalStipplePatternLen); - currentPhaseShift = static_cast(glm::fract(currentPhaseShift + changeInPhaseShift)); - } - - if (allSolid) - beziers.push_back(m_quadBeziers[section.index + quadBezierCount - 1u].shape); - - currentPolyline.addQuadBeziers({ beziers.data(), beziers.data() + beziers.size() }); - beziers.clear(); - } - - prevPoint = getSectionLastPoint(section); - } - - flushCurrentPolyline(); -} \ No newline at end of file diff --git a/62_CAD/Polyline.h b/62_CAD/Polyline.h index 31ba9eb15..b41d8a4cc 100644 --- a/62_CAD/Polyline.h +++ b/62_CAD/Polyline.h @@ -7,14 +7,7 @@ #include #include "Curves.h" -struct PolylineSettings -{ - // Reset Line Style after gaps - static constexpr bool ResetLineStyleOnDiscontinuity = false; -}; - -// holds values for `LineStyle` struct and caculates stipple pattern processed values, cant think of better name -// Also used for TextStyles aliased with some members here. (temporarily?) +// holds values for `LineStyle` struct and caculates stipple pattern re values, cant think of better name struct LineStyleInfo { static constexpr int32_t InvalidStipplePatternSize = -1; @@ -23,9 +16,9 @@ struct LineStyleInfo static constexpr float PatternEpsilon = 1e-3f; // TODO: I think for phase shift in normalized stipple space this is a reasonable value? right? static const uint32_t StipplePatternMaxSize = LineStyle::StipplePatternMaxSize; - float32_t4 color = {}; - float screenSpaceLineWidth = 0.0f; // alternatively used as TextStyle::italicTiltSlope - float worldSpaceLineWidth = 0.0f; // alternatively used as TextStyle::boldInPixels + float32_t4 color; + float screenSpaceLineWidth; + float worldSpaceLineWidth; /* Stippling Values: @@ -66,6 +59,8 @@ struct LineStyleInfo rigidSegmentIdx = InvalidRigidSegmentIndex; phaseShift = 0.0f; + assert(stipplePatternUnnormalizedRepresentation.size() <= StipplePatternMaxSize); + if (stipplePatternUnnormalizedRepresentation.size() == 0) { stipplePatternSize = 0; @@ -108,8 +103,6 @@ struct LineStyleInfo stipplePatternTransformed[0] += stipplePatternTransformed[stipplePatternTransformed.size() - 1]; stipplePatternTransformed.pop_back(); } - - assert(stipplePatternTransformed.size() <= StipplePatternMaxSize); if (stipplePatternTransformed.size() != 1) { @@ -355,8 +348,7 @@ class CPolylineBase virtual const QuadraticBezierInfo& getQuadBezierInfoAt(const uint32_t idx) const = 0; virtual const LinePointInfo& getLinePointAt(const uint32_t idx) const = 0; virtual std::span getConnectors() const = 0; - virtual bool checkSectionsContinuity(float64_t discontinuityErrorTolerance = 1e-5) const = 0; - virtual bool checkSectionsActuallyClosed(float64_t discontinuityErrorTolerance = 1e-5) const = 0; + virtual bool checkSectionsContinuity() const = 0; }; // It is not optimized because how you feed a Polyline to our cad renderer is your choice. this is just for convenience @@ -366,7 +358,7 @@ class CPolyline : public CPolylineBase public: CPolyline() : m_Min(float64_t2(nbl::hlsl::numeric_limits::max, nbl::hlsl::numeric_limits::max)), - m_Max(float64_t2(nbl::hlsl::numeric_limits::lowest, nbl::hlsl::numeric_limits::lowest)), + m_Max(float64_t2(nbl::hlsl::numeric_limits::min, nbl::hlsl::numeric_limits::min)), m_closedPolygon(false) {} @@ -391,30 +383,20 @@ class CPolyline : public CPolylineBase { return m_polylineConnector; } - - // Check for Gap/Discontinuity, it won't affect complex styling but will affect the polyline offsetting, if you don't care about those then ignore checking for discontinuity. - bool checkSectionsContinuity(float64_t discontinuityErrorTolerance = 1e-5) const override + + bool checkSectionsContinuity() const override { - const float64_t2 DiscontinuityErrorTolerance = float64_t2(discontinuityErrorTolerance, discontinuityErrorTolerance); // Check for continuity for (uint32_t i = 1; i < m_sections.size(); ++i) { + constexpr float64_t POINT_EQUALITY_THRESHOLD = 1e-12; const float64_t2 firstPoint = getSectionFirstPoint(m_sections[i]); const float64_t2 prevLastPoint = getSectionLastPoint(m_sections[i - 1u]); - if (glm::any(glm::greaterThan(glm::abs(firstPoint - prevLastPoint), DiscontinuityErrorTolerance))) // checking individual components rather than dist. + if (glm::distance(firstPoint, prevLastPoint) > POINT_EQUALITY_THRESHOLD) + { + // DISCONNECTION DETECTED, will break styling and offsetting the polyline, if you don't care about those then ignore discontinuity. return false; - } - return true; - } - - bool checkSectionsActuallyClosed(float64_t discontinuityErrorTolerance = 1e-5) const override - { - const float64_t2 DiscontinuityErrorTolerance = float64_t2(discontinuityErrorTolerance, discontinuityErrorTolerance); - if (m_sections.size() > 0u) - { - const float64_t2 firstPoint = getSectionFirstPoint(m_sections.front()); - const float64_t2 lastPoint = getSectionLastPoint(m_sections.back()); - return glm::all(glm::lessThan(glm::abs(firstPoint - lastPoint), DiscontinuityErrorTolerance)); + } } return true; } @@ -523,7 +505,149 @@ class CPolyline : public CPolylineBase typedef std::function AddShapeFunc; - void preprocessPolylineWithStyle(const LineStyleInfo& lineStyle, float64_t discontinuityErrorTolerance = 1e-5, const AddShapeFunc& addShape = {}); + void preprocessPolylineWithStyle(const LineStyleInfo& lineStyle, const AddShapeFunc& addShape = {}) + { + if (lineStyle.skipPreprocess()) + return; + // DISCONNECTION DETECTED, will break styling and offsetting the polyline, if you don't care about those then ignore discontinuity. + // _NBL_DEBUG_BREAK_IF(!checkSectionsContinuity()); + PolylineConnectorBuilder connectorBuilder; + + const bool shouldAddShapes = (lineStyle.hasShape() && addShape.operator bool()); + // When stretchToFit is true, the curve section and individual lines should start from the beginning of the pattern (phaseShift = lineStyle.phaseShift) + const bool patternStartAgain = lineStyle.stretchToFit; + float currentPhaseShift = lineStyle.phaseShift; + + for (uint32_t sectionIdx = 0u; sectionIdx < m_sections.size(); sectionIdx++) + { + const auto& section = m_sections[sectionIdx]; + + if (section.type == ObjectType::LINE) + { + // calculate phase shift at each point of each line in section + const uint32_t lineCount = section.count; + for (uint32_t i = 0u; i < lineCount; i++) + { + const uint32_t currIdx = section.index + i; + auto& linePoint = m_linePoints[currIdx]; + const auto& nextLinePoint = m_linePoints[currIdx + 1u]; + const float64_t2 lineVector = nextLinePoint.p - linePoint.p; + const float64_t lineLen = glm::length(lineVector); + const float32_t stretchValue = lineStyle.calculateStretchValue(lineLen); + const float rcpStretchedPatternLen = (lineStyle.reciprocalStipplePatternLen) / stretchValue; + + if (patternStartAgain) + currentPhaseShift = lineStyle.getStretchedPhaseShift(stretchValue); + + linePoint.phaseShift = currentPhaseShift; + linePoint.stretchValue = stretchValue; + + if (lineStyle.isRoadStyleFlag) + connectorBuilder.addLineNormal(lineVector, lineLen, linePoint.p, currentPhaseShift); + + if (shouldAddShapes) + { + float shapeOffsetNormalized = lineStyle.getStretchedShapeNormalizedPlaceInPattern(stretchValue); + // next shape Offset from start of line/curve + float nextShapeOffset = shapeOffsetNormalized - currentPhaseShift; + if (nextShapeOffset < 0.0f) + nextShapeOffset += 1.0f; + + int32_t numberOfShapes = static_cast(std::ceil(lineLen * rcpStretchedPatternLen - nextShapeOffset)); // numberOfShapes = (ArcLen - nextShapeOffset*PatternLen)/PatternLen + 1 + + float64_t stretchedPatternLen = 1.0 / (float64_t)rcpStretchedPatternLen; + float64_t currentWorldSpaceOffset = nextShapeOffset * stretchedPatternLen; + float64_t2 direction = lineVector / lineLen; + for (int32_t s = 0; s < numberOfShapes; ++s) + { + const float64_t2 position = linePoint.p + direction * currentWorldSpaceOffset; + addShape(position, direction, stretchValue); + currentWorldSpaceOffset += stretchedPatternLen; + } + } + + if (!patternStartAgain) + { + // setting next phase shift based on current arc length + const double changeInPhaseShift = glm::fract(lineLen * rcpStretchedPatternLen); + currentPhaseShift = static_cast(glm::fract(currentPhaseShift + changeInPhaseShift)); + } + } + } + else if (section.type == ObjectType::QUAD_BEZIER) + { + const uint32_t quadBezierCount = section.count; + + // when stretchToFit is true, we need to calculate the whole section arc length to figure out the stretch value needed for stippling phaseshift + float stretchValue = 1.0; + if (lineStyle.stretchToFit) + { + float64_t sectionArcLen = 0.0; + for (uint32_t i = 0u; i < quadBezierCount; i++) + { + const QuadraticBezierInfo& quadBezierInfo = m_quadBeziers[section.index + i]; + nbl::hlsl::shapes::Quadratic quadratic = nbl::hlsl::shapes::Quadratic::constructFromBezier(quadBezierInfo.shape); + nbl::hlsl::shapes::Quadratic::ArcLengthCalculator arcLenCalc = nbl::hlsl::shapes::Quadratic::ArcLengthCalculator::construct(quadratic); + sectionArcLen += arcLenCalc.calcArcLen(1.0); + } + stretchValue = lineStyle.calculateStretchValue(sectionArcLen); + } + + if (patternStartAgain) + currentPhaseShift = lineStyle.getStretchedPhaseShift(stretchValue); + + const float rcpStretchedPatternLen = (lineStyle.reciprocalStipplePatternLen) / stretchValue; + + // calculate phase shift at point P0 of each bezier + for (uint32_t i = 0u; i < quadBezierCount; i++) + { + const uint32_t currIdx = section.index + i; + + QuadraticBezierInfo& quadBezierInfo = m_quadBeziers[currIdx]; + quadBezierInfo.phaseShift = currentPhaseShift; + quadBezierInfo.stretchValue = stretchValue; + + if (lineStyle.isRoadStyleFlag) + connectorBuilder.addBezierNormals(m_quadBeziers[currIdx], currentPhaseShift); + + // setting next phase shift based on current arc length + nbl::hlsl::shapes::Quadratic quadratic = nbl::hlsl::shapes::Quadratic::constructFromBezier(quadBezierInfo.shape); + nbl::hlsl::shapes::Quadratic::ArcLengthCalculator arcLenCalc = nbl::hlsl::shapes::Quadratic::ArcLengthCalculator::construct(quadratic); + const double bezierLen = arcLenCalc.calcArcLen(1.0); + + if (shouldAddShapes) + { + float shapeOffsetNormalized = lineStyle.getStretchedShapeNormalizedPlaceInPattern(stretchValue); + + // next shape Offset from start of line/curve + float nextShapeOffset = shapeOffsetNormalized - currentPhaseShift; + if (nextShapeOffset < 0.0f) + nextShapeOffset += 1.0f; + + int32_t numberOfShapes = static_cast(std::ceil(bezierLen * rcpStretchedPatternLen - nextShapeOffset)); // numberOfShapes = (ArcLen - nextShapeOffset*PatternLen)/PatternLen + 1 + + float64_t stretchedPatternLen = 1.0 / (float64_t)rcpStretchedPatternLen; + float64_t currentWorldSpaceOffset = nextShapeOffset * stretchedPatternLen; + for (int32_t s = 0; s < numberOfShapes; ++s) + { + float64_t t = arcLenCalc.calcArcLenInverse(quadratic, 0.0, 1.0, currentWorldSpaceOffset, 1e-5, 0.5); + addShape(quadratic.evaluate(t), quadratic.derivative(t), stretchValue); + currentWorldSpaceOffset += stretchedPatternLen; + } + } + + const double changeInPhaseShift = glm::fract(bezierLen * rcpStretchedPatternLen); + currentPhaseShift = static_cast(glm::fract(currentPhaseShift + changeInPhaseShift)); + } + } + } + + if (lineStyle.isRoadStyleFlag) + { + connectorBuilder.setPhaseShiftAtEndOfPolyline(currentPhaseShift); + m_polylineConnector = connectorBuilder.buildConnectors(lineStyle, m_closedPolygon); + } + } float64_t2 getSectionFirstPoint(const SectionInfo& section) const { @@ -565,6 +689,7 @@ class CPolyline : public CPolylineBase float64_t2 getSectionFirstTangent(const SectionInfo& section) const { + if (section.type == ObjectType::LINE) { const uint32_t firstLinePointIdx = section.index; @@ -601,15 +726,469 @@ class CPolyline : public CPolylineBase } } - CPolyline generateParallelPolyline(float64_t offset, const float64_t maxError = 1e-5) const; + CPolyline generateParallelPolyline(float64_t offset, const float64_t maxError = 1e-5) const + { + // DISCONNECTION DETECTED, will break styling and offsetting the polyline, if you don't care about those then ignore discontinuity. + _NBL_DEBUG_BREAK_IF(!checkSectionsContinuity()); + + CPolyline parallelPolyline = {}; + parallelPolyline.setClosed(m_closedPolygon); + + // The next two lamda functions connect offsetted beziers and lines + // This function adds a bezier section and connects it to the previous section + auto& newSections = parallelPolyline.m_sections; + + constexpr uint32_t InvalidSectionIdx = ~0u; + uint32_t previousLineSectionIdx = InvalidSectionIdx; + constexpr float64_t CROSS_PRODUCT_LINEARITY_EPSILON = 1e-5; + auto connectTwoSections = [&](uint32_t prevSectionIdx, uint32_t nextSectionIdx) + { + auto& prevSection = newSections[prevSectionIdx]; + auto& nextSection = newSections[nextSectionIdx]; + + float64_t2 prevTangent = parallelPolyline.getSectionLastTangent(prevSection); + float64_t2 nextTangent = parallelPolyline.getSectionFirstTangent(nextSection); + const float64_t crossProduct = nbl::hlsl::cross2D(prevTangent, nextTangent); + + if (abs(crossProduct) > CROSS_PRODUCT_LINEARITY_EPSILON) + { + if (crossProduct * offset > 0u) // Outward, needs connection + { + float64_t2 prevSectionEndPos = parallelPolyline.getSectionLastPoint(prevSection); + float64_t2 nextSectionStartPos = parallelPolyline.getSectionFirstPoint(nextSection); + float64_t2 intersection = nbl::hlsl::shapes::util::LineLineIntersection(prevSectionEndPos, prevTangent, nextSectionStartPos, nextTangent); + + if (nextSection.type == ObjectType::LINE) + { + if (prevSection.type == ObjectType::LINE) + { + // Change last point of left segment and first point of right segment to be equal to their intersection. + parallelPolyline.m_linePoints[prevSection.index + prevSection.count].p = intersection; + parallelPolyline.m_linePoints[nextSection.index].p = intersection; + } + else if (prevSection.type == ObjectType::QUAD_BEZIER) + { + // Add QuadBez Position + Intersection to start of right segment + float64_t2 newLinePoints[2u] = { prevSectionEndPos, intersection }; + parallelPolyline.insertLinePointsToSection(nextSectionIdx, 0u, newLinePoints); + } + } + else if (nextSection.type == ObjectType::QUAD_BEZIER) + { + if (prevSection.type == ObjectType::LINE) + { + // Add Intersection + right Segment first line position to end of leftSegment + float64_t2 newLinePoints[2u] = { intersection, nextSectionStartPos }; + parallelPolyline.insertLinePointsToSection(prevSectionIdx, prevSection.count + 1u, newLinePoints); + } + else if (prevSection.type == ObjectType::QUAD_BEZIER) + { + // Add Intersection + right Segment first line position to end of leftSegment + float64_t2 newLinePoints[3u] = { prevSectionEndPos, intersection, nextSectionStartPos }; + + uint32_t linePointsInsertion = 0u; + if (previousLineSectionIdx != InvalidSectionIdx) + linePointsInsertion = newSections[previousLineSectionIdx].index + newSections[previousLineSectionIdx].count + 1u; + + const uint32_t newSectionIdx = prevSectionIdx + 1u; + SectionInfo newSection = {}; + newSection.type = ObjectType::LINE; + newSection.index = linePointsInsertion; + newSection.count = 0u; + newSections.insert(newSections.begin() + newSectionIdx, newSection); + parallelPolyline.insertLinePointsToSection(newSectionIdx, 0u, newLinePoints); + previousLineSectionIdx = newSectionIdx; + } + } + } + else // Inward Needs Trim and Prune + { + SectionIntersectResult sectionIntersectResult = parallelPolyline.intersectTwoSections(prevSection, nextSection); + + if (sectionIntersectResult.valid()) + { + const bool sameSectionIntersection = (prevSectionIdx == nextSectionIdx); + if (sameSectionIntersection) + std::swap(sectionIntersectResult.prevObjIndex, sectionIntersectResult.nextObjIndex); // because `intersectionTwoSections` function prioritizes the largest distance intersection, it will get the indices swapped to get the largest indices distance + + assert(sectionIntersectResult.prevObjIndex < prevSection.count); + assert(sectionIntersectResult.nextObjIndex < nextSection.count); + + // we want to first delete from (idx to end) and then (begin to idx) + parallelPolyline.removeSectionObjectsFromIdxToEnd(prevSectionIdx, sectionIntersectResult.prevObjIndex + 1u); + parallelPolyline.removeSectionObjectsFromBeginToIdx(nextSectionIdx, sectionIntersectResult.nextObjIndex); + const bool removePrevSection = (prevSection.count == 0u); + const bool removeNextSection = (nextSection.count == 0u); + + if (nextSection.type == ObjectType::LINE) + { + if (prevSection.type == ObjectType::LINE) + { + if (!removePrevSection) + parallelPolyline.m_linePoints[prevSection.index + prevSection.count].p = sectionIntersectResult.intersection; + if (!removeNextSection) + parallelPolyline.m_linePoints[nextSection.index].p = sectionIntersectResult.intersection; + } + else if (prevSection.type == ObjectType::QUAD_BEZIER) + { + if (!removePrevSection) + parallelPolyline.m_quadBeziers[prevSection.index + prevSection.count - 1u].shape.splitFromStart(sectionIntersectResult.prevT); + if (!removeNextSection) + parallelPolyline.m_linePoints[nextSection.index].p = sectionIntersectResult.intersection; + } + } + else if (nextSection.type == ObjectType::QUAD_BEZIER) + { + if (prevSection.type == ObjectType::LINE) + { + if (!removePrevSection) + parallelPolyline.m_linePoints[prevSection.index + prevSection.count].p = sectionIntersectResult.intersection; + if (!removeNextSection) + parallelPolyline.m_quadBeziers[nextSection.index].shape.splitToEnd(sectionIntersectResult.nextT); + } + else if (prevSection.type == ObjectType::QUAD_BEZIER) + { + if (!removePrevSection) + parallelPolyline.m_quadBeziers[prevSection.index + prevSection.count - 1u].shape.splitFromStart(sectionIntersectResult.prevT); + if (!removeNextSection) + parallelPolyline.m_quadBeziers[nextSection.index].shape.splitToEnd(sectionIntersectResult.nextT); + } + } + + // Remove Sections that got their whole objects removed + if (nextSectionIdx >= prevSectionIdx) + { + // we want to first delete the higher idx + if (removeNextSection) + parallelPolyline.m_sections.erase(parallelPolyline.m_sections.begin() + nextSectionIdx); + if (removePrevSection && !sameSectionIntersection) + parallelPolyline.m_sections.erase(parallelPolyline.m_sections.begin() + prevSectionIdx); + } + else + { + if (removePrevSection) + parallelPolyline.m_sections.erase(parallelPolyline.m_sections.begin() + prevSectionIdx); + if (removeNextSection) + parallelPolyline.m_sections.erase(parallelPolyline.m_sections.begin() + nextSectionIdx); + } + } + else + { + // TODO: If Polyline is continuous, and the tangents are reported correctly this shouldn't happen + } + } + } + }; + auto connectBezierSection = [&](std::vector>&& beziers) + { + parallelPolyline.addQuadBeziers(beziers); + // If there is a previous section, connect to that + if (newSections.size() > 1u) + { + const uint32_t prevSectionIdx = newSections.size() - 2u; + const uint32_t nextSectionIdx = newSections.size() - 1u; + connectTwoSections(prevSectionIdx, nextSectionIdx); + } + }; + auto connectLinesSection = [&](std::vector&& linePoints) + { + parallelPolyline.addLinePoints(linePoints); + // If there is a previous section, connect to that + if (newSections.size() > 1u) + { + const uint32_t prevSectionIdx = newSections.size() - 2u; + const uint32_t nextSectionIdx = newSections.size() - 1u; + connectTwoSections(prevSectionIdx, nextSectionIdx); + } + previousLineSectionIdx = newSections.size() - 1u; + }; + + // This loop Generates Mitered Line Sections and Offseted Beziers -> will still have breaks and disconnections + // then we call addBezierSection and it connects it to the previous section + for (uint32_t i = 0; i < m_sections.size(); ++i) + { + const auto& section = m_sections[i]; + if (section.type == ObjectType::LINE) + { + // TODO: try merging lines if they have same tangent (resultin in less points) + std::vector newLinePoints; + newLinePoints.reserve(m_linePoints.size()); + for (uint32_t j = 0; j < section.count + 1; ++j) + { + const uint32_t linePointIdx = section.index + j; + float64_t2 offsetVector; + if (j == 0) + { + const float64_t2 tangent = glm::normalize(m_linePoints[linePointIdx + 1].p - m_linePoints[linePointIdx].p); + offsetVector = float64_t2(tangent.y, -tangent.x); + } + else if (j == section.count) + { + const float64_t2 tangent = glm::normalize(m_linePoints[linePointIdx].p - m_linePoints[linePointIdx - 1].p); + offsetVector = float64_t2(tangent.y, -tangent.x); + } + else + { + const float64_t2 tangentPrevLine = glm::normalize(m_linePoints[linePointIdx].p - m_linePoints[linePointIdx - 1].p); + const float64_t2 normalPrevLine = float64_t2(tangentPrevLine.y, -tangentPrevLine.x); + const float64_t2 tangentNextLine = glm::normalize(m_linePoints[linePointIdx + 1].p - m_linePoints[linePointIdx].p); + const float64_t2 normalNextLine = float64_t2(tangentNextLine.y, -tangentNextLine.x); + + const float64_t2 intersectionDirection = glm::normalize(normalPrevLine + normalNextLine); + const float64_t cosAngleBetweenNormals = glm::dot(normalPrevLine, normalNextLine); + offsetVector = intersectionDirection * sqrt(2.0 / (1.0 + cosAngleBetweenNormals)); + } + newLinePoints.push_back(m_linePoints[linePointIdx].p + offsetVector * offset); + } + connectLinesSection(std::move(newLinePoints)); + } + else if (section.type == ObjectType::QUAD_BEZIER) + { + std::vector> newBeziers; + curves::Subdivision::AddBezierFunc addToBezier = [&](nbl::hlsl::shapes::QuadraticBezier&& info) -> void + { + newBeziers.push_back(info); + }; + for (uint32_t j = 0; j < section.count; ++j) + { + const uint32_t bezierIdx = section.index + j; + curves::OffsettedBezier offsettedBezier(m_quadBeziers[bezierIdx].shape, offset); + curves::Subdivision::adaptive(offsettedBezier, maxError, addToBezier, 10u); + } + connectBezierSection(std::move(newBeziers)); + } + } + + if (parallelPolyline.m_closedPolygon) + { + const uint32_t prevSectionIdx = newSections.size() - 1u; + const uint32_t nextSectionIdx = 0u; + connectTwoSections(prevSectionIdx, nextSectionIdx); + } + + return parallelPolyline; + } // outputs two offsets to the polyline and connects the ends if not closed - void makeWideWhole(CPolyline& outOffset1, CPolyline& outOffset2, float64_t offset, const float64_t maxError = 1e-5) const; + void makeWideWhole(CPolyline& outOffset1, CPolyline& outOffset2, float64_t offset, const float64_t maxError = 1e-5) const + { + outOffset1 = generateParallelPolyline(offset, maxError); + outOffset2 = generateParallelPolyline(-1.0 * offset, maxError); + if (!m_closedPolygon) + { + nbl::hlsl::float64_t2 beginToBeginConnector[2u]; + beginToBeginConnector[0u] = outOffset1.getSectionFirstPoint(outOffset1.getSectionInfoAt(0u)); + beginToBeginConnector[1u] = outOffset2.getSectionFirstPoint(outOffset2.getSectionInfoAt(0u)); + nbl::hlsl::float64_t2 endToEndConnector[2u]; + endToEndConnector[0u] = outOffset1.getSectionLastPoint(outOffset1.getSectionInfoAt(outOffset1.getSectionsCount() - 1u)); + endToEndConnector[1u] = outOffset2.getSectionLastPoint(outOffset2.getSectionInfoAt(outOffset2.getSectionsCount() - 1u)); + outOffset2.addLinePoints({ beginToBeginConnector, beginToBeginConnector + 2 }); + outOffset2.addLinePoints({ endToEndConnector, endToEndConnector + 2 }); + } + } + // Manual CPU Styling: breaks the current polyline into more polylines based the stipple pattern // we could output a list/vector of polylines instead of using lambda but most of the time we need to work with the output and throw it away immediately. typedef std::function OutputPolylineFunc; - void stippleBreakDown(const LineStyleInfo& lineStyle, const OutputPolylineFunc& addPolyline, float64_t discontinuityErrorTolerance = 1e-5) const; + void stippleBreakDown(const LineStyleInfo& lineStyle, const OutputPolylineFunc& addPolyline) const + { + if (!lineStyle.isVisible()) + return; + + // currently only works for road styles with only 2 stipple values (1 draw, 1 gap) + assert(lineStyle.stipplePatternSize == 1); + + const float64_t patternLen = 1.0 / lineStyle.reciprocalStipplePatternLen; + const float32_t drawSectionNormalizedLen = lineStyle.stipplePattern[0]; + const float32_t gapSectionNormalizedLen = 1.0 - lineStyle.stipplePattern[0]; + const float32_t drawSectionLen = drawSectionNormalizedLen / lineStyle.reciprocalStipplePatternLen; + + CPolyline currentPolyline; + std::vector linePoints; + std::vector> beziers; + auto flushCurrentPolyline = [&]() + { + if (linePoints.size() > 1u) + { + currentPolyline.addLinePoints({ linePoints.data(), linePoints.data() + linePoints.size() }); + linePoints.clear(); + } + if (beziers.size() > 0u) + { + currentPolyline.addQuadBeziers({ beziers.data(), beziers.data() + beziers.size() }); + beziers.clear(); + } + if (currentPolyline.getSectionsCount() > 0u) + addPolyline(currentPolyline); + currentPolyline.clearEverything(); + }; + auto pushBackToLinePoints = [&](const float64_t2& point) + { + if (linePoints.empty()) + { + linePoints.push_back(point); + } + else if (linePoints.back() != point) + { + linePoints.push_back(point); + } + }; + + float currentPhaseShift = lineStyle.phaseShift; + for (uint32_t sectionIdx = 0u; sectionIdx < m_sections.size(); sectionIdx++) + { + const auto& section = m_sections[sectionIdx]; + + if (section.type == ObjectType::LINE) + { + // calculate phase shift at each point of each line in section + const uint32_t lineCount = section.count; + for (uint32_t i = 0u; i < lineCount; i++) + { + const uint32_t currIdx = section.index + i; + const auto& currlinePoint = m_linePoints[currIdx]; + const auto& nextLinePoint = m_linePoints[currIdx + 1u]; + const float64_t2 lineVector = nextLinePoint.p - currlinePoint.p; + const float64_t lineLen = glm::length(lineVector); + const float64_t2 lineVectorNormalized = lineVector / lineLen; + + float64_t currentTracedLen = 0.0; + const float32_t differenceToNextDrawSectionEnd = drawSectionNormalizedLen - currentPhaseShift; + const bool insideDrawSection = differenceToNextDrawSectionEnd > 0.0f; + if (insideDrawSection) + { + const float64_t nextDrawSectionEnd = differenceToNextDrawSectionEnd / lineStyle.reciprocalStipplePatternLen; + const bool finishesOnThisShape = nextDrawSectionEnd <= lineLen; + + pushBackToLinePoints(currlinePoint.p); + + if (finishesOnThisShape) + { + pushBackToLinePoints(currlinePoint.p + nextDrawSectionEnd * lineVectorNormalized); + flushCurrentPolyline(); + } + else + { + pushBackToLinePoints(nextLinePoint.p); + } + currentTracedLen = nbl::core::min(nextDrawSectionEnd, lineLen); + } + + const float32_t currentTracedLenPlaceInPattern = glm::fract(currentPhaseShift + currentTracedLen * lineStyle.reciprocalStipplePatternLen); + const float32_t differenceToNextDrawSectionBegin = glm::fract(1.0 - currentTracedLenPlaceInPattern); // 0.0 gives 0.0, and 1.0 gives 0.0 + const float64_t lenToNextDrawBegin = differenceToNextDrawSectionBegin / lineStyle.reciprocalStipplePatternLen; + currentTracedLen = nbl::core::min(currentTracedLen + lenToNextDrawBegin, lineLen); + const float64_t remainingLen = lineLen - currentTracedLen; + + if (remainingLen > 0.0) + { + float64_t remainingLenNormalized = remainingLen * lineStyle.reciprocalStipplePatternLen; + int32_t fullPatternsFitNumber = static_cast(std::ceil(remainingLenNormalized)); + linePoints.reserve(linePoints.size() + fullPatternsFitNumber * 2u); + + for (int32_t s = 0; s < fullPatternsFitNumber; ++s) + { + const bool completelyInsideShape = (currentTracedLen + drawSectionLen) <= lineLen; + const float64_t nextDrawSectionEnd = (completelyInsideShape) ? (currentTracedLen + drawSectionLen) : lineLen; + + pushBackToLinePoints(currlinePoint.p + currentTracedLen * lineVectorNormalized); + pushBackToLinePoints(currlinePoint.p + nextDrawSectionEnd * lineVectorNormalized); + + if (completelyInsideShape) + flushCurrentPolyline(); + + currentTracedLen = nbl::core::min(currentTracedLen + patternLen, lineLen); + } + } + + const double changeInPhaseShift = glm::fract(lineLen * lineStyle.reciprocalStipplePatternLen); + currentPhaseShift = static_cast(glm::fract(currentPhaseShift + changeInPhaseShift)); + } + + currentPolyline.addLinePoints({ linePoints.data(), linePoints.data() + linePoints.size() }); + linePoints.clear(); + } + else if (section.type == ObjectType::QUAD_BEZIER) + { + const uint32_t quadBezierCount = section.count; + + // calculate phase shift at point P0 of each bezier + for (uint32_t i = 0u; i < quadBezierCount; i++) + { + const uint32_t currIdx = section.index + i; + const QuadraticBezierInfo& quadBezierInfo = m_quadBeziers[currIdx]; + // setting next phase shift based on current arc length + nbl::hlsl::shapes::Quadratic quadratic = nbl::hlsl::shapes::Quadratic::constructFromBezier(quadBezierInfo.shape); + nbl::hlsl::shapes::Quadratic::ArcLengthCalculator arcLenCalc = nbl::hlsl::shapes::Quadratic::ArcLengthCalculator::construct(quadratic); + const double bezierLen = arcLenCalc.calcArcLen(1.0); + + float64_t currentTracedLen = 0.0; + const float32_t differenceToNextDrawSectionEnd = drawSectionNormalizedLen - currentPhaseShift; + const bool insideDrawSection = differenceToNextDrawSectionEnd > 0.0f; + if (insideDrawSection) + { + const float64_t nextDrawSectionEnd = differenceToNextDrawSectionEnd / lineStyle.reciprocalStipplePatternLen; + const bool finishesOnThisShape = nextDrawSectionEnd <= bezierLen; + + auto newBezier = quadBezierInfo.shape; + if (finishesOnThisShape) + { + float64_t t = arcLenCalc.calcArcLenInverse(quadratic, 0.0, 1.0, nextDrawSectionEnd, 1e-5, 0.5); + newBezier.splitFromStart(t); + beziers.push_back(std::move(newBezier)); + flushCurrentPolyline(); + } + else + { + // draw section covers entire bezier, no need for clipping + beziers.push_back(std::move(newBezier)); + } + + currentTracedLen = nbl::core::min(nextDrawSectionEnd, bezierLen); + } + + const float32_t currentTracedLenPlaceInPattern = glm::fract(currentPhaseShift + currentTracedLen * lineStyle.reciprocalStipplePatternLen); + const float32_t differenceToNextDrawSectionBegin = glm::fract(1.0 - currentTracedLenPlaceInPattern); // 0.0 gives 0.0, and 1.0 gives 0.0 + const float64_t lenToNextDrawBegin = differenceToNextDrawSectionBegin / lineStyle.reciprocalStipplePatternLen; + currentTracedLen = nbl::core::min(currentTracedLen + lenToNextDrawBegin, bezierLen); + const float64_t remainingLen = bezierLen - currentTracedLen; + + if (remainingLen > 0.0) + { + float64_t remainingLenNormalized = remainingLen * lineStyle.reciprocalStipplePatternLen; + int32_t fullPatternsFitNumber = static_cast(std::ceil(remainingLenNormalized)); + beziers.reserve(beziers.size() + fullPatternsFitNumber); + + for (int32_t s = 0; s < fullPatternsFitNumber; ++s) + { + const bool completelyInsideShape = (currentTracedLen + drawSectionLen) <= bezierLen; + const float64_t nextDrawSectionEnd = (completelyInsideShape) ? (currentTracedLen + drawSectionLen) : bezierLen; + float64_t tStart = arcLenCalc.calcArcLenInverse(quadratic, 0.0, 1.0, currentTracedLen, 1e-5, 0.5); + float64_t tEnd = arcLenCalc.calcArcLenInverse(quadratic, 0.0, 1.0, nextDrawSectionEnd, 1e-5, 0.5); + + auto newBezier = quadBezierInfo.shape; + newBezier.splitFromMinToMax(tStart, tEnd); + beziers.push_back(std::move(newBezier)); + + if (completelyInsideShape) + flushCurrentPolyline(); + + currentTracedLen = nbl::core::min(currentTracedLen + patternLen, bezierLen); + } + } + + const double changeInPhaseShift = glm::fract(bezierLen * lineStyle.reciprocalStipplePatternLen); + currentPhaseShift = static_cast(glm::fract(currentPhaseShift + changeInPhaseShift)); + } + + currentPolyline.addQuadBeziers({ beziers.data(), beziers.data() + beziers.size() }); + beziers.clear(); + } + } + + flushCurrentPolyline(); + } void setClosed(bool closed) { @@ -619,49 +1198,11 @@ class CPolyline : public CPolylineBase float64_t2 getMin() const { return m_Min; } float64_t2 getMax() const { return m_Max; } - void transform(const float64_t2x2& rotScale, float64_t2 translate) - { - // transform is linear - for (auto& linePoint : m_linePoints) - linePoint.p = mul(rotScale,linePoint.p) + translate; - for (auto& bezierPoint : m_quadBeziers) - { - bezierPoint.shape.P0 = mul(rotScale,bezierPoint.shape.P0) + translate; - bezierPoint.shape.P1 = mul(rotScale,bezierPoint.shape.P1) + translate; - bezierPoint.shape.P2 = mul(rotScale,bezierPoint.shape.P2) + translate; - } - - // not useful for markers: -#if 0 - std::array corners = { - float64_t2{ m_Min.x, m_Min.y }, - float64_t2{ m_Max.x, m_Min.y }, - float64_t2{ m_Min.x, m_Max.y }, - float64_t2{ m_Max.x, m_Max.y } - }; - - std::array transformedCorners; - for (uint32_t i = 0; i < 4u; ++i) - transformedCorners[i] = mul(rotScale, corners[i]) + translate; - - // Compute new AABB by finding min and max of transformed corners (OBB) - m_Min = { nbl::hlsl::numeric_limits::max, nbl::hlsl::numeric_limits::max }; - m_Max = { nbl::hlsl::numeric_limits::lowest, nbl::hlsl::numeric_limits::lowest }; - - for (const auto& corner : transformedCorners) { - m_Min.x = nbl::core::min(m_Min.x, corner.x); - m_Min.y = nbl::core::min(m_Min.y, corner.y); - m_Max.x = nbl::core::max(m_Max.x, corner.x); - m_Max.y = nbl::core::max(m_Max.y, corner.y); - } -#endif - } - protected: std::vector m_polylineConnector; std::vector m_sections; std::vector m_linePoints; - std::vector m_quadBeziers; // series of connected beziers, startig with P0 and ending with P1 + std::vector m_quadBeziers; uint32_t lastSectionsSize = std::numeric_limits::max(); // important for miter and parallel generation bool m_closedPolygon = false; @@ -674,7 +1215,6 @@ class CPolyline : public CPolylineBase m_Max.y = nbl::hlsl::max(m_Max.y, point.y); } - // AABB float64_t2 m_Min; // min coordinate of the whole polyline float64_t2 m_Max; // max coordinate of the whole polyline @@ -1086,37 +1626,156 @@ class CPolyline : public CPolylineBase return ret; } - bool checkIfInDrawSection(const LineStyleInfo& lineStyle, float normalizedPlaceInPattern) +private: + class PolylineConnectorBuilder { - const uint32_t patternIdx = lineStyle.getPatternIdxFromNormalizedPosition(normalizedPlaceInPattern); - // odd patternIdx means a "no draw section" and current candidate should split into two nearest draw sections - return !(patternIdx & 0x1); - } + public: + void addLineNormal(const float64_t2& line, float64_t lineLen, const float64_t2& worldSpaceCircleCenter, float phaseShift) + { + PolylineConnectorNormalHelperInfo connectorNormalInfo; + connectorNormalInfo.worldSpaceCircleCenter = worldSpaceCircleCenter; + connectorNormalInfo.normal = float64_t2(-line.y, line.x) / lineLen; + connectorNormalInfo.phaseShift = phaseShift; + connectorNormalInfo.type = ObjectType::LINE; - void addMiterIfVisible( - const float32_t2 prevLineNormal, - const float32_t2 nextLineNormal, - const float32_t2 center) - { - const float crossProductZ = nbl::hlsl::cross2D(nextLineNormal, prevLineNormal); - constexpr float CROSS_PRODUCT_LINEARITY_EPSILON = 1.0e-6f; - const bool isMiterVisible = std::abs(crossProductZ) >= CROSS_PRODUCT_LINEARITY_EPSILON; - if (isMiterVisible) + connectorNormalInfos.push_back(connectorNormalInfo); + } + + void addBezierNormals(const QuadraticBezierInfo& quadBezierInfo, float phaseShift) { - const float64_t2 intersectionDirection = glm::normalize(prevLineNormal + nextLineNormal); - const float64_t cosAngleBetweenNormals = glm::dot(prevLineNormal, nextLineNormal); + // TODO: we already calculate quadratic form of each bezier (except of the last one), maybe store this info in an array and use it later to calculate normals? + const float64_t2 bezierDerivativeValueAtP0 = 2.0 * (quadBezierInfo.shape.P1 - quadBezierInfo.shape.P0); + const float32_t2 tangentAtP0 = glm::normalize(bezierDerivativeValueAtP0); + //const float_t2 A = P0 - 2.0 * P1 + P2; + const float64_t2 bezierDerivativeValueAtP2 = 2.0 * (quadBezierInfo.shape.P0 - 2.0 * quadBezierInfo.shape.P1 + quadBezierInfo.shape.P2) + 2.0 * (quadBezierInfo.shape.P1 - quadBezierInfo.shape.P0); + const float32_t2 tangentAtP2 = glm::normalize(bezierDerivativeValueAtP2); + + PolylineConnectorNormalHelperInfo connectorNormalInfoAtP0{}; + connectorNormalInfoAtP0.worldSpaceCircleCenter = quadBezierInfo.shape.P0; + connectorNormalInfoAtP0.normal = float32_t2(-tangentAtP0.y, tangentAtP0.x); + connectorNormalInfoAtP0.phaseShift = phaseShift; + connectorNormalInfoAtP0.type = ObjectType::QUAD_BEZIER; + + PolylineConnectorNormalHelperInfo connectorNormalInfoAtP2{}; + connectorNormalInfoAtP2.worldSpaceCircleCenter = quadBezierInfo.shape.P2; + connectorNormalInfoAtP2.normal = float32_t2(-tangentAtP2.y, tangentAtP2.x); + connectorNormalInfoAtP2.phaseShift = phaseShift; + connectorNormalInfoAtP2.type = ObjectType::QUAD_BEZIER; + + connectorNormalInfos.push_back(connectorNormalInfoAtP0); + connectorNormalInfos.push_back(connectorNormalInfoAtP2); + } - PolylineConnector res = { - .circleCenter = center, - .v = static_cast(intersectionDirection * std::sqrt(2.0 / (1.0 + cosAngleBetweenNormals))), - .cosAngleDifferenceHalf = static_cast(std::sqrt((1.0 + cosAngleBetweenNormals) * 0.5)), - }; + std::vector buildConnectors(const LineStyleInfo& lineStyle, bool isClosedPolygon) + { + std::vector connectors; - // need To flip direction? - if (crossProductZ < 0.0f) - res.v = -res.v; + if (connectorNormalInfos.size() < 2u) + return {}; - m_polylineConnector.push_back(res); + uint32_t i = getConnectorNormalInfoCountOfLineType(connectorNormalInfos[0].type); + while (i < connectorNormalInfos.size()) + { + const auto& prevLine = connectorNormalInfos[i - 1]; + const auto& nextLine = connectorNormalInfos[i]; + constructMiterIfVisible(lineStyle, prevLine, nextLine, false, connectors); + + i += getConnectorNormalInfoCountOfLineType(nextLine.type); + } + + if (isClosedPolygon) + { + const auto& prevLine = connectorNormalInfos[connectorNormalInfos.size() - 1u]; + const auto& nextLine = connectorNormalInfos[0u]; + constructMiterIfVisible(lineStyle, prevLine, nextLine, true, connectors); + } + + return connectors; } - } -}; \ No newline at end of file + + inline void setPhaseShiftAtEndOfPolyline(float phaseShift) + { + phaseShiftAtEndOfPolyline = phaseShift; + } + + private: + struct PolylineConnectorNormalHelperInfo + { + float64_t2 worldSpaceCircleCenter; + float32_t2 normal; + float phaseShift; + ObjectType type; + }; + + inline uint32_t getConnectorNormalInfoCountOfLineType(ObjectType type) + { + static constexpr uint32_t NORMAL_INFO_COUNT_OF_A_LINE = 1u; + static constexpr uint32_t NORMAL_INFO_COUNT_OF_A_QUADRATIC_BEZIER = 2u; + + if (type == ObjectType::LINE) + { + return NORMAL_INFO_COUNT_OF_A_LINE; + } + else if (type == ObjectType::QUAD_BEZIER) + { + return NORMAL_INFO_COUNT_OF_A_QUADRATIC_BEZIER; + } + else + { + assert(false); + return -1u; + } + } + + bool checkIfInDrawSection(const LineStyleInfo& lineStyle, float normalizedPlaceInPattern) + { + const uint32_t patternIdx = lineStyle.getPatternIdxFromNormalizedPosition(normalizedPlaceInPattern); + // odd patternIdx means a "no draw section" and current candidate should split into two nearest draw sections + return !(patternIdx & 0x1); + } + + void constructMiterIfVisible( + const LineStyleInfo& lineStyle, + const PolylineConnectorNormalHelperInfo& prevLine, + const PolylineConnectorNormalHelperInfo& nextLine, + bool isMiterClosingPolyline, + std::vector& connectors) + { + const float32_t2 prevLineNormal = prevLine.normal; + const float32_t2 nextLineNormal = nextLine.normal; + + const float crossProductZ = nbl::hlsl::cross2D(nextLineNormal, prevLineNormal); + constexpr float CROSS_PRODUCT_LINEARITY_EPSILON = 1.0e-6f; + const bool isMiterVisible = std::abs(crossProductZ) >= CROSS_PRODUCT_LINEARITY_EPSILON; + bool isMiterInDrawSection = checkIfInDrawSection(lineStyle, nextLine.phaseShift); + if (isMiterClosingPolyline) + { + isMiterInDrawSection = isMiterInDrawSection && checkIfInDrawSection(lineStyle, phaseShiftAtEndOfPolyline); + } + + if (isMiterVisible && isMiterInDrawSection) + { + const float64_t2 intersectionDirection = glm::normalize(prevLineNormal + nextLineNormal); + const float64_t cosAngleBetweenNormals = glm::dot(prevLineNormal, nextLineNormal); + + PolylineConnector res{}; + res.circleCenter = nextLine.worldSpaceCircleCenter; + res.v = static_cast(intersectionDirection * std::sqrt(2.0 / (1.0 + cosAngleBetweenNormals))); + res.cosAngleDifferenceHalf = static_cast(std::sqrt((1.0 + cosAngleBetweenNormals) * 0.5)); + + const bool needToFlipDirection = crossProductZ < 0.0f; + if (needToFlipDirection) + { + res.v = -res.v; + } + // Negating y to avoid doing it in vertex shader when working in screen space, where y is in the opposite direction of worldspace y direction + res.v.y = -res.v.y; + + connectors.push_back(res); + } + } + + nbl::core::vector connectorNormalInfos; + float phaseShiftAtEndOfPolyline = 0.0f; + }; +}; diff --git a/62_CAD/SingleLineText.cpp b/62_CAD/SingleLineText.cpp index 76eb797e7..017743cf1 100644 --- a/62_CAD/SingleLineText.cpp +++ b/62_CAD/SingleLineText.cpp @@ -1,7 +1,8 @@ #include "SingleLineText.h" -SingleLineText::SingleLineText(nbl::ext::TextRendering::FontFace* face, const std::wstring& text) +SingleLineText::SingleLineText(core::smart_refctd_ptr&& face, const std::string& text) { + m_face = std::move(face); m_glyphBoxes.reserve(text.length()); m_boundingBox.min = float64_t2(0.0, 0.0); @@ -11,14 +12,14 @@ SingleLineText::SingleLineText(nbl::ext::TextRendering::FontFace* face, const st float64_t2 currentPos = float32_t2(0.0, 0.0); for (uint32_t i = 0; i < text.length(); i++) { - const auto glyphIndex = face->getGlyphIndex(text.at(i)); - const auto glyphMetrics = face->getGlyphMetrics(glyphIndex); + const auto glyphIndex = m_face->getGlyphIndex(wchar_t(text.at(i))); + const auto glyphMetrics = m_face->getGlyphMetrics(glyphIndex); const bool skipGenerateGlyph = (glyphIndex == 0 || (glyphMetrics.size.x == 0.0 && glyphMetrics.size.y == 0.0)); if (!skipGenerateGlyph) { #ifdef VERIFY_DEBUG - msdfgen::Shape shape = face->generateGlyphShape(glyphIndex); + msdfgen::Shape shape = m_face->generateGlyphShape(glyphIndex); _NBL_BREAK_IF(shape.contours.empty()); #endif GlyphBox glyphBbox = @@ -42,29 +43,30 @@ SingleLineText::SingleLineText(nbl::ext::TextRendering::FontFace* face, const st void SingleLineText::Draw( DrawResourcesFiller& drawResourcesFiller, SIntendedSubmitInfo& intendedNextSubmit, - nbl::ext::TextRendering::FontFace* face, const float64_t2& baselineStart, const float32_t2& scale, const float32_t& rotateAngle, - const float32_t4& color, - const float32_t tiltTiltAngle, - const float32_t boldInPixels) const + const float32_t4& color) const { float32_t2 vec(cos(rotateAngle), sin(rotateAngle)); - float64_t3x3 transformation = + float64_t3x3 rotationMulScaleMat = { - vec.x * scale.x, vec.y * scale.y, baselineStart.x, - -vec.y * scale.x, vec.x * scale.y, baselineStart.y, + vec.x * scale.x, vec.y * scale.y, 0.0, + -vec.y * scale.x, vec.x * scale.y, 0.0, 0.0, 0.0, 1.0, }; + float64_t3x3 translationMat = + { + 1.0, 0.0, baselineStart.x, + 0.0, 1.0, baselineStart.y, + 0.0, 0.0, 1.0, + }; + float64_t3x3 transformation = mul(translationMat, rotationMulScaleMat); - // TODO: Use Separate TextStyleInfo or something, and somehow alias with line style for improved readability LineStyleInfo lineStyle = {}; lineStyle.color = color; - lineStyle.screenSpaceLineWidth = tan(tiltTiltAngle); - lineStyle.worldSpaceLineWidth = boldInPixels; - drawResourcesFiller.setActiveLineStyle(lineStyle); - drawResourcesFiller.beginMainObject(MainObjectType::TEXT); + const uint32_t styleIdx = drawResourcesFiller.addLineStyle_SubmitIfNeeded(lineStyle, intendedNextSubmit); + auto glyphObjectIdx = drawResourcesFiller.addMainObject_SubmitIfNeeded(styleIdx, intendedNextSubmit); for (const auto& glyphBox : m_glyphBoxes) { @@ -74,9 +76,8 @@ void SingleLineText::Draw( // float32_t3 xx = float64_t3(0.0, -glyphBox.size.y, 0.0); const float32_t aspectRatio = static_cast(glm::length(dirV) / glm::length(dirU)); // check if you can just do: (glyphBox.size.y * scale.y) / glyphBox.size.x * scale.x) - const float32_t2 minUV = face->getUV(float32_t2(0.0f,0.0f), glyphBox.size, drawResourcesFiller.getMSDFResolution(), MSDFPixelRange); - drawResourcesFiller.drawFontGlyph(face, glyphBox.glyphIdx, topLeft, dirU, aspectRatio, minUV, intendedNextSubmit); + const float32_t2 minUV = m_face->getUV(float32_t2(0.0f,0.0f), glyphBox.size, drawResourcesFiller.getMSDFResolution(), MSDFPixelRange); + drawResourcesFiller.drawFontGlyph(m_face.get(), glyphBox.glyphIdx, topLeft, dirU, aspectRatio, minUV, glyphObjectIdx, intendedNextSubmit); } - drawResourcesFiller.endMainObject(); } \ No newline at end of file diff --git a/62_CAD/SingleLineText.h b/62_CAD/SingleLineText.h index 624f3399f..fc7c42ca6 100644 --- a/62_CAD/SingleLineText.h +++ b/62_CAD/SingleLineText.h @@ -7,12 +7,11 @@ using namespace nbl::core; using namespace nbl::asset; using namespace nbl::ext::TextRendering; -/// This is basically a Text Layout in Font Unit Space + a Draw method with transformations, color and additional parameters class SingleLineText { public: // constructs and fills the `glyphBoxes` - SingleLineText(nbl::ext::TextRendering::FontFace* face, const std::wstring& text); + SingleLineText(core::smart_refctd_ptr&& face, const std::string& text); struct BoundingBox { @@ -24,20 +23,13 @@ class SingleLineText // iterates over `glyphBoxes` generates textures msdfs if failed to add to cache (through that lambda you put) // void Draw(DrawResourcesFiller& drawResourcesFiller, SIntendedSubmitInfo& intendedNextSubmit); - // ! `baselineStart`, `scale` and `rotateAngle` affect the whole line - // ! `color`, `italicTiltAngle`, `boldInPixels` affect the rendering of each glyph - // ! `italicTiltAngle` to emulate italic fonts - // ! `boldInPixels` to bolden the font, don't try more than 1 pixel for various reasons and limitations with msdf ranges void Draw( DrawResourcesFiller& drawResourcesFiller, SIntendedSubmitInfo& intendedNextSubmit, - nbl::ext::TextRendering::FontFace* face, const float64_t2& baselineStart = float64_t2(0.0,0.0), const float32_t2& scale = float64_t2(1.0f, 1.0f), const float32_t& rotateAngle = 0.0f, - const float32_t4& color = float32_t4(1.0f,1.0f,1.0f,1.0f), - const float32_t italicTilt = 0.0f, - const float32_t boldInPixels = 0.0f) const; + const float32_t4& color = float32_t4(1.0f,1.0f,1.0f,1.0f)) const; protected: @@ -51,4 +43,5 @@ class SingleLineText BoundingBox m_boundingBox = {}; std::vector m_glyphBoxes; + core::smart_refctd_ptr m_face; }; \ No newline at end of file diff --git a/62_CAD/TransparencyAndAA.md.orig b/62_CAD/TransparencyAndAA.md.orig new file mode 100644 index 000000000..bc0c5c585 --- /dev/null +++ b/62_CAD/TransparencyAndAA.md.orig @@ -0,0 +1,15 @@ +The Algorithm used here for AntiAliasing and Transparency for 2D Lines and Curves relies on `VK_EXT_fragment_shader_interlock`; enabling this capability provides a critical section for fragment shaders to avoid overlapping pixels being processed at the same time, and certain guarantees about the ordering of fragment shader invocations of fragments of overlapping pixels. + +Such a guarantee is useful for applications like blending in the fragment shader, where an application requires that fragment values to be composited in the framebuffer in primitive order. + +For example Programmable blending operations in the fragment shader, where the destination buffer is read via image loads and the final value is written via image stores. + +Alpha value isn't the only thing we store in our R32_UINT texture. We also store an `object id` so that we can avoid objects self intersections (think of polylines crossing over themselves) with 24 bits for ID and 8 bits for alpha. + +In more details: +1. Every fragment being processed checks if it's object id is the same as the one in the Read/Write R32_UINT Texture. + - if it's the same then it does a MAX operation from the current calculate alpha and the one existing in the Texture. + - if it's not the same then it **resolves**: + - It renders the pixel using the "Previous!" object's style that was in the texture + +2. There is a last fullscreen pass that resolves anything unresolved \ No newline at end of file diff --git a/62_CAD/common.hlsl b/62_CAD/common.hlsl new file mode 100644 index 000000000..6df2c5666 --- /dev/null +++ b/62_CAD/common.hlsl @@ -0,0 +1,527 @@ +#ifndef _CAD_EXAMPLE_COMMON_HLSL_INCLUDED_ +#define _CAD_EXAMPLE_COMMON_HLSL_INCLUDED_ + +#include +#include +#include +#ifdef __HLSL_VERSION +#include +#endif + +enum class ObjectType : uint32_t +{ + LINE = 0u, + QUAD_BEZIER = 1u, + CURVE_BOX = 2u, + POLYLINE_CONNECTOR = 3u, + FONT_GLYPH = 4u, + IMAGE = 5u +}; + +enum class MajorAxis : uint32_t +{ + MAJOR_X = 0u, + MAJOR_Y = 1u, +}; + +// Consists of multiple DrawObjects +struct MainObject +{ + uint32_t styleIdx; + uint32_t pad; // do I even need this? it's stored in structured buffer not bda + uint64_t clipProjectionAddress; +}; + +struct DrawObject +{ + uint32_t type_subsectionIdx; // packed two uint16 into uint32 + uint32_t mainObjIndex; + uint64_t geometryAddress; +}; + +struct LinePointInfo +{ + float64_t2 p; + float32_t phaseShift; + float32_t stretchValue; +}; + +struct QuadraticBezierInfo +{ + nbl::hlsl::shapes::QuadraticBezier shape; // 48bytes = 3 (control points) x 16 (float64_t2) + float32_t phaseShift; + float32_t stretchValue; +}; +#ifndef __HLSL_VERSION +static_assert(offsetof(QuadraticBezierInfo, phaseShift) == 48u); +#endif + +struct GlyphInfo +{ + float64_t2 topLeft; // 2 * 8 = 16 bytes + float32_t2 dirU; // 2 * 4 = 8 bytes (24) + float32_t aspectRatio; // 4 bytes (32) + // unorm8 minU; + // unorm8 minV; + // uint16 textureId; + uint32_t minUV_textureID_packed; // 4 bytes (36) + +#ifndef __HLSL_VERSION + GlyphInfo(float64_t2 topLeft, float32_t2 dirU, float32_t aspectRatio, uint16_t textureId, float32_t2 minUV) : + topLeft(topLeft), + dirU(dirU), + aspectRatio(aspectRatio) + { + assert(textureId < nbl::hlsl::numeric_limits::max); + packMinUV_TextureID(minUV, textureId); + } +#endif + + void packMinUV_TextureID(float32_t2 minUV, uint16_t textureId) + { + minUV_textureID_packed = textureId; + uint32_t uPacked = (uint32_t)(clamp(minUV.x, 0.0f, 1.0f) * 255.0f); + uint32_t vPacked = (uint32_t)(clamp(minUV.y, 0.0f, 1.0f) * 255.0f); + minUV_textureID_packed = nbl::hlsl::glsl::bitfieldInsert(minUV_textureID_packed, uPacked, 16, 8); + minUV_textureID_packed = nbl::hlsl::glsl::bitfieldInsert(minUV_textureID_packed, vPacked, 24, 8); + } + + float32_t2 getMinUV() + { + return float32_t2( + float32_t(nbl::hlsl::glsl::bitfieldExtract(minUV_textureID_packed, 16, 8)) / 255.0, + float32_t(nbl::hlsl::glsl::bitfieldExtract(minUV_textureID_packed, 24, 8)) / 255.0 + ); + } + + uint16_t getTextureID() + { + return uint16_t(nbl::hlsl::glsl::bitfieldExtract(minUV_textureID_packed, 0, 16)); + } +}; + +struct ImageObjectInfo +{ + float64_t2 topLeft; // 2 * 8 = 16 bytes (16) + float32_t2 dirU; // 2 * 4 = 8 bytes (24) + float32_t aspectRatio; // 4 bytes (28) + uint32_t textureID; // 4 bytes (32) +}; + +static uint32_t packR11G11B10_UNORM(float32_t3 color) +{ + // Scale and convert to integers + uint32_t r = (uint32_t)(clamp(color.r, 0.0f, 1.0f) * 2047.0f + 0.5f); // 11 bits -> 2^11 - 1 = 2047 + uint32_t g = (uint32_t)(clamp(color.g, 0.0f, 1.0f) * 2047.0f + 0.5f); // 11 bits -> 2^11 - 1 = 2047 + uint32_t b = (uint32_t)(clamp(color.b, 0.0f, 1.0f) * 1023.0f + 0.5f); // 10 bits -> 2^10 - 1 = 1023 + + // Insert each component into the correct position + uint32_t packed = r; // R: bits 0-10 + packed = nbl::hlsl::glsl::bitfieldInsert(packed, g, 11, 11); // G: bits 11-21 + packed = nbl::hlsl::glsl::bitfieldInsert(packed, b, 22, 10); // B: bits 22-31 + + return packed; +} + +static float32_t3 unpackR11G11B10_UNORM(uint32_t packed) +{ + float32_t3 color; + + // Extract each component from the packed integer + uint32_t r = nbl::hlsl::glsl::bitfieldExtract(packed, 0, 11); // R: bits 0-10 + uint32_t g = nbl::hlsl::glsl::bitfieldExtract(packed, 11, 11); // G: bits 11-21 + uint32_t b = nbl::hlsl::glsl::bitfieldExtract(packed, 22, 10); // B: bits 22-31 + + // Convert back to float and scale to [0, 1] range + color.r = (float32_t)(r) / 2047.0f; + color.g = (float32_t)(g) / 2047.0f; + color.b = (float32_t)(b) / 1023.0f; + + return color; +} + +struct PolylineConnector +{ + float64_t2 circleCenter; + float32_t2 v; + float32_t cosAngleDifferenceHalf; + float32_t _reserved_pad; +}; + +// NOTE: Don't attempt to pack curveMin/Max to uints because of limited range of values, we need the logarithmic precision of floats (more precision near 0) +struct CurveBox +{ + // will get transformed in the vertex shader, and will be calculated on the cpu when generating these boxes + float64_t2 aabbMin; // 16 + float64_t2 aabbMax; // 32 , TODO: we know it's a square/box -> we save 8 bytes if we needed to store extra data + float32_t2 curveMin[3]; // 56 + float32_t2 curveMax[3]; // 80 +}; + +#ifndef __HLSL_VERSION +static_assert(offsetof(CurveBox, aabbMin) == 0u); +static_assert(offsetof(CurveBox, aabbMax) == 16u); +static_assert(offsetof(CurveBox, curveMin[0]) == 32u); +static_assert(offsetof(CurveBox, curveMax[0]) == 56u); +static_assert(sizeof(CurveBox) == 80u); +#endif +// TODO: Compute this in a compute shader from the world counterparts +// because this struct includes NDC coordinates, the values will change based camera zoom and move +// of course we could have the clip values to be in world units and also the matrix to transform to world instead of ndc but that requires extra computations(matrix multiplications) per vertex +struct ClipProjectionData +{ + float64_t3x3 projectionToNDC; // 72 -> because we use scalar_layout + float32_t2 minClipNDC; // 80 + float32_t2 maxClipNDC; // 88 +}; + +#ifndef __HLSL_VERSION +static_assert(offsetof(ClipProjectionData, projectionToNDC) == 0u); +static_assert(offsetof(ClipProjectionData, minClipNDC) == 72u); +static_assert(offsetof(ClipProjectionData, maxClipNDC) == 80u); +#endif + +struct Globals +{ + ClipProjectionData defaultClipProjection; // 88 + double screenToWorldRatio; // 96 + double worldToScreenRatio; // 100 + uint32_t2 resolution; // 108 + float antiAliasingFactor; // 112 + float miterLimit; // 116 + float32_t2 _padding; // 128 +}; + +#ifndef __HLSL_VERSION +static_assert(offsetof(Globals, defaultClipProjection) == 0u); +static_assert(offsetof(Globals, screenToWorldRatio) == 88u); +static_assert(offsetof(Globals, worldToScreenRatio) == 96u); +static_assert(offsetof(Globals, resolution) == 104u); +static_assert(offsetof(Globals, antiAliasingFactor) == 112u); +static_assert(offsetof(Globals, miterLimit) == 116u); +#endif + +NBL_CONSTEXPR uint32_t InvalidRigidSegmentIndex = 0xffffffff; +NBL_CONSTEXPR float InvalidStyleStretchValue = nbl::hlsl::numeric_limits::infinity; + +struct LineStyle +{ + const static uint32_t StipplePatternMaxSize = 14u; + + // common data + float32_t4 color; + float screenSpaceLineWidth; + float worldSpaceLineWidth; + + // stipple pattern data + int32_t stipplePatternSize; + float reciprocalStipplePatternLen; + uint32_t stipplePattern[StipplePatternMaxSize]; // packed float into uint (top two msb indicate leftIsDotPattern and rightIsDotPattern as an optimization) + uint32_t isRoadStyleFlag; + uint32_t rigidSegmentIdx; // TODO: can be more mem efficient with styles by packing this along other values, since stipple pattern size is bounded by StipplePatternMaxSize + + float getStippleValue(const uint32_t ix) + { + const uint32_t floatValBis = 0xffffffff >> 2; // clear two msb bits reserved for something else + return (stipplePattern[ix] & floatValBis) / float(1u << 29); + } + + void setStippleValue(const uint32_t ix, const float val) + { + stipplePattern[ix] = (uint32_t)(val * (1u << 29u)); + } + + bool isLeftDot(const uint32_t ix) + { + // stipplePatternSize is odd by construction (pattern starts with + and ends with -) + return (stipplePattern[ix] & (1u << 30)) > 0; + } + + bool isRightDot(const uint32_t ix) + { + // stipplePatternSize is odd by construction (pattern starts with + and ends with -) + return (stipplePattern[ix] & (1u << 31)) > 0; + } + + bool hasStipples() + { + return stipplePatternSize > 0 ? true : false; + } + + void stretch(float stretch) + { + reciprocalStipplePatternLen /= stretch; + } +}; + +#ifndef __HLSL_VERSION +inline bool operator==(const LineStyle& lhs, const LineStyle& rhs) +{ + // Compare bits of the screen space line width values, as they may have been bit cast into integers + // for the texture IDs, and can't be compared when that results in a NaN or Infinity float + const int comparisonResult = std::memcmp(&lhs.screenSpaceLineWidth, &rhs.screenSpaceLineWidth, sizeof(float)); + const bool areParametersEqual = + lhs.color == rhs.color && + comparisonResult == 0 && + lhs.worldSpaceLineWidth == rhs.worldSpaceLineWidth && + lhs.stipplePatternSize == rhs.stipplePatternSize && + lhs.reciprocalStipplePatternLen == rhs.reciprocalStipplePatternLen && + lhs.isRoadStyleFlag == rhs.isRoadStyleFlag && + lhs.rigidSegmentIdx == rhs.rigidSegmentIdx; + + if (!areParametersEqual) + return false; + + const bool isStipplePatternArrayEqual = (lhs.stipplePatternSize > 0) ? (std::memcmp(lhs.stipplePattern, rhs.stipplePattern, sizeof(uint32_t) * lhs.stipplePatternSize) == 0) : true; + + return isStipplePatternArrayEqual; +} +#endif + +NBL_CONSTEXPR uint32_t MainObjectIdxBits = 24u; // It will be packed next to alpha in a texture +NBL_CONSTEXPR uint32_t AlphaBits = 32u - MainObjectIdxBits; +NBL_CONSTEXPR uint32_t MaxIndexableMainObjects = (1u << MainObjectIdxBits) - 1u; +NBL_CONSTEXPR uint32_t InvalidStyleIdx = nbl::hlsl::numeric_limits::max; +NBL_CONSTEXPR uint32_t InvalidMainObjectIdx = MaxIndexableMainObjects; +NBL_CONSTEXPR uint64_t InvalidClipProjectionAddress = nbl::hlsl::numeric_limits::max; +NBL_CONSTEXPR uint32_t InvalidTextureIdx = nbl::hlsl::numeric_limits::max; +NBL_CONSTEXPR MajorAxis SelectedMajorAxis = MajorAxis::MAJOR_Y; +// TODO: get automatic version working on HLSL +NBL_CONSTEXPR MajorAxis SelectedMinorAxis = MajorAxis::MAJOR_X; //(MajorAxis) (1 - (uint32_t) SelectedMajorAxis); +NBL_CONSTEXPR float MSDFPixelRange = 4.0; +NBL_CONSTEXPR float MSDFSize = 32.0; +NBL_CONSTEXPR uint32_t MSDFMips = 4; +NBL_CONSTEXPR float HatchFillMSDFSceenSpaceSize = 8.0; + +#ifdef __HLSL_VERSION + +// TODO: Use these in C++ as well once nbl::hlsl::numeric_limits compiles on C++ +float32_t2 unpackCurveBoxUnorm(uint32_t2 value) +{ + return float32_t2(value) / float32_t(nbl::hlsl::numeric_limits::max); +} + +float32_t2 unpackCurveBoxSnorm(int32_t2 value) +{ + return float32_t2(value) / float32_t(nbl::hlsl::numeric_limits::max); +} + + +uint32_t2 packCurveBoxUnorm(float32_t2 value) +{ + return value * float32_t(nbl::hlsl::numeric_limits::max); +} + +int32_t2 packCurveBoxSnorm(float32_t2 value) +{ + return value * float32_t(nbl::hlsl::numeric_limits::max); +} + +// TODO: Remove these two when we include our builtin shaders +#define nbl_hlsl_PI 3.14159265359 +#define nbl_hlsl_FLT_EPSILON 5.96046447754e-08 +#define UINT32_MAX 0xffffffffu + +// The root we're always looking for: +// 2 * C / (-B - detSqrt) +// We send to the FS: -B, 2C, det +template +struct PrecomputedRootFinder +{ + using float_t2 = vector; + using float_t3 = vector; + + float_t C2; + float_t negB; + float_t det; + + float_t computeRoots() + { + return C2 / (negB - sqrt(det)); + } + + static PrecomputedRootFinder construct(float_t negB, float_t C2, float_t det) + { + PrecomputedRootFinder result; + result.C2 = C2; + result.det = det; + result.negB = negB; + return result; + } + + static PrecomputedRootFinder construct(nbl::hlsl::math::equations::Quadratic quadratic) + { + PrecomputedRootFinder result; + result.C2 = quadratic.c * 2.0; + result.negB = -quadratic.b; + result.det = quadratic.b * quadratic.b - 4.0 * quadratic.a * quadratic.c; + return result; + } +}; + +struct PSInput +{ + float4 position : SV_Position; + float4 clip : SV_ClipDistance; + [[vk::location(0)]] nointerpolation uint4 data1 : COLOR1; + [[vk::location(1)]] nointerpolation float4 data2 : COLOR2; + [[vk::location(2)]] nointerpolation float4 data3 : COLOR3; + [[vk::location(3)]] nointerpolation float4 data4 : COLOR4; + // Data segments that need interpolation, mostly for hatches + [[vk::location(5)]] float2 interp_data5 : COLOR5; + // ArcLenCalculator + + // Set functions used in vshader, get functions used in fshader + // We have to do this because we don't have union in hlsl and this is the best way to alias + + /* SHARED: ALL ObjectTypes */ + ObjectType getObjType() { return (ObjectType) data1.x; } + uint getMainObjectIdx() { return data1.y; } + + void setObjType(ObjectType objType) { data1.x = (uint) objType; } + void setMainObjectIdx(uint mainObjIdx) { data1.y = mainObjIdx; } + + /* SHARED: LINE + QUAD_BEZIER (Curve Outlines) */ + float getLineThickness() { return asfloat(data1.z); } + float getPatternStretch() { return asfloat(data1.w); } + + void setLineThickness(float lineThickness) { data1.z = asuint(lineThickness); } + void setPatternStretch(float stretch) { data1.w = asuint(stretch); } + + void setCurrentPhaseShift(float phaseShift) { interp_data5.x = phaseShift; } + float getCurrentPhaseShift() { return interp_data5.x; } + + void setCurrentWorldToScreenRatio(float worldToScreen) { interp_data5.y = worldToScreen; } + float getCurrentWorldToScreenRatio() { return interp_data5.y; } + + /* LINE */ + float2 getLineStart() { return data2.xy; } + float2 getLineEnd() { return data2.zw; } + void setLineStart(float2 lineStart) { data2.xy = lineStart; } + void setLineEnd(float2 lineEnd) { data2.zw = lineEnd; } + + /* QUAD_BEZIER */ + nbl::hlsl::shapes::Quadratic getQuadratic() + { + return nbl::hlsl::shapes::Quadratic::construct(data2.xy, data2.zw, data3.xy); + } + void setQuadratic(nbl::hlsl::shapes::Quadratic quadratic) + { + data2.xy = quadratic.A; + data2.zw = quadratic.B; + data3.xy = quadratic.C; + } + + void setQuadraticPrecomputedArcLenData(nbl::hlsl::shapes::Quadratic::ArcLengthCalculator preCompData) + { + data3.zw = float2(preCompData.lenA2, preCompData.AdotB); + data4 = float4(preCompData.a, preCompData.b, preCompData.c, preCompData.b_over_4a); + } + nbl::hlsl::shapes::Quadratic::ArcLengthCalculator getQuadraticArcLengthCalculator() + { + return nbl::hlsl::shapes::Quadratic::ArcLengthCalculator::construct(data3.z, data3.w, data4.x, data4.y, data4.z, data4.w); + } + + /* CURVE_BOX */ + // Curves are split in the vertex shader based on their tmin and tmax + // Min curve is smaller in the minor coordinate (e.g. in the default of y top to bottom sweep, + // curveMin = smaller x / left, curveMax = bigger x / right) + // TODO: possible optimization: passing precomputed values for solving the quadratic equation instead + + // data2, data3, data4 + nbl::hlsl::math::equations::Quadratic getCurveMinMinor() { + return nbl::hlsl::math::equations::Quadratic::construct(data2.x, data2.y, data2.z); + } + nbl::hlsl::math::equations::Quadratic getCurveMaxMinor() { + return nbl::hlsl::math::equations::Quadratic::construct(data2.w, data3.x, data3.y); + } + + void setCurveMinMinor(nbl::hlsl::math::equations::Quadratic bezier) { + data2.x = bezier.a; + data2.y = bezier.b; + data2.z = bezier.c; + } + void setCurveMaxMinor(nbl::hlsl::math::equations::Quadratic bezier) { + data2.w = bezier.a; + data3.x = bezier.b; + data3.y = bezier.c; + } + + // data4 + nbl::hlsl::math::equations::Quadratic getCurveMinMajor() { + return nbl::hlsl::math::equations::Quadratic::construct(data4.x, data4.y, data3.z); + } + nbl::hlsl::math::equations::Quadratic getCurveMaxMajor() { + return nbl::hlsl::math::equations::Quadratic::construct(data4.z, data4.w, data3.w); + } + + void setCurveMinMajor(nbl::hlsl::math::equations::Quadratic bezier) { + data4.x = bezier.a; + data4.y = bezier.b; + data3.z = bezier.c; + } + void setCurveMaxMajor(nbl::hlsl::math::equations::Quadratic bezier) { + data4.z = bezier.a; + data4.w = bezier.b; + data3.w = bezier.c; + } + + // Curve box value along minor & major axis + float getMinorBBoxUV() { return interp_data5.x; }; + void setMinorBBoxUV(float minorBBoxUV) { interp_data5.x = minorBBoxUV; } + float getMajorBBoxUV() { return interp_data5.y; }; + void setMajorBBoxUV(float majorBBoxUV) { interp_data5.y = majorBBoxUV; } + + float2 getCurveBoxScreenSpaceSize() { return asfloat(data1.zw); } + void setCurveBoxScreenSpaceSize(float2 aabbSize) { data1.zw = asuint(aabbSize); } + + /* POLYLINE_CONNECTOR */ + void setPolylineConnectorTrapezoidStart(float2 trapezoidStart) { data2.xy = trapezoidStart; } + void setPolylineConnectorTrapezoidEnd(float2 trapezoidEnd) { data2.zw = trapezoidEnd; } + void setPolylineConnectorTrapezoidShortBase(float shortBase) { data3.x = shortBase; } + void setPolylineConnectorTrapezoidLongBase(float longBase) { data3.y = longBase; } + void setPolylineConnectorCircleCenter(float2 C) { data3.zw = C; } + + float2 getPolylineConnectorTrapezoidStart() { return data2.xy; } + float2 getPolylineConnectorTrapezoidEnd() { return data2.zw; } + float getPolylineConnectorTrapezoidShortBase() { return data3.x; } + float getPolylineConnectorTrapezoidLongBase() { return data3.y; } + float2 getPolylineConnectorCircleCenter() { return data3.zw; } + + /* FONT_GLYPH */ + float2 getFontGlyphUV() { return interp_data5.xy; } + uint32_t getFontGlyphTextureId() { return asuint(data2.x); } + float getFontGlyphScreenPxRange() { return data2.y; } + + void setFontGlyphUV(float2 uv) { interp_data5.xy = uv; } + void setFontGlyphTextureId(uint32_t textureId) { data2.x = asfloat(textureId); } + void setFontGlyphScreenPxRange(float glyphScreenPxRange) { data2.y = glyphScreenPxRange; } + + + /* IMAGE */ + float2 getImageUV() { return interp_data5.xy; } + uint32_t getImageTextureId() { return asuint(data2.x); } + + void setImageUV(float2 uv) { interp_data5.xy = uv; } + void setImageTextureId(uint32_t textureId) { data2.x = asfloat(textureId); } +}; + +// Set 0 - Scene Data and Globals, buffer bindings don't change the buffers only get updated +[[vk::binding(0, 0)]] ConstantBuffer globals : register(b0); +[[vk::binding(1, 0)]] StructuredBuffer drawObjects : register(t0); +[[vk::binding(2, 0)]] StructuredBuffer mainObjects : register(t1); +[[vk::binding(3, 0)]] StructuredBuffer lineStyles : register(t2); + +[[vk::combinedImageSampler]][[vk::binding(4, 0)]] Texture2DArray msdfTextures : register(t3); +[[vk::combinedImageSampler]][[vk::binding(4, 0)]] SamplerState msdfSampler : register(s3); + +[[vk::binding(5, 0)]] SamplerState textureSampler : register(s4); +[[vk::binding(6, 0)]] Texture2D textures[128] : register(t4); + +// Set 1 - Window dependant data which has higher update frequency due to multiple windows and resize need image recreation and descriptor writes +[[vk::binding(0, 1)]] globallycoherent RWTexture2D pseudoStencil : register(u0); +[[vk::binding(1, 1)]] globallycoherent RWTexture2D colorStorage : register(u1); + +#endif + +#endif diff --git a/62_CAD/curves.cpp b/62_CAD/curves.cpp index 99438766b..1114a6540 100644 --- a/62_CAD/curves.cpp +++ b/62_CAD/curves.cpp @@ -478,8 +478,8 @@ void Subdivision::adaptive(const OffsettedBezier& curve, float64_t targetMaxErro { const float64_t2 cusps = curve.findCusps(); - const float64_t t0 = nbl::core::min(cusps[0], cusps[1]); - const float64_t t1 = nbl::core::max(cusps[0], cusps[1]); + const float64_t t0 = min(cusps[0], cusps[1]); + const float64_t t1 = max(cusps[0], cusps[1]); const bool firstCusp = t0 > 0.0 && t0 < 1.0; const bool secondCusp = t1 > 0.0 && t1 < 1.0; diff --git a/62_CAD/curves.h b/62_CAD/curves.h index f9dbb7a0e..d8924697d 100644 --- a/62_CAD/curves.h +++ b/62_CAD/curves.h @@ -8,11 +8,11 @@ #include using namespace nbl::hlsl; +#include "common.hlsl" #include #include -#include "shaders/globals.hlsl" namespace curves { // Base class for all our curves diff --git a/62_CAD/fragment_shader.hlsl b/62_CAD/fragment_shader.hlsl new file mode 100644 index 000000000..7daa45ac9 --- /dev/null +++ b/62_CAD/fragment_shader.hlsl @@ -0,0 +1,641 @@ +#pragma shader_stage(fragment) + +#include "common.hlsl" +#include +#include +#include +#include +#include +#include +#include +#include + +template +struct DefaultClipper +{ + using float_t2 = vector; + NBL_CONSTEXPR_STATIC_INLINE float_t AccuracyThresholdT = 0.0; + + static DefaultClipper construct() + { + DefaultClipper ret; + return ret; + } + + inline float_t2 operator()(const float_t t) + { + const float_t ret = clamp(t, 0.0, 1.0); + return float_t2(ret, ret); + } +}; + +// for usage in upper_bound function +struct StyleAccessor +{ + LineStyle style; + using value_type = float; + + float operator[](const uint32_t ix) + { + return style.getStippleValue(ix); + } +}; + +template +struct StyleClipper +{ + using float_t = typename CurveType::scalar_t; + using float_t2 = typename CurveType::float_t2; + using float_t3 = typename CurveType::float_t3; + NBL_CONSTEXPR_STATIC_INLINE float_t AccuracyThresholdT = 0.000001; + + static StyleClipper construct( + LineStyle style, + CurveType curve, + typename CurveType::ArcLengthCalculator arcLenCalc, + float phaseShift, + float stretch, + float worldToScreenRatio) + { + StyleClipper ret = { style, curve, arcLenCalc, phaseShift, stretch, worldToScreenRatio, 0.0f, 0.0f, 0.0f, 0.0f }; + + // values for non-uniform stretching with a rigid segment + if (style.rigidSegmentIdx != InvalidRigidSegmentIndex && stretch != 1.0f) + { + // rigidSegment info in old non stretched pattern + ret.rigidSegmentStart = (style.rigidSegmentIdx >= 1u) ? style.getStippleValue(style.rigidSegmentIdx - 1u) : 0.0f; + ret.rigidSegmentEnd = (style.rigidSegmentIdx < style.stipplePatternSize) ? style.getStippleValue(style.rigidSegmentIdx) : 1.0f; + ret.rigidSegmentLen = ret.rigidSegmentEnd - ret.rigidSegmentStart; + // stretch value for non rigid segments + ret.nonRigidSegmentStretchValue = (stretch - ret.rigidSegmentLen) / (1.0f - ret.rigidSegmentLen); + // rigidSegment info to new stretched pattern + ret.rigidSegmentStart *= ret.nonRigidSegmentStretchValue / stretch; // get the new normalized rigid segment start + ret.rigidSegmentLen /= stretch; // get the new rigid segment normalized len + ret.rigidSegmentEnd = ret.rigidSegmentStart + ret.rigidSegmentLen; // get the new normalized rigid segment end + } + else + { + ret.nonRigidSegmentStretchValue = stretch; + } + + return ret; + } + + // For non-uniform stretching with a rigid segment (the one segement that shouldn't stretch) the whole pattern changes + // instead of transforming each of the style.stipplePattern values (max 14 of them), we transform the normalized place in pattern + float getRealNormalizedPlaceInPattern(float normalizedPlaceInPattern) + { + if (style.rigidSegmentIdx != InvalidRigidSegmentIndex && stretch != 1.0f) + { + float ret = min(normalizedPlaceInPattern, rigidSegmentStart) / nonRigidSegmentStretchValue; // unstretch parts before rigid segment + ret += max(normalizedPlaceInPattern - rigidSegmentEnd, 0.0f) / nonRigidSegmentStretchValue; // unstretch parts after rigid segment + ret += max(min(rigidSegmentLen, normalizedPlaceInPattern - rigidSegmentStart), 0.0f); // unstretch parts inside rigid segment + ret *= stretch; + return ret; + } + else + { + return normalizedPlaceInPattern; + } + } + + float_t2 operator()(float_t t) + { + // basicaly 0.0 and 1.0 but with a guardband to discard outside the range + const float_t minT = 0.0 - 1.0; + const float_t maxT = 1.0 + 1.0; + + StyleAccessor styleAccessor = { style }; + const float_t reciprocalStretchedStipplePatternLen = style.reciprocalStipplePatternLen / stretch; + const float_t patternLenInScreenSpace = 1.0 / (worldToScreenRatio * style.reciprocalStipplePatternLen); + + const float_t arcLen = arcLenCalc.calcArcLen(t); + const float_t worldSpaceArcLen = arcLen * float_t(worldToScreenRatio); + float_t normalizedPlaceInPattern = frac(worldSpaceArcLen * reciprocalStretchedStipplePatternLen + phaseShift); + normalizedPlaceInPattern = getRealNormalizedPlaceInPattern(normalizedPlaceInPattern); + uint32_t patternIdx = nbl::hlsl::upper_bound(styleAccessor, 0, style.stipplePatternSize, normalizedPlaceInPattern); + + const float_t InvalidT = nbl::hlsl::numeric_limits::infinity; + float_t2 ret = float_t2(InvalidT, InvalidT); + + // odd patternIdx means a "no draw section" and current candidate should split into two nearest draw sections + const bool notInDrawSection = patternIdx & 0x1; + + // TODO[Erfan]: Disable this piece of code after clipping, and comment the reason, that the bezier start and end at 0.0 and 1.0 should be in drawable sections + float_t minDrawT = 0.0; + float_t maxDrawT = 1.0; + { + float_t normalizedPlaceInPatternBegin = frac(phaseShift); + normalizedPlaceInPatternBegin = getRealNormalizedPlaceInPattern(normalizedPlaceInPatternBegin); + uint32_t patternIdxBegin = nbl::hlsl::upper_bound(styleAccessor, 0, style.stipplePatternSize, normalizedPlaceInPatternBegin); + const bool BeginInNonDrawSection = patternIdxBegin & 0x1; + + if (BeginInNonDrawSection) + { + float_t diffToRightDrawableSection = (patternIdxBegin == style.stipplePatternSize) ? 1.0 : styleAccessor[patternIdxBegin]; + diffToRightDrawableSection -= normalizedPlaceInPatternBegin; + float_t scrSpcOffsetToArcLen1 = diffToRightDrawableSection * patternLenInScreenSpace * ((patternIdxBegin != style.rigidSegmentIdx) ? nonRigidSegmentStretchValue : 1.0); + const float_t arcLenForT1 = 0.0 + scrSpcOffsetToArcLen1; + minDrawT = arcLenCalc.calcArcLenInverse(curve, minT, maxT, arcLenForT1, AccuracyThresholdT, 0.0); + } + + // Completely in non-draw section -> clip away: + if (minDrawT >= 1.0) + return ret; + + const float_t arcLenEnd = arcLenCalc.calcArcLen(1.0); + const float_t worldSpaceArcLenEnd = arcLenEnd * float_t(worldToScreenRatio); + float_t normalizedPlaceInPatternEnd = frac(worldSpaceArcLenEnd * reciprocalStretchedStipplePatternLen + phaseShift); + normalizedPlaceInPatternEnd = getRealNormalizedPlaceInPattern(normalizedPlaceInPatternEnd); + uint32_t patternIdxEnd = nbl::hlsl::upper_bound(styleAccessor, 0, style.stipplePatternSize, normalizedPlaceInPatternEnd); + const bool EndInNonDrawSection = patternIdxEnd & 0x1; + + if (EndInNonDrawSection) + { + float_t diffToLeftDrawableSection = (patternIdxEnd == 0) ? 0.0 : styleAccessor[patternIdxEnd - 1]; + diffToLeftDrawableSection -= normalizedPlaceInPatternEnd; + float_t scrSpcOffsetToArcLen0 = diffToLeftDrawableSection * patternLenInScreenSpace * ((patternIdxEnd != style.rigidSegmentIdx) ? nonRigidSegmentStretchValue : 1.0); + const float_t arcLenForT0 = arcLenEnd + scrSpcOffsetToArcLen0; + maxDrawT = arcLenCalc.calcArcLenInverse(curve, minT, maxT, arcLenForT0, AccuracyThresholdT, 1.0); + } + } + + if (notInDrawSection) + { + float toScreenSpaceLen = patternLenInScreenSpace * ((patternIdx != style.rigidSegmentIdx) ? nonRigidSegmentStretchValue : 1.0); + + float_t diffToLeftDrawableSection = (patternIdx == 0) ? 0.0 : styleAccessor[patternIdx - 1]; + diffToLeftDrawableSection -= normalizedPlaceInPattern; + float_t scrSpcOffsetToArcLen0 = diffToLeftDrawableSection * toScreenSpaceLen; + const float_t arcLenForT0 = arcLen + scrSpcOffsetToArcLen0; + float_t t0 = arcLenCalc.calcArcLenInverse(curve, minT, maxT, arcLenForT0, AccuracyThresholdT, t); + t0 = clamp(t0, minDrawT, maxDrawT); + + float_t diffToRightDrawableSection = (patternIdx == style.stipplePatternSize) ? 1.0 : styleAccessor[patternIdx]; + diffToRightDrawableSection -= normalizedPlaceInPattern; + float_t scrSpcOffsetToArcLen1 = diffToRightDrawableSection * toScreenSpaceLen; + const float_t arcLenForT1 = arcLen + scrSpcOffsetToArcLen1; + float_t t1 = arcLenCalc.calcArcLenInverse(curve, minT, maxT, arcLenForT1, AccuracyThresholdT, t); + t1 = clamp(t1, minDrawT, maxDrawT); + + ret = float_t2(t0, t1); + } + else + { + t = clamp(t, minDrawT, maxDrawT); + ret = float_t2(t, t); + } + + return ret; + } + + LineStyle style; + CurveType curve; + typename CurveType::ArcLengthCalculator arcLenCalc; + float phaseShift; + float stretch; + float worldToScreenRatio; + // precomp value for non uniform stretching + float rigidSegmentStart; + float rigidSegmentEnd; + float rigidSegmentLen; + float nonRigidSegmentStretchValue; +}; + +template > +struct ClippedSignedDistance +{ + using float_t = typename CurveType::scalar_t; + using float_t2 = typename CurveType::float_t2; + using float_t3 = typename CurveType::float_t3; + + const static float_t sdf(CurveType curve, float_t2 pos, float_t thickness, bool isRoadStyle, Clipper clipper = DefaultClipper::construct()) + { + typename CurveType::Candidates candidates = curve.getClosestCandidates(pos); + + const float_t InvalidT = nbl::hlsl::numeric_limits::max; + // TODO: Fix and test, we're not working with squared distance anymore + const float_t MAX_DISTANCE_SQUARED = (thickness + 1.0f) * (thickness + 1.0f); // TODO: ' + 1' is too much? + + bool clipped = false; + float_t closestDistanceSquared = MAX_DISTANCE_SQUARED; + float_t closestT = InvalidT; + [[unroll(CurveType::MaxCandidates)]] + for (uint32_t i = 0; i < CurveType::MaxCandidates; i++) + { + const float_t candidateDistanceSquared = length(curve.evaluate(candidates[i]) - pos); + if (candidateDistanceSquared < closestDistanceSquared) + { + float_t2 snappedTs = clipper(candidates[i]); + + if (snappedTs[0] == InvalidT) + { + continue; + } + + if (snappedTs[0] != candidates[i]) + { + // left snapped or clamped + const float_t leftSnappedCandidateDistanceSquared = length(curve.evaluate(snappedTs[0]) - pos); + if (leftSnappedCandidateDistanceSquared < closestDistanceSquared) + { + clipped = true; + closestT = snappedTs[0]; + closestDistanceSquared = leftSnappedCandidateDistanceSquared; + } + + if (snappedTs[0] != snappedTs[1]) + { + // right snapped or clamped + const float_t rightSnappedCandidateDistanceSquared = length(curve.evaluate(snappedTs[1]) - pos); + if (rightSnappedCandidateDistanceSquared < closestDistanceSquared) + { + clipped = true; + closestT = snappedTs[1]; + closestDistanceSquared = rightSnappedCandidateDistanceSquared; + } + } + } + else + { + // no snapping + if (candidateDistanceSquared < closestDistanceSquared) + { + clipped = false; + closestT = candidates[i]; + closestDistanceSquared = candidateDistanceSquared; + } + } + } + } + + + float_t roundedDistance = closestDistanceSquared - thickness; + if(!isRoadStyle) + { + return roundedDistance; + } + else + { + const float_t aaWidth = globals.antiAliasingFactor; + float_t rectCappedDistance = roundedDistance; + + if (clipped) + { + float_t2 q = mul(curve.getLocalCoordinateSpace(closestT), pos - curve.evaluate(closestT)); + rectCappedDistance = capSquare(q, thickness, aaWidth); + } + + return rectCappedDistance; + } + } + + static float capSquare(float_t2 q, float_t th, float_t aaWidth) + { + float_t2 d = abs(q) - float_t2(aaWidth, th); + return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0); + } +}; + +// sdf of Isosceles Trapezoid y-aligned by https://iquilezles.org/articles/distfunctions2d/ +float sdTrapezoid(float2 p, float r1, float r2, float he) +{ + float2 k1 = float2(r2, he); + float2 k2 = float2(r2 - r1, 2.0 * he); + + p.x = abs(p.x); + float2 ca = float2(max(0.0, p.x - ((p.y < 0.0) ? r1 : r2)), abs(p.y) - he); + float2 cb = p - k1 + k2 * clamp(dot(k1 - p, k2) / dot(k2,k2), 0.0, 1.0); + + float s = (cb.x < 0.0 && ca.y < 0.0) ? -1.0 : 1.0; + + return s * sqrt(min(dot(ca,ca), dot(cb,cb))); +} + +// line segment sdf which returns the distance vector specialized for usage in hatch box line boundaries +float2 sdLineDstVec(float2 P, float2 A, float2 B) +{ + const float2 PA = P - A; + const float2 BA = B - A; + float h = clamp(dot(PA, BA) / dot(BA, BA), 0.0, 1.0); + return PA - BA * h; +} + +float miterSDF(float2 p, float thickness, float2 a, float2 b, float ra, float rb) +{ + float h = length(b - a) / 2.0; + float2 d = normalize(b - a); + float2x2 rot = float2x2(d.y, -d.x, d.x, d.y); + p = mul(rot, p); + p.y -= h - thickness; + return sdTrapezoid(p, ra, rb, h); +} + +typedef StyleClipper< nbl::hlsl::shapes::Quadratic > BezierStyleClipper; +typedef StyleClipper< nbl::hlsl::shapes::Line > LineStyleClipper; + +// We need to specialize color calculation based on FragmentShaderInterlock feature availability for our transparency algorithm +// because there is no `if constexpr` in hlsl +// @params +// textureColor: color sampled from a texture +// useStyleColor: instead of writing and reading from colorStorage, use main object Idx to find the style color for the object. +template +float32_t4 calculateFinalColor(const uint2 fragCoord, const float localAlpha, const uint32_t currentMainObjectIdx, float3 textureColor); + +template<> +float32_t4 calculateFinalColor(const uint2 fragCoord, const float localAlpha, const uint32_t currentMainObjectIdx, float3 localTextureColor) +{ + uint32_t styleIdx = mainObjects[currentMainObjectIdx].styleIdx; + const bool colorFromStyle = styleIdx != InvalidStyleIdx; + if (colorFromStyle) + { + float32_t4 col = lineStyles[styleIdx].color; + col.w *= localAlpha; + return float4(col); + } + else + return float4(localTextureColor, localAlpha); +} +template<> +float32_t4 calculateFinalColor(const uint2 fragCoord, const float localAlpha, const uint32_t currentMainObjectIdx, float3 localTextureColor) +{ + float32_t4 color; + + nbl::hlsl::spirv::execution_mode::PixelInterlockOrderedEXT(); + nbl::hlsl::spirv::beginInvocationInterlockEXT(); + + const uint32_t packedData = pseudoStencil[fragCoord]; + + const uint32_t localQuantizedAlpha = (uint32_t)(localAlpha * 255.f); + const uint32_t storedQuantizedAlpha = nbl::hlsl::glsl::bitfieldExtract(packedData,0,AlphaBits); + const uint32_t storedMainObjectIdx = nbl::hlsl::glsl::bitfieldExtract(packedData,AlphaBits,MainObjectIdxBits); + // if geomID has changed, we resolve the SDF alpha (draw using blend), else accumulate + const bool resolve = currentMainObjectIdx != storedMainObjectIdx; + uint32_t resolveStyleIdx = mainObjects[storedMainObjectIdx].styleIdx; + const bool resolveColorFromStyle = resolveStyleIdx != InvalidStyleIdx; + + // load from colorStorage only if we want to resolve color from texture instead of style + // sampling from colorStorage needs to happen in critical section because another fragment may also want to store into it at the same time + need to happen before store + if (resolve && !resolveColorFromStyle) + color = float32_t4(unpackR11G11B10_UNORM(colorStorage[fragCoord]), 1.0f); + + if (resolve || localQuantizedAlpha > storedQuantizedAlpha) + { + pseudoStencil[fragCoord] = nbl::hlsl::glsl::bitfieldInsert(localQuantizedAlpha,currentMainObjectIdx,AlphaBits,MainObjectIdxBits); + colorStorage[fragCoord] = packR11G11B10_UNORM(localTextureColor); + } + + nbl::hlsl::spirv::endInvocationInterlockEXT(); + + if (!resolve) + discard; + + // draw with previous geometry's style's color or stored in texture buffer :kek: + // we don't need to load the style's color in critical section because we've already retrieved the style index from the stored main obj + if (resolveColorFromStyle) + color = lineStyles[resolveStyleIdx].color; + color.a *= float(storedQuantizedAlpha) / 255.f; + + return color; +} + + +float4 main(PSInput input) : SV_TARGET +{ + float localAlpha = 0.0f; + ObjectType objType = input.getObjType(); + const uint32_t currentMainObjectIdx = input.getMainObjectIdx(); + const MainObject mainObj = mainObjects[currentMainObjectIdx]; + float3 textureColor = float3(0, 0, 0); // color sampled from a texture + + // figure out local alpha with sdf + if (objType == ObjectType::LINE || objType == ObjectType::QUAD_BEZIER || objType == ObjectType::POLYLINE_CONNECTOR) + { + float distance = nbl::hlsl::numeric_limits::max; + if (objType == ObjectType::LINE) + { + const float2 start = input.getLineStart(); + const float2 end = input.getLineEnd(); + const uint32_t styleIdx = mainObj.styleIdx; + const float thickness = input.getLineThickness(); + const float phaseShift = input.getCurrentPhaseShift(); + const float stretch = input.getPatternStretch(); + const float worldToScreenRatio = input.getCurrentWorldToScreenRatio(); + + nbl::hlsl::shapes::Line lineSegment = nbl::hlsl::shapes::Line::construct(start, end); + nbl::hlsl::shapes::Line::ArcLengthCalculator arcLenCalc = nbl::hlsl::shapes::Line::ArcLengthCalculator::construct(lineSegment); + + LineStyle style = lineStyles[styleIdx]; + + if (!style.hasStipples() || stretch == InvalidStyleStretchValue) + { + distance = ClippedSignedDistance< nbl::hlsl::shapes::Line >::sdf(lineSegment, input.position.xy, thickness, style.isRoadStyleFlag); + } + else + { + LineStyleClipper clipper = LineStyleClipper::construct(lineStyles[styleIdx], lineSegment, arcLenCalc, phaseShift, stretch, worldToScreenRatio); + distance = ClippedSignedDistance, LineStyleClipper>::sdf(lineSegment, input.position.xy, thickness, style.isRoadStyleFlag, clipper); + } + } + else if (objType == ObjectType::QUAD_BEZIER) + { + nbl::hlsl::shapes::Quadratic quadratic = input.getQuadratic(); + nbl::hlsl::shapes::Quadratic::ArcLengthCalculator arcLenCalc = input.getQuadraticArcLengthCalculator(); + + const uint32_t styleIdx = mainObj.styleIdx; + const float thickness = input.getLineThickness(); + const float phaseShift = input.getCurrentPhaseShift(); + const float stretch = input.getPatternStretch(); + const float worldToScreenRatio = input.getCurrentWorldToScreenRatio(); + + LineStyle style = lineStyles[styleIdx]; + if (!style.hasStipples() || stretch == InvalidStyleStretchValue) + { + distance = ClippedSignedDistance< nbl::hlsl::shapes::Quadratic >::sdf(quadratic, input.position.xy, thickness, style.isRoadStyleFlag); + } + else + { + BezierStyleClipper clipper = BezierStyleClipper::construct(lineStyles[styleIdx], quadratic, arcLenCalc, phaseShift, stretch, worldToScreenRatio); + distance = ClippedSignedDistance, BezierStyleClipper>::sdf(quadratic, input.position.xy, thickness, style.isRoadStyleFlag, clipper); + } + } + else if (objType == ObjectType::POLYLINE_CONNECTOR) + { + const float2 P = input.position.xy - input.getPolylineConnectorCircleCenter(); + distance = miterSDF( + P, + input.getLineThickness(), + input.getPolylineConnectorTrapezoidStart(), + input.getPolylineConnectorTrapezoidEnd(), + input.getPolylineConnectorTrapezoidLongBase(), + input.getPolylineConnectorTrapezoidShortBase()); + + } + localAlpha = smoothstep(+globals.antiAliasingFactor, -globals.antiAliasingFactor, distance); + } + else if (objType == ObjectType::CURVE_BOX) + { + const float minorBBoxUV = input.getMinorBBoxUV(); + const float majorBBoxUV = input.getMajorBBoxUV(); + + nbl::hlsl::math::equations::Quadratic curveMinMinor = input.getCurveMinMinor(); + nbl::hlsl::math::equations::Quadratic curveMinMajor = input.getCurveMinMajor(); + nbl::hlsl::math::equations::Quadratic curveMaxMinor = input.getCurveMaxMinor(); + nbl::hlsl::math::equations::Quadratic curveMaxMajor = input.getCurveMaxMajor(); + + // TODO(Optimization): Can we ignore this majorBBoxUV clamp and rely on the t clamp that happens next? then we can pass `PrecomputedRootFinder`s instead of computing the values per pixel. + nbl::hlsl::math::equations::Quadratic minCurveEquation = nbl::hlsl::math::equations::Quadratic::construct(curveMinMajor.a, curveMinMajor.b, curveMinMajor.c - clamp(majorBBoxUV, 0.0, 1.0)); + nbl::hlsl::math::equations::Quadratic maxCurveEquation = nbl::hlsl::math::equations::Quadratic::construct(curveMaxMajor.a, curveMaxMajor.b, curveMaxMajor.c - clamp(majorBBoxUV, 0.0, 1.0)); + + const float minT = clamp(PrecomputedRootFinder::construct(minCurveEquation).computeRoots(), 0.0, 1.0); + const float minEv = curveMinMinor.evaluate(minT); + + const float maxT = clamp(PrecomputedRootFinder::construct(maxCurveEquation).computeRoots(), 0.0, 1.0); + const float maxEv = curveMaxMinor.evaluate(maxT); + + const bool insideMajor = majorBBoxUV >= 0.0 && majorBBoxUV <= 1.0; + const bool insideMinor = minorBBoxUV >= minEv && minorBBoxUV <= maxEv; + + if (insideMinor && insideMajor) + { + localAlpha = 1.0; + } + else + { + // Find the true SDF of a hatch box boundary which is bounded by two curves, It requires knowing the distance from the current UV to the closest point on bounding curves and the limiting lines (in major direction) + // We also keep track of distance vector (minor, major) to convert to screenspace distance for anti-aliasing with screenspace aaFactor + const float InvalidT = nbl::hlsl::numeric_limits::max; + const float MAX_DISTANCE_SQUARED = nbl::hlsl::numeric_limits::max; + + const float2 boxScreenSpaceSize = input.getCurveBoxScreenSpaceSize(); + + + float closestDistanceSquared = MAX_DISTANCE_SQUARED; + const float2 pos = float2(minorBBoxUV, majorBBoxUV) * boxScreenSpaceSize; + + if (minorBBoxUV < minEv) + { + // DO SDF of Min Curve + nbl::hlsl::shapes::Quadratic minCurve = nbl::hlsl::shapes::Quadratic::construct( + float2(curveMinMinor.a, curveMinMajor.a) * boxScreenSpaceSize, + float2(curveMinMinor.b, curveMinMajor.b) * boxScreenSpaceSize, + float2(curveMinMinor.c, curveMinMajor.c) * boxScreenSpaceSize); + + nbl::hlsl::shapes::Quadratic::Candidates candidates = minCurve.getClosestCandidates(pos); + [[unroll(nbl::hlsl::shapes::Quadratic::MaxCandidates)]] + for (uint32_t i = 0; i < nbl::hlsl::shapes::Quadratic::MaxCandidates; i++) + { + candidates[i] = clamp(candidates[i], 0.0, 1.0); + const float2 distVector = minCurve.evaluate(candidates[i]) - pos; + const float candidateDistanceSquared = dot(distVector, distVector); + if (candidateDistanceSquared < closestDistanceSquared) + closestDistanceSquared = candidateDistanceSquared; + } + } + else if (minorBBoxUV > maxEv) + { + // Do SDF of Max Curve + nbl::hlsl::shapes::Quadratic maxCurve = nbl::hlsl::shapes::Quadratic::construct( + float2(curveMaxMinor.a, curveMaxMajor.a) * boxScreenSpaceSize, + float2(curveMaxMinor.b, curveMaxMajor.b) * boxScreenSpaceSize, + float2(curveMaxMinor.c, curveMaxMajor.c) * boxScreenSpaceSize); + nbl::hlsl::shapes::Quadratic::Candidates candidates = maxCurve.getClosestCandidates(pos); + [[unroll(nbl::hlsl::shapes::Quadratic::MaxCandidates)]] + for (uint32_t i = 0; i < nbl::hlsl::shapes::Quadratic::MaxCandidates; i++) + { + candidates[i] = clamp(candidates[i], 0.0, 1.0); + const float2 distVector = maxCurve.evaluate(candidates[i]) - pos; + const float candidateDistanceSquared = dot(distVector, distVector); + if (candidateDistanceSquared < closestDistanceSquared) + closestDistanceSquared = candidateDistanceSquared; + } + } + + if (!insideMajor) + { + const bool minLessThanMax = minEv < maxEv; + float2 majorDistVector = float2(MAX_DISTANCE_SQUARED, MAX_DISTANCE_SQUARED); + if (majorBBoxUV > 1.0) + { + const float2 minCurveEnd = float2(minEv, 1.0) * boxScreenSpaceSize; + if (minLessThanMax) + majorDistVector = sdLineDstVec(pos, minCurveEnd, float2(maxEv, 1.0) * boxScreenSpaceSize); + else + majorDistVector = pos - minCurveEnd; + } + else + { + const float2 minCurveStart = float2(minEv, 0.0) * boxScreenSpaceSize; + if (minLessThanMax) + majorDistVector = sdLineDstVec(pos, minCurveStart, float2(maxEv, 0.0) * boxScreenSpaceSize); + else + majorDistVector = pos - minCurveStart; + } + + const float majorDistSq = dot(majorDistVector, majorDistVector); + if (majorDistSq < closestDistanceSquared) + closestDistanceSquared = majorDistSq; + } + + const float dist = sqrt(closestDistanceSquared); + localAlpha = 1.0f - smoothstep(0.0, globals.antiAliasingFactor, dist); + } + + LineStyle style = lineStyles[mainObj.styleIdx]; + uint32_t textureId = asuint(style.screenSpaceLineWidth); + if (textureId != InvalidTextureIdx) + { + // For Hatch fiils we sample the first mip as we don't fill the others, because they are constant in screenspace and render as expected + // If later on we decided that we can have different sizes here, we should do computations similar to FONT_GLYPH + float3 msdfSample = msdfTextures.SampleLevel(msdfSampler, float3(frac(input.position.xy / HatchFillMSDFSceenSpaceSize), float(textureId)), 0.0).xyz; + float msdf = nbl::hlsl::text::msdfDistance(msdfSample, MSDFPixelRange * HatchFillMSDFSceenSpaceSize / MSDFSize); + localAlpha *= smoothstep(+globals.antiAliasingFactor / 2.0, -globals.antiAliasingFactor / 2.0f, msdf); + } + } + else if (objType == ObjectType::FONT_GLYPH) + { + const float2 uv = input.getFontGlyphUV(); + const uint32_t textureId = input.getFontGlyphTextureId(); + + if (textureId != InvalidTextureIdx) + { + float mipLevel = msdfTextures.CalculateLevelOfDetail(msdfSampler, uv); + float3 msdfSample = msdfTextures.SampleLevel(msdfSampler, float3(uv, float(textureId)), mipLevel); + float msdf = nbl::hlsl::text::msdfDistance(msdfSample, input.getFontGlyphScreenPxRange()); + /* + explaining "*= exp2(max(mipLevel,0.0))" + Each mip level has constant MSDFPixelRange + Which essentially makes the msdfSamples here (Harware Sampled) have different scales per mip + As we go up 1 mip level, the msdf distance should be multiplied by 2.0 + While this makes total sense for NEAREST mip sampling when mipLevel is an integer and only one mip is being sampled. + It's a bit complex when it comes to trilinear filtering (LINEAR mip sampling), but it works in practice! + + Alternatively you can think of it as doing this instead: + localAlpha = smoothstep(+globals.antiAliasingFactor / exp2(max(mipLevel,0.0)), 0.0, msdf); + Which is reducing the aa feathering as we go up the mip levels. + to avoid aa feathering of the MAX_MSDF_DISTANCE_VALUE to be less than aa factor and eventually color it and cause greyed out area around the main glyph + */ + msdf *= exp2(max(mipLevel,0.0)); + localAlpha = smoothstep(+globals.antiAliasingFactor / 2.0f, -globals.antiAliasingFactor / 2.0f, msdf); + } + } + else if (objType == ObjectType::IMAGE) + { + const float2 uv = input.getImageUV(); + const uint32_t textureId = input.getImageTextureId(); + + if (textureId != InvalidTextureIdx) + { + float4 colorSample = textures[NonUniformResourceIndex(textureId)].Sample(textureSampler, float2(uv.x, uv.y)); + textureColor = colorSample.rgb; + localAlpha = colorSample.a; + } + } + + uint2 fragCoord = uint2(input.position.xy); + + if (localAlpha <= 0) + discard; + + return calculateFinalColor(fragCoord, localAlpha, currentMainObjectIdx, textureColor); +} diff --git a/62_CAD/fragment_shader_debug.hlsl b/62_CAD/fragment_shader_debug.hlsl new file mode 100644 index 000000000..9b9de0826 --- /dev/null +++ b/62_CAD/fragment_shader_debug.hlsl @@ -0,0 +1,16 @@ + +#pragma shader_stage(fragment) + +struct PSInput +{ + float4 position : SV_Position; + [[vk::location(0)]] float4 color : COLOR; + [[vk::location(1)]] nointerpolation float4 start_end : COLOR1; + [[vk::location(2)]] nointerpolation uint3 lineWidth_eccentricity_objType : COLOR2; +}; + +float4 main(PSInput input) : SV_TARGET +{ + return float4(1.0, 1.0, 1.0, 1.0); +// return input.color; +} \ No newline at end of file diff --git a/62_CAD/main.cpp b/62_CAD/main.cpp index 6cde0a8d6..1c9ac5586 100644 --- a/62_CAD/main.cpp +++ b/62_CAD/main.cpp @@ -1,8 +1,4 @@ -// TODO: Copyright notice -#include "nbl/this_example/builtin/build/spirv/keys.hpp" - -#include "nbl/examples/examples.hpp" - + using namespace nbl::hlsl; using namespace nbl; using namespace core; @@ -11,9 +7,10 @@ using namespace asset; using namespace ui; using namespace video; -#include "nbl/examples/common/BuiltinResourcesApplication.hpp" -#include "nbl/examples/common/SimpleWindowedApplication.hpp" -#include "nbl/examples/common/InputSystem.hpp" + +#include "nbl/application_templates/MonoAssetManagerAndBuiltinResourceApplication.hpp" +#include "SimpleWindowedApplication.hpp" +#include "InputSystem.hpp" #include "nbl/video/utilities/CSimpleResizeSurface.h" #include "nbl/ext/FullScreenTriangle/FullScreenTriangle.h" @@ -33,17 +30,16 @@ using namespace video; #include "HatchGlyphBuilder.h" -#include - #include #define BENCHMARK_TILL_FIRST_FRAME +// Shader cache tests. Only define one of these, or none for no use of the Cache +//#define SHADER_CACHE_TEST_COMPILATION_CACHE_STORE +//#define SHADER_CACHE_TEST_CACHE_RETRIEVE + static constexpr bool DebugModeWireframe = false; static constexpr bool DebugRotatingViewProj = false; static constexpr bool FragmentShaderPixelInterlock = true; -static constexpr bool LargeGeoTextureStreaming = true; -static constexpr bool CacheAndReplay = false; // caches first frame resources (buffers and images) from DrawResourcesFiller and replays in future frames, skiping CPU Logic -static constexpr bool testCameraRotation = false; enum class ExampleMode { @@ -56,10 +52,6 @@ enum class ExampleMode CASE_6, // Custom Clip Projections CASE_7, // Images CASE_8, // MSDF and Text - CASE_9, // DTM - CASE_10, // testing fixed geometry and emulated fp64 corner cases - CASE_11, // grid DTM - CASE_12, // Georeferenced streamed images CASE_COUNT }; @@ -74,13 +66,9 @@ constexpr std::array cameraExtents = 10.0, // CASE_6 10.0, // CASE_7 600.0, // CASE_8 - 600.0, // CASE_9 - 10.0, // CASE_10 - 1000.0, // CASE_11 - 10.0 // CASE_12 }; -constexpr ExampleMode mode = ExampleMode::CASE_4; +constexpr ExampleMode mode = ExampleMode::CASE_7; class Camera2D { @@ -130,7 +118,7 @@ class Camera2D if (ev.type == nbl::ui::SMouseEvent::EET_SCROLL) { - m_bounds = m_bounds + float64_t2{ (double)ev.scrollEvent.verticalScroll * -0.025 * m_aspectRatio, (double)ev.scrollEvent.verticalScroll * -0.025}; + m_bounds = m_bounds + float64_t2{ (double)ev.scrollEvent.verticalScroll * -0.1 * m_aspectRatio, (double)ev.scrollEvent.verticalScroll * -0.1}; m_bounds = float64_t2{ core::max(m_aspectRatio, m_bounds.x), core::max(1.0, m_bounds.y) }; } } @@ -170,14 +158,14 @@ class Camera2D class CEventCallback : public ISimpleManagedSurface::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)){} + 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)){} CEventCallback() {} void setLogger(nbl::system::logger_opt_smart_ptr& logger) { m_logger = logger; } - void setInputSystem(nbl::core::smart_refctd_ptr&& m_inputSystem) + void setInputSystem(nbl::core::smart_refctd_ptr&& m_inputSystem) { m_inputSystem = std::move(m_inputSystem); } @@ -205,7 +193,7 @@ class CEventCallback : public ISimpleManagedSurface::ICallback } private: - nbl::core::smart_refctd_ptr m_inputSystem = nullptr; + nbl::core::smart_refctd_ptr m_inputSystem = nullptr; nbl::system::logger_opt_smart_ptr m_logger = nullptr; }; @@ -243,7 +231,7 @@ class CSwapchainResources : public ISimpleManagedSurface::ISwapchainResources std::fill(m_framebuffers.begin(),m_framebuffers.end(),nullptr); } - // For creating extra per-image or swapchain resourcesCollection you might need + // For creating extra per-image or swapchain resources you might need virtual inline bool onCreateSwapchain_impl(const uint8_t qFam) { auto device = const_cast(m_renderpass->getOriginDevice()); @@ -277,319 +265,47 @@ class CSwapchainResources : public ISimpleManagedSurface::ISwapchainResources std::array,ISwapchain::MaxImages> m_framebuffers; }; - -// TODO: Move this funcitons that help with creating a new promoted CPUImage -template -struct PromotionComponentSwizzle -{ - template - void operator()(const InT* in, OutT* out) const - { - using in_t = std::conditional_t, uint64_t, InT>; - using out_t = std::conditional_t, uint64_t, OutT>; - - reinterpret_cast(out)[0u] = reinterpret_cast(in)[0u]; - - if constexpr (SRC_CHANNELS > 1) - reinterpret_cast(out)[1u] = reinterpret_cast(in)[1u]; - else - reinterpret_cast(out)[1u] = static_cast(0); - - if constexpr (SRC_CHANNELS > 2) - reinterpret_cast(out)[2u] = reinterpret_cast(in)[2u]; - else - reinterpret_cast(out)[2u] = static_cast(0); - - if constexpr (SRC_CHANNELS > 3) - reinterpret_cast(out)[3u] = reinterpret_cast(in)[3u]; - else - reinterpret_cast(out)[3u] = static_cast(1); - } -}; -template -bool performCopyUsingImageFilter( - const core::smart_refctd_ptr& inCPUImage, - const core::smart_refctd_ptr& outCPUImage) +class ComputerAidedDesign final : public examples::SimpleWindowedApplication, public application_templates::MonoAssetManagerAndBuiltinResourceApplication { - Filter filter; - - const uint32_t mipLevels = inCPUImage->getCreationParameters().mipLevels; - - for (uint32_t level = 0u; level < mipLevels; ++level) - { - const auto regions = inCPUImage->getRegions(level); - - for (auto& region : regions) - { - typename Filter::state_type state = {}; - state.extent = region.imageExtent; - state.layerCount = region.imageSubresource.layerCount; - state.inImage = inCPUImage.get(); - state.outImage = outCPUImage.get(); - state.inOffsetBaseLayer = core::vectorSIMDu32(region.imageOffset.x, region.imageOffset.y, region.imageOffset.z, region.imageSubresource.baseArrayLayer); - state.outOffsetBaseLayer = core::vectorSIMDu32(0u); - state.inMipLevel = region.imageSubresource.mipLevel; - state.outMipLevel = region.imageSubresource.mipLevel; - - if (!filter.execute(core::execution::par_unseq, &state)) - return false; - } - } - return true; -} - -bool performImageFormatPromotionCopy(const core::smart_refctd_ptr& inCPUImage, const core::smart_refctd_ptr& outCPUImage) -{ - asset::E_FORMAT srcImageFormat = inCPUImage->getCreationParameters().format; - asset::E_FORMAT dstImageFormat = outCPUImage->getCreationParameters().format; - - // In = srcData, Out = stagingBuffer - if (srcImageFormat == dstImageFormat) - return false; - - auto srcChannelCount = asset::getFormatChannelCount(srcImageFormat); - if (srcChannelCount == 1u) - return performCopyUsingImageFilter>>(inCPUImage, outCPUImage); - else if (srcChannelCount == 2u) - return performCopyUsingImageFilter>>(inCPUImage, outCPUImage); - else if (srcChannelCount == 3u) - return performCopyUsingImageFilter>>(inCPUImage, outCPUImage); - else - return performCopyUsingImageFilter>>(inCPUImage, outCPUImage); -} - -// Used by case 12 -struct ImageLoader : public IImageRegionLoader -{ - ImageLoader(asset::IAssetManager* assetMgr, system::ILogger* logger, video::IPhysicalDevice* physicalDevice) - : m_assetMgr(assetMgr), m_logger(logger), m_physicalDevice(physicalDevice) - { - auto loadImage = [&](const std::string& imagePath) -> smart_refctd_ptr - { - system::path m_loadCWD = ".."; - constexpr auto cachingFlags = static_cast(IAssetLoader::ECF_DONT_CACHE_REFERENCES & IAssetLoader::ECF_DONT_CACHE_TOP_LEVEL); - const IAssetLoader::SAssetLoadParams loadParams(0ull, nullptr, cachingFlags, IAssetLoader::ELPF_NONE, m_logger, m_loadCWD); - auto bundle = m_assetMgr->getAsset(imagePath, loadParams); - auto contents = bundle.getContents(); - if (contents.empty()) - { - m_logger->log("Failed to load image with path %s, skipping!", ILogger::ELL_ERROR, (m_loadCWD / imagePath).c_str()); - return nullptr; - } - - smart_refctd_ptr cpuImgView; - const auto& asset = contents[0]; - switch (asset->getAssetType()) - { - case IAsset::ET_IMAGE: - { - auto image = smart_refctd_ptr_static_cast(asset); - auto& flags = image->getCreationParameters().flags; - // assert if asset is mutable - const_cast&>(flags) |= asset::IImage::E_CREATE_FLAGS::ECF_MUTABLE_FORMAT_BIT; - const auto format = image->getCreationParameters().format; - - ICPUImageView::SCreationParams viewParams = { - .flags = ICPUImageView::E_CREATE_FLAGS::ECF_NONE, - .image = std::move(image), - .viewType = IImageView::E_TYPE::ET_2D, - .format = format, - .subresourceRange = { - .aspectMask = IImage::E_ASPECT_FLAGS::EAF_COLOR_BIT, - .baseMipLevel = 0u, - .levelCount = ICPUImageView::remaining_mip_levels, - .baseArrayLayer = 0u, - .layerCount = ICPUImageView::remaining_array_layers - } - }; - - cpuImgView = ICPUImageView::create(std::move(viewParams)); - } break; - - case IAsset::ET_IMAGE_VIEW: - cpuImgView = smart_refctd_ptr_static_cast(asset); - break; - default: - m_logger->log("Failed to load ICPUImage or ICPUImageView got some other Asset Type, skipping!", ILogger::ELL_ERROR); - return nullptr; - } - - const auto loadedCPUImage = cpuImgView->getCreationParameters().image; - const auto loadedCPUImageCreationParams = loadedCPUImage->getCreationParameters(); - - // Promoting the image to a format GPU supports. (so that updateImageViaStagingBuffer doesn't have to handle that each frame if overflow-submit needs to happen) - auto promotedCPUImageCreationParams = loadedCPUImage->getCreationParameters(); - - promotedCPUImageCreationParams.usage |= IGPUImage::EUF_TRANSFER_DST_BIT | IGPUImage::EUF_SAMPLED_BIT; - // promote format because RGB8 and friends don't actually exist in HW - { - const IPhysicalDevice::SImageFormatPromotionRequest request = { - .originalFormat = promotedCPUImageCreationParams.format, - .usages = IPhysicalDevice::SFormatImageUsages::SUsage(promotedCPUImageCreationParams.usage) - }; - promotedCPUImageCreationParams.format = m_physicalDevice->promoteImageFormat(request, video::IGPUImage::TILING::OPTIMAL); - } - - if (loadedCPUImageCreationParams.format != promotedCPUImageCreationParams.format) - { - smart_refctd_ptr promotedCPUImage = ICPUImage::create(promotedCPUImageCreationParams); - core::rational bytesPerPixel = asset::getBytesPerPixel(promotedCPUImageCreationParams.format); - - const auto extent = loadedCPUImageCreationParams.extent; - const uint32_t mipLevels = loadedCPUImageCreationParams.mipLevels; - const uint32_t arrayLayers = loadedCPUImageCreationParams.arrayLayers; - - // Only supporting 1 mip, it's just for test.. - const size_t byteSize = (bytesPerPixel * extent.width * extent.height * extent.depth * arrayLayers).getIntegerApprox(); // TODO: consider mips - ICPUBuffer::SCreationParams bufferCreationParams = {}; - bufferCreationParams.size = byteSize; - smart_refctd_ptr promotedCPUImageBuffer = ICPUBuffer::create(std::move(bufferCreationParams)); - - auto newRegions = core::make_refctd_dynamic_array>(1u); - ICPUImage::SBufferCopy& region = newRegions->front(); - region.imageSubresource.aspectMask = IImage::E_ASPECT_FLAGS::EAF_COLOR_BIT; - region.imageSubresource.mipLevel = 0u; // TODO - region.imageSubresource.baseArrayLayer = 0u; - region.imageSubresource.layerCount = arrayLayers; - region.bufferOffset = 0u; - region.bufferRowLength = 0u; - region.bufferImageHeight = 0u; - region.imageOffset = { 0u, 0u, 0u }; - region.imageExtent = extent; - promotedCPUImage->setBufferAndRegions(std::move(promotedCPUImageBuffer), newRegions); - - performImageFormatPromotionCopy(loadedCPUImage, promotedCPUImage); - return promotedCPUImage; - } - else - { - return loadedCPUImage; - } - }; - - // This is all hardcoded for the example - const std::string basePath = "../../media/npot_geotex_mip_"; - smart_refctd_ptr img = loadImage(basePath + "0_base.png"); - - // This is hardcoded - const uint32_t maxMipLevel = 7; - baseMipLevels.reserve(maxMipLevel + 1); - baseMipLevels.emplace_back(std::move(img)); - for (auto i = 1u; i <= maxMipLevel; i++) - { - baseMipLevels.emplace_back(loadImage(basePath + std::to_string(i) + "_base.png")); - } - downsampledMipLevels.reserve(maxMipLevel + 1); - for (auto i = 0u; i <= maxMipLevel; i++) - { - downsampledMipLevels.emplace_back(loadImage(basePath + std::to_string(i) + "_downsampled.png")); - } - } - - uint32_t2 getExtents(std::filesystem::path imagePath, uint32_t mipLevel) - { - return { baseMipLevels[mipLevel]->getCreationParameters().extent.width, baseMipLevels[mipLevel]->getCreationParameters().extent.height }; - } - - uint32_t2 getExtents(std::filesystem::path imagePath) override - { - return getExtents(imagePath, 0); - } - - asset::E_FORMAT getFormat(std::filesystem::path imagePath) override - { - return baseMipLevels[0]->getCreationParameters().format; - } - - bool hasPrecomputedMips(std::filesystem::path imagePath) const override - { - return true; - } - -private: - - // Assume offset always fits in the image, but maybe offset + extent doesn't - // Example of a precomputed mip loader with 2x mip levels - core::smart_refctd_ptr load_impl(std::filesystem::path imagePath, uint32_t2 offset, uint32_t2 extent, uint32_t mipLevel, bool downsample) override - { - // Hardcoded tile size that's not accessible - auto mippedImageExtents = getExtents(imagePath, mipLevel); - // If `offset + extent` exceeds the extent of the image at the current mip level, we clamp it - extent = nbl::hlsl::min(mippedImageExtents - offset, extent); - // Image path ignored for this hardcoded example - const auto& image = downsample ? downsampledMipLevels[mipLevel] : baseMipLevels[mipLevel]; - const auto& imageBuffer = image->getBuffer(); - const core::rational bytesPerPixel = asset::getBytesPerPixel(image->getCreationParameters().format); - const size_t bytesPerRow = (bytesPerPixel * extent.x).getIntegerApprox(); - const size_t loadedImageBytes = bytesPerRow * extent.y; - asset::IBuffer::SCreationParams bufCreationParams = { .size = loadedImageBytes, .usage = imageBuffer->getCreationParams().usage }; - ICPUBuffer::SCreationParams cpuBufCreationParams(std::move(bufCreationParams)); - core::smart_refctd_ptr retVal = ICPUBuffer::create(std::move(cpuBufCreationParams)); - - // Copy row by row into the new buffer - uint8_t* dataPtr = reinterpret_cast(retVal->getPointer()); - const uint8_t* imageBufferDataPtr = reinterpret_cast(imageBuffer->getPointer()); - const size_t bytesPerImageRow = (bytesPerPixel * image->getCreationParameters().extent.width).getIntegerApprox(); - for (auto row = 0u; row < extent.y; row++) - { - const size_t imageBufferOffset = bytesPerImageRow * (offset.y + row) + (bytesPerPixel * offset.x).getIntegerApprox(); - std::memcpy(dataPtr + row * bytesPerRow, imageBufferDataPtr + imageBufferOffset, bytesPerRow); - } - return retVal; - } - - // These are here for the example, might not be class members when porting to n4ce - asset::IAssetManager* m_assetMgr = {}; - system::ILogger* m_logger = {}; - video::IPhysicalDevice* m_physicalDevice = {}; - // We're going to fake it in the example so it's easier to work with, but the interface remains - core::vector> baseMipLevels = {}; - core::vector> downsampledMipLevels = {}; -}; - -class ComputerAidedDesign final : public nbl::examples::SimpleWindowedApplication, public nbl::examples::BuiltinResourcesApplication -{ - using device_base_t = nbl::examples::SimpleWindowedApplication; - using asset_base_t = nbl::examples::BuiltinResourcesApplication; + using device_base_t = examples::SimpleWindowedApplication; + using asset_base_t = application_templates::MonoAssetManagerAndBuiltinResourceApplication; using clock_t = std::chrono::steady_clock; constexpr static uint32_t WindowWidthRequest = 1600u; constexpr static uint32_t WindowHeightRequest = 900u; - constexpr static uint32_t MaxFramesInFlight = 3u; - constexpr static uint32_t MaxSubmitsInFlight = 16u; + constexpr static uint32_t MaxFramesInFlight = 8u; public: - void allocateResources() + void allocateResources(uint32_t maxObjects) { - // TODO: currently using the same utils for buffers and images, make them separate staging buffers - drawResourcesFiller = DrawResourcesFiller(core::smart_refctd_ptr(m_device), core::smart_refctd_ptr(m_utils), core::smart_refctd_ptr(m_utils), getGraphicsQueue(), core::smart_refctd_ptr(m_logger)); - - // Just wanting to try memory type indices with device local flag, TODO: later improve to prioritize pure device local - std::vector deviceLocalMemoryTypeIndices; - for (uint32_t i = 0u; i < m_physicalDevice->getMemoryProperties().memoryTypeCount; ++i) - { - const auto& memType = m_physicalDevice->getMemoryProperties().memoryTypes[i]; - if (memType.propertyFlags.hasFlags(IDeviceMemoryAllocation::EMPF_DEVICE_LOCAL_BIT)) - deviceLocalMemoryTypeIndices.push_back(i); - } - - size_t maxImagesMemSize = 1024ull * 1024ull * 1024ull; // 1024 MB - size_t maxBufferMemSize = 1024ull * 1024ull * 1024ull; // 1024 MB - - drawResourcesFiller.allocateDrawResourcesWithinAvailableVRAM(m_device.get(), maxImagesMemSize, maxBufferMemSize, deviceLocalMemoryTypeIndices); - drawResourcesFiller.allocateMSDFTextures(m_device.get(), 256u, uint32_t2(MSDFSize, MSDFSize)); + drawResourcesFiller = DrawResourcesFiller(core::smart_refctd_ptr(m_utils), getGraphicsQueue()); + + // TODO: move individual allocations to DrawResourcesFiller::allocateResources(memory) + // Issue warning error, if we can't store our largest geomm struct + clip proj data inside geometry buffer along linestyle and mainObject + uint32_t maxIndices = maxObjects * 6u * 2u; + drawResourcesFiller.allocateIndexBuffer(m_device.get(), maxIndices); + drawResourcesFiller.allocateMainObjectsBuffer(m_device.get(), maxObjects); + drawResourcesFiller.allocateDrawObjectsBuffer(m_device.get(), maxObjects * 5u); + drawResourcesFiller.allocateStylesBuffer(m_device.get(), 32u); + + // * 3 because I just assume there is on average 3x beziers per actual object (cause we approximate other curves/arcs with beziers now) + // + 128 ClipProjData + size_t geometryBufferSize = maxObjects * sizeof(QuadraticBezierInfo) * 3 + 128 * sizeof(ClipProjectionData); + drawResourcesFiller.allocateGeometryBuffer(m_device.get(), geometryBufferSize); + drawResourcesFiller.allocateMSDFTextures(m_device.get(), 512u, uint32_t2(MSDFSize, MSDFSize)); { IGPUBuffer::SCreationParams globalsCreationParams = {}; globalsCreationParams.size = sizeof(Globals); globalsCreationParams.usage = IGPUBuffer::EUF_UNIFORM_BUFFER_BIT | IGPUBuffer::EUF_TRANSFER_DST_BIT | IGPUBuffer::EUF_INLINE_UPDATE_VIA_CMDBUF; - m_globalsBuffer = m_device->createBuffer(std::move(globalsCreationParams)); + globalsBuffer = m_device->createBuffer(std::move(globalsCreationParams)); - IDeviceMemoryBacked::SDeviceMemoryRequirements memReq = m_globalsBuffer->getMemoryReqs(); + IDeviceMemoryBacked::SDeviceMemoryRequirements memReq = globalsBuffer->getMemoryReqs(); memReq.memoryTypeBits &= m_device->getPhysicalDevice()->getDeviceLocalMemoryTypeBits(); - auto globalsBufferMem = m_device->allocate(memReq, m_globalsBuffer.get()); + auto globalsBufferMem = m_device->allocate(memReq, globalsBuffer.get()); } + // pseudoStencil { asset::E_FORMAT pseudoStencilFormat = asset::EF_R32_UINT; @@ -670,44 +386,22 @@ class ComputerAidedDesign final : public nbl::examples::SimpleWindowedApplicatio } } - // MSDF Image Sampler - { - IGPUSampler::SParams samplerParams = {}; - samplerParams.TextureWrapU = IGPUSampler::E_TEXTURE_CLAMP::ETC_CLAMP_TO_BORDER; - samplerParams.TextureWrapV = IGPUSampler::E_TEXTURE_CLAMP::ETC_CLAMP_TO_BORDER; - samplerParams.TextureWrapW = IGPUSampler::E_TEXTURE_CLAMP::ETC_CLAMP_TO_BORDER; - samplerParams.BorderColor = IGPUSampler::ETBC_FLOAT_OPAQUE_WHITE; // positive means outside shape - samplerParams.MinFilter = IGPUSampler::ETF_LINEAR; - samplerParams.MaxFilter = IGPUSampler::ETF_LINEAR; - samplerParams.MipmapMode = IGPUSampler::ESMM_LINEAR; - samplerParams.AnisotropicFilter = 3; - samplerParams.CompareEnable = false; - samplerParams.CompareFunc = ECO_GREATER; - samplerParams.LodBias = 0.f; - samplerParams.MinLod = -1000.f; - samplerParams.MaxLod = 1000.f; - msdfImageSampler = m_device->createSampler(samplerParams); - } - - // Static Image Sampler - { - IGPUSampler::SParams samplerParams = {}; - samplerParams.TextureWrapU = IGPUSampler::E_TEXTURE_CLAMP::ETC_REPEAT; - samplerParams.TextureWrapV = IGPUSampler::E_TEXTURE_CLAMP::ETC_REPEAT; - samplerParams.TextureWrapW = IGPUSampler::E_TEXTURE_CLAMP::ETC_REPEAT; - samplerParams.BorderColor = IGPUSampler::ETBC_FLOAT_TRANSPARENT_BLACK; - samplerParams.MinFilter = IGPUSampler::ETF_LINEAR; - samplerParams.MaxFilter = IGPUSampler::ETF_LINEAR; - samplerParams.MipmapMode = IGPUSampler::ESMM_LINEAR; - samplerParams.AnisotropicFilter = 3; - samplerParams.CompareEnable = false; - samplerParams.CompareFunc = ECO_GREATER; - samplerParams.LodBias = 0.f; - samplerParams.MinLod = -1000.f; - samplerParams.MaxLod = 1000.f; - staticImageSampler = m_device->createSampler(samplerParams); - } - + IGPUSampler::SParams samplerParams = {}; + samplerParams.TextureWrapU = IGPUSampler::ETC_CLAMP_TO_BORDER; + samplerParams.TextureWrapV = IGPUSampler::ETC_CLAMP_TO_BORDER; + samplerParams.TextureWrapW = IGPUSampler::ETC_CLAMP_TO_BORDER; + samplerParams.BorderColor = IGPUSampler::ETBC_FLOAT_OPAQUE_WHITE; // positive means outside shape + samplerParams.MinFilter = IGPUSampler::ETF_LINEAR; + samplerParams.MaxFilter = IGPUSampler::ETF_LINEAR; + samplerParams.MipmapMode = IGPUSampler::ESMM_LINEAR; + samplerParams.AnisotropicFilter = 3; + samplerParams.CompareEnable = false; + samplerParams.CompareFunc = ECO_GREATER; + samplerParams.LodBias = 0.f; + samplerParams.MinLod = -1000.f; + samplerParams.MaxLod = 1000.f; + msdfTextureSampler = m_device->createSampler(samplerParams); + // Initial Pipeline Transitions and Clearing of PseudoStencil and ColorStorage // Recorded to Temporary CommandBuffer, Submitted to Graphics Queue, and Blocked on here { @@ -930,9 +624,14 @@ class ComputerAidedDesign final : public nbl::examples::SimpleWindowedApplicatio return {}; } + double dt = 0; + double m_timeElapsed = 0.0; + std::chrono::steady_clock::time_point lastTime; + uint32_t m_hatchDebugStep = 0u; + inline bool onAppInitialized(smart_refctd_ptr&& system) override { - m_inputSystem = make_smart_refctd_ptr(logger_opt_smart_ptr(smart_refctd_ptr(m_logger))); + m_inputSystem = make_smart_refctd_ptr(logger_opt_smart_ptr(smart_refctd_ptr(m_logger))); // Remember to call the base class initialization! if (!device_base_t::onAppInitialized(smart_refctd_ptr(system))) @@ -942,6 +641,21 @@ class ComputerAidedDesign final : public nbl::examples::SimpleWindowedApplicatio fragmentShaderInterlockEnabled = m_device->getEnabledFeatures().fragmentShaderPixelInterlock; + // Create the Semaphores + m_renderSemaphore = m_device->createSemaphore(0ull); + m_renderSemaphore->setObjectDebugName("m_renderSemaphore"); + m_overflowSubmitScratchSemaphore = m_device->createSemaphore(0ull); + m_overflowSubmitScratchSemaphore->setObjectDebugName("m_overflowSubmitScratchSemaphore"); + if (!m_renderSemaphore || !m_overflowSubmitScratchSemaphore) + return logFail("Failed to Create Semaphores!"); + + // Set Queue and ScratchSemaInfo -> wait semaphores and command buffers will be modified by workLoop each frame + m_intendedNextSubmit.queue = getGraphicsQueue(); + m_intendedNextSubmit.scratchSemaphore = { + .semaphore = m_overflowSubmitScratchSemaphore.get(), + .value = 0ull, + }; + // Let's just use the same queue since there's no need for async present if (!m_surface) return logFail("Could not create Window & Surface!"); @@ -958,7 +672,9 @@ class ComputerAidedDesign final : public nbl::examples::SimpleWindowedApplicatio if (!m_surface->init(getGraphicsQueue(),std::move(scResources),{})) return logFail("Could not initialize the Surface!"); - allocateResources(); + m_framesInFlight = min(m_surface->getMaxFramesInFlight(), MaxFramesInFlight); + + allocateResources(256u); const bitflag bindlessTextureFlags = IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_UPDATE_AFTER_BIND_BIT | @@ -966,7 +682,6 @@ class ComputerAidedDesign final : public nbl::examples::SimpleWindowedApplicatio IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_PARTIALLY_BOUND_BIT; // Create DescriptorSetLayout, PipelineLayout and update DescriptorSets - const uint32_t imagesBinding = 3u; { video::IGPUDescriptorSetLayout::SBinding bindingsSet0[] = { { @@ -978,24 +693,45 @@ class ComputerAidedDesign final : public nbl::examples::SimpleWindowedApplicatio }, { .binding = 1u, + .type = asset::IDescriptor::E_TYPE::ET_STORAGE_BUFFER, + .createFlags = IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, + .stageFlags = asset::IShader::E_SHADER_STAGE::ESS_VERTEX | asset::IShader::E_SHADER_STAGE::ESS_FRAGMENT, + .count = 1u, + }, + { + .binding = 2u, + .type = asset::IDescriptor::E_TYPE::ET_STORAGE_BUFFER, + .createFlags = IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, + .stageFlags = asset::IShader::E_SHADER_STAGE::ESS_VERTEX | asset::IShader::E_SHADER_STAGE::ESS_FRAGMENT, + .count = 1u, + }, + { + .binding = 3u, + .type = asset::IDescriptor::E_TYPE::ET_STORAGE_BUFFER, + .createFlags = IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, + .stageFlags = asset::IShader::E_SHADER_STAGE::ESS_VERTEX | asset::IShader::E_SHADER_STAGE::ESS_FRAGMENT, + .count = 1u, + }, + { + .binding = 4u, .type = asset::IDescriptor::E_TYPE::ET_COMBINED_IMAGE_SAMPLER, .createFlags = IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, .stageFlags = asset::IShader::E_SHADER_STAGE::ESS_FRAGMENT, .count = 1u, }, { - .binding = 2u, + .binding = 5u, .type = asset::IDescriptor::E_TYPE::ET_SAMPLER, .createFlags = IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, .stageFlags = asset::IShader::E_SHADER_STAGE::ESS_FRAGMENT, .count = 1u, }, { - .binding = imagesBinding, + .binding = 6u, .type = asset::IDescriptor::E_TYPE::ET_SAMPLED_IMAGE, .createFlags = bindlessTextureFlags, .stageFlags = asset::IShader::E_SHADER_STAGE::ESS_FRAGMENT, - .count = ImagesBindingArraySize, + .count = 128u, }, }; descriptorSetLayout0 = m_device->createDescriptorSetLayout(bindingsSet0); @@ -1035,23 +771,35 @@ class ComputerAidedDesign final : public nbl::examples::SimpleWindowedApplicatio { descriptorSet0 = descriptorPool->createDescriptorSet(smart_refctd_ptr(descriptorSetLayout0)); descriptorSet1 = descriptorPool->createDescriptorSet(smart_refctd_ptr(descriptorSetLayout1)); - constexpr uint32_t DescriptorCountSet0 = 3u; + constexpr uint32_t DescriptorCountSet0 = 6u; video::IGPUDescriptorSet::SDescriptorInfo descriptorInfosSet0[DescriptorCountSet0] = {}; // Descriptors For Set 0: descriptorInfosSet0[0u].info.buffer.offset = 0u; - descriptorInfosSet0[0u].info.buffer.size = m_globalsBuffer->getCreationParams().size; - descriptorInfosSet0[0u].desc = m_globalsBuffer; + descriptorInfosSet0[0u].info.buffer.size = globalsBuffer->getCreationParams().size; + descriptorInfosSet0[0u].desc = globalsBuffer; - descriptorInfosSet0[1u].info.combinedImageSampler.imageLayout = IImage::LAYOUT::READ_ONLY_OPTIMAL; - descriptorInfosSet0[1u].info.combinedImageSampler.sampler = msdfImageSampler; - descriptorInfosSet0[1u].desc = drawResourcesFiller.getMSDFsTextureArray(); + descriptorInfosSet0[1u].info.buffer.offset = 0u; + descriptorInfosSet0[1u].info.buffer.size = drawResourcesFiller.gpuDrawBuffers.drawObjectsBuffer->getCreationParams().size; + descriptorInfosSet0[1u].desc = drawResourcesFiller.gpuDrawBuffers.drawObjectsBuffer; - descriptorInfosSet0[2u].desc = staticImageSampler; // TODO[Erfan]: different sampler and make immutable? + descriptorInfosSet0[2u].info.buffer.offset = 0u; + descriptorInfosSet0[2u].info.buffer.size = drawResourcesFiller.gpuDrawBuffers.mainObjectsBuffer->getCreationParams().size; + descriptorInfosSet0[2u].desc = drawResourcesFiller.gpuDrawBuffers.mainObjectsBuffer; + + descriptorInfosSet0[3u].info.buffer.offset = 0u; + descriptorInfosSet0[3u].info.buffer.size = drawResourcesFiller.gpuDrawBuffers.lineStylesBuffer->getCreationParams().size; + descriptorInfosSet0[3u].desc = drawResourcesFiller.gpuDrawBuffers.lineStylesBuffer; + + descriptorInfosSet0[4u].info.combinedImageSampler.imageLayout = IImage::LAYOUT::READ_ONLY_OPTIMAL; + descriptorInfosSet0[4u].info.combinedImageSampler.sampler = msdfTextureSampler; + descriptorInfosSet0[4u].desc = drawResourcesFiller.getMSDFsTextureArray(); + + descriptorInfosSet0[5u].desc = msdfTextureSampler; // TODO[Erfan]: different sampler and make immutable? // This is bindless to we write to it later. - // descriptorInfosSet0[3u].info.image.imageLayout = IImage::LAYOUT::READ_ONLY_OPTIMAL; - // descriptorInfosSet0[3u].desc = drawResourcesFiller.getMSDFsTextureArray(); + // descriptorInfosSet0[6u].info.image.imageLayout = IImage::LAYOUT::READ_ONLY_OPTIMAL; + // descriptorInfosSet0[6u].desc = drawResourcesFiller.getMSDFsTextureArray(); // Descriptors For Set 1: constexpr uint32_t DescriptorCountSet1 = 2u; @@ -1068,209 +816,281 @@ class ComputerAidedDesign final : public nbl::examples::SimpleWindowedApplicatio video::IGPUDescriptorSet::SWriteDescriptorSet descriptorUpdates[DescriptorUpdatesCount] = {}; // Set 0 Updates: - // globals descriptorUpdates[0u].dstSet = descriptorSet0.get(); descriptorUpdates[0u].binding = 0u; descriptorUpdates[0u].arrayElement = 0u; descriptorUpdates[0u].count = 1u; descriptorUpdates[0u].info = &descriptorInfosSet0[0u]; - // mdfs textures descriptorUpdates[1u].dstSet = descriptorSet0.get(); descriptorUpdates[1u].binding = 1u; descriptorUpdates[1u].arrayElement = 0u; descriptorUpdates[1u].count = 1u; descriptorUpdates[1u].info = &descriptorInfosSet0[1u]; - - // general texture sampler + descriptorUpdates[2u].dstSet = descriptorSet0.get(); descriptorUpdates[2u].binding = 2u; descriptorUpdates[2u].arrayElement = 0u; descriptorUpdates[2u].count = 1u; descriptorUpdates[2u].info = &descriptorInfosSet0[2u]; - // Set 1 Updates: - descriptorUpdates[3u].dstSet = descriptorSet1.get(); - descriptorUpdates[3u].binding = 0u; + descriptorUpdates[3u].dstSet = descriptorSet0.get(); + descriptorUpdates[3u].binding = 3u; descriptorUpdates[3u].arrayElement = 0u; descriptorUpdates[3u].count = 1u; - descriptorUpdates[3u].info = &descriptorInfosSet1[0u]; - - descriptorUpdates[4u].dstSet = descriptorSet1.get(); - descriptorUpdates[4u].binding = 1u; + descriptorUpdates[3u].info = &descriptorInfosSet0[3u]; + + descriptorUpdates[4u].dstSet = descriptorSet0.get(); + descriptorUpdates[4u].binding = 4u; descriptorUpdates[4u].arrayElement = 0u; descriptorUpdates[4u].count = 1u; - descriptorUpdates[4u].info = &descriptorInfosSet1[1u]; + descriptorUpdates[4u].info = &descriptorInfosSet0[4u]; + + descriptorUpdates[5u].dstSet = descriptorSet0.get(); + descriptorUpdates[5u].binding = 5u; + descriptorUpdates[5u].arrayElement = 0u; + descriptorUpdates[5u].count = 1u; + descriptorUpdates[5u].info = &descriptorInfosSet0[5u]; - m_device->updateDescriptorSets(DescriptorUpdatesCount, descriptorUpdates, 0u, nullptr); - } + // Set 1 Updates: + descriptorUpdates[6u].dstSet = descriptorSet1.get(); + descriptorUpdates[6u].binding = 0u; + descriptorUpdates[6u].arrayElement = 0u; + descriptorUpdates[6u].count = 1u; + descriptorUpdates[6u].info = &descriptorInfosSet1[0u]; - const asset::SPushConstantRange range = { - .stageFlags = IShader::E_SHADER_STAGE::ESS_VERTEX | IShader::E_SHADER_STAGE::ESS_FRAGMENT, - .offset = 0, - .size = sizeof(PushConstants) - }; + descriptorUpdates[7u].dstSet = descriptorSet1.get(); + descriptorUpdates[7u].binding = 1u; + descriptorUpdates[7u].arrayElement = 0u; + descriptorUpdates[7u].count = 1u; + descriptorUpdates[7u].info = &descriptorInfosSet1[1u]; - m_pipelineLayout = m_device->createPipelineLayout({ &range,1 }, core::smart_refctd_ptr(descriptorSetLayout0), core::smart_refctd_ptr(descriptorSetLayout1), nullptr, nullptr); - } - drawResourcesFiller.setTexturesDescriptorSetAndBinding(core::smart_refctd_ptr(descriptorSet0), imagesBinding); + m_device->updateDescriptorSets(DescriptorUpdatesCount, descriptorUpdates, 0u, nullptr); + } - smart_refctd_ptr mainPipelineFragmentShaders = {}; - smart_refctd_ptr mainPipelineVertexShader = {}; - std::array, 2u> geoTexturePipelineShaders = {}; + pipelineLayout = m_device->createPipelineLayout({}, core::smart_refctd_ptr(descriptorSetLayout0), core::smart_refctd_ptr(descriptorSetLayout1), nullptr, nullptr); + } + + // Shaders + std::array, 4u> shaders = {}; { - // Load Custom Shader - auto loadPrecompiledShader = [&]() -> smart_refctd_ptr - { - IAssetLoader::SAssetLoadParams lp = {}; - lp.logger = m_logger.get(); - lp.workingDirectory = "app_resources"; + constexpr auto vertexShaderPath = "../vertex_shader.hlsl"; + constexpr auto fragmentShaderPath = "../fragment_shader.hlsl"; + constexpr auto debugfragmentShaderPath = "../fragment_shader_debug.hlsl"; + constexpr auto resolveAlphasShaderPath = "../resolve_alphas.hlsl"; +#if defined(SHADER_CACHE_TEST_COMPILATION_CACHE_STORE) + auto cache = core::make_smart_refctd_ptr(); - auto key = nbl::this_example::builtin::build::get_spirv_key(m_device.get()); - auto assetBundle = m_assetMgr->getAsset(key.data(), lp); - const auto assets = assetBundle.getContents(); - if (assets.empty()) - { - m_logger->log("Failed to load a precompiled ahsder.", ILogger::ELL_ERROR); - return nullptr; - } - + // Load Custom Shader + auto loadCompileAndCreateShader = [&](const std::string& relPath, IShader::E_SHADER_STAGE stage) -> smart_refctd_ptr + { + IAssetLoader::SAssetLoadParams lp = {}; + lp.logger = m_logger.get(); + lp.workingDirectory = ""; // virtual root + auto assetBundle = m_assetMgr->getAsset(relPath, lp); + const auto assets = assetBundle.getContents(); + if (assets.empty()) + return nullptr; + + // lets go straight from ICPUSpecializedShader to IGPUSpecializedShader + auto cpuShader = IAsset::castDown(assets[0]); + cpuShader->setShaderStage(stage); + if (!cpuShader) + return nullptr; + + return m_device->createShader({ cpuShader.get(), nullptr, nullptr, cache.get() }); + }; + shaders[0] = loadCompileAndCreateShader(vertexShaderPath, IShader::E_SHADER_STAGE::ESS_VERTEX); + shaders[1] = loadCompileAndCreateShader(fragmentShaderPath, IShader::E_SHADER_STAGE::ESS_FRAGMENT); + shaders[2] = loadCompileAndCreateShader(debugfragmentShaderPath, IShader::E_SHADER_STAGE::ESS_FRAGMENT); + shaders[3] = loadCompileAndCreateShader(resolveAlphasShaderPath, IShader::E_SHADER_STAGE::ESS_FRAGMENT); + + auto serializedCache = cache->serialize(); + auto savePath = localOutputCWD / "cache.bin"; + core::smart_refctd_ptr f; + { + system::ISystem::future_t> future; + m_system->createFile(future, savePath.c_str(), system::IFile::ECF_WRITE); + if (!future.wait()) + return {}; + future.acquire().move_into(f); + } + if (!f) + return {}; + system::IFile::success_t succ; + f->write(succ, serializedCache->getPointer(), 0, serializedCache->getSize()); + const bool success = bool(succ); + assert(success); +#elif defined(SHADER_CACHE_TEST_CACHE_RETRIEVE) + auto savePath = localOutputCWD / "cache.bin"; + + core::smart_refctd_ptr f; + { + system::ISystem::future_t> future; + m_system->createFile(future, savePath.c_str(), system::IFile::ECF_READ); + if (!future.wait()) + return {}; + future.acquire().move_into(f); + } + if (!f) + return {}; + const size_t size = f->getSize(); + + std::vector contents(size); + system::IFile::success_t succ; + f->read(succ, contents.data(), 0, size); + const bool success = bool(succ); + assert(success); + + auto cache = IShaderCompiler::CCache::deserialize(contents); - auto shader = IAsset::castDown(assets[0]); - return shader; - }; + // Load Custom Shader + auto loadCompileAndCreateShader = [&](const std::string& relPath, IShader::E_SHADER_STAGE stage) -> smart_refctd_ptr + { + IAssetLoader::SAssetLoadParams lp = {}; + lp.logger = m_logger.get(); + lp.workingDirectory = ""; // virtual root + auto assetBundle = m_assetMgr->getAsset(relPath, lp); + const auto assets = assetBundle.getContents(); + if (assets.empty()) + return nullptr; + + // lets go straight from ICPUSpecializedShader to IGPUSpecializedShader + auto cpuShader = IAsset::castDown(assets[0]); + cpuShader->setShaderStage(stage); + if (!cpuShader) + return nullptr; + + return m_device->createShader({ cpuShader.get(), nullptr, cache.get(), nullptr }); + }; + shaders[0] = loadCompileAndCreateShader(vertexShaderPath, IShader::E_SHADER_STAGE::ESS_VERTEX); + shaders[1] = loadCompileAndCreateShader(fragmentShaderPath, IShader::E_SHADER_STAGE::ESS_FRAGMENT); + shaders[2] = loadCompileAndCreateShader(debugfragmentShaderPath, IShader::E_SHADER_STAGE::ESS_FRAGMENT); + shaders[3] = loadCompileAndCreateShader(resolveAlphasShaderPath, IShader::E_SHADER_STAGE::ESS_FRAGMENT); +#else - mainPipelineFragmentShaders = loadPrecompiledShader.operator()<"main_pipeline_fragment_shader">(); // "../shaders/main_pipeline/fragment.hlsl" - mainPipelineVertexShader = loadPrecompiledShader.operator() <"main_pipeline_vertex_shader">(); // "../shaders/main_pipeline/vertex_shader.hlsl" + // Load Custom Shader + auto loadCompileAndCreateShader = [&](const std::string& relPath, IShader::E_SHADER_STAGE stage) -> smart_refctd_ptr + { + IAssetLoader::SAssetLoadParams lp = {}; + lp.logger = m_logger.get(); + lp.workingDirectory = ""; // virtual root + auto assetBundle = m_assetMgr->getAsset(relPath, lp); + const auto assets = assetBundle.getContents(); + if (assets.empty()) + return nullptr; + + // lets go straight from ICPUSpecializedShader to IGPUSpecializedShader + auto cpuShader = IAsset::castDown(assets[0]); + cpuShader->setShaderStage(stage); + if (!cpuShader) + return nullptr; + + return m_device->createShader(cpuShader.get()); + }; + shaders[0] = loadCompileAndCreateShader(vertexShaderPath, IShader::E_SHADER_STAGE::ESS_VERTEX); + shaders[1] = loadCompileAndCreateShader(fragmentShaderPath, IShader::E_SHADER_STAGE::ESS_FRAGMENT); + shaders[2] = loadCompileAndCreateShader(debugfragmentShaderPath, IShader::E_SHADER_STAGE::ESS_FRAGMENT); + shaders[3] = loadCompileAndCreateShader(resolveAlphasShaderPath, IShader::E_SHADER_STAGE::ESS_FRAGMENT); +#endif } - // Shared Blend Params between pipelines - // Premultiplied over-blend (back-to-front) - SBlendParams premultipliedOverBlendParams = {}; - premultipliedOverBlendParams.blendParams[0u].srcColorFactor = asset::EBF_ONE; - premultipliedOverBlendParams.blendParams[0u].dstColorFactor = asset::EBF_ONE_MINUS_SRC_ALPHA; - premultipliedOverBlendParams.blendParams[0u].colorBlendOp = asset::EBO_ADD; - premultipliedOverBlendParams.blendParams[0u].srcAlphaFactor = asset::EBF_ONE; - premultipliedOverBlendParams.blendParams[0u].dstAlphaFactor = asset::EBF_ONE_MINUS_SRC_ALPHA; - premultipliedOverBlendParams.blendParams[0u].alphaBlendOp = asset::EBO_ADD; - premultipliedOverBlendParams.blendParams[0u].colorWriteMask = (1u << 4u) - 1u; - // Premultiplied UNDER-blend (front-to-back) - SBlendParams premultipliedUnderBlendParams = {}; - premultipliedUnderBlendParams.blendParams[0u].srcColorFactor = asset::EBF_ONE_MINUS_DST_ALPHA; - premultipliedUnderBlendParams.blendParams[0u].dstColorFactor = asset::EBF_ONE; - premultipliedUnderBlendParams.blendParams[0u].colorBlendOp = asset::EBO_ADD; - premultipliedUnderBlendParams.blendParams[0u].srcAlphaFactor = asset::EBF_ONE; - premultipliedUnderBlendParams.blendParams[0u].dstAlphaFactor = asset::EBF_ONE_MINUS_SRC_ALPHA; - premultipliedUnderBlendParams.blendParams[0u].alphaBlendOp = asset::EBO_ADD; - premultipliedUnderBlendParams.blendParams[0u].colorWriteMask = (1u << 4u) - 1u; - - IGPUGraphicsPipeline::SCreationParams mainGraphicsPipelineParams = {}; - mainGraphicsPipelineParams.layout = m_pipelineLayout.get(); - mainGraphicsPipelineParams.cached = { - .vertexInput = {}, - .primitiveAssembly = { - .primitiveType = E_PRIMITIVE_TOPOLOGY::EPT_TRIANGLE_LIST, - }, - .rasterization = { - .polygonMode = EPM_FILL, - .faceCullingMode = EFCM_NONE, - .depthWriteEnable = false, - }, - .blend = premultipliedOverBlendParams, - }; - mainGraphicsPipelineParams.renderpass = compatibleRenderPass.get(); - - // Create Main Graphics Pipelines + SBlendParams blendParams = {}; + blendParams.blendParams[0u].srcColorFactor = asset::EBF_SRC_ALPHA; + blendParams.blendParams[0u].dstColorFactor = asset::EBF_ONE_MINUS_SRC_ALPHA; + blendParams.blendParams[0u].colorBlendOp = asset::EBO_ADD; + blendParams.blendParams[0u].srcAlphaFactor = asset::EBF_ONE; + blendParams.blendParams[0u].dstAlphaFactor = asset::EBF_ZERO; + blendParams.blendParams[0u].alphaBlendOp = asset::EBO_ADD; + blendParams.blendParams[0u].colorWriteMask = (1u << 4u) - 1u; + + // Create Alpha Resovle Pipeline { - video::IGPUPipelineBase::SShaderSpecInfo specInfo[2] = { - { .shader = mainPipelineVertexShader.get(), .entryPoint = "vtxMain" }, - { .shader = mainPipelineFragmentShaders.get(), .entryPoint = "fragMain" }, - }; + // Load FSTri Shader + ext::FullScreenTriangle::ProtoPipeline fsTriangleProtoPipe(m_assetMgr.get(),m_device.get(),m_logger.get()); - IGPUGraphicsPipeline::SCreationParams params[1] = { mainGraphicsPipelineParams }; - params[0].vertexShader = specInfo[0]; - params[0].fragmentShader = specInfo[1]; + const IGPUShader::SSpecInfo fragSpec = { + .entryPoint = "main", + .shader = shaders[3u].get() + }; - if (!m_device->createGraphicsPipelines(nullptr,params,&m_graphicsPipeline)) + resolveAlphaGraphicsPipeline = fsTriangleProtoPipe.createPipeline(fragSpec, pipelineLayout.get(), compatibleRenderPass.get(), 0u, blendParams); + if (!resolveAlphaGraphicsPipeline) return logFail("Graphics Pipeline Creation Failed."); + } - // Debug Pipeline - if constexpr (DebugModeWireframe) + // Create Main Graphics Pipelines { - // Create Main Graphics Pipelines - video::IGPUPipelineBase::SShaderSpecInfo specInfo[2] = { - { .shader=mainPipelineVertexShader.get(), .entryPoint = "vtxMain" }, - { .shader=mainPipelineFragmentShaders.get(), .entryPoint = "fragDebugMain" }, + + IGPUShader::SSpecInfo specInfo[2] = { + {.shader=shaders[0u].get() }, + {.shader=shaders[1u].get() }, }; - IGPUGraphicsPipeline::SCreationParams debugGraphicsPipelineParams[1] = { mainGraphicsPipelineParams }; - debugGraphicsPipelineParams[0].cached.rasterization.polygonMode = asset::EPM_LINE; - debugGraphicsPipelineParams[0].vertexShader = specInfo[0]; - debugGraphicsPipelineParams[0].fragmentShader = specInfo[1]; - - if (!m_device->createGraphicsPipelines(nullptr,debugGraphicsPipelineParams,&m_debugGraphicsPipeline)) - return logFail("Debug Graphics Pipeline Creation Failed."); - } - // StreamedImages Pipeline - { - video::IGPUPipelineBase::SShaderSpecInfo specInfo[2] = { - { .shader=mainPipelineVertexShader.get(), .entryPoint = "vtxMain" }, - { .shader=mainPipelineFragmentShaders.get(), .entryPoint = "fragGeoref" }, + IGPUGraphicsPipeline::SCreationParams params[1] = {}; + params[0].layout = pipelineLayout.get(); + params[0].shaders = specInfo; + params[0].cached = { + .vertexInput = {}, + .primitiveAssembly = { + .primitiveType = E_PRIMITIVE_TOPOLOGY::EPT_TRIANGLE_LIST, + }, + .rasterization = { + .polygonMode = EPM_FILL, + .faceCullingMode = EFCM_NONE, + .depthWriteEnable = false, + }, + .blend = blendParams, }; - - IGPUGraphicsPipeline::SCreationParams params[1] = { mainGraphicsPipelineParams }; - params[0].vertexShader = specInfo[0]; - params[0].fragmentShader = specInfo[1]; - params[0].cached.blend = premultipliedUnderBlendParams; + params[0].renderpass = compatibleRenderPass.get(); - if (!m_device->createGraphicsPipelines(nullptr, params, &m_streamedImagesGraphicsPipeline)) - return logFail("StreamedImages Graphics Pipeline Creation Failed."); - } - - // Create Alpha Resovle Pipeline - { - // Load FSTri Shader - ext::FullScreenTriangle::ProtoPipeline fsTriangleProtoPipe(m_assetMgr.get(),m_device.get(),m_logger.get()); - - const video::IGPUPipelineBase::SShaderSpecInfo fragSpec = { .shader = mainPipelineFragmentShaders.get(), .entryPoint = "fragShaderResolveAlphas" }; - - resolveAlphaGraphicsPipeline = fsTriangleProtoPipe.createPipeline(fragSpec, m_pipelineLayout.get(), compatibleRenderPass.get(), 0u, premultipliedOverBlendParams); - if (!resolveAlphaGraphicsPipeline) + if (!m_device->createGraphicsPipelines(nullptr,params,&graphicsPipeline)) return logFail("Graphics Pipeline Creation Failed."); + if constexpr (DebugModeWireframe) + { + specInfo[1u].shader = shaders[2u].get(); // change only fragment shader to fragment_shader_debug.hlsl + params[0].cached.rasterization.polygonMode = asset::EPM_LINE; + + if (!m_device->createGraphicsPipelines(nullptr,params,&debugGraphicsPipeline)) + return logFail("Debug Graphics Pipeline Creation Failed."); + } } - + // Create the commandbuffers and pools, this time properly 1 pool per FIF - m_graphicsCommandPool = m_device->createCommandPool(getGraphicsQueue()->getFamilyIndex(),IGPUCommandPool::CREATE_FLAGS::RESET_COMMAND_BUFFER_BIT); - if (!m_graphicsCommandPool) - return logFail("Couldn't create Command Pool!"); - if (!m_graphicsCommandPool->createCommandBuffers(IGPUCommandPool::BUFFER_LEVEL::PRIMARY,{m_commandBuffersInFlight.data(),MaxSubmitsInFlight})) - return logFail("Couldn't create Command Buffers!"); + for (auto i=0u; icreateCommandPool(getGraphicsQueue()->getFamilyIndex(),IGPUCommandPool::CREATE_FLAGS::RESET_COMMAND_BUFFER_BIT); + if (!m_graphicsCommandPools[i]) + return logFail("Couldn't create Command Pool!"); + if (!m_graphicsCommandPools[i]->createCommandBuffers(IGPUCommandPool::BUFFER_LEVEL::PRIMARY,{m_commandBuffers.data()+i,1})) + return logFail("Couldn't create Command Buffer!"); + } m_Camera.setOrigin({ 0.0, 0.0 }); m_Camera.setAspectRatio((double)m_window->getWidth() / m_window->getHeight()); m_Camera.setSize(cameraExtents[uint32_t(mode)]); + m_timeElapsed = 0.0; + // Loading font stuff m_textRenderer = nbl::core::make_smart_refctd_ptr(); - m_font = FontFace::create(core::smart_refctd_ptr(m_textRenderer), std::string("C:\\Windows\\Fonts\\arial.ttf")); - - if (m_font->getFreetypeFace()->num_charmaps > 0) - FT_Set_Charmap(m_font->getFreetypeFace(), m_font->getFreetypeFace()->charmaps[0]); - - const std::wstring str = L"MSDF: ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnoprstuvwxyz '1234567890-=\"!@#$%&*()_+"; + m_arialFont = nbl::core::make_smart_refctd_ptr(core::smart_refctd_ptr(m_textRenderer), std::string("C:\\Windows\\Fonts\\arial.ttf")); + const auto str = "MSDF: ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnoprstuvwxyz '1234567890-=\"!@#$%&*()_+"; singleLineText = std::unique_ptr(new SingleLineText( - m_font.get(), - str)); + core::smart_refctd_ptr(m_arialFont), + std::string(str))); drawResourcesFiller.setGlyphMSDFTextureFunction( [&](nbl::ext::TextRendering::FontFace* face, uint32_t glyphIdx) -> core::smart_refctd_ptr { - return face->generateGlyphMSDF(MSDFPixelRange, glyphIdx, drawResourcesFiller.getMSDFResolution(), MSDFMips); + return std::move(face->generateGlyphMSDF(MSDFPixelRange, glyphIdx, drawResourcesFiller.getMSDFResolution(), MSDFMips)); } ); @@ -1280,197 +1100,16 @@ class ComputerAidedDesign final : public nbl::examples::SimpleWindowedApplicatio return Hatch::generateHatchFillPatternMSDF(m_textRenderer.get(), pattern, drawResourcesFiller.getMSDFResolution()); } ); - - // Create the Semaphores - m_renderSemaphore = m_device->createSemaphore(0ull); - m_renderSemaphore->setObjectDebugName("m_renderSemaphore"); - m_overflowSubmitScratchSemaphore = m_device->createSemaphore(0ull); - m_overflowSubmitScratchSemaphore->setObjectDebugName("m_overflowSubmitScratchSemaphore"); - if (!m_renderSemaphore || !m_overflowSubmitScratchSemaphore) - return logFail("Failed to Create Semaphores!"); - - // Set Queue and ScratchSemaInfo -> wait semaphores and command buffers will be modified by workLoop each frame - m_intendedNextSubmit.queue = getGraphicsQueue(); - m_intendedNextSubmit.scratchSemaphore = { - .semaphore = m_overflowSubmitScratchSemaphore.get(), - .value = 0ull, - }; - for (uint32_t i = 0; i < MaxSubmitsInFlight; ++i) - m_commandBufferInfos[i] = { .cmdbuf = m_commandBuffersInFlight[i].get() }; - m_intendedNextSubmit.scratchCommandBuffers = m_commandBufferInfos; - m_currentRecordingCommandBufferInfo = &m_commandBufferInfos[0]; - - // Load image - system::path m_loadCWD = ".."; - - /** - * @param formatOverride override format of an image view, use special argument asset::E_FORMAT::EF_COUNT to don't override image view format and use one retrieved from the loaded image - */ - auto loadImage = [&](const std::string& imagePath) -> smart_refctd_ptr - { - constexpr auto cachingFlags = static_cast(IAssetLoader::ECF_DONT_CACHE_REFERENCES & IAssetLoader::ECF_DONT_CACHE_TOP_LEVEL); - const IAssetLoader::SAssetLoadParams loadParams(0ull, nullptr, cachingFlags, IAssetLoader::ELPF_NONE, m_logger.get(), m_loadCWD); - auto bundle = m_assetMgr->getAsset(imagePath, loadParams); - auto contents = bundle.getContents(); - if (contents.empty()) - { - m_logger->log("Failed to load image with path %s, skipping!", ILogger::ELL_ERROR, (m_loadCWD / imagePath).c_str()); - return nullptr; - } - - smart_refctd_ptr cpuImgView; - const auto& asset = contents[0]; - switch (asset->getAssetType()) - { - case IAsset::ET_IMAGE: - { - auto image = smart_refctd_ptr_static_cast(asset); - auto& flags = image->getCreationParameters().flags; - // assert if asset is mutable - const_cast&>(flags) |= asset::IImage::E_CREATE_FLAGS::ECF_MUTABLE_FORMAT_BIT; - const auto format = image->getCreationParameters().format; - - ICPUImageView::SCreationParams viewParams = { - .flags = ICPUImageView::E_CREATE_FLAGS::ECF_NONE, - .image = std::move(image), - .viewType = IImageView::E_TYPE::ET_2D, - .format = format, - .subresourceRange = { - .aspectMask = IImage::E_ASPECT_FLAGS::EAF_COLOR_BIT, - .baseMipLevel = 0u, - .levelCount = ICPUImageView::remaining_mip_levels, - .baseArrayLayer = 0u, - .layerCount = ICPUImageView::remaining_array_layers - } - }; - - cpuImgView = ICPUImageView::create(std::move(viewParams)); - } break; - - case IAsset::ET_IMAGE_VIEW: - cpuImgView = smart_refctd_ptr_static_cast(asset); - break; - default: - m_logger->log("Failed to load ICPUImage or ICPUImageView got some other Asset Type, skipping!", ILogger::ELL_ERROR); - return nullptr; - } - - const auto loadedCPUImage = cpuImgView->getCreationParameters().image; - const auto loadedCPUImageCreationParams = loadedCPUImage->getCreationParameters(); - - // Promoting the image to a format GPU supports. (so that updateImageViaStagingBuffer doesn't have to handle that each frame if overflow-submit needs to happen) - auto promotedCPUImageCreationParams = loadedCPUImage->getCreationParameters(); - - promotedCPUImageCreationParams.usage |= IGPUImage::EUF_TRANSFER_DST_BIT|IGPUImage::EUF_SAMPLED_BIT; - // promote format because RGB8 and friends don't actually exist in HW - { - const IPhysicalDevice::SImageFormatPromotionRequest request = { - .originalFormat = promotedCPUImageCreationParams.format, - .usages = IPhysicalDevice::SFormatImageUsages::SUsage(promotedCPUImageCreationParams.usage) - }; - promotedCPUImageCreationParams.format = m_physicalDevice->promoteImageFormat(request,video::IGPUImage::TILING::OPTIMAL); - } - - if (loadedCPUImageCreationParams.format != promotedCPUImageCreationParams.format) - { - smart_refctd_ptr promotedCPUImage = ICPUImage::create(promotedCPUImageCreationParams); - core::rational bytesPerPixel = asset::getBytesPerPixel(promotedCPUImageCreationParams.format); - - const auto extent = loadedCPUImageCreationParams.extent; - const uint32_t mipLevels = loadedCPUImageCreationParams.mipLevels; - const uint32_t arrayLayers = loadedCPUImageCreationParams.arrayLayers; - - // Only supporting 1 mip, it's just for test.. - const size_t byteSize = (bytesPerPixel * extent.width * extent.height * extent.depth * arrayLayers).getIntegerApprox(); // TODO: consider mips - ICPUBuffer::SCreationParams bufferCreationParams = {}; - bufferCreationParams.size = byteSize; - smart_refctd_ptr promotedCPUImageBuffer = ICPUBuffer::create(std::move(bufferCreationParams)); - - auto newRegions = core::make_refctd_dynamic_array>(1u); - ICPUImage::SBufferCopy& region = newRegions->front(); - region.imageSubresource.aspectMask = IImage::E_ASPECT_FLAGS::EAF_COLOR_BIT; - region.imageSubresource.mipLevel = 0u; // TODO - region.imageSubresource.baseArrayLayer = 0u; - region.imageSubresource.layerCount = arrayLayers; - region.bufferOffset = 0u; - region.bufferRowLength = 0u; - region.bufferImageHeight = 0u; - region.imageOffset = { 0u, 0u, 0u }; - region.imageExtent = extent; - promotedCPUImage->setBufferAndRegions(std::move(promotedCPUImageBuffer), newRegions); - - performImageFormatPromotionCopy(loadedCPUImage, promotedCPUImage); - return promotedCPUImage; - } - else - { - return loadedCPUImage; - } - }; - - if constexpr (mode == ExampleMode::CASE_7) - { - std::string imagePaths[] = - { - "../../media/color_space_test/R8G8B8_1.jpg", - "../../media/color_space_test/R8G8B8_1.png", - "../../media/color_space_test/R8G8B8A8_2.png", - "../../media/color_space_test/R8G8B8A8_1.png", - }; - for (const auto& imagePath : imagePaths) - { - auto image = loadImage(imagePath); - if (image) - sampleImages.push_back(image); - } - assert(gridDTMHeightMap); - } - - if constexpr (mode == ExampleMode::CASE_11) - { - gridDTMHeightMap = loadImage("../../media/gridDTMHeightMap.exr"); - // set diagonals of cells to TOP_LEFT_TO_BOTTOM_RIGHT or BOTTOM_LEFT_TO_TOP_RIGHT randomly - { - // assumption is that format of the grid DTM height map is *_SRGB, I don't think we need any code to ensure that - - auto* region = gridDTMHeightMap->getRegion(0, core::vectorSIMDu32(0.0f)); - auto imageExtent = region->getExtent(); - auto imagePixelSize = asset::getBytesPerPixel(gridDTMHeightMap->getCreationParameters().format).getIntegerApprox(); - float* imageData = static_cast(gridDTMHeightMap->getBuffer()->getPointer()) + region->bufferOffset; - const size_t imageByteSize = gridDTMHeightMap->getImageDataSizeInBytes(); - assert(imageByteSize % sizeof(float) == 0); - - std::random_device rd; - std::mt19937 mt(rd()); - std::uniform_int_distribution dist(0, 1); - - for (int i = 0; i < imageByteSize; i += sizeof(float)) - { - const bool isTexelEven = static_cast(dist(mt)); - E_CELL_DIAGONAL diagonal = isTexelEven ? TOP_LEFT_TO_BOTTOM_RIGHT : BOTTOM_LEFT_TO_TOP_RIGHT; - - setDiagonalModeBit(imageData, diagonal); - imageData++; - } - - } - } - - // Create case 12 image loader - if constexpr (mode == ExampleMode::CASE_12) - drawResourcesFiller.setGeoreferencedImageLoader(make_smart_refctd_ptr(m_assetMgr.get(), m_logger.get(), m_physicalDevice)); - - - m_timeElapsed = 0.0; - return true; } // We do a very simple thing, display an image and wait `DisplayImageMs` to show it inline void workLoopBody() override { + const auto resourceIx = m_realFrameIx%m_framesInFlight; + auto now = std::chrono::high_resolution_clock::now(); - double dt = std::chrono::duration_cast(now - lastTime).count(); + dt = std::chrono::duration_cast(now - lastTime).count(); lastTime = now; m_timeElapsed += dt; if constexpr (mode == ExampleMode::CASE_0) @@ -1483,8 +1122,7 @@ class ComputerAidedDesign final : public nbl::examples::SimpleWindowedApplicatio mouse.consumeEvents([&](const IMouseEventChannel::range_t& events) -> void { - if (m_window->hasMouseFocus()) - m_Camera.mouseProcess(events); + m_Camera.mouseProcess(events); } , m_logger.get()); keyboard.consumeEvents([&](const IKeyboardEventChannel::range_t& events) -> void @@ -1503,30 +1141,10 @@ class ComputerAidedDesign final : public nbl::examples::SimpleWindowedApplicatio { m_hatchDebugStep--; } - if (ev.action == nbl::ui::SKeyboardEvent::E_KEY_ACTION::ECA_PRESSED && ev.keyCode == nbl::ui::E_KEY_CODE::EKC_1) - { - m_shadingModeExample = E_HEIGHT_SHADING_MODE::DISCRETE_VARIABLE_LENGTH_INTERVALS; - } - if (ev.action == nbl::ui::SKeyboardEvent::E_KEY_ACTION::ECA_PRESSED && ev.keyCode == nbl::ui::E_KEY_CODE::EKC_2) - { - m_shadingModeExample = E_HEIGHT_SHADING_MODE::DISCRETE_FIXED_LENGTH_INTERVALS; - } - if (ev.action == nbl::ui::SKeyboardEvent::E_KEY_ACTION::ECA_PRESSED && ev.keyCode == nbl::ui::E_KEY_CODE::EKC_3) - { - m_shadingModeExample = E_HEIGHT_SHADING_MODE::CONTINOUS_INTERVALS; - } } } , m_logger.get()); - const bool isCachingDraw = CacheAndReplay && m_realFrameIx == 0u; - if (isCachingDraw) - { - SIntendedSubmitInfo invalidSubmit = {}; - addObjects(invalidSubmit); // if any overflows happen here, it will add to our replay cache and not submit anything - replayCaches.push_back(drawResourcesFiller.createReplayCache()); - finishedCachingDraw = true; - } if (!beginFrameRender()) return; @@ -1544,31 +1162,16 @@ class ComputerAidedDesign final : public nbl::examples::SimpleWindowedApplicatio .stageMask = asset::PIPELINE_STAGE_FLAGS::ALL_COMMANDS_BITS }; + IQueue::SSubmitInfo::SCommandBufferInfo cmdbufs[1u] = { {.cmdbuf = m_commandBuffers[resourceIx].get() } }; IQueue::SSubmitInfo::SSemaphoreInfo waitSems[2u] = { acquired, prevFrameRendered }; + m_intendedNextSubmit.waitSemaphores = waitSems; + m_intendedNextSubmit.commandBuffers = cmdbufs; + + addObjects(m_intendedNextSubmit); - if (CacheAndReplay) - { - // to size-1u because we only want to submit overflows here. - for (uint32_t i = 0u; i < replayCaches.size() - 1u; ++i) - { - drawResourcesFiller.setReplayCache(replayCaches[i].get()); - submitDraws(m_intendedNextSubmit, true); - drawResourcesFiller.unsetReplayCache(); - } - if (!replayCaches.empty()) - drawResourcesFiller.setReplayCache(replayCaches.back().get()); - } - else - { - addObjects(m_intendedNextSubmit); - } - endFrameRender(m_intendedNextSubmit); - if (CacheAndReplay) - drawResourcesFiller.unsetReplayCache(); - #ifdef BENCHMARK_TILL_FIRST_FRAME if (!stopBenchamrkFlag) { @@ -1582,37 +1185,49 @@ class ComputerAidedDesign final : public nbl::examples::SimpleWindowedApplicatio bool beginFrameRender() { - // framesInFlight: ensuring safe execution of command buffers and acquires, `framesInFlight` only affect semaphore waits, don't use this to index your resources because it can change with swapchain recreation. - const uint32_t framesInFlight = core::min(MaxFramesInFlight, m_surface->getMaxAcquiresInFlight()); - // We block for semaphores for 2 reasons here: - // A) Resource: Can't use resource like a command buffer BEFORE previous use is finished! [MaxFramesInFlight] - // B) Acquire: Can't have more acquires in flight than a certain threshold returned by swapchain or your surface helper class. [MaxAcquiresInFlight] - if (m_realFrameIx>=framesInFlight) + // Can't reset a cmdbuffer before the previous use of commandbuffer is finished! + if (m_realFrameIx>=m_framesInFlight) { const ISemaphore::SWaitInfo cmdbufDonePending[] = { { .semaphore = m_renderSemaphore.get(), - .value = m_realFrameIx+1-framesInFlight + .value = m_realFrameIx+1-m_framesInFlight } }; if (m_device->blockForSemaphores(cmdbufDonePending)!=ISemaphore::WAIT_RESULT::SUCCESS) return false; } - + // Acquire m_currentImageAcquire = m_surface->acquireNextImage(); if (!m_currentImageAcquire) return false; - - const bool beganSuccess = m_intendedNextSubmit.beginNextCommandBuffer(m_currentRecordingCommandBufferInfo); - assert(beganSuccess); - auto* cb = m_currentRecordingCommandBufferInfo->cmdbuf; + + const auto resourceIx = m_realFrameIx%m_framesInFlight; + auto& cb = m_commandBuffers[resourceIx]; + auto& commandPool = m_graphicsCommandPools[resourceIx]; // safe to proceed - // no need to reset and begin new command buffers as SIntendedSubmitInfo already handled that. - // cb->reset(video::IGPUCommandBuffer::RESET_FLAGS::RELEASE_RESOURCES_BIT); - // cb->begin(video::IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT); + cb->reset(video::IGPUCommandBuffer::RESET_FLAGS::RELEASE_RESOURCES_BIT); + cb->begin(video::IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT); cb->beginDebugMarker("Frame"); + + float64_t3x3 projectionToNDC; + projectionToNDC = m_Camera.constructViewProjection(); + + Globals globalData = {}; + globalData.antiAliasingFactor = 1.0;// +abs(cos(m_timeElapsed * 0.0008)) * 20.0f; + globalData.resolution = uint32_t2{ m_window->getWidth(), m_window->getHeight() }; + globalData.defaultClipProjection.projectionToNDC = projectionToNDC; + globalData.defaultClipProjection.minClipNDC = float32_t2(-1.0, -1.0); + globalData.defaultClipProjection.maxClipNDC = float32_t2(+1.0, +1.0); + auto screenToWorld = getScreenToWorldRatio(globalData.defaultClipProjection.projectionToNDC, globalData.resolution); + globalData.screenToWorldRatio = screenToWorld; + globalData.worldToScreenRatio = (1.0/screenToWorld); + globalData.miterLimit = 10.0f; + SBufferRange globalBufferUpdateRange = { .offset = 0ull, .size = sizeof(Globals), .buffer = globalsBuffer.get() }; + bool updateSuccess = cb->updateBuffer(globalBufferUpdateRange, &globalData); + assert(updateSuccess); nbl::video::IGPUCommandBuffer::SRenderpassBeginInfo beginInfo; auto scRes = static_cast(m_surface->getSwapchainResources()); @@ -1643,57 +1258,10 @@ class ComputerAidedDesign final : public nbl::examples::SimpleWindowedApplicatio void submitDraws(SIntendedSubmitInfo& intendedSubmitInfo, bool inBetweenSubmit) { - const bool isCachingDraw = CacheAndReplay && m_realFrameIx == 0u && !finishedCachingDraw; - if (isCachingDraw) - { - drawResourcesFiller.markFrameUsageComplete(intendedSubmitInfo.getFutureScratchSemaphore().value); - replayCaches.push_back(drawResourcesFiller.createReplayCache()); - return; // we don't record, submit or do anything, just caching the draw resources - } - - drawResourcesFiller.pushAllUploads(intendedSubmitInfo); - - m_currentRecordingCommandBufferInfo = intendedSubmitInfo.getCommandBufferForRecording(); // drawResourcesFiller.pushAllUploads might've overflow submitted and changed the current recording command buffer - - // Use the current recording command buffer of the intendedSubmitInfos scratchCommandBuffers, it should be in recording state - auto* cb = m_currentRecordingCommandBufferInfo->cmdbuf; + const auto resourceIx = m_realFrameIx%m_framesInFlight; + auto* cb = intendedSubmitInfo.getScratchCommandBuffer(); + auto&r = drawResourcesFiller; - const auto& resourcesCollection = drawResourcesFiller.getResourcesCollection(); - const auto& resourcesGPUBuffer = drawResourcesFiller.getResourcesGPUBuffer(); - - float64_t3x3 projectionToNDC; - projectionToNDC = m_Camera.constructViewProjection(); - - // TEST CAMERA ROTATION - if constexpr (testCameraRotation) - projectionToNDC = rotateBasedOnTime(projectionToNDC); - - Globals globalData = {}; - uint64_t baseAddress = resourcesGPUBuffer->getDeviceAddress(); - globalData.pointers = { - .lineStyles = baseAddress + resourcesCollection.lineStyles.bufferOffset, - .dtmSettings = baseAddress + resourcesCollection.dtmSettings.bufferOffset, - .customProjections = baseAddress + resourcesCollection.customProjections.bufferOffset, - .customClipRects = baseAddress + resourcesCollection.customClipRects.bufferOffset, - .mainObjects = baseAddress + resourcesCollection.mainObjects.bufferOffset, - .drawObjects = baseAddress + resourcesCollection.drawObjects.bufferOffset, - .geometryBuffer = baseAddress + resourcesCollection.geometryInfo.bufferOffset, - }; - globalData.antiAliasingFactor = 1.0;// +abs(cos(m_timeElapsed * 0.0008)) * 20.0f; - globalData.minLineWidth = 0.0f; // minimum line width in screenspace pixels (will clamp if it's lower) - globalData.minLineThicknessToEnableAA = 0.0f; // lines/curves with screenspace (pixel) widths lower than this will skip AA - globalData.resolution = uint32_t2{ m_window->getWidth(), m_window->getHeight() }; - globalData.defaultProjectionToNDC = projectionToNDC; - float screenToWorld = getScreenToWorldRatio(globalData.defaultProjectionToNDC, globalData.resolution); - globalData.screenToWorldScaleTransform = float64_t3x3( 1.0f / screenToWorld, 0.0f, 0.0f, - 0.0f, 1.0f / screenToWorld, 0.0f, - 0.0f, 0.0f, 1.0f); - globalData.miterLimit = 10.0f; - globalData.currentlyActiveMainObjectIndex = drawResourcesFiller.getActiveMainObjectIndex(); - SBufferRange globalBufferUpdateRange = { .offset = 0ull, .size = sizeof(Globals), .buffer = m_globalsBuffer}; - bool updateSuccess = cb->updateBuffer(globalBufferUpdateRange, &globalData); - assert(updateSuccess); - asset::SViewport vp = { .x = 0u, @@ -1714,13 +1282,26 @@ class ComputerAidedDesign final : public nbl::examples::SimpleWindowedApplicatio // pipelineBarriersBeforeDraw { - constexpr uint32_t MaxBufferBarriersCount = 2u; + constexpr uint32_t MaxBufferBarriersCount = 6u; uint32_t bufferBarriersCount = 0u; IGPUCommandBuffer::SPipelineBarrierDependencyInfo::buffer_barrier_t bufferBarriers[MaxBufferBarriersCount]; - - const auto& resourcesCollection = drawResourcesFiller.getResourcesCollection(); - if (m_globalsBuffer->getSize() > 0u) + // Index Buffer Copy Barrier -> Only do once at the beginning of the frames + if (m_realFrameIx == 0u) + { + auto& bufferBarrier = bufferBarriers[bufferBarriersCount++]; + bufferBarrier.barrier.dep.srcStageMask = PIPELINE_STAGE_FLAGS::COPY_BIT; + bufferBarrier.barrier.dep.srcAccessMask = ACCESS_FLAGS::TRANSFER_WRITE_BIT; + bufferBarrier.barrier.dep.dstStageMask = PIPELINE_STAGE_FLAGS::VERTEX_INPUT_BITS; + bufferBarrier.barrier.dep.dstAccessMask = ACCESS_FLAGS::INDEX_READ_BIT; + bufferBarrier.range = + { + .offset = 0u, + .size = drawResourcesFiller.gpuDrawBuffers.indexBuffer->getSize(), + .buffer = drawResourcesFiller.gpuDrawBuffers.indexBuffer, + }; + } + if (globalsBuffer->getSize() > 0u) { auto& bufferBarrier = bufferBarriers[bufferBarriersCount++]; bufferBarrier.barrier.dep.srcStageMask = PIPELINE_STAGE_FLAGS::COPY_BIT; @@ -1730,22 +1311,64 @@ class ComputerAidedDesign final : public nbl::examples::SimpleWindowedApplicatio bufferBarrier.range = { .offset = 0u, - .size = m_globalsBuffer->getSize(), - .buffer = m_globalsBuffer, + .size = globalsBuffer->getSize(), + .buffer = globalsBuffer, }; } - if (drawResourcesFiller.getCopiedResourcesSize() > 0u) + if (drawResourcesFiller.getCurrentDrawObjectsBufferSize() > 0u) { auto& bufferBarrier = bufferBarriers[bufferBarriersCount++]; bufferBarrier.barrier.dep.srcStageMask = PIPELINE_STAGE_FLAGS::COPY_BIT; bufferBarrier.barrier.dep.srcAccessMask = ACCESS_FLAGS::TRANSFER_WRITE_BIT; - bufferBarrier.barrier.dep.dstStageMask = PIPELINE_STAGE_FLAGS::VERTEX_INPUT_BITS | PIPELINE_STAGE_FLAGS::VERTEX_SHADER_BIT | PIPELINE_STAGE_FLAGS::FRAGMENT_SHADER_BIT; - bufferBarrier.barrier.dep.dstAccessMask = ACCESS_FLAGS::MEMORY_READ_BITS | ACCESS_FLAGS::MEMORY_WRITE_BITS; + bufferBarrier.barrier.dep.dstStageMask = PIPELINE_STAGE_FLAGS::VERTEX_SHADER_BIT; + bufferBarrier.barrier.dep.dstAccessMask = ACCESS_FLAGS::SHADER_READ_BITS; bufferBarrier.range = { .offset = 0u, - .size = drawResourcesFiller.getCopiedResourcesSize(), - .buffer = drawResourcesFiller.getResourcesGPUBuffer(), + .size = drawResourcesFiller.getCurrentDrawObjectsBufferSize(), + .buffer = drawResourcesFiller.gpuDrawBuffers.drawObjectsBuffer, + }; + } + if (drawResourcesFiller.getCurrentGeometryBufferSize() > 0u) + { + auto& bufferBarrier = bufferBarriers[bufferBarriersCount++]; + bufferBarrier.barrier.dep.srcStageMask = PIPELINE_STAGE_FLAGS::COPY_BIT; + bufferBarrier.barrier.dep.srcAccessMask = ACCESS_FLAGS::TRANSFER_WRITE_BIT; + bufferBarrier.barrier.dep.dstStageMask = PIPELINE_STAGE_FLAGS::VERTEX_SHADER_BIT; + bufferBarrier.barrier.dep.dstAccessMask = ACCESS_FLAGS::SHADER_READ_BITS; + bufferBarrier.range = + { + .offset = 0u, + .size = drawResourcesFiller.getCurrentGeometryBufferSize(), + .buffer = drawResourcesFiller.gpuDrawBuffers.geometryBuffer, + }; + } + if (drawResourcesFiller.getCurrentMainObjectsBufferSize() > 0u) + { + auto& bufferBarrier = bufferBarriers[bufferBarriersCount++]; + bufferBarrier.barrier.dep.srcStageMask = PIPELINE_STAGE_FLAGS::COPY_BIT; + bufferBarrier.barrier.dep.srcAccessMask = ACCESS_FLAGS::TRANSFER_WRITE_BIT; + bufferBarrier.barrier.dep.dstStageMask = PIPELINE_STAGE_FLAGS::VERTEX_SHADER_BIT | PIPELINE_STAGE_FLAGS::FRAGMENT_SHADER_BIT; + bufferBarrier.barrier.dep.dstAccessMask = ACCESS_FLAGS::SHADER_READ_BITS; + bufferBarrier.range = + { + .offset = 0u, + .size = drawResourcesFiller.getCurrentMainObjectsBufferSize(), + .buffer = drawResourcesFiller.gpuDrawBuffers.mainObjectsBuffer, + }; + } + if (drawResourcesFiller.getCurrentLineStylesBufferSize() > 0u) + { + auto& bufferBarrier = bufferBarriers[bufferBarriersCount++]; + bufferBarrier.barrier.dep.srcStageMask = PIPELINE_STAGE_FLAGS::COPY_BIT; + bufferBarrier.barrier.dep.srcAccessMask = ACCESS_FLAGS::TRANSFER_WRITE_BIT; + bufferBarrier.barrier.dep.dstStageMask = PIPELINE_STAGE_FLAGS::VERTEX_SHADER_BIT | PIPELINE_STAGE_FLAGS::FRAGMENT_SHADER_BIT; + bufferBarrier.barrier.dep.dstAccessMask = ACCESS_FLAGS::SHADER_READ_BITS; + bufferBarrier.range = + { + .offset = 0u, + .size = drawResourcesFiller.getCurrentLineStylesBufferSize(), + .buffer = drawResourcesFiller.gpuDrawBuffers.lineStylesBuffer, }; } cb->pipelineBarrier(E_DEPENDENCY_FLAGS::EDF_NONE, { .bufBarriers = {bufferBarriers, bufferBarriersCount}, .imgBarriers = {} }); @@ -1770,86 +1393,41 @@ class ComputerAidedDesign final : public nbl::examples::SimpleWindowedApplicatio }; } cb->beginRenderPass(beginInfo, IGPUCommandBuffer::SUBPASS_CONTENTS::INLINE); - - IGPUDescriptorSet* descriptorSets[] = { descriptorSet0.get(), descriptorSet1.get() }; - cb->bindDescriptorSets(asset::EPBP_GRAPHICS, m_pipelineLayout.get(), 0u, 2u, descriptorSets); - - cb->bindGraphicsPipeline(m_graphicsPipeline.get()); - - for (auto& drawCall : drawResourcesFiller.getDrawCalls()) - { - if (drawCall.isDTMRendering) - { - cb->bindIndexBuffer({ .offset = resourcesCollection.geometryInfo.bufferOffset + drawCall.dtm.indexBufferOffset, .buffer = drawResourcesFiller.getResourcesGPUBuffer()}, asset::EIT_32BIT); - - PushConstants pc = { - .triangleMeshVerticesBaseAddress = drawCall.dtm.triangleMeshVerticesBaseAddress + resourcesGPUBuffer->getDeviceAddress() + resourcesCollection.geometryInfo.bufferOffset, - .triangleMeshMainObjectIndex = drawCall.dtm.triangleMeshMainObjectIndex, - .isDTMRendering = true - }; - cb->pushConstants(m_graphicsPipeline->getLayout(), IShader::E_SHADER_STAGE::ESS_VERTEX | IShader::E_SHADER_STAGE::ESS_FRAGMENT, 0, sizeof(PushConstants), &pc); - - cb->drawIndexed(drawCall.dtm.indexCount, 1u, 0u, 0u, 0u); - } - else - { - PushConstants pc = { - .isDTMRendering = false - }; - cb->pushConstants(m_graphicsPipeline->getLayout(), IShader::E_SHADER_STAGE::ESS_VERTEX | IShader::E_SHADER_STAGE::ESS_FRAGMENT, 0, sizeof(PushConstants), &pc); - const uint64_t indexOffset = drawCall.drawObj.drawObjectStart * 6u; - const uint64_t indexCount = drawCall.drawObj.drawObjectCount * 6u; - - // assert(currentIndexCount == resourcesCollection.indexBuffer.getCount()); - cb->bindIndexBuffer({ .offset = resourcesCollection.indexBuffer.bufferOffset + indexOffset * sizeof(uint32_t), .buffer = resourcesGPUBuffer}, asset::EIT_32BIT); - cb->drawIndexed(indexCount, 1u, 0u, 0u, 0u); - } - } + const uint32_t currentIndexCount = drawResourcesFiller.getDrawObjectCount() * 6u; + IGPUDescriptorSet* descriptorSets[] = { descriptorSet0.get(), descriptorSet1.get() }; + cb->bindDescriptorSets(asset::EPBP_GRAPHICS, pipelineLayout.get(), 0u, 2u, descriptorSets); + cb->bindIndexBuffer({ .offset = 0u, .buffer = drawResourcesFiller.gpuDrawBuffers.indexBuffer.get() }, asset::EIT_32BIT); + cb->bindGraphicsPipeline(graphicsPipeline.get()); + cb->drawIndexed(currentIndexCount, 1u, 0u, 0u, 0u); if (fragmentShaderInterlockEnabled) { cb->bindGraphicsPipeline(resolveAlphaGraphicsPipeline.get()); nbl::ext::FullScreenTriangle::recordDrawCall(cb); } - + if constexpr (DebugModeWireframe) { - cb->bindGraphicsPipeline(m_debugGraphicsPipeline.get()); - - for (auto& drawCall : drawResourcesFiller.getDrawCalls()) - { - PushConstants pc = { - .isDTMRendering = false - }; - cb->pushConstants(m_debugGraphicsPipeline->getLayout(), IShader::E_SHADER_STAGE::ESS_VERTEX | IShader::E_SHADER_STAGE::ESS_FRAGMENT, 0, sizeof(PushConstants), &pc); - - const uint64_t indexOffset = drawCall.drawObj.drawObjectStart * 6u; - const uint64_t indexCount = drawCall.drawObj.drawObjectCount * 6u; - - // assert(currentIndexCount == resourcesCollection.indexBuffer.getCount()); - cb->bindIndexBuffer({ .offset = resourcesCollection.indexBuffer.bufferOffset + indexOffset * sizeof(uint32_t), .buffer = resourcesGPUBuffer}, asset::EIT_32BIT); - - cb->drawIndexed(indexCount, 1u, 0u, 0u, 0u); - } + cb->bindGraphicsPipeline(debugGraphicsPipeline.get()); + cb->drawIndexed(currentIndexCount, 1u, 0u, 0u, 0u); } + cb->endRenderPass(); if (!inBetweenSubmit) cb->endDebugMarker(); - - drawResourcesFiller.markFrameUsageComplete(intendedSubmitInfo.getFutureScratchSemaphore().value); if (inBetweenSubmit) { - if (intendedSubmitInfo.overflowSubmit(m_currentRecordingCommandBufferInfo) != IQueue::RESULT::SUCCESS) + if (intendedSubmitInfo.overflowSubmit() != IQueue::RESULT::SUCCESS) { m_logger->log("overflow submit failed.", ILogger::ELL_ERROR); } } else { - // cb->end(); + cb->end(); const auto nextFrameIx = m_realFrameIx+1u; const IQueue::SSubmitInfo::SSemaphoreInfo thisFrameRendered = { @@ -1857,7 +1435,7 @@ class ComputerAidedDesign final : public nbl::examples::SimpleWindowedApplicatio .value = nextFrameIx, .stageMask = asset::PIPELINE_STAGE_FLAGS::ALL_COMMANDS_BITS }; - if (intendedSubmitInfo.submit(m_currentRecordingCommandBufferInfo, { &thisFrameRendered,1 }) == IQueue::RESULT::SUCCESS) + if (getGraphicsQueue()->submit(intendedSubmitInfo.popSubmit({&thisFrameRendered,1})) == IQueue::RESULT::SUCCESS) { m_realFrameIx = nextFrameIx; @@ -1888,8 +1466,6 @@ class ComputerAidedDesign final : public nbl::examples::SimpleWindowedApplicatio virtual bool onAppTerminated() override { - m_currentRecordingCommandBufferInfo->cmdbuf->end(); - // We actually want to wait for all the frames to finish rendering, otherwise our destructors will run out of order late m_device->waitIdle(); @@ -1904,15 +1480,6 @@ class ComputerAidedDesign final : public nbl::examples::SimpleWindowedApplicatio { auto retval = device_base_t::getRequiredDeviceFeatures(); retval.fragmentShaderPixelInterlock = FragmentShaderPixelInterlock; - retval.nullDescriptor = true; - return retval; - } - - virtual video::SPhysicalDeviceLimits getRequiredDeviceLimits() const override - { - video::SPhysicalDeviceLimits retval = base_t::getRequiredDeviceLimits(); - retval.fragmentShaderBarycentric = true; - return retval; } @@ -1922,14 +1489,28 @@ class ComputerAidedDesign final : public nbl::examples::SimpleWindowedApplicatio // We only support one swapchain mode, surface, the other one is Display which we have not implemented yet. retval.swapchainMode = video::E_SWAPCHAIN_MODE::ESM_SURFACE; retval.validations = true; - retval.synchronizationValidation = false; return retval; } - protected: - + void addObjects(SIntendedSubmitInfo& intendedNextSubmit) { + // we record upload of our objects and if we failed to allocate we submit everything + if (!intendedNextSubmit.valid()) + { + // log("intendedNextSubmit is invalid.", nbl::system::ILogger::ELL_ERROR); + assert(false); + return; + } + + // Use the last command buffer in intendedNextSubmit, it should be in recording state + auto* cmdbuf = intendedNextSubmit.getScratchCommandBuffer(); + + assert(cmdbuf->getState() == video::IGPUCommandBuffer::STATE::RECORDING && cmdbuf->isResettable()); + assert(cmdbuf->getRecordingFlags().hasFlags(video::IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT)); + + auto* cmdpool = cmdbuf->getPool(); + drawResourcesFiller.setSubmitDrawsFunction( [&](SIntendedSubmitInfo& intendedNextSubmit) { @@ -1957,84 +1538,29 @@ class ComputerAidedDesign final : public nbl::examples::SimpleWindowedApplicatio } else if (mode == ExampleMode::CASE_1) { - // For Taking - } - else if (mode == ExampleMode::CASE_2) - { - auto debug = [&](CPolyline polyline, LineStyleInfo lineStyle) - { - drawResourcesFiller.drawPolyline(polyline, lineStyle, intendedNextSubmit); - }; - - int32_t hatchDebugStep = m_hatchDebugStep; - - if (false) - { - // Degenerate and Corner cases for hatches - { - { - float64_t miniGap = 1.0e-15; - // degenerate major const line points: - float64_t2 pointA = { 0.0, -50.0 + miniGap }; - float64_t2 pointB = { 50.0, -50.0 }; - CPolyline polyline; - std::vector linePoints; - { - linePoints.push_back({ 0.0, 0.0 }); - linePoints.push_back({ 50.0, 0.0 }); - linePoints.push_back(pointB); - linePoints.push_back(pointA); - linePoints.push_back({ 0.0, 0.0 }); - } - polyline.addLinePoints(linePoints); - Hatch hatch({ &polyline, 1u }, SelectedMajorAxis, logger_opt_smart_ptr(smart_refctd_ptr(m_logger)), &hatchDebugStep, debug); - drawResourcesFiller.drawHatch(hatch, float32_t4(0.4f, 1.0f, 0.1f, 1.0f), intendedNextSubmit); - linePoints.clear(); - polyline.clearEverything(); - { - linePoints.push_back(pointA); - linePoints.push_back(pointB); - linePoints.push_back({ 50.0, -100.0 }); - linePoints.push_back({ 0.0, -100.0 }); - linePoints.push_back(pointA); - } - polyline.addLinePoints(linePoints); - Hatch hatch2({ &polyline, 1u }, SelectedMajorAxis, logger_opt_smart_ptr(smart_refctd_ptr(m_logger)), &hatchDebugStep, debug); - drawResourcesFiller.drawHatch(hatch2, float32_t4(0.4f, 0.6f, 0.8f, 1.0f), intendedNextSubmit); - } - } - + LineStyleInfo style = {}; + style.screenSpaceLineWidth = 0.0f; + style.worldSpaceLineWidth = 0.8f; + style.color = float32_t4(0.619f, 0.325f, 0.709f, 0.2f); + LineStyleInfo style2 = {}; + style2.screenSpaceLineWidth = 0.0f; + style2.worldSpaceLineWidth = 0.8f; + style2.color = float32_t4(0.119f, 0.825f, 0.709f, 0.5f); - { - { - float64_t2 offset = { 150.0, 0.0 }; - float64_t miniGap = 1.0e-15 + abs(cos(m_timeElapsed * 0.00018)) * 15.0f; - CPolyline polyline; - std::vector linePoints; - { - linePoints.push_back(offset + float64_t2{ 0.0, 0.0 }); - linePoints.push_back(offset + float64_t2{ 100.0, 0.0 }); - linePoints.push_back(offset + float64_t2{ 100.0, -100.0 }); - linePoints.push_back(offset + float64_t2{ 0.0, -100.0 + miniGap }); - linePoints.push_back(offset + float64_t2{ 0.0, 0.0 }); - } - polyline.addLinePoints(linePoints); - linePoints.clear(); - { - linePoints.push_back(offset + float64_t2{ 20.0, -20.0 }); - linePoints.push_back(offset + float64_t2{ 80.0, -20.0 }); - linePoints.push_back(offset + float64_t2{ 80.0, -80.0 }); - linePoints.push_back(offset + float64_t2{ 20.0, -80.0 + miniGap }); - linePoints.push_back(offset + float64_t2{ 20.0, -20.0 }); - } - polyline.addLinePoints(linePoints); - Hatch hatch({ &polyline, 1u }, SelectedMajorAxis, logger_opt_smart_ptr(smart_refctd_ptr(m_logger)), &hatchDebugStep, debug); - drawResourcesFiller.drawHatch(hatch, float32_t4(0.4f, 1.0f, 0.1f, 1.0f), intendedNextSubmit); - } - } - } - if (true) + // drawResourcesFiller.drawPolyline(bigPolyline, style, intendedNextSubmit); + // drawResourcesFiller.drawPolyline(bigPolyline2, style2, intendedNextSubmit); + } + else if (mode == ExampleMode::CASE_2) + { + auto debug = [&](CPolyline polyline, LineStyleInfo lineStyle) + { + drawResourcesFiller.drawPolyline(polyline, lineStyle, intendedNextSubmit); + }; + + int32_t hatchDebugStep = m_hatchDebugStep; + + if (hatchDebugStep > 0) { #include "bike_hatch.h" for (uint32_t i = 0; i < polylines.size(); i++) @@ -2045,59 +1571,25 @@ class ComputerAidedDesign final : public nbl::examples::SimpleWindowedApplicatio // assert(polylines[i].checkSectionsContunuity()); //drawResourcesFiller.drawPolyline(polylines[i], lineStyle, intendedNextSubmit); } - -#if 1 - auto circleThing = [&](float64_t2 offset) - { - CPolyline polyline; - std::vector> beziers; - - beziers.push_back({ float64_t2(0, -1), float64_t2(-1, -1),float64_t2(-1, 0) }); - beziers.push_back({ float64_t2(0, -1), float64_t2(1, -1),float64_t2(1, 0) }); - beziers.push_back({ float64_t2(-1, 0), float64_t2(-1, 1),float64_t2(0, 1) }); - beziers.push_back({ float64_t2(1, 0), float64_t2(1, 1),float64_t2(0, 1) }); - - for (uint32_t i = 0; i < beziers.size(); i++) - { - beziers[i].P0 = (beziers[i].P0 * 50.0) + offset; - beziers[i].P1 = (beziers[i].P1 * 50.0) + offset; - beziers[i].P2 = (beziers[i].P2 * 50.0) + offset; - } - - polyline.addQuadBeziers(beziers); - - polylines.push_back(polyline); - }; - - float64_t2 offsettMain = { 50.0, 50.0 }; - float64_t2 offsett = {30.0 * cos(m_timeElapsed * 0.002), 20.0 * sin(m_timeElapsed * 0.002)}; - float64_t2 offsett2 = {30.0 * cos(m_timeElapsed * 0.002 + 1.0) , 20.0 * sin(m_timeElapsed * 0.002 + 1.0)}; - - circleThing(float64_t2(-50, 0) - offsett + offsettMain); - circleThing(float64_t2(50, 0) - offsett2 + offsettMain); - circleThing(float64_t2(0, -50) + offsett + offsettMain); - circleThing(float64_t2(0, 50) + offsett2 + offsettMain); - -#endif - //printf("hatchDebugStep = %d\n", hatchDebugStep); std::chrono::steady_clock::time_point begin = std::chrono::steady_clock::now(); - Hatch hatch(polylines, SelectedMajorAxis, logger_opt_smart_ptr(smart_refctd_ptr(m_logger)), &hatchDebugStep, debug); + Hatch hatch(polylines, SelectedMajorAxis, &hatchDebugStep, debug); std::chrono::steady_clock::time_point end = std::chrono::steady_clock::now(); - //// std::cout << "Hatch::Hatch time = " << std::chrono::duration_cast(end - begin).count() << "[us]" << std::endl; - //std::sort(hatch.intersectionAmounts.begin(), hatch.intersectionAmounts.end()); + // std::cout << "Hatch::Hatch time = " << std::chrono::duration_cast(end - begin).count() << "[us]" << std::endl; + std::sort(hatch.intersectionAmounts.begin(), hatch.intersectionAmounts.end()); - //auto percentile = [&](float percentile) - // { - // return hatch.intersectionAmounts[uint32_t(round(percentile * float(hatch.intersectionAmounts.size() - 1)))]; - // }; + auto percentile = [&](float percentile) + { + return hatch.intersectionAmounts[uint32_t(round(percentile * float(hatch.intersectionAmounts.size() - 1)))]; + }; //printf(std::format( // "Intersection amounts: 10%%: {}, 25%%: {}, 50%%: {}, 75%%: {}, 90%%: {}, 100%% (max): {}\n", // percentile(0.1), percentile(0.25), percentile(0.5), percentile(0.75), percentile(0.9), hatch.intersectionAmounts[hatch.intersectionAmounts.size() - 1] //).c_str()); drawResourcesFiller.drawHatch(hatch, float32_t4(0.6, 0.6, 0.1, 1.0f), intendedNextSubmit); } - if (false) + + if (hatchDebugStep > 0) { std::vector polylines; auto line = [&](float64_t2 begin, float64_t2 end) { @@ -2137,10 +1629,11 @@ class ComputerAidedDesign final : public nbl::examples::SimpleWindowedApplicatio polylines.push_back(polyline); } - Hatch hatch(polylines, SelectedMajorAxis, logger_opt_smart_ptr(smart_refctd_ptr(m_logger)), &hatchDebugStep, debug); + Hatch hatch(polylines, SelectedMajorAxis, &hatchDebugStep, debug); drawResourcesFiller.drawHatch(hatch, float32_t4(0.0, 1.0, 0.1, 1.0f), intendedNextSubmit); } - if (false) + + if (hatchDebugStep > 0) { std::vector polylines; auto circleThing = [&](float64_t2 offset) @@ -2169,10 +1662,11 @@ class ComputerAidedDesign final : public nbl::examples::SimpleWindowedApplicatio circleThing(float64_t2(0, -500)); circleThing(float64_t2(0, 500)); - Hatch hatch(polylines, SelectedMajorAxis, logger_opt_smart_ptr(smart_refctd_ptr(m_logger)), &hatchDebugStep, debug); + Hatch hatch(polylines, SelectedMajorAxis, &hatchDebugStep, debug); drawResourcesFiller.drawHatch(hatch, float32_t4(1.0, 0.1, 0.1, 1.0f), intendedNextSubmit); } - if (false) + + if (hatchDebugStep > 0) { std::vector polylines; auto line = [&](float64_t2 begin, float64_t2 end) { @@ -2210,7 +1704,8 @@ class ComputerAidedDesign final : public nbl::examples::SimpleWindowedApplicatio polyline.addQuadBeziers(beziers); } } - if (false) + + if (hatchDebugStep > 0) { std::vector polylines; { @@ -2321,10 +1816,11 @@ class ComputerAidedDesign final : public nbl::examples::SimpleWindowedApplicatio polyline.addLinePoints(points); polylines.push_back(polyline); } - Hatch hatch(polylines, SelectedMajorAxis, logger_opt_smart_ptr(smart_refctd_ptr(m_logger)), &hatchDebugStep, debug); + Hatch hatch(polylines, SelectedMajorAxis, &hatchDebugStep, debug); drawResourcesFiller.drawHatch(hatch, float32_t4(0.0, 0.0, 1.0, 1.0f), intendedNextSubmit); } - if (false) + + if (hatchDebugStep > 0) { std::vector points; double sqrt3 = sqrt(3.0); @@ -2359,10 +1855,11 @@ class ComputerAidedDesign final : public nbl::examples::SimpleWindowedApplicatio polyline.addLinePoints(points); polyline.addQuadBeziers(beziers); - Hatch hatch({&polyline, 1u}, SelectedMajorAxis, logger_opt_smart_ptr(smart_refctd_ptr(m_logger)), &hatchDebugStep, debug); + Hatch hatch({&polyline, 1u}, SelectedMajorAxis, &hatchDebugStep, debug); drawResourcesFiller.drawHatch(hatch, float32_t4(1.0f, 0.325f, 0.103f, 1.0f), intendedNextSubmit); } - if (false) + + if (hatchDebugStep > 0) { CPolyline polyline; std::vector> beziers; @@ -2376,33 +1873,16 @@ class ComputerAidedDesign final : public nbl::examples::SimpleWindowedApplicatio 100.0 * float64_t2(3.7, 7.27) }); polyline.addQuadBeziers(beziers); - Hatch hatch({&polyline, 1u}, SelectedMajorAxis, logger_opt_smart_ptr(smart_refctd_ptr(m_logger)), &hatchDebugStep, debug); + Hatch hatch({&polyline, 1u}, SelectedMajorAxis, &hatchDebugStep, debug); drawResourcesFiller.drawHatch(hatch, float32_t4(0.619f, 0.325f, 0.709f, 0.9f), intendedNextSubmit); } } else if (mode == ExampleMode::CASE_3) { - // Testing Degenerate Cases Causing Bugs/Nan/Crashes - // 0 Sized Rect --> generateOffsetPolyline shouldn't return nan - { - CPolyline polyline; - std::vector linePoints; - { - linePoints.push_back({ 1.0, -20.0 }); - linePoints.push_back({ 1.0, -20.0 }); - linePoints.push_back({ 1.0, 0.0 }); - linePoints.push_back({ 1.0, 0.0 }); - linePoints.push_back({ 1.0, -20.0 }); - } - polyline.addLinePoints(linePoints); - auto parallelPoly = polyline.generateParallelPolyline(1.0); - assert(!std::isnan(parallelPoly.getLinePointAt(0).p[0])); - } - LineStyleInfo style = {}; style.screenSpaceLineWidth = 4.0f; - style.worldSpaceLineWidth = 2.0f; - style.color = float32_t4(0.7f, 0.3f, 0.1f, 0.1f); + style.worldSpaceLineWidth = 0.0f; + style.color = float32_t4(0.7f, 0.3f, 0.1f, 0.5f); LineStyleInfo style2 = {}; style2.screenSpaceLineWidth = 2.0f; @@ -2475,7 +1955,7 @@ class ComputerAidedDesign final : public nbl::examples::SimpleWindowedApplicatio myCurve.majorAxis = { -10.0, 5.0 }; myCurve.center = { 0, -5.0 }; myCurve.angleBounds = { - nbl::core::PI() * 1.0, + nbl::core::PI() * 2.0, nbl::core::PI() * 0.0 }; myCurve.eccentricity = 1.0; @@ -2503,10 +1983,10 @@ class ComputerAidedDesign final : public nbl::examples::SimpleWindowedApplicatio } drawResourcesFiller.drawPolyline(originalPolyline, style, intendedNextSubmit); - CPolyline offsettedPolyline = originalPolyline.generateParallelPolyline(+0.0 - 3.0 * abs(cos(10.0 * 0.0009))); - CPolyline offsettedPolyline2 = originalPolyline.generateParallelPolyline(+0.0 + 3.0 * abs(cos(10.0 * 0.0009))); - drawResourcesFiller.drawPolyline(offsettedPolyline, style2, intendedNextSubmit); - drawResourcesFiller.drawPolyline(offsettedPolyline2, style2, intendedNextSubmit); + //CPolyline offsettedPolyline = originalPolyline.generateParallelPolyline(+0.0 - 3.0 * abs(cos(m_timeElapsed * 0.0009))); + //CPolyline offsettedPolyline2 = originalPolyline.generateParallelPolyline(+0.0 + 3.0 * abs(cos(m_timeElapsed * 0.0009))); + //drawResourcesFiller.drawPolyline(offsettedPolyline, style2, intendedNextSubmit); + //drawResourcesFiller.drawPolyline(offsettedPolyline2, style2, intendedNextSubmit); } else if (mode == ExampleMode::CASE_4) { @@ -2660,11 +2140,11 @@ class ComputerAidedDesign final : public nbl::examples::SimpleWindowedApplicatio else if (mode == ExampleMode::CASE_5) { //#define CASE_5_POLYLINE_1 // animated stipple pattern -#define CASE_5_POLYLINE_2 // miter test static +//#define CASE_5_POLYLINE_2 // miter test static //#define CASE_5_POLYLINE_3 // miter test animated //#define CASE_5_POLYLINE_4 // miter test animated (every angle) //#define CASE_5_POLYLINE_5 // closed polygon -// #define CASE_5_POLYLINE_6 // stretching +#define CASE_5_POLYLINE_6 // stretching //#define CASE_5_POLYLINE_7 // wide non solid lines #if defined(CASE_5_POLYLINE_1) @@ -2780,7 +2260,7 @@ class ComputerAidedDesign final : public nbl::examples::SimpleWindowedApplicatio /*quadratics2[3].P0 = {20.0, 50.0}; quadratics2[3].P1 = { -80.0, 100.0 }; quadratics2[3].P2 = { -100.0, 90.0 };*/ - polyline.addQuadBeziers(quadratics2); + polyline.addQuadBeziers(core::SRange>(quadratics2.data(), quadratics2.data() + quadratics2.size())); // section 3: lines std::vector linePoints2; @@ -3011,7 +2491,7 @@ class ComputerAidedDesign final : public nbl::examples::SimpleWindowedApplicatio linePoints.push_back({ -50.0, 50.0 }); linePoints.push_back({ -50.0 + linesLength, 50.0 }); polyline.addLinePoints(linePoints); - polyline.preprocessPolylineWithStyle(style, 1e-5, addShapesFunction); + polyline.preprocessPolylineWithStyle(style, addShapesFunction); drawResourcesFiller.drawPolyline(polyline, style, intendedNextSubmit); } @@ -3022,7 +2502,7 @@ class ComputerAidedDesign final : public nbl::examples::SimpleWindowedApplicatio linePoints.push_back({ -50.0, 48.0 }); linePoints.push_back({ -50.0 + linesLength, 48.0 }); polyline.addLinePoints(linePoints); - polyline.preprocessPolylineWithStyle(style, 1e-5, addShapesFunction); + polyline.preprocessPolylineWithStyle(style, addShapesFunction); drawResourcesFiller.drawPolyline(polyline, style, intendedNextSubmit); } @@ -3034,7 +2514,7 @@ class ComputerAidedDesign final : public nbl::examples::SimpleWindowedApplicatio linePoints.push_back({ -50.0, 46.0 }); linePoints.push_back({ -50.0 + linesLength, 46.0 }); polyline.addLinePoints(linePoints); - polyline.preprocessPolylineWithStyle(style, 1e-5, addShapesFunction); + polyline.preprocessPolylineWithStyle(style, addShapesFunction); drawResourcesFiller.drawPolyline(polyline, style, intendedNextSubmit); } @@ -3061,7 +2541,7 @@ class ComputerAidedDesign final : public nbl::examples::SimpleWindowedApplicatio curves::Subdivision::adaptive(myCurve, 1e-3, addToBezier, 10u); polyline.addQuadBeziers(quadBeziers); - polyline.preprocessPolylineWithStyle(style, 1e-5, addShapesFunction); + polyline.preprocessPolylineWithStyle(style, addShapesFunction); drawResourcesFiller.drawPolyline(polyline, style, intendedNextSubmit); } @@ -3089,7 +2569,7 @@ class ComputerAidedDesign final : public nbl::examples::SimpleWindowedApplicatio curves::Subdivision::adaptive(myCurve, 1e-3, addToBezier, 10u); polyline.addQuadBeziers(quadBeziers); - polyline.preprocessPolylineWithStyle(style, 1e-5, addShapesFunction); + polyline.preprocessPolylineWithStyle(style, addShapesFunction); drawResourcesFiller.drawPolyline(polyline, style, intendedNextSubmit); } @@ -3218,20 +2698,16 @@ class ComputerAidedDesign final : public nbl::examples::SimpleWindowedApplicatio } else if (mode == ExampleMode::CASE_6) { - float64_t3x3 customProjection = float64_t3x3{ - 1.0, 0.0, cos(m_timeElapsed * 0.0005) * 100.0, - 0.0, 1.0, 0.0, - 0.0, 0.0, 1.0 - }; - - /// [NOTE]: We set minClip and maxClip (in default worldspace) in such a way that minClip.y > maxClip.y so that minClipNDC.y < maxClipNDC.y - // left half should be red and right half should be green - WorldClipRect showLeft = {}; - showLeft.minClip = float64_t2(-100.0, +1000.0); - showLeft.maxClip = float64_t2(0.0, -1000.0); - WorldClipRect showRight = {}; - showRight.minClip = float64_t2(0.0, +1000.0); - showRight.maxClip = float64_t2(100.0, -1000.0); + // left half of screen should be red and right half should be green + const auto& cameraProj = m_Camera.constructViewProjection(); + ClipProjectionData showLeft = {}; + showLeft.projectionToNDC = cameraProj; + showLeft.minClipNDC = float32_t2(-1.0, -1.0); + showLeft.maxClipNDC = float32_t2(0.0, +1.0); + ClipProjectionData showRight = {}; + showRight.projectionToNDC = cameraProj; + showRight.minClipNDC = float32_t2(0.0, -1.0); + showRight.maxClipNDC = float32_t2(+1.0, +1.0); LineStyleInfo leftLineStyle = {}; leftLineStyle.screenSpaceLineWidth = 3.0f; @@ -3286,50 +2762,183 @@ class ComputerAidedDesign final : public nbl::examples::SimpleWindowedApplicatio } // we do redundant and nested push/pops to test - drawResourcesFiller.pushCustomClipRect(showLeft); + drawResourcesFiller.pushClipProjectionData(showLeft); { drawResourcesFiller.drawPolyline(polyline1, leftLineStyle, intendedNextSubmit); - drawResourcesFiller.pushCustomClipRect(showRight); - drawResourcesFiller.pushCustomProjection(customProjection); + drawResourcesFiller.pushClipProjectionData(showRight); { drawResourcesFiller.drawPolyline(polyline1, rightLineStyle, intendedNextSubmit); drawResourcesFiller.drawPolyline(polyline2, rightLineStyle, intendedNextSubmit); } - drawResourcesFiller.popCustomProjection(); - drawResourcesFiller.popCustomClipRect(); + drawResourcesFiller.popClipProjectionData(); drawResourcesFiller.drawPolyline(polyline2, leftLineStyle, intendedNextSubmit); - drawResourcesFiller.pushCustomClipRect(showRight); + drawResourcesFiller.pushClipProjectionData(showRight); { drawResourcesFiller.drawPolyline(polyline3, rightLineStyle, intendedNextSubmit); drawResourcesFiller.drawPolyline(polyline2, rightLineStyle, intendedNextSubmit); - drawResourcesFiller.pushCustomClipRect(showLeft); + drawResourcesFiller.pushClipProjectionData(showLeft); { drawResourcesFiller.drawPolyline(polyline1, leftLineStyle, intendedNextSubmit); } - drawResourcesFiller.popCustomClipRect(); + drawResourcesFiller.popClipProjectionData(); } - drawResourcesFiller.popCustomClipRect(); + drawResourcesFiller.popClipProjectionData(); drawResourcesFiller.drawPolyline(polyline2, leftLineStyle, intendedNextSubmit); } - drawResourcesFiller.popCustomClipRect(); + drawResourcesFiller.popClipProjectionData(); } else if (mode == ExampleMode::CASE_7) { - for (uint32_t i = 0; i < sampleImages.size(); ++i) + if (m_realFrameIx == 0u) { - uint64_t imageID = i * 69ull; // it can be hash or something of the file path the image was loaded from - //printf(std::format("\n Image {} \n", i).c_str()); - drawResourcesFiller.ensureStaticImageAvailability({ imageID, sampleImages[i] }, intendedNextSubmit); - drawResourcesFiller.addImageObject(imageID, { .topLeft = { 0.0 + (i) * 3.0, 0.0 }, .dirU = { 3.0 , 0.0 }, .aspectRatio = 1.0 }, intendedNextSubmit); - //printf("\n"); - } + // Load image + system::path m_loadCWD = ".."; + std::string imagePath = "../../media/color_space_test/R8G8B8A8_1.png"; + + constexpr auto cachingFlags = static_cast(IAssetLoader::ECF_DONT_CACHE_REFERENCES & IAssetLoader::ECF_DONT_CACHE_TOP_LEVEL); + const IAssetLoader::SAssetLoadParams loadParams(0ull, nullptr, cachingFlags, IAssetLoader::ELPF_NONE, m_logger.get(),m_loadCWD); + auto bundle = m_assetMgr->getAsset(imagePath,loadParams); + auto contents = bundle.getContents(); + if (contents.empty()) + { + m_logger->log("Failed to load image with path %s, skipping!",ILogger::ELL_ERROR,(m_loadCWD/imagePath).c_str()); + } + + smart_refctd_ptr cpuImgView; + const auto& asset = contents[0]; + switch (asset->getAssetType()) + { + case IAsset::ET_IMAGE: + { + auto image = smart_refctd_ptr_static_cast(asset); + const auto format = image->getCreationParameters().format; + + ICPUImageView::SCreationParams viewParams = { + .flags = ICPUImageView::E_CREATE_FLAGS::ECF_NONE, + .image = std::move(image), + .viewType = IImageView::E_TYPE::ET_2D, + .format = format, + .subresourceRange = { + .aspectMask = IImage::E_ASPECT_FLAGS::EAF_COLOR_BIT, + .baseMipLevel = 0u, + .levelCount = ICPUImageView::remaining_mip_levels, + .baseArrayLayer = 0u, + .layerCount = ICPUImageView::remaining_array_layers + } + }; + + cpuImgView = ICPUImageView::create(std::move(viewParams)); + } break; + + case IAsset::ET_IMAGE_VIEW: + cpuImgView = smart_refctd_ptr_static_cast(asset); + break; + default: + m_logger->log("Failed to load ICPUImage or ICPUImageView got some other Asset Type, skipping!",ILogger::ELL_ERROR); + return; + } + + + // create matching size gpu image + smart_refctd_ptr gpuImg; + const auto& origParams = cpuImgView->getCreationParameters(); + const auto origImage = origParams.image; + IGPUImage::SCreationParams imageParams = {}; + imageParams = origImage->getCreationParameters(); + imageParams.usage |= IGPUImage::EUF_TRANSFER_DST_BIT|IGPUImage::EUF_SAMPLED_BIT; + // promote format because RGB8 and friends don't actually exist in HW + { + const IPhysicalDevice::SImageFormatPromotionRequest request = { + .originalFormat = imageParams.format, + .usages = IPhysicalDevice::SFormatImageUsages::SUsage(imageParams.usage) + }; + imageParams.format = m_physicalDevice->promoteImageFormat(request,imageParams.tiling); + } + gpuImg = m_device->createImage(std::move(imageParams)); + if (!gpuImg || !m_device->allocate(gpuImg->getMemoryReqs(),gpuImg.get()).isValid()) + return; + gpuImg->setObjectDebugName(imagePath.c_str()); + + IGPUImageView::SCreationParams viewParams = { + .image = gpuImg, + .viewType = IGPUImageView::ET_2D, + .format = gpuImg->getCreationParameters().format + }; + auto gpuImgView = m_device->createImageView(std::move(viewParams)); + // Bind gpu image view to descriptor set + video::IGPUDescriptorSet::SDescriptorInfo dsInfo; + dsInfo.info.image.imageLayout = IImage::LAYOUT::READ_ONLY_OPTIMAL; + dsInfo.desc = gpuImgView; + + IGPUDescriptorSet::SWriteDescriptorSet dsWrites[1u] = + { + { + .dstSet = descriptorSet0.get(), + .binding = 6u, + .arrayElement = 0u, + .count = 1u, + .info = &dsInfo, + } + }; + m_device->updateDescriptorSets(1u, dsWrites, 0u, nullptr); + + // Upload Loaded CPUImageData to GPU + IGPUCommandBuffer::SPipelineBarrierDependencyInfo::image_barrier_t beforeCopyImageBarriers[] = + { + { + .barrier = { + .dep = { + .srcStageMask = PIPELINE_STAGE_FLAGS::NONE, // previous top of pipe -> top_of_pipe in first scope = none + .srcAccessMask = ACCESS_FLAGS::NONE, + .dstStageMask = PIPELINE_STAGE_FLAGS::COPY_BIT, + .dstAccessMask = ACCESS_FLAGS::TRANSFER_WRITE_BIT, + } + // .ownershipOp. No queueFam ownership transfer + }, + .image = gpuImg.get(), + .subresourceRange = origParams.subresourceRange, + .oldLayout = IImage::LAYOUT::UNDEFINED, + .newLayout = IImage::LAYOUT::TRANSFER_DST_OPTIMAL, + } + }; + + cmdbuf->pipelineBarrier(E_DEPENDENCY_FLAGS::EDF_NONE, { .imgBarriers = beforeCopyImageBarriers }); + m_utils->updateImageViaStagingBuffer( + intendedNextSubmit, + origImage->getBuffer()->getPointer(), origImage->getCreationParameters().format, + gpuImg.get(), IImage::LAYOUT::TRANSFER_DST_OPTIMAL, + origImage->getRegions()); + + IGPUCommandBuffer::SPipelineBarrierDependencyInfo::image_barrier_t afterCopyImageBarriers[] = + { + { + .barrier = { + .dep = { + .srcStageMask = PIPELINE_STAGE_FLAGS::COPY_BIT, // previous top of pipe -> top_of_pipe in first scope = none + .srcAccessMask = ACCESS_FLAGS::TRANSFER_WRITE_BIT, + .dstStageMask = PIPELINE_STAGE_FLAGS::FRAGMENT_SHADER_BIT, + .dstAccessMask = ACCESS_FLAGS::SHADER_READ_BITS, + } + // .ownershipOp. No queueFam ownership transfer + }, + .image = gpuImg.get(), + .subresourceRange = origParams.subresourceRange, + .oldLayout = IImage::LAYOUT::TRANSFER_DST_OPTIMAL, + .newLayout = IImage::LAYOUT::READ_ONLY_OPTIMAL, + } + }; + cmdbuf->pipelineBarrier(E_DEPENDENCY_FLAGS::EDF_NONE, { .imgBarriers = afterCopyImageBarriers }); + } + drawResourcesFiller._test_addImageObject({ 0.0, 0.0 }, { 100.0, 100.0 }, 0.0, intendedNextSubmit); + drawResourcesFiller._test_addImageObject({ 40.0, +40.0 }, { 100.0, 100.0 }, 0.0, intendedNextSubmit); + LineStyleInfo lineStyle = { .color = float32_t4(1.0f, 0.1f, 0.1f, 0.9f), @@ -3341,8 +2950,8 @@ class ComputerAidedDesign final : public nbl::examples::SimpleWindowedApplicatio { std::vector linePoints; linePoints.push_back({ 0.0, 0.0 }); - linePoints.push_back({ 1.0, 0.0 }); - linePoints.push_back({ 1.0, -1.0 }); + linePoints.push_back({ 100.0, 0.0 }); + linePoints.push_back({ 100.0, -100.0 }); polyline.addLinePoints(linePoints); } drawResourcesFiller.drawPolyline(polyline, lineStyle, intendedNextSubmit); @@ -3367,7 +2976,7 @@ class ComputerAidedDesign final : public nbl::examples::SimpleWindowedApplicatio auto addPt = [&](float64_t2 p) { auto point = p / 8.0; - points.push_back(point * hatchFillShapeSize + float64_t2(offset, -300.0 - hatchFillShapeSize)); + points.push_back(point * hatchFillShapeSize + float64_t2(offset, -200.0 - hatchFillShapeSize)); }; addPt(float64_t2(0.0, 0.0)); addPt(float64_t2(8.0, 0.0)); @@ -3377,7 +2986,7 @@ class ComputerAidedDesign final : public nbl::examples::SimpleWindowedApplicatio squareBelow.addLinePoints(points); } - Hatch filledHatch({&squareBelow, 1}, SelectedMajorAxis); + Hatch filledHatch(std::span{std::addressof(squareBelow), 1}, SelectedMajorAxis); // This draws a square that is textured with the fill pattern at hatchFillShapeIdx drawResourcesFiller.drawHatch( filledHatch, @@ -3392,15 +3001,10 @@ class ComputerAidedDesign final : public nbl::examples::SimpleWindowedApplicatio if (singleLineText) { float32_t rotation = 0.0; // nbl::core::PI()* abs(cos(m_timeElapsed * 0.00005)); - float32_t italicTiltAngle = nbl::core::PI() / 9.0f; - singleLineText->Draw(drawResourcesFiller, intendedNextSubmit, m_font.get(), float64_t2(0.0,-100.0), float32_t2(1.0, 1.0), rotation, float32_t4(1.0, 1.0, 1.0, 1.0), 0.0f, 0.0f); - singleLineText->Draw(drawResourcesFiller, intendedNextSubmit, m_font.get(), float64_t2(0.0,-150.0), float32_t2(1.0, 1.0), rotation, float32_t4(1.0, 1.0, 1.0, 1.0), 0.0f, 0.5f); - singleLineText->Draw(drawResourcesFiller, intendedNextSubmit, m_font.get(), float64_t2(0.0,-200.0), float32_t2(1.0, 1.0), rotation, float32_t4(1.0, 1.0, 1.0, 1.0), italicTiltAngle, 0.0f); - singleLineText->Draw(drawResourcesFiller, intendedNextSubmit, m_font.get(), float64_t2(0.0,-250.0), float32_t2(1.0, 1.0), rotation, float32_t4(1.0, 1.0, 1.0, 1.0), italicTiltAngle, 0.5f); - // singleLineText->Draw(drawResourcesFiller, intendedNextSubmit, float64_t2(0.0,-200.0), float32_t2(1.0, 1.0), nbl::core::PI() * abs(cos(m_timeElapsed * 0.00005))); - // Smaller text to test level maps - //singleLineText->Draw(drawResourcesFiller, intendedNextSubmit, float64_t2(0.0,-130.0), float32_t2(0.4, 0.4), rotation); - //singleLineText->Draw(drawResourcesFiller, intendedNextSubmit, float64_t2(0.0,-150.0), float32_t2(0.2, 0.2), rotation); + singleLineText->Draw(drawResourcesFiller, intendedNextSubmit, float64_t2(0.0,-100.0), float32_t2(1.0, 1.0), rotation); + // Smaller text to test mip maps + singleLineText->Draw(drawResourcesFiller, intendedNextSubmit, float64_t2(0.0,-130.0), float32_t2(0.4, 0.4), rotation); + singleLineText->Draw(drawResourcesFiller, intendedNextSubmit, float64_t2(0.0,-150.0), float32_t2(0.2, 0.2), rotation); } const bool drawTextHatches = true; @@ -3412,14 +3016,23 @@ class ComputerAidedDesign final : public nbl::examples::SimpleWindowedApplicatio auto penY = -500.0; auto previous = 0; + uint32_t glyphObjectIdx; + { + LineStyleInfo lineStyle = {}; + lineStyle.color = float32_t4(1.0, 1.0, 1.0, 1.0); + const uint32_t styleIdx = drawResourcesFiller.addLineStyle_SubmitIfNeeded(lineStyle, intendedNextSubmit); + + glyphObjectIdx = drawResourcesFiller.addMainObject_SubmitIfNeeded(styleIdx, intendedNextSubmit); + } + float64_t2 currentBaselineStart = float64_t2(0.0, 0.0); float64_t scale = 1.0 / 64.0; for (uint32_t i = 0; i < strlen(TestString); i++) { char k = TestString[i]; - auto glyphIndex = m_font->getGlyphIndex(wchar_t(k)); - const auto glyphMetrics = m_font->getGlyphMetrics(glyphIndex); + auto glyphIndex = m_arialFont->getGlyphIndex(wchar_t(k)); + const auto glyphMetrics = m_arialFont->getGlyphMetrics(glyphIndex); const float64_t2 baselineStart = currentBaselineStart; currentBaselineStart += glyphMetrics.advance; @@ -3467,12 +3080,12 @@ class ComputerAidedDesign final : public nbl::examples::SimpleWindowedApplicatio ftFunctions.cubic_to = &ftCubicTo; ftFunctions.shift = 0; ftFunctions.delta = 0; - auto error = FT_Outline_Decompose(&m_font->getGlyphSlot(glyphIndex)->outline, &ftFunctions, &hatchBuilder); + auto error = FT_Outline_Decompose(&m_arialFont->getGlyphSlot(glyphIndex)->outline, &ftFunctions, &hatchBuilder); assert(!error); hatchBuilder.finish(); } msdfgen::Shape glyphShape; - bool loadedGlyph = drawFreetypeGlyph(glyphShape, m_textRenderer->getFreetypeLibrary(), m_font->getFreetypeFace()); + bool loadedGlyph = drawFreetypeGlyph(glyphShape, m_textRenderer->getFreetypeLibrary(), m_arialFont->getFreetypeFace()); assert(loadedGlyph); auto& shapePolylines = hatchBuilder.polylines; @@ -3544,449 +3157,38 @@ class ComputerAidedDesign final : public nbl::examples::SimpleWindowedApplicatio } } - else if (mode == ExampleMode::CASE_9) - { - // PYRAMID - core::vector vertices = { - //{ float64_t2(0.0, 0.0), 100.0 }, //0 - //{ float64_t2(-200.0, -200.0), 10.0 }, //1 - //{ float64_t2(200.0, -200.0), 10.0 }, //2 - //{ float64_t2(200.0, 200.0), -20.0 }, //3 - //{ float64_t2(-200.0, 200.0), 10.0 }, //4 - - { float64_t2(0.0, 0.0), 100.0 }, - { float64_t2(-200.0, -200.0), 10.0 }, - { float64_t2(200.0, -100.0), 10.0 }, - { float64_t2(0.0, 0.0), 100.0 }, - { float64_t2(200.0, -100.0), 10.0 }, - { float64_t2(200.0, 200.0), -20.0 }, - { float64_t2(0.0, 0.0), 100.0 }, - { float64_t2(200.0, 200.0), -20.0 }, - { float64_t2(-200.0, 200.0), 10.0 }, - { float64_t2(0.0, 0.0), 100.0 }, - { float64_t2(-200.0, 200.0), 10.0 }, - { float64_t2(-200.0, -200.0), 10.0 }, - }; - - core::vector indices = { - 0, 1, 2, - 3, 4, 5, - 6, 7, 8, - 9, 10, 11 - }; - - // SINGLE TRIANGLE - /*core::vector vertices = { - { float64_t2(0.0, 0.0), -20.0 }, - { float64_t2(-200.0, -200.0), 100.0 }, - { float64_t2(200.0, -100.0), 80.0 }, - }; - - core::vector indices = { - 0, 1, 2 - };*/ - - CTriangleMesh mesh; - mesh.setVertices(std::move(vertices)); - mesh.setIndices(std::move(indices)); - - DTMSettingsInfo dtmInfo{}; - //dtmInfo.mode |= E_DTM_MODE::OUTLINE; - dtmInfo.mode |= E_DTM_MODE::HEIGHT_SHADING; - dtmInfo.mode |= E_DTM_MODE::CONTOUR; - - dtmInfo.outlineStyleInfo.screenSpaceLineWidth = 0.0f; - dtmInfo.outlineStyleInfo.worldSpaceLineWidth = 1.0f; - dtmInfo.outlineStyleInfo.color = float32_t4(0.0f, 0.39f, 0.0f, 1.0f); - std::array outlineStipplePattern = { 0.0f, -5.0f, 20.0f, -5.0f }; - dtmInfo.outlineStyleInfo.setStipplePatternData(outlineStipplePattern); - - dtmInfo.contourSettingsCount = 2u; - dtmInfo.contourSettings[0u].startHeight = 20; - dtmInfo.contourSettings[0u].endHeight = 90; - dtmInfo.contourSettings[0u].heightInterval = 9.98; - dtmInfo.contourSettings[0u].lineStyleInfo.screenSpaceLineWidth = 0.0f; - dtmInfo.contourSettings[0u].lineStyleInfo.worldSpaceLineWidth = 1.0f; - dtmInfo.contourSettings[0u].lineStyleInfo.color = float32_t4(0.0f, 0.0f, 1.0f, 0.7f); - std::array contourStipplePattern = { 0.0f, -5.0f, 10.0f, -5.0f }; - dtmInfo.contourSettings[0u].lineStyleInfo.setStipplePatternData(contourStipplePattern); - - dtmInfo.contourSettings[1u] = dtmInfo.contourSettings[0u]; - dtmInfo.contourSettings[1u].startHeight += 5.0f; - dtmInfo.contourSettings[1u].heightInterval = 13.0f; - dtmInfo.contourSettings[1u].lineStyleInfo.color = float32_t4(0.8f, 0.4f, 0.3f, 1.0f); - - // PRESS 1, 2, 3 TO SWITCH HEIGHT SHADING MODE - // 1 - DISCRETE_VARIABLE_LENGTH_INTERVALS - // 2 - DISCRETE_FIXED_LENGTH_INTERVALS - // 3 - CONTINOUS_INTERVALS - float animatedAlpha = (std::cos(m_timeElapsed * 0.0005) + 1.0) * 0.5; - switch (m_shadingModeExample) - { - case E_HEIGHT_SHADING_MODE::DISCRETE_VARIABLE_LENGTH_INTERVALS: - { - dtmInfo.heightShadingInfo.heightShadingMode = E_HEIGHT_SHADING_MODE::DISCRETE_VARIABLE_LENGTH_INTERVALS; - - dtmInfo.heightShadingInfo.addHeightColorMapEntry(-10.0f, float32_t4(0.5f, 1.0f, 1.0f, 1.0f)); - dtmInfo.heightShadingInfo.addHeightColorMapEntry(20.0f, float32_t4(0.0f, 1.0f, 0.0f, 1.0f)); - dtmInfo.heightShadingInfo.addHeightColorMapEntry(25.0f, float32_t4(1.0f, 1.0f, 0.0f, animatedAlpha)); - dtmInfo.heightShadingInfo.addHeightColorMapEntry(70.0f, float32_t4(1.0f, 0.0f, 0.0f, 1.0f)); - dtmInfo.heightShadingInfo.addHeightColorMapEntry(90.0f, float32_t4(1.0f, 0.0f, 0.0f, 1.0f)); - - break; - } - case E_HEIGHT_SHADING_MODE::DISCRETE_FIXED_LENGTH_INTERVALS: - { - dtmInfo.heightShadingInfo.intervalLength = 10.0f; - dtmInfo.heightShadingInfo.intervalIndexToHeightMultiplier = dtmInfo.heightShadingInfo.intervalLength; - dtmInfo.heightShadingInfo.isCenteredShading = false; - dtmInfo.heightShadingInfo.heightShadingMode = E_HEIGHT_SHADING_MODE::DISCRETE_FIXED_LENGTH_INTERVALS; - dtmInfo.heightShadingInfo.addHeightColorMapEntry(0.0f, float32_t4(0.0f, 0.0f, 1.0f, animatedAlpha)); - dtmInfo.heightShadingInfo.addHeightColorMapEntry(25.0f, float32_t4(0.0f, 1.0f, 1.0f, animatedAlpha)); - dtmInfo.heightShadingInfo.addHeightColorMapEntry(50.0f, float32_t4(0.0f, 1.0f, 0.0f, animatedAlpha)); - dtmInfo.heightShadingInfo.addHeightColorMapEntry(75.0f, float32_t4(1.0f, 1.0f, 0.0f, animatedAlpha)); - dtmInfo.heightShadingInfo.addHeightColorMapEntry(100.0f, float32_t4(1.0f, 0.0f, 0.0f, animatedAlpha)); - - break; - } - case E_HEIGHT_SHADING_MODE::CONTINOUS_INTERVALS: - { - dtmInfo.heightShadingInfo.heightShadingMode = E_HEIGHT_SHADING_MODE::CONTINOUS_INTERVALS; - dtmInfo.heightShadingInfo.addHeightColorMapEntry(0.0f, float32_t4(0.0f, 0.0f, 1.0f, animatedAlpha)); - dtmInfo.heightShadingInfo.addHeightColorMapEntry(25.0f, float32_t4(0.0f, 1.0f, 1.0f, animatedAlpha)); - dtmInfo.heightShadingInfo.addHeightColorMapEntry(50.0f, float32_t4(0.0f, 1.0f, 0.0f, animatedAlpha)); - dtmInfo.heightShadingInfo.addHeightColorMapEntry(75.0f, float32_t4(1.0f, 1.0f, 0.0f, animatedAlpha)); - dtmInfo.heightShadingInfo.addHeightColorMapEntry(90.0f, float32_t4(1.0f, 0.0f, 0.0f, animatedAlpha)); - - break; - } - } - - drawResourcesFiller.drawTriangleMesh(mesh, dtmInfo, intendedNextSubmit); - - dtmInfo.contourSettings[0u].lineStyleInfo.color = float32_t4(1.0f, 0.39f, 0.0f, 1.0f); - dtmInfo.outlineStyleInfo.color = float32_t4(0.0f, 0.39f, 1.0f, 1.0f); - for (auto& v : mesh.m_vertices) - { - v.pos += float64_t2(450.0, 200.0); - v.height -= 10.0; - } - - drawResourcesFiller.drawTriangleMesh(mesh, dtmInfo, intendedNextSubmit); - } - else if (mode == ExampleMode::CASE_10) - { - CPolyline polyline; - - LineStyleInfo style = {}; - style.screenSpaceLineWidth = 4.0f; - style.color = float32_t4(0.619f, 0.325f, 0.709f, 0.5f); - - for (uint32_t i = 0; i < 128u; ++i) - { - std::vector> quadBeziers; - curves::EllipticalArcInfo myCircle; - { - myCircle.majorAxis = { 0.05 , 0.0}; - myCircle.center = { 0.0 + i * 0.1, i * 0.1 }; - myCircle.angleBounds = { - nbl::core::PI() * 0.0, - nbl::core::PI() * 2.0 - }; - myCircle.eccentricity = 1.0; - } - - curves::Subdivision::AddBezierFunc addToBezier = [&](shapes::QuadraticBezier&& info) -> void - { - quadBeziers.push_back(info); - }; - - curves::Subdivision::adaptive(myCircle, 1e-5, addToBezier, 10u); - polyline.addQuadBeziers(quadBeziers); - // drawResourcesFiller.drawPolyline(polyline, style, intendedNextSubmit); - polyline.clearEverything(); - } - - // Testing Fixed Geometry - { - float64_t2 line0[2u] = - { - float64_t2(-1.0, 0.0), - float64_t2(+1.0, 0.0), - }; - float64_t2 line1[3u] = - { - float64_t2(0.0, -1.0), - float64_t2(0.0, +1.0), - float64_t2(+1.0, +1.0), - }; - - float64_t3x3 translateMat = - { - 1.0, 0.0, 0.0, - 0.0, 1.0, 0.0, - 0.0, 0.0, 1.0 - }; - - float64_t angle = m_timeElapsed * 0.001; - float64_t2 dir = float64_t2{ cos(angle), sin(angle) }; - float64_t3x3 rotateMat = - { - dir.x, -dir.y, 0.0, - dir.y, dir.x, 0.0, - 0.0, 0.0, 1.0 - }; - - float64_t2 scale = float64_t2{ 100.0, 100.0 }; - float64_t3x3 scaleMat = - { - scale.x, 0.0, 0.0, - 0.0, scale.y, 0.0, - 0.0, 0.0, 1.0 - }; - - float64_t3x3 transformation = nbl::hlsl::mul(translateMat, nbl::hlsl::mul(rotateMat, scaleMat)); - polyline.addLinePoints(line0); - polyline.addLinePoints(line1); - polyline.preprocessPolylineWithStyle(style); - // drawResourcesFiller.drawPolyline(polyline, intendedNextSubmit); - drawResourcesFiller.drawFixedGeometryPolyline(polyline, style, transformation, TransformationType::TT_FIXED_SCREENSPACE_SIZE, intendedNextSubmit); - } - } - else if (mode == ExampleMode::CASE_11) - { - DTMSettingsInfo dtmInfo{}; - dtmInfo.mode |= E_DTM_MODE::OUTLINE; - dtmInfo.mode |= E_DTM_MODE::HEIGHT_SHADING; - dtmInfo.mode |= E_DTM_MODE::CONTOUR; - - dtmInfo.outlineStyleInfo.screenSpaceLineWidth = 0.0f; - dtmInfo.outlineStyleInfo.worldSpaceLineWidth = 1.0f; - dtmInfo.outlineStyleInfo.color = float32_t4(0.0f, 0.39f, 0.0f, 1.0f); - //std::array outlineStipplePattern = { 0.0f, -5.0f, 20.0f, -5.0f }; - std::array outlineStipplePattern = { -10.0f, 10.0f }; - dtmInfo.outlineStyleInfo.setStipplePatternData(outlineStipplePattern); - - dtmInfo.contourSettingsCount = 2u; - dtmInfo.contourSettings[0u].startHeight = 20; - dtmInfo.contourSettings[0u].endHeight = 90; - dtmInfo.contourSettings[0u].heightInterval = 10; - dtmInfo.contourSettings[0u].lineStyleInfo.screenSpaceLineWidth = 0.0f; - dtmInfo.contourSettings[0u].lineStyleInfo.worldSpaceLineWidth = 3.0f; - dtmInfo.contourSettings[0u].lineStyleInfo.color = float32_t4(0.0f, 0.0f, 1.0f, 0.7f); - std::array contourStipplePattern = { 0.0f, -5.0f, 10.0f, -5.0f }; - dtmInfo.contourSettings[0u].lineStyleInfo.setStipplePatternData(contourStipplePattern); - - dtmInfo.contourSettings[1u] = dtmInfo.contourSettings[0u]; - dtmInfo.contourSettings[1u].startHeight += 5.0f; - dtmInfo.contourSettings[1u].heightInterval = 13.0f; - dtmInfo.contourSettings[1u].lineStyleInfo.color = float32_t4(0.8f, 0.4f, 0.3f, 1.0f); - - // PRESS 1, 2, 3 TO SWITCH HEIGHT SHADING MODE - // 1 - DISCRETE_VARIABLE_LENGTH_INTERVALS - // 2 - DISCRETE_FIXED_LENGTH_INTERVALS - // 3 - CONTINOUS_INTERVALS - float animatedAlpha = (std::cos(m_timeElapsed * 0.0005) + 1.0) * 0.5; - animatedAlpha = 1.0f; - switch (m_shadingModeExample) - { - case E_HEIGHT_SHADING_MODE::DISCRETE_VARIABLE_LENGTH_INTERVALS: - { - dtmInfo.heightShadingInfo.heightShadingMode = E_HEIGHT_SHADING_MODE::DISCRETE_VARIABLE_LENGTH_INTERVALS; - - dtmInfo.heightShadingInfo.addHeightColorMapEntry(-10.0f, float32_t4(0.5f, 1.0f, 1.0f, 1.0f)); - dtmInfo.heightShadingInfo.addHeightColorMapEntry(20.0f, float32_t4(0.0f, 1.0f, 0.0f, 1.0f)); - dtmInfo.heightShadingInfo.addHeightColorMapEntry(25.0f, float32_t4(1.0f, 1.0f, 0.0f, animatedAlpha)); - dtmInfo.heightShadingInfo.addHeightColorMapEntry(70.0f, float32_t4(1.0f, 0.0f, 0.0f, 1.0f)); - dtmInfo.heightShadingInfo.addHeightColorMapEntry(90.0f, float32_t4(1.0f, 0.0f, 0.0f, 1.0f)); - - break; - } - case E_HEIGHT_SHADING_MODE::DISCRETE_FIXED_LENGTH_INTERVALS: - { - dtmInfo.heightShadingInfo.intervalLength = 10.0f; - dtmInfo.heightShadingInfo.intervalIndexToHeightMultiplier = dtmInfo.heightShadingInfo.intervalLength; - dtmInfo.heightShadingInfo.isCenteredShading = false; - dtmInfo.heightShadingInfo.heightShadingMode = E_HEIGHT_SHADING_MODE::DISCRETE_FIXED_LENGTH_INTERVALS; - dtmInfo.heightShadingInfo.addHeightColorMapEntry(-20.0f, float32_t4(0.0f, 0.5f, 0.0f, animatedAlpha)); - dtmInfo.heightShadingInfo.addHeightColorMapEntry(25.0f, float32_t4(0.0f, 0.7f, 0.0f, animatedAlpha)); - dtmInfo.heightShadingInfo.addHeightColorMapEntry(50.0f, float32_t4(0.0f, 1.0f, 0.0f, animatedAlpha)); - dtmInfo.heightShadingInfo.addHeightColorMapEntry(75.0f, float32_t4(1.0f, 1.0f, 0.0f, animatedAlpha)); - dtmInfo.heightShadingInfo.addHeightColorMapEntry(100.0f, float32_t4(1.0f, 0.0f, 0.0f, animatedAlpha)); - - break; - } - case E_HEIGHT_SHADING_MODE::CONTINOUS_INTERVALS: - { - dtmInfo.heightShadingInfo.heightShadingMode = E_HEIGHT_SHADING_MODE::CONTINOUS_INTERVALS; - dtmInfo.heightShadingInfo.addHeightColorMapEntry(0.0f, float32_t4(0.0f, 0.0f, 1.0f, animatedAlpha)); - dtmInfo.heightShadingInfo.addHeightColorMapEntry(25.0f, float32_t4(0.0f, 1.0f, 1.0f, animatedAlpha)); - dtmInfo.heightShadingInfo.addHeightColorMapEntry(50.0f, float32_t4(0.0f, 1.0f, 0.0f, animatedAlpha)); - dtmInfo.heightShadingInfo.addHeightColorMapEntry(75.0f, float32_t4(1.0f, 1.0f, 0.0f, animatedAlpha)); - dtmInfo.heightShadingInfo.addHeightColorMapEntry(90.0f, float32_t4(1.0f, 0.0f, 0.0f, animatedAlpha)); - - break; - } - } - - constexpr float HeightMapCellWidth = 20.0f; - const auto heightMapExtent = gridDTMHeightMap->getCreationParameters().extent; - assert(heightMapExtent.width > 0 && heightMapExtent.height > 0); - - float64_t2 worldSpaceExtents; - const float64_t2 topLeft = { -400.0f, 400.0f }; - worldSpaceExtents.x = (heightMapExtent.width - 1) * HeightMapCellWidth; - worldSpaceExtents.y = (heightMapExtent.height - 1) * HeightMapCellWidth; - const uint64_t heightMapTextureID = 0ull; - - constexpr bool DrawGridOnly = false; - - if(DrawGridOnly) - { - dtmInfo.mode = E_DTM_MODE::OUTLINE; - drawResourcesFiller.drawGridDTM(topLeft, worldSpaceExtents, HeightMapCellWidth, InvalidTextureIndex, dtmInfo, intendedNextSubmit); - } - else - { - StaticImageInfo heightMapStaticImageInfo = { - .imageID = heightMapTextureID, - .cpuImage = gridDTMHeightMap, - .forceUpdate = false, - .imageViewFormatOverride = asset::E_FORMAT::EF_R32G32B32A32_UINT // for now we use only R32G32B32A32_* anyway - }; - - if (!drawResourcesFiller.ensureStaticImageAvailability(heightMapStaticImageInfo, intendedNextSubmit)) - m_logger->log("Grid DTM height map texture unavailable!", ILogger::ELL_ERROR); - drawResourcesFiller.drawGridDTM(topLeft, worldSpaceExtents, HeightMapCellWidth, heightMapTextureID, dtmInfo, intendedNextSubmit); - } - - // draw test polyline -#if 0 - { - LineStyleInfo style = {}; - style.screenSpaceLineWidth = 0.0f; - style.worldSpaceLineWidth = 15.0f; - style.color = float32_t4(0.7f, 0.3f, 0.1f, 0.5f); - - CPolyline polyline; - { - std::vector linePoints; - linePoints.push_back(topLeft); - linePoints.push_back(topLeft + float64_t2(worldSpaceExtents.x, 0.0)); - linePoints.push_back(topLeft + float64_t2(worldSpaceExtents.x, -worldSpaceExtents.y)); - linePoints.push_back(topLeft + float64_t2(0.0, -worldSpaceExtents.y)); - linePoints.push_back(topLeft); - polyline.addLinePoints(linePoints); - } - - drawResourcesFiller.drawPolyline(polyline, style, intendedNextSubmit); - } -#endif - } - else if (mode == ExampleMode::CASE_12) - { - // [TODO]: Use streamedImagesGraphicsPipeline which is underblended, it requires: - // 1. Only queuing draws here to be rendered(underblended) later. (same as n4ce's Nabla Renderer) - // 2. EndFrame should do some special checks, then call these drawResourcesFiller functions (launchLoad -> draw -> finalize) after everything else was rendered. - // 3. Basically another submit at the end for underblending the georeferenced images using it's own dedicated pipeline - // 4. since CASE_12 only has images, underblending or overblending doesn't matter and VirtualTexturing might make a lot of these efforts useless - // 5. so let's just render the georeferenced images normally (over blend on bg) here for now - - // placeholder, actual path is right now hardcoded into the loader - const static std::string georeferencedImagePath = "../../media/tiled_grid_mip_0.exr"; - - constexpr float64_t3 topLeftViewportH = float64_t3(-1.0, -1.0, 1.0); - constexpr float64_t3 topRightViewportH = float64_t3(1.0, -1.0, 1.0); - constexpr float64_t3 bottomLeftViewportH = float64_t3(-1.0, 1.0, 1.0); - constexpr float64_t3 bottomRightViewportH = float64_t3(1.0, 1.0, 1.0); - - //GeoreferencedImageParams georeferencedImageParams; - //georeferencedImageParams.storagePath = georeferencedImagePath; - //georeferencedImageParams.format = drawResourcesFiller.queryGeoreferencedImageFormat(georeferencedImagePath); - //georeferencedImageParams.imageExtents = drawResourcesFiller.queryGeoreferencedImageExtents(georeferencedImagePath); - - image_id georefImageID = 6996; - // Position at topLeft viewport - auto projectionToNDC = m_Camera.constructViewProjection(); - // TEST CAMERA ROTATION - if constexpr (testCameraRotation) - projectionToNDC = rotateBasedOnTime(projectionToNDC); - auto inverseViewProj = nbl::hlsl::inverse(projectionToNDC); - - // Get 1 viewport pixel to match `startingImagePixelsPerViewportPixel` pixels of the image by choosing appropriate dirU - const static float64_t startingImagePixelsPerViewportPixels = 1.0; - const static auto startingViewportWidthVector = nbl::hlsl::mul(inverseViewProj, topRightViewportH - topLeftViewportH); - const static auto dirU = startingViewportWidthVector * float64_t(drawResourcesFiller.queryGeoreferencedImageExtents(georeferencedImagePath).x) / float64_t(startingImagePixelsPerViewportPixels * m_window->getWidth()); - - const static auto startingTopLeft = nbl::hlsl::mul(inverseViewProj, topLeftViewportH); - const uint32_t2 imageExtents = drawResourcesFiller.queryGeoreferencedImageExtents(georeferencedImagePath); - OrientedBoundingBox2D georefImageBB = { .topLeft = startingTopLeft, .dirU = dirU, .aspectRatio = float32_t(imageExtents.y) / imageExtents.x }; - - auto streamingState = drawResourcesFiller.ensureGeoreferencedImageEntry(georefImageID, georefImageBB, uint32_t2(m_window->getWidth(), m_window->getHeight()), inverseViewProj, georeferencedImagePath); - constexpr static WorldClipRect invalidClipRect = { .minClip = float64_t2(std::numeric_limits::signaling_NaN()) }; - drawResourcesFiller.launchGeoreferencedImageTileLoads(georefImageID, streamingState.get(), invalidClipRect); - - drawResourcesFiller.drawGeoreferencedImage(georefImageID, std::move(streamingState), intendedNextSubmit); - - drawResourcesFiller.finalizeGeoreferencedImageTileLoads(intendedNextSubmit); - - //drawResourcesFiller.ensureGeoreferencedImageAvailability_AllocateIfNeeded(georefImageID, std::move(georeferencedImageParams), intendedNextSubmit); - - //drawResourcesFiller.addGeoreferencedImage(georefImageID, inverseViewProj, intendedNextSubmit); - } + drawResourcesFiller.finalizeAllCopiesToGPU(intendedNextSubmit); } double getScreenToWorldRatio(const float64_t3x3& viewProjectionMatrix, uint32_t2 windowSize) { double idx_0_0 = viewProjectionMatrix[0u][0u] * (windowSize.x / 2.0); - double idx_1_0 = viewProjectionMatrix[1u][0u] * (windowSize.y / 2.0); - return hlsl::length(float64_t2(idx_0_0, idx_1_0)); - } - - float64_t3x3 rotateBasedOnTime(const float64_t3x3& projectionMatrix) - { - double rotation = abs(cos(m_timeElapsed * 0.0004)) * 0.25 * PI(); - float64_t2 rotationVec = float64_t2(cos(rotation), sin(rotation)); - float64_t3x3 rotationParameter = float64_t3x3{ - rotationVec.x, rotationVec.y, 0.0, - -rotationVec.y, rotationVec.x, 0.0, - 0.0, 0.0, 1.0 - }; - return nbl::hlsl::mul(projectionMatrix, rotationParameter); + double idx_1_1 = viewProjectionMatrix[1u][1u] * (windowSize.y / 2.0); + double det_2x2_mat = idx_0_0 * idx_1_1; + return static_cast(core::sqrt(core::abs(det_2x2_mat))); } protected: - clock_t::time_point start; std::chrono::seconds timeout = std::chrono::seconds(0x7fffFFFFu); - - double m_timeElapsed = 0.0; - std::chrono::steady_clock::time_point lastTime; - - std::vector> replayCaches = {}; // vector because there can be overflow submits - bool finishedCachingDraw = false; + clock_t::time_point start; bool fragmentShaderInterlockEnabled = false; - core::smart_refctd_ptr m_inputSystem; - nbl::examples::InputSystem::ChannelReader mouse; - nbl::examples::InputSystem::ChannelReader keyboard; + core::smart_refctd_ptr m_inputSystem; + InputSystem::ChannelReader mouse; + InputSystem::ChannelReader keyboard; smart_refctd_ptr renderpassInitial; // this renderpass will clear the attachment and transition it to COLOR_ATTACHMENT_OPTIMAL smart_refctd_ptr renderpassInBetween; // this renderpass will load the attachment and transition it to COLOR_ATTACHMENT_OPTIMAL smart_refctd_ptr renderpassFinal; // this renderpass will load the attachment and transition it to PRESENT - smart_refctd_ptr m_graphicsCommandPool; - std::array, MaxSubmitsInFlight> m_commandBuffersInFlight; - // ref to above cmd buffers, these go into SIntendedSubmitInfo as command buffers available for recording. - std::array m_commandBufferInfos; - // pointer to one of the command buffer infos from above, this is the only command buffer used to record current submit in current frame, it will be updated by SIntendedSubmitInfo - IQueue::SSubmitInfo::SCommandBufferInfo const * m_currentRecordingCommandBufferInfo; // pointer can change, value cannot - - smart_refctd_ptr msdfImageSampler; - smart_refctd_ptr staticImageSampler; + std::array, MaxFramesInFlight> m_graphicsCommandPools; + std::array, MaxFramesInFlight> m_commandBuffers; + + smart_refctd_ptr msdfTextureSampler; - smart_refctd_ptr m_globalsBuffer; + smart_refctd_ptr globalsBuffer; smart_refctd_ptr descriptorSet0; smart_refctd_ptr descriptorSet1; DrawResourcesFiller drawResourcesFiller; // you can think of this as the scene data needed to draw everything, we only have one instance so let's use a timeline semaphore to sync all renders @@ -3999,15 +3201,16 @@ class ComputerAidedDesign final : public nbl::examples::SimpleWindowedApplicatio ISimpleManagedSurface::SAcquireResult m_currentImageAcquire = {}; - uint64_t m_realFrameIx = 0u; + uint64_t m_realFrameIx : 59 = 0; + // Maximum frames which can be simultaneously rendered + uint64_t m_framesInFlight : 5; + smart_refctd_ptr debugGraphicsPipeline; smart_refctd_ptr descriptorSetLayout0; smart_refctd_ptr descriptorSetLayout1; - smart_refctd_ptr m_pipelineLayout; + smart_refctd_ptr pipelineLayout; smart_refctd_ptr resolveAlphaGraphicsPipeline; - smart_refctd_ptr m_debugGraphicsPipeline; - smart_refctd_ptr m_graphicsPipeline; - smart_refctd_ptr m_streamedImagesGraphicsPipeline; + smart_refctd_ptr graphicsPipeline; Camera2D m_Camera; @@ -4016,14 +3219,13 @@ class ComputerAidedDesign final : public nbl::examples::SimpleWindowedApplicatio smart_refctd_ptr pseudoStencilImageView; smart_refctd_ptr colorStorageImageView; smart_refctd_ptr m_textRenderer; - smart_refctd_ptr m_font; + smart_refctd_ptr m_arialFont; + smart_refctd_ptr m_webdingsFont; std::unique_ptr singleLineText = nullptr; + std::unique_ptr webdingsSquareText = nullptr; std::vector> m_shapeMSDFImages = {}; - std::vector> sampleImages; - smart_refctd_ptr gridDTMHeightMap; - static constexpr char FirstGeneratedCharacter = ' '; static constexpr char LastGeneratedCharacter = '~'; @@ -4033,11 +3235,6 @@ class ComputerAidedDesign final : public nbl::examples::SimpleWindowedApplicatio const std::chrono::steady_clock::time_point startBenchmark = std::chrono::high_resolution_clock::now(); bool stopBenchamrkFlag = false; #endif - - // Example Specific Settings: - uint32_t m_hatchDebugStep = 0u; // setting for CASE_2 - E_HEIGHT_SHADING_MODE m_shadingModeExample = E_HEIGHT_SHADING_MODE::DISCRETE_VARIABLE_LENGTH_INTERVALS; // setting for CASE_11 & CASE_9 }; NBL_MAIN_FUNC(ComputerAidedDesign) - diff --git a/62_CAD/resolve_alphas.hlsl b/62_CAD/resolve_alphas.hlsl new file mode 100644 index 000000000..087f49235 --- /dev/null +++ b/62_CAD/resolve_alphas.hlsl @@ -0,0 +1,47 @@ +#pragma shader_stage(fragment) + +#include "common.hlsl" +#include +#include + +template +float32_t4 calculateFinalColor(const uint2 fragCoord); + + +template<> +float32_t4 calculateFinalColor(const uint2 fragCoord) +{ + return float4(0.0f, 0.0f, 0.0f, 0.0f); +} + +template<> +float32_t4 calculateFinalColor(const uint2 fragCoord) +{ + float32_t4 color; + + nbl::hlsl::spirv::execution_mode::PixelInterlockOrderedEXT(); + nbl::hlsl::spirv::beginInvocationInterlockEXT(); + + const uint32_t packedData = pseudoStencil[fragCoord]; + const uint32_t storedQuantizedAlpha = nbl::hlsl::glsl::bitfieldExtract(packedData,0,AlphaBits); + const uint32_t storedMainObjectIdx = nbl::hlsl::glsl::bitfieldExtract(packedData,AlphaBits,MainObjectIdxBits); + pseudoStencil[fragCoord] = nbl::hlsl::glsl::bitfieldInsert(0, InvalidMainObjectIdx, AlphaBits, MainObjectIdxBits); + + uint32_t resolveStyleIdx = mainObjects[storedMainObjectIdx].styleIdx; + const bool resolveColorFromStyle = resolveStyleIdx != InvalidStyleIdx; + if (!resolveColorFromStyle) + color = float32_t4(unpackR11G11B10_UNORM(colorStorage[fragCoord]), 1.0f); + + nbl::hlsl::spirv::endInvocationInterlockEXT(); + + if (resolveColorFromStyle) + color = lineStyles[resolveStyleIdx].color; + color.a *= float(storedQuantizedAlpha) / 255.f; + + return color; +} + +float4 main(float4 position : SV_Position) : SV_TARGET +{ + return calculateFinalColor(position.xy); +} diff --git a/62_CAD/resolve_alphas.hlsl.orig b/62_CAD/resolve_alphas.hlsl.orig new file mode 100644 index 000000000..615e3940e --- /dev/null +++ b/62_CAD/resolve_alphas.hlsl.orig @@ -0,0 +1,37 @@ +#pragma shader_stage(fragment) + +#include "common.hlsl" + +#if defined(NBL_FEATURE_FRAGMENT_SHADER_PIXEL_INTERLOCK) +[[vk::ext_instruction(/* OpBeginInvocationInterlockEXT */ 5364)]] +void beginInvocationInterlockEXT(); +[[vk::ext_instruction(/* OpEndInvocationInterlockEXT */ 5365)]] +void endInvocationInterlockEXT(); +#endif + +float4 main(float4 position : SV_Position) : SV_TARGET +{ +#if defined(NBL_FEATURE_FRAGMENT_SHADER_PIXEL_INTERLOCK) + [[vk::ext_capability(/*FragmentShaderPixelInterlockEXT*/ 5378)]] + [[vk::ext_extension("SPV_EXT_fragment_shader_interlock")]] + vk::ext_execution_mode(/*PixelInterlockOrderedEXT*/ 5366); +#endif + + uint2 fragCoord = uint2(position.xy); + +#if defined(NBL_FEATURE_FRAGMENT_SHADER_PIXEL_INTERLOCK) + beginInvocationInterlockEXT(); + const uint packedData = pseudoStencil[fragCoord]; + pseudoStencil[fragCoord] = bitfieldInsert(0, InvalidMainObjectIdx, AlphaBits, MainObjectIdxBits); + endInvocationInterlockEXT(); + + const uint quantizedAlpha = bitfieldExtract(packedData,0,AlphaBits); + const uint mainObjectIdx = bitfieldExtract(packedData,AlphaBits,MainObjectIdxBits); + // draw with previous geometry's style :kek: + float4 color = lineStyles[mainObjects[mainObjectIdx].styleIdx].color; + color.a *= float(quantizedAlpha)/255.f; + return color; +#else + return float4(0.0f, 0.0f, 0.0f, 0.0f); +#endif +} \ No newline at end of file diff --git a/62_CAD/scripts/generate_mipmaps.py b/62_CAD/scripts/generate_mipmaps.py deleted file mode 100644 index 78420cda5..000000000 --- a/62_CAD/scripts/generate_mipmaps.py +++ /dev/null @@ -1,47 +0,0 @@ -import OpenEXR -import Imath -import numpy as np - -def read_exr(path): - exr = OpenEXR.InputFile(path) - dw = exr.header()['dataWindow'] - size = (dw.max.x - dw.min.x + 1, dw.max.y - dw.min.y + 1) - - pt = Imath.PixelType(Imath.PixelType.FLOAT) - channels = ['R', 'G', 'B'] - data = [np.frombuffer(exr.channel(c, pt), dtype=np.float32).reshape(size[1], size[0]) for c in channels] - return np.stack(data, axis=-1) # shape: (H, W, 3) - -def write_exr(path, arr): - H, W, C = arr.shape - assert C == 3, "Only RGB supported" - header = OpenEXR.Header(W, H) - pt = Imath.PixelType(Imath.PixelType.FLOAT) - channels = { - 'R': arr[:, :, 0].astype(np.float32).tobytes(), - 'G': arr[:, :, 1].astype(np.float32).tobytes(), - 'B': arr[:, :, 2].astype(np.float32).tobytes() - } - exr = OpenEXR.OutputFile(path, header) - exr.writePixels(channels) - -def mipmap_exr(): - img = read_exr("../../media/tiled_grid_mip_0.exr") - h, w, _ = img.shape - base_path = "../../media/tiled_grid_mip_" - tile_size = 128 - mip_level = 1 - tile_length = h // (2 * tile_size) - - while tile_length > 0: - # Reshape and average 2x2 blocks - reshaped = img.reshape(h//2, 2, w//2, 2, 3) - mipmap = reshaped.mean(axis=(1, 3)) - write_exr(base_path + str(mip_level) + ".exr", mipmap) - img = mipmap - mip_level = mip_level + 1 - tile_length = tile_length // 2 - h = h // 2 - w = w // 2 - -mipmap_exr() \ No newline at end of file diff --git a/62_CAD/scripts/tiled_grid.py b/62_CAD/scripts/tiled_grid.py deleted file mode 100644 index 89c637338..000000000 --- a/62_CAD/scripts/tiled_grid.py +++ /dev/null @@ -1,266 +0,0 @@ -from PIL import Image, ImageDraw, ImageFont -import numpy as np -import os -import OpenImageIO as oiio - - - -def create_single_tile(tile_size, color, x_coord, y_coord, font_path=None): - """ - Creates a single square tile image with a given color and two lines of centered text. - - Args: - tile_size (int): The sidelength of the square tile in pixels. - color (tuple): A tuple of three floats (R, G, B) representing the color (0.0-1.0). - x_coord (int): The X coordinate to display on the tile. - y_coord (int): The Y coordinate to display on the tile. - font_path (str, optional): The path to a TrueType font file (.ttf). - If None, a default PIL font will be used. - Returns: - PIL.Image.Image: The created tile image with text. - """ - # Convert float color (0.0-1.0) to 8-bit integer color (0-255) - int_color = tuple(int(max(0, min(1, c)) * 255) for c in color) # Ensure color components are clamped - - img = Image.new('RGB', (tile_size, tile_size), int_color) - draw = ImageDraw.Draw(img) - - text_line1 = f"x = {x_coord}" - text_line2 = f"y = {y_coord}" - - text_fill_color = (255, 255, 255) - - # --- Dynamic Font Size Adjustment --- - # Start with a relatively large font size and shrink if needed - font_size = int(tile_size * 0.25) # Initial guess for font size - max_font_size = int(tile_size * 0.25) # Don't exceed this - - font = None - max_iterations = 100 # Prevent infinite loops in font size reduction - - for _ in range(max_iterations): - current_font_path = font_path - current_font_size = max(1, font_size) # Ensure font size is at least 1 - - try: - if current_font_path and os.path.exists(current_font_path): - font = ImageFont.truetype(current_font_path, current_font_size) - else: - # Fallback to default font (size argument might not always work perfectly) - font = ImageFont.load_default() - # For default font, try to scale if load_default(size=...) is supported and works - try: - scaled_font = ImageFont.load_default(size=current_font_size) - if draw.textbbox((0, 0), text_line1, font=scaled_font)[2] > 0: # Check if usable - font = scaled_font - except Exception: - pass # Stick with original default font - - if font is None: # Last resort if no font could be loaded - font = ImageFont.load_default() - - # Measure text dimensions - bbox1 = draw.textbbox((0, 0), text_line1, font=font) - text_width1 = bbox1[2] - bbox1[0] - text_height1 = bbox1[3] - bbox1[1] - - bbox2 = draw.textbbox((0, 0), text_line2, font=font) - text_width2 = bbox2[2] - bbox2[0] - text_height2 = bbox2[3] - bbox2[1] - - # Calculate total height needed for both lines plus some padding - # Let's assume a small gap between lines (e.g., 0.1 * text_height) - line_gap = int(text_height1 * 0.2) # 20% of line height - total_text_height = text_height1 + text_height2 + line_gap - - # Check if text fits vertically and horizontally - if (total_text_height < tile_size * 0.9) and \ - (text_width1 < tile_size * 0.9) and \ - (text_width2 < tile_size * 0.9): - break # Font size is good, break out of loop - else: - font_size -= 1 # Reduce font size - if font_size <= 0: # Prevent infinite loop if text can never fit - font_size = 1 # Smallest possible font size - break - - except Exception as e: - # Handle cases where font loading or textbbox fails - print(f"Error during font sizing: {e}. Reducing font size and retrying.") - font_size -= 1 - if font_size <= 0: - font_size = 1 - break # Cannot make font smaller, stop - - # Final check: if font_size became 0 or less, ensure it's at least 1 - if font_size <= 0: - font_size = 1 - # Reload font with minimum size if needed - if font_path and os.path.exists(font_path): - font = ImageFont.truetype(font_path, font_size) - else: - font = ImageFont.load_default() - try: - scaled_font = ImageFont.load_default(size=font_size) - if draw.textbbox((0, 0), text_line1, font=scaled_font)[2] > 0: - font = scaled_font - except Exception: - pass - - - # Re-measure with final font size to ensure accurate positioning - bbox1 = draw.textbbox((0, 0), text_line1, font=font) - text_width1 = bbox1[2] - bbox1[0] - text_height1 = bbox1[3] - bbox1[1] - - bbox2 = draw.textbbox((0, 0), text_line2, font=font) - text_width2 = bbox2[2] - bbox2[0] - text_height2 = bbox2[3] - bbox2[1] - - # Calculate positions for centering - # Line 1: centered horizontally, midpoint at 1/3 tile height - x1 = (tile_size - text_width1) / 2 - y1 = (tile_size / 3) - (text_height1 / 2) - - # Line 2: centered horizontally, midpoint at 2/3 tile height - x2 = (tile_size - text_width2) / 2 - y2 = (tile_size * 2 / 3) - (text_height2 / 2) - - # Draw the text - draw.text((x1, y1), text_line1, fill=text_fill_color, font=font) - draw.text((x2, y2), text_line2, fill=text_fill_color, font=font) - - return img - -def generate_interpolated_grid_image(tile_size, count, font_path=None): - """ - Generates a large image composed of 'count' x 'count' tiles, - with colors bilinearly interpolated from corners and text indicating tile index. - - Args: - tile_size (int): The sidelength of each individual square tile in pixels. - count (int): The number of tiles per side of the large grid (e.g., if count=3, - it's a 3x3 grid of tiles). - font_path (str, optional): Path to a TrueType font file for the tile text. - If None, a default PIL font will be used. - - Returns: - PIL.Image.Image: The generated large grid image. - """ - if count <= 0: - raise ValueError("Count must be a positive integer.") - - total_image_size = count * tile_size - main_img = Image.new('RGB', (total_image_size, total_image_size)) - - # Corner colors (R, G, B) as floats (0.0-1.0) - corner_colors = { - "top_left": (1.0, 0.0, 0.0), # Red - "top_right": (1.0, 0.0, 1.0), # Purple - "bottom_left": (0.0, 1.0, 0.0), # Green - "bottom_right": (0.0, 0.0, 1.0) # Blue - } - - # Handle the edge case where count is 1 - if count == 1: - # If count is 1, there's only one tile, which is the top-left corner - tile_color = corner_colors["top_left"] - tile_image = create_single_tile(tile_size, tile_color, 0, 0, font_path=font_path) - main_img.paste(tile_image, (0, 0)) - return main_img - - for y_tile in range(count): - for x_tile in range(count): - # Calculate normalized coordinates (u, v) for interpolation - # We divide by (count - 1) to ensure 0 and 1 values at the edges - u = x_tile / (count - 1) - v = y_tile / (count - 1) - - # Apply the simplified bilinear interpolation formulas - r_component = 1 - v - g_component = v * (1 - u) - b_component = u - - # Clamp components to be within 0.0 and 1.0 (due to potential floating point inaccuracies) - current_color = ( - max(0.0, min(1.0, r_component)), - max(0.0, min(1.0, g_component)), - max(0.0, min(1.0, b_component)) - ) - - # Create the individual tile - tile_image = create_single_tile(tile_size, current_color, x_tile, y_tile, font_path=font_path) - - # Paste the tile onto the main image - paste_x = x_tile * tile_size - paste_y = y_tile * tile_size - main_img.paste(tile_image, (paste_x, paste_y)) - - return main_img - - - - -import argparse -parser = argparse.ArgumentParser(description="Process two optional named parameters.") -parser.add_argument('--ts', type=int, default=128, help='Tile Size') -parser.add_argument('--gs', type=int, default=128, help='Grid Size') - -# Parse the arguments -args = parser.parse_args() - - -# --- Configuration --- -tile_sidelength = args.ts # Size of each individual tile in pixels -grid_count = args.gs # Number of tiles per side (e.g., 15 means 15x15 grid) - -# Path to a font file (adjust this for your system) -# On Windows, you can typically use 'C:/Windows/Fonts/arial.ttf' or similar -# You might need to find a suitable font on your system. -# For testing, you can use None to let PIL use its default font. -# If a specific font path is provided and doesn't exist, it will fall back to default. -windows_font_path = "C:/Windows/Fonts/arial.ttf" # Example path for Windows -# If Arial is not found, try Times New Roman: -# windows_font_path = "C:/Windows/Fonts/times.ttf" - -font_to_use = None -if os.name == 'nt': # Check if OS is Windows - if os.path.exists(windows_font_path): - font_to_use = windows_font_path - print(f"Using font: {windows_font_path}") - else: - print(f"Warning: Windows font not found at '{windows_font_path}'. Using default PIL font.") -else: # Assume Linux/macOS for other OS types - # Common Linux/macOS font paths (adjust as needed) - linux_font_path = "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf" - mac_font_path = "/Library/Fonts/Arial.ttf" - if os.path.exists(linux_font_path): - font_to_use = linux_font_path - print(f"Using font: {linux_font_path}") - elif os.path.exists(mac_font_path): - font_to_use = mac_font_path - print(f"Using font: {mac_font_path}") - else: - print("Warning: No common Linux/macOS font found. Using default PIL font.") - - -# --- Generate and save the image --- -print(f"Generating a {grid_count}x{grid_count} grid of tiles, each {tile_sidelength}x{tile_sidelength} pixels.") -print(f"Total image size will be {grid_count * tile_sidelength}x{grid_count * tile_sidelength} pixels.") - -try: - final_image = generate_interpolated_grid_image(tile_sidelength, grid_count, font_path=font_to_use) - output_filename = "../../media/tiled_grid_mip_0.exr" - np_img = np.array(final_image).astype(np.float32) / 255.0 # Normalize for EXR - spec = oiio.ImageSpec(final_image.width, final_image.height, 3, oiio.TypeDesc("float")) - out = oiio.ImageOutput.create(output_filename) - out.open(output_filename, spec) - out.write_image(np_img.reshape(-1)) # Flatten for OIIO’s expected input - out.close() - - print(f"Successfully created '{output_filename}'") - -except ValueError as e: - print(f"Error: {e}") -except Exception as e: - print(f"An unexpected error occurred: {e}") \ No newline at end of file diff --git a/62_CAD/shaders/globals.hlsl b/62_CAD/shaders/globals.hlsl deleted file mode 100644 index 0d2c1190a..000000000 --- a/62_CAD/shaders/globals.hlsl +++ /dev/null @@ -1,633 +0,0 @@ -#ifndef _CAD_EXAMPLE_GLOBALS_HLSL_INCLUDED_ -#define _CAD_EXAMPLE_GLOBALS_HLSL_INCLUDED_ - -// TODO[Erfan]: Turn off in the future, but keep enabled to test -// #define NBL_FORCE_EMULATED_FLOAT_64 - -#include -#include -#include -#include -#include -#include -#include - -#ifdef __HLSL_VERSION -#include -#endif - -using namespace nbl::hlsl; - -#ifdef __HLSL_VERSION -using pfloat64_t = portable_float64_t; -using pfloat64_t2 = portable_float64_t2; -using pfloat64_t3 = portable_float64_t3; -#else -using pfloat64_t = float64_t; -using pfloat64_t2 = nbl::hlsl::vector; -using pfloat64_t3 = nbl::hlsl::vector; -#endif - -using pfloat64_t3x3 = portable_matrix_t3x3; - -struct PushConstants -{ - uint64_t triangleMeshVerticesBaseAddress; - uint32_t triangleMeshMainObjectIndex; - uint32_t isDTMRendering; -}; - -// Please note minClip.y > maxClip.y --> TODO[Erfan]: fix later, because I get confused everytime dealing with min/max clip stuff -struct WorldClipRect -{ - pfloat64_t2 minClip; // min clip of a rect in worldspace coordinates of the original space (globals.defaultProjectionToNDC) - pfloat64_t2 maxClip; // max clip of a rect in worldspace coordinates of the original space (globals.defaultProjectionToNDC) -}; - -struct Pointers -{ - uint64_t lineStyles; - uint64_t dtmSettings; - uint64_t customProjections; - uint64_t customClipRects; - uint64_t mainObjects; - uint64_t drawObjects; - uint64_t geometryBuffer; -}; -#ifndef __HLSL_VERSION -static_assert(sizeof(Pointers) == 56u); -#endif - -struct Globals -{ - Pointers pointers; - pfloat64_t3x3 defaultProjectionToNDC; - pfloat64_t3x3 screenToWorldScaleTransform; // Pre-multiply your transform with this to scale in screen space (e.g., scale 100.0 means 100 screen pixels). - uint32_t2 resolution; - float32_t antiAliasingFactor; - float32_t minLineWidth; // minimum line width in screenspace pixels (will clamp if it's lower) - float32_t minLineThicknessToEnableAA; // lines/curves with screenspace (pixel) widths lower than this will skip AA - uint32_t miterLimit; - uint32_t currentlyActiveMainObjectIndex; // for alpha resolve to skip resolving activeMainObjectIdx and prep it for next submit - uint32_t _padding; -}; -#ifndef __HLSL_VERSION -static_assert(sizeof(Globals) == 232u); -#endif - -#ifdef __HLSL_VERSION -pfloat64_t2 transformPointNdc(NBL_CONST_REF_ARG(pfloat64_t3x3) transformation, NBL_CONST_REF_ARG(pfloat64_t2) point2d) -{ - pfloat64_t3 point3d; - point3d.x = point2d.x; - point3d.y = point2d.y; - point3d.z = _static_cast(1.0f); - - pfloat64_t3 transformationResult = nbl::hlsl::mul(transformation, point3d); - - pfloat64_t2 output; - output.x = transformationResult.x; - output.y = transformationResult.y; - - return output; -} -pfloat64_t2 transformVectorNdc(NBL_CONST_REF_ARG(pfloat64_t3x3) transformation, NBL_CONST_REF_ARG(pfloat64_t2) vector2d) -{ - pfloat64_t3 vector3d; - vector3d.x = vector2d.x; - vector3d.y = vector2d.y; - vector3d.z = _static_cast(0.0f); - - pfloat64_t3 transformationResult = nbl::hlsl::mul(transformation, vector3d); - pfloat64_t2 output; - output.x = transformationResult.x; - output.y = transformationResult.y; - - return output; -} -#endif - -enum class MainObjectType : uint32_t -{ - NONE = 0u, - POLYLINE, - HATCH, - TEXT, - STATIC_IMAGE, - DTM, - GRID_DTM, - STREAMED_IMAGE, -}; - -enum class ObjectType : uint32_t -{ - LINE = 0u, - QUAD_BEZIER = 1u, - CURVE_BOX = 2u, - POLYLINE_CONNECTOR = 3u, - FONT_GLYPH = 4u, - STATIC_IMAGE = 5u, - TRIANGLE_MESH = 6u, - GRID_DTM = 7u, - STREAMED_IMAGE = 8u, -}; - -enum class MajorAxis : uint32_t -{ - MAJOR_X = 0u, - MAJOR_Y = 1u, -}; - -enum TransformationType -{ - TT_NORMAL = 0, - TT_FIXED_SCREENSPACE_SIZE -}; - - -// Consists of multiple DrawObjects -// [IDEA]: In GPU-driven rendering, to save mem for MainObject data fetching: many of these can be shared amongst different main objects, we could find these styles, settings, etc indices with upper_bound -// [TODO]: pack indices and members of mainObject and DrawObject + enforce max size for autosubmit --> but do it only after the mainobject definition is finalized in gpu-driven rendering work -struct MainObject -{ - uint32_t styleIdx; - uint32_t dtmSettingsIdx; - uint32_t customProjectionIndex; - uint32_t customClipRectIndex; - uint32_t transformationType; // todo pack later, it's just 2 possible values atm -}; - -struct DrawObject -{ - uint32_t type_subsectionIdx; // packed two uint16 into uint32 - uint32_t mainObjIndex; - uint64_t geometryAddress; -}; - -// Goes into geometry buffer, needs to be aligned by 8 -struct LinePointInfo -{ - pfloat64_t2 p; - float32_t phaseShift; - float32_t stretchValue; -}; - -// Goes into geometry buffer, needs to be aligned by 8 -struct QuadraticBezierInfo -{ - nbl::hlsl::shapes::QuadraticBezier shape; // 48bytes = 3 (control points) x 16 (float64_t2) - float32_t phaseShift; - float32_t stretchValue; -}; -#ifndef __HLSL_VERSION -static_assert(offsetof(QuadraticBezierInfo, phaseShift) == 48u); -#endif - -// Goes into geometry buffer, needs to be aligned by 8 -struct GlyphInfo -{ - pfloat64_t2 topLeft; // 2 * 8 = 16 bytes - float32_t2 dirU; // 2 * 4 = 8 bytes (24) - float32_t aspectRatio; // 4 bytes (32) - // unorm8 minU; - // unorm8 minV; - // uint16 textureId; - uint32_t minUV_textureID_packed; // 4 bytes (36) - -#ifndef __HLSL_VERSION - GlyphInfo(pfloat64_t2 topLeft, float32_t2 dirU, float32_t aspectRatio, uint16_t textureId, float32_t2 minUV) : - topLeft(topLeft), - dirU(dirU), - aspectRatio(aspectRatio) - { - assert(textureId < nbl::hlsl::numeric_limits::max); - packMinUV_TextureID(minUV, textureId); - } -#endif - - void packMinUV_TextureID(float32_t2 minUV, uint16_t textureId) - { - minUV_textureID_packed = textureId; - uint32_t uPacked = (uint32_t)(nbl::hlsl::clamp(minUV.x, 0.0f, 1.0f) * 255.0f); - uint32_t vPacked = (uint32_t)(nbl::hlsl::clamp(minUV.y, 0.0f, 1.0f) * 255.0f); - minUV_textureID_packed = nbl::hlsl::glsl::bitfieldInsert(minUV_textureID_packed, uPacked, 16, 8); - minUV_textureID_packed = nbl::hlsl::glsl::bitfieldInsert(minUV_textureID_packed, vPacked, 24, 8); - } - - float32_t2 getMinUV() - { - return float32_t2( - float32_t(nbl::hlsl::glsl::bitfieldExtract(minUV_textureID_packed, 16, 8)) / 255.0, - float32_t(nbl::hlsl::glsl::bitfieldExtract(minUV_textureID_packed, 24, 8)) / 255.0 - ); - } - - uint16_t getTextureID() - { - return uint16_t(nbl::hlsl::glsl::bitfieldExtract(minUV_textureID_packed, 0, 16)); - } -}; - -// Goes into geometry buffer, needs to be aligned by 8 -struct ImageObjectInfo -{ - pfloat64_t2 topLeft; // 2 * 8 = 16 bytes (16) - float32_t2 dirU; // 2 * 4 = 8 bytes (24) - float32_t aspectRatio; // 4 bytes (28) - uint32_t textureID; // 4 bytes (32) -}; - -// Goes into geometry buffer, needs to be aligned by 8 -// Currently a simple OBB like ImageObject, but later will be fullscreen with additional info about UV offset for toroidal(mirror) addressing -struct GeoreferencedImageInfo -{ - pfloat64_t2 topLeft; // 2 * 8 = 16 bytes (16) - float32_t2 dirU; // 2 * 4 = 8 bytes (24) - float32_t aspectRatio; // 4 bytes (28) - uint32_t textureID; // 4 bytes (32) - float32_t2 minUV; // 2 * 4 = 8 bytes (40) - float32_t2 maxUV; // 2 * 4 = 8 bytes (48) -}; - -// Goes into geometry buffer, needs to be aligned by 8 -struct GridDTMInfo -{ - pfloat64_t2 topLeft; // 2 * 8 = 16 bytes (16) - pfloat64_t2 worldSpaceExtents; // 16 bytes (32) - uint32_t textureID; // 4 bytes (36) - float gridCellWidth; // 4 bytes (40) - float thicknessOfTheThickestLine; // 4 bytes (44) - float _padding; // 4 bytes (48) -}; - -enum E_CELL_DIAGONAL : uint32_t -{ - TOP_LEFT_TO_BOTTOM_RIGHT = 0u, - BOTTOM_LEFT_TO_TOP_RIGHT = 1u, - INVALID = 2u -}; - -#ifndef __HLSL_VERSION - -// sets last bit of data to 1 or 0 depending on diagonalMode -static void setDiagonalModeBit(float* data, E_CELL_DIAGONAL diagonalMode) -{ - if (diagonalMode == E_CELL_DIAGONAL::INVALID) - return; - - uint32_t dataAsUint = reinterpret_cast(*data); - constexpr uint32_t HEIGHT_VALUE_MASK = 0xFFFFFFFEu; - dataAsUint &= HEIGHT_VALUE_MASK; - dataAsUint |= static_cast(diagonalMode); - *data = reinterpret_cast(dataAsUint); - - uint32_t dataAsUintDbg = reinterpret_cast(*data); -} - -#endif - -// Top left corner holds diagonal mode info of a cell -static E_CELL_DIAGONAL getDiagonalModeFromCellCornerData(uint32_t cellCornerData) -{ - return (cellCornerData & 0x1u) ? BOTTOM_LEFT_TO_TOP_RIGHT : TOP_LEFT_TO_BOTTOM_RIGHT; -} - -static uint32_t packR11G11B10_UNORM(float32_t3 color) -{ - // Scale and convert to integers - uint32_t r = (uint32_t)(nbl::hlsl::clamp(color.r, 0.0f, 1.0f) * 2047.0f + 0.5f); // 11 bits -> 2^11 - 1 = 2047 - uint32_t g = (uint32_t)(nbl::hlsl::clamp(color.g, 0.0f, 1.0f) * 2047.0f + 0.5f); // 11 bits -> 2^11 - 1 = 2047 - uint32_t b = (uint32_t)(nbl::hlsl::clamp(color.b, 0.0f, 1.0f) * 1023.0f + 0.5f); // 10 bits -> 2^10 - 1 = 1023 - - // Insert each component into the correct position - uint32_t packed = r; // R: bits 0-10 - packed = nbl::hlsl::glsl::bitfieldInsert(packed, g, 11, 11); // G: bits 11-21 - packed = nbl::hlsl::glsl::bitfieldInsert(packed, b, 22, 10); // B: bits 22-31 - - return packed; -} - -static float32_t3 unpackR11G11B10_UNORM(uint32_t packed) -{ - float32_t3 color; - - // Extract each component from the packed integer - uint32_t r = nbl::hlsl::glsl::bitfieldExtract(packed, 0, 11); // R: bits 0-10 - uint32_t g = nbl::hlsl::glsl::bitfieldExtract(packed, 11, 11); // G: bits 11-21 - uint32_t b = nbl::hlsl::glsl::bitfieldExtract(packed, 22, 10); // B: bits 22-31 - - // Convert back to float and scale to [0, 1] range - color.r = (float32_t)(r) / 2047.0f; - color.g = (float32_t)(g) / 2047.0f; - color.b = (float32_t)(b) / 1023.0f; - - return color; -} - -struct PolylineConnector -{ - pfloat64_t2 circleCenter; - float32_t2 v; // the vector from circle center to the intersection of the line ends, it's normalized such that the radius of the circle is equal to 1 - float32_t cosAngleDifferenceHalf; - float32_t _reserved_pad; -}; - -// NOTE: Don't attempt to pack curveMin/Max to uints because of limited range of values, we need the logarithmic precision of floats (more precision near 0) -// Goes into geometry buffer, needs to be aligned by 8 -struct CurveBox -{ - // will get transformed in the vertex shader, and will be calculated on the cpu when generating these boxes - pfloat64_t2 aabbMin; // 16 - pfloat64_t2 aabbMax; // 32 , TODO: we know it's a square/box -> we save 8 bytes if we needed to store extra data - float32_t2 curveMin[3]; // 56 - float32_t2 curveMax[3]; // 80 -}; - -#ifndef __HLSL_VERSION -static_assert(offsetof(CurveBox, aabbMin) == 0u); -static_assert(offsetof(CurveBox, aabbMax) == 16u); -static_assert(offsetof(CurveBox, curveMin[0]) == 32u); -static_assert(offsetof(CurveBox, curveMax[0]) == 56u); -static_assert(sizeof(CurveBox) == 80u); -#endif - -NBL_CONSTEXPR_INLINE_NSPC_SCOPE_VAR uint32_t InvalidRigidSegmentIndex = 0xffffffff; -NBL_CONSTEXPR_INLINE_NSPC_SCOPE_VAR float InvalidStyleStretchValue = nbl::hlsl::numeric_limits::infinity; - - -// TODO[Przemek]: we will need something similar to LineStyles but related to heigh shading settings which is user customizable (like stipple patterns) and requires upper_bound to figure out the color based on height value. -// We'll discuss that later or what it will be looking like and how it's gonna get passed to our shaders. - -struct TriangleMeshVertex -{ - pfloat64_t2 pos; - pfloat64_t height; // TODO: can be of type float32_t instead -}; - -// The color parameter is also used for styling non-curve objects such as text glyphs and hatches with solid color -struct LineStyle -{ - const static uint32_t StipplePatternMaxSize = 12u; - - // common data - float32_t4 color; - float screenSpaceLineWidth; // alternatively used as TextStyle::italicTiltSlope - float worldSpaceLineWidth; // alternatively used as TextStyle::boldInPixels - - // stipple pattern data - int32_t stipplePatternSize; - float reciprocalStipplePatternLen; - uint32_t stipplePattern[StipplePatternMaxSize]; // packed float into uint (top two msb indicate leftIsDotPattern and rightIsDotPattern as an optimization) - uint32_t isRoadStyleFlag; - uint32_t rigidSegmentIdx; // TODO: can be more mem efficient with styles by packing this along other values, since stipple pattern size is bounded by StipplePatternMaxSize - - float getStippleValue(const uint32_t ix) - { - const uint32_t floatValBis = 0xffffffff >> 2; // clear two msb bits reserved for something else - return (stipplePattern[ix] & floatValBis) / float(1u << 29); - } - - void setStippleValue(const uint32_t ix, const float val) - { - stipplePattern[ix] = (uint32_t)(val * (1u << 29u)); - } - - bool isLeftDot(const uint32_t ix) - { - // stipplePatternSize is odd by construction (pattern starts with + and ends with -) - return (stipplePattern[ix] & (1u << 30)) > 0; - } - - bool isRightDot(const uint32_t ix) - { - // stipplePatternSize is odd by construction (pattern starts with + and ends with -) - return (stipplePattern[ix] & (1u << 31)) > 0; - } - - bool hasStipples() - { - return stipplePatternSize > 0 ? true : false; - } - - void stretch(float stretch) - { - reciprocalStipplePatternLen /= stretch; - } -}; - -enum E_DTM_MODE -{ - OUTLINE = 1 << 0, - CONTOUR = 1 << 1, - HEIGHT_SHADING = 1 << 2, -}; - -enum class E_HEIGHT_SHADING_MODE : uint32_t -{ - DISCRETE_VARIABLE_LENGTH_INTERVALS, - DISCRETE_FIXED_LENGTH_INTERVALS, - CONTINOUS_INTERVALS -}; - -struct DTMContourSettings -{ - uint32_t contourLineStyleIdx; // index into line styles - float contourLinesStartHeight; - float contourLinesEndHeight; - float contourLinesHeightInterval; -}; - -struct DTMHeightShadingSettings -{ - const static uint32_t HeightColorMapMaxEntries = 16u; - - // height-color map - float intervalLength; - float intervalIndexToHeightMultiplier; - int isCenteredShading; - - uint32_t heightColorEntryCount; - float heightColorMapHeights[HeightColorMapMaxEntries]; - float32_t4 heightColorMapColors[HeightColorMapMaxEntries]; - - E_HEIGHT_SHADING_MODE determineHeightShadingMode() - { - if (nbl::hlsl::isinf(intervalLength)) - return E_HEIGHT_SHADING_MODE::DISCRETE_VARIABLE_LENGTH_INTERVALS; - if (intervalLength == 0.0f) - return E_HEIGHT_SHADING_MODE::CONTINOUS_INTERVALS; - return E_HEIGHT_SHADING_MODE::DISCRETE_FIXED_LENGTH_INTERVALS; - } -}; - -// Documentation and explanation of variables in DTMSettingsInfo -struct DTMSettings -{ - const static uint32_t MaxContourSettings = 8u; - - uint32_t mode; // E_DTM_MODE - - // outline - uint32_t outlineLineStyleIdx; - - // contour lines - uint32_t contourSettingsCount; - DTMContourSettings contourSettings[MaxContourSettings]; - - // height shading - DTMHeightShadingSettings heightShadingSettings; - - bool drawOutlineEnabled() NBL_CONST_MEMBER_FUNC { return (mode & E_DTM_MODE::OUTLINE) != 0u; } - bool drawContourEnabled() NBL_CONST_MEMBER_FUNC { return (mode & E_DTM_MODE::CONTOUR) != 0u; } - bool drawHeightShadingEnabled() NBL_CONST_MEMBER_FUNC { return (mode & E_DTM_MODE::HEIGHT_SHADING) != 0u; } -}; - -#ifndef __HLSL_VERSION -inline bool operator==(const LineStyle& lhs, const LineStyle& rhs) -{ - // Compare bits of the screen space line width values, as they may have been bit cast into integers - // for the texture IDs, and can't be compared when that results in a NaN or Infinity float - const int comparisonResult = std::memcmp(&lhs.screenSpaceLineWidth, &rhs.screenSpaceLineWidth, sizeof(float)); - const bool areParametersEqual = - lhs.color == rhs.color && - comparisonResult == 0 && - lhs.worldSpaceLineWidth == rhs.worldSpaceLineWidth && - lhs.stipplePatternSize == rhs.stipplePatternSize && - lhs.reciprocalStipplePatternLen == rhs.reciprocalStipplePatternLen && - lhs.isRoadStyleFlag == rhs.isRoadStyleFlag && - lhs.rigidSegmentIdx == rhs.rigidSegmentIdx; - - if (!areParametersEqual) - return false; - - const bool isStipplePatternArrayEqual = (lhs.stipplePatternSize > 0) ? (std::memcmp(lhs.stipplePattern, rhs.stipplePattern, sizeof(uint32_t) * lhs.stipplePatternSize) == 0) : true; - - return isStipplePatternArrayEqual; -} - -inline bool operator==(const DTMSettings& lhs, const DTMSettings& rhs) -{ - if (lhs.mode != rhs.mode) - return false; - - if (lhs.drawOutlineEnabled()) - { - if (lhs.outlineLineStyleIdx != rhs.outlineLineStyleIdx) - return false; - } - - if (lhs.drawContourEnabled()) - { - if (lhs.contourSettingsCount != rhs.contourSettingsCount) - return false; - if (memcmp(lhs.contourSettings, rhs.contourSettings, lhs.contourSettingsCount * sizeof(DTMContourSettings))) - return false; - } - - if (lhs.drawHeightShadingEnabled()) - { - if (lhs.heightShadingSettings.intervalLength != rhs.heightShadingSettings.intervalLength) - return false; - if (lhs.heightShadingSettings.intervalIndexToHeightMultiplier != rhs.heightShadingSettings.intervalIndexToHeightMultiplier) - return false; - if (lhs.heightShadingSettings.isCenteredShading != rhs.heightShadingSettings.isCenteredShading) - return false; - if (lhs.heightShadingSettings.heightColorEntryCount != rhs.heightShadingSettings.heightColorEntryCount) - return false; - - - if(memcmp(lhs.heightShadingSettings.heightColorMapHeights, rhs.heightShadingSettings.heightColorMapHeights, lhs.heightShadingSettings.heightColorEntryCount * sizeof(float))) - return false; - if(memcmp(lhs.heightShadingSettings.heightColorMapColors, rhs.heightShadingSettings.heightColorMapColors, lhs.heightShadingSettings.heightColorEntryCount * sizeof(float32_t4))) - return false; - } - - return true; -} -#endif - -NBL_CONSTEXPR_INLINE_NSPC_SCOPE_VAR uint32_t ImagesBindingArraySize = 128; -NBL_CONSTEXPR_INLINE_NSPC_SCOPE_VAR uint32_t MainObjectIdxBits = 24u; // It will be packed next to alpha in a texture -NBL_CONSTEXPR_INLINE_NSPC_SCOPE_VAR uint32_t AlphaBits = 32u - MainObjectIdxBits; -NBL_CONSTEXPR_INLINE_NSPC_SCOPE_VAR uint32_t MaxIndexableMainObjects = (1u << MainObjectIdxBits) - 1u; -NBL_CONSTEXPR_INLINE_NSPC_SCOPE_VAR uint32_t InvalidStyleIdx = nbl::hlsl::numeric_limits::max; -NBL_CONSTEXPR_INLINE_NSPC_SCOPE_VAR uint32_t InvalidDTMSettingsIdx = nbl::hlsl::numeric_limits::max; -NBL_CONSTEXPR_INLINE_NSPC_SCOPE_VAR uint32_t InvalidMainObjectIdx = MaxIndexableMainObjects; -NBL_CONSTEXPR_INLINE_NSPC_SCOPE_VAR uint32_t InvalidCustomProjectionIndex = nbl::hlsl::numeric_limits::max; -NBL_CONSTEXPR_INLINE_NSPC_SCOPE_VAR uint32_t InvalidCustomClipRectIndex = nbl::hlsl::numeric_limits::max; -NBL_CONSTEXPR_INLINE_NSPC_SCOPE_VAR uint32_t InvalidTextureIndex = nbl::hlsl::numeric_limits::max; - -// Hatches -NBL_CONSTEXPR_INLINE_NSPC_SCOPE_VAR MajorAxis SelectedMajorAxis = MajorAxis::MAJOR_Y; -NBL_CONSTEXPR_INLINE_NSPC_SCOPE_VAR MajorAxis SelectedMinorAxis = MajorAxis::MAJOR_X; //(MajorAxis) (1 - (uint32_t) SelectedMajorAxis); - -// Text or MSDF Hatches -NBL_CONSTEXPR_INLINE_NSPC_SCOPE_VAR float MSDFPixelRange = 4.0f; -NBL_CONSTEXPR_INLINE_NSPC_SCOPE_VAR float MSDFPixelRangeHalf = MSDFPixelRange / 2.0f; -NBL_CONSTEXPR_INLINE_NSPC_SCOPE_VAR float MSDFSize = 64.0f; -NBL_CONSTEXPR_INLINE_NSPC_SCOPE_VAR uint32_t MSDFMips = 4; -NBL_CONSTEXPR_INLINE_NSPC_SCOPE_VAR float HatchFillMSDFSceenSpaceSize = 8.0; - -inline bool isInvalidGridDtmHeightValue(float value) -{ - return nbl::hlsl::isnan(value); -} - -// Used in CPU-side only for now -struct OrientedBoundingBox2D -{ - pfloat64_t2 topLeft; // 2 * 8 = 16 bytes (16) - float32_t2 dirU; // 2 * 4 = 8 bytes (24) - float32_t aspectRatio; // 4 bytes (28) - -#ifndef __HLSL_VERSION - void transform(pfloat64_t3x3 transformation) - { - // We want to do tile streaming and clipping calculations in the same space; hence, we transform the obb (defined in local DWG or symbol) space to worldspace, and we use ndcToWorldTransformation + worldToUV to calculate which tiles are visible in current view) - const pfloat64_t2 prevDirV = pfloat64_t2(dirU.y, -dirU.x) * pfloat64_t(aspectRatio); - topLeft = nbl::hlsl::mul(transformation, pfloat64_t3(topLeft, 1)); - dirU = nbl::hlsl::mul(transformation, pfloat64_t3(dirU, 0)); - const pfloat64_t2 newDirV = nbl::hlsl::mul(transformation, pfloat64_t3(prevDirV, 0)); - aspectRatio = nbl::hlsl::length(newDirV) / nbl::hlsl::length(dirU); // TODO: maybe we could compute new transformed aspect ratio "smarter" - } -#endif -}; - -#ifdef __HLSL_VERSION -[[vk::binding(0, 0)]] ConstantBuffer globals : register(b0); - -LineStyle loadLineStyle(const uint32_t index) -{ - return vk::RawBufferLoad(globals.pointers.lineStyles + index * sizeof(LineStyle), 4u); -} -DTMSettings loadDTMSettings(const uint32_t index) -{ - return vk::RawBufferLoad(globals.pointers.dtmSettings + index * sizeof(DTMSettings), 4u); -} -pfloat64_t3x3 loadCustomProjection(const uint32_t index) -{ - return vk::RawBufferLoad(globals.pointers.customProjections + index * sizeof(pfloat64_t3x3), 8u); -} -WorldClipRect loadCustomClipRect(const uint32_t index) -{ - return vk::RawBufferLoad(globals.pointers.customClipRects + index * sizeof(WorldClipRect), 8u); -} -MainObject loadMainObject(const uint32_t index) -{ - return vk::RawBufferLoad(globals.pointers.mainObjects + index * sizeof(MainObject), 4u); -} -DrawObject loadDrawObject(const uint32_t index) -{ - return vk::RawBufferLoad(globals.pointers.drawObjects + index * sizeof(DrawObject), 8u); -} -#else -static_assert(alignof(LineStyle)==4u); -static_assert(alignof(DTMSettings)==4u); -static_assert(alignof(pfloat64_t3x3)==8u); -static_assert(alignof(WorldClipRect)==8u); -static_assert(alignof(MainObject)==4u); -static_assert(alignof(DrawObject)==8u); -#endif - - -#endif diff --git a/62_CAD/shaders/main_pipeline/all_fragment_shaders.hlsl b/62_CAD/shaders/main_pipeline/all_fragment_shaders.hlsl deleted file mode 100644 index 2968a6095..000000000 --- a/62_CAD/shaders/main_pipeline/all_fragment_shaders.hlsl +++ /dev/null @@ -1,5 +0,0 @@ -#include "fragment_shader.hlsl" -#include "fragment_shader_debug.hlsl" -#include "fragment_shader_georef.hlsl" -#include "fragment_shader_resolve_alphas.hlsl" - diff --git a/62_CAD/shaders/main_pipeline/common.hlsl b/62_CAD/shaders/main_pipeline/common.hlsl deleted file mode 100644 index fc80c67f8..000000000 --- a/62_CAD/shaders/main_pipeline/common.hlsl +++ /dev/null @@ -1,265 +0,0 @@ -#ifndef _CAD_EXAMPLE_MAIN_PIPELINE_COMMON_HLSL_INCLUDED_ -#define _CAD_EXAMPLE_MAIN_PIPELINE_COMMON_HLSL_INCLUDED_ - -#include "../globals.hlsl" - -// This function soley exists to match n4ce's behaviour, colors and color operations for DTMs, Curves, Lines, Hatches are done in linear space and then outputted to linear surface (as if surface had UNORM format, but ours is SRGB) -// We should do gamma "uncorrection" to account for the fact that our surface format is SRGB and will do gamma correction -void gammaUncorrect(inout float3 col) -{ - bool outputToSRGB = true; // TODO - float gamma = (outputToSRGB) ? 2.2f : 1.0f; - col.rgb = pow(col.rgb, gamma); -} - -// TODO: Use these in C++ as well once numeric_limits compiles on C++ -float32_t2 unpackCurveBoxUnorm(uint32_t2 value) -{ - return float32_t2(value) / float32_t(numeric_limits::max); -} - -float32_t2 unpackCurveBoxSnorm(int32_t2 value) -{ - return float32_t2(value) / float32_t(numeric_limits::max); -} - - -uint32_t2 packCurveBoxUnorm(float32_t2 value) -{ - return value * float32_t(numeric_limits::max); -} - -int32_t2 packCurveBoxSnorm(float32_t2 value) -{ - return value * float32_t(numeric_limits::max); -} - -// The root we're always looking for: -// 2 * C / (-B - detSqrt) -// We send to the FS: -B, 2C, det -template -struct PrecomputedRootFinder -{ - using float_t2 = vector; - using float_t3 = vector; - - float_t C2; - float_t negB; - float_t det; - - float_t computeRoots() - { - return C2 / (negB - sqrt(det)); - } - - static PrecomputedRootFinder construct(float_t negB, float_t C2, float_t det) - { - PrecomputedRootFinder result; - result.C2 = C2; - result.det = det; - result.negB = negB; - return result; - } - - static PrecomputedRootFinder construct(math::equations::Quadratic quadratic) - { - PrecomputedRootFinder result; - result.C2 = quadratic.c * 2.0; - result.negB = -quadratic.b; - result.det = quadratic.b * quadratic.b - 4.0 * quadratic.a * quadratic.c; - return result; - } -}; - - // TODO[Przemek]: your triangle mesh passed parameters from vtx to fragment will need to be here (e.g. height). - // As always try to reuse parameters and try not to introduce new ones -struct PSInput -{ - [[vk::location(0)]] float4 position : SV_Position; - [[vk::location(1)]] float4 clip : SV_ClipDistance; - - [[vk::location(2)]] nointerpolation uint4 data1 : COLOR1; - [[vk::location(3)]] nointerpolation float4 data2 : COLOR2; - [[vk::location(4)]] nointerpolation float4 data3 : COLOR3; - [[vk::location(5)]] nointerpolation float4 data4 : COLOR4; - // Data segments that need interpolation, mostly for hatches - [[vk::location(6)]] float4 interp_data5 : COLOR5; - [[vk::location(7)]] nointerpolation float data6 : COLOR6; - -#ifdef FRAGMENT_SHADER_INPUT - [[vk::location(8)]] [[vk::ext_decorate(/*spv::DecoratePerVertexKHR*/5285)]] float3 vertexScreenSpacePos[3] : COLOR7; -#else - [[vk::location(8)]] float3 vertexScreenSpacePos : COLOR7; -#endif - // ArcLenCalculator - - // Set functions used in vshader, get functions used in fshader - // We have to do this because we don't have union in hlsl and this is the best way to alias - - /* SHARED: ALL ObjectTypes */ - ObjectType getObjType() { return (ObjectType) data1.x; } - uint getMainObjectIdx() { return data1.y; } - - void setObjType(ObjectType objType) { data1.x = (uint) objType; } - void setMainObjectIdx(uint mainObjIdx) { data1.y = mainObjIdx; } - - /* SHARED: LINE + QUAD_BEZIER (Curve Outlines) */ - float getLineThickness() { return asfloat(data1.z); } - float getPatternStretch() { return asfloat(data1.w); } - - void setLineThickness(float lineThickness) { data1.z = asuint(lineThickness); } - void setPatternStretch(float stretch) { data1.w = asuint(stretch); } - - void setCurrentPhaseShift(float phaseShift) { interp_data5.x = phaseShift; } - float getCurrentPhaseShift() { return interp_data5.x; } - - /* LINE */ - float2 getLineStart() { return data2.xy; } - float2 getLineEnd() { return data2.zw; } - void setLineStart(float2 lineStart) { data2.xy = lineStart; } - void setLineEnd(float2 lineEnd) { data2.zw = lineEnd; } - - /* QUAD_BEZIER */ - shapes::Quadratic getQuadratic() - { - return shapes::Quadratic::construct(data2.xy, data2.zw, data3.xy); - } - void setQuadratic(shapes::Quadratic quadratic) - { - data2.xy = quadratic.A; - data2.zw = quadratic.B; - data3.xy = quadratic.C; - } - - void setQuadraticPrecomputedArcLenData(shapes::Quadratic::ArcLengthCalculator preCompData) - { - data3.zw = float2(preCompData.lenA2, preCompData.AdotB); - data4 = float4(preCompData.a, preCompData.b, preCompData.c, preCompData.b_over_4a); - } - shapes::Quadratic::ArcLengthCalculator getQuadraticArcLengthCalculator() - { - return shapes::Quadratic::ArcLengthCalculator::construct(data3.z, data3.w, data4.x, data4.y, data4.z, data4.w); - } - - /* CURVE_BOX */ - // Curves are split in the vertex shader based on their tmin and tmax - // Min curve is smaller in the minor coordinate (e.g. in the default of y top to bottom sweep, - // curveMin = smaller x / left, curveMax = bigger x / right) - // TODO: possible optimization: passing precomputed values for solving the quadratic equation instead - - // data2, data3, data4 - math::equations::Quadratic getCurveMinMinor() { - return math::equations::Quadratic::construct(data2.x, data2.y, data2.z); - } - math::equations::Quadratic getCurveMaxMinor() { - return math::equations::Quadratic::construct(data2.w, data3.x, data3.y); - } - - void setCurveMinMinor(math::equations::Quadratic bezier) { - data2.x = bezier.a; - data2.y = bezier.b; - data2.z = bezier.c; - } - void setCurveMaxMinor(math::equations::Quadratic bezier) { - data2.w = bezier.a; - data3.x = bezier.b; - data3.y = bezier.c; - } - - // data4 - math::equations::Quadratic getCurveMinMajor() { - return math::equations::Quadratic::construct(data4.x, data4.y, data3.z); - } - math::equations::Quadratic getCurveMaxMajor() { - return math::equations::Quadratic::construct(data4.z, data4.w, data3.w); - } - - void setCurveMinMajor(math::equations::Quadratic bezier) { - data4.x = bezier.a; - data4.y = bezier.b; - data3.z = bezier.c; - } - void setCurveMaxMajor(math::equations::Quadratic bezier) { - data4.z = bezier.a; - data4.w = bezier.b; - data3.w = bezier.c; - } - - // Curve box value along minor & major axis - float getMinorBBoxUV() { return interp_data5.x; }; - void setMinorBBoxUV(float minorBBoxUV) { interp_data5.x = minorBBoxUV; } - float getMajorBBoxUV() { return interp_data5.y; }; - void setMajorBBoxUV(float majorBBoxUV) { interp_data5.y = majorBBoxUV; } - - float2 getCurveBoxScreenSpaceSize() { return asfloat(data1.zw); } - void setCurveBoxScreenSpaceSize(float2 aabbSize) { data1.zw = asuint(aabbSize); } - - /* POLYLINE_CONNECTOR */ - void setPolylineConnectorTrapezoidStart(float2 trapezoidStart) { data2.xy = trapezoidStart; } - void setPolylineConnectorTrapezoidEnd(float2 trapezoidEnd) { data2.zw = trapezoidEnd; } - void setPolylineConnectorTrapezoidShortBase(float shortBase) { data3.x = shortBase; } - void setPolylineConnectorTrapezoidLongBase(float longBase) { data3.y = longBase; } - void setPolylineConnectorCircleCenter(float2 C) { data3.zw = C; } - - float2 getPolylineConnectorTrapezoidStart() { return data2.xy; } - float2 getPolylineConnectorTrapezoidEnd() { return data2.zw; } - float getPolylineConnectorTrapezoidShortBase() { return data3.x; } - float getPolylineConnectorTrapezoidLongBase() { return data3.y; } - float2 getPolylineConnectorCircleCenter() { return data3.zw; } - - /* FONT_GLYPH */ - float2 getFontGlyphUV() { return interp_data5.xy; } - uint32_t getFontGlyphTextureId() { return asuint(data2.x); } - float getFontGlyphPxRange() { return data2.y; } - - void setFontGlyphUV(float2 uv) { interp_data5.xy = uv; } - void setFontGlyphTextureId(uint32_t textureId) { data2.x = asfloat(textureId); } - void setFontGlyphPxRange(float glyphPxRange) { data2.y = glyphPxRange; } - - /* IMAGE */ - float2 getImageUV() { return interp_data5.xy; } - uint32_t getImageTextureId() { return asuint(data2.x); } - - void setImageUV(float2 uv) { interp_data5.xy = uv; } - void setImageTextureId(uint32_t textureId) { data2.x = asfloat(textureId); } - - /* TRIANGLE MESH */ - -#ifndef FRAGMENT_SHADER_INPUT // vertex shader - void setScreenSpaceVertexAttribs(float3 pos) { vertexScreenSpacePos = pos; } -#else // fragment shader - float3 getScreenSpaceVertexAttribs(uint32_t vertexIndex) { return vertexScreenSpacePos[vertexIndex]; } -#endif - - /* GRID DTM */ - uint getGridDTMHeightTextureID() { return data1.z; } - float2 getGridDTMScreenSpaceGridExtents() { return data2.xy; } - float getGridDTMScreenSpaceCellWidth() { return data2.z; } - - void setGridDTMHeightTextureID(uint textureID) { data1.z = textureID; } - void setGridDTMScreenSpaceGridExtents(float2 screenSpaceGridExtends) { data2.xy = screenSpaceGridExtends; } - void setGridDTMScreenSpaceCellWidth(float screenSpaceGridWidth) { data2.z = screenSpaceGridWidth; } - - void setCurrentWorldToScreenRatio(float worldToScreen) { data6.x = worldToScreen; } - float getCurrentWorldToScreenRatio() { return data6.x; } - -}; - -// Set 0 - Scene Data and Globals, buffer bindings don't change the buffers only get updated - -// [[vk::binding(0, 0)]] ConstantBuffer globals; ---> moved to globals.hlsl - -[[vk::push_constant]] PushConstants pc; - -[[vk::combinedImageSampler]][[vk::binding(1, 0)]] Texture2DArray msdfTextures : register(t4); -[[vk::combinedImageSampler]][[vk::binding(1, 0)]] SamplerState msdfSampler : register(s4); - -[[vk::binding(2, 0)]] SamplerState textureSampler : register(s5); -[[vk::binding(3, 0)]] Texture2D textures[ImagesBindingArraySize] : register(t5); -[[vk::binding(3, 0)]] Texture2D texturesU32[ImagesBindingArraySize] : register(t5); - -// Set 1 - Window dependant data which has higher update frequency due to multiple windows and resize need image recreation and descriptor writes -[[vk::binding(0, 1)]] globallycoherent RWTexture2D pseudoStencil : register(u0); -[[vk::binding(1, 1)]] globallycoherent RWTexture2D colorStorage : register(u1); - -#endif diff --git a/62_CAD/shaders/main_pipeline/dtm.hlsl b/62_CAD/shaders/main_pipeline/dtm.hlsl deleted file mode 100644 index 749d0fd6f..000000000 --- a/62_CAD/shaders/main_pipeline/dtm.hlsl +++ /dev/null @@ -1,524 +0,0 @@ -#ifndef _CAD_EXAMPLE_DTM_HLSL_INCLUDED_ -#define _CAD_EXAMPLE_DTM_HLSL_INCLUDED_ - -#include "line_style.hlsl" - -namespace dtm -{ - -// for usage in upper_bound function -struct DTMSettingsHeightsAccessor -{ - DTMHeightShadingSettings settings; - using value_type = float; - - float operator[](const uint32_t ix) - { - return settings.heightColorMapHeights[ix]; - } -}; - -float dot2(in float2 vec) -{ - return dot(vec, vec); -} - -struct HeightSegmentTransitionData -{ - float currentHeight; - float4 currentSegmentColor; - float boundaryHeight; - float4 otherSegmentColor; -}; - -// This function interpolates between the current and nearest segment colors based on the -// screen-space distance to the segment boundary. The result is a smoothly blended color -// useful for visualizing discrete height levels without harsh edges. -float4 smoothHeightSegmentTransition(in HeightSegmentTransitionData transitionInfo, in float heightDeriv) -{ - float pxDistanceToNearestSegment = abs((transitionInfo.currentHeight - transitionInfo.boundaryHeight) / heightDeriv); - float nearestSegmentColorCoverage = smoothstep(-globals.antiAliasingFactor, globals.antiAliasingFactor, pxDistanceToNearestSegment); - float4 localHeightColor = lerp(transitionInfo.otherSegmentColor, transitionInfo.currentSegmentColor, nearestSegmentColorCoverage); - return localHeightColor; -} - -// Computes the continuous position of a height value within uniform intervals. -// flooring this value will give the interval index -// -// If `isCenteredShading` is true, the intervals are centered around `minHeight`, meaning the -// first interval spans [minHeight - intervalLength / 2.0, minHeight + intervalLength / 2.0]. -// Otherwise, intervals are aligned from `minHeight` upward, so the first interval spans -// [minHeight, minHeight + intervalLength]. -// -// Parameters: -// - height: The height value to classify. -// - minHeight: The reference starting height for interval calculation. -// - intervalLength: The length of each interval segment. -// - isCenteredShading: Whether to center the shading intervals around minHeight. -// -// Returns: -// - A float representing the continuous position within the interval grid. -float getIntervalPosition(in float height, in float minHeight, in float intervalLength, in bool isCenteredShading) -{ - if (isCenteredShading) - return ((height - minHeight) / intervalLength + 0.5f); - else - return ((height - minHeight) / intervalLength); -} - -void getIntervalHeightAndColor(in int intervalIndex, in DTMHeightShadingSettings settings, out float4 outIntervalColor, out float outIntervalHeight) -{ - float minShadingHeight = settings.heightColorMapHeights[0]; - float heightForColor = minShadingHeight + float(intervalIndex) * settings.intervalIndexToHeightMultiplier; - - if (settings.isCenteredShading) - outIntervalHeight = minShadingHeight + (float(intervalIndex) - 0.5) * settings.intervalLength; - else - outIntervalHeight = minShadingHeight + (float(intervalIndex)) * settings.intervalLength; - - DTMSettingsHeightsAccessor dtmHeightsAccessor = { settings }; - int32_t upperBoundHeightIndex = min(nbl::hlsl::upper_bound(dtmHeightsAccessor, 0, settings.heightColorEntryCount, heightForColor), settings.heightColorEntryCount - 1u); - int32_t lowerBoundHeightIndex = max(upperBoundHeightIndex - 1, 0); - - float upperBoundHeight = settings.heightColorMapHeights[upperBoundHeightIndex]; - float lowerBoundHeight = settings.heightColorMapHeights[lowerBoundHeightIndex]; - - float4 upperBoundColor = settings.heightColorMapColors[upperBoundHeightIndex]; - float4 lowerBoundColor = settings.heightColorMapColors[lowerBoundHeightIndex]; - - if (upperBoundHeight == lowerBoundHeight) - { - outIntervalColor = upperBoundColor; - } - else - { - float interpolationVal = (heightForColor - lowerBoundHeight) / (upperBoundHeight - lowerBoundHeight); - outIntervalColor = lerp(lowerBoundColor, upperBoundColor, interpolationVal); - } -} - -float3 calculateDTMTriangleBarycentrics(in float2 v1, in float2 v2, in float2 v3, in float2 p) -{ - float denom = (v2.x - v1.x) * (v3.y - v1.y) - (v3.x - v1.x) * (v2.y - v1.y); - float u = ((v2.y - v3.y) * (p.x - v3.x) + (v3.x - v2.x) * (p.y - v3.y)) / denom; - float v = ((v3.y - v1.y) * (p.x - v3.x) + (v1.x - v3.x) * (p.y - v3.y)) / denom; - float w = 1.0 - u - v; - return float3(u, v, w); -} - -float4 calculateDTMHeightColor(in DTMHeightShadingSettings settings, in float3 triangleVertices[3], in float heightDeriv, in float2 fragPos, in float height) -{ - float4 outputColor = float4(0.0f, 0.0f, 0.0f, 0.0f); - - // HEIGHT SHADING - const uint32_t heightMapSize = settings.heightColorEntryCount; - float minShadingHeight = settings.heightColorMapHeights[0]; - float maxShadingHeight = settings.heightColorMapHeights[heightMapSize - 1]; - - if (heightMapSize > 0) - { - // Do the triangle SDF: - float2 e0 = (triangleVertices[1] - triangleVertices[0]).xy; - float2 e1 = (triangleVertices[2] - triangleVertices[1]).xy; - float2 e2 = (triangleVertices[0] - triangleVertices[2]).xy; - - float2 v0 = fragPos - triangleVertices[0].xy; - float2 v1 = fragPos - triangleVertices[1].xy; - float2 v2 = fragPos - triangleVertices[2].xy; - - float distanceToLine0 = dot2(v0 - e0 * clamp(dot(v0, e0) / dot(e0, e0), 0.0, 1.0)); - float distanceToLine1 = dot2(v1 - e1 * clamp(dot(v1, e1) / dot(e1, e1), 0.0, 1.0)); - float distanceToLine2 = dot2(v2 - e2 * clamp(dot(v2, e2) / dot(e2, e2), 0.0, 1.0)); - - // TODO[Optization]: We can get the sign (whether inside or outside the triangle) from the barycentric coords we already compute outside this func - // So we can skip this part which tries to figure out which side of each triangle edge line the fragPos relies on - float o = e0.x * e2.y - e0.y * e2.x; - float2 d = min(min(float2(distanceToLine0, o * (v0.x * e0.y - v0.y * e0.x)), - float2(distanceToLine1, o * (v1.x * e1.y - v1.y * e1.x))), - float2(distanceToLine2, o * (v2.x * e2.y - v2.y * e2.x))); - - float triangleSDF = -sqrt(d.x) * sign(d.y); - - // Intersect with the region between min and max height shading. - float minHeightShadingLine = (minShadingHeight - height) / heightDeriv; - float maxHeightShadingLine = (height - maxShadingHeight) / heightDeriv; - - float convexPolygonSdf = triangleSDF; - convexPolygonSdf = max(convexPolygonSdf, minHeightShadingLine); - convexPolygonSdf = max(convexPolygonSdf, maxHeightShadingLine); - outputColor.a = 1.0f - smoothstep(0.0f, globals.antiAliasingFactor + globals.antiAliasingFactor, convexPolygonSdf); - - // calculate height color - E_HEIGHT_SHADING_MODE mode = settings.determineHeightShadingMode(); - if (mode == E_HEIGHT_SHADING_MODE::DISCRETE_VARIABLE_LENGTH_INTERVALS) - { - DTMSettingsHeightsAccessor dtmHeightsAccessor = { settings }; - int upperBoundIndex = min(nbl::hlsl::upper_bound(dtmHeightsAccessor, 0u, heightMapSize, height), heightMapSize - 1u); - int mapIndex = max(upperBoundIndex - 1, 0); - int mapIndexPrev = max(mapIndex - 1, 0); - int mapIndexNext = min(mapIndex + 1, heightMapSize - 1); - - // logic explainer: if colorIdx is 0.0 then it means blend with next - // if color idx is >= length of the colours array then it means it's also > 0.0 and this blend with prev is true - // if color idx is > 0 and < len - 1, then it depends on the current pixel's height value and two closest height values - bool blendWithPrev = (mapIndex > 0) - && (mapIndex >= heightMapSize - 1 || (height * 2.0 < settings.heightColorMapHeights[upperBoundIndex] + settings.heightColorMapHeights[mapIndex])); - - HeightSegmentTransitionData transitionInfo; - transitionInfo.currentHeight = height; - transitionInfo.currentSegmentColor = settings.heightColorMapColors[mapIndex]; - transitionInfo.boundaryHeight = blendWithPrev ? settings.heightColorMapHeights[mapIndex] : settings.heightColorMapHeights[mapIndexNext]; - transitionInfo.otherSegmentColor = blendWithPrev ? settings.heightColorMapColors[mapIndexPrev] : settings.heightColorMapColors[mapIndexNext]; - - float4 localHeightColor = smoothHeightSegmentTransition(transitionInfo, heightDeriv); - outputColor.rgb = localHeightColor.rgb; - outputColor.a *= localHeightColor.a; - } - else if (mode == E_HEIGHT_SHADING_MODE::DISCRETE_FIXED_LENGTH_INTERVALS) - { - float intervalPosition = getIntervalPosition(height, minShadingHeight, settings.intervalLength, settings.isCenteredShading); - float positionWithinInterval = frac(intervalPosition); - int intervalIndex = nbl::hlsl::_static_cast(intervalPosition); - - float4 currentIntervalColor; - float currentIntervalHeight; - getIntervalHeightAndColor(intervalIndex, settings, currentIntervalColor, currentIntervalHeight); - - bool blendWithPrev = (positionWithinInterval < 0.5f); - - HeightSegmentTransitionData transitionInfo; - transitionInfo.currentHeight = height; - transitionInfo.currentSegmentColor = currentIntervalColor; - if (blendWithPrev) - { - int prevIntervalIdx = max(intervalIndex - 1, 0); - float prevIntervalHeight; // unused, the currentIntervalHeight is the boundary height between current and prev - getIntervalHeightAndColor(prevIntervalIdx, settings, transitionInfo.otherSegmentColor, prevIntervalHeight); - transitionInfo.boundaryHeight = currentIntervalHeight; - } - else - { - int nextIntervalIdx = intervalIndex + 1; - getIntervalHeightAndColor(nextIntervalIdx, settings, transitionInfo.otherSegmentColor, transitionInfo.boundaryHeight); - } - - float4 localHeightColor = smoothHeightSegmentTransition(transitionInfo, heightDeriv); - outputColor.rgb = localHeightColor.rgb; - outputColor.a *= localHeightColor.a; - } - else if (mode == E_HEIGHT_SHADING_MODE::CONTINOUS_INTERVALS) - { - DTMSettingsHeightsAccessor dtmHeightsAccessor = { settings }; - uint32_t upperBoundHeightIndex = min(nbl::hlsl::upper_bound(dtmHeightsAccessor, 0u, heightMapSize - 1u, height), heightMapSize - 1u); - uint32_t lowerBoundHeightIndex = upperBoundHeightIndex == 0 ? upperBoundHeightIndex : upperBoundHeightIndex - 1; - - float upperBoundHeight = settings.heightColorMapHeights[upperBoundHeightIndex]; - float lowerBoundHeight = settings.heightColorMapHeights[lowerBoundHeightIndex]; - - float4 upperBoundColor = settings.heightColorMapColors[upperBoundHeightIndex]; - float4 lowerBoundColor = settings.heightColorMapColors[lowerBoundHeightIndex]; - - float interpolationVal; - if (upperBoundHeightIndex == 0) - interpolationVal = 1.0f; - else - interpolationVal = (height - lowerBoundHeight) / (upperBoundHeight - lowerBoundHeight); - - float4 localHeightColor = lerp(lowerBoundColor, upperBoundColor, interpolationVal); - - outputColor.a *= localHeightColor.a; - outputColor.rgb = localHeightColor.rgb * outputColor.a + outputColor.rgb * (1.0f - outputColor.a); - } - } - - return outputColor; -} - -float calculateDTMContourSDF(in DTMContourSettings contourSettings, in LineStyle contourStyle, in float worldToScreenRatio, in float3 v[3], in float2 fragPos, in float height) -{ - float distance = nbl::hlsl::numeric_limits::max; - const float contourThickness = (contourStyle.screenSpaceLineWidth + contourStyle.worldSpaceLineWidth / worldToScreenRatio) * 0.5f; - const float stretch = 1.0f; - const float phaseShift = 0.0f; - - const float startHeight = contourSettings.contourLinesStartHeight; - const float endHeight = contourSettings.contourLinesEndHeight; - const float interval = contourSettings.contourLinesHeightInterval; - const int maxContourLineIdx = (endHeight - startHeight) / interval; - - // TODO: it actually can output a negative number, fix - int contourLineIdx = nbl::hlsl::_static_cast((height - startHeight) / interval + 0.5f); - contourLineIdx = clamp(contourLineIdx, 0, maxContourLineIdx); - float contourLineHeight = startHeight + interval * contourLineIdx; - - - // Sort so that v[0].z >= v[1].z >= v[2].z - if (v[0].z < v[1].z) - nbl::hlsl::swap(v[0], v[1]); - if (v[0].z < v[2].z) - nbl::hlsl::swap(v[0], v[2]); - if (v[1].z < v[2].z) - nbl::hlsl::swap(v[1], v[2]); - - int contourLinePointsIdx = 0; - float2 contourLinePoints[2]; - for (int i = 0; i < 3; ++i) - { - if (contourLinePointsIdx == 2) - break; - - int minvIdx = 0; - int maxvIdx = 0; - - if (i == 0) { minvIdx = 2; maxvIdx = 0; } - if (i == 1) { minvIdx = 1; maxvIdx = 0; } - if (i == 2) { minvIdx = 2; maxvIdx = 1; } - - float3 minV = v[minvIdx]; - float3 maxV = v[maxvIdx]; - - if (contourLineHeight >= minV.z && contourLineHeight <= maxV.z) - { - float interpolationVal = (contourLineHeight - minV.z) / (maxV.z - minV.z); - contourLinePoints[contourLinePointsIdx] = lerp(minV.xy, maxV.xy, clamp(interpolationVal, 0.0f, 1.0f)); - ++contourLinePointsIdx; - } - } - - if (contourLinePointsIdx == 2) - { - nbl::hlsl::shapes::Line lineSegment = nbl::hlsl::shapes::Line::construct(contourLinePoints[0], contourLinePoints[1]); - - if (!contourStyle.hasStipples() || stretch == InvalidStyleStretchValue) - { - distance = ClippedSignedDistance< nbl::hlsl::shapes::Line >::sdf(lineSegment, fragPos, contourThickness, contourStyle.isRoadStyleFlag); - } - else - { - // TODO: - // It might be beneficial to calculate distance between pixel and contour line to early out some pixels and save yourself from stipple sdf computations! - // where you only compute the complex sdf if abs((height - contourVal) / heightDeriv) <= aaFactor - nbl::hlsl::shapes::Line::ArcLengthCalculator arcLenCalc = nbl::hlsl::shapes::Line::ArcLengthCalculator::construct(lineSegment); - LineStyleClipper clipper = LineStyleClipper::construct(contourStyle, lineSegment, arcLenCalc, phaseShift, stretch, worldToScreenRatio); - distance = ClippedSignedDistance, LineStyleClipper>::sdf(lineSegment, fragPos, contourThickness, contourStyle.isRoadStyleFlag, clipper); - } - } - - return distance; -} - -float4 calculateDTMOutlineColor(in uint outlineLineStyleIdx, in float worldToScreenRatio, in float3 v[3], in float2 fragPos) -{ - float4 outputColor; - - LineStyle outlineStyle = loadLineStyle(outlineLineStyleIdx); - const float outlineThickness = (outlineStyle.screenSpaceLineWidth + outlineStyle.worldSpaceLineWidth / worldToScreenRatio) * 0.5f; - const float phaseShift = 0.0f; // input.getCurrentPhaseShift(); - const float stretch = 1.0f; - - // index of vertex opposing an edge, needed for calculation of triangle heights - uint opposingVertexIdx[3]; - opposingVertexIdx[0] = 2; - opposingVertexIdx[1] = 0; - opposingVertexIdx[2] = 1; - - float minDistance = nbl::hlsl::numeric_limits::max; - if (!outlineStyle.hasStipples() || stretch == InvalidStyleStretchValue) - { - for (int i = 0; i < 3; ++i) - { - float3 p0 = v[i]; - float3 p1 = v[(i + 1) % 3]; - - float distance = nbl::hlsl::numeric_limits::max; - nbl::hlsl::shapes::Line lineSegment = nbl::hlsl::shapes::Line::construct(float2(p0.x, p0.y), float2(p1.x, p1.y)); - distance = ClippedSignedDistance >::sdf(lineSegment, fragPos, outlineThickness, outlineStyle.isRoadStyleFlag); - - minDistance = min(minDistance, distance); - } - } - else - { - for (int i = 0; i < 3; ++i) - { - float3 p0 = v[i]; - float3 p1 = v[(i + 1) % 3]; - - // long story short, in order for stipple patterns to be consistent: - // - point with lesser x coord should be starting point - // - if x coord of both points are equal then point with lesser y value should be starting point - if (p1.x < p0.x) - nbl::hlsl::swap(p0, p1); - else if (p1.x == p0.x && p1.y < p0.y) - nbl::hlsl::swap(p0, p1); - - nbl::hlsl::shapes::Line lineSegment = nbl::hlsl::shapes::Line::construct(float2(p0.x, p0.y), float2(p1.x, p1.y)); - - float distance = nbl::hlsl::numeric_limits::max; - nbl::hlsl::shapes::Line::ArcLengthCalculator arcLenCalc = nbl::hlsl::shapes::Line::ArcLengthCalculator::construct(lineSegment); - LineStyleClipper clipper = LineStyleClipper::construct(outlineStyle, lineSegment, arcLenCalc, phaseShift, stretch, worldToScreenRatio); - distance = ClippedSignedDistance, LineStyleClipper>::sdf(lineSegment, fragPos, outlineThickness, outlineStyle.isRoadStyleFlag, clipper); - - minDistance = min(minDistance, distance); - } - } - - outputColor.a = 1.0f - smoothstep(-globals.antiAliasingFactor, globals.antiAliasingFactor, minDistance); - outputColor.a *= outlineStyle.color.a; - outputColor.rgb = outlineStyle.color.rgb; - - return outputColor; -} - -// TODO: -// It's literally sdf with a line shape -// so it should be moved somewhere else and used for every line maybe -float calculateLineSDF(in LineStyle lineStyle, in float worldToScreenRatio, in nbl::hlsl::shapes::Line lineSegment, in float2 fragPos, in float phaseShift) -{ - const float outlineThickness = (lineStyle.screenSpaceLineWidth + lineStyle.worldSpaceLineWidth / worldToScreenRatio) * 0.5f; - const float stretch = 1.0f; - - float minDistance = nbl::hlsl::numeric_limits::max; - if (!lineStyle.hasStipples() || stretch == InvalidStyleStretchValue) - { - float distance = nbl::hlsl::numeric_limits::max; - distance = ClippedSignedDistance >::sdf(lineSegment, fragPos, outlineThickness, lineStyle.isRoadStyleFlag); - minDistance = min(minDistance, distance); - } - else - { - float distance = nbl::hlsl::numeric_limits::max; - nbl::hlsl::shapes::Line::ArcLengthCalculator arcLenCalc = nbl::hlsl::shapes::Line::ArcLengthCalculator::construct(lineSegment); - LineStyleClipper clipper = LineStyleClipper::construct(lineStyle, lineSegment, arcLenCalc, phaseShift, stretch, worldToScreenRatio); - distance = ClippedSignedDistance, LineStyleClipper>::sdf(lineSegment, fragPos, outlineThickness, lineStyle.isRoadStyleFlag, clipper); - - minDistance = min(minDistance, distance); - } - - return minDistance; -} - -float4 blendUnder(in float4 dstColor, in float4 srcColor) -{ - dstColor.rgb = dstColor.rgb + (1 - dstColor.a) * srcColor.a * srcColor.rgb; - dstColor.a = (1.0f - srcColor.a) * dstColor.a + srcColor.a; - - return dstColor; -} - -E_CELL_DIAGONAL resolveGridDTMCellDiagonal(in uint32_t4 cellData) -{ - float4 cellHeights = asfloat(cellData); - - const bool4 invalidHeights = bool4( - isInvalidGridDtmHeightValue(cellHeights.x), - isInvalidGridDtmHeightValue(cellHeights.y), - isInvalidGridDtmHeightValue(cellHeights.z), - isInvalidGridDtmHeightValue(cellHeights.w) - ); - - int invalidHeightsCount = 0; - for (int i = 0; i < 4; ++i) - invalidHeightsCount += int(invalidHeights[i]); - - if (invalidHeightsCount == 0) - return getDiagonalModeFromCellCornerData(cellData.w); - - if (invalidHeightsCount > 1) - return INVALID; - - if (invalidHeights.x || invalidHeights.z) - return TOP_LEFT_TO_BOTTOM_RIGHT; - else if (invalidHeights.y || invalidHeights.w) - return BOTTOM_LEFT_TO_TOP_RIGHT; - - return INVALID; -} - -struct GridDTMTriangle -{ - float3 vertices[3]; -}; - -/** -* grid consists of square cells and cells are divided into two triangles: -* depending on mode it is -* either: or: -* v2a-------v1 v0-------v2b -* | A / | | \ B | -* | / | | \ | -* | / B | | A \ | -* v0-------v2b v2a-------v1 -*/ -struct GridDTMCell -{ - GridDTMTriangle triangleA; - GridDTMTriangle triangleB; - bool validA; - bool validB; -}; - -struct GridDTMHeightMapData -{ - // heihts.x - bottom left texel - // heihts.y - bottom right texel - // heihts.z - top right texel - // heihts.w - top left texel - float4 heights; - E_CELL_DIAGONAL cellDiagonal; -}; - -GridDTMHeightMapData retrieveGridDTMCellDataFromHeightMap(in float2 gridDimensions, in float2 cellCoords, in Texture2D heightMap) -{ - GridDTMHeightMapData output; - - const float2 location = (cellCoords + float2(0.5f, 0.5f)) / gridDimensions; - uint32_t4 cellData = heightMap.Gather(textureSampler, float2(location.x, location.y), 0); - - // printf("%u %u %u %u", cellData.x, cellData.y, cellData.z, cellData.w); - - output.heights = asfloat(cellData); - output.cellDiagonal = dtm::resolveGridDTMCellDiagonal(cellData); - return output; -} - -GridDTMCell calculateCellTriangles(in dtm::GridDTMHeightMapData heightData, in float2 cellCoords, const float cellWidth) -{ - GridDTMCell output; - - // heightData.heihts.x - bottom left texel - // heightData.heihts.y - bottom right texel - // heightData.heihts.z - top right texel - // heightData.heihts.w - top left texel - float2 gridSpaceCellTopLeftCoords = cellCoords * cellWidth; - - if (heightData.cellDiagonal == E_CELL_DIAGONAL::TOP_LEFT_TO_BOTTOM_RIGHT) - { - output.triangleA.vertices[0] = float3(gridSpaceCellTopLeftCoords.x, gridSpaceCellTopLeftCoords.y, heightData.heights.w); - output.triangleA.vertices[1] = float3(gridSpaceCellTopLeftCoords.x + cellWidth, gridSpaceCellTopLeftCoords.y + cellWidth, heightData.heights.y); - output.triangleA.vertices[2] = float3(gridSpaceCellTopLeftCoords.x, gridSpaceCellTopLeftCoords.y + cellWidth, heightData.heights.x); - - output.triangleB.vertices[0] = float3(gridSpaceCellTopLeftCoords.x, gridSpaceCellTopLeftCoords.y, heightData.heights.w); - output.triangleB.vertices[1] = float3(gridSpaceCellTopLeftCoords.x + cellWidth, gridSpaceCellTopLeftCoords.y + cellWidth, heightData.heights.y); - output.triangleB.vertices[2] = float3(gridSpaceCellTopLeftCoords.x + cellWidth, gridSpaceCellTopLeftCoords.y, heightData.heights.z); - } - else - { - output.triangleA.vertices[0] = float3(gridSpaceCellTopLeftCoords.x, gridSpaceCellTopLeftCoords.y + cellWidth, heightData.heights.x); - output.triangleA.vertices[1] = float3(gridSpaceCellTopLeftCoords.x + cellWidth, gridSpaceCellTopLeftCoords.y, heightData.heights.z); - output.triangleA.vertices[2] = float3(gridSpaceCellTopLeftCoords.x, gridSpaceCellTopLeftCoords.y, heightData.heights.w); - - output.triangleB.vertices[0] = float3(gridSpaceCellTopLeftCoords.x, gridSpaceCellTopLeftCoords.y + cellWidth, heightData.heights.x); - output.triangleB.vertices[1] = float3(gridSpaceCellTopLeftCoords.x + cellWidth, gridSpaceCellTopLeftCoords.y, heightData.heights.z); - output.triangleB.vertices[2] = float3(gridSpaceCellTopLeftCoords.x + cellWidth, gridSpaceCellTopLeftCoords.y + cellWidth, heightData.heights.y); - } - - output.validA = !isInvalidGridDtmHeightValue(output.triangleA.vertices[0].z) && !isInvalidGridDtmHeightValue(output.triangleA.vertices[1].z) && !isInvalidGridDtmHeightValue(output.triangleA.vertices[2].z); - output.validB = !isInvalidGridDtmHeightValue(output.triangleB.vertices[0].z) && !isInvalidGridDtmHeightValue(output.triangleB.vertices[1].z) && !isInvalidGridDtmHeightValue(output.triangleB.vertices[2].z); - - return output; -} - -} - -#endif \ No newline at end of file diff --git a/62_CAD/shaders/main_pipeline/fragment_shader.hlsl b/62_CAD/shaders/main_pipeline/fragment_shader.hlsl deleted file mode 100644 index d4222d229..000000000 --- a/62_CAD/shaders/main_pipeline/fragment_shader.hlsl +++ /dev/null @@ -1,715 +0,0 @@ -#define FRAGMENT_SHADER_INPUT -#include "common.hlsl" -#include "dtm.hlsl" -#include -#include -#include -#include -#include -#include -#include -//#include - -// sdf of Isosceles Trapezoid y-aligned by https://iquilezles.org/articles/distfunctions2d/ -// Trapezoid centered around origin (0,0), the top edge has length r2, the bottom edge has length r1, the height of the trapezoid is he*2.0 -float sdTrapezoid(float2 p, float r1, float r2, float he) -{ - float2 k1 = float2(r2, he); - float2 k2 = float2(r2 - r1, 2.0 * he); - - p.x = abs(p.x); - float2 ca = float2(max(0.0, p.x - ((p.y < 0.0) ? r1 : r2)), abs(p.y) - he); - float2 cb = p - k1 + k2 * clamp(dot(k1 - p, k2) / dot(k2,k2), 0.0, 1.0); - - float s = (cb.x < 0.0 && ca.y < 0.0) ? -1.0 : 1.0; - - return s * sqrt(min(dot(ca,ca), dot(cb,cb))); -} - -// line segment sdf which returns the distance vector specialized for usage in hatch box line boundaries -float2 sdLineDstVec(float2 P, float2 A, float2 B) -{ - const float2 PA = P - A; - const float2 BA = B - A; - float h = clamp(dot(PA, BA) / dot(BA, BA), 0.0, 1.0); - return PA - BA * h; -} - -/* - XXXXXXX b XXXXXX Long Base (len = rb) - X X - X X - X X - X XXXXXXXXXXX X - X XXXX | XXXX X - XXX | XXXX - XX | XX - XX | XX - XX | XX - XX T Trapz Center XX (2) p.y = 0 after p.y = p.y - halfHeight + radius - XX | XX - X X C Circle Center X (1) p = (0,0) at circle center - X X | X - X X | X X - X X | X X - X X | X X - X XX | XX X - X XXX | XXX X -X XXXX | XXXX X -XXXXXXXXXXXXXXXXXXXXXXXXX a XXXXXXXXXXXXXXXXXXXXX Short Base (len = ra) -*/ -// p is in circle's space (the circle centered at line intersection and radius = thickness) -// a and b are points at each trapezoid base (short and long base) -// TODO[Optimization] we can probably send less info, since we only use length of b-a and the normalize vector -float miterSDF(float2 p, float thickness, float2 a, float2 b, float ra, float rb) -{ - float halfHeight = length(b - a) / 2.0; - float2 d = normalize(b - a); - float2x2 rot = float2x2(d.y, -d.x, d.x, d.y); - p = mul(rot, p); // rotate(change of basis) such that the point is now in the space where trapezoid is y-axis aligned, see (1) above - p.y = p.y - halfHeight + thickness; // see (2) above - return sdTrapezoid(p, ra, rb, halfHeight); -} - -// We need to specialize color calculation based on FragmentShaderInterlock feature availability for our transparency algorithm -// because there is no `if constexpr` in hlsl -// @params -// textureColor: color sampled from a texture -// useStyleColor: instead of writing and reading from colorStorage, use main object Idx to find the style color for the object. -template -float32_t4 calculateFinalColor(const uint2 fragCoord, const float localAlpha, const uint32_t currentMainObjectIdx, float3 textureColor, bool colorFromTexture); - -template<> -float32_t4 calculateFinalColor(const uint2 fragCoord, const float localAlpha, const uint32_t currentMainObjectIdx, float3 localTextureColor, bool colorFromTexture) -{ - float32_t4 color; - uint32_t styleIdx = loadMainObject(currentMainObjectIdx).styleIdx; - if (!colorFromTexture) - { - color = loadLineStyle(styleIdx).color; - color.w *= localAlpha; - } - else - color = float4(localTextureColor, localAlpha); - - color.rgb *= color.a; - return color; -} -template<> -float32_t4 calculateFinalColor(const uint2 fragCoord, const float localAlpha, const uint32_t currentMainObjectIdx, float3 localTextureColor, bool colorFromTexture) -{ - float32_t4 color; - nbl::hlsl::spirv::beginInvocationInterlockEXT(); - - const uint32_t packedData = pseudoStencil[fragCoord]; - - const uint32_t localQuantizedAlpha = (uint32_t)(localAlpha * 255.f); - const uint32_t storedQuantizedAlpha = nbl::hlsl::glsl::bitfieldExtract(packedData,0,AlphaBits); - const uint32_t storedMainObjectIdx = nbl::hlsl::glsl::bitfieldExtract(packedData,AlphaBits,MainObjectIdxBits); - // if geomID has changed, we resolve the SDF alpha (draw using blend), else accumulate - const bool differentMainObject = currentMainObjectIdx != storedMainObjectIdx; // meaning current pixel's main object is different than what is already stored - const bool resolve = differentMainObject && storedMainObjectIdx != InvalidMainObjectIdx; - uint32_t toResolveStyleIdx = InvalidStyleIdx; - - // load from colorStorage only if we want to resolve color from texture instead of style - // sampling from colorStorage needs to happen in critical section because another fragment may also want to store into it at the same time + need to happen before store - if (resolve) - { - toResolveStyleIdx = loadMainObject(storedMainObjectIdx).styleIdx; - if (toResolveStyleIdx == InvalidStyleIdx) // if style idx to resolve is invalid, then it means we should resolve from color - color = float32_t4(unpackR11G11B10_UNORM(colorStorage[fragCoord]), 1.0f); - } - - // If current localAlpha is higher than what is already stored in pseudoStencil we will update the value in pseudoStencil or the color in colorStorage, this is equivalent to programmable blending MAX operation. - // OR If previous pixel has a different ID than current's (i.e. previous either empty/invalid or a differnet mainObject), we should update our alpha and color storages. - if (differentMainObject || localQuantizedAlpha > storedQuantizedAlpha) - { - pseudoStencil[fragCoord] = nbl::hlsl::glsl::bitfieldInsert(localQuantizedAlpha,currentMainObjectIdx,AlphaBits,MainObjectIdxBits); - if (colorFromTexture) // writing color from texture - colorStorage[fragCoord] = packR11G11B10_UNORM(localTextureColor); - } - - nbl::hlsl::spirv::endInvocationInterlockEXT(); - - if (!resolve) - discard; - - // draw with previous geometry's style's color or stored in texture buffer :kek: - // we don't need to load the style's color in critical section because we've already retrieved the style index from the stored main obj - if (toResolveStyleIdx != InvalidStyleIdx) // if toResolveStyleIdx is valid then that means our resolved color should come from line style - { - color = loadLineStyle(toResolveStyleIdx).color; - gammaUncorrect(color.rgb); // want to output to SRGB without gamma correction - } - - color.a *= float(storedQuantizedAlpha) / 255.f; - - color.rgb *= color.a; - return color; -} - -bool isLineValid(in nbl::hlsl::shapes::Line l) -{ - bool isAnyLineComponentNaN = any(bool4(isnan(l.P0.x), isnan(l.P0.y), isnan(l.P1.x), isnan(l.P1.y))); - if (isAnyLineComponentNaN) - return false; - return true; -} - -[[vk::spvexecutionmode(spv::ExecutionModePixelInterlockOrderedEXT)]] -[shader("pixel")] -float4 fragMain(PSInput input) : SV_TARGET -{ - float localAlpha = 0.0f; - float3 textureColor = float3(0, 0, 0); // color sampled from a texture - - ObjectType objType = input.getObjType(); - const uint32_t currentMainObjectIdx = input.getMainObjectIdx(); - const MainObject mainObj = loadMainObject(currentMainObjectIdx); - float worldToScreenRatio = input.getCurrentWorldToScreenRatio(); - - if (pc.isDTMRendering) - { - DTMSettings dtmSettings = loadDTMSettings(mainObj.dtmSettingsIdx); - - float3 triangleVertices[3]; - triangleVertices[0] = input.getScreenSpaceVertexAttribs(0); - triangleVertices[1] = input.getScreenSpaceVertexAttribs(1); - triangleVertices[2] = input.getScreenSpaceVertexAttribs(2); - - const float3 baryCoord = dtm::calculateDTMTriangleBarycentrics(triangleVertices[0].xy, triangleVertices[1].xy, triangleVertices[2].xy, input.position.xy); - - float height = baryCoord.x * triangleVertices[0].z + baryCoord.y * triangleVertices[1].z + baryCoord.z * triangleVertices[2].z; - float heightDeriv = fwidth(height); - - float4 dtmColor = float4(0.0f, 0.0f, 0.0f, 0.0f); - - if (dtmSettings.drawOutlineEnabled()) // TODO: do i need 'height' paramter here? - dtmColor = dtm::blendUnder(dtmColor, dtm::calculateDTMOutlineColor(dtmSettings.outlineLineStyleIdx, worldToScreenRatio, triangleVertices, input.position.xy)); - if (dtmSettings.drawContourEnabled()) - { - for(uint32_t i = 0; i < dtmSettings.contourSettingsCount; ++i) // TODO: should reverse the order with blendUnder - { - LineStyle contourStyle = loadLineStyle(dtmSettings.contourSettings[i].contourLineStyleIdx); - float sdf = dtm::calculateDTMContourSDF(dtmSettings.contourSettings[i], contourStyle, worldToScreenRatio, triangleVertices, input.position.xy, height); - float4 contourColor = contourStyle.color; - contourColor.a *= 1.0f - smoothstep(-globals.antiAliasingFactor, globals.antiAliasingFactor, sdf); - dtmColor = dtm::blendUnder(dtmColor, contourColor); - } - } - if (dtmSettings.drawHeightShadingEnabled()) - dtmColor = dtm::blendUnder(dtmColor, dtm::calculateDTMHeightColor(dtmSettings.heightShadingSettings, triangleVertices, heightDeriv, input.position.xy, height)); - - textureColor = dtmColor.rgb / dtmColor.a; - localAlpha = dtmColor.a; - - // because final color is premultiplied by alpha - textureColor = dtmColor.rgb / dtmColor.a; - - gammaUncorrect(textureColor); // want to output to SRGB without gamma correction - return calculateFinalColor(uint2(input.position.xy), localAlpha, currentMainObjectIdx, textureColor, true); - } - else - { - // figure out local alpha with sdf - if (objType == ObjectType::LINE || objType == ObjectType::QUAD_BEZIER || objType == ObjectType::POLYLINE_CONNECTOR) - { - float distance = nbl::hlsl::numeric_limits::max; - const float thickness = input.getLineThickness(); - - if (objType == ObjectType::LINE) - { - const float2 start = input.getLineStart(); - const float2 end = input.getLineEnd(); - const uint32_t styleIdx = mainObj.styleIdx; - const float phaseShift = input.getCurrentPhaseShift(); - const float stretch = input.getPatternStretch(); - - nbl::hlsl::shapes::Line lineSegment = nbl::hlsl::shapes::Line::construct(start, end); - - LineStyle style = loadLineStyle(styleIdx); - - if (!style.hasStipples() || stretch == InvalidStyleStretchValue) - { - distance = ClippedSignedDistance< nbl::hlsl::shapes::Line >::sdf(lineSegment, input.position.xy, thickness, style.isRoadStyleFlag); - } - else - { - nbl::hlsl::shapes::Line::ArcLengthCalculator arcLenCalc = nbl::hlsl::shapes::Line::ArcLengthCalculator::construct(lineSegment); - LineStyleClipper clipper = LineStyleClipper::construct(loadLineStyle(styleIdx), lineSegment, arcLenCalc, phaseShift, stretch, worldToScreenRatio); - distance = ClippedSignedDistance, LineStyleClipper>::sdf(lineSegment, input.position.xy, thickness, style.isRoadStyleFlag, clipper); - } - } - else if (objType == ObjectType::QUAD_BEZIER) - { - nbl::hlsl::shapes::Quadratic quadratic = input.getQuadratic(); - nbl::hlsl::shapes::Quadratic::ArcLengthCalculator arcLenCalc = input.getQuadraticArcLengthCalculator(); - - const uint32_t styleIdx = mainObj.styleIdx; - const float phaseShift = input.getCurrentPhaseShift(); - const float stretch = input.getPatternStretch(); - - LineStyle style = loadLineStyle(styleIdx); - if (!style.hasStipples() || stretch == InvalidStyleStretchValue) - { - distance = ClippedSignedDistance< nbl::hlsl::shapes::Quadratic >::sdf(quadratic, input.position.xy, thickness, style.isRoadStyleFlag); - } - else - { - BezierStyleClipper clipper = BezierStyleClipper::construct(loadLineStyle(styleIdx), quadratic, arcLenCalc, phaseShift, stretch, worldToScreenRatio ); - distance = ClippedSignedDistance, BezierStyleClipper>::sdf(quadratic, input.position.xy, thickness, style.isRoadStyleFlag, clipper); - } - } - else if (objType == ObjectType::POLYLINE_CONNECTOR) - { - const float2 P = input.position.xy - input.getPolylineConnectorCircleCenter(); - distance = miterSDF( - P, - thickness, - input.getPolylineConnectorTrapezoidStart(), - input.getPolylineConnectorTrapezoidEnd(), - input.getPolylineConnectorTrapezoidLongBase(), - input.getPolylineConnectorTrapezoidShortBase()); - - } - - const bool lineAA = thickness >= globals.minLineThicknessToEnableAA; - if (lineAA) - localAlpha = 1.0f - smoothstep(-globals.antiAliasingFactor, globals.antiAliasingFactor, distance); - else - localAlpha = (distance < 0.0f) ? 1.0f : 0.0f; - } - else if (objType == ObjectType::CURVE_BOX) - { - const float minorBBoxUV = input.getMinorBBoxUV(); - const float majorBBoxUV = input.getMajorBBoxUV(); - - nbl::hlsl::math::equations::Quadratic curveMinMinor = input.getCurveMinMinor(); - nbl::hlsl::math::equations::Quadratic curveMinMajor = input.getCurveMinMajor(); - nbl::hlsl::math::equations::Quadratic curveMaxMinor = input.getCurveMaxMinor(); - nbl::hlsl::math::equations::Quadratic curveMaxMajor = input.getCurveMaxMajor(); - - // TODO(Optimization): Can we ignore this majorBBoxUV clamp and rely on the t clamp that happens next? then we can pass `PrecomputedRootFinder`s instead of computing the values per pixel. - nbl::hlsl::math::equations::Quadratic minCurveEquation = nbl::hlsl::math::equations::Quadratic::construct(curveMinMajor.a, curveMinMajor.b, curveMinMajor.c - clamp(majorBBoxUV, 0.0, 1.0)); - nbl::hlsl::math::equations::Quadratic maxCurveEquation = nbl::hlsl::math::equations::Quadratic::construct(curveMaxMajor.a, curveMaxMajor.b, curveMaxMajor.c - clamp(majorBBoxUV, 0.0, 1.0)); - - const float minT = clamp(PrecomputedRootFinder::construct(minCurveEquation).computeRoots(), 0.0, 1.0); - const float minEv = curveMinMinor.evaluate(minT); - - const float maxT = clamp(PrecomputedRootFinder::construct(maxCurveEquation).computeRoots(), 0.0, 1.0); - const float maxEv = curveMaxMinor.evaluate(maxT); - - const bool insideMajor = majorBBoxUV >= 0.0 && majorBBoxUV <= 1.0; - const bool insideMinor = minorBBoxUV >= minEv && minorBBoxUV <= maxEv; - - if (insideMinor && insideMajor) - { - localAlpha = 1.0; - } - else - { - // Find the true SDF of a hatch box boundary which is bounded by two curves, It requires knowing the distance from the current UV to the closest point on bounding curves and the limiting lines (in major direction) - // We also keep track of distance vector (minor, major) to convert to screenspace distance for anti-aliasing with screenspace aaFactor - const float InvalidT = nbl::hlsl::numeric_limits::max; - const float MAX_DISTANCE_SQUARED = nbl::hlsl::numeric_limits::max; - - const float2 boxScreenSpaceSize = input.getCurveBoxScreenSpaceSize(); - - - float closestDistanceSquared = MAX_DISTANCE_SQUARED; - const float2 pos = float2(minorBBoxUV, majorBBoxUV) * boxScreenSpaceSize; - - if (minorBBoxUV < minEv) - { - // DO SDF of Min Curve - nbl::hlsl::shapes::Quadratic minCurve = nbl::hlsl::shapes::Quadratic::construct( - float2(curveMinMinor.a, curveMinMajor.a) * boxScreenSpaceSize, - float2(curveMinMinor.b, curveMinMajor.b) * boxScreenSpaceSize, - float2(curveMinMinor.c, curveMinMajor.c) * boxScreenSpaceSize); - - nbl::hlsl::shapes::Quadratic::Candidates candidates = minCurve.getClosestCandidates(pos); - [[unroll(nbl::hlsl::shapes::Quadratic::MaxCandidates)]] - for (uint32_t i = 0; i < nbl::hlsl::shapes::Quadratic::MaxCandidates; i++) - { - candidates[i] = clamp(candidates[i], 0.0, 1.0); - const float2 distVector = minCurve.evaluate(candidates[i]) - pos; - const float candidateDistanceSquared = dot(distVector, distVector); - if (candidateDistanceSquared < closestDistanceSquared) - closestDistanceSquared = candidateDistanceSquared; - } - } - else if (minorBBoxUV > maxEv) - { - // Do SDF of Max Curve - nbl::hlsl::shapes::Quadratic maxCurve = nbl::hlsl::shapes::Quadratic::construct( - float2(curveMaxMinor.a, curveMaxMajor.a) * boxScreenSpaceSize, - float2(curveMaxMinor.b, curveMaxMajor.b) * boxScreenSpaceSize, - float2(curveMaxMinor.c, curveMaxMajor.c) * boxScreenSpaceSize); - nbl::hlsl::shapes::Quadratic::Candidates candidates = maxCurve.getClosestCandidates(pos); - [[unroll(nbl::hlsl::shapes::Quadratic::MaxCandidates)]] - for (uint32_t i = 0; i < nbl::hlsl::shapes::Quadratic::MaxCandidates; i++) - { - candidates[i] = clamp(candidates[i], 0.0, 1.0); - const float2 distVector = maxCurve.evaluate(candidates[i]) - pos; - const float candidateDistanceSquared = dot(distVector, distVector); - if (candidateDistanceSquared < closestDistanceSquared) - closestDistanceSquared = candidateDistanceSquared; - } - } - - if (!insideMajor) - { - const bool minLessThanMax = minEv < maxEv; - float2 majorDistVector = float2(MAX_DISTANCE_SQUARED, MAX_DISTANCE_SQUARED); - if (majorBBoxUV > 1.0) - { - const float2 minCurveEnd = float2(minEv, 1.0) * boxScreenSpaceSize; - if (minLessThanMax) - majorDistVector = sdLineDstVec(pos, minCurveEnd, float2(maxEv, 1.0) * boxScreenSpaceSize); - else - majorDistVector = pos - minCurveEnd; - } - else - { - const float2 minCurveStart = float2(minEv, 0.0) * boxScreenSpaceSize; - if (minLessThanMax) - majorDistVector = sdLineDstVec(pos, minCurveStart, float2(maxEv, 0.0) * boxScreenSpaceSize); - else - majorDistVector = pos - minCurveStart; - } - - const float majorDistSq = dot(majorDistVector, majorDistVector); - if (majorDistSq < closestDistanceSquared) - closestDistanceSquared = majorDistSq; - } - - const float dist = sqrt(closestDistanceSquared); - localAlpha = 1.0f - smoothstep(0.0, globals.antiAliasingFactor, dist); - } - - LineStyle style = loadLineStyle(mainObj.styleIdx); - uint32_t textureId = asuint(style.screenSpaceLineWidth); - if (textureId != InvalidTextureIndex) - { - // For Hatch fiils we sample the first mip as we don't fill the others, because they are constant in screenspace and render as expected - // If later on we decided that we can have different sizes here, we should do computations similar to FONT_GLYPH - float3 msdfSample = msdfTextures.SampleLevel(msdfSampler, float3(frac(input.position.xy / HatchFillMSDFSceenSpaceSize), float(textureId)), 0.0).xyz; - float msdf = nbl::hlsl::text::msdfDistance(msdfSample, MSDFPixelRange * HatchFillMSDFSceenSpaceSize / MSDFSize); - localAlpha *= 1.0f - smoothstep(-globals.antiAliasingFactor / 2.0f, globals.antiAliasingFactor / 2.0f, msdf); - } - } - else if (objType == ObjectType::FONT_GLYPH) - { - const float2 uv = input.getFontGlyphUV(); - const uint32_t textureId = input.getFontGlyphTextureId(); - - if (textureId != InvalidTextureIndex) - { - float mipLevel = msdfTextures.CalculateLevelOfDetail(msdfSampler, uv); - float3 msdfSample = msdfTextures.SampleLevel(msdfSampler, float3(uv, float(textureId)), mipLevel); - float msdf = nbl::hlsl::text::msdfDistance(msdfSample, input.getFontGlyphPxRange()); - /* - explaining "*= exp2(max(mipLevel,0.0))" - Each mip level has constant MSDFPixelRange - Which essentially makes the msdfSamples here (Harware Sampled) have different scales per mip - As we go up 1 mip level, the msdf distance should be multiplied by 2.0 - While this makes total sense for NEAREST mip sampling when mipLevel is an integer and only one mip is being sampled. - It's a bit complex when it comes to trilinear filtering (LINEAR mip sampling), but it works in practice! - - Alternatively you can think of it as doing this instead: - localAlpha = smoothstep(+globals.antiAliasingFactor / exp2(max(mipLevel,0.0)), 0.0, msdf); - Which is reducing the aa feathering as we go up the mip levels. - to avoid aa feathering of the MAX_MSDF_DISTANCE_VALUE to be less than aa factor and eventually color it and cause greyed out area around the main glyph - */ - msdf *= exp2(max(mipLevel,0.0)); - - LineStyle style = loadLineStyle(mainObj.styleIdx); - const float screenPxRange = input.getFontGlyphPxRange() / MSDFPixelRangeHalf; - const float bolden = style.worldSpaceLineWidth * screenPxRange; // worldSpaceLineWidth is actually boldenInPixels, aliased TextStyle with LineStyle - localAlpha = 1.0f - smoothstep(-globals.antiAliasingFactor / 2.0f + bolden, globals.antiAliasingFactor / 2.0f + bolden, msdf); - } - } - else if (objType == ObjectType::STATIC_IMAGE) - { - const float2 uv = input.getImageUV(); - const uint32_t textureId = input.getImageTextureId(); - - if (textureId != InvalidTextureIndex) - { - float4 colorSample = textures[NonUniformResourceIndex(textureId)].Sample(textureSampler, float2(uv.x, uv.y)); - textureColor = colorSample.rgb; - localAlpha = colorSample.a; - } - } - else if (objType == ObjectType::GRID_DTM) - { - DTMSettings dtmSettings = loadDTMSettings(mainObj.dtmSettingsIdx); - - if (!dtmSettings.drawContourEnabled() && !dtmSettings.drawOutlineEnabled() && !dtmSettings.drawHeightShadingEnabled()) - discard; - - float2 uv = input.getImageUV(); - const uint32_t textureId = input.getGridDTMHeightTextureID(); - - float2 gridExtents = input.getGridDTMScreenSpaceGridExtents(); - const float cellWidth = input.getGridDTMScreenSpaceCellWidth(); - // TODO: I think we can get it from the height map size if texture is valid?!, better if it comes directly from CPU side, vertex shader or something, division + round to integer is error-prone for large integer values - float2 gridDimensions = round(gridExtents / cellWidth); // texturesU32[NonUniformResourceIndex(textureId)].GetDimensions()? - - float2 gridSpacePos = uv * gridExtents; - float2 gridSpacePosDivGridCellWidth = gridSpacePos / cellWidth; - float2 currentCellCoord; - { - currentCellCoord.x = floor(gridSpacePosDivGridCellWidth.x); - currentCellCoord.y = floor(gridSpacePosDivGridCellWidth.y); - } - - // grid consists of square cells and cells are divided into two triangles: - // depending on mode it is - // either: or: - // v2a-------v1 v0-------v2b - // | A / | | \ B | - // | / | | \ | - // | / B | | A \ | - // v0-------v2b v2a-------v1 - // - - const bool gridOnly = textureId == InvalidTextureIndex && dtmSettings.drawOutlineEnabled(); - if (gridOnly) - { - nbl::hlsl::shapes::Line outlineLineSegments[2]; - - const float halfCellWidth = cellWidth * 0.5f; - const float2 horizontalBounds = float2(0.0f, gridExtents.y); - const float2 verticalBounds = float2(0.0f, gridExtents.x); - float2 nearestLineRemainingCoords = int2((gridSpacePos + halfCellWidth) / cellWidth) * cellWidth; - // shift lines outside of the grid to a bound - nearestLineRemainingCoords.x = clamp(nearestLineRemainingCoords.x, verticalBounds.x, verticalBounds.y); - nearestLineRemainingCoords.y = clamp(nearestLineRemainingCoords.y, horizontalBounds.x, horizontalBounds.y); - - // find the nearest horizontal line - outlineLineSegments[0].P0 = float32_t2(verticalBounds.x, nearestLineRemainingCoords.y); - outlineLineSegments[0].P1 = float32_t2(verticalBounds.y, nearestLineRemainingCoords.y); - // find the nearest vertical line - outlineLineSegments[1].P0 = float32_t2(nearestLineRemainingCoords.x, horizontalBounds.x); - outlineLineSegments[1].P1 = float32_t2(nearestLineRemainingCoords.x, horizontalBounds.y); - - LineStyle outlineStyle = loadLineStyle(dtmSettings.outlineLineStyleIdx); - float sdf = dtm::calculateLineSDF(outlineStyle, worldToScreenRatio, outlineLineSegments[0], gridSpacePos, 0.0f); - sdf = min(sdf, dtm::calculateLineSDF(outlineStyle, worldToScreenRatio, outlineLineSegments[1], gridSpacePos, 0.0f)); - - float4 dtmColor = outlineStyle.color; - dtmColor.a *= 1.0f - smoothstep(-globals.antiAliasingFactor, globals.antiAliasingFactor, sdf); - - textureColor = dtmColor.rgb; - localAlpha = dtmColor.a; - } - else - { - // calculate localUV and figure out the 4 cells we're gonna do sdf with - float2 localUV = gridSpacePosDivGridCellWidth - currentCellCoord; // TODO: use fmod instead? - int2 roundedLocalUV = round(localUV); - float2 offset = roundedLocalUV * 2.0f - 1.0f; - - // Triangles - const uint32_t MaxTrianglesToDoSDFWith = 8u; - dtm::GridDTMTriangle triangles[MaxTrianglesToDoSDFWith]; - float interpolatedHeights[MaxTrianglesToDoSDFWith]; // these are height based on barycentric interpolation of current pixel with all the triangles above - uint32_t triangleCount = 0u; - - // We can do sdf for up to 4 maximum lines for the outlines, 2 belong to the current cell and the other 2 belong to the opposite neighbouring cell - /* Example: - | - | opposite cell - | - ------+------ - | - current cell | - | - - `+` is the current corner and we draw the 4 lines leading up to it. - */ - - // curr cell horizontal, curr cell vertical, opposite cell horizontal, opposite cell vertical - bool4 linesValidity = bool4(false, false, false, false); - - [unroll] - for (int i = 0; i < 2; ++i) - { - for (int j = 0; j < 2; ++j) - { - float2 cellCoord = currentCellCoord + float2(i, j) * offset; - const bool isCellWithinRange = - cellCoord.x >= 0.0f && cellCoord.y >= 0.0f && - cellCoord.x < gridDimensions.x && cellCoord.y < gridDimensions.y; - if (isCellWithinRange) - { - dtm::GridDTMHeightMapData heightData = dtm::retrieveGridDTMCellDataFromHeightMap(gridDimensions, cellCoord, texturesU32[NonUniformResourceIndex(textureId)]); - dtm::GridDTMCell gridCellFormed = dtm::calculateCellTriangles(heightData, cellCoord, cellWidth); - if (gridCellFormed.validA) - triangles[triangleCount++] = gridCellFormed.triangleA; - if (gridCellFormed.validB) - triangles[triangleCount++] = gridCellFormed.triangleB; - - // we just need to check and set lines validity - // Formulas to get current cell's horizontal and vertical lines validity - // All this to avoid extra texel fetch to check validity and use the Gather result instead :D - // TODO: Only 0,0 and 1,1 is enough to check if cells are valid, but other checks required in case current cell is invalid (out of bounds) but it's line is valid - if (i == 0 && j == 0) - { - // current cell's line validity - linesValidity[0] = !isInvalidGridDtmHeightValue(heightData.heights[2 - (roundedLocalUV.y * 2)]) && !isInvalidGridDtmHeightValue(heightData.heights[3 - (roundedLocalUV.y * 2)]); - linesValidity[1] = !isInvalidGridDtmHeightValue(heightData.heights[roundedLocalUV.x ^ 0]) && !isInvalidGridDtmHeightValue(heightData.heights[roundedLocalUV.x ^ 3]); - } - if (i == 1 && j == 0) - { - linesValidity[1] = !isInvalidGridDtmHeightValue(heightData.heights[roundedLocalUV.x ^ 1]) && !isInvalidGridDtmHeightValue(heightData.heights[roundedLocalUV.x ^ 2]); - linesValidity[2] = !isInvalidGridDtmHeightValue(heightData.heights[2 - (roundedLocalUV.y * 2)]) && !isInvalidGridDtmHeightValue(heightData.heights[3 - (roundedLocalUV.y * 2)]);; - } - if (i == 0 && j == 1) - { - linesValidity[0] = !isInvalidGridDtmHeightValue(heightData.heights[roundedLocalUV.y * 2]) && !isInvalidGridDtmHeightValue(heightData.heights[roundedLocalUV.y * 2 + 1]); - linesValidity[3] = !isInvalidGridDtmHeightValue(heightData.heights[roundedLocalUV.x ^ 0]) && !isInvalidGridDtmHeightValue(heightData.heights[roundedLocalUV.x ^ 3]); - } - if (i == 1 && j == 1) - { - linesValidity[2] = !isInvalidGridDtmHeightValue(heightData.heights[roundedLocalUV.y * 2]) && !isInvalidGridDtmHeightValue(heightData.heights[roundedLocalUV.y * 2 + 1]); - linesValidity[3] = !isInvalidGridDtmHeightValue(heightData.heights[roundedLocalUV.x ^ 1]) && !isInvalidGridDtmHeightValue(heightData.heights[roundedLocalUV.x ^ 2]); - } - } - } - } - - const uint32_t InvalidTriangleIndex = nbl::hlsl::numeric_limits::max; - uint32_t currentTriangleIndex = InvalidTriangleIndex; - // For height shading, merge this loop with the previous one, because baryCoord all positive means point inside triangle and we can use that to figure out the triangle we want to do height shading for. - for (int t = 0; t < triangleCount; ++t) - { - dtm::GridDTMTriangle tri = triangles[t]; - const float3 baryCoord = dtm::calculateDTMTriangleBarycentrics(tri.vertices[0].xy, tri.vertices[1].xy, tri.vertices[2].xy, gridSpacePos); - interpolatedHeights[t] = baryCoord.x * tri.vertices[0].z + baryCoord.y * tri.vertices[1].z + baryCoord.z * tri.vertices[2].z; - - if (currentTriangleIndex == InvalidTriangleIndex) - { - const float minValue = 0.0f - nbl::hlsl::numeric_limits::epsilon; - const float maxValue = 1.0f + nbl::hlsl::numeric_limits::epsilon; - if (all(baryCoord >= minValue) && all(baryCoord <= maxValue)) - currentTriangleIndex = t; - } - } - - float4 dtmColor = float4(0.0f, 0.0f, 0.0f, 0.0f); - if (dtmSettings.drawContourEnabled()) - { - for (int i = dtmSettings.contourSettingsCount-1u; i >= 0; --i) - { - LineStyle contourStyle = loadLineStyle(dtmSettings.contourSettings[i].contourLineStyleIdx); - float sdf = nbl::hlsl::numeric_limits::max; - for (int t = 0; t < triangleCount; ++t) - { - const dtm::GridDTMTriangle tri = triangles[t]; - const float currentInterpolatedHeight = interpolatedHeights[t]; - sdf = min(sdf, dtm::calculateDTMContourSDF(dtmSettings.contourSettings[i], contourStyle, worldToScreenRatio, tri.vertices, gridSpacePos, currentInterpolatedHeight)); - } - - float4 contourColor = contourStyle.color; contourColor.a = 0.5f; - contourColor.a *= 1.0f - smoothstep(-globals.antiAliasingFactor, globals.antiAliasingFactor, sdf); - dtmColor = dtm::blendUnder(dtmColor, contourColor); - } - } - - if (dtmSettings.drawOutlineEnabled()) - { - float sdf = nbl::hlsl::numeric_limits::max; - LineStyle outlineStyle = loadLineStyle(dtmSettings.outlineLineStyleIdx); - nbl::hlsl::shapes::Line lineSegment; - - // Doing SDF of outlines as if cooridnate system is centered around the nearest corner of the cell - float2 localCellSpaceOrigin = (currentCellCoord + float2(roundedLocalUV)) * cellWidth; // in local cell space, origin - float2 localGridTopLeftCorner = -localCellSpaceOrigin; // top left in local cell space: topLeft is (0, 0) implicitly - float2 localFragPos = gridSpacePos - localCellSpaceOrigin; // we compute the current fragment pos, in local cell space - - float phaseShift = 0.0f; - const bool hasStipples = outlineStyle.hasStipples(); - const float rcpPattenLenScreenSpace = outlineStyle.reciprocalStipplePatternLen * worldToScreenRatio; - // Drawing the lines that form a plus sign around the current corner: - if (linesValidity[0]) - { - // this cells horizontal line - lineSegment.P0 = float2((offset.x > 0) ? -offset.x * cellWidth : 0.0f, 0.0f); - lineSegment.P1 = float2((offset.x < 0) ? -offset.x * cellWidth : 0.0f, 0.0f); - phaseShift = fract((lineSegment.P0.x - localGridTopLeftCorner.x) * rcpPattenLenScreenSpace); - sdf = min(sdf, dtm::calculateLineSDF(outlineStyle, worldToScreenRatio, lineSegment, localFragPos, phaseShift)); - } - if (linesValidity[1]) - { - // this cells vertical line - lineSegment.P0 = float2(0.0f, (offset.y > 0) ? -offset.y * cellWidth : 0.0f); - lineSegment.P1 = float2(0.0f, (offset.y < 0) ? -offset.y * cellWidth : 0.0f); - phaseShift = fract((lineSegment.P0.y - localGridTopLeftCorner.y) * rcpPattenLenScreenSpace); - sdf = min(sdf, dtm::calculateLineSDF(outlineStyle, worldToScreenRatio, lineSegment, localFragPos, phaseShift)); - } - if (linesValidity[2]) - { - // opposite cell horizontal line - lineSegment.P0 = float2((offset.x < 0) ? offset.x * cellWidth : 0.0f, 0.0f); - lineSegment.P1 = float2((offset.x > 0) ? offset.x * cellWidth : 0.0f, 0.0f); - phaseShift = fract((lineSegment.P0.x - localGridTopLeftCorner.x) * rcpPattenLenScreenSpace); - sdf = min(sdf, dtm::calculateLineSDF(outlineStyle, worldToScreenRatio, lineSegment, localFragPos, phaseShift)); - } - if (linesValidity[3]) - { - // opposite cell vertical line - lineSegment.P0 = float2(0.0f, (offset.y < 0) ? offset.y * cellWidth : 0.0f); - lineSegment.P1 = float2(0.0f, (offset.y > 0) ? offset.y * cellWidth : 0.0f); - phaseShift = fract((lineSegment.P0.y - localGridTopLeftCorner.y) * rcpPattenLenScreenSpace); - sdf = min(sdf, dtm::calculateLineSDF(outlineStyle, worldToScreenRatio, lineSegment, localFragPos, phaseShift)); - } - - float4 outlineColor = outlineStyle.color; - outlineColor.a *= 1.0f - smoothstep(-globals.antiAliasingFactor, globals.antiAliasingFactor, sdf); - dtmColor = dtm::blendUnder(dtmColor, outlineColor); - } - - if (dtmSettings.drawHeightShadingEnabled()) - { - if (currentTriangleIndex != InvalidTriangleIndex) - { - dtm::GridDTMTriangle currentTriangle = triangles[currentTriangleIndex]; - float heightDeriv = fwidth(interpolatedHeights[currentTriangleIndex]); - dtmColor = dtm::blendUnder(dtmColor, dtm::calculateDTMHeightColor(dtmSettings.heightShadingSettings, currentTriangle.vertices, heightDeriv, gridSpacePos, interpolatedHeights[currentTriangleIndex])); - } - else - { - // TODO[Future]: Average color of nearby valid triangles (dtm height function should return color + polygon sdf) - } - - } - - textureColor = dtmColor.rgb / dtmColor.a; - localAlpha = dtmColor.a; - } - - } - else if (objType == ObjectType::STREAMED_IMAGE) - { - const float2 uv = input.getImageUV(); - const uint32_t textureId = input.getImageTextureId(); - - if (textureId != InvalidTextureIndex) - { - float4 colorSample = textures[NonUniformResourceIndex(textureId)].Sample(textureSampler, float2(uv.x, uv.y)); - textureColor = colorSample.rgb; - localAlpha = colorSample.a; - } - } - - if (localAlpha <= 0) - discard; - - uint2 fragCoord = uint2(input.position.xy); - const bool colorFromTexture = objType == ObjectType::STREAMED_IMAGE || objType == ObjectType::STATIC_IMAGE || objType == ObjectType::GRID_DTM; - - return calculateFinalColor(fragCoord, localAlpha, currentMainObjectIdx, textureColor, colorFromTexture); - } -} diff --git a/62_CAD/shaders/main_pipeline/fragment_shader_debug.hlsl b/62_CAD/shaders/main_pipeline/fragment_shader_debug.hlsl deleted file mode 100644 index 2955d22fe..000000000 --- a/62_CAD/shaders/main_pipeline/fragment_shader_debug.hlsl +++ /dev/null @@ -1,11 +0,0 @@ -struct PSInputDebug -{ - float4 position : SV_Position; -}; - -[shader("pixel")] -float4 fragDebugMain(PSInputDebug input) : SV_TARGET -{ - return float4(1.0, 1.0, 1.0, 1.0); -// return input.color; -} \ No newline at end of file diff --git a/62_CAD/shaders/main_pipeline/fragment_shader_georef.hlsl b/62_CAD/shaders/main_pipeline/fragment_shader_georef.hlsl deleted file mode 100644 index ff60c2b47..000000000 --- a/62_CAD/shaders/main_pipeline/fragment_shader_georef.hlsl +++ /dev/null @@ -1,23 +0,0 @@ -#define FRAGMENT_SHADER_INPUT -#include "common.hlsl" - -[shader("pixel")] -float4 fragGeoref(PSInput input) : SV_TARGET -{ - ObjectType objType = input.getObjType(); - - float4 color = float4(0.0f, 0.0f, 0.0f, 0.0f); - - if (objType == ObjectType::STREAMED_IMAGE) - { - const float2 uv = input.getImageUV(); - const uint32_t textureId = input.getImageTextureId(); - - if (textureId != InvalidTextureIndex) - color = textures[NonUniformResourceIndex(textureId)].Sample(textureSampler, float2(uv.x, uv.y)); - } - - color.rgb *= color.a; // premul alpha - - return color; -} diff --git a/62_CAD/shaders/main_pipeline/fragment_shader_resolve_alphas.hlsl b/62_CAD/shaders/main_pipeline/fragment_shader_resolve_alphas.hlsl deleted file mode 100644 index b30fe8431..000000000 --- a/62_CAD/shaders/main_pipeline/fragment_shader_resolve_alphas.hlsl +++ /dev/null @@ -1,81 +0,0 @@ -#include "common.hlsl" -#include - -template -float32_t4 calculateFinalColor(const uint2 fragCoord); - - -template<> -float32_t4 calculateFinalColor(const uint2 fragCoord) -{ - return float4(0.0f, 0.0f, 0.0f, 0.0f); -} - -template<> -float32_t4 calculateFinalColor(const uint2 fragCoord) -{ - float32_t4 color; - - nbl::hlsl::spirv::beginInvocationInterlockEXT(); - - bool resolve = false; - uint32_t toResolveStyleIdx = InvalidStyleIdx; - const uint32_t packedData = pseudoStencil[fragCoord]; - const uint32_t storedQuantizedAlpha = nbl::hlsl::glsl::bitfieldExtract(packedData,0,AlphaBits); - const uint32_t storedMainObjectIdx = nbl::hlsl::glsl::bitfieldExtract(packedData,AlphaBits,MainObjectIdxBits); - - const bool currentlyActiveMainObj = (storedMainObjectIdx == globals.currentlyActiveMainObjectIndex); - if (!currentlyActiveMainObj) - { - // Normal Scenario, this branch will always be taken if there is no overflow submit in the middle of an active mainObject - //we do the final resolve of the pixel and invalidate the pseudo-stencil - pseudoStencil[fragCoord] = nbl::hlsl::glsl::bitfieldInsert(0, InvalidMainObjectIdx, AlphaBits, MainObjectIdxBits); - - // if geomID has changed, we resolve the SDF alpha (draw using blend), else accumulate - resolve = storedMainObjectIdx != InvalidMainObjectIdx; - - // load from colorStorage only if we want to resolve color from texture instead of style - // sampling from colorStorage needs to happen in critical section because another fragment may also want to store into it at the same time + need to happen before store - if (resolve) - { - toResolveStyleIdx = loadMainObject(storedMainObjectIdx).styleIdx; - if (toResolveStyleIdx == InvalidStyleIdx) // if style idx to resolve is invalid, then it means we should resolve from color - color = float32_t4(unpackR11G11B10_UNORM(colorStorage[fragCoord]), 1.0f); - } - } - else if (globals.currentlyActiveMainObjectIndex != InvalidMainObjectIdx) - { - // Being here means there was an overflow submit in the middle of an active main objejct - // We don't want to resolve the active mainObj, because it needs to fully resolved later when the mainObject actually finishes. - // We change the active main object index in our pseudo-stencil to 0u, because that will be it's new index in the next submit. - uint32_t newMainObjectIdx = 0u; - pseudoStencil[fragCoord] = nbl::hlsl::glsl::bitfieldInsert(storedQuantizedAlpha, newMainObjectIdx, AlphaBits, MainObjectIdxBits); - resolve = false; // just to re-iterate that we don't want to resolve this. - } - - - nbl::hlsl::spirv::endInvocationInterlockEXT(); - - if (!resolve) - discard; - - - // draw with previous geometry's style's color or stored in texture buffer :kek: - // we don't need to load the style's color in critical section because we've already retrieved the style index from the stored main obj - if (toResolveStyleIdx != InvalidStyleIdx) // if toResolveStyleIdx is valid then that means our resolved color should come from line style - { - color = loadLineStyle(toResolveStyleIdx).color; - gammaUncorrect(color.rgb); // want to output to SRGB without gamma correction - } - - color.a *= float(storedQuantizedAlpha) / 255.f; - color.rgb *= color.a; // premul alpha - return color; -} - -[[vk::spvexecutionmode(spv::ExecutionModePixelInterlockOrderedEXT)]] -[shader("pixel")] -float4 fragShaderResolveAlphas(float4 position : SV_Position) : SV_TARGET -{ - return calculateFinalColor(position.xy); -} diff --git a/62_CAD/shaders/main_pipeline/line_style.hlsl b/62_CAD/shaders/main_pipeline/line_style.hlsl deleted file mode 100644 index f50127667..000000000 --- a/62_CAD/shaders/main_pipeline/line_style.hlsl +++ /dev/null @@ -1,297 +0,0 @@ -#ifndef _CAD_EXAMPLE_LINE_STYLE_HLSL_INCLUDED_ -#define _CAD_EXAMPLE_LINE_STYLE_HLSL_INCLUDED_ - -#include -#include - -// for usage in upper_bound function -struct StyleAccessor -{ - LineStyle style; - using value_type = float; - - float operator[](const uint32_t ix) - { - return style.getStippleValue(ix); - } -}; - -template -struct StyleClipper -{ - using float_t = typename CurveType::scalar_t; - using float_t2 = typename CurveType::float_t2; - using float_t3 = typename CurveType::float_t3; - NBL_CONSTEXPR_STATIC_INLINE float_t AccuracyThresholdT = 0.000001; - - static StyleClipper construct( - LineStyle style, - CurveType curve, - typename CurveType::ArcLengthCalculator arcLenCalc, - float phaseShift, - float stretch, - float worldToScreenRatio) - { - StyleClipper ret = { style, curve, arcLenCalc, phaseShift, stretch, worldToScreenRatio, 0.0f, 0.0f, 0.0f, 0.0f }; - - // values for non-uniform stretching with a rigid segment - if (style.rigidSegmentIdx != InvalidRigidSegmentIndex && stretch != 1.0f) - { - // rigidSegment info in old non stretched pattern - ret.rigidSegmentStart = (style.rigidSegmentIdx >= 1u) ? style.getStippleValue(style.rigidSegmentIdx - 1u) : 0.0f; - ret.rigidSegmentEnd = (style.rigidSegmentIdx < style.stipplePatternSize) ? style.getStippleValue(style.rigidSegmentIdx) : 1.0f; - ret.rigidSegmentLen = ret.rigidSegmentEnd - ret.rigidSegmentStart; - // stretch value for non rigid segments - ret.nonRigidSegmentStretchValue = (stretch - ret.rigidSegmentLen) / (1.0f - ret.rigidSegmentLen); - // rigidSegment info to new stretched pattern - ret.rigidSegmentStart *= ret.nonRigidSegmentStretchValue / stretch; // get the new normalized rigid segment start - ret.rigidSegmentLen /= stretch; // get the new rigid segment normalized len - ret.rigidSegmentEnd = ret.rigidSegmentStart + ret.rigidSegmentLen; // get the new normalized rigid segment end - } - else - { - ret.nonRigidSegmentStretchValue = stretch; - } - - return ret; - } - - // For non-uniform stretching with a rigid segment (the one segement that shouldn't stretch) the whole pattern changes - // instead of transforming each of the style.stipplePattern values (max 14 of them), we transform the normalized place in pattern - float getRealNormalizedPlaceInPattern(float normalizedPlaceInPattern) - { - if (style.rigidSegmentIdx != InvalidRigidSegmentIndex && stretch != 1.0f) - { - float ret = min(normalizedPlaceInPattern, rigidSegmentStart) / nonRigidSegmentStretchValue; // unstretch parts before rigid segment - ret += max(normalizedPlaceInPattern - rigidSegmentEnd, 0.0f) / nonRigidSegmentStretchValue; // unstretch parts after rigid segment - ret += max(min(rigidSegmentLen, normalizedPlaceInPattern - rigidSegmentStart), 0.0f); // unstretch parts inside rigid segment - ret *= stretch; - return ret; - } - else - { - return normalizedPlaceInPattern; - } - } - - float_t2 operator()(float_t t) - { - // basicaly 0.0 and 1.0 but with a guardband to discard outside the range - const float_t minT = 0.0 - 1.0; - const float_t maxT = 1.0 + 1.0; - - StyleAccessor styleAccessor = { style }; - const float_t reciprocalStretchedStipplePatternLen = style.reciprocalStipplePatternLen / stretch; - const float_t patternLenInScreenSpace = 1.0 / (worldToScreenRatio * style.reciprocalStipplePatternLen); - - const float_t arcLen = arcLenCalc.calcArcLen(t); - const float_t worldSpaceArcLen = arcLen * float_t(worldToScreenRatio); - float_t normalizedPlaceInPattern = frac(worldSpaceArcLen * reciprocalStretchedStipplePatternLen + phaseShift); - normalizedPlaceInPattern = getRealNormalizedPlaceInPattern(normalizedPlaceInPattern); - uint32_t patternIdx = nbl::hlsl::upper_bound(styleAccessor, 0, style.stipplePatternSize, normalizedPlaceInPattern); - - const float_t InvalidT = nbl::hlsl::numeric_limits::infinity; - float_t2 ret = float_t2(InvalidT, InvalidT); - - // odd patternIdx means a "no draw section" and current candidate should split into two nearest draw sections - const bool notInDrawSection = patternIdx & 0x1; - - // TODO[Erfan]: Disable this piece of code after clipping, and comment the reason, that the bezier start and end at 0.0 and 1.0 should be in drawable sections - float_t minDrawT = 0.0; - float_t maxDrawT = 1.0; - { - float_t normalizedPlaceInPatternBegin = frac(phaseShift); - normalizedPlaceInPatternBegin = getRealNormalizedPlaceInPattern(normalizedPlaceInPatternBegin); - uint32_t patternIdxBegin = nbl::hlsl::upper_bound(styleAccessor, 0, style.stipplePatternSize, normalizedPlaceInPatternBegin); - const bool BeginInNonDrawSection = patternIdxBegin & 0x1; - - if (BeginInNonDrawSection) - { - float_t diffToRightDrawableSection = (patternIdxBegin == style.stipplePatternSize) ? 1.0 : styleAccessor[patternIdxBegin]; - diffToRightDrawableSection -= normalizedPlaceInPatternBegin; - float_t scrSpcOffsetToArcLen1 = diffToRightDrawableSection * patternLenInScreenSpace * ((patternIdxBegin != style.rigidSegmentIdx) ? nonRigidSegmentStretchValue : 1.0); - const float_t arcLenForT1 = 0.0 + scrSpcOffsetToArcLen1; - minDrawT = arcLenCalc.calcArcLenInverse(curve, minT, maxT, arcLenForT1, AccuracyThresholdT, 0.0); - } - - // Completely in non-draw section -> clip away: - if (minDrawT >= 1.0) - return ret; - - const float_t arcLenEnd = arcLenCalc.calcArcLen(1.0); - const float_t worldSpaceArcLenEnd = arcLenEnd * float_t(worldToScreenRatio); - float_t normalizedPlaceInPatternEnd = frac(worldSpaceArcLenEnd * reciprocalStretchedStipplePatternLen + phaseShift); - normalizedPlaceInPatternEnd = getRealNormalizedPlaceInPattern(normalizedPlaceInPatternEnd); - uint32_t patternIdxEnd = nbl::hlsl::upper_bound(styleAccessor, 0, style.stipplePatternSize, normalizedPlaceInPatternEnd); - const bool EndInNonDrawSection = patternIdxEnd & 0x1; - - if (EndInNonDrawSection) - { - float_t diffToLeftDrawableSection = (patternIdxEnd == 0) ? 0.0 : styleAccessor[patternIdxEnd - 1]; - diffToLeftDrawableSection -= normalizedPlaceInPatternEnd; - float_t scrSpcOffsetToArcLen0 = diffToLeftDrawableSection * patternLenInScreenSpace * ((patternIdxEnd != style.rigidSegmentIdx) ? nonRigidSegmentStretchValue : 1.0); - const float_t arcLenForT0 = arcLenEnd + scrSpcOffsetToArcLen0; - maxDrawT = arcLenCalc.calcArcLenInverse(curve, minT, maxT, arcLenForT0, AccuracyThresholdT, 1.0); - } - } - - if (notInDrawSection) - { - float toScreenSpaceLen = patternLenInScreenSpace * ((patternIdx != style.rigidSegmentIdx) ? nonRigidSegmentStretchValue : 1.0); - - float_t diffToLeftDrawableSection = (patternIdx == 0) ? 0.0 : styleAccessor[patternIdx - 1]; - diffToLeftDrawableSection -= normalizedPlaceInPattern; - float_t scrSpcOffsetToArcLen0 = diffToLeftDrawableSection * toScreenSpaceLen; - const float_t arcLenForT0 = arcLen + scrSpcOffsetToArcLen0; - float_t t0 = arcLenCalc.calcArcLenInverse(curve, minT, maxT, arcLenForT0, AccuracyThresholdT, t); - t0 = clamp(t0, minDrawT, maxDrawT); - - float_t diffToRightDrawableSection = (patternIdx == style.stipplePatternSize) ? 1.0 : styleAccessor[patternIdx]; - diffToRightDrawableSection -= normalizedPlaceInPattern; - float_t scrSpcOffsetToArcLen1 = diffToRightDrawableSection * toScreenSpaceLen; - const float_t arcLenForT1 = arcLen + scrSpcOffsetToArcLen1; - float_t t1 = arcLenCalc.calcArcLenInverse(curve, minT, maxT, arcLenForT1, AccuracyThresholdT, t); - t1 = clamp(t1, minDrawT, maxDrawT); - - ret = float_t2(t0, t1); - } - else - { - t = clamp(t, minDrawT, maxDrawT); - ret = float_t2(t, t); - } - - return ret; - } - - LineStyle style; - CurveType curve; - typename CurveType::ArcLengthCalculator arcLenCalc; - float phaseShift; - float stretch; - float worldToScreenRatio; - // precomp value for non uniform stretching - float rigidSegmentStart; - float rigidSegmentEnd; - float rigidSegmentLen; - float nonRigidSegmentStretchValue; -}; - -typedef StyleClipper< nbl::hlsl::shapes::Quadratic > BezierStyleClipper; -typedef StyleClipper< nbl::hlsl::shapes::Line > LineStyleClipper; - -template -struct DefaultClipper -{ - using float_t2 = vector; - NBL_CONSTEXPR_STATIC_INLINE float_t AccuracyThresholdT = 0.0; - - static DefaultClipper construct() - { - DefaultClipper ret; - return ret; - } - - inline float_t2 operator()(const float_t t) - { - const float_t ret = clamp(t, 0.0, 1.0); - return float_t2(ret, ret); - } -}; - -template > -struct ClippedSignedDistance -{ - using float_t = typename CurveType::scalar_t; - using float_t2 = typename CurveType::float_t2; - using float_t3 = typename CurveType::float_t3; - - const static float_t sdf(CurveType curve, float_t2 pos, float_t thickness, bool isRoadStyle, Clipper clipper = DefaultClipper::construct()) - { - typename CurveType::Candidates candidates = curve.getClosestCandidates(pos); - - const float_t InvalidT = nbl::hlsl::numeric_limits::max; - // TODO: Fix and test, we're not working with squared distance anymore - const float_t MAX_DISTANCE_SQUARED = (thickness + 1.0f) * (thickness + 1.0f); // TODO: ' + 1' is too much? - - bool clipped = false; - float_t closestDistanceSquared = MAX_DISTANCE_SQUARED; - float_t closestT = InvalidT; - [[unroll(CurveType::MaxCandidates)]] - for (uint32_t i = 0; i < CurveType::MaxCandidates; i++) - { - const float_t candidateDistanceSquared = length(curve.evaluate(candidates[i]) - pos); - if (candidateDistanceSquared < closestDistanceSquared) - { - float_t2 snappedTs = clipper(candidates[i]); - - if (snappedTs[0] == InvalidT) - { - continue; - } - - if (snappedTs[0] != candidates[i]) - { - // left snapped or clamped - const float_t leftSnappedCandidateDistanceSquared = length(curve.evaluate(snappedTs[0]) - pos); - if (leftSnappedCandidateDistanceSquared < closestDistanceSquared) - { - clipped = true; - closestT = snappedTs[0]; - closestDistanceSquared = leftSnappedCandidateDistanceSquared; - } - - if (snappedTs[0] != snappedTs[1]) - { - // right snapped or clamped - const float_t rightSnappedCandidateDistanceSquared = length(curve.evaluate(snappedTs[1]) - pos); - if (rightSnappedCandidateDistanceSquared < closestDistanceSquared) - { - clipped = true; - closestT = snappedTs[1]; - closestDistanceSquared = rightSnappedCandidateDistanceSquared; - } - } - } - else - { - // no snapping - if (candidateDistanceSquared < closestDistanceSquared) - { - clipped = false; - closestT = candidates[i]; - closestDistanceSquared = candidateDistanceSquared; - } - } - } - } - - - float_t roundedDistance = closestDistanceSquared - thickness; - if (!isRoadStyle) - { - return roundedDistance; - } - else - { - const float_t aaWidth = globals.antiAliasingFactor; - float_t rectCappedDistance = roundedDistance; - - if (clipped) - { - float_t2 q = mul(curve.getLocalCoordinateSpace(closestT), pos - curve.evaluate(closestT)); - rectCappedDistance = capSquare(q, thickness, aaWidth); - } - - return rectCappedDistance; - } - } - - static float capSquare(float_t2 q, float_t th, float_t aaWidth) - { - float_t2 d = abs(q) - float_t2(aaWidth, th); - return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0); - } -}; - -#endif \ No newline at end of file diff --git a/62_CAD/shaders/main_pipeline/vertex_shader.hlsl b/62_CAD/shaders/main_pipeline/vertex_shader.hlsl deleted file mode 100644 index 45a308daa..000000000 --- a/62_CAD/shaders/main_pipeline/vertex_shader.hlsl +++ /dev/null @@ -1,779 +0,0 @@ -#pragma shader_stage(vertex) - -#include "common.hlsl" -#include -#include -#include -#include - -// TODO[Lucas]: Move these functions to builtin hlsl functions (Even the shadertoy obb and aabb ones) -float cross2D(float2 a, float2 b) -{ - return determinant(float2x2(a,b)); -} - -float2 BezierTangent(float2 p0, float2 p1, float2 p2, float t) -{ - return 2.0 * (1.0 - t) * (p1 - p0) + 2.0 * t * (p2 - p1); -} - -float2 QuadraticBezier(float2 p0, float2 p1, float2 p2, float t) -{ - return shapes::QuadraticBezier::construct(p0, p1, p2).evaluate(t); -} - -struct NDCClipProjectionData -{ - pfloat64_t3x3 projectionToNDC; // pre-multiplied projection in a tree - float32_t2 minClipNDC; - float32_t2 maxClipNDC; -}; - -NDCClipProjectionData getClipProjectionData(in MainObject mainObj) -{ - NDCClipProjectionData ret; - if (mainObj.customProjectionIndex != InvalidCustomProjectionIndex) - { - // If projection type is worldspace projection and clip: - pfloat64_t3x3 customProjection = loadCustomProjection(mainObj.customProjectionIndex); - ret.projectionToNDC = nbl::hlsl::mul(globals.defaultProjectionToNDC, customProjection); - } - else - ret.projectionToNDC = globals.defaultProjectionToNDC; - - if (mainObj.customClipRectIndex != InvalidCustomClipRectIndex) - { - WorldClipRect worldClipRect = loadCustomClipRect(mainObj.customClipRectIndex); - - /// [NOTE]: Optimization: we avoid looking for min/max in the shader because minClip and maxClip in default worldspace are defined in such a way that minClip.y > maxClip.y so minClipNDC.y < maxClipNDC.y - ret.minClipNDC = nbl::hlsl::_static_cast(transformPointNdc(globals.defaultProjectionToNDC, worldClipRect.minClip)); - ret.maxClipNDC = nbl::hlsl::_static_cast(transformPointNdc(globals.defaultProjectionToNDC, worldClipRect.maxClip)); - } - else - { - ret.minClipNDC = float2(-1.0f, -1.0f); - ret.maxClipNDC = float2(+1.0f, +1.0f); - } - - if (mainObj.transformationType == TransformationType::TT_FIXED_SCREENSPACE_SIZE) - ret.projectionToNDC = nbl::hlsl::mul(ret.projectionToNDC, globals.screenToWorldScaleTransform); - - return ret; -} - -float2 transformPointScreenSpace(pfloat64_t3x3 transformation, uint32_t2 resolution, pfloat64_t2 point2d) -{ - pfloat64_t2 ndc = transformPointNdc(transformation, point2d); - pfloat64_t2 result = (ndc + 1.0f) * 0.5f * _static_cast(resolution); - - return _static_cast(result); -} -float2 transformVectorScreenSpace(pfloat64_t3x3 transformation, uint32_t2 resolution, pfloat64_t2 vec2d) -{ - pfloat64_t2 ndc = transformVectorNdc(transformation, vec2d); - pfloat64_t2 result = (ndc) * 0.5f * _static_cast(resolution); - return _static_cast(result); -} -float32_t4 transformFromSreenSpaceToNdc(float2 pos, uint32_t2 resolution) -{ - return float32_t4((pos.xy / (float32_t2)resolution) * 2.0f - 1.0f, 0.0f, 1.0f); -} -float32_t getScreenToWorldRatio(pfloat64_t3x3 transformation, uint32_t2 resolution) -{ - pfloat64_t idx_0_0 = transformation[0u].x * (resolution.x / 2.0); - pfloat64_t idx_1_0 = transformation[1u].x * (resolution.y / 2.0); - float32_t2 firstCol; firstCol.x = _static_cast(idx_0_0); firstCol.y = _static_cast(idx_1_0); - return nbl::hlsl::length(firstCol); // TODO: Do length in fp64? -} - -template -void dilateHatch(out float2 outOffsetVec, out float2 outUV, const float2 undilatedCorner, const float2 dilateRate, const float2 ndcAxisU, const float2 ndcAxisV); - -// Dilate with ease, our transparency algorithm will handle the overlaps easily with the help of FragmentShaderPixelInterlock -template<> -void dilateHatch(out float2 outOffsetVec, out float2 outUV, const float2 undilatedCorner, const float2 dilateRate, const float2 ndcAxisU, const float2 ndcAxisV) -{ - const float2 dilatationFactor = 1.0 + 2.0 * dilateRate; - - // cornerMultiplier stores the direction of the corner to dilate: - // (-1,-1)|--|(1,-1) - // | | - // (-1,1) |--|(1,1) - const float2 cornerMultiplier = float2(undilatedCorner * 2.0 - 1.0); - outUV = float2((cornerMultiplier * dilatationFactor + 1.0) * 0.5); - - // vx/vy are vectors in direction of the box's axes and their length is equal to X pixels (X = globals.antiAliasingFactor + 1.0) - // and we use them for dilation of X pixels in ndc space by adding them to the currentCorner in NDC space - const float2 vx = ndcAxisU * dilateRate.x; - const float2 vy = ndcAxisV * dilateRate.y; - outOffsetVec = vx * cornerMultiplier.x + vy * cornerMultiplier.y; // (0, 0) should do -vx-vy and (1, 1) should do +vx+vy -} - -// Don't dilate which causes overlap of colors when no fragshaderInterlock which powers our transparency and overlap resolving algorithm -template<> -void dilateHatch(out float2 outOffsetVec, out float2 outUV, const float2 undilatedCorner, const float2 dilateRate, const float2 ndcAxisU, const float2 ndcAxisV) -{ - outOffsetVec = float2(0.0f, 0.0f); - outUV = undilatedCorner; - // TODO: If it became a huge bummer on AMD devices we can consider dilating only in minor direction which may still avoid color overlaps - // Or optionally we could dilate and stuff when we know this hatch is opaque (alpha = 1.0) -} - -[shader("vertex")] -PSInput vtxMain(uint vertexID : SV_VertexID) -{ - NDCClipProjectionData clipProjectionData; - - PSInput outV; - - // Default Initialize PS Input - outV.position.zw = float2(0.0, 1.0); - outV.data1 = uint4(0, 0, 0, 0); - outV.data2 = float4(0, 0, 0, 0); - outV.data3 = float4(0, 0, 0, 0); - outV.data4 = float4(0, 0, 0, 0); - outV.interp_data5 = float4(0, 0, 0, 0); - - if (pc.isDTMRendering) - { - outV.setObjType(ObjectType::TRIANGLE_MESH); - outV.setMainObjectIdx(pc.triangleMeshMainObjectIndex); - - TriangleMeshVertex vtx = vk::RawBufferLoad(pc.triangleMeshVerticesBaseAddress + sizeof(TriangleMeshVertex) * vertexID, 8u); - - MainObject mainObj = loadMainObject(pc.triangleMeshMainObjectIndex); - clipProjectionData = getClipProjectionData(mainObj); - - float screenToWorldRatio = getScreenToWorldRatio(clipProjectionData.projectionToNDC, globals.resolution); - float worldToScreenRatio = 1.0f / screenToWorldRatio; - outV.setCurrentWorldToScreenRatio(worldToScreenRatio); - - // assuming there are 3 * N vertices, number of vertices is equal to number of indices and indices are sequential starting from 0 - float2 transformedOriginalPos; - float2 transformedDilatedPos; - { - uint32_t currentVertexWithinTriangleIndex = vertexID % 3; - uint32_t firstVertexOfCurrentTriangleIndex = vertexID - currentVertexWithinTriangleIndex; - - TriangleMeshVertex triangleVertices[3]; - triangleVertices[0] = vk::RawBufferLoad(pc.triangleMeshVerticesBaseAddress + sizeof(TriangleMeshVertex) * firstVertexOfCurrentTriangleIndex, 8u); - triangleVertices[1] = vk::RawBufferLoad(pc.triangleMeshVerticesBaseAddress + sizeof(TriangleMeshVertex) * (firstVertexOfCurrentTriangleIndex + 1), 8u); - triangleVertices[2] = vk::RawBufferLoad(pc.triangleMeshVerticesBaseAddress + sizeof(TriangleMeshVertex) * (firstVertexOfCurrentTriangleIndex + 2), 8u); - transformedOriginalPos = transformPointScreenSpace(clipProjectionData.projectionToNDC, globals.resolution, triangleVertices[currentVertexWithinTriangleIndex].pos); - - pfloat64_t2 triangleCentroid; - triangleCentroid.x = (triangleVertices[0].pos.x + triangleVertices[1].pos.x + triangleVertices[2].pos.x) / _static_cast(3.0f); - triangleCentroid.y = (triangleVertices[0].pos.y + triangleVertices[1].pos.y + triangleVertices[2].pos.y) / _static_cast(3.0f); - - // move triangles to local space, with centroid at (0, 0) - triangleVertices[0].pos = triangleVertices[0].pos - triangleCentroid; - triangleVertices[1].pos = triangleVertices[1].pos - triangleCentroid; - triangleVertices[2].pos = triangleVertices[2].pos - triangleCentroid; - - // TODO: calculate dialation factor - // const float dilateByPixels = 0.5 * (dtmSettings.maxScreenSpaceLineWidth + dtmSettings.maxWorldSpaceLineWidth * screenToWorldRatio) + aaFactor; - - pfloat64_t dialationFactor = _static_cast(2.0f); - pfloat64_t2 dialatedVertex = triangleVertices[currentVertexWithinTriangleIndex].pos * dialationFactor; - - dialatedVertex = dialatedVertex + triangleCentroid; - - transformedDilatedPos = transformPointScreenSpace(clipProjectionData.projectionToNDC, globals.resolution, dialatedVertex); - } - - outV.position = transformFromSreenSpaceToNdc(transformedDilatedPos, globals.resolution); - const float heightAsFloat = nbl::hlsl::_static_cast(vtx.height); - outV.setScreenSpaceVertexAttribs(float3(transformedOriginalPos, heightAsFloat)); - - // full screen triangle (this will destroy outline, contour line and height drawing) -#if 0 - const uint vertexIdx = vertexID % 3; - if(vertexIdx == 0) - outV.position.xy = float2(-1.0f, -1.0f); - else if (vertexIdx == 1) - outV.position.xy = float2(-1.0f, 3.0f); - else if (vertexIdx == 2) - outV.position.xy = float2(3.0f, -1.0f); -#endif - } - else - { - const uint vertexIdx = vertexID & 0x3u; - const uint objectID = vertexID >> 2; - - DrawObject drawObj = loadDrawObject(objectID); - - ObjectType objType = (ObjectType)(drawObj.type_subsectionIdx & 0x0000FFFF); - uint32_t subsectionIdx = drawObj.type_subsectionIdx >> 16; - outV.setObjType(objType); - outV.setMainObjectIdx(drawObj.mainObjIndex); - - MainObject mainObj = loadMainObject(drawObj.mainObjIndex); - clipProjectionData = getClipProjectionData(mainObj); - - float screenToWorldRatio = getScreenToWorldRatio(clipProjectionData.projectionToNDC, globals.resolution); - float worldToScreenRatio = 1.0f / screenToWorldRatio; - outV.setCurrentWorldToScreenRatio(worldToScreenRatio); - - // We only need these for Outline type objects like lines and bezier curves - if (objType == ObjectType::LINE || objType == ObjectType::QUAD_BEZIER || objType == ObjectType::POLYLINE_CONNECTOR) - { - LineStyle lineStyle = loadLineStyle(mainObj.styleIdx); - - // Width is on both sides, thickness is one one side of the curve (div by 2.0f) - const float screenSpaceLineWidth = lineStyle.screenSpaceLineWidth + lineStyle.worldSpaceLineWidth * screenToWorldRatio; - const float antiAliasedLineThickness = screenSpaceLineWidth * 0.5f + globals.antiAliasingFactor; - const float sdfLineThickness = max(screenSpaceLineWidth, globals.minLineWidth) / 2.0f; - - outV.setLineThickness(sdfLineThickness); - - if (objType == ObjectType::LINE) - { - pfloat64_t2 points[2u]; - points[0u] = vk::RawBufferLoad(globals.pointers.geometryBuffer + drawObj.geometryAddress, 8u); - points[1u] = vk::RawBufferLoad(globals.pointers.geometryBuffer + drawObj.geometryAddress + sizeof(LinePointInfo), 8u); - - const float phaseShift = vk::RawBufferLoad(globals.pointers.geometryBuffer + drawObj.geometryAddress + sizeof(pfloat64_t2), 8u); - const float patternStretch = vk::RawBufferLoad(globals.pointers.geometryBuffer + drawObj.geometryAddress + sizeof(pfloat64_t2) + sizeof(float), 8u); - outV.setCurrentPhaseShift(phaseShift); - outV.setPatternStretch(patternStretch); - - float2 transformedPoints[2u]; - for (uint i = 0u; i < 2u; ++i) - { - transformedPoints[i] = transformPointScreenSpace(clipProjectionData.projectionToNDC, globals.resolution, points[i]); - } - - const float2 lineVector = normalize(transformedPoints[1u] - transformedPoints[0u]); - const float2 normalToLine = float2(-lineVector.y, lineVector.x); - - if (vertexIdx == 0u || vertexIdx == 1u) - { - // work in screen space coordinates because of fixed pixel size - outV.position.xy = transformedPoints[0u] - + normalToLine * (((float)vertexIdx - 0.5f) * 2.0f * antiAliasedLineThickness) - - lineVector * antiAliasedLineThickness; - } - else // if (vertexIdx == 2u || vertexIdx == 3u) - { - // work in screen space coordinates because of fixed pixel size - outV.position.xy = transformedPoints[1u] - + normalToLine * (((float)vertexIdx - 2.5f) * 2.0f * antiAliasedLineThickness) - + lineVector * antiAliasedLineThickness; - } - - outV.setLineStart(transformedPoints[0u]); - outV.setLineEnd(transformedPoints[1u]); - - outV.position.xy = transformFromSreenSpaceToNdc(outV.position.xy, globals.resolution).xy; - } - else if (objType == ObjectType::QUAD_BEZIER) - { - pfloat64_t2 points[3u]; - points[0u] = vk::RawBufferLoad(globals.pointers.geometryBuffer + drawObj.geometryAddress, 8u); - points[1u] = vk::RawBufferLoad(globals.pointers.geometryBuffer + drawObj.geometryAddress + sizeof(pfloat64_t2), 8u); - points[2u] = vk::RawBufferLoad(globals.pointers.geometryBuffer + drawObj.geometryAddress + sizeof(pfloat64_t2) * 2u, 8u); - - const float phaseShift = vk::RawBufferLoad(globals.pointers.geometryBuffer + drawObj.geometryAddress + sizeof(pfloat64_t2) * 3u, 8u); - const float patternStretch = vk::RawBufferLoad(globals.pointers.geometryBuffer + drawObj.geometryAddress + sizeof(pfloat64_t2) * 3u + sizeof(float), 8u); - outV.setCurrentPhaseShift(phaseShift); - outV.setPatternStretch(patternStretch); - - // transform these points into screen space and pass to fragment - float2 transformedPoints[3u]; - for (uint i = 0u; i < 3u; ++i) - { - transformedPoints[i] = transformPointScreenSpace(clipProjectionData.projectionToNDC, globals.resolution, points[i]); - } - - shapes::QuadraticBezier quadraticBezier = shapes::QuadraticBezier::construct(transformedPoints[0u], transformedPoints[1u], transformedPoints[2u]); - shapes::Quadratic quadratic = shapes::Quadratic::constructFromBezier(quadraticBezier); - shapes::Quadratic::ArcLengthCalculator preCompData = shapes::Quadratic::ArcLengthCalculator::construct(quadratic); - - outV.setQuadratic(quadratic); - outV.setQuadraticPrecomputedArcLenData(preCompData); - - float2 Mid = (transformedPoints[0u] + transformedPoints[2u]) / 2.0f; - float Radius = length(Mid - transformedPoints[0u]) / 2.0f; - - // https://algorithmist.wordpress.com/2010/12/01/quad-bezier-curvature/ - float2 vectorAB = transformedPoints[1u] - transformedPoints[0u]; - float2 vectorAC = transformedPoints[2u] - transformedPoints[1u]; - float area = abs(vectorAB.x * vectorAC.y - vectorAB.y * vectorAC.x) * 0.5; - float MaxCurvature; - if (length(transformedPoints[1u] - lerp(transformedPoints[0u], transformedPoints[2u], 0.25f)) > Radius && length(transformedPoints[1u] - lerp(transformedPoints[0u], transformedPoints[2u], 0.75f)) > Radius) - MaxCurvature = pow(length(transformedPoints[1u] - Mid), 3) / (area * area); - else - MaxCurvature = max(area / pow(length(transformedPoints[0u] - transformedPoints[1u]), 3), area / pow(length(transformedPoints[2u] - transformedPoints[1u]), 3)); - - // We only do this adaptive thing when "MinRadiusOfOsculatingCircle = RadiusOfMaxCurvature < screenSpaceLineWidth/4" OR "MaxCurvature > 4/screenSpaceLineWidth"; - // which means there is a self intersection because of large lineWidth relative to the curvature (in screenspace) - // the reason for division by 4.0f is 1. screenSpaceLineWidth is expanded on both sides and 2. the fact that diameter/2=radius, - const bool noCurvature = abs(dot(normalize(vectorAB), normalize(vectorAC)) - 1.0f) < exp2(-10.0f); - if (MaxCurvature * screenSpaceLineWidth > 4.0f || noCurvature) - { - //OBB Fallback - float2 obbV0; - float2 obbV1; - float2 obbV2; - float2 obbV3; - quadraticBezier.computeOBB(antiAliasedLineThickness, obbV0, obbV1, obbV2, obbV3); - if (subsectionIdx == 0) - { - if (vertexIdx == 0u) - outV.position = float4(obbV0, 0.0, 1.0f); - else if (vertexIdx == 1u) - outV.position = float4(obbV1, 0.0, 1.0f); - else if (vertexIdx == 2u) - outV.position = float4(obbV3, 0.0, 1.0f); - else if (vertexIdx == 3u) - outV.position = float4(obbV2, 0.0, 1.0f); - } - else - outV.position = float4(0.0f, 0.0f, 0.0f, 0.0f); - } - else - { - // this optimal value is hardcoded based on tests and benchmarks of pixel shader invocation - // this is the place where we use it's tangent in the bezier to form sides the cages - const float optimalT = 0.145f; - - // Whether or not to flip the the interior cage nodes - int flip = cross2D(transformedPoints[0u] - transformedPoints[1u], transformedPoints[2u] - transformedPoints[1u]) > 0.0f ? -1 : 1; - - const float middleT = 0.5f; - float2 midPos = QuadraticBezier(transformedPoints[0u], transformedPoints[1u], transformedPoints[2u], middleT); - float2 midTangent = normalize(BezierTangent(transformedPoints[0u], transformedPoints[1u], transformedPoints[2u], middleT)); - float2 midNormal = float2(-midTangent.y, midTangent.x) * flip; - - /* - P1 - + - - - exterior0 exterior1 - ---------------------- - / \- - -/ ---------------- \ - / -/interior0 interior1 - / / \ \- - -/ -/ \- \ - / -/ \ \- - / / \- \ - P0 + \ + P2 - */ - - // Internal cage points - float2 interior0; - float2 interior1; - - float2 middleExteriorPoint = midPos - midNormal * antiAliasedLineThickness; - - - float2 leftTangent = normalize(BezierTangent(transformedPoints[0u], transformedPoints[1u], transformedPoints[2u], optimalT)); - float2 leftNormal = normalize(float2(-leftTangent.y, leftTangent.x)) * flip; - float2 leftExteriorPoint = QuadraticBezier(transformedPoints[0u], transformedPoints[1u], transformedPoints[2u], optimalT) - leftNormal * antiAliasedLineThickness; - float2 exterior0 = shapes::util::LineLineIntersection(middleExteriorPoint, midTangent, leftExteriorPoint, leftTangent); - - float2 rightTangent = normalize(BezierTangent(transformedPoints[0u], transformedPoints[1u], transformedPoints[2u], 1.0f - optimalT)); - float2 rightNormal = normalize(float2(-rightTangent.y, rightTangent.x)) * flip; - float2 rightExteriorPoint = QuadraticBezier(transformedPoints[0u], transformedPoints[1u], transformedPoints[2u], 1.0f - optimalT) - rightNormal * antiAliasedLineThickness; - float2 exterior1 = shapes::util::LineLineIntersection(middleExteriorPoint, midTangent, rightExteriorPoint, rightTangent); - - // Interiors - { - float2 tangent = normalize(BezierTangent(transformedPoints[0u], transformedPoints[1u], transformedPoints[2u], 0.286f)); - float2 normal = normalize(float2(-tangent.y, tangent.x)) * flip; - interior0 = QuadraticBezier(transformedPoints[0u], transformedPoints[1u], transformedPoints[2u], 0.286) + normal * antiAliasedLineThickness; - } - { - float2 tangent = normalize(BezierTangent(transformedPoints[0u], transformedPoints[1u], transformedPoints[2u], 0.714f)); - float2 normal = normalize(float2(-tangent.y, tangent.x)) * flip; - interior1 = QuadraticBezier(transformedPoints[0u], transformedPoints[1u], transformedPoints[2u], 0.714f) + normal * antiAliasedLineThickness; - } - - if (subsectionIdx == 0u) - { - float2 endPointTangent = normalize(transformedPoints[1u] - transformedPoints[0u]); - float2 endPointNormal = float2(-endPointTangent.y, endPointTangent.x) * flip; - float2 endPointExterior = transformedPoints[0u] - endPointTangent * antiAliasedLineThickness; - - if (vertexIdx == 0u) - outV.position = float4(shapes::util::LineLineIntersection(leftExteriorPoint, leftTangent, endPointExterior, endPointNormal), 0.0, 1.0f); - else if (vertexIdx == 1u) - outV.position = float4(transformedPoints[0u] + endPointNormal * antiAliasedLineThickness - endPointTangent * antiAliasedLineThickness, 0.0, 1.0f); - else if (vertexIdx == 2u) - outV.position = float4(exterior0, 0.0, 1.0f); - else if (vertexIdx == 3u) - outV.position = float4(interior0, 0.0, 1.0f); - } - else if (subsectionIdx == 1u) - { - if (vertexIdx == 0u) - outV.position = float4(exterior0, 0.0, 1.0f); - else if (vertexIdx == 1u) - outV.position = float4(interior0, 0.0, 1.0f); - else if (vertexIdx == 2u) - outV.position = float4(exterior1, 0.0, 1.0f); - else if (vertexIdx == 3u) - outV.position = float4(interior1, 0.0, 1.0f); - } - else if (subsectionIdx == 2u) - { - float2 endPointTangent = normalize(transformedPoints[2u] - transformedPoints[1u]); - float2 endPointNormal = float2(-endPointTangent.y, endPointTangent.x) * flip; - float2 endPointExterior = transformedPoints[2u] + endPointTangent * antiAliasedLineThickness; - - if (vertexIdx == 0u) - outV.position = float4(shapes::util::LineLineIntersection(rightExteriorPoint, rightTangent, endPointExterior, endPointNormal), 0.0, 1.0f); - else if (vertexIdx == 1u) - outV.position = float4(transformedPoints[2u] + endPointNormal * antiAliasedLineThickness + endPointTangent * antiAliasedLineThickness, 0.0, 1.0f); - else if (vertexIdx == 2u) - outV.position = float4(exterior1, 0.0, 1.0f); - else if (vertexIdx == 3u) - outV.position = float4(interior1, 0.0, 1.0f); - } - } - - outV.position.xy = (outV.position.xy / globals.resolution) * 2.0f - 1.0f; - } - else if (objType == ObjectType::POLYLINE_CONNECTOR) - { - const float FLOAT_INF = numeric_limits::infinity; - const float4 INVALID_VERTEX = float4(FLOAT_INF, FLOAT_INF, FLOAT_INF, FLOAT_INF); - - if (lineStyle.isRoadStyleFlag) - { - const pfloat64_t2 circleCenter = vk::RawBufferLoad(globals.pointers.geometryBuffer + drawObj.geometryAddress, 8u); - const float2 v = vk::RawBufferLoad(globals.pointers.geometryBuffer + drawObj.geometryAddress + sizeof(pfloat64_t2), 8u); - const float cosHalfAngleBetweenNormals = vk::RawBufferLoad(globals.pointers.geometryBuffer + drawObj.geometryAddress + sizeof(pfloat64_t2) + sizeof(float2), 8u); - - const float2 circleCenterScreenSpace = transformPointScreenSpace(clipProjectionData.projectionToNDC, globals.resolution, circleCenter); - outV.setPolylineConnectorCircleCenter(circleCenterScreenSpace); - - // to better understand variables at play, and the circle space, see documentation of `miterSDF` in fragment shader - // length of vector from circle center to intersection position (normalized so that circle radius = line thickness = 1.0) - float vLen = length(v); - float2 intersectionDirection_Screenspace = normalize(transformVectorScreenSpace(clipProjectionData.projectionToNDC, globals.resolution, _static_cast(v))); - const float2 v_Screenspace = intersectionDirection_Screenspace * vLen; - - // Find other miter vertices - const float sinHalfAngleBetweenNormals = sqrt(1.0f - (cosHalfAngleBetweenNormals * cosHalfAngleBetweenNormals)); - const float32_t2x2 rotationMatrix = float32_t2x2(cosHalfAngleBetweenNormals, -sinHalfAngleBetweenNormals, sinHalfAngleBetweenNormals, cosHalfAngleBetweenNormals); - - // Pass the precomputed trapezoid values for the sdf - { - float longBase = sinHalfAngleBetweenNormals; - float shortBase = max((vLen - globals.miterLimit) * cosHalfAngleBetweenNormals / sinHalfAngleBetweenNormals, 0.0); - // height of the trapezoid / triangle - float hLen = min(globals.miterLimit, vLen); - - outV.setPolylineConnectorTrapezoidStart(-1.0 * intersectionDirection_Screenspace * sdfLineThickness); - outV.setPolylineConnectorTrapezoidEnd(intersectionDirection_Screenspace * hLen * sdfLineThickness); - outV.setPolylineConnectorTrapezoidLongBase(sinHalfAngleBetweenNormals * ((1.0 + vLen) / (vLen - cosHalfAngleBetweenNormals)) * sdfLineThickness); - outV.setPolylineConnectorTrapezoidShortBase(shortBase * sdfLineThickness); - } - - if (vertexIdx == 0u) - { - // multiplying the other way to rotate by -theta - const float2 V1 = normalize(mul(v_Screenspace, rotationMatrix)) * antiAliasedLineThickness * 2.0f; - const float2 screenSpaceV1 = circleCenterScreenSpace + V1; - outV.position = float4(screenSpaceV1, 0.0f, 1.0f); - } - else if (vertexIdx == 1u) - { - outV.position = float4(circleCenterScreenSpace, 0.0f, 1.0f); - } - else if (vertexIdx == 2u) - { - // find intersection point vertex - float2 intersectionPoint = v_Screenspace * antiAliasedLineThickness * 2.0f; - intersectionPoint += circleCenterScreenSpace; - outV.position = float4(intersectionPoint, 0.0f, 1.0f); - } - else if (vertexIdx == 3u) - { - const float2 V2 = normalize(mul(rotationMatrix, v_Screenspace)) * antiAliasedLineThickness * 2.0f; - const float2 screenSpaceV2 = circleCenterScreenSpace + V2; - outV.position = float4(screenSpaceV2, 0.0f, 1.0f); - } - - outV.position.xy = transformFromSreenSpaceToNdc(outV.position.xy, globals.resolution).xy; - } - else - { - outV.position = INVALID_VERTEX; - } - } - } - else if (objType == ObjectType::CURVE_BOX) - { - CurveBox curveBox; - curveBox.aabbMin = vk::RawBufferLoad(globals.pointers.geometryBuffer + drawObj.geometryAddress, 8u); - curveBox.aabbMax = vk::RawBufferLoad(globals.pointers.geometryBuffer + drawObj.geometryAddress + sizeof(pfloat64_t2), 8u); - - for (uint32_t i = 0; i < 3; i ++) - { - curveBox.curveMin[i] = vk::RawBufferLoad(globals.pointers.geometryBuffer + drawObj.geometryAddress + sizeof(pfloat64_t2) * 2 + sizeof(float32_t2) * i, 4u); - curveBox.curveMax[i] = vk::RawBufferLoad(globals.pointers.geometryBuffer + drawObj.geometryAddress + sizeof(pfloat64_t2) * 2 + sizeof(float32_t2) * (3 + i), 4u); - } - - pfloat64_t2 aabbMaxXMinY; - aabbMaxXMinY.x = curveBox.aabbMax.x; - aabbMaxXMinY.y = curveBox.aabbMin.y; - - pfloat64_t2 aabbMinXMaxY; - aabbMinXMaxY.x = curveBox.aabbMin.x; - aabbMinXMaxY.y = curveBox.aabbMax.y; - - const float2 ndcAxisU = _static_cast(transformVectorNdc(clipProjectionData.projectionToNDC, aabbMaxXMinY - curveBox.aabbMin)); - const float2 ndcAxisV = _static_cast(transformVectorNdc(clipProjectionData.projectionToNDC, aabbMinXMaxY - curveBox.aabbMin)); - - const float2 screenSpaceAabbExtents = float2(length(ndcAxisU * float2(globals.resolution)) / 2.0, length(ndcAxisV * float2(globals.resolution)) / 2.0); - - // we could use something like this to compute screen space change over minor/major change and avoid ddx(minor), ddy(major) in frag shader (the code below doesn't account for rotation) - outV.setCurveBoxScreenSpaceSize(float2(screenSpaceAabbExtents)); - - const float2 undilatedCorner = float2(bool2(vertexIdx & 0x1u, vertexIdx >> 1)); - const pfloat64_t2 undilatedCornerF64 = _static_cast(undilatedCorner); - - // We don't dilate on AMD (= no fragShaderInterlock) - const float pixelsToIncreaseOnEachSide = globals.antiAliasingFactor + 1.0; - const float2 dilateRate = pixelsToIncreaseOnEachSide / screenSpaceAabbExtents; // float sufficient to hold the dilate rect? - float2 dilateVec; - float2 dilatedUV; - dilateHatch(dilateVec, dilatedUV, undilatedCorner, dilateRate, ndcAxisU, ndcAxisV); - - // doing interpolation this way to ensure correct endpoints and 0 and 1, we can alternatively use branches to set current corner based on vertexIdx - const pfloat64_t2 currentCorner = curveBox.aabbMin * (_static_cast(float2(1.0f, 1.0f)) - undilatedCornerF64) + - curveBox.aabbMax * undilatedCornerF64; - - const float2 coord = _static_cast(transformPointNdc(clipProjectionData.projectionToNDC, currentCorner) + _static_cast(dilateVec)); - - outV.position = float4(coord, 0.f, 1.f); - - const uint major = (uint)SelectedMajorAxis; - const uint minor = 1-major; - - // A, B & C get converted from unorm to [0, 1] - // A & B get converted from [0,1] to [-2, 2] - shapes::Quadratic curveMin = shapes::Quadratic::construct( - curveBox.curveMin[0], curveBox.curveMin[1], curveBox.curveMin[2]); - shapes::Quadratic curveMax = shapes::Quadratic::construct( - curveBox.curveMax[0], curveBox.curveMax[1], curveBox.curveMax[2]); - - outV.setMinorBBoxUV(dilatedUV[minor]); - outV.setMajorBBoxUV(dilatedUV[major]); - - outV.setCurveMinMinor(math::equations::Quadratic::construct( - curveMin.A[minor], - curveMin.B[minor], - curveMin.C[minor])); - outV.setCurveMinMajor(math::equations::Quadratic::construct( - curveMin.A[major], - curveMin.B[major], - curveMin.C[major])); - - outV.setCurveMaxMinor(math::equations::Quadratic::construct( - curveMax.A[minor], - curveMax.B[minor], - curveMax.C[minor])); - outV.setCurveMaxMajor(math::equations::Quadratic::construct( - curveMax.A[major], - curveMax.B[major], - curveMax.C[major])); - - //math::equations::Quadratic curveMinRootFinding = math::equations::Quadratic::construct( - // curveMin.A[major], - // curveMin.B[major], - // curveMin.C[major] - maxCorner[major]); - //math::equations::Quadratic curveMaxRootFinding = math::equations::Quadratic::construct( - // curveMax.A[major], - // curveMax.B[major], - // curveMax.C[major] - maxCorner[major]); - //outV.setMinCurvePrecomputedRootFinders(PrecomputedRootFinder::construct(curveMinRootFinding)); - //outV.setMaxCurvePrecomputedRootFinders(PrecomputedRootFinder::construct(curveMaxRootFinding)); - } - else if (objType == ObjectType::FONT_GLYPH) - { - LineStyle lineStyle = loadLineStyle(mainObj.styleIdx); - const float italicTiltSlope = lineStyle.screenSpaceLineWidth; // aliased text style member with line style - - GlyphInfo glyphInfo; - glyphInfo.topLeft = vk::RawBufferLoad(globals.pointers.geometryBuffer + drawObj.geometryAddress, 8u); - glyphInfo.dirU = vk::RawBufferLoad(globals.pointers.geometryBuffer + drawObj.geometryAddress + sizeof(pfloat64_t2), 4u); - glyphInfo.aspectRatio = vk::RawBufferLoad(globals.pointers.geometryBuffer + drawObj.geometryAddress + sizeof(pfloat64_t2) + sizeof(float2), 4u); - glyphInfo.minUV_textureID_packed = vk::RawBufferLoad(globals.pointers.geometryBuffer + drawObj.geometryAddress + sizeof(pfloat64_t2) + sizeof(float2) + sizeof(float), 4u); - - float32_t2 minUV = glyphInfo.getMinUV(); - uint16_t textureID = glyphInfo.getTextureID(); - - const float32_t2 dirV = float32_t2(glyphInfo.dirU.y, -glyphInfo.dirU.x) * glyphInfo.aspectRatio; - const float2 screenTopLeft = _static_cast(transformPointNdc(clipProjectionData.projectionToNDC, glyphInfo.topLeft)); - const float2 screenDirU = _static_cast(transformVectorNdc(clipProjectionData.projectionToNDC, _static_cast(glyphInfo.dirU))); - const float2 screenDirV = _static_cast(transformVectorNdc(clipProjectionData.projectionToNDC, _static_cast(dirV))); - - const float2 corner = float2(bool2(vertexIdx & 0x1u, vertexIdx >> 1)); // corners of square from (0, 0) to (1, 1) - const float2 undilatedCornerNDC = corner * 2.0 - 1.0; // corners of square from (-1, -1) to (1, 1) - - const float2 screenSpaceAabbExtents = float2(length(screenDirU * float2(globals.resolution)) / 2.0, length(screenDirV * float2(globals.resolution)) / 2.0); - const float pixelsToIncreaseOnEachSide = globals.antiAliasingFactor + 1.0; - const float2 dilateRate = (pixelsToIncreaseOnEachSide / screenSpaceAabbExtents); - - const float2 vx = screenDirU * dilateRate.x; - const float2 vy = screenDirV * dilateRate.y; - const float2 offsetVec = vx * undilatedCornerNDC.x + vy * undilatedCornerNDC.y; - float2 coord = screenTopLeft + corner.x * screenDirU + corner.y * screenDirV + offsetVec; - - if (corner.y == 0 && italicTiltSlope > 0.0f) - coord += normalize(screenDirU) * length(screenDirV) * italicTiltSlope * float(globals.resolution.y) / float(globals.resolution.x); - - // If aspect ratio of the dimensions and glyph inside the texture are the same then screenPxRangeX === screenPxRangeY - // but if the glyph box is stretched in any way then we won't get correct msdf - // in that case we need to take the max(screenPxRangeX, screenPxRangeY) to avoid blur due to underexaggerated distances - // We compute screenPxRange using the ratio of our screenspace extent to the texel space our glyph takes inside the texture - // Our glyph is centered inside the texture, so `maxUV = 1.0 - minUV` and `glyphTexelSize = (1.0-2.0*minUV) * MSDFSize - const float screenPxRangeX = screenSpaceAabbExtents.x / ((1.0 - 2.0 * minUV.x)); // division by MSDFSize happens after max - const float screenPxRangeY = screenSpaceAabbExtents.y / ((1.0 - 2.0 * minUV.y)); // division by MSDFSize happens after max - outV.setFontGlyphPxRange((max(max(screenPxRangeX, screenPxRangeY), 1.0) * MSDFPixelRangeHalf) / MSDFSize); // we premultuply by MSDFPixelRange/2.0, to avoid doing it in frag shader - - // In order to keep the shape scale constant with any dilation values: - // We compute the new dilated minUV that gets us minUV when interpolated on the previous undilated top left - const float2 topLeftInterpolationValue = (dilateRate/(1.0+2.0*dilateRate)); - const float2 dilatedMinUV = (topLeftInterpolationValue - minUV) / (2.0 * topLeftInterpolationValue - 1.0); - const float2 dilatedMaxUV = float2(1.0, 1.0) - dilatedMinUV; - - const float2 uv = dilatedMinUV + corner * (dilatedMaxUV - dilatedMinUV); - - outV.position = float4(coord, 0.f, 1.f); - outV.setFontGlyphUV(uv); - outV.setFontGlyphTextureId(textureID); - } - else if (objType == ObjectType::STATIC_IMAGE) - { - pfloat64_t2 topLeft = vk::RawBufferLoad(globals.pointers.geometryBuffer + drawObj.geometryAddress, 8u); - float32_t2 dirU = vk::RawBufferLoad(globals.pointers.geometryBuffer + drawObj.geometryAddress + sizeof(pfloat64_t2), 4u); - float32_t aspectRatio = vk::RawBufferLoad(globals.pointers.geometryBuffer + drawObj.geometryAddress + sizeof(pfloat64_t2) + sizeof(float2), 4u); - uint32_t textureID = vk::RawBufferLoad(globals.pointers.geometryBuffer + drawObj.geometryAddress + sizeof(pfloat64_t2) + sizeof(float2) + sizeof(float), 4u); - - // TODO[DEVSH]: make sure it's documented properly that for topLeft+dirV+aspectRatio to work it's computing dirU like below (they need to be careful with transformations when y increases when you go down in screen - const float32_t2 dirV = float32_t2(dirU.y, -dirU.x) * aspectRatio; - const float2 ndcTopLeft = _static_cast(transformPointNdc(clipProjectionData.projectionToNDC, topLeft)); - const float2 ndcDirU = _static_cast(transformVectorNdc(clipProjectionData.projectionToNDC, _static_cast(dirU))); - const float2 ndcDirV = _static_cast(transformVectorNdc(clipProjectionData.projectionToNDC, _static_cast(dirV))); - - float2 corner = float2(bool2(vertexIdx & 0x1u, vertexIdx >> 1)); - float2 uv = corner; // non-dilated - - float2 ndcCorner = ndcTopLeft + corner.x * ndcDirU + corner.y * ndcDirV; - - outV.position = float4(ndcCorner, 0.f, 1.f); - outV.setImageUV(uv); - outV.setImageTextureId(textureID); - } - else if (objType == ObjectType::GRID_DTM) - { - pfloat64_t2 topLeft = vk::RawBufferLoad(globals.pointers.geometryBuffer + drawObj.geometryAddress, 8u); - const pfloat64_t2 worldSpaceExtents = vk::RawBufferLoad(globals.pointers.geometryBuffer + drawObj.geometryAddress + sizeof(pfloat64_t2), 8u); - uint32_t textureID = vk::RawBufferLoad(globals.pointers.geometryBuffer + drawObj.geometryAddress + 2 * sizeof(pfloat64_t2), 8u); - float gridCellWidth = vk::RawBufferLoad(globals.pointers.geometryBuffer + drawObj.geometryAddress + 2 * sizeof(pfloat64_t2) + sizeof(uint32_t), 8u); - float thicknessOfTheThickestLine = vk::RawBufferLoad(globals.pointers.geometryBuffer + drawObj.geometryAddress + 2 * sizeof(pfloat64_t2) + sizeof(uint32_t) + sizeof(float), 8u); - - // TODO: remove - // test large dilation - //thicknessOfTheThickestLine += 200.0f; - - const float2 corner = float2(bool2(vertexIdx & 0x1u, vertexIdx >> 1)); - - outV.setGridDTMHeightTextureID(textureID); - outV.setGridDTMScreenSpaceCellWidth(gridCellWidth * screenToWorldRatio); - outV.setGridDTMScreenSpaceGridExtents(_static_cast(worldSpaceExtents) * screenToWorldRatio); - - static const float SquareRootOfTwo = 1.4142135f; - const pfloat64_t dilationFactor = _static_cast(SquareRootOfTwo * thicknessOfTheThickestLine); - pfloat64_t2 dilationVector; - dilationVector.x = dilationFactor; - dilationVector.y = dilationFactor; - - const pfloat64_t dilationFactorTimesTwo = dilationFactor * 2.0f; - pfloat64_t2 dilationFactorTimesTwoVector; - dilationFactorTimesTwoVector.x = dilationFactorTimesTwo; - dilationFactorTimesTwoVector.y = dilationFactorTimesTwo; - const pfloat64_t2 dilatedGridExtents = worldSpaceExtents + dilationFactorTimesTwoVector; - const float2 uvScale = _static_cast(worldSpaceExtents) / _static_cast(dilatedGridExtents); - float2 uvOffset = _static_cast(dilationVector) / _static_cast(dilatedGridExtents); - uvOffset /= uvScale; - - if (corner.x == 0.0f && corner.y == 0.0f) - { - dilationVector.x = ieee754::flipSign(dilationVector.x, true); - uvOffset.x = -uvOffset.x; - uvOffset.y = -uvOffset.y; - } - else if (corner.x == 0.0f && corner.y == 1.0f) - { - dilationVector.x = ieee754::flipSign(dilationVector.x, true); - dilationVector.y = ieee754::flipSign(dilationVector.y, true); - uvOffset.x = -uvOffset.x; - } - else if (corner.x == 1.0f && corner.y == 1.0f) - { - dilationVector.y = ieee754::flipSign(dilationVector.y, true); - } - else if (corner.x == 1.0f && corner.y == 0.0f) - { - uvOffset.y = -uvOffset.y; - } - - const float2 uv = corner + uvOffset; - outV.setImageUV(uv); - - pfloat64_t2 worldSpaceExtentsYAxisFlipped; - worldSpaceExtentsYAxisFlipped.x = worldSpaceExtents.x; - worldSpaceExtentsYAxisFlipped.y = ieee754::flipSign(worldSpaceExtents.y, true); - const pfloat64_t2 vtxPos = topLeft + worldSpaceExtentsYAxisFlipped * _static_cast(corner); - const pfloat64_t2 dilatedVtxPos = vtxPos + dilationVector; - - float2 ndcVtxPos = _static_cast(transformPointNdc(clipProjectionData.projectionToNDC, dilatedVtxPos)); - outV.position = float4(ndcVtxPos, 0.0f, 1.0f); - } - else if (objType == ObjectType::STREAMED_IMAGE) - { - const pfloat64_t2 topLeft = vk::RawBufferLoad(globals.pointers.geometryBuffer + drawObj.geometryAddress, 8u); - const float32_t2 dirU = vk::RawBufferLoad(globals.pointers.geometryBuffer + drawObj.geometryAddress + sizeof(pfloat64_t2), 4u); - const float32_t aspectRatio = vk::RawBufferLoad(globals.pointers.geometryBuffer + drawObj.geometryAddress + sizeof(pfloat64_t2) + sizeof(float32_t2), 4u); - const uint32_t textureID = vk::RawBufferLoad(globals.pointers.geometryBuffer + drawObj.geometryAddress + sizeof(pfloat64_t2) + sizeof(float32_t2) + sizeof(float32_t), 4u); - const float32_t2 minUV = vk::RawBufferLoad(globals.pointers.geometryBuffer + drawObj.geometryAddress + sizeof(pfloat64_t2) + sizeof(float32_t2) + sizeof(float32_t) + sizeof(uint32_t), 4u); - const float32_t2 maxUV = vk::RawBufferLoad(globals.pointers.geometryBuffer + drawObj.geometryAddress + sizeof(pfloat64_t2) + 2 * sizeof(float32_t2) + sizeof(float32_t) + sizeof(uint32_t), 4u); - - const float32_t2 dirV = float32_t2(dirU.y, -dirU.x) * aspectRatio; - const float32_t2 ndcTopLeft = _static_cast(transformPointNdc(clipProjectionData.projectionToNDC, topLeft)); - const float32_t2 ndcDirU = _static_cast(transformVectorNdc(clipProjectionData.projectionToNDC, _static_cast(dirU))); - const float32_t2 ndcDirV = _static_cast(transformVectorNdc(clipProjectionData.projectionToNDC, _static_cast(dirV))); - - const bool2 corner = bool2(vertexIdx & 0x1u, vertexIdx >> 1u); - - const float32_t2 ndcCorner = ndcTopLeft + corner.x * ndcDirU + corner.y * ndcDirV; - const float32_t2 uv = float32_t2(corner.x ? maxUV.x : minUV.x, corner.y ? maxUV.y : minUV.y); - - outV.position = float4(ndcCorner, 0.f, 1.f); - outV.setImageUV(uv); - outV.setImageTextureId(textureID); - } - - // Make the cage fullscreen for testing: -#if 0 - if (vertexIdx == 0u) - outV.position = float4(-1, -1, 0, 1); - else if (vertexIdx == 1u) - outV.position = float4(-1, +1, 0, 1); - else if (vertexIdx == 2u) - outV.position = float4(+1, -1, 0, 1); - else if (vertexIdx == 3u) - outV.position = float4(+1, +1, 0, 1); -#endif - } - outV.clip = float4(outV.position.x - clipProjectionData.minClipNDC.x, outV.position.y - clipProjectionData.minClipNDC.y, clipProjectionData.maxClipNDC.x - outV.position.x, clipProjectionData.maxClipNDC.y - outV.position.y); - return outV; -} diff --git a/62_CAD/shaders/runtimeDeviceConfigCaps.hlsl b/62_CAD/shaders/runtimeDeviceConfigCaps.hlsl deleted file mode 100644 index 96647c0e7..000000000 --- a/62_CAD/shaders/runtimeDeviceConfigCaps.hlsl +++ /dev/null @@ -1,6 +0,0 @@ -#ifndef _RUNTIME_DEVICE_CONFIG_CAPS_HLSL_INCLUDED_ -#define _RUNTIME_DEVICE_CONFIG_CAPS_HLSL_INCLUDED_ - -#include -using DeviceConfigCaps = nbl::hlsl::jit::device_capabilities; -#endif // _RUNTIME_DEVICE_CONFIG_CAPS_HLSL_INCLUDED_ diff --git a/62_CAD/shaders/solid_color/fragment_shader_solid_color.hlsl b/62_CAD/shaders/solid_color/fragment_shader_solid_color.hlsl deleted file mode 100644 index fe948cfbb..000000000 --- a/62_CAD/shaders/solid_color/fragment_shader_solid_color.hlsl +++ /dev/null @@ -1,13 +0,0 @@ -// TODO: extract into common.hlsl, make sure to differentiate with other push constants of other pipelines (use namespaces?!) -struct PushConstants -{ - float4 solidColor; -}; - -[[vk::push_constant]] PushConstants pc; - -[shader("pixel")] -float4 fragSolidColor(float4 position : SV_Position) : SV_TARGET -{ - return pc.solidColor; -} diff --git a/62_CAD/shaders/tools/fragment_shader_sample_texture.hlsl b/62_CAD/shaders/tools/fragment_shader_sample_texture.hlsl deleted file mode 100644 index 273b633d2..000000000 --- a/62_CAD/shaders/tools/fragment_shader_sample_texture.hlsl +++ /dev/null @@ -1,12 +0,0 @@ -// vertex shader is provided by the fullScreenTriangle extension -#include -using namespace nbl::hlsl::ext::FullScreenTriangle; - -[[vk::combinedImageSampler]][[vk::binding(0,2)]] Texture2D texture; -[[vk::combinedImageSampler]][[vk::binding(0,2)]] SamplerState samplerState; - -[shader("pixel")] -float4 main(SVertexAttributes vxAttr) : SV_Target0 -{ - return texture.Sample(samplerState,float32_t2(vxAttr.uv)); -} \ No newline at end of file diff --git a/62_CAD/shaders/tools/texture_render_fragment_shader.hlsl b/62_CAD/shaders/tools/texture_render_fragment_shader.hlsl deleted file mode 100644 index 35aed75f8..000000000 --- a/62_CAD/shaders/tools/texture_render_fragment_shader.hlsl +++ /dev/null @@ -1,13 +0,0 @@ -#pragma wave shader_stage(fragment) - -// vertex shader is provided by the fullScreenTriangle extension -#include -using namespace nbl::hlsl::ext::FullScreenTriangle; - -[[vk::combinedImageSampler]][[vk::binding(0,2)]] Texture2D texture; -[[vk::combinedImageSampler]][[vk::binding(0,2)]] SamplerState samplerState; - -[[vk::location(0)]] float32_t4 main(SVertexAttributes vxAttr) : SV_Target0 -{ - return texture.Sample(samplerState,float32_t2(vxAttr.uv)); -} \ No newline at end of file diff --git a/62_CAD/vertex_shader.hlsl b/62_CAD/vertex_shader.hlsl new file mode 100644 index 000000000..b63de24c9 --- /dev/null +++ b/62_CAD/vertex_shader.hlsl @@ -0,0 +1,572 @@ +#pragma shader_stage(vertex) + +#include "common.hlsl" +#include +#include +#include +#include +#include + +// TODO[Lucas]: Move these functions to builtin hlsl functions (Even the shadertoy obb and aabb ones) +float cross2D(float2 a, float2 b) +{ + return determinant(float2x2(a,b)); +} + +float2 BezierTangent(float2 p0, float2 p1, float2 p2, float t) +{ + return 2.0 * (1.0 - t) * (p1 - p0) + 2.0 * t * (p2 - p1); +} + +float2 QuadraticBezier(float2 p0, float2 p1, float2 p2, float t) +{ + return nbl::hlsl::shapes::QuadraticBezier::construct(p0, p1, p2).evaluate(t); +} + +ClipProjectionData getClipProjectionData(in MainObject mainObj) +{ + if (mainObj.clipProjectionAddress != InvalidClipProjectionAddress) + { + ClipProjectionData ret; + ret.projectionToNDC = vk::RawBufferLoad(mainObj.clipProjectionAddress, 8u); + ret.minClipNDC = vk::RawBufferLoad(mainObj.clipProjectionAddress + sizeof(float64_t3x3), 8u); + ret.maxClipNDC = vk::RawBufferLoad(mainObj.clipProjectionAddress + sizeof(float64_t3x3) + sizeof(float32_t2), 8u); + return ret; + } + else + { + return globals.defaultClipProjection; + } +} + +double2 transformPointNdc(float64_t3x3 transformation, double2 point2d) +{ + return mul(transformation, float64_t3(point2d, 1)).xy; +} +double2 transformVectorNdc(float64_t3x3 transformation, double2 vector2d) +{ + return mul(transformation, float64_t3(vector2d, 0)).xy; +} +float2 transformPointScreenSpace(float64_t3x3 transformation, double2 point2d) +{ + double2 ndc = transformPointNdc(transformation, point2d); + return (float2)((ndc + 1.0) * 0.5 * globals.resolution); +} +float4 transformFromSreenSpaceToNdc(float2 pos) +{ + return float4((pos.xy / globals.resolution) * 2.0 - 1.0, 0.0f, 1.0f); +} + +template +void dilateHatch(out float2 outOffsetVec, out float2 outUV, const float2 undilatedCorner, const float2 dilateRate, const float2 ndcAxisU, const float2 ndcAxisV); + +// Dilate with ease, our transparency algorithm will handle the overlaps easily with the help of FragmentShaderPixelInterlock +template<> +void dilateHatch(out float2 outOffsetVec, out float2 outUV, const float2 undilatedCorner, const float2 dilateRate, const float2 ndcAxisU, const float2 ndcAxisV) +{ + const float2 dilatationFactor = 1.0 + 2.0 * dilateRate; + + // cornerMultiplier stores the direction of the corner to dilate: + // (-1,-1)|--|(1,-1) + // | | + // (-1,1) |--|(1,1) + const float2 cornerMultiplier = float2(undilatedCorner * 2.0 - 1.0); + outUV = float2((cornerMultiplier * dilatationFactor + 1.0) * 0.5); + + // vx/vy are vectors in direction of the box's axes and their length is equal to X pixels (X = globals.antiAliasingFactor + 1.0) + // and we use them for dilation of X pixels in ndc space by adding them to the currentCorner in NDC space + const float2 vx = ndcAxisU * dilateRate.x; + const float2 vy = ndcAxisV * dilateRate.y; + outOffsetVec = vx * cornerMultiplier.x + vy * cornerMultiplier.y; // (0, 0) should do -vx-vy and (1, 1) should do +vx+vy +} + +// Don't dilate which causes overlap of colors when no fragshaderInterlock which powers our transparency and overlap resolving algorithm +template<> +void dilateHatch(out float2 outOffsetVec, out float2 outUV, const float2 undilatedCorner, const float2 dilateRate, const float2 ndcAxisU, const float2 ndcAxisV) +{ + outOffsetVec = float2(0.0f, 0.0f); + outUV = undilatedCorner; + // TODO: If it became a huge bummer on AMD devices we can consider dilating only in minor direction which may still avoid color overlaps + // Or optionally we could dilate and stuff when we know this hatch is opaque (alpha = 1.0) +} + +PSInput main(uint vertexID : SV_VertexID) +{ + const uint vertexIdx = vertexID & 0x3u; + const uint objectID = vertexID >> 2; + + DrawObject drawObj = drawObjects[objectID]; + + ObjectType objType = (ObjectType)(drawObj.type_subsectionIdx & 0x0000FFFF); + uint32_t subsectionIdx = drawObj.type_subsectionIdx >> 16; + PSInput outV; + + // Default Initialize PS Input + outV.position.z = 0.0; + outV.data1 = uint4(0, 0, 0, 0); + outV.data2 = float4(0, 0, 0, 0); + outV.data3 = float4(0, 0, 0, 0); + outV.data4 = float4(0, 0, 0, 0); + outV.interp_data5 = float2(0, 0); + outV.setObjType(objType); + outV.setMainObjectIdx(drawObj.mainObjIndex); + + MainObject mainObj = mainObjects[drawObj.mainObjIndex]; + ClipProjectionData clipProjectionData = getClipProjectionData(mainObj); + + // We only need these for Outline type objects like lines and bezier curves + if (objType == ObjectType::LINE || objType == ObjectType::QUAD_BEZIER || objType == ObjectType::POLYLINE_CONNECTOR) + { + LineStyle lineStyle = lineStyles[mainObj.styleIdx]; + + // Width is on both sides, thickness is one one side of the curve (div by 2.0f) + const float screenSpaceLineWidth = lineStyle.screenSpaceLineWidth + float(lineStyle.worldSpaceLineWidth * globals.screenToWorldRatio); + const float antiAliasedLineThickness = screenSpaceLineWidth * 0.5f + globals.antiAliasingFactor; + const float sdfLineThickness = screenSpaceLineWidth / 2.0f; + outV.setLineThickness(sdfLineThickness); + outV.setCurrentWorldToScreenRatio((float)(2.0 / (clipProjectionData.projectionToNDC[0][0] * globals.resolution.x))); + + if (objType == ObjectType::LINE) + { + double2 points[2u]; + points[0u] = vk::RawBufferLoad(drawObj.geometryAddress, 8u); + points[1u] = vk::RawBufferLoad(drawObj.geometryAddress + sizeof(LinePointInfo), 8u); + + const float phaseShift = vk::RawBufferLoad(drawObj.geometryAddress + sizeof(double2), 8u); + const float patternStretch = vk::RawBufferLoad(drawObj.geometryAddress + sizeof(double2) + sizeof(float), 8u); + outV.setCurrentPhaseShift(phaseShift); + outV.setPatternStretch(patternStretch); + + float2 transformedPoints[2u]; + for (uint i = 0u; i < 2u; ++i) + { + transformedPoints[i] = transformPointScreenSpace(clipProjectionData.projectionToNDC, points[i]); + } + + const float2 lineVector = normalize(transformedPoints[1u] - transformedPoints[0u]); + const float2 normalToLine = float2(-lineVector.y, lineVector.x); + + if (vertexIdx == 0u || vertexIdx == 1u) + { + // work in screen space coordinates because of fixed pixel size + outV.position.xy = transformedPoints[0u] + + normalToLine * (((float)vertexIdx - 0.5f) * 2.0f * antiAliasedLineThickness) + - lineVector * antiAliasedLineThickness; + } + else // if (vertexIdx == 2u || vertexIdx == 3u) + { + // work in screen space coordinates because of fixed pixel size + outV.position.xy = transformedPoints[1u] + + normalToLine * (((float)vertexIdx - 2.5f) * 2.0f * antiAliasedLineThickness) + + lineVector * antiAliasedLineThickness; + } + + outV.setLineStart(transformedPoints[0u]); + outV.setLineEnd(transformedPoints[1u]); + + outV.position = transformFromSreenSpaceToNdc(outV.position.xy); + } + else if (objType == ObjectType::QUAD_BEZIER) + { + double2 points[3u]; + points[0u] = vk::RawBufferLoad(drawObj.geometryAddress, 8u); + points[1u] = vk::RawBufferLoad(drawObj.geometryAddress + sizeof(double2), 8u); + points[2u] = vk::RawBufferLoad(drawObj.geometryAddress + sizeof(double2) * 2u, 8u); + + const float phaseShift = vk::RawBufferLoad(drawObj.geometryAddress + sizeof(double2) * 3u, 8u); + const float patternStretch = vk::RawBufferLoad(drawObj.geometryAddress + sizeof(double2) * 3u + sizeof(float), 8u); + outV.setCurrentPhaseShift(phaseShift); + outV.setPatternStretch(patternStretch); + + // transform these points into screen space and pass to fragment + float2 transformedPoints[3u]; + for (uint i = 0u; i < 3u; ++i) + { + transformedPoints[i] = transformPointScreenSpace(clipProjectionData.projectionToNDC, points[i]); + } + + nbl::hlsl::shapes::QuadraticBezier quadraticBezier = nbl::hlsl::shapes::QuadraticBezier::construct(transformedPoints[0u], transformedPoints[1u], transformedPoints[2u]); + nbl::hlsl::shapes::Quadratic quadratic = nbl::hlsl::shapes::Quadratic::constructFromBezier(quadraticBezier); + nbl::hlsl::shapes::Quadratic::ArcLengthCalculator preCompData = nbl::hlsl::shapes::Quadratic::ArcLengthCalculator::construct(quadratic); + + outV.setQuadratic(quadratic); + outV.setQuadraticPrecomputedArcLenData(preCompData); + + float2 Mid = (transformedPoints[0u] + transformedPoints[2u]) / 2.0f; + float Radius = length(Mid - transformedPoints[0u]) / 2.0f; + + // https://algorithmist.wordpress.com/2010/12/01/quad-bezier-curvature/ + float2 vectorAB = transformedPoints[1u] - transformedPoints[0u]; + float2 vectorAC = transformedPoints[2u] - transformedPoints[1u]; + float area = abs(vectorAB.x * vectorAC.y - vectorAB.y * vectorAC.x) * 0.5; + float MaxCurvature; + if (length(transformedPoints[1u] - lerp(transformedPoints[0u], transformedPoints[2u], 0.25f)) > Radius && length(transformedPoints[1u] - lerp(transformedPoints[0u], transformedPoints[2u], 0.75f)) > Radius) + MaxCurvature = pow(length(transformedPoints[1u] - Mid), 3) / (area * area); + else + MaxCurvature = max(area / pow(length(transformedPoints[0u] - transformedPoints[1u]), 3), area / pow(length(transformedPoints[2u] - transformedPoints[1u]), 3)); + + // We only do this adaptive thing when "MinRadiusOfOsculatingCircle = RadiusOfMaxCurvature < screenSpaceLineWidth/4" OR "MaxCurvature > 4/screenSpaceLineWidth"; + // which means there is a self intersection because of large lineWidth relative to the curvature (in screenspace) + // the reason for division by 4.0f is 1. screenSpaceLineWidth is expanded on both sides and 2. the fact that diameter/2=radius, + const bool noCurvature = abs(dot(normalize(vectorAB), normalize(vectorAC)) - 1.0f) < exp2(-10.0f); + if (MaxCurvature * screenSpaceLineWidth > 4.0f || noCurvature) + { + //OBB Fallback + float2 obbV0; + float2 obbV1; + float2 obbV2; + float2 obbV3; + quadraticBezier.computeOBB(antiAliasedLineThickness, obbV0, obbV1, obbV2, obbV3); + if (subsectionIdx == 0) + { + if (vertexIdx == 0u) + outV.position = float4(obbV0, 0.0, 1.0f); + else if (vertexIdx == 1u) + outV.position = float4(obbV1, 0.0, 1.0f); + else if (vertexIdx == 2u) + outV.position = float4(obbV3, 0.0, 1.0f); + else if (vertexIdx == 3u) + outV.position = float4(obbV2, 0.0, 1.0f); + } + else + outV.position = float4(0.0f, 0.0f, 0.0f, 0.0f); + } + else + { + // this optimal value is hardcoded based on tests and benchmarks of pixel shader invocation + // this is the place where we use it's tangent in the bezier to form sides the cages + const float optimalT = 0.145f; + + //Whether or not to flip the the interior cage nodes + int flip = cross2D(transformedPoints[0u] - transformedPoints[1u], transformedPoints[2u] - transformedPoints[1u]) > 0.0f ? -1 : 1; + + const float middleT = 0.5f; + float2 midPos = QuadraticBezier(transformedPoints[0u], transformedPoints[1u], transformedPoints[2u], middleT); + float2 midTangent = normalize(BezierTangent(transformedPoints[0u], transformedPoints[1u], transformedPoints[2u], middleT)); + float2 midNormal = float2(-midTangent.y, midTangent.x) * flip; + + /* + P1 + + + + + exterior0 exterior1 + ---------------------- + / \- + -/ ---------------- \ + / -/interior0 interior1 + / / \ \- + -/ -/ \- \ + / -/ \ \- + / / \- \ + P0 + \ + P2 + */ + + //Internal cage points + float2 interior0; + float2 interior1; + + float2 middleExteriorPoint = midPos - midNormal * antiAliasedLineThickness; + + + float2 leftTangent = normalize(BezierTangent(transformedPoints[0u], transformedPoints[1u], transformedPoints[2u], optimalT)); + float2 leftNormal = normalize(float2(-leftTangent.y, leftTangent.x)) * flip; + float2 leftExteriorPoint = QuadraticBezier(transformedPoints[0u], transformedPoints[1u], transformedPoints[2u], optimalT) - leftNormal * antiAliasedLineThickness; + float2 exterior0 = nbl::hlsl::shapes::util::LineLineIntersection(middleExteriorPoint, midTangent, leftExteriorPoint, leftTangent); + + float2 rightTangent = normalize(BezierTangent(transformedPoints[0u], transformedPoints[1u], transformedPoints[2u], 1.0f - optimalT)); + float2 rightNormal = normalize(float2(-rightTangent.y, rightTangent.x)) * flip; + float2 rightExteriorPoint = QuadraticBezier(transformedPoints[0u], transformedPoints[1u], transformedPoints[2u], 1.0f - optimalT) - rightNormal * antiAliasedLineThickness; + float2 exterior1 = nbl::hlsl::shapes::util::LineLineIntersection(middleExteriorPoint, midTangent, rightExteriorPoint, rightTangent); + + // Interiors + { + float2 tangent = normalize(BezierTangent(transformedPoints[0u], transformedPoints[1u], transformedPoints[2u], 0.286f)); + float2 normal = normalize(float2(-tangent.y, tangent.x)) * flip; + interior0 = QuadraticBezier(transformedPoints[0u], transformedPoints[1u], transformedPoints[2u], 0.286) + normal * antiAliasedLineThickness; + } + { + float2 tangent = normalize(BezierTangent(transformedPoints[0u], transformedPoints[1u], transformedPoints[2u], 0.714f)); + float2 normal = normalize(float2(-tangent.y, tangent.x)) * flip; + interior1 = QuadraticBezier(transformedPoints[0u], transformedPoints[1u], transformedPoints[2u], 0.714f) + normal * antiAliasedLineThickness; + } + + if (subsectionIdx == 0u) + { + float2 endPointTangent = normalize(transformedPoints[1u] - transformedPoints[0u]); + float2 endPointNormal = float2(-endPointTangent.y, endPointTangent.x) * flip; + float2 endPointExterior = transformedPoints[0u] - endPointTangent * antiAliasedLineThickness; + + if (vertexIdx == 0u) + outV.position = float4(nbl::hlsl::shapes::util::LineLineIntersection(leftExteriorPoint, leftTangent, endPointExterior, endPointNormal), 0.0, 1.0f); + else if (vertexIdx == 1u) + outV.position = float4(transformedPoints[0u] + endPointNormal * antiAliasedLineThickness - endPointTangent * antiAliasedLineThickness, 0.0, 1.0f); + else if (vertexIdx == 2u) + outV.position = float4(exterior0, 0.0, 1.0f); + else if (vertexIdx == 3u) + outV.position = float4(interior0, 0.0, 1.0f); + } + else if (subsectionIdx == 1u) + { + if (vertexIdx == 0u) + outV.position = float4(exterior0, 0.0, 1.0f); + else if (vertexIdx == 1u) + outV.position = float4(interior0, 0.0, 1.0f); + else if (vertexIdx == 2u) + outV.position = float4(exterior1, 0.0, 1.0f); + else if (vertexIdx == 3u) + outV.position = float4(interior1, 0.0, 1.0f); + } + else if (subsectionIdx == 2u) + { + float2 endPointTangent = normalize(transformedPoints[2u] - transformedPoints[1u]); + float2 endPointNormal = float2(-endPointTangent.y, endPointTangent.x) * flip; + float2 endPointExterior = transformedPoints[2u] + endPointTangent * antiAliasedLineThickness; + + if (vertexIdx == 0u) + outV.position = float4(nbl::hlsl::shapes::util::LineLineIntersection(rightExteriorPoint, rightTangent, endPointExterior, endPointNormal), 0.0, 1.0f); + else if (vertexIdx == 1u) + outV.position = float4(transformedPoints[2u] + endPointNormal * antiAliasedLineThickness + endPointTangent * antiAliasedLineThickness, 0.0, 1.0f); + else if (vertexIdx == 2u) + outV.position = float4(exterior1, 0.0, 1.0f); + else if (vertexIdx == 3u) + outV.position = float4(interior1, 0.0, 1.0f); + } + } + + outV.position.xy = (outV.position.xy / globals.resolution) * 2.0 - 1.0; + } + else if (objType == ObjectType::POLYLINE_CONNECTOR) + { + const float FLOAT_INF = nbl::hlsl::numeric_limits::infinity; + const float4 INVALID_VERTEX = float4(FLOAT_INF, FLOAT_INF, FLOAT_INF, FLOAT_INF); + + if (lineStyle.isRoadStyleFlag) + { + const double2 circleCenter = vk::RawBufferLoad(drawObj.geometryAddress, 8u); + const float2 v = vk::RawBufferLoad(drawObj.geometryAddress + sizeof(double2), 8u); + const float cosHalfAngleBetweenNormals = vk::RawBufferLoad(drawObj.geometryAddress + sizeof(double2) + sizeof(float2), 8u); + + const float2 circleCenterScreenSpace = transformPointScreenSpace(clipProjectionData.projectionToNDC, circleCenter); + outV.setPolylineConnectorCircleCenter(circleCenterScreenSpace); + + // Find other miter vertices + const float sinHalfAngleBetweenNormals = sqrt(1.0f - (cosHalfAngleBetweenNormals * cosHalfAngleBetweenNormals)); + const float32_t2x2 rotationMatrix = float32_t2x2(cosHalfAngleBetweenNormals, -sinHalfAngleBetweenNormals, sinHalfAngleBetweenNormals, cosHalfAngleBetweenNormals); + + // Pass the precomputed trapezoid values for the sdf + { + float vLen = length(v); + float2 intersectionDirection = v / vLen; + + float longBase = sinHalfAngleBetweenNormals; + float shortBase = max((vLen - globals.miterLimit) * cosHalfAngleBetweenNormals / sinHalfAngleBetweenNormals, 0.0); + // height of the trapezoid / triangle + float hLen = min(globals.miterLimit, vLen); + + outV.setPolylineConnectorTrapezoidStart(-1.0 * intersectionDirection * sdfLineThickness); + outV.setPolylineConnectorTrapezoidEnd(intersectionDirection * hLen * sdfLineThickness); + outV.setPolylineConnectorTrapezoidLongBase(sinHalfAngleBetweenNormals * ((1.0 + vLen) / (vLen - cosHalfAngleBetweenNormals)) * sdfLineThickness); + outV.setPolylineConnectorTrapezoidShortBase(shortBase * sdfLineThickness); + } + + if (vertexIdx == 0u) + { + const float2 V1 = normalize(mul(v, rotationMatrix)) * antiAliasedLineThickness * 2.0f; + const float2 screenSpaceV1 = circleCenterScreenSpace + V1; + outV.position = float4(screenSpaceV1, 0.0f, 1.0f); + } + else if (vertexIdx == 1u) + { + outV.position = float4(circleCenterScreenSpace, 0.0f, 1.0f); + } + else if (vertexIdx == 2u) + { + // find intersection point vertex + float2 intersectionPoint = v * antiAliasedLineThickness * 2.0f; + intersectionPoint += circleCenterScreenSpace; + outV.position = float4(intersectionPoint, 0.0f, 1.0f); + } + else if (vertexIdx == 3u) + { + const float2 V2 = normalize(mul(rotationMatrix, v)) * antiAliasedLineThickness * 2.0f; + const float2 screenSpaceV2 = circleCenterScreenSpace + V2; + outV.position = float4(screenSpaceV2, 0.0f, 1.0f); + } + + outV.position = transformFromSreenSpaceToNdc(outV.position.xy); + } + else + { + outV.position = INVALID_VERTEX; + } + } + } + else if (objType == ObjectType::CURVE_BOX) + { + CurveBox curveBox; + curveBox.aabbMin = vk::RawBufferLoad(drawObj.geometryAddress, 8u); + curveBox.aabbMax = vk::RawBufferLoad(drawObj.geometryAddress + sizeof(double2), 8u); + for (uint32_t i = 0; i < 3; i ++) + { + curveBox.curveMin[i] = vk::RawBufferLoad(drawObj.geometryAddress + sizeof(double2) * 2 + sizeof(float32_t2) * i, 4u); + curveBox.curveMax[i] = vk::RawBufferLoad(drawObj.geometryAddress + sizeof(double2) * 2 + sizeof(float32_t2) * (3 + i), 4u); + } + + const float2 ndcAxisU = (float2)transformVectorNdc(clipProjectionData.projectionToNDC, double2(curveBox.aabbMax.x, curveBox.aabbMin.y) - curveBox.aabbMin); + const float2 ndcAxisV = (float2)transformVectorNdc(clipProjectionData.projectionToNDC, double2(curveBox.aabbMin.x, curveBox.aabbMax.y) - curveBox.aabbMin); + + const float2 screenSpaceAabbExtents = float2(length(ndcAxisU * float2(globals.resolution)) / 2.0, length(ndcAxisV * float2(globals.resolution)) / 2.0); + + // we could use something like this to compute screen space change over minor/major change and avoid ddx(minor), ddy(major) in frag shader (the code below doesn't account for rotation) + outV.setCurveBoxScreenSpaceSize(float2(screenSpaceAabbExtents)); + + const float2 undilatedCorner = float2(bool2(vertexIdx & 0x1u, vertexIdx >> 1)); + + // We don't dilate on AMD (= no fragShaderInterlock) + const float pixelsToIncreaseOnEachSide = globals.antiAliasingFactor + 1.0; + const float2 dilateRate = pixelsToIncreaseOnEachSide / screenSpaceAabbExtents; // float sufficient to hold the dilate rect? + float2 dilateVec; + float2 dilatedUV; + dilateHatch(dilateVec, dilatedUV, undilatedCorner, dilateRate, ndcAxisU, ndcAxisV); + + // doing interpolation this way to ensure correct endpoints and 0 and 1, we can alternatively use branches to set current corner based on vertexIdx + const double2 currentCorner = curveBox.aabbMin * (1.0 - undilatedCorner) + curveBox.aabbMax * undilatedCorner; + const float2 coord = (float2) (transformPointNdc(clipProjectionData.projectionToNDC, currentCorner) + dilateVec); + + outV.position = float4(coord, 0.f, 1.f); + + const uint major = (uint)SelectedMajorAxis; + const uint minor = 1-major; + + // A, B & C get converted from unorm to [0, 1] + // A & B get converted from [0,1] to [-2, 2] + nbl::hlsl::shapes::Quadratic curveMin = nbl::hlsl::shapes::Quadratic::construct( + curveBox.curveMin[0], curveBox.curveMin[1], curveBox.curveMin[2]); + nbl::hlsl::shapes::Quadratic curveMax = nbl::hlsl::shapes::Quadratic::construct( + curveBox.curveMax[0], curveBox.curveMax[1], curveBox.curveMax[2]); + + outV.setMinorBBoxUV(dilatedUV[minor]); + outV.setMajorBBoxUV(dilatedUV[major]); + + outV.setCurveMinMinor(nbl::hlsl::math::equations::Quadratic::construct( + curveMin.A[minor], + curveMin.B[minor], + curveMin.C[minor])); + outV.setCurveMinMajor(nbl::hlsl::math::equations::Quadratic::construct( + curveMin.A[major], + curveMin.B[major], + curveMin.C[major])); + + outV.setCurveMaxMinor(nbl::hlsl::math::equations::Quadratic::construct( + curveMax.A[minor], + curveMax.B[minor], + curveMax.C[minor])); + outV.setCurveMaxMajor(nbl::hlsl::math::equations::Quadratic::construct( + curveMax.A[major], + curveMax.B[major], + curveMax.C[major])); + + //nbl::hlsl::math::equations::Quadratic curveMinRootFinding = nbl::hlsl::math::equations::Quadratic::construct( + // curveMin.A[major], + // curveMin.B[major], + // curveMin.C[major] - maxCorner[major]); + //nbl::hlsl::math::equations::Quadratic curveMaxRootFinding = nbl::hlsl::math::equations::Quadratic::construct( + // curveMax.A[major], + // curveMax.B[major], + // curveMax.C[major] - maxCorner[major]); + //outV.setMinCurvePrecomputedRootFinders(PrecomputedRootFinder::construct(curveMinRootFinding)); + //outV.setMaxCurvePrecomputedRootFinders(PrecomputedRootFinder::construct(curveMaxRootFinding)); + } + else if (objType == ObjectType::FONT_GLYPH) + { + GlyphInfo glyphInfo; + glyphInfo.topLeft = vk::RawBufferLoad(drawObj.geometryAddress, 8u); + glyphInfo.dirU = vk::RawBufferLoad(drawObj.geometryAddress + sizeof(double2), 4u); + glyphInfo.aspectRatio = vk::RawBufferLoad(drawObj.geometryAddress + sizeof(double2) + sizeof(float2), 4u); + glyphInfo.minUV_textureID_packed = vk::RawBufferLoad(drawObj.geometryAddress + sizeof(double2) + sizeof(float2) + sizeof(float), 4u); + + float32_t2 minUV = glyphInfo.getMinUV(); + uint16_t textureID = glyphInfo.getTextureID(); + + const float32_t2 dirV = float32_t2(glyphInfo.dirU.y, -glyphInfo.dirU.x) * glyphInfo.aspectRatio; + const float2 screenTopLeft = (float2) transformPointNdc(clipProjectionData.projectionToNDC, glyphInfo.topLeft); + const float2 screenDirU = (float2) transformVectorNdc(clipProjectionData.projectionToNDC, glyphInfo.dirU); + const float2 screenDirV = (float2) transformVectorNdc(clipProjectionData.projectionToNDC, dirV); + + const float2 corner = float2(bool2(vertexIdx & 0x1u, vertexIdx >> 1)); // corners of square from (0, 0) to (1, 1) + const float2 undilatedCornerNDC = corner * 2.0 - 1.0; // corners of square from (-1, -1) to (1, 1) + + const float2 screenSpaceAabbExtents = float2(length(screenDirU * float2(globals.resolution)) / 2.0, length(screenDirV * float2(globals.resolution)) / 2.0); + const float pixelsToIncreaseOnEachSide = globals.antiAliasingFactor + 1.0; + const float2 dilateRate = (float2)(pixelsToIncreaseOnEachSide / screenSpaceAabbExtents); + + const float2 vx = screenDirU * dilateRate.x; + const float2 vy = screenDirV * dilateRate.y; + const float2 offsetVec = vx * undilatedCornerNDC.x + vy * undilatedCornerNDC.y; + const float2 coord = screenTopLeft + corner.x * screenDirU + corner.y * screenDirV + offsetVec; + + // If aspect ratio of the dimensions and glyph inside the texture are the same then screenPxRangeX === screenPxRangeY + // but if the glyph box is stretched in any way then we won't get correct msdf + // in that case we need to take the max(screenPxRangeX, screenPxRangeY) to avoid blur due to underexaggerated distances + // We compute screenPxRange using the ratio of our screenspace extent to the texel space our glyph takes inside the texture + // Our glyph is centered inside the texture, so `maxUV = 1.0 - minUV` and `glyphTexelSize = (1.0-2.0*minUV) * MSDFSize + const float screenPxRangeX = screenSpaceAabbExtents.x / ((1.0 - 2.0 * minUV.x)); + const float screenPxRangeY = screenSpaceAabbExtents.y / ((1.0 - 2.0 * minUV.y)); + float screenPxRange = max(max(screenPxRangeX, screenPxRangeY), 1.0) * MSDFPixelRange / MSDFSize; + + // In order to keep the shape scale constant with any dilation values: + // We compute the new dilated minUV that gets us minUV when interpolated on the previous undilated top left + const float2 topLeftInterpolationValue = (dilateRate/(1.0+2.0*dilateRate)); + const float2 dilatedMinUV = (topLeftInterpolationValue - minUV) / (2.0 * topLeftInterpolationValue - 1.0); + const float2 dilatedMaxUV = float2(1.0, 1.0) - dilatedMinUV; + + const float2 uv = dilatedMinUV + corner * (dilatedMaxUV - dilatedMinUV); + + outV.position = float4(coord, 0.f, 1.f); + outV.setFontGlyphUV(uv); + outV.setFontGlyphTextureId(textureID); + outV.setFontGlyphScreenPxRange(screenPxRange); + } + else if (objType == ObjectType::IMAGE) + { + float64_t2 topLeft = vk::RawBufferLoad(drawObj.geometryAddress, 8u); + float32_t2 dirU = vk::RawBufferLoad(drawObj.geometryAddress + sizeof(double2), 4u); + float32_t aspectRatio = vk::RawBufferLoad(drawObj.geometryAddress + sizeof(double2) + sizeof(float2), 4u); + uint32_t textureID = vk::RawBufferLoad(drawObj.geometryAddress + sizeof(double2) + sizeof(float2) + sizeof(float), 4u); + + const float32_t2 dirV = float32_t2(dirU.y, -dirU.x) * aspectRatio; + const float2 ndcTopLeft = (float2) transformPointNdc(clipProjectionData.projectionToNDC, topLeft); + const float2 ndcDirU = (float2) transformVectorNdc(clipProjectionData.projectionToNDC, dirU); + const float2 ndcDirV = (float2) transformVectorNdc(clipProjectionData.projectionToNDC, dirV); + + float2 corner = float2(bool2(vertexIdx & 0x1u, vertexIdx >> 1)); + float2 uv = corner; // non-dilated + + float2 ndcCorner = ndcTopLeft + corner.x * ndcDirU + corner.y * ndcDirV; + + outV.position = float4(ndcCorner, 0.f, 1.f); + outV.setImageUV(uv); + outV.setImageTextureId(textureID); + } + + +// Make the cage fullscreen for testing: +#if 0 + // disabled for object of POLYLINE_CONNECTOR type, since miters would cover whole screen + if(objType != ObjectType::POLYLINE_CONNECTOR) + { + if (vertexIdx == 0u) + outV.position = float4(-1, -1, 0, 1); + else if (vertexIdx == 1u) + outV.position = float4(-1, +1, 0, 1); + else if (vertexIdx == 2u) + outV.position = float4(+1, -1, 0, 1); + else if (vertexIdx == 3u) + outV.position = float4(+1, +1, 0, 1); + } +#endif + + outV.clip = float4(outV.position.x - clipProjectionData.minClipNDC.x, outV.position.y - clipProjectionData.minClipNDC.y, clipProjectionData.maxClipNDC.x - outV.position.x, clipProjectionData.maxClipNDC.y - outV.position.y); + return outV; +} diff --git a/64_EmulatedFloatTest/CMakeLists.txt b/64_EmulatedFloatTest/CMakeLists.txt deleted file mode 100644 index bd4de23ce..000000000 --- a/64_EmulatedFloatTest/CMakeLists.txt +++ /dev/null @@ -1,72 +0,0 @@ -include(common RESULT_VARIABLE RES) -if(NOT RES) - message(FATAL_ERROR "common.cmake not found. Should be in {repo_root}/cmake directory") -endif() - -nbl_create_executable_project("" "" "" "" "${NBL_EXECUTABLE_PROJECT_CREATION_PCH_TARGET}") - -if(NBL_EMBED_BUILTIN_RESOURCES) - set(_BR_TARGET_ ${EXECUTABLE_NAME}_builtinResourceData) - set(RESOURCE_DIR "app_resources") - - get_filename_component(_SEARCH_DIRECTORIES_ "${CMAKE_CURRENT_SOURCE_DIR}" ABSOLUTE) - get_filename_component(_OUTPUT_DIRECTORY_SOURCE_ "${CMAKE_CURRENT_BINARY_DIR}/src" ABSOLUTE) - get_filename_component(_OUTPUT_DIRECTORY_HEADER_ "${CMAKE_CURRENT_BINARY_DIR}/include" ABSOLUTE) - - file(GLOB_RECURSE BUILTIN_RESOURCE_FILES RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}/${RESOURCE_DIR}" CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/${RESOURCE_DIR}/*") - foreach(RES_FILE ${BUILTIN_RESOURCE_FILES}) - LIST_BUILTIN_RESOURCE(RESOURCES_TO_EMBED "${RES_FILE}") - endforeach() - - ADD_CUSTOM_BUILTIN_RESOURCES(${_BR_TARGET_} RESOURCES_TO_EMBED "${_SEARCH_DIRECTORIES_}" "${RESOURCE_DIR}" "nbl::this_example::builtin" "${_OUTPUT_DIRECTORY_HEADER_}" "${_OUTPUT_DIRECTORY_SOURCE_}") - - LINK_BUILTIN_RESOURCES_TO_TARGET(${EXECUTABLE_NAME} ${_BR_TARGET_}) -endif() - -if(MSVC) - target_compile_options("${EXECUTABLE_NAME}" PUBLIC "/fp:strict") -else() - target_compile_options("${EXECUTABLE_NAME}" PUBLIC -ffloat-store -frounding-math -fsignaling-nans -ftrapping-math) -endif() - -set(OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/auto-gen") - -set(SM 6_8) -set(JSON [=[ -[ - { - "INPUT": "app_resources/test.comp.hlsl", - "KEY": "test", - }, - { - "INPUT": "app_resources/benchmark/benchmark.comp.hlsl", - "KEY": "benchmark", - }, -] -]=]) -string(CONFIGURE "${JSON}" JSON) - -set(COMPILE_OPTIONS - -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} -) diff --git a/64_EmulatedFloatTest/app_resources/benchmark/benchmark.comp.hlsl b/64_EmulatedFloatTest/app_resources/benchmark/benchmark.comp.hlsl deleted file mode 100644 index a515f6bcb..000000000 --- a/64_EmulatedFloatTest/app_resources/benchmark/benchmark.comp.hlsl +++ /dev/null @@ -1,125 +0,0 @@ -//// Copyright (C) 2023-2024 - 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 shader_stage(compute) - -#include "app_resources/benchmark/common.hlsl" -#include -#include -#include -#include -#include - -using namespace nbl::hlsl; - -[[vk::binding(0, 0)]] RWByteAddressBuffer outputBuffer; -[[vk::push_constant]] BenchmarkPushConstants pc; - -template -struct Random16thPolynomial -{ - void randomizeCoefficients() - { - Xoroshiro64Star rng = Xoroshiro64Star::construct(100); - - for (int i = 0; i < CoefficientNumber; ++i) - { - uint64_t exp = uint64_t(rng()) % 16 + ieee754::traits::exponentBias; - exp <<= ieee754::traits::mantissaBitCnt; - uint64_t mantissa = (uint64_t(rng()) << 32) | uint64_t(rng()); - mantissa &= ieee754::traits::mantissaMask; - - coefficients[i] = bit_cast(exp | mantissa); - } - } - - F64 operator()(F64 x) - { - F64 result = coefficients[CoefficientNumber - 1]; - - F64 xn = x; - for (int i = CoefficientNumber - 2; i >= 0; --i) - { - result = result + coefficients[i] * xn; - xn = xn * x; - } - - return result; - } - - static const int CoefficientNumber = 16; - static const bool reasonableCoefficients = true; - - F64 coefficients[CoefficientNumber]; -}; - -template -uint64_t calcIntegral() -{ - Random16thPolynomial polynomial; - polynomial.randomizeCoefficients(); - - using Integrator = math::quadrature::GaussLegendreIntegration<15, F64, Random16thPolynomial >; - F64 integral = Integrator::calculateIntegral(polynomial, _static_cast(0.0f), _static_cast(69.0f)); - - return bit_cast(integral); -} - -[numthreads(BENCHMARK_WORKGROUP_DIMENSION_SIZE_X, 1, 1)] -[shader("compute")] -void main(uint3 invocationID : SV_DispatchThreadID) -{ - static const uint32_t NativeToEmulatedRatio = 6; - // slightly more invocations will go to native so `NativeToEmulatedRatio-1 < real ratio <= NativeToEmulatedRatio` - const bool nativeSubgroup = bool(glsl::gl_SubgroupID() % NativeToEmulatedRatio); - - uint64_t output = 0ull; - - switch (pc.benchmarkMode) - { - case NATIVE: - output = calcIntegral(); - break; - case EF64_FAST_MATH_ENABLED: - output = calcIntegral >(); - break; - case EF64_FAST_MATH_DISABLED: - output = calcIntegral >(); - break; - case SUBGROUP_DIVIDED_WORK: - if (nativeSubgroup) - output = calcIntegral(); - else - output = calcIntegral >(); - break; - case INTERLEAVED: - output = calcIntegral >(); - break; - } - - for (uint32_t i = 0; i < NativeToEmulatedRatio; ++i) - { - switch (pc.benchmarkMode) - { - case NATIVE: - case INTERLEAVED: - output ^= calcIntegral(); - break; - case EF64_FAST_MATH_ENABLED: - output ^= calcIntegral >(); - break; - case EF64_FAST_MATH_DISABLED: - output ^= calcIntegral >(); - break; - case SUBGROUP_DIVIDED_WORK: - if (nativeSubgroup) - output ^= calcIntegral(); - else - output ^= calcIntegral >(); - break; - } - } - - const uint32_t offset = sizeof(uint64_t) * invocationID.x; - outputBuffer.Store(offset, output); -} diff --git a/64_EmulatedFloatTest/app_resources/benchmark/common.hlsl b/64_EmulatedFloatTest/app_resources/benchmark/common.hlsl deleted file mode 100644 index 7f6d1dec1..000000000 --- a/64_EmulatedFloatTest/app_resources/benchmark/common.hlsl +++ /dev/null @@ -1,24 +0,0 @@ -//// Copyright (C) 2023-2024 - 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 - -NBL_CONSTEXPR_INLINE_NSPC_SCOPE_VAR uint32_t BENCHMARK_WORKGROUP_DIMENSION_SIZE_X = 128u; -NBL_CONSTEXPR_INLINE_NSPC_SCOPE_VAR uint32_t BENCHMARK_WORKGROUP_DIMENSION_SIZE_Y = 1u; -NBL_CONSTEXPR_INLINE_NSPC_SCOPE_VAR uint32_t BENCHMARK_WORKGROUP_DIMENSION_SIZE_Z = 1u; -NBL_CONSTEXPR_INLINE_NSPC_SCOPE_VAR uint32_t BENCHMARK_WORKGROUP_COUNT = 1024u; - -enum EF64_BENCHMARK_MODE -{ - NATIVE, - EF64_FAST_MATH_ENABLED, - EF64_FAST_MATH_DISABLED, - SUBGROUP_DIVIDED_WORK, - INTERLEAVED -}; - -struct BenchmarkPushConstants -{ - EF64_BENCHMARK_MODE benchmarkMode; -}; \ No newline at end of file diff --git a/64_EmulatedFloatTest/app_resources/common.hlsl b/64_EmulatedFloatTest/app_resources/common.hlsl deleted file mode 100644 index 0e8762c5a..000000000 --- a/64_EmulatedFloatTest/app_resources/common.hlsl +++ /dev/null @@ -1,60 +0,0 @@ -//// Copyright (C) 2023-2024 - 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 - -NBL_CONSTEXPR_INLINE_NSPC_SCOPE_VAR uint32_t WORKGROUP_SIZE = 1; - -using namespace nbl; -using namespace hlsl; - -struct ConstructorTestValues -{ - int32_t int32; - int64_t int64; - uint32_t uint32; - uint64_t uint64; - float32_t float32; - float64_t float64; -}; - -struct PushConstants -{ - uint64_t a; - uint64_t b; - ConstructorTestValues constrTestVals; -}; - -template -struct TestValues -{ - uint64_t a; - uint64_t b; - - // constructors - typename nbl::hlsl::emulated_float64_t::storage_t int32CreateVal; - typename nbl::hlsl::emulated_float64_t::storage_t int64CreateVal; - typename nbl::hlsl::emulated_float64_t::storage_t uint32CreateVal; - typename nbl::hlsl::emulated_float64_t::storage_t uint64CreateVal; - typename nbl::hlsl::emulated_float64_t::storage_t float32CreateVal; - typename nbl::hlsl::emulated_float64_t::storage_t float64CreateVal; - - // arithmetic - typename nbl::hlsl::emulated_float64_t::storage_t additionVal; - typename nbl::hlsl::emulated_float64_t::storage_t substractionVal; - typename nbl::hlsl::emulated_float64_t::storage_t multiplicationVal; - typename nbl::hlsl::emulated_float64_t::storage_t divisionVal; - - // relational - int lessOrEqualVal; - int greaterOrEqualVal; - int equalVal; - int notEqualVal; - int lessVal; - int greaterVal; -}; \ No newline at end of file diff --git a/64_EmulatedFloatTest/app_resources/test.comp.hlsl b/64_EmulatedFloatTest/app_resources/test.comp.hlsl deleted file mode 100644 index e95eadd49..000000000 --- a/64_EmulatedFloatTest/app_resources/test.comp.hlsl +++ /dev/null @@ -1,43 +0,0 @@ -//// Copyright (C) 2023-2024 - 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 shader_stage(compute) - -#include "app_resources/common.hlsl" -#include - -[[vk::binding(0, 0)]] RWStructuredBuffer > testValuesOutput; - -[[vk::push_constant]] -PushConstants pc; - -[numthreads(WORKGROUP_SIZE, 1, 1)] -[shader("compute")] -void main(uint3 invocationID : SV_DispatchThreadID) -{ - const nbl::hlsl::emulated_float64_t a = nbl::hlsl::bit_cast >(pc.a); - const nbl::hlsl::emulated_float64_t b = nbl::hlsl::bit_cast >(pc.b); - - // "constructors" - testValuesOutput[0].int32CreateVal = nbl::hlsl::emulated_float64_t::create(pc.constrTestVals.int32).data; - testValuesOutput[0].int64CreateVal = nbl::hlsl::emulated_float64_t::create(pc.constrTestVals.int64).data; - testValuesOutput[0].uint32CreateVal = nbl::hlsl::emulated_float64_t::create(pc.constrTestVals.uint32).data; - testValuesOutput[0].uint64CreateVal = nbl::hlsl::emulated_float64_t::create(pc.constrTestVals.uint64).data; - testValuesOutput[0].float32CreateVal = nbl::hlsl::emulated_float64_t::create(pc.constrTestVals.float32).data; - testValuesOutput[0].float64CreateVal = nbl::hlsl::emulated_float64_t::create(pc.constrTestVals.float64).data; - - // arithmetic operators - testValuesOutput[0].additionVal = (a+b).data; - testValuesOutput[0].substractionVal = (a-b).data; - testValuesOutput[0].multiplicationVal = (a*b).data; - testValuesOutput[0].divisionVal = (a/b).data; - - // relational operators - testValuesOutput[0].lessOrEqualVal = int(a<=b); - testValuesOutput[0].greaterOrEqualVal = int(a>=b); - testValuesOutput[0].equalVal = int(a==b); - testValuesOutput[0].notEqualVal = int(a!=b); - testValuesOutput[0].lessVal = int(ab); - -} diff --git a/64_EmulatedFloatTest/main.cpp b/64_EmulatedFloatTest/main.cpp deleted file mode 100644 index 7919f68c5..000000000 --- a/64_EmulatedFloatTest/main.cpp +++ /dev/null @@ -1,1220 +0,0 @@ -// Copyright (C) 2018-2024 - 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 "nbl/this_example/builtin/build/spirv/keys.hpp" - -#include "nbl/examples/examples.hpp" - -#include -#include -#include -#include -#include - -#include "app_resources/common.hlsl" -#include "app_resources/benchmark/common.hlsl" -#include "nbl/builtin/hlsl/ieee754.hlsl" - -#include - - -using namespace nbl::core; -using namespace nbl::hlsl; -using namespace nbl::system; -using namespace nbl::asset; -using namespace nbl::video; -using namespace nbl::application_templates; -using namespace nbl::examples; - -constexpr bool DoTests = true; -constexpr bool DoBenchmark = true; - -class CompatibilityTest final : public MonoDeviceApplication, public BuiltinResourcesApplication -{ - using device_base_t = MonoDeviceApplication; - using asset_base_t = BuiltinResourcesApplication; -public: - CompatibilityTest(const path& _localInputCWD, const path& _localOutputCWD, const path& _sharedInputCWD, const path& _sharedOutputCWD) : - IApplicationFramework(_localInputCWD, _localOutputCWD, _sharedInputCWD, _sharedOutputCWD) {} - - virtual SPhysicalDeviceFeatures getPreferredDeviceFeatures() const override - { - auto retval = device_base_t::getPreferredDeviceFeatures(); - retval.pipelineExecutableInfo = true; - return retval; - } - - bool onAppInitialized(smart_refctd_ptr&& system) override - { - // since emulated_float64_t rounds to zero - std::fesetround(FE_TOWARDZERO); - - if (!device_base_t::onAppInitialized(smart_refctd_ptr(system))) - return false; - if (!asset_base_t::onAppInitialized(std::move(system))) - return false; - - return true; - } - - void onAppTerminated_impl() override - { - m_device->waitIdle(); - } - - void workLoopBody() override - { - if constexpr (DoTests) - { - emulated_float64_tests(); - } - if constexpr (DoBenchmark) - { - EF64Benchmark benchmark(*this); - benchmark.run(); - } - - m_keepRunning = false; - } - - bool keepRunning() override - { - return m_keepRunning; - } - - -private: - - bool m_keepRunning = true; - - constexpr static inline uint32_t EmulatedFloat64TestIterations = 1000u; - - enum class EmulatedFloatTestDevice - { - CPU, - GPU - }; - - template - bool compareEmulatedFloat64TestValues(const TestValues& expectedValues, const TestValues& testValues) - { - bool success = true; - - auto printOnFailure = [this](EmulatedFloatTestDevice device) - { - std::string errorMsgPrefix = ""; - if (device == EmulatedFloatTestDevice::CPU) - errorMsgPrefix = "CPU test fail:"; - else - errorMsgPrefix = "GPU test fail:"; - - m_logger->log("%s", ILogger::ELL_ERROR, errorMsgPrefix.c_str()); - m_logFile << errorMsgPrefix << '\n'; - }; - - auto printOnArithmeticFailure = [this](const char* valName, uint64_t expectedValue, uint64_t testValue, uint64_t a, uint64_t b) - { - double expectedAsDouble = reinterpret_cast(expectedValue); - double testAsDouble = reinterpret_cast(testValue); - double error = std::abs(expectedAsDouble - testAsDouble); - - std::stringstream ss; - ss << "for input values: A = " << reinterpret_cast(a) << " B = " << reinterpret_cast(b) << '\n'; - ss << valName << " not equal!"; - ss << "\nexpected value: " << std::fixed << std::setprecision(20) << expectedAsDouble; - ss << "\ntest value: " << std::fixed << std::setprecision(20) << testAsDouble; - ss << "\nerror = " << error << '\n'; - ss << "bit representations: \n"; - ss << "seeeeeeeeeeemmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm\n"; - ss << std::bitset<64>(expectedValue) << " - expectedValue bit pattern\n"; - ss << std::bitset<64>(testValue) << " - testValue bit pattern \n"; - - m_logger->log("%s", ILogger::ELL_ERROR, ss.str().c_str()); - m_logFile << ss.str() << '\n'; - - //std::cout << "ULP error: " << std::max(expectedValue, testValue) - std::min(expectedValue, testValue) << "\n\n"; - - }; - - auto calcULPError = [](emulated_float64_t::storage_t expectedValue, emulated_float64_t::storage_t testValue) - { - return std::max(expectedValue, testValue) - std::min(expectedValue, testValue); - }; - - auto printOnComparisonFailure = [this](const char* valName, int expectedValue, int testValue, double a, double b) - { - std::string inputValuesStr = std::string("for input values: A = ") + std::to_string(a) + std::string(" B = ") + std::to_string(b); - - m_logger->log("%s", ILogger::ELL_ERROR, inputValuesStr.c_str()); - m_logFile << inputValuesStr << '\n'; - - std::stringstream ss; - ss << valName << " not equal!"; - ss << "\nexpected value: " << std::boolalpha << bool(expectedValue); - ss << "\ntest value: " << std::boolalpha << bool(testValue); - - m_logger->log("%s", ILogger::ELL_ERROR, ss.str().c_str()); - m_logFile << ss.str() << '\n'; - }; - - if (calcULPError(expectedValues.int32CreateVal, testValues.int32CreateVal) > 1u) - { - printOnFailure(Device); - printOnArithmeticFailure("int32CreateVal", expectedValues.int32CreateVal, testValues.int32CreateVal, expectedValues.a, expectedValues.b); - success = false; - } - if (calcULPError(expectedValues.int64CreateVal, testValues.int64CreateVal) > 1u) - { - printOnFailure(Device); - printOnArithmeticFailure("int64CreateVal", expectedValues.int64CreateVal, testValues.int64CreateVal, expectedValues.a, expectedValues.b); - success = false; - } - if (calcULPError(expectedValues.uint32CreateVal, testValues.uint32CreateVal) > 1u) - { - printOnFailure(Device); - printOnArithmeticFailure("uint32CreateVal", expectedValues.uint32CreateVal, testValues.uint32CreateVal, expectedValues.a, expectedValues.b); - success = false; - } - if (calcULPError(expectedValues.uint64CreateVal, testValues.uint64CreateVal) > 1u) - { - printOnFailure(Device); - printOnArithmeticFailure("uint64CreateVal", expectedValues.uint64CreateVal, testValues.uint64CreateVal, expectedValues.a, expectedValues.b); - success = false; - } - if (calcULPError(expectedValues.float32CreateVal, testValues.float32CreateVal) > 1u) - { - printOnFailure(Device); - printOnArithmeticFailure("float32CreateVal", expectedValues.float32CreateVal, testValues.float32CreateVal, expectedValues.a, expectedValues.b); - success = false; - } - if (expectedValues.float64CreateVal != testValues.float64CreateVal) - { - printOnFailure(Device); - printOnArithmeticFailure("float64CreateVal", expectedValues.float64CreateVal, testValues.float64CreateVal, expectedValues.a, expectedValues.b); - success = false; - } - if (calcULPError(expectedValues.additionVal, testValues.additionVal) > 1u) - { - printOnFailure(Device); - printOnArithmeticFailure("additionVal", expectedValues.additionVal, testValues.additionVal, expectedValues.a, expectedValues.b); - success = false; - } - if (calcULPError(expectedValues.substractionVal, testValues.substractionVal) > 1u) - { - printOnFailure(Device); - printOnArithmeticFailure("substractionVal", expectedValues.substractionVal, testValues.substractionVal, expectedValues.a, expectedValues.b); - success = false; - } - if (calcULPError(expectedValues.multiplicationVal, testValues.multiplicationVal) > 1u) - { - printOnFailure(Device); - printOnArithmeticFailure("multiplicationVal", expectedValues.multiplicationVal, testValues.multiplicationVal, expectedValues.a, expectedValues.b); - success = false; - } - if (calcULPError(expectedValues.divisionVal, testValues.divisionVal) > 1u) - { - printOnFailure(Device); - printOnArithmeticFailure("divisionVal", expectedValues.divisionVal, testValues.divisionVal, expectedValues.a, expectedValues.b); - success = false; - } - if (expectedValues.lessOrEqualVal != testValues.lessOrEqualVal) - { - printOnFailure(Device); - printOnComparisonFailure("lessOrEqualVal", expectedValues.lessOrEqualVal, testValues.lessOrEqualVal, expectedValues.a, expectedValues.b); - success = false; - } - if (expectedValues.greaterOrEqualVal != testValues.greaterOrEqualVal) - { - printOnFailure(Device); - printOnComparisonFailure("greaterOrEqualVal", expectedValues.greaterOrEqualVal, testValues.greaterOrEqualVal, expectedValues.a, expectedValues.b); - success = false; - } - if (expectedValues.equalVal != testValues.equalVal) - { - printOnFailure(Device); - printOnComparisonFailure("equalVal", expectedValues.equalVal, testValues.equalVal, expectedValues.a, expectedValues.b); - success = false; - } - if (expectedValues.notEqualVal != testValues.notEqualVal) - { - printOnFailure(Device); - printOnComparisonFailure("notEqualVal", expectedValues.notEqualVal, testValues.notEqualVal, expectedValues.a, expectedValues.b); - success = false; - } - if (expectedValues.lessVal != testValues.lessVal) - { - printOnFailure(Device); - printOnComparisonFailure("lessVal", expectedValues.lessVal, testValues.lessVal, expectedValues.a, expectedValues.b); - success = false; - } - if (expectedValues.greaterVal != testValues.greaterVal) - { - printOnFailure(Device); - printOnComparisonFailure("greaterVal", expectedValues.greaterVal, testValues.greaterVal, expectedValues.a, expectedValues.b); - success = false; - } - - return success; - }; - - class EF64Submitter - { - public: - EF64Submitter(CompatibilityTest& base) - :m_base(base), m_pushConstants({}), m_semaphoreCounter(0) - { - // setting up pipeline in the constructor - m_queueFamily = base.getComputeQueue()->getFamilyIndex(); - m_semaphore = base.m_device->createSemaphore(0); - m_cmdpool = base.m_device->createCommandPool(m_queueFamily, IGPUCommandPool::CREATE_FLAGS::RESET_COMMAND_BUFFER_BIT); - if (!m_cmdpool->createCommandBuffers(IGPUCommandPool::BUFFER_LEVEL::PRIMARY, 1u, &m_cmdbuf)) - base.logFail("Failed to create Command Buffers!\n"); - - // Load shaders, set up pipeline - { - smart_refctd_ptr shader; - { - IAssetLoader::SAssetLoadParams lp = {}; - lp.logger = base.m_logger.get(); - lp.workingDirectory = "app_resources"; // virtual root - - auto key = nbl::this_example::builtin::build::get_spirv_key<"test">(base.m_device.get()); - auto assetBundle = base.m_assetMgr->getAsset(key.data(), lp); - const auto assets = assetBundle.getContents(); - if (assets.empty()) - { - base.logFail("Could not load shader!"); - assert(0); - } - - // It would be super weird if loading a shader from a file produced more than 1 asset - assert(assets.size() == 1); - shader = IAsset::castDown(assets[0]); - } - - if (!shader) - base.logFail("Failed to load precompiled \"test\" shader!\n"); - - nbl::video::IGPUDescriptorSetLayout::SBinding bindings[1] = { - { - .binding = 0, - .type = nbl::asset::IDescriptor::E_TYPE::ET_STORAGE_BUFFER, - .createFlags = IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - .stageFlags = ShaderStage::ESS_COMPUTE, - .count = 1 - } - }; - smart_refctd_ptr dsLayout = base.m_device->createDescriptorSetLayout(bindings); - if (!dsLayout) - base.logFail("Failed to create a Descriptor Layout!\n"); - - SPushConstantRange pushConstantRanges[] = { - { - .stageFlags = ShaderStage::ESS_COMPUTE, - .offset = 0, - .size = sizeof(PushConstants) - } - }; - m_pplnLayout = base.m_device->createPipelineLayout(pushConstantRanges, smart_refctd_ptr(dsLayout)); - if (!m_pplnLayout) - base.logFail("Failed to create a Pipeline Layout!\n"); - - { - IGPUComputePipeline::SCreationParams params = {}; - params.layout = m_pplnLayout.get(); - params.shader.entryPoint = "main"; - params.shader.shader = shader.get(); - if (base.m_device->getEnabledFeatures().pipelineExecutableInfo) - { - params.flags |= IGPUComputePipeline::SCreationParams::FLAGS::CAPTURE_STATISTICS; - params.flags |= IGPUComputePipeline::SCreationParams::FLAGS::CAPTURE_INTERNAL_REPRESENTATIONS; - } - if (!base.m_device->createComputePipelines(nullptr, { ¶ms,1 }, &m_pipeline)) - base.logFail("Failed to create pipelines (compile & link shaders)!\n"); - - if (base.m_device->getEnabledFeatures().pipelineExecutableInfo) - { - auto report = system::to_string(m_pipeline->getExecutableInfo()); - base.m_logger->log("EF64Submitter Pipeline Executable Report:\n%s", ILogger::ELL_PERFORMANCE, report.c_str()); - } - } - - // Allocate the memory - { - constexpr size_t BufferSize = sizeof(TestValues); - - nbl::video::IGPUBuffer::SCreationParams params = {}; - params.size = BufferSize; - params.usage = IGPUBuffer::EUF_STORAGE_BUFFER_BIT; - smart_refctd_ptr outputBuff = base.m_device->createBuffer(std::move(params)); - if (!outputBuff) - base.logFail("Failed to create a GPU Buffer of size %d!\n", params.size); - - outputBuff->setObjectDebugName("emulated_float64_t output buffer"); - - nbl::video::IDeviceMemoryBacked::SDeviceMemoryRequirements reqs = outputBuff->getMemoryReqs(); - reqs.memoryTypeBits &= base.m_physicalDevice->getHostVisibleMemoryTypeBits(); - - m_allocation = base.m_device->allocate(reqs, outputBuff.get(), nbl::video::IDeviceMemoryAllocation::EMAF_NONE); - if (!m_allocation.isValid()) - base.logFail("Failed to allocate Device Memory compatible with our GPU Buffer!\n"); - - assert(outputBuff->getBoundMemory().memory == m_allocation.memory.get()); - smart_refctd_ptr pool = base.m_device->createDescriptorPoolForDSLayouts(IDescriptorPool::ECF_NONE, { &dsLayout.get(),1 }); - - m_ds = pool->createDescriptorSet(std::move(dsLayout)); - { - IGPUDescriptorSet::SDescriptorInfo info[1]; - info[0].desc = smart_refctd_ptr(outputBuff); - info[0].info.buffer = { .offset = 0,.size = BufferSize }; - IGPUDescriptorSet::SWriteDescriptorSet writes[1] = { - {.dstSet = m_ds.get(),.binding = 0,.arrayElement = 0,.count = 1,.info = info} - }; - base.m_device->updateDescriptorSets(writes, {}); - } - } - - if (!m_allocation.memory->map({ 0ull,m_allocation.memory->getAllocationSize() }, IDeviceMemoryAllocation::EMCAF_READ)) - base.logFail("Failed to map the Device Memory!\n"); - } - - // if the mapping is not coherent the range needs to be invalidated to pull in new data for the CPU's caches - const ILogicalDevice::MappedMemoryRange memoryRange(m_allocation.memory.get(), 0ull, m_allocation.memory->getAllocationSize()); - if (!m_allocation.memory->getMemoryPropertyFlags().hasFlags(IDeviceMemoryAllocation::EMPF_HOST_COHERENT_BIT)) - base.m_device->invalidateMappedMemoryRanges(1, &memoryRange); - - assert(memoryRange.valid() && memoryRange.length >= sizeof(TestValues)); - - m_queue = m_base.m_device->getQueue(m_queueFamily, 0); - } - - ~EF64Submitter() - { - m_allocation.memory->unmap(); - } - - void setPushConstants(PushConstants& pc) - { - m_pushConstants = pc; - } - - TestValues submitGetGPUTestValues() - { - // record command buffer - m_cmdbuf->reset(IGPUCommandBuffer::RESET_FLAGS::NONE); - m_cmdbuf->begin(IGPUCommandBuffer::USAGE::NONE); - m_cmdbuf->beginDebugMarker("emulated_float64_t compute dispatch", vectorSIMDf(0, 1, 0, 1)); - m_cmdbuf->bindComputePipeline(m_pipeline.get()); - m_cmdbuf->bindDescriptorSets(nbl::asset::EPBP_COMPUTE, m_pplnLayout.get(), 0, 1, &m_ds.get()); - m_cmdbuf->pushConstants(m_pplnLayout.get(), IShader::E_SHADER_STAGE::ESS_COMPUTE, 0, sizeof(PushConstants), &m_pushConstants); - m_cmdbuf->dispatch(WORKGROUP_SIZE, 1, 1); - m_cmdbuf->endDebugMarker(); - m_cmdbuf->end(); - - IQueue::SSubmitInfo submitInfos[1] = {}; - const IQueue::SSubmitInfo::SCommandBufferInfo cmdbufs[] = { {.cmdbuf = m_cmdbuf.get()}}; - submitInfos[0].commandBuffers = cmdbufs; - const IQueue::SSubmitInfo::SSemaphoreInfo signals[] = { {.semaphore = m_semaphore.get(), .value = ++m_semaphoreCounter, .stageMask = asset::PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT}}; - submitInfos[0].signalSemaphores = signals; - - m_base.m_api->startCapture(); - m_queue->submit(submitInfos); - m_base.m_api->endCapture(); - - m_base.m_device->waitIdle(); - TestValues output; - std::memcpy(&output, static_cast*>(m_allocation.memory->getMappedPointer()), sizeof(TestValues)); - m_base.m_device->waitIdle(); - - return output; - } - - private: - uint32_t m_queueFamily; - nbl::video::IDeviceMemoryAllocator::SAllocation m_allocation = {}; - smart_refctd_ptr m_cmdbuf = nullptr; - smart_refctd_ptr m_cmdpool = nullptr; - smart_refctd_ptr m_ds = nullptr; - smart_refctd_ptr m_pplnLayout = nullptr; - PushConstants m_pushConstants; - CompatibilityTest& m_base; - smart_refctd_ptr m_pipeline; - smart_refctd_ptr m_semaphore; - IQueue* m_queue; - uint64_t m_semaphoreCounter; - }; - - void emulated_float64_tests() - { - EF64Submitter submitter(*this); - - auto printTestOutput = [this](const std::string& functionName, const EmulatedFloat64TestOutput& testResult) - { - std::cout << functionName << ": " << std::endl; - - if (!testResult.cpuTestsSucceed) - logFail("Incorrect CPU determinated values!"); - else - m_logger->log("Correct CPU determinated values!", ILogger::ELL_PERFORMANCE); - - if (!testResult.gpuTestsSucceed) - logFail("Incorrect GPU determinated values!"); - else - m_logger->log("Correct GPU determinated values!", ILogger::ELL_PERFORMANCE); - }; - - m_logFile.open("EmulatedFloatTestLog.txt", std::ios::out | std::ios::trunc); - if (!m_logFile.is_open()) - m_logger->log("Failed to open log file!", system::ILogger::ELL_ERROR); - - printTestOutput("emulatedFloat64RandomValuesTest", emulatedFloat64RandomValuesTest(submitter)); - printTestOutput("emulatedFloat64RandomValuesTestContrastingExponents", emulatedFloat64RandomValuesTestContrastingExponents(submitter)); - printTestOutput("emulatedFloat64NegAndPosZeroTest", emulatedFloat64NegAndPosZeroTest(submitter)); - printTestOutput("emulatedFloat64BothValuesInfTest", emulatedFloat64BothValuesInfTest(submitter)); - printTestOutput("emulatedFloat64BothValuesNegInfTest", emulatedFloat64BothValuesNegInfTest(submitter)); - printTestOutput("emulatedFloat64OneValIsInfOtherIsNegInfTest", emulatedFloat64OneValIsInfOtherIsNegInfTest(submitter)); - printTestOutput("emulatedFloat64OneValIsInfTest", emulatedFloat64OneValIsInfTest(submitter)); - printTestOutput("emulatedFloat64OneValIsNegInfTest", emulatedFloat64OneValIsNegInfTest(submitter)); - if(false) // doesn't work for some reason + fast math is enabled by default - printTestOutput("emulatedFloat64BNaNTest", emulatedFloat64BNaNTest(submitter)); - printTestOutput("emulatedFloat64BInfTest", emulatedFloat64OneValIsZeroTest(submitter)); - printTestOutput("emulatedFloat64BNegInfTest", emulatedFloat64OneValIsNegZeroTest(submitter)); - - m_logFile.close(); - } - - template - struct EmulatedFloat64TestValuesInfo - { - emulated_float64_t a; - emulated_float64_t b; - ConstructorTestValues constrTestValues; - TestValues expectedTestValues; - - void fillExpectedTestValues() - { - double aAsDouble = reinterpret_cast(a); - double bAsDouble = reinterpret_cast(b); - - expectedTestValues.a = a.data; - expectedTestValues.b = b.data; - - expectedTestValues.int32CreateVal = bit_cast(double(constrTestValues.int32)); - expectedTestValues.int64CreateVal = bit_cast(double(constrTestValues.int64)); - expectedTestValues.uint32CreateVal = bit_cast(double(constrTestValues.uint32)); - expectedTestValues.uint64CreateVal = bit_cast(double(constrTestValues.uint64)); - expectedTestValues.float32CreateVal = bit_cast(double(constrTestValues.float32)); - expectedTestValues.float64CreateVal = bit_cast(constrTestValues.float64); - expectedTestValues.additionVal = emulated_float64_t::create(aAsDouble + bAsDouble).data; - expectedTestValues.substractionVal = emulated_float64_t::create(aAsDouble - bAsDouble).data; - expectedTestValues.multiplicationVal = emulated_float64_t::create(aAsDouble * bAsDouble).data; - expectedTestValues.divisionVal = emulated_float64_t::create(aAsDouble / bAsDouble).data; - expectedTestValues.lessOrEqualVal = aAsDouble <= bAsDouble; - expectedTestValues.greaterOrEqualVal = aAsDouble >= bAsDouble; - expectedTestValues.equalVal = aAsDouble == bAsDouble; - expectedTestValues.notEqualVal = aAsDouble != bAsDouble; - expectedTestValues.lessVal = aAsDouble < bAsDouble; - expectedTestValues.greaterVal = aAsDouble > bAsDouble; - } - }; - - struct EmulatedFloat64TestOutput - { - bool cpuTestsSucceed; - bool gpuTestsSucceed; - }; - - EmulatedFloat64TestOutput emulatedFloat64LoopedTests_impl(EF64Submitter& submitter, - const uint32_t iterations, - const std::function& determineValueA, - const std::function& determineValueB) - { - EmulatedFloat64TestOutput output = { true, true }; - - std::uniform_int_distribution i32Distribution(-std::numeric_limits::max(), std::numeric_limits::max()); - std::uniform_int_distribution i64Distribution(-std::numeric_limits::max(), std::numeric_limits::max()); - std::uniform_int_distribution u32Distribution(-std::numeric_limits::max(), std::numeric_limits::max()); - std::uniform_int_distribution u64Distribution(-std::numeric_limits::max(), std::numeric_limits::max()); - std::uniform_real_distribution fDistribution(-100000.0, 100000.0); - - std::random_device rd; - std::mt19937 mt(rd()); - - for (uint32_t i = 0u; i < iterations; ++i) - { - // generate random test values - EmulatedFloat64TestValuesInfo testValInfo; - double aTmp = determineValueA(); - double bTmp = determineValueB(); - testValInfo.a.data = reinterpret_cast::storage_t&>(aTmp); - testValInfo.b.data = reinterpret_cast::storage_t&>(bTmp); - testValInfo.constrTestValues.int32 = i32Distribution(mt); - testValInfo.constrTestValues.int64 = i64Distribution(mt); - testValInfo.constrTestValues.uint32 = u32Distribution(mt); - testValInfo.constrTestValues.uint64 = u64Distribution(mt); - testValInfo.constrTestValues.float32 = fDistribution(mt); - testValInfo.constrTestValues.float64 = fDistribution(mt); - - testValInfo.fillExpectedTestValues(); - auto singleTestOutput = performEmulatedFloat64Tests(testValInfo, submitter); - - if (!singleTestOutput.cpuTestsSucceed) - output.cpuTestsSucceed = false; - if (!singleTestOutput.gpuTestsSucceed) - output.gpuTestsSucceed = false; - } - - return output; - } - - EmulatedFloat64TestOutput emulatedFloat64RandomValuesTest(EF64Submitter& submitter) - { - auto getRandomFloat64 = []() - { - static std::random_device rd; - static std::mt19937 mt(rd()); - static std::uniform_real_distribution distribution(-100000.0, 100000.0); - - - return distribution(mt); - }; - - return emulatedFloat64LoopedTests_impl(submitter, EmulatedFloat64TestIterations, getRandomFloat64, getRandomFloat64); - } - - EmulatedFloat64TestOutput emulatedFloat64RandomValuesTestContrastingExponents(EF64Submitter& submitter) - { - auto getRandomSmallFloat64 = []() - { - static std::random_device rd; - static std::mt19937 mt(rd()); - static std::uniform_real_distribution distribution(-0.01, 0.01); - - return distribution(mt); - }; - - auto getRandomLargeFloat64 = []() - { - static std::random_device rd; - static std::mt19937 mt(rd()); - static std::uniform_real_distribution distribution(1000000000.0, 2000000000.0); - static std::uniform_int_distribution coinFlipDistribution(0, 1); - - double output = distribution(mt); - if (coinFlipDistribution(mt)) - output = -output; - - return output; - }; - - EmulatedFloat64TestOutput firstTestOutput = emulatedFloat64LoopedTests_impl(submitter, EmulatedFloat64TestIterations / 2, getRandomSmallFloat64, getRandomLargeFloat64); - EmulatedFloat64TestOutput secondTestOutput = emulatedFloat64LoopedTests_impl(submitter, EmulatedFloat64TestIterations / 2, getRandomLargeFloat64, getRandomSmallFloat64); - - EmulatedFloat64TestOutput output; - output.cpuTestsSucceed = firstTestOutput.cpuTestsSucceed && secondTestOutput.cpuTestsSucceed; - output.gpuTestsSucceed = firstTestOutput.gpuTestsSucceed && secondTestOutput.gpuTestsSucceed; - return output; - } - - EmulatedFloat64TestOutput emulatedFloat64BothValuesNaNTest(EF64Submitter& submitter) - { - smart_refctd_ptr semaphore = m_device->createSemaphore(0); - - EmulatedFloat64TestValuesInfo testValInfo; - const float32_t nan32 = std::numeric_limits::quiet_NaN(); - const float64_t nan64 = std::numeric_limits::quiet_NaN(); - testValInfo.a = emulated_float64_t::create(nan64); - testValInfo.b = emulated_float64_t::create(nan64); - testValInfo.constrTestValues = { - .int32 = std::bit_cast(nan32), - .int64 = std::bit_cast(nan64), - .uint32 = std::bit_cast(nan32), - .uint64 = std::bit_cast(nan64), - .float32 = nan32 - //.float64 = nan64 - }; - - testValInfo.fillExpectedTestValues(); - return performEmulatedFloat64Tests(testValInfo, submitter); - } - - EmulatedFloat64TestOutput emulatedFloat64NegAndPosZeroTest(EF64Submitter& submitter) - { - smart_refctd_ptr semaphore = m_device->createSemaphore(0); - - EmulatedFloat64TestValuesInfo testValInfo; - testValInfo.a = emulated_float64_t::create(ieee754::traits::signMask); - testValInfo.b = emulated_float64_t::create(std::bit_cast(0.0)); - testValInfo.constrTestValues = { - .int32 = 0, - .int64 = 0, - .uint32 = 0, - .uint64 = 0, - .float32 = 0 - }; - - testValInfo.fillExpectedTestValues(); - auto firstTestOutput = performEmulatedFloat64Tests(testValInfo, submitter); - std::swap(testValInfo.a, testValInfo.b); - testValInfo.fillExpectedTestValues(); - auto secondTestOutput = performEmulatedFloat64Tests(testValInfo, submitter); - - return { firstTestOutput.cpuTestsSucceed && secondTestOutput.cpuTestsSucceed, firstTestOutput.gpuTestsSucceed && secondTestOutput.gpuTestsSucceed }; - } - - EmulatedFloat64TestOutput emulatedFloat64BothValuesInfTest(EF64Submitter& submitter) - { - smart_refctd_ptr semaphore = m_device->createSemaphore(0); - - EmulatedFloat64TestValuesInfo testValInfo; - const float32_t inf32 = std::numeric_limits::infinity(); - const float64_t inf64 = std::numeric_limits::infinity(); - testValInfo.a = emulated_float64_t::create(inf64); - testValInfo.b = emulated_float64_t::create(inf64); - testValInfo.constrTestValues = { - .int32 = 0, - .int64 = 0, - .uint32 = 0, - .uint64 = 0, - .float32 = inf32 - //.float64 = inf64 - }; - - testValInfo.fillExpectedTestValues(); - return performEmulatedFloat64Tests(testValInfo, submitter); - } - - EmulatedFloat64TestOutput emulatedFloat64BothValuesNegInfTest(EF64Submitter& submitter) - { - smart_refctd_ptr semaphore = m_device->createSemaphore(0); - - EmulatedFloat64TestValuesInfo testValInfo; - const float32_t inf32 = -std::numeric_limits::infinity(); - const float64_t inf64 = -std::numeric_limits::infinity(); - testValInfo.a = emulated_float64_t::create(inf64); - testValInfo.b = emulated_float64_t::create(inf64); - testValInfo.constrTestValues = { - .int32 = 0, - .int64 = 0, - .uint32 = 0, - .uint64 = 0, - .float32 = inf32 - //.float64 = inf64 - }; - - testValInfo.fillExpectedTestValues(); - return performEmulatedFloat64Tests(testValInfo, submitter); - } - - EmulatedFloat64TestOutput emulatedFloat64OneValIsInfOtherIsNegInfTest(EF64Submitter& submitter) - { - smart_refctd_ptr semaphore = m_device->createSemaphore(0); - - EmulatedFloat64TestValuesInfo testValInfo; - const float64_t inf64 = -std::numeric_limits::infinity(); - testValInfo.a = emulated_float64_t::create(inf64); - testValInfo.b = emulated_float64_t::create(inf64); - testValInfo.constrTestValues = { - .int32 = 0, - .int64 = 0, - .uint32 = 0, - .uint64 = 0, - .float32 = 0 - //.float64 = inf64 - }; - - testValInfo.fillExpectedTestValues(); - auto firstTestOutput = performEmulatedFloat64Tests(testValInfo, submitter); - std::swap(testValInfo.a, testValInfo.b); - testValInfo.fillExpectedTestValues(); - auto secondTestOutput = performEmulatedFloat64Tests(testValInfo, submitter); - - return { firstTestOutput.cpuTestsSucceed && secondTestOutput.cpuTestsSucceed, firstTestOutput.gpuTestsSucceed && secondTestOutput.gpuTestsSucceed }; - } - - // TODO: fix - EmulatedFloat64TestOutput emulatedFloat64BNaNTest(EF64Submitter& submitter) - { - EmulatedFloat64TestOutput output = { true, true }; - smart_refctd_ptr semaphore = m_device->createSemaphore(0); - - for (uint32_t i = 0u; i < EmulatedFloat64TestIterations; ++i) - { - std::random_device rd; - std::mt19937 mt(rd()); - - std::uniform_int_distribution i32Distribution(-std::numeric_limits::max(), std::numeric_limits::max()); - std::uniform_int_distribution i64Distribution(-std::numeric_limits::max(), std::numeric_limits::max()); - std::uniform_int_distribution u32Distribution(-std::numeric_limits::max(), std::numeric_limits::max()); - std::uniform_int_distribution u64Distribution(-std::numeric_limits::max(), std::numeric_limits::max()); - std::uniform_real_distribution f32Distribution(-100000.0f, 100000.0f); - std::uniform_real_distribution f64Distribution(-100000.0, 100000.0); - - EmulatedFloat64TestValuesInfo testValInfo; - double aTmp = f64Distribution(mt); - double bTmp = std::numeric_limits::quiet_NaN(); - testValInfo.a.data = reinterpret_cast::storage_t&>(aTmp); - testValInfo.b.data = reinterpret_cast::storage_t&>(bTmp); - testValInfo.constrTestValues.int32 = i32Distribution(mt); - testValInfo.constrTestValues.int64 = i64Distribution(mt); - testValInfo.constrTestValues.uint32 = u32Distribution(mt); - testValInfo.constrTestValues.uint64 = u64Distribution(mt); - testValInfo.constrTestValues.float32 = f32Distribution(mt); - //testValInfo.constrTestValues.float64 = f64Distribution(mt); - - testValInfo.fillExpectedTestValues(); - auto singleTestOutput = performEmulatedFloat64Tests(testValInfo, submitter); - - if (!singleTestOutput.cpuTestsSucceed) - output.cpuTestsSucceed = false; - if (!singleTestOutput.gpuTestsSucceed) - output.gpuTestsSucceed = false; - } - - return output; - } - - EmulatedFloat64TestOutput emulatedFloat64OneValIsInfTest(EF64Submitter& submitter) - { - auto getRandomFloat64 = []() - { - static std::random_device rd; - static std::mt19937 mt(rd()); - static std::uniform_real_distribution distribution(-100000.0, 100000.0); - - return distribution(mt); - }; - - auto getInfinity = []() - { - return std::numeric_limits::infinity(); - }; - - EmulatedFloat64TestOutput firstTestOutput = emulatedFloat64LoopedTests_impl(submitter, EmulatedFloat64TestIterations / 2, getRandomFloat64, getInfinity); - EmulatedFloat64TestOutput secondTestOutput = emulatedFloat64LoopedTests_impl(submitter, EmulatedFloat64TestIterations / 2, getInfinity, getRandomFloat64); - - EmulatedFloat64TestOutput output; - output.cpuTestsSucceed = firstTestOutput.cpuTestsSucceed && secondTestOutput.cpuTestsSucceed; - output.gpuTestsSucceed = firstTestOutput.gpuTestsSucceed && secondTestOutput.gpuTestsSucceed; - return output; - } - - EmulatedFloat64TestOutput emulatedFloat64OneValIsNegInfTest(EF64Submitter& submitter) - { - auto getRandomFloat64 = []() - { - static std::random_device rd; - static std::mt19937 mt(rd()); - static std::uniform_real_distribution distribution(-100000.0, 100000.0); - - - return distribution(mt); - }; - - auto getNegInfinity = []() - { - return -std::numeric_limits::infinity(); - }; - - EmulatedFloat64TestOutput firstTestOutput = emulatedFloat64LoopedTests_impl(submitter, EmulatedFloat64TestIterations / 2, getRandomFloat64, getNegInfinity); - EmulatedFloat64TestOutput secondTestOutput = emulatedFloat64LoopedTests_impl(submitter, EmulatedFloat64TestIterations / 2, getNegInfinity, getRandomFloat64); - - EmulatedFloat64TestOutput output; - output.cpuTestsSucceed = firstTestOutput.cpuTestsSucceed && secondTestOutput.cpuTestsSucceed; - output.gpuTestsSucceed = firstTestOutput.gpuTestsSucceed && secondTestOutput.gpuTestsSucceed; - return output; - } - - EmulatedFloat64TestOutput emulatedFloat64OneValIsZeroTest(EF64Submitter& submitter) - { - auto getRandomFloat64 = []() - { - static std::random_device rd; - static std::mt19937 mt(rd()); - static std::uniform_real_distribution distribution(-100000.0, 100000.0); - - return distribution(mt); - }; - - auto getZero = []() - { - return 0.0; - }; - - EmulatedFloat64TestOutput firstTestOutput = emulatedFloat64LoopedTests_impl(submitter, EmulatedFloat64TestIterations / 2, getRandomFloat64, getZero); - EmulatedFloat64TestOutput secondTestOutput = emulatedFloat64LoopedTests_impl(submitter, EmulatedFloat64TestIterations / 2, getZero, getRandomFloat64); - - EmulatedFloat64TestOutput output; - output.cpuTestsSucceed = firstTestOutput.cpuTestsSucceed && secondTestOutput.cpuTestsSucceed; - output.gpuTestsSucceed = firstTestOutput.gpuTestsSucceed && secondTestOutput.gpuTestsSucceed; - return output; - } - - EmulatedFloat64TestOutput emulatedFloat64OneValIsNegZeroTest(EF64Submitter& submitter) - { - auto getRandomFloat64 = []() - { - static std::random_device rd; - static std::mt19937 mt(rd()); - static std::uniform_real_distribution distribution(-100000.0, 100000.0); - - return distribution(mt); - }; - - auto getNegZero = []() - { - return -0.0; - }; - - EmulatedFloat64TestOutput firstTestOutput = emulatedFloat64LoopedTests_impl(submitter, EmulatedFloat64TestIterations / 2, getRandomFloat64, getNegZero); - EmulatedFloat64TestOutput secondTestOutput = emulatedFloat64LoopedTests_impl(submitter, EmulatedFloat64TestIterations / 2, getNegZero, getRandomFloat64); - - EmulatedFloat64TestOutput output; - output.cpuTestsSucceed = firstTestOutput.cpuTestsSucceed && secondTestOutput.cpuTestsSucceed; - output.gpuTestsSucceed = firstTestOutput.gpuTestsSucceed && secondTestOutput.gpuTestsSucceed; - return output; - } - - template - EmulatedFloat64TestOutput performEmulatedFloat64Tests(EmulatedFloat64TestValuesInfo& testValInfo, EF64Submitter& submitter) - { - emulated_float64_t a = testValInfo.a; - emulated_float64_t b = testValInfo.b; - - const TestValues cpuTestValues = { - .int32CreateVal = emulated_float64_t::create(testValInfo.constrTestValues.int32).data, - .int64CreateVal = emulated_float64_t::create(testValInfo.constrTestValues.int64).data, - .uint32CreateVal = emulated_float64_t::create(testValInfo.constrTestValues.uint32).data, - .uint64CreateVal = emulated_float64_t::create(testValInfo.constrTestValues.uint64).data, - .float32CreateVal = emulated_float64_t::create(testValInfo.constrTestValues.float32).data, - .float64CreateVal = emulated_float64_t::create(testValInfo.constrTestValues.float64).data, - .additionVal = (a + b).data, - .substractionVal = (a - b).data, - .multiplicationVal = (a * b).data, - .divisionVal = (a / b).data, - .lessOrEqualVal = a <= b, - .greaterOrEqualVal = a >= b, - .equalVal = a == b, - .notEqualVal = a != b, - .lessVal = a < b, - .greaterVal = a > b - }; - - EmulatedFloat64TestOutput output; - - // cpu validation - output.cpuTestsSucceed = compareEmulatedFloat64TestValues(testValInfo.expectedTestValues, cpuTestValues); - - // gpu validation - PushConstants pc; - pc.a = reinterpret_cast(a); - pc.b = reinterpret_cast(b); - pc.constrTestVals = testValInfo.constrTestValues; - - submitter.setPushConstants(pc); - auto gpuTestValues = submitter.submitGetGPUTestValues(); - - output.gpuTestsSucceed = compareEmulatedFloat64TestValues(testValInfo.expectedTestValues, gpuTestValues); - - return output; - } - - class EF64Benchmark final - { - public: - EF64Benchmark(CompatibilityTest& base) - { - m_device = base.m_device; - m_logger = base.m_logger; - m_api = base.m_api; - - // setting up pipeline in the constructor - m_queueFamily = base.getComputeQueue()->getFamilyIndex(); - m_cmdpool = base.m_device->createCommandPool(m_queueFamily, IGPUCommandPool::CREATE_FLAGS::RESET_COMMAND_BUFFER_BIT); - //core::smart_refctd_ptr* cmdBuffs[] = { &m_cmdbuf, &m_timestampBeforeCmdBuff, &m_timestampAfterCmdBuff }; - if (!m_cmdpool->createCommandBuffers(IGPUCommandPool::BUFFER_LEVEL::PRIMARY, 1u, &m_cmdbuf)) - base.logFail("Failed to create Command Buffers!\n"); - if (!m_cmdpool->createCommandBuffers(IGPUCommandPool::BUFFER_LEVEL::PRIMARY, 1u, &m_timestampBeforeCmdBuff)) - base.logFail("Failed to create Command Buffers!\n"); - if (!m_cmdpool->createCommandBuffers(IGPUCommandPool::BUFFER_LEVEL::PRIMARY, 1u, &m_timestampAfterCmdBuff)) - base.logFail("Failed to create Command Buffers!\n"); - - // Load shaders, set up pipeline - { - smart_refctd_ptr shader; - { - IAssetLoader::SAssetLoadParams lp = {}; - lp.logger = base.m_logger.get(); - lp.workingDirectory = "app_resources"; // virtual root - // this time we load a shader directly from a file - auto key = nbl::this_example::builtin::build::get_spirv_key<"benchmark">(m_device.get()); - auto assetBundle = base.m_assetMgr->getAsset(key.data(), lp); - const auto assets = assetBundle.getContents(); - if (assets.empty()) - { - base.logFail("Could not load shader!"); - assert(0); - } - - // It would be super weird if loading a shader from a file produced more than 1 asset - assert(assets.size() == 1); - shader = IAsset::castDown(assets[0]); - } - - if (!shader) - base.logFail("Failed to load precompiled \"benchmark\" shader!\n"); - - nbl::video::IGPUDescriptorSetLayout::SBinding bindings[1] = { - { - .binding = 0, - .type = nbl::asset::IDescriptor::E_TYPE::ET_STORAGE_BUFFER, - .createFlags = IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - .stageFlags = ShaderStage::ESS_COMPUTE, - .count = 1 - } - }; - smart_refctd_ptr dsLayout = base.m_device->createDescriptorSetLayout(bindings); - if (!dsLayout) - base.logFail("Failed to create a Descriptor Layout!\n"); - - SPushConstantRange pushConstantRanges[] = { - { - .stageFlags = ShaderStage::ESS_COMPUTE, - .offset = 0, - .size = sizeof(BenchmarkPushConstants) - } - }; - m_pplnLayout = base.m_device->createPipelineLayout(pushConstantRanges, smart_refctd_ptr(dsLayout)); - if (!m_pplnLayout) - base.logFail("Failed to create a Pipeline Layout!\n"); - - { - IGPUComputePipeline::SCreationParams params = {}; - params.layout = m_pplnLayout.get(); - params.shader.entryPoint = "main"; - params.shader.shader = shader.get(); - if (base.m_device->getEnabledFeatures().pipelineExecutableInfo) - { - params.flags |= IGPUComputePipeline::SCreationParams::FLAGS::CAPTURE_STATISTICS; - params.flags |= IGPUComputePipeline::SCreationParams::FLAGS::CAPTURE_INTERNAL_REPRESENTATIONS; - } - if (!base.m_device->createComputePipelines(nullptr, { ¶ms,1 }, &m_pipeline)) - base.logFail("Failed to create pipelines (compile & link shaders)!\n"); - - if (base.m_device->getEnabledFeatures().pipelineExecutableInfo) - { - auto report = system::to_string(m_pipeline->getExecutableInfo()); - base.m_logger->log("EF64Benchmark Pipeline Executable Report:\n%s", ILogger::ELL_PERFORMANCE, report.c_str()); - } - } - - // Allocate the memory - { - static_assert(sizeof(float64_t) == sizeof(benchmark_emulated_float64_t)); - constexpr size_t BufferSize = BENCHMARK_WORKGROUP_COUNT * BENCHMARK_WORKGROUP_DIMENSION_SIZE_X * - BENCHMARK_WORKGROUP_DIMENSION_SIZE_Y * BENCHMARK_WORKGROUP_DIMENSION_SIZE_Z * sizeof(float64_t); - - nbl::video::IGPUBuffer::SCreationParams params = {}; - params.size = BufferSize; - params.usage = IGPUBuffer::EUF_STORAGE_BUFFER_BIT; - smart_refctd_ptr dummyBuff = base.m_device->createBuffer(std::move(params)); - if (!dummyBuff) - base.logFail("Failed to create a GPU Buffer of size %d!\n", params.size); - - dummyBuff->setObjectDebugName("benchmark buffer"); - - nbl::video::IDeviceMemoryBacked::SDeviceMemoryRequirements reqs = dummyBuff->getMemoryReqs(); - - m_allocation = base.m_device->allocate(reqs, dummyBuff.get(), nbl::video::IDeviceMemoryAllocation::EMAF_NONE); - if (!m_allocation.isValid()) - base.logFail("Failed to allocate Device Memory compatible with our GPU Buffer!\n"); - - assert(dummyBuff->getBoundMemory().memory == m_allocation.memory.get()); - smart_refctd_ptr pool = base.m_device->createDescriptorPoolForDSLayouts(IDescriptorPool::ECF_NONE, { &dsLayout.get(),1 }); - - m_ds = pool->createDescriptorSet(std::move(dsLayout)); - { - IGPUDescriptorSet::SDescriptorInfo info[1]; - info[0].desc = smart_refctd_ptr(dummyBuff); - info[0].info.buffer = { .offset = 0,.size = BufferSize }; - IGPUDescriptorSet::SWriteDescriptorSet writes[1] = { - {.dstSet = m_ds.get(),.binding = 0,.arrayElement = 0,.count = 1,.info = info} - }; - base.m_device->updateDescriptorSets(writes, {}); - } - } - } - - IQueryPool::SCreationParams queryPoolCreationParams{}; - queryPoolCreationParams.queryType = IQueryPool::TYPE::TIMESTAMP; - queryPoolCreationParams.queryCount = 2; - queryPoolCreationParams.pipelineStatisticsFlags = IQueryPool::PIPELINE_STATISTICS_FLAGS::NONE; - m_queryPool = m_device->createQueryPool(queryPoolCreationParams); - - m_computeQueue = m_device->getQueue(m_queueFamily, 0); - } - - void run() - { - m_logger->log("\n\nfloat64_t benchmark result:", ILogger::ELL_PERFORMANCE); - performBenchmark(EF64_BENCHMARK_MODE::NATIVE); - m_logger->log("emulated_float64_t benchmark, fast math enabled result:", ILogger::ELL_PERFORMANCE); - performBenchmark(EF64_BENCHMARK_MODE::EF64_FAST_MATH_ENABLED); - m_logger->log("emulated_float64_t benchmark, fast math disabled result:", ILogger::ELL_PERFORMANCE); - performBenchmark(EF64_BENCHMARK_MODE::EF64_FAST_MATH_DISABLED); - // every subgroup with even ID do calculations with the `emulated_float64_t` type, other subgroups do calculations with float64_t - m_logger->log("emulated_float64_t benchmark, subgroup divided work result:", ILogger::ELL_PERFORMANCE); - performBenchmark(EF64_BENCHMARK_MODE::SUBGROUP_DIVIDED_WORK); - // every item does calculations with both emulated and native types - m_logger->log("emulated_float64_t benchmark, interleaved result:", ILogger::ELL_PERFORMANCE); - performBenchmark(EF64_BENCHMARK_MODE::INTERLEAVED); - } - - private: - void performBenchmark(EF64_BENCHMARK_MODE mode) - { - m_device->waitIdle(); - - recordTimestampQueryCmdBuffers(); - - uint64_t semaphoreCounter = 0; - smart_refctd_ptr semaphore = m_device->createSemaphore(semaphoreCounter); - - IQueue::SSubmitInfo::SSemaphoreInfo signals[] = { {.semaphore = semaphore.get(), .value = 0u, .stageMask = asset::PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT} }; - IQueue::SSubmitInfo::SSemaphoreInfo waits[] = { {.semaphore = semaphore.get(), .value = 0u, .stageMask = asset::PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT } }; - - IQueue::SSubmitInfo beforeTimestapSubmitInfo[1] = {}; - const IQueue::SSubmitInfo::SCommandBufferInfo cmdbufsBegin[] = { {.cmdbuf = m_timestampBeforeCmdBuff.get()} }; - beforeTimestapSubmitInfo[0].commandBuffers = cmdbufsBegin; - beforeTimestapSubmitInfo[0].signalSemaphores = signals; - beforeTimestapSubmitInfo[0].waitSemaphores = waits; - - IQueue::SSubmitInfo afterTimestapSubmitInfo[1] = {}; - const IQueue::SSubmitInfo::SCommandBufferInfo cmdbufsEnd[] = { {.cmdbuf = m_timestampAfterCmdBuff.get()} }; - afterTimestapSubmitInfo[0].commandBuffers = cmdbufsEnd; - afterTimestapSubmitInfo[0].signalSemaphores = signals; - afterTimestapSubmitInfo[0].waitSemaphores = waits; - - IQueue::SSubmitInfo benchmarkSubmitInfos[1] = {}; - const IQueue::SSubmitInfo::SCommandBufferInfo cmdbufs[] = { {.cmdbuf = m_cmdbuf.get()} }; - benchmarkSubmitInfos[0].commandBuffers = cmdbufs; - benchmarkSubmitInfos[0].signalSemaphores = signals; - benchmarkSubmitInfos[0].waitSemaphores = waits; - - - m_pushConstants.benchmarkMode = mode; - recordCmdBuff(); - - // warmup runs - for (int i = 0; i < WarmupIterations; ++i) - { - if(i == 0) - m_api->startCapture(); - waits[0].value = semaphoreCounter; - signals[0].value = ++semaphoreCounter; - m_computeQueue->submit(benchmarkSubmitInfos); - if (i == 0) - m_api->endCapture(); - } - - waits[0].value = semaphoreCounter; - signals[0].value = ++semaphoreCounter; - m_computeQueue->submit(beforeTimestapSubmitInfo); - - // actual benchmark runs - for (int i = 0; i < Iterations; ++i) - { - waits[0].value = semaphoreCounter; - signals[0].value = ++semaphoreCounter; - m_computeQueue->submit(benchmarkSubmitInfos); - } - - waits[0].value = semaphoreCounter; - signals[0].value = ++semaphoreCounter; - m_computeQueue->submit(afterTimestapSubmitInfo); - - m_device->waitIdle(); - - const uint64_t nativeBenchmarkTimeElapsedNanoseconds = calcTimeElapsed(); - const float nativeBenchmarkTimeElapsedSeconds = double(nativeBenchmarkTimeElapsedNanoseconds) / 1000000000.0; - - m_logger->log("%llu ns, %f s", ILogger::ELL_PERFORMANCE, nativeBenchmarkTimeElapsedNanoseconds, nativeBenchmarkTimeElapsedSeconds); - } - - void recordCmdBuff() - { - m_cmdbuf->begin(IGPUCommandBuffer::USAGE::SIMULTANEOUS_USE_BIT); - m_cmdbuf->beginDebugMarker("emulated_float64_t compute dispatch", vectorSIMDf(0, 1, 0, 1)); - m_cmdbuf->bindComputePipeline(m_pipeline.get()); - m_cmdbuf->bindDescriptorSets(nbl::asset::EPBP_COMPUTE, m_pplnLayout.get(), 0, 1, &m_ds.get()); - m_cmdbuf->pushConstants(m_pplnLayout.get(), IShader::E_SHADER_STAGE::ESS_COMPUTE, 0, sizeof(BenchmarkPushConstants), &m_pushConstants); - m_cmdbuf->dispatch(BENCHMARK_WORKGROUP_COUNT, 1, 1); - m_cmdbuf->endDebugMarker(); - m_cmdbuf->end(); - } - - void recordTimestampQueryCmdBuffers() - { - static bool firstInvocation = true; - - if (!firstInvocation) - { - m_timestampBeforeCmdBuff->reset(IGPUCommandBuffer::RESET_FLAGS::NONE); - m_timestampBeforeCmdBuff->reset(IGPUCommandBuffer::RESET_FLAGS::NONE); - } - - m_timestampBeforeCmdBuff->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT); - m_timestampBeforeCmdBuff->resetQueryPool(m_queryPool.get(), 0, 2); - m_timestampBeforeCmdBuff->writeTimestamp(PIPELINE_STAGE_FLAGS::NONE, m_queryPool.get(), 0); - m_timestampBeforeCmdBuff->end(); - - m_timestampAfterCmdBuff->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT); - m_timestampAfterCmdBuff->writeTimestamp(PIPELINE_STAGE_FLAGS::NONE, m_queryPool.get(), 1); - m_timestampAfterCmdBuff->end(); - - firstInvocation = false; - } - - uint64_t calcTimeElapsed() - { - uint64_t timestamps[2]; - const core::bitflag flags = core::bitflag(IQueryPool::RESULTS_FLAGS::_64_BIT) | core::bitflag(IQueryPool::RESULTS_FLAGS::WAIT_BIT); - m_device->getQueryPoolResults(m_queryPool.get(), 0, 2, ×tamps, sizeof(uint64_t), flags); - return timestamps[1] - timestamps[0]; - } - - private: - core::smart_refctd_ptr m_api; - smart_refctd_ptr m_device; - smart_refctd_ptr m_logger; - - nbl::video::IDeviceMemoryAllocator::SAllocation m_allocation = {}; - smart_refctd_ptr m_cmdpool = nullptr; - smart_refctd_ptr m_cmdbuf = nullptr; - smart_refctd_ptr m_ds = nullptr; - smart_refctd_ptr m_pplnLayout = nullptr; - BenchmarkPushConstants m_pushConstants; - smart_refctd_ptr m_pipeline; - - smart_refctd_ptr m_timestampBeforeCmdBuff = nullptr; - smart_refctd_ptr m_timestampAfterCmdBuff = nullptr; - smart_refctd_ptr m_queryPool = nullptr; - - uint32_t m_queueFamily; - IQueue* m_computeQueue; - static constexpr int WarmupIterations = 1000; - static constexpr int Iterations = 1000; - using benchmark_emulated_float64_t = emulated_float64_t; - }; - - template - inline bool logFail(const char* msg, Args&&... args) - { - m_logger->log(msg, ILogger::ELL_ERROR, std::forward(args)...); - return false; - } - - std::ofstream m_logFile; -}; - -NBL_MAIN_FUNC(CompatibilityTest) \ No newline at end of file diff --git a/66_HLSLBxDFTests/CMakeLists.txt b/66_HLSLBxDFTests/CMakeLists.txt deleted file mode 100644 index 608d46d8f..000000000 --- a/66_HLSLBxDFTests/CMakeLists.txt +++ /dev/null @@ -1,54 +0,0 @@ -set(NBL_INCLUDE_SEARCH_DIRECTORIES - "${CMAKE_CURRENT_SOURCE_DIR}/include" -) - -include(common RESULT_VARIABLE RES) -if(NOT RES) - message(FATAL_ERROR "common.cmake not found. Should be in {repo_root}/cmake directory") -endif() - -nbl_create_executable_project("" "" "${NBL_INCLUDE_SEARCH_DIRECTORIES}" "" "${NBL_EXECUTABLE_PROJECT_CREATION_PCH_TARGET}") - -if(NBL_EMBED_BUILTIN_RESOURCES) - set(_BR_TARGET_ ${EXECUTABLE_NAME}_builtinResourceData) - set(RESOURCE_DIR "app_resources") - - get_filename_component(_SEARCH_DIRECTORIES_ "${CMAKE_CURRENT_SOURCE_DIR}" ABSOLUTE) - get_filename_component(_OUTPUT_DIRECTORY_SOURCE_ "${CMAKE_CURRENT_BINARY_DIR}/src" ABSOLUTE) - get_filename_component(_OUTPUT_DIRECTORY_HEADER_ "${CMAKE_CURRENT_BINARY_DIR}/include" ABSOLUTE) - - file(GLOB_RECURSE BUILTIN_RESOURCE_FILES RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}/${RESOURCE_DIR}" CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/${RESOURCE_DIR}/*") - foreach(RES_FILE ${BUILTIN_RESOURCE_FILES}) - LIST_BUILTIN_RESOURCE(RESOURCES_TO_EMBED "${RES_FILE}") - endforeach() - - ADD_CUSTOM_BUILTIN_RESOURCES(${_BR_TARGET_} RESOURCES_TO_EMBED "${_SEARCH_DIRECTORIES_}" "${RESOURCE_DIR}" "nbl::this_example::builtin" "${_OUTPUT_DIRECTORY_HEADER_}" "${_OUTPUT_DIRECTORY_SOURCE_}") - - LINK_BUILTIN_RESOURCES_TO_TARGET(${EXECUTABLE_NAME} ${_BR_TARGET_}) -endif() - -set(OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/auto-gen") - -set(JSON [=[ -[ - { - "INPUT": "app_resources/test_compile.comp.hlsl", - "KEY": "shader", - "COMPILE_OPTIONS": ["-T", "cs_6_8"], - "DEPENDS": [], - "CAPS": [] - } -] -]=]) - -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 -I ${CMAKE_CURRENT_SOURCE_DIR} - OUTPUT_VAR KEYS - INCLUDE nbl/this_example/builtin/build/spirv/keys.hpp - NAMESPACE nbl::this_example::builtin::build - INPUTS ${JSON} -) diff --git a/66_HLSLBxDFTests/app_resources/config.json b/66_HLSLBxDFTests/app_resources/config.json deleted file mode 100644 index 611f1c996..000000000 --- a/66_HLSLBxDFTests/app_resources/config.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "logInfo": false, - "TestJacobian": { - "runs": 10, - "verbose": true - }, - "TestReciprocity": { - "runs": 10, - "verbose": true - }, - "TestModifiedWhiteFurnace": { - "runs": 10, - "samples": 100000 - }, - "TestChi2": { - "runs": 10, - "samples": 1000000, - "thetaSplits": 80, - "phiSplits": 160, - "writeFrequencies": 1 - }, - "TestNDF": { - "runs": 10, - "verbose": true - }, - "TestCTGenerateH": { - "runs": 10, - "samples": 1000000, - "immediateFail": false - } -} diff --git a/66_HLSLBxDFTests/app_resources/test_compile.comp.hlsl b/66_HLSLBxDFTests/app_resources/test_compile.comp.hlsl deleted file mode 100644 index c293e65bb..000000000 --- a/66_HLSLBxDFTests/app_resources/test_compile.comp.hlsl +++ /dev/null @@ -1,103 +0,0 @@ -#include "nbl/builtin/hlsl/cpp_compat.hlsl" - -#include "nbl/builtin/hlsl/bxdf/common.hlsl" -#include "nbl/builtin/hlsl/bxdf/reflection.hlsl" -#include "nbl/builtin/hlsl/bxdf/transmission.hlsl" - -[[vk::binding(0,0)]] RWStructuredBuffer buff; - -using namespace nbl::hlsl; - -using spectral_t = vector; -using ray_dir_info_t = bxdf::ray_dir_info::SBasic; -using iso_interaction = bxdf::surface_interactions::SIsotropic; -using aniso_interaction = bxdf::surface_interactions::SAnisotropic; -using sample_t = bxdf::SLightSample; -using iso_cache = bxdf::SIsotropicMicrofacetCache; -using aniso_cache = bxdf::SAnisotropicMicrofacetCache; -using quotient_pdf_t = sampling::quotient_and_pdf; - -using iso_config_t = bxdf::SConfiguration; -using aniso_config_t = bxdf::SConfiguration; -using iso_microfacet_config_t = bxdf::SMicrofacetConfiguration; -using aniso_microfacet_config_t = bxdf::SMicrofacetConfiguration; - -[numthreads(64,1,1)] -void main(uint32_t3 ID : SV_DispatchThreadID) -{ - bxdf::reflection::SLambertian lambertianBRDF; - bxdf::reflection::SOrenNayar orenNayarBRDF; - bxdf::reflection::SDeltaDistribution deltaDistBRDF; - bxdf::reflection::SBeckmannIsotropic beckmannIsoBRDF; - bxdf::reflection::SBeckmannAnisotropic beckmannAnisoBRDF; - bxdf::reflection::SGGXIsotropic ggxIsoBRDF; - bxdf::reflection::SGGXAnisotropic ggxAnisoBRDF; - bxdf::reflection::SIridescent iridBRDF; - - bxdf::transmission::SLambertian lambertianBSDF; - bxdf::transmission::SOrenNayar orenNayarBSDF; - bxdf::transmission::SSmoothDielectric smoothDielectricBSDF; - bxdf::transmission::SThinSmoothDielectric thinSmoothDielectricBSDF; - bxdf::transmission::SDeltaDistribution deltaDistBSDF; - bxdf::transmission::SBeckmannDielectricIsotropic beckmannIsoBSDF; - bxdf::transmission::SBeckmannDielectricAnisotropic beckmannAnisoBSDF; - bxdf::transmission::SGGXDielectricIsotropic ggxIsoBSDF; - bxdf::transmission::SGGXDielectricAnisotropic ggxAnisoBSDF; - bxdf::transmission::SIridescent iridBSDF; - - - // do some nonsense calculations, but call all the relevant functions - ray_dir_info_t V; - V.direction = nbl::hlsl::normalize(float3(1, 1, 1)); - const float3 N = float3(0, 1, 0); - float3 T, B; - math::frisvad(N, T, B); - const float3 u = float3(0.5, 0.5, 0); - - iso_interaction isointer = iso_interaction::create(V, N); - aniso_interaction anisointer = aniso_interaction::create(isointer, T, B); - aniso_cache cache; - - float3 L = float3(0,0,0); - float3 q = float3(0,0,0); - sample_t s = lambertianBRDF.generate(anisointer, u.xy); - L += s.L.direction; - - s = orenNayarBRDF.generate(anisointer, u.xy); - L += s.L.direction; - - quotient_pdf_t qp = orenNayarBRDF.quotient_and_pdf(s, isointer); - L -= qp.quotient; - - s = beckmannAnisoBRDF.generate(anisointer, u.xy, cache); - L += s.L.direction; - - qp = beckmannAnisoBRDF.quotient_and_pdf(s, anisointer, cache); - L -= qp.quotient; - - s = ggxAnisoBRDF.generate(anisointer, u.xy, cache); - L += s.L.direction; - - qp = iridBRDF.quotient_and_pdf(s, anisointer, cache); - L -= qp.quotient; - - qp = ggxAnisoBRDF.quotient_and_pdf(s, anisointer, cache); - L -= qp.quotient; - - s = lambertianBSDF.generate(anisointer, u); - L += s.L.direction; - - s = thinSmoothDielectricBSDF.generate(anisointer, u); - L += s.L.direction; - - qp = thinSmoothDielectricBSDF.quotient_and_pdf(s, isointer); - L -= qp.quotient; - - s = ggxAnisoBSDF.generate(anisointer, u, cache); - L += s.L.direction; - - qp = ggxAnisoBSDF.quotient_and_pdf(s, anisointer, cache); - L -= qp.quotient; - - buff[ID.x] = L; -} diff --git a/66_HLSLBxDFTests/app_resources/test_components.hlsl b/66_HLSLBxDFTests/app_resources/test_components.hlsl deleted file mode 100644 index 2570d68f9..000000000 --- a/66_HLSLBxDFTests/app_resources/test_components.hlsl +++ /dev/null @@ -1,342 +0,0 @@ -#ifndef BXDFTESTS_TEST_COMPONENTS_HLSL -#define BXDFTESTS_TEST_COMPONENTS_HLSL - -#include "tests_common.hlsl" - -template // only for cook torrance bxdfs -struct TestNDF : TestBxDF -{ - using base_t = TestBxDFBase; - using this_t = TestNDF; - using traits_t = bxdf::traits; - - TestResult compute() - { - aniso_cache dummy; - iso_cache dummy_iso; - - // avoid cases where ux or uy might end up outside the input domain when eps is added - if (!checkLt(base_t::rc.u, hlsl::promote(1.0-base_t::rc.eps))) - return BTR_INVALID_TEST_CONFIG; - - base_t::rc.u.z = hlsl::mix(0.0, 1.0, base_t::rc.u.z > 0.5); - float32_t3 ux = base_t::rc.u + float32_t3(eps,0,0); - float32_t3 uy = base_t::rc.u + float32_t3(0,eps,0); - - NBL_IF_CONSTEXPR(traits_t::type == bxdf::BT_BRDF && traits_t::IsMicrofacet) - { - NBL_IF_CONSTEXPR(aniso) - { - s = base_t::bxdf.generate(base_t::anisointer, base_t::rc.u.xy, cache); - sx = base_t::bxdf.generate(base_t::anisointer, ux.xy, dummy); - sy = base_t::bxdf.generate(base_t::anisointer, uy.xy, dummy); - } - else - { - s = base_t::bxdf.generate(base_t::isointer, base_t::rc.u.xy, isocache); - sx = base_t::bxdf.generate(base_t::isointer, ux.xy, dummy_iso); - sy = base_t::bxdf.generate(base_t::isointer, uy.xy, dummy_iso); - } - } - NBL_IF_CONSTEXPR(traits_t::type == bxdf::BT_BSDF && traits_t::IsMicrofacet) - { - NBL_IF_CONSTEXPR(aniso) - { - s = base_t::bxdf.generate(base_t::anisointer, base_t::rc.u, cache); - sx = base_t::bxdf.generate(base_t::anisointer, ux, dummy); - sy = base_t::bxdf.generate(base_t::anisointer, uy, dummy); - } - else - { - s = base_t::bxdf.generate(base_t::isointer, base_t::rc.u, isocache); - sx = base_t::bxdf.generate(base_t::isointer, ux, dummy_iso); - sy = base_t::bxdf.generate(base_t::isointer, uy, dummy_iso); - } - } - - if (!BxDF::ndf_type::GuaranteedVNDF && !(s.isValid() && sx.isValid() && sy.isValid())) - return BTR_INVALID_TEST_CONFIG; - - using ndf_type = typename base_t::bxdf_t::ndf_type; - using quant_type = typename ndf_type::quant_type; - using quant_query_type = typename ndf_type::quant_query_type; - using dg1_query_type = typename ndf_type::dg1_query_type; - using fresnel_type = typename base_t::bxdf_t::fresnel_type; - - float reflectance; - bool isNdfInfinity; - bool transmitted; - NBL_IF_CONSTEXPR(aniso) - { - dg1_query_type dq = base_t::bxdf.ndf.template createDG1Query(base_t::anisointer, cache); - fresnel_type _f = base_t::bxdf_t::__getOrientedFresnel(base_t::bxdf.fresnel, base_t::anisointer.getNdotV()); - quant_query_type qq = bxdf::impl::quant_query_helper::template __call(base_t::bxdf.ndf, _f, base_t::anisointer, cache); - quant_type DG1 = base_t::bxdf.ndf.template DG1(dq, qq, s, base_t::anisointer, isNdfInfinity); - dg1 = DG1.projectedLightMeasure; - - float VdotH = cache.getVdotH(); - NBL_IF_CONSTEXPR (traits_t::type == bxdf::BT_BSDF) - VdotH = hlsl::abs(VdotH); - reflectance = _f(VdotH)[0]; - transmitted = cache.isTransmission(); - } - else - { - dg1_query_type dq = base_t::bxdf.ndf.template createDG1Query(base_t::isointer, isocache); - fresnel_type _f = base_t::bxdf_t::__getOrientedFresnel(base_t::bxdf.fresnel, base_t::isointer.getNdotV()); - quant_query_type qq = bxdf::impl::quant_query_helper::template __call(base_t::bxdf.ndf, _f, base_t::isointer, isocache); - quant_type DG1 = base_t::bxdf.ndf.template DG1(dq, qq, s, base_t::isointer, isNdfInfinity); - dg1 = DG1.projectedLightMeasure; - - float VdotH = isocache.getVdotH(); - NBL_IF_CONSTEXPR(traits_t::type == bxdf::BT_BSDF) - VdotH = hlsl::abs(VdotH); - reflectance = _f(VdotH)[0]; - transmitted = isocache.isTransmission(); - } - - if (isNdfInfinity) - return BTR_INVALID_TEST_CONFIG; - - if (reflectance < 0.f || reflectance > 1.f) - { -#ifndef __HLSL_VERSION - if (verbose) - base_t::errMsg += std::format("reflectance={}, eta={}, transmitted={}", reflectance, base_t::rc.eta.x, transmitted ? "true" : "false"); -#endif - return BTR_ERROR_REFLECTANCE_OUT_OF_RANGE; - } - - return BTR_NONE; - } - - TestResult test() - { - if (traits_t::type == bxdf::BT_BRDF) - { - if (base_t::isointer.getNdotV() <= bit_cast(numeric_limits::min)) - return BTR_INVALID_TEST_CONFIG; - } - else if (traits_t::type == bxdf::BT_BSDF) - { - if (hlsl::abs(base_t::isointer.getNdotV()) <= bit_cast(numeric_limits::min)) - return BTR_INVALID_TEST_CONFIG; - } - - TestResult res = compute(); - if (res != BTR_NONE) - return res; - - const float absNdotL = hlsl::abs(s.getNdotL()); - if (absNdotL <= bit_cast(numeric_limits::min)) - return BTR_INVALID_TEST_CONFIG; - - // get jacobian - float32_t2x2 m = float32_t2x2( - sx.getTdotL() - s.getTdotL(), sy.getTdotL() - s.getTdotL(), - sx.getBdotL() - s.getBdotL(), sy.getBdotL() - s.getBdotL() - ); - float det = nbl::hlsl::determinant(m) / (eps * eps); - - float jacobi_dg1 = det * dg1 / absNdotL; - if (!checkZero(det, 1e-3) && !testing::relativeApproxCompare(jacobi_dg1, 1.0, 0.1)) - { -#ifndef __HLSL_VERSION - if (verbose) - base_t::errMsg += std::format("VdotH={}, NdotV={}, LdotH={}, NdotL={}, eta={}, alpha=[{},{}] Jacobian={}, DG1={}, Jacobian*DG1={}", - aniso ? cache.getVdotH() : isocache.getVdotH(), aniso ? base_t::anisointer.getNdotV() : base_t::isointer.getNdotV(), - aniso ? cache.getLdotH() : isocache.getLdotH(), s.getNdotL(), base_t::rc.eta.x, base_t::rc.alpha.x, base_t::rc.alpha.y, - det, dg1, jacobi_dg1); -#endif - return BTR_ERROR_JACOBIAN_TEST_FAIL; - } - - return BTR_NONE; - } - - static bool run(NBL_CONST_REF_ARG(STestInitParams) initparams, NBL_REF_ARG(FailureCallback) cb) - { - this_t t; - t.init(initparams.halfSeed); - t.rc.halfSeed = initparams.halfSeed; - t.verbose = initparams.verbose; - t.initBxDF(t.rc); - - TestResult e = t.test(); - if (e != BTR_NONE) - cb.__call(e, t, initparams.logInfo); - return e >= BTR_NOBREAK; - } - - float eps = 1e-5; - sample_t s, sx, sy; - aniso_cache cache; - iso_cache isocache; - float dg1; - bool verbose; -}; - -template -struct TestCTGenerateH : TestBxDF -{ - using base_t = TestBxDFBase; - using this_t = TestCTGenerateH; - using traits_t = bxdf::traits; - - TestResult compute() - { - counter.reset(); - - sample_t s; - iso_cache isocache; - aniso_cache cache; - for (uint32_t i = 0; i < numSamples; i++) - { - float32_t3 u = ConvertToFloat01::__call(base_t::rc.rng_vec<3>()); - - NBL_IF_CONSTEXPR(traits_t::type == bxdf::BT_BRDF && !traits_t::IsMicrofacet) - { - s = base_t::bxdf.generate(base_t::anisointer, u.xy); - } - NBL_IF_CONSTEXPR(traits_t::type == bxdf::BT_BRDF && traits_t::IsMicrofacet) - { - NBL_IF_CONSTEXPR(aniso) - s = base_t::bxdf.generate(base_t::anisointer, u.xy, cache); - else - s = base_t::bxdf.generate(base_t::isointer, u.xy, isocache); - } - NBL_IF_CONSTEXPR(traits_t::type == bxdf::BT_BSDF && !traits_t::IsMicrofacet) - { - s = base_t::bxdf.generate(base_t::anisointer, u); - } - NBL_IF_CONSTEXPR(traits_t::type == bxdf::BT_BSDF && traits_t::IsMicrofacet) - { - NBL_IF_CONSTEXPR(aniso) - s = base_t::bxdf.generate(base_t::anisointer, u, cache); - else - s = base_t::bxdf.generate(base_t::isointer, u, isocache); - } - - if (!BxDF::ndf_type::GuaranteedVNDF && !s.isValid()) - continue; - - bool transmitted; - float NdotV, VdotH; - float dotProductVdotL, VdotL; - NBL_IF_CONSTEXPR(aniso) - { - NdotV = base_t::anisointer.getNdotV(); - VdotH = cache.getVdotH(); - transmitted = cache.isTransmission(); - dotProductVdotL = hlsl::dot(base_t::anisointer.getV().getDirection(), s.getL().getDirection()); - VdotL = cache.getVdotL(); - } - else - { - NdotV = base_t::isointer.getNdotV(); - VdotH = isocache.getVdotH(); - transmitted = isocache.isTransmission(); - dotProductVdotL = hlsl::dot(base_t::isointer.getV().getDirection(), s.getL().getDirection()); - VdotL = isocache.getVdotL(); - } - - if (!(NdotV * VdotH >= 0.f)) - { - if (immediateFail) - { - base_t::errMsg += std::format("first failed case (NdotV*VdotH): i={}, u=[{},{},{}] NdotV={}, VdotH={}", i, u.x, u.y, u.z, NdotV, VdotH); - return BTR_WARNING_GENERATED_H_INVALID; - } - else - { - counter.NdotVVdotHfail++; - transmitted ? counter.transmitted++ : counter.reflected++; - } - } - - if (!checkZero(dotProductVdotL - VdotL, 1e-4)) - { - if (immediateFail) - { - base_t::errMsg += std::format("first failed case (compare VdotL): i={}, u=[{},{},{}] {}!={}", i, u.x, u.y, u.z, dotProductVdotL, VdotL); - return BTR_WARNING_GENERATED_H_INVALID; - } - else - { - counter.VdotLfail++; - transmitted ? counter.transmitted++ : counter.reflected++; - } - } - - counter.total++; - } - - float totalFails = counter.totalFails(); - if (totalFails > 0) - { - base_t::errMsg += std::format("fail count={} out of {} valid samples: [{}] NdotV*VdotH, [{}] compare VdotL, [{}] transmitted, [{}] reflected, alpha=[{},{}]", - totalFails, counter.total, counter.NdotVVdotHfail, counter.VdotLfail, - counter.transmitted, counter.reflected, base_t::rc.alpha.x, base_t::rc.alpha.y); - return BTR_WARNING_GENERATED_H_INVALID; - } - - return BTR_NONE; - } - - TestResult test() - { - if (traits_t::type == bxdf::BT_BRDF) - if (base_t::isointer.getNdotV() <= numeric_limits::min) - return BTR_INVALID_TEST_CONFIG; - else if (traits_t::type == bxdf::BT_BSDF) - if (hlsl::abs(base_t::isointer.getNdotV()) <= numeric_limits::min) - return BTR_INVALID_TEST_CONFIG; - - TestResult res = compute(); - if (res != BTR_NONE) - return res; - - return BTR_NONE; - } - - static bool run(NBL_CONST_REF_ARG(STestInitParams) initparams, NBL_REF_ARG(FailureCallback) cb) - { - this_t t; - t.init(initparams.halfSeed); - t.rc.halfSeed = initparams.halfSeed; - t.numSamples = initparams.samples; - t.immediateFail = initparams.immediateFail; - t.initBxDF(t.rc); - - TestResult e = t.test(); - if (e != BTR_NONE) - cb.__call(e, t, initparams.logInfo); - return e >= BTR_NOBREAK; - } - - struct Counter - { - uint32_t NdotVVdotHfail; - uint32_t VdotLfail; - uint32_t reflected; - uint32_t transmitted; - uint32_t total; - - void reset() - { - NdotVVdotHfail = 0; - VdotLfail = 0; - reflected = 0; - transmitted = 0; - total = 0; - } - - float totalFails() { return NdotVVdotHfail + VdotLfail; } - }; - - bool immediateFail = false; - uint32_t numSamples = 1000000; - Counter counter; -}; - -#endif diff --git a/66_HLSLBxDFTests/app_resources/tests.hlsl b/66_HLSLBxDFTests/app_resources/tests.hlsl deleted file mode 100644 index 790dab2f3..000000000 --- a/66_HLSLBxDFTests/app_resources/tests.hlsl +++ /dev/null @@ -1,394 +0,0 @@ -#ifndef BXDFTESTS_TESTS_HLSL -#define BXDFTESTS_TESTS_HLSL - -#include "tests_common.hlsl" - -template -struct TestJacobian : TestBxDF -{ - using base_t = TestBxDFBase; - using this_t = TestJacobian; - using traits_t = bxdf::traits; - - TestResult compute() - { - aniso_cache cache, dummy; - iso_cache isocache, dummy_iso; - - // avoid cases where ux or uy might end up outside the input domain when eps is added - if (!checkLt(base_t::rc.u, hlsl::promote(1.0-base_t::rc.eps))) - return BTR_INVALID_TEST_CONFIG; - - base_t::rc.u.z = hlsl::mix(0.0, 1.0, base_t::rc.u.z > 0.5); - float32_t3 ux = base_t::rc.u + float32_t3(base_t::rc.eps,0,0); - float32_t3 uy = base_t::rc.u + float32_t3(0,base_t::rc.eps,0); - - NBL_IF_CONSTEXPR(traits_t::type == bxdf::BT_BRDF && !traits_t::IsMicrofacet) - { - s = base_t::bxdf.generate(base_t::isointer, base_t::rc.u.xy); - sx = base_t::bxdf.generate(base_t::isointer, ux.xy); - sy = base_t::bxdf.generate(base_t::isointer, uy.xy); - } - NBL_IF_CONSTEXPR(traits_t::type == bxdf::BT_BRDF && traits_t::IsMicrofacet) - { - NBL_IF_CONSTEXPR(aniso) - { - s = base_t::bxdf.generate(base_t::anisointer, base_t::rc.u.xy, cache); - sx = base_t::bxdf.generate(base_t::anisointer, ux.xy, dummy); - sy = base_t::bxdf.generate(base_t::anisointer, uy.xy, dummy); - } - else - { - s = base_t::bxdf.generate(base_t::isointer, base_t::rc.u.xy, isocache); - sx = base_t::bxdf.generate(base_t::isointer, ux.xy, dummy_iso); - sy = base_t::bxdf.generate(base_t::isointer, uy.xy, dummy_iso); - } - } - NBL_IF_CONSTEXPR(traits_t::type == bxdf::BT_BSDF && !traits_t::IsMicrofacet) - { - s = base_t::bxdf.generate(base_t::anisointer, base_t::rc.u); - sx = base_t::bxdf.generate(base_t::anisointer, ux); - sy = base_t::bxdf.generate(base_t::anisointer, uy); - } - NBL_IF_CONSTEXPR(traits_t::type == bxdf::BT_BSDF && traits_t::IsMicrofacet) - { - NBL_IF_CONSTEXPR(aniso) - { - s = base_t::bxdf.generate(base_t::anisointer, base_t::rc.u, cache); - sx = base_t::bxdf.generate(base_t::anisointer, ux, dummy); - sy = base_t::bxdf.generate(base_t::anisointer, uy, dummy); - } - else - { - s = base_t::bxdf.generate(base_t::isointer, base_t::rc.u, isocache); - sx = base_t::bxdf.generate(base_t::isointer, ux, dummy_iso); - sy = base_t::bxdf.generate(base_t::isointer, uy, dummy_iso); - } - } - - // TODO: might want to distinguish between invalid H and sample produced below hemisphere - if (!(s.isValid() && sx.isValid() && sy.isValid())) - return BTR_INVALID_TEST_CONFIG; - - NBL_IF_CONSTEXPR(!traits_t::IsMicrofacet) - { - sampledLi = base_t::bxdf.quotient_and_pdf(s, base_t::isointer); - Li = float32_t3(base_t::bxdf.eval(s, base_t::isointer)); - transmitted = base_t::isointer.getNdotV() * s.getNdotL() < 0.f; - } - NBL_IF_CONSTEXPR(traits_t::IsMicrofacet) - { - NBL_IF_CONSTEXPR(aniso) - { - sampledLi = base_t::bxdf.quotient_and_pdf(s, base_t::anisointer, cache); - Li = float32_t3(base_t::bxdf.eval(s, base_t::anisointer, cache)); - transmitted = cache.isTransmission(); - } - else - { - sampledLi = base_t::bxdf.quotient_and_pdf(s, base_t::isointer, isocache); - Li = float32_t3(base_t::bxdf.eval(s, base_t::isointer, isocache)); - transmitted = isocache.isTransmission(); - } - } - - return BTR_NONE; - } - - TestResult test() - { - if (traits_t::type == bxdf::BT_BRDF) - { - if (base_t::isointer.getNdotV() <= bit_cast(numeric_limits::min)) - return BTR_INVALID_TEST_CONFIG; - } - else if (traits_t::type == bxdf::BT_BSDF) - { - if (hlsl::abs(base_t::isointer.getNdotV()) <= bit_cast(numeric_limits::min)) - return BTR_INVALID_TEST_CONFIG; - } - - TestResult res = compute(); - if (res != BTR_NONE) - return res; - - if (sampledLi.pdf < 0.f) // pdf should not be negative - return BTR_ERROR_NEGATIVE_VAL; - - if (sampledLi.pdf < bit_cast(numeric_limits::min)) // there's exceptional cases where pdf=0, so we check here to avoid adding all edge-cases, but quotient must be positive afterwards - return BTR_NONE; - - if (checkLt(Li, hlsl::promote(0.0)) || checkLt(sampledLi.quotient, hlsl::promote(0.0))) - return BTR_ERROR_NEGATIVE_VAL; - - if (!checkLt(sampledLi.quotient, hlsl::promote(bit_cast(numeric_limits::infinity)))) // importance sampler's job to prevent inf - return BTR_ERROR_QUOTIENT_INF; - - // we've already checked above if: - // 1. PDF is positive - // 2. quotient is positive and (1) already checked - // So if we must have `eval == quotient*pdf` , then eval must also be positive - // However for mixture of, or singular delta BxDF the bsdf can be less due to removal of Dirac-Delta lobes from the eval method, which is why allow `BTR_NONE` in this case - if (checkZero(Li, 1e-5) || checkZero(sampledLi.quotient, 1e-5)) - return BTR_NONE; - - if (hlsl::isnan(sampledLi.pdf)) - return BTR_ERROR_GENERATED_SAMPLE_NAN_PDF; - - // get jacobian - float32_t2x2 m = float32_t2x2( - sx.getTdotL() - s.getTdotL(), sy.getTdotL() - s.getTdotL(), - sx.getBdotL() - s.getBdotL(), sy.getBdotL() - s.getBdotL() - ); - float det = nbl::hlsl::determinant(m); - - if (hlsl::isinf(sampledLi.pdf)) - { - // if pdf is infinite then density is infinite and no differential area inbetween samples - if (!checkZero(det, numeric_limits::min * base_t::rc.eps * base_t::rc.eps)) - return BTR_ERROR_JACOBIAN_TEST_FAIL; - // valid behaviour, but obviously can't check eval = quotient*pdf - return BTR_NONE; - } - else if (checkZero(det, numeric_limits::min * base_t::rc.eps * base_t::rc.eps)) - { -#ifndef __HLSL_VERSION - if (verbose) - base_t::errMsg += std::format("determinant={}", det); -#endif - return BTR_ERROR_JACOBIAN_TEST_FAIL; - } - - float32_t3 quo_pdf = sampledLi.value(); - if (!testing::relativeApproxCompare(quo_pdf, Li, 1e-4)) - { -#ifndef __HLSL_VERSION - if (verbose) - base_t::errMsg += std::format("transmitted={}, quotient*pdf=[{},{},{}] eval=[{},{},{}]", - transmitted ? "true" : "false", - quo_pdf.x, quo_pdf.y, quo_pdf.z, - Li.x, Li.y, Li.z); -#endif - return BTR_ERROR_PDF_EVAL_DIFF; - } - - return BTR_NONE; - } - - static bool run(NBL_CONST_REF_ARG(STestInitParams) initparams, NBL_REF_ARG(FailureCallback) cb) - { - this_t t; - t.init(initparams.halfSeed); - t.rc.halfSeed = initparams.halfSeed; - t.verbose = initparams.verbose; - t.initBxDF(t.rc); - - TestResult e = t.test(); - if (e != BTR_NONE) - cb.__call(e, t, initparams.logInfo); - return e >= BTR_NOBREAK; - } - - sample_t s, sx, sy; - quotient_pdf_t sampledLi; - float32_t3 Li; - bool transmitted; - bool verbose; -}; - -template -struct TestReciprocity : TestBxDF -{ - using base_t = TestBxDFBase; - using this_t = TestReciprocity; - using traits_t = bxdf::traits; - - using iso_interaction_t = typename BxDF::isotropic_interaction_type; - using aniso_interaction_t = typename BxDF::anisotropic_interaction_type; - - TestResult compute() - { - aniso_cache cache, rec_cache; - iso_cache isocache, rec_isocache; - - NBL_IF_CONSTEXPR(traits_t::type == bxdf::BT_BSDF && traits_t::IsMicrofacet) - { - isointer = iso_interaction_t::template copy(base_t::isointer); - anisointer = aniso_interaction_t::template copy(base_t::anisointer); - } - else - { - isointer = base_t::isointer; - anisointer = base_t::anisointer; - } - - NBL_IF_CONSTEXPR(traits_t::type == bxdf::BT_BRDF && !traits_t::IsMicrofacet) - { - s = base_t::bxdf.generate(anisointer, base_t::rc.u.xy); - } - NBL_IF_CONSTEXPR(traits_t::type == bxdf::BT_BRDF && traits_t::IsMicrofacet) - { - NBL_IF_CONSTEXPR(aniso) - { - s = base_t::bxdf.generate(anisointer, base_t::rc.u.xy, cache); - } - else - { - s = base_t::bxdf.generate(isointer, base_t::rc.u.xy, isocache); - } - } - NBL_IF_CONSTEXPR(traits_t::type == bxdf::BT_BSDF && !traits_t::IsMicrofacet) - { - s = base_t::bxdf.generate(anisointer, base_t::rc.u); - } - NBL_IF_CONSTEXPR(traits_t::type == bxdf::BT_BSDF && traits_t::IsMicrofacet) - { - NBL_IF_CONSTEXPR(aniso) - { - s = base_t::bxdf.generate(anisointer, base_t::rc.u, cache); - } - else - { - s = base_t::bxdf.generate(isointer, base_t::rc.u, isocache); - } - } - - // TODO: might want to distinguish between invalid H and sample produced below hemisphere - if (!s.isValid()) - return BTR_INVALID_TEST_CONFIG; - - float32_t3x3 toTangentSpace = anisointer.getToTangentSpace(); - ray_dir_info_t rec_V = s.getL(); - ray_dir_info_t rec_localV = rec_V.transform(toTangentSpace); - ray_dir_info_t rec_localL = base_t::rc.V.transform(toTangentSpace); - rec_s = sample_t::createFromTangentSpace(rec_localL, anisointer.getFromTangentSpace()); - - NBL_IF_CONSTEXPR(traits_t::IsMicrofacet) - transmitted = aniso ? cache.isTransmission() : isocache.isTransmission(); - else - transmitted = false; - - rec_isointer = iso_interaction_t::create(rec_V, base_t::rc.N); - rec_isointer.luminosityContributionHint = isointer.luminosityContributionHint; - rec_anisointer = aniso_interaction_t::create(rec_isointer, base_t::rc.T, base_t::rc.B); - rec_cache = cache; - rec_cache.iso_cache.VdotH = cache.iso_cache.getLdotH(); - rec_cache.iso_cache.LdotH = cache.iso_cache.getVdotH(); - rec_isocache = isocache; - rec_isocache.VdotH = isocache.getLdotH(); - rec_isocache.LdotH = isocache.getVdotH(); - - NBL_IF_CONSTEXPR(!traits_t::IsMicrofacet) - { - Li = float32_t3(base_t::bxdf.eval(s, isointer)); - recLi = float32_t3(base_t::bxdf.eval(rec_s, rec_isointer)); - } - NBL_IF_CONSTEXPR(traits_t::type == bxdf::BT_BRDF && traits_t::IsMicrofacet) - { - NBL_IF_CONSTEXPR(aniso) - { - Li = float32_t3(base_t::bxdf.eval(s, anisointer, cache)); - recLi = float32_t3(base_t::bxdf.eval(rec_s, rec_anisointer, rec_cache)); - } - else - { - Li = float32_t3(base_t::bxdf.eval(s, isointer, isocache)); - recLi = float32_t3(base_t::bxdf.eval(rec_s, rec_isointer, rec_isocache)); - } - } - NBL_IF_CONSTEXPR(traits_t::type == bxdf::BT_BSDF && traits_t::IsMicrofacet) - { - NBL_IF_CONSTEXPR(aniso) - { - anisointer.isotropic.pathOrigin = bxdf::PathOrigin::PO_SENSOR; - Li = float32_t3(base_t::bxdf.eval(s, anisointer, cache)); - rec_anisointer.isotropic.pathOrigin = bxdf::PathOrigin::PO_LIGHT; - recLi = float32_t3(base_t::bxdf.eval(rec_s, rec_anisointer, rec_cache)); - } - else - { - isointer.pathOrigin = bxdf::PathOrigin::PO_SENSOR; - Li = float32_t3(base_t::bxdf.eval(s, isointer, isocache)); - rec_isointer.pathOrigin = bxdf::PathOrigin::PO_LIGHT; - recLi = float32_t3(base_t::bxdf.eval(rec_s, rec_isointer, rec_isocache)); - } - } - -#ifndef __HLSL_VERSION - if (verbose) - base_t::errMsg += std::format("isTransmission: {}, NdotV: {}, NdotL: {}, VdotH: {}, LdotH: {}, NdotH: {}", - transmitted ? "true" : "false", - isointer.getNdotV(), s.getNdotL(), - aniso ? cache.getVdotH() : isocache.getVdotH(), aniso ? cache.getLdotH() : isocache.getLdotH(), aniso ? cache.getAbsNdotH() : isocache.getAbsNdotH()); -#endif - - return BTR_NONE; - } - - TestResult test() - { - if (traits_t::type == bxdf::BT_BRDF) - { - if (base_t::isointer.getNdotV() <= bit_cast(numeric_limits::min)) - return BTR_INVALID_TEST_CONFIG; - } - else if (traits_t::type == bxdf::BT_BSDF) - { - if (hlsl::abs(base_t::isointer.getNdotV()) <= bit_cast(numeric_limits::min)) - return BTR_INVALID_TEST_CONFIG; - } - - TestResult res = compute(); - if (res != BTR_NONE) - return res; - - const float absNdotL = hlsl::abs(s.getNdotL()); - if (absNdotL <= bit_cast(numeric_limits::min)) - return BTR_INVALID_TEST_CONFIG; - - if (checkLt(Li, hlsl::promote(0.0))) - return BTR_ERROR_NEGATIVE_VAL; - - if (checkZero(Li, 1e-5)) // we don't have a pdf to check like in the one above but - return BTR_NONE; - - float32_t3 a = Li / absNdotL; - float32_t3 b = recLi / hlsl::abs(rec_s.getNdotL()); - if (!(a == b)) // avoid division by 0 - if (!testing::relativeApproxCompare(a, b, 1.25e-2)) - { -#ifndef __HLSL_VERSION - if (verbose) - base_t::errMsg += std::format(" front=[{},{},{}] rec=[{},{},{}]", - a.x, a.y, a.z, - b.x, b.y, b.z); -#endif - return BTR_ERROR_NO_RECIPROCITY; - } - - return BTR_NONE; - } - - static bool run(NBL_CONST_REF_ARG(STestInitParams) initparams, NBL_REF_ARG(FailureCallback) cb) - { - this_t t; - t.init(initparams.halfSeed); - t.rc.halfSeed = initparams.halfSeed; - t.verbose = initparams.verbose; - t.initBxDF(t.rc); - - TestResult e = t.test(); - if (e != BTR_NONE) - cb.__call(e, t, initparams.logInfo); - return e >= BTR_NOBREAK; - } - - sample_t s, rec_s; - float32_t3 Li, recLi; - iso_interaction_t isointer, rec_isointer; - aniso_interaction_t anisointer, rec_anisointer; - bool transmitted; - bool verbose; -}; - -#endif diff --git a/66_HLSLBxDFTests/app_resources/tests_common.hlsl b/66_HLSLBxDFTests/app_resources/tests_common.hlsl deleted file mode 100644 index 478cd1f55..000000000 --- a/66_HLSLBxDFTests/app_resources/tests_common.hlsl +++ /dev/null @@ -1,642 +0,0 @@ -#ifndef BXDFTESTS_TESTS_COMMON_HLSL -#define BXDFTESTS_TESTS_COMMON_HLSL - -#include "nbl/builtin/hlsl/cpp_compat.hlsl" - -#include "nbl/builtin/hlsl/random/xoroshiro.hlsl" -#include "nbl/builtin/hlsl/random/pcg.hlsl" -#include "nbl/builtin/hlsl/random/dim_adaptor_recursive.hlsl" -#include "nbl/builtin/hlsl/sampling/uniform_spheres.hlsl" -#include "nbl/builtin/hlsl/math/quaternions.hlsl" -#include "nbl/builtin/hlsl/math/polar.hlsl" -#include "nbl/builtin/hlsl/bxdf/common.hlsl" -#include "nbl/builtin/hlsl/bxdf/reflection.hlsl" -#include "nbl/builtin/hlsl/bxdf/transmission.hlsl" -#include "nbl/builtin/hlsl/bxdf/bxdf_traits.hlsl" -#include "nbl/builtin/hlsl/colorspace/encodeCIEXYZ.hlsl" -#include "nbl/builtin/hlsl/testing/relative_approx_compare.hlsl" - -using namespace nbl; -using namespace hlsl; - -using spectral_t = hlsl::vector; -using ray_dir_info_t = bxdf::ray_dir_info::SBasic; -using iso_interaction = bxdf::surface_interactions::SIsotropic; -using aniso_interaction = bxdf::surface_interactions::SAnisotropic; -using sample_t = bxdf::SLightSample; -using iso_cache = bxdf::SIsotropicMicrofacetCache; -using aniso_cache = bxdf::SAnisotropicMicrofacetCache; -using quotient_pdf_t = sampling::quotient_and_pdf; - -using iso_config_t = bxdf::SConfiguration; -using aniso_config_t = bxdf::SConfiguration; -using iso_microfacet_config_t = bxdf::SMicrofacetConfiguration; -using aniso_microfacet_config_t = bxdf::SMicrofacetConfiguration; - -using bool32_t3 = hlsl::vector; - -template) -struct ConvertToFloat01 -{ - using ret_t = conditional_t::Dimension==1, float, hlsl::vector::Dimension> >; - - static ret_t __call(T x) - { - return ret_t(x) / hlsl::promote(numeric_limits::max); - } -}; - -template -bool checkLt(T a, T b) -{ - return nbl::hlsl::all::Dimension> >(a < b); -} - -template -bool checkZero(T a, float32_t eps) -{ - return nbl::hlsl::all::Dimension> >(nbl::hlsl::abs(a) < hlsl::promote(eps)); -} - -template<> -bool checkZero(float32_t a, float32_t eps) -{ - return nbl::hlsl::abs(a) < eps; -} - -struct SBxDFTestResources -{ - static SBxDFTestResources create(uint32_t _halfseed) - { - random::PCG32 pcg = random::PCG32::construct(_halfseed); - uint32_t2 seed = nbl::hlsl::random::DimAdaptorRecursive::__call(pcg); - - SBxDFTestResources retval; - retval.rng = nbl::hlsl::Xoroshiro64Star::construct(seed); - retval.u = ConvertToFloat01::__call(retval.rng_vec<3>()); - retval.u.x = hlsl::clamp(retval.u.x, retval.eps, 1.f-retval.eps); - retval.u.y = hlsl::clamp(retval.u.y, retval.eps, 1.f-retval.eps); - - retval.V.direction = nbl::hlsl::normalize(sampling::UniformSphere::generate(ConvertToFloat01::__call(retval.rng_vec<2>()))); - retval.N = nbl::hlsl::normalize(sampling::UniformSphere::generate(ConvertToFloat01::__call(retval.rng_vec<2>()))); - - float32_t3 tangent, bitangent; - math::frisvad(retval.N, tangent, bitangent); - tangent = nbl::hlsl::normalize(tangent); - bitangent = nbl::hlsl::normalize(bitangent); - - const float angle = 2.0f * numbers::pi * ConvertToFloat01::__call(retval.rng()); - math::quaternion rot = math::quaternion::create(retval.N, angle); - retval.T = rot.transformVector(tangent); - retval.B = rot.transformVector(bitangent); - - retval.alpha.x = hlsl::max(ConvertToFloat01::__call(retval.rng()), 1e-4f); - retval.alpha.y = hlsl::max(ConvertToFloat01::__call(retval.rng()), 1e-4f); - retval.eta = ConvertToFloat01::__call(retval.rng_vec<3>()) * hlsl::promote(1.5) + hlsl::promote(1.1); // range [1.1,2.6], also only do eta = eta/1.0 (air) - retval.etak = ConvertToFloat01::__call(retval.rng_vec<3>()) * hlsl::promote(1.5) + hlsl::promote(1.1); // same as above - retval.luma_coeff = colorspace::scRGBtoXYZ[1]; - - retval.Dinc = ConvertToFloat01::__call(retval.rng()) * 2400.0f + 100.0f; - retval.etaThinFilm = ConvertToFloat01::__call(retval.rng()) * 0.5 + 1.1f; // range [1.1,1.6] - return retval; - } - - template - hlsl::vector rng_vec() - { - // don't construct an adaptor, use a static call which takes base RNG by reference, so modifies its state while producing numbers - return nbl::hlsl::random::DimAdaptorRecursive::__call(rng); - } - - float eps = 1e-3; // epsilon - uint32_t halfSeed; // init state seed, expect each seed to be unique so threads don't clash writing to same filename - - nbl::hlsl::Xoroshiro64Star rng; - ray_dir_info_t V; - float32_t3 N; - float32_t3 T; - float32_t3 B; - - float32_t3 u; - float32_t2 alpha; - float32_t3 eta; - float32_t3 etak; - float32_t3 luma_coeff; - - // thin film stuff; - float Dinc; // in nm [100, 2500] - float etaThinFilm; -}; - -// refer to config to see which params are used in which test -struct STestInitParams -{ - bool logInfo; - uint32_t halfSeed; // state used to get vec2 seed from hash, default: iteration no. - uint32_t samples; // num samples generated for distribution tests, e.g. chi2, bucket, etc. - uint32_t thetaSplits; - uint32_t phiSplits; - uint16_t writeFrequencies; - bool immediateFail; - bool verbose; -}; - -enum TestResult -{ - BTR_NOBREAK = 0, - BTR_NONE = 1, - BTR_PRINT_MSG = 2, - - BTR_ERROR_NEGATIVE_VAL = -1, // pdf/quotient/eval < 0 - BTR_ERROR_GENERATED_SAMPLE_NAN_PDF = -2, // pdf = 0 - BTR_ERROR_QUOTIENT_INF = -3, // quotient -> inf - BTR_ERROR_JACOBIAN_TEST_FAIL = -4, // jacobian * pdf != 0 - BTR_ERROR_PDF_EVAL_DIFF = -5, // quotient * pdf != eval - BTR_ERROR_NO_RECIPROCITY = -6, // eval(incoming) != eval(outgoing) - BTR_ERROR_REFLECTANCE_OUT_OF_RANGE = -7, // reflectance not [0, 1] - BTR_ERROR_QUOTIENT_SUM_TOO_LARGE = -8, // accumulated quotient >= 1.0 - - BTR_WARNING_GENERATED_H_INVALID = 3, // generated H is invalid - - BTR_INVALID_TEST_CONFIG = 4 // returned when test values are outside of expected usage -}; - -struct TestBase -{ - void init(uint32_t halfSeed) - { - rc = SBxDFTestResources::create(halfSeed); - - isointer = iso_interaction::create(rc.V, rc.N); - isointer.luminosityContributionHint = rc.luma_coeff; - anisointer = aniso_interaction::create(isointer, rc.T, rc.B); - } - - SBxDFTestResources rc; - - iso_interaction isointer; - aniso_interaction anisointer; - -#ifndef __HLSL_VERSION - std::string name = "base"; - std::string errMsg = ""; -#endif -}; - -template -struct FailureCallback -{ - virtual void __call(TestResult error, NBL_REF_ARG(TestT) failedFor, bool logInfo) {} -}; - -template -struct TestBxDFBase : TestBase -{ - using bxdf_t = BxDF; - BxDF bxdf; -}; - -template -struct TestBxDF : TestBxDFBase -{ - using base_t = TestBxDFBase; - - void initBxDF(SBxDFTestResources _rc) - { - // default to lambertian bxdf -#ifndef __HLSL_VERSION - base_t::name = "Lambertian BxDF"; -#endif - } -}; - -template<> -struct TestBxDF> : TestBxDFBase> -{ - using base_t = TestBxDFBase>; - - void initBxDF(SBxDFTestResources _rc) - { - base_t::bxdf_t::creation_type params; - params.A = _rc.alpha.x; - base_t::bxdf = bxdf::reflection::SOrenNayar::create(params); -#ifndef __HLSL_VERSION - base_t::name = "OrenNayar BRDF"; -#endif - } -}; - -template<> -struct TestBxDF> : TestBxDFBase> -{ - using base_t = TestBxDFBase>; - - void initBxDF(SBxDFTestResources _rc) - { -#ifndef __HLSL_VERSION - base_t::name = "Delta Distribution BRDF"; -#endif - } -}; - -template<> -struct TestBxDF> : TestBxDFBase> -{ - using base_t = TestBxDFBase>; - - void initBxDF(SBxDFTestResources _rc) - { - base_t::bxdf.ndf = base_t::bxdf_t::ndf_type::create(_rc.alpha.x); - base_t::bxdf.fresnel = base_t::bxdf_t::fresnel_type::create(_rc.eta,_rc.etak); -#ifndef __HLSL_VERSION - base_t::name = "Beckmann BRDF"; -#endif - } -}; - -template<> -struct TestBxDF> : TestBxDFBase> -{ - using base_t = TestBxDFBase>; - - void initBxDF(SBxDFTestResources _rc) - { - base_t::bxdf.ndf = base_t::bxdf_t::ndf_type::create(_rc.alpha.x, _rc.alpha.y); - base_t::bxdf.fresnel = base_t::bxdf_t::fresnel_type::create(_rc.eta,_rc.etak); -#ifndef __HLSL_VERSION - base_t::name = "Beckmann Aniso BRDF"; -#endif - } -}; - -template<> -struct TestBxDF> : TestBxDFBase> -{ - using base_t = TestBxDFBase>; - - void initBxDF(SBxDFTestResources _rc) - { - base_t::bxdf.ndf = base_t::bxdf_t::ndf_type::create(_rc.alpha.x); - base_t::bxdf.fresnel = base_t::bxdf_t::fresnel_type::create(_rc.eta,_rc.etak); -#ifndef __HLSL_VERSION - base_t::name = "GGX BRDF"; -#endif - } -}; - -template<> -struct TestBxDF> : TestBxDFBase> -{ - using base_t = TestBxDFBase>; - - void initBxDF(SBxDFTestResources _rc) - { - base_t::bxdf.ndf = base_t::bxdf_t::ndf_type::create(_rc.alpha.x, _rc.alpha.y); - base_t::bxdf.fresnel = base_t::bxdf_t::fresnel_type::create(_rc.eta,_rc.etak); -#ifndef __HLSL_VERSION - base_t::name = "GGX Aniso BRDF"; -#endif - } -}; - -template<> -struct TestBxDF> : TestBxDFBase> -{ - using base_t = TestBxDFBase>; - - void initBxDF(SBxDFTestResources _rc) - { - base_t::bxdf.ndf = base_t::bxdf_t::ndf_type::create(_rc.alpha.x); - using creation_params_t = base_t::bxdf_t::fresnel_type::creation_params_type; - creation_params_t params; - params.Dinc = _rc.Dinc; - params.ior1 = hlsl::promote(1.0); - params.ior2 = hlsl::promote(_rc.etaThinFilm); - params.ior3 = _rc.eta; - params.iork3 = _rc.etak; - base_t::bxdf.fresnel = base_t::bxdf_t::fresnel_type::create(params); -#ifndef __HLSL_VERSION - base_t::name = "Iridescent BRDF"; -#endif - } -}; - -template<> -struct TestBxDF> : TestBxDFBase> -{ - using base_t = TestBxDFBase>; - - void initBxDF(SBxDFTestResources _rc) - { - base_t::bxdf_t::creation_type params; - params.A = _rc.alpha.x; - base_t::bxdf = bxdf::transmission::SOrenNayar::create(params); -#ifndef __HLSL_VERSION - base_t::name = "OrenNayar BSDF"; -#endif - } -}; - -template<> -struct TestBxDF> : TestBxDFBase> -{ - using base_t = TestBxDFBase>; - - void initBxDF(SBxDFTestResources _rc) - { - base_t::bxdf.orientedEta = bxdf::fresnel::OrientedEtas::create(base_t::isointer.getNdotV(bxdf::BxDFClampMode::BCM_ABS), hlsl::promote(_rc.eta.x)); -#ifndef __HLSL_VERSION - base_t::name = "Smooth dielectric BSDF"; -#endif - } -}; - -template<> -struct TestBxDF> : TestBxDFBase> -{ - using base_t = TestBxDFBase>; - - void initBxDF(SBxDFTestResources _rc) - { - using spectral_type = typename base_t::bxdf_t::spectral_type; - base_t::bxdf.fresnel = bxdf::fresnel::Dielectric::create(bxdf::fresnel::OrientedEtas::create(base_t::isointer.getNdotV(bxdf::BxDFClampMode::BCM_ABS), hlsl::promote(_rc.eta.x))); -#ifndef __HLSL_VERSION - base_t::name = "Thin smooth dielectric BSDF"; -#endif - } -}; - -template<> -struct TestBxDF> : TestBxDFBase> -{ - using base_t = TestBxDFBase>; - - void initBxDF(SBxDFTestResources _rc) - { -#ifndef __HLSL_VERSION - base_t::name = "Delta Distribution BSDF"; -#endif - } -}; - -template -struct TestBxDF> : TestBxDFBase> -{ - using base_t = TestBxDFBase>; - - void initBxDF(SBxDFTestResources _rc) - { - bxdf::fresnel::OrientedEtas orientedEta = bxdf::fresnel::OrientedEtas::create(1.0, hlsl::promote(_rc.eta.x)); - base_t::bxdf.ndf = base_t::bxdf_t::ndf_type::create(_rc.alpha.x); - base_t::bxdf.fresnel = base_t::bxdf_t::fresnel_type::create(orientedEta); -#ifndef __HLSL_VERSION - base_t::name = "Beckmann Dielectric BSDF"; -#endif - } -}; - -template -struct TestBxDF> : TestBxDFBase> -{ - using base_t = TestBxDFBase>; - - void initBxDF(SBxDFTestResources _rc) - { - bxdf::fresnel::OrientedEtas orientedEta = bxdf::fresnel::OrientedEtas::create(1.0, hlsl::promote(_rc.eta.x)); - base_t::bxdf.ndf = base_t::bxdf_t::ndf_type::create(_rc.alpha.x, _rc.alpha.y); - base_t::bxdf.fresnel = base_t::bxdf_t::fresnel_type::create(orientedEta); -#ifndef __HLSL_VERSION - base_t::name = "Beckmann Dielectric Aniso BSDF"; -#endif - } -}; - -template -struct TestBxDF> : TestBxDFBase> -{ - using base_t = TestBxDFBase>; - - void initBxDF(SBxDFTestResources _rc) - { - bxdf::fresnel::OrientedEtas orientedEta = bxdf::fresnel::OrientedEtas::create(1.0, hlsl::promote(_rc.eta.x)); - base_t::bxdf.ndf = base_t::bxdf_t::ndf_type::create(_rc.alpha.x); - base_t::bxdf.fresnel = base_t::bxdf_t::fresnel_type::create(orientedEta); -#ifndef __HLSL_VERSION - base_t::name = "GGX Dielectric BSDF"; -#endif - } -}; - -template -struct TestBxDF> : TestBxDFBase> -{ - using base_t = TestBxDFBase>; - - void initBxDF(SBxDFTestResources _rc) - { - bxdf::fresnel::OrientedEtas orientedEta = bxdf::fresnel::OrientedEtas::create(1.0, hlsl::promote(_rc.eta.x)); - base_t::bxdf.ndf = base_t::bxdf_t::ndf_type::create(_rc.alpha.x, _rc.alpha.y); - base_t::bxdf.fresnel = base_t::bxdf_t::fresnel_type::create(orientedEta); -#ifndef __HLSL_VERSION - base_t::name = "GGX Dielectric Aniso BSDF"; -#endif - } -}; - -template -struct TestBxDF> : TestBxDFBase> -{ - using base_t = TestBxDFBase>; - - void initBxDF(SBxDFTestResources _rc) - { - base_t::bxdf.ndf = base_t::bxdf_t::ndf_type::create(_rc.alpha.x); - using creation_params_t = base_t::bxdf_t::fresnel_type::creation_params_type; - creation_params_t params; - params.Dinc = _rc.Dinc; - params.ior1 = hlsl::promote(1.0); - params.ior2 = hlsl::promote(_rc.etaThinFilm); - params.ior3 = hlsl::promote(_rc.eta.x); - base_t::bxdf.fresnel = base_t::bxdf_t::fresnel_type::create(params); -#ifndef __HLSL_VERSION - base_t::name = "Iridescent BSDF"; -#endif - } -}; - - -namespace reciprocity_test_impl -{ -template && concepts::FloatingPointLikeVectorial) -struct SIsotropic -{ - using this_t = SIsotropic; - using ray_dir_info_type = RayDirInfo; - using scalar_type = typename RayDirInfo::scalar_type; - using vector3_type = typename RayDirInfo::vector3_type; - using spectral_type = Spectrum; - - // WARNING: Changed since GLSL, now arguments need to be normalized! - static this_t create(NBL_CONST_REF_ARG(RayDirInfo) normalizedV, const vector3_type normalizedN) - { - this_t retval; - retval.V = normalizedV; - retval.N = normalizedN; - retval.NdotV = nbl::hlsl::dot(retval.N, retval.V.getDirection()); - retval.NdotV2 = retval.NdotV * retval.NdotV; - retval.luminosityContributionHint = hlsl::promote(1.0); - - return retval; - } - - template) - static this_t copy(NBL_CONST_REF_ARG(I) other) - { - this_t retval; - retval.V = other.getV(); - retval.N = other.getN(); - retval.NdotV = other.getNdotV(); - retval.NdotV2 = other.getNdotV2(); - retval.pathOrigin = bxdf::PathOrigin::PO_SENSOR; - retval.luminosityContributionHint = other.luminosityContributionHint; - return retval; - } - - RayDirInfo getV() NBL_CONST_MEMBER_FUNC { return V; } - vector3_type getN() NBL_CONST_MEMBER_FUNC { return N; } - scalar_type getNdotV(bxdf::BxDFClampMode _clamp = bxdf::BxDFClampMode::BCM_NONE) NBL_CONST_MEMBER_FUNC - { - return bxdf::conditionalAbsOrMax(NdotV, _clamp); - } - scalar_type getNdotV2() NBL_CONST_MEMBER_FUNC { return NdotV2; } - - bxdf::PathOrigin getPathOrigin() NBL_CONST_MEMBER_FUNC { return pathOrigin; } - spectral_type getLuminosityContributionHint() NBL_CONST_MEMBER_FUNC { return luminosityContributionHint; } - - RayDirInfo V; - vector3_type N; - scalar_type NdotV; - scalar_type NdotV2; - bxdf::PathOrigin pathOrigin; - spectral_type luminosityContributionHint; -}; - -template) -struct SAnisotropic -{ - using this_t = SAnisotropic; - using isotropic_interaction_type = IsotropicInteraction; - using ray_dir_info_type = typename isotropic_interaction_type::ray_dir_info_type; - using scalar_type = typename ray_dir_info_type::scalar_type; - using vector3_type = typename ray_dir_info_type::vector3_type; - using matrix3x3_type = hlsl::matrix; - using spectral_type = typename isotropic_interaction_type::spectral_type; - - // WARNING: Changed since GLSL, now arguments need to be normalized! - static this_t create( - NBL_CONST_REF_ARG(isotropic_interaction_type) isotropic, - const vector3_type normalizedT, - const vector3_type normalizedB - ) - { - this_t retval; - retval.isotropic = isotropic; - - retval.T = normalizedT; - retval.B = normalizedB; - - retval.TdotV = nbl::hlsl::dot(retval.isotropic.getV().getDirection(), retval.T); - retval.BdotV = nbl::hlsl::dot(retval.isotropic.getV().getDirection(), retval.B); - - return retval; - } - static this_t create(NBL_CONST_REF_ARG(isotropic_interaction_type) isotropic, const vector3_type normalizedT) - { - return create(isotropic, normalizedT, cross(isotropic.getN(), normalizedT)); - } - static this_t create(NBL_CONST_REF_ARG(isotropic_interaction_type) isotropic) - { - vector3_type T, B; - math::frisvad(isotropic.getN(), T, B); - return create(isotropic, nbl::hlsl::normalize(T), nbl::hlsl::normalize(B)); - } - - static this_t create(NBL_CONST_REF_ARG(ray_dir_info_type) normalizedV, const vector3_type normalizedN) - { - isotropic_interaction_type isotropic = isotropic_interaction_type::create(normalizedV, normalizedN); - return create(isotropic); - } - - template) - static this_t copy(NBL_CONST_REF_ARG(I) other) - { - this_t retval; - retval.isotropic = isotropic_interaction_type::template copy(other.isotropic); - retval.T = other.getT(); - retval.B = other.getB(); - retval.TdotV = other.getTdotV(); - retval.BdotV = other.getBdotV(); - return retval; - } - - ray_dir_info_type getV() NBL_CONST_MEMBER_FUNC { return isotropic.getV(); } - vector3_type getN() NBL_CONST_MEMBER_FUNC { return isotropic.getN(); } - scalar_type getNdotV(bxdf::BxDFClampMode _clamp = bxdf::BxDFClampMode::BCM_NONE) NBL_CONST_MEMBER_FUNC { return isotropic.getNdotV(_clamp); } - scalar_type getNdotV2() NBL_CONST_MEMBER_FUNC { return isotropic.getNdotV2(); } - bxdf::PathOrigin getPathOrigin() NBL_CONST_MEMBER_FUNC { return isotropic.getPathOrigin(); } - spectral_type getLuminosityContributionHint() NBL_CONST_MEMBER_FUNC { return isotropic.getLuminosityContributionHint(); } - - vector3_type getT() NBL_CONST_MEMBER_FUNC { return T; } - vector3_type getB() NBL_CONST_MEMBER_FUNC { return B; } - scalar_type getTdotV() NBL_CONST_MEMBER_FUNC { return TdotV; } - scalar_type getTdotV2() NBL_CONST_MEMBER_FUNC { const scalar_type t = getTdotV(); return t*t; } - scalar_type getBdotV() NBL_CONST_MEMBER_FUNC { return BdotV; } - scalar_type getBdotV2() NBL_CONST_MEMBER_FUNC { const scalar_type t = getBdotV(); return t*t; } - - vector3_type getTangentSpaceV() NBL_CONST_MEMBER_FUNC { return vector3_type(TdotV, BdotV, isotropic.getNdotV()); } - matrix3x3_type getToTangentSpace() NBL_CONST_MEMBER_FUNC { return matrix3x3_type(T, B, isotropic.getN()); } - matrix3x3_type getFromTangentSpace() NBL_CONST_MEMBER_FUNC { return nbl::hlsl::transpose(matrix3x3_type(T, B, isotropic.getN())); } - - isotropic_interaction_type isotropic; - vector3_type T; - vector3_type B; - scalar_type TdotV; - scalar_type BdotV; -}; - - -template -struct CustomIsoMicrofacetConfiguration; - -template -NBL_BOOL_CONCEPT CustomMicrofacetConfigIso = bxdf::LightSample && bxdf::surface_interactions::Isotropic && !bxdf::surface_interactions::Anisotropic && bxdf::CreatableIsotropicMicrofacetCache && !bxdf::AnisotropicMicrofacetCache && concepts::FloatingPointLikeVectorial; - -template -NBL_PARTIAL_REQ_TOP(CustomMicrofacetConfigIso) -struct CustomIsoMicrofacetConfiguration) > -#undef MICROFACET_CONF_ISO -{ - NBL_CONSTEXPR_STATIC_INLINE bool IsAnisotropic = false; - - using scalar_type = typename LS::scalar_type; - using ray_dir_info_type = typename LS::ray_dir_info_type; - using vector2_type = hlsl::vector; - using vector3_type = hlsl::vector; - using monochrome_type = hlsl::vector; - using matrix3x3_type = hlsl::matrix; - using isotropic_interaction_type = Interaction; - using anisotropic_interaction_type = reciprocity_test_impl::SAnisotropic; - using sample_type = LS; - using spectral_type = Spectrum; - using quotient_pdf_type = sampling::quotient_and_pdf; - using isocache_type = MicrofacetCache; - using anisocache_type = bxdf::SAnisotropicMicrofacetCache; -}; -} - -using rectest_iso_interaction = reciprocity_test_impl::SIsotropic; -using rectest_aniso_interaction = reciprocity_test_impl::SAnisotropic; -using rectest_iso_microfacet_config_t = reciprocity_test_impl::CustomIsoMicrofacetConfiguration; -using rectest_aniso_microfacet_config_t = bxdf::SMicrofacetConfiguration; - -#endif diff --git a/66_HLSLBxDFTests/main.cpp b/66_HLSLBxDFTests/main.cpp deleted file mode 100644 index 1ec68ebd4..000000000 --- a/66_HLSLBxDFTests/main.cpp +++ /dev/null @@ -1,502 +0,0 @@ -#include -#include -#include -#include -#include - -#include "nbl/examples/examples.hpp" -#include "nbl/this_example/builtin/build/spirv/keys.hpp" - -using namespace nbl; -using namespace core; -using namespace system; -using namespace asset; -using namespace video; -using namespace nbl::hlsl; -using namespace nbl::examples; - -#include "app_resources/test_components.hlsl" -#include "app_resources/tests.hlsl" -#include "tests.h" -#include "nbl/builtin/hlsl/math/angle_adding.hlsl" -#include "nbl/builtin/hlsl/bxdf/ndf.hlsl" -#include "nbl/builtin/hlsl/bxdf/fresnel.hlsl" - -#include "nlohmann/json.hpp" - -using json = nlohmann::json; - -#define FOR_EACH_BEGIN_EX(r, ex) std::for_each(ex, r.begin(), r.end(), [&](uint32_t i) { -#define FOR_EACH_BEGIN(r) std::for_each(std::execution::par_unseq, r.begin(), r.end(), [&](uint32_t i) { -#define FOR_EACH_END }); - -#define RUN_TEST_OF_TYPE(TEST_TYPE, INIT_PARAMS) {\ - PrintFailureCallback cb;\ - cb.logger = m_logger;\ - pass &= BOOST_PP_REMOVE_PARENS(TEST_TYPE)::run(INIT_PARAMS, cb);\ -}\ - -#define RUN_CHI2_TEST_WRITE_EXR(TEST_TYPE, INIT_PARAMS) {\ - PrintFailureCallback cb;\ - cb.logger = m_logger;\ - BOOST_PP_REMOVE_PARENS(TEST_TYPE) t;\ - t.init(initparams.halfSeed);\ - t.rc.halfSeed = initparams.halfSeed;\ - t.numSamples = initparams.samples;\ - t.thetaSplits = initparams.thetaSplits;\ - t.phiSplits = initparams.phiSplits;\ - t.write_frequencies = static_cast(initparams.writeFrequencies);\ - t.initBxDF(t.rc);\ - TestResult e = t.test();\ - if (e != BTR_INVALID_TEST_CONFIG)\ - {\ - if (e != BTR_NONE)\ - {\ - if (initparams.writeFrequencies >= BOOST_PP_REMOVE_PARENS(TEST_TYPE)::WFE_WRITE_ERRORS)\ - writeToEXR(t);\ - cb.__call(e, t, initparams.logInfo);\ - }\ - else if (initparams.writeFrequencies == BOOST_PP_REMOVE_PARENS(TEST_TYPE)::WFE_WRITE_ALL)\ - writeToEXR(t);\ - }\ - pass &= e >= BTR_NOBREAK;\ -}\ - -class HLSLBxDFTests final : public application_templates::MonoDeviceApplication, public BuiltinResourcesApplication -{ - using device_base_t = application_templates::MonoDeviceApplication; - using asset_base_t = BuiltinResourcesApplication; - -public: - HLSLBxDFTests(const path& _localInputCWD, const path& _localOutputCWD, const path& _sharedInputCWD, const path& _sharedOutputCWD) : - system::IApplicationFramework(_localInputCWD, _localOutputCWD, _sharedInputCWD, _sharedOutputCWD) {} - - inline bool onAppInitialized(smart_refctd_ptr&& system) override - { - if (!device_base_t::onAppInitialized(smart_refctd_ptr(system))) - return false; - - if (!asset_base_t::onAppInitialized(std::move(system))) - return false; - - std::ifstream f("../app_resources/config.json"); - if (f.fail()) - { - m_logger->log("could not open config file", ILogger::ELL_ERROR); - return false; - } - try - { - testconfigs = json::parse(f); - } - catch (json::parse_error& ex) - { - m_logger->log("parse_error.%d failed to parse config file at byte %u: %s", ILogger::ELL_ERROR, ex.id, ex.byte, ex.what()); - return false; - } - - // test compile with dxc - { - IAssetLoader::SAssetLoadParams lp = {}; - lp.logger = m_logger.get(); - lp.workingDirectory = "app_resources"; // virtual root - auto key = nbl::this_example::builtin::build::get_spirv_key<"shader">(m_device.get()); - auto bundle = m_assetMgr->getAsset(key.c_str(), lp); - - const auto assets = bundle.getContents(); - if (assets.empty()) - m_logger->log("Could not load shader!", ILogger::ELL_ERROR); - - // Cast down the asset to its proper type - auto shader = IAsset::castDown(assets[0]); - - if (!shader) - m_logger->log("compile shader test failed!", ILogger::ELL_ERROR); - } - - // test concepts, not comprehensive - static_assert(bxdf::surface_interactions::Isotropic); - static_assert(bxdf::surface_interactions::Isotropic); - static_assert(bxdf::surface_interactions::Anisotropic); - - static_assert(bxdf::CreatableIsotropicMicrofacetCache); - static_assert(bxdf::ReadableIsotropicMicrofacetCache); - static_assert(bxdf::AnisotropicMicrofacetCache); - - using ndf_beckmann_t = bxdf::ndf::Beckmann; - static_assert(bxdf::ndf::NDF); - using ndf_ggx_t = bxdf::ndf::GGX; - static_assert(bxdf::ndf::NDF); - - using fresnel_schlick_t = bxdf::fresnel::Schlick; - static_assert(bxdf::fresnel::Fresnel); - - static_assert(bxdf::bxdf_concepts::IsotropicBxDF>); - static_assert(bxdf::bxdf_concepts::IsotropicBxDF>); - static_assert(bxdf::bxdf_concepts::IsotropicBxDF>); - - static_assert(bxdf::bxdf_concepts::MicrofacetBxDF>); - static_assert(bxdf::bxdf_concepts::MicrofacetBxDF>); - static_assert(bxdf::bxdf_concepts::MicrofacetBxDF>); - static_assert(bxdf::bxdf_concepts::MicrofacetBxDF>); - - return runTests(); - } - - void workLoopBody() override {} - - bool keepRunning() override { return false; } - - bool onAppTerminated() override - { - return device_base_t::onAppTerminated(); - } - -private: - template - struct PrintFailureCallback : FailureCallback - { - void __call(TestResult error, NBL_REF_ARG(TestT) failedFor, bool logInfo) override - { - switch (error) - { - case BTR_INVALID_TEST_CONFIG: - if (logInfo) - logger->log("seed %u: %s skipping test due to invalid NdotV/NdotL config", ILogger::ELL_INFO, failedFor.rc.halfSeed, failedFor.name.c_str()); - break; - case BTR_ERROR_NEGATIVE_VAL: - logger->log("seed %u: %s pdf/quotient/eval < 0", ILogger::ELL_ERROR, failedFor.rc.halfSeed, failedFor.name.c_str()); - break; - case BTR_ERROR_GENERATED_SAMPLE_NAN_PDF: - logger->log("seed %u: %s generated sample has pdf = NaN", ILogger::ELL_ERROR, failedFor.rc.halfSeed, failedFor.name.c_str()); - break; - case BTR_ERROR_QUOTIENT_INF: - logger->log("seed %u: %s quotient -> inf", ILogger::ELL_ERROR, failedFor.rc.halfSeed, failedFor.name.c_str()); - break; - case BTR_ERROR_JACOBIAN_TEST_FAIL: - logger->log("seed %u: %s failed the jacobian * pdf test %s", ILogger::ELL_ERROR, failedFor.rc.halfSeed, failedFor.name.c_str(), failedFor.errMsg.c_str()); - break; - case BTR_ERROR_PDF_EVAL_DIFF: - logger->log("seed %u: %s quotient * pdf != eval %s", ILogger::ELL_ERROR, failedFor.rc.halfSeed, failedFor.name.c_str(), failedFor.errMsg.c_str()); - break; - case BTR_ERROR_NO_RECIPROCITY: - logger->log("seed %u: %s failed the reciprocity test %s", ILogger::ELL_ERROR, failedFor.rc.halfSeed, failedFor.name.c_str(), failedFor.errMsg.c_str()); - break; - case BTR_ERROR_REFLECTANCE_OUT_OF_RANGE: - logger->log("seed %u: %s reflectance not between 0 and 1 %s", ILogger::ELL_ERROR, failedFor.rc.halfSeed, failedFor.name.c_str(), failedFor.errMsg.c_str()); - break; - case BTR_PRINT_MSG: - logger->log("seed %u: %s error message %s", ILogger::ELL_ERROR, failedFor.rc.halfSeed, failedFor.name.c_str(), failedFor.errMsg.c_str()); - break; - case BTR_ERROR_QUOTIENT_SUM_TOO_LARGE: - logger->log("seed %u: %s accumulated quotient > 1.0 %s", ILogger::ELL_ERROR, failedFor.rc.halfSeed, failedFor.name.c_str(), failedFor.errMsg.c_str()); - break; - case BTR_WARNING_GENERATED_H_INVALID: - logger->log("seed %u: %s failed invalid H configuration generated %s", ILogger::ELL_WARNING, failedFor.rc.halfSeed, failedFor.name.c_str(), failedFor.errMsg.c_str()); - break; - default: - logger->log("seed %u: %s unknown error", ILogger::ELL_ERROR, failedFor.rc.halfSeed, failedFor.name.c_str()); - } - -#ifdef _NBL_DEBUG - for (volatile bool repeat = true; IsDebuggerPresent() && repeat && error < BTR_NOBREAK; ) - { - repeat = false; - _NBL_DEBUG_BREAK_IF(true); - failedFor.compute(); - } -#endif - } - - smart_refctd_ptr logger; - }; - - bool runTests() - { - const bool logInfo = testconfigs["logInfo"]; - bool pass = true; - - // test jacobian * pdf - uint32_t runs = testconfigs["TestJacobian"]["runs"]; - auto rJacobian = std::ranges::views::iota(0u, runs); - FOR_EACH_BEGIN(rJacobian) - STestInitParams initparams{ .logInfo = logInfo }; - initparams.halfSeed = i; - initparams.verbose = testconfigs["TestJacobian"]["verbose"]; - - RUN_TEST_OF_TYPE((TestJacobian>), initparams); - RUN_TEST_OF_TYPE((TestJacobian>), initparams); - RUN_TEST_OF_TYPE((TestJacobian>), initparams); - RUN_TEST_OF_TYPE((TestJacobian, false>), initparams); - RUN_TEST_OF_TYPE((TestJacobian, true>), initparams); - RUN_TEST_OF_TYPE((TestJacobian, false>), initparams); - RUN_TEST_OF_TYPE((TestJacobian, true>), initparams); - RUN_TEST_OF_TYPE((TestJacobian, false>), initparams); - - RUN_TEST_OF_TYPE((TestJacobian>), initparams); - RUN_TEST_OF_TYPE((TestJacobian>), initparams); - RUN_TEST_OF_TYPE((TestJacobian >), initparams); - RUN_TEST_OF_TYPE((TestJacobian >), initparams); - RUN_TEST_OF_TYPE((TestJacobian>), initparams); - RUN_TEST_OF_TYPE((TestJacobian, false>), initparams); - RUN_TEST_OF_TYPE((TestJacobian, true>), initparams); - RUN_TEST_OF_TYPE((TestJacobian, false>), initparams); - RUN_TEST_OF_TYPE((TestJacobian, true>), initparams); - RUN_TEST_OF_TYPE((TestJacobian, false>), initparams); - FOR_EACH_END - - - // test reciprocity - runs = testconfigs["TestReciprocity"]["runs"]; - auto rReciprocity = std::ranges::views::iota(0u, runs); - FOR_EACH_BEGIN(rReciprocity) - STestInitParams initparams{ .logInfo = logInfo }; - initparams.halfSeed = i; - initparams.verbose = testconfigs["TestReciprocity"]["verbose"]; - - RUN_TEST_OF_TYPE((TestReciprocity>), initparams); - RUN_TEST_OF_TYPE((TestReciprocity>), initparams); - RUN_TEST_OF_TYPE((TestReciprocity>), initparams); - RUN_TEST_OF_TYPE((TestReciprocity, false>), initparams); - RUN_TEST_OF_TYPE((TestReciprocity, true>), initparams); - RUN_TEST_OF_TYPE((TestReciprocity, false>), initparams); - RUN_TEST_OF_TYPE((TestReciprocity, true>), initparams); - RUN_TEST_OF_TYPE((TestReciprocity, false>), initparams); - - RUN_TEST_OF_TYPE((TestReciprocity>), initparams); - RUN_TEST_OF_TYPE((TestReciprocity>), initparams); - RUN_TEST_OF_TYPE((TestReciprocity >), initparams); - RUN_TEST_OF_TYPE((TestReciprocity >), initparams); - RUN_TEST_OF_TYPE((TestReciprocity>), initparams); - RUN_TEST_OF_TYPE((TestReciprocity, false>), initparams); - RUN_TEST_OF_TYPE((TestReciprocity, true>), initparams); - RUN_TEST_OF_TYPE((TestReciprocity, false>), initparams); - RUN_TEST_OF_TYPE((TestReciprocity, true>), initparams); - RUN_TEST_OF_TYPE((TestReciprocity, false>), initparams); - FOR_EACH_END - - - // test modified white furnace, where accumulated quotient <= 1.f for non-energy conserving bxdfs - runs = testconfigs["TestModifiedWhiteFurnace"]["runs"]; - auto rBucket = std::ranges::views::iota(0u, runs); - FOR_EACH_BEGIN(rBucket) - STestInitParams initparams{ .logInfo = logInfo }; - initparams.halfSeed = i; - initparams.samples = testconfigs["TestModifiedWhiteFurnace"]["samples"]; - - RUN_TEST_OF_TYPE((TestModifiedWhiteFurnace>), initparams); - RUN_TEST_OF_TYPE((TestModifiedWhiteFurnace>), initparams); - RUN_TEST_OF_TYPE((TestModifiedWhiteFurnace>), initparams); - RUN_TEST_OF_TYPE((TestModifiedWhiteFurnace, false>), initparams); - RUN_TEST_OF_TYPE((TestModifiedWhiteFurnace, true>), initparams); - RUN_TEST_OF_TYPE((TestModifiedWhiteFurnace, false>), initparams); - RUN_TEST_OF_TYPE((TestModifiedWhiteFurnace, true>), initparams); - RUN_TEST_OF_TYPE((TestModifiedWhiteFurnace, false>), initparams); - - RUN_TEST_OF_TYPE((TestModifiedWhiteFurnace>), initparams); - RUN_TEST_OF_TYPE((TestModifiedWhiteFurnace>), initparams); - RUN_TEST_OF_TYPE((TestModifiedWhiteFurnace>), initparams); - RUN_TEST_OF_TYPE((TestModifiedWhiteFurnace>), initparams); - RUN_TEST_OF_TYPE((TestModifiedWhiteFurnace>), initparams); - RUN_TEST_OF_TYPE((TestModifiedWhiteFurnace, false>), initparams); - RUN_TEST_OF_TYPE((TestModifiedWhiteFurnace, true>), initparams); - RUN_TEST_OF_TYPE((TestModifiedWhiteFurnace, false>), initparams); - RUN_TEST_OF_TYPE((TestModifiedWhiteFurnace, true>), initparams); - RUN_TEST_OF_TYPE((TestModifiedWhiteFurnace, false>), initparams); - FOR_EACH_END - - - // chi2 test for sampling and pdf - runs = testconfigs["TestChi2"]["runs"]; - auto rChi2 = std::ranges::views::iota(0u, runs); - FOR_EACH_BEGIN_EX(rChi2, std::execution::par_unseq) - STestInitParams initparams{ .logInfo = logInfo }; - initparams.halfSeed = i; - initparams.samples = testconfigs["TestChi2"]["samples"]; - initparams.thetaSplits = testconfigs["TestChi2"]["thetaSplits"]; - initparams.phiSplits = testconfigs["TestChi2"]["phiSplits"]; - initparams.writeFrequencies = testconfigs["TestChi2"]["writeFrequencies"]; - - RUN_CHI2_TEST_WRITE_EXR((TestChi2>), initparams); - RUN_CHI2_TEST_WRITE_EXR((TestChi2>), initparams); - RUN_CHI2_TEST_WRITE_EXR((TestChi2, false>), initparams); - RUN_CHI2_TEST_WRITE_EXR((TestChi2, true>), initparams); - RUN_CHI2_TEST_WRITE_EXR((TestChi2, false>), initparams); - RUN_CHI2_TEST_WRITE_EXR((TestChi2, true>), initparams); - RUN_CHI2_TEST_WRITE_EXR((TestChi2, false>), initparams); - - RUN_CHI2_TEST_WRITE_EXR((TestChi2>), initparams); - RUN_CHI2_TEST_WRITE_EXR((TestChi2>), initparams); - RUN_CHI2_TEST_WRITE_EXR((TestChi2, false>), initparams); - RUN_CHI2_TEST_WRITE_EXR((TestChi2, true>), initparams); - RUN_CHI2_TEST_WRITE_EXR((TestChi2, false>), initparams); - RUN_CHI2_TEST_WRITE_EXR((TestChi2, true>), initparams); - RUN_CHI2_TEST_WRITE_EXR((TestChi2, false>), initparams); - FOR_EACH_END - - // testing ndf jacobian * dg1, ONLY for cook torrance bxdfs - runs = testconfigs["TestNDF"]["runs"]; - auto rNdf = std::ranges::views::iota(0u, runs); - FOR_EACH_BEGIN(rNdf) - STestInitParams initparams{ .logInfo = logInfo }; - initparams.halfSeed = i; - initparams.verbose = testconfigs["TestNDF"]["verbose"]; - - RUN_TEST_OF_TYPE((TestNDF, false>), initparams); - RUN_TEST_OF_TYPE((TestNDF, true>), initparams); - RUN_TEST_OF_TYPE((TestNDF, false>), initparams); - RUN_TEST_OF_TYPE((TestNDF, true>), initparams); - - RUN_TEST_OF_TYPE((TestNDF, false>), initparams); - RUN_TEST_OF_TYPE((TestNDF, true>), initparams); - RUN_TEST_OF_TYPE((TestNDF, false>), initparams); - RUN_TEST_OF_TYPE((TestNDF, true>), initparams); - FOR_EACH_END - - // test generated H that NdotV*VdotH>=0.0, VdotL calculation - runs = testconfigs["TestCTGenerateH"]["runs"]; - auto rGenerateH = std::ranges::views::iota(0u, runs); - FOR_EACH_BEGIN_EX(rGenerateH, std::execution::par_unseq) - STestInitParams initparams{ .logInfo = logInfo }; - initparams.halfSeed = i; - initparams.samples = testconfigs["TestCTGenerateH"]["samples"]; - initparams.immediateFail = testconfigs["TestCTGenerateH"]["immediateFail"]; - - RUN_TEST_OF_TYPE((TestCTGenerateH, false>), initparams); - RUN_TEST_OF_TYPE((TestCTGenerateH, true>), initparams); - RUN_TEST_OF_TYPE((TestCTGenerateH, false>), initparams); - RUN_TEST_OF_TYPE((TestCTGenerateH, true>), initparams); - - RUN_TEST_OF_TYPE((TestCTGenerateH, false>), initparams); - RUN_TEST_OF_TYPE((TestCTGenerateH, true>), initparams); - RUN_TEST_OF_TYPE((TestCTGenerateH, false>), initparams); - RUN_TEST_OF_TYPE((TestCTGenerateH, true>), initparams); - FOR_EACH_END - - // test arccos angle sums - { - Xoroshiro64Star rng = Xoroshiro64Star::construct(uint32_t2(4, 2)); - math::sincos_accumulator angle_adder; - - auto Sin = [&](const float cosA) -> float - { - return nbl::hlsl::sqrt(1.f - cosA * cosA); - }; - - for (uint32_t i = 0; i < 10; i++) - { - const float a = ConvertToFloat01::__call(rng()) * 2.f - 1.f; - const float b = ConvertToFloat01::__call(rng()) * 2.f - 1.f; - const float c = ConvertToFloat01::__call(rng()) * 2.f - 1.f; - const float d = ConvertToFloat01::__call(rng()) * 2.f - 1.f; - - const float exAB = acos(a) + acos(b); - angle_adder = math::sincos_accumulator::create(a, Sin(a)); - angle_adder.addAngle(b, Sin(b)); - float res = angle_adder.getSumofArccos(); - bool twoAnglesAcos = testing::relativeApproxCompare(res, exAB, 1e-3); - pass &= twoAnglesAcos; - if (!twoAnglesAcos) - m_logger->log("angle adding (2 angles) failed! expected %f, got %f", ILogger::ELL_ERROR, exAB, res); - - const float exABCD = exAB + acos(c) + acos(d); - angle_adder = math::sincos_accumulator::create(a, Sin(a)); - angle_adder.addAngle(b, Sin(b)); - angle_adder.addAngle(c, Sin(c)); - angle_adder.addAngle(d, Sin(d)); - res = angle_adder.getSumofArccos(); - bool fourAnglesAcos = testing::relativeApproxCompare(res, exABCD, 1e-3); - pass &= fourAnglesAcos; - if (!fourAnglesAcos) - m_logger->log("angle adding (4 angles) failed! expected %f, got %f", ILogger::ELL_ERROR, exABCD, res); - } - } - - return pass; - } - - template - static smart_refctd_ptr writeToCPUImage(const Chi2Test& test) - { - const uint32_t totalWidth = test.phiSplits; - const uint32_t totalHeight = 2 * test.thetaSplits; - const auto format = E_FORMAT::EF_R32G32B32A32_SFLOAT; - - IImage::SCreationParams imageParams = {}; - imageParams.type = IImage::E_TYPE::ET_2D; - imageParams.format = format; - imageParams.extent = { totalWidth, totalHeight, 1 }; - imageParams.mipLevels = 1; - imageParams.arrayLayers = 1; - imageParams.samples = ICPUImage::ESCF_1_BIT; - imageParams.usage = IImage::EUF_SAMPLED_BIT; - - smart_refctd_ptr image = ICPUImage::create(std::move(imageParams)); - assert(image); - - const size_t bufferSize = totalWidth * totalHeight * getTexelOrBlockBytesize(format); - { - auto imageRegions = make_refctd_dynamic_array>(1ull); - auto& region = imageRegions->front(); - region.bufferImageHeight = 0u; - region.bufferOffset = 0ull; - region.bufferRowLength = totalWidth; - region.imageExtent = { totalWidth, totalHeight, 1 }; - region.imageOffset = { 0u, 0u, 0u }; - region.imageSubresource.aspectMask = IImage::EAF_COLOR_BIT; - region.imageSubresource.baseArrayLayer = 0u; - region.imageSubresource.layerCount = 1; - region.imageSubresource.mipLevel = 0; - - image->setBufferAndRegions(ICPUBuffer::create({ bufferSize }), std::move(imageRegions)); - } - - uint8_t* bytePtr = reinterpret_cast(image->getBuffer()->getPointer()); - - // write sample count from generate, top half - for (uint64_t j = 0u; j < test.thetaSplits; ++j) - for (uint64_t i = 0u; i < test.phiSplits; ++i) - { - float32_t3 pixelColor = hlsl::visualization::Turbo::__call(test.countFreq[j * test.phiSplits + i] / test.maxCountFreq); - double decodedPixel[4] = { pixelColor[0], pixelColor[1], pixelColor[2], 1 }; - - const uint64_t pixelIndex = j * test.phiSplits + i; - asset::encodePixelsRuntime(format, bytePtr + pixelIndex * asset::getTexelOrBlockBytesize(format), decodedPixel); - } - - // write values of pdf, bottom half - for (uint64_t j = 0u; j < test.thetaSplits; ++j) - for (uint64_t i = 0u; i < test.phiSplits; ++i) - { - float32_t3 pixelColor = hlsl::visualization::Turbo::__call(test.integrateFreq[j * test.phiSplits + i] / test.maxIntFreq); - double decodedPixel[4] = { pixelColor[0], pixelColor[1], pixelColor[2], 1 }; - - const uint64_t pixelIndex = (test.thetaSplits + j) * test.phiSplits + i; - asset::encodePixelsRuntime(format, bytePtr + pixelIndex * asset::getTexelOrBlockBytesize(format), decodedPixel); - } - - return image; - } - - template - void writeToEXR(const Chi2Test& test) - { - std::string filename = std::format("chi2test_{}_{}.exr", test.rc.halfSeed, test.name); - - auto cpuImage = writeToCPUImage(test); - ICPUImageView::SCreationParams imgViewParams; - imgViewParams.flags = static_cast(0u); - imgViewParams.format = cpuImage->getCreationParameters().format; - imgViewParams.image = smart_refctd_ptr(cpuImage); - imgViewParams.viewType = ICPUImageView::ET_2D; - imgViewParams.subresourceRange = { static_cast(0u),0u,1u,0u,1u }; - smart_refctd_ptr imageView = ICPUImageView::create(std::move(imgViewParams)); - - IAssetWriter::SAssetWriteParams wp(imageView.get()); - m_assetMgr->writeAsset(filename, wp); - } - - json testconfigs; -}; - -NBL_MAIN_FUNC(HLSLBxDFTests) diff --git a/66_HLSLBxDFTests/tests.h b/66_HLSLBxDFTests/tests.h deleted file mode 100644 index cccc9dd51..000000000 --- a/66_HLSLBxDFTests/tests.h +++ /dev/null @@ -1,478 +0,0 @@ -#ifndef BXDFTESTS_TESTS_H -#define BXDFTESTS_TESTS_H -// cpp only tests - -#define GLM_ENABLE_EXPERIMENTAL -#include -#include -#include -#include -#include -#include - -#include "app_resources/tests_common.hlsl" -#include "nbl/builtin/hlsl/visualization/turbo.hlsl" -#include "nbl/builtin/hlsl/math/quadrature/adaptive_simpson.hlsl" - -template -struct TestModifiedWhiteFurnace : TestBxDF -{ - using base_t = TestBxDFBase; - using this_t = TestModifiedWhiteFurnace; - using traits_t = bxdf::traits; - - TestResult compute() - { - aniso_cache cache; - iso_cache isocache; - - sample_t s; - quotient_pdf_t sampledLi; - - uint32_t deltaSampleCount = 0u; - uint32_t continuousSampleCount = 0u; - float32_t3 deltaQuotientSum = float32_t3(0.f, 0.f, 0.f); - float32_t3 continuousQuotientSum = float32_t3(0.f, 0.f, 0.f); - for (uint32_t i = 0; i < numSamples; i++) - { - float32_t3 u = ConvertToFloat01::__call(base_t::rc.rng_vec<3>()); - - NBL_IF_CONSTEXPR(traits_t::type == bxdf::BT_BRDF && !traits_t::IsMicrofacet) - { - s = base_t::bxdf.generate(base_t::anisointer, u.xy); - } - NBL_IF_CONSTEXPR(traits_t::type == bxdf::BT_BRDF && traits_t::IsMicrofacet) - { - NBL_IF_CONSTEXPR(aniso) - { - s = base_t::bxdf.generate(base_t::anisointer, u.xy, cache); - } - else - { - s = base_t::bxdf.generate(base_t::isointer, u.xy, isocache); - } - } - NBL_IF_CONSTEXPR(traits_t::type == bxdf::BT_BSDF && !traits_t::IsMicrofacet) - { - s = base_t::bxdf.generate(base_t::anisointer, u); - } - NBL_IF_CONSTEXPR(traits_t::type == bxdf::BT_BSDF && traits_t::IsMicrofacet) - { - NBL_IF_CONSTEXPR(aniso) - { - s = base_t::bxdf.generate(base_t::anisointer, u, cache); - } - else - { - s = base_t::bxdf.generate(base_t::isointer, u, isocache); - } - } - - if (!s.isValid()) - continue; - - NBL_IF_CONSTEXPR(!traits_t::IsMicrofacet) - { - sampledLi = base_t::bxdf.quotient_and_pdf(s, base_t::isointer); - } - NBL_IF_CONSTEXPR(traits_t::IsMicrofacet) - { - NBL_IF_CONSTEXPR(aniso) - { - sampledLi = base_t::bxdf.quotient_and_pdf(s, base_t::anisointer, cache); - } - else - { - sampledLi = base_t::bxdf.quotient_and_pdf(s, base_t::isointer, isocache); - } - } - - if (hlsl::isinf(sampledLi.pdf)) // is from dirac delta distribution - { - // might have to be by weight of dirac delta sample - deltaQuotientSum += sampledLi.quotient; - deltaSampleCount++; - } - else - { - continuousQuotientSum += sampledLi.quotient; - continuousSampleCount++; - } - } - - accumulatedQuotient = deltaQuotientSum / float(deltaSampleCount) + continuousQuotientSum / float(continuousSampleCount); - - return BTR_NONE; - } - - TestResult test() - { - if (traits_t::type == bxdf::BT_BRDF) - { - if (base_t::isointer.getNdotV() <= bit_cast(numeric_limits::min)) - return BTR_INVALID_TEST_CONFIG; - } - else if (traits_t::type == bxdf::BT_BSDF) - { - if (hlsl::abs(base_t::isointer.getNdotV()) <= bit_cast(numeric_limits::min)) - return BTR_INVALID_TEST_CONFIG; - } - - TestResult res = compute(); - if (res != BTR_NONE) - return res; - - if (nbl::hlsl::any >(accumulatedQuotient > hlsl::promote(1.f))) - { - base_t::errMsg += std::format("({}, {}, {})", accumulatedQuotient.x, accumulatedQuotient.y, accumulatedQuotient.z); - return BTR_ERROR_QUOTIENT_SUM_TOO_LARGE; - } - - return BTR_NONE; - } - - static bool run(NBL_CONST_REF_ARG(STestInitParams) initparams, NBL_REF_ARG(FailureCallback) cb) - { - this_t t; - t.init(initparams.halfSeed); - t.rc.halfSeed = initparams.halfSeed; - t.numSamples = initparams.samples; - t.initBxDF(t.rc); - - TestResult e = t.test(); - if (e != BTR_NONE) - cb.__call(e, t, initparams.logInfo); - return e >= BTR_NOBREAK; - } - - uint32_t numSamples; - float32_t3 accumulatedQuotient; -}; - -template -struct CalculatePdfSinTheta -{ - using traits_t = bxdf::traits; - - float operator()(float theta, float phi) NBL_CONST_MEMBER_FUNC - { - float cosTheta = std::cos(theta), sinTheta = std::sin(theta); - float cosPhi = std::cos(phi), sinPhi = std::sin(phi); - - ray_dir_info_t L; - L.direction = hlsl::normalize(float32_t3(sinTheta * cosPhi, sinTheta * sinPhi, cosTheta)); - float32_t3 N = anisointer.getN(); - float NdotL = hlsl::dot(N, L.direction); - - const float32_t3 T = anisointer.getT(); - const float32_t3 B = anisointer.getB(); - sample_t s = sample_t::create(L, T, B, NdotL); - aniso_cache cache; - - float tmpeta = 1.f; - NBL_IF_CONSTEXPR(traits_t::IsMicrofacet) - { - const float NdotV = anisointer.getNdotV(); - NBL_IF_CONSTEXPR(traits_t::type == bxdf::BT_BRDF) - if (NdotV < 0.f) return 0.f; - - const float NdotL = s.getNdotL(); - if (NdotV * NdotL < 0.f) - tmpeta = NdotV < 0.f ? 1.f / eta : eta; - float32_t3 H = hlsl::normalize(V.getDirection() + L.getDirection() * tmpeta); - float VdotH = hlsl::dot(V.getDirection(), H); - if (NdotV * VdotH < 0.f) - { - H = -H; - VdotH = -VdotH; - } - - cache.iso_cache.VdotH = VdotH; - cache.iso_cache.LdotH = hlsl::dot(L.getDirection(), H); - cache.iso_cache.VdotL = hlsl::dot(V.getDirection(), L.getDirection()); - cache.iso_cache.absNdotH = hlsl::abs(hlsl::dot(N, H)); - cache.iso_cache.NdotH2 = cache.iso_cache.absNdotH * cache.iso_cache.absNdotH; - - if (!cache.isValid(bxdf::fresnel::OrientedEtas >::create(1.f, hlsl::promote >(tmpeta)))) - return 0.f; - - cache.fillTangents(T, B, H); - } - - float pdf; - NBL_IF_CONSTEXPR(!traits_t::IsMicrofacet) - { - pdf = bxdf.pdf(s, isointer); - } - NBL_IF_CONSTEXPR(traits_t::IsMicrofacet) - { - NBL_IF_CONSTEXPR(aniso) - { - pdf = bxdf.pdf(s, anisointer, cache); - } - else - { - pdf = bxdf.pdf(s, isointer, cache.iso_cache); - } - } - - return pdf * sinTheta; - } - - BxDF bxdf; - ray_dir_info_t V; - iso_interaction isointer; - aniso_interaction anisointer; - float eta; -}; - -// adapted from pbrt chi2 test: https://github.com/mmp/pbrt-v4/blob/792aaaa08d97dbedf11a3bb23e246b6443d847b4/src/pbrt/bsdfs_test.cpp#L280 -template -struct TestChi2 : TestBxDF -{ - using base_t = TestBxDFBase; - using this_t = TestChi2; - using traits_t = bxdf::traits; - - void clearBuckets() - { - const uint32_t freqSize = thetaSplits * phiSplits; - countFreq.resize(freqSize); - std::fill(countFreq.begin(), countFreq.end(), 0); - integrateFreq.resize(freqSize); - std::fill(integrateFreq.begin(), integrateFreq.end(), 0); - maxCountFreq = 0.f; - maxIntFreq = 0.f; - } - - double chi2CDF(double x, int dof) - { - if (dof < 1 || x < 0) - { - return 0.0; - } - else if (dof == 2) - { - return 1.0 - hlsl::exp(-0.5 * x); - } - else - { - return hlsl::gamma(0.5 * dof, 0.5 * x); - } - } - - enum WriteFrequenciesToEXR : uint16_t - { - WFE_DONT_WRITE = 0, - WFE_WRITE_ERRORS = 1, - WFE_WRITE_ALL = 2 - }; - - TestResult compute() - { - clearBuckets(); - - float thetaFactor = thetaSplits * numbers::inv_pi; - float phiFactor = phiSplits * 0.5f * numbers::inv_pi; - - uint32_t numObservedSamples = 0u; - sample_t s; - iso_cache isocache; - aniso_cache cache; - for (uint32_t i = 0; i < numSamples; i++) - { - float32_t3 u = ConvertToFloat01::__call(base_t::rc.rng_vec<3>()); - - NBL_IF_CONSTEXPR(traits_t::type == bxdf::BT_BRDF && !traits_t::IsMicrofacet) - { - s = base_t::bxdf.generate(base_t::anisointer, u.xy); - } - NBL_IF_CONSTEXPR(traits_t::type == bxdf::BT_BRDF && traits_t::IsMicrofacet) - { - if NBL_CONSTEXPR_FUNC(aniso) - s = base_t::bxdf.generate(base_t::anisointer, u.xy, cache); - else - s = base_t::bxdf.generate(base_t::isointer, u.xy, isocache); - } - NBL_IF_CONSTEXPR(traits_t::type == bxdf::BT_BSDF && !traits_t::IsMicrofacet) - { - s = base_t::bxdf.generate(base_t::anisointer, u); - } - NBL_IF_CONSTEXPR(traits_t::type == bxdf::BT_BSDF && traits_t::IsMicrofacet) - { - if NBL_CONSTEXPR_FUNC(aniso) - s = base_t::bxdf.generate(base_t::anisointer, u, cache); - else - s = base_t::bxdf.generate(base_t::isointer, u, isocache); - } - - if (!s.isValid()) - continue; - - // put s into bucket - math::Polar polarCoords = math::Polar::createFromCartesian(s.getL().getDirection()); - polarCoords.theta *= thetaFactor; - polarCoords.phi *= phiFactor; - if (polarCoords.phi < 0) - polarCoords.phi += 2.f * numbers::pi * phiFactor; - - int thetaBin = clamp((int)std::floor(polarCoords.theta), 0, thetaSplits - 1); - int phiBin = clamp((int)std::floor(polarCoords.phi), 0, phiSplits - 1); - - uint32_t freqidx = thetaBin * phiSplits + phiBin; - countFreq[freqidx] += 1; - numObservedSamples++; - - if (write_frequencies && maxCountFreq < countFreq[freqidx]) - maxCountFreq = countFreq[freqidx]; - } - - thetaFactor = 1.f / thetaFactor; - phiFactor = 1.f / phiFactor; - - uint32_t intidx = 0; - for (int i = 0; i < thetaSplits; i++) - { - for (int j = 0; j < phiSplits; j++) - { - uint32_t lastidx = intidx; - CalculatePdfSinTheta pdfSinTheta; - pdfSinTheta.bxdf = base_t::bxdf; - pdfSinTheta.V = base_t::rc.V; - pdfSinTheta.isointer = base_t::isointer; - pdfSinTheta.anisointer = base_t::anisointer; - pdfSinTheta.eta = base_t::rc.eta.x; - integrateFreq[intidx++] = numObservedSamples * math::quadrature::AdaptiveSimpson2D, float>::__call( - pdfSinTheta, float32_t2(i * thetaFactor, j * phiFactor), float32_t2((i + 1) * thetaFactor, (j + 1) * phiFactor)); - - if (write_frequencies && maxIntFreq < integrateFreq[lastidx]) - maxIntFreq = integrateFreq[lastidx]; - } - } - - return BTR_NONE; - } - - TestResult test() - { - if (traits_t::type == bxdf::BT_BRDF) - if (base_t::isointer.getNdotV() <= numeric_limits::min) - return BTR_INVALID_TEST_CONFIG; - else if (traits_t::type == bxdf::BT_BSDF) - if (hlsl::abs(base_t::isointer.getNdotV()) <= numeric_limits::min) - return BTR_INVALID_TEST_CONFIG; - - TestResult res = compute(); - if (res != BTR_NONE) - return res; - - // chi2 - std::vector cells(thetaSplits * phiSplits); - for (uint32_t i = 0; i < cells.size(); i++) - { - cells[i].expFreq = integrateFreq[i]; - cells[i].index = i; - } - std::sort(cells.begin(), cells.end(), [](const Cell& a, const Cell& b) - { - return a.expFreq < b.expFreq; - }); - - float pooledFreqs = 0, pooledExpFreqs = 0, chsq = 0; - int pooledCells = 0, dof = 0; - - for (const Cell& c : cells) - { - if (integrateFreq[c.index] == 0) - { - if (countFreq[c.index] > numSamples * 1e-5) - { - base_t::errMsg = std::format("expected frequency of 0 for c but found {} samples", countFreq[c.index]); - return BTR_PRINT_MSG; - } - } - else if (integrateFreq[c.index] < minFreq) - { - pooledFreqs += countFreq[c.index]; - pooledExpFreqs += integrateFreq[c.index]; - pooledCells++; - } - else if (pooledExpFreqs > 0 && pooledExpFreqs < minFreq) - { - pooledFreqs += countFreq[c.index]; - pooledExpFreqs += integrateFreq[c.index]; - pooledCells++; - } - else - { - float diff = countFreq[c.index] - integrateFreq[c.index]; - chsq += (diff * diff) / integrateFreq[c.index]; - dof++; - } - } - - if (pooledExpFreqs > 0 || pooledFreqs > 0) - { - float diff = pooledFreqs - pooledExpFreqs; - chsq += (diff * diff) / pooledExpFreqs; - dof++; - } - dof -= 1; - - if (dof <= 0) - { - base_t::errMsg = std::format("degrees of freedom {} too low", dof); - return BTR_PRINT_MSG; - } - - float pval = 1.0f - static_cast(chi2CDF(chsq, dof)); - float alpha = 1.0f - std::pow(1.0f - threshold, 1.0f / numTests); - - if (pval < alpha || !std::isfinite(pval)) - { - base_t::errMsg = std::format("chi2 test: rejected the null hypothesis (p-value = {:.3f}, significance level = {:.3f}", pval, alpha); - return BTR_PRINT_MSG; - } - - return BTR_NONE; - } - - static bool run(NBL_CONST_REF_ARG(STestInitParams) initparams, NBL_REF_ARG(FailureCallback) cb) - { - this_t t; - t.init(initparams.halfSeed); - t.rc.halfSeed = initparams.halfSeed; - t.numSamples = initparams.samples; - t.thetaSplits = initparams.thetaSplits; - t.phiSplits = initparams.phiSplits; - t.write_frequencies = static_cast(initparams.writeFrequencies); - t.initBxDF(t.rc); - - TestResult e = t.test(); - if (e != BTR_NONE) - cb.__call(e, t, initparams.logInfo); - return e >= BTR_NOBREAK; - } - - struct Cell { - float expFreq; - uint32_t index; - }; - - uint32_t thetaSplits = 80; - uint32_t phiSplits = 160; - uint32_t numSamples = 1000000; - - uint32_t threshold = 1e-2; - uint32_t minFreq = 5; - uint32_t numTests = 5; - - WriteFrequenciesToEXR write_frequencies; - float maxCountFreq; - float maxIntFreq; - - std::vector countFreq; - std::vector integrateFreq; -}; - -#endif diff --git a/67_RayQueryGeometry/CMakeLists.txt b/67_RayQueryGeometry/CMakeLists.txt deleted file mode 100644 index 768379100..000000000 --- a/67_RayQueryGeometry/CMakeLists.txt +++ /dev/null @@ -1,66 +0,0 @@ -set(NBL_INCLUDE_SEARCH_DIRECTORIES - "${CMAKE_CURRENT_SOURCE_DIR}/include" -) - -include(common RESULT_VARIABLE RES) -if(NOT RES) - message(FATAL_ERROR "common.cmake not found. Should be in {repo_root}/cmake directory") -endif() - -nbl_create_executable_project("" "" "${NBL_INCLUDE_SEARCH_DIRECTORIES}" "" "${NBL_EXECUTABLE_PROJECT_CREATION_PCH_TARGET}") - -if(NBL_EMBED_BUILTIN_RESOURCES) - set(_BR_TARGET_ ${EXECUTABLE_NAME}_builtinResourceData) - set(RESOURCE_DIR "app_resources") - - get_filename_component(_SEARCH_DIRECTORIES_ "${CMAKE_CURRENT_SOURCE_DIR}" ABSOLUTE) - get_filename_component(_OUTPUT_DIRECTORY_SOURCE_ "${CMAKE_CURRENT_BINARY_DIR}/src" ABSOLUTE) - get_filename_component(_OUTPUT_DIRECTORY_HEADER_ "${CMAKE_CURRENT_BINARY_DIR}/include" ABSOLUTE) - - file(GLOB_RECURSE BUILTIN_RESOURCE_FILES RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}/${RESOURCE_DIR}" CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/${RESOURCE_DIR}/*") - foreach(RES_FILE ${BUILTIN_RESOURCE_FILES}) - LIST_BUILTIN_RESOURCE(RESOURCES_TO_EMBED "${RES_FILE}") - endforeach() - - ADD_CUSTOM_BUILTIN_RESOURCES(${_BR_TARGET_} RESOURCES_TO_EMBED "${_SEARCH_DIRECTORIES_}" "${RESOURCE_DIR}" "nbl::this_example::builtin" "${_OUTPUT_DIRECTORY_HEADER_}" "${_OUTPUT_DIRECTORY_SOURCE_}") - - LINK_BUILTIN_RESOURCES_TO_TARGET(${EXECUTABLE_NAME} ${_BR_TARGET_}) -endif() - -set(OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/auto-gen") - -set(SM 6_8) -set(JSON [=[ -[ - { - "INPUT": "app_resources/render.comp.hlsl", - "KEY": "render", - } -] -]=]) -string(CONFIGURE "${JSON}" JSON) - -set(COMPILE_OPTIONS - -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} -) diff --git a/67_RayQueryGeometry/README.md b/67_RayQueryGeometry/README.md deleted file mode 100644 index d8ace560b..000000000 --- a/67_RayQueryGeometry/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Geometry Creator - -Use WASD/arrows to move camera and mouse scroll to change geometry. - -![](https://github.com/Devsh-Graphics-Programming/Nabla-Site-Media/blob/master/examples/GeometryCreator/demo.gif?raw=true) \ No newline at end of file diff --git a/67_RayQueryGeometry/app_resources/common.hlsl b/67_RayQueryGeometry/app_resources/common.hlsl deleted file mode 100644 index ecac0f59d..000000000 --- a/67_RayQueryGeometry/app_resources/common.hlsl +++ /dev/null @@ -1,36 +0,0 @@ -#ifndef RQG_COMMON_HLSL -#define RQG_COMMON_HLSL - -#include "nbl/builtin/hlsl/cpp_compat.hlsl" - -NBL_CONSTEXPR_INLINE_NSPC_SCOPE_VAR uint32_t WorkgroupSize = 16; - -enum NormalType : uint32_t -{ - NT_R8G8B8A8_SNORM, - NT_R32G32B32_SFLOAT, -}; - -// we need bitfield support in NBL_HLSL_DECLARE_STRUCT it seems -struct SGeomInfo -{ - uint64_t vertexBufferAddress; - uint64_t indexBufferAddress; - uint64_t normalBufferAddress; - - uint32_t normalType : 1; - uint32_t indexType : 1; // 16 bit, 32 bit -}; - -struct SPushConstants -{ - uint64_t geometryInfoBuffer; - - float32_t3 camPos; - float32_t4x4 invMVP; - - float32_t2 scaleNDC; - float32_t2 offsetNDC; -}; - -#endif // RQG_COMMON_HLSL diff --git a/67_RayQueryGeometry/app_resources/render.comp.hlsl b/67_RayQueryGeometry/app_resources/render.comp.hlsl deleted file mode 100644 index 889e1f38b..000000000 --- a/67_RayQueryGeometry/app_resources/render.comp.hlsl +++ /dev/null @@ -1,136 +0,0 @@ -#include "common.hlsl" - -#include "nbl/builtin/hlsl/glsl_compat/core.hlsl" -#include "nbl/builtin/hlsl/spirv_intrinsics/raytracing.hlsl" -#include "nbl/builtin/hlsl/bda/__ptr.hlsl" - - -using namespace nbl::hlsl; - -[[vk::push_constant]] SPushConstants pc; - -[[vk::binding(0, 0)]] RaytracingAccelerationStructure topLevelAS; - -[[vk::binding(1, 0)]] RWTexture2D outImage; -[[vk::constant_id(0)]] const float shader_variant = 1.0; - -float3 unpackNormals3x10(uint32_t v) -{ - // host side changes float32_t3 to EF_A2B10G10R10_SNORM_PACK32 - // follows unpacking scheme from https://github.com/KhronosGroup/SPIRV-Cross/blob/main/reference/shaders-hlsl/frag/unorm-snorm-packing.frag - int signedValue = int(v); - int3 pn = int3(signedValue << 22, signedValue << 12, signedValue << 2) >> 22; - return clamp(float3(pn) / 511.0, -1.0, 1.0); -} - -float3 calculateNormals(int primID, SGeomInfo geom, float2 bary) -{ - const uint indexType = geom.indexType; - const uint normalType = geom.normalType; - - const uint64_t vertexBufferAddress = geom.vertexBufferAddress; - const uint64_t indexBufferAddress = geom.indexBufferAddress; - const uint64_t normalBufferAddress = geom.normalBufferAddress; - - uint32_t3 indices; - if (indexBufferAddress == 0) - { - indices[0] = primID * 3; - indices[1] = indices[0] + 1; - indices[2] = indices[0] + 2; - } - else { - switch (indexType) - { - case 0: // EIT_16BIT - indices = uint32_t3((nbl::hlsl::bda::__ptr::create(indexBufferAddress)+primID).deref().load()); - break; - case 1: // EIT_32BIT - indices = uint32_t3((nbl::hlsl::bda::__ptr::create(indexBufferAddress)+primID).deref().load()); - break; - } - } - - if (normalBufferAddress == 0) - { - float3 v0 = vk::RawBufferLoad(vertexBufferAddress + indices[0] * 12); - float3 v1 = vk::RawBufferLoad(vertexBufferAddress + indices[1] * 12); - float3 v2 = vk::RawBufferLoad(vertexBufferAddress + indices[2] * 12); - - return normalize(cross(v1 - v0, v2 - v0)); - } - - float3 n0, n1, n2; - switch (normalType) - { - case NT_R8G8B8A8_SNORM: - { - uint32_t v0 = vk::RawBufferLoad(normalBufferAddress + indices[0] * 4); - uint32_t v1 = vk::RawBufferLoad(normalBufferAddress + indices[1] * 4); - uint32_t v2 = vk::RawBufferLoad(normalBufferAddress + indices[2] * 4); - - n0 = normalize(nbl::hlsl::spirv::unpackSnorm4x8(v0).xyz); - n1 = normalize(nbl::hlsl::spirv::unpackSnorm4x8(v1).xyz); - n2 = normalize(nbl::hlsl::spirv::unpackSnorm4x8(v2).xyz); - } - break; - case NT_R32G32B32_SFLOAT: - { - n0 = normalize(vk::RawBufferLoad(normalBufferAddress + indices[0] * 12)); - n1 = normalize(vk::RawBufferLoad(normalBufferAddress + indices[1] * 12)); - n2 = normalize(vk::RawBufferLoad(normalBufferAddress + indices[2] * 12)); - } - break; - } - - float3 barycentrics = float3(0.0, bary); - barycentrics.x = 1.0 - barycentrics.y - barycentrics.z; - - return barycentrics.x * n0 + barycentrics.y * n1 + barycentrics.z * n2; -} - -[numthreads(WorkgroupSize, WorkgroupSize, 1)] -[shader("compute")] -void main(uint32_t3 threadID : SV_DispatchThreadID) -{ - uint2 coords = threadID.xy; - coords.y = nbl::hlsl::glsl::gl_NumWorkGroups().y * WorkgroupSize - coords.y; // need to invert it - - float4 NDC; - NDC.xy = float2(coords) * pc.scaleNDC; - NDC.xy += pc.offsetNDC; - NDC.zw = float2(0, 1); - float3 targetPos; - { - float4 tmp = mul(pc.invMVP, NDC); - targetPos = tmp.xyz / tmp.w; - } - - float3 direction = normalize(targetPos - pc.camPos); - - spirv::RayQueryKHR query; - spirv::rayQueryInitializeKHR(query, topLevelAS, spv::RayFlagsOpaqueKHRMask, 0xFF, pc.camPos, 0.01, direction, 1000.0); - - while (spirv::rayQueryProceedKHR(query)) {} - - float4 color = float4(0, 0, 0, 1); - - if (spirv::rayQueryGetIntersectionTypeKHR(query, true) == spv::RayQueryCommittedIntersectionTypeRayQueryCommittedIntersectionTriangleKHR) - { - const int instanceCustomIndex = spirv::rayQueryGetIntersectionInstanceCustomIndexKHR(query, true); - const int geometryIndex = spirv::rayQueryGetIntersectionGeometryIndexKHR(query, true); - const int primID = spirv::rayQueryGetIntersectionPrimitiveIndexKHR(query, true); - - // TODO: candidate for `bda::__ptr` - const SGeomInfo geom = vk::RawBufferLoad(pc.geometryInfoBuffer + (instanceCustomIndex + geometryIndex) * sizeof(SGeomInfo), 8); - - float3 normals; - float2 barycentrics = spirv::rayQueryGetIntersectionBarycentricsKHR(query, true); - normals = calculateNormals(primID, geom, barycentrics); - - normals = normalize(normals) * 0.5 + 0.5; - color = float4(normals, 1.0); - } - - outImage[threadID.xy] = color; -} diff --git a/67_RayQueryGeometry/include/common.hpp b/67_RayQueryGeometry/include/common.hpp deleted file mode 100644 index ac774b0df..000000000 --- a/67_RayQueryGeometry/include/common.hpp +++ /dev/null @@ -1,34 +0,0 @@ -#ifndef _NBL_THIS_EXAMPLE_COMMON_H_INCLUDED_ -#define _NBL_THIS_EXAMPLE_COMMON_H_INCLUDED_ - -#include "nbl/examples/examples.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::application_templates; -using namespace nbl::examples; - -#include "app_resources/common.hlsl" - -namespace nbl::scene -{ - -using PolygonGeometryData = core::smart_refctd_ptr; -using GeometryCollectionData = core::smart_refctd_ptr; -using GeometryData = std::variant; -struct ReferenceObjectCpu -{ - hlsl::float32_t3x4 transform; - GeometryData data; - uint32_t instanceID; -}; - -} - - -#endif // _NBL_THIS_EXAMPLE_COMMON_H_INCLUDED_ \ No newline at end of file diff --git a/67_RayQueryGeometry/main.cpp b/67_RayQueryGeometry/main.cpp deleted file mode 100644 index 63346ac4c..000000000 --- a/67_RayQueryGeometry/main.cpp +++ /dev/null @@ -1,998 +0,0 @@ -// Copyright (C) 2018-2024 - 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 "common.hpp" -#include -#include "nbl/this_example/builtin/build/spirv/keys.hpp" - -class RayQueryGeometryApp final : public SimpleWindowedApplication, public BuiltinResourcesApplication -{ - using device_base_t = SimpleWindowedApplication; - using asset_base_t = BuiltinResourcesApplication; - using clock_t = std::chrono::steady_clock; - - constexpr static inline uint32_t WIN_W = 1280, WIN_H = 720; - constexpr static inline uint32_t MaxFramesInFlight = 3u; - - constexpr static inline clock_t::duration DisplayImageDuration = std::chrono::milliseconds(900); - - public: - inline RayQueryGeometryApp(const path& _localInputCWD, const path& _localOutputCWD, const path& _sharedInputCWD, const path& _sharedOutputCWD) - : IApplicationFramework(_localInputCWD, _localOutputCWD, _sharedInputCWD, _sharedOutputCWD) {} - - inline SPhysicalDeviceFeatures getRequiredDeviceFeatures() const override - { - auto retval = device_base_t::getRequiredDeviceFeatures(); - retval.accelerationStructure = true; - retval.rayQuery = true; - return retval; - } - - inline SPhysicalDeviceFeatures getPreferredDeviceFeatures() const override - { - auto retval = device_base_t::getPreferredDeviceFeatures(); - retval.accelerationStructureHostCommands = true; - return retval; - } - - inline core::vector getSurfaces() const override - { - if (!m_surface) - { - { - auto windowCallback = core::make_smart_refctd_ptr(smart_refctd_ptr(m_inputSystem), smart_refctd_ptr(m_logger)); - IWindow::SCreationParams params = {}; - params.callback = core::make_smart_refctd_ptr(); - params.width = WIN_W; - params.height = WIN_H; - params.x = 32; - params.y = 32; - params.flags = ui::IWindow::ECF_HIDDEN | IWindow::ECF_BORDERLESS | IWindow::ECF_RESIZABLE; - params.windowCaption = "RayQueryGeometryApp"; - params.callback = windowCallback; - 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) = CSimpleResizeSurface::create(std::move(surface)); - } - - if (m_surface) - return { {m_surface->getSurface()/*,EQF_NONE*/} }; - - return {}; - } - - // so that we can use the same queue for asset converter and rendering - inline core::vector getQueueRequirements() const override - { - auto reqs = device_base_t::getQueueRequirements(); - reqs.front().requiredFlags |= IQueue::FAMILY_FLAGS::TRANSFER_BIT; - return reqs; - } - - inline bool onAppInitialized(smart_refctd_ptr&& system) override - { - m_inputSystem = make_smart_refctd_ptr(logger_opt_smart_ptr(smart_refctd_ptr(m_logger))); - - if (!device_base_t::onAppInitialized(smart_refctd_ptr(system))) - return false; - - if (!asset_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!"); - - ISwapchain::SCreationParams swapchainParams = { .surface = m_surface->getSurface() }; - if (!swapchainParams.deduceFormat(m_physicalDevice)) - return logFail("Could not choose a Surface Format for the Swapchain!"); - - auto gQueue = getGraphicsQueue(); - if (!m_surface || !m_surface->init(gQueue, std::make_unique(), swapchainParams.sharedParams)) - return logFail("Could not create Window & Surface or initialize the Surface!"); - - auto pool = m_device->createCommandPool(gQueue->getFamilyIndex(), IGPUCommandPool::CREATE_FLAGS::RESET_COMMAND_BUFFER_BIT); - - for (auto i = 0u; i < MaxFramesInFlight; i++) - { - if (!pool) - return logFail("Couldn't create Command Pool!"); - if (!pool->createCommandBuffers(IGPUCommandPool::BUFFER_LEVEL::PRIMARY, { m_cmdBufs.data() + i, 1 })) - return logFail("Couldn't create Command Buffer!"); - } - - m_winMgr->setWindowSize(m_window.get(), WIN_W, WIN_H); - m_surface->recreateSwapchain(); - - // create output images - outHDRImage = m_device->createImage({ - { - .type = IGPUImage::ET_2D, - .samples = asset::ICPUImage::ESCF_1_BIT, - .format = asset::EF_R16G16B16A16_SFLOAT, - .extent = {WIN_W, WIN_H, 1}, - .mipLevels = 1, - .arrayLayers = 1, - .flags = IImage::ECF_NONE, - .usage = core::bitflag(asset::IImage::EUF_STORAGE_BIT) | asset::IImage::EUF_TRANSFER_SRC_BIT - } - }); - if (!outHDRImage || !m_device->allocate(outHDRImage->getMemoryReqs(), outHDRImage.get()).isValid()) - return logFail("Could not create HDR Image"); - - auto assetManager = make_smart_refctd_ptr(smart_refctd_ptr(system)); - - auto cQueue = getComputeQueue(); - - // create blas/tlas - renderDs = -//#define TRY_BUILD_FOR_NGFX // Validation errors on the fake Acquire-Presents, TODO fix -#ifdef TRY_BUILD_FOR_NGFX - // Nsight is special and can't do debugger delay so you can debug your CPU stuff during a capture - // Renderdoc-like Debugger Delay for NSight so that one may CPU debug applications launched from NSight - if (m_api->runningInGraphicsDebugger()==IAPIConnection::EDebuggerType::NSight) - { - static volatile bool debugger_not_attached = true; - while (debugger_not_attached) - std::this_thread::yield(); - } - // Nsight is special and can't capture anything not on the queue that performs the swapchain acquire/release - createAccelerationStructureDS(gQueue); -#else - createAccelerationStructureDS(cQueue); -#endif - if (!renderDs) - return logFail("Could not create acceleration structures and descriptor set"); - - // create pipelines - { - // shader - const std::string shaderPath = "app_resources/render.comp.hlsl"; - IAssetLoader::SAssetLoadParams lparams = {}; - lparams.logger = m_logger.get(); - lparams.workingDirectory = "app_resources"; - - auto key = nbl::this_example::builtin::build::get_spirv_key<"render">(m_device.get()); - auto bundle = m_assetMgr->getAsset(key.data(), lparams); - if (bundle.getContents().empty() || bundle.getAssetType() != IAsset::ET_SHADER) - { - m_logger->log("Shader %s not found!", ILogger::ELL_ERROR, shaderPath); - exit(-1); - } - - const auto assets = bundle.getContents(); - assert(assets.size() == 1); - smart_refctd_ptr shader = IAsset::castDown(assets[0]); - if (!shader) - return logFail("Failed to load precompiled shader!"); - - SPushConstantRange pcRange = { .stageFlags = IShader::E_SHADER_STAGE::ESS_COMPUTE, .offset = 0u, .size = sizeof(SPushConstants)}; - auto pipelineLayout = m_device->createPipelineLayout({ &pcRange, 1 }, smart_refctd_ptr(renderDs->getLayout()), nullptr, nullptr, nullptr); - - IGPUComputePipeline::SCreationParams params = {}; - params.layout = pipelineLayout.get(); - params.shader.shader = shader.get(); - params.shader.entryPoint = "main"; - if (!m_device->createComputePipelines(nullptr, { ¶ms, 1 }, &renderPipeline)) - return logFail("Failed to create compute pipeline"); - } - - // write descriptors - { - IGPUDescriptorSet::SDescriptorInfo info = {}; - info.desc = m_device->createImageView({ - .flags = IGPUImageView::ECF_NONE, - .subUsages = IGPUImage::E_USAGE_FLAGS::EUF_STORAGE_BIT, - .image = outHDRImage, - .viewType = IGPUImageView::E_TYPE::ET_2D, - .format = asset::EF_R16G16B16A16_SFLOAT - }); - if (!info.desc) - return logFail("Failed to create image view"); - info.info.image.imageLayout = IImage::LAYOUT::GENERAL; - const IGPUDescriptorSet::SWriteDescriptorSet write = {.dstSet=renderDs.get(), .binding=1, .arrayElement=0, .count=1, .info=&info}; - m_device->updateDescriptorSets({&write,1}, {}); - } - - // camera - { - 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); - } - - m_winMgr->show(m_window.get()); - oracle.reportBeginFrameRecord(); - - return true; - } - - inline void workLoopBody() override - { - const auto resourceIx = m_realFrameIx % MaxFramesInFlight; - - const uint32_t framesInFlight = core::min(MaxFramesInFlight, m_surface->getMaxAcquiresInFlight()); - - if (m_realFrameIx >= framesInFlight) - { - const ISemaphore::SWaitInfo cbDonePending[] = - { - { - .semaphore = m_semaphore.get(), - .value = m_realFrameIx + 1 - framesInFlight - } - }; - if (m_device->blockForSemaphores(cbDonePending) != ISemaphore::WAIT_RESULT::SUCCESS) - return; - } - - m_inputSystem->getDefaultMouse(&mouse); - m_inputSystem->getDefaultKeyboard(&keyboard); - - auto updatePresentationTimestamp = [&]() - { - m_currentImageAcquire = m_surface->acquireNextImage(); - - oracle.reportEndFrameRecord(); - const auto timestamp = oracle.getNextPresentationTimeStamp(); - oracle.reportBeginFrameRecord(); - - return timestamp; - }; - - const auto nextPresentationTimestamp = updatePresentationTimestamp(); - - if (!m_currentImageAcquire) - return; - - static bool first = true; - if (first) - { - first = false; - } - - auto* const cmdbuf = m_cmdBufs.data()[resourceIx].get(); - cmdbuf->reset(IGPUCommandBuffer::RESET_FLAGS::RELEASE_RESOURCES_BIT); - 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); - } - - const auto viewMatrix = camera.getViewMatrix(); - const auto projectionMatrix = camera.getProjectionMatrix(); - const auto viewProjectionMatrix = camera.getConcatenatedMatrix(); - - hlsl::float32_t3x4 modelMatrix = hlsl::math::linalg::identity(); - - hlsl::float32_t4x4 modelViewProjectionMatrix = nbl::hlsl::math::linalg::promoted_mul(viewProjectionMatrix, modelMatrix); - hlsl::float32_t4x4 invModelViewProjectionMatrix = hlsl::inverse(modelViewProjectionMatrix); - - auto* queue = getGraphicsQueue(); - - { - IGPUCommandBuffer::SPipelineBarrierDependencyInfo::image_barrier_t imageBarriers[1]; - imageBarriers[0].barrier = { - .dep = { - .srcStageMask = PIPELINE_STAGE_FLAGS::NONE, - .srcAccessMask = ACCESS_FLAGS::NONE, - .dstStageMask = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT, - .dstAccessMask = ACCESS_FLAGS::SHADER_WRITE_BITS - } - }; - imageBarriers[0].image = outHDRImage.get(); - imageBarriers[0].subresourceRange = { - .aspectMask = IImage::EAF_COLOR_BIT, - .baseMipLevel = 0u, - .levelCount = 1u, - .baseArrayLayer = 0u, - .layerCount = 1u - }; - imageBarriers[0].oldLayout = IImage::LAYOUT::UNDEFINED; - imageBarriers[0].newLayout = IImage::LAYOUT::GENERAL; - cmdbuf->pipelineBarrier(E_DEPENDENCY_FLAGS::EDF_NONE, { .imgBarriers = imageBarriers }); - } - - // do ray query - SPushConstants pc; - pc.geometryInfoBuffer = geometryInfoBuffer->getDeviceAddress(); - - const core::vector3df camPos = camera.getPosition().getAsVector3df(); - pc.camPos = { camPos.X, camPos.Y, camPos.Z }; - pc.invMVP = invModelViewProjectionMatrix; - - pc.scaleNDC = { 2.f / WIN_W, -2.f / WIN_H }; - pc.offsetNDC = { -1.f, 1.f }; - - cmdbuf->bindComputePipeline(renderPipeline.get()); - cmdbuf->pushConstants(renderPipeline->getLayout(), IShader::E_SHADER_STAGE::ESS_COMPUTE, 0, sizeof(SPushConstants), &pc); - cmdbuf->bindDescriptorSets(EPBP_COMPUTE, renderPipeline->getLayout(), 0, 1, &renderDs.get()); - cmdbuf->dispatch(getWorkgroupCount(WIN_W, WorkgroupSize), getWorkgroupCount(WIN_H, WorkgroupSize), 1); - - // blit - { - IGPUCommandBuffer::SPipelineBarrierDependencyInfo::image_barrier_t imageBarriers[2]; - imageBarriers[0].barrier = { - .dep = { - .srcStageMask = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT, - .srcAccessMask = ACCESS_FLAGS::SHADER_WRITE_BITS, - .dstStageMask = PIPELINE_STAGE_FLAGS::BLIT_BIT, - .dstAccessMask = ACCESS_FLAGS::TRANSFER_WRITE_BIT - } - }; - imageBarriers[0].image = outHDRImage.get(); - imageBarriers[0].subresourceRange = { - .aspectMask = IImage::EAF_COLOR_BIT, - .baseMipLevel = 0u, - .levelCount = 1u, - .baseArrayLayer = 0u, - .layerCount = 1u - }; - imageBarriers[0].oldLayout = IImage::LAYOUT::UNDEFINED; - imageBarriers[0].newLayout = IImage::LAYOUT::TRANSFER_SRC_OPTIMAL; - - imageBarriers[1].barrier = { - .dep = { - .srcStageMask = PIPELINE_STAGE_FLAGS::NONE, - .srcAccessMask = ACCESS_FLAGS::NONE, - .dstStageMask = PIPELINE_STAGE_FLAGS::BLIT_BIT, - .dstAccessMask = ACCESS_FLAGS::TRANSFER_WRITE_BIT - } - }; - imageBarriers[1].image = m_surface->getSwapchainResources()->getImage(m_currentImageAcquire.imageIndex); - imageBarriers[1].subresourceRange = { - .aspectMask = IImage::EAF_COLOR_BIT, - .baseMipLevel = 0u, - .levelCount = 1u, - .baseArrayLayer = 0u, - .layerCount = 1u - }; - imageBarriers[1].oldLayout = IImage::LAYOUT::UNDEFINED; - imageBarriers[1].newLayout = IImage::LAYOUT::TRANSFER_DST_OPTIMAL; - - cmdbuf->pipelineBarrier(E_DEPENDENCY_FLAGS::EDF_NONE, { .imgBarriers = imageBarriers }); - } - - { - IGPUCommandBuffer::SImageBlit regions[] = {{ - .srcMinCoord = {0,0,0}, - .srcMaxCoord = {WIN_W,WIN_H,1}, - .dstMinCoord = {0,0,0}, - .dstMaxCoord = {WIN_W,WIN_H,1}, - .layerCount = 1, - .srcBaseLayer = 0, - .dstBaseLayer = 0, - .srcMipLevel = 0, - .dstMipLevel = 0, - .aspectMask = IGPUImage::E_ASPECT_FLAGS::EAF_COLOR_BIT - }}; - - auto srcImg = outHDRImage.get(); - auto scRes = static_cast(m_surface->getSwapchainResources()); - auto dstImg = scRes->getImage(m_currentImageAcquire.imageIndex); - - cmdbuf->blitImage(srcImg, IImage::LAYOUT::TRANSFER_SRC_OPTIMAL, dstImg, IImage::LAYOUT::TRANSFER_DST_OPTIMAL, regions, ISampler::ETF_NEAREST); - } - - // TODO: transition to present - { - IGPUCommandBuffer::SPipelineBarrierDependencyInfo::image_barrier_t imageBarriers[1]; - imageBarriers[0].barrier = { - .dep = { - .srcStageMask = PIPELINE_STAGE_FLAGS::BLIT_BIT, - .srcAccessMask = ACCESS_FLAGS::TRANSFER_WRITE_BIT, - .dstStageMask = PIPELINE_STAGE_FLAGS::NONE, - .dstAccessMask = ACCESS_FLAGS::NONE - } - }; - imageBarriers[0].image = m_surface->getSwapchainResources()->getImage(m_currentImageAcquire.imageIndex); - imageBarriers[0].subresourceRange = { - .aspectMask = IImage::EAF_COLOR_BIT, - .baseMipLevel = 0u, - .levelCount = 1u, - .baseArrayLayer = 0u, - .layerCount = 1u - }; - imageBarriers[0].oldLayout = IImage::LAYOUT::TRANSFER_DST_OPTIMAL; - imageBarriers[0].newLayout = IImage::LAYOUT::PRESENT_SRC; - - cmdbuf->pipelineBarrier(E_DEPENDENCY_FLAGS::EDF_NONE, { .imgBarriers = imageBarriers }); - } - - cmdbuf->endDebugMarker(); - cmdbuf->end(); - - { - const IQueue::SSubmitInfo::SSemaphoreInfo rendered[] = - { - { - .semaphore = m_semaphore.get(), - .value = ++m_realFrameIx, - .stageMask = PIPELINE_STAGE_FLAGS::ALL_TRANSFER_BITS - } - }; - { - { - const IQueue::SSubmitInfo::SCommandBufferInfo commandBuffers[] = - { - {.cmdbuf = cmdbuf } - }; - - const IQueue::SSubmitInfo::SSemaphoreInfo acquired[] = - { - { - .semaphore = m_currentImageAcquire.semaphore, - .value = m_currentImageAcquire.acquireCount, - .stageMask = PIPELINE_STAGE_FLAGS::NONE - } - }; - const IQueue::SSubmitInfo infos[] = - { - { - .waitSemaphores = acquired, - .commandBuffers = commandBuffers, - .signalSemaphores = rendered - } - }; - - if (queue->submit(infos) == IQueue::RESULT::SUCCESS) - { - const nbl::video::ISemaphore::SWaitInfo waitInfos[] = - { { - .semaphore = m_semaphore.get(), - .value = m_realFrameIx - } }; - - m_device->blockForSemaphores(waitInfos); // this is not solution, quick wa to not throw validation errors - } - else - --m_realFrameIx; - } - } - - std::string caption = "[Nabla Engine] Geometry Creator"; - { - caption += ", displaying [all objects]"; - m_window->setCaption(caption); - } - m_surface->present(m_currentImageAcquire.imageIndex, rendered); - } - } - - inline bool keepRunning() override - { - if (m_surface->irrecoverable()) - return false; - - return true; - } - - inline bool onAppTerminated() override - { - return device_base_t::onAppTerminated(); - } - - private: - uint32_t getWorkgroupCount(uint32_t dim, uint32_t size) - { - return (dim + size - 1) / size; - } - - smart_refctd_ptr createAccelerationStructureDS(video::CThreadSafeQueueAdapter* queue) - { - using namespace nbl::scene; - - // triangles geometries - auto gc = make_smart_refctd_ptr(); - - auto transform_i = 0; - auto nextTransform = [&transform_i]() - { - hlsl::float32_t3x4 transform = hlsl::math::linalg::identity(); - hlsl::math::linalg::setTranslation(transform, hlsl::float32_t3(5.f * transform_i, 0.0f, 0.0f)); - transform_i++; - return transform; - }; - - std::vector cpuObjects; - - const auto prism = gc->createPrism(2, 5, 9); - cpuObjects.push_back(ReferenceObjectCpu{ .transform = nextTransform(), .data = prism}); - const auto unweldedPrism = CPolygonGeometryManipulator::createUnweldedList(prism.get()); - const auto smoothedPrism = CPolygonGeometryManipulator::createSmoothVertexNormal(unweldedPrism.get(), true); - cpuObjects.push_back(ReferenceObjectCpu{ .transform = nextTransform(), .data = smoothedPrism}); - - cpuObjects.push_back(ReferenceObjectCpu{ .transform = nextTransform(), .data = gc->createArrow() }); - cpuObjects.push_back(ReferenceObjectCpu{ .transform = nextTransform(), .data = CPolygonGeometryManipulator::createTriangleListIndexing(gc->createDisk(1.0f, 12).get()) }); - cpuObjects.push_back(ReferenceObjectCpu{ .transform = nextTransform(), .data = gc->createCube({1.f, 1.f, 1.f})}); - cpuObjects.push_back(ReferenceObjectCpu{ .transform = nextTransform(), .data = gc->createSphere(2, 16, 16)}); - cpuObjects.push_back(ReferenceObjectCpu{ .transform = nextTransform(), .data = gc->createCylinder(2, 2, 20)}); - cpuObjects.push_back(ReferenceObjectCpu{ .transform = nextTransform(), .data = gc->createRectangle({1.5, 3})}); - cpuObjects.push_back(ReferenceObjectCpu{ .transform = nextTransform(), .data = gc->createCone(2, 3, 10)}); - cpuObjects.push_back(ReferenceObjectCpu{ .transform = nextTransform(), .data = gc->createIcoSphere(1, 3, true)}); - - const auto geometryCount = [&cpuObjects] - { - size_t count = 0; - for (auto& cpuObject: cpuObjects) - { - const auto data = cpuObject.data; - cpuObject.instanceID = count; - if (std::holds_alternative(data)) - { - count += 1; - } else if (std::holds_alternative(data)) - { - const auto colData = std::get(data); - count += colData->getGeometries()->size(); - } - } - return count; - }(); - - auto geomInfoBuffer = ICPUBuffer::create({ geometryCount * sizeof(SGeomInfo) }); - - SGeomInfo* geomInfos = reinterpret_cast(geomInfoBuffer->getPointer()); - - // get ICPUBuffers into ICPUBottomLevelAccelerationStructures - std::vector> cpuBlas(cpuObjects.size()); - for (uint32_t blas_i = 0; blas_i < cpuBlas.size(); blas_i++) - { - auto& blas = cpuBlas[blas_i]; - blas = make_smart_refctd_ptr(); - if (std::holds_alternative(cpuObjects[blas_i].data)) - { - const auto data = std::get(cpuObjects[blas_i].data); - - auto triangles = make_refctd_dynamic_array>>(1u); - auto primitiveCounts = make_refctd_dynamic_array>(1u); - - auto& tri = triangles->front(); - - auto& primCount = primitiveCounts->front(); - primCount = data->getPrimitiveCount(); - - tri = data->exportForBLAS(); - - blas->setGeometries(std::move(triangles), std::move(primitiveCounts)); - - - } - else if (std::holds_alternative(cpuObjects[blas_i].data)) - { - - const auto data = std::get(cpuObjects[blas_i].data); - // TODO: make this codepath use blas exports - const auto& geometries = *data->getGeometries(); - const auto geometryCount = geometries.size(); - - auto triangles = make_refctd_dynamic_array>>(geometryCount); - auto primitiveCounts = make_refctd_dynamic_array>(geometryCount); - - for (auto geometry_i = 0u; geometry_i < geometryCount; geometry_i++) - { - const auto& geometry = geometries[geometry_i]; - const auto* polyGeo = static_cast(geometry.geometry.get()); - primitiveCounts->operator[](geometry_i) = polyGeo->getPrimitiveCount(); - auto& triangle = triangles->operator[](geometry_i); - triangle = polyGeo->exportForBLAS(); - if (geometry.hasTransform()) - triangle.transform = geometry.transform; - } - - blas->setGeometries(std::move(triangles), std::move(primitiveCounts)); - - } - auto blasFlags = bitflag(IGPUBottomLevelAccelerationStructure::BUILD_FLAGS::PREFER_FAST_TRACE_BIT) | IGPUBottomLevelAccelerationStructure::BUILD_FLAGS::ALLOW_COMPACTION_BIT; - if (m_physicalDevice->getProperties().limits.rayTracingPositionFetch) - blasFlags |= IGPUBottomLevelAccelerationStructure::BUILD_FLAGS::ALLOW_DATA_ACCESS; - - blas->setBuildFlags(blasFlags); - blas->setContentHash(blas->computeContentHash()); - } - - // get ICPUBottomLevelAccelerationStructure into ICPUTopLevelAccelerationStructure - auto geomInstances = make_refctd_dynamic_array>(cpuObjects.size()); - { - uint32_t i = 0; - for (auto instance = geomInstances->begin(); instance != geomInstances->end(); instance++, i++) - { - ICPUTopLevelAccelerationStructure::StaticInstance inst; - inst.base.blas = cpuBlas[i]; - inst.base.flags = static_cast(IGPUTopLevelAccelerationStructure::INSTANCE_FLAGS::TRIANGLE_FACING_CULL_DISABLE_BIT); - inst.base.instanceCustomIndex = cpuObjects[i].instanceID; - inst.base.instanceShaderBindingTableRecordOffset = 0; - inst.base.mask = 0xFF; - inst.transform = cpuObjects[i].transform; - instance->instance = inst; - } - } - - auto cpuTlas = make_smart_refctd_ptr(); - cpuTlas->setInstances(std::move(geomInstances)); - cpuTlas->setBuildFlags(IGPUTopLevelAccelerationStructure::BUILD_FLAGS::PREFER_FAST_TRACE_BIT); - - // descriptor set and layout - ICPUDescriptorSetLayout::SBinding bindings[] = { - { - .binding = 0, - .type = asset::IDescriptor::E_TYPE::ET_ACCELERATION_STRUCTURE, - .createFlags = IDescriptorSetLayoutBase::SBindingBase::E_CREATE_FLAGS::ECF_NONE, - .stageFlags = asset::IShader::E_SHADER_STAGE::ESS_COMPUTE, - .count = 1, - }, - { - .binding = 1, - .type = asset::IDescriptor::E_TYPE::ET_STORAGE_IMAGE, - .createFlags = IDescriptorSetLayoutBase::SBindingBase::E_CREATE_FLAGS::ECF_NONE, - .stageFlags = asset::IShader::E_SHADER_STAGE::ESS_COMPUTE, - .count = 1, - } - }; - auto descriptorSet = core::make_smart_refctd_ptr(core::make_smart_refctd_ptr(bindings)); - descriptorSet->getDescriptorInfos(IDescriptorSetLayoutBase::CBindingRedirect::binding_number_t{0},IDescriptor::E_TYPE::ET_ACCELERATION_STRUCTURE).front().desc = cpuTlas; - -//#define TEST_REBAR_FALLBACK - // convert with asset converter - smart_refctd_ptr converter = CAssetConverter::create({ .device = m_device.get(), .optimizer = {} }); - struct MyInputs : CAssetConverter::SInputs - { -#ifndef TEST_REBAR_FALLBACK - inline uint32_t constrainMemoryTypeBits(const size_t groupCopyID, const IAsset* canonicalAsset, const blake3_hash_t& contentHash, const IDeviceMemoryBacked* memoryBacked) const override - { - assert(memoryBacked); - return memoryBacked->getObjectType()!=IDeviceMemoryBacked::EOT_BUFFER ? (~0u):rebarMemoryTypes; - } -#endif - uint32_t rebarMemoryTypes; - } inputs = {}; - inputs.logger = m_logger.get(); - inputs.rebarMemoryTypes = m_physicalDevice->getDirectVRAMAccessMemoryTypeBits(); -#ifndef TEST_REBAR_FALLBACK - struct MyAllocator final : public IDeviceMemoryAllocator - { - ILogicalDevice* getDeviceForAllocations() const override {return device;} - - SAllocation allocate(const SAllocateInfo& info) override - { - auto retval = device->allocate(info); - // map what is mappable by default so ReBAR checks succeed - if (retval.isValid() && retval.memory->isMappable()) - retval.memory->map({.offset=0,.length=info.size}); - return retval; - } - - ILogicalDevice* device; - } myalloc; - myalloc.device = m_device.get(); - inputs.allocator = &myalloc; -#endif - - CAssetConverter::patch_t tlasPatch = {}; - tlasPatch.compactAfterBuild = true; - std::vector> tmpBLASPatches(cpuObjects.size()); - std::vector tmpGeometries; - tmpGeometries.reserve(geometryCount); - std::vector> tmpGeometryPatches; - tmpGeometryPatches.reserve(geometryCount); - { - tmpBLASPatches.front().compactAfterBuild = true; - std::fill(tmpBLASPatches.begin(),tmpBLASPatches.end(),tmpBLASPatches.front()); - // - for (uint32_t i = 0; i < cpuObjects.size(); i++) - { - const auto data = cpuObjects[i].data; - if (std::holds_alternative(data)) - { - const auto polygonData = std::get(data); - tmpGeometries.push_back(polygonData.get()); - tmpGeometryPatches.push_back({}); - tmpGeometryPatches.back().indexBufferUsages = IGPUBuffer::E_USAGE_FLAGS::EUF_SHADER_DEVICE_ADDRESS_BIT; - } else if (std::holds_alternative(data)) - { - const auto collectionData = std::get(data); - for (const auto& geometryRef : *collectionData->getGeometries()) - { - auto* polyGeo = static_cast(geometryRef.geometry.get()); - tmpGeometries.push_back(polyGeo); - tmpGeometryPatches.push_back({}); - tmpGeometryPatches.back().indexBufferUsages = IGPUBuffer::E_USAGE_FLAGS::EUF_SHADER_DEVICE_ADDRESS_BIT; - } - } - } - assert(tmpGeometries.size() == geometryCount); - assert(tmpGeometryPatches.size() == geometryCount); - - std::get>(inputs.assets) = {&descriptorSet.get(),1}; - std::get>(inputs.assets) = {&cpuTlas.get(),1}; - std::get>(inputs.patches) = {&tlasPatch,1}; - std::get>(inputs.assets) = {&cpuBlas.data()->get(),cpuBlas.size()}; - std::get>(inputs.patches) = tmpBLASPatches; - std::get>(inputs.assets) = tmpGeometries; - std::get>(inputs.patches) = tmpGeometryPatches; - } - - auto reservation = converter->reserve(inputs); - - constexpr auto XferBufferCount = 2; - std::array,XferBufferCount> xferBufs = {}; - std::array xferBufInfos = {}; - { - auto pool = m_device->createCommandPool(getTransferUpQueue()->getFamilyIndex(),IGPUCommandPool::CREATE_FLAGS::RESET_COMMAND_BUFFER_BIT | IGPUCommandPool::CREATE_FLAGS::TRANSIENT_BIT); - pool->createCommandBuffers(IGPUCommandPool::BUFFER_LEVEL::PRIMARY,xferBufs); - xferBufs.front()->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT); - for (auto i=0; icreateSemaphore(0u); - xferSema->setObjectDebugName("Transfer Semaphore"); - SIntendedSubmitInfo transfer = {}; - transfer.queue = getTransferUpQueue(); - transfer.scratchCommandBuffers = xferBufInfos; - transfer.scratchSemaphore = { - .semaphore = xferSema.get(), - .value = 0u, - .stageMask = PIPELINE_STAGE_FLAGS::ALL_TRANSFER_BITS - }; - - constexpr auto CompBufferCount = 2; - std::array,CompBufferCount> compBufs = {}; - std::array compBufInfos = {}; - { - auto pool = m_device->createCommandPool(getComputeQueue()->getFamilyIndex(),IGPUCommandPool::CREATE_FLAGS::RESET_COMMAND_BUFFER_BIT|IGPUCommandPool::CREATE_FLAGS::TRANSIENT_BIT); - pool->createCommandBuffers(IGPUCommandPool::BUFFER_LEVEL::PRIMARY,compBufs); - compBufs.front()->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT); - for (auto i=0; icreateSemaphore(0u); - compSema->setObjectDebugName("Compute Semaphore"); - SIntendedSubmitInfo compute = {}; - compute.queue = getComputeQueue(); - compute.scratchCommandBuffers = compBufInfos; - compute.scratchSemaphore = { - .semaphore = compSema.get(), - .value = 0u, - .stageMask = PIPELINE_STAGE_FLAGS::ACCELERATION_STRUCTURE_BUILD_BIT|PIPELINE_STAGE_FLAGS::ACCELERATION_STRUCTURE_COPY_BIT - }; - // convert -#ifdef TRY_BUILD_FOR_NGFX // NSight is "debugger-challenged" it can't capture anything not happenning "during a frame", so we need to trick it - m_currentImageAcquire = m_surface->acquireNextImage(); - { - const IQueue::SSubmitInfo::SSemaphoreInfo acquired[] = { { - .semaphore = m_currentImageAcquire.semaphore, - .value = m_currentImageAcquire.acquireCount, - .stageMask = PIPELINE_STAGE_FLAGS::ALL_COMMANDS_BITS - } }; - m_surface->present(m_currentImageAcquire.imageIndex,acquired); - } - m_currentImageAcquire = m_surface->acquireNextImage(); -#endif - m_api->startCapture(); - auto gQueue = getGraphicsQueue(); - { - smart_refctd_ptr scratchAlloc; - { - constexpr auto MaxAlignment = 256; - constexpr auto MinAllocationSize = 1024; - const auto scratchSize = core::alignUp(reservation.getMinASBuildScratchSize(false),MaxAlignment); - - - IGPUBuffer::SCreationParams creationParams = {}; - creationParams.size = scratchSize; - creationParams.usage = IGPUBuffer::EUF_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT|IGPUBuffer::EUF_SHADER_DEVICE_ADDRESS_BIT|IGPUBuffer::EUF_STORAGE_BUFFER_BIT; -#ifdef TEST_REBAR_FALLBACK - creationParams.usage |= IGPUBuffer::EUF_TRANSFER_DST_BIT; - core::unordered_set sharingSet = {compute.queue->getFamilyIndex(),transfer.queue->getFamilyIndex()}; - core::vector sharingIndices(sharingSet.begin(),sharingSet.end()); - if (sharingIndices.size()>1) - creationParams.queueFamilyIndexCount = sharingIndices.size(); - creationParams.queueFamilyIndices = sharingIndices.data(); -#endif - auto scratchBuffer = m_device->createBuffer(std::move(creationParams)); - - auto reqs = scratchBuffer->getMemoryReqs(); -#ifndef TEST_REBAR_FALLBACK - reqs.memoryTypeBits &= m_physicalDevice->getDirectVRAMAccessMemoryTypeBits(); -#endif - auto allocation = m_device->allocate(reqs,scratchBuffer.get(),IDeviceMemoryAllocation::EMAF_DEVICE_ADDRESS_BIT); -#ifndef TEST_REBAR_FALLBACK - allocation.memory->map({.offset=0,.length=reqs.size}); -#endif - - scratchAlloc = make_smart_refctd_ptr( - SBufferRange{0ull,scratchSize,std::move(scratchBuffer)}, - core::allocator(),MaxAlignment,MinAllocationSize - ); - } - - struct MyParams final : CAssetConverter::SConvertParams - { - inline uint32_t getFinalOwnerQueueFamily(const IGPUBuffer* buffer, const core::blake3_hash_t& createdFrom) override - { - return finalUser; - } - inline uint32_t getFinalOwnerQueueFamily(const IGPUAccelerationStructure* image, const core::blake3_hash_t& createdFrom) override - { - return finalUser; - } - - uint8_t finalUser; - } params = {}; - params.utilities = m_utils.get(); - params.transfer = &transfer; - params.compute = &compute; - params.scratchForDeviceASBuild = scratchAlloc.get(); - params.finalUser = gQueue->getFamilyIndex(); - - auto future = reservation.convert(params); - if (future.copy() != IQueue::RESULT::SUCCESS) - { - m_logger->log("Failed to await submission feature!", ILogger::ELL_ERROR); - return {}; - } - - auto&& tlases = reservation.getGPUObjects(); - m_gpuTlas = tlases[0].value; - - auto&& gpuPolygonGeometries = reservation.getGPUObjects(); - m_gpuPolygons.resize(gpuPolygonGeometries.size()); - - // assign gpu objects to output - for (uint32_t i = 0; i < gpuPolygonGeometries.size(); i++) - { - const auto& gpuPolygon = gpuPolygonGeometries[i].value; - const auto gpuTriangles = gpuPolygon->exportForBLAS(); - - const auto& vertexBufferBinding = gpuTriangles.vertexData[0]; - const uint64_t vertexBufferAddress = vertexBufferBinding.buffer->getDeviceAddress() + vertexBufferBinding.offset; - - const auto& normalView = gpuPolygon->getNormalView(); - const uint64_t normalBufferAddress = normalView ? normalView.src.buffer->getDeviceAddress() + normalView.src.offset : 0; - - auto normalType = NT_R32G32B32_SFLOAT; - if (normalView && normalView.composed.format == EF_R8G8B8A8_SNORM) - normalType = NT_R8G8B8A8_SNORM; - - const auto& indexBufferBinding = gpuTriangles.indexData; - auto& geomInfo = geomInfos[i]; - geomInfo = { - .vertexBufferAddress = vertexBufferAddress, - .indexBufferAddress = indexBufferBinding.buffer ? indexBufferBinding.buffer->getDeviceAddress() + indexBufferBinding.offset : 0, - .normalBufferAddress = normalBufferAddress, - .normalType = normalType, - .indexType = gpuTriangles.indexType, - }; - - m_gpuPolygons[i] = gpuPolygon; - } - } - - // - { - IGPUBuffer::SCreationParams params; - params.usage = IGPUBuffer::EUF_STORAGE_BUFFER_BIT | IGPUBuffer::EUF_TRANSFER_DST_BIT | IGPUBuffer::EUF_SHADER_DEVICE_ADDRESS_BIT; - params.size = geometryCount * sizeof(SGeomInfo); - m_utils->createFilledDeviceLocalBufferOnDedMem(SIntendedSubmitInfo{ .queue = gQueue }, std::move(params), geomInfos).move_into(geometryInfoBuffer); - } - - // acquire ownership - { - smart_refctd_ptr cmdbuf; - { - const auto gQFI = gQueue->getFamilyIndex(); - m_device->createCommandPool(gQFI,IGPUCommandPool::CREATE_FLAGS::TRANSIENT_BIT)->createCommandBuffers(IGPUCommandPool::BUFFER_LEVEL::PRIMARY,{&cmdbuf,1}); - cmdbuf->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT); - { - core::vector> bufBarriers; - auto acquireBufferRange = [&bufBarriers](const uint8_t otherQueueFamilyIndex, const SBufferRange& bufferRange) - { - bufBarriers.push_back({ - .barrier = { - .dep = { - .srcStageMask = PIPELINE_STAGE_FLAGS::NONE, - .srcAccessMask = ACCESS_FLAGS::NONE, - .dstStageMask = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT, - // we don't care what exactly, uncomplex our code - .dstAccessMask = ACCESS_FLAGS::SHADER_READ_BITS - }, - .ownershipOp = IGPUCommandBuffer::SOwnershipTransferBarrier::OWNERSHIP_OP::ACQUIRE, - .otherQueueFamilyIndex = otherQueueFamilyIndex - }, - .range = bufferRange - }); - }; -#ifdef TEST_REBAR_FALLBACK - if (const auto otherQueueFamilyIndex=transfer.queue->getFamilyIndex(); gQFI!=otherQueueFamilyIndex) - for (const auto& buffer : reservation.getGPUObjects()) - { - const auto& buff = buffer.value; - if (buff) - acquireBufferRange(otherQueueFamilyIndex,{.offset=0,.size=buff->getSize(),.buffer=buff}); - } -#endif - if (const auto otherQueueFamilyIndex=compute.queue->getFamilyIndex(); gQFI!=otherQueueFamilyIndex) - { - auto acquireAS = [&acquireBufferRange,otherQueueFamilyIndex](const IGPUAccelerationStructure* as) - { - acquireBufferRange(otherQueueFamilyIndex,as->getCreationParams().bufferRange); - }; - for (const auto& blas : reservation.getGPUObjects()) - acquireAS(blas.value.get()); - acquireAS(reservation.getGPUObjects().front().value.get()); - } - if (!bufBarriers.empty()) - cmdbuf->pipelineBarrier(asset::E_DEPENDENCY_FLAGS::EDF_NONE,{.memBarriers={},.bufBarriers=bufBarriers}); - } - cmdbuf->end(); - } - if (!cmdbuf->empty()) - { - const IQueue::SSubmitInfo::SCommandBufferInfo cmdbufInfo = { - .cmdbuf = cmdbuf.get() - }; - const IQueue::SSubmitInfo::SSemaphoreInfo signal = { - .semaphore = compute.scratchSemaphore.semaphore, - .value = compute.getFutureScratchSemaphore().value, - .stageMask = asset::PIPELINE_STAGE_FLAGS::ALL_COMMANDS_BITS - }; - auto wait = signal; - wait.value--; - const IQueue::SSubmitInfo info = { - .waitSemaphores = {&wait,1}, // we already waited with the host on the AS build - .commandBuffers = {&cmdbufInfo,1}, - .signalSemaphores = {&signal,1} - }; - if (const auto retval=gQueue->submit({&info,1}); retval!=IQueue::RESULT::SUCCESS) - m_logger->log("Failed to transfer ownership with code %d!",system::ILogger::ELL_ERROR,retval); - } - } -#undef TEST_REBAR_FALLBACK - -#ifdef TRY_BUILD_FOR_NGFX - { - const IQueue::SSubmitInfo::SSemaphoreInfo acquired[] = { { - .semaphore = m_currentImageAcquire.semaphore, - .value = m_currentImageAcquire.acquireCount, - .stageMask = PIPELINE_STAGE_FLAGS::ALL_COMMANDS_BITS - } }; - m_surface->present(m_currentImageAcquire.imageIndex,acquired); - } -#endif - m_api->endCapture(); - - return reservation.getGPUObjects().front().value; - } - - - smart_refctd_ptr m_window; - smart_refctd_ptr> m_surface; - smart_refctd_ptr m_semaphore; - uint64_t m_realFrameIx = 0; - std::array, MaxFramesInFlight> m_cmdBufs; - ISimpleManagedSurface::SAcquireResult m_currentImageAcquire = {}; - - core::smart_refctd_ptr m_inputSystem; - InputSystem::ChannelReader mouse; - InputSystem::ChannelReader keyboard; - - Camera camera = Camera(core::vectorSIMDf(0, 0, 0), core::vectorSIMDf(0, 0, 0), hlsl::float32_t4x4()); - video::CDumbPresentationOracle oracle; - - smart_refctd_ptr geometryInfoBuffer; - smart_refctd_ptr outHDRImage; - core::vector> m_gpuPolygons; - smart_refctd_ptr m_gpuTlas; - - smart_refctd_ptr renderPipeline; - smart_refctd_ptr renderDs; - -}; - -NBL_MAIN_FUNC(RayQueryGeometryApp) \ No newline at end of file diff --git a/68_JpegLoading/CMakeLists.txt b/68_JpegLoading/CMakeLists.txt deleted file mode 100644 index 54aa0c43b..000000000 --- a/68_JpegLoading/CMakeLists.txt +++ /dev/null @@ -1,27 +0,0 @@ -include(common RESULT_VARIABLE RES) -if(NOT RES) - message(FATAL_ERROR "common.cmake not found. Should be in {repo_root}/cmake directory") -endif() - -nbl_create_executable_project("" "" "" "" "${NBL_EXECUTABLE_PROJECT_CREATION_PCH_TARGET}") - -if(NBL_EMBED_BUILTIN_RESOURCES) - set(_BR_TARGET_ ${EXECUTABLE_NAME}_builtinResourceData) - set(RESOURCE_DIR "app_resources") - - get_filename_component(_SEARCH_DIRECTORIES_ "${CMAKE_CURRENT_SOURCE_DIR}" ABSOLUTE) - get_filename_component(_OUTPUT_DIRECTORY_SOURCE_ "${CMAKE_CURRENT_BINARY_DIR}/src" ABSOLUTE) - get_filename_component(_OUTPUT_DIRECTORY_HEADER_ "${CMAKE_CURRENT_BINARY_DIR}/include" ABSOLUTE) - - file(GLOB_RECURSE BUILTIN_RESOURCE_FILES RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}/${RESOURCE_DIR}" CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/${RESOURCE_DIR}/*") - foreach(RES_FILE ${BUILTIN_RESOURCE_FILES}) - LIST_BUILTIN_RESOURCE(RESOURCES_TO_EMBED "${RES_FILE}") - endforeach() - - ADD_CUSTOM_BUILTIN_RESOURCES(${_BR_TARGET_} RESOURCES_TO_EMBED "${_SEARCH_DIRECTORIES_}" "${RESOURCE_DIR}" "nbl::this_example::builtin" "${_OUTPUT_DIRECTORY_HEADER_}" "${_OUTPUT_DIRECTORY_SOURCE_}") - - LINK_BUILTIN_RESOURCES_TO_TARGET(${EXECUTABLE_NAME} ${_BR_TARGET_}) -endif() - -add_dependencies(${EXECUTABLE_NAME} argparse) -target_include_directories(${EXECUTABLE_NAME} PUBLIC $) \ No newline at end of file diff --git a/68_JpegLoading/main.cpp b/68_JpegLoading/main.cpp deleted file mode 100644 index 8df56837d..000000000 --- a/68_JpegLoading/main.cpp +++ /dev/null @@ -1,197 +0,0 @@ -// Copyright (C) 2018-2024 - DevSH GrapMonoAssetManagerAndBuiltinResourceApplicationhics 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 "nbl/examples/examples.hpp" - -#include - -#include "nlohmann/json.hpp" -#include "argparse/argparse.hpp" - - -using json = nlohmann::json; - -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; - -class ThreadPool -{ -using task_t = std::function; -public: - ThreadPool(size_t workers = std::thread::hardware_concurrency()) - { - for (size_t i = 0; i < workers; i++) - { - m_workers.emplace_back([this] { - task_t task; - - while (1) - { - { - std::unique_lock lock(m_queueLock); - m_taskAvailable.wait(lock, [this] { return !m_tasks.empty() || m_shouldStop; }); - - if (m_shouldStop && m_tasks.empty()) { - return; - } - - task = std::move(m_tasks.front()); - m_tasks.pop(); - } - - task(); - } - }); - } - } - - ~ThreadPool() - { - m_shouldStop = true; - m_taskAvailable.notify_all(); - - for (auto& worker : m_workers) - { - worker.join(); - } - } - - void enqueue(task_t task) - { - { - std::lock_guard lock(m_queueLock); - m_tasks.emplace(std::move(task)); - } - m_taskAvailable.notify_one(); - } - private: - std::mutex m_queueLock; - std::condition_variable m_taskAvailable; - std::vector m_workers; - std::queue m_tasks; - std::atomic m_shouldStop = false; -}; - -class JpegLoaderApp final : public BuiltinResourcesApplication -{ - using clock_t = std::chrono::steady_clock; - using clock_resolution_t = std::chrono::milliseconds; - using base_t = BuiltinResourcesApplication; - public: - using base_t::base_t; - - inline bool onAppInitialized(smart_refctd_ptr&& system) override - { - argparse::ArgumentParser program("Color Space"); - - program.add_argument("--directory") - .required() - .help("Path to a directory where all JPEG files are stored (not recursive)"); - - program.add_argument("--output") - .default_value("output.json") - .help("Path to the file where the benchmark result will be stored"); - - try - { - program.parse_args({ argv.data(), argv.data() + argv.size() }); - } - catch (const std::exception& err) - { - std::cerr << err.what() << std::endl << program; // NOTE: std::cerr because logger isn't initialized yet - return false; - } - - if (!base_t::onAppInitialized(std::move(system))) - return false; - - options.directory = program.get("--directory"); - options.outputFile = program.get("--output"); - - // check if directory exists - if (!std::filesystem::exists(options.directory)) - { - logFail("Provided directory doesn't exist"); - return false; - } - - auto start = clock_t::now(); - std::vector files; - - { - ThreadPool tp; - - constexpr auto cachingFlags = static_cast(IAssetLoader::ECF_DONT_CACHE_REFERENCES & IAssetLoader::ECF_DONT_CACHE_TOP_LEVEL); - const IAssetLoader::SAssetLoadParams loadParams(0ull, nullptr, cachingFlags, IAssetLoader::ELPF_NONE, m_logger.get()); - - for (auto& item : std::filesystem::directory_iterator(options.directory)) - { - auto& path = item.path(); - if (path.has_extension() && path.extension() == ".jpg") - { - files.emplace_back(std::move(path.generic_string())); - - ISystem::future_t> future; - m_system->createFile(future, path, IFile::ECF_READ | IFile::ECF_MAPPABLE); - - if (auto pFile = future.acquire(); pFile && pFile->get()) - { - auto& file = *pFile; - tp.enqueue([=] { - m_logger->log("Loading %S", ILogger::ELL_INFO, path.c_str()); - m_assetMgr->getAsset(file.get(), path.generic_string(), loadParams); - }); - } - } - } - } - - auto stop = clock_t::now(); - auto time = std::chrono::duration_cast(stop - start).count(); - - m_logger->log("Process took %llu ms", ILogger::ELL_INFO, time); - - // Dump data to JSON - ::json j; - j["loaded_files"] = files; - j["duration_ms"] = time; - - std::ofstream output(options.outputFile); - if (!output.good()) - { - logFail("Failed to open %S", options.outputFile); - return false; - } - - output << j; - - return true; - } - - inline bool keepRunning() override - { - return false; - } - - inline void workLoopBody() override - { - - } - -private: - struct NBL_APP_OPTIONS - { - std::string directory; - std::string outputFile; - } options; -}; - -NBL_MAIN_FUNC(JpegLoaderApp) \ No newline at end of file diff --git a/70_FLIPFluids/CMakeLists.txt b/70_FLIPFluids/CMakeLists.txt deleted file mode 100644 index 96eb752c3..000000000 --- a/70_FLIPFluids/CMakeLists.txt +++ /dev/null @@ -1,99 +0,0 @@ -include(common RESULT_VARIABLE RES) -if(NOT RES) - message(FATAL_ERROR "common.cmake not found. Should be in {repo_root}/cmake directory") -endif() - -nbl_create_executable_project("" "" "" "" "${NBL_EXECUTABLE_PROJECT_CREATION_PCH_TARGET}") - -if(NBL_EMBED_BUILTIN_RESOURCES) - set(_BR_TARGET_ ${EXECUTABLE_NAME}_builtinResourceData) - set(RESOURCE_DIR "app_resources") - - get_filename_component(_SEARCH_DIRECTORIES_ "${CMAKE_CURRENT_SOURCE_DIR}" ABSOLUTE) - get_filename_component(_OUTPUT_DIRECTORY_SOURCE_ "${CMAKE_CURRENT_BINARY_DIR}/src" ABSOLUTE) - get_filename_component(_OUTPUT_DIRECTORY_HEADER_ "${CMAKE_CURRENT_BINARY_DIR}/include" ABSOLUTE) - - file(GLOB_RECURSE BUILTIN_RESOURCE_FILES RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}/${RESOURCE_DIR}" CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/${RESOURCE_DIR}/*") - foreach(RES_FILE ${BUILTIN_RESOURCE_FILES}) - LIST_BUILTIN_RESOURCE(RESOURCES_TO_EMBED "${RES_FILE}") - endforeach() - - ADD_CUSTOM_BUILTIN_RESOURCES(${_BR_TARGET_} RESOURCES_TO_EMBED "${_SEARCH_DIRECTORIES_}" "${RESOURCE_DIR}" "nbl::this_example::builtin" "${_OUTPUT_DIRECTORY_HEADER_}" "${_OUTPUT_DIRECTORY_SOURCE_}") - - LINK_BUILTIN_RESOURCES_TO_TARGET(${EXECUTABLE_NAME} ${_BR_TARGET_}) -endif() - -set(OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/auto-gen") - -set(SM 6_8) -set(JSON [=[ -[ - { - "INPUT": "app_resources/compute/diffusion.comp.hlsl", - "KEY": "diffusion", - }, - { - "INPUT": "app_resources/fluidParticles.vertex.hlsl", - "KEY": "fluidParticles_vertex", - }, - { - "INPUT": "app_resources/fluidParticles.fragment.hlsl", - "KEY": "fluidParticles_fragment", - }, - { - "INPUT": "app_resources/compute/particlesInit.comp.hlsl", - "KEY": "particlesInit", - }, - { - "INPUT": "app_resources/compute/genParticleVertices.comp.hlsl", - "KEY": "genParticleVertices", - }, - { - "INPUT": "app_resources/compute/prepareCellUpdate.comp.hlsl", - "KEY": "prepareCellUpdate", - }, - { - "INPUT": "app_resources/compute/updateFluidCells.comp.hlsl", - "KEY": "updateFluidCells", - }, - { - "INPUT": "app_resources/compute/applyBodyForces.comp.hlsl", - "KEY": "applyBodyForces", - }, - { - "INPUT": "app_resources/compute/pressureSolver.comp.hlsl", - "KEY": "pressureSolver", - }, - { - "INPUT": "app_resources/compute/advectParticles.comp.hlsl", - "KEY": "advectParticles", - } - -] -]=]) -string(CONFIGURE "${JSON}" JSON) - -set(COMPILE_OPTIONS - -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} -) diff --git a/70_FLIPFluids/README.md b/70_FLIPFluids/README.md deleted file mode 100644 index 5dc4e9fe4..000000000 --- a/70_FLIPFluids/README.md +++ /dev/null @@ -1 +0,0 @@ -https://github.com/user-attachments/assets/fc8bfac9-c8fb-49f1-87d6-251aae015945 diff --git a/70_FLIPFluids/app_resources/cellUtils.hlsl b/70_FLIPFluids/app_resources/cellUtils.hlsl deleted file mode 100644 index 8130257da..000000000 --- a/70_FLIPFluids/app_resources/cellUtils.hlsl +++ /dev/null @@ -1,202 +0,0 @@ -#ifndef _FLIP_EXAMPLE_CELL_UTILS_HLSL -#define _FLIP_EXAMPLE_CELL_UTILS_HLSL - -// Use when the "enum gets accidentally bitcasted to float" DXC bug is gone -#if 0 -enum/* class*/ CellMaterial : uint16_t -{ - CM_SOLID = 0, - CM_FLUID = 1, - CM_AIR = 2 -}; -#endif - -#ifdef __HLSL_VERSION -static const uint CM_SOLID = 0; -static const uint CM_FLUID = 1; -static const uint CM_AIR = 2; - -// TODO: Optimize,the cell material should probably stored in R8_UINT, in a 2x2 pattern, then preloaded into shared memory this way you don't store the x,y,z neighbours and just check in smem -/* -struct SCell -{ - CellMaterial data; -}; -*/ -// 1) move all functions below into methods of struct above -// 2) make a special accessor or something over some provided shared memory that handles accessing neighbours from a larger texture - -static const uint CellMatMask = 0x00000003u; -static const uint CellMatMaskShift = 0; -static const uint XPrevMatMask = 0x0000000cu; -static const uint XPrevMatMaskShift = 2; -static const uint XNextMatMask = 0x00000030u; -static const uint XNextMatMaskShift = 4; -static const uint YPrevMatMask = 0x000000c0u; -static const uint YPrevMatMaskShift = 6; -static const uint YNextMatMask = 0x00000300u; -static const uint YNextMatMaskShift = 8; -static const uint ZPrevMatMask = 0x00000c00u; -static const uint ZPrevMatMaskShift = 10; -static const uint ZNextMatMask = 0x00003000u; -static const uint ZNextMatMaskShift = 12; - -inline void setCellMaterial(inout uint cellMaterials, uint cellMaterial) -{ - cellMaterials = (cellMaterials & ~CellMatMask) | ((cellMaterial << CellMatMaskShift) & CellMatMask); -} -inline uint getCellMaterial(uint cellMaterials) -{ - return (cellMaterials & CellMatMask) >> CellMatMaskShift; -} -inline void setXPrevMaterial(inout uint cellMaterials, uint cellMaterial) -{ - cellMaterials = (cellMaterials & ~XPrevMatMask) | ((cellMaterial << XPrevMatMaskShift) & XPrevMatMask); -} -inline uint getXPrevMaterial(uint cellMaterials) -{ - return (cellMaterials & XPrevMatMask) >> XPrevMatMaskShift; -} -inline void setXNextMaterial(inout uint cellMaterials, uint cellMaterial) -{ - cellMaterials = (cellMaterials & ~XNextMatMask) | ((cellMaterial << XNextMatMaskShift) & XNextMatMask); -} -inline uint getXNextMaterial(uint cellMaterials) -{ - return (cellMaterials & XNextMatMask) >> XNextMatMaskShift; -} -inline void setYPrevMaterial(inout uint cellMaterials, uint cellMaterial) -{ - cellMaterials = (cellMaterials & ~YPrevMatMask) | ((cellMaterial << YPrevMatMaskShift) & YPrevMatMask); -} -inline uint getYPrevMaterial(uint cellMaterials) -{ - return (cellMaterials & YPrevMatMask) >> YPrevMatMaskShift; -} -inline void setYNextMaterial(inout uint cellMaterials, uint cellMaterial) -{ - cellMaterials = (cellMaterials & ~YNextMatMask) | ((cellMaterial << YNextMatMaskShift) & YNextMatMask); -} -inline uint getYNextMaterial(uint cellMaterials) -{ - return (cellMaterials & YNextMatMask) >> YNextMatMaskShift; -} -inline void setZPrevMaterial(inout uint cellMaterials, uint cellMaterial) -{ - cellMaterials = (cellMaterials & ~ZPrevMatMask) | ((cellMaterial << ZPrevMatMaskShift) & ZPrevMatMask); -} -inline uint getZPrevMaterial(uint cellMaterials) -{ - return (cellMaterials & ZPrevMatMask) >> ZPrevMatMaskShift; -} -inline void setZNextMaterial(inout uint cellMaterials, uint cellMaterial) -{ - cellMaterials = (cellMaterials & ~ZNextMatMask) | ((cellMaterial << ZNextMatMaskShift) & ZNextMatMask); -} -inline uint getZNextMaterial(uint cellMaterials) -{ - return (cellMaterials & ZNextMatMask) >> ZNextMatMaskShift; -} - - -inline void setCellMaterial(inout uint3 cellMaterials, uint3 cellMaterial) -{ - cellMaterials = (cellMaterials & ~CellMatMask) | ((cellMaterial << CellMatMaskShift) & CellMatMask); -} -inline uint3 getCellMaterial(uint3 cellMaterials) -{ - return (cellMaterials & CellMatMask) >> CellMatMaskShift; -} -inline void setXPrevMaterial(inout uint3 cellMaterials, uint3 cellMaterial) -{ - cellMaterials = (cellMaterials & ~XPrevMatMask) | ((cellMaterial << XPrevMatMaskShift) & XPrevMatMask); -} -inline uint3 getXPrevMaterial(uint3 cellMaterials) -{ - return (cellMaterials & XPrevMatMask) >> XPrevMatMaskShift; -} -inline void setXNextMaterial(inout uint3 cellMaterials, uint3 cellMaterial) -{ - cellMaterials = (cellMaterials & ~XNextMatMask) | ((cellMaterial << XNextMatMaskShift) & XNextMatMask); -} -inline uint3 getXNextMaterial(uint3 cellMaterials) -{ - return (cellMaterials & XNextMatMask) >> XNextMatMaskShift; -} -inline void setYPrevMaterial(inout uint3 cellMaterials, uint3 cellMaterial) -{ - cellMaterials = (cellMaterials & ~YPrevMatMask) | ((cellMaterial << YPrevMatMaskShift) & YPrevMatMask); -} -inline uint3 getYPrevMaterial(uint3 cellMaterials) -{ - return (cellMaterials & YPrevMatMask) >> YPrevMatMaskShift; -} -inline void setYNextMaterial(inout uint3 cellMaterials, uint3 cellMaterial) -{ - cellMaterials = (cellMaterials & ~YNextMatMask) | ((cellMaterial << YNextMatMaskShift) & YNextMatMask); -} -inline uint3 getYNextMaterial(uint3 cellMaterials) -{ - return (cellMaterials & YNextMatMask) >> YNextMatMaskShift; -} -inline void setZPrevMaterial(inout uint3 cellMaterials, uint3 cellMaterial) -{ - cellMaterials = (cellMaterials & ~ZPrevMatMask) | ((cellMaterial << ZPrevMatMaskShift) & ZPrevMatMask); -} -inline uint3 getZPrevMaterial(uint3 cellMaterials) -{ - return (cellMaterials & ZPrevMatMask) >> ZPrevMatMaskShift; -} -inline void setZNextMaterial(inout uint3 cellMaterials, uint3 cellMaterial) -{ - cellMaterials = (cellMaterials & ~ZNextMatMask) | ((cellMaterial << ZNextMatMaskShift) & ZNextMatMask); -} -inline uint3 getZNextMaterial(uint3 cellMaterials) -{ - return (cellMaterials & ZNextMatMask) >> ZNextMatMaskShift; -} - - -inline bool isSolidCell(uint cellMaterial) -{ - return cellMaterial == CM_SOLID; -} -inline bool isFluidCell(uint cellMaterial) -{ - return cellMaterial == CM_FLUID; -} -inline bool isAirCell(uint cellMaterial) -{ - return cellMaterial == CM_AIR; -} - -inline bool3 isSolidCell(uint3 cellMaterial) -{ - return cellMaterial == (uint3)CM_SOLID; -} -inline bool3 isFluidCell(uint3 cellMaterial) -{ - return cellMaterial == (uint3)CM_FLUID; -} -inline bool3 isAirCell(uint3 cellMaterial) -{ - return cellMaterial == (uint3)CM_AIR; -} - -void enforceBoundaryCondition(inout float3 velocity, uint cellMaterial) -{ - bool3 is_solid_cell = - or((bool3)isSolidCell(getCellMaterial(cellMaterial)), - bool3(isSolidCell(getXPrevMaterial(cellMaterial)), isSolidCell(getYPrevMaterial(cellMaterial)), isSolidCell(getZPrevMaterial(cellMaterial)))); - velocity = select(is_solid_cell, 0.0f, velocity); -} - -// handling solid obstacles -inline bool3 isSolidCell(float3 position) -{ - // no obstacles for now, in cuboid sim area - return false; -} - -#endif -#endif \ No newline at end of file diff --git a/70_FLIPFluids/app_resources/common.hlsl b/70_FLIPFluids/app_resources/common.hlsl deleted file mode 100644 index 7a310ae85..000000000 --- a/70_FLIPFluids/app_resources/common.hlsl +++ /dev/null @@ -1,34 +0,0 @@ -#ifndef _FLIP_EXAMPLE_COMMON_HLSL -#define _FLIP_EXAMPLE_COMMON_HLSL - -#include "nbl/builtin/hlsl/cpp_compat.hlsl" - -// TODO: Increase the `WorkgroupSize` to 512 -NBL_CONSTEXPR uint32_t WorkgroupSize = 128; -// TODO: Use "virtual workgroups" and Morton Code to map the 512 linear above to 8x8x8 -NBL_CONSTEXPR uint32_t WorkgroupGridDim = 8; -NBL_CONSTEXPR float ratioFLIPPIC = 0.95; -NBL_CONSTEXPR float deltaTime = 1.0f / 90.0f; -NBL_CONSTEXPR float gravity = 15.0f; - -#ifdef __HLSL_VERSION -// TODO: don't share this struct with C++ -struct Particle -{ - float32_t3 position; - float32_t3 velocity; -}; - -// TODO: after trimming this should fit in push constants -struct SMVPParams -{ - float4 camPos; // TODO: make it a `float32_t3` - - float4x4 MVP; - float4x4 M; // TODO: remove - float4x4 V; // TODO: only one `float32_t3` is needed out of the view matrix - float4x4 P; // TODO: remove -}; -#endif - -#endif diff --git a/70_FLIPFluids/app_resources/compute/advectParticles.comp.hlsl b/70_FLIPFluids/app_resources/compute/advectParticles.comp.hlsl deleted file mode 100644 index 64e94f262..000000000 --- a/70_FLIPFluids/app_resources/compute/advectParticles.comp.hlsl +++ /dev/null @@ -1,63 +0,0 @@ -#include "../common.hlsl" -#include "../gridSampling.hlsl" -#include "../descriptor_bindings.hlsl" - -struct SPushConstants -{ - uint64_t particlePosAddress; - uint64_t particleVelAddress; -}; - -[[vk::push_constant]] SPushConstants pc; - -[[vk::binding(b_apGridData, s_ap)]] -cbuffer GridData -{ - SGridData gridData; -}; - - -[[vk::binding(b_apVelField, s_ap)]] Texture3D velocityField[3]; -[[vk::binding(b_apPrevVelField, s_ap)]] Texture3D prevVelocityField[3]; -[[vk::binding(b_apVelSampler, s_ap)]] SamplerState velocityFieldSampler; - -#include "nbl/builtin/hlsl/bda/__ptr.hlsl" -using namespace nbl::hlsl; - -// TODO: delta time push constant? (but then for CI need a commandline `-fixed-timestep=MS` and `-frames=N` option too) -[numthreads(WorkgroupSize, 1, 1)] -[shader("compute")] -void main(uint32_t3 ID : SV_DispatchThreadID) -{ - uint32_t pid = ID.x; - Particle p; - - // use a restrict reference for speed - bda::__ref rPosition = (bda::__ptr::create(pc.particlePosAddress)+pid).deref<4,true>(); - bda::__ref rVelocity = (bda::__ptr::create(pc.particleVelAddress)+pid).deref<4,true>(); - p.position = rPosition.load(); - p.velocity = rVelocity.load(); - - // advect velocity - float3 gridPrevVel = sampleVelocityAt(p.position, prevVelocityField, velocityFieldSampler, gridData); - float3 gridVel = sampleVelocityAt(p.position, velocityField, velocityFieldSampler, gridData); - - float3 picVel = gridVel; - float3 flipVel = p.velocity + gridVel - gridPrevVel; - - p.velocity = lerp(picVel, flipVel, ratioFLIPPIC); - - // move particle, use RK4 - float3 k1 = gridVel; - float3 k2 = sampleVelocityAt(p.position + k1 * 0.5f * deltaTime, velocityField, velocityFieldSampler, gridData); - float3 k3 = sampleVelocityAt(p.position + k2 * 0.5f * deltaTime, velocityField, velocityFieldSampler, gridData); - float3 k4 = sampleVelocityAt(p.position + k3 * deltaTime, velocityField, velocityFieldSampler, gridData); - float3 velocity = (k1 + 2.0f * k2 + 2.0f * k3 + k4) / 6.0f; - - p.position += velocity * deltaTime; - - p.position = clampPosition(p.position, gridData.worldMin, gridData.worldMax); - - rPosition.store(p.position); - rVelocity.store(p.velocity); -} diff --git a/70_FLIPFluids/app_resources/compute/applyBodyForces.comp.hlsl b/70_FLIPFluids/app_resources/compute/applyBodyForces.comp.hlsl deleted file mode 100644 index b2c1e0b3f..000000000 --- a/70_FLIPFluids/app_resources/compute/applyBodyForces.comp.hlsl +++ /dev/null @@ -1,35 +0,0 @@ -#include "../common.hlsl" -#include "../gridUtils.hlsl" -#include "../cellUtils.hlsl" -#include "../descriptor_bindings.hlsl" - -[[vk::binding(b_abfGridData, s_abf)]] -cbuffer GridData -{ - SGridData gridData; -}; - -[[vk::binding(b_abfVelField, s_abf)]] RWTexture3D velocityField[3]; -[[vk::binding(b_abfCM, s_abf)]] RWTexture3D cellMaterialGrid; - -// TODO: can this kernel be fused with any preceeding/succeeding it? -[numthreads(WorkgroupGridDim, WorkgroupGridDim, WorkgroupGridDim)] -[shader("compute")] -void main(uint32_t3 ID : SV_DispatchThreadID) -{ - // only gravity for now - int3 cIdx = ID; - - float3 velocity; - velocity.x = velocityField[0][cIdx]; - velocity.y = velocityField[1][cIdx]; - velocity.z = velocityField[2][cIdx]; - - velocity += float3(0, -1, 0) * gravity * deltaTime; - - enforceBoundaryCondition(velocity, cellMaterialGrid[cIdx]); - - velocityField[0][cIdx] = velocity.x; - velocityField[1][cIdx] = velocity.y; - velocityField[2][cIdx] = velocity.z; -} diff --git a/70_FLIPFluids/app_resources/compute/diffusion.comp.hlsl b/70_FLIPFluids/app_resources/compute/diffusion.comp.hlsl deleted file mode 100644 index 288b82764..000000000 --- a/70_FLIPFluids/app_resources/compute/diffusion.comp.hlsl +++ /dev/null @@ -1,261 +0,0 @@ -#include "../common.hlsl" -#include "../gridUtils.hlsl" -#include "../cellUtils.hlsl" -#include "../descriptor_bindings.hlsl" - -#include "nbl/builtin/hlsl/glsl_compat/core.hlsl" - -struct SPushConstants // TODO: each Push Constant struct should be HLSL/C++ shared and called `S[DispatchName]PushConstants` -{ - // DOCS DOCS and SEMANTICALLY USEFUL NAMES - float32_t4 diffusionParameters; -}; - -[[vk::push_constant]] SPushConstants pc; - -[[vk::binding(b_dGridData, s_d)]] -cbuffer GridData -{ - SGridData gridData; -}; - -[[vk::binding(b_dCM, s_d)]] RWTexture3D cellMaterialGrid; -[[vk::binding(b_dVel, s_d)]] RWTexture3D velocityField[3]; -// THESE ARE HORRIBLY INEFFICIENT DATA STORAGE (each axis cell is 14 bits, gives 42 for 3 axes, not 128) -// TODO: there's no need for these to be stored at all, can be worked out from `cellMaterial` entirely in shared memory! Data is only needed during diffusion step! -// TODO: investigate using a staggered grid, there's something fishy about each axis needing a full XYZ cell! -[[vk::binding(b_dAxisIn, s_d)]] RWTexture3D axisCellMaterialIn; -[[vk::binding(b_dAxisOut, s_d)]] RWTexture3D axisCellMaterialOut; -[[vk::binding(b_dDiff, s_d)]] RWTexture3D gridDiffusion; - -// TODO: THOU SHALT NOT USE INTEGER DIVISION AND MODULO IN SHADERS! To stop using `flatIdxToLocalGridID`, the shared arrays need to be flat. -// TODO: `vector<>` types should not be used in `groupshared` because they cause bank conflicts, they should be laid out `SoA` instead (component has largest stride) -groupshared uint16_t3 sAxisCellMat[14][14][14]; // TODO: `uint16_t` per axis is too much -groupshared float16_t3 sDiffusion[14][14][14]; - -[numthreads(WorkgroupGridDim, WorkgroupGridDim, WorkgroupGridDim)] -[shader("compute")] -void setAxisCellMaterial(uint32_t3 ID : SV_DispatchThreadID) -{ - int3 cellIdx = ID; - - uint cellMaterial = cellMaterialGrid[cellIdx]; - - uint this_cm = getCellMaterial(cellMaterial); - uint xp_cm = getXPrevMaterial(cellMaterial); - uint yp_cm = getYPrevMaterial(cellMaterial); - uint zp_cm = getZPrevMaterial(cellMaterial); - - uint3 cellAxisType; - cellAxisType.x = - isSolidCell(this_cm) || isSolidCell(xp_cm) ? CM_SOLID : - isFluidCell(this_cm) || isFluidCell(xp_cm) ? CM_FLUID : - CM_AIR; - cellAxisType.y = - isSolidCell(this_cm) || isSolidCell(yp_cm) ? CM_SOLID : - isFluidCell(this_cm) || isFluidCell(yp_cm) ? CM_FLUID : - CM_AIR; - cellAxisType.z = - isSolidCell(this_cm) || isSolidCell(zp_cm) ? CM_SOLID : - isFluidCell(this_cm) || isFluidCell(zp_cm) ? CM_FLUID : - CM_AIR; - - uint3 cmAxisTypes = 0; - setCellMaterial(cmAxisTypes, cellAxisType); - - axisCellMaterialOut[cellIdx].xyz = cmAxisTypes; -} - -[numthreads(WorkgroupGridDim, WorkgroupGridDim, WorkgroupGridDim)] -[shader("compute")] -void setNeighborAxisCellMaterial(uint32_t3 ID : SV_DispatchThreadID) -{ - int3 cellIdx = ID; - - uint3 axisCm = (uint3)0; - uint3 this_axiscm = getCellMaterial(axisCellMaterialIn[cellIdx].xyz); - setCellMaterial(axisCm, this_axiscm); - - uint3 xp_axiscm = cellIdx.x == 0 ? (uint3)CM_SOLID : getCellMaterial(axisCellMaterialIn[clampToGrid(cellIdx + int3(-1, 0, 0), gridData.gridSize)].xyz); - setXPrevMaterial(axisCm, xp_axiscm); - - uint3 xn_axiscm = cellIdx.x == gridData.gridSize.x - 1 ? (uint3)CM_SOLID : getCellMaterial(axisCellMaterialIn[clampToGrid(cellIdx + int3(1, 0, 0), gridData.gridSize)].xyz); - setXNextMaterial(axisCm, xn_axiscm); - - uint3 yp_axiscm = cellIdx.y == 0 ? (uint3)CM_SOLID : getCellMaterial(axisCellMaterialIn[clampToGrid(cellIdx + int3(0, -1, 0), gridData.gridSize)].xyz); - setYPrevMaterial(axisCm, yp_axiscm); - - uint3 yn_axiscm = cellIdx.y == gridData.gridSize.y - 1 ? (uint3)CM_SOLID : getCellMaterial(axisCellMaterialIn[clampToGrid(cellIdx + int3(0, 1, 0), gridData.gridSize)].xyz); - setYNextMaterial(axisCm, yn_axiscm); - - uint3 zp_axiscm = cellIdx.z == 0 ? (uint3)CM_SOLID : getCellMaterial(axisCellMaterialIn[clampToGrid(cellIdx + int3(0, 0, -1), gridData.gridSize)].xyz); - setZPrevMaterial(axisCm, zp_axiscm); - - uint3 zn_axiscm = cellIdx.z == gridData.gridSize.z - 1 ? (uint3)CM_SOLID : getCellMaterial(axisCellMaterialIn[clampToGrid(cellIdx + int3(0, 0, 1), gridData.gridSize)].xyz); - setZNextMaterial(axisCm, zn_axiscm); - - axisCellMaterialOut[cellIdx].xyz = axisCm; -} - -float3 calculateDiffusionVelStep(int3 idx, float3 sampledVelocity, uint cellMaterial) -{ - float3 velocity = (float3)0; - uint3 axisCm = uint3(sAxisCellMat[idx.x][idx.y][idx.z]); - - float3 diff = float3(sDiffusion[idx.x][idx.y][idx.z]); - float3 xp = select(isFluidCell(getXPrevMaterial(axisCm)), float3(sDiffusion[idx.x - 1][idx.y][idx.z]), diff); - velocity += pc.diffusionParameters.x * xp; - - float3 xn = select(isFluidCell(getXNextMaterial(axisCm)), float3(sDiffusion[idx.x + 1][idx.y][idx.z]), diff); - velocity += pc.diffusionParameters.x * xn; - - float3 yp = select(isFluidCell(getYPrevMaterial(axisCm)), float3(sDiffusion[idx.x][idx.y - 1][idx.z]), diff); - velocity += pc.diffusionParameters.y * yp; - - float3 yn = select(isFluidCell(getYNextMaterial(axisCm)), float3(sDiffusion[idx.x][idx.y + 1][idx.z]), diff); - velocity += pc.diffusionParameters.y * yn; - - float3 zp = select(isFluidCell(getZPrevMaterial(axisCm)), float3(sDiffusion[idx.x][idx.y][idx.z - 1]), diff); - velocity += pc.diffusionParameters.z * zp; - - float3 zn = select(isFluidCell(getZNextMaterial(axisCm)), float3(sDiffusion[idx.x][idx.y][idx.z + 1]), diff); - velocity += pc.diffusionParameters.z * zn; - - velocity += pc.diffusionParameters.w * sampledVelocity; - enforceBoundaryCondition(velocity, cellMaterial); - - return velocity; -} - -[numthreads(WorkgroupGridDim, WorkgroupGridDim, WorkgroupGridDim)] -[shader("compute")] -void iterateDiffusion(uint32_t3 ID : SV_DispatchThreadID) -{ - uint3 gid = nbl::hlsl::glsl::gl_WorkGroupID(); - int3 cellIdx = ID; - - float3 sampledVel; - sampledVel.x = velocityField[0][cellIdx]; - sampledVel.y = velocityField[1][cellIdx]; - sampledVel.z = velocityField[2][cellIdx]; - - uint cellMaterial = cellMaterialGrid[cellIdx]; - - // load shared mem - for (uint virtualIdx = nbl::hlsl::glsl::gl_LocalInvocationIndex(); - virtualIdx < 14 * 14 * 14; virtualIdx += 8 * 8 * 8) - { - // TODO: THOU SHALT NOT USE INTEGER DIVISION AND MODULO IN SHADERS! STOP USING `flatIdxToLocalGridID` - int3 lid = flatIdxToLocalGridID(virtualIdx, 14); - - int3 cellIdx = clampToGrid(lid + int3(-3, -3, -3) + gid * WorkgroupGridDim, gridData.gridSize); - sAxisCellMat[lid.x][lid.y][lid.z] = uint16_t3(axisCellMaterialIn[cellIdx].xyz); - sDiffusion[lid.x][lid.y][lid.z] = float16_t3(gridDiffusion[cellIdx].xyz); - } - GroupMemoryBarrierWithGroupSync(); - - // TODO: undo the unroll, use two nested for loops - - // do 12x12x12 iteration - float3 tmp[6]; - uint i = 0; - for (uint virtualIdx = nbl::hlsl::glsl::gl_LocalInvocationIndex(); - virtualIdx < 12 * 12 * 12; virtualIdx += 8 * 8 * 8) - { - // TODO: THOU SHALT NOT USE INTEGER DIVISION AND MODULO IN SHADERS! STOP USING `flatIdxToLocalGridID` - int3 lid = flatIdxToLocalGridID(virtualIdx, 12); - lid += int3(1, 1, 1); - - float3 velocity = calculateDiffusionVelStep(lid, sampledVel, cellMaterial); - tmp[i++] = velocity; - } - GroupMemoryBarrierWithGroupSync(); - - i = 0; - for (uint virtualIdx = nbl::hlsl::glsl::gl_LocalInvocationIndex(); - virtualIdx < 12 * 12 * 12; virtualIdx += 8 * 8 * 8) - { - int3 lid = flatIdxToLocalGridID(virtualIdx, 12); - lid += int3(1, 1, 1); - sDiffusion[lid.x][lid.y][lid.z] = float16_t3(tmp[i++]); - } - GroupMemoryBarrierWithGroupSync(); - - // do 10x10x10 iteration - i = 0; - for (uint virtualIdx = nbl::hlsl::glsl::gl_LocalInvocationIndex(); - virtualIdx < 10 * 10 * 10; virtualIdx += 8 * 8 * 8) - { - int3 lid = flatIdxToLocalGridID(virtualIdx, 10); - lid += int3(2, 2, 2); - - float3 velocity = calculateDiffusionVelStep(lid, sampledVel, cellMaterial); - tmp[i++] = velocity; - } - GroupMemoryBarrierWithGroupSync(); - - i = 0; - for (uint virtualIdx = nbl::hlsl::glsl::gl_LocalInvocationIndex(); - virtualIdx < 10 * 10 * 10; virtualIdx += 8 * 8 * 8) - { - int3 lid = flatIdxToLocalGridID(virtualIdx, 10); - lid += int3(2, 2, 2); - sDiffusion[lid.x][lid.y][lid.z] = float16_t3(tmp[i++]); - } - GroupMemoryBarrierWithGroupSync(); - - // do 8x8x8 iteration (final) and write - int3 lid = nbl::hlsl::glsl::gl_LocalInvocationID(); - lid += int3(3, 3, 3); - - float3 velocity = calculateDiffusionVelStep(lid, sampledVel, cellMaterial); - gridDiffusion[cellIdx].xyz = velocity; -} - -// TODO: same as the pressure solver, this kernel/dispatch should be fused onto `iterateDiffusion` guarded by `isLastIteration` push constant -[numthreads(WorkgroupGridDim, WorkgroupGridDim, WorkgroupGridDim)] -[shader("compute")] -void applyDiffusion(uint32_t3 ID : SV_DispatchThreadID) -{ - int3 cellIdx = ID; - - uint3 axisCm = axisCellMaterialIn[cellIdx].xyz; - float3 velocity = (float3)0; - - float3 diff = gridDiffusion[cellIdx].xyz; - float3 xp_diff = select(isFluidCell(getXPrevMaterial(axisCm)), - gridDiffusion[clampToGrid(cellIdx + int3(-1, 0, 0), gridData.gridSize)].xyz, diff); - velocity += pc.diffusionParameters.x * xp_diff; - - float3 xn_diff = select(isFluidCell(getXNextMaterial(axisCm)), - gridDiffusion[clampToGrid(cellIdx + int3(1, 0, 0), gridData.gridSize)].xyz, diff); - velocity += pc.diffusionParameters.x * xn_diff; - - float3 yp_diff = select(isFluidCell(getYPrevMaterial(axisCm)), - gridDiffusion[clampToGrid(cellIdx + int3(0, -1, 0), gridData.gridSize)].xyz, diff); - velocity += pc.diffusionParameters.y * yp_diff; - - float3 yn_diff = select(isFluidCell(getYNextMaterial(axisCm)), - gridDiffusion[clampToGrid(cellIdx + int3(0, 1, 0), gridData.gridSize)].xyz, diff); - velocity += pc.diffusionParameters.y * yn_diff; - - float3 zp_diff = select(isFluidCell(getZPrevMaterial(axisCm)), - gridDiffusion[clampToGrid(cellIdx + int3(0, 0, -1), gridData.gridSize)].xyz, diff); - velocity += pc.diffusionParameters.z * zp_diff; - - float3 zn_diff = select(isFluidCell(getZNextMaterial(axisCm)), - gridDiffusion[clampToGrid(cellIdx + int3(0, 0, 1), gridData.gridSize)].xyz, diff); - velocity += pc.diffusionParameters.z * zn_diff; - - float3 sampledVel; - sampledVel.x = velocityField[0][cellIdx]; - sampledVel.y = velocityField[1][cellIdx]; - sampledVel.z = velocityField[2][cellIdx]; - velocity += pc.diffusionParameters.w * sampledVel; - - enforceBoundaryCondition(velocity, cellMaterialGrid[cellIdx]); - - velocityField[0][cellIdx] = velocity.x; - velocityField[1][cellIdx] = velocity.y; - velocityField[2][cellIdx] = velocity.z; -} diff --git a/70_FLIPFluids/app_resources/compute/genParticleVertices.comp.hlsl b/70_FLIPFluids/app_resources/compute/genParticleVertices.comp.hlsl deleted file mode 100644 index 4c4a76690..000000000 --- a/70_FLIPFluids/app_resources/compute/genParticleVertices.comp.hlsl +++ /dev/null @@ -1,121 +0,0 @@ -#pragma shader_stage(compute) - -#include "../common.hlsl" -#include "../render_common.hlsl" -#include "../descriptor_bindings.hlsl" - - -// TODO: this whole shader needs to disappear! -// One can just compute `offset` on line 61 from `gl_InstanceIndex` and `i` from `gl_VertexIndex&0b11u` -// Sure it leads to redundant computation of everything before the `for` loop but no bandwidth consumed and no extra compute dispatch -struct SPushConstants -{ - uint64_t particlePosAddress; - uint64_t particleVelAddress; - uint64_t particleVerticesAddress; -}; - -[[vk::push_constant]] SPushConstants pc; - -[[vk::binding(b_gpvCamData, s_gpv)]] -cbuffer CameraData -{ - SMVPParams camParams; -}; - -[[vk::binding(b_gpvPParams, s_gpv)]] -cbuffer ParticleParams -{ - SParticleRenderParams pParams; -}; - - -static const uint vertexOrder[6] = {0, 1, 2, 2, 1, 3}; - -// static const float4 quadVertices[4] = { -// float4(-1, -1, 0, 1), -// float4(1, -1, 0, 1), -// float4(-1, 1, 0, 1), -// float4(1, 1, 0, 1) -// }; - -static const float4 quadVertices[4] = { - float4(-0.5f, -0.5f, 0.0f, 1.0f), - float4(0.5f, -0.5f, 0.0f, 1.0f), - float4(-0.5f, 0.5f, 0.0f, 1.0f), - float4(0.5f, 0.5f, 0.0f, 1.0f), -}; - -static const float2 quadUVs[4] = { - float2(0, 0), - float2(1, 0), - float2(0, 1), - float2(1, 1) -}; - -#include "nbl/builtin/hlsl/bda/__ptr.hlsl" -using namespace nbl::hlsl; - -[numthreads(WorkgroupSize, 1, 1)] -[shader("compute")] -void main(uint32_t3 ID : SV_DispatchThreadID) -{ - uint32_t pid = ID.x; - Particle p; - - int offset = sizeof(float32_t3) * pid; - p.position = vk::RawBufferLoad(pc.particlePosAddress + offset); - p.velocity = vk::RawBufferLoad(pc.particleVelAddress + offset); - - uint32_t quadBeginIdx = pid * 6; - - float3 wsSpherePos = p.position; - float radius = pParams.radius; - - float3 wsCamPos = camParams.camPos.xyz; - float3 viewDir = wsCamPos - wsSpherePos; - float dist = length(viewDir); - - float3 z = normalize(-viewDir); - float3 x = normalize(cross(camParams.V._m10_m11_m12, z)); - float3 y = normalize(cross(z, x)); - - float scaledRadius = dist * radius / sqrt(dist * dist - radius * radius); - - const float4 color1 = float4(0, 0.35, 0.75, 1); - const float4 color2 = float4(0.85, 0.2, 0, 1); - float speed = length(p.velocity); - float factor = saturate((speed - 0.5) / 10.0); - float4 color = lerp(color1, color2, factor); - - float4x4 mat = 0; - mat._m00_m10_m20 = x; - mat._m01_m11_m21 = y; - mat._m02_m12_m22 = z; - mat._m00_m10_m20 *= scaledRadius; - mat._m01_m11_m21 *= scaledRadius; - mat._m03_m13_m23 = wsSpherePos; - mat._m33 = 1; - - float vertScale = (dist - radius) / dist; - mat._m00_m10_m20 *= vertScale; - mat._m01_m11_m21 *= vertScale; - mat._m03_m13_m23 += viewDir * radius / dist; - - for (uint i = 0; i < 6; i++) - { - VertexInfo vertex; - - vertex.radius = radius; - - vertex.vsSpherePos = float4(mul(camParams.V, float4(wsSpherePos, 1)).xyz, 1); - - vertex.position = mul(camParams.MVP, float4(mul(mat, quadVertices[vertexOrder[i]]).xyz, 1)); - - vertex.color = color; - - vertex.uv = quadUVs[vertexOrder[i]]; - - (bda::__ptr::create(pc.particleVerticesAddress)+(quadBeginIdx+i)).deref_restrict().store(vertex); - } -} diff --git a/70_FLIPFluids/app_resources/compute/particlesInit.comp.hlsl b/70_FLIPFluids/app_resources/compute/particlesInit.comp.hlsl deleted file mode 100644 index 27bf4366f..000000000 --- a/70_FLIPFluids/app_resources/compute/particlesInit.comp.hlsl +++ /dev/null @@ -1,39 +0,0 @@ -#include "../common.hlsl" -#include "../gridUtils.hlsl" -#include "../descriptor_bindings.hlsl" - -struct SPushConstants -{ - uint64_t particlePosAddress; - uint64_t particleVelAddress; -}; - -[[vk::push_constant]] SPushConstants pc; - -[[vk::binding(b_piGridData, s_pi)]] -cbuffer GridData -{ - SGridData gridData; -}; - -[numthreads(WorkgroupSize, 1, 1)] -[shader("compute")] -void main(uint32_t3 ID : SV_DispatchThreadID) -{ - uint32_t pid = ID.x; - - Particle p; - - int x = pid % (gridData.particleInitSize.x * 2); - int y = pid / (gridData.particleInitSize.x * 2) % (gridData.particleInitSize.y * 2); - int z = pid / ((gridData.particleInitSize.x * 2) * (gridData.particleInitSize.y * 2)); - float3 position = gridPosToWorldPos(gridData.particleInitMin.xyz + 0.25f + float3(x, y, z) * 0.5f, gridData); - position = clampPosition(position, gridData.worldMin, gridData.worldMax); - - p.position = position; - p.velocity = (float3)0; - - int offset = sizeof(float32_t3) * pid; - vk::RawBufferStore(pc.particlePosAddress + offset, p.position); - vk::RawBufferStore(pc.particleVelAddress + offset, p.velocity); -} diff --git a/70_FLIPFluids/app_resources/compute/prepareCellUpdate.comp.hlsl b/70_FLIPFluids/app_resources/compute/prepareCellUpdate.comp.hlsl deleted file mode 100644 index 157da5bb8..000000000 --- a/70_FLIPFluids/app_resources/compute/prepareCellUpdate.comp.hlsl +++ /dev/null @@ -1,91 +0,0 @@ -#include "../common.hlsl" -#include "../gridUtils.hlsl" -#include "../descriptor_bindings.hlsl" - -struct SPushConstants -{ - uint64_t particlePosAddress; - uint64_t particleVelAddress; -}; - -[[vk::push_constant]] SPushConstants pc; - -[[vk::binding(b_ufcGridData, s_ufc)]] -cbuffer GridData -{ - SGridData gridData; -}; - -[[vk::binding(b_ufcGridPCount, s_ufc)]] RWTexture3D gridParticleCount; - -[[vk::binding(b_ufcVel, s_ufc)]] RWTexture3D velocityField[3]; -[[vk::binding(b_ufcPrevVel, s_ufc)]] RWTexture3D prevVelocityField[3]; - -// TODO: use Atomic floats if JIT Device Traits say they exist -void casAdd(RWTexture3D grid, int3 idx, float value) -{ - uint actualValue = 0; - uint expectedValue; - do - { - expectedValue = actualValue; - uint newValue = asuint(asfloat(actualValue) + value); - InterlockedCompareExchange(grid[idx], expectedValue, newValue, actualValue); - } while (actualValue != expectedValue); -} - -float getWeight(float3 pPos, float3 cPos, float invSpacing) -{ - float3 dist = abs((pPos - cPos) * invSpacing); - float3 weight = saturate(1.0f - dist); - return weight.x * weight.y * weight.z; -} - -[numthreads(WorkgroupSize, 1, 1)] -[shader("compute")] -void main(uint32_t3 ID : SV_DispatchThreadID) -{ - uint pid = ID.x; - Particle p; - - int offset = sizeof(float32_t3) * pid; - p.position = vk::RawBufferLoad(pc.particlePosAddress + offset); - p.velocity = vk::RawBufferLoad(pc.particleVelAddress + offset); - - int3 cIdx = worldPosToCellIdx(p.position, gridData); - - for (int i = max(cIdx.x - 1, 0); i <= min(cIdx.x + 1, gridData.gridSize.x - 1); i++) - { - for (int j = max(cIdx.y - 1, 0); j <= min(cIdx.y + 1, gridData.gridSize.y - 1); j++) - { - for (int k = max(cIdx.z - 1, 0); k <= min(cIdx.z + 1, gridData.gridSize.z - 1); k++) - { - int3 cellIdx = int3(i, j, k); - float3 position = cellIdxToWorldPos(cellIdx, gridData); - float3 posvx = position + float3(-0.5f * gridData.gridCellSize, 0.0f, 0.0f); - float3 posvy = position + float3(0.0f, -0.5f * gridData.gridCellSize, 0.0f); - float3 posvz = position + float3(0.0f, 0.0f, -0.5f * gridData.gridCellSize); - - float3 weight; - weight.x = getWeight(p.position, posvx, gridData.gridInvCellSize); - weight.y = getWeight(p.position, posvy, gridData.gridInvCellSize); - weight.z = getWeight(p.position, posvz, gridData.gridInvCellSize); - - float3 velocity = weight * p.velocity; - - // store weighted velocity in velocity buffer - casAdd(velocityField[0], cellIdx, velocity.x); - casAdd(velocityField[1], cellIdx, velocity.y); - casAdd(velocityField[2], cellIdx, velocity.z); - - // store total weight in prev velocity buffer - casAdd(prevVelocityField[0], cellIdx, weight.x); - casAdd(prevVelocityField[1], cellIdx, weight.y); - casAdd(prevVelocityField[2], cellIdx, weight.z); - } - } - } - - // TODO: no sorting anymore, can just move to performing atomic OR on the cellMaterial image directly - InterlockedAdd(gridParticleCount[cIdx], 1); -} diff --git a/70_FLIPFluids/app_resources/compute/pressureSolver.comp.hlsl b/70_FLIPFluids/app_resources/compute/pressureSolver.comp.hlsl deleted file mode 100644 index e71f05912..000000000 --- a/70_FLIPFluids/app_resources/compute/pressureSolver.comp.hlsl +++ /dev/null @@ -1,202 +0,0 @@ -#include "../common.hlsl" -#include "../gridUtils.hlsl" -#include "../cellUtils.hlsl" -#include "../descriptor_bindings.hlsl" - -#include "nbl/builtin/hlsl/glsl_compat/core.hlsl" - -struct SPressureSolverParams -{ - // TODO: DOCS and Semantically sound names! - float4 coeff1; - float4 coeff2; // W component is unused -}; - -[[vk::binding(b_psGridData, s_ps)]] -cbuffer GridData -{ - SGridData gridData; -}; - -[[vk::binding(b_psParams, s_ps)]] -cbuffer PressureSolverParams -{ - SPressureSolverParams params; -}; - -[[vk::binding(b_psCM, s_ps)]] RWTexture3D cellMaterialGrid; -[[vk::binding(b_psVel, s_ps)]] RWTexture3D velocityField[3]; -[[vk::binding(b_psDiv, s_ps)]] RWTexture3D divergenceGrid; -[[vk::binding(b_psPres, s_ps)]] RWTexture3D pressureGrid; - -// TODO: make the shared memory arrays, flat arrays so you don't need to call `flatIdxToLocalGridID` !!!!! INTEGER DIVISION! -// TODO: full 32 bits are not needed to store the cell materials, could be stored in the LSB bits of the pressure and divergence -groupshared uint sCellMat[14][14][14]; -groupshared float sDivergence[14][14][14]; -groupshared float sPressure[14][14][14]; - -[numthreads(WorkgroupGridDim, WorkgroupGridDim, WorkgroupGridDim)] -[shader("compute")] -void calculateNegativeDivergence(uint32_t3 ID : SV_DispatchThreadID) -{ - int3 cellIdx = ID; - - float3 param = (float3)gridData.gridInvCellSize; - float3 velocity; - velocity.x = velocityField[0][cellIdx]; - velocity.y = velocityField[1][cellIdx]; - velocity.z = velocityField[2][cellIdx]; - - float divergence = 0; - if (isFluidCell(getCellMaterial(cellMaterialGrid[cellIdx]))) - { - int3 cell_xn = cellIdx + int3(1, 0, 0); - divergence += param.x * ((cell_xn.x < gridData.gridSize.x ? velocityField[0][cell_xn] : 0.0f) - velocity.x); - - int3 cell_yn = cellIdx + int3(0, 1, 0); - divergence += param.y * ((cell_yn.y < gridData.gridSize.y ? velocityField[1][cell_yn] : 0.0f) - velocity.y); - - int3 cell_zn = cellIdx + int3(0, 0, 1); - divergence += param.z * ((cell_zn.z < gridData.gridSize.z ? velocityField[2][cell_zn] : 0.0f) - velocity.z); - } - - divergenceGrid[cellIdx] = divergence; -} - -float calculatePressureStep(int3 idx) -{ - float pressure = 0.0f; - uint cellMaterial = sCellMat[idx.x][idx.y][idx.z]; - - if (isFluidCell(getCellMaterial(cellMaterial))) - { - int3 xp = isSolidCell(getXPrevMaterial(cellMaterial)) ? idx : idx + int3(-1, 0, 0); - int3 xn = isSolidCell(getXNextMaterial(cellMaterial)) ? idx : idx + int3(1, 0, 0); - pressure += params.coeff1.x * (sPressure[xp.x][xp.y][xp.z] + sPressure[xn.x][xn.y][xn.z]); - - int3 yp = isSolidCell(getYPrevMaterial(cellMaterial)) ? idx : idx + int3(0, -1, 0); - int3 yn = isSolidCell(getYNextMaterial(cellMaterial)) ? idx : idx + int3(0, 1, 0); - pressure += params.coeff1.y * (sPressure[yp.x][yp.y][yp.z] + sPressure[yn.x][yn.y][yn.z]); - - int3 zp = isSolidCell(getZPrevMaterial(cellMaterial)) ? idx : idx + int3(0, 0, -1); - int3 zn = isSolidCell(getZNextMaterial(cellMaterial)) ? idx : idx + int3(0, 0, 1); - pressure += params.coeff1.z * (sPressure[zp.x][zp.y][zp.z] + sPressure[zn.x][zn.y][zn.z]); - - pressure += params.coeff1.w * sDivergence[idx.x][idx.y][idx.z]; - } - - return pressure; -} - -[numthreads(WorkgroupGridDim, WorkgroupGridDim, WorkgroupGridDim)] -[shader("compute")] -void iteratePressureSystem(uint32_t3 ID : SV_DispatchThreadID) -{ - uint3 gid = nbl::hlsl::glsl::gl_WorkGroupID(); - - // load shared mem - for (uint virtualIdx = nbl::hlsl::glsl::gl_LocalInvocationIndex(); - virtualIdx < 14 * 14 * 14; virtualIdx += 8 * 8 * 8) - { - // TODO: DO NOT USE THE `flatIdxToLocalGridID` FUNCTION! - int3 lid = flatIdxToLocalGridID(virtualIdx, 14); - - int3 cellIdx = clampToGrid(lid + int3(-3, -3, -3) + gid * WorkgroupGridDim, gridData.gridSize); - sCellMat[lid.x][lid.y][lid.z] = cellMaterialGrid[cellIdx]; - sDivergence[lid.x][lid.y][lid.z] = divergenceGrid[cellIdx]; - sPressure[lid.x][lid.y][lid.z] = pressureGrid[cellIdx]; - } - GroupMemoryBarrierWithGroupSync(); - - // TODO: Undo the unroll, write as two nested `for` (compiler will unroll them anyway) - - // do 12x12x12 iteration - float tmp[6]; - uint i = 0; - for (uint virtualIdx = nbl::hlsl::glsl::gl_LocalInvocationIndex(); - virtualIdx < 12 * 12 * 12; virtualIdx += 8 * 8 * 8) - { - int3 lid = flatIdxToLocalGridID(virtualIdx, 12); - lid += int3(1, 1, 1); - - float pressure = calculatePressureStep(lid); - - tmp[i++] = pressure; - } - GroupMemoryBarrierWithGroupSync(); - - i = 0; - for (uint virtualIdx = nbl::hlsl::glsl::gl_LocalInvocationIndex(); - virtualIdx < 12 * 12 * 12; virtualIdx += 8 * 8 * 8) - { - int3 lid = flatIdxToLocalGridID(virtualIdx, 12); - lid += int3(1, 1, 1); - sPressure[lid.x][lid.y][lid.z] = tmp[i++]; - } - GroupMemoryBarrierWithGroupSync(); - - // do 10x10x10 iteration - i = 0; - for (uint virtualIdx = nbl::hlsl::glsl::gl_LocalInvocationIndex(); - virtualIdx < 10 * 10 * 10; virtualIdx += 8 * 8 * 8) - { - int3 lid = flatIdxToLocalGridID(virtualIdx, 10); - lid += int3(2, 2, 2); - - float pressure = calculatePressureStep(lid); - - tmp[i++] = pressure; - } - GroupMemoryBarrierWithGroupSync(); - - i = 0; - for (uint virtualIdx = nbl::hlsl::glsl::gl_LocalInvocationIndex(); - virtualIdx < 10 * 10 * 10; virtualIdx += 8 * 8 * 8) - { - int3 lid = flatIdxToLocalGridID(virtualIdx, 10); - lid += int3(2, 2, 2); - sPressure[lid.x][lid.y][lid.z] = tmp[i++]; - } - GroupMemoryBarrierWithGroupSync(); - - // do 8x8x8 iteration (final) and write - int3 lid = nbl::hlsl::glsl::gl_LocalInvocationID(); - lid += int3(3, 3, 3); - - float pressure = calculatePressureStep(lid); - pressureGrid[ID] = pressure; -} - -// TODO: why doesn't the last invocation of `iteratePressureSystem` have this step fused into it!? It would be just a simple push constant `isLastIteration` that would decide whether to run this dispatch -[numthreads(WorkgroupGridDim, WorkgroupGridDim, WorkgroupGridDim)] -[shader("compute")] -void updateVelocities(uint32_t3 ID : SV_DispatchThreadID) -{ - int3 cellIdx = ID; - - uint cellMaterial = cellMaterialGrid[cellIdx]; - - float3 velocity; - velocity.x = velocityField[0][cellIdx]; - velocity.y = velocityField[1][cellIdx]; - velocity.z = velocityField[2][cellIdx]; - float pressure = pressureGrid[cellIdx]; - - int3 cell_xp = clampToGrid(cellIdx + int3(-1, 0, 0), gridData.gridSize); - cell_xp = isSolidCell(getXPrevMaterial(cellMaterial)) ? cellIdx : cell_xp; - velocity.x -= params.coeff2.x * (pressure - pressureGrid[cell_xp]); - - int3 cell_yp = clampToGrid(cellIdx + int3(0, -1, 0), gridData.gridSize); - cell_yp = isSolidCell(getYPrevMaterial(cellMaterial)) ? cellIdx : cell_yp; - velocity.y -= params.coeff2.y * (pressure - pressureGrid[cell_yp]); - - int3 cell_zp = clampToGrid(cellIdx + int3(0, 0, -1), gridData.gridSize); - cell_zp = isSolidCell(getZPrevMaterial(cellMaterial)) ? cellIdx : cell_zp; - velocity.z -= params.coeff2.z * (pressure - pressureGrid[cell_zp]); - - enforceBoundaryCondition(velocity, cellMaterial); - - velocityField[0][cellIdx] = velocity.x; - velocityField[1][cellIdx] = velocity.y; - velocityField[2][cellIdx] = velocity.z; -} diff --git a/70_FLIPFluids/app_resources/compute/updateFluidCells.comp.hlsl b/70_FLIPFluids/app_resources/compute/updateFluidCells.comp.hlsl deleted file mode 100644 index ea37660c1..000000000 --- a/70_FLIPFluids/app_resources/compute/updateFluidCells.comp.hlsl +++ /dev/null @@ -1,95 +0,0 @@ -#include "../common.hlsl" -#include "../gridUtils.hlsl" -#include "../cellUtils.hlsl" -#include "../descriptor_bindings.hlsl" - -#include "nbl/builtin/hlsl/limits.hlsl" - -[[vk::binding(b_ufcGridData, s_ufc)]] -cbuffer GridData -{ - SGridData gridData; -}; - -// TODO: image shouldn't exist, atomics should be performed directly on `cellMaterial` -[[vk::binding(b_ufcGridPCount, s_ufc)]] RWTexture3D gridParticleCount; - -// TODO: If 0 is AIR, and >=2 is SOLID, we can perform Atomic OR 0b01 to set FLUID, this means we only need to reset the `cellMaterial` with a bitwise-AND and only need one image copy -[[vk::binding(b_ufcCMIn, s_ufc)]] RWTexture3D cellMaterialIn; -[[vk::binding(b_ufcCMOut, s_ufc)]] RWTexture3D cellMaterialOut; - -[[vk::binding(b_ufcVel, s_ufc)]] RWTexture3D velocityField[3]; -[[vk::binding(b_ufcPrevVel, s_ufc)]] RWTexture3D prevVelocityField[3]; - -// TODO: f 0 is AIR, and >=2 is SOLID, we can perform Atomic OR 0b01 to have a particle set the cell to FLUID, and this dispatch looping over all grid cells is not needed! -[numthreads(WorkgroupGridDim, WorkgroupGridDim, WorkgroupGridDim)] -[shader("compute")] -void updateFluidCells(uint32_t3 ID : SV_DispatchThreadID) -{ - int3 cIdx = ID; - - uint count = gridParticleCount[cIdx]; - uint thisCellMaterial = - isSolidCell(cellIdxToWorldPos(cIdx, gridData)) ? CM_SOLID : - count > 0 ? CM_FLUID : - CM_AIR; - - uint cellMaterial = 0; - setCellMaterial(cellMaterial, thisCellMaterial); - - cellMaterialOut[cIdx] = cellMaterial; -} - -[numthreads(WorkgroupGridDim, WorkgroupGridDim, WorkgroupGridDim)] -[shader("compute")] -void updateNeighborFluidCells(uint32_t3 ID : SV_DispatchThreadID) -{ - int3 cIdx = ID; - - uint thisCellMaterial = getCellMaterial(cellMaterialIn[cIdx]); - uint cellMaterial = 0; - setCellMaterial(cellMaterial, thisCellMaterial); - - uint xpCm = cIdx.x == 0 ? CM_SOLID : getCellMaterial(cellMaterialIn[clampToGrid(cIdx + int3(-1, 0, 0), gridData.gridSize)]); - setXPrevMaterial(cellMaterial, xpCm); - - uint xnCm = cIdx.x == gridData.gridSize.x - 1 ? CM_SOLID : getCellMaterial(cellMaterialIn[clampToGrid(cIdx + int3(1, 0, 0), gridData.gridSize)]); - setXNextMaterial(cellMaterial, xnCm); - - uint ypCm = cIdx.y == 0 ? CM_SOLID : getCellMaterial(cellMaterialIn[clampToGrid(cIdx + int3(0, -1, 0), gridData.gridSize)]); - setYPrevMaterial(cellMaterial, ypCm); - - uint ynCm = cIdx.y == gridData.gridSize.y - 1 ? CM_SOLID : getCellMaterial(cellMaterialIn[clampToGrid(cIdx + int3(0, 1, 0), gridData.gridSize)]); - setYNextMaterial(cellMaterial, ynCm); - - uint zpCm = cIdx.z == 0 ? CM_SOLID : getCellMaterial(cellMaterialIn[clampToGrid(cIdx + int3(0, 0, -1), gridData.gridSize)]); - setZPrevMaterial(cellMaterial, zpCm); - - uint znCm = cIdx.z == gridData.gridSize.z - 1 ? CM_SOLID : getCellMaterial(cellMaterialIn[clampToGrid(cIdx + int3(0, 0, 1), gridData.gridSize)]); - setZNextMaterial(cellMaterial, znCm); - - cellMaterialOut[cIdx] = cellMaterial; - - GroupMemoryBarrierWithGroupSync(); - - // do final velocity weight here, after sync - float3 totalVelocity; - totalVelocity.x = velocityField[0][cIdx]; - totalVelocity.y = velocityField[1][cIdx]; - totalVelocity.z = velocityField[2][cIdx]; - - float3 totalWeight; - totalWeight.x = prevVelocityField[0][cIdx]; - totalWeight.y = prevVelocityField[1][cIdx]; - totalWeight.z = prevVelocityField[2][cIdx]; - - float3 velocity = select(totalWeight > 0, totalVelocity / max(totalWeight, nbl::hlsl::numeric_limits::min), 0.0f); - enforceBoundaryCondition(velocity, cellMaterial); - - velocityField[0][cIdx] = velocity.x; - velocityField[1][cIdx] = velocity.y; - velocityField[2][cIdx] = velocity.z; - prevVelocityField[0][cIdx] = velocity.x; - prevVelocityField[1][cIdx] = velocity.y; - prevVelocityField[2][cIdx] = velocity.z; -} diff --git a/70_FLIPFluids/app_resources/descriptor_bindings.hlsl b/70_FLIPFluids/app_resources/descriptor_bindings.hlsl deleted file mode 100644 index 9486e4d8a..000000000 --- a/70_FLIPFluids/app_resources/descriptor_bindings.hlsl +++ /dev/null @@ -1,436 +0,0 @@ -#ifndef _FLIP_EXAMPLE_BINDINGS_HLSL -#define _FLIP_EXAMPLE_BINDINGS_HLSL - -#include "nbl/builtin/hlsl/cpp_compat.hlsl" - -#ifndef __HLSL_VERSION -using namespace nbl; -using namespace video; -#endif - -// particlesInit -NBL_CONSTEXPR uint32_t s_pi = 1; -NBL_CONSTEXPR uint32_t b_piGridData = 0; - -#ifndef __HLSL_VERSION -NBL_CONSTEXPR IGPUDescriptorSetLayout::SBinding piParticlesInit_bs1[] = { - { - .binding = b_piGridData, - .type = asset::IDescriptor::E_TYPE::ET_UNIFORM_BUFFER, - .createFlags = IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - .stageFlags = asset::IShader::E_SHADER_STAGE::ESS_COMPUTE, - .count = 1, - } -}; -#endif - -// genParticleVertices -NBL_CONSTEXPR uint32_t s_gpv = 1; -NBL_CONSTEXPR uint32_t b_gpvCamData = 0; -NBL_CONSTEXPR uint32_t b_gpvPParams = 1; - -#ifndef __HLSL_VERSION -NBL_CONSTEXPR IGPUDescriptorSetLayout::SBinding gpvGenVertices_bs1[] = { - { - .binding = b_gpvCamData, - .type = asset::IDescriptor::E_TYPE::ET_UNIFORM_BUFFER, - .createFlags = IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - .stageFlags = asset::IShader::E_SHADER_STAGE::ESS_COMPUTE, - .count = 1, - }, - { - .binding = b_gpvPParams, - .type = asset::IDescriptor::E_TYPE::ET_UNIFORM_BUFFER, - .createFlags = IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - .stageFlags = asset::IShader::E_SHADER_STAGE::ESS_COMPUTE, - .count = 1, - } -}; -#endif - -// updateFluidCells -NBL_CONSTEXPR uint32_t s_ufc = 1; -NBL_CONSTEXPR uint32_t b_ufcGridData = 0; -NBL_CONSTEXPR uint32_t b_ufcGridPCount = 1; -NBL_CONSTEXPR uint32_t b_ufcCMIn = 2; -NBL_CONSTEXPR uint32_t b_ufcCMOut = 3; -NBL_CONSTEXPR uint32_t b_ufcVel = 4; -NBL_CONSTEXPR uint32_t b_ufcPrevVel = 5; - -#ifndef __HLSL_VERSION -NBL_CONSTEXPR IGPUDescriptorSetLayout::SBinding ufcAccWeights_bs1[] = { - { - .binding = b_ufcGridData, - .type = asset::IDescriptor::E_TYPE::ET_UNIFORM_BUFFER, - .createFlags = IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - .stageFlags = asset::IShader::E_SHADER_STAGE::ESS_COMPUTE, - .count = 1, - }, - { - .binding = b_ufcGridPCount, - .type = asset::IDescriptor::E_TYPE::ET_STORAGE_IMAGE, - .createFlags = IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - .stageFlags = asset::IShader::E_SHADER_STAGE::ESS_COMPUTE, - .count = 1, - }, - { - .binding = b_ufcVel, - .type = asset::IDescriptor::E_TYPE::ET_STORAGE_IMAGE, - .createFlags = IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - .stageFlags = asset::IShader::E_SHADER_STAGE::ESS_COMPUTE, - .count = 3, - }, - { - .binding = b_ufcPrevVel, - .type = asset::IDescriptor::E_TYPE::ET_STORAGE_IMAGE, - .createFlags = IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - .stageFlags = asset::IShader::E_SHADER_STAGE::ESS_COMPUTE, - .count = 3, - } -}; -NBL_CONSTEXPR IGPUDescriptorSetLayout::SBinding ufcFluidCell_bs1[] = { - { - .binding = b_ufcGridData, - .type = asset::IDescriptor::E_TYPE::ET_UNIFORM_BUFFER, - .createFlags = IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - .stageFlags = asset::IShader::E_SHADER_STAGE::ESS_COMPUTE, - .count = 1, - }, - { - .binding = b_ufcGridPCount, - .type = asset::IDescriptor::E_TYPE::ET_STORAGE_IMAGE, - .createFlags = IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - .stageFlags = asset::IShader::E_SHADER_STAGE::ESS_COMPUTE, - .count = 1, - }, - { - .binding = b_ufcCMOut, - .type = asset::IDescriptor::E_TYPE::ET_STORAGE_IMAGE, - .createFlags = IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - .stageFlags = asset::IShader::E_SHADER_STAGE::ESS_COMPUTE, - .count = 1, - } -}; -NBL_CONSTEXPR IGPUDescriptorSetLayout::SBinding ufcNeighborCell_bs1[] = { - { - .binding = b_ufcGridData, - .type = asset::IDescriptor::E_TYPE::ET_UNIFORM_BUFFER, - .createFlags = IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - .stageFlags = asset::IShader::E_SHADER_STAGE::ESS_COMPUTE, - .count = 1, - }, - { - .binding = b_ufcCMIn, - .type = asset::IDescriptor::E_TYPE::ET_STORAGE_IMAGE, - .createFlags = IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - .stageFlags = asset::IShader::E_SHADER_STAGE::ESS_COMPUTE, - .count = 1, - }, - { - .binding = b_ufcCMOut, - .type = asset::IDescriptor::E_TYPE::ET_STORAGE_IMAGE, - .createFlags = IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - .stageFlags = asset::IShader::E_SHADER_STAGE::ESS_COMPUTE, - .count = 1, - }, - { - .binding = b_ufcVel, - .type = asset::IDescriptor::E_TYPE::ET_STORAGE_IMAGE, - .createFlags = IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - .stageFlags = asset::IShader::E_SHADER_STAGE::ESS_COMPUTE, - .count = 3, - }, - { - .binding = b_ufcPrevVel, - .type = asset::IDescriptor::E_TYPE::ET_STORAGE_IMAGE, - .createFlags = IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - .stageFlags = asset::IShader::E_SHADER_STAGE::ESS_COMPUTE, - .count = 3, - } -}; -#endif - -// applyBodyForces -NBL_CONSTEXPR uint32_t s_abf = 1; -NBL_CONSTEXPR uint32_t b_abfGridData = 0; -NBL_CONSTEXPR uint32_t b_abfVelField = 1; -NBL_CONSTEXPR uint32_t b_abfCM = 2; - -#ifndef __HLSL_VERSION -NBL_CONSTEXPR IGPUDescriptorSetLayout::SBinding abfApplyForces_bs1[] = { - { - .binding = b_abfGridData, - .type = asset::IDescriptor::E_TYPE::ET_UNIFORM_BUFFER, - .createFlags = IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - .stageFlags = asset::IShader::E_SHADER_STAGE::ESS_COMPUTE, - .count = 1, - }, - { - .binding = b_abfVelField, - .type = asset::IDescriptor::E_TYPE::ET_STORAGE_IMAGE, - .createFlags = IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - .stageFlags = asset::IShader::E_SHADER_STAGE::ESS_COMPUTE, - .count = 3, - }, - { - .binding = b_abfCM, - .type = asset::IDescriptor::E_TYPE::ET_STORAGE_IMAGE, - .createFlags = IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - .stageFlags = asset::IShader::E_SHADER_STAGE::ESS_COMPUTE, - .count = 1, - } -}; -#endif - -// diffusion -NBL_CONSTEXPR uint32_t s_d = 1; -NBL_CONSTEXPR uint32_t b_dGridData = 0; -NBL_CONSTEXPR uint32_t b_dCM = 1; -NBL_CONSTEXPR uint32_t b_dVel = 2; -NBL_CONSTEXPR uint32_t b_dAxisIn = 3; -NBL_CONSTEXPR uint32_t b_dAxisOut = 4; -NBL_CONSTEXPR uint32_t b_dDiff = 5; - -#ifndef __HLSL_VERSION -NBL_CONSTEXPR IGPUDescriptorSetLayout::SBinding dAxisCM_bs1[] = { - { - .binding = b_dGridData, - .type = asset::IDescriptor::E_TYPE::ET_UNIFORM_BUFFER, - .createFlags = IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - .stageFlags = asset::IShader::E_SHADER_STAGE::ESS_COMPUTE, - .count = 1, - }, - { - .binding = b_dCM, - .type = asset::IDescriptor::E_TYPE::ET_STORAGE_IMAGE, - .createFlags = IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - .stageFlags = asset::IShader::E_SHADER_STAGE::ESS_COMPUTE, - .count = 1, - }, - { - .binding = b_dAxisOut, - .type = asset::IDescriptor::E_TYPE::ET_STORAGE_IMAGE, - .createFlags = IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - .stageFlags = asset::IShader::E_SHADER_STAGE::ESS_COMPUTE, - .count = 1, - } -}; -NBL_CONSTEXPR IGPUDescriptorSetLayout::SBinding dNeighborAxisCM_bs1[] = { - { - .binding = b_dGridData, - .type = asset::IDescriptor::E_TYPE::ET_UNIFORM_BUFFER, - .createFlags = IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - .stageFlags = asset::IShader::E_SHADER_STAGE::ESS_COMPUTE, - .count = 1, - }, - { - .binding = b_dAxisIn, - .type = asset::IDescriptor::E_TYPE::ET_STORAGE_IMAGE, - .createFlags = IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - .stageFlags = asset::IShader::E_SHADER_STAGE::ESS_COMPUTE, - .count = 1, - }, - { - .binding = b_dAxisOut, - .type = asset::IDescriptor::E_TYPE::ET_STORAGE_IMAGE, - .createFlags = IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - .stageFlags = asset::IShader::E_SHADER_STAGE::ESS_COMPUTE, - .count = 1, - } -}; -NBL_CONSTEXPR IGPUDescriptorSetLayout::SBinding dDiffuse_bs1[] = { - { - .binding = b_dGridData, - .type = asset::IDescriptor::E_TYPE::ET_UNIFORM_BUFFER, - .createFlags = IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - .stageFlags = asset::IShader::E_SHADER_STAGE::ESS_COMPUTE, - .count = 1, - }, - { - .binding = b_dCM, - .type = asset::IDescriptor::E_TYPE::ET_STORAGE_IMAGE, - .createFlags = IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - .stageFlags = asset::IShader::E_SHADER_STAGE::ESS_COMPUTE, - .count = 1, - }, - { - .binding = b_dVel, - .type = asset::IDescriptor::E_TYPE::ET_STORAGE_IMAGE, - .createFlags = IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - .stageFlags = asset::IShader::E_SHADER_STAGE::ESS_COMPUTE, - .count = 3, - }, - { - .binding = b_dAxisIn, - .type = asset::IDescriptor::E_TYPE::ET_STORAGE_IMAGE, - .createFlags = IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - .stageFlags = asset::IShader::E_SHADER_STAGE::ESS_COMPUTE, - .count = 1, - }, - { - .binding = b_dDiff, - .type = asset::IDescriptor::E_TYPE::ET_STORAGE_IMAGE, - .createFlags = IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - .stageFlags = asset::IShader::E_SHADER_STAGE::ESS_COMPUTE, - .count = 1, - } -}; -#endif - -// pressureSolver -NBL_CONSTEXPR uint32_t s_ps = 1; -NBL_CONSTEXPR uint32_t b_psGridData = 0; -NBL_CONSTEXPR uint32_t b_psParams = 1; -NBL_CONSTEXPR uint32_t b_psCM = 2; -NBL_CONSTEXPR uint32_t b_psVel = 3; -NBL_CONSTEXPR uint32_t b_psDiv = 4; -NBL_CONSTEXPR uint32_t b_psPres = 5; - -#ifndef __HLSL_VERSION -NBL_CONSTEXPR IGPUDescriptorSetLayout::SBinding psDivergence_bs1[] = { - { - .binding = b_psGridData, - .type = asset::IDescriptor::E_TYPE::ET_UNIFORM_BUFFER, - .createFlags = IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - .stageFlags = asset::IShader::E_SHADER_STAGE::ESS_COMPUTE, - .count = 1, - }, - { - .binding = b_psCM, - .type = asset::IDescriptor::E_TYPE::ET_STORAGE_IMAGE, - .createFlags = IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - .stageFlags = asset::IShader::E_SHADER_STAGE::ESS_COMPUTE, - .count = 1, - }, - { - .binding = b_psVel, - .type = asset::IDescriptor::E_TYPE::ET_STORAGE_IMAGE, - .createFlags = IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - .stageFlags = asset::IShader::E_SHADER_STAGE::ESS_COMPUTE, - .count = 3, - }, - { - .binding = b_psDiv, - .type = asset::IDescriptor::E_TYPE::ET_STORAGE_IMAGE, - .createFlags = IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - .stageFlags = asset::IShader::E_SHADER_STAGE::ESS_COMPUTE, - .count = 1, - } -}; -NBL_CONSTEXPR IGPUDescriptorSetLayout::SBinding psIteratePressure_bs1[] = { - { - .binding = b_psGridData, - .type = asset::IDescriptor::E_TYPE::ET_UNIFORM_BUFFER, - .createFlags = IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - .stageFlags = asset::IShader::E_SHADER_STAGE::ESS_COMPUTE, - .count = 1, - }, - { - .binding = b_psParams, - .type = asset::IDescriptor::E_TYPE::ET_UNIFORM_BUFFER, - .createFlags = IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - .stageFlags = asset::IShader::E_SHADER_STAGE::ESS_COMPUTE, - .count = 1, - }, - { - .binding = b_psCM, - .type = asset::IDescriptor::E_TYPE::ET_STORAGE_IMAGE, - .createFlags = IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - .stageFlags = asset::IShader::E_SHADER_STAGE::ESS_COMPUTE, - .count = 1, - }, - { - .binding = b_psDiv, - .type = asset::IDescriptor::E_TYPE::ET_STORAGE_IMAGE, - .createFlags = IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - .stageFlags = asset::IShader::E_SHADER_STAGE::ESS_COMPUTE, - .count = 1, - }, - { - .binding = b_psPres, - .type = asset::IDescriptor::E_TYPE::ET_STORAGE_IMAGE, - .createFlags = IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - .stageFlags = asset::IShader::E_SHADER_STAGE::ESS_COMPUTE, - .count = 1, - } -}; -NBL_CONSTEXPR IGPUDescriptorSetLayout::SBinding psUpdateVelPs_bs1[] = { - { - .binding = b_psGridData, - .type = asset::IDescriptor::E_TYPE::ET_UNIFORM_BUFFER, - .createFlags = IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - .stageFlags = asset::IShader::E_SHADER_STAGE::ESS_COMPUTE, - .count = 1, - }, - { - .binding = b_psParams, - .type = asset::IDescriptor::E_TYPE::ET_UNIFORM_BUFFER, - .createFlags = IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - .stageFlags = asset::IShader::E_SHADER_STAGE::ESS_COMPUTE, - .count = 1, - }, - { - .binding = b_psCM, - .type = asset::IDescriptor::E_TYPE::ET_STORAGE_IMAGE, - .createFlags = IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - .stageFlags = asset::IShader::E_SHADER_STAGE::ESS_COMPUTE, - .count = 1, - }, - { - .binding = b_psVel, - .type = asset::IDescriptor::E_TYPE::ET_STORAGE_IMAGE, - .createFlags = IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - .stageFlags = asset::IShader::E_SHADER_STAGE::ESS_COMPUTE, - .count = 3, - }, - { - .binding = b_psPres, - .type = asset::IDescriptor::E_TYPE::ET_STORAGE_IMAGE, - .createFlags = IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - .stageFlags = asset::IShader::E_SHADER_STAGE::ESS_COMPUTE, - .count = 1, - } -}; -#endif - -// advectParticles -NBL_CONSTEXPR uint32_t s_ap = 1; -NBL_CONSTEXPR uint32_t b_apGridData = 0; -NBL_CONSTEXPR uint32_t b_apVelField = 1; -NBL_CONSTEXPR uint32_t b_apPrevVelField = 2; -NBL_CONSTEXPR uint32_t b_apVelSampler = 3; - -#ifndef __HLSL_VERSION -NBL_CONSTEXPR IGPUDescriptorSetLayout::SBinding apAdvectParticles_bs1[] = { - { - .binding = b_apGridData, - .type = asset::IDescriptor::E_TYPE::ET_UNIFORM_BUFFER, - .createFlags = IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - .stageFlags = asset::IShader::E_SHADER_STAGE::ESS_COMPUTE, - .count = 1, - }, - { - .binding = b_apVelField, - .type = asset::IDescriptor::E_TYPE::ET_SAMPLED_IMAGE, - .createFlags = IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - .stageFlags = asset::IShader::E_SHADER_STAGE::ESS_COMPUTE, - .count = 3, - }, - { - .binding = b_apPrevVelField, - .type = asset::IDescriptor::E_TYPE::ET_SAMPLED_IMAGE, - .createFlags = IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - .stageFlags = asset::IShader::E_SHADER_STAGE::ESS_COMPUTE, - .count = 3, - }, - { - .binding = b_apVelSampler, - .type = asset::IDescriptor::E_TYPE::ET_SAMPLER, - .createFlags = IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - .stageFlags = asset::IShader::E_SHADER_STAGE::ESS_COMPUTE, - .count = 1, - } -}; -#endif - -#endif \ No newline at end of file diff --git a/70_FLIPFluids/app_resources/fluidParticles.fragment.hlsl b/70_FLIPFluids/app_resources/fluidParticles.fragment.hlsl deleted file mode 100644 index cac1bfa4a..000000000 --- a/70_FLIPFluids/app_resources/fluidParticles.fragment.hlsl +++ /dev/null @@ -1,35 +0,0 @@ -#pragma shader_stage(fragment) - -#include "common.hlsl" -#include "render_common.hlsl" - -[[vk::binding(0, 1)]] -cbuffer CameraData // TODO: BDA instead of UBO, one less thing in DSLayout -{ - SMVPParams camParams; -}; - -[shader("pixel")] -float4 main(PSInput input, out float depthTest : SV_DEPTHGREATEREQUAL) : SV_TARGET -{ - float3 N; - N.xy = input.uv * 2.0 - 1.0; - float r2 = dot(N.xy, N.xy); - if (r2 > 1.0) - discard; - N.z = -sqrt(1.0 - r2); - - float4 pixelPos = float4(input.vsSpherePos + N * input.radius, 1.0); - float4 clipSpacePos = mul(camParams.P, pixelPos); - - // invert, because reverse z-buffer - depthTest = 1.0 - clipSpacePos.z / clipSpacePos.w; - - float4 outColor = input.color; - - const float3 lightDir = float3(1, 0.5, 0.5); - float diffuse = max(0.0, dot(N, lightDir)); - outColor += float4(1, 1, 1, 1) * diffuse; - - return outColor; -} \ No newline at end of file diff --git a/70_FLIPFluids/app_resources/fluidParticles.vertex.hlsl b/70_FLIPFluids/app_resources/fluidParticles.vertex.hlsl deleted file mode 100644 index 89d37eb6f..000000000 --- a/70_FLIPFluids/app_resources/fluidParticles.vertex.hlsl +++ /dev/null @@ -1,32 +0,0 @@ -#include "common.hlsl" -#include "render_common.hlsl" - -// TODO: move the Compute Shader that generates vertices into this! -// Also do an indexed draw -struct SPushConstants -{ - uint64_t particleVerticesAddress; -}; - -[[vk::push_constant]] SPushConstants pc; - - -#include "nbl/builtin/hlsl/bda/__ptr.hlsl" -using namespace nbl::hlsl; - -[shader("vertex")] -PSInput main(uint vertexID : SV_VertexID) -{ - PSInput output; - - VertexInfo vertex = (bda::__ptr::create(pc.particleVerticesAddress)+vertexID).deref_restrict().load(); - - output.position = vertex.position; - output.vsSpherePos = vertex.vsSpherePos.xyz; - - output.radius = vertex.radius; - output.color = vertex.color; - output.uv = vertex.uv; - - return output; -} \ No newline at end of file diff --git a/70_FLIPFluids/app_resources/gridSampling.hlsl b/70_FLIPFluids/app_resources/gridSampling.hlsl deleted file mode 100644 index 21a0e81b4..000000000 --- a/70_FLIPFluids/app_resources/gridSampling.hlsl +++ /dev/null @@ -1,106 +0,0 @@ -#ifndef _FLIP_EXAMPLE_GRID_SAMPLING_HLSL -#define _FLIP_EXAMPLE_GRID_SAMPLING_HLSL - -#include "gridUtils.hlsl" - -#ifdef __HLSL_VERSION - -// adapted from CUDA Cubic B-Spline Interpolation by Danny Ruitjers -// https://www.dannyruijters.nl/cubicinterpolation/ -inline float4 cubic(float v) -{ - float4 n = float4(1.0f, 2.0f, 3.0f, 4.0f) - v; - float4 s = n * n * n; - float x = s.x; - float y = s.y - 4.0f * s.x; - float z = s.z - 4.0f * s.y + 6.0f * s.x; - float w = 6.0 - x - y - z; - return float4(x, y, z, w) * (1.0f / 6.0f); -} - -inline float tricubicInterpolate(Texture3D grid, SamplerState _sampler, float3 coords, int4 gridSize) -{ - float4 texelSize = float4(1.0f / gridSize.xz, gridSize.xz); - coords = coords * gridSize.xyz - 0.5f; - - float3 invCoords = frac(coords); - coords -= invCoords; - - float4 xcubic = cubic(invCoords.x); - float4 ycubic = cubic(invCoords.y); - float4 zcubic = cubic(invCoords.z); - - float2 cx = coords.xx + float2(-0.5f, 1.5f); - float2 cy = coords.yy + float2(-0.5f, 1.5f); - float2 cz = coords.zz + float2(-0.5f, 1.5f); - - float2 sx = xcubic.xz + xcubic.yw; - float2 sy = ycubic.xz + ycubic.yw; - float2 sz = zcubic.xz + zcubic.yw; - - float2 offsetx = cx + xcubic.yw / sx; - float2 offsety = cy + ycubic.yw / sy; - float2 offsetz = cz + zcubic.yw / sz; - offsetx /= gridSize.xx; - offsety /= gridSize.yy; - offsetz /= gridSize.zz; - - float tex0 = grid.SampleLevel(_sampler, float3(offsetx.x, offsety.x, offsetz.x), 0); - float tex1 = grid.SampleLevel(_sampler, float3(offsetx.y, offsety.x, offsetz.x), 0); - float tex2 = grid.SampleLevel(_sampler, float3(offsetx.x, offsety.y, offsetz.x), 0); - float tex3 = grid.SampleLevel(_sampler, float3(offsetx.y, offsety.y, offsetz.x), 0); - float tex4 = grid.SampleLevel(_sampler, float3(offsetx.x, offsety.x, offsetz.y), 0); - float tex5 = grid.SampleLevel(_sampler, float3(offsetx.y, offsety.x, offsetz.y), 0); - float tex6 = grid.SampleLevel(_sampler, float3(offsetx.x, offsety.y, offsetz.y), 0); - float tex7 = grid.SampleLevel(_sampler, float3(offsetx.y, offsety.y, offsetz.y), 0); - - float gx = sx.x / (sx.x + sx.y); - float gy = sy.x / (sy.x + sy.y); - float gz = sz.x / (sz.x + sz.y); - - float x0 = lerp(tex1, tex0, gx); - float x1 = lerp(tex3, tex2, gx); - float x2 = lerp(tex5, tex4, gx); - float x3 = lerp(tex6, tex6, gx); - - float y0 = lerp(x1, x0, gy); - float y1 = lerp(x3, x2, gy); - - return lerp(y1, y0, gz); -} - -inline float _sampleVelocity(float3 pos, Texture3D grid, SamplerState _sampler, SGridData gridData) -{ - float3 coords = pos / gridData.gridSize.xyz; - return tricubicInterpolate(grid, _sampler, coords, gridData.gridSize); -} - -inline float _sampleVelX(float3 pos, Texture3D grid, SamplerState _sampler, SGridData gridData) -{ - float3 gridPos = worldPosToGridPos(pos, gridData) + float3(0.5f, 0.0f, 0.0f); - return _sampleVelocity(gridPos, grid, _sampler, gridData); -} - -inline float _sampleVelY(float3 pos, Texture3D grid, SamplerState _sampler, SGridData gridData) -{ - float3 gridPos = worldPosToGridPos(pos, gridData) + float3(0.0f, 0.5f, 0.0f); - return _sampleVelocity(gridPos, grid, _sampler, gridData); -} - -inline float _sampleVelZ(float3 pos, Texture3D grid, SamplerState _sampler, SGridData gridData) -{ - float3 gridPos = worldPosToGridPos(pos, gridData) + float3(0.0f, 0.0f, 0.5f); - return _sampleVelocity(gridPos, grid, _sampler, gridData); -} - -inline float3 sampleVelocityAt(float3 pos, Texture3D grid[3], SamplerState _sampler, SGridData gridData) -{ - float3 val; - val.x = _sampleVelX(pos, grid[0], _sampler, gridData); - val.y = _sampleVelY(pos, grid[1], _sampler, gridData); - val.z = _sampleVelZ(pos, grid[2], _sampler, gridData); - return val; -} - -#endif -#endif \ No newline at end of file diff --git a/70_FLIPFluids/app_resources/gridUtils.hlsl b/70_FLIPFluids/app_resources/gridUtils.hlsl deleted file mode 100644 index 6ba51f4bc..000000000 --- a/70_FLIPFluids/app_resources/gridUtils.hlsl +++ /dev/null @@ -1,91 +0,0 @@ -#ifndef _FLIP_EXAMPLE_GRID_UTILS_HLSL -#define _FLIP_EXAMPLE_GRID_UTILS_HLSL - -// TODO: Use `float32_t3` for 3D quantities, don't waste the W coordinate -struct SGridData -{ - float32_t gridCellSize; - float32_t gridInvCellSize; - - int32_t4 particleInitMin; - int32_t4 particleInitMax; - int32_t4 particleInitSize; - - float32_t4 worldMin; - float32_t4 worldMax; - int32_t4 gridSize; // TODO: maybe `gridMax` instead because of clamping? -}; - -#ifdef __HLSL_VERSION - -static const float POSITION_EPSILON = 1e-4; - -// TODO: since these rely on the implicit knowledge of the grid size, they should probably be member functions of SGridData -// TODO: many of the arguments and return values that are `[u]int` could be `[u]int16_t` because of their limited range, this allows GPU to use FP32 units for integer math sometimes! -float3 clampPosition(float3 position, float4 gridMin, float4 gridMax) -{ - return clamp(position, gridMin.xyz + POSITION_EPSILON, gridMax.xyz - POSITION_EPSILON); -} - -int3 clampToGrid(int3 index, int4 gridSize) -{ - return clamp(index, (int3)0, gridSize.xyz - (int3)1); -} - -inline uint cellIdxToFlatIdx(int3 index, int4 gridSize) -{ - uint3 idxClamp = clamp(index, (int3)0, gridSize.xyz - (int3)1); - return idxClamp.x + idxClamp.y * gridSize.x + idxClamp.z * gridSize.x * gridSize.y; -} - -// INTEGER DIVISION AND MODULO ARE EXPENSIVE!!! -// TODO: try to compile without it and see how many places we die -// TODO: when absolutely necessary, use a variant that uses 16-bit ints instead of 32-bit because maybe final compiler will use float32_t for the short-int math -inline int3 flatIdxToCellIdx(uint id, int4 gridSize) -{ - int x = id % gridSize.x; - int y = id / gridSize.x % gridSize.y; - int z = id / (gridSize.x * gridSize.y); - return int3(x, y, z); -} - -inline float3 cellIdxToWorldPos(int3 index, SGridData data) -{ - return data.worldMin.xyz + ((float3)index + nbl::hlsl::promote(0.5f)) * data.gridCellSize; -} - -inline float3 worldPosToGridPos(float3 position, SGridData data) -{ - return (position - data.worldMin.xyz) * data.gridInvCellSize; -} - -inline int3 worldPosToCellIdx(float3 position, SGridData data) -{ - return floor(worldPosToGridPos(position, data)); -} - -inline uint worldPosToFlatIdx(float3 position, SGridData data) -{ - return cellIdxToFlatIdx(worldPosToCellIdx(position, data), data.gridSize); -} - -inline float3 gridPosToWorldPos(float3 position, SGridData data) -{ - return data.worldMin.xyz + position * data.gridCellSize; -} - -// INTEGER DIVISION AND MODULO ARE EXPENSIVE!!! -// TODO: try to compile without it and see how many places we die -int3 flatIdxToLocalGridID(uint idx, int size) -{ - uint a = size * size; - int3 b; - b.z = idx / a; - b.x = idx - b.z * a; - b.y = b.x / size; - b.x = b.x - b.y * size; - return b; -} -#endif - -#endif \ No newline at end of file diff --git a/70_FLIPFluids/app_resources/render_common.hlsl b/70_FLIPFluids/app_resources/render_common.hlsl deleted file mode 100644 index efd9bbbed..000000000 --- a/70_FLIPFluids/app_resources/render_common.hlsl +++ /dev/null @@ -1,34 +0,0 @@ -#ifndef _FLIP_EXAMPLE_RENDER_COMMON_HLSL -#define _FLIP_EXAMPLE_RENDER_COMMON_HLSL -#include "nbl/builtin/hlsl/bda/struct_declare.hlsl" - -struct SParticleRenderParams -{ - float radius; - float zNear; - float zFar; -}; - -// TODO: This struct shouldn't exist if there's no "vertex generation" shader -struct VertexInfo; -// TODO: don't use 4D vectors for 3D quantities -NBL_HLSL_DEFINE_STRUCT((VertexInfo), - ((position, float32_t4)) - ((vsSpherePos, float32_t4)) - ((radius, float32_t)) - ((color, float32_t4)) - ((uv, float32_t2)) -); - -#ifdef __HLSL_VERSION -struct PSInput -{ - float4 position : SV_Position; - float2 uv : TEXCOORD0; - nointerpolation float3 vsSpherePos : TEXCOORD1; - nointerpolation float radius : TEXCOORD2; - nointerpolation float4 color : TEXCOORD3; // TODO: unless you plan on using transparency, don't use RGBA, use RGB (float3) instead -}; -#endif - -#endif \ No newline at end of file diff --git a/70_FLIPFluids/main.cpp b/70_FLIPFluids/main.cpp deleted file mode 100644 index c702d512d..000000000 --- a/70_FLIPFluids/main.cpp +++ /dev/null @@ -1,1884 +0,0 @@ -// Copyright (C) 2024-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 - -#include "nbl/this_example/builtin/build/spirv/keys.hpp" - -#include "nbl/examples/examples.hpp" -// TODO: why is it not in nabla.h ? -#include "nbl/asset/metadata/CHLSLMetadata.h" -#include - -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; - -#include "app_resources/common.hlsl" -#include "app_resources/gridUtils.hlsl" -#include "app_resources/render_common.hlsl" -#include "app_resources/descriptor_bindings.hlsl" - - -enum SimPresets -{ - CENTER_DROP, - LONG_BOX -}; - -struct SMVPParams -{ - float cameraPosition[4]; - - float MVP[4*4]; - float M[4*4]; - float V[4*4]; - float P[4*4]; -}; - -class CSwapchainFramebuffersAndDepth final : public nbl::video::CDefaultSwapchainFramebuffers -{ - using scbase_t = CDefaultSwapchainFramebuffers; - -public: - template - inline CSwapchainFramebuffersAndDepth(ILogicalDevice* device, const asset::E_FORMAT _desiredDepthFormat, Args&&... args) - : CDefaultSwapchainFramebuffers(device, std::forward(args)...) - { - const IPhysicalDevice::SImageFormatPromotionRequest req = { - .originalFormat = _desiredDepthFormat, - .usages = {IGPUImage::EUF_RENDER_ATTACHMENT_BIT} - }; - m_depthFormat = m_device->getPhysicalDevice()->promoteImageFormat(req, IGPUImage::TILING::OPTIMAL); - - const static IGPURenderpass::SCreationParams::SDepthStencilAttachmentDescription depthAttachments[] = { - {{ - { - .format = m_depthFormat, - .samples = IGPUImage::ESCF_1_BIT, - .mayAlias = false - }, - /*.loadOp = */{IGPURenderpass::LOAD_OP::CLEAR}, - /*.storeOp = */{IGPURenderpass::STORE_OP::STORE}, - /*.initialLayout = */{IGPUImage::LAYOUT::UNDEFINED}, // because we clear we don't care about contents - /*.finalLayout = */{IGPUImage::LAYOUT::ATTACHMENT_OPTIMAL} // transition to presentation right away so we can skip a barrier - }}, - IGPURenderpass::SCreationParams::DepthStencilAttachmentsEnd - }; - m_params.depthStencilAttachments = depthAttachments; - - static IGPURenderpass::SCreationParams::SSubpassDescription subpasses[] = { - m_params.subpasses[0], - IGPURenderpass::SCreationParams::SubpassesEnd - }; - subpasses[0].depthStencilAttachment.render = { .attachmentIndex = 0,.layout = IGPUImage::LAYOUT::ATTACHMENT_OPTIMAL }; - m_params.subpasses = subpasses; - - // TODO: Two subpass external dependencies SRC and DST needed! - } - -protected: - inline bool onCreateSwapchain_impl(const uint8_t qFam) override - { - auto device = const_cast(m_renderpass->getOriginDevice()); - - const auto depthFormat = m_renderpass->getCreationParameters().depthStencilAttachments[0].format; - const auto& sharedParams = getSwapchain()->getCreationParameters().sharedParams; - auto image = device->createImage({ IImage::SCreationParams{ - .type = IGPUImage::ET_2D, - .samples = IGPUImage::ESCF_1_BIT, - .format = depthFormat, - .extent = {sharedParams.width,sharedParams.height,1}, - .mipLevels = 1, - .arrayLayers = 1, - .depthUsage = IGPUImage::EUF_RENDER_ATTACHMENT_BIT - } }); - - device->allocate(image->getMemoryReqs(), image.get()); - - m_depthBuffer = device->createImageView({ - .flags = IGPUImageView::ECF_NONE, - .subUsages = IGPUImage::EUF_RENDER_ATTACHMENT_BIT, - .image = std::move(image), - .viewType = IGPUImageView::ET_2D, - .format = depthFormat, - .subresourceRange = {IGPUImage::EAF_DEPTH_BIT,0,1,0,1} - }); - - const auto retval = scbase_t::onCreateSwapchain_impl(qFam); - m_depthBuffer = nullptr; - return retval; - } - - inline smart_refctd_ptr createFramebuffer(IGPUFramebuffer::SCreationParams&& params) override - { - params.depthStencilAttachments = &m_depthBuffer.get(); - return m_device->createFramebuffer(std::move(params)); - } - - E_FORMAT m_depthFormat; - smart_refctd_ptr m_depthBuffer; -}; - -class CEventCallback : public ISimpleManagedSurface::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)) {} - CEventCallback() {} - - void setLogger(nbl::system::logger_opt_smart_ptr& logger) - { - m_logger = logger; - } - void setInputSystem(nbl::core::smart_refctd_ptr&& m_inputSystem) - { - m_inputSystem = std::move(m_inputSystem); - } -private: - - void onMouseConnected_impl(nbl::core::smart_refctd_ptr&& mch) override - { - m_logger.log("A mouse %p has been connected", nbl::system::ILogger::ELL_INFO, mch.get()); - m_inputSystem.get()->add(m_inputSystem.get()->m_mouse, std::move(mch)); - } - void onMouseDisconnected_impl(nbl::ui::IMouseEventChannel* mch) override - { - m_logger.log("A mouse %p has been disconnected", nbl::system::ILogger::ELL_INFO, mch); - m_inputSystem.get()->remove(m_inputSystem.get()->m_mouse, mch); - } - void onKeyboardConnected_impl(nbl::core::smart_refctd_ptr&& kbch) override - { - m_logger.log("A keyboard %p has been connected", nbl::system::ILogger::ELL_INFO, kbch.get()); - m_inputSystem.get()->add(m_inputSystem.get()->m_keyboard, std::move(kbch)); - } - void onKeyboardDisconnected_impl(nbl::ui::IKeyboardEventChannel* kbch) override - { - m_logger.log("A keyboard %p has been disconnected", nbl::system::ILogger::ELL_INFO, kbch); - m_inputSystem.get()->remove(m_inputSystem.get()->m_keyboard, kbch); - } - -private: - nbl::core::smart_refctd_ptr m_inputSystem = nullptr; - nbl::system::logger_opt_smart_ptr m_logger = nullptr; -}; - -class FLIPFluidsApp final : public SimpleWindowedApplication, public BuiltinResourcesApplication -{ - using device_base_t = SimpleWindowedApplication; - using asset_base_t = BuiltinResourcesApplication; - using clock_t = std::chrono::steady_clock; - - constexpr static inline uint32_t WIN_WIDTH = 1280, WIN_HEIGHT = 720; - constexpr static inline uint32_t MaxFramesInFlight = 3u; - - constexpr static inline clock_t::duration DisplayImageDuration = std::chrono::milliseconds(900); - -public: - inline FLIPFluidsApp(const path& _localInputCWD, const path& _localOutputCWD, const path& _sharedInputCWD, const path& _sharedOutputCWD) - : IApplicationFramework(_localInputCWD, _localOutputCWD, _sharedInputCWD, _sharedOutputCWD) {} - - inline SPhysicalDeviceFeatures getPreferredDeviceFeatures() const override - { - auto retval = device_base_t::getPreferredDeviceFeatures(); - retval.pipelineExecutableInfo = true; - return retval; - } - - inline core::vector getSurfaces() const override - { - if (!m_surface) - { - { - auto windowCallback = core::make_smart_refctd_ptr(smart_refctd_ptr(m_inputSystem), smart_refctd_ptr(m_logger)); - IWindow::SCreationParams params{ - .callback = core::make_smart_refctd_ptr(), - .x = 32, - .y = 32, - .width = WIN_WIDTH, - .height = WIN_HEIGHT, - .flags = IWindow::ECF_HIDDEN | IWindow::ECF_BORDERLESS | IWindow::ECF_RESIZABLE, - .windowCaption = "FLIPFluidsApp" - }; - params.callback = windowCallback; - 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) = nbl::video::CSimpleResizeSurface::create(std::move(surface)); - } - - if (m_surface) - return { { m_surface->getSurface() } }; - - return {}; - } - - inline bool onAppInitialized(smart_refctd_ptr&& system) override - { - m_inputSystem = make_smart_refctd_ptr(logger_opt_smart_ptr(smart_refctd_ptr(m_logger))); - - if (!device_base_t::onAppInitialized(std::move(system))) - return false; - if (!asset_base_t::onAppInitialized(std::move(system))) - return false; - - // init grid params - usePreset(CENTER_DROP); - - WorkgroupCountParticles = (numParticles + WorkgroupSize - 1) / WorkgroupSize; - WorkgroupCountGrid = { - (m_gridData.gridSize.x + WorkgroupGridDim - 1) / WorkgroupGridDim, - (m_gridData.gridSize.y + WorkgroupGridDim - 1) / WorkgroupGridDim, - (m_gridData.gridSize.z + WorkgroupGridDim - 1) / WorkgroupGridDim - }; - - { - 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); - - m_pRenderParams.zNear = zNear; - m_pRenderParams.zFar = zFar; - } - m_pRenderParams.radius = m_gridData.gridCellSize * 0.4f; - - // create buffers - video::IGPUBuffer::SCreationParams params = {}; - params.size = sizeof(SGridData); - params.usage = IGPUBuffer::EUF_UNIFORM_BUFFER_BIT | IGPUBuffer::EUF_TRANSFER_DST_BIT | IGPUBuffer::EUF_INLINE_UPDATE_VIA_CMDBUF; - createBuffer(gridDataBuffer, params); - - params.size = 2 * sizeof(float32_t4); - createBuffer(pressureParamsBuffer, params); - - params.size = sizeof(SMVPParams); - createBuffer(cameraBuffer, params); - - params.size = sizeof(SParticleRenderParams); - createBuffer(pParamsBuffer, params); - - params.size = numParticles * sizeof(float32_t3); - params.usage = IGPUBuffer::EUF_STORAGE_BUFFER_BIT | IGPUBuffer::EUF_SHADER_DEVICE_ADDRESS_BIT;; - createBuffer(particleData.positionBuffer, params, IDeviceMemoryAllocation::EMAF_DEVICE_ADDRESS_BIT); - createBuffer(particleData.velocityBuffer, params, IDeviceMemoryAllocation::EMAF_DEVICE_ADDRESS_BIT); - - params.size = numParticles * 6 * sizeof(VertexInfo); - createBuffer(particleVertexBuffer, params, IDeviceMemoryAllocation::EMAF_DEVICE_ADDRESS_BIT); - - asset::VkExtent3D gridExtent = { m_gridData.gridSize.x, m_gridData.gridSize.y, m_gridData.gridSize.z }; - - // cell materials - createGridTexture(gridCellMaterialImageView, asset::EF_R16_UINT, gridExtent, asset::IImage::EUF_STORAGE_BIT, "cell material0"); - createGridTexture(tempCellMaterialImageView, asset::EF_R16_UINT, gridExtent, asset::IImage::EUF_STORAGE_BIT, "cell material1"); - - // TODO: What is this texture used for, and why is it 128 bits!?> - createGridTexture(gridAxisCellMaterialImageView, asset::EF_R32G32B32A32_UINT, gridExtent, asset::IImage::EUF_STORAGE_BIT, "axis cell material0"); - createGridTexture(tempAxisCellMaterialImageView, asset::EF_R32G32B32A32_UINT, gridExtent, asset::IImage::EUF_STORAGE_BIT, "axis cell material1"); - - createGridTexture(gridParticleCountImageView, asset::EF_R32_UINT, gridExtent, - asset::IImage::EUF_STORAGE_BIT | asset::IImage::EUF_TRANSFER_DST_BIT, "particle counts"); - - // diffusion grids - createGridTexture(gridDiffusionImageView, asset::EF_R32G32B32A32_SFLOAT, gridExtent, asset::IImage::EUF_STORAGE_BIT, "diffusion"); - - // pressure grids - createGridTexture(pressureImageView, asset::EF_R32_SFLOAT, gridExtent, asset::IImage::EUF_STORAGE_BIT, "pressure"); - createGridTexture(divergenceImageView, asset::EF_R32_SFLOAT, gridExtent, asset::IImage::EUF_STORAGE_BIT, "divergence"); - - // velocity field stuffs - core::bitflag imgCreateFlags = asset::IImage::ECF_EXTENDED_USAGE_BIT; - imgCreateFlags |= asset::IImage::ECF_MUTABLE_FORMAT_BIT; - - for (uint32_t i = 0; i < 3; i++) - { - { - createGridTexture(velocityFieldImageViews[i], asset::EF_R32_SFLOAT, gridExtent, - asset::IImage::EUF_STORAGE_BIT | asset::IImage::EUF_TRANSFER_DST_BIT | asset::IImage::EUF_SAMPLED_BIT, - "velocity field" + std::to_string(i), imgCreateFlags); - - // view as uint for cas atomic op - IGPUImageView::SCreationParams imgViewInfo; - imgViewInfo.image = velocityFieldImageViews[i]->getCreationParameters().image; - imgViewInfo.format = asset::EF_R32_UINT; - imgViewInfo.viewType = IGPUImageView::ET_3D; - imgViewInfo.flags = IGPUImageView::E_CREATE_FLAGS::ECF_NONE; - imgViewInfo.subresourceRange.aspectMask = asset::IImage::E_ASPECT_FLAGS::EAF_COLOR_BIT; - imgViewInfo.subresourceRange.baseArrayLayer = 0u; - imgViewInfo.subresourceRange.baseMipLevel = 0u; - imgViewInfo.subresourceRange.layerCount = 1u; - imgViewInfo.subresourceRange.levelCount = 1u; - - velocityFieldUintViews[i] = m_device->createImageView(std::move(imgViewInfo)); - } - - { - createGridTexture(prevVelocityFieldImageViews[i], asset::EF_R32_SFLOAT, gridExtent, - asset::IImage::EUF_STORAGE_BIT | asset::IImage::EUF_TRANSFER_DST_BIT | asset::IImage::EUF_SAMPLED_BIT, - "prev velocity field" + std::to_string(i), imgCreateFlags); - - IGPUImageView::SCreationParams imgViewInfo; - imgViewInfo.image = prevVelocityFieldImageViews[i]->getCreationParameters().image; - imgViewInfo.format = asset::EF_R32_UINT; - imgViewInfo.viewType = IGPUImageView::ET_3D; - imgViewInfo.flags = IGPUImageView::E_CREATE_FLAGS::ECF_NONE; - imgViewInfo.subresourceRange.aspectMask = asset::IImage::E_ASPECT_FLAGS::EAF_COLOR_BIT; - imgViewInfo.subresourceRange.baseArrayLayer = 0u; - imgViewInfo.subresourceRange.baseMipLevel = 0u; - imgViewInfo.subresourceRange.layerCount = 1u; - imgViewInfo.subresourceRange.levelCount = 1u; - - prevVelocityFieldUintViews[i] = m_device->createImageView(std::move(imgViewInfo)); - } - } - - IGPUSampler::SParams samplerParams = {}; - samplerParams.TextureWrapU = IGPUSampler::E_TEXTURE_CLAMP::ETC_CLAMP_TO_BORDER; - samplerParams.TextureWrapV = IGPUSampler::E_TEXTURE_CLAMP::ETC_CLAMP_TO_BORDER; - samplerParams.TextureWrapW = IGPUSampler::E_TEXTURE_CLAMP::ETC_CLAMP_TO_BORDER; - samplerParams.BorderColor = IGPUSampler::E_TEXTURE_BORDER_COLOR::ETBC_FLOAT_TRANSPARENT_BLACK; - samplerParams.MinFilter = IGPUSampler::E_TEXTURE_FILTER::ETF_LINEAR; - samplerParams.MaxFilter = IGPUSampler::E_TEXTURE_FILTER::ETF_LINEAR; - samplerParams.MipmapMode = IGPUSampler::E_SAMPLER_MIPMAP_MODE::ESMM_LINEAR; - samplerParams.AnisotropicFilter = 3; - samplerParams.CompareEnable = false; - velocityFieldSampler = m_device->createSampler(samplerParams); - - // init render pipeline - if (!initGraphicsPipeline()) - return logFail("Failed to initialize render pipeline!\n"); - - - auto createComputePipeline = [&](smart_refctd_ptr& pipeline, smart_refctd_ptr& pool, - smart_refctd_ptr& set, const std::string& entryPoint, - const std::span bindings, const asset::SPushConstantRange& pcRange = {}) -> void - { - auto shader = loadPrecompiledShader(); - - auto descriptorSetLayout1 = m_device->createDescriptorSetLayout(bindings); - - const std::array dscLayoutPtrs = { - nullptr, - descriptorSetLayout1.get(), - nullptr, - nullptr - }; - pool = m_device->createDescriptorPoolForDSLayouts(IDescriptorPool::ECF_UPDATE_AFTER_BIND_BIT, std::span(dscLayoutPtrs.begin(), dscLayoutPtrs.end())); - set = pool->createDescriptorSet(descriptorSetLayout1); - - smart_refctd_ptr pipelineLayout; - if (pcRange.size == 0) - pipelineLayout = m_device->createPipelineLayout({}, nullptr, smart_refctd_ptr(descriptorSetLayout1), nullptr, nullptr); - else - pipelineLayout = m_device->createPipelineLayout({ &pcRange, 1 }, nullptr, smart_refctd_ptr(descriptorSetLayout1), nullptr, nullptr); - - IGPUComputePipeline::SCreationParams params = {}; - params.layout = pipelineLayout.get(); - params.shader.entryPoint = entryPoint; - params.shader.shader = shader.get(); - if (m_device->getEnabledFeatures().pipelineExecutableInfo) - { - params.flags |= IGPUComputePipeline::SCreationParams::FLAGS::CAPTURE_STATISTICS; - params.flags |= IGPUComputePipeline::SCreationParams::FLAGS::CAPTURE_INTERNAL_REPRESENTATIONS; - } - m_device->createComputePipelines(nullptr, { ¶ms,1 }, &pipeline); - - if (m_device->getEnabledFeatures().pipelineExecutableInfo && pipeline) - { - auto report = system::to_string(pipeline->getExecutableInfo()); - m_logger->log("%s Pipeline Executable Report:\n%s", ILogger::ELL_PERFORMANCE, ShaderKey.value, report.c_str()); - } - }; - - { - // init particles pipeline - const asset::SPushConstantRange pcRange = { .stageFlags = IShader::E_SHADER_STAGE::ESS_COMPUTE, .offset = 0, .size = 2 * sizeof(uint64_t) }; - createComputePipeline.operator()<"particlesInit">(m_initParticlePipeline, m_initParticlePool, m_initParticleDs, - "main", piParticlesInit_bs1, pcRange); - - { - IGPUDescriptorSet::SDescriptorInfo infos[1]; - infos[0].desc = smart_refctd_ptr(gridDataBuffer); - infos[0].info.buffer = {.offset = 0, .size = gridDataBuffer->getSize()}; - IGPUDescriptorSet::SWriteDescriptorSet writes[1] = { - {.dstSet = m_initParticleDs.get(), .binding = b_piGridData, .arrayElement = 0, .count = 1, .info = &infos[0]} - }; - m_device->updateDescriptorSets(std::span(writes, 1), {}); - } - } - // TODO: get rid of this pipeline! - { - // generate particle vertex pipeline - const asset::SPushConstantRange pcRange = { .stageFlags = IShader::E_SHADER_STAGE::ESS_COMPUTE, .offset = 0, .size = 3 * sizeof(uint64_t) }; - createComputePipeline.operator()<"genParticleVertices">(m_genParticleVerticesPipeline, m_genVerticesPool, m_genVerticesDs, - "main", gpvGenVertices_bs1, pcRange); - - { - IGPUDescriptorSet::SDescriptorInfo infos[2]; - infos[0].desc = smart_refctd_ptr(cameraBuffer); - infos[0].info.buffer = {.offset = 0, .size = cameraBuffer->getSize()}; - infos[1].desc = smart_refctd_ptr(pParamsBuffer); - infos[1].info.buffer = {.offset = 0, .size = pParamsBuffer->getSize()}; - IGPUDescriptorSet::SWriteDescriptorSet writes[2] = { - {.dstSet = m_genVerticesDs.get(), .binding = b_gpvCamData, .arrayElement = 0, .count = 1, .info = &infos[0]}, - {.dstSet = m_genVerticesDs.get(), .binding = b_gpvPParams, .arrayElement = 0, .count = 1, .info = &infos[1]}, - }; - m_device->updateDescriptorSets(std::span(writes, 2), {}); - } - } - // update fluid cells pipelines - { - const asset::SPushConstantRange pcRange = { .stageFlags = IShader::E_SHADER_STAGE::ESS_COMPUTE, .offset = 0, .size = 2 * sizeof(uint64_t) }; - createComputePipeline.operator()<"prepareCellUpdate">(m_accumulateWeightsPipeline, m_accumulateWeightsPool, m_accumulateWeightsDs, - "main", ufcAccWeights_bs1, pcRange); - - { - IGPUDescriptorSet::SDescriptorInfo infos[2]; - infos[0].desc = smart_refctd_ptr(gridDataBuffer); - infos[0].info.buffer = { .offset = 0, .size = gridDataBuffer->getSize() }; - infos[1].desc = gridParticleCountImageView; - infos[1].info.image.imageLayout = IImage::LAYOUT::GENERAL; - infos[1].info.combinedImageSampler.sampler = nullptr; - - IGPUDescriptorSet::SDescriptorInfo imgInfosVel0[3]; - imgInfosVel0[0].desc = velocityFieldUintViews[0]; - imgInfosVel0[0].info.image.imageLayout = IImage::LAYOUT::GENERAL; - imgInfosVel0[0].info.combinedImageSampler.sampler = nullptr; - imgInfosVel0[1].desc = velocityFieldUintViews[1]; - imgInfosVel0[1].info.image.imageLayout = IImage::LAYOUT::GENERAL; - imgInfosVel0[1].info.combinedImageSampler.sampler = nullptr; - imgInfosVel0[2].desc = velocityFieldUintViews[2]; - imgInfosVel0[2].info.image.imageLayout = IImage::LAYOUT::GENERAL; - imgInfosVel0[2].info.combinedImageSampler.sampler = nullptr; - - IGPUDescriptorSet::SDescriptorInfo imgInfosVel1[3]; - imgInfosVel1[0].desc = prevVelocityFieldUintViews[0]; - imgInfosVel1[0].info.image.imageLayout = IImage::LAYOUT::GENERAL; - imgInfosVel1[0].info.combinedImageSampler.sampler = nullptr; - imgInfosVel1[1].desc = prevVelocityFieldUintViews[1]; - imgInfosVel1[1].info.image.imageLayout = IImage::LAYOUT::GENERAL; - imgInfosVel1[1].info.combinedImageSampler.sampler = nullptr; - imgInfosVel1[2].desc = prevVelocityFieldUintViews[2]; - imgInfosVel1[2].info.image.imageLayout = IImage::LAYOUT::GENERAL; - imgInfosVel1[2].info.combinedImageSampler.sampler = nullptr; - - IGPUDescriptorSet::SWriteDescriptorSet writes[4] = { - {.dstSet = m_accumulateWeightsDs.get(), .binding = b_ufcGridData, .arrayElement = 0, .count = 1, .info = &infos[0]}, - {.dstSet = m_accumulateWeightsDs.get(), .binding = b_ufcGridPCount, .arrayElement = 0, .count = 1, .info = &infos[1]}, - {.dstSet = m_accumulateWeightsDs.get(), .binding = b_ufcVel, .arrayElement = 0, .count = 3, .info = imgInfosVel0}, - {.dstSet = m_accumulateWeightsDs.get(), .binding = b_ufcPrevVel, .arrayElement = 0, .count = 3, .info = imgInfosVel1}, - }; - m_device->updateDescriptorSets(std::span(writes, 4), {}); - } - } - { - createComputePipeline.operator()<"updateFluidCells">(m_updateFluidCellsPipeline, m_updateFluidCellsPool, m_updateFluidCellsDs, - "updateFluidCells", ufcFluidCell_bs1); - - { - IGPUDescriptorSet::SDescriptorInfo infos[3]; - infos[0].desc = smart_refctd_ptr(gridDataBuffer); - infos[0].info.buffer = {.offset = 0, .size = gridDataBuffer->getSize()}; - infos[1].desc = gridParticleCountImageView; - infos[1].info.image.imageLayout = IImage::LAYOUT::GENERAL; - infos[1].info.combinedImageSampler.sampler = nullptr; - infos[2].desc = tempCellMaterialImageView; - infos[2].info.image.imageLayout = IImage::LAYOUT::GENERAL; - infos[2].info.combinedImageSampler.sampler = nullptr; - IGPUDescriptorSet::SWriteDescriptorSet writes[3] = { - {.dstSet = m_updateFluidCellsDs.get(), .binding = b_ufcGridData, .arrayElement = 0, .count = 1, .info = &infos[0]}, - {.dstSet = m_updateFluidCellsDs.get(), .binding = b_ufcGridPCount, .arrayElement = 0, .count = 1, .info = &infos[1]}, - {.dstSet = m_updateFluidCellsDs.get(), .binding = b_ufcCMOut, .arrayElement = 0, .count = 1, .info = &infos[2]}, - }; - m_device->updateDescriptorSets(std::span(writes, 3), {}); - } - } - { - createComputePipeline.operator()<"updateFluidCells">(m_updateNeighborCellsPipeline, m_updateNeighborCellsPool, m_updateNeighborCellsDs, - "updateNeighborFluidCells", ufcNeighborCell_bs1); - - { - IGPUDescriptorSet::SDescriptorInfo infos[3]; - infos[0].desc = smart_refctd_ptr(gridDataBuffer); - infos[0].info.buffer = {.offset = 0, .size = gridDataBuffer->getSize()}; - infos[1].desc = tempCellMaterialImageView; - infos[1].info.image.imageLayout = IImage::LAYOUT::GENERAL; - infos[1].info.combinedImageSampler.sampler = nullptr; - infos[2].desc = gridCellMaterialImageView; - infos[2].info.image.imageLayout = IImage::LAYOUT::GENERAL; - infos[2].info.combinedImageSampler.sampler = nullptr; - - IGPUDescriptorSet::SDescriptorInfo imgInfosVel0[3]; - imgInfosVel0[0].desc = velocityFieldImageViews[0]; - imgInfosVel0[0].info.image.imageLayout = IImage::LAYOUT::GENERAL; - imgInfosVel0[0].info.combinedImageSampler.sampler = nullptr; - imgInfosVel0[1].desc = velocityFieldImageViews[1]; - imgInfosVel0[1].info.image.imageLayout = IImage::LAYOUT::GENERAL; - imgInfosVel0[1].info.combinedImageSampler.sampler = nullptr; - imgInfosVel0[2].desc = velocityFieldImageViews[2]; - imgInfosVel0[2].info.image.imageLayout = IImage::LAYOUT::GENERAL; - imgInfosVel0[2].info.combinedImageSampler.sampler = nullptr; - - IGPUDescriptorSet::SDescriptorInfo imgInfosVel1[3]; - imgInfosVel1[0].desc = prevVelocityFieldImageViews[0]; - imgInfosVel1[0].info.image.imageLayout = IImage::LAYOUT::GENERAL; - imgInfosVel1[0].info.combinedImageSampler.sampler = nullptr; - imgInfosVel1[1].desc = prevVelocityFieldImageViews[1]; - imgInfosVel1[1].info.image.imageLayout = IImage::LAYOUT::GENERAL; - imgInfosVel1[1].info.combinedImageSampler.sampler = nullptr; - imgInfosVel1[2].desc = prevVelocityFieldImageViews[2]; - imgInfosVel1[2].info.image.imageLayout = IImage::LAYOUT::GENERAL; - imgInfosVel1[2].info.combinedImageSampler.sampler = nullptr; - - IGPUDescriptorSet::SWriteDescriptorSet writes[5] = { - {.dstSet = m_updateNeighborCellsDs.get(), .binding = b_ufcGridData, .arrayElement = 0, .count = 1, .info = &infos[0]}, - {.dstSet = m_updateNeighborCellsDs.get(), .binding = b_ufcCMIn, .arrayElement = 0, .count = 1, .info = &infos[1]}, - {.dstSet = m_updateNeighborCellsDs.get(), .binding = b_ufcCMOut, .arrayElement = 0, .count = 1, .info = &infos[2]}, - {.dstSet = m_updateNeighborCellsDs.get(), .binding = b_ufcVel, .arrayElement = 0, .count = 3, .info = imgInfosVel0}, - {.dstSet = m_updateNeighborCellsDs.get(), .binding = b_ufcPrevVel, .arrayElement = 0, .count = 3, .info = imgInfosVel1}, - }; - m_device->updateDescriptorSets(std::span(writes, 5), {}); - } - } - { - // apply forces pipeline - createComputePipeline.operator()<"applyBodyForces">(m_applyBodyForcesPipeline, m_applyForcesPool, m_applyForcesDs, - "main", abfApplyForces_bs1); - - { - IGPUDescriptorSet::SDescriptorInfo infos[2]; - infos[0].desc = smart_refctd_ptr(gridDataBuffer); - infos[0].info.buffer = {.offset = 0, .size = gridDataBuffer->getSize()}; - infos[1].desc = gridCellMaterialImageView; - infos[1].info.image.imageLayout = IImage::LAYOUT::GENERAL; - infos[1].info.combinedImageSampler.sampler = nullptr; - - IGPUDescriptorSet::SDescriptorInfo imgInfosVel0[3]; - imgInfosVel0[0].desc = velocityFieldImageViews[0]; - imgInfosVel0[0].info.image.imageLayout = IImage::LAYOUT::GENERAL; - imgInfosVel0[0].info.combinedImageSampler.sampler = nullptr; - imgInfosVel0[1].desc = velocityFieldImageViews[1]; - imgInfosVel0[1].info.image.imageLayout = IImage::LAYOUT::GENERAL; - imgInfosVel0[1].info.combinedImageSampler.sampler = nullptr; - imgInfosVel0[2].desc = velocityFieldImageViews[2]; - imgInfosVel0[2].info.image.imageLayout = IImage::LAYOUT::GENERAL; - imgInfosVel0[2].info.combinedImageSampler.sampler = nullptr; - - IGPUDescriptorSet::SWriteDescriptorSet writes[3] = { - {.dstSet = m_applyForcesDs.get(), .binding = b_abfGridData, .arrayElement = 0, .count = 1, .info = &infos[0]}, - {.dstSet = m_applyForcesDs.get(), .binding = b_abfVelField, .arrayElement = 0, .count = 3, .info = imgInfosVel0}, - {.dstSet = m_applyForcesDs.get(), .binding = b_abfCM, .arrayElement = 0, .count = 1, .info = &infos[1]}, - }; - m_device->updateDescriptorSets(std::span(writes, 3), {}); - } - } - // apply diffusion pipelines - { - createComputePipeline.operator()<"diffusion">(m_axisCellsPipeline, m_axisCellsPool, m_axisCellsDs, - "setAxisCellMaterial", dAxisCM_bs1); - - { - IGPUDescriptorSet::SDescriptorInfo infos[3]; - infos[0].desc = smart_refctd_ptr(gridDataBuffer); - infos[0].info.buffer = {.offset = 0, .size = gridDataBuffer->getSize()}; - infos[1].desc = gridCellMaterialImageView; - infos[1].info.image.imageLayout = IImage::LAYOUT::GENERAL; - infos[1].info.combinedImageSampler.sampler = nullptr; - infos[2].desc = tempAxisCellMaterialImageView; - infos[2].info.image.imageLayout = IImage::LAYOUT::GENERAL; - infos[2].info.combinedImageSampler.sampler = nullptr; - IGPUDescriptorSet::SWriteDescriptorSet writes[3] = { - {.dstSet = m_axisCellsDs.get(), .binding = b_dGridData, .arrayElement = 0, .count = 1, .info = &infos[0]}, - {.dstSet = m_axisCellsDs.get(), .binding = b_dCM, .arrayElement = 0, .count = 1, .info = &infos[1]}, - {.dstSet = m_axisCellsDs.get(), .binding = b_dAxisOut, .arrayElement = 0, .count = 1, .info = &infos[2]}, - }; - m_device->updateDescriptorSets(std::span(writes, 3), {}); - } - } - { - createComputePipeline.operator()<"diffusion">(m_neighborAxisCellsPipeline, m_neighborAxisCellsPool, m_neighborAxisCellsDs, - "setNeighborAxisCellMaterial", dNeighborAxisCM_bs1); - - { - IGPUDescriptorSet::SDescriptorInfo infos[3]; - infos[0].desc = smart_refctd_ptr(gridDataBuffer); - infos[0].info.buffer = {.offset = 0, .size = gridDataBuffer->getSize()}; - infos[1].desc = tempAxisCellMaterialImageView; - infos[1].info.image.imageLayout = IImage::LAYOUT::GENERAL; - infos[1].info.combinedImageSampler.sampler = nullptr; - infos[2].desc = gridAxisCellMaterialImageView; - infos[2].info.image.imageLayout = IImage::LAYOUT::GENERAL; - infos[2].info.combinedImageSampler.sampler = nullptr; - IGPUDescriptorSet::SWriteDescriptorSet writes[3] = { - {.dstSet = m_neighborAxisCellsDs.get(), .binding = b_dGridData, .arrayElement = 0, .count = 1, .info = &infos[0]}, - {.dstSet = m_neighborAxisCellsDs.get(), .binding = b_dAxisIn, .arrayElement = 0, .count = 1, .info = &infos[1]}, - {.dstSet = m_neighborAxisCellsDs.get(), .binding = b_dAxisOut, .arrayElement = 0, .count = 1, .info = &infos[2]}, - }; - m_device->updateDescriptorSets(std::span(writes, 3), {}); - } - } - { - smart_refctd_ptr diffusion = loadPrecompiledShader<"diffusion">(); // "app_resources/compute/diffusion.comp.hlsl" - - auto descriptorSetLayout1 = m_device->createDescriptorSetLayout(dDiffuse_bs1); - - const std::array dscLayoutPtrs = { - nullptr, - descriptorSetLayout1.get() - }; - const uint32_t setCounts[2u] = { 0u, 2u }; - m_diffusionPool = m_device->createDescriptorPoolForDSLayouts(IDescriptorPool::ECF_UPDATE_AFTER_BIND_BIT, std::span(dscLayoutPtrs.begin(), dscLayoutPtrs.end()), setCounts); - m_diffusionDs = m_diffusionPool->createDescriptorSet(descriptorSetLayout1); - - const asset::SPushConstantRange pcRange = { .stageFlags = IShader::E_SHADER_STAGE::ESS_COMPUTE, .offset = 0, .size = 4 * sizeof(uint32_t) }; - - smart_refctd_ptr pipelineLayout = m_device->createPipelineLayout({ &pcRange, 1 }, nullptr, smart_refctd_ptr(descriptorSetLayout1), nullptr, nullptr); - - { - IGPUComputePipeline::SCreationParams params = {}; - params.layout = pipelineLayout.get(); - params.shader.entryPoint = "iterateDiffusion"; - params.shader.shader = diffusion.get(); - if (m_device->getEnabledFeatures().pipelineExecutableInfo) - { - params.flags |= IGPUComputePipeline::SCreationParams::FLAGS::CAPTURE_STATISTICS; - params.flags |= IGPUComputePipeline::SCreationParams::FLAGS::CAPTURE_INTERNAL_REPRESENTATIONS; - } - if (!m_device->createComputePipelines(nullptr, { ¶ms,1 }, &m_iterateDiffusionPipeline)) - m_logger->log("Failed to create iterateDiffusion pipeline!\n"); - - if (m_device->getEnabledFeatures().pipelineExecutableInfo) - { - auto report = system::to_string(m_iterateDiffusionPipeline->getExecutableInfo()); - m_logger->log("iterateDiffusion Pipeline Executable Report:\n%s", ILogger::ELL_PERFORMANCE, report.c_str()); - } - } - { - IGPUComputePipeline::SCreationParams params = {}; - params.layout = pipelineLayout.get(); - params.shader.entryPoint = "applyDiffusion"; - params.shader.shader = diffusion.get(); - if (m_device->getEnabledFeatures().pipelineExecutableInfo) - { - params.flags |= IGPUComputePipeline::SCreationParams::FLAGS::CAPTURE_STATISTICS; - params.flags |= IGPUComputePipeline::SCreationParams::FLAGS::CAPTURE_INTERNAL_REPRESENTATIONS; - } - if (!m_device->createComputePipelines(nullptr, { ¶ms,1 }, &m_diffusionPipeline)) - m_logger->log("Failed to create applyDiffusion pipeline!\n"); - - if (m_device->getEnabledFeatures().pipelineExecutableInfo) - { - auto report = system::to_string(m_diffusionPipeline->getExecutableInfo()); - m_logger->log("applyDiffusion Pipeline Executable Report:\n%s", ILogger::ELL_PERFORMANCE, report.c_str()); - } - } - - { - IGPUDescriptorSet::SDescriptorInfo infos[4]; - infos[0].desc = smart_refctd_ptr(gridDataBuffer); - infos[0].info.buffer = {.offset = 0, .size = gridDataBuffer->getSize()}; - infos[1].desc = gridCellMaterialImageView; - infos[1].info.image.imageLayout = IImage::LAYOUT::GENERAL; - infos[1].info.combinedImageSampler.sampler = nullptr; - infos[2].desc = gridAxisCellMaterialImageView; - infos[2].info.image.imageLayout = IImage::LAYOUT::GENERAL; - infos[2].info.combinedImageSampler.sampler = nullptr; - infos[3].desc = gridDiffusionImageView; - infos[3].info.image.imageLayout = IImage::LAYOUT::GENERAL; - infos[3].info.combinedImageSampler.sampler = nullptr; - - IGPUDescriptorSet::SDescriptorInfo imgInfosVel0[3]; - imgInfosVel0[0].desc = velocityFieldImageViews[0]; - imgInfosVel0[0].info.image.imageLayout = IImage::LAYOUT::GENERAL; - imgInfosVel0[0].info.combinedImageSampler.sampler = nullptr; - imgInfosVel0[1].desc = velocityFieldImageViews[1]; - imgInfosVel0[1].info.image.imageLayout = IImage::LAYOUT::GENERAL; - imgInfosVel0[1].info.combinedImageSampler.sampler = nullptr; - imgInfosVel0[2].desc = velocityFieldImageViews[2]; - imgInfosVel0[2].info.image.imageLayout = IImage::LAYOUT::GENERAL; - imgInfosVel0[2].info.combinedImageSampler.sampler = nullptr; - - IGPUDescriptorSet::SWriteDescriptorSet writes[5] = { - {.dstSet = m_diffusionDs.get(), .binding = b_dGridData, .arrayElement = 0, .count = 1, .info = &infos[0]}, - {.dstSet = m_diffusionDs.get(), .binding = b_dCM, .arrayElement = 0, .count = 1, .info = &infos[1]}, - {.dstSet = m_diffusionDs.get(), .binding = b_dVel, .arrayElement = 0, .count = 3, .info = imgInfosVel0}, - {.dstSet = m_diffusionDs.get(), .binding = b_dAxisIn, .arrayElement = 0, .count = 1, .info = &infos[2]}, - {.dstSet = m_diffusionDs.get(), .binding = b_dDiff, .arrayElement = 0, .count = 1, .info = &infos[3]} - }; - m_device->updateDescriptorSets(std::span(writes, 5), {}); - } - } - // solve pressure system pipelines - { - createComputePipeline.operator()<"pressureSolver">(m_calcDivergencePipeline, m_calcDivergencePool, m_calcDivergenceDs, - "calculateNegativeDivergence", psDivergence_bs1); - - { - IGPUDescriptorSet::SDescriptorInfo infos[3]; - infos[0].desc = smart_refctd_ptr(gridDataBuffer); - infos[0].info.buffer = {.offset = 0, .size = gridDataBuffer->getSize()}; - infos[1].desc = gridCellMaterialImageView; - infos[1].info.image.imageLayout = IImage::LAYOUT::GENERAL; - infos[1].info.combinedImageSampler.sampler = nullptr; - infos[2].desc = divergenceImageView; - infos[2].info.image.imageLayout = IImage::LAYOUT::GENERAL; - infos[2].info.combinedImageSampler.sampler = nullptr; - - IGPUDescriptorSet::SDescriptorInfo imgInfosVel0[3]; - imgInfosVel0[0].desc = velocityFieldImageViews[0]; - imgInfosVel0[0].info.image.imageLayout = IImage::LAYOUT::GENERAL; - imgInfosVel0[0].info.combinedImageSampler.sampler = nullptr; - imgInfosVel0[1].desc = velocityFieldImageViews[1]; - imgInfosVel0[1].info.image.imageLayout = IImage::LAYOUT::GENERAL; - imgInfosVel0[1].info.combinedImageSampler.sampler = nullptr; - imgInfosVel0[2].desc = velocityFieldImageViews[2]; - imgInfosVel0[2].info.image.imageLayout = IImage::LAYOUT::GENERAL; - imgInfosVel0[2].info.combinedImageSampler.sampler = nullptr; - - IGPUDescriptorSet::SWriteDescriptorSet writes[4] = { - {.dstSet = m_calcDivergenceDs.get(), .binding = b_psGridData, .arrayElement = 0, .count = 1, .info = &infos[0]}, - {.dstSet = m_calcDivergenceDs.get(), .binding = b_psCM, .arrayElement = 0, .count = 1, .info = &infos[1]}, - {.dstSet = m_calcDivergenceDs.get(), .binding = b_psVel, .arrayElement = 0, .count = 3, .info = imgInfosVel0}, - {.dstSet = m_calcDivergenceDs.get(), .binding = b_psDiv, .arrayElement = 0, .count = 1, .info = &infos[2]}, - }; - m_device->updateDescriptorSets(std::span(writes, 4), {}); - } - } - { - createComputePipeline.operator()<"pressureSolver">(m_iteratePressurePipeline, m_iteratePressurePool, m_iteratePressureDs, - "iteratePressureSystem", psIteratePressure_bs1); - - { - IGPUDescriptorSet::SDescriptorInfo infos[5]; - infos[0].desc = smart_refctd_ptr(gridDataBuffer); - infos[0].info.buffer = { .offset = 0, .size = gridDataBuffer->getSize() }; - infos[1].desc = smart_refctd_ptr(pressureParamsBuffer); - infos[1].info.buffer = { .offset = 0, .size = pressureParamsBuffer->getSize() }; - infos[2].desc = gridCellMaterialImageView; - infos[2].info.image.imageLayout = IImage::LAYOUT::GENERAL; - infos[2].info.combinedImageSampler.sampler = nullptr; - infos[3].desc = divergenceImageView; - infos[3].info.image.imageLayout = IImage::LAYOUT::GENERAL; - infos[3].info.combinedImageSampler.sampler = nullptr; - infos[4].desc = pressureImageView; - infos[4].info.image.imageLayout = IImage::LAYOUT::GENERAL; - infos[4].info.combinedImageSampler.sampler = nullptr; - IGPUDescriptorSet::SWriteDescriptorSet writes[5] = { - {.dstSet = m_iteratePressureDs.get(), .binding = b_psGridData, .arrayElement = 0, .count = 1, .info = &infos[0]}, - {.dstSet = m_iteratePressureDs.get(), .binding = b_psParams, .arrayElement = 0, .count = 1, .info = &infos[1]}, - {.dstSet = m_iteratePressureDs.get(), .binding = b_psCM, .arrayElement = 0, .count = 1, .info = &infos[2]}, - {.dstSet = m_iteratePressureDs.get(), .binding = b_psDiv, .arrayElement = 0, .count = 1, .info = &infos[3]}, - {.dstSet = m_iteratePressureDs.get(), .binding = b_psPres, .arrayElement = 0, .count = 1, .info = &infos[4]}, - }; - m_device->updateDescriptorSets(std::span(writes, 5), {}); - } - } - { - createComputePipeline.operator()<"pressureSolver">(m_updateVelPsPipeline, m_updateVelPsPool, m_updateVelPsDs, - "updateVelocities", psUpdateVelPs_bs1); - - { - IGPUDescriptorSet::SDescriptorInfo infos[4]; - infos[0].desc = smart_refctd_ptr(gridDataBuffer); - infos[0].info.buffer = {.offset = 0, .size = gridDataBuffer->getSize()}; - infos[1].desc = smart_refctd_ptr(pressureParamsBuffer); - infos[1].info.buffer = {.offset = 0, .size = pressureParamsBuffer->getSize()}; - infos[2].desc = gridCellMaterialImageView; - infos[2].info.image.imageLayout = IImage::LAYOUT::GENERAL; - infos[2].info.combinedImageSampler.sampler = nullptr; - infos[3].desc = pressureImageView; - infos[3].info.image.imageLayout = IImage::LAYOUT::GENERAL; - infos[3].info.combinedImageSampler.sampler = nullptr; - - IGPUDescriptorSet::SDescriptorInfo imgInfosVel0[3]; - imgInfosVel0[0].desc = velocityFieldImageViews[0]; - imgInfosVel0[0].info.image.imageLayout = IImage::LAYOUT::GENERAL; - imgInfosVel0[0].info.combinedImageSampler.sampler = nullptr; - imgInfosVel0[1].desc = velocityFieldImageViews[1]; - imgInfosVel0[1].info.image.imageLayout = IImage::LAYOUT::GENERAL; - imgInfosVel0[1].info.combinedImageSampler.sampler = nullptr; - imgInfosVel0[2].desc = velocityFieldImageViews[2]; - imgInfosVel0[2].info.image.imageLayout = IImage::LAYOUT::GENERAL; - imgInfosVel0[2].info.combinedImageSampler.sampler = nullptr; - - IGPUDescriptorSet::SWriteDescriptorSet writes[5] = { - {.dstSet = m_updateVelPsDs.get(), .binding = b_psGridData, .arrayElement = 0, .count = 1, .info = &infos[0]}, - {.dstSet = m_updateVelPsDs.get(), .binding = b_psParams, .arrayElement = 0, .count = 1, .info = &infos[1]}, - {.dstSet = m_updateVelPsDs.get(), .binding = b_psCM, .arrayElement = 0, .count = 1, .info = &infos[2]}, - {.dstSet = m_updateVelPsDs.get(), .binding = b_psVel, .arrayElement = 0, .count = 3, .info = imgInfosVel0}, - {.dstSet = m_updateVelPsDs.get(), .binding = b_psPres, .arrayElement = 0, .count = 1, .info = &infos[3]}, - }; - m_device->updateDescriptorSets(std::span(writes, 5), {}); - } - } - { - // advect particles pipeline - const asset::SPushConstantRange pcRange = { .stageFlags = IShader::E_SHADER_STAGE::ESS_COMPUTE, .offset = 0, .size = 2 * sizeof(uint64_t) }; - createComputePipeline.operator()<"advectParticles">(m_advectParticlesPipeline, m_advectParticlesPool, m_advectParticlesDs, - "main", apAdvectParticles_bs1, pcRange); - - { - IGPUDescriptorSet::SDescriptorInfo infos[2]; - infos[0].desc = smart_refctd_ptr(gridDataBuffer); - infos[0].info.buffer = {.offset = 0, .size = gridDataBuffer->getSize()}; - infos[1].desc = velocityFieldSampler; - - IGPUDescriptorSet::SDescriptorInfo imgInfosVel0[3]; - imgInfosVel0[0].desc = velocityFieldImageViews[0]; - imgInfosVel0[0].info.image.imageLayout = IImage::LAYOUT::GENERAL; - imgInfosVel0[0].info.combinedImageSampler.sampler = nullptr; - imgInfosVel0[1].desc = velocityFieldImageViews[1]; - imgInfosVel0[1].info.image.imageLayout = IImage::LAYOUT::GENERAL; - imgInfosVel0[1].info.combinedImageSampler.sampler = nullptr; - imgInfosVel0[2].desc = velocityFieldImageViews[2]; - imgInfosVel0[2].info.image.imageLayout = IImage::LAYOUT::GENERAL; - imgInfosVel0[2].info.combinedImageSampler.sampler = nullptr; - - IGPUDescriptorSet::SDescriptorInfo imgInfosVel1[3]; - imgInfosVel1[0].desc = prevVelocityFieldImageViews[0]; - imgInfosVel1[0].info.image.imageLayout = IImage::LAYOUT::GENERAL; - imgInfosVel1[0].info.combinedImageSampler.sampler = nullptr; - imgInfosVel1[1].desc = prevVelocityFieldImageViews[1]; - imgInfosVel1[1].info.image.imageLayout = IImage::LAYOUT::GENERAL; - imgInfosVel1[1].info.combinedImageSampler.sampler = nullptr; - imgInfosVel1[2].desc = prevVelocityFieldImageViews[2]; - imgInfosVel1[2].info.image.imageLayout = IImage::LAYOUT::GENERAL; - imgInfosVel1[2].info.combinedImageSampler.sampler = nullptr; - - IGPUDescriptorSet::SWriteDescriptorSet writes[4] = { - {.dstSet = m_advectParticlesDs.get(), .binding = b_apGridData, .arrayElement = 0, .count = 1, .info = &infos[0]}, - {.dstSet = m_advectParticlesDs.get(), .binding = b_apVelField, .arrayElement = 0, .count = 3, .info = imgInfosVel0}, - {.dstSet = m_advectParticlesDs.get(), .binding = b_apPrevVelField, .arrayElement = 0, .count = 3, .info = imgInfosVel1}, - {.dstSet = m_advectParticlesDs.get(), .binding = b_apVelSampler, .arrayElement = 0, .count = 1, .info = &infos[1]} - }; - m_device->updateDescriptorSets(std::span(writes, 4), {}); - } - } - - m_winMgr->show(m_window.get()); - - return true; - } - - inline void workLoopBody() override - { - const uint32_t framesInFlight = core::min(MaxFramesInFlight, m_surface->getMaxAcquiresInFlight()); - - if (m_realFrameIx >= framesInFlight) - { - const ISemaphore::SWaitInfo cbDonePending[] = - { - { - .semaphore = m_renderSemaphore.get(), - .value = m_realFrameIx + 1 - framesInFlight - } - }; - if (m_device->blockForSemaphores(cbDonePending) != ISemaphore::WAIT_RESULT::SUCCESS) - return; - } - - const auto resourceIx = m_realFrameIx % MaxFramesInFlight; - - m_inputSystem->getDefaultMouse(&mouse); - m_inputSystem->getDefaultKeyboard(&keyboard); - - auto updatePresentationTimestamp = [&]() - { - m_currentImageAcquire = m_surface->acquireNextImage(); - - oracle.reportEndFrameRecord(); - const auto timestamp = oracle.getNextPresentationTimeStamp(); - oracle.reportBeginFrameRecord(); - - return timestamp; - }; - - const auto nextPresentationTimestamp = updatePresentationTimestamp(); - - if (!m_currentImageAcquire) - return; - - auto* const cmdbuf = m_cmdBufs.data()[resourceIx].get(); - cmdbuf->reset(IGPUCommandBuffer::RESET_FLAGS::RELEASE_RESOURCES_BIT); - 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); - } - - // TODO: also need to protect from previous frame still reading while we overwrite UBO - - SMVPParams camData; - SBufferRange camDataRange; - { - const auto viewMatrix = camera.getViewMatrix(); - const auto projectionMatrix = camera.getProjectionMatrix(); - const auto viewProjectionMatrix = camera.getConcatenatedMatrix(); - - hlsl::float32_t3x4 modelMatrix = hlsl::math::linalg::identity(); - - hlsl::float32_t3x4 modelViewMatrix = viewMatrix; - hlsl::float32_t4x4 modelViewProjectionMatrix = viewProjectionMatrix; - - auto modelMat = hlsl::math::linalg::promote_affine<4, 4, 3, 4>(modelMatrix); - - const core::vector3df camPos = camera.getPosition().getAsVector3df(); - - camPos.getAs4Values(camData.cameraPosition); - memcpy(camData.MVP, &modelViewProjectionMatrix[0][0], sizeof(camData.MVP)); - memcpy(camData.M, &modelMat[0][0], sizeof(camData.M)); - memcpy(camData.V, &viewMatrix[0][0], sizeof(camData.V)); - memcpy(camData.P, &projectionMatrix[0][0], sizeof(camData.P)); - { - camDataRange.buffer = cameraBuffer; - camDataRange.size = cameraBuffer->getSize(); - - cmdbuf->updateBuffer(camDataRange, &camData); - } - } - - bool bCaptureTestInitParticles = false; - float32_t4 pressureSolverParams[2]; - SBufferRange gridDataRange; - SBufferRange pParamsRange; - SBufferRange pressureParamsRange; - if (m_shouldInitParticles) // TODO: why on earth is this in `workLoopBody()` and not `onAppInitialized` ? - { - bCaptureTestInitParticles = true; - - { - gridDataRange.size = gridDataBuffer->getSize(); - gridDataRange.buffer = gridDataBuffer; - } - cmdbuf->updateBuffer(gridDataRange, &m_gridData); - - { - pParamsRange.size = pParamsBuffer->getSize(); - pParamsRange.buffer = pParamsBuffer; - } - cmdbuf->updateBuffer(pParamsRange, &m_pRenderParams); - - float a = m_gridData.gridInvCellSize * m_gridData.gridInvCellSize; - float b = 1.f / (2.f * (a * 3)); - pressureSolverParams[0] = float32_t4(b * a, b * a, b * a, -b); - pressureSolverParams[1] = float32_t4(m_gridData.gridInvCellSize); - - { - pressureParamsRange.size = pressureParamsBuffer->getSize(); - pressureParamsRange.buffer = pressureParamsBuffer; - } - cmdbuf->updateBuffer(pressureParamsRange, &pressureSolverParams); - - initializeParticles(cmdbuf); - - transitionGridImageLayouts(cmdbuf); - - // TODO: fat pipeline barrier! The bottom one only ever protected the UBO write. - } - - { - SMemoryBarrier memBarrier; - memBarrier.srcStageMask = PIPELINE_STAGE_FLAGS::ALL_TRANSFER_BITS; - memBarrier.srcAccessMask = ACCESS_FLAGS::MEMORY_WRITE_BITS; - //memBarrier.srcAccessMask = ACCESS_FLAGS::TRANSFER_WRITE_BIT; // after the initialization with compute shaders is moved out - memBarrier.dstStageMask = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT; - memBarrier.dstAccessMask = ACCESS_FLAGS::SHADER_READ_BITS; - cmdbuf->pipelineBarrier(E_DEPENDENCY_FLAGS::EDF_NONE, {.memBarriers = {&memBarrier, 1}}); - } - - // simulation steps - for (uint32_t i = 0; i < m_substepsPerFrame; i++) - { - dispatchUpdateFluidCells(cmdbuf); // particle to grid - dispatchApplyBodyForces(cmdbuf, i == 0); // external forces, e.g. gravity - dispatchApplyDiffusion(cmdbuf); - dispatchApplyPressure(cmdbuf); - dispatchAdvection(cmdbuf); // update/advect fluid - } - - // TODO: remove the compute shader generating vertices, collapse the two barriers around it into one. Need to think about next frame compute/transfer stepping on our toes. - // The pipeline barrier shouldn't really be here and should be expressed through a subpass external dependency - { - SMemoryBarrier memBarrier; - memBarrier.srcStageMask = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT; - memBarrier.srcAccessMask = ACCESS_FLAGS::SHADER_WRITE_BITS; - memBarrier.dstStageMask = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT; - memBarrier.dstAccessMask = ACCESS_FLAGS::SHADER_READ_BITS; - cmdbuf->pipelineBarrier(E_DEPENDENCY_FLAGS::EDF_NONE, {.memBarriers = {&memBarrier, 1}}); - } - - // prepare particle vertices for render - { - const uint64_t bufferAddr[3] = { - particleData.positionBuffer->getDeviceAddress(), - particleData.velocityBuffer->getDeviceAddress(), - particleVertexBuffer->getDeviceAddress() - }; - - cmdbuf->bindComputePipeline(m_genParticleVerticesPipeline.get()); - cmdbuf->pushConstants(m_genParticleVerticesPipeline->getLayout(), IShader::E_SHADER_STAGE::ESS_COMPUTE, 0, 3 * sizeof(uint64_t), bufferAddr); - cmdbuf->bindDescriptorSets(nbl::asset::EPBP_COMPUTE, m_genParticleVerticesPipeline->getLayout(), 1, 1, &m_genVerticesDs.get()); - cmdbuf->dispatch(WorkgroupCountParticles, 1, 1); - } - - { - SMemoryBarrier memBarrier; - memBarrier.srcStageMask = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT; - memBarrier.srcAccessMask = ACCESS_FLAGS::SHADER_WRITE_BITS; - memBarrier.dstStageMask = PIPELINE_STAGE_FLAGS::VERTEX_INPUT_BITS; - memBarrier.dstAccessMask = ACCESS_FLAGS::MEMORY_READ_BITS; - cmdbuf->pipelineBarrier(E_DEPENDENCY_FLAGS::EDF_NONE, {.memBarriers = {&memBarrier, 1}}); - } - - // draw particles - auto* queue = getGraphicsQueue(); - - asset::SViewport viewport; - { - viewport.minDepth = 1.f; - viewport.maxDepth = 0.f; - viewport.x = 0u; - viewport.y = 0u; - viewport.width = m_window->getWidth(); - viewport.height = m_window->getHeight(); - } - cmdbuf->setViewport(0u, 1u, &viewport); - - VkRect2D scissor{ - .offset = { 0, 0 }, - .extent = { m_window->getWidth(), m_window->getHeight() } - }; - cmdbuf->setScissor(0u, 1u, &scissor); - - IGPUCommandBuffer::SRenderpassBeginInfo beginInfo; - VkRect2D currentRenderArea; - const IGPUCommandBuffer::SClearColorValue clearValue = { .float32 = {0.f,0.f,0.f,1.f} }; - const IGPUCommandBuffer::SClearDepthStencilValue depthValue = { .depth = 0.f }; - { - currentRenderArea = - { - .offset = {0,0}, - .extent = {m_window->getWidth(),m_window->getHeight()} - }; - - auto scRes = static_cast(m_surface->getSwapchainResources()); - beginInfo = - { - .framebuffer = scRes->getFramebuffer(m_currentImageAcquire.imageIndex), - .colorClearValues = &clearValue, - .depthStencilClearValues = &depthValue, - .renderArea = currentRenderArea - }; - } - cmdbuf->beginRenderPass(beginInfo, IGPUCommandBuffer::SUBPASS_CONTENTS::INLINE); - - const uint64_t bufferAddr = particleVertexBuffer->getDeviceAddress(); - - cmdbuf->bindGraphicsPipeline(m_graphicsPipeline.get()); - cmdbuf->pushConstants(m_graphicsPipeline->getLayout(), IShader::E_SHADER_STAGE::ESS_VERTEX, 0, sizeof(uint64_t), &bufferAddr); - cmdbuf->bindDescriptorSets(EPBP_GRAPHICS, m_graphicsPipeline->getLayout(), 1, 1, &m_renderDs.get()); - - // TODO: INDEXED and INSTANCED DRAWS! - cmdbuf->draw(numParticles * 6, 1, 0, 0); - - cmdbuf->endRenderPass(); - - // turn into a subpass external dst dependency - { - SMemoryBarrier memBarrier; - memBarrier.srcStageMask = PIPELINE_STAGE_FLAGS::ALL_GRAPHICS_BITS; - memBarrier.srcAccessMask = ACCESS_FLAGS::MEMORY_WRITE_BITS; - memBarrier.dstStageMask = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT; - memBarrier.dstAccessMask = ACCESS_FLAGS::SHADER_READ_BITS; // TODO: also write bits - cmdbuf->pipelineBarrier(E_DEPENDENCY_FLAGS::EDF_NONE, {.memBarriers = {&memBarrier, 1}}); - } - - cmdbuf->endDebugMarker(); - cmdbuf->end(); - - // submit - { - const IQueue::SSubmitInfo::SSemaphoreInfo rendered[] = - { - { - .semaphore = m_renderSemaphore.get(), - .value = ++m_realFrameIx, - .stageMask = PIPELINE_STAGE_FLAGS::ALL_GRAPHICS_BITS - } - }; - { - { - const IQueue::SSubmitInfo::SCommandBufferInfo commandBuffers[] = - { - { .cmdbuf = cmdbuf } - }; - - const IQueue::SSubmitInfo::SSemaphoreInfo acquired[] = - { - { - .semaphore = m_currentImageAcquire.semaphore, - .value = m_currentImageAcquire.acquireCount, - .stageMask = PIPELINE_STAGE_FLAGS::NONE - } - }; - const IQueue::SSubmitInfo infos[] = - { - { - .waitSemaphores = acquired, - .commandBuffers = commandBuffers, - .signalSemaphores = rendered - } - }; - - if (queue->submit(infos) == IQueue::RESULT::SUCCESS) - { - const nbl::video::ISemaphore::SWaitInfo waitInfos[] = - { { - .semaphore = m_renderSemaphore.get(), - .value = m_realFrameIx - } }; - - m_device->blockForSemaphores(waitInfos); // this is not solution, quick wa to not throw validation errors - } - else - --m_realFrameIx; - } - } - - m_surface->present(m_currentImageAcquire.imageIndex, rendered); - } - } - - inline bool keepRunning() override - { - if (m_surface->irrecoverable()) - return false; - - return true; - } - - inline bool onAppTerminated() override - { - return device_base_t::onAppTerminated(); - } - - void dispatchUpdateFluidCells(IGPUCommandBuffer* cmdbuf) - { - { - SMemoryBarrier memBarrier; - memBarrier.srcStageMask = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT; - memBarrier.srcAccessMask = ACCESS_FLAGS::SHADER_WRITE_BITS; - memBarrier.dstStageMask = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT; - memBarrier.dstAccessMask = ACCESS_FLAGS::SHADER_READ_BITS; - cmdbuf->pipelineBarrier(E_DEPENDENCY_FLAGS::EDF_NONE, { .memBarriers = {&memBarrier, 1} }); - } - - // clear velocity stuffs - IGPUCommandBuffer::SClearColorValue clear = {}; - - asset::IImage::SSubresourceRange subresourceRange = {}; - subresourceRange.aspectMask = asset::IImage::E_ASPECT_FLAGS::EAF_COLOR_BIT; - subresourceRange.baseArrayLayer = 0u; - subresourceRange.baseMipLevel = 0u; - subresourceRange.layerCount = 1u; - subresourceRange.levelCount = 1u; - - for (uint32_t i = 0; i < 3; i++) - { - cmdbuf->clearColorImage(velocityFieldImageViews[i]->getCreationParameters().image.get(), - asset::IImage::LAYOUT::GENERAL, &clear, 1, &subresourceRange); - - cmdbuf->clearColorImage(prevVelocityFieldImageViews[i]->getCreationParameters().image.get(), - asset::IImage::LAYOUT::GENERAL, &clear, 1, &subresourceRange); - } - - cmdbuf->clearColorImage(gridParticleCountImageView->getCreationParameters().image.get(), - asset::IImage::LAYOUT::GENERAL, &clear, 1, &subresourceRange); - - { - SMemoryBarrier memBarrier; - memBarrier.srcStageMask = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT; - memBarrier.srcAccessMask = ACCESS_FLAGS::SHADER_WRITE_BITS; - memBarrier.dstStageMask = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT; - memBarrier.dstAccessMask = ACCESS_FLAGS::SHADER_READ_BITS; - cmdbuf->pipelineBarrier(E_DEPENDENCY_FLAGS::EDF_NONE, { .memBarriers = {&memBarrier, 1} }); - } - - const uint64_t bufferAddr[2] = { - particleData.positionBuffer->getDeviceAddress(), - particleData.velocityBuffer->getDeviceAddress() - }; - - cmdbuf->bindComputePipeline(m_accumulateWeightsPipeline.get()); - cmdbuf->pushConstants(m_accumulateWeightsPipeline->getLayout(), IShader::E_SHADER_STAGE::ESS_COMPUTE, 0, 2 * sizeof(uint64_t), bufferAddr); - cmdbuf->bindDescriptorSets(nbl::asset::EPBP_COMPUTE, m_accumulateWeightsPipeline->getLayout(), 1, 1, &m_accumulateWeightsDs.get()); - cmdbuf->dispatch(WorkgroupCountParticles, 1, 1); - - { - SMemoryBarrier memBarrier; - memBarrier.srcStageMask = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT; - memBarrier.srcAccessMask = ACCESS_FLAGS::SHADER_WRITE_BITS; - memBarrier.dstStageMask = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT; - memBarrier.dstAccessMask = ACCESS_FLAGS::SHADER_READ_BITS; - cmdbuf->pipelineBarrier(E_DEPENDENCY_FLAGS::EDF_NONE, {.memBarriers = {&memBarrier, 1}}); - } - - cmdbuf->bindComputePipeline(m_updateFluidCellsPipeline.get()); - cmdbuf->bindDescriptorSets(nbl::asset::EPBP_COMPUTE, m_updateFluidCellsPipeline->getLayout(), 1, 1, &m_updateFluidCellsDs.get()); - cmdbuf->dispatch(WorkgroupCountGrid.x, WorkgroupCountGrid.y, WorkgroupCountGrid.z); - - { - SMemoryBarrier memBarrier; - memBarrier.srcStageMask = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT; - memBarrier.srcAccessMask = ACCESS_FLAGS::SHADER_WRITE_BITS; - memBarrier.dstStageMask = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT; - memBarrier.dstAccessMask = ACCESS_FLAGS::SHADER_READ_BITS; - cmdbuf->pipelineBarrier(E_DEPENDENCY_FLAGS::EDF_NONE, {.memBarriers = {&memBarrier, 1}}); - } - - cmdbuf->bindComputePipeline(m_updateNeighborCellsPipeline.get()); - cmdbuf->bindDescriptorSets(nbl::asset::EPBP_COMPUTE, m_updateNeighborCellsPipeline->getLayout(), 1, 1, &m_updateNeighborCellsDs.get()); - cmdbuf->dispatch(WorkgroupCountGrid.x, WorkgroupCountGrid.y, WorkgroupCountGrid.z); - } - - void dispatchApplyBodyForces(IGPUCommandBuffer* cmdbuf, bool isFirstSubstep) - { - { - SMemoryBarrier memBarrier; - memBarrier.srcStageMask = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT; - memBarrier.srcAccessMask = ACCESS_FLAGS::SHADER_WRITE_BITS; - memBarrier.dstStageMask = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT; - memBarrier.dstAccessMask = ACCESS_FLAGS::SHADER_READ_BITS; - cmdbuf->pipelineBarrier(E_DEPENDENCY_FLAGS::EDF_NONE, {.memBarriers = {&memBarrier, 1}}); - } - - cmdbuf->bindComputePipeline(m_applyBodyForcesPipeline.get()); - cmdbuf->bindDescriptorSets(nbl::asset::EPBP_COMPUTE, m_applyBodyForcesPipeline->getLayout(), 1, 1, &m_applyForcesDs.get()); - cmdbuf->dispatch(WorkgroupCountGrid.x, WorkgroupCountGrid.y, WorkgroupCountGrid.z); - } - - void dispatchApplyDiffusion(IGPUCommandBuffer* cmdbuf) - { - if (viscosity <= 0.f) - return; - - { - SMemoryBarrier memBarrier; - memBarrier.srcStageMask = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT; - memBarrier.srcAccessMask = ACCESS_FLAGS::SHADER_WRITE_BITS; - memBarrier.dstStageMask = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT; - memBarrier.dstAccessMask = ACCESS_FLAGS::SHADER_READ_BITS; - cmdbuf->pipelineBarrier(E_DEPENDENCY_FLAGS::EDF_NONE, {.memBarriers = {&memBarrier, 1}}); - } - - cmdbuf->bindComputePipeline(m_axisCellsPipeline.get()); - cmdbuf->bindDescriptorSets(nbl::asset::EPBP_COMPUTE, m_axisCellsPipeline->getLayout(), 1, 1, &m_axisCellsDs.get()); - cmdbuf->dispatch(WorkgroupCountGrid.x, WorkgroupCountGrid.y, WorkgroupCountGrid.z); - - { - SMemoryBarrier memBarrier; - memBarrier.srcStageMask = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT; - memBarrier.srcAccessMask = ACCESS_FLAGS::SHADER_WRITE_BITS; - memBarrier.dstStageMask = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT; - memBarrier.dstAccessMask = ACCESS_FLAGS::SHADER_READ_BITS; - cmdbuf->pipelineBarrier(E_DEPENDENCY_FLAGS::EDF_NONE, {.memBarriers = {&memBarrier, 1}}); - } - - cmdbuf->bindComputePipeline(m_neighborAxisCellsPipeline.get()); - cmdbuf->bindDescriptorSets(nbl::asset::EPBP_COMPUTE, m_neighborAxisCellsPipeline->getLayout(), 1, 1, &m_neighborAxisCellsDs.get()); - cmdbuf->dispatch(WorkgroupCountGrid.x, WorkgroupCountGrid.y, WorkgroupCountGrid.z); - - float a = viscosity * deltaTime; - float32_t3 b = float32_t3(m_gridData.gridInvCellSize * m_gridData.gridInvCellSize); - float c = 1.f / (1.f + 2.f *(b.x + b.y + b.z) * a); - float32_t4 diffParam = {}; // as push constant - diffParam.xyz = a * b * c; - diffParam.w = c; - - cmdbuf->bindComputePipeline(m_iterateDiffusionPipeline.get()); - cmdbuf->pushConstants(m_iterateDiffusionPipeline->getLayout(), IShader::E_SHADER_STAGE::ESS_COMPUTE, 0, sizeof(float32_t4), &diffParam); - cmdbuf->bindDescriptorSets(nbl::asset::EPBP_COMPUTE, m_iterateDiffusionPipeline->getLayout(), 1, 1, &m_diffusionDs.get()); - for (int i = 0; i < diffusionIterations; i++) - { - { - SMemoryBarrier memBarrier; - memBarrier.srcStageMask = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT; - memBarrier.srcAccessMask = ACCESS_FLAGS::SHADER_WRITE_BITS; - memBarrier.dstStageMask = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT; - memBarrier.dstAccessMask = ACCESS_FLAGS::SHADER_READ_BITS; - cmdbuf->pipelineBarrier(E_DEPENDENCY_FLAGS::EDF_NONE, {.memBarriers = {&memBarrier, 1}}); - } - - cmdbuf->dispatch(WorkgroupCountGrid.x, WorkgroupCountGrid.y, WorkgroupCountGrid.z); - } - - { - SMemoryBarrier memBarrier; - memBarrier.srcStageMask = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT; - memBarrier.srcAccessMask = ACCESS_FLAGS::SHADER_WRITE_BITS; - memBarrier.dstStageMask = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT; - memBarrier.dstAccessMask = ACCESS_FLAGS::SHADER_READ_BITS; - cmdbuf->pipelineBarrier(E_DEPENDENCY_FLAGS::EDF_NONE, {.memBarriers = {&memBarrier, 1}}); - } - - cmdbuf->bindComputePipeline(m_diffusionPipeline.get()); - cmdbuf->pushConstants(m_diffusionPipeline->getLayout(), IShader::E_SHADER_STAGE::ESS_COMPUTE, 0, sizeof(float32_t4), &diffParam); - cmdbuf->bindDescriptorSets(nbl::asset::EPBP_COMPUTE, m_diffusionPipeline->getLayout(), 1, 1, &m_diffusionDs.get()); - cmdbuf->dispatch(WorkgroupCountGrid.x, WorkgroupCountGrid.y, WorkgroupCountGrid.z); - } - - void dispatchApplyPressure(IGPUCommandBuffer* cmdbuf) - { - { - SMemoryBarrier memBarrier; - memBarrier.srcStageMask = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT; - memBarrier.srcAccessMask = ACCESS_FLAGS::SHADER_WRITE_BITS; - memBarrier.dstStageMask = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT; - memBarrier.dstAccessMask = ACCESS_FLAGS::SHADER_READ_BITS; - cmdbuf->pipelineBarrier(E_DEPENDENCY_FLAGS::EDF_NONE, {.memBarriers = {&memBarrier, 1}}); - } - - cmdbuf->bindComputePipeline(m_calcDivergencePipeline.get()); - cmdbuf->bindDescriptorSets(nbl::asset::EPBP_COMPUTE, m_calcDivergencePipeline->getLayout(), 1, 1, &m_calcDivergenceDs.get()); - cmdbuf->dispatch(WorkgroupCountGrid.x, WorkgroupCountGrid.y, WorkgroupCountGrid.z); - - cmdbuf->bindComputePipeline(m_iteratePressurePipeline.get()); - cmdbuf->bindDescriptorSets(nbl::asset::EPBP_COMPUTE, m_iteratePressurePipeline->getLayout(), 1, 1, &m_iteratePressureDs.get()); - for (int i = 0; i < pressureSolverIterations; i++) - { - { - SMemoryBarrier memBarrier; - memBarrier.srcStageMask = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT; - memBarrier.srcAccessMask = ACCESS_FLAGS::SHADER_WRITE_BITS; - memBarrier.dstStageMask = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT; - memBarrier.dstAccessMask = ACCESS_FLAGS::SHADER_READ_BITS; - cmdbuf->pipelineBarrier(E_DEPENDENCY_FLAGS::EDF_NONE, {.memBarriers = {&memBarrier, 1}}); - } - - cmdbuf->dispatch(WorkgroupCountGrid.x, WorkgroupCountGrid.y, WorkgroupCountGrid.z); - } - - { - SMemoryBarrier memBarrier; - memBarrier.srcStageMask = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT; - memBarrier.srcAccessMask = ACCESS_FLAGS::SHADER_WRITE_BITS; - memBarrier.dstStageMask = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT; - memBarrier.dstAccessMask = ACCESS_FLAGS::SHADER_READ_BITS; - cmdbuf->pipelineBarrier(E_DEPENDENCY_FLAGS::EDF_NONE, {.memBarriers = {&memBarrier, 1}}); - } - - cmdbuf->bindComputePipeline(m_updateVelPsPipeline.get()); - cmdbuf->bindDescriptorSets(nbl::asset::EPBP_COMPUTE, m_updateVelPsPipeline->getLayout(), 1, 1, &m_updateVelPsDs.get()); - cmdbuf->dispatch(WorkgroupCountGrid.x, WorkgroupCountGrid.y, WorkgroupCountGrid.z); - } - - void dispatchAdvection(IGPUCommandBuffer* cmdbuf) - { - { - SMemoryBarrier memBarrier; - memBarrier.srcStageMask = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT; - memBarrier.srcAccessMask = ACCESS_FLAGS::SHADER_WRITE_BITS; - memBarrier.dstStageMask = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT; - memBarrier.dstAccessMask = ACCESS_FLAGS::SHADER_READ_BITS; - cmdbuf->pipelineBarrier(E_DEPENDENCY_FLAGS::EDF_NONE, {.memBarriers = {&memBarrier, 1}}); - } - - const uint64_t bufferAddr[2] = { - particleData.positionBuffer->getDeviceAddress(), - particleData.velocityBuffer->getDeviceAddress() - }; - - cmdbuf->bindComputePipeline(m_advectParticlesPipeline.get()); - cmdbuf->pushConstants(m_advectParticlesPipeline->getLayout(), IShader::E_SHADER_STAGE::ESS_COMPUTE, 0, 2 * sizeof(uint64_t), bufferAddr); - cmdbuf->bindDescriptorSets(nbl::asset::EPBP_COMPUTE, m_advectParticlesPipeline->getLayout(), 1, 1, &m_advectParticlesDs.get()); - cmdbuf->dispatch(WorkgroupCountParticles, 1, 1); - } - -private: - void usePreset(SimPresets preset) - { - m_gridData.gridCellSize = 0.25f; - m_gridData.gridInvCellSize = 1.f / m_gridData.gridCellSize; - - switch (preset) - { - case LONG_BOX: - m_gridData.gridSize = int32_t4{48, 24, 24, 0}; - m_gridData.particleInitMin = int32_t4{4, 4, 4, 0}; - m_gridData.particleInitMax = int32_t4{20, 20, 20, 0}; - break; - case CENTER_DROP: - default: - m_gridData.gridSize = int32_t4{32, 32, 32, 0}; - m_gridData.particleInitMin = int32_t4{4, 12, 4, 0}; - m_gridData.particleInitMax = int32_t4{28, 28, 28, 0}; - break; - } - - fillGridData(); - } - - void fillGridData() - { - m_gridData.particleInitSize = m_gridData.particleInitMax - m_gridData.particleInitMin; - float32_t4 simAreaSize = m_gridData.gridSize; - simAreaSize *= m_gridData.gridCellSize; - m_gridData.worldMin = float32_t4(0.f); - m_gridData.worldMax = simAreaSize; - numGridCells = m_gridData.gridSize.x * m_gridData.gridSize.y * m_gridData.gridSize.z; - numParticles = m_gridData.particleInitSize.x * m_gridData.particleInitSize.y * m_gridData.particleInitSize.z * particlesPerCell; - } - - template - smart_refctd_ptr loadPrecompiledShader() - { - IAssetLoader::SAssetLoadParams lparams = {}; - lparams.logger = m_logger.get(); - lparams.workingDirectory = "app_resources"; - auto key = nbl::this_example::builtin::build::get_spirv_key(m_device.get()); - auto bundle = m_assetMgr->getAsset(key.data(), lparams); - if (bundle.getContents().empty() || bundle.getAssetType() != IAsset::ET_SHADER) - { - m_logger->log("Failed to find shader with key '%s'.", ILogger::ELL_ERROR, ShaderKey); - exit(-1); - } - - const auto assets = bundle.getContents(); - assert(assets.size() == 1); - smart_refctd_ptr shader = IAsset::castDown(assets[0]); - - return shader; - } - - // TODO: there's a method in IUtilities for this - bool createBuffer(smart_refctd_ptr& buffer, video::IGPUBuffer::SCreationParams& params, - core::bitflag allocFlags = IDeviceMemoryAllocation::E_MEMORY_ALLOCATE_FLAGS::EMAF_NONE) - { - buffer = m_device->createBuffer(std::move(params)); - if (!buffer) - return logFail("Failed to create GPU buffer of size %d!\n", params.size); - - video::IDeviceMemoryBacked::SDeviceMemoryRequirements reqs = buffer->getMemoryReqs(); - reqs.memoryTypeBits &= m_physicalDevice->getDeviceLocalMemoryTypeBits(); - - auto bufMem = m_device->allocate(reqs, buffer.get(), allocFlags); - if (!bufMem.isValid()) - return logFail("Failed to allocate device memory compatible with gpu buffer!\n"); - - return true; - } - - bool createGridTexture(smart_refctd_ptr& imageView, asset::E_FORMAT format, asset::VkExtent3D extent, - core::bitflag usage, const std::string& debugName = "", - core::bitflag flags = asset::IImage::E_CREATE_FLAGS::ECF_NONE) - { - IGPUImage::SCreationParams imgInfo; - imgInfo.format = format; - imgInfo.type = IGPUImage::ET_3D; - imgInfo.extent = extent; - imgInfo.mipLevels = 1u; - imgInfo.arrayLayers = 1u; - imgInfo.samples = asset::ICPUImage::ESCF_1_BIT; - imgInfo.flags = flags; - imgInfo.usage = usage; - imgInfo.tiling = IGPUImage::TILING::OPTIMAL; - - auto image = m_device->createImage(std::move(imgInfo)); - auto imageMemReqs = image->getMemoryReqs(); - imageMemReqs.memoryTypeBits &= m_physicalDevice->getDeviceLocalMemoryTypeBits(); - m_device->allocate(imageMemReqs, image.get()); - - if (!debugName.empty()) - image->setObjectDebugName(debugName.c_str()); - - IGPUImageView::SCreationParams imgViewInfo; - imgViewInfo.image = std::move(image); - imgViewInfo.format = format; - imgViewInfo.viewType = IGPUImageView::ET_3D; - imgViewInfo.flags = IGPUImageView::E_CREATE_FLAGS::ECF_NONE; - imgViewInfo.subresourceRange.aspectMask = asset::IImage::E_ASPECT_FLAGS::EAF_COLOR_BIT; - imgViewInfo.subresourceRange.baseArrayLayer = 0u; - imgViewInfo.subresourceRange.baseMipLevel = 0u; - imgViewInfo.subresourceRange.layerCount = 1u; - imgViewInfo.subresourceRange.levelCount = 1u; - - imageView = m_device->createImageView(std::move(imgViewInfo)); - - return true; - } - - bool initGraphicsPipeline() - { - m_renderSemaphore = m_device->createSemaphore(m_realFrameIx); - if (!m_renderSemaphore) - return logFail("Failed to create render semaphore!\n"); - - ISwapchain::SCreationParams swapchainParams{ - .surface = m_surface->getSurface() - }; - if (!swapchainParams.deduceFormat(m_physicalDevice)) - return logFail("Could not choose a surface format for the swapchain!\n"); - - const static IGPURenderpass::SCreationParams::SSubpassDependency dependencies[] = { - { - .srcSubpass = IGPURenderpass::SCreationParams::SSubpassDependency::External, - .dstSubpass = 0, - .memoryBarrier = { - .srcStageMask = PIPELINE_STAGE_FLAGS::LATE_FRAGMENT_TESTS_BIT, - .srcAccessMask = ACCESS_FLAGS::DEPTH_STENCIL_ATTACHMENT_WRITE_BIT, - .dstStageMask = PIPELINE_STAGE_FLAGS::EARLY_FRAGMENT_TESTS_BIT, - .dstAccessMask = ACCESS_FLAGS::DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | ACCESS_FLAGS::DEPTH_STENCIL_ATTACHMENT_READ_BIT - } - }, - // color from ATTACHMENT_OPTIMAL to PRESENT_SRC - { - .srcSubpass = 0, - .dstSubpass = IGPURenderpass::SCreationParams::SSubpassDependency::External, - .memoryBarrier = { - .srcStageMask = PIPELINE_STAGE_FLAGS::COLOR_ATTACHMENT_OUTPUT_BIT, - .srcAccessMask = ACCESS_FLAGS::COLOR_ATTACHMENT_WRITE_BIT - } - }, - IGPURenderpass::SCreationParams::DependenciesEnd - }; - - auto scResources = std::make_unique(m_device.get(), EF_D16_UNORM, swapchainParams.surfaceFormat.format, dependencies); - auto* renderpass = scResources->getRenderpass(); - if (!renderpass) - return logFail("Failed to create renderpass!\n"); - - auto queue = getGraphicsQueue(); - if (!m_surface || !m_surface->init(queue, std::move(scResources), swapchainParams.sharedParams)) - return logFail("Could not create window & surface or initialize surface\n"); - - m_cmdPool = m_device->createCommandPool(queue->getFamilyIndex(), IGPUCommandPool::CREATE_FLAGS::RESET_COMMAND_BUFFER_BIT); - for (auto i = 0u; i < MaxFramesInFlight; i++) - { - if (!m_cmdPool) - return logFail("Couldn't create command pool\n"); - - if (!m_cmdPool->createCommandBuffers(IGPUCommandPool::BUFFER_LEVEL::PRIMARY, { m_cmdBufs.data() + i, 1 })) - return logFail("Couldn't create command buffer\n"); - } - - m_winMgr->setWindowSize(m_window.get(), WIN_WIDTH, WIN_HEIGHT); - m_surface->recreateSwapchain(); - - // init shaders and pipeline - - auto loadPrecompiledShader = [&]() -> smart_refctd_ptr - { - IAssetLoader::SAssetLoadParams lparams = {}; - lparams.logger = m_logger.get(); - lparams.workingDirectory = "app_resources"; - auto key = nbl::this_example::builtin::build::get_spirv_key(m_device.get()); - auto bundle = m_assetMgr->getAsset(key.data(), lparams); - if (bundle.getContents().empty() || bundle.getAssetType() != IAsset::ET_SHADER) - { - m_logger->log("Failed to find shader with key '%s'.", ILogger::ELL_ERROR, ShaderKey); - exit(-1); - } - - const auto assets = bundle.getContents(); - assert(assets.size() == 1); - smart_refctd_ptr shader = IAsset::castDown(assets[0]); - - return shader; - }; - auto vs = loadPrecompiledShader.operator()<"fluidParticles_vertex">(); // "app_resources/fluidParticles.vertex.hlsl" - auto fs = loadPrecompiledShader.operator()<"fluidParticles_fragment">(); // "app_resources/fluidParticles.fragment.hlsl" - - smart_refctd_ptr descriptorSetLayout1; - { - // init descriptors - video::IGPUDescriptorSetLayout::SBinding bindingsSet1[] = { - { - .binding = 0u, - .type = asset::IDescriptor::E_TYPE::ET_UNIFORM_BUFFER, - .createFlags = IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - .stageFlags = asset::IShader::E_SHADER_STAGE::ESS_FRAGMENT, - .count = 1u, - } - }; - descriptorSetLayout1 = m_device->createDescriptorSetLayout(bindingsSet1); - if (!descriptorSetLayout1) - return logFail("Failed to Create Render Descriptor Layout 1"); - - const auto maxDescriptorSets = ICPUPipelineLayout::DESCRIPTOR_SET_COUNT; - const std::array dscLayoutPtrs = { - nullptr, - descriptorSetLayout1.get(), - nullptr, - nullptr - }; - m_renderDsPool = m_device->createDescriptorPoolForDSLayouts(IDescriptorPool::ECF_UPDATE_AFTER_BIND_BIT, std::span(dscLayoutPtrs.begin(), dscLayoutPtrs.end())); - m_renderDs = m_renderDsPool->createDescriptorSet(descriptorSetLayout1); - } - - // write descriptors - { - IGPUDescriptorSet::SDescriptorInfo camInfo; - camInfo.desc = smart_refctd_ptr(cameraBuffer); - camInfo.info.buffer = {.offset = 0, .size = cameraBuffer->getSize()}; - IGPUDescriptorSet::SWriteDescriptorSet writes[1] = { - {.dstSet = m_renderDs.get(), .binding = 0, .arrayElement = 0, .count = 1, .info = &camInfo} - }; - m_device->updateDescriptorSets(std::span(writes, 1), {}); - } - - SBlendParams blendParams = {}; - blendParams.logicOp = ELO_NO_OP; - blendParams.blendParams[0u].colorWriteMask = (1u << 0u) | (1u << 1u) | (1u << 2u) | (1u << 3u); - - { - const asset::SPushConstantRange pcRange = { .stageFlags = IShader::E_SHADER_STAGE::ESS_VERTEX, .offset = 0, .size = sizeof(uint64_t) }; - const auto pipelineLayout = m_device->createPipelineLayout({ &pcRange , 1 }, nullptr, smart_refctd_ptr(descriptorSetLayout1), nullptr, nullptr); - - SRasterizationParams rasterizationParams{}; - rasterizationParams.faceCullingMode = EFCM_NONE; - rasterizationParams.depthWriteEnable = true; - - IGPUGraphicsPipeline::SCreationParams params[1] = {}; - params[0].layout = pipelineLayout.get(); - params[0].vertexShader = { .shader = vs.get(), .entryPoint = "main", }; - params[0].fragmentShader = { .shader = fs.get(), .entryPoint = "main", }; - params[0].cached = { - .vertexInput = { - }, - .primitiveAssembly = { - .primitiveType = E_PRIMITIVE_TOPOLOGY::EPT_TRIANGLE_LIST, - }, - .rasterization = rasterizationParams, - .blend = blendParams, - }; - params[0].renderpass = renderpass; - - if (!m_device->createGraphicsPipelines(nullptr, params, &m_graphicsPipeline)) - return logFail("Graphics pipeline creation failed"); - } - - return true; - } - - void mouseProcess(const nbl::ui::IMouseEventChannel::range_t& events) - { - for (auto eventIt = events.begin(); eventIt != events.end(); eventIt++) - { - auto ev = *eventIt; - - // do nothing - } - } - - - // in-loop functions - void initializeParticles(IGPUCommandBuffer* cmdbuf) - { - { - SMemoryBarrier memBarrier; - memBarrier.srcStageMask = PIPELINE_STAGE_FLAGS::ALL_COMMANDS_BITS; - memBarrier.srcAccessMask = ACCESS_FLAGS::MEMORY_READ_BITS | ACCESS_FLAGS::MEMORY_WRITE_BITS;; - memBarrier.dstStageMask = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT; - memBarrier.dstAccessMask = ACCESS_FLAGS::SHADER_READ_BITS; - cmdbuf->pipelineBarrier(E_DEPENDENCY_FLAGS::EDF_NONE, {.memBarriers = {&memBarrier, 1}}); - } - - const uint64_t bufferAddr[2] = { - particleData.positionBuffer->getDeviceAddress(), - particleData.velocityBuffer->getDeviceAddress() - }; - - cmdbuf->bindComputePipeline(m_initParticlePipeline.get()); - cmdbuf->pushConstants(m_initParticlePipeline->getLayout(), IShader::E_SHADER_STAGE::ESS_COMPUTE, 0, 2 * sizeof(uint64_t), bufferAddr); - cmdbuf->bindDescriptorSets(nbl::asset::EPBP_COMPUTE, m_initParticlePipeline->getLayout(), 1, 1, &m_initParticleDs.get()); - cmdbuf->dispatch(WorkgroupCountParticles, 1, 1); - - m_shouldInitParticles = false; - } - - void transitionGridImageLayouts(IGPUCommandBuffer* cmdbuf) - { - // transition layouts, only after after initialization - auto fillGridBarrierInfo = [&](IGPUCommandBuffer::SPipelineBarrierDependencyInfo::image_barrier_t& barrier, - smart_refctd_ptr& imageView) -> void - { - barrier.barrier = { - .dep = { - .srcStageMask = PIPELINE_STAGE_FLAGS::NONE, - .srcAccessMask = ACCESS_FLAGS::NONE, - .dstStageMask = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT, - .dstAccessMask = ACCESS_FLAGS::SHADER_WRITE_BITS - } - }; - barrier.image = imageView->getCreationParameters().image.get(); - barrier.subresourceRange = { - .aspectMask = IImage::EAF_COLOR_BIT, - .baseMipLevel = 0u, - .levelCount = 1u, - .baseArrayLayer = 0u, - .layerCount = 1u - }; - barrier.oldLayout = IImage::LAYOUT::UNDEFINED; - barrier.newLayout = IImage::LAYOUT::GENERAL; - }; - - uint32_t count = 0; - IGPUCommandBuffer::SPipelineBarrierDependencyInfo::image_barrier_t imageBarriers[14]; - for (uint32_t i = 0; i < 3; i++) - { - fillGridBarrierInfo(imageBarriers[count++], velocityFieldImageViews[i]); - fillGridBarrierInfo(imageBarriers[count++], prevVelocityFieldImageViews[i]); - } - fillGridBarrierInfo(imageBarriers[count++], gridParticleCountImageView); - - fillGridBarrierInfo(imageBarriers[count++], gridCellMaterialImageView); - fillGridBarrierInfo(imageBarriers[count++], tempCellMaterialImageView); - - fillGridBarrierInfo(imageBarriers[count++], gridAxisCellMaterialImageView); - fillGridBarrierInfo(imageBarriers[count++], tempAxisCellMaterialImageView); - - fillGridBarrierInfo(imageBarriers[count++], gridDiffusionImageView); - - fillGridBarrierInfo(imageBarriers[count++], pressureImageView); - fillGridBarrierInfo(imageBarriers[count++], divergenceImageView); - - cmdbuf->pipelineBarrier(E_DEPENDENCY_FLAGS::EDF_NONE, { .imgBarriers = imageBarriers }); - } - - - smart_refctd_ptr m_window; - smart_refctd_ptr> m_surface; - smart_refctd_ptr m_graphicsPipeline; - smart_refctd_ptr m_renderSemaphore; - smart_refctd_ptr m_cmdPool; - std::array, MaxFramesInFlight> m_cmdBufs; - uint64_t m_realFrameIx : 59 = 0; - ISimpleManagedSurface::SAcquireResult m_currentImageAcquire = {}; - - smart_refctd_ptr m_renderDsPool; - smart_refctd_ptr m_renderDs; - - // simulation compute shaders - // TODO: unsure many of the axis-cell material pipelines need to exist - smart_refctd_ptr m_initParticlePipeline; - - smart_refctd_ptr m_accumulateWeightsPipeline; - smart_refctd_ptr m_updateFluidCellsPipeline; - smart_refctd_ptr m_updateNeighborCellsPipeline; - - smart_refctd_ptr m_applyBodyForcesPipeline; - - smart_refctd_ptr m_axisCellsPipeline; - smart_refctd_ptr m_neighborAxisCellsPipeline; - smart_refctd_ptr m_iterateDiffusionPipeline; - smart_refctd_ptr m_diffusionPipeline; - - smart_refctd_ptr m_calcDivergencePipeline; - smart_refctd_ptr m_iteratePressurePipeline; - smart_refctd_ptr m_updateVelPsPipeline; - - smart_refctd_ptr m_advectParticlesPipeline; - smart_refctd_ptr m_genParticleVerticesPipeline; - - // descriptors - // TODO: why does every single descriptor set have its own pool!? - smart_refctd_ptr m_initParticlePool; - smart_refctd_ptr m_initParticleDs; - - smart_refctd_ptr m_accumulateWeightsPool; - smart_refctd_ptr m_accumulateWeightsDs; - smart_refctd_ptr m_updateFluidCellsPool; - smart_refctd_ptr m_updateFluidCellsDs; - smart_refctd_ptr m_updateNeighborCellsPool; - smart_refctd_ptr m_updateNeighborCellsDs; - - smart_refctd_ptr m_applyForcesPool; - smart_refctd_ptr m_applyForcesDs; - - smart_refctd_ptr m_axisCellsPool; - smart_refctd_ptr m_axisCellsDs; - smart_refctd_ptr m_neighborAxisCellsPool; - smart_refctd_ptr m_neighborAxisCellsDs; - smart_refctd_ptr m_diffusionPool; - smart_refctd_ptr m_diffusionDs; - - smart_refctd_ptr m_calcDivergencePool; - smart_refctd_ptr m_calcDivergenceDs; - smart_refctd_ptr m_iteratePressurePool; - smart_refctd_ptr m_iteratePressureDs; - smart_refctd_ptr m_updateVelPsPool; - smart_refctd_ptr m_updateVelPsDs; - - smart_refctd_ptr m_advectParticlesPool; - smart_refctd_ptr m_advectParticlesDs; - smart_refctd_ptr m_genVerticesPool; - smart_refctd_ptr m_genVerticesDs; - - // input system - smart_refctd_ptr m_inputSystem; - InputSystem::ChannelReader mouse; - InputSystem::ChannelReader keyboard; - - Camera camera = Camera(core::vectorSIMDf(0,0,0), core::vectorSIMDf(0,0,0), hlsl::float32_t4x4()); - video::CDumbPresentationOracle oracle; - - bool m_shouldInitParticles = true; - - // simulation constants - size_t WorkgroupCountParticles; - uint32_t3 WorkgroupCountGrid; - uint32_t m_substepsPerFrame = 1; - SGridData m_gridData; - SParticleRenderParams m_pRenderParams; - uint32_t particlesPerCell = 8; - uint32_t numParticles; - uint32_t numGridCells; - - const float viscosity = 0.f; - const uint32_t diffusionIterations = 5; - const uint32_t pressureSolverIterations = 5; - - // buffers - smart_refctd_ptr cameraBuffer; - - struct ParticleData - { - smart_refctd_ptr positionBuffer; - smart_refctd_ptr velocityBuffer; - }; - ParticleData particleData; - - smart_refctd_ptr pParamsBuffer; // SParticleRenderParams - // TODO: remove! - smart_refctd_ptr particleVertexBuffer; // VertexInfo * 6 vertices - - smart_refctd_ptr gridDataBuffer; // SGridData - smart_refctd_ptr pressureParamsBuffer; // SPressureSolverParams - smart_refctd_ptr gridParticleCountImageView; // uint - smart_refctd_ptr gridCellMaterialImageView; // uint, fluid or solid - - std::array, 3> velocityFieldImageViews; // float * 3 (per axis) - std::array, 3> prevVelocityFieldImageViews; // float * 3 - smart_refctd_ptr velocityFieldSampler; - - std::array, 3> velocityFieldUintViews; - std::array, 3> prevVelocityFieldUintViews; - - smart_refctd_ptr gridDiffusionImageView; // float4 - smart_refctd_ptr gridAxisCellMaterialImageView; // uint4 - smart_refctd_ptr divergenceImageView; // float - smart_refctd_ptr pressureImageView; // float - - smart_refctd_ptr tempCellMaterialImageView; // uint, fluid or solid - smart_refctd_ptr tempAxisCellMaterialImageView; // uint4 -}; - -NBL_MAIN_FUNC(FLIPFluidsApp) \ No newline at end of file diff --git a/71_RayTracingPipeline/CMakeLists.txt b/71_RayTracingPipeline/CMakeLists.txt deleted file mode 100644 index 250f7444e..000000000 --- a/71_RayTracingPipeline/CMakeLists.txt +++ /dev/null @@ -1,120 +0,0 @@ -include(common RESULT_VARIABLE RES) -if(NOT RES) - message(FATAL_ERROR "common.cmake not found. Should be in {repo_root}/cmake directory") -endif() - -if(NBL_BUILD_IMGUI) - set(NBL_INCLUDE_SERACH_DIRECTORIES - "${CMAKE_CURRENT_SOURCE_DIR}/include" - ) - - list(APPEND NBL_LIBRARIES - imtestengine - "${NBL_EXT_IMGUI_UI_LIB}" - Nabla::ext::FullScreenTriangle - ) - - nbl_create_executable_project("" "" "${NBL_INCLUDE_SERACH_DIRECTORIES}" "${NBL_LIBRARIES}" "${NBL_EXECUTABLE_PROJECT_CREATION_PCH_TARGET}") - - if(NBL_EMBED_BUILTIN_RESOURCES) - set(_BR_TARGET_ ${EXECUTABLE_NAME}_builtinResourceData) - set(RESOURCE_DIR "app_resources") - - get_filename_component(_SEARCH_DIRECTORIES_ "${CMAKE_CURRENT_SOURCE_DIR}" ABSOLUTE) - get_filename_component(_OUTPUT_DIRECTORY_SOURCE_ "${CMAKE_CURRENT_BINARY_DIR}/src" ABSOLUTE) - get_filename_component(_OUTPUT_DIRECTORY_HEADER_ "${CMAKE_CURRENT_BINARY_DIR}/include" ABSOLUTE) - - file(GLOB_RECURSE BUILTIN_RESOURCE_FILES RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}/${RESOURCE_DIR}" "${CMAKE_CURRENT_SOURCE_DIR}/${RESOURCE_DIR}/*") - foreach(RES_FILE ${BUILTIN_RESOURCE_FILES}) - LIST_BUILTIN_RESOURCE(RESOURCES_TO_EMBED "${RES_FILE}") - endforeach() - - ADD_CUSTOM_BUILTIN_RESOURCES(${_BR_TARGET_} RESOURCES_TO_EMBED "${_SEARCH_DIRECTORIES_}" "${RESOURCE_DIR}" "nbl::this_example::builtin" "${_OUTPUT_DIRECTORY_HEADER_}" "${_OUTPUT_DIRECTORY_SOURCE_}") - - LINK_BUILTIN_RESOURCES_TO_TARGET(${EXECUTABLE_NAME} ${_BR_TARGET_}) - endif() -endif() - -set(OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/auto-gen") - -set(SM 6_8) -set(JSON [=[ -[ - { - "INPUT": "app_resources/raytrace.rgen.hlsl", - "KEY": "raytrace_rgen", - }, - { - "INPUT": "app_resources/raytrace.rchit.hlsl", - "KEY": "raytrace_rchit", - }, - { - "INPUT": "app_resources/raytrace_procedural.rchit.hlsl", - "KEY": "raytrace_procedural_rchit", - }, - { - "INPUT": "app_resources/raytrace.rint.hlsl", - "KEY": "raytrace_rint", - }, - { - "INPUT": "app_resources/raytrace.rahit.hlsl", - "KEY": "raytrace_rahit", - }, - { - "INPUT": "app_resources/raytrace_shadow.rahit.hlsl", - "KEY": "raytrace_shadow_rahit", - }, - { - "INPUT": "app_resources/raytrace.rmiss.hlsl", - "KEY": "raytrace_rmiss", - }, - { - "INPUT": "app_resources/raytrace_shadow.rmiss.hlsl", - "KEY": "raytrace_shadow_rmiss", - }, - { - "INPUT": "app_resources/light_directional.rcall.hlsl", - "KEY": "light_directional_rcall", - }, - { - "INPUT": "app_resources/light_point.rcall.hlsl", - "KEY": "light_point_rcall", - }, - { - "INPUT": "app_resources/light_spot.rcall.hlsl", - "KEY": "light_spot_rcall", - }, - { - "INPUT": "app_resources/present.frag.hlsl", - "KEY": "present_frag", - } -] -]=]) -string(CONFIGURE "${JSON}" JSON) - -set(COMPILE_OPTIONS - -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} -) - - diff --git a/71_RayTracingPipeline/Readme.md b/71_RayTracingPipeline/Readme.md deleted file mode 100644 index 4317be9c3..000000000 --- a/71_RayTracingPipeline/Readme.md +++ /dev/null @@ -1,11 +0,0 @@ -# Vulkan Ray Tracing Pipeline Demo -![finalResult](docs/Images/final_result.png) - -The scene is rendered using two ray. The first ray(primary ray) is shoot from the camera/generation shader and the second ray(occlusion ray) is shoot from the closest hit shader. -To test intersection shader, the acceleration structures consist of two types of geometries. The cubes are stored as triangle geometries while the spheres are stored as procedural geometries. -To test callable shader, we calculate lighting information of different type in its own callable shader - -## Shader Table Layout -![shaderBindingTable](docs/Images/shader_binding_table.png) - - diff --git a/71_RayTracingPipeline/app_resources/common.hlsl b/71_RayTracingPipeline/app_resources/common.hlsl deleted file mode 100644 index 502b53160..000000000 --- a/71_RayTracingPipeline/app_resources/common.hlsl +++ /dev/null @@ -1,326 +0,0 @@ -#ifndef RQG_COMMON_HLSL -#define RQG_COMMON_HLSL - -#include "nbl/builtin/hlsl/cpp_compat.hlsl" -#include "nbl/builtin/hlsl/cpp_compat/basic.h" -#include "nbl/builtin/hlsl/random/pcg.hlsl" -#include "nbl/builtin/hlsl/type_traits.hlsl" - -NBL_CONSTEXPR uint32_t WorkgroupSize = 16; -NBL_CONSTEXPR uint32_t MAX_UNORM_10 = 1023; -NBL_CONSTEXPR uint32_t MAX_UNORM_22 = 4194303; - -inline uint32_t packUnorm10(float32_t v) -{ - return trunc(v * float32_t(MAX_UNORM_10) + 0.5f); -} - -inline float32_t unpackUnorm10(uint32_t packed) -{ - return float32_t(packed & 0x3ff) * (1.0f / float32_t(MAX_UNORM_10)); -} - -inline uint32_t packUnorm22(float32_t v) -{ - const float maxValue = float32_t(MAX_UNORM_22); - return trunc(v * maxValue + 0.5f); -} - -inline float32_t unpackUnorm22(uint32_t packed) -{ - const float maxValue = float32_t(MAX_UNORM_22); - return float32_t(packed & 0x3fffff) * (1.0f / maxValue); -} - -inline uint32_t packUnorm3x10(float32_t3 v) -{ - return (packUnorm10(v.z) << 20 | (packUnorm10(v.y) << 10 | packUnorm10(v.x))); -} - -inline float32_t3 unpackUnorm3x10(uint32_t packed) -{ - return float32_t3(unpackUnorm10(packed), unpackUnorm10(packed >> 10), unpackUnorm10(packed >> 20)); -} - -struct Material -{ - float32_t3 ambient; - float32_t3 diffuse; - float32_t3 specular; - float32_t shininess; - float32_t alpha; - - bool isTransparent() NBL_CONST_MEMBER_FUNC - { - return alpha < 1.0; - } - - bool alphaTest(const float32_t xi) NBL_CONST_MEMBER_FUNC - { - return xi > alpha; - } -}; - -struct MaterialPacked -{ - uint32_t ambient; - uint32_t diffuse; - uint32_t specular; - uint32_t shininess: 22; - uint32_t alpha : 10; - - bool isTransparent() NBL_CONST_MEMBER_FUNC - { - return alpha != MAX_UNORM_10; - } - - bool alphaTest(const uint32_t xi) NBL_CONST_MEMBER_FUNC - { - return (xi>>22) > alpha; - } -}; -#ifdef __HLSL_VERSION -NBL_REGISTER_OBJ_TYPE(MaterialPacked, 4) -#endif - -struct SProceduralGeomInfo -{ - MaterialPacked material; - float32_t3 center; - float32_t radius; -}; - -enum NormalType : uint32_t -{ - NT_R8G8B8A8_SNORM, - NT_R32G32B32_SFLOAT, -}; - -struct STriangleGeomInfo -{ - MaterialPacked material; - uint64_t vertexBufferAddress; - uint64_t indexBufferAddress; - uint64_t normalBufferAddress; - - uint32_t normalType : 1; - uint32_t indexType : 1; // 16 bit, 32 bit - -}; -#ifdef __HLSL_VERSION -NBL_REGISTER_OBJ_TYPE(STriangleGeomInfo, 8) -#endif - -enum E_GEOM_TYPE : uint16_t -{ - EGT_TRIANGLES, - EGT_PROCEDURAL, - EGT_COUNT -}; - -enum E_RAY_TYPE : uint16_t -{ - ERT_PRIMARY, // Ray shoot from camera - ERT_OCCLUSION, - ERT_COUNT -}; - -enum E_MISS_TYPE : uint16_t -{ - EMT_PRIMARY, - EMT_OCCLUSION, - EMT_COUNT -}; - -enum E_LIGHT_TYPE : uint16_t -{ - ELT_DIRECTIONAL, - ELT_POINT, - ELT_SPOT, - ELT_COUNT -}; - -struct Light -{ - float32_t3 direction; - float32_t3 position; - float32_t outerCutoff; - uint16_t type; - - -#ifndef __HLSL_VERSION - bool operator==(const Light&) const = default; -#endif - -}; - -static const float LightIntensity = 100.0f; - -struct SPushConstants -{ - uint64_t proceduralGeomInfoBuffer; - uint64_t triangleGeomInfoBuffer; - - float32_t3 camPos; - uint32_t frameCounter; - float32_t4x4 invMVP; - - Light light; -}; - - -struct RayLight -{ - float32_t3 inHitPosition; - float32_t outLightDistance; - float32_t3 outLightDir; - float32_t outIntensity; -}; - -#ifdef __HLSL_VERSION - -struct [raypayload] OcclusionPayload -{ - // TODO: will this break DXC? Tbh should come from push constant or some autoexposure feedback - // NBL_CONSTEXPR_STATIC_INLINE float32_t MinAttenuation = 1.f/1024.f; - - float32_t attenuation : read(caller,anyhit,miss) : write(caller,anyhit,miss); -}; - -struct MaterialId -{ - const static uint32_t PROCEDURAL_FLAG = (1 << 31); - const static uint32_t PROCEDURAL_MASK = ~PROCEDURAL_FLAG; - - uint32_t data; - - static MaterialId createProcedural(uint32_t index) - { - MaterialId id; - id.data = index | PROCEDURAL_FLAG; - return id; - } - - static MaterialId createTriangle(uint32_t index) - { - MaterialId id; - id.data = index; - return id; - } - - uint32_t getMaterialIndex() - { - return data & PROCEDURAL_MASK; - } - - bool isHitProceduralGeom() - { - return data & PROCEDURAL_FLAG; - } -}; - -struct [raypayload] PrimaryPayload -{ - using generator_t = nbl::hlsl::random::PCG32; - - float32_t3 worldNormal : read(caller) : write(closesthit); - float32_t rayDistance : read(caller) : write(closesthit,miss); - generator_t pcg : read(anyhit) : write(caller,anyhit); - MaterialId materialId : read(caller) : write(closesthit); - -}; - -struct ProceduralHitAttribute -{ - float32_t3 center; -}; - -enum ObjectType : uint32_t // matches c++ -{ - OT_CUBE = 0, - OT_SPHERE, - OT_CYLINDER, - OT_RECTANGLE, - OT_DISK, - OT_ARROW, - OT_CONE, - OT_ICOSPHERE, - - OT_COUNT -}; - -float32_t3 computeDiffuse(Material mat, float32_t3 light_dir, float32_t3 normal) -{ - float32_t dotNL = max(dot(normal, light_dir), 0.0); - float32_t3 c = mat.diffuse * dotNL; - return c; -} - -float32_t3 computeSpecular(Material mat, float32_t3 view_dir, - float32_t3 light_dir, float32_t3 normal) -{ - const float32_t kPi = 3.14159265; - const float32_t kShininess = max(mat.shininess, 4.0); - - // Specular - const float32_t kEnergyConservation = (2.0 + kShininess) / (2.0 * kPi); - float32_t3 V = normalize(-view_dir); - float32_t3 R = reflect(-light_dir, normal); - float32_t specular = kEnergyConservation * pow(max(dot(V, R), 0.0), kShininess); - - return float32_t3(mat.specular * specular); -} - -float3 unpackNormals3x10(uint32_t v) -{ - // host side changes float32_t3 to EF_A2B10G10R10_SNORM_PACK32 - // follows unpacking scheme from https://github.com/KhronosGroup/SPIRV-Cross/blob/main/reference/shaders-hlsl/frag/unorm-snorm-packing.frag - int signedValue = int(v); - int3 pn = int3(signedValue << 22, signedValue << 12, signedValue << 2) >> 22; - return clamp(float3(pn) / 511.0, -1.0, 1.0); -} - -#endif - -namespace nbl -{ -namespace hlsl -{ -namespace impl -{ - -template<> -struct static_cast_helper -{ - static inline Material cast(MaterialPacked packed) - { - Material material; - material.ambient = unpackUnorm3x10(packed.ambient); - material.diffuse = unpackUnorm3x10(packed.diffuse); - material.specular = unpackUnorm3x10(packed.specular); - material.shininess = unpackUnorm22(packed.shininess); - material.alpha = unpackUnorm10(packed.alpha); - return material; - } -}; - -template<> -struct static_cast_helper -{ - static inline MaterialPacked cast(Material material) - { - MaterialPacked packed; - packed.ambient = packUnorm3x10(material.ambient); - packed.diffuse = packUnorm3x10(material.diffuse); - packed.specular = packUnorm3x10(material.specular); - packed.shininess = packUnorm22(material.shininess); - packed.alpha = packUnorm10(material.alpha); - return packed; - } -}; - -} -} -} - -#endif // RQG_COMMON_HLSL diff --git a/71_RayTracingPipeline/app_resources/light_directional.rcall.hlsl b/71_RayTracingPipeline/app_resources/light_directional.rcall.hlsl deleted file mode 100644 index 1eb18be34..000000000 --- a/71_RayTracingPipeline/app_resources/light_directional.rcall.hlsl +++ /dev/null @@ -1,11 +0,0 @@ -#include "common.hlsl" - -[[vk::push_constant]] SPushConstants pc; - -[shader("callable")] -void main(inout RayLight cLight) -{ - cLight.outLightDir = normalize(-pc.light.direction); - cLight.outIntensity = 1; - cLight.outLightDistance = 10000000; -} diff --git a/71_RayTracingPipeline/app_resources/light_point.rcall.hlsl b/71_RayTracingPipeline/app_resources/light_point.rcall.hlsl deleted file mode 100644 index 2265a98e7..000000000 --- a/71_RayTracingPipeline/app_resources/light_point.rcall.hlsl +++ /dev/null @@ -1,13 +0,0 @@ -#include "common.hlsl" - -[[vk::push_constant]] SPushConstants pc; - -[shader("callable")] -void main(inout RayLight cLight) -{ - float32_t3 lDir = pc.light.position - cLight.inHitPosition; - float lightDistance = length(lDir); - cLight.outIntensity = LightIntensity / (lightDistance * lightDistance); - cLight.outLightDir = normalize(lDir); - cLight.outLightDistance = lightDistance; -} \ No newline at end of file diff --git a/71_RayTracingPipeline/app_resources/light_spot.rcall.hlsl b/71_RayTracingPipeline/app_resources/light_spot.rcall.hlsl deleted file mode 100644 index f298e4643..000000000 --- a/71_RayTracingPipeline/app_resources/light_spot.rcall.hlsl +++ /dev/null @@ -1,16 +0,0 @@ -#include "common.hlsl" - -[[vk::push_constant]] SPushConstants pc; - -[shader("callable")] -void main(inout RayLight cLight) -{ - float32_t3 lDir = pc.light.position - cLight.inHitPosition; - cLight.outLightDistance = length(lDir); - cLight.outIntensity = LightIntensity / (cLight.outLightDistance * cLight.outLightDistance); - cLight.outLightDir = normalize(lDir); - float theta = dot(cLight.outLightDir, normalize(-pc.light.direction)); - float epsilon = 1.f - pc.light.outerCutoff; - float spotIntensity = clamp((theta - pc.light.outerCutoff) / epsilon, 0.0, 1.0); - cLight.outIntensity *= spotIntensity; -} diff --git a/71_RayTracingPipeline/app_resources/present.frag.hlsl b/71_RayTracingPipeline/app_resources/present.frag.hlsl deleted file mode 100644 index 00ab6e31d..000000000 --- a/71_RayTracingPipeline/app_resources/present.frag.hlsl +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (C) 2024-2024 - 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) - -// vertex shader is provided by the fullScreenTriangle extension -#include -using namespace nbl::hlsl; -using namespace ext::FullScreenTriangle; - -// binding 0 set 0 -[[vk::combinedImageSampler]] [[vk::binding(0, 0)]] Texture2D texture; -[[vk::combinedImageSampler]] [[vk::binding(0, 0)]] SamplerState samplerState; - -[[vk::location(0)]] float32_t4 main(SVertexAttributes vxAttr) : SV_Target0 -{ - return float32_t4(texture.Sample(samplerState, vxAttr.uv).rgb, 1.0f); -} diff --git a/71_RayTracingPipeline/app_resources/raytrace.rahit.hlsl b/71_RayTracingPipeline/app_resources/raytrace.rahit.hlsl deleted file mode 100644 index da7cc1594..000000000 --- a/71_RayTracingPipeline/app_resources/raytrace.rahit.hlsl +++ /dev/null @@ -1,20 +0,0 @@ -#include "common.hlsl" - -#include "nbl/builtin/hlsl/spirv_intrinsics/raytracing.hlsl" - -using namespace nbl::hlsl; - -[[vk::push_constant]] SPushConstants pc; - -[shader("anyhit")] -void main(inout PrimaryPayload payload, in BuiltInTriangleIntersectionAttributes attribs) -{ - const int instID = spirv::InstanceCustomIndexKHR; - const static uint64_t STriangleGeomInfoAlignment = nbl::hlsl::alignment_of_v; - const STriangleGeomInfo geom = vk::BufferPointer(pc.triangleGeomInfoBuffer + instID * sizeof(STriangleGeomInfo)).Get(); - - const uint32_t bitpattern = payload.pcg(); - // Cannot use spirv::ignoreIntersectionKHR and spirv::terminateRayKHR due to https://github.com/microsoft/DirectXShaderCompiler/issues/7279 - if (geom.material.alphaTest(bitpattern)) - IgnoreHit(); -} diff --git a/71_RayTracingPipeline/app_resources/raytrace.rchit.hlsl b/71_RayTracingPipeline/app_resources/raytrace.rchit.hlsl deleted file mode 100644 index e6ebcda78..000000000 --- a/71_RayTracingPipeline/app_resources/raytrace.rchit.hlsl +++ /dev/null @@ -1,98 +0,0 @@ -#include "common.hlsl" - -#include "nbl/builtin/hlsl/spirv_intrinsics/core.hlsl" -#include "nbl/builtin/hlsl/spirv_intrinsics/raytracing.hlsl" -#include "nbl/builtin/hlsl/bda/__ptr.hlsl" - -using namespace nbl::hlsl; - -[[vk::push_constant]] SPushConstants pc; - -float3 calculateNormals(int primID, STriangleGeomInfo geom, float2 bary) -{ - const uint indexType = geom.indexType; - const uint normalType = geom.normalType; - - const uint64_t vertexBufferAddress = geom.vertexBufferAddress; - const uint64_t indexBufferAddress = geom.indexBufferAddress; - const uint64_t normalBufferAddress = geom.normalBufferAddress; - - uint32_t3 indices; - if (indexBufferAddress == 0) - { - indices[0] = primID * 3; - indices[1] = indices[0] + 1; - indices[2] = indices[0] + 2; - } - else { - switch (indexType) - { - case 0: // EIT_16BIT - indices = uint32_t3((nbl::hlsl::bda::__ptr::create(indexBufferAddress)+primID).deref().load()); - break; - case 1: // EIT_32BIT - indices = uint32_t3((nbl::hlsl::bda::__ptr::create(indexBufferAddress)+primID).deref().load()); - break; - } - } - - if (normalBufferAddress == 0) - { - float3 v0 = (nbl::hlsl::bda::__ptr::create(vertexBufferAddress) + indices[0]).deref().load(); - float3 v1 = (nbl::hlsl::bda::__ptr::create(vertexBufferAddress) + indices[1]).deref().load(); - float3 v2 = (nbl::hlsl::bda::__ptr::create(vertexBufferAddress) + indices[2]).deref().load(); - - return normalize(cross(v2 - v0, v1 - v0)); - } - - float3 n0, n1, n2; - switch (normalType) - { - case NT_R8G8B8A8_SNORM: - { - uint32_t v0 = (nbl::hlsl::bda::__ptr::create(normalBufferAddress) + indices[0]).deref().load(); - uint32_t v1 = (nbl::hlsl::bda::__ptr::create(normalBufferAddress) + indices[1]).deref().load(); - uint32_t v2 = (nbl::hlsl::bda::__ptr::create(normalBufferAddress) + indices[2]).deref().load(); - - n0 = normalize(nbl::hlsl::spirv::unpackSnorm4x8(v0).xyz); - n1 = normalize(nbl::hlsl::spirv::unpackSnorm4x8(v1).xyz); - n2 = normalize(nbl::hlsl::spirv::unpackSnorm4x8(v2).xyz); - } - break; - case NT_R32G32B32_SFLOAT: - { - float3 v0 = (nbl::hlsl::bda::__ptr::create(normalBufferAddress) + indices[0]).deref().load(); - float3 v1 = (nbl::hlsl::bda::__ptr::create(normalBufferAddress) + indices[1]).deref().load(); - float3 v2 = (nbl::hlsl::bda::__ptr::create(normalBufferAddress) + indices[2]).deref().load(); - - n0 = normalize(v0); - n1 = normalize(v1); - n2 = normalize(v2); - } - break; - } - - float3 barycentrics = float3(0.0, bary); - barycentrics.x = 1.0 - barycentrics.y - barycentrics.z; - - return barycentrics.x * n0 + barycentrics.y * n1 + barycentrics.z * n2; -} - - -[shader("closesthit")] -void main(inout PrimaryPayload payload, in BuiltInTriangleIntersectionAttributes attribs) -{ - const int primID = spirv::PrimitiveId; - const int instanceCustomIndex = spirv::InstanceCustomIndexKHR; - const int geometryIndex = spirv::RayGeometryIndexKHR; - const static uint64_t STriangleGeomInfoAlignment = nbl::hlsl::alignment_of_v; - const STriangleGeomInfo geom = vk::BufferPointer(pc.triangleGeomInfoBuffer + (instanceCustomIndex + geometryIndex) * sizeof(STriangleGeomInfo)).Get(); - const float32_t3 vertexNormal = calculateNormals(primID, geom, attribs.barycentrics); - const float32_t3 worldNormal = normalize(mul(vertexNormal, transpose(spirv::WorldToObjectKHR)).xyz); - - payload.materialId = MaterialId::createTriangle(instanceCustomIndex); - - payload.worldNormal = worldNormal; - payload.rayDistance = spirv::RayTmaxKHR; - -} \ No newline at end of file diff --git a/71_RayTracingPipeline/app_resources/raytrace.rgen.hlsl b/71_RayTracingPipeline/app_resources/raytrace.rgen.hlsl deleted file mode 100644 index c42d5a7df..000000000 --- a/71_RayTracingPipeline/app_resources/raytrace.rgen.hlsl +++ /dev/null @@ -1,139 +0,0 @@ -#include "common.hlsl" - -#include "nbl/builtin/hlsl/random/xoroshiro.hlsl" - -#include "nbl/builtin/hlsl/glsl_compat/core.hlsl" -#include "nbl/builtin/hlsl/spirv_intrinsics/raytracing.hlsl" - -static const int32_t s_sampleCount = 10; -static const float32_t3 s_clearColor = float32_t3(0.3, 0.3, 0.8); - -using namespace nbl::hlsl; - -[[vk::push_constant]] SPushConstants pc; - -[[vk::binding(0, 0)]] RaytracingAccelerationStructure topLevelAS; - -[[vk::binding(1, 0)]] RWTexture2D colorImage; - -float32_t nextRandomUnorm(inout nbl::hlsl::Xoroshiro64StarStar rnd) -{ - return float32_t(rnd()) / float32_t(0xFFFFFFFF); -} - -[shader("raygeneration")] -void main() -{ - const uint32_t3 launchID = spirv::LaunchIdKHR; - const uint32_t3 launchSize = spirv::LaunchSizeKHR; - const uint32_t2 coords = launchID.xy; - - const uint32_t seed1 = nbl::hlsl::random::PCG32::construct(pc.frameCounter)(); - const uint32_t seed2 = nbl::hlsl::random::PCG32::construct(launchID.y * launchSize.x + launchID.x)(); - nbl::hlsl::Xoroshiro64StarStar rnd = nbl::hlsl::Xoroshiro64StarStar::construct(uint32_t2(seed1, seed2)); - - float32_t3 hitValues = float32_t3(0, 0, 0); - for (uint32_t sample_i = 0; sample_i < s_sampleCount; sample_i++) - { - const float32_t r1 = nextRandomUnorm(rnd); - const float32_t r2 = nextRandomUnorm(rnd); - const float32_t2 subpixelJitter = pc.frameCounter == 0 ? float32_t2(0.5f, 0.5f) : float32_t2(r1, r2); - - const float32_t2 pixelCenter = float32_t2(coords) + subpixelJitter; - const float32_t2 inUV = pixelCenter / float32_t2(launchSize.xy); - - const float32_t2 d = inUV * 2.0 - 1.0; - const float32_t4 tmp = mul(pc.invMVP, float32_t4(d.x, d.y, 1, 1)); - const float32_t3 targetPos = tmp.xyz / tmp.w; - - const float32_t3 camDirection = normalize(targetPos - pc.camPos); - - RayDesc rayDesc; - rayDesc.Origin = pc.camPos; - rayDesc.Direction = camDirection; - rayDesc.TMin = 0.01; - rayDesc.TMax = 10000.0; - - [[vk::ext_storage_class(spv::StorageClassRayPayloadKHR)]] - PrimaryPayload payload; - payload.pcg = PrimaryPayload::generator_t::construct(rnd()); - spirv::traceRayKHR(topLevelAS, spv::RayFlagsMaskNone, 0xff, ERT_PRIMARY, 0, EMT_PRIMARY, rayDesc.Origin, rayDesc.TMin, rayDesc.Direction, rayDesc.TMax, payload); - // TraceRay(topLevelAS, RAY_FLAG_NONE, 0xff, ERT_PRIMARY, 0, EMT_PRIMARY, rayDesc, payload); - - const float32_t rayDistance = payload.rayDistance; - if (rayDistance < 0) - { - hitValues += s_clearColor; - continue; - } - - const float32_t3 worldPosition = pc.camPos + (camDirection * rayDistance); - - // make sure to call with least live state - [[vk::ext_storage_class(spv::StorageClassCallableDataKHR)]] - RayLight cLight; - cLight.inHitPosition = worldPosition; - spirv::executeCallable(pc.light.type, cLight); - - const float32_t3 worldNormal = payload.worldNormal; - - Material material; - MaterialId materialId = payload.materialId; - const static uint64_t MaterialPackedAlignment = nbl::hlsl::alignment_of_v; - // we use negative index to indicate that this is a procedural geometry - if (materialId.isHitProceduralGeom()) - { - const MaterialPacked materialPacked = vk::BufferPointer(pc.proceduralGeomInfoBuffer + materialId.getMaterialIndex() * sizeof(SProceduralGeomInfo)).Get(); - material = nbl::hlsl::_static_cast(materialPacked); - } - else - { - const MaterialPacked materialPacked = vk::BufferPointer(pc.triangleGeomInfoBuffer + materialId.getMaterialIndex() * sizeof(STriangleGeomInfo)).Get(); - material = nbl::hlsl::_static_cast(materialPacked); - } - - float32_t attenuation = 1; - - if (dot(worldNormal, cLight.outLightDir) > 0) - { - RayDesc rayDesc; - rayDesc.Origin = worldPosition; - rayDesc.Direction = cLight.outLightDir; - rayDesc.TMin = 0.01; - rayDesc.TMax = cLight.outLightDistance; - - [[vk::ext_storage_class(spv::StorageClassRayPayloadKHR)]] - OcclusionPayload occlusionPayload; - // negative means its a hit, the miss shader will flip it back around to positive - occlusionPayload.attenuation = -1.f; - // abuse of miss shader to mean "not hit shader" solves us having to call closest hit shaders - uint32_t shadowRayFlags = spv::RayFlagsTerminateOnFirstHitKHRMask | spv::RayFlagsSkipClosestHitShaderKHRMask; - spirv::traceRayKHR(topLevelAS, shadowRayFlags, 0xFF, ERT_OCCLUSION, 0, EMT_OCCLUSION, rayDesc.Origin, rayDesc.TMin, rayDesc.Direction, rayDesc.TMax, occlusionPayload); - - // uint32_t shadowRayFlags = RAY_FLAG_ACCEPT_FIRST_HIT_AND_END_SEARCH | RAY_FLAG_SKIP_CLOSEST_HIT_SHADER; - // TraceRay(topLevelAS, shadowRayFlags, 0xFF, ERT_OCCLUSION, 0, EMT_OCCLUSION, rayDesc, occlusionPayload); - - attenuation = occlusionPayload.attenuation; - if (occlusionPayload.attenuation > 1.f/1024.f) - { - const float32_t3 diffuse = computeDiffuse(material, cLight.outLightDir, worldNormal); - const float32_t3 specular = computeSpecular(material, camDirection, cLight.outLightDir, worldNormal); - hitValues += (cLight.outIntensity * attenuation * (diffuse + specular)); - } - } - hitValues += material.ambient; - } - - const float32_t3 hitValue = hitValues / s_sampleCount; - - if (pc.frameCounter > 0) - { - float32_t a = 1.0f / float32_t(pc.frameCounter + 1); - float32_t3 oldColor = colorImage[coords].xyz; - colorImage[coords] = float32_t4(lerp(oldColor, hitValue, a), 1.0f); - } - else - { - colorImage[coords] = float32_t4(hitValue, 1.0f); - } -} diff --git a/71_RayTracingPipeline/app_resources/raytrace.rint.hlsl b/71_RayTracingPipeline/app_resources/raytrace.rint.hlsl deleted file mode 100644 index 551be1c8a..000000000 --- a/71_RayTracingPipeline/app_resources/raytrace.rint.hlsl +++ /dev/null @@ -1,54 +0,0 @@ -#include "common.hlsl" - -#include "nbl/builtin/hlsl/spirv_intrinsics/core.hlsl" -#include "nbl/builtin/hlsl/spirv_intrinsics/raytracing.hlsl" - -using namespace nbl::hlsl; - -[[vk::push_constant]] SPushConstants pc; - -struct Ray -{ - float32_t3 origin; - float32_t3 direction; -}; - -// Ray-Sphere intersection -// http://viclw17.github.io/2018/07/16/raytracing-ray-sphere-intersection/ -float32_t hitSphere(SProceduralGeomInfo s, Ray r) -{ - float32_t3 oc = r.origin - s.center; - float32_t a = dot(r.direction, r.direction); - float32_t b = 2.0 * dot(oc, r.direction); - float32_t c = dot(oc, oc) - s.radius * s.radius; - float32_t discriminant = b * b - 4 * a * c; - - // return whatever, if the discriminant is negative, it will produce a NaN, and NaN will compare false - return (-b - sqrt(discriminant)) / (2.0 * a); -} - -[shader("intersection")] -void main() -{ - Ray ray; - ray.origin = spirv::WorldRayOriginKHR; - ray.direction = spirv::WorldRayDirectionKHR; - - const int primID = spirv::PrimitiveId; - - const static uint64_t SProceduralGeomInfoAlignment = nbl::hlsl::alignment_of_v; - // Sphere data - SProceduralGeomInfo sphere = vk::BufferPointer(pc.proceduralGeomInfoBuffer + primID * sizeof(SProceduralGeomInfo)).Get(); - - const float32_t tHit = hitSphere(sphere, ray); - - [[vk::ext_storage_class(spv::StorageClassHitAttributeKHR)]] - ProceduralHitAttribute hitAttrib; - - // Report hit point - if (tHit > 0) - { - hitAttrib.center = sphere.center; - spirv::reportIntersectionKHR(tHit, 0); - } -} \ No newline at end of file diff --git a/71_RayTracingPipeline/app_resources/raytrace.rmiss.hlsl b/71_RayTracingPipeline/app_resources/raytrace.rmiss.hlsl deleted file mode 100644 index 5ccfed470..000000000 --- a/71_RayTracingPipeline/app_resources/raytrace.rmiss.hlsl +++ /dev/null @@ -1,7 +0,0 @@ -#include "common.hlsl" - -[shader("miss")] -void main(inout PrimaryPayload payload) -{ - payload.rayDistance = -1; -} diff --git a/71_RayTracingPipeline/app_resources/raytrace_procedural.rchit.hlsl b/71_RayTracingPipeline/app_resources/raytrace_procedural.rchit.hlsl deleted file mode 100644 index 6c2dc9903..000000000 --- a/71_RayTracingPipeline/app_resources/raytrace_procedural.rchit.hlsl +++ /dev/null @@ -1,20 +0,0 @@ -#include "common.hlsl" - -#include "nbl/builtin/hlsl/spirv_intrinsics/core.hlsl" -#include "nbl/builtin/hlsl/spirv_intrinsics/raytracing.hlsl" -using namespace nbl::hlsl; - -[[vk::push_constant]] SPushConstants pc; - -[shader("closesthit")] -void main(inout PrimaryPayload payload, in ProceduralHitAttribute attrib) -{ - const float32_t3 worldPosition = spirv::WorldRayOriginKHR + spirv::WorldRayDirectionKHR * spirv::RayTmaxKHR; - const float32_t3 worldNormal = normalize(worldPosition - attrib.center); - - payload.materialId = MaterialId::createProcedural(spirv::PrimitiveId); // we use negative value to indicate that this is procedural - - payload.worldNormal = worldNormal; - payload.rayDistance = spirv::RayTmaxKHR; - -} \ No newline at end of file diff --git a/71_RayTracingPipeline/app_resources/raytrace_shadow.rahit.hlsl b/71_RayTracingPipeline/app_resources/raytrace_shadow.rahit.hlsl deleted file mode 100644 index d87b8dd5d..000000000 --- a/71_RayTracingPipeline/app_resources/raytrace_shadow.rahit.hlsl +++ /dev/null @@ -1,28 +0,0 @@ -#include "common.hlsl" -#include "nbl/builtin/hlsl/spirv_intrinsics/raytracing.hlsl" -#include "nbl/builtin/hlsl/spirv_intrinsics/core.hlsl" -#include "nbl/builtin/hlsl/type_traits.hlsl" - -using namespace nbl::hlsl; - -[[vk::push_constant]] SPushConstants pc; - -[shader("anyhit")] -void main(inout OcclusionPayload payload, in BuiltInTriangleIntersectionAttributes attribs) -{ - const int instID = spirv::InstanceCustomIndexKHR; - const static uint64_t STriangleGeomInfoAlignment = nbl::hlsl::alignment_of_v; - const STriangleGeomInfo geom = vk::BufferPointer(pc.triangleGeomInfoBuffer + instID * sizeof(STriangleGeomInfo)).Get(); - const Material material = nbl::hlsl::_static_cast(geom.material); - - const float attenuation = (1.f-material.alpha) * payload.attenuation; - // DXC cogegens weird things in the presence of termination instructions - payload.attenuation = attenuation; - - - // Cannot use spirv::ignoreIntersectionKHR and spirv::terminateRayKHR due to https://github.com/microsoft/DirectXShaderCompiler/issues/7279 - // arbitrary constant, whatever you want the smallest attenuation to be. Remember until miss, the attenuatio is negative - if (attenuation > -1.f/1024.f) - AcceptHitAndEndSearch(); - IgnoreHit(); -} diff --git a/71_RayTracingPipeline/app_resources/raytrace_shadow.rmiss.hlsl b/71_RayTracingPipeline/app_resources/raytrace_shadow.rmiss.hlsl deleted file mode 100644 index 441a1b42a..000000000 --- a/71_RayTracingPipeline/app_resources/raytrace_shadow.rmiss.hlsl +++ /dev/null @@ -1,8 +0,0 @@ -#include "common.hlsl" - -[shader("miss")] -void main(inout OcclusionPayload payload) -{ - // make positive - payload.attenuation = -payload.attenuation; -} diff --git a/71_RayTracingPipeline/docs/Images/final_result.png b/71_RayTracingPipeline/docs/Images/final_result.png deleted file mode 100644 index af1f2b9b88c16271ba23a15333ee4ea0e9549d9a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 103835 zcmb@u1z1#F+cu1WqDb8aN;oPdBB0dJqkte%5{fh^DGUuGF^n`yN=hr;-QC?aFbv%> zbTbU!M#1NP?&p2K#2Rm)Fu_L{}-U;^E;DiN6$)!^69X z!^1n@LvRUrN2&r;2>d!{Atxq;m(fAB0KB-U_gv~Z9$vODAyx|?czwn6rIH069&zo- z&$&jEByBvr<9KnA=L(enC2wGeL)>TgBP}{SC)$TXXc5jAuO&S5 z=ukAC1xBry^*kDSG>w=g_bf@frEl%^ZmvQ0so+uux0Tz?ADAxrKfWwX$l>;qEbgT^ z9*h((drF{ecSU)3z&v%<9$_P0k(Sn$mR4fef8=m=pKUWDfFXCWzdUo%Gaxi{d&WD1 z6!Y0DGC<2A`Zjf1abtSz-75p!$#G^jg= zJI{^U)~z0Ms6Bg}j?phEiqMYP9h@fvY%73@%S zFXpIWtW!ipgd+nDlX94obTWwyO^iCA(76BL^oY4qONIbG@%y;$Q4JzHJ-RQxUlTh! z!Qprtdv=0M>fWXQf0sPm?3346Le0FBkmy9wJ&38*FR z*j)y`n0pQ9@$1kxT368c*U0O}eSg32;h}&2=^oA5nXqTWB@JV-zJiZr_}5+3Ja^mJ zJDV&z1@w^gjfxc;HzO-o;j6toWKy@(r%u12VW>b9d%rC_GBPN7KQ#JFyWX|qKkExw)?eF=T(uX&t&tqRo*O3Pe{<{lKtM|`fk}8n(jlQSdz6Z_`por19m;%QljE6@=BM2e%F<%Tr z@!J=>Kk+<4?g{VBCYU~)YpL0;&e1Hsp6?xyM}6k2-9>y)fct{ML^K9`F1FI9cl-{csE)!%5DeDDE?L{=QH!mfeIc36j%8gm;W2tz zblA1E_v@00pX}}*6mhWZEN4`Rh)lV`w&{LNKE}*i(|G0izo6m%KeF+^F^s<0%A(!Y z#&zp29&T3K_@hq`e@v>uzx;xbeuP=w-zbaMlm^EF$68o3Mv_-|kR`jMzk7fFs=J!3 z&e@E_Z)3e3WzTaPZSHFvIzjLcZ|D2`MmMmCmz^swbl{s=7V+;->tV?pzJCyC^2WR7 zdtNg4ZvM!G1c@I+N`|Oa{Jh|v-{EE^C(`WtN$!Vrq zFklBPd1hR1w`x%Lxta6XS~aYNL?Td6HgNpR0hKN#g8@R^>U4a0@M}1F)gF&(VCeSu z>jD7uhB^_xk7s31`C|}DJ5gjb?o0B0G`XTi43R&b--tGrQ-8R+G zTBvAxY$R;kbu>D?sEg&@-D@qE@`)(&)B?x3NYO0G#Qr-W1;jmfm!$2m>kM&k?fNoK zk>*Vfh|J+*Dy;YQiCN2*ODZC-lpzqGwMs1T)ZIQhk{~v_)#B!$szL z;KftW5>o=7L|Y42#*x{@4s_#$k~sA_G_wRBzKgOhrVA_-^RM#yO~tjTBNG=9GZp70mF=A$fT_aRX%)t!RfH(Mxer(CxD;sQCjfR!8Y!rd0)m3i}D0x_}$|$#63$wzo%r`r5$Cv zXsA|7WeCOa*z64HEh|^XhD2vc48s=RS#Ts?Nqb*)oMcCzKNILa9C1#0n$yP_9LzD(RbC4(1QYp`cpb6xw1qpO8iFjl-HR-PN=8L#|v1yFp)oc_3x+uotgd~2(JSIeCP$+7AX4U z{a>q|wPoE2WlPT)l%Kc=T}&+i1!9)}XW*SE-w7196SeB00eyOM)R6^EyxhR@KO`UT{$w6|;3VG_Amfmb+4g1l(&QrA zR^gA(F#rtre()R2K&UcKkf+B4aeojHvio_2#^l87{)T-iYsW^OtW5W5|xsj~u2J&082L_@7^rGPn8QXle`&S}04)*3OqXs-R4r0_|XaVT) z|3zXY8@m~EzFM>NIh>bjzwj`lqU$*K=FP!k(kD~|B?Z;C3{+HZhu%v8{Js#JOW#Ld zAV&Um!~mo8)sKFGkFb35y$d^%4 z4`DUirLrN~nY&=doka?S4CF|~dLiK?epZW#tTQ|f@VmJIPjpv(a_i=I{8p!>lPmfJ z0~U!r%dg>XZ()f39y6@oO|GyB^JjKoSNvfy=&28j1f1}c zKgT0e@S^|nHtwOa!hA?wKJ5DG)zs>MLoi19+pQ5%9-O{Mh7T;T9ql81LLVQ(0e7)> z%v&JzAM!q;zQkvvI2PAyc^Thn0@`aVcJyzQ<6+$-ed&LQkvaevJ9lJGNZ~AY`iDmR zuSP|fmus+v0K^yzS$CE?w*0u8rTcm98A&w&<2Z}3AA~(SW6S@fFKt6<6i}nf1-U6T5u(}|X21jaIZ18$bROB*A%?t30^ivacSLf>`9=O$37Jd7i z@i^Hbkk=!1tbs3b+)>_N;==1vI(NKV>woiwbx5*|Wu$`Zw=96Er6TyGD%x;}N7B7Q? zms-kmfAYL~6jM|6{G3+SSQ0d{=!nN)m$Fc*2;OJ=8gGeZG3SEuM^;5o(`}wF?YwzDus$aIO41`AuBKQbCI+nCy=ISHN zb_MLhSy)Y>_W^b$Qo!seynu|xrTQfw7*p?2T0VM~*u~eLz<0|tO49G?Ac|my+dz}<2Ps3C%}%isQht~YBf`efs+Ua$o$+~ z&(usLN{xEmeXT?4+RX0>0{`xGR75U@b5NMRwow(hmU1d14<7Jd+o9VNei*_*L8@cs zU*N1MaOUsbTKTEf(UdlGPJ*WW$7oZWgp ztA&5N4_RW(Gk`)_Y%J$?4(2tz~p#3Q~hH01H!ruRJh|Prm)O)Xg z<~AaE;GHc64Hv|?C|(KU<7DDX{b!6xMr{pTQcUJxHld1s`Rs9HKToyopJiJt1RU9E zLQNI#+RXQ|9T1eFT9|;Rc0_=ePXRsEr2xxePzIXN%OTp zH+S)Y5VrFcCi6cEFGKy~ti(=ym#jtZ0{Ig8zW{A#{&zE~{C$DCDlQ`U+yAxtuspVQ#%K+D{oH3slp79USW1K#F{N{9i!Q<)b>aIzPj(+%sVf zr1(gW%vLFT=%lL#B`^2?F!+pbCIzQpvMO1!C~0u?GgdSI4~dC)@*gpPwK!nFEzfyx z{YFO2i1iQWUujCd#DW$j*wtGZ*5zgHCEjV$SK>T-yjJH0@qUEm4~Rm@jabKDNs6-H zV7E5V^ZM(nR`~c0okU;u<{xueA&sZWX5JXF(Hi*GcPFUev7yFVq9!PIfSMzgYB;G= zAbI7-^TAz&t!{m^=#}G|P7^ux@DaBs?NZK8;8UEU_yESX_<41Eaw*5I0h2F9p*6P{ zgQ^SSb6&iB#T{V=FV2ZOkX^i&;SdNtUG|0{$7{F($|_p#3X1%dd*e%NgGn?;{_4Zd zd#Jj|oUh}LxX-gben)=O9DfNWBnPSfKy+7Pk0&nO!fE^YgcXBSG*I@Z-MqpM9u7uJ zs$3NdljBAOVuj>$*qOh1L4{8YU1~h$1i8pTAmwy@-Aeo~2Q|?v`!!u8VzMi~nxy2! zGo%&8jN+ewsx}G~YA%)%5@tRbn`*v3_jYEbGrf5PGU9LNkiZPHk6|uEq|p`H=dq9r zmOR#TSd`+~$p&j5OcchMS}VmB>DZkRKulzTNi`^=)~$+;8xt5;A(}qSWUn?^jI5RV zlwK3la_NfO7h{2k3;PWH-Sg;qK0vLzs{@*0I*ae+JM3dvc8|Ra`1jVs(C|a?V$7Zr zk0KvKVrt@y_I4aqv>=OEirDmgpR7{aV_SJEQ6H+-)@(kc_C&T~+(wmzKTl3u_b*1^ zTYeU`$oG=_`+X9PkWXf@7o4BR5U$^z_3nt|Dh==#JtCqY1GuC#)>=$>1?L*fzZdYP z*uM2n@#~^{iEUFwHjl#^BW&8fi}mv-u*u6cE5=1I*~u3?jyDpYn~_vX$hny;r4whp zF4a7C#Qd$8Zef=DsvlreeTM(KUeUGxw^S@%UH07DCtlCts@&JnTxi|Ck~|=7IHXv& zDrQ{CAM#mSqPVtOLv#E(WOlDMbHbsFi}a?x>xKLxKjyM@!|TSNe7NawQbq0L&dnQz z(Q&u{pATJ6iBZs43qr$2`}DcWY--z7z^%&Z!5pZBYf#DuBdK$Ag(XSwlR8JYKC$*1IK!H6 zk5bQ$3`=DGS_7FhZjxAsuNFvrLWm^NO9DI9B@vamkHpx-EIBml`M=J7<0Nr$U|FlJ zCBd%g4`pSNksXAxwUHexltByX^7{RE-m36!zhE3G5@s98<`;K8S-P=x`5PzVS>GaU zHH~EePy3YKno*byjA~hOs!aOkKyXO$$&xasxR{Z0n`r3WnBRf28rMLZeSf$~T3B-% z7zwuemw1u6ST8!JE>Y<~S{neaYJQ#N7f1e4-ZZ1~Cl_X#q}|Qe`866c@OkByU>5 z;Y0qzQD{GTb_VTge$!ZD)y_^=2Lh}qH3Y^Cp`Jl*i0*T8Val{xve1KpZ+r8ewyxx9 zM2vbSkFW{kl^dj%)2XPa^ekA$4P#o~>Pf6_dBSzfU-0-JJ!RBQusS}uJ;&|eZ4p@# zudULx>sCBm79zUs7ZSHisyZV(Jio!MHs67L`*%{(($bqYdUDbpXEybC)2XRfWG^#+ za2R1yQBqz{?59l4DJY{ilrYSx(6&|H5k2l#l&sW+TUg0+1%tIM@HcPT*d58ljZ7F+ zp4`;{(*zXXW5nx#4Ak=BOo{nMEt5H}ri_Y_YJqaW-EdzESW*bur4}?%n4R3)FzeP) z8;jc#y(^U~RZBTD=ev_Duh+U|lxUoo8jE8K_42{iCRIvu(G1zYO-NywS*~%X3oo`Q zd3pkn7axFV^M*6fVxShyC1Gu?w|itBXDgD>e&Hv`c=FtetwVcy0tYxeCp+7WearBB zvT=eGE1sp}z4MUAQ$YOTzze=!Q;|L!46`-S>{%m8Jg{yXB|T6oR+V34tG9z;^riWMnp@g`A8JXt~P4Gkp6L|I`}B`3e?oXw?VWPbo2JLi=59? z#{&BeQ#THnI>pN!)O>%5^5kg%D-dbDvXRpks7o8&yrj=ZbW|cWqz`RE9fYY@k_43M zf`kNJ94NN5N(yM1z-6}OJ^*x=QhCLi=A0^m4Au7=4%~cjwFH$~99&$}c4LP!v$vt= zey!qZJwT4PkUqEfMR~AB;Yg5mZhGwMqM?8|4~4wZ^=|pGTKUWD*_@WMnQh33~0QXec4&Va~|q;VTgA1ZbG9CAcsytLT1E zZmOJM?t}R2hI0HR^!xRk&Fw}W(}kb{fS9`XCfWr&Jl?MXF`wnTY2SgFx>^Y~#i#)D zjK{^s(j_Wt$0*y|`*TjuByZ=tQ`*h9^P67Q=nGFSo*3D>fSjzgI~Ml559@gZgf>K2yo6jl)uJ`qi?@?ZDM!*wL$dRO%4-R}XsU|Xo};;^KSH-8>N ze!#gnx}kp2q2tSSlGb<$h%S;|#UX*tqR^;mD?7L^AtCNN>#O^Ue)e3MKSAm+G|`=E zkWl5@QaTLR`O1VmW#&k*%Is>g4z8lDteCM!J^WU&nKGLSeknO1*ehZ7t?F%RXmSPr=5d%&QNo?lhpnv;aeudey*`}$-${| zjBn!<9vaLi#$v1NW5!S$E1#D|_YDT!Ka)6WXbP>lho=;glAfCwSTt+Si1b^2JG6A- z81ywha3P`BPE~bZuF5!V8_>G4Uei5rJtqE)AOUOn@t-Y&SVYExj`OozIdV4ym9J5p(;O% zo=?=bfK7&(ciZ0|-yqkf2gk~DDh9o(crYWR^lIP#QqgVv@2%6cdrhdz(lZ^_?f#)z zUX1d#5@ER-A?=HW@!=EOct8eAU+;sr)Cgq-1Yg7c z8%_`xhawAB>En@B!+km>C7WIr?@HBlW(JT-Fd^*CblbOHbNkt%$J4hs)>6YR*Hrl1 zj4OK)90GkkYJO7mgy>!F_QIi_-u$OxNyF?zY*0hg`0AKcWWqvXe_+sxqRyKN2DJ!b zNxplN~t_4guQRD#)u9RUTml8{W!&E zUcSw_U}>`m^f0@NP}$?GRM)h4sBlD-m*`!k#NkA!&aB3#20M$Z=ZFstr9?z5{j4mY zLm%z5drAIIy|SOoI)sn+^{0R{2IYRn~j1l(C@cjlin3!(v3hF@X2N9myJ&&1zgoP2w)NRG- zS4XWq3k8@_0HOL3%L{7ofb($=Wq-;RK^Q)cb^MsRijt1*Q12shf=(5Pr+EIwPs~ts z^kdG?l1jSIy2An4Jy8#|NxS2gZh_-14#NGRI@=tbZ$}96D7W2=GN87wB{jqOg>uYJ zHL=6!F%CgHWYm%29n$m7EdpUF?9&Mf*`^ZB}jHq?9tz zj&?&>uh4r<4jUQS(wxIk!?V4k77b_j`08EAB5cJp*%$Kh_(om)6rI&A2!eX<@NgcU z+oYJod?N+cntL!CsAK5&05$a(aU`SB%cVi&3yKGr_5zsVSiTfw&oGV4vJQ0qr}Ij7 zBaW$g&eIxde`7Yx9G7jy!wVC+l|UZ%c1;KZ`^Y-ZYj=BQ&fS4})pArbD-j5x#z_+Q zQRWw&S@t}Mj*Qmk#xB;mIeafY(qp?58eSnR4YH{W2HdCn(_nA79WWXowQ=OWlDJ>8 zRDOT0*IYffdg52fc|jCD#o!;8|%Y)1{YRUFk?e8r3aS76r$KK8cZ^gz;ekpWJZ3>gEq9UyJG6btM(${M@ z;*n|g#3%6w%P8W-Y+|BL3I`oPbgO-Y#+IP-kWDZRDuXzWiQ5O?GCUq$;(oAYaX*vLeq_2vFk;bL9I41PGZS+BjEgeWHUe-}%eCJz z4R2b>9U`1%A82Jo8Qg2zOL$b!z_8Ui^U8mr{KlxGB2Vdq+}vLh(GfxyvM#;!V%}ZW zfnv`J95=dEBM1(YsTBZgWd9t?DO|3325uo4F79@$&gvxj^mK>}_*`glX)CMr6=Lbm`I(6wuAxtc8&v@}H5H5VsffY6ULBja{PM98$Tq2qg({ zrf|`S(bU8pQ5Of?yx=Cxch|ABVC-IND{HGaW#+H84{y`&iVwBiolV)S-iI#@bK_xK zQ0VvMp`84w4If=0o@)exfGu2cy>~J{fA{d1fATxl zv-FrLoGtH#gH}uEvy&DTctVUc}wZ)BVb5%q9bL;E|U?$BaSuUCU<$5B2wGN zMoCx@OfsM)v^QrwJ|VhW{LKMJQ0eWqV%V3(n$-P{?XdD!@HhYC>>feWNl}ZQ<0cWt zvt%5coF*Z}6&nk%`0x3!6^x}-pIYuvV7s?eG7^{$gc7S|t`!a0AOWvd5kOQMf!`iQ zDkhx-5WW%Zb1oUjBDBKzi+NgQM*s1YtW0F!X0xo7QFX7Le{))a* zt6*I1&1p6XI2G|vIq7Mi`V4H%g_}HYBB@hL_byQj*xDGa%!g~d>*=*qn|Ifg7Op&S zL#RQ>m(w$B5;xprmaLGQ7RdG9leDaK7TFr9<>a5k%HI!5Xvrsht# z1Y3||JLN}l+TH>Ipvd~76}}NrxzV|7o~FA=WV79`xJ*ITJz=_n#%H`GGYpgmn@!6e zZQv}_Y{vF8B2-8jXI}^>&S~s_gFaPXDVk!4fBkSc-#OiMnVK;q$G>x$?!(fJIJwU4ZXm_ex<7k6eh965MDx`n-Rx{pEE%ir zeEX9M>UQ1`NoDdPpXgGROvEdb*mxu%nuL3#aO7@hZyijfqR)Rn7dedg?te6Y3>GMFzpGC#~sWqHE5?c>CP5Qs4|0#V}EkWB`5hAMn>{hImR~}qDqL|qpi-dCkB#)Te`hSfOBbK^vM~Hxd-?@_RI*&iUD9!TgJEFD6A>Ak|3?ug{=nZoX_Za8m^Np5`Xktuk#oHrhEGIWO(T{|!6Vry)v@pIgJS=b|_b=Fb=9@7gSGsb)-|-nL7x4_x63*eJwfcbD&~jpRaSUV!MVNE zJRe(9Z1P%~bPqpY1X-9!WE3%mN{fme70cU?G(o@zGPQ!>;;zoV;w}=$=%@j^&ns1` zsgYd0WTn=hbO5y#$fs@WMCzDnDc4r)1M>=vT0dWzoTBjPCS%a6*-cmQN7j&58!Zhk z!-B%qHLx?SzR7ZeoOGHk+D5`fB_@CQYAD;K!2S_q%YDC3L@b23Q##^n31?ra-fO5YZ`#_t}bM6+S-Ix&T<)C@AH zeCH){!L@Ut;ZX&KWam2xk>IpjrRK&N%p?&;EetHs7QGeqkm%A_77)*vpPg^sv_d{j z5PHSOa{1**X+g9=J`&&l_Dmvjg|N+P8YFr+l&rKXdT_sw$3r=tsMM1C%`9@JFP+JJ zvMvpQ5iB$MR*^&x2a-g5Y}#L7PQGc)at*y`G|#yY>=-R_UTtWXx3fDBK^N8HJLrDo zRH3*^t%;6Dp_kbFZfqKN^_r=@PMlD0#YTqj;*WxOA6Q!Sl6kj^%-e}IkrbtFGHCPrZv_@ zt@F$9y1a3{S(lLq!$34!ZqukM(c18WMKSm~1IuuJb4atb&cyN?9`n5E$R0jSWKSw= zk@-{N=6eN=+oJpV>w`I3AR&IfVolcDYfvsOlZ@fruLV%9GQ6S3hmO(I3yBuJl|Ea9 zs>hw)kBth$b8>c4Bo#Zw1(9~%l0dCT5~zTCyg$i~jue=_mhGyF3R)VzWqGU{_i^BG zBi*B_l~tUew#Nlg7$wM@JClQ*VLCj%v(|$S*8HA?yPexnxSmaoJV?IB;-PgF~ zjwI%kclD7B;k7MU3k@wm!{Zp|652Pmf**h1kOtGJ=za`O4mW5Ooz+<=C>`~9%)Zdh zOS*$k?*e})_L*HeA+fWmJGO8rzd5+sS{t{_m-8u(mN1vlwox5jb>V09Xc-Ot7W*mz z(UIyn`L+EgB_shV1M*d}ZsR7Kx}@{76nk1*Nkm6meVQj72gBiT#GpK!b1N5lT-#?1 z*0z}u%Xxxu*Us&Z{p*W|zWJ~I%mvolIcIw#yglYkKLOj4+uZk0w%m7*7I8;^AN3DxV|K7TZP6YugRMSi~sytECFCrC`npLcNdFUwR4D^So=5%e4|^DrH#I z?W5JgjdJxygK5OB+A3!vKAPCPT1BZe-Pp2qcwQ~*Krqd*SDReY98iUu#nw<& z*Y?keDC+*ibI~?qd$EV6&ZK-3EY-Z=Rk29ZIn6`j%r~7))P(|fLd*a%a~c1=k$Fd{ z!+zaiP2sJ>x&Y7qeB=mb;?+cmq8;c zv5GQ+$v-usJ*K2L?eFEhP=$ZR#T220GME|@b9$h#vMLgqe9{fAilLsRz2bdtH-{$z za5Rfl!Q@t0Tbp$`B~0NYrUPny;uU^ilgq0nR5XXAY0yyjeac|b#Gm)0DVr5DkvioS z88B0I4ULd(&RnCGC&fzw2dxR^;idvFVe+@B@%0&~nZNedk0e)q(r~5sy0om+c-U2T z*+dngq`u!By?8Ry>E9_xo{Z0wE;Vd=I%G6FM2tl{Ub#j8GbtsbYfVChwfOea=Rp8{ z7jFtSXg|2@N2eqUb~6N7hGz-&y77Cc#G7A zwiE6>fjNSl1i>g7VMuBDB?lmfz5qEcK@c31?oAwNUl1hPCtbO}Rm)h-HEof2=w`(W zwOOx+aBZ$rL0^f8IIo}V?s)pig5&{8++xOy-<*ZPcg#PaRC-oBMZdQE_27*gl(^?V zGBV%jmqz6m!52W?RvA=?V9yseVy(wQBelXpik88U733hT(8WsjLld3fY=OQOPcOkgD+hKO;1(5TsRCw=|oCO|#xB(K~bX`=d zB99=Q#h00?-#+3%=OXuo=jTP&io-w66Z8Z^g1sC3+d9+(&23P@q-5Q+|7vpNUGM1B znXc&N;Smh)Hr#))4)LnLsQ;x}IVnRA>QVAz@Cd(UL@u}AKV-~S!eQ-TX=9sQBM_OP zQOMamYOx-JHlK9iH(l_j3J=eXKc}nlO^nR!T1A)Hp`kE8Xv`08V1Rb;Vlr&qaLvSJ zb1X?kDpQW?rNu9yBFUR z!N1it7O0xBJ4*``YH=qie*H--68mUYWJMq};ILCW-j5^>enq40D6EJpf!}(miVF^X zaIh)i@53;`ZJB^@`(9!jdmkAbq67UP%&SY?S&^R~AG_z%(Qa^OR>> zReqE*ng2z_ZD zQd!r82LWi8Wgd(b#TGARL_-7U2s(A}J7;9HdF> zio0!$>(otYHDvbOWf6|w95Tv{edRwrg>FYRECCMMV*)n5#14jes#aioe~tS^9lin( z4}0j}E@aW2$%Lo|M%yJlOq|PApTEpSS{*IW{Zi<445!&PV4>D7u64V$z3o93T|~B{ z^pCR47d>Vor>;b<#Mkn)4x?9vbH214)D?iXTTD}ThW;f4;kKA1+hn5^m|71`RYjJ* zn#&n&5~S`&?if#YTM$9eUa>19-)l#T6;05N;nr#C&5Nj%$~4#-rJlsWf;XQL zchb)vrDpC#e`Oz!5FBY5EYNL63;=}=GO4R4nVE2_1IeRoN@FayK7Q?ZXOS6Y==RFXrt zzU9YK38Nj1Y_y?hhxZ3x4-faZ?C7V6i+snd6VcgzL9UZ;m!%9Fl#S(chJyD?SBQ?D zXemVbY~{kHtbRs=aQ)DxLgRt#vf2LP0+y{_fcUU&alp?F{-eSXbZJ$m&0ORh<}Xa4 zQ6cmGirbCSRJnyRnxOPTosUaW$psqU7}?(_XTB`n&IzQ11is(oYz70azDeC|l>WsC z`gQyO*e!ACWWV_h=ac7062G@LIqR95Bb&<1m&p3ju~%v(i0av2uWsvGlFJzJdzJiPCI-gtYtJ%fp#~`*jVdpQCd9W2R38qf zsUlRb?ere6jRRx!FB^|)1*;*x@31WswrHDg41TD5RIFMZ+@;hc$rPiz5>IL20zeB_wvoBFa#@+ZQ--UL^u^n7L4Swk z)Xw$pkYJY<4TnSM{v7atwKWurHG$yFia%kC)I2q#O-jSRL(D$E!I zdtC-$>5|`)FH>M{p^;_x!r6k9{NESQ6|0JxyGFpKzN@~?8A{V$hVXxnt_ljd)VBinf1yw>179yZlf^ zS3`Htcix*xo<=wSwip3EbbT6~%54Cj|5AcW*NOq`4`2RX(u4w3V%w~dv=F3nSBbJP z+M^pKE3wBOKLwbbGFefKm|#*m3-+gd%C0>9WeAyrq| zL@!oj>d1e{UlKzp_b7OaxB_qjS;g6BYHO1#IP=kUpc`IjN6ZYW~Wa;*MwZ=lg+YAZN>J|3>N{#?DXEMeF!01 zBb}W%`Y^Cvt}QIMJ9@RE(RPK{JNd(yiSDrKe%|yW23p0Wg$f{ZIcc7JtseKCH?5wp zXieNO$9e2Oa9i5Jr@KfM>jJ7MI*2=306jhBU;#tnJ|5*TQ!3tG$OSjFZ*U#vf~1-@ zR5Lv4pa}k~&tdYw%xy&GN0~vz_v6hm9MtB6L31#b{T%tNX+^;U+2r8Xf{QZA$HfOm zYTBMv){(}d11dI32D3BDu}L%+LfQ}B3L#6A!_`9C`U=}t9i48NMA{| z4|apkBn<&J18zwcd6rcb2Or)4eUY?w$&e&21?$N72Um~FRaKO}9q)h2+faURG7K*gSj1t+ZTeGusuY@aw)Me)S{q6wUCd&HBHYVVcLJ&A;h=;zc6K%(g z9o#(-U)66w=_NVUYsDSQNq_hIPIKoxcuE~tCMETZInoRYvPBkP6)GWp1p2yHf8~8nK zo+e%0j;5L#8=%$G@={Ef4XFe0LT`RdpYBIC0WdRrE_;b;`Wu}TF1n!PuhInPC1znl zKP@dT-lIRZMa=5a-@f|59G1-|x}qgpYjDw(n|V;u_|B>pGy7_}CZ$?~9h$!YMVXU* z@HLMjRW5>8cL#U~PEjNeaTeEd*s!onPk(>~O=nBc@sxTjS-)^0xp4kVku9?nYM>7~ z(>9kPr^sN`)d8a`)Val`mb(b`gxJ}Nqd;~2B;22$eDqKl86+JnNP`#+!7!aF5X!8ThQlgio3#O)LJ?+ z7^|NeGHc`nM_Adn);?E;?{5i1rhdHTVhIK;5tO_VdRH?u8p6Vfuv-jCK2B`f$YnUQwy z{d(g4{Tc;AWD29t%;c;3uw|d7$l9OTD_|?$zK+*kNw^EyVMRCA@#$x@%{|yd4tC{? zJ1(IHO?nBpMDeU8e=d;6UdUFnCbYoFv0c$4i193%62C@${#PO5B>U=y#4M@WXh6|| z_hMLoi**I+{?0^ZJ*GBTY~WJCLbC6?goMxxYvoY^c5`f>y0oBjh2KF{aMO{bX>u!% z`3irc3z9w|K`*?5>*>*c{4hGY_vs6wP3L^{qn1~>nzKczrm|rK!xt7+5OgXdsiR+q zL~BXC;&SeP<-;IdZ5^ug7F9zF>dP#$WR|>v#zC$5LXYndClCxtg|=5P<$rv8(%I(@ zLgK^Y&BdJsUF>;NSVMY0cXq~L?Al!|s&|#1m^Ki|KQ5I`STXLwQO$7K4}7L*EdU)0 ze1#7(Qk^f6{mgw!vcBah@W8sWfBOu^@vrWpiIe@Yw}HMMhr@=_w^He8pzIVl^Hb#& zo1%qAnr-Fau6PPafjnhZx1@)~!zj3&oWX)$O@5?3RCiW_xiR8VSd**^Rmta*`@w+cZ=G$kEDyZsHA3%}9) z4xTT(T&v+Z?9S|)Mr7105Be1=&KYvrZ1vFDY>vQYPzNQLgMRWsC1|elM}+{kOL7=<6Uq!7GJ;hy#Vt}- z+~MCzgNr_AnrLhMkd|41+R48kclg}i77?_+Z(;4-^UxZsEr@vWG2bZ)m72KwJ`nhe zJOgVcEx_I%7mWhHplclx;L4@-Qpld@KvlaB*S6T!{jA(rX)B*ys!?ahlj?UdrEQUX z-;tnXsEResu@enmwh1|2Npinx&)>}=&f7Qnz3AQ2cmC03 zCa+kim|hlpn8@9Cu5pdC?ITKpB`!UP6E>ZT>7ycAg zi#a#^J}+9ZWUrS`joUD|`&J?oSBq{GdqA>?2En;}oi?*YgAdllGMWH1f8wzDMMv`| zC8)AYt_%T@K7(eKm0Ar-Yta4VMd8Dw;MR?%cnP`YnQ5`r3MS@Fe0V0>X3_=Q<1R?+ zJL*t6ZF=4DTqi`XrKI95+3-G{IGiFo|0iY=HK8zJZY(kDDaE6b-b!hxs8ig%CPYRa zT-2@T_(R5Iz|rdQa2CmRry4Ws<-u|`0eD|V_r3a+lD3)57KQvYG9Qen*epcJE^^%Y|8GghUu~}Kx|B2j3Vq__;gMMs&6ts z(pe+DvR!Lr@|JHX!zdWDeJDv*!?PLfGg!OJ%dv4+w^>2|nZC^Cizgm_Bv3Na#h1@w z4R@S-5oWwkP~#|szB6V+*QdhtRo1@DVXdH9t)EYF{^1#My{>)+>XK@I6DaLt>OML^i(2Vvdl=%~5JMjL%C{KAKwqIw}Qtcvbbr&l){`s4d z`rwRMtdn%K!@HIlgHloPt0N`k2mOzLJuRD?@{t*~$c^wYf4O?Ta-{dh^$2FqvCSP= zIUzRg11@(Jyz6k)&Bk_4-;Vu`T48Gj)vB1HPx0{Rwrev9b~)~+hGrg+fDbWJ;qnW) zbaWQ1LPPmF*L?NcP97Bhl>%snvFJ?W*kA0ku7vxScHY!^Wip_}6#0Lcdh38D+cs`k z0b#eq;6??h5e6vTJwiZ~P*4$x(IMSEnxP;lBAp_o(p{rP8Uz{Lj2I(FE#SL&p7(jb z_aFWSj`KRtaiy`)En#C4^$R!o66QM{GKl`&NrjPX_Ld-dnpMpR}vj_F@2h_5`=cI&TCM-*LuaA_KL8e-YnIA>}^^9!3UJp{%>|XlsDQ={!&x^AEfzf__aI&hqOVWhXyseUw^S5pTi)j zd8FzbZv3<@H&+X}WJ{@8ice4oN zaI+#4_AjbXAifwoQ(9({bDnwh^2`T%n&dexr9nJ9Y>QC31{|hOn$LN)`T-At&o)p2 zFILfXKmCHYcWLpi3Q(UOK&)P$1<2vL!{&eUeC`x7tI8#j%3PF)aXX9a)6^eLUNdD7 zm4xVz8#HGYJz?W@&GZ0`b?Du{Yp~ENxz|aMV8b^6Y7}q@hTKCcwFftFtzl(ImoB}5 zaa0j-b6?mDn++=96_cPzer$&P*!ARlc$a?}UEU`BS1!*nR+kjfSbV36 zkH~kyg^ot1gHXy4JF9yU-XL6gOBR{qOb$!`8y9>>am!pAlWK(TYo8hOtWB$C&e7^_ zHbVP7&<%!Tb2*BLjXF!&U&oD*Ln<dwk}W#6&ws;Q(mwGj%(*PHK2CI1}3td(^) z$Gu@WVVOBVPS=xquso%~mxGV$w(8U6V{-r}^mz+WKb0x{S=K)z<^220nsq~E^&O|f z$RICE)xOBEI2!qua?Uejow&iFz7AZqPIn-D(c_&$*ofr$G=0x3&odP2frVV_ol;#R z4*i)rli@cxr4@gZE@q@!S#BRU{9}GyrL}8aDk_dWK*_n4)aea$;f+S3V?i$;=*zTZ zLDbJS*#iM%jLt65yN}NWm@J~z7dML=Lor7UoZ$Ws>YeFQgKRuRvfS*b5bwn7)*#Nk zL%`rM0xo1sZ!mp2A`kYVT0rgrhJ?`G>VxE^8f{Swo}g3X zj|PrbIL}3u{+U|qaD};fV(4xtXh)a~jGZ%9m5;@XdEjVeWrq*?nxPVHVqP1JtiBo> z?AZo3C!veasGFa;T6(E!fJI`us zgr^a;Wi*_N~CmeVdLHSWZOh;vGIu}8Jc*1-kx1@{`Dpz(|0;Qfr| zCgeQR@fpVG^lHtA`@e%uNM8pA*yLBc5S= zQ)H|}4gEH9VUoiE>Zo@ci^+gA zA?zNp>~i1_be^Ao_$+0QNnnIpaQ79gA9)I=#%Es=2860vL{WULT>E#X+j~=*s2#e!R6MnB}ktmFiGVE>6*+cTJ(J>e!2xo z04Axs*v`kL?6bPd$IpkkzLhCjK;MY=9Y$jwYW{&=4q^$%5`7h_c1`u(b%GrGyOmpr zV1dJ`@DUdkb{Tgmm&uQuV@^*S4r}Aks*A}&=%W{=v-RXre4CD7 zoN1(c{n&Z-G}O51S9-|5?4HCyoy&jna+j8lB1!2b{H(`zE&SwFen`_H88QWYss}CS zNSKZ|9+iZ}z1m;v56s=FmDPK@NXcS4iINx;MgjljXlZs-V01^cY*VZfU2b}lI)qGy zcBR%iD!5)fXCl3p`9rdrneB8^nxPO*OaU{f>R6A^hcis)3QXIV+a-D3$@aT3J+Hn2 z$%SlElH=e1)ULQFR}OXj879AkfORhd*+r$CX06$OicKXkRZ;(gK6tpm;!X0 z`X@r;v5xSI;EXU@ljfb*!>^?xd7GLiY`a^?u!I{L+d4)q^l2(?j> zOO$xe4j2MMA!zA*pTjvX`%;N&cC)A}l#FE10TH|qb6*#wc)O(c4z<%tSvM{}|G8H! zzx$7Rk_ImCt_UFx02VLkw|>iaXCHs}Xj<*5eb+OC7w z;DX!KlyH`<%?qo>+vnyg8--UF@q<@JBH{3W0k^m1oBg}2xv)-F?IIv#h6s2YnNFuP zRW(@;BG(1?SSKbX3XGeM697at{5Rb5GNW}`LF2p3+2ox5!9z&u=abpzU;exp`q9R; zrwkkUyk-7Ofo775>|N4X*tf#gTi5-kC(!}VF6Tg{?c%&CPaka&~&vuFmt#!N%hXy7s7<&0w?S5C1@I+lS`C3h`IOEmy^ z5IR4Ax@sr0rN|g&0;G07zS59%9?y5n7c4m{fcJLRrVkd(&)pnm45chc@XMmrH|0B8 z7)@S+xwCIfhn(|tV|grC1vEDv4ld}&ryMAU8e2JaNy0T%f-59Ko$%9lunr;OgzzG7 zaCYlP{9>9_y=G@5HKZJPCrA9BR>~X(%*Gv1pAT^IcLqEnuQS^~=m)T-E1)*Vijhl* zfOB4`+!lakmCTHQ^R8+K_|Pgr_o{*jJfv-2XvX`U4n^AYu+VJUW7iA?#ty|p4qdrM zp`jmT@NOV~Ncl3hk)eAL2n?;=MUJWY4GY{XegR<0?Kwgm0K*YGsWB7i*XT}a)cVq2 z`4;KQMeMh(Vo7QcdUV=>5`iVs)7e}%Ozlg)W6=Aw@O?$_e6kYlY9({S`k>gKBi&6e zDU_?vzE|P3JOOmbJr=e!?6atI&u=-Ymh9%-JIerVHsiJJ4I;idDgd~6J>i}Pb0qWf zIMC|L2I;(!m~K z(9}DB`rZ#E8yFl>3n8r*UYky=v{Y1E!KF~&j2~!@%HMbNK(A_MLQISkr^gd(I~SNJ z2)mYjJPu^ofGr;UDA37x0n&&^Se}CHE}^vo!CFb@3kH(C<}5(_gqjmXDHR}vWiMxu zod372Oe-d=xCA{tU!fM74D0n=uh2i3BxNV!UzwKHJ*nJ0k0;}cJ$@+BdOzR&IiOBU z{rbFqi zO$qHSCkU@|x=?6IzBVnz{u($Gb%1Y-N=nJGj=PD%kzX{VuE)Dago4Q82L|m8Zv-mG z-r(JJU;yY1>g4pJx-!!*Oh|%B1AeFJAkP3M0Sim7_4cgyUc%w3XWf^=qC#U15A_6- zkNlQn{J|bHK|Nr%VIdrw(o^ofF90a_VOm2AS`iF3+tNGP{jX@EV(nASWq(zVj8z`i zu*KUd4M+m)RW{I~7PzFl%0;YQET06!O8;VNttwv(0x1T>ig?Ytdmf|PtJe7@NwlRsw9~ewQ(GM7Izk4v!N>E&N5+UoFhj!I4b_{3mcCO}3 zDKo0{^)warJ5*V1VGUWt2ZK(oNKuZm`kLXgTOJCB$!qyvFC)GGFb1#C9_r`18P+;r zqp|hFYtz|TQ*eg$@p4KC@b+wRpN`DlfY*IRB-IDijF|{cCYPPXs<$E@d z;;#PCYSSkrDYe!QBcwI&W$pqg2ni*VK_ChS%AtXQ-z1*}ft!aul2ExHJ&E{JuB8UG z$~6Eu1>PJt5^MN*9R984bk8w)fncSi1#9b_+}2EASSU<1r!{37iS585kG6gsv<9B_ z1Eyl)awoW7!FC8pY}(l^;<;_;l5AZSE?myNTiaJz`vrCI&L%4udER{E`fZ^-0GB%? zR6l#Laq(!c?9BEa`=;%Ar$YhQSZ*S&?@$9XwKoJAwIx@K2ohYS&Z@-3?}*LR!;L>O z)8L6RWQtV_W+UrKK1TOGf9~NRP@!w~&Ij*&4Zn;@_>aCI{O=RP2Fy39#0*N^PK@Y#}-?dY(YSAi9*++O2 zQQJNZHia9Wx3-=H=)qto*G~+s-#+1Jynr{jqGRLe9x-pkNW)>qCgYgI1BtETm<^4m zU-5O6B=)1faphoOy7+Z;&5hK3bnWtCimeyQ+{|XUltQL91|JTH9c22YqCPB*b{*M~GvAgPM${`O+qAq-5f!v?`1474WJkZ2a9#zcw z3&Xz6!%s|Hz8OpYxO%&ki^};nDkD8Tb#(MCX!4+dolMF{`+neaRr;d-g4v+`KavGB zF#7$wv`438??!{p&MLF!W1fflHtL2{kVCUmh1FQ$xk=N ztikX+J{YHQa8wpNm&7K|%-~C$6!E*@iT~X&R@g!>ywjk(akPh0S?fzu8^=}e0`$)R zUn;t$_TckM z>ipfS%ces2H@&DUNsivv^%frVv5>V-B>r^1X+`dHmqmlMEOlYfpc%009#g79<)*={ zVp7PkZ2^hf8c@%JGBxER)AR?yVh>G6Gxl~?VV%X2i0wsCY zZyg-8MZV9FcHX5(Kkipc^A4ZB^LJP2LaG#oDyM-J%`(aG^9UQgF*zy&X8vG?#wC1k zh_x|<+MK1YoE_;|;^erI{5B%AeuMS>QE^dM?280(K*e0Bqc!?auwD%XrZ!xNdtiLB+(;2~f{U==)gv8J-JWY(l5P z_9dL)C&W2$(BoTgCCWHAk1GFN@K6>hGjIC*dCgl*1B9IV*&CIL8ceK2k*W@o(I@q1 zEr!CaJx@&+5e|(Oe?GxO<%r9fk*MBl9)i_ld^ff?d7-gr>MxdmtbszzSF0vFWy`X+ zwGF4HHgKNIom6>$pu2XcIFNGkwsbTx)(>jQ&gJWDZ_;=Q=|tOhHM`}1mdZ957}iAv z&TlSPJsl;czuhW#R7{y1va99Yz%`|-6Umt!n zEkWBY+yO>)bu#fs*qr`rgPhuA58Af|x3l+&WT&S6p$Gf?Qfot)(VIialv)&XQdeTw zw&*FiIlH3erSNoI+!OoOgIK1X5lI-X5*_YpnU{+dFBfw0077*?EudU)J#og+WH9DEMDFt~JOVyAsmGt)m) zuMgRSd=EB8=htuTzp?8LK^2urj?V)Uiu;??=Q*v}0N5C`0oe~yn|M@x@-zxdO$I2# zZo9z%u@6rKAO#fjTJR%zok<1b3v11YiGV&Q822guw{gwer$6ga{US%?Z)Ov5EV2dK zP3dE!p--`VE#8aigMdY`_@rv4(ddsMKJ7gIHZ5^GV&Q_fb5MQ{3ajSh@cl3lu`n!H zuUfReFe)W|AXLslM=Xz3B*M23l<9~Wj3qmf-Z2>0xcZubIh36%^vUbFXQhFEyK>r`*}LGwQc#QQdNuVWrsSn>kwhKe-cK z6JaQE^`PPXz~D_H*quBT>`IP(l>QCRP}kzN5>@`C6tB}#vl zm8`8{)1Gz#cf;&3pT3aXQt0)sV-Tf?1$`d*D73bxGLHE0Mzv6)IlIraS~iG4KRK9= zbi=lPKjSp6o8dI>lH8d2p!En8y+_{@qQv0DaJ5rZRU+NpXvDC}>>i;x+_~{fp^z689%_lo}{#Gn_w*3T< z@zrMtLwBDKJACYF&C7}Qwm(6%t9+o-Vf-~0@emLlv0@Wui65GM8nIrRKZ#$g4h>1! z5B%Q>-8+$q|3)*iEepm zJoWKUlS2YbEZaoV6?n|EhEt*LHgQd_G0}=;GD#8}Vta>+XkRcnddK$BLjQ3~i8Z<< z-fu7H14pC&-A`wRpDcSGJyEN2p$yIYA1}zRo*$^bBWIL!GAnv_>PZ(B|Diem7jbOu zjLo?0{c#)M!XcMR{ojtkAHZ_Tdrt5=RGXt0D8|(-8i~v49Un>tA>`C8$|(EFB>v`;yw;NepF@Rk6vZ z{E|vjDOX06+N4_MFNvu)gwm)$e;LXFrwznHVVatSC?fi#G|C+$HgMx6(flABIxsr) zcM8ZpAQ{6z%D?XHd2sk|BwN3IfDgVm+MYDE^~CKjHQ~C{R@$cZz`9Drf`SK;hVxVv z6iqXGF25MG34dxa~>?j0t)OaICjdo=3eJ(qhJkvxjICeZ;0#-d*7Cst0NqLn`MQz+t@ueUJ z$$-DP&8vh9SJKrWOpU!`VCH7)s<4FZyb0Tp4c{Q2$pKPCF-3JfC0D=ljfMQd?WzlM zWJ*E4w&R%8o~~?J^o3(9Z~?)4R8;t19C@-nI~m0B9>sfNpcg7-k)^c0LEH|iWtl>N z21RpH!)lP86rSX435kP=I>P8D3(5wg34c=0Z*x_f45yU@tZltK3zk2OttNvmP>r?# zwjo`G)0}}94|<2GfnKC%9Bk({mJ`$tRMJT+UA-_Tbn(dh<(APk!}$ljX@+`{U7coPcvBzlDH#d zl7MhdO(>okv6rz&8h^7Vmm5WSZ|OS|cA9q6x=W*+@k@WSm^16%lsS}3nv`r6SEtZ7 zb47O|Ud3*8Qt3xHgVyN3FR2!>+EcOoO2rO@WktmZ#h1=Ux^7S_vmZ0k+#Hp177rbT zZ$QlNbjU}<7m?#4dn5fOA!LgDJp5fAS8?$q^HyHf0G!`W*FPl&wN8NV^gkbE8KCxW zJvF%KE7eZ2D82(dKV3O!oAbdqKoW6{4F{KM-*Y`+Y%XgR+1M>8^Nt8IOaDC`?L7y{z_f+3G|O_BWZbKXX~1kt_OKZ++gT_-Bf zispC;WKN&lvjNeoq^#Q1{ac)01{rsa$)1vRVZGKN>kZJ%JY<2a9L#XWf1U zDn0NkO#AiZ(0V1O`gRSS76}-8DJ?_vZA9LH+gWS79zz31)Xbtpl|l+2ii4}p384;z z(56kwY3^^GzvU_5ygiDp(}F5$v|9rRn`+f72z?g@`sMKIIFR_vvbbsBNcVwO%~lZS zuR{?=3+Of^$Se_pw#(znTk23|dc1tPZ^-VExXG&uiJ(e>3?2LnhK#k zVD!`4qE-PqpsJ%&zw?r^K(+Jb4^o~aPH7i-0rYSqPil9THSxf&TlBK;5x0X0n+3zM zIV)jG>1LQgy3(7kzci-891N_ye(=t_bo#RH!c}XhpTd7LFyW6fe%alFE_Uv|ZQ?NY z23Gmh=L`$vxky2zfJQY&RLoNO2b;GC z8E{KF6N0Ch+xu_tkz|BF6K=PW@51;AQ1V=1T_J=z4$p=#U0Gi5 zI3GBJhNq-k@?|yg=C9b^G2-{rO$uz#g&!E4r<%n5bwqNNA@T2lz-L*uq0A=E?qxuP zB7Qz3bZb=~JlGeola<61gI4`?CM@+4-rHW-L=nElSfs`r zH8LESFb-~z*1i?Wp(4lMH0pkO<-O$Vp5l6G31%ny$y`Xa8Wgl5tnv)noc7NV%at^6 zE$Ax4l`U;mEjbc&y+Aj@J;Z5*LpeLxn6eKSUYhDDryu3;&sVmdC7qiSmf}Zvt{MK7 z`n?SC6!A4_Nj9iD!*R20f&BZVdwbv$#-zCt>@M_WYrV#iqz70GiR*JJo&R#Ak%y9!X)M&b5-HVv5yuQbf*6t zt(^Gpwq_5p4OcG~3d;Ml3X`}WtyLm*WA(Cb%W%FgDn1_yNQk!GzV8F@6ZYcqbiIrR zkNs+Y>RXpCYggSY9(t88@6=cDP_H!AUrXPbPy0)Av4x^T;-GD&p7KF=GqeGi*%h=h zC}W3Nk3G%U`Itd8M+j$<_Oe=KPbJ;{2wXNi%cvq9>iNV`R-(xGGc2`FjS?cei>?wbr>q-Gw+>_+*xcD z6e%kPI%m5DeWp=e_u-i`M3?p;HW3^NV${^=5p{p{O|16?S7Ti@fu=fUhD`e)HSR?vt>N#o z-gh09#v{V6hH+~c(R7Et4>i_Pj->%hWN=uMt4^>BRTz+l71|3v?ZC7~JnLcPL1FJ1{llN#3GyBFnYYml@LHdXFvoV=A1>ze&#vjOj`Cees zO-*{g_Lb@Q%Yh#6El(BTJ(z7?sR@2iPL1CSkAkQS%ns+LF4^{xTpJg4`pY>2Q?i@7 zG|Y^G%;tzRHLpth(M>Lo3*_&fJvl-n!6N@h>o_Vcr?CEEC*X9)UdHchn2U7nJ@sj@ zTKIJSCY6)-_iSmNaVKX-J0Q1_c8LWYvPXm2p}Y<1cfBs4A|A-wE!u~>aOQ~>`%LLp zz`=tnj|FFLfR`Xh@#U2gYFxR()|_O@3ME8Z;`L0*IXy3@7SNrs`mljcU1{vXks|)| zS97yCVlBniLL@;C+y+1*6>&~;9vxoSN2UCC{Jgf2+-iSwH3`mRLq$w`KM@rCLV_RCF@-z=>Uhr$CWX)zoSi*tOe z(uniDVB{J+WLxr!%%6<09YJyqitI*>z&%pq605XRd;hNCrxT24>^63v${P!>cR+#s zyc~p|@06D8GT7RrY-mAp96SNQ?5;~4D`;zNH}$L7M#m}vv4vobw=RND`Le(Kan$S+ zvrc`JazNJFkRA$%fVj5D7~p>^Z#-0EpF_x~^?Yk$2b1~V16L1XX9L!&&lZz3-mbk|?jOt%dSyS|mgC)t`+P?ms(GY2 zqcQGO^X7cBE@XsGnX^O6rxVOBVT&B#O6fd4oqRVM3?e*>&!G)Xi*oJ$dPehiyba8i zN$F-2r^t}S*`~S|wMdOQJy?(XXNfIo6uL z!s|CCUz&F7JA)MxOvMa1^f8ARDjefo$?BPr zieWA7-b_I4wo*5D&nH{HI4^Wx&ZhG`?)Ei!4*-~ch6QemoOP}B%XZMC_B)4to_#kk zA948dRq-#J8`x&%EVhx7q0|&=)P-wko=U8Hg^>yeF-=T0Q2HHHQn27sWz-yt)@=+ZuAx@_D&%jrX74xpkph2}FwkFQq1`?Uej zd?nygod9<7O)bCH)4VM$!_`}%wXZwef)68^R#PvC+16gx-9J(|p%-INfx0X?TU7J< zPYYAsOS_BzTp$DBc{IMDG(A*Q#ev_j)PeOm3>in~g>KghfgHzBHcThig0OnW@3I?M z9ULkx;2OTB* z(2ZqE$ch)+I=mho2vxgXEyO&MJW((QnXDzRi{3Z{g0T+<;^#$Y(qu`)@>)%Sg6t1_2|GoaPRNa$(&$c|x8c(TScm)gsds>!m3&KbkjW*&$}Z9soXZv_5#h_s zfXJa+`%#c-3L9=lgZ|8d0CRf;Fy^tpw4L6Fme-H>iv4LM2|HzYupBO-@(7nVv(#BK zMK^Mb&`}He$90PXuSOq@oh?!gEb>Bq9J@GlM%%?(wj<2jkVmTJh8jl34)i20&?C86 z?ylN;MpsBx<`7P|KPl4Q{zWEV%q;iFgn+r{szK{M?b#U6&$BQ;m*LuJ3N$>A0S8Z( z`Vnm*hiNiFHEv#)=i$YL8Nhu3lOn(Ekh~enuFTrVNc1pMDu2ZBY3`ek zdL`c&!=M=*F>Six(138d*zGaQ@y&ztXvK`mhxOM%iZ3Xr{#|i$xE};-;vCZQXYVf> z8P)TX*XsIR8F&ws)!UsE#gfji zK3dWYqg?AM(68Zd3lct`EWG;CdQxF|P;nyg#3^^26@PW2)7p))MtcUOgZu0a)ud!s zWz$D6_=66vgx*eKC^wHYqf<_mdwI)=VUmDs?wLhRq*t_9kbcE)X%x88UBMmO^;jt)48-ro z&kI(8?%d;^gv5cZPtDlQ`cnJ>8ZJp7=Eh4Vl;X3UB;l|7;qeOhE;e>a3Vz!yyAm*^ z=1=h}&R$L>yIy;!D8E6@WiM9ESw-*fv+sYh=L!+GX+`VR{40VY<9M)&V{8}YL=Gxk z=G);J1*C~DT+`FV{^Yiu)b{hC#2EPRMP0N{fsy)TZYbiR598Ks4M~L&zMxZ?dK(@! zFLYL;ok~K-e;axGG}mT#GKST6AixLHZQru<2EHQ;XTDlgc${|PkIYEc>Ln&F^3W4!~k!vo}0Z%K`THrP~Ya$ z3dn`o7ko3%ZUWL9pO#(zOoEgyB@3huQ=b-IhX_~#85by5aC{rz&JJ2)2dJJ64oIYE zuFsjhXaFS=W}@z)py+mt<(4Ri_Z*B)!FbWuzv`|O#3krLcNGgEMV@@4yE;D`9{oG| zIw?+j&CdfoZSY0q)sCD&k}kB8(`R0h=jEZfPE>;-;&ZK2nz!8Ou+Fw^6CwJ$7(~hK zjgX=h_s{lBLkT-Eh&w~VI4dpf)w$nt6rLd=gCG8MicVYn2`O94)QefGwFiLAxZ~Hq zsO;F)QciYgVz1C^L^=31JdX;oR4Fga0}Y-tuvoxQfKysF{P7jyBJe6{};6p@N)Z{@Fcttnzk`8D9GwfjH=5EPHl7=V=H{NlZUH`%7CKw65Ko>^{HQA~s*# z0wVT}d0W9aw>=6^x%WCHNTSyc^%-dJ#*<7$^Y$6$&u8lf!IT4pxk!H&bm}mh;CQF| z4{YW{(ZIOsi(7_X)DHjME0R$)i7aT^26mrI4sjjB-$ z!39tXXAr48R<>H19uZ{mjRWr$6iSVG7S9mE+=5_AHv5Il1FYMJ3Pi{aek`iXz%52aqc@_h7g=Sj2U zJ&ICRbPGbXSqg(Dec!!*INu?EygLDK$TRnH%ajSB?E-q`1kD)(-KK9Vb%(F{vS2B# z-62khT|cILzbt!LjEOUX^+)sG@k{9MF|(^&W@areQ(8MGC+6<^%Zz%0!+`gnI&#gc zIEX;od$S5C@|bnYJ4Dt{%!tbtdb2sdQX#+2_s}0c6?lvWL7R^l@Yj|us+LL|(Xt)G z^A&H%Wh1=XIl(B5>LQ}ytgdU&1hlc`dHZPc7cPHogE5Fpx=XY}u?l>F<^r7$48Iii zJPD@54zqIDXs_eTt#?NCZ6UlyM({b@Ls2oGC)o$zmVu2D8_%A~wj@iPkx(rX@yBb!qndt|8CssJvgxq&5%4_fe>@#-7iS+ z$uzW#T8k-QNwJAmtiJOs?u2fhRVWojTeeh_f7fqjU_Xm}VqhTFbj%&@taEFy+N@yn z?aS}_`mzZ(c2&2VE{bC7EhD34Z=#qeYBwFKVhCYK?>yW;-c7$HMS_kKe_>J4q$%p_ zHJdXQ&maYg-KN}xceBoI=hfaLA~K=gGqs{@;)Rwa46Jn~? zHW5Y-rw~r9tEO~mPxnh1rb{v2TSxdy5~(SJ+Z%rP^p|Fv!AgUnI57;q3SH1PoJC0O zbYVB~Udl4`2DkDrFKr38dJcaUl?qnR#^1SQED;kq4tSt>ZgF*#74pdv>|djn_@s=& zib1UOp3Zm|0yv6oQ_%K-VxzXpH%3LPWmO&(Y6|NRha+<Sy=h5oDhj5Fn5A+8d(;kcQMhnzD_gEM6=Of z#_pa?ZbXGAOvjskz99BFNU0)}_7;=&GxVDq88O5&aPL&rZiPejj3`#1n6&mV0h2O( z>u-845d8`eUg=iP7biT*jH4=?zV@e49Q#)7eZI9P{TQ!F@ zAIID1zruL2N3N@HEs@^~lS}0KruqzB+JgAxf7pcp`5PknS`)21dyIu9Y zT$c3nT?eYc8d8sUf05&D-z=`r6I8EhAq~kL z41Ia%j-B&qp_D7KJBJTt%J3xbHa;E7lIN>=`*-5CTMzB?VX5UaUZbpW4PBEQwq3+- zD=vX}4k3Ly`}4OlpFqC5W+Z6*aqPa%1}0L@WTl0572-GRVBQt=)UAx_5n$q>*$}Ff zEQUVb5G(j4wyKz0DDK=UY_~xF-{OC?feihZ8^z{Wx|3lkoy}+Fg)an&LNG#scwO_t zN&8;=HF|_05P*t+TuLE%0<&_Lyc+e+&N2RJF6Q5J+F`|L6Vxzd6CvX~|Ga{S^!{3= zt2Pj#xm0x}3`%>!Ph5TLYI83rKD`&bNPo`t4bT{u$6Vhx9zk3g`OOs?crA0Q?wTs@z$dDO>%!m^_KJK?qL<2FM0$q)XA=H5Y0+PZ~m^r7G^P#=AY zc_M^NiozxZ<8?Z~W^AOy8x5`$lIN%?#j$0bFL}eRl zWDD+*^XIV@ZZ0!lS>iYjUk6g&CT)+gVQSSLQZ()G5vDLe+Yq!QvnVq-{Kz=DcN_!6 zt-Slchu~|SupTb^1=X8>AM`jxD!I-De)5r`HWUk9Kb89>s!qF%_hMz1c=yR3*aYJlKs!Y_;#KDi)|lFsS=F18P8 zuFf`qQ-L*Y$4~Za9_sm%(pCr7A2Z?Y_Y9hD`-ZSO--tEsYQu09Z#M6AKzr#WIFX5v-I$w@Rl|` zakUnSFKRTqos|1V;VxKy@Al!YznK-D;!+HVcrrZ#W+sm6H278nw)|J0yDe{injO5T zV(Aa`)pU}Y-g(hPDn)#g%x%=k<8nA^6tqm7lLIc=5X7^d%!wHPjE;Bu_ZTR-H-KM| zev)gv6QTRl(ul$ZwCd@ZV>qFY&~Dk88W^RT&Z8wudp||#B+LR{1MTl|F_*`ZhF|<> zgpoeRfctDZ_H%V(|9xZ^=Gx~(1>&dx)3Oe-9qq7$N$VI(2`5Y;F6r$eS)*G8`3zpZ zbf3u96a-YTBp`!;Zlkr#JB4%)$8IR5wLt&3RRHrtb7KWjhJ0|Dl!m&+7N~KHY3Y<)7Oy@sνH~R;ctLQ{2V6a=LFBXI!4y5s=54 zBqQ?dY%zdR%@HbLHxIsi+>@mwSBq_1`2}L}Rqf=d!4Ds5tmY!l3(ihPZ;h1pi4WR- zD7#Q{zjnCmTeCRV&19ZUOY-cvOvP0LHfUTTqakg&7lmKWbc1DPRf_!&9CtKCu-2>e8*;1 zCxm_S9qk~!bnHYY{fy;55Os!(C0RevH2tc5h^?aM*Jf+Q#J6%{8jDe2Alq-4pJ&?x z1Df6w`8W64)?O|%L~oR-kAdQ=(7>q6P<3Z52nSR(Z;yT?SC=A;*{dKZ&APht{P_-< z#(-P%2AF4Twn$|gf_0_w(~q;ph5A=_o48f5hqXN}00W2#*QM=A>nYEcYwZ71)?Y`E z`_=(YH&GLb;2P93=)0Ne+90QY&KJ^!UeDJ&=((zHj66oC9}s<7A1)9l>Toao zGGyaVidlQNzMT=Y58s^K{iiGsI5}K**(}q}xo5ul&cFNP2zfhJr}kb^U7_zcmU3Iy zVGJqw->waq#P8^Q4nF(kMPh%FmEC;N39K@({^4*QboxIsm8Zg-={=HOtdzv^J=gyR$S`u7CdtUobA z^!BYOZNgZeJYYBvpWc1}(e(a4A6Vp=1}+En(9d%PlE=2GK8_l>s_%N+3j`4cIFZ>o z6V%)QIV6msZEetdlJOqUbqEh7=7IV1Xn|sLPkQ81#*y=oM(*p({B@vPe(^#-PLxdu z4LZDnnY?ur;s9E{#}(pE!Mg$JiCT}1Z_zK%)h~9!PXYp#qDDXm{dGuTSTXeA+c~Kw z1~ew_Q5;Q1}V zNz=&PChh0g-4lIpYmD@XM;Vv&w3`^+)aYvB1hBEb84EjIO;^L-|6JVE61OluTensQbi z*sKIZdI_$Ch741}8=d=cGS@gt1z$%Y9)c(CV)}mlN@J7rZZd%c7hf)Dl#K;e-sgwz zf|KKtSIK<_`^HsdNOhpYtB$;#)w(m@_h)E=#hn7gcV;(0ooDu*rZ5i_)HPzjeF#fo%qR6yoeuI%r`ez<(ZI2>@qgjA4jLwB; zSGBQ@7U(E;ROOUL5J6vuB7-NH;%HG>Mk(iw|AKim#Upp`B?1dr{VnX6^U=?4p9s3?I^~Qy#B!->rk;t$2I3?Ndr*V#PI|psV<%?9 z_#uZ9ikfGJb8^bqOtl`nT(L@Uqe#R$xOqRTIF0b?E{{LJ+0avxLS48tGIh00EGXB zM~wWIi1hCfTK7Wr1G=^Il<4o9=SuY2Bx1&MFH)yi&Xh&MmA)J(qE z=bl^y~xqum3ZEnn{W~g#^C?}=W4g?We^w@qpy}*d{ zO?1DjbIb7jKGTRb;z?WVq(lY&kfR1Xbqs=YurNbbe8~f4{7MdJd(p{!5u%3B$4Pj` zzo@Bg4FXL3G0m`#WeSSy_Cx#| z?x=$45%y7v6f2%N?ln|Dd)IZwXkhBH{UFkO&lP2@(tzscFf~H0nYKr~;-F$mEIdKw zjZ#)&nNwlS;b8!Yy|c{nKu3T?Ks2m(ZoWRfhQ}HSsD;Bf zh`~*0`!4mAr~8M$9IJy8r|~KEdpK+NvS+1NZdk+2<+ zQq$kdM(UpUSR;)b1v)ff|7tVIC0YYb6PcFTVCLGXsye!7cD?^BbauI7MQLNd_Q_4i z(ZXWoL$4U1e6ujDfOihs=;JkffVy)H;@J-wjlfgozod5PhF^hIYDR;ujei~8_zZTS zMwF&G3FQQN*=S(o0`Irp&=I#VF@uc&6N&wt)s8@uD!ntAJ+_?uIO;O%Pg^7ovdn&cHdI!b-TlULSsx1Aknvw4136xLR0) zhJ0S?Q)5Q${&>7`jb3?R=ZST=2DK2C0m|vvhYeot?DuRe~RsILnsdv3~FuTQ> z<}G3Mdcf=!x?84A4$v8dy-2ad^I1BSvjM1>um&hwaPIo}64(Lu>0V74Rkfz(QThii zyr0&yo{e41pM{M&!(LR=ZdsAZ`5pc4vM8F>$NW8nP zHt%FDe|dax(r>vA^kMl_zKDTj#>q1Zb-l2@U$n=lLEp87f1sqM`fWGSl3>6#YufkGYiqP zqmVEI&21pqI$7}5)EaE<|LE?CIg-OG%mO(%*Bh-@km>R82CXpJg|ZU=3D<1}3&dD6(vI zOE)Rdhg5>`@PeFRz>fhGq5wy|WjiU*IU#q{|4DI@n|)^nqv*UC{Fe2=8qYg1im(b8 z;R0gmi05s6WCMO5Lc^60F-$t`jMK z6Hp*D>$R($W$qyfY`lo^o&1(vyDMm9!|>j(hkq;gvv?*c=N<^L_8vG%REAy~5_iu# ziP-<;K;p5NDfap-7G!v|B7rmp5ZLi|9i*z`4||-z_Ok(+0L#1i4N=unlYWEOD``JX zm%2NiSJ)5JeKJXsOaj&39?cu6OYsK+nJY6HAMJT%(^l_8fuE}|<1F3eeJBf^8r`)` z&bSOM8np;ebtSU13coGQV4?N1a%%2oiu>Lyy5-Xq{7V0mS-maDXk#=t39tu-%vHnH z6e|P+$zGXS#etAN+Xh?hBFOQ;OMDx9|AvXbwm8uc%JpN*fK8(FU{CD6Q{julmUDqG zRq1rHxorVf2Ozn|f(FZZhMXU#tidyKHdJ;`0-zS&2&3a)VS+JhV-w<4?iW6x&NI(_ zfMOeYDlU_NU1lM#m#qP#FyLni=m$BtTo zKAsvB2Ck;Gv2lF~yT89oT$`5Dm41njtQ#wT;6&U*u6OhJ(TRL`X6Ur^0yFvQXiE_v z|9amq>HBY5M(};Q!>QS3=Z`ykpNflAHZqJH%C%owFNaTh{12Lv?>{I$>Bce+ytRA>m^`upbEkTA>>U67gv>qnq4E^*{NT?CF+cunoubU~Pn|$Ok zFz=W~4%;--&b~Y}o9q%9KRu6!(<901S=p4WdHi->j^LMQJYEhHzO2AIC+Yq6QJX2(?n&Iu4(Hq)$xk*)VY186EhMzsR zy%x0!CFs+Qi6S{Ghs@P{A9Mgr>kIx7$KS0(@VhmGI5^^;iocj^jC$%vj5Ut$WIwX& z3w+E2RP(y#cpAK75?oVI3mlBkuI#OKv#XwoY498!z0Y~l|&%#3EMiwt?GO1 z0r!aI4uDPZbT6O_=Lh*pS-D_#BKh1uBLmx8zE@45b_&yHR{*B^Ex?BaNsgeq`CTWK z^9f_YxNGG=F79W$VYirjJ9P4whV(|ymJrnLoMH$I<7Roi=AT$@iBo=ed@KJWYI(d@ zYgA&V?7sf`u?nO-zev;uUw!GfMB)}Y`NKg7AOtD+`*jxFn8ELdLf?Y^2CwnQf)que zn~iLp6M&=WgP=72`YSVXao?B5bBZRfBSI#{f9mRF1UXD77vM4~K0i)Di!tn4E*q@c z5Q8il&;XCINq>4w)cN9a=@h*4XeAH!5h5!2q$mmL7_^y*`lCM0@E9$N)CxVfy~mkp zw}K{g+|M(4JJ%bX?{(|v*o`jtio50`Ecq@Gf!ls8dlM1RG}i*CVyE7Sn=Ul<6kLnq zaNYhejV(OHx1^e9d)vkg}8T{K5esEfm`VTFUHcuL6VbCCdx z@S`2`Xoj-v69KBwo!2No#KfNvh1=8_*y0ly%O0E?hyy7uz_&U@b&$TRq>rOa$}=+p z?ERY5#@p_ao`08>V1v>sLg<3pj(yFA?|fhfa^_-_Cju;=O9P~7YJ4~9L;Pxsx8fw& z390zo5CE;QMAR$!z7ou3dO-ntjj&q`Ih9vXz@5*^AuCvxyOPMV*tyzP!^^Hq;p z^5+-}_VH7yN*`uCUqo|zsxWHawfvCf(muit8WS1)3ur_H(cZbH8E?2a(U0v8q00A0 zgh|ZteI5Tc;%4+Si)GCL9a{x1d@rff|O{eo!SS^CtvHux|Qev)Q2MXqB10enI@>Y>X?`Ees}#tev(6#!SDFs z6tfLv@mg+y3&P}u!|2(=2^J;EEw*f`q)%oBU_p}!QWmiY%LvW5Lknb7Q;4PkEPp3) zLq&3NkiAbw-zLb+r)!1LW89@rc_PT{tz80!t=_r88mhF_j{o@2_m?BB$LBf1cb+`z zz4+(Ar?+!+a318ct>SQf9@E*cIm!?|D(*CXU=R7$<2)Sq*brt;dQ{Z2aBz=x1d#GP z{XkF?@01K-g{jQ`3JqCIIkLs+!VRO#{0}n6LdpVd=s;d$d12psHvDpt<=_jy>NNz+naL-Cmi5P8zbKc0|r$+o2r@+4ZcBpHt z?w~iGPI(ME9D7S00(i!^?ip`)o?qQP`v9@>Ton&U?C4IdO%5dc#nCoD)!)V+9PR`pMhJ;G_mrvQkdY?gV ztxWzHssX%klM;SAKLA&s9_^|*Q;{C<=!=swf9&;H@6>!>F*p#!4EKX?#61+Fn7R#lHO_v zkk*|znEB?KEJ0~1$4QJ?@k~^w5acB@&5IZ#l${RXt{*Zc(S7W5&!s;j03*1%`U``o z1kE*{27rS*rhox3=@1KB2d3ZfQM)Jg7M)Z8i{Q$ll-qE~DXmSOHy;7j(th6Eo|EX> z+WIM$7Fe0tW2e0TASorOJuMPRD zj_>`_TLulR0`AiPo8OOE;{1ASkiO3boNlDd`)sK|oDP-zS67YwHd>-6E**~t4h4Xd zQJ?n>8!mB}plnkqMLa{2!DEu_B(Jt#yez=Q3m z%siUj6DUKfC#!9EhVC~49x&7goK9TOuK>Z}@qnBB1&BH(1@WdYX5 zt@_z}=|@hoRJD6NH`cu(u8+Jsxki1N-DyqfANY2_io3Q`7I8Cf-0sP~;9*&+ z_hIk%m(_6?Fa`l#^~z4n9^UY1R^ksIa{m*rKZv1JMrLa?gL315XPIL&nQ~RX2+~|P z&pwt@q^p`CC6_KlS|WdSqn24AheJ*|QJ&}Glz%H<{&!n(a2h)Zc2abWI*FK_f7Dk8 zaKczNB! zy|4X^e7fc{yS!mDjLQu!oW7OSq06`u>KthtYW1J?nJf>CGjj(%eGk^`HJT4H`c%3Z zKREH&P$wsBp<{qQz~m47qx#0`XWGj^7Whp`=Pxt!OyiPw^t6ac(i2sNMOAjn{B+7! zMA|(>$bcv93Q=fCtsf=5LuVhw5n7od*#w2~vLA?at5LuHeySi1EgBaJ^hEgRW?USP z6Lid+jyt1nRB1zL6$rXz`%g|c*WMlV4~`F1sqMVu=v?tamxv$QIAFWfyEQZn*fxm# z-1rtGbPN(@#bw6w{6i{mRGfemYV99WGf2_`5(ry-;6wTIV1FqNp@R(aSFbG$deG!3 zXfRlz3t&S?&DDUml3l>Pky5t(1^i9>+iqC-hGWJM^vfvfn0>QAK3DwD@xqryk3pIs za((X8Twl;mHl5o+{pddr1SK~P+QiLwjwYAlNnY;4V{D@sA~kw?ub2*@@o*7hI=Bva zJ&3Eqjn3cRs4}^$S(*Pz z`*Pl+VnDW=G0BO?o2tL}$-Cz7XV06;{p)B=(6BPdQM-DrJlv7H1m(&i#20cmFkZE| z!dAIbfVLqqoMTcf~K%qHFWBDgB`3{#gnBV>mTI54=DI8uE6^6B>^`4p4MRn^i$%V4=@Jfj1FVmH zS?8v+KeE%O{e$jjIZ_Vv7D*-hpCyeZEQgxz~a}S}u>y>&z{xDIe16?gwM14&EjL~Y5scO z7BtEG!1)XR(RMG5E0t(47xdG!gI`9J3%5(}7tDbu%+FOWHtYL|2ph>3w5J;b6S=QN zMfr4?`5b&P~(icxNSGyKyIt$OM}$QZHg-J06q`g7P#b6c!xy4 z1I<&Q9JYP6(70&jlLEchXCYH;U?R*I+wW5S{P*DXl4u1q-15q0<$Pesq~_%2$V#e9 zq6|EwUb2I}U>S}U1XP3yK@o#qQoN5VWL6R#+wCYV-mj$jua>JJnND%6R`JDuRtmY7 z+WS5C;&?pDNb_xkBipR)WkfE0TP;|=_kKCxKl?-cU%!SA8Xri&Lcz8>Q^Znr49}g! ztjhd=NzNhUFgs_DH;dyPgKnzym@NI10(`vk^76_(0avxYMTsL?-}tXr5hEA?pRa5D z>rpgVdTAtsgDNwRqqIMh8mD}HfJmthd#?8{F|L-3kPjuDrX%J43XSbxF5|1z=u{3Z zSO&g_jw3}~+b-xE_<+4hGlh%XBGlf#o{l-dj%t-w3+26gduwqJ{pgW8RVaWvD1*b_ z9~XQm+_IzCIaNIwE&KJl?Al0|keK+5Y3ofBW6y58$S zE{i|?D%sf#Jt0LmKsDS9~(NU8>Qvt}a|9%O*-|R=*(@oK88DUhpS)6#@rWSQf{Y%2rPWjC{XJFgqq5LE;Wf2MW8^WXj%3hCG7*NBZ_#=~E1AfhPq+SSr)`?i`gt$*SU!=s=Kl~lCwRQV?1Udt{*Js~%Eny=;{G2b z%l%OpEU&@aao&eC-qp3$`O9%Dn-B7$BflsSk9E*Z!jE5jDVJ3A1c>R(mHVR@o-z{c z4}S}^Th%qx#F)&gvItpe^&Tw#JZ18`61sFK9!hPMTe8)O1LL-}_C}|H7(>1I6u!ij zjrE2Jw)Jj*y!1TNI^gY-Kdz-m{zPO#y&^a}ufGF=8UIfu!n<|n{$%e-w3ob}?=p7a z(1+PCi+SHdpiLyELFy-S>{}HfY&o%zpeSPZMOiqGZIs+ zNBlHP;(4m9Q_sGvHey9W0c(9_kkAkDA1;8m%KK_IG3E~h$~!!A>8jr$0LSqK1Xj;F z@2|es>vY>T-v}Tbi_V5_9LKsH&oMd~B^zFL5r{kgksSX2;I0m9Ow_-n7x0_~#{lvC zF%E>ZUIr|Y+$Ut|k35);G@oc+O}crh?(y|UZ>*!>|MTnEKZs%7;kuE7Pa7Mz_0SGF z8uigd=4jD=x9aeodv4?nEvbGHi$x~Rr58)EF9WwzyXzbm4^M->jW}V&XQgh$3fAeZ zR}!*;5WeHHtSq{XRaJduQa{HH5c6a{CXju!T%U^TMWaT&KOB6S6xFQ>J#Lzi<;zBR z`}WS|AK^hvEySx`G08Hw{GF7;97mid-tLbYvtT99uputXW6Xj%zF5+OG}oB)|Q4jOxLj!D!_2Y_p<&Y;B;64XE30`dp=o(hgY)PHNN z$vhr5;Cs0O9*PXLsbDGQgB zid8-u?R&Y%`k?4N;Zt+1`1QZ2o1Iu8RAhbQ-e`6EC8_b<53Gxd|3^Ivpfv2*zJ987 znZ_IMoBiT$Q6CO=e?1J~fAkIUqd-dujEf?i<@3(qpb2_^F1pWru-+_~QXu9>fz}fW zaB=8HEb>evh-AC4p4%?Ag5l3iq0X3oJ{N~`m8jHxgF1V%uAzpn-CH2XNG3tGJu(tY}~JOq-W< zCy1%nbxqNfjxC$ao-nfwQI!s}x-;9fgQ_~aS)8A}d`Ey~i2eL5rYtsJ=0J#F{ToU; z%pGvWIJ!#_z*n6fKb6CX9u1NvTW2(`-I{JjeBsWty) zwQ(xIc0^zIu|de`&jFvhoMSl&FpLhN3a{=Q^OqLaC}5(w|6VK0eq)zJS5#50uwkKT zi5w%o#be=*0cbzsh(<}bY2GGj&zirlL|^`^_cRI};jc#(n%5V4gptT*!5JHKzvK=w zPQAi2lP6zgIO>o=oaFts?>$V4`r#Q)6PK^ElH?wL)_t*Z67gk>F>EZROya6TKsLE;#-!dZv@@2vu8nUPA$2O0PeJXIG}XDLBx?16Nm3oaww~YJlUD9XK>fu&uiJ5HZ#CDQl}1?o@bSb;o>z|eD!oh! zD48s#-XB-it*LuinnuyDh`HqgwiyIGdbgtc*?f96$gkPiIZsK2*)a8;Sd$Ix^3C1# z?k;Ddnx2S|n2^`yhLw4heIA9J0^5vD4~b0>>0o2nxb$am(K0O%`Ee{qP-&(2`xz4I zAX^YN%OSsv@{tkZR#)hBJp-sHg-&)(?4CF&azY>FjjLbbR5=x*{$WNz$hTYMilmJt z4lw-yy4+JPV2kI_SGLIL9~9Q8;vB62^n0Vuz&3BNcQ!M~6Ck1gno>={-T*jh7KOn; zFyB~u1AA+vaOp;)L^AWnLFnR8BqtCkx^Q{R%XQYA?f!o94wKX0;U&21RNzQ$x{G<0 za8dnkf&(w!+o+PX5dW71d12}OY`$lMyL)pxoot%=Wr034R__1Nln85NAjAd)QJgO` zUaowgM!E?!fthkSTSTGf{pl}w{H$<`|~sG z;g29M{3+l5J7MB4y7N>WsXi5~b39q`Sti5Odod$tjU=;7%7#N$!8KTAr;KBdq7|XS zo^Ob2A4XgzBufh!yqK7Zqd8=`nNgnsXM&X#g{^xS?S5{!Gh~A?$6aSLau7k1+QK zFG>0UXJy)wn&++f=tG$(M1D7aDb;)Y*+^Yg1H-)EWlr3r?IXJly;nPxg^DoZ5ZVRj z^XdfCTY5wNE59~=Ug>)G$=k%cq6SuPzRJ_L5)ru=v~6X+qwZO2EIbu}m6?>WEH_CI zGbz7q{LV%!!QN>Y{n%7Jgq}GBxA*nLg;zgR*W~=1CuwhC^L3DoRwoa99B+wcIHeOfMZN~Jp6~klLvHrx3EUB-T z-o0@cdl)kVWuX1xs(*#iigVX?n||8pYcoyiNwFl1{aP($C*U9OocB7rAoe_${s$qC z`S)1MpSfB5HjUS{-qc06F8Y(RfD|)w?S2fuD%;@L+b7YlL(Hb=b+NI}|G7|I$OBV$ z6JSlRY)mh`Wb$*rt=vAV#8XRkB&BiD6?e(urh9FTP^U;V+BhGoa zEN`|y;f}uc5Pif#YVf#Bws4JlsPSp*rc`E=LHo{%lB)wYu%9Gc0#8RtTd`QY`1#SN zo4VHF)vG2u4YmW#f3Covm*#ZTZ|Zz*ye+-PG>Le{vgQbJZb;f*m>KwZ%hfNi_O0z@ zK|I@(4+sd=oUHQM?CDl77mR@GR+`Btzv&-g0D+K*Q5O;O)16_Wo3^*8gsAXt?D2 zCmugn_V#~LiS%>eS>(f4)TbGGGNJ}aB3 z0R)WI^l{Jvt4-d&cO?vqc@MJFg5)Sc+w;1K$;q?kw&&Pb8VlFPjYD5{wZ;?&_3)BB z!u_s(1pof>1CnFO5iKR_A*5@95R7@r>utJ%Ln?4)h_^4 zA*7EsP42u%HFcOXo>e%ChSK6jD~r~Uo?fto0uwyF&*bhU^i#cWn_BGjDi@j_yD-+e zZLm*7xh$;BXV}e$6upO-2&7c@O9=&f(E`a~{Ci5}W%Ni2dQ{Zf^ z{TXq%{FE#KJGeO!5nGQu!p1eFi*at^a5F$)(hKu0%Z8chqwH!{IMKjN-nTxNxEW z%e0DKg!+iwIF$eH+1%D#;WS0vN^+I`_yV%|3_u$8S-QFk;D22Ew?&5hw8`gFCWd?U zSGaxNw-rF^)tc^_&gxn_xhFM~Nxy%0ZF>l4Jy9;Vj)0q9gh1J%N=x$}gg5H~hf9`f ztb81+Zz`GKKQaXn#Sb{@ZMb0Ptg}xQ%{MZfjocJ;?}d~Uc^OzV~T25xw`g*DqQfiL#*w~LT%;#!@N2kn_fg5p+-+YY8WG3Z4b zW3NC;IPOIwukj=YIVp7WHxmAoJSN-d6vDBYcrVmg-CJMzilxh4?fuSGrqYNqk@36h z82iI~>;;id0KI$T8Q-4HT*BIi> zu_?W!e zJ9j?U<(*AfSm4uDWN20ID|rA>Q0U-w*fd&2#Xxr&b9e(*oObw&&NPb(g8Peu!@%&x z79bGkOD6K?Nfa9Ai^)49MF(E)k7ZFSBbWqZAwd>F{Ay;=Eem9x*SK@dz~>Ch1t}~R zihOS0HsL12xlQ$MgK0f6`k`q=Bfo@0uovF z{qoOTcnt7iqY4kpyHjLoE->;b6FSpRg^R)4BPyS9>b9m&=!CzE&71ToF(NlHpq~d9 zx8vIc9c3MOn2%azf(tJX3b-#Dw7Mt8scOl;dv6%#sm>w7Yv2p%-&A=Qbgehdq^h%I zetIKRn?P_~T-5iSYi1_JF_RTfyYLFr{GJvd-+joCd}!DON%qtJ-s`P!i-^LXiIRA^d8WE@Kb%E5urbOdszp=K*_!ZLu3b`;w6tiU%pJ-i zUt7(}=i+-6^eha_kEvLh{pi0KXGoGuMqzz+^ybw;SDgD>48K{vKkszgP&Rg#uuZ;+ zt_*IG&93E+@?mS;kY^lf<%cW2iA;&hAHJ^KnI)b?QzS8Pwq6*&>Zc5F*fxs^EC-mi#9u#9Ysb^2cgw|k`vcl!X0BBJd>$; z_3s3N7``&(p5J$djaPZ3<-(^4vJ7^r{M@yosczX7Vm;1w8`R3_e&>$A#nB;~CO!pP zUm5$D5ObH`(3fhetb-1Ax4am=wom35!idbbre{cSsMY%o1~sF9_FfU81eM8axedy-XHB zjM|-gq*r=tU6O(C)VI(caG-@6D`GZ>ZyqoXXvuTR3f2^P`@4BNTX{^1QF#`aS2$4& zjC@l0<%^2Wr4+OI!GYMpDc-gJ~Q^`3jZAs2K3%m7jdYxuj!-Xhy|^cg`1Dt-n5Dblg{kP z6R#vAoCol}UBi9AwM%xnv1VQZx92JAnp%WMS!t0joMK)@YF0f_>URxfG%-#NNfWa8utprz!CH~h}HzcqoY_V z$TwBHLrZ)OScfpSf|NGvZ@_6}9H9hi9vnX2h4sj1YaAm6%Y6n7MwE}51c>xYIJ5pt zwC4v^_DofCA=;dxF$yzpEY0Z`K*;jU;*-};?QZf>J7>zTbPNAH3qO~nq=Hd1AmpN0 z8yy$=whL}o=GYaQ+r%`u)Dch7c0L+TFURn8Pk{vJlq#WBMKZPE+B2K=K)Q(Q&X!Gi znys#9EqbltB>HQxh1dtVh5R(>g!c>9Uxf0sUiWc_`n63<-33dZDiVP9inR<5%Q1}$ z9vNacLY$o=BbHxotcI@#OL0#WgEW2avWnD9F(0geUXjpVST})#^`x{2{@s*wWVX|} zSs9t1V)-p&JZbrlNzW{lokP5$*AE|&NB?wcRIoBDJDz`;G<=N+Da5+zviqD4Fm*5W zIq!LjzvfQ7{mi5T`KnXL(X1OSPI93k7#7p(a&D^biRHh2l-q4@UYXp^$u|=dmyeP& zVK{kb-0)HB-PE@Z91HFp3t5(&cD$UOcy+@z!x1fw^zb-04skbWvJoSAxwxv8&yC!q z6x198fm3FD0*mwJvlv^gv)TC^nat}@hq!xDc+DuDV*N01bmOffD}mZh))Y4~zZ~(6 za+n-zw$V;pn?Buvi>_|TT_e7l726ka(3Ci{Uu}pI{fN&3e15A19s1{LjPF0;8;(UM zav_K^fBG4%38{&63&FtOaeT?{RO+(oc;>g!DR3|1y@mKi-E3)Cy!erz2N5SF@4qRG z@3gz~e1Y&uym3$~wgHlk6iinLtlaco$+R~cm5RUe)9%Opp^t)!-iLQ-8!*aw(-=}o zG_~9*6h&IK?9(liVH&R+JpkB2Hrz--QI|ny|dy}su%b<2|}=WUerVDeE$9>`^)uU&o&0iG@1Og zQVu#|zudy2Jc=A5mfDRJ`e1N4R`gTkVj=o?uMfPWsXsy-tKiA~#2b{AXZH zg??~?)2xoQ>OOU6dxq0}VlVNL&JM!TpEswtYIvk#D`9H!^)1JK4T%@V9?q7*as$Q2 z;RM`uOy4-h4SuR5Z^!pNTZ*lG@A@pC!}L>MV1cL|U`=t54?p}CrF#xO-BR)ml_68X zy$}2^1Uj5=`wXH?bDMjG8s2E_QKpBvLX@^v z7Oqhb&_spTt-FKG&e41Lu+aH9IwSP<%(f(W`{({l0T0T$Hu_-gv9}ZW5XJxAI zKTL@)Zjp>?=#5xy;xIPvqe#Vw83=^6y=XoCoFz(_?RZ|g-_mU2YqO3&TGVn?B_^!B zjb1&xigWFg*euZTOI6ey{lGypPO3-H*}9m8eD5l1THPgJH`>nzK4B3&+FPAsVOtw- zD?If<*tlNwiH2q|mOq1mxqgC)r6(E%fG)tGYTv`JD$zz^S3A9(dW!dqVA+@ZWoFXm zRdUQ3f%C8LThBqS1PLB#RSu5VFbE=cGPD*Ren-B_lk#ji?!Q7d^LuUf;6Ciq&O z@^$|507VxQlWma;g$l<{45#V#@9r%vV6OeC+>lC3Tnq^Cf$XSD7?nh#Buq<~!>2vq z-7`h%R zvB&!%T7IOrJ!7tov9tK!SAF-(l^z`(W_D@3c)^}VBU;L@ZNACBRpH#&W!Kx_1M_D_ z@6)zj_s8T&-WG5{f-bxI^v49);bz>-cP{hkOL8t#6>Q#cTz_IHoJE>xSe56NiDK*{ z^R=-He_s9>ihObRiKEbCIGVw(SoW1HE|1MUhW^{!yulZC45R^dLGtI+&^slbBnS#ZKiTetBZt z=UltDF25N5T?LaD?Oj44tAZgg#Gq*9!pyGZG{IKj0rASMeWu_%v{SxAY9#WyQ$|p= z@=euPs^DO>1ICsG++k}c;$cE~sYDZmJw@r30r17DIJzW8?vF4Mm|XEC`r#HbEMCQt z;Sz!RsC|t?GLL`0TV=7k49AXTO#O8*`X_*#-Qa>1psdMBuW${RRNUJa68(NM9QD@e zU)v$*A8bVfn1xjjCO>o9?S@KbZV00re~NyE^Q4vGfhC5`$>>%u!}jz<$!MbXpDuJs z5x=u`({16JhF11bgYwpd+py+6^FR)9d~HVsEyVEU>;m24>aP*N=VW&L%_v37u{${! zOPMiIfgb)UYSc4FrfJP>cf~26$quEpuwb+%V$Ct)epAfT;tJd_c<8ah)D*U;Wu zp>~u5xo5H9HPFL=pt(c+(~r%TRF-N~7V+YSn#hSheN~*ADm?ma=1|jJ z^iU`eL7CgHd|A0j6_klEMk7+p4n74c zih;k(-rBSxiq-ml6#&l{TcTp*hMKtHi<7alqrXG+O=-rit6~*ckKy8|z!Iru-k0&p zQ&%}FMxTZ)^2bkZQ9`iY3H2ED*SXtkA)5c2x@U~e2e|;{#`1}Vvh&F|3 z`CdANQK-aLqo{DNV*);r-ie$PvUxFv{0wF}DDlP;m~AgCNukr!6-0}#9wckZtx6rO z*B?MCbI5n=9-s5{!PNO)H7Wz?!Rk5-{Ao3DK^N9zt@wz3HnyLVU97c>S(|p}?zgG? zT}5m1Ml@s?zqrHkLxFoi<%NH|ne@9zhp{)U=1>HHPhvpsp-2iu`ONqfSug9!?3vPe zMTQ6WLlp`8V_n)&k`M%ot&91qjwp#HTFm=fhNnf1WbT}CL69xIFJ5*e=AL1KNWRN=qaO-CZd+;@s544PRYQOX0}WcmJFW8RXH zhpR)R9F^E22JS5`xzuKP37CmK`sE981sm>WLSwU85K>&|#J0SXB`PRG_Eue$3ABJx z+pRtJ9qX88rn1kMzzkAHFLs*fQeGQzQQBBK$!k71`R4h{^RaU5aR1q!GsM_5CKCZH z#SLH+UT`(bM5!BEIBhxca*dv@J~{-RZ7xRkNBa>V9(FBbY6vYJt3?E*( zW*4~#+$A=+XfEg;Obk5lH$gB1&m&Z4wnoHFVu;v1II4yeqhf;Os=3+Dw4(+EwNa&3Ku&)4|jD)*<8 zSIjL(u#L&?xDzj8YVB_a1&9Z+f@*HY#kIIpX@Kaxq}aeGN9{sqjQ)g5aYq^MWwMP) zK^@sO37ZS_t(%)BQCK#<*Y;}A1QGvOmX^wn2;Htq^Jq0b^hX}Kw&NK@QnT9Kh%?9< z#u>3Xkq~A~@14mKKgub4mmZw~L;;gaDa1n*$kyfKh4>^(tKO@G{@ii}SZp}Yi~ zEGmTsPm$?h3A0ZFa91#zW)l`BZpIFkGjv6*YKq`Fq6~?6v!+ri!ti7(8W*SrvyBt{ z7KN_tkrl?16~TC-7-lgv=5FnI3Fk}FS=_ZDY};1i(U?}jkSa+*FJn%rtwzl&QwrP& z;xe>$wg~#;BI2?23*W@LIqvPRmN8l!3f?VmWN?-D)RN-{Wq;`Nnb+6Dd-{asO^#tLctB|&{lK+|V+-}{z-%5bq_1uk7-B^6aJw`imJ64(g+BKBLcWd|@ zNM-EO=WM6Bn~S3EZ;HjFAiZ8&m+bAz3VQy2W-ersv>OiUyj|JvWY?Em3zR!F6vFKU zEM_8k(~6>9wH;MspdptdF6HKBv#d{oPk?~A0e95Su*kC%8ARps^nZO#aS6p1xFS^U z4|W5PNp$rhnbK`GFCRM>6LtMEg9{BiIKnuII~Vn6swA>4**{)@PrAa+)<}jG=G$gC zYdOf$2-&m9X5;h5B?5ak_+;_sC2F?l%S`du(i|(G$7{iXh=zEx)FH;MZrzKkD-piK`=TyphrvN|RD^%JexN;B6Ns zlGDSFF)j2{HveqGEGAB=wc;P*zU_Q+DXT8i@Lf%L;cYHllE%!&50Hp}els^A>tlof z4}aJ}jSt3jXB!%{##@n$EuA;V(HgXbjj_-(%fk+LKqdA5#zP9lhvmINl_jz#78XXD zI;whHy`4EHp7RIW(^rpWm!Es&~#;Ib~sm%nj;+6HqFBabdY){nL9V$%glm!_u{6`Gv0f@5m;6*E#$ z76xKLCSqg8bP~LnaG{qo;2K``6c?;KrS$&PG*A?$N_bFxn~l7_IARpK%}@}?MJ*Nw+y@2iBA2St)rYcOHyssbX>U;3y(4S@iloo$eHSb4J+3onJ|!&weTWI6;VrPIMC`B+;WqPof8rM4~f#XNb-qf@p(?h!#W_ zy?3Mc-bOb>@53mg|4**#zMtoPe($IGFrW6`>)dOt<2cr_?{1Yqe~Hh=S_?y(kM%u* zA!B|F*5l2KXUBy$K`ZUH8UPiF!x&U$sub zg6rss1C&%~KaIM#bN(57OGCQp%p-S`L1|Lbzr=NQvmSlUJB$>r6FuE}%6TAXAp6%`P8EfW@SB&JJ zYHH;t!o*L{6O-zEP$oNQlG8q5hSwHCmd)stl=2h<`-GvS;VPmwC5C>)hJ8)p z(H#5s_q~c70xvPI9(teg!Y>{i7R7b!%MjcAWttFZU12y~IL7eL|D|&&s$`ar=M=u` zmjqbikaN@u>jbLh7a?!pU0;d9lNjIWM`?XQ`+K`8tmpCVvPYdRC#TMsg%s(Tx5P&_ zGk1x1C)4zWC#Sj*?04vtFhY^W8td&#C)Ad)h`5IUw(8IwMQc6vzI9>XuW!BQR6r_EaLGPBX?KvBoHQt@4pF*J0yW8`IH|LETe(BSVUIDnv;o z-f=lH9PVM4xkf>qvYPRNUzJ>lFUVFGi{lQlI~`{9b?V?@w8P`mIAnthmu@46_amv@ z+{7?Cknh>mt7={8^7cMJP`yH55qJ8xGY z1Zs7L;2XNPUbtzU6Bo#n|80@Us3&}V%RecFiCU+D5qA%}KF!ucl(})42m!ab=_C+y}dMSz#z$nQg#vMPBTz_{8cy8l=2dIS8g@l``zi zB8VGK3T%tc8b#L_w)5K+$r0bYG0icVJPfTL*wcOPHi&~6Kr-_30X=R(6FIFi?D^Yz zdV?d#fw>hruzSZ23Gi7N6IYVj%rBT98%B`;Ij>K=&Ju6Vo$7Fqs*tU<3WLU3G5!>S-VE+c_oRYJhugb zTsY@Ee+y8Te3j7X*DtG3Fk<+z7i8oW!1Bz;SAwT9+}pV4A;j;y5k-h~napN)FITRZIy0v>A7Z(yt-B5VrY95+k?4>f4hP*30h7JLzgeen!4C8ri= zEmF{Sv|!J5eBMP6yEdIM>JuAJ2cp#LXP!Rvc&V@TYIG%MJ(o?5f&7XDE_ng9{L`*y zM5<#u89P8t$0Z~1wc^D6DCH8vj78_M+(R?NrVzgZhy+hyP zL#V3H5F}(nVRV1xg-YE$l0)2&8Yf*=6ih`OeLeSY7~_O?nVK!7?xeI}fiU(hDWAiS zZu^_yRh`3mpVLEd;B100I-ha^hOTEp`r4Qp=EwUFtE9^I*4?ZcW(G$QwuV7b~S@ zWw&Ny8gO>!Didw_g`N3a6);a&1P-6+|EtZhwU+)r(wxI2zHVnRQFcswR|0B((r>29 zV{L6M!0XJL29Y<})o-rOf1nNqD!Y3lLvy&0IpDs%N=ZaT_;~Tawi!PPUyz))WP1Ei z4ARyuhmINN82*K9EreOM$2)}=n_I`w6AEHRK!*x<=H)Hb)l)!CH2t1Cx8|(Bv)gT+$0oq4X_J> zc@2p)!E2K~3)d}xo7dJD?QFe-Al~!<*4YqPzLhB}C>DDT&+p>kILkylL&YHje@1943o4&GdwHEPxqHj5XSYkYb+<#?CoH;xF7`Z~ znvqiC_vwUejlx9F?@Qt!3qoDz+sqz%>w~;^G@vacHb)EQR|^1ago`^th=sCxQTOSV z7s|J<$u}isMVBq;{Em>>JB*o}j0^T`9TlE?jWgCU#DhqbzZ=6UKs$hA-NIQS*i)<| zm+>g15348ISxusU)a(K{f1pUM3CGA{81R zAa=VrENpswRUce*cUA^pYH`@fUiWRXAbz>>COPIT?_EshuW)P5M?dUnlgxkH{+w+( z0_d9nJkWX85E^lD-f*}H@(13UmzXGBwawkZWqf)=>AYjwnFL(Ci^V*xnu(7jGPJ1j4+->E+Jxil%N}T;dhaZP{{A!*f zYy}?Y5IOr>$WWTakbGS~Db@hFDFdaT_hP2<$g+X$>G2fnORW2{4Z?nDBGs^%_0$Q@R23MP{sQl+zRwYM7lXSb`R-O+7QLkQ)3_i0 zx9?%9Lt%M3-(Gae0wg@nLItqrvy3l(m zO!@pT+oefHiY(Tahthu!wnUy}xVW9DZ-n=&2z2Cj6S$5g7ub;{P|Gm7o*t+g4%9ev zXmqd{=+ro>Vl3q-LI&dfwWm6Qemz6vfBVIVkPCgR>Z5~3K0cphpq;vPJa@+Ab}-~a zm>?|GE=gQy=LD!*3iNgj2^+BSyzh8;q>jJ!C#lKh0WNyQt=UmLkT;Y?e6Q7PqR5N- zcdpsh13z}oF8Rm6=8U1D!GR?38cF!MlQRIi1cpS1#R2Snj3HUnLE9<=!`3~xex0|*A@4xhDZt~G@)(8xA4bCum$Z`veV-4W~y^F=1Y-h_jRj3 zVVRj{rGl=K*398C^#MpK?jCz~bRip^V<^NuCB_TWDUdG&KYN zq!_F|ag~YIiNF6r?p@oLmtCG$vf5!>oezr=z&5j|xYRV&1V4ZNOhXb1r^QC+;^fMi zn*$oT16<&Xt1q%1*GY>PmT%~IzsfFy`_JC_&i9EDZBc)&lqk7vkS=^>@}*(6;3vhq zT5F;h470KGh1V%%e)MV@Eit7vzZtEXS-AMw@NhRa9TtXnsJJgq#vp1oQQ)W#7P7^x zcl5|KqM|g36u$v8>4O=O3Md^f*vsEQH&)o9AU=m0(VN2)WO*7kJ+Qk7If?;J=GnW` zUcXvHanG2Cf;)SBO~g&yNg^35@#WMN+5~Ka51768r>s$xJavtgJ4FtQX}m%e1c*Xp zk0il;q-cx`a|cO&z{FMfyZnx11+#*XFk=(R{RuJbHAf5z*_5Fq<#)Ayu^Y(LMF`XM zuZb1=k2v)^bseqy{*i5LT!LTMx@Bx!V!-;EudBWJRgoX)1b9W2#m}#|-EL-dq($;H zMc5;}595o;?~iw+^jI%YgEb=GHi>6_{`v)v>j{}{GL3nR=X@6W6tdZtyuLX;AQPcx zzu0aWn*?u_55e=HIy{E_ZAd;sBURgkjW>yIfZTfqHowW|pu5ytHJ*eoAioE;3EbcR z^&32_-k5H$T@nmF+|JEi?HQ0)$=Q2bEMyGdQdpyx+`40ZmyUae?L%;T{1&syk*opL z@+-jidO%eYpL{5EqXv~(@3{c>p2yyDAGxoFHq}WLROo8}I_Jf>7t%Eqq&Z$Nfv}LW zNh)7lvj5_$&Bo6bvGTwEi>sy^#aFO;fZdO@Fbl=TP^J6&cd8r~=?vW52w*UqFvf&b zswoDjK&TDatk3oS_iwlyi=Wut&$~ar28+9N1ZljIyg2arG8b-&Y4Aq*4d)hW%KQtj z#>cDZi<=obkoN1t?{^F&`Z*4{)_t1Blg7s8rI$d2!(M<@iO<4?y=h4VWCO-F3)H^< zqw}U9yhE&g-GK9ZLv{==dw<2nDFVBTOHOV+-yX)FM@qdB^5Xo0U%W2rVuyx)hFSvg z(mZS4F#w;%2+Y4iDE?m3rj4 z$K0|t6N(32S_6L;*0D_m`Qes6hVrv8*ZSJ&li!1Q>?5zTu39T|i1jaC+aE2*$8x!Ogu}Nt*>UG`?Zh zh$1}wC(lQb1My#IlsCyRbOXID+5%7fwRM6E25eLZ7(P$W?{XEFmPQp7@#3keE!v+Q z;q>;-!18){rKIjeMNy?8>1MZ1hKI$`0QQe^#F?HJODQdl5X98f;-wZ4#KYV-DgU;_H}w5MUcLco6C0Zx8DEL~E6%_x$@l#3pXz zVQ?%P$XC53wX+4F0iV3*qe*VX&Zz1suluenJIPW2a&g!?6Z+Yp?bmw zM`a4DPuwy613pAC1^Rk?>bL4%+0U*$hRHSkw&EJ(zpEw;ye7U|w znC|I;X^8uG{j7T~prM+!H)v8bxz6}OBacMQ?NcE_EGO+>Gan+~vOjnC@7zqaB)3i& zV0|+_o|R0TJV*;@a8>o_xYWdq$QC&u*}rh%Iql!xKBBcQ2^9dob-l<<#DTy$v8rTO zk_Js(zp+~CdW^~0(KZV1T8AxEpi^n2fDyQfUFHyZ9NHYV0(%)2^ZTVi2P%Rjk#A)5 z$!Y%$-OhV|Vl(qwC*t-TFjwBIV$dFt+j4)6^Kr{m`Hiq+ZIjNjhM89^LkxGw$#@4A zocN*G0oE&T1_DXl<7qfoJUaJ*?s^)U7(C<|dYySKHjpy)&t&wFIP2!vdfo8UPXo7r zfx#3)I#=hyWdl_AVxhuJnt1imioa_i6gOlHfKf#59R}#x+gbjAkmeHJWHNsiVx%BW z!(P(1{eU#xZ|f?&-}M2K_79LroF4vcGTc-W3D0A6Yl>^&Y~Jn|)X*QGM1u4_Linea znFJY`h{Cs+l?8O2t5q_GZSog0mRipY(i?Y_Com;=Jm@Z{r{JsW0eQj=Kd8>ej7{z5&{_?t6xl4e!@6sfnJb|+d?J=7uaBLw0 zv2%aKT8)DGx4JUwa3zS^I5M#n13mITkQpTh9e+7XDqnK7DQ2H)Ql(thV&gTx0|4E2 za_4FYgO|oB>ox0L(Pq!x_gQ;;2U=2Hda1floFZ`Qy@xwe%qWIZ01VeK7ijUUu$w1&%r=6>t*};v0i`gCmt9N zG5UI&Acv3ce&L;O((^-A1PWup4sJR9z_`riT=m$I9-j0X=9<^9PZ3GIKhX337zxL)`Ry zW5KAspU8Ig@SpY{GAYT!u2!>>zBmf+VPkqSDqR_1u&pnG)<=y~<+W+sDV#D9?I25;}+&&pE1Gwn;oM#UO6j^b-|f66V=5%(Q7D-Y|dFg37ORs|{#|*kAE~ zb@E@z%j0<}q4abriYo6MI}7`mRkS}fADR>v5ugyazz3#4%vw1vleDT5Zob36fO3{O z@G^6j-os zNv)@Cq%pG)&7p)bNNW46B2_m$aFm?=Gj&S!m*^cHdQqGNVZg1qsmUUznUYr;h%|SR z1Ub{GXJMV(^zSfq#v~)CvaV!WCC|Sq7Vv{+0%)%8g9n+0RvY552+)_2ET7hbyT1|> z!^5lB+-!IK3%HvY4D&U$?j-8*in!y4Gn|RR@1QO{cY^z`JTU4q;X!gCVytDbzZAb} zp>a~yghi3wj#l^Hfa~rS{(62piyP2i#nG&-@2Kk&YEq}{b2#bo8=c2x5OUD9=J*v{ z52k%9GB*Z^m$*_0Z82~hZ@}OBpN_v~c(r_#VV!Gy9c$s>XsE8B?t!Us#fkDEbdofR z8tm>|Zn_ub_jSXH1K?#Luwa3cY07t@$hNlkX z&&;h4&Mc09a&4J0$+;3BW`*xvtIqr%1N(JT0K)#wkjyXTJ?ZKW=iXI;q$(jQpvr<* zK%LQfH*z2_GykjQFBQgFYOuGcs3fpu8@%d28A!v{r$|6YFKC-H2`WtikR+1rjJb&8 z6VqPlI#3JfAg?6r9##!oF(&v79sgOA<$ynOb7i_!&%g^Rff17XL2hAV!Rkhz zptP*#KS*gkVhX|LeGOx{bJQl&UkUAZ`x7}-8pD67@{a-g6Hg(&cKtP)oKMbN@M6=t zsTyc@1oggXmGOUywZmOgTTtejQpw658wxluRg?c@69v$=OfxIsU zdg+g$0W2lIuqNOWJbY(uD7qB3F(&H1FK>@tQ3O6kOn+jLsSOPB@fOes{w|EDm$-Vba$L-X zA;xmcV3}jWjHzWAm=Rrx+%c$X0N2_5l!7SEPv%v6DC-fl^S3ErCg4au!$AiPU zQ_o~*ryW>{!q(`!Q zH4Lf3V>e_Jt?4 zAc03mL~C2hP$4kZPze`jZ!w?SFx+O0Zzc}*nCUsVuUU(d7 zg=^faa&)9@YGX=P#j($pgC!~Rnc(kAz;5#pF?=}+Cz>eouAf}B4JOIwkOw&X zn7rtaItSKq`FB3b?JcS5G97`W40K?(BP~tNb!vPMGzCpER#QjcNXVNk`Bcd5bhbe z9#T%k$}S|(kWQ-Yh2@v&{lM!QYBgURB0xjDcRlS74#zcMwDW?BZhj=^x-aS!@7h7M zbp7qL81C>>*H?KDOe2Y0ccc07luA>x#XuT|J z)?(Yg9?3fyh3q?w-KE*}>@`=8-uj3jnfO2`@^5~;?umYI7+TY+O!6JIu8*MF3cV>a zv(zO^m18vjg!qgh3iJp-9)JqI=PO~KkEpi&N~alVW4=G7HD~e*JN?&MrIwlK-s| z{y!yGxIT`@!bcyZ_o-!ULNb_-j1c$i%y1W&%{AXtU)wGA*|au>SnN=7uucilu*cvo zOa{~geWJ>BpBVJhWO|CxZGeT&z#dKV!nL0Pu1?ORs%(SOis0d*Vi?gR_erR^-i{}) zqy_bt0GcbZp7Y}ERKZ?QV}VuYziIG$iTt$I0HFGIvzGKQ40pZqI;vi+$`EuzU7juY z?Rrol|FR-}?{U^=lJ+teu~hUSsLdiR{-?hdXXED{w9=&ChaNA-ELacub*>W6UAjy> ztpdE*!c5QnTjU(CQ`z-&xN!s>pq*9)bkp475sn0rxP`Z78jSI3If~Y$(gn8hfkGS* zL<%hSegd7_cuOIf)yZ$Zkh1$2cg|y5kiXCQj&0FZq{^Vgl)Uh!f}e(d5Toe=e?#F( zl|^iOJSM@4Mf6g%29P7~w?}>x3860?BWKzd9($o|RX#M{#+qzx8-nQ#>Mf9dqoR^V z28Q-b&epn?%1w?1<5$gLf%+pfE-p6kBnO>Fh@Xg9$I7j7jgU;BOV&R`ZKm;M!2)IT z6_N%b^gRi=XKUIj@6sd=4J-tLpd?dzQd!S}cZ%~jvlR0X(<3c&b5o=j^zYHCy@zY& z*iv|?gA64bB=OPV`o&xbs&<^!OX&MMUgUh_0G`$s)y1LB})>PqS3qpyaq1v1_{a5 z^2+=!Wn4{@ZG=Tj7mrzTZZOeoF5qZCgMFsetFDTobIL+{mK5DSQi|z^Z}S zV`nd0tKq%le^yOur2LgL)5?V$x+L(YWlY1c2Ntwda{kPdR@R$lLWZ7>#uty6{_|wO zohqm0dz?HAw&EMNg)T1JIwadArF-tq3NBN-6O^IhTfKzt??pVHmL84Ee{eMK~@n+S_NeqT?bm8Sw~IL2B}9PdNDX~S_}!a+%j3#Nko zE-j8Pn~C23ts0CPgfVVW&!faM57#hfCcEE^&p#%KIXtY=@s>6FA=k zy!4mMA%DoC)>AkD+|^$kH%c5g<^wHJVH3gO&RW}E!+9doccH0?5<6K9-ODV4rfO_A z*?d~+IGRSUW*e%V&LVP1P~7R`ZiJO2>HP~r`gE?=M+)|s7Sa5%e=@@6gDaRFke%gC z@CxC&+D>ATM@8|{T-Y(S8erT1MQ|Z*)$s|oUGlLU%RQ_)n_|hN0ahD8!&~(|(gJ^N zg%OD2FM4zy3%19Feui~g@;nzDAD8hDZ+9r_I{u)9kWqD04ebq}pS)sOs_N5x=fUQp zM%elhDPZ1JQ(iq3$UM-y#i>gR3N-iLBY5s|{=D}iDnH@s+!*B7=f;j?O|51Lt`-Ul zM!YaE22xXyTt9WR5HJkGMq*jSLk4sz^a|}Plp_<}L$4z)mr#ZnBd1=Hlk)-4`NR9^ zbYdkJ-()IOpW6s@WW8+W5q_ms+QmWXxHTiE=`je@8Gg|53X@#8PlfRvm=|Gz+-VZjX_?74neBFPw@!b629!+SHLU7 zNZA%4#5G7)_JH9^JegVGRd`RkjS&gh1AZ<&C2d*N&hkWT`hBap9J6PX3P}$zn7~Xt zEbz3%+Jc)4_&NZrXt^bDp>rtBese`d@;#C5_BN^*d?q+iZJSVZ2{?ZYB}$!rFK4OP zWN6_evn?Z#e}5Uq6&xPX4jL|cd@#)7a{Iejo=>&DGp^&z|uu#oo!+U!deKX0kZ!tTC0k`ISVTqb%2Q`~z(#cX^v@J#3H zX#TSgoHQw4rYO^PjZo`HDu=P_jGWpV)X6<;0)BsFA+|{qnKtQ`nSd8;6RM)BI=^`X=<|gb-4bwiadI%3D9ICm=zppMUx=ml6@5L0e?a;EF8g!&H`MsiQT$NZClzgKYV zKLqNn+J{M6!!Gc|vbidE@;9%VuLLccJVyw9$1J@5(_&CjZ|E;E|=D z8~ocg4h}Rn@3$w2#G4a5QNbe;K#nThlwo6CZq}C6NjejKKpBR0U4}i5hVQtz*^iTm zpMF)jg*`iy^O@Fj0pB?_RXS*fJHDbgWf6MTwsf+&R&907x|Y)b;twO@KVkme>p8iW zYJx%jES9wjQC_4Ivei}jmaNO;6Z^kK04oV9Ztz!gFrdVf;aQ@hdJsEH1v(2uey#-^ zAn-NKWUA6>DQY1z_?Uyz^V+yuOwx6&FSM~D;+xcgjMa9VV0xwj|XQat5Y-K3TyvlA2 zrQVq>Ct#c?#j6n&D?bjUB(qI~@%PlDN>4m74~n&KZuE>IFFl^Ri%VzT8f%K*(!;%J zY+&cR?)%Iwu5YL$rZp3uM^Q6?M`)oIKScnV!>m^W_9r}8$jGShm88tLu&J@my@s3k zml<|>$H*#Qx~EJszzJ*#v7#k6Cud_D&_JB{;Vq2D@!O-@!=hg*c`vene7$N$3L5CJl0EO-9b9l+D~ysmV602t#(-|>OH&|BZx_);m3x@Du+AUQJ|M|s8# zDdd*_=;gD+^xrGc@Y{rmmLfuCDS21 zkktzinI*-{M#|2wrkrgRB5UIRk*WZfU*8M0TAJwns7cajvyq_3#AK827H2Y)>^HWw z+%6`zPfKiW-HP^6lzI7q@3}c9P9NlkJe!Zi#sfgN9JY$+622-GUS`ITYcm#mnV)LE zS!{dbEUq)b-9>tZ8^r2x**wV?L@&{uME+^6+MM`&_~IDNb?5&l6DU;@^@nvS0(L;> z_g}0#C<{zb_*^0zZ&kXw%aQMkLLY52A9-~_g3SZMu5Lk}>=MBgW!l#K@jVvM%t z5-gtt=mzfZ7Xj13SSo$f{QUMS&3D|60;uL^PSCu2>BBc>JD?5@|Ax;`E(jKw0tc6_^4{;n=CF@{8@a8b>sF&h0;dXB1B205~2QTm>LX6-=nYEtX)BLMyUIQ#w#GK>ldSMyr95vEf*sdMi|){6wbYYX4{>m%wv=c$frePq43r zI?Wt@^$e(=o*f3eUYoa1pL*b*SXECAu9$FBFj?nJdswg@wP5d7aLU@xo=G7-n&BMddLF|xXMxeLC0hGaIe zVWj=yb2fKeBG_AydTY!(h!m4<_QOOxSGzxd1BV zZMc5uVSo$`z1R!b9Sb7;RC+i~J z2V9LwA?7;FDy$_vMy4+Qw!s2|e@&05KcbZZHy*T8WL>+i*@u6zX?no8juVfWTFh(t z-gykfD-=TdY_8Omr!K+=p6ryIu6Tl<_NiRs<&&I|8; zjkrAZrk)T~w;3T$@GQYgkt;L9JPvs);$<;~P?v!PH-sl!+EIRFbRB!Jz~BC%M}|G! zg~Oo4%(AvI0x&0v6373+NV{hl&-{K*&MH_lnoWBAYp0W}g+-7Bt>75SwDRTev7)=} z`VyW$rC2`9)LWAK{L~!4GN^WnAWrUxysLKc!{<|OVI>vgWCKX@2oNLi)UT@)*})2) zDmfnQn_^Z5eEKCh_*ir5r-+o!Y5vVz?OT(hcvllQmebJxyg;`OpDP+AtZ@{&BRLM(sV|9uH=g z7DAW^|51EdkZHF)qxwA4&qz`zUftQE9~>~K!&fO_Hrcvlsn9{V`gBoXc2qi4(9iEY z?)D!q)*0iZBjb48k9~E9IknjEeCO+;YoQWODR~<1+Sqb-+|o(nm8si%s8c!5A$3Z6 zwgYP`Mi4swcPUeKCrR%xe#i*|5GJ}d#r#a+K*JB2GA@k4<2-rTozZzg${dlKWLswm zvt-os0ZZ~p9(-8jE)EKPyYN>!&yBQ|-h?UKh~U!O-@IGoi4pYj;vG$> zDzz;8dg9r}Pup=@dVJska7YbLJ#5^l8Eek$FwvFugp%|U6_6>MZTPOejx;K~6MWXC zIzQEyF!L8Q>Ah|VfYDZd-hc0Yd0S$bPA7TVDhDjEo|w^nI(&`uPjZACmMZ-)aq1LB z`2f_qc4M-2x;^&yV&P_50$_zf1FY?-WNYkwEf^0M=rog-ZHg_($=Nn27;;ad&@A3a z+3Nkn5gT#xwLj0Y+kZS>F;278lc$Y2guUzK3ahN4RB)v37bJuSpoF~O+T5;%;DKbV zZ&UKk5|vX0N3zOwXlSSj7ZxPJ>NLlyl;*^3_D^B&EQ5K6_CAIdR1`ujoi0dbYbYuJ5GUz{ z(8{@%PtBf4Mdr|t!iVp}Gufm8ETGd#ExX zQnsg0N^xkM?)jk6D(cQG& zX*2Rf)`d=`FYhGi)02HbP_z-Yr(TI=N#rL=hl)jyVn&QwP`o|}*im=ZQ6w+7xs=uF zD>7|4hdU{HYE~*WqM`=l0Qj0B+(09Fba~v82C%1_9qr7OPy3(Uj~W?4&8{W~wmk~W zMcp6IC0@&!{|8_-5Z(kvdF=v_SLVk}vk`W;sa2qJ!A~&q?A~Q$UGC8**(@n5^GX9B zI|L9nr!^jSj@pFHwFW@#SG9h_VA?E zk4{!}CX?p+R!E(^*HiWz79vZ(*xP_ufbxzWiFjWB_K5WV`NP6endp6s{BU zOU9!xFO_dmc!oM(#9^J>q0y716_19OfH2Jk{5bqEpD79}x2Z2ja@EO;!W2^XfDS z9Uk+-$cj7OSIKh(5RYT-&yo6QTj`B*DwY2VE(|FQ2(jAV7x4%36I_2nGUe>8FsPH{eVX2qN z`{#H~YMy*BZiYTHf!`x!IMfA&(eg8Xj|DVRR;QLlrcOVkqJDeMtYOxzPUQF@?LB4s z2`4lN#Oy3#0-_**orwN3(rvtU9#nreiyDM`74emnlc`lgsE)LJ7+wGdVQkk(Os@Zh z4&FT2$DaNC6td+cj+4r`pBZCv;Q)PG@nwkyWO1Y20i_&j0OxtPghRX zR-|>yx#GsYiYhxRefhZfzM)Yy^U1qj=AL})h`z^bR^8{!wJINl z_@*AOu+m57hvA1Dk{W5Vqy{PaC5>mVX9aAbFTSr8LivRpIOVSKGaKvs!7eJe9WLbW z==Eui2j}WhzQ9e1yvzU_K?vcVXD-w<^h<9J+p71nstXcFF*+Zw5e4no0wV9yaGy`a zb;r`4zdfgeCP7*Uz^O24sf}6Xihb190iSrSO#sAg+zKi`an*0JM#Q3^ds4455w z&)>$30U9#zO|MThrC#r=ARwm325MF1UU~A4OOBmSw{7K+xDNF8~ECd*}+e z2FyxGMTKO2o-6Nk>mU?^VL4;cc?PGGJaANGB`4tsV8V-p!=c~Ar2skR1c`{sb`TF& zq{I)Nzd3s81%7PP-!Z0nGlgB;L8d>;Zpvk$B%1318I5qDcWx`;dYRNLKpI&9Kq&w+ zk~6SweErvdd!#=^*OJ#K`8?+bS*WPRj*|yWe-k~A8u1vR_NLMmJVqbRSc6}%L%_k; z4buJp<=(Jd$dDf?zm}qfY;MNV?VvMTI-dC~?sPnhYTTKH_K!|2{(_W6?AZtVB#K0&L4G4c^|X$|O-<5*Fc za4>&g%4Xr>>a{>pEi8~)I;7XqN*-zvurI4y4m8fed(CpB*Xo6%58~6$di?~Ff)7ag z#4UO>1@|8c2}`_;Zx?WE1Fjsix-!Jw1h06Ra-wUn>_vJLT)~N1*Ae zA9S^MvD@JIG1>^+njQX!?Y(z%Ha(Y+RpdXY=yfZ^CfH^HxQJ>ZDyb#xR8&iA!5|SY zTC%jh&0A z)~(4R{0GO1in)T58{^9fi~{1B>-|+nVmrCJS&G`c<47YVC1j)Uhi8TYi|2=j& z6~X@kV!?0pfkMqMf{T;8`D(c|*>nJ(&owNpslk1`7V|*9Vuf$WExhC269z-&HwEn$ z`YL=gJmWfQJU3l&H<)9pLa4P^lC`2$7*XYdX7e;~w1*loTSym`mXcT4PTV5ioh9(| z@|xMWIdwnu+5FTefVa}70qPzZLQ9CXAGR3o{5CNJN0aKJ1Y!FudeSCWEsfZ^P3nhp z&g4OB*5isgKAu($aH!va>>VgE4LkA}LextVDqEVepN&?tGp z@l}|_QizbK`3eO;ekt$7_*h$ta@gxT+cG<>yQi&T^+a&qyLt{l$p7gWwTCMHbq3*J zbwXic!Mgr!YsE!nI6y_b z-iE6CQX&FG_FnNitn;8S%sUFxCFSqE$xxEhALZUD=xU4F_V`pfhx0As7ps#(8s5&G4@8r){YSCPk zWN~)=^q-tb11jD?TRKYQSQkm#=o_GM5E5p_$?{Ms^1@`YFt5}w)%;u3g(~qs8U`ls zm5b+PMZao0HpZ4soV7)t(+q7~Cs?cINc0G5Nh7=kEhSFKTiKaUe;%N}1WJAmjueSB znR~Wh$QGi%`mI{m$<`P!p)*o0^RnC+U1f>cjwPt5DS4hC;$IfHPZ$v4Az>7i@`?7wOZ$&;q4jmU#sY!3C;);-IJ+lWc)kFs72d zw+^!8IgC*!n^U^@@BU|>8_pHN<%ndKGctb1R^qFW{&A}?O^ri+lzl?Ug%(U19DFx$ zIF!w|y|*iMZiu0RcyVm$Rff;r4X3kSCyt7eLhcwqOdW^XZpt3q;> z%MrL1E^|p15CDdZ>3hl=5kR4R;?k}ITzWMp{>r6Y z*XYoLc!iPJT<+$JgmxU-UZXz@BNN}(vJoO|i%lJ7R1fB7Fwg15o$u*a+uv@_Qr|Uq z4?8+&Y&e_8n_SIPJbnqY=`&8l8BC^X^Y|(`iTNYbABI2(|tizrbH zFRK8&%986xsHDV8;f1U=CD@v(AQURp>9DK&yp{*19pf95=~ktVPHU6n+FzElurjN2 zvJU3^^Q0X}a4B%=8;idiF=6DIq=e6{%ord&4&~fbrT(*YXY+oLDYC@b`=3H@%L>kC z4^@idJ7B3%s!WS213-7^kt0Kd!%#6z?rZq7trTF9VNX|v%O(Xv;+yFz&`S zYz%(y63O^Iid}B(+xNB{fyti=Q2^=!6r$p#K~5~Nc&o2d7Jzj6oKA-FSu_ezzKC=d4;vlLPAGirg^$$MB%Hy;cJ`k2*T9rqjv>sqd=dE? z)#3DW|99@ym}BvVe+CWy(?YqF$uXt`T6zW9hAG{CcRYD#aq5iNH`pPQa zK*vvoks;)IABGOe4aqli?et7Mu4B4;aO^Nri7F=k8nfsWzUEz&hL;k9YylhwpTBcs zMPO1^a09`WLpn5e-7hL=kR+GSq^JOOd5VzHZ*JKf!}p#cNd?>!C1!*(fSIoyxCX~- z;(pM<3WGpyw}C+A?U$v--rkJddiD1T0ew3Uz2yBxa&lg($(pqCbwG3s5!mTE7T<&=Jx#Pp*WqV5N_o#t|rTvF9x-PYQgzvo8LV#NJC9bE~d~!X6z09^< zSSbDOMMwI)l={nTH2tefjuZ%Vd_!$OE;oo;&xc%(0Vxg|-=mHO&vqzp2PwiL>QbB{ z%OpM&VA)J*EHVLD4ETES5{PcxZJ zUj?Uhjo3$9G{jnrb`2sT_&z7gJT1dU=)ki9xz!gVL&iDv3@g?7{WcLR<4<&?@Grc) zPzZTOU#`vZQn}xE*CiFB_Q~azEd=EV9alW80dVIZOMKaB#Mf9v;KXQbI~yeAW`ys_ zHaaw6zI^Rvnb5UmW!XbVf*#aXFXKvB404u{dMXDqMSKE`wH- z%<(9gtQk{IwsqNHdc0$)qd@IwN@8B$P!&jLLo@nRSN)iA7 z(g#B(<3a&)ITr^Nl`W9#MZ7Rx}u53_m*Nz=pCX;LDWDsk)?haYt|WZn>MwYr z1fZm32Hb`Xx1I9$5(Sr^UH(PN(*6pGe`dkhHI{Sl48-Xy*K^_{)Z&iif>xYWLqnk%1V7mZP|K@-c)=UO?<|+x>`b=le?7oa|pSo~2 zn5*)7e~qHY`V!?g9SWJ`f#CswGIf6{a;VhlYCoJ;RH%!~*OXEsxU=MDI5WXztm?BG zoddgPr4+P*Qo6D36?(p#r!KWIURc8ybk8WRfrE6ifLbB#AON(+XXsFvoB;I7ttpXc zu51uS>!y^{QH84=9bPWev&Lz^Ac#8(ZEb^MUFMKU7eA}YV4s|{(=v>sjoTz(wcdM}SMwOZn!A{R;DJK=QWNcF5zq`ti=ovuO) z2nS9^Nqxb7V0<&e>1M0zZ!!CYisfO7x9sk*JP8(~Y%m`IUF5EHizX;97Z4iNylP~ z1f-TUQ38$ou~vO73w!3ZLEd#Bl9_jl6!f2!$GuEZ5XoRcJ5DS3aBgeIyzF5f8cbQf zY_@=9UVs()YUE}?x#m_I8_P}^zEMWU8)TOSZ<@by2PpFj-&~!G7WNPiIyPNZM`zZ@ ze4+K5tM|BTEar*@S6EaE3;W!relLAkzKna+Vd|LQV&482#>z;53%5>t0O?2vLTx4O#6NXX)`uq>f4U7+ij}^ zvK5oMT4cnFQSW`CO;=E=Pa&624Vyg)!f>!nE7~&Sa-R zQKV9eOgV>l^(Zn$e)8HR&DD%r33!@aDHs-}adv~}Pnn1yxzG0F)DAVgzM=X2C!ow5 z%Pi{DH2G&L2#-q-A%CF2eVz4fU((8;e8kXl+OoAVW$LpWt|~P%u=bsVUwYmasQiQiLeVIi@F8`~mDp?&7tpIf!9i24^$l9Ti(@k_6 zLIO)9xx)nU@)Hg~Y%xy^_Il#Ccf7;v;zD|2{{v@}YstiKpGfzzP^?eig8?2Ph-Lug z7M?@a4m5Hi!)#lGuY5MQ&qiOzToiVM2aqoMvcK`oKY0~#cvdK*gM6rit4i?q+8&4H zMA^!^1<01CodY?MguSwt16csOY8a;Y*}`9`P7=rMkr#SC`m06>#!@i+y+IU%H_QR_ z)^`Nbmvb#wn|y=i{T5z$u?($nY`XV?2T^TR?bthG;|0sp->WOkhG;f-wjwaV@g2xN5@%^wk%b1AX+dpOf zM)u|_+-8OAeOIwns23)YBDyTXzxUBM4!FLYwsYLx$T?e{d|UgTiwm?u5It?lM>v<%t3N_4_)_is( zazy>hjuQN_1^m>I568tA%H%bCJN$TYKEM zNtOkYd73zwjTjr;JycyoM*)|aM7`P&WBzo859UU>y*`0_qi)-89ui?E|K>!nF#u?? zg^bR=l8e|$GkU((KJvNA>!s9b-83yPni*?jLsTo9_soHOt@%->lqQ-^}v+ z%@9!gU|}NXEybi&fZ01EKnwkc9w5A9s@i_ic~w!A*tN+Z659~;*}yNf(4-4n{=&ky zA&w@H>zX&)%PSBk@~56~K*XtIDkk`b5iIjlP+;$>lHNn`?WhbBhSU%prAXE6J=_FKLj`y z5gnbdK02&cpE0rRDH<4J)j(Yd8)x~;?+df}8HDrIw?`?NnHxFl!POBzN$J`i-B0zs zt1(~9`oMS>8BNfEAEa|5qt*E)psEY=9~^G1tjxzYC4*gzi8uBD8FqW6ab`7z9S9-` z5`2P{hO;pC3(!;j&&|p9pMk6QF9&o?txd%5EzJiiIQ{98mOaRM{?R9a=xYrBl;9f% zDhFhv%Id(8(3l$&PaL>R7+o=$05EOv9K60LamSa#RTVKW&}(7neeVyv=@he=zp2=} z3|=`ke|~Xh`e;D`i!ijC?56?2z~M$grg1_C^$!Qcol7p_z%~S&y;fQ=F>$lYa}QlZ z!?m2r8ztR=0o`E?#(4LE<4The)4qGLpU(ga6yOrp9WiOWhe^KcHc+~^D>(H1G=UAe zvA2PN2LWb@Mg-f~`vZQZ)N?x(9L}#rT``bo8wE@R3?aP?AJ3vnP8w3I9vAY7NH0~XV_A6ebAzN2L@Y7-@k>ph zty!H}{KY0lxw*g>#_TTCz$Nf;pF&=!f`0sqkTH%eVf1lN-Dk)7dXl~G)i{(nWNibh ztB_pZPA9x>aV!fKVrdrQ^0W>PES#KOv3D_3fSy2T-)x=P&_3@4T{kx6#n@d;EVrC2 zi4H$6CGYPCKlhvEPt%)ghhMVjlVSS2SzX6{iK&72xH#Jzhl0mUB5qx&QvA{FCdw8g zuQZX}YT9g^u=sn7Viz5Mon1kNEvOxIdwK#E9%smGVwC!x7$)}k2x!DNK*P;q_2x(ZSSu-H7_ftB#BhY>3*-IrsAbT^<$=yVsEjH(Q#z>>nQ>Z z$%QT1S1gBn;dR&DMkbb!kvVTp^8pI&kh`9mw|R-?ErgZ}s5KtX2yG1`aTHMB9+|3& zB!`*75Ww8s^(feVmz8gV%_rL+Z)Z*Ec2lA{-&=oD9x=)veJQ=L`Eg;pxfGn#j9HNbHxh}N!ZAHJ{DALcIjOm(^(>=(|`9&bS{N`Lefx*GH z^n7Yy?icBhJ{*le*}aiso5sPBhf@#3o#vVS--LYA={lV%p;4H!`>4ED0!|>26^Jp5 zol~jOzX$&0Bl4rsBbv8VAtWZqMI~DeRQekE27YU?*#_w75fEL--4V_>`7wP{yw1Tw zCr?#X9ZkagP=t+bc|Ve@lqBN9)Z6rJoEBZrs)o4OzRRL~e1kil)_7T-Ont*#50JG7 zW9A-o-L7WtH9OnT?J;bJWC)Eh;-^c?Z<0wzOtE&h5*FD;DHAkS<|Ck+=g*Xs))T0H zzAjKUCK2|PvVeS7h=3TaTBe+CzXnE@9%sl=u+XCYCo@Mm3R+qH*yA|6^Avebdq;jF zvfs?K{-Alod`I7B8vyK`O#$y*5g=*U>PcW_WqpQALTZ15LDHXw+qk8l<}IL10`>2z z^@xkRCW_`;S&0(%crs*tDsz_{6qtKi#(P(iw^gtcI6}@Y(Vu%6g zU_h*P@L2+&icN+=y}u&XY{~`9v+4g|g_lQcL<`5i2D_s7=QDrb%iqFpm!+nr2@;v} z{8!3i5Lo_43V>iywPge@r9^yMx?cp#-?H8z@_o@wg=|;8 zJ9|bsW_PVz=)K&1bgS8WlihtWBa~oPQQPtAT}*|T$nTYNG()A+<8NnLam8fW+L^w! z`n6}OE3&7kv+u(kG0F^oPk-7e7+Bb-atNJRdR_iPmDv!C#Dv=M-XRIBfSGGhtkGBW zxJWV2BHqf1lTF>36OGB`6?PtcUd|cqV_9A1U98=_baV$;Hq&j4!2FaR*{jK?k1pSd z{CVoLM@J{>f#2BJIBoFuAa?NHygzKeS{$D}Xr0eK^g%MnlSl|yq9@`0B{Yu@2t72{ zd_LOfvB<@ufx6|Ru^s0TU6C4s66sI%?)yX)+Zsyp=m|A(yb7o>oT#-uCA$}e~m~dK!E*LB8>FSP;d(1ny9F$c2FW(FRK%LfHUV!MI z-F|3jZJZqq4eV@ocGe6~)NuYY?3yb2q88KO@qZK2JVbzrC|&iMt}bqvH7q3hVbK*Dxq2GAzHCoF5y@!)Ty2BkUtLwH;K-t%=Ei5)8RL z8ixB2o|sIw(%n&pYtURb2T0Pr^l&=#<)4>k}CDVi-;|Z%0xt-1MFW2q;Te zzke{kSmUw}LBYq*V?)>0QatLRh;*ej=aG=5@< zd_eYO^qWTVnR{(rN9<7Q@}5zXsktku<3eK>@P~nA9@ad@A#y#jmA}RKpB6gafYB+z zTCt0);!Li%r_pB$tka~NJ4>FM8gbKXF+L{41`;b2+1*isvdx!mT?XePYyial)u z((0Fw7(>KCx|${p0~3`)CA8Mr<*U(BJ^_tl+=VMf2+Lk~|Bbt#Kot#i1j!a`&9!Ji zC=R?^+v}RU+ZZ6KJM>&6+^G#ql_c9c^q<)NDi%5@vjiq~h(Xd-ofS{Li#t9(YQJjQ z3&JQSbe0)7gIzy zvkJ4U<(&CHB!oTk_{Mn0D3)`$=?!D zUDKU5Ih7^~HVRK_JXYNN-<;$5b+ar%aL&(lZ#$!bN%yU7P)d6GI*UAsVM74Dv8d-g zVC!6k|2q+LTUigoPoe7vg~9(p73_GM=5WZyf^8%*W-o8Hq$IUEBt1;mM)=DyLhTuL zqYk##5@XL=@Z)6uU*U0(RzVeamERWb7X62A3W+@kY+8rZG==p8nZjm_$u|N!=~Fs) zKBvlwlt*Vemfm~OoK&nViPx47)Z&2}4%Xf|e@t zBlAe?E)kEfOBQEVYszIPuz&aKvF&s#obj`W%L~z~gO^efcA7`6hw&piE%`LUq8=7e zC*xx-$CdPMGWoRZCN$%hQrrAvC-Lf3$%ahkTtXlXC|?ZSU`GWd)T;wt!gg3~n2i0m zs(zxPBg~f2fPW41lnELB(j>jhWiLoTc&}Zu$xALUP&%#69S_ixJAJHyCE_?A)WN3K z-UUX|=-i+Yxls~@l8_ymdWr0q2~QchbQLkwqd^(Sb6{E`4_>aW9P6ESWx062(PXS4 zIG$p?|O%yYZ+HnVgkCcwi@%)V>qXGi$0=%O1ef8BSn}{p1qNf z-_k-a4A;9&^kb38VGMmYnFmdh{g}0&V0jamqBvhsxKGazcD1muNjz%P4yH4R7;+ht zXOF8kW`LpXzOu3kX4vV!RjdDtCSK1ted_-61y9<~j6L8N|KYLeOxc7(vVO9LIq8%#9gDs%wPuJpP-7 zmlTVL`%`H$s`JA7 z3SFBk#moAsF9PD73eP=8%X%J!4{GL_l8uL04av5oHP|9QJp5MkHxtibMc0kKDk=Z4 zlc1SHoSK5$R49G~G}-nXW|uI2)DGAJu~`2_DIz;1JvA5@E@}q@jO)fRjm@|dJY(pS z&cmmCtfmZEt{Q0km~fw(I#e;#b#&iK$7_X=20 zQ#kZ+fi)R3VWMM*4k^~m2PddgtO~^8GM+Qp9mSjABhcxlMcKeu#Z6Au%fC@w0W{0@?;GU7TpRqVm%OUaJPT}+jhwFn7%4Gl2qNF z6;s87E`mA`!U>nmi^M*+a*7`cbngtu@TFw}vDPUbF{J>ibQNZGqc}w=>=YV+Pv_Lx zxdtR?r%KcH4vHbMsX`JQF`wzqesg;} z_aeziqTj?q=e^~@V1{|`d0nd1+o5x7!9D%t7+9<7{?WjD+liE)%WqLMd1C-_mx^*9 z?ZfEI7LOhDeb8hKxLS1jbI8+yvv{UvcO$9YD!k;kIe-rJk|u=R2q-pMyL<0>VSM-Vpw#mYH8eA z{eyv@Iovui%&SJOp%I7)Ed|T&Z=w>D=BS~{v~u#hE%~?_%>d~3;#4>T?_aRn+I^cI zjJQ3dbCbO@!29+G%(j@t^U?677+^7*q@*My1b1k({YId+oP8`PPS^6+tLhQGU$tX9 zWq)hNi6DX1y5-ft15&!nD?Hx|Cyrn?>4OfDS{OGXWw=W)S=qtX9bxEpUMd!XJii4@Nsu3Qwn-1Bb=Wuq+zlO{cM$$(E-Ub)@aU<6%ghdwi!+| zTNwc4GLI{J!734SPln*bYTiG9DgDfVHJ1Hu9EP{UHmSG+x(lEr*v0(WCa(9ryNM2m zcS^@-dSHsP`KJQeiGK%@&CG161Kc;+FNYSJ@tv<&>qxnjBNCoW)JRsk74SwJRoXzJ((tg&Fwi8}Qm zUUr%~I(S)zi`j<)B4=~*59Op z3Y$}a3y2RXe^C5xR~K8xLGd8zxm(0kKPATdij?%115*_XC%r8Dl|+H}_hwqt4e(6f z-y@V&^7CWrDWH^!ni?7Emxd4 zL#v(01Bx8_-ISihs;Z0LSyiuNhZooGwCqdWpMaH}p5-PJe*#nUFoVdXJL+qY%)#%Q z4HKrMV3<c@gb5rPyx_$3M8r%(C-NW(8XYmZ4iA*hTYuA>|%R+iouV<{QPUHCvFRvISJC@iOiU&XhZBJU5xRT#)u zb#qu}xgee6ouFcplAzPgS_^8~Xjmd{qFmp<&!+Raccp3xX`0|E@nHO9X*8Mp}9BfT85HVz8a zL^dSa*zU^@Ap4Tak{kl&pUyTyJ|~Eb^dLMz&Aa!_HIo5iZtQet%95OtPKOWx#wv3s zpVbdz-Vgq;T>i`(_4Op}*YflEy@wkUc_75<9L-pg5Hn_uvo$IsPT0o-eUPhT@MgBv zN8mC6u*rbmfVM4w70LK1;V?dOzyBoLw%A3bcrD)2Ap;}lJQkZx0^U4m z^Ir?YxxP#cGE4jd@XUJQJF|6hih(Vgi%<0#h}d2d*w@&jFG|G$@8sy1sn3770$Sy6 zu&jim@j~Moqb7*Yple=zn&e3hKxSF_Y{wMT@Kc|2s{D!Ubw=a9N`c!#AQaLo2(f80 zO^^l_66K9nr`rP5xO*qZUoc4l0S;k%t!k<%P^x#An}m0DDeNia>WPct|61_S8JDV@ z$60uCUjNHE4GD#=1N(8VA)>b!h^U5>s(j=tk8v~Bg;Had-)?#xH!&A}hVz-&7W-j{ zy2!AWNsqMFo*?|7wtg+K>%59Zm=7@B6qJglCxiysurninI<;EZY-u6ur^W@y?Mr7F zKd=WVX$4WBZP*W`XrW1sx+&fwNh0{V#+b5yA<#4;N=23h zLnPm9r}5_73^gg)8qQX=!&g081HyHVh7rE@V6}t-V9lxLy*}Ds??&hVbtFMUbmYpw zg!|AgiP@v7^KU2YL&+N-EbXcaw&S(gikl)D3VDYztGQKsftDs2X0GLoJ;`Q*O`d}7 zeh0_R3t$Ci@J<$h(*H5?0@i2y`&nTF7*_*w`%d{uN6FRw+KYz|Z-PmIu8>Y7s3xU{ z*}Wq%4;byOxrHe}%t6jq+xIZ#pKf(Cu7!aj`(bhlE_TM+shEu5B&Kks%FbZ+*Q)Qn zP}GhqG71HTjl|%s`9eEO+0=G>w(b@LuDucd$WUK!Z1h)V_x8U4&fgPEz7@HU^bNr0PHV=#Q1t!mkiu=| zb=hz-=H9yK1rQ_Py|3mE|hV_3aAg6|Ou zn|TllDYLMs(t<8t((yvu_F$|ScAU$!Qoi@lC!M;1X41-eBclq>H&`dTGk1vQ-H7aU znfk)ie1#DlPSKvhDwcb6`Yza5Xl!-jJR@7epTcqf6h1 zo}K)h!;8b9PxiMNc)BSQ-k7J4j zc`gdkZh8P3G!0$H6r@+H@hhqjR0U1jI}Q!GPCft!9_F*n`E80(X|dtQ7h3Ug3f zWP?P+=JPO8Fs&LP6ncS^9OhYOuUZ&4wgSbf1hb)_k?Tb)mnnmq(=Un#M=O>5cM_}I zIlpOj^;JZUDb^~&zyn`+lS-okkHO2WkauJ5y zFxEv17GIJy6_U4-FjyQAkvrpkZ@aK4X3J=~vW(KoB2 zxVk;FQZN|+jva{U$mF@M`_5R_w$x5ne0V*v?>l}Qgx$cd@)Bmz$0cIZRHTtQ?AtFZ z%h?#{p-l9QXy#WPHu+;N100_=~eNAV(V}87B_xGB=bNlyi&RT9S+<+hLH*L!I zY5Zga;zmnK5eG3}B=VDRmb+aU7n!&J>bAe0%Pyb3g@92E0tHe! zP#~>0+WrH5+1n9`E(#_!tUOKF40BRb8;{!WZ-@aSt48j0_QY}-Ok3Yt8Imc+UFcoI zOqeJ?Gxt|PGT*Q5c@eKwsF?cFk>}T!$vl}>g$a7hgOqvxG|&N?u+8-j!^|`h??gwL z{J`>R=b(HCYo?>hrkMR>c6AuBk0q@2hw$kr|JdlUN7=vz4HFaU@mGFribQJVjSuVY zfJuC$1B3=DDTPE7LQ<-UhCTpL*R@3mpJ`f%oe>+g-6I5l8I4RnJpxkv{iO##W-D)F z{C4dU0k9GgEaK=Kci13a>`*cgBl*z=?$JHq!e|S0>s`9on|{KCj*~v$=H`w$sD87OUq7a(cIImO4_3PpWD@je8gS50 zh!YLb4KKC@_ACu&fi*uLCdG($of-#czbf^>Xy_K}idymOyepTf`6NNGRsUkwlDJB0 zWQH2?ysacBc%JDLUS{x}K1c*^fCH%P@VcN`IeB>8dRBaF{sd_2?BaS$-~u&NsD#yR zfx0;YD$L-!hS&AW%ewx%$=n@Nd&2i;o<0<1d0-c}g9m#7F;aWr;TmQvV(^=I=h66) zW!)F~0qcc_%trHuOxr|4BM(9S%PR$7vVVCBj8XizYJP3w2Td572~BgN88CE7!?+%s`4_vBKD|6XMtm&Ja`>8oRa<4lsejsWHc0$z(mdu5;ZU zjCoump#|OU9FxcB*kOIt+4JrM)%J^%AMYe*>#9?ORpuJ+pzq$D*W=Mv!ALNgU7bs~HHrlff$ZvY4yrFU3N zOS|_Ct8;`A0yGR;6Xhu4Vu~5dNlS}Qcy%;G(aJlfH?=i1>qD=7TF9dUZuJhHjJ6w1 z=i*s`zi$9_b?^>e5L7>ReQ^?Pab6A}&9VZZ3TU`EN zAN9ass@z)jS+i&_fUN-nT|z=#l5^8_;N-~|Nmrz-+aJqj`(NTp;xl~e%AYH-k;#h7 zC;TlPMHJUyK*(+7fg9m!3%U(wJ**IZbb$x+UxPuZbb_3pdvlIN@Pjk#QZ0+I^z%H%v5EaqJ1ns z*rv=#j=ieuv(QUDes74BWs@3FvM@xAO@7A%D=JPKc>|0eu%c2Hy%VA94&^W2jc2(6 z!V-gZ5rSX+&D`j|cVd0b{RrUW^@zESx(Kmr-4R@(r6Fl!2?@fEyLT(Bb|Su;fU;-m zGfG1xzK@Ry_hoMEh-W(4yUq@O7t|ryG5^S-C5d@_mQz2o0i!^6Ygi`76q&=GuQ^Dd zB^U8O6urd6jqfRgprur*vK+^mVk;qr`KXD;CFZyRW$&)%f_WZ2_fhgR+KHZm`S)AoWaT!? z`$^)(+fqJds&O}S@ah9ZQSEyqgB{G)7y&I{EIsKVt4G)LRQS`W2;Ili>lCA};y1Zu z#36wvH-U?QdS0bJzzxFfjdx4fKWOtN2{@Q1o0FZONe*O!543S_Ocn_cQ4uhnDNJ`K zM%oUsURhqihE;JEVa$@rXSSWi68sk2h{wEW)P0oASIyGkCr28^Eto zFnFT7hA7e!X9dOaa<2;r@{Ud%3FMa|L_VKWz)e8~KaZ0)5gnvQ?aV+fcMk(DKEO9O zbD<76-v*sFjWFJ-=kF?$cW@Ei`W7QJl(p)V4c!3io5W8~Scx-^KQ-B*EKPqMTA{1IRg?_*;1);Sa^FZBK2D%Bsj0Dl zoly2lecJw3Hw>^=oEo$a)@lt>G#4Vk8c|za!J|W)*A+nEAUpRi4G8hG;pLI&X^%w9ddXlMbanTAVU9j=L;(+Ta{14Kq*%o zJJKp@%TRV`US0<&B45@jq~Obq9*+P4n5&`P6Wp>v?&HL#B4NyJ&LJ1#Ag%#hJ05({ zRTY?QxnV&iBrMF6GTj$a?jk!YpC9y2<&t^EB67xW9ahq>u^i2SZHy97@gXs7Y&$Hph@ZiBA}X3vOH(>Ikf^>qh^C z;vU=E-K*?Q5(F_9mW0Dk!Fbmxlg7>1jLabEb9Aut7Dg;?f^atB>HJeo|4g<-AAKpe zv;2Mp2rt2d-Az@Y!Dzea{9xIy^;Yq|r-^~RR)^Nd;J7mmLH7wja@6j6^PXKLbLXYe zuV^($MqGEjL*mz`NKxcF*zj2K_AQjywA4`Mf1(D3nXM1?6)N0%>278K-qQKKI#R^f z6D$>x;a_;h@oYUl4O*)TQ<2kC+XLJzHG#P9U|fw7CsiuR0bP)m6=yn2CrpgFq_8ie z*g)*=*kx2&b=>9LVv(Jmnr4%e{b4~jesRsKpy9ra*2&42r#)HAj!4)8q_>yto)pW;shA zC&i}FQp!0%VVfktMf4;mGo42~ROn$vm(q$qB?Qg!#QU0?rs=d}qFO8?^N!Iar z%I5a@`N`dP%8r;@foBiE#0Ek4`;OlftDa~%>b?Kj*kTHkwl;!&u)U}Pyj!51O5L~5 z0*B4Uh$4^<2S%5@2zqeXOk_x+4ZsNHK4-?6pB7%tasrQr;yT_ytE6%!DB46_i=WEhFoUTSM><$XFZxDhHpoH zugknu)|`8(MU$GKHh+JSt)}&;Ut+9wh$|XhwsBX}pX|@8R)tr2z7QnyH_f$IK>GDv zYc9eC>IA(Qdg%34hayj_W{@fY$i4pF#I-UzTX442P8#clLWmlqpN%XlE6d2zn7h+s z*cP&*aH>5qHJ%pzhdNn(JdJc`6e&CcJQvJx6R=Fz;u1wCaVAp!b$n3_pSNwWOy06m zl$y}_7QOs3W#C8rxggd>Q29Hae7Wmd4jGNyZa?L`;4RuL?Fszi2r_^HUSC)Z8|kp$h(h)I>( zaCIJ`D?6g=P-EBcR<7R8CUtfh?PDZ9uhb2wlKJvKTDXA9+3E3hIX^=;^{ZJ-BF0lf&8qpG4Y7h?j*R;P1| zW8+Hs$#t2ZeaHLvCHdtRupPxZLwbZue*H08p7#Z=DhU(1-iAsjQ1gukbjo1zjHwZE zb~2vV4`%B?>m$ylRu@x=yS7n@Bwe$QpIUJdb@@^cy~xk0l;&p$N(obtyCGI6%> z<1-I1X0`|u-L~Agii;@{3i^6f`FGB-Q&f|P!TF!3?cY5wgPx?}8}G2kXZWc9>#^3{ z-+&LdCuI8F3Aoy|a7V{YhV>ZSSeO&TANcf3cwy*;93e{clH> z#+sE}QVN}Q3tb?YvaW^)-7jgtnJ(C^z>;)upEOCoE@S1R4}l@bedq`sR=r>sm;9rB zcmDfn{I2H@+asNv--xQXQMLS(U2_}bIV}{rHW1xA&Auo~$jUg*@?|ttLuetrW!gRMW2_0B)(zHi z0YvKSb^lz0R3~X_h0??ULqT&81C%dWA3wWhkO^g9_~b}<^s)JFR4<7|;(&`ZeopkM zPwzmMMzCk182UhRBg7b9<>0s|q;tOnimjXJ=v(4ccKz8q+0B4=+j_KGy+1)^a4`5m5j zYf%JsV==dP{^rNp)NI^p9SMOh#Ict@?zP_nAOWq-hZE_<;A#AeE_Xt>n{XuZ_;|M< z+DURTVjFE(U8~Ow7~p5hw#g?EuCr_S`k5R4;~^@u$@3z@XAavT4}H=lU9qA zgW-u;h-ZxzLsqhCOf1@NNjZFcT#bt0pUkiHb?fiTN2f;xM7Ipn)+n24g`EpvMTYXN|T%)qt{TobkUP6%cur!Y+G`93N zrx5Q&@4CH2WLAVn;`JS{eR85PaVxp?2rdWc=a)wu zm$yqf7C+O9`!~g;u$%wB-43wnMn8d^I_?>wThR8&HX14*=uwDJ+bN1ZNfQhpBz6Ml zLd&1~&RPLI+t_PQ8T2+;ZSHN{pDncWVFM+%GPRZC5M^@d=hA1{cx3FDv^@v>aI1CbXV+5$?P` zK7aFuyu{l+_dqKrF{#c$fPUG{>@V~!o%E<{BTW0HxYbVLNmGnFGbvPN485cpw zZnP+B2~4^1^A>d>9OqBj%i`Yi+mWvy8HGC%4mHd2EDEv2TipPOaBge~62*ZzK=_q^ zARl!Y8L50-rTwfIao{ zz|Odwh<5(5Vcih=7;BtNgFcXKNWz4XtshQP?9F(*^;({LoK9hQk$ipza?w}#O=nXXWN74EFVlG$5u!fKYY??d6gZBo6>Y`q+u{-#4qD=J5uqUz7wWrI zb(cM(eCH@>c^0|f64rWbSlB)XW0pU_$Gkp{2)oZT0qR@ltEPIqjHx7n;GEHKAdDhk^=l2o(6d3NqHG1Q zH-MOy0ts_q2J7E@&@R%;#NYK5tf_$jNIn*da7Ut9>*65~wvlIGEC1E zer!gdr+6KLGg3p19Yd~FEbvSc;dX;Igcv2mca3^n+`5AuFig}jE;)LY=_x6Ntoz~N z*0rI(`5M?-K-6$C>x8zJpTEXn0mw()xB)-&q9oWE-TSnTc!5_^bo_wl{7v;>!Yn(B zK{9)pPCQ~0b+}ugoOmQ(8RjeeJ>48Kg8Yz;Dv4Q7OSAj9UQ?(*vutMK^QDr|tY&8p z0(T27l#9u?8F}HB>1)6u(^@sv7VNmEVQ?L8Wpsme{`_9AQ~yfT8h~Z$`_3S8Cc*3V z&bjR4hfH<}wv?5G`Jzz^BCKaoa0kSSU%G;Cqwq~x9tI9go2r7tUUNgcqaZWxth#rK zOaj$<(gQ-sn8RIbg<-m2RBWZ8b?)Z5E82GW(dv2yb_hedL-gpP@Cbn4{-yZ_$1#=0_V-uP2?%31Y9h)5D3o%0ox=;NHzKx?i@%m#v3m+6lU{V zY|&MvB_V3)vOSgn1PcZ8G0=eXN?7F=Wo zj}5KQmU9|zlXdx~l~=#cPU@~M{z-IGhJ)Ke@{K5$UA!VzN^)X$HsK!%)?R4f=o}y6 z#k2ccewnOCu8Bf!*3A&)T+~yzh8L~TfsWGOJH}ex>Od})|Ji=fo@ndH#4a<&)>Y<8 z%AKl2f)*H(xtQi*MVIvJd&A`lQ6D-K)vv*$-IAlXalcJJFI&JHXlRcAib)UA2B4C3R#D~@y2oZueZNtr!hQVE zJXK2`26$5WYSzT$j9W?qCLc%7jq&28)f3|Gwmhty0qGC+NPNAazC&B1s9OB0!!DaX z>0kd~_-+sD^pG?+U2`v&Jg>VNN-}-lk4cLQ1kEWW)byh-nlZe-*r3PZGR8aj1F%%w z#eh4nwi^coRdy-96pMAG6-ycroi1eFJa$cVaGUKs%dnnwNpbR;r2`TLyuY3!^sUIB z(^Kd||fp z8BAvI&li6DDH>_L+^61#g+U89=Wo5M5QTnGR%St?P9UD^q#7QIbYm~B)KNGe;s((wx1u272_p1m^ zW)$Fwj@&>|3mIwYMNB-XgGEh7+kn=J_E;&5RQHk(rrzuLLI(%T3Enev9<{7|T&z5K zAVAd8wk>N;@48{##OSa>7e~#}Upg*Rki7T9K>V*sf9gIR5&FHE7{*n|!PUk%7AF;TE+?-Ue=@{*ZYwK-mt&bN zxCu(-u6LK;kC5$CibK6*Ybnbb7fL)9cZO#z{r^eBE%^xipy-$t|KYy7`EG|5Bg#F$ z=wAC;!apGAe0FjxV`q-NXV{E4oL=N(FZLL4m^Uc;V>HS%sQw;ND=2T>@^nH(y?rs@ z#PTg(<5@WIB3mT@sYo@{4wXl7_^C0gfZ2Ts;b-ov{uf)Xk9=XTpZU*F3xY{}MF&|f z@pa9gvLSuex^^}<&TR(!9>=MSEPH?W2o$HG#_(g^PI0SJR@R-RMieVKl%t!q;6Z+G zf782#JWY{*{z24Tq}5Vin=c;W?@jVLVy@urHQ|XqPinRUX4xr?L{ZpM6G{v&L$V2v zjyCy>NG=loZsZEwrI5fvq<7*+{^%D3`Dos>%*wzArNyr%jAQifbegr{vD{|B0WDC? zAZ+34i5qrbpI?vzZ|Tn8x0JYcfm04ZC07#dp$2oZyeev~{uzZh5?jdZAD3B=EsU$u zSP|sF-e7e7re1e35DFB^z|^{fTq3f$dMh;8eLvjh^&K2k`k-yvF+SkJ%I+EPvTYz= zBI&P|55<%Jr>N_2g!=#gBqfraB7~0-Au_TCTh7Z-^j$6)E>!mDnSj8Zbs{-}gAr(R0f*rX*8!ni})jMz{|Iu%W^&KmQId@Z@nI zH|f%QoUG7h^;C93q&sqco^y4)@vcak@@T*}iNn+^`V6g3TICg+w5P!=3E*+d6j}Dw ztG%{|VLdx@Ta24qvM0oS>D2>#L86%&oW^(2$9nE-Jw*;EW(CE*364-afCG z=4fDWyUpQ9Ve}s?9d+M@!!dMqL3%VCk|F@?DOjng!;iil;7f}k+^>%j*vIf_>|>aT z2z%~WZHc-@Mp%`R^CoJ8*W=|K{avshSSSO+plZXpic&C;iwo1 zbcH?C>OjE+XyXo>= zoeOJ7t_%v@)on4RBcAs?bo&ca&Y`z~)A&XW(I( zF@~je7vtH%Fo{3?*PQ+OH*cuVPY2BdjFIMxcU$jG(a5lH4e~@uK_*W5#ltbi>@z!$ zK#&(cQRfpher5Ei-`{-T{8j-8xKjFQ4^CYv;&TQR5~{bKqk8~ z?)`;~8xxvIP+T~-l{!)XoPpkiv(6t3g6JJiHhliRWjuqiBhokuCLgJ!fq_2$!%_ zX4+SX6&ZGa6_g|233TB|L}7jva_Pb>2FN3wq*W#hTl?rFI^yF2cHOwRZnLB_8--h* zxPrQ4%oY7S{@m^u3e#ZC_uKRY^e*nIWOFAkkaQXmIEkBvQ=B`38Feuz9>)gnoodXy zpA5fmVkxw!&mh|y1(nj`YvksF{ex^_I4qLX&&`8a1?6;PqnF|8+PsAsog(`ixOhv@{Zk22U+5Wxjm zZ2|C#rLEDSN)ppuOzQYw3t9A8-@A4A4PRw0mW$QcB!%Knh5};z=o~b;YkMjk`bknT5Fh_`uwa5>y0$LIF zH7VBji7ITmANwbrunJF0NO7;pM9x=H$N`BfFoE-Ov`eB+#>C!5&3+P82O zG}?3WwXf!AV9iiqr6(EVIGeRxoEwkRU2@Njywul;L7G&Q1l*rFTzp}m;M-AUv&wRA zO(Vq1ZsxD_tN`0<<|ZM7kfn#s?XO%|@6~dRe9^2m-Bkt4%?8EPLi!uJMBupd;dYz% zc>CbSIo<}jCwt2#NyDS48P5nre(~HeBj;fCGB;lP4q#=#iyGnu(s3efI2DpJQYkLgZ-1m=R28J0J7=>tp*SNtus>o|?9y^l{==!A#_GM9i0(<<${A~4q2KMhV0h*2Obp-X z>H%u#$iwuHhspi(3_s~DvsI%%yE0-&kXYl6lH=OmYxX+BRW^ZHLK~OOtM-IjY}8?u z{F%5%pf^fB#GV6@Z_xro4s^xG^#E9E6)u$)RlxDPZtkgV_ozkro#K1W#Qi3B>4Lcm z_xWpKuFijdDlZvdFSyFQz16s>FMwWNV&O>d(@%vWI~{+Rd#M2vRW|NV&3CkI0ahjS zel05iBNd~DNfZz|K;`ld9mk!pH)l3Xxtu<|(9%`f3VuB5KK0!70P#28lH5vi?VH9= zVVzP}RT2Osw1Ma{2F(>n-x#(Q;C>bf?S7j{jTz?7b%BKJbnWq5nlglWghIK#zh(ultHu=G@vZM@iqL`h^ICRJUg^~iU;@C(MHA_EV&-~8jWl;Zd) zs`EJkiq_py*rdGuP@q5`$*mIHlHU~WD~mUX_J}w&O9d@%ont#MCBEa#pne2pUtH*T zv>)lLK_AiMfZ_)zXj_v*U0b1Mp^$$*jiPyvessOAD(-x9hhuOmJbhdrS2#ENw}uY7 zaS85ZLFTW6PkKWMOEf%rwRse?UiNOMj60KvW1(X(p?x&T@uxFd7$>*JHR+Y(i9 zofTrGn)uRl#ihJ?m{gC6-_#_tqlve2vQt0l^o+z9gA6g&XULvOK-zoh5t=$*COp>gb|b7f7D&wyT@_b_aDGCk>j z_53-Gk3|DoC+&J-dQ8+2{fRLy($lLzL=~=)+s)k3j_2^u@pG544skwfx~t&{YiP;k zp6UOTL7~?FxV5m+=i2#CSSX>($OQ8zt&Be4?u)AJL!&C5k|r1R5){4Q+oT5{R&|Se zIx2XF;D#&h)+kyjL=;PcW2_iguJ{DpGkYEw=FH#!441O7;%E4U4O?UT8rWfrxW-~^ zww0ipGeQ{ooklv$@z0E4;AYp)r*WGLT;>5u{_m(Q-iI6O3)-YFBz|zp6x~7MQfn{` zM$rkM^&H4ym9o-UUCi%K+dWH8wRi7=>`*d+Qsj}ynhMflANmH;R&eb}4)KBElRxRAkCz5xEYnjzi&mpmflhqSzX&(CR;M8>2s3tK& zXZ0~L2oC33kl89md|CA51R6y!KrnY{ ziX#!<(*98G2DzJgn%1`2R~17z$-UARqu&;eq_EHE$uGgp@N`CP_yifyRNK%$KhlpN1zWTn2H?j^LXonfEsv%sT?KY(3zp=X$rE{l_ZxzZHa3eak%lVel-vQP>aRs zq9L)mNkFuZCz;gSFSRZM&^3CBJv}F-ev+l-@@cu+a@WN_V4ECm^IDDK;qF_FmL!=I zq2@h&MM)`=?#(FTj7q9hff>!j#(sh8P2HQw|jT7AM^vu{#B@{JBcL_>kTtO60#grav3JkwK7L zSfxbC`~*U!wHtW@3$|(MG7K&Pnur!OBnclBs*~5elS!;EzK>btKt8r>k#$YM_r(Mt zjY6bw*h=arK!g!lnp7oUmmv2Q!=*b)VWr6;toWo+x*z5^$F7EhlsXus`JI9tP zr(O{~$Ex`XW~)2^j!ac`#ym!ujeCd6yoanz4D{1f;<4NE0TL|FZfS}#kH6i-H7^1@ zDB1G|q^~pWBQ7?DBTx4;|!arleBY#P6fnQC&_iiVK zg{hX*e=kXZ(4@8<5%gA%tA9x5v(8fYn=!8ApyY@R`Wdu1&Y>jfdt|2(cGkz-NB1cL z#@1=#LPv~!)Sn1dsDpmrJNaIfVcdNWo04p|xMW|)A~Me-W=`1PylaU5FVSdga~3T9 z>~8WQH#?oa6>#)CD`Re6@#dUrAy36PILvmn)}IVM!Kpjt?+67psY;`M(1&z-PQ@Tq ztjh$9es5UYiZUZMaxNsHqi}3G}BL3BIn}mIZFe5J!Q_sjbX$Oho zYRqif^Syh;J&xQUgq{zq?BW_$JJwKCHvu)6%kRDP9G|a5|HfJd_v^0bTUcZ(4qyL& zeaU5by>M0F!Rl#iK{P7T5rYaZ+BsLXl#DBQmW0|3(^fKs6ue<1HDjD|8V5LFT%~&- zK=`KOLjZil8(iO>S?4Hap=~RiFgZbc{+f;F8NA&RpV}?9mC9uwc15q3gMT)@tzjIkfFP0#{03ii``7)T&P%TuHunn&kR2>8V6HTPY9tJ;bcGR{x zcc6JqZKz+?v2$C9W&u4;w_yil~Z@ELdQ@=l39VN%KONNJ6A(aV}i$b-1ZC83QIEVioM|>i51<} zYRap3+qwcRrCC|T$;Jzq2R-k49ZX|0Do&(QkiBI0X5z&+$~=%5$W+ejecSsdSmDCH zrh!h|c1LVUhyK)uWmlNXgQR5TANLJE8_l<_@}B&N-p2E70Q>=e_JZ(Ve$?TaG`o}K zPrRgQitkT<&Hhb)KS)f-Pse&jV&ja5Syo$J1D{hCtTRc;1z-l>8oAk&EmmcT*kbx* zN><#B(blCgE}Pm{1PS;e3l`5Swmcit^!`Eg9NK>;0>9!n+n!1%x?|?7uZ*#2v|(HdY8bVKehCjE?8sGqVa>9UpjE>gcMa*^59SjPB== z$_Qrs@Y@x?0X`G}#b?0hLZ{t@cNS?fuLx}k`4=G{;wo*F%b{qy&(bIo=+X?EUR@{T6tmq{F)x1_Agc3G=S_r`Vqy2{dk3f93A@Tlu`C=K2QI#S(q7q z7iKD?7G9GxP-d5%APZ;$^mCp-iT6R2t7ZJmzjE+CN&P2WU1z<_6ASb)&8o68_g2rBr$z|J0UL(t=fIm~f_feGurzx+s{uM#ZhWB^uR5nXWnrxo^R zxfw5CI^FWf&4r}s)`KixCW{b_?Jx%FIpQK$DXhWjC(~ssD)W!hlP@gydN5_bvu@2C z^`^#vrppdKayi)zF_a%XSC9GlbKe1f872MeEv?}Sgij$+>d)P#mEI`B)a2D?IP-04KjFLa zC$k5-*?QPg$?UC-Y-S*vn4RH)6y61l4wRg5JXNM*|Cos8trJ`H1M&zI+K?y<_YWGn zHCO;#lv%RN`ROMOtM-=~rL_W@i4c1V53$(SU7`$sx>MKX-p!*tbvGBx8?GUxA$Kh- z!;Np{SU?Z0V!!8*W&_psCAt7l;$wn#(!p%`|=22#H78X^yB z(sZ^r$Qj4SS5A?Re|?!`1H9bsez=`vMpWG+yPO}b-!%k)^auHFX}8gf1WAheWtasl z&wi2NB~GjNKB{?JJmMYgZJ7JO$oj5GLC}0roinpUK&n6BiY1K_zi0Jx5@q;q+cmnG zqS2k!!uE2O#A=v?d`~`C(Tv3J3mj#@chdYilbCrUzvR8*0mKEsfi>+1sA>(JF{I}# zXY|ms)#-9sQq=S_#{pB)-3<^CzBHInJiunN#*eV4wHhu`J}f-T<#(b zfYpVJIuhHQOpLk�By+ttAtV^JH%4Kay;sj5BG(@pxoX3(@Ii7eJr>s3euys8RYa zoXuXAW9HyxVW~Ydme6Iu4MsRh`#6iI{(JFwBtqgrQY~nywvvB~bstt>8_*{w42qm@ z+^b1MRGU{U#gE^t#LYs^grmvl({HJ7>?MZJ4{G1{8lv-0_)jLZ9pQKa(U_)9i3fh` zQq63Tb23X3{A9)}d%C`Ec*?6bdG|@QgSlP-Ff_}$P<&;c%!l4^E8vuGtuW}F z(;~LdCfd8Bt;o=xQzj;cbN!W+4TGDTwwIWEP z+W$4rqhceewYIVt<>?WN@oFRtB0+6WH7gXIb<#4 z??!)t-iIpIKKL8)n|Ie_j#Bh3=T<~q_SEPYfCx7cvwkzLc%>A9XzB0UeksxOiFxRK z=)sZRUNyF;x7y?QKE3W{trt-Py6S%^XShbX>bn14L-}S#A^s*@Ior+FK>3Kpm)C2H z)_XFvhvt{Wqmv@lY|H#rP?S8vn*qQ)QA#qkN|u^W@8pQe$m*>fbx{BKuY{5UJQ8nS zgYnVt@l!c*x&Qlxp3U)^NAI@FLw_!i(Gw;b6>00f7;z90t~6On9-*{v{e~68Bkj0S zZC(#WhML>UrZ1zyQ&YDSeEO;%!Y10l45ZOU0CU5_V=qiWNhi?M&2T2?06)|k99^ln zas!Wi0R95UlnL{=L9>vi(>@phO;t_srUnyZFl>+bi7DbofYC#QqRqbD^u}F^CO;nk z-^b@?MpSqi>%C`L`stUo@O?xnstNj^hdSPgjJW@1}36wFnU8Y3cH2 zL4WfORxH7RUsH{(WS2X5qr-t_Se1VdcJ3c9S9Q%$uzEPyGuZKYs5^0I%OLMx-&qH?zQGzQ+4b&+~j zB3KOU6%+BFK)V?#CjH2c2RrG#F@4|=%5naQoAGZN*7n(4ZIV5~I2>|LpIS$((<)Qu zQ_m@S^rs4GY}VXJb2kJTCM<;)O7vuqYXgKj6^A+@-LiI^pHP}xb}z>M^VM;;-B#h# zszIg?yvQh`$J_p|%$!~6_C(BmBl$`rvN;Esyf8V`xO_*qXTq|1v8=-0-@A?V$~)ik zHR38qdF}UBKdi}6eOr8ls=|Bd|Dx+?L_7YHdK$yuynMby((E0Y=0A$sT09BXBPlAb zmUy)KqoQlX>jmbg9d|g=*G8h?_{Mo7a6BoRhX3S4=Z&;bRcDcwcOd}tI6Vj}l+TaY zcYKA3;s1~ScguJOucw$+&8D{u7A|yC;w5*~QGiXh_#*lm<<~{lqMuz@Wi4eu`u21} zNvU%XXQ?$^dOAy|qAXb&sFp(H(9F_iOnH$0e?E&iYTruRw9AscRy8=eka+h)N$SW) zGx!mEvk`D#v5_#v@mDhYu-CaTtY z&(n^0vtNK1lj9H!)&6ue(TNKNv@>xur1z>gavc1OYPPh7 zkBfgTT@?I(J`&NA%LsoT1}YfMM~bMLJRf^_H1tp z=)Vt@7QP@m27d^L)OS)?$Zy#c6ukTE_!JS)?WKgLrQYHFkf|0RG$!` zk$%@dTnZ&#>B0aY^Bcmi8)F{fBypT~Kg+(YGE6kf-8oJ&%xPri-!*sId`)AC=zm5_ zZHsyH#+^t1{h5rB%zj$(d5J5sey>0ZiH>~(5dGJ9GyLkrp8a;&X!ErA3-DRI<^^G< zVx{^3SLKuDn~c({ZfcC>bC3ciq)0}&&qe~FU{s4f0z4;h`?@~?L3X_DU69AhH_sYR zh?69_+B#oy4rKhZrlv0-D9SgbIvF|YnS3W}oM}+OqKHGha`nC*bJnj?b!$BY7YWZ! z#eAt;G>T+rNM4blEJpLpM|txm0fEX|$GnfYyW{h_p-XXo1hhkpX$}eBEh3Ro?_#e7 zv}&11zQjNH=5^xeW@g0XkmP1#K69nxUr~c4y>>G(-HVZJY`tJU;!SxQB1iTYPu$-# zdZTjP8y^oko!krdOk0g8o;R5HJ%^MFPT%gVn6}AqPp~29{bwc1tYWa9sUkc2} z+b#3+vJ|hib@t5HuuIWegev8-HdGZXzT0>jJ63R^>MY@;C2eNkVq(9SEoBnSGVwSo zvcLM~-R02}a4p!`Dct?6L(B6M#Ch>KR>gy?krHnz1bNA+$nG!Ggf;JdUcAmvoWzlQ zV8|YDpS6tG(XVsNVq3!rNQ|!AyndxVT{k|;xUskJrA*Y?tTSUJnr~xAH#6x?9+9bP zH{YTBrF_44Onu-u!WPRJGMm0!e??`irTjJQTBNOCmQ$+ts)z(8JTo>`l~CZvq%>fx)+-e)5GvhV_J}ifPjFh-e~-@HN<+mEkQe$BRJBZ z^6Qr(BWEX~>CHFP;6-yNe?*=GhNYnB^!WkbyqHTD4Nd>#C0<`<=Uub{0#``iNEws6gof*6@tej8Qx$ z_Mp#f8E6BSWX7SE*t&mQoY8gq^WJxTc!>W$U6#9xsShN?|B*Fip*KWw9k7wq%5lgQ zYKw~g7;+w04-!XIh`;fIcyuqNzg}p##v=XXodyogJ9>;^Co(Wc92UU~%VH-3DJ~s^ zfWbxD6z>nWySDG?BgjJ5LwIM&jV&olZpiuNFZ&=We4-QdGefbSollqGuNDp0m7NU? z{VC{QPxj~j=;S)DXS@8Q?-domBy0N41*@5`mm;6VsswNHHiD|$&#AAQs!RY9`Vs3& z&pM)Zqf^=8G9-p4m~HnAjQS@W{)L74PZjindI-JTb*Tl&`;g@|1h>gO@{Y?0L~w6d z)+~Vn|7~$6BW!6*fz0TKMb}$m1zGobhNDVJM@(ZXty%j~#YQGkk2ZYd{UE`>7$OOF z%zC0qlj(qZGp^a&#_te{)W76=?zaFZ##`*180~T2r;17>h_8m2<3eS0^G@BjyK~nib`FtQt>`J0{5Ux%=Rk8zJ#hz_! zp5pxHAD&UgSH7_OEkq?*A+3~Y(WZUR>g;i&LdL(LDy%}H?CPF4uY#Qt*quS#&z{EV ztJv}G$+HH|P|c{GHRCw8y>$8udpgdP+bQyQ;3Ok+7QE>Xcb^3E8|Bo=sKYnlo%X)z zaU!whmBJdUQQ_ezYr>rciYrlaP0(LdAHCL&elPNnH?DV8&Mez?9r1917d{C~7TJ=d zZz3}}Yjk*VwGwJm^ZdQpO19o=(WT$IlbNSZJ%hrPVDNc|_bS zNh}Fl^6cI=SwhXq;g5gddKu=qBCiuT4A8gDQ6PYLlOsaHdgQmNo=(T{gmhgH^7!~F zhEaxf((rmL)Ul>8n{-|5nP)$?nov7|Vn=<5a8;#_R>sO7a?|!P1;g&%SX3~!{ha^2 zq*UNas)U9!_FYNX!i6R~Scw%YK|ux%G!$dmrIqM*1Xdp3OAGLV#lBtG47%sIoyO)xZL zAQlx;&?#Od_10ONr}dt&x4vjzB(B;|?lS&Tew8Fr-ERLyWtd^~YJKQAx6R$N>+v?> zvT92IAYXE0;CerA2l_~*vy(etYVs>c?|#mB|?=>?)9 zAEW>ElV;lto0`b7lY;sLdO%M>RJC_*|VoymgA0aE_U9$+%T^8VR{0fzI*DG3jeTA!&yi(H!7hX$>-r&TB5t~_g8!e?P!PTJ03&8H)DjEk-;g5OEPTThN4b&fG zA;;wPEz)M}i80C}=n5&bLiehZ8ka#UlR81n?RjLOuxYCpE%+)saEE>PB_i%&t9B!+ zFWgDEp(dJjR@V{-4*FzIV|2Yi;u?vu4ll{0>ujHg>EBVL{0zv0Cp6IY`lsD1zS05d zYe+6@;d)=s*3P_6)*`uRHeCJCI92P51)<^NTy2Ez;#P?tC62Vr?CmrbII@$zkKNq( z$c&E(o>$gOER52MI~a`WZCH!+7xq8ayqOiIVA%d3gc#_T=qSHYIdpu@%vgh`SK_@f zL`S-$bv|Pk8P|-#x-_oi9$dQe^ADze72KVTjIl@EjvksyGS_61mT$8*k0L%$VxpTx zyMaf{u&AAz&-~@|XvbyIX0YZ?5FQL_wJN3`%EoqJ>jG`rD^x@<6T`;B(3 zN@vI~tJ{H$BKWz%ad*n9c<)V@VBVA3E5U0~AB4iTTe`LH`%1X(my4iz5uoEo-> zRelMgaW4~eWkm*mcO*OYra6U4bj(QJ08O{g9C}7y8cg;JQG90cu><-xWl`AisraO-gtx*&<|afGa>Ke4#85ZvPIqkU zd)mLRhp&0vg0Bt@QnCYi9QFmBZnVG~kQX78^4bbK6O5Ad+tDq!9{{ZUb<1IpAbLC` zLEGTBx?pW%`@ylcw7(ptjz+z~w@`J<^^OALl`vk3Fp$^my2;6Gq*|+7h>2($=~r2dQt~?GMDgD6A0_e`^@M=#E&)95Y{J65i7*)(AfO zDt*o$C|h{-DVyW_SpTL5X zRdkCB-WXM{jPYIXd^g}|_B3y@@kHpi=0(bjLy|$EP$Ka_3jG!Z%#HyBHAWf&782&f zW@t;8Q!OLxZ}$%|mwIXAsqZffo9C)xS^T|CgX&%Ifp*z9Y$ilsIl96+Z8u+U{rG0% zOv!oU7eUsc=T+PwaBeUe|140_-?fJJpK9|OX1cv@s>}>AfuL+@-F($oF*l$O=$PiQy)0gH*!F33>4DiPJ4NgB z!%^DwXJfln#yiA!Jk(@J~HJq+SdN7XA7DJx@)J%>xQzh07>J zU~@(T^H`W8tMT*)=Gp$bn4}l4-jNoS*FH)0ovfTMX#FVF$y)8!(QRm*H7HclNj+}b zeQ2j(asATV2t39HL|mjy6Ez3bd&bi{W^mfKzC=(PBsh4290sh6Zz-m1RnC)uO(%-@ zJA9%Lwrjo$W=TFEkZMfBBuJTUjPhc1c>Yc&`-yWNvIU6%4jiQM!@zuU{(d;)pL# zgv65200-q0`dEukrMWKs1)V0zE12gbXQBKhzi}$qxj8Qn8vOTvFNbW!xNh`y>q7+c zN*@drURaTpJ;pGqa)mMXvestjnLebR;M!M18}GVziZ(vq)pP9T`%&@r37_fPjeX7w zQv4aoTUM3|=kDjhQtzI*Zp^VX<^{Weuid#rDZt&u*b&eAO;B7Z>x|9K@1u?wt*>2u zZ+FdsyNx-Q%ha(>?ZFzXzN-4!qni}x0AJsIF4=4N6L-n`;i?yIfb;WZ=`xV(TMwi% zxD5H^$F|=ntMJP+2W@$ViTRh&Nbf`fKZIUv)Ukq@zYmDaNA3RT>nb(OjQTlw0pDY_ z?gZY>yrzo~FMXbbgUXHM`X4hX~_;EZY*F1Y{|IHr~aY{wJz2 zP7Orr?}tNFW)+{=Duv1n+r+<`j8)ls!a_KFhnZKXY^!iVRILcO{Mu@+^!Dx3f0Fo_ i5|m@syB8MnPFZ4Mo#o6YXCVB!1g{h|UX;jN1pXi3?2hsP diff --git a/71_RayTracingPipeline/docs/Images/shader_binding_table.png b/71_RayTracingPipeline/docs/Images/shader_binding_table.png deleted file mode 100644 index b146adeec959656a937e5f8b05b10980f5a08700..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8569 zcmbVy2UJs8*LIX)7^%*nB1lnCIz);fU6f*hAWClmlqS8`1eKvl6C=Gzks>9u&`Ctl z&=HUl0)!5sh0sYz_`~>q@Bjbb%vxVL>#pqFyUxA$+57Cf&p!J((T_peER0-?004mH z!F`P<0KhNpwEu5@`<3=vM2w52i8J0$wC@5c`nmD6lV2Rvb=3iYssyHE8+zLLIgk6M z-T(kA?Jm~#GidkX=K#Pp)(0Bu1_4$ZQ>|{;?n^7gMJAQI)rt1*nP0SKzOc>67S&ZT z8A}dZ+;J-B|5*B7g3mqZ;)N^xSfe%;7Ymwe0B9%|&ME9=3K#xX_2;1rML545>;=P zi>4i|ZSf}AoXW~yLn(X4yNrNJ-C|}s+J%?54#@;mzA@mX>s`Ccr(FO5v#{=)r_v-0 z7kZ)+L=PAjG>AAuyYQLon26>Z6a##g%avw6?E(OlS9ynqs>N9e@$&I4&d#a}!lGl* z&jP8(fwBxgdyIo{NJxxTqf?ggMt3D%w*InmRZY#k5EZQu%0C`PLP)c*ug~87yxoyS z^mH(3?K{^`7y!VBTR+hQ0C$`pbDoYeO_R|N|Hh>LpH%?>0&Jwy3_yQ>z)KFrEYPCH z-*2e>^{2@Ifd2&ot5hAqB=)~QU?vyPJ})otbLq6s0OjQ6ft((nbaUqHaSY}fl2-r;{l{bY5K7*cAob|3 zwK#G{j5L$4!e8^v^k=NC#C_k(m}$+Dx1GM#qX9R@lj4quLQe%*GCtsxbiQSU1nfOj zolL8D7)1bI8j6y#CI}p@e%Ih`DRStskQ7t1`l}_SV@rpHLuT7q>1BOH-4$~75*YA# z(yApuUy-tpbvKBxv1APk1&8H5ST~c5Bw0N_&_U{`b?fggKXjbkFj;t^cSh7K4=4#*^+TxH-E<51^MnPeqOm<=>+39UE9)(Im(-`@9w2y>vrs zn3}88j7e+CYKPvGhw>DNnYh(umpY9hHeE^4F6__{)26=O?^ z(&4(Hy}4K+_D@u{I1;1$dks#{ieo`6tVNMc7Y*y&M5j`q+Y|YRt4B9rCUO3f6z4pA zz#kaO<Z?vPXt3PDWAB-@x| zz+iNZJmGMHDz2N_1wGX6N3@8vc$>=&cpaEZx;1&dYh5hyzl+Lz;6+EYzv@-7#z=^_ zVi4oo*S4P;7E7Er<|~+P9eA}X3A!O1MW3VecG_y+Gt5$foVW9O&gMY$A=&1!A5-1M zkIO6hh4V3)X-Ziy_2L)ii-B#LdT;$@B!2HK<=qlSYV+BV20`lx3)ZfjEJNHHsGgnc*`0X_msMr_{_RoKXe zbXjD+@2lb7e6+V0W7#6z32_+P-rBzLCn z^KoPShpY3697V__Q^03Mea?tI4CN_E7iA;Rs3P3VUg|HRw`dEL{EVR>-uLx1G_urP zh-O57%xNTxwy)SZ>g~foeV@i982lC&+jNH*B`^3bG>tVUW=v;4KohSDf8u4pmq5@L z#^om7-7%*#fjQaS6xv=*e&HPgU+n9*^3Yd#2b{JttiO$%sO}0`_HLXK6VaA6W17EG zK-l|Q5|Ss#45$Q%wpVJYw`oZ(>_k4|?0=}__CWnsYHi7}v+=c2DPZA6_eeS^mcd>e z(V>DiV6Uc9yw0~WI~A1;_B4+3G=HmgI=N4Zj2x2Zk#Sbcd;MvCkmbXz&QhqeZAc$I zDv%d9|EBVyQwUY%9n7un3w?CNS7Sbc1amlhG)fD6JxJLW&p_2C)t>k=ELal zdl6nFhc5bvcngCcXY*aG9zM2KB>Wowp*O2%?GdL2aHsaM@>_o*gAb$noqebJk6Z!#2pz=ywm^ zPJZk!vSDY84Az^{cW5}^-2Q~)x&-bk3RY2QNPjrUepq=+9N8%Sr|M8jxksB$&75;n z|IH*{MW>Hx_x&@(5H)tGW{cRDmWAp0A-BYGo{K-#a(<#9Ue|k&#Xz)-DHm~I$03Ac z-9^ippg68#=JueCjUJoF7eqIpG+(#i>G0ow{lNV@+TW*V$<9f;vqW1oe-Sai_IiU4 z((*2>7!L`W(%!BE4#kY_DGEn@B)oYsRzt`eVEK5`l~_1fKFQr8OK?8(7|RkH>=+h3bldPCjBVQkg^?Lbm^Rtfo}0m*v(3ffZB=W( zXj!?c>djpNXJ())=4yW(zVx0=z9;4QN`bFWkd)ZH^%fXK-&4^%Y<>sUOx_ZmNbuye z=Sp^8vwG|vrj^vrpT49?>d#;&TBd9W)AaQfiGmv)nmS2v-+_O5E?ttT5&6CsG*1j5 zt&PF)1379%YS3fr75#zVae;V`nTl(l>xaANudu{TZ6dB^6AKT7k7w=VqY`Sbbpv0r-NR9bl%y6B z8Vqj851)|48sC)GDECxVa1W+KUI)j%49;-+ij%k!J5c_wY2I7uUBZn7rrK}QNM>DN zv;Rb7d5gHpnLZM;6eOeoWNo)2jv#qXWY11J>~aREq}|*l4-3FgWA|zJwj;*z?ujR3 z_zF=7(UF`7-O_EEgupQ_?rXmr@pnMTo7q{9{5|k3D{gKI>*vnnSiT*c>&A5W%Mjy8 z;n=n4UEC!LKFMfW?ie$us;%=gWR;e)vF?{pwbva;G>?__ynQmGv2E|5CkfJyeZl~^ zbH(YT@ty?GaJavqy}Hk{>jS;h*PIgUlO;j?n1n?n`3oX#u~O(xh(LC7O|<)SI^syO z^F)(NkYs)Ejj^alw3P4j>(^l!v>?ouB3-6vCd5!v+_>&M_NreBc*%Dk`+j!14?mvi z*emnF;LkIF??z_4o^{2)0e@>RO5e?cYD?DkYXWcKG~(M*nte74+KjQ z56hw#D~s;_$E#sS#@Z77M#bf)dEq5LAFr#e(>I;6*9!7eE=+wC%SicyTQ);B6P5kp zAjs={=7*SU(k}pj>-mIFX`AxPq&}<1$d#Bt61w(F=RW@F_b&oQ^D<$FtM$bLA1q8u ziY-Pfy~bhu$cXwympNx;yAW@jKnQp3r;V@t>0r*}7fTL$o1mr2yQF)FgTV5KaS~hC zQJk&jGcWo%r|CW1YgS~;buw5PBHM=~x#CqL+S$-JQ}?+J1l4Y*noD*vIONIAKFECS zudq_Ro<2sx3yhPFpfOY~&>;>zYB!pr!t09bp9aif1L&7a`&h!=aOc2@pK##=RYJ(} zE^MKm!N$u8QU$sl89V3IwV4(&)DNFOg1axyROa2#h#T-(l+JWeTxC`I+W#;miLqPx zlRC|KS|Ry%DiXAB+!0gMjZ!z$F5;+JYtQnpRucs+Ke1@niAT@l;b2suS|~M>dPr<% z)@?*EMoe(yJ+dou6%uRihxAeV*8O?T*8NUC7Ja@`G7Di=pGF`Kt|#T2SXj)NeQ+|@^wBI@GmA#|?saPJ(~?aPKYmC4fGP!5&^ z6(UQm0@CEW-~09`LeqIL>K1L$L0H^)_O3ZJyW7S_+p{M9*+#8oiRP%?yXHkBrfg&P z1>H|yb;@gr%5l0<=4!n*NR!wxK{HM1n`ga=8O_G$;zp}*S#d&Y`)^S z`P$5%Vc1`@XTSd2w;+^9#z^u>pGn!o=AZcrWt@BY3kp82t;Ca67dDef7hah_ei#E! z;B{)&B|fH}czE6GmYhEplKkoLRv2_XE&ut!(Rk>xm{je1u`grCB2BD8tPZV8R;q}p3da0SoxR9yjVU1%w1 zmgy6MIp~GKJ~~-l_7#_>rLVV@F+5p5c9+$*6dB(D^x0e2+y;}Wssr0UBpP-iua15^cz3^!%$}zw$g?82hJb2 zQ_J~LN!))KVW@>qY*pKamaAXyYwQ_IdYB*=rnflQg#Kh5n!2>qMvixNk(k4_9Hv8T z_p%bBZHNaWjAwUlQ##3l0SVlHX$GNWw zEqW{1m6YPxv2w1%pFE$T08Rsim5&$LnruUAlGr9fqZF3=YEItF;j34UFhg&~*>Se2 z9UOqla++>65f@sAHnFeSv-9)iAT1K&F5KnoCRQ#d%83}I!G;uSG6{~hwZC@>*kzI5z-yUq(S+xfv! zvu?QMWZu@~C2?8H%ltqpx9mB#?YT7&|! z>I8!qK_H-&kiSq*UA>4upOvP+e!GbIfINq{c0*ohqG+i}q=awEM7B{woS^w_R1y!^ z)pv(lk(uRh=aJc%0f$`C0@A*rEaM+Sw)D7u`VRSWDWb#?64M>Ikg701t(kKY6|hG| z2NVYvg+ui^R1*$^*m19=wLXa>lD%|mk%n-kQb&p%wmhTIus%UntCGlnqk#V2NGb^TuXN5s%To0na%^k@QHzjjaz+?MOt#+*5>wLnGXP3s# zcD!n-1%1%wgD>1s!v0tU9z2Bz7cB9xWhIOq_3xH^$V&P~mQ9V1q?qQI8AJ{g+G!5;%9=HKv=)ZQq zeMH@Tly4?)4ReWRCUv3&71q?%IO*Z;7jn3+i z`8dmGwF(udb(M8l-h?6)l#)(8KJq5bqZ6DeP0sTr#F2*E-8>pZ@^(HdNtoshwj>Y~ z4Jh_TuF)Iisz~GE3Nf{X?Py6Mdoq9IFjY7RyN9G;)`O};!M^zes|M8;TQBOedc7hk zPN*vbuoCaFrBM#gLLjQ5)SA*2lV~0o0{?AAysk=~ zzHCP@@jGG9VvP4}t54@%Mz+N|dZ$S`z*voCNk4Ic%z&3dN3M`#?c2Nf{+iwdq?<10 zqUwJ`8>4;)158p?vsc$swFrG`YKuu)Ndt2& zV~K254D*u#JnQq+M=;ii$HzQZ24XKO1(OP7?d2SfjcVXggMZ|$75Yr>itv-8b6@Sw z33Y8Ta(1fI8MsZOij8MNQT^^%nL_Fs6ioIl?Ji@j?| zQ)u=(VJCI(-V^0tf=1r1jVkl-AS#Dvd*2?kJ?CZ99m-U2!tjuO#l~vWw<0GisMB&W z^QE{bAV9pr#XzSSTvs`OX>h3u_LH1yvkS;o_BVANlz#ev)6oed+uEn>1+M2Fd!8n} zVx`AgF-NhyD1&X_xp<2qvgFuH)I$M z2hS72@1e$W{dw1^n%hDw?LJG+=02Uit?j>C{s8-iq1c{Mk~${ayBM$+RHW#Il4j3l zktV)xzg;pRz2S!VMuwzXVl zrab6zi}~>}IX|@@6DwC&V?48n?xq$c%yc*TTFK{ekXk!}#-%9{lYQ3L_ys9Nrwc0f zY0XNF9dFmFW}e^bj?3eUKReG0f*MnmFE&C#Megw?(lT%D{`2EjU(PJ9S7uUpnb-uu38oyQM%hII(#Z5X#ZYM{!D}3u*J%L`2>J558UF zY<9>gfDas?g@E1;-=Hq)rU{FeH z#ut^U3bOAgZ^Pv~QAoz_C+INDGyRX%i`=Cl21R8_)z)bxdCaw$F6q;c2qj{{brV6| zjXT%09!bS9#mk`GAsT~jEPk{q$Kctf&>QhY#d%ys!xPSkZ_?rFr82%*y<6kDA~W)- z_|l$O058Ym_INYY_4?~E5dpv;oRM@US)X$4yRq~_5a=zH7RQ-ER`A{HRpwHeEh2Au zJA0BcGv!K;jwDVR+P+0pPT!3Ch$l;8IBc?IQFvX`p33PCWi9F+!_in#sss53OV5-3 zipM-di>?FOuUC_R^AGmbl2Bq1)K5 ze42l;SabJrR;qy7CAVSQWX}cMI+=d0Y1JP2OnIHc{@tf6c@xV;{vysnU{czTKiC*h zJSf?#zqs=?q3cHbG1_Y_=Js$3cTIOwYOgX*wx@ZDcTe{xNhNthAL`tuTe zZKXi0{jf1%u>3xZ{8FaM$0wF+YLJPq)RUzLj_$}iSR2xQxjR3*>|KR;&j*W%ID(H( zr@}&6wt+_+EV(G-(!Dp)w-&cH?#3Sf_{(#a6a!548%mVcT}Hex)blT2Ea^XKmcbU5 z_PQJ1E8}~pR&iOD!)+hur{-xBdai$VjzvkWU#2V=>C_PTLpZSP!*k0vso9yinnvqp z=%BETU2DmTEbOAQ z%Q@RxtF&j6FehVLG`J_@bmqn3$ORw9w0IR)`u6@&OIF8H#UqL8^lPJ!B4YfD>vc;{ z(n{U7{5_=H_gc?;!^Lh3Mp^aJa|E^<`aW)Q^`X7}qZdL|Uz0HQ$A$IY%2}dnT_jt5 z*?Ou@@2MT+wW#!ft29z6{Q&`>XdW1@tEAgQnY3JbZR4;e$xv&Ubu2h0#%-&D_4FFF zETOLt+|_@r8OG#CPAseT-RRCK;;$zF?Z$V}uC+;8pFV=FV_y^$BfeF)yJp1KocUY~QEV8B`Sg*9S06raf0N_fJjidN5IGi(4&W#@> zKwJ2aK-Fz^~KJr$H}9SOMxV8%%FmasV6U>pWCX& zC>+(7bfPw56adyLem?iR{()!85bWH7UE+^|qoUxrC<6PS(FFpyo5LY;aP3}*{K`37 zP)N>iNA-|Sl*u2&y(`2$^Q^-cmK+i^8d2EuVAgQ_4L`36+qA$E^=k6Qz1}kb94Zu; zMLk;PGmChY?uq*yOv?)X;TVYqo<8HWeHZ;ZllZ@2)wcescmAEr1OR;$v${`9X@9kU zrs4j_2wHBY>4*ybolX7Blm#j}o!WlBt^6O@)BiaF08mE|_D?Eo-7Oj}$^5&{{{?^h zUm@_@BoK-Q*E;@xCOt;CdU%vCnkaK?>Q{xDdvNZBl!(mCvO!q^AtCQ0BN@_k1f_Ws7BMvrOTK9V(0^|m z$O)w;;-F+2-TEK!<8~Vpk+ucE1e&C6BKWzPpkB9-fI6YTkJrE_Z>Bx5&q+Vz2x^+N heFXn91FTDfs#Z0vo4agJEIw83fhI_!;;!}U{{!}yoZJ8a diff --git a/71_RayTracingPipeline/include/common.hpp b/71_RayTracingPipeline/include/common.hpp deleted file mode 100644 index e6b538618..000000000 --- a/71_RayTracingPipeline/include/common.hpp +++ /dev/null @@ -1,35 +0,0 @@ -#ifndef _NBL_THIS_EXAMPLE_COMMON_H_INCLUDED_ -#define _NBL_THIS_EXAMPLE_COMMON_H_INCLUDED_ - -#include "nbl/examples/examples.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::application_templates; -using namespace nbl::examples; - -#include "nbl/ui/ICursorControl.h" -#include "nbl/ext/ImGui/ImGui.h" -#include "imgui/imgui_internal.h" - -#include "app_resources/common.hlsl" - -namespace nbl::scene -{ - -struct ReferenceObjectCpu -{ - core::smart_refctd_ptr data; - Material material; - hlsl::float32_t3x4 transform; - -}; - -} - -#endif // __NBL_THIS_EXAMPLE_COMMON_H_INCLUDED__ diff --git a/71_RayTracingPipeline/main.cpp b/71_RayTracingPipeline/main.cpp deleted file mode 100644 index d18b85daf..000000000 --- a/71_RayTracingPipeline/main.cpp +++ /dev/null @@ -1,1522 +0,0 @@ -// Copyright (C) 2018-2024 - 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 "common.hpp" - -#include "nbl/this_example/builtin/build/spirv/keys.hpp" - -#include "nbl/ext/FullScreenTriangle/FullScreenTriangle.h" -#include "nbl/builtin/hlsl/indirect_commands.hlsl" - -#include "nbl/examples/common/BuiltinResourcesApplication.hpp" -#include -#include -#include -#include - -class RaytracingPipelineApp final : public SimpleWindowedApplication, public BuiltinResourcesApplication -{ - using device_base_t = SimpleWindowedApplication; - using asset_base_t = BuiltinResourcesApplication; - using clock_t = std::chrono::steady_clock; - - constexpr static inline uint32_t WIN_W = 1280, WIN_H = 720; - constexpr static inline uint32_t MaxFramesInFlight = 3u; - constexpr static inline uint8_t MaxUITextureCount = 1u; - constexpr static inline uint32_t NumberOfProceduralGeometries = 5; - - static constexpr const char* s_lightTypeNames[E_LIGHT_TYPE::ELT_COUNT] = { - "Directional", - "Point", - "Spot" - }; - - -public: - inline RaytracingPipelineApp(const path& _localInputCWD, const path& _localOutputCWD, const path& _sharedInputCWD, const path& _sharedOutputCWD) - : IApplicationFramework(_localInputCWD, _localOutputCWD, _sharedInputCWD, _sharedOutputCWD) - { - } - - inline SPhysicalDeviceFeatures getRequiredDeviceFeatures() const override - { - auto retval = device_base_t::getRequiredDeviceFeatures(); - retval.rayTracingPipeline = true; - retval.accelerationStructure = true; - retval.rayQuery = true; - return retval; - } - - inline SPhysicalDeviceFeatures getPreferredDeviceFeatures() const override - { - auto retval = device_base_t::getPreferredDeviceFeatures(); - retval.accelerationStructureHostCommands = true; - retval.pipelineExecutableInfo = true; - return retval; - } - - inline core::vector getSurfaces() const override - { - if (!m_surface) - { - { - auto windowCallback = core::make_smart_refctd_ptr(smart_refctd_ptr(m_inputSystem), smart_refctd_ptr(m_logger)); - IWindow::SCreationParams params = {}; - params.callback = core::make_smart_refctd_ptr(); - params.width = WIN_W; - params.height = WIN_H; - params.x = 32; - params.y = 32; - params.flags = ui::IWindow::ECF_HIDDEN | IWindow::ECF_BORDERLESS | IWindow::ECF_RESIZABLE; - params.windowCaption = "RaytracingPipelineApp"; - params.callback = windowCallback; - 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) = CSimpleResizeSurface::create(std::move(surface)); - } - - if (m_surface) - return { {m_surface->getSurface()/*,EQF_NONE*/} }; - - return {}; - } - - // so that we can use the same queue for asset converter and rendering - inline core::vector getQueueRequirements() const override - { - auto reqs = device_base_t::getQueueRequirements(); - reqs.front().requiredFlags |= IQueue::FAMILY_FLAGS::COMPUTE_BIT; - return reqs; - } - - inline bool onAppInitialized(smart_refctd_ptr&& system) override - { - m_inputSystem = make_smart_refctd_ptr(logger_opt_smart_ptr(smart_refctd_ptr(m_logger))); - - if (!device_base_t::onAppInitialized(std::move(system))) - return false; - - if (!asset_base_t::onAppInitialized(smart_refctd_ptr(m_system))) - return false; - - // Load Custom Shader - auto loadPrecompiledShader = [&]() -> smart_refctd_ptr - { - IAssetLoader::SAssetLoadParams lp = {}; - lp.logger = m_logger.get(); - lp.workingDirectory = "app_resources"; // virtual root - auto key = nbl::this_example::builtin::build::get_spirv_key(m_device.get()); - auto assetBundle = m_assetMgr->getAsset(key.data(), lp); - const auto assets = assetBundle.getContents(); - if (assets.empty()) - return nullptr; - - // lets go straight from ICPUSpecializedShader to IGPUSpecializedShader - auto shader = IAsset::castDown(assets[0]); - if (!shader) - { - m_logger->log("Failed to load a precompiled shader.", ILogger::ELL_ERROR); - return nullptr; - } - - return shader; - }; - - // load shaders - const auto raygenShader = loadPrecompiledShader.operator() < "raytrace_rgen" > (); // "app_resources/raytrace.rgen.hlsl" - const auto closestHitShader = loadPrecompiledShader.operator() < "raytrace_rchit" > (); // "app_resources/raytrace.rchit.hlsl" - const auto proceduralClosestHitShader = loadPrecompiledShader.operator() < "raytrace_procedural_rchit" > (); // "app_resources/raytrace_procedural.rchit.hlsl" - const auto intersectionHitShader = loadPrecompiledShader.operator() < "raytrace_rint" > (); // "app_resources/raytrace.rint.hlsl" - const auto anyHitShaderColorPayload = loadPrecompiledShader.operator() < "raytrace_rahit" > (); // "app_resources/raytrace.rahit.hlsl" - const auto anyHitShaderShadowPayload = loadPrecompiledShader.operator() < "raytrace_shadow_rahit" > (); // "app_resources/raytrace_shadow.rahit.hlsl" - const auto missShader = loadPrecompiledShader.operator() < "raytrace_rmiss" > (); // "app_resources/raytrace.rmiss.hlsl" - const auto missShadowShader = loadPrecompiledShader.operator() < "raytrace_shadow_rmiss" > (); // "app_resources/raytrace_shadow.rmiss.hlsl" - const auto directionalLightCallShader = loadPrecompiledShader.operator() < "light_directional_rcall" > (); // "app_resources/light_directional.rcall.hlsl" - const auto pointLightCallShader = loadPrecompiledShader.operator() < "light_point_rcall" > (); // "app_resources/light_point.rcall.hlsl" - const auto spotLightCallShader = loadPrecompiledShader.operator() < "light_spot_rcall" > (); // "app_resources/light_spot.rcall.hlsl" - const auto fragmentShader = loadPrecompiledShader.operator() < "present_frag" > (); // "app_resources/present.frag.hlsl" - - m_semaphore = m_device->createSemaphore(m_realFrameIx); - if (!m_semaphore) - return logFail("Failed to Create a Semaphore!"); - - auto gQueue = getGraphicsQueue(); - - // Create renderpass and init surface - nbl::video::IGPURenderpass* renderpass; - { - ISwapchain::SCreationParams swapchainParams = { .surface = smart_refctd_ptr(m_surface->getSurface()) }; - if (!swapchainParams.deduceFormat(m_physicalDevice)) - return logFail("Could not choose a Surface Format for the Swapchain!"); - - const static IGPURenderpass::SCreationParams::SSubpassDependency dependencies[] = - { - { - .srcSubpass = IGPURenderpass::SCreationParams::SSubpassDependency::External, - .dstSubpass = 0, - .memoryBarrier = - { - .srcStageMask = asset::PIPELINE_STAGE_FLAGS::COPY_BIT, - .srcAccessMask = asset::ACCESS_FLAGS::TRANSFER_WRITE_BIT, - .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 - } - }, - IGPURenderpass::SCreationParams::DependenciesEnd - }; - - auto scResources = std::make_unique(m_device.get(), swapchainParams.surfaceFormat.format, dependencies); - renderpass = scResources->getRenderpass(); - - if (!renderpass) - return logFail("Failed to create Renderpass!"); - - if (!m_surface || !m_surface->init(gQueue, std::move(scResources), swapchainParams.sharedParams)) - return logFail("Could not create Window & Surface or initialize the Surface!"); - } - - auto pool = m_device->createCommandPool(gQueue->getFamilyIndex(), IGPUCommandPool::CREATE_FLAGS::RESET_COMMAND_BUFFER_BIT); - - m_converter = CAssetConverter::create({ .device = m_device.get(), .optimizer = {} }); - - for (auto i = 0u; i < MaxFramesInFlight; i++) - { - if (!pool) - return logFail("Couldn't create Command Pool!"); - if (!pool->createCommandBuffers(IGPUCommandPool::BUFFER_LEVEL::PRIMARY, { m_cmdBufs.data() + i, 1 })) - return logFail("Couldn't create Command Buffer!"); - } - - m_winMgr->setWindowSize(m_window.get(), WIN_W, WIN_H); - m_surface->recreateSwapchain(); - - - // create output images - m_hdrImage = m_device->createImage({ - { - .type = IGPUImage::ET_2D, - .samples = ICPUImage::ESCF_1_BIT, - .format = EF_R16G16B16A16_SFLOAT, - .extent = {WIN_W, WIN_H, 1}, - .mipLevels = 1, - .arrayLayers = 1, - .flags = IImage::ECF_NONE, - .usage = bitflag(IImage::EUF_STORAGE_BIT) | IImage::EUF_TRANSFER_SRC_BIT | IImage::EUF_SAMPLED_BIT - } - }); - - if (!m_hdrImage || !m_device->allocate(m_hdrImage->getMemoryReqs(), m_hdrImage.get()).isValid()) - return logFail("Could not create HDR Image"); - - m_hdrImageView = m_device->createImageView({ - .flags = IGPUImageView::ECF_NONE, - .subUsages = IGPUImage::E_USAGE_FLAGS::EUF_STORAGE_BIT | IGPUImage::E_USAGE_FLAGS::EUF_SAMPLED_BIT, - .image = m_hdrImage, - .viewType = IGPUImageView::E_TYPE::ET_2D, - .format = asset::EF_R16G16B16A16_SFLOAT - }); - - - - // ray trace pipeline and descriptor set layout setup - { - const auto bindings = std::array{ - ICPUDescriptorSetLayout::SBinding{ - .binding = 0, - .type = asset::IDescriptor::E_TYPE::ET_ACCELERATION_STRUCTURE, - .createFlags = IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - .stageFlags = asset::IShader::E_SHADER_STAGE::ESS_RAYGEN, - .count = 1, - }, - { - .binding = 1, - .type = asset::IDescriptor::E_TYPE::ET_STORAGE_IMAGE, - .createFlags = IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - .stageFlags = asset::IShader::E_SHADER_STAGE::ESS_RAYGEN, - .count = 1, - } - }; - auto cpuDescriptorSetLayout = core::make_smart_refctd_ptr(bindings); - - const SPushConstantRange pcRange = { - .stageFlags = IShader::E_SHADER_STAGE::ESS_ALL_RAY_TRACING, - .offset = 0u, - .size = sizeof(SPushConstants), - }; - const auto cpuPipelineLayout = core::make_smart_refctd_ptr(std::span({ pcRange }), std::move(cpuDescriptorSetLayout), nullptr, nullptr, nullptr); - - const auto pipeline = ICPURayTracingPipeline::create(cpuPipelineLayout.get()); - { - core::bitflag flags = IGPURayTracingPipeline::SCreationParams::FLAGS::NO_NULL_INTERSECTION_SHADERS; - if (m_device->getEnabledFeatures().pipelineExecutableInfo) - { - flags |= IGPURayTracingPipeline::SCreationParams::FLAGS::CAPTURE_STATISTICS; - flags |= IGPURayTracingPipeline::SCreationParams::FLAGS::CAPTURE_INTERNAL_REPRESENTATIONS; - } - pipeline->getCachedCreationParams() = { - .flags = flags, - .maxRecursionDepth = 1, - .dynamicStackSize = true, - }; - } - - pipeline->getSpecInfos(ESS_RAYGEN)[0] = { - .shader = raygenShader, - .entryPoint = "main", - }; - - pipeline->getSpecInfoVector(ESS_MISS)->resize(EMT_COUNT); - const auto missGroups = pipeline->getSpecInfos(ESS_MISS); - missGroups[EMT_PRIMARY] = { .shader = missShader, .entryPoint = "main" }; - missGroups[EMT_OCCLUSION] = { .shader = missShadowShader, .entryPoint = "main" }; - - auto getHitGroupIndex = [](E_GEOM_TYPE geomType, E_RAY_TYPE rayType) - { - return geomType * ERT_COUNT + rayType; - }; - - const auto hitGroupCount = ERT_COUNT * EGT_COUNT; - pipeline->getSpecInfoVector(ESS_CLOSEST_HIT)->resize(hitGroupCount); - pipeline->getSpecInfoVector(ESS_ANY_HIT)->resize(hitGroupCount); - pipeline->getSpecInfoVector(ESS_INTERSECTION)->resize(hitGroupCount); - - const auto closestHitSpecs = pipeline->getSpecInfos(ESS_CLOSEST_HIT); - const auto anyHitSpecs = pipeline->getSpecInfos(ESS_ANY_HIT); - const auto intersectionSpecs = pipeline->getSpecInfos(ESS_INTERSECTION); - - closestHitSpecs[getHitGroupIndex(EGT_TRIANGLES, ERT_PRIMARY)] = { .shader = closestHitShader, .entryPoint = "main" }; - anyHitSpecs[getHitGroupIndex(EGT_TRIANGLES, ERT_PRIMARY)] = { .shader = anyHitShaderColorPayload, .entryPoint = "main" }; - - anyHitSpecs[getHitGroupIndex(EGT_TRIANGLES, ERT_OCCLUSION)] = { .shader = anyHitShaderShadowPayload, .entryPoint = "main" }; - - closestHitSpecs[getHitGroupIndex(EGT_PROCEDURAL, ERT_PRIMARY)] = { .shader = proceduralClosestHitShader, .entryPoint = "main" }; - anyHitSpecs[getHitGroupIndex(EGT_PROCEDURAL, ERT_PRIMARY)] = { .shader = anyHitShaderColorPayload, .entryPoint = "main" }; - intersectionSpecs[getHitGroupIndex(EGT_PROCEDURAL, ERT_PRIMARY)] = { .shader = intersectionHitShader, .entryPoint = "main" }; - - anyHitSpecs[getHitGroupIndex(EGT_PROCEDURAL, ERT_OCCLUSION)] = { .shader = anyHitShaderShadowPayload, .entryPoint = "main" }; - intersectionSpecs[getHitGroupIndex(EGT_PROCEDURAL, ERT_OCCLUSION)] = { .shader = intersectionHitShader, .entryPoint = "main" }; - - pipeline->getSpecInfoVector(ESS_CALLABLE)->resize(ELT_COUNT); - const auto callableGroups = pipeline->getSpecInfos(ESS_CALLABLE); - callableGroups[ELT_DIRECTIONAL] = { .shader = directionalLightCallShader, .entryPoint = "main" }; - callableGroups[ELT_POINT] = { .shader = pointLightCallShader, .entryPoint = "main" }; - callableGroups[ELT_SPOT] = { .shader = spotLightCallShader, .entryPoint = "main" }; - - smart_refctd_ptr converter = CAssetConverter::create({ .device = m_device.get(), .optimizer = {} }); - CAssetConverter::SInputs inputs = {}; - inputs.logger = m_logger.get(); - - const std::array cpuPipelines = { pipeline.get() }; - std::get>(inputs.assets) = cpuPipelines; - - CAssetConverter::SConvertParams params = {}; - params.utilities = m_utils.get(); - - auto reservation = converter->reserve(inputs); - auto future = reservation.convert(params); - if (future.copy() != IQueue::RESULT::SUCCESS) - { - m_logger->log("Failed to await submission feature!", ILogger::ELL_ERROR); - return false; - } - - // assign gpu objects to output - auto&& pipelines = reservation.getGPUObjects(); - m_rayTracingPipeline = pipelines[0].value; - - if (m_device->getEnabledFeatures().pipelineExecutableInfo) - { - auto report = system::to_string(m_rayTracingPipeline->getExecutableInfo()); - m_logger->log("Ray Tracing Pipeline Executable Report:\n%s", ILogger::ELL_PERFORMANCE, report.c_str()); - } - const auto* gpuDsLayout = m_rayTracingPipeline->getLayout()->getDescriptorSetLayouts()[0]; - - const std::array dsLayoutPtrs = { gpuDsLayout }; - m_rayTracingDsPool = m_device->createDescriptorPoolForDSLayouts(IDescriptorPool::ECF_UPDATE_AFTER_BIND_BIT, std::span(dsLayoutPtrs.begin(), dsLayoutPtrs.end())); - m_rayTracingDs = m_rayTracingDsPool->createDescriptorSet(core::smart_refctd_ptr(gpuDsLayout)); - - calculateRayTracingStackSize(m_rayTracingPipeline); - - if (!createShaderBindingTable(m_rayTracingPipeline)) - return logFail("Could not create shader binding table"); - - } - - auto assetManager = make_smart_refctd_ptr(smart_refctd_ptr(m_system)); - - if (!createIndirectBuffer()) - return logFail("Could not create indirect buffer"); - - if (!createAccelerationStructuresFromGeometry()) - return logFail("Could not create acceleration structures from geometry creator"); - - ISampler::SParams samplerParams = { - .AnisotropicFilter = 0 - }; - auto defaultSampler = m_device->createSampler(samplerParams); - - { - const IGPUDescriptorSetLayout::SBinding bindings[] = { - { - .binding = 0u, - .type = nbl::asset::IDescriptor::E_TYPE::ET_COMBINED_IMAGE_SAMPLER, - .createFlags = ICPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - .stageFlags = IShader::E_SHADER_STAGE::ESS_FRAGMENT, - .count = 1u, - .immutableSamplers = &defaultSampler - } - }; - auto gpuPresentDescriptorSetLayout = m_device->createDescriptorSetLayout(bindings); - const video::IGPUDescriptorSetLayout* const layouts[] = { gpuPresentDescriptorSetLayout.get() }; - const uint32_t setCounts[] = { 1u }; - m_presentDsPool = m_device->createDescriptorPoolForDSLayouts(IDescriptorPool::E_CREATE_FLAGS::ECF_NONE, layouts, setCounts); - m_presentDs = m_presentDsPool->createDescriptorSet(gpuPresentDescriptorSetLayout); - - auto scRes = static_cast(m_surface->getSwapchainResources()); - ext::FullScreenTriangle::ProtoPipeline fsTriProtoPPln(m_assetMgr.get(), m_device.get(), m_logger.get()); - if (!fsTriProtoPPln) - return logFail("Failed to create Full Screen Triangle protopipeline or load its vertex shader!"); - - const IGPUPipelineBase::SShaderSpecInfo fragSpec = { - .shader = fragmentShader.get(), - .entryPoint = "main", - }; - - auto presentLayout = m_device->createPipelineLayout( - {}, - core::smart_refctd_ptr(gpuPresentDescriptorSetLayout), - nullptr, - nullptr, - nullptr - ); - m_presentPipeline = fsTriProtoPPln.createPipeline(fragSpec, presentLayout.get(), scRes->getRenderpass()); - if (!m_presentPipeline) - return logFail("Could not create Graphics Pipeline!"); - } - - // write descriptors - IGPUDescriptorSet::SDescriptorInfo infos[3]; - infos[0].desc = m_gpuTlas; - - infos[1].desc = m_hdrImageView; - if (!infos[1].desc) - return logFail("Failed to create image view"); - infos[1].info.image.imageLayout = IImage::LAYOUT::GENERAL; - - infos[2].desc = m_hdrImageView; - infos[2].info.image.imageLayout = IImage::LAYOUT::READ_ONLY_OPTIMAL; - - IGPUDescriptorSet::SWriteDescriptorSet writes[] = { - {.dstSet = m_rayTracingDs.get(), .binding = 0, .arrayElement = 0, .count = 1, .info = &infos[0]}, - {.dstSet = m_rayTracingDs.get(), .binding = 1, .arrayElement = 0, .count = 1, .info = &infos[1]}, - {.dstSet = m_presentDs.get(), .binding = 0, .arrayElement = 0, .count = 1, .info = &infos[2] }, - }; - m_device->updateDescriptorSets(std::span(writes), {}); - - // gui descriptor setup - { - using binding_flags_t = IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS; - { - IGPUSampler::SParams params; - params.AnisotropicFilter = 1u; - params.TextureWrapU = ETC_REPEAT; - params.TextureWrapV = ETC_REPEAT; - params.TextureWrapW = ETC_REPEAT; - - m_ui.samplers.gui = m_device->createSampler(params); - m_ui.samplers.gui->setObjectDebugName("Nabla IMGUI UI Sampler"); - } - - std::array, 69u> immutableSamplers; - for (auto& it : immutableSamplers) - it = smart_refctd_ptr(m_ui.samplers.scene); - - immutableSamplers[nbl::ext::imgui::UI::FontAtlasTexId] = smart_refctd_ptr(m_ui.samplers.gui); - - 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, MaxUITextureCount); - params.renderpass = smart_refctd_ptr(renderpass); - params.streamingBuffer = nullptr; - params.subpassIx = 0u; - params.transfer = getGraphicsQueue(); - params.utilities = m_utils; - { - m_ui.manager = ext::imgui::UI::create(std::move(params)); - - // note that we use default layout provided by our extension, but you are free to create your own by filling nbl::ext::imgui::UI::S_CREATION_PARAMETERS::resources - const auto* descriptorSetLayout = m_ui.manager->getPipeline()->getLayout()->getDescriptorSetLayout(0u); - const auto& params = m_ui.manager->getCreationParameters(); - - IDescriptorPool::SCreateInfo descriptorPoolInfo = {}; - descriptorPoolInfo.maxDescriptorCount[static_cast(asset::IDescriptor::E_TYPE::ET_SAMPLER)] = (uint32_t)nbl::ext::imgui::UI::DefaultSamplerIx::COUNT; - descriptorPoolInfo.maxDescriptorCount[static_cast(asset::IDescriptor::E_TYPE::ET_SAMPLED_IMAGE)] = MaxUITextureCount; - descriptorPoolInfo.maxSets = 1u; - descriptorPoolInfo.flags = IDescriptorPool::E_CREATE_FLAGS::ECF_UPDATE_AFTER_BIND_BIT; - - m_guiDescriptorSetPool = m_device->createDescriptorPool(std::move(descriptorPoolInfo)); - assert(m_guiDescriptorSetPool); - - m_guiDescriptorSetPool->createDescriptorSets(1u, &descriptorSetLayout, &m_ui.descriptorSet); - assert(m_ui.descriptorSet); - } - } - - m_ui.manager->registerListener( - [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; - }()); - - 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("Controls"); - - ImGui::SameLine(); - - ImGui::Text("Camera"); - - ImGui::SliderFloat("Move speed", &m_cameraSetting.moveSpeed, 0.1f, 10.f); - ImGui::SliderFloat("Rotate speed", &m_cameraSetting.rotateSpeed, 0.1f, 10.f); - ImGui::SliderFloat("Fov", &m_cameraSetting.fov, 20.f, 150.f); - ImGui::SliderFloat("zNear", &m_cameraSetting.zNear, 0.1f, 100.f); - ImGui::SliderFloat("zFar", &m_cameraSetting.zFar, 110.f, 10000.f); - Light m_oldLight = m_light; - int light_type = m_light.type; - ImGui::ListBox("LightType", &light_type, s_lightTypeNames, ELT_COUNT); - m_light.type = static_cast(light_type); - if (m_light.type == ELT_DIRECTIONAL) - { - ImGui::SliderFloat3("Light Direction", &m_light.direction.x, -1.f, 1.f); - } - else if (m_light.type == ELT_POINT) - { - ImGui::SliderFloat3("Light Position", &m_light.position.x, -20.f, 20.f); - } - else if (m_light.type == ELT_SPOT) - { - ImGui::SliderFloat3("Light Direction", &m_light.direction.x, -1.f, 1.f); - ImGui::SliderFloat3("Light Position", &m_light.position.x, -20.f, 20.f); - - float32_t dOuterCutoff = hlsl::degrees(acos(m_light.outerCutoff)); - if (ImGui::SliderFloat("Light Outer Cutoff", &dOuterCutoff, 0.0f, 45.0f)) - { - m_light.outerCutoff = cos(hlsl::radians(dOuterCutoff)); - } - } - ImGui::Checkbox("Use Indirect Command", &m_useIndirectCommand); - if (m_light != m_oldLight) - { - m_frameAccumulationCounter = 0; - } - - ImGui::Text("X: %f Y: %f", io.MousePos.x, io.MousePos.y); - - ImGui::End(); - } - ); - - // Set Camera - { - core::vectorSIMDf cameraPosition(0, 5, -10); - hlsl::float32_t4x4 proj = hlsl::math::thin_lens::rhPerspectiveFovMatrix( - core::radians(60.0f), - float(WIN_W / WIN_H), - 0.01f, - 500.0f - ); - m_camera = Camera(cameraPosition, core::vectorSIMDf(0, 0, 0), proj); - } - - 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; - } - - bool updateGUIDescriptorSet() - { - // texture atlas, note we don't create info & write pair for the font sampler because UI extension's is immutable and baked into DS layout - static std::array descriptorInfo; - static IGPUDescriptorSet::SWriteDescriptorSet writes[MaxUITextureCount]; - - descriptorInfo[nbl::ext::imgui::UI::FontAtlasTexId].info.image.imageLayout = IImage::LAYOUT::READ_ONLY_OPTIMAL; - descriptorInfo[nbl::ext::imgui::UI::FontAtlasTexId].desc = smart_refctd_ptr(m_ui.manager->getFontAtlasView()); - - for (uint32_t i = 0; i < descriptorInfo.size(); ++i) - { - writes[i].dstSet = m_ui.descriptorSet.get(); - writes[i].binding = 0u; - writes[i].arrayElement = i; - writes[i].count = 1u; - } - writes[nbl::ext::imgui::UI::FontAtlasTexId].info = descriptorInfo.data() + nbl::ext::imgui::UI::FontAtlasTexId; - - return m_device->updateDescriptorSets(writes, {}); - } - - inline void workLoopBody() override - { - // framesInFlight: ensuring safe execution of command buffers and acquires, `framesInFlight` only affect semaphore waits, don't use this to index your resources because it can change with swapchain recreation. - const uint32_t framesInFlight = core::min(MaxFramesInFlight, m_surface->getMaxAcquiresInFlight()); - // We block for semaphores for 2 reasons here: - // A) Resource: Can't use resource like a command buffer BEFORE previous use is finished! [MaxFramesInFlight] - // B) Acquire: Can't have more acquires in flight than a certain threshold returned by swapchain or your surface helper class. [MaxAcquiresInFlight] - if (m_realFrameIx >= framesInFlight) - { - const ISemaphore::SWaitInfo cbDonePending[] = - { - { - .semaphore = m_semaphore.get(), - .value = m_realFrameIx + 1 - framesInFlight - } - }; - if (m_device->blockForSemaphores(cbDonePending) != ISemaphore::WAIT_RESULT::SUCCESS) - return; - } - const auto resourceIx = m_realFrameIx % MaxFramesInFlight; - - m_api->startCapture(); - - update(); - - auto queue = getGraphicsQueue(); - auto cmdbuf = m_cmdBufs[resourceIx].get(); - - if (!keepRunning()) - return; - - cmdbuf->reset(IGPUCommandBuffer::RESET_FLAGS::RELEASE_RESOURCES_BIT); - 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(); - - //hlsl::float32_t3x4 modelMatrix; - - hlsl::float32_t4x4 modelViewProjectionMatrix = viewProjectionMatrix; - if (m_cachedModelViewProjectionMatrix != modelViewProjectionMatrix) - { - m_frameAccumulationCounter = 0; - m_cachedModelViewProjectionMatrix = modelViewProjectionMatrix; - } - hlsl::float32_t4x4 invModelViewProjectionMatrix = hlsl::inverse(modelViewProjectionMatrix); - - { - IGPUCommandBuffer::SPipelineBarrierDependencyInfo::image_barrier_t imageBarriers[1]; - imageBarriers[0].barrier = { - .dep = { - .srcStageMask = PIPELINE_STAGE_FLAGS::FRAGMENT_SHADER_BIT, // previous frame read from framgent shader - .srcAccessMask = ACCESS_FLAGS::SHADER_READ_BITS, - .dstStageMask = PIPELINE_STAGE_FLAGS::RAY_TRACING_SHADER_BIT, - .dstAccessMask = ACCESS_FLAGS::SHADER_WRITE_BITS - } - }; - imageBarriers[0].image = m_hdrImage.get(); - imageBarriers[0].subresourceRange = { - .aspectMask = IImage::EAF_COLOR_BIT, - .baseMipLevel = 0u, - .levelCount = 1u, - .baseArrayLayer = 0u, - .layerCount = 1u - }; - imageBarriers[0].oldLayout = m_frameAccumulationCounter == 0 ? IImage::LAYOUT::UNDEFINED : IImage::LAYOUT::READ_ONLY_OPTIMAL; - imageBarriers[0].newLayout = IImage::LAYOUT::GENERAL; - cmdbuf->pipelineBarrier(E_DEPENDENCY_FLAGS::EDF_NONE, { .imgBarriers = imageBarriers }); - } - - // Trace Rays Pass - { - SPushConstants pc; - pc.light = m_light; - pc.proceduralGeomInfoBuffer = m_proceduralGeomInfoBuffer->getDeviceAddress(); - pc.triangleGeomInfoBuffer = m_triangleGeomInfoBuffer->getDeviceAddress(); - pc.frameCounter = m_frameAccumulationCounter; - const core::vector3df camPos = m_camera.getPosition().getAsVector3df(); - pc.camPos = { camPos.X, camPos.Y, camPos.Z }; - pc.invMVP = invModelViewProjectionMatrix; - - cmdbuf->bindRayTracingPipeline(m_rayTracingPipeline.get()); - cmdbuf->setRayTracingPipelineStackSize(m_rayTracingStackSize); - cmdbuf->pushConstants(m_rayTracingPipeline->getLayout(), IShader::E_SHADER_STAGE::ESS_ALL_RAY_TRACING, 0, sizeof(SPushConstants), &pc); - cmdbuf->bindDescriptorSets(EPBP_RAY_TRACING, m_rayTracingPipeline->getLayout(), 0, 1, &m_rayTracingDs.get()); - if (m_useIndirectCommand) - cmdbuf->traceRaysIndirect({ .offset = 0,.buffer = m_indirectBuffer }); - else - cmdbuf->traceRays(m_shaderBindingTable, WIN_W, WIN_H, 1); - } - - // pipeline barrier - { - IGPUCommandBuffer::SPipelineBarrierDependencyInfo::image_barrier_t imageBarriers[1]; - imageBarriers[0].barrier = { - .dep = { - .srcStageMask = PIPELINE_STAGE_FLAGS::RAY_TRACING_SHADER_BIT, - .srcAccessMask = ACCESS_FLAGS::SHADER_WRITE_BITS, - .dstStageMask = PIPELINE_STAGE_FLAGS::COLOR_ATTACHMENT_OUTPUT_BIT, - .dstAccessMask = ACCESS_FLAGS::COLOR_ATTACHMENT_WRITE_BIT - } - }; - imageBarriers[0].image = m_hdrImage.get(); - imageBarriers[0].subresourceRange = { - .aspectMask = IImage::EAF_COLOR_BIT, - .baseMipLevel = 0u, - .levelCount = 1u, - .baseArrayLayer = 0u, - .layerCount = 1u - }; - imageBarriers[0].oldLayout = IImage::LAYOUT::GENERAL; - imageBarriers[0].newLayout = IImage::LAYOUT::READ_ONLY_OPTIMAL; - - cmdbuf->pipelineBarrier(E_DEPENDENCY_FLAGS::EDF_NONE, { .imgBarriers = imageBarriers }); - } - - { - asset::SViewport viewport; - { - viewport.minDepth = 1.f; - viewport.maxDepth = 0.f; - viewport.x = 0u; - viewport.y = 0u; - viewport.width = WIN_W; - viewport.height = WIN_H; - } - cmdbuf->setViewport(0u, 1u, &viewport); - - - VkRect2D defaultScisors[] = { {.offset = {(int32_t)viewport.x, (int32_t)viewport.y}, .extent = {(uint32_t)viewport.width, (uint32_t)viewport.height}} }; - cmdbuf->setScissor(defaultScisors); - - auto scRes = static_cast(m_surface->getSwapchainResources()); - const VkRect2D currentRenderArea = - { - .offset = {0,0}, - .extent = {m_window->getWidth(),m_window->getHeight()} - }; - const IGPUCommandBuffer::SClearColorValue clearColor = { .float32 = {0.f,0.f,0.f,1.f} }; - const IGPUCommandBuffer::SRenderpassBeginInfo info = - { - .framebuffer = scRes->getFramebuffer(m_currentImageAcquire.imageIndex), - .colorClearValues = &clearColor, - .depthStencilClearValues = nullptr, - .renderArea = currentRenderArea - }; - nbl::video::ISemaphore::SWaitInfo waitInfo = { .semaphore = m_semaphore.get(), .value = m_realFrameIx + 1u }; - - cmdbuf->beginRenderPass(info, IGPUCommandBuffer::SUBPASS_CONTENTS::INLINE); - - cmdbuf->bindGraphicsPipeline(m_presentPipeline.get()); - cmdbuf->bindDescriptorSets(EPBP_GRAPHICS, m_presentPipeline->getLayout(), 0, 1u, &m_presentDs.get()); - ext::FullScreenTriangle::recordDrawCall(cmdbuf); - - const auto uiParams = m_ui.manager->getCreationParameters(); - auto* uiPipeline = m_ui.manager->getPipeline(); - cmdbuf->bindGraphicsPipeline(uiPipeline); - cmdbuf->bindDescriptorSets(EPBP_GRAPHICS, uiPipeline->getLayout(), uiParams.resources.texturesInfo.setIx, 1u, &m_ui.descriptorSet.get()); - m_ui.manager->render(cmdbuf, waitInfo); - - cmdbuf->endRenderPass(); - - } - - cmdbuf->endDebugMarker(); - cmdbuf->end(); - - { - const IQueue::SSubmitInfo::SSemaphoreInfo rendered[] = - { - { - .semaphore = m_semaphore.get(), - .value = ++m_realFrameIx, - .stageMask = PIPELINE_STAGE_FLAGS::ALL_TRANSFER_BITS - } - }; - { - { - const IQueue::SSubmitInfo::SCommandBufferInfo commandBuffers[] = - { - {.cmdbuf = cmdbuf } - }; - - const IQueue::SSubmitInfo::SSemaphoreInfo acquired[] = - { - { - .semaphore = m_currentImageAcquire.semaphore, - .value = m_currentImageAcquire.acquireCount, - .stageMask = PIPELINE_STAGE_FLAGS::NONE - } - }; - const IQueue::SSubmitInfo infos[] = - { - { - .waitSemaphores = acquired, - .commandBuffers = commandBuffers, - .signalSemaphores = rendered - } - }; - - updateGUIDescriptorSet(); - - if (queue->submit(infos) != IQueue::RESULT::SUCCESS) - m_realFrameIx--; - } - } - - m_window->setCaption("[Nabla Engine] Ray Tracing Pipeline"); - m_surface->present(m_currentImageAcquire.imageIndex, rendered); - } - m_api->endCapture(); - m_frameAccumulationCounter++; - } - - inline void update() - { - m_camera.setMoveSpeed(m_cameraSetting.moveSpeed); - m_camera.setRotateSpeed(m_cameraSetting.rotateSpeed); - - static std::chrono::microseconds previousEventTimestamp{}; - - m_inputSystem->getDefaultMouse(&m_mouse); - m_inputSystem->getDefaultKeyboard(&m_keyboard); - - auto updatePresentationTimestamp = [&]() - { - m_currentImageAcquire = m_surface->acquireNextImage(); - - m_oracle.reportEndFrameRecord(); - const auto timestamp = m_oracle.getNextPresentationTimeStamp(); - m_oracle.reportBeginFrameRecord(); - - return timestamp; - }; - - const auto nextPresentationTimestamp = updatePresentationTimestamp(); - - struct - { - std::vector mouse{}; - std::vector keyboard{}; - } 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 - { - if (e.timeStamp < previousEventTimestamp) - continue; - - previousEventTimestamp = e.timeStamp; - capturedEvents.mouse.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 - { - if (e.timeStamp < previousEventTimestamp) - continue; - - previousEventTimestamp = e.timeStamp; - capturedEvents.keyboard.emplace_back(e); - } - }, m_logger.get()); - - } - m_camera.endInputProcessing(nextPresentationTimestamp); - - 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()); - const auto cursorPosition = m_window->getCursorControl()->getPosition(); - const auto mousePosition = float32_t2(cursorPosition.x, cursorPosition.y) - float32_t2(m_window->getX(), m_window->getY()); - - const ext::imgui::UI::SUpdateParameters params = - { - .mousePosition = mousePosition, - .displaySize = { m_window->getWidth(), m_window->getHeight() }, - .mouseEvents = mouseEvents, - .keyboardEvents = keyboardEvents - }; - - m_ui.manager->update(params); - } - - inline bool keepRunning() override - { - if (m_surface->irrecoverable()) - return false; - - return true; - } - - inline bool onAppTerminated() override - { - return device_base_t::onAppTerminated(); - } - -private: - uint32_t getWorkgroupCount(uint32_t dim, uint32_t size) - { - return (dim + size - 1) / size; - } - - bool createIndirectBuffer() - { - const auto getBufferRangeAddress = [](const SBufferRange& range) - { - return range.buffer->getDeviceAddress() + range.offset; - }; - const auto command = TraceRaysIndirectCommand_t{ - .raygenShaderRecordAddress = getBufferRangeAddress(m_shaderBindingTable.raygen), - .raygenShaderRecordSize = m_shaderBindingTable.raygen.size, - .missShaderBindingTableAddress = getBufferRangeAddress(m_shaderBindingTable.miss.range), - .missShaderBindingTableSize = m_shaderBindingTable.miss.range.size, - .missShaderBindingTableStride = m_shaderBindingTable.miss.stride, - .hitShaderBindingTableAddress = getBufferRangeAddress(m_shaderBindingTable.hit.range), - .hitShaderBindingTableSize = m_shaderBindingTable.hit.range.size, - .hitShaderBindingTableStride = m_shaderBindingTable.hit.stride, - .callableShaderBindingTableAddress = getBufferRangeAddress(m_shaderBindingTable.callable.range), - .callableShaderBindingTableSize = m_shaderBindingTable.callable.range.size, - .callableShaderBindingTableStride = m_shaderBindingTable.callable.stride, - .width = WIN_W, - .height = WIN_H, - .depth = 1, - }; - IGPUBuffer::SCreationParams params; - params.usage = IGPUBuffer::EUF_TRANSFER_DST_BIT | IGPUBuffer::EUF_INDIRECT_BUFFER_BIT | IGPUBuffer::EUF_SHADER_DEVICE_ADDRESS_BIT; - params.size = sizeof(TraceRaysIndirectCommand_t); - m_utils->createFilledDeviceLocalBufferOnDedMem(SIntendedSubmitInfo{ .queue = getGraphicsQueue() }, std::move(params), &command).move_into(m_indirectBuffer); - return true; - } - - void calculateRayTracingStackSize(const smart_refctd_ptr& pipeline) - { - const auto raygenStackSize = pipeline->getRaygenStackSize(); - auto getMaxSize = [&](auto ranges, auto valProj) -> uint16_t - { - auto maxValue = 0; - for (const auto& val : ranges) - { - maxValue = std::max(maxValue, std::invoke(valProj, val)); - } - return maxValue; - }; - - const auto closestHitStackMax = getMaxSize(pipeline->getHitStackSizes(), &IGPURayTracingPipeline::SHitGroupStackSize::closestHit); - const auto anyHitStackMax = getMaxSize(pipeline->getHitStackSizes(), &IGPURayTracingPipeline::SHitGroupStackSize::anyHit); - const auto intersectionStackMax = getMaxSize(pipeline->getHitStackSizes(), &IGPURayTracingPipeline::SHitGroupStackSize::intersection); - const auto missStackMax = getMaxSize(pipeline->getMissStackSizes(), std::identity{}); - const auto callableStackMax = getMaxSize(pipeline->getCallableStackSizes(), std::identity{}); - auto firstDepthStackSizeMax = std::max(closestHitStackMax, missStackMax); - firstDepthStackSizeMax = std::max(firstDepthStackSizeMax, intersectionStackMax + anyHitStackMax); - m_rayTracingStackSize = raygenStackSize + std::max(firstDepthStackSizeMax, callableStackMax); - } - - bool createShaderBindingTable(const smart_refctd_ptr& pipeline) - { - const auto& limits = m_device->getPhysicalDevice()->getLimits(); - const auto handleSize = SPhysicalDeviceLimits::ShaderGroupHandleSize; - const auto handleSizeAligned = nbl::core::alignUp(handleSize, limits.shaderGroupHandleAlignment); - - auto& raygenRange = m_shaderBindingTable.raygen; - - auto& hitRange = m_shaderBindingTable.hit.range; - const auto hitHandles = pipeline->getHitHandles(); - - auto& missRange = m_shaderBindingTable.miss.range; - const auto missHandles = pipeline->getMissHandles(); - - auto& callableRange = m_shaderBindingTable.callable.range; - const auto callableHandles = pipeline->getCallableHandles(); - - raygenRange = { - .offset = 0, - .size = core::alignUp(handleSizeAligned, limits.shaderGroupBaseAlignment) - }; - - missRange = { - .offset = raygenRange.size, - .size = core::alignUp(missHandles.size() * handleSizeAligned, limits.shaderGroupBaseAlignment), - }; - m_shaderBindingTable.miss.stride = handleSizeAligned; - - hitRange = { - .offset = missRange.offset + missRange.size, - .size = core::alignUp(hitHandles.size() * handleSizeAligned, limits.shaderGroupBaseAlignment), - }; - m_shaderBindingTable.hit.stride = handleSizeAligned; - - callableRange = { - .offset = hitRange.offset + hitRange.size, - .size = core::alignUp(callableHandles.size() * handleSizeAligned, limits.shaderGroupBaseAlignment), - }; - m_shaderBindingTable.callable.stride = handleSizeAligned; - - const auto bufferSize = raygenRange.size + missRange.size + hitRange.size + callableRange.size; - - ICPUBuffer::SCreationParams cpuBufferParams; - cpuBufferParams.size = bufferSize; - auto cpuBuffer = ICPUBuffer::create(std::move(cpuBufferParams)); - uint8_t* pData = reinterpret_cast(cpuBuffer->getPointer()); - - // copy raygen region - memcpy(pData, &pipeline->getRaygen(), handleSize); - - // copy miss region - uint8_t* pMissData = pData + missRange.offset; - for (const auto& handle : missHandles) - { - memcpy(pMissData, &handle, handleSize); - pMissData += m_shaderBindingTable.miss.stride; - } - - // copy hit region - uint8_t* pHitData = pData + hitRange.offset; - for (const auto& handle : hitHandles) - { - memcpy(pHitData, &handle, handleSize); - pHitData += m_shaderBindingTable.miss.stride; - } - - // copy callable region - uint8_t* pCallableData = pData + callableRange.offset; - for (const auto& handle : callableHandles) - { - memcpy(pCallableData, &handle, handleSize); - pCallableData += m_shaderBindingTable.callable.stride; - } - - { - smart_refctd_ptr buffer; - { - IGPUBuffer::SCreationParams params; - params.usage = IGPUBuffer::EUF_TRANSFER_DST_BIT | IGPUBuffer::EUF_INLINE_UPDATE_VIA_CMDBUF | IGPUBuffer::EUF_SHADER_DEVICE_ADDRESS_BIT | IGPUBuffer::EUF_SHADER_BINDING_TABLE_BIT; - params.size = bufferSize; - m_utils->createFilledDeviceLocalBufferOnDedMem(SIntendedSubmitInfo{ .queue = getGraphicsQueue() }, std::move(params), pData).move_into(buffer); - } - raygenRange.buffer = smart_refctd_ptr(buffer); - missRange.buffer = smart_refctd_ptr(raygenRange.buffer); - hitRange.buffer = smart_refctd_ptr(raygenRange.buffer); - callableRange.buffer = smart_refctd_ptr(raygenRange.buffer); - } - - return true; - } - - bool createAccelerationStructuresFromGeometry() - { - auto queue = getGraphicsQueue(); - // get geometries into ICPUBuffers - auto pool = m_device->createCommandPool(queue->getFamilyIndex(), IGPUCommandPool::CREATE_FLAGS::RESET_COMMAND_BUFFER_BIT); - if (!pool) - return logFail("Couldn't create Command Pool for geometry creation!"); - - const auto defaultMaterial = Material{ - .ambient = {0.2, 0.1, 0.1}, - .diffuse = {0.8, 0.3, 0.3}, - .specular = {0.8, 0.8, 0.8}, - .shininess = 1.0f, - .alpha = 1.0f, - }; - - auto getTranslationMatrix = [](float32_t x, float32_t y, float32_t z) - { - hlsl::float32_t3x4 transform = hlsl::math::linalg::identity(); - hlsl::math::linalg::setTranslation(transform, float32_t3(x, y, z)); - - return transform; - }; - - const auto planeRotation = hlsl::math::quaternion::create(hlsl::float32_t3(1.f, 0.f, 0.f), core::radians(-90.0f)); - hlsl::float32_t3x4 planeTransform = hlsl::math::linalg::promote_affine<3, 4, 3, 3>(hlsl::_static_cast(planeRotation)); - - // triangles geometries - auto geometryCreator = make_smart_refctd_ptr(); - - const auto cpuObjects = std::array{ - scene::ReferenceObjectCpu { - .data = geometryCreator->createRectangle({10, 10}), - .material = defaultMaterial, - .transform = planeTransform, - }, - scene::ReferenceObjectCpu { - .data = geometryCreator->createCube({1, 1, 1}), - .material = defaultMaterial, - .transform = getTranslationMatrix(0, 0.5f, 0), - }, - scene::ReferenceObjectCpu { - .data = geometryCreator->createCube({1.5, 1.5, 1.5}), - .material = Material{ - .ambient = {0.1, 0.1, 0.2}, - .diffuse = {0.2, 0.2, 0.8}, - .specular = {0.8, 0.8, 0.8}, - .shininess = 1.0f, - .alpha = 1.0f, - }, - .transform = getTranslationMatrix(-5.0f, 1.0f, 0), - }, - scene::ReferenceObjectCpu { - .data = geometryCreator->createCube({1.5, 1.5, 1.5}), - .material = Material{ - .ambient = {0.1, 0.2, 0.1}, - .diffuse = {0.2, 0.8, 0.2}, - .specular = {0.8, 0.8, 0.8}, - .shininess = 1.0f, - .alpha = 0.2, - }, - .transform = getTranslationMatrix(5.0f, 1.0f, 0), - }, - }; - - // procedural geometries - using Aabb = IGPUBottomLevelAccelerationStructure::AABB_t; - - smart_refctd_ptr cpuProcBuffer; - { - ICPUBuffer::SCreationParams params; - params.size = NumberOfProceduralGeometries * sizeof(Aabb); - cpuProcBuffer = ICPUBuffer::create(std::move(params)); - } - - core::vector proceduralGeoms; - proceduralGeoms.reserve(NumberOfProceduralGeometries); - auto proceduralGeometries = reinterpret_cast(cpuProcBuffer->getPointer()); - for (int32_t i = 0; i < NumberOfProceduralGeometries; i++) - { - const auto middle_i = NumberOfProceduralGeometries / 2.0; - SProceduralGeomInfo sphere = { - .material = hlsl::_static_cast(Material{ - .ambient = {0.1, 0.05 * i, 0.1}, - .diffuse = {0.3, 0.2 * i, 0.3}, - .specular = {0.8, 0.8, 0.8}, - .shininess = 1.0f, - }), - .center = float32_t3((i - middle_i) * 4.0, 2, 5.0), - .radius = 1, - }; - - proceduralGeoms.push_back(sphere); - const auto sphereMin = sphere.center - sphere.radius; - const auto sphereMax = sphere.center + sphere.radius; - proceduralGeometries[i] = { - vector3d(sphereMin.x, sphereMin.y, sphereMin.z), - vector3d(sphereMax.x, sphereMax.y, sphereMax.z) - }; - } - - { - IGPUBuffer::SCreationParams params; - params.usage = IGPUBuffer::EUF_STORAGE_BUFFER_BIT | IGPUBuffer::EUF_TRANSFER_DST_BIT | IGPUBuffer::EUF_INLINE_UPDATE_VIA_CMDBUF | IGPUBuffer::EUF_SHADER_DEVICE_ADDRESS_BIT; - params.size = proceduralGeoms.size() * sizeof(SProceduralGeomInfo); - m_utils->createFilledDeviceLocalBufferOnDedMem(SIntendedSubmitInfo{ .queue = queue }, std::move(params), proceduralGeoms.data()).move_into(m_proceduralGeomInfoBuffer); - } - - // get ICPUBuffers into ICPUBLAS - // TODO use one BLAS and multiple triangles/aabbs in one - const auto blasCount = std::size(cpuObjects) + 1; - const auto proceduralBlasIdx = std::size(cpuObjects); - - std::array, std::size(cpuObjects) + 1u> cpuBlasList; - for (uint32_t i = 0; i < blasCount; i++) - { - auto& blas = cpuBlasList[i]; - blas = make_smart_refctd_ptr(); - - if (i == proceduralBlasIdx) - { - auto aabbs = make_refctd_dynamic_array>>(1u); - auto primitiveCounts = make_refctd_dynamic_array>(1u); - - auto& aabb = aabbs->front(); - auto& primCount = primitiveCounts->front(); - - primCount = NumberOfProceduralGeometries; - aabb.data = { .offset = 0, .buffer = cpuProcBuffer }; - aabb.stride = sizeof(IGPUBottomLevelAccelerationStructure::AABB_t); - aabb.geometryFlags = IGPUBottomLevelAccelerationStructure::GEOMETRY_FLAGS::OPAQUE_BIT; // only allow opaque for now - - blas->setGeometries(std::move(aabbs), std::move(primitiveCounts)); - } - else - { - auto triangles = make_refctd_dynamic_array>>(1u); - auto primitiveCounts = make_refctd_dynamic_array>(1u); - - auto& tri = triangles->front(); - - auto& primCount = primitiveCounts->front(); - primCount = cpuObjects[i].data->getPrimitiveCount(); - - tri = cpuObjects[i].data->exportForBLAS(); - tri.geometryFlags = cpuObjects[i].material.isTransparent() ? - IGPUBottomLevelAccelerationStructure::GEOMETRY_FLAGS::NO_DUPLICATE_ANY_HIT_INVOCATION_BIT : - IGPUBottomLevelAccelerationStructure::GEOMETRY_FLAGS::OPAQUE_BIT; - - blas->setGeometries(std::move(triangles), std::move(primitiveCounts)); - } - - auto blasFlags = bitflag(IGPUBottomLevelAccelerationStructure::BUILD_FLAGS::PREFER_FAST_TRACE_BIT) | IGPUBottomLevelAccelerationStructure::BUILD_FLAGS::ALLOW_COMPACTION_BIT; - if (i == proceduralBlasIdx) - blasFlags |= IGPUBottomLevelAccelerationStructure::BUILD_FLAGS::GEOMETRY_TYPE_IS_AABB_BIT; - - blas->setBuildFlags(blasFlags); - blas->setContentHash(blas->computeContentHash()); - } - - auto geomInfoBuffer = ICPUBuffer::create({ std::size(cpuObjects) * sizeof(STriangleGeomInfo) }); - STriangleGeomInfo* geomInfos = reinterpret_cast(geomInfoBuffer->getPointer()); - - // get ICPUBLAS into ICPUTLAS - auto geomInstances = make_refctd_dynamic_array>(blasCount); - { - uint32_t i = 0; - for (auto instance = geomInstances->begin(); instance != geomInstances->end(); instance++, i++) - { - const auto isProceduralInstance = i == proceduralBlasIdx; - ICPUTopLevelAccelerationStructure::StaticInstance inst; - inst.base.blas = cpuBlasList[i]; - inst.base.flags = static_cast(IGPUTopLevelAccelerationStructure::INSTANCE_FLAGS::TRIANGLE_FACING_CULL_DISABLE_BIT); - inst.base.instanceCustomIndex = i; - inst.base.instanceShaderBindingTableRecordOffset = isProceduralInstance ? 2 : 0; - inst.base.mask = 0xFF; - inst.transform = isProceduralInstance ? hlsl::float32_t3x4() : cpuObjects[i].transform; - - instance->instance = inst; - } - } - - auto cpuTlas = make_smart_refctd_ptr(); - cpuTlas->setInstances(std::move(geomInstances)); - cpuTlas->setBuildFlags(IGPUTopLevelAccelerationStructure::BUILD_FLAGS::PREFER_FAST_TRACE_BIT); - - // convert with asset converter - smart_refctd_ptr converter = CAssetConverter::create({ .device = m_device.get(), .optimizer = {} }); - struct MyInputs : CAssetConverter::SInputs - { - // For the GPU Buffers to be directly writeable and so that we don't need a Transfer Queue submit at all - inline uint32_t constrainMemoryTypeBits(const size_t groupCopyID, const IAsset* canonicalAsset, const blake3_hash_t& contentHash, const IDeviceMemoryBacked* memoryBacked) const override - { - assert(memoryBacked); - return memoryBacked->getObjectType() != IDeviceMemoryBacked::EOT_BUFFER ? (~0u) : rebarMemoryTypes; - } - - uint32_t rebarMemoryTypes; - } inputs = {}; - inputs.logger = m_logger.get(); - inputs.rebarMemoryTypes = m_physicalDevice->getDirectVRAMAccessMemoryTypeBits(); - // the allocator needs to be overriden to hand out memory ranges which have already been mapped so that the ReBAR fast-path can kick in - // (multiple buffers can be bound to same memory, but memory can only be mapped once at one place, so Asset Converter can't do it) - struct MyAllocator final : public IDeviceMemoryAllocator - { - ILogicalDevice* getDeviceForAllocations() const override { return device; } - - SAllocation allocate(const SAllocateInfo& info) override - { - auto retval = device->allocate(info); - // map what is mappable by default so ReBAR checks succeed - if (retval.isValid() && retval.memory->isMappable()) - retval.memory->map({ .offset = 0,.length = info.size }); - return retval; - } - - ILogicalDevice* device; - } myalloc; - myalloc.device = m_device.get(); - inputs.allocator = &myalloc; - - std::array tmpTlas; - std::array tmpBuffers; - std::array tmpGeometries; - std::array, std::size(cpuObjects)> tmpGeometryPatches; - { - tmpTlas[0] = cpuTlas.get(); - tmpBuffers[0] = cpuProcBuffer.get(); - for (uint32_t i = 0; i < cpuObjects.size(); i++) - { - tmpGeometries[i] = cpuObjects[i].data.get(); - tmpGeometryPatches[i].indexBufferUsages = IGPUBuffer::E_USAGE_FLAGS::EUF_SHADER_DEVICE_ADDRESS_BIT; - } - - std::get>(inputs.assets) = tmpTlas; - std::get>(inputs.assets) = tmpBuffers; - std::get>(inputs.assets) = tmpGeometries; - std::get>(inputs.patches) = tmpGeometryPatches; - } - - auto reservation = converter->reserve(inputs); - { - auto prepass = [&](const auto& references) -> bool - { - auto objects = reservation.getGPUObjects(); - uint32_t counter = {}; - for (auto& object : objects) - { - auto gpu = object.value; - auto* reference = references[counter]; - - if (reference) - { - if (!gpu) - { - m_logger->log("Failed to convert a CPU object to GPU!", ILogger::ELL_ERROR); - return false; - } - } - counter++; - } - return true; - }; - - prepass.template operator() < ICPUTopLevelAccelerationStructure > (tmpTlas); - prepass.template operator() < ICPUBuffer > (tmpBuffers); - prepass.template operator() < ICPUPolygonGeometry > (tmpGeometries); - } - - constexpr auto CompBufferCount = 2; - std::array, CompBufferCount> compBufs = {}; - std::array compBufInfos = {}; - { - auto pool = m_device->createCommandPool(queue->getFamilyIndex(), IGPUCommandPool::CREATE_FLAGS::RESET_COMMAND_BUFFER_BIT | IGPUCommandPool::CREATE_FLAGS::TRANSIENT_BIT); - pool->createCommandBuffers(IGPUCommandPool::BUFFER_LEVEL::PRIMARY, compBufs); - compBufs.front()->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT); - for (auto i = 0; i < CompBufferCount; i++) - compBufInfos[i].cmdbuf = compBufs[i].get(); - } - auto compSema = m_device->createSemaphore(0u); - SIntendedSubmitInfo compute = {}; - compute.queue = queue; - compute.scratchCommandBuffers = compBufInfos; - compute.scratchSemaphore = { - .semaphore = compSema.get(), - .value = 0u, - .stageMask = PIPELINE_STAGE_FLAGS::ACCELERATION_STRUCTURE_BUILD_BIT | PIPELINE_STAGE_FLAGS::ACCELERATION_STRUCTURE_COPY_BIT - }; - // convert - { - smart_refctd_ptr scratchAlloc; - { - constexpr auto MaxAlignment = 256; - constexpr auto MinAllocationSize = 1024; - const auto scratchSize = core::alignUp(reservation.getMaxASBuildScratchSize(false), MaxAlignment); - - - IGPUBuffer::SCreationParams creationParams = {}; - creationParams.size = scratchSize; - creationParams.usage = IGPUBuffer::EUF_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT | IGPUBuffer::EUF_SHADER_DEVICE_ADDRESS_BIT | IGPUBuffer::EUF_STORAGE_BUFFER_BIT; - auto scratchBuffer = m_device->createBuffer(std::move(creationParams)); - - auto reqs = scratchBuffer->getMemoryReqs(); - reqs.memoryTypeBits &= m_physicalDevice->getDirectVRAMAccessMemoryTypeBits(); - - auto allocation = m_device->allocate(reqs, scratchBuffer.get(), IDeviceMemoryAllocation::EMAF_DEVICE_ADDRESS_BIT); - allocation.memory->map({ .offset = 0,.length = reqs.size }); - - scratchAlloc = make_smart_refctd_ptr( - SBufferRange{0ull, scratchSize, std::move(scratchBuffer)}, - core::allocator(), MaxAlignment, MinAllocationSize - ); - } - - struct MyParams final : CAssetConverter::SConvertParams - { - inline uint32_t getFinalOwnerQueueFamily(const IGPUBuffer* buffer, const core::blake3_hash_t& createdFrom) override - { - return finalUser; - } - inline uint32_t getFinalOwnerQueueFamily(const IGPUAccelerationStructure* image, const core::blake3_hash_t& createdFrom) override - { - return finalUser; - } - - uint8_t finalUser; - } params = {}; - params.utilities = m_utils.get(); - params.compute = &compute; - params.scratchForDeviceASBuild = scratchAlloc.get(); - params.finalUser = queue->getFamilyIndex(); - - auto future = reservation.convert(params); - if (future.copy() != IQueue::RESULT::SUCCESS) - { - m_logger->log("Failed to await submission future!", ILogger::ELL_ERROR); - return false; - } - // 2 submits, BLAS build, TLAS build, DO NOT ADD COMPACTIONS IN THIS EXAMPLE! - if (compute.getFutureScratchSemaphore().value > 3) - m_logger->log("Overflow submitted on Compute Queue despite using ReBAR (no transfer submits or usage of staging buffer) and providing a AS Build Scratch Buffer of correctly queried max size!", system::ILogger::ELL_ERROR); - - // assign gpu objects to output - auto&& tlases = reservation.getGPUObjects(); - m_gpuTlas = tlases[0].value; - auto&& buffers = reservation.getGPUObjects(); - m_proceduralAabbBuffer = buffers[0].value; - - auto&& gpuPolygonGeometries = reservation.getGPUObjects(); - m_gpuPolygons.resize(gpuPolygonGeometries.size()); - - for (uint32_t i = 0; i < gpuPolygonGeometries.size(); i++) - { - const auto& cpuObject = cpuObjects[i]; - const auto& gpuPolygon = gpuPolygonGeometries[i].value; - const auto gpuTriangles = gpuPolygon->exportForBLAS(); - - const auto& vertexBufferBinding = gpuTriangles.vertexData[0]; - const uint64_t vertexBufferAddress = vertexBufferBinding.buffer->getDeviceAddress() + vertexBufferBinding.offset; - - const auto& normalView = gpuPolygon->getNormalView(); - const uint64_t normalBufferAddress = normalView ? normalView.src.buffer->getDeviceAddress() + normalView.src.offset : 0; - auto normalType = NT_R32G32B32_SFLOAT; - if (normalView && normalView.composed.format == EF_R8G8B8A8_SNORM) - normalType = NT_R8G8B8A8_SNORM; - - const auto& indexBufferBinding = gpuTriangles.indexData; - auto& geomInfo = geomInfos[i]; - geomInfo = { - .material = hlsl::_static_cast(cpuObject.material), - .vertexBufferAddress = vertexBufferAddress, - .indexBufferAddress = indexBufferBinding.buffer ? indexBufferBinding.buffer->getDeviceAddress() + indexBufferBinding.offset : vertexBufferAddress, - .normalBufferAddress = normalBufferAddress, - .normalType = normalType, - .indexType = gpuTriangles.indexType, - }; - - m_gpuPolygons[i] = gpuPolygon; - } - } - - { - IGPUBuffer::SCreationParams params; - params.usage = IGPUBuffer::EUF_STORAGE_BUFFER_BIT | IGPUBuffer::EUF_TRANSFER_DST_BIT | IGPUBuffer::EUF_INLINE_UPDATE_VIA_CMDBUF | IGPUBuffer::EUF_SHADER_DEVICE_ADDRESS_BIT; - params.size = geomInfoBuffer->getSize(); - m_utils->createFilledDeviceLocalBufferOnDedMem(SIntendedSubmitInfo{ .queue = queue }, std::move(params), geomInfos).move_into(m_triangleGeomInfoBuffer); - } - - return true; - } - - smart_refctd_ptr m_window; - smart_refctd_ptr> m_surface; - smart_refctd_ptr m_semaphore; - uint64_t m_realFrameIx = 0; - uint32_t m_frameAccumulationCounter = 0; - std::array, MaxFramesInFlight> m_cmdBufs; - ISimpleManagedSurface::SAcquireResult m_currentImageAcquire = {}; - - core::smart_refctd_ptr m_inputSystem; - InputSystem::ChannelReader m_mouse; - InputSystem::ChannelReader m_keyboard; - - struct CameraSetting - { - float fov = 60.f; - float zNear = 0.1f; - float zFar = 10000.f; - float moveSpeed = 1.f; - float rotateSpeed = 1.f; - float viewWidth = 10.f; - float camYAngle = 165.f / 180.f * 3.14159f; - 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()); - - Light m_light = { - .direction = {-1.0f, -1.0f, -0.4f}, - .position = {10.0f, 15.0f, 8.0f}, - .outerCutoff = 0.866025404f, // {cos(radians(30.0f))}, - .type = ELT_DIRECTIONAL - }; - - video::CDumbPresentationOracle m_oracle; - - struct C_UI - { - nbl::core::smart_refctd_ptr manager; - - struct - { - core::smart_refctd_ptr gui, scene; - } samplers; - - core::smart_refctd_ptr descriptorSet; - } m_ui; - core::smart_refctd_ptr m_guiDescriptorSetPool; - - core::vector m_gpuIntersectionSpheres; - uint32_t m_intersectionHitGroupIdx; - - core::vector> m_gpuPolygons; - smart_refctd_ptr m_gpuTlas; - smart_refctd_ptr m_instanceBuffer; - - smart_refctd_ptr m_triangleGeomInfoBuffer; - smart_refctd_ptr m_proceduralGeomInfoBuffer; - smart_refctd_ptr m_proceduralAabbBuffer; - smart_refctd_ptr m_indirectBuffer; - - smart_refctd_ptr m_hdrImage; - smart_refctd_ptr m_hdrImageView; - - smart_refctd_ptr m_rayTracingDsPool; - smart_refctd_ptr m_rayTracingDs; - smart_refctd_ptr m_rayTracingPipeline; - uint64_t m_rayTracingStackSize; - IGPURayTracingPipeline::SShaderBindingTable m_shaderBindingTable; - - smart_refctd_ptr m_presentDs; - smart_refctd_ptr m_presentDsPool; - smart_refctd_ptr m_presentPipeline; - - smart_refctd_ptr m_converter; - - - hlsl::float32_t4x4 m_cachedModelViewProjectionMatrix; - bool m_useIndirectCommand = false; - -}; -NBL_MAIN_FUNC(RaytracingPipelineApp) diff --git a/72_CooperativeBinarySearch/CMakeLists.txt b/72_CooperativeBinarySearch/CMakeLists.txt deleted file mode 100644 index b7e52875d..000000000 --- a/72_CooperativeBinarySearch/CMakeLists.txt +++ /dev/null @@ -1,24 +0,0 @@ -include(common RESULT_VARIABLE RES) -if(NOT RES) - message(FATAL_ERROR "common.cmake not found. Should be in {repo_root}/cmake directory") -endif() - -nbl_create_executable_project("" "" "" "" "${NBL_EXECUTABLE_PROJECT_CREATION_PCH_TARGET}") - -if(NBL_EMBED_BUILTIN_RESOURCES) - set(_BR_TARGET_ ${EXECUTABLE_NAME}_builtinResourceData) - set(RESOURCE_DIR "app_resources") - - get_filename_component(_SEARCH_DIRECTORIES_ "${CMAKE_CURRENT_SOURCE_DIR}" ABSOLUTE) - get_filename_component(_OUTPUT_DIRECTORY_SOURCE_ "${CMAKE_CURRENT_BINARY_DIR}/src" ABSOLUTE) - get_filename_component(_OUTPUT_DIRECTORY_HEADER_ "${CMAKE_CURRENT_BINARY_DIR}/include" ABSOLUTE) - - file(GLOB_RECURSE BUILTIN_RESOURCE_FILES RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}/${RESOURCE_DIR}" CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/${RESOURCE_DIR}/*") - foreach(RES_FILE ${BUILTIN_RESOURCE_FILES}) - LIST_BUILTIN_RESOURCE(RESOURCES_TO_EMBED "${RES_FILE}") - endforeach() - - ADD_CUSTOM_BUILTIN_RESOURCES(${_BR_TARGET_} RESOURCES_TO_EMBED "${_SEARCH_DIRECTORIES_}" "${RESOURCE_DIR}" "nbl::this_example::builtin" "${_OUTPUT_DIRECTORY_HEADER_}" "${_OUTPUT_DIRECTORY_SOURCE_}") - - LINK_BUILTIN_RESOURCES_TO_TARGET(${EXECUTABLE_NAME} ${_BR_TARGET_}) -endif() \ No newline at end of file diff --git a/72_CooperativeBinarySearch/app_resources/binarySearch.comp.hlsl b/72_CooperativeBinarySearch/app_resources/binarySearch.comp.hlsl deleted file mode 100644 index 0834e8f91..000000000 --- a/72_CooperativeBinarySearch/app_resources/binarySearch.comp.hlsl +++ /dev/null @@ -1,120 +0,0 @@ -// Copyright (C) 2024-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 -#pragma wave shader_stage(compute) - -#include "common.h" - -#include "nbl/builtin/hlsl/glsl_compat/subgroup_ballot.hlsl" - -using namespace nbl::hlsl; - -[[vk::push_constant]] PushConstants Constants; -[[vk::binding(0)]] StructuredBuffer Histogram; -[[vk::binding(1)]] RWStructuredBuffer Output; - - -uint getNextPowerOfTwo(uint number) { - return 2 << firstbithigh(number - 1); -} - -uint getLaneWithFirstBitSet(bool condition) { - uint4 ballot = WaveActiveBallot(condition); - if (all(ballot == 0)) { - return WaveGetLaneCount(); - } - return nbl::hlsl::glsl::subgroupBallotFindLSB(ballot); -} - -// findValue must be the same across the entire wave -// Could use something like WaveReadFirstLane to be fully sure -uint binarySearchLowerBoundFindValue(uint findValue, StructuredBuffer searchBuffer, uint searchBufferSize) { - uint lane = WaveGetLaneIndex(); - - uint left = 0; - uint right = searchBufferSize - 1; - - uint32_t range = getNextPowerOfTwo(right - left); - // do pivots as long as we can't coalesced load - while (range > WaveGetLaneCount()) - { - // there must be at least 1 gap between subsequent pivots - const uint32_t step = range / WaveGetLaneCount(); - const uint32_t halfStep = step >> 1; - const uint32_t pivotOffset = lane * step+halfStep; - const uint32_t pivotIndex = left + pivotOffset; - - uint4 notGreaterPivots = WaveActiveBallot(pivotIndex < right && !(findValue < searchBuffer[pivotIndex])); - uint partition = nbl::hlsl::glsl::subgroupBallotBitCount(notGreaterPivots); - // only move left if needed - if (partition != 0) - left += partition * step - halfStep; - // if we go into final half partition, the range becomes less too - range = partition != WaveGetLaneCount() ? step : halfStep; - } - - uint threadSearchIndex = left + lane; - bool laneValid = threadSearchIndex < searchBufferSize; - uint histAtIndex = laneValid ? searchBuffer[threadSearchIndex] : -1; - uint firstLaneGreaterThan = getLaneWithFirstBitSet(histAtIndex > findValue); - - return left + firstLaneGreaterThan - 1; -} - -static const uint32_t GroupsharedSize = WorkgroupSize; -groupshared uint shared_groupSearchBufferMinIndex; -groupshared uint shared_groupSearchBufferMaxIndex; -groupshared uint shared_groupSearchValues[WorkgroupSize]; - -// Binary search using the entire workgroup, making it log32 or log64 (every iteration, the possible set of -// values is divided by the number of lanes in a wave) -uint binarySearchLowerBoundCooperative(uint groupIndex, uint groupThread, StructuredBuffer searchBuffer, uint searchBufferSize) { - uint minSearchValue = groupIndex.x * GroupsharedSize; - uint maxSearchValue = ((groupIndex.x + 1) * GroupsharedSize) - 1; - - // On each workgroup, two subgroups do the search - // - One searches for the minimum, the other searches for the maximum - // - Store the minimum and maximum on groupshared memory, then do a barrier - uint wave = groupThread / WaveGetLaneCount(); - if (wave < 2) { - uint search = wave == 0 ? minSearchValue : maxSearchValue; - uint searchResult = binarySearchLowerBoundFindValue(search, searchBuffer, searchBufferSize); - if (WaveIsFirstLane()) { - if (wave == 0) shared_groupSearchBufferMinIndex = searchResult; - else shared_groupSearchBufferMaxIndex = searchResult; - } - } - GroupMemoryBarrierWithGroupSync(); - - // Since every instance has at least one triangle, we know that having workgroup values - // for each value in the range of minimum to maximum will suffice. - - // Write every value in the range to groupshared memory and barrier. - uint idx = shared_groupSearchBufferMinIndex + groupThread.x; - if (idx <= shared_groupSearchBufferMaxIndex) { - shared_groupSearchValues[groupThread.x] = searchBuffer[idx]; - } - GroupMemoryBarrierWithGroupSync(); - - uint maxValueIndex = shared_groupSearchBufferMaxIndex - shared_groupSearchBufferMinIndex; - - uint searchValue = minSearchValue + groupThread; - uint currentSearchValueIndex = 0; - uint laneValue = shared_groupSearchBufferMaxIndex; - while (currentSearchValueIndex <= maxValueIndex) { - uint curValue = shared_groupSearchValues[currentSearchValueIndex]; - if (curValue > searchValue) { - laneValue = shared_groupSearchBufferMinIndex + currentSearchValueIndex - 1; - break; - } - currentSearchValueIndex ++; - } - - return laneValue; -} - -[numthreads(WorkgroupSize,1,1)] -void main(const uint3 thread : SV_DispatchThreadID, const uint3 groupThread : SV_GroupThreadID, const uint3 group : SV_GroupID) -{ - Output[thread.x] = binarySearchLowerBoundCooperative(group.x, groupThread.x, Histogram, Constants.EntityCount); -} \ No newline at end of file diff --git a/72_CooperativeBinarySearch/app_resources/common.h b/72_CooperativeBinarySearch/app_resources/common.h deleted file mode 100644 index 65f606b08..000000000 --- a/72_CooperativeBinarySearch/app_resources/common.h +++ /dev/null @@ -1,15 +0,0 @@ -#ifndef _COOPERATIVE_BINARY_SEARCH_H_INCLUDED_ -#define _COOPERATIVE_BINARY_SEARCH_H_INCLUDED_ - -#include -#include - -// TODO: NBL_CONSTEXPR_NSPC_VAR -static const uint32_t WorkgroupSize = 256; - -struct PushConstants -{ - uint32_t EntityCount; -}; - -#endif // _COOPERATIVE_BINARY_SEARCH_H_INCLUDED_ diff --git a/72_CooperativeBinarySearch/include/nbl/this_example/common.hpp b/72_CooperativeBinarySearch/include/nbl/this_example/common.hpp deleted file mode 100644 index 3745ca512..000000000 --- a/72_CooperativeBinarySearch/include/nbl/this_example/common.hpp +++ /dev/null @@ -1,11 +0,0 @@ -#ifndef _NBL_THIS_EXAMPLE_COMMON_H_INCLUDED_ -#define _NBL_THIS_EXAMPLE_COMMON_H_INCLUDED_ - -#include "nbl/examples/examples.hpp" - -// example's own headers -#include "nbl/ui/ICursorControl.h" // TODO: why not in nabla.h ? -#include "nbl/ext/ImGui/ImGui.h" -#include "imgui/imgui_internal.h" - -#endif // _NBL_THIS_EXAMPLE_COMMON_H_INCLUDED_ \ No newline at end of file diff --git a/72_CooperativeBinarySearch/main.cpp b/72_CooperativeBinarySearch/main.cpp deleted file mode 100644 index aef50f68c..000000000 --- a/72_CooperativeBinarySearch/main.cpp +++ /dev/null @@ -1,284 +0,0 @@ -// Copyright (C) 2018-2024 - 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 "nbl/examples/examples.hpp" -#include "nbl/system/IApplicationFramework.h" -#include "app_resources/common.h" - -#include -#include -#include - - -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; - -// -constexpr uint32_t TestCaseIndices[] = { -#include "testCaseData.h" -}; -constexpr uint32_t numIndices = sizeof(TestCaseIndices) / sizeof(TestCaseIndices[0]); -constexpr uint32_t lastValue = TestCaseIndices[numIndices - 1]; -// just some extra stuff over the edge -constexpr uint32_t totalValues = lastValue + 100; - - -void cpu_tests(); - -class CooperativeBinarySearch final : public application_templates::MonoDeviceApplication, public BuiltinResourcesApplication -{ - using device_base_t = application_templates::MonoDeviceApplication; - using asset_base_t = BuiltinResourcesApplication; -public: - CooperativeBinarySearch(const path& _localInputCWD, const path& _localOutputCWD, const path& _sharedInputCWD, const path& _sharedOutputCWD) : - IApplicationFramework(_localInputCWD, _localOutputCWD, _sharedInputCWD, _sharedOutputCWD) {} - - virtual SPhysicalDeviceFeatures getPreferredDeviceFeatures() const override - { - auto retval = device_base_t::getPreferredDeviceFeatures(); - retval.pipelineExecutableInfo = true; - return retval; - } - - bool onAppInitialized(smart_refctd_ptr&& system) override - { - // Remember to call the base class initialization! - if (!device_base_t::onAppInitialized(smart_refctd_ptr(system))) - return false; - if (!asset_base_t::onAppInitialized(std::move(system))) - return false; - - m_queue = m_device->getQueue(0, 0); - m_commandPool = m_device->createCommandPool(m_queue->getFamilyIndex(), IGPUCommandPool::CREATE_FLAGS::RESET_COMMAND_BUFFER_BIT); - m_commandPool->createCommandBuffers(IGPUCommandPool::BUFFER_LEVEL::PRIMARY, { &m_cmdbuf,1 }, smart_refctd_ptr(m_logger)); - - smart_refctd_ptr shader; - { - IAssetLoader::SAssetLoadParams lp = {}; - lp.logger = m_logger.get(); - lp.workingDirectory = ""; // virtual root - auto assetBundle = m_assetMgr->getAsset("app_resources/binarySearch.comp.hlsl", lp); - const auto assets = assetBundle.getContents(); - if (assets.empty()) - return logFail("Could not load shader!"); - - auto source = IAsset::castDown(assets[0]); - // The down-cast should not fail! - assert(source); - - // this time we skip the use of the asset converter since the ICPUShader->IGPUShader path is quick and simple - shader = m_device->compileShader({ source.get() }); - if (!shader) - return logFail("Creation of a GPU Shader to from CPU Shader source failed!"); - } - - const uint32_t bindingCount = 2u; - IGPUDescriptorSetLayout::SBinding bindings[bindingCount] = {}; - bindings[0].type = IDescriptor::E_TYPE::ET_STORAGE_BUFFER; // [[vk::binding(0)]] StructuredBuffer Histogram; - bindings[1].type = IDescriptor::E_TYPE::ET_STORAGE_BUFFER; // [[vk::binding(1)]] RWStructuredBuffer Output; - - for(int i = 0; i < bindingCount; ++i) - { - bindings[i].stageFlags = IShader::E_SHADER_STAGE::ESS_COMPUTE; - bindings[i].count = 1; - bindings[i].binding = i; - } - m_descriptorSetLayout = m_device->createDescriptorSetLayout(bindings); - { - SPushConstantRange pcRange = {}; - pcRange.stageFlags = IShader::E_SHADER_STAGE::ESS_COMPUTE; - pcRange.offset = 0u; - pcRange.size = sizeof(PushConstants); - auto layout = m_device->createPipelineLayout({ &pcRange,1 }, smart_refctd_ptr(m_descriptorSetLayout)); - IGPUComputePipeline::SCreationParams params = {}; - params.layout = layout.get(); - params.shader.shader = shader.get(); - params.shader.entryPoint = "main"; - if (m_device->getEnabledFeatures().pipelineExecutableInfo) - { - params.flags |= IGPUComputePipeline::SCreationParams::FLAGS::CAPTURE_STATISTICS; - params.flags |= IGPUComputePipeline::SCreationParams::FLAGS::CAPTURE_INTERNAL_REPRESENTATIONS; - } - if (!m_device->createComputePipelines(nullptr, { ¶ms,1 }, &m_pipeline)) - return logFail("Failed to create compute pipeline!\n"); - - if (m_device->getEnabledFeatures().pipelineExecutableInfo) - { - auto report = system::to_string(m_pipeline->getExecutableInfo()); - m_logger->log("Cooperative Binary Search Pipeline Executable Report:\n%s", ILogger::ELL_PERFORMANCE, report.c_str()); - } - } - - const size_t sizes[2] = {sizeof(TestCaseIndices),sizeof(uint32_t)*totalValues}; - for (uint32_t i = 0; i < bindingCount; i++) - { - m_buffers[i] = m_device->createBuffer(IGPUBuffer::SCreationParams { - {.size = sizes[i], .usage = - IGPUBuffer::E_USAGE_FLAGS::EUF_TRANSFER_DST_BIT | IGPUBuffer::E_USAGE_FLAGS::EUF_TRANSFER_SRC_BIT | - IGPUBuffer::E_USAGE_FLAGS::EUF_STORAGE_BUFFER_BIT, - } - }); - - auto reqs = m_buffers[i]->getMemoryReqs(); - reqs.memoryTypeBits &= m_device->getPhysicalDevice()->getHostVisibleMemoryTypeBits(); - - m_allocations[i] = m_device->allocate(reqs, m_buffers[i].get()); - - auto allocationType = i == 0 ? IDeviceMemoryAllocation::EMCAF_WRITE : IDeviceMemoryAllocation::EMCAF_READ; - auto mapResult = m_allocations[i].memory->map({ 0ull,m_allocations[i].memory->getAllocationSize() }, allocationType); - assert(mapResult); - } - - smart_refctd_ptr descriptorPool = nullptr; - { - IDescriptorPool::SCreateInfo createInfo = {}; - createInfo.maxSets = 1; - createInfo.maxDescriptorCount[static_cast(IDescriptor::E_TYPE::ET_STORAGE_BUFFER)] = bindingCount; - descriptorPool = m_device->createDescriptorPool(std::move(createInfo)); - } - - m_descriptorSet = descriptorPool->createDescriptorSet(smart_refctd_ptr(m_descriptorSetLayout)); - - IGPUDescriptorSet::SDescriptorInfo descriptorInfos[bindingCount] = {}; - IGPUDescriptorSet::SWriteDescriptorSet writeDescriptorSets[bindingCount] = {}; - - for(int i = 0; i < bindingCount; ++i) - { - writeDescriptorSets[i].info = &descriptorInfos[i]; - writeDescriptorSets[i].dstSet = m_descriptorSet.get(); - writeDescriptorSets[i].binding = i; - writeDescriptorSets[i].count = bindings[i].count; - - descriptorInfos[i].desc = m_buffers[i]; - descriptorInfos[i].info.buffer.size = ~0ull; - } - - m_device->updateDescriptorSets(bindingCount, writeDescriptorSets, 0u, nullptr); - - // Write test data to the m_buffers[0] - auto outPtr = m_allocations[0].memory->getMappedPointer(); - assert(outPtr); - memcpy( - reinterpret_cast(outPtr), - reinterpret_cast(&TestCaseIndices[0]), - sizeof(TestCaseIndices) - ); - - // In contrast to fences, we just need one semaphore to rule all dispatches - return true; - } - - void onAppTerminated_impl() override - { - m_device->waitIdle(); - } - - void workLoopBody() override - { - cpu_tests(); - - constexpr auto StartedValue = 0; - - smart_refctd_ptr progress = m_device->createSemaphore(StartedValue); - - m_cmdbuf->reset(IGPUCommandBuffer::RESET_FLAGS::RELEASE_RESOURCES_BIT); - m_cmdbuf->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT); - - - IGPUCommandBuffer::SPipelineBarrierDependencyInfo::buffer_barrier_t layoutBufferBarrier[1] = { { - .barrier = { - .dep = { - .srcStageMask = PIPELINE_STAGE_FLAGS::HOST_BIT, - .srcAccessMask = ACCESS_FLAGS::HOST_WRITE_BIT, - .dstStageMask = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT, - .dstAccessMask = ACCESS_FLAGS::SHADER_WRITE_BITS - } - }, - // whole buffer because we transferred the contents into it - .range = {.offset = 0,.size = m_buffers[1]->getCreationParams().size,.buffer = m_buffers[1]} - } }; - - const IGPUCommandBuffer::SPipelineBarrierDependencyInfo depInfo = { .bufBarriers = layoutBufferBarrier }; - m_cmdbuf->pipelineBarrier(EDF_NONE, depInfo); - - - const IGPUDescriptorSet* set = m_descriptorSet.get(); - PushConstants coopBinarySearchPC = { - .EntityCount = numIndices, - }; - - m_cmdbuf->bindComputePipeline(m_pipeline.get()); - m_cmdbuf->bindDescriptorSets(EPBP_COMPUTE, m_pipeline->getLayout(), 0u, 1u, &set); - m_cmdbuf->pushConstants(m_pipeline->getLayout(), nbl::hlsl::ShaderStage::ESS_COMPUTE, 0u, sizeof(PushConstants), &coopBinarySearchPC); - m_cmdbuf->dispatch((totalValues + 255u) / 256u, 1u, 1u); - - layoutBufferBarrier[0].barrier.dep = layoutBufferBarrier[0].barrier.dep.nextBarrier(PIPELINE_STAGE_FLAGS::COPY_BIT,ACCESS_FLAGS::TRANSFER_READ_BIT); - m_cmdbuf->pipelineBarrier(EDF_NONE,depInfo); - - m_cmdbuf->end(); - - { - constexpr auto FinishedValue = 69; - IQueue::SSubmitInfo submitInfos[1] = {}; - const IQueue::SSubmitInfo::SCommandBufferInfo cmdbufs[] = { {.cmdbuf = m_cmdbuf.get()} }; - submitInfos[0].commandBuffers = cmdbufs; - const IQueue::SSubmitInfo::SSemaphoreInfo signals[] = { {.semaphore = progress.get(),.value = FinishedValue,.stageMask = PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT} }; - submitInfos[0].signalSemaphores = signals; - m_api->startCapture(); - m_queue->submit(submitInfos); - m_api->endCapture(); - const ISemaphore::SWaitInfo waitInfos[] = { { - .semaphore = progress.get(), - .value = FinishedValue - } }; - m_device->blockForSemaphores(waitInfos); - } - - auto ptr = m_allocations[1].memory->getMappedPointer(); - assert(ptr); - - uint32_t* valuesPtr = reinterpret_cast(ptr); - for (uint32_t i = 0; i < totalValues; i++) { - uint32_t value = valuesPtr[i]; - const uint32_t* binarySearchResult = std::upper_bound(TestCaseIndices, TestCaseIndices + numIndices, i); - uint32_t lowerBoundIndex = std::distance(TestCaseIndices, binarySearchResult) - 1; - assert(value == lowerBoundIndex); - } - - m_keepRunning = false; - } - - bool keepRunning() override - { - return m_keepRunning; - } - - -private: - smart_refctd_ptr m_pipeline = nullptr; - smart_refctd_ptr m_descriptorSetLayout; - smart_refctd_ptr m_descriptorSet; - - smart_refctd_ptr m_buffers[2]; - nbl::video::IDeviceMemoryAllocator::SAllocation m_allocations[2] = {}; - smart_refctd_ptr m_cmdbuf = nullptr; - IQueue* m_queue; - smart_refctd_ptr m_commandPool; - uint64_t m_iteration = 0; - constexpr static inline uint64_t MaxIterations = 200; - - bool m_keepRunning = true; -}; - -NBL_MAIN_FUNC(CooperativeBinarySearch) - -void cpu_tests() -{ -} diff --git a/72_CooperativeBinarySearch/testCaseData.h b/72_CooperativeBinarySearch/testCaseData.h deleted file mode 100644 index 16153780e..000000000 --- a/72_CooperativeBinarySearch/testCaseData.h +++ /dev/null @@ -1,1192 +0,0 @@ -0, -298, -554, -582, -912, -1074, -1076, -1078, -1170, -1188, -2140, -2414, -2736, -2738, -3980, -4800, -5898, -5900, -6936, -8106, -8152, -8650, -8844, -8930, -9504, -10244, -10826, -10828, -11126, -11430, -12206, -13764, -14010, -15302, -15624, -15656, -16414, -16494, -17368, -17432, -18312, -18948, -19376, -19818, -20146, -20604, -21240, -22446, -23482, -24914, -25042, -25538, -26764, -27564, -27566, -28472, -29450, -30202, -31474, -32160, -32676, -33792, -33794, -34704, -36540, -37456, -37950, -38364, -39274, -40442, -40518, -41412, -41590, -41950, -42022, -42714, -43464, -43790, -43792, -44876, -44878, -46188, -46572, -47352, -47650, -48242, -49856, -49858, -50506, -50968, -50970, -51152, -51154, -52870, -52884, -53332, -53334, -53904, -53964, -53966, -53968, -53970, -53972, -53974, -53976, -53978, -53980, -54514, -54516, -54518, -54520, -54762, -55866, -56462, -56478, -56480, -56482, -57510, -57568, -57570, -57572, -57846, -57848, -58760, -59408, -59438, -60198, -60200, -60202, -60204, -60284, -60938, -61274, -61720, -62296, -63116, -63378, -63380, -63382, -63384, -63386, -63388, -63904, -64572, -65142, -65144, -65146, -65554, -65738, -66052, -67016, -67424, -67566, -68270, -68272, -68610, -69240, -69870, -70988, -72622, -73258, -73260, -73580, -74524, -74880, -74958, -74960, -74962, -75114, -75116, -75622, -77144, -77798, -77800, -78314, -79566, -79568, -79570, -79572, -79850, -79852, -81576, -81684, -81686, -82492, -82494, -82496, -82498, -83990, -84860, -84988, -84990, -85138, -85772, -86120, -86122, -86564, -87402, -87404, -87602, -88676, -88714, -88780, -89560, -89732, -90786, -91128, -91130, -91272, -91522, -91804, -92588, -92590, -92834, -93268, -93736, -94448, -94704, -94706, -95074, -95076, -96706, -97040, -97770, -98000, -98676, -99968, -100074, -100318, -100602, -100914, -101020, -101872, -101878, -103078, -104246, -104266, -105436, -106332, -106954, -107856, -108954, -110320, -110780, -111588, -111882, -112502, -112676, -113496, -114070, -115204, -115422, -115424, -115858, -116420, -117426, -118504, -118870, -119296, -119618, -119650, -120408, -120488, -121362, -121426, -122306, -122942, -123370, -123812, -124140, -124598, -125234, -126440, -127476, -128908, -129036, -129532, -130758, -131558, -131560, -132466, -133444, -134196, -135468, -136154, -136670, -137786, -137788, -138698, -140534, -140832, -141608, -142422, -143220, -143468, -143714, -144504, -145078, -145670, -146224, -146874, -147726, -148692, -149536, -151032, -151126, -153382, -154128, -155190, -155212, -156324, -156484, -156526, -157026, -158242, -158446, -158448, -158594, -159256, -160350, -160444, -161040, -161624, -162418, -162524, -162768, -163052, -163364, -163470, -164322, -164328, -165528, -166696, -166716, -167886, -168782, -169404, -170306, -171404, -172770, -173230, -174038, -174332, -174952, -175126, -175946, -176520, -177654, -177872, -177874, -178308, -178870, -179876, -180954, -181320, -181746, -182160, -183070, -184238, -184314, -185208, -185386, -185746, -185818, -186510, -187260, -187586, -187588, -188672, -188674, -189984, -190368, -191148, -191446, -192038, -193652, -193654, -194302, -194764, -194766, -194948, -194950, -196666, -196680, -197128, -197130, -197700, -198048, -198824, -199638, -200436, -200684, -200930, -201720, -202294, -202886, -203440, -204090, -204942, -205908, -206752, -208248, -208342, -210598, -211344, -212406, -212428, -213540, -213700, -213742, -214242, -215458, -215662, -215664, -215810, -216472, -217566, -217660, -218256, -218316, -218318, -218320, -218322, -218324, -218326, -218328, -218330, -218332, -218866, -218868, -218870, -218872, -219114, -220218, -220814, -220830, -220832, -220834, -221862, -221920, -221922, -221924, -222198, -222200, -223112, -223760, -223790, -224550, -224552, -224554, -224556, -225140, -225794, -226130, -226576, -227152, -227972, -228234, -228236, -228238, -228240, -228242, -228244, -228760, -229428, -229998, -230000, -230002, -230410, -230594, -230908, -231872, -232280, -232422, -233126, -233128, -233466, -234096, -234726, -235844, -237478, -238114, -238116, -238512, -239256, -239812, -240660, -241950, -243244, -243366, -244346, -244412, -244710, -245202, -246504, -246728, -246988, -247592, -248630, -249562, -250962, -251964, -252562, -253140, -253412, -254672, -255276, -256084, -256160, -256378, -257104, -257602, -257776, -258240, -258556, -258614, -259208, -260496, -261202, -261398, -262284, -262610, -262976, -263578, -264622, -265558, -266692, -266756, -268110, -268994, -269158, -269718, -270388, -270768, -271098, -271786, -272398, -272996, -273140, -273612, -274226, -274660, -275070, -275416, -275634, -275680, -276088, -276408, -276410, -276852, -277690, -277692, -277890, -278964, -279002, -279068, -279848, -280020, -281074, -281416, -281418, -281560, -281810, -282092, -282876, -282878, -283122, -283556, -284024, -284736, -284992, -284994, -285362, -285364, -286994, -287328, -288058, -288288, -288964, -289708, -289746, -290266, -291136, -292152, -292740, -292834, -293708, -293768, -293936, -294846, -295028, -295040, -295130, -295372, -296154, -296736, -297250, -297606, -298068, -298310, -299420, -300362, -301176, -301502, -301878, -302702, -303576, -303896, -305170, -305928, -306070, -306150, -307094, -307450, -307528, -307530, -307532, -307684, -307686, -308192, -309714, -310368, -310370, -310884, -312136, -312138, -312140, -312142, -312420, -312422, -314146, -314254, -314256, -315062, -315064, -315066, -315068, -316560, -317430, -317558, -317560, -317708, -318342, -319182, -319992, -320612, -320956, -321068, -321076, -322784, -322914, -323106, -324036, -324708, -326092, -326994, -327332, -328080, -328444, -329022, -329256, -330454, -331304, -331610, -332432, -332440, -333298, -334300, -334478, -334622, -335370, -335818, -336456, -336618, -337930, -338932, -339158, -339258, -339746, -340226, -340254, -340256, -340988, -341638, -342674, -343168, -343440, -344024, -344026, -344106, -345118, -346124, -347350, -348560, -348878, -349066, -350192, -350840, -351388, -353610, -354562, -355208, -356084, -356966, -358222, -359304, -359470, -360054, -360710, -360920, -361896, -362930, -362962, -363128, -363234, -363272, -363284, -363456, -363732, -364418, -364926, -365096, -365170, -365920, -366796, -367838, -368232, -368940, -369508, -369530, -370886, -371156, -371348, -372384, -372680, -372690, -373252, -373676, -374168, -374424, -374452, -374782, -374944, -374946, -374948, -375040, -375058, -376010, -376284, -376606, -376608, -377850, -378670, -379768, -379770, -380806, -381976, -382022, -382520, -382714, -382800, -383374, -384114, -384696, -384698, -384996, -385300, -386076, -387634, -387880, -388796, -389290, -389302, -389314, -389338, -389406, -389434, -389470, -389840, -389952, -390908, -391076, -391188, -392118, -392458, -392472, -392622, -392766, -393448, -394586, -394816, -394824, -395486, -396218, -396880, -396910, -397066, -397076, -397124, -397678, -398050, -399160, -400080, -401696, -401762, -402400, -402500, -402512, -403152, -404038, -404444, -404648, -404740, -405322, -406252, -407076, -408252, -408634, -409354, -410112, -411138, -411672, -411880, -412232, -412926, -412956, -413864, -414624, -415770, -415978, -417234, -417256, -417264, -418562, -418812, -418824, -418836, -418860, -418928, -418956, -418992, -419362, -419474, -420430, -420598, -420710, -421640, -421980, -421994, -422144, -422288, -422970, -424108, -424338, -424346, -425008, -425740, -426402, -426432, -426588, -426598, -426646, -427200, -427572, -428682, -429602, -430346, -430412, -431050, -431150, -431162, -431802, -432688, -433094, -433298, -433390, -433972, -434902, -435726, -436902, -437284, -438004, -438762, -439788, -440322, -440530, -440882, -441576, -441606, -442514, -443274, -444420, -444628, -445884, -445906, -445914, -447212, -447462, -448464, -448690, -448790, -449278, -449758, -449786, -449788, -450520, -451170, -452206, -452700, -452972, -453556, -453558, -453638, -454650, -455656, -456882, -458092, -458410, -458598, -459724, -460372, -460920, -463142, -464094, -464740, -465616, -466498, -467754, -468836, -469002, -469586, -470180, -471468, -472174, -472370, -473256, -473582, -473948, -474550, -475594, -476530, -477664, -477728, -479082, -479966, -480130, -480690, -481360, -481740, -482070, -482758, -483370, -483968, -484112, -484584, -485198, -485632, -486042, -486388, -486606, -486652, -487060, -488676, -489420, -489976, -490824, -492114, -493408, -493530, -494510, -494576, -494874, -495366, -496668, -496892, -497152, -497756, -498794, -499726, -501126, -502128, -502726, -503304, -503576, -504836, -505440, -506248, -506324, -506542, -507268, -507766, -507940, -508404, -508720, -509514, -510170, -510380, -511356, -512390, -512422, -512588, -512694, -512732, -512744, -512916, -513192, -513878, -514386, -514556, -514630, -515380, -516256, -517298, -517692, -518400, -518968, -518990, -520346, -520616, -520808, -521844, -522140, -522150, -522712, -523136, -523628, -524468, -525278, -525898, -526242, -526354, -526362, -528070, -528200, -528392, -529322, -529994, -531378, -532280, -532618, -533366, -533730, -534308, -534542, -535740, -536590, -536896, -537718, -537726, -538584, -539586, -539764, -539908, -540656, -541104, -541742, -541904, -543216, -543612, -543650, -544170, -545040, -546056, -546644, -546738, -547612, -547672, -547840, -548750, -548932, -548944, -549034, -549276, -550058, -550640, -551154, -551510, -551972, -552214, -553324, -554266, -555080, -555406, -555782, -556606, -557480, -557800, -559074, -559832, -559974, -550468, -551276, -552568, -552866, -553798, -554120, -554294, -555554, -556448, -556874, -557328, -557680, -558532, -559844, -560774, -561050, -561458, -562684, -563910, -564026, -564542, -565294, -565434, -566278, -567580, -568006, -568328, -569626, -570350, -570998, -572812, -573008, -573500, -573828, -573840, -573842, -574798, -576066, -576774, -577182, -577184, -577522, -577524, -578734, -579854, -579856, -581128, -581278, -582296, -583496, -583944, -584160, -584844, -584954, -584968, -585486, -586592, -586594, -587158, -587320, -588006, -589012, -590302, -590366, -590444, -590944, -581786, -582234, -582920, -582922, -564780, -565486, -565684, -566570, -566896, -567262, -567864, -568958, -570268, -570844, -572014, -573368, -574252, -574416, -574976, -575646, -576026, -576356, -577044, -577046, -577644, -577788, -578260, -578874, -579308, -579718, -580288, -580942, -581534, -581536, -576350, -576352 \ No newline at end of file diff --git a/73_GeometryInspector/CMakeLists.txt b/73_GeometryInspector/CMakeLists.txt deleted file mode 100644 index 8eed20a70..000000000 --- a/73_GeometryInspector/CMakeLists.txt +++ /dev/null @@ -1,23 +0,0 @@ -include(common RESULT_VARIABLE RES) -if(NOT RES) - message(FATAL_ERROR "common.cmake not found. Should be in {repo_root}/cmake directory") -endif() - -if(NBL_BUILD_IMGUI AND NBL_BUILD_DEBUG_DRAW) - set(NBL_INCLUDE_SERACH_DIRECTORIES - "${CMAKE_CURRENT_SOURCE_DIR}/include" - "${NBL_EXT_MITSUBA_LOADER_INCLUDE_DIRS}" - ) - - list(APPEND NBL_LIBRARIES - imtestengine - imguizmo - "${NBL_EXT_IMGUI_UI_LIB}" - "${NBL_EXT_MITSUBA_LOADER_LIB}" - ) - - nbl_create_executable_project("" "" "${NBL_INCLUDE_SERACH_DIRECTORIES}" "${NBL_LIBRARIES}" "${NBL_EXECUTABLE_PROJECT_CREATION_PCH_TARGET}") - - target_link_libraries(${EXECUTABLE_NAME} PRIVATE Nabla::ext::DebugDraw) -endif() - diff --git a/73_GeometryInspector/include/common.hpp b/73_GeometryInspector/include/common.hpp deleted file mode 100644 index cc06db2c1..000000000 --- a/73_GeometryInspector/include/common.hpp +++ /dev/null @@ -1,22 +0,0 @@ -#ifndef _NBL_THIS_EXAMPLE_COMMON_H_INCLUDED_ -#define _NBL_THIS_EXAMPLE_COMMON_H_INCLUDED_ - - -#include "nbl/examples/examples.hpp" - -using namespace nbl; -using namespace core; -using namespace hlsl; -using namespace system; -using namespace asset; -using namespace ui; -using namespace video; -using namespace scene; -using namespace nbl::examples; - -#include "transform.hpp" -#include "nbl/ui/ICursorControl.h" -#include "nbl/ext/ImGui/ImGui.h" -#include "imgui/imgui_internal.h" - -#endif // __NBL_THIS_EXAMPLE_COMMON_H_INCLUDED__ \ No newline at end of file diff --git a/73_GeometryInspector/include/transform.hpp b/73_GeometryInspector/include/transform.hpp deleted file mode 100644 index 6ac299c4b..000000000 --- a/73_GeometryInspector/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 = false, 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/73_GeometryInspector/main.cpp b/73_GeometryInspector/main.cpp deleted file mode 100644 index 570ce52d2..000000000 --- a/73_GeometryInspector/main.cpp +++ /dev/null @@ -1,741 +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 -#include "common.hpp" - -#include "../3rdparty/portable-file-dialogs/portable-file-dialogs.h" -#include - -#ifdef NBL_BUILD_MITSUBA_LOADER -#include "nbl/ext/MitsubaLoader/CSerializedLoader.h" -#endif - -#include "nbl/ext/DebugDraw/CDrawAABB.h" -#include "nbl/ext/ImGui/ImGui.h" - -class GeometryInspectorApp final : public MonoWindowApplication, public BuiltinResourcesApplication -{ - using device_base_t = MonoWindowApplication; - using asset_base_t = BuiltinResourcesApplication; - - enum DrawBoundingBoxMode - { - DBBM_NONE, - DBBM_AABB, - DBBM_OBB, - DBBM_COUNT - }; - - public: - inline GeometryInspectorApp(const path& _localInputCWD, const path& _localOutputCWD, const path& _sharedInputCWD, const path& _sharedOutputCWD) - : IApplicationFramework(_localInputCWD, _localOutputCWD, _sharedInputCWD, _sharedOutputCWD), - device_base_t({1280,720}, EF_D32_SFLOAT, _localInputCWD, _localOutputCWD, _sharedInputCWD, _sharedOutputCWD) {} - - inline bool onAppInitialized(smart_refctd_ptr&& system) override - { - if (!asset_base_t::onAppInitialized(smart_refctd_ptr(system))) - return false; - #ifdef NBL_BUILD_MITSUBA_LOADER - m_assetMgr->addAssetLoader(make_smart_refctd_ptr()); - #endif - 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!"); - } - - - auto scRes = static_cast(m_surface->getSwapchainResources()); - m_renderer = CSimpleDebugRenderer::create(m_assetMgr.get(),scRes->getRenderpass(),0,{}); - if (!m_renderer) - return logFail("Failed to create renderer!"); - - auto* renderpass = scRes->getRenderpass(); - - { - ext::debug_draw::DrawAABB::SCreationParameters params = {}; - params.assetManager = m_assetMgr; - params.transfer = getTransferUpQueue(); - params.drawMode = ext::debug_draw::DrawAABB::ADM_DRAW_BATCH; - params.batchPipelineLayout = ext::debug_draw::DrawAABB::createDefaultPipelineLayout(m_device.get()); - params.renderpass = smart_refctd_ptr(renderpass); - params.utilities = m_utils; - m_bbRenderer = ext::debug_draw::DrawAABB::create(std::move(params)); - } - - // gui descriptor setup - { - using binding_flags_t = IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS; - { - IGPUSampler::SParams params; - params.AnisotropicFilter = 1u; - params.TextureWrapU = ETC_REPEAT; - params.TextureWrapV = ETC_REPEAT; - params.TextureWrapW = ETC_REPEAT; - - m_ui.samplers.gui = m_device->createSampler(params); - m_ui.samplers.gui->setObjectDebugName("Nabla IMGUI UI Sampler"); - } - - std::array, 69u> immutableSamplers; - for (auto& it : immutableSamplers) - it = smart_refctd_ptr(m_ui.samplers.scene); - - immutableSamplers[nbl::ext::imgui::UI::FontAtlasTexId] = smart_refctd_ptr(m_ui.samplers.gui); - - 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, MaxUITextureCount); - params.renderpass = smart_refctd_ptr(renderpass); - params.streamingBuffer = nullptr; - params.subpassIx = 0u; - params.transfer = getGraphicsQueue(); - params.utilities = m_utils; - { - m_ui.manager = ext::imgui::UI::create(std::move(params)); - - // note that we use default layout provided by our extension, but you are free to create your own by filling nbl::ext::imgui::UI::S_CREATION_PARAMETERS::resources - const auto* descriptorSetLayout = m_ui.manager->getPipeline()->getLayout()->getDescriptorSetLayout(0u); - - IDescriptorPool::SCreateInfo descriptorPoolInfo = {}; - descriptorPoolInfo.maxDescriptorCount[static_cast(asset::IDescriptor::E_TYPE::ET_SAMPLER)] = (uint32_t)nbl::ext::imgui::UI::DefaultSamplerIx::COUNT; - descriptorPoolInfo.maxDescriptorCount[static_cast(asset::IDescriptor::E_TYPE::ET_SAMPLED_IMAGE)] = MaxUITextureCount; - descriptorPoolInfo.maxSets = 1u; - descriptorPoolInfo.flags = IDescriptorPool::E_CREATE_FLAGS::ECF_UPDATE_AFTER_BIND_BIT; - - m_guiDescriptorSetPool = m_device->createDescriptorPool(std::move(descriptorPoolInfo)); - assert(m_guiDescriptorSetPool); - - m_guiDescriptorSetPool->createDescriptorSets(1u, &descriptorSetLayout, &m_ui.descriptorSet); - assert(m_ui.descriptorSet); - } - } - - m_ui.manager->registerListener( - [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; - }()); - - ImGuizmo::SetOrthographic(false); - ImGuizmo::BeginFrame(); - - // create a window and insert the inspector - ImGui::SetNextWindowPos(ImVec2(10, 10), ImGuiCond_Appearing); - ImGui::SetNextWindowSize(ImVec2(320, 340), ImGuiCond_Appearing); - ImGui::Begin("Controls"); - - ImGui::SameLine(); - - ImGui::Text("Camera"); - - ImGui::SliderFloat("Move speed", &m_cameraSetting.moveSpeed, 0.1f, 10.f); - ImGui::SliderFloat("Rotate speed", &m_cameraSetting.rotateSpeed, 0.1f, 10.f); - ImGui::SliderFloat("Fov", &m_cameraSetting.fov, 20.f, 150.f); - ImGui::SliderFloat("zNear", &m_cameraSetting.zNear, 0.1f, 100.f); - ImGui::SliderFloat("zFar", &m_cameraSetting.zFar, 110.f, 10000.f); - - - ImGui::Text("Inspector"); - ImGui::ListBox("Selected polygon", &m_selectedMesh, - [](void* userData, int index) -> const char* { - auto* meshInstances = reinterpret_cast(userData); - return meshInstances[index].name.data(); - }, - m_meshInstances.data(), - m_meshInstances.size()); - - ImGui::Checkbox("Draw AABB", &m_shouldDrawAABB); - ImGui::Checkbox("Draw OBB", &m_shouldDrawOBB); - 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(); - - static struct - { - hlsl::float32_t4x4 view, projection, model; - } imguizmoM16InOut; - - ImGuizmo::SetID(0u); - - 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.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)); - { - m_transformParams.enableViewManipulate = true; - EditTransform(&imguizmoM16InOut.view[0][0], &imguizmoM16InOut.projection[0][0], &imguizmoM16InOut.model[0][0], m_transformParams); - } - selectedInstance.world = hlsl::float32_t3x4(hlsl::transpose(imguizmoM16InOut.model)); - - ImGui::End(); - }); - // - if (!reloadModel()) - return false; - - m_camera.mapKeysToArrows(); - - onAppInitializedFinish(); - return true; - } - - bool updateGUIDescriptorSet() - { - // texture atlas, note we don't create info & write pair for the font sampler because UI extension's is immutable and baked into DS layout - static std::array descriptorInfo; - static IGPUDescriptorSet::SWriteDescriptorSet writes[MaxUITextureCount]; - - descriptorInfo[nbl::ext::imgui::UI::FontAtlasTexId].info.image.imageLayout = IImage::LAYOUT::READ_ONLY_OPTIMAL; - descriptorInfo[nbl::ext::imgui::UI::FontAtlasTexId].desc = smart_refctd_ptr(m_ui.manager->getFontAtlasView()); - - for (uint32_t i = 0; i < descriptorInfo.size(); ++i) - { - writes[i].dstSet = m_ui.descriptorSet.get(); - writes[i].binding = 0u; - writes[i].arrayElement = i; - writes[i].count = 1u; - } - writes[nbl::ext::imgui::UI::FontAtlasTexId].info = descriptorInfo.data() + nbl::ext::imgui::UI::FontAtlasTexId; - - return m_device->updateDescriptorSets(writes, {}); - } - - inline void update(const std::chrono::microseconds nextPresentationTimestamp) - { - m_camera.setMoveSpeed(m_cameraSetting.moveSpeed); - m_camera.setRotateSpeed(m_cameraSetting.rotateSpeed); - - static std::chrono::microseconds previousEventTimestamp{}; - - m_inputSystem->getDefaultMouse(&m_mouse); - m_inputSystem->getDefaultKeyboard(&m_keyboard); - - struct - { - std::vector mouse{}; - std::vector keyboard{}; - } 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 m_camera handle them with its impl - - for (const auto& e : events) // here capture - { - if (e.timeStamp < previousEventTimestamp) - continue; - - previousEventTimestamp = e.timeStamp; - capturedEvents.mouse.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 - { - if (e.timeStamp < previousEventTimestamp) - continue; - if (e.keyCode == E_KEY_CODE::EKC_R && e.action == SKeyboardEvent::ECA_RELEASED) - reload = true; - - previousEventTimestamp = e.timeStamp; - capturedEvents.keyboard.emplace_back(e); - } - }, m_logger.get()); - if (reload) reloadModel(); - - } - m_camera.endInputProcessing(nextPresentationTimestamp); - - 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()); - const auto cursorPosition = m_window->getCursorControl()->getPosition(); - const auto mousePosition = float32_t2(cursorPosition.x, cursorPosition.y) - float32_t2(m_window->getX(), m_window->getY()); - - const ext::imgui::UI::SUpdateParameters params = - { - .mousePosition = mousePosition, - .displaySize = { m_window->getWidth(), m_window->getHeight() }, - .mouseEvents = mouseEvents, - .keyboardEvents = keyboardEvents - }; - - m_ui.manager->update(params); - } - - inline IQueue::SSubmitInfo::SSemaphoreInfo renderFrame(const std::chrono::microseconds nextPresentationTimestamp) override - { - update(nextPresentationTimestamp); - - // - 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 - { - // begin renderpass - { - auto scRes = static_cast(m_surface->getSwapchainResources()); - auto* framebuffer = scRes->getFramebuffer(device_base_t::getCurrentAcquire().imageIndex); - const IGPUCommandBuffer::SClearColorValue clearValue = { .float32 = {0.f,0.f,0.f,1.f} }; - const IGPUCommandBuffer::SClearDepthStencilValue depthValue = { .depth = 0.f }; - const VkRect2D currentRenderArea = - { - .offset = {0,0}, - .extent = {framebuffer->getCreationParameters().width,framebuffer->getCreationParameters().height} - }; - const IGPUCommandBuffer::SRenderpassBeginInfo info = - { - .framebuffer = framebuffer, - .colorClearValues = &clearValue, - .depthStencilClearValues = &depthValue, - .renderArea = currentRenderArea - }; - cb->beginRenderPass(info,IGPUCommandBuffer::SUBPASS_CONTENTS::INLINE); - - const SViewport viewport = { - .x = static_cast(currentRenderArea.offset.x), - .y = static_cast(currentRenderArea.offset.y), - .width = static_cast(currentRenderArea.extent.width), - .height = static_cast(currentRenderArea.extent.height) - }; - cb->setViewport(0u,1u,&viewport); - - cb->setScissor(0u,1u,¤tRenderArea); - } - - // draw scene - float32_t3x4 viewMatrix = m_camera.getViewMatrix(); - float32_t4x4 viewProjMatrix = m_camera.getConcatenatedMatrix(); - - m_renderer->render(cb,CSimpleDebugRenderer::SViewParams(viewMatrix,viewProjMatrix)); - - const ISemaphore::SWaitInfo drawFinished = { .semaphore = m_semaphore.get(),.value = m_realFrameIx + 1u }; - const auto& renderInstance = m_renderer->getInstance(m_selectedMesh); - const auto& meshInstance = m_meshInstances[m_selectedMesh]; - core::vector debugDrawInstances; - debugDrawInstances.reserve(2); - const auto world4x4 = float32_t4x4{ - renderInstance.world[0], - renderInstance.world[1], - renderInstance.world[2], - float32_t4(0, 0, 0, 1) - }; - if (m_shouldDrawAABB) - { - const auto aabbTransform = ext::debug_draw::DrawAABB::getTransformFromAABB(meshInstance.aabb); - debugDrawInstances.push_back(ext::debug_draw::InstanceData{ .transform = math::linalg::promoted_mul(world4x4, aabbTransform), .color = float32_t4(1, 1, 1, 1)}); - } - if (m_shouldDrawOBB) - { - debugDrawInstances.push_back(ext::debug_draw::InstanceData{ .transform = math::linalg::promoted_mul(world4x4, meshInstance.obb.transform), .color = float32_t4(0, 0, 1, 1)}); - } - m_bbRenderer->render({ cb, viewProjMatrix }, drawFinished, debugDrawInstances); - - cb->beginDebugMarker("Render ImGui"); - const auto uiParams = m_ui.manager->getCreationParameters(); - auto* uiPipeline = m_ui.manager->getPipeline(); - cb->bindGraphicsPipeline(uiPipeline); - cb->bindDescriptorSets(EPBP_GRAPHICS, uiPipeline->getLayout(), uiParams.resources.texturesInfo.setIx, 1u, &m_ui.descriptorSet.get()); - if (!m_ui.manager->render(cb, drawFinished)) - { - m_logger->log("TODO: need to present acquired image before bailing because its already acquired.",ILogger::ELL_ERROR); - return {}; - } - cb->endDebugMarker(); - - cb->endRenderPass(); - } - cb->end(); - - 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--; - } - - std::string caption = "[Nabla Engine] Geometry Inspector"; - { - caption += ", displaying ["; - caption += m_modelPath; - caption += "]"; - m_window->setCaption(caption); - } - - updateGUIDescriptorSet(); - return retval; - } - - protected: - const video::IGPURenderpass::SCreationParams::SSubpassDependency* getDefaultSubpassDependencies() const override - { - // Subsequent submits don't wait for each other, hence its important to have External Dependencies which prevent users of the depth attachment overlapping. - 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 - .srcStageMask = PIPELINE_STAGE_FLAGS::LATE_FRAGMENT_TESTS_BIT, - // don't want any writes to be available, we'll clear - .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 - }, - // color from ATTACHMENT_OPTIMAL to PRESENT_SRC - { - .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: - // TODO: standardise this across examples, and take from `argv` - bool m_nonInteractiveTest = false; - - bool reloadModel() - { - if (m_nonInteractiveTest) // TODO: maybe also take from argv and argc - m_modelPath = (sharedInputCWD/"ply/Spanner-ply.ply").string(); - else - { - pfd::open_file file("Choose a supported Model File", sharedInputCWD.string(), - { - "All Supported Formats", "*.ply *.stl *.serialized *.obj", - "TODO (.ply)", "*.ply", - "TODO (.stl)", "*.stl", - "Mitsuba 0.6 Serialized (.serialized)", "*.serialized", - "Wavefront Object (.obj)", "*.obj" - }, - false - ); - if (file.result().empty()) - return false; - m_modelPath = file.result()[0]; - } - - // free up - m_renderer->m_instances.clear(); - m_renderer->clearGeometries({.semaphore=m_semaphore.get(),.value=m_realFrameIx}); - m_assetMgr->clearAllAssetCache(); - - //! load the geometry - IAssetLoader::SAssetLoadParams params = {}; - params.logger = m_logger.get(); - auto bundle = m_assetMgr->getAsset(m_modelPath,params); - if (bundle.getContents().empty()) - return false; - - // - core::vector> geometries; - switch (bundle.getAssetType()) - { - case IAsset::E_TYPE::ET_GEOMETRY: - for (const auto& item : bundle.getContents()) - if (auto polyGeo=IAsset::castDown(item); polyGeo) - geometries.push_back(polyGeo); - break; - default: - m_logger->log("Asset loaded but not a supported type (ET_GEOMETRY,ET_GEOMETRY_COLLECTION)",ILogger::ELL_ERROR); - break; - } - if (geometries.empty()) - return false; - - using aabb_t = hlsl::shapes::AABB<3,float32_t>; - auto printAABB = [&](const aabb_t& aabb, const char* extraMsg="")->void - { - m_logger->log("%s AABB is (%f,%f,%f) -> (%f,%f,%f)",ILogger::ELL_INFO,extraMsg,aabb.minVx.x,aabb.minVx.y,aabb.minVx.z,aabb.maxVx.x,aabb.maxVx.y,aabb.maxVx.z); - }; - auto bound = aabb_t::create(); - // convert the geometries - { - smart_refctd_ptr converter = CAssetConverter::create({.device=m_device.get()}); - - const auto transferFamily = getTransferUpQueue()->getFamilyIndex(); - - struct SInputs : CAssetConverter::SInputs - { - virtual inline std::span getSharedOwnershipQueueFamilies(const size_t groupCopyID, const asset::ICPUBuffer* buffer, const CAssetConverter::patch_t& patch) const - { - return sharedBufferOwnership; - } - - core::vector sharedBufferOwnership; - } inputs = {}; - core::vector> patches(geometries.size(),CSimpleDebugRenderer::DefaultPolygonGeometryPatch); - { - inputs.logger = m_logger.get(); - std::get>(inputs.assets) = {&geometries.front().get(),geometries.size()}; - std::get>(inputs.patches) = patches; - // set up shared ownership so we don't have to - core::unordered_set families; - families.insert(transferFamily); - families.insert(getGraphicsQueue()->getFamilyIndex()); - if (families.size()>1) - for (const auto fam : families) - inputs.sharedBufferOwnership.push_back(fam); - } - - // reserve - auto reservation = converter->reserve(inputs); - if (!reservation) - { - m_logger->log("Failed to reserve GPU objects for CPU->GPU conversion!",ILogger::ELL_ERROR); - return false; - } - - // convert - { - auto semaphore = m_device->createSemaphore(0u); - - constexpr auto MultiBuffering = 2; - std::array,MultiBuffering> commandBuffers = {}; - { - auto pool = m_device->createCommandPool(transferFamily,IGPUCommandPool::CREATE_FLAGS::RESET_COMMAND_BUFFER_BIT|IGPUCommandPool::CREATE_FLAGS::TRANSIENT_BIT); - pool->createCommandBuffers(IGPUCommandPool::BUFFER_LEVEL::PRIMARY,commandBuffers,smart_refctd_ptr(m_logger)); - } - commandBuffers.front()->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT); - - std::array commandBufferSubmits; - for (auto i=0; ilog("Failed to await submission feature!", ILogger::ELL_ERROR); - return false; - } - } - - auto tmp = hlsl::float32_t4x3( - hlsl::float32_t3(1,0,0), - hlsl::float32_t3(0,1,0), - hlsl::float32_t3(0,0,1), - hlsl::float32_t3(0,0,0) - ); - const auto& converted = reservation.getGPUObjects(); - core::vector meshWorlds; - for (uint32_t i = 0; i < converted.size(); i++) - { - const auto& geom = converted[i]; - const auto aabb = geom.value->getAABB(); - printAABB(aabb,"Geometry"); - tmp[3].x += aabb.getExtent().x; - meshWorlds.emplace_back(hlsl::transpose(tmp)); - const auto transformed = hlsl::shapes::util::transform(meshWorlds.back(), aabb); - bound = hlsl::shapes::util::union_(transformed,bound); - - const auto& cpuGeom = geometries[i].get(); - const auto obb = CPolygonGeometryManipulator::calculateOBB( - cpuGeom->getPositionView().getElementCount(), - [geo = cpuGeom](size_t vertex_i) { - hlsl::float32_t3 pt; - geo->getPositionView().decodeElement(vertex_i, pt); - return pt; - }); - - m_meshInstances.push_back({ .name = std::format("Mesh {}", i), .aabb = aabb, .obb = obb }); - } - - printAABB(bound,"Total"); - if (!m_renderer->addGeometries({ &converted.front().get(),converted.size() })) - return false; - - for (auto geom_i = 0u; geom_i < m_renderer->getGeometries().size(); geom_i++) - m_renderer->m_instances.push_back({ - .world = meshWorlds[geom_i], - .packedGeo = &m_renderer->getGeometry(geom_i) - }); - } - - // get scene bounds and reset m_camera - { - const float32_t distance = 0.05; - const auto diagonal = bound.getExtent(); - { - 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); - } - 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)); - } - - // TODO: write out the geometry - - return true; - } - - // 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 uint8_t MaxUITextureCount = 1u; - // - smart_refctd_ptr m_renderer; - - struct MeshInstance - { - std::string name; - hlsl::shapes::AABB<3, float32_t> aabb; - hlsl::shapes::OBB<3, float32_t> obb; - }; - core::vector m_meshInstances; - int m_selectedMesh = 0; - // - smart_refctd_ptr m_semaphore; - uint64_t m_realFrameIx = 0; - std::array,MaxFramesInFlight> m_cmdBufs; - // - InputSystem::ChannelReader m_mouse; - InputSystem::ChannelReader m_keyboard; - // - struct CameraSetting - { - float fov = 60.f; - float zNear = 0.1f; - float zFar = 10000.f; - float moveSpeed = 1.f; - float rotateSpeed = 1.f; - float viewWidth = 10.f; - float camYAngle = 165.f / 180.f * 3.14159f; - 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()); - // mutables - std::string m_modelPath; - - smart_refctd_ptr m_bbRenderer; - bool m_shouldDrawAABB; - bool m_shouldDrawOBB; - - struct C_UI - { - nbl::core::smart_refctd_ptr manager; - - struct - { - core::smart_refctd_ptr gui, scene; - } samplers; - - core::smart_refctd_ptr descriptorSet; - } m_ui; - core::smart_refctd_ptr m_guiDescriptorSetPool; - - TransformRequestParams m_transformParams; - }; - -NBL_MAIN_FUNC(GeometryInspectorApp) diff --git a/74_QuantizedSequenceTests/CMakeLists.txt b/74_QuantizedSequenceTests/CMakeLists.txt deleted file mode 100644 index a8dfb6781..000000000 --- a/74_QuantizedSequenceTests/CMakeLists.txt +++ /dev/null @@ -1,50 +0,0 @@ -include(common RESULT_VARIABLE RES) -if(NOT RES) - message(FATAL_ERROR "common.cmake not found. Should be in {repo_root}/cmake directory") -endif() - -nbl_create_executable_project("" "" "" "" "${NBL_EXECUTABLE_PROJECT_CREATION_PCH_TARGET}") - -if(MSVC) - target_compile_options("${EXECUTABLE_NAME}" PUBLIC "/fp:strict") -else() - target_compile_options("${EXECUTABLE_NAME}" PUBLIC -ffloat-store -frounding-math -fsignaling-nans -ftrapping-math) -endif() - -set(OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/auto-gen") - -set(SM 6_8) -set(JSON [=[ -[ - { - "INPUT": "app_resources/quantizedSequenceTest.comp.hlsl", - "KEY": "quantizedSequenceTest", - } -] -]=]) -string(CONFIGURE "${JSON}" JSON) - -set(COMPILE_OPTIONS - -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} -) diff --git a/74_QuantizedSequenceTests/CQuantizedSequenceTester.h b/74_QuantizedSequenceTests/CQuantizedSequenceTester.h deleted file mode 100644 index baeb928b0..000000000 --- a/74_QuantizedSequenceTests/CQuantizedSequenceTester.h +++ /dev/null @@ -1,304 +0,0 @@ -#ifndef _NBL_EXAMPLES_TESTS_74_QUANTIZED_SEQUENCE_TESTER_INCLUDED_ -#define _NBL_EXAMPLES_TESTS_74_QUANTIZED_SEQUENCE_TESTER_INCLUDED_ - -#define GLM_FORCE_RADIANS -#include -#include -#define GLM_ENABLE_EXPERIMENTAL -#include -#include - -#include "nbl/examples/examples.hpp" -#include "app_resources/common.hlsl" -#include "nbl/examples/Tester/ITester.h" -#include -#include - -using namespace nbl; - -class CQuantizedSequenceTester final : public ITester -{ - using base_t = ITester; - -public: - CQuantizedSequenceTester(const uint32_t testBatchCount) - : base_t(testBatchCount) {}; - -private: - QuantizedSequenceInputTestValues generateInputTestValues() override - { - std::uniform_real_distribution realDistribution(0.0f, 1.0f); - std::uniform_int_distribution uint32Distribution(0, std::numeric_limits::max()); - std::uniform_int_distribution uint16Distribution(0, std::numeric_limits::max()); - - QuantizedSequenceInputTestValues testInput; - testInput.scalar = uint16Distribution(getRandomEngine()); - testInput.u16vec2 = uint32_t2(uint16Distribution(getRandomEngine()), uint16Distribution(getRandomEngine())); - testInput.u16vec3 = uint32_t3(uint16Distribution(getRandomEngine()), uint16Distribution(getRandomEngine()), uint16Distribution(getRandomEngine())); - testInput.u16vec4 = uint32_t4(uint16Distribution(getRandomEngine()), uint16Distribution(getRandomEngine()), uint16Distribution(getRandomEngine()), uint16Distribution(getRandomEngine())); - - testInput.scalar16 = uint32Distribution(getRandomEngine()); - testInput.uvec2 = uint32_t2(uint32Distribution(getRandomEngine()), uint32Distribution(getRandomEngine())); - testInput.uvec3 = uint32_t3(uint32Distribution(getRandomEngine()), uint32Distribution(getRandomEngine()), uint32Distribution(getRandomEngine())); - testInput.uvec4 = uint32_t4(uint32Distribution(getRandomEngine()), uint32Distribution(getRandomEngine()), uint32Distribution(getRandomEngine()), uint32Distribution(getRandomEngine())); - - testInput.unorm1 = float32_t1(realDistribution(getRandomEngine())); - testInput.unorm2 = float32_t2(realDistribution(getRandomEngine()), realDistribution(getRandomEngine())); - testInput.unorm3 = float32_t3(realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), realDistribution(getRandomEngine())); - testInput.unorm4 = float32_t4(realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), realDistribution(getRandomEngine()), realDistribution(getRandomEngine())); - - testInput.scrambleKey1 = uint32_t1(uint32Distribution(getRandomEngine())); - testInput.scrambleKey2 = uint32_t2(uint32Distribution(getRandomEngine()), uint32Distribution(getRandomEngine())); - testInput.scrambleKey3 = uint32_t3(uint32Distribution(getRandomEngine()), uint32Distribution(getRandomEngine()), uint32Distribution(getRandomEngine())); - testInput.scrambleKey4 = uint32_t4(uint32Distribution(getRandomEngine()), uint32Distribution(getRandomEngine()), uint32Distribution(getRandomEngine()), uint32Distribution(getRandomEngine())); - - return testInput; - } - - QuantizedSequenceTestValues determineExpectedResults(const QuantizedSequenceInputTestValues& testInput) override - { - QuantizedSequenceTestValues expected; - // test create/set/get - expected.uintDim1 = testInput.scalar; - { - for (uint32_t i = 0; i < 2; i++) - expected.uintDim2[i] = testInput.uvec2[i] >> 16u; - } - { - for (uint32_t i = 0; i < 3; i++) - expected.uintDim3[i] = testInput.uvec3[i] >> 22u; - } - { - for (uint32_t i = 0; i < 4; i++) - expected.uintDim4[i] = testInput.uvec4[i] >> 24u; - } - - expected.uintVec2_Dim2 = testInput.uvec2; - { - for (uint32_t i = 0; i < 3; i++) - expected.uintVec2_Dim3[i] = testInput.uvec3[i] >> 11u; - } - - expected.uintVec3_Dim3 = testInput.uvec3; - expected.uintVec4_Dim4 = testInput.uvec4; - - expected.u16Dim1 = testInput.scalar16; - { - for (uint32_t i = 0; i < 2; i++) - expected.u16Dim2[i] = testInput.u16vec2[i] >> 8u; - } - { - for (uint32_t i = 0; i < 3; i++) - expected.u16Dim3[i] = testInput.u16vec3[i] >> 11u; - } - { - for (uint32_t i = 0; i < 4; i++) - expected.u16Dim4[i] = testInput.u16vec4[i] >> 12u; - } - - expected.u16Vec2_Dim2 = testInput.u16vec2; - { - for (uint32_t i = 0; i < 4; i++) - expected.u16Vec2_Dim4[i] = testInput.u16vec4[i] >> 8u; - } - - expected.u16Vec3_Dim3 = testInput.u16vec3; - expected.u16Vec4_Dim4 = testInput.u16vec4; - - // test encode/decode uint32, dim 1..4 - { - const uint32_t fullWidthMultiplier = (1u << 31u) - 1u; - uint32_t1 stored; - stored[0] = uint32_t(testInput.unorm1[0] * fullWidthMultiplier); - expected.unorm1_pre_u32 = float32_t1(stored ^ testInput.scrambleKey1) * bit_cast(0x2f800004u); - } - { - const uint32_t multiplier = (1u << 31u) - 1u; - uint32_t1 stored; - stored[0] = uint32_t(testInput.unorm1[0] * multiplier); - expected.unorm1_post_u32 = float32_t1(stored ^ testInput.scrambleKey1) * bit_cast(0x2f800004u); - } - { - const uint32_t bitsPerComponent = 16u; - const uint32_t discardBits = 32u - bitsPerComponent; - const uint32_t fullWidthMultiplier = (1u << 31u) - 1u; - uint32_t2 stored; - for (uint32_t i = 0; i < 2; i++) - stored[i] = uint32_t(testInput.unorm2[i] * fullWidthMultiplier) >> discardBits; - expected.unorm2_pre_u32 = float32_t2(stored ^ testInput.scrambleKey2) * bit_cast(0x2f800004u); - } - { - const uint32_t bitsPerComponent = 16u; - const uint32_t discardBits = 32u - bitsPerComponent; - const uint32_t multiplier = (1u << bitsPerComponent) - 1u; - uint32_t2 stored, scrambleKey; - for (uint32_t i = 0; i < 2; i++) - { - stored[i] = uint32_t(testInput.unorm2[i] * multiplier) >> discardBits; - scrambleKey[i] = testInput.scrambleKey2[i] >> discardBits; - } - expected.unorm2_post_u32 = float32_t2(stored ^ scrambleKey) * bit_cast(0x37800080u); - } - { - const uint32_t bitsPerComponent = 10u; - const uint32_t discardBits = 32u - bitsPerComponent; - const uint32_t fullWidthMultiplier = (1u << 31u) - 1u; - uint32_t3 stored; - for (uint32_t i = 0; i < 3; i++) - stored[i] = uint32_t(testInput.unorm3[i] * fullWidthMultiplier) >> discardBits; - expected.unorm3_pre_u32 = float32_t3(stored ^ testInput.scrambleKey3) * bit_cast(0x2f800004u); - } - { - const uint32_t bitsPerComponent = 10u; - const uint32_t discardBits = 32u - bitsPerComponent; - const uint32_t multiplier = (1u << bitsPerComponent) - 1u; - uint32_t3 stored, scrambleKey; - for (uint32_t i = 0; i < 3; i++) - { - stored[i] = uint32_t(testInput.unorm3[i] * multiplier) >> discardBits; - scrambleKey[i] = testInput.scrambleKey3[i] >> discardBits; - } - expected.unorm3_post_u32 = float32_t3(stored ^ scrambleKey) * bit_cast(0x3a802008u); - } - { - const uint32_t bitsPerComponent = 8u; - const uint32_t discardBits = 32u - bitsPerComponent; - const uint32_t fullWidthMultiplier = (1u << 31u) - 1u; - uint32_t4 stored; - for (uint32_t i = 0; i < 4; i++) - stored[i] = uint32_t(testInput.unorm4[i] * fullWidthMultiplier) >> discardBits; - expected.unorm4_pre_u32 = float32_t4(stored ^ testInput.scrambleKey4) * bit_cast(0x2f800004u); - } - { - const uint32_t bitsPerComponent = 8u; - const uint32_t discardBits = 32u - bitsPerComponent; - const uint32_t multiplier = (1u << bitsPerComponent) - 1u; - uint32_t4 stored, scrambleKey; - for (uint32_t i = 0; i < 4; i++) - { - stored[i] = uint32_t(testInput.unorm4[i] * multiplier) >> discardBits; - scrambleKey[i] = testInput.scrambleKey4[i] >> discardBits; - } - expected.unorm4_post_u32 = float32_t4(stored ^ scrambleKey) * bit_cast(0x3b808081u); - } - - // test encode/decode uint32_tN storage, dim == N - { - const uint32_t fullWidthMultiplier = (1u << 31u) - 1u; - uint32_t2 stored; - for (uint32_t i = 0; i < 2; i++) - stored[i] = uint32_t(testInput.unorm2[i] * fullWidthMultiplier); - expected.unorm2_pre_u32t2 = float32_t2(stored ^ testInput.scrambleKey2) * bit_cast(0x2f800004u); - } - { - const uint32_t multiplier = (1u << 31u) - 1u; - uint32_t2 stored; - for (uint32_t i = 0; i < 2; i++) - stored[i] = uint32_t(testInput.unorm2[i] * multiplier); - expected.unorm2_post_u32t2 = float32_t2(stored ^ testInput.scrambleKey2) * bit_cast(0x2f800004u); - } - { - const uint32_t fullWidthMultiplier = (1u << 31u) - 1u; - uint32_t3 stored; - for (uint32_t i = 0; i < 3; i++) - stored[i] = uint32_t(testInput.unorm3[i] * fullWidthMultiplier); - expected.unorm3_pre_u32t3 = float32_t3(stored ^ testInput.scrambleKey3) * bit_cast(0x2f800004u); - } - { - const uint32_t multiplier = (1u << 31u) - 1u; - uint32_t3 stored; - for (uint32_t i = 0; i < 3; i++) - stored[i] = uint32_t(testInput.unorm3[i] * multiplier); - expected.unorm3_post_u32t3 = float32_t3(stored ^ testInput.scrambleKey3) * bit_cast(0x2f800004u); - } - { - const uint32_t fullWidthMultiplier = (1u << 31u) - 1u; - uint32_t4 stored; - for (uint32_t i = 0; i < 4; i++) - stored[i] = uint32_t(testInput.unorm4[i] * fullWidthMultiplier); - expected.unorm4_pre_u32t4 = float32_t4(stored ^ testInput.scrambleKey4) * bit_cast(0x2f800004u); - } - { - const uint32_t multiplier = (1u << 31u) - 1u; - uint32_t4 stored; - for (uint32_t i = 0; i < 4; i++) - stored[i] = uint32_t(testInput.unorm4[i] * multiplier); - expected.unorm4_post_u32t4 = float32_t4(stored ^ testInput.scrambleKey4) * bit_cast(0x2f800004u); - } - - // test encode/decode uint32_t2 storage, dim 3 - { - const uint32_t bitsPerComponent = 21u; - const uint32_t discardBits = 32u - bitsPerComponent; - const uint32_t fullWidthMultiplier = (1u << 31u) - 1u; - uint32_t3 stored; - for (uint32_t i = 0; i < 3; i++) - stored[i] = uint32_t(testInput.unorm3[i] * fullWidthMultiplier) >> discardBits; - expected.unorm3_pre_u32t2 = float32_t3(stored ^ testInput.scrambleKey3) * bit_cast(0x2f800004u); - } - { - const uint32_t bitsPerComponent = 21u; - const uint32_t discardBits = 32u - bitsPerComponent; - const uint32_t multiplier = (1u << bitsPerComponent) - 1u; - uint32_t3 stored, scrambleKey; - for (uint32_t i = 0; i < 3; i++) - { - stored[i] = uint32_t(testInput.unorm3[i] * multiplier) >> discardBits; - scrambleKey[i] = testInput.scrambleKey3[i] >> discardBits; - } - expected.unorm3_post_u32t2 = float32_t3(stored ^ scrambleKey) * bit_cast(0x35000004u); - } - - return expected; - } - - bool verifyTestResults(const QuantizedSequenceTestValues& expectedTestValues, const QuantizedSequenceTestValues& testValues, const size_t testIteration, const uint32_t seed, TestType testType) override - { - bool pass = true; - pass &= verifyTestValue("get uint from u32", expectedTestValues.uintDim1, testValues.uintDim1, testIteration, seed, testType); - pass &= verifyTestValue("get uint2 from u32", expectedTestValues.uintDim2, testValues.uintDim2, testIteration, seed, testType); - pass &= verifyTestValue("get uint3 from u32", expectedTestValues.uintDim3, testValues.uintDim3, testIteration, seed, testType); - pass &= verifyTestValue("get uint4 from u32", expectedTestValues.uintDim4, testValues.uintDim4, testIteration, seed, testType); - - pass &= verifyTestValue("get uint2 from u32 vec2", expectedTestValues.uintVec2_Dim2, testValues.uintVec2_Dim2, testIteration, seed, testType); - pass &= verifyTestValue("get uint3 from u32 vec2", expectedTestValues.uintVec2_Dim3, testValues.uintVec2_Dim3, testIteration, seed, testType); - - pass &= verifyTestValue("get uint3 from u32 vec3", expectedTestValues.uintVec3_Dim3, testValues.uintVec3_Dim3, testIteration, seed, testType); - pass &= verifyTestValue("get uint4 from u32 vec4", expectedTestValues.uintVec4_Dim4, testValues.uintVec4_Dim4, testIteration, seed, testType); - - pass &= verifyTestValue("get uint from u16", expectedTestValues.u16Dim1, testValues.u16Dim1, testIteration, seed, testType); - pass &= verifyTestValue("get uint2 from u16", expectedTestValues.u16Dim2, testValues.u16Dim2, testIteration, seed, testType); - pass &= verifyTestValue("get uint3 from u16", expectedTestValues.u16Dim3, testValues.u16Dim3, testIteration, seed, testType); - pass &= verifyTestValue("get uint4 from u16", expectedTestValues.u16Dim3, testValues.u16Dim3, testIteration, seed, testType); - - pass &= verifyTestValue("get uint2 from u16 vec2", expectedTestValues.u16Vec2_Dim2, testValues.u16Vec2_Dim2, testIteration, seed, testType); - pass &= verifyTestValue("get uint4 from u16 vec2", expectedTestValues.u16Vec2_Dim4, testValues.u16Vec2_Dim4, testIteration, seed, testType); - - pass &= verifyTestValue("get uint3 from u16 vec3", expectedTestValues.u16Vec3_Dim3, testValues.u16Vec3_Dim3, testIteration, seed, testType); - pass &= verifyTestValue("get uint4 from u16 vec4", expectedTestValues.u16Vec4_Dim4, testValues.u16Vec4_Dim4, testIteration, seed, testType); - - pass &= verifyTestValue("encode/decode unorm from u32 (fullwidth)", expectedTestValues.unorm1_pre_u32, testValues.unorm1_pre_u32, testIteration, seed, testType); - pass &= verifyTestValue("encode/decode unorm from u32", expectedTestValues.unorm1_post_u32, testValues.unorm1_post_u32, testIteration, seed, testType); - pass &= verifyTestValue("encode/decode unorm2 from u32 (fullwidth)", expectedTestValues.unorm2_pre_u32, testValues.unorm2_pre_u32, testIteration, seed, testType); - pass &= verifyTestValue("encode/decode unorm2 from u32", expectedTestValues.unorm2_post_u32, testValues.unorm2_post_u32, testIteration, seed, testType); - pass &= verifyTestValue("encode/decode unorm3 from u32 (fullwidth)", expectedTestValues.unorm3_pre_u32, testValues.unorm3_pre_u32, testIteration, seed, testType); - pass &= verifyTestValue("encode/decode unorm3 from u32", expectedTestValues.unorm3_post_u32, testValues.unorm3_post_u32, testIteration, seed, testType); - pass &= verifyTestValue("encode/decode unorm4 from u32 (fullwidth)", expectedTestValues.unorm4_pre_u32, testValues.unorm4_pre_u32, testIteration, seed, testType); - pass &= verifyTestValue("encode/decode unorm4 from u32", expectedTestValues.unorm4_post_u32, testValues.unorm4_post_u32, testIteration, seed, testType); - - pass &= verifyTestValue("encode/decode unorm2 from u32 vec2 (fullwidth)", expectedTestValues.unorm2_pre_u32t2, testValues.unorm2_pre_u32t2, testIteration, seed, testType); - pass &= verifyTestValue("encode/decode unorm2 from u32 vec2", expectedTestValues.unorm2_post_u32t2, testValues.unorm2_post_u32t2, testIteration, seed, testType); - pass &= verifyTestValue("encode/decode unorm3 from u32 vec3 (fullwidth)", expectedTestValues.unorm3_pre_u32t3, testValues.unorm3_pre_u32t3, testIteration, seed, testType); - pass &= verifyTestValue("encode/decode unorm3 from u32 vec3", expectedTestValues.unorm3_post_u32t3, testValues.unorm3_post_u32t3, testIteration, seed, testType); - pass &= verifyTestValue("encode/decode unorm4 from u32 vec4 (fullwidth)", expectedTestValues.unorm4_pre_u32t4, testValues.unorm4_pre_u32t4, testIteration, seed, testType); - pass &= verifyTestValue("encode/decode unorm4 from u32 vec4", expectedTestValues.unorm4_post_u32t4, testValues.unorm4_post_u32t4, testIteration, seed, testType); - - pass &= verifyTestValue("encode/decode unorm3 from u32 vec2 (fullwidth)", expectedTestValues.unorm3_pre_u32t2, testValues.unorm3_pre_u32t2, testIteration, seed, testType); - pass &= verifyTestValue("encode/decode unorm3 from u32 vec2", expectedTestValues.unorm3_post_u32t2, testValues.unorm3_post_u32t2, testIteration, seed, testType); - - return pass; - } - -}; - -#endif diff --git a/74_QuantizedSequenceTests/app_resources/common.hlsl b/74_QuantizedSequenceTests/app_resources/common.hlsl deleted file mode 100644 index d19ed0c60..000000000 --- a/74_QuantizedSequenceTests/app_resources/common.hlsl +++ /dev/null @@ -1,253 +0,0 @@ -//// Copyright (C) 2023-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 - -#ifndef _NBL_EXAMPLES_TESTS_74_QUANTIZED_SEQUENCE_COMMON_INCLUDED_ -#define _NBL_EXAMPLES_TESTS_74_QUANTIZED_SEQUENCE_COMMON_INCLUDED_ - -#include - -using namespace nbl::hlsl; -struct QuantizedSequenceInputTestValues -{ - uint32_t scalar; - uint32_t2 uvec2; - uint32_t3 uvec3; - uint32_t4 uvec4; - - uint16_t scalar16; - uint16_t2 u16vec2; - uint16_t3 u16vec3; - uint16_t4 u16vec4; - - float32_t1 unorm1; - float32_t2 unorm2; - float32_t3 unorm3; - float32_t4 unorm4; - - uint32_t1 scrambleKey1; - uint32_t2 scrambleKey2; - uint32_t3 scrambleKey3; - uint32_t4 scrambleKey4; -}; - -struct QuantizedSequenceTestValues -{ - uint32_t uintDim1; - uint32_t2 uintDim2; - uint32_t3 uintDim3; - uint32_t4 uintDim4; - - uint32_t2 uintVec2_Dim2; - uint32_t3 uintVec2_Dim3; - - uint32_t3 uintVec3_Dim3; - uint32_t4 uintVec4_Dim4; - - uint16_t u16Dim1; - uint16_t2 u16Dim2; - uint16_t3 u16Dim3; - uint16_t4 u16Dim4; - - uint16_t2 u16Vec2_Dim2; - uint16_t4 u16Vec2_Dim4; - - uint16_t3 u16Vec3_Dim3; - uint16_t4 u16Vec4_Dim4; - - // pre decode scramble - float32_t1 unorm1_pre_u32; - float32_t2 unorm2_pre_u32; - float32_t3 unorm3_pre_u32; - float32_t4 unorm4_pre_u32; - - float32_t2 unorm2_pre_u32t2; - float32_t3 unorm3_pre_u32t3; - float32_t4 unorm4_pre_u32t4; - - float32_t3 unorm3_pre_u32t2; - - // post decode scramble - float32_t1 unorm1_post_u32; - float32_t2 unorm2_post_u32; - float32_t3 unorm3_post_u32; - float32_t4 unorm4_post_u32; - - float32_t2 unorm2_post_u32t2; - float32_t3 unorm3_post_u32t3; - float32_t4 unorm4_post_u32t4; - - float32_t3 unorm3_post_u32t2; -}; - -struct QuantizedSequenceTestExecutor -{ - void operator()(NBL_CONST_REF_ARG(QuantizedSequenceInputTestValues) input, NBL_REF_ARG(QuantizedSequenceTestValues) output) - { - // test get/set/create - { - sampling::QuantizedSequence qs = sampling::QuantizedSequence::create(input.scalar); - output.uintDim1 = qs.get(0); - } - { - sampling::QuantizedSequence qs = sampling::QuantizedSequence::create(input.uvec2); - for (uint32_t i = 0; i < 2; i++) - output.uintDim2[i] = qs.get(i); - } - { - sampling::QuantizedSequence qs = sampling::QuantizedSequence::create(input.uvec3); - for (uint32_t i = 0; i < 3; i++) - output.uintDim3[i] = qs.get(i); - } - { - sampling::QuantizedSequence qs = sampling::QuantizedSequence::create(input.uvec4); - for (uint32_t i = 0; i < 4; i++) - output.uintDim4[i] = qs.get(i); - } - - { - sampling::QuantizedSequence qs = sampling::QuantizedSequence::create(input.uvec2); - for (uint32_t i = 0; i < 2; i++) - output.uintVec2_Dim2[i] = qs.get(i); - } - { - sampling::QuantizedSequence qs = sampling::QuantizedSequence::create(input.uvec3); - for (uint32_t i = 0; i < 3; i++) - output.uintVec2_Dim3[i] = qs.get(i); - } - - { - sampling::QuantizedSequence qs = sampling::QuantizedSequence::create(input.uvec3); - for (uint32_t i = 0; i < 3; i++) - output.uintVec3_Dim3[i] = qs.get(i); - } - { - sampling::QuantizedSequence qs = sampling::QuantizedSequence::create(input.uvec4); - for (uint32_t i = 0; i < 4; i++) - output.uintVec4_Dim4[i] = qs.get(i); - } - - // u16 - { - sampling::QuantizedSequence qs = sampling::QuantizedSequence::create(input.scalar16); - output.u16Dim1 = qs.get(0); - } - { - sampling::QuantizedSequence qs = sampling::QuantizedSequence::create(input.u16vec2); - for (uint32_t i = 0; i < 2; i++) - output.u16Dim2[i] = qs.get(i); - } - { - sampling::QuantizedSequence qs = sampling::QuantizedSequence::create(input.u16vec3); - for (uint32_t i = 0; i < 3; i++) - output.u16Dim3[i] = qs.get(i); - } - { - sampling::QuantizedSequence qs = sampling::QuantizedSequence::create(input.u16vec4); - for (uint32_t i = 0; i < 4; i++) - output.u16Dim4[i] = qs.get(i); - } - - { - sampling::QuantizedSequence qs = sampling::QuantizedSequence::create(input.u16vec2); - for (uint32_t i = 0; i < 2; i++) - output.u16Vec2_Dim2[i] = qs.get(i); - } - { - sampling::QuantizedSequence qs = sampling::QuantizedSequence::create(input.u16vec4); - for (uint32_t i = 0; i < 4; i++) - output.u16Vec2_Dim4[i] = qs.get(i); - } - - { - sampling::QuantizedSequence qs = sampling::QuantizedSequence::create(input.u16vec3); - for (uint32_t i = 0; i < 3; i++) - output.u16Vec3_Dim3[i] = qs.get(i); - } - { - sampling::QuantizedSequence qs = sampling::QuantizedSequence::create(input.u16vec4); - for (uint32_t i = 0; i < 4; i++) - output.u16Vec4_Dim4[i] = qs.get(i); - } - - // test encode/decode uint32, dim 1..4 - { - sampling::QuantizedSequence qs = sampling::QuantizedSequence::template encode(input.unorm1); - output.unorm1_pre_u32 = qs.template decode(input.scrambleKey1); - } - { - sampling::QuantizedSequence qs = sampling::QuantizedSequence::template encode(input.unorm1); - sampling::QuantizedSequence key = sampling::QuantizedSequence::create(input.scrambleKey1[0]); - output.unorm1_post_u32 = qs.template decode(key); - } - { - sampling::QuantizedSequence qs = sampling::QuantizedSequence::template encode(input.unorm2); - output.unorm2_pre_u32 = qs.template decode(input.scrambleKey2); - } - { - sampling::QuantizedSequence qs = sampling::QuantizedSequence::template encode(input.unorm2); - sampling::QuantizedSequence key = sampling::QuantizedSequence::create(input.scrambleKey2); - output.unorm2_post_u32 = qs.template decode(key); - } - { - sampling::QuantizedSequence qs = sampling::QuantizedSequence::template encode(input.unorm3); - output.unorm3_pre_u32 = qs.template decode(input.scrambleKey3); - } - { - sampling::QuantizedSequence qs = sampling::QuantizedSequence::template encode(input.unorm3); - sampling::QuantizedSequence key = sampling::QuantizedSequence::create(input.scrambleKey3); - output.unorm3_post_u32 = qs.template decode(key); - } - { - sampling::QuantizedSequence qs = sampling::QuantizedSequence::template encode(input.unorm4); - output.unorm4_pre_u32 = qs.template decode(input.scrambleKey4); - } - { - sampling::QuantizedSequence qs = sampling::QuantizedSequence::template encode(input.unorm4); - sampling::QuantizedSequence key = sampling::QuantizedSequence::create(input.scrambleKey4); - output.unorm4_post_u32 = qs.template decode(key); - } - - // test encode/decode uint32_tN storage, dim == N - { - sampling::QuantizedSequence qs = sampling::QuantizedSequence::template encode(input.unorm2); - output.unorm2_pre_u32t2 = qs.template decode(input.scrambleKey2); - } - { - sampling::QuantizedSequence qs = sampling::QuantizedSequence::template encode(input.unorm2); - sampling::QuantizedSequence key = sampling::QuantizedSequence::create(input.scrambleKey2); - output.unorm2_post_u32t2 = qs.template decode(key); - } - { - sampling::QuantizedSequence qs = sampling::QuantizedSequence::template encode(input.unorm3); - output.unorm3_pre_u32t3 = qs.template decode(input.scrambleKey3); - } - { - sampling::QuantizedSequence qs = sampling::QuantizedSequence::template encode(input.unorm3); - sampling::QuantizedSequence key = sampling::QuantizedSequence::create(input.scrambleKey3); - output.unorm3_post_u32t3 = qs.template decode(key); - } - { - sampling::QuantizedSequence qs = sampling::QuantizedSequence::template encode(input.unorm4); - output.unorm4_pre_u32t4 = qs.template decode(input.scrambleKey4); - } - { - sampling::QuantizedSequence qs = sampling::QuantizedSequence::template encode(input.unorm4); - sampling::QuantizedSequence key = sampling::QuantizedSequence::create(input.scrambleKey4); - output.unorm4_post_u32t4 = qs.template decode(key); - } - - // test encode/decode uint32_t2 storage, dim 3 - { - sampling::QuantizedSequence qs = sampling::QuantizedSequence::template encode(input.unorm3); - output.unorm3_pre_u32t2 = qs.template decode(input.scrambleKey3); - } - { - sampling::QuantizedSequence qs = sampling::QuantizedSequence::template encode(input.unorm3); - sampling::QuantizedSequence key = sampling::QuantizedSequence::create(input.scrambleKey3); - output.unorm3_post_u32t2 = qs.template decode(key); - } - } -}; - -#endif diff --git a/74_QuantizedSequenceTests/app_resources/quantizedSequenceTest.comp.hlsl b/74_QuantizedSequenceTests/app_resources/quantizedSequenceTest.comp.hlsl deleted file mode 100644 index 50a58bdde..000000000 --- a/74_QuantizedSequenceTests/app_resources/quantizedSequenceTest.comp.hlsl +++ /dev/null @@ -1,19 +0,0 @@ -//// Copyright (C) 2023-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 shader_stage(compute) - -#include "common.hlsl" -#include - -[[vk::binding(0, 0)]] RWStructuredBuffer inputTestValues; -[[vk::binding(1, 0)]] RWStructuredBuffer outputTestValues; - -[numthreads(256, 1, 1)] -[shader("compute")] -void main() -{ - const uint invID = nbl::hlsl::glsl::gl_GlobalInvocationID().x; - QuantizedSequenceTestExecutor executor; - executor(inputTestValues[invID], outputTestValues[invID]); -} \ No newline at end of file diff --git a/74_QuantizedSequenceTests/main.cpp b/74_QuantizedSequenceTests/main.cpp deleted file mode 100644 index dbba8a35f..000000000 --- a/74_QuantizedSequenceTests/main.cpp +++ /dev/null @@ -1,72 +0,0 @@ -// 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 "nbl/this_example/builtin/build/spirv/keys.hpp" - -#include "app_resources/common.hlsl" - -#include "CQuantizedSequenceTester.h" - -#include -#include -#include - - -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; - -//using namespace glm; - -class QuantizedSequenceTest final : public application_templates::MonoDeviceApplication, public BuiltinResourcesApplication -{ - using device_base_t = application_templates::MonoDeviceApplication; - using asset_base_t = BuiltinResourcesApplication; -public: - QuantizedSequenceTest(const path& _localInputCWD, const path& _localOutputCWD, const path& _sharedInputCWD, const path& _sharedOutputCWD) : - IApplicationFramework(_localInputCWD, _localOutputCWD, _sharedInputCWD, _sharedOutputCWD) {} - - bool onAppInitialized(smart_refctd_ptr&& system) override - { - // Remember to call the base class initialization! - if (!device_base_t::onAppInitialized(smart_refctd_ptr(system))) - return false; - if (!asset_base_t::onAppInitialized(std::move(system))) - return false; - - { - CQuantizedSequenceTester::PipelineSetupData pplnSetupData; - pplnSetupData.device = m_device; - pplnSetupData.api = m_api; - pplnSetupData.assetMgr = m_assetMgr; - pplnSetupData.logger = m_logger; - pplnSetupData.physicalDevice = m_physicalDevice; - pplnSetupData.computeFamilyIndex = getComputeQueue()->getFamilyIndex(); - pplnSetupData.shaderKey = nbl::this_example::builtin::build::get_spirv_key<"quantizedSequenceTest">(m_device.get()); - - CQuantizedSequenceTester quantizedSequenceTester(8); - quantizedSequenceTester.setupPipeline(pplnSetupData); - if (!quantizedSequenceTester.performTestsAndVerifyResults("QuantizedSequenceTestLog.txt")) - return false; - } - - // In contrast to fences, we just need one semaphore to rule all dispatches - return true; - } - - void onAppTerminated_impl() override - { - m_device->waitIdle(); - } - - void workLoopBody() override {} - - bool keepRunning() override { return false; } -}; - -NBL_MAIN_FUNC(QuantizedSequenceTest) diff --git a/CMakeLists.txt b/CMakeLists.txt index 17c9c4999..cfecd4a98 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,137 +1,86 @@ -# Copyright (C) 2018-2025 - DevSH Graphics Programming Sp. z O.O. +# 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 -if(NBL_BUILD_EXAMPLES) - project(NablaExamples) - - if(NBL_BUILD_ANDROID) - nbl_android_create_media_storage_apk() +function(NBL_HOOK_COMMON_API NBL_EXCLUDE_TARGETS_LIST) + if(NOT TARGET nblCommonAPI) + message(FATAL_ERROR "nblCommonAPI not defined!") endif() - #! Common api library & precompiled headers for Nabla framework examples - add_subdirectory(common EXCLUDE_FROM_ALL) + NBL_GET_ALL_TARGETS(NBL_TARGETS) - #! use "EXCLUDE_FROM_ALL" to exclude an example from the NablaExamples project - #[[ - useful if we don't want the example to be tested by CI but still want - the example's project to be generated + foreach(NBL_TARGET IN LISTS NBL_TARGETS) + # TODO: exclude builtin targets created by examples as well - doesn't impact anything at all now + if(NOT ${NBL_TARGET} IN_LIST NBL_EXCLUDE_TARGETS_LIST) - https://cmake.org/cmake/help/latest/prop_tgt/EXCLUDE_FROM_ALL.html - ]] + target_include_directories(${NBL_TARGET} PRIVATE $) + target_link_libraries(${NBL_TARGET} PRIVATE nblCommonAPI) + endif() + endforeach() +endfunction() + +# PCH & CommonAPI library for Nabla framework examples +add_subdirectory(common EXCLUDE_FROM_ALL) + +if(NBL_BUILD_EXAMPLES) + if(NBL_BUILD_ANDROID) + nbl_android_create_media_storage_apk() + endif() # showcase the use of `nbl::core`,`nbl::system` and `nbl::asset` - add_subdirectory(01_HelloCoreSystemAsset) + add_subdirectory(01_HelloCoreSystemAsset EXCLUDE_FROM_ALL) # showcase the use of `system::IApplicationFramework` and `nbl::video` - add_subdirectory(02_HelloCompute) + add_subdirectory(02_HelloCompute EXCLUDE_FROM_ALL) # showcase physical device selection, resource embedding and the use of identical headers in HLSL and C++ - add_subdirectory(03_DeviceSelectionAndSharedSources) + add_subdirectory(03_DeviceSelectionAndSharedSources EXCLUDE_FROM_ALL) # showcase the creation of windows and polling for input - add_subdirectory(04_HelloUI) + add_subdirectory(04_HelloUI EXCLUDE_FROM_ALL) # showcase the semi-advanced use of Nabla's Streaming Buffers and BDA - add_subdirectory(05_StreamingAndBufferDeviceAddressApp) + add_subdirectory(05_StreamingAndBufferDeviceAddressApp EXCLUDE_FROM_ALL) # showcase the use of a graphics queue - add_subdirectory(06_HelloGraphicsQueue) + add_subdirectory(06_HelloGraphicsQueue EXCLUDE_FROM_ALL) # showcase the set-up of multiple queues - add_subdirectory(07_StagingAndMultipleQueues) + add_subdirectory(07_StagingAndMultipleQueues EXCLUDE_FROM_ALL) # showcase the set-up of a swapchain and picking of a matching device - add_subdirectory(08_HelloSwapchain) - add_subdirectory(09_GeometryCreator) - # demonstrate the counting sort utility - add_subdirectory(10_CountingSort) - # showcase use of FFT for post-FX Bloom effect - add_subdirectory(11_FFT) - # - add_subdirectory(12_MeshLoaders) - # - add_subdirectory(13_MaterialCompilerTest) - # - add_subdirectory(14_Mortons) - # - if (NBL_BUILD_MITSUBA_LOADER) - add_subdirectory(15_MitsubaLoader) - endif() - add_subdirectory(16_ZipArchiveLoaderTest) - + add_subdirectory(08_HelloSwapchain EXCLUDE_FROM_ALL) + add_subdirectory(09_GeometryCreator EXCLUDE_FROM_ALL) + # demonstrate the counting sort utility + add_subdirectory(10_CountingSort EXCLUDE_FROM_ALL) # Waiting for a refactor - #add_subdirectory(27_PLYSTLDemo) - #add_subdirectory(33_Draw3DLine) + #add_subdirectory(27_PLYSTLDemo EXCLUDE_FROM_ALL) + #add_subdirectory(29_SpecializationConstants EXCLUDE_FROM_ALL) + #add_subdirectory(33_Draw3DLine EXCLUDE_FROM_ALL) # Unit Test Examples - add_subdirectory(20_AllocatorTest) - add_subdirectory(21_LRUCacheUnitTest) - add_subdirectory(22_CppCompat) - add_subdirectory(23_Arithmetic2UnitTest) - add_subdirectory(24_ColorSpaceTest) + add_subdirectory(20_AllocatorTest EXCLUDE_FROM_ALL) + add_subdirectory(21_LRUCacheUnitTest EXCLUDE_FROM_ALL) + add_subdirectory(22_CppCompat EXCLUDE_FROM_ALL) + add_subdirectory(23_ArithmeticUnitTest EXCLUDE_FROM_ALL) + add_subdirectory(24_ColorSpaceTest EXCLUDE_FROM_ALL) add_subdirectory(25_FilterTest EXCLUDE_FROM_ALL) - add_subdirectory(26_Blur) - add_subdirectory(27_MPMCScheduler) - add_subdirectory(28_FFTBloom) - add_subdirectory(29_Arithmetic2Bench) - # add_subdirectory(36_CUDAInterop) - - add_subdirectory(31_HLSLPathTracer) - add_subdirectory(34_DebugDraw) - - add_subdirectory(38_EXRSplit) - if (NBL_BUILD_MITSUBA_LOADER) - # if (NBL_BUILD_OPTIX) - # add_subdirectory(39_DenoiserTonemapper) - # endif() - if(NBL_BUILD_IMGUI) - add_subdirectory(40_PathTracer) - endif() - endif() - - #add_subdirectory(43_SumAndCDFFilters) + add_subdirectory(27_MPMCScheduler EXCLUDE_FROM_ALL) + # add_subdirectory(36_CUDAInterop EXCLUDE_FROM_ALL) + + add_subdirectory(38_EXRSplit EXCLUDE_FROM_ALL) + # if (NBL_BUILD_MITSUBA_LOADER AND NBL_BUILD_OPTIX) + # add_subdirectory(39_DenoiserTonemapper EXCLUDE_FROM_ALL) + # endif() + + add_subdirectory(42_FragmentShaderPathTracer EXCLUDE_FROM_ALL) + #add_subdirectory(43_SumAndCDFFilters EXCLUDE_FROM_ALL) + #add_subdirectory(45_BRDFEvalTest EXCLUDE_FROM_ALL) + #add_subdirectory(46_SamplingValidation EXCLUDE_FROM_ALL) add_subdirectory(47_DerivMapTest EXCLUDE_FROM_ALL) - add_subdirectory(50.IESViewer) + add_subdirectory(53_ComputeShaders EXCLUDE_FROM_ALL) add_subdirectory(54_Transformations EXCLUDE_FROM_ALL) add_subdirectory(55_RGB18E7S3 EXCLUDE_FROM_ALL) - add_subdirectory(59_QuaternionTests) - add_subdirectory(61_UI) - add_subdirectory(62_CAD EXCLUDE_FROM_ALL) # TODO: Erfan, Przemek, Francisco and co. need to resurrect this + add_subdirectory(56_RayQuery EXCLUDE_FROM_ALL) + add_subdirectory(60_ClusteredRendering EXCLUDE_FROM_ALL) + add_subdirectory(61_UI EXCLUDE_FROM_ALL) + add_subdirectory(62_CAD EXCLUDE_FROM_ALL) add_subdirectory(62_SchusslerTest EXCLUDE_FROM_ALL) - add_subdirectory(64_EmulatedFloatTest) add_subdirectory(0_ImportanceSamplingEnvMaps EXCLUDE_FROM_ALL) #TODO: integrate back into 42 - add_subdirectory(66_HLSLBxDFTests EXCLUDE_FROM_ALL) - add_subdirectory(67_RayQueryGeometry) - add_subdirectory(68_JpegLoading) - - add_subdirectory(70_FLIPFluids) - add_subdirectory(71_RayTracingPipeline) - add_subdirectory(72_CooperativeBinarySearch) - - if (NBL_BUILD_MITSUBA_LOADER) - add_subdirectory(73_GeometryInspector) - endif() - - add_subdirectory(74_QuantizedSequenceTests) - - # add new examples *before* NBL_GET_ALL_TARGETS invocation, it gathers recursively all targets created so far in this subdirectory - NBL_GET_ALL_TARGETS(TARGETS) - - # we want to loop only over the examples so we exclude examples' interface libraries created in common subdirectory - list(REMOVE_ITEM TARGETS ${NBL_EXAMPLES_API_TARGET} ${NBL_EXAMPLES_API_LIBRARIES}) - - # 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) - target_link_libraries(${T} PUBLIC ${NBL_EXAMPLES_API_TARGET}) - target_include_directories(${T} PUBLIC $) - set_target_properties(${T} PROPERTIES DISABLE_PRECOMPILE_HEADERS OFF) - target_precompile_headers(${T} REUSE_FROM "${NBL_EXAMPLES_API_TARGET}") - - if(NBL_EMBED_BUILTIN_RESOURCES) - LINK_BUILTIN_RESOURCES_TO_TARGET(${T} NblExtExamplesAPIBuiltinsSource) - LINK_BUILTIN_RESOURCES_TO_TARGET(${T} NblExtExamplesAPIBuiltinsInclude) - LINK_BUILTIN_RESOURCES_TO_TARGET(${T} NblExtExamplesAPIBuiltinsBuild) - endif() - endif() - endforeach() - - NBL_ADJUST_FOLDERS(examples) + NBL_HOOK_COMMON_API("${NBL_COMMON_API_TARGETS}") endif() \ No newline at end of file diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index b91dfc91e..d9073f273 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -1,109 +1,28 @@ -#! Examples API proxy library -#[[ - We create the Nabla Examples API as a static library extension, this - allows all examples to reuse a single precompiled header (PCH) - instead of generating their own +########################################### +# TODO: the way it should work is following (remove the comment once all done!) +# - one top PCH which includes -> currently not done +# - sources used only within examples splitted into "common libraries" (optional -> with options to toggle if include them to build tree), each common library should reuse the above top PCH +# - examples_tests CMake loop over example targets and hook the interface library with NBL_HOOK_COMMON_API [done] +# - each common library should declare ONLY interface and never expose source definition into headers nor any 3rdparty stuff! +## - The PCH includes Nabla.h + example common interface headers and takes - around 1 GB per configuration, so sharing it avoids significant disk space waste -]] +# interface libraries don't have build rules (except custom commands however it doesn't matter here) but properties +add_library(nblCommonAPI INTERFACE) +set(NBL_COMMON_API_INCLUDE_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/include") +target_include_directories(nblCommonAPI INTERFACE "${NBL_COMMON_API_INCLUDE_DIRECTORY}") -nbl_create_ext_library_project(ExamplesAPI "" "${CMAKE_CURRENT_SOURCE_DIR}/src/nbl/examples/pch.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/include" "" "") +add_subdirectory(src EXCLUDE_FROM_ALL) -set_target_properties(${LIB_NAME} PROPERTIES DISABLE_PRECOMPILE_HEADERS OFF) -target_precompile_headers(${LIB_NAME} PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include/nbl/examples/PCH.hpp") +########## <- +# TODO: disable this CommonPCH thing! + DEPRICATED! +# TODO: move asset converer into separate library -set(COMMON_INCLUDE_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/include") +nbl_create_ext_library_project(CommonAPI "" "${CMAKE_CURRENT_SOURCE_DIR}/src/empty.cpp" "" "" "") +set(NBL_EXECUTABLE_COMMON_API_TARGET "${LIB_NAME}" CACHE INTERNAL "") -function(INTERFACE_TO_BUILTINS TARGET) - #[[ - even though builtin target is static library its still valid to reuse - common PCH to boost its build speed to not preprocess entire Nabla again (**) - ]] - set_target_properties(${TARGET} PROPERTIES DISABLE_PRECOMPILE_HEADERS OFF) - target_precompile_headers(${TARGET} REUSE_FROM "${LIB_NAME}") +add_subdirectory(CommonPCH EXCLUDE_FROM_ALL) - target_include_directories(${TARGET} PUBLIC "${COMMON_INCLUDE_DIRECTORY}") - target_link_libraries(${TARGET} INTERFACE ${LIB_NAME}) -endfunction() +#target_precompile_headers("${NBL_EXECUTABLE_COMMON_API_TARGET}" REUSE_FROM "${NBL_EXECUTABLE_PROJECT_CREATION_PCH_TARGET}") +########## <- -function(REGISTER_COMMON_BUILTINS) - cmake_parse_arguments(EX "" "TARGET;BIND;NAMESPACE" "GLOB_RGX" ${ARGN}) - get_filename_component(MOUNT_POINT "${CMAKE_CURRENT_SOURCE_DIR}/${EX_BIND}" ABSOLUTE) - list(TRANSFORM EX_GLOB_RGX PREPEND "${MOUNT_POINT}/") - file(GLOB_RECURSE KEYS RELATIVE "${MOUNT_POINT}" CONFIGURE_DEPENDS ${EX_GLOB_RGX}) - - NBL_CREATE_RESOURCE_ARCHIVE( - TARGET ${EX_TARGET} - BIND "${MOUNT_POINT}" - BUILTINS ${KEYS} - NAMESPACE ${EX_NAMESPACE} - ) - INTERFACE_TO_BUILTINS(${EX_TARGET}) -endfunction() - -#! common example API builtins as static library targets linked to each example -if(NBL_EMBED_BUILTIN_RESOURCES) - REGISTER_COMMON_BUILTINS( - TARGET NblExtExamplesAPIBuiltinsSource - BIND src/nbl/examples - NAMESPACE nbl::builtin::examples::src - GLOB_RGX *.hlsl *.txt - ) - - REGISTER_COMMON_BUILTINS( - TARGET NblExtExamplesAPIBuiltinsInclude - BIND include/nbl/examples - NAMESPACE nbl::builtin::examples::include - GLOB_RGX *.hpp *.h *.hlsl *.txt - ) -endif() - -#! Examples API common libraries -#[[ - The rule is to avoid creating additional libraries as part of the examples' common - interface in order to prevent generating another precompiled header (PCH) and wasting disk space - - If you have new utilities that could be shared across examples then try to implement them as header only - and include in the PCH or in `examples.h` *if you cannot* (open the header to see details) - - but If you have a good reason to create library because you cannot make it header only - AND you *can REUSE* the examples' PCH then go ahead anyway and put it under `src/nbl/examples`, - otherwise keep it header only - a good example would be to use our embedded-whatever-you-want tool - which does create library but can reuse example's PCH (see NblExtExamplesAPIBuiltinsSource - and NblExtExamplesAPIBuiltinsInclude targets) -]] - -add_subdirectory("src/nbl/examples" EXCLUDE_FROM_ALL) -target_link_libraries(${LIB_NAME} PUBLIC NblExtExamplesAPISPIRV) -if(NBL_EMBED_BUILTIN_RESOURCES) - INTERFACE_TO_BUILTINS(NblExtExamplesAPIBuiltinsBuild) - - #[[ - we have SPIRV keys include file in examples' PCH which then gets REUSE(d) by common archives (**) in built-in mode, - to not glitch compiler we need to ensure we inherit interface properties (include directories needed) for all targets - which share PCH, also note it doest really link any library, the target we inherit properties from is INTERFACE - ]] - target_link_libraries(NblExtExamplesAPIBuiltinsSource PUBLIC NblExtExamplesAPISPIRV) - target_link_libraries(NblExtExamplesAPIBuiltinsInclude PUBLIC NblExtExamplesAPISPIRV) - target_link_libraries(NblExtExamplesAPIBuiltinsBuild PUBLIC NblExtExamplesAPISPIRV) -endif() - -NBL_GET_ALL_TARGETS(TARGETS) -list(REMOVE_ITEM TARGETS ${LIB_NAME}) - -# the Examples API proxy library CMake target name -#[[ - this one gets linked to each executable automatically with its interface libraries -]] -set(NBL_EXAMPLES_API_TARGET ${LIB_NAME} PARENT_SCOPE) - -#! names of CMake targets created in src/nbl/examples -#[[ - if your example wants to use anything from src/nbl/examples - then you must target_link_libraries() the lib you want as we - don't link all those libraries to each executable automatically -]] -set(NBL_EXAMPLES_API_LIBRARIES ${TARGETS} PARENT_SCOPE) - -NBL_ADJUST_FOLDERS(common) \ No newline at end of file +set(NBL_COMMON_API_TARGETS nblCommonAPI ${NBL_COMMON_API_TARGETS} ${NBL_EXECUTABLE_COMMON_API_TARGET} PARENT_SCOPE) diff --git a/common/CommonAPI.h b/common/CommonAPI.h new file mode 100644 index 000000000..aca8c0741 --- /dev/null +++ b/common/CommonAPI.h @@ -0,0 +1,111 @@ +#ifndef __NBL_COMMON_API_H_INCLUDED__ +#define __NBL_COMMON_API_H_INCLUDED__ + +#include + +#include "MonoSystemMonoLoggerApplication.hpp" + +#include "nbl/ui/CGraphicalApplicationAndroid.h" +#include "nbl/ui/CWindowManagerAndroid.h" + +// TODO: see TODO below +// TODO: make these include themselves via `nabla.h` + +#include "nbl/video/utilities/SPhysicalDeviceFilter.h" + +#if 0 +class CommonAPI +{ + CommonAPI() = delete; +public: + class CommonAPIEventCallback : public nbl::ui::IWindow::IEventCallback + { + public: + CommonAPIEventCallback(nbl::core::smart_refctd_ptr&& inputSystem, nbl::system::logger_opt_smart_ptr&& logger) : m_inputSystem(std::move(inputSystem)), m_logger(std::move(logger)), m_gotWindowClosedMsg(false){} + CommonAPIEventCallback() {} + bool isWindowOpen() const {return !m_gotWindowClosedMsg;} + void setLogger(nbl::system::logger_opt_smart_ptr& logger) + { + m_logger = logger; + } + void setInputSystem(nbl::core::smart_refctd_ptr&& inputSystem) + { + m_inputSystem = std::move(inputSystem); + } + private: + + bool onWindowClosed_impl() override + { + m_logger.log("Window closed"); + m_gotWindowClosedMsg = true; + return true; + } + + void onMouseConnected_impl(nbl::core::smart_refctd_ptr&& mch) override + { + m_logger.log("A mouse %p has been connected", nbl::system::ILogger::ELL_INFO, mch.get()); + m_inputSystem.get()->add(m_inputSystem.get()->m_mouse,std::move(mch)); + } + void onMouseDisconnected_impl(nbl::ui::IMouseEventChannel* mch) override + { + m_logger.log("A mouse %p has been disconnected", nbl::system::ILogger::ELL_INFO, mch); + m_inputSystem.get()->remove(m_inputSystem.get()->m_mouse,mch); + } + void onKeyboardConnected_impl(nbl::core::smart_refctd_ptr&& kbch) override + { + m_logger.log("A keyboard %p has been connected", nbl::system::ILogger::ELL_INFO, kbch.get()); + m_inputSystem.get()->add(m_inputSystem.get()->m_keyboard,std::move(kbch)); + } + void onKeyboardDisconnected_impl(nbl::ui::IKeyboardEventChannel* kbch) override + { + m_logger.log("A keyboard %p has been disconnected", nbl::system::ILogger::ELL_INFO, kbch); + m_inputSystem.get()->remove(m_inputSystem.get()->m_keyboard,kbch); + } + + private: + nbl::core::smart_refctd_ptr m_inputSystem = nullptr; + nbl::system::logger_opt_smart_ptr m_logger = nullptr; + bool m_gotWindowClosedMsg; + }; + + // old code from init + { + // ... + + result.inputSystem = nbl::core::make_smart_refctd_ptr(system::logger_opt_smart_ptr(nbl::core::smart_refctd_ptr(result.logger))); + result.assetManager = nbl::core::make_smart_refctd_ptr(nbl::core::smart_refctd_ptr(result.system), nbl::core::smart_refctd_ptr(result.compilerSet)); // we should let user choose it? + + if (!headlessCompute) + { + params.windowCb->setInputSystem(nbl::core::smart_refctd_ptr(result.inputSystem)); + if (!params.window) + { + #ifdef _NBL_PLATFORM_WINDOWS_ + result.windowManager = ui::IWindowManagerWin32::create(); // on the Windows path + #elif defined(_NBL_PLATFORM_LINUX_) + result.windowManager = nbl::core::make_smart_refctd_ptr(); // on the Android path + #else + #error "Unsupported platform" + #endif + + nbl::ui::IWindow::SCreationParams windowsCreationParams; + windowsCreationParams.width = params.windowWidth; + windowsCreationParams.height = params.windowHeight; + windowsCreationParams.x = 64u; + windowsCreationParams.y = 64u; + windowsCreationParams.flags = nbl::ui::IWindow::ECF_RESIZABLE; + windowsCreationParams.windowCaption = params.appName.data(); + windowsCreationParams.callback = params.windowCb; + + params.window = result.windowManager->createWindow(std::move(windowsCreationParams)); + } + params.windowCb = nbl::core::smart_refctd_ptr((CommonAPIEventCallback*) params.window->getEventCallback()); + } + + // ... + } +}; + +#endif + +#endif diff --git a/common/CommonPCH/CMakeLists.txt b/common/CommonPCH/CMakeLists.txt new file mode 100644 index 000000000..5e62f885f --- /dev/null +++ b/common/CommonPCH/CMakeLists.txt @@ -0,0 +1,15 @@ +include(common RESULT_VARIABLE RES) +if(NOT RES) + message(FATAL_ERROR "common.cmake not found. Should be in '${NBL_ROOT_PATH}/cmake' directory") +endif() + +nbl_create_executable_project("" "" "" "" "") + +set(NBL_EXECUTABLE_PROJECT_CREATION_PCH_TARGET "${EXECUTABLE_NAME}" CACHE INTERNAL "") +get_target_property(NBL_NABLA_TARGET_SOURCE_DIR Nabla SOURCE_DIR) +set_target_properties("${EXECUTABLE_NAME}" PROPERTIES DISABLE_PRECOMPILE_HEADERS OFF) +target_precompile_headers("${EXECUTABLE_NAME}" PUBLIC + "${CMAKE_CURRENT_SOURCE_DIR}/PCH.hpp" # Common PCH for examples + "${NBL_NABLA_TARGET_SOURCE_DIR}/pch.h" # Nabla's PCH +) +unset(NBL_NABLA_TARGET_SOURCE_DIR) \ No newline at end of file diff --git a/common/CommonPCH/PCH.hpp b/common/CommonPCH/PCH.hpp new file mode 100644 index 000000000..5b9d6a433 --- /dev/null +++ b/common/CommonPCH/PCH.hpp @@ -0,0 +1,13 @@ +// Copyright (C) 2018-2022 - 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 _EXAMPLES_COMMON_PCH_HPP_ +#define _EXAMPLES_COMMON_PCH_HPP_ + +#include + +#include +#include +#include + +#endif // _EXAMPLES_COMMON_PCH_HPP_ \ No newline at end of file diff --git a/common/CommonPCH/main.cpp b/common/CommonPCH/main.cpp new file mode 100644 index 000000000..c19ee3c45 --- /dev/null +++ b/common/CommonPCH/main.cpp @@ -0,0 +1,9 @@ +// Copyright (C) 2018-2022 - 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 + +int main(int argc, char** argv) +{ + return 0; +} + diff --git a/common/include/nbl/examples/cameras/CCamera.hpp b/common/include/CCamera.hpp similarity index 72% rename from common/include/nbl/examples/cameras/CCamera.hpp rename to common/include/CCamera.hpp index f185e60f6..1b0fe9c0f 100644 --- a/common/include/nbl/examples/cameras/CCamera.hpp +++ b/common/include/CCamera.hpp @@ -1,26 +1,21 @@ // 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_ +#ifndef _CAMERA_IMPL_ +#define _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)) + Camera(const nbl::core::vectorSIMDf& position, const nbl::core::vectorSIMDf& lookat, const nbl::core::matrix4SIMD& 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) @@ -30,7 +25,6 @@ class Camera , rotateSpeed(rotateSpeed) , upVector(upVec) , backupUpVector(backupUpVec) - , viewMatrix(nbl::hlsl::math::linalg::diagonal(1.0f)) { initDefaultKeysMap(); allKeysUp(); @@ -67,15 +61,19 @@ class Camera 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 const nbl::core::matrix4SIMD& getProjectionMatrix() const { return projMatrix; } + inline const nbl::core::matrix3x4SIMD& getViewMatrix() const { return viewMatrix; } + inline const nbl::core::matrix4SIMD& getConcatenatedMatrix() const { return concatMatrix; } - inline void setProjectionMatrix(const nbl::hlsl::float32_t4x4& projection) + inline void setProjectionMatrix(const nbl::core::matrix4SIMD& projection) { projMatrix = projection; - leftHanded = nbl::hlsl::determinant(projMatrix) < 0.f; - concatMatrix = nbl::hlsl::math::linalg::promoted_mul(projMatrix, viewMatrix); + + const auto hlslMatMap = *reinterpret_cast(&projMatrix); // TEMPORARY TILL THE CAMERA CLASS IS REFACTORED TO WORK WITH HLSL MATRICIES! + { + leftHanded = nbl::hlsl::determinant(hlslMatMap) < 0.f; + } + concatMatrix = nbl::core::matrix4SIMD::concatenateBFollowedByAPrecisely(projMatrix, nbl::core::matrix4SIMD(viewMatrix)); } inline void setPosition(const nbl::core::vectorSIMDf& pos) @@ -112,26 +110,22 @@ class Camera 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; + nbl::core::vectorSIMDf pos = position; + nbl::core::vectorSIMDf localTarget = nbl::core::normalize(target - pos); // 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; + 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::convertToHLSLVector(nbl::core::normalize(backupUpVector)); + up = nbl::core::normalize(backupUpVector); if (leftHanded) - viewMatrix = nbl::hlsl::math::linalg::lhLookAt(pos, _target, up); + viewMatrix = nbl::core::matrix3x4SIMD::buildCameraLookAtMatrixLH(pos, target, up); else - viewMatrix = nbl::hlsl::math::linalg::rhLookAt(pos, _target, up); - - concatMatrix = nbl::hlsl::math::linalg::promoted_mul(projMatrix, viewMatrix); + viewMatrix = nbl::core::matrix3x4SIMD::buildCameraLookAtMatrixRH(pos, target, up); + concatMatrix = nbl::core::matrix4SIMD::concatenateBFollowedByAPrecisely(projMatrix, nbl::core::matrix4SIMD(viewMatrix)); } inline bool getLeftHanded() const { return leftHanded; } @@ -152,14 +146,14 @@ class Camera 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; + nbl::core::vectorSIMDf pos = getPosition(); + nbl::core::vectorSIMDf localTarget = 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; + 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; @@ -178,18 +172,13 @@ class Camera 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); + localTarget.set(0,0, nbl::core::max(1.f, nbl::core::length(pos)[0]), 1.f); - 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); + nbl::core::matrix3x4SIMD mat; + mat.setRotation(nbl::core::quaternion(relativeRotationX, relativeRotationY, 0)); + mat.transformVect(localTarget); + + setTarget(localTarget + pos); } } } @@ -209,8 +198,7 @@ class Camera if(keysDown[k]) { auto timeDiff = std::chrono::duration_cast(nextPresentationTimeStamp - lastVirtualUpTimeStamp).count(); - if (timeDiff < 0) - timeDiff = 0; + assert(timeDiff >= 0); perActionDt[k] += timeDiff; } @@ -219,9 +207,8 @@ class Camera 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; + const auto timeDiff = std::chrono::duration_cast(nextPresentationTimeStamp - ev.timeStamp).count(); + assert(timeDiff >= 0); // handle camera movement for (const auto logicalKey : { ECMK_MOVE_FORWARD, ECMK_MOVE_BACKWARD, ECMK_MOVE_LEFT, ECMK_MOVE_RIGHT }) @@ -322,8 +309,8 @@ class Camera 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; + nbl::core::matrix3x4SIMD viewMatrix; + nbl::core::matrix4SIMD concatMatrix, projMatrix; float moveSpeed, rotateSpeed; bool leftHanded, firstUpdate = true, mouseDown = false; @@ -335,4 +322,5 @@ class Camera std::chrono::microseconds nextPresentationTimeStamp, lastVirtualUpTimeStamp; }; -#endif + +#endif // _CAMERA_IMPL_ \ No newline at end of file diff --git a/common/include/CEventCallback.hpp b/common/include/CEventCallback.hpp new file mode 100644 index 000000000..2d4e36932 --- /dev/null +++ b/common/include/CEventCallback.hpp @@ -0,0 +1,49 @@ +#ifndef __NBL_C_EVENT_CALLBACK_HPP_INCLUDED__ +#define __NBL_C_EVENT_CALLBACK_HPP_INCLUDED__ + +#include "nbl/video/utilities/CSimpleResizeSurface.h" +#include "InputSystem.hpp" + +class CEventCallback : public nbl::video::ISimpleManagedSurface::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)) {} + CEventCallback() {} + + void setLogger(nbl::system::logger_opt_smart_ptr& logger) + { + m_logger = logger; + } + void setInputSystem(nbl::core::smart_refctd_ptr&& m_inputSystem) + { + m_inputSystem = std::move(m_inputSystem); + } +private: + + void onMouseConnected_impl(nbl::core::smart_refctd_ptr&& mch) override + { + m_logger.log("A mouse %p has been connected", nbl::system::ILogger::ELL_INFO, mch.get()); + m_inputSystem.get()->add(m_inputSystem.get()->m_mouse, std::move(mch)); + } + void onMouseDisconnected_impl(nbl::ui::IMouseEventChannel* mch) override + { + m_logger.log("A mouse %p has been disconnected", nbl::system::ILogger::ELL_INFO, mch); + m_inputSystem.get()->remove(m_inputSystem.get()->m_mouse, mch); + } + void onKeyboardConnected_impl(nbl::core::smart_refctd_ptr&& kbch) override + { + m_logger.log("A keyboard %p has been connected", nbl::system::ILogger::ELL_INFO, kbch.get()); + m_inputSystem.get()->add(m_inputSystem.get()->m_keyboard, std::move(kbch)); + } + void onKeyboardDisconnected_impl(nbl::ui::IKeyboardEventChannel* kbch) override + { + m_logger.log("A keyboard %p has been disconnected", nbl::system::ILogger::ELL_INFO, kbch); + m_inputSystem.get()->remove(m_inputSystem.get()->m_keyboard, kbch); + } + +private: + nbl::core::smart_refctd_ptr m_inputSystem = nullptr; + nbl::system::logger_opt_smart_ptr m_logger = nullptr; +}; + +#endif // __NBL_C_EVENT_CALLBACK_HPP_INCLUDED__ \ No newline at end of file diff --git a/common/include/CGeomtryCreatorScene.hpp b/common/include/CGeomtryCreatorScene.hpp new file mode 100644 index 000000000..ab90f59b9 --- /dev/null +++ b/common/include/CGeomtryCreatorScene.hpp @@ -0,0 +1,1346 @@ +#ifndef _NBL_GEOMETRY_CREATOR_SCENE_H_INCLUDED_ +#define _NBL_GEOMETRY_CREATOR_SCENE_H_INCLUDED_ + +#include + +#include "nbl/asset/utils/CGeometryCreator.h" +#include "SBasicViewParameters.hlsl" +#include "geometry/creator/spirv/builtin/CArchive.h" +#include "geometry/creator/spirv/builtin/builtinResources.h" + +namespace nbl::scene::geometrycreator +{ + +enum ObjectType : uint8_t +{ + OT_CUBE, + OT_SPHERE, + OT_CYLINDER, + OT_RECTANGLE, + OT_DISK, + OT_ARROW, + OT_CONE, + OT_ICOSPHERE, + + OT_COUNT, + OT_UNKNOWN = std::numeric_limits::max() +}; + +struct ObjectMeta +{ + ObjectType type = OT_UNKNOWN; + std::string_view name = "Unknown"; +}; + +constexpr static inline struct ClearValues +{ + nbl::video::IGPUCommandBuffer::SClearColorValue color = { .float32 = {0.f,0.f,0.f,1.f} }; + nbl::video::IGPUCommandBuffer::SClearDepthStencilValue depth = { .depth = 0.f }; +} clear; + +#define TYPES_IMPL_BOILERPLATE(WithConverter) struct Types \ +{ \ + using descriptor_set_layout_t = std::conditional_t; \ + using pipeline_layout_t = std::conditional_t; \ + using renderpass_t = std::conditional_t; \ + using image_view_t = std::conditional_t; \ + using image_t = std::conditional_t; \ + using buffer_t = std::conditional_t; \ + using shader_t = std::conditional_t; \ + using graphics_pipeline_t = std::conditional_t; \ + using descriptor_set = std::conditional_t; \ +} + +template +struct ResourcesBundleBase +{ + TYPES_IMPL_BOILERPLATE(withAssetConverter); + + struct ReferenceObject + { + struct Bindings + { + nbl::asset::SBufferBinding vertex, index; + }; + + nbl::core::smart_refctd_ptr pipeline = nullptr; + + Bindings bindings; + nbl::asset::E_INDEX_TYPE indexType = nbl::asset::E_INDEX_TYPE::EIT_UNKNOWN; + uint32_t indexCount = {}; + }; + + using ReferenceDrawHook = std::pair; + + nbl::core::smart_refctd_ptr renderpass; + std::array objects; + nbl::asset::SBufferBinding ubo; + + struct + { + nbl::core::smart_refctd_ptr color, depth; + } attachments; + + nbl::core::smart_refctd_ptr descriptorSet; +}; + +struct ResourcesBundle : public ResourcesBundleBase +{ + using base_t = ResourcesBundleBase; +}; + +#define EXPOSE_NABLA_NAMESPACES() using namespace nbl; \ +using namespace core; \ +using namespace asset; \ +using namespace video; \ +using namespace scene; \ +using namespace system + +template +class ResourceBuilder +{ +public: + TYPES_IMPL_BOILERPLATE(withAssetConverter); + + using this_t = ResourceBuilder; + + ResourceBuilder(nbl::video::IUtilities* const _utilities, nbl::video::IGPUCommandBuffer* const _commandBuffer, nbl::system::ILogger* const _logger, const nbl::asset::IGeometryCreator* const _geometryCreator) + : utilities(_utilities), commandBuffer(_commandBuffer), logger(_logger), geometries(_geometryCreator) + { + assert(utilities); + assert(logger); + } + + /* + if (withAssetConverter) then + -> .build cpu objects + else + -> .build gpu objects & record any resource update upload transfers into command buffer + */ + + inline bool build() + { + EXPOSE_NABLA_NAMESPACES(); + + if constexpr (!withAssetConverter) + { + commandBuffer->reset(IGPUCommandBuffer::RESET_FLAGS::RELEASE_RESOURCES_BIT); + commandBuffer->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT); + commandBuffer->beginDebugMarker("Resources builder's buffers upload [manual]"); + } + + using functor_t = std::function; + + auto work = std::to_array + ({ + functor_t(std::bind(&this_t::createDescriptorSetLayout, this)), + functor_t(std::bind(&this_t::createPipelineLayout, this)), + functor_t(std::bind(&this_t::createRenderpass, this)), + functor_t(std::bind(&this_t::createFramebufferAttachments, this)), + functor_t(std::bind(&this_t::createShaders, this)), + functor_t(std::bind(&this_t::createGeometries, this)), + functor_t(std::bind(&this_t::createViewParametersUboBuffer, this)), + functor_t(std::bind(&this_t::createDescriptorSet, this)) + }); + + for (auto& task : work) + if (!task()) + return false; + + if constexpr (!withAssetConverter) + commandBuffer->end(); + + return true; + } + + /* + if (withAssetConverter) then + -> .convert cpu objects to gpu & update gpu buffers + else + -> update gpu buffers + */ + + inline bool finalize(ResourcesBundle& output, nbl::video::CThreadSafeQueueAdapter* transferCapableQueue) + { + EXPOSE_NABLA_NAMESPACES(); + + // TODO: use multiple command buffers + std::array commandBuffers = {}; + { + commandBuffers.front().cmdbuf = commandBuffer; + } + + if constexpr (withAssetConverter) + { + // note that asset converter records basic transfer uploads itself, we only begin the recording with ONE_TIME_SUBMIT_BIT + commandBuffer->reset(IGPUCommandBuffer::RESET_FLAGS::RELEASE_RESOURCES_BIT); + commandBuffer->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT); + commandBuffer->beginDebugMarker("Resources builder's buffers upload [asset converter]"); + + // asset converter - scratch at this point has ready to convert cpu resources + smart_refctd_ptr converter = CAssetConverter::create({ .device = utilities->getLogicalDevice(),.optimizer = {} }); + CAssetConverter::SInputs inputs = {}; + inputs.logger = logger; + + struct ProxyCpuHooks + { + using object_size_t = std::tuple_size; + + std::array renderpass; + std::array pipelines; + std::array buffers; + std::array attachments; + std::array descriptorSet; + } hooks; + + enum AttachmentIx + { + AI_COLOR = 0u, + AI_DEPTH = 1u, + + AI_COUNT + }; + + // gather CPU assets into span memory views + { + hooks.renderpass.front() = scratch.renderpass.get(); + for (uint32_t i = 0u; i < hooks.pipelines.size(); ++i) + { + auto& [reference, meta] = scratch.objects[static_cast(i)]; + hooks.pipelines[i] = reference.pipeline.get(); + + // [[ [vertex, index] [vertex, index] [vertex, index] ... [ubo] ]] + hooks.buffers[2u * i + 0u] = reference.bindings.vertex.buffer.get(); + hooks.buffers[2u * i + 1u] = reference.bindings.index.buffer.get(); + } + hooks.buffers.back() = scratch.ubo.buffer.get(); + hooks.attachments[AI_COLOR] = scratch.attachments.color.get(); + hooks.attachments[AI_DEPTH] = scratch.attachments.depth.get(); + hooks.descriptorSet.front() = scratch.descriptorSet.get(); + } + + // assign the CPU hooks to converter's inputs + { + std::get>(inputs.assets) = hooks.renderpass; + std::get>(inputs.assets) = hooks.pipelines; + std::get>(inputs.assets) = hooks.buffers; + // std::get>(inputs.assets) = hooks.attachments; // NOTE: THIS IS NOT IMPLEMENTED YET IN CONVERTER! + std::get>(inputs.assets) = hooks.descriptorSet; + } + + // reserve and create the GPU object handles + auto reservation = converter->reserve(inputs); + { + auto prepass = [&](const auto& references) -> bool + { + // retrieve the reserved handles + auto objects = reservation.getGPUObjects(); + + uint32_t counter = {}; + for (auto& object : objects) + { + // anything that fails to be reserved is a nullptr in the span of GPU Objects + auto gpu = object.value; + auto* reference = references[counter]; + + if (reference) + { + // validate + if (!gpu) // throw errors only if corresponding cpu hook was VALID (eg. we may have nullptr for some index buffers in the span for converter but it's OK, I'm too lazy to filter them before passing to the converter inputs and don't want to deal with dynamic alloc) + { + logger->log("Failed to convert a CPU object to GPU!", ILogger::ELL_ERROR); + return false; + } + } + + ++counter; + } + + return true; + }; + + prepass.template operator() < ICPURenderpass > (hooks.renderpass); + prepass.template operator() < ICPUGraphicsPipeline > (hooks.pipelines); + prepass.template operator() < ICPUBuffer > (hooks.buffers); + // validate.template operator() < ICPUImageView > (hooks.attachments); + prepass.template operator() < ICPUDescriptorSet > (hooks.descriptorSet); + } + + auto semaphore = utilities->getLogicalDevice()->createSemaphore(0u); + + // TODO: compute submit as well for the images' mipmaps + SIntendedSubmitInfo transfer = {}; + transfer.queue = transferCapableQueue; + transfer.scratchCommandBuffers = commandBuffers; + transfer.scratchSemaphore = { + .semaphore = semaphore.get(), + .value = 0u, + .stageMask = PIPELINE_STAGE_FLAGS::ALL_TRANSFER_BITS + }; + // issue the convert call + { + CAssetConverter::SConvertParams params = {}; + params.utilities = utilities; + params.transfer = &transfer; + + // basically it records all data uploads and submits them right away + auto future = reservation.convert(params); + if (future.copy()!=IQueue::RESULT::SUCCESS) + { + logger->log("Failed to await submission feature!", ILogger::ELL_ERROR); + return false; + } + + // assign gpu objects to output + auto& base = static_cast(output); + { + auto&& [renderpass, pipelines, buffers, descriptorSet] = std::make_tuple(reservation.getGPUObjects().front().value, reservation.getGPUObjects(), reservation.getGPUObjects(), reservation.getGPUObjects().front().value); + { + base.renderpass = renderpass; + for (uint32_t i = 0u; i < pipelines.size(); ++i) + { + const auto type = static_cast(i); + const auto& [rcpu, rmeta] = scratch.objects[type]; + auto& [gpu, meta] = base.objects[type]; + + gpu.pipeline = pipelines[i].value; + // [[ [vertex, index] [vertex, index] [vertex, index] ... [ubo] ]] + gpu.bindings.vertex = {.offset = 0u, .buffer = buffers[2u * i + 0u].value}; + gpu.bindings.index = {.offset = 0u, .buffer = buffers[2u * i + 1u].value}; + + gpu.indexCount = rcpu.indexCount; + gpu.indexType = rcpu.indexType; + meta.name = rmeta.name; + meta.type = rmeta.type; + } + base.ubo = {.offset = 0u, .buffer = buffers.back().value}; + base.descriptorSet = descriptorSet; + + /* + // base.attachments.color = attachments[AI_COLOR].value; + // base.attachments.depth = attachments[AI_DEPTH].value; + + note conversion of image views is not yet supported by the asset converter + - it's complicated, we have to kinda temporary ignore DRY a bit here to not break the design which is correct + + TEMPORARY: we patch attachments by allocating them ourselves here given cpu instances & parameters + TODO: remove following code once asset converter works with image views & update stuff + */ + + for (uint32_t i = 0u; i < AI_COUNT; ++i) + { + const auto* reference = hooks.attachments[i]; + auto& out = (i == AI_COLOR ? base.attachments.color : base.attachments.depth); + + const auto& viewParams = reference->getCreationParameters(); + const auto& imageParams = viewParams.image->getCreationParameters(); + + auto image = utilities->getLogicalDevice()->createImage + ( + IGPUImage::SCreationParams + ({ + .type = imageParams.type, + .samples = imageParams.samples, + .format = imageParams.format, + .extent = imageParams.extent, + .mipLevels = imageParams.mipLevels, + .arrayLayers = imageParams.arrayLayers, + .usage = imageParams.usage + }) + ); + + if (!image) + { + logger->log("Could not create image!", ILogger::ELL_ERROR); + return false; + } + + bool IS_DEPTH = isDepthOrStencilFormat(imageParams.format); + std::string_view DEBUG_NAME = IS_DEPTH ? "UI Scene Depth Attachment Image" : "UI Scene Color Attachment Image"; + image->setObjectDebugName(DEBUG_NAME.data()); + + if (!utilities->getLogicalDevice()->allocate(image->getMemoryReqs(), image.get()).isValid()) + { + logger->log("Could not allocate memory for an image!", ILogger::ELL_ERROR); + return false; + } + + out = utilities->getLogicalDevice()->createImageView + ( + IGPUImageView::SCreationParams + ({ + .flags = viewParams.flags, + .subUsages = viewParams.subUsages, + .image = std::move(image), + .viewType = viewParams.viewType, + .format = viewParams.format, + .subresourceRange = viewParams.subresourceRange + }) + ); + + if (!out) + { + logger->log("Could not create image view!", ILogger::ELL_ERROR); + return false; + } + } + + logger->log("Image View attachments has been allocated by hand after asset converter successful submit becasuse it doesn't support converting them yet!", ILogger::ELL_WARNING); + } + } + } + } + else + { + auto completed = utilities->getLogicalDevice()->createSemaphore(0u); + + std::array signals; + { + auto& signal = signals.front(); + signal.value = 1; + signal.stageMask = bitflag(PIPELINE_STAGE_FLAGS::ALL_TRANSFER_BITS); + signal.semaphore = completed.get(); + } + + const IQueue::SSubmitInfo infos [] = + { + { + .waitSemaphores = {}, + .commandBuffers = commandBuffers, // note that here our command buffer is already recorded! + .signalSemaphores = signals + } + }; + + if (transferCapableQueue->submit(infos) != IQueue::RESULT::SUCCESS) + { + logger->log("Failed to submit transfer upload operations!", ILogger::ELL_ERROR); + return false; + } + + const ISemaphore::SWaitInfo info [] = + { { + .semaphore = completed.get(), + .value = 1 + } }; + + utilities->getLogicalDevice()->blockForSemaphores(info); + + static_cast(output) = static_cast(scratch); // scratch has all ready to use allocated gpu resources with uploaded memory so now just assign resources to base output + } + + // write the descriptor set + { + // descriptor write ubo + IGPUDescriptorSet::SWriteDescriptorSet write; + write.dstSet = output.descriptorSet.get(); + write.binding = 0; + write.arrayElement = 0u; + write.count = 1u; + + IGPUDescriptorSet::SDescriptorInfo info; + { + info.desc = smart_refctd_ptr(output.ubo.buffer); + info.info.buffer.offset = output.ubo.offset; + info.info.buffer.size = output.ubo.buffer->getSize(); + } + + write.info = &info; + + if(!utilities->getLogicalDevice()->updateDescriptorSets(1u, &write, 0u, nullptr)) + { + logger->log("Could not write descriptor set!", ILogger::ELL_ERROR); + return false; + } + } + + return true; + } + +private: + bool createDescriptorSetLayout() + { + EXPOSE_NABLA_NAMESPACES(); + + typename Types::descriptor_set_layout_t::SBinding bindings[] = + { + { + .binding = 0u, + .type = IDescriptor::E_TYPE::ET_UNIFORM_BUFFER, + .createFlags = Types::descriptor_set_layout_t::SBinding::E_CREATE_FLAGS::ECF_NONE, + .stageFlags = IShader::E_SHADER_STAGE::ESS_VERTEX | IShader::E_SHADER_STAGE::ESS_FRAGMENT, + .count = 1u, + } + }; + + if constexpr (withAssetConverter) + scratch.descriptorSetLayout = make_smart_refctd_ptr(bindings); + else + scratch.descriptorSetLayout = utilities->getLogicalDevice()->createDescriptorSetLayout(bindings); + + if (!scratch.descriptorSetLayout) + { + logger->log("Could not descriptor set layout!", ILogger::ELL_ERROR); + return false; + } + + return true; + } + + bool createDescriptorSet() + { + EXPOSE_NABLA_NAMESPACES(); + + if constexpr (withAssetConverter) + scratch.descriptorSet = make_smart_refctd_ptr(smart_refctd_ptr(scratch.descriptorSetLayout)); + else + { + const IGPUDescriptorSetLayout* const layouts[] = { scratch.descriptorSetLayout.get()}; + const uint32_t setCounts[] = { 1u }; + + // note descriptor set has back smart pointer to its pool, so we dont need to keep it explicitly + auto pool = utilities->getLogicalDevice()->createDescriptorPoolForDSLayouts(IDescriptorPool::E_CREATE_FLAGS::ECF_NONE, layouts, setCounts); + + if (!pool) + { + logger->log("Could not create Descriptor Pool!", ILogger::ELL_ERROR); + return false; + } + + pool->createDescriptorSets(layouts, &scratch.descriptorSet); + } + + if (!scratch.descriptorSet) + { + logger->log("Could not create Descriptor Set!", ILogger::ELL_ERROR); + return false; + } + + return true; + } + + bool createPipelineLayout() + { + EXPOSE_NABLA_NAMESPACES(); + + const std::span range = {}; + + if constexpr (withAssetConverter) + scratch.pipelineLayout = make_smart_refctd_ptr(range, nullptr, smart_refctd_ptr(scratch.descriptorSetLayout), nullptr, nullptr); + else + scratch.pipelineLayout = utilities->getLogicalDevice()->createPipelineLayout(range, nullptr, smart_refctd_ptr(scratch.descriptorSetLayout), nullptr, nullptr); + + if (!scratch.pipelineLayout) + { + logger->log("Could not create pipeline layout!", ILogger::ELL_ERROR); + return false; + } + + return true; + } + + bool createRenderpass() + { + EXPOSE_NABLA_NAMESPACES(); + + static constexpr Types::renderpass_t::SCreationParams::SColorAttachmentDescription colorAttachments[] = + { + { + { + { + .format = ColorFboAttachmentFormat, + .samples = Samples, + .mayAlias = false + }, + /* .loadOp = */ Types::renderpass_t::LOAD_OP::CLEAR, + /* .storeOp = */ Types::renderpass_t::STORE_OP::STORE, + /* .initialLayout = */ Types::image_t::LAYOUT::UNDEFINED, + /* .finalLayout = */ Types::image_t::LAYOUT::READ_ONLY_OPTIMAL + } + }, + Types::renderpass_t::SCreationParams::ColorAttachmentsEnd + }; + + static constexpr Types::renderpass_t::SCreationParams::SDepthStencilAttachmentDescription depthAttachments[] = + { + { + { + { + .format = DepthFboAttachmentFormat, + .samples = Samples, + .mayAlias = false + }, + /* .loadOp = */ {Types::renderpass_t::LOAD_OP::CLEAR}, + /* .storeOp = */ {Types::renderpass_t::STORE_OP::STORE}, + /* .initialLayout = */ {Types::image_t::LAYOUT::UNDEFINED}, + /* .finalLayout = */ {Types::image_t::LAYOUT::ATTACHMENT_OPTIMAL} + } + }, + Types::renderpass_t::SCreationParams::DepthStencilAttachmentsEnd + }; + + typename Types::renderpass_t::SCreationParams::SSubpassDescription subpasses[] = + { + {}, + Types::renderpass_t::SCreationParams::SubpassesEnd + }; + + subpasses[0].depthStencilAttachment.render = { .attachmentIndex = 0u,.layout = Types::image_t::LAYOUT::ATTACHMENT_OPTIMAL }; + subpasses[0].colorAttachments[0] = { .render = {.attachmentIndex = 0u, .layout = Types::image_t::LAYOUT::ATTACHMENT_OPTIMAL } }; + + static constexpr Types::renderpass_t::SCreationParams::SSubpassDependency dependencies[] = + { + // wipe-transition of Color to ATTACHMENT_OPTIMAL + { + .srcSubpass = Types::renderpass_t::SCreationParams::SSubpassDependency::External, + .dstSubpass = 0, + .memoryBarrier = + { + // + .srcStageMask = PIPELINE_STAGE_FLAGS::FRAGMENT_SHADER_BIT, + // only write ops, reads can't be made available + .srcAccessMask = ACCESS_FLAGS::SAMPLED_READ_BIT, + // destination needs to wait as early as possible + .dstStageMask = PIPELINE_STAGE_FLAGS::EARLY_FRAGMENT_TESTS_BIT | PIPELINE_STAGE_FLAGS::COLOR_ATTACHMENT_OUTPUT_BIT, + // because of depth test needing a read and a write + .dstAccessMask = ACCESS_FLAGS::DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | ACCESS_FLAGS::DEPTH_STENCIL_ATTACHMENT_READ_BIT | ACCESS_FLAGS::COLOR_ATTACHMENT_READ_BIT | ACCESS_FLAGS::COLOR_ATTACHMENT_WRITE_BIT + } + // leave view offsets and flags default + }, + // color from ATTACHMENT_OPTIMAL to PRESENT_SRC + { + .srcSubpass = 0, + .dstSubpass = Types::renderpass_t::SCreationParams::SSubpassDependency::External, + .memoryBarrier = + { + // last place where the depth can get modified + .srcStageMask = PIPELINE_STAGE_FLAGS::COLOR_ATTACHMENT_OUTPUT_BIT, + // only write ops, reads can't be made available + .srcAccessMask = ACCESS_FLAGS::COLOR_ATTACHMENT_WRITE_BIT, + // + .dstStageMask = PIPELINE_STAGE_FLAGS::FRAGMENT_SHADER_BIT, + // + .dstAccessMask = ACCESS_FLAGS::SAMPLED_READ_BIT + // + } + // leave view offsets and flags default + }, + Types::renderpass_t::SCreationParams::DependenciesEnd + }; + + typename Types::renderpass_t::SCreationParams params = {}; + params.colorAttachments = colorAttachments; + params.depthStencilAttachments = depthAttachments; + params.subpasses = subpasses; + params.dependencies = dependencies; + + if constexpr (withAssetConverter) + scratch.renderpass = ICPURenderpass::create(params); + else + scratch.renderpass = utilities->getLogicalDevice()->createRenderpass(params); + + if (!scratch.renderpass) + { + logger->log("Could not create render pass!", ILogger::ELL_ERROR); + return false; + } + + return true; + } + + bool createFramebufferAttachments() + { + EXPOSE_NABLA_NAMESPACES(); + + auto createImageView = [&](smart_refctd_ptr& outView) -> smart_refctd_ptr + { + constexpr bool IS_DEPTH = isDepthOrStencilFormat(); + constexpr auto USAGE = [](const bool isDepth) + { + bitflag usage = Types::image_t::EUF_RENDER_ATTACHMENT_BIT; + + if (!isDepth) + usage |= Types::image_t::EUF_SAMPLED_BIT; + + return usage; + }(IS_DEPTH); + constexpr auto ASPECT = IS_DEPTH ? IImage::E_ASPECT_FLAGS::EAF_DEPTH_BIT : IImage::E_ASPECT_FLAGS::EAF_COLOR_BIT; + constexpr std::string_view DEBUG_NAME = IS_DEPTH ? "UI Scene Depth Attachment Image" : "UI Scene Color Attachment Image"; + { + smart_refctd_ptr image; + { + auto params = typename Types::image_t::SCreationParams( + { + .type = Types::image_t::ET_2D, + .samples = Samples, + .format = format, + .extent = { FramebufferW, FramebufferH, 1u }, + .mipLevels = 1u, + .arrayLayers = 1u, + .usage = USAGE + }); + + if constexpr (withAssetConverter) + image = ICPUImage::create(params); + else + image = utilities->getLogicalDevice()->createImage(std::move(params)); + } + + if (!image) + { + logger->log("Could not create image!", ILogger::ELL_ERROR); + return nullptr; + } + + if constexpr (withAssetConverter) + { + auto dummyBuffer = make_smart_refctd_ptr(FramebufferW * FramebufferH * getTexelOrBlockBytesize()); + dummyBuffer->setContentHash(dummyBuffer->computeContentHash()); + + auto regions = make_refctd_dynamic_array>(1u); + auto& region = regions->front(); + + region.imageSubresource = { .aspectMask = ASPECT, .mipLevel = 0u, .baseArrayLayer = 0u, .layerCount = 0u }; + region.bufferOffset = 0u; + region.bufferRowLength = IImageAssetHandlerBase::calcPitchInBlocks(FramebufferW, getTexelOrBlockBytesize()); + region.bufferImageHeight = 0u; + region.imageOffset = { 0u, 0u, 0u }; + region.imageExtent = { FramebufferW, FramebufferH, 1u }; + + if (!image->setBufferAndRegions(std::move(dummyBuffer), regions)) + { + logger->log("Could not set image's regions!", ILogger::ELL_ERROR); + return nullptr; + } + image->setContentHash(image->computeContentHash()); + } + else + { + image->setObjectDebugName(DEBUG_NAME.data()); + + if (!utilities->getLogicalDevice()->allocate(image->getMemoryReqs(), image.get()).isValid()) + { + logger->log("Could not allocate memory for an image!", ILogger::ELL_ERROR); + return nullptr; + } + } + + auto params = typename Types::image_view_t::SCreationParams + ({ + .flags = Types::image_view_t::ECF_NONE, + .subUsages = USAGE, + .image = std::move(image), + .viewType = Types::image_view_t::ET_2D, + .format = format, + .subresourceRange = { .aspectMask = ASPECT, .baseMipLevel = 0u, .levelCount = 1u, .baseArrayLayer = 0u, .layerCount = 1u } + }); + + if constexpr (withAssetConverter) + outView = make_smart_refctd_ptr(std::move(params)); + else + outView = utilities->getLogicalDevice()->createImageView(std::move(params)); + + if (!outView) + { + logger->log("Could not create image view!", ILogger::ELL_ERROR); + return nullptr; + } + + return smart_refctd_ptr(outView); + } + }; + + const bool allocated = createImageView.template operator() < ColorFboAttachmentFormat > (scratch.attachments.color) && createImageView.template operator() < DepthFboAttachmentFormat > (scratch.attachments.depth); + + if (!allocated) + { + logger->log("Could not allocate frame buffer's attachments!", ILogger::ELL_ERROR); + return false; + } + + return true; + } + + bool createShaders() + { + EXPOSE_NABLA_NAMESPACES(); + + auto createShader = [&](IShader::E_SHADER_STAGE stage, smart_refctd_ptr& outShader) -> smart_refctd_ptr + { + // TODO: use SPIRV loader & our ::system ns to get those cpu shaders, do not create myself (shit I forgot it exists) + + const SBuiltinFile& in = ::geometry::creator::spirv::builtin::get_resource(); + const auto buffer = make_smart_refctd_ptr, true> >(in.size, (void*)in.contents, adopt_memory); + auto shader = make_smart_refctd_ptr(smart_refctd_ptr(buffer), stage, IShader::E_CONTENT_TYPE::ECT_SPIRV, ""); // must create cpu instance regardless underlying type + + if constexpr (withAssetConverter) + { + buffer->setContentHash(buffer->computeContentHash()); + outShader = std::move(shader); + } + else + outShader = utilities->getLogicalDevice()->createShader(shader.get()); + + return outShader; + }; + + typename ResourcesBundleScratch::Shaders& basic = scratch.shaders[GeometriesCpu::GP_BASIC]; + createShader.template operator() < NBL_CORE_UNIQUE_STRING_LITERAL_TYPE("geometryCreator/spirv/gc.basic.vertex.spv") > (IShader::E_SHADER_STAGE::ESS_VERTEX, basic.vertex); + createShader.template operator() < NBL_CORE_UNIQUE_STRING_LITERAL_TYPE("geometryCreator/spirv/gc.basic.fragment.spv") > (IShader::E_SHADER_STAGE::ESS_FRAGMENT, basic.fragment); + + typename ResourcesBundleScratch::Shaders& cone = scratch.shaders[GeometriesCpu::GP_CONE]; + createShader.template operator() < NBL_CORE_UNIQUE_STRING_LITERAL_TYPE("geometryCreator/spirv/gc.cone.vertex.spv") > (IShader::E_SHADER_STAGE::ESS_VERTEX, cone.vertex); + createShader.template operator() < NBL_CORE_UNIQUE_STRING_LITERAL_TYPE("geometryCreator/spirv/gc.basic.fragment.spv") > (IShader::E_SHADER_STAGE::ESS_FRAGMENT, cone.fragment); // note we reuse fragment from basic! + + typename ResourcesBundleScratch::Shaders& ico = scratch.shaders[GeometriesCpu::GP_ICO]; + createShader.template operator() < NBL_CORE_UNIQUE_STRING_LITERAL_TYPE("geometryCreator/spirv/gc.ico.vertex.spv") > (IShader::E_SHADER_STAGE::ESS_VERTEX, ico.vertex); + createShader.template operator() < NBL_CORE_UNIQUE_STRING_LITERAL_TYPE("geometryCreator/spirv/gc.basic.fragment.spv") > (IShader::E_SHADER_STAGE::ESS_FRAGMENT, ico.fragment); // note we reuse fragment from basic! + + for (const auto& it : scratch.shaders) + { + if (!it.vertex || !it.fragment) + { + logger->log("Could not create shaders!", ILogger::ELL_ERROR); + return false; + } + } + + return true; + } + + bool createGeometries() + { + EXPOSE_NABLA_NAMESPACES(); + + for (uint32_t i = 0; i < geometries.objects.size(); ++i) + { + const auto& inGeometry = geometries.objects[i]; + auto& [obj, meta] = scratch.objects[i]; + + bool status = true; + + meta.name = inGeometry.meta.name; + meta.type = inGeometry.meta.type; + + struct + { + SBlendParams blend; + SRasterizationParams rasterization; + typename Types::graphics_pipeline_t::SCreationParams pipeline; + } params; + + { + params.blend.logicOp = ELO_NO_OP; + + auto& b = params.blend.blendParams[0]; + b.srcColorFactor = EBF_SRC_ALPHA; + b.dstColorFactor = EBF_ONE_MINUS_SRC_ALPHA; + b.colorBlendOp = EBO_ADD; + b.srcAlphaFactor = EBF_SRC_ALPHA; + b.dstAlphaFactor = EBF_SRC_ALPHA; + b.alphaBlendOp = EBO_ADD; + b.colorWriteMask = (1u << 0u) | (1u << 1u) | (1u << 2u) | (1u << 3u); + } + + params.rasterization.faceCullingMode = EFCM_NONE; + { + const typename Types::shader_t::SSpecInfo info [] = + { + {.entryPoint = "VSMain", .shader = scratch.shaders[inGeometry.shadersType].vertex.get() }, + {.entryPoint = "PSMain", .shader = scratch.shaders[inGeometry.shadersType].fragment.get() } + }; + + params.pipeline.layout = scratch.pipelineLayout.get(); + params.pipeline.shaders = info; + params.pipeline.renderpass = scratch.renderpass.get(); + params.pipeline.cached = { .vertexInput = inGeometry.data.inputParams, .primitiveAssembly = inGeometry.data.assemblyParams, .rasterization = params.rasterization, .blend = params.blend, .subpassIx = 0u }; + + obj.indexCount = inGeometry.data.indexCount; + obj.indexType = inGeometry.data.indexType; + + // TODO: cache pipeline & try lookup for existing one first maybe + + // similar issue like with shaders again, in this case gpu contructor allows for extra cache parameters + there is no constructor you can use to fire make_smart_refctd_ptr yourself for cpu + if constexpr (withAssetConverter) + obj.pipeline = ICPUGraphicsPipeline::create(params.pipeline); + else + { + const std::array info = { { params.pipeline } }; + utilities->getLogicalDevice()->createGraphicsPipelines(nullptr, info, &obj.pipeline); + } + + if (!obj.pipeline) + { + logger->log("Could not create graphics pipeline for [%s] object!", ILogger::ELL_ERROR, meta.name.data()); + status = false; + } + + // object buffers + auto createVIBuffers = [&]() -> bool + { + using ibuffer_t = ::nbl::asset::IBuffer; // seems to be ambigous, both asset & core namespaces has IBuffer + + // note: similar issue like with shaders, this time with cpu-gpu constructors differing in arguments + auto vBuffer = smart_refctd_ptr(inGeometry.data.bindings[0].buffer); // no offset + constexpr static auto VERTEX_USAGE = bitflag(ibuffer_t::EUF_VERTEX_BUFFER_BIT) | ibuffer_t::EUF_TRANSFER_DST_BIT | ibuffer_t::EUF_INLINE_UPDATE_VIA_CMDBUF; + obj.bindings.vertex.offset = 0u; + + auto iBuffer = smart_refctd_ptr(inGeometry.data.indexBuffer.buffer); // no offset + constexpr static auto INDEX_USAGE = bitflag(ibuffer_t::EUF_INDEX_BUFFER_BIT) | ibuffer_t::EUF_VERTEX_BUFFER_BIT | ibuffer_t::EUF_TRANSFER_DST_BIT | ibuffer_t::EUF_INLINE_UPDATE_VIA_CMDBUF; + obj.bindings.index.offset = 0u; + + if constexpr (withAssetConverter) + { + if (!vBuffer) + return false; + + vBuffer->addUsageFlags(VERTEX_USAGE); + vBuffer->setContentHash(vBuffer->computeContentHash()); + obj.bindings.vertex = { .offset = 0u, .buffer = vBuffer }; + + if (inGeometry.data.indexType != EIT_UNKNOWN) + if (iBuffer) + { + iBuffer->addUsageFlags(INDEX_USAGE); + iBuffer->setContentHash(iBuffer->computeContentHash()); + } + else + return false; + + obj.bindings.index = { .offset = 0u, .buffer = iBuffer }; + } + else + { + auto vertexBuffer = utilities->getLogicalDevice()->createBuffer(IGPUBuffer::SCreationParams({ .size = vBuffer->getSize(), .usage = VERTEX_USAGE })); + auto indexBuffer = iBuffer ? utilities->getLogicalDevice()->createBuffer(IGPUBuffer::SCreationParams({ .size = iBuffer->getSize(), .usage = INDEX_USAGE })) : nullptr; + + if (!vertexBuffer) + return false; + + if (inGeometry.data.indexType != EIT_UNKNOWN) + if (!indexBuffer) + return false; + + const auto mask = utilities->getLogicalDevice()->getPhysicalDevice()->getUpStreamingMemoryTypeBits(); + for (auto it : { vertexBuffer , indexBuffer }) + { + if (it) + { + auto reqs = it->getMemoryReqs(); + reqs.memoryTypeBits &= mask; + + utilities->getLogicalDevice()->allocate(reqs, it.get()); + } + } + + // record transfer uploads + obj.bindings.vertex = { .offset = 0u, .buffer = std::move(vertexBuffer) }; + { + const SBufferRange range = { .offset = obj.bindings.vertex.offset, .size = obj.bindings.vertex.buffer->getSize(), .buffer = obj.bindings.vertex.buffer }; + if (!commandBuffer->updateBuffer(range, vBuffer->getPointer())) + { + logger->log("Could not record vertex buffer transfer upload for [%s] object!", ILogger::ELL_ERROR, meta.name.data()); + status = false; + } + } + obj.bindings.index = { .offset = 0u, .buffer = std::move(indexBuffer) }; + { + if (iBuffer) + { + const SBufferRange range = { .offset = obj.bindings.index.offset, .size = obj.bindings.index.buffer->getSize(), .buffer = obj.bindings.index.buffer }; + + if (!commandBuffer->updateBuffer(range, iBuffer->getPointer())) + { + logger->log("Could not record index buffer transfer upload for [%s] object!", ILogger::ELL_ERROR, meta.name.data()); + status = false; + } + } + } + } + + return true; + }; + + if (!createVIBuffers()) + { + logger->log("Could not create buffers for [%s] object!", ILogger::ELL_ERROR, meta.name.data()); + status = false; + } + + if (!status) + { + logger->log("[%s] object will not be created!", ILogger::ELL_ERROR, meta.name.data()); + + obj.bindings.vertex = {}; + obj.bindings.index = {}; + obj.indexCount = 0u; + obj.indexType = E_INDEX_TYPE::EIT_UNKNOWN; + obj.pipeline = nullptr; + + continue; + } + } + } + + return true; + } + + bool createViewParametersUboBuffer() + { + EXPOSE_NABLA_NAMESPACES(); + + using ibuffer_t = ::nbl::asset::IBuffer; // seems to be ambigous, both asset & core namespaces has IBuffer + constexpr static auto UboUsage = bitflag(ibuffer_t::EUF_UNIFORM_BUFFER_BIT) | ibuffer_t::EUF_TRANSFER_DST_BIT | ibuffer_t::EUF_INLINE_UPDATE_VIA_CMDBUF; + + if constexpr (withAssetConverter) + { + auto uboBuffer = make_smart_refctd_ptr(sizeof(SBasicViewParameters)); + uboBuffer->addUsageFlags(UboUsage); + uboBuffer->setContentHash(uboBuffer->computeContentHash()); + scratch.ubo = { .offset = 0u, .buffer = std::move(uboBuffer) }; + } + else + { + const auto mask = utilities->getLogicalDevice()->getPhysicalDevice()->getUpStreamingMemoryTypeBits(); + auto uboBuffer = utilities->getLogicalDevice()->createBuffer(IGPUBuffer::SCreationParams({ .size = sizeof(SBasicViewParameters), .usage = UboUsage })); + + if (!uboBuffer) + return false; + + for (auto it : { uboBuffer }) + { + IDeviceMemoryBacked::SDeviceMemoryRequirements reqs = it->getMemoryReqs(); + reqs.memoryTypeBits &= mask; + + utilities->getLogicalDevice()->allocate(reqs, it.get()); + } + + scratch.ubo = { .offset = 0u, .buffer = std::move(uboBuffer) }; + } + + return true; + } + + struct GeometriesCpu + { + enum GeometryShader + { + GP_BASIC = 0, + GP_CONE, + GP_ICO, + + GP_COUNT + }; + + struct ReferenceObjectCpu + { + ObjectMeta meta; + GeometryShader shadersType; + nbl::asset::CGeometryCreator::return_type data; + }; + + GeometriesCpu(const nbl::asset::IGeometryCreator* _gc) + : gc(_gc), + objects + ({ + ReferenceObjectCpu {.meta = {.type = OT_CUBE, .name = "Cube Mesh" }, .shadersType = GP_BASIC, .data = gc->createCubeMesh(nbl::core::vector3df(1.f, 1.f, 1.f)) }, + ReferenceObjectCpu {.meta = {.type = OT_SPHERE, .name = "Sphere Mesh" }, .shadersType = GP_BASIC, .data = gc->createSphereMesh(2, 16, 16) }, + ReferenceObjectCpu {.meta = {.type = OT_CYLINDER, .name = "Cylinder Mesh" }, .shadersType = GP_BASIC, .data = gc->createCylinderMesh(2, 2, 20) }, + ReferenceObjectCpu {.meta = {.type = OT_RECTANGLE, .name = "Rectangle Mesh" }, .shadersType = GP_BASIC, .data = gc->createRectangleMesh(nbl::core::vector2df_SIMD(1.5, 3)) }, + ReferenceObjectCpu {.meta = {.type = OT_DISK, .name = "Disk Mesh" }, .shadersType = GP_BASIC, .data = gc->createDiskMesh(2, 30) }, + ReferenceObjectCpu {.meta = {.type = OT_ARROW, .name = "Arrow Mesh" }, .shadersType = GP_BASIC, .data = gc->createArrowMesh() }, + ReferenceObjectCpu {.meta = {.type = OT_CONE, .name = "Cone Mesh" }, .shadersType = GP_CONE, .data = gc->createConeMesh(2, 3, 10) }, + ReferenceObjectCpu {.meta = {.type = OT_ICOSPHERE, .name = "Icoshpere Mesh" }, .shadersType = GP_ICO, .data = gc->createIcoSphere(1, 3, true) } + }) + { + gc = nullptr; // one shot + } + + private: + const nbl::asset::IGeometryCreator* gc; + + public: + const std::array objects; + }; + + using resources_bundle_base_t = ResourcesBundleBase; + + struct ResourcesBundleScratch : public resources_bundle_base_t + { + using Types = resources_bundle_base_t::Types; + + ResourcesBundleScratch() + : resources_bundle_base_t() {} + + struct Shaders + { + nbl::core::smart_refctd_ptr vertex = nullptr, fragment = nullptr; + }; + + nbl::core::smart_refctd_ptr descriptorSetLayout; + nbl::core::smart_refctd_ptr pipelineLayout; + std::array shaders; + }; + + // TODO: we could make those params templated with default values like below + static constexpr auto FramebufferW = 1280u, FramebufferH = 720u; + static constexpr auto ColorFboAttachmentFormat = nbl::asset::EF_R8G8B8A8_SRGB, DepthFboAttachmentFormat = nbl::asset::EF_D16_UNORM; + static constexpr auto Samples = nbl::video::IGPUImage::ESCF_1_BIT; + + ResourcesBundleScratch scratch; + + nbl::video::IUtilities* const utilities; + nbl::video::IGPUCommandBuffer* const commandBuffer; + nbl::system::ILogger* const logger; + GeometriesCpu geometries; +}; + +#undef TYPES_IMPL_BOILERPLATE + +struct ObjectDrawHookCpu +{ + nbl::core::matrix3x4SIMD model; + nbl::asset::SBasicViewParameters viewParameters; + ObjectMeta meta; +}; + +/* + Rendering to offline framebuffer which we don't present, color + scene attachment texture we use for second UI renderpass + sampling it & rendering into desired GUI area. + + The scene can be created from simple geometry + using our Geomtry Creator class. +*/ + +class CScene final : public nbl::core::IReferenceCounted +{ +public: + ObjectDrawHookCpu object; // TODO: this could be a vector (to not complicate the example I leave it single object), we would need a better system for drawing then to make only 1 max 2 indirect draw calls (indexed and not indexed objects) + + struct + { + const uint32_t startedValue = 0, finishedValue = 0x45; + nbl::core::smart_refctd_ptr progress; + } semaphore; + + struct CreateResourcesDirectlyWithDevice { using Builder = ResourceBuilder; }; + struct CreateResourcesWithAssetConverter { using Builder = ResourceBuilder; }; + + ~CScene() {} + + static inline nbl::core::smart_refctd_ptr createCommandBuffer(nbl::video::ILogicalDevice* const device, nbl::system::ILogger* const logger, const uint32_t familyIx) + { + EXPOSE_NABLA_NAMESPACES(); + auto pool = device->createCommandPool(familyIx, IGPUCommandPool::CREATE_FLAGS::RESET_COMMAND_BUFFER_BIT); + + if (!pool) + { + logger->log("Couldn't create Command Pool!", ILogger::ELL_ERROR); + return nullptr; + } + + nbl::core::smart_refctd_ptr cmd; + + if (!pool->createCommandBuffers(IGPUCommandPool::BUFFER_LEVEL::PRIMARY, { &cmd , 1 })) + { + logger->log("Couldn't create Command Buffer!", ILogger::ELL_ERROR); + return nullptr; + } + + return cmd; + } + + template + static auto create(Args&&... args) -> decltype(auto) + { + EXPOSE_NABLA_NAMESPACES(); + + /* + user should call the constructor's args without last argument explicitly, this is a trick to make constructor templated, + eg.create(smart_refctd_ptr(device), smart_refctd_ptr(logger), queuePointer, geometryPointer) + */ + + auto* scene = new CScene(std::forward(args)..., CreateWith {}); + smart_refctd_ptr smart(scene, dont_grab); + + return smart; + } + + inline void begin() + { + EXPOSE_NABLA_NAMESPACES(); + + m_commandBuffer->reset(IGPUCommandBuffer::RESET_FLAGS::RELEASE_RESOURCES_BIT); + m_commandBuffer->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT); + m_commandBuffer->beginDebugMarker("UISampleApp Offline Scene Frame"); + + semaphore.progress = m_utilities->getLogicalDevice()->createSemaphore(semaphore.startedValue); + } + + inline void record() + { + EXPOSE_NABLA_NAMESPACES(); + + const struct + { + const uint32_t width, height; + } fbo = { .width = m_frameBuffer->getCreationParameters().width, .height = m_frameBuffer->getCreationParameters().height }; + + SViewport viewport; + { + viewport.minDepth = 1.f; + viewport.maxDepth = 0.f; + viewport.x = 0u; + viewport.y = 0u; + viewport.width = fbo.width; + viewport.height = fbo.height; + } + + m_commandBuffer->setViewport(0u, 1u, &viewport); + + VkRect2D scissor = {}; + scissor.offset = { 0, 0 }; + scissor.extent = { fbo.width, fbo.height }; + m_commandBuffer->setScissor(0u, 1u, &scissor); + + const VkRect2D renderArea = + { + .offset = { 0,0 }, + .extent = { fbo.width, fbo.height } + }; + + const IGPUCommandBuffer::SRenderpassBeginInfo info = + { + .framebuffer = m_frameBuffer.get(), + .colorClearValues = &clear.color, + .depthStencilClearValues = &clear.depth, + .renderArea = renderArea + }; + + m_commandBuffer->beginRenderPass(info, IGPUCommandBuffer::SUBPASS_CONTENTS::INLINE); + + const auto& [hook, meta] = resources.objects[object.meta.type]; + auto* rawPipeline = hook.pipeline.get(); + + SBufferBinding vertex = hook.bindings.vertex, index = hook.bindings.index; + + m_commandBuffer->bindGraphicsPipeline(rawPipeline); + m_commandBuffer->bindDescriptorSets(EPBP_GRAPHICS, rawPipeline->getLayout(), 1, 1, &resources.descriptorSet.get()); + m_commandBuffer->bindVertexBuffers(0, 1, &vertex); + + if (index.buffer && hook.indexType != EIT_UNKNOWN) + { + m_commandBuffer->bindIndexBuffer(index, hook.indexType); + m_commandBuffer->drawIndexed(hook.indexCount, 1, 0, 0, 0); + } + else + m_commandBuffer->draw(hook.indexCount, 1, 0, 0); + + m_commandBuffer->endRenderPass(); + } + + inline void end() + { + m_commandBuffer->end(); + } + + inline bool submit() + { + EXPOSE_NABLA_NAMESPACES(); + + const IQueue::SSubmitInfo::SCommandBufferInfo buffers[] = + { + { .cmdbuf = m_commandBuffer.get() } + }; + + const IQueue::SSubmitInfo::SSemaphoreInfo signals[] = { {.semaphore = semaphore.progress.get(),.value = semaphore.finishedValue,.stageMask = PIPELINE_STAGE_FLAGS::FRAMEBUFFER_SPACE_BITS} }; + + const IQueue::SSubmitInfo infos[] = + { + { + .waitSemaphores = {}, + .commandBuffers = buffers, + .signalSemaphores = signals + } + }; + + return queue->submit(infos) == IQueue::RESULT::SUCCESS; + } + + // note: must be updated outside render pass + inline void update() + { + EXPOSE_NABLA_NAMESPACES(); + + SBufferRange range; + range.buffer = smart_refctd_ptr(resources.ubo.buffer); + range.size = resources.ubo.buffer->getSize(); + + m_commandBuffer->updateBuffer(range, &object.viewParameters); + } + + inline decltype(auto) getResources() + { + return (resources); // note: do not remove "()" - it makes the return type lvalue reference instead of copy + } + +private: + template // TODO: enforce constraints, only those 2 above are valid + CScene(nbl::core::smart_refctd_ptr _utilities, nbl::core::smart_refctd_ptr _logger, nbl::video::CThreadSafeQueueAdapter* _graphicsQueue, const nbl::asset::IGeometryCreator* _geometryCreator, CreateWith createWith = {}) + : m_utilities(nbl::core::smart_refctd_ptr(_utilities)), m_logger(nbl::core::smart_refctd_ptr(_logger)), queue(_graphicsQueue) + { + EXPOSE_NABLA_NAMESPACES(); + using Builder = typename CreateWith::Builder; + + m_commandBuffer = createCommandBuffer(m_utilities->getLogicalDevice(), m_utilities->getLogger(), queue->getFamilyIndex()); + Builder builder(m_utilities.get(), m_commandBuffer.get(), m_logger.get(), _geometryCreator); + + // gpu resources + if (builder.build()) + { + if (!builder.finalize(resources, queue)) + m_logger->log("Could not finalize resource objects to gpu objects!", ILogger::ELL_ERROR); + } + else + m_logger->log("Could not build resource objects!", ILogger::ELL_ERROR); + + // frame buffer + { + const auto extent = resources.attachments.color->getCreationParameters().image->getCreationParameters().extent; + + IGPUFramebuffer::SCreationParams params = + { + { + .renderpass = smart_refctd_ptr(resources.renderpass), + .depthStencilAttachments = &resources.attachments.depth.get(), + .colorAttachments = &resources.attachments.color.get(), + .width = extent.width, + .height = extent.height, + .layers = 1u + } + }; + + m_frameBuffer = m_utilities->getLogicalDevice()->createFramebuffer(std::move(params)); + + if (!m_frameBuffer) + { + m_logger->log("Could not create frame buffer!", ILogger::ELL_ERROR); + return; + } + } + } + + nbl::core::smart_refctd_ptr m_utilities; + nbl::core::smart_refctd_ptr m_logger; + + nbl::video::CThreadSafeQueueAdapter* queue; + nbl::core::smart_refctd_ptr m_commandBuffer; + + nbl::core::smart_refctd_ptr m_frameBuffer; + + ResourcesBundle resources; +}; + +} // nbl::scene::geometrycreator + +#endif // _NBL_GEOMETRY_CREATOR_SCENE_H_INCLUDED_ \ No newline at end of file diff --git a/common/include/nbl/examples/common/InputSystem.hpp b/common/include/InputSystem.hpp similarity index 84% rename from common/include/nbl/examples/common/InputSystem.hpp rename to common/include/InputSystem.hpp index c30fc1212..c42b738d0 100644 --- a/common/include/nbl/examples/common/InputSystem.hpp +++ b/common/include/InputSystem.hpp @@ -4,19 +4,16 @@ #ifndef _NBL_EXAMPLES_COMMON_INPUT_SYSTEM_HPP_INCLUDED_ #define _NBL_EXAMPLES_COMMON_INPUT_SYSTEM_HPP_INCLUDED_ -namespace nbl::examples -{ - -class InputSystem : public core::IReferenceCounted +class InputSystem : public nbl::core::IReferenceCounted { public: template struct Channels { - core::mutex lock; + nbl::core::mutex lock; std::condition_variable added; - core::vector> channels; - core::vector timeStamps; + nbl::core::vector> channels; + nbl::core::vector timeStamps; uint32_t defaultChannelIndex = 0; }; // TODO: move to "nbl/ui/InputEventChannel.h" once the interface of this utility struct matures, also maybe rename to `Consumer` ? @@ -24,7 +21,7 @@ class InputSystem : public core::IReferenceCounted struct ChannelReader { template - inline void consumeEvents(F&& processFunc, system::logger_opt_ptr logger=nullptr) + inline void consumeEvents(F&& processFunc, nbl::system::logger_opt_ptr logger=nullptr) { auto events = channel->getEvents(); const auto frontBufferCapacity = channel->getFrontBufferCapacity(); @@ -32,7 +29,7 @@ class InputSystem : public core::IReferenceCounted { logger.log( "Detected overflow, %d unconsumed events in channel of size %d!", - system::ILogger::ELL_ERROR,events.size()-consumedCounter,frontBufferCapacity + nbl::system::ILogger::ELL_ERROR,events.size()-consumedCounter,frontBufferCapacity ); consumedCounter = events.size()-frontBufferCapacity; } @@ -41,22 +38,22 @@ class InputSystem : public core::IReferenceCounted consumedCounter = events.size(); } - core::smart_refctd_ptr channel = nullptr; + nbl::core::smart_refctd_ptr channel = nullptr; uint64_t consumedCounter = 0ull; }; - InputSystem(system::logger_opt_smart_ptr&& logger) : m_logger(std::move(logger)) {} + InputSystem(nbl::system::logger_opt_smart_ptr&& logger) : m_logger(std::move(logger)) {} - void getDefaultMouse(ChannelReader* reader) + void getDefaultMouse(ChannelReader* reader) { getDefault(m_mouse,reader); } - void getDefaultKeyboard(ChannelReader* reader) + void getDefaultKeyboard(ChannelReader* reader) { getDefault(m_keyboard,reader); } template - void add(Channels& channels, core::smart_refctd_ptr&& channel) + void add(Channels& channels, nbl::core::smart_refctd_ptr&& channel) { std::unique_lock lock(channels.lock); channels.channels.push_back(std::move(channel)); @@ -97,7 +94,7 @@ class InputSystem : public core::IReferenceCounted std::unique_lock lock(channels.lock); while (channels.channels.empty()) { - m_logger.log("Waiting For Input Device to be connected...",system::ILogger::ELL_INFO); + m_logger.log("Waiting For Input Device to be connected...",nbl::system::ILogger::ELL_INFO); channels.added.wait(lock); } @@ -162,7 +159,7 @@ class InputSystem : public core::IReferenceCounted } if(defaultIdx != newDefaultIdx) { - m_logger.log("Default InputChannel for ChannelType changed from %u to %u",system::ILogger::ELL_INFO, defaultIdx, newDefaultIdx); + m_logger.log("Default InputChannel for ChannelType changed from %u to %u",nbl::system::ILogger::ELL_INFO, defaultIdx, newDefaultIdx); defaultIdx = newDefaultIdx; channels.defaultChannelIndex = newDefaultIdx; @@ -180,10 +177,10 @@ class InputSystem : public core::IReferenceCounted reader->consumedCounter = consumedCounter; } - system::logger_opt_smart_ptr m_logger; - Channels m_mouse; - Channels m_keyboard; + nbl::system::logger_opt_smart_ptr m_logger; + Channels m_mouse; + Channels m_keyboard; }; -} + #endif diff --git a/common/include/SBasicViewParameters.hlsl b/common/include/SBasicViewParameters.hlsl new file mode 100644 index 000000000..0d0990186 --- /dev/null +++ b/common/include/SBasicViewParameters.hlsl @@ -0,0 +1,17 @@ +#ifndef _S_BASIC_VIEW_PARAMETERS_COMMON_HLSL_ +#define _S_BASIC_VIEW_PARAMETERS_COMMON_HLSL_ + +#ifdef __HLSL_VERSION +struct SBasicViewParameters //! matches CPU version size & alignment (160, 4) +{ + float4x4 MVP; + float3x4 MV; + float3x3 normalMat; +}; +#endif // _S_BASIC_VIEW_PARAMETERS_COMMON_HLSL_ + +#endif + +/* + do not remove this text, WAVE is so bad that you can get errors if no proper ending xD +*/ \ No newline at end of file diff --git a/common/include/nbl/examples/common/SimpleWindowedApplication.hpp b/common/include/SimpleWindowedApplication.hpp similarity index 99% rename from common/include/nbl/examples/common/SimpleWindowedApplication.hpp rename to common/include/SimpleWindowedApplication.hpp index ddb510eb7..802a93188 100644 --- a/common/include/nbl/examples/common/SimpleWindowedApplication.hpp +++ b/common/include/SimpleWindowedApplication.hpp @@ -88,4 +88,5 @@ class SimpleWindowedApplication : public virtual application_templates::BasicMul }; } -#endif \ No newline at end of file + +#endif // _CAMERA_IMPL_ \ No newline at end of file diff --git a/common/include/camera/CCubeProjection.hpp b/common/include/camera/CCubeProjection.hpp new file mode 100644 index 000000000..ef3d25d71 --- /dev/null +++ b/common/include/camera/CCubeProjection.hpp @@ -0,0 +1,31 @@ +#ifndef _NBL_CCUBE_PROJECTION_HPP_ +#define _NBL_CCUBE_PROJECTION_HPP_ + +#include "ILinearProjection.hpp" + +namespace nbl::hlsl +{ + +struct CCubeProjectionBase +{ + using base_t = ILinearProjection>; +}; + +//! Class providing linear cube projections with projection matrix per face of a cube, each projection matrix represents a single view-port +class CCubeProjection : public CCubeProjectionBase::base_t +{ +public: + using base_t = typename CCubeProjectionBase::base_t; + using range_t = typename base_t::range_t; + + CCubeProjection(range_t&& matrices = {}) : base_t(std::move(matrices)) {} + + range_t& getCubeFaceProjectionMatrices() + { + return base_t::getViewportMatrices(); + } +}; + +} // nbl::hlsl namespace + +#endif // _NBL_CCUBE_PROJECTION_HPP_ \ No newline at end of file diff --git a/common/include/camera/ILinearProjection.hpp b/common/include/camera/ILinearProjection.hpp new file mode 100644 index 000000000..ba39018a5 --- /dev/null +++ b/common/include/camera/ILinearProjection.hpp @@ -0,0 +1,28 @@ +#ifndef _NBL_ILINEAR_PROJECTION_HPP_ +#define _NBL_ILINEAR_PROJECTION_HPP_ + +#include "IProjection.hpp" + +namespace nbl::hlsl +{ + +//! Interface class for linear projections range storage - a projection matrix represents single view-port +template +class ILinearProjection : protected IProjection +{ +public: + using base_t = typename IProjection; + using range_t = typename base_t::range_t; + + ILinearProjection(range_t&& matrices) : base_t(matrices) {} + +protected: + inline range_t& getViewportMatrices() + { + return base_t::m_projMatrices; + } +}; + +} // nbl::hlsl namespace + +#endif // _NBL_ILINEAR_PROJECTION_HPP_ \ No newline at end of file diff --git a/common/include/camera/IProjection.hpp b/common/include/camera/IProjection.hpp new file mode 100644 index 000000000..164c48b9f --- /dev/null +++ b/common/include/camera/IProjection.hpp @@ -0,0 +1,24 @@ +#ifndef _NBL_IPROJECTION_HPP_ +#define _NBL_IPROJECTION_HPP_ + +#include + +namespace nbl::hlsl +{ +//! Interface class for projection matrices range storage +template> +requires nbl::is_any_of_v, float64_t4x4, float32_t4x4> +class IProjection +{ +public: + using value_t = std::ranges::iterator_t; + using range_t = Range; + +protected: + IProjection(const range_t& matrices) : m_projMatrices(matrices) {} + range_t m_projMatrices; +}; + +} // nbl::hlsl namespace + +#endif // _NBL_IPROJECTION_HPP_ \ No newline at end of file diff --git a/common/include/nbl/examples/PCH.hpp b/common/include/nbl/examples/PCH.hpp deleted file mode 100644 index a20984464..000000000 --- a/common/include/nbl/examples/PCH.hpp +++ /dev/null @@ -1,29 +0,0 @@ -// 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_PCH_HPP_ -#define _NBL_EXAMPLES_PCH_HPP_ - -//! Precompiled header (PCH) for Nabla Examples -/* - NOTE: currently our whole public and private interface is broken - and private headers leak to public includes -*/ - -//! Nabla declarations -#include "nabla.h" - -//! Common example interface headers -#include "nbl/examples/common/build/spirv/keys.hpp" -#include "nbl/examples/common/SimpleWindowedApplication.hpp" -#include "nbl/examples/common/MonoWindowApplication.hpp" -#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 diff --git a/common/include/nbl/examples/Tester/ITester.h b/common/include/nbl/examples/Tester/ITester.h deleted file mode 100644 index afdb01633..000000000 --- a/common/include/nbl/examples/Tester/ITester.h +++ /dev/null @@ -1,435 +0,0 @@ -#ifndef _NBL_COMMON_I_TESTER_INCLUDED_ -#define _NBL_COMMON_I_TESTER_INCLUDED_ - -#include -#include -#include -#include - -using namespace nbl; - -#include - -template -class ITester -{ -public: - struct PipelineSetupData - { - std::string shaderKey; - core::smart_refctd_ptr device; - core::smart_refctd_ptr api; - core::smart_refctd_ptr assetMgr; - core::smart_refctd_ptr logger; - video::IPhysicalDevice* physicalDevice; - uint32_t computeFamilyIndex; - }; - - void setupPipeline(const PipelineSetupData& pipleineSetupData) - { - // setting up pipeline in the constructor - m_device = core::smart_refctd_ptr(pipleineSetupData.device); - m_physicalDevice = pipleineSetupData.physicalDevice; - m_api = core::smart_refctd_ptr(pipleineSetupData.api); - m_assetMgr = core::smart_refctd_ptr(pipleineSetupData.assetMgr); - m_logger = core::smart_refctd_ptr(pipleineSetupData.logger); - m_queueFamily = pipleineSetupData.computeFamilyIndex; - m_semaphoreCounter = 0; - m_semaphore = m_device->createSemaphore(0); - m_cmdpool = m_device->createCommandPool(m_queueFamily, video::IGPUCommandPool::CREATE_FLAGS::RESET_COMMAND_BUFFER_BIT); - if (!m_cmdpool->createCommandBuffers(video::IGPUCommandPool::BUFFER_LEVEL::PRIMARY, 1u, &m_cmdbuf)) - logFail("Failed to create Command Buffers!\n"); - - // Load shaders, set up pipeline - core::smart_refctd_ptr shader; - { - asset::IAssetLoader::SAssetLoadParams lp = {}; - lp.logger = m_logger.get(); - lp.workingDirectory = "app_resources"; // virtual root - auto assetBundle = m_assetMgr->getAsset(pipleineSetupData.shaderKey.data(), lp); - const auto assets = assetBundle.getContents(); - if (assets.empty()) - return logFail("Could not load shader!"); - - // It would be super weird if loading a shader from a file produced more than 1 asset - assert(assets.size() == 1); - core::smart_refctd_ptr source = asset::IAsset::castDown(assets[0]); - - shader = m_device->compileShader({ source.get() }); - } - - video::IGPUDescriptorSetLayout::SBinding bindings[2] = { - { - .binding = 0, - .type = asset::IDescriptor::E_TYPE::ET_STORAGE_BUFFER, - .createFlags = video::IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - .stageFlags = ShaderStage::ESS_COMPUTE, - .count = 1 - }, - { - .binding = 1, - .type = asset::IDescriptor::E_TYPE::ET_STORAGE_BUFFER, - .createFlags = video::IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, - .stageFlags = ShaderStage::ESS_COMPUTE, - .count = 1 - } - }; - - core::smart_refctd_ptr dsLayout = m_device->createDescriptorSetLayout(bindings); - if (!dsLayout) - logFail("Failed to create a Descriptor Layout!\n"); - - m_pplnLayout = m_device->createPipelineLayout({}, core::smart_refctd_ptr(dsLayout)); - if (!m_pplnLayout) - logFail("Failed to create a Pipeline Layout!\n"); - - { - video::IGPUComputePipeline::SCreationParams params = {}; - params.layout = m_pplnLayout.get(); - params.shader.entryPoint = "main"; - params.shader.shader = shader.get(); - if (!m_device->createComputePipelines(nullptr, { ¶ms,1 }, &m_pipeline)) - logFail("Failed to create pipelines (compile & link shaders)!\n"); - } - - // Allocate memory of the input buffer - { - const size_t BufferSize = sizeof(InputTestValues) * m_testIterationCount; - - video::IGPUBuffer::SCreationParams params = {}; - params.size = BufferSize; - params.usage = video::IGPUBuffer::EUF_STORAGE_BUFFER_BIT; - core::smart_refctd_ptr inputBuff = m_device->createBuffer(std::move(params)); - if (!inputBuff) - logFail("Failed to create a GPU Buffer of size %d!\n", params.size); - - inputBuff->setObjectDebugName("emulated_float64_t output buffer"); - - video::IDeviceMemoryBacked::SDeviceMemoryRequirements reqs = inputBuff->getMemoryReqs(); - reqs.memoryTypeBits &= m_physicalDevice->getHostVisibleMemoryTypeBits(); - - m_inputBufferAllocation = m_device->allocate(reqs, inputBuff.get(), video::IDeviceMemoryAllocation::EMAF_NONE); - if (!m_inputBufferAllocation.isValid()) - logFail("Failed to allocate Device Memory compatible with our GPU Buffer!\n"); - - assert(inputBuff->getBoundMemory().memory == m_inputBufferAllocation.memory.get()); - core::smart_refctd_ptr pool = m_device->createDescriptorPoolForDSLayouts(video::IDescriptorPool::ECF_NONE, { &dsLayout.get(),1 }); - - m_ds = pool->createDescriptorSet(core::smart_refctd_ptr(dsLayout)); - { - video::IGPUDescriptorSet::SDescriptorInfo info[1]; - info[0].desc = core::smart_refctd_ptr(inputBuff); - info[0].info.buffer = { .offset = 0,.size = BufferSize }; - video::IGPUDescriptorSet::SWriteDescriptorSet writes[1] = { - {.dstSet = m_ds.get(),.binding = 0,.arrayElement = 0,.count = 1,.info = info} - }; - m_device->updateDescriptorSets(writes, {}); - } - } - - // Allocate memory of the output buffer - { - const size_t BufferSize = sizeof(TestResults) * m_testIterationCount; - - video::IGPUBuffer::SCreationParams params = {}; - params.size = BufferSize; - params.usage = video::IGPUBuffer::EUF_STORAGE_BUFFER_BIT; - core::smart_refctd_ptr outputBuff = m_device->createBuffer(std::move(params)); - if (!outputBuff) - logFail("Failed to create a GPU Buffer of size %d!\n", params.size); - - outputBuff->setObjectDebugName("emulated_float64_t output buffer"); - - video::IDeviceMemoryBacked::SDeviceMemoryRequirements reqs = outputBuff->getMemoryReqs(); - reqs.memoryTypeBits &= m_physicalDevice->getHostVisibleMemoryTypeBits(); - - m_outputBufferAllocation = m_device->allocate(reqs, outputBuff.get(), video::IDeviceMemoryAllocation::EMAF_NONE); - if (!m_outputBufferAllocation.isValid()) - logFail("Failed to allocate Device Memory compatible with our GPU Buffer!\n"); - - assert(outputBuff->getBoundMemory().memory == m_outputBufferAllocation.memory.get()); - core::smart_refctd_ptr pool = m_device->createDescriptorPoolForDSLayouts(video::IDescriptorPool::ECF_NONE, { &dsLayout.get(),1 }); - - { - video::IGPUDescriptorSet::SDescriptorInfo info[1]; - info[0].desc = core::smart_refctd_ptr(outputBuff); - info[0].info.buffer = { .offset = 0,.size = BufferSize }; - video::IGPUDescriptorSet::SWriteDescriptorSet writes[1] = { - {.dstSet = m_ds.get(),.binding = 1,.arrayElement = 0,.count = 1,.info = info} - }; - m_device->updateDescriptorSets(writes, {}); - } - } - - if (!m_outputBufferAllocation.memory->map({ 0ull,m_outputBufferAllocation.memory->getAllocationSize() }, video::IDeviceMemoryAllocation::EMCAF_READ)) - logFail("Failed to map the Device Memory!\n"); - - // if the mapping is not coherent the range needs to be invalidated to pull in new data for the CPU's caches - const video::ILogicalDevice::MappedMemoryRange memoryRange(m_outputBufferAllocation.memory.get(), 0ull, m_outputBufferAllocation.memory->getAllocationSize()); - if (!m_outputBufferAllocation.memory->getMemoryPropertyFlags().hasFlags(video::IDeviceMemoryAllocation::EMPF_HOST_COHERENT_BIT)) - m_device->invalidateMappedMemoryRanges(1, &memoryRange); - - assert(memoryRange.valid() && memoryRange.length >= sizeof(TestResults)); - - m_queue = m_device->getQueue(m_queueFamily, 0); - } - - bool performTestsAndVerifyResults(const std::string& logFileName) - { - m_logFile.open(logFileName, std::ios::out | std::ios::trunc); - if (!m_logFile.is_open()) - m_logger->log("Failed to open log file!", system::ILogger::ELL_ERROR); - - core::vector inputTestValues; - core::vector exceptedTestResults; - - inputTestValues.reserve(m_testIterationCount); - exceptedTestResults.reserve(m_testIterationCount); - - m_logger->log("TESTS:", system::ILogger::ELL_PERFORMANCE); - for (int i = 0; i < m_testIterationCount; ++i) - { - // Set input thest values that will be used in both CPU and GPU tests - InputTestValues testInput = generateInputTestValues(); - // use std library or glm functions to determine expected test values, the output of functions from intrinsics.hlsl will be verified against these values - TestResults expected = determineExpectedResults(testInput); - - inputTestValues.push_back(testInput); - exceptedTestResults.push_back(expected); - } - - core::vector cpuTestResults = performCpuTests(inputTestValues); - core::vector gpuTestResults = performGpuTests(inputTestValues); - - bool pass = verifyAllTestResults(cpuTestResults, gpuTestResults, exceptedTestResults); - - m_logger->log("TESTS DONE.", system::ILogger::ELL_PERFORMANCE); - reloadSeed(); - - m_logFile.close(); - return pass; - } - - virtual ~ITester() - { - m_outputBufferAllocation.memory->unmap(); - }; - -protected: - enum class TestType - { - CPU, - GPU - }; - - /** - * @param testBatchCount one test batch is equal to m_WorkgroupSize, so number of tests performed will be m_WorkgroupSize * testbatchCount - */ - ITester(const uint32_t testBatchCount) - : m_testBatchCount(testBatchCount), m_testIterationCount(testBatchCount * m_WorkgroupSize) - { - reloadSeed(); - }; - - virtual bool verifyTestResults(const TestResults& expectedTestValues, const TestResults& testValues, const size_t testIteration, const uint32_t seed, TestType testType) = 0; - - virtual InputTestValues generateInputTestValues() = 0; - - virtual TestResults determineExpectedResults(const InputTestValues& testInput) = 0; - - std::mt19937& getRandomEngine() - { - return m_mersenneTwister; - } - -protected: - uint32_t m_queueFamily; - core::smart_refctd_ptr m_device; - core::smart_refctd_ptr m_api; - video::IPhysicalDevice* m_physicalDevice; - core::smart_refctd_ptr m_assetMgr; - core::smart_refctd_ptr m_logger; - video::IDeviceMemoryAllocator::SAllocation m_inputBufferAllocation = {}; - video::IDeviceMemoryAllocator::SAllocation m_outputBufferAllocation = {}; - core::smart_refctd_ptr m_cmdbuf = nullptr; - core::smart_refctd_ptr m_cmdpool = nullptr; - core::smart_refctd_ptr m_ds = nullptr; - core::smart_refctd_ptr m_pplnLayout = nullptr; - core::smart_refctd_ptr m_pipeline; - core::smart_refctd_ptr m_semaphore; - video::IQueue* m_queue; - uint64_t m_semaphoreCounter; - - void dispatchGpuTests(const core::vector& input, core::vector& output) - { - // Update input buffer - if (!m_inputBufferAllocation.memory->map({ 0ull,m_inputBufferAllocation.memory->getAllocationSize() }, video::IDeviceMemoryAllocation::EMCAF_READ)) - logFail("Failed to map the Device Memory!\n"); - - const video::ILogicalDevice::MappedMemoryRange memoryRange(m_inputBufferAllocation.memory.get(), 0ull, m_inputBufferAllocation.memory->getAllocationSize()); - if (!m_inputBufferAllocation.memory->getMemoryPropertyFlags().hasFlags(video::IDeviceMemoryAllocation::EMPF_HOST_COHERENT_BIT)) - m_device->invalidateMappedMemoryRanges(1, &memoryRange); - - assert(m_testIterationCount == input.size()); - const size_t inputDataSize = sizeof(InputTestValues) * m_testIterationCount; - std::memcpy(static_cast(m_inputBufferAllocation.memory->getMappedPointer()), input.data(), inputDataSize); - - m_inputBufferAllocation.memory->unmap(); - - // record command buffer - const uint32_t dispatchSizeX = m_testBatchCount; - m_cmdbuf->reset(video::IGPUCommandBuffer::RESET_FLAGS::NONE); - m_cmdbuf->begin(video::IGPUCommandBuffer::USAGE::NONE); - m_cmdbuf->beginDebugMarker("test", core::vector4df_SIMD(0, 1, 0, 1)); - m_cmdbuf->bindComputePipeline(m_pipeline.get()); - m_cmdbuf->bindDescriptorSets(nbl::asset::EPBP_COMPUTE, m_pplnLayout.get(), 0, 1, &m_ds.get()); - m_cmdbuf->dispatch(dispatchSizeX, 1, 1); - m_cmdbuf->endDebugMarker(); - m_cmdbuf->end(); - - video::IQueue::SSubmitInfo submitInfos[1] = {}; - const video::IQueue::SSubmitInfo::SCommandBufferInfo cmdbufs[] = { {.cmdbuf = m_cmdbuf.get()} }; - submitInfos[0].commandBuffers = cmdbufs; - const video::IQueue::SSubmitInfo::SSemaphoreInfo signals[] = { {.semaphore = m_semaphore.get(), .value = ++m_semaphoreCounter, .stageMask = asset::PIPELINE_STAGE_FLAGS::COMPUTE_SHADER_BIT} }; - submitInfos[0].signalSemaphores = signals; - - m_api->startCapture(); - m_queue->submit(submitInfos); - m_api->endCapture(); - - m_device->waitIdle(); - - // save test results - assert(m_testIterationCount == output.size()); - const size_t outputDataSize = sizeof(TestResults) * m_testIterationCount; - std::memcpy(output.data(), static_cast(m_outputBufferAllocation.memory->getMappedPointer()), outputDataSize); - - m_device->waitIdle(); - } - - template - bool verifyTestValue(const std::string& memberName, const T& expectedVal, const T& testVal, - const size_t testIteration, const uint32_t seed, const TestType testType, const float64_t maxAllowedDifference = 0.0) - { - if (compareTestValues(expectedVal, testVal, maxAllowedDifference)) - return true; - - printTestFail(memberName, expectedVal, testVal, testIteration, seed, testType); - if constexpr (std::is_same_v) - { - auto& record = m_maxErrors[memberName]; - record.abs = hlsl::max(hlsl::abs(expectedVal-testVal),record.abs); - record.rel = hlsl::max(hlsl::max(expectedVal/testVal,testVal/expectedVal)-1.f,record.rel); - } - return false; - } - - template - void printTestFail(const std::string& memberName, const T& expectedVal, const T& testVal, - const size_t testIteration, const uint32_t seed, const TestType testType) - { - std::stringstream ss; - switch (testType) - { - case TestType::CPU: - ss << "CPU TEST ERROR:\n"; - break; - case TestType::GPU: - ss << "GPU TEST ERROR:\n"; - } - - ss << "nbl::hlsl::" << memberName << " produced incorrect output!" << '\n'; - ss << "TEST ITERATION INDEX: " << testIteration << " SEED: " << seed << '\n'; - // TODO: `system::to_string` doesn't print floats exactly - ss << "EXPECTED VALUE: " << system::to_string(expectedVal) << " TEST VALUE: " << system::to_string(testVal) << '\n'; - - m_logger->log("%s", system::ILogger::ELL_ERROR, ss.str().c_str()); - m_logFile << ss.str() << '\n'; - } - -private: - template - inline void logFail(const char* msg, Args&&... args) - { - m_logger->log(msg, system::ILogger::ELL_ERROR, std::forward(args)...); - exit(-1); - } - - core::vector performCpuTests(const core::vector& inputTestValues) - { - core::vector output(m_testIterationCount); - TestExecutor testExecutor; - - auto iterations = std::views::iota(0ull, m_testIterationCount); - std::for_each(std::execution::par_unseq, iterations.begin(), iterations.end(), - [&](size_t i) - { - testExecutor(inputTestValues[i], output[i]); - } - ); - - return output; - } - - core::vector performGpuTests(const core::vector& inputTestValues) - { - core::vector output(m_testIterationCount); - dispatchGpuTests(inputTestValues, output); - - return output; - } - - bool verifyAllTestResults(const core::vector& cpuTestReults, const core::vector& gpuTestReults, const core::vector& exceptedTestReults) - { - bool pass = true; - for (int i = 0; i < m_testIterationCount; ++i) - { - pass = verifyTestResults(exceptedTestReults[i], cpuTestReults[i], i, m_seed, ITester::TestType::CPU) && pass; - pass = verifyTestResults(exceptedTestReults[i], gpuTestReults[i], i, m_seed, ITester::TestType::GPU) && pass; - } - std::stringstream maxErrors; - for (const auto& error : m_maxErrors) - { - maxErrors << "Max Error nbl::hlsl::" << error.first << " abs: " << error.second.abs << " rel: " << error.second.rel << "\n"; - } - if (const auto str = maxErrors.str(); !str.empty()) - m_logger->log("Max Errors \n %s",system::ILogger::ELL_ERROR,str.c_str()); - return pass; - } - - void reloadSeed() - { - std::random_device rd; - m_seed = rd(); - m_mersenneTwister = std::mt19937(m_seed); - } - - template - bool compareTestValues(const T& lhs, const T& rhs, const float64_t maxAllowedDifference) - { - return lhs == rhs; - } - template requires concepts::FloatingPointLikeScalar || concepts::FloatingPointLikeVectorial || (concepts::Matricial && concepts::FloatingPointLikeScalar::scalar_type>) - bool compareTestValues(const T& lhs, const T& rhs, const float64_t maxAllowedDifference) - { - return nbl::hlsl::testing::relativeApproxCompare(lhs, rhs, maxAllowedDifference); - } - - const size_t m_testIterationCount; - const uint32_t m_testBatchCount; - static constexpr size_t m_WorkgroupSize = 256u; - // seed will change after every call to performTestsAndVerifyResults() - std::mt19937 m_mersenneTwister; - uint32_t m_seed; - std::ofstream m_logFile; - // TODO support more types - template - struct SMaxError - { - T abs = 0.f; - T rel = 0.f; - }; - core::unordered_map> m_maxErrors; -}; - -#endif \ No newline at end of file diff --git a/common/include/nbl/examples/common/BuiltinResourcesApplication.hpp b/common/include/nbl/examples/common/BuiltinResourcesApplication.hpp deleted file mode 100644 index 19a5482a0..000000000 --- a/common/include/nbl/examples/common/BuiltinResourcesApplication.hpp +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright (C) 2023-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_BUILTIN_RESOURCE_APPLICATION_HPP_INCLUDED_ -#define _NBL_EXAMPLES_BUILTIN_RESOURCE_APPLICATION_HPP_INCLUDED_ - -// we need a system, logger and an asset manager -#include "nbl/application_templates/MonoAssetManagerApplication.hpp" - -#ifdef NBL_EMBED_BUILTIN_RESOURCES - #include "nbl/builtin/examples/include/CArchive.h" - #include "nbl/builtin/examples/src/CArchive.h" - #include "nbl/builtin/examples/build/CArchive.h" - #if __has_include("nbl/this_example/builtin/CArchive.h") - #include "nbl/this_example/builtin/CArchive.h" - #endif - #if __has_include("nbl/this_example/builtin/build/CArchive.h") - #include "nbl/this_example/builtin/build/CArchive.h" - #endif -#endif - -namespace nbl::examples -{ - -// Virtual Inheritance because apps might end up doing diamond inheritance -class BuiltinResourcesApplication : public virtual application_templates::MonoAssetManagerApplication -{ - using base_t = MonoAssetManagerApplication; - - public: - using base_t::base_t; - - protected: - // need this one for skipping passing all args into ApplicationFramework - BuiltinResourcesApplication() = default; - - virtual bool onAppInitialized(core::smart_refctd_ptr&& system) override - { - if (!base_t::onAppInitialized(std::move(system))) - return false; - - using namespace core; - - smart_refctd_ptr examplesHeaderArch,examplesSourceArch,examplesBuildArch,thisExampleArch, thisExampleBuildArch; - #ifdef NBL_EMBED_BUILTIN_RESOURCES - examplesHeaderArch = core::make_smart_refctd_ptr(smart_refctd_ptr(m_logger)); - examplesSourceArch = core::make_smart_refctd_ptr(smart_refctd_ptr(m_logger)); - examplesBuildArch = core::make_smart_refctd_ptr(smart_refctd_ptr(m_logger)); - - #ifdef _NBL_THIS_EXAMPLE_BUILTIN_C_ARCHIVE_H_ - thisExampleArch = make_smart_refctd_ptr(smart_refctd_ptr(m_logger)); - #endif - - #ifdef _NBL_THIS_EXAMPLE_BUILTIN_BUILD_C_ARCHIVE_H_ - thisExampleBuildArch = make_smart_refctd_ptr(smart_refctd_ptr(m_logger)); - #endif - - #else - examplesHeaderArch = make_smart_refctd_ptr(localInputCWD/"../common/include/nbl/examples",smart_refctd_ptr(m_logger),m_system.get()); - examplesSourceArch = make_smart_refctd_ptr(localInputCWD/"../common/src/nbl/examples",smart_refctd_ptr(m_logger),m_system.get()); - examplesBuildArch = make_smart_refctd_ptr(NBL_EXAMPLES_BUILD_MOUNT_POINT, smart_refctd_ptr(m_logger), m_system.get()); - thisExampleArch = make_smart_refctd_ptr(localInputCWD/"app_resources",smart_refctd_ptr(m_logger),m_system.get()); - #ifdef NBL_THIS_EXAMPLE_BUILD_MOUNT_POINT - thisExampleBuildArch = make_smart_refctd_ptr(NBL_THIS_EXAMPLE_BUILD_MOUNT_POINT, smart_refctd_ptr(m_logger), m_system.get()); - #endif - #endif - // yes all 3 aliases are meant to be the same - m_system->mount(std::move(examplesHeaderArch),"nbl/examples"); - m_system->mount(std::move(examplesSourceArch),"nbl/examples"); - m_system->mount(std::move(examplesBuildArch),"nbl/examples"); - if (thisExampleArch) - m_system->mount(std::move(thisExampleArch),"app_resources"); - - if(thisExampleBuildArch) - m_system->mount(std::move(thisExampleBuildArch), "app_resources"); - - return true; - } -}; - -} - -#endif // _NBL_EXAMPLES_BUILTIN_RESOURCE_APPLICATION_HPP_INCLUDED_ \ No newline at end of file diff --git a/common/include/nbl/examples/common/CCachedOwenScrambledSequence.hpp b/common/include/nbl/examples/common/CCachedOwenScrambledSequence.hpp deleted file mode 100644 index 52c6dfb08..000000000 --- a/common/include/nbl/examples/common/CCachedOwenScrambledSequence.hpp +++ /dev/null @@ -1,198 +0,0 @@ -// Copyright (C) 2023-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 -#ifndef _NBL_EXAMPLES_COMMON_C_CACHED_OWEN_SCRAMBLED_SEQUENCE_HPP_INCLUDED_ -#define _NBL_EXAMPLES_COMMON_C_CACHED_OWEN_SCRAMBLED_SEQUENCE_HPP_INCLUDED_ - - -#include "nbl/builtin/hlsl/sampling/quantized_sequence.hlsl" -#include - - -namespace nbl::examples -{ - -// Each Atom of the Quantized Sample Sequence provides 3N dimensions (3 for BxDF, 3 for NEE, etc.) -// If we implement Heitz's Ranking and Scrambling Blue noise then each pixel gets its own scramble (texture read) - thats fine -// but it also gets a rank scramble, meaning that for the same sample ID within a progressive render, the sampleID will be scrambled. -// Since the sequence can be several MB, it would make sense to keep samples together first, then dimensions. -// Then Atoms are ordered by sampleID, then dimension (cache will be fully trashed by tracing TLASes until next bounce) -class CCachedOwenScrambledSequence final : public core::IReferenceCounted -{ - public: - // for 1024 spp renders `uint32_t` would have been enough - using sequence_type = hlsl::sampling::QuantizedSequence; - - struct SCacheHeader - { - constexpr static inline const char* Magic = "NBL_LDS_CACHE"; - constexpr static inline size_t MagicLen = std::string_view(Magic).size(); - - inline uint64_t sequenceByteSize() const - { - const uint32_t quantizedDimensions = (maxDimensions + 2u) / 3u; - return quantizedDimensions * sizeof(sequence_type) << maxSamplesLog2; - } - - uint32_t maxSamplesLog2 : 5 = 24; - uint32_t maxDimensions : 27 = 96; - }; - constexpr static inline size_t HeaderSize = SCacheHeader::MagicLen+sizeof(SCacheHeader); - - struct SCreationParams - { - constexpr static inline const char* DefaultFilename = "owen_sampler_buffer.bin"; - - inline operator bool() const {return assMan && !cachePath.empty();} - - std::string cachePath = DefaultFilename; - asset::IAssetManager* assMan = nullptr; - SCacheHeader header = {}; - }; - - static inline core::smart_refctd_ptr create(const SCreationParams& params) - { - if (!params) - return nullptr; - - using namespace nbl::core; - using namespace nbl::system; - using namespace nbl::asset; - using namespace nbl::video; - - // read cache file - SCacheHeader oldHeader = {.maxSamplesLog2=0,.maxDimensions=0}; - smart_refctd_ptr oldBuffer; - { - IAssetLoader::SAssetLoadParams loadParams = {}; - loadParams.cacheFlags = IAssetLoader::E_CACHING_FLAGS::ECF_DUPLICATE_REFERENCES; - auto bundle = params.assMan->getAsset(params.cachePath,{}); - if (const auto contents=bundle.getContents(); contents.size() && bundle.getAssetType()==IAsset::E_TYPE::ET_BUFFER) - { - oldBuffer = IAsset::castDown(*contents.begin()); - // check the magic number - if (oldBuffer->getSize()>HeaderSize && memcmp(oldBuffer->getPointer(),SCacheHeader::Magic,SCacheHeader::MagicLen)==0) - { - oldHeader = *reinterpret_cast(reinterpret_cast(oldBuffer->getPointer())+SCacheHeader::MagicLen); - if (oldBuffer->getSize()!=oldHeader.sequenceByteSize()+HeaderSize) - oldHeader = {.maxSamplesLog2=0,.maxDimensions=0}; - } - } - } - - ICPUBuffer::SCreationParams bufparams = {}; - bufparams.usage = asset::IBuffer::EUF_TRANSFER_DST_BIT | asset::IBuffer::EUF_STORAGE_BUFFER_BIT | asset::IBuffer::EUF_SHADER_DEVICE_ADDRESS_BIT; - bufparams.size = params.header.sequenceByteSize(); - auto buffer = ICPUBuffer::create(std::move(bufparams)); - if (!buffer) - return nullptr; - - // keep on getting bigger and bigger - const bool oldFullyContains = oldHeader.maxSamplesLog2>=params.header.maxSamplesLog2 && oldHeader.maxDimensions>=params.header.maxDimensions; - - auto* const system = params.assMan->getSystem(); - if (!oldFullyContains) - system->deleteFile(params.cachePath); - - auto* const out = reinterpret_cast(buffer->getPointer()); - // generate missing bits of the sequence - { - std::unique_ptr> sampler; - if (!oldFullyContains) - sampler = std::make_unique>(params.header.maxDimensions,0xdeadbeefu); // TODO: put the seed in the header to check or replace - // - const sequence_type* const in = oldBuffer ? reinterpret_cast(reinterpret_cast(oldBuffer->getPointer())+HeaderSize):nullptr; - // thread this so it doesn't take forever - const auto range = std::ranges::iota_view{0u,params.header.maxDimensions}; - std::for_each(std::execution::par,range.begin(),range.end(),[out,params,&sampler,oldHeader,in](const uint32_t dim)->void - { - const uint32_t quant_dim = dim / 3u; - const uint32_t quant_comp = dim % 3; - auto* const outDimSamples = out+(quant_dim<=params.header.maxSamplesLog2) - return; - firstInvalidSample = 0x1u << oldHeader.maxSamplesLog2; - } - const auto dimSampler = sampler->prepareDimension(dim); - // generate samples that werent in the original sequence - for (uint32_t i=firstInvalidSample; (i>>params.header.maxSamplesLog2)==0; i++) - { - const auto _sample = dimSampler.sample(i); - outDimSamples[i].set(quant_comp,_sample); - const auto recovered = outDimSamples[i].get(quant_comp); - assert(recovered==_sample>>11); - } - } - ); - } -#if 0 - for (auto d=0u; d<(params.header.maxDimensions+2)/3; d++) - { - core::vector stratification[3]; // TODO: check stratification and (t,s) sequence property in base 2 - printf("Dimension Triplet %d\n",d); - for (auto s=0u; s<(0x1u<(hlsl::uint32_t3(0,0,0)); - printf("{%f,%f,%f}\n",fp.x,fp.y,fp.z); - } - } -#endif - if (!oldFullyContains) - { - IFile::success_t succ; - { - // TODO: until Arek makes an option to create directories on the way on a new file path - const auto dir = path(params.cachePath).parent_path(); - if (!system->exists(dir,IFileBase::E_CREATE_FLAGS::ECF_WRITE)) - system->createDirectory(dir); - smart_refctd_ptr file; - { - ISystem::future_t> future; - system->createFile(future,params.cachePath,IFile::ECF_WRITE); - if (auto lock=future.acquire(); lock) - lock.move_into(file); - } - if (file) - { - IFile::success_t succ2; - file->write(succ2,SCacheHeader::Magic,0,SCacheHeader::MagicLen); - if (succ2) - { - IFile::success_t succ1; - file->write(succ1,¶ms.header,SCacheHeader::MagicLen,sizeof(params.header)); - if (succ1) - file->write(succ,out,HeaderSize,buffer->getSize()); - } - } - } - if (!succ) - system->deleteFile(params.cachePath); - } - - return core::smart_refctd_ptr(new CCachedOwenScrambledSequence(std::move(buffer),params.header)); - } - - inline const asset::ICPUBuffer* getBuffer() const {return buffer.get();} - - inline const SCacheHeader& getHeader() const {return header;} - - private: - inline CCachedOwenScrambledSequence(core::smart_refctd_ptr&& _buffer, const SCacheHeader& _header) : buffer(std::move(_buffer)), header(_header) {} - - core::smart_refctd_ptr buffer; - SCacheHeader header; -}; - -} - -#endif diff --git a/common/include/nbl/examples/common/CEventCallback.hpp b/common/include/nbl/examples/common/CEventCallback.hpp deleted file mode 100644 index cae6dc7de..000000000 --- a/common/include/nbl/examples/common/CEventCallback.hpp +++ /dev/null @@ -1,54 +0,0 @@ -#ifndef _NBL_EXAMPLES_COMMON_C_EVENT_CALLBACK_HPP_INCLUDED_ -#define _NBL_EXAMPLES_COMMON_C_EVENT_CALLBACK_HPP_INCLUDED_ - - -#include "nbl/video/utilities/CSimpleResizeSurface.h" - -#include "nbl/examples/common/InputSystem.hpp" - - -namespace nbl::examples -{ -class CEventCallback : public nbl::video::ISimpleManagedSurface::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)) {} - CEventCallback() {} - - void setLogger(nbl::system::logger_opt_smart_ptr& logger) - { - m_logger = logger; - } - void setInputSystem(nbl::core::smart_refctd_ptr&& m_inputSystem) - { - m_inputSystem = std::move(m_inputSystem); - } - - private: - void onMouseConnected_impl(nbl::core::smart_refctd_ptr&& mch) override - { - m_logger.log("A mouse %p has been connected", nbl::system::ILogger::ELL_INFO, mch.get()); - m_inputSystem.get()->add(m_inputSystem.get()->m_mouse, std::move(mch)); - } - void onMouseDisconnected_impl(nbl::ui::IMouseEventChannel* mch) override - { - m_logger.log("A mouse %p has been disconnected", nbl::system::ILogger::ELL_INFO, mch); - m_inputSystem.get()->remove(m_inputSystem.get()->m_mouse, mch); - } - void onKeyboardConnected_impl(nbl::core::smart_refctd_ptr&& kbch) override - { - m_logger.log("A keyboard %p has been connected", nbl::system::ILogger::ELL_INFO, kbch.get()); - m_inputSystem.get()->add(m_inputSystem.get()->m_keyboard, std::move(kbch)); - } - void onKeyboardDisconnected_impl(nbl::ui::IKeyboardEventChannel* kbch) override - { - m_logger.log("A keyboard %p has been disconnected", nbl::system::ILogger::ELL_INFO, kbch); - m_inputSystem.get()->remove(m_inputSystem.get()->m_keyboard, kbch); - } - - private: - nbl::core::smart_refctd_ptr m_inputSystem = nullptr; - nbl::system::logger_opt_smart_ptr m_logger = nullptr; -}; -} -#endif \ No newline at end of file diff --git a/common/include/nbl/examples/common/CSwapchainFramebuffersAndDepth.hpp b/common/include/nbl/examples/common/CSwapchainFramebuffersAndDepth.hpp deleted file mode 100644 index c7d780fdf..000000000 --- a/common/include/nbl/examples/common/CSwapchainFramebuffersAndDepth.hpp +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright (C) 2023-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_COMMON_C_SWAPCHAIN_FRAMEBUFFERS_AND_DEPTH_HPP_INCLUDED_ -#define _NBL_EXAMPLES_COMMON_C_SWAPCHAIN_FRAMEBUFFERS_AND_DEPTH_HPP_INCLUDED_ - -// Build on top of the previous one -#include "nbl/application_templates/BasicMultiQueueApplication.hpp" - -namespace nbl::examples -{ - -class CSwapchainFramebuffersAndDepth final : public video::CDefaultSwapchainFramebuffers -{ - using base_t = CDefaultSwapchainFramebuffers; - - public: - template - inline CSwapchainFramebuffersAndDepth(video::ILogicalDevice* device, const asset::E_FORMAT _desiredDepthFormat, Args&&... args) : base_t(device,std::forward(args)...) - { - // user didn't want any depth - if (_desiredDepthFormat==asset::EF_UNKNOWN) - return; - - using namespace nbl::asset; - using namespace nbl::video; - const IPhysicalDevice::SImageFormatPromotionRequest req = { - .originalFormat = _desiredDepthFormat, - .usages = {IGPUImage::EUF_RENDER_ATTACHMENT_BIT} - }; - m_depthFormat = m_device->getPhysicalDevice()->promoteImageFormat(req,IGPUImage::TILING::OPTIMAL); - - const static IGPURenderpass::SCreationParams::SDepthStencilAttachmentDescription depthAttachments[] = { - {{ - { - .format = m_depthFormat, - .samples = IGPUImage::ESCF_1_BIT, - .mayAlias = false - }, - /*.loadOp = */{IGPURenderpass::LOAD_OP::CLEAR}, - /*.storeOp = */{IGPURenderpass::STORE_OP::STORE}, - /*.initialLayout = */{IGPUImage::LAYOUT::UNDEFINED}, // because we clear we don't care about contents - /*.finalLayout = */{IGPUImage::LAYOUT::ATTACHMENT_OPTIMAL} - }}, - IGPURenderpass::SCreationParams::DepthStencilAttachmentsEnd - }; - m_params.depthStencilAttachments = depthAttachments; - - static IGPURenderpass::SCreationParams::SSubpassDescription subpasses[] = { - m_params.subpasses[0], - IGPURenderpass::SCreationParams::SubpassesEnd - }; - subpasses[0].depthStencilAttachment.render = { .attachmentIndex = 0,.layout = IGPUImage::LAYOUT::ATTACHMENT_OPTIMAL }; - m_params.subpasses = subpasses; - } - - protected: - inline bool onCreateSwapchain_impl(const uint8_t qFam) override - { - using namespace nbl::asset; - using namespace nbl::video; - if (m_depthFormat!=asset::EF_UNKNOWN) - { - // DOCS: why are we not using `m_device` here? any particular reason? - auto device = const_cast(m_renderpass->getOriginDevice()); - - const auto depthFormat = m_renderpass->getCreationParameters().depthStencilAttachments[0].format; - const auto& sharedParams = getSwapchain()->getCreationParameters().sharedParams; - auto image = device->createImage({ IImage::SCreationParams{ - .type = IGPUImage::ET_2D, - .samples = IGPUImage::ESCF_1_BIT, - .format = depthFormat, - .extent = {sharedParams.width,sharedParams.height,1}, - .mipLevels = 1, - .arrayLayers = 1, - .depthUsage = IGPUImage::EUF_RENDER_ATTACHMENT_BIT - } }); - - device->allocate(image->getMemoryReqs(), image.get()); - - m_depthBuffer = device->createImageView({ - .flags = IGPUImageView::ECF_NONE, - .subUsages = IGPUImage::EUF_RENDER_ATTACHMENT_BIT, - .image = std::move(image), - .viewType = IGPUImageView::ET_2D, - .format = depthFormat, - .subresourceRange = {IGPUImage::EAF_DEPTH_BIT,0,1,0,1} - }); - } - const auto retval = base_t::onCreateSwapchain_impl(qFam); - m_depthBuffer = nullptr; - return retval; - } - - inline core::smart_refctd_ptr createFramebuffer(video::IGPUFramebuffer::SCreationParams&& params) override - { - if (m_depthBuffer) - params.depthStencilAttachments = &m_depthBuffer.get(); - return m_device->createFramebuffer(std::move(params)); - } - - asset::E_FORMAT m_depthFormat = asset::EF_UNKNOWN; - // only used to pass a parameter from `onCreateSwapchain_impl` to `createFramebuffer` - core::smart_refctd_ptr m_depthBuffer; -}; - -} -#endif \ No newline at end of file diff --git a/common/include/nbl/examples/common/CachedPipelineState.hpp b/common/include/nbl/examples/common/CachedPipelineState.hpp deleted file mode 100644 index 5307fba3c..000000000 --- a/common/include/nbl/examples/common/CachedPipelineState.hpp +++ /dev/null @@ -1,278 +0,0 @@ -#ifndef _NBL_EXAMPLES_COMMON_CACHED_PIPELINE_STATE_HPP_INCLUDED_ -#define _NBL_EXAMPLES_COMMON_CACHED_PIPELINE_STATE_HPP_INCLUDED_ - -#include "nbl/examples/examples.hpp" -#include "nbl/asset/utils/ISPIRVEntryPointTrimmer.h" - -#include -#include -#include -#include -#include - -namespace nbl::examples::common -{ -using pipeline_future_t = std::future>; - -template -struct SRenderPipelineStorage -{ - using shader_array_t = std::array, GeometryCount>; - using pipeline_method_array_t = std::array, MethodCount>; - using pipeline_future_method_array_t = std::array; - using pipeline_array_t = std::array; - using pipeline_future_array_t = std::array; - - std::array, BinaryToggleCount> shaders = {}; - std::array, BinaryToggleCount> pipelines = {}; - std::array, BinaryToggleCount> pendingPipelines = {}; - - static constexpr size_t boolToIndex(const bool value) - { - return static_cast(value); - } - - shader_array_t& getShaders(const bool persistentWorkGroups, const bool rwmc) - { - return shaders[boolToIndex(rwmc)][boolToIndex(persistentWorkGroups)]; - } - - const shader_array_t& getShaders(const bool persistentWorkGroups, const bool rwmc) const - { - return shaders[boolToIndex(rwmc)][boolToIndex(persistentWorkGroups)]; - } - - pipeline_array_t& getPipelines(const bool persistentWorkGroups, const bool rwmc) - { - return pipelines[boolToIndex(rwmc)][boolToIndex(persistentWorkGroups)]; - } - - const pipeline_array_t& getPipelines(const bool persistentWorkGroups, const bool rwmc) const - { - return pipelines[boolToIndex(rwmc)][boolToIndex(persistentWorkGroups)]; - } - - pipeline_future_array_t& getPendingPipelines(const bool persistentWorkGroups, const bool rwmc) - { - return pendingPipelines[boolToIndex(rwmc)][boolToIndex(persistentWorkGroups)]; - } - - const pipeline_future_array_t& getPendingPipelines(const bool persistentWorkGroups, const bool rwmc) const - { - return pendingPipelines[boolToIndex(rwmc)][boolToIndex(persistentWorkGroups)]; - } -}; - -struct SResolvePipelineState -{ - core::smart_refctd_ptr layout; - core::smart_refctd_ptr shader; - core::smart_refctd_ptr pipeline; - pipeline_future_t pendingPipeline; -}; - -template -struct SWarmupJob -{ - enum class E_TYPE : uint8_t - { - Render, - Resolve - }; - - E_TYPE type = E_TYPE::Render; - GeometryType geometry = {}; - bool persistentWorkGroups = false; - bool rwmc = false; - MethodType method = {}; -}; - -template -struct SPipelineCacheState -{ - struct STrimmedShaderCache - { - core::smart_refctd_ptr trimmer; - system::path rootDir; - system::path validationDir; - size_t loadedFromDiskCount = 0ull; - size_t generatedCount = 0ull; - size_t savedToDiskCount = 0ull; - size_t loadedBytes = 0ull; - size_t savedBytes = 0ull; - core::unordered_map> runtimeShaders; - std::mutex mutex; - } trimmedShaders; - - struct SWarmupState - { - bool started = false; - bool loggedComplete = false; - std::chrono::steady_clock::time_point beganAt = std::chrono::steady_clock::now(); - size_t budget = 1ull; - size_t queuedJobs = 0ull; - size_t launchedJobs = 0ull; - size_t skippedJobs = 0ull; - std::deque queue; - } warmup; - - core::smart_refctd_ptr object; - system::path blobPath; - bool dirty = false; - bool loadedFromDisk = false; - bool clearedOnStartup = false; - size_t loadedBytes = 0ull; - size_t savedBytes = 0ull; - size_t newlyReadyPipelinesSinceLastSave = 0ull; - bool checkpointedAfterFirstSubmit = false; - std::chrono::steady_clock::time_point lastSaveAt = std::chrono::steady_clock::now(); -}; - -struct SStartupLogState -{ - bool hasPathtraceOutput = false; - bool loggedFirstFrameLoop = false; - bool loggedFirstRenderDispatch = false; - bool loggedFirstRenderSubmit = false; -}; - -template -inline bool pollPendingPipeline(PipelineFuture& future, PipelinePtr& pipeline) -{ - if (!future.valid() || pipeline) - return false; - if (future.wait_for(std::chrono::milliseconds(0)) != std::future_status::ready) - return false; - pipeline = future.get(); - return static_cast(pipeline); -} - -template -inline bool waitForPendingPipeline(PipelineFuture& future, PipelinePtr& pipeline) -{ - if (!future.valid() || pipeline) - return false; - future.wait(); - pipeline = future.get(); - return static_cast(pipeline); -} - -template -inline size_t getRunningPipelineBuildCount( - const SRenderPipelineStorage& renderStorage, - const SResolvePipelineState& resolveState) -{ - size_t count = 0ull; - for (const auto rwmc : { false, true }) - { - for (const auto persistentWorkGroups : { false, true }) - { - const auto& futures = renderStorage.getPendingPipelines(persistentWorkGroups, rwmc); - const auto& pipelines = renderStorage.getPipelines(persistentWorkGroups, rwmc); - for (size_t geometry = 0ull; geometry < GeometryCount; ++geometry) - { - for (size_t method = 0ull; method < MethodCount; ++method) - { - if (futures[geometry][method].valid() && !pipelines[geometry][method]) - ++count; - } - } - } - } - if (resolveState.pendingPipeline.valid() && !resolveState.pipeline) - ++count; - return count; -} - -template -inline size_t getReadyRenderPipelineCount(const SRenderPipelineStorage& renderStorage) -{ - size_t count = 0ull; - for (const auto rwmc : { false, true }) - { - for (const auto persistentWorkGroups : { false, true }) - { - const auto& pipelines = renderStorage.getPipelines(persistentWorkGroups, rwmc); - for (const auto& perGeometry : pipelines) - { - for (const auto& pipeline : perGeometry) - { - if (pipeline) - ++count; - } - } - } - } - return count; -} - -template -inline void pollPendingPipelines( - SRenderPipelineStorage& renderStorage, - SResolvePipelineState& resolveState, - bool& dirty, - size_t& newlyReadyPipelinesSinceLastSave) -{ - for (const auto rwmc : { false, true }) - { - for (const auto persistentWorkGroups : { false, true }) - { - auto& pendingPipelines = renderStorage.getPendingPipelines(persistentWorkGroups, rwmc); - auto& pipelines = renderStorage.getPipelines(persistentWorkGroups, rwmc); - for (size_t geometry = 0ull; geometry < GeometryCount; ++geometry) - { - for (size_t method = 0ull; method < MethodCount; ++method) - { - if (pollPendingPipeline(pendingPipelines[geometry][method], pipelines[geometry][method])) - { - dirty = true; - ++newlyReadyPipelinesSinceLastSave; - } - } - } - } - } - - if (pollPendingPipeline(resolveState.pendingPipeline, resolveState.pipeline)) - { - dirty = true; - ++newlyReadyPipelinesSinceLastSave; - } -} - -template -inline void waitForPendingPipelines( - SRenderPipelineStorage& renderStorage, - SResolvePipelineState& resolveState, - bool& dirty, - size_t& newlyReadyPipelinesSinceLastSave) -{ - for (const auto rwmc : { false, true }) - { - for (const auto persistentWorkGroups : { false, true }) - { - auto& pendingPipelines = renderStorage.getPendingPipelines(persistentWorkGroups, rwmc); - auto& pipelines = renderStorage.getPipelines(persistentWorkGroups, rwmc); - for (size_t geometry = 0ull; geometry < GeometryCount; ++geometry) - { - for (size_t method = 0ull; method < MethodCount; ++method) - { - if (waitForPendingPipeline(pendingPipelines[geometry][method], pipelines[geometry][method])) - { - dirty = true; - ++newlyReadyPipelinesSinceLastSave; - } - } - } - } - } - - if (waitForPendingPipeline(resolveState.pendingPipeline, resolveState.pipeline)) - { - dirty = true; - ++newlyReadyPipelinesSinceLastSave; - } -} -} - -#endif diff --git a/common/include/nbl/examples/common/KeyedQuantizedSequence.hlsl b/common/include/nbl/examples/common/KeyedQuantizedSequence.hlsl deleted file mode 100644 index 5c795a023..000000000 --- a/common/include/nbl/examples/common/KeyedQuantizedSequence.hlsl +++ /dev/null @@ -1,45 +0,0 @@ -#ifndef _NBL_EXAMPLES_KEYED_QUANTIZED_SEQUENCE_HLSL_ -#define _NBL_EXAMPLES_KEYED_QUANTIZED_SEQUENCE_HLSL_ - - -#include "nbl/builtin/hlsl/sampling/quantized_sequence.hlsl" -#include "nbl/builtin/hlsl/random/xoroshiro.hlsl" - - -namespace nbl -{ -namespace hlsl -{ -namespace examples -{ - -template -struct KeyedQuantizedSequence -{ - using rng_type = RNG; // legacy - using key_rng_type = RNG; - using sequence_type = hlsl::sampling::QuantizedSequence; - using return_type = vector; - - // baseDimension: offset index of the sequence - // sampleIndex: iteration number of current pixel (samples per pixel) - return_type operator()(uint32_t baseDimension, const uint32_t sampleIndex) - { - const uint32_t address = sampleIndex|(baseDimension<(pSampleBuffer + address * sizeof(sequence_type)); - sequence_type scramble; - scramble.data[0] = rng(); - scramble.data[1] = rng(); - return tmpSeq.template decode(scramble); - } - - // could be vk::BufferPointer but no arithmetic - uint64_t pSampleBuffer; - key_rng_type rng; - uint16_t sequenceSamplesLog2; -}; - -} -} -} -#endif diff --git a/common/include/nbl/examples/common/MonoWindowApplication.hpp b/common/include/nbl/examples/common/MonoWindowApplication.hpp deleted file mode 100644 index ab7cff307..000000000 --- a/common/include/nbl/examples/common/MonoWindowApplication.hpp +++ /dev/null @@ -1,190 +0,0 @@ -// Copyright (C) 2023-2023 - 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_COMMON_MONO_WINDOW_APPLICATION_HPP_INCLUDED_ -#define _NBL_EXAMPLES_COMMON_MONO_WINDOW_APPLICATION_HPP_INCLUDED_ - -// Build on top of the previous one -#include "nbl/examples/common/SimpleWindowedApplication.hpp" -#include "nbl/examples/common/CSwapchainFramebuffersAndDepth.hpp" -#include "nbl/examples/common/CEventCallback.hpp" - -namespace nbl::examples -{ - -// Virtual Inheritance because apps might end up doing diamond inheritance -class MonoWindowApplication : public virtual SimpleWindowedApplication -{ - using base_t = SimpleWindowedApplication; - - public: - // Maximum frames which can be simultaneously submitted, used to cycle through our per-frame resources like command buffers - constexpr static inline uint8_t MaxFramesInFlight = 3; - - template - MonoWindowApplication(const hlsl::uint16_t2 _initialResolution, const asset::E_FORMAT _depthFormat, Args&&... args) : - base_t(std::forward(args)...), m_initialResolution(_initialResolution), m_depthFormat(_depthFormat) {} - - // - inline core::vector getSurfaces() const override final - { - if (!m_surface) - { - using namespace nbl::core; - using namespace nbl::ui; - using namespace nbl::video; - { - auto windowCallback = make_smart_refctd_ptr(smart_refctd_ptr(m_inputSystem),smart_refctd_ptr(m_logger)); - IWindow::SCreationParams params = {}; - params.callback = make_smart_refctd_ptr(); - params.width = m_initialResolution[0]; - params.height = m_initialResolution[1]; - params.x = 32; - params.y = 32; - params.flags = ui::IWindow::ECF_HIDDEN | IWindow::ECF_BORDERLESS | IWindow::ECF_RESIZABLE | IWindow::ECF_CAN_MINIMIZE; - params.windowCaption = "MonoWindowApplication"; - params.callback = windowCallback; - 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) = CSimpleResizeSurface::create(std::move(surface)); - } - - if (m_surface) - return { {m_surface->getSurface()/*,EQF_NONE*/} }; - - return {}; - } - - virtual inline bool onAppInitialized(core::smart_refctd_ptr&& system) override - { - using namespace nbl::core; - using namespace nbl::video; - // want to have a usable system and logger first - if (!MonoSystemMonoLoggerApplication::onAppInitialized(std::move(system))) - return false; - - m_inputSystem = make_smart_refctd_ptr(system::logger_opt_smart_ptr(smart_refctd_ptr(m_logger))); - if (!base_t::onAppInitialized(std::move(system))) - return false; - - ISwapchain::SCreationParams swapchainParams = { .surface = smart_refctd_ptr(m_surface->getSurface()) }; - if (!swapchainParams.deduceFormat(m_physicalDevice)) - return logFail("Could not choose a Surface Format for the Swapchain!"); - - // TODO: option without depth - auto scResources = std::make_unique(m_device.get(),m_depthFormat,swapchainParams.surfaceFormat.format,getDefaultSubpassDependencies()); - auto* renderpass = scResources->getRenderpass(); - - if (!renderpass) - return logFail("Failed to create Renderpass!"); - - auto gQueue = getGraphicsQueue(); - if (!m_surface || !m_surface->init(gQueue,std::move(scResources),swapchainParams.sharedParams)) - return logFail("Could not create Window & Surface or initialize the Surface!"); - - m_winMgr->setWindowSize(m_window.get(),m_initialResolution[0],m_initialResolution[1]); - m_surface->recreateSwapchain(); - - return true; - } - - // we do slight inversion of control here - inline void workLoopBody() override final - { - using namespace nbl::core; - using namespace nbl::video; - // framesInFlight: ensuring safe execution of command buffers and acquires, `framesInFlight` only affect semaphore waits, don't use this to index your resources because it can change with swapchain recreation. - const uint32_t framesInFlightCount = hlsl::min(MaxFramesInFlight,m_surface->getMaxAcquiresInFlight()); - // We block for semaphores for 2 reasons here: - // A) Resource: Can't use resource like a command buffer BEFORE previous use is finished! [MaxFramesInFlight] - // B) Acquire: Can't have more acquires in flight than a certain threshold returned by swapchain or your surface helper class. [MaxAcquiresInFlight] - if (m_framesInFlight.size()>=framesInFlightCount) - { - const ISemaphore::SWaitInfo framesDone[] = - { - { - .semaphore = m_framesInFlight.front().semaphore.get(), - .value = m_framesInFlight.front().value - } - }; - if (m_device->blockForSemaphores(framesDone)!=ISemaphore::WAIT_RESULT::SUCCESS) - return; - m_framesInFlight.pop_front(); - } - - auto updatePresentationTimestamp = [&]() - { - m_currentImageAcquire = m_surface->acquireNextImage(); - - // TODO: better frame pacing than this - oracle.reportEndFrameRecord(); - const auto timestamp = oracle.getNextPresentationTimeStamp(); - oracle.reportBeginFrameRecord(); - - return timestamp; - }; - - const auto nextPresentationTimestamp = updatePresentationTimestamp(); - - if (!m_currentImageAcquire) - return; - - const IQueue::SSubmitInfo::SSemaphoreInfo rendered[] = {renderFrame(nextPresentationTimestamp)}; - m_surface->present(m_currentImageAcquire.imageIndex,rendered); - if (rendered->semaphore) - m_framesInFlight.emplace_back(smart_refctd_ptr(rendered->semaphore),rendered->value); - } - - // - virtual inline bool keepRunning() override - { - if (m_surface->irrecoverable()) - return false; - - return true; - } - - // - virtual inline bool onAppTerminated() - { - m_inputSystem = nullptr; - if (m_device) - m_device->waitIdle(); - m_framesInFlight.clear(); - m_surface = nullptr; - m_window = nullptr; - return base_t::onAppTerminated(); - } - - protected: - inline void onAppInitializedFinish() - { - m_winMgr->show(m_window.get()); - oracle.reportBeginFrameRecord(); - } - inline const auto& getCurrentAcquire() const {return m_currentImageAcquire;} - - virtual const video::IGPURenderpass::SCreationParams::SSubpassDependency* getDefaultSubpassDependencies() const = 0; - virtual video::IQueue::SSubmitInfo::SSemaphoreInfo renderFrame(const std::chrono::microseconds nextPresentationTimestamp) = 0; - - const hlsl::uint16_t2 m_initialResolution; - const asset::E_FORMAT m_depthFormat; - core::smart_refctd_ptr m_inputSystem; - core::smart_refctd_ptr m_window; - core::smart_refctd_ptr> m_surface; - - private: - struct SSubmittedFrame - { - core::smart_refctd_ptr semaphore; - uint64_t value; - }; - core::deque m_framesInFlight; - video::ISimpleManagedSurface::SAcquireResult m_currentImageAcquire = {}; - video::CDumbPresentationOracle oracle; -}; - -} -#endif diff --git a/common/include/nbl/examples/common/SBasicViewParameters.hlsl b/common/include/nbl/examples/common/SBasicViewParameters.hlsl deleted file mode 100644 index b7ad31cb6..000000000 --- a/common/include/nbl/examples/common/SBasicViewParameters.hlsl +++ /dev/null @@ -1,29 +0,0 @@ -#ifndef _NBL_EXAMPLES_S_BASIC_VIEW_PARAMETERS_HLSL_ -#define _NBL_EXAMPLES_S_BASIC_VIEW_PARAMETERS_HLSL_ - - -#include "nbl/builtin/hlsl/cpp_compat/matrix.hlsl" - - -namespace nbl -{ -namespace hlsl -{ -namespace examples -{ - -struct SBasicViewParameters -{ - float32_t4x4 MVP; - float32_t3x4 MV; - float32_t3x3 normalMat; -}; - -} -} -} -#endif - -/* - do not remove this text, WAVE is so bad that you can get errors if no proper ending xD -*/ \ No newline at end of file diff --git a/common/include/nbl/examples/examples.hpp b/common/include/nbl/examples/examples.hpp deleted file mode 100644 index d40950501..000000000 --- a/common/include/nbl/examples/examples.hpp +++ /dev/null @@ -1,34 +0,0 @@ -// 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_HPP_ -#define _NBL_EXAMPLES_HPP_ - - -//! Precompiled header shared across all examples -#include "nbl/examples/PCH.hpp" - -//! Example specific headers that must not be included in the PCH -/* - NOTE: Add here if they depend on preprocessor definitions - or macros that are specific to individual example targets - (eg. defined in CMake) -*/ - -// #include "..." - -// cannot be in PCH because depens on definition of `this_example` for Example's builtins -#include "nbl/examples/common/BuiltinResourcesApplication.hpp" - -#define NBL_EXPOSE_NAMESPACES \ -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::scene; \ -using namespace nbl::examples; - -#endif // _NBL_EXAMPLES_HPP_ diff --git a/common/include/nbl/examples/geometry/CGeometryCreatorScene.hpp b/common/include/nbl/examples/geometry/CGeometryCreatorScene.hpp deleted file mode 100644 index 1bcbd1fd1..000000000 --- a/common/include/nbl/examples/geometry/CGeometryCreatorScene.hpp +++ /dev/null @@ -1,216 +0,0 @@ -#ifndef _NBL_EXAMPLES_C_GEOMETRY_CREATOR_SCENE_H_INCLUDED_ -#define _NBL_EXAMPLES_C_GEOMETRY_CREATOR_SCENE_H_INCLUDED_ - - -#include -#include -#include "nbl/asset/utils/CGeometryCreator.h" - -namespace nbl::examples -{ - -class CGeometryCreatorScene : public core::IReferenceCounted -{ -#define EXPOSE_NABLA_NAMESPACES \ - using namespace nbl::core; \ - using namespace nbl::system; \ - using namespace nbl::asset; \ - using namespace nbl::video - public: - - struct SGeometryEntry - { - std::string name; - core::smart_refctd_ptr geometry; - }; - - struct SCreateParams - { - video::IQueue* transferQueue; - video::IUtilities* utilities; - system::ILogger* logger; - std::span addtionalBufferOwnershipFamilies = {}; - }; - - // Creates and initializes a scene. Override addGeometries() to supply custom meshes. - template - static inline core::smart_refctd_ptr create(SCreateParams&& params, const video::CAssetConverter::patch_t& geometryPatch, Args&&... args) - { - static_assert(std::is_base_of_v); - auto scene = core::smart_refctd_ptr(new SceneT(std::forward(args)...), core::dont_grab); - if (!scene->initialize(std::move(params), geometryPatch)) - return nullptr; - return scene; - } - - // - struct SInitParams - { - core::vector> geometries; - core::vector geometryNames; - }; - const SInitParams& getInitParams() const {return m_init;} - - protected: - inline CGeometryCreatorScene() = default; - - // Override to supply custom geometries, names are used as UI labels - virtual core::vector addGeometries(asset::CGeometryCreator* creator) const - { - core::vector entries; - entries.push_back({ "Cube", creator->createCube({ 1.f,1.f,1.f }) }); - entries.push_back({ "Rectangle", creator->createRectangle({ 1.5f,3.f }) }); - entries.push_back({ "Disk", creator->createDisk(2.f, 30) }); - entries.push_back({ "Sphere", creator->createSphere(2, 16, 16) }); - entries.push_back({ "Cylinder", creator->createCylinder(2, 2, 20) }); - entries.push_back({ "Cone", creator->createCone(2, 3, 10) }); - entries.push_back({ "Icosphere", creator->createIcoSphere(1, 4, true) }); - entries.push_back({ "Grid", creator->createGrid({ 32u, 32u }) }); - return entries; - } - - inline bool initialize(SCreateParams&& params, const video::CAssetConverter::patch_t& geometryPatch) - { - EXPOSE_NABLA_NAMESPACES; - auto* logger = params.logger; - assert(logger); - if (!params.transferQueue) - { - logger->log("Pass a non-null `IQueue* transferQueue`!",ILogger::ELL_ERROR); - return false; - } - if (!params.utilities) - { - logger->log("Pass a non-null `IUtilities* utilities`!",ILogger::ELL_ERROR); - return false; - } - - SInitParams init = {}; - core::vector> geometries; - // create out geometries - { - auto creator = core::make_smart_refctd_ptr(); - auto entries = addGeometries(creator.get()); - if (entries.empty()) - return false; - - init.geometryNames.reserve(entries.size()); - geometries.reserve(entries.size()); - for (auto& entry : entries) - { - if (!entry.geometry) - continue; - init.geometryNames.emplace_back(entry.name); - geometries.push_back(std::move(entry.geometry)); - } - - if (geometries.empty()) - return false; - } - init.geometries.reserve(init.geometryNames.size()); - - // convert the geometries - { - auto device = params.utilities->getLogicalDevice(); - smart_refctd_ptr converter = CAssetConverter::create({.device=device}); - - const auto transferFamily = params.transferQueue->getFamilyIndex(); - - struct SInputs : CAssetConverter::SInputs - { - virtual inline std::span getSharedOwnershipQueueFamilies(const size_t groupCopyID, const asset::ICPUBuffer* buffer, const CAssetConverter::patch_t& patch) const - { - return sharedBufferOwnership; - } - - core::vector sharedBufferOwnership; - } inputs = {}; - core::vector> patches(geometries.size(),geometryPatch); - { - inputs.logger = logger; - std::get>(inputs.assets) = {&geometries.front().get(),geometries.size()}; - std::get>(inputs.patches) = patches; - // set up shared ownership so we don't have to - core::unordered_set families; - families.insert(transferFamily); - families.insert(params.addtionalBufferOwnershipFamilies.begin(),params.addtionalBufferOwnershipFamilies.end()); - if (families.size()>1) - for (const auto fam : families) - inputs.sharedBufferOwnership.push_back(fam); - } - - // reserve - auto reservation = converter->reserve(inputs); - if (!reservation) - { - logger->log("Failed to reserve GPU objects for CPU->GPU conversion!",ILogger::ELL_ERROR); - return false; - } - - // convert - { - auto semaphore = device->createSemaphore(0u); - - constexpr auto MultiBuffering = 2; - std::array,MultiBuffering> commandBuffers = {}; - { - auto pool = device->createCommandPool(transferFamily,IGPUCommandPool::CREATE_FLAGS::RESET_COMMAND_BUFFER_BIT|IGPUCommandPool::CREATE_FLAGS::TRANSIENT_BIT); - pool->createCommandBuffers(IGPUCommandPool::BUFFER_LEVEL::PRIMARY,commandBuffers,smart_refctd_ptr(logger)); - } - commandBuffers.front()->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT); - - std::array commandBufferSubmits; - for (auto i=0; ilog("Failed to await submission feature!", ILogger::ELL_ERROR); - return false; - } - } - - // assign outputs - { - auto inIt = reservation.getGPUObjects().data(); - for (auto outIt=init.geometryNames.begin(); outIt!=init.geometryNames.end(); inIt++) - { - if (inIt->value) - { - init.geometries.push_back(inIt->value); - outIt++; - } - else - { - logger->log("Failed to convert ICPUPolygonGeometry %s to GPU!",ILogger::ELL_ERROR,outIt->c_str()); - outIt = init.geometryNames.erase(outIt); - } - } - } - } - - m_init = std::move(init); - return true; - } - - SInitParams m_init; -#undef EXPOSE_NABLA_NAMESPACES -}; - -} -#endif diff --git a/common/include/nbl/examples/geometry/CSimpleDebugRenderer.hpp b/common/include/nbl/examples/geometry/CSimpleDebugRenderer.hpp deleted file mode 100644 index 6e5c24614..000000000 --- a/common/include/nbl/examples/geometry/CSimpleDebugRenderer.hpp +++ /dev/null @@ -1,431 +0,0 @@ -#ifndef _NBL_EXAMPLES_C_SIMPLE_DEBUG_RENDERER_H_INCLUDED_ -#define _NBL_EXAMPLES_C_SIMPLE_DEBUG_RENDERER_H_INCLUDED_ - -#include "nbl/builtin/hlsl/math/linalg/fast_affine.hlsl" -#include "nbl/examples/geometry/SPushConstants.hlsl" - -namespace nbl::examples -{ - -class CSimpleDebugRenderer final : public core::IReferenceCounted -{ -#define EXPOSE_NABLA_NAMESPACES \ - using namespace nbl::core; \ - using namespace nbl::system; \ - using namespace nbl::asset; \ - using namespace nbl::video - - public: - // - constexpr static inline uint16_t VertexAttrubUTBDescBinding = 0; - // - struct SViewParams - { - inline SViewParams(const hlsl::float32_t3x4& _view, const hlsl::float32_t4x4& _viewProj) - { - view = _view; - viewProj = _viewProj; - using namespace nbl::hlsl; - normal = transpose(inverse(float32_t3x3(view))); - } - - inline auto computeForInstance(hlsl::float32_t3x4 world) const - { - using namespace nbl::hlsl; - 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)); - retval.normal = float32_t3x3(transpose(inverse(sub3x3))); - return retval; - } - - hlsl::float32_t3x4 view; - hlsl::float32_t4x4 viewProj; - hlsl::float32_t3x3 normal; - }; - // - struct SPackedGeometry - { - core::smart_refctd_ptr pipeline = {}; - asset::SBufferBinding indexBuffer = {}; - uint32_t elementCount = 0; - // indices into the descriptor set - constexpr static inline auto MissingView = hlsl::examples::geometry_creator_scene::SPushConstants::DescriptorCount; - uint16_t positionView = MissingView; - uint16_t normalView = MissingView; - asset::E_INDEX_TYPE indexType = asset::EIT_UNKNOWN; - }; - // - struct SInstance - { - using SPushConstants = hlsl::examples::geometry_creator_scene::SPushConstants; - inline SPushConstants computePushConstants(const SViewParams& viewParams) const - { - using namespace hlsl; - return { - .matrices = viewParams.computeForInstance(world), - .positionView = packedGeo->positionView, - .normalView = packedGeo->normalView - }; - } - - hlsl::float32_t3x4 world; - const SPackedGeometry* packedGeo; - }; - - // - constexpr static inline auto DefaultPolygonGeometryPatch = []()->video::CAssetConverter::patch_t - { - // we want to use the vertex data through UTBs - using usage_f = video::IGPUBuffer::E_USAGE_FLAGS; - video::CAssetConverter::patch_t patch = {}; - patch.positionBufferUsages = usage_f::EUF_UNIFORM_TEXEL_BUFFER_BIT; - patch.indexBufferUsages = usage_f::EUF_INDEX_BUFFER_BIT; - patch.otherBufferUsages = usage_f::EUF_UNIFORM_TEXEL_BUFFER_BIT; - return patch; - }(); - - // - static inline core::smart_refctd_ptr create(asset::IAssetManager* assMan, video::IGPURenderpass* renderpass, const uint32_t subpassIX) - { - EXPOSE_NABLA_NAMESPACES; - - if (!renderpass) - return nullptr; - auto device = const_cast(renderpass->getOriginDevice()); - auto logger = device->getLogger(); - - if (!assMan) - return nullptr; - - // load shader - smart_refctd_ptr shader; - { - auto key = "nbl/examples/" + nbl::builtin::examples::build::get_spirv_key<"shaders/geometry/unified">(device); - const auto bundle = assMan->getAsset(key.data(), {}); - - const auto contents = bundle.getContents(); - if (contents.empty() || bundle.getAssetType()!=IAsset::ET_SHADER) - return nullptr; - shader = IAsset::castDown(contents[0]); - - if (!shader) - return nullptr; - } - - SInitParams init; - - // create descriptor set - { - // create Descriptor Set Layout - smart_refctd_ptr dsLayout; - { - using binding_flags_t = IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS; - const IGPUDescriptorSetLayout::SBinding bindings[] = - { - { - .binding = VertexAttrubUTBDescBinding, - .type = IDescriptor::E_TYPE::ET_UNIFORM_TEXEL_BUFFER, - // need this trifecta of flags for `SubAllocatedDescriptorSet` to accept the binding as suballocatable - .createFlags = binding_flags_t::ECF_UPDATE_AFTER_BIND_BIT|binding_flags_t::ECF_UPDATE_UNUSED_WHILE_PENDING_BIT |binding_flags_t::ECF_PARTIALLY_BOUND_BIT, - .stageFlags = IShader::E_SHADER_STAGE::ESS_VERTEX|IShader::E_SHADER_STAGE::ESS_FRAGMENT, - .count = SPackedGeometry::MissingView - } - }; - dsLayout = device->createDescriptorSetLayout(bindings); - if (!dsLayout) - { - logger->log("Could not create descriptor set layout!",ILogger::ELL_ERROR); - return nullptr; - } - } - - // create Descriptor Set - auto pool = device->createDescriptorPoolForDSLayouts(IDescriptorPool::ECF_UPDATE_AFTER_BIND_BIT,{&dsLayout.get(),1}); - auto ds = pool->createDescriptorSet(std::move(dsLayout)); - if (!ds) - { - logger->log("Could not descriptor set!",ILogger::ELL_ERROR); - return nullptr; - } - init.subAllocDS = make_smart_refctd_ptr(std::move(ds)); - } - - // create pipeline layout - const SPushConstantRange ranges[] = {{ - .stageFlags = hlsl::ShaderStage::ESS_VERTEX|hlsl::ShaderStage::ESS_FRAGMENT, - .offset = 0, - .size = sizeof(SInstance::SPushConstants), - }}; - init.layout = device->createPipelineLayout(ranges,smart_refctd_ptr(init.subAllocDS->getDescriptorSet()->getLayout())); - - // create pipelines - using pipeline_e = SInitParams::PipelineType; - { - IGPUGraphicsPipeline::SCreationParams params[pipeline_e::Count] = {}; - params[pipeline_e::BasicTriangleList].vertexShader = {.shader=shader.get(),.entryPoint="BasicVS"}; - params[pipeline_e::BasicTriangleList].fragmentShader = {.shader=shader.get(),.entryPoint="BasicFS"}; - params[pipeline_e::BasicTriangleFan].vertexShader = {.shader=shader.get(),.entryPoint="BasicVS"}; - params[pipeline_e::BasicTriangleFan].fragmentShader = {.shader=shader.get(),.entryPoint="BasicFS"}; - params[pipeline_e::GridSnakeStrip].vertexShader = { .shader = shader.get(),.entryPoint = "BasicVS" }; - params[pipeline_e::GridSnakeStrip].fragmentShader = { .shader = shader.get(),.entryPoint = "BasicFSSnake" }; - params[pipeline_e::Cone].vertexShader = {.shader=shader.get(),.entryPoint="ConeVS"}; - params[pipeline_e::Cone].fragmentShader = {.shader=shader.get(),.entryPoint="ConeFS"}; - for (auto i=0; i(i); - switch (type) - { - case pipeline_e::BasicTriangleFan: - primitiveAssembly.primitiveType = E_PRIMITIVE_TOPOLOGY::EPT_TRIANGLE_FAN; - break; - case pipeline_e::GridSnakeStrip: - primitiveAssembly.primitiveType = E_PRIMITIVE_TOPOLOGY::EPT_TRIANGLE_STRIP; - break; - default: - primitiveAssembly.primitiveType = E_PRIMITIVE_TOPOLOGY::EPT_TRIANGLE_LIST; - break; - } - primitiveAssembly.primitiveRestartEnable = false; - primitiveAssembly.tessPatchVertCount = 3; - rasterization.faceCullingMode = EFCM_NONE; - params[i].cached.subpassIx = subpassIX; - params[i].renderpass = renderpass; - } - if (!device->createGraphicsPipelines(nullptr,params,init.pipelines)) - { - logger->log("Could not create Graphics Pipelines!",ILogger::ELL_ERROR); - return nullptr; - } - } - - return smart_refctd_ptr(new CSimpleDebugRenderer(std::move(init)),dont_grab); - } - - // - static inline core::smart_refctd_ptr create(asset::IAssetManager* assMan, video::IGPURenderpass* renderpass, const uint32_t subpassIX, const std::span geometries) - { - auto retval = create(assMan,renderpass,subpassIX); - if (retval) - retval->addGeometries(geometries); - return retval; - } - - // - struct SInitParams - { - enum PipelineType : uint8_t - { - BasicTriangleList, - BasicTriangleFan, - GridSnakeStrip, - Cone, // special case - Count - }; - - core::smart_refctd_ptr subAllocDS; - core::smart_refctd_ptr layout; - core::smart_refctd_ptr pipelines[PipelineType::Count]; - }; - inline const SInitParams& getInitParams() const {return m_params;} - - // - inline bool addGeometries(const std::span geometries) - { - EXPOSE_NABLA_NAMESPACES; - if (geometries.empty()) - return false; - auto device = const_cast(m_params.layout->getOriginDevice()); - - core::vector writes; - core::vector infos; - bool anyFailed = false; - auto allocateUTB = [&](const IGeometry::SDataView& view)->decltype(SubAllocatedDescriptorSet::invalid_value) - { - if (!view) - return SPackedGeometry::MissingView; - auto index = SubAllocatedDescriptorSet::invalid_value; - if (m_params.subAllocDS->multi_allocate(VertexAttrubUTBDescBinding,1,&index)!=0) - { - anyFailed = true; - return SPackedGeometry::MissingView; - } - const auto infosOffset = infos.size(); - infos.emplace_back().desc = device->createBufferView(view.src,view.composed.format); - writes.emplace_back() = { - .dstSet = m_params.subAllocDS->getDescriptorSet(), - .binding = VertexAttrubUTBDescBinding, - .arrayElement = index, - .count = 1, - .info = reinterpret_cast(infosOffset) - }; - return index; - }; - if (anyFailed) - device->getLogger()->log("Failed to allocate a UTB for some geometries, probably ran out of space in Descriptor Set!",system::ILogger::ELL_ERROR); - - auto sizeToSet = m_geoms.size(); - auto resetGeoms = core::makeRAIIExiter([&]()->void - { - for (auto& write : writes) - immediateDealloc(write.arrayElement); - m_geoms.resize(sizeToSet); - } - ); - for (const auto geom : geometries) - { - // could also check device origin on all buffers - if (!geom->valid()) - return false; - auto& out = m_geoms.emplace_back(); - using pipeline_e = SInitParams::PipelineType; - switch (geom->getIndexingCallback()->knownTopology()) - { - case E_PRIMITIVE_TOPOLOGY::EPT_TRIANGLE_FAN: - out.pipeline = m_params.pipelines[pipeline_e::BasicTriangleFan]; - break; - case E_PRIMITIVE_TOPOLOGY::EPT_TRIANGLE_STRIP: - out.pipeline = m_params.pipelines[pipeline_e::GridSnakeStrip]; - break; - default: - out.pipeline = m_params.pipelines[pipeline_e::BasicTriangleList]; - break; - } - if (const auto& view=geom->getIndexView(); view) - { - out.indexBuffer.offset = view.src.offset; - out.indexBuffer.buffer = view.src.buffer; - switch (view.composed.format) - { - case E_FORMAT::EF_R16_UINT: - out.indexType = EIT_16BIT; - break; - case E_FORMAT::EF_R32_UINT: - out.indexType = EIT_32BIT; - break; - default: - return false; - } - } - out.elementCount = geom->getVertexReferenceCount(); - out.positionView = allocateUTB(geom->getPositionView()); - out.normalView = allocateUTB(geom->getNormalView()); - } - - // no geometry - if (infos.empty()) - return false; - - // unbase our pointers - for (auto& write : writes) - write.info = infos.data()+reinterpret_cast(write.info); - if (!device->updateDescriptorSets(writes,{})) - return false; - - // retain - writes.clear(); - sizeToSet = m_geoms.size(); - return true; - } - - // - inline void removeGeometry(const uint32_t ix, const video::ISemaphore::SWaitInfo& info) - { - EXPOSE_NABLA_NAMESPACES; - if (ix>=m_geoms.size()) - return; - - core::vector deferredFree; - deferredFree.reserve(3); - auto deallocate = [&](SubAllocatedDescriptorSet::value_type index)->void - { - if (index>=SPackedGeometry::MissingView) - return; - if (info.semaphore) - deferredFree.push_back(index); - else - immediateDealloc(index); - }; - auto geo = m_geoms.begin() + ix; - deallocate(geo->positionView); - deallocate(geo->normalView); - m_geoms.erase(geo); - - if (deferredFree.empty()) - return; - m_params.subAllocDS->multi_deallocate(VertexAttrubUTBDescBinding,deferredFree.size(),deferredFree.data(),info); - } - - // - inline void clearGeometries(const video::ISemaphore::SWaitInfo& info) - { - // back to front to avoid O(n^2) resize - while (!m_geoms.empty()) - removeGeometry(m_geoms.size()-1,info); - } - - // - inline const auto& getGeometries() const {return m_geoms;} - inline auto& getGeometry(const uint32_t ix) {return m_geoms[ix];} - - inline const auto& getInstances() const {return m_instances;} - inline auto& getInstance(const uint32_t ix) {return m_instances[ix];} - - // - inline void render(video::IGPUCommandBuffer* cmdbuf, const SViewParams& viewParams) const - { - EXPOSE_NABLA_NAMESPACES; - - cmdbuf->beginDebugMarker("CSimpleDebugRenderer::render"); - - const auto* layout = m_params.layout.get(); - const auto ds = m_params.subAllocDS->getDescriptorSet(); - cmdbuf->bindDescriptorSets(E_PIPELINE_BIND_POINT::EPBP_GRAPHICS,layout,0,1,&ds); - - for (const auto& instance : m_instances) - { - const auto* geo = instance.packedGeo; - cmdbuf->bindGraphicsPipeline(geo->pipeline.get()); - const auto pc = instance.computePushConstants(viewParams); - cmdbuf->pushConstants(layout,hlsl::ShaderStage::ESS_VERTEX|hlsl::ShaderStage::ESS_FRAGMENT,0,sizeof(pc),&pc); - if (geo->indexBuffer) - { - cmdbuf->bindIndexBuffer(geo->indexBuffer,geo->indexType); - cmdbuf->drawIndexed(geo->elementCount,1,0,0,0); - } - else - cmdbuf->draw(geo->elementCount,1,0,0); - } - cmdbuf->endDebugMarker(); - } - - core::vector m_instances; - - protected: - inline CSimpleDebugRenderer(SInitParams&& _params) : m_params(std::move(_params)) {} - inline ~CSimpleDebugRenderer() - { - // clean shutdown, can also make SubAllocatedDescriptorSet resillient against that, and issue `device->waitIdle` if not everything is freed - const_cast(m_params.layout->getOriginDevice())->waitIdle(); - clearGeometries({}); - } - - inline void immediateDealloc(video::SubAllocatedDescriptorSet::value_type index) - { - video::IGPUDescriptorSet::SDropDescriptorSet dummy[1]; - m_params.subAllocDS->multi_deallocate(dummy,VertexAttrubUTBDescBinding,1,&index); - } - - SInitParams m_params; - core::vector m_geoms; -#undef EXPOSE_NABLA_NAMESPACES -}; - -} -#endif diff --git a/common/include/nbl/examples/geometry/SPushConstants.hlsl b/common/include/nbl/examples/geometry/SPushConstants.hlsl deleted file mode 100644 index 74cbfd565..000000000 --- a/common/include/nbl/examples/geometry/SPushConstants.hlsl +++ /dev/null @@ -1,40 +0,0 @@ -#ifndef _NBL_EXAMPLES_S_PUSH_CONSTANTS_HLSL_ -#define _NBL_EXAMPLES_S_PUSH_CONSTANTS_HLSL_ - - -#include "nbl/builtin/hlsl/cpp_compat.hlsl" - - -namespace nbl -{ -namespace hlsl -{ -namespace examples -{ -namespace geometry_creator_scene -{ - -struct SInstanceMatrices -{ - float32_t4x4 worldViewProj; - float32_t3x3 normal; -}; - -struct SPushConstants -{ - NBL_CONSTEXPR_STATIC_INLINE uint32_t DescriptorCount = (0x1<<16)-1; - - SInstanceMatrices matrices; - uint32_t positionView : 16; - uint32_t normalView : 16; -}; - -} -} -} -} -#endif - -/* - do not remove this text, WAVE is so bad that you can get errors if no proper ending xD -*/ \ No newline at end of file diff --git a/common/include/nbl/examples/workgroup/DataAccessors.hlsl b/common/include/nbl/examples/workgroup/DataAccessors.hlsl deleted file mode 100644 index ca5915f2c..000000000 --- a/common/include/nbl/examples/workgroup/DataAccessors.hlsl +++ /dev/null @@ -1,131 +0,0 @@ -#ifndef _NBL_EXAMPLES_WORKGROUP_DATA_ACCESSORS_HLSL_ -#define _NBL_EXAMPLES_WORKGROUP_DATA_ACCESSORS_HLSL_ - - -#include "nbl/builtin/hlsl/bda/legacy_bda_accessor.hlsl" - - -namespace nbl -{ -namespace hlsl -{ -namespace examples -{ -namespace workgroup -{ - -struct ScratchProxy -{ - template - void get(const uint32_t ix, NBL_REF_ARG(AccessType) value) - { - value = scratch[ix]; - } - template - void set(const uint32_t ix, const AccessType value) - { - scratch[ix] = value; - } - - uint32_t atomicOr(const uint32_t ix, const uint32_t value) - { - return glsl::atomicOr(scratch[ix],value); - } - - void workgroupExecutionAndMemoryBarrier() - { - glsl::barrier(); - //glsl::memoryBarrierShared(); implied by the above - } -}; - -template -struct DataProxy -{ - using dtype_t = vector; - // function template AccessType should be the same as dtype_t - - static DataProxy create(const uint64_t inputBuf, const uint64_t outputBuf) - { - DataProxy retval; - const uint32_t workgroupOffset = glsl::gl_WorkGroupID().x * VirtualWorkgroupSize * sizeof(dtype_t); - retval.accessor = DoubleLegacyBdaAccessor::create(inputBuf + workgroupOffset, outputBuf + workgroupOffset); - return retval; - } - - template - void get(const IndexType ix, NBL_REF_ARG(AccessType) value) - { - accessor.get(ix, value); - } - template - void set(const IndexType ix, const AccessType value) - { - accessor.set(ix, value); - } - - void workgroupExecutionAndMemoryBarrier() - { - glsl::barrier(); - //glsl::memoryBarrierShared(); implied by the above - } - - DoubleLegacyBdaAccessor accessor; -}; - -template -struct PreloadedDataProxy -{ - using dtype_t = vector; - - NBL_CONSTEXPR_STATIC_INLINE uint16_t WorkgroupSize = uint16_t(1u) << WorkgroupSizeLog2; - NBL_CONSTEXPR_STATIC_INLINE uint16_t PreloadedDataCount = VirtualWorkgroupSize / WorkgroupSize; - - static PreloadedDataProxy create(const uint64_t inputBuf, const uint64_t outputBuf) - { - PreloadedDataProxy retval; - retval.data = DataProxy::create(inputBuf, outputBuf); - return retval; - } - - template - void get(const IndexType ix, NBL_REF_ARG(AccessType) value) - { - value = preloaded[ix>>WorkgroupSizeLog2]; - } - template - void set(const IndexType ix, const AccessType value) - { - preloaded[ix>>WorkgroupSizeLog2] = value; - } - - void preload() - { - const uint16_t invocationIndex = hlsl::workgroup::SubgroupContiguousIndex(); - [unroll] - for (uint16_t idx = 0; idx < PreloadedDataCount; idx++) - data.template get(idx * WorkgroupSize + invocationIndex, preloaded[idx]); - } - void unload() - { - const uint16_t invocationIndex = hlsl::workgroup::SubgroupContiguousIndex(); - [unroll] - for (uint16_t idx = 0; idx < PreloadedDataCount; idx++) - data.template set(idx * WorkgroupSize + invocationIndex, preloaded[idx]); - } - - void workgroupExecutionAndMemoryBarrier() - { - glsl::barrier(); - //glsl::memoryBarrierShared(); implied by the above - } - - DataProxy data; - dtype_t preloaded[PreloadedDataCount]; -}; - -} -} -} -} -#endif diff --git a/common/src/CMakeLists.txt b/common/src/CMakeLists.txt new file mode 100644 index 000000000..1399b949e --- /dev/null +++ b/common/src/CMakeLists.txt @@ -0,0 +1,14 @@ +# we add common libraries +# add_subdirectory(camera EXCLUDE_FROM_ALL) # header only currently +add_subdirectory(geometry EXCLUDE_FROM_ALL) + +# we get all available targets inclusive & below this directory +NBL_GET_ALL_TARGETS(NBL_SUBDIRECTORY_TARGETS) + +# then we expose common include search directories to all common libraries + create link interface +foreach(NBL_TARGET IN LISTS NBL_SUBDIRECTORY_TARGETS) + target_include_directories(${NBL_TARGET} PUBLIC $) + target_link_libraries(nblCommonAPI INTERFACE ${NBL_TARGET}) +endforeach() + +set(NBL_COMMON_API_TARGETS ${NBL_SUBDIRECTORY_TARGETS} PARENT_SCOPE) \ No newline at end of file diff --git a/common/src/camera/CMakeLists.txt b/common/src/camera/CMakeLists.txt new file mode 100644 index 000000000..eedf690aa --- /dev/null +++ b/common/src/camera/CMakeLists.txt @@ -0,0 +1,7 @@ +# header only currently + +#set(NBL_LIB_SOURCES +# "${CMAKE_CURRENT_SOURCE_DIR}/*.cpp" +#) + +#nbl_create_ext_library_project(Camera "" "${NBL_LIB_SOURCES}" "" "" "") \ No newline at end of file diff --git a/common/src/empty.cpp b/common/src/empty.cpp new file mode 100644 index 000000000..e69de29bb diff --git a/common/src/geometry/CMakeLists.txt b/common/src/geometry/CMakeLists.txt new file mode 100644 index 000000000..fb33ec637 --- /dev/null +++ b/common/src/geometry/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(creator EXCLUDE_FROM_ALL) \ No newline at end of file diff --git a/common/src/geometry/creator/CMakeLists.txt b/common/src/geometry/creator/CMakeLists.txt new file mode 100644 index 000000000..336d32fe5 --- /dev/null +++ b/common/src/geometry/creator/CMakeLists.txt @@ -0,0 +1,69 @@ +# shaders IO directories +set(NBL_THIS_EXAMPLE_INPUT_SHADERS_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/shaders") +get_filename_component(_THIS_EXAMPLE_SPIRV_BR_BUNDLE_SEARCH_DIRECTORY_ "${CMAKE_CURRENT_BINARY_DIR}/shaders/include" ABSOLUTE) +get_filename_component(_THIS_EXAMPLE_SPIRV_BR_OUTPUT_DIRECTORY_HEADER_ "${CMAKE_CURRENT_BINARY_DIR}/builtin/include" ABSOLUTE) +get_filename_component(_THIS_EXAMPLE_SPIRV_BR_OUTPUT_DIRECTORY_SOURCE_ "${CMAKE_CURRENT_BINARY_DIR}/builtin/src" ABSOLUTE) +set(NBL_THIS_EXAMPLE_OUTPUT_SPIRV_DIRECTORY "${_THIS_EXAMPLE_SPIRV_BR_BUNDLE_SEARCH_DIRECTORY_}/nbl/geometryCreator/spirv") + +# list of input source shaders +set(NBL_THIS_EXAMPLE_INPUT_SHADERS + # geometry creator + "${NBL_THIS_EXAMPLE_INPUT_SHADERS_DIRECTORY}/gc.basic.fragment.hlsl" + "${NBL_THIS_EXAMPLE_INPUT_SHADERS_DIRECTORY}/gc.basic.vertex.hlsl" + "${NBL_THIS_EXAMPLE_INPUT_SHADERS_DIRECTORY}/gc.cone.vertex.hlsl" + "${NBL_THIS_EXAMPLE_INPUT_SHADERS_DIRECTORY}/gc.ico.vertex.hlsl" + + # grid + "${NBL_THIS_EXAMPLE_INPUT_SHADERS_DIRECTORY}/grid.vertex.hlsl" + "${NBL_THIS_EXAMPLE_INPUT_SHADERS_DIRECTORY}/grid.fragment.hlsl" +) + +file(GLOB_RECURSE NBL_THIS_EXAMPLE_INPUT_COMMONS CONFIGURE_DEPENDS "${NBL_THIS_EXAMPLE_INPUT_SHADERS_DIRECTORY}/template/*.hlsl") + +include("${NBL_ROOT_PATH}/src/nbl/builtin/utils.cmake") + +foreach(NBL_INPUT_SHADER IN LISTS NBL_THIS_EXAMPLE_INPUT_SHADERS) + cmake_path(GET NBL_INPUT_SHADER FILENAME NBL_INPUT_SHADER_FILENAME) + cmake_path(GET NBL_INPUT_SHADER_FILENAME STEM LAST_ONLY NBL_SHADER_STEM) # filename without .hlsl extension + cmake_path(GET NBL_SHADER_STEM EXTENSION LAST_ONLY NBL_SHADER_TYPE) # . + + set(NBL_OUTPUT_SPIRV_FILENAME "${NBL_SHADER_STEM}.spv") + set(NBL_OUTPUT_SPIRV_PATH "${NBL_THIS_EXAMPLE_OUTPUT_SPIRV_DIRECTORY}/${NBL_OUTPUT_SPIRV_FILENAME}") + + if(NBL_SHADER_TYPE STREQUAL .vertex) + set(NBL_NSC_COMPILE_OPTIONS -T vs_6_7 -E VSMain) + elseif(NBL_SHADER_TYPE STREQUAL .geometry) + set(NBL_NSC_COMPILE_OPTIONS -T gs_6_7 -E GSMain) + elseif(NBL_SHADER_TYPE STREQUAL .fragment) + set(NBL_NSC_COMPILE_OPTIONS -T ps_6_7 -E PSMain) + else() + message(FATAL_ERROR "Input shader is supposed to be ..hlsl!") + endif() + + set(NBL_NSC_COMPILE_COMMAND + "$" + -Fc "${NBL_OUTPUT_SPIRV_PATH}" + -I "${NBL_COMMON_API_INCLUDE_DIRECTORY}" + ${NBL_NSC_COMPILE_OPTIONS} # this should come from shader's [#pragma WAVE ] but our NSC doesn't seem to work properly currently + "${NBL_INPUT_SHADER}" + ) + + set(NBL_DEPENDS + "${NBL_INPUT_SHADER}" + ${NBL_THIS_EXAMPLE_INPUT_COMMONS} + ) + + add_custom_command(OUTPUT "${NBL_OUTPUT_SPIRV_PATH}" + COMMAND ${NBL_NSC_COMPILE_COMMAND} + DEPENDS ${NBL_DEPENDS} + WORKING_DIRECTORY "${NBL_THIS_EXAMPLE_INPUT_SHADERS_DIRECTORY}" + COMMENT "Generating \"${NBL_OUTPUT_SPIRV_PATH}\"" + VERBATIM + COMMAND_EXPAND_LISTS + ) + + list(APPEND NBL_THIS_EXAMPLE_OUTPUT_SPIRV_BUILTINS "${NBL_OUTPUT_SPIRV_PATH}") + LIST_BUILTIN_RESOURCE(GEOMETRY_CREATOR_SPIRV_RESOURCES_TO_EMBED "geometryCreator/spirv/${NBL_OUTPUT_SPIRV_FILENAME}") +endforeach() + +ADD_CUSTOM_BUILTIN_RESOURCES(geometryCreatorSpirvBRD GEOMETRY_CREATOR_SPIRV_RESOURCES_TO_EMBED "${_THIS_EXAMPLE_SPIRV_BR_BUNDLE_SEARCH_DIRECTORY_}" "nbl" "geometry::creator::spirv::builtin" "${_THIS_EXAMPLE_SPIRV_BR_OUTPUT_DIRECTORY_HEADER_}" "${_THIS_EXAMPLE_SPIRV_BR_OUTPUT_DIRECTORY_SOURCE_}" "STATIC" "INTERNAL") \ No newline at end of file diff --git a/common/src/geometry/creator/shaders/gc.basic.fragment.hlsl b/common/src/geometry/creator/shaders/gc.basic.fragment.hlsl new file mode 100644 index 000000000..3dc9b9f1d --- /dev/null +++ b/common/src/geometry/creator/shaders/gc.basic.fragment.hlsl @@ -0,0 +1,6 @@ +#include "template/gc.common.hlsl" + +float4 PSMain(PSInput input) : SV_Target0 +{ + return input.color; +} \ No newline at end of file diff --git a/common/src/geometry/creator/shaders/gc.basic.vertex.hlsl b/common/src/geometry/creator/shaders/gc.basic.vertex.hlsl new file mode 100644 index 000000000..1afd468d9 --- /dev/null +++ b/common/src/geometry/creator/shaders/gc.basic.vertex.hlsl @@ -0,0 +1,6 @@ +#include "template/gc.basic.vertex.input.hlsl" +#include "template/gc.vertex.hlsl" + +/* + do not remove this text, WAVE is so bad that you can get errors if no proper ending xD +*/ diff --git a/common/src/geometry/creator/shaders/gc.cone.vertex.hlsl b/common/src/geometry/creator/shaders/gc.cone.vertex.hlsl new file mode 100644 index 000000000..ee0c42431 --- /dev/null +++ b/common/src/geometry/creator/shaders/gc.cone.vertex.hlsl @@ -0,0 +1,6 @@ +#include "template/gc.cone.vertex.input.hlsl" +#include "template/gc.vertex.hlsl" + +/* + do not remove this text, WAVE is so bad that you can get errors if no proper ending xD +*/ diff --git a/common/src/geometry/creator/shaders/gc.ico.vertex.hlsl b/common/src/geometry/creator/shaders/gc.ico.vertex.hlsl new file mode 100644 index 000000000..d63fdc809 --- /dev/null +++ b/common/src/geometry/creator/shaders/gc.ico.vertex.hlsl @@ -0,0 +1,6 @@ +#include "template/gc.ico.vertex.input.hlsl" +#include "template/gc.vertex.hlsl" + +/* + do not remove this text, WAVE is so bad that you can get errors if no proper ending xD +*/ diff --git a/common/src/geometry/creator/shaders/grid.fragment.hlsl b/common/src/geometry/creator/shaders/grid.fragment.hlsl new file mode 100644 index 000000000..4b4c1e691 --- /dev/null +++ b/common/src/geometry/creator/shaders/grid.fragment.hlsl @@ -0,0 +1,12 @@ +#include "template/grid.common.hlsl" + +float4 PSMain(PSInput input) : SV_Target0 +{ + float2 uv = (input.uv - float2(0.5, 0.5)) + 0.5 / 30.0; + float grid = gridTextureGradBox(uv, ddx(input.uv), ddy(input.uv)); + float4 fragColor = float4(1.0 - grid, 1.0 - grid, 1.0 - grid, 1.0); + fragColor *= 0.25; + fragColor *= 0.3 + 0.6 * smoothstep(0.0, 0.1, 1.0 - length(input.uv) / 5.5); + + return fragColor; +} \ No newline at end of file diff --git a/common/src/geometry/creator/shaders/grid.vertex.hlsl b/common/src/geometry/creator/shaders/grid.vertex.hlsl new file mode 100644 index 000000000..167b981d3 --- /dev/null +++ b/common/src/geometry/creator/shaders/grid.vertex.hlsl @@ -0,0 +1,17 @@ +#include "template/grid.common.hlsl" + +// set 1, binding 0 +[[vk::binding(0, 1)]] +cbuffer CameraData +{ + SBasicViewParameters params; +}; + +PSInput VSMain(VSInput input) +{ + PSInput output; + output.position = mul(params.MVP, float4(input.position, 1.0)); + output.uv = (input.uv - float2(0.5, 0.5)) * abs(input.position.xy); + + return output; +} \ No newline at end of file diff --git a/common/src/geometry/creator/shaders/template/gc.basic.vertex.input.hlsl b/common/src/geometry/creator/shaders/template/gc.basic.vertex.input.hlsl new file mode 100644 index 000000000..d9e2fa172 --- /dev/null +++ b/common/src/geometry/creator/shaders/template/gc.basic.vertex.input.hlsl @@ -0,0 +1,16 @@ +#ifndef _THIS_EXAMPLE_GC_BASIC_VERTEX_INPUT_HLSL_ +#define _THIS_EXAMPLE_GC_BASIC_VERTEX_INPUT_HLSL_ + +struct VSInput +{ + [[vk::location(0)]] float3 position : POSITION; + [[vk::location(1)]] float4 color : COLOR; + [[vk::location(2)]] float2 uv : TEXCOORD; + [[vk::location(3)]] float3 normal : NORMAL; +}; + +#endif // _THIS_EXAMPLE_GC_BASIC_VERTEX_INPUT_HLSL_ + +/* + do not remove this text, WAVE is so bad that you can get errors if no proper ending xD +*/ diff --git a/common/src/geometry/creator/shaders/template/gc.common.hlsl b/common/src/geometry/creator/shaders/template/gc.common.hlsl new file mode 100644 index 000000000..4590cd4a3 --- /dev/null +++ b/common/src/geometry/creator/shaders/template/gc.common.hlsl @@ -0,0 +1,18 @@ +#ifndef _THIS_EXAMPLE_GC_COMMON_HLSL_ +#define _THIS_EXAMPLE_GC_COMMON_HLSL_ + +#ifdef __HLSL_VERSION + struct PSInput + { + float4 position : SV_Position; + float4 color : COLOR0; + }; +#endif // __HLSL_VERSION + +#include "SBasicViewParameters.hlsl" + +#endif // _THIS_EXAMPLE_GC_COMMON_HLSL_ + +/* + do not remove this text, WAVE is so bad that you can get errors if no proper ending xD +*/ \ No newline at end of file diff --git a/common/src/geometry/creator/shaders/template/gc.cone.vertex.input.hlsl b/common/src/geometry/creator/shaders/template/gc.cone.vertex.input.hlsl new file mode 100644 index 000000000..66221fef1 --- /dev/null +++ b/common/src/geometry/creator/shaders/template/gc.cone.vertex.input.hlsl @@ -0,0 +1,15 @@ +#ifndef _THIS_EXAMPLE_GC_CONE_VERTEX_INPUT_HLSL_ +#define _THIS_EXAMPLE_GC_CONE_VERTEX_INPUT_HLSL_ + +struct VSInput +{ + [[vk::location(0)]] float3 position : POSITION; + [[vk::location(1)]] float4 color : COLOR; + [[vk::location(2)]] float3 normal : NORMAL; +}; + +#endif // _THIS_EXAMPLE_GC_CONE_VERTEX_INPUT_HLSL_ + +/* + do not remove this text, WAVE is so bad that you can get errors if no proper ending xD +*/ diff --git a/common/src/geometry/creator/shaders/template/gc.ico.vertex.input.hlsl b/common/src/geometry/creator/shaders/template/gc.ico.vertex.input.hlsl new file mode 100644 index 000000000..6b85486d9 --- /dev/null +++ b/common/src/geometry/creator/shaders/template/gc.ico.vertex.input.hlsl @@ -0,0 +1,15 @@ +#ifndef _THIS_EXAMPLE_GC_ICO_VERTEX_INPUT_HLSL_ +#define _THIS_EXAMPLE_GC_ICO_VERTEX_INPUT_HLSL_ + +struct VSInput +{ + [[vk::location(0)]] float3 position : POSITION; + [[vk::location(1)]] float3 normal : NORMAL; + [[vk::location(2)]] float2 uv : TEXCOORD; +}; + +#endif // _THIS_EXAMPLE_GC_ICO_VERTEX_INPUT_HLSL_ + +/* + do not remove this text, WAVE is so bad that you can get errors if no proper ending xD +*/ diff --git a/common/src/geometry/creator/shaders/template/gc.vertex.hlsl b/common/src/geometry/creator/shaders/template/gc.vertex.hlsl new file mode 100644 index 000000000..5a8f26722 --- /dev/null +++ b/common/src/geometry/creator/shaders/template/gc.vertex.hlsl @@ -0,0 +1,22 @@ +#include "gc.common.hlsl" + +// set 1, binding 0 +[[vk::binding(0, 1)]] +cbuffer CameraData +{ + SBasicViewParameters params; +}; + +PSInput VSMain(VSInput input) +{ + PSInput output; + + output.position = mul(params.MVP, float4(input.position, 1.0)); + output.color = float4(input.normal * 0.5 + 0.5, 1.0); + + return output; +} + +/* + do not remove this text, WAVE is so bad that you can get errors if no proper ending xD +*/ diff --git a/common/src/geometry/creator/shaders/template/grid.common.hlsl b/common/src/geometry/creator/shaders/template/grid.common.hlsl new file mode 100644 index 000000000..bc6516600 --- /dev/null +++ b/common/src/geometry/creator/shaders/template/grid.common.hlsl @@ -0,0 +1,40 @@ +#ifndef _THIS_EXAMPLE_GRID_COMMON_HLSL_ +#define _THIS_EXAMPLE_GRID_COMMON_HLSL_ + +#ifdef __HLSL_VERSION + struct VSInput + { + [[vk::location(0)]] float3 position : POSITION; + [[vk::location(1)]] float4 color : COLOR; + [[vk::location(2)]] float2 uv : TEXCOORD; + [[vk::location(3)]] float3 normal : NORMAL; + }; + + struct PSInput + { + float4 position : SV_Position; + float2 uv : TEXCOORD0; + }; + + float gridTextureGradBox(float2 p, float2 ddx, float2 ddy) + { + float N = 30.0; // grid ratio + float2 w = max(abs(ddx), abs(ddy)) + 0.01; // filter kernel + + // analytic (box) filtering + float2 a = p + 0.5 * w; + float2 b = p - 0.5 * w; + float2 i = (floor(a) + min(frac(a) * N, 1.0) - floor(b) - min(frac(b) * N, 1.0)) / (N * w); + + // pattern + return (1.0 - i.x) * (1.0 - i.y); + } +#endif // __HLSL_VERSION + +#include "SBasicViewParameters.hlsl" + +#endif // _THIS_EXAMPLE_GRID_COMMON_HLSL_ + +/* + do not remove this text, WAVE is so bad that you can get errors if no proper ending xD +*/ \ No newline at end of file diff --git a/common/src/nbl/examples/CMakeLists.txt b/common/src/nbl/examples/CMakeLists.txt deleted file mode 100644 index f3ad7fe91..000000000 --- a/common/src/nbl/examples/CMakeLists.txt +++ /dev/null @@ -1,86 +0,0 @@ -set(OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/auto-gen") -set(ARGS - # meta INTERFACE target with NSC compilation rules - TARGET NblExtExamplesAPISPIRV - - # build directory for its SPIRV outputs - BINARY_DIR ${OUTPUT_DIRECTORY} - - # preprocessor #define for BINARY_DIR bind point - MOUNT_POINT_DEFINE NBL_EXAMPLES_BUILD_MOUNT_POINT - - # extra NSC compile options - COMMON_OPTIONS -I "${COMMON_INCLUDE_DIRECTORY}" - - # out variable to which SPIRV access keys are appended to (including permutations), relative to BINARY_DIR - OUTPUT_VAR KEYS - - # include file with inline template key getters, use with #include directive on downstream targets - INCLUDE nbl/examples/common/build/spirv/keys.hpp - - # namespace for key getters in the include file - NAMESPACE nbl::builtin::examples::build -) - -# note json is array of objects, you can register all rules at once -set(JSON [=[ -[ - { - "INPUT": "shaders/geometry/unified.hlsl", - "KEY": "shaders/geometry/unified", - "COMPILE_OPTIONS": ["-T", "lib_6_6"], - "CAPS": [] - } -] -]=]) - -NBL_CREATE_NSC_COMPILE_RULES(${ARGS} INPUTS ${JSON}) - -set(JSON [=[ -[ - { - "INPUT": "shaders/geometry/unified.hlsl", - "KEY": "shaders/geometry/unified-caps", - "COMPILE_OPTIONS": ["-T", "lib_6_6"], - "CAPS": [ - { - "name": "shaderFloat64", - "type": "bool", - "values": [1, 0] - }, - { - "name": "subgroupSize", - "type": "uint16_t", - "values": [32, 64] - } - ] - }, - { - "INPUT": "shaders/geometry/unified.hlsl", - "KEY": "shaders/geometry/unified-caps-2", - "COMPILE_OPTIONS": ["-T", "lib_6_6"], - "CAPS": [ - { - "name": "shaderFloat64", - "type": "bool", - "values": [1, 0] - } - ] - } -] -]=]) - -# it also supports incremental rule updates, uncomment to add rules with permutation caps (testing purposes, remove after review) -# NBL_CREATE_NSC_COMPILE_RULES(${ARGS} INPUTS ${JSON}) - -# note we can add more inputs from build dir which keys can be part of the same archive/mount point, -# ex. one could auto generate bc texture or whatever and add here like -# file(WRITE "${OUTPUT_DIRECTORY}/dummy.txt" "dummy, test") -# list(APPEND KEYS dummy.txt) - -NBL_CREATE_RESOURCE_ARCHIVE( - TARGET NblExtExamplesAPIBuiltinsBuild - BIND "${OUTPUT_DIRECTORY}" - BUILTINS ${KEYS} - NAMESPACE nbl::builtin::examples::build -) diff --git a/common/src/nbl/examples/pch.cpp b/common/src/nbl/examples/pch.cpp deleted file mode 100644 index 39a146f1d..000000000 --- a/common/src/nbl/examples/pch.cpp +++ /dev/null @@ -1 +0,0 @@ -#include "nbl/examples/PCH.hpp" \ No newline at end of file diff --git a/common/src/nbl/examples/shaders/geometry/unified.hlsl b/common/src/nbl/examples/shaders/geometry/unified.hlsl deleted file mode 100644 index 40c90204d..000000000 --- a/common/src/nbl/examples/shaders/geometry/unified.hlsl +++ /dev/null @@ -1,82 +0,0 @@ -// -#include "nbl/examples/geometry/SPushConstants.hlsl" -using namespace nbl::hlsl; -using namespace nbl::hlsl::examples::geometry_creator_scene; - -// for dat sweet programmable pulling -[[vk::binding(0)]] Buffer utbs[SPushConstants::DescriptorCount]; - -// -[[vk::push_constant]] SPushConstants pc; - -// -struct SInterpolants -{ - float32_t4 ndc : SV_Position; - float32_t3 meta : COLOR1; -}; -#include "nbl/builtin/hlsl/math/linalg/fast_affine.hlsl" - -float32_t3 reconstructGeometricNormal(float32_t3 pos) -{ - const float32_t2x3 dPos_dScreen = float32_t2x3( - ddx(pos), - ddy(pos) - ); - return cross(dPos_dScreen[0],dPos_dScreen[1]); -} - -// -[shader("vertex")] -SInterpolants BasicVS(uint32_t VertexIndex : SV_VertexID) -{ - const float32_t3 position = utbs[pc.positionView][VertexIndex].xyz; - - SInterpolants output; - output.ndc = math::linalg::promoted_mul(pc.matrices.worldViewProj,position); - if (pc.normalView(0.5f),1.f); -} - -// 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 -{ - 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); - - 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; - - float3 col = base * (0.2 + 0.8 * grad) + rim; - return float4(col, 1.0); -} - -// TODO: do smooth normals on the cone -[shader("vertex")] -SInterpolants ConeVS(uint32_t VertexIndex : SV_VertexID) -{ - const float32_t3 position = utbs[pc.positionView][VertexIndex].xyz; - - SInterpolants output; - output.ndc = math::linalg::promoted_mul(pc.matrices.worldViewProj,position); - output.meta = mul(inverse(transpose(pc.matrices.normal)),position); - return output; -} -[shader("pixel")] -float32_t4 ConeFS(SInterpolants input) : SV_Target0 -{ - const float32_t3 normal = reconstructGeometricNormal(input.meta); - return float32_t4(normalize(normal)*0.5f+promote(0.5f),1.f); -} diff --git a/media b/media index 0f7ad42b3..a98646358 160000 --- a/media +++ b/media @@ -1 +1 @@ -Subproject commit 0f7ad42b33abe3143a5d69c4d14b26cf3e538c88 +Subproject commit a9864635879e5a616ac400eecd8b6451b498fbf1 diff --git a/old_to_refactor/03_GPU_Mesh/CMakeLists.txt b/old_to_refactor/03_GPU_Mesh/CMakeLists.txt new file mode 100644 index 000000000..a476b6203 --- /dev/null +++ b/old_to_refactor/03_GPU_Mesh/CMakeLists.txt @@ -0,0 +1,7 @@ + +include(common RESULT_VARIABLE RES) +if(NOT RES) + message(FATAL_ERROR "common.cmake not found. Should be in {repo_root}/cmake directory") +endif() + +nbl_create_executable_project("" "" "" "" "${NBL_EXECUTABLE_PROJECT_CREATION_PCH_TARGET}") \ No newline at end of file diff --git a/old_to_refactor/03_GPU_Mesh/main.cpp b/old_to_refactor/03_GPU_Mesh/main.cpp new file mode 100644 index 000000000..cc871bb9f --- /dev/null +++ b/old_to_refactor/03_GPU_Mesh/main.cpp @@ -0,0 +1,244 @@ + +#include "CCamera.hpp" + + +#include "nbl/nblpack.h" +struct VertexStruct +{ + /// every member needs to be at location aligned to its type size for GLSL + float Pos[3]; /// uses float hence need 4 byte alignment + uint8_t Col[2]; /// same logic needs 1 byte alignment + uint8_t uselessPadding[2]; /// so if there is a member with 4 byte alignment then whole struct needs 4 byte align, so pad it +} PACK_STRUCT; +#include "nbl/nblunpack.h" + +const char* vertexSource = R"===( +#version 430 core + +layout(location = 0) in vec4 vPos; //only a 3d position is passed from Nabla, but last (the W) coordinate gets filled with default 1.0 +layout(location = 1) in vec4 vCol; + +layout( push_constant, row_major ) uniform Block { + mat4 modelViewProj; +} PushConstants; + +layout(location = 0) out vec4 Color; //per vertex output color, will be interpolated across the triangle + +void main() +{ + gl_Position = PushConstants.modelViewProj*vPos; //only thing preventing the shader from being core-compliant + Color = vCol; +} +)==="; + +const char* fragmentSource = R"===( +#version 430 core + +layout(location = 0) in vec4 Color; //per vertex output color, will be interpolated across the triangle + +layout(location = 0) out vec4 pixelColor; + +void main() +{ + pixelColor = Color; +} +)==="; + +class GPUMesh : public ApplicationBase +{ + +public: + + nbl::core::smart_refctd_ptr gpuTransferFence; + nbl::core::smart_refctd_ptr gpuComputeFence; + nbl::video::IGPUObjectFromAssetConverter cpu2gpu; + + CommonAPI::InputSystem::ChannelReader mouse; + CommonAPI::InputSystem::ChannelReader keyboard; + Camera camera = Camera(vectorSIMDf(0, 0, 0), vectorSIMDf(0, 0, 0), matrix4SIMD()); + + int resourceIx = -1; + uint32_t acquiredNextFBO = {}; + std::chrono::system_clock::time_point lastTime; + bool frameDataFilled = false; + size_t frame_count = 0ull; + double time_sum = 0; + double dtList[NBL_FRAMES_TO_AVERAGE] = {}; + + core::smart_refctd_ptr frameComplete[FRAMES_IN_FLIGHT] = { nullptr }; + core::smart_refctd_ptr imageAcquire[FRAMES_IN_FLIGHT] = { nullptr }; + core::smart_refctd_ptr renderFinished[FRAMES_IN_FLIGHT] = { nullptr }; + core::smart_refctd_ptr commandBuffers[FRAMES_IN_FLIGHT]; + + nbl::video::ISwapchain::SCreationParams m_swapchainCreationParams; + + + + + void onAppInitialized_impl() override + { + + for (size_t i = 0ull; i < NBL_FRAMES_TO_AVERAGE; ++i) + dtList[i] = 0.0; + + matrix4SIMD projectionMatrix = matrix4SIMD::buildProjectionMatrixPerspectiveFovLH(core::radians(60.0f), float(WIN_W) / WIN_H, 0.1, 1000); + camera = Camera(core::vectorSIMDf(-4, 0, 0), core::vectorSIMDf(0, 0, 0), projectionMatrix); + } + + void workLoopBody() override + { + + auto renderStart = std::chrono::system_clock::now(); + const auto renderDt = std::chrono::duration_cast(renderStart - lastTime).count(); + lastTime = renderStart; + { // Calculate Simple Moving Average for FrameTime + time_sum -= dtList[frame_count]; + time_sum += renderDt; + dtList[frame_count] = renderDt; + frame_count++; + if (frame_count >= NBL_FRAMES_TO_AVERAGE) + { + frameDataFilled = true; + frame_count = 0; + } + + } + const double averageFrameTime = frameDataFilled ? (time_sum / (double)NBL_FRAMES_TO_AVERAGE) : (time_sum / frame_count); + +#ifdef NBL_MORE_LOGS + logger->log("renderDt = %f ------ averageFrameTime = %f", system::ILogger::ELL_INFO, renderDt, averageFrameTime); +#endif // NBL_MORE_LOGS + + auto averageFrameTimeDuration = std::chrono::duration(averageFrameTime); + auto nextPresentationTime = renderStart + averageFrameTimeDuration; + auto nextPresentationTimeStamp = std::chrono::duration_cast(nextPresentationTime.time_since_epoch()); + + inputSystem->getDefaultMouse(&mouse); + inputSystem->getDefaultKeyboard(&keyboard); + + camera.beginInputProcessing(nextPresentationTimeStamp); + mouse.consumeEvents([&](const ui::IMouseEventChannel::range_t& events) -> void { camera.mouseProcess(events); }, logger.get()); + keyboard.consumeEvents([&](const ui::IKeyboardEventChannel::range_t& events) -> void { camera.keyboardProcess(events); }, logger.get()); + camera.endInputProcessing(nextPresentationTimeStamp); + + const auto& mvp = camera.getConcatenatedMatrix(); + + + + + + + + + asset::SViewport viewport; + viewport.minDepth = 1.f; + viewport.maxDepth = 0.f; + viewport.x = 0u; + viewport.y = 0u; + viewport.width = WIN_W; + viewport.height = WIN_H; + commandBuffer->setViewport(0u, 1u, &viewport); + + + + + + + //! Stress test for memleaks aside from demo how to create meshes that live on the GPU RAM + { + VertexStruct vertices[8]; + vertices[0] = VertexStruct{ {-1.f,-1.f,-1.f},{ 0, 0} }; + vertices[1] = VertexStruct{ { 1.f,-1.f,-1.f},{127, 0} }; + vertices[2] = VertexStruct{ {-1.f, 1.f,-1.f},{255, 0} }; + vertices[3] = VertexStruct{ { 1.f, 1.f,-1.f},{ 0,127} }; + vertices[4] = VertexStruct{ {-1.f,-1.f, 1.f},{127,127} }; + vertices[5] = VertexStruct{ { 1.f,-1.f, 1.f},{255,127} }; + vertices[6] = VertexStruct{ {-1.f, 1.f, 1.f},{ 0,255} }; + vertices[7] = VertexStruct{ { 1.f, 1.f, 1.f},{127,255} }; + + uint16_t indices_indexed16[] = + { + 0,1,2,1,2,3, + 4,5,6,5,6,7, + 0,1,4,1,4,5, + 2,3,6,3,6,7, + 0,2,4,2,4,6, + 1,3,5,3,5,7 + }; + + // auto upStreamBuff = driver->getDefaultUpStreamingBuffer(); + // core::smart_refctd_ptr upStreamRef(upStreamBuff->getBuffer()); + + // const void* dataToPlace[2] = { vertices,indices_indexed16 }; + // uint32_t offsets[2] = { video::StreamingTransientDataBufferMT<>::invalid_address,video::StreamingTransientDataBufferMT<>::invalid_address }; + // uint32_t alignments[2] = { sizeof(decltype(vertices[0u])),sizeof(decltype(indices_indexed16[0u])) }; + // uint32_t sizes[2] = { sizeof(vertices),sizeof(indices_indexed16) }; + // upStreamBuff->multi_place(2u, (const void* const*)dataToPlace, (uint32_t*)offsets, (uint32_t*)sizes, (uint32_t*)alignments); + // if (upStreamBuff->needsManualFlushOrInvalidate()) + // { + // auto upStreamMem = upStreamBuff->getBuffer()->getBoundMemory(); + // driver->flushMappedMemoryRanges({ video::IDeviceMemoryAllocation::MappedMemoryRange(upStreamMem,offsets[0],sizes[0]),video::IDeviceMemoryAllocation::MappedMemoryRange(upStreamMem,offsets[1],sizes[1]) }); + // } + + // asset::SPushConstantRange range[1] = { asset::ISpecializedShader::ESS_VERTEX,0u,sizeof(core::matrix4SIMD) }; + + // auto createSpecializedShaderFromSource = [=](const char* source, asset::ISpecializedShader::E_SHADER_STAGE stage) + // { + // auto spirv = device->getAssetManager()->getGLSLCompiler()->createSPIRVFromGLSL(source, stage, "main", "runtimeID"); + // auto unspec = driver->createShader(std::move(spirv)); + // return driver->createSpecializedShader(unspec.get(), { nullptr,nullptr,"main",stage }); + // }; + // // origFilepath is only relevant when you have filesystem #includes in your shader + // auto createSpecializedShaderFromSourceWithIncludes = [&](const char* source, asset::ISpecializedShader::E_SHADER_STAGE stage, const char* origFilepath) + // { + // auto resolved_includes = device->getAssetManager()->getGLSLCompiler()->resolveIncludeDirectives(source, stage, origFilepath); + // return createSpecializedShaderFromSource(reinterpret_cast(resolved_includes->getContent()->getPointer()), stage); + // }; + // core::smart_refctd_ptr shaders[2] = + // { + // createSpecializedShaderFromSourceWithIncludes(vertexSource,asset::ISpecializedShader::ESS_VERTEX, "shader.vert"), + // createSpecializedShaderFromSource(fragmentSource,asset::ISpecializedShader::ESS_FRAGMENT) + // }; + // auto shadersPtr = reinterpret_cast(shaders); + + // asset::SVertexInputParams inputParams; + // inputParams.enabledAttribFlags = 0b11u; + // inputParams.enabledBindingFlags = 0b1u; + // inputParams.attributes[0].binding = 0u; + // inputParams.attributes[0].format = asset::EF_R32G32B32_SFLOAT; + // inputParams.attributes[0].relativeOffset = offsetof(VertexStruct, Pos[0]); + // inputParams.attributes[1].binding = 0u; + // inputParams.attributes[1].format = asset::EF_R8G8_UNORM; + // inputParams.attributes[1].relativeOffset = offsetof(VertexStruct, Col[0]); + // inputParams.bindings[0].stride = sizeof(VertexStruct); + // inputParams.bindings[0].inputRate = asset::EVIR_PER_VERTEX; + + // asset::SBlendParams blendParams; // defaults are sane + + // asset::SPrimitiveAssemblyParams assemblyParams = { asset::EPT_TRIANGLE_LIST,false,1u }; + + // asset::SStencilOpParams defaultStencil; + // asset::SRasterizationParams rasterParams; + // rasterParams.faceCullingMode = asset::EFCM_NONE; + // auto pipeline = driver->createRenderpassIndependentPipeline(nullptr, driver->createPipelineLayout(range, range + 1u, nullptr, nullptr, nullptr, nullptr), + // shadersPtr, shadersPtr + sizeof(shaders) / sizeof(core::smart_refctd_ptr), + // inputParams, blendParams, assemblyParams, rasterParams); + + // asset::SBufferBinding bindings[video::IGPUMeshBuffer::MAX_ATTR_BUF_BINDING_COUNT]; + // bindings[0u] = { offsets[0],upStreamRef }; + // auto mb = core::make_smart_refctd_ptr(std::move(pipeline), nullptr, bindings, asset::SBufferBinding{offsets[1], upStreamRef}); + // { + // mb->setIndexType(asset::EIT_16BIT); + // mb->setIndexCount(2 * 3 * 6); + // } + + // driver->bindGraphicsPipeline(mb->getPipeline()); + // driver->pushConstants(mb->getPipeline()->getLayout(), asset::ISpecializedShader::ESS_VERTEX, 0u, sizeof(core::matrix4SIMD), mvp.pointer()); + // driver->drawMeshBuffer(mb.get()); + + // upStreamBuff->multi_free(2u, (uint32_t*)&offsets, (uint32_t*)&sizes, driver->placeFence()); + //} + //driver->endScene(); + } + } +}; diff --git a/68_JpegLoading/pipeline.groovy b/old_to_refactor/03_GPU_Mesh/pipeline.groovy similarity index 88% rename from 68_JpegLoading/pipeline.groovy rename to old_to_refactor/03_GPU_Mesh/pipeline.groovy index ece6ffebc..b19625fa7 100644 --- a/68_JpegLoading/pipeline.groovy +++ b/old_to_refactor/03_GPU_Mesh/pipeline.groovy @@ -2,9 +2,9 @@ import org.DevshGraphicsProgramming.Agent import org.DevshGraphicsProgramming.BuilderInfo import org.DevshGraphicsProgramming.IBuilder -class CJpegLoading extends IBuilder +class CGPUMeshBuilder extends IBuilder { - public CJpegLoading(Agent _agent, _info) + public CGPUMeshBuilder(Agent _agent, _info) { super(_agent, _info) } @@ -44,7 +44,7 @@ class CJpegLoading extends IBuilder def create(Agent _agent, _info) { - return new CJpegLoading(_agent, _info) + return new CGPUMeshBuilder(_agent, _info) } return this \ No newline at end of file diff --git a/old_to_refactor/04_Keyframe/config.json.template b/old_to_refactor/04_Keyframe/config.json.template new file mode 100644 index 000000000..f961745c1 --- /dev/null +++ b/old_to_refactor/04_Keyframe/config.json.template @@ -0,0 +1,28 @@ +{ + "enableParallelBuild": true, + "threadsPerBuildProcess" : 2, + "isExecuted": false, + "scriptPath": "", + "cmake": { + "configurations": [ "Release", "Debug", "RelWithDebInfo" ], + "buildModes": [], + "requiredOptions": [] + }, + "profiles": [ + { + "backend": "vulkan", + "platform": "windows", + "buildModes": [], + "runConfiguration": "Release", + "gpuArchitectures": [] + } + ], + "dependencies": [], + "data": [ + { + "dependencies": [], + "command": [""], + "outputs": [] + } + ] +} \ No newline at end of file diff --git a/old_to_refactor/05_NablaTutorialExample/CMakeLists.txt b/old_to_refactor/05_NablaTutorialExample/CMakeLists.txt new file mode 100644 index 000000000..a476b6203 --- /dev/null +++ b/old_to_refactor/05_NablaTutorialExample/CMakeLists.txt @@ -0,0 +1,7 @@ + +include(common RESULT_VARIABLE RES) +if(NOT RES) + message(FATAL_ERROR "common.cmake not found. Should be in {repo_root}/cmake directory") +endif() + +nbl_create_executable_project("" "" "" "" "${NBL_EXECUTABLE_PROJECT_CREATION_PCH_TARGET}") \ No newline at end of file diff --git a/old_to_refactor/05_NablaTutorialExample/config.json.template b/old_to_refactor/05_NablaTutorialExample/config.json.template new file mode 100644 index 000000000..f961745c1 --- /dev/null +++ b/old_to_refactor/05_NablaTutorialExample/config.json.template @@ -0,0 +1,28 @@ +{ + "enableParallelBuild": true, + "threadsPerBuildProcess" : 2, + "isExecuted": false, + "scriptPath": "", + "cmake": { + "configurations": [ "Release", "Debug", "RelWithDebInfo" ], + "buildModes": [], + "requiredOptions": [] + }, + "profiles": [ + { + "backend": "vulkan", + "platform": "windows", + "buildModes": [], + "runConfiguration": "Release", + "gpuArchitectures": [] + } + ], + "dependencies": [], + "data": [ + { + "dependencies": [], + "command": [""], + "outputs": [] + } + ] +} \ No newline at end of file diff --git a/old_to_refactor/05_NablaTutorialExample/main.cpp b/old_to_refactor/05_NablaTutorialExample/main.cpp new file mode 100644 index 000000000..abebb882c --- /dev/null +++ b/old_to_refactor/05_NablaTutorialExample/main.cpp @@ -0,0 +1,593 @@ +// 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 + +#define _NBL_STATIC_LIB_ +#include +#include +#include + +#include "CCamera.hpp" +#include "../common/CommonAPI.h" + +/* + General namespaces. Entire engine consists of those bellow. +*/ + +using namespace nbl; +using namespace asset; +using namespace video; +using namespace core; + +/* + Uncomment for more detailed logging +*/ + +// #define NBL_MORE_LOGS + +class NablaTutorialExampleApp : public ApplicationBase +{ + /* + SIrrlichtCreationParameters holds some specific initialization information + about driver being used, size of window, stencil buffer or depth buffer. + Used to create a device. + */ + + constexpr static uint32_t WIN_W = 1280; + constexpr static uint32_t WIN_H = 720; + constexpr static uint32_t SC_IMG_COUNT = 3u; + constexpr static uint32_t FRAMES_IN_FLIGHT = 5u; + constexpr static uint64_t MAX_TIMEOUT = 99999999999999ull; + constexpr static size_t NBL_FRAMES_TO_AVERAGE = 100ull; + + static_assert(FRAMES_IN_FLIGHT > SC_IMG_COUNT); + +public: + /* + Most important objects to manage literally whole stuff are bellow. + By their usage you can create for example GPU objects, load or write + assets or manage objects on a scene. + */ + + nbl::core::smart_refctd_ptr windowManager; + nbl::core::smart_refctd_ptr window; + nbl::core::smart_refctd_ptr windowCb; + nbl::core::smart_refctd_ptr apiConnection; + nbl::core::smart_refctd_ptr surface; + nbl::core::smart_refctd_ptr utilities; + nbl::core::smart_refctd_ptr logicalDevice; + nbl::video::IPhysicalDevice* physicalDevice; + std::array queues = { nullptr, nullptr, nullptr, nullptr }; + nbl::core::smart_refctd_ptr swapchain; + nbl::core::smart_refctd_ptr renderpass; + nbl::core::smart_refctd_dynamic_array> fbo; + std::array, CommonAPI::InitOutput::MaxFramesInFlight>, CommonAPI::InitOutput::MaxQueuesCount> commandPools; // TODO: Multibuffer and reset the commandpools + nbl::core::smart_refctd_ptr system; + nbl::core::smart_refctd_ptr assetManager; + nbl::video::IGPUObjectFromAssetConverter::SParams cpu2gpuParams; + nbl::core::smart_refctd_ptr logger; + nbl::core::smart_refctd_ptr inputSystem; + + nbl::core::smart_refctd_ptr gpuTransferFence; + nbl::core::smart_refctd_ptr gpuComputeFence; + nbl::video::IGPUObjectFromAssetConverter cpu2gpu; + + core::smart_refctd_ptr gpuMeshBuffer; + core::smart_refctd_ptr gpuRenderpassIndependentPipeline; + core::smart_refctd_ptr gpuubo; + core::smart_refctd_ptr gpuDescriptorSet1; + core::smart_refctd_ptr gpuDescriptorSet3; + core::smart_refctd_ptr gpuGraphicsPipeline; + + core::smart_refctd_ptr frameComplete[FRAMES_IN_FLIGHT] = { nullptr }; + core::smart_refctd_ptr imageAcquire[FRAMES_IN_FLIGHT] = { nullptr }; + core::smart_refctd_ptr renderFinished[FRAMES_IN_FLIGHT] = { nullptr }; + core::smart_refctd_ptr commandBuffers[FRAMES_IN_FLIGHT]; + + nbl::video::ISwapchain::SCreationParams m_swapchainCreationParams; + + CommonAPI::InputSystem::ChannelReader mouse; + CommonAPI::InputSystem::ChannelReader keyboard; + Camera camera = Camera(vectorSIMDf(0, 0, 0), vectorSIMDf(0, 0, 0), matrix4SIMD()); + + uint32_t ds1UboBinding = 0; + int resourceIx; + uint32_t acquiredNextFBO = {}; + std::chrono::system_clock::time_point lastTime; + bool frameDataFilled = false; + size_t frame_count = 0ull; + double time_sum = 0; + double dtList[NBL_FRAMES_TO_AVERAGE] = {}; + + void setWindow(core::smart_refctd_ptr&& wnd) override + { + window = std::move(wnd); + } + void setSystem(core::smart_refctd_ptr&& s) override + { + system = std::move(s); + } + nbl::ui::IWindow* getWindow() override + { + return window.get(); + } + video::IAPIConnection* getAPIConnection() override + { + return apiConnection.get(); + } + video::ILogicalDevice* getLogicalDevice() override + { + return logicalDevice.get(); + } + video::IGPURenderpass* getRenderpass() override + { + return renderpass.get(); + } + void setSurface(core::smart_refctd_ptr&& s) override + { + surface = std::move(s); + } + void setFBOs(std::vector>& f) override + { + for (int i = 0; i < f.size(); i++) + { + fbo->begin()[i] = core::smart_refctd_ptr(f[i]); + } + } + void setSwapchain(core::smart_refctd_ptr&& s) override + { + swapchain = std::move(s); + } + uint32_t getSwapchainImageCount() override + { + return swapchain->getImageCount(); + } + virtual nbl::asset::E_FORMAT getDepthFormat() override + { + return nbl::asset::EF_D32_SFLOAT; + } + + APP_CONSTRUCTOR(NablaTutorialExampleApp) + + void onAppInitialized_impl() override + { + const auto swapchainImageUsage = static_cast(asset::IImage::EUF_COLOR_ATTACHMENT_BIT); + CommonAPI::InitParams initParams; + initParams.window = core::smart_refctd_ptr(window); + initParams.apiType = video::EAT_VULKAN; + initParams.appName = { _NBL_APP_NAME_ }; + initParams.framesInFlight = FRAMES_IN_FLIGHT; + initParams.windowWidth = WIN_W; + initParams.windowHeight = WIN_H; + initParams.swapchainImageCount = SC_IMG_COUNT; + initParams.swapchainImageUsage = swapchainImageUsage; + initParams.depthFormat = nbl::asset::EF_D32_SFLOAT; + auto initOutput = CommonAPI::InitWithDefaultExt(std::move(initParams)); + + window = std::move(initParams.window); + windowCb = std::move(initParams.windowCb); + apiConnection = std::move(initOutput.apiConnection); + surface = std::move(initOutput.surface); + utilities = std::move(initOutput.utilities); + logicalDevice = std::move(initOutput.logicalDevice); + physicalDevice = initOutput.physicalDevice; + queues = std::move(initOutput.queues); + renderpass = std::move(initOutput.renderToSwapchainRenderpass); + commandPools = std::move(initOutput.commandPools); + system = std::move(initOutput.system); + assetManager = std::move(initOutput.assetManager); + cpu2gpuParams = std::move(initOutput.cpu2gpuParams); + logger = std::move(initOutput.logger); + inputSystem = std::move(initOutput.inputSystem); + m_swapchainCreationParams = std::move(initOutput.swapchainCreationParams); + + CommonAPI::createSwapchain(std::move(logicalDevice), m_swapchainCreationParams, WIN_W, WIN_H, swapchain); + assert(swapchain); + fbo = CommonAPI::createFBOWithSwapchainImages( + swapchain->getImageCount(), WIN_W, WIN_H, + logicalDevice, swapchain, renderpass, + nbl::asset::EF_D32_SFLOAT + ); + + gpuTransferFence = logicalDevice->createFence(static_cast(0)); + gpuComputeFence = logicalDevice->createFence(static_cast(0)); + + /* + Helpfull class for managing basic geometry objects. + Thanks to it you can get half filled pipeline for your + geometries such as cubes, cones or spheres. + */ + + auto geometryCreator = assetManager->getGeometryCreator(); + auto rectangleGeometry = geometryCreator->createRectangleMesh(nbl::core::vector2df_SIMD(1.5, 3)); + + /* + Loading an asset bundle. You can specify some flags + and parameters to have an impact on extraordinary + tasks while loading for example. + */ + + asset::IAssetLoader::SAssetLoadParams loadingParams; + auto images_bundle = assetManager->getAsset("../../media/color_space_test/R8G8B8A8_1.png", loadingParams); + assert(!images_bundle.getContents().empty()); + auto image = images_bundle.getContents().begin()[0]; + + /* + By default an image that comes out of an image loader will only have the TRANSFER_DST usage flag. + We need to add more usages, as only we know what we'll do with the image farther along in the pipeline. + */ + auto image_raw = static_cast(image.get()); + image_raw->addImageUsageFlags(asset::IImage::EUF_SAMPLED_BIT); + + /* + Specifing gpu image view parameters to create a gpu + image view through the driver. + */ + + cpu2gpuParams.beginCommandBuffers(); + auto gpuImage = cpu2gpu.getGPUObjectsFromAssets(&image_raw, &image_raw + 1, cpu2gpuParams)->front(); + cpu2gpuParams.waitForCreationToComplete(); + auto& gpuParams = gpuImage->getCreationParameters(); + + IImageView::SCreationParams gpuImageViewParams = {}; + // Compute mipmap creation in Asset Converter tends to create some extra raw UINT views with STORAGE of the original image, + // so we need to declare that we won't be using STORAGE on this view or we wouldn't be able to use the SRGB format for it + gpuImageViewParams.subUsages = IGPUImage::EUF_SAMPLED_BIT; + gpuImageViewParams.image = gpuImage; + gpuImageViewParams.viewType = IGPUImageView::ET_2D; + gpuImageViewParams.format = gpuParams.format; + auto gpuImageView = logicalDevice->createImageView(std::move(gpuImageViewParams)); + + /* + Specifying cache key to default exsisting cached asset bundle + and specifying it's size where end is determined by + static_cast(0u) + */ + + const IAsset::E_TYPE types[]{ IAsset::E_TYPE::ET_SPECIALIZED_SHADER, IAsset::E_TYPE::ET_SPECIALIZED_SHADER, static_cast(0u) }; + + auto cpuVertexShader = core::smart_refctd_ptr_static_cast(assetManager->findAssets("nbl/builtin/material/lambertian/singletexture/specialized_shader.vert", types)->front().getContents().begin()[0]); + auto cpuFragmentShader = core::smart_refctd_ptr_static_cast(assetManager->findAssets("nbl/builtin/material/lambertian/singletexture/specialized_shader.frag", types)->front().getContents().begin()[0]); + + cpu2gpuParams.beginCommandBuffers(); + auto gpuVertexShader = cpu2gpu.getGPUObjectsFromAssets(&cpuVertexShader.get(), &cpuVertexShader.get() + 1, cpu2gpuParams)->front(); + auto gpuFragmentShader = cpu2gpu.getGPUObjectsFromAssets(&cpuFragmentShader.get(), &cpuFragmentShader.get() + 1, cpu2gpuParams)->front(); + cpu2gpuParams.waitForCreationToComplete(); + std::array gpuShaders = { gpuVertexShader.get(), gpuFragmentShader.get() }; + + size_t ds0SamplerBinding = 0, ds1UboBinding = 0; + { + /* + SBinding for the texture (sampler). + */ + + IGPUDescriptorSetLayout::SBinding gpuSamplerBinding; + gpuSamplerBinding.binding = ds0SamplerBinding; + gpuSamplerBinding.type = asset::IDescriptor::E_TYPE::ET_COMBINED_IMAGE_SAMPLER; + gpuSamplerBinding.count = 1u; + gpuSamplerBinding.stageFlags = static_cast(IGPUShader::ESS_FRAGMENT); + gpuSamplerBinding.samplers = nullptr; + + /* + SBinding for UBO - basic view parameters. + */ + + IGPUDescriptorSetLayout::SBinding gpuUboBinding; + gpuUboBinding.count = 1u; + gpuUboBinding.binding = ds1UboBinding; + gpuUboBinding.stageFlags = static_cast(asset::ICPUShader::ESS_VERTEX | asset::ICPUShader::ESS_FRAGMENT); + gpuUboBinding.type = asset::IDescriptor::E_TYPE::ET_UNIFORM_BUFFER; + + /* + Creating specific descriptor set layouts from specialized bindings. + Those layouts needs to attached to pipeline layout if required by user. + IrrlichtBaW provides 4 places for descriptor set layout usage. + */ + + auto gpuDs1Layout = logicalDevice->createDescriptorSetLayout(&gpuUboBinding, &gpuUboBinding + 1); + auto gpuDs3Layout = logicalDevice->createDescriptorSetLayout(&gpuSamplerBinding, &gpuSamplerBinding + 1); + + /* + Creating gpu UBO with appropiate size. + + We know ahead of time that `SBasicViewParameters` struct is the expected structure of the only UBO block in the descriptor set nr. 1 of the shader. + */ + { + IGPUBuffer::SCreationParams creationParams = {}; + creationParams.usage = core::bitflag(asset::IBuffer::EUF_UNIFORM_BUFFER_BIT)|asset::IBuffer::EUF_TRANSFER_DST_BIT|asset::IBuffer::EUF_INLINE_UPDATE_VIA_CMDBUF; + creationParams.size = sizeof(SBasicViewParameters); + gpuubo = logicalDevice->createBuffer(std::move(creationParams)); + + IDeviceMemoryBacked::SDeviceMemoryRequirements memReq = gpuubo->getMemoryReqs(); + memReq.memoryTypeBits &= physicalDevice->getDeviceLocalMemoryTypeBits(); + logicalDevice->allocate(memReq, gpuubo.get()); + } + + /* + Creating descriptor sets - texture (sampler) and basic view parameters (UBO). + Specifying info and write parameters for updating certain descriptor set to the driver. + + We know ahead of time that `SBasicViewParameters` struct is the expected structure of the only UBO block in the descriptor set nr. 1 of the shader. + */ + + nbl::core::smart_refctd_ptr descriptorPool = nullptr; + { + constexpr uint32_t DescriptorSetCount = 2u; + + video::IDescriptorPool::SCreateInfo createInfo = {}; + createInfo.maxSets = DescriptorSetCount; + createInfo.maxDescriptorCount[static_cast(asset::IDescriptor::E_TYPE::ET_UNIFORM_BUFFER)] = 1; // DS1 uses one UBO descriptor. + createInfo.maxDescriptorCount[static_cast(asset::IDescriptor::E_TYPE::ET_COMBINED_IMAGE_SAMPLER)] = 1; // DS3 uses one combined image sampler descriptor. + + descriptorPool = logicalDevice->createDescriptorPool(std::move(createInfo)); + } + + gpuDescriptorSet3 = descriptorPool->createDescriptorSet(gpuDs3Layout); + { + video::IGPUDescriptorSet::SWriteDescriptorSet write; + write.dstSet = gpuDescriptorSet3.get(); + write.binding = ds0SamplerBinding; + write.count = 1u; + write.arrayElement = 0u; + write.descriptorType = asset::IDescriptor::E_TYPE::ET_COMBINED_IMAGE_SAMPLER; + IGPUDescriptorSet::SDescriptorInfo info; + { + info.desc = std::move(gpuImageView); + ISampler::SParams samplerParams = { ISampler::ETC_CLAMP_TO_EDGE,ISampler::ETC_CLAMP_TO_EDGE,ISampler::ETC_CLAMP_TO_EDGE,ISampler::ETBC_FLOAT_OPAQUE_BLACK,ISampler::ETF_LINEAR,ISampler::ETF_LINEAR,ISampler::ESMM_LINEAR,0u,false,ECO_ALWAYS }; + info.info.image = { logicalDevice->createSampler(samplerParams),IGPUImage::EL_SHADER_READ_ONLY_OPTIMAL }; + } + write.info = &info; + logicalDevice->updateDescriptorSets(1u, &write, 0u, nullptr); + } + + gpuDescriptorSet1 = descriptorPool->createDescriptorSet(gpuDs1Layout); + { + video::IGPUDescriptorSet::SWriteDescriptorSet write; + write.dstSet = gpuDescriptorSet1.get(); + write.binding = ds1UboBinding; + write.count = 1u; + write.arrayElement = 0u; + write.descriptorType = asset::IDescriptor::E_TYPE::ET_UNIFORM_BUFFER; + video::IGPUDescriptorSet::SDescriptorInfo info; + { + info.desc = gpuubo; + info.info.buffer.offset = 0ull; + info.info.buffer.size = sizeof(SBasicViewParameters); + } + write.info = &info; + logicalDevice->updateDescriptorSets(1u, &write, 0u, nullptr); + } + + auto gpuPipelineLayout = logicalDevice->createPipelineLayout(nullptr, nullptr, nullptr, std::move(gpuDs1Layout), nullptr, std::move(gpuDs3Layout)); + + /* + Preparing required pipeline parameters and filling choosen one. + Note that some of them are returned from geometry creator according + to what I mentioned in returning half pipeline parameters. + */ + + asset::SBlendParams blendParams; + asset::SRasterizationParams rasterParams; + rasterParams.faceCullingMode = asset::EFCM_NONE; + + /* + Creating gpu pipeline with it's pipeline layout and specilized parameters. + Attaching vertex shader and fragment shaders. + */ + + gpuRenderpassIndependentPipeline = logicalDevice->createRenderpassIndependentPipeline(nullptr, std::move(gpuPipelineLayout), gpuShaders.data(), gpuShaders.data() + gpuShaders.size(), rectangleGeometry.inputParams, blendParams, rectangleGeometry.assemblyParams, rasterParams); + + nbl::video::IGPUGraphicsPipeline::SCreationParams graphicsPipelineParams; + graphicsPipelineParams.renderpassIndependent = core::smart_refctd_ptr(gpuRenderpassIndependentPipeline.get()); + graphicsPipelineParams.renderpass = core::smart_refctd_ptr(renderpass); + gpuGraphicsPipeline = logicalDevice->createGraphicsPipeline(nullptr, std::move(graphicsPipelineParams)); + + core::vectorSIMDf cameraPosition(-5, 0, 0); + matrix4SIMD projectionMatrix = matrix4SIMD::buildProjectionMatrixPerspectiveFovLH(core::radians(60.0f), float(WIN_W) / WIN_H, 0.01, 1000); + camera = Camera(cameraPosition, core::vectorSIMDf(0, 0, 0), projectionMatrix, 10.f, 1.f); + + /* + Creating gpu meshbuffer from parameters fetched from geometry creator return value. + */ + + constexpr auto MAX_ATTR_BUF_BINDING_COUNT = video::IGPUMeshBuffer::MAX_ATTR_BUF_BINDING_COUNT; + constexpr auto MAX_DATA_BUFFERS = MAX_ATTR_BUF_BINDING_COUNT + 1; + core::vector cpubuffers; + cpubuffers.reserve(MAX_DATA_BUFFERS); + for (auto i = 0; i < MAX_ATTR_BUF_BINDING_COUNT; i++) + { + auto buf = rectangleGeometry.bindings[i].buffer.get(); + if (buf) + cpubuffers.push_back(buf); + } + auto cpuindexbuffer = rectangleGeometry.indexBuffer.buffer.get(); + if (cpuindexbuffer) + cpubuffers.push_back(cpuindexbuffer); + + cpu2gpuParams.beginCommandBuffers(); + auto gpubuffers = cpu2gpu.getGPUObjectsFromAssets(cpubuffers.data(), cpubuffers.data() + cpubuffers.size(), cpu2gpuParams); + cpu2gpuParams.waitForCreationToComplete(); + + asset::SBufferBinding bindings[MAX_DATA_BUFFERS]; + for (auto i = 0, j = 0; i < MAX_ATTR_BUF_BINDING_COUNT; i++) + { + if (!rectangleGeometry.bindings[i].buffer) + continue; + auto buffPair = gpubuffers->operator[](j++); + bindings[i].offset = buffPair->getOffset(); + bindings[i].buffer = core::smart_refctd_ptr(buffPair->getBuffer()); + } + if (cpuindexbuffer) + { + auto buffPair = gpubuffers->back(); + bindings[MAX_ATTR_BUF_BINDING_COUNT].offset = buffPair->getOffset(); + bindings[MAX_ATTR_BUF_BINDING_COUNT].buffer = core::smart_refctd_ptr(buffPair->getBuffer()); + } + + gpuMeshBuffer = core::make_smart_refctd_ptr(core::smart_refctd_ptr(gpuRenderpassIndependentPipeline), nullptr, bindings, std::move(bindings[MAX_ATTR_BUF_BINDING_COUNT])); + { + gpuMeshBuffer->setIndexType(rectangleGeometry.indexType); + gpuMeshBuffer->setIndexCount(rectangleGeometry.indexCount); + gpuMeshBuffer->setBoundingBox(rectangleGeometry.bbox); + } + } + + const auto& graphicsCommandPools = commandPools[CommonAPI::InitOutput::EQT_GRAPHICS]; + for (uint32_t i = 0u; i < FRAMES_IN_FLIGHT; i++) + { + logicalDevice->createCommandBuffers(graphicsCommandPools[i].get(), video::IGPUCommandBuffer::EL_PRIMARY, 1, commandBuffers+i); + imageAcquire[i] = logicalDevice->createSemaphore(); + renderFinished[i] = logicalDevice->createSemaphore(); + } + } + + /* + Hot loop for rendering a scene. + */ + + void workLoopBody() override + { + ++resourceIx; + if (resourceIx >= FRAMES_IN_FLIGHT) + resourceIx = 0; + + auto& commandBuffer = commandBuffers[resourceIx]; + auto& fence = frameComplete[resourceIx]; + + if (fence) + { + logicalDevice->blockForFences(1u,&fence.get()); + logicalDevice->resetFences(1u,&fence.get()); + } + else + fence = logicalDevice->createFence(static_cast(0)); + + auto renderStart = std::chrono::system_clock::now(); + const auto renderDt = std::chrono::duration_cast(renderStart - lastTime).count(); + lastTime = renderStart; + { // Calculate Simple Moving Average for FrameTime + time_sum -= dtList[frame_count]; + time_sum += renderDt; + dtList[frame_count] = renderDt; + frame_count++; + if (frame_count >= NBL_FRAMES_TO_AVERAGE) + { + frameDataFilled = true; + frame_count = 0; + } + + } + const double averageFrameTime = frameDataFilled ? (time_sum / (double)NBL_FRAMES_TO_AVERAGE) : (time_sum / frame_count); + +#ifdef NBL_MORE_LOGS + logger->log("renderDt = %f ------ averageFrameTime = %f", system::ILogger::ELL_INFO, renderDt, averageFrameTime); +#endif // NBL_MORE_LOGS + + auto averageFrameTimeDuration = std::chrono::duration(averageFrameTime); + auto nextPresentationTime = renderStart + averageFrameTimeDuration; + auto nextPresentationTimeStamp = std::chrono::duration_cast(nextPresentationTime.time_since_epoch()); + + inputSystem->getDefaultMouse(&mouse); + inputSystem->getDefaultKeyboard(&keyboard); + + camera.beginInputProcessing(nextPresentationTimeStamp); + mouse.consumeEvents([&](const ui::IMouseEventChannel::range_t& events) -> void { camera.mouseProcess(events); }, logger.get()); + keyboard.consumeEvents([&](const ui::IKeyboardEventChannel::range_t& events) -> void { camera.keyboardProcess(events); }, logger.get()); + camera.endInputProcessing(nextPresentationTimeStamp); + + const auto& viewMatrix = camera.getViewMatrix(); + const auto& viewProjectionMatrix = camera.getConcatenatedMatrix(); + + commandBuffer->reset(nbl::video::IGPUCommandBuffer::ERF_RELEASE_RESOURCES_BIT); + commandBuffer->begin(IGPUCommandBuffer::EU_NONE); + + asset::SViewport viewport; + viewport.minDepth = 1.f; + viewport.maxDepth = 0.f; + viewport.x = 0u; + viewport.y = 0u; + viewport.width = WIN_W; + viewport.height = WIN_H; + commandBuffer->setViewport(0u, 1u, &viewport); + VkRect2D scissor; + scissor.offset = {0u,0u}; + scissor.extent = {WIN_W,WIN_H}; + commandBuffer->setScissor(0u,1u,&scissor); + + const auto viewProjection = camera.getConcatenatedMatrix(); + core::matrix3x4SIMD modelMatrix; + modelMatrix.setRotation(nbl::core::quaternion(0, 1, 0)); + + auto mv = core::concatenateBFollowedByA(camera.getViewMatrix(), modelMatrix); + auto mvp = core::concatenateBFollowedByA(viewProjection, modelMatrix); + core::matrix3x4SIMD normalMat; + mv.getSub3x3InverseTranspose(normalMat); + + /* + Updating UBO for basic view parameters and sending + updated data to staging buffer that will redirect + the data to graphics card - to vertex shader. + */ + + SBasicViewParameters uboData; + memcpy(uboData.MV, mv.pointer(), sizeof(mv)); + memcpy(uboData.MVP, mvp.pointer(), sizeof(mvp)); + memcpy(uboData.NormalMat, normalMat.pointer(), sizeof(normalMat)); + commandBuffer->updateBuffer(gpuubo.get(), 0ull, gpuubo->getSize(), &uboData); + + swapchain->acquireNextImage(MAX_TIMEOUT, imageAcquire[resourceIx].get(), nullptr, &acquiredNextFBO); + + nbl::video::IGPUCommandBuffer::SRenderpassBeginInfo beginInfo; + { + VkRect2D area; + area.offset = { 0,0 }; + area.extent = { WIN_W, WIN_H }; + asset::SClearValue clear[2] = {}; + clear[0].color.float32[0] = 0.f; + clear[0].color.float32[1] = 0.f; + clear[0].color.float32[2] = 0.f; + clear[0].color.float32[3] = 1.f; + clear[1].depthStencil.depth = 0.f; + + beginInfo.clearValueCount = 2u; + beginInfo.framebuffer = fbo->begin()[acquiredNextFBO]; + beginInfo.renderpass = renderpass; + beginInfo.renderArea = area; + beginInfo.clearValues = clear; + } + commandBuffer->beginRenderPass(&beginInfo, nbl::asset::ESC_INLINE); + + /* + Binding the most important objects needed to + render anything on the screen with textures: + + - gpu pipeline + - gpu descriptor sets + */ + + commandBuffer->bindGraphicsPipeline(gpuGraphicsPipeline.get()); + commandBuffer->bindDescriptorSets(asset::EPBP_GRAPHICS, gpuRenderpassIndependentPipeline->getLayout(), 1u, 1u, &gpuDescriptorSet1.get(), 0u); + commandBuffer->bindDescriptorSets(asset::EPBP_GRAPHICS, gpuRenderpassIndependentPipeline->getLayout(), 3u, 1u, &gpuDescriptorSet3.get(), 0u); + + /* + Drawing a mesh (created rectangle) with it's gpu mesh buffer usage. + */ + + commandBuffer->drawMeshBuffer(gpuMeshBuffer.get()); + + commandBuffer->endRenderPass(); + commandBuffer->end(); + + CommonAPI::Submit(logicalDevice.get(), commandBuffer.get(), queues[CommonAPI::InitOutput::EQT_GRAPHICS], imageAcquire[resourceIx].get(), renderFinished[resourceIx].get(), fence.get()); + CommonAPI::Present(logicalDevice.get(), swapchain.get(), queues[CommonAPI::InitOutput::EQT_GRAPHICS], renderFinished[resourceIx].get(), acquiredNextFBO); + } + + bool keepRunning() override + { + return windowCb->isWindowOpen(); + } + + void onAppTerminated_impl() override { logicalDevice->waitIdle(); } +}; + +NBL_COMMON_API_MAIN(NablaTutorialExampleApp) diff --git a/old_to_refactor/05_NablaTutorialExample/pipeline.groovy b/old_to_refactor/05_NablaTutorialExample/pipeline.groovy new file mode 100644 index 000000000..31cadf9e9 --- /dev/null +++ b/old_to_refactor/05_NablaTutorialExample/pipeline.groovy @@ -0,0 +1,50 @@ +import org.DevshGraphicsProgramming.Agent +import org.DevshGraphicsProgramming.BuilderInfo +import org.DevshGraphicsProgramming.IBuilder + +class CNablaTutorialExampleBuilder extends IBuilder +{ + public CNablaTutorialExampleBuilder(Agent _agent, _info) + { + super(_agent, _info) + } + + @Override + public boolean prepare(Map axisMapping) + { + return true + } + + @Override + public boolean build(Map axisMapping) + { + IBuilder.CONFIGURATION config = axisMapping.get("CONFIGURATION") + IBuilder.BUILD_TYPE buildType = axisMapping.get("BUILD_TYPE") + + def nameOfBuildDirectory = getNameOfBuildDirectory(buildType) + def nameOfConfig = getNameOfConfig(config) + + agent.execute("cmake --build ${info.rootProjectPath}/${nameOfBuildDirectory}/${info.targetProjectPathRelativeToRoot} --target ${info.targetBaseName} --config ${nameOfConfig} -j12 -v") + + return true + } + + @Override + public boolean test(Map axisMapping) + { + return true + } + + @Override + public boolean install(Map axisMapping) + { + return true + } +} + +def create(Agent _agent, _info) +{ + return new CNablaTutorialExampleBuilder(_agent, _info) +} + +return this \ No newline at end of file diff --git a/old_to_refactor/07_SubpassBaking/config.json.template b/old_to_refactor/07_SubpassBaking/config.json.template new file mode 100644 index 000000000..f961745c1 --- /dev/null +++ b/old_to_refactor/07_SubpassBaking/config.json.template @@ -0,0 +1,28 @@ +{ + "enableParallelBuild": true, + "threadsPerBuildProcess" : 2, + "isExecuted": false, + "scriptPath": "", + "cmake": { + "configurations": [ "Release", "Debug", "RelWithDebInfo" ], + "buildModes": [], + "requiredOptions": [] + }, + "profiles": [ + { + "backend": "vulkan", + "platform": "windows", + "buildModes": [], + "runConfiguration": "Release", + "gpuArchitectures": [] + } + ], + "dependencies": [], + "data": [ + { + "dependencies": [], + "command": [""], + "outputs": [] + } + ] +} \ No newline at end of file diff --git a/old_to_refactor/11_LoDSystem/config.json.template b/old_to_refactor/11_LoDSystem/config.json.template new file mode 100644 index 000000000..f961745c1 --- /dev/null +++ b/old_to_refactor/11_LoDSystem/config.json.template @@ -0,0 +1,28 @@ +{ + "enableParallelBuild": true, + "threadsPerBuildProcess" : 2, + "isExecuted": false, + "scriptPath": "", + "cmake": { + "configurations": [ "Release", "Debug", "RelWithDebInfo" ], + "buildModes": [], + "requiredOptions": [] + }, + "profiles": [ + { + "backend": "vulkan", + "platform": "windows", + "buildModes": [], + "runConfiguration": "Release", + "gpuArchitectures": [] + } + ], + "dependencies": [], + "data": [ + { + "dependencies": [], + "command": [""], + "outputs": [] + } + ] +} \ No newline at end of file diff --git a/old_to_refactor/12_glTF/config.json.template b/old_to_refactor/12_glTF/config.json.template new file mode 100644 index 000000000..f961745c1 --- /dev/null +++ b/old_to_refactor/12_glTF/config.json.template @@ -0,0 +1,28 @@ +{ + "enableParallelBuild": true, + "threadsPerBuildProcess" : 2, + "isExecuted": false, + "scriptPath": "", + "cmake": { + "configurations": [ "Release", "Debug", "RelWithDebInfo" ], + "buildModes": [], + "requiredOptions": [] + }, + "profiles": [ + { + "backend": "vulkan", + "platform": "windows", + "buildModes": [], + "runConfiguration": "Release", + "gpuArchitectures": [] + } + ], + "dependencies": [], + "data": [ + { + "dependencies": [], + "command": [""], + "outputs": [] + } + ] +} \ No newline at end of file diff --git a/old_to_refactor/14_ComputeScan/config.json.template b/old_to_refactor/14_ComputeScan/config.json.template new file mode 100644 index 000000000..a4ee411fa --- /dev/null +++ b/old_to_refactor/14_ComputeScan/config.json.template @@ -0,0 +1,28 @@ +{ + "enableParallelBuild": true, + "threadsPerBuildProcess" : 2, + "isExecuted": false, + "scriptPath": "", + "cmake": { + "configurations": [ "Release", "Debug", "RelWithDebInfo" ], + "buildModes": [], + "requiredOptions": [ "NBL_BUILD_CEGUI" ] + }, + "profiles": [ + { + "backend": "vulkan", + "platform": "windows", + "buildModes": [], + "runConfiguration": "Release", + "gpuArchitectures": [] + } + ], + "dependencies": [], + "data": [ + { + "dependencies": [], + "command": [""], + "outputs": [] + } + ] +} \ No newline at end of file diff --git a/old_to_refactor/16_OrderIndependentTransparency/config.json.template b/old_to_refactor/16_OrderIndependentTransparency/config.json.template new file mode 100644 index 000000000..f961745c1 --- /dev/null +++ b/old_to_refactor/16_OrderIndependentTransparency/config.json.template @@ -0,0 +1,28 @@ +{ + "enableParallelBuild": true, + "threadsPerBuildProcess" : 2, + "isExecuted": false, + "scriptPath": "", + "cmake": { + "configurations": [ "Release", "Debug", "RelWithDebInfo" ], + "buildModes": [], + "requiredOptions": [] + }, + "profiles": [ + { + "backend": "vulkan", + "platform": "windows", + "buildModes": [], + "runConfiguration": "Release", + "gpuArchitectures": [] + } + ], + "dependencies": [], + "data": [ + { + "dependencies": [], + "command": [""], + "outputs": [] + } + ] +} \ No newline at end of file diff --git a/old_to_refactor/17_SimpleBulletIntegration/config.json.template b/old_to_refactor/17_SimpleBulletIntegration/config.json.template new file mode 100644 index 000000000..6438e469a --- /dev/null +++ b/old_to_refactor/17_SimpleBulletIntegration/config.json.template @@ -0,0 +1,28 @@ +{ + "enableParallelBuild": true, + "threadsPerBuildProcess" : 2, + "isExecuted": false, + "scriptPath": "", + "cmake": { + "configurations": [ "Release", "Debug", "RelWithDebInfo" ], + "buildModes": [], + "requiredOptions": [ "NBL_BUILD_BULLET" ] + }, + "profiles": [ + { + "backend": "vulkan", + "platform": "windows", + "buildModes": [], + "runConfiguration": "Release", + "gpuArchitectures": [] + } + ], + "dependencies": [], + "data": [ + { + "dependencies": [], + "command": [""], + "outputs": [] + } + ] +} \ No newline at end of file diff --git a/old_to_refactor/18_MitsubaLoader/config.json.template b/old_to_refactor/18_MitsubaLoader/config.json.template new file mode 100644 index 000000000..2c42b001d --- /dev/null +++ b/old_to_refactor/18_MitsubaLoader/config.json.template @@ -0,0 +1,28 @@ +{ + "enableParallelBuild": true, + "threadsPerBuildProcess" : 2, + "isExecuted": false, + "scriptPath": "", + "cmake": { + "configurations": [ "Release", "Debug", "RelWithDebInfo" ], + "buildModes": [], + "requiredOptions": [ "NBL_BUILD_MITSUBA_LOADER" ] + }, + "profiles": [ + { + "backend": "vulkan", + "platform": "windows", + "buildModes": [], + "runConfiguration": "Release", + "gpuArchitectures": [] + } + ], + "dependencies": [], + "data": [ + { + "dependencies": [], + "command": [""], + "outputs": [] + } + ] +} \ No newline at end of file diff --git a/old_to_refactor/20_Megatexture/config.json.template b/old_to_refactor/20_Megatexture/config.json.template new file mode 100644 index 000000000..f961745c1 --- /dev/null +++ b/old_to_refactor/20_Megatexture/config.json.template @@ -0,0 +1,28 @@ +{ + "enableParallelBuild": true, + "threadsPerBuildProcess" : 2, + "isExecuted": false, + "scriptPath": "", + "cmake": { + "configurations": [ "Release", "Debug", "RelWithDebInfo" ], + "buildModes": [], + "requiredOptions": [] + }, + "profiles": [ + { + "backend": "vulkan", + "platform": "windows", + "buildModes": [], + "runConfiguration": "Release", + "gpuArchitectures": [] + } + ], + "dependencies": [], + "data": [ + { + "dependencies": [], + "command": [""], + "outputs": [] + } + ] +} \ No newline at end of file diff --git a/old_to_refactor/20_Megatexture/main.cpp b/old_to_refactor/20_Megatexture/main.cpp index 5c309ff24..35d0692af 100644 --- a/old_to_refactor/20_Megatexture/main.cpp +++ b/old_to_refactor/20_Megatexture/main.cpp @@ -684,7 +684,7 @@ APP_CONSTRUCTOR(MegaTextureApp) video::IGPUBuffer::SCreationParams bufferCreationParams; bufferCreationParams.usage = asset::IBuffer::EUF_STORAGE_BUFFER_BIT; bufferCreationParams.size = sizeof(video::IGPUVirtualTexture::SPrecomputedData); - core::smart_refctd_ptr utilities = video::IUtilities::create(core::smart_refctd_ptr(logicalDevice)); + core::smart_refctd_ptr utilities = core::make_smart_refctd_ptr(core::smart_refctd_ptr(logicalDevice)); core::smart_refctd_ptr buffer = utilities->createFilledDeviceLocalBufferOnDedMem(queues[CommonAPI::InitOutput::EQT_TRANSFER_UP], std::move(bufferCreationParams), &gpuvt->getPrecomputedData()); { diff --git a/old_to_refactor/21_DynamicTextureIndexing/CMakeLists.txt b/old_to_refactor/21_DynamicTextureIndexing/CMakeLists.txt new file mode 100644 index 000000000..a476b6203 --- /dev/null +++ b/old_to_refactor/21_DynamicTextureIndexing/CMakeLists.txt @@ -0,0 +1,7 @@ + +include(common RESULT_VARIABLE RES) +if(NOT RES) + message(FATAL_ERROR "common.cmake not found. Should be in {repo_root}/cmake directory") +endif() + +nbl_create_executable_project("" "" "" "" "${NBL_EXECUTABLE_PROJECT_CREATION_PCH_TARGET}") \ No newline at end of file diff --git a/old_to_refactor/21_DynamicTextureIndexing/config.json.template b/old_to_refactor/21_DynamicTextureIndexing/config.json.template new file mode 100644 index 000000000..f961745c1 --- /dev/null +++ b/old_to_refactor/21_DynamicTextureIndexing/config.json.template @@ -0,0 +1,28 @@ +{ + "enableParallelBuild": true, + "threadsPerBuildProcess" : 2, + "isExecuted": false, + "scriptPath": "", + "cmake": { + "configurations": [ "Release", "Debug", "RelWithDebInfo" ], + "buildModes": [], + "requiredOptions": [] + }, + "profiles": [ + { + "backend": "vulkan", + "platform": "windows", + "buildModes": [], + "runConfiguration": "Release", + "gpuArchitectures": [] + } + ], + "dependencies": [], + "data": [ + { + "dependencies": [], + "command": [""], + "outputs": [] + } + ] +} \ No newline at end of file diff --git a/old_to_refactor/21_DynamicTextureIndexing/main.cpp b/old_to_refactor/21_DynamicTextureIndexing/main.cpp new file mode 100644 index 000000000..596ab1f0e --- /dev/null +++ b/old_to_refactor/21_DynamicTextureIndexing/main.cpp @@ -0,0 +1,690 @@ +// 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 + +#define _NBL_STATIC_LIB_ +#include + +//! I advise to check out this file, its a basic input handler +#include "CCamera.hpp" +#include "../common/CommonAPI.h" + +#include "nbl/asset/utils/CCPUMeshPackerV1.h" + +#include "nbl/ext/ScreenShot/ScreenShot.h" + +using namespace nbl; +using namespace core; +using namespace asset; +using namespace video; + +class DynamicTextureIndexingApp : public ApplicationBase +{ + constexpr static uint32_t WIN_W = 1280; + constexpr static uint32_t WIN_H = 720; + constexpr static uint32_t SC_IMG_COUNT = 3u; + constexpr static uint32_t FRAMES_IN_FLIGHT = 5u; + constexpr static uint64_t MAX_TIMEOUT = 99999999999999ull; + constexpr static size_t NBL_FRAMES_TO_AVERAGE = 100ull; + + static_assert(FRAMES_IN_FLIGHT > SC_IMG_COUNT); + + +public: + struct CustomIndirectCommand : DrawElementsIndirectCommand_t + { + uint32_t diffuseTexBinding = std::numeric_limits::max(); + uint32_t bumpTexBinding = std::numeric_limits::max(); + }; + using MeshPacker = asset::CCPUMeshPackerV1; + + nbl::core::smart_refctd_ptr windowManager; + nbl::core::smart_refctd_ptr window; + nbl::core::smart_refctd_ptr windowCb; + nbl::core::smart_refctd_ptr gl; + nbl::core::smart_refctd_ptr surface; + nbl::core::smart_refctd_ptr utilities; + nbl::core::smart_refctd_ptr logicalDevice; + nbl::video::IPhysicalDevice* gpuPhysicalDevice; + std::array queues = { nullptr, nullptr, nullptr, nullptr }; + nbl::core::smart_refctd_ptr swapchain; + nbl::core::smart_refctd_ptr renderpass; + nbl::core::smart_refctd_dynamic_array> fbo; + std::array, CommonAPI::InitOutput::MaxFramesInFlight>, CommonAPI::InitOutput::MaxQueuesCount> commandPools; + nbl::core::smart_refctd_ptr system; + nbl::core::smart_refctd_ptr assetManager; + nbl::video::IGPUObjectFromAssetConverter::SParams cpu2gpuParams; + nbl::core::smart_refctd_ptr logger; + nbl::core::smart_refctd_ptr inputSystem; + + nbl::video::IGPUObjectFromAssetConverter cpu2gpu; + + asset::ICPUMesh* meshRaw = nullptr; + const asset::COBJMetadata* metaOBJ = nullptr; + + core::smart_refctd_ptr frameComplete[FRAMES_IN_FLIGHT] = { nullptr }; + core::smart_refctd_ptr imageAcquire[FRAMES_IN_FLIGHT] = { nullptr }; + core::smart_refctd_ptr renderFinished[FRAMES_IN_FLIGHT] = { nullptr }; + core::smart_refctd_ptr commandBuffers[FRAMES_IN_FLIGHT]; + + using GpuDescriptoSetPair = std::pair, core::smart_refctd_ptr>; + core::vector desc; + + CommonAPI::InputSystem::ChannelReader mouse; + CommonAPI::InputSystem::ChannelReader keyboard; + Camera camera = Camera(vectorSIMDf(0, 0, 0), vectorSIMDf(0, 0, 0), matrix4SIMD()); + + core::smart_refctd_ptr descriptorPool; + + core::smart_refctd_ptr gpuPipelineLayout; + core::smart_refctd_ptr gpuPipeline; + core::smart_refctd_ptr gpuGraphicsPipeline; + core::smart_refctd_ptr ds0layout; + core::smart_refctd_ptr ds1layout; + + struct DrawIndexedIndirectInput + { + asset::SBufferBinding vtxBindings[video::IGPUMeshBuffer::MAX_ATTR_BUF_BINDING_COUNT]; + const video::IGPUBuffer* vtxBindingsBuffers[video::IGPUMeshBuffer::MAX_ATTR_BUF_BINDING_COUNT]; + size_t vtxBindingsOffsets[video::IGPUMeshBuffer::MAX_ATTR_BUF_BINDING_COUNT]; + asset::E_PRIMITIVE_TOPOLOGY mode = asset::EPT_TRIANGLE_LIST; + asset::E_INDEX_TYPE indexType = asset::EIT_16BIT; + core::smart_refctd_ptr indexBuff = nullptr; + core::smart_refctd_ptr indirectDrawBuff = nullptr; + size_t offset = 0u; + size_t maxCount = 0u; + size_t stride = 0u; + core::smart_refctd_ptr countBuffer = nullptr; + size_t countOffset = 0u; + }; + + core::vector packedMeshBuffer; + core::vector mdiCallParams; + core::vector> gpuIndirectDrawBuffer; + nbl::video::ISwapchain::SCreationParams m_swapchainCreationParams; + + uint32_t ds1UboBinding = 0; + int resourceIx; + uint32_t acquiredNextFBO = {}; + std::chrono::system_clock::time_point lastTime; + bool frameDataFilled = false; + size_t frame_count = 0ull; + double time_sum = 0; + double dtList[NBL_FRAMES_TO_AVERAGE] = {}; + + auto createDescriptorPool(const uint32_t textureCount) + { + constexpr uint32_t maxItemCount = 256u; + { + nbl::video::IDescriptorPool::SDescriptorPoolSize poolSize; + poolSize.count = textureCount; + poolSize.type = nbl::asset::EDT_COMBINED_IMAGE_SAMPLER; + return logicalDevice->createDescriptorPool(static_cast(0), maxItemCount, 1u, &poolSize); + } + } + + void setWindow(core::smart_refctd_ptr&& wnd) override + { + window = std::move(wnd); + } + nbl::ui::IWindow* getWindow() override + { + return window.get(); + } + void setSystem(core::smart_refctd_ptr&& s) override + { + system = std::move(s); + } + video::IAPIConnection* getAPIConnection() override + { + return gl.get(); + } + video::ILogicalDevice* getLogicalDevice() override + { + return logicalDevice.get(); + } + video::IGPURenderpass* getRenderpass() override + { + return renderpass.get(); + } + void setSurface(core::smart_refctd_ptr&& s) override + { + surface = std::move(s); + } + void setFBOs(std::vector>& f) override + { + for (int i = 0; i < f.size(); i++) + { + fbo->begin()[i] = core::smart_refctd_ptr(f[i]); + } + } + void setSwapchain(core::smart_refctd_ptr&& s) override + { + swapchain = std::move(s); + } + uint32_t getSwapchainImageCount() override + { + return swapchain->getImageCount(); + } + virtual nbl::asset::E_FORMAT getDepthFormat() override + { + return nbl::asset::EF_D32_SFLOAT; + } + + APP_CONSTRUCTOR(DynamicTextureIndexingApp) + + void onAppInitialized_impl() override + { + const auto swapchainImageUsage = static_cast(asset::IImage::EUF_COLOR_ATTACHMENT_BIT | asset::IImage::EUF_STORAGE_BIT); + CommonAPI::InitParams initParams; + initParams.window = core::smart_refctd_ptr(window); + initParams.apiType = video::EAT_VULKAN; + initParams.appName = { _NBL_APP_NAME_ }; + initParams.framesInFlight = FRAMES_IN_FLIGHT; + initParams.windowWidth = WIN_W; + initParams.windowHeight = WIN_H; + initParams.swapchainImageCount = SC_IMG_COUNT; + initParams.swapchainImageUsage = swapchainImageUsage; + initParams.depthFormat = nbl::asset::EF_D32_SFLOAT; + auto initOutput = CommonAPI::InitWithDefaultExt(std::move(initParams)); + + window = std::move(initParams.window); + windowCb = std::move(initParams.windowCb); + gl = std::move(initOutput.apiConnection); + surface = std::move(initOutput.surface); + utilities = std::move(initOutput.utilities); + logicalDevice = std::move(initOutput.logicalDevice); + gpuPhysicalDevice = initOutput.physicalDevice; + queues = std::move(initOutput.queues); + renderpass = std::move(initOutput.renderToSwapchainRenderpass); + commandPools = std::move(initOutput.commandPools); + system = std::move(initOutput.system); + assetManager = std::move(initOutput.assetManager); + cpu2gpuParams = std::move(initOutput.cpu2gpuParams); + logger = std::move(initOutput.logger); + inputSystem = std::move(initOutput.inputSystem); + m_swapchainCreationParams = std::move(initOutput.swapchainCreationParams); + + CommonAPI::createSwapchain(std::move(logicalDevice), m_swapchainCreationParams, WIN_W, WIN_H, swapchain); + assert(swapchain); + fbo = CommonAPI::createFBOWithSwapchainImages( + swapchain->getImageCount(), WIN_W, WIN_H, + logicalDevice, swapchain, renderpass, + nbl::asset::EF_D32_SFLOAT + ); + + descriptorPool = createDescriptorPool(1u); + + { + auto* quantNormalCache = assetManager->getMeshManipulator()->getQuantNormalCache(); + quantNormalCache->loadCacheFromFile(system.get(), "../../tmp/normalCache101010.sse"); + + system::path archPath = sharedInputCWD / "sponza.zip"; + auto arch = system->openFileArchive(archPath); + // test no alias loading (TODO: fix loading from absolute paths) + system->mount(std::move(arch)); + asset::IAssetLoader::SAssetLoadParams loadParams; + loadParams.workingDirectory = sharedInputCWD; + loadParams.logger = logger.get(); + auto meshes_bundle = assetManager->getAsset((sharedInputCWD / "sponza.zip/sponza.obj").string(), loadParams); + assert(!meshes_bundle.getContents().empty()); + + metaOBJ = meshes_bundle.getMetadata()->selfCast(); + + auto cpuMesh = meshes_bundle.getContents().begin()[0]; + meshRaw = static_cast(cpuMesh.get()); + + quantNormalCache->saveCacheToFile(system.get(), "../../tmp/normalCache101010.sse"); + } + + const auto meshbuffers = meshRaw->getMeshBuffers(); + core::vector meshBuffers(meshbuffers.begin(), meshbuffers.end()); + + //divide mesh buffers into sets, where sum of textures used by meshes in one set is <= 16 + struct MBRangeTexturesPair + { + core::SRange::iterator, core::vector::iterator> mbRanges; + core::unordered_map textures; + }; + + core::vector mbRangesTex; + { + auto rangeBegin = meshBuffers.begin(); + core::unordered_map textures; + textures.reserve(16u); + uint32_t texBinding = 0u; + + for (auto it = meshBuffers.begin(); it < meshBuffers.end(); it++) + { + auto ds3 = (*it)->getAttachedDescriptorSet(); + ICPUImageView* tex = dynamic_cast(ds3->getDescriptors(0u).begin()->desc.get()); + ICPUImageView* texBump = dynamic_cast(ds3->getDescriptors(6u).begin()->desc.get()); + + auto a = textures.insert(std::make_pair(tex, texBinding)); + if (a.second == true) + texBinding++; + + auto b = textures.insert(std::make_pair(texBump, texBinding)); + if (b.second == true) + texBinding++; + + if (texBinding >= 15u) + { + mbRangesTex.push_back({ {rangeBegin, it + 1}, textures }); + rangeBegin = it + 1; + texBinding = 0; + textures.clear(); + } + + if (it + 1 == meshBuffers.end()) + { + mbRangesTex.push_back({ {rangeBegin, meshBuffers.end()}, textures }); + break; + } + } + } + + packedMeshBuffer = core::vector(mbRangesTex.size()); + mdiCallParams = core::vector(mbRangesTex.size()); + gpuIndirectDrawBuffer = core::vector>(mbRangesTex.size()); + for (size_t i = 0u; i < mbRangesTex.size(); i++) + { + asset::SVertexInputParams& inputParams = meshRaw->getMeshBuffers().begin()[0]->getPipeline()->getVertexInputParams(); + MeshPacker::AllocationParams allocationParams; + + auto& mbRange = mbRangesTex[i].mbRanges; + const ptrdiff_t meshBuffersInRangeCnt = std::distance(mbRange.begin(), mbRange.end()); + + allocationParams.indexBuffSupportedCnt = 20000000u; + allocationParams.indexBufferMinAllocCnt = 5000u; + allocationParams.vertexBuffSupportedByteSize = 2147483648u; + allocationParams.vertexBufferMinAllocByteSize = 500u; + allocationParams.MDIDataBuffSupportedCnt = 20000u; + allocationParams.vertexBufferMinAllocByteSize = 1u; + allocationParams.perInstanceVertexBuffSupportedByteSize = 0u; + allocationParams.perInstanceVertexBufferMinAllocByteSize = 0u; + + //pack mesh buffers + MeshPacker packer(inputParams, allocationParams, std::numeric_limits::max() / 3u, std::numeric_limits::max() / 3u); + + MeshPacker::ReservedAllocationMeshBuffers resData; + MeshPacker::PackedMeshBufferData pmbData; + + resData = packer.alloc(mbRange.begin(), mbRange.end()); + if (!resData.isValid()) + { + logger->log("Allocation failed.", system::ILogger::ELL_ERROR); + assert(false); + } + + packer.instantiateDataStorage(); + + pmbData = packer.commit(mbRange.begin(), mbRange.end(), resData, nullptr); + if (!pmbData.isValid()) + { + logger->log("Commit failed.", system::ILogger::ELL_ERROR); + assert(false); + } + assert(pmbData.isValid()); + + packedMeshBuffer[i] = packer.getPackerDataStore(); + assert(packedMeshBuffer[i].isValid()); + + //fill ssbo with correct values + auto& textures = mbRangesTex[i].textures; + CustomIndirectCommand* ssboBuffer = static_cast(packedMeshBuffer[i].MDIDataBuffer->getPointer()); + const size_t mdiBuffSz = packedMeshBuffer[i].MDIDataBuffer->getSize(); + + uint32_t mbIdx = 0u; + for (auto it = mbRange.begin(); it != mbRange.end(); it++) + { + auto ds3 = (*it)->getAttachedDescriptorSet(); + ICPUImageView* tex = dynamic_cast(ds3->getDescriptors(0u).begin()->desc.get()); + ICPUImageView* texBump = dynamic_cast(ds3->getDescriptors(6u).begin()->desc.get()); + + auto texBindDiffuseIt = textures.find(tex); + auto texBindBumpIt = textures.find(texBump); + assert(texBindDiffuseIt != textures.end()); + assert(texBindBumpIt != textures.end()); + + const uint32_t texBindDiffuse = (*texBindDiffuseIt).second; + const uint32_t texBindBump = (*texBindBumpIt).second; + + auto mdiCmdForMb = ssboBuffer + mbIdx++; + mdiCmdForMb->diffuseTexBinding = texBindDiffuse; + mdiCmdForMb->bumpTexBinding = texBindBump; + } + + assert(pmbData.mdiParameterCount == meshBuffersInRangeCnt); + + //create draw call inputs + video::IGPUBuffer::SCreationParams indexbufferCreationParams; + indexbufferCreationParams.size = packedMeshBuffer[i].indexBuffer.buffer->getSize(); + indexbufferCreationParams.usage = asset::IBuffer::E_USAGE_FLAGS(video::IGPUBuffer::EUF_INDEX_BUFFER_BIT | video::IGPUBuffer::EUF_TRANSFER_DST_BIT); + mdiCallParams[i].indexBuff = utilities->createFilledDeviceLocalBufferOnDedMem(queues[CommonAPI::InitOutput::EQT_TRANSFER_UP], std::move(indexbufferCreationParams), packedMeshBuffer[i].indexBuffer.buffer->getPointer()); + + auto& cpuVtxBuff = packedMeshBuffer[i].vertexBufferBindings[0].buffer; + + video::IGPUBuffer::SCreationParams indirectbufferCreationParams; + indirectbufferCreationParams.size = sizeof(CustomIndirectCommand) * pmbData.mdiParameterCount; + indirectbufferCreationParams.usage = asset::IBuffer::E_USAGE_FLAGS(video::IGPUBuffer::EUF_INDIRECT_BUFFER_BIT | video::IGPUBuffer::EUF_STORAGE_BUFFER_BIT | video::IGPUBuffer::EUF_TRANSFER_DST_BIT); + gpuIndirectDrawBuffer[i] = utilities->createFilledDeviceLocalBufferOnDedMem(queues[CommonAPI::InitOutput::EQT_TRANSFER_UP], std::move(indirectbufferCreationParams), packedMeshBuffer[i].MDIDataBuffer->getPointer()); + mdiCallParams[i].indirectDrawBuff = core::smart_refctd_ptr(gpuIndirectDrawBuffer[i]); + + video::IGPUBuffer::SCreationParams vertexbufferCreationParams; + vertexbufferCreationParams.size = cpuVtxBuff->getSize(); + vertexbufferCreationParams.usage = asset::IBuffer::E_USAGE_FLAGS(video::IGPUBuffer::EUF_VERTEX_BUFFER_BIT | video::IGPUBuffer::EUF_TRANSFER_DST_BIT); + auto gpuVtxBuff = utilities->createFilledDeviceLocalBufferOnDedMem(queues[CommonAPI::InitOutput::EQT_TRANSFER_UP], std::move(vertexbufferCreationParams), cpuVtxBuff->getPointer()); + + for (uint32_t j = 0u; j < video::IGPUMeshBuffer::MAX_ATTR_BUF_BINDING_COUNT; j++) + { + mdiCallParams[i].vtxBindings[j] = { packedMeshBuffer[i].vertexBufferBindings[j].offset, std::move(gpuVtxBuff) }; + mdiCallParams[i].vtxBindingsBuffers[j] = mdiCallParams[i].vtxBindings[j].buffer.get(); + mdiCallParams[i].vtxBindingsOffsets[j] = mdiCallParams[i].vtxBindings[j].offset; + } + + + mdiCallParams[i].stride = sizeof(CustomIndirectCommand); + mdiCallParams[i].maxCount = pmbData.mdiParameterCount; + } + + // + cpu2gpuParams.beginCommandBuffers(); + + //create pipeline + IAssetLoader::SAssetLoadParams lp; + auto vertexShaderBundle = assetManager->getAsset("../mesh.vert", lp); + auto fragShaderBundle = assetManager->getAsset("../mesh.frag", lp); + ICPUSpecializedShader* shaders[2] = + { + IAsset::castDown(vertexShaderBundle.getContents().begin()->get()), + IAsset::castDown(fragShaderBundle.getContents().begin()->get()) + }; + + ISampler::SParams sp; + sp.TextureWrapU = ISampler::E_TEXTURE_CLAMP::ETC_REPEAT; + sp.TextureWrapV = ISampler::E_TEXTURE_CLAMP::ETC_REPEAT; + sp.MinFilter = ISampler::E_TEXTURE_FILTER::ETF_LINEAR; + sp.MaxFilter = ISampler::E_TEXTURE_FILTER::ETF_LINEAR; + sp.BorderColor = 0; + auto sampler = logicalDevice->createSampler(sp); + { + asset::SPushConstantRange range[1] = { asset::IShader::ESS_VERTEX, 0u, sizeof(core::matrix4SIMD) }; + + core::smart_refctd_ptr samplerArray[16u] = { nullptr }; + for (uint32_t i = 0u; i < 16u; i++) + samplerArray[i] = core::smart_refctd_ptr(sampler); + + //set layout + { + video::IGPUDescriptorSetLayout::SBinding b[1]; + b[0].binding = 1u; + b[0].count = 16u; + b[0].type = EDT_COMBINED_IMAGE_SAMPLER; + b[0].stageFlags = IShader::ESS_FRAGMENT; + b[0].samplers = samplerArray; + + ds0layout = logicalDevice->createDescriptorSetLayout(b, b + 1); + } + { + video::IGPUDescriptorSetLayout::SBinding b[1]; + b[0].binding = 0u; + b[0].count = 1u; + b[0].type = EDT_STORAGE_BUFFER; + b[0].stageFlags = IShader::ESS_FRAGMENT; + + ds1layout = logicalDevice->createDescriptorSetLayout(b, b + 1); + } + + auto gpuShaders = cpu2gpu.getGPUObjectsFromAssets(shaders, shaders + 2, cpu2gpuParams); + IGPUSpecializedShader* shaders[2] = { gpuShaders->operator[](0).get(), gpuShaders->operator[](1).get() }; + + gpuPipelineLayout = logicalDevice->createPipelineLayout(range, range + 1, core::smart_refctd_ptr(ds0layout), core::smart_refctd_ptr(ds1layout)); + asset::SRasterizationParams rp; + + gpuPipeline = logicalDevice->createRenderpassIndependentPipeline(nullptr, core::smart_refctd_ptr(gpuPipelineLayout), shaders, shaders + 2u, packedMeshBuffer[0].vertexInputParams, asset::SBlendParams(), asset::SPrimitiveAssemblyParams(), rp); + + nbl::video::IGPUGraphicsPipeline::SCreationParams graphicsPipelineParams; + graphicsPipelineParams.renderpassIndependent = core::smart_refctd_ptr(const_cast(gpuPipeline.get())); + graphicsPipelineParams.renderpass = core::smart_refctd_ptr(renderpass); + + gpuGraphicsPipeline = logicalDevice->createGraphicsPipeline(nullptr, std::move(graphicsPipelineParams)); + } + + desc = core::vector(mbRangesTex.size()); + for (uint32_t i = 0u; i < mbRangesTex.size(); i++) + { + auto& texDesc = desc[i].first; + auto& ssboDesc = desc[i].second; + + auto& range = mbRangesTex[i].mbRanges; + auto& textures = mbRangesTex[i].textures; + const uint32_t texCnt = textures.size(); + + texDesc = logicalDevice->createDescriptorSet(descriptorPool.get(), core::smart_refctd_ptr(ds0layout)); + ssboDesc = logicalDevice->createDescriptorSet(descriptorPool.get(), core::smart_refctd_ptr(ds1layout)); + video::IGPUDescriptorSet::SDescriptorInfo info[16u]; + video::IGPUDescriptorSet::SWriteDescriptorSet w[16u]; + + for (auto& texBind : textures) + { + auto texture = texBind.first; + const uint32_t bind = texBind.second; + + auto gpuTexture = cpu2gpu.getGPUObjectsFromAssets(&texture, &texture + 1, cpu2gpuParams)->front(); + for (auto i = 0; i < video::IGPUObjectFromAssetConverter::EQU_COUNT; i++) + { + if (cpu2gpuParams.perQueue[i].cmdbuf->getState() != video::IGPUCommandBuffer::ES_RECORDING) + { + cpu2gpuParams.perQueue[i].cmdbuf->begin(video::IGPUCommandBuffer::EU_ONE_TIME_SUBMIT_BIT); + } + } + + info[bind].image.imageLayout = asset::IImage::EL_SHADER_READ_ONLY_OPTIMAL; + info[bind].image.sampler = core::smart_refctd_ptr(sampler); + info[bind].desc = core::smart_refctd_ptr(gpuTexture); + + w[bind].binding = 1u; + w[bind].arrayElement = bind; + w[bind].count = 1u; + w[bind].descriptorType = EDT_COMBINED_IMAGE_SAMPLER; + w[bind].dstSet = texDesc.get(); + w[bind].info = &info[bind]; + } + logicalDevice->updateDescriptorSets(texCnt, w, 0u, nullptr); + + { + video::IGPUDescriptorSet::SDescriptorInfo _info; + _info.buffer.offset = 0u; + _info.buffer.size = gpuIndirectDrawBuffer[i]->getSize(); + _info.desc = core::smart_refctd_ptr(gpuIndirectDrawBuffer[i]); + + video::IGPUDescriptorSet::SWriteDescriptorSet _w; + _w.binding = 0u; + _w.arrayElement = 0u; + _w.count = 1u; + _w.descriptorType = EDT_STORAGE_BUFFER; + _w.dstSet = ssboDesc.get(); + _w.info = &_info; + + logicalDevice->updateDescriptorSets(1u, &_w, 0u, nullptr); + } + } + + cpu2gpuParams.waitForCreationToComplete(); + + core::vectorSIMDf cameraPosition(-4, 0, 0); + matrix4SIMD projectionMatrix = matrix4SIMD::buildProjectionMatrixPerspectiveFovLH(core::radians(60.0f), video::ISurface::getTransformedAspectRatio(swapchain->getPreTransform(), WIN_W, WIN_H), 0.1, 100000); + camera = Camera(cameraPosition, core::vectorSIMDf(0, 0, 0), projectionMatrix, 1.f, 1.f); + + uint64_t lastFPSTime = 0; + + for (size_t i = 0ull; i < NBL_FRAMES_TO_AVERAGE; ++i) + dtList[i] = 0.0; + + + const auto& graphicsCommandPools = commandPools[CommonAPI::InitOutput::EQT_GRAPHICS]; + for (uint32_t i = 0u; i < FRAMES_IN_FLIGHT; i++) + { + logicalDevice->createCommandBuffers(graphicsCommandPools[i].get(), video::IGPUCommandBuffer::EL_PRIMARY, 1, commandBuffers+i); + imageAcquire[i] = logicalDevice->createSemaphore(); + renderFinished[i] = logicalDevice->createSemaphore(); + } + + constexpr uint64_t MAX_TIMEOUT = 99999999999999ull; + uint32_t acquiredNextFBO = {}; + resourceIx = -1; + } + + void workLoopBody() override + { + ++resourceIx; + if (resourceIx >= FRAMES_IN_FLIGHT) + resourceIx = 0; + + auto& commandBuffer = commandBuffers[resourceIx]; + auto& fence = frameComplete[resourceIx]; + + if (fence) + { + logicalDevice->blockForFences(1u, &fence.get()); + logicalDevice->resetFences(1u, &fence.get()); + } + else + fence = logicalDevice->createFence(static_cast(0)); + + auto renderStart = std::chrono::system_clock::now(); + const auto renderDt = std::chrono::duration_cast(renderStart - lastTime).count(); + lastTime = renderStart; + { // Calculate Simple Moving Average for FrameTime + time_sum -= dtList[frame_count]; + time_sum += renderDt; + dtList[frame_count] = renderDt; + frame_count++; + if (frame_count >= NBL_FRAMES_TO_AVERAGE) + { + frameDataFilled = true; + frame_count = 0; + } + + } + const double averageFrameTime = frameDataFilled ? (time_sum / (double)NBL_FRAMES_TO_AVERAGE) : (time_sum / frame_count); + +#ifdef NBL_MORE_LOGS + logger->log("renderDt = %f ------ averageFrameTime = %f", system::ILogger::ELL_INFO, renderDt, averageFrameTime); +#endif // NBL_MORE_LOGS + + auto averageFrameTimeDuration = std::chrono::duration(averageFrameTime); + auto nextPresentationTime = renderStart + averageFrameTimeDuration; + auto nextPresentationTimeStamp = std::chrono::duration_cast(nextPresentationTime.time_since_epoch()); + + inputSystem->getDefaultMouse(&mouse); + inputSystem->getDefaultKeyboard(&keyboard); + + camera.beginInputProcessing(nextPresentationTimeStamp); + mouse.consumeEvents([&](const ui::IMouseEventChannel::range_t& events) -> void { camera.mouseProcess(events); }, logger.get()); + keyboard.consumeEvents([&](const ui::IKeyboardEventChannel::range_t& events) -> void { camera.keyboardProcess(events); }, logger.get()); + camera.endInputProcessing(nextPresentationTimeStamp); + + const auto& viewMatrix = camera.getViewMatrix(); + const auto& viewProjectionMatrix = matrix4SIMD::concatenateBFollowedByAPrecisely( + video::ISurface::getSurfaceTransformationMatrix(swapchain->getPreTransform()), + camera.getConcatenatedMatrix() + ); + + commandBuffer->reset(nbl::video::IGPUCommandBuffer::ERF_RELEASE_RESOURCES_BIT); + commandBuffer->begin(IGPUCommandBuffer::EU_NONE); + + asset::SViewport viewport; + viewport.minDepth = 1.f; + viewport.maxDepth = 0.f; + viewport.x = 0u; + viewport.y = 0u; + viewport.width = WIN_W; + viewport.height = WIN_H; + commandBuffer->setViewport(0u, 1u, &viewport); + + VkRect2D scissor; + scissor.offset = { 0, 0 }; + scissor.extent = { WIN_W,WIN_H }; + commandBuffer->setScissor(0u, 1u, &scissor); + + swapchain->acquireNextImage(MAX_TIMEOUT, imageAcquire[resourceIx].get(), nullptr, &acquiredNextFBO); + + nbl::video::IGPUCommandBuffer::SRenderpassBeginInfo beginInfo; + { + VkRect2D area; + area.offset = { 0,0 }; + area.extent = { WIN_W, WIN_H }; + asset::SClearValue clear[2] = {}; + clear[0].color.float32[0] = 1.f; + clear[0].color.float32[1] = 1.f; + clear[0].color.float32[2] = 1.f; + clear[0].color.float32[3] = 1.f; + clear[1].depthStencil.depth = 0.f; + + beginInfo.clearValueCount = 2u; + beginInfo.framebuffer = fbo->begin()[acquiredNextFBO]; + beginInfo.renderpass = renderpass; + beginInfo.renderArea = area; + beginInfo.clearValues = clear; + } + + commandBuffer->beginRenderPass(&beginInfo, nbl::asset::ESC_INLINE); + + core::matrix3x4SIMD modelMatrix; + modelMatrix.setTranslation(nbl::core::vectorSIMDf(0, 0, 0, 0)); + + commandBuffer->bindGraphicsPipeline(gpuGraphicsPipeline.get()); + + for (uint32_t i = 0u; i < mdiCallParams.size(); i++) + { + commandBuffer->bindDescriptorSets(asset::EPBP_GRAPHICS, gpuPipeline->getLayout(), 0u, 2u, &desc[i].first.get(), 0u); + commandBuffer->bindIndexBuffer(mdiCallParams[i].indexBuff.get(), 0ull, mdiCallParams[i].indexType); + commandBuffer->bindVertexBuffers(0u, 1u, mdiCallParams[i].vtxBindingsBuffers, &mdiCallParams[i].vtxBindingsOffsets[0]); + commandBuffer->bindVertexBuffers(2u, 1u, mdiCallParams[i].vtxBindingsBuffers, &mdiCallParams[i].vtxBindingsOffsets[2]); + commandBuffer->bindVertexBuffers(3u, 1u, mdiCallParams[i].vtxBindingsBuffers, &mdiCallParams[i].vtxBindingsOffsets[3]); + commandBuffer->pushConstants(gpuPipeline->getLayout(), video::IGPUShader::ESS_VERTEX, 0u, sizeof(core::matrix4SIMD), viewProjectionMatrix.pointer()); + + commandBuffer->drawIndexedIndirect(mdiCallParams[i].indirectDrawBuff.get(), mdiCallParams[i].offset, mdiCallParams[i].maxCount, mdiCallParams[i].stride); + } + + commandBuffer->endRenderPass(); + commandBuffer->end(); + + CommonAPI::Submit(logicalDevice.get(), + commandBuffer.get(), + queues[CommonAPI::InitOutput::EQT_GRAPHICS], + imageAcquire[resourceIx].get(), + renderFinished[resourceIx].get(), + fence.get()); + CommonAPI::Present(logicalDevice.get(), + swapchain.get(), + queues[CommonAPI::InitOutput::EQT_GRAPHICS], renderFinished[resourceIx].get(), acquiredNextFBO); + } + + void onAppTerminated_impl() override + { + const auto& fboCreationParams = fbo->begin()[acquiredNextFBO]->getCreationParameters(); + auto gpuSourceImageView = fboCreationParams.attachments[0]; + + bool status = ext::ScreenShot::createScreenShot( + logicalDevice.get(), + queues[CommonAPI::InitOutput::EQT_TRANSFER_UP], + renderFinished[resourceIx].get(), + gpuSourceImageView.get(), + assetManager.get(), + "ScreenShot.png", + asset::IImage::EL_PRESENT_SRC, + asset::EAF_NONE); + + assert(status); + } + + bool keepRunning() override + { + return windowCb->isWindowOpen(); + } +}; + +NBL_COMMON_API_MAIN(DynamicTextureIndexingApp, DynamicTextureIndexingApp::Nabla) \ No newline at end of file diff --git a/old_to_refactor/21_DynamicTextureIndexing/mesh.frag b/old_to_refactor/21_DynamicTextureIndexing/mesh.frag new file mode 100644 index 000000000..39fa77d97 --- /dev/null +++ b/old_to_refactor/21_DynamicTextureIndexing/mesh.frag @@ -0,0 +1,29 @@ +// 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 + +#version 460 core + +struct SSBOContents +{ + uint irrevelant[5]; + uint diffBind; + uint bumpBind; +}; + +layout(set = 0, binding = 0, std430) restrict readonly buffer SSBO +{ + SSBOContents ssboContents[]; +}; + +layout(set = 0, binding = 1) uniform sampler2D tex[16]; + +layout(location = 0) in vec2 texCoord; +layout(location = 1) flat in uint drawID; + +layout(location = 0) out vec4 pixelColor; + +void main() +{ + pixelColor = texture(tex[ssboContents[drawID].diffBind], texCoord); +} diff --git a/old_to_refactor/21_DynamicTextureIndexing/mesh.vert b/old_to_refactor/21_DynamicTextureIndexing/mesh.vert new file mode 100644 index 000000000..eb0328e55 --- /dev/null +++ b/old_to_refactor/21_DynamicTextureIndexing/mesh.vert @@ -0,0 +1,25 @@ +// 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 + +#version 460 core + +layout(push_constant, row_major) uniform PushConstants +{ + mat4 vp; +} pc; + +layout(location = 0) in vec4 vPos; +layout(location = 2) in vec2 vTexCoord; +layout(location = 3) in vec3 vNormal; + +layout(location = 0) out vec2 texCoord; +layout(location = 1) flat out uint drawID; + +void main() +{ + gl_Position = pc.vp * vPos; + + drawID = gl_DrawID; + texCoord = vTexCoord; +} diff --git a/old_to_refactor/21_DynamicTextureIndexing/pipeline.groovy b/old_to_refactor/21_DynamicTextureIndexing/pipeline.groovy new file mode 100644 index 000000000..4d3c9a5d0 --- /dev/null +++ b/old_to_refactor/21_DynamicTextureIndexing/pipeline.groovy @@ -0,0 +1,50 @@ +import org.DevshGraphicsProgramming.Agent +import org.DevshGraphicsProgramming.BuilderInfo +import org.DevshGraphicsProgramming.IBuilder + +class CDynamicTextureIndexingBuilder extends IBuilder +{ + public CDynamicTextureIndexingBuilder(Agent _agent, _info) + { + super(_agent, _info) + } + + @Override + public boolean prepare(Map axisMapping) + { + return true + } + + @Override + public boolean build(Map axisMapping) + { + IBuilder.CONFIGURATION config = axisMapping.get("CONFIGURATION") + IBuilder.BUILD_TYPE buildType = axisMapping.get("BUILD_TYPE") + + def nameOfBuildDirectory = getNameOfBuildDirectory(buildType) + def nameOfConfig = getNameOfConfig(config) + + agent.execute("cmake --build ${info.rootProjectPath}/${nameOfBuildDirectory}/${info.targetProjectPathRelativeToRoot} --target ${info.targetBaseName} --config ${nameOfConfig} -j12 -v") + + return true + } + + @Override + public boolean test(Map axisMapping) + { + return true + } + + @Override + public boolean install(Map axisMapping) + { + return true + } +} + +def create(Agent _agent, _info) +{ + return new CDynamicTextureIndexingBuilder(_agent, _info) +} + +return this \ No newline at end of file diff --git a/old_to_refactor/49_ComputeFFT/CMakeLists.txt b/old_to_refactor/49_ComputeFFT/CMakeLists.txt new file mode 100644 index 000000000..b591db9e9 --- /dev/null +++ b/old_to_refactor/49_ComputeFFT/CMakeLists.txt @@ -0,0 +1,11 @@ + +include(common RESULT_VARIABLE RES) +if(NOT RES) + message(FATAL_ERROR "common.cmake not found. Should be in {repo_root}/cmake directory") +endif() + +set(EXAMPLE_SOURCES + ../../src/nbl/ext/FFT/FFT.cpp +) + +nbl_create_executable_project("${EXAMPLE_SOURCES}" "" "" "" "${NBL_EXECUTABLE_PROJECT_CREATION_PCH_TARGET}") \ No newline at end of file diff --git a/old_to_refactor/49_ComputeFFT/config.json.template b/old_to_refactor/49_ComputeFFT/config.json.template new file mode 100644 index 000000000..f961745c1 --- /dev/null +++ b/old_to_refactor/49_ComputeFFT/config.json.template @@ -0,0 +1,28 @@ +{ + "enableParallelBuild": true, + "threadsPerBuildProcess" : 2, + "isExecuted": false, + "scriptPath": "", + "cmake": { + "configurations": [ "Release", "Debug", "RelWithDebInfo" ], + "buildModes": [], + "requiredOptions": [] + }, + "profiles": [ + { + "backend": "vulkan", + "platform": "windows", + "buildModes": [], + "runConfiguration": "Release", + "gpuArchitectures": [] + } + ], + "dependencies": [], + "data": [ + { + "dependencies": [], + "command": [""], + "outputs": [] + } + ] +} \ No newline at end of file diff --git a/old_to_refactor/49_ComputeFFT/extra_parameters.glsl b/old_to_refactor/49_ComputeFFT/extra_parameters.glsl new file mode 100644 index 000000000..032f4c363 --- /dev/null +++ b/old_to_refactor/49_ComputeFFT/extra_parameters.glsl @@ -0,0 +1,16 @@ +// 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 "nbl/builtin/glsl/ext/FFT/parameters_struct.glsl" +struct convolve_parameters_t +{ + nbl_glsl_ext_FFT_Parameters_t fft; + vec2 kernel_half_pixel_size; +}; + +struct image_store_parameters_t +{ + nbl_glsl_ext_FFT_Parameters_t fft; + ivec2 unpad_offset; +}; \ No newline at end of file diff --git a/old_to_refactor/49_ComputeFFT/fft_convolve_ifft.comp b/old_to_refactor/49_ComputeFFT/fft_convolve_ifft.comp new file mode 100644 index 000000000..18702fe81 --- /dev/null +++ b/old_to_refactor/49_ComputeFFT/fft_convolve_ifft.comp @@ -0,0 +1,109 @@ +layout(local_size_x=_NBL_GLSL_WORKGROUP_SIZE_, local_size_y=1, local_size_z=1) in; + +layout(set=0, binding=2) uniform sampler2D NormalizedKernel[3]; + +/* TODO: Hardcode the parameters for the frequent FFTs +uvec3 nbl_glsl_ext_FFT_Parameters_t_getDimensions() +{ + return uvec3(1280u,1024u,1u); +} +bool nbl_glsl_ext_FFT_Parameters_t_getIsInverse() +{ + return false; +} +uint nbl_glsl_ext_FFT_Parameters_t_getDirection() +{ + return 0u; +} +uint nbl_glsl_ext_FFT_Parameters_t_getMaxChannel() +{ + return 2u; +} +uint nbl_glsl_ext_FFT_Parameters_t_getLog2FFTSize() +{ + return 11u; +} +uint nbl_glsl_ext_FFT_Parameters_t_getPaddingType() +{ + return 3u; // _NBL_GLSL_EXT_FFT_PAD_MIRROR_; +} +uvec4 nbl_glsl_ext_FFT_Parameters_t_getInputStrides() +{ + return uvec4(1024u,1u,0u,1024u*1280u); +} +uvec4 nbl_glsl_ext_FFT_Parameters_t_getOutputStrides() +{ + return uvec4(1u,1280u,0u,1280u*1024u); +} +#define _NBL_GLSL_EXT_FFT_PARAMETERS_METHODS_DECLARED_ +*/ + +#include "extra_parameters.glsl" +layout(push_constant) uniform PushConstants +{ + convolve_parameters_t params; +} pc; +#define _NBL_GLSL_EXT_FFT_PUSH_CONSTANTS_DEFINED_ + +nbl_glsl_ext_FFT_Parameters_t nbl_glsl_ext_FFT_getParameters() +{ + return pc.params.fft; +} +#define _NBL_GLSL_EXT_FFT_GET_PARAMETERS_DEFINED_ + +#define _NBL_GLSL_EXT_FFT_MAIN_DEFINED_ +#include "nbl/builtin/glsl/ext/FFT/default_compute_fft.comp" + +void convolve(in uint item_per_thread_count, in uint ch) +{ + // TODO: decouple kernel size from image size (can't get the math to work in my head) + for(uint t=0u; t>1u; + const uint shifted = tid-padding; + if (tid>=padding && shifted +nbl_glsl_complex nbl_glsl_ext_FFT_getPaddedData(in ivec3 coordinate, in uint channel) +{ + ivec2 inputImageSize = textureSize(inputImage, 0); + vec2 normalizedCoords = (vec2(coordinate.xy)+vec2(0.5f))/(vec2(inputImageSize)*KERNEL_SCALE); + vec4 texelValue = textureLod(inputImage, normalizedCoords+vec2(0.5-0.5/KERNEL_SCALE), -log2(KERNEL_SCALE)); + return nbl_glsl_complex(texelValue[channel], 0.0f); +} +#define _NBL_GLSL_EXT_FFT_GET_PADDED_DATA_DEFINED_ + + +/* TODO: Hardcode the parameters for the frequent FFTs +#if _NBL_GLSL_EXT_FFT_MAX_DIM_SIZE_>512 +uvec3 nbl_glsl_ext_FFT_Parameters_t_getDimensions() +{ + return uvec3(1280u,720u,1u); +} +bool nbl_glsl_ext_FFT_Parameters_t_getIsInverse() +{ + return false; +} +uint nbl_glsl_ext_FFT_Parameters_t_getDirection() +{ + return 1u; +} +uint nbl_glsl_ext_FFT_Parameters_t_getMaxChannel() +{ + return 2u; +} +uint nbl_glsl_ext_FFT_Parameters_t_getLog2FFTSize() +{ + return 10u; +} +uint nbl_glsl_ext_FFT_Parameters_t_getPaddingType() +{ + return 3u; // _NBL_GLSL_EXT_FFT_PAD_MIRROR_; +} +uvec4 nbl_glsl_ext_FFT_Parameters_t_getInputStrides() +{ + return uvec4(0xdeadbeefu); +} +uvec4 nbl_glsl_ext_FFT_Parameters_t_getOutputStrides() +{ + return uvec4(1024u,1u,0u,1024u*1280u); +} +#define _NBL_GLSL_EXT_FFT_PARAMETERS_METHODS_DECLARED_ +#endif +*/ + +#include "nbl/builtin/glsl/ext/FFT/default_compute_fft.comp" \ No newline at end of file diff --git a/old_to_refactor/49_ComputeFFT/last_fft.comp b/old_to_refactor/49_ComputeFFT/last_fft.comp new file mode 100644 index 000000000..2183ef63c --- /dev/null +++ b/old_to_refactor/49_ComputeFFT/last_fft.comp @@ -0,0 +1,72 @@ +layout(local_size_x=_NBL_GLSL_WORKGROUP_SIZE_, local_size_y=1, local_size_z=1) in; + +// Output Descriptor +layout(set=0, binding=1, rgba16f) uniform image2D outImage; +#define _NBL_GLSL_EXT_FFT_OUTPUT_DESCRIPTOR_DEFINED_ + +/* TODO: Hardcode the parameters for the frequent FFTs +uvec3 nbl_glsl_ext_FFT_Parameters_t_getDimensions() +{ + return uvec3(1280u,1024u,1u); +} +bool nbl_glsl_ext_FFT_Parameters_t_getIsInverse() +{ + return true; +} +uint nbl_glsl_ext_FFT_Parameters_t_getDirection() +{ + return 1u; +} +uint nbl_glsl_ext_FFT_Parameters_t_getMaxChannel() +{ + return 2u; +} +uint nbl_glsl_ext_FFT_Parameters_t_getLog2FFTSize() +{ + return 10u; +} +uint nbl_glsl_ext_FFT_Parameters_t_getPaddingType() +{ + return 3u; // _NBL_GLSL_EXT_FFT_PAD_MIRROR_; +} +uvec4 nbl_glsl_ext_FFT_Parameters_t_getInputStrides() +{ + return uvec4(1u,1280u,0u,1280u*1024u); +} +uvec4 nbl_glsl_ext_FFT_Parameters_t_getOutputStrides() +{ + return uvec4(0xdeadbeefu); +} +#define _NBL_GLSL_EXT_FFT_PARAMETERS_METHODS_DECLARED_ +*/ + +#include "extra_parameters.glsl" +layout(push_constant) uniform PushConstants +{ + image_store_parameters_t params; +} pc; +#define _NBL_GLSL_EXT_FFT_PUSH_CONSTANTS_DEFINED_ + +nbl_glsl_ext_FFT_Parameters_t nbl_glsl_ext_FFT_getParameters() +{ + return pc.params.fft; +} +#define _NBL_GLSL_EXT_FFT_GET_PARAMETERS_DEFINED_ + + +#include +void nbl_glsl_ext_FFT_setData(in uvec3 coordinate, in uint channel, in nbl_glsl_complex complex_value) +{ + const ivec2 coords = ivec2(coordinate.xy)-pc.params.unpad_offset; + + if (all(lessThanEqual(ivec2(0),coords)) && all(greaterThan(imageSize(outImage),coords))) + { + vec4 color_value = imageLoad(outImage, coords); + color_value[channel] = complex_value.x; + imageStore(outImage, coords, color_value); + } +} +#define _NBL_GLSL_EXT_FFT_SET_DATA_DEFINED_ + + +#include "nbl/builtin/glsl/ext/FFT/default_compute_fft.comp" \ No newline at end of file diff --git a/old_to_refactor/49_ComputeFFT/main.cpp b/old_to_refactor/49_ComputeFFT/main.cpp new file mode 100644 index 000000000..ba2b7e33e --- /dev/null +++ b/old_to_refactor/49_ComputeFFT/main.cpp @@ -0,0 +1,753 @@ +// 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 + +#define _NBL_STATIC_LIB_ +#include +#include +#include + +#include "nbl/ext/FFT/FFT.h" +#include "../common/QToQuitEventReceiver.h" + +using namespace nbl; +using namespace nbl::core; +using namespace nbl::asset; +using namespace nbl::video; + +using FFTClass = ext::FFT::FFT; + +constexpr uint32_t channelCountOverride = 3u; + +inline core::smart_refctd_ptr createShader( + video::IVideoDriver* driver, + const uint32_t maxFFTlen, + const bool useHalfStorage, + const char* includeMainName, + float kernelScale = 1.f) +{ + const char* sourceFmt = +R"===(#version 430 core + +#define _NBL_GLSL_WORKGROUP_SIZE_ %u +#define _NBL_GLSL_EXT_FFT_MAX_DIM_SIZE_ %u +#define _NBL_GLSL_EXT_FFT_HALF_STORAGE_ %u + +#define KERNEL_SCALE %f + +#include "%s" + +)==="; + + const size_t extraSize = 4u+8u+8u+128u; + + constexpr uint32_t DEFAULT_WORK_GROUP_SIZE = FFTClass::DEFAULT_WORK_GROUP_SIZE; + auto shader = core::make_smart_refctd_ptr(strlen(sourceFmt)+extraSize+1u); + snprintf( + reinterpret_cast(shader->getPointer()),shader->getSize(), sourceFmt, + DEFAULT_WORK_GROUP_SIZE, + maxFFTlen, + useHalfStorage ? 1u:0u, + kernelScale, + includeMainName + ); + + auto cpuSpecializedShader = core::make_smart_refctd_ptr( + core::make_smart_refctd_ptr(std::move(shader),ICPUShader::buffer_contains_glsl), + ISpecializedShader::SInfo{nullptr, nullptr, "main", asset::ISpecializedShader::ESS_COMPUTE} + ); + + auto gpuShader = driver->createShader(nbl::core::smart_refctd_ptr(cpuSpecializedShader->getUnspecialized())); + + auto gpuSpecializedShader = driver->createSpecializedShader(gpuShader.get(), cpuSpecializedShader->getSpecializationInfo()); + + return gpuSpecializedShader; +} + + + +inline void updateDescriptorSet_Convolution ( + video::IVideoDriver * driver, + video::IGPUDescriptorSet * set, + core::smart_refctd_ptr inputBufferDescriptor, + core::smart_refctd_ptr outputBufferDescriptor, + const core::smart_refctd_ptr* kernelNormalizedSpectrumImageDescriptors) +{ + constexpr uint32_t descCount = 3u; + video::IGPUDescriptorSet::SDescriptorInfo pInfos[descCount-1u+channelCountOverride]; + video::IGPUDescriptorSet::SWriteDescriptorSet pWrites[descCount]; + + for (auto i = 0; i < descCount; i++) + { + pWrites[i].binding = i; + pWrites[i].dstSet = set; + pWrites[i].arrayElement = 0u; + pWrites[i].info = pInfos+i; + } + + // Input Buffer + pWrites[0].descriptorType = asset::EDT_STORAGE_BUFFER; + pWrites[0].count = 1; + pInfos[0].desc = inputBufferDescriptor; + pInfos[0].buffer.size = inputBufferDescriptor->getSize(); + pInfos[0].buffer.offset = 0u; + + // + pWrites[1].descriptorType = asset::EDT_STORAGE_BUFFER; + pWrites[1].count = 1; + pInfos[1].desc = outputBufferDescriptor; + pInfos[1].buffer.size = outputBufferDescriptor->getSize(); + pInfos[1].buffer.offset = 0u; + + // Kernel Buffer + pWrites[2].descriptorType = asset::EDT_COMBINED_IMAGE_SAMPLER; + pWrites[2].count = channelCountOverride; + for (uint32_t i=0u; iupdateDescriptorSets(descCount, pWrites, 0u, nullptr); +} +inline void updateDescriptorSet_LastFFT ( + video::IVideoDriver * driver, + video::IGPUDescriptorSet * set, + core::smart_refctd_ptr inputBufferDescriptor, + core::smart_refctd_ptr outputImageDescriptor) +{ + video::IGPUDescriptorSet::SDescriptorInfo pInfos[2]; + video::IGPUDescriptorSet::SWriteDescriptorSet pWrites[2]; + + for (auto i = 0; i< 2; i++) + { + pWrites[i].dstSet = set; + pWrites[i].arrayElement = 0u; + pWrites[i].count = 1u; + pWrites[i].info = pInfos+i; + } + + // Input Buffer + pWrites[0].binding = 0; + pWrites[0].descriptorType = asset::EDT_STORAGE_BUFFER; + pWrites[0].count = 1; + pInfos[0].desc = inputBufferDescriptor; + pInfos[0].buffer.size = inputBufferDescriptor->getSize(); + pInfos[0].buffer.offset = 0u; + + // Output Buffer + pWrites[1].binding = 1; + pWrites[1].descriptorType = asset::EDT_STORAGE_IMAGE; + pWrites[1].count = 1; + pInfos[1].desc = outputImageDescriptor; + pInfos[1].image.sampler = nullptr; + pInfos[1].image.imageLayout = static_cast(0u);; + + driver->updateDescriptorSets(2u, pWrites, 0u, nullptr); +} + +using nbl_glsl_ext_FFT_Parameters_t = ext::FFT::FFT::Parameters_t; +struct vec2 +{ + float x,y; +}; +struct ivec2 +{ + int32_t x,y; +}; +#include "extra_parameters.glsl" + + +int main() +{ + nbl::SIrrlichtCreationParameters deviceParams; + deviceParams.Bits = 24; //may have to set to 32bit for some platforms + deviceParams.ZBufferBits = 24; //we'd like 32bit here + deviceParams.DriverType = EDT_OPENGL; //! Only Well functioning driver, software renderer left for sake of 2D image drawing + deviceParams.WindowSize = dimension2d(1280, 720); + deviceParams.Fullscreen = false; + deviceParams.Vsync = true; //! If supported by target platform + deviceParams.Doublebuffer = true; + deviceParams.Stencilbuffer = false; //! This will not even be a choice soon + + auto device = createDeviceEx(deviceParams); + if (!device) + return 1; // could not create selected driver. + + QToQuitEventReceiver receiver; + device->setEventReceiver(&receiver); + + IVideoDriver* driver = device->getVideoDriver(); + + nbl::io::IFileSystem* filesystem = device->getFileSystem(); + IAssetManager* am = device->getAssetManager(); + // Loading SrcImage and Kernel Image from File + + IAssetLoader::SAssetLoadParams lp; + auto srcImageBundle = am->getAsset("../../media/colorexr.exr", lp); + auto kerImageBundle = am->getAsset("../../media/kernels/physical_flare_256.exr", lp); + + // get GPU image views + smart_refctd_ptr srcImageView; + { + auto srcGpuImages = driver->getGPUObjectsFromAssets(srcImageBundle.getContents()); + + IGPUImageView::SCreationParams srcImgViewInfo; + srcImgViewInfo.flags = static_cast(0u); + srcImgViewInfo.image = srcGpuImages->operator[](0u); + srcImgViewInfo.viewType = IGPUImageView::ET_2D; + srcImgViewInfo.format = srcImgViewInfo.image->getCreationParameters().format; + srcImgViewInfo.subresourceRange.aspectMask = static_cast(0u); + srcImgViewInfo.subresourceRange.baseMipLevel = 0; + srcImgViewInfo.subresourceRange.levelCount = 1; + srcImgViewInfo.subresourceRange.baseArrayLayer = 0; + srcImgViewInfo.subresourceRange.layerCount = 1; + srcImageView = driver->createImageView(std::move(srcImgViewInfo)); + } + smart_refctd_ptr kerImageView; + { + auto kerGpuImages = driver->getGPUObjectsFromAssets(kerImageBundle.getContents()); + + IGPUImageView::SCreationParams kerImgViewInfo; + kerImgViewInfo.flags = static_cast(0u); + kerImgViewInfo.image = kerGpuImages->operator[](0u); + kerImgViewInfo.viewType = IGPUImageView::ET_2D; + kerImgViewInfo.format = kerImgViewInfo.image->getCreationParameters().format; + kerImgViewInfo.subresourceRange.aspectMask = static_cast(0u); + kerImgViewInfo.subresourceRange.baseMipLevel = 0; + kerImgViewInfo.subresourceRange.levelCount = kerImgViewInfo.image->getCreationParameters().mipLevels; + kerImgViewInfo.subresourceRange.baseArrayLayer = 0; + kerImgViewInfo.subresourceRange.layerCount = 1; + kerImageView = driver->createImageView(std::move(kerImgViewInfo)); + } + + // agree on formats + const E_FORMAT srcFormat = srcImageView->getCreationParameters().format; + uint32_t srcNumChannels = getFormatChannelCount(srcFormat); + uint32_t kerNumChannels = getFormatChannelCount(kerImageView->getCreationParameters().format); + //! OVERRIDE (we dont need alpha) + srcNumChannels = channelCountOverride; + kerNumChannels = channelCountOverride; + assert(srcNumChannels == kerNumChannels); // Just to make sure, because the other case is not handled in this example + + // Create Out Image + smart_refctd_ptr outImg; + smart_refctd_ptr outImgView; + { + auto dstImgViewInfo = srcImageView->getCreationParameters(); + + auto dstImgInfo = dstImgViewInfo.image->getCreationParameters(); + outImg = driver->createDeviceLocalGPUImageOnDedMem(std::move(dstImgInfo)); + + dstImgViewInfo.image = outImg; + outImgView = driver->createImageView(IGPUImageView::SCreationParams(dstImgViewInfo)); + } + + // input pipeline + auto imageFirstFFTPipelineLayout = [driver]() -> auto + { + IGPUDescriptorSetLayout::SBinding bnd[] = + { + { + 0u, + EDT_COMBINED_IMAGE_SAMPLER, + 1u, + ISpecializedShader::ESS_COMPUTE, + nullptr + }, + { + 1u, + EDT_STORAGE_BUFFER, + 1u, + ISpecializedShader::ESS_COMPUTE, + nullptr + } + }; + + core::SRange pcRange = FFTClass::getDefaultPushConstantRanges(); + core::SRange bindings = {bnd,bnd+sizeof(bnd)/sizeof(IGPUDescriptorSetLayout::SBinding)}; + + return driver->createPipelineLayout( + pcRange.begin(),pcRange.end(), + driver->createDescriptorSetLayout(bindings.begin(),bindings.end()),nullptr,nullptr,nullptr + ); + }(); + auto convolvePipelineLayout = [driver]() -> auto + { + IGPUSampler::SParams params = + { + { + ISampler::ETC_REPEAT, + ISampler::ETC_REPEAT, + ISampler::ETC_REPEAT, + ISampler::ETBC_FLOAT_OPAQUE_BLACK, + ISampler::ETF_LINEAR, // is it needed? + ISampler::ETF_LINEAR, + ISampler::ESMM_NEAREST, + 0u, + 0u, + ISampler::ECO_ALWAYS + } + }; + auto sampler = driver->createSampler(std::move(params)); + smart_refctd_ptr samplers[channelCountOverride]; + std::fill_n(samplers,channelCountOverride,sampler); + + IGPUDescriptorSetLayout::SBinding bnd[] = + { + { + 0u, + EDT_STORAGE_BUFFER, + 1u, + ISpecializedShader::ESS_COMPUTE, + nullptr + }, + { + 1u, + EDT_STORAGE_BUFFER, + 1u, + ISpecializedShader::ESS_COMPUTE, + nullptr + }, + { + 2u, + EDT_COMBINED_IMAGE_SAMPLER, + channelCountOverride, + ISpecializedShader::ESS_COMPUTE, + samplers + } + }; + + const asset::SPushConstantRange pcRange = {ISpecializedShader::ESS_COMPUTE,0u,sizeof(convolve_parameters_t)}; + core::SRange bindings = {bnd,bnd+sizeof(bnd)/sizeof(IGPUDescriptorSetLayout::SBinding)}; + + return driver->createPipelineLayout( + &pcRange,&pcRange+1, + driver->createDescriptorSetLayout(bindings.begin(),bindings.end()),nullptr,nullptr,nullptr + ); + }(); + auto lastFFTPipelineLayout = [driver]() -> auto + { + IGPUDescriptorSetLayout::SBinding bnd[] = + { + { + 0u, + EDT_STORAGE_BUFFER, + 1u, + ISpecializedShader::ESS_COMPUTE, + nullptr + }, + { + 1u, + EDT_STORAGE_IMAGE, + 1u, + ISpecializedShader::ESS_COMPUTE, + nullptr + }, + }; + + const asset::SPushConstantRange pcRange = {ISpecializedShader::ESS_COMPUTE,0u,sizeof(image_store_parameters_t)}; + core::SRange bindings = {bnd, bnd+sizeof(bnd)/sizeof(IGPUDescriptorSetLayout::SBinding)};; + + return driver->createPipelineLayout( + &pcRange,&pcRange+1, + driver->createDescriptorSetLayout(bindings.begin(),bindings.end()),nullptr,nullptr,nullptr + ); + }(); + + const float bloomRelativeScale = 0.25f; + const auto kerDim = kerImageView->getCreationParameters().image->getCreationParameters().extent; + const auto srcDim = srcImageView->getCreationParameters().image->getCreationParameters().extent; + const float bloomScale = core::min(float(srcDim.width)/float(kerDim.width),float(srcDim.height)/float(kerDim.height))*bloomRelativeScale; + if (bloomScale>1.f) + std::cout << "WARNING: Bloom Kernel will Clip and loose sharpness, increase resolution of bloom kernel!" << std::endl; + const auto marginSrcDim = [srcDim,kerDim,bloomScale]() -> auto + { + auto tmp = srcDim; + for (auto i=0u; i<3u; i++) + { + const auto coord = (&kerDim.width)[i]; + if (coord>1u) + (&tmp.width)[i] += core::max(coord*bloomScale,1u)-1u; + } + return tmp; + }(); + constexpr bool useHalfFloats = true; + // Allocate Output Buffer + auto fftOutputBuffer_0 = driver->createDeviceLocalGPUBufferOnDedMem(FFTClass::getOutputBufferSize(useHalfFloats,marginSrcDim,srcNumChannels)); + auto fftOutputBuffer_1 = driver->createDeviceLocalGPUBufferOnDedMem(FFTClass::getOutputBufferSize(useHalfFloats,marginSrcDim,srcNumChannels)); + core::smart_refctd_ptr kernelNormalizedSpectrums[channelCountOverride]; + + auto updateDescriptorSet = [driver](video::IGPUDescriptorSet* set, core::smart_refctd_ptr inputImageDescriptor, asset::ISampler::E_TEXTURE_CLAMP textureWrap, core::smart_refctd_ptr outputBufferDescriptor) -> void + { + IGPUSampler::SParams params = + { + { + textureWrap, + textureWrap, + textureWrap, + ISampler::ETBC_FLOAT_OPAQUE_BLACK, + ISampler::ETF_LINEAR, + ISampler::ETF_LINEAR, + ISampler::ESMM_LINEAR, + 8u, + 0u, + ISampler::ECO_ALWAYS + } + }; + auto sampler = driver->createSampler(std::move(params)); + + constexpr auto kDescriptorCount = 2u; + video::IGPUDescriptorSet::SDescriptorInfo pInfos[kDescriptorCount]; + video::IGPUDescriptorSet::SWriteDescriptorSet pWrites[kDescriptorCount]; + + for (auto i=0; i(0u); + + // Output Buffer + pWrites[1].binding = 1; + pWrites[1].descriptorType = asset::EDT_STORAGE_BUFFER; + pWrites[1].count = 1; + pInfos[1].desc = outputBufferDescriptor; + pInfos[1].buffer.size = outputBufferDescriptor->getSize(); + pInfos[1].buffer.offset = 0u; + + driver->updateDescriptorSets(2u, pWrites, 0u, nullptr); + }; + + // Precompute Kernel FFT + { + const VkExtent3D paddedKerDim = FFTClass::padDimensions(kerDim); + + // create kernel spectrums + auto createKernelSpectrum = [&]() -> auto + { + video::IGPUImage::SCreationParams imageParams; + imageParams.flags = static_cast(0u); + imageParams.type = asset::IImage::ET_2D; + imageParams.format = useHalfFloats ? EF_R16G16_SFLOAT:EF_R32G32_SFLOAT; + imageParams.extent = { paddedKerDim.width,paddedKerDim.height,1u}; + imageParams.mipLevels = 1u; + imageParams.arrayLayers = 1u; + imageParams.samples = asset::IImage::ESCF_1_BIT; + + video::IGPUImageView::SCreationParams viewParams; + viewParams.flags = static_cast(0u); + viewParams.image = driver->createGPUImageOnDedMem(std::move(imageParams),driver->getDeviceLocalGPUMemoryReqs()); + viewParams.viewType = video::IGPUImageView::ET_2D; + viewParams.format = useHalfFloats ? EF_R16G16_SFLOAT:EF_R32G32_SFLOAT; + viewParams.components = {}; + viewParams.subresourceRange = {}; + viewParams.subresourceRange.levelCount = 1u; + viewParams.subresourceRange.layerCount = 1u; + return driver->createImageView(std::move(viewParams)); + }; + for (uint32_t i=0u; i fftPipeline_SSBOInput(core::make_smart_refctd_ptr(driver,0x1u<getDefaultPipeline()); + + // descriptor sets + core::smart_refctd_ptr fftDescriptorSet_Ker_FFT[2] = + { + driver->createDescriptorSet(core::smart_refctd_ptr(imageFirstFFTPipelineLayout->getDescriptorSetLayout(0u))), + driver->createDescriptorSet(core::smart_refctd_ptr(fftPipeline_SSBOInput->getLayout()->getDescriptorSetLayout(0u))) + }; + updateDescriptorSet(fftDescriptorSet_Ker_FFT[0].get(), kerImageView, ISampler::ETC_CLAMP_TO_BORDER, fftOutputBuffer_0); + FFTClass::updateDescriptorSet(driver,fftDescriptorSet_Ker_FFT[1].get(), fftOutputBuffer_0, fftOutputBuffer_1); + + // Normalization of FFT spectrum + struct NormalizationPushConstants + { + ext::FFT::uvec4 stride; + ext::FFT::uvec4 bitreverse_shift; + }; + auto fftPipelineLayout_KernelNormalization = [&]() -> auto + { + IGPUDescriptorSetLayout::SBinding bnd[] = + { + { + 0u, + EDT_STORAGE_BUFFER, + 1u, + ISpecializedShader::ESS_COMPUTE, + nullptr + }, + { + 1u, + EDT_STORAGE_IMAGE, + channelCountOverride, + ISpecializedShader::ESS_COMPUTE, + nullptr + }, + }; + SPushConstantRange pc_rng; + pc_rng.offset = 0u; + pc_rng.size = sizeof(NormalizationPushConstants); + pc_rng.stageFlags = ISpecializedShader::ESS_COMPUTE; + return driver->createPipelineLayout( + &pc_rng,&pc_rng+1u, + driver->createDescriptorSetLayout(bnd,bnd+2),nullptr,nullptr,nullptr + ); + }(); + auto fftDescriptorSet_KernelNormalization = [&]() -> auto + { + auto dset = driver->createDescriptorSet(core::smart_refctd_ptr(fftPipelineLayout_KernelNormalization->getDescriptorSetLayout(0u))); + + video::IGPUDescriptorSet::SDescriptorInfo pInfos[1+channelCountOverride]; + video::IGPUDescriptorSet::SWriteDescriptorSet pWrites[2]; + + for (auto i = 0; i < 2; i++) + { + pWrites[i].dstSet = dset.get(); + pWrites[i].arrayElement = 0u; + pWrites[i].count = 1u; + pWrites[i].info = pInfos + i; + } + + // In Buffer + pWrites[0].binding = 0; + pWrites[0].descriptorType = asset::EDT_STORAGE_BUFFER; + pWrites[0].count = 1; + pInfos[0].desc = fftOutputBuffer_1; + pInfos[0].buffer.size = fftOutputBuffer_1->getSize(); + pInfos[0].buffer.offset = 0u; + + // Out Buffer + pWrites[1].binding = 1; + pWrites[1].descriptorType = asset::EDT_STORAGE_IMAGE; + pWrites[1].count = channelCountOverride; + for (uint32_t i=0u; iupdateDescriptorSets(2u, pWrites, 0u, nullptr); + return dset; + }(); + + // Ker Image First Axis FFT + { + auto fftPipeline_ImageInput = driver->createComputePipeline(nullptr,core::smart_refctd_ptr(imageFirstFFTPipelineLayout),createShader(driver,0x1u<bindComputePipeline(fftPipeline_ImageInput.get()); + driver->bindDescriptorSets(EPBP_COMPUTE, imageFirstFFTPipelineLayout.get(), 0u, 1u, &fftDescriptorSet_Ker_FFT[0].get(), nullptr); + FFTClass::dispatchHelper(driver, imageFirstFFTPipelineLayout.get(), fftPushConstants[0], fftDispatchInfo[0]); + } + + // Ker Image Last Axis FFT + driver->bindComputePipeline(fftPipeline_SSBOInput.get()); + driver->bindDescriptorSets(EPBP_COMPUTE, fftPipeline_SSBOInput->getLayout(), 0u, 1u, &fftDescriptorSet_Ker_FFT[1].get(), nullptr); + FFTClass::dispatchHelper(driver, fftPipeline_SSBOInput->getLayout(), fftPushConstants[1], fftDispatchInfo[1]); + + // Ker Normalization + auto fftPipeline_KernelNormalization = driver->createComputePipeline(nullptr,core::smart_refctd_ptr(fftPipelineLayout_KernelNormalization),createShader(driver,0xdeadbeefu,useHalfFloats,"../normalization.comp")); + driver->bindComputePipeline(fftPipeline_KernelNormalization.get()); + driver->bindDescriptorSets(EPBP_COMPUTE, fftPipelineLayout_KernelNormalization.get(), 0u, 1u, &fftDescriptorSet_KernelNormalization.get(), nullptr); + { + NormalizationPushConstants normalizationPC; + normalizationPC.stride = fftPushConstants[1].output_strides; + normalizationPC.bitreverse_shift.x = 32-core::findMSB(paddedKerDim.width); + normalizationPC.bitreverse_shift.y = 32-core::findMSB(paddedKerDim.height); + normalizationPC.bitreverse_shift.z = 0; + driver->pushConstants(fftPipelineLayout_KernelNormalization.get(),ICPUSpecializedShader::ESS_COMPUTE,0u,sizeof(normalizationPC),&normalizationPC); + } + { + const uint32_t dispatchSizeX = (paddedKerDim.width-1u)/16u+1u; + const uint32_t dispatchSizeY = (paddedKerDim.height-1u)/16u+1u; + driver->dispatch(dispatchSizeX,dispatchSizeY,kerNumChannels); + FFTClass::defaultBarrier(); + } + } + + FFTClass::Parameters_t fftPushConstants[3]; + FFTClass::DispatchInfo_t fftDispatchInfo[3]; + const ISampler::E_TEXTURE_CLAMP fftPadding[2] = {ISampler::ETC_MIRROR,ISampler::ETC_MIRROR}; + const auto passes = FFTClass::buildParameters(false,srcNumChannels,srcDim,fftPushConstants,fftDispatchInfo,fftPadding,marginSrcDim); + { + // override for less work and storage (dont need to store the extra padding of the last axis after iFFT) + fftPushConstants[1].output_strides.x = fftPushConstants[0].input_strides.x; + fftPushConstants[1].output_strides.y = fftPushConstants[0].input_strides.y; + fftPushConstants[1].output_strides.z = fftPushConstants[1].input_strides.z; + fftPushConstants[1].output_strides.w = fftPushConstants[1].input_strides.w; + // iFFT + fftPushConstants[2].input_dimensions = fftPushConstants[1].input_dimensions; + { + fftPushConstants[2].input_dimensions.w = fftPushConstants[0].input_dimensions.w^0x80000000u; + fftPushConstants[2].input_strides = fftPushConstants[1].output_strides; + fftPushConstants[2].output_strides = fftPushConstants[0].input_strides; + } + fftDispatchInfo[2] = fftDispatchInfo[0]; + } + assert(passes==2); + // pipelines + auto fftPipeline_ImageInput = driver->createComputePipeline(nullptr,core::smart_refctd_ptr(imageFirstFFTPipelineLayout),createShader(driver,0x1u<createComputePipeline(nullptr, std::move(convolvePipelineLayout), createShader(driver,0x1u<createComputePipeline(nullptr, std::move(lastFFTPipelineLayout), createShader(driver,0x1u<createDescriptorSet(core::smart_refctd_ptr(imageFirstFFTPipelineLayout->getDescriptorSetLayout(0u))); + updateDescriptorSet(fftDescriptorSet_Src_FirstFFT.get(), srcImageView, ISampler::ETC_MIRROR, fftOutputBuffer_0); + + // Convolution + auto convolveDescriptorSet = driver->createDescriptorSet(core::smart_refctd_ptr(convolvePipeline->getLayout()->getDescriptorSetLayout(0u))); + updateDescriptorSet_Convolution(driver, convolveDescriptorSet.get(), fftOutputBuffer_0, fftOutputBuffer_1, kernelNormalizedSpectrums); + + // Last Axis IFFT + auto lastFFTDescriptorSet = driver->createDescriptorSet(core::smart_refctd_ptr(lastFFTPipeline->getLayout()->getDescriptorSetLayout(0u))); + updateDescriptorSet_LastFFT(driver, lastFFTDescriptorSet.get(), fftOutputBuffer_1, outImgView); + + uint32_t outBufferIx = 0u; + auto lastPresentStamp = std::chrono::high_resolution_clock::now(); + bool savedToFile = false; + + auto downloadStagingArea = driver->getDefaultDownStreamingBuffer(); + + auto blitFBO = driver->addFrameBuffer(); + blitFBO->attach(video::EFAP_COLOR_ATTACHMENT0, std::move(outImgView)); + + while (device->run() && receiver.keepOpen()) + { + driver->beginScene(false, false); + + // Src Image First Axis FFT + driver->bindComputePipeline(fftPipeline_ImageInput.get()); + driver->bindDescriptorSets(EPBP_COMPUTE, imageFirstFFTPipelineLayout.get(), 0u, 1u, &fftDescriptorSet_Src_FirstFFT.get(), nullptr); + FFTClass::dispatchHelper(driver, imageFirstFFTPipelineLayout.get(), fftPushConstants[0], fftDispatchInfo[0]); + + // Src Image Last Axis FFT + Convolution + Convolved Last Axis IFFT Y + driver->bindComputePipeline(convolvePipeline.get()); + driver->bindDescriptorSets(EPBP_COMPUTE, convolvePipeline->getLayout(), 0u, 1u, &convolveDescriptorSet.get(), nullptr); + { + const auto& kernelImgExtent = kernelNormalizedSpectrums[0]->getCreationParameters().image->getCreationParameters().extent; + vec2 kernel_half_pixel_size{0.5f,0.5f}; + kernel_half_pixel_size.x /= kernelImgExtent.width; + kernel_half_pixel_size.y /= kernelImgExtent.height; + driver->pushConstants(convolvePipeline->getLayout(),ISpecializedShader::ESS_COMPUTE,offsetof(convolve_parameters_t,kernel_half_pixel_size),sizeof(convolve_parameters_t::kernel_half_pixel_size),&kernel_half_pixel_size); + } + FFTClass::dispatchHelper(driver, convolvePipeline->getLayout(), fftPushConstants[1], fftDispatchInfo[1]); + + // Last FFT Padding and Copy to GPU Image + driver->bindComputePipeline(lastFFTPipeline.get()); + driver->bindDescriptorSets(EPBP_COMPUTE, lastFFTPipeline->getLayout(), 0u, 1u, &lastFFTDescriptorSet.get(), nullptr); + { + const auto paddedSrcDim = FFTClass::padDimensions(marginSrcDim); + ivec2 unpad_offset = { 0,0 }; + for (auto i=0u; i<2u; i++) + if (fftDispatchInfo[2].workGroupCount[i]==1u) + (&unpad_offset.x)[i] = ((&paddedSrcDim.width)[i]-(&srcDim.width)[i])>>1u; + driver->pushConstants(lastFFTPipeline->getLayout(),ISpecializedShader::ESS_COMPUTE,offsetof(image_store_parameters_t,unpad_offset),sizeof(image_store_parameters_t::unpad_offset),&unpad_offset); + } + FFTClass::dispatchHelper(driver, lastFFTPipeline->getLayout(), fftPushConstants[2], fftDispatchInfo[2]); + + if(!savedToFile) + { + savedToFile = true; + + core::smart_refctd_ptr imageView; + const uint32_t colorBufferBytesize = srcDim.height * srcDim.width * asset::getTexelOrBlockBytesize(srcFormat); + + // create image + ICPUImage::SCreationParams imgParams; + imgParams.flags = static_cast(0u); // no flags + imgParams.type = ICPUImage::ET_2D; + imgParams.format = srcFormat; + imgParams.extent = srcDim; + imgParams.mipLevels = 1u; + imgParams.arrayLayers = 1u; + imgParams.samples = ICPUImage::ESCF_1_BIT; + auto image = ICPUImage::create(std::move(imgParams)); + + constexpr uint64_t timeoutInNanoSeconds = 300000000000u; + const auto waitPoint = std::chrono::high_resolution_clock::now()+std::chrono::nanoseconds(timeoutInNanoSeconds); + + uint32_t address = std::remove_pointer::type::invalid_address; // remember without initializing the address to be allocated to invalid_address you won't get an allocation! + const uint32_t alignment = 4096u; // common page size + auto unallocatedSize = downloadStagingArea->multi_alloc(waitPoint, 1u, &address, &colorBufferBytesize, &alignment); + if (unallocatedSize) + { + os::Printer::log("Could not download the buffer from the GPU!", ELL_ERROR); + } + + // set up regions + auto regions = core::make_refctd_dynamic_array >(1u); + { + auto& region = regions->front(); + + region.bufferOffset = 0u; + region.bufferRowLength = 0u; + region.bufferImageHeight = 0u; + //region.imageSubresource.aspectMask = wait for Vulkan; + region.imageSubresource.mipLevel = 0u; + region.imageSubresource.baseArrayLayer = 0u; + region.imageSubresource.layerCount = 1u; + region.imageOffset = { 0u,0u,0u }; + region.imageExtent = imgParams.extent; + } + + driver->copyImageToBuffer(outImg.get(), downloadStagingArea->getBuffer(), 1, ®ions->front()); + + auto downloadFence = driver->placeFence(true); + + auto* data = reinterpret_cast(downloadStagingArea->getBufferPointer()) + address; + auto cpubufferalias = core::make_smart_refctd_ptr > >(colorBufferBytesize, data, core::adopt_memory); + image->setBufferAndRegions(std::move(cpubufferalias),regions); + + // wait for download fence and then invalidate the CPU cache + { + auto result = downloadFence->waitCPU(timeoutInNanoSeconds,true); + if (result==E_DRIVER_FENCE_RETVAL::EDFR_TIMEOUT_EXPIRED||result==E_DRIVER_FENCE_RETVAL::EDFR_FAIL) + { + os::Printer::log("Could not download the buffer from the GPU, fence not signalled!", ELL_ERROR); + downloadStagingArea->multi_free(1u, &address, &colorBufferBytesize, nullptr); + continue; + } + if (downloadStagingArea->needsManualFlushOrInvalidate()) + driver->invalidateMappedMemoryRanges({{downloadStagingArea->getBuffer()->getBoundMemory(),address,colorBufferBytesize}}); + } + + // create image view + ICPUImageView::SCreationParams imgViewParams; + imgViewParams.flags = static_cast(0u); + imgViewParams.format = image->getCreationParameters().format; + imgViewParams.image = std::move(image); + imgViewParams.viewType = ICPUImageView::ET_2D; + imgViewParams.subresourceRange = {static_cast(0u),0u,1u,0u,1u}; + imageView = ICPUImageView::create(std::move(imgViewParams)); + + IAssetWriter::SAssetWriteParams wp(imageView.get()); + volatile bool success = am->writeAsset("convolved_exr.exr", wp); + assert(success); + } + + driver->blitRenderTargets(blitFBO, nullptr, false, false); + + driver->endScene(); + } + + return 0; +} \ No newline at end of file diff --git a/old_to_refactor/49_ComputeFFT/normalization.comp b/old_to_refactor/49_ComputeFFT/normalization.comp new file mode 100644 index 000000000..b3926090d --- /dev/null +++ b/old_to_refactor/49_ComputeFFT/normalization.comp @@ -0,0 +1,34 @@ +layout(local_size_x=16, local_size_y=16, local_size_z=1) in; + +#include + +layout(set=0, binding=0) restrict readonly buffer InBuffer +{ + nbl_glsl_ext_FFT_storage_t in_data[]; +}; + +layout(set=0, binding=1, rg16f) uniform image2D NormalizedKernel[3]; + +layout(push_constant) uniform PushConstants +{ + uvec4 strides; + uvec4 bitreverse_shift; +} pc; + +#include + +void main() +{ + nbl_glsl_complex value = nbl_glsl_ext_FFT_storage_t_get(in_data[nbl_glsl_dot(gl_GlobalInvocationID,pc.strides.xyz)]); + + // imaginary component will be 0, image shall be positive + vec3 avg; + for (uint i=0u; i<3u; i++) + avg[i] = nbl_glsl_ext_FFT_storage_t_get(in_data[pc.strides.z*i]).x; + const float power = (nbl_glsl_scRGBtoXYZ*avg).y; + + const uvec2 coord = bitfieldReverse(gl_GlobalInvocationID.xy)>>pc.bitreverse_shift.xy; + const nbl_glsl_complex shift = nbl_glsl_expImaginary(-nbl_glsl_PI*float(coord.x+coord.y)); + value = nbl_glsl_complex_mul(value,shift)/power; + imageStore(NormalizedKernel[gl_WorkGroupID.z],ivec2(coord),vec4(value,0.0,0.0)); +} \ No newline at end of file diff --git a/old_to_refactor/49_ComputeFFT/pipeline.groovy b/old_to_refactor/49_ComputeFFT/pipeline.groovy new file mode 100644 index 000000000..64874da2a --- /dev/null +++ b/old_to_refactor/49_ComputeFFT/pipeline.groovy @@ -0,0 +1,50 @@ +import org.DevshGraphicsProgramming.Agent +import org.DevshGraphicsProgramming.BuilderInfo +import org.DevshGraphicsProgramming.IBuilder + +class CComputeFFTBuilder extends IBuilder +{ + public CComputeFFTBuilder(Agent _agent, _info) + { + super(_agent, _info) + } + + @Override + public boolean prepare(Map axisMapping) + { + return true + } + + @Override + public boolean build(Map axisMapping) + { + IBuilder.CONFIGURATION config = axisMapping.get("CONFIGURATION") + IBuilder.BUILD_TYPE buildType = axisMapping.get("BUILD_TYPE") + + def nameOfBuildDirectory = getNameOfBuildDirectory(buildType) + def nameOfConfig = getNameOfConfig(config) + + agent.execute("cmake --build ${info.rootProjectPath}/${nameOfBuildDirectory}/${info.targetProjectPathRelativeToRoot} --target ${info.targetBaseName} --config ${nameOfConfig} -j12 -v") + + return true + } + + @Override + public boolean test(Map axisMapping) + { + return true + } + + @Override + public boolean install(Map axisMapping) + { + return true + } +} + +def create(Agent _agent, _info) +{ + return new CComputeFFTBuilder(_agent, _info) +} + +return this \ No newline at end of file diff --git a/old_to_refactor/51_RadixSort/config.json.template b/old_to_refactor/51_RadixSort/config.json.template new file mode 100644 index 000000000..f0ec8f284 --- /dev/null +++ b/old_to_refactor/51_RadixSort/config.json.template @@ -0,0 +1,30 @@ +{ + "enableParallelBuild": true, + "threadsPerBuildProcess" : 2, + "isExecuted": false, + "scriptPath": "", + "cmake": { + "configurations": [ "Release", "Debug", "RelWithDebInfo" ], + "buildModes": [], + "requiredOptions": [] + }, + "profiles": [ + { + "backend": "vulkan", + "platform": "windows", + "buildModes": [], + "runConfiguration": "Release", + "gpuArchitectures": [], + "inputs": [] + }, + { + "backend": "vulkan", + "platform": "windows", + "buildModes": [], + "runConfiguration": "Debug", + "gpuArchitectures": [], + "inputs": [] + } + ], + "inputs": [] +} \ No newline at end of file diff --git a/old_to_refactor/51_RadixSort/config.json.template.orig b/old_to_refactor/51_RadixSort/config.json.template.orig new file mode 100644 index 000000000..f0ec8f284 --- /dev/null +++ b/old_to_refactor/51_RadixSort/config.json.template.orig @@ -0,0 +1,30 @@ +{ + "enableParallelBuild": true, + "threadsPerBuildProcess" : 2, + "isExecuted": false, + "scriptPath": "", + "cmake": { + "configurations": [ "Release", "Debug", "RelWithDebInfo" ], + "buildModes": [], + "requiredOptions": [] + }, + "profiles": [ + { + "backend": "vulkan", + "platform": "windows", + "buildModes": [], + "runConfiguration": "Release", + "gpuArchitectures": [], + "inputs": [] + }, + { + "backend": "vulkan", + "platform": "windows", + "buildModes": [], + "runConfiguration": "Debug", + "gpuArchitectures": [], + "inputs": [] + } + ], + "inputs": [] +} \ No newline at end of file diff --git a/tmp/.gitignore b/tmp/.gitignore new file mode 100644 index 000000000..d809be6e2 --- /dev/null +++ b/tmp/.gitignore @@ -0,0 +1,2 @@ +./rtSamples.bin +./normalCache101010.sse \ No newline at end of file From 960ef01c5aa567c7a6826b6449abec327f52e2d9 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Sun, 20 Oct 2024 19:18:14 +0200 Subject: [PATCH 002/205] add range_value_t to IProjection --- common/include/camera/IProjection.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/include/camera/IProjection.hpp b/common/include/camera/IProjection.hpp index 164c48b9f..d0486c013 100644 --- a/common/include/camera/IProjection.hpp +++ b/common/include/camera/IProjection.hpp @@ -11,8 +11,8 @@ requires nbl::is_any_of_v, float64_t4x4, float class IProjection { public: - using value_t = std::ranges::iterator_t; using range_t = Range; + using range_value_t = std::ranges::range_value_t; protected: IProjection(const range_t& matrices) : m_projMatrices(matrices) {} From 2801151cbee9e64797dcadcb12db93675beeb109 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Mon, 21 Oct 2024 14:39:53 +0200 Subject: [PATCH 003/205] create IProjectionRange, pair with IProjection, use concepts for specific range type & projection matrix type constraints, update ILinearProjection and CCubeProjection - now I can pair IProjection with a gimbal --- common/include/camera/CCubeProjection.hpp | 12 +++--- common/include/camera/ILinearProjection.hpp | 11 +++--- common/include/camera/IProjection.hpp | 43 +++++++++++++++++---- 3 files changed, 49 insertions(+), 17 deletions(-) diff --git a/common/include/camera/CCubeProjection.hpp b/common/include/camera/CCubeProjection.hpp index ef3d25d71..2e2a91218 100644 --- a/common/include/camera/CCubeProjection.hpp +++ b/common/include/camera/CCubeProjection.hpp @@ -8,7 +8,9 @@ namespace nbl::hlsl struct CCubeProjectionBase { - using base_t = ILinearProjection>; + using projection_t = typename IProjection; + using projection_range_t = std::array; + using base_t = ILinearProjection; }; //! Class providing linear cube projections with projection matrix per face of a cube, each projection matrix represents a single view-port @@ -16,13 +18,13 @@ class CCubeProjection : public CCubeProjectionBase::base_t { public: using base_t = typename CCubeProjectionBase::base_t; - using range_t = typename base_t::range_t; + using projection_range_t = typename base_t::range_t; - CCubeProjection(range_t&& matrices = {}) : base_t(std::move(matrices)) {} + CCubeProjection(projection_range_t&& projections = {}) : base_t(std::move(projections)) {} - range_t& getCubeFaceProjectionMatrices() + projection_range_t& getCubeFaceProjections() { - return base_t::getViewportMatrices(); + return base_t::getViewportProjections(); } }; diff --git a/common/include/camera/ILinearProjection.hpp b/common/include/camera/ILinearProjection.hpp index ba39018a5..3539b153f 100644 --- a/common/include/camera/ILinearProjection.hpp +++ b/common/include/camera/ILinearProjection.hpp @@ -8,18 +8,19 @@ namespace nbl::hlsl //! Interface class for linear projections range storage - a projection matrix represents single view-port template -class ILinearProjection : protected IProjection +class ILinearProjection : protected IProjectionRange { public: - using base_t = typename IProjection; + using base_t = typename IProjectionRange; using range_t = typename base_t::range_t; + using projection_t = typename base_t::projection_t; - ILinearProjection(range_t&& matrices) : base_t(matrices) {} + ILinearProjection(range_t&& projections) : base_t(projections) {} protected: - inline range_t& getViewportMatrices() + inline range_t& getViewportProjections() { - return base_t::m_projMatrices; + return base_t::m_projectionRange; } }; diff --git a/common/include/camera/IProjection.hpp b/common/include/camera/IProjection.hpp index d0486c013..fbd6f43e0 100644 --- a/common/include/camera/IProjection.hpp +++ b/common/include/camera/IProjection.hpp @@ -5,20 +5,49 @@ namespace nbl::hlsl { -//! Interface class for projection matrices range storage -template> -requires nbl::is_any_of_v, float64_t4x4, float32_t4x4> + +template +concept ProjectionMatrix = is_any_of_v; + +//! Interface class for projection +template class IProjection { +public: + using value_t = T; + + IProjection(const value_t& matrix = {}) : m_projectionMatrix(matrix) {} + value_t& getProjectionMatrix() { return m_projectionMatrix; } + +protected: + value_t m_projectionMatrix; +}; + +template +concept ProjectionRange = requires +{ + typename std::ranges::range_value_t; + std::is_base_of_v::value_t>, std::ranges::range_value_t>; +}; + +//! Interface class for a range of IProjection projections +template, 1u>> +class IProjectionRange +{ public: using range_t = Range; - using range_value_t = std::ranges::range_value_t; + using projection_t = std::ranges::range_value_t; + + //! Constructor for the range of projections + IProjectionRange(const range_t& projections) : m_projectionRange(projections) {} + + //! Get the stored range of projections + const range_t& getProjections() const { return m_projectionRange; } protected: - IProjection(const range_t& matrices) : m_projMatrices(matrices) {} - range_t m_projMatrices; + range_t m_projectionRange; }; -} // nbl::hlsl namespace +} // namespace nbl::hlsl #endif // _NBL_IPROJECTION_HPP_ \ No newline at end of file From 2fe2a7fb4b740fdb5023666f29995ff7eeca9c7c Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Mon, 21 Oct 2024 18:39:36 +0200 Subject: [PATCH 004/205] save draft of CVirtualCameraEvent.hpp & ICameraControl.hpp, update sources, save main.cpp tests, add TODO comments --- 61_UI/main.cpp | 24 ++++ common/include/camera/CCubeProjection.hpp | 19 +-- common/include/camera/CVirtualCameraEvent.hpp | 63 ++++++++++ common/include/camera/ICameraControl.hpp | 117 ++++++++++++++++++ common/include/camera/ILinearProjection.hpp | 2 +- common/include/camera/IProjection.hpp | 14 +-- 6 files changed, 223 insertions(+), 16 deletions(-) create mode 100644 common/include/camera/CVirtualCameraEvent.hpp create mode 100644 common/include/camera/ICameraControl.hpp diff --git a/61_UI/main.cpp b/61_UI/main.cpp index ef03d88d6..37e50805c 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -4,6 +4,10 @@ #include "common.hpp" +#include "camera/CCubeProjection.hpp" +#include "camera/ICameraControl.hpp" +#include "glm/glm/ext/matrix_clip_space.hpp" // TODO: TESTING + /* Renders scene texture to an offline framebuffer which color attachment @@ -489,6 +493,26 @@ class UISampleApp final : public examples::SimpleWindowedApplication oracle.reportBeginFrameRecord(); camera.mapKeysToArrows(); + /* + TESTS, TODO: remove all once finished work & integrate with the example properly + */ + + using cube_projection_t = CCubeProjection; + using constraints_t = CCubeProjection<>::constraints_t; + using camera_control_t = ICameraController; + using gimbal_t = camera_control_t::CGimbal; + + cube_projection_t cubeProjection; // can init all at construction, but will init only first for tests + auto& projections = cubeProjection.getCubeFaceProjections(); + auto firstFaceProjection = projections.front(); + firstFaceProjection = make_smart_refctd_ptr(glm::perspectiveLH(glm::radians(fov), float(m_window->getWidth()) / float(m_window->getHeight()), zNear, zFar)); + + const float32_t3 position(cosf(camYAngle)* cosf(camXAngle)* transformParams.camDistance, sinf(camXAngle)* transformParams.camDistance, sinf(camYAngle)* cosf(camXAngle)* transformParams.camDistance), + target(0.f, 0.f, 0.f), up(0.f, 1.f, 0.f); + + auto gimbal = make_smart_refctd_ptr(smart_refctd_ptr(firstFaceProjection), position, target, up); + auto controller = make_smart_refctd_ptr(smart_refctd_ptr(gimbal)); + return true; } diff --git a/common/include/camera/CCubeProjection.hpp b/common/include/camera/CCubeProjection.hpp index 2e2a91218..e09d01dce 100644 --- a/common/include/camera/CCubeProjection.hpp +++ b/common/include/camera/CCubeProjection.hpp @@ -6,23 +6,26 @@ namespace nbl::hlsl { -struct CCubeProjectionBase +template +struct CCubeProjectionConstraints { - using projection_t = typename IProjection; - using projection_range_t = std::array; + using matrix_t = typename T; + using projection_t = typename IProjection; + using projection_range_t = std::array, 6u>; using base_t = ILinearProjection; }; //! Class providing linear cube projections with projection matrix per face of a cube, each projection matrix represents a single view-port -class CCubeProjection : public CCubeProjectionBase::base_t +template +class CCubeProjection : public CCubeProjectionConstraints::base_t { public: - using base_t = typename CCubeProjectionBase::base_t; - using projection_range_t = typename base_t::range_t; + using constraints_t = CCubeProjectionConstraints; + using base_t = typename constraints_t::base_t; - CCubeProjection(projection_range_t&& projections = {}) : base_t(std::move(projections)) {} + CCubeProjection(constraints_t::projection_range_t&& projections = {}) : base_t(std::move(projections)) {} - projection_range_t& getCubeFaceProjections() + constraints_t::projection_range_t& getCubeFaceProjections() { return base_t::getViewportProjections(); } diff --git a/common/include/camera/CVirtualCameraEvent.hpp b/common/include/camera/CVirtualCameraEvent.hpp new file mode 100644 index 000000000..b91712321 --- /dev/null +++ b/common/include/camera/CVirtualCameraEvent.hpp @@ -0,0 +1,63 @@ +#ifndef _NBL_VIRTUAL_CAMERA_EVENT_HPP_ +#define _NBL_VIRTUAL_CAMERA_EVENT_HPP_ + +#include "nbl/builtin/hlsl/cpp_compat/matrix.hlsl" + +// TODO: DIFFERENT NAMESPACE +namespace nbl::hlsl +{ + +//! Virtual camera event representing a manipulation +enum class VirtualEventType +{ + //! Moves the camera forward or backward + MoveForward, + + //! Zooms in or out + Zoom, + + //! Moves the camera left/right and up/down + Strafe, + + //! Rotates the camera horizontally + Pan, + + //! Rotates the camera vertically + Tilt, + + //! Rolls the camera around the Z axis + Roll, + + //! Resets the camera to a default position and orientation + Reset +}; + +//! We encode all manipulation type values into a vec4 & assume they are written in NDC coordinate system with respect to camera +using manipulation_value_t = float64_t4x4; + +class CVirtualCameraEvent +{ +public: + CVirtualCameraEvent(VirtualEventType type, const manipulation_value_t value) + : type(type), value(value) {} + + // Returns the type of virtual event + VirtualEventType getType() const + { + return type; + } + + // Returns the associated value for manipulation + manipulation_value_t getValue() const + { + return value; + } + +private: + VirtualEventType type; + manipulation_value_t value; +}; + +} + +#endif // _NBL_VIRTUAL_CAMERA_EVENT_HPP_ \ No newline at end of file diff --git a/common/include/camera/ICameraControl.hpp b/common/include/camera/ICameraControl.hpp new file mode 100644 index 000000000..c62871644 --- /dev/null +++ b/common/include/camera/ICameraControl.hpp @@ -0,0 +1,117 @@ +#ifndef _NBL_I_CAMERA_CONTROLLER_HPP_ +#define _NBL_I_CAMERA_CONTROLLER_HPP_ + +#include "IProjection.hpp" +#include "CVirtualCameraEvent.hpp" +#include "glm/glm/ext/matrix_transform.hpp" // TODO: TEMPORARY!!! whatever used will be moved to cpp + +// TODO: DIFFERENT NAMESPACE +namespace nbl::hlsl +{ + +template +class ICameraController : virtual public core::IReferenceCounted +{ +public: + using projection_t = typename IProjection; + + class CGimbal : virtual public core::IReferenceCounted + { + public: + CGimbal(const core::smart_refctd_ptr&& projection, const float32_t3& position, const float32_t3& lookat, const float32_t3& upVec = float32_t3(0.0f, 1.0f, 0.0f), const float32_t3& backupUpVec = float32_t3(0.5f, 1.0f, 0.0f)) + : m_projection(std::move(projection)), m_position(position), m_target(lookat), m_upVec(upVec), m_backupUpVec(backupUpVec), m_initialPosition(position), m_initialTarget(lookat), m_viewMatrix({}), m_isLeftHanded(nbl::hlsl::determinant(m_projection->getProjectionMatrix()) < 0.f) + { + //! I make an assumption that we take ownership of projection (just to make less calculations on gimbal update iterations [but maybe I shouldnt, think of it!]) + recomputeViewMatrix(); + } + + // TODO: ctor with core::path to json config file to load defaults + + const projection_t* getProjection() { return m_projection.get(); } + + // TODO: gimbal methods (to handle VirtualEventType requests) + + private: + inline void recomputeViewMatrix() + { + auto localTarget = glm::normalize(m_target - m_position); + + // If up vector and vector to the target are the same, adjust the up vector + auto up = glm::normalize(m_upVec); + auto cross = glm::cross(localTarget, up); + bool upVectorNeedsChange = glm::dot(cross, cross) == 0; + + if (upVectorNeedsChange) + up = glm::normalize(m_backupUpVec); + + if (m_isLeftHanded) + m_viewMatrix = glm::lookAtLH(m_position, m_target, up); + else + m_viewMatrix = glm::lookAtRH(m_position, m_target, up); + } + + const core::smart_refctd_ptr m_projection; + float32_t3 m_position, m_target, m_upVec, m_backupUpVec; + const float32_t3 m_initialPosition, m_initialTarget; + + float64_t4x4 m_viewMatrix; + const bool m_isLeftHanded; + }; + + ICameraController(core::smart_refctd_ptr&& gimbal) + : m_gimbal(std::move(gimbal)) {} + + void processVirtualEvent(const CVirtualCameraEvent& virtualEvent) + { + // we will treat all manipulation event values as NDC, also for non manipulation events + // we will define how to handle it, all values are encoded onto vec4 (manipulation_value_t) + manipulation_value_t value = virtualEvent.getValue(); + + // TODO: this will use gimbal to handle a virtual event registered by a class (wip) which maps physical keys to virtual events + + case VirtualEventType::MoveForward: + { + // TODO + } break; + + case VirtualEventType::Strafe: + { + // TODO + } break; + + case VirtualEventType::Zoom: + { + // TODO + } break; + + case VirtualEventType::Pan: + { + // TODO + } break; + + case VirtualEventType::Tilt: + { + // TODO + } break; + + case VirtualEventType::Roll: + { + // TODO + } break; + + case VirtualEventType::Reset: + { + // TODO + } break; + + default: + break; + } + +private: + core::smart_refctd_ptr m_gimbal; +}; + +} // nbl::hlsl namespace + +#endif // _NBL_I_CAMERA_CONTROLLER_HPP_ \ No newline at end of file diff --git a/common/include/camera/ILinearProjection.hpp b/common/include/camera/ILinearProjection.hpp index 3539b153f..30a4db358 100644 --- a/common/include/camera/ILinearProjection.hpp +++ b/common/include/camera/ILinearProjection.hpp @@ -15,7 +15,7 @@ class ILinearProjection : protected IProjectionRange using range_t = typename base_t::range_t; using projection_t = typename base_t::projection_t; - ILinearProjection(range_t&& projections) : base_t(projections) {} + ILinearProjection(range_t&& projections) : base_t(std::move(projections)) {} protected: inline range_t& getViewportProjections() diff --git a/common/include/camera/IProjection.hpp b/common/include/camera/IProjection.hpp index fbd6f43e0..f3edfccc4 100644 --- a/common/include/camera/IProjection.hpp +++ b/common/include/camera/IProjection.hpp @@ -1,7 +1,7 @@ #ifndef _NBL_IPROJECTION_HPP_ #define _NBL_IPROJECTION_HPP_ -#include +#include "nbl/builtin/hlsl/cpp_compat/matrix.hlsl" namespace nbl::hlsl { @@ -10,8 +10,8 @@ template concept ProjectionMatrix = is_any_of_v; //! Interface class for projection -template -class IProjection +template +class IProjection : virtual public core::IReferenceCounted { public: using value_t = T; @@ -24,14 +24,14 @@ class IProjection }; template -concept ProjectionRange = requires +concept ProjectionRange = requires { typename std::ranges::range_value_t; - std::is_base_of_v::value_t>, std::ranges::range_value_t>; + // TODO: smart_refctd_ptr check for range_value_t + its type check to grant IProjection }; //! Interface class for a range of IProjection projections -template, 1u>> +template>, 1u>> class IProjectionRange { public: @@ -39,7 +39,7 @@ class IProjectionRange using projection_t = std::ranges::range_value_t; //! Constructor for the range of projections - IProjectionRange(const range_t& projections) : m_projectionRange(projections) {} + IProjectionRange(range_t&& projections) : m_projectionRange(std::move(projections)) {} //! Get the stored range of projections const range_t& getProjections() const { return m_projectionRange; } From ebd0a6e30a634e111c9c600e3fe7aa86dc0781c1 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Wed, 23 Oct 2024 11:53:07 +0200 Subject: [PATCH 005/205] update ICameraControl & CVirtualCameraEvent, add processVirtualEvent --- common/include/camera/CVirtualCameraEvent.hpp | 78 +++++---- common/include/camera/ICameraControl.hpp | 161 +++++++++++++----- 2 files changed, 165 insertions(+), 74 deletions(-) diff --git a/common/include/camera/CVirtualCameraEvent.hpp b/common/include/camera/CVirtualCameraEvent.hpp index b91712321..efc336695 100644 --- a/common/include/camera/CVirtualCameraEvent.hpp +++ b/common/include/camera/CVirtualCameraEvent.hpp @@ -10,52 +10,72 @@ namespace nbl::hlsl //! Virtual camera event representing a manipulation enum class VirtualEventType { - //! Moves the camera forward or backward - MoveForward, - - //! Zooms in or out - Zoom, - - //! Moves the camera left/right and up/down + //! Move the camera in the direction of strafe vector Strafe, - //! Rotates the camera horizontally - Pan, - - //! Rotates the camera vertically - Tilt, + //! Update orientation of camera by rotating around X, Y, Z axes + Rotate, - //! Rolls the camera around the Z axis - Roll, - - //! Resets the camera to a default position and orientation - Reset + //! Signals boolean state, for example "reset" + State }; -//! We encode all manipulation type values into a vec4 & assume they are written in NDC coordinate system with respect to camera -using manipulation_value_t = float64_t4x4; - class CVirtualCameraEvent { public: - CVirtualCameraEvent(VirtualEventType type, const manipulation_value_t value) - : type(type), value(value) {} + using manipulation_encode_t = float32_t4; + + struct StrafeManipulation + { + float32_t3 direction = {}; + float distance = {}; + }; + + struct RotateManipulation + { + float pitch = {}, roll = {}, yaw = {}; + }; + + struct StateManipulation + { + uint32_t reset : 1; + uint32_t reserved : 31; + + StateManipulation() { memset(this, 0, sizeof(StateManipulation)); } + ~StateManipulation() {} + }; + + union ManipulationValue + { + StrafeManipulation strafe; + RotateManipulation rotation; + StateManipulation state; + + ManipulationValue() { memset(this, 0, sizeof(ManipulationValue)); } + ~ManipulationValue() {} + }; + + CVirtualCameraEvent(VirtualEventType type, const ManipulationValue manipulation) + : m_type(type), m_manipulation(manipulation) + { + static_assert(sizeof(manipulation_encode_t) == sizeof(ManipulationValue)); + } - // Returns the type of virtual event + // Returns the type of manipulation value VirtualEventType getType() const { - return type; + return m_type; } - // Returns the associated value for manipulation - manipulation_value_t getValue() const + // Returns manipulation value + ManipulationValue getManipulation() const { - return value; + return m_manipulation; } private: - VirtualEventType type; - manipulation_value_t value; + VirtualEventType m_type; + ManipulationValue m_manipulation; }; } diff --git a/common/include/camera/ICameraControl.hpp b/common/include/camera/ICameraControl.hpp index c62871644..b6be74801 100644 --- a/common/include/camera/ICameraControl.hpp +++ b/common/include/camera/ICameraControl.hpp @@ -4,6 +4,7 @@ #include "IProjection.hpp" #include "CVirtualCameraEvent.hpp" #include "glm/glm/ext/matrix_transform.hpp" // TODO: TEMPORARY!!! whatever used will be moved to cpp +#include "glm/glm/gtc/quaternion.hpp" // TODO: DIFFERENT NAMESPACE namespace nbl::hlsl @@ -19,30 +20,98 @@ class ICameraController : virtual public core::IReferenceCounted { public: CGimbal(const core::smart_refctd_ptr&& projection, const float32_t3& position, const float32_t3& lookat, const float32_t3& upVec = float32_t3(0.0f, 1.0f, 0.0f), const float32_t3& backupUpVec = float32_t3(0.5f, 1.0f, 0.0f)) - : m_projection(std::move(projection)), m_position(position), m_target(lookat), m_upVec(upVec), m_backupUpVec(backupUpVec), m_initialPosition(position), m_initialTarget(lookat), m_viewMatrix({}), m_isLeftHanded(nbl::hlsl::determinant(m_projection->getProjectionMatrix()) < 0.f) + : m_projection(projection), m_position(position), m_target(lookat), m_upVec(upVec), m_backupUpVec(backupUpVec), m_initialPosition(position), m_initialTarget(lookat), m_orientation(glm::quat(1.0f, 0.0f, 0.0f, 0.0f)), m_viewMatrix({}), m_isLeftHanded(isLeftHanded(m_projection->getProjectionMatrix())) { - //! I make an assumption that we take ownership of projection (just to make less calculations on gimbal update iterations [but maybe I shouldnt, think of it!]) recomputeViewMatrix(); } + //! Start a gimbal manipulation session + inline void begin() + { + needsToRecomputeViewMatrix = false; + } + + //! End the gimbal manipulation session, recompute matrices and check projection + inline void end() + { + m_isLeftHanded = isLeftHanded(m_projection->getProjectionMatrix()); + + // Recompute the view matrix + if(needsToRecomputeViewMatrix) + recomputeViewMatrix(); + + needsToRecomputeViewMatrix = false; + } + + inline float32_t3 getLocalTarget() const + { + return m_target - m_position; + } + + inline float32_t3 getForwardDirection() const + { + return glm::normalize(getLocalTarget()); + } + + //! Reset the gimbal to its initial position, target, and orientation + inline void reset() + { + m_position = m_initialPosition; + m_target = m_initialTarget; + m_orientation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f); + + recomputeViewMatrix(); // Recompute the view matrix after resetting + } + + //! Move the camera in the direction of strafe (mostly left/right, up/down) + void strafe(const glm::vec3& direction, float distance) + { + if (distance != 0.0f) + { + const auto strafeVector = glm::normalize(direction) * distance; + m_position += strafeVector; + m_target += strafeVector; + + needsToRecomputeViewMatrix = true; + } + } + + //! Update orientation of camera by rotating around all XYZ axes - delta rotations in radians + void rotate(float dPitchRadians, float dYawDeltaRadians, float dRollDeltaRadians) + { + // Rotate around X (pitch) + glm::quat qPitch = glm::angleAxis(dPitchRadians, glm::vec3(1.0f, 0.0f, 0.0f)); + + // Rotate around Y (yaw) + glm::quat qYaw = glm::angleAxis(dYawDeltaRadians, glm::vec3(0.0f, 1.0f, 0.0f)); + + // Rotate around Z (roll) // TODO: handness!! + glm::quat qRoll = glm::angleAxis(dRollDeltaRadians, glm::vec3(0.0f, 0.0f, 1.0f)); + + // Combine the new rotations with the current orientation + m_orientation = glm::normalize(qYaw * qPitch * qRoll * m_orientation); + + // Now we have rotation transformation as 3x3 matrix + auto rotate = glm::mat3_cast(m_orientation); + + // We do not change magnitude of the vector + auto localTargetRotated = rotate * getLocalTarget(); + + // And we can simply update target vector + m_target = m_position + localTargetRotated; + + // TODO: std::any + nice checks for deltas (radians - periodic!) + needsToRecomputeViewMatrix = true; + } + // TODO: ctor with core::path to json config file to load defaults const projection_t* getProjection() { return m_projection.get(); } - // TODO: gimbal methods (to handle VirtualEventType requests) - private: inline void recomputeViewMatrix() { - auto localTarget = glm::normalize(m_target - m_position); - - // If up vector and vector to the target are the same, adjust the up vector - auto up = glm::normalize(m_upVec); - auto cross = glm::cross(localTarget, up); - bool upVectorNeedsChange = glm::dot(cross, cross) == 0; - - if (upVectorNeedsChange) - up = glm::normalize(m_backupUpVec); + auto up = getPatchedUpVector(); if (m_isLeftHanded) m_viewMatrix = glm::lookAtLH(m_position, m_target, up); @@ -50,12 +119,37 @@ class ICameraController : virtual public core::IReferenceCounted m_viewMatrix = glm::lookAtRH(m_position, m_target, up); } + inline float32_t3 getPatchedUpVector() + { + // if up vector and vector to the target are the same we patch the up vector + auto up = glm::normalize(m_upVec); + + const auto localTarget = getForwardDirection(); + const auto cross = glm::cross(localTarget, up); + + // we compute squared length but for checking if the len is 0 it doesnt matter + const bool upVectorZeroLength = glm::dot(cross, cross) == 0.f; + + if (upVectorZeroLength) + up = glm::normalize(m_backupUpVec); + + return up; + } + + inline bool isLeftHanded(const auto& projectionMatrix) + { + return nbl::hlsl::determinant(projectionMatrix) < 0.f; + } + const core::smart_refctd_ptr m_projection; float32_t3 m_position, m_target, m_upVec, m_backupUpVec; const float32_t3 m_initialPosition, m_initialTarget; + glm::quat m_orientation; float64_t4x4 m_viewMatrix; - const bool m_isLeftHanded; + bool m_isLeftHanded; + + bool needsToRecomputeViewMatrix = false; }; ICameraController(core::smart_refctd_ptr&& gimbal) @@ -63,45 +157,22 @@ class ICameraController : virtual public core::IReferenceCounted void processVirtualEvent(const CVirtualCameraEvent& virtualEvent) { - // we will treat all manipulation event values as NDC, also for non manipulation events - // we will define how to handle it, all values are encoded onto vec4 (manipulation_value_t) - manipulation_value_t value = virtualEvent.getValue(); - - // TODO: this will use gimbal to handle a virtual event registered by a class (wip) which maps physical keys to virtual events + const auto manipulation = virtualEvent.getManipulation(); - case VirtualEventType::MoveForward: - { - // TODO - } break; - case VirtualEventType::Strafe: { - // TODO - } break; - - case VirtualEventType::Zoom: - { - // TODO + m_gimbal->strafe(manipulation.strafe.direction, manipulation.strafe.distance); } break; - - case VirtualEventType::Pan: - { - // TODO - } break; - - case VirtualEventType::Tilt: - { - // TODO - } break; - - case VirtualEventType::Roll: + + case VirtualEventType::Rotate: { - // TODO + m_gimbal->rotate(manipulation.rotation.pitch, manipulation.rotation.yaw, manipulation.rotation.roll); } break; - case VirtualEventType::Reset: + case VirtualEventType::State: { - // TODO + if (manipulation.state.reset) + m_gimbal->reset(); } break; default: From a0ec5aef6bab247a39b011a96ce201194e46dcc2 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Wed, 23 Oct 2024 18:18:17 +0200 Subject: [PATCH 006/205] put CVirtualCameraEvent logic into ICameraController, update the class with abstract manipulate method - expect virtual event & a gimbal. Update CGimbal with new manipulation & set methods, start replacing CCamera content with new gimbal & controller --- common/include/CCamera.hpp | 67 +---- common/include/camera/CVirtualCameraEvent.hpp | 71 ----- common/include/camera/ICameraControl.hpp | 283 ++++++++++++++---- 3 files changed, 241 insertions(+), 180 deletions(-) diff --git a/common/include/CCamera.hpp b/common/include/CCamera.hpp index 1b0fe9c0f..54c6477a3 100644 --- a/common/include/CCamera.hpp +++ b/common/include/CCamera.hpp @@ -11,70 +11,21 @@ #include #include -class Camera +#include "camera/ICameraControl.hpp" + +// FPS Camera, we will have more types soon + +template +class Camera : public ICameraController { public: - Camera() = default; - Camera(const nbl::core::vectorSIMDf& position, const nbl::core::vectorSIMDf& lookat, const nbl::core::matrix4SIMD& 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) - { - initDefaultKeysMap(); - allKeysUp(); - setProjectionMatrix(projection); - recomputeViewMatrix(); - } + using matrix_t = T; + Camera() = default; ~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::core::matrix4SIMD& getProjectionMatrix() const { return projMatrix; } - inline const nbl::core::matrix3x4SIMD& getViewMatrix() const { return viewMatrix; } - inline const nbl::core::matrix4SIMD& getConcatenatedMatrix() const { return concatMatrix; } - - inline void setProjectionMatrix(const nbl::core::matrix4SIMD& projection) - { - projMatrix = projection; - const auto hlslMatMap = *reinterpret_cast(&projMatrix); // TEMPORARY TILL THE CAMERA CLASS IS REFACTORED TO WORK WITH HLSL MATRICIES! - { - leftHanded = nbl::hlsl::determinant(hlslMatMap) < 0.f; - } - concatMatrix = nbl::core::matrix4SIMD::concatenateBFollowedByAPrecisely(projMatrix, nbl::core::matrix4SIMD(viewMatrix)); - } + inline void setPosition(const nbl::core::vectorSIMDf& pos) { diff --git a/common/include/camera/CVirtualCameraEvent.hpp b/common/include/camera/CVirtualCameraEvent.hpp index efc336695..309c91662 100644 --- a/common/include/camera/CVirtualCameraEvent.hpp +++ b/common/include/camera/CVirtualCameraEvent.hpp @@ -7,77 +7,6 @@ namespace nbl::hlsl { -//! Virtual camera event representing a manipulation -enum class VirtualEventType -{ - //! Move the camera in the direction of strafe vector - Strafe, - - //! Update orientation of camera by rotating around X, Y, Z axes - Rotate, - - //! Signals boolean state, for example "reset" - State -}; - -class CVirtualCameraEvent -{ -public: - using manipulation_encode_t = float32_t4; - - struct StrafeManipulation - { - float32_t3 direction = {}; - float distance = {}; - }; - - struct RotateManipulation - { - float pitch = {}, roll = {}, yaw = {}; - }; - - struct StateManipulation - { - uint32_t reset : 1; - uint32_t reserved : 31; - - StateManipulation() { memset(this, 0, sizeof(StateManipulation)); } - ~StateManipulation() {} - }; - - union ManipulationValue - { - StrafeManipulation strafe; - RotateManipulation rotation; - StateManipulation state; - - ManipulationValue() { memset(this, 0, sizeof(ManipulationValue)); } - ~ManipulationValue() {} - }; - - CVirtualCameraEvent(VirtualEventType type, const ManipulationValue manipulation) - : m_type(type), m_manipulation(manipulation) - { - static_assert(sizeof(manipulation_encode_t) == sizeof(ManipulationValue)); - } - - // Returns the type of manipulation value - VirtualEventType getType() const - { - return m_type; - } - - // Returns manipulation value - ManipulationValue getManipulation() const - { - return m_manipulation; - } - -private: - VirtualEventType m_type; - ManipulationValue m_manipulation; -}; - } #endif // _NBL_VIRTUAL_CAMERA_EVENT_HPP_ \ No newline at end of file diff --git a/common/include/camera/ICameraControl.hpp b/common/include/camera/ICameraControl.hpp index b6be74801..0618f90de 100644 --- a/common/include/camera/ICameraControl.hpp +++ b/common/include/camera/ICameraControl.hpp @@ -16,55 +16,227 @@ class ICameraController : virtual public core::IReferenceCounted public: using projection_t = typename IProjection; + enum VirtualEventType : uint8_t + { + // Strafe forward + MoveForward = 0, + + // Strafe backward + MoveBackward, + + // Strafe left + MoveLeft, + + // Strafe right + MoveRight, + + // Strafe up + MoveUp, + + // Strafe down + MoveDown, + + // Tilt the camera upward (pitch) + TiltUp, + + // Tilt the camera downward (pitch) + TiltDown, + + // Rotate the camera left around the vertical axis (yaw) + PanLeft, + + // Rotate the camera right around the vertical axis (yaw) + PanRight, + + // Roll the camera counterclockwise around the forward axis (roll) + RollLeft, + + // Roll the camera clockwise around the forward axis (roll) + RollRight, + + // Reset the camera to the default state + Reset, + + EventsCount + }; + class CGimbal : virtual public core::IReferenceCounted { public: + //! Virtual event representing a manipulation + enum VirtualEventType + { + //! Move the camera in the direction of strafe vector + Strafe, + + //! Update orientation of camera by rotating around X, Y, Z axes + Rotate, + + //! Signals boolean state, for example "reset" + State + }; + + class CVirtualEvent + { + public: + using manipulation_encode_t = float32_t4; + + struct StrafeManipulation + { + float32_t3 direction = {}; + float distance = {}; + }; + + struct RotateManipulation + { + float pitch = {}, roll = {}, yaw = {}; + }; + + struct StateManipulation + { + uint32_t reset : 1; + uint32_t reserved : 31; + + StateManipulation() { memset(this, 0, sizeof(StateManipulation)); } + ~StateManipulation() {} + }; + + union ManipulationValue + { + StrafeManipulation strafe; + RotateManipulation rotation; + StateManipulation state; + + ManipulationValue() { memset(this, 0, sizeof(ManipulationValue)); } + ~ManipulationValue() {} + }; + + CVirtualEvent(VirtualEventType type, const ManipulationValue manipulation) + : m_type(type), m_manipulation(manipulation) + { + static_assert(sizeof(manipulation_encode_t) == sizeof(ManipulationValue)); + } + + // Returns the type of manipulation value + VirtualEventType getType() const + { + return m_type; + } + + // Returns manipulation value + ManipulationValue getManipulation() const + { + return m_manipulation; + } + + private: + VirtualEventType m_type; + ManipulationValue m_manipulation; + }; + CGimbal(const core::smart_refctd_ptr&& projection, const float32_t3& position, const float32_t3& lookat, const float32_t3& upVec = float32_t3(0.0f, 1.0f, 0.0f), const float32_t3& backupUpVec = float32_t3(0.5f, 1.0f, 0.0f)) : m_projection(projection), m_position(position), m_target(lookat), m_upVec(upVec), m_backupUpVec(backupUpVec), m_initialPosition(position), m_initialTarget(lookat), m_orientation(glm::quat(1.0f, 0.0f, 0.0f, 0.0f)), m_viewMatrix({}), m_isLeftHanded(isLeftHanded(m_projection->getProjectionMatrix())) { recomputeViewMatrix(); } + // TODO: ctor with core::path to json config file to load defaults + //! Start a gimbal manipulation session inline void begin() { - needsToRecomputeViewMatrix = false; + m_needsToRecomputeViewMatrix = false; + m_recordingManipulation = true; } - //! End the gimbal manipulation session, recompute matrices and check projection - inline void end() + //! Record manipulation of the gimbal, note those events are delta manipulations + void manipulate(const CVirtualEvent& virtualEvent) { - m_isLeftHanded = isLeftHanded(m_projection->getProjectionMatrix()); + if (!m_recordingManipulation) + return; // TODO: log it - // Recompute the view matrix - if(needsToRecomputeViewMatrix) - recomputeViewMatrix(); + const auto manipulation = virtualEvent.getManipulation(); - needsToRecomputeViewMatrix = false; + case VirtualEventType::Strafe: + { + strafe(manipulation.strafe.direction, manipulation.strafe.distance); + } break; + + case VirtualEventType::Rotate: + { + rotate(manipulation.rotation.pitch, manipulation.rotation.yaw, manipulation.rotation.roll); + } break; + + case VirtualEventType::State: + { + if (manipulation.state.reset) + reset(); + } break; + + default: + break; } - inline float32_t3 getLocalTarget() const + // Record change of position vector, global update + inline void setPosition(const float32_t3& position) { - return m_target - m_position; + if (!m_recordingManipulation) + return; // TODO: log it + + m_position = position; } - inline float32_t3 getForwardDirection() const + // Record change of target vector, global update + inline void setTarget(const float32_t3& target) { - return glm::normalize(getLocalTarget()); + if (!m_recordingManipulation) + return; // TODO: log it + + m_target = target; } + // Change up vector, global update + inline void setUpVector(const float32_t3& up) { m_upVec = up; } + + // Change backupUp vector, global update + inline void setBackupUpVector(const float32_t3& backupUp) { m_backupUpVec = backupUp; } + + //! End the gimbal manipulation session, recompute view matrix if required and update handedness state from projection + inline void end() + { + m_isLeftHanded = isLeftHanded(m_projection->getProjectionMatrix()); + + if (m_needsToRecomputeViewMatrix) + recomputeViewMatrix(); + + m_needsToRecomputeViewMatrix = false; + m_recordingManipulation = false; + } + + inline const float32_t3& getPosition() const { return m_position; } + inline const float32_t3& getTarget() const { return m_target; } + inline const float32_t3& getUpVector() const { return m_upVec; } + inline const float32_t3& getBackupUpVector() const { return m_backupUpVec; } + inline const float32_t3 getLocalTarget() const { return m_target - m_position; } + inline const float32_t3 getForwardDirection() const { return glm::normalize(getLocalTarget()); } + inline const projection_t* getProjection() { return m_projection.get(); } + + // TODO: getConcatenatedMatrix() + // TODO: getViewMatrix() + + private: //! Reset the gimbal to its initial position, target, and orientation inline void reset() { - m_position = m_initialPosition; + m_position = m_initialPosition; m_target = m_initialTarget; m_orientation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f); recomputeViewMatrix(); // Recompute the view matrix after resetting } - //! Move the camera in the direction of strafe (mostly left/right, up/down) - void strafe(const glm::vec3& direction, float distance) + //! Move in the direction of strafe (mostly left/right, up/down) + inline void strafe(const glm::vec3& direction, float distance) { if (distance != 0.0f) { @@ -72,12 +244,12 @@ class ICameraController : virtual public core::IReferenceCounted m_position += strafeVector; m_target += strafeVector; - needsToRecomputeViewMatrix = true; + m_needsToRecomputeViewMatrix = true; } } - //! Update orientation of camera by rotating around all XYZ axes - delta rotations in radians - void rotate(float dPitchRadians, float dYawDeltaRadians, float dRollDeltaRadians) + //! Update orientation by rotating around all XYZ axes - delta rotations in radians + inline void rotate(float dPitchRadians, float dYawDeltaRadians, float dRollDeltaRadians) { // Rotate around X (pitch) glm::quat qPitch = glm::angleAxis(dPitchRadians, glm::vec3(1.0f, 0.0f, 0.0f)); @@ -101,14 +273,9 @@ class ICameraController : virtual public core::IReferenceCounted m_target = m_position + localTargetRotated; // TODO: std::any + nice checks for deltas (radians - periodic!) - needsToRecomputeViewMatrix = true; + m_needsToRecomputeViewMatrix = true; } - // TODO: ctor with core::path to json config file to load defaults - - const projection_t* getProjection() { return m_projection.get(); } - - private: inline void recomputeViewMatrix() { auto up = getPatchedUpVector(); @@ -138,49 +305,63 @@ class ICameraController : virtual public core::IReferenceCounted inline bool isLeftHanded(const auto& projectionMatrix) { - return nbl::hlsl::determinant(projectionMatrix) < 0.f; + return hlsl::determinant(projectionMatrix) < 0.f; } - const core::smart_refctd_ptr m_projection; + core::smart_refctd_ptr m_projection; float32_t3 m_position, m_target, m_upVec, m_backupUpVec; const float32_t3 m_initialPosition, m_initialTarget; glm::quat m_orientation; float64_t4x4 m_viewMatrix; - bool m_isLeftHanded; - bool needsToRecomputeViewMatrix = false; + bool m_isLeftHanded, + m_needsToRecomputeViewMatrix = false, + m_recordingManipulation = false; }; - ICameraController(core::smart_refctd_ptr&& gimbal) - : m_gimbal(std::move(gimbal)) {} + ICameraController() : {} - void processVirtualEvent(const CVirtualCameraEvent& virtualEvent) + // controller overrides how a manipulation is done for a given camera event with a gimbal + virtual void manipulate(CGimbal* gimbal, VirtualEventType event) = 0; + + // controller can override default set of event map + virtual void initKeysToEvent() { - const auto manipulation = virtualEvent.getManipulation(); + m_keysToEvent[MoveForward] = { ui::E_KEY_CODE::EKC_W }; + m_keysToEvent[MoveBackward] = { ui::E_KEY_CODE::EKC_S }; + m_keysToEvent[MoveLeft] = { ui::E_KEY_CODE::EKC_A }; + m_keysToEvent[MoveRight] = { ui::E_KEY_CODE::EKC_D }; + m_keysToEvent[MoveUp] = { ui::E_KEY_CODE::EKC_SPACE }; + m_keysToEvent[MoveDown] = { ui::E_KEY_CODE::EKC_LEFT_SHIFT }; + m_keysToEvent[TiltUp] = { ui::E_KEY_CODE::EKC_NONE }; + m_keysToEvent[TiltDown] = { ui::E_KEY_CODE::EKC_NONE }; + m_keysToEvent[PanLeft] = { ui::E_KEY_CODE::EKC_NONE }; + m_keysToEvent[PanRight] = { ui::E_KEY_CODE::EKC_NONE }; + m_keysToEvent[RollLeft] = { ui::E_KEY_CODE::EKC_NONE }; + m_keysToEvent[RollRight] = { ui::E_KEY_CODE::EKC_NONE }; + m_keysToEvent[Reset] = { ui::E_KEY_CODE::EKC_R }; + } - case VirtualEventType::Strafe: - { - m_gimbal->strafe(manipulation.strafe.direction, manipulation.strafe.distance); - } break; - - case VirtualEventType::Rotate: - { - m_gimbal->rotate(manipulation.rotation.pitch, manipulation.rotation.yaw, manipulation.rotation.roll); - } break; + // controller can override which keys correspond to which event + void updateKeysToEvent(const std::vector& codes, VirtualEventType event) + { + m_keysToEvent[event] = std::move(codes); + } - case VirtualEventType::State: - { - if (manipulation.state.reset) - m_gimbal->reset(); - } break; +protected: + std::array, EventsCount> m_keysToEvent = {}; - default: - break; - } + // speed factors + float m_moveSpeed = 1.f, m_rotateSpeed = 1.f; + + // states signaling if keys are pressed down or not + bool m_keysDown[EventsCount] = {}; + + // durations for which the key was being held down from lastVirtualUpTimeStamp(=last "guessed" presentation time) to nextPresentationTimeStamp + double m_perActionDt[EventsCount] = {}; -private: - core::smart_refctd_ptr m_gimbal; + std::chrono::microseconds nextPresentationTimeStamp, lastVirtualUpTimeStamp; }; } // nbl::hlsl namespace From 1b9b9038427fa4f209f04907c1b62ecaa5022f91 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Wed, 23 Oct 2024 18:22:20 +0200 Subject: [PATCH 007/205] add more getters & setters to ICameraController, remove some content from CCamera, mark TODOs for tomorrow --- common/include/CCamera.hpp | 74 +----------------------- common/include/camera/ICameraControl.hpp | 7 +++ 2 files changed, 10 insertions(+), 71 deletions(-) diff --git a/common/include/CCamera.hpp b/common/include/CCamera.hpp index 54c6477a3..b2a56cead 100644 --- a/common/include/CCamera.hpp +++ b/common/include/CCamera.hpp @@ -24,62 +24,9 @@ class Camera : public ICameraController Camera() = default; ~Camera() = default; - - - - 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::core::vectorSIMDf pos = position; - nbl::core::vectorSIMDf localTarget = nbl::core::normalize(target - pos); - - // 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); - - if (leftHanded) - viewMatrix = nbl::core::matrix3x4SIMD::buildCameraLookAtMatrixLH(pos, target, up); - else - viewMatrix = nbl::core::matrix3x4SIMD::buildCameraLookAtMatrixRH(pos, target, up); - concatMatrix = nbl::core::matrix4SIMD::concatenateBFollowedByAPrecisely(projMatrix, nbl::core::matrix4SIMD(viewMatrix)); - } - - inline bool getLeftHanded() const { return leftHanded; } + /* + TODO: controller + gimbal to do all of this -> override virtual manipulate method + */ public: @@ -257,21 +204,6 @@ class Camera : public ICameraController mouseDown = false; } - -private: - nbl::core::vectorSIMDf initialPosition, initialTarget, position, target, upVector, backupUpVector; // TODO: make first 2 const + add default copy constructor - nbl::core::matrix3x4SIMD viewMatrix; - nbl::core::matrix4SIMD 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 // _CAMERA_IMPL_ \ No newline at end of file diff --git a/common/include/camera/ICameraControl.hpp b/common/include/camera/ICameraControl.hpp index 0618f90de..9dfe758f1 100644 --- a/common/include/camera/ICameraControl.hpp +++ b/common/include/camera/ICameraControl.hpp @@ -350,6 +350,13 @@ class ICameraController : virtual public core::IReferenceCounted } protected: + + inline void setMoveSpeed(const float moveSpeed) { moveSpeed = m_moveSpeed; } + inline void setRotateSpeed(const float rotateSpeed) { rotateSpeed = m_rotateSpeed; } + + inline const float getMoveSpeed() const { return m_moveSpeed; } + inline const float getRotateSpeed() const { return m_rotateSpeed; } + std::array, EventsCount> m_keysToEvent = {}; // speed factors From 71b4e76e9afc8fb63349eb6790544ac2beea8b06 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Thu, 24 Oct 2024 20:24:01 +0200 Subject: [PATCH 008/205] save work, make classes compile (but runtime crashes), mark TODOs for tomorrow - make CCamera use gimbal & treat as FPS controller --- 61_UI/main.cpp | 159 +++++++------ common/include/CCamera.hpp | 273 +++++++++-------------- common/include/CGeomtryCreatorScene.hpp | 2 +- common/include/camera/ICameraControl.hpp | 271 ++++++++++++++-------- 4 files changed, 372 insertions(+), 333 deletions(-) diff --git a/61_UI/main.cpp b/61_UI/main.cpp index 37e50805c..374719bab 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -8,6 +8,11 @@ #include "camera/ICameraControl.hpp" #include "glm/glm/ext/matrix_clip_space.hpp" // TODO: TESTING +// FPS Camera, TESTS +using camera_t = Camera; +using gimbal_t = camera_t::CGimbal; +using projection_t = camera_t::base_t::projection_t; + /* Renders scene texture to an offline framebuffer which color attachment @@ -170,28 +175,32 @@ class UISampleApp final : public examples::SimpleWindowedApplication pass.ui.manager->registerListener([this]() -> void { ImGuiIO& io = ImGui::GetIO(); - - camera.setProjectionMatrix([&]() { - static matrix4SIMD projection; + auto& projection = gimbal->getProjection()->getProjectionMatrix(); + + + // TODO CASTS + + /* if (isPerspective) - if(isLH) - projection = matrix4SIMD::buildProjectionMatrixPerspectiveFovLH(core::radians(fov), io.DisplaySize.x / io.DisplaySize.y, zNear, zFar); + { + if (isLH) + projection = glm::perspectiveLH(glm::radians(fov), io.DisplaySize.x / io.DisplaySize.y, zNear, zFar); else - projection = matrix4SIMD::buildProjectionMatrixPerspectiveFovRH(core::radians(fov), io.DisplaySize.x / io.DisplaySize.y, zNear, zFar); + projection = glm::perspectiveRH(glm::radians(fov), io.DisplaySize.x / io.DisplaySize.y, zNear, zFar); + } else { float viewHeight = viewWidth * io.DisplaySize.y / io.DisplaySize.x; - if(isLH) - projection = matrix4SIMD::buildProjectionMatrixOrthoLH(viewWidth, viewHeight, zNear, zFar); + if (isLH) + projection = glm::orthoLH(-viewWidth / 2.0f, viewWidth / 2.0f, -viewHeight / 2.0f, viewHeight / 2.0f, zNear, zFar); else - projection = matrix4SIMD::buildProjectionMatrixOrthoRH(viewWidth, viewHeight, zNear, zFar); + projection = glm::orthoRH(-viewWidth / 2.0f, viewWidth / 2.0f, -viewHeight / 2.0f, viewHeight / 2.0f, zNear, zFar); } - - return projection; - }()); + */ + } ImGuizmo::SetOrthographic(false); ImGuizmo::BeginFrame(); @@ -250,15 +259,17 @@ class UISampleApp final : public examples::SimpleWindowedApplication 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); + float32_t3 cameraPosition(cosf(camYAngle)* cosf(camXAngle)* transformParams.camDistance, sinf(camXAngle)* transformParams.camDistance, sinf(camYAngle)* cosf(camXAngle)* transformParams.camDistance); + float32_t3 cameraTarget(0.f, 0.f, 0.f); + const static float32_t3 up(0.f, 1.f, 0.f); - camera.setPosition(cameraPosition); - camera.setTarget(cameraTarget); - camera.setBackupUpVector(up); - - camera.recomputeViewMatrix(); + gimbal->begin(); + { + gimbal->setPosition(cameraPosition); + gimbal->setTarget(cameraTarget); + gimbal->setBackupUpVector(up); + } + gimbal->end(); firstFrame = false; } @@ -326,35 +337,42 @@ class UISampleApp final : public examples::SimpleWindowedApplication static struct { - core::matrix4SIMD view, projection, model; + float32_t4x4 view, projection, model; } imguizmoM16InOut; ImGuizmo::SetID(0u); - imguizmoM16InOut.view = core::transpose(matrix4SIMD(camera.getViewMatrix())); - imguizmoM16InOut.projection = core::transpose(camera.getProjectionMatrix()); - imguizmoM16InOut.model = core::transpose(core::matrix4SIMD(pass.scene->object.model)); + imguizmoM16InOut.view = transpose(gimbal->getViewMatrix()); + imguizmoM16InOut.projection = transpose(gimbal->getProjection()->getProjectionMatrix()); + imguizmoM16InOut.model = transpose(pass.scene->object.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; - EditTransform(imguizmoM16InOut.view.pointer(), imguizmoM16InOut.projection.pointer(), imguizmoM16InOut.model.pointer(), transformParams); + EditTransform(&imguizmoM16InOut.view[0][0], &imguizmoM16InOut.projection[0][0], &imguizmoM16InOut.model[0][0], transformParams); } + // to Nabla + update camera & model matrices - const auto& view = camera.getViewMatrix(); - const auto& projection = camera.getProjectionMatrix(); + const auto& view = gimbal->getViewMatrix(); + const auto& projection = gimbal->getProjection()->getProjectionMatrix(); + + + /* + + TODO!!! + // TODO: make it more nicely - const_cast(view) = core::transpose(imguizmoM16InOut.view).extractSub3x4(); // 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) + const_cast(view) = 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) camera.setProjectionMatrix(projection); // update concatanated matrix { static nbl::core::matrix3x4SIMD modelView, normal; static nbl::core::matrix4SIMD modelViewProjection; auto& hook = pass.scene->object; - hook.model = core::transpose(imguizmoM16InOut.model).extractSub3x4(); + hook.model = core::transpose(imguizmoM16InOut.model); { const auto& references = pass.scene->getResources().objects; const auto type = static_cast(gcIndex); @@ -382,6 +400,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication } } + // view matrices editor { ImGui::Begin("Matrices"); @@ -413,6 +432,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication 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, @@ -491,27 +511,18 @@ class UISampleApp final : public examples::SimpleWindowedApplication m_surface->recreateSwapchain(); m_winMgr->show(m_window.get()); oracle.reportBeginFrameRecord(); - camera.mapKeysToArrows(); /* TESTS, TODO: remove all once finished work & integrate with the example properly */ - using cube_projection_t = CCubeProjection; - using constraints_t = CCubeProjection<>::constraints_t; - using camera_control_t = ICameraController; - using gimbal_t = camera_control_t::CGimbal; - - cube_projection_t cubeProjection; // can init all at construction, but will init only first for tests - auto& projections = cubeProjection.getCubeFaceProjections(); - auto firstFaceProjection = projections.front(); - firstFaceProjection = make_smart_refctd_ptr(glm::perspectiveLH(glm::radians(fov), float(m_window->getWidth()) / float(m_window->getHeight()), zNear, zFar)); - const float32_t3 position(cosf(camYAngle)* cosf(camXAngle)* transformParams.camDistance, sinf(camXAngle)* transformParams.camDistance, sinf(camYAngle)* cosf(camXAngle)* transformParams.camDistance), target(0.f, 0.f, 0.f), up(0.f, 1.f, 0.f); - auto gimbal = make_smart_refctd_ptr(smart_refctd_ptr(firstFaceProjection), position, target, up); - auto controller = make_smart_refctd_ptr(smart_refctd_ptr(gimbal)); + auto pMatrix = glm::perspectiveLH(glm::radians(fov), float(m_window->getWidth()) / float(m_window->getHeight()), zNear, zFar); + auto projection = make_smart_refctd_ptr(); // TODO: CASTS FOR PROJ + gimbal = make_smart_refctd_ptr(smart_refctd_ptr(projection), position, target, up); + camera = make_smart_refctd_ptr(smart_refctd_ptr(gimbal)); // note we still have shared ownership, TESTS return true; } @@ -690,8 +701,8 @@ class UISampleApp final : public examples::SimpleWindowedApplication inline void update() { - camera.setMoveSpeed(moveSpeed); - camera.setRotateSpeed(rotateSpeed); + camera->setMoveSpeed(moveSpeed); + camera->setRotateSpeed(rotateSpeed); static std::chrono::microseconds previousEventTimestamp{}; @@ -716,47 +727,40 @@ class UISampleApp final : public examples::SimpleWindowedApplication std::vector mouse{}; std::vector keyboard{}; } capturedEvents; + + if (move) + camera->begin(nextPresentationTimestamp); - if (move) camera.beginInputProcessing(nextPresentationTimestamp); + mouse.consumeEvents([&](const IMouseEventChannel::range_t& events) -> void { - mouse.consumeEvents([&](const IMouseEventChannel::range_t& events) -> void + for (const auto& e : events) // here capture { - if (move) - camera.mouseProcess(events); // don't capture the events, only let camera handle them with its impl + if (e.timeStamp < previousEventTimestamp) + continue; - for (const auto& e : events) // here capture - { - if (e.timeStamp < previousEventTimestamp) - continue; - - previousEventTimestamp = e.timeStamp; - capturedEvents.mouse.emplace_back(e); + previousEventTimestamp = e.timeStamp; + capturedEvents.mouse.emplace_back(e); - if (e.type == nbl::ui::SMouseEvent::EET_SCROLL) - gcIndex = std::clamp(int16_t(gcIndex) + int16_t(core::sign(e.scrollEvent.verticalScroll)), int64_t(0), int64_t(OT_COUNT - (uint8_t)1u)); - } - }, m_logger.get()); + if (e.type == nbl::ui::SMouseEvent::EET_SCROLL) + gcIndex = std::clamp(int16_t(gcIndex) + int16_t(core::sign(e.scrollEvent.verticalScroll)), int64_t(0), int64_t(OT_COUNT - (uint8_t)1u)); + } + }, m_logger.get()); keyboard.consumeEvents([&](const IKeyboardEventChannel::range_t& events) -> void + { + for (const auto& e : events) // here capture { - if (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; + if (e.timeStamp < previousEventTimestamp) + continue; - previousEventTimestamp = e.timeStamp; - capturedEvents.keyboard.emplace_back(e); - } - }, m_logger.get()); - } - if (move) camera.endInputProcessing(nextPresentationTimestamp); + previousEventTimestamp = e.timeStamp; + capturedEvents.keyboard.emplace_back(e); + } + }, m_logger.get()); const auto cursorPosition = m_window->getCursorControl()->getPosition(); - nbl::ext::imgui::UI::SUpdateParameters params = + nbl::ext::imgui::UI::SUpdateParameters params = { .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() }, @@ -764,6 +768,12 @@ class UISampleApp final : public examples::SimpleWindowedApplication .keyboardEvents = { capturedEvents.keyboard.data(), capturedEvents.keyboard.size() } }; + if (move) + { + camera->manipulate({ .mouseEvents = params.mouseEvents, .keyboardEvents = params.keyboardEvents, }); + camera->end(nextPresentationTimestamp); + } + pass.ui.manager->update(params); } @@ -805,7 +815,8 @@ class UISampleApp final : public examples::SimpleWindowedApplication C_UI ui; } pass; - Camera camera = Camera(core::vectorSIMDf(0, 0, 0), core::vectorSIMDf(0, 0, 0), core::matrix4SIMD()); + core::smart_refctd_ptr gimbal; + core::smart_refctd_ptr camera; video::CDumbPresentationOracle oracle; 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 diff --git a/common/include/CCamera.hpp b/common/include/CCamera.hpp index b2a56cead..401c5aa4e 100644 --- a/common/include/CCamera.hpp +++ b/common/include/CCamera.hpp @@ -15,195 +15,132 @@ // FPS Camera, we will have more types soon +namespace nbl::hlsl // TODO: DIFFERENT NAMESPACE +{ + template class Camera : public ICameraController { public: - using matrix_t = T; - - Camera() = default; + using matrix_t = typename T; + using base_t = typename ICameraController; + using gimbal_t = typename base_t::CGimbal; + using gimbal_virtual_event_t = typename gimbal_t::CVirtualEvent; + using controller_virtual_event_t = typename base_t::CVirtualEvent; + + Camera(core::smart_refctd_ptr&& gimbal) + : base_t(core::smart_refctd_ptr(gimbal)) {} ~Camera() = default; - /* - TODO: controller + gimbal to do all of this -> override virtual manipulate method - */ - public: - void mouseProcess(const nbl::ui::IMouseEventChannel::range_t& events) + void manipulate(base_t::SUpdateParameters parameters) { - for (auto eventIt=events.begin(); eventIt!=events.end(); eventIt++) + auto* gimbal = base_t::m_gimbal.get(); + + auto process = [&](const std::vector& virtualEvents) -> void { - auto ev = *eventIt; + const auto forward = gimbal->getForwardDirection(); + const auto up = gimbal->getPatchedUpVector(); + const bool leftHanded = gimbal->isLeftHanded(); - 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; + // strafe vector we move along when requesting left/right movements + const auto strafeLeftRight = leftHanded ? glm::normalize(glm::cross(forward, up)) : glm::normalize(glm::cross(up, forward)); - if(ev.type == nbl::ui::SMouseEvent::EET_MOVEMENT && mouseDown) - { - nbl::core::vectorSIMDf pos = getPosition(); - nbl::core::vectorSIMDf localTarget = 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; - - localTarget.set(0,0, nbl::core::max(1.f, nbl::core::length(pos)[0]), 1.f); - - nbl::core::matrix3x4SIMD mat; - mat.setRotation(nbl::core::quaternion(relativeRotationX, relativeRotationY, 0)); - mat.transformVect(localTarget); - - setTarget(localTarget + pos); - } - } - } + constexpr auto MoveSpeedScale = 0.003f; + constexpr auto RotateSpeedScale = 0.003f; - 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; + const auto dMoveFactor = base_t::m_moveSpeed * MoveSpeedScale; + const auto dRotateFactor = base_t::m_rotateSpeed * RotateSpeedScale; - /* - * 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) - */ + // TODO: UB/LB for pitch [-88,88]!!! we are not in cosmos but handle FPS camera - for(uint32_t k = 0; k < E_CAMERA_MOVE_KEYS::ECMK_COUNT; ++k) - if(keysDown[k]) + for (const controller_virtual_event_t& ev : virtualEvents) { - auto timeDiff = std::chrono::duration_cast(nextPresentationTimeStamp - lastVirtualUpTimeStamp).count(); - assert(timeDiff >= 0); - perActionDt[k] += timeDiff; + const auto dMoveValue = ev.value * dMoveFactor; + const auto dRotateValue = ev.value * dRotateFactor; + + gimbal_virtual_event_t gimbalEvent; + + switch (ev.type) + { + case base_t::MoveForward: + { + gimbalEvent.type = gimbal_t::Strafe; + gimbalEvent.manipulation.strafe.direction = forward; + gimbalEvent.manipulation.strafe.distance = dMoveValue; + } break; + + case base_t::MoveBackward: + { + gimbalEvent.type = gimbal_t::Strafe; + gimbalEvent.manipulation.strafe.direction = -forward; + gimbalEvent.manipulation.strafe.distance = dMoveValue; + } break; + + case base_t::MoveLeft: + { + gimbalEvent.type = gimbal_t::Strafe; + gimbalEvent.manipulation.strafe.direction = -strafeLeftRight; + gimbalEvent.manipulation.strafe.distance = dMoveValue; + } break; + + case base_t::MoveRight: + { + gimbalEvent.type = gimbal_t::Strafe; + gimbalEvent.manipulation.strafe.direction = strafeLeftRight; + gimbalEvent.manipulation.strafe.distance = dMoveValue; + } break; + + case base_t::TiltUp: + { + gimbalEvent.type = gimbal_t::Rotate; + gimbalEvent.manipulation.rotation.pitch = dRotateValue; + gimbalEvent.manipulation.rotation.roll = 0.0f; + gimbalEvent.manipulation.rotation.yaw = 0.0f; + } break; + + case base_t::TiltDown: + { + gimbalEvent.type = gimbal_t::Rotate; + gimbalEvent.manipulation.rotation.pitch = -dRotateValue; + gimbalEvent.manipulation.rotation.roll = 0.0f; + gimbalEvent.manipulation.rotation.yaw = 0.0f; + } break; + + case base_t::PanLeft: + { + gimbalEvent.type = gimbal_t::Rotate; + gimbalEvent.manipulation.rotation.pitch = 0.0f; + gimbalEvent.manipulation.rotation.roll = 0.0f; + gimbalEvent.manipulation.rotation.yaw = -dRotateValue; + } break; + + case base_t::PanRight: + { + gimbalEvent.type = gimbal_t::Rotate; + gimbalEvent.manipulation.rotation.pitch = 0.0f; + gimbalEvent.manipulation.rotation.roll = 0.0f; + gimbalEvent.manipulation.rotation.yaw = dRotateValue; + } break; + + default: + continue; + } + + gimbal->manipulate(gimbalEvent); } + }; - for (auto eventIt=events.begin(); eventIt!=events.end(); eventIt++) + gimbal->begin(); { - const auto ev = *eventIt; - - // accumulate the periods for which a key was down - const auto timeDiff = std::chrono::duration_cast(nextPresentationTimeStamp - ev.timeStamp).count(); - assert(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(); - } + process(base_t::processMouse(parameters.mouseEvents)); + process(base_t::processKeyboard(parameters.keyboardEvents)); } - } - - 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; + gimbal->end(); } }; +} + #endif // _CAMERA_IMPL_ \ No newline at end of file diff --git a/common/include/CGeomtryCreatorScene.hpp b/common/include/CGeomtryCreatorScene.hpp index ab90f59b9..547c0aaf7 100644 --- a/common/include/CGeomtryCreatorScene.hpp +++ b/common/include/CGeomtryCreatorScene.hpp @@ -1098,7 +1098,7 @@ class ResourceBuilder struct ObjectDrawHookCpu { - nbl::core::matrix3x4SIMD model; + nbl::hlsl::float32_t4x4 model; nbl::asset::SBasicViewParameters viewParameters; ObjectMeta meta; }; diff --git a/common/include/camera/ICameraControl.hpp b/common/include/camera/ICameraControl.hpp index 9dfe758f1..c52558438 100644 --- a/common/include/camera/ICameraControl.hpp +++ b/common/include/camera/ICameraControl.hpp @@ -60,10 +60,19 @@ class ICameraController : virtual public core::IReferenceCounted EventsCount }; + //! Virtual event representing a manipulation + struct CVirtualEvent + { + using manipulation_encode_t = float64_t; + + VirtualEventType type; + manipulation_encode_t value; + }; + class CGimbal : virtual public core::IReferenceCounted { public: - //! Virtual event representing a manipulation + //! Virtual event representing a combined gimbal manipulation enum VirtualEventType { //! Move the camera in the direction of strafe vector @@ -111,31 +120,20 @@ class ICameraController : virtual public core::IReferenceCounted ~ManipulationValue() {} }; - CVirtualEvent(VirtualEventType type, const ManipulationValue manipulation) - : m_type(type), m_manipulation(manipulation) - { - static_assert(sizeof(manipulation_encode_t) == sizeof(ManipulationValue)); - } - - // Returns the type of manipulation value - VirtualEventType getType() const - { - return m_type; - } + CVirtualEvent() {} - // Returns manipulation value - ManipulationValue getManipulation() const + CVirtualEvent(VirtualEventType _type, const ManipulationValue _manipulation) + : type(_type), manipulation(_manipulation) { - return m_manipulation; + static_assert(sizeof(manipulation_encode_t) == sizeof(ManipulationValue)); } - private: - VirtualEventType m_type; - ManipulationValue m_manipulation; + VirtualEventType type; + ManipulationValue manipulation; }; CGimbal(const core::smart_refctd_ptr&& projection, const float32_t3& position, const float32_t3& lookat, const float32_t3& upVec = float32_t3(0.0f, 1.0f, 0.0f), const float32_t3& backupUpVec = float32_t3(0.5f, 1.0f, 0.0f)) - : m_projection(projection), m_position(position), m_target(lookat), m_upVec(upVec), m_backupUpVec(backupUpVec), m_initialPosition(position), m_initialTarget(lookat), m_orientation(glm::quat(1.0f, 0.0f, 0.0f, 0.0f)), m_viewMatrix({}), m_isLeftHanded(isLeftHanded(m_projection->getProjectionMatrix())) + : m_projection(projection), m_position(position), m_target(lookat), m_upVec(upVec), m_backupUpVec(backupUpVec), m_initialPosition(position), m_initialTarget(lookat), m_orientation(glm::quat(1.0f, 0.0f, 0.0f, 0.0f)), m_viewMatrix({}), m_isLeftHanded(checkIfLeftHanded(m_projection->getProjectionMatrix())) { recomputeViewMatrix(); } @@ -155,26 +153,29 @@ class ICameraController : virtual public core::IReferenceCounted if (!m_recordingManipulation) return; // TODO: log it - const auto manipulation = virtualEvent.getManipulation(); + const auto& manipulation = virtualEvent.manipulation; - case VirtualEventType::Strafe: + switch (virtualEvent.type) { - strafe(manipulation.strafe.direction, manipulation.strafe.distance); - } break; - - case VirtualEventType::Rotate: - { - rotate(manipulation.rotation.pitch, manipulation.rotation.yaw, manipulation.rotation.roll); - } break; - - case VirtualEventType::State: - { - if (manipulation.state.reset) - reset(); - } break; - - default: - break; + case VirtualEventType::Strafe: + { + strafe(manipulation.strafe.direction, manipulation.strafe.distance); + } break; + + case VirtualEventType::Rotate: + { + rotate(manipulation.rotation.pitch, manipulation.rotation.yaw, manipulation.rotation.roll); + } break; + + case VirtualEventType::State: + { + if (manipulation.state.reset) + reset(); + } break; + + default: + break; + } } // Record change of position vector, global update @@ -204,7 +205,7 @@ class ICameraController : virtual public core::IReferenceCounted //! End the gimbal manipulation session, recompute view matrix if required and update handedness state from projection inline void end() { - m_isLeftHanded = isLeftHanded(m_projection->getProjectionMatrix()); + m_isLeftHanded = checkIfLeftHanded(m_projection->getProjectionMatrix()); if (m_needsToRecomputeViewMatrix) recomputeViewMatrix(); @@ -213,13 +214,32 @@ class ICameraController : virtual public core::IReferenceCounted m_recordingManipulation = false; } + inline projection_t* getProjection() { return m_projection.get(); } inline const float32_t3& getPosition() const { return m_position; } inline const float32_t3& getTarget() const { return m_target; } inline const float32_t3& getUpVector() const { return m_upVec; } inline const float32_t3& getBackupUpVector() const { return m_backupUpVec; } inline const float32_t3 getLocalTarget() const { return m_target - m_position; } inline const float32_t3 getForwardDirection() const { return glm::normalize(getLocalTarget()); } - inline const projection_t* getProjection() { return m_projection.get(); } + inline const float32_t4x4& getViewMatrix() const { return m_viewMatrix; } + inline bool isLeftHanded() { return m_isLeftHanded; } + + inline float32_t3 getPatchedUpVector() + { + // if up vector and vector to the target are the same we patch the up vector + auto up = glm::normalize(m_upVec); + + const auto localTarget = getForwardDirection(); + const auto cross = glm::cross(localTarget, up); + + // we compute squared length but for checking if the len is 0 it doesnt matter + const bool upVectorZeroLength = glm::dot(cross, cross) == 0.f; + + if (upVectorZeroLength) + up = glm::normalize(m_backupUpVec); + + return up; + } // TODO: getConcatenatedMatrix() // TODO: getViewMatrix() @@ -257,7 +277,7 @@ class ICameraController : virtual public core::IReferenceCounted // Rotate around Y (yaw) glm::quat qYaw = glm::angleAxis(dYawDeltaRadians, glm::vec3(0.0f, 1.0f, 0.0f)); - // Rotate around Z (roll) // TODO: handness!! + // Rotate around Z (roll) glm::quat qRoll = glm::angleAxis(dRollDeltaRadians, glm::vec3(0.0f, 0.0f, 1.0f)); // Combine the new rotations with the current orientation @@ -280,30 +300,17 @@ class ICameraController : virtual public core::IReferenceCounted { auto up = getPatchedUpVector(); + // TODO!!!! CASTS + + /* if (m_isLeftHanded) m_viewMatrix = glm::lookAtLH(m_position, m_target, up); else m_viewMatrix = glm::lookAtRH(m_position, m_target, up); + */ } - inline float32_t3 getPatchedUpVector() - { - // if up vector and vector to the target are the same we patch the up vector - auto up = glm::normalize(m_upVec); - - const auto localTarget = getForwardDirection(); - const auto cross = glm::cross(localTarget, up); - - // we compute squared length but for checking if the len is 0 it doesnt matter - const bool upVectorZeroLength = glm::dot(cross, cross) == 0.f; - - if (upVectorZeroLength) - up = glm::normalize(m_backupUpVec); - - return up; - } - - inline bool isLeftHanded(const auto& projectionMatrix) + inline bool checkIfLeftHanded(const auto& projectionMatrix) { return hlsl::determinant(projectionMatrix) < 0.f; } @@ -313,35 +320,24 @@ class ICameraController : virtual public core::IReferenceCounted const float32_t3 m_initialPosition, m_initialTarget; glm::quat m_orientation; - float64_t4x4 m_viewMatrix; + float32_t4x4 m_viewMatrix; bool m_isLeftHanded, m_needsToRecomputeViewMatrix = false, m_recordingManipulation = false; }; - ICameraController() : {} + struct SUpdateParameters + { + //! Nabla mouse events you want to be handled with a controller + std::span mouseEvents = {}; - // controller overrides how a manipulation is done for a given camera event with a gimbal - virtual void manipulate(CGimbal* gimbal, VirtualEventType event) = 0; + //! Nabla keyboard events you want to be handled with a controller + std::span keyboardEvents = {}; + }; - // controller can override default set of event map - virtual void initKeysToEvent() - { - m_keysToEvent[MoveForward] = { ui::E_KEY_CODE::EKC_W }; - m_keysToEvent[MoveBackward] = { ui::E_KEY_CODE::EKC_S }; - m_keysToEvent[MoveLeft] = { ui::E_KEY_CODE::EKC_A }; - m_keysToEvent[MoveRight] = { ui::E_KEY_CODE::EKC_D }; - m_keysToEvent[MoveUp] = { ui::E_KEY_CODE::EKC_SPACE }; - m_keysToEvent[MoveDown] = { ui::E_KEY_CODE::EKC_LEFT_SHIFT }; - m_keysToEvent[TiltUp] = { ui::E_KEY_CODE::EKC_NONE }; - m_keysToEvent[TiltDown] = { ui::E_KEY_CODE::EKC_NONE }; - m_keysToEvent[PanLeft] = { ui::E_KEY_CODE::EKC_NONE }; - m_keysToEvent[PanRight] = { ui::E_KEY_CODE::EKC_NONE }; - m_keysToEvent[RollLeft] = { ui::E_KEY_CODE::EKC_NONE }; - m_keysToEvent[RollRight] = { ui::E_KEY_CODE::EKC_NONE }; - m_keysToEvent[Reset] = { ui::E_KEY_CODE::EKC_R }; - } + ICameraController(core::smart_refctd_ptr&& gimbal) + : m_gimbal(core::smart_refctd_ptr(gimbal)) {} // controller can override which keys correspond to which event void updateKeysToEvent(const std::vector& codes, VirtualEventType event) @@ -349,26 +345,121 @@ class ICameraController : virtual public core::IReferenceCounted m_keysToEvent[event] = std::move(codes); } -protected: + virtual void begin(std::chrono::microseconds nextPresentationTimeStamp) + { + m_nextPresentationTimeStamp = nextPresentationTimeStamp; + return; + } - inline void setMoveSpeed(const float moveSpeed) { moveSpeed = m_moveSpeed; } - inline void setRotateSpeed(const float rotateSpeed) { rotateSpeed = m_rotateSpeed; } + virtual void manipulate(SUpdateParameters parameters) = 0; + + void end(std::chrono::microseconds nextPresentationTimeStamp) + { + m_lastVirtualUpTimeStamp = nextPresentationTimeStamp; + } + + inline void setMoveSpeed(const float moveSpeed) { m_moveSpeed = moveSpeed; } + inline void setRotateSpeed(const float rotateSpeed) { m_rotateSpeed = rotateSpeed; } inline const float getMoveSpeed() const { return m_moveSpeed; } inline const float getRotateSpeed() const { return m_rotateSpeed; } - std::array, EventsCount> m_keysToEvent = {}; +protected: + // process keyboard to generate virtual manipulation events + std::vector processKeyboard(std::span events) + { + std::vector virtualEvents; - // speed factors - float m_moveSpeed = 1.f, m_rotateSpeed = 1.f; + for (const auto& ev : events) + { + constexpr auto NblVirtualKeys = std::to_array({ MoveForward, MoveBackward, MoveLeft, MoveRight, MoveUp, MoveDown, TiltUp, TiltDown, PanLeft, PanRight, RollLeft, RollRight, Reset }); + static_assert(NblVirtualKeys.size() == EventsCount); - // states signaling if keys are pressed down or not - bool m_keysDown[EventsCount] = {}; + for (const auto virtualKey : NblVirtualKeys) + { + const auto code = m_keysToEvent[virtualKey]; + + if (ev.keyCode == code) + { + if (code == ui::EKC_NONE) + continue; + + const auto dt = std::chrono::duration_cast(m_nextPresentationTimeStamp - ev.timeStamp).count(); + assert(dt >= 0); + + if (ev.action == nbl::ui::SKeyboardEvent::ECA_PRESSED && !m_keysDown[virtualKey]) + { + m_keysDown[virtualKey] = true; + virtualEvents.emplace_back(CVirtualEvent{ virtualKey, static_cast(dt) }); + } + else if (ev.action == nbl::ui::SKeyboardEvent::ECA_RELEASED) + { + m_keysDown[virtualKey] = false; + } + } + } + } + + return virtualEvents; + } + + /* + // [OPTIONAL]: process mouse to generate virtual manipulation events + // note: + // - all manipulations *may* be done with keyboard keys, it means you can have a pad/touch pad for which keys could be bound and skip the mouse + // - default implementation which is FPS camera-like controller doesn't perform roll rotation which is around Z axis! If you want more complex manipulations then override the method + // - it doesn't make the manipulation itself! It only process your mouse events you got from OS & accumulate data values to create manipulation virtual events! + */ + virtual std::vector processMouse(std::span events) const + { + // accumulate total pitch & yaw delta from mouse movement events + const auto [dTotalPitch, dTotalYaw] = [&]() + { + double dPitch = {}, dYaw = {}; + + for (const auto& ev : events) + if (ev.type == nbl::ui::SMouseEvent::EET_MOVEMENT) + { + dYaw += ev.movementEvent.relativeMovementX; // (yaw) + dPitch -= ev.movementEvent.relativeMovementY; // (pitch) + } + + return std::make_tuple(dPitch, dYaw); + }(); - // durations for which the key was being held down from lastVirtualUpTimeStamp(=last "guessed" presentation time) to nextPresentationTimeStamp - double m_perActionDt[EventsCount] = {}; + CVirtualEvent pitch; + pitch.type = (pitch.value = dTotalPitch) > 0.f ? TiltUp : TiltDown; + + CVirtualEvent yaw; + yaw.type = (yaw.value = dTotalYaw) > 0.f ? PanRight : PanLeft; + + return { pitch, yaw }; + } + + // controller can override default set of event map + virtual void initKeysToEvent() + { + m_keysToEvent[MoveForward] = ui::E_KEY_CODE::EKC_W ; + m_keysToEvent[MoveBackward] = ui::E_KEY_CODE::EKC_S ; + m_keysToEvent[MoveLeft] = ui::E_KEY_CODE::EKC_A ; + m_keysToEvent[MoveRight] = ui::E_KEY_CODE::EKC_D ; + m_keysToEvent[MoveUp] = ui::E_KEY_CODE::EKC_SPACE ; + m_keysToEvent[MoveDown] = ui::E_KEY_CODE::EKC_LEFT_SHIFT ; + m_keysToEvent[TiltUp] = ui::E_KEY_CODE::EKC_NONE ; + m_keysToEvent[TiltDown] = ui::E_KEY_CODE::EKC_NONE ; + m_keysToEvent[PanLeft] = ui::E_KEY_CODE::EKC_NONE ; + m_keysToEvent[PanRight] = ui::E_KEY_CODE::EKC_NONE ; + m_keysToEvent[RollLeft] = ui::E_KEY_CODE::EKC_NONE ; + m_keysToEvent[RollRight] = ui::E_KEY_CODE::EKC_NONE ; + m_keysToEvent[Reset] = ui::E_KEY_CODE::EKC_R ; + } + + core::smart_refctd_ptr m_gimbal; + std::array m_keysToEvent = {}; + float m_moveSpeed = 1.f, m_rotateSpeed = 1.f; + bool m_keysDown[EventsCount] = {}; - std::chrono::microseconds nextPresentationTimeStamp, lastVirtualUpTimeStamp; + std::chrono::microseconds m_nextPresentationTimeStamp, m_lastVirtualUpTimeStamp; }; } // nbl::hlsl namespace From 31813e6a473b95b8209f5b9d3f29db1ec7f3788a Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Fri, 25 Oct 2024 14:13:35 +0200 Subject: [PATCH 009/205] create ICamera.hpp interface for all types of cameraz, update IProjection with isLeftHanded method, update sources & some of matrices to HLSL - the pure virtual controller manipulate method now takes a gimbal + span of virtual events --- 61_UI/main.cpp | 71 +++---- common/include/CCamera.hpp | 233 +++++++++++------------ common/include/CGeomtryCreatorScene.hpp | 2 +- common/include/ICamera.hpp | 40 ++++ common/include/camera/ICameraControl.hpp | 72 +++---- common/include/camera/IProjection.hpp | 5 + 6 files changed, 222 insertions(+), 201 deletions(-) create mode 100644 common/include/ICamera.hpp diff --git a/61_UI/main.cpp b/61_UI/main.cpp index 374719bab..c78059ae5 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -7,9 +7,11 @@ #include "camera/CCubeProjection.hpp" #include "camera/ICameraControl.hpp" #include "glm/glm/ext/matrix_clip_space.hpp" // TODO: TESTING +#include "nbl/builtin/hlsl/matrix_utils/transformation_matrix_utils.hlsl" // FPS Camera, TESTS -using camera_t = Camera; +using projection_matrix_t = float32_t4x4; +using camera_t = Camera; using gimbal_t = camera_t::CGimbal; using projection_t = camera_t::base_t::projection_t; @@ -177,29 +179,23 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGuiIO& io = ImGui::GetIO(); { auto& projection = gimbal->getProjection()->getProjectionMatrix(); - - - // TODO CASTS - - /* if (isPerspective) { if (isLH) - projection = glm::perspectiveLH(glm::radians(fov), io.DisplaySize.x / io.DisplaySize.y, zNear, zFar); + projection = projection_t::value_t(glm::perspectiveLH(glm::radians(fov), io.DisplaySize.x / io.DisplaySize.y, zNear, zFar)); else - projection = glm::perspectiveRH(glm::radians(fov), io.DisplaySize.x / io.DisplaySize.y, zNear, zFar); + projection = projection_t::value_t(glm::perspectiveRH(glm::radians(fov), io.DisplaySize.x / io.DisplaySize.y, zNear, zFar)); } else { float viewHeight = viewWidth * io.DisplaySize.y / io.DisplaySize.x; if (isLH) - projection = glm::orthoLH(-viewWidth / 2.0f, viewWidth / 2.0f, -viewHeight / 2.0f, viewHeight / 2.0f, zNear, zFar); + projection = projection_t::value_t(glm::orthoLH(-viewWidth / 2.0f, viewWidth / 2.0f, -viewHeight / 2.0f, viewHeight / 2.0f, zNear, zFar)); else - projection = glm::orthoRH(-viewWidth / 2.0f, viewWidth / 2.0f, -viewHeight / 2.0f, viewHeight / 2.0f, zNear, zFar); + projection = projection_t::value_t(glm::orthoRH(-viewWidth / 2.0f, viewWidth / 2.0f, -viewHeight / 2.0f, viewHeight / 2.0f, zNear, zFar)); } - */ } ImGuizmo::SetOrthographic(false); @@ -335,14 +331,22 @@ class UISampleApp final : public examples::SimpleWindowedApplication * note it also modifies input view matrix but projection matrix is immutable */ + + + /* + + TODODOD + + + + static struct { float32_t4x4 view, projection, model; } imguizmoM16InOut; ImGuizmo::SetID(0u); - - imguizmoM16InOut.view = transpose(gimbal->getViewMatrix()); + imguizmoM16InOut.view = transpose(getMatrix3x4As4x4(gimbal->getViewMatrix())); imguizmoM16InOut.projection = transpose(gimbal->getProjection()->getProjectionMatrix()); imguizmoM16InOut.model = transpose(pass.scene->object.model); { @@ -353,26 +357,19 @@ class UISampleApp final : public examples::SimpleWindowedApplication EditTransform(&imguizmoM16InOut.view[0][0], &imguizmoM16InOut.projection[0][0], &imguizmoM16InOut.model[0][0], transformParams); } - // to Nabla + update camera & model matrices const auto& view = gimbal->getViewMatrix(); const auto& projection = gimbal->getProjection()->getProjectionMatrix(); - - - /* - - TODO!!! - - + // TODO: make it more nicely - const_cast(view) = 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) - camera.setProjectionMatrix(projection); // update concatanated matrix + const_cast(view) = float32_t3x4(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) + //camera.setProjectionMatrix(projection); // update concatanated matrix { - static nbl::core::matrix3x4SIMD modelView, normal; - static nbl::core::matrix4SIMD modelViewProjection; + static float32_t3x4 modelView, normal; + static float32_t4x4 modelViewProjection; auto& hook = pass.scene->object; - hook.model = core::transpose(imguizmoM16InOut.model); + hook.model = core::transpose(float32_t3x4(imguizmoM16InOut.model)); { const auto& references = pass.scene->getResources().objects; const auto type = static_cast(gcIndex); @@ -384,8 +381,10 @@ class UISampleApp final : public examples::SimpleWindowedApplication auto& ubo = hook.viewParameters; - modelView = nbl::core::concatenateBFollowedByA(view, hook.model); - modelView.getSub3x3InverseTranspose(normal); + modelView = concatenateBFollowedByA(view, hook.model); + + // TODO + //modelView.getSub3x3InverseTranspose(normal); modelViewProjection = nbl::core::concatenateBFollowedByA(camera.getConcatenatedMatrix(), hook.model); memcpy(ubo.MVP, modelViewProjection.pointer(), sizeof(ubo.MVP)); @@ -432,7 +431,8 @@ class UISampleApp final : public examples::SimpleWindowedApplication 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, @@ -522,7 +522,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication auto pMatrix = glm::perspectiveLH(glm::radians(fov), float(m_window->getWidth()) / float(m_window->getHeight()), zNear, zFar); auto projection = make_smart_refctd_ptr(); // TODO: CASTS FOR PROJ gimbal = make_smart_refctd_ptr(smart_refctd_ptr(projection), position, target, up); - camera = make_smart_refctd_ptr(smart_refctd_ptr(gimbal)); // note we still have shared ownership, TESTS + camera = make_smart_refctd_ptr(); return true; } @@ -770,7 +770,14 @@ class UISampleApp final : public examples::SimpleWindowedApplication if (move) { - camera->manipulate({ .mouseEvents = params.mouseEvents, .keyboardEvents = params.keyboardEvents, }); + const auto virtualMouseEvents = camera->processMouse(params.mouseEvents); + const auto virtualKeyboardEvents = camera->processMouse(params.mouseEvents); + + gimbal->begin(); + camera->manipulate(gimbal.get(), { virtualMouseEvents.data(), virtualMouseEvents.size()}); + camera->manipulate(gimbal.get(), { virtualKeyboardEvents.data(), virtualKeyboardEvents.size()}); + gimbal->end(); + camera->end(nextPresentationTimestamp); } @@ -816,7 +823,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication } pass; core::smart_refctd_ptr gimbal; - core::smart_refctd_ptr camera; + core::smart_refctd_ptr> camera; video::CDumbPresentationOracle oracle; 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 diff --git a/common/include/CCamera.hpp b/common/include/CCamera.hpp index 401c5aa4e..1c306c651 100644 --- a/common/include/CCamera.hpp +++ b/common/include/CCamera.hpp @@ -2,145 +2,126 @@ // This file is part of the "Nabla Engine". // For conditions of distribution and use, see copyright notice in nabla.h -#ifndef _CAMERA_IMPL_ -#define _CAMERA_IMPL_ +#ifndef _C_CAMERA_HPP_ +#define _C_CAMERA_HPP_ -#include -#include -#include -#include -#include - -#include "camera/ICameraControl.hpp" - -// FPS Camera, we will have more types soon +#include "ICamera.hpp" namespace nbl::hlsl // TODO: DIFFERENT NAMESPACE { +// FPS Camera template -class Camera : public ICameraController +class Camera : public ICamera { public: - using matrix_t = typename T; - using base_t = typename ICameraController; - using gimbal_t = typename base_t::CGimbal; - using gimbal_virtual_event_t = typename gimbal_t::CVirtualEvent; - using controller_virtual_event_t = typename base_t::CVirtualEvent; - - Camera(core::smart_refctd_ptr&& gimbal) - : base_t(core::smart_refctd_ptr(gimbal)) {} - ~Camera() = default; + using base_t = ICamera; + using traits_t = typename base_t::Traits; -public: + Camera() : base_t() {} + ~Camera() = default; - void manipulate(base_t::SUpdateParameters parameters) + virtual void manipulate(traits_t::gimbal_t* gimbal, std::span virtualEvents) override { - auto* gimbal = base_t::m_gimbal.get(); - - auto process = [&](const std::vector& virtualEvents) -> void - { - const auto forward = gimbal->getForwardDirection(); - const auto up = gimbal->getPatchedUpVector(); - const bool leftHanded = gimbal->isLeftHanded(); - - // strafe vector we move along when requesting left/right movements - const auto strafeLeftRight = leftHanded ? glm::normalize(glm::cross(forward, up)) : glm::normalize(glm::cross(up, forward)); - - constexpr auto MoveSpeedScale = 0.003f; - constexpr auto RotateSpeedScale = 0.003f; - - const auto dMoveFactor = base_t::m_moveSpeed * MoveSpeedScale; - const auto dRotateFactor = base_t::m_rotateSpeed * RotateSpeedScale; - - // TODO: UB/LB for pitch [-88,88]!!! we are not in cosmos but handle FPS camera - - for (const controller_virtual_event_t& ev : virtualEvents) - { - const auto dMoveValue = ev.value * dMoveFactor; - const auto dRotateValue = ev.value * dRotateFactor; - - gimbal_virtual_event_t gimbalEvent; - - switch (ev.type) - { - case base_t::MoveForward: - { - gimbalEvent.type = gimbal_t::Strafe; - gimbalEvent.manipulation.strafe.direction = forward; - gimbalEvent.manipulation.strafe.distance = dMoveValue; - } break; - - case base_t::MoveBackward: - { - gimbalEvent.type = gimbal_t::Strafe; - gimbalEvent.manipulation.strafe.direction = -forward; - gimbalEvent.manipulation.strafe.distance = dMoveValue; - } break; - - case base_t::MoveLeft: - { - gimbalEvent.type = gimbal_t::Strafe; - gimbalEvent.manipulation.strafe.direction = -strafeLeftRight; - gimbalEvent.manipulation.strafe.distance = dMoveValue; - } break; - - case base_t::MoveRight: - { - gimbalEvent.type = gimbal_t::Strafe; - gimbalEvent.manipulation.strafe.direction = strafeLeftRight; - gimbalEvent.manipulation.strafe.distance = dMoveValue; - } break; - - case base_t::TiltUp: - { - gimbalEvent.type = gimbal_t::Rotate; - gimbalEvent.manipulation.rotation.pitch = dRotateValue; - gimbalEvent.manipulation.rotation.roll = 0.0f; - gimbalEvent.manipulation.rotation.yaw = 0.0f; - } break; - - case base_t::TiltDown: - { - gimbalEvent.type = gimbal_t::Rotate; - gimbalEvent.manipulation.rotation.pitch = -dRotateValue; - gimbalEvent.manipulation.rotation.roll = 0.0f; - gimbalEvent.manipulation.rotation.yaw = 0.0f; - } break; - - case base_t::PanLeft: - { - gimbalEvent.type = gimbal_t::Rotate; - gimbalEvent.manipulation.rotation.pitch = 0.0f; - gimbalEvent.manipulation.rotation.roll = 0.0f; - gimbalEvent.manipulation.rotation.yaw = -dRotateValue; - } break; - - case base_t::PanRight: - { - gimbalEvent.type = gimbal_t::Rotate; - gimbalEvent.manipulation.rotation.pitch = 0.0f; - gimbalEvent.manipulation.rotation.roll = 0.0f; - gimbalEvent.manipulation.rotation.yaw = dRotateValue; - } break; - - default: - continue; - } - - gimbal->manipulate(gimbalEvent); - } - }; - - gimbal->begin(); - { - process(base_t::processMouse(parameters.mouseEvents)); - process(base_t::processKeyboard(parameters.keyboardEvents)); - } - gimbal->end(); + if (!gimbal) + return; // TODO: LOG + + if (!gimbal->isRecordingManipulation()) + return; // TODO: LOG + + const auto forward = gimbal->getForwardDirection(); + const auto up = gimbal->getPatchedUpVector(); + const bool leftHanded = gimbal->isLeftHanded(); + + // strafe vector we move along when requesting left/right movements + const auto strafeLeftRight = leftHanded ? glm::normalize(glm::cross(forward, up)) : glm::normalize(glm::cross(up, forward)); + + constexpr auto MoveSpeedScale = 0.003f; + constexpr auto RotateSpeedScale = 0.003f; + + const auto dMoveFactor = traits_t::controller_t::m_moveSpeed * MoveSpeedScale; + const auto dRotateFactor = traits_t::controller_t::m_rotateSpeed * RotateSpeedScale; + + // TODO: UB/LB for pitch [-88,88]!!! we are not in cosmos but handle FPS camera in default case + + for (const traits_t::controller_virtual_event_t& ev : virtualEvents) + { + const auto dMoveValue = ev.value * dMoveFactor; + const auto dRotateValue = ev.value * dRotateFactor; + + typename traits_t::gimbal_virtual_event_t gimbalEvent; + + switch (ev.type) + { + case traits_t::controller_t::MoveForward: + { + gimbalEvent.type = traits_t::gimbal_t::Strafe; + gimbalEvent.manipulation.strafe.direction = forward; + gimbalEvent.manipulation.strafe.distance = dMoveValue; + } break; + + case traits_t::controller_t::MoveBackward: + { + gimbalEvent.type = traits_t::gimbal_t::Strafe; + gimbalEvent.manipulation.strafe.direction = -forward; + gimbalEvent.manipulation.strafe.distance = dMoveValue; + } break; + + case traits_t::controller_t::MoveLeft: + { + gimbalEvent.type = traits_t::gimbal_t::Strafe; + gimbalEvent.manipulation.strafe.direction = -strafeLeftRight; + gimbalEvent.manipulation.strafe.distance = dMoveValue; + } break; + + case traits_t::controller_t::MoveRight: + { + gimbalEvent.type = traits_t::gimbal_t::Strafe; + gimbalEvent.manipulation.strafe.direction = strafeLeftRight; + gimbalEvent.manipulation.strafe.distance = dMoveValue; + } break; + + case traits_t::controller_t::TiltUp: + { + gimbalEvent.type = traits_t::gimbal_t::Rotate; + gimbalEvent.manipulation.rotation.pitch = dRotateValue; + gimbalEvent.manipulation.rotation.roll = 0.0f; + gimbalEvent.manipulation.rotation.yaw = 0.0f; + } break; + + case traits_t::controller_t::TiltDown: + { + gimbalEvent.type = traits_t::gimbal_t::Rotate; + gimbalEvent.manipulation.rotation.pitch = -dRotateValue; + gimbalEvent.manipulation.rotation.roll = 0.0f; + gimbalEvent.manipulation.rotation.yaw = 0.0f; + } break; + + case traits_t::controller_t::PanLeft: + { + gimbalEvent.type = traits_t::gimbal_t::Rotate; + gimbalEvent.manipulation.rotation.pitch = 0.0f; + gimbalEvent.manipulation.rotation.roll = 0.0f; + gimbalEvent.manipulation.rotation.yaw = -dRotateValue; + } break; + + case traits_t::controller_t::PanRight: + { + gimbalEvent.type = traits_t::gimbal_t::Rotate; + gimbalEvent.manipulation.rotation.pitch = 0.0f; + gimbalEvent.manipulation.rotation.roll = 0.0f; + gimbalEvent.manipulation.rotation.yaw = dRotateValue; + } break; + + default: + continue; + } + + gimbal->manipulate(gimbalEvent); + } } }; } -#endif // _CAMERA_IMPL_ \ No newline at end of file +#endif // _C_CAMERA_HPP_ \ No newline at end of file diff --git a/common/include/CGeomtryCreatorScene.hpp b/common/include/CGeomtryCreatorScene.hpp index 547c0aaf7..7b6492d11 100644 --- a/common/include/CGeomtryCreatorScene.hpp +++ b/common/include/CGeomtryCreatorScene.hpp @@ -1098,7 +1098,7 @@ class ResourceBuilder struct ObjectDrawHookCpu { - nbl::hlsl::float32_t4x4 model; + nbl::hlsl::float32_t3x4 model; nbl::asset::SBasicViewParameters viewParameters; ObjectMeta meta; }; diff --git a/common/include/ICamera.hpp b/common/include/ICamera.hpp new file mode 100644 index 000000000..22245c1e6 --- /dev/null +++ b/common/include/ICamera.hpp @@ -0,0 +1,40 @@ +// 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 _I_CAMERA_HPP_ +#define _I_CAMERA_HPP_ + +#include +#include +#include +#include +#include + +#include "camera/ICameraControl.hpp" + +namespace nbl::hlsl // TODO: DIFFERENT NAMESPACE +{ + +template +class ICamera : public ICameraController +{ +public: + using base_t = typename ICameraController; + + struct Traits + { + using controller_t = base_t; + using projection_t = typename controller_t::projection_t; + using gimbal_t = typename controller_t::CGimbal; + using gimbal_virtual_event_t = typename gimbal_t::CVirtualEvent; + using controller_virtual_event_t = typename controller_t::CVirtualEvent; + }; + + ICamera() : base_t() {} + ~ICamera() = default; +}; + +} + +#endif // _I_CAMERA_HPP_ \ No newline at end of file diff --git a/common/include/camera/ICameraControl.hpp b/common/include/camera/ICameraControl.hpp index c52558438..bcc1b6d54 100644 --- a/common/include/camera/ICameraControl.hpp +++ b/common/include/camera/ICameraControl.hpp @@ -2,6 +2,7 @@ #define _NBL_I_CAMERA_CONTROLLER_HPP_ #include "IProjection.hpp" +#include "../ICamera.hpp" #include "CVirtualCameraEvent.hpp" #include "glm/glm/ext/matrix_transform.hpp" // TODO: TEMPORARY!!! whatever used will be moved to cpp #include "glm/glm/gtc/quaternion.hpp" @@ -133,7 +134,7 @@ class ICameraController : virtual public core::IReferenceCounted }; CGimbal(const core::smart_refctd_ptr&& projection, const float32_t3& position, const float32_t3& lookat, const float32_t3& upVec = float32_t3(0.0f, 1.0f, 0.0f), const float32_t3& backupUpVec = float32_t3(0.5f, 1.0f, 0.0f)) - : m_projection(projection), m_position(position), m_target(lookat), m_upVec(upVec), m_backupUpVec(backupUpVec), m_initialPosition(position), m_initialTarget(lookat), m_orientation(glm::quat(1.0f, 0.0f, 0.0f, 0.0f)), m_viewMatrix({}), m_isLeftHanded(checkIfLeftHanded(m_projection->getProjectionMatrix())) + : m_projection(projection), m_position(position), m_target(lookat), m_upVec(upVec), m_backupUpVec(backupUpVec), m_initialPosition(position), m_initialTarget(lookat), m_orientation(glm::quat(1.0f, 0.0f, 0.0f, 0.0f)), m_viewMatrix({}) { recomputeViewMatrix(); } @@ -205,8 +206,6 @@ class ICameraController : virtual public core::IReferenceCounted //! End the gimbal manipulation session, recompute view matrix if required and update handedness state from projection inline void end() { - m_isLeftHanded = checkIfLeftHanded(m_projection->getProjectionMatrix()); - if (m_needsToRecomputeViewMatrix) recomputeViewMatrix(); @@ -214,6 +213,7 @@ class ICameraController : virtual public core::IReferenceCounted m_recordingManipulation = false; } + inline bool isRecordingManipulation() { return m_recordingManipulation; } inline projection_t* getProjection() { return m_projection.get(); } inline const float32_t3& getPosition() const { return m_position; } inline const float32_t3& getTarget() const { return m_target; } @@ -221,7 +221,7 @@ class ICameraController : virtual public core::IReferenceCounted inline const float32_t3& getBackupUpVector() const { return m_backupUpVec; } inline const float32_t3 getLocalTarget() const { return m_target - m_position; } inline const float32_t3 getForwardDirection() const { return glm::normalize(getLocalTarget()); } - inline const float32_t4x4& getViewMatrix() const { return m_viewMatrix; } + inline const float32_t3x4& getViewMatrix() const { return m_viewMatrix; } inline bool isLeftHanded() { return m_isLeftHanded; } inline float32_t3 getPatchedUpVector() @@ -300,19 +300,12 @@ class ICameraController : virtual public core::IReferenceCounted { auto up = getPatchedUpVector(); - // TODO!!!! CASTS + m_isLeftHanded = m_projection->isLeftHanded(); - /* if (m_isLeftHanded) - m_viewMatrix = glm::lookAtLH(m_position, m_target, up); + m_viewMatrix = float32_t3x4(float32_t4x4(glm::lookAtLH(m_position, m_target, up))); else - m_viewMatrix = glm::lookAtRH(m_position, m_target, up); - */ - } - - inline bool checkIfLeftHanded(const auto& projectionMatrix) - { - return hlsl::determinant(projectionMatrix) < 0.f; + m_viewMatrix = float32_t3x4(float32_t4x4(glm::lookAtRH(m_position, m_target, up))); } core::smart_refctd_ptr m_projection; @@ -320,52 +313,42 @@ class ICameraController : virtual public core::IReferenceCounted const float32_t3 m_initialPosition, m_initialTarget; glm::quat m_orientation; - float32_t4x4 m_viewMatrix; + float32_t3x4 m_viewMatrix; - bool m_isLeftHanded, + bool m_isLeftHanded = false, m_needsToRecomputeViewMatrix = false, m_recordingManipulation = false; }; - struct SUpdateParameters - { - //! Nabla mouse events you want to be handled with a controller - std::span mouseEvents = {}; - - //! Nabla keyboard events you want to be handled with a controller - std::span keyboardEvents = {}; - }; - - ICameraController(core::smart_refctd_ptr&& gimbal) - : m_gimbal(core::smart_refctd_ptr(gimbal)) {} + ICameraController() {} - // controller can override which keys correspond to which event + // override controller keys map, it binds a key code to a virtual event void updateKeysToEvent(const std::vector& codes, VirtualEventType event) { m_keysToEvent[event] = std::move(codes); } + // start controller manipulation session virtual void begin(std::chrono::microseconds nextPresentationTimeStamp) { m_nextPresentationTimeStamp = nextPresentationTimeStamp; return; } - virtual void manipulate(SUpdateParameters parameters) = 0; + // manipulate camera with gimbal & virtual events, begin must be called before that! + virtual void manipulate(CGimbal* gimbal, std::span virtualEvents) = 0; + // finish controller manipulation session, call after last manipulate in the hot loop void end(std::chrono::microseconds nextPresentationTimeStamp) { m_lastVirtualUpTimeStamp = nextPresentationTimeStamp; } - inline void setMoveSpeed(const float moveSpeed) { m_moveSpeed = moveSpeed; } - inline void setRotateSpeed(const float rotateSpeed) { m_rotateSpeed = rotateSpeed; } - - inline const float getMoveSpeed() const { return m_moveSpeed; } - inline const float getRotateSpeed() const { return m_rotateSpeed; } - -protected: + /* // process keyboard to generate virtual manipulation events + // note that: + // - it doesn't make the manipulation itself! + */ std::vector processKeyboard(std::span events) { std::vector virtualEvents; @@ -405,12 +388,11 @@ class ICameraController : virtual public core::IReferenceCounted /* // [OPTIONAL]: process mouse to generate virtual manipulation events - // note: - // - all manipulations *may* be done with keyboard keys, it means you can have a pad/touch pad for which keys could be bound and skip the mouse - // - default implementation which is FPS camera-like controller doesn't perform roll rotation which is around Z axis! If you want more complex manipulations then override the method - // - it doesn't make the manipulation itself! It only process your mouse events you got from OS & accumulate data values to create manipulation virtual events! + // note that: + // - all manipulations *may* be done with keyboard keys (if you have a touchpad or sth an ui:: event could be a code!) + // - it doesn't make the manipulation itself! */ - virtual std::vector processMouse(std::span events) const + std::vector processMouse(std::span events) const { // accumulate total pitch & yaw delta from mouse movement events const auto [dTotalPitch, dTotalYaw] = [&]() @@ -436,6 +418,13 @@ class ICameraController : virtual public core::IReferenceCounted return { pitch, yaw }; } + inline void setMoveSpeed(const float moveSpeed) { m_moveSpeed = moveSpeed; } + inline void setRotateSpeed(const float rotateSpeed) { m_rotateSpeed = rotateSpeed; } + + inline const float getMoveSpeed() const { return m_moveSpeed; } + inline const float getRotateSpeed() const { return m_rotateSpeed; } + +protected: // controller can override default set of event map virtual void initKeysToEvent() { @@ -454,7 +443,6 @@ class ICameraController : virtual public core::IReferenceCounted m_keysToEvent[Reset] = ui::E_KEY_CODE::EKC_R ; } - core::smart_refctd_ptr m_gimbal; std::array m_keysToEvent = {}; float m_moveSpeed = 1.f, m_rotateSpeed = 1.f; bool m_keysDown[EventsCount] = {}; diff --git a/common/include/camera/IProjection.hpp b/common/include/camera/IProjection.hpp index f3edfccc4..2210da7e5 100644 --- a/common/include/camera/IProjection.hpp +++ b/common/include/camera/IProjection.hpp @@ -19,6 +19,11 @@ class IProjection : virtual public core::IReferenceCounted IProjection(const value_t& matrix = {}) : m_projectionMatrix(matrix) {} value_t& getProjectionMatrix() { return m_projectionMatrix; } + inline bool isLeftHanded() + { + return hlsl::determinant(m_projectionMatrix) < 0.f; + } + protected: value_t m_projectionMatrix; }; From be7021311e2481d270c0c7e4268991c1054fd21c Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Fri, 25 Oct 2024 15:18:47 +0200 Subject: [PATCH 010/205] improve IProjection, make the matrix private & allow to change it with a setter & get with a getter therefore determine at set time handness (compute determinant only once after the matrix gets changed!), update main.cpp & sources - time to test it --- 61_UI/main.cpp | 48 ++++++++++-------------- common/include/CCamera.hpp | 2 +- common/include/camera/ICameraControl.hpp | 12 +----- common/include/camera/IProjection.hpp | 19 +++++++--- 4 files changed, 36 insertions(+), 45 deletions(-) diff --git a/61_UI/main.cpp b/61_UI/main.cpp index c78059ae5..c561ab326 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -178,23 +178,23 @@ class UISampleApp final : public examples::SimpleWindowedApplication { ImGuiIO& io = ImGui::GetIO(); { - auto& projection = gimbal->getProjection()->getProjectionMatrix(); + auto* projection = gimbal->getProjection(); if (isPerspective) { if (isLH) - projection = projection_t::value_t(glm::perspectiveLH(glm::radians(fov), io.DisplaySize.x / io.DisplaySize.y, zNear, zFar)); + projection->setMatrix(projection_t::value_t(glm::perspectiveLH(glm::radians(fov), io.DisplaySize.x / io.DisplaySize.y, zNear, zFar))); else - projection = projection_t::value_t(glm::perspectiveRH(glm::radians(fov), io.DisplaySize.x / io.DisplaySize.y, zNear, zFar)); + projection->setMatrix(projection_t::value_t(glm::perspectiveRH(glm::radians(fov), io.DisplaySize.x / io.DisplaySize.y, zNear, zFar))); } else { float viewHeight = viewWidth * io.DisplaySize.y / io.DisplaySize.x; if (isLH) - projection = projection_t::value_t(glm::orthoLH(-viewWidth / 2.0f, viewWidth / 2.0f, -viewHeight / 2.0f, viewHeight / 2.0f, zNear, zFar)); + projection->setMatrix(projection_t::value_t(glm::orthoLH(-viewWidth / 2.0f, viewWidth / 2.0f, -viewHeight / 2.0f, viewHeight / 2.0f, zNear, zFar))); else - projection = projection_t::value_t(glm::orthoRH(-viewWidth / 2.0f, viewWidth / 2.0f, -viewHeight / 2.0f, viewHeight / 2.0f, zNear, zFar)); + projection->setMatrix(projection_t::value_t(glm::orthoRH(-viewWidth / 2.0f, viewWidth / 2.0f, -viewHeight / 2.0f, viewHeight / 2.0f, zNear, zFar))); } } @@ -331,24 +331,17 @@ class UISampleApp final : public examples::SimpleWindowedApplication * note it also modifies input view matrix but projection matrix is immutable */ - - - /* - - TODODOD - - - - static struct { float32_t4x4 view, projection, model; } imguizmoM16InOut; + const auto& projectionMatrix = gimbal->getProjection()->getMatrix(); + ImGuizmo::SetID(0u); imguizmoM16InOut.view = transpose(getMatrix3x4As4x4(gimbal->getViewMatrix())); - imguizmoM16InOut.projection = transpose(gimbal->getProjection()->getProjectionMatrix()); - imguizmoM16InOut.model = transpose(pass.scene->object.model); + imguizmoM16InOut.projection = transpose(projectionMatrix); + imguizmoM16InOut.model = transpose(getMatrix3x4As4x4(pass.scene->object.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/ @@ -359,17 +352,15 @@ class UISampleApp final : public examples::SimpleWindowedApplication // to Nabla + update camera & model matrices const auto& view = gimbal->getViewMatrix(); - const auto& projection = gimbal->getProjection()->getProjectionMatrix(); // TODO: make it more nicely const_cast(view) = float32_t3x4(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) - //camera.setProjectionMatrix(projection); // update concatanated matrix { static float32_t3x4 modelView, normal; static float32_t4x4 modelViewProjection; auto& hook = pass.scene->object; - hook.model = core::transpose(float32_t3x4(imguizmoM16InOut.model)); + hook.model = float32_t3x4(transpose(imguizmoM16InOut.model)); { const auto& references = pass.scene->getResources().objects; const auto type = static_cast(gcIndex); @@ -385,11 +376,13 @@ class UISampleApp final : public examples::SimpleWindowedApplication // TODO //modelView.getSub3x3InverseTranspose(normal); - modelViewProjection = nbl::core::concatenateBFollowedByA(camera.getConcatenatedMatrix(), hook.model); - memcpy(ubo.MVP, modelViewProjection.pointer(), sizeof(ubo.MVP)); - memcpy(ubo.MV, modelView.pointer(), sizeof(ubo.MV)); - memcpy(ubo.NormalMat, normal.pointer(), sizeof(ubo.NormalMat)); + auto concatMatrix = mul(projectionMatrix, getMatrix3x4As4x4(view)); + modelViewProjection = mul(concatMatrix, getMatrix3x4As4x4(hook.model)); + + memcpy(ubo.MVP, &modelViewProjection[0][0], sizeof(ubo.MVP)); + memcpy(ubo.MV, &modelView[0][0], sizeof(ubo.MV)); + memcpy(ubo.NormalMat, &normal[0][0], sizeof(ubo.NormalMat)); // object meta display { @@ -399,7 +392,6 @@ class UISampleApp final : public examples::SimpleWindowedApplication } } - // view matrices editor { ImGui::Begin("Matrices"); @@ -425,15 +417,13 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGui::Separator(); }; - addMatrixTable("Model Matrix", "ModelMatrixTable", 3, 4, pass.scene->object.model.pointer()); - addMatrixTable("Camera View Matrix", "ViewMatrixTable", 3, 4, view.pointer()); - addMatrixTable("Camera View Projection Matrix", "ViewProjectionMatrixTable", 4, 4, projection.pointer(), false); + addMatrixTable("Model Matrix", "ModelMatrixTable", 3, 4, &pass.scene->object.model[0][0]); + addMatrixTable("Camera Gimbal View Matrix", "ViewMatrixTable", 3, 4, &view[0][0]); + addMatrixTable("Camera Gimbal Projection Matrix", "ProjectionMatrixTable", 4, 4, &projectionMatrix[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. diff --git a/common/include/CCamera.hpp b/common/include/CCamera.hpp index 1c306c651..7024e69a9 100644 --- a/common/include/CCamera.hpp +++ b/common/include/CCamera.hpp @@ -31,7 +31,7 @@ class Camera : public ICamera const auto forward = gimbal->getForwardDirection(); const auto up = gimbal->getPatchedUpVector(); - const bool leftHanded = gimbal->isLeftHanded(); + const bool leftHanded = gimbal->getProjection()->isLeftHanded(); // strafe vector we move along when requesting left/right movements const auto strafeLeftRight = leftHanded ? glm::normalize(glm::cross(forward, up)) : glm::normalize(glm::cross(up, forward)); diff --git a/common/include/camera/ICameraControl.hpp b/common/include/camera/ICameraControl.hpp index bcc1b6d54..c6cef13ad 100644 --- a/common/include/camera/ICameraControl.hpp +++ b/common/include/camera/ICameraControl.hpp @@ -222,7 +222,6 @@ class ICameraController : virtual public core::IReferenceCounted inline const float32_t3 getLocalTarget() const { return m_target - m_position; } inline const float32_t3 getForwardDirection() const { return glm::normalize(getLocalTarget()); } inline const float32_t3x4& getViewMatrix() const { return m_viewMatrix; } - inline bool isLeftHanded() { return m_isLeftHanded; } inline float32_t3 getPatchedUpVector() { @@ -241,9 +240,6 @@ class ICameraController : virtual public core::IReferenceCounted return up; } - // TODO: getConcatenatedMatrix() - // TODO: getViewMatrix() - private: //! Reset the gimbal to its initial position, target, and orientation inline void reset() @@ -292,7 +288,6 @@ class ICameraController : virtual public core::IReferenceCounted // And we can simply update target vector m_target = m_position + localTargetRotated; - // TODO: std::any + nice checks for deltas (radians - periodic!) m_needsToRecomputeViewMatrix = true; } @@ -300,9 +295,7 @@ class ICameraController : virtual public core::IReferenceCounted { auto up = getPatchedUpVector(); - m_isLeftHanded = m_projection->isLeftHanded(); - - if (m_isLeftHanded) + if (m_projection->isLeftHanded()) m_viewMatrix = float32_t3x4(float32_t4x4(glm::lookAtLH(m_position, m_target, up))); else m_viewMatrix = float32_t3x4(float32_t4x4(glm::lookAtRH(m_position, m_target, up))); @@ -315,8 +308,7 @@ class ICameraController : virtual public core::IReferenceCounted glm::quat m_orientation; float32_t3x4 m_viewMatrix; - bool m_isLeftHanded = false, - m_needsToRecomputeViewMatrix = false, + bool m_needsToRecomputeViewMatrix = true, m_recordingManipulation = false; }; diff --git a/common/include/camera/IProjection.hpp b/common/include/camera/IProjection.hpp index 2210da7e5..1384f8e87 100644 --- a/common/include/camera/IProjection.hpp +++ b/common/include/camera/IProjection.hpp @@ -16,16 +16,25 @@ class IProjection : virtual public core::IReferenceCounted public: using value_t = T; - IProjection(const value_t& matrix = {}) : m_projectionMatrix(matrix) {} - value_t& getProjectionMatrix() { return m_projectionMatrix; } + IProjection(const value_t& matrix = {}) : m_projectionMatrix(matrix) { updateHandnessState(); } - inline bool isLeftHanded() + inline void setMatrix(const value_t& projectionMatrix) { - return hlsl::determinant(m_projectionMatrix) < 0.f; + m_projectionMatrix = projectionMatrix; + updateHandnessState(); + } + + inline const value_t& getMatrix() { return m_projectionMatrix; } + inline bool isLeftHanded() { return m_isLeftHanded; } + +private: + inline void updateHandnessState() + { + m_isLeftHanded = hlsl::determinant(m_projectionMatrix) < 0.f; } -protected: value_t m_projectionMatrix; + bool m_isLeftHanded; }; template From b7e6d9eccb06caffac3447994558967a7daf982c Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Fri, 25 Oct 2024 16:21:40 +0200 Subject: [PATCH 011/205] address https://github.com/Devsh-Graphics-Programming/Nabla/pull/760/files#r1816728485 comment in https://github.com/Devsh-Graphics-Programming/Nabla/pull/760 PR --- 61_UI/main.cpp | 5 ++--- common/include/camera/ICameraControl.hpp | 5 +++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/61_UI/main.cpp b/61_UI/main.cpp index c561ab326..321da0fce 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -7,7 +7,6 @@ #include "camera/CCubeProjection.hpp" #include "camera/ICameraControl.hpp" #include "glm/glm/ext/matrix_clip_space.hpp" // TODO: TESTING -#include "nbl/builtin/hlsl/matrix_utils/transformation_matrix_utils.hlsl" // FPS Camera, TESTS using projection_matrix_t = float32_t4x4; @@ -509,8 +508,8 @@ class UISampleApp final : public examples::SimpleWindowedApplication const float32_t3 position(cosf(camYAngle)* cosf(camXAngle)* transformParams.camDistance, sinf(camXAngle)* transformParams.camDistance, sinf(camYAngle)* cosf(camXAngle)* transformParams.camDistance), target(0.f, 0.f, 0.f), up(0.f, 1.f, 0.f); - auto pMatrix = glm::perspectiveLH(glm::radians(fov), float(m_window->getWidth()) / float(m_window->getHeight()), zNear, zFar); - auto projection = make_smart_refctd_ptr(); // TODO: CASTS FOR PROJ + auto projection = make_smart_refctd_ptr(); + projection->setMatrix(projection_matrix_t(glm::perspectiveLH(glm::radians(fov), float(m_window->getWidth()) / float(m_window->getHeight()), zNear, zFar))); gimbal = make_smart_refctd_ptr(smart_refctd_ptr(projection), position, target, up); camera = make_smart_refctd_ptr(); diff --git a/common/include/camera/ICameraControl.hpp b/common/include/camera/ICameraControl.hpp index c6cef13ad..96951dbb9 100644 --- a/common/include/camera/ICameraControl.hpp +++ b/common/include/camera/ICameraControl.hpp @@ -6,6 +6,7 @@ #include "CVirtualCameraEvent.hpp" #include "glm/glm/ext/matrix_transform.hpp" // TODO: TEMPORARY!!! whatever used will be moved to cpp #include "glm/glm/gtc/quaternion.hpp" +#include "nbl/builtin/hlsl/matrix_utils/transformation_matrix_utils.hlsl" // TODO: DIFFERENT NAMESPACE namespace nbl::hlsl @@ -296,9 +297,9 @@ class ICameraController : virtual public core::IReferenceCounted auto up = getPatchedUpVector(); if (m_projection->isLeftHanded()) - m_viewMatrix = float32_t3x4(float32_t4x4(glm::lookAtLH(m_position, m_target, up))); + m_viewMatrix = buildCameraLookAtMatrixLH(m_position, m_target, up); else - m_viewMatrix = float32_t3x4(float32_t4x4(glm::lookAtRH(m_position, m_target, up))); + m_viewMatrix = buildCameraLookAtMatrixRH(m_position, m_target, up); } core::smart_refctd_ptr m_projection; From 9bb8823037b6bb3150586b66bf6c98c4fa8535d6 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Fri, 25 Oct 2024 16:55:34 +0200 Subject: [PATCH 012/205] init default keys to virtual events, make ICameraController inheritance virtual, init time stamps with defaults --- common/include/CCamera.hpp | 4 ++-- common/include/ICamera.hpp | 2 +- common/include/camera/ICameraControl.hpp | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/common/include/CCamera.hpp b/common/include/CCamera.hpp index 7024e69a9..11a532719 100644 --- a/common/include/CCamera.hpp +++ b/common/include/CCamera.hpp @@ -12,13 +12,13 @@ namespace nbl::hlsl // TODO: DIFFERENT NAMESPACE // FPS Camera template -class Camera : public ICamera +class Camera final : public ICamera { public: using base_t = ICamera; using traits_t = typename base_t::Traits; - Camera() : base_t() {} + Camera() : base_t() { traits_t::controller_t::initKeysToEvent(); } ~Camera() = default; virtual void manipulate(traits_t::gimbal_t* gimbal, std::span virtualEvents) override diff --git a/common/include/ICamera.hpp b/common/include/ICamera.hpp index 22245c1e6..da01f830b 100644 --- a/common/include/ICamera.hpp +++ b/common/include/ICamera.hpp @@ -17,7 +17,7 @@ namespace nbl::hlsl // TODO: DIFFERENT NAMESPACE { template -class ICamera : public ICameraController +class ICamera : public virtual ICameraController { public: using base_t = typename ICameraController; diff --git a/common/include/camera/ICameraControl.hpp b/common/include/camera/ICameraControl.hpp index 96951dbb9..8896b5390 100644 --- a/common/include/camera/ICameraControl.hpp +++ b/common/include/camera/ICameraControl.hpp @@ -440,7 +440,7 @@ class ICameraController : virtual public core::IReferenceCounted float m_moveSpeed = 1.f, m_rotateSpeed = 1.f; bool m_keysDown[EventsCount] = {}; - std::chrono::microseconds m_nextPresentationTimeStamp, m_lastVirtualUpTimeStamp; + std::chrono::microseconds m_nextPresentationTimeStamp = {}, m_lastVirtualUpTimeStamp = {}; }; } // nbl::hlsl namespace From afe6ad05ae8dad2c32a11e34ccfbb654ffc57022 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Sat, 26 Oct 2024 17:41:40 +0200 Subject: [PATCH 013/205] split responsibilities & update sources --- 61_UI/main.cpp | 33 ++- common/include/CCamera.hpp | 130 +++++------- common/include/ICamera.hpp | 48 ++++- common/include/camera/ICameraControl.hpp | 245 ++++------------------- 4 files changed, 148 insertions(+), 308 deletions(-) diff --git a/61_UI/main.cpp b/61_UI/main.cpp index 321da0fce..388eab1e8 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -177,7 +177,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication { ImGuiIO& io = ImGui::GetIO(); { - auto* projection = gimbal->getProjection(); + auto* projection = camera->getProjection(); if (isPerspective) { @@ -256,15 +256,9 @@ class UISampleApp final : public examples::SimpleWindowedApplication { float32_t3 cameraPosition(cosf(camYAngle)* cosf(camXAngle)* transformParams.camDistance, sinf(camXAngle)* transformParams.camDistance, sinf(camYAngle)* cosf(camXAngle)* transformParams.camDistance); float32_t3 cameraTarget(0.f, 0.f, 0.f); - const static float32_t3 up(0.f, 1.f, 0.f); - gimbal->begin(); - { - gimbal->setPosition(cameraPosition); - gimbal->setTarget(cameraTarget); - gimbal->setBackupUpVector(up); - } - gimbal->end(); + gimbal->setPosition(cameraPosition); + camera->setTarget(cameraTarget); firstFrame = false; } @@ -335,10 +329,10 @@ class UISampleApp final : public examples::SimpleWindowedApplication float32_t4x4 view, projection, model; } imguizmoM16InOut; - const auto& projectionMatrix = gimbal->getProjection()->getMatrix(); + const auto& projectionMatrix = camera->getProjection()->getMatrix(); ImGuizmo::SetID(0u); - imguizmoM16InOut.view = transpose(getMatrix3x4As4x4(gimbal->getViewMatrix())); + imguizmoM16InOut.view = transpose(getMatrix3x4As4x4(camera->getViewMatrix())); imguizmoM16InOut.projection = transpose(projectionMatrix); imguizmoM16InOut.model = transpose(getMatrix3x4As4x4(pass.scene->object.model)); { @@ -350,7 +344,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication } // to Nabla + update camera & model matrices - const auto& view = gimbal->getViewMatrix(); + const auto& view = camera->getViewMatrix(); // TODO: make it more nicely const_cast(view) = float32_t3x4(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) @@ -449,6 +443,8 @@ class UISampleApp final : public examples::SimpleWindowedApplication 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) @@ -506,12 +502,13 @@ class UISampleApp final : public examples::SimpleWindowedApplication */ const float32_t3 position(cosf(camYAngle)* cosf(camXAngle)* transformParams.camDistance, sinf(camXAngle)* transformParams.camDistance, sinf(camYAngle)* cosf(camXAngle)* transformParams.camDistance), - target(0.f, 0.f, 0.f), up(0.f, 1.f, 0.f); + target(0.f, 0.f, 0.f); auto projection = make_smart_refctd_ptr(); projection->setMatrix(projection_matrix_t(glm::perspectiveLH(glm::radians(fov), float(m_window->getWidth()) / float(m_window->getHeight()), zNear, zFar))); - gimbal = make_smart_refctd_ptr(smart_refctd_ptr(projection), position, target, up); - camera = make_smart_refctd_ptr(); + + gimbal = make_smart_refctd_ptr(position); + camera = make_smart_refctd_ptr(core::smart_refctd_ptr(gimbal), core::smart_refctd_ptr(projection), target); return true; } @@ -762,10 +759,8 @@ class UISampleApp final : public examples::SimpleWindowedApplication const auto virtualMouseEvents = camera->processMouse(params.mouseEvents); const auto virtualKeyboardEvents = camera->processMouse(params.mouseEvents); - gimbal->begin(); - camera->manipulate(gimbal.get(), { virtualMouseEvents.data(), virtualMouseEvents.size()}); - camera->manipulate(gimbal.get(), { virtualKeyboardEvents.data(), virtualKeyboardEvents.size()}); - gimbal->end(); + camera->manipulate({ virtualMouseEvents.data(), virtualMouseEvents.size()}); + camera->manipulate({ virtualKeyboardEvents.data(), virtualKeyboardEvents.size()}); camera->end(nextPresentationTimestamp); } diff --git a/common/include/CCamera.hpp b/common/include/CCamera.hpp index 11a532719..bf33373a1 100644 --- a/common/include/CCamera.hpp +++ b/common/include/CCamera.hpp @@ -18,23 +18,20 @@ class Camera final : public ICamera using base_t = ICamera; using traits_t = typename base_t::Traits; - Camera() : base_t() { traits_t::controller_t::initKeysToEvent(); } + Camera(core::smart_refctd_ptr&& gimbal, core::smart_refctd_ptr projection, const float32_t3& target = { 0,0,0 }) + : base_t(core::smart_refctd_ptr(gimbal), core::smart_refctd_ptr(projection), target) { traits_t::controller_t::initKeysToEvent(); } ~Camera() = default; - virtual void manipulate(traits_t::gimbal_t* gimbal, std::span virtualEvents) override + virtual void manipulate(std::span virtualEvents) override { - if (!gimbal) - return; // TODO: LOG + auto* gimbal = traits_t::controller_t::m_gimbal.get(); + auto* projection = base_t::getProjection(); - if (!gimbal->isRecordingManipulation()) - return; // TODO: LOG + assert(gimbal); // TODO + assert(projection); // TODO - const auto forward = gimbal->getForwardDirection(); - const auto up = gimbal->getPatchedUpVector(); - const bool leftHanded = gimbal->getProjection()->isLeftHanded(); - - // strafe vector we move along when requesting left/right movements - const auto strafeLeftRight = leftHanded ? glm::normalize(glm::cross(forward, up)) : glm::normalize(glm::cross(up, forward)); + const auto [forward, up, right] = std::make_tuple(gimbal->getZAxis(), gimbal->getYAxis(), gimbal->getXAxis()); + const bool isLeftHanded = projection->isLeftHanded(); // TODO? constexpr auto MoveSpeedScale = 0.003f; constexpr auto RotateSpeedScale = 0.003f; @@ -43,81 +40,58 @@ class Camera final : public ICamera const auto dRotateFactor = traits_t::controller_t::m_rotateSpeed * RotateSpeedScale; // TODO: UB/LB for pitch [-88,88]!!! we are not in cosmos but handle FPS camera in default case + // TODO: accumulate move scalars & rotate scalars then do single move & rotate for (const traits_t::controller_virtual_event_t& ev : virtualEvents) { const auto dMoveValue = ev.value * dMoveFactor; const auto dRotateValue = ev.value * dRotateFactor; - typename traits_t::gimbal_virtual_event_t gimbalEvent; - switch (ev.type) { - case traits_t::controller_t::MoveForward: - { - gimbalEvent.type = traits_t::gimbal_t::Strafe; - gimbalEvent.manipulation.strafe.direction = forward; - gimbalEvent.manipulation.strafe.distance = dMoveValue; - } break; - - case traits_t::controller_t::MoveBackward: - { - gimbalEvent.type = traits_t::gimbal_t::Strafe; - gimbalEvent.manipulation.strafe.direction = -forward; - gimbalEvent.manipulation.strafe.distance = dMoveValue; - } break; - - case traits_t::controller_t::MoveLeft: - { - gimbalEvent.type = traits_t::gimbal_t::Strafe; - gimbalEvent.manipulation.strafe.direction = -strafeLeftRight; - gimbalEvent.manipulation.strafe.distance = dMoveValue; - } break; - - case traits_t::controller_t::MoveRight: - { - gimbalEvent.type = traits_t::gimbal_t::Strafe; - gimbalEvent.manipulation.strafe.direction = strafeLeftRight; - gimbalEvent.manipulation.strafe.distance = dMoveValue; - } break; - - case traits_t::controller_t::TiltUp: - { - gimbalEvent.type = traits_t::gimbal_t::Rotate; - gimbalEvent.manipulation.rotation.pitch = dRotateValue; - gimbalEvent.manipulation.rotation.roll = 0.0f; - gimbalEvent.manipulation.rotation.yaw = 0.0f; - } break; - - case traits_t::controller_t::TiltDown: - { - gimbalEvent.type = traits_t::gimbal_t::Rotate; - gimbalEvent.manipulation.rotation.pitch = -dRotateValue; - gimbalEvent.manipulation.rotation.roll = 0.0f; - gimbalEvent.manipulation.rotation.yaw = 0.0f; - } break; - - case traits_t::controller_t::PanLeft: - { - gimbalEvent.type = traits_t::gimbal_t::Rotate; - gimbalEvent.manipulation.rotation.pitch = 0.0f; - gimbalEvent.manipulation.rotation.roll = 0.0f; - gimbalEvent.manipulation.rotation.yaw = -dRotateValue; - } break; - - case traits_t::controller_t::PanRight: - { - gimbalEvent.type = traits_t::gimbal_t::Rotate; - gimbalEvent.manipulation.rotation.pitch = 0.0f; - gimbalEvent.manipulation.rotation.roll = 0.0f; - gimbalEvent.manipulation.rotation.yaw = dRotateValue; - } break; - - default: - continue; + case traits_t::controller_t::MoveForward: + { + gimbal->advance(dMoveValue); + } break; + + case traits_t::controller_t::MoveBackward: + { + gimbal->advance(-dMoveValue); + } break; + + case traits_t::controller_t::MoveLeft: + { + gimbal->strafe(dMoveValue); + } break; + + case traits_t::controller_t::MoveRight: + { + gimbal->strafe(-dMoveValue); + } break; + + case traits_t::controller_t::TiltUp: + { + gimbal->rotate(right, dRotateValue); + } break; + + case traits_t::controller_t::TiltDown: + { + gimbal->rotate(right, -dRotateValue); + } break; + + case traits_t::controller_t::PanLeft: + { + gimbal->rotate(up, dRotateValue); + } break; + + case traits_t::controller_t::PanRight: + { + gimbal->rotate(up, -dRotateValue); + } break; + + default: + continue; } - - gimbal->manipulate(gimbalEvent); } } }; diff --git a/common/include/ICamera.hpp b/common/include/ICamera.hpp index da01f830b..e436bd8d7 100644 --- a/common/include/ICamera.hpp +++ b/common/include/ICamera.hpp @@ -17,7 +17,7 @@ namespace nbl::hlsl // TODO: DIFFERENT NAMESPACE { template -class ICamera : public virtual ICameraController +class ICamera : public ICameraController { public: using base_t = typename ICameraController; @@ -27,12 +27,54 @@ class ICamera : public virtual ICameraController using controller_t = base_t; using projection_t = typename controller_t::projection_t; using gimbal_t = typename controller_t::CGimbal; - using gimbal_virtual_event_t = typename gimbal_t::CVirtualEvent; using controller_virtual_event_t = typename controller_t::CVirtualEvent; }; - ICamera() : base_t() {} + ICamera(core::smart_refctd_ptr&& gimbal, core::smart_refctd_ptr projection, const float32_t3& target = {0,0,0}) + : base_t(core::smart_refctd_ptr(gimbal)), m_projection(core::smart_refctd_ptr(projection)) { setTarget(target); } ~ICamera() = default; + + inline void setPosition(const float32_t3& position) + { + const auto* gimbal = base_t::m_gimbal.get(); + gimbal->setPosition(position); + recomputeViewMatrix(); + } + + inline void setTarget(const float32_t3& position) + { + m_target = position; + + const auto* gimbal = base_t::m_gimbal.get(); + auto localTarget = m_target - gimbal->getPosition(); + + // TODO: use gimbal to perform a rotation! + + recomputeViewMatrix(); + } + + inline const float32_t3& getPosition() { return base_t::m_gimbal->getPosition(); } + inline const float32_t3& getTarget() { return m_target; } + inline const float32_t3x4& getViewMatrix() const { return m_viewMatrix; } + inline Traits::projection_t* getProjection() { return m_projection.get(); } + +private: + inline void recomputeViewMatrix() + { + // TODO: adjust for handedness (axes flip) + const bool isLeftHanded = m_projection->isLeftHanded(); + const auto* gimbal = base_t::m_gimbal.get(); + const auto& position = gimbal->getPosition(); + + const auto [xaxis, yaxis, zaxis] = std::make_tuple(gimbal->getXAxis(), gimbal->getYAxis(), gimbal->getZAxis()); + m_viewMatrix[0u] = float32_t4(xaxis, -hlsl::dot(xaxis, position)); + m_viewMatrix[1u] = float32_t4(yaxis, -hlsl::dot(yaxis, position)); + m_viewMatrix[2u] = float32_t4(zaxis, -hlsl::dot(zaxis, position)); + } + + const core::smart_refctd_ptr m_projection; + float32_t3x4 m_viewMatrix; + float32_t3 m_target; }; } diff --git a/common/include/camera/ICameraControl.hpp b/common/include/camera/ICameraControl.hpp index 8896b5390..96e738b65 100644 --- a/common/include/camera/ICameraControl.hpp +++ b/common/include/camera/ICameraControl.hpp @@ -74,246 +74,74 @@ class ICameraController : virtual public core::IReferenceCounted class CGimbal : virtual public core::IReferenceCounted { public: - //! Virtual event representing a combined gimbal manipulation - enum VirtualEventType - { - //! Move the camera in the direction of strafe vector - Strafe, - - //! Update orientation of camera by rotating around X, Y, Z axes - Rotate, - - //! Signals boolean state, for example "reset" - State - }; - - class CVirtualEvent - { - public: - using manipulation_encode_t = float32_t4; - - struct StrafeManipulation - { - float32_t3 direction = {}; - float distance = {}; - }; - - struct RotateManipulation - { - float pitch = {}, roll = {}, yaw = {}; - }; - - struct StateManipulation - { - uint32_t reset : 1; - uint32_t reserved : 31; - - StateManipulation() { memset(this, 0, sizeof(StateManipulation)); } - ~StateManipulation() {} - }; - - union ManipulationValue - { - StrafeManipulation strafe; - RotateManipulation rotation; - StateManipulation state; - - ManipulationValue() { memset(this, 0, sizeof(ManipulationValue)); } - ~ManipulationValue() {} - }; - - CVirtualEvent() {} - - CVirtualEvent(VirtualEventType _type, const ManipulationValue _manipulation) - : type(_type), manipulation(_manipulation) - { - static_assert(sizeof(manipulation_encode_t) == sizeof(ManipulationValue)); - } - - VirtualEventType type; - ManipulationValue manipulation; - }; + CGimbal(const float32_t3& position, glm::quat orientation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f)) + : m_position(position), m_orientation(orientation) {} - CGimbal(const core::smart_refctd_ptr&& projection, const float32_t3& position, const float32_t3& lookat, const float32_t3& upVec = float32_t3(0.0f, 1.0f, 0.0f), const float32_t3& backupUpVec = float32_t3(0.5f, 1.0f, 0.0f)) - : m_projection(projection), m_position(position), m_target(lookat), m_upVec(upVec), m_backupUpVec(backupUpVec), m_initialPosition(position), m_initialTarget(lookat), m_orientation(glm::quat(1.0f, 0.0f, 0.0f, 0.0f)), m_viewMatrix({}) - { - recomputeViewMatrix(); - } - - // TODO: ctor with core::path to json config file to load defaults - - //! Start a gimbal manipulation session - inline void begin() - { - m_needsToRecomputeViewMatrix = false; - m_recordingManipulation = true; - } - - //! Record manipulation of the gimbal, note those events are delta manipulations - void manipulate(const CVirtualEvent& virtualEvent) - { - if (!m_recordingManipulation) - return; // TODO: log it - - const auto& manipulation = virtualEvent.manipulation; - - switch (virtualEvent.type) - { - case VirtualEventType::Strafe: - { - strafe(manipulation.strafe.direction, manipulation.strafe.distance); - } break; - - case VirtualEventType::Rotate: - { - rotate(manipulation.rotation.pitch, manipulation.rotation.yaw, manipulation.rotation.roll); - } break; - - case VirtualEventType::State: - { - if (manipulation.state.reset) - reset(); - } break; - - default: - break; - } - } - - // Record change of position vector, global update inline void setPosition(const float32_t3& position) { - if (!m_recordingManipulation) - return; // TODO: log it - m_position = position; } - // Record change of target vector, global update - inline void setTarget(const float32_t3& target) + inline void rotate(const float32_t3& axis, float dRadians) { - if (!m_recordingManipulation) - return; // TODO: log it - - m_target = target; + glm::quat dRotation = glm::angleAxis(dRadians, axis); + m_orientation = glm::normalize(dRotation * m_orientation); + m_orthonormal = float32_t3x3(glm::mat3_cast(m_orientation)); } - // Change up vector, global update - inline void setUpVector(const float32_t3& up) { m_upVec = up; } - - // Change backupUp vector, global update - inline void setBackupUpVector(const float32_t3& backupUp) { m_backupUpVec = backupUp; } - - //! End the gimbal manipulation session, recompute view matrix if required and update handedness state from projection - inline void end() + inline void strafe(float distance) { - if (m_needsToRecomputeViewMatrix) - recomputeViewMatrix(); - - m_needsToRecomputeViewMatrix = false; - m_recordingManipulation = false; + move({ 0.f, distance, 0.f }); } - inline bool isRecordingManipulation() { return m_recordingManipulation; } - inline projection_t* getProjection() { return m_projection.get(); } - inline const float32_t3& getPosition() const { return m_position; } - inline const float32_t3& getTarget() const { return m_target; } - inline const float32_t3& getUpVector() const { return m_upVec; } - inline const float32_t3& getBackupUpVector() const { return m_backupUpVec; } - inline const float32_t3 getLocalTarget() const { return m_target - m_position; } - inline const float32_t3 getForwardDirection() const { return glm::normalize(getLocalTarget()); } - inline const float32_t3x4& getViewMatrix() const { return m_viewMatrix; } - - inline float32_t3 getPatchedUpVector() + inline void climb(float distance) { - // if up vector and vector to the target are the same we patch the up vector - auto up = glm::normalize(m_upVec); - - const auto localTarget = getForwardDirection(); - const auto cross = glm::cross(localTarget, up); - - // we compute squared length but for checking if the len is 0 it doesnt matter - const bool upVectorZeroLength = glm::dot(cross, cross) == 0.f; - - if (upVectorZeroLength) - up = glm::normalize(m_backupUpVec); - - return up; + move({ 0.f, 0.f, distance }); } - private: - //! Reset the gimbal to its initial position, target, and orientation - inline void reset() + inline void advance(float distance) { - m_position = m_initialPosition; - m_target = m_initialTarget; - m_orientation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f); - - recomputeViewMatrix(); // Recompute the view matrix after resetting + move({ distance, 0.f, 0.f }); } - //! Move in the direction of strafe (mostly left/right, up/down) - inline void strafe(const glm::vec3& direction, float distance) + inline void move(float32_t3 delta) { - if (distance != 0.0f) - { - const auto strafeVector = glm::normalize(direction) * distance; - m_position += strafeVector; - m_target += strafeVector; - - m_needsToRecomputeViewMatrix = true; - } + m_position += mul(m_orthonormal, delta); } - //! Update orientation by rotating around all XYZ axes - delta rotations in radians - inline void rotate(float dPitchRadians, float dYawDeltaRadians, float dRollDeltaRadians) + inline void reset() { - // Rotate around X (pitch) - glm::quat qPitch = glm::angleAxis(dPitchRadians, glm::vec3(1.0f, 0.0f, 0.0f)); - - // Rotate around Y (yaw) - glm::quat qYaw = glm::angleAxis(dYawDeltaRadians, glm::vec3(0.0f, 1.0f, 0.0f)); - - // Rotate around Z (roll) - glm::quat qRoll = glm::angleAxis(dRollDeltaRadians, glm::vec3(0.0f, 0.0f, 1.0f)); - - // Combine the new rotations with the current orientation - m_orientation = glm::normalize(qYaw * qPitch * qRoll * m_orientation); - - // Now we have rotation transformation as 3x3 matrix - auto rotate = glm::mat3_cast(m_orientation); + // TODO + } - // We do not change magnitude of the vector - auto localTargetRotated = rotate * getLocalTarget(); + // Position of gimbal + inline const float32_t3& getPosition() const { return m_position; } - // And we can simply update target vector - m_target = m_position + localTargetRotated; + // Orientation of gimbal + inline const glm::quat& getOrientation() const { return m_orientation; } - m_needsToRecomputeViewMatrix = true; - } + // Orthonormal [getXAxis(), getYAxis(), getZAxis()] matrix + inline const float32_t3x3& getOrthonornalMatrix() const { return m_orthonormal; } - inline void recomputeViewMatrix() - { - auto up = getPatchedUpVector(); + // Base right vector in orthonormal basis, base "right" vector (X-axis) + inline const float32_t3& getXAxis() const { return m_orthonormal[0u]; } - if (m_projection->isLeftHanded()) - m_viewMatrix = buildCameraLookAtMatrixLH(m_position, m_target, up); - else - m_viewMatrix = buildCameraLookAtMatrixRH(m_position, m_target, up); - } + // Base up vector in orthonormal basis, base "up" vector (Y-axis) + inline const float32_t3& getYAxis() const { return m_orthonormal[1u]; } - core::smart_refctd_ptr m_projection; - float32_t3 m_position, m_target, m_upVec, m_backupUpVec; - const float32_t3 m_initialPosition, m_initialTarget; + // Base forward vector in orthonormal basis, base "forward" vector (Z-axis) + inline const float32_t3& getZAxis() const { return m_orthonormal[2u]; } + private: + float32_t3 m_position; glm::quat m_orientation; - float32_t3x4 m_viewMatrix; - bool m_needsToRecomputeViewMatrix = true, - m_recordingManipulation = false; + // Represents the camera's orthonormal basis + // https://en.wikipedia.org/wiki/Orthonormal_basis + float32_t3x3 m_orthonormal; }; - ICameraController() {} + ICameraController(core::smart_refctd_ptr&& gimbal) : m_gimbal(core::smart_refctd_ptr(gimbal)) {} // override controller keys map, it binds a key code to a virtual event void updateKeysToEvent(const std::vector& codes, VirtualEventType event) @@ -329,7 +157,7 @@ class ICameraController : virtual public core::IReferenceCounted } // manipulate camera with gimbal & virtual events, begin must be called before that! - virtual void manipulate(CGimbal* gimbal, std::span virtualEvents) = 0; + virtual void manipulate(std::span virtualEvents) = 0; // finish controller manipulation session, call after last manipulate in the hot loop void end(std::chrono::microseconds nextPresentationTimeStamp) @@ -436,6 +264,7 @@ class ICameraController : virtual public core::IReferenceCounted m_keysToEvent[Reset] = ui::E_KEY_CODE::EKC_R ; } + core::smart_refctd_ptr m_gimbal; std::array m_keysToEvent = {}; float m_moveSpeed = 1.f, m_rotateSpeed = 1.f; bool m_keysDown[EventsCount] = {}; From 82efd375db1ee17ed8217ad5908d9243a97c902e Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Sat, 26 Oct 2024 17:50:08 +0200 Subject: [PATCH 014/205] add updateOrthonormalMatrix --- common/include/camera/ICameraControl.hpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/common/include/camera/ICameraControl.hpp b/common/include/camera/ICameraControl.hpp index 96e738b65..e650324f5 100644 --- a/common/include/camera/ICameraControl.hpp +++ b/common/include/camera/ICameraControl.hpp @@ -75,7 +75,7 @@ class ICameraController : virtual public core::IReferenceCounted { public: CGimbal(const float32_t3& position, glm::quat orientation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f)) - : m_position(position), m_orientation(orientation) {} + : m_position(position), m_orientation(orientation) { updateOrthonormalMatrix(); } inline void setPosition(const float32_t3& position) { @@ -86,7 +86,7 @@ class ICameraController : virtual public core::IReferenceCounted { glm::quat dRotation = glm::angleAxis(dRadians, axis); m_orientation = glm::normalize(dRotation * m_orientation); - m_orthonormal = float32_t3x3(glm::mat3_cast(m_orientation)); + updateOrthonormalMatrix(); } inline void strafe(float distance) @@ -133,6 +133,8 @@ class ICameraController : virtual public core::IReferenceCounted inline const float32_t3& getZAxis() const { return m_orthonormal[2u]; } private: + inline void updateOrthonormalMatrix() { m_orthonormal = float32_t3x3(glm::mat3_cast(glm::normalize(m_orientation))); } + float32_t3 m_position; glm::quat m_orientation; From 06400fbebd81f73828d1a5030c6d58b704ea9744 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Sat, 26 Oct 2024 18:14:49 +0200 Subject: [PATCH 015/205] start eliminating first runtime bugs & typos --- 61_UI/main.cpp | 2 +- common/include/CCamera.hpp | 2 +- common/include/camera/ICameraControl.hpp | 42 +++++++++++++----------- 3 files changed, 24 insertions(+), 22 deletions(-) diff --git a/61_UI/main.cpp b/61_UI/main.cpp index 388eab1e8..aca5ec34d 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -757,7 +757,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication if (move) { const auto virtualMouseEvents = camera->processMouse(params.mouseEvents); - const auto virtualKeyboardEvents = camera->processMouse(params.mouseEvents); + const auto virtualKeyboardEvents = camera->processKeyboard(params.keyboardEvents); camera->manipulate({ virtualMouseEvents.data(), virtualMouseEvents.size()}); camera->manipulate({ virtualKeyboardEvents.data(), virtualKeyboardEvents.size()}); diff --git a/common/include/CCamera.hpp b/common/include/CCamera.hpp index bf33373a1..2b610082c 100644 --- a/common/include/CCamera.hpp +++ b/common/include/CCamera.hpp @@ -40,7 +40,7 @@ class Camera final : public ICamera const auto dRotateFactor = traits_t::controller_t::m_rotateSpeed * RotateSpeedScale; // TODO: UB/LB for pitch [-88,88]!!! we are not in cosmos but handle FPS camera in default case - // TODO: accumulate move scalars & rotate scalars then do single move & rotate + // TODO: accumulate move & rotate scalars then do single move & rotate gimbal manipulation for (const traits_t::controller_virtual_event_t& ev : virtualEvents) { diff --git a/common/include/camera/ICameraControl.hpp b/common/include/camera/ICameraControl.hpp index e650324f5..67781b250 100644 --- a/common/include/camera/ICameraControl.hpp +++ b/common/include/camera/ICameraControl.hpp @@ -174,7 +174,7 @@ class ICameraController : virtual public core::IReferenceCounted */ std::vector processKeyboard(std::span events) { - std::vector virtualEvents; + std::vector output; for (const auto& ev : events) { @@ -196,7 +196,7 @@ class ICameraController : virtual public core::IReferenceCounted if (ev.action == nbl::ui::SKeyboardEvent::ECA_PRESSED && !m_keysDown[virtualKey]) { m_keysDown[virtualKey] = true; - virtualEvents.emplace_back(CVirtualEvent{ virtualKey, static_cast(dt) }); + output.emplace_back(CVirtualEvent{ virtualKey, static_cast(dt) }); } else if (ev.action == nbl::ui::SKeyboardEvent::ECA_RELEASED) { @@ -206,7 +206,7 @@ class ICameraController : virtual public core::IReferenceCounted } } - return virtualEvents; + return output; } /* @@ -217,28 +217,30 @@ class ICameraController : virtual public core::IReferenceCounted */ std::vector processMouse(std::span events) const { - // accumulate total pitch & yaw delta from mouse movement events - const auto [dTotalPitch, dTotalYaw] = [&]() - { - double dPitch = {}, dYaw = {}; + double dPitch = {}, dYaw = {}; - for (const auto& ev : events) - if (ev.type == nbl::ui::SMouseEvent::EET_MOVEMENT) - { - dYaw += ev.movementEvent.relativeMovementX; // (yaw) - dPitch -= ev.movementEvent.relativeMovementY; // (pitch) - } + for (const auto& ev : events) + if (ev.type == nbl::ui::SMouseEvent::EET_MOVEMENT) + { + dYaw += ev.movementEvent.relativeMovementX; + dPitch += ev.movementEvent.relativeMovementY; + } - return std::make_tuple(dPitch, dYaw); - }(); + std::vector output; - CVirtualEvent pitch; - pitch.type = (pitch.value = dTotalPitch) > 0.f ? TiltUp : TiltDown; + if (dPitch) + { + auto& pitch = output.emplace_back(); + pitch.type = (pitch.value = dPitch) > 0.f ? TiltUp : TiltDown; + } - CVirtualEvent yaw; - yaw.type = (yaw.value = dTotalYaw) > 0.f ? PanRight : PanLeft; + if (dYaw) + { + auto& yaw = output.emplace_back(); + yaw.type = (yaw.value = dYaw) > 0.f ? PanRight : PanLeft; + } - return { pitch, yaw }; + return output; } inline void setMoveSpeed(const float moveSpeed) { m_moveSpeed = moveSpeed; } From de18f199c00232b8e5c6e829bcf909a6b5a835fe Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Mon, 28 Oct 2024 12:29:23 +0100 Subject: [PATCH 016/205] add debug asserts for matrix base checks --- common/include/camera/ICameraControl.hpp | 26 +++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/common/include/camera/ICameraControl.hpp b/common/include/camera/ICameraControl.hpp index 67781b250..00fe649a2 100644 --- a/common/include/camera/ICameraControl.hpp +++ b/common/include/camera/ICameraControl.hpp @@ -133,7 +133,31 @@ class ICameraController : virtual public core::IReferenceCounted inline const float32_t3& getZAxis() const { return m_orthonormal[2u]; } private: - inline void updateOrthonormalMatrix() { m_orthonormal = float32_t3x3(glm::mat3_cast(glm::normalize(m_orientation))); } + inline void updateOrthonormalMatrix() + { + m_orthonormal = float32_t3x3(glm::mat3_cast(glm::normalize(m_orientation))); + + // DEBUG + const auto [xaxis, yaxis, zaxis] = std::make_tuple(getXAxis(),getYAxis(), getZAxis()); + + auto isNormalized = [](const auto& v, float epsilon) -> bool + { + return glm::epsilonEqual(glm::length(v), 1.0f, epsilon); + }; + + auto isOrthogonal = [](const auto& a, const auto& b, float epsilon) -> bool + { + return glm::epsilonEqual(glm::dot(a, b), 0.0f, epsilon); + }; + + auto isOrthoBase = [&](const auto& x, const auto& y, const auto& z, float epsilon = 1e-6f) -> bool + { + return isNormalized(x, epsilon) && isNormalized(y, epsilon) && isNormalized(z, epsilon) && + isOrthogonal(x, y, epsilon) && isOrthogonal(x, z, epsilon) && isOrthogonal(y, z, epsilon); + }; + + assert(isOrthoBase(xaxis, yaxis, zaxis)); + } float32_t3 m_position; glm::quat m_orientation; From 9e12014534440827c07ddc57857b5c8275b485bb Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Mon, 28 Oct 2024 15:45:43 +0100 Subject: [PATCH 017/205] fix another bugs, make the rotation work! --- 61_UI/main.cpp | 21 ++++++++++++++------- common/include/CCamera.hpp | 12 +++++++++--- common/include/ICamera.hpp | 2 +- common/include/camera/ICameraControl.hpp | 6 ++++-- 4 files changed, 28 insertions(+), 13 deletions(-) diff --git a/61_UI/main.cpp b/61_UI/main.cpp index aca5ec34d..9e65776a7 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -182,18 +182,18 @@ class UISampleApp final : public examples::SimpleWindowedApplication if (isPerspective) { if (isLH) - projection->setMatrix(projection_t::value_t(glm::perspectiveLH(glm::radians(fov), io.DisplaySize.x / io.DisplaySize.y, zNear, zFar))); + projection->setMatrix(buildProjectionMatrixPerspectiveFovLH(glm::radians(fov), io.DisplaySize.x / io.DisplaySize.y, zNear, zFar)); else - projection->setMatrix(projection_t::value_t(glm::perspectiveRH(glm::radians(fov), io.DisplaySize.x / io.DisplaySize.y, zNear, zFar))); + projection->setMatrix(buildProjectionMatrixPerspectiveFovRH(glm::radians(fov), io.DisplaySize.x / io.DisplaySize.y, zNear, zFar)); } else { float viewHeight = viewWidth * io.DisplaySize.y / io.DisplaySize.x; if (isLH) - projection->setMatrix(projection_t::value_t(glm::orthoLH(-viewWidth / 2.0f, viewWidth / 2.0f, -viewHeight / 2.0f, viewHeight / 2.0f, zNear, zFar))); + projection->setMatrix(buildProjectionMatrixOrthoLH(viewWidth, viewHeight, zNear, zFar)); else - projection->setMatrix(projection_t::value_t(glm::orthoRH(-viewWidth / 2.0f, viewWidth / 2.0f, -viewHeight / 2.0f, viewHeight / 2.0f, zNear, zFar))); + projection->setMatrix(buildProjectionMatrixOrthoRH(viewWidth, viewHeight, zNear, zFar)); } } @@ -410,7 +410,16 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGui::Separator(); }; + auto& orientation = gimbal->getOrthonornalMatrix(); + addMatrixTable("Model Matrix", "ModelMatrixTable", 3, 4, &pass.scene->object.model[0][0]); + + addMatrixTable("Right", "OrientationRightVec", 1, 3, &gimbal->getXAxis()[0]); + addMatrixTable("Up", "OrientationUpVec", 1, 3, &gimbal->getYAxis()[0]); + addMatrixTable("Forward", "OrientationForwardVec", 1, 3, &gimbal->getZAxis()[0]); + addMatrixTable("Position", "PositionForwardVec", 1, 3, &gimbal->getPosition()[0]); + + //addMatrixTable("Camera Gimbal Orientation Matrix", "OrientationMatrixTable", 3, 3, &orientation[0][0]); addMatrixTable("Camera Gimbal View Matrix", "ViewMatrixTable", 3, 4, &view[0][0]); addMatrixTable("Camera Gimbal Projection Matrix", "ProjectionMatrixTable", 4, 4, &projectionMatrix[0][0], false); @@ -443,8 +452,6 @@ class UISampleApp final : public examples::SimpleWindowedApplication 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) @@ -505,7 +512,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication target(0.f, 0.f, 0.f); auto projection = make_smart_refctd_ptr(); - projection->setMatrix(projection_matrix_t(glm::perspectiveLH(glm::radians(fov), float(m_window->getWidth()) / float(m_window->getHeight()), zNear, zFar))); + projection->setMatrix(buildProjectionMatrixPerspectiveFovLH(glm::radians(fov), float(m_window->getWidth()) / float(m_window->getHeight()), zNear, zFar)); gimbal = make_smart_refctd_ptr(position); camera = make_smart_refctd_ptr(core::smart_refctd_ptr(gimbal), core::smart_refctd_ptr(projection), target); diff --git a/common/include/CCamera.hpp b/common/include/CCamera.hpp index 2b610082c..2f9751969 100644 --- a/common/include/CCamera.hpp +++ b/common/include/CCamera.hpp @@ -19,7 +19,11 @@ class Camera final : public ICamera using traits_t = typename base_t::Traits; Camera(core::smart_refctd_ptr&& gimbal, core::smart_refctd_ptr projection, const float32_t3& target = { 0,0,0 }) - : base_t(core::smart_refctd_ptr(gimbal), core::smart_refctd_ptr(projection), target) { traits_t::controller_t::initKeysToEvent(); } + : base_t(core::smart_refctd_ptr(gimbal), core::smart_refctd_ptr(projection), target) + { + traits_t::controller_t::initKeysToEvent(); + base_t::recomputeViewMatrix(); + } ~Camera() = default; virtual void manipulate(std::span virtualEvents) override @@ -79,12 +83,12 @@ class Camera final : public ICamera gimbal->rotate(right, -dRotateValue); } break; - case traits_t::controller_t::PanLeft: + case traits_t::controller_t::PanRight: { gimbal->rotate(up, dRotateValue); } break; - case traits_t::controller_t::PanRight: + case traits_t::controller_t::PanLeft: { gimbal->rotate(up, -dRotateValue); } break; @@ -93,6 +97,8 @@ class Camera final : public ICamera continue; } } + + base_t::recomputeViewMatrix(); } }; diff --git a/common/include/ICamera.hpp b/common/include/ICamera.hpp index e436bd8d7..2f17fe79d 100644 --- a/common/include/ICamera.hpp +++ b/common/include/ICamera.hpp @@ -58,7 +58,7 @@ class ICamera : public ICameraController inline const float32_t3x4& getViewMatrix() const { return m_viewMatrix; } inline Traits::projection_t* getProjection() { return m_projection.get(); } -private: +protected: inline void recomputeViewMatrix() { // TODO: adjust for handedness (axes flip) diff --git a/common/include/camera/ICameraControl.hpp b/common/include/camera/ICameraControl.hpp index 00fe649a2..52a6020ed 100644 --- a/common/include/camera/ICameraControl.hpp +++ b/common/include/camera/ICameraControl.hpp @@ -255,13 +255,15 @@ class ICameraController : virtual public core::IReferenceCounted if (dPitch) { auto& pitch = output.emplace_back(); - pitch.type = (pitch.value = dPitch) > 0.f ? TiltUp : TiltDown; + pitch.type = dPitch > 0.f ? TiltUp : TiltDown; + pitch.value = std::abs(dPitch); } if (dYaw) { auto& yaw = output.emplace_back(); - yaw.type = (yaw.value = dYaw) > 0.f ? PanRight : PanLeft; + yaw.type = dYaw > 0.f ? PanRight : PanLeft; + yaw.value = std::abs(dYaw); } return output; From 21a46e408e54708e4aa6138cb307ac49462e4e51 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Mon, 28 Oct 2024 17:16:22 +0100 Subject: [PATCH 018/205] movement fixes, finally gimbal fully controls camera position & orientation (TODO: constraints for type of camera + I broke continuity of events signaling & need those keysPressed bools I guess) --- common/include/CCamera.hpp | 18 ++++++++++-------- common/include/camera/ICameraControl.hpp | 11 ++--------- 2 files changed, 12 insertions(+), 17 deletions(-) diff --git a/common/include/CCamera.hpp b/common/include/CCamera.hpp index 2f9751969..9ccd59521 100644 --- a/common/include/CCamera.hpp +++ b/common/include/CCamera.hpp @@ -37,6 +37,8 @@ class Camera final : public ICamera const auto [forward, up, right] = std::make_tuple(gimbal->getZAxis(), gimbal->getYAxis(), gimbal->getXAxis()); const bool isLeftHanded = projection->isLeftHanded(); // TODO? + const auto moveDirection = float32_t3(gimbal->getOrientation() * glm::vec3(0.0f, 0.0f, 1.0f)); + constexpr auto MoveSpeedScale = 0.003f; constexpr auto RotateSpeedScale = 0.003f; @@ -48,29 +50,29 @@ class Camera final : public ICamera for (const traits_t::controller_virtual_event_t& ev : virtualEvents) { - const auto dMoveValue = ev.value * dMoveFactor; - const auto dRotateValue = ev.value * dRotateFactor; + const float dMoveValue = ev.value * dMoveFactor; + const float dRotateValue = ev.value * dRotateFactor; switch (ev.type) { case traits_t::controller_t::MoveForward: { - gimbal->advance(dMoveValue); + gimbal->move(moveDirection * dMoveValue); } break; case traits_t::controller_t::MoveBackward: { - gimbal->advance(-dMoveValue); + gimbal->move(moveDirection * (-dMoveValue)); } break; - case traits_t::controller_t::MoveLeft: + case traits_t::controller_t::MoveRight: { - gimbal->strafe(dMoveValue); + gimbal->move(right * dMoveValue); } break; - case traits_t::controller_t::MoveRight: + case traits_t::controller_t::MoveLeft: { - gimbal->strafe(-dMoveValue); + gimbal->move(right * (-dMoveValue)); } break; case traits_t::controller_t::TiltUp: diff --git a/common/include/camera/ICameraControl.hpp b/common/include/camera/ICameraControl.hpp index 52a6020ed..fe248c76d 100644 --- a/common/include/camera/ICameraControl.hpp +++ b/common/include/camera/ICameraControl.hpp @@ -106,7 +106,7 @@ class ICameraController : virtual public core::IReferenceCounted inline void move(float32_t3 delta) { - m_position += mul(m_orthonormal, delta); + m_position += delta; } inline void reset() @@ -217,15 +217,8 @@ class ICameraController : virtual public core::IReferenceCounted const auto dt = std::chrono::duration_cast(m_nextPresentationTimeStamp - ev.timeStamp).count(); assert(dt >= 0); - if (ev.action == nbl::ui::SKeyboardEvent::ECA_PRESSED && !m_keysDown[virtualKey]) - { - m_keysDown[virtualKey] = true; + if (ev.action == nbl::ui::SKeyboardEvent::ECA_PRESSED) output.emplace_back(CVirtualEvent{ virtualKey, static_cast(dt) }); - } - else if (ev.action == nbl::ui::SKeyboardEvent::ECA_RELEASED) - { - m_keysDown[virtualKey] = false; - } } } } From 384674c25551a282e693b4d680be3a7a21c5ed84 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Mon, 28 Oct 2024 18:17:35 +0100 Subject: [PATCH 019/205] small typo --- common/include/CCamera.hpp | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/common/include/CCamera.hpp b/common/include/CCamera.hpp index 9ccd59521..7a49fffe1 100644 --- a/common/include/CCamera.hpp +++ b/common/include/CCamera.hpp @@ -34,11 +34,8 @@ class Camera final : public ICamera assert(gimbal); // TODO assert(projection); // TODO - const auto [forward, up, right] = std::make_tuple(gimbal->getZAxis(), gimbal->getYAxis(), gimbal->getXAxis()); const bool isLeftHanded = projection->isLeftHanded(); // TODO? - const auto moveDirection = float32_t3(gimbal->getOrientation() * glm::vec3(0.0f, 0.0f, 1.0f)); - constexpr auto MoveSpeedScale = 0.003f; constexpr auto RotateSpeedScale = 0.003f; @@ -50,6 +47,7 @@ class Camera final : public ICamera for (const traits_t::controller_virtual_event_t& ev : virtualEvents) { + const auto& forward = gimbal->getZAxis(), up = gimbal->getYAxis(), right = gimbal->getXAxis(); const float dMoveValue = ev.value * dMoveFactor; const float dRotateValue = ev.value * dRotateFactor; @@ -57,12 +55,12 @@ class Camera final : public ICamera { case traits_t::controller_t::MoveForward: { - gimbal->move(moveDirection * dMoveValue); + gimbal->move(forward * dMoveValue); } break; case traits_t::controller_t::MoveBackward: { - gimbal->move(moveDirection * (-dMoveValue)); + gimbal->move(forward * (-dMoveValue)); } break; case traits_t::controller_t::MoveRight: From 7d582bc99e67e5c9dcab813b84159176a2cfd9f4 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Tue, 29 Oct 2024 16:48:57 +0100 Subject: [PATCH 020/205] accumulate move & rotation deltas, add setOrientation & correct processKeyboard a bit --- common/include/CCamera.hpp | 114 ++++++++++------------- common/include/camera/ICameraControl.hpp | 55 ++++++++--- 2 files changed, 92 insertions(+), 77 deletions(-) diff --git a/common/include/CCamera.hpp b/common/include/CCamera.hpp index 7a49fffe1..31a105824 100644 --- a/common/include/CCamera.hpp +++ b/common/include/CCamera.hpp @@ -26,80 +26,68 @@ class Camera final : public ICamera } ~Camera() = default; - virtual void manipulate(std::span virtualEvents) override - { + virtual void manipulate(std::span virtualEvents) override + { auto* gimbal = traits_t::controller_t::m_gimbal.get(); - auto* projection = base_t::getProjection(); + assert(gimbal); - assert(gimbal); // TODO - assert(projection); // TODO + constexpr float MoveSpeedScale = 0.01f, RotateSpeedScale = 0.003f, MaxVerticalAngle = glm::radians(88.0f), MinVerticalAngle = -MaxVerticalAngle; + const auto& gForward = gimbal->getZAxis(), gRight = gimbal->getXAxis(); - const bool isLeftHanded = projection->isLeftHanded(); // TODO? - - constexpr auto MoveSpeedScale = 0.003f; - constexpr auto RotateSpeedScale = 0.003f; - - const auto dMoveFactor = traits_t::controller_t::m_moveSpeed * MoveSpeedScale; - const auto dRotateFactor = traits_t::controller_t::m_rotateSpeed * RotateSpeedScale; - - // TODO: UB/LB for pitch [-88,88]!!! we are not in cosmos but handle FPS camera in default case - // TODO: accumulate move & rotate scalars then do single move & rotate gimbal manipulation + struct + { + float dPitch = 0.f, dYaw = 0.f; + float32_t3 dMove = { 0.f, 0.f, 0.f }; + } accumulated; - for (const traits_t::controller_virtual_event_t& ev : virtualEvents) + for (const auto& event : virtualEvents) { - const auto& forward = gimbal->getZAxis(), up = gimbal->getYAxis(), right = gimbal->getXAxis(); - const float dMoveValue = ev.value * dMoveFactor; - const float dRotateValue = ev.value * dRotateFactor; + const float moveScalar = event.value * MoveSpeedScale; + const float rotateScalar = event.value * RotateSpeedScale; - switch (ev.type) + switch (event.type) { - case traits_t::controller_t::MoveForward: - { - gimbal->move(forward * dMoveValue); - } break; - - case traits_t::controller_t::MoveBackward: - { - gimbal->move(forward * (-dMoveValue)); - } break; - - case traits_t::controller_t::MoveRight: - { - gimbal->move(right * dMoveValue); - } break; - - case traits_t::controller_t::MoveLeft: - { - gimbal->move(right * (-dMoveValue)); - } break; - - case traits_t::controller_t::TiltUp: - { - gimbal->rotate(right, dRotateValue); - } break; - - case traits_t::controller_t::TiltDown: - { - gimbal->rotate(right, -dRotateValue); - } break; - - case traits_t::controller_t::PanRight: - { - gimbal->rotate(up, dRotateValue); - } break; - - case traits_t::controller_t::PanLeft: - { - gimbal->rotate(up, -dRotateValue); - } break; - - default: - continue; + case traits_t::controller_t::MoveForward: + accumulated.dMove += gForward * moveScalar; + break; + case traits_t::controller_t::MoveBackward: + accumulated.dMove -= gForward * moveScalar; + break; + case traits_t::controller_t::MoveRight: + accumulated.dMove += gRight * moveScalar; + break; + case traits_t::controller_t::MoveLeft: + accumulated.dMove -= gRight * moveScalar; + break; + case traits_t::controller_t::TiltUp: + accumulated.dPitch += rotateScalar; + break; + case traits_t::controller_t::TiltDown: + accumulated.dPitch -= rotateScalar; + break; + case traits_t::controller_t::PanRight: + accumulated.dYaw += rotateScalar; + break; + case traits_t::controller_t::PanLeft: + accumulated.dYaw -= rotateScalar; + break; + default: + break; } } + float currentPitch = atan2(glm::length(glm::vec2(gForward.x, gForward.z)), gForward.y) - glm::half_pi(); + float currentYaw = atan2(gForward.x, gForward.z); + + currentPitch = std::clamp(currentPitch + accumulated.dPitch, MinVerticalAngle, MaxVerticalAngle); + currentYaw += accumulated.dYaw; + + glm::quat orientation = glm::quat(glm::vec3(currentPitch, currentYaw, 0.0f)); + gimbal->setOrientation(orientation); + gimbal->move(accumulated.dMove); + base_t::recomputeViewMatrix(); - } + } }; } diff --git a/common/include/camera/ICameraControl.hpp b/common/include/camera/ICameraControl.hpp index fe248c76d..8503746da 100644 --- a/common/include/camera/ICameraControl.hpp +++ b/common/include/camera/ICameraControl.hpp @@ -82,6 +82,12 @@ class ICameraController : virtual public core::IReferenceCounted m_position = position; } + inline void setOrientation(const glm::quat& orientation) + { + m_orientation = glm::normalize(orientation); + updateOrthonormalMatrix(); + } + inline void rotate(const float32_t3& axis, float dRadians) { glm::quat dRotation = glm::angleAxis(dRadians, axis); @@ -198,29 +204,50 @@ class ICameraController : virtual public core::IReferenceCounted */ std::vector processKeyboard(std::span events) { + if (events.empty()) + return {}; + std::vector output; - for (const auto& ev : events) + constexpr auto NblVirtualKeys = std::to_array({ MoveForward, MoveBackward, MoveLeft, MoveRight, MoveUp, MoveDown, TiltUp, TiltDown, PanLeft, PanRight, RollLeft, RollRight, Reset }); + static_assert(NblVirtualKeys.size() == EventsCount); + + for (const auto virtualKey : NblVirtualKeys) { - constexpr auto NblVirtualKeys = std::to_array({ MoveForward, MoveBackward, MoveLeft, MoveRight, MoveUp, MoveDown, TiltUp, TiltDown, PanLeft, PanRight, RollLeft, RollRight, Reset }); - static_assert(NblVirtualKeys.size() == EventsCount); + const auto code = m_keysToEvent[virtualKey]; + bool& keyDown = m_keysDown[virtualKey]; - for (const auto virtualKey : NblVirtualKeys) + using virtual_key_state_t = std::tuple; + + auto updateVirtualState = [&]() -> virtual_key_state_t { - const auto code = m_keysToEvent[virtualKey]; + virtual_key_state_t state = { ui::E_KEY_CODE::EKC_NONE, false, 0.f }; - if (ev.keyCode == code) + for (const auto& ev : events) // TODO: improve the search { - if (code == ui::EKC_NONE) - continue; + if (ev.keyCode == code) + { + if (ev.action == nbl::ui::SKeyboardEvent::ECA_PRESSED && !keyDown) + keyDown = true; + else if (ev.action == nbl::ui::SKeyboardEvent::ECA_RELEASED) + keyDown = false; + + const auto dt = std::chrono::duration_cast(m_nextPresentationTimeStamp - ev.timeStamp).count(); + assert(dt >= 0); + + state = std::make_tuple(code, keyDown, dt); + break; + } + } + + return state; + }; - const auto dt = std::chrono::duration_cast(m_nextPresentationTimeStamp - ev.timeStamp).count(); - assert(dt >= 0); + const auto&& [physicalKey, isDown, dtAction] = updateVirtualState(); - if (ev.action == nbl::ui::SKeyboardEvent::ECA_PRESSED) - output.emplace_back(CVirtualEvent{ virtualKey, static_cast(dt) }); - } - } + if (physicalKey != ui::E_KEY_CODE::EKC_NONE) + if (isDown) + output.emplace_back(CVirtualEvent{ virtualKey, static_cast(dtAction) }); } return output; From 8ef623f781e1f2ab74d4d1631182d3adfd07fd7c Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Wed, 30 Oct 2024 07:46:11 +0100 Subject: [PATCH 021/205] remove old test methods --- common/include/camera/ICameraControl.hpp | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/common/include/camera/ICameraControl.hpp b/common/include/camera/ICameraControl.hpp index 8503746da..cafbbf1b0 100644 --- a/common/include/camera/ICameraControl.hpp +++ b/common/include/camera/ICameraControl.hpp @@ -95,21 +95,6 @@ class ICameraController : virtual public core::IReferenceCounted updateOrthonormalMatrix(); } - inline void strafe(float distance) - { - move({ 0.f, distance, 0.f }); - } - - inline void climb(float distance) - { - move({ 0.f, 0.f, distance }); - } - - inline void advance(float distance) - { - move({ distance, 0.f, 0.f }); - } - inline void move(float32_t3 delta) { m_position += delta; From 94dffeb16b921c6a2d323285f9099d5b6cee5cca Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Sat, 2 Nov 2024 15:49:33 +0100 Subject: [PATCH 022/205] remove gimbal member from camera interface, have matrix precision typename, rename CCamera to CFPSCamera, create GeneralPurposeRange and update sources, remove unused files, add some concepts, mark notes & todos --- 61_UI/include/common.hpp | 2 +- 61_UI/main.cpp | 45 +++++++------- .../include/{CCamera.hpp => CFPSCamera.hpp} | 33 ++++++----- common/include/ICamera.hpp | 58 ++++++------------- common/include/IRange.hpp | 29 ++++++++++ common/include/camera/CVirtualCameraEvent.hpp | 12 ---- common/include/camera/ICameraControl.hpp | 45 +++++++++++--- common/include/camera/IProjection.hpp | 35 ++++++----- 8 files changed, 147 insertions(+), 112 deletions(-) rename common/include/{CCamera.hpp => CFPSCamera.hpp} (77%) create mode 100644 common/include/IRange.hpp delete mode 100644 common/include/camera/CVirtualCameraEvent.hpp diff --git a/61_UI/include/common.hpp b/61_UI/include/common.hpp index a5def7551..cf8afeea8 100644 --- a/61_UI/include/common.hpp +++ b/61_UI/include/common.hpp @@ -4,7 +4,7 @@ #include // common api -#include "CCamera.hpp" +#include "CFPSCamera.hpp" #include "SimpleWindowedApplication.hpp" #include "CEventCallback.hpp" diff --git a/61_UI/main.cpp b/61_UI/main.cpp index 9e65776a7..b9a72c618 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -3,16 +3,13 @@ // For conditions of distribution and use, see copyright notice in nabla.h #include "common.hpp" - #include "camera/CCubeProjection.hpp" -#include "camera/ICameraControl.hpp" #include "glm/glm/ext/matrix_clip_space.hpp" // TODO: TESTING // FPS Camera, TESTS -using projection_matrix_t = float32_t4x4; -using camera_t = Camera; -using gimbal_t = camera_t::CGimbal; -using projection_t = camera_t::base_t::projection_t; +using matrix_precision_t = float32_t; +using camera_t = CFPSCamera; +using projection_t = camera_t::traits_t::projection_t; /* Renders scene texture to an offline @@ -182,18 +179,18 @@ class UISampleApp final : public examples::SimpleWindowedApplication if (isPerspective) { if (isLH) - projection->setMatrix(buildProjectionMatrixPerspectiveFovLH(glm::radians(fov), io.DisplaySize.x / io.DisplaySize.y, zNear, zFar)); + projection->setMatrix(buildProjectionMatrixPerspectiveFovLH(glm::radians(fov), io.DisplaySize.x / io.DisplaySize.y, zNear, zFar)); else - projection->setMatrix(buildProjectionMatrixPerspectiveFovRH(glm::radians(fov), io.DisplaySize.x / io.DisplaySize.y, zNear, zFar)); + projection->setMatrix(buildProjectionMatrixPerspectiveFovRH(glm::radians(fov), io.DisplaySize.x / io.DisplaySize.y, zNear, zFar)); } else { float viewHeight = viewWidth * io.DisplaySize.y / io.DisplaySize.x; if (isLH) - projection->setMatrix(buildProjectionMatrixOrthoLH(viewWidth, viewHeight, zNear, zFar)); + projection->setMatrix(buildProjectionMatrixOrthoLH(viewWidth, viewHeight, zNear, zFar)); else - projection->setMatrix(buildProjectionMatrixOrthoRH(viewWidth, viewHeight, zNear, zFar)); + projection->setMatrix(buildProjectionMatrixOrthoRH(viewWidth, viewHeight, zNear, zFar)); } } @@ -257,8 +254,11 @@ class UISampleApp final : public examples::SimpleWindowedApplication float32_t3 cameraPosition(cosf(camYAngle)* cosf(camXAngle)* transformParams.camDistance, sinf(camXAngle)* transformParams.camDistance, sinf(camYAngle)* cosf(camXAngle)* transformParams.camDistance); float32_t3 cameraTarget(0.f, 0.f, 0.f); - gimbal->setPosition(cameraPosition); - camera->setTarget(cameraTarget); + // TODO: lets generate events and make it + // happen purely on gimbal manipulation! + + //camera->getGimbal()->setPosition(cameraPosition); + //camera->getGimbal()->setTarget(cameraTarget); firstFrame = false; } @@ -410,14 +410,14 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGui::Separator(); }; - auto& orientation = gimbal->getOrthonornalMatrix(); + const auto& orientation = camera->getGimbal().getOrthonornalMatrix(); addMatrixTable("Model Matrix", "ModelMatrixTable", 3, 4, &pass.scene->object.model[0][0]); - addMatrixTable("Right", "OrientationRightVec", 1, 3, &gimbal->getXAxis()[0]); - addMatrixTable("Up", "OrientationUpVec", 1, 3, &gimbal->getYAxis()[0]); - addMatrixTable("Forward", "OrientationForwardVec", 1, 3, &gimbal->getZAxis()[0]); - addMatrixTable("Position", "PositionForwardVec", 1, 3, &gimbal->getPosition()[0]); + addMatrixTable("Right", "OrientationRightVec", 1, 3, &camera->getGimbal().getXAxis()[0]); + addMatrixTable("Up", "OrientationUpVec", 1, 3, &camera->getGimbal().getYAxis()[0]); + addMatrixTable("Forward", "OrientationForwardVec", 1, 3, &camera->getGimbal().getZAxis()[0]); + addMatrixTable("Position", "PositionForwardVec", 1, 3, &camera->getGimbal().getPosition()[0]); //addMatrixTable("Camera Gimbal Orientation Matrix", "OrientationMatrixTable", 3, 3, &orientation[0][0]); addMatrixTable("Camera Gimbal View Matrix", "ViewMatrixTable", 3, 4, &view[0][0]); @@ -508,14 +508,12 @@ class UISampleApp final : public examples::SimpleWindowedApplication TESTS, TODO: remove all once finished work & integrate with the example properly */ - const float32_t3 position(cosf(camYAngle)* cosf(camXAngle)* transformParams.camDistance, sinf(camXAngle)* transformParams.camDistance, sinf(camYAngle)* cosf(camXAngle)* transformParams.camDistance), - target(0.f, 0.f, 0.f); + const float32_t3 position(cosf(camYAngle)* cosf(camXAngle)* transformParams.camDistance, sinf(camXAngle)* transformParams.camDistance, sinf(camYAngle)* cosf(camXAngle)* transformParams.camDistance); auto projection = make_smart_refctd_ptr(); - projection->setMatrix(buildProjectionMatrixPerspectiveFovLH(glm::radians(fov), float(m_window->getWidth()) / float(m_window->getHeight()), zNear, zFar)); + projection->setMatrix(buildProjectionMatrixPerspectiveFovLH(glm::radians(fov), float(m_window->getWidth()) / float(m_window->getHeight()), zNear, zFar)); - gimbal = make_smart_refctd_ptr(position); - camera = make_smart_refctd_ptr(core::smart_refctd_ptr(gimbal), core::smart_refctd_ptr(projection), target); + camera = make_smart_refctd_ptr(core::smart_refctd_ptr(projection), position); return true; } @@ -813,8 +811,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication C_UI ui; } pass; - core::smart_refctd_ptr gimbal; - core::smart_refctd_ptr> camera; + core::smart_refctd_ptr> camera; video::CDumbPresentationOracle oracle; 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 diff --git a/common/include/CCamera.hpp b/common/include/CFPSCamera.hpp similarity index 77% rename from common/include/CCamera.hpp rename to common/include/CFPSCamera.hpp index 31a105824..8962a521c 100644 --- a/common/include/CCamera.hpp +++ b/common/include/CFPSCamera.hpp @@ -11,28 +11,30 @@ namespace nbl::hlsl // TODO: DIFFERENT NAMESPACE { // FPS Camera -template -class Camera final : public ICamera +template +class CFPSCamera final : public ICamera { public: - using base_t = ICamera; + using base_t = ICamera; using traits_t = typename base_t::Traits; - Camera(core::smart_refctd_ptr&& gimbal, core::smart_refctd_ptr projection, const float32_t3& target = { 0,0,0 }) - : base_t(core::smart_refctd_ptr(gimbal), core::smart_refctd_ptr(projection), target) + CFPSCamera(core::smart_refctd_ptr projection, const float32_t3& position, glm::quat orientation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f)) + : base_t(core::smart_refctd_ptr(projection)), m_gimbal(position, orientation) { traits_t::controller_t::initKeysToEvent(); - base_t::recomputeViewMatrix(); + base_t::recomputeViewMatrix(m_gimbal); } - ~Camera() = default; + ~CFPSCamera() = default; - virtual void manipulate(std::span virtualEvents) override + const typename traits_t::gimbal_t& getGimbal() override { - auto* gimbal = traits_t::controller_t::m_gimbal.get(); - assert(gimbal); + return m_gimbal; + } + virtual void manipulate(std::span virtualEvents) override + { constexpr float MoveSpeedScale = 0.01f, RotateSpeedScale = 0.003f, MaxVerticalAngle = glm::radians(88.0f), MinVerticalAngle = -MaxVerticalAngle; - const auto& gForward = gimbal->getZAxis(), gRight = gimbal->getXAxis(); + const auto& gForward = m_gimbal.getZAxis(), gRight = m_gimbal.getXAxis(); struct { @@ -83,11 +85,14 @@ class Camera final : public ICamera currentYaw += accumulated.dYaw; glm::quat orientation = glm::quat(glm::vec3(currentPitch, currentYaw, 0.0f)); - gimbal->setOrientation(orientation); - gimbal->move(accumulated.dMove); + m_gimbal.setOrientation(orientation); + m_gimbal.move(accumulated.dMove); - base_t::recomputeViewMatrix(); + base_t::recomputeViewMatrix(m_gimbal); } + +private: + traits_t::gimbal_t m_gimbal; }; } diff --git a/common/include/ICamera.hpp b/common/include/ICamera.hpp index 2f17fe79d..b7978d100 100644 --- a/common/include/ICamera.hpp +++ b/common/include/ICamera.hpp @@ -16,11 +16,11 @@ namespace nbl::hlsl // TODO: DIFFERENT NAMESPACE { -template -class ICamera : public ICameraController +template +class ICamera : public ICameraController { public: - using base_t = typename ICameraController; + using base_t = typename ICameraController; struct Traits { @@ -28,53 +28,33 @@ class ICamera : public ICameraController using projection_t = typename controller_t::projection_t; using gimbal_t = typename controller_t::CGimbal; using controller_virtual_event_t = typename controller_t::CVirtualEvent; + using matrix_precision_t = typename T; // TODO: actually all vectors/scalars should have precision type T and because of projection matrix constraints allowed is only float32_t & float64_t }; - ICamera(core::smart_refctd_ptr&& gimbal, core::smart_refctd_ptr projection, const float32_t3& target = {0,0,0}) - : base_t(core::smart_refctd_ptr(gimbal)), m_projection(core::smart_refctd_ptr(projection)) { setTarget(target); } + ICamera(core::smart_refctd_ptr projection) + : base_t(), m_projection(core::smart_refctd_ptr(projection)) {} ~ICamera() = default; - inline void setPosition(const float32_t3& position) - { - const auto* gimbal = base_t::m_gimbal.get(); - gimbal->setPosition(position); - recomputeViewMatrix(); - } - - inline void setTarget(const float32_t3& position) - { - m_target = position; + // NOTE: I dont like to make it virtual but if we assume we can + // have more gimbals & we dont store single one in the interface + // then one of them must be the one we model the camera view with + // eg "Follow Camera" -> range of 2 gimbals but only first + // models the camera view + virtual const Traits::gimbal_t& getGimbal() = 0u; - const auto* gimbal = base_t::m_gimbal.get(); - auto localTarget = m_target - gimbal->getPosition(); - - // TODO: use gimbal to perform a rotation! - - recomputeViewMatrix(); - } - - inline const float32_t3& getPosition() { return base_t::m_gimbal->getPosition(); } - inline const float32_t3& getTarget() { return m_target; } - inline const float32_t3x4& getViewMatrix() const { return m_viewMatrix; } + inline const matrix& getViewMatrix() const { return m_viewMatrix; } inline Traits::projection_t* getProjection() { return m_projection.get(); } protected: - inline void recomputeViewMatrix() + // Recomputes view matrix for a given gimbal. Note that a camera type implementation could have multiple gimbals - not all of them will be used to model the camera view itself but one + // (TODO: (*) unless? I guess its to decide if we talk bout single view or to allow to have view per gimbal, but imho we should have 1 -> could just spam more instances of camera type to cover more views) + inline void recomputeViewMatrix(Traits::gimbal_t& gimbal) { - // TODO: adjust for handedness (axes flip) - const bool isLeftHanded = m_projection->isLeftHanded(); - const auto* gimbal = base_t::m_gimbal.get(); - const auto& position = gimbal->getPosition(); - - const auto [xaxis, yaxis, zaxis] = std::make_tuple(gimbal->getXAxis(), gimbal->getYAxis(), gimbal->getZAxis()); - m_viewMatrix[0u] = float32_t4(xaxis, -hlsl::dot(xaxis, position)); - m_viewMatrix[1u] = float32_t4(yaxis, -hlsl::dot(yaxis, position)); - m_viewMatrix[2u] = float32_t4(zaxis, -hlsl::dot(zaxis, position)); + gimbal.computeViewMatrix(m_viewMatrix, m_projection->isLeftHanded()); } - const core::smart_refctd_ptr m_projection; - float32_t3x4 m_viewMatrix; - float32_t3 m_target; + const core::smart_refctd_ptr m_projection; // TODO: move it from here + matrix m_viewMatrix; }; } diff --git a/common/include/IRange.hpp b/common/include/IRange.hpp new file mode 100644 index 000000000..5c6fe751b --- /dev/null +++ b/common/include/IRange.hpp @@ -0,0 +1,29 @@ +#ifndef _NBL_IRANGE_HPP_ +#define _NBL_IRANGE_HPP_ + +namespace nbl::hlsl +{ + +template +concept GeneralPurposeRange = requires +{ + typename std::ranges::range_value_t; +}; + +//! Interface class for a general purpose range +template +class IRange +{ +public: + using range_t = Range; + using range_value_t = std::ranges::range_value_t; + + IRange(range_t&& range) : m_range(std::move(range)) {} + +protected: + range_t m_range; +}; + +} // namespace nbl::hlsl + +#endif // _NBL_IRANGE_HPP_ \ No newline at end of file diff --git a/common/include/camera/CVirtualCameraEvent.hpp b/common/include/camera/CVirtualCameraEvent.hpp deleted file mode 100644 index 309c91662..000000000 --- a/common/include/camera/CVirtualCameraEvent.hpp +++ /dev/null @@ -1,12 +0,0 @@ -#ifndef _NBL_VIRTUAL_CAMERA_EVENT_HPP_ -#define _NBL_VIRTUAL_CAMERA_EVENT_HPP_ - -#include "nbl/builtin/hlsl/cpp_compat/matrix.hlsl" - -// TODO: DIFFERENT NAMESPACE -namespace nbl::hlsl -{ - -} - -#endif // _NBL_VIRTUAL_CAMERA_EVENT_HPP_ \ No newline at end of file diff --git a/common/include/camera/ICameraControl.hpp b/common/include/camera/ICameraControl.hpp index cafbbf1b0..565b7970a 100644 --- a/common/include/camera/ICameraControl.hpp +++ b/common/include/camera/ICameraControl.hpp @@ -2,8 +2,6 @@ #define _NBL_I_CAMERA_CONTROLLER_HPP_ #include "IProjection.hpp" -#include "../ICamera.hpp" -#include "CVirtualCameraEvent.hpp" #include "glm/glm/ext/matrix_transform.hpp" // TODO: TEMPORARY!!! whatever used will be moved to cpp #include "glm/glm/gtc/quaternion.hpp" #include "nbl/builtin/hlsl/matrix_utils/transformation_matrix_utils.hlsl" @@ -12,11 +10,12 @@ namespace nbl::hlsl { -template +template class ICameraController : virtual public core::IReferenceCounted { public: - using projection_t = typename IProjection; + using matrix_precision_t = typename T; + using projection_t = typename IProjection>; enum VirtualEventType : uint8_t { @@ -71,7 +70,7 @@ class ICameraController : virtual public core::IReferenceCounted manipulation_encode_t value; }; - class CGimbal : virtual public core::IReferenceCounted + class CGimbal { public: CGimbal(const float32_t3& position, glm::quat orientation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f)) @@ -123,6 +122,14 @@ class ICameraController : virtual public core::IReferenceCounted // Base forward vector in orthonormal basis, base "forward" vector (Z-axis) inline const float32_t3& getZAxis() const { return m_orthonormal[2u]; } + inline void computeViewMatrix(matrix& output, bool isLeftHanded) + { + const auto&& [xaxis, yaxis, zaxis] = std::make_tuple(getXAxis(), getYAxis(), getZAxis() * (isLeftHanded ? 1.f : -1.f)); + output[0u] = vector(xaxis, -hlsl::dot(xaxis, m_position)); + output[1u] = vector(yaxis, -hlsl::dot(yaxis, m_position)); + output[2u] = vector(zaxis, -hlsl::dot(zaxis, m_position)); + } + private: inline void updateOrthonormalMatrix() { @@ -158,7 +165,7 @@ class ICameraController : virtual public core::IReferenceCounted float32_t3x3 m_orthonormal; }; - ICameraController(core::smart_refctd_ptr&& gimbal) : m_gimbal(core::smart_refctd_ptr(gimbal)) {} + ICameraController() {} // override controller keys map, it binds a key code to a virtual event void updateKeysToEvent(const std::vector& codes, VirtualEventType event) @@ -299,7 +306,6 @@ class ICameraController : virtual public core::IReferenceCounted m_keysToEvent[Reset] = ui::E_KEY_CODE::EKC_R ; } - core::smart_refctd_ptr m_gimbal; std::array m_keysToEvent = {}; float m_moveSpeed = 1.f, m_rotateSpeed = 1.f; bool m_keysDown[EventsCount] = {}; @@ -307,6 +313,31 @@ class ICameraController : virtual public core::IReferenceCounted std::chrono::microseconds m_nextPresentationTimeStamp = {}, m_lastVirtualUpTimeStamp = {}; }; +template +concept GimbalRange = GeneralPurposeRange && requires +{ + requires ProjectionMatrix::projection_t>; + requires std::same_as, typename ICameraController::projection_t>::CGimbal>; +}; + +template +class IGimbalRange : public IRange +{ +public: + using base_t = IRange; + using range_t = typename base_t::range_t; + using gimbal_t = typename base_t::range_value_t; + + IGimbalRange(range_t&& gimbals) : base_t(std::move(gimbals)) {} + inline const range_t& getGimbals() const { return base_t::m_range; } + +protected: + inline range_t& getGimbals() const { return base_t::m_range; } +}; + +// TODO NOTE: eg. "follow camera" should use GimbalRange::CGimbal, 2u>>, +// one per camera itself and one for target it follows + } // nbl::hlsl namespace #endif // _NBL_I_CAMERA_CONTROLLER_HPP_ \ No newline at end of file diff --git a/common/include/camera/IProjection.hpp b/common/include/camera/IProjection.hpp index 1384f8e87..8de5c19f6 100644 --- a/common/include/camera/IProjection.hpp +++ b/common/include/camera/IProjection.hpp @@ -2,12 +2,13 @@ #define _NBL_IPROJECTION_HPP_ #include "nbl/builtin/hlsl/cpp_compat/matrix.hlsl" +#include "IRange.hpp" namespace nbl::hlsl { template -concept ProjectionMatrix = is_any_of_v; +concept ProjectionMatrix = is_any_of_v; //! Interface class for projection template @@ -37,29 +38,33 @@ class IProjection : virtual public core::IReferenceCounted bool m_isLeftHanded; }; +template +struct is_projection : std::false_type {}; + +template +struct is_projection> : std::true_type {}; + +template +inline constexpr bool is_projection_v = is_projection::value; + template -concept ProjectionRange = requires +concept ProjectionRange = GeneralPurposeRange && requires { - typename std::ranges::range_value_t; - // TODO: smart_refctd_ptr check for range_value_t + its type check to grant IProjection + requires core::is_smart_refctd_ptr_v>; + requires is_projection_v::pointee>; }; //! Interface class for a range of IProjection projections template>, 1u>> -class IProjectionRange +class IProjectionRange : public IRange { public: - using range_t = Range; - using projection_t = std::ranges::range_value_t; - - //! Constructor for the range of projections - IProjectionRange(range_t&& projections) : m_projectionRange(std::move(projections)) {} - - //! Get the stored range of projections - const range_t& getProjections() const { return m_projectionRange; } + using base_t = IRange; + using range_t = typename base_t::range_t; + using projection_t = typename base_t::range_value_t; -protected: - range_t m_projectionRange; + IProjectionRange(range_t&& projections) : base_t(std::move(projections)) {} + const range_t& getProjections() const { return base_t::m_range; } }; } // namespace nbl::hlsl From ddb86a6725ed309dea04d4a4bc70ab7437ee9da3 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Sun, 3 Nov 2024 19:29:23 +0100 Subject: [PATCH 023/205] move virtual events outside templates into CVirtualGimbalEvent, have nice constexpr table to iterate over them, move view responsibility modeling to gimbal + make it optional! (yes - it's a good idea imo), keep manipulation state flag & manipulation counter to avoid updating the view too frequently, remove projection from cameras (projections will own/reference cameras), update a few structs, sources, clean some code, mark TODOs & notes --- 61_UI/main.cpp | 41 ++- common/include/CFPSCamera.hpp | 54 ++-- common/include/ICamera.hpp | 25 +- common/include/camera/ICameraControl.hpp | 338 ++++++++++++++--------- 4 files changed, 259 insertions(+), 199 deletions(-) diff --git a/61_UI/main.cpp b/61_UI/main.cpp index b9a72c618..1d6016830 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -9,7 +9,7 @@ // FPS Camera, TESTS using matrix_precision_t = float32_t; using camera_t = CFPSCamera; -using projection_t = camera_t::traits_t::projection_t; +using projection_t = IProjection>; // TODO: temporary -> projections will own/reference cameras /* Renders scene texture to an offline @@ -174,8 +174,6 @@ class UISampleApp final : public examples::SimpleWindowedApplication { ImGuiIO& io = ImGui::GetIO(); { - auto* projection = camera->getProjection(); - if (isPerspective) { if (isLH) @@ -329,10 +327,11 @@ class UISampleApp final : public examples::SimpleWindowedApplication float32_t4x4 view, projection, model; } imguizmoM16InOut; - const auto& projectionMatrix = camera->getProjection()->getMatrix(); + const auto& projectionMatrix = projection->getMatrix(); + const auto& view = camera->getGimbal().getView().value(); ImGuizmo::SetID(0u); - imguizmoM16InOut.view = transpose(getMatrix3x4As4x4(camera->getViewMatrix())); + imguizmoM16InOut.view = transpose(getMatrix3x4As4x4(view.matrix)); imguizmoM16InOut.projection = transpose(projectionMatrix); imguizmoM16InOut.model = transpose(getMatrix3x4As4x4(pass.scene->object.model)); { @@ -344,10 +343,9 @@ class UISampleApp final : public examples::SimpleWindowedApplication } // to Nabla + update camera & model matrices - const auto& view = camera->getViewMatrix(); // TODO: make it more nicely - const_cast(view) = float32_t3x4(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) + const_cast(view.matrix) = float32_t3x4(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) { static float32_t3x4 modelView, normal; static float32_t4x4 modelViewProjection; @@ -365,12 +363,12 @@ class UISampleApp final : public examples::SimpleWindowedApplication auto& ubo = hook.viewParameters; - modelView = concatenateBFollowedByA(view, hook.model); + modelView = concatenateBFollowedByA(view.matrix, hook.model); // TODO //modelView.getSub3x3InverseTranspose(normal); - auto concatMatrix = mul(projectionMatrix, getMatrix3x4As4x4(view)); + auto concatMatrix = mul(projectionMatrix, getMatrix3x4As4x4(view.matrix)); modelViewProjection = mul(concatMatrix, getMatrix3x4As4x4(hook.model)); memcpy(ubo.MVP, &modelViewProjection[0][0], sizeof(ubo.MVP)); @@ -420,7 +418,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication addMatrixTable("Position", "PositionForwardVec", 1, 3, &camera->getGimbal().getPosition()[0]); //addMatrixTable("Camera Gimbal Orientation Matrix", "OrientationMatrixTable", 3, 3, &orientation[0][0]); - addMatrixTable("Camera Gimbal View Matrix", "ViewMatrixTable", 3, 4, &view[0][0]); + addMatrixTable("Camera Gimbal View Matrix", "ViewMatrixTable", 3, 4, &view.matrix[0][0]); addMatrixTable("Camera Gimbal Projection Matrix", "ProjectionMatrixTable", 4, 4, &projectionMatrix[0][0], false); ImGui::End(); @@ -509,11 +507,8 @@ class UISampleApp final : public examples::SimpleWindowedApplication */ const float32_t3 position(cosf(camYAngle)* cosf(camXAngle)* transformParams.camDistance, sinf(camXAngle)* transformParams.camDistance, sinf(camYAngle)* cosf(camXAngle)* transformParams.camDistance); - - auto projection = make_smart_refctd_ptr(); projection->setMatrix(buildProjectionMatrixPerspectiveFovLH(glm::radians(fov), float(m_window->getWidth()) / float(m_window->getHeight()), zNear, zFar)); - - camera = make_smart_refctd_ptr(core::smart_refctd_ptr(projection), position); + camera = make_smart_refctd_ptr(position); return true; } @@ -692,9 +687,6 @@ class UISampleApp final : public examples::SimpleWindowedApplication inline void update() { - camera->setMoveSpeed(moveSpeed); - camera->setRotateSpeed(rotateSpeed); - static std::chrono::microseconds previousEventTimestamp{}; m_inputSystem->getDefaultMouse(&mouse); @@ -718,9 +710,6 @@ class UISampleApp final : public examples::SimpleWindowedApplication std::vector mouse{}; std::vector keyboard{}; } capturedEvents; - - if (move) - camera->begin(nextPresentationTimestamp); mouse.consumeEvents([&](const IMouseEventChannel::range_t& events) -> void { @@ -761,13 +750,14 @@ class UISampleApp final : public examples::SimpleWindowedApplication if (move) { - const auto virtualMouseEvents = camera->processMouse(params.mouseEvents); - const auto virtualKeyboardEvents = camera->processKeyboard(params.keyboardEvents); + static std::vector virtualMouseEvents(CVirtualGimbalEvent::VirtualEventsTypeTable.size()), virtualKeyboardEvents(CVirtualGimbalEvent::VirtualEventsTypeTable.size()); + uint32_t vEventsMouseCount, vEventsKeyboardCount; - camera->manipulate({ virtualMouseEvents.data(), virtualMouseEvents.size()}); - camera->manipulate({ virtualKeyboardEvents.data(), virtualKeyboardEvents.size()}); + camera->processMouse(virtualMouseEvents.data(), vEventsMouseCount, params.mouseEvents); + camera->processKeyboard(virtualKeyboardEvents.data(), vEventsKeyboardCount, params.keyboardEvents); - camera->end(nextPresentationTimestamp); + camera->manipulate({ virtualMouseEvents.data(), vEventsMouseCount }); + camera->manipulate({ virtualKeyboardEvents.data(), vEventsKeyboardCount }); } pass.ui.manager->update(params); @@ -811,6 +801,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication C_UI ui; } pass; + smart_refctd_ptr projection = make_smart_refctd_ptr(); // TMP! core::smart_refctd_ptr> camera; video::CDumbPresentationOracle oracle; diff --git a/common/include/CFPSCamera.hpp b/common/include/CFPSCamera.hpp index 8962a521c..58e07e1c0 100644 --- a/common/include/CFPSCamera.hpp +++ b/common/include/CFPSCamera.hpp @@ -18,11 +18,10 @@ class CFPSCamera final : public ICamera using base_t = ICamera; using traits_t = typename base_t::Traits; - CFPSCamera(core::smart_refctd_ptr projection, const float32_t3& position, glm::quat orientation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f)) - : base_t(core::smart_refctd_ptr(projection)), m_gimbal(position, orientation) + CFPSCamera(const float32_t3& position, glm::quat orientation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f)) + : base_t(), m_gimbal({ .position = position, .orientation = orientation, .withView = true }) { - traits_t::controller_t::initKeysToEvent(); - base_t::recomputeViewMatrix(m_gimbal); + initKeysToEvent(); } ~CFPSCamera() = default; @@ -31,8 +30,11 @@ class CFPSCamera final : public ICamera return m_gimbal; } - virtual void manipulate(std::span virtualEvents) override + virtual void manipulate(std::span virtualEvents) override { + if (!virtualEvents.size()) + return; + constexpr float MoveSpeedScale = 0.01f, RotateSpeedScale = 0.003f, MaxVerticalAngle = glm::radians(88.0f), MinVerticalAngle = -MaxVerticalAngle; const auto& gForward = m_gimbal.getZAxis(), gRight = m_gimbal.getXAxis(); @@ -44,33 +46,33 @@ class CFPSCamera final : public ICamera for (const auto& event : virtualEvents) { - const float moveScalar = event.value * MoveSpeedScale; - const float rotateScalar = event.value * RotateSpeedScale; + const float moveScalar = event.magnitude * MoveSpeedScale; + const float rotateScalar = event.magnitude * RotateSpeedScale; switch (event.type) { - case traits_t::controller_t::MoveForward: + case CVirtualGimbalEvent::MoveForward: accumulated.dMove += gForward * moveScalar; break; - case traits_t::controller_t::MoveBackward: + case CVirtualGimbalEvent::MoveBackward: accumulated.dMove -= gForward * moveScalar; break; - case traits_t::controller_t::MoveRight: + case CVirtualGimbalEvent::MoveRight: accumulated.dMove += gRight * moveScalar; break; - case traits_t::controller_t::MoveLeft: + case CVirtualGimbalEvent::MoveLeft: accumulated.dMove -= gRight * moveScalar; break; - case traits_t::controller_t::TiltUp: + case CVirtualGimbalEvent::TiltUp: accumulated.dPitch += rotateScalar; break; - case traits_t::controller_t::TiltDown: + case CVirtualGimbalEvent::TiltDown: accumulated.dPitch -= rotateScalar; break; - case traits_t::controller_t::PanRight: + case CVirtualGimbalEvent::PanRight: accumulated.dYaw += rotateScalar; break; - case traits_t::controller_t::PanLeft: + case CVirtualGimbalEvent::PanLeft: accumulated.dYaw -= rotateScalar; break; default: @@ -85,13 +87,29 @@ class CFPSCamera final : public ICamera currentYaw += accumulated.dYaw; glm::quat orientation = glm::quat(glm::vec3(currentPitch, currentYaw, 0.0f)); - m_gimbal.setOrientation(orientation); - m_gimbal.move(accumulated.dMove); - base_t::recomputeViewMatrix(m_gimbal); + m_gimbal.begin(); + { + m_gimbal.setOrientation(orientation); + m_gimbal.move(accumulated.dMove); + } + m_gimbal.end(); } private: + void initKeysToEvent() override + { + traits_t::controller_t::updateKeysToEvent([](CVirtualGimbalEvent::keys_to_virtual_events_t& keys) + { + keys[CVirtualGimbalEvent::MoveForward] = ui::E_KEY_CODE::EKC_W; + keys[CVirtualGimbalEvent::MoveBackward] = ui::E_KEY_CODE::EKC_S; + keys[CVirtualGimbalEvent::MoveLeft] = ui::E_KEY_CODE::EKC_A; + keys[CVirtualGimbalEvent::MoveRight] = ui::E_KEY_CODE::EKC_D; + keys[CVirtualGimbalEvent::MoveUp] = ui::E_KEY_CODE::EKC_SPACE; + keys[CVirtualGimbalEvent::MoveDown] = ui::E_KEY_CODE::EKC_LEFT_SHIFT; + }); + } + traits_t::gimbal_t m_gimbal; }; diff --git a/common/include/ICamera.hpp b/common/include/ICamera.hpp index b7978d100..4faebe536 100644 --- a/common/include/ICamera.hpp +++ b/common/include/ICamera.hpp @@ -25,36 +25,15 @@ class ICamera : public ICameraController struct Traits { using controller_t = base_t; - using projection_t = typename controller_t::projection_t; using gimbal_t = typename controller_t::CGimbal; - using controller_virtual_event_t = typename controller_t::CVirtualEvent; using matrix_precision_t = typename T; // TODO: actually all vectors/scalars should have precision type T and because of projection matrix constraints allowed is only float32_t & float64_t }; - ICamera(core::smart_refctd_ptr projection) - : base_t(), m_projection(core::smart_refctd_ptr(projection)) {} + ICamera() : base_t() {} ~ICamera() = default; - // NOTE: I dont like to make it virtual but if we assume we can - // have more gimbals & we dont store single one in the interface - // then one of them must be the one we model the camera view with - // eg "Follow Camera" -> range of 2 gimbals but only first - // models the camera view + // Returns a gimbal which *models the camera view*, note that a camera type implementation may have multiple gimbals under the hood virtual const Traits::gimbal_t& getGimbal() = 0u; - - inline const matrix& getViewMatrix() const { return m_viewMatrix; } - inline Traits::projection_t* getProjection() { return m_projection.get(); } - -protected: - // Recomputes view matrix for a given gimbal. Note that a camera type implementation could have multiple gimbals - not all of them will be used to model the camera view itself but one - // (TODO: (*) unless? I guess its to decide if we talk bout single view or to allow to have view per gimbal, but imho we should have 1 -> could just spam more instances of camera type to cover more views) - inline void recomputeViewMatrix(Traits::gimbal_t& gimbal) - { - gimbal.computeViewMatrix(m_viewMatrix, m_projection->isLeftHanded()); - } - - const core::smart_refctd_ptr m_projection; // TODO: move it from here - matrix m_viewMatrix; }; } diff --git a/common/include/camera/ICameraControl.hpp b/common/include/camera/ICameraControl.hpp index 565b7970a..ff918faa4 100644 --- a/common/include/camera/ICameraControl.hpp +++ b/common/include/camera/ICameraControl.hpp @@ -10,13 +10,9 @@ namespace nbl::hlsl { -template -class ICameraController : virtual public core::IReferenceCounted +struct CVirtualGimbalEvent { -public: - using matrix_precision_t = typename T; - using projection_t = typename IProjection>; - + //! Virtual event representing a gimbal manipulation enum VirtualEventType : uint8_t { // Strafe forward @@ -55,53 +51,125 @@ class ICameraController : virtual public core::IReferenceCounted // Roll the camera clockwise around the forward axis (roll) RollRight, - // Reset the camera to the default state - Reset, - EventsCount }; - //! Virtual event representing a manipulation - struct CVirtualEvent + using manipulation_encode_t = float64_t; + using keys_to_virtual_events_t = std::array; + + VirtualEventType type; + manipulation_encode_t magnitude; + + static inline constexpr auto VirtualEventsTypeTable = []() { - using manipulation_encode_t = float64_t; + std::array output; - VirtualEventType type; - manipulation_encode_t value; - }; + for (uint16_t i = 0u; i < EventsCount - 1u; ++i) + { + output[i] = static_cast(i); + } + + return output; + }(); +}; + +template +class ICameraController : virtual public core::IReferenceCounted +{ +public: + using matrix_precision_t = typename T; class CGimbal { public: - CGimbal(const float32_t3& position, glm::quat orientation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f)) - : m_position(position), m_orientation(orientation) { updateOrthonormalMatrix(); } + struct SCreationParameters + { + float32_t3 position; + glm::quat orientation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f); + bool withView = true; + }; + + // Gimbal's view matrix consists of an orthonormal basis (https://en.wikipedia.org/wiki/Orthonormal_basis) + // for orientation and a translation component that positions the world relative to the gimbal's position. + // Any camera type is supposed to manipulate a position & orientation of a gimbal + // with "virtual events" which model its view bound to the camera + struct SView + { + matrix matrix = {}; + bool isLeftHandSystem = true; + }; + + CGimbal(SCreationParameters&& parameters) + : m_position(parameters.position), m_orientation(parameters.orientation) + { + updateOrthonormalOrientationBase(); + + if (parameters.withView) + { + m_view = std::optional(SView{}); // RVO + updateView(); + } + } + + void begin() + { + m_isManipulating = true; + m_counter = 0u; + } inline void setPosition(const float32_t3& position) { + assert(m_isManipulating); // TODO: log error and return without doing nothing + + if (m_position != position) + m_counter++; + m_position = position; } inline void setOrientation(const glm::quat& orientation) { + assert(m_isManipulating); // TODO: log error and return without doing nothing + + if(m_orientation != orientation) + m_counter++; + m_orientation = glm::normalize(orientation); - updateOrthonormalMatrix(); + updateOrthonormalOrientationBase(); } inline void rotate(const float32_t3& axis, float dRadians) { + assert(m_isManipulating); // TODO: log error and return without doing nothing + + if(dRadians) + m_counter++; + glm::quat dRotation = glm::angleAxis(dRadians, axis); m_orientation = glm::normalize(dRotation * m_orientation); - updateOrthonormalMatrix(); + updateOrthonormalOrientationBase(); } inline void move(float32_t3 delta) { - m_position += delta; + assert(m_isManipulating); // TODO: log error and return without doing nothing + + auto newPosition = m_position + delta; + + if (newPosition != m_position) + m_counter++; + + m_position = newPosition; } - inline void reset() + void end() { - // TODO + m_isManipulating = false; + + if (m_counter > 0u) + updateView(); + + m_counter = 0u; } // Position of gimbal @@ -110,104 +178,113 @@ class ICameraController : virtual public core::IReferenceCounted // Orientation of gimbal inline const glm::quat& getOrientation() const { return m_orientation; } - // Orthonormal [getXAxis(), getYAxis(), getZAxis()] matrix + // Orthonormal [getXAxis(), getYAxis(), getZAxis()] orientation matrix inline const float32_t3x3& getOrthonornalMatrix() const { return m_orthonormal; } - // Base right vector in orthonormal basis, base "right" vector (X-axis) + // Base "right" vector in orthonormal orientation basis (X-axis) inline const float32_t3& getXAxis() const { return m_orthonormal[0u]; } - // Base up vector in orthonormal basis, base "up" vector (Y-axis) + // Base "up" vector in orthonormal orientation basis (Y-axis) inline const float32_t3& getYAxis() const { return m_orthonormal[1u]; } - // Base forward vector in orthonormal basis, base "forward" vector (Z-axis) + // Base "forward" vector in orthonormal orientation basis (Z-axis) inline const float32_t3& getZAxis() const { return m_orthonormal[2u]; } - inline void computeViewMatrix(matrix& output, bool isLeftHanded) + // Optional view of a gimbal + inline const std::optional& getView() const { return m_view; } + + inline const size_t& getManipulationCounter() { return m_counter; } + inline bool isManipulating() const { return m_isManipulating; } + + private: + inline void updateOrthonormalOrientationBase() { - const auto&& [xaxis, yaxis, zaxis] = std::make_tuple(getXAxis(), getYAxis(), getZAxis() * (isLeftHanded ? 1.f : -1.f)); - output[0u] = vector(xaxis, -hlsl::dot(xaxis, m_position)); - output[1u] = vector(yaxis, -hlsl::dot(yaxis, m_position)); - output[2u] = vector(zaxis, -hlsl::dot(zaxis, m_position)); + m_orthonormal = matrix(glm::mat3_cast(glm::normalize(m_orientation))); } - private: - inline void updateOrthonormalMatrix() - { - m_orthonormal = float32_t3x3(glm::mat3_cast(glm::normalize(m_orientation))); + inline void updateView() + { + if (m_view.has_value()) // TODO: this could be templated + constexpr actually if gimbal doesn't init this on runtime depending on sth + { + auto& view = m_view.value(); + const auto& gRight = getXAxis(), gUp = getYAxis(), gForward = getZAxis(); - // DEBUG - const auto [xaxis, yaxis, zaxis] = std::make_tuple(getXAxis(),getYAxis(), getZAxis()); + // TODO: I think I will provide convert utility allowing to go from one hand system to another, its just a matter to take care of m_view->matrix[2u] to perform a LH/RH flip + // in general this should not know about projections which are now supposed to be independent and store reference to a camera (or own it) + view.isLeftHandSystem = hlsl::determinant(m_orthonormal) < 0.0f; - auto isNormalized = [](const auto& v, float epsilon) -> bool - { - return glm::epsilonEqual(glm::length(v), 1.0f, epsilon); - }; + auto isNormalized = [](const auto& v, float epsilon) -> bool + { + return glm::epsilonEqual(glm::length(v), 1.0f, epsilon); + }; - auto isOrthogonal = [](const auto& a, const auto& b, float epsilon) -> bool - { - return glm::epsilonEqual(glm::dot(a, b), 0.0f, epsilon); - }; + auto isOrthogonal = [](const auto& a, const auto& b, float epsilon) -> bool + { + return glm::epsilonEqual(glm::dot(a, b), 0.0f, epsilon); + }; - auto isOrthoBase = [&](const auto& x, const auto& y, const auto& z, float epsilon = 1e-6f) -> bool - { - return isNormalized(x, epsilon) && isNormalized(y, epsilon) && isNormalized(z, epsilon) && - isOrthogonal(x, y, epsilon) && isOrthogonal(x, z, epsilon) && isOrthogonal(y, z, epsilon); - }; + auto isOrthoBase = [&](const auto& x, const auto& y, const auto& z, float epsilon = 1e-6f) -> bool + { + return isNormalized(x, epsilon) && isNormalized(y, epsilon) && isNormalized(z, epsilon) && + isOrthogonal(x, y, epsilon) && isOrthogonal(x, z, epsilon) && isOrthogonal(y, z, epsilon); + }; + + assert(isOrthoBase(gRight, gUp, gForward)); - assert(isOrthoBase(xaxis, yaxis, zaxis)); + view.matrix[0u] = vector(gRight, -glm::dot(gRight, m_position)); + view.matrix[1u] = vector(gUp, -glm::dot(gUp, m_position)); + view.matrix[2u] = vector(gForward, -glm::dot(gForward, m_position)); + } } float32_t3 m_position; glm::quat m_orientation; + matrix m_orthonormal; + + // For a camera implementation at least one gimbal models its view but not all gimbals (if multiple) are expected to do so + std::optional m_view = std::nullopt; + + // Counts *performed* manipulations, a manipulation with 0 delta is not counted! + size_t m_counter = {}; - // Represents the camera's orthonormal basis - // https://en.wikipedia.org/wiki/Orthonormal_basis - float32_t3x3 m_orthonormal; + // Records manipulation state + bool m_isManipulating = false; }; ICameraController() {} - // override controller keys map, it binds a key code to a virtual event - void updateKeysToEvent(const std::vector& codes, VirtualEventType event) + // Binds key codes to virtual events, the mapKeys lambda will be executed with controller CVirtualGimbalEvent::keys_to_virtual_events_t table + void updateKeysToEvent(const std::function& mapKeys) { - m_keysToEvent[event] = std::move(codes); + mapKeys(m_keysToVirtualEvents); } - // start controller manipulation session - virtual void begin(std::chrono::microseconds nextPresentationTimeStamp) - { - m_nextPresentationTimeStamp = nextPresentationTimeStamp; - return; - } + // Manipulates camera with view gimbal & virtual events + virtual void manipulate(std::span virtualEvents) = 0; - // manipulate camera with gimbal & virtual events, begin must be called before that! - virtual void manipulate(std::span virtualEvents) = 0; + // TODO: *maybe* would be good to have a class interface for virtual event generators, + // eg keyboard, mouse but maybe custom stuff too eg events from gimbal drag & drop - // finish controller manipulation session, call after last manipulate in the hot loop - void end(std::chrono::microseconds nextPresentationTimeStamp) + // Processes keyboard events to generate virtual manipulation events, note that it doesn't make the manipulation itself! + void processKeyboard(CVirtualGimbalEvent* output, uint32_t& count, std::span events) { - m_lastVirtualUpTimeStamp = nextPresentationTimeStamp; - } + if (!output) + { + count = CVirtualGimbalEvent::EventsCount; + return; + } - /* - // process keyboard to generate virtual manipulation events - // note that: - // - it doesn't make the manipulation itself! - */ - std::vector processKeyboard(std::span events) - { - if (events.empty()) - return {}; + count = 0u; - std::vector output; + if (events.empty()) + return; - constexpr auto NblVirtualKeys = std::to_array({ MoveForward, MoveBackward, MoveLeft, MoveRight, MoveUp, MoveDown, TiltUp, TiltDown, PanLeft, PanRight, RollLeft, RollRight, Reset }); - static_assert(NblVirtualKeys.size() == EventsCount); + const auto timestamp = getEventGenerationTimestamp(); - for (const auto virtualKey : NblVirtualKeys) + for (const auto virtualEventType : CVirtualGimbalEvent::VirtualEventsTypeTable) { - const auto code = m_keysToEvent[virtualKey]; - bool& keyDown = m_keysDown[virtualKey]; + const auto code = m_keysToVirtualEvents[virtualEventType]; + bool& keyDown = m_keysDown[virtualEventType]; using virtual_key_state_t = std::tuple; @@ -224,7 +301,7 @@ class ICameraController : virtual public core::IReferenceCounted else if (ev.action == nbl::ui::SKeyboardEvent::ECA_RELEASED) keyDown = false; - const auto dt = std::chrono::duration_cast(m_nextPresentationTimeStamp - ev.timeStamp).count(); + const auto dt = std::chrono::duration_cast(timestamp - ev.timeStamp).count(); assert(dt >= 0); state = std::make_tuple(code, keyDown, dt); @@ -239,21 +316,34 @@ class ICameraController : virtual public core::IReferenceCounted if (physicalKey != ui::E_KEY_CODE::EKC_NONE) if (isDown) - output.emplace_back(CVirtualEvent{ virtualKey, static_cast(dtAction) }); - } + { + auto* virtualEvent = output + count; + assert(virtualEvent); // TODO: maybe just log error and return 0 count - return output; + virtualEvent->type = virtualEventType; + virtualEvent->magnitude = static_cast(dtAction); + ++count; + } + } } - /* - // [OPTIONAL]: process mouse to generate virtual manipulation events - // note that: - // - all manipulations *may* be done with keyboard keys (if you have a touchpad or sth an ui:: event could be a code!) - // - it doesn't make the manipulation itself! - */ - std::vector processMouse(std::span events) const + // Processes mouse events to generate virtual manipulation events, note that it doesn't make the manipulation itself! + // Limited to Pan & Tilt rotation events, camera type implements how event magnitudes should be interpreted + void processMouse(CVirtualGimbalEvent* output, uint32_t& count, std::span events) { - double dPitch = {}, dYaw = {}; + if (!output) + { + count = 2u; + return; + } + + count = 0u; + + if (events.empty()) + return; + + const auto timestamp = getEventGenerationTimestamp(); + double dYaw = {}, dPitch = {}; for (const auto& ev : events) if (ev.type == nbl::ui::SMouseEvent::EET_MOVEMENT) @@ -262,57 +352,38 @@ class ICameraController : virtual public core::IReferenceCounted dPitch += ev.movementEvent.relativeMovementY; } - std::vector output; - if (dPitch) { - auto& pitch = output.emplace_back(); - pitch.type = dPitch > 0.f ? TiltUp : TiltDown; - pitch.value = std::abs(dPitch); + auto* pitch = output + count; + assert(pitch); // TODO: maybe just log error and return 0 count + pitch->type = dPitch > 0.f ? CVirtualGimbalEvent::TiltUp : CVirtualGimbalEvent::TiltDown; + pitch->magnitude = std::abs(dPitch); + count++; } if (dYaw) { - auto& yaw = output.emplace_back(); - yaw.type = dYaw > 0.f ? PanRight : PanLeft; - yaw.value = std::abs(dYaw); + auto* yaw = output + count; + assert(yaw); // TODO: maybe just log error and return 0 count + yaw->type = dYaw > 0.f ? CVirtualGimbalEvent::PanRight : CVirtualGimbalEvent::PanLeft; + yaw->magnitude = std::abs(dYaw); + count++; } - - return output; } - inline void setMoveSpeed(const float moveSpeed) { m_moveSpeed = moveSpeed; } - inline void setRotateSpeed(const float rotateSpeed) { m_rotateSpeed = rotateSpeed; } - - inline const float getMoveSpeed() const { return m_moveSpeed; } - inline const float getRotateSpeed() const { return m_rotateSpeed; } - protected: - // controller can override default set of event map - virtual void initKeysToEvent() - { - m_keysToEvent[MoveForward] = ui::E_KEY_CODE::EKC_W ; - m_keysToEvent[MoveBackward] = ui::E_KEY_CODE::EKC_S ; - m_keysToEvent[MoveLeft] = ui::E_KEY_CODE::EKC_A ; - m_keysToEvent[MoveRight] = ui::E_KEY_CODE::EKC_D ; - m_keysToEvent[MoveUp] = ui::E_KEY_CODE::EKC_SPACE ; - m_keysToEvent[MoveDown] = ui::E_KEY_CODE::EKC_LEFT_SHIFT ; - m_keysToEvent[TiltUp] = ui::E_KEY_CODE::EKC_NONE ; - m_keysToEvent[TiltDown] = ui::E_KEY_CODE::EKC_NONE ; - m_keysToEvent[PanLeft] = ui::E_KEY_CODE::EKC_NONE ; - m_keysToEvent[PanRight] = ui::E_KEY_CODE::EKC_NONE ; - m_keysToEvent[RollLeft] = ui::E_KEY_CODE::EKC_NONE ; - m_keysToEvent[RollRight] = ui::E_KEY_CODE::EKC_NONE ; - m_keysToEvent[Reset] = ui::E_KEY_CODE::EKC_R ; - } + virtual void initKeysToEvent() = 0; - std::array m_keysToEvent = {}; - float m_moveSpeed = 1.f, m_rotateSpeed = 1.f; - bool m_keysDown[EventsCount] = {}; +private: + CVirtualGimbalEvent::keys_to_virtual_events_t m_keysToVirtualEvents = { { ui::E_KEY_CODE::EKC_NONE } }; + bool m_keysDown[CVirtualGimbalEvent::EventsCount] = {}; - std::chrono::microseconds m_nextPresentationTimeStamp = {}, m_lastVirtualUpTimeStamp = {}; + // exactly what our Nabla events do, actually I don't want users to pass timestamp since I know when it should be best to make a request -> just before generating events! + // TODO: need to think about this + inline std::chrono::microseconds getEventGenerationTimestamp() { return std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()); } }; +#if 0 // TOOD: update template concept GimbalRange = GeneralPurposeRange && requires { @@ -337,6 +408,7 @@ class IGimbalRange : public IRange // TODO NOTE: eg. "follow camera" should use GimbalRange::CGimbal, 2u>>, // one per camera itself and one for target it follows +#endif } // nbl::hlsl namespace From 5f4bd96c2628040a4f75ca924bbf478095c52ea6 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Sun, 3 Nov 2024 20:11:43 +0100 Subject: [PATCH 024/205] bad typo --- common/include/camera/ICameraControl.hpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/common/include/camera/ICameraControl.hpp b/common/include/camera/ICameraControl.hpp index ff918faa4..94e612bf0 100644 --- a/common/include/camera/ICameraControl.hpp +++ b/common/include/camera/ICameraControl.hpp @@ -55,16 +55,16 @@ struct CVirtualGimbalEvent }; using manipulation_encode_t = float64_t; - using keys_to_virtual_events_t = std::array; + using keys_to_virtual_events_t = std::array; VirtualEventType type; manipulation_encode_t magnitude; static inline constexpr auto VirtualEventsTypeTable = []() { - std::array output; + std::array output; - for (uint16_t i = 0u; i < EventsCount - 1u; ++i) + for (uint16_t i = 0u; i < EventsCount; ++i) { output[i] = static_cast(i); } From a91b5cf462a27b3184d1abbea81e7c284cb4466a Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Wed, 6 Nov 2024 13:00:24 +0100 Subject: [PATCH 025/205] create CGeneralPurposeGimbal & IGimbal (which will apply to any kind of object), store `const uintptr_t m_id;` in gimbal intercae for imguizmo/imgui api, update sources --- 61_UI/main.cpp | 2 +- common/include/CFPSCamera.hpp | 5 +- common/include/ICamera.hpp | 8 +- common/include/camera/ICameraControl.hpp | 229 +++-------------------- common/include/camera/IGimbal.hpp | 193 +++++++++++++++++++ 5 files changed, 225 insertions(+), 212 deletions(-) create mode 100644 common/include/camera/IGimbal.hpp diff --git a/61_UI/main.cpp b/61_UI/main.cpp index 1d6016830..e18185453 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -328,7 +328,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication } imguizmoM16InOut; const auto& projectionMatrix = projection->getMatrix(); - const auto& view = camera->getGimbal().getView().value(); + const auto& view = camera->getGimbal().getView(); ImGuizmo::SetID(0u); imguizmoM16InOut.view = transpose(getMatrix3x4As4x4(view.matrix)); diff --git a/common/include/CFPSCamera.hpp b/common/include/CFPSCamera.hpp index 58e07e1c0..e3ee863a0 100644 --- a/common/include/CFPSCamera.hpp +++ b/common/include/CFPSCamera.hpp @@ -18,8 +18,8 @@ class CFPSCamera final : public ICamera using base_t = ICamera; using traits_t = typename base_t::Traits; - CFPSCamera(const float32_t3& position, glm::quat orientation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f)) - : base_t(), m_gimbal({ .position = position, .orientation = orientation, .withView = true }) + CFPSCamera(const vector& position, glm::quat orientation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f)) + : base_t(), m_gimbal({ .position = position, .orientation = orientation }) { initKeysToEvent(); } @@ -92,6 +92,7 @@ class CFPSCamera final : public ICamera { m_gimbal.setOrientation(orientation); m_gimbal.move(accumulated.dMove); + m_gimbal.updateView(); } m_gimbal.end(); } diff --git a/common/include/ICamera.hpp b/common/include/ICamera.hpp index 4faebe536..4556a8097 100644 --- a/common/include/ICamera.hpp +++ b/common/include/ICamera.hpp @@ -5,12 +5,6 @@ #ifndef _I_CAMERA_HPP_ #define _I_CAMERA_HPP_ -#include -#include -#include -#include -#include - #include "camera/ICameraControl.hpp" namespace nbl::hlsl // TODO: DIFFERENT NAMESPACE @@ -26,7 +20,7 @@ class ICamera : public ICameraController { using controller_t = base_t; using gimbal_t = typename controller_t::CGimbal; - using matrix_precision_t = typename T; // TODO: actually all vectors/scalars should have precision type T and because of projection matrix constraints allowed is only float32_t & float64_t + using precision_t = typename T; // TODO: actually all vectors/scalars should have precision type T and because of projection matrix constraints allowed is only float32_t & float64_t }; ICamera() : base_t() {} diff --git a/common/include/camera/ICameraControl.hpp b/common/include/camera/ICameraControl.hpp index 94e612bf0..af1fc9fbe 100644 --- a/common/include/camera/ICameraControl.hpp +++ b/common/include/camera/ICameraControl.hpp @@ -1,217 +1,48 @@ #ifndef _NBL_I_CAMERA_CONTROLLER_HPP_ #define _NBL_I_CAMERA_CONTROLLER_HPP_ +#include +#include +#include +#include +#include + #include "IProjection.hpp" -#include "glm/glm/ext/matrix_transform.hpp" // TODO: TEMPORARY!!! whatever used will be moved to cpp -#include "glm/glm/gtc/quaternion.hpp" -#include "nbl/builtin/hlsl/matrix_utils/transformation_matrix_utils.hlsl" +#include "CGeneralPurposeGimbal.hpp" // TODO: DIFFERENT NAMESPACE namespace nbl::hlsl { -struct CVirtualGimbalEvent -{ - //! Virtual event representing a gimbal manipulation - enum VirtualEventType : uint8_t - { - // Strafe forward - MoveForward = 0, - - // Strafe backward - MoveBackward, - - // Strafe left - MoveLeft, - - // Strafe right - MoveRight, - - // Strafe up - MoveUp, - - // Strafe down - MoveDown, - - // Tilt the camera upward (pitch) - TiltUp, - - // Tilt the camera downward (pitch) - TiltDown, - - // Rotate the camera left around the vertical axis (yaw) - PanLeft, - - // Rotate the camera right around the vertical axis (yaw) - PanRight, - - // Roll the camera counterclockwise around the forward axis (roll) - RollLeft, - - // Roll the camera clockwise around the forward axis (roll) - RollRight, - - EventsCount - }; - - using manipulation_encode_t = float64_t; - using keys_to_virtual_events_t = std::array; - - VirtualEventType type; - manipulation_encode_t magnitude; - - static inline constexpr auto VirtualEventsTypeTable = []() - { - std::array output; - - for (uint16_t i = 0u; i < EventsCount; ++i) - { - output[i] = static_cast(i); - } - - return output; - }(); -}; - template class ICameraController : virtual public core::IReferenceCounted { public: - using matrix_precision_t = typename T; + using precision_t = typename T; - class CGimbal + // Gimbal with view parameters representing a camera in world space + class CGimbal : public IGimbal { public: - struct SCreationParameters - { - float32_t3 position; - glm::quat orientation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f); - bool withView = true; - }; + using base_t = IGimbal; + + CGimbal(typename base_t::SCreationParameters&& parameters) : base_t(std::move(parameters)) {} + ~CGimbal() = default; - // Gimbal's view matrix consists of an orthonormal basis (https://en.wikipedia.org/wiki/Orthonormal_basis) - // for orientation and a translation component that positions the world relative to the gimbal's position. - // Any camera type is supposed to manipulate a position & orientation of a gimbal - // with "virtual events" which model its view bound to the camera struct SView { - matrix matrix = {}; + matrix matrix = {}; bool isLeftHandSystem = true; }; - CGimbal(SCreationParameters&& parameters) - : m_position(parameters.position), m_orientation(parameters.orientation) - { - updateOrthonormalOrientationBase(); - - if (parameters.withView) - { - m_view = std::optional(SView{}); // RVO - updateView(); - } - } - - void begin() - { - m_isManipulating = true; - m_counter = 0u; - } - - inline void setPosition(const float32_t3& position) - { - assert(m_isManipulating); // TODO: log error and return without doing nothing - - if (m_position != position) - m_counter++; - - m_position = position; - } - - inline void setOrientation(const glm::quat& orientation) - { - assert(m_isManipulating); // TODO: log error and return without doing nothing - - if(m_orientation != orientation) - m_counter++; - - m_orientation = glm::normalize(orientation); - updateOrthonormalOrientationBase(); - } - - inline void rotate(const float32_t3& axis, float dRadians) - { - assert(m_isManipulating); // TODO: log error and return without doing nothing - - if(dRadians) - m_counter++; - - glm::quat dRotation = glm::angleAxis(dRadians, axis); - m_orientation = glm::normalize(dRotation * m_orientation); - updateOrthonormalOrientationBase(); - } - - inline void move(float32_t3 delta) - { - assert(m_isManipulating); // TODO: log error and return without doing nothing - - auto newPosition = m_position + delta; - - if (newPosition != m_position) - m_counter++; - - m_position = newPosition; - } - - void end() - { - m_isManipulating = false; - - if (m_counter > 0u) - updateView(); - - m_counter = 0u; - } - - // Position of gimbal - inline const float32_t3& getPosition() const { return m_position; } - - // Orientation of gimbal - inline const glm::quat& getOrientation() const { return m_orientation; } - - // Orthonormal [getXAxis(), getYAxis(), getZAxis()] orientation matrix - inline const float32_t3x3& getOrthonornalMatrix() const { return m_orthonormal; } - - // Base "right" vector in orthonormal orientation basis (X-axis) - inline const float32_t3& getXAxis() const { return m_orthonormal[0u]; } - - // Base "up" vector in orthonormal orientation basis (Y-axis) - inline const float32_t3& getYAxis() const { return m_orthonormal[1u]; } - - // Base "forward" vector in orthonormal orientation basis (Z-axis) - inline const float32_t3& getZAxis() const { return m_orthonormal[2u]; } - - // Optional view of a gimbal - inline const std::optional& getView() const { return m_view; } - - inline const size_t& getManipulationCounter() { return m_counter; } - inline bool isManipulating() const { return m_isManipulating; } - - private: - inline void updateOrthonormalOrientationBase() - { - m_orthonormal = matrix(glm::mat3_cast(glm::normalize(m_orientation))); - } - inline void updateView() { - if (m_view.has_value()) // TODO: this could be templated + constexpr actually if gimbal doesn't init this on runtime depending on sth + if (base_t::getManipulationCounter()) { - auto& view = m_view.value(); - const auto& gRight = getXAxis(), gUp = getYAxis(), gForward = getZAxis(); + const auto& gRight = base_t::getXAxis(), gUp = base_t::getYAxis(), gForward = base_t::getZAxis(); - // TODO: I think I will provide convert utility allowing to go from one hand system to another, its just a matter to take care of m_view->matrix[2u] to perform a LH/RH flip - // in general this should not know about projections which are now supposed to be independent and store reference to a camera (or own it) - view.isLeftHandSystem = hlsl::determinant(m_orthonormal) < 0.0f; + // TODO: I think this should be set as request state, depending on the value we do m_view.matrix[2u] flip accordingly + // m_view.isLeftHandSystem; auto isNormalized = [](const auto& v, float epsilon) -> bool { @@ -231,24 +62,18 @@ class ICameraController : virtual public core::IReferenceCounted assert(isOrthoBase(gRight, gUp, gForward)); - view.matrix[0u] = vector(gRight, -glm::dot(gRight, m_position)); - view.matrix[1u] = vector(gUp, -glm::dot(gUp, m_position)); - view.matrix[2u] = vector(gForward, -glm::dot(gForward, m_position)); + const auto& position = base_t::getPosition(); + m_view.matrix[0u] = vector(gRight, -glm::dot(gRight, position)); + m_view.matrix[1u] = vector(gUp, -glm::dot(gUp, position)); + m_view.matrix[2u] = vector(gForward, -glm::dot(gForward, position)); } } - float32_t3 m_position; - glm::quat m_orientation; - matrix m_orthonormal; + // Getter for gimbal's view + inline const SView& getView() const { return m_view; } - // For a camera implementation at least one gimbal models its view but not all gimbals (if multiple) are expected to do so - std::optional m_view = std::nullopt; - - // Counts *performed* manipulations, a manipulation with 0 delta is not counted! - size_t m_counter = {}; - - // Records manipulation state - bool m_isManipulating = false; + private: + SView m_view; }; ICameraController() {} diff --git a/common/include/camera/IGimbal.hpp b/common/include/camera/IGimbal.hpp new file mode 100644 index 000000000..38d99ecf0 --- /dev/null +++ b/common/include/camera/IGimbal.hpp @@ -0,0 +1,193 @@ +#ifndef _NBL_IGIMBAL_HPP_ +#define _NBL_IGIMBAL_HPP_ + +#include "glm/glm/ext/matrix_transform.hpp" // TODO: TEMPORARY!!! whatever used will be moved to cpp +#include "glm/glm/gtc/quaternion.hpp" +#include "nbl/builtin/hlsl/matrix_utils/transformation_matrix_utils.hlsl" + +// TODO: DIFFERENT NAMESPACE +namespace nbl::hlsl +{ + struct CVirtualGimbalEvent + { + //! Virtual event representing a gimbal manipulation + enum VirtualEventType : uint8_t + { + // Strafe forward + MoveForward = 0, + + // Strafe backward + MoveBackward, + + // Strafe left + MoveLeft, + + // Strafe right + MoveRight, + + // Strafe up + MoveUp, + + // Strafe down + MoveDown, + + // Tilt the camera upward (pitch) + TiltUp, + + // Tilt the camera downward (pitch) + TiltDown, + + // Rotate the camera left around the vertical axis (yaw) + PanLeft, + + // Rotate the camera right around the vertical axis (yaw) + PanRight, + + // Roll the camera counterclockwise around the forward axis (roll) + RollLeft, + + // Roll the camera clockwise around the forward axis (roll) + RollRight, + + // TODO: scale events + + EventsCount + }; + + using manipulation_encode_t = float64_t; + using keys_to_virtual_events_t = std::array; + + VirtualEventType type; + manipulation_encode_t magnitude; + + static inline constexpr auto VirtualEventsTypeTable = []() + { + std::array output; + + for (uint16_t i = 0u; i < EventsCount; ++i) + output[i] = static_cast(i); + + return output; + }(); + }; + + template + requires is_any_of_v + class IGimbal + { + public: + using precision_t = T; + + struct SCreationParameters + { + vector position; + glm::quat orientation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f); + }; + + IGimbal(SCreationParameters&& parameters) + : m_position(parameters.position), m_orientation(parameters.orientation), m_id(reinterpret_cast(this)) + { + updateOrthonormalOrientationBase(); + } + + inline const uintptr_t getID() const { return m_id; } + + void begin() + { + m_isManipulating = true; + m_counter = 0u; + } + + inline void setPosition(const vector& position) + { + assert(m_isManipulating); // TODO: log error and return without doing nothing + + if (m_position != position) + m_counter++; + + m_position = position; + } + + inline void setOrientation(const glm::quat& orientation) + { + assert(m_isManipulating); // TODO: log error and return without doing nothing + + if(m_orientation != orientation) + m_counter++; + + m_orientation = glm::normalize(orientation); + updateOrthonormalOrientationBase(); + } + + inline void rotate(const vector& axis, float dRadians) + { + assert(m_isManipulating); // TODO: log error and return without doing nothing + + if(dRadians) + m_counter++; + + glm::quat dRotation = glm::angleAxis(dRadians, axis); + m_orientation = glm::normalize(dRotation * m_orientation); + updateOrthonormalOrientationBase(); + } + + inline void move(vector delta) + { + assert(m_isManipulating); // TODO: log error and return without doing nothing + + auto newPosition = m_position + delta; + + if (newPosition != m_position) + m_counter++; + + m_position = newPosition; + } + + void end() + { + m_isManipulating = false; + } + + // Position of gimbal + inline const auto& getPosition() const { return m_position; } + + // Orientation of gimbal + inline const auto& getOrientation() const { return m_orientation; } + + // Orthonormal [getXAxis(), getYAxis(), getZAxis()] orientation matrix + inline const auto& getOrthonornalMatrix() const { return m_orthonormal; } + + // Base "right" vector in orthonormal orientation basis (X-axis) + inline const auto& getXAxis() const { return m_orthonormal[0u]; } + + // Base "up" vector in orthonormal orientation basis (Y-axis) + inline const auto& getYAxis() const { return m_orthonormal[1u]; } + + // Base "forward" vector in orthonormal orientation basis (Z-axis) + inline const auto& getZAxis() const { return m_orthonormal[2u]; } + + inline const auto& getManipulationCounter() { return m_counter; } + inline bool isManipulating() const { return m_isManipulating; } + + private: + inline void updateOrthonormalOrientationBase() + { + m_orthonormal = matrix(glm::mat3_cast(glm::normalize(m_orientation))); + } + + vector m_position; + glm::quat m_orientation; // TODO: precision + matrix m_orthonormal; + + // Counts *performed* manipulations, a manipulation with 0 delta is not counted! + size_t m_counter = {}; + + // Records manipulation state + bool m_isManipulating = false; + + // the fact ImGUIZMO has global context I don't like, however for IDs we can do a life-tracking trick and cast addresses which are unique & we don't need any global associative container to track them! + const uintptr_t m_id; + }; +} // namespace nbl::hlsl + +#endif // _NBL_IGIMBAL_HPP_ \ No newline at end of file From 7ef8a92e77fcd5ea2f02a5d643395ea67ee37acc Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Wed, 6 Nov 2024 17:57:56 +0100 Subject: [PATCH 026/205] change keys lookup table, add scale events to CVirtualGimbalEvent::VirtualEventType + change it to bitmask, create IGimbal::VirtualImpulse & corresponding template accumulate method which may be useful as general purpose utility for various camera types, update CFPSCamera to use it, update sources --- common/include/CFPSCamera.hpp | 68 ++----- .../include/camera/CGeneralPurposeGimbal.hpp | 19 ++ common/include/camera/ICameraControl.hpp | 54 +++--- common/include/camera/IGimbal.hpp | 178 ++++++++++++++---- 4 files changed, 192 insertions(+), 127 deletions(-) create mode 100644 common/include/camera/CGeneralPurposeGimbal.hpp diff --git a/common/include/CFPSCamera.hpp b/common/include/CFPSCamera.hpp index e3ee863a0..5a367dfef 100644 --- a/common/include/CFPSCamera.hpp +++ b/common/include/CFPSCamera.hpp @@ -32,66 +32,30 @@ class CFPSCamera final : public ICamera virtual void manipulate(std::span virtualEvents) override { + constexpr auto AllowedEvents = CVirtualGimbalEvent::MoveForward | CVirtualGimbalEvent::MoveBackward | CVirtualGimbalEvent::MoveRight | CVirtualGimbalEvent::MoveLeft | CVirtualGimbalEvent::TiltUp | CVirtualGimbalEvent::TiltDown | CVirtualGimbalEvent::PanRight | CVirtualGimbalEvent::PanLeft; + constexpr float MoveSpeedScale = 0.01f, RotateSpeedScale = 0.003f, MaxVerticalAngle = glm::radians(88.0f), MinVerticalAngle = -MaxVerticalAngle; + if (!virtualEvents.size()) return; - constexpr float MoveSpeedScale = 0.01f, RotateSpeedScale = 0.003f, MaxVerticalAngle = glm::radians(88.0f), MinVerticalAngle = -MaxVerticalAngle; + const auto impulse = m_gimbal.accumulate(virtualEvents); const auto& gForward = m_gimbal.getZAxis(), gRight = m_gimbal.getXAxis(); - struct - { - float dPitch = 0.f, dYaw = 0.f; - float32_t3 dMove = { 0.f, 0.f, 0.f }; - } accumulated; - - for (const auto& event : virtualEvents) - { - const float moveScalar = event.magnitude * MoveSpeedScale; - const float rotateScalar = event.magnitude * RotateSpeedScale; - - switch (event.type) - { - case CVirtualGimbalEvent::MoveForward: - accumulated.dMove += gForward * moveScalar; - break; - case CVirtualGimbalEvent::MoveBackward: - accumulated.dMove -= gForward * moveScalar; - break; - case CVirtualGimbalEvent::MoveRight: - accumulated.dMove += gRight * moveScalar; - break; - case CVirtualGimbalEvent::MoveLeft: - accumulated.dMove -= gRight * moveScalar; - break; - case CVirtualGimbalEvent::TiltUp: - accumulated.dPitch += rotateScalar; - break; - case CVirtualGimbalEvent::TiltDown: - accumulated.dPitch -= rotateScalar; - break; - case CVirtualGimbalEvent::PanRight: - accumulated.dYaw += rotateScalar; - break; - case CVirtualGimbalEvent::PanLeft: - accumulated.dYaw -= rotateScalar; - break; - default: - break; - } - } - float currentPitch = atan2(glm::length(glm::vec2(gForward.x, gForward.z)), gForward.y) - glm::half_pi(); float currentYaw = atan2(gForward.x, gForward.z); - currentPitch = std::clamp(currentPitch + accumulated.dPitch, MinVerticalAngle, MaxVerticalAngle); - currentYaw += accumulated.dYaw; + // adjust the current pitch and yaw + currentPitch = std::clamp(currentPitch + impulse.dVirtualRotation.x * RotateSpeedScale, MinVerticalAngle, MaxVerticalAngle); + currentYaw += impulse.dVirtualRotation.y * RotateSpeedScale; + // create new orientation based on accumulated pitch and yaw from a virtual impulse glm::quat orientation = glm::quat(glm::vec3(currentPitch, currentYaw, 0.0f)); + // manipulate view gimbal m_gimbal.begin(); { m_gimbal.setOrientation(orientation); - m_gimbal.move(accumulated.dMove); + m_gimbal.move(impulse.dVirtualTranslate * MoveSpeedScale); m_gimbal.updateView(); } m_gimbal.end(); @@ -100,14 +64,12 @@ class CFPSCamera final : public ICamera private: void initKeysToEvent() override { - traits_t::controller_t::updateKeysToEvent([](CVirtualGimbalEvent::keys_to_virtual_events_t& keys) + traits_t::controller_t::updateKeysToEvent([](traits_t::controller_t::keys_to_virtual_events_t& keys) { - keys[CVirtualGimbalEvent::MoveForward] = ui::E_KEY_CODE::EKC_W; - keys[CVirtualGimbalEvent::MoveBackward] = ui::E_KEY_CODE::EKC_S; - keys[CVirtualGimbalEvent::MoveLeft] = ui::E_KEY_CODE::EKC_A; - keys[CVirtualGimbalEvent::MoveRight] = ui::E_KEY_CODE::EKC_D; - keys[CVirtualGimbalEvent::MoveUp] = ui::E_KEY_CODE::EKC_SPACE; - keys[CVirtualGimbalEvent::MoveDown] = ui::E_KEY_CODE::EKC_LEFT_SHIFT; + keys[ui::E_KEY_CODE::EKC_W] = CVirtualGimbalEvent::MoveForward; + keys[ui::E_KEY_CODE::EKC_S] = CVirtualGimbalEvent::MoveBackward; + keys[ui::E_KEY_CODE::EKC_A] = CVirtualGimbalEvent::MoveLeft; + keys[ui::E_KEY_CODE::EKC_D] = CVirtualGimbalEvent::MoveRight; }); } diff --git a/common/include/camera/CGeneralPurposeGimbal.hpp b/common/include/camera/CGeneralPurposeGimbal.hpp new file mode 100644 index 000000000..9a66d5712 --- /dev/null +++ b/common/include/camera/CGeneralPurposeGimbal.hpp @@ -0,0 +1,19 @@ +#ifndef _NBL_CGENERAL_PURPOSE_GIMBAL_HPP_ +#define _NBL_CGENERAL_PURPOSE_GIMBAL_HPP_ + +#include "IGimbal.hpp" + +// TODO: DIFFERENT NAMESPACE +namespace nbl::hlsl +{ + template + class CGeneralPurposeGimbal : public IGimbal + { + using base_t = IGimbal; + + CGeneralPurposeGimbal(base_t::SCreationParameters&& parameters) : base_t(std::move(parameters)) {} + ~CGeneralPurposeGimbal() = default; + }; +} + +#endif // _NBL_IGIMBAL_HPP_ \ No newline at end of file diff --git a/common/include/camera/ICameraControl.hpp b/common/include/camera/ICameraControl.hpp index af1fc9fbe..da40d04ac 100644 --- a/common/include/camera/ICameraControl.hpp +++ b/common/include/camera/ICameraControl.hpp @@ -19,6 +19,7 @@ class ICameraController : virtual public core::IReferenceCounted { public: using precision_t = typename T; + using keys_to_virtual_events_t = std::unordered_map; // Gimbal with view parameters representing a camera in world space class CGimbal : public IGimbal @@ -78,8 +79,8 @@ class ICameraController : virtual public core::IReferenceCounted ICameraController() {} - // Binds key codes to virtual events, the mapKeys lambda will be executed with controller CVirtualGimbalEvent::keys_to_virtual_events_t table - void updateKeysToEvent(const std::function& mapKeys) + // Binds key codes to virtual events, the mapKeys lambda will be executed with controller keys_to_virtual_events_t table + void updateKeysToEvent(const std::function& mapKeys) { mapKeys(m_keysToVirtualEvents); } @@ -95,7 +96,7 @@ class ICameraController : virtual public core::IReferenceCounted { if (!output) { - count = CVirtualGimbalEvent::EventsCount; + count = events.size(); return; } @@ -106,49 +107,36 @@ class ICameraController : virtual public core::IReferenceCounted const auto timestamp = getEventGenerationTimestamp(); - for (const auto virtualEventType : CVirtualGimbalEvent::VirtualEventsTypeTable) + for (const auto& keyboardEvent : events) { - const auto code = m_keysToVirtualEvents[virtualEventType]; - bool& keyDown = m_keysDown[virtualEventType]; + auto request = m_keysToVirtualEvents.find(keyboardEvent.keyCode); + bool isKeyMapped = request != std::end(m_keysToVirtualEvents); - using virtual_key_state_t = std::tuple; - - auto updateVirtualState = [&]() -> virtual_key_state_t + if (isKeyMapped) { - virtual_key_state_t state = { ui::E_KEY_CODE::EKC_NONE, false, 0.f }; + auto& key = request->first; auto& info = request->second; - for (const auto& ev : events) // TODO: improve the search + if (keyboardEvent.keyCode == key) { - if (ev.keyCode == code) - { - if (ev.action == nbl::ui::SKeyboardEvent::ECA_PRESSED && !keyDown) - keyDown = true; - else if (ev.action == nbl::ui::SKeyboardEvent::ECA_RELEASED) - keyDown = false; - - const auto dt = std::chrono::duration_cast(timestamp - ev.timeStamp).count(); - assert(dt >= 0); - - state = std::make_tuple(code, keyDown, dt); - break; - } + if (keyboardEvent.action == nbl::ui::SKeyboardEvent::ECA_PRESSED && !info.active) + info.active = true; + else if (keyboardEvent.action == nbl::ui::SKeyboardEvent::ECA_RELEASED) + info.active = false; } - return state; - }; - - const auto&& [physicalKey, isDown, dtAction] = updateVirtualState(); - - if (physicalKey != ui::E_KEY_CODE::EKC_NONE) - if (isDown) + if (info.active) { + const auto dtAction = std::chrono::duration_cast(timestamp - keyboardEvent.timeStamp).count(); + assert(dtAction >= 0); + auto* virtualEvent = output + count; assert(virtualEvent); // TODO: maybe just log error and return 0 count - virtualEvent->type = virtualEventType; + virtualEvent->type = info.type; virtualEvent->magnitude = static_cast(dtAction); ++count; } + } } } @@ -200,7 +188,7 @@ class ICameraController : virtual public core::IReferenceCounted virtual void initKeysToEvent() = 0; private: - CVirtualGimbalEvent::keys_to_virtual_events_t m_keysToVirtualEvents = { { ui::E_KEY_CODE::EKC_NONE } }; + keys_to_virtual_events_t m_keysToVirtualEvents; bool m_keysDown[CVirtualGimbalEvent::EventsCount] = {}; // exactly what our Nabla events do, actually I don't want users to pass timestamp since I know when it should be best to make a request -> just before generating events! diff --git a/common/include/camera/IGimbal.hpp b/common/include/camera/IGimbal.hpp index 38d99ecf0..db108ed7b 100644 --- a/common/include/camera/IGimbal.hpp +++ b/common/include/camera/IGimbal.hpp @@ -10,53 +10,50 @@ namespace nbl::hlsl { struct CVirtualGimbalEvent { - //! Virtual event representing a gimbal manipulation - enum VirtualEventType : uint8_t + enum VirtualEventType : uint32_t { - // Strafe forward - MoveForward = 0, - - // Strafe backward - MoveBackward, - - // Strafe left - MoveLeft, - - // Strafe right - MoveRight, - - // Strafe up - MoveUp, - - // Strafe down - MoveDown, - - // Tilt the camera upward (pitch) - TiltUp, - - // Tilt the camera downward (pitch) - TiltDown, - - // Rotate the camera left around the vertical axis (yaw) - PanLeft, - - // Rotate the camera right around the vertical axis (yaw) - PanRight, - - // Roll the camera counterclockwise around the forward axis (roll) - RollLeft, - - // Roll the camera clockwise around the forward axis (roll) - RollRight, + None = 0, + + // Individual events + MoveForward = core::createBitmask({ 0 }), + MoveBackward = core::createBitmask({ 1 }), + MoveLeft = core::createBitmask({ 2 }), + MoveRight = core::createBitmask({ 3 }), + MoveUp = core::createBitmask({ 4 }), + MoveDown = core::createBitmask({ 5 }), + TiltUp = core::createBitmask({ 6 }), + TiltDown = core::createBitmask({ 7 }), + PanLeft = core::createBitmask({ 8 }), + PanRight = core::createBitmask({ 9 }), + RollLeft = core::createBitmask({ 10 }), + RollRight = core::createBitmask({ 11 }), + ScaleXInc = core::createBitmask({ 12 }), + ScaleXDec = core::createBitmask({ 13 }), + ScaleYInc = core::createBitmask({ 14 }), + ScaleYDec = core::createBitmask({ 15 }), + ScaleZInc = core::createBitmask({ 16 }), + ScaleZDec = core::createBitmask({ 17 }), + + EventsCount = 18, + + // Grouped bitmasks + Translate = MoveForward | MoveBackward | MoveLeft | MoveRight | MoveUp | MoveDown, + Rotate = TiltUp | TiltDown | PanLeft | PanRight | RollLeft | RollRight, + Scale = ScaleXInc | ScaleXDec | ScaleYInc | ScaleYDec | ScaleZInc | ScaleZDec + }; - // TODO: scale events + struct CRequestInfo + { + CRequestInfo() : type(None) {} + CRequestInfo(VirtualEventType _type) : type(_type) {} + ~CRequestInfo() = default; - EventsCount + VirtualEventType type; + bool active = false; }; using manipulation_encode_t = float64_t; - using keys_to_virtual_events_t = std::array; - + VirtualEventType type; manipulation_encode_t magnitude; @@ -78,6 +75,105 @@ namespace nbl::hlsl public: using precision_t = T; + struct VirtualImpulse + { + vector dVirtualTranslate { 0.0f }, dVirtualRotation { 0.0f }, dVirtualScale { 0.0f }; + }; + + template + VirtualImpulse accumulate(std::span virtualEvents, const vector& gRightOverride, const vector& gUpOverride, const vector& gForwardOverride) + { + VirtualImpulse impulse; + + const auto& gRight = gRightOverride, gUp = gUpOverride, gForward = gForwardOverride; + + for (const auto& event : virtualEvents) + { + // translation events + if constexpr (AllowedEvents & CVirtualGimbalEvent::MoveForward) + if (event.type == CVirtualGimbalEvent::MoveForward) + impulse.dVirtualTranslate += gForward * static_cast(event.magnitude); + + if constexpr (AllowedEvents & CVirtualGimbalEvent::MoveBackward) + if (event.type == CVirtualGimbalEvent::MoveBackward) + impulse.dVirtualTranslate -= gForward * static_cast(event.magnitude); + + if constexpr (AllowedEvents & CVirtualGimbalEvent::MoveRight) + if (event.type == CVirtualGimbalEvent::MoveRight) + impulse.dVirtualTranslate += gRight * static_cast(event.magnitude); + + if constexpr (AllowedEvents & CVirtualGimbalEvent::MoveLeft) + if (event.type == CVirtualGimbalEvent::MoveLeft) + impulse.dVirtualTranslate -= gRight * static_cast(event.magnitude); + + if constexpr (AllowedEvents & CVirtualGimbalEvent::MoveUp) + if (event.type == CVirtualGimbalEvent::MoveUp) + impulse.dVirtualTranslate += gUp * static_cast(event.magnitude); + + if constexpr (AllowedEvents & CVirtualGimbalEvent::MoveDown) + if (event.type == CVirtualGimbalEvent::MoveDown) + impulse.dVirtualTranslate -= gUp * static_cast(event.magnitude); + + // rotation events + if constexpr (AllowedEvents & CVirtualGimbalEvent::TiltUp) + if (event.type == CVirtualGimbalEvent::TiltUp) + impulse.dVirtualRotation.x += static_cast(event.magnitude); + + if constexpr (AllowedEvents & CVirtualGimbalEvent::TiltDown) + if (event.type == CVirtualGimbalEvent::TiltDown) + impulse.dVirtualRotation.x -= static_cast(event.magnitude); + + if constexpr (AllowedEvents & CVirtualGimbalEvent::PanRight) + if (event.type == CVirtualGimbalEvent::PanRight) + impulse.dVirtualRotation.y += static_cast(event.magnitude); + + if constexpr (AllowedEvents & CVirtualGimbalEvent::PanLeft) + if (event.type == CVirtualGimbalEvent::PanLeft) + impulse.dVirtualRotation.y -= static_cast(event.magnitude); + + if constexpr (AllowedEvents & CVirtualGimbalEvent::RollRight) + if (event.type == CVirtualGimbalEvent::RollRight) + impulse.dVirtualRotation.z += static_cast(event.magnitude); + + if constexpr (AllowedEvents & CVirtualGimbalEvent::RollLeft) + if (event.type == CVirtualGimbalEvent::RollLeft) + impulse.dVirtualRotation.z -= static_cast(event.magnitude); + + // scaling events + if constexpr (AllowedEvents & CVirtualGimbalEvent::ScaleXInc) + if (event.type == CVirtualGimbalEvent::ScaleXInc) + impulse.dVirtualScale.x += static_cast(event.magnitude); + + if constexpr (AllowedEvents & CVirtualGimbalEvent::ScaleXDec) + if (event.type == CVirtualGimbalEvent::ScaleXDec) + impulse.dVirtualScale.x -= static_cast(event.magnitude); + + if constexpr (AllowedEvents & CVirtualGimbalEvent::ScaleYInc) + if (event.type == CVirtualGimbalEvent::ScaleYInc) + impulse.dVirtualScale.y += static_cast(event.magnitude); + + if constexpr (AllowedEvents & CVirtualGimbalEvent::ScaleYDec) + if (event.type == CVirtualGimbalEvent::ScaleYDec) + impulse.dVirtualScale.y -= static_cast(event.magnitude); + + if constexpr (AllowedEvents & CVirtualGimbalEvent::ScaleZInc) + if (event.type == CVirtualGimbalEvent::ScaleZInc) + impulse.dVirtualScale.z += static_cast(event.magnitude); + + if constexpr (AllowedEvents & CVirtualGimbalEvent::ScaleZDec) + if (event.type == CVirtualGimbalEvent::ScaleZDec) + impulse.dVirtualScale.z -= static_cast(event.magnitude); + } + + return impulse; + } + + template + VirtualImpulse accumulate(std::span virtualEvents) + { + return accumulate(virtualEvents, getXAxis(), getYAxis(), getZAxis()); + } + struct SCreationParameters { vector position; From 263feae6a0c7c9d9d29128f4d70a2d2f4743b075 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Wed, 6 Nov 2024 18:28:09 +0100 Subject: [PATCH 027/205] lets call manipulate only one by putting all virtual events into single vector & offsetting --- 61_UI/main.cpp | 9 ++++----- common/include/CFPSCamera.hpp | 16 ++++------------ 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/61_UI/main.cpp b/61_UI/main.cpp index e18185453..41d25c8ea 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -750,14 +750,13 @@ class UISampleApp final : public examples::SimpleWindowedApplication if (move) { - static std::vector virtualMouseEvents(CVirtualGimbalEvent::VirtualEventsTypeTable.size()), virtualKeyboardEvents(CVirtualGimbalEvent::VirtualEventsTypeTable.size()); + static std::vector virtualEvents(CVirtualGimbalEvent::VirtualEventsTypeTable.size() * 2); uint32_t vEventsMouseCount, vEventsKeyboardCount; - camera->processMouse(virtualMouseEvents.data(), vEventsMouseCount, params.mouseEvents); - camera->processKeyboard(virtualKeyboardEvents.data(), vEventsKeyboardCount, params.keyboardEvents); + camera->processMouse(virtualEvents.data(), vEventsMouseCount, params.mouseEvents); + camera->processKeyboard(virtualEvents.data() + vEventsMouseCount, vEventsKeyboardCount, params.keyboardEvents); - camera->manipulate({ virtualMouseEvents.data(), vEventsMouseCount }); - camera->manipulate({ virtualKeyboardEvents.data(), vEventsKeyboardCount }); + camera->manipulate({ virtualEvents.data(), vEventsMouseCount + vEventsKeyboardCount }); } pass.ui.manager->update(params); diff --git a/common/include/CFPSCamera.hpp b/common/include/CFPSCamera.hpp index 5a367dfef..486063dab 100644 --- a/common/include/CFPSCamera.hpp +++ b/common/include/CFPSCamera.hpp @@ -39,22 +39,14 @@ class CFPSCamera final : public ICamera return; const auto impulse = m_gimbal.accumulate(virtualEvents); - const auto& gForward = m_gimbal.getZAxis(), gRight = m_gimbal.getXAxis(); - float currentPitch = atan2(glm::length(glm::vec2(gForward.x, gForward.z)), gForward.y) - glm::half_pi(); - float currentYaw = atan2(gForward.x, gForward.z); + const auto& gForward = m_gimbal.getZAxis(); + const float currentPitch = atan2(glm::length(glm::vec2(gForward.x, gForward.z)), gForward.y) - glm::half_pi(), currentYaw = atan2(gForward.x, gForward.z); + const auto newPitch = std::clamp(currentPitch + impulse.dVirtualRotation.x * RotateSpeedScale, MinVerticalAngle, MaxVerticalAngle), newYaw = currentYaw + impulse.dVirtualRotation.y * RotateSpeedScale; - // adjust the current pitch and yaw - currentPitch = std::clamp(currentPitch + impulse.dVirtualRotation.x * RotateSpeedScale, MinVerticalAngle, MaxVerticalAngle); - currentYaw += impulse.dVirtualRotation.y * RotateSpeedScale; - - // create new orientation based on accumulated pitch and yaw from a virtual impulse - glm::quat orientation = glm::quat(glm::vec3(currentPitch, currentYaw, 0.0f)); - - // manipulate view gimbal m_gimbal.begin(); { - m_gimbal.setOrientation(orientation); + m_gimbal.setOrientation(glm::quat(glm::vec3(newPitch, newYaw, 0.0f))); m_gimbal.move(impulse.dVirtualTranslate * MoveSpeedScale); m_gimbal.updateView(); } From 6624e39986dfe71ca262db54ceef977bc153143a Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Thu, 7 Nov 2024 09:56:44 +0100 Subject: [PATCH 028/205] Add key-mapping UI window. Virtual events handling fixed! I was not excepting filtering some events out could cause my issues which lead to glitches. Timestamps may not strictly follow order in which they were generated, filtering based on previous timestamp is actually not quite correct I think because it may lost some events such as in my case - release events when a lot of events from mouse & keyboard and generated at the same time. --- 61_UI/main.cpp | 57 +++++++++---- common/include/camera/ICameraControl.hpp | 100 +++++++++++++++-------- common/include/camera/IGimbal.hpp | 41 +++++++--- 3 files changed, 139 insertions(+), 59 deletions(-) diff --git a/61_UI/main.cpp b/61_UI/main.cpp index 41d25c8ea..334cc416a 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -493,6 +493,39 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGui::End(); } + { + ImGui::Begin("Key Mappings & Virtual States"); + + ImGui::Text("Key Mappings"); + ImGui::Separator(); + + const auto& keysToVirtualEvents = camera->getKeysToVirtualEvents(); + + for (const auto& [key, info] : keysToVirtualEvents) + { + const char physicalChar = ui::keyCodeToChar(key, true); + const auto eventName = CVirtualGimbalEvent::virtualEventToString(info.type); + + ImGui::Text("Key: %s", &physicalChar); + ImGui::SameLine(); + ImGui::Text("Virtual Event: %s", eventName.data()); + ImGui::SameLine(); + + if (info.active) + { + ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), "Active"); + ImGui::SameLine(); + ImGui::Text("Delta Time: %.2f ms", info.dtAction); + } + else + { + ImGui::TextColored(ImVec4(1.0f, 0.0f, 0.0f, 1.0f), "Inactive"); + } + } + + ImGui::End(); + } + ImGui::End(); } ); @@ -687,8 +720,6 @@ class UISampleApp final : public examples::SimpleWindowedApplication inline void update() { - static std::chrono::microseconds previousEventTimestamp{}; - m_inputSystem->getDefaultMouse(&mouse); m_inputSystem->getDefaultKeyboard(&keyboard); @@ -707,18 +738,14 @@ class UISampleApp final : public examples::SimpleWindowedApplication struct { - std::vector mouse{}; - std::vector keyboard{}; + std::vector mouse {}; + std::vector keyboard {}; } capturedEvents; mouse.consumeEvents([&](const IMouseEventChannel::range_t& events) -> void { - 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 (e.type == nbl::ui::SMouseEvent::EET_SCROLL) @@ -728,12 +755,8 @@ class UISampleApp final : public examples::SimpleWindowedApplication keyboard.consumeEvents([&](const IKeyboardEventChannel::range_t& events) -> void { - 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); } }, m_logger.get()); @@ -753,8 +776,10 @@ class UISampleApp final : public examples::SimpleWindowedApplication static std::vector virtualEvents(CVirtualGimbalEvent::VirtualEventsTypeTable.size() * 2); uint32_t vEventsMouseCount, vEventsKeyboardCount; - camera->processMouse(virtualEvents.data(), vEventsMouseCount, params.mouseEvents); - camera->processKeyboard(virtualEvents.data() + vEventsMouseCount, vEventsKeyboardCount, params.keyboardEvents); + camera->beginInputProcessing(nextPresentationTimestamp); + camera->processKeyboard(virtualEvents.data(), vEventsKeyboardCount, params.keyboardEvents); + camera->processMouse(virtualEvents.data() + vEventsKeyboardCount, vEventsMouseCount, params.mouseEvents); + camera->endInputProcessing(); camera->manipulate({ virtualEvents.data(), vEventsMouseCount + vEventsKeyboardCount }); } diff --git a/common/include/camera/ICameraControl.hpp b/common/include/camera/ICameraControl.hpp index da40d04ac..71e27773e 100644 --- a/common/include/camera/ICameraControl.hpp +++ b/common/include/camera/ICameraControl.hpp @@ -19,7 +19,19 @@ class ICameraController : virtual public core::IReferenceCounted { public: using precision_t = typename T; - using keys_to_virtual_events_t = std::unordered_map; + + struct CRequestInfo + { + CRequestInfo() {} + CRequestInfo(CVirtualGimbalEvent::VirtualEventType _type) : type(_type) {} + ~CRequestInfo() = default; + + CVirtualGimbalEvent::VirtualEventType type = CVirtualGimbalEvent::VirtualEventType::None; + bool active = false; + float64_t dtAction = {}; + }; + + using keys_to_virtual_events_t = std::unordered_map; // Gimbal with view parameters representing a camera in world space class CGimbal : public IGimbal @@ -91,51 +103,70 @@ class ICameraController : virtual public core::IReferenceCounted // TODO: *maybe* would be good to have a class interface for virtual event generators, // eg keyboard, mouse but maybe custom stuff too eg events from gimbal drag & drop + void beginInputProcessing(const std::chrono::microseconds _nextPresentationTimeStamp) + { + nextPresentationTimeStamp = _nextPresentationTimeStamp; + } + // Processes keyboard events to generate virtual manipulation events, note that it doesn't make the manipulation itself! void processKeyboard(CVirtualGimbalEvent* output, uint32_t& count, std::span events) { + count = 0u; + if (!output) { - count = events.size(); + count = m_keysToVirtualEvents.size(); return; } - count = 0u; + const auto frameDeltaTime = std::chrono::duration_cast(nextPresentationTimeStamp - lastVirtualUpTimeStamp).count(); + assert(frameDeltaTime >= 0.f); - if (events.empty()) - return; + for (auto& [key, info] : m_keysToVirtualEvents) + { + info.dtAction = 0.f; - const auto timestamp = getEventGenerationTimestamp(); + /* + if a key was already being held down from previous frames we compute with this + assumption that the key will be held down for this whole frame as well and its + delta action time is simply frame delta time + */ + + if (info.active) + info.dtAction = static_cast(frameDeltaTime); + } for (const auto& keyboardEvent : events) { auto request = m_keysToVirtualEvents.find(keyboardEvent.keyCode); - bool isKeyMapped = request != std::end(m_keysToVirtualEvents); - - if (isKeyMapped) + if (request != std::end(m_keysToVirtualEvents)) { - auto& key = request->first; auto& info = request->second; + auto& info = request->second; - if (keyboardEvent.keyCode == key) + if (keyboardEvent.action == nbl::ui::SKeyboardEvent::ECA_PRESSED) { - if (keyboardEvent.action == nbl::ui::SKeyboardEvent::ECA_PRESSED && !info.active) + if (!info.active) + { + const auto keyDeltaTime = std::chrono::duration_cast(nextPresentationTimeStamp - keyboardEvent.timeStamp).count(); + assert(keyDeltaTime >= 0); + info.active = true; - else if (keyboardEvent.action == nbl::ui::SKeyboardEvent::ECA_RELEASED) - info.active = false; + info.dtAction = keyDeltaTime; + } } + else if (keyboardEvent.action == nbl::ui::SKeyboardEvent::ECA_RELEASED) + info.active = false; + } + } - if (info.active) - { - const auto dtAction = std::chrono::duration_cast(timestamp - keyboardEvent.timeStamp).count(); - assert(dtAction >= 0); - - auto* virtualEvent = output + count; - assert(virtualEvent); // TODO: maybe just log error and return 0 count - - virtualEvent->type = info.type; - virtualEvent->magnitude = static_cast(dtAction); - ++count; - } + for (const auto& [key, info] : m_keysToVirtualEvents) + { + if (info.active) + { + auto* virtualEvent = output + count; + virtualEvent->type = info.type; + virtualEvent->magnitude = info.dtAction; + ++count; } } } @@ -144,18 +175,17 @@ class ICameraController : virtual public core::IReferenceCounted // Limited to Pan & Tilt rotation events, camera type implements how event magnitudes should be interpreted void processMouse(CVirtualGimbalEvent* output, uint32_t& count, std::span events) { + count = 0u; + if (!output) { count = 2u; return; } - count = 0u; - if (events.empty()) return; - const auto timestamp = getEventGenerationTimestamp(); double dYaw = {}, dPitch = {}; for (const auto& ev : events) @@ -184,16 +214,20 @@ class ICameraController : virtual public core::IReferenceCounted } } + void endInputProcessing() + { + lastVirtualUpTimeStamp = nextPresentationTimeStamp; + } + + inline const keys_to_virtual_events_t& getKeysToVirtualEvents() { return m_keysToVirtualEvents; } + protected: virtual void initKeysToEvent() = 0; private: keys_to_virtual_events_t m_keysToVirtualEvents; bool m_keysDown[CVirtualGimbalEvent::EventsCount] = {}; - - // exactly what our Nabla events do, actually I don't want users to pass timestamp since I know when it should be best to make a request -> just before generating events! - // TODO: need to think about this - inline std::chrono::microseconds getEventGenerationTimestamp() { return std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()); } + std::chrono::microseconds nextPresentationTimeStamp, lastVirtualUpTimeStamp; }; #if 0 // TOOD: update diff --git a/common/include/camera/IGimbal.hpp b/common/include/camera/IGimbal.hpp index db108ed7b..4f84e6013 100644 --- a/common/include/camera/IGimbal.hpp +++ b/common/include/camera/IGimbal.hpp @@ -42,21 +42,42 @@ namespace nbl::hlsl Scale = ScaleXInc | ScaleXDec | ScaleYInc | ScaleYDec | ScaleZInc | ScaleZDec }; - struct CRequestInfo - { - CRequestInfo() : type(None) {} - CRequestInfo(VirtualEventType _type) : type(_type) {} - ~CRequestInfo() = default; - - VirtualEventType type; - bool active = false; - }; - using manipulation_encode_t = float64_t; VirtualEventType type; manipulation_encode_t magnitude; + static constexpr std::string_view virtualEventToString(VirtualEventType event) + { + switch (event) + { + case MoveForward: return "MoveForward"; + case MoveBackward: return "MoveBackward"; + case MoveLeft: return "MoveLeft"; + case MoveRight: return "MoveRight"; + case MoveUp: return "MoveUp"; + case MoveDown: return "MoveDown"; + case TiltUp: return "TiltUp"; + case TiltDown: return "TiltDown"; + case PanLeft: return "PanLeft"; + case PanRight: return "PanRight"; + case RollLeft: return "RollLeft"; + case RollRight: return "RollRight"; + case ScaleXInc: return "ScaleXInc"; + case ScaleXDec: return "ScaleXDec"; + case ScaleYInc: return "ScaleYInc"; + case ScaleYDec: return "ScaleYDec"; + case ScaleZInc: return "ScaleZInc"; + case ScaleZDec: return "ScaleZDec"; + case Translate: return "Translate"; + case Rotate: return "Rotate"; + case Scale: return "Scale"; + case None: return "None"; + default: return "Unknown"; + } + } + + static inline constexpr auto VirtualEventsTypeTable = []() { std::array output; From 03a8bf8bd013d0a7a99c190a0088fd4d188296b6 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Thu, 7 Nov 2024 20:59:07 +0100 Subject: [PATCH 029/205] create keysmapping.hpp with displayKeyMappingsAndVirtualStates utility for a camera, update CVirtualGimbalEvent::VirtualEventType, fix a bitmask bug, add pure virtual getAllowedVirtualEvents to ICamera interface, update sources --- 61_UI/include/common.hpp | 4 + 61_UI/include/keysmapping.hpp | 205 +++++++++++++++++++++++ 61_UI/main.cpp | 35 +--- common/include/CFPSCamera.hpp | 13 +- common/include/ICamera.hpp | 3 + common/include/camera/ICameraControl.hpp | 82 ++++----- common/include/camera/IGimbal.hpp | 7 +- 7 files changed, 272 insertions(+), 77 deletions(-) create mode 100644 61_UI/include/keysmapping.hpp diff --git a/61_UI/include/common.hpp b/61_UI/include/common.hpp index cf8afeea8..eb1a58c14 100644 --- a/61_UI/include/common.hpp +++ b/61_UI/include/common.hpp @@ -3,6 +3,8 @@ #include +#include + // common api #include "CFPSCamera.hpp" #include "SimpleWindowedApplication.hpp" @@ -22,4 +24,6 @@ using namespace video; using namespace scene; using namespace geometrycreator; +using matrix_precision_t = float32_t; + #endif // __NBL_THIS_EXAMPLE_COMMON_H_INCLUDED__ \ No newline at end of file diff --git a/61_UI/include/keysmapping.hpp b/61_UI/include/keysmapping.hpp new file mode 100644 index 000000000..c2466853e --- /dev/null +++ b/61_UI/include/keysmapping.hpp @@ -0,0 +1,205 @@ +#ifndef __NBL_KEYSMAPPING_H_INCLUDED__ +#define __NBL_KEYSMAPPING_H_INCLUDED__ + +#include "common.hpp" + +template +void displayKeyMappingsAndVirtualStates(ICamera* camera) +{ + static bool addMode = false; + static bool pendingChanges = false; + static std::unordered_map> tempKeyMappings; + static CVirtualGimbalEvent::VirtualEventType selectedEventType = CVirtualGimbalEvent::VirtualEventType::MoveForward; + static ui::E_KEY_CODE newKey = ui::E_KEY_CODE::EKC_A; + + const uint32_t allowedEventsMask = camera->getAllowedVirtualEvents(); + + std::vector allowedEvents; + for (const auto& eventType : CVirtualGimbalEvent::VirtualEventsTypeTable) + if ((eventType & allowedEventsMask)) + allowedEvents.push_back(eventType); + + ImGui::SetNextWindowSize(ImVec2(600, 400), ImGuiCond_FirstUseEver); + ImGui::SetNextWindowSizeConstraints(ImVec2(600, 400), ImVec2(600, 69000)); + + ImGui::Begin("Key Mappings & Virtual States", nullptr, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_AlwaysVerticalScrollbar); + + ImVec2 windowPadding = ImGui::GetStyle().WindowPadding; + float verticalPadding = ImGui::GetStyle().FramePadding.y; + + if (ImGui::Button("Add key", ImVec2(100, 30))) + addMode = !addMode; + + ImGui::Separator(); + + ImGui::BeginTable("KeyMappingsTable", 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("Delta Time (ms)", ImGuiTableColumnFlags_WidthStretch, 0.2f); + ImGui::TableSetupColumn("Actions", ImGuiTableColumnFlags_WidthStretch, 0.2f); + ImGui::TableHeadersRow(); + + const auto& keysToVirtualEvents = camera->getKeysToVirtualEvents(); + for (const auto& eventType : allowedEvents) + { + auto it = std::find_if(keysToVirtualEvents.begin(), keysToVirtualEvents.end(), [eventType](const auto& pair) + { + return pair.second.type == eventType; + }); + + if (it != keysToVirtualEvents.end()) + { + ImGui::TableNextRow(); + + const char* eventName = CVirtualGimbalEvent::virtualEventToString(eventType).data(); + ImGui::TableSetColumnIndex(0); + ImGui::AlignTextToFramePadding(); + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (ImGui::GetColumnWidth() - ImGui::CalcTextSize(eventName).x) * 0.5f); + ImGui::TextWrapped("%s", eventName); + + ImGui::TableSetColumnIndex(1); + std::vector mappedKeys; + for (const auto& [key, info] : keysToVirtualEvents) + if (info.type == eventType) + mappedKeys.push_back(key); + + if (!mappedKeys.empty()) + { + std::string concatenatedKeys; + for (size_t i = 0; i < mappedKeys.size(); ++i) + { + if (i > 0) + concatenatedKeys += " | "; + if (keysToVirtualEvents.at(mappedKeys[i]).active) + concatenatedKeys += "[" + std::string(1, ui::keyCodeToChar(mappedKeys[i], true)) + "]"; + else + concatenatedKeys += std::string(1, ui::keyCodeToChar(mappedKeys[i], true)); + } + ImGui::AlignTextToFramePadding(); + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (ImGui::GetColumnWidth() - ImGui::CalcTextSize(concatenatedKeys.c_str()).x) * 0.5f); + ImGui::TextWrapped("%s", concatenatedKeys.c_str()); + } + + ImGui::TableSetColumnIndex(2); + ImVec4 statusColor = it->second.active ? ImVec4(0.0f, 1.0f, 0.0f, 1.0f) : ImVec4(1.0f, 0.0f, 0.0f, 1.0f); + const char* statusText = it->second.active ? "Active" : "Inactive"; + ImGui::AlignTextToFramePadding(); + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (ImGui::GetColumnWidth() - ImGui::CalcTextSize(statusText).x) * 0.5f); + ImGui::TextColored(statusColor, "%s", statusText); + + ImGui::TableSetColumnIndex(3); + float accumulatedDelta = 0.0f; + for (const auto& [key, info] : keysToVirtualEvents) + if (info.type == eventType) + accumulatedDelta += info.dtAction; + + char deltaText[16]; + snprintf(deltaText, 16, "%.2f", accumulatedDelta); + ImGui::AlignTextToFramePadding(); + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (ImGui::GetColumnWidth() - ImGui::CalcTextSize(deltaText).x) * 0.5f); + ImGui::TextWrapped("%s", deltaText); + + ImGui::TableSetColumnIndex(4); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(5, 5)); + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (ImGui::GetColumnWidth() - 80) * 0.5f); + if (ImGui::Button(std::string("Delete##deleteKey" + std::to_string(static_cast(eventType))).c_str(), ImVec2(80, 30))) + { + camera->updateKeysToEvent([&](auto& keys) + { + for (auto it = keys.begin(); it != keys.end();) + { + if (it->second.type == eventType) + it = keys.erase(it); + else + ++it; + } + }); + pendingChanges = true; + } + ImGui::PopStyleVar(); + } + } + ImGui::EndTable(); + + if (addMode) + { + ImGui::Separator(); + + ImGui::BeginTable("AddKeyMappingTable", 3, ImGuiTableFlags_Borders | ImGuiTableFlags_Resizable | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchSame); + ImGui::TableSetupColumn("Virtual Event", ImGuiTableColumnFlags_WidthStretch, 0.33f); + ImGui::TableSetupColumn("Key", ImGuiTableColumnFlags_WidthStretch, 0.33f); + ImGui::TableSetupColumn("Actions", ImGuiTableColumnFlags_WidthStretch, 0.33f); + ImGui::TableHeadersRow(); + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::AlignTextToFramePadding(); + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + verticalPadding); + ImGui::PushItemWidth(ImGui::GetContentRegionAvail().x - 2 * ImGui::GetStyle().FramePadding.x); + if (ImGui::BeginCombo("##selectEvent", CVirtualGimbalEvent::virtualEventToString(selectedEventType).data(), ImGuiComboFlags_None)) + { + for (const auto& eventType : allowedEvents) + { + bool isSelected = (selectedEventType == eventType); + if (ImGui::Selectable(CVirtualGimbalEvent::virtualEventToString(eventType).data(), isSelected)) + selectedEventType = eventType; + if (isSelected) + ImGui::SetItemDefaultFocus(); + } + ImGui::EndCombo(); + } + ImGui::PopItemWidth(); + + ImGui::TableSetColumnIndex(1); + ImGui::AlignTextToFramePadding(); + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + verticalPadding); + ImGui::PushItemWidth(ImGui::GetContentRegionAvail().x - 2 * ImGui::GetStyle().FramePadding.x); + char newKeyDisplay[2] = { ui::keyCodeToChar(newKey, true), '\0' }; + if (ImGui::BeginCombo("##selectKey", newKeyDisplay, ImGuiComboFlags_None)) + { + for (int i = ui::E_KEY_CODE::EKC_A; i <= ui::E_KEY_CODE::EKC_Z; ++i) + { + bool isSelected = (newKey == static_cast(i)); + char label[2] = { ui::keyCodeToChar(static_cast(i), true), '\0' }; + if (ImGui::Selectable(label, isSelected)) + { + auto duplicateKey = std::find_if(tempKeyMappings[selectedEventType].begin(), tempKeyMappings[selectedEventType].end(), + [i](const auto& key) { + return key == static_cast(i); + }); + + if (duplicateKey == tempKeyMappings[selectedEventType].end()) + newKey = static_cast(i); + } + if (isSelected) + ImGui::SetItemDefaultFocus(); + } + ImGui::EndCombo(); + } + ImGui::PopItemWidth(); + + ImGui::TableSetColumnIndex(2); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(5, 5)); + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (ImGui::GetColumnWidth() - 100) * 0.5f); + if (ImGui::Button("Confirm Add", ImVec2(100, 30))) + { + tempKeyMappings[selectedEventType].push_back(newKey); + pendingChanges = true; + addMode = false; + + camera->updateKeysToEvent([&](auto& keys) + { + keys.emplace(newKey, selectedEventType); + }); + } + ImGui::PopStyleVar(); + + ImGui::EndTable(); + } + + ImGui::Dummy(ImVec2(0.0f, verticalPadding)); + ImGui::End(); +} + +#endif // __NBL_KEYSMAPPING_H_INCLUDED__ \ No newline at end of file diff --git a/61_UI/main.cpp b/61_UI/main.cpp index 334cc416a..e7a4f3305 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -3,11 +3,11 @@ // For conditions of distribution and use, see copyright notice in nabla.h #include "common.hpp" +#include "keysmapping.hpp" #include "camera/CCubeProjection.hpp" #include "glm/glm/ext/matrix_clip_space.hpp" // TODO: TESTING // FPS Camera, TESTS -using matrix_precision_t = float32_t; using camera_t = CFPSCamera; using projection_t = IProjection>; // TODO: temporary -> projections will own/reference cameras @@ -493,38 +493,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGui::End(); } - { - ImGui::Begin("Key Mappings & Virtual States"); - - ImGui::Text("Key Mappings"); - ImGui::Separator(); - - const auto& keysToVirtualEvents = camera->getKeysToVirtualEvents(); - - for (const auto& [key, info] : keysToVirtualEvents) - { - const char physicalChar = ui::keyCodeToChar(key, true); - const auto eventName = CVirtualGimbalEvent::virtualEventToString(info.type); - - ImGui::Text("Key: %s", &physicalChar); - ImGui::SameLine(); - ImGui::Text("Virtual Event: %s", eventName.data()); - ImGui::SameLine(); - - if (info.active) - { - ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), "Active"); - ImGui::SameLine(); - ImGui::Text("Delta Time: %.2f ms", info.dtAction); - } - else - { - ImGui::TextColored(ImVec4(1.0f, 0.0f, 0.0f, 1.0f), "Inactive"); - } - } - - ImGui::End(); - } + displayKeyMappingsAndVirtualStates(camera.get()); ImGui::End(); } diff --git a/common/include/CFPSCamera.hpp b/common/include/CFPSCamera.hpp index 486063dab..37024b6dd 100644 --- a/common/include/CFPSCamera.hpp +++ b/common/include/CFPSCamera.hpp @@ -32,13 +32,12 @@ class CFPSCamera final : public ICamera virtual void manipulate(std::span virtualEvents) override { - constexpr auto AllowedEvents = CVirtualGimbalEvent::MoveForward | CVirtualGimbalEvent::MoveBackward | CVirtualGimbalEvent::MoveRight | CVirtualGimbalEvent::MoveLeft | CVirtualGimbalEvent::TiltUp | CVirtualGimbalEvent::TiltDown | CVirtualGimbalEvent::PanRight | CVirtualGimbalEvent::PanLeft; constexpr float MoveSpeedScale = 0.01f, RotateSpeedScale = 0.003f, MaxVerticalAngle = glm::radians(88.0f), MinVerticalAngle = -MaxVerticalAngle; if (!virtualEvents.size()) return; - const auto impulse = m_gimbal.accumulate(virtualEvents); + const auto impulse = m_gimbal.accumulate(virtualEvents); const auto& gForward = m_gimbal.getZAxis(); const float currentPitch = atan2(glm::length(glm::vec2(gForward.x, gForward.z)), gForward.y) - glm::half_pi(), currentYaw = atan2(gForward.x, gForward.z); @@ -53,6 +52,11 @@ class CFPSCamera final : public ICamera m_gimbal.end(); } + virtual const uint32_t getAllowedVirtualEvents() override + { + return AllowedVirtualEvents; + } + private: void initKeysToEvent() override { @@ -62,10 +66,15 @@ class CFPSCamera final : public ICamera keys[ui::E_KEY_CODE::EKC_S] = CVirtualGimbalEvent::MoveBackward; keys[ui::E_KEY_CODE::EKC_A] = CVirtualGimbalEvent::MoveLeft; keys[ui::E_KEY_CODE::EKC_D] = CVirtualGimbalEvent::MoveRight; + keys[ui::E_KEY_CODE::EKC_I] = CVirtualGimbalEvent::TiltDown; + keys[ui::E_KEY_CODE::EKC_K] = CVirtualGimbalEvent::TiltUp; + keys[ui::E_KEY_CODE::EKC_J] = CVirtualGimbalEvent::PanLeft; + keys[ui::E_KEY_CODE::EKC_L] = CVirtualGimbalEvent::PanRight; }); } traits_t::gimbal_t m_gimbal; + static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::MoveForward | CVirtualGimbalEvent::MoveBackward | CVirtualGimbalEvent::MoveRight | CVirtualGimbalEvent::MoveLeft | CVirtualGimbalEvent::TiltUp | CVirtualGimbalEvent::TiltDown | CVirtualGimbalEvent::PanRight | CVirtualGimbalEvent::PanLeft; }; } diff --git a/common/include/ICamera.hpp b/common/include/ICamera.hpp index 4556a8097..e681ff26b 100644 --- a/common/include/ICamera.hpp +++ b/common/include/ICamera.hpp @@ -28,6 +28,9 @@ class ICamera : public ICameraController // Returns a gimbal which *models the camera view*, note that a camera type implementation may have multiple gimbals under the hood virtual const Traits::gimbal_t& getGimbal() = 0u; + + // VirtualEventType bitmask for a camera view gimbal manipulation requests filtering + virtual const uint32_t getAllowedVirtualEvents() = 0u; }; } diff --git a/common/include/camera/ICameraControl.hpp b/common/include/camera/ICameraControl.hpp index 71e27773e..f017eff14 100644 --- a/common/include/camera/ICameraControl.hpp +++ b/common/include/camera/ICameraControl.hpp @@ -112,61 +112,66 @@ class ICameraController : virtual public core::IReferenceCounted void processKeyboard(CVirtualGimbalEvent* output, uint32_t& count, std::span events) { count = 0u; + const auto mappedVirtualEventsCount = m_keysToVirtualEvents.size(); if (!output) { - count = m_keysToVirtualEvents.size(); + count = mappedVirtualEventsCount; return; } - const auto frameDeltaTime = std::chrono::duration_cast(nextPresentationTimeStamp - lastVirtualUpTimeStamp).count(); - assert(frameDeltaTime >= 0.f); - - for (auto& [key, info] : m_keysToVirtualEvents) + if (mappedVirtualEventsCount) { - info.dtAction = 0.f; + const auto frameDeltaTime = std::chrono::duration_cast(nextPresentationTimeStamp - lastVirtualUpTimeStamp).count(); + assert(frameDeltaTime >= 0.f); - /* - if a key was already being held down from previous frames we compute with this - assumption that the key will be held down for this whole frame as well and its - delta action time is simply frame delta time - */ + for (auto& [key, info] : m_keysToVirtualEvents) + { + info.dtAction = 0.f; - if (info.active) - info.dtAction = static_cast(frameDeltaTime); - } + /* + if a key was already being held down from previous frames we compute with this + assumption that the key will be held down for this whole frame as well and its + delta action is simply frame delta time + */ - for (const auto& keyboardEvent : events) - { - auto request = m_keysToVirtualEvents.find(keyboardEvent.keyCode); - if (request != std::end(m_keysToVirtualEvents)) - { - auto& info = request->second; + if (info.active) + info.dtAction = frameDeltaTime; + } - if (keyboardEvent.action == nbl::ui::SKeyboardEvent::ECA_PRESSED) + for (const auto& keyboardEvent : events) + { + auto request = m_keysToVirtualEvents.find(keyboardEvent.keyCode); + if (request != std::end(m_keysToVirtualEvents)) { - if (!info.active) - { - const auto keyDeltaTime = std::chrono::duration_cast(nextPresentationTimeStamp - keyboardEvent.timeStamp).count(); - assert(keyDeltaTime >= 0); + auto& info = request->second; - info.active = true; - info.dtAction = keyDeltaTime; + if (keyboardEvent.action == nbl::ui::SKeyboardEvent::ECA_PRESSED) + { + if (!info.active) + { + // and if a key has been pressed after its last release event then first delta action is the key delta + const auto keyDeltaTime = std::chrono::duration_cast(nextPresentationTimeStamp - keyboardEvent.timeStamp).count(); + assert(keyDeltaTime >= 0); + + info.active = true; + info.dtAction = keyDeltaTime; + } } + else if (keyboardEvent.action == nbl::ui::SKeyboardEvent::ECA_RELEASED) + info.active = false; } - else if (keyboardEvent.action == nbl::ui::SKeyboardEvent::ECA_RELEASED) - info.active = false; } - } - for (const auto& [key, info] : m_keysToVirtualEvents) - { - if (info.active) + for (const auto& [key, info] : m_keysToVirtualEvents) { - auto* virtualEvent = output + count; - virtualEvent->type = info.type; - virtualEvent->magnitude = info.dtAction; - ++count; + if (info.active) + { + auto* virtualEvent = output + count; + virtualEvent->type = info.type; + virtualEvent->magnitude = info.dtAction; + ++count; + } } } } @@ -198,7 +203,6 @@ class ICameraController : virtual public core::IReferenceCounted if (dPitch) { auto* pitch = output + count; - assert(pitch); // TODO: maybe just log error and return 0 count pitch->type = dPitch > 0.f ? CVirtualGimbalEvent::TiltUp : CVirtualGimbalEvent::TiltDown; pitch->magnitude = std::abs(dPitch); count++; @@ -227,7 +231,7 @@ class ICameraController : virtual public core::IReferenceCounted private: keys_to_virtual_events_t m_keysToVirtualEvents; bool m_keysDown[CVirtualGimbalEvent::EventsCount] = {}; - std::chrono::microseconds nextPresentationTimeStamp, lastVirtualUpTimeStamp; + std::chrono::microseconds nextPresentationTimeStamp = {}, lastVirtualUpTimeStamp = {}; }; #if 0 // TOOD: update diff --git a/common/include/camera/IGimbal.hpp b/common/include/camera/IGimbal.hpp index 4f84e6013..fed8f9f7a 100644 --- a/common/include/camera/IGimbal.hpp +++ b/common/include/camera/IGimbal.hpp @@ -39,7 +39,9 @@ namespace nbl::hlsl // Grouped bitmasks Translate = MoveForward | MoveBackward | MoveLeft | MoveRight | MoveUp | MoveDown, Rotate = TiltUp | TiltDown | PanLeft | PanRight | RollLeft | RollRight, - Scale = ScaleXInc | ScaleXDec | ScaleYInc | ScaleYDec | ScaleZInc | ScaleZDec + Scale = ScaleXInc | ScaleXDec | ScaleYInc | ScaleYDec | ScaleZInc | ScaleZDec, + + All = Translate | Rotate | Scale }; using manipulation_encode_t = float64_t; @@ -77,13 +79,12 @@ namespace nbl::hlsl } } - static inline constexpr auto VirtualEventsTypeTable = []() { std::array output; for (uint16_t i = 0u; i < EventsCount; ++i) - output[i] = static_cast(i); + output[i] = static_cast(core::createBitmask({ i })); return output; }(); From 2f50f96b827314c358bd34322c6774084910cd77 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Fri, 8 Nov 2024 16:26:30 +0100 Subject: [PATCH 030/205] create CCameraController, update ICameraController, have mouse controller which all codes can be mapped to virtual keys, update virtual mapping state utility to display & map controllers to virtual events (mouse & keyboard), remove controllers from camera interface --- 61_UI/include/common.hpp | 1 + 61_UI/include/keysmapping.hpp | 338 +++++++++++--------- 61_UI/main.cpp | 45 ++- common/include/CFPSCamera.hpp | 27 +- common/include/ICamera.hpp | 74 ++++- common/include/camera/CCameraController.hpp | 260 +++++++++++++++ common/include/camera/ICameraControl.hpp | 202 ++---------- common/include/camera/ICameraController.hpp | 68 ++++ common/include/camera/IGimbal.hpp | 4 +- 9 files changed, 638 insertions(+), 381 deletions(-) create mode 100644 common/include/camera/CCameraController.hpp create mode 100644 common/include/camera/ICameraController.hpp diff --git a/61_UI/include/common.hpp b/61_UI/include/common.hpp index eb1a58c14..b509031cf 100644 --- a/61_UI/include/common.hpp +++ b/61_UI/include/common.hpp @@ -7,6 +7,7 @@ // common api #include "CFPSCamera.hpp" +#include "camera/CCameraController.hpp" #include "SimpleWindowedApplication.hpp" #include "CEventCallback.hpp" diff --git a/61_UI/include/keysmapping.hpp b/61_UI/include/keysmapping.hpp index c2466853e..d30fa6b9c 100644 --- a/61_UI/include/keysmapping.hpp +++ b/61_UI/include/keysmapping.hpp @@ -2,203 +2,225 @@ #define __NBL_KEYSMAPPING_H_INCLUDED__ #include "common.hpp" +#include "camera/CCameraController.hpp" + +template +void handleAddMapping(const char* tableID, CCameraController* controller, ICameraController::ControllerType activeController, CVirtualGimbalEvent::VirtualEventType& selectedEventType, ui::E_KEY_CODE& newKey, ui::E_MOUSE_CODE& newMouseCode, bool& addMode) +{ + ImGui::BeginTable(tableID, 3, ImGuiTableFlags_Borders | ImGuiTableFlags_Resizable | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchSame); + ImGui::TableSetupColumn("Virtual Event", ImGuiTableColumnFlags_WidthStretch, 0.33f); + ImGui::TableSetupColumn("Key", ImGuiTableColumnFlags_WidthStretch, 0.33f); + ImGui::TableSetupColumn("Actions", ImGuiTableColumnFlags_WidthStretch, 0.33f); + 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 (activeController == ICameraController::Keyboard) + { + char newKeyDisplay[2] = { ui::keyCodeToChar(newKey, true), '\0' }; + if (ImGui::BeginCombo("##selectKey", newKeyDisplay)) + { + for (int i = ui::E_KEY_CODE::EKC_A; i <= ui::E_KEY_CODE::EKC_Z; ++i) + { + bool isSelected = (newKey == static_cast(i)); + char label[2] = { ui::keyCodeToChar(static_cast(i), true), '\0' }; + if (ImGui::Selectable(label, 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", ImVec2(100, 30))) + { + if (activeController == ICameraController::Keyboard) + controller->updateKeyboardMapping([&](auto& keys) { keys[newKey] = selectedEventType; }); + else + controller->updateMouseMapping([&](auto& mouse) { mouse[newMouseCode] = selectedEventType; }); + addMode = false; + } + + ImGui::EndTable(); +} template -void displayKeyMappingsAndVirtualStates(ICamera* camera) +void displayKeyMappingsAndVirtualStates(CCameraController* controller) { - static bool addMode = false; - static bool pendingChanges = false; - static std::unordered_map> tempKeyMappings; + static bool addMode = false, pendingChanges = false; static CVirtualGimbalEvent::VirtualEventType selectedEventType = CVirtualGimbalEvent::VirtualEventType::MoveForward; static ui::E_KEY_CODE newKey = ui::E_KEY_CODE::EKC_A; + static ui::E_MOUSE_CODE newMouseCode = ui::EMC_LEFT_BUTTON; + static ICameraController::ControllerType activeController = ICameraController::Keyboard; - const uint32_t allowedEventsMask = camera->getAllowedVirtualEvents(); - - std::vector allowedEvents; - for (const auto& eventType : CVirtualGimbalEvent::VirtualEventsTypeTable) - if ((eventType & allowedEventsMask)) - allowedEvents.push_back(eventType); + const auto& keyboardMappings = controller->getKeyboardVirtualEventMap(); + const auto& mouseMappings = controller->getMouseVirtualEventMap(); ImGui::SetNextWindowSize(ImVec2(600, 400), ImGuiCond_FirstUseEver); ImGui::SetNextWindowSizeConstraints(ImVec2(600, 400), ImVec2(600, 69000)); - ImGui::Begin("Key Mappings & Virtual States", nullptr, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_AlwaysVerticalScrollbar); + ImGui::Begin("Controller Mappings & Virtual States", nullptr, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_AlwaysVerticalScrollbar); - ImVec2 windowPadding = ImGui::GetStyle().WindowPadding; - float verticalPadding = ImGui::GetStyle().FramePadding.y; + if (ImGui::BeginTabBar("ControllersTabBar")) + { + if (ImGui::BeginTabItem("Keyboard")) + { + activeController = ICameraController::Keyboard; + ImGui::Separator(); - if (ImGui::Button("Add key", ImVec2(100, 30))) - addMode = !addMode; + if (ImGui::Button("Add key", ImVec2(100, 30))) + addMode = !addMode; - ImGui::Separator(); + ImGui::Separator(); - ImGui::BeginTable("KeyMappingsTable", 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("Delta Time (ms)", ImGuiTableColumnFlags_WidthStretch, 0.2f); - ImGui::TableSetupColumn("Actions", ImGuiTableColumnFlags_WidthStretch, 0.2f); - ImGui::TableHeadersRow(); + 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(); - const auto& keysToVirtualEvents = camera->getKeysToVirtualEvents(); - for (const auto& eventType : allowedEvents) - { - auto it = std::find_if(keysToVirtualEvents.begin(), keysToVirtualEvents.end(), [eventType](const auto& pair) - { - return pair.second.type == eventType; - }); + for (const auto& [keyboardCode, hash] : keyboardMappings) + { + ImGui::TableNextRow(); - if (it != keysToVirtualEvents.end()) - { - ImGui::TableNextRow(); + const char* eventName = CVirtualGimbalEvent::virtualEventToString(hash.event.type).data(); + ImGui::TableSetColumnIndex(0); + ImGui::AlignTextToFramePadding(); + ImGui::TextWrapped("%s", eventName); - const char* eventName = CVirtualGimbalEvent::virtualEventToString(eventType).data(); - ImGui::TableSetColumnIndex(0); - ImGui::AlignTextToFramePadding(); - ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (ImGui::GetColumnWidth() - ImGui::CalcTextSize(eventName).x) * 0.5f); - ImGui::TextWrapped("%s", eventName); + ImGui::TableSetColumnIndex(1); + std::string keyString(1, ui::keyCodeToChar(keyboardCode, true)); + ImGui::AlignTextToFramePadding(); + ImGui::TextWrapped("%s", keyString.c_str()); - ImGui::TableSetColumnIndex(1); - std::vector mappedKeys; - for (const auto& [key, info] : keysToVirtualEvents) - if (info.type == eventType) - mappedKeys.push_back(key); + ImGui::TableSetColumnIndex(2); + bool isActive = (hash.event.magnitude > 0); + ImVec4 statusColor = isActive ? ImVec4(0.0f, 1.0f, 0.0f, 1.0f) : ImVec4(1.0f, 0.0f, 0.0f, 1.0f); + ImGui::TextColored(statusColor, "%s", isActive ? "Active" : "Inactive"); - if (!mappedKeys.empty()) - { - std::string concatenatedKeys; - for (size_t i = 0; i < mappedKeys.size(); ++i) - { - if (i > 0) - concatenatedKeys += " | "; - if (keysToVirtualEvents.at(mappedKeys[i]).active) - concatenatedKeys += "[" + std::string(1, ui::keyCodeToChar(mappedKeys[i], true)) + "]"; - else - concatenatedKeys += std::string(1, ui::keyCodeToChar(mappedKeys[i], true)); - } + ImGui::TableSetColumnIndex(3); + std::array deltaText; + snprintf(deltaText.data(), deltaText.size(), "%.2f", hash.event.magnitude); ImGui::AlignTextToFramePadding(); - ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (ImGui::GetColumnWidth() - ImGui::CalcTextSize(concatenatedKeys.c_str()).x) * 0.5f); - ImGui::TextWrapped("%s", concatenatedKeys.c_str()); - } + ImGui::TextWrapped("%s", deltaText.data()); - ImGui::TableSetColumnIndex(2); - ImVec4 statusColor = it->second.active ? ImVec4(0.0f, 1.0f, 0.0f, 1.0f) : ImVec4(1.0f, 0.0f, 0.0f, 1.0f); - const char* statusText = it->second.active ? "Active" : "Inactive"; - ImGui::AlignTextToFramePadding(); - ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (ImGui::GetColumnWidth() - ImGui::CalcTextSize(statusText).x) * 0.5f); - ImGui::TextColored(statusColor, "%s", statusText); - - ImGui::TableSetColumnIndex(3); - float accumulatedDelta = 0.0f; - for (const auto& [key, info] : keysToVirtualEvents) - if (info.type == eventType) - accumulatedDelta += info.dtAction; - - char deltaText[16]; - snprintf(deltaText, 16, "%.2f", accumulatedDelta); - ImGui::AlignTextToFramePadding(); - ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (ImGui::GetColumnWidth() - ImGui::CalcTextSize(deltaText).x) * 0.5f); - ImGui::TextWrapped("%s", deltaText); - - ImGui::TableSetColumnIndex(4); - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(5, 5)); - ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (ImGui::GetColumnWidth() - 80) * 0.5f); - if (ImGui::Button(std::string("Delete##deleteKey" + std::to_string(static_cast(eventType))).c_str(), ImVec2(80, 30))) - { - camera->updateKeysToEvent([&](auto& keys) + ImGui::TableSetColumnIndex(4); + if (ImGui::Button(("Delete##deleteKey" + std::to_string(static_cast(hash.event.type))).c_str())) { - for (auto it = keys.begin(); it != keys.end();) - { - if (it->second.type == eventType) - it = keys.erase(it); - else - ++it; - } - }); - pendingChanges = true; + controller->updateKeyboardMapping([&](auto& keys) { keys.erase(keyboardCode); }); + pendingChanges = true; + break; + } } - ImGui::PopStyleVar(); - } - } - ImGui::EndTable(); + ImGui::EndTable(); - if (addMode) - { - ImGui::Separator(); - - ImGui::BeginTable("AddKeyMappingTable", 3, ImGuiTableFlags_Borders | ImGuiTableFlags_Resizable | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchSame); - ImGui::TableSetupColumn("Virtual Event", ImGuiTableColumnFlags_WidthStretch, 0.33f); - ImGui::TableSetupColumn("Key", ImGuiTableColumnFlags_WidthStretch, 0.33f); - ImGui::TableSetupColumn("Actions", ImGuiTableColumnFlags_WidthStretch, 0.33f); - ImGui::TableHeadersRow(); - - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::AlignTextToFramePadding(); - ImGui::SetCursorPosY(ImGui::GetCursorPosY() + verticalPadding); - ImGui::PushItemWidth(ImGui::GetContentRegionAvail().x - 2 * ImGui::GetStyle().FramePadding.x); - if (ImGui::BeginCombo("##selectEvent", CVirtualGimbalEvent::virtualEventToString(selectedEventType).data(), ImGuiComboFlags_None)) - { - for (const auto& eventType : allowedEvents) + if (addMode) { - bool isSelected = (selectedEventType == eventType); - if (ImGui::Selectable(CVirtualGimbalEvent::virtualEventToString(eventType).data(), isSelected)) - selectedEventType = eventType; - if (isSelected) - ImGui::SetItemDefaultFocus(); + ImGui::Separator(); + handleAddMapping("AddKeyboardMappingTable", controller, activeController, selectedEventType, newKey, newMouseCode, addMode); } - ImGui::EndCombo(); + + ImGui::EndTabItem(); } - ImGui::PopItemWidth(); - ImGui::TableSetColumnIndex(1); - ImGui::AlignTextToFramePadding(); - ImGui::SetCursorPosY(ImGui::GetCursorPosY() + verticalPadding); - ImGui::PushItemWidth(ImGui::GetContentRegionAvail().x - 2 * ImGui::GetStyle().FramePadding.x); - char newKeyDisplay[2] = { ui::keyCodeToChar(newKey, true), '\0' }; - if (ImGui::BeginCombo("##selectKey", newKeyDisplay, ImGuiComboFlags_None)) + if (ImGui::BeginTabItem("Mouse")) { - for (int i = ui::E_KEY_CODE::EKC_A; i <= ui::E_KEY_CODE::EKC_Z; ++i) + activeController = ICameraController::Mouse; + ImGui::Separator(); + + if (ImGui::Button("Add key", ImVec2(100, 30))) + addMode = !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) { - bool isSelected = (newKey == static_cast(i)); - char label[2] = { ui::keyCodeToChar(static_cast(i), true), '\0' }; - if (ImGui::Selectable(label, isSelected)) - { - auto duplicateKey = std::find_if(tempKeyMappings[selectedEventType].begin(), tempKeyMappings[selectedEventType].end(), - [i](const auto& key) { - return key == static_cast(i); - }); + 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); - if (duplicateKey == tempKeyMappings[selectedEventType].end()) - newKey = static_cast(i); + ImGui::TableSetColumnIndex(2); + bool isActive = (hash.event.magnitude > 0); + ImVec4 statusColor = isActive ? ImVec4(0.0f, 1.0f, 0.0f, 1.0f) : ImVec4(1.0f, 0.0f, 0.0f, 1.0f); + ImGui::TextColored(statusColor, "%s", isActive ? "Active" : "Inactive"); + + ImGui::TableSetColumnIndex(3); + std::array deltaText; + snprintf(deltaText.data(), deltaText.size(), "%.2f", hash.event.magnitude); + ImGui::AlignTextToFramePadding(); + ImGui::TextWrapped("%s", deltaText.data()); + + ImGui::TableSetColumnIndex(4); + if (ImGui::Button(("Delete##deleteMouse" + std::to_string(static_cast(hash.event.type))).c_str())) + { + controller->updateMouseMapping([&](auto& mouse) { mouse.erase(mouseCode); }); + pendingChanges = true; + break; } - if (isSelected) - ImGui::SetItemDefaultFocus(); } - ImGui::EndCombo(); - } - ImGui::PopItemWidth(); + ImGui::EndTable(); - ImGui::TableSetColumnIndex(2); - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(5, 5)); - ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (ImGui::GetColumnWidth() - 100) * 0.5f); - if (ImGui::Button("Confirm Add", ImVec2(100, 30))) - { - tempKeyMappings[selectedEventType].push_back(newKey); - pendingChanges = true; - addMode = false; - - camera->updateKeysToEvent([&](auto& keys) + if (addMode) { - keys.emplace(newKey, selectedEventType); - }); + ImGui::Separator(); + handleAddMapping("AddMouseMappingTable", controller, activeController, selectedEventType, newKey, newMouseCode, addMode); + } + ImGui::EndTabItem(); } - ImGui::PopStyleVar(); - ImGui::EndTable(); + ImGui::EndTabBar(); } - ImGui::Dummy(ImVec2(0.0f, verticalPadding)); ImGui::End(); } diff --git a/61_UI/main.cpp b/61_UI/main.cpp index e7a4f3305..b57c0b5d9 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -9,6 +9,7 @@ // FPS Camera, TESTS using camera_t = CFPSCamera; +using controller_t = CCameraController; using projection_t = IProjection>; // TODO: temporary -> projections will own/reference cameras /* @@ -493,7 +494,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGui::End(); } - displayKeyMappingsAndVirtualStates(camera.get()); + displayKeyMappingsAndVirtualStates(controller.get()); ImGui::End(); } @@ -511,6 +512,29 @@ class UISampleApp final : public examples::SimpleWindowedApplication const float32_t3 position(cosf(camYAngle)* cosf(camXAngle)* transformParams.camDistance, sinf(camXAngle)* transformParams.camDistance, sinf(camYAngle)* cosf(camXAngle)* transformParams.camDistance); projection->setMatrix(buildProjectionMatrixPerspectiveFovLH(glm::radians(fov), float(m_window->getWidth()) / float(m_window->getHeight()), zNear, zFar)); camera = make_smart_refctd_ptr(position); + controller = make_smart_refctd_ptr(core::smart_refctd_ptr(camera)); + + // init keyboard map + controller->updateKeyboardMapping([](auto& keys) + { + keys[ui::E_KEY_CODE::EKC_W] = CVirtualGimbalEvent::MoveForward; + keys[ui::E_KEY_CODE::EKC_S] = CVirtualGimbalEvent::MoveBackward; + keys[ui::E_KEY_CODE::EKC_A] = CVirtualGimbalEvent::MoveLeft; + keys[ui::E_KEY_CODE::EKC_D] = CVirtualGimbalEvent::MoveRight; + keys[ui::E_KEY_CODE::EKC_I] = CVirtualGimbalEvent::TiltDown; + keys[ui::E_KEY_CODE::EKC_K] = CVirtualGimbalEvent::TiltUp; + keys[ui::E_KEY_CODE::EKC_J] = CVirtualGimbalEvent::PanLeft; + keys[ui::E_KEY_CODE::EKC_L] = CVirtualGimbalEvent::PanRight; + }); + + // init mouse map + controller->updateMouseMapping([](auto& keys) + { + keys[ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_X] = CVirtualGimbalEvent::PanRight; + keys[ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_X] = CVirtualGimbalEvent::PanLeft; + keys[ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_Y] = CVirtualGimbalEvent::TiltUp; + keys[ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_Y] = CVirtualGimbalEvent::TiltDown; + }); return true; } @@ -742,15 +766,19 @@ class UISampleApp final : public examples::SimpleWindowedApplication if (move) { - static std::vector virtualEvents(CVirtualGimbalEvent::VirtualEventsTypeTable.size() * 2); - uint32_t vEventsMouseCount, vEventsKeyboardCount; + static std::vector virtualEvents(0x45); + uint32_t vCount; - camera->beginInputProcessing(nextPresentationTimestamp); - camera->processKeyboard(virtualEvents.data(), vEventsKeyboardCount, params.keyboardEvents); - camera->processMouse(virtualEvents.data() + vEventsKeyboardCount, vEventsMouseCount, params.mouseEvents); - camera->endInputProcessing(); + controller->beginInputProcessing(nextPresentationTimestamp); + controller->process(nullptr, vCount); - camera->manipulate({ virtualEvents.data(), vEventsMouseCount + vEventsKeyboardCount }); + if (virtualEvents.size() < vCount) + virtualEvents.resize(vCount); + + controller->process(virtualEvents.data(), vCount, { params.keyboardEvents, params.mouseEvents }); + controller->endInputProcessing(); + + controller->getCamera()->manipulate({ virtualEvents.data(), vCount }); } pass.ui.manager->update(params); @@ -796,6 +824,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication smart_refctd_ptr projection = make_smart_refctd_ptr(); // TMP! core::smart_refctd_ptr> camera; + core::smart_refctd_ptr controller; video::CDumbPresentationOracle oracle; 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 diff --git a/common/include/CFPSCamera.hpp b/common/include/CFPSCamera.hpp index 37024b6dd..3f95de0dc 100644 --- a/common/include/CFPSCamera.hpp +++ b/common/include/CFPSCamera.hpp @@ -16,16 +16,12 @@ class CFPSCamera final : public ICamera { public: using base_t = ICamera; - using traits_t = typename base_t::Traits; - CFPSCamera(const vector& position, glm::quat orientation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f)) - : base_t(), m_gimbal({ .position = position, .orientation = orientation }) - { - initKeysToEvent(); - } + CFPSCamera(const vector& position, glm::quat orientation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f)) + : base_t(), m_gimbal({ .position = position, .orientation = orientation }) {} ~CFPSCamera() = default; - const typename traits_t::gimbal_t& getGimbal() override + const typename base_t::CGimbal& getGimbal() override { return m_gimbal; } @@ -58,22 +54,7 @@ class CFPSCamera final : public ICamera } private: - void initKeysToEvent() override - { - traits_t::controller_t::updateKeysToEvent([](traits_t::controller_t::keys_to_virtual_events_t& keys) - { - keys[ui::E_KEY_CODE::EKC_W] = CVirtualGimbalEvent::MoveForward; - keys[ui::E_KEY_CODE::EKC_S] = CVirtualGimbalEvent::MoveBackward; - keys[ui::E_KEY_CODE::EKC_A] = CVirtualGimbalEvent::MoveLeft; - keys[ui::E_KEY_CODE::EKC_D] = CVirtualGimbalEvent::MoveRight; - keys[ui::E_KEY_CODE::EKC_I] = CVirtualGimbalEvent::TiltDown; - keys[ui::E_KEY_CODE::EKC_K] = CVirtualGimbalEvent::TiltUp; - keys[ui::E_KEY_CODE::EKC_J] = CVirtualGimbalEvent::PanLeft; - keys[ui::E_KEY_CODE::EKC_L] = CVirtualGimbalEvent::PanRight; - }); - } - - traits_t::gimbal_t m_gimbal; + typename base_t::CGimbal m_gimbal; static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::MoveForward | CVirtualGimbalEvent::MoveBackward | CVirtualGimbalEvent::MoveRight | CVirtualGimbalEvent::MoveLeft | CVirtualGimbalEvent::TiltUp | CVirtualGimbalEvent::TiltDown | CVirtualGimbalEvent::PanRight | CVirtualGimbalEvent::PanLeft; }; diff --git a/common/include/ICamera.hpp b/common/include/ICamera.hpp index e681ff26b..1e6f8cc3b 100644 --- a/common/include/ICamera.hpp +++ b/common/include/ICamera.hpp @@ -5,29 +5,81 @@ #ifndef _I_CAMERA_HPP_ #define _I_CAMERA_HPP_ -#include "camera/ICameraControl.hpp" +#include "camera/IGimbal.hpp" namespace nbl::hlsl // TODO: DIFFERENT NAMESPACE { template -class ICamera : public ICameraController +class ICamera : virtual public core::IReferenceCounted { public: - using base_t = typename ICameraController; + using precision_t = T; - struct Traits - { - using controller_t = base_t; - using gimbal_t = typename controller_t::CGimbal; - using precision_t = typename T; // TODO: actually all vectors/scalars should have precision type T and because of projection matrix constraints allowed is only float32_t & float64_t - }; + // Gimbal with view parameters representing a camera in world space + class CGimbal : public IGimbal + { + public: + using base_t = IGimbal; - ICamera() : base_t() {} + CGimbal(typename base_t::SCreationParameters&& parameters) : base_t(std::move(parameters)) {} + ~CGimbal() = default; + + struct SView + { + matrix matrix = {}; + bool isLeftHandSystem = true; + }; + + inline void updateView() + { + if (base_t::getManipulationCounter()) + { + const auto& gRight = base_t::getXAxis(), gUp = base_t::getYAxis(), gForward = base_t::getZAxis(); + + // TODO: I think this should be set as request state, depending on the value we do m_view.matrix[2u] flip accordingly + // m_view.isLeftHandSystem; + + auto isNormalized = [](const auto& v, float epsilon) -> bool + { + return glm::epsilonEqual(glm::length(v), 1.0f, epsilon); + }; + + auto isOrthogonal = [](const auto& a, const auto& b, float epsilon) -> bool + { + return glm::epsilonEqual(glm::dot(a, b), 0.0f, epsilon); + }; + + auto isOrthoBase = [&](const auto& x, const auto& y, const auto& z, float epsilon = 1e-6f) -> bool + { + return isNormalized(x, epsilon) && isNormalized(y, epsilon) && isNormalized(z, epsilon) && + isOrthogonal(x, y, epsilon) && isOrthogonal(x, z, epsilon) && isOrthogonal(y, z, epsilon); + }; + + assert(isOrthoBase(gRight, gUp, gForward)); + + const auto& position = base_t::getPosition(); + m_view.matrix[0u] = vector(gRight, -glm::dot(gRight, position)); + m_view.matrix[1u] = vector(gUp, -glm::dot(gUp, position)); + m_view.matrix[2u] = vector(gForward, -glm::dot(gForward, position)); + } + } + + // Getter for gimbal's view + inline const SView& getView() const { return m_view; } + + private: + SView m_view; + }; + + ICamera() {} ~ICamera() = default; // Returns a gimbal which *models the camera view*, note that a camera type implementation may have multiple gimbals under the hood - virtual const Traits::gimbal_t& getGimbal() = 0u; + virtual const CGimbal& getGimbal() = 0u; + + // Manipulates camera with view gimbal & virtual events + virtual void manipulate(std::span virtualEvents) = 0; // VirtualEventType bitmask for a camera view gimbal manipulation requests filtering virtual const uint32_t getAllowedVirtualEvents() = 0u; diff --git a/common/include/camera/CCameraController.hpp b/common/include/camera/CCameraController.hpp new file mode 100644 index 000000000..0dad4e2f4 --- /dev/null +++ b/common/include/camera/CCameraController.hpp @@ -0,0 +1,260 @@ +#ifndef _NBL_C_CAMERA_CONTROLLER_HPP_ +#define _NBL_C_CAMERA_CONTROLLER_HPP_ + +#include "ICameraController.hpp" +#include "ICamera.hpp" + +// TODO: DIFFERENT NAMESPACE +namespace nbl::hlsl +{ + +template +class CCameraController : public ICameraController +{ +public: + using ICameraController::ICameraController; + using interface_camera_t = ICamera; + + using keyboard_to_virtual_events_t = std::unordered_map; + using mouse_to_virtual_events_t = std::unordered_map; + + CCameraController(core::smart_refctd_ptr camera) : m_camera(core::smart_refctd_ptr(camera)) {} + + // Binds mouse key codes to virtual events, the mapKeys lambda will be executed with controller mouse_to_virtual_events_t table + void updateKeyboardMapping(const std::function& mapKeys) + { + mapKeys(m_keyboardVirtualEventMap); + } + + // Binds mouse key codes to virtual events, the mapKeys lambda will be executed with controller mouse_to_virtual_events_t table + void updateMouseMapping(const std::function& mapKeys) + { + mapKeys(m_mouseVirtualEventMap); + } + + struct SUpdateParameters + { + std::span keyboardEvents = {}; + std::span mouseEvents = {}; + }; + + // Processes SUpdateParameters events to generate virtual manipulation events + void process(CVirtualGimbalEvent* output, uint32_t& count, const SUpdateParameters parameters = {}) + { + count = 0u; + uint32_t vKeyboardEventsCount, vMouseEventsCount; + + if (output) + { + processKeyboard(output, vKeyboardEventsCount, parameters.keyboardEvents); + processMouse(output + vKeyboardEventsCount, vMouseEventsCount, parameters.mouseEvents); + } + else + { + processKeyboard(nullptr, vKeyboardEventsCount, {}); + processMouse(nullptr, vMouseEventsCount, {}); + } + + count = vKeyboardEventsCount + vMouseEventsCount; + } + + inline const keyboard_to_virtual_events_t& getKeyboardVirtualEventMap() { return m_keyboardVirtualEventMap; } + inline const mouse_to_virtual_events_t& getMouseVirtualEventMap() { return m_mouseVirtualEventMap; } + inline interface_camera_t* getCamera() { return m_camera.get(); } + +private: + // Processes keyboard events to generate virtual manipulation events + void processKeyboard(CVirtualGimbalEvent* output, uint32_t& count, std::span events) + { + count = 0u; + const auto mappedVirtualEventsCount = m_keyboardVirtualEventMap.size(); + + if (!output) + { + count = mappedVirtualEventsCount; + return; + } + + if (mappedVirtualEventsCount) + { + const auto frameDeltaTime = std::chrono::duration_cast(nextPresentationTimeStamp - lastVirtualUpTimeStamp).count(); + assert(frameDeltaTime >= 0.f); + + for (auto& [key, hash] : m_keyboardVirtualEventMap) + { + hash.event.magnitude = 0.f; + + /* + if a key was already being held down from previous frames we compute with this + assumption that the key will be held down for this whole frame as well and its + delta action is simply frame delta time + */ + + if (hash.active) + hash.event.magnitude = frameDeltaTime; + } + + for (const auto& keyboardEvent : events) + { + auto request = m_keyboardVirtualEventMap.find(keyboardEvent.keyCode); + if (request != std::end(m_keyboardVirtualEventMap)) + { + auto& hash = request->second; + + if (keyboardEvent.action == nbl::ui::SKeyboardEvent::ECA_PRESSED) + { + if (!hash.active) + { + // and if a key has been pressed after its last release event then first delta action is the key delta + const auto keyDeltaTime = std::chrono::duration_cast(nextPresentationTimeStamp - keyboardEvent.timeStamp).count(); + assert(keyDeltaTime >= 0); + + hash.active = true; + hash.event.magnitude = keyDeltaTime; + } + } + else if (keyboardEvent.action == nbl::ui::SKeyboardEvent::ECA_RELEASED) + hash.active = false; + } + } + + for (const auto& [key, hash] : m_keyboardVirtualEventMap) + { + if (hash.event.magnitude) + { + auto* virtualEvent = output + count; + virtualEvent->type = hash.event.type; + virtualEvent->magnitude = hash.event.magnitude; + ++count; + } + } + } + } + + // Processes mouse events to generate virtual manipulation events + void processMouse(CVirtualGimbalEvent* output, uint32_t& count, std::span events) + { + count = 0u; + const auto mappedVirtualEventsCount = m_mouseVirtualEventMap.size(); + + if (!output) + { + count = mappedVirtualEventsCount; + return; + } + + if (mappedVirtualEventsCount) + { + const auto frameDeltaTime = std::chrono::duration_cast(nextPresentationTimeStamp - lastVirtualUpTimeStamp).count(); + assert(frameDeltaTime >= 0.f); + + for (auto& [key, hash] : m_mouseVirtualEventMap) + { + hash.event.magnitude = 0.f; + if (hash.active) + hash.event.magnitude = frameDeltaTime; + } + + for (const auto& mouseEvent : events) + { + ui::E_MOUSE_CODE mouseCode = ui::EMC_NONE; + + switch (mouseEvent.type) + { + case ui::SMouseEvent::EET_CLICK: + { + switch (mouseEvent.clickEvent.mouseButton) + { + case ui::EMB_LEFT_BUTTON: mouseCode = ui::EMC_LEFT_BUTTON; break; + case ui::EMB_RIGHT_BUTTON: mouseCode = ui::EMC_RIGHT_BUTTON; break; + case ui::EMB_MIDDLE_BUTTON: mouseCode = ui::EMC_MIDDLE_BUTTON; break; + case ui::EMB_BUTTON_4: mouseCode = ui::EMC_BUTTON_4; break; + case ui::EMB_BUTTON_5: mouseCode = ui::EMC_BUTTON_5; break; + default: continue; + } + + auto request = m_mouseVirtualEventMap.find(mouseCode); + if (request != std::end(m_mouseVirtualEventMap)) + { + auto& hash = request->second; + + if (mouseEvent.clickEvent.action == ui::SMouseEvent::SClickEvent::EA_PRESSED) + { + if (!hash.active) + { + const auto keyDeltaTime = std::chrono::duration_cast(nextPresentationTimeStamp - mouseEvent.timeStamp).count(); + assert(keyDeltaTime >= 0); + + hash.active = true; + hash.event.magnitude += keyDeltaTime; + } + } + else if (mouseEvent.clickEvent.action == ui::SMouseEvent::SClickEvent::EA_RELEASED) + hash.active = false; + } + } break; + + case ui::SMouseEvent::EET_SCROLL: + { + if (mouseEvent.scrollEvent.verticalScroll != 0) + { + mouseCode = (mouseEvent.scrollEvent.verticalScroll > 0) ? ui::EMC_VERTICAL_POSITIVE_SCROLL : ui::EMC_VERTICAL_NEGATIVE_SCROLL; + auto request = m_mouseVirtualEventMap.find(mouseCode); + if (request != std::end(m_mouseVirtualEventMap)) + request->second.event.magnitude += std::abs(mouseEvent.scrollEvent.verticalScroll); + } + + if (mouseEvent.scrollEvent.horizontalScroll != 0) + { + mouseCode = (mouseEvent.scrollEvent.horizontalScroll > 0) ? ui::EMC_HORIZONTAL_POSITIVE_SCROLL : ui::EMC_HORIZONTAL_NEGATIVE_SCROLL; + auto request = m_mouseVirtualEventMap.find(mouseCode); + if (request != std::end(m_mouseVirtualEventMap)) + request->second.event.magnitude += std::abs(mouseEvent.scrollEvent.horizontalScroll); + } + } break; + + case ui::SMouseEvent::EET_MOVEMENT: + { + if (mouseEvent.movementEvent.relativeMovementX != 0) + { + mouseCode = (mouseEvent.movementEvent.relativeMovementX > 0) ? ui::EMC_RELATIVE_POSITIVE_MOVEMENT_X : ui::EMC_RELATIVE_NEGATIVE_MOVEMENT_X; + auto request = m_mouseVirtualEventMap.find(mouseCode); + if (request != std::end(m_mouseVirtualEventMap)) + request->second.event.magnitude += std::abs(mouseEvent.movementEvent.relativeMovementX); + } + + if (mouseEvent.movementEvent.relativeMovementY != 0) + { + mouseCode = (mouseEvent.movementEvent.relativeMovementY > 0) ? ui::EMC_RELATIVE_POSITIVE_MOVEMENT_Y : ui::EMC_RELATIVE_NEGATIVE_MOVEMENT_Y; + auto request = m_mouseVirtualEventMap.find(mouseCode); + if (request != std::end(m_mouseVirtualEventMap)) + request->second.event.magnitude += std::abs(mouseEvent.movementEvent.relativeMovementY); + } + } break; + + default: + break; + } + } + + for (const auto& [key, hash] : m_mouseVirtualEventMap) + { + if (hash.event.magnitude) + { + auto* virtualEvent = output + count; + virtualEvent->type = hash.event.type; + virtualEvent->magnitude = hash.event.magnitude; + ++count; + } + } + } + } + + core::smart_refctd_ptr m_camera; + keyboard_to_virtual_events_t m_keyboardVirtualEventMap; + mouse_to_virtual_events_t m_mouseVirtualEventMap; +}; + +} // nbl::hlsl namespace + +#endif // _NBL_C_CAMERA_CONTROLLER_HPP_ \ No newline at end of file diff --git a/common/include/camera/ICameraControl.hpp b/common/include/camera/ICameraControl.hpp index f017eff14..56c3b289d 100644 --- a/common/include/camera/ICameraControl.hpp +++ b/common/include/camera/ICameraControl.hpp @@ -14,81 +14,44 @@ namespace nbl::hlsl { -template class ICameraController : virtual public core::IReferenceCounted { public: - using precision_t = typename T; - - struct CRequestInfo + enum ControllerType : uint8_t { - CRequestInfo() {} - CRequestInfo(CVirtualGimbalEvent::VirtualEventType _type) : type(_type) {} - ~CRequestInfo() = default; + Keyboard = 0, + Mouse = 1, - CVirtualGimbalEvent::VirtualEventType type = CVirtualGimbalEvent::VirtualEventType::None; - bool active = false; - float64_t dtAction = {}; + Count }; - using keys_to_virtual_events_t = std::unordered_map; - - // Gimbal with view parameters representing a camera in world space - class CGimbal : public IGimbal + struct CKeyInfo { - public: - using base_t = IGimbal; - - CGimbal(typename base_t::SCreationParameters&& parameters) : base_t(std::move(parameters)) {} - ~CGimbal() = default; - - struct SView + union { - matrix matrix = {}; - bool isLeftHandSystem = true; + ui::E_KEY_CODE keyboardCode; + ui::E_MOUSE_CODE mouseCode; }; - inline void updateView() - { - if (base_t::getManipulationCounter()) - { - const auto& gRight = base_t::getXAxis(), gUp = base_t::getYAxis(), gForward = base_t::getZAxis(); - - // TODO: I think this should be set as request state, depending on the value we do m_view.matrix[2u] flip accordingly - // m_view.isLeftHandSystem; - - auto isNormalized = [](const auto& v, float epsilon) -> bool - { - return glm::epsilonEqual(glm::length(v), 1.0f, epsilon); - }; - - auto isOrthogonal = [](const auto& a, const auto& b, float epsilon) -> bool - { - return glm::epsilonEqual(glm::dot(a, b), 0.0f, epsilon); - }; - - auto isOrthoBase = [&](const auto& x, const auto& y, const auto& z, float epsilon = 1e-6f) -> bool - { - return isNormalized(x, epsilon) && isNormalized(y, epsilon) && isNormalized(z, epsilon) && - isOrthogonal(x, y, epsilon) && isOrthogonal(x, z, epsilon) && isOrthogonal(y, z, epsilon); - }; - - assert(isOrthoBase(gRight, gUp, gForward)); + CKeyInfo(ControllerType _type, ui::E_KEY_CODE _keyboardCode) : keyboardCode(_keyboardCode), type(_type) {} + CKeyInfo(ControllerType _type, ui::E_MOUSE_CODE _mouseCode) : mouseCode(_mouseCode), type(_type) {} - const auto& position = base_t::getPosition(); - m_view.matrix[0u] = vector(gRight, -glm::dot(gRight, position)); - m_view.matrix[1u] = vector(gUp, -glm::dot(gUp, position)); - m_view.matrix[2u] = vector(gForward, -glm::dot(gForward, position)); - } - } + ControllerType type; + }; - // Getter for gimbal's view - inline const SView& getView() const { return m_view; } + struct CHashInfo + { + CHashInfo() {} + CHashInfo(CVirtualGimbalEvent::VirtualEventType _type) : type(_type) {} + ~CHashInfo() = default; - private: - SView m_view; + CVirtualGimbalEvent::VirtualEventType type = CVirtualGimbalEvent::VirtualEventType::None; + bool active = false; + float64_t dtAction = {}; }; + using keys_to_virtual_events_t = std::unordered_map; + ICameraController() {} // Binds key codes to virtual events, the mapKeys lambda will be executed with controller keys_to_virtual_events_t table @@ -97,140 +60,21 @@ class ICameraController : virtual public core::IReferenceCounted mapKeys(m_keysToVirtualEvents); } - // Manipulates camera with view gimbal & virtual events - virtual void manipulate(std::span virtualEvents) = 0; + inline const keys_to_virtual_events_t& getKeysToVirtualEvents() { return m_keysToVirtualEvents; } - // TODO: *maybe* would be good to have a class interface for virtual event generators, - // eg keyboard, mouse but maybe custom stuff too eg events from gimbal drag & drop void beginInputProcessing(const std::chrono::microseconds _nextPresentationTimeStamp) { nextPresentationTimeStamp = _nextPresentationTimeStamp; } - // Processes keyboard events to generate virtual manipulation events, note that it doesn't make the manipulation itself! - void processKeyboard(CVirtualGimbalEvent* output, uint32_t& count, std::span events) - { - count = 0u; - const auto mappedVirtualEventsCount = m_keysToVirtualEvents.size(); - - if (!output) - { - count = mappedVirtualEventsCount; - return; - } - - if (mappedVirtualEventsCount) - { - const auto frameDeltaTime = std::chrono::duration_cast(nextPresentationTimeStamp - lastVirtualUpTimeStamp).count(); - assert(frameDeltaTime >= 0.f); - - for (auto& [key, info] : m_keysToVirtualEvents) - { - info.dtAction = 0.f; - - /* - if a key was already being held down from previous frames we compute with this - assumption that the key will be held down for this whole frame as well and its - delta action is simply frame delta time - */ - - if (info.active) - info.dtAction = frameDeltaTime; - } - - for (const auto& keyboardEvent : events) - { - auto request = m_keysToVirtualEvents.find(keyboardEvent.keyCode); - if (request != std::end(m_keysToVirtualEvents)) - { - auto& info = request->second; - - if (keyboardEvent.action == nbl::ui::SKeyboardEvent::ECA_PRESSED) - { - if (!info.active) - { - // and if a key has been pressed after its last release event then first delta action is the key delta - const auto keyDeltaTime = std::chrono::duration_cast(nextPresentationTimeStamp - keyboardEvent.timeStamp).count(); - assert(keyDeltaTime >= 0); - - info.active = true; - info.dtAction = keyDeltaTime; - } - } - else if (keyboardEvent.action == nbl::ui::SKeyboardEvent::ECA_RELEASED) - info.active = false; - } - } - - for (const auto& [key, info] : m_keysToVirtualEvents) - { - if (info.active) - { - auto* virtualEvent = output + count; - virtualEvent->type = info.type; - virtualEvent->magnitude = info.dtAction; - ++count; - } - } - } - } - - // Processes mouse events to generate virtual manipulation events, note that it doesn't make the manipulation itself! - // Limited to Pan & Tilt rotation events, camera type implements how event magnitudes should be interpreted - void processMouse(CVirtualGimbalEvent* output, uint32_t& count, std::span events) - { - count = 0u; - - if (!output) - { - count = 2u; - return; - } - - if (events.empty()) - return; - - double dYaw = {}, dPitch = {}; - - for (const auto& ev : events) - if (ev.type == nbl::ui::SMouseEvent::EET_MOVEMENT) - { - dYaw += ev.movementEvent.relativeMovementX; - dPitch += ev.movementEvent.relativeMovementY; - } - - if (dPitch) - { - auto* pitch = output + count; - pitch->type = dPitch > 0.f ? CVirtualGimbalEvent::TiltUp : CVirtualGimbalEvent::TiltDown; - pitch->magnitude = std::abs(dPitch); - count++; - } - - if (dYaw) - { - auto* yaw = output + count; - assert(yaw); // TODO: maybe just log error and return 0 count - yaw->type = dYaw > 0.f ? CVirtualGimbalEvent::PanRight : CVirtualGimbalEvent::PanLeft; - yaw->magnitude = std::abs(dYaw); - count++; - } - } - void endInputProcessing() { lastVirtualUpTimeStamp = nextPresentationTimeStamp; } - inline const keys_to_virtual_events_t& getKeysToVirtualEvents() { return m_keysToVirtualEvents; } - -protected: - virtual void initKeysToEvent() = 0; - private: keys_to_virtual_events_t m_keysToVirtualEvents; - bool m_keysDown[CVirtualGimbalEvent::EventsCount] = {}; std::chrono::microseconds nextPresentationTimeStamp = {}, lastVirtualUpTimeStamp = {}; }; diff --git a/common/include/camera/ICameraController.hpp b/common/include/camera/ICameraController.hpp new file mode 100644 index 000000000..6df41d709 --- /dev/null +++ b/common/include/camera/ICameraController.hpp @@ -0,0 +1,68 @@ +#ifndef _NBL_I_CAMERA_CONTROLLER_HPP_ +#define _NBL_I_CAMERA_CONTROLLER_HPP_ + +#include +#include +#include +#include +#include + +#include "IProjection.hpp" +#include "CGeneralPurposeGimbal.hpp" + +// TODO: DIFFERENT NAMESPACE +namespace nbl::hlsl +{ + +class ICameraController : virtual public core::IReferenceCounted +{ +public: + enum ControllerType : uint8_t + { + Keyboard, + Mouse, + + Count + }; + + struct CKeyInfo + { + union + { + ui::E_KEY_CODE keyboardCode; + ui::E_MOUSE_CODE mouseCode; + }; + + CKeyInfo(ui::E_KEY_CODE _keyboardCode) : keyboardCode(_keyboardCode), type(Keyboard) {} + CKeyInfo(ui::E_MOUSE_CODE _mouseCode) : mouseCode(_mouseCode), type(Mouse) {} + + ControllerType type; + }; + + struct CHashInfo + { + CHashInfo() {} + CHashInfo(CVirtualGimbalEvent::VirtualEventType _type) : event({ .type = _type }) {} + ~CHashInfo() = default; + + CVirtualGimbalEvent event = {}; + bool active = false; + }; + + void beginInputProcessing(const std::chrono::microseconds _nextPresentationTimeStamp) + { + nextPresentationTimeStamp = _nextPresentationTimeStamp; + } + + void endInputProcessing() + { + lastVirtualUpTimeStamp = nextPresentationTimeStamp; + } + +protected: + std::chrono::microseconds nextPresentationTimeStamp = {}, lastVirtualUpTimeStamp = {}; +}; + +} // nbl::hlsl namespace + +#endif // _NBL_I_CAMERA_CONTROLLER_HPP_ \ No newline at end of file diff --git a/common/include/camera/IGimbal.hpp b/common/include/camera/IGimbal.hpp index fed8f9f7a..fcc316ddd 100644 --- a/common/include/camera/IGimbal.hpp +++ b/common/include/camera/IGimbal.hpp @@ -46,8 +46,8 @@ namespace nbl::hlsl using manipulation_encode_t = float64_t; - VirtualEventType type; - manipulation_encode_t magnitude; + VirtualEventType type = None; + manipulation_encode_t magnitude = {}; static constexpr std::string_view virtualEventToString(VirtualEventType event) { From 360d9aa11931b01d69129872bb930523a0ecde5d Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Mon, 11 Nov 2024 18:20:22 +0100 Subject: [PATCH 031/205] rename + refactor ICameraController to more abstract IGimbalController, unify controller API, add ImGuizmo controller (untested yet), add controller key map preset to ICamera interface for view gimbal, create CCameraController which will be capable of both manipulating camera view & target gimbals with *any controller type*, write down my thoughts & TODOs in comments, polish code, update sources, adjust to changes, eliminate caught bugs --- 61_UI/include/keysmapping.hpp | 12 +- 61_UI/main.cpp | 39 +- common/include/CFPSCamera.hpp | 100 ++++- common/include/ICamera.hpp | 25 +- common/include/camera/CCameraController.hpp | 290 +++----------- .../include/camera/CGeneralPurposeGimbal.hpp | 3 +- common/include/camera/ICameraControl.hpp | 110 ------ common/include/camera/ICameraController.hpp | 68 ---- common/include/camera/IGimbal.hpp | 55 ++- common/include/camera/IGimbalController.hpp | 374 ++++++++++++++++++ 10 files changed, 607 insertions(+), 469 deletions(-) delete mode 100644 common/include/camera/ICameraControl.hpp delete mode 100644 common/include/camera/ICameraController.hpp create mode 100644 common/include/camera/IGimbalController.hpp diff --git a/61_UI/include/keysmapping.hpp b/61_UI/include/keysmapping.hpp index d30fa6b9c..25bba3e68 100644 --- a/61_UI/include/keysmapping.hpp +++ b/61_UI/include/keysmapping.hpp @@ -5,7 +5,7 @@ #include "camera/CCameraController.hpp" template -void handleAddMapping(const char* tableID, CCameraController* controller, ICameraController::ControllerType activeController, CVirtualGimbalEvent::VirtualEventType& selectedEventType, ui::E_KEY_CODE& newKey, ui::E_MOUSE_CODE& newMouseCode, bool& addMode) +void handleAddMapping(const char* tableID, CCameraController* controller, IGimbalManipulateEncoder::EncoderType activeController, CVirtualGimbalEvent::VirtualEventType& selectedEventType, ui::E_KEY_CODE& newKey, ui::E_MOUSE_CODE& newMouseCode, bool& addMode) { ImGui::BeginTable(tableID, 3, ImGuiTableFlags_Borders | ImGuiTableFlags_Resizable | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchSame); ImGui::TableSetupColumn("Virtual Event", ImGuiTableColumnFlags_WidthStretch, 0.33f); @@ -30,7 +30,7 @@ void handleAddMapping(const char* tableID, CCameraController* controller, ICa } ImGui::TableSetColumnIndex(1); - if (activeController == ICameraController::Keyboard) + if (activeController == IGimbalManipulateEncoder::Keyboard) { char newKeyDisplay[2] = { ui::keyCodeToChar(newKey, true), '\0' }; if (ImGui::BeginCombo("##selectKey", newKeyDisplay)) @@ -66,7 +66,7 @@ void handleAddMapping(const char* tableID, CCameraController* controller, ICa ImGui::TableSetColumnIndex(2); if (ImGui::Button("Confirm Add", ImVec2(100, 30))) { - if (activeController == ICameraController::Keyboard) + if (activeController == IGimbalManipulateEncoder::Keyboard) controller->updateKeyboardMapping([&](auto& keys) { keys[newKey] = selectedEventType; }); else controller->updateMouseMapping([&](auto& mouse) { mouse[newMouseCode] = selectedEventType; }); @@ -83,7 +83,7 @@ void displayKeyMappingsAndVirtualStates(CCameraController* controller) static CVirtualGimbalEvent::VirtualEventType selectedEventType = CVirtualGimbalEvent::VirtualEventType::MoveForward; static ui::E_KEY_CODE newKey = ui::E_KEY_CODE::EKC_A; static ui::E_MOUSE_CODE newMouseCode = ui::EMC_LEFT_BUTTON; - static ICameraController::ControllerType activeController = ICameraController::Keyboard; + static IGimbalManipulateEncoder::EncoderType activeController = IGimbalManipulateEncoder::Keyboard; const auto& keyboardMappings = controller->getKeyboardVirtualEventMap(); const auto& mouseMappings = controller->getMouseVirtualEventMap(); @@ -97,7 +97,7 @@ void displayKeyMappingsAndVirtualStates(CCameraController* controller) { if (ImGui::BeginTabItem("Keyboard")) { - activeController = ICameraController::Keyboard; + activeController = IGimbalManipulateEncoder::Keyboard; ImGui::Separator(); if (ImGui::Button("Add key", ImVec2(100, 30))) @@ -159,7 +159,7 @@ void displayKeyMappingsAndVirtualStates(CCameraController* controller) if (ImGui::BeginTabItem("Mouse")) { - activeController = ICameraController::Mouse; + activeController = IGimbalManipulateEncoder::Mouse; ImGui::Separator(); if (ImGui::Button("Add key", ImVec2(100, 30))) diff --git a/61_UI/main.cpp b/61_UI/main.cpp index b57c0b5d9..3851d7177 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -515,25 +515,21 @@ class UISampleApp final : public examples::SimpleWindowedApplication controller = make_smart_refctd_ptr(core::smart_refctd_ptr(camera)); // init keyboard map - controller->updateKeyboardMapping([](auto& keys) + controller->updateKeyboardMapping([&](auto& keys) { - keys[ui::E_KEY_CODE::EKC_W] = CVirtualGimbalEvent::MoveForward; - keys[ui::E_KEY_CODE::EKC_S] = CVirtualGimbalEvent::MoveBackward; - keys[ui::E_KEY_CODE::EKC_A] = CVirtualGimbalEvent::MoveLeft; - keys[ui::E_KEY_CODE::EKC_D] = CVirtualGimbalEvent::MoveRight; - keys[ui::E_KEY_CODE::EKC_I] = CVirtualGimbalEvent::TiltDown; - keys[ui::E_KEY_CODE::EKC_K] = CVirtualGimbalEvent::TiltUp; - keys[ui::E_KEY_CODE::EKC_J] = CVirtualGimbalEvent::PanLeft; - keys[ui::E_KEY_CODE::EKC_L] = CVirtualGimbalEvent::PanRight; + keys = controller->getCamera()->getKeyboardMappingPreset(); }); // init mouse map - controller->updateMouseMapping([](auto& keys) + controller->updateMouseMapping([&](auto& keys) { - keys[ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_X] = CVirtualGimbalEvent::PanRight; - keys[ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_X] = CVirtualGimbalEvent::PanLeft; - keys[ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_Y] = CVirtualGimbalEvent::TiltUp; - keys[ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_Y] = CVirtualGimbalEvent::TiltDown; + keys = controller->getCamera()->getMouseMappingPreset(); + }); + + // init imguizmo map + controller->updateImguizmoMapping([&](auto& keys) + { + keys = controller->getCamera()->getImguizmoMappingPreset(); }); return true; @@ -766,19 +762,8 @@ class UISampleApp final : public examples::SimpleWindowedApplication if (move) { - static std::vector virtualEvents(0x45); - uint32_t vCount; - - controller->beginInputProcessing(nextPresentationTimestamp); - controller->process(nullptr, vCount); - - if (virtualEvents.size() < vCount) - virtualEvents.resize(vCount); - - controller->process(virtualEvents.data(), vCount, { params.keyboardEvents, params.mouseEvents }); - controller->endInputProcessing(); - - controller->getCamera()->manipulate({ virtualEvents.data(), vCount }); + // TODO: testing + controller->manipulateViewGimbal({ params.keyboardEvents, params.mouseEvents }, nextPresentationTimestamp); } pass.ui.manager->update(params); diff --git a/common/include/CFPSCamera.hpp b/common/include/CFPSCamera.hpp index 3f95de0dc..e0f9dec53 100644 --- a/common/include/CFPSCamera.hpp +++ b/common/include/CFPSCamera.hpp @@ -21,41 +21,119 @@ class CFPSCamera final : public ICamera : base_t(), m_gimbal({ .position = position, .orientation = orientation }) {} ~CFPSCamera() = default; + const base_t::keyboard_to_virtual_events_t& getKeyboardMappingPreset() const override { return m_keyboard_to_virtual_events_preset; } + const base_t::mouse_to_virtual_events_t& getMouseMappingPreset() const override { return m_mouse_to_virtual_events_preset; } + const base_t::imguizmo_to_virtual_events_t& getImguizmoMappingPreset() const override { return m_imguizmo_to_virtual_events_preset; } + const typename base_t::CGimbal& getGimbal() override { return m_gimbal; } - virtual void manipulate(std::span virtualEvents) override + virtual bool manipulate(std::span virtualEvents, base_t::ManipulationMode mode) override { constexpr float MoveSpeedScale = 0.01f, RotateSpeedScale = 0.003f, MaxVerticalAngle = glm::radians(88.0f), MinVerticalAngle = -MaxVerticalAngle; if (!virtualEvents.size()) - return; - - const auto impulse = m_gimbal.accumulate(virtualEvents); + return false; const auto& gForward = m_gimbal.getZAxis(); - const float currentPitch = atan2(glm::length(glm::vec2(gForward.x, gForward.z)), gForward.y) - glm::half_pi(), currentYaw = atan2(gForward.x, gForward.z); - const auto newPitch = std::clamp(currentPitch + impulse.dVirtualRotation.x * RotateSpeedScale, MinVerticalAngle, MaxVerticalAngle), newYaw = currentYaw + impulse.dVirtualRotation.y * RotateSpeedScale; + const float gPitch = atan2(glm::length(glm::vec2(gForward.x, gForward.z)), gForward.y) - glm::half_pi(), gYaw = atan2(gForward.x, gForward.z); + + glm::quat newOrientation; vector newPosition; + + switch (mode) + { + case base_t::Local: + { + const auto impulse = m_gimbal.accumulate(virtualEvents); + const auto newPitch = std::clamp(gPitch + impulse.dVirtualRotation.x * RotateSpeedScale, MinVerticalAngle, MaxVerticalAngle), newYaw = gYaw + impulse.dVirtualRotation.y * RotateSpeedScale; + newOrientation = glm::quat(glm::vec3(newPitch, newYaw, 0.0f)); newPosition = m_gimbal.getPosition() + impulse.dVirtualTranslate * MoveSpeedScale; + } break; + + case base_t::World: + { + const auto transform = m_gimbal.accumulate(virtualEvents); + const auto newPitch = std::clamp(transform.dVirtualRotation.x, MinVerticalAngle, MaxVerticalAngle), newYaw = transform.dVirtualRotation.y; + newOrientation = glm::quat(glm::vec3(newPitch, newYaw, 0.0f)); newPosition = transform.dVirtualTranslate; + } break; + + default: return false; + } + + bool manipulated = true; m_gimbal.begin(); { - m_gimbal.setOrientation(glm::quat(glm::vec3(newPitch, newYaw, 0.0f))); - m_gimbal.move(impulse.dVirtualTranslate * MoveSpeedScale); + m_gimbal.setOrientation(newOrientation); + m_gimbal.setPosition(newPosition); m_gimbal.updateView(); + manipulated &= bool(m_gimbal.getManipulationCounter()); } m_gimbal.end(); + + return manipulated; } - virtual const uint32_t getAllowedVirtualEvents() override + virtual const uint32_t getAllowedVirtualEvents(base_t::ManipulationMode mode) override { - return AllowedVirtualEvents; + switch (mode) + { + case base_t::Local: return AllowedLocalVirtualEvents; + case base_t::World: return AllowedWorldVirtualEvents; + default: return CVirtualGimbalEvent::None; + } } private: typename base_t::CGimbal m_gimbal; - static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::MoveForward | CVirtualGimbalEvent::MoveBackward | CVirtualGimbalEvent::MoveRight | CVirtualGimbalEvent::MoveLeft | CVirtualGimbalEvent::TiltUp | CVirtualGimbalEvent::TiltDown | CVirtualGimbalEvent::PanRight | CVirtualGimbalEvent::PanLeft; + + static inline constexpr auto AllowedLocalVirtualEvents = CVirtualGimbalEvent::MoveForward | CVirtualGimbalEvent::MoveBackward | CVirtualGimbalEvent::MoveRight | CVirtualGimbalEvent::MoveLeft | CVirtualGimbalEvent::TiltUp | CVirtualGimbalEvent::TiltDown | CVirtualGimbalEvent::PanRight | CVirtualGimbalEvent::PanLeft; + static inline constexpr auto AllowedWorldVirtualEvents = AllowedLocalVirtualEvents | CVirtualGimbalEvent::Translate; + + static inline const auto m_keyboard_to_virtual_events_preset = []() + { + typename base_t::keyboard_to_virtual_events_t preset; + + preset[ui::E_KEY_CODE::EKC_W] = CVirtualGimbalEvent::MoveForward; + preset[ui::E_KEY_CODE::EKC_S] = CVirtualGimbalEvent::MoveBackward; + preset[ui::E_KEY_CODE::EKC_A] = CVirtualGimbalEvent::MoveLeft; + preset[ui::E_KEY_CODE::EKC_D] = CVirtualGimbalEvent::MoveRight; + preset[ui::E_KEY_CODE::EKC_I] = CVirtualGimbalEvent::TiltDown; + preset[ui::E_KEY_CODE::EKC_K] = CVirtualGimbalEvent::TiltUp; + preset[ui::E_KEY_CODE::EKC_J] = CVirtualGimbalEvent::PanLeft; + preset[ui::E_KEY_CODE::EKC_L] = CVirtualGimbalEvent::PanRight; + + return preset; + }(); + + static inline const auto m_mouse_to_virtual_events_preset = []() + { + typename base_t::mouse_to_virtual_events_t preset; + + preset[ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_X] = CVirtualGimbalEvent::PanRight; + preset[ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_X] = CVirtualGimbalEvent::PanLeft; + preset[ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_Y] = CVirtualGimbalEvent::TiltUp; + preset[ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_Y] = CVirtualGimbalEvent::TiltDown; + + return preset; + }(); + + static inline const auto m_imguizmo_to_virtual_events_preset = []() + { + typename base_t::imguizmo_to_virtual_events_t preset; + + preset[CVirtualGimbalEvent::MoveForward] = CVirtualGimbalEvent::MoveForward; + preset[CVirtualGimbalEvent::MoveBackward] = CVirtualGimbalEvent::MoveBackward; + preset[CVirtualGimbalEvent::MoveLeft] = CVirtualGimbalEvent::MoveLeft; + preset[CVirtualGimbalEvent::MoveRight] = CVirtualGimbalEvent::MoveRight; + preset[CVirtualGimbalEvent::TiltDown] = CVirtualGimbalEvent::TiltDown; + preset[CVirtualGimbalEvent::TiltUp] = CVirtualGimbalEvent::TiltUp; + preset[CVirtualGimbalEvent::PanLeft] = CVirtualGimbalEvent::PanLeft; + preset[CVirtualGimbalEvent::PanRight] = CVirtualGimbalEvent::PanRight; + + return preset; + }(); }; } diff --git a/common/include/ICamera.hpp b/common/include/ICamera.hpp index 1e6f8cc3b..fbbb5a863 100644 --- a/common/include/ICamera.hpp +++ b/common/include/ICamera.hpp @@ -5,17 +5,29 @@ #ifndef _I_CAMERA_HPP_ #define _I_CAMERA_HPP_ -#include "camera/IGimbal.hpp" +#include "camera/IGimbalController.hpp" namespace nbl::hlsl // TODO: DIFFERENT NAMESPACE { template -class ICamera : virtual public core::IReferenceCounted +class ICamera : public IGimbalManipulateEncoder, virtual public core::IReferenceCounted { public: + using IGimbalManipulateEncoder::IGimbalManipulateEncoder; using precision_t = T; + //! Manipulation mode for virtual events + //! TODO: this should belong to IObjectTransform or something + enum ManipulationMode + { + // Interpret virtual events as accumulated impulse representing relative manipulation with respect to view gimbal base + Local, + + // Interpret virtual events as accumulated absolute manipulation with respect to world base + World + }; + // Gimbal with view parameters representing a camera in world space class CGimbal : public IGimbal { @@ -75,14 +87,15 @@ class ICamera : virtual public core::IReferenceCounted ICamera() {} ~ICamera() = default; - // Returns a gimbal which *models the camera view*, note that a camera type implementation may have multiple gimbals under the hood + // Returns a gimbal which *models the camera view* virtual const CGimbal& getGimbal() = 0u; - // Manipulates camera with view gimbal & virtual events - virtual void manipulate(std::span virtualEvents) = 0; + // Manipulates camera with virtual events, returns true if *any* manipulation happens, it may fail partially or fully because each camera type has certain constraints which determine how it actually works + // TODO: this really needs to be moved to more abstract interface, eg. IObjectTransform or something and ICamera should inherit it (its also an object!) + virtual bool manipulate(std::span virtualEvents, ManipulationMode mode) = 0; // VirtualEventType bitmask for a camera view gimbal manipulation requests filtering - virtual const uint32_t getAllowedVirtualEvents() = 0u; + virtual const uint32_t getAllowedVirtualEvents(ManipulationMode mode) = 0u; }; } diff --git a/common/include/camera/CCameraController.hpp b/common/include/camera/CCameraController.hpp index 0dad4e2f4..c858c00ee 100644 --- a/common/include/camera/CCameraController.hpp +++ b/common/include/camera/CCameraController.hpp @@ -1,260 +1,90 @@ #ifndef _NBL_C_CAMERA_CONTROLLER_HPP_ #define _NBL_C_CAMERA_CONTROLLER_HPP_ -#include "ICameraController.hpp" +#include "IGimbalController.hpp" +#include "CGeneralPurposeGimbal.hpp" #include "ICamera.hpp" // TODO: DIFFERENT NAMESPACE -namespace nbl::hlsl +namespace nbl::hlsl { - -template -class CCameraController : public ICameraController -{ -public: - using ICameraController::ICameraController; - using interface_camera_t = ICamera; - - using keyboard_to_virtual_events_t = std::unordered_map; - using mouse_to_virtual_events_t = std::unordered_map; - - CCameraController(core::smart_refctd_ptr camera) : m_camera(core::smart_refctd_ptr(camera)) {} - - // Binds mouse key codes to virtual events, the mapKeys lambda will be executed with controller mouse_to_virtual_events_t table - void updateKeyboardMapping(const std::function& mapKeys) - { - mapKeys(m_keyboardVirtualEventMap); - } - - // Binds mouse key codes to virtual events, the mapKeys lambda will be executed with controller mouse_to_virtual_events_t table - void updateMouseMapping(const std::function& mapKeys) + //! Controls any type of camera with available controllers using virtual gimbal events in Local mode + template + class CCameraController final : public IGimbalController { - mapKeys(m_mouseVirtualEventMap); - } - - struct SUpdateParameters - { - std::span keyboardEvents = {}; - std::span mouseEvents = {}; - }; - - // Processes SUpdateParameters events to generate virtual manipulation events - void process(CVirtualGimbalEvent* output, uint32_t& count, const SUpdateParameters parameters = {}) - { - count = 0u; - uint32_t vKeyboardEventsCount, vMouseEventsCount; - - if (output) + public: + using IGimbalController::IGimbalController; + using precision_t = T; + + using interface_camera_t = ICamera; + using interface_gimbal_t = IGimbal; + + CCameraController(core::smart_refctd_ptr camera) + : m_camera(std::move(camera)), m_target(interface_gimbal_t::SCreationParameters({ .position = m_camera->getGimbal().getWorldTarget() })) {} + ~CCameraController() {} + + const keyboard_to_virtual_events_t& getKeyboardMappingPreset() const override { return m_camera->getKeyboardMappingPreset(); } + const mouse_to_virtual_events_t& getMouseMappingPreset() const override { return m_camera->getMouseMappingPreset(); } + const imguizmo_to_virtual_events_t& getImguizmoMappingPreset() const override { return m_camera->getImguizmoMappingPreset(); } + + //! Manipulate the camera view gimbal by requesting a manipulation to its world target represented by a target gimbal, + //! on success it may change both the camera view gimbal & target gimbal + bool manipulateTargetGimbal(SUpdateParameters parameters, std::chrono::microseconds nextPresentationTimestamp) { - processKeyboard(output, vKeyboardEventsCount, parameters.keyboardEvents); - processMouse(output + vKeyboardEventsCount, vMouseEventsCount, parameters.mouseEvents); + // TODO & note to self: + // thats a little bit tricky -> a request of m_target manipulation which represents camera world target is a step where we consider only change of its + // position and translate that onto virtual orientation events we fire camera->manipulate with. Note that *we can fail* the manipulation because each + // camera type has some constraints on how it works right, however.. if any manipulation happens it means "target vector" changes and it doesn't matter + // what camera type is bound to the camera controller! If so, on success we can simply update m_target gimbal accordingly and represent it nicely on the + // screen with gizmo (as we do for camera view gimbal in Drag & Drop mode) or whatever, otherwise we do nothing because it means we failed the gimbal view + // manipulation hence "target vector" did not change (its really the orientation which changes right, but an orientation change means target vector changes) + + // and whats nice is we can do it with ANY controller now } - else - { - processKeyboard(nullptr, vKeyboardEventsCount, {}); - processMouse(nullptr, vMouseEventsCount, {}); - } - - count = vKeyboardEventsCount + vMouseEventsCount; - } - - inline const keyboard_to_virtual_events_t& getKeyboardVirtualEventMap() { return m_keyboardVirtualEventMap; } - inline const mouse_to_virtual_events_t& getMouseVirtualEventMap() { return m_mouseVirtualEventMap; } - inline interface_camera_t* getCamera() { return m_camera.get(); } - -private: - // Processes keyboard events to generate virtual manipulation events - void processKeyboard(CVirtualGimbalEvent* output, uint32_t& count, std::span events) - { - count = 0u; - const auto mappedVirtualEventsCount = m_keyboardVirtualEventMap.size(); - if (!output) + //! Manipulate the camera view gimbal directly, + //! on success it may change both the camera view gimbal & target gimbal + bool manipulateViewGimbal(SUpdateParameters parameters, std::chrono::microseconds nextPresentationTimestamp) { - count = mappedVirtualEventsCount; - return; - } - - if (mappedVirtualEventsCount) - { - const auto frameDeltaTime = std::chrono::duration_cast(nextPresentationTimeStamp - lastVirtualUpTimeStamp).count(); - assert(frameDeltaTime >= 0.f); + // TODO & note to self: + // and here there is a small difference because we don't map any target gimbal position change to virtual orientation events to request camera view + // gimbal manipulation but we directly try to manipulate the view gimbal of our camera and if we success then we simply update our m_target gimbal accordingly - for (auto& [key, hash] : m_keyboardVirtualEventMap) - { - hash.event.magnitude = 0.f; - - /* - if a key was already being held down from previous frames we compute with this - assumption that the key will be held down for this whole frame as well and its - delta action is simply frame delta time - */ + // and whats nice is we can do it with ANY controller now - if (hash.active) - hash.event.magnitude = frameDeltaTime; - } + std::vector virtualEvents(0x45); + uint32_t vCount; - for (const auto& keyboardEvent : events) + beginInputProcessing(nextPresentationTimestamp); { - auto request = m_keyboardVirtualEventMap.find(keyboardEvent.keyCode); - if (request != std::end(m_keyboardVirtualEventMap)) - { - auto& hash = request->second; + process(nullptr, vCount); - if (keyboardEvent.action == nbl::ui::SKeyboardEvent::ECA_PRESSED) - { - if (!hash.active) - { - // and if a key has been pressed after its last release event then first delta action is the key delta - const auto keyDeltaTime = std::chrono::duration_cast(nextPresentationTimeStamp - keyboardEvent.timeStamp).count(); - assert(keyDeltaTime >= 0); + if (virtualEvents.size() < vCount) + virtualEvents.resize(vCount); - hash.active = true; - hash.event.magnitude = keyDeltaTime; - } - } - else if (keyboardEvent.action == nbl::ui::SKeyboardEvent::ECA_RELEASED) - hash.active = false; - } + process(virtualEvents.data(), vCount, parameters); } + endInputProcessing(); - for (const auto& [key, hash] : m_keyboardVirtualEventMap) - { - if (hash.event.magnitude) - { - auto* virtualEvent = output + count; - virtualEvent->type = hash.event.type; - virtualEvent->magnitude = hash.event.magnitude; - ++count; - } - } - } - } + bool manipulated = m_camera->manipulate({ virtualEvents.data(), vCount }, interface_camera_t::Local); - // Processes mouse events to generate virtual manipulation events - void processMouse(CVirtualGimbalEvent* output, uint32_t& count, std::span events) - { - count = 0u; - const auto mappedVirtualEventsCount = m_mouseVirtualEventMap.size(); - - if (!output) - { - count = mappedVirtualEventsCount; - return; - } - - if (mappedVirtualEventsCount) - { - const auto frameDeltaTime = std::chrono::duration_cast(nextPresentationTimeStamp - lastVirtualUpTimeStamp).count(); - assert(frameDeltaTime >= 0.f); - - for (auto& [key, hash] : m_mouseVirtualEventMap) + if (manipulated) { - hash.event.magnitude = 0.f; - if (hash.active) - hash.event.magnitude = frameDeltaTime; + // TODO: *any* manipulate success? -> update m_target } - for (const auto& mouseEvent : events) - { - ui::E_MOUSE_CODE mouseCode = ui::EMC_NONE; - - switch (mouseEvent.type) - { - case ui::SMouseEvent::EET_CLICK: - { - switch (mouseEvent.clickEvent.mouseButton) - { - case ui::EMB_LEFT_BUTTON: mouseCode = ui::EMC_LEFT_BUTTON; break; - case ui::EMB_RIGHT_BUTTON: mouseCode = ui::EMC_RIGHT_BUTTON; break; - case ui::EMB_MIDDLE_BUTTON: mouseCode = ui::EMC_MIDDLE_BUTTON; break; - case ui::EMB_BUTTON_4: mouseCode = ui::EMC_BUTTON_4; break; - case ui::EMB_BUTTON_5: mouseCode = ui::EMC_BUTTON_5; break; - default: continue; - } - - auto request = m_mouseVirtualEventMap.find(mouseCode); - if (request != std::end(m_mouseVirtualEventMap)) - { - auto& hash = request->second; - - if (mouseEvent.clickEvent.action == ui::SMouseEvent::SClickEvent::EA_PRESSED) - { - if (!hash.active) - { - const auto keyDeltaTime = std::chrono::duration_cast(nextPresentationTimeStamp - mouseEvent.timeStamp).count(); - assert(keyDeltaTime >= 0); - - hash.active = true; - hash.event.magnitude += keyDeltaTime; - } - } - else if (mouseEvent.clickEvent.action == ui::SMouseEvent::SClickEvent::EA_RELEASED) - hash.active = false; - } - } break; - - case ui::SMouseEvent::EET_SCROLL: - { - if (mouseEvent.scrollEvent.verticalScroll != 0) - { - mouseCode = (mouseEvent.scrollEvent.verticalScroll > 0) ? ui::EMC_VERTICAL_POSITIVE_SCROLL : ui::EMC_VERTICAL_NEGATIVE_SCROLL; - auto request = m_mouseVirtualEventMap.find(mouseCode); - if (request != std::end(m_mouseVirtualEventMap)) - request->second.event.magnitude += std::abs(mouseEvent.scrollEvent.verticalScroll); - } - - if (mouseEvent.scrollEvent.horizontalScroll != 0) - { - mouseCode = (mouseEvent.scrollEvent.horizontalScroll > 0) ? ui::EMC_HORIZONTAL_POSITIVE_SCROLL : ui::EMC_HORIZONTAL_NEGATIVE_SCROLL; - auto request = m_mouseVirtualEventMap.find(mouseCode); - if (request != std::end(m_mouseVirtualEventMap)) - request->second.event.magnitude += std::abs(mouseEvent.scrollEvent.horizontalScroll); - } - } break; - - case ui::SMouseEvent::EET_MOVEMENT: - { - if (mouseEvent.movementEvent.relativeMovementX != 0) - { - mouseCode = (mouseEvent.movementEvent.relativeMovementX > 0) ? ui::EMC_RELATIVE_POSITIVE_MOVEMENT_X : ui::EMC_RELATIVE_NEGATIVE_MOVEMENT_X; - auto request = m_mouseVirtualEventMap.find(mouseCode); - if (request != std::end(m_mouseVirtualEventMap)) - request->second.event.magnitude += std::abs(mouseEvent.movementEvent.relativeMovementX); - } - - if (mouseEvent.movementEvent.relativeMovementY != 0) - { - mouseCode = (mouseEvent.movementEvent.relativeMovementY > 0) ? ui::EMC_RELATIVE_POSITIVE_MOVEMENT_Y : ui::EMC_RELATIVE_NEGATIVE_MOVEMENT_Y; - auto request = m_mouseVirtualEventMap.find(mouseCode); - if (request != std::end(m_mouseVirtualEventMap)) - request->second.event.magnitude += std::abs(mouseEvent.movementEvent.relativeMovementY); - } - } break; + return manipulated; + } - default: - break; - } - } + inline const interface_camera_t* getCamera() { return m_camera.get(); } - for (const auto& [key, hash] : m_mouseVirtualEventMap) - { - if (hash.event.magnitude) - { - auto* virtualEvent = output + count; - virtualEvent->type = hash.event.type; - virtualEvent->magnitude = hash.event.magnitude; - ++count; - } - } - } - } + private: + core::smart_refctd_ptr m_camera; - core::smart_refctd_ptr m_camera; - keyboard_to_virtual_events_t m_keyboardVirtualEventMap; - mouse_to_virtual_events_t m_mouseVirtualEventMap; -}; + //! Represents the camera world target vector as gimbal we can manipulate + CGeneralPurposeGimbal m_target; + }; } // nbl::hlsl namespace -#endif // _NBL_C_CAMERA_CONTROLLER_HPP_ \ No newline at end of file +#endif // _NBL_C_CAMERA_CONTROLLER_HPP_ diff --git a/common/include/camera/CGeneralPurposeGimbal.hpp b/common/include/camera/CGeneralPurposeGimbal.hpp index 9a66d5712..7e1c07096 100644 --- a/common/include/camera/CGeneralPurposeGimbal.hpp +++ b/common/include/camera/CGeneralPurposeGimbal.hpp @@ -9,9 +9,10 @@ namespace nbl::hlsl template class CGeneralPurposeGimbal : public IGimbal { + public: using base_t = IGimbal; - CGeneralPurposeGimbal(base_t::SCreationParameters&& parameters) : base_t(std::move(parameters)) {} + CGeneralPurposeGimbal(typename base_t::SCreationParameters&& parameters) : base_t(std::move(parameters)) {} ~CGeneralPurposeGimbal() = default; }; } diff --git a/common/include/camera/ICameraControl.hpp b/common/include/camera/ICameraControl.hpp deleted file mode 100644 index 56c3b289d..000000000 --- a/common/include/camera/ICameraControl.hpp +++ /dev/null @@ -1,110 +0,0 @@ -#ifndef _NBL_I_CAMERA_CONTROLLER_HPP_ -#define _NBL_I_CAMERA_CONTROLLER_HPP_ - -#include -#include -#include -#include -#include - -#include "IProjection.hpp" -#include "CGeneralPurposeGimbal.hpp" - -// TODO: DIFFERENT NAMESPACE -namespace nbl::hlsl -{ - -class ICameraController : virtual public core::IReferenceCounted -{ -public: - enum ControllerType : uint8_t - { - Keyboard = 0, - Mouse = 1, - - Count - }; - - struct CKeyInfo - { - union - { - ui::E_KEY_CODE keyboardCode; - ui::E_MOUSE_CODE mouseCode; - }; - - CKeyInfo(ControllerType _type, ui::E_KEY_CODE _keyboardCode) : keyboardCode(_keyboardCode), type(_type) {} - CKeyInfo(ControllerType _type, ui::E_MOUSE_CODE _mouseCode) : mouseCode(_mouseCode), type(_type) {} - - ControllerType type; - }; - - struct CHashInfo - { - CHashInfo() {} - CHashInfo(CVirtualGimbalEvent::VirtualEventType _type) : type(_type) {} - ~CHashInfo() = default; - - CVirtualGimbalEvent::VirtualEventType type = CVirtualGimbalEvent::VirtualEventType::None; - bool active = false; - float64_t dtAction = {}; - }; - - using keys_to_virtual_events_t = std::unordered_map; - - ICameraController() {} - - // Binds key codes to virtual events, the mapKeys lambda will be executed with controller keys_to_virtual_events_t table - void updateKeysToEvent(const std::function& mapKeys) - { - mapKeys(m_keysToVirtualEvents); - } - - inline const keys_to_virtual_events_t& getKeysToVirtualEvents() { return m_keysToVirtualEvents; } - - - void beginInputProcessing(const std::chrono::microseconds _nextPresentationTimeStamp) - { - nextPresentationTimeStamp = _nextPresentationTimeStamp; - } - - void endInputProcessing() - { - lastVirtualUpTimeStamp = nextPresentationTimeStamp; - } - -private: - keys_to_virtual_events_t m_keysToVirtualEvents; - std::chrono::microseconds nextPresentationTimeStamp = {}, lastVirtualUpTimeStamp = {}; -}; - -#if 0 // TOOD: update -template -concept GimbalRange = GeneralPurposeRange && requires -{ - requires ProjectionMatrix::projection_t>; - requires std::same_as, typename ICameraController::projection_t>::CGimbal>; -}; - -template -class IGimbalRange : public IRange -{ -public: - using base_t = IRange; - using range_t = typename base_t::range_t; - using gimbal_t = typename base_t::range_value_t; - - IGimbalRange(range_t&& gimbals) : base_t(std::move(gimbals)) {} - inline const range_t& getGimbals() const { return base_t::m_range; } - -protected: - inline range_t& getGimbals() const { return base_t::m_range; } -}; - -// TODO NOTE: eg. "follow camera" should use GimbalRange::CGimbal, 2u>>, -// one per camera itself and one for target it follows -#endif - -} // nbl::hlsl namespace - -#endif // _NBL_I_CAMERA_CONTROLLER_HPP_ \ No newline at end of file diff --git a/common/include/camera/ICameraController.hpp b/common/include/camera/ICameraController.hpp deleted file mode 100644 index 6df41d709..000000000 --- a/common/include/camera/ICameraController.hpp +++ /dev/null @@ -1,68 +0,0 @@ -#ifndef _NBL_I_CAMERA_CONTROLLER_HPP_ -#define _NBL_I_CAMERA_CONTROLLER_HPP_ - -#include -#include -#include -#include -#include - -#include "IProjection.hpp" -#include "CGeneralPurposeGimbal.hpp" - -// TODO: DIFFERENT NAMESPACE -namespace nbl::hlsl -{ - -class ICameraController : virtual public core::IReferenceCounted -{ -public: - enum ControllerType : uint8_t - { - Keyboard, - Mouse, - - Count - }; - - struct CKeyInfo - { - union - { - ui::E_KEY_CODE keyboardCode; - ui::E_MOUSE_CODE mouseCode; - }; - - CKeyInfo(ui::E_KEY_CODE _keyboardCode) : keyboardCode(_keyboardCode), type(Keyboard) {} - CKeyInfo(ui::E_MOUSE_CODE _mouseCode) : mouseCode(_mouseCode), type(Mouse) {} - - ControllerType type; - }; - - struct CHashInfo - { - CHashInfo() {} - CHashInfo(CVirtualGimbalEvent::VirtualEventType _type) : event({ .type = _type }) {} - ~CHashInfo() = default; - - CVirtualGimbalEvent event = {}; - bool active = false; - }; - - void beginInputProcessing(const std::chrono::microseconds _nextPresentationTimeStamp) - { - nextPresentationTimeStamp = _nextPresentationTimeStamp; - } - - void endInputProcessing() - { - lastVirtualUpTimeStamp = nextPresentationTimeStamp; - } - -protected: - std::chrono::microseconds nextPresentationTimeStamp = {}, lastVirtualUpTimeStamp = {}; -}; - -} // nbl::hlsl namespace - -#endif // _NBL_I_CAMERA_CONTROLLER_HPP_ \ No newline at end of file diff --git a/common/include/camera/IGimbal.hpp b/common/include/camera/IGimbal.hpp index fcc316ddd..334ebbbb8 100644 --- a/common/include/camera/IGimbal.hpp +++ b/common/include/camera/IGimbal.hpp @@ -202,6 +202,11 @@ namespace nbl::hlsl glm::quat orientation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f); }; + IGimbal(const IGimbal&) = default; + IGimbal(IGimbal&&) noexcept = default; + IGimbal& operator=(const IGimbal&) = default; + IGimbal& operator=(IGimbal&&) noexcept = default; + IGimbal(SCreationParameters&& parameters) : m_position(parameters.position), m_orientation(parameters.orientation), m_id(reinterpret_cast(this)) { @@ -261,30 +266,54 @@ namespace nbl::hlsl m_position = newPosition; } + inline void strafe(precision_t distance) + { + move(getXAxis() * distance); + } + + inline void climb(precision_t distance) + { + move(getYAxis() * distance); + } + + inline void advance(precision_t distance) + { + move(getZAxis() * distance); + } + void end() { m_isManipulating = false; } - // Position of gimbal + //! Position of gimbal in world space inline const auto& getPosition() const { return m_position; } - // Orientation of gimbal + //! Orientation of gimbal inline const auto& getOrientation() const { return m_orientation; } - // Orthonormal [getXAxis(), getYAxis(), getZAxis()] orientation matrix + //! Orthonormal [getXAxis(), getYAxis(), getZAxis()] orientation matrix inline const auto& getOrthonornalMatrix() const { return m_orthonormal; } - // Base "right" vector in orthonormal orientation basis (X-axis) + //! Base "right" vector in orthonormal orientation basis (X-axis) inline const auto& getXAxis() const { return m_orthonormal[0u]; } - // Base "up" vector in orthonormal orientation basis (Y-axis) + //! Base "up" vector in orthonormal orientation basis (Y-axis) inline const auto& getYAxis() const { return m_orthonormal[1u]; } - // Base "forward" vector in orthonormal orientation basis (Z-axis) + //! Base "forward" vector in orthonormal orientation basis (Z-axis) inline const auto& getZAxis() const { return m_orthonormal[2u]; } + //! Target vector in local space, alias for getZAxis() + inline const auto getLocalTarget() const { return getZAxis(); } + + //! Target vector in world space + inline const auto getWorldTarget() const { return getPosition() + getLocalTarget(); } + + //! Counts how many times a valid manipulation has been performed, the counter resets when begin() is called inline const auto& getManipulationCounter() { return m_counter; } + + //! Returns true if gimbal records a manipulation inline bool isManipulating() const { return m_isManipulating; } private: @@ -293,17 +322,23 @@ namespace nbl::hlsl m_orthonormal = matrix(glm::mat3_cast(glm::normalize(m_orientation))); } + //! Position of a gimbal in world space vector m_position; - glm::quat m_orientation; // TODO: precision + + //! Normalized orientation of gimbal + //! TODO: precision + replace with our "quat at home" + glm::quat m_orientation; + + //! Orthonormal base composed from "m_orientation" representing gimbal's "forward", "up" & "right" vectors in local space matrix m_orthonormal; - // Counts *performed* manipulations, a manipulation with 0 delta is not counted! + //! Counter that increments for each performed manipulation, resets with each begin() call size_t m_counter = {}; - // Records manipulation state + //! Tracks whether gimbal is currently in manipulation mode bool m_isManipulating = false; - // the fact ImGUIZMO has global context I don't like, however for IDs we can do a life-tracking trick and cast addresses which are unique & we don't need any global associative container to track them! + //! The fact ImGUIZMO has global context I don't like, however for IDs we can do a life-tracking trick and cast addresses which are unique & we don't need any global associative container to track them! const uintptr_t m_id; }; } // namespace nbl::hlsl diff --git a/common/include/camera/IGimbalController.hpp b/common/include/camera/IGimbalController.hpp new file mode 100644 index 000000000..a1201a7c1 --- /dev/null +++ b/common/include/camera/IGimbalController.hpp @@ -0,0 +1,374 @@ +#ifndef _NBL_I_CAMERA_CONTROLLER_HPP_ +#define _NBL_I_CAMERA_CONTROLLER_HPP_ + +///////////////////////// +// TODO: TEMPORARY!!! +#include "common.hpp" +namespace ImGuizmo +{ + void DecomposeMatrixToComponents(const float*, float*, float*, float*); +} +///////////////////////// + +#include "IProjection.hpp" +#include "IGimbal.hpp" + +// TODO: DIFFERENT NAMESPACE +namespace nbl::hlsl +{ + +struct IGimbalManipulateEncoder +{ + IGimbalManipulateEncoder() {} + virtual ~IGimbalManipulateEncoder() {} + + //! output of any controller process method + using gimbal_event_t = CVirtualGimbalEvent; + + //! encode keyboard code used to translate to gimbal_event_t event + using encode_keyboard_code_t = ui::E_KEY_CODE; + + //! encode mouse code used to translate to gimbal_event_t event + using encode_mouse_code_t = ui::E_MOUSE_CODE; + + //! encode ImGuizmo code used to translate to gimbal_event_t event + using encode_imguizmo_code_t = gimbal_event_t::VirtualEventType; + + //! Encoder types, a controller takes encoder type events and outputs gimbal_event_t events + enum EncoderType : uint8_t + { + Keyboard, + Mouse, + Imguizmo, + + Count + }; + + //! A key in a hash map which is "encode__code_t" as union with information about EncoderType the encode value got produced from + struct CKeyInfo + { + union + { + encode_keyboard_code_t keyboardCode; + encode_mouse_code_t mouseCode; + encode_imguizmo_code_t imguizmoCode; + }; + + CKeyInfo(encode_keyboard_code_t code) : keyboardCode(code), type(Keyboard) {} + CKeyInfo(encode_mouse_code_t code) : mouseCode(code), type(Mouse) {} + CKeyInfo(encode_imguizmo_code_t code) : imguizmoCode(code), type(Imguizmo) {} + + EncoderType type; + }; + + //! Hash value in hash map which is gimbal_event_t & state + struct CHashInfo + { + CHashInfo() {} + CHashInfo(gimbal_event_t::VirtualEventType _type) : event({ .type = _type }) {} + ~CHashInfo() = default; + + gimbal_event_t event = {}; + bool active = false; + }; + + using keyboard_to_virtual_events_t = std::unordered_map; + using mouse_to_virtual_events_t = std::unordered_map; + using imguizmo_to_virtual_events_t = std::unordered_map; + + //! default preset with encode_keyboard_code_t to gimbal_virtual_event_t map + virtual const keyboard_to_virtual_events_t& getKeyboardMappingPreset() const = 0u; + + //! default preset with encode_keyboard_code_t to gimbal_virtual_event_t map + virtual const mouse_to_virtual_events_t& getMouseMappingPreset() const = 0u; + + //! default preset with encode_keyboard_code_t to gimbal_virtual_event_t map + virtual const imguizmo_to_virtual_events_t& getImguizmoMappingPreset() const = 0u; +}; + +class IGimbalController : public IGimbalManipulateEncoder, virtual public core::IReferenceCounted +{ +public: + using IGimbalManipulateEncoder::IGimbalManipulateEncoder; + + IGimbalController() {} + virtual ~IGimbalController() {} + + //! input of keyboard gimbal controller process utility - Nabla UI event handler produces ui::SKeyboardEvent events + using input_keyboard_event_t = ui::SKeyboardEvent; + + //! input of mouse gimbal controller process utility - Nabla UI event handler produces ui::SMouseEvent events + using input_mouse_event_t = ui::SMouseEvent; + + //! input of ImGuizmo gimbal controller process utility - ImGuizmo manipulate utility produces "delta (TRS) matrix" events + using input_imguizmo_event_t = float32_t4x4; + + void beginInputProcessing(const std::chrono::microseconds nextPresentationTimeStamp) + { + m_nextPresentationTimeStamp = nextPresentationTimeStamp; + m_frameDeltaTime = std::chrono::duration_cast(m_nextPresentationTimeStamp - m_lastVirtualUpTimeStamp).count(); + assert(m_frameDeltaTime >= 0.f); + } + + void endInputProcessing() + { + m_lastVirtualUpTimeStamp = m_nextPresentationTimeStamp; + } + + // Binds mouse key codes to virtual events, the mapKeys lambda will be executed with controller keyboard_to_virtual_events_t table + void updateKeyboardMapping(const std::function& mapKeys) { mapKeys(m_keyboardVirtualEventMap); } + + // Binds mouse key codes to virtual events, the mapKeys lambda will be executed with controller mouse_to_virtual_events_t table + void updateMouseMapping(const std::function& mapKeys) { mapKeys(m_mouseVirtualEventMap); } + + // Binds imguizmo key codes to virtual events, the mapKeys lambda will be executed with controller imguizmo_to_virtual_events_t table + void updateImguizmoMapping(const std::function& mapKeys) { mapKeys(m_imguizmoVirtualEventMap); } + + struct SUpdateParameters + { + std::span keyboardEvents = {}; + std::span mouseEvents = {}; + std::span imguizmoEvents = {}; + }; + + // Processes SUpdateParameters events to generate virtual manipulation events + void process(gimbal_event_t* output, uint32_t& count, const SUpdateParameters parameters = {}) + { + count = 0u; + uint32_t vKeyboardEventsCount = {}, vMouseEventsCount = {}, vImguizmoEventsCount = {}; + + if (output) + { + processKeyboard(output, vKeyboardEventsCount, parameters.keyboardEvents); output += vKeyboardEventsCount; + processMouse(output, vMouseEventsCount, parameters.mouseEvents); output += vMouseEventsCount; + processImguizmo(output, vImguizmoEventsCount, parameters.imguizmoEvents); + } + else + { + processKeyboard(nullptr, vKeyboardEventsCount, {}); + processMouse(nullptr, vMouseEventsCount, {}); + processImguizmo(nullptr, vImguizmoEventsCount, {}); + } + + count = vKeyboardEventsCount + vMouseEventsCount + vImguizmoEventsCount; + } + + inline const keyboard_to_virtual_events_t& getKeyboardVirtualEventMap() { return m_keyboardVirtualEventMap; } + inline const mouse_to_virtual_events_t& getMouseVirtualEventMap() { return m_mouseVirtualEventMap; } + inline const imguizmo_to_virtual_events_t& getImguizmoVirtualEventMap() { return m_imguizmoVirtualEventMap; } + +private: + // Processes keyboard events to generate virtual manipulation events + void processKeyboard(gimbal_event_t* output, uint32_t& count, std::span events) + { + count = 0u; + const auto mappedVirtualEventsCount = m_keyboardVirtualEventMap.size(); + + if (!output) + { + count = mappedVirtualEventsCount; + return; + } + + if (mappedVirtualEventsCount) + { + preprocess(m_keyboardVirtualEventMap); + + for (const auto& keyboardEvent : events) + { + auto request = m_keyboardVirtualEventMap.find(keyboardEvent.keyCode); + if (request != std::end(m_keyboardVirtualEventMap)) + { + auto& hash = request->second; + + if (keyboardEvent.action == input_keyboard_event_t::ECA_PRESSED) + { + if (!hash.active) + { + const auto keyDeltaTime = std::chrono::duration_cast(m_nextPresentationTimeStamp - keyboardEvent.timeStamp).count(); + assert(keyDeltaTime >= 0); + + hash.active = true; + hash.event.magnitude = keyDeltaTime; + } + } + else if (keyboardEvent.action == input_keyboard_event_t::ECA_RELEASED) + hash.active = false; + } + } + + postprocess(m_keyboardVirtualEventMap, output, count); + } + } + + // Processes mouse events to generate virtual manipulation events + void processMouse(gimbal_event_t* output, uint32_t& count, std::span events) + { + count = 0u; + const auto mappedVirtualEventsCount = m_mouseVirtualEventMap.size(); + + if (!output) + { + count = mappedVirtualEventsCount; + return; + } + + if (mappedVirtualEventsCount) + { + preprocess(m_mouseVirtualEventMap); + + for (const auto& mouseEvent : events) + { + ui::E_MOUSE_CODE mouseCode = ui::EMC_NONE; + + switch (mouseEvent.type) + { + case input_mouse_event_t::EET_CLICK: + { + switch (mouseEvent.clickEvent.mouseButton) + { + case ui::EMB_LEFT_BUTTON: mouseCode = ui::EMC_LEFT_BUTTON; break; + case ui::EMB_RIGHT_BUTTON: mouseCode = ui::EMC_RIGHT_BUTTON; break; + case ui::EMB_MIDDLE_BUTTON: mouseCode = ui::EMC_MIDDLE_BUTTON; break; + case ui::EMB_BUTTON_4: mouseCode = ui::EMC_BUTTON_4; break; + case ui::EMB_BUTTON_5: mouseCode = ui::EMC_BUTTON_5; break; + default: continue; + } + + auto request = m_mouseVirtualEventMap.find(mouseCode); + if (request != std::end(m_mouseVirtualEventMap)) + { + auto& hash = request->second; + + if (mouseEvent.clickEvent.action == input_mouse_event_t::SClickEvent::EA_PRESSED) + { + if (!hash.active) + { + const auto keyDeltaTime = std::chrono::duration_cast(m_nextPresentationTimeStamp - mouseEvent.timeStamp).count(); + assert(keyDeltaTime >= 0); + + hash.active = true; + hash.event.magnitude += keyDeltaTime; + } + } + else if (mouseEvent.clickEvent.action == input_mouse_event_t::SClickEvent::EA_RELEASED) + hash.active = false; + } + } break; + + case input_mouse_event_t::EET_SCROLL: + { + requestMagnitudeUpdateWithScalar(0.f, float(mouseEvent.scrollEvent.verticalScroll), float(std::abs(mouseEvent.scrollEvent.verticalScroll)), ui::EMC_VERTICAL_POSITIVE_SCROLL, ui::EMC_VERTICAL_NEGATIVE_SCROLL, m_mouseVirtualEventMap); + requestMagnitudeUpdateWithScalar(0.f, mouseEvent.scrollEvent.horizontalScroll, std::abs(mouseEvent.scrollEvent.horizontalScroll), ui::EMC_HORIZONTAL_POSITIVE_SCROLL, ui::EMC_HORIZONTAL_NEGATIVE_SCROLL, m_mouseVirtualEventMap); + } break; + + case input_mouse_event_t::EET_MOVEMENT: + { + requestMagnitudeUpdateWithScalar(0.f, mouseEvent.movementEvent.relativeMovementX, std::abs(mouseEvent.movementEvent.relativeMovementX), ui::EMC_RELATIVE_POSITIVE_MOVEMENT_X, ui::EMC_RELATIVE_NEGATIVE_MOVEMENT_X, m_mouseVirtualEventMap); + requestMagnitudeUpdateWithScalar(0.f, mouseEvent.movementEvent.relativeMovementY, std::abs(mouseEvent.movementEvent.relativeMovementY), ui::EMC_RELATIVE_POSITIVE_MOVEMENT_Y, ui::EMC_RELATIVE_NEGATIVE_MOVEMENT_Y, m_mouseVirtualEventMap); + } break; + + default: + break; + } + } + + postprocess(m_mouseVirtualEventMap, output, count); + } + } + + void processImguizmo(gimbal_event_t* output, uint32_t& count, std::span events) + { + count = 0u; + const auto mappedVirtualEventsCount = m_imguizmoVirtualEventMap.size(); + + if (!output) + { + count = mappedVirtualEventsCount; + return; + } + + if (mappedVirtualEventsCount) + { + preprocess(m_imguizmoVirtualEventMap); + + for (const auto& deltaMatrix : events) + { + std::array dTranslation, dRotation, dScale; + + // TODO: since I assume the delta matrix is from imguizmo I must follow its API (but this one I could actually steal & rewrite to Nabla to not call imguizmo stuff here) + ImGuizmo::DecomposeMatrixToComponents(&deltaMatrix[0][0], dTranslation.data(), dRotation.data(), dScale.data()); + + // note that translating imguizmo delta matrix to gimbal_event_t events is indeed hardcoded, but what + // *isn't* is those gimbal_event_t events are then translated according to m_imguizmoVirtualEventMap + // user defined map to + + // Delta translation impulse + requestMagnitudeUpdateWithScalar(0.f, dTranslation[0], dTranslation[0], gimbal_event_t::MoveRight, gimbal_event_t::MoveLeft, m_imguizmoVirtualEventMap); + requestMagnitudeUpdateWithScalar(0.f, dTranslation[1], dTranslation[1], gimbal_event_t::MoveUp, gimbal_event_t::MoveDown, m_imguizmoVirtualEventMap); + requestMagnitudeUpdateWithScalar(0.f, dTranslation[2], dTranslation[2], gimbal_event_t::MoveForward, gimbal_event_t::MoveBackward, m_imguizmoVirtualEventMap); + + // Delta rotation impulse + requestMagnitudeUpdateWithScalar(0.f, dRotation[0], dRotation[0], gimbal_event_t::PanRight, gimbal_event_t::PanLeft, m_imguizmoVirtualEventMap); + requestMagnitudeUpdateWithScalar(0.f, dRotation[1], dRotation[1], gimbal_event_t::TiltUp, gimbal_event_t::TiltDown, m_imguizmoVirtualEventMap); + requestMagnitudeUpdateWithScalar(0.f, dRotation[2], dRotation[2], gimbal_event_t::RollRight, gimbal_event_t::RollLeft, m_imguizmoVirtualEventMap); + + // Delta scale impulse + requestMagnitudeUpdateWithScalar(1.f, dScale[0], dScale[0], gimbal_event_t::ScaleXInc, gimbal_event_t::ScaleXDec, m_imguizmoVirtualEventMap); + requestMagnitudeUpdateWithScalar(1.f, dScale[1], dScale[1], gimbal_event_t::ScaleYInc, gimbal_event_t::ScaleYDec, m_imguizmoVirtualEventMap); + requestMagnitudeUpdateWithScalar(1.f, dScale[2], dScale[2], gimbal_event_t::ScaleZInc, gimbal_event_t::ScaleZDec, m_imguizmoVirtualEventMap); + } + + postprocess(m_imguizmoVirtualEventMap, output, count); + } + } + + //! helper utility, for any controller this should be called before any update of hash map + void preprocess(auto& map) + { + for (auto& [key, hash] : map) + { + hash.event.magnitude = 0.0f; + + if (hash.active) + hash.event.magnitude = m_frameDeltaTime; + } + } + + //! helper utility, for any controller this should be called after updating a hash map + void postprocess(const auto& map, gimbal_event_t* output, uint32_t& count) + { + for (const auto& [key, hash] : map) + if (hash.event.magnitude) + { + auto* virtualEvent = output + count; + virtualEvent->type = hash.event.type; + virtualEvent->magnitude = hash.event.magnitude; + ++count; + } + } + + //! helper utility, it *doesn't* assume we keep requested events alive but only increase their magnitude + template + void requestMagnitudeUpdateWithScalar(float signPivot, float dScalar, float dMagnitude, EncodeType positive, EncodeType negative, Map& map) + { + if (dScalar != signPivot) + { + auto code = (dScalar > signPivot) ? positive : negative; + auto request = map.find(code); + if (request != map.end()) + request->second.event.magnitude += dMagnitude; + } + } + + keyboard_to_virtual_events_t m_keyboardVirtualEventMap; + mouse_to_virtual_events_t m_mouseVirtualEventMap; + imguizmo_to_virtual_events_t m_imguizmoVirtualEventMap; + + size_t m_frameDeltaTime = {}; + std::chrono::microseconds m_nextPresentationTimeStamp = {}, m_lastVirtualUpTimeStamp = {}; +}; + +} // nbl::hlsl namespace + +#endif // _NBL_I_CAMERA_CONTROLLER_HPP_ \ No newline at end of file From f0d4690a4363d7e18e29e58699f0258d652a3c12 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Tue, 12 Nov 2024 08:22:37 +0100 Subject: [PATCH 032/205] accumulate transform in FPS camera' base_t::World case with respect to standard base (write a note in comments), adjust UI's main.cpp to new frames in flight + a few small updates --- 61_UI/main.cpp | 30 ++++++++--------- common/include/CFPSCamera.hpp | 11 +++++-- common/include/ICamera.hpp | 55 ++++++++++++++----------------- common/include/camera/IGimbal.hpp | 1 + 4 files changed, 48 insertions(+), 49 deletions(-) diff --git a/61_UI/main.cpp b/61_UI/main.cpp index 3851d7177..520682545 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -12,6 +12,9 @@ using camera_t = CFPSCamera; using controller_t = CCameraController; using projection_t = IProjection>; // TODO: temporary -> projections will own/reference cameras +// 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; + /* Renders scene texture to an offline framebuffer which color attachment @@ -117,16 +120,9 @@ class UISampleApp final : public examples::SimpleWindowedApplication if (!m_surface || !m_surface->init(gQueue, std::move(scResources), swapchainParams.sharedParams)) return logFail("Could not create Window & Surface or initialize the Surface!"); - m_maxFramesInFlight = m_surface->getMaxFramesInFlight(); - if (FRAMES_IN_FLIGHT < m_maxFramesInFlight) - { - m_logger->log("Lowering frames in flight!", ILogger::ELL_WARNING); - m_maxFramesInFlight = FRAMES_IN_FLIGHT; - } - m_cmdPool = m_device->createCommandPool(gQueue->getFamilyIndex(), IGPUCommandPool::CREATE_FLAGS::RESET_COMMAND_BUFFER_BIT); - for (auto i = 0u; i < m_maxFramesInFlight; i++) + for (auto i = 0u; i < MaxFramesInFlight; i++) { if (!m_cmdPool) return logFail("Couldn't create Command Pool!"); @@ -562,21 +558,26 @@ class UISampleApp final : public examples::SimpleWindowedApplication inline void workLoopBody() override { - const auto resourceIx = m_realFrameIx % m_maxFramesInFlight; - - if (m_realFrameIx >= m_maxFramesInFlight) + // framesInFlight: ensuring safe execution of command buffers and acquires, `framesInFlight` only affect semaphore waits, don't use this to index your resources because it can change with swapchain recreation. + const uint32_t framesInFlight = core::min(MaxFramesInFlight, m_surface->getMaxAcquiresInFlight()); + // We block for semaphores for 2 reasons here: + // A) Resource: Can't use resource like a command buffer BEFORE previous use is finished! [MaxFramesInFlight] + // B) Acquire: Can't have more acquires in flight than a certain threshold returned by swapchain or your surface helper class. [MaxAcquiresInFlight] + if (m_realFrameIx >= framesInFlight) { const ISemaphore::SWaitInfo cbDonePending[] = { { .semaphore = m_semaphore.get(), - .value = m_realFrameIx + 1 - m_maxFramesInFlight + .value = m_realFrameIx + 1 - framesInFlight } }; if (m_device->blockForSemaphores(cbDonePending) != ISemaphore::WAIT_RESULT::SUCCESS) return; } + const auto resourceIx = m_realFrameIx % MaxFramesInFlight; + // CPU events update(); @@ -775,9 +776,8 @@ class UISampleApp final : public examples::SimpleWindowedApplication smart_refctd_ptr m_pipeline; smart_refctd_ptr m_semaphore; smart_refctd_ptr m_cmdPool; - uint64_t m_realFrameIx : 59 = 0; - uint64_t m_maxFramesInFlight : 5; - std::array, ISwapchain::MaxImages> m_cmdBufs; + uint64_t m_realFrameIx = 0; + std::array, MaxFramesInFlight> m_cmdBufs; ISimpleManagedSurface::SAcquireResult m_currentImageAcquire = {}; smart_refctd_ptr m_assetManager; diff --git a/common/include/CFPSCamera.hpp b/common/include/CFPSCamera.hpp index e0f9dec53..dc358ecfe 100644 --- a/common/include/CFPSCamera.hpp +++ b/common/include/CFPSCamera.hpp @@ -53,7 +53,9 @@ class CFPSCamera final : public ICamera case base_t::World: { - const auto transform = m_gimbal.accumulate(virtualEvents); + // TODO: I make assumption what world base is now (at least temporary), I need to think of this but maybe each ITransformObject should know what its world is + // in ideal scenario we would define this crazy enum with all possible standard bases but in Nabla we only really care about LH/RH coordinate systems + const auto transform = m_gimbal.accumulate(virtualEvents, { 1, 0, 0 }, { 0, 1, 0 }, { 0, 0, 1 }); const auto newPitch = std::clamp(transform.dVirtualRotation.x, MinVerticalAngle, MaxVerticalAngle), newYaw = transform.dVirtualRotation.y; newOrientation = glm::quat(glm::vec3(newPitch, newYaw, 0.0f)); newPosition = transform.dVirtualTranslate; } break; @@ -67,11 +69,14 @@ class CFPSCamera final : public ICamera { m_gimbal.setOrientation(newOrientation); m_gimbal.setPosition(newPosition); - m_gimbal.updateView(); - manipulated &= bool(m_gimbal.getManipulationCounter()); } m_gimbal.end(); + manipulated &= bool(m_gimbal.getManipulationCounter()); + + if(manipulated) + m_gimbal.updateView(); + return manipulated; } diff --git a/common/include/ICamera.hpp b/common/include/ICamera.hpp index fbbb5a863..630c7b624 100644 --- a/common/include/ICamera.hpp +++ b/common/include/ICamera.hpp @@ -40,41 +40,34 @@ class ICamera : public IGimbalManipulateEncoder, virtual public core::IReference struct SView { matrix matrix = {}; - bool isLeftHandSystem = true; }; inline void updateView() - { - if (base_t::getManipulationCounter()) + { + const auto& gRight = base_t::getXAxis(), gUp = base_t::getYAxis(), gForward = base_t::getZAxis(); + + auto isNormalized = [](const auto& v, float epsilon) -> bool + { + return glm::epsilonEqual(glm::length(v), 1.0f, epsilon); + }; + + auto isOrthogonal = [](const auto& a, const auto& b, float epsilon) -> bool { - const auto& gRight = base_t::getXAxis(), gUp = base_t::getYAxis(), gForward = base_t::getZAxis(); - - // TODO: I think this should be set as request state, depending on the value we do m_view.matrix[2u] flip accordingly - // m_view.isLeftHandSystem; - - auto isNormalized = [](const auto& v, float epsilon) -> bool - { - return glm::epsilonEqual(glm::length(v), 1.0f, epsilon); - }; - - auto isOrthogonal = [](const auto& a, const auto& b, float epsilon) -> bool - { - return glm::epsilonEqual(glm::dot(a, b), 0.0f, epsilon); - }; - - auto isOrthoBase = [&](const auto& x, const auto& y, const auto& z, float epsilon = 1e-6f) -> bool - { - return isNormalized(x, epsilon) && isNormalized(y, epsilon) && isNormalized(z, epsilon) && - isOrthogonal(x, y, epsilon) && isOrthogonal(x, z, epsilon) && isOrthogonal(y, z, epsilon); - }; - - assert(isOrthoBase(gRight, gUp, gForward)); - - const auto& position = base_t::getPosition(); - m_view.matrix[0u] = vector(gRight, -glm::dot(gRight, position)); - m_view.matrix[1u] = vector(gUp, -glm::dot(gUp, position)); - m_view.matrix[2u] = vector(gForward, -glm::dot(gForward, position)); - } + return glm::epsilonEqual(glm::dot(a, b), 0.0f, epsilon); + }; + + auto isOrthoBase = [&](const auto& x, const auto& y, const auto& z, float epsilon = 1e-6f) -> bool + { + return isNormalized(x, epsilon) && isNormalized(y, epsilon) && isNormalized(z, epsilon) && + isOrthogonal(x, y, epsilon) && isOrthogonal(x, z, epsilon) && isOrthogonal(y, z, epsilon); + }; + + assert(isOrthoBase(gRight, gUp, gForward)); + + const auto& position = base_t::getPosition(); + m_view.matrix[0u] = vector(gRight, -glm::dot(gRight, position)); + m_view.matrix[1u] = vector(gUp, -glm::dot(gUp, position)); + m_view.matrix[2u] = vector(gForward, -glm::dot(gForward, position)); } // Getter for gimbal's view diff --git a/common/include/camera/IGimbal.hpp b/common/include/camera/IGimbal.hpp index 334ebbbb8..341f55e17 100644 --- a/common/include/camera/IGimbal.hpp +++ b/common/include/camera/IGimbal.hpp @@ -102,6 +102,7 @@ namespace nbl::hlsl vector dVirtualTranslate { 0.0f }, dVirtualRotation { 0.0f }, dVirtualScale { 0.0f }; }; + // TODO: document it template VirtualImpulse accumulate(std::span virtualEvents, const vector& gRightOverride, const vector& gUpOverride, const vector& gForwardOverride) { From 6dab0d472b8a89f44944b22fae5af0ed9683a83a Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Thu, 14 Nov 2024 14:28:06 +0100 Subject: [PATCH 033/205] I need more space for more viewports, allow to resize the window --- 61_UI/main.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/61_UI/main.cpp b/61_UI/main.cpp index 520682545..a350ba107 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -51,7 +51,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication params.height = WIN_H; params.x = 32; params.y = 32; - params.flags = ui::IWindow::ECF_HIDDEN | IWindow::ECF_BORDERLESS | IWindow::ECF_RESIZABLE; + params.flags = ui::IWindow::ECF_HIDDEN | IWindow::ECF_BORDERLESS | IWindow::ECF_RESIZABLE | IWindow::ECF_CAN_RESIZE | IWindow::ECF_CAN_MAXIMIZE; params.windowCaption = "UISampleApp"; params.callback = windowCallback; const_cast&>(m_window) = m_winMgr->createWindow(std::move(params)); @@ -603,8 +603,8 @@ class UISampleApp final : public examples::SimpleWindowedApplication viewport.maxDepth = 0.f; viewport.x = 0u; viewport.y = 0u; - viewport.width = WIN_W; - viewport.height = WIN_H; + viewport.width = m_window->getWidth(); + viewport.height = m_window->getHeight(); } cb->setViewport(0u, 1u, &viewport); From a50cf75d33dc22dea16d76621ba83449c4fbaa1f Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Fri, 15 Nov 2024 09:23:48 +0100 Subject: [PATCH 034/205] set better initial position & orientation of camera, cleanup in GC shaders --- 61_UI/main.cpp | 20 ++++------ common/include/CFPSCamera.hpp | 2 +- common/src/geometry/creator/CMakeLists.txt | 4 -- .../creator/shaders/gc.basic.fragment.hlsl | 2 +- .../creator/shaders/gc.basic.vertex.hlsl | 13 +++--- .../creator/shaders/gc.cone.vertex.hlsl | 12 +++--- .../creator/shaders/gc.ico.vertex.hlsl | 12 +++--- .../creator/shaders/grid.fragment.hlsl | 12 ------ .../geometry/creator/shaders/grid.vertex.hlsl | 17 -------- .../template/gc.basic.vertex.input.hlsl | 16 -------- .../creator/shaders/template/gc.common.hlsl | 4 -- .../template/gc.cone.vertex.input.hlsl | 15 ------- .../shaders/template/gc.ico.vertex.input.hlsl | 15 ------- .../creator/shaders/template/gc.vertex.hlsl | 4 -- .../creator/shaders/template/grid.common.hlsl | 40 ------------------- 15 files changed, 31 insertions(+), 157 deletions(-) delete mode 100644 common/src/geometry/creator/shaders/grid.fragment.hlsl delete mode 100644 common/src/geometry/creator/shaders/grid.vertex.hlsl delete mode 100644 common/src/geometry/creator/shaders/template/gc.basic.vertex.input.hlsl delete mode 100644 common/src/geometry/creator/shaders/template/gc.cone.vertex.input.hlsl delete mode 100644 common/src/geometry/creator/shaders/template/gc.ico.vertex.input.hlsl delete mode 100644 common/src/geometry/creator/shaders/template/grid.common.hlsl diff --git a/61_UI/main.cpp b/61_UI/main.cpp index a350ba107..12c204d0f 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -51,7 +51,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication params.height = WIN_H; params.x = 32; params.y = 32; - params.flags = ui::IWindow::ECF_HIDDEN | IWindow::ECF_BORDERLESS | IWindow::ECF_RESIZABLE | IWindow::ECF_CAN_RESIZE | IWindow::ECF_CAN_MAXIMIZE; + params.flags = ui::IWindow::ECF_HIDDEN | IWindow::ECF_BORDERLESS | IWindow::ECF_MAXIMIZED; params.windowCaption = "UISampleApp"; params.callback = windowCallback; const_cast&>(m_window) = m_winMgr->createWindow(std::move(params)); @@ -407,16 +407,11 @@ class UISampleApp final : public examples::SimpleWindowedApplication const auto& orientation = camera->getGimbal().getOrthonornalMatrix(); - addMatrixTable("Model Matrix", "ModelMatrixTable", 3, 4, &pass.scene->object.model[0][0]); - - addMatrixTable("Right", "OrientationRightVec", 1, 3, &camera->getGimbal().getXAxis()[0]); - addMatrixTable("Up", "OrientationUpVec", 1, 3, &camera->getGimbal().getYAxis()[0]); - addMatrixTable("Forward", "OrientationForwardVec", 1, 3, &camera->getGimbal().getZAxis()[0]); - addMatrixTable("Position", "PositionForwardVec", 1, 3, &camera->getGimbal().getPosition()[0]); - - //addMatrixTable("Camera Gimbal Orientation Matrix", "OrientationMatrixTable", 3, 3, &orientation[0][0]); - addMatrixTable("Camera Gimbal View Matrix", "ViewMatrixTable", 3, 4, &view.matrix[0][0]); - addMatrixTable("Camera Gimbal Projection Matrix", "ProjectionMatrixTable", 4, 4, &projectionMatrix[0][0], false); + addMatrixTable("Object's Model Matrix", "ModelMatrixTable", 3, 4, &pass.scene->object.model[0][0]); + addMatrixTable("Camera's Position", "PositionForwardVec", 1, 3, &camera->getGimbal().getPosition()[0]); + addMatrixTable("Camera's Orientation Quat", "OrientationQuatTable", 1, 4, &camera->getGimbal().getOrientation()[0]); + addMatrixTable("Camera's View Matrix", "ViewMatrixTable", 3, 4, &view.matrix[0][0]); + addMatrixTable("Bound Projection Matrix", "ProjectionMatrixTable", 4, 4, &projectionMatrix[0][0], false); ImGui::End(); } @@ -505,9 +500,8 @@ class UISampleApp final : public examples::SimpleWindowedApplication TESTS, TODO: remove all once finished work & integrate with the example properly */ - const float32_t3 position(cosf(camYAngle)* cosf(camXAngle)* transformParams.camDistance, sinf(camXAngle)* transformParams.camDistance, sinf(camYAngle)* cosf(camXAngle)* transformParams.camDistance); projection->setMatrix(buildProjectionMatrixPerspectiveFovLH(glm::radians(fov), float(m_window->getWidth()) / float(m_window->getHeight()), zNear, zFar)); - camera = make_smart_refctd_ptr(position); + camera = make_smart_refctd_ptr(float32_t3{ -1.958f, 0.697f, 0.881f }, glm::quat(0.092f, 0.851f, -0.159f, 0.492f)); controller = make_smart_refctd_ptr(core::smart_refctd_ptr(camera)); // init keyboard map diff --git a/common/include/CFPSCamera.hpp b/common/include/CFPSCamera.hpp index dc358ecfe..92943d01e 100644 --- a/common/include/CFPSCamera.hpp +++ b/common/include/CFPSCamera.hpp @@ -17,7 +17,7 @@ class CFPSCamera final : public ICamera public: using base_t = ICamera; - CFPSCamera(const vector& position, glm::quat orientation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f)) + CFPSCamera(const vector& position, const glm::quat& orientation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f)) : base_t(), m_gimbal({ .position = position, .orientation = orientation }) {} ~CFPSCamera() = default; diff --git a/common/src/geometry/creator/CMakeLists.txt b/common/src/geometry/creator/CMakeLists.txt index 336d32fe5..f0e824ebe 100644 --- a/common/src/geometry/creator/CMakeLists.txt +++ b/common/src/geometry/creator/CMakeLists.txt @@ -12,10 +12,6 @@ set(NBL_THIS_EXAMPLE_INPUT_SHADERS "${NBL_THIS_EXAMPLE_INPUT_SHADERS_DIRECTORY}/gc.basic.vertex.hlsl" "${NBL_THIS_EXAMPLE_INPUT_SHADERS_DIRECTORY}/gc.cone.vertex.hlsl" "${NBL_THIS_EXAMPLE_INPUT_SHADERS_DIRECTORY}/gc.ico.vertex.hlsl" - - # grid - "${NBL_THIS_EXAMPLE_INPUT_SHADERS_DIRECTORY}/grid.vertex.hlsl" - "${NBL_THIS_EXAMPLE_INPUT_SHADERS_DIRECTORY}/grid.fragment.hlsl" ) file(GLOB_RECURSE NBL_THIS_EXAMPLE_INPUT_COMMONS CONFIGURE_DEPENDS "${NBL_THIS_EXAMPLE_INPUT_SHADERS_DIRECTORY}/template/*.hlsl") diff --git a/common/src/geometry/creator/shaders/gc.basic.fragment.hlsl b/common/src/geometry/creator/shaders/gc.basic.fragment.hlsl index 3dc9b9f1d..fd5cd4d34 100644 --- a/common/src/geometry/creator/shaders/gc.basic.fragment.hlsl +++ b/common/src/geometry/creator/shaders/gc.basic.fragment.hlsl @@ -3,4 +3,4 @@ float4 PSMain(PSInput input) : SV_Target0 { return input.color; -} \ No newline at end of file +} diff --git a/common/src/geometry/creator/shaders/gc.basic.vertex.hlsl b/common/src/geometry/creator/shaders/gc.basic.vertex.hlsl index 1afd468d9..4ca66e4ab 100644 --- a/common/src/geometry/creator/shaders/gc.basic.vertex.hlsl +++ b/common/src/geometry/creator/shaders/gc.basic.vertex.hlsl @@ -1,6 +1,9 @@ -#include "template/gc.basic.vertex.input.hlsl" -#include "template/gc.vertex.hlsl" +struct VSInput +{ + [[vk::location(0)]] float3 position : POSITION; + [[vk::location(1)]] float4 color : COLOR; + [[vk::location(2)]] float2 uv : TEXCOORD; + [[vk::location(3)]] float3 normal : NORMAL; +}; -/* - do not remove this text, WAVE is so bad that you can get errors if no proper ending xD -*/ +#include "template/gc.vertex.hlsl" diff --git a/common/src/geometry/creator/shaders/gc.cone.vertex.hlsl b/common/src/geometry/creator/shaders/gc.cone.vertex.hlsl index ee0c42431..0552842a5 100644 --- a/common/src/geometry/creator/shaders/gc.cone.vertex.hlsl +++ b/common/src/geometry/creator/shaders/gc.cone.vertex.hlsl @@ -1,6 +1,8 @@ -#include "template/gc.cone.vertex.input.hlsl" -#include "template/gc.vertex.hlsl" +struct VSInput +{ + [[vk::location(0)]] float3 position : POSITION; + [[vk::location(1)]] float4 color : COLOR; + [[vk::location(2)]] float3 normal : NORMAL; +}; -/* - do not remove this text, WAVE is so bad that you can get errors if no proper ending xD -*/ +#include "template/gc.vertex.hlsl" diff --git a/common/src/geometry/creator/shaders/gc.ico.vertex.hlsl b/common/src/geometry/creator/shaders/gc.ico.vertex.hlsl index d63fdc809..917bd2677 100644 --- a/common/src/geometry/creator/shaders/gc.ico.vertex.hlsl +++ b/common/src/geometry/creator/shaders/gc.ico.vertex.hlsl @@ -1,6 +1,8 @@ -#include "template/gc.ico.vertex.input.hlsl" -#include "template/gc.vertex.hlsl" +struct VSInput +{ + [[vk::location(0)]] float3 position : POSITION; + [[vk::location(1)]] float3 normal : NORMAL; + [[vk::location(2)]] float2 uv : TEXCOORD; +}; -/* - do not remove this text, WAVE is so bad that you can get errors if no proper ending xD -*/ +#include "template/gc.vertex.hlsl" diff --git a/common/src/geometry/creator/shaders/grid.fragment.hlsl b/common/src/geometry/creator/shaders/grid.fragment.hlsl deleted file mode 100644 index 4b4c1e691..000000000 --- a/common/src/geometry/creator/shaders/grid.fragment.hlsl +++ /dev/null @@ -1,12 +0,0 @@ -#include "template/grid.common.hlsl" - -float4 PSMain(PSInput input) : SV_Target0 -{ - float2 uv = (input.uv - float2(0.5, 0.5)) + 0.5 / 30.0; - float grid = gridTextureGradBox(uv, ddx(input.uv), ddy(input.uv)); - float4 fragColor = float4(1.0 - grid, 1.0 - grid, 1.0 - grid, 1.0); - fragColor *= 0.25; - fragColor *= 0.3 + 0.6 * smoothstep(0.0, 0.1, 1.0 - length(input.uv) / 5.5); - - return fragColor; -} \ No newline at end of file diff --git a/common/src/geometry/creator/shaders/grid.vertex.hlsl b/common/src/geometry/creator/shaders/grid.vertex.hlsl deleted file mode 100644 index 167b981d3..000000000 --- a/common/src/geometry/creator/shaders/grid.vertex.hlsl +++ /dev/null @@ -1,17 +0,0 @@ -#include "template/grid.common.hlsl" - -// set 1, binding 0 -[[vk::binding(0, 1)]] -cbuffer CameraData -{ - SBasicViewParameters params; -}; - -PSInput VSMain(VSInput input) -{ - PSInput output; - output.position = mul(params.MVP, float4(input.position, 1.0)); - output.uv = (input.uv - float2(0.5, 0.5)) * abs(input.position.xy); - - return output; -} \ No newline at end of file diff --git a/common/src/geometry/creator/shaders/template/gc.basic.vertex.input.hlsl b/common/src/geometry/creator/shaders/template/gc.basic.vertex.input.hlsl deleted file mode 100644 index d9e2fa172..000000000 --- a/common/src/geometry/creator/shaders/template/gc.basic.vertex.input.hlsl +++ /dev/null @@ -1,16 +0,0 @@ -#ifndef _THIS_EXAMPLE_GC_BASIC_VERTEX_INPUT_HLSL_ -#define _THIS_EXAMPLE_GC_BASIC_VERTEX_INPUT_HLSL_ - -struct VSInput -{ - [[vk::location(0)]] float3 position : POSITION; - [[vk::location(1)]] float4 color : COLOR; - [[vk::location(2)]] float2 uv : TEXCOORD; - [[vk::location(3)]] float3 normal : NORMAL; -}; - -#endif // _THIS_EXAMPLE_GC_BASIC_VERTEX_INPUT_HLSL_ - -/* - do not remove this text, WAVE is so bad that you can get errors if no proper ending xD -*/ diff --git a/common/src/geometry/creator/shaders/template/gc.common.hlsl b/common/src/geometry/creator/shaders/template/gc.common.hlsl index 4590cd4a3..d0f9aa116 100644 --- a/common/src/geometry/creator/shaders/template/gc.common.hlsl +++ b/common/src/geometry/creator/shaders/template/gc.common.hlsl @@ -12,7 +12,3 @@ #include "SBasicViewParameters.hlsl" #endif // _THIS_EXAMPLE_GC_COMMON_HLSL_ - -/* - do not remove this text, WAVE is so bad that you can get errors if no proper ending xD -*/ \ No newline at end of file diff --git a/common/src/geometry/creator/shaders/template/gc.cone.vertex.input.hlsl b/common/src/geometry/creator/shaders/template/gc.cone.vertex.input.hlsl deleted file mode 100644 index 66221fef1..000000000 --- a/common/src/geometry/creator/shaders/template/gc.cone.vertex.input.hlsl +++ /dev/null @@ -1,15 +0,0 @@ -#ifndef _THIS_EXAMPLE_GC_CONE_VERTEX_INPUT_HLSL_ -#define _THIS_EXAMPLE_GC_CONE_VERTEX_INPUT_HLSL_ - -struct VSInput -{ - [[vk::location(0)]] float3 position : POSITION; - [[vk::location(1)]] float4 color : COLOR; - [[vk::location(2)]] float3 normal : NORMAL; -}; - -#endif // _THIS_EXAMPLE_GC_CONE_VERTEX_INPUT_HLSL_ - -/* - do not remove this text, WAVE is so bad that you can get errors if no proper ending xD -*/ diff --git a/common/src/geometry/creator/shaders/template/gc.ico.vertex.input.hlsl b/common/src/geometry/creator/shaders/template/gc.ico.vertex.input.hlsl deleted file mode 100644 index 6b85486d9..000000000 --- a/common/src/geometry/creator/shaders/template/gc.ico.vertex.input.hlsl +++ /dev/null @@ -1,15 +0,0 @@ -#ifndef _THIS_EXAMPLE_GC_ICO_VERTEX_INPUT_HLSL_ -#define _THIS_EXAMPLE_GC_ICO_VERTEX_INPUT_HLSL_ - -struct VSInput -{ - [[vk::location(0)]] float3 position : POSITION; - [[vk::location(1)]] float3 normal : NORMAL; - [[vk::location(2)]] float2 uv : TEXCOORD; -}; - -#endif // _THIS_EXAMPLE_GC_ICO_VERTEX_INPUT_HLSL_ - -/* - do not remove this text, WAVE is so bad that you can get errors if no proper ending xD -*/ diff --git a/common/src/geometry/creator/shaders/template/gc.vertex.hlsl b/common/src/geometry/creator/shaders/template/gc.vertex.hlsl index 5a8f26722..783421613 100644 --- a/common/src/geometry/creator/shaders/template/gc.vertex.hlsl +++ b/common/src/geometry/creator/shaders/template/gc.vertex.hlsl @@ -16,7 +16,3 @@ PSInput VSMain(VSInput input) return output; } - -/* - do not remove this text, WAVE is so bad that you can get errors if no proper ending xD -*/ diff --git a/common/src/geometry/creator/shaders/template/grid.common.hlsl b/common/src/geometry/creator/shaders/template/grid.common.hlsl deleted file mode 100644 index bc6516600..000000000 --- a/common/src/geometry/creator/shaders/template/grid.common.hlsl +++ /dev/null @@ -1,40 +0,0 @@ -#ifndef _THIS_EXAMPLE_GRID_COMMON_HLSL_ -#define _THIS_EXAMPLE_GRID_COMMON_HLSL_ - -#ifdef __HLSL_VERSION - struct VSInput - { - [[vk::location(0)]] float3 position : POSITION; - [[vk::location(1)]] float4 color : COLOR; - [[vk::location(2)]] float2 uv : TEXCOORD; - [[vk::location(3)]] float3 normal : NORMAL; - }; - - struct PSInput - { - float4 position : SV_Position; - float2 uv : TEXCOORD0; - }; - - float gridTextureGradBox(float2 p, float2 ddx, float2 ddy) - { - float N = 30.0; // grid ratio - float2 w = max(abs(ddx), abs(ddy)) + 0.01; // filter kernel - - // analytic (box) filtering - float2 a = p + 0.5 * w; - float2 b = p - 0.5 * w; - float2 i = (floor(a) + min(frac(a) * N, 1.0) - floor(b) - min(frac(b) * N, 1.0)) / (N * w); - - // pattern - return (1.0 - i.x) * (1.0 - i.y); - } -#endif // __HLSL_VERSION - -#include "SBasicViewParameters.hlsl" - -#endif // _THIS_EXAMPLE_GRID_COMMON_HLSL_ - -/* - do not remove this text, WAVE is so bad that you can get errors if no proper ending xD -*/ \ No newline at end of file From b8e179b804e505bf85e32321954346f8c7c99842 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Fri, 15 Nov 2024 13:35:21 +0100 Subject: [PATCH 035/205] update the UI example to use TRI buffer & smooth resize surface -> I need better control over my window because I want to spam multiple view-ports in GUIs --- 08_HelloSwapchain/main.cpp | 1 + 61_UI/main.cpp | 1334 ++++++++++++++++++++++-------------- 2 files changed, 838 insertions(+), 497 deletions(-) diff --git a/08_HelloSwapchain/main.cpp b/08_HelloSwapchain/main.cpp index 9137fe77a..6c5e3ea82 100644 --- a/08_HelloSwapchain/main.cpp +++ b/08_HelloSwapchain/main.cpp @@ -160,6 +160,7 @@ class HelloSwapchainApp final : public examples::SimpleWindowedApplication auto window = m_winMgr->createWindow(std::move(params)); // uncomment for some nasty testing of swapchain creation! //m_winMgr->minimize(window.get()); + const_cast&>(m_surface) = CSmoothResizeSurface::create(CSurfaceVulkanWin32::create(smart_refctd_ptr(m_api),move_and_static_cast(window))); } return {{m_surface->getSurface()/*,EQF_NONE*/}}; diff --git a/61_UI/main.cpp b/61_UI/main.cpp index 50becd42f..0725bc98f 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -12,8 +12,168 @@ using camera_t = CFPSCamera; using controller_t = CCameraController; using projection_t = IProjection>; // TODO: temporary -> projections will own/reference cameras -// 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 IGPUImage::SSubresourceRange TripleBufferUsedSubresourceRange = +{ + .aspectMask = IGPUImage::EAF_COLOR_BIT, + .baseMipLevel = 0, + .levelCount = 1, + .baseArrayLayer = 0, + .layerCount = 1 +}; + +class CUIEventCallback : public nbl::video::ISmoothResizeSurface::ICallback // I cannot use common CEventCallback because I MUST inherit this callback in order to use smooth resize surface with window callback (for my input events) +{ +public: + CUIEventCallback(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)) {} + CUIEventCallback() {} + + void setLogger(nbl::system::logger_opt_smart_ptr& logger) + { + m_logger = logger; + } + void setInputSystem(nbl::core::smart_refctd_ptr&& m_inputSystem) + { + m_inputSystem = std::move(m_inputSystem); + } +private: + + void onMouseConnected_impl(nbl::core::smart_refctd_ptr&& mch) override + { + m_logger.log("A mouse %p has been connected", nbl::system::ILogger::ELL_INFO, mch.get()); + m_inputSystem.get()->add(m_inputSystem.get()->m_mouse, std::move(mch)); + } + void onMouseDisconnected_impl(nbl::ui::IMouseEventChannel* mch) override + { + m_logger.log("A mouse %p has been disconnected", nbl::system::ILogger::ELL_INFO, mch); + m_inputSystem.get()->remove(m_inputSystem.get()->m_mouse, mch); + } + void onKeyboardConnected_impl(nbl::core::smart_refctd_ptr&& kbch) override + { + m_logger.log("A keyboard %p has been connected", nbl::system::ILogger::ELL_INFO, kbch.get()); + m_inputSystem.get()->add(m_inputSystem.get()->m_keyboard, std::move(kbch)); + } + void onKeyboardDisconnected_impl(nbl::ui::IKeyboardEventChannel* kbch) override + { + m_logger.log("A keyboard %p has been disconnected", nbl::system::ILogger::ELL_INFO, kbch); + m_inputSystem.get()->remove(m_inputSystem.get()->m_keyboard, kbch); + } + +private: + nbl::core::smart_refctd_ptr m_inputSystem = nullptr; + nbl::system::logger_opt_smart_ptr m_logger = nullptr; +}; + +class CSwapchainResources final : public ISmoothResizeSurface::ISwapchainResources +{ +public: + // Because we blit to the swapchain image asynchronously, we need a queue which can not only present but also perform graphics commands. + // If we for example used a compute shader to tonemap and MSAA resolve, we'd request the COMPUTE_BIT here. + constexpr static inline IQueue::FAMILY_FLAGS RequiredQueueFlags = IQueue::FAMILY_FLAGS::GRAPHICS_BIT; + +protected: + // We can return `BLIT_BIT` here, because the Source Image will be already in the correct layout to be used for the present + 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); + + // Ownership of the Source Blit Image, not the Swapchain Image + const bool needToAcquireSrcOwnership = qFamToAcquireSrcFrom != IQueue::FamilyIgnored; + // Should never get asked to transfer ownership if the source is concurrent sharing + assert(!source.image->getCachedCreationParams().isConcurrentSharing() || !needToAcquireSrcOwnership); + + const auto blitDstLayout = IGPUImage::LAYOUT::TRANSFER_DST_OPTIMAL; + IGPUCommandBuffer::SPipelineBarrierDependencyInfo depInfo = {}; + + // barrier before to transition the swapchain image layout + using image_barrier_t = decltype(depInfo.imgBarriers)::element_type; + const image_barrier_t preBarriers[2] = { + { + .barrier = { + .dep = { + .srcStageMask = asset::PIPELINE_STAGE_FLAGS::NONE, // acquire isn't a stage + .srcAccessMask = asset::ACCESS_FLAGS::NONE, // performs no accesses + .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, // I do not care about previous contents of the swapchain + .newLayout = blitDstLayout + }, + { + .barrier = { + .dep = { + // when acquiring ownership the source access masks don't matter + .srcStageMask = asset::PIPELINE_STAGE_FLAGS::NONE, + // Acquire must Happen-Before Semaphore wait, but neither has a true stage so NONE here + // https://github.com/KhronosGroup/Vulkan-Docs/issues/2319 + // If no ownership acquire needed then this dep info won't be used at all + .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 + // no layout transition, already in the correct layout for the blit + } + }; + // We only barrier the source image if we need to acquire ownership, otherwise thanks to Timeline Semaphores all sync is good + depInfo.imgBarriers = { preBarriers,needToAcquireSrcOwnership ? 2ull : 1ull }; + success &= cmdbuf->pipelineBarrier(asset::EDF_NONE, depInfo); + + // TODO: Implement scaling modes other than a plain STRETCH, and allow for using subrectangles of the initial contents + { + 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); + } + + // Barrier after, note that I don't care about preserving the contents of the Triple Buffer when the Render queue starts writing to it again. + // Therefore no ownership release, and no layout transition. + const image_barrier_t postBarrier[1] = { + { + .barrier = { + // When transitioning the image to VK_IMAGE_LAYOUT_SHARED_PRESENT_KHR or VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, there is no need to delay subsequent processing, + // or perform any visibility operations (as vkQueuePresentKHR performs automatic visibility operations). + // To achieve this, the dstAccessMask member of the VkImageMemoryBarrier should be set to 0, and the dstStageMask parameter should be set to VK_PIPELINE_STAGE_2_NONE + .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; + } +}; /* Renders scene texture to an offline @@ -27,132 +187,225 @@ constexpr static inline uint32_t MaxFramesInFlight = 3u; class UISampleApp final : public examples::SimpleWindowedApplication { - using device_base_t = examples::SimpleWindowedApplication; + using base_t = examples::SimpleWindowedApplication; using clock_t = std::chrono::steady_clock; - _NBL_STATIC_INLINE_CONSTEXPR uint32_t WIN_W = 1280, WIN_H = 720; + //_NBL_STATIC_INLINE_CONSTEXPR uint32_t WIN_W = 1280, WIN_H = 720; constexpr static inline clock_t::duration DisplayImageDuration = std::chrono::milliseconds(900); public: + using base_t::base_t; + inline UISampleApp(const path& _localInputCWD, const path& _localOutputCWD, const path& _sharedInputCWD, const path& _sharedOutputCWD) : IApplicationFramework(_localInputCWD, _localOutputCWD, _sharedInputCWD, _sharedOutputCWD) {} - inline core::vector getSurfaces() const override + // Will get called mid-initialization, via `filterDevices` between when the API Connection is created and Physical Device is chosen + core::vector getSurfaces() const override { + // So let's create our Window and Surface then! if (!m_surface) { { - auto windowCallback = core::make_smart_refctd_ptr(smart_refctd_ptr(m_inputSystem), smart_refctd_ptr(m_logger)); + const auto dpyInfo = m_winMgr->getPrimaryDisplayInfo(); + auto windowCallback = core::make_smart_refctd_ptr(smart_refctd_ptr(m_inputSystem), smart_refctd_ptr(m_logger)); + IWindow::SCreationParams params = {}; - params.callback = core::make_smart_refctd_ptr(); - params.width = WIN_W; - params.height = WIN_H; + params.callback = core::make_smart_refctd_ptr(); + params.width = dpyInfo.resX; + params.height = dpyInfo.resY; params.x = 32; params.y = 32; - params.flags = ui::IWindow::ECF_HIDDEN | IWindow::ECF_BORDERLESS | IWindow::ECF_MAXIMIZED; - params.windowCaption = "UISampleApp"; + params.flags = IWindow::ECF_INPUT_FOCUS | IWindow::ECF_CAN_RESIZE | IWindow::ECF_CAN_MAXIMIZE | IWindow::ECF_CAN_MINIMIZE; + params.windowCaption = "[Nabla Engine] UI App"; params.callback = windowCallback; + 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) = nbl::video::CSimpleResizeSurface::create(std::move(surface)); + const_cast&>(m_surface) = CSmoothResizeSurface::create(std::move(surface)); } if (m_surface) + { + m_window->getManager()->maximize(m_window.get()); return { {m_surface->getSurface()/*,EQF_NONE*/} }; - + } + return {}; } inline bool onAppInitialized(smart_refctd_ptr&& system) override { + // Create imput system m_inputSystem = make_smart_refctd_ptr(logger_opt_smart_ptr(smart_refctd_ptr(m_logger))); - if (!device_base_t::onAppInitialized(smart_refctd_ptr(system))) + // Remember to call the base class initialization! + if (!base_t::onAppInitialized(std::move(system))) return false; + // Create asset manager m_assetManager = make_smart_refctd_ptr(smart_refctd_ptr(m_system)); - auto* geometry = m_assetManager->getGeometryCreator(); + // First create the resources that don't depend on a swapchain m_semaphore = m_device->createSemaphore(m_realFrameIx); if (!m_semaphore) return logFail("Failed to Create a Semaphore!"); - ISwapchain::SCreationParams swapchainParams = { .surface = m_surface->getSurface() }; - if (!swapchainParams.deduceFormat(m_physicalDevice)) - return logFail("Could not choose a Surface Format for the Swapchain!"); + // The nice thing about having a triple buffer is that you don't need to do acrobatics to account for the formats available to the surface. + // You can transcode to the swapchain's format while copying, and I actually recommend to do surface rotation, tonemapping and OETF application there. + const auto format = asset::EF_R8G8B8A8_SRGB; + // Could be more clever and use the copy Triple Buffer to Swapchain as an opportunity to do a MSAA resolve or something + const auto samples = IGPUImage::ESCF_1_BIT; - const static IGPURenderpass::SCreationParams::SSubpassDependency dependencies[] = + // Create the renderpass { - { - .srcSubpass = IGPURenderpass::SCreationParams::SSubpassDependency::External, - .dstSubpass = 0, - .memoryBarrier = + 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, // because we clear we don't care about contents when we grab the triple buffer img again + /*.finalLayout = */IGPUImage::LAYOUT::TRANSFER_SRC_OPTIMAL // put it already in the correct layout for the blit operation + }}, + IGPURenderpass::SCreationParams::ColorAttachmentsEnd + }; + IGPURenderpass::SCreationParams::SSubpassDescription subpasses[] = { + {}, + IGPURenderpass::SCreationParams::SubpassesEnd + }; + subpasses[0].colorAttachments[0] = { .render = {.attachmentIndex = 0,.layout = IGPUImage::LAYOUT::ATTACHMENT_OPTIMAL} }; + // We actually need external dependencies to ensure ordering of the Implicit Layout Transitions relative to the semaphore signals + IGPURenderpass::SCreationParams::SSubpassDependency dependencies[] = { + // wipe-transition to ATTACHMENT_OPTIMAL { - .srcStageMask = asset::PIPELINE_STAGE_FLAGS::COPY_BIT, - .srcAccessMask = asset::ACCESS_FLAGS::TRANSFER_WRITE_BIT, + .srcSubpass = IGPURenderpass::SCreationParams::SSubpassDependency::External, + .dstSubpass = 0, + .memoryBarrier = { + // we can have NONE as Sources because the semaphore wait is ALL_COMMANDS + // https://github.com/KhronosGroup/Vulkan-Docs/issues/2319 .dstStageMask = asset::PIPELINE_STAGE_FLAGS::COLOR_ATTACHMENT_OUTPUT_BIT, .dstAccessMask = asset::ACCESS_FLAGS::COLOR_ATTACHMENT_WRITE_BIT } + // leave view offsets and flags default }, - { - .srcSubpass = 0, - .dstSubpass = IGPURenderpass::SCreationParams::SSubpassDependency::External, - .memoryBarrier = + // ATTACHMENT_OPTIMAL to PRESENT_SRC { - .srcStageMask = asset::PIPELINE_STAGE_FLAGS::COLOR_ATTACHMENT_OUTPUT_BIT, - .srcAccessMask = 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 + // we can have NONE as the Destinations because the semaphore signal is ALL_COMMANDS + // https://github.com/KhronosGroup/Vulkan-Docs/issues/2319 + } + // leave view offsets and flags default }, IGPURenderpass::SCreationParams::DependenciesEnd - }; + }; - auto scResources = std::make_unique(m_device.get(), swapchainParams.surfaceFormat.format, dependencies); - auto* renderpass = scResources->getRenderpass(); - - if (!renderpass) - return logFail("Failed to create Renderpass!"); + IGPURenderpass::SCreationParams params = {}; + params.colorAttachments = colorAttachments; + params.subpasses = subpasses; + params.dependencies = dependencies; + m_renderpass = m_device->createRenderpass(params); + if (!m_renderpass) + return logFail("Failed to Create a Renderpass!"); + } - auto gQueue = getGraphicsQueue(); - if (!m_surface || !m_surface->init(gQueue, std::move(scResources), swapchainParams.sharedParams)) - return logFail("Could not create Window & Surface or initialize the Surface!"); + // We just live life in easy mode and have the Swapchain Creation Parameters get deduced from the surface. + // We don't need any control over the format of the swapchain because we'll be only using Renderpasses this time! + // TODO: improve the queue allocation/choice and allocate a dedicated presentation queue to improve responsiveness and race to present. + if (!m_surface || !m_surface->init(m_surface->pickQueue(m_device.get()), std::make_unique(), {})) + return logFail("Failed to Create a Swapchain!"); - m_cmdPool = m_device->createCommandPool(gQueue->getFamilyIndex(), IGPUCommandPool::CREATE_FLAGS::RESET_COMMAND_BUFFER_BIT); - - for (auto i = 0u; i < MaxFramesInFlight; i++) + // Normally you'd want to recreate these images whenever the swapchain is resized in some increment, like 64 pixels or something. + // But I'm super lazy here and will just create "worst case sized images" and waste all the VRAM I can get. + const auto dpyInfo = m_winMgr->getPrimaryDisplayInfo(); + for (auto i = 0; i < MaxFramesInFlight; i++) { - if (!m_cmdPool) - return logFail("Couldn't create Command Pool!"); - if (!m_cmdPool->createCommandBuffers(IGPUCommandPool::BUFFER_LEVEL::PRIMARY, { m_cmdBufs.data() + i, 1 })) - return logFail("Couldn't create Command Buffer!"); + auto& image = m_tripleBuffers[i]; + { + IGPUImage::SCreationParams params = {}; + params = asset::IImage::SCreationParams{ + .type = IGPUImage::ET_2D, + .samples = samples, + .format = format, + .extent = {dpyInfo.resX,dpyInfo.resY,1}, + .mipLevels = 1, + .arrayLayers = 1, + .flags = IGPUImage::ECF_NONE, + // in this example I'll be using a renderpass to clear the image, and then a blit to copy it to the swapchain + .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!"); + + // use dedicated allocations, we have plenty of allocations left, even on Win32 + 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()); + + // create framebuffers for the images + { + auto imageView = m_device->createImageView({ + .flags = IGPUImageView::ECF_NONE, + // give it a Transfer SRC usage flag so we can transition to the Tranfer SRC layout with End Renderpass + .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); + } } - - //pass.scene = CScene::create(smart_refctd_ptr(m_utils), smart_refctd_ptr(m_logger), gQueue, geometry); - pass.scene = CScene::create(smart_refctd_ptr(m_utils), smart_refctd_ptr(m_logger), gQueue, geometry); - nbl::ext::imgui::UI::SCreationParameters params; + // This time we'll create all CommandBuffers from one CommandPool, to keep life simple. However the Pool must support individually resettable CommandBuffers + // because they cannot be pre-recorded because the fraembuffers/swapchain images they use will change when a swapchain recreates. + 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!"); - params.resources.texturesInfo = { .setIx = 0u, .bindingIx = 0u }; - params.resources.samplersInfo = { .setIx = 0u, .bindingIx = 1u }; - params.assetManager = m_assetManager; - params.pipelineCache = nullptr; - params.pipelineLayout = nbl::ext::imgui::UI::createDefaultPipelineLayout(m_utils->getLogicalDevice(), params.resources.texturesInfo, params.resources.samplersInfo, TexturesAmount); - params.renderpass = smart_refctd_ptr(renderpass); - params.streamingBuffer = nullptr; - params.subpassIx = 0u; - params.transfer = getTransferUpQueue(); - params.utilities = m_utils; + // UI { - pass.ui.manager = nbl::ext::imgui::UI::create(std::move(params)); + { + nbl::ext::imgui::UI::SCreationParameters params; + params.resources.texturesInfo = { .setIx = 0u, .bindingIx = 0u }; + params.resources.samplersInfo = { .setIx = 0u, .bindingIx = 1u }; + params.assetManager = m_assetManager; + params.pipelineCache = nullptr; + params.pipelineLayout = nbl::ext::imgui::UI::createDefaultPipelineLayout(m_utils->getLogicalDevice(), params.resources.texturesInfo, params.resources.samplersInfo, TexturesAmount); + params.renderpass = smart_refctd_ptr(m_renderpass); + params.streamingBuffer = nullptr; + params.subpassIx = 0u; + params.transfer = getTransferUpQueue(); + params.utilities = m_utils; + + pass.ui.manager = nbl::ext::imgui::UI::create(std::move(params)); + } if (!pass.ui.manager) return false; // note that we use default layout provided by our extension, but you are free to create your own by filling nbl::ext::imgui::UI::S_CREATION_PARAMETERS::resources const auto* descriptorSetLayout = pass.ui.manager->getPipeline()->getLayout()->getDescriptorSetLayout(0u); - const auto& params = pass.ui.manager->getCreationParameters(); IDescriptorPool::SCreateInfo descriptorPoolInfo = {}; descriptorPoolInfo.maxDescriptorCount[static_cast(asset::IDescriptor::E_TYPE::ET_SAMPLER)] = (uint32_t)nbl::ext::imgui::UI::DefaultSamplerIx::COUNT; @@ -165,334 +418,16 @@ class UISampleApp final : public examples::SimpleWindowedApplication m_descriptorSetPool->createDescriptorSets(1u, &descriptorSetLayout, &pass.ui.descriptorSet); assert(pass.ui.descriptorSet); - } - pass.ui.manager->registerListener([this]() -> void - { - ImGuiIO& io = ImGui::GetIO(); - { - if (isPerspective) - { - if (isLH) - projection->setMatrix(buildProjectionMatrixPerspectiveFovLH(glm::radians(fov), io.DisplaySize.x / io.DisplaySize.y, zNear, zFar)); - else - projection->setMatrix(buildProjectionMatrixPerspectiveFovRH(glm::radians(fov), io.DisplaySize.x / io.DisplaySize.y, zNear, zFar)); - } - else - { - float viewHeight = viewWidth * io.DisplaySize.y / io.DisplaySize.x; - - if (isLH) - projection->setMatrix(buildProjectionMatrixOrthoLH(viewWidth, viewHeight, zNear, zFar)); - else - projection->setMatrix(buildProjectionMatrixOrthoRH(viewWidth, viewHeight, zNear, zFar)); - } - } - - 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); - - if (viewDirty || firstFrame) - { - float32_t3 cameraPosition(cosf(camYAngle)* cosf(camXAngle)* transformParams.camDistance, sinf(camXAngle)* transformParams.camDistance, sinf(camYAngle)* cosf(camXAngle)* transformParams.camDistance); - float32_t3 cameraTarget(0.f, 0.f, 0.f); - - // TODO: lets generate events and make it - // happen purely on gimbal manipulation! - - //camera->getGimbal()->setPosition(cameraPosition); - //camera->getGimbal()->setTarget(cameraTarget); - - 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 - { - float32_t4x4 view, projection, model; - } imguizmoM16InOut; - - const auto& projectionMatrix = projection->getMatrix(); - const auto& view = camera->getGimbal().getView(); - - ImGuizmo::SetID(0u); - imguizmoM16InOut.view = transpose(getMatrix3x4As4x4(view.matrix)); - imguizmoM16InOut.projection = transpose(projectionMatrix); - imguizmoM16InOut.model = transpose(getMatrix3x4As4x4(pass.scene->object.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; - EditTransform(&imguizmoM16InOut.view[0][0], &imguizmoM16InOut.projection[0][0], &imguizmoM16InOut.model[0][0], transformParams); - } - - // to Nabla + update camera & model matrices - - // TODO: make it more nicely - const_cast(view.matrix) = float32_t3x4(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) - { - static float32_t3x4 modelView, normal; - static float32_t4x4 modelViewProjection; - - auto& hook = pass.scene->object; - hook.model = float32_t3x4(transpose(imguizmoM16InOut.model)); - { - const auto& references = pass.scene->getResources().objects; - const auto type = static_cast(gcIndex); - - const auto& [gpu, meta] = references[type]; - hook.meta.type = type; - hook.meta.name = meta.name; - } - - auto& ubo = hook.viewParameters; - - modelView = concatenateBFollowedByA(view.matrix, hook.model); - - // TODO - //modelView.getSub3x3InverseTranspose(normal); - - auto concatMatrix = mul(projectionMatrix, getMatrix3x4As4x4(view.matrix)); - modelViewProjection = mul(concatMatrix, getMatrix3x4As4x4(hook.model)); - - memcpy(ubo.MVP, &modelViewProjection[0][0], sizeof(ubo.MVP)); - memcpy(ubo.MV, &modelView[0][0], sizeof(ubo.MV)); - memcpy(ubo.NormalMat, &normal[0][0], sizeof(ubo.NormalMat)); - - // object meta display - { - ImGui::Begin("Object"); - ImGui::Text("type: \"%s\"", hook.meta.name.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(); - }; - - const auto& orientation = camera->getGimbal().getOrthonornalMatrix(); - - addMatrixTable("Object's Model Matrix", "ModelMatrixTable", 3, 4, &pass.scene->object.model[0][0]); - addMatrixTable("Camera's Position", "PositionForwardVec", 1, 3, &camera->getGimbal().getPosition()[0]); - addMatrixTable("Camera's Orientation Quat", "OrientationQuatTable", 1, 4, &camera->getGimbal().getOrientation()[0]); - addMatrixTable("Camera's View Matrix", "ViewMatrixTable", 3, 4, &view.matrix[0][0]); - addMatrixTable("Bound Projection Matrix", "ProjectionMatrixTable", 4, 4, &projectionMatrix[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 = pass.ui.manager->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(); - } - - displayKeyMappingsAndVirtualStates(controller.get()); + pass.ui.manager->registerListener([this]() -> void { imguiListen(); }); + } - ImGui::End(); - } - ); + // Geometry Creator Scene + { + //pass.scene = CScene::create(smart_refctd_ptr(m_utils), smart_refctd_ptr(m_logger), getGraphicsQueue(), m_assetManager->getGeometryCreator()); + pass.scene = CScene::create(smart_refctd_ptr(m_utils), smart_refctd_ptr(m_logger), getGraphicsQueue(), m_assetManager->getGeometryCreator()); + } - m_winMgr->setWindowSize(m_window.get(), WIN_W, WIN_H); - m_surface->recreateSwapchain(); - m_winMgr->show(m_window.get()); oracle.reportBeginFrameRecord(); /* @@ -521,6 +456,9 @@ class UISampleApp final : public examples::SimpleWindowedApplication keys = controller->getCamera()->getImguizmoMappingPreset(); }); + 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; } @@ -558,133 +496,199 @@ class UISampleApp final : public examples::SimpleWindowedApplication // B) Acquire: Can't have more acquires in flight than a certain threshold returned by swapchain or your surface helper class. [MaxAcquiresInFlight] if (m_realFrameIx >= framesInFlight) { - const ISemaphore::SWaitInfo cbDonePending[] = - { + const ISemaphore::SWaitInfo cmdbufDonePending[] = { { .semaphore = m_semaphore.get(), .value = m_realFrameIx + 1 - framesInFlight } }; - if (m_device->blockForSemaphores(cbDonePending) != ISemaphore::WAIT_RESULT::SUCCESS) + if (m_device->blockForSemaphores(cmdbufDonePending) != ISemaphore::WAIT_RESULT::SUCCESS) return; } + // Predict size of next render, and bail if nothing to do + const auto currentSwapchainExtent = m_surface->getCurrentExtent(); + if (currentSwapchainExtent.width * currentSwapchainExtent.height <= 0) + return; + // The extent of the swapchain might change between now and `present` but the blit should adapt nicely + const VkRect2D currentRenderArea = { .offset = {0,0},.extent = currentSwapchainExtent }; + + // You explicitly should not use `getAcquireCount()` see the comment on `m_realFrameIx` const auto resourceIx = m_realFrameIx % MaxFramesInFlight; - // CPU events + // We will be using this command buffer to produce the frame + auto frame = m_tripleBuffers[resourceIx].get(); + auto cmdbuf = m_cmdBufs[resourceIx].get(); + + // update CPU stuff - controllers, events, UI state update(); - // render whole scene to offline frame buffer & submit - pass.scene->begin(); + bool willSubmit = true; { - pass.scene->update(); - pass.scene->record(); - pass.scene->end(); - } - pass.scene->submit(); + // render geometry creator scene to offline frame buffer & submit + // TODO: OK with TRI buffer this thing is retarded now + // (**) <- a note why bellow before submit + pass.scene->begin(); + { + pass.scene->update(); + pass.scene->record(); + pass.scene->end(); + } + pass.scene->submit(); - auto* const cb = m_cmdBufs.data()[resourceIx].get(); - cb->reset(IGPUCommandBuffer::RESET_FLAGS::RELEASE_RESOURCES_BIT); - cb->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT); - cb->beginDebugMarker("UISampleApp IMGUI Frame"); + willSubmit &= cmdbuf->reset(IGPUCommandBuffer::RESET_FLAGS::RELEASE_RESOURCES_BIT); + willSubmit &= cmdbuf->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT); + willSubmit &= cmdbuf->beginDebugMarker("UIApp Frame"); + + const IGPUCommandBuffer::SClearColorValue clearValue = { .float32 = {0.f,0.f,0.f,1.f} }; + const IGPUCommandBuffer::SRenderpassBeginInfo info = { + .framebuffer = m_framebuffers[resourceIx].get(), + .colorClearValues = &clearValue, + .depthStencilClearValues = nullptr, + .renderArea = currentRenderArea + }; - auto* queue = getGraphicsQueue(); + // UI renderpass + willSubmit &= cmdbuf->beginRenderPass(info, IGPUCommandBuffer::SUBPASS_CONTENTS::INLINE); + { + asset::SViewport viewport; + { + viewport.minDepth = 1.f; + viewport.maxDepth = 0.f; + viewport.x = 0u; + viewport.y = 0u; + viewport.width = m_window->getWidth(); + viewport.height = m_window->getHeight(); + } - asset::SViewport viewport; - { - viewport.minDepth = 1.f; - viewport.maxDepth = 0.f; - viewport.x = 0u; - viewport.y = 0u; - viewport.width = m_window->getWidth(); - viewport.height = m_window->getHeight(); - } - cb->setViewport(0u, 1u, &viewport); + willSubmit &= cmdbuf->setViewport(0u, 1u, &viewport); - const VkRect2D currentRenderArea = - { - .offset = {0,0}, - .extent = {m_window->getWidth(),m_window->getHeight()} - }; + const VkRect2D currentRenderArea = + { + .offset = {0,0}, + .extent = {m_window->getWidth(),m_window->getHeight()} + }; - IQueue::SSubmitInfo::SCommandBufferInfo commandBuffersInfo[] = {{.cmdbuf = cb }}; + IQueue::SSubmitInfo::SCommandBufferInfo commandBuffersInfo[] = { {.cmdbuf = cmdbuf } }; - // UI render pass - { - auto scRes = static_cast(m_surface->getSwapchainResources()); - const IGPUCommandBuffer::SRenderpassBeginInfo renderpassInfo = - { - .framebuffer = scRes->getFramebuffer(m_currentImageAcquire.imageIndex), - .colorClearValues = &clear.color, - .depthStencilClearValues = nullptr, - .renderArea = currentRenderArea - }; - nbl::video::ISemaphore::SWaitInfo waitInfo = { .semaphore = m_semaphore.get(), .value = m_realFrameIx + 1u }; + const IGPUCommandBuffer::SRenderpassBeginInfo info = + { + .framebuffer = m_framebuffers[resourceIx].get(), + .colorClearValues = &clear.color, + .depthStencilClearValues = nullptr, + .renderArea = currentRenderArea + }; - cb->beginRenderPass(renderpassInfo, IGPUCommandBuffer::SUBPASS_CONTENTS::INLINE); - const auto uiParams = pass.ui.manager->getCreationParameters(); - auto* pipeline = pass.ui.manager->getPipeline(); - cb->bindGraphicsPipeline(pipeline); - cb->bindDescriptorSets(EPBP_GRAPHICS, pipeline->getLayout(), uiParams.resources.texturesInfo.setIx, 1u, &pass.ui.descriptorSet.get()); // note that we use default UI pipeline layout where uiParams.resources.textures.setIx == uiParams.resources.samplers.setIx - - if (!keepRunning()) - return; - - if (!pass.ui.manager->render(cb,waitInfo)) + nbl::video::ISemaphore::SWaitInfo waitInfo = { .semaphore = m_semaphore.get(), .value = m_realFrameIx + 1u }; + const auto uiParams = pass.ui.manager->getCreationParameters(); + auto* pipeline = pass.ui.manager->getPipeline(); + + cmdbuf->bindGraphicsPipeline(pipeline); + cmdbuf->bindDescriptorSets(EPBP_GRAPHICS, pipeline->getLayout(), uiParams.resources.texturesInfo.setIx, 1u, &pass.ui.descriptorSet.get()); // note that we use default UI pipeline layout where uiParams.resources.textures.setIx == uiParams.resources.samplers.setIx + + if (!keepRunning()) + return; + + willSubmit &= pass.ui.manager->render(cmdbuf, waitInfo); + } + willSubmit &= cmdbuf->endRenderPass(); + + // If the Rendering and Blit/Present Queues don't come from the same family we need to transfer ownership, because we need to preserve contents between them. + auto blitQueueFamily = m_surface->getAssignedQueue()->getFamilyIndex(); + // Also should crash/error if concurrent sharing enabled but would-be-user-queue is not in the share set, but oh well. + const bool needOwnershipRelease = cmdbuf->getQueueFamilyIndex() != blitQueueFamily && !frame->getCachedCreationParams().isConcurrentSharing(); + if (needOwnershipRelease) { - // TODO: need to present acquired image before bailing because its already acquired - return; + const IGPUCommandBuffer::SPipelineBarrierDependencyInfo::image_barrier_t barrier[] = { { + .barrier = { + .dep = { + // Normally I'd put `COLOR_ATTACHMENT` on the masks, but we want this to happen after Layout Transition :( + // https://github.com/KhronosGroup/Vulkan-Docs/issues/2319 + .srcStageMask = asset::PIPELINE_STAGE_FLAGS::ALL_COMMANDS_BITS, + .srcAccessMask = asset::ACCESS_FLAGS::MEMORY_READ_BITS | asset::ACCESS_FLAGS::MEMORY_WRITE_BITS, + // For a Queue Family Ownership Release the destination access masks are irrelevant + // and source stage mask can be NONE as long as the semaphore signals ALL_COMMANDS_BIT + .dstStageMask = asset::PIPELINE_STAGE_FLAGS::NONE, + .dstAccessMask = asset::ACCESS_FLAGS::NONE + }, + .ownershipOp = IGPUCommandBuffer::SOwnershipTransferBarrier::OWNERSHIP_OP::RELEASE, + .otherQueueFamilyIndex = blitQueueFamily + }, + .image = frame, + .subresourceRange = TripleBufferUsedSubresourceRange + // there will be no layout transition, already done by the Renderpass End + } }; + const IGPUCommandBuffer::SPipelineBarrierDependencyInfo depInfo = { .imgBarriers = barrier }; + willSubmit &= cmdbuf->pipelineBarrier(asset::EDF_NONE, depInfo); } - cb->endRenderPass(); } - cb->end(); + willSubmit &= cmdbuf->end(); + + // submit and present under a mutex ASAP + if (willSubmit) { - const IQueue::SSubmitInfo::SSemaphoreInfo rendered[] = - { - { - .semaphore = m_semaphore.get(), - .value = ++m_realFrameIx, - .stageMask = PIPELINE_STAGE_FLAGS::COLOR_ATTACHMENT_OUTPUT_BIT - } + // We will signal a semaphore in the rendering queue, and await it with the presentation/blit queue + const IQueue::SSubmitInfo::SSemaphoreInfo rendered = + { + .semaphore = m_semaphore.get(), + .value = m_realFrameIx + 1, + // Normally I'd put `COLOR_ATTACHMENT` on the masks, but we want to signal after Layout Transitions and optional Ownership Release + // https://github.com/KhronosGroup/Vulkan-Docs/issues/2319 + .stageMask = asset::PIPELINE_STAGE_FLAGS::ALL_COMMANDS_BITS }; - + const IQueue::SSubmitInfo::SCommandBufferInfo cmdbufs[1] = + { { + .cmdbuf = cmdbuf + } }; + // We need to wait on previous triple buffer blits/presents from our source image to complete + auto* pBlitWaitValue = m_blitWaitValues.data() + resourceIx; + auto swapchainLock = m_surface->pseudoAcquire(pBlitWaitValue); + const IQueue::SSubmitInfo::SSemaphoreInfo blitted = + { + .semaphore = m_surface->getPresentSemaphore(), + .value = pBlitWaitValue->load(), + // Normally I'd put `BLIT` on the masks, but we want to wait before Implicit Layout Transitions and optional Implicit Ownership Acquire + // https://github.com/KhronosGroup/Vulkan-Docs/issues/2319 + .stageMask = asset::PIPELINE_STAGE_FLAGS::ALL_COMMANDS_BITS + }; + const IQueue::SSubmitInfo submitInfos[1] = { { - const IQueue::SSubmitInfo::SSemaphoreInfo acquired[] = - { - { - .semaphore = m_currentImageAcquire.semaphore, - .value = m_currentImageAcquire.acquireCount, - .stageMask = PIPELINE_STAGE_FLAGS::NONE - } - }; - - const IQueue::SSubmitInfo infos[] = - { - { - .waitSemaphores = acquired, - .commandBuffers = commandBuffersInfo, - .signalSemaphores = rendered - } - }; - - const nbl::video::ISemaphore::SWaitInfo waitInfos[] = - { { - .semaphore = pass.scene->semaphore.progress.get(), - .value = pass.scene->semaphore.finishedValue - } }; - - m_device->blockForSemaphores(waitInfos); - - updateGUIDescriptorSet(); - - if (queue->submit(infos) != IQueue::RESULT::SUCCESS) - m_realFrameIx--; + .waitSemaphores = {&blitted,1}, + .commandBuffers = cmdbufs, + .signalSemaphores = {&rendered,1} } + }; + + // (**) -> wait on offline framebuffer + { + const nbl::video::ISemaphore::SWaitInfo waitInfos[] = + { { + .semaphore = pass.scene->semaphore.progress.get(), + .value = pass.scene->semaphore.finishedValue + } }; + + m_device->blockForSemaphores(waitInfos); + updateGUIDescriptorSet(); } - m_window->setCaption("[Nabla Engine] UI App Test Demo"); - m_surface->present(m_currentImageAcquire.imageIndex, rendered); + if (getGraphicsQueue()->submit(submitInfos) != IQueue::RESULT::SUCCESS) + return; + + m_realFrameIx++; + + // only present if there's successful content to show + const ISmoothResizeSurface::SPresentInfo presentInfo = { + { + .source = {.image = frame,.rect = currentRenderArea}, + .waitSemaphore = rendered.semaphore, + .waitValue = rendered.value, + .pPresentSemaphoreWaitValue = pBlitWaitValue, + }, + // The Graphics Queue will be the the most recent owner just before it releases ownership + cmdbuf->getQueueFamilyIndex() + }; + m_surface->present(std::move(swapchainLock), presentInfo); } } @@ -698,7 +702,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication inline bool onAppTerminated() override { - return device_base_t::onAppTerminated(); + return base_t::onAppTerminated(); } inline void update() @@ -708,8 +712,6 @@ class UISampleApp final : public examples::SimpleWindowedApplication auto updatePresentationTimestamp = [&]() { - m_currentImageAcquire = m_surface->acquireNextImage(); - oracle.reportEndFrameRecord(); const auto timestamp = oracle.getNextPresentationTimeStamp(); oracle.reportBeginFrameRecord(); @@ -764,21 +766,359 @@ class UISampleApp final : public examples::SimpleWindowedApplication } private: - // 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; + inline void imguiListen() + { + ImGuiIO& io = ImGui::GetIO(); + { + if (isPerspective) + { + if (isLH) + projection->setMatrix(buildProjectionMatrixPerspectiveFovLH(glm::radians(fov), io.DisplaySize.x / io.DisplaySize.y, zNear, zFar)); + else + projection->setMatrix(buildProjectionMatrixPerspectiveFovRH(glm::radians(fov), io.DisplaySize.x / io.DisplaySize.y, zNear, zFar)); + } + else + { + float viewHeight = viewWidth * io.DisplaySize.y / io.DisplaySize.x; + + if (isLH) + projection->setMatrix(buildProjectionMatrixOrthoLH(viewWidth, viewHeight, zNear, zFar)); + else + projection->setMatrix(buildProjectionMatrixOrthoRH(viewWidth, viewHeight, zNear, zFar)); + } + } + + 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); + + if (viewDirty || firstFrame) + { + float32_t3 cameraPosition(cosf(camYAngle) * cosf(camXAngle) * transformParams.camDistance, sinf(camXAngle) * transformParams.camDistance, sinf(camYAngle) * cosf(camXAngle) * transformParams.camDistance); + float32_t3 cameraTarget(0.f, 0.f, 0.f); + + // TODO: lets generate events and make it + // happen purely on gimbal manipulation! + + //camera->getGimbal()->setPosition(cameraPosition); + //camera->getGimbal()->setTarget(cameraTarget); + + 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 + { + float32_t4x4 view, projection, model; + } imguizmoM16InOut; + + const auto& projectionMatrix = projection->getMatrix(); + const auto& view = camera->getGimbal().getView(); + + ImGuizmo::SetID(0u); + imguizmoM16InOut.view = transpose(getMatrix3x4As4x4(view.matrix)); + imguizmoM16InOut.projection = transpose(projectionMatrix); + imguizmoM16InOut.model = transpose(getMatrix3x4As4x4(pass.scene->object.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; + EditTransform(&imguizmoM16InOut.view[0][0], &imguizmoM16InOut.projection[0][0], &imguizmoM16InOut.model[0][0], transformParams); + } + + // to Nabla + update camera & model matrices + + // TODO: make it more nicely + const_cast(view.matrix) = float32_t3x4(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) + { + static float32_t3x4 modelView, normal; + static float32_t4x4 modelViewProjection; + + auto& hook = pass.scene->object; + hook.model = float32_t3x4(transpose(imguizmoM16InOut.model)); + { + const auto& references = pass.scene->getResources().objects; + const auto type = static_cast(gcIndex); + + const auto& [gpu, meta] = references[type]; + hook.meta.type = type; + hook.meta.name = meta.name; + } + + auto& ubo = hook.viewParameters; + + modelView = concatenateBFollowedByA(view.matrix, hook.model); + + // TODO + //modelView.getSub3x3InverseTranspose(normal); + + auto concatMatrix = mul(projectionMatrix, getMatrix3x4As4x4(view.matrix)); + modelViewProjection = mul(concatMatrix, getMatrix3x4As4x4(hook.model)); + + memcpy(ubo.MVP, &modelViewProjection[0][0], sizeof(ubo.MVP)); + memcpy(ubo.MV, &modelView[0][0], sizeof(ubo.MV)); + memcpy(ubo.NormalMat, &normal[0][0], sizeof(ubo.NormalMat)); + + // object meta display + { + ImGui::Begin("Object"); + ImGui::Text("type: \"%s\"", hook.meta.name.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(); + }; + + const auto& orientation = camera->getGimbal().getOrthonornalMatrix(); + + addMatrixTable("Object's Model Matrix", "ModelMatrixTable", 3, 4, &pass.scene->object.model[0][0]); + addMatrixTable("Camera's Position", "PositionForwardVec", 1, 3, &camera->getGimbal().getPosition()[0]); + addMatrixTable("Camera's Orientation Quat", "OrientationQuatTable", 1, 4, &camera->getGimbal().getOrientation()[0]); + addMatrixTable("Camera's View Matrix", "ViewMatrixTable", 3, 4, &view.matrix[0][0]); + addMatrixTable("Bound Projection Matrix", "ProjectionMatrixTable", 4, 4, &projectionMatrix[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 = pass.ui.manager->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(); + } + + displayKeyMappingsAndVirtualStates(controller.get()); + + ImGui::End(); + } + + std::chrono::seconds timeout = std::chrono::seconds(0x7fffFFFFu); + clock_t::time_point start; + + //! One window & surface + smart_refctd_ptr> m_surface; smart_refctd_ptr m_window; - smart_refctd_ptr> m_surface; - smart_refctd_ptr m_pipeline; + // 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; - smart_refctd_ptr m_cmdPool; + // 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; + // 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; - ISimpleManagedSurface::SAcquireResult m_currentImageAcquire = {}; - + // 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 = {}; + // We will use it to get some asset stuff like geometry creator smart_refctd_ptr m_assetManager; + // Input system for capturing system events core::smart_refctd_ptr m_inputSystem; + // Handles mouse events InputSystem::ChannelReader mouse; + // Handles keyboard events InputSystem::ChannelReader keyboard; constexpr static inline auto TexturesAmount = 2u; From 6ae13274089d32509b1c3eac8d1bffe643ab1731 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Fri, 15 Nov 2024 14:15:07 +0100 Subject: [PATCH 036/205] fix some init bugs --- 61_UI/include/common.hpp | 2 +- 61_UI/main.cpp | 2 +- common/include/CGeomtryCreatorScene.hpp | 2 +- common/include/ICamera.hpp | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/61_UI/include/common.hpp b/61_UI/include/common.hpp index b509031cf..653d2fc20 100644 --- a/61_UI/include/common.hpp +++ b/61_UI/include/common.hpp @@ -9,7 +9,7 @@ #include "CFPSCamera.hpp" #include "camera/CCameraController.hpp" #include "SimpleWindowedApplication.hpp" -#include "CEventCallback.hpp" +#include "InputSystem.hpp" // the example's headers #include "transform.hpp" diff --git a/61_UI/main.cpp b/61_UI/main.cpp index 0725bc98f..2e1da13d2 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -435,7 +435,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication */ projection->setMatrix(buildProjectionMatrixPerspectiveFovLH(glm::radians(fov), float(m_window->getWidth()) / float(m_window->getHeight()), zNear, zFar)); - camera = make_smart_refctd_ptr(float32_t3{ -1.958f, 0.697f, 0.881f }, glm::quat(0.092f, 0.851f, -0.159f, 0.492f)); + camera = make_smart_refctd_ptr(float32_t3{ -2.017f, 0.386f, 0.684f }, glm::quat(0.55f, 0.047f, 0.830f, -0.072f)); // order important for quat, the ctor is GLM_FUNC_QUALIFIER GLM_CONSTEXPR qua::qua(T _w, T _x, T _y, T _z) controller = make_smart_refctd_ptr(core::smart_refctd_ptr(camera)); // init keyboard map diff --git a/common/include/CGeomtryCreatorScene.hpp b/common/include/CGeomtryCreatorScene.hpp index 7b6492d11..fdd877efb 100644 --- a/common/include/CGeomtryCreatorScene.hpp +++ b/common/include/CGeomtryCreatorScene.hpp @@ -1098,7 +1098,7 @@ class ResourceBuilder struct ObjectDrawHookCpu { - nbl::hlsl::float32_t3x4 model; + nbl::hlsl::float32_t3x4 model = nbl::hlsl::float32_t3x4(1.f); nbl::asset::SBasicViewParameters viewParameters; ObjectMeta meta; }; diff --git a/common/include/ICamera.hpp b/common/include/ICamera.hpp index 630c7b624..7488a2af6 100644 --- a/common/include/ICamera.hpp +++ b/common/include/ICamera.hpp @@ -34,7 +34,7 @@ class ICamera : public IGimbalManipulateEncoder, virtual public core::IReference public: using base_t = IGimbal; - CGimbal(typename base_t::SCreationParameters&& parameters) : base_t(std::move(parameters)) {} + CGimbal(typename base_t::SCreationParameters&& parameters) : base_t(std::move(parameters)) { updateView(); } ~CGimbal() = default; struct SView From ac0977bc0a58b497fe5bc30fd4360b328254ec9a Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Fri, 15 Nov 2024 14:39:42 +0100 Subject: [PATCH 037/205] play with aspect ratio, make projection work in GUI window on window resize requests - add notes in comments, I will have to keep track for aspects per GUI windows in which I will render scene from active camera perspective bound to the GUI window --- 61_UI/include/transform.hpp | 12 ++++++++--- 61_UI/main.cpp | 41 +++++++++++++++++++++---------------- 2 files changed, 32 insertions(+), 21 deletions(-) diff --git a/61_UI/include/transform.hpp b/61_UI/include/transform.hpp index 88a78f751..e02f0f533 100644 --- a/61_UI/include/transform.hpp +++ b/61_UI/include/transform.hpp @@ -10,11 +10,11 @@ static constexpr inline auto OfflineSceneTextureIx = 1u; struct TransformRequestParams { - bool useWindow = true, editTransformDecomposition = false, enableViewManipulate = false; - float camDistance = 8.f; + bool useWindow = true, editTransformDecomposition = false, enableViewManipulate = false; + float camDistance = 8.f, aspectRatio = {}, invAspectRatio = {}; }; -void EditTransform(float* cameraView, const float* cameraProjection, float* matrix, const TransformRequestParams& params) +void EditTransform(float* cameraView, const float* cameraProjection, float* matrix, TransformRequestParams& params) { static ImGuizmo::OPERATION mCurrentGizmoOperation(ImGuizmo::TRANSLATE); static ImGuizmo::MODE mCurrentGizmoMode(ImGuizmo::LOCAL); @@ -116,6 +116,9 @@ void EditTransform(float* cameraView, const float* cameraProjection, float* matr ImVec2 windowPos = ImGui::GetWindowPos(); ImVec2 cursorPos = ImGui::GetCursorScreenPos(); + params.aspectRatio = contentRegionSize.x / contentRegionSize.y; + params.invAspectRatio = contentRegionSize.y / contentRegionSize.x; + ImGui::Image(info, contentRegionSize); ImGuizmo::SetRect(cursorPos.x, cursorPos.y, contentRegionSize.x, contentRegionSize.y); @@ -140,6 +143,9 @@ void EditTransform(float* cameraView, const float* cameraProjection, float* matr viewManipulateRight = cursorPos.x + contentRegionSize.x; viewManipulateTop = cursorPos.y; + + params.aspectRatio = io.DisplaySize.x / io.DisplaySize.y; + params.invAspectRatio = io.DisplaySize.y / io.DisplaySize.x; } ImGuizmo::Manipulate(cameraView, cameraProjection, mCurrentGizmoOperation, mCurrentGizmoMode, matrix, NULL, useSnap ? &snap[0] : NULL, boundSizing ? bounds : NULL, boundSizingSnap ? boundsSnap : NULL); diff --git a/61_UI/main.cpp b/61_UI/main.cpp index 2e1da13d2..b828a84b0 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -434,7 +434,9 @@ class UISampleApp final : public examples::SimpleWindowedApplication TESTS, TODO: remove all once finished work & integrate with the example properly */ - projection->setMatrix(buildProjectionMatrixPerspectiveFovLH(glm::radians(fov), float(m_window->getWidth()) / float(m_window->getHeight()), zNear, zFar)); + transformParams.aspectRatio = float(m_window->getWidth()) / float(m_window->getHeight()); + transformParams.invAspectRatio = float(m_window->getHeight()) / float(m_window->getWidth()); + camera = make_smart_refctd_ptr(float32_t3{ -2.017f, 0.386f, 0.684f }, glm::quat(0.55f, 0.047f, 0.830f, -0.072f)); // order important for quat, the ctor is GLM_FUNC_QUALIFIER GLM_CONSTEXPR qua::qua(T _w, T _x, T _y, T _z) controller = make_smart_refctd_ptr(core::smart_refctd_ptr(camera)); @@ -769,17 +771,34 @@ class UISampleApp final : public examples::SimpleWindowedApplication inline void imguiListen() { ImGuiIO& io = ImGui::GetIO(); + + 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; + + // TODO: I need this logic per viewport we will render a scene from a point of view of its bound camera { + if (isPerspective) { if (isLH) - projection->setMatrix(buildProjectionMatrixPerspectiveFovLH(glm::radians(fov), io.DisplaySize.x / io.DisplaySize.y, zNear, zFar)); + projection->setMatrix(buildProjectionMatrixPerspectiveFovLH(glm::radians(fov), transformParams.aspectRatio, zNear, zFar)); else - projection->setMatrix(buildProjectionMatrixPerspectiveFovRH(glm::radians(fov), io.DisplaySize.x / io.DisplaySize.y, zNear, zFar)); + projection->setMatrix(buildProjectionMatrixPerspectiveFovRH(glm::radians(fov), transformParams.aspectRatio, zNear, zFar)); } else { - float viewHeight = viewWidth * io.DisplaySize.y / io.DisplaySize.x; + float viewHeight = viewWidth * transformParams.invAspectRatio; if (isLH) projection->setMatrix(buildProjectionMatrixOrthoLH(viewWidth, viewHeight, zNear, zFar)); @@ -788,20 +807,6 @@ class UISampleApp final : public examples::SimpleWindowedApplication } } - 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)) From cd26e13720ef8083558c6c153b5feca08e21bf45 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Mon, 18 Nov 2024 14:55:24 +0100 Subject: [PATCH 038/205] Make room for multiple geometry creator scene frame-buffers, add second camera to the example, do a few clean-ups. Mark TODOs, time to test imguizmo controller but first I need to pull latest imguizmo --- 61_UI/include/common.hpp | 6 +- 61_UI/include/keysmapping.hpp | 22 +- 61_UI/include/transform.hpp | 160 --- 61_UI/main.cpp | 318 ++++- common/include/CGeomtryCreatorScene.hpp | 1371 ++++++------------- common/include/ICamera.hpp | 4 +- common/include/camera/CCameraController.hpp | 90 -- common/include/camera/IGimbalController.hpp | 2 +- 8 files changed, 723 insertions(+), 1250 deletions(-) delete mode 100644 61_UI/include/transform.hpp delete mode 100644 common/include/camera/CCameraController.hpp diff --git a/61_UI/include/common.hpp b/61_UI/include/common.hpp index 653d2fc20..c5a1860da 100644 --- a/61_UI/include/common.hpp +++ b/61_UI/include/common.hpp @@ -7,12 +7,14 @@ // common api #include "CFPSCamera.hpp" -#include "camera/CCameraController.hpp" #include "SimpleWindowedApplication.hpp" #include "InputSystem.hpp" // the example's headers -#include "transform.hpp" +#include "nbl/ui/ICursorControl.h" +#include "nbl/ext/ImGui/ImGui.h" +#include "imgui/imgui_internal.h" +#include "imguizmo/ImGuizmo.h" #include "CGeomtryCreatorScene.hpp" using namespace nbl; diff --git a/61_UI/include/keysmapping.hpp b/61_UI/include/keysmapping.hpp index 25bba3e68..69872c4ca 100644 --- a/61_UI/include/keysmapping.hpp +++ b/61_UI/include/keysmapping.hpp @@ -2,10 +2,10 @@ #define __NBL_KEYSMAPPING_H_INCLUDED__ #include "common.hpp" -#include "camera/CCameraController.hpp" +#include "ICamera.hpp" template -void handleAddMapping(const char* tableID, CCameraController* controller, IGimbalManipulateEncoder::EncoderType activeController, CVirtualGimbalEvent::VirtualEventType& selectedEventType, ui::E_KEY_CODE& newKey, ui::E_MOUSE_CODE& newMouseCode, bool& addMode) +void handleAddMapping(const char* tableID, ICamera* camera, IGimbalManipulateEncoder::EncoderType activeController, CVirtualGimbalEvent::VirtualEventType& selectedEventType, ui::E_KEY_CODE& newKey, ui::E_MOUSE_CODE& newMouseCode, bool& addMode) { ImGui::BeginTable(tableID, 3, ImGuiTableFlags_Borders | ImGuiTableFlags_Resizable | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchSame); ImGui::TableSetupColumn("Virtual Event", ImGuiTableColumnFlags_WidthStretch, 0.33f); @@ -67,9 +67,9 @@ void handleAddMapping(const char* tableID, CCameraController* controller, IGi if (ImGui::Button("Confirm Add", ImVec2(100, 30))) { if (activeController == IGimbalManipulateEncoder::Keyboard) - controller->updateKeyboardMapping([&](auto& keys) { keys[newKey] = selectedEventType; }); + camera->updateKeyboardMapping([&](auto& keys) { keys[newKey] = selectedEventType; }); else - controller->updateMouseMapping([&](auto& mouse) { mouse[newMouseCode] = selectedEventType; }); + camera->updateMouseMapping([&](auto& mouse) { mouse[newMouseCode] = selectedEventType; }); addMode = false; } @@ -77,7 +77,7 @@ void handleAddMapping(const char* tableID, CCameraController* controller, IGi } template -void displayKeyMappingsAndVirtualStates(CCameraController* controller) +void displayKeyMappingsAndVirtualStates(ICamera* camera) { static bool addMode = false, pendingChanges = false; static CVirtualGimbalEvent::VirtualEventType selectedEventType = CVirtualGimbalEvent::VirtualEventType::MoveForward; @@ -85,8 +85,8 @@ void displayKeyMappingsAndVirtualStates(CCameraController* controller) static ui::E_MOUSE_CODE newMouseCode = ui::EMC_LEFT_BUTTON; static IGimbalManipulateEncoder::EncoderType activeController = IGimbalManipulateEncoder::Keyboard; - const auto& keyboardMappings = controller->getKeyboardVirtualEventMap(); - const auto& mouseMappings = controller->getMouseVirtualEventMap(); + const auto& keyboardMappings = camera->getKeyboardVirtualEventMap(); + const auto& mouseMappings = camera->getMouseVirtualEventMap(); ImGui::SetNextWindowSize(ImVec2(600, 400), ImGuiCond_FirstUseEver); ImGui::SetNextWindowSizeConstraints(ImVec2(600, 400), ImVec2(600, 69000)); @@ -141,7 +141,7 @@ void displayKeyMappingsAndVirtualStates(CCameraController* controller) ImGui::TableSetColumnIndex(4); if (ImGui::Button(("Delete##deleteKey" + std::to_string(static_cast(hash.event.type))).c_str())) { - controller->updateKeyboardMapping([&](auto& keys) { keys.erase(keyboardCode); }); + camera->updateKeyboardMapping([&](auto& keys) { keys.erase(keyboardCode); }); pendingChanges = true; break; } @@ -151,7 +151,7 @@ void displayKeyMappingsAndVirtualStates(CCameraController* controller) if (addMode) { ImGui::Separator(); - handleAddMapping("AddKeyboardMappingTable", controller, activeController, selectedEventType, newKey, newMouseCode, addMode); + handleAddMapping("AddKeyboardMappingTable", camera, activeController, selectedEventType, newKey, newMouseCode, addMode); } ImGui::EndTabItem(); @@ -203,7 +203,7 @@ void displayKeyMappingsAndVirtualStates(CCameraController* controller) ImGui::TableSetColumnIndex(4); if (ImGui::Button(("Delete##deleteMouse" + std::to_string(static_cast(hash.event.type))).c_str())) { - controller->updateMouseMapping([&](auto& mouse) { mouse.erase(mouseCode); }); + camera->updateMouseMapping([&](auto& mouse) { mouse.erase(mouseCode); }); pendingChanges = true; break; } @@ -213,7 +213,7 @@ void displayKeyMappingsAndVirtualStates(CCameraController* controller) if (addMode) { ImGui::Separator(); - handleAddMapping("AddMouseMappingTable", controller, activeController, selectedEventType, newKey, newMouseCode, addMode); + handleAddMapping("AddMouseMappingTable", camera, activeController, selectedEventType, newKey, newMouseCode, addMode); } ImGui::EndTabItem(); } diff --git a/61_UI/include/transform.hpp b/61_UI/include/transform.hpp deleted file mode 100644 index e02f0f533..000000000 --- a/61_UI/include/transform.hpp +++ /dev/null @@ -1,160 +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" - -static constexpr inline auto OfflineSceneTextureIx = 1u; - -struct TransformRequestParams -{ - bool useWindow = true, editTransformDecomposition = false, enableViewManipulate = false; - float camDistance = 8.f, aspectRatio = {}, invAspectRatio = {}; -}; - -void EditTransform(float* cameraView, const float* cameraProjection, float* matrix, 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 - */ - - SImResourceInfo info; - info.textureID = OfflineSceneTextureIx; - info.samplerIx = (uint16_t)nbl::ext::imgui::UI::DefaultSamplerIx::USER; - - 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(); - - params.aspectRatio = contentRegionSize.x / contentRegionSize.y; - params.invAspectRatio = contentRegionSize.y / contentRegionSize.x; - - ImGui::Image(info, contentRegionSize); - ImGuizmo::SetRect(cursorPos.x, cursorPos.y, 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); - - viewManipulateRight = cursorPos.x + contentRegionSize.x; - viewManipulateTop = cursorPos.y; - - params.aspectRatio = io.DisplaySize.x / io.DisplaySize.y; - params.invAspectRatio = io.DisplaySize.y / io.DisplaySize.x; - } - - 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(); -} - -#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 b828a84b0..7ee57ca3a 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -9,7 +9,6 @@ // FPS Camera, TESTS using camera_t = CFPSCamera; -using controller_t = CCameraController; using projection_t = IProjection>; // TODO: temporary -> projections will own/reference cameras constexpr IGPUImage::SSubresourceRange TripleBufferUsedSubresourceRange = @@ -398,14 +397,14 @@ class UISampleApp final : public examples::SimpleWindowedApplication params.transfer = getTransferUpQueue(); params.utilities = m_utils; - pass.ui.manager = nbl::ext::imgui::UI::create(std::move(params)); + m_ui.manager = nbl::ext::imgui::UI::create(std::move(params)); } - if (!pass.ui.manager) + if (!m_ui.manager) return false; // note that we use default layout provided by our extension, but you are free to create your own by filling nbl::ext::imgui::UI::S_CREATION_PARAMETERS::resources - const auto* descriptorSetLayout = pass.ui.manager->getPipeline()->getLayout()->getDescriptorSetLayout(0u); + const auto* descriptorSetLayout = m_ui.manager->getPipeline()->getLayout()->getDescriptorSetLayout(0u); IDescriptorPool::SCreateInfo descriptorPoolInfo = {}; descriptorPoolInfo.maxDescriptorCount[static_cast(asset::IDescriptor::E_TYPE::ET_SAMPLER)] = (uint32_t)nbl::ext::imgui::UI::DefaultSamplerIx::COUNT; @@ -416,16 +415,52 @@ class UISampleApp final : public examples::SimpleWindowedApplication m_descriptorSetPool = m_device->createDescriptorPool(std::move(descriptorPoolInfo)); assert(m_descriptorSetPool); - m_descriptorSetPool->createDescriptorSets(1u, &descriptorSetLayout, &pass.ui.descriptorSet); - assert(pass.ui.descriptorSet); + m_descriptorSetPool->createDescriptorSets(1u, &descriptorSetLayout, &m_ui.descriptorSet); + assert(m_ui.descriptorSet); - pass.ui.manager->registerListener([this]() -> void { imguiListen(); }); + m_ui.manager->registerListener([this]() -> void { imguiListen(); }); } - // Geometry Creator Scene + // Geometry Creator Render Scenes { - //pass.scene = CScene::create(smart_refctd_ptr(m_utils), smart_refctd_ptr(m_logger), getGraphicsQueue(), m_assetManager->getGeometryCreator()); - pass.scene = CScene::create(smart_refctd_ptr(m_utils), smart_refctd_ptr(m_logger), getGraphicsQueue(), m_assetManager->getGeometryCreator()); + resources = ResourcesBundle::create(m_device.get(), m_logger.get(), getGraphicsQueue(), m_assetManager->getGeometryCreator()); + + if (!resources) + { + m_logger->log("Could not create geometry creator gpu resources!", ILogger::ELL_ERROR); + return false; + } + + for (auto& camera : cameraz) + camera = make_smart_refctd_ptr(float32_t3{ -2.017f, 0.386f, 0.684f }, glm::quat(0.55f, 0.047f, 0.830f, -0.072f)); // order important for quat, the ctor is GLM_FUNC_QUALIFIER GLM_CONSTEXPR qua::qua(T _w, T _x, T _y, T _z) + + // lets use key map presets to update the controller + auto& camera = cameraz.front(); + scene = CScene::create(smart_refctd_ptr(m_device), smart_refctd_ptr(m_logger), getGraphicsQueue(), smart_refctd_ptr(resources)); + + if (!scene) + { + m_logger->log("Could not create geometry creator scene!", ILogger::ELL_ERROR); + return false; + } + + // init keyboard map + camera->updateKeyboardMapping([&](auto& keys) + { + keys = camera->getKeyboardMappingPreset(); + }); + + // init mouse map + camera->updateMouseMapping([&](auto& keys) + { + keys = camera->getMouseMappingPreset(); + }); + + // init imguizmo map + camera->updateImguizmoMapping([&](auto& keys) + { + keys = camera->getImguizmoMappingPreset(); + }); } oracle.reportBeginFrameRecord(); @@ -437,27 +472,6 @@ class UISampleApp final : public examples::SimpleWindowedApplication transformParams.aspectRatio = float(m_window->getWidth()) / float(m_window->getHeight()); transformParams.invAspectRatio = float(m_window->getHeight()) / float(m_window->getWidth()); - camera = make_smart_refctd_ptr(float32_t3{ -2.017f, 0.386f, 0.684f }, glm::quat(0.55f, 0.047f, 0.830f, -0.072f)); // order important for quat, the ctor is GLM_FUNC_QUALIFIER GLM_CONSTEXPR qua::qua(T _w, T _x, T _y, T _z) - controller = make_smart_refctd_ptr(core::smart_refctd_ptr(camera)); - - // init keyboard map - controller->updateKeyboardMapping([&](auto& keys) - { - keys = controller->getCamera()->getKeyboardMappingPreset(); - }); - - // init mouse map - controller->updateMouseMapping([&](auto& keys) - { - keys = controller->getCamera()->getMouseMappingPreset(); - }); - - // init imguizmo map - controller->updateImguizmoMapping([&](auto& keys) - { - keys = controller->getCamera()->getImguizmoMappingPreset(); - }); - if (base_t::argv.size() >= 3 && argv[1] == "-timeout_seconds") timeout = std::chrono::seconds(std::atoi(argv[2].c_str())); start = clock_t::now(); @@ -471,14 +485,14 @@ class UISampleApp final : public examples::SimpleWindowedApplication static IGPUDescriptorSet::SWriteDescriptorSet writes[TexturesAmount]; descriptorInfo[nbl::ext::imgui::UI::FontAtlasTexId].info.image.imageLayout = IImage::LAYOUT::READ_ONLY_OPTIMAL; - descriptorInfo[nbl::ext::imgui::UI::FontAtlasTexId].desc = core::smart_refctd_ptr(pass.ui.manager->getFontAtlasView()); + descriptorInfo[nbl::ext::imgui::UI::FontAtlasTexId].desc = core::smart_refctd_ptr(m_ui.manager->getFontAtlasView()); descriptorInfo[OfflineSceneTextureIx].info.image.imageLayout = IImage::LAYOUT::READ_ONLY_OPTIMAL; - descriptorInfo[OfflineSceneTextureIx].desc = pass.scene->getResources().attachments.color; + descriptorInfo[OfflineSceneTextureIx].desc = scene->getColorAttachment(); for (uint32_t i = 0; i < descriptorInfo.size(); ++i) { - writes[i].dstSet = pass.ui.descriptorSet.get(); + writes[i].dstSet = m_ui.descriptorSet.get(); writes[i].binding = 0u; writes[i].arrayElement = i; writes[i].count = 1u; @@ -530,13 +544,13 @@ class UISampleApp final : public examples::SimpleWindowedApplication // render geometry creator scene to offline frame buffer & submit // TODO: OK with TRI buffer this thing is retarded now // (**) <- a note why bellow before submit - pass.scene->begin(); + scene->begin(); { - pass.scene->update(); - pass.scene->record(); - pass.scene->end(); + scene->update(); + scene->record(); + scene->end(); } - pass.scene->submit(); + scene->submit(getGraphicsQueue()); willSubmit &= cmdbuf->reset(IGPUCommandBuffer::RESET_FLAGS::RELEASE_RESOURCES_BIT); willSubmit &= cmdbuf->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT); @@ -573,25 +587,27 @@ class UISampleApp final : public examples::SimpleWindowedApplication IQueue::SSubmitInfo::SCommandBufferInfo commandBuffersInfo[] = { {.cmdbuf = cmdbuf } }; + + const IGPUCommandBuffer::SRenderpassBeginInfo info = { .framebuffer = m_framebuffers[resourceIx].get(), - .colorClearValues = &clear.color, + .colorClearValues = &Traits::clearColor, .depthStencilClearValues = nullptr, .renderArea = currentRenderArea }; nbl::video::ISemaphore::SWaitInfo waitInfo = { .semaphore = m_semaphore.get(), .value = m_realFrameIx + 1u }; - const auto uiParams = pass.ui.manager->getCreationParameters(); - auto* pipeline = pass.ui.manager->getPipeline(); + const auto uiParams = m_ui.manager->getCreationParameters(); + auto* pipeline = m_ui.manager->getPipeline(); cmdbuf->bindGraphicsPipeline(pipeline); - cmdbuf->bindDescriptorSets(EPBP_GRAPHICS, pipeline->getLayout(), uiParams.resources.texturesInfo.setIx, 1u, &pass.ui.descriptorSet.get()); // note that we use default UI pipeline layout where uiParams.resources.textures.setIx == uiParams.resources.samplers.setIx + cmdbuf->bindDescriptorSets(EPBP_GRAPHICS, pipeline->getLayout(), uiParams.resources.texturesInfo.setIx, 1u, &m_ui.descriptorSet.get()); // note that we use default UI pipeline layout where uiParams.resources.textures.setIx == uiParams.resources.samplers.setIx if (!keepRunning()) return; - willSubmit &= pass.ui.manager->render(cmdbuf, waitInfo); + willSubmit &= m_ui.manager->render(cmdbuf, waitInfo); } willSubmit &= cmdbuf->endRenderPass(); @@ -666,8 +682,8 @@ class UISampleApp final : public examples::SimpleWindowedApplication { const nbl::video::ISemaphore::SWaitInfo waitInfos[] = { { - .semaphore = pass.scene->semaphore.progress.get(), - .value = pass.scene->semaphore.finishedValue + .semaphore = scene->semaphore.progress.get(), + .value = scene->semaphore.finishedValue } }; m_device->blockForSemaphores(waitInfos); @@ -761,10 +777,27 @@ class UISampleApp final : public examples::SimpleWindowedApplication if (move) { // TODO: testing - controller->manipulateViewGimbal({ params.keyboardEvents, params.mouseEvents }, nextPresentationTimestamp); + auto& camera = cameraz.front().get(); + + + std::vector virtualEvents(0x45); // TODO: tmp + uint32_t vCount; + + camera->beginInputProcessing(nextPresentationTimestamp); + { + camera->process(nullptr, vCount); + + if (virtualEvents.size() < vCount) + virtualEvents.resize(vCount); + + camera->process(virtualEvents.data(), vCount, { params.keyboardEvents, params.mouseEvents }); + } + camera->endInputProcessing(); + + camera->manipulate({ virtualEvents.data(), vCount }, ICamera::Local); } - pass.ui.manager->update(params); + m_ui.manager->update(params); } private: @@ -929,18 +962,165 @@ class UISampleApp final : public examples::SimpleWindowedApplication } imguizmoM16InOut; const auto& projectionMatrix = projection->getMatrix(); - const auto& view = camera->getGimbal().getView(); + const auto& view = cameraz.front()->getGimbal().getView(); ImGuizmo::SetID(0u); imguizmoM16InOut.view = transpose(getMatrix3x4As4x4(view.matrix)); imguizmoM16InOut.projection = transpose(projectionMatrix); - imguizmoM16InOut.model = transpose(getMatrix3x4As4x4(pass.scene->object.model)); + imguizmoM16InOut.model = transpose(getMatrix3x4As4x4(scene->object.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; - EditTransform(&imguizmoM16InOut.view[0][0], &imguizmoM16InOut.projection[0][0], &imguizmoM16InOut.model[0][0], transformParams); + { + 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; + + auto* cameraView = &imguizmoM16InOut.view[0][0]; + auto* cameraProjection = &imguizmoM16InOut.projection[0][0]; + auto* matrix = &imguizmoM16InOut.model[0][0]; + + if (transformParams.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 + */ + + SImResourceInfo info; + info.textureID = OfflineSceneTextureIx; + info.samplerIx = (uint16_t)nbl::ext::imgui::UI::DefaultSamplerIx::USER; + + if (transformParams.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(); + + transformParams.aspectRatio = contentRegionSize.x / contentRegionSize.y; + transformParams.invAspectRatio = contentRegionSize.y / contentRegionSize.x; + + ImGui::Image(info, contentRegionSize); + ImGuizmo::SetRect(cursorPos.x, cursorPos.y, 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); + + viewManipulateRight = cursorPos.x + contentRegionSize.x; + viewManipulateTop = cursorPos.y; + + transformParams.aspectRatio = io.DisplaySize.x / io.DisplaySize.y; + transformParams.invAspectRatio = io.DisplaySize.y / io.DisplaySize.x; + } + + // object gizmo + ImGuizmo::Manipulate(cameraView, cameraProjection, mCurrentGizmoOperation, mCurrentGizmoMode, matrix, NULL, useSnap ? &snap[0] : NULL, boundSizing ? bounds : NULL, boundSizingSnap ? boundsSnap : NULL); + + // second camera + { + // TODO: manipulate second camera given view & projection from first, use delta matrix to be passed to imguizmo controller + } + + ImGui::End(); + ImGui::PopStyleColor(); + } } // to Nabla + update camera & model matrices @@ -951,10 +1131,10 @@ class UISampleApp final : public examples::SimpleWindowedApplication static float32_t3x4 modelView, normal; static float32_t4x4 modelViewProjection; - auto& hook = pass.scene->object; + auto& hook = scene->object; hook.model = float32_t3x4(transpose(imguizmoM16InOut.model)); { - const auto& references = pass.scene->getResources().objects; + const auto& references = resources->objects; const auto type = static_cast(gcIndex); const auto& [gpu, meta] = references[type]; @@ -1009,9 +1189,10 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGui::Separator(); }; - const auto& orientation = camera->getGimbal().getOrthonornalMatrix(); + auto& camera = cameraz.front(); + const auto& orientation = cameraz.front()->getGimbal().getOrthonornalMatrix(); - addMatrixTable("Object's Model Matrix", "ModelMatrixTable", 3, 4, &pass.scene->object.model[0][0]); + addMatrixTable("Object's Model Matrix", "ModelMatrixTable", 3, 4, &scene->object.model[0][0]); addMatrixTable("Camera's Position", "PositionForwardVec", 1, 3, &camera->getGimbal().getPosition()[0]); addMatrixTable("Camera's Orientation Quat", "OrientationQuatTable", 1, 4, &camera->getGimbal().getOrientation()[0]); addMatrixTable("Camera's View Matrix", "ViewMatrixTable", 3, 4, &view.matrix[0][0]); @@ -1024,7 +1205,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication // 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 = pass.ui.manager->getStreamingBuffer(); + auto* streaminingBuffer = m_ui.manager->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 @@ -1089,7 +1270,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGui::End(); } - displayKeyMappingsAndVirtualStates(controller.get()); + displayKeyMappingsAndVirtualStates(cameraz.front().get()); ImGui::End(); } @@ -1130,7 +1311,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication core::smart_refctd_ptr m_descriptorSetPool; - struct C_UI + struct CRenderUI { nbl::core::smart_refctd_ptr manager; @@ -1142,19 +1323,26 @@ class UISampleApp final : public examples::SimpleWindowedApplication core::smart_refctd_ptr descriptorSet; }; - struct E_APP_PASS - { - nbl::core::smart_refctd_ptr scene; - C_UI ui; - } pass; + nbl::core::smart_refctd_ptr scene; + // lets first test 2 cameras & imguizmo controller, then we will add second scene to render into 2 frame buffers + std::array>, 2u> cameraz; + nbl::core::smart_refctd_ptr resources; + + CRenderUI m_ui; smart_refctd_ptr projection = make_smart_refctd_ptr(); // TMP! - core::smart_refctd_ptr> camera; - core::smart_refctd_ptr controller; video::CDumbPresentationOracle oracle; 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 + struct TransformRequestParams + { + bool useWindow = true, editTransformDecomposition = false, enableViewManipulate = false; + float camDistance = 8.f, aspectRatio = {}, invAspectRatio = {}; + }; + + static constexpr inline auto OfflineSceneTextureIx = 1u; + TransformRequestParams transformParams; bool isPerspective = true, isLH = true, flipGizmoY = true, move = false; float fov = 60.f, zNear = 0.1f, zFar = 10000.f, moveSpeed = 1.f, rotateSpeed = 1.f; diff --git a/common/include/CGeomtryCreatorScene.hpp b/common/include/CGeomtryCreatorScene.hpp index fdd877efb..07f54b19a 100644 --- a/common/include/CGeomtryCreatorScene.hpp +++ b/common/include/CGeomtryCreatorScene.hpp @@ -10,6 +10,21 @@ namespace nbl::scene::geometrycreator { +#define EXPOSE_NABLA_NAMESPACES() using namespace nbl; \ +using namespace core; \ +using namespace asset; \ +using namespace video; \ +using namespace scene; \ +using namespace system + +struct Traits +{ + static constexpr auto FramebufferW = 1280u, FramebufferH = 720u; + static constexpr auto ColorFboAttachmentFormat = nbl::asset::EF_R8G8B8A8_SRGB, DepthFboAttachmentFormat = nbl::asset::EF_D16_UNORM; + static constexpr auto Samples = nbl::video::IGPUImage::ESCF_1_BIT; + static constexpr nbl::video::IGPUCommandBuffer::SClearColorValue clearColor = { .float32 = {0.f,0.f,0.f,1.f} }; + static constexpr nbl::video::IGPUCommandBuffer::SClearDepthStencilValue clearDepth = { .depth = 0.f }; +}; enum ObjectType : uint8_t { @@ -32,38 +47,16 @@ struct ObjectMeta std::string_view name = "Unknown"; }; -constexpr static inline struct ClearValues +struct ResourcesBundle : public virtual nbl::core::IReferenceCounted { - nbl::video::IGPUCommandBuffer::SClearColorValue color = { .float32 = {0.f,0.f,0.f,1.f} }; - nbl::video::IGPUCommandBuffer::SClearDepthStencilValue depth = { .depth = 0.f }; -} clear; - -#define TYPES_IMPL_BOILERPLATE(WithConverter) struct Types \ -{ \ - using descriptor_set_layout_t = std::conditional_t; \ - using pipeline_layout_t = std::conditional_t; \ - using renderpass_t = std::conditional_t; \ - using image_view_t = std::conditional_t; \ - using image_t = std::conditional_t; \ - using buffer_t = std::conditional_t; \ - using shader_t = std::conditional_t; \ - using graphics_pipeline_t = std::conditional_t; \ - using descriptor_set = std::conditional_t; \ -} - -template -struct ResourcesBundleBase -{ - TYPES_IMPL_BOILERPLATE(withAssetConverter); - struct ReferenceObject { struct Bindings { - nbl::asset::SBufferBinding vertex, index; + nbl::asset::SBufferBinding vertex, index; }; - nbl::core::smart_refctd_ptr pipeline = nullptr; + nbl::core::smart_refctd_ptr pipeline = nullptr; Bindings bindings; nbl::asset::E_INDEX_TYPE indexType = nbl::asset::E_INDEX_TYPE::EIT_UNKNOWN; @@ -72,526 +65,129 @@ struct ResourcesBundleBase using ReferenceDrawHook = std::pair; - nbl::core::smart_refctd_ptr renderpass; std::array objects; - nbl::asset::SBufferBinding ubo; + nbl::core::smart_refctd_ptr renderpass; + nbl::core::smart_refctd_ptr dsLayout; - struct - { - nbl::core::smart_refctd_ptr color, depth; - } attachments; - - nbl::core::smart_refctd_ptr descriptorSet; -}; - -struct ResourcesBundle : public ResourcesBundleBase -{ - using base_t = ResourcesBundleBase; -}; - -#define EXPOSE_NABLA_NAMESPACES() using namespace nbl; \ -using namespace core; \ -using namespace asset; \ -using namespace video; \ -using namespace scene; \ -using namespace system - -template -class ResourceBuilder -{ -public: - TYPES_IMPL_BOILERPLATE(withAssetConverter); - - using this_t = ResourceBuilder; - - ResourceBuilder(nbl::video::IUtilities* const _utilities, nbl::video::IGPUCommandBuffer* const _commandBuffer, nbl::system::ILogger* const _logger, const nbl::asset::IGeometryCreator* const _geometryCreator) - : utilities(_utilities), commandBuffer(_commandBuffer), logger(_logger), geometries(_geometryCreator) - { - assert(utilities); - assert(logger); - } - - /* - if (withAssetConverter) then - -> .build cpu objects - else - -> .build gpu objects & record any resource update upload transfers into command buffer - */ - - inline bool build() + static inline nbl::core::smart_refctd_ptr create(nbl::video::ILogicalDevice* const device, nbl::system::ILogger* const logger, nbl::video::CThreadSafeQueueAdapter* transferCapableQueue, const nbl::asset::IGeometryCreator* gc) { EXPOSE_NABLA_NAMESPACES(); - if constexpr (!withAssetConverter) - { - commandBuffer->reset(IGPUCommandBuffer::RESET_FLAGS::RELEASE_RESOURCES_BIT); - commandBuffer->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT); - commandBuffer->beginDebugMarker("Resources builder's buffers upload [manual]"); - } - - using functor_t = std::function; - - auto work = std::to_array - ({ - functor_t(std::bind(&this_t::createDescriptorSetLayout, this)), - functor_t(std::bind(&this_t::createPipelineLayout, this)), - functor_t(std::bind(&this_t::createRenderpass, this)), - functor_t(std::bind(&this_t::createFramebufferAttachments, this)), - functor_t(std::bind(&this_t::createShaders, this)), - functor_t(std::bind(&this_t::createGeometries, this)), - functor_t(std::bind(&this_t::createViewParametersUboBuffer, this)), - functor_t(std::bind(&this_t::createDescriptorSet, this)) - }); - - for (auto& task : work) - if (!task()) - return false; - - if constexpr (!withAssetConverter) - commandBuffer->end(); - - return true; - } + if (!device) + return nullptr; - /* - if (withAssetConverter) then - -> .convert cpu objects to gpu & update gpu buffers - else - -> update gpu buffers - */ + if (!logger) + return nullptr; - inline bool finalize(ResourcesBundle& output, nbl::video::CThreadSafeQueueAdapter* transferCapableQueue) - { - EXPOSE_NABLA_NAMESPACES(); + if (!transferCapableQueue) + return nullptr; - // TODO: use multiple command buffers - std::array commandBuffers = {}; - { - commandBuffers.front().cmdbuf = commandBuffer; - } + auto cPool = device->createCommandPool(transferCapableQueue->getFamilyIndex(), IGPUCommandPool::CREATE_FLAGS::RESET_COMMAND_BUFFER_BIT); - if constexpr (withAssetConverter) + if (!cPool) { - // note that asset converter records basic transfer uploads itself, we only begin the recording with ONE_TIME_SUBMIT_BIT - commandBuffer->reset(IGPUCommandBuffer::RESET_FLAGS::RELEASE_RESOURCES_BIT); - commandBuffer->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT); - commandBuffer->beginDebugMarker("Resources builder's buffers upload [asset converter]"); - - // asset converter - scratch at this point has ready to convert cpu resources - smart_refctd_ptr converter = CAssetConverter::create({ .device = utilities->getLogicalDevice(),.optimizer = {} }); - CAssetConverter::SInputs inputs = {}; - inputs.logger = logger; - - struct ProxyCpuHooks - { - using object_size_t = std::tuple_size; - - std::array renderpass; - std::array pipelines; - std::array buffers; - std::array attachments; - std::array descriptorSet; - } hooks; - - enum AttachmentIx - { - AI_COLOR = 0u, - AI_DEPTH = 1u, - - AI_COUNT - }; - - // gather CPU assets into span memory views - { - hooks.renderpass.front() = scratch.renderpass.get(); - for (uint32_t i = 0u; i < hooks.pipelines.size(); ++i) - { - auto& [reference, meta] = scratch.objects[static_cast(i)]; - hooks.pipelines[i] = reference.pipeline.get(); - - // [[ [vertex, index] [vertex, index] [vertex, index] ... [ubo] ]] - hooks.buffers[2u * i + 0u] = reference.bindings.vertex.buffer.get(); - hooks.buffers[2u * i + 1u] = reference.bindings.index.buffer.get(); - } - hooks.buffers.back() = scratch.ubo.buffer.get(); - hooks.attachments[AI_COLOR] = scratch.attachments.color.get(); - hooks.attachments[AI_DEPTH] = scratch.attachments.depth.get(); - hooks.descriptorSet.front() = scratch.descriptorSet.get(); - } - - // assign the CPU hooks to converter's inputs - { - std::get>(inputs.assets) = hooks.renderpass; - std::get>(inputs.assets) = hooks.pipelines; - std::get>(inputs.assets) = hooks.buffers; - // std::get>(inputs.assets) = hooks.attachments; // NOTE: THIS IS NOT IMPLEMENTED YET IN CONVERTER! - std::get>(inputs.assets) = hooks.descriptorSet; - } - - // reserve and create the GPU object handles - auto reservation = converter->reserve(inputs); - { - auto prepass = [&](const auto& references) -> bool - { - // retrieve the reserved handles - auto objects = reservation.getGPUObjects(); - - uint32_t counter = {}; - for (auto& object : objects) - { - // anything that fails to be reserved is a nullptr in the span of GPU Objects - auto gpu = object.value; - auto* reference = references[counter]; - - if (reference) - { - // validate - if (!gpu) // throw errors only if corresponding cpu hook was VALID (eg. we may have nullptr for some index buffers in the span for converter but it's OK, I'm too lazy to filter them before passing to the converter inputs and don't want to deal with dynamic alloc) - { - logger->log("Failed to convert a CPU object to GPU!", ILogger::ELL_ERROR); - return false; - } - } - - ++counter; - } - - return true; - }; - - prepass.template operator() < ICPURenderpass > (hooks.renderpass); - prepass.template operator() < ICPUGraphicsPipeline > (hooks.pipelines); - prepass.template operator() < ICPUBuffer > (hooks.buffers); - // validate.template operator() < ICPUImageView > (hooks.attachments); - prepass.template operator() < ICPUDescriptorSet > (hooks.descriptorSet); - } - - auto semaphore = utilities->getLogicalDevice()->createSemaphore(0u); - - // TODO: compute submit as well for the images' mipmaps - SIntendedSubmitInfo transfer = {}; - transfer.queue = transferCapableQueue; - transfer.scratchCommandBuffers = commandBuffers; - transfer.scratchSemaphore = { - .semaphore = semaphore.get(), - .value = 0u, - .stageMask = PIPELINE_STAGE_FLAGS::ALL_TRANSFER_BITS - }; - // issue the convert call - { - CAssetConverter::SConvertParams params = {}; - params.utilities = utilities; - params.transfer = &transfer; - - // basically it records all data uploads and submits them right away - auto future = reservation.convert(params); - if (future.copy()!=IQueue::RESULT::SUCCESS) - { - logger->log("Failed to await submission feature!", ILogger::ELL_ERROR); - return false; - } - - // assign gpu objects to output - auto& base = static_cast(output); - { - auto&& [renderpass, pipelines, buffers, descriptorSet] = std::make_tuple(reservation.getGPUObjects().front().value, reservation.getGPUObjects(), reservation.getGPUObjects(), reservation.getGPUObjects().front().value); - { - base.renderpass = renderpass; - for (uint32_t i = 0u; i < pipelines.size(); ++i) - { - const auto type = static_cast(i); - const auto& [rcpu, rmeta] = scratch.objects[type]; - auto& [gpu, meta] = base.objects[type]; - - gpu.pipeline = pipelines[i].value; - // [[ [vertex, index] [vertex, index] [vertex, index] ... [ubo] ]] - gpu.bindings.vertex = {.offset = 0u, .buffer = buffers[2u * i + 0u].value}; - gpu.bindings.index = {.offset = 0u, .buffer = buffers[2u * i + 1u].value}; - - gpu.indexCount = rcpu.indexCount; - gpu.indexType = rcpu.indexType; - meta.name = rmeta.name; - meta.type = rmeta.type; - } - base.ubo = {.offset = 0u, .buffer = buffers.back().value}; - base.descriptorSet = descriptorSet; - - /* - // base.attachments.color = attachments[AI_COLOR].value; - // base.attachments.depth = attachments[AI_DEPTH].value; - - note conversion of image views is not yet supported by the asset converter - - it's complicated, we have to kinda temporary ignore DRY a bit here to not break the design which is correct - - TEMPORARY: we patch attachments by allocating them ourselves here given cpu instances & parameters - TODO: remove following code once asset converter works with image views & update stuff - */ - - for (uint32_t i = 0u; i < AI_COUNT; ++i) - { - const auto* reference = hooks.attachments[i]; - auto& out = (i == AI_COLOR ? base.attachments.color : base.attachments.depth); - - const auto& viewParams = reference->getCreationParameters(); - const auto& imageParams = viewParams.image->getCreationParameters(); - - auto image = utilities->getLogicalDevice()->createImage - ( - IGPUImage::SCreationParams - ({ - .type = imageParams.type, - .samples = imageParams.samples, - .format = imageParams.format, - .extent = imageParams.extent, - .mipLevels = imageParams.mipLevels, - .arrayLayers = imageParams.arrayLayers, - .usage = imageParams.usage - }) - ); - - if (!image) - { - logger->log("Could not create image!", ILogger::ELL_ERROR); - return false; - } - - bool IS_DEPTH = isDepthOrStencilFormat(imageParams.format); - std::string_view DEBUG_NAME = IS_DEPTH ? "UI Scene Depth Attachment Image" : "UI Scene Color Attachment Image"; - image->setObjectDebugName(DEBUG_NAME.data()); - - if (!utilities->getLogicalDevice()->allocate(image->getMemoryReqs(), image.get()).isValid()) - { - logger->log("Could not allocate memory for an image!", ILogger::ELL_ERROR); - return false; - } - - out = utilities->getLogicalDevice()->createImageView - ( - IGPUImageView::SCreationParams - ({ - .flags = viewParams.flags, - .subUsages = viewParams.subUsages, - .image = std::move(image), - .viewType = viewParams.viewType, - .format = viewParams.format, - .subresourceRange = viewParams.subresourceRange - }) - ); - - if (!out) - { - logger->log("Could not create image view!", ILogger::ELL_ERROR); - return false; - } - } - - logger->log("Image View attachments has been allocated by hand after asset converter successful submit becasuse it doesn't support converting them yet!", ILogger::ELL_WARNING); - } - } - } + logger->log("Couldn't create command pool!", ILogger::ELL_ERROR); + return nullptr; } - else - { - auto completed = utilities->getLogicalDevice()->createSemaphore(0u); - std::array signals; - { - auto& signal = signals.front(); - signal.value = 1; - signal.stageMask = bitflag(PIPELINE_STAGE_FLAGS::ALL_TRANSFER_BITS); - signal.semaphore = completed.get(); - } - - const IQueue::SSubmitInfo infos [] = - { - { - .waitSemaphores = {}, - .commandBuffers = commandBuffers, // note that here our command buffer is already recorded! - .signalSemaphores = signals - } - }; - - if (transferCapableQueue->submit(infos) != IQueue::RESULT::SUCCESS) - { - logger->log("Failed to submit transfer upload operations!", ILogger::ELL_ERROR); - return false; - } - - const ISemaphore::SWaitInfo info [] = - { { - .semaphore = completed.get(), - .value = 1 - } }; - - utilities->getLogicalDevice()->blockForSemaphores(info); - - static_cast(output) = static_cast(scratch); // scratch has all ready to use allocated gpu resources with uploaded memory so now just assign resources to base output - } + nbl::core::smart_refctd_ptr cmd; - // write the descriptor set + if (!cPool->createCommandBuffers(IGPUCommandPool::BUFFER_LEVEL::PRIMARY, { &cmd , 1 })) { - // descriptor write ubo - IGPUDescriptorSet::SWriteDescriptorSet write; - write.dstSet = output.descriptorSet.get(); - write.binding = 0; - write.arrayElement = 0u; - write.count = 1u; - - IGPUDescriptorSet::SDescriptorInfo info; - { - info.desc = smart_refctd_ptr(output.ubo.buffer); - info.info.buffer.offset = output.ubo.offset; - info.info.buffer.size = output.ubo.buffer->getSize(); - } - - write.info = &info; - - if(!utilities->getLogicalDevice()->updateDescriptorSets(1u, &write, 0u, nullptr)) - { - logger->log("Could not write descriptor set!", ILogger::ELL_ERROR); - return false; - } + logger->log("Couldn't create command buffer!", ILogger::ELL_ERROR); + return nullptr; } - return true; - } + if (!cmd) + return nullptr; -private: - bool createDescriptorSetLayout() - { - EXPOSE_NABLA_NAMESPACES(); + cmd->reset(IGPUCommandBuffer::RESET_FLAGS::RELEASE_RESOURCES_BIT); + cmd->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT); + cmd->beginDebugMarker("GC Scene resources upload buffer"); - typename Types::descriptor_set_layout_t::SBinding bindings[] = + //! descriptor set layout + + IGPUDescriptorSetLayout::SBinding bindings[] = { { .binding = 0u, .type = IDescriptor::E_TYPE::ET_UNIFORM_BUFFER, - .createFlags = Types::descriptor_set_layout_t::SBinding::E_CREATE_FLAGS::ECF_NONE, + .createFlags = IGPUDescriptorSetLayout::SBinding::E_CREATE_FLAGS::ECF_NONE, .stageFlags = IShader::E_SHADER_STAGE::ESS_VERTEX | IShader::E_SHADER_STAGE::ESS_FRAGMENT, .count = 1u, } }; - if constexpr (withAssetConverter) - scratch.descriptorSetLayout = make_smart_refctd_ptr(bindings); - else - scratch.descriptorSetLayout = utilities->getLogicalDevice()->createDescriptorSetLayout(bindings); + auto dsLayout = device->createDescriptorSetLayout(bindings); - if (!scratch.descriptorSetLayout) + if (!dsLayout) { logger->log("Could not descriptor set layout!", ILogger::ELL_ERROR); - return false; - } - - return true; - } - - bool createDescriptorSet() - { - EXPOSE_NABLA_NAMESPACES(); - - if constexpr (withAssetConverter) - scratch.descriptorSet = make_smart_refctd_ptr(smart_refctd_ptr(scratch.descriptorSetLayout)); - else - { - const IGPUDescriptorSetLayout* const layouts[] = { scratch.descriptorSetLayout.get()}; - const uint32_t setCounts[] = { 1u }; - - // note descriptor set has back smart pointer to its pool, so we dont need to keep it explicitly - auto pool = utilities->getLogicalDevice()->createDescriptorPoolForDSLayouts(IDescriptorPool::E_CREATE_FLAGS::ECF_NONE, layouts, setCounts); - - if (!pool) - { - logger->log("Could not create Descriptor Pool!", ILogger::ELL_ERROR); - return false; - } - - pool->createDescriptorSets(layouts, &scratch.descriptorSet); - } - - if (!scratch.descriptorSet) - { - logger->log("Could not create Descriptor Set!", ILogger::ELL_ERROR); - return false; + return nullptr; } - return true; - } - - bool createPipelineLayout() - { - EXPOSE_NABLA_NAMESPACES(); - - const std::span range = {}; - - if constexpr (withAssetConverter) - scratch.pipelineLayout = make_smart_refctd_ptr(range, nullptr, smart_refctd_ptr(scratch.descriptorSetLayout), nullptr, nullptr); - else - scratch.pipelineLayout = utilities->getLogicalDevice()->createPipelineLayout(range, nullptr, smart_refctd_ptr(scratch.descriptorSetLayout), nullptr, nullptr); + //! pipeline layout + + auto pipelineLayout = device->createPipelineLayout({}, nullptr, smart_refctd_ptr(dsLayout), nullptr, nullptr); - if (!scratch.pipelineLayout) + if (!pipelineLayout) { logger->log("Could not create pipeline layout!", ILogger::ELL_ERROR); - return false; + return nullptr; } - return true; - } - - bool createRenderpass() - { - EXPOSE_NABLA_NAMESPACES(); - - static constexpr Types::renderpass_t::SCreationParams::SColorAttachmentDescription colorAttachments[] = + //! renderpass + + static constexpr IGPURenderpass::SCreationParams::SColorAttachmentDescription colorAttachments[] = { { { { - .format = ColorFboAttachmentFormat, - .samples = Samples, + .format = Traits::ColorFboAttachmentFormat, + .samples = Traits::Samples, .mayAlias = false }, - /* .loadOp = */ Types::renderpass_t::LOAD_OP::CLEAR, - /* .storeOp = */ Types::renderpass_t::STORE_OP::STORE, - /* .initialLayout = */ Types::image_t::LAYOUT::UNDEFINED, - /* .finalLayout = */ Types::image_t::LAYOUT::READ_ONLY_OPTIMAL + /* .loadOp = */ IGPURenderpass::LOAD_OP::CLEAR, + /* .storeOp = */ IGPURenderpass::STORE_OP::STORE, + /* .initialLayout = */ IGPUImage::LAYOUT::UNDEFINED, + /* .finalLayout = */ IGPUImage::LAYOUT::READ_ONLY_OPTIMAL } }, - Types::renderpass_t::SCreationParams::ColorAttachmentsEnd + IGPURenderpass::SCreationParams::ColorAttachmentsEnd }; - static constexpr Types::renderpass_t::SCreationParams::SDepthStencilAttachmentDescription depthAttachments[] = + static constexpr IGPURenderpass::SCreationParams::SDepthStencilAttachmentDescription depthAttachments[] = { { { { - .format = DepthFboAttachmentFormat, - .samples = Samples, + .format = Traits::DepthFboAttachmentFormat, + .samples = Traits::Samples, .mayAlias = false }, - /* .loadOp = */ {Types::renderpass_t::LOAD_OP::CLEAR}, - /* .storeOp = */ {Types::renderpass_t::STORE_OP::STORE}, - /* .initialLayout = */ {Types::image_t::LAYOUT::UNDEFINED}, - /* .finalLayout = */ {Types::image_t::LAYOUT::ATTACHMENT_OPTIMAL} + /* .loadOp = */ {IGPURenderpass::LOAD_OP::CLEAR}, + /* .storeOp = */ {IGPURenderpass::STORE_OP::STORE}, + /* .initialLayout = */ {IGPUImage::LAYOUT::UNDEFINED}, + /* .finalLayout = */ {IGPUImage::LAYOUT::ATTACHMENT_OPTIMAL} } }, - Types::renderpass_t::SCreationParams::DepthStencilAttachmentsEnd + IGPURenderpass::SCreationParams::DepthStencilAttachmentsEnd }; - typename Types::renderpass_t::SCreationParams::SSubpassDescription subpasses[] = + IGPURenderpass::SCreationParams::SSubpassDescription subpasses[] = { {}, - Types::renderpass_t::SCreationParams::SubpassesEnd + IGPURenderpass::SCreationParams::SubpassesEnd }; - subpasses[0].depthStencilAttachment.render = { .attachmentIndex = 0u,.layout = Types::image_t::LAYOUT::ATTACHMENT_OPTIMAL }; - subpasses[0].colorAttachments[0] = { .render = {.attachmentIndex = 0u, .layout = Types::image_t::LAYOUT::ATTACHMENT_OPTIMAL } }; + subpasses[0].depthStencilAttachment.render = { .attachmentIndex = 0u,.layout = IGPUImage::LAYOUT::ATTACHMENT_OPTIMAL }; + subpasses[0].colorAttachments[0] = { .render = {.attachmentIndex = 0u, .layout = IGPUImage::LAYOUT::ATTACHMENT_OPTIMAL } }; - static constexpr Types::renderpass_t::SCreationParams::SSubpassDependency dependencies[] = + static constexpr IGPURenderpass::SCreationParams::SSubpassDependency dependencies[] = { // wipe-transition of Color to ATTACHMENT_OPTIMAL { - .srcSubpass = Types::renderpass_t::SCreationParams::SSubpassDependency::External, + .srcSubpass = IGPURenderpass::SCreationParams::SSubpassDependency::External, .dstSubpass = 0, .memoryBarrier = { @@ -609,7 +205,7 @@ class ResourceBuilder // color from ATTACHMENT_OPTIMAL to PRESENT_SRC { .srcSubpass = 0, - .dstSubpass = Types::renderpass_t::SCreationParams::SSubpassDependency::External, + .dstSubpass = IGPURenderpass::SCreationParams::SSubpassDependency::External, .memoryBarrier = { // last place where the depth can get modified @@ -624,199 +220,117 @@ class ResourceBuilder } // leave view offsets and flags default }, - Types::renderpass_t::SCreationParams::DependenciesEnd + IGPURenderpass::SCreationParams::DependenciesEnd }; - typename Types::renderpass_t::SCreationParams params = {}; + IGPURenderpass::SCreationParams params = {}; params.colorAttachments = colorAttachments; params.depthStencilAttachments = depthAttachments; params.subpasses = subpasses; params.dependencies = dependencies; - if constexpr (withAssetConverter) - scratch.renderpass = ICPURenderpass::create(params); - else - scratch.renderpass = utilities->getLogicalDevice()->createRenderpass(params); + auto renderpass = device->createRenderpass(params); - if (!scratch.renderpass) + if (!renderpass) { logger->log("Could not create render pass!", ILogger::ELL_ERROR); - return false; + return nullptr; } - return true; - } - - bool createFramebufferAttachments() - { - EXPOSE_NABLA_NAMESPACES(); - - auto createImageView = [&](smart_refctd_ptr& outView) -> smart_refctd_ptr - { - constexpr bool IS_DEPTH = isDepthOrStencilFormat(); - constexpr auto USAGE = [](const bool isDepth) - { - bitflag usage = Types::image_t::EUF_RENDER_ATTACHMENT_BIT; - - if (!isDepth) - usage |= Types::image_t::EUF_SAMPLED_BIT; - - return usage; - }(IS_DEPTH); - constexpr auto ASPECT = IS_DEPTH ? IImage::E_ASPECT_FLAGS::EAF_DEPTH_BIT : IImage::E_ASPECT_FLAGS::EAF_COLOR_BIT; - constexpr std::string_view DEBUG_NAME = IS_DEPTH ? "UI Scene Depth Attachment Image" : "UI Scene Color Attachment Image"; - { - smart_refctd_ptr image; - { - auto params = typename Types::image_t::SCreationParams( - { - .type = Types::image_t::ET_2D, - .samples = Samples, - .format = format, - .extent = { FramebufferW, FramebufferH, 1u }, - .mipLevels = 1u, - .arrayLayers = 1u, - .usage = USAGE - }); - - if constexpr (withAssetConverter) - image = ICPUImage::create(params); - else - image = utilities->getLogicalDevice()->createImage(std::move(params)); - } - - if (!image) - { - logger->log("Could not create image!", ILogger::ELL_ERROR); - return nullptr; - } - - if constexpr (withAssetConverter) - { - auto dummyBuffer = make_smart_refctd_ptr(FramebufferW * FramebufferH * getTexelOrBlockBytesize()); - dummyBuffer->setContentHash(dummyBuffer->computeContentHash()); - - auto regions = make_refctd_dynamic_array>(1u); - auto& region = regions->front(); - - region.imageSubresource = { .aspectMask = ASPECT, .mipLevel = 0u, .baseArrayLayer = 0u, .layerCount = 0u }; - region.bufferOffset = 0u; - region.bufferRowLength = IImageAssetHandlerBase::calcPitchInBlocks(FramebufferW, getTexelOrBlockBytesize()); - region.bufferImageHeight = 0u; - region.imageOffset = { 0u, 0u, 0u }; - region.imageExtent = { FramebufferW, FramebufferH, 1u }; - - if (!image->setBufferAndRegions(std::move(dummyBuffer), regions)) - { - logger->log("Could not set image's regions!", ILogger::ELL_ERROR); - return nullptr; - } - image->setContentHash(image->computeContentHash()); - } - else - { - image->setObjectDebugName(DEBUG_NAME.data()); - - if (!utilities->getLogicalDevice()->allocate(image->getMemoryReqs(), image.get()).isValid()) - { - logger->log("Could not allocate memory for an image!", ILogger::ELL_ERROR); - return nullptr; - } - } - - auto params = typename Types::image_view_t::SCreationParams - ({ - .flags = Types::image_view_t::ECF_NONE, - .subUsages = USAGE, - .image = std::move(image), - .viewType = Types::image_view_t::ET_2D, - .format = format, - .subresourceRange = { .aspectMask = ASPECT, .baseMipLevel = 0u, .levelCount = 1u, .baseArrayLayer = 0u, .layerCount = 1u } - }); - - if constexpr (withAssetConverter) - outView = make_smart_refctd_ptr(std::move(params)); - else - outView = utilities->getLogicalDevice()->createImageView(std::move(params)); - - if (!outView) - { - logger->log("Could not create image view!", ILogger::ELL_ERROR); - return nullptr; - } - - return smart_refctd_ptr(outView); - } - }; - - const bool allocated = createImageView.template operator() < ColorFboAttachmentFormat > (scratch.attachments.color) && createImageView.template operator() < DepthFboAttachmentFormat > (scratch.attachments.depth); - - if (!allocated) - { - logger->log("Could not allocate frame buffer's attachments!", ILogger::ELL_ERROR); - return false; - } + //! shaders + + auto createShader = [&](IShader::E_SHADER_STAGE stage, smart_refctd_ptr& outShader) -> smart_refctd_ptr + { + const SBuiltinFile& in = ::geometry::creator::spirv::builtin::get_resource(); + const auto buffer = make_smart_refctd_ptr, true> >(in.size, (void*)in.contents, adopt_memory); + auto shader = make_smart_refctd_ptr(smart_refctd_ptr(buffer), stage, IShader::E_CONTENT_TYPE::ECT_SPIRV, ""); - return true; - } + outShader = device->createShader(shader.get()); - bool createShaders() - { - EXPOSE_NABLA_NAMESPACES(); + return outShader; + }; - auto createShader = [&](IShader::E_SHADER_STAGE stage, smart_refctd_ptr& outShader) -> smart_refctd_ptr + struct GeometriesCpu { - // TODO: use SPIRV loader & our ::system ns to get those cpu shaders, do not create myself (shit I forgot it exists) + enum GeometryShader + { + GP_BASIC = 0, + GP_CONE, + GP_ICO, - const SBuiltinFile& in = ::geometry::creator::spirv::builtin::get_resource(); - const auto buffer = make_smart_refctd_ptr, true> >(in.size, (void*)in.contents, adopt_memory); - auto shader = make_smart_refctd_ptr(smart_refctd_ptr(buffer), stage, IShader::E_CONTENT_TYPE::ECT_SPIRV, ""); // must create cpu instance regardless underlying type + GP_COUNT + }; + + struct ReferenceObjectCpu + { + ObjectMeta meta; + GeometryShader shadersType; + nbl::asset::CGeometryCreator::return_type data; + }; - if constexpr (withAssetConverter) + GeometriesCpu(const nbl::asset::IGeometryCreator* _gc) + : gc(_gc), + objects + ({ + ReferenceObjectCpu {.meta = {.type = OT_CUBE, .name = "Cube Mesh" }, .shadersType = GP_BASIC, .data = gc->createCubeMesh(nbl::core::vector3df(1.f, 1.f, 1.f)) }, + ReferenceObjectCpu {.meta = {.type = OT_SPHERE, .name = "Sphere Mesh" }, .shadersType = GP_BASIC, .data = gc->createSphereMesh(2, 16, 16) }, + ReferenceObjectCpu {.meta = {.type = OT_CYLINDER, .name = "Cylinder Mesh" }, .shadersType = GP_BASIC, .data = gc->createCylinderMesh(2, 2, 20) }, + ReferenceObjectCpu {.meta = {.type = OT_RECTANGLE, .name = "Rectangle Mesh" }, .shadersType = GP_BASIC, .data = gc->createRectangleMesh(nbl::core::vector2df_SIMD(1.5, 3)) }, + ReferenceObjectCpu {.meta = {.type = OT_DISK, .name = "Disk Mesh" }, .shadersType = GP_BASIC, .data = gc->createDiskMesh(2, 30) }, + ReferenceObjectCpu {.meta = {.type = OT_ARROW, .name = "Arrow Mesh" }, .shadersType = GP_BASIC, .data = gc->createArrowMesh() }, + ReferenceObjectCpu {.meta = {.type = OT_CONE, .name = "Cone Mesh" }, .shadersType = GP_CONE, .data = gc->createConeMesh(2, 3, 10) }, + ReferenceObjectCpu {.meta = {.type = OT_ICOSPHERE, .name = "Icoshpere Mesh" }, .shadersType = GP_ICO, .data = gc->createIcoSphere(1, 3, true) } + }) { - buffer->setContentHash(buffer->computeContentHash()); - outShader = std::move(shader); + gc = nullptr; // one shot } - else - outShader = utilities->getLogicalDevice()->createShader(shader.get()); - return outShader; + private: + const nbl::asset::IGeometryCreator* gc; + + public: + const std::array objects; + }; + + struct Shaders + { + nbl::core::smart_refctd_ptr vertex = nullptr, fragment = nullptr; }; - typename ResourcesBundleScratch::Shaders& basic = scratch.shaders[GeometriesCpu::GP_BASIC]; + GeometriesCpu geometries(gc); + std::array shaders; + + auto& basic = shaders[GeometriesCpu::GP_BASIC]; createShader.template operator() < NBL_CORE_UNIQUE_STRING_LITERAL_TYPE("geometryCreator/spirv/gc.basic.vertex.spv") > (IShader::E_SHADER_STAGE::ESS_VERTEX, basic.vertex); createShader.template operator() < NBL_CORE_UNIQUE_STRING_LITERAL_TYPE("geometryCreator/spirv/gc.basic.fragment.spv") > (IShader::E_SHADER_STAGE::ESS_FRAGMENT, basic.fragment); - typename ResourcesBundleScratch::Shaders& cone = scratch.shaders[GeometriesCpu::GP_CONE]; + auto& cone = shaders[GeometriesCpu::GP_CONE]; createShader.template operator() < NBL_CORE_UNIQUE_STRING_LITERAL_TYPE("geometryCreator/spirv/gc.cone.vertex.spv") > (IShader::E_SHADER_STAGE::ESS_VERTEX, cone.vertex); createShader.template operator() < NBL_CORE_UNIQUE_STRING_LITERAL_TYPE("geometryCreator/spirv/gc.basic.fragment.spv") > (IShader::E_SHADER_STAGE::ESS_FRAGMENT, cone.fragment); // note we reuse fragment from basic! - typename ResourcesBundleScratch::Shaders& ico = scratch.shaders[GeometriesCpu::GP_ICO]; + auto& ico = shaders[GeometriesCpu::GP_ICO]; createShader.template operator() < NBL_CORE_UNIQUE_STRING_LITERAL_TYPE("geometryCreator/spirv/gc.ico.vertex.spv") > (IShader::E_SHADER_STAGE::ESS_VERTEX, ico.vertex); createShader.template operator() < NBL_CORE_UNIQUE_STRING_LITERAL_TYPE("geometryCreator/spirv/gc.basic.fragment.spv") > (IShader::E_SHADER_STAGE::ESS_FRAGMENT, ico.fragment); // note we reuse fragment from basic! - - for (const auto& it : scratch.shaders) + + for (const auto& it : shaders) { if (!it.vertex || !it.fragment) { - logger->log("Could not create shaders!", ILogger::ELL_ERROR); - return false; + logger->log("Could not create a shader!", ILogger::ELL_ERROR); + return nullptr; } } - return true; - } + // geometries - bool createGeometries() - { - EXPOSE_NABLA_NAMESPACES(); + auto output = make_smart_refctd_ptr(); + output->renderpass = smart_refctd_ptr(renderpass); + output->dsLayout = smart_refctd_ptr(dsLayout); for (uint32_t i = 0; i < geometries.objects.size(); ++i) { const auto& inGeometry = geometries.objects[i]; - auto& [obj, meta] = scratch.objects[i]; - - bool status = true; + auto& [obj, meta] = output->objects[i]; meta.name = inGeometry.meta.name; meta.type = inGeometry.meta.type; @@ -825,7 +339,7 @@ class ResourceBuilder { SBlendParams blend; SRasterizationParams rasterization; - typename Types::graphics_pipeline_t::SCreationParams pipeline; + IGPUGraphicsPipeline::SCreationParams pipeline; } params; { @@ -843,35 +357,27 @@ class ResourceBuilder params.rasterization.faceCullingMode = EFCM_NONE; { - const typename Types::shader_t::SSpecInfo info [] = + const IGPUShader::SSpecInfo sInfo [] = { - {.entryPoint = "VSMain", .shader = scratch.shaders[inGeometry.shadersType].vertex.get() }, - {.entryPoint = "PSMain", .shader = scratch.shaders[inGeometry.shadersType].fragment.get() } + {.entryPoint = "VSMain", .shader = shaders[inGeometry.shadersType].vertex.get() }, + {.entryPoint = "PSMain", .shader = shaders[inGeometry.shadersType].fragment.get() } }; - params.pipeline.layout = scratch.pipelineLayout.get(); - params.pipeline.shaders = info; - params.pipeline.renderpass = scratch.renderpass.get(); + params.pipeline.layout = pipelineLayout.get(); + params.pipeline.shaders = sInfo; + params.pipeline.renderpass = renderpass.get(); params.pipeline.cached = { .vertexInput = inGeometry.data.inputParams, .primitiveAssembly = inGeometry.data.assemblyParams, .rasterization = params.rasterization, .blend = params.blend, .subpassIx = 0u }; obj.indexCount = inGeometry.data.indexCount; obj.indexType = inGeometry.data.indexType; - // TODO: cache pipeline & try lookup for existing one first maybe - - // similar issue like with shaders again, in this case gpu contructor allows for extra cache parameters + there is no constructor you can use to fire make_smart_refctd_ptr yourself for cpu - if constexpr (withAssetConverter) - obj.pipeline = ICPUGraphicsPipeline::create(params.pipeline); - else - { - const std::array info = { { params.pipeline } }; - utilities->getLogicalDevice()->createGraphicsPipelines(nullptr, info, &obj.pipeline); - } + const std::array pInfo = { { params.pipeline } }; + device->createGraphicsPipelines(nullptr, pInfo, &obj.pipeline); if (!obj.pipeline) { logger->log("Could not create graphics pipeline for [%s] object!", ILogger::ELL_ERROR, meta.name.data()); - status = false; + return nullptr; } // object buffers @@ -888,71 +394,48 @@ class ResourceBuilder constexpr static auto INDEX_USAGE = bitflag(ibuffer_t::EUF_INDEX_BUFFER_BIT) | ibuffer_t::EUF_VERTEX_BUFFER_BIT | ibuffer_t::EUF_TRANSFER_DST_BIT | ibuffer_t::EUF_INLINE_UPDATE_VIA_CMDBUF; obj.bindings.index.offset = 0u; - if constexpr (withAssetConverter) - { - if (!vBuffer) - return false; - - vBuffer->addUsageFlags(VERTEX_USAGE); - vBuffer->setContentHash(vBuffer->computeContentHash()); - obj.bindings.vertex = { .offset = 0u, .buffer = vBuffer }; - - if (inGeometry.data.indexType != EIT_UNKNOWN) - if (iBuffer) - { - iBuffer->addUsageFlags(INDEX_USAGE); - iBuffer->setContentHash(iBuffer->computeContentHash()); - } - else - return false; + auto vertexBuffer = device->createBuffer(IGPUBuffer::SCreationParams({ .size = vBuffer->getSize(), .usage = VERTEX_USAGE })); + auto indexBuffer = iBuffer ? device->createBuffer(IGPUBuffer::SCreationParams({ .size = iBuffer->getSize(), .usage = INDEX_USAGE })) : nullptr; - obj.bindings.index = { .offset = 0u, .buffer = iBuffer }; - } - else - { - auto vertexBuffer = utilities->getLogicalDevice()->createBuffer(IGPUBuffer::SCreationParams({ .size = vBuffer->getSize(), .usage = VERTEX_USAGE })); - auto indexBuffer = iBuffer ? utilities->getLogicalDevice()->createBuffer(IGPUBuffer::SCreationParams({ .size = iBuffer->getSize(), .usage = INDEX_USAGE })) : nullptr; + if (!vertexBuffer) + return false; - if (!vertexBuffer) + if (inGeometry.data.indexType != EIT_UNKNOWN) + if (!indexBuffer) return false; - if (inGeometry.data.indexType != EIT_UNKNOWN) - if (!indexBuffer) - return false; - - const auto mask = utilities->getLogicalDevice()->getPhysicalDevice()->getUpStreamingMemoryTypeBits(); - for (auto it : { vertexBuffer , indexBuffer }) + const auto mask = device->getPhysicalDevice()->getUpStreamingMemoryTypeBits(); + for (auto it : { vertexBuffer , indexBuffer }) + { + if (it) { - if (it) - { - auto reqs = it->getMemoryReqs(); - reqs.memoryTypeBits &= mask; + auto reqs = it->getMemoryReqs(); + reqs.memoryTypeBits &= mask; - utilities->getLogicalDevice()->allocate(reqs, it.get()); - } + device->allocate(reqs, it.get()); } + } - // record transfer uploads - obj.bindings.vertex = { .offset = 0u, .buffer = std::move(vertexBuffer) }; + // record transfer uploads + obj.bindings.vertex = { .offset = 0u, .buffer = std::move(vertexBuffer) }; + { + const SBufferRange range = { .offset = obj.bindings.vertex.offset, .size = obj.bindings.vertex.buffer->getSize(), .buffer = obj.bindings.vertex.buffer }; + if (!cmd->updateBuffer(range, vBuffer->getPointer())) { - const SBufferRange range = { .offset = obj.bindings.vertex.offset, .size = obj.bindings.vertex.buffer->getSize(), .buffer = obj.bindings.vertex.buffer }; - if (!commandBuffer->updateBuffer(range, vBuffer->getPointer())) - { - logger->log("Could not record vertex buffer transfer upload for [%s] object!", ILogger::ELL_ERROR, meta.name.data()); - status = false; - } + logger->log("Could not record vertex buffer transfer upload for [%s] object!", ILogger::ELL_ERROR, meta.name.data()); + return false; } - obj.bindings.index = { .offset = 0u, .buffer = std::move(indexBuffer) }; + } + obj.bindings.index = { .offset = 0u, .buffer = std::move(indexBuffer) }; + { + if (iBuffer) { - if (iBuffer) - { - const SBufferRange range = { .offset = obj.bindings.index.offset, .size = obj.bindings.index.buffer->getSize(), .buffer = obj.bindings.index.buffer }; + const SBufferRange range = { .offset = obj.bindings.index.offset, .size = obj.bindings.index.buffer->getSize(), .buffer = obj.bindings.index.buffer }; - if (!commandBuffer->updateBuffer(range, iBuffer->getPointer())) - { - logger->log("Could not record index buffer transfer upload for [%s] object!", ILogger::ELL_ERROR, meta.name.data()); - status = false; - } + if (!cmd->updateBuffer(range, iBuffer->getPointer())) + { + logger->log("Could not record index buffer transfer upload for [%s] object!", ILogger::ELL_ERROR, meta.name.data()); + return false; } } } @@ -963,209 +446,289 @@ class ResourceBuilder if (!createVIBuffers()) { logger->log("Could not create buffers for [%s] object!", ILogger::ELL_ERROR, meta.name.data()); - status = false; + return nullptr; } + } + } - if (!status) - { - logger->log("[%s] object will not be created!", ILogger::ELL_ERROR, meta.name.data()); + cmd->end(); + + // submit + { + std::array commandBuffers = {}; + { + commandBuffers.front().cmdbuf = cmd.get(); + } - obj.bindings.vertex = {}; - obj.bindings.index = {}; - obj.indexCount = 0u; - obj.indexType = E_INDEX_TYPE::EIT_UNKNOWN; - obj.pipeline = nullptr; + auto completed = device->createSemaphore(0u); + + std::array signals; + { + auto& signal = signals.front(); + signal.value = 1; + signal.stageMask = bitflag(PIPELINE_STAGE_FLAGS::ALL_TRANSFER_BITS); + signal.semaphore = completed.get(); + } - continue; + const IQueue::SSubmitInfo infos[] = + { + { + .waitSemaphores = {}, + .commandBuffers = commandBuffers, + .signalSemaphores = signals } + }; + + if (transferCapableQueue->submit(infos) != IQueue::RESULT::SUCCESS) + { + logger->log("Failed to submit transfer upload operations!", ILogger::ELL_ERROR); + return nullptr; } + + const ISemaphore::SWaitInfo info[] = + { { + .semaphore = completed.get(), + .value = 1 + } }; + + device->blockForSemaphores(info); } - return true; + return output; } +}; + +struct ObjectInstance +{ + nbl::hlsl::float32_t3x4 model = nbl::hlsl::float32_t3x4(1.f); + nbl::asset::SBasicViewParameters viewParameters; + ObjectMeta meta; +}; - bool createViewParametersUboBuffer() +class CScene final : public nbl::core::IReferenceCounted +{ +public: + ObjectInstance object; // optional TODO: MDI, allow for multiple objects on the scene -> read (*) bellow at private class members + + struct + { + static constexpr uint32_t startedValue = 0, finishedValue = 0x45; + nbl::core::smart_refctd_ptr progress; + } semaphore; + + static inline nbl::core::smart_refctd_ptr create(nbl::core::smart_refctd_ptr device, nbl::core::smart_refctd_ptr logger, nbl::video::CThreadSafeQueueAdapter* const transferCapableQueue, const nbl::core::smart_refctd_ptr resources) { EXPOSE_NABLA_NAMESPACES(); - using ibuffer_t = ::nbl::asset::IBuffer; // seems to be ambigous, both asset & core namespaces has IBuffer - constexpr static auto UboUsage = bitflag(ibuffer_t::EUF_UNIFORM_BUFFER_BIT) | ibuffer_t::EUF_TRANSFER_DST_BIT | ibuffer_t::EUF_INLINE_UPDATE_VIA_CMDBUF; + if (!device) + return nullptr; - if constexpr (withAssetConverter) - { - auto uboBuffer = make_smart_refctd_ptr(sizeof(SBasicViewParameters)); - uboBuffer->addUsageFlags(UboUsage); - uboBuffer->setContentHash(uboBuffer->computeContentHash()); - scratch.ubo = { .offset = 0u, .buffer = std::move(uboBuffer) }; - } - else - { - const auto mask = utilities->getLogicalDevice()->getPhysicalDevice()->getUpStreamingMemoryTypeBits(); - auto uboBuffer = utilities->getLogicalDevice()->createBuffer(IGPUBuffer::SCreationParams({ .size = sizeof(SBasicViewParameters), .usage = UboUsage })); + if (!logger) + return nullptr; - if (!uboBuffer) - return false; + if (!transferCapableQueue) + return nullptr; - for (auto it : { uboBuffer }) - { - IDeviceMemoryBacked::SDeviceMemoryRequirements reqs = it->getMemoryReqs(); - reqs.memoryTypeBits &= mask; + if (!resources) + return nullptr; - utilities->getLogicalDevice()->allocate(reqs, it.get()); - } + // cmd - scratch.ubo = { .offset = 0u, .buffer = std::move(uboBuffer) }; + auto cPool = device->createCommandPool(transferCapableQueue->getFamilyIndex(), IGPUCommandPool::CREATE_FLAGS::RESET_COMMAND_BUFFER_BIT); + + if (!cPool) + { + logger->log("Couldn't create command pool!", ILogger::ELL_ERROR); + return nullptr; } - return true; - } + nbl::core::smart_refctd_ptr cmd; - struct GeometriesCpu - { - enum GeometryShader + if (!cPool->createCommandBuffers(IGPUCommandPool::BUFFER_LEVEL::PRIMARY, { &cmd , 1 })) { - GP_BASIC = 0, - GP_CONE, - GP_ICO, + logger->log("Couldn't create command buffer!", ILogger::ELL_ERROR); + return nullptr; + } - GP_COUNT - }; + if (!cmd) + return nullptr; - struct ReferenceObjectCpu - { - ObjectMeta meta; - GeometryShader shadersType; - nbl::asset::CGeometryCreator::return_type data; - }; + // UBO with basic view parameters + + using ibuffer_t = ::nbl::asset::IBuffer; + constexpr static auto UboUsage = bitflag(ibuffer_t::EUF_UNIFORM_BUFFER_BIT) | ibuffer_t::EUF_TRANSFER_DST_BIT | ibuffer_t::EUF_INLINE_UPDATE_VIA_CMDBUF; + + const auto mask = device->getPhysicalDevice()->getUpStreamingMemoryTypeBits(); + auto uboBuffer = device->createBuffer(IGPUBuffer::SCreationParams({ .size = sizeof(SBasicViewParameters), .usage = UboUsage })); + + if (!uboBuffer) + logger->log("Could not create UBO!", ILogger::ELL_ERROR); - GeometriesCpu(const nbl::asset::IGeometryCreator* _gc) - : gc(_gc), - objects - ({ - ReferenceObjectCpu {.meta = {.type = OT_CUBE, .name = "Cube Mesh" }, .shadersType = GP_BASIC, .data = gc->createCubeMesh(nbl::core::vector3df(1.f, 1.f, 1.f)) }, - ReferenceObjectCpu {.meta = {.type = OT_SPHERE, .name = "Sphere Mesh" }, .shadersType = GP_BASIC, .data = gc->createSphereMesh(2, 16, 16) }, - ReferenceObjectCpu {.meta = {.type = OT_CYLINDER, .name = "Cylinder Mesh" }, .shadersType = GP_BASIC, .data = gc->createCylinderMesh(2, 2, 20) }, - ReferenceObjectCpu {.meta = {.type = OT_RECTANGLE, .name = "Rectangle Mesh" }, .shadersType = GP_BASIC, .data = gc->createRectangleMesh(nbl::core::vector2df_SIMD(1.5, 3)) }, - ReferenceObjectCpu {.meta = {.type = OT_DISK, .name = "Disk Mesh" }, .shadersType = GP_BASIC, .data = gc->createDiskMesh(2, 30) }, - ReferenceObjectCpu {.meta = {.type = OT_ARROW, .name = "Arrow Mesh" }, .shadersType = GP_BASIC, .data = gc->createArrowMesh() }, - ReferenceObjectCpu {.meta = {.type = OT_CONE, .name = "Cone Mesh" }, .shadersType = GP_CONE, .data = gc->createConeMesh(2, 3, 10) }, - ReferenceObjectCpu {.meta = {.type = OT_ICOSPHERE, .name = "Icoshpere Mesh" }, .shadersType = GP_ICO, .data = gc->createIcoSphere(1, 3, true) } - }) + for (auto it : { uboBuffer }) { - gc = nullptr; // one shot + IDeviceMemoryBacked::SDeviceMemoryRequirements reqs = it->getMemoryReqs(); + reqs.memoryTypeBits &= mask; + + device->allocate(reqs, it.get()); } - private: - const nbl::asset::IGeometryCreator* gc; + nbl::asset::SBufferBinding ubo = { .offset = 0u, .buffer = std::move(uboBuffer) }; - public: - const std::array objects; - }; + // descriptor set for the resource + + const IGPUDescriptorSetLayout* const layouts[] = { resources->dsLayout.get() }; + const uint32_t setCounts[] = { 1u }; - using resources_bundle_base_t = ResourcesBundleBase; + // note descriptor set has back smart pointer to its pool, so we dont need to keep it explicitly + auto dPool = device->createDescriptorPoolForDSLayouts(IDescriptorPool::E_CREATE_FLAGS::ECF_NONE, layouts, setCounts); - struct ResourcesBundleScratch : public resources_bundle_base_t - { - using Types = resources_bundle_base_t::Types; + if (!dPool) + { + logger->log("Could not create Descriptor Pool!", ILogger::ELL_ERROR); + return nullptr; + } - ResourcesBundleScratch() - : resources_bundle_base_t() {} + nbl::core::smart_refctd_ptr ds; + dPool->createDescriptorSets(layouts, &ds); - struct Shaders + if (!ds) { - nbl::core::smart_refctd_ptr vertex = nullptr, fragment = nullptr; - }; + logger->log("Could not create Descriptor Set!", ILogger::ELL_ERROR); + return nullptr; + } - nbl::core::smart_refctd_ptr descriptorSetLayout; - nbl::core::smart_refctd_ptr pipelineLayout; - std::array shaders; - }; + // write the descriptor set + { + // descriptor write ubo + IGPUDescriptorSet::SWriteDescriptorSet write; + write.dstSet = ds.get(); + write.binding = 0; + write.arrayElement = 0u; + write.count = 1u; - // TODO: we could make those params templated with default values like below - static constexpr auto FramebufferW = 1280u, FramebufferH = 720u; - static constexpr auto ColorFboAttachmentFormat = nbl::asset::EF_R8G8B8A8_SRGB, DepthFboAttachmentFormat = nbl::asset::EF_D16_UNORM; - static constexpr auto Samples = nbl::video::IGPUImage::ESCF_1_BIT; + IGPUDescriptorSet::SDescriptorInfo info; + { + info.desc = smart_refctd_ptr(ubo.buffer); + info.info.buffer.offset = ubo.offset; + info.info.buffer.size = ubo.buffer->getSize(); + } - ResourcesBundleScratch scratch; + write.info = &info; - nbl::video::IUtilities* const utilities; - nbl::video::IGPUCommandBuffer* const commandBuffer; - nbl::system::ILogger* const logger; - GeometriesCpu geometries; -}; + if (!device->updateDescriptorSets(1u, &write, 0u, nullptr)) + { + logger->log("Could not write descriptor set!", ILogger::ELL_ERROR); + return nullptr; + } + } -#undef TYPES_IMPL_BOILERPLATE + // color & depth attachments + + auto createImageView = [&](smart_refctd_ptr&outView) -> smart_refctd_ptr + { + constexpr bool IS_DEPTH = isDepthOrStencilFormat(); + constexpr auto USAGE = [](const bool isDepth) + { + bitflag usage = IGPUImage::EUF_RENDER_ATTACHMENT_BIT; -struct ObjectDrawHookCpu -{ - nbl::hlsl::float32_t3x4 model = nbl::hlsl::float32_t3x4(1.f); - nbl::asset::SBasicViewParameters viewParameters; - ObjectMeta meta; -}; + if (!isDepth) + usage |= IGPUImage::EUF_SAMPLED_BIT; -/* - Rendering to offline framebuffer which we don't present, color - scene attachment texture we use for second UI renderpass - sampling it & rendering into desired GUI area. + return usage; + }(IS_DEPTH); + constexpr auto ASPECT = IS_DEPTH ? IImage::E_ASPECT_FLAGS::EAF_DEPTH_BIT : IImage::E_ASPECT_FLAGS::EAF_COLOR_BIT; + constexpr std::string_view DEBUG_NAME = IS_DEPTH ? "GC Scene Depth Attachment Image" : "GC Scene Color Attachment Image"; + { + smart_refctd_ptr image; + { + auto params = IGPUImage::SCreationParams( + { + .type = IGPUImage::ET_2D, + .samples = Traits::Samples, + .format = format, + .extent = { Traits::FramebufferW, Traits::FramebufferH, 1u }, + .mipLevels = 1u, + .arrayLayers = 1u, + .usage = USAGE + }); + + image = device->createImage(std::move(params)); + } - The scene can be created from simple geometry - using our Geomtry Creator class. -*/ + if (!image) + { + logger->log("Could not create image!", ILogger::ELL_ERROR); + } -class CScene final : public nbl::core::IReferenceCounted -{ -public: - ObjectDrawHookCpu object; // TODO: this could be a vector (to not complicate the example I leave it single object), we would need a better system for drawing then to make only 1 max 2 indirect draw calls (indexed and not indexed objects) + image->setObjectDebugName(DEBUG_NAME.data()); - struct - { - const uint32_t startedValue = 0, finishedValue = 0x45; - nbl::core::smart_refctd_ptr progress; - } semaphore; + if (!device->allocate(image->getMemoryReqs(), image.get()).isValid()) + { + logger->log("Could not allocate memory for an image!", ILogger::ELL_ERROR); + return nullptr; + } - struct CreateResourcesDirectlyWithDevice { using Builder = ResourceBuilder; }; - struct CreateResourcesWithAssetConverter { using Builder = ResourceBuilder; }; + auto params = IGPUImageView::SCreationParams + ({ + .flags = IGPUImageView::ECF_NONE, + .subUsages = USAGE, + .image = std::move(image), + .viewType = IGPUImageView::ET_2D, + .format = format, + .subresourceRange = {.aspectMask = ASPECT, .baseMipLevel = 0u, .levelCount = 1u, .baseArrayLayer = 0u, .layerCount = 1u } + }); - ~CScene() {} + outView = device->createImageView(std::move(params)); - static inline nbl::core::smart_refctd_ptr createCommandBuffer(nbl::video::ILogicalDevice* const device, nbl::system::ILogger* const logger, const uint32_t familyIx) - { - EXPOSE_NABLA_NAMESPACES(); - auto pool = device->createCommandPool(familyIx, IGPUCommandPool::CREATE_FLAGS::RESET_COMMAND_BUFFER_BIT); + if (!outView) + { + logger->log("Could not create image view!", ILogger::ELL_ERROR); + return nullptr; + } - if (!pool) - { - logger->log("Couldn't create Command Pool!", ILogger::ELL_ERROR); - return nullptr; - } + return smart_refctd_ptr(outView); + } + }; - nbl::core::smart_refctd_ptr cmd; + nbl::core::smart_refctd_ptr color, depth; + const bool allocated = createImageView.template operator() < Traits::ColorFboAttachmentFormat > (color) && createImageView.template operator() < Traits::DepthFboAttachmentFormat > (depth); - if (!pool->createCommandBuffers(IGPUCommandPool::BUFFER_LEVEL::PRIMARY, { &cmd , 1 })) + if (!allocated) { - logger->log("Couldn't create Command Buffer!", ILogger::ELL_ERROR); + logger->log("Could not allocate frame buffer's attachments!", ILogger::ELL_ERROR); return nullptr; } - return cmd; - } + //! frame buffer + + const auto extent = color->getCreationParameters().image->getCreationParameters().extent; - template - static auto create(Args&&... args) -> decltype(auto) - { - EXPOSE_NABLA_NAMESPACES(); + IGPUFramebuffer::SCreationParams params = + { + { + .renderpass = smart_refctd_ptr((IGPURenderpass*)resources->renderpass.get()), // NOTE: those creation params are to be corrected & this should take immutable renderpass (smart pointer OK but take const for type) + .depthStencilAttachments = &depth.get(), + .colorAttachments = &color.get(), + .width = extent.width, + .height = extent.height, + .layers = 1u + } + }; - /* - user should call the constructor's args without last argument explicitly, this is a trick to make constructor templated, - eg.create(smart_refctd_ptr(device), smart_refctd_ptr(logger), queuePointer, geometryPointer) - */ + auto frameBuffer = device->createFramebuffer(std::move(params)); - auto* scene = new CScene(std::forward(args)..., CreateWith {}); - smart_refctd_ptr smart(scene, dont_grab); + if (!frameBuffer) + { + logger->log("Could not create frame buffer!", ILogger::ELL_ERROR); + return nullptr; + } - return smart; + auto output = new CScene(smart_refctd_ptr(device), smart_refctd_ptr(logger), smart_refctd_ptr(cmd), smart_refctd_ptr(frameBuffer), smart_refctd_ptr(ds), ubo, smart_refctd_ptr(color), smart_refctd_ptr(depth), smart_refctd_ptr(resources)); + return smart_refctd_ptr(output); } + ~CScene() {} + inline void begin() { EXPOSE_NABLA_NAMESPACES(); @@ -1174,13 +737,15 @@ class CScene final : public nbl::core::IReferenceCounted m_commandBuffer->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT); m_commandBuffer->beginDebugMarker("UISampleApp Offline Scene Frame"); - semaphore.progress = m_utilities->getLogicalDevice()->createSemaphore(semaphore.startedValue); + semaphore.progress = m_device->createSemaphore(semaphore.startedValue); } - inline void record() + inline bool record() { EXPOSE_NABLA_NAMESPACES(); + bool valid = true; + const struct { const uint32_t width, height; @@ -1196,12 +761,13 @@ class CScene final : public nbl::core::IReferenceCounted viewport.height = fbo.height; } - m_commandBuffer->setViewport(0u, 1u, &viewport); + valid &= m_commandBuffer->setViewport(0u, 1u, &viewport); VkRect2D scissor = {}; scissor.offset = { 0, 0 }; scissor.extent = { fbo.width, fbo.height }; - m_commandBuffer->setScissor(0u, 1u, &scissor); + + valid &= m_commandBuffer->setScissor(0u, 1u, &scissor); const VkRect2D renderArea = { @@ -1212,31 +778,33 @@ class CScene final : public nbl::core::IReferenceCounted const IGPUCommandBuffer::SRenderpassBeginInfo info = { .framebuffer = m_frameBuffer.get(), - .colorClearValues = &clear.color, - .depthStencilClearValues = &clear.depth, + .colorClearValues = &Traits::clearColor, + .depthStencilClearValues = &Traits::clearDepth, .renderArea = renderArea }; - m_commandBuffer->beginRenderPass(info, IGPUCommandBuffer::SUBPASS_CONTENTS::INLINE); + valid &= m_commandBuffer->beginRenderPass(info, IGPUCommandBuffer::SUBPASS_CONTENTS::INLINE); - const auto& [hook, meta] = resources.objects[object.meta.type]; - auto* rawPipeline = hook.pipeline.get(); + const auto& [hook, meta] = m_resources->objects[object.meta.type]; + const auto* rawPipeline = hook.pipeline.get(); SBufferBinding vertex = hook.bindings.vertex, index = hook.bindings.index; - m_commandBuffer->bindGraphicsPipeline(rawPipeline); - m_commandBuffer->bindDescriptorSets(EPBP_GRAPHICS, rawPipeline->getLayout(), 1, 1, &resources.descriptorSet.get()); - m_commandBuffer->bindVertexBuffers(0, 1, &vertex); + valid &= m_commandBuffer->bindGraphicsPipeline(rawPipeline); + valid &= m_commandBuffer->bindDescriptorSets(EPBP_GRAPHICS, rawPipeline->getLayout(), 1, 1, &m_ds.get()); + valid &= m_commandBuffer->bindVertexBuffers(0, 1, &vertex); if (index.buffer && hook.indexType != EIT_UNKNOWN) { - m_commandBuffer->bindIndexBuffer(index, hook.indexType); - m_commandBuffer->drawIndexed(hook.indexCount, 1, 0, 0, 0); + valid &= m_commandBuffer->bindIndexBuffer(index, hook.indexType); + valid &= m_commandBuffer->drawIndexed(hook.indexCount, 1, 0, 0, 0); } else - m_commandBuffer->draw(hook.indexCount, 1, 0, 0); + valid &= m_commandBuffer->draw(hook.indexCount, 1, 0, 0); + + valid &= m_commandBuffer->endRenderPass(); - m_commandBuffer->endRenderPass(); + return valid; } inline void end() @@ -1244,10 +812,13 @@ class CScene final : public nbl::core::IReferenceCounted m_commandBuffer->end(); } - inline bool submit() + inline bool submit(nbl::video::CThreadSafeQueueAdapter* queue) { EXPOSE_NABLA_NAMESPACES(); + if (!queue) + return false; + const IQueue::SSubmitInfo::SCommandBufferInfo buffers[] = { { .cmdbuf = m_commandBuffer.get() } @@ -1267,78 +838,40 @@ class CScene final : public nbl::core::IReferenceCounted return queue->submit(infos) == IQueue::RESULT::SUCCESS; } - // note: must be updated outside render pass inline void update() { EXPOSE_NABLA_NAMESPACES(); SBufferRange range; - range.buffer = smart_refctd_ptr(resources.ubo.buffer); - range.size = resources.ubo.buffer->getSize(); + range.buffer = smart_refctd_ptr(m_ubo.buffer); + range.size = m_ubo.buffer->getSize(); m_commandBuffer->updateBuffer(range, &object.viewParameters); } - inline decltype(auto) getResources() - { - return (resources); // note: do not remove "()" - it makes the return type lvalue reference instead of copy - } + inline auto getColorAttachment() { return nbl::core::smart_refctd_ptr(m_color); } private: - template // TODO: enforce constraints, only those 2 above are valid - CScene(nbl::core::smart_refctd_ptr _utilities, nbl::core::smart_refctd_ptr _logger, nbl::video::CThreadSafeQueueAdapter* _graphicsQueue, const nbl::asset::IGeometryCreator* _geometryCreator, CreateWith createWith = {}) - : m_utilities(nbl::core::smart_refctd_ptr(_utilities)), m_logger(nbl::core::smart_refctd_ptr(_logger)), queue(_graphicsQueue) - { - EXPOSE_NABLA_NAMESPACES(); - using Builder = typename CreateWith::Builder; - - m_commandBuffer = createCommandBuffer(m_utilities->getLogicalDevice(), m_utilities->getLogger(), queue->getFamilyIndex()); - Builder builder(m_utilities.get(), m_commandBuffer.get(), m_logger.get(), _geometryCreator); - - // gpu resources - if (builder.build()) - { - if (!builder.finalize(resources, queue)) - m_logger->log("Could not finalize resource objects to gpu objects!", ILogger::ELL_ERROR); - } - else - m_logger->log("Could not build resource objects!", ILogger::ELL_ERROR); - - // frame buffer - { - const auto extent = resources.attachments.color->getCreationParameters().image->getCreationParameters().extent; - - IGPUFramebuffer::SCreationParams params = - { - { - .renderpass = smart_refctd_ptr(resources.renderpass), - .depthStencilAttachments = &resources.attachments.depth.get(), - .colorAttachments = &resources.attachments.color.get(), - .width = extent.width, - .height = extent.height, - .layers = 1u - } - }; - - m_frameBuffer = m_utilities->getLogicalDevice()->createFramebuffer(std::move(params)); - - if (!m_frameBuffer) - { - m_logger->log("Could not create frame buffer!", ILogger::ELL_ERROR); - return; - } - } - } + CScene(nbl::core::smart_refctd_ptr device, nbl::core::smart_refctd_ptr logger, nbl::core::smart_refctd_ptr commandBuffer, nbl::core::smart_refctd_ptr frameBuffer, nbl::core::smart_refctd_ptr ds, nbl::asset::SBufferBinding ubo, nbl::core::smart_refctd_ptr color, nbl::core::smart_refctd_ptr depth, const nbl::core::smart_refctd_ptr resources) + : m_device(nbl::core::smart_refctd_ptr(device)), m_logger(nbl::core::smart_refctd_ptr(logger)), m_commandBuffer(nbl::core::smart_refctd_ptr(commandBuffer)), m_frameBuffer(nbl::core::smart_refctd_ptr(frameBuffer)), m_ds(nbl::core::smart_refctd_ptr(ds)), m_ubo(ubo), m_color(nbl::core::smart_refctd_ptr(color)), m_depth(nbl::core::smart_refctd_ptr(depth)), m_resources(nbl::core::smart_refctd_ptr(resources)) {} - nbl::core::smart_refctd_ptr m_utilities; + nbl::core::smart_refctd_ptr m_device; nbl::core::smart_refctd_ptr m_logger; - nbl::video::CThreadSafeQueueAdapter* queue; - nbl::core::smart_refctd_ptr m_commandBuffer; + //! (*) I still make an assumption we have only one object on the scene, + //! I'm not going to make it ext-like and go with MDI + streaming buffer now, + //! but I want it to be easy to spam multiple instances of this class to have many + //! frame buffers we can render too given resource geometry buffers with Traits constraints + //! + //! optional TODO: make it ImGUI-ext-like -> renderpass as creation input, ST buffer, MDI, ds outside - nbl::core::smart_refctd_ptr m_frameBuffer; + nbl::core::smart_refctd_ptr m_commandBuffer = nullptr; + nbl::core::smart_refctd_ptr m_frameBuffer = nullptr; + nbl::core::smart_refctd_ptr m_ds = nullptr; + nbl::asset::SBufferBinding m_ubo = {}; + nbl::core::smart_refctd_ptr m_color = nullptr, m_depth = nullptr; - ResourcesBundle resources; + const nbl::core::smart_refctd_ptr m_resources; }; } // nbl::scene::geometrycreator diff --git a/common/include/ICamera.hpp b/common/include/ICamera.hpp index 7488a2af6..db5cf969d 100644 --- a/common/include/ICamera.hpp +++ b/common/include/ICamera.hpp @@ -11,10 +11,10 @@ namespace nbl::hlsl // TODO: DIFFERENT NAMESPACE { template -class ICamera : public IGimbalManipulateEncoder, virtual public core::IReferenceCounted +class ICamera : public IGimbalController, virtual public core::IReferenceCounted { public: - using IGimbalManipulateEncoder::IGimbalManipulateEncoder; + using IGimbalController::IGimbalController; using precision_t = T; //! Manipulation mode for virtual events diff --git a/common/include/camera/CCameraController.hpp b/common/include/camera/CCameraController.hpp deleted file mode 100644 index c858c00ee..000000000 --- a/common/include/camera/CCameraController.hpp +++ /dev/null @@ -1,90 +0,0 @@ -#ifndef _NBL_C_CAMERA_CONTROLLER_HPP_ -#define _NBL_C_CAMERA_CONTROLLER_HPP_ - -#include "IGimbalController.hpp" -#include "CGeneralPurposeGimbal.hpp" -#include "ICamera.hpp" - -// TODO: DIFFERENT NAMESPACE -namespace nbl::hlsl -{ - //! Controls any type of camera with available controllers using virtual gimbal events in Local mode - template - class CCameraController final : public IGimbalController - { - public: - using IGimbalController::IGimbalController; - using precision_t = T; - - using interface_camera_t = ICamera; - using interface_gimbal_t = IGimbal; - - CCameraController(core::smart_refctd_ptr camera) - : m_camera(std::move(camera)), m_target(interface_gimbal_t::SCreationParameters({ .position = m_camera->getGimbal().getWorldTarget() })) {} - ~CCameraController() {} - - const keyboard_to_virtual_events_t& getKeyboardMappingPreset() const override { return m_camera->getKeyboardMappingPreset(); } - const mouse_to_virtual_events_t& getMouseMappingPreset() const override { return m_camera->getMouseMappingPreset(); } - const imguizmo_to_virtual_events_t& getImguizmoMappingPreset() const override { return m_camera->getImguizmoMappingPreset(); } - - //! Manipulate the camera view gimbal by requesting a manipulation to its world target represented by a target gimbal, - //! on success it may change both the camera view gimbal & target gimbal - bool manipulateTargetGimbal(SUpdateParameters parameters, std::chrono::microseconds nextPresentationTimestamp) - { - // TODO & note to self: - // thats a little bit tricky -> a request of m_target manipulation which represents camera world target is a step where we consider only change of its - // position and translate that onto virtual orientation events we fire camera->manipulate with. Note that *we can fail* the manipulation because each - // camera type has some constraints on how it works right, however.. if any manipulation happens it means "target vector" changes and it doesn't matter - // what camera type is bound to the camera controller! If so, on success we can simply update m_target gimbal accordingly and represent it nicely on the - // screen with gizmo (as we do for camera view gimbal in Drag & Drop mode) or whatever, otherwise we do nothing because it means we failed the gimbal view - // manipulation hence "target vector" did not change (its really the orientation which changes right, but an orientation change means target vector changes) - - // and whats nice is we can do it with ANY controller now - } - - //! Manipulate the camera view gimbal directly, - //! on success it may change both the camera view gimbal & target gimbal - bool manipulateViewGimbal(SUpdateParameters parameters, std::chrono::microseconds nextPresentationTimestamp) - { - // TODO & note to self: - // and here there is a small difference because we don't map any target gimbal position change to virtual orientation events to request camera view - // gimbal manipulation but we directly try to manipulate the view gimbal of our camera and if we success then we simply update our m_target gimbal accordingly - - // and whats nice is we can do it with ANY controller now - - std::vector virtualEvents(0x45); - uint32_t vCount; - - beginInputProcessing(nextPresentationTimestamp); - { - process(nullptr, vCount); - - if (virtualEvents.size() < vCount) - virtualEvents.resize(vCount); - - process(virtualEvents.data(), vCount, parameters); - } - endInputProcessing(); - - bool manipulated = m_camera->manipulate({ virtualEvents.data(), vCount }, interface_camera_t::Local); - - if (manipulated) - { - // TODO: *any* manipulate success? -> update m_target - } - - return manipulated; - } - - inline const interface_camera_t* getCamera() { return m_camera.get(); } - - private: - core::smart_refctd_ptr m_camera; - - //! Represents the camera world target vector as gimbal we can manipulate - CGeneralPurposeGimbal m_target; - }; - -} // nbl::hlsl namespace - -#endif // _NBL_C_CAMERA_CONTROLLER_HPP_ diff --git a/common/include/camera/IGimbalController.hpp b/common/include/camera/IGimbalController.hpp index a1201a7c1..1aae6617b 100644 --- a/common/include/camera/IGimbalController.hpp +++ b/common/include/camera/IGimbalController.hpp @@ -86,7 +86,7 @@ struct IGimbalManipulateEncoder virtual const imguizmo_to_virtual_events_t& getImguizmoMappingPreset() const = 0u; }; -class IGimbalController : public IGimbalManipulateEncoder, virtual public core::IReferenceCounted +class IGimbalController : public IGimbalManipulateEncoder { public: using IGimbalManipulateEncoder::IGimbalManipulateEncoder; From f0d82b340fb16db6e2f6277cb247ecef07977cf8 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Mon, 18 Nov 2024 16:34:37 +0100 Subject: [PATCH 039/205] display second gizmo on the scene (and make it capable of manipulations) , prepare for imguizmo gimbal controller which I will use to manipulate secondary camera from the first camera's perspective --- 61_UI/main.cpp | 358 +++++++++++++++++++++++++------------------------ 1 file changed, 184 insertions(+), 174 deletions(-) diff --git a/61_UI/main.cpp b/61_UI/main.cpp index 7ee57ca3a..91576820b 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -469,8 +469,8 @@ class UISampleApp final : public examples::SimpleWindowedApplication TESTS, TODO: remove all once finished work & integrate with the example properly */ - transformParams.aspectRatio = float(m_window->getWidth()) / float(m_window->getHeight()); - transformParams.invAspectRatio = float(m_window->getHeight()) / float(m_window->getWidth()); + aspectRatio = float(m_window->getWidth()) / float(m_window->getHeight()); + invAspectRatio = float(m_window->getHeight()) / float(m_window->getWidth()); if (base_t::argv.size() >= 3 && argv[1] == "-timeout_seconds") timeout = std::chrono::seconds(std::atoi(argv[2].c_str())); @@ -816,8 +816,8 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGui::SetNextWindowSize(ImVec2(320, 340), ImGuiCond_Appearing); ImGui::Begin("Editor"); - if (ImGui::RadioButton("Full view", !transformParams.useWindow)) - transformParams.useWindow = false; + if (ImGui::RadioButton("Full view", !useWindow)) + useWindow = false; // TODO: I need this logic per viewport we will render a scene from a point of view of its bound camera { @@ -825,13 +825,13 @@ class UISampleApp final : public examples::SimpleWindowedApplication if (isPerspective) { if (isLH) - projection->setMatrix(buildProjectionMatrixPerspectiveFovLH(glm::radians(fov), transformParams.aspectRatio, zNear, zFar)); + projection->setMatrix(buildProjectionMatrixPerspectiveFovLH(glm::radians(fov), aspectRatio, zNear, zFar)); else - projection->setMatrix(buildProjectionMatrixPerspectiveFovRH(glm::radians(fov), transformParams.aspectRatio, zNear, zFar)); + projection->setMatrix(buildProjectionMatrixPerspectiveFovRH(glm::radians(fov), aspectRatio, zNear, zFar)); } else { - float viewHeight = viewWidth * transformParams.invAspectRatio; + float viewHeight = viewWidth * invAspectRatio; if (isLH) projection->setMatrix(buildProjectionMatrixOrthoLH(viewWidth, viewHeight, zNear, zFar)); @@ -842,12 +842,11 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGui::SameLine(); - if (ImGui::RadioButton("Window", transformParams.useWindow)) - transformParams.useWindow = true; + if (ImGui::RadioButton("Window", useWindow)) + useWindow = true; ImGui::Text("Camera"); - bool viewDirty = false; - + if (ImGui::RadioButton("LH", isLH)) isLH = true; @@ -864,7 +863,6 @@ class UISampleApp final : public examples::SimpleWindowedApplication 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); @@ -879,22 +877,6 @@ class UISampleApp final : public examples::SimpleWindowedApplication 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); - - if (viewDirty || firstFrame) - { - float32_t3 cameraPosition(cosf(camYAngle) * cosf(camXAngle) * transformParams.camDistance, sinf(camXAngle) * transformParams.camDistance, sinf(camYAngle) * cosf(camXAngle) * transformParams.camDistance); - float32_t3 cameraTarget(0.f, 0.f, 0.f); - - // TODO: lets generate events and make it - // happen purely on gimbal manipulation! - - //camera->getGimbal()->setPosition(cameraPosition); - //camera->getGimbal()->setTarget(cameraTarget); - - firstFrame = false; - } - ImGui::Text("X: %f Y: %f", io.MousePos.x, io.MousePos.y); if (ImGuizmo::IsUsing()) { @@ -972,154 +954,30 @@ class UISampleApp final : public examples::SimpleWindowedApplication 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; + // imguizmo manipulations { - 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; - auto* cameraView = &imguizmoM16InOut.view[0][0]; auto* cameraProjection = &imguizmoM16InOut.projection[0][0]; - auto* matrix = &imguizmoM16InOut.model[0][0]; - - if (transformParams.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; + float* objectMatrices[2u]; + + objectMatrices[0u] = &imguizmoM16InOut.model[0][0]; - /* - for the "useWindow" case we just render to a gui area, - otherwise to fake full screen transparent window + // TODO: here we will use second camera model as temp input to get deltra TRS matrix for imguizmo controller + objectMatrices[1u] = secondCameraModel.data(); - note that for both cases we make sure gizmo being - rendered is aligned to our texture scene using - imgui "cursor" screen positions - */ - - SImResourceInfo info; - info.textureID = OfflineSceneTextureIx; - info.samplerIx = (uint16_t)nbl::ext::imgui::UI::DefaultSamplerIx::USER; - - if (transformParams.useWindow) + TransformStart(cameraView, cameraProjection, objectMatrices[lastUsing]); + for (int matId = 0; matId < 2u; matId++) { - 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(); + ImGuizmo::PushID(matId); - transformParams.aspectRatio = contentRegionSize.x / contentRegionSize.y; - transformParams.invAspectRatio = contentRegionSize.y / contentRegionSize.x; - - ImGui::Image(info, contentRegionSize); - ImGuizmo::SetRect(cursorPos.x, cursorPos.y, 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); - - viewManipulateRight = cursorPos.x + contentRegionSize.x; - viewManipulateTop = cursorPos.y; - - transformParams.aspectRatio = io.DisplaySize.x / io.DisplaySize.y; - transformParams.invAspectRatio = io.DisplaySize.y / io.DisplaySize.x; + EditTransform(cameraView, cameraProjection, objectMatrices[matId]); + if (ImGuizmo::IsUsing()) + lastUsing = matId; + ImGuizmo::PopID(); } + TransformEnd(); - // object gizmo - ImGuizmo::Manipulate(cameraView, cameraProjection, mCurrentGizmoOperation, mCurrentGizmoMode, matrix, NULL, useSnap ? &snap[0] : NULL, boundSizing ? bounds : NULL, boundSizingSnap ? boundsSnap : NULL); - - // second camera - { - // TODO: manipulate second camera given view & projection from first, use delta matrix to be passed to imguizmo controller - } - - ImGui::End(); - ImGui::PopStyleColor(); + //ImGui::End(); } } @@ -1275,6 +1133,149 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGui::End(); } + + void TransformStart(float* cameraView, float* cameraProjection, float* matrix) + { + 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 (ImGui::IsKeyPressed(ImGuiKey_T)) + mCurrentGizmoOperation = ImGuizmo::TRANSLATE; + if (ImGui::IsKeyPressed(ImGuiKey_E)) + mCurrentGizmoOperation = ImGuizmo::ROTATE; + if (ImGui::IsKeyPressed(ImGuiKey_R)) // r Key + 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; + 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)) + useSnap = !useSnap; + ImGui::Checkbox(" ", &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; + } + + 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 + */ + + SImResourceInfo info; + info.textureID = OfflineSceneTextureIx; + info.samplerIx = (uint16_t)nbl::ext::imgui::UI::DefaultSamplerIx::USER; + + if (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(); + + aspectRatio = contentRegionSize.x / contentRegionSize.y; + invAspectRatio = contentRegionSize.y / contentRegionSize.x; + + ImGui::Image(info, contentRegionSize); + ImGuizmo::SetRect(cursorPos.x, cursorPos.y, 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); + + viewManipulateRight = cursorPos.x + contentRegionSize.x; + viewManipulateTop = cursorPos.y; + + aspectRatio = io.DisplaySize.x / io.DisplaySize.y; + invAspectRatio = io.DisplaySize.y / io.DisplaySize.x; + } + } + + void EditTransform(float* cameraView, float* cameraProjection, float* matrix) + { + ImGuiIO& io = ImGui::GetIO(); + float windowWidth = (float)ImGui::GetWindowWidth(); + float windowHeight = (float)ImGui::GetWindowHeight(); + if (!useWindow) + { + ImGuizmo::SetRect(0, 0, io.DisplaySize.x, io.DisplaySize.y); + } + else + { + ImGuizmo::SetRect(ImGui::GetWindowPos().x, ImGui::GetWindowPos().y, windowWidth, windowHeight); + } + ImGuizmo::Manipulate(cameraView, cameraProjection, mCurrentGizmoOperation, mCurrentGizmoMode, matrix, NULL, useSnap ? &snap[0] : NULL); + } + + void TransformEnd() + { + if (useWindow) + { + ImGui::End(); + } + ImGui::PopStyleColor(1); + } + std::chrono::seconds timeout = std::chrono::seconds(0x7fffFFFFu); clock_t::time_point start; @@ -1335,15 +1336,8 @@ class UISampleApp final : public examples::SimpleWindowedApplication 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 - struct TransformRequestParams - { - bool useWindow = true, editTransformDecomposition = false, enableViewManipulate = false; - float camDistance = 8.f, aspectRatio = {}, invAspectRatio = {}; - }; - static constexpr inline auto OfflineSceneTextureIx = 1u; - TransformRequestParams transformParams; bool isPerspective = true, isLH = true, flipGizmoY = true, move = false; float fov = 60.f, zNear = 0.1f, zFar = 10000.f, moveSpeed = 1.f, rotateSpeed = 1.f; float viewWidth = 10.f; @@ -1351,6 +1345,22 @@ class UISampleApp final : public examples::SimpleWindowedApplication float camXAngle = 32.f / 180.f * 3.14159f; bool firstFrame = true; + + // gizmo stuff + float camDistance = 8.f, aspectRatio = {}, invAspectRatio = {}; + bool useWindow = true, useSnap = false; + ImGuizmo::OPERATION mCurrentGizmoOperation = ImGuizmo::TRANSLATE; + ImGuizmo::MODE mCurrentGizmoMode = ImGuizmo::WORLD; + float snap[3] = { 1.f, 1.f, 1.f }; + int lastUsing = 0; + + // TMP + std::array secondCameraModel = { + 1.f, 0.f, 0.f, 0.f, + 0.f, 1.f, 0.f, 0.f, + 0.f, 0.f, 1.f, 0.f, + 0.f, 0.f, 0.f, 1.f + }; }; NBL_MAIN_FUNC(UISampleApp) \ No newline at end of file From 827787c95eda48e8bcfe93b24b3372c87dd9d48e Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Fri, 22 Nov 2024 13:14:10 +0100 Subject: [PATCH 040/205] add `inline const matrix operator()() const` IGimbal operator returning TRS, update the example and display imguizmo of second camera given its TRS matrix --- 61_UI/main.cpp | 75 +++++++++++++------------------ common/include/camera/IGimbal.hpp | 31 ++++++++++++- 2 files changed, 60 insertions(+), 46 deletions(-) diff --git a/61_UI/main.cpp b/61_UI/main.cpp index 91576820b..6194e40a0 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -432,10 +432,30 @@ class UISampleApp final : public examples::SimpleWindowedApplication } for (auto& camera : cameraz) + { + // lets use key map presets to update the controller + camera = make_smart_refctd_ptr(float32_t3{ -2.017f, 0.386f, 0.684f }, glm::quat(0.55f, 0.047f, 0.830f, -0.072f)); // order important for quat, the ctor is GLM_FUNC_QUALIFIER GLM_CONSTEXPR qua::qua(T _w, T _x, T _y, T _z) - - // lets use key map presets to update the controller - auto& camera = cameraz.front(); + + // init keyboard map + camera->updateKeyboardMapping([&](auto& keys) + { + keys = camera->getKeyboardMappingPreset(); + }); + + // init mouse map + camera->updateMouseMapping([&](auto& keys) + { + keys = camera->getMouseMappingPreset(); + }); + + // init imguizmo map + camera->updateImguizmoMapping([&](auto& keys) + { + keys = camera->getImguizmoMappingPreset(); + }); + } + scene = CScene::create(smart_refctd_ptr(m_device), smart_refctd_ptr(m_logger), getGraphicsQueue(), smart_refctd_ptr(resources)); if (!scene) @@ -444,23 +464,6 @@ class UISampleApp final : public examples::SimpleWindowedApplication return false; } - // init keyboard map - camera->updateKeyboardMapping([&](auto& keys) - { - keys = camera->getKeyboardMappingPreset(); - }); - - // init mouse map - camera->updateMouseMapping([&](auto& keys) - { - keys = camera->getMouseMappingPreset(); - }); - - // init imguizmo map - camera->updateImguizmoMapping([&](auto& keys) - { - keys = camera->getImguizmoMappingPreset(); - }); } oracle.reportBeginFrameRecord(); @@ -587,8 +590,6 @@ class UISampleApp final : public examples::SimpleWindowedApplication IQueue::SSubmitInfo::SCommandBufferInfo commandBuffersInfo[] = { {.cmdbuf = cmdbuf } }; - - const IGPUCommandBuffer::SRenderpassBeginInfo info = { .framebuffer = m_framebuffers[resourceIx].get(), @@ -779,7 +780,6 @@ class UISampleApp final : public examples::SimpleWindowedApplication // TODO: testing auto& camera = cameraz.front().get(); - std::vector virtualEvents(0x45); // TODO: tmp uint32_t vCount; @@ -940,7 +940,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication static struct { - float32_t4x4 view, projection, model; + float32_t4x4 view, projection, model[2u], deltaTRS[2u]; } imguizmoM16InOut; const auto& projectionMatrix = projection->getMatrix(); @@ -949,7 +949,8 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGuizmo::SetID(0u); imguizmoM16InOut.view = transpose(getMatrix3x4As4x4(view.matrix)); imguizmoM16InOut.projection = transpose(projectionMatrix); - imguizmoM16InOut.model = transpose(getMatrix3x4As4x4(scene->object.model)); + imguizmoM16InOut.model[0] = transpose(getMatrix3x4As4x4(scene->object.model)); + imguizmoM16InOut.model[1] = transpose(getMatrix3x4As4x4(cameraz.back()->getGimbal()())); { 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/ @@ -958,19 +959,13 @@ class UISampleApp final : public examples::SimpleWindowedApplication { auto* cameraView = &imguizmoM16InOut.view[0][0]; auto* cameraProjection = &imguizmoM16InOut.projection[0][0]; - float* objectMatrices[2u]; - - objectMatrices[0u] = &imguizmoM16InOut.model[0][0]; - - // TODO: here we will use second camera model as temp input to get deltra TRS matrix for imguizmo controller - objectMatrices[1u] = secondCameraModel.data(); - TransformStart(cameraView, cameraProjection, objectMatrices[lastUsing]); + TransformStart(cameraView, cameraProjection, &imguizmoM16InOut.model[lastUsing][0][0]); for (int matId = 0; matId < 2u; matId++) { ImGuizmo::PushID(matId); - EditTransform(cameraView, cameraProjection, objectMatrices[matId]); + EditTransform(cameraView, cameraProjection, &imguizmoM16InOut.model[matId][0][0], &imguizmoM16InOut.deltaTRS[matId][0][0]); if (ImGuizmo::IsUsing()) lastUsing = matId; ImGuizmo::PopID(); @@ -990,7 +985,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication static float32_t4x4 modelViewProjection; auto& hook = scene->object; - hook.model = float32_t3x4(transpose(imguizmoM16InOut.model)); + hook.model = float32_t3x4(transpose(imguizmoM16InOut.model[0])); { const auto& references = resources->objects; const auto type = static_cast(gcIndex); @@ -1251,7 +1246,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication } } - void EditTransform(float* cameraView, float* cameraProjection, float* matrix) + void EditTransform(float* cameraView, float* cameraProjection, float* matrix, float* deltaTRS) { ImGuiIO& io = ImGui::GetIO(); float windowWidth = (float)ImGui::GetWindowWidth(); @@ -1264,7 +1259,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication { ImGuizmo::SetRect(ImGui::GetWindowPos().x, ImGui::GetWindowPos().y, windowWidth, windowHeight); } - ImGuizmo::Manipulate(cameraView, cameraProjection, mCurrentGizmoOperation, mCurrentGizmoMode, matrix, NULL, useSnap ? &snap[0] : NULL); + ImGuizmo::Manipulate(cameraView, cameraProjection, mCurrentGizmoOperation, mCurrentGizmoMode, matrix, deltaTRS, useSnap ? &snap[0] : NULL); } void TransformEnd() @@ -1353,14 +1348,6 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGuizmo::MODE mCurrentGizmoMode = ImGuizmo::WORLD; float snap[3] = { 1.f, 1.f, 1.f }; int lastUsing = 0; - - // TMP - std::array secondCameraModel = { - 1.f, 0.f, 0.f, 0.f, - 0.f, 1.f, 0.f, 0.f, - 0.f, 0.f, 1.f, 0.f, - 0.f, 0.f, 0.f, 1.f - }; }; NBL_MAIN_FUNC(UISampleApp) \ No newline at end of file diff --git a/common/include/camera/IGimbal.hpp b/common/include/camera/IGimbal.hpp index 341f55e17..a3d153b92 100644 --- a/common/include/camera/IGimbal.hpp +++ b/common/include/camera/IGimbal.hpp @@ -232,6 +232,12 @@ namespace nbl::hlsl m_position = position; } + inline void setScale(const vector& scale) + { + // we are not consider it a manipulation because it only impacts TRS world matrix we do not store + m_scale = scale; + } + inline void setOrientation(const glm::quat& orientation) { assert(m_isManipulating); // TODO: log error and return without doing nothing @@ -293,6 +299,24 @@ namespace nbl::hlsl //! Orientation of gimbal inline const auto& getOrientation() const { return m_orientation; } + //! Scale transform component + inline const auto& getScale() const { return m_scale; } + + //! World matrix (TRS) + inline const matrix operator()() const + { + const auto& position = getPosition(); + const auto& rotation = getOrthonornalMatrix(); + const auto& scale = getScale(); + + return + { + vector(rotation[0] * scale.x, position.x), + vector(rotation[1] * scale.y, position.y), + vector(rotation[2] * scale.z, position.z) + }; + } + //! Orthonormal [getXAxis(), getYAxis(), getZAxis()] orientation matrix inline const auto& getOrthonornalMatrix() const { return m_orthonormal; } @@ -330,9 +354,12 @@ namespace nbl::hlsl //! TODO: precision + replace with our "quat at home" glm::quat m_orientation; - //! Orthonormal base composed from "m_orientation" representing gimbal's "forward", "up" & "right" vectors in local space + //! Scale transform component + vector m_scale = { 1.f, 1.f , 1.f }; + + //! Orthonormal base composed from "m_orientation" representing gimbal's "forward", "up" & "right" vectors in local space - basically it spans orientation space matrix m_orthonormal; - + //! Counter that increments for each performed manipulation, resets with each begin() call size_t m_counter = {}; From 7ae02fd48a7da8e7906aa986caea4aa07362b3aa Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Fri, 22 Nov 2024 15:01:17 +0100 Subject: [PATCH 041/205] add virtual getIdentifier to camera interface, display matrices for both cameras in a window --- 61_UI/main.cpp | 129 +++++++++++++++++++++++++--------- common/include/CFPSCamera.hpp | 5 ++ common/include/ICamera.hpp | 3 + 3 files changed, 105 insertions(+), 32 deletions(-) diff --git a/61_UI/main.cpp b/61_UI/main.cpp index 6194e40a0..dadc1d7bc 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -976,6 +976,8 @@ class UISampleApp final : public examples::SimpleWindowedApplication } } + auto& hook = scene->object; + // to Nabla + update camera & model matrices // TODO: make it more nicely @@ -984,7 +986,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication static float32_t3x4 modelView, normal; static float32_t4x4 modelViewProjection; - auto& hook = scene->object; + hook.model = float32_t3x4(transpose(imguizmoM16InOut.model[0])); { const auto& references = resources->objects; @@ -1008,52 +1010,115 @@ class UISampleApp final : public examples::SimpleWindowedApplication memcpy(ubo.MVP, &modelViewProjection[0][0], sizeof(ubo.MVP)); memcpy(ubo.MV, &modelView[0][0], sizeof(ubo.MV)); memcpy(ubo.NormalMat, &normal[0][0], sizeof(ubo.NormalMat)); - - // object meta display - { - ImGui::Begin("Object"); - ImGui::Text("type: \"%s\"", hook.meta.name.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) + auto addMatrixTable = [&](const char* topText, const char* tableName, int rows, int columns, const float* pointer, bool withSeparator = true) + { + 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)) { - ImGui::Text(topText); - if (ImGui::BeginTable(tableName, columns)) + for (int y = 0; y < rows; ++y) { - for (int y = 0; y < rows; ++y) + ImGui::TableNextRow(); + for (int x = 0; x < columns; ++x) { - ImGui::TableNextRow(); - for (int x = 0; x < columns; ++x) - { - ImGui::TableSetColumnIndex(x); + ImGui::TableSetColumnIndex(x); + if (pointer) ImGui::Text("%.3f", *(pointer + (y * columns) + x)); - } + else + ImGui::Text("-"); } - ImGui::EndTable(); } + ImGui::EndTable(); + } + ImGui::PopStyleColor(2); + if (withSeparator) + ImGui::Separator(); + }; - if (withSeparator) - ImGui::Separator(); - }; + // Scene Model Object + { + ImGui::SetNextWindowSize(ImVec2(600, 400), ImGuiCond_FirstUseEver); + ImGui::Begin("Object Info and Model Matrix", nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysVerticalScrollbar); - auto& camera = cameraz.front(); - const auto& orientation = cameraz.front()->getGimbal().getOrthonornalMatrix(); + ImGui::Text("Type: \"%s\"", hook.meta.name.data()); + ImGui::Separator(); - addMatrixTable("Object's Model Matrix", "ModelMatrixTable", 3, 4, &scene->object.model[0][0]); - addMatrixTable("Camera's Position", "PositionForwardVec", 1, 3, &camera->getGimbal().getPosition()[0]); - addMatrixTable("Camera's Orientation Quat", "OrientationQuatTable", 1, 4, &camera->getGimbal().getOrientation()[0]); - addMatrixTable("Camera's View Matrix", "ViewMatrixTable", 3, 4, &view.matrix[0][0]); - addMatrixTable("Bound Projection Matrix", "ProjectionMatrixTable", 4, 4, &projectionMatrix[0][0], false); + ImGui::PushStyleColor(ImGuiCol_TableRowBg, ImGui::GetStyleColorVec4(ImGuiCol_FrameBg)); + ImGui::PushStyleColor(ImGuiCol_TableRowBgAlt, ImGui::GetStyleColorVec4(ImGuiCol_FrameBgHovered)); - ImGui::End(); + addMatrixTable("Model Matrix", "ModelMatrixTable", 3, 4, &scene->object.model[0][0]); + + ImGui::PopStyleColor(2); + ImGui::End(); + } + + // Cameraz + { + size_t cameraCount = cameraz.size(); + if (cameraCount > 0) + { + size_t columns = std::max(1, static_cast(std::sqrt(static_cast(cameraCount)))); + size_t rows = (cameraCount + columns - 1) / columns; + + ImGui::Begin("Camera Matrices", nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysVerticalScrollbar); + + ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2(4.0f, 2.0f)); + + for (size_t row = 0; row < rows; ++row) + { + for (size_t col = 0; col < columns; ++col) + { + size_t cameraIndex = row * columns + col; + if (cameraIndex >= cameraCount) + break; + + auto& camera = cameraz[cameraIndex]; + if (!camera) + continue; + + auto& gimbal = camera->getGimbal(); + const auto& position = gimbal.getPosition(); + const auto& orientation = gimbal.getOrientation(); + const auto& viewMatrix = gimbal.getView().matrix; + + ImGui::Text("ID: %zu", cameraIndex); + ImGui::Separator(); + + ImGui::Text("Type: %s", camera->getIdentifier().data()); + ImGui::Separator(); + + ImGui::PushStyleColor(ImGuiCol_TableRowBg, ImGui::GetStyleColorVec4(ImGuiCol_ChildBg)); + ImGui::PushStyleColor(ImGuiCol_TableRowBgAlt, ImGui::GetStyleColorVec4(ImGuiCol_WindowBg)); + + if (ImGui::BeginTable(("CameraTable" + std::to_string(cameraIndex)).c_str(), 1, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchSame)) + { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + + addMatrixTable("Position", ("PositionTable" + std::to_string(cameraIndex)).c_str(), 1, 3, &position[0], false); + addMatrixTable("Orientation (Quaternion)", ("OrientationTable" + std::to_string(cameraIndex)).c_str(), 1, 4, &orientation[0], false); + addMatrixTable("View Matrix", ("ViewMatrixTable" + std::to_string(cameraIndex)).c_str(), 3, 4, &viewMatrix[0][0], true); + + ImGui::EndTable(); + } + + ImGui::PopStyleColor(2); + } + } + + ImGui::PopStyleVar(); + ImGui::End(); + } + else + ImGui::Text("No camera properties to display."); + } } + // 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. diff --git a/common/include/CFPSCamera.hpp b/common/include/CFPSCamera.hpp index 92943d01e..477fa8df4 100644 --- a/common/include/CFPSCamera.hpp +++ b/common/include/CFPSCamera.hpp @@ -90,6 +90,11 @@ class CFPSCamera final : public ICamera } } + virtual const std::string_view getIdentifier() override + { + return "FPS Camera"; + } + private: typename base_t::CGimbal m_gimbal; diff --git a/common/include/ICamera.hpp b/common/include/ICamera.hpp index db5cf969d..b9a5eca8c 100644 --- a/common/include/ICamera.hpp +++ b/common/include/ICamera.hpp @@ -89,6 +89,9 @@ class ICamera : public IGimbalController, virtual public core::IReferenceCounted // VirtualEventType bitmask for a camera view gimbal manipulation requests filtering virtual const uint32_t getAllowedVirtualEvents(ManipulationMode mode) = 0u; + + // Identifier of a camera type + virtual const std::string_view getIdentifier() = 0u; }; } From 09bacb075c35652c79a85a2918fa109db074579c Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Mon, 2 Dec 2024 19:31:36 +0100 Subject: [PATCH 042/205] one step ahead to imguizmo controller - display the scene from second camera perspective & deploy imguizmo controller to manipulate second camera (first tests, a lot of bugs there to fix) --- 61_UI/main.cpp | 444 +++++++++++++++--------- common/include/CFPSCamera.hpp | 6 +- common/include/CGeomtryCreatorScene.hpp | 1 - 3 files changed, 287 insertions(+), 164 deletions(-) diff --git a/61_UI/main.cpp b/61_UI/main.cpp index dadc1d7bc..59afdb870 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -390,7 +390,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication params.resources.samplersInfo = { .setIx = 0u, .bindingIx = 1u }; params.assetManager = m_assetManager; params.pipelineCache = nullptr; - params.pipelineLayout = nbl::ext::imgui::UI::createDefaultPipelineLayout(m_utils->getLogicalDevice(), params.resources.texturesInfo, params.resources.samplersInfo, TexturesAmount); + 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.streamingBuffer = nullptr; params.subpassIx = 0u; @@ -408,7 +408,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication IDescriptorPool::SCreateInfo descriptorPoolInfo = {}; descriptorPoolInfo.maxDescriptorCount[static_cast(asset::IDescriptor::E_TYPE::ET_SAMPLER)] = (uint32_t)nbl::ext::imgui::UI::DefaultSamplerIx::COUNT; - descriptorPoolInfo.maxDescriptorCount[static_cast(asset::IDescriptor::E_TYPE::ET_SAMPLED_IMAGE)] = TexturesAmount; + descriptorPoolInfo.maxDescriptorCount[static_cast(asset::IDescriptor::E_TYPE::ET_SAMPLED_IMAGE)] = TotalUISampleTexturesAmount; descriptorPoolInfo.maxSets = 1u; descriptorPoolInfo.flags = IDescriptorPool::E_CREATE_FLAGS::ECF_UPDATE_AFTER_BIND_BIT; @@ -421,6 +421,9 @@ class UISampleApp final : public examples::SimpleWindowedApplication m_ui.manager->registerListener([this]() -> void { imguiListen(); }); } + for (auto& projection : projections) + projection = make_smart_refctd_ptr(); + // Geometry Creator Render Scenes { resources = ResourcesBundle::create(m_device.get(), m_logger.get(), getGraphicsQueue(), m_assetManager->getGeometryCreator()); @@ -431,11 +434,18 @@ class UISampleApp final : public examples::SimpleWindowedApplication return false; } - for (auto& camera : cameraz) + // TOOD: we should be able to load position & orientation from json file, support multiple cameraz + const float32_t3 iPosition[CamerazCount] = { float32_t3{ -2.238f, 1.438f, -1.558f }, float32_t3{ -2.017f, 0.386f, 0.684f } }; + // order important for glm::quat, the ctor is GLM_FUNC_QUALIFIER GLM_CONSTEXPR qua::qua(T _w, T _x, T _y, T _z) but memory layout is x,y,z,w + const glm::quat iOrientation[CamerazCount] = { glm::quat(0.888f, 0.253f, 0.368f, -0.105f), glm::quat(0.55f, 0.047f, 0.830f, -0.072f) }; + + for (uint32_t i = 0u; i < cameraz.size(); ++i) { + auto& camera = cameraz[i]; + // lets use key map presets to update the controller - camera = make_smart_refctd_ptr(float32_t3{ -2.017f, 0.386f, 0.684f }, glm::quat(0.55f, 0.047f, 0.830f, -0.072f)); // order important for quat, the ctor is GLM_FUNC_QUALIFIER GLM_CONSTEXPR qua::qua(T _w, T _x, T _y, T _z) + camera = make_smart_refctd_ptr(iPosition[i], iOrientation[i]); // init keyboard map camera->updateKeyboardMapping([&](auto& keys) @@ -456,14 +466,17 @@ class UISampleApp final : public examples::SimpleWindowedApplication }); } - scene = CScene::create(smart_refctd_ptr(m_device), smart_refctd_ptr(m_logger), getGraphicsQueue(), smart_refctd_ptr(resources)); - - if (!scene) + for (uint32_t i = 0u; i < scenez.size(); ++i) { - m_logger->log("Could not create geometry creator scene!", ILogger::ELL_ERROR); - return false; + auto& scene = scenez[i]; + scene = CScene::create(smart_refctd_ptr(m_device), smart_refctd_ptr(m_logger), getGraphicsQueue(), smart_refctd_ptr(resources)); + + if (!scene) + { + m_logger->log("Could not create geometry creator scene[%d]!", ILogger::ELL_ERROR, i); + return false; + } } - } oracle.reportBeginFrameRecord(); @@ -472,8 +485,14 @@ class UISampleApp final : public examples::SimpleWindowedApplication TESTS, TODO: remove all once finished work & integrate with the example properly */ - aspectRatio = float(m_window->getWidth()) / float(m_window->getHeight()); - invAspectRatio = float(m_window->getHeight()) / float(m_window->getWidth()); + const auto iAspectRatio = float(m_window->getWidth()) / float(m_window->getHeight()); + const auto iInvAspectRatio = float(m_window->getHeight()) / float(m_window->getWidth()); + + for (uint32_t i = 0u; i < ProjectionsCount; ++i) + { + aspectRatio[i] = iAspectRatio; + invAspectRatio[i] = iInvAspectRatio; + } if (base_t::argv.size() >= 3 && argv[1] == "-timeout_seconds") timeout = std::chrono::seconds(std::atoi(argv[2].c_str())); @@ -483,15 +502,18 @@ class UISampleApp final : public examples::SimpleWindowedApplication bool updateGUIDescriptorSet() { - // texture atlas + our scene texture, note we don't create info & write pair for the font sampler because UI extension's is immutable and baked into DS layout - static std::array descriptorInfo; - static IGPUDescriptorSet::SWriteDescriptorSet writes[TexturesAmount]; + // UI texture atlas + our camera scene textures, note we don't create info & write pair for the font sampler because UI extension's is immutable and baked into DS layout + static std::array descriptorInfo; + static IGPUDescriptorSet::SWriteDescriptorSet writes[TotalUISampleTexturesAmount]; descriptorInfo[nbl::ext::imgui::UI::FontAtlasTexId].info.image.imageLayout = IImage::LAYOUT::READ_ONLY_OPTIMAL; descriptorInfo[nbl::ext::imgui::UI::FontAtlasTexId].desc = core::smart_refctd_ptr(m_ui.manager->getFontAtlasView()); - descriptorInfo[OfflineSceneTextureIx].info.image.imageLayout = IImage::LAYOUT::READ_ONLY_OPTIMAL; - descriptorInfo[OfflineSceneTextureIx].desc = scene->getColorAttachment(); + descriptorInfo[OfflineSceneFirstCameraTextureIx].info.image.imageLayout = IImage::LAYOUT::READ_ONLY_OPTIMAL; + descriptorInfo[OfflineSceneFirstCameraTextureIx].desc = scenez[0]->getColorAttachment(); + + descriptorInfo[OfflineSceneSecondCameraTextureIx].info.image.imageLayout = IImage::LAYOUT::READ_ONLY_OPTIMAL; + descriptorInfo[OfflineSceneSecondCameraTextureIx].desc = scenez[1]->getColorAttachment(); for (uint32_t i = 0; i < descriptorInfo.size(); ++i) { @@ -501,7 +523,8 @@ class UISampleApp final : public examples::SimpleWindowedApplication writes[i].count = 1u; } writes[nbl::ext::imgui::UI::FontAtlasTexId].info = descriptorInfo.data() + nbl::ext::imgui::UI::FontAtlasTexId; - writes[OfflineSceneTextureIx].info = descriptorInfo.data() + OfflineSceneTextureIx; + writes[OfflineSceneFirstCameraTextureIx].info = descriptorInfo.data() + OfflineSceneFirstCameraTextureIx; + writes[OfflineSceneSecondCameraTextureIx].info = descriptorInfo.data() + OfflineSceneSecondCameraTextureIx; return m_device->updateDescriptorSets(writes, {}); } @@ -547,13 +570,17 @@ class UISampleApp final : public examples::SimpleWindowedApplication // render geometry creator scene to offline frame buffer & submit // TODO: OK with TRI buffer this thing is retarded now // (**) <- a note why bellow before submit - scene->begin(); + + for (auto scene : scenez) { - scene->update(); - scene->record(); - scene->end(); + scene->begin(); + { + scene->update(); + scene->record(); + scene->end(); + } + scene->submit(getGraphicsQueue()); } - scene->submit(getGraphicsQueue()); willSubmit &= cmdbuf->reset(IGPUCommandBuffer::RESET_FLAGS::RELEASE_RESOURCES_BIT); willSubmit &= cmdbuf->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT); @@ -679,13 +706,21 @@ class UISampleApp final : public examples::SimpleWindowedApplication } }; - // (**) -> wait on offline framebuffer + // (**) -> wait on offline framebuffers { const nbl::video::ISemaphore::SWaitInfo waitInfos[] = - { { - .semaphore = scene->semaphore.progress.get(), - .value = scene->semaphore.finishedValue - } }; + { + // wait for first camera scene view fb + { + .semaphore = scenez[0]->semaphore.progress.get(), + .value = scenez[0]->semaphore.finishedValue + }, + // and second one too + { + .semaphore = scenez[1]->semaphore.progress.get(), + .value = scenez[1]->semaphore.finishedValue + }, + }; m_device->blockForSemaphores(waitInfos); updateGUIDescriptorSet(); @@ -738,7 +773,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication return timestamp; }; - const auto nextPresentationTimestamp = updatePresentationTimestamp(); + m_nextPresentationTimestamp = updatePresentationTimestamp(); struct { @@ -780,10 +815,10 @@ class UISampleApp final : public examples::SimpleWindowedApplication // TODO: testing auto& camera = cameraz.front().get(); - std::vector virtualEvents(0x45); // TODO: tmp + static std::vector virtualEvents(0x45); uint32_t vCount; - camera->beginInputProcessing(nextPresentationTimestamp); + camera->beginInputProcessing(m_nextPresentationTimestamp); { camera->process(nullptr, vCount); @@ -793,7 +828,6 @@ class UISampleApp final : public examples::SimpleWindowedApplication camera->process(virtualEvents.data(), vCount, { params.keyboardEvents, params.mouseEvents }); } camera->endInputProcessing(); - camera->manipulate({ virtualEvents.data(), vCount }, ICamera::Local); } @@ -816,34 +850,41 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGui::SetNextWindowSize(ImVec2(320, 340), ImGuiCond_Appearing); ImGui::Begin("Editor"); - if (ImGui::RadioButton("Full view", !useWindow)) - useWindow = false; + //if (ImGui::RadioButton("Full view", !useWindow)) + // useWindow = false; // TODO: I need this logic per viewport we will render a scene from a point of view of its bound camera { - if (isPerspective) + for (uint32_t i = 0u; i < projections.size(); ++i) { - if (isLH) - projection->setMatrix(buildProjectionMatrixPerspectiveFovLH(glm::radians(fov), aspectRatio, zNear, zFar)); - else - projection->setMatrix(buildProjectionMatrixPerspectiveFovRH(glm::radians(fov), aspectRatio, zNear, zFar)); - } - else - { - float viewHeight = viewWidth * invAspectRatio; + auto& projection = projections[i]; - if (isLH) - projection->setMatrix(buildProjectionMatrixOrthoLH(viewWidth, viewHeight, zNear, zFar)); + if (isPerspective) + { + if (isLH) + projection->setMatrix(buildProjectionMatrixPerspectiveFovLH(glm::radians(fov), aspectRatio[i], zNear, zFar)); + else + projection->setMatrix(buildProjectionMatrixPerspectiveFovRH(glm::radians(fov), aspectRatio[i], zNear, zFar)); + } else - projection->setMatrix(buildProjectionMatrixOrthoRH(viewWidth, viewHeight, zNear, zFar)); + { + float viewHeight = viewWidth * invAspectRatio[i]; + + if (isLH) + projection->setMatrix(buildProjectionMatrixOrthoLH(viewWidth, viewHeight, zNear, zFar)); + else + projection->setMatrix(buildProjectionMatrixOrthoRH(viewWidth, viewHeight, zNear, zFar)); + } } + + } ImGui::SameLine(); - if (ImGui::RadioButton("Window", useWindow)) - useWindow = true; + //if (ImGui::RadioButton("Window", useWindow)) + // useWindow = true; ImGui::Text("Camera"); @@ -855,13 +896,13 @@ class UISampleApp final : public examples::SimpleWindowedApplication if (ImGui::RadioButton("RH", !isLH)) isLH = false; - if (ImGui::RadioButton("Perspective", isPerspective)) - isPerspective = true; + //if (ImGui::RadioButton("Perspective", isPerspective)) + // isPerspective = true; - ImGui::SameLine(); + //ImGui::SameLine(); - if (ImGui::RadioButton("Orthographic", !isPerspective)) - isPerspective = false; + //if (ImGui::RadioButton("Orthographic", !isPerspective)) + // isPerspective = false; ImGui::Checkbox("Enable camera movement", &move); ImGui::SliderFloat("Move speed", &moveSpeed, 0.1f, 10.f); @@ -934,82 +975,171 @@ class UISampleApp final : public examples::SimpleWindowedApplication = 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 { - float32_t4x4 view, projection, model[2u], deltaTRS[2u]; + float32_t4x4 view[CamerazCount], projection[ProjectionsCount], inModel[2u], outModel[2u], outDeltaTRS[2u]; } imguizmoM16InOut; - const auto& projectionMatrix = projection->getMatrix(); - const auto& view = cameraz.front()->getGimbal().getView(); + //const auto& projectionMatrix = projection->getMatrix(); + + const auto& firstcamera = cameraz.front(); + const auto& secondcamera = cameraz.back(); ImGuizmo::SetID(0u); - imguizmoM16InOut.view = transpose(getMatrix3x4As4x4(view.matrix)); - imguizmoM16InOut.projection = transpose(projectionMatrix); - imguizmoM16InOut.model[0] = transpose(getMatrix3x4As4x4(scene->object.model)); - imguizmoM16InOut.model[1] = transpose(getMatrix3x4As4x4(cameraz.back()->getGimbal()())); + + imguizmoM16InOut.view[0u] = transpose(getMatrix3x4As4x4(firstcamera->getGimbal().getView().matrix)); + imguizmoM16InOut.view[1u] = transpose(getMatrix3x4As4x4(secondcamera->getGimbal().getView().matrix)); + + for(uint32_t i = 0u; i < ProjectionsCount; ++i) + imguizmoM16InOut.projection[i] = transpose(projections[i]->getMatrix()); + + // we will transform a scene object's model + imguizmoM16InOut.inModel[0] = transpose(getMatrix3x4As4x4(m_model)); + // and second camera's model too + imguizmoM16InOut.inModel[1] = transpose(getMatrix3x4As4x4(secondcamera->getGimbal()())); { + float* views[] + { + &imguizmoM16InOut.view[0u][0][0], // first camera + &imguizmoM16InOut.view[1u][0][0] // second camera + }; + + float* projections[] + { + &imguizmoM16InOut.projection[0u][0][0], // full screen, tmp off + &imguizmoM16InOut.projection[1u][0][0], // first camera + &imguizmoM16InOut.projection[2u][0][0] // second camera + }; + 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/ + for(uint32_t i = 0u; i < ProjectionsCount; ++i) + imguizmoM16InOut.projection[i][1][1] *= -1.f; // https://johannesugb.github.io/gpu-programming/why-do-opengl-proj-matrices-fail-in-vulkan/ + + float* lastUsedModel = &imguizmoM16InOut.inModel[lastUsing][0][0]; - // imguizmo manipulations + // ImGuizmo manipulations on the last used model matrix + TransformEditor(lastUsedModel); + + for (uint32_t windowIndex = 0; windowIndex < 2u; ++windowIndex) { - auto* cameraView = &imguizmoM16InOut.view[0][0]; - auto* cameraProjection = &imguizmoM16InOut.projection[0][0]; + auto* cameraView = views[windowIndex]; + auto* cameraProjection = projections[1u + windowIndex]; + + TransformStart(windowIndex); - TransformStart(cameraView, cameraProjection, &imguizmoM16InOut.model[lastUsing][0][0]); - for (int matId = 0; matId < 2u; matId++) + for (uint32_t matId = 0; matId < 2; matId++) { + if(matId == 0) + ImGuizmo::AllowAxisFlip(true); + else + ImGuizmo::AllowAxisFlip(false); + + // we must work on copies otherwise we enter bug zone, you need to be careful to not edit the same model twice since we share them with camera fbos + auto model = imguizmoM16InOut.inModel[matId]; + float32_t4x4 deltaOutputTRS; + ImGuizmo::PushID(matId); - EditTransform(cameraView, cameraProjection, &imguizmoM16InOut.model[matId][0][0], &imguizmoM16InOut.deltaTRS[matId][0][0]); + ImGuiIO& io = ImGui::GetIO(); + bool success = ImGuizmo::Manipulate(cameraView, cameraProjection, mCurrentGizmoOperation, mCurrentGizmoMode, &model[0][0], &deltaOutputTRS[0][0], useSnap ? &snap[0] : NULL); + + imguizmoM16InOut.outModel[matId] = model; + imguizmoM16InOut.outDeltaTRS[matId] = deltaOutputTRS; + if (ImGuizmo::IsUsing()) lastUsing = matId; + ImGuizmo::PopID(); } - TransformEnd(); - //ImGui::End(); + TransformEnd(); } } - auto& hook = scene->object; + /* + testing imguizmo controller, we use deltra TRS matrix to generate virtual events + */ + + { + static std::vector virtualEvents(0x45); + uint32_t vCount; - // to Nabla + update camera & model matrices + secondcamera->beginInputProcessing(m_nextPresentationTimestamp); + { + secondcamera->process(nullptr, vCount); + + if (virtualEvents.size() < vCount) + virtualEvents.resize(vCount); + + IGimbalController::SUpdateParameters params; + + params.imguizmoEvents = { { imguizmoM16InOut.outDeltaTRS[1u] } }; + + secondcamera->process(virtualEvents.data(), vCount, params); + } + secondcamera->endInputProcessing(); + + // TOOD: this needs debugging + secondcamera->manipulate({ virtualEvents.data(), vCount }, ICamera::Local); + /* + if(vCount) + switch (mCurrentGizmoMode) + { + case ImGuizmo::MODE::LOCAL: + secondcamera->manipulate({ virtualEvents.data(), vCount }, ICamera::Local); break; + case ImGuizmo::MODE::WORLD: + secondcamera->manipulate({ virtualEvents.data(), vCount }, ICamera::World); break; + default: break; + } + */ + } + // update scenes data + // to Nabla + update camera & model matrices + // TODO: make it more nicely - const_cast(view.matrix) = float32_t3x4(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) + std::string_view mName; + const_cast(firstcamera->getGimbal().getView().matrix) = float32_t3x4(transpose(imguizmoM16InOut.view[0u])); // 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) { - static float32_t3x4 modelView, normal; - static float32_t4x4 modelViewProjection; + m_model = float32_t3x4(transpose(imguizmoM16InOut.outModel[0])); + + const float32_t3x4* views[] = + { + &firstcamera->getGimbal().getView().matrix, + &secondcamera->getGimbal().getView().matrix + }; + const auto& references = resources->objects; + const auto type = static_cast(gcIndex); + const auto& [gpu, meta] = references[type]; - hook.model = float32_t3x4(transpose(imguizmoM16InOut.model[0])); + for (uint32_t i = 0u; i < cameraz.size(); ++i) { - const auto& references = resources->objects; - const auto type = static_cast(gcIndex); + auto& scene = scenez[i]; + auto& hook = scene->object; - const auto& [gpu, meta] = references[type]; hook.meta.type = type; - hook.meta.name = meta.name; - } - - auto& ubo = hook.viewParameters; + mName = hook.meta.name = meta.name; + { + float32_t3x4 modelView, normal; + float32_t4x4 modelViewProjection; - modelView = concatenateBFollowedByA(view.matrix, hook.model); + const auto& viewMatrix = *views[i]; + modelView = concatenateBFollowedByA(viewMatrix, m_model); - // TODO - //modelView.getSub3x3InverseTranspose(normal); + // TODO + //modelView.getSub3x3InverseTranspose(normal); - auto concatMatrix = mul(projectionMatrix, getMatrix3x4As4x4(view.matrix)); - modelViewProjection = mul(concatMatrix, getMatrix3x4As4x4(hook.model)); + auto concatMatrix = mul(projections[i]->getMatrix(), getMatrix3x4As4x4(viewMatrix)); + modelViewProjection = mul(concatMatrix, getMatrix3x4As4x4(m_model)); - memcpy(ubo.MVP, &modelViewProjection[0][0], sizeof(ubo.MVP)); - memcpy(ubo.MV, &modelView[0][0], sizeof(ubo.MV)); - memcpy(ubo.NormalMat, &normal[0][0], sizeof(ubo.NormalMat)); + memcpy(hook.viewParameters.MVP, &modelViewProjection[0][0], sizeof(hook.viewParameters.MVP)); + memcpy(hook.viewParameters.MV, &modelView[0][0], sizeof(hook.viewParameters.MV)); + memcpy(hook.viewParameters.NormalMat, &normal[0][0], sizeof(hook.viewParameters.NormalMat)); + } + } } { @@ -1044,13 +1174,13 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGui::SetNextWindowSize(ImVec2(600, 400), ImGuiCond_FirstUseEver); ImGui::Begin("Object Info and Model Matrix", nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysVerticalScrollbar); - ImGui::Text("Type: \"%s\"", hook.meta.name.data()); + ImGui::Text("Type: \"%s\"", mName.data()); ImGui::Separator(); ImGui::PushStyleColor(ImGuiCol_TableRowBg, ImGui::GetStyleColorVec4(ImGuiCol_FrameBg)); ImGui::PushStyleColor(ImGuiCol_TableRowBgAlt, ImGui::GetStyleColorVec4(ImGuiCol_FrameBgHovered)); - addMatrixTable("Model Matrix", "ModelMatrixTable", 3, 4, &scene->object.model[0][0]); + addMatrixTable("Scene Object Model Matrix", "ModelMatrixTable", 3, 4, &m_model[0][0]); ImGui::PopStyleColor(2); ImGui::End(); @@ -1084,6 +1214,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication const auto& position = gimbal.getPosition(); const auto& orientation = gimbal.getOrientation(); const auto& viewMatrix = gimbal.getView().matrix; + const auto trsMatrix = gimbal(); ImGui::Text("ID: %zu", cameraIndex); ImGui::Separator(); @@ -1099,9 +1230,11 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); - addMatrixTable("Position", ("PositionTable" + std::to_string(cameraIndex)).c_str(), 1, 3, &position[0], false); - addMatrixTable("Orientation (Quaternion)", ("OrientationTable" + std::to_string(cameraIndex)).c_str(), 1, 4, &orientation[0], false); - addMatrixTable("View Matrix", ("ViewMatrixTable" + std::to_string(cameraIndex)).c_str(), 3, 4, &viewMatrix[0][0], true); + const auto idxstr = std::to_string(cameraIndex); + addMatrixTable("Position", ("PositionTable" + idxstr).c_str(), 1, 3, &position[0], false); + addMatrixTable("Orientation (Quaternion)", ("OrientationTable" + idxstr).c_str(), 1, 4, &orientation[0], false); + addMatrixTable("View Matrix", ("ViewMatrixTable" + idxstr).c_str(), 3, 4, &viewMatrix[0][0], false); + addMatrixTable("TRS Matrix", ("TRSMatrixTable" + idxstr).c_str(), 3, 4, &trsMatrix[0][0], true); ImGui::EndTable(); } @@ -1193,8 +1326,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGui::End(); } - - void TransformStart(float* cameraView, float* cameraProjection, float* matrix) + void TransformEditor(float* matrix) { static float bounds[] = { -0.5f, -0.5f, -0.5f, 0.5f, 0.5f, 0.5f }; static float boundsSnap[] = { 0.1f, 0.1f, 0.1f }; @@ -1247,48 +1379,50 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGui::InputFloat("Scale Snap", &snap[0]); break; } + } + void TransformStart(uint32_t wCameraIndex) + { 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 - */ + static ImGuiWindowFlags gizmoWindowFlags = ImGuiWindowFlags_NoMove; SImResourceInfo info; - info.textureID = OfflineSceneTextureIx; + switch (wCameraIndex) + { + case 0: + info.textureID = OfflineSceneFirstCameraTextureIx; + break; + case 1: + info.textureID = OfflineSceneSecondCameraTextureIx; + break; + default: + assert(false); // "wCameraIndex OOB!" + break; + } info.samplerIx = (uint16_t)nbl::ext::imgui::UI::DefaultSamplerIx::USER; + aspectRatio[0] = io.DisplaySize.x / io.DisplaySize.y; + invAspectRatio[0] = io.DisplaySize.y / io.DisplaySize.x; + if (useWindow) { ImGui::SetNextWindowSize(ImVec2(800, 400), ImGuiCond_Appearing); - ImGui::SetNextWindowPos(ImVec2(400, 20), ImGuiCond_Appearing); + ImGui::SetNextWindowPos(ImVec2(400, 20 + wCameraIndex * 420), ImGuiCond_Appearing); ImGui::PushStyleColor(ImGuiCol_WindowBg, (ImVec4)ImColor(0.35f, 0.3f, 0.3f)); - ImGui::Begin("Gizmo", 0, gizmoWindowFlags); + std::string ident = "Active Camera ID: " + std::to_string(wCameraIndex); + ImGui::Begin(ident.data(), 0, gizmoWindowFlags); ImGuizmo::SetDrawlist(); - ImVec2 contentRegionSize = ImGui::GetContentRegionAvail(); - ImVec2 windowPos = ImGui::GetWindowPos(); - ImVec2 cursorPos = ImGui::GetCursorScreenPos(); + ImVec2 contentRegionSize, windowPos, cursorPos; + contentRegionSize = ImGui::GetContentRegionAvail(); + cursorPos = ImGui::GetCursorScreenPos(); + windowPos = ImGui::GetWindowPos(); - aspectRatio = contentRegionSize.x / contentRegionSize.y; - invAspectRatio = contentRegionSize.y / contentRegionSize.x; + aspectRatio[wCameraIndex + 1] = contentRegionSize.x / contentRegionSize.y; + invAspectRatio[wCameraIndex + 1] = contentRegionSize.y / contentRegionSize.x; ImGui::Image(info, contentRegionSize); ImGuizmo::SetRect(cursorPos.x, cursorPos.y, 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 { @@ -1297,34 +1431,17 @@ class UISampleApp final : public examples::SimpleWindowedApplication 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(); + ImVec2 contentRegionSize, windowPos, cursorPos; + contentRegionSize = ImGui::GetContentRegionAvail(); + cursorPos = ImGui::GetCursorScreenPos(); + windowPos = ImGui::GetWindowPos(); ImGui::Image(info, contentRegionSize); ImGuizmo::SetRect(cursorPos.x, cursorPos.y, contentRegionSize.x, contentRegionSize.y); - - viewManipulateRight = cursorPos.x + contentRegionSize.x; - viewManipulateTop = cursorPos.y; - - aspectRatio = io.DisplaySize.x / io.DisplaySize.y; - invAspectRatio = io.DisplaySize.y / io.DisplaySize.x; } - } - void EditTransform(float* cameraView, float* cameraProjection, float* matrix, float* deltaTRS) - { - ImGuiIO& io = ImGui::GetIO(); - float windowWidth = (float)ImGui::GetWindowWidth(); - float windowHeight = (float)ImGui::GetWindowHeight(); - if (!useWindow) - { - ImGuizmo::SetRect(0, 0, io.DisplaySize.x, io.DisplaySize.y); - } - else - { - ImGuizmo::SetRect(ImGui::GetWindowPos().x, ImGui::GetWindowPos().y, windowWidth, windowHeight); - } - ImGuizmo::Manipulate(cameraView, cameraProjection, mCurrentGizmoOperation, mCurrentGizmoMode, matrix, deltaTRS, useSnap ? &snap[0] : NULL); + ImGuiWindow* window = ImGui::GetCurrentWindow(); + gizmoWindowFlags = ImGuiWindowFlags_NoMove;//(ImGuizmo::IsUsing() && ImGui::IsWindowHovered() && ImGui::IsMouseHoveringRect(window->InnerRect.Min, window->InnerRect.Max) ? ImGuiWindowFlags_NoMove : 0); } void TransformEnd() @@ -1367,8 +1484,11 @@ class UISampleApp final : public examples::SimpleWindowedApplication InputSystem::ChannelReader mouse; // Handles keyboard events InputSystem::ChannelReader keyboard; + //! next presentation timestamp + std::chrono::microseconds m_nextPresentationTimestamp = {}; - constexpr static inline auto TexturesAmount = 2u; + // UI font atlas; first camera fb, second camera fb + constexpr static inline auto TotalUISampleTexturesAmount = 3u; core::smart_refctd_ptr m_descriptorSetPool; @@ -1384,35 +1504,37 @@ class UISampleApp final : public examples::SimpleWindowedApplication core::smart_refctd_ptr descriptorSet; }; - nbl::core::smart_refctd_ptr scene; - // lets first test 2 cameras & imguizmo controller, then we will add second scene to render into 2 frame buffers - std::array>, 2u> cameraz; + // one model object in the world, testing 2 cameraz for which view is rendered to separate frame buffers (so what they see) with new controller API including imguizmo + nbl::hlsl::float32_t3x4 m_model = nbl::hlsl::float32_t3x4(1.f); + static constexpr inline auto CamerazCount = 2u; + std::array, CamerazCount> scenez; + std::array>, CamerazCount> cameraz; nbl::core::smart_refctd_ptr resources; - CRenderUI m_ui; + static constexpr inline auto OfflineSceneFirstCameraTextureIx = 1u; + static constexpr inline auto OfflineSceneSecondCameraTextureIx = 2u; - smart_refctd_ptr projection = make_smart_refctd_ptr(); // TMP! + CRenderUI m_ui; video::CDumbPresentationOracle oracle; 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 - static constexpr inline auto OfflineSceneTextureIx = 1u; - - bool isPerspective = true, isLH = true, flipGizmoY = true, move = false; + static constexpr inline auto ProjectionsCount = 3u; + std::array, ProjectionsCount> projections; // TMP! + bool isPerspective[ProjectionsCount] = { true, true, true }, isLH = true, flipGizmoY = true, move = false; 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; - bool firstFrame = true; - - // gizmo stuff - float camDistance = 8.f, aspectRatio = {}, invAspectRatio = {}; + float camDistance = 8.f, aspectRatio[ProjectionsCount] = {}, invAspectRatio[ProjectionsCount] = {}; bool useWindow = true, useSnap = false; ImGuizmo::OPERATION mCurrentGizmoOperation = ImGuizmo::TRANSLATE; ImGuizmo::MODE mCurrentGizmoMode = ImGuizmo::WORLD; float snap[3] = { 1.f, 1.f, 1.f }; int lastUsing = 0; + + bool firstFrame = true; }; NBL_MAIN_FUNC(UISampleApp) \ No newline at end of file diff --git a/common/include/CFPSCamera.hpp b/common/include/CFPSCamera.hpp index 477fa8df4..90e01b4e7 100644 --- a/common/include/CFPSCamera.hpp +++ b/common/include/CFPSCamera.hpp @@ -98,8 +98,8 @@ class CFPSCamera final : public ICamera private: typename base_t::CGimbal m_gimbal; - static inline constexpr auto AllowedLocalVirtualEvents = CVirtualGimbalEvent::MoveForward | CVirtualGimbalEvent::MoveBackward | CVirtualGimbalEvent::MoveRight | CVirtualGimbalEvent::MoveLeft | CVirtualGimbalEvent::TiltUp | CVirtualGimbalEvent::TiltDown | CVirtualGimbalEvent::PanRight | CVirtualGimbalEvent::PanLeft; - static inline constexpr auto AllowedWorldVirtualEvents = AllowedLocalVirtualEvents | CVirtualGimbalEvent::Translate; + static inline constexpr auto AllowedLocalVirtualEvents = CVirtualGimbalEvent::Translate | CVirtualGimbalEvent::TiltUp | CVirtualGimbalEvent::TiltDown | CVirtualGimbalEvent::PanRight | CVirtualGimbalEvent::PanLeft; + static inline constexpr auto AllowedWorldVirtualEvents = AllowedLocalVirtualEvents; static inline const auto m_keyboard_to_virtual_events_preset = []() { @@ -137,6 +137,8 @@ class CFPSCamera final : public ICamera preset[CVirtualGimbalEvent::MoveBackward] = CVirtualGimbalEvent::MoveBackward; preset[CVirtualGimbalEvent::MoveLeft] = CVirtualGimbalEvent::MoveLeft; preset[CVirtualGimbalEvent::MoveRight] = CVirtualGimbalEvent::MoveRight; + preset[CVirtualGimbalEvent::MoveUp] = CVirtualGimbalEvent::MoveUp; + preset[CVirtualGimbalEvent::MoveDown] = CVirtualGimbalEvent::MoveDown; preset[CVirtualGimbalEvent::TiltDown] = CVirtualGimbalEvent::TiltDown; preset[CVirtualGimbalEvent::TiltUp] = CVirtualGimbalEvent::TiltUp; preset[CVirtualGimbalEvent::PanLeft] = CVirtualGimbalEvent::PanLeft; diff --git a/common/include/CGeomtryCreatorScene.hpp b/common/include/CGeomtryCreatorScene.hpp index 07f54b19a..e744b795e 100644 --- a/common/include/CGeomtryCreatorScene.hpp +++ b/common/include/CGeomtryCreatorScene.hpp @@ -500,7 +500,6 @@ struct ResourcesBundle : public virtual nbl::core::IReferenceCounted struct ObjectInstance { - nbl::hlsl::float32_t3x4 model = nbl::hlsl::float32_t3x4(1.f); nbl::asset::SBasicViewParameters viewParameters; ObjectMeta meta; }; From f88acfe727503f4bfb49bfa9bf78d4dafa836ce4 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Tue, 3 Dec 2024 16:44:46 +0100 Subject: [PATCH 043/205] fix gizmo manipulation bugs, make sure a model is manipulated only once regardless GUI window amount --- 61_UI/main.cpp | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/61_UI/main.cpp b/61_UI/main.cpp index 59afdb870..620939966 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -1021,7 +1021,10 @@ class UISampleApp final : public examples::SimpleWindowedApplication // ImGuizmo manipulations on the last used model matrix TransformEditor(lastUsedModel); + uint32_t gizmoID = {}; + bool manipulatedFromAnyWindow = false; + // we have 2 GUI windows we render into with FBOs for (uint32_t windowIndex = 0; windowIndex < 2u; ++windowIndex) { auto* cameraView = views[windowIndex]; @@ -1029,6 +1032,8 @@ class UISampleApp final : public examples::SimpleWindowedApplication TransformStart(windowIndex); + // but only 2 objects with matrices we will try to manipulate & we can do this from both windows! + // however note it only makes sense to obsly assume we cannot manipulate 2 objects at the same time for (uint32_t matId = 0; matId < 2; matId++) { if(matId == 0) @@ -1036,22 +1041,34 @@ class UISampleApp final : public examples::SimpleWindowedApplication else ImGuizmo::AllowAxisFlip(false); - // we must work on copies otherwise we enter bug zone, you need to be careful to not edit the same model twice since we share them with camera fbos + // and because of imguizmo API usage to achive it we must work on copies & filter the output (unless we try enable/disable logic described bellow) + // -> in general again we need to be careful to not edit the same model twice auto model = imguizmoM16InOut.inModel[matId]; float32_t4x4 deltaOutputTRS; - ImGuizmo::PushID(matId); + // note we also need to take care of unique gizmo IDs, we have in total 4 gizmos even though we only want to manipulate 2 objects in total + ImGuizmo::PushID(gizmoID); ImGuiIO& io = ImGui::GetIO(); - bool success = ImGuizmo::Manipulate(cameraView, cameraProjection, mCurrentGizmoOperation, mCurrentGizmoMode, &model[0][0], &deltaOutputTRS[0][0], useSnap ? &snap[0] : NULL); + const bool success = ImGuizmo::Manipulate(cameraView, cameraProjection, mCurrentGizmoOperation, mCurrentGizmoMode, &model[0][0], &deltaOutputTRS[0][0], useSnap ? &snap[0] : NULL); - imguizmoM16InOut.outModel[matId] = model; - imguizmoM16InOut.outDeltaTRS[matId] = deltaOutputTRS; + // we manipulated a gizmo from a X-th window, now we update output matrices and assume no more gizmos can be manipulated at the same frame + // here we could also extend the logic & keep track of the gizmo being handled and as long as its manipulated (hold by mouse) -> then given ID of this gizmo we would + // enable it & disable all other gizmos till we finish the manipulation + if (!manipulatedFromAnyWindow) + { + imguizmoM16InOut.outModel[matId] = model; + imguizmoM16InOut.outDeltaTRS[matId] = deltaOutputTRS; + } + + if (success) + manipulatedFromAnyWindow = true; if (ImGuizmo::IsUsing()) lastUsing = matId; ImGuizmo::PopID(); + ++gizmoID; } TransformEnd(); From 6c9b04ab78d3d2ea55aa1d6708f160a14662688d Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Wed, 4 Dec 2024 10:02:44 +0100 Subject: [PATCH 044/205] add toRetardedImguizmoTRSInput, have nicely aligned *local base* orientation of my gimbals to imguizmo --- 61_UI/main.cpp | 51 ++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 45 insertions(+), 6 deletions(-) diff --git a/61_UI/main.cpp b/61_UI/main.cpp index 620939966..764b15b6a 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -995,10 +995,25 @@ class UISampleApp final : public examples::SimpleWindowedApplication for(uint32_t i = 0u; i < ProjectionsCount; ++i) imguizmoM16InOut.projection[i] = transpose(projections[i]->getMatrix()); + // wth its kinda column major but seems to interprete RS part as row major? + // will think of it later and check what its doing under the hood, now it + // seems to match my local base orientation correctly + auto toRetardedImguizmoTRSInput = [&](const float32_t3x4& nablaTrs) -> float32_t4x4 + { + // *do not transpose whole matrix*, only the translate part + float32_t4x4 trs = getMatrix3x4As4x4(nablaTrs); + trs[3] = float32_t4(nablaTrs[0][3], nablaTrs[1][3], nablaTrs[2][3], 1.f); + trs[0][3] = 0.f; + trs[1][3] = 0.f; + trs[2][3] = 0.f; + + return trs; + }; + // we will transform a scene object's model - imguizmoM16InOut.inModel[0] = transpose(getMatrix3x4As4x4(m_model)); + imguizmoM16InOut.inModel[0] = toRetardedImguizmoTRSInput(m_model); // and second camera's model too - imguizmoM16InOut.inModel[1] = transpose(getMatrix3x4As4x4(secondcamera->getGimbal()())); + imguizmoM16InOut.inModel[1] = toRetardedImguizmoTRSInput(secondcamera->getGimbal()()); { float* views[] { @@ -1036,9 +1051,9 @@ class UISampleApp final : public examples::SimpleWindowedApplication // however note it only makes sense to obsly assume we cannot manipulate 2 objects at the same time for (uint32_t matId = 0; matId < 2; matId++) { - if(matId == 0) - ImGuizmo::AllowAxisFlip(true); - else + //if(matId == 0) + // ImGuizmo::AllowAxisFlip(true); + //else ImGuizmo::AllowAxisFlip(false); // and because of imguizmo API usage to achive it we must work on copies & filter the output (unless we try enable/disable logic described bellow) @@ -1186,7 +1201,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGui::Separator(); }; - // Scene Model Object + // Scene Models { ImGui::SetNextWindowSize(ImVec2(600, 400), ImGuiCond_FirstUseEver); ImGui::Begin("Object Info and Model Matrix", nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysVerticalScrollbar); @@ -1203,6 +1218,30 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGui::End(); } + // ImGuizmo inputs + { + ImGui::SetNextWindowSize(ImVec2(600, 400), ImGuiCond_FirstUseEver); + ImGui::Begin("ImGuizmo model inputs", nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysVerticalScrollbar); + + ImGui::PushStyleColor(ImGuiCol_TableRowBg, ImGui::GetStyleColorVec4(ImGuiCol_FrameBg)); + ImGui::PushStyleColor(ImGuiCol_TableRowBgAlt, ImGui::GetStyleColorVec4(ImGuiCol_FrameBgHovered)); + + ImGui::Text("Type: GC Object"); + ImGui::Separator(); + + addMatrixTable("Model", "GCIMGUIZMOTRS", 4, 4, &imguizmoM16InOut.inModel[0][0][0]); + + ImGui::Text("Type: Camera ID \"1\" object"); + ImGui::Separator(); + + addMatrixTable("Model", "CAMERASECIMGUIZMOTRS", 4, 4, &imguizmoM16InOut.inModel[1][0][0]); + + ImGui::PopStyleColor(2); + ImGui::End(); + } + + //imguizmoM16InOut.inModel + // Cameraz { size_t cameraCount = cameraz.size(); From e9d8a24aff168d3aacef74f86ca5133180a1945b Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Wed, 4 Dec 2024 14:37:55 +0100 Subject: [PATCH 045/205] make the imguizmo controller work for translations --- 61_UI/main.cpp | 109 +++++++++++++------- common/include/camera/IGimbalController.hpp | 56 +++++++--- 2 files changed, 112 insertions(+), 53 deletions(-) diff --git a/61_UI/main.cpp b/61_UI/main.cpp index 764b15b6a..b3a211ee6 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -982,8 +982,6 @@ class UISampleApp final : public examples::SimpleWindowedApplication float32_t4x4 view[CamerazCount], projection[ProjectionsCount], inModel[2u], outModel[2u], outDeltaTRS[2u]; } imguizmoM16InOut; - //const auto& projectionMatrix = projection->getMatrix(); - const auto& firstcamera = cameraz.front(); const auto& secondcamera = cameraz.back(); @@ -995,10 +993,8 @@ class UISampleApp final : public examples::SimpleWindowedApplication for(uint32_t i = 0u; i < ProjectionsCount; ++i) imguizmoM16InOut.projection[i] = transpose(projections[i]->getMatrix()); - // wth its kinda column major but seems to interprete RS part as row major? - // will think of it later and check what its doing under the hood, now it - // seems to match my local base orientation correctly - auto toRetardedImguizmoTRSInput = [&](const float32_t3x4& nablaTrs) -> float32_t4x4 + // TODO: need to inspect where I'm wrong, workaround + auto gimbalToImguizmoTRS = [&](const float32_t3x4& nablaTrs) -> float32_t4x4 { // *do not transpose whole matrix*, only the translate part float32_t4x4 trs = getMatrix3x4As4x4(nablaTrs); @@ -1010,10 +1006,12 @@ class UISampleApp final : public examples::SimpleWindowedApplication return trs; }; + const auto secondCameraGimbalModel = secondcamera->getGimbal()(); + // we will transform a scene object's model - imguizmoM16InOut.inModel[0] = toRetardedImguizmoTRSInput(m_model); + imguizmoM16InOut.inModel[0] = transpose(getMatrix3x4As4x4(m_model)); // and second camera's model too - imguizmoM16InOut.inModel[1] = toRetardedImguizmoTRSInput(secondcamera->getGimbal()()); + imguizmoM16InOut.inModel[1] = gimbalToImguizmoTRS(secondCameraGimbalModel); { float* views[] { @@ -1032,13 +1030,16 @@ class UISampleApp final : public examples::SimpleWindowedApplication for(uint32_t i = 0u; i < ProjectionsCount; ++i) imguizmoM16InOut.projection[i][1][1] *= -1.f; // https://johannesugb.github.io/gpu-programming/why-do-opengl-proj-matrices-fail-in-vulkan/ - float* lastUsedModel = &imguizmoM16InOut.inModel[lastUsing][0][0]; + float* lastUsedModel = &imguizmoM16InOut.inModel[lastManipulatedModelIx][0][0]; // ImGuizmo manipulations on the last used model matrix TransformEditor(lastUsedModel); uint32_t gizmoID = {}; bool manipulatedFromAnyWindow = false; + ImGuizmo::AllowAxisFlip(false); + ImGuizmo::Enable(true); + // we have 2 GUI windows we render into with FBOs for (uint32_t windowIndex = 0; windowIndex < 2u; ++windowIndex) { @@ -1051,13 +1052,12 @@ class UISampleApp final : public examples::SimpleWindowedApplication // however note it only makes sense to obsly assume we cannot manipulate 2 objects at the same time for (uint32_t matId = 0; matId < 2; matId++) { - //if(matId == 0) - // ImGuizmo::AllowAxisFlip(true); - //else - ImGuizmo::AllowAxisFlip(false); - - // and because of imguizmo API usage to achive it we must work on copies & filter the output (unless we try enable/disable logic described bellow) - // -> in general again we need to be careful to not edit the same model twice + // future logic will need to filter gizmos which represents gimbal of camera which view matrix we use to render scene with + if (gizmoID == 3) + continue; + + // and because of imguizmo API usage to achieve it we must work on copies & filter the output (unless we try gizmo enable/disable) + // -> in general we need to be careful to not edit the same model twice auto model = imguizmoM16InOut.inModel[matId]; float32_t4x4 deltaOutputTRS; @@ -1068,19 +1068,49 @@ class UISampleApp final : public examples::SimpleWindowedApplication const bool success = ImGuizmo::Manipulate(cameraView, cameraProjection, mCurrentGizmoOperation, mCurrentGizmoMode, &model[0][0], &deltaOutputTRS[0][0], useSnap ? &snap[0] : NULL); // we manipulated a gizmo from a X-th window, now we update output matrices and assume no more gizmos can be manipulated at the same frame - // here we could also extend the logic & keep track of the gizmo being handled and as long as its manipulated (hold by mouse) -> then given ID of this gizmo we would - // enable it & disable all other gizmos till we finish the manipulation if (!manipulatedFromAnyWindow) { - imguizmoM16InOut.outModel[matId] = model; - imguizmoM16InOut.outDeltaTRS[matId] = deltaOutputTRS; + // TMP, imguizmo controller doesnt support rotation & scale yet + auto discard = gizmoID == 1 && mCurrentGizmoOperation != ImGuizmo::TRANSLATE; + if (!discard) + { + imguizmoM16InOut.outModel[matId] = model; + imguizmoM16InOut.outDeltaTRS[matId] = deltaOutputTRS; + } } if (success) manipulatedFromAnyWindow = true; - + if (ImGuizmo::IsUsing()) - lastUsing = matId; + { + lastManipulatedModelIx = matId; + lastManipulatedGizmoIx = gizmoID; + } + + if (ImGuizmo::IsOver()) + { + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.2f, 0.2f, 0.2f, 0.8f)); + ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.5f); + + ImVec2 mousePos = io.MousePos; + ImGui::SetNextWindowPos(ImVec2(mousePos.x + 10, mousePos.y + 10), ImGuiCond_Always); + + ImGui::Begin("InfoOverlay", NULL, + ImGuiWindowFlags_NoDecoration | + ImGuiWindowFlags_AlwaysAutoResize | + ImGuiWindowFlags_NoSavedSettings); + + ImGui::Text("Type: %s", matId == 0 ? "Geometry Creator Object" : "Camera FPS"); + ImGui::Text("Model ID: %u", matId); + ImGui::Text("Gizmo ID: %u", gizmoID); + + ImGui::End(); + + ImGui::PopStyleVar(); + ImGui::PopStyleColor(2); + } ImGuizmo::PopID(); ++gizmoID; @@ -1091,7 +1121,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication } /* - testing imguizmo controller, we use deltra TRS matrix to generate virtual events + testing imguizmo controller for second camera, we use delta world imguizmo TRS matrix to generate virtual events */ { @@ -1107,25 +1137,31 @@ class UISampleApp final : public examples::SimpleWindowedApplication IGimbalController::SUpdateParameters params; - params.imguizmoEvents = { { imguizmoM16InOut.outDeltaTRS[1u] } }; + auto getOrientationBasis = [&]() + { + auto orientationBasis = (float32_t3x3(1.f)); + + switch (mCurrentGizmoMode) + { + case ImGuizmo::LOCAL: + { + orientationBasis = (float32_t3x3(secondCameraGimbalModel)); + } break; + + case ImGuizmo::WORLD: break; + default: break; + } + return orientationBasis; + }; + + params.imguizmoEvents = { { std::make_pair(imguizmoM16InOut.outDeltaTRS[1u], getOrientationBasis()) } }; secondcamera->process(virtualEvents.data(), vCount, params); } secondcamera->endInputProcessing(); // TOOD: this needs debugging secondcamera->manipulate({ virtualEvents.data(), vCount }, ICamera::Local); - /* - if(vCount) - switch (mCurrentGizmoMode) - { - case ImGuizmo::MODE::LOCAL: - secondcamera->manipulate({ virtualEvents.data(), vCount }, ICamera::Local); break; - case ImGuizmo::MODE::WORLD: - secondcamera->manipulate({ virtualEvents.data(), vCount }, ICamera::World); break; - default: break; - } - */ } // update scenes data @@ -1588,7 +1624,8 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGuizmo::OPERATION mCurrentGizmoOperation = ImGuizmo::TRANSLATE; ImGuizmo::MODE mCurrentGizmoMode = ImGuizmo::WORLD; float snap[3] = { 1.f, 1.f, 1.f }; - int lastUsing = 0; + int lastManipulatedModelIx = 0; + int lastManipulatedGizmoIx = 0; bool firstFrame = true; }; diff --git a/common/include/camera/IGimbalController.hpp b/common/include/camera/IGimbalController.hpp index 1aae6617b..b3f3b5917 100644 --- a/common/include/camera/IGimbalController.hpp +++ b/common/include/camera/IGimbalController.hpp @@ -101,7 +101,7 @@ class IGimbalController : public IGimbalManipulateEncoder using input_mouse_event_t = ui::SMouseEvent; //! input of ImGuizmo gimbal controller process utility - ImGuizmo manipulate utility produces "delta (TRS) matrix" events - using input_imguizmo_event_t = float32_t4x4; + using input_imguizmo_event_t = std::pair; void beginInputProcessing(const std::chrono::microseconds nextPresentationTimeStamp) { @@ -292,31 +292,53 @@ class IGimbalController : public IGimbalManipulateEncoder { preprocess(m_imguizmoVirtualEventMap); - for (const auto& deltaMatrix : events) + for (const auto& ev : events) { - std::array dTranslation, dRotation, dScale; + // TODO: debug assert "orientationBasis" is orthonormal + const auto& [deltaWorldTRS, orientationBasis] = ev; - // TODO: since I assume the delta matrix is from imguizmo I must follow its API (but this one I could actually steal & rewrite to Nabla to not call imguizmo stuff here) - ImGuizmo::DecomposeMatrixToComponents(&deltaMatrix[0][0], dTranslation.data(), dRotation.data(), dScale.data()); + struct + { + float32_t3 dTranslation, dRotation, dScale; + } world, local; // its important to notice our imguizmo deltas are written in world base, so I will assume you generate events with respect for input local basis + + // TODO: since I assume the delta matrix is from imguizmo I must follow its API (but this one I could actually steal & rewrite to Nabla to not call imguizmo stuff here <- its TEMP) + ImGuizmo::DecomposeMatrixToComponents(&deltaWorldTRS[0][0], &world.dTranslation[0], &world.dRotation[0], &world.dScale[0]); + + // FP precision threshold, lets filter small artifacts for this event generator to be more accurate + constexpr float threshold = 1e-8f; - // note that translating imguizmo delta matrix to gimbal_event_t events is indeed hardcoded, but what - // *isn't* is those gimbal_event_t events are then translated according to m_imguizmoVirtualEventMap - // user defined map to + auto filterDelta = [](const float32_t3& in) -> float32_t3 + { + return + { + (std::abs(in[0]) > threshold) ? in[0] : 0.0f, + (std::abs(in[1]) > threshold) ? in[1] : 0.0f, + (std::abs(in[2]) > threshold) ? in[2] : 0.0f + }; + }; + + local.dTranslation = filterDelta(mul(orientationBasis, world.dTranslation)); + + // TODO + local.dRotation = { 0.f, 0.f, 0.f }; + // TODO + local.dScale = { 1.f, 1.f, 1.f }; // Delta translation impulse - requestMagnitudeUpdateWithScalar(0.f, dTranslation[0], dTranslation[0], gimbal_event_t::MoveRight, gimbal_event_t::MoveLeft, m_imguizmoVirtualEventMap); - requestMagnitudeUpdateWithScalar(0.f, dTranslation[1], dTranslation[1], gimbal_event_t::MoveUp, gimbal_event_t::MoveDown, m_imguizmoVirtualEventMap); - requestMagnitudeUpdateWithScalar(0.f, dTranslation[2], dTranslation[2], gimbal_event_t::MoveForward, gimbal_event_t::MoveBackward, m_imguizmoVirtualEventMap); + requestMagnitudeUpdateWithScalar(0.f, local.dTranslation[0], std::abs(local.dTranslation[0]), gimbal_event_t::MoveRight, gimbal_event_t::MoveLeft, m_imguizmoVirtualEventMap); + requestMagnitudeUpdateWithScalar(0.f, local.dTranslation[1], std::abs(local.dTranslation[1]), gimbal_event_t::MoveUp, gimbal_event_t::MoveDown, m_imguizmoVirtualEventMap); + requestMagnitudeUpdateWithScalar(0.f, local.dTranslation[2], std::abs(local.dTranslation[2]), gimbal_event_t::MoveForward, gimbal_event_t::MoveBackward, m_imguizmoVirtualEventMap); // Delta rotation impulse - requestMagnitudeUpdateWithScalar(0.f, dRotation[0], dRotation[0], gimbal_event_t::PanRight, gimbal_event_t::PanLeft, m_imguizmoVirtualEventMap); - requestMagnitudeUpdateWithScalar(0.f, dRotation[1], dRotation[1], gimbal_event_t::TiltUp, gimbal_event_t::TiltDown, m_imguizmoVirtualEventMap); - requestMagnitudeUpdateWithScalar(0.f, dRotation[2], dRotation[2], gimbal_event_t::RollRight, gimbal_event_t::RollLeft, m_imguizmoVirtualEventMap); + requestMagnitudeUpdateWithScalar(0.f, local.dRotation[0], std::abs(local.dRotation[0]), gimbal_event_t::PanRight, gimbal_event_t::PanLeft, m_imguizmoVirtualEventMap); + requestMagnitudeUpdateWithScalar(0.f, local.dRotation[1], std::abs(local.dRotation[1]), gimbal_event_t::TiltUp, gimbal_event_t::TiltDown, m_imguizmoVirtualEventMap); + requestMagnitudeUpdateWithScalar(0.f, local.dRotation[2], std::abs(local.dRotation[2]), gimbal_event_t::RollRight, gimbal_event_t::RollLeft, m_imguizmoVirtualEventMap); // Delta scale impulse - requestMagnitudeUpdateWithScalar(1.f, dScale[0], dScale[0], gimbal_event_t::ScaleXInc, gimbal_event_t::ScaleXDec, m_imguizmoVirtualEventMap); - requestMagnitudeUpdateWithScalar(1.f, dScale[1], dScale[1], gimbal_event_t::ScaleYInc, gimbal_event_t::ScaleYDec, m_imguizmoVirtualEventMap); - requestMagnitudeUpdateWithScalar(1.f, dScale[2], dScale[2], gimbal_event_t::ScaleZInc, gimbal_event_t::ScaleZDec, m_imguizmoVirtualEventMap); + requestMagnitudeUpdateWithScalar(1.f, local.dScale[0], std::abs(local.dScale[0]), gimbal_event_t::ScaleXInc, gimbal_event_t::ScaleXDec, m_imguizmoVirtualEventMap); + requestMagnitudeUpdateWithScalar(1.f, local.dScale[1], std::abs(local.dScale[1]), gimbal_event_t::ScaleYInc, gimbal_event_t::ScaleYDec, m_imguizmoVirtualEventMap); + requestMagnitudeUpdateWithScalar(1.f, local.dScale[2], std::abs(local.dScale[2]), gimbal_event_t::ScaleZInc, gimbal_event_t::ScaleZDec, m_imguizmoVirtualEventMap); } postprocess(m_imguizmoVirtualEventMap, output, count); From bc670ca22dd88affc158216b6c0bf8effc683f78 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Wed, 4 Dec 2024 15:19:58 +0100 Subject: [PATCH 046/205] fix ManipulationMode::World manipulations, treat impulses as relative deltas regardless different world basis --- 61_UI/main.cpp | 21 +++++++++------------ common/include/CFPSCamera.hpp | 27 +++++++-------------------- common/include/ICamera.hpp | 2 +- 3 files changed, 17 insertions(+), 33 deletions(-) diff --git a/61_UI/main.cpp b/61_UI/main.cpp index b3a211ee6..e420ffc81 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -894,7 +894,8 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGui::SameLine(); if (ImGui::RadioButton("RH", !isLH)) - isLH = false; + isLH = true; // TMP WIP + //isLH = false; //if (ImGui::RadioButton("Perspective", isPerspective)) // isPerspective = true; @@ -1070,7 +1071,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication // we manipulated a gizmo from a X-th window, now we update output matrices and assume no more gizmos can be manipulated at the same frame if (!manipulatedFromAnyWindow) { - // TMP, imguizmo controller doesnt support rotation & scale yet + // TMP WIP, our imguizmo controller doesnt support rotation & scale yet auto discard = gizmoID == 1 && mCurrentGizmoOperation != ImGuizmo::TRANSLATE; if (!discard) { @@ -1127,6 +1128,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication { static std::vector virtualEvents(0x45); uint32_t vCount; + auto nblManipulationMode = ICamera::Local; secondcamera->beginInputProcessing(m_nextPresentationTimestamp); { @@ -1143,13 +1145,9 @@ class UISampleApp final : public examples::SimpleWindowedApplication switch (mCurrentGizmoMode) { - case ImGuizmo::LOCAL: - { - orientationBasis = (float32_t3x3(secondCameraGimbalModel)); - } break; - - case ImGuizmo::WORLD: break; - default: break; + case ImGuizmo::LOCAL: orientationBasis = (float32_t3x3(secondCameraGimbalModel)); break; + case ImGuizmo::WORLD: nblManipulationMode = ICamera::World; break; + default: assert(false); break; } return orientationBasis; @@ -1160,8 +1158,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication } secondcamera->endInputProcessing(); - // TOOD: this needs debugging - secondcamera->manipulate({ virtualEvents.data(), vCount }, ICamera::Local); + secondcamera->manipulate({ virtualEvents.data(), vCount }, nblManipulationMode); } // update scenes data @@ -1326,7 +1323,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication addMatrixTable("Position", ("PositionTable" + idxstr).c_str(), 1, 3, &position[0], false); addMatrixTable("Orientation (Quaternion)", ("OrientationTable" + idxstr).c_str(), 1, 4, &orientation[0], false); addMatrixTable("View Matrix", ("ViewMatrixTable" + idxstr).c_str(), 3, 4, &viewMatrix[0][0], false); - addMatrixTable("TRS Matrix", ("TRSMatrixTable" + idxstr).c_str(), 3, 4, &trsMatrix[0][0], true); + //addMatrixTable("TRS Matrix", ("TRSMatrixTable" + idxstr).c_str(), 3, 4, &trsMatrix[0][0], true); ImGui::EndTable(); } diff --git a/common/include/CFPSCamera.hpp b/common/include/CFPSCamera.hpp index 90e01b4e7..ae887aa0c 100644 --- a/common/include/CFPSCamera.hpp +++ b/common/include/CFPSCamera.hpp @@ -42,26 +42,13 @@ class CFPSCamera final : public ICamera glm::quat newOrientation; vector newPosition; - switch (mode) - { - case base_t::Local: - { - const auto impulse = m_gimbal.accumulate(virtualEvents); - const auto newPitch = std::clamp(gPitch + impulse.dVirtualRotation.x * RotateSpeedScale, MinVerticalAngle, MaxVerticalAngle), newYaw = gYaw + impulse.dVirtualRotation.y * RotateSpeedScale; - newOrientation = glm::quat(glm::vec3(newPitch, newYaw, 0.0f)); newPosition = m_gimbal.getPosition() + impulse.dVirtualTranslate * MoveSpeedScale; - } break; - - case base_t::World: - { - // TODO: I make assumption what world base is now (at least temporary), I need to think of this but maybe each ITransformObject should know what its world is - // in ideal scenario we would define this crazy enum with all possible standard bases but in Nabla we only really care about LH/RH coordinate systems - const auto transform = m_gimbal.accumulate(virtualEvents, { 1, 0, 0 }, { 0, 1, 0 }, { 0, 0, 1 }); - const auto newPitch = std::clamp(transform.dVirtualRotation.x, MinVerticalAngle, MaxVerticalAngle), newYaw = transform.dVirtualRotation.y; - newOrientation = glm::quat(glm::vec3(newPitch, newYaw, 0.0f)); newPosition = transform.dVirtualTranslate; - } break; - - default: return false; - } + // TODO: I make assumption what world base is now (at least temporary), I need to think of this but maybe each ITransformObject should know what its world is + // in ideal scenario we would define this crazy enum with all possible standard bases + const auto impulse = mode == base_t::Local ? m_gimbal.accumulate(virtualEvents) + : m_gimbal.accumulate(virtualEvents, { 1, 0, 0 }, { 0, 1, 0 }, { 0, 0, 1 }); + + const auto newPitch = std::clamp(gPitch + impulse.dVirtualRotation.x * RotateSpeedScale, MinVerticalAngle, MaxVerticalAngle), newYaw = gYaw + impulse.dVirtualRotation.y * RotateSpeedScale; + newOrientation = glm::quat(glm::vec3(newPitch, newYaw, 0.0f)); newPosition = m_gimbal.getPosition() + impulse.dVirtualTranslate * MoveSpeedScale; bool manipulated = true; diff --git a/common/include/ICamera.hpp b/common/include/ICamera.hpp index b9a5eca8c..c2980676c 100644 --- a/common/include/ICamera.hpp +++ b/common/include/ICamera.hpp @@ -24,7 +24,7 @@ class ICamera : public IGimbalController, virtual public core::IReferenceCounted // Interpret virtual events as accumulated impulse representing relative manipulation with respect to view gimbal base Local, - // Interpret virtual events as accumulated absolute manipulation with respect to world base + // Interpret virtual events as accumulated impulse representing relative manipulation with respect to world base World }; From 558238ecf8cef2b1367c2b658a7ff70db38d3236 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Thu, 5 Dec 2024 21:57:20 +0100 Subject: [PATCH 047/205] save work to continue tomorrow - start adjusting to comments regarding projections and re-think the design. `IProjection` is now indeed for any type of projection & only provides interface to project/unproject (inverse can fail we return a bool); `ILinearProjection` & `IQuadProjection` provide interfaces for span of their `CProjection`s (custom projection structs inheriting from `IProjection` allowing C class types to have any generic container/range type to store projections. `CCubeProjection` inherits from `IQuadProjection` and we have 6 quads each represented by a projection which is pre-transform nonlinear/skewed projection matrix concatenated with linear projection matrix (viewport) --- 61_UI/include/common.hpp | 3 + 61_UI/main.cpp | 11 +- common/include/camera/CCubeProjection.hpp | 39 +++---- common/include/{ => camera}/CFPSCamera.hpp | 0 common/include/camera/CLinearProjection.hpp | 30 ++++++ common/include/{ => camera}/ICamera.hpp | 13 ++- common/include/camera/ILinearProjection.hpp | 82 ++++++++++++--- common/include/camera/IProjection.hpp | 108 +++++++++----------- common/include/camera/IQuadProjection.hpp | 43 ++++++++ common/include/{ => camera}/IRange.hpp | 0 10 files changed, 227 insertions(+), 102 deletions(-) rename common/include/{ => camera}/CFPSCamera.hpp (100%) create mode 100644 common/include/camera/CLinearProjection.hpp rename common/include/{ => camera}/ICamera.hpp (90%) create mode 100644 common/include/camera/IQuadProjection.hpp rename common/include/{ => camera}/IRange.hpp (100%) diff --git a/61_UI/include/common.hpp b/61_UI/include/common.hpp index c5a1860da..f9e297858 100644 --- a/61_UI/include/common.hpp +++ b/61_UI/include/common.hpp @@ -10,6 +10,9 @@ #include "SimpleWindowedApplication.hpp" #include "InputSystem.hpp" +#include "camera/ILinearProjection.hpp" +#include "camera/IQuadProjection.hpp" + // the example's headers #include "nbl/ui/ICursorControl.h" #include "nbl/ext/ImGui/ImGui.h" diff --git a/61_UI/main.cpp b/61_UI/main.cpp index e420ffc81..9affd29f1 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -905,7 +905,6 @@ class UISampleApp final : public examples::SimpleWindowedApplication //if (ImGui::RadioButton("Orthographic", !isPerspective)) // isPerspective = false; - ImGui::Checkbox("Enable camera movement", &move); ImGui::SliderFloat("Move speed", &moveSpeed, 0.1f, 10.f); ImGui::SliderFloat("Rotate speed", &rotateSpeed, 0.1f, 10.f); @@ -926,6 +925,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication } else { + /* ImGui::Text(ImGuizmo::IsOver() ? "Over gizmo" : ""); ImGui::SameLine(); ImGui::Text(ImGuizmo::IsOver(ImGuizmo::TRANSLATE) ? "Over translate gizmo" : ""); @@ -933,6 +933,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGui::Text(ImGuizmo::IsOver(ImGuizmo::ROTATE) ? "Over rotate gizmo" : ""); ImGui::SameLine(); ImGui::Text(ImGuizmo::IsOver(ImGuizmo::SCALE) ? "Over scale gizmo" : ""); + */ } ImGui::Separator(); @@ -995,11 +996,11 @@ class UISampleApp final : public examples::SimpleWindowedApplication imguizmoM16InOut.projection[i] = transpose(projections[i]->getMatrix()); // TODO: need to inspect where I'm wrong, workaround - auto gimbalToImguizmoTRS = [&](const float32_t3x4& nablaTrs) -> float32_t4x4 + auto gimbalToImguizmoTRS = [&](const float32_t3x4& nblGimbalTrs) -> float32_t4x4 { // *do not transpose whole matrix*, only the translate part - float32_t4x4 trs = getMatrix3x4As4x4(nablaTrs); - trs[3] = float32_t4(nablaTrs[0][3], nablaTrs[1][3], nablaTrs[2][3], 1.f); + float32_t4x4 trs = getMatrix3x4As4x4(nblGimbalTrs); + trs[3] = float32_t4(nblGimbalTrs[0][3], nblGimbalTrs[1][3], nblGimbalTrs[2][3], 1.f); trs[0][3] = 0.f; trs[1][3] = 0.f; trs[2][3] = 0.f; @@ -1498,7 +1499,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGui::SetNextWindowSize(ImVec2(800, 400), ImGuiCond_Appearing); ImGui::SetNextWindowPos(ImVec2(400, 20 + wCameraIndex * 420), ImGuiCond_Appearing); ImGui::PushStyleColor(ImGuiCol_WindowBg, (ImVec4)ImColor(0.35f, 0.3f, 0.3f)); - std::string ident = "Active Camera ID: " + std::to_string(wCameraIndex); + std::string ident = "Rendered from Camera \"" + std::to_string(wCameraIndex) + "\" Ix perspective"; ImGui::Begin(ident.data(), 0, gizmoWindowFlags); ImGuizmo::SetDrawlist(); diff --git a/common/include/camera/CCubeProjection.hpp b/common/include/camera/CCubeProjection.hpp index e09d01dce..0bd6e19a7 100644 --- a/common/include/camera/CCubeProjection.hpp +++ b/common/include/camera/CCubeProjection.hpp @@ -1,34 +1,37 @@ #ifndef _NBL_CCUBE_PROJECTION_HPP_ #define _NBL_CCUBE_PROJECTION_HPP_ -#include "ILinearProjection.hpp" +#include "IRange.hpp" +#include "IQuadProjection.hpp" namespace nbl::hlsl { -template -struct CCubeProjectionConstraints -{ - using matrix_t = typename T; - using projection_t = typename IProjection; - using projection_range_t = std::array, 6u>; - using base_t = ILinearProjection; -}; - -//! Class providing linear cube projections with projection matrix per face of a cube, each projection matrix represents a single view-port -template -class CCubeProjection : public CCubeProjectionConstraints::base_t +// A projection where each cube face is a quad we project onto +class CCubeProjection final : public IQuadProjection { public: - using constraints_t = CCubeProjectionConstraints; - using base_t = typename constraints_t::base_t; + static inline constexpr auto CubeFaces = 6u; + + inline static core::smart_refctd_ptr create(core::smart_refctd_ptr&& camera) + { + if (!camera) + return nullptr; - CCubeProjection(constraints_t::projection_range_t&& projections = {}) : base_t(std::move(projections)) {} + return core::smart_refctd_ptr(new CCubeProjection(core::smart_refctd_ptr(camera)), core::dont_grab); + } - constraints_t::projection_range_t& getCubeFaceProjections() + virtual std::span getQuadProjections() const { - return base_t::getViewportProjections(); + return m_cubefaceProjections; } + +private: + CCubeProjection(core::smart_refctd_ptr&& camera) + : IQuadProjection(core::smart_refctd_ptr(camera)) {} + virtual ~CCubeProjection() = default; + + std::array m_cubefaceProjections; }; } // nbl::hlsl namespace diff --git a/common/include/CFPSCamera.hpp b/common/include/camera/CFPSCamera.hpp similarity index 100% rename from common/include/CFPSCamera.hpp rename to common/include/camera/CFPSCamera.hpp diff --git a/common/include/camera/CLinearProjection.hpp b/common/include/camera/CLinearProjection.hpp new file mode 100644 index 000000000..6dbe41b82 --- /dev/null +++ b/common/include/camera/CLinearProjection.hpp @@ -0,0 +1,30 @@ +#ifndef _NBL_C_LINEAR_PROJECTION_HPP_ +#define _NBL_C_LINEAR_PROJECTION_HPP_ + +#include "ILinearProjection.hpp" + +namespace nbl::hlsl +{ + +class CLinearProjection : public ILinearProjection +{ +public: + using ILinearProjection::ILinearProjection; + + inline static core::smart_refctd_ptr create(core::smart_refctd_ptr&& camera) + { + if (!camera) + return nullptr; + + return core::smart_refctd_ptr(new CLinearProjection(core::smart_refctd_ptr(camera)), core::dont_grab); + } + +private: + CLinearProjection(core::smart_refctd_ptr&& camera) + : ILinearProjection(core::smart_refctd_ptr(camera)) {} + virtual ~CLinearProjection() = default; +}; + +} // nbl::hlsl namespace + +#endif // _NBL_C_LINEAR_PROJECTION_HPP_ \ No newline at end of file diff --git a/common/include/ICamera.hpp b/common/include/camera/ICamera.hpp similarity index 90% rename from common/include/ICamera.hpp rename to common/include/camera/ICamera.hpp index c2980676c..d4b3dbe8c 100644 --- a/common/include/ICamera.hpp +++ b/common/include/camera/ICamera.hpp @@ -10,12 +10,11 @@ namespace nbl::hlsl // TODO: DIFFERENT NAMESPACE { -template class ICamera : public IGimbalController, virtual public core::IReferenceCounted { public: + using precision_t = float64_t; using IGimbalController::IGimbalController; - using precision_t = T; //! Manipulation mode for virtual events //! TODO: this should belong to IObjectTransform or something @@ -46,17 +45,17 @@ class ICamera : public IGimbalController, virtual public core::IReferenceCounted { const auto& gRight = base_t::getXAxis(), gUp = base_t::getYAxis(), gForward = base_t::getZAxis(); - auto isNormalized = [](const auto& v, float epsilon) -> bool + auto isNormalized = [](const auto& v, precision_t epsilon) -> bool { - return glm::epsilonEqual(glm::length(v), 1.0f, epsilon); + return glm::epsilonEqual(glm::length(v), 1.0, epsilon); }; - auto isOrthogonal = [](const auto& a, const auto& b, float epsilon) -> bool + auto isOrthogonal = [](const auto& a, const auto& b, precision_t epsilon) -> bool { - return glm::epsilonEqual(glm::dot(a, b), 0.0f, epsilon); + return glm::epsilonEqual(glm::dot(a, b), 0.0, epsilon); }; - auto isOrthoBase = [&](const auto& x, const auto& y, const auto& z, float epsilon = 1e-6f) -> bool + auto isOrthoBase = [&](const auto& x, const auto& y, const auto& z, precision_t epsilon = 1e-6) -> bool { return isNormalized(x, epsilon) && isNormalized(y, epsilon) && isNormalized(z, epsilon) && isOrthogonal(x, y, epsilon) && isOrthogonal(x, z, epsilon) && isOrthogonal(y, z, epsilon); diff --git a/common/include/camera/ILinearProjection.hpp b/common/include/camera/ILinearProjection.hpp index 30a4db358..a03e15cb4 100644 --- a/common/include/camera/ILinearProjection.hpp +++ b/common/include/camera/ILinearProjection.hpp @@ -1,29 +1,83 @@ -#ifndef _NBL_ILINEAR_PROJECTION_HPP_ -#define _NBL_ILINEAR_PROJECTION_HPP_ +#ifndef _NBL_I_LINEAR_PROJECTION_HPP_ +#define _NBL_I_LINEAR_PROJECTION_HPP_ #include "IProjection.hpp" +#include "ICamera.hpp" namespace nbl::hlsl { -//! Interface class for linear projections range storage - a projection matrix represents single view-port -template -class ILinearProjection : protected IProjectionRange +//! Interface class for linear projections like Perspective, Orthographic, Oblique, Axonometric, Shear projections or any custom linear transformation - any linear projection represents transform to a viewport +class ILinearProjection { public: - using base_t = typename IProjectionRange; - using range_t = typename base_t::range_t; - using projection_t = typename base_t::projection_t; + struct CViewportProjection : public IProjection + { + using IProjection::IProjection; + + //! underlying type for linear projection matrix type + using projection_matrix_t = float64_t4x4; + + inline void setProjectionMatrix(const projection_matrix_t& matrix) + { + m_projectionMatrix = matrix; + const auto det = hlsl::determinant(m_projectionMatrix); + + // no singularity for linear projections, but we need to handle it somehow! + m_isProjectionSingular = not det; + + if (m_isProjectionSingular) + { + m_isProjectionLeftHanded = std::nullopt; + m_invProjectionMatrix = std::nullopt; + } + else + { + m_isProjectionLeftHanded = det < 0.0; + m_invProjectionMatrix = inverse(m_projectionMatrix); + } + } + + inline const projection_matrix_t& getProjectionMatrix() const { return m_projectionMatrix; } + inline const std::optional& getInvProjectionMatrix() const { return m_invProjectionMatrix; } + inline const std::optional& isProjectionLeftHanded() const { return m_isProjectionLeftHanded; } + inline bool isProjectionSingular() const { return m_isProjectionSingular; } + virtual ProjectionType getProjectionType() const override { return ProjectionType::Linear; } + + virtual void project(const projection_vector_t& vecToProjectionSpace, projection_vector_t& output) const override + { + output = mul(m_projectionMatrix, vecToProjectionSpace); + } - ILinearProjection(range_t&& projections) : base_t(std::move(projections)) {} + virtual bool unproject(const projection_vector_t& vecFromProjectionSpace, projection_vector_t& output) const override + { + if (m_isProjectionSingular) + return false; + + output = mul(m_invProjectionMatrix.value(), vecFromProjectionSpace); + + return true; + } + + private: + projection_matrix_t m_projectionMatrix; + std::optional m_invProjectionMatrix; + std::optional m_isProjectionLeftHanded; + bool m_isProjectionSingular; + }; + + using CProjection = CViewportProjection; + + virtual std::span getViewportProjections() const = 0; protected: - inline range_t& getViewportProjections() - { - return base_t::m_projectionRange; - } + ILinearProjection(core::smart_refctd_ptr&& camera) + : m_camera(core::smart_refctd_ptr(camera)) {} + virtual ~ILinearProjection() = default; + + core::smart_refctd_ptr m_camera; }; } // nbl::hlsl namespace -#endif // _NBL_ILINEAR_PROJECTION_HPP_ \ No newline at end of file +#endif // _NBL_I_LINEAR_PROJECTION_HPP_ \ No newline at end of file diff --git a/common/include/camera/IProjection.hpp b/common/include/camera/IProjection.hpp index 8de5c19f6..eb985c1b0 100644 --- a/common/include/camera/IProjection.hpp +++ b/common/include/camera/IProjection.hpp @@ -1,70 +1,62 @@ -#ifndef _NBL_IPROJECTION_HPP_ -#define _NBL_IPROJECTION_HPP_ +#ifndef _NBL_I_PROJECTION_HPP_ +#define _NBL_I_PROJECTION_HPP_ -#include "nbl/builtin/hlsl/cpp_compat/matrix.hlsl" -#include "IRange.hpp" +#include namespace nbl::hlsl { -template -concept ProjectionMatrix = is_any_of_v; - -//! Interface class for projection -template -class IProjection : virtual public core::IReferenceCounted +//! Interface class for any type of projection +class IProjection { public: - using value_t = T; - - IProjection(const value_t& matrix = {}) : m_projectionMatrix(matrix) { updateHandnessState(); } - - inline void setMatrix(const value_t& projectionMatrix) - { - m_projectionMatrix = projectionMatrix; - updateHandnessState(); - } - - inline const value_t& getMatrix() { return m_projectionMatrix; } - inline bool isLeftHanded() { return m_isLeftHanded; } + //! underlying type for all vectors we project or un-project (inverse), projections *may* transform vectors in less dimensions + using projection_vector_t = float64_t4; -private: - inline void updateHandnessState() + enum class ProjectionType { - m_isLeftHanded = hlsl::determinant(m_projectionMatrix) < 0.f; - } - - value_t m_projectionMatrix; - bool m_isLeftHanded; -}; - -template -struct is_projection : std::false_type {}; - -template -struct is_projection> : std::true_type {}; - -template -inline constexpr bool is_projection_v = is_projection::value; - -template -concept ProjectionRange = GeneralPurposeRange && requires -{ - requires core::is_smart_refctd_ptr_v>; - requires is_projection_v::pointee>; -}; - -//! Interface class for a range of IProjection projections -template>, 1u>> -class IProjectionRange : public IRange -{ -public: - using base_t = IRange; - using range_t = typename base_t::range_t; - using projection_t = typename base_t::range_value_t; - - IProjectionRange(range_t&& projections) : base_t(std::move(projections)) {} - const range_t& getProjections() const { return base_t::m_range; } + //! Perspective, Orthographic, Oblique, Axonometric, Shear projections or any custom linear transformation - a linear projection represents transform to a view-port + Linear, + + //! Represents a non-linear/skewed pre-transform *concatenated* with linear view-port transform + Quad, + + Spherical, + ThinLens, + + Count + }; + + IProjection() = default; + virtual ~IProjection() = default; + + /** + * @brief Transforms a vector from its input space into the projection space. + * + * @param "vecToProjectionSpace" is a vector to transform from its space into projection space. + * @param "output" is a vector which is "vecToProjectionSpace" transformed into projection space. + * @return The vector in projection space. + */ + virtual void project(const projection_vector_t& vecToProjectionSpace, projection_vector_t& output) const = 0; + + /** + * @brief Transforms a vector from the projection space back to the original space. + * Note the inverse transform may fail because original projection may be singular. + * + * @param "vecFromProjectionSpace" is a vector in the projection space to transform back to original space. + * @param "output" is a vector which is "vecFromProjectionSpace" transformed back to its original space. + * @return The vector in the original space. + */ + virtual bool unproject(const projection_vector_t& vecFromProjectionSpace, projection_vector_t& output) const = 0; + + /** + * @brief Returns the specific type of the projection + * (e.g., linear, spherical, thin-lens) as defined by the + * ProjectionType enumeration. + * + * @return The type of this projection. + */ + virtual ProjectionType getProjectionType() const = 0; }; } // namespace nbl::hlsl diff --git a/common/include/camera/IQuadProjection.hpp b/common/include/camera/IQuadProjection.hpp new file mode 100644 index 000000000..28e973572 --- /dev/null +++ b/common/include/camera/IQuadProjection.hpp @@ -0,0 +1,43 @@ +#ifndef _NBL_I_QUAD_PROJECTION_HPP_ +#define _NBL_I_QUAD_PROJECTION_HPP_ + +#include "ILinearProjection.hpp" + +namespace nbl::hlsl +{ + +//! Interface class for quad projections, basically represents a non-linear/skewed pre-transform concatenated with linear viewport transform, think of it as single quad of a cave designer +class IQuadProjection +{ +public: + struct CCaveFaceProjection : public ILinearProjection::CViewportProjection + { + using base_t = ILinearProjection::CViewportProjection; + + //! underlying type for pre-transform projection matrix type + using pretransform_matrix_t = float64_t3x4; + + inline void setProjectionMatrix(const pretransform_matrix_t& pretransform, const base_t::projection_matrix_t& viewport) + { + auto projection = mul(pretransform, viewport); + base_t::setProjectionMatrix(getMatrix3x4As4x4(projection)); + } + + // TODO: could store "pretransform" & "viewport", may be useful to combine with camera and extract matrices + }; + + using CProjection = CCaveFaceProjection; + + virtual std::span getQuadProjections() const = 0; + +protected: + IQuadProjection(core::smart_refctd_ptr&& camera) + : m_camera(core::smart_refctd_ptr(camera)) {} + virtual ~IQuadProjection() = default; + + core::smart_refctd_ptr m_camera; +}; + +} // nbl::hlsl namespace + +#endif // _NBL_I_QUAD_PROJECTION_HPP_ \ No newline at end of file diff --git a/common/include/IRange.hpp b/common/include/camera/IRange.hpp similarity index 100% rename from common/include/IRange.hpp rename to common/include/camera/IRange.hpp From c3b0118566d3d45a52b4f24f2dc3e78a1e09136e Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Fri, 6 Dec 2024 10:40:17 +0100 Subject: [PATCH 048/205] make CCubeProjection use IQuadProjection & IProjection interface, update ILinearProjection to split out `MVP`, `MV`, `MV-1`, `MVP-1` matrices (`P`, `P-1` are returned by `ILinearProjection::CProjection` getters) --- 61_UI/include/common.hpp | 2 +- common/include/camera/CCubeProjection.hpp | 67 ++++++++++-- common/include/camera/IGimbal.hpp | 4 +- common/include/camera/ILinearProjection.hpp | 113 +++++++++++++++++--- common/include/camera/IProjection.hpp | 3 + common/include/camera/IQuadProjection.hpp | 46 ++++---- 6 files changed, 192 insertions(+), 43 deletions(-) diff --git a/61_UI/include/common.hpp b/61_UI/include/common.hpp index f9e297858..92335efa8 100644 --- a/61_UI/include/common.hpp +++ b/61_UI/include/common.hpp @@ -6,7 +6,7 @@ #include // common api -#include "CFPSCamera.hpp" +#include "camera/CFPSCamera.hpp" #include "SimpleWindowedApplication.hpp" #include "InputSystem.hpp" diff --git a/common/include/camera/CCubeProjection.hpp b/common/include/camera/CCubeProjection.hpp index 0bd6e19a7..9c3fae30b 100644 --- a/common/include/camera/CCubeProjection.hpp +++ b/common/include/camera/CCubeProjection.hpp @@ -7,11 +7,40 @@ namespace nbl::hlsl { -// A projection where each cube face is a quad we project onto -class CCubeProjection final : public IQuadProjection +/** +* @brief A projection where each cube face is a quad we project onto. +* +* Represents a cube projection given direction vector where each face of +* the cube is treated as a quad. The projection onto the cube is done using +* these quads and each face has its own unique pre-transform and +* view-port linear matrix. +*/ +class CCubeProjection final : public IQuadProjection, public IProjection { public: - static inline constexpr auto CubeFaces = 6u; + //! Represents six face identifiers of a cube. + enum CubeFaces : uint8_t + { + //! Cube face in the +X base direction + PositiveX = 0, + + //! Cube face in the -X base direction + NegativeX, + + //! Cube face in the +Y base direction + PositiveY, + + //! Cube face in the -Y base direction + NegativeY, + + //! Cube face in the +Z base direction + PositiveZ, + + //! Cube face in the -Z base direction + NegativeZ, + + CubeFacesCount + }; inline static core::smart_refctd_ptr create(core::smart_refctd_ptr&& camera) { @@ -21,9 +50,35 @@ class CCubeProjection final : public IQuadProjection return core::smart_refctd_ptr(new CCubeProjection(core::smart_refctd_ptr(camera)), core::dont_grab); } - virtual std::span getQuadProjections() const + virtual std::span getLinearProjections() const override + { + return { reinterpret_cast(m_quads.data()), m_quads.size() }; + } + + void transformCube() + { + // TODO: update m_quads + } + + virtual ProjectionType getProjectionType() const override { return ProjectionType::Cube; } + + virtual void project(const projection_vector_t& vecToProjectionSpace, projection_vector_t& output) const override + { + auto direction = normalize(vecToProjectionSpace); + + // TODO: project onto cube using quads representing faces + } + + virtual bool unproject(const projection_vector_t& vecFromProjectionSpace, projection_vector_t& output) const override + { + // TODO: return back direction vector? + } + + template + requires (FaceIx != CubeFacesCount) + inline const CProjection& getQuad() { - return m_cubefaceProjections; + return m_quads[FaceIx]; } private: @@ -31,7 +86,7 @@ class CCubeProjection final : public IQuadProjection : IQuadProjection(core::smart_refctd_ptr(camera)) {} virtual ~CCubeProjection() = default; - std::array m_cubefaceProjections; + std::array m_quads; }; } // nbl::hlsl namespace diff --git a/common/include/camera/IGimbal.hpp b/common/include/camera/IGimbal.hpp index a3d153b92..3fe98d3eb 100644 --- a/common/include/camera/IGimbal.hpp +++ b/common/include/camera/IGimbal.hpp @@ -96,6 +96,8 @@ namespace nbl::hlsl { public: using precision_t = T; + //! underlying type for world matrix (TRS) + using model_matrix_t = matrix; struct VirtualImpulse { @@ -303,7 +305,7 @@ namespace nbl::hlsl inline const auto& getScale() const { return m_scale; } //! World matrix (TRS) - inline const matrix operator()() const + inline const model_matrix_t operator()() const { const auto& position = getPosition(); const auto& rotation = getOrthonornalMatrix(); diff --git a/common/include/camera/ILinearProjection.hpp b/common/include/camera/ILinearProjection.hpp index a03e15cb4..069507f01 100644 --- a/common/include/camera/ILinearProjection.hpp +++ b/common/include/camera/ILinearProjection.hpp @@ -1,4 +1,4 @@ -#ifndef _NBL_I_LINEAR_PROJECTION_HPP_ +#ifndef _NBL_I_LINEAR_PROJECTION_HPP_ #define _NBL_I_LINEAR_PROJECTION_HPP_ #include "IProjection.hpp" @@ -7,23 +7,43 @@ namespace nbl::hlsl { -//! Interface class for linear projections like Perspective, Orthographic, Oblique, Axonometric, Shear projections or any custom linear transformation - any linear projection represents transform to a viewport +/** + * @brief Interface class for any custom linear projection transformation (matrix elements are already evaluated scalars) + * referencing a camera, great for Perspective, Orthographic, Oblique, Axonometric and Shear projections + */ class ILinearProjection { +protected: + ILinearProjection(core::smart_refctd_ptr&& camera) + : m_camera(core::smart_refctd_ptr(camera)) {} + virtual ~ILinearProjection() = default; + + core::smart_refctd_ptr m_camera; public: - struct CViewportProjection : public IProjection + //! underlying type for linear world TRS matrix + using model_matrix_t = typename decltype(m_camera)::pointee::CGimbal::model_matrix_t; + + //! underlying type for linear concatenated matrix + using concatenated_matrix_t = float64_t4x4; + + //! underlying type for linear inverse of concatenated matrix + using inv_concatenated_matrix_t = std::optional; + + struct CProjection : public IProjection { using IProjection::IProjection; + using projection_matrix_t = concatenated_matrix_t; + using inv_projection_matrix_t = inv_concatenated_matrix_t; - //! underlying type for linear projection matrix type - using projection_matrix_t = float64_t4x4; + CProjection(const projection_matrix_t& matrix) { setProjectionMatrix(matrix); } inline void setProjectionMatrix(const projection_matrix_t& matrix) { m_projectionMatrix = matrix; const auto det = hlsl::determinant(m_projectionMatrix); - // no singularity for linear projections, but we need to handle it somehow! + // we will allow you to lose a dimension since such a projection itself *may* + // be valid, however then you cannot un-project because the inverse doesn't exist! m_isProjectionSingular = not det; if (m_isProjectionSingular) @@ -38,8 +58,12 @@ class ILinearProjection } } + //! Returns P (Projection matrix) inline const projection_matrix_t& getProjectionMatrix() const { return m_projectionMatrix; } - inline const std::optional& getInvProjectionMatrix() const { return m_invProjectionMatrix; } + + //! Returns Pâ»Â¹ (Inverse of Projection matrix) *if it exists* + inline const inv_projection_matrix_t& getInvProjectionMatrix() const { return m_invProjectionMatrix; } + inline const std::optional& isProjectionLeftHanded() const { return m_isProjectionLeftHanded; } inline bool isProjectionSingular() const { return m_isProjectionSingular; } virtual ProjectionType getProjectionType() const override { return ProjectionType::Linear; } @@ -61,21 +85,76 @@ class ILinearProjection private: projection_matrix_t m_projectionMatrix; - std::optional m_invProjectionMatrix; + inv_projection_matrix_t m_invProjectionMatrix; std::optional m_isProjectionLeftHanded; bool m_isProjectionSingular; }; - using CProjection = CViewportProjection; - - virtual std::span getViewportProjections() const = 0; + virtual std::span getLinearProjections() const = 0; -protected: - ILinearProjection(core::smart_refctd_ptr&& camera) - : m_camera(core::smart_refctd_ptr(camera)) {} - virtual ~ILinearProjection() = default; - - core::smart_refctd_ptr m_camera; + /** + * @brief Computes Model View (MV) matrix + * @param "model" is world TRS matrix + * @return Returns MV matrix + */ + inline concatenated_matrix_t getMV(const model_matrix_t& model) const + { + const auto& v = m_camera->getGimbal().getView().matrix; + return mul(getMatrix3x4As4x4(v), getMatrix3x4As4x4(model)); + } + + /** + * @brief Computes Model View Projection (MVP) matrix + * @param "projection" is linear projection + * @param "model" is world TRS matrix + * @return Returns MVP matrix + */ + inline concatenated_matrix_t getMVP(const CProjection& projection, const model_matrix_t& model) const + { + const auto& v = m_camera->getGimbal().getView().matrix; + const auto& p = projection.getProjectionMatrix(); + auto mv = mul(getMatrix3x4As4x4(v), getMatrix3x4As4x4(model)); + return mul(p, mv); + } + + /** + * @brief Computes Model View Projection (MVP) matrix + * @param "projection" is linear projection + * @param "mv" is Model View (MV) matrix + * @return Returns MVP matrix + */ + inline concatenated_matrix_t getMVP(const CProjection& projection, const concatenated_matrix_t& mv) const + { + const auto& p = projection.getProjectionMatrix(); + return mul(p, mv); + } + + /** + * @brief Computes Inverse of Model View ((MV)â»Â¹) matrix + * @param "mv" is Model View (MV) matrix + * @return Returns ((MV)â»Â¹) matrix *if it exists*, otherwise returns std::nullopt + */ + inline inv_concatenated_matrix_t getMVInverse(const model_matrix_t& model) const + { + const auto mv = getMV(model); + if (auto det = determinant(mv); det) + return inverse(mv); + return std::nullopt; + } + + /** + * @brief Computes Inverse of Model View Projection ((MVP)â»Â¹) matrix + * @param "projection" is linear projection + * @param "model" is world TRS matrix + * @return Returns ((MVP)â»Â¹) matrix *if it exists*, otherwise returns std::nullopt + */ + inline inv_concatenated_matrix_t getMVPInverse(const CProjection& projection, const model_matrix_t& model) const + { + const auto mvp = getMVP(projection, model); + if (auto det = determinant(mvp); det) + return inverse(mvp); + return std::nullopt; + } }; } // nbl::hlsl namespace diff --git a/common/include/camera/IProjection.hpp b/common/include/camera/IProjection.hpp index eb985c1b0..8b3b9337c 100644 --- a/common/include/camera/IProjection.hpp +++ b/common/include/camera/IProjection.hpp @@ -21,6 +21,9 @@ class IProjection //! Represents a non-linear/skewed pre-transform *concatenated* with linear view-port transform Quad, + //! Represents a projection onto cube consisting of 6 quad cube faces + Cube, + Spherical, ThinLens, diff --git a/common/include/camera/IQuadProjection.hpp b/common/include/camera/IQuadProjection.hpp index 28e973572..7a4d5ebaa 100644 --- a/common/include/camera/IQuadProjection.hpp +++ b/common/include/camera/IQuadProjection.hpp @@ -6,36 +6,46 @@ namespace nbl::hlsl { -//! Interface class for quad projections, basically represents a non-linear/skewed pre-transform concatenated with linear viewport transform, think of it as single quad of a cave designer -class IQuadProjection +/** +* @brief Interface class for quad projections. +* +* This projection transforms a vector into the **model space of a quad** +* (defined by the pre-transform matrix) and then projects it onto the quad using +* the linear view-port transform. +* +* A quad projection is represented by: +* - A **pre-transform matrix** (non-linear/skewed transformation). +* - A **linear view-port transform matrix**. +* +* The final projection matrix is the concatenation of the pre-transform and the linear view-port transform. +* +* @note Single quad projection can represent a face quad of a CAVE-like system. +*/ +class IQuadProjection : public ILinearProjection { public: - struct CCaveFaceProjection : public ILinearProjection::CViewportProjection + struct CProjection : ILinearProjection::CProjection { - using base_t = ILinearProjection::CViewportProjection; + using base_t = ILinearProjection::CProjection; - //! underlying type for pre-transform projection matrix type - using pretransform_matrix_t = float64_t3x4; - - inline void setProjectionMatrix(const pretransform_matrix_t& pretransform, const base_t::projection_matrix_t& viewport) + inline void setQuadTransform(const ILinearProjection::model_matrix_t& pretransform, ILinearProjection::concatenated_matrix_t viewport) { - auto projection = mul(pretransform, viewport); - base_t::setProjectionMatrix(getMatrix3x4As4x4(projection)); + auto concatenated = mul(getMatrix3x4As4x4(pretransform), viewport); + base_t::setProjectionMatrix(concatenated); + + m_pretransform = pretransform; + m_viewport = viewport; } - // TODO: could store "pretransform" & "viewport", may be useful to combine with camera and extract matrices + private: + ILinearProjection::model_matrix_t m_pretransform; + ILinearProjection::concatenated_matrix_t m_viewport; }; - using CProjection = CCaveFaceProjection; - - virtual std::span getQuadProjections() const = 0; - protected: IQuadProjection(core::smart_refctd_ptr&& camera) - : m_camera(core::smart_refctd_ptr(camera)) {} + : ILinearProjection(core::smart_refctd_ptr(camera)) {} virtual ~IQuadProjection() = default; - - core::smart_refctd_ptr m_camera; }; } // nbl::hlsl namespace From a0a13801e38060c891a347915e855cd961045f70 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Tue, 10 Dec 2024 14:37:35 +0100 Subject: [PATCH 049/205] add some getters to IQuadProjection --- common/include/camera/IProjection.hpp | 2 +- common/include/camera/IQuadProjection.hpp | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/common/include/camera/IProjection.hpp b/common/include/camera/IProjection.hpp index 8b3b9337c..7e145ffee 100644 --- a/common/include/camera/IProjection.hpp +++ b/common/include/camera/IProjection.hpp @@ -15,7 +15,7 @@ class IProjection enum class ProjectionType { - //! Perspective, Orthographic, Oblique, Axonometric, Shear projections or any custom linear transformation - a linear projection represents transform to a view-port + //! Perspective, Orthographic, Oblique, Axonometric, Shear projections or any custom linear transformation Linear, //! Represents a non-linear/skewed pre-transform *concatenated* with linear view-port transform diff --git a/common/include/camera/IQuadProjection.hpp b/common/include/camera/IQuadProjection.hpp index 7a4d5ebaa..43ba680ec 100644 --- a/common/include/camera/IQuadProjection.hpp +++ b/common/include/camera/IQuadProjection.hpp @@ -28,6 +28,11 @@ class IQuadProjection : public ILinearProjection { using base_t = ILinearProjection::CProjection; + CProjection(const ILinearProjection::model_matrix_t& pretransform, ILinearProjection::concatenated_matrix_t viewport) + { + setQuadTransform(pretransform, viewport); + } + inline void setQuadTransform(const ILinearProjection::model_matrix_t& pretransform, ILinearProjection::concatenated_matrix_t viewport) { auto concatenated = mul(getMatrix3x4As4x4(pretransform), viewport); @@ -37,6 +42,9 @@ class IQuadProjection : public ILinearProjection m_viewport = viewport; } + inline const ILinearProjection::model_matrix_t& getPretransform() const { return m_pretransform; } + inline const ILinearProjection::concatenated_matrix_t& getViewportProjection() const { return m_viewport; } + private: ILinearProjection::model_matrix_t m_pretransform; ILinearProjection::concatenated_matrix_t m_viewport; From c02e9f67c5c3ee0d6a2afc8dda5766cafba29401 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Thu, 12 Dec 2024 11:06:38 +0100 Subject: [PATCH 050/205] remove templates from ICamera interface, hardcode 64bit for its gimbal and vector/matrix data, add `getCastedMatrix` & `getCastedVector` to obtain data with specified precision, rename `IQuadProjection` to `IPerspectiveProjection`. Create ContiguousGeneralPurposeRangeOf concept and use in `CLinearProjection` class, adjust main.cpp to new changes and use `CLinearProjection` with fixed `std::array` range in the example --- 61_UI/include/common.hpp | 6 +- 61_UI/include/keysmapping.hpp | 8 +-- 61_UI/main.cpp | 70 ++++++++++--------- common/include/camera/CCubeProjection.hpp | 10 +-- common/include/camera/CFPSCamera.hpp | 17 +++-- common/include/camera/CLinearProjection.hpp | 16 +++++ common/include/camera/ICamera.hpp | 17 +++-- common/include/camera/ILinearProjection.hpp | 18 ++++- ...jection.hpp => IPerspectiveProjection.hpp} | 21 +++--- common/include/camera/IProjection.hpp | 6 +- common/include/camera/IRange.hpp | 17 ++--- 11 files changed, 112 insertions(+), 94 deletions(-) rename common/include/camera/{IQuadProjection.hpp => IPerspectiveProjection.hpp} (71%) diff --git a/61_UI/include/common.hpp b/61_UI/include/common.hpp index 92335efa8..0c33a5813 100644 --- a/61_UI/include/common.hpp +++ b/61_UI/include/common.hpp @@ -10,8 +10,8 @@ #include "SimpleWindowedApplication.hpp" #include "InputSystem.hpp" -#include "camera/ILinearProjection.hpp" -#include "camera/IQuadProjection.hpp" +#include "camera/CCubeProjection.hpp" +#include "camera/CLinearProjection.hpp" // the example's headers #include "nbl/ui/ICursorControl.h" @@ -30,6 +30,4 @@ using namespace video; using namespace scene; using namespace geometrycreator; -using matrix_precision_t = float32_t; - #endif // __NBL_THIS_EXAMPLE_COMMON_H_INCLUDED__ \ No newline at end of file diff --git a/61_UI/include/keysmapping.hpp b/61_UI/include/keysmapping.hpp index 69872c4ca..eebfa263f 100644 --- a/61_UI/include/keysmapping.hpp +++ b/61_UI/include/keysmapping.hpp @@ -2,10 +2,9 @@ #define __NBL_KEYSMAPPING_H_INCLUDED__ #include "common.hpp" -#include "ICamera.hpp" +#include "camera/ICamera.hpp" -template -void handleAddMapping(const char* tableID, ICamera* camera, IGimbalManipulateEncoder::EncoderType activeController, CVirtualGimbalEvent::VirtualEventType& selectedEventType, ui::E_KEY_CODE& newKey, ui::E_MOUSE_CODE& newMouseCode, bool& addMode) +void handleAddMapping(const char* tableID, ICamera* camera, IGimbalManipulateEncoder::EncoderType activeController, CVirtualGimbalEvent::VirtualEventType& selectedEventType, ui::E_KEY_CODE& newKey, ui::E_MOUSE_CODE& newMouseCode, bool& addMode) { ImGui::BeginTable(tableID, 3, ImGuiTableFlags_Borders | ImGuiTableFlags_Resizable | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchSame); ImGui::TableSetupColumn("Virtual Event", ImGuiTableColumnFlags_WidthStretch, 0.33f); @@ -76,8 +75,7 @@ void handleAddMapping(const char* tableID, ICamera* camera, IGimbalManipulate ImGui::EndTable(); } -template -void displayKeyMappingsAndVirtualStates(ICamera* camera) +void displayKeyMappingsAndVirtualStates(ICamera* camera) { static bool addMode = false, pendingChanges = false; static CVirtualGimbalEvent::VirtualEventType selectedEventType = CVirtualGimbalEvent::VirtualEventType::MoveForward; diff --git a/61_UI/main.cpp b/61_UI/main.cpp index 9affd29f1..7a8e366a7 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -7,10 +7,6 @@ #include "camera/CCubeProjection.hpp" #include "glm/glm/ext/matrix_clip_space.hpp" // TODO: TESTING -// FPS Camera, TESTS -using camera_t = CFPSCamera; -using projection_t = IProjection>; // TODO: temporary -> projections will own/reference cameras - constexpr IGPUImage::SSubresourceRange TripleBufferUsedSubresourceRange = { .aspectMask = IGPUImage::EAF_COLOR_BIT, @@ -421,9 +417,6 @@ class UISampleApp final : public examples::SimpleWindowedApplication m_ui.manager->registerListener([this]() -> void { imguiListen(); }); } - for (auto& projection : projections) - projection = make_smart_refctd_ptr(); - // Geometry Creator Render Scenes { resources = ResourcesBundle::create(m_device.get(), m_logger.get(), getGraphicsQueue(), m_assetManager->getGeometryCreator()); @@ -445,7 +438,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication // lets use key map presets to update the controller - camera = make_smart_refctd_ptr(iPosition[i], iOrientation[i]); + camera = make_smart_refctd_ptr(iPosition[i], iOrientation[i]); // init keyboard map camera->updateKeyboardMapping([&](auto& keys) @@ -466,6 +459,10 @@ class UISampleApp final : public examples::SimpleWindowedApplication }); } + + // projections + projections = linear_projection_t::create(smart_refctd_ptr(cameraz.front())); + for (uint32_t i = 0u; i < scenez.size(); ++i) { auto& scene = scenez[i]; @@ -828,7 +825,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication camera->process(virtualEvents.data(), vCount, { params.keyboardEvents, params.mouseEvents }); } camera->endInputProcessing(); - camera->manipulate({ virtualEvents.data(), vCount }, ICamera::Local); + camera->manipulate({ virtualEvents.data(), vCount }, ICamera::Local); } m_ui.manager->update(params); @@ -855,26 +852,27 @@ class UISampleApp final : public examples::SimpleWindowedApplication // TODO: I need this logic per viewport we will render a scene from a point of view of its bound camera { + auto projectionMatrices = smart_refctd_ptr_static_cast(projections)->getLinearProjections(); - for (uint32_t i = 0u; i < projections.size(); ++i) + for (uint32_t i = 0u; i < projectionMatrices.size(); ++i) { - auto& projection = projections[i]; + auto& projection = projectionMatrices[i]; if (isPerspective) { if (isLH) - projection->setMatrix(buildProjectionMatrixPerspectiveFovLH(glm::radians(fov), aspectRatio[i], zNear, zFar)); + projection.setProjectionMatrix(buildProjectionMatrixPerspectiveFovLH(glm::radians(fov), aspectRatio[i], zNear, zFar)); else - projection->setMatrix(buildProjectionMatrixPerspectiveFovRH(glm::radians(fov), aspectRatio[i], zNear, zFar)); + projection.setProjectionMatrix(buildProjectionMatrixPerspectiveFovRH(glm::radians(fov), aspectRatio[i], zNear, zFar)); } else { float viewHeight = viewWidth * invAspectRatio[i]; if (isLH) - projection->setMatrix(buildProjectionMatrixOrthoLH(viewWidth, viewHeight, zNear, zFar)); + projection.setProjectionMatrix(buildProjectionMatrixOrthoLH(viewWidth, viewHeight, zNear, zFar)); else - projection->setMatrix(buildProjectionMatrixOrthoRH(viewWidth, viewHeight, zNear, zFar)); + projection.setProjectionMatrix(buildProjectionMatrixOrthoRH(viewWidth, viewHeight, zNear, zFar)); } } @@ -989,11 +987,12 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGuizmo::SetID(0u); - imguizmoM16InOut.view[0u] = transpose(getMatrix3x4As4x4(firstcamera->getGimbal().getView().matrix)); - imguizmoM16InOut.view[1u] = transpose(getMatrix3x4As4x4(secondcamera->getGimbal().getView().matrix)); + imguizmoM16InOut.view[0u] = getCastedMatrix(transpose(getMatrix3x4As4x4(firstcamera->getGimbal().getViewMatrix()))); + imguizmoM16InOut.view[1u] = getCastedMatrix(transpose(getMatrix3x4As4x4(secondcamera->getGimbal().getViewMatrix()))); + auto linearProjections = projections->getLinearProjections(); for(uint32_t i = 0u; i < ProjectionsCount; ++i) - imguizmoM16InOut.projection[i] = transpose(projections[i]->getMatrix()); + imguizmoM16InOut.projection[i] = getCastedMatrix(transpose(linearProjections[i].getProjectionMatrix())); // TODO: need to inspect where I'm wrong, workaround auto gimbalToImguizmoTRS = [&](const float32_t3x4& nblGimbalTrs) -> float32_t4x4 @@ -1013,7 +1012,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication // we will transform a scene object's model imguizmoM16InOut.inModel[0] = transpose(getMatrix3x4As4x4(m_model)); // and second camera's model too - imguizmoM16InOut.inModel[1] = gimbalToImguizmoTRS(secondCameraGimbalModel); + imguizmoM16InOut.inModel[1] = gimbalToImguizmoTRS(getCastedMatrix(secondCameraGimbalModel)); { float* views[] { @@ -1129,7 +1128,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication { static std::vector virtualEvents(0x45); uint32_t vCount; - auto nblManipulationMode = ICamera::Local; + auto nblManipulationMode = ICamera::Local; secondcamera->beginInputProcessing(m_nextPresentationTimestamp); { @@ -1146,8 +1145,8 @@ class UISampleApp final : public examples::SimpleWindowedApplication switch (mCurrentGizmoMode) { - case ImGuizmo::LOCAL: orientationBasis = (float32_t3x3(secondCameraGimbalModel)); break; - case ImGuizmo::WORLD: nblManipulationMode = ICamera::World; break; + case ImGuizmo::LOCAL: orientationBasis = (float32_t3x3(getCastedMatrix(secondCameraGimbalModel))); break; + case ImGuizmo::WORLD: nblManipulationMode = ICamera::World; break; default: assert(false); break; } @@ -1167,14 +1166,17 @@ class UISampleApp final : public examples::SimpleWindowedApplication // TODO: make it more nicely std::string_view mName; - const_cast(firstcamera->getGimbal().getView().matrix) = float32_t3x4(transpose(imguizmoM16InOut.view[0u])); // 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) + const_cast(firstcamera->getGimbal().getViewMatrix()) = float64_t3x4(getCastedMatrix(transpose(imguizmoM16InOut.view[0u]))); // 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) { m_model = float32_t3x4(transpose(imguizmoM16InOut.outModel[0])); - const float32_t3x4* views[] = + auto firstCameraView = getCastedMatrix(firstcamera->getGimbal().getViewMatrix()); + auto secondCameraView = getCastedMatrix(secondcamera->getGimbal().getViewMatrix()); + + const float32_t3x4* views[] = { - &firstcamera->getGimbal().getView().matrix, - &secondcamera->getGimbal().getView().matrix + &firstCameraView, + &secondCameraView }; const auto& references = resources->objects; @@ -1198,7 +1200,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication // TODO //modelView.getSub3x3InverseTranspose(normal); - auto concatMatrix = mul(projections[i]->getMatrix(), getMatrix3x4As4x4(viewMatrix)); + auto concatMatrix = mul(getCastedMatrix(linearProjections[i].getProjectionMatrix()), getMatrix3x4As4x4(viewMatrix)); modelViewProjection = mul(concatMatrix, getMatrix3x4As4x4(m_model)); memcpy(hook.viewParameters.MVP, &modelViewProjection[0][0], sizeof(hook.viewParameters.MVP)); @@ -1301,9 +1303,9 @@ class UISampleApp final : public examples::SimpleWindowedApplication continue; auto& gimbal = camera->getGimbal(); - const auto& position = gimbal.getPosition(); + const auto position = getCastedVector(gimbal.getPosition()); const auto& orientation = gimbal.getOrientation(); - const auto& viewMatrix = gimbal.getView().matrix; + const auto viewMatrix = getCastedMatrix(gimbal.getViewMatrix()); const auto trsMatrix = gimbal(); ImGui::Text("ID: %zu", cameraIndex); @@ -1598,7 +1600,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication nbl::hlsl::float32_t3x4 m_model = nbl::hlsl::float32_t3x4(1.f); static constexpr inline auto CamerazCount = 2u; std::array, CamerazCount> scenez; - std::array>, CamerazCount> cameraz; + std::array, CamerazCount> cameraz; nbl::core::smart_refctd_ptr resources; static constexpr inline auto OfflineSceneFirstCameraTextureIx = 1u; @@ -1606,11 +1608,13 @@ class UISampleApp final : public examples::SimpleWindowedApplication CRenderUI m_ui; video::CDumbPresentationOracle oracle; - 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 - static constexpr inline auto ProjectionsCount = 3u; - std::array, ProjectionsCount> projections; // TMP! + static constexpr inline auto ProjectionsCount = 3u; // full screen, first & second GUI windows + using linear_projections_range_t = std::array; + using linear_projection_t = CLinearProjection; + nbl::core::smart_refctd_ptr projections; + bool isPerspective[ProjectionsCount] = { true, true, true }, isLH = true, flipGizmoY = true, move = false; float fov = 60.f, zNear = 0.1f, zFar = 10000.f, moveSpeed = 1.f, rotateSpeed = 1.f; float viewWidth = 10.f; diff --git a/common/include/camera/CCubeProjection.hpp b/common/include/camera/CCubeProjection.hpp index 9c3fae30b..d47c5c6b8 100644 --- a/common/include/camera/CCubeProjection.hpp +++ b/common/include/camera/CCubeProjection.hpp @@ -2,20 +2,20 @@ #define _NBL_CCUBE_PROJECTION_HPP_ #include "IRange.hpp" -#include "IQuadProjection.hpp" +#include "IPerspectiveProjection.hpp" namespace nbl::hlsl { /** -* @brief A projection where each cube face is a quad we project onto. +* @brief A projection where each cube face is a perspective quad we project onto. * * Represents a cube projection given direction vector where each face of * the cube is treated as a quad. The projection onto the cube is done using * these quads and each face has its own unique pre-transform and * view-port linear matrix. */ -class CCubeProjection final : public IQuadProjection, public IProjection +class CCubeProjection final : public IPerspectiveProjection, public IProjection { public: //! Represents six face identifiers of a cube. @@ -76,14 +76,14 @@ class CCubeProjection final : public IQuadProjection, public IProjection template requires (FaceIx != CubeFacesCount) - inline const CProjection& getQuad() + inline const CProjection& getProjectionQuad() { return m_quads[FaceIx]; } private: CCubeProjection(core::smart_refctd_ptr&& camera) - : IQuadProjection(core::smart_refctd_ptr(camera)) {} + : IPerspectiveProjection(core::smart_refctd_ptr(camera)) {} virtual ~CCubeProjection() = default; std::array m_quads; diff --git a/common/include/camera/CFPSCamera.hpp b/common/include/camera/CFPSCamera.hpp index ae887aa0c..188a8a47f 100644 --- a/common/include/camera/CFPSCamera.hpp +++ b/common/include/camera/CFPSCamera.hpp @@ -2,8 +2,8 @@ // This file is part of the "Nabla Engine". // For conditions of distribution and use, see copyright notice in nabla.h -#ifndef _C_CAMERA_HPP_ -#define _C_CAMERA_HPP_ +#ifndef _C_FPS_CAMERA_HPP_ +#define _C_FPS_CAMERA_HPP_ #include "ICamera.hpp" @@ -11,13 +11,12 @@ namespace nbl::hlsl // TODO: DIFFERENT NAMESPACE { // FPS Camera -template -class CFPSCamera final : public ICamera +class CFPSCamera final : public ICamera { public: - using base_t = ICamera; + using base_t = ICamera; - CFPSCamera(const vector& position, const glm::quat& orientation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f)) + CFPSCamera(const float64_t3& position, const glm::quat& orientation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f)) : base_t(), m_gimbal({ .position = position, .orientation = orientation }) {} ~CFPSCamera() = default; @@ -32,7 +31,7 @@ class CFPSCamera final : public ICamera virtual bool manipulate(std::span virtualEvents, base_t::ManipulationMode mode) override { - constexpr float MoveSpeedScale = 0.01f, RotateSpeedScale = 0.003f, MaxVerticalAngle = glm::radians(88.0f), MinVerticalAngle = -MaxVerticalAngle; + constexpr double MoveSpeedScale = 0.01, RotateSpeedScale = 0.003, MaxVerticalAngle = glm::radians(88.0f), MinVerticalAngle = -MaxVerticalAngle; if (!virtualEvents.size()) return false; @@ -40,7 +39,7 @@ class CFPSCamera final : public ICamera const auto& gForward = m_gimbal.getZAxis(); const float gPitch = atan2(glm::length(glm::vec2(gForward.x, gForward.z)), gForward.y) - glm::half_pi(), gYaw = atan2(gForward.x, gForward.z); - glm::quat newOrientation; vector newPosition; + glm::quat newOrientation; float64_t3 newPosition; // TODO: I make assumption what world base is now (at least temporary), I need to think of this but maybe each ITransformObject should know what its world is // in ideal scenario we would define this crazy enum with all possible standard bases @@ -137,4 +136,4 @@ class CFPSCamera final : public ICamera } -#endif // _C_CAMERA_HPP_ \ No newline at end of file +#endif // _C_FPS_CAMERA_HPP_ \ No newline at end of file diff --git a/common/include/camera/CLinearProjection.hpp b/common/include/camera/CLinearProjection.hpp index 6dbe41b82..77725125b 100644 --- a/common/include/camera/CLinearProjection.hpp +++ b/common/include/camera/CLinearProjection.hpp @@ -2,15 +2,19 @@ #define _NBL_C_LINEAR_PROJECTION_HPP_ #include "ILinearProjection.hpp" +#include "IRange.hpp" namespace nbl::hlsl { +template ProjectionsRange> class CLinearProjection : public ILinearProjection { public: using ILinearProjection::ILinearProjection; + CLinearProjection() = default; + inline static core::smart_refctd_ptr create(core::smart_refctd_ptr&& camera) { if (!camera) @@ -19,10 +23,22 @@ class CLinearProjection : public ILinearProjection return core::smart_refctd_ptr(new CLinearProjection(core::smart_refctd_ptr(camera)), core::dont_grab); } + virtual std::span getLinearProjections() const override + { + return std::span(m_projections.data(), m_projections.size()); + } + + inline std::span getLinearProjections() + { + return std::span(m_projections.data(), m_projections.size()); + } + private: CLinearProjection(core::smart_refctd_ptr&& camera) : ILinearProjection(core::smart_refctd_ptr(camera)) {} virtual ~CLinearProjection() = default; + + ProjectionsRange m_projections; }; } // nbl::hlsl namespace diff --git a/common/include/camera/ICamera.hpp b/common/include/camera/ICamera.hpp index d4b3dbe8c..1f81903e5 100644 --- a/common/include/camera/ICamera.hpp +++ b/common/include/camera/ICamera.hpp @@ -13,7 +13,6 @@ namespace nbl::hlsl // TODO: DIFFERENT NAMESPACE class ICamera : public IGimbalController, virtual public core::IReferenceCounted { public: - using precision_t = float64_t; using IGimbalController::IGimbalController; //! Manipulation mode for virtual events @@ -28,10 +27,10 @@ class ICamera : public IGimbalController, virtual public core::IReferenceCounted }; // Gimbal with view parameters representing a camera in world space - class CGimbal : public IGimbal + class CGimbal : public IGimbal { public: - using base_t = IGimbal; + using base_t = IGimbal; CGimbal(typename base_t::SCreationParameters&& parameters) : base_t(std::move(parameters)) { updateView(); } ~CGimbal() = default; @@ -64,16 +63,16 @@ class ICamera : public IGimbalController, virtual public core::IReferenceCounted assert(isOrthoBase(gRight, gUp, gForward)); const auto& position = base_t::getPosition(); - m_view.matrix[0u] = vector(gRight, -glm::dot(gRight, position)); - m_view.matrix[1u] = vector(gUp, -glm::dot(gUp, position)); - m_view.matrix[2u] = vector(gForward, -glm::dot(gForward, position)); + + m_viewMatrix[0u] = float64_t4(gRight, -glm::dot(gRight, position)); + m_viewMatrix[1u] = float64_t4(gUp, -glm::dot(gUp, position)); + m_viewMatrix[2u] = float64_t4(gForward, -glm::dot(gForward, position)); } - // Getter for gimbal's view - inline const SView& getView() const { return m_view; } + inline const float64_t3x4& getViewMatrix() const { return m_viewMatrix; } private: - SView m_view; + float64_t3x4 m_viewMatrix; }; ICamera() {} diff --git a/common/include/camera/ILinearProjection.hpp b/common/include/camera/ILinearProjection.hpp index 069507f01..c51cb1780 100644 --- a/common/include/camera/ILinearProjection.hpp +++ b/common/include/camera/ILinearProjection.hpp @@ -11,7 +11,7 @@ namespace nbl::hlsl * @brief Interface class for any custom linear projection transformation (matrix elements are already evaluated scalars) * referencing a camera, great for Perspective, Orthographic, Oblique, Axonometric and Shear projections */ -class ILinearProjection +class ILinearProjection : virtual public core::IReferenceCounted { protected: ILinearProjection(core::smart_refctd_ptr&& camera) @@ -35,6 +35,7 @@ class ILinearProjection using projection_matrix_t = concatenated_matrix_t; using inv_projection_matrix_t = inv_concatenated_matrix_t; + CProjection() : CProjection(projection_matrix_t(1)) {} CProjection(const projection_matrix_t& matrix) { setProjectionMatrix(matrix); } inline void setProjectionMatrix(const projection_matrix_t& matrix) @@ -91,6 +92,17 @@ class ILinearProjection }; virtual std::span getLinearProjections() const = 0; + + bool setCamera(core::smart_refctd_ptr&& camera) + { + if (camera) + { + m_camera = camera; + return true; + } + + return false; + } /** * @brief Computes Model View (MV) matrix @@ -99,7 +111,7 @@ class ILinearProjection */ inline concatenated_matrix_t getMV(const model_matrix_t& model) const { - const auto& v = m_camera->getGimbal().getView().matrix; + const auto& v = m_camera->getGimbal().getViewMatrix(); return mul(getMatrix3x4As4x4(v), getMatrix3x4As4x4(model)); } @@ -111,7 +123,7 @@ class ILinearProjection */ inline concatenated_matrix_t getMVP(const CProjection& projection, const model_matrix_t& model) const { - const auto& v = m_camera->getGimbal().getView().matrix; + const auto& v = m_camera->getGimbal().getViewMatrix(); const auto& p = projection.getProjectionMatrix(); auto mv = mul(getMatrix3x4As4x4(v), getMatrix3x4As4x4(model)); return mul(p, mv); diff --git a/common/include/camera/IQuadProjection.hpp b/common/include/camera/IPerspectiveProjection.hpp similarity index 71% rename from common/include/camera/IQuadProjection.hpp rename to common/include/camera/IPerspectiveProjection.hpp index 43ba680ec..479db170e 100644 --- a/common/include/camera/IQuadProjection.hpp +++ b/common/include/camera/IPerspectiveProjection.hpp @@ -9,25 +9,26 @@ namespace nbl::hlsl /** * @brief Interface class for quad projections. * -* This projection transforms a vector into the **model space of a quad** -* (defined by the pre-transform matrix) and then projects it onto the quad using -* the linear view-port transform. +* This projection transforms a vector into the **model space of a perspective quad** +* (defined by the pre-transform matrix) and then projects it onto the perspective quad +* using the linear view-port transform. * -* A quad projection is represented by: +* A perspective quad projection is represented by: * - A **pre-transform matrix** (non-linear/skewed transformation). * - A **linear view-port transform matrix**. * * The final projection matrix is the concatenation of the pre-transform and the linear view-port transform. * -* @note Single quad projection can represent a face quad of a CAVE-like system. +* @note Single perspective quad projection can represent a face quad of a CAVE-like system. */ -class IQuadProjection : public ILinearProjection +class IPerspectiveProjection : public ILinearProjection { public: struct CProjection : ILinearProjection::CProjection { using base_t = ILinearProjection::CProjection; + CProjection() = default; CProjection(const ILinearProjection::model_matrix_t& pretransform, ILinearProjection::concatenated_matrix_t viewport) { setQuadTransform(pretransform, viewport); @@ -46,14 +47,14 @@ class IQuadProjection : public ILinearProjection inline const ILinearProjection::concatenated_matrix_t& getViewportProjection() const { return m_viewport; } private: - ILinearProjection::model_matrix_t m_pretransform; - ILinearProjection::concatenated_matrix_t m_viewport; + ILinearProjection::model_matrix_t m_pretransform = ILinearProjection::model_matrix_t(1); + ILinearProjection::concatenated_matrix_t m_viewport = ILinearProjection::concatenated_matrix_t(1); }; protected: - IQuadProjection(core::smart_refctd_ptr&& camera) + IPerspectiveProjection(core::smart_refctd_ptr&& camera) : ILinearProjection(core::smart_refctd_ptr(camera)) {} - virtual ~IQuadProjection() = default; + virtual ~IPerspectiveProjection() = default; }; } // nbl::hlsl namespace diff --git a/common/include/camera/IProjection.hpp b/common/include/camera/IProjection.hpp index 7e145ffee..7fa94e517 100644 --- a/common/include/camera/IProjection.hpp +++ b/common/include/camera/IProjection.hpp @@ -18,10 +18,10 @@ class IProjection //! Perspective, Orthographic, Oblique, Axonometric, Shear projections or any custom linear transformation Linear, - //! Represents a non-linear/skewed pre-transform *concatenated* with linear view-port transform - Quad, + //! Represents pre-transform *concatenated* with linear view-port transform, projects onto a quad + Perspective, - //! Represents a projection onto cube consisting of 6 quad cube faces + //! Represents a Perspective projection onto cube consisting of 6 quad cube faces Cube, Spherical, diff --git a/common/include/camera/IRange.hpp b/common/include/camera/IRange.hpp index 5c6fe751b..a6ed29270 100644 --- a/common/include/camera/IRange.hpp +++ b/common/include/camera/IRange.hpp @@ -10,19 +10,10 @@ concept GeneralPurposeRange = requires typename std::ranges::range_value_t; }; -//! Interface class for a general purpose range -template -class IRange -{ -public: - using range_t = Range; - using range_value_t = std::ranges::range_value_t; - - IRange(range_t&& range) : m_range(std::move(range)) {} - -protected: - range_t m_range; -}; +template +concept ContiguousGeneralPurposeRangeOf = GeneralPurposeRange && +std::ranges::contiguous_range && +std::same_as, T>; } // namespace nbl::hlsl From 09ac39ec5d46aeff00f9b0d1f989d344a5503d2a Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Thu, 12 Dec 2024 13:15:55 +0100 Subject: [PATCH 051/205] fix a typo which broke my projections, comment out a few debug windows --- 61_UI/main.cpp | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/61_UI/main.cpp b/61_UI/main.cpp index 7a8e366a7..0d582e8f2 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -850,29 +850,28 @@ class UISampleApp final : public examples::SimpleWindowedApplication //if (ImGui::RadioButton("Full view", !useWindow)) // useWindow = false; - // TODO: I need this logic per viewport we will render a scene from a point of view of its bound camera + auto projectionMatrices = projections->getLinearProjections(); { - auto projectionMatrices = smart_refctd_ptr_static_cast(projections)->getLinearProjections(); - - for (uint32_t i = 0u; i < projectionMatrices.size(); ++i) + auto mutableRange = smart_refctd_ptr_static_cast(projections)->getLinearProjections(); + for (uint32_t i = 0u; i < mutableRange.size(); ++i) { - auto& projection = projectionMatrices[i]; + auto projection = mutableRange.begin() + i; if (isPerspective) { if (isLH) - projection.setProjectionMatrix(buildProjectionMatrixPerspectiveFovLH(glm::radians(fov), aspectRatio[i], zNear, zFar)); + projection->setProjectionMatrix(buildProjectionMatrixPerspectiveFovLH(glm::radians(fov), aspectRatio[i], zNear, zFar)); else - projection.setProjectionMatrix(buildProjectionMatrixPerspectiveFovRH(glm::radians(fov), aspectRatio[i], zNear, zFar)); + projection->setProjectionMatrix(buildProjectionMatrixPerspectiveFovRH(glm::radians(fov), aspectRatio[i], zNear, zFar)); } else { float viewHeight = viewWidth * invAspectRatio[i]; if (isLH) - projection.setProjectionMatrix(buildProjectionMatrixOrthoLH(viewWidth, viewHeight, zNear, zFar)); + projection->setProjectionMatrix(buildProjectionMatrixOrthoLH(viewWidth, viewHeight, zNear, zFar)); else - projection.setProjectionMatrix(buildProjectionMatrixOrthoRH(viewWidth, viewHeight, zNear, zFar)); + projection->setProjectionMatrix(buildProjectionMatrixOrthoRH(viewWidth, viewHeight, zNear, zFar)); } } @@ -990,9 +989,8 @@ class UISampleApp final : public examples::SimpleWindowedApplication imguizmoM16InOut.view[0u] = getCastedMatrix(transpose(getMatrix3x4As4x4(firstcamera->getGimbal().getViewMatrix()))); imguizmoM16InOut.view[1u] = getCastedMatrix(transpose(getMatrix3x4As4x4(secondcamera->getGimbal().getViewMatrix()))); - auto linearProjections = projections->getLinearProjections(); for(uint32_t i = 0u; i < ProjectionsCount; ++i) - imguizmoM16InOut.projection[i] = getCastedMatrix(transpose(linearProjections[i].getProjectionMatrix())); + imguizmoM16InOut.projection[i] = getCastedMatrix(transpose(projectionMatrices[i].getProjectionMatrix())); // TODO: need to inspect where I'm wrong, workaround auto gimbalToImguizmoTRS = [&](const float32_t3x4& nblGimbalTrs) -> float32_t4x4 @@ -1166,7 +1164,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication // TODO: make it more nicely std::string_view mName; - const_cast(firstcamera->getGimbal().getViewMatrix()) = float64_t3x4(getCastedMatrix(transpose(imguizmoM16InOut.view[0u]))); // 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) + //const_cast(firstcamera->getGimbal().getViewMatrix()) = float64_t3x4(getCastedMatrix(transpose(imguizmoM16InOut.view[0u]))); // a hack for "view manipulate", 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) { m_model = float32_t3x4(transpose(imguizmoM16InOut.outModel[0])); @@ -1200,7 +1198,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication // TODO //modelView.getSub3x3InverseTranspose(normal); - auto concatMatrix = mul(getCastedMatrix(linearProjections[i].getProjectionMatrix()), getMatrix3x4As4x4(viewMatrix)); + auto concatMatrix = mul(getCastedMatrix(projectionMatrices[i + 1u].getProjectionMatrix()), getMatrix3x4As4x4(viewMatrix)); modelViewProjection = mul(concatMatrix, getMatrix3x4As4x4(m_model)); memcpy(hook.viewParameters.MVP, &modelViewProjection[0][0], sizeof(hook.viewParameters.MVP)); @@ -1254,6 +1252,8 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGui::End(); } + /* + // ImGuizmo inputs { ImGui::SetNextWindowSize(ImVec2(600, 400), ImGuiCond_FirstUseEver); @@ -1276,6 +1276,8 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGui::End(); } + */ + //imguizmoM16InOut.inModel // Cameraz @@ -1343,6 +1345,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication } } + /* // 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, @@ -1413,6 +1416,8 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGui::End(); } + */ + displayKeyMappingsAndVirtualStates(cameraz.front().get()); ImGui::End(); @@ -1532,6 +1537,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGuizmo::SetRect(cursorPos.x, cursorPos.y, contentRegionSize.x, contentRegionSize.y); } + ImGuiWindow* window = ImGui::GetCurrentWindow(); gizmoWindowFlags = ImGuiWindowFlags_NoMove;//(ImGuizmo::IsUsing() && ImGui::IsWindowHovered() && ImGui::IsMouseHoveringRect(window->InnerRect.Min, window->InnerRect.Max) ? ImGuiWindowFlags_NoMove : 0); } From 01a73231563208e32a2e61ec81a609b2894b22eb Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Thu, 12 Dec 2024 15:32:21 +0100 Subject: [PATCH 052/205] add camera selection with mouse disabled on move, display camera properties with node tree --- 61_UI/main.cpp | 137 ++++++++++++-------- common/include/camera/ILinearProjection.hpp | 7 +- 2 files changed, 88 insertions(+), 56 deletions(-) diff --git a/61_UI/main.cpp b/61_UI/main.cpp index 0d582e8f2..f14fbf51a 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -224,6 +224,9 @@ class UISampleApp final : public examples::SimpleWindowedApplication if (m_surface) { m_window->getManager()->maximize(m_window.get()); + auto* cc = m_window->getCursorControl(); + cc->setVisible(false); + return { {m_surface->getSurface()/*,EQF_NONE*/} }; } @@ -459,7 +462,6 @@ class UISampleApp final : public examples::SimpleWindowedApplication }); } - // projections projections = linear_projection_t::create(smart_refctd_ptr(cameraz.front())); @@ -807,10 +809,9 @@ class UISampleApp final : public examples::SimpleWindowedApplication .keyboardEvents = { capturedEvents.keyboard.data(), capturedEvents.keyboard.size() } }; - if (move) + if (enableActiveCameraMovement) { - // TODO: testing - auto& camera = cameraz.front().get(); + auto& camera = cameraz[activeCameraIndex]; static std::vector virtualEvents(0x45); uint32_t vCount; @@ -874,8 +875,6 @@ class UISampleApp final : public examples::SimpleWindowedApplication projection->setProjectionMatrix(buildProjectionMatrixOrthoRH(viewWidth, viewHeight, zNear, zFar)); } } - - } ImGui::SameLine(); @@ -1280,68 +1279,94 @@ class UISampleApp final : public examples::SimpleWindowedApplication //imguizmoM16InOut.inModel - // Cameraz { - size_t cameraCount = cameraz.size(); - if (cameraCount > 0) - { - size_t columns = std::max(1, static_cast(std::sqrt(static_cast(cameraCount)))); - size_t rows = (cameraCount + columns - 1) / columns; + ImGuiIO& io = ImGui::GetIO(); - ImGui::Begin("Camera Matrices", nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysVerticalScrollbar); + if (ImGui::IsKeyPressed(ImGuiKey_C)) + enableActiveCameraMovement = !enableActiveCameraMovement; - ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2(4.0f, 2.0f)); + if (enableActiveCameraMovement) + { + io.ConfigFlags |= ImGuiConfigFlags_NoMouse; + io.MouseDrawCursor = false; + io.WantCaptureMouse = false; + } + else + { + io.ConfigFlags &= ~ImGuiConfigFlags_NoMouse; + io.MouseDrawCursor = true; + io.WantCaptureMouse = true; + } - for (size_t row = 0; row < rows; ++row) - { - for (size_t col = 0; col < columns; ++col) - { - size_t cameraIndex = row * columns + col; - if (cameraIndex >= cameraCount) - break; + ImGui::Begin("Cameras", nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysVerticalScrollbar); + ImGui::Text("Select Active Camera:"); + ImGui::Separator(); - auto& camera = cameraz[cameraIndex]; - if (!camera) - continue; + if (ImGui::BeginCombo("Active Camera", ("Camera " + std::to_string(activeCameraIndex)).c_str())) + { + for (uint32_t cameraIndex = 0; cameraIndex < CamerazCount; ++cameraIndex) + { + bool isSelected = (cameraIndex == activeCameraIndex); + std::string comboLabel = "Camera " + std::to_string(cameraIndex); - auto& gimbal = camera->getGimbal(); - const auto position = getCastedVector(gimbal.getPosition()); - const auto& orientation = gimbal.getOrientation(); - const auto viewMatrix = getCastedMatrix(gimbal.getViewMatrix()); - const auto trsMatrix = gimbal(); + if (ImGui::Selectable(comboLabel.c_str(), isSelected)) + activeCameraIndex = cameraIndex; - ImGui::Text("ID: %zu", cameraIndex); - ImGui::Separator(); + if (isSelected) + ImGui::SetItemDefaultFocus(); + } + ImGui::EndCombo(); + } - ImGui::Text("Type: %s", camera->getIdentifier().data()); - ImGui::Separator(); + ImGui::Separator(); - ImGui::PushStyleColor(ImGuiCol_TableRowBg, ImGui::GetStyleColorVec4(ImGuiCol_ChildBg)); - ImGui::PushStyleColor(ImGuiCol_TableRowBgAlt, ImGui::GetStyleColorVec4(ImGuiCol_WindowBg)); + if (enableActiveCameraMovement) + ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), "Active Camera Movement: Enabled"); + else + ImGui::TextColored(ImVec4(1.0f, 0.0f, 0.0f, 1.0f), "Active Camera Movement: Disabled"); - if (ImGui::BeginTable(("CameraTable" + std::to_string(cameraIndex)).c_str(), 1, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchSame)) - { - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); + if (ImGui::IsItemHovered()) + { + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.2f, 0.2f, 0.2f, 0.8f)); + ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.5f); + ImVec2 mousePos = ImGui::GetMousePos(); + ImGui::SetNextWindowPos(ImVec2(mousePos.x + 10, mousePos.y + 10), ImGuiCond_Always); + ImGui::Begin("InfoOverlay", NULL, + ImGuiWindowFlags_NoDecoration | + ImGuiWindowFlags_AlwaysAutoResize | + ImGuiWindowFlags_NoSavedSettings); + ImGui::Text("Press 'C' to enable/disable selected camera movement."); + ImGui::End(); + ImGui::PopStyleVar(); + ImGui::PopStyleColor(2); + } - const auto idxstr = std::to_string(cameraIndex); - addMatrixTable("Position", ("PositionTable" + idxstr).c_str(), 1, 3, &position[0], false); - addMatrixTable("Orientation (Quaternion)", ("OrientationTable" + idxstr).c_str(), 1, 4, &orientation[0], false); - addMatrixTable("View Matrix", ("ViewMatrixTable" + idxstr).c_str(), 3, 4, &viewMatrix[0][0], false); - //addMatrixTable("TRS Matrix", ("TRSMatrixTable" + idxstr).c_str(), 3, 4, &trsMatrix[0][0], true); + ImGui::Separator(); - ImGui::EndTable(); - } + for (size_t cameraIndex = 0; cameraIndex < CamerazCount; ++cameraIndex) + { + auto& camera = cameraz[cameraIndex]; + if (!camera) + continue; - ImGui::PopStyleColor(2); - } + std::string treeNodeLabel = "Camera " + std::to_string(cameraIndex); + if (ImGui::TreeNode(treeNodeLabel.c_str())) + { + auto& gimbal = camera->getGimbal(); + const auto position = getCastedVector(gimbal.getPosition()); + const auto& orientation = gimbal.getOrientation(); + const auto viewMatrix = getCastedMatrix(gimbal.getViewMatrix()); + ImGui::Text("Type: %s", camera->getIdentifier().data()); + ImGui::Separator(); + addMatrixTable("Position", ("PositionTable_" + std::to_string(cameraIndex)).c_str(), 1, 3, &position[0], false); + addMatrixTable("Orientation (Quaternion)", ("OrientationTable_" + std::to_string(cameraIndex)).c_str(), 1, 4, &orientation[0], false); + addMatrixTable("View Matrix", ("ViewMatrixTable_" + std::to_string(cameraIndex)).c_str(), 3, 4, &viewMatrix[0][0], false); + ImGui::TreePop(); } - - ImGui::PopStyleVar(); - ImGui::End(); } - else - ImGui::Text("No camera properties to display."); + + ImGui::End(); } } @@ -1506,7 +1531,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGui::SetNextWindowSize(ImVec2(800, 400), ImGuiCond_Appearing); ImGui::SetNextWindowPos(ImVec2(400, 20 + wCameraIndex * 420), ImGuiCond_Appearing); ImGui::PushStyleColor(ImGuiCol_WindowBg, (ImVec4)ImColor(0.35f, 0.3f, 0.3f)); - std::string ident = "Rendered from Camera \"" + std::to_string(wCameraIndex) + "\" Ix perspective"; + std::string ident = "Camera \"" + std::to_string(wCameraIndex) + "\" View"; ImGui::Begin(ident.data(), 0, gizmoWindowFlags); ImGuizmo::SetDrawlist(); @@ -1607,6 +1632,8 @@ class UISampleApp final : public examples::SimpleWindowedApplication static constexpr inline auto CamerazCount = 2u; std::array, CamerazCount> scenez; std::array, CamerazCount> cameraz; + uint32_t activeCameraIndex = 0; + bool enableActiveCameraMovement = false; nbl::core::smart_refctd_ptr resources; static constexpr inline auto OfflineSceneFirstCameraTextureIx = 1u; @@ -1621,7 +1648,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication using linear_projection_t = CLinearProjection; nbl::core::smart_refctd_ptr projections; - bool isPerspective[ProjectionsCount] = { true, true, true }, isLH = true, flipGizmoY = true, move = false; + bool isPerspective[ProjectionsCount] = { true, true, true }, isLH = true, flipGizmoY = true; 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; diff --git a/common/include/camera/ILinearProjection.hpp b/common/include/camera/ILinearProjection.hpp index c51cb1780..6222a58b4 100644 --- a/common/include/camera/ILinearProjection.hpp +++ b/common/include/camera/ILinearProjection.hpp @@ -93,7 +93,7 @@ class ILinearProjection : virtual public core::IReferenceCounted virtual std::span getLinearProjections() const = 0; - bool setCamera(core::smart_refctd_ptr&& camera) + inline bool setCamera(core::smart_refctd_ptr&& camera) { if (camera) { @@ -104,6 +104,11 @@ class ILinearProjection : virtual public core::IReferenceCounted return false; } + inline ICamera* getCamera() + { + return m_camera.get(); + } + /** * @brief Computes Model View (MV) matrix * @param "model" is world TRS matrix From d5749a1bde6aab909e6142a04cad2f3c9af1e118 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Thu, 12 Dec 2024 17:53:24 +0100 Subject: [PATCH 053/205] have virtual events mapping per camera in node tree, disable all gizmos on active camera movement, a few updates & clean-ups --- 61_UI/include/keysmapping.hpp | 71 ++++++++++---------- 61_UI/main.cpp | 119 ++++++++++++++++++---------------- 2 files changed, 99 insertions(+), 91 deletions(-) diff --git a/61_UI/include/keysmapping.hpp b/61_UI/include/keysmapping.hpp index eebfa263f..053acff6e 100644 --- a/61_UI/include/keysmapping.hpp +++ b/61_UI/include/keysmapping.hpp @@ -75,31 +75,41 @@ void handleAddMapping(const char* tableID, ICamera* camera, IGimbalManipulateEnc ImGui::EndTable(); } -void displayKeyMappingsAndVirtualStates(ICamera* camera) +void displayKeyMappingsAndVirtualStatesInline(ICamera* camera, bool spawnWindow = false) { - static bool addMode = false, pendingChanges = false; - static CVirtualGimbalEvent::VirtualEventType selectedEventType = CVirtualGimbalEvent::VirtualEventType::MoveForward; - static ui::E_KEY_CODE newKey = ui::E_KEY_CODE::EKC_A; - static ui::E_MOUSE_CODE newMouseCode = ui::EMC_LEFT_BUTTON; - static IGimbalManipulateEncoder::EncoderType activeController = IGimbalManipulateEncoder::Keyboard; + if (!camera) return; + + // Per-camera state for UI rendering + 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; + IGimbalManipulateEncoder::EncoderType activeController = IGimbalManipulateEncoder::Keyboard; + }; + + static std::unordered_map cameraStates; + auto& state = cameraStates[camera]; const auto& keyboardMappings = camera->getKeyboardVirtualEventMap(); const auto& mouseMappings = camera->getMouseVirtualEventMap(); - ImGui::SetNextWindowSize(ImVec2(600, 400), ImGuiCond_FirstUseEver); - ImGui::SetNextWindowSizeConstraints(ImVec2(600, 400), ImVec2(600, 69000)); - - ImGui::Begin("Controller Mappings & Virtual States", nullptr, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_AlwaysVerticalScrollbar); + if (spawnWindow) + { + ImGui::SetNextWindowSize(ImVec2(600, 400), ImGuiCond_FirstUseEver); + ImGui::Begin("Controller Mappings & Virtual States", nullptr, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_AlwaysVerticalScrollbar); + } if (ImGui::BeginTabBar("ControllersTabBar")) { if (ImGui::BeginTabItem("Keyboard")) { - activeController = IGimbalManipulateEncoder::Keyboard; + state.activeController = IGimbalManipulateEncoder::Keyboard; ImGui::Separator(); - if (ImGui::Button("Add key", ImVec2(100, 30))) - addMode = !addMode; + if (ImGui::Button("Add Key", ImVec2(100, 30))) + state.addMode = !state.addMode; ImGui::Separator(); @@ -114,7 +124,6 @@ void displayKeyMappingsAndVirtualStates(ICamera* camera) for (const auto& [keyboardCode, hash] : keyboardMappings) { ImGui::TableNextRow(); - const char* eventName = CVirtualGimbalEvent::virtualEventToString(hash.event.type).data(); ImGui::TableSetColumnIndex(0); ImGui::AlignTextToFramePadding(); @@ -131,25 +140,21 @@ void displayKeyMappingsAndVirtualStates(ICamera* camera) ImGui::TextColored(statusColor, "%s", isActive ? "Active" : "Inactive"); ImGui::TableSetColumnIndex(3); - std::array deltaText; - snprintf(deltaText.data(), deltaText.size(), "%.2f", hash.event.magnitude); - ImGui::AlignTextToFramePadding(); - ImGui::TextWrapped("%s", deltaText.data()); + ImGui::Text("%.2f", hash.event.magnitude); ImGui::TableSetColumnIndex(4); if (ImGui::Button(("Delete##deleteKey" + std::to_string(static_cast(hash.event.type))).c_str())) { - camera->updateKeyboardMapping([&](auto& keys) { keys.erase(keyboardCode); }); - pendingChanges = true; + camera->updateKeyboardMapping([keyboardCode](auto& keys) { keys.erase(keyboardCode); }); break; } } ImGui::EndTable(); - if (addMode) + if (state.addMode) { ImGui::Separator(); - handleAddMapping("AddKeyboardMappingTable", camera, activeController, selectedEventType, newKey, newMouseCode, addMode); + handleAddMapping("AddKeyboardMappingTable", camera, state.activeController, state.selectedEventType, state.newKey, state.newMouseCode, state.addMode); } ImGui::EndTabItem(); @@ -157,11 +162,11 @@ void displayKeyMappingsAndVirtualStates(ICamera* camera) if (ImGui::BeginTabItem("Mouse")) { - activeController = IGimbalManipulateEncoder::Mouse; + state.activeController = IGimbalManipulateEncoder::Mouse; ImGui::Separator(); - if (ImGui::Button("Add key", ImVec2(100, 30))) - addMode = !addMode; + if (ImGui::Button("Add Key", ImVec2(100, 30))) + state.addMode = !state.addMode; ImGui::Separator(); @@ -176,7 +181,6 @@ void displayKeyMappingsAndVirtualStates(ICamera* camera) for (const auto& [mouseCode, hash] : mouseMappings) { ImGui::TableNextRow(); - const char* eventName = CVirtualGimbalEvent::virtualEventToString(hash.event.type).data(); ImGui::TableSetColumnIndex(0); ImGui::AlignTextToFramePadding(); @@ -193,25 +197,21 @@ void displayKeyMappingsAndVirtualStates(ICamera* camera) ImGui::TextColored(statusColor, "%s", isActive ? "Active" : "Inactive"); ImGui::TableSetColumnIndex(3); - std::array deltaText; - snprintf(deltaText.data(), deltaText.size(), "%.2f", hash.event.magnitude); - ImGui::AlignTextToFramePadding(); - ImGui::TextWrapped("%s", deltaText.data()); + ImGui::Text("%.2f", hash.event.magnitude); ImGui::TableSetColumnIndex(4); if (ImGui::Button(("Delete##deleteMouse" + std::to_string(static_cast(hash.event.type))).c_str())) { - camera->updateMouseMapping([&](auto& mouse) { mouse.erase(mouseCode); }); - pendingChanges = true; + camera->updateMouseMapping([mouseCode](auto& mouse) { mouse.erase(mouseCode); }); break; } } ImGui::EndTable(); - if (addMode) + if (state.addMode) { ImGui::Separator(); - handleAddMapping("AddMouseMappingTable", camera, activeController, selectedEventType, newKey, newMouseCode, addMode); + handleAddMapping("AddMouseMappingTable", camera, state.activeController, state.selectedEventType, state.newKey, state.newMouseCode, state.addMode); } ImGui::EndTabItem(); } @@ -219,7 +219,8 @@ void displayKeyMappingsAndVirtualStates(ICamera* camera) ImGui::EndTabBar(); } - ImGui::End(); + if (spawnWindow) + ImGui::End(); } #endif // __NBL_KEYSMAPPING_H_INCLUDED__ \ No newline at end of file diff --git a/61_UI/main.cpp b/61_UI/main.cpp index f14fbf51a..14cc93a80 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -1036,7 +1036,11 @@ class UISampleApp final : public examples::SimpleWindowedApplication bool manipulatedFromAnyWindow = false; ImGuizmo::AllowAxisFlip(false); - ImGuizmo::Enable(true); + + if(enableActiveCameraMovement) + ImGuizmo::Enable(false); + else + ImGuizmo::Enable(true); // we have 2 GUI windows we render into with FBOs for (uint32_t windowIndex = 0; windowIndex < 2u; ++windowIndex) @@ -1124,38 +1128,42 @@ class UISampleApp final : public examples::SimpleWindowedApplication { static std::vector virtualEvents(0x45); - uint32_t vCount; - auto nblManipulationMode = ICamera::Local; - - secondcamera->beginInputProcessing(m_nextPresentationTimestamp); + + if (ImGuizmo::IsUsingAny()) { - secondcamera->process(nullptr, vCount); + uint32_t vCount; + auto nblManipulationMode = ICamera::Local; - if (virtualEvents.size() < vCount) - virtualEvents.resize(vCount); + secondcamera->beginInputProcessing(m_nextPresentationTimestamp); + { + secondcamera->process(nullptr, vCount); - IGimbalController::SUpdateParameters params; + if (virtualEvents.size() < vCount) + virtualEvents.resize(vCount); - auto getOrientationBasis = [&]() - { - auto orientationBasis = (float32_t3x3(1.f)); + IGimbalController::SUpdateParameters params; - switch (mCurrentGizmoMode) - { - case ImGuizmo::LOCAL: orientationBasis = (float32_t3x3(getCastedMatrix(secondCameraGimbalModel))); break; - case ImGuizmo::WORLD: nblManipulationMode = ICamera::World; break; - default: assert(false); break; - } + auto getOrientationBasis = [&]() + { + auto orientationBasis = (float32_t3x3(1.f)); - return orientationBasis; - }; + switch (mCurrentGizmoMode) + { + case ImGuizmo::LOCAL: orientationBasis = (float32_t3x3(getCastedMatrix(secondCameraGimbalModel))); break; + case ImGuizmo::WORLD: nblManipulationMode = ICamera::World; break; + default: assert(false); break; + } - params.imguizmoEvents = { { std::make_pair(imguizmoM16InOut.outDeltaTRS[1u], getOrientationBasis()) } }; - secondcamera->process(virtualEvents.data(), vCount, params); - } - secondcamera->endInputProcessing(); + return orientationBasis; + }; + + params.imguizmoEvents = { { std::make_pair(imguizmoM16InOut.outDeltaTRS[1u], getOrientationBasis()) } }; + secondcamera->process(virtualEvents.data(), vCount, params); + } + secondcamera->endInputProcessing(); - secondcamera->manipulate({ virtualEvents.data(), vCount }, nblManipulationMode); + secondcamera->manipulate({ virtualEvents.data(), vCount }, nblManipulationMode); + } } // update scenes data @@ -1325,23 +1333,6 @@ class UISampleApp final : public examples::SimpleWindowedApplication else ImGui::TextColored(ImVec4(1.0f, 0.0f, 0.0f, 1.0f), "Active Camera Movement: Disabled"); - if (ImGui::IsItemHovered()) - { - ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.2f, 0.2f, 0.2f, 0.8f)); - ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); - ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.5f); - ImVec2 mousePos = ImGui::GetMousePos(); - ImGui::SetNextWindowPos(ImVec2(mousePos.x + 10, mousePos.y + 10), ImGuiCond_Always); - ImGui::Begin("InfoOverlay", NULL, - ImGuiWindowFlags_NoDecoration | - ImGuiWindowFlags_AlwaysAutoResize | - ImGuiWindowFlags_NoSavedSettings); - ImGui::Text("Press 'C' to enable/disable selected camera movement."); - ImGui::End(); - ImGui::PopStyleVar(); - ImGui::PopStyleColor(2); - } - ImGui::Separator(); for (size_t cameraIndex = 0; cameraIndex < CamerazCount; ++cameraIndex) @@ -1350,24 +1341,39 @@ class UISampleApp final : public examples::SimpleWindowedApplication if (!camera) continue; + const auto flags = (activeCameraIndex == cameraIndex) ? ImGuiTreeNodeFlags_DefaultOpen : ImGuiTreeNodeFlags_None; std::string treeNodeLabel = "Camera " + std::to_string(cameraIndex); - if (ImGui::TreeNode(treeNodeLabel.c_str())) + + if (ImGui::TreeNodeEx(treeNodeLabel.c_str(), flags)) { - auto& gimbal = camera->getGimbal(); - const auto position = getCastedVector(gimbal.getPosition()); - const auto& orientation = gimbal.getOrientation(); - const auto viewMatrix = getCastedMatrix(gimbal.getViewMatrix()); - ImGui::Text("Type: %s", camera->getIdentifier().data()); - ImGui::Separator(); - addMatrixTable("Position", ("PositionTable_" + std::to_string(cameraIndex)).c_str(), 1, 3, &position[0], false); - addMatrixTable("Orientation (Quaternion)", ("OrientationTable_" + std::to_string(cameraIndex)).c_str(), 1, 4, &orientation[0], false); - addMatrixTable("View Matrix", ("ViewMatrixTable_" + std::to_string(cameraIndex)).c_str(), 3, 4, &viewMatrix[0][0], false); + if (ImGui::TreeNodeEx("Data", ImGuiTreeNodeFlags_None)) + { + auto& gimbal = camera->getGimbal(); + const auto position = getCastedVector(gimbal.getPosition()); + const auto& orientation = gimbal.getOrientation(); + const auto viewMatrix = getCastedMatrix(gimbal.getViewMatrix()); + + ImGui::Text("Type: %s", camera->getIdentifier().data()); + ImGui::Separator(); + addMatrixTable("Position", ("PositionTable_" + std::to_string(cameraIndex)).c_str(), 1, 3, &position[0], false); + addMatrixTable("Orientation (Quaternion)", ("OrientationTable_" + std::to_string(cameraIndex)).c_str(), 1, 4, &orientation[0], false); + addMatrixTable("View Matrix", ("ViewMatrixTable_" + std::to_string(cameraIndex)).c_str(), 3, 4, &viewMatrix[0][0], false); + ImGui::TreePop(); + } + + if (ImGui::TreeNodeEx("Virtual Event Mappings", ImGuiTreeNodeFlags_DefaultOpen)) + { + displayKeyMappingsAndVirtualStatesInline(camera.get()); + ImGui::TreePop(); + } + ImGui::TreePop(); } } ImGui::End(); } + } /* @@ -1443,8 +1449,6 @@ class UISampleApp final : public examples::SimpleWindowedApplication */ - displayKeyMappingsAndVirtualStates(cameraz.front().get()); - ImGui::End(); } @@ -1455,12 +1459,15 @@ class UISampleApp final : public examples::SimpleWindowedApplication static bool boundSizing = false; static bool boundSizingSnap = false; + /* if (ImGui::IsKeyPressed(ImGuiKey_T)) mCurrentGizmoOperation = ImGuizmo::TRANSLATE; if (ImGui::IsKeyPressed(ImGuiKey_E)) mCurrentGizmoOperation = ImGuizmo::ROTATE; - if (ImGui::IsKeyPressed(ImGuiKey_R)) // r Key + if (ImGui::IsKeyPressed(ImGuiKey_R)) mCurrentGizmoOperation = ImGuizmo::SCALE; + */ + if (ImGui::RadioButton("Translate", mCurrentGizmoOperation == ImGuizmo::TRANSLATE)) mCurrentGizmoOperation = ImGuizmo::TRANSLATE; ImGui::SameLine(); @@ -1657,7 +1664,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication float camDistance = 8.f, aspectRatio[ProjectionsCount] = {}, invAspectRatio[ProjectionsCount] = {}; bool useWindow = true, useSnap = false; ImGuizmo::OPERATION mCurrentGizmoOperation = ImGuizmo::TRANSLATE; - ImGuizmo::MODE mCurrentGizmoMode = ImGuizmo::WORLD; + ImGuizmo::MODE mCurrentGizmoMode = ImGuizmo::LOCAL; float snap[3] = { 1.f, 1.f, 1.f }; int lastManipulatedModelIx = 0; int lastManipulatedGizmoIx = 0; From 551b34b93be4a5c700764817cb4165957074c279 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Fri, 13 Dec 2024 10:59:13 +0100 Subject: [PATCH 054/205] modify projection parameters independently of camera, improve TRS editor, fix a bug with removing virtual events from hash map --- 61_UI/include/keysmapping.hpp | 4 +- 61_UI/main.cpp | 235 +++++++++++++++------------------- 2 files changed, 107 insertions(+), 132 deletions(-) diff --git a/61_UI/include/keysmapping.hpp b/61_UI/include/keysmapping.hpp index 053acff6e..474827b77 100644 --- a/61_UI/include/keysmapping.hpp +++ b/61_UI/include/keysmapping.hpp @@ -143,7 +143,7 @@ void displayKeyMappingsAndVirtualStatesInline(ICamera* camera, bool spawnWindow ImGui::Text("%.2f", hash.event.magnitude); ImGui::TableSetColumnIndex(4); - if (ImGui::Button(("Delete##deleteKey" + std::to_string(static_cast(hash.event.type))).c_str())) + if (ImGui::Button(("Delete##deleteKey" + std::to_string(static_cast(keyboardCode))).c_str())) { camera->updateKeyboardMapping([keyboardCode](auto& keys) { keys.erase(keyboardCode); }); break; @@ -200,7 +200,7 @@ void displayKeyMappingsAndVirtualStatesInline(ICamera* camera, bool spawnWindow ImGui::Text("%.2f", hash.event.magnitude); ImGui::TableSetColumnIndex(4); - if (ImGui::Button(("Delete##deleteMouse" + std::to_string(static_cast(hash.event.type))).c_str())) + if (ImGui::Button(("Delete##deleteMouse" + std::to_string(static_cast(mouseCode))).c_str())) { camera->updateMouseMapping([mouseCode](auto& mouse) { mouse.erase(mouseCode); }); break; diff --git a/61_UI/main.cpp b/61_UI/main.cpp index 14cc93a80..58ea61747 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -846,7 +846,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication // create a window and insert the inspector ImGui::SetNextWindowPos(ImVec2(10, 10), ImGuiCond_Appearing); ImGui::SetNextWindowSize(ImVec2(320, 340), ImGuiCond_Appearing); - ImGui::Begin("Editor"); + ImGui::Begin("TRS Editor"); //if (ImGui::RadioButton("Full view", !useWindow)) // useWindow = false; @@ -858,21 +858,21 @@ class UISampleApp final : public examples::SimpleWindowedApplication { auto projection = mutableRange.begin() + i; - if (isPerspective) + if (isPerspective[i]) { - if (isLH) - projection->setProjectionMatrix(buildProjectionMatrixPerspectiveFovLH(glm::radians(fov), aspectRatio[i], zNear, zFar)); + if (isLH[i]) + projection->setProjectionMatrix(buildProjectionMatrixPerspectiveFovLH(glm::radians(fov[i]), aspectRatio[i], zNear[i], zFar[i])); else - projection->setProjectionMatrix(buildProjectionMatrixPerspectiveFovRH(glm::radians(fov), aspectRatio[i], zNear, zFar)); + projection->setProjectionMatrix(buildProjectionMatrixPerspectiveFovRH(glm::radians(fov[i]), aspectRatio[i], zNear[i], zFar[i])); } else { - float viewHeight = viewWidth * invAspectRatio[i]; + float viewHeight = viewWidth[i] * invAspectRatio[i]; - if (isLH) - projection->setProjectionMatrix(buildProjectionMatrixOrthoLH(viewWidth, viewHeight, zNear, zFar)); + if (isLH[i]) + projection->setProjectionMatrix(buildProjectionMatrixOrthoLH(viewWidth[i], viewHeight, zNear[i], zFar[i])); else - projection->setProjectionMatrix(buildProjectionMatrixOrthoRH(viewWidth, viewHeight, zNear, zFar)); + projection->setProjectionMatrix(buildProjectionMatrixOrthoRH(viewWidth[i], viewHeight, zNear[i], zFar[i])); } } } @@ -882,38 +882,8 @@ class UISampleApp final : public examples::SimpleWindowedApplication //if (ImGui::RadioButton("Window", useWindow)) // useWindow = true; - ImGui::Text("Camera"); - - if (ImGui::RadioButton("LH", isLH)) - isLH = true; - - ImGui::SameLine(); - - if (ImGui::RadioButton("RH", !isLH)) - isLH = true; // TMP WIP - //isLH = false; - - //if (ImGui::RadioButton("Perspective", isPerspective)) - // isPerspective = true; - - //ImGui::SameLine(); - - //if (ImGui::RadioButton("Orthographic", !isPerspective)) - // isPerspective = false; - - 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); - ImGui::Text("X: %f Y: %f", io.MousePos.x, io.MousePos.y); if (ImGuizmo::IsUsing()) { @@ -1088,6 +1058,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication { lastManipulatedModelIx = matId; lastManipulatedGizmoIx = gizmoID; + lastManipulatedModelIdentifier = lastManipulatedModelIx == 0 ? "Geometry Creator Object" : "Camera FPS"; } if (ImGuizmo::IsOver()) @@ -1104,9 +1075,9 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings); - ImGui::Text("Type: %s", matId == 0 ? "Geometry Creator Object" : "Camera FPS"); - ImGui::Text("Model ID: %u", matId); - ImGui::Text("Gizmo ID: %u", gizmoID); + ImGui::Text("Identifier: %s", lastManipulatedModelIdentifier.c_str()); + ImGui::Text("Object Ix: %u", matId); + //ImGui::Text("Gizmo Ix: %u", gizmoID); ImGui::End(); @@ -1169,8 +1140,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication // update scenes data // to Nabla + update camera & model matrices - // TODO: make it more nicely - std::string_view mName; + // TODO: make it more nicely once view manipulate supported //const_cast(firstcamera->getGimbal().getViewMatrix()) = float64_t3x4(getCastedMatrix(transpose(imguizmoM16InOut.view[0u]))); // a hack for "view manipulate", 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) { m_model = float32_t3x4(transpose(imguizmoM16InOut.outModel[0])); @@ -1194,7 +1164,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication auto& hook = scene->object; hook.meta.type = type; - mName = hook.meta.name = meta.name; + hook.meta.name = meta.name; { float32_t3x4 modelView, normal; float32_t4x4 modelViewProjection; @@ -1216,81 +1186,10 @@ class UISampleApp final : public examples::SimpleWindowedApplication } { - auto addMatrixTable = [&](const char* topText, const char* tableName, int rows, int columns, const float* pointer, bool withSeparator = true) - { - 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(); - }; - - // Scene Models - { - ImGui::SetNextWindowSize(ImVec2(600, 400), ImGuiCond_FirstUseEver); - ImGui::Begin("Object Info and Model Matrix", nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysVerticalScrollbar); - - ImGui::Text("Type: \"%s\"", mName.data()); - ImGui::Separator(); - - ImGui::PushStyleColor(ImGuiCol_TableRowBg, ImGui::GetStyleColorVec4(ImGuiCol_FrameBg)); - ImGui::PushStyleColor(ImGuiCol_TableRowBgAlt, ImGui::GetStyleColorVec4(ImGuiCol_FrameBgHovered)); - - addMatrixTable("Scene Object Model Matrix", "ModelMatrixTable", 3, 4, &m_model[0][0]); - - ImGui::PopStyleColor(2); - ImGui::End(); - } - - /* - - // ImGuizmo inputs - { - ImGui::SetNextWindowSize(ImVec2(600, 400), ImGuiCond_FirstUseEver); - ImGui::Begin("ImGuizmo model inputs", nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysVerticalScrollbar); - - ImGui::PushStyleColor(ImGuiCol_TableRowBg, ImGui::GetStyleColorVec4(ImGuiCol_FrameBg)); - ImGui::PushStyleColor(ImGuiCol_TableRowBgAlt, ImGui::GetStyleColorVec4(ImGuiCol_FrameBgHovered)); - - ImGui::Text("Type: GC Object"); - ImGui::Separator(); - - addMatrixTable("Model", "GCIMGUIZMOTRS", 4, 4, &imguizmoM16InOut.inModel[0][0][0]); - - ImGui::Text("Type: Camera ID \"1\" object"); - ImGui::Separator(); - - addMatrixTable("Model", "CAMERASECIMGUIZMOTRS", 4, 4, &imguizmoM16InOut.inModel[1][0][0]); - - ImGui::PopStyleColor(2); - ImGui::End(); - } - - */ - - //imguizmoM16InOut.inModel - { ImGuiIO& io = ImGui::GetIO(); - if (ImGui::IsKeyPressed(ImGuiKey_C)) + if (ImGui::IsKeyPressed(ImGuiKey_LeftShift)) enableActiveCameraMovement = !enableActiveCameraMovement; if (enableActiveCameraMovement) @@ -1346,6 +1245,9 @@ class UISampleApp final : public examples::SimpleWindowedApplication if (ImGui::TreeNodeEx(treeNodeLabel.c_str(), flags)) { + ImGui::SliderFloat("Move speed", &moveSpeed[cameraIndex], 0.1f, 10.f); + ImGui::SliderFloat("Rotate speed", &rotateSpeed[cameraIndex], 0.1f, 10.f); + if (ImGui::TreeNodeEx("Data", ImGuiTreeNodeFlags_None)) { auto& gimbal = camera->getGimbal(); @@ -1376,6 +1278,39 @@ class UISampleApp final : public examples::SimpleWindowedApplication } + // Projections + { + ImGui::Begin("Projection"); + + ImGui::Text("Ix: %s", std::to_string(lastProjectionIx).c_str()); + + if (ImGui::RadioButton("Perspective", isPerspective[lastProjectionIx])) + isPerspective[lastProjectionIx] = true; + + ImGui::SameLine(); + + if (ImGui::RadioButton("Orthographic", !isPerspective[lastProjectionIx])) + isPerspective[lastProjectionIx] = false; + + if (ImGui::RadioButton("LH", isLH[lastProjectionIx])) + isLH[lastProjectionIx] = true; + + ImGui::SameLine(); + + if (ImGui::RadioButton("RH", !isLH[lastProjectionIx])) + isLH[lastProjectionIx] = false; + + if (isPerspective[lastProjectionIx]) + ImGui::SliderFloat("Fov", &fov[lastProjectionIx], 20.f, 150.f, "%.1f"); + else + ImGui::SliderFloat("Ortho width", &viewWidth[lastProjectionIx], 1.f, 20.f, "%.1f"); + + ImGui::SliderFloat("zNear", &zNear[lastProjectionIx], 0.1f, 100.f, "%.2f"); + ImGui::SliderFloat("zFar", &zFar[lastProjectionIx], 110.f, 10000.f, "%.1f"); + + ImGui::End(); + } + /* // Nabla Imgui backend MDI buffer info @@ -1452,13 +1387,21 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGui::End(); } - void TransformEditor(float* matrix) + inline void TransformEditor(float* matrix) { 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; + ImGui::Text("Object Ix: \"%s\"", std::to_string(lastManipulatedModelIx).c_str()); + ImGui::Separator(); + + ImGui::Text("Identifier: \"%s\"", lastManipulatedModelIdentifier.c_str()); + ImGui::Separator(); + + addMatrixTable("Model (TRS) Matrix", "ModelMatrixTable", 4, 4, matrix); + /* if (ImGui::IsKeyPressed(ImGuiKey_T)) mCurrentGizmoOperation = ImGuizmo::TRANSLATE; @@ -1492,8 +1435,9 @@ class UISampleApp final : public examples::SimpleWindowedApplication mCurrentGizmoMode = ImGuizmo::WORLD; } - if (ImGui::IsKeyPressed(ImGuiKey_S)) - useSnap = !useSnap; + //if (ImGui::IsKeyPressed(ImGuiKey_S)) + // useSnap = !useSnap; + ImGui::Checkbox(" ", &useSnap); ImGui::SameLine(); switch (mCurrentGizmoOperation) @@ -1510,7 +1454,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication } } - void TransformStart(uint32_t wCameraIndex) + inline void TransformStart(uint32_t wCameraIndex) { ImGuiIO& io = ImGui::GetIO(); static ImGuiWindowFlags gizmoWindowFlags = ImGuiWindowFlags_NoMove; @@ -1552,6 +1496,9 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGui::Image(info, contentRegionSize); ImGuizmo::SetRect(cursorPos.x, cursorPos.y, contentRegionSize.x, contentRegionSize.y); + + if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) + lastProjectionIx = (int)(wCameraIndex + 1u); } else { @@ -1567,14 +1514,15 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGui::Image(info, contentRegionSize); ImGuizmo::SetRect(cursorPos.x, cursorPos.y, contentRegionSize.x, contentRegionSize.y); - } + lastProjectionIx = 0; + } ImGuiWindow* window = ImGui::GetCurrentWindow(); gizmoWindowFlags = ImGuiWindowFlags_NoMove;//(ImGuizmo::IsUsing() && ImGui::IsWindowHovered() && ImGui::IsMouseHoveringRect(window->InnerRect.Min, window->InnerRect.Max) ? ImGuiWindowFlags_NoMove : 0); } - void TransformEnd() + inline void TransformEnd() { if (useWindow) { @@ -1583,6 +1531,32 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGui::PopStyleColor(1); } + inline void addMatrixTable(const char* topText, const char* tableName, int rows, int columns, const float* pointer, bool withSeparator = true) + { + 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(); + } + std::chrono::seconds timeout = std::chrono::seconds(0x7fffFFFFu); clock_t::time_point start; @@ -1655,19 +1629,20 @@ class UISampleApp final : public examples::SimpleWindowedApplication using linear_projection_t = CLinearProjection; nbl::core::smart_refctd_ptr projections; - bool isPerspective[ProjectionsCount] = { true, true, true }, isLH = true, flipGizmoY = true; - float fov = 60.f, zNear = 0.1f, zFar = 10000.f, moveSpeed = 1.f, rotateSpeed = 1.f; - float viewWidth = 10.f; + bool flipGizmoY = true; + std::array isPerspective = { true, true, true }, isLH = { true, true, true }; + std::array fov = { 60.f, 60.f, 60.f }, zNear = { 0.1f, 0.1f, 0.1f }, zFar = { 10000.f, 10000.f, 10000.f }, viewWidth = { 10.f, 10.f, 10.f }, aspectRatio = {}, invAspectRatio = {}; + std::array moveSpeed = { 1.f, 1.f }, rotateSpeed = { 1.f, 1.f }; + float camYAngle = 165.f / 180.f * 3.14159f; float camXAngle = 32.f / 180.f * 3.14159f; - - float camDistance = 8.f, aspectRatio[ProjectionsCount] = {}, invAspectRatio[ProjectionsCount] = {}; + float camDistance = 8.f; bool useWindow = true, useSnap = false; ImGuizmo::OPERATION mCurrentGizmoOperation = ImGuizmo::TRANSLATE; ImGuizmo::MODE mCurrentGizmoMode = ImGuizmo::LOCAL; float snap[3] = { 1.f, 1.f, 1.f }; - int lastManipulatedModelIx = 0; - int lastManipulatedGizmoIx = 0; + int lastManipulatedModelIx = 0, lastManipulatedGizmoIx = 0, lastProjectionIx = 0; + std::string lastManipulatedModelIdentifier = "Geometry Creator Object"; bool firstFrame = true; }; From 2d0fbfcaca98ae39359e6dcc9d266e3f056d4bc9 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Fri, 13 Dec 2024 15:46:24 +0100 Subject: [PATCH 055/205] increase FBOs resolution, make full screen mode work again after updates --- 61_UI/main.cpp | 481 +++++++++--------------- common/include/CGeomtryCreatorScene.hpp | 58 +-- 2 files changed, 221 insertions(+), 318 deletions(-) diff --git a/61_UI/main.cpp b/61_UI/main.cpp index 58ea61747..af88e8a8a 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -185,10 +185,14 @@ class UISampleApp final : public examples::SimpleWindowedApplication using base_t = examples::SimpleWindowedApplication; using clock_t = std::chrono::steady_clock; - //_NBL_STATIC_INLINE_CONSTEXPR uint32_t WIN_W = 1280, WIN_H = 720; - constexpr static inline clock_t::duration DisplayImageDuration = std::chrono::milliseconds(900); + enum CameraRenderImguiTextureIx + { + OfflineSceneFirstCameraTextureIx = 1u, + OfflineSceneSecondCameraTextureIx = 2u + }; + public: using base_t::base_t; @@ -465,10 +469,11 @@ class UISampleApp final : public examples::SimpleWindowedApplication // projections projections = linear_projection_t::create(smart_refctd_ptr(cameraz.front())); + const auto dpyInfo = m_winMgr->getPrimaryDisplayInfo(); for (uint32_t i = 0u; i < scenez.size(); ++i) { auto& scene = scenez[i]; - scene = CScene::create(smart_refctd_ptr(m_device), smart_refctd_ptr(m_logger), getGraphicsQueue(), smart_refctd_ptr(resources)); + scene = CScene::create(smart_refctd_ptr(m_device), smart_refctd_ptr(m_logger), getGraphicsQueue(), smart_refctd_ptr(resources), dpyInfo.resX, dpyInfo.resY); if (!scene) { @@ -566,11 +571,15 @@ class UISampleApp final : public examples::SimpleWindowedApplication bool willSubmit = true; { + willSubmit &= cmdbuf->reset(IGPUCommandBuffer::RESET_FLAGS::RELEASE_RESOURCES_BIT); + willSubmit &= cmdbuf->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT); + willSubmit &= cmdbuf->beginDebugMarker("UIApp Frame"); + // render geometry creator scene to offline frame buffer & submit // TODO: OK with TRI buffer this thing is retarded now // (**) <- a note why bellow before submit - for (auto scene : scenez) + auto renderOfflineScene = [&](auto& scene) { scene->begin(); { @@ -579,11 +588,13 @@ class UISampleApp final : public examples::SimpleWindowedApplication scene->end(); } scene->submit(getGraphicsQueue()); - } + }; - willSubmit &= cmdbuf->reset(IGPUCommandBuffer::RESET_FLAGS::RELEASE_RESOURCES_BIT); - willSubmit &= cmdbuf->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT); - willSubmit &= cmdbuf->beginDebugMarker("UIApp Frame"); + if (useWindow) + for (auto scene : scenez) + renderOfflineScene(scene); + else + renderOfflineScene(scenez.front().get()); // just to not render to all at once const IGPUCommandBuffer::SClearColorValue clearValue = { .float32 = {0.f,0.f,0.f,1.f} }; const IGPUCommandBuffer::SRenderpassBeginInfo info = { @@ -705,10 +716,10 @@ class UISampleApp final : public examples::SimpleWindowedApplication } }; - // (**) -> wait on offline framebuffers + // (**) -> wait on offline framebuffers in window mode { const nbl::video::ISemaphore::SWaitInfo waitInfos[] = - { + { // wait for first camera scene view fb { .semaphore = scenez[0]->semaphore.progress.get(), @@ -721,7 +732,38 @@ class UISampleApp final : public examples::SimpleWindowedApplication }, }; - m_device->blockForSemaphores(waitInfos); + if (useWindow) + { + m_device->blockForSemaphores(std::to_array + ( + { + // wait for first camera scene view fb + { + .semaphore = scenez[0]->semaphore.progress.get(), + .value = scenez[0]->semaphore.finishedValue + }, + // and second one too + { + .semaphore = scenez[1]->semaphore.progress.get(), + .value = scenez[1]->semaphore.finishedValue + }, + } + )); + } + else + { + m_device->blockForSemaphores(std::to_array + ( + { + // wait for first only, we use it temporary for FS render mode + { + .semaphore = scenez.front()->semaphore.progress.get(), + .value = scenez.front()->semaphore.finishedValue + } + } + )); + } + updateGUIDescriptorSet(); } @@ -839,17 +881,6 @@ class UISampleApp final : public examples::SimpleWindowedApplication 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("TRS Editor"); - - //if (ImGui::RadioButton("Full view", !useWindow)) - // useWindow = false; auto projectionMatrices = projections->getLinearProjections(); { @@ -876,33 +907,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication } } } - - ImGui::SameLine(); - - //if (ImGui::RadioButton("Window", useWindow)) - // useWindow = true; - - // ImGui::Checkbox("Flip Gizmo's Y axis", &flipGizmoY); // let's not expose it to be changed in UI but keep the logic in case - - 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 @@ -981,30 +986,10 @@ class UISampleApp final : public examples::SimpleWindowedApplication // and second camera's model too imguizmoM16InOut.inModel[1] = gimbalToImguizmoTRS(getCastedMatrix(secondCameraGimbalModel)); { - float* views[] - { - &imguizmoM16InOut.view[0u][0][0], // first camera - &imguizmoM16InOut.view[1u][0][0] // second camera - }; - - float* projections[] - { - &imguizmoM16InOut.projection[0u][0][0], // full screen, tmp off - &imguizmoM16InOut.projection[1u][0][0], // first camera - &imguizmoM16InOut.projection[2u][0][0] // second camera - }; - if (flipGizmoY) // note we allow to flip gizmo just to match our coordinates for(uint32_t i = 0u; i < ProjectionsCount; ++i) imguizmoM16InOut.projection[i][1][1] *= -1.f; // https://johannesugb.github.io/gpu-programming/why-do-opengl-proj-matrices-fail-in-vulkan/ - float* lastUsedModel = &imguizmoM16InOut.inModel[lastManipulatedModelIx][0][0]; - - // ImGuizmo manipulations on the last used model matrix - TransformEditor(lastUsedModel); - uint32_t gizmoID = {}; - bool manipulatedFromAnyWindow = false; - ImGuizmo::AllowAxisFlip(false); if(enableActiveCameraMovement) @@ -1012,84 +997,145 @@ class UISampleApp final : public examples::SimpleWindowedApplication else ImGuizmo::Enable(true); - // we have 2 GUI windows we render into with FBOs - for (uint32_t windowIndex = 0; windowIndex < 2u; ++windowIndex) + aspectRatio[0] = io.DisplaySize.x / io.DisplaySize.y; + invAspectRatio[0] = io.DisplaySize.y / io.DisplaySize.x; + + SImResourceInfo info; + info.samplerIx = (uint16_t)nbl::ext::imgui::UI::DefaultSamplerIx::USER; + + // render camera views onto GUIs + if (useWindow) { - auto* cameraView = views[windowIndex]; - auto* cameraProjection = projections[1u + windowIndex]; + // ImGuizmo manipulations on the last used model matrix in window mode + TransformEditor(&imguizmoM16InOut.inModel[lastManipulatedModelIx][0][0]); - TransformStart(windowIndex); + uint32_t gizmoIx = {}; + bool manipulatedFromAnyWindow = false; - // but only 2 objects with matrices we will try to manipulate & we can do this from both windows! - // however note it only makes sense to obsly assume we cannot manipulate 2 objects at the same time - for (uint32_t matId = 0; matId < 2; matId++) + // we have 2 GUI windows we render into with FBOs + for (uint32_t windowIndex = 0; windowIndex < 2u; ++windowIndex) { - // future logic will need to filter gizmos which represents gimbal of camera which view matrix we use to render scene with - if (gizmoID == 3) - continue; - - // and because of imguizmo API usage to achieve it we must work on copies & filter the output (unless we try gizmo enable/disable) - // -> in general we need to be careful to not edit the same model twice - auto model = imguizmoM16InOut.inModel[matId]; - float32_t4x4 deltaOutputTRS; + const auto& cameraIx = windowIndex; // tmp bound & hardcoded, we will extend it later + const auto projectionIx = cameraIx + 1u; // offset because first projection belongs to full screen (**) + info.textureID = projectionIx; + + ImGui::SetNextWindowSize(ImVec2(800, 400), ImGuiCond_Appearing); + ImGui::SetNextWindowPos(ImVec2(400, 20 + cameraIx * 420), ImGuiCond_Appearing); + ImGui::PushStyleColor(ImGuiCol_WindowBg, (ImVec4)ImColor(0.35f, 0.3f, 0.3f)); + std::string ident = "Camera \"" + std::to_string(cameraIx) + "\" View"; + ImGui::Begin(ident.data(), 0, ImGuiWindowFlags_NoMove); + ImGuizmo::SetDrawlist(); + + ImVec2 contentRegionSize, windowPos, cursorPos; + contentRegionSize = ImGui::GetContentRegionAvail(); + cursorPos = ImGui::GetCursorScreenPos(); + windowPos = ImGui::GetWindowPos(); + + // (**) + aspectRatio[projectionIx] = contentRegionSize.x / contentRegionSize.y; + invAspectRatio[projectionIx] = contentRegionSize.y / contentRegionSize.x; + + ImGui::Image(info, contentRegionSize); + ImGuizmo::SetRect(cursorPos.x, cursorPos.y, contentRegionSize.x, contentRegionSize.y); + + if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) + lastProjectionIx = projectionIx; + + // but only 2 objects with matrices we will try to manipulate & we can do this from both windows! + // however note it only makes sense to obsly assume we cannot manipulate 2 objects at the same time + for (uint32_t modelIx = 0; modelIx < 2; modelIx++) + { + // future logic will need to filter gizmos which represents gimbal of camera which view matrix we use to render scene with + if (gizmoIx == 3) + continue; - // note we also need to take care of unique gizmo IDs, we have in total 4 gizmos even though we only want to manipulate 2 objects in total - ImGuizmo::PushID(gizmoID); + // and because of imguizmo API usage to achieve it we must work on copies & filter the output (unless we try gizmo enable/disable) + // -> in general we need to be careful to not edit the same model twice + + auto model = imguizmoM16InOut.inModel[modelIx]; + float32_t4x4 deltaOutputTRS; - ImGuiIO& io = ImGui::GetIO(); - const bool success = ImGuizmo::Manipulate(cameraView, cameraProjection, mCurrentGizmoOperation, mCurrentGizmoMode, &model[0][0], &deltaOutputTRS[0][0], useSnap ? &snap[0] : NULL); + // note we also need to take care of unique gizmo IDs, we have in total 4 gizmos even though we only want to manipulate 2 objects in total + ImGuizmo::PushID(gizmoIx); - // we manipulated a gizmo from a X-th window, now we update output matrices and assume no more gizmos can be manipulated at the same frame - if (!manipulatedFromAnyWindow) - { - // TMP WIP, our imguizmo controller doesnt support rotation & scale yet - auto discard = gizmoID == 1 && mCurrentGizmoOperation != ImGuizmo::TRANSLATE; - if (!discard) + ImGuiIO& io = ImGui::GetIO(); + + const bool success = ImGuizmo::Manipulate(&imguizmoM16InOut.view[cameraIx][0][0], &imguizmoM16InOut.projection[projectionIx][0][0], mCurrentGizmoOperation, mCurrentGizmoMode, &model[0][0], &deltaOutputTRS[0][0], useSnap ? &snap[0] : nullptr); + + // we manipulated a gizmo from a X-th window, now we update output matrices and assume no more gizmos can be manipulated at the same frame + if (!manipulatedFromAnyWindow) { - imguizmoM16InOut.outModel[matId] = model; - imguizmoM16InOut.outDeltaTRS[matId] = deltaOutputTRS; + // TMP WIP, our imguizmo controller doesnt support rotation & scale yet + auto discard = gizmoIx == 1 && mCurrentGizmoOperation != ImGuizmo::TRANSLATE; + if (!discard) + { + imguizmoM16InOut.outModel[modelIx] = model; + imguizmoM16InOut.outDeltaTRS[modelIx] = deltaOutputTRS; + } } - } - if (success) - manipulatedFromAnyWindow = true; + if (success) + manipulatedFromAnyWindow = true; - if (ImGuizmo::IsUsing()) - { - lastManipulatedModelIx = matId; - lastManipulatedGizmoIx = gizmoID; - lastManipulatedModelIdentifier = lastManipulatedModelIx == 0 ? "Geometry Creator Object" : "Camera FPS"; - } + if (ImGuizmo::IsUsing()) + { + lastManipulatedModelIx = modelIx; + lastManipulatedGizmoIx = gizmoIx; + lastManipulatedModelIdentifier = lastManipulatedModelIx == 0 ? "Geometry Creator Object" : "Camera FPS"; + } - if (ImGuizmo::IsOver()) - { - ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.2f, 0.2f, 0.2f, 0.8f)); - ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); - ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.5f); + if (ImGuizmo::IsOver()) + { + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.2f, 0.2f, 0.2f, 0.8f)); + ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.5f); - ImVec2 mousePos = io.MousePos; - ImGui::SetNextWindowPos(ImVec2(mousePos.x + 10, mousePos.y + 10), ImGuiCond_Always); + ImVec2 mousePos = io.MousePos; + ImGui::SetNextWindowPos(ImVec2(mousePos.x + 10, mousePos.y + 10), ImGuiCond_Always); - ImGui::Begin("InfoOverlay", NULL, - ImGuiWindowFlags_NoDecoration | - ImGuiWindowFlags_AlwaysAutoResize | - ImGuiWindowFlags_NoSavedSettings); + ImGui::Begin("InfoOverlay", nullptr, + ImGuiWindowFlags_NoDecoration | + ImGuiWindowFlags_AlwaysAutoResize | + ImGuiWindowFlags_NoSavedSettings); - ImGui::Text("Identifier: %s", lastManipulatedModelIdentifier.c_str()); - ImGui::Text("Object Ix: %u", matId); - //ImGui::Text("Gizmo Ix: %u", gizmoID); + ImGui::Text("Identifier: %s", lastManipulatedModelIdentifier.c_str()); + ImGui::Text("Object Ix: %u", modelIx); - ImGui::End(); + ImGui::End(); + + ImGui::PopStyleVar(); + ImGui::PopStyleColor(2); + } - ImGui::PopStyleVar(); - ImGui::PopStyleColor(2); + ImGuizmo::PopID(); + ++gizmoIx; } - ImGuizmo::PopID(); - ++gizmoID; + ImGui::End(); + ImGui::PopStyleColor(1); } + } + // render selected camera view onto full screen + else + { + info.textureID = OfflineSceneFirstCameraTextureIx;; + lastProjectionIx = 0; + + 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, windowPos, cursorPos; + contentRegionSize = ImGui::GetContentRegionAvail(); + cursorPos = ImGui::GetCursorScreenPos(); + windowPos = ImGui::GetWindowPos(); - TransformEnd(); + ImGui::Image(info, contentRegionSize); + ImGuizmo::SetRect(cursorPos.x, cursorPos.y, contentRegionSize.x, contentRegionSize.y); + + ImGui::End(); + ImGui::PopStyleColor(1); } } @@ -1169,13 +1215,13 @@ class UISampleApp final : public examples::SimpleWindowedApplication float32_t3x4 modelView, normal; float32_t4x4 modelViewProjection; - const auto& viewMatrix = *views[i]; + const auto& viewMatrix = *views[useWindow ? i : activeCameraIndex]; modelView = concatenateBFollowedByA(viewMatrix, m_model); // TODO //modelView.getSub3x3InverseTranspose(normal); - auto concatMatrix = mul(getCastedMatrix(projectionMatrices[i + 1u].getProjectionMatrix()), getMatrix3x4As4x4(viewMatrix)); + auto concatMatrix = mul(getCastedMatrix(projectionMatrices[useWindow ? i + 1u : 0u].getProjectionMatrix()), getMatrix3x4As4x4(viewMatrix)); modelViewProjection = mul(concatMatrix, getMatrix3x4As4x4(m_model)); memcpy(hook.viewParameters.MVP, &modelViewProjection[0][0], sizeof(hook.viewParameters.MVP)); @@ -1206,6 +1252,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication } ImGui::Begin("Cameras", nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysVerticalScrollbar); + ImGui::Checkbox("Window mode##useWindow", &useWindow); ImGui::Text("Select Active Camera:"); ImGui::Separator(); @@ -1310,81 +1357,6 @@ class UISampleApp final : public examples::SimpleWindowedApplication 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 = m_ui.manager->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(); } inline void TransformEditor(float* matrix) @@ -1394,6 +1366,17 @@ class UISampleApp final : public examples::SimpleWindowedApplication static bool boundSizing = false; static bool boundSizingSnap = false; + ImGui::SetNextWindowPos(ImVec2(10, 10), ImGuiCond_Appearing); + ImGui::SetNextWindowSize(ImVec2(320, 340), ImGuiCond_Appearing); + ImGui::Begin("TRS Editor"); + ImGui::SameLine(); + + ImGuiIO& io = ImGui::GetIO(); + ImGui::Text("X: %f Y: %f", io.MousePos.x, io.MousePos.y); + if (ImGuizmo::IsUsing()) + ImGui::Text("Using gizmo"); + ImGui::Separator(); + ImGui::Text("Object Ix: \"%s\"", std::to_string(lastManipulatedModelIx).c_str()); ImGui::Separator(); @@ -1402,15 +1385,6 @@ class UISampleApp final : public examples::SimpleWindowedApplication addMatrixTable("Model (TRS) Matrix", "ModelMatrixTable", 4, 4, matrix); - /* - if (ImGui::IsKeyPressed(ImGuiKey_T)) - mCurrentGizmoOperation = ImGuizmo::TRANSLATE; - if (ImGui::IsKeyPressed(ImGuiKey_E)) - mCurrentGizmoOperation = ImGuizmo::ROTATE; - if (ImGui::IsKeyPressed(ImGuiKey_R)) - mCurrentGizmoOperation = ImGuizmo::SCALE; - */ - if (ImGui::RadioButton("Translate", mCurrentGizmoOperation == ImGuizmo::TRANSLATE)) mCurrentGizmoOperation = ImGuizmo::TRANSLATE; ImGui::SameLine(); @@ -1435,9 +1409,6 @@ class UISampleApp final : public examples::SimpleWindowedApplication mCurrentGizmoMode = ImGuizmo::WORLD; } - //if (ImGui::IsKeyPressed(ImGuiKey_S)) - // useSnap = !useSnap; - ImGui::Checkbox(" ", &useSnap); ImGui::SameLine(); switch (mCurrentGizmoOperation) @@ -1452,83 +1423,8 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGui::InputFloat("Scale Snap", &snap[0]); break; } - } - - inline void TransformStart(uint32_t wCameraIndex) - { - ImGuiIO& io = ImGui::GetIO(); - static ImGuiWindowFlags gizmoWindowFlags = ImGuiWindowFlags_NoMove; - - SImResourceInfo info; - switch (wCameraIndex) - { - case 0: - info.textureID = OfflineSceneFirstCameraTextureIx; - break; - case 1: - info.textureID = OfflineSceneSecondCameraTextureIx; - break; - default: - assert(false); // "wCameraIndex OOB!" - break; - } - info.samplerIx = (uint16_t)nbl::ext::imgui::UI::DefaultSamplerIx::USER; - - aspectRatio[0] = io.DisplaySize.x / io.DisplaySize.y; - invAspectRatio[0] = io.DisplaySize.y / io.DisplaySize.x; - - if (useWindow) - { - ImGui::SetNextWindowSize(ImVec2(800, 400), ImGuiCond_Appearing); - ImGui::SetNextWindowPos(ImVec2(400, 20 + wCameraIndex * 420), ImGuiCond_Appearing); - ImGui::PushStyleColor(ImGuiCol_WindowBg, (ImVec4)ImColor(0.35f, 0.3f, 0.3f)); - std::string ident = "Camera \"" + std::to_string(wCameraIndex) + "\" View"; - ImGui::Begin(ident.data(), 0, gizmoWindowFlags); - ImGuizmo::SetDrawlist(); - - ImVec2 contentRegionSize, windowPos, cursorPos; - contentRegionSize = ImGui::GetContentRegionAvail(); - cursorPos = ImGui::GetCursorScreenPos(); - windowPos = ImGui::GetWindowPos(); - - aspectRatio[wCameraIndex + 1] = contentRegionSize.x / contentRegionSize.y; - invAspectRatio[wCameraIndex + 1] = contentRegionSize.y / contentRegionSize.x; - - ImGui::Image(info, contentRegionSize); - ImGuizmo::SetRect(cursorPos.x, cursorPos.y, contentRegionSize.x, contentRegionSize.y); - - if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) - lastProjectionIx = (int)(wCameraIndex + 1u); - } - 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, windowPos, cursorPos; - contentRegionSize = ImGui::GetContentRegionAvail(); - cursorPos = ImGui::GetCursorScreenPos(); - windowPos = ImGui::GetWindowPos(); - ImGui::Image(info, contentRegionSize); - ImGuizmo::SetRect(cursorPos.x, cursorPos.y, contentRegionSize.x, contentRegionSize.y); - - lastProjectionIx = 0; - } - - ImGuiWindow* window = ImGui::GetCurrentWindow(); - gizmoWindowFlags = ImGuiWindowFlags_NoMove;//(ImGuizmo::IsUsing() && ImGui::IsWindowHovered() && ImGui::IsMouseHoveringRect(window->InnerRect.Min, window->InnerRect.Max) ? ImGuiWindowFlags_NoMove : 0); - } - - inline void TransformEnd() - { - if (useWindow) - { - ImGui::End(); - } - ImGui::PopStyleColor(1); + ImGui::End(); } inline void addMatrixTable(const char* topText, const char* tableName, int rows, int columns, const float* pointer, bool withSeparator = true) @@ -1617,9 +1513,6 @@ class UISampleApp final : public examples::SimpleWindowedApplication bool enableActiveCameraMovement = false; nbl::core::smart_refctd_ptr resources; - static constexpr inline auto OfflineSceneFirstCameraTextureIx = 1u; - static constexpr inline auto OfflineSceneSecondCameraTextureIx = 2u; - CRenderUI m_ui; video::CDumbPresentationOracle oracle; 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 diff --git a/common/include/CGeomtryCreatorScene.hpp b/common/include/CGeomtryCreatorScene.hpp index e744b795e..444a58b4d 100644 --- a/common/include/CGeomtryCreatorScene.hpp +++ b/common/include/CGeomtryCreatorScene.hpp @@ -19,7 +19,7 @@ using namespace system struct Traits { - static constexpr auto FramebufferW = 1280u, FramebufferH = 720u; + static constexpr auto DefaultFramebufferW = 1280u, DefaultFramebufferH = 720u; static constexpr auto ColorFboAttachmentFormat = nbl::asset::EF_R8G8B8A8_SRGB, DepthFboAttachmentFormat = nbl::asset::EF_D16_UNORM; static constexpr auto Samples = nbl::video::IGPUImage::ESCF_1_BIT; static constexpr nbl::video::IGPUCommandBuffer::SClearColorValue clearColor = { .float32 = {0.f,0.f,0.f,1.f} }; @@ -515,7 +515,7 @@ class CScene final : public nbl::core::IReferenceCounted nbl::core::smart_refctd_ptr progress; } semaphore; - static inline nbl::core::smart_refctd_ptr create(nbl::core::smart_refctd_ptr device, nbl::core::smart_refctd_ptr logger, nbl::video::CThreadSafeQueueAdapter* const transferCapableQueue, const nbl::core::smart_refctd_ptr resources) + static inline nbl::core::smart_refctd_ptr create(nbl::core::smart_refctd_ptr device, nbl::core::smart_refctd_ptr logger, nbl::video::CThreadSafeQueueAdapter* const transferCapableQueue, const nbl::core::smart_refctd_ptr resources, const uint32_t framebufferW = Traits::DefaultFramebufferW, const uint32_t framebufferH = Traits::DefaultFramebufferH) { EXPOSE_NABLA_NAMESPACES(); @@ -645,7 +645,7 @@ class CScene final : public nbl::core::IReferenceCounted .type = IGPUImage::ET_2D, .samples = Traits::Samples, .format = format, - .extent = { Traits::FramebufferW, Traits::FramebufferH, 1u }, + .extent = { framebufferW, framebufferH, 1u }, .mipLevels = 1u, .arrayLayers = 1u, .usage = USAGE @@ -742,7 +742,6 @@ class CScene final : public nbl::core::IReferenceCounted inline bool record() { EXPOSE_NABLA_NAMESPACES(); - bool valid = true; const struct @@ -783,24 +782,7 @@ class CScene final : public nbl::core::IReferenceCounted }; valid &= m_commandBuffer->beginRenderPass(info, IGPUCommandBuffer::SUBPASS_CONTENTS::INLINE); - - const auto& [hook, meta] = m_resources->objects[object.meta.type]; - const auto* rawPipeline = hook.pipeline.get(); - - SBufferBinding vertex = hook.bindings.vertex, index = hook.bindings.index; - - valid &= m_commandBuffer->bindGraphicsPipeline(rawPipeline); - valid &= m_commandBuffer->bindDescriptorSets(EPBP_GRAPHICS, rawPipeline->getLayout(), 1, 1, &m_ds.get()); - valid &= m_commandBuffer->bindVertexBuffers(0, 1, &vertex); - - if (index.buffer && hook.indexType != EIT_UNKNOWN) - { - valid &= m_commandBuffer->bindIndexBuffer(index, hook.indexType); - valid &= m_commandBuffer->drawIndexed(hook.indexCount, 1, 0, 0, 0); - } - else - valid &= m_commandBuffer->draw(hook.indexCount, 1, 0, 0); - + valid &= draw(m_commandBuffer.get()); valid &= m_commandBuffer->endRenderPass(); return valid; @@ -837,7 +819,7 @@ class CScene final : public nbl::core::IReferenceCounted return queue->submit(infos) == IQueue::RESULT::SUCCESS; } - inline void update() + inline bool update(nbl::video::IGPUCommandBuffer* cmdbuf = nullptr) { EXPOSE_NABLA_NAMESPACES(); @@ -845,12 +827,40 @@ class CScene final : public nbl::core::IReferenceCounted range.buffer = smart_refctd_ptr(m_ubo.buffer); range.size = m_ubo.buffer->getSize(); - m_commandBuffer->updateBuffer(range, &object.viewParameters); + if(cmdbuf) + return cmdbuf->updateBuffer(range, &object.viewParameters); + + return m_commandBuffer->updateBuffer(range, &object.viewParameters); } inline auto getColorAttachment() { return nbl::core::smart_refctd_ptr(m_color); } private: + inline bool draw(nbl::video::IGPUCommandBuffer* cmdbuf) + { + EXPOSE_NABLA_NAMESPACES(); + bool valid = true; + + const auto& [hook, meta] = m_resources->objects[object.meta.type]; + const auto* rawPipeline = hook.pipeline.get(); + + SBufferBinding vertex = hook.bindings.vertex, index = hook.bindings.index; + + valid &= cmdbuf->bindGraphicsPipeline(rawPipeline); + valid &= cmdbuf->bindDescriptorSets(EPBP_GRAPHICS, rawPipeline->getLayout(), 1, 1, &m_ds.get()); + valid &= cmdbuf->bindVertexBuffers(0, 1, &vertex); + + if (index.buffer && hook.indexType != EIT_UNKNOWN) + { + valid &= cmdbuf->bindIndexBuffer(index, hook.indexType); + valid &= cmdbuf->drawIndexed(hook.indexCount, 1, 0, 0, 0); + } + else + valid &= cmdbuf->draw(hook.indexCount, 1, 0, 0); + + return valid; + } + CScene(nbl::core::smart_refctd_ptr device, nbl::core::smart_refctd_ptr logger, nbl::core::smart_refctd_ptr commandBuffer, nbl::core::smart_refctd_ptr frameBuffer, nbl::core::smart_refctd_ptr ds, nbl::asset::SBufferBinding ubo, nbl::core::smart_refctd_ptr color, nbl::core::smart_refctd_ptr depth, const nbl::core::smart_refctd_ptr resources) : m_device(nbl::core::smart_refctd_ptr(device)), m_logger(nbl::core::smart_refctd_ptr(logger)), m_commandBuffer(nbl::core::smart_refctd_ptr(commandBuffer)), m_frameBuffer(nbl::core::smart_refctd_ptr(frameBuffer)), m_ds(nbl::core::smart_refctd_ptr(ds)), m_ubo(ubo), m_color(nbl::core::smart_refctd_ptr(color)), m_depth(nbl::core::smart_refctd_ptr(depth)), m_resources(nbl::core::smart_refctd_ptr(resources)) {} From 1c87634db423f436e5e6e8b68fb483698e7aea57 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Fri, 13 Dec 2024 16:01:12 +0100 Subject: [PATCH 056/205] disable WiP features (display on red or vanish) --- 61_UI/main.cpp | 47 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/61_UI/main.cpp b/61_UI/main.cpp index af88e8a8a..921ea2413 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -1395,9 +1395,23 @@ class UISampleApp final : public examples::SimpleWindowedApplication mCurrentGizmoOperation = ImGuizmo::SCALE; 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); + const bool isCameraModelBound = lastManipulatedModelIx == 1u; + { + ImGuiInputTextFlags flags = 0; + + if (isCameraModelBound) // TODO: cameras are WiP here + { + ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(1.0f, 0.0f, 0.0f, 0.5f)); + flags |= ImGuiInputTextFlags_ReadOnly; + } + + ImGui::InputFloat3("Tr", matrixTranslation, "%.3f", flags); + ImGui::InputFloat3("Rt", matrixRotation, "%.3f", flags); + ImGui::InputFloat3("Sc", matrixScale, "%.3f", flags); + + if(isCameraModelBound) + ImGui::PopStyleColor(); + } ImGuizmo::RecomposeMatrixFromComponents(matrixTranslation, matrixRotation, matrixScale, matrix); if (mCurrentGizmoOperation != ImGuizmo::SCALE) @@ -1409,19 +1423,22 @@ class UISampleApp final : public examples::SimpleWindowedApplication mCurrentGizmoMode = ImGuizmo::WORLD; } - ImGui::Checkbox(" ", &useSnap); - ImGui::SameLine(); - switch (mCurrentGizmoOperation) + if (not isCameraModelBound) // TODO: cameras are WiP here { - 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(" ", &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::End(); From 742b41a1635a11100221d9a7fbd509c7a3fe4a7e Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Fri, 13 Dec 2024 16:05:37 +0100 Subject: [PATCH 057/205] add help text for disabling/enabling selected camera movement --- 61_UI/main.cpp | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/61_UI/main.cpp b/61_UI/main.cpp index 921ea2413..caf888e4e 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -1279,6 +1279,28 @@ class UISampleApp final : public examples::SimpleWindowedApplication else ImGui::TextColored(ImVec4(1.0f, 0.0f, 0.0f, 1.0f), "Active Camera Movement: Disabled"); + if (ImGui::IsItemHovered()) + { + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.2f, 0.2f, 0.2f, 0.8f)); + ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.5f); + + ImVec2 mousePos = ImGui::GetMousePos(); + ImGui::SetNextWindowPos(ImVec2(mousePos.x + 10, mousePos.y + 10), ImGuiCond_Always); + + ImGui::Begin("HoverOverlay", nullptr, + ImGuiWindowFlags_NoDecoration | + ImGuiWindowFlags_AlwaysAutoResize | + ImGuiWindowFlags_NoSavedSettings); + + ImGui::Text("Press 'Left Shift' to Enable/Disable selected camera movement"); + + ImGui::End(); + + ImGui::PopStyleVar(); + ImGui::PopStyleColor(2); + } + ImGui::Separator(); for (size_t cameraIndex = 0; cameraIndex < CamerazCount; ++cameraIndex) From 702d7479aec19ad7836a22a8a5706395919931db Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Fri, 13 Dec 2024 17:20:21 +0100 Subject: [PATCH 058/205] Release & RWDI will need closer look, comment out more WiP --- 61_UI/main.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/61_UI/main.cpp b/61_UI/main.cpp index caf888e4e..081c6e194 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -1314,8 +1314,9 @@ class UISampleApp final : public examples::SimpleWindowedApplication if (ImGui::TreeNodeEx(treeNodeLabel.c_str(), flags)) { - ImGui::SliderFloat("Move speed", &moveSpeed[cameraIndex], 0.1f, 10.f); - ImGui::SliderFloat("Rotate speed", &rotateSpeed[cameraIndex], 0.1f, 10.f); + // WiP, does not affect camera yet + //ImGui::SliderFloat("Move speed", &moveSpeed[cameraIndex], 0.1f, 10.f); + //ImGui::SliderFloat("Rotate speed", &rotateSpeed[cameraIndex], 0.1f, 10.f); if (ImGui::TreeNodeEx("Data", ImGuiTreeNodeFlags_None)) { From 521116ea7c390dcf94e51c9276660d241559bc08 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Sat, 14 Dec 2024 14:50:51 +0100 Subject: [PATCH 059/205] allow to manipulate model gizmos in ortho mode, add sliders to move/rotation speed factors, fix issues with camera speed while being manipulated from imguizmo controller (write thoughts about sensitivity of event magnitudes), allow for axes flip for better visibility, bind camera movement into space instead of left shift, assume imguizmo controller generates events always from World space because of what its delta TRS matrix represents (no base map), add more docs --- 61_UI/main.cpp | 168 +++++++++++++------- common/include/camera/CFPSCamera.hpp | 22 ++- common/include/camera/ICamera.hpp | 2 +- common/include/camera/IGimbalController.hpp | 133 +++++++++++----- common/include/camera/IProjection.hpp | 4 +- 5 files changed, 231 insertions(+), 98 deletions(-) diff --git a/61_UI/main.cpp b/61_UI/main.cpp index 081c6e194..d5dc40d66 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -879,7 +879,6 @@ class UISampleApp final : public examples::SimpleWindowedApplication { ImGuiIO& io = ImGui::GetIO(); - ImGuizmo::SetOrthographic(false); ImGuizmo::BeginFrame(); auto projectionMatrices = projections->getLinearProjections(); @@ -1007,7 +1006,39 @@ class UISampleApp final : public examples::SimpleWindowedApplication if (useWindow) { // ImGuizmo manipulations on the last used model matrix in window mode - TransformEditor(&imguizmoM16InOut.inModel[lastManipulatedModelIx][0][0]); + IGimbalController::input_imguizmo_event_t deltaTRS; + TransformEditor(&imguizmoM16InOut.inModel[lastManipulatedModelIx][0][0], &deltaTRS); // TODO! DELTA TRS IS ONLY TRANSLATE TEMPORARY + + if (isCameraModelBound) + { + { + static std::vector virtualEvents(0x45); + + uint32_t vCount; + + secondcamera->beginInputProcessing(m_nextPresentationTimestamp); + { + secondcamera->process(nullptr, vCount); + + if (virtualEvents.size() < vCount) + virtualEvents.resize(vCount); + + IGimbalController::SUpdateParameters params; + params.imguizmoEvents = { { deltaTRS } }; + secondcamera->process(virtualEvents.data(), vCount, params); + } + secondcamera->endInputProcessing(); + + // I start to think controller should be able to set sensitivity to scale magnitudes of generated events + // in order for camera to not keep any magnitude scalars like move or rotation speed scales + smart_refctd_ptr_static_cast(secondcamera)->setMoveSpeedScale(1); + smart_refctd_ptr_static_cast(secondcamera)->setRotationSpeedScale(1); + + // NOTE: generated events from ImGuizmo controller are always in world space! + if(vCount) + secondcamera->manipulate({ virtualEvents.data(), vCount }, ICamera::World); + } + } uint32_t gizmoIx = {}; bool manipulatedFromAnyWindow = false; @@ -1019,6 +1050,16 @@ class UISampleApp final : public examples::SimpleWindowedApplication const auto projectionIx = cameraIx + 1u; // offset because first projection belongs to full screen (**) info.textureID = projectionIx; + if(isPerspective[projectionIx]) + ImGuizmo::SetOrthographic(false); + else + ImGuizmo::SetOrthographic(true); + + if (areAxesFlipped[projectionIx]) + ImGuizmo::AllowAxisFlip(true); + else + ImGuizmo::AllowAxisFlip(false); + ImGui::SetNextWindowSize(ImVec2(800, 400), ImGuiCond_Appearing); ImGui::SetNextWindowPos(ImVec2(400, 20 + cameraIx * 420), ImGuiCond_Appearing); ImGui::PushStyleColor(ImGuiCol_WindowBg, (ImVec4)ImColor(0.35f, 0.3f, 0.3f)); @@ -1045,6 +1086,9 @@ class UISampleApp final : public examples::SimpleWindowedApplication // however note it only makes sense to obsly assume we cannot manipulate 2 objects at the same time for (uint32_t modelIx = 0; modelIx < 2; modelIx++) { + const bool isCameraGizmoBound = gizmoIx == 1; + const bool discard = isCameraGizmoBound && mCurrentGizmoOperation != ImGuizmo::TRANSLATE; + // future logic will need to filter gizmos which represents gimbal of camera which view matrix we use to render scene with if (gizmoIx == 3) continue; @@ -1065,8 +1109,10 @@ class UISampleApp final : public examples::SimpleWindowedApplication // we manipulated a gizmo from a X-th window, now we update output matrices and assume no more gizmos can be manipulated at the same frame if (!manipulatedFromAnyWindow) { - // TMP WIP, our imguizmo controller doesnt support rotation & scale yet - auto discard = gizmoIx == 1 && mCurrentGizmoOperation != ImGuizmo::TRANSLATE; + // TMP WIP, our imguizmo controller doesnt support rotation & scale yet and its because + // - there are numerical issues with imguizmo decompose/recompose TRS (and the author also says it) + // - in rotate mode delta TRS matrix contains translate part (how? no idea, maybe imguizmo bug) and it glitches everything + if (!discard) { imguizmoM16InOut.outModel[modelIx] = model; @@ -1081,7 +1127,9 @@ class UISampleApp final : public examples::SimpleWindowedApplication { lastManipulatedModelIx = modelIx; lastManipulatedGizmoIx = gizmoIx; - lastManipulatedModelIdentifier = lastManipulatedModelIx == 0 ? "Geometry Creator Object" : "Camera FPS"; + isCameraModelBound = lastManipulatedModelIx == 1u; + + lastManipulatedModelIdentifier = isCameraModelBound ? "Camera FPS" : "Geometry Creator Object"; } if (ImGuizmo::IsOver()) @@ -1149,7 +1197,6 @@ class UISampleApp final : public examples::SimpleWindowedApplication if (ImGuizmo::IsUsingAny()) { uint32_t vCount; - auto nblManipulationMode = ICamera::Local; secondcamera->beginInputProcessing(m_nextPresentationTimestamp); { @@ -1159,30 +1206,28 @@ class UISampleApp final : public examples::SimpleWindowedApplication virtualEvents.resize(vCount); IGimbalController::SUpdateParameters params; - - auto getOrientationBasis = [&]() - { - auto orientationBasis = (float32_t3x3(1.f)); - - switch (mCurrentGizmoMode) - { - case ImGuizmo::LOCAL: orientationBasis = (float32_t3x3(getCastedMatrix(secondCameraGimbalModel))); break; - case ImGuizmo::WORLD: nblManipulationMode = ICamera::World; break; - default: assert(false); break; - } - - return orientationBasis; - }; - - params.imguizmoEvents = { { std::make_pair(imguizmoM16InOut.outDeltaTRS[1u], getOrientationBasis()) } }; + params.imguizmoEvents = { { imguizmoM16InOut.outDeltaTRS[1u] } }; secondcamera->process(virtualEvents.data(), vCount, params); } secondcamera->endInputProcessing(); - secondcamera->manipulate({ virtualEvents.data(), vCount }, nblManipulationMode); + // I start to think controller should be able to set sensitivity to scale magnitudes of generated events + // in order for camera to not keep any magnitude scalars like move or rotation speed scales + smart_refctd_ptr_static_cast(secondcamera)->setMoveSpeedScale(1); + smart_refctd_ptr_static_cast(secondcamera)->setRotationSpeedScale(1); + + // NOTE: generated events from ImGuizmo controller are always in world space! + secondcamera->manipulate({ virtualEvents.data(), vCount }, ICamera::World); } } + for (uint32_t i = 0u; i < cameraz.size(); ++i) + { + auto& camera = cameraz[i]; + smart_refctd_ptr_static_cast(camera)->setMoveSpeedScale(moveSpeed[i]); + smart_refctd_ptr_static_cast(camera)->setRotationSpeedScale(rotateSpeed[i]); + } + // update scenes data // to Nabla + update camera & model matrices @@ -1235,7 +1280,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication { ImGuiIO& io = ImGui::GetIO(); - if (ImGui::IsKeyPressed(ImGuiKey_LeftShift)) + if (ImGui::IsKeyPressed(ImGuiKey_Space)) enableActiveCameraMovement = !enableActiveCameraMovement; if (enableActiveCameraMovement) @@ -1293,7 +1338,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings); - ImGui::Text("Press 'Left Shift' to Enable/Disable selected camera movement"); + ImGui::Text("Press 'Space' to Enable/Disable selected camera movement"); ImGui::End(); @@ -1314,9 +1359,8 @@ class UISampleApp final : public examples::SimpleWindowedApplication if (ImGui::TreeNodeEx(treeNodeLabel.c_str(), flags)) { - // WiP, does not affect camera yet - //ImGui::SliderFloat("Move speed", &moveSpeed[cameraIndex], 0.1f, 10.f); - //ImGui::SliderFloat("Rotate speed", &rotateSpeed[cameraIndex], 0.1f, 10.f); + ImGui::SliderFloat("Move speed factor", &moveSpeed[cameraIndex], 0.0001f, 10.f, "%.4f", ImGuiSliderFlags_Logarithmic); + ImGui::SliderFloat("Rotate speed factor", &rotateSpeed[cameraIndex], 0.0001f, 10.f, "%.4f", ImGuiSliderFlags_Logarithmic); if (ImGui::TreeNodeEx("Data", ImGuiTreeNodeFlags_None)) { @@ -1370,6 +1414,9 @@ class UISampleApp final : public examples::SimpleWindowedApplication if (ImGui::RadioButton("RH", !isLH[lastProjectionIx])) isLH[lastProjectionIx] = false; + if(useWindow) + ImGui::Checkbox("Allow axes to flip##allowAxesToFlip", areAxesFlipped.data() + lastProjectionIx); + if (isPerspective[lastProjectionIx]) ImGui::SliderFloat("Fov", &fov[lastProjectionIx], 20.f, 150.f, "%.1f"); else @@ -1382,7 +1429,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication } } - inline void TransformEditor(float* matrix) + inline void TransformEditor(float* matrix, IGimbalController::input_imguizmo_event_t* deltaTRS = nullptr) { static float bounds[] = { -0.5f, -0.5f, -0.5f, 0.5f, 0.5f, 0.5f }; static float boundsSnap[] = { 0.1f, 0.1f, 0.1f }; @@ -1410,32 +1457,45 @@ class UISampleApp final : public examples::SimpleWindowedApplication 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; - float matrixTranslation[3], matrixRotation[3], matrixScale[3]; - ImGuizmo::DecomposeMatrixToComponents(matrix, matrixTranslation, matrixRotation, matrixScale); - const bool isCameraModelBound = lastManipulatedModelIx == 1u; + + float32_t3 matrixTranslation, matrixRotation, matrixScale; + IGimbalController::input_imguizmo_event_t decomposed, recomposed; + + if (deltaTRS) + *deltaTRS = IGimbalController::input_imguizmo_event_t(1); + + ImGuizmo::DecomposeMatrixToComponents(matrix, &matrixTranslation[0], &matrixRotation[0], &matrixScale[0]); + decomposed = *reinterpret_cast(matrix); { ImGuiInputTextFlags flags = 0; - if (isCameraModelBound) // TODO: cameras are WiP here + ImGui::InputFloat3("Tr", &matrixTranslation[0], "%.3f", flags); + + if (isCameraModelBound) // TODO: cameras are WiP here, imguizmo controller only works with translate manipulation + abs are banned currently { ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(1.0f, 0.0f, 0.0f, 0.5f)); flags |= ImGuiInputTextFlags_ReadOnly; } - ImGui::InputFloat3("Tr", matrixTranslation, "%.3f", flags); - ImGui::InputFloat3("Rt", matrixRotation, "%.3f", flags); - ImGui::InputFloat3("Sc", matrixScale, "%.3f", flags); + ImGui::InputFloat3("Rt", &matrixRotation[0], "%.3f", flags); + ImGui::InputFloat3("Sc", &matrixScale[0], "%.3f", flags); if(isCameraModelBound) ImGui::PopStyleColor(); } - ImGuizmo::RecomposeMatrixFromComponents(matrixTranslation, matrixRotation, matrixScale, matrix); + ImGuizmo::RecomposeMatrixFromComponents(&matrixTranslation[0], &matrixRotation[0], &matrixScale[0], matrix); + recomposed = *reinterpret_cast(matrix); + + // TODO AND NOTE: I only take care of translate part temporary! + if(deltaTRS) + deltaTRS->operator[](3) = recomposed[3] - decomposed[3]; if (mCurrentGizmoOperation != ImGuizmo::SCALE) { @@ -1446,22 +1506,19 @@ class UISampleApp final : public examples::SimpleWindowedApplication mCurrentGizmoMode = ImGuizmo::WORLD; } - if (not isCameraModelBound) // TODO: cameras are WiP here + ImGui::Checkbox(" ", &useSnap); + ImGui::SameLine(); + switch (mCurrentGizmoOperation) { - ImGui::Checkbox(" ", &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; - } + 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::End(); @@ -1562,11 +1619,12 @@ class UISampleApp final : public examples::SimpleWindowedApplication using linear_projection_t = CLinearProjection; nbl::core::smart_refctd_ptr projections; - bool flipGizmoY = true; - std::array isPerspective = { true, true, true }, isLH = { true, true, true }; + const bool flipGizmoY = true; + std::array isPerspective = { true, true, true }, isLH = { true, true, true }, areAxesFlipped = { false, false, false }; std::array fov = { 60.f, 60.f, 60.f }, zNear = { 0.1f, 0.1f, 0.1f }, zFar = { 10000.f, 10000.f, 10000.f }, viewWidth = { 10.f, 10.f, 10.f }, aspectRatio = {}, invAspectRatio = {}; - std::array moveSpeed = { 1.f, 1.f }, rotateSpeed = { 1.f, 1.f }; + std::array moveSpeed = { 0.01, 0.01 }, rotateSpeed = { 0.003, 0.003 }; + bool isCameraModelBound = false; float camYAngle = 165.f / 180.f * 3.14159f; float camXAngle = 32.f / 180.f * 3.14159f; float camDistance = 8.f; diff --git a/common/include/camera/CFPSCamera.hpp b/common/include/camera/CFPSCamera.hpp index 188a8a47f..8b5868566 100644 --- a/common/include/camera/CFPSCamera.hpp +++ b/common/include/camera/CFPSCamera.hpp @@ -31,7 +31,7 @@ class CFPSCamera final : public ICamera virtual bool manipulate(std::span virtualEvents, base_t::ManipulationMode mode) override { - constexpr double MoveSpeedScale = 0.01, RotateSpeedScale = 0.003, MaxVerticalAngle = glm::radians(88.0f), MinVerticalAngle = -MaxVerticalAngle; + constexpr double MaxVerticalAngle = glm::radians(88.0f), MinVerticalAngle = -MaxVerticalAngle; if (!virtualEvents.size()) return false; @@ -46,8 +46,8 @@ class CFPSCamera final : public ICamera const auto impulse = mode == base_t::Local ? m_gimbal.accumulate(virtualEvents) : m_gimbal.accumulate(virtualEvents, { 1, 0, 0 }, { 0, 1, 0 }, { 0, 0, 1 }); - const auto newPitch = std::clamp(gPitch + impulse.dVirtualRotation.x * RotateSpeedScale, MinVerticalAngle, MaxVerticalAngle), newYaw = gYaw + impulse.dVirtualRotation.y * RotateSpeedScale; - newOrientation = glm::quat(glm::vec3(newPitch, newYaw, 0.0f)); newPosition = m_gimbal.getPosition() + impulse.dVirtualTranslate * MoveSpeedScale; + const auto newPitch = std::clamp(gPitch + impulse.dVirtualRotation.x * m_rotationSpeedScale, MinVerticalAngle, MaxVerticalAngle), newYaw = gYaw + impulse.dVirtualRotation.y * m_rotationSpeedScale; + newOrientation = glm::quat(glm::vec3(newPitch, newYaw, 0.0f)); newPosition = m_gimbal.getPosition() + impulse.dVirtualTranslate * m_moveSpeedScale; bool manipulated = true; @@ -81,9 +81,25 @@ class CFPSCamera final : public ICamera return "FPS Camera"; } + // (***) + inline void setMoveSpeedScale(double scalar) + { + m_moveSpeedScale = scalar; + } + + // (***) + inline void setRotationSpeedScale(double scalar) + { + m_rotationSpeedScale = scalar; + } + private: typename base_t::CGimbal m_gimbal; + // (***) TODO: I need to think whether a camera should own this or controllers should be able + // to set sensitivity to scale magnitudes of generated events we put into manipulate method + double m_moveSpeedScale = 1, m_rotationSpeedScale = 1; + static inline constexpr auto AllowedLocalVirtualEvents = CVirtualGimbalEvent::Translate | CVirtualGimbalEvent::TiltUp | CVirtualGimbalEvent::TiltDown | CVirtualGimbalEvent::PanRight | CVirtualGimbalEvent::PanLeft; static inline constexpr auto AllowedWorldVirtualEvents = AllowedLocalVirtualEvents; diff --git a/common/include/camera/ICamera.hpp b/common/include/camera/ICamera.hpp index 1f81903e5..fa3d62770 100644 --- a/common/include/camera/ICamera.hpp +++ b/common/include/camera/ICamera.hpp @@ -83,7 +83,7 @@ class ICamera : public IGimbalController, virtual public core::IReferenceCounted // Manipulates camera with virtual events, returns true if *any* manipulation happens, it may fail partially or fully because each camera type has certain constraints which determine how it actually works // TODO: this really needs to be moved to more abstract interface, eg. IObjectTransform or something and ICamera should inherit it (its also an object!) - virtual bool manipulate(std::span virtualEvents, ManipulationMode mode) = 0; + virtual bool manipulate(std::span virtualEvents, ManipulationMode mode) = 0; // VirtualEventType bitmask for a camera view gimbal manipulation requests filtering virtual const uint32_t getAllowedVirtualEvents(ManipulationMode mode) = 0u; diff --git a/common/include/camera/IGimbalController.hpp b/common/include/camera/IGimbalController.hpp index b3f3b5917..b6e2ec00a 100644 --- a/common/include/camera/IGimbalController.hpp +++ b/common/include/camera/IGimbalController.hpp @@ -101,7 +101,7 @@ class IGimbalController : public IGimbalManipulateEncoder using input_mouse_event_t = ui::SMouseEvent; //! input of ImGuizmo gimbal controller process utility - ImGuizmo manipulate utility produces "delta (TRS) matrix" events - using input_imguizmo_event_t = std::pair; + using input_imguizmo_event_t = float32_t4x4; void beginInputProcessing(const std::chrono::microseconds nextPresentationTimeStamp) { @@ -131,7 +131,29 @@ class IGimbalController : public IGimbalManipulateEncoder std::span imguizmoEvents = {}; }; - // Processes SUpdateParameters events to generate virtual manipulation events + /** + * @brief Processes combined events from SUpdateParameters to generate virtual manipulation events. + * + * @note This function combines the processing of events from keyboards, mouse and ImGuizmo. + * It delegates the actual processing to the respective functions: + * - @ref processKeyboard for keyboard events + * - @ref processMouse for mouse events + * - @ref processImguizmo for ImGuizmo events + * The results are accumulated into the output array and the total count. + * + * @param "output" is a pointer to the array where all generated gimbal events will be stored. + * If nullptr, the function will only calculate the total count of potential + * output events without processing. + * + * @param "count" is a uint32_t reference to store the total count of generated gimbal events. + * + * @param "parameters" is an SUpdateParameters structure containing the individual event arrays + * for keyboard, mouse, and ImGuizmo inputs. + * + * @return void. If "count" > 0 and "output" is a valid pointer, use it to dereference your "output" + * containing "count" events. If "output" is nullptr, "count" tells you the total size of "output" + * you must guarantee to be valid. + */ void process(gimbal_event_t* output, uint32_t& count, const SUpdateParameters parameters = {}) { count = 0u; @@ -158,7 +180,25 @@ class IGimbalController : public IGimbalManipulateEncoder inline const imguizmo_to_virtual_events_t& getImguizmoVirtualEventMap() { return m_imguizmoVirtualEventMap; } private: - // Processes keyboard events to generate virtual manipulation events + /** + * @brief Processes keyboard events to generate virtual manipulation events. + * + * @note This function maps keyboard events into virtual gimbal manipulation events + * based on predefined mappings. It supports event types such as key press and key release + * to trigger corresponding actions. + * + * @param "output" is a pointer to the array where generated gimbal events will be stored. + * If nullptr, the function will only calculate the count of potential + * output events without processing. + * + * @param "count" is a uint32_t reference to store the count of generated gimbal events. + * + * @param "events" is a span of input_keyboard_event_t. Each such event contains a key code and action, + * such as key press or release. + * + * @return void. If "count" > 0 and "output" is a valid pointer, use it to dereference your "output" + * containing "count" events. If "output" is nullptr, "count" tells you the size of "output" you must guarantee to be valid. + */ void processKeyboard(gimbal_event_t* output, uint32_t& count, std::span events) { count = 0u; @@ -201,7 +241,25 @@ class IGimbalController : public IGimbalManipulateEncoder } } - // Processes mouse events to generate virtual manipulation events + /** + * @brief Processes mouse events to generate virtual manipulation events. + * + * @note This function processes mouse input, including clicks, scrolls, and movements, + * and maps them into virtual gimbal manipulation events. Mouse actions are processed + * using predefined mappings to determine corresponding gimbal manipulations. + * + * @param "output" is a pointer to the array where generated gimbal events will be stored. + * If nullptr, the function will only calculate the count of potential + * output events without processing. + * + * @param "count" is a uint32_t reference to store the count of generated gimbal events. + * + * @param "events" is a span of input_mouse_event_t. Each such event represents a mouse action, + * including clicks, scrolls, or movements. + * + * @return void. If "count" > 0 and "output" is a valid pointer, use it to dereference your "output" + * containing "count" events. If "output" is nullptr, "count" tells you the size of "output" you must guarantee to be valid. + */ void processMouse(gimbal_event_t* output, uint32_t& count, std::span events) { count = 0u; @@ -277,6 +335,26 @@ class IGimbalController : public IGimbalManipulateEncoder } } + /** + * @brief Processes input events from ImGuizmo and generates virtual gimbal events. + * + * @note This function is intended to process transformations provided by ImGuizmo and convert + * them into virtual gimbal events for the ICamera::World mode (ICamera::Local is invalid!). + * The function computes translation, rotation, and scale deltas from ImGuizmo's delta matrix, + * which are then mapped to corresponding virtual events using a predefined mapping. + * + * @param "output" is pointer to the array where generated gimbal events will be stored. + * If nullptr, the function will only calculate the count of potential + * output events without processing. + * + * @param "count" is uint32_t reference to store the count of generated gimbal events. + * + * @param "events" is a span of input_imguizmo_event_t. Each such event contains a delta + * transformation matrix that represents changes in world space. + * + * @return void. If "count" > 0 & "output" was valid pointer then use it to dereference your "output" containing "count" events. + * If "output" is nullptr then "count" tells you about size of "output" you must guarantee to be valid. + */ void processImguizmo(gimbal_event_t* output, uint32_t& count, std::span events) { count = 0u; @@ -294,51 +372,32 @@ class IGimbalController : public IGimbalManipulateEncoder for (const auto& ev : events) { - // TODO: debug assert "orientationBasis" is orthonormal - const auto& [deltaWorldTRS, orientationBasis] = ev; + const auto& deltaWorldTRS = ev; struct { float32_t3 dTranslation, dRotation, dScale; - } world, local; // its important to notice our imguizmo deltas are written in world base, so I will assume you generate events with respect for input local basis + } world; - // TODO: since I assume the delta matrix is from imguizmo I must follow its API (but this one I could actually steal & rewrite to Nabla to not call imguizmo stuff here <- its TEMP) + // TODO: since I assume the delta matrix is from imguizmo I must follow its API (but this one I could actually steal & rewrite to Nabla to not call imguizmo stuff here <- its TEMP!!!!!!!) + // - there are numerical issues with imguizmo decompose/recompose TRS (and the author also says it, we should have this util in Nabla and work with doubles not floats) + // - in rotate mode delta TRS matrix contains translate part (how? no idea, maybe imguizmo bug) and it glitches everything, I get translations e-07 or something ImGuizmo::DecomposeMatrixToComponents(&deltaWorldTRS[0][0], &world.dTranslation[0], &world.dRotation[0], &world.dScale[0]); - // FP precision threshold, lets filter small artifacts for this event generator to be more accurate - constexpr float threshold = 1e-8f; - - auto filterDelta = [](const float32_t3& in) -> float32_t3 - { - return - { - (std::abs(in[0]) > threshold) ? in[0] : 0.0f, - (std::abs(in[1]) > threshold) ? in[1] : 0.0f, - (std::abs(in[2]) > threshold) ? in[2] : 0.0f - }; - }; - - local.dTranslation = filterDelta(mul(orientationBasis, world.dTranslation)); - - // TODO - local.dRotation = { 0.f, 0.f, 0.f }; - // TODO - local.dScale = { 1.f, 1.f, 1.f }; - // Delta translation impulse - requestMagnitudeUpdateWithScalar(0.f, local.dTranslation[0], std::abs(local.dTranslation[0]), gimbal_event_t::MoveRight, gimbal_event_t::MoveLeft, m_imguizmoVirtualEventMap); - requestMagnitudeUpdateWithScalar(0.f, local.dTranslation[1], std::abs(local.dTranslation[1]), gimbal_event_t::MoveUp, gimbal_event_t::MoveDown, m_imguizmoVirtualEventMap); - requestMagnitudeUpdateWithScalar(0.f, local.dTranslation[2], std::abs(local.dTranslation[2]), gimbal_event_t::MoveForward, gimbal_event_t::MoveBackward, m_imguizmoVirtualEventMap); + requestMagnitudeUpdateWithScalar(0.f, world.dTranslation[0], std::abs(world.dTranslation[0]), gimbal_event_t::MoveRight, gimbal_event_t::MoveLeft, m_imguizmoVirtualEventMap); + requestMagnitudeUpdateWithScalar(0.f, world.dTranslation[1], std::abs(world.dTranslation[1]), gimbal_event_t::MoveUp, gimbal_event_t::MoveDown, m_imguizmoVirtualEventMap); + requestMagnitudeUpdateWithScalar(0.f, world.dTranslation[2], std::abs(world.dTranslation[2]), gimbal_event_t::MoveForward, gimbal_event_t::MoveBackward, m_imguizmoVirtualEventMap); // Delta rotation impulse - requestMagnitudeUpdateWithScalar(0.f, local.dRotation[0], std::abs(local.dRotation[0]), gimbal_event_t::PanRight, gimbal_event_t::PanLeft, m_imguizmoVirtualEventMap); - requestMagnitudeUpdateWithScalar(0.f, local.dRotation[1], std::abs(local.dRotation[1]), gimbal_event_t::TiltUp, gimbal_event_t::TiltDown, m_imguizmoVirtualEventMap); - requestMagnitudeUpdateWithScalar(0.f, local.dRotation[2], std::abs(local.dRotation[2]), gimbal_event_t::RollRight, gimbal_event_t::RollLeft, m_imguizmoVirtualEventMap); + //requestMagnitudeUpdateWithScalar(0.f, world.dRotation[0], std::abs(world.dRotation[0]), gimbal_event_t::TiltUp , gimbal_event_t::TiltDown, m_imguizmoVirtualEventMap); + //requestMagnitudeUpdateWithScalar(0.f, world.dRotation[1], std::abs(world.dRotation[1]), gimbal_event_t::PanRight, gimbal_event_t::PanLeft, m_imguizmoVirtualEventMap); + //requestMagnitudeUpdateWithScalar(0.f, world.dRotation[2], std::abs(world.dRotation[2]), gimbal_event_t::RollRight, gimbal_event_t::RollLeft, m_imguizmoVirtualEventMap); // Delta scale impulse - requestMagnitudeUpdateWithScalar(1.f, local.dScale[0], std::abs(local.dScale[0]), gimbal_event_t::ScaleXInc, gimbal_event_t::ScaleXDec, m_imguizmoVirtualEventMap); - requestMagnitudeUpdateWithScalar(1.f, local.dScale[1], std::abs(local.dScale[1]), gimbal_event_t::ScaleYInc, gimbal_event_t::ScaleYDec, m_imguizmoVirtualEventMap); - requestMagnitudeUpdateWithScalar(1.f, local.dScale[2], std::abs(local.dScale[2]), gimbal_event_t::ScaleZInc, gimbal_event_t::ScaleZDec, m_imguizmoVirtualEventMap); + //requestMagnitudeUpdateWithScalar(1.f, world.dScale[0], std::abs(world.dScale[0]), gimbal_event_t::ScaleXInc, gimbal_event_t::ScaleXDec, m_imguizmoVirtualEventMap); + //requestMagnitudeUpdateWithScalar(1.f, world.dScale[1], std::abs(world.dScale[1]), gimbal_event_t::ScaleYInc, gimbal_event_t::ScaleYDec, m_imguizmoVirtualEventMap); + //requestMagnitudeUpdateWithScalar(1.f, world.dScale[2], std::abs(world.dScale[2]), gimbal_event_t::ScaleZInc, gimbal_event_t::ScaleZDec, m_imguizmoVirtualEventMap); } postprocess(m_imguizmoVirtualEventMap, output, count); diff --git a/common/include/camera/IProjection.hpp b/common/include/camera/IProjection.hpp index 7fa94e517..357adefe9 100644 --- a/common/include/camera/IProjection.hpp +++ b/common/include/camera/IProjection.hpp @@ -38,7 +38,7 @@ class IProjection * * @param "vecToProjectionSpace" is a vector to transform from its space into projection space. * @param "output" is a vector which is "vecToProjectionSpace" transformed into projection space. - * @return The vector in projection space. + * @return void. "output" is the vector in projection space. */ virtual void project(const projection_vector_t& vecToProjectionSpace, projection_vector_t& output) const = 0; @@ -48,7 +48,7 @@ class IProjection * * @param "vecFromProjectionSpace" is a vector in the projection space to transform back to original space. * @param "output" is a vector which is "vecFromProjectionSpace" transformed back to its original space. - * @return The vector in the original space. + * @return true if inverse succeeded and then "output" is the vector in the original space. False otherwise. */ virtual bool unproject(const projection_vector_t& vecFromProjectionSpace, projection_vector_t& output) const = 0; From 2aa102126e6cf21b888574602fbf40ff24fc95ca Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Sat, 14 Dec 2024 16:03:03 +0100 Subject: [PATCH 060/205] allow to pick combo with object type when TRS editor has object from geometry creator bound (instead of mouse scroll), fix issues with capturing events when window has no focus (no queues & discard old ones) --- 61_UI/main.cpp | 57 +++++++++++++++++++++++++++++++++++++------------- 1 file changed, 42 insertions(+), 15 deletions(-) diff --git a/61_UI/main.cpp b/61_UI/main.cpp index d5dc40d66..10e7d1bc4 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -785,6 +785,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication }; m_surface->present(std::move(swapchainLock), presentInfo); } + firstFrame = false; } inline bool keepRunning() override @@ -821,25 +822,21 @@ class UISampleApp final : public examples::SimpleWindowedApplication std::vector mouse {}; std::vector keyboard {}; } capturedEvents; - - mouse.consumeEvents([&](const IMouseEventChannel::range_t& events) -> void { - for (const auto& e : events) + mouse.consumeEvents([&](const IMouseEventChannel::range_t& events) -> void { - capturedEvents.mouse.emplace_back(e); - - if (e.type == nbl::ui::SMouseEvent::EET_SCROLL) - gcIndex = std::clamp(int16_t(gcIndex) + int16_t(core::sign(e.scrollEvent.verticalScroll)), int64_t(0), int64_t(OT_COUNT - (uint8_t)1u)); - } - }, m_logger.get()); + if (m_window->hasInputFocus()) + for (const auto& e : events) + capturedEvents.mouse.emplace_back(e); + }, m_logger.get()); - keyboard.consumeEvents([&](const IKeyboardEventChannel::range_t& events) -> void - { - for (const auto& e : events) + keyboard.consumeEvents([&](const IKeyboardEventChannel::range_t& events) -> void { - capturedEvents.keyboard.emplace_back(e); - } - }, m_logger.get()); + if (m_window->hasInputFocus()) + for (const auto& e : events) + capturedEvents.keyboard.emplace_back(e); + }, m_logger.get()); + } const auto cursorPosition = m_window->getCursorControl()->getPosition(); @@ -1453,6 +1450,36 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGui::Text("Identifier: \"%s\"", lastManipulatedModelIdentifier.c_str()); ImGui::Separator(); + + if (!isCameraModelBound) + { + static const char* gcObjectTypeNames[] = { + "Cube", + "Sphere", + "Cylinder", + "Rectangle", + "Disk", + "Arrow", + "Cone", + "Icosphere" + }; + + if (ImGui::BeginCombo("Object Type", gcObjectTypeNames[gcIndex])) + { + for (uint8_t i = 0; i < ObjectType::OT_COUNT; ++i) + { + bool isSelected = (static_cast(gcIndex) == static_cast(i)); + if (ImGui::Selectable(gcObjectTypeNames[i], isSelected)) + gcIndex = i; + + if (isSelected) + ImGui::SetItemDefaultFocus(); + } + ImGui::EndCombo(); + } + } + + addMatrixTable("Model (TRS) Matrix", "ModelMatrixTable", 4, 4, matrix); if (ImGui::RadioButton("Translate", mCurrentGizmoOperation == ImGuizmo::TRANSLATE)) From 318a7c98abef7941bcb1364c5ac5e58d53efe7b7 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Sat, 14 Dec 2024 17:03:41 +0100 Subject: [PATCH 061/205] forgot to remove old struct from ICamera interface, also use ImGuiSliderFlags_Logarithmic for some sliders --- 61_UI/main.cpp | 9 ++++----- common/include/camera/ICamera.hpp | 5 ----- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/61_UI/main.cpp b/61_UI/main.cpp index 10e7d1bc4..acc42106e 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -1386,7 +1386,6 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGui::End(); } - } // Projections @@ -1415,12 +1414,12 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGui::Checkbox("Allow axes to flip##allowAxesToFlip", areAxesFlipped.data() + lastProjectionIx); if (isPerspective[lastProjectionIx]) - ImGui::SliderFloat("Fov", &fov[lastProjectionIx], 20.f, 150.f, "%.1f"); + ImGui::SliderFloat("Fov", &fov[lastProjectionIx], 20.f, 150.f, "%.1f", ImGuiSliderFlags_Logarithmic); else - ImGui::SliderFloat("Ortho width", &viewWidth[lastProjectionIx], 1.f, 20.f, "%.1f"); + ImGui::SliderFloat("Ortho width", &viewWidth[lastProjectionIx], 1.f, 20.f, "%.1f", ImGuiSliderFlags_Logarithmic); - ImGui::SliderFloat("zNear", &zNear[lastProjectionIx], 0.1f, 100.f, "%.2f"); - ImGui::SliderFloat("zFar", &zFar[lastProjectionIx], 110.f, 10000.f, "%.1f"); + ImGui::SliderFloat("zNear", &zNear[lastProjectionIx], 0.1f, 100.f, "%.2f", ImGuiSliderFlags_Logarithmic); + ImGui::SliderFloat("zFar", &zFar[lastProjectionIx], 110.f, 10000.f, "%.1f", ImGuiSliderFlags_Logarithmic); ImGui::End(); } diff --git a/common/include/camera/ICamera.hpp b/common/include/camera/ICamera.hpp index fa3d62770..33890fef1 100644 --- a/common/include/camera/ICamera.hpp +++ b/common/include/camera/ICamera.hpp @@ -35,11 +35,6 @@ class ICamera : public IGimbalController, virtual public core::IReferenceCounted CGimbal(typename base_t::SCreationParameters&& parameters) : base_t(std::move(parameters)) { updateView(); } ~CGimbal() = default; - struct SView - { - matrix matrix = {}; - }; - inline void updateView() { const auto& gRight = base_t::getXAxis(), gUp = base_t::getYAxis(), gForward = base_t::getZAxis(); From 1f69a54a251a5d03c5080829b2fd0212b3482184 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Tue, 17 Dec 2024 20:51:08 +0100 Subject: [PATCH 062/205] create `CPlanarProjection.hpp` & `IPlanarProjection.hpp` classes, add 61_UI/cameras.json. Make a few changes to interface classes, load demo data from the json file. TODO for tomorrow: use loaded planar projections vector to interact with the application. --- 61_UI/CMakeLists.txt | 5 +- 61_UI/cameras.json | 105 +++++++ 61_UI/config.json.template | 28 -- 61_UI/include/common.hpp | 1 + 61_UI/main.cpp | 297 +++++++++++++++++++- 61_UI/pipeline.groovy | 50 ---- common/include/camera/CFPSCamera.hpp | 6 +- common/include/camera/CLinearProjection.hpp | 53 ++-- common/include/camera/CPlanarProjection.hpp | 42 +++ common/include/camera/IGimbal.hpp | 27 ++ common/include/camera/IGimbalController.hpp | 36 ++- common/include/camera/ILinearProjection.hpp | 43 +-- common/include/camera/IPlanarProjection.hpp | 116 ++++++++ common/include/camera/IProjection.hpp | 11 +- 14 files changed, 668 insertions(+), 152 deletions(-) create mode 100644 61_UI/cameras.json delete mode 100644 61_UI/config.json.template delete mode 100644 61_UI/pipeline.groovy create mode 100644 common/include/camera/CPlanarProjection.hpp create mode 100644 common/include/camera/IPlanarProjection.hpp diff --git a/61_UI/CMakeLists.txt b/61_UI/CMakeLists.txt index a34e46ce6..1930cb17f 100644 --- a/61_UI/CMakeLists.txt +++ b/61_UI/CMakeLists.txt @@ -15,4 +15,7 @@ if(NBL_BUILD_IMGUI) nbl_create_executable_project("${NBL_EXTRA_SOURCES}" "" "${NBL_INCLUDE_SERACH_DIRECTORIES}" "${NBL_LIBRARIES}" "${NBL_EXECUTABLE_PROJECT_CREATION_PCH_TARGET}") LINK_BUILTIN_RESOURCES_TO_TARGET(${EXECUTABLE_NAME} geometryCreatorSpirvBRD) -endif() \ No newline at end of file +endif() + +add_dependencies(${EXECUTABLE_NAME} argparse) +target_include_directories(${EXECUTABLE_NAME} PUBLIC $) \ No newline at end of file diff --git a/61_UI/cameras.json b/61_UI/cameras.json new file mode 100644 index 000000000..bd4d7aaa9 --- /dev/null +++ b/61_UI/cameras.json @@ -0,0 +1,105 @@ +{ + "cameras": [ + { + "type": "FPS", + "position": [-2.238, 1.438, -1.558], + "orientation": [0.253, 0.368, -0.105, 0.888] + }, + { + "type": "FPS", + "position": [-2.017, 0.386, 0.684], + "orientation": [0.047, 0.830, -0.072, 0.55] + } + ], + "projections": [ + { + "type": "perspective", + "fov": 60.0, + "zNear": 0.1, + "zFar": 100.0, + "leftHanded": true + }, + { + "type": "orthographic", + "orthoWidth": 10.0, + "zNear": 0.1, + "zFar": 100.0, + "leftHanded": true + } + ], + "viewports": [ + { + "camera": 0, + "planarControllerSet": [ + { + "projection": 0, + "controllers": { + "keyboard": 0, + "mouse": 0 + } + }, + { + "projection": 1, + "controllers": { + "keyboard": 1, + "mouse": 0 + } + } + ] + }, + { + "camera": 1, + "planarControllerSet": [ + { + "projection": 0, + "controllers": { + "keyboard": 0, + "mouse": 0 + } + }, + { + "projection": 1, + "controllers": { + "keyboard": 1, + "mouse": 0 + } + } + ] + } + ], + "controllers": { + "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" + } + } + ], + "mouse": [ + { + "mappings": { + "RELATIVE_POSITIVE_MOVEMENT_X": "PanRight", + "RELATIVE_NEGATIVE_MOVEMENT_X": "PanLeft", + "RELATIVE_POSITIVE_MOVEMENT_Y": "TiltUp", + "RELATIVE_NEGATIVE_MOVEMENT_Y": "TiltDown" + } + } + ] + } + } + \ No newline at end of file diff --git a/61_UI/config.json.template b/61_UI/config.json.template deleted file mode 100644 index f961745c1..000000000 --- a/61_UI/config.json.template +++ /dev/null @@ -1,28 +0,0 @@ -{ - "enableParallelBuild": true, - "threadsPerBuildProcess" : 2, - "isExecuted": false, - "scriptPath": "", - "cmake": { - "configurations": [ "Release", "Debug", "RelWithDebInfo" ], - "buildModes": [], - "requiredOptions": [] - }, - "profiles": [ - { - "backend": "vulkan", - "platform": "windows", - "buildModes": [], - "runConfiguration": "Release", - "gpuArchitectures": [] - } - ], - "dependencies": [], - "data": [ - { - "dependencies": [], - "command": [""], - "outputs": [] - } - ] -} \ No newline at end of file diff --git a/61_UI/include/common.hpp b/61_UI/include/common.hpp index 0c33a5813..4033afc14 100644 --- a/61_UI/include/common.hpp +++ b/61_UI/include/common.hpp @@ -12,6 +12,7 @@ #include "camera/CCubeProjection.hpp" #include "camera/CLinearProjection.hpp" +#include "camera/CPlanarProjection.hpp" // the example's headers #include "nbl/ui/ICursorControl.h" diff --git a/61_UI/main.cpp b/61_UI/main.cpp index acc42106e..57bc61b78 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -2,11 +2,21 @@ // This file is part of the "Nabla Engine". // For conditions of distribution and use, see copyright notice in nabla.h +#include "nlohmann/json.hpp" +#include "argparse/argparse.hpp" +using json = nlohmann::json; + #include "common.hpp" #include "keysmapping.hpp" #include "camera/CCubeProjection.hpp" #include "glm/glm/ext/matrix_clip_space.hpp" // TODO: TESTING +//! TODO: this could be engine class actually, temporary keepin it in the example though +class CPlanarProjectionCameraController +{ + +}; + constexpr IGPUImage::SSubresourceRange TripleBufferUsedSubresourceRange = { .aspectMask = IGPUImage::EAF_COLOR_BIT, @@ -239,6 +249,22 @@ class UISampleApp final : public examples::SimpleWindowedApplication inline bool onAppInitialized(smart_refctd_ptr&& system) override { + argparse::ArgumentParser program("Virtual camera event system demo"); + + program.add_argument("--file") + .required() + .help("Path to json file with camera inputs"); + + try + { + program.parse_args({ argv.data(), argv.data() + argv.size() }); + } + catch (const std::exception& err) + { + std::cerr << err.what() << std::endl << program; + return false; + } + // Create imput system m_inputSystem = make_smart_refctd_ptr(logger_opt_smart_ptr(smart_refctd_ptr(m_logger))); @@ -485,9 +511,261 @@ class UISampleApp final : public examples::SimpleWindowedApplication oracle.reportBeginFrameRecord(); - /* - TESTS, TODO: remove all once finished work & integrate with the example properly - */ + // json file with camera inputs + // @Yas: TODO: THIS NEEDS BETTER VALIDATION AND LOGS ON FAILURE (+ status logs would be nice)!!! + { + const auto cameraJsonFile = program.get("--file"); + + std::ifstream file(cameraJsonFile.c_str()); + if (!file.is_open()) + { + std::cerr << "Error: Cannot open input \"" << cameraJsonFile.c_str() << "\" json file."; + return false; + } + + json j; + file >> j; + + std::vector> cameras; + for (const auto& jCamera : j["cameras"]) + { + if (jCamera.contains("type")) + { + if (jCamera["type"] == "FPS") + { + if (!jCamera.contains("position")) + { + std::cerr << "Expected \"position\" keyword for camera definition!"; + return false; + } + + if (!jCamera.contains("orientation")) + { + std::cerr << "Expected \"orientation\" keyword for camera definition!"; + return false; + } + + auto position = [&]() + { + auto jret = jCamera["position"].get>(); + return float32_t3(jret[0], jret[1], jret[2]); + }(); + + auto orientation = [&]() + { + auto jret = jCamera["orientation"].get>(); + return glm::quat(jret[0], jret[1], jret[2], jret[3]); + }(); + + cameras.emplace_back() = make_smart_refctd_ptr(position, orientation); + } + else + { + std::cerr << "Unsupported camera type!"; + return false; + } + } + else + { + std::cerr << "Expected \"type\" keyword for camera definition!"; + return false; + } + } + + std::vector projections; + for (const auto& jProjection : j["projections"]) + { + if (jProjection.contains("type")) + { + float zNear, zFar; + bool leftHanded; + + if (!jProjection.contains("zNear")) + { + "Expected \"zNear\" keyword for planar projection definition!"; + return false; + } + + if (!jProjection.contains("zFar")) + { + "Expected \"zFar\" keyword for planar projection definition!"; + return false; + } + + if (!jProjection.contains("leftHanded")) + { + "Expected \"leftHanded\" keyword for planar projection definition!"; + return false; + } + + zNear = jProjection["zNear"].get(); + zFar = jProjection["zFar"].get(); + leftHanded = jProjection["leftHanded"].get(); + + if (jProjection["type"] == "perspective") + { + if (!jProjection.contains("fov")) + { + "Expected \"fov\" keyword for planar perspective projection definition!"; + return false; + } + + float fov = jProjection["fov"].get(); + projections.emplace_back(IPlanarProjection::CProjection::create(leftHanded, zNear, zFar, fov)); + + } + else if (jProjection["type"] == "orthographic") + { + if (!jProjection.contains("orthoWidth")) + { + "Expected \"orthoWidth\" keyword for planar orthographic projection definition!"; + return false; + } + + float orthoWidth = jProjection["orthoWidth"].get(); + projections.emplace_back(IPlanarProjection::CProjection::create(leftHanded, zNear, zFar, orthoWidth)); + } + else + { + std::cerr << "Unsupported projection!"; + return false; + } + } + } + + struct + { + std::vector keyboard; + std::vector mouse; + } controllers; + + if (j.contains("controllers")) + { + const auto& jControllers = j["controllers"]; + + if (jControllers.contains("keyboard")) + { + for (const auto& jKeyboard : jControllers["keyboard"]) + { + if (jKeyboard.contains("mappings")) + { + auto& controller = controllers.keyboard.emplace_back(); + for (const auto& [key, value] : jKeyboard["mappings"].items()) + { + const auto nativeCode = stringToKeyCode(key.c_str()); + + if (nativeCode == EKC_NONE) + { + std::cerr << "Invalid native key \"" << key.c_str() << "\" code mapping for keyboard controller!" << std::endl; + return false; + } + + controller[nativeCode] = CVirtualGimbalEvent::stringToVirtualEvent(value.get()); + } + } + else + { + std::cerr << "Expected \"mappings\" keyword for keyboard controller definition!" << std::endl; + return false; + } + } + } + else + { + std::cerr << "Expected \"keyboard\" keyword in controllers definition!" << std::endl; + return false; + } + + if (jControllers.contains("mouse")) + { + for (const auto& jMouse : jControllers["mouse"]) + { + if (jMouse.contains("mappings")) + { + auto& controller = controllers.mouse.emplace_back(); + for (const auto& [key, value] : jMouse["mappings"].items()) + { + const auto nativeCode = stringToMouseCode(key.c_str()); + + if (nativeCode == EMC_NONE) + { + std::cerr << "Invalid native key \"" << key.c_str() << "\" code mapping for mouse controller!" << std::endl; + return false; + } + + controller[nativeCode] = CVirtualGimbalEvent::stringToVirtualEvent(value.get()); + } + } + else + { + std::cerr << "Expected \"mappings\" keyword for mouse controller definition!" << std::endl; + return false; + } + } + } + else + { + std::cerr << "Expected \"mouse\" keyword in controllers definition!" << std::endl; + return false; + } + } + else + { + std::cerr << "Expected \"controllers\" keyword in JSON!" << std::endl; + return false; + } + + + // viewpoets here + if (j.contains("viewports")) + { + for (const auto& jViewport : j["viewports"]) + { + if (!jViewport.contains("camera")) + { + std::cerr << "Expected \"camera\" keyword in viewport definition!" << std::endl; + return false; + } + + const auto cameraIx = jViewport["camera"].get(); + auto& planars = m_planarProjections.emplace_back() = planar_projection_t::create(smart_refctd_ptr(cameras[cameraIx])); + + if (!jViewport.contains("planarControllerSet")) + { + std::cerr << "Expected \"planarControllerSet\" keyword in viewport definition!" << std::endl; + return false; + } + + for (const auto& jPlanarController : jViewport["planarControllerSet"]) + { + if (!jPlanarController.contains("projection")) + { + std::cerr << "Expected \"projection\" keyword in planarControllerSet!" << std::endl; + return false; + } + + if (!jPlanarController.contains("controllers")) + { + std::cerr << "Expected \"controllers\" keyword in planarControllerSet!" << std::endl; + return false; + } + + auto projectionIx = jPlanarController["projection"].get(); + auto keyboardControllerIx = jPlanarController["controllers"]["keyboard"].get(); + auto mouseControllerIx = jPlanarController["controllers"]["mouse"].get(); + + auto& projection = planars->getPlanarProjections().emplace_back(projections[projectionIx]); + projection.updateKeyboardMapping([&](auto& map) { map = controllers.keyboard[keyboardControllerIx]; }); + projection.updateMouseMapping([&](auto& map) { map = controllers.mouse[mouseControllerIx]; }); + } + } + } + else + { + std::cerr << "Expected \"viewports\" keyword in JSON!" << std::endl; + return false; + } + } const auto iAspectRatio = float(m_window->getWidth()) / float(m_window->getHeight()); const auto iInvAspectRatio = float(m_window->getHeight()) / float(m_window->getWidth()); @@ -880,7 +1158,11 @@ class UISampleApp final : public examples::SimpleWindowedApplication auto projectionMatrices = projections->getLinearProjections(); { - auto mutableRange = smart_refctd_ptr_static_cast(projections)->getLinearProjections(); + + /* + TODO: update it + + auto mutableRange = smart_refctd_ptr_static_cast(projections)->getLinearProjections(); for (uint32_t i = 0u; i < mutableRange.size(); ++i) { auto projection = mutableRange.begin() + i; @@ -902,6 +1184,9 @@ class UISampleApp final : public examples::SimpleWindowedApplication projection->setProjectionMatrix(buildProjectionMatrixOrthoRH(viewWidth[i], viewHeight, zNear[i], zFar[i])); } } + */ + + } /* @@ -1662,6 +1947,10 @@ class UISampleApp final : public examples::SimpleWindowedApplication std::string lastManipulatedModelIdentifier = "Geometry Creator Object"; bool firstFrame = true; + + using planar_projections_range_t = std::vector; + using planar_projection_t = CPlanarProjection; + std::vector> m_planarProjections; }; NBL_MAIN_FUNC(UISampleApp) \ No newline at end of file diff --git a/61_UI/pipeline.groovy b/61_UI/pipeline.groovy deleted file mode 100644 index 7b7c9702a..000000000 --- a/61_UI/pipeline.groovy +++ /dev/null @@ -1,50 +0,0 @@ -import org.DevshGraphicsProgramming.Agent -import org.DevshGraphicsProgramming.BuilderInfo -import org.DevshGraphicsProgramming.IBuilder - -class CUIBuilder extends IBuilder -{ - public CUIBuilder(Agent _agent, _info) - { - super(_agent, _info) - } - - @Override - public boolean prepare(Map axisMapping) - { - return true - } - - @Override - public boolean build(Map axisMapping) - { - IBuilder.CONFIGURATION config = axisMapping.get("CONFIGURATION") - IBuilder.BUILD_TYPE buildType = axisMapping.get("BUILD_TYPE") - - def nameOfBuildDirectory = getNameOfBuildDirectory(buildType) - def nameOfConfig = getNameOfConfig(config) - - agent.execute("cmake --build ${info.rootProjectPath}/${nameOfBuildDirectory}/${info.targetProjectPathRelativeToRoot} --target ${info.targetBaseName} --config ${nameOfConfig} -j12 -v") - - return true - } - - @Override - public boolean test(Map axisMapping) - { - return true - } - - @Override - public boolean install(Map axisMapping) - { - return true - } -} - -def create(Agent _agent, _info) -{ - return new CUIBuilder(_agent, _info) -} - -return this \ No newline at end of file diff --git a/common/include/camera/CFPSCamera.hpp b/common/include/camera/CFPSCamera.hpp index 8b5868566..a6d169f09 100644 --- a/common/include/camera/CFPSCamera.hpp +++ b/common/include/camera/CFPSCamera.hpp @@ -20,9 +20,9 @@ class CFPSCamera final : public ICamera : base_t(), m_gimbal({ .position = position, .orientation = orientation }) {} ~CFPSCamera() = default; - const base_t::keyboard_to_virtual_events_t& getKeyboardMappingPreset() const override { return m_keyboard_to_virtual_events_preset; } - const base_t::mouse_to_virtual_events_t& getMouseMappingPreset() const override { return m_mouse_to_virtual_events_preset; } - const base_t::imguizmo_to_virtual_events_t& getImguizmoMappingPreset() const override { return m_imguizmo_to_virtual_events_preset; } + const base_t::keyboard_to_virtual_events_t getKeyboardMappingPreset() const override { return m_keyboard_to_virtual_events_preset; } + const base_t::mouse_to_virtual_events_t getMouseMappingPreset() const override { return m_mouse_to_virtual_events_preset; } + const base_t::imguizmo_to_virtual_events_t getImguizmoMappingPreset() const override { return m_imguizmo_to_virtual_events_preset; } const typename base_t::CGimbal& getGimbal() override { diff --git a/common/include/camera/CLinearProjection.hpp b/common/include/camera/CLinearProjection.hpp index 77725125b..791e9cb1b 100644 --- a/common/include/camera/CLinearProjection.hpp +++ b/common/include/camera/CLinearProjection.hpp @@ -6,40 +6,39 @@ namespace nbl::hlsl { + template ProjectionsRange> + class CLinearProjection : public ILinearProjection + { + public: + using ILinearProjection::ILinearProjection; -template ProjectionsRange> -class CLinearProjection : public ILinearProjection -{ -public: - using ILinearProjection::ILinearProjection; - - CLinearProjection() = default; + CLinearProjection() = default; - inline static core::smart_refctd_ptr create(core::smart_refctd_ptr&& camera) - { - if (!camera) - return nullptr; + inline static core::smart_refctd_ptr create(core::smart_refctd_ptr&& camera) + { + if (!camera) + return nullptr; - return core::smart_refctd_ptr(new CLinearProjection(core::smart_refctd_ptr(camera)), core::dont_grab); - } + return core::smart_refctd_ptr(new CLinearProjection(core::smart_refctd_ptr(camera)), core::dont_grab); + } - virtual std::span getLinearProjections() const override - { - return std::span(m_projections.data(), m_projections.size()); - } + virtual std::span getLinearProjections() const override + { + return std::span(m_projections.data(), m_projections.size()); + } - inline std::span getLinearProjections() - { - return std::span(m_projections.data(), m_projections.size()); - } + inline std::span getLinearProjections() + { + return std::span(m_projections.data(), m_projections.size()); + } -private: - CLinearProjection(core::smart_refctd_ptr&& camera) - : ILinearProjection(core::smart_refctd_ptr(camera)) {} - virtual ~CLinearProjection() = default; + private: + CLinearProjection(core::smart_refctd_ptr&& camera) + : ILinearProjection(core::smart_refctd_ptr(camera)) {} + virtual ~CLinearProjection() = default; - ProjectionsRange m_projections; -}; + ProjectionsRange m_projections; + }; } // nbl::hlsl namespace diff --git a/common/include/camera/CPlanarProjection.hpp b/common/include/camera/CPlanarProjection.hpp new file mode 100644 index 000000000..f726ea959 --- /dev/null +++ b/common/include/camera/CPlanarProjection.hpp @@ -0,0 +1,42 @@ +#ifndef _NBL_C_PLANAR_PROJECTION_HPP_ +#define _NBL_C_PLANAR_PROJECTION_HPP_ + +#include "IPlanarProjection.hpp" +#include "IRange.hpp" + +namespace nbl::hlsl +{ + template ProjectionsRange> + class CPlanarProjection : public IPlanarProjection + { + public: + virtual ~CPlanarProjection() = default; + + inline static core::smart_refctd_ptr create(core::smart_refctd_ptr&& camera) + { + if (!camera) + return nullptr; + + return core::smart_refctd_ptr(new CPlanarProjection(core::smart_refctd_ptr(camera)), core::dont_grab); + } + + virtual std::span getLinearProjections() const override + { + return { reinterpret_cast(m_projections.data()), m_projections.size() }; + } + + inline ProjectionsRange& getPlanarProjections() + { + return m_projections; + } + + private: + CPlanarProjection(core::smart_refctd_ptr&& camera) + : IPlanarProjection(core::smart_refctd_ptr(camera)) {} + + ProjectionsRange m_projections; + }; + +} // nbl::hlsl namespace + +#endif // _NBL_C_PLANAR_PROJECTION_HPP_ \ No newline at end of file diff --git a/common/include/camera/IGimbal.hpp b/common/include/camera/IGimbal.hpp index 3fe98d3eb..1a94affc5 100644 --- a/common/include/camera/IGimbal.hpp +++ b/common/include/camera/IGimbal.hpp @@ -79,6 +79,33 @@ namespace nbl::hlsl } } + static constexpr VirtualEventType stringToVirtualEvent(std::string_view event) + { + if (event == "MoveForward") return MoveForward; + if (event == "MoveBackward") return MoveBackward; + if (event == "MoveLeft") return MoveLeft; + if (event == "MoveRight") return MoveRight; + if (event == "MoveUp") return MoveUp; + if (event == "MoveDown") return MoveDown; + if (event == "TiltUp") return TiltUp; + if (event == "TiltDown") return TiltDown; + if (event == "PanLeft") return PanLeft; + if (event == "PanRight") return PanRight; + if (event == "RollLeft") return RollLeft; + if (event == "RollRight") return RollRight; + if (event == "ScaleXInc") return ScaleXInc; + if (event == "ScaleXDec") return ScaleXDec; + if (event == "ScaleYInc") return ScaleYInc; + if (event == "ScaleYDec") return ScaleYDec; + if (event == "ScaleZInc") return ScaleZInc; + if (event == "ScaleZDec") return ScaleZDec; + if (event == "Translate") return Translate; + if (event == "Rotate") return Rotate; + if (event == "Scale") return Scale; + if (event == "None") return None; + return None; + } + static inline constexpr auto VirtualEventsTypeTable = []() { std::array output; diff --git a/common/include/camera/IGimbalController.hpp b/common/include/camera/IGimbalController.hpp index b6e2ec00a..c7a0c507a 100644 --- a/common/include/camera/IGimbalController.hpp +++ b/common/include/camera/IGimbalController.hpp @@ -77,13 +77,26 @@ struct IGimbalManipulateEncoder using imguizmo_to_virtual_events_t = std::unordered_map; //! default preset with encode_keyboard_code_t to gimbal_virtual_event_t map - virtual const keyboard_to_virtual_events_t& getKeyboardMappingPreset() const = 0u; + virtual const keyboard_to_virtual_events_t getKeyboardMappingPreset() const { return {}; } //! default preset with encode_keyboard_code_t to gimbal_virtual_event_t map - virtual const mouse_to_virtual_events_t& getMouseMappingPreset() const = 0u; + virtual const mouse_to_virtual_events_t getMouseMappingPreset() const { return {}; } //! default preset with encode_keyboard_code_t to gimbal_virtual_event_t map - virtual const imguizmo_to_virtual_events_t& getImguizmoMappingPreset() const = 0u; + virtual const imguizmo_to_virtual_events_t getImguizmoMappingPreset() const { return {}; } + + virtual const keyboard_to_virtual_events_t& getKeyboardVirtualEventMap() const = 0; + virtual const mouse_to_virtual_events_t& getMouseVirtualEventMap() const = 0; + virtual const imguizmo_to_virtual_events_t& getImguizmoVirtualEventMap() const = 0; + + // Binds mouse key codes to virtual events, the mapKeys lambda will be executed with controller keyboard_to_virtual_events_t table + virtual void updateKeyboardMapping(const std::function& mapKeys) = 0; + + // Binds mouse key codes to virtual events, the mapKeys lambda will be executed with controller mouse_to_virtual_events_t table + virtual void updateMouseMapping(const std::function& mapKeys) = 0; + + // Binds imguizmo key codes to virtual events, the mapKeys lambda will be executed with controller imguizmo_to_virtual_events_t table + virtual void updateImguizmoMapping(const std::function& mapKeys) = 0; }; class IGimbalController : public IGimbalManipulateEncoder @@ -115,14 +128,9 @@ class IGimbalController : public IGimbalManipulateEncoder m_lastVirtualUpTimeStamp = m_nextPresentationTimeStamp; } - // Binds mouse key codes to virtual events, the mapKeys lambda will be executed with controller keyboard_to_virtual_events_t table - void updateKeyboardMapping(const std::function& mapKeys) { mapKeys(m_keyboardVirtualEventMap); } - - // Binds mouse key codes to virtual events, the mapKeys lambda will be executed with controller mouse_to_virtual_events_t table - void updateMouseMapping(const std::function& mapKeys) { mapKeys(m_mouseVirtualEventMap); } - - // Binds imguizmo key codes to virtual events, the mapKeys lambda will be executed with controller imguizmo_to_virtual_events_t table - void updateImguizmoMapping(const std::function& mapKeys) { mapKeys(m_imguizmoVirtualEventMap); } + virtual void updateKeyboardMapping(const std::function& mapKeys) override { mapKeys(m_keyboardVirtualEventMap); } + virtual void updateMouseMapping(const std::function& mapKeys) override { mapKeys(m_mouseVirtualEventMap); } + virtual void updateImguizmoMapping(const std::function& mapKeys) override { mapKeys(m_imguizmoVirtualEventMap); } struct SUpdateParameters { @@ -175,9 +183,9 @@ class IGimbalController : public IGimbalManipulateEncoder count = vKeyboardEventsCount + vMouseEventsCount + vImguizmoEventsCount; } - inline const keyboard_to_virtual_events_t& getKeyboardVirtualEventMap() { return m_keyboardVirtualEventMap; } - inline const mouse_to_virtual_events_t& getMouseVirtualEventMap() { return m_mouseVirtualEventMap; } - inline const imguizmo_to_virtual_events_t& getImguizmoVirtualEventMap() { return m_imguizmoVirtualEventMap; } + virtual const keyboard_to_virtual_events_t& getKeyboardVirtualEventMap() const override { return m_keyboardVirtualEventMap; } + virtual const mouse_to_virtual_events_t& getMouseVirtualEventMap() const override { return m_mouseVirtualEventMap; } + virtual const imguizmo_to_virtual_events_t& getImguizmoVirtualEventMap() const override { return m_imguizmoVirtualEventMap; } private: /** diff --git a/common/include/camera/ILinearProjection.hpp b/common/include/camera/ILinearProjection.hpp index 6222a58b4..fc0115610 100644 --- a/common/include/camera/ILinearProjection.hpp +++ b/common/include/camera/ILinearProjection.hpp @@ -38,27 +38,6 @@ class ILinearProjection : virtual public core::IReferenceCounted CProjection() : CProjection(projection_matrix_t(1)) {} CProjection(const projection_matrix_t& matrix) { setProjectionMatrix(matrix); } - inline void setProjectionMatrix(const projection_matrix_t& matrix) - { - m_projectionMatrix = matrix; - const auto det = hlsl::determinant(m_projectionMatrix); - - // we will allow you to lose a dimension since such a projection itself *may* - // be valid, however then you cannot un-project because the inverse doesn't exist! - m_isProjectionSingular = not det; - - if (m_isProjectionSingular) - { - m_isProjectionLeftHanded = std::nullopt; - m_invProjectionMatrix = std::nullopt; - } - else - { - m_isProjectionLeftHanded = det < 0.0; - m_invProjectionMatrix = inverse(m_projectionMatrix); - } - } - //! Returns P (Projection matrix) inline const projection_matrix_t& getProjectionMatrix() const { return m_projectionMatrix; } @@ -84,6 +63,28 @@ class ILinearProjection : virtual public core::IReferenceCounted return true; } + protected: + inline void setProjectionMatrix(const projection_matrix_t& matrix) + { + m_projectionMatrix = matrix; + const auto det = hlsl::determinant(m_projectionMatrix); + + // we will allow you to lose a dimension since such a projection itself *may* + // be valid, however then you cannot un-project because the inverse doesn't exist! + m_isProjectionSingular = not det; + + if (m_isProjectionSingular) + { + m_isProjectionLeftHanded = std::nullopt; + m_invProjectionMatrix = std::nullopt; + } + else + { + m_isProjectionLeftHanded = det < 0.0; + m_invProjectionMatrix = inverse(m_projectionMatrix); + } + } + private: projection_matrix_t m_projectionMatrix; inv_projection_matrix_t m_invProjectionMatrix; diff --git a/common/include/camera/IPlanarProjection.hpp b/common/include/camera/IPlanarProjection.hpp new file mode 100644 index 000000000..4ca9a0475 --- /dev/null +++ b/common/include/camera/IPlanarProjection.hpp @@ -0,0 +1,116 @@ +#ifndef _NBL_I_PLANAR_PROJECTION_HPP_ +#define _NBL_I_PLANAR_PROJECTION_HPP_ + +#include "ILinearProjection.hpp" + +namespace nbl::hlsl +{ + +class IPlanarProjection : public ILinearProjection +{ +public: + struct CProjection : public ILinearProjection::CProjection, public IGimbalManipulateEncoder + { + using base_t = ILinearProjection::CProjection; + + enum ProjectionType : uint8_t + { + Perspective, + Orthographic, + + Count + }; + + template + static CProjection create(Args&&... args) + requires (T != Count) + { + CProjection output; + + if constexpr (T == Perspective) output.setPerspective(std::forward(args)...); + else if (T == Orthographic) output.setOrthographic(std::forward(args)...); + + return output; + } + + CProjection(const CProjection& other) = default; + CProjection(CProjection&& other) noexcept = default; + + struct ProjectionParameters + { + ProjectionType m_type; + + union PlanarParameters + { + struct + { + float fov; + } perspective; + + struct + { + float orthoWidth; + } orthographic; + + PlanarParameters() {} + ~PlanarParameters() {} + } m_planar; + + float m_zNear; + float m_zFar; + }; + + inline void setPerspective(bool leftHanded = true, float zNear = 0.1f, float zFar = 100.f, float fov = 60.f, float aspectRatio = 16.f / 9.f) + { + m_parameters.m_type = Perspective; + m_parameters.m_planar.perspective.fov = fov; + m_parameters.m_zNear = zNear; + m_parameters.m_zFar = zFar; + + if (leftHanded) + base_t::setProjectionMatrix(buildProjectionMatrixPerspectiveFovLH(glm::radians(fov), aspectRatio, zNear, zFar)); + else + base_t::setProjectionMatrix(buildProjectionMatrixPerspectiveFovRH(glm::radians(fov), aspectRatio, zNear, zFar)); + } + + inline void setOrthographic(bool leftHanded = true, float zNear = 0.1f, float zFar = 100.f, float orthoWidth = 10.f, float aspectRatio = 16.f / 9.f) + { + m_parameters.m_type = Orthographic; + m_parameters.m_planar.orthographic.orthoWidth = orthoWidth; + m_parameters.m_zNear = zNear; + m_parameters.m_zFar = zFar; + + const auto viewHeight = orthoWidth * core::reciprocal(aspectRatio); + + if (leftHanded) + base_t::setProjectionMatrix(buildProjectionMatrixOrthoLH(orthoWidth, viewHeight, zNear, zFar)); + else + base_t::setProjectionMatrix(buildProjectionMatrixOrthoRH(orthoWidth, viewHeight, zNear, zFar)); + } + + virtual void updateKeyboardMapping(const std::function& mapKeys) override { mapKeys(m_keyboardVirtualEventMap); } + virtual void updateMouseMapping(const std::function& mapKeys) override { mapKeys(m_mouseVirtualEventMap); } + virtual void updateImguizmoMapping(const std::function& mapKeys) override { mapKeys(m_imguizmoVirtualEventMap); } + + virtual const keyboard_to_virtual_events_t& getKeyboardVirtualEventMap() const override { return m_keyboardVirtualEventMap; } + virtual const mouse_to_virtual_events_t& getMouseVirtualEventMap() const override { return m_mouseVirtualEventMap; } + virtual const imguizmo_to_virtual_events_t& getImguizmoVirtualEventMap() const override { return m_imguizmoVirtualEventMap; } + inline const ProjectionParameters& getParameters() const { return m_parameters; } + private: + CProjection() = default; + ProjectionParameters m_parameters; + + keyboard_to_virtual_events_t m_keyboardVirtualEventMap; + mouse_to_virtual_events_t m_mouseVirtualEventMap; + imguizmo_to_virtual_events_t m_imguizmoVirtualEventMap; + }; + +protected: + IPlanarProjection(core::smart_refctd_ptr&& camera) + : ILinearProjection(core::smart_refctd_ptr(camera)) {} + virtual ~IPlanarProjection() = default; +}; + +} // nbl::hlsl namespace + +#endif // _NBL_I_PLANAR_PROJECTION_HPP_ \ No newline at end of file diff --git a/common/include/camera/IProjection.hpp b/common/include/camera/IProjection.hpp index 357adefe9..cb6facdcc 100644 --- a/common/include/camera/IProjection.hpp +++ b/common/include/camera/IProjection.hpp @@ -15,13 +15,16 @@ class IProjection enum class ProjectionType { - //! Perspective, Orthographic, Oblique, Axonometric, Shear projections or any custom linear transformation + //! Any raw linear transformation, for example it may represent Perspective, Orthographic, Oblique, Axonometric, Shear projections Linear, - //! Represents pre-transform *concatenated* with linear view-port transform, projects onto a quad - Perspective, + //! Specialized linear projection for planar projections with parameters + Planar, - //! Represents a Perspective projection onto cube consisting of 6 quad cube faces + //! Extension of planar projection represented by pre-transform & planar transform combined projecting onto R3 cave quad + CaveQuad, + + //! Specialized CaveQuad projection, represents planar projections onto cube with 6 quad cube faces Cube, Spherical, From 507d9f0ff6ab270d2538e8d07ae2c398ceefb3c3 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Wed, 18 Dec 2024 16:39:26 +0100 Subject: [PATCH 063/205] planar projections are bound to controllers, now I need to fix manipulation bugs after demo & API updates --- 61_UI/include/keysmapping.hpp | 28 +- 61_UI/main.cpp | 867 ++++++++++++++------------- common/include/camera/CFPSCamera.hpp | 5 +- 3 files changed, 459 insertions(+), 441 deletions(-) diff --git a/61_UI/include/keysmapping.hpp b/61_UI/include/keysmapping.hpp index 474827b77..83ec22c32 100644 --- a/61_UI/include/keysmapping.hpp +++ b/61_UI/include/keysmapping.hpp @@ -2,9 +2,8 @@ #define __NBL_KEYSMAPPING_H_INCLUDED__ #include "common.hpp" -#include "camera/ICamera.hpp" -void handleAddMapping(const char* tableID, ICamera* camera, IGimbalManipulateEncoder::EncoderType activeController, CVirtualGimbalEvent::VirtualEventType& selectedEventType, ui::E_KEY_CODE& newKey, ui::E_MOUSE_CODE& newMouseCode, bool& addMode) +void handleAddMapping(const char* tableID, IGimbalManipulateEncoder* encoder, IGimbalManipulateEncoder::EncoderType activeController, CVirtualGimbalEvent::VirtualEventType& selectedEventType, ui::E_KEY_CODE& newKey, ui::E_MOUSE_CODE& newMouseCode, bool& addMode) { ImGui::BeginTable(tableID, 3, ImGuiTableFlags_Borders | ImGuiTableFlags_Resizable | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchSame); ImGui::TableSetupColumn("Virtual Event", ImGuiTableColumnFlags_WidthStretch, 0.33f); @@ -66,20 +65,19 @@ void handleAddMapping(const char* tableID, ICamera* camera, IGimbalManipulateEnc if (ImGui::Button("Confirm Add", ImVec2(100, 30))) { if (activeController == IGimbalManipulateEncoder::Keyboard) - camera->updateKeyboardMapping([&](auto& keys) { keys[newKey] = selectedEventType; }); + encoder->updateKeyboardMapping([&](auto& keys) { keys[newKey] = selectedEventType; }); else - camera->updateMouseMapping([&](auto& mouse) { mouse[newMouseCode] = selectedEventType; }); + encoder->updateMouseMapping([&](auto& mouse) { mouse[newMouseCode] = selectedEventType; }); addMode = false; } ImGui::EndTable(); } -void displayKeyMappingsAndVirtualStatesInline(ICamera* camera, bool spawnWindow = false) +void displayKeyMappingsAndVirtualStatesInline(IGimbalManipulateEncoder* encoder, bool spawnWindow = false) { - if (!camera) return; + if (!encoder) return; - // Per-camera state for UI rendering struct MappingState { bool addMode = false; @@ -89,11 +87,11 @@ void displayKeyMappingsAndVirtualStatesInline(ICamera* camera, bool spawnWindow IGimbalManipulateEncoder::EncoderType activeController = IGimbalManipulateEncoder::Keyboard; }; - static std::unordered_map cameraStates; - auto& state = cameraStates[camera]; + static std::unordered_map cameraStates; + auto& state = cameraStates[encoder]; - const auto& keyboardMappings = camera->getKeyboardVirtualEventMap(); - const auto& mouseMappings = camera->getMouseVirtualEventMap(); + const auto& keyboardMappings = encoder->getKeyboardVirtualEventMap(); + const auto& mouseMappings = encoder->getMouseVirtualEventMap(); if (spawnWindow) { @@ -145,7 +143,7 @@ void displayKeyMappingsAndVirtualStatesInline(ICamera* camera, bool spawnWindow ImGui::TableSetColumnIndex(4); if (ImGui::Button(("Delete##deleteKey" + std::to_string(static_cast(keyboardCode))).c_str())) { - camera->updateKeyboardMapping([keyboardCode](auto& keys) { keys.erase(keyboardCode); }); + encoder->updateKeyboardMapping([keyboardCode](auto& keys) { keys.erase(keyboardCode); }); break; } } @@ -154,7 +152,7 @@ void displayKeyMappingsAndVirtualStatesInline(ICamera* camera, bool spawnWindow if (state.addMode) { ImGui::Separator(); - handleAddMapping("AddKeyboardMappingTable", camera, state.activeController, state.selectedEventType, state.newKey, state.newMouseCode, state.addMode); + handleAddMapping("AddKeyboardMappingTable", encoder, state.activeController, state.selectedEventType, state.newKey, state.newMouseCode, state.addMode); } ImGui::EndTabItem(); @@ -202,7 +200,7 @@ void displayKeyMappingsAndVirtualStatesInline(ICamera* camera, bool spawnWindow ImGui::TableSetColumnIndex(4); if (ImGui::Button(("Delete##deleteMouse" + std::to_string(static_cast(mouseCode))).c_str())) { - camera->updateMouseMapping([mouseCode](auto& mouse) { mouse.erase(mouseCode); }); + encoder->updateMouseMapping([mouseCode](auto& mouse) { mouse.erase(mouseCode); }); break; } } @@ -211,7 +209,7 @@ void displayKeyMappingsAndVirtualStatesInline(ICamera* camera, bool spawnWindow if (state.addMode) { ImGui::Separator(); - handleAddMapping("AddMouseMappingTable", camera, state.activeController, state.selectedEventType, state.newKey, state.newMouseCode, state.addMode); + handleAddMapping("AddMouseMappingTable", encoder, state.activeController, state.selectedEventType, state.newKey, state.newMouseCode, state.addMode); } ImGui::EndTabItem(); } diff --git a/61_UI/main.cpp b/61_UI/main.cpp index 57bc61b78..109f24bdd 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -11,12 +11,6 @@ using json = nlohmann::json; #include "camera/CCubeProjection.hpp" #include "glm/glm/ext/matrix_clip_space.hpp" // TODO: TESTING -//! TODO: this could be engine class actually, temporary keepin it in the example though -class CPlanarProjectionCameraController -{ - -}; - constexpr IGPUImage::SSubresourceRange TripleBufferUsedSubresourceRange = { .aspectMask = IGPUImage::EAF_COLOR_BIT, @@ -197,12 +191,6 @@ class UISampleApp final : public examples::SimpleWindowedApplication constexpr static inline clock_t::duration DisplayImageDuration = std::chrono::milliseconds(900); - enum CameraRenderImguiTextureIx - { - OfflineSceneFirstCameraTextureIx = 1u, - OfflineSceneSecondCameraTextureIx = 2u - }; - public: using base_t::base_t; @@ -450,7 +438,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication m_ui.manager->registerListener([this]() -> void { imguiListen(); }); } - // Geometry Creator Render Scenes + // Geometry Creator Render Scene FBOs { resources = ResourcesBundle::create(m_device.get(), m_logger.get(), getGraphicsQueue(), m_assetManager->getGeometryCreator()); @@ -460,45 +448,11 @@ class UISampleApp final : public examples::SimpleWindowedApplication return false; } - // TOOD: we should be able to load position & orientation from json file, support multiple cameraz - const float32_t3 iPosition[CamerazCount] = { float32_t3{ -2.238f, 1.438f, -1.558f }, float32_t3{ -2.017f, 0.386f, 0.684f } }; - // order important for glm::quat, the ctor is GLM_FUNC_QUALIFIER GLM_CONSTEXPR qua::qua(T _w, T _x, T _y, T _z) but memory layout is x,y,z,w - const glm::quat iOrientation[CamerazCount] = { glm::quat(0.888f, 0.253f, 0.368f, -0.105f), glm::quat(0.55f, 0.047f, 0.830f, -0.072f) }; - - for (uint32_t i = 0u; i < cameraz.size(); ++i) - { - auto& camera = cameraz[i]; - - // lets use key map presets to update the controller - - camera = make_smart_refctd_ptr(iPosition[i], iOrientation[i]); - - // init keyboard map - camera->updateKeyboardMapping([&](auto& keys) - { - keys = camera->getKeyboardMappingPreset(); - }); - - // init mouse map - camera->updateMouseMapping([&](auto& keys) - { - keys = camera->getMouseMappingPreset(); - }); - - // init imguizmo map - camera->updateImguizmoMapping([&](auto& keys) - { - keys = camera->getImguizmoMappingPreset(); - }); - } - - // projections - projections = linear_projection_t::create(smart_refctd_ptr(cameraz.front())); - const auto dpyInfo = m_winMgr->getPrimaryDisplayInfo(); - for (uint32_t i = 0u; i < scenez.size(); ++i) + + for (uint32_t i = 0u; i < sceneControlBinding.size(); ++i) { - auto& scene = scenez[i]; + auto& scene = sceneControlBinding[i].scene; scene = CScene::create(smart_refctd_ptr(m_device), smart_refctd_ptr(m_logger), getGraphicsQueue(), smart_refctd_ptr(resources), dpyInfo.resX, dpyInfo.resY); if (!scene) @@ -715,8 +669,6 @@ class UISampleApp final : public examples::SimpleWindowedApplication return false; } - - // viewpoets here if (j.contains("viewports")) { for (const auto& jViewport : j["viewports"]) @@ -765,15 +717,19 @@ class UISampleApp final : public examples::SimpleWindowedApplication std::cerr << "Expected \"viewports\" keyword in JSON!" << std::endl; return false; } - } - const auto iAspectRatio = float(m_window->getWidth()) / float(m_window->getHeight()); - const auto iInvAspectRatio = float(m_window->getHeight()) / float(m_window->getWidth()); + if (m_planarProjections.size() < sceneControlBinding.size()) + { + // TODO, temporary assuming it + std::cerr << "Expected at least " << std::to_string(sceneControlBinding.size()) << " planars!" << std::endl; + return false; + } - for (uint32_t i = 0u; i < ProjectionsCount; ++i) - { - aspectRatio[i] = iAspectRatio; - invAspectRatio[i] = iInvAspectRatio; + for (uint32_t i = 0u; i < sceneControlBinding.size(); ++i) + { + auto& planar = sceneControlBinding[i].planar; + planar = smart_refctd_ptr(m_planarProjections[i]); + } } if (base_t::argv.size() >= 3 && argv[1] == "-timeout_seconds") @@ -790,12 +746,18 @@ class UISampleApp final : public examples::SimpleWindowedApplication descriptorInfo[nbl::ext::imgui::UI::FontAtlasTexId].info.image.imageLayout = IImage::LAYOUT::READ_ONLY_OPTIMAL; descriptorInfo[nbl::ext::imgui::UI::FontAtlasTexId].desc = core::smart_refctd_ptr(m_ui.manager->getFontAtlasView()); + writes[nbl::ext::imgui::UI::FontAtlasTexId].info = descriptorInfo.data() + nbl::ext::imgui::UI::FontAtlasTexId; - descriptorInfo[OfflineSceneFirstCameraTextureIx].info.image.imageLayout = IImage::LAYOUT::READ_ONLY_OPTIMAL; - descriptorInfo[OfflineSceneFirstCameraTextureIx].desc = scenez[0]->getColorAttachment(); + for (uint32_t i = 0; i < sceneControlBinding.size(); ++i) + { + const auto textureIx = i + 1u; - descriptorInfo[OfflineSceneSecondCameraTextureIx].info.image.imageLayout = IImage::LAYOUT::READ_ONLY_OPTIMAL; - descriptorInfo[OfflineSceneSecondCameraTextureIx].desc = scenez[1]->getColorAttachment(); + descriptorInfo[textureIx].info.image.imageLayout = IImage::LAYOUT::READ_ONLY_OPTIMAL; + descriptorInfo[textureIx].desc = sceneControlBinding[i].scene->getColorAttachment(); + + writes[textureIx].info = descriptorInfo.data() + textureIx; + writes[textureIx].info = descriptorInfo.data() + textureIx; + } for (uint32_t i = 0; i < descriptorInfo.size(); ++i) { @@ -804,9 +766,6 @@ class UISampleApp final : public examples::SimpleWindowedApplication writes[i].arrayElement = i; writes[i].count = 1u; } - writes[nbl::ext::imgui::UI::FontAtlasTexId].info = descriptorInfo.data() + nbl::ext::imgui::UI::FontAtlasTexId; - writes[OfflineSceneFirstCameraTextureIx].info = descriptorInfo.data() + OfflineSceneFirstCameraTextureIx; - writes[OfflineSceneSecondCameraTextureIx].info = descriptorInfo.data() + OfflineSceneSecondCameraTextureIx; return m_device->updateDescriptorSets(writes, {}); } @@ -869,10 +828,10 @@ class UISampleApp final : public examples::SimpleWindowedApplication }; if (useWindow) - for (auto scene : scenez) - renderOfflineScene(scene); + for (auto binding : sceneControlBinding) + renderOfflineScene(binding.scene); else - renderOfflineScene(scenez.front().get()); // just to not render to all at once + renderOfflineScene(sceneControlBinding.front().scene.get()); // just to not render to all at once const IGPUCommandBuffer::SClearColorValue clearValue = { .float32 = {0.f,0.f,0.f,1.f} }; const IGPUCommandBuffer::SRenderpassBeginInfo info = { @@ -996,35 +955,21 @@ class UISampleApp final : public examples::SimpleWindowedApplication // (**) -> wait on offline framebuffers in window mode { - const nbl::video::ISemaphore::SWaitInfo waitInfos[] = - { - // wait for first camera scene view fb - { - .semaphore = scenez[0]->semaphore.progress.get(), - .value = scenez[0]->semaphore.finishedValue - }, - // and second one too - { - .semaphore = scenez[1]->semaphore.progress.get(), - .value = scenez[1]->semaphore.finishedValue - }, - }; - if (useWindow) { m_device->blockForSemaphores(std::to_array ( { - // wait for first camera scene view fb + // wait for first planar scene view fb { - .semaphore = scenez[0]->semaphore.progress.get(), - .value = scenez[0]->semaphore.finishedValue + .semaphore = sceneControlBinding[0].scene->semaphore.progress.get(), + .value = sceneControlBinding[0].scene->semaphore.finishedValue }, // and second one too { - .semaphore = scenez[1]->semaphore.progress.get(), - .value = scenez[1]->semaphore.finishedValue - }, + .semaphore = sceneControlBinding[1].scene->semaphore.progress.get(), + .value = sceneControlBinding[1].scene->semaphore.finishedValue + } } )); } @@ -1033,10 +978,10 @@ class UISampleApp final : public examples::SimpleWindowedApplication m_device->blockForSemaphores(std::to_array ( { - // wait for first only, we use it temporary for FS render mode + // wait for picked planar only { - .semaphore = scenez.front()->semaphore.progress.get(), - .value = scenez.front()->semaphore.finishedValue + .semaphore = sceneControlBinding[activePlanarIx].scene->semaphore.progress.get(), + .value = sceneControlBinding[activePlanarIx].scene->semaphore.finishedValue } } )); @@ -1128,7 +1073,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication if (enableActiveCameraMovement) { - auto& camera = cameraz[activeCameraIndex]; + auto* camera = m_planarProjections[activePlanarIx]->getCamera(); static std::vector virtualEvents(0x45); uint32_t vCount; @@ -1156,7 +1101,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGuizmo::BeginFrame(); - auto projectionMatrices = projections->getLinearProjections(); + //auto projectionMatrices = projections->getLinearProjections(); { /* @@ -1231,22 +1176,6 @@ class UISampleApp final : public examples::SimpleWindowedApplication * */ - static struct - { - float32_t4x4 view[CamerazCount], projection[ProjectionsCount], inModel[2u], outModel[2u], outDeltaTRS[2u]; - } imguizmoM16InOut; - - const auto& firstcamera = cameraz.front(); - const auto& secondcamera = cameraz.back(); - - ImGuizmo::SetID(0u); - - imguizmoM16InOut.view[0u] = getCastedMatrix(transpose(getMatrix3x4As4x4(firstcamera->getGimbal().getViewMatrix()))); - imguizmoM16InOut.view[1u] = getCastedMatrix(transpose(getMatrix3x4As4x4(secondcamera->getGimbal().getViewMatrix()))); - - for(uint32_t i = 0u; i < ProjectionsCount; ++i) - imguizmoM16InOut.projection[i] = getCastedMatrix(transpose(projectionMatrices[i].getProjectionMatrix())); - // TODO: need to inspect where I'm wrong, workaround auto gimbalToImguizmoTRS = [&](const float32_t3x4& nblGimbalTrs) -> float32_t4x4 { @@ -1260,26 +1189,30 @@ class UISampleApp final : public examples::SimpleWindowedApplication return trs; }; + + + ImGuizmo::SetID(0u); + + /* + + imguizmoM16InOut.view[0u] = getCastedMatrix(transpose(getMatrix3x4As4x4(firstcamera->getGimbal().getViewMatrix()))); + imguizmoM16InOut.view[1u] = getCastedMatrix(transpose(getMatrix3x4As4x4(secondcamera->getGimbal().getViewMatrix()))); + + for (uint32_t i = 0u; i < ProjectionsCount; ++i) + imguizmoM16InOut.projection[i] = getCastedMatrix(transpose(projectionMatrices[i].getProjectionMatrix())); + const auto secondCameraGimbalModel = secondcamera->getGimbal()(); // we will transform a scene object's model imguizmoM16InOut.inModel[0] = transpose(getMatrix3x4As4x4(m_model)); // and second camera's model too imguizmoM16InOut.inModel[1] = gimbalToImguizmoTRS(getCastedMatrix(secondCameraGimbalModel)); - { - if (flipGizmoY) // note we allow to flip gizmo just to match our coordinates - for(uint32_t i = 0u; i < ProjectionsCount; ++i) - imguizmoM16InOut.projection[i][1][1] *= -1.f; // https://johannesugb.github.io/gpu-programming/why-do-opengl-proj-matrices-fail-in-vulkan/ - - ImGuizmo::AllowAxisFlip(false); - if(enableActiveCameraMovement) - ImGuizmo::Enable(false); - else - ImGuizmo::Enable(true); + */ - aspectRatio[0] = io.DisplaySize.x / io.DisplaySize.y; - invAspectRatio[0] = io.DisplaySize.y / io.DisplaySize.x; + { + ImGuizmo::AllowAxisFlip(false); + ImGuizmo::Enable(false); SImResourceInfo info; info.samplerIx = (uint16_t)nbl::ext::imgui::UI::DefaultSamplerIx::USER; @@ -1287,158 +1220,259 @@ class UISampleApp final : public examples::SimpleWindowedApplication // render camera views onto GUIs if (useWindow) { - // ImGuizmo manipulations on the last used model matrix in window mode - IGimbalController::input_imguizmo_event_t deltaTRS; - TransformEditor(&imguizmoM16InOut.inModel[lastManipulatedModelIx][0][0], &deltaTRS); // TODO! DELTA TRS IS ONLY TRANSLATE TEMPORARY + // the only reason for those is to remind we must go with transpose & 4x4 matrices + struct ImGuizmoPlanarM16InOut + { + float32_t4x4 view, projection; + }; - if (isCameraModelBound) + struct ImGuizmoModelM16InOut { - { - static std::vector virtualEvents(0x45); + float32_t4x4 inTRS, outTRS, outDeltaTRS; + }; + + //////////////////////////////////////////////////////////////////////////// + // ABS TRS control with editor, only single object can be manipulated at time + { + ImGuizmoModelM16InOut imguizmoModel; + + if (boundCameraToManipulate) + imguizmoModel.inTRS = gimbalToImguizmoTRS(getCastedMatrix(boundCameraToManipulate->getGimbal()())); + else + imguizmoModel.inTRS = transpose(getMatrix3x4As4x4(m_model)); - uint32_t vCount; + imguizmoModel.outTRS = imguizmoModel.inTRS; - secondcamera->beginInputProcessing(m_nextPresentationTimestamp); + // TODO! DELTA TRS IS ONLY TRANSLATE TEMPORARY + TransformEditor(&imguizmoModel.outTRS[0][0], &imguizmoModel.outDeltaTRS); + + // generate virtual events given delta TRS matrix + if (boundCameraToManipulate) + { { - secondcamera->process(nullptr, vCount); + static std::vector virtualEvents(0x45); - if (virtualEvents.size() < vCount) - virtualEvents.resize(vCount); + uint32_t vCount; - IGimbalController::SUpdateParameters params; - params.imguizmoEvents = { { deltaTRS } }; - secondcamera->process(virtualEvents.data(), vCount, params); - } - secondcamera->endInputProcessing(); + boundCameraToManipulate->beginInputProcessing(m_nextPresentationTimestamp); + { + boundCameraToManipulate->process(nullptr, vCount); - // I start to think controller should be able to set sensitivity to scale magnitudes of generated events - // in order for camera to not keep any magnitude scalars like move or rotation speed scales - smart_refctd_ptr_static_cast(secondcamera)->setMoveSpeedScale(1); - smart_refctd_ptr_static_cast(secondcamera)->setRotationSpeedScale(1); + if (virtualEvents.size() < vCount) + virtualEvents.resize(vCount); - // NOTE: generated events from ImGuizmo controller are always in world space! - if(vCount) - secondcamera->manipulate({ virtualEvents.data(), vCount }, ICamera::World); - } - } + IGimbalController::SUpdateParameters params; + params.imguizmoEvents = { { imguizmoModel.outDeltaTRS } }; + boundCameraToManipulate->process(virtualEvents.data(), vCount, params); + } + boundCameraToManipulate->endInputProcessing(); - uint32_t gizmoIx = {}; - bool manipulatedFromAnyWindow = false; + // I start to think controller should be able to set sensitivity to scale magnitudes of generated events + // in order for camera to not keep any magnitude scalars like move or rotation speed scales - // we have 2 GUI windows we render into with FBOs - for (uint32_t windowIndex = 0; windowIndex < 2u; ++windowIndex) - { - const auto& cameraIx = windowIndex; // tmp bound & hardcoded, we will extend it later - const auto projectionIx = cameraIx + 1u; // offset because first projection belongs to full screen (**) - info.textureID = projectionIx; + auto* fps = dynamic_cast(boundCameraToManipulate.get()); - if(isPerspective[projectionIx]) - ImGuizmo::SetOrthographic(false); - else - ImGuizmo::SetOrthographic(true); + if (fps) + { + smart_refctd_ptr_static_cast(boundCameraToManipulate)->setMoveSpeedScale(1); + smart_refctd_ptr_static_cast(boundCameraToManipulate)->setRotationSpeedScale(1); + } - if (areAxesFlipped[projectionIx]) - ImGuizmo::AllowAxisFlip(true); + // NOTE: generated events from ImGuizmo controller are always in world space! + if (vCount) + boundCameraToManipulate->manipulate({ virtualEvents.data(), vCount }, ICamera::World); + } + } else - ImGuizmo::AllowAxisFlip(false); + { + // for scene demo model full affine transformation without limits is assumed + m_model = float32_t3x4(transpose(imguizmoModel.outTRS)); + } + } + //////////////////////////////////////////////////////////////////////////// + for (uint32_t windowIx = 0; windowIx < sceneControlBinding.size(); ++windowIx) + { + // setup imgui window ImGui::SetNextWindowSize(ImVec2(800, 400), ImGuiCond_Appearing); - ImGui::SetNextWindowPos(ImVec2(400, 20 + cameraIx * 420), ImGuiCond_Appearing); + ImGui::SetNextWindowPos(ImVec2(400, 20 + windowIx * 420), ImGuiCond_Appearing); ImGui::PushStyleColor(ImGuiCol_WindowBg, (ImVec4)ImColor(0.35f, 0.3f, 0.3f)); - std::string ident = "Camera \"" + std::to_string(cameraIx) + "\" View"; + const std::string ident = "Render Window \"" + std::to_string(windowIx) + "\""; ImGui::Begin(ident.data(), 0, ImGuiWindowFlags_NoMove); - ImGuizmo::SetDrawlist(); + const ImVec2 contentRegionSize = ImGui::GetContentRegionAvail(), windowPos = ImGui::GetCursorScreenPos(), cursorPos = ImGui::GetWindowPos(); + + // setup bound entities for the window like camera & projections + auto& binding = sceneControlBinding[windowIx]; + binding.aspectRatio = contentRegionSize.x / contentRegionSize.y; + auto* planarViewCameraBound = binding.planar->getCamera(); + assert(planarViewCameraBound); + + auto& projection = binding.planar->getPlanarProjections()[binding.boundProjectionIx]; - ImVec2 contentRegionSize, windowPos, cursorPos; - contentRegionSize = ImGui::GetContentRegionAvail(); - cursorPos = ImGui::GetCursorScreenPos(); - windowPos = ImGui::GetWindowPos(); + // first 0th texture is for UI texture atlas, then there are our window textures + auto fboImguiTextureID = windowIx + 1u; + info.textureID = fboImguiTextureID; - // (**) - aspectRatio[projectionIx] = contentRegionSize.x / contentRegionSize.y; - invAspectRatio[projectionIx] = contentRegionSize.y / contentRegionSize.x; + if(binding.allowGizmoAxesToFlip) + ImGuizmo::AllowAxisFlip(true); + else + ImGuizmo::AllowAxisFlip(false); + + if(projection.getParameters().m_type == IPlanarProjection::CProjection::Orthographic) + ImGuizmo::SetOrthographic(true); + else + ImGuizmo::SetOrthographic(false); + + // set imguizmo draw lists + ImGuizmo::SetDrawlist(); ImGui::Image(info, contentRegionSize); ImGuizmo::SetRect(cursorPos.x, cursorPos.y, contentRegionSize.x, contentRegionSize.y); + // I will assume we need to focus a window to start manipulating objects from it if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) - lastProjectionIx = projectionIx; + activePlanarIx = windowIx; - // but only 2 objects with matrices we will try to manipulate & we can do this from both windows! - // however note it only makes sense to obsly assume we cannot manipulate 2 objects at the same time - for (uint32_t modelIx = 0; modelIx < 2; modelIx++) - { - const bool isCameraGizmoBound = gizmoIx == 1; - const bool discard = isCameraGizmoBound && mCurrentGizmoOperation != ImGuizmo::TRANSLATE; + if (windowIx == activePlanarIx && not enableActiveCameraMovement) + ImGuizmo::Enable(true); - // future logic will need to filter gizmos which represents gimbal of camera which view matrix we use to render scene with - if (gizmoIx == 3) - continue; + ImGuizmoPlanarM16InOut imguizmoPlanar; + imguizmoPlanar.view = getCastedMatrix(transpose(getMatrix3x4As4x4(planarViewCameraBound->getGimbal().getViewMatrix()))); + imguizmoPlanar.projection = getCastedMatrix(transpose(projection.getProjectionMatrix())); - // and because of imguizmo API usage to achieve it we must work on copies & filter the output (unless we try gizmo enable/disable) - // -> in general we need to be careful to not edit the same model twice - - auto model = imguizmoM16InOut.inModel[modelIx]; - float32_t4x4 deltaOutputTRS; + if (flipGizmoY) // note we allow to flip gizmo just to match our coordinates + imguizmoPlanar.projection[1][1] *= -1.f; // https://johannesugb.github.io/gpu-programming/why-do-opengl-proj-matrices-fail-in-vulkan/ - // note we also need to take care of unique gizmo IDs, we have in total 4 gizmos even though we only want to manipulate 2 objects in total - ImGuizmo::PushID(gizmoIx); + for (uint32_t modelIx = 0; modelIx < 1u + m_planarProjections.size(); modelIx++) + { + const bool isCameraGimbalTarget = modelIx; // I assume scene demo model is 0th ix then cameras + ICamera* const targetGimbalManipulationCamera = isCameraGimbalTarget ? m_planarProjections[modelIx - 1u]->getCamera() : nullptr; + const bool discard = isCameraGimbalTarget && mCurrentGizmoOperation != ImGuizmo::TRANSLATE; // discard WiP stuff - ImGuiIO& io = ImGui::GetIO(); + // if we try to manipulate a camera which appears to be the same camera we see scene from then obvsly it doesn't make sense to manipulate its gizmo so we skip it + if (targetGimbalManipulationCamera == planarViewCameraBound) + continue; - const bool success = ImGuizmo::Manipulate(&imguizmoM16InOut.view[cameraIx][0][0], &imguizmoM16InOut.projection[projectionIx][0][0], mCurrentGizmoOperation, mCurrentGizmoMode, &model[0][0], &deltaOutputTRS[0][0], useSnap ? &snap[0] : nullptr); + ImGuizmoModelM16InOut imguizmoModel; - // we manipulated a gizmo from a X-th window, now we update output matrices and assume no more gizmos can be manipulated at the same frame - if (!manipulatedFromAnyWindow) + if (isCameraGimbalTarget) { - // TMP WIP, our imguizmo controller doesnt support rotation & scale yet and its because - // - there are numerical issues with imguizmo decompose/recompose TRS (and the author also says it) - // - in rotate mode delta TRS matrix contains translate part (how? no idea, maybe imguizmo bug) and it glitches everything - - if (!discard) - { - imguizmoM16InOut.outModel[modelIx] = model; - imguizmoM16InOut.outDeltaTRS[modelIx] = deltaOutputTRS; - } + assert(targetGimbalManipulationCamera); + imguizmoModel.inTRS = gimbalToImguizmoTRS(getCastedMatrix(targetGimbalManipulationCamera->getGimbal()())); } + else + imguizmoModel.inTRS = transpose(getMatrix3x4As4x4(m_model)); - if (success) - manipulatedFromAnyWindow = true; - - if (ImGuizmo::IsUsing()) + imguizmoModel.outTRS = imguizmoModel.inTRS; + + ImGuizmo::PushID(modelIx); { - lastManipulatedModelIx = modelIx; - lastManipulatedGizmoIx = gizmoIx; - isCameraModelBound = lastManipulatedModelIx == 1u; + const bool success = ImGuizmo::Manipulate(&imguizmoPlanar.view[0][0], &imguizmoPlanar.projection[0][0], mCurrentGizmoOperation, mCurrentGizmoMode, &imguizmoModel.outTRS[0][0], &imguizmoModel.outDeltaTRS[0][0], useSnap ? &snap[0] : nullptr); - lastManipulatedModelIdentifier = isCameraModelBound ? "Camera FPS" : "Geometry Creator Object"; - } + if (success) + { + // TMP WIP, our imguizmo controller doesnt support rotation & scale yet and its because + // - there are numerical issues with imguizmo decompose/recompose TRS (and the author also says it) + // - in rotate mode delta TRS matrix contains translate part (how? no idea, maybe imguizmo bug) and it glitches everything - if (ImGuizmo::IsOver()) - { - ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.2f, 0.2f, 0.2f, 0.8f)); - ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); - ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.5f); + if (!discard) + { + if (targetGimbalManipulationCamera) + { + boundCameraToManipulate = smart_refctd_ptr(targetGimbalManipulationCamera); + + /* + testing imguizmo controller for target camera, we use delta world imguizmo TRS matrix to generate virtual events + */ + + { + static std::vector virtualEvents(0x45); + + if (ImGuizmo::IsUsingAny()) + { + uint32_t vCount; + + targetGimbalManipulationCamera->beginInputProcessing(m_nextPresentationTimestamp); + { + targetGimbalManipulationCamera->process(nullptr, vCount); + + if (virtualEvents.size() < vCount) + virtualEvents.resize(vCount); + + IGimbalController::SUpdateParameters params; + params.imguizmoEvents = { { imguizmoModel.outTRS } }; + targetGimbalManipulationCamera->process(virtualEvents.data(), vCount, params); + } + targetGimbalManipulationCamera->endInputProcessing(); + + auto* fps = dynamic_cast(targetGimbalManipulationCamera); + + float pMoveSpeed = 1.f, pRotationSpeed = 1.f; + + if (fps) + { + pMoveSpeed = fps->getMoveSpeedScale(); + pRotationSpeed = fps->getRotationSpeedScale(); + + // I start to think controller should be able to set sensitivity to scale magnitudes of generated events + // in order for camera to not keep any magnitude scalars like move or rotation speed scales + + fps->setMoveSpeedScale(1); + fps->setRotationSpeedScale(1); + } + + // NOTE: generated events from ImGuizmo controller are always in world space! + targetGimbalManipulationCamera->manipulate({ virtualEvents.data(), vCount }, ICamera::World); + + if (fps) + { + fps->setMoveSpeedScale(pMoveSpeed); + fps->setRotationSpeedScale(pRotationSpeed); + } + } + } + } + else + { + // again, for scene demo model full affine transformation without limits is assumed + m_model = float32_t3x4(transpose(imguizmoModel.outTRS)); + boundCameraToManipulate = nullptr; + } + } + } + + if (ImGuizmo::IsOver()) + { + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.2f, 0.2f, 0.2f, 0.8f)); + ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.5f); - ImVec2 mousePos = io.MousePos; - ImGui::SetNextWindowPos(ImVec2(mousePos.x + 10, mousePos.y + 10), ImGuiCond_Always); + ImGuiIO& io = ImGui::GetIO(); + ImVec2 mousePos = io.MousePos; + ImGui::SetNextWindowPos(ImVec2(mousePos.x + 10, mousePos.y + 10), ImGuiCond_Always); - ImGui::Begin("InfoOverlay", nullptr, - ImGuiWindowFlags_NoDecoration | - ImGuiWindowFlags_AlwaysAutoResize | - ImGuiWindowFlags_NoSavedSettings); + ImGui::Begin("InfoOverlay", nullptr, + ImGuiWindowFlags_NoDecoration | + ImGuiWindowFlags_AlwaysAutoResize | + ImGuiWindowFlags_NoSavedSettings); - ImGui::Text("Identifier: %s", lastManipulatedModelIdentifier.c_str()); - ImGui::Text("Object Ix: %u", modelIx); + std::string ident; - ImGui::End(); + if (targetGimbalManipulationCamera) + ident = targetGimbalManipulationCamera->getIdentifier(); + else + ident = "Geometry Creator Object"; - ImGui::PopStyleVar(); - ImGui::PopStyleColor(2); - } + ImGui::Text("Identifier: %s", ident.c_str()); + ImGui::Text("Object Ix: %u", modelIx); + ImGui::End(); + + ImGui::PopStyleVar(); + ImGui::PopStyleColor(2); + } + } ImGuizmo::PopID(); - ++gizmoIx; } ImGui::End(); @@ -1448,18 +1482,13 @@ class UISampleApp final : public examples::SimpleWindowedApplication // render selected camera view onto full screen else { - info.textureID = OfflineSceneFirstCameraTextureIx;; - lastProjectionIx = 0; + info.textureID = 1u + activePlanarIx; 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, windowPos, cursorPos; - contentRegionSize = ImGui::GetContentRegionAvail(); - cursorPos = ImGui::GetCursorScreenPos(); - windowPos = ImGui::GetWindowPos(); + const ImVec2 contentRegionSize = ImGui::GetContentRegionAvail(), windowPos = ImGui::GetCursorScreenPos(), cursorPos = ImGui::GetWindowPos(); ImGui::Image(info, contentRegionSize); ImGuizmo::SetRect(cursorPos.x, cursorPos.y, contentRegionSize.x, contentRegionSize.y); @@ -1469,96 +1498,133 @@ class UISampleApp final : public examples::SimpleWindowedApplication } } - /* - testing imguizmo controller for second camera, we use delta world imguizmo TRS matrix to generate virtual events - */ - + // to Nabla + update camera & model matrices { - static std::vector virtualEvents(0x45); - - if (ImGuizmo::IsUsingAny()) + const auto& references = resources->objects; + const auto type = static_cast(gcIndex); + const auto& [gpu, meta] = references[type]; + + for (uint32_t i = 0u; i < sceneControlBinding.size(); ++i) { - uint32_t vCount; + auto& binding = sceneControlBinding[i]; + auto& hook = binding.scene->object; - secondcamera->beginInputProcessing(m_nextPresentationTimestamp); + auto* boundPlanarCamera = binding.planar->getCamera(); + + hook.meta.type = type; + hook.meta.name = meta.name; { - secondcamera->process(nullptr, vCount); + float32_t3x4 viewMatrix, modelViewMatrix, normalMatrix; + float32_t4x4 modelViewProjectionMatrix; - if (virtualEvents.size() < vCount) - virtualEvents.resize(vCount); + viewMatrix = getCastedMatrix(boundPlanarCamera->getGimbal().getViewMatrix()); + modelViewMatrix = concatenateBFollowedByA(viewMatrix, m_model); - IGimbalController::SUpdateParameters params; - params.imguizmoEvents = { { imguizmoM16InOut.outDeltaTRS[1u] } }; - secondcamera->process(virtualEvents.data(), vCount, params); - } - secondcamera->endInputProcessing(); + auto& projection = binding.planar->getPlanarProjections()[binding.boundProjectionIx]; - // I start to think controller should be able to set sensitivity to scale magnitudes of generated events - // in order for camera to not keep any magnitude scalars like move or rotation speed scales - smart_refctd_ptr_static_cast(secondcamera)->setMoveSpeedScale(1); - smart_refctd_ptr_static_cast(secondcamera)->setRotationSpeedScale(1); + auto concatMatrix = mul(getCastedMatrix(projection.getProjectionMatrix()), getMatrix3x4As4x4(viewMatrix)); + modelViewProjectionMatrix = mul(concatMatrix, getMatrix3x4As4x4(m_model)); - // NOTE: generated events from ImGuizmo controller are always in world space! - secondcamera->manipulate({ virtualEvents.data(), vCount }, ICamera::World); + memcpy(hook.viewParameters.MVP, &modelViewProjectionMatrix[0][0], sizeof(hook.viewParameters.MVP)); + memcpy(hook.viewParameters.MV, &modelViewMatrix[0][0], sizeof(hook.viewParameters.MV)); + memcpy(hook.viewParameters.NormalMat, &normalMatrix[0][0], sizeof(hook.viewParameters.NormalMat)); + } } } - for (uint32_t i = 0u; i < cameraz.size(); ++i) + // Planars { - auto& camera = cameraz[i]; - smart_refctd_ptr_static_cast(camera)->setMoveSpeedScale(moveSpeed[i]); - smart_refctd_ptr_static_cast(camera)->setRotationSpeedScale(rotateSpeed[i]); - } + ImGui::Begin("Projection"); - // update scenes data - // to Nabla + update camera & model matrices - - // TODO: make it more nicely once view manipulate supported - //const_cast(firstcamera->getGimbal().getViewMatrix()) = float64_t3x4(getCastedMatrix(transpose(imguizmoM16InOut.view[0u]))); // a hack for "view manipulate", 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) - { - m_model = float32_t3x4(transpose(imguizmoM16InOut.outModel[0])); + ImGui::Checkbox("Window mode##useWindow", &useWindow); - auto firstCameraView = getCastedMatrix(firstcamera->getGimbal().getViewMatrix()); - auto secondCameraView = getCastedMatrix(secondcamera->getGimbal().getViewMatrix()); + ImGui::Separator(); - const float32_t3x4* views[] = + const auto activePlanarIxString = std::to_string(activePlanarIx); + ImGui::Text("Active Planar Ix: %s", activePlanarIxString.c_str()); + + auto& active = sceneControlBinding[activePlanarIx]; + const auto& params = active.planar->getPlanarProjections()[active.boundProjectionIx].getParameters(); + auto selectedProjectionType = params.m_type; { - &firstCameraView, - &secondCameraView - }; + const char* labels[] = { "Perspective", "Orthographic" }; + int type = static_cast(params.m_type); - const auto& references = resources->objects; - const auto type = static_cast(gcIndex); - const auto& [gpu, meta] = references[type]; + if (ImGui::Combo("Projection Type", &type, labels, IM_ARRAYSIZE(labels))) + selectedProjectionType = static_cast(type); + } - for (uint32_t i = 0u; i < cameraz.size(); ++i) + auto getPresetName = [&](auto ix) -> std::string { - auto& scene = scenez[i]; - auto& hook = scene->object; + switch (selectedProjectionType) + { + case IPlanarProjection::CProjection::Perspective: return "Perspective Projection Preset " + std::to_string(ix); + case IPlanarProjection::CProjection::Orthographic: return "Orthographic Projection Preset " + std::to_string(ix); + default: return "Unknown Projection Preset " + std::to_string(ix); + } + }; - hook.meta.type = type; - hook.meta.name = meta.name; + if (ImGui::BeginCombo("Projection Preset", getPresetName(active.boundProjectionIx).c_str())) + { + auto& projections = active.planar->getPlanarProjections(); + + for (uint32_t i = 0; i < projections.size(); ++i) { - float32_t3x4 modelView, normal; - float32_t4x4 modelViewProjection; + const auto& projection = projections[i]; + const auto& params = projection.getParameters(); - const auto& viewMatrix = *views[useWindow ? i : activeCameraIndex]; - modelView = concatenateBFollowedByA(viewMatrix, m_model); + if (params.m_type != selectedProjectionType) + continue; - // TODO - //modelView.getSub3x3InverseTranspose(normal); + bool isSelected = (i == active.boundProjectionIx); - auto concatMatrix = mul(getCastedMatrix(projectionMatrices[useWindow ? i + 1u : 0u].getProjectionMatrix()), getMatrix3x4As4x4(viewMatrix)); - modelViewProjection = mul(concatMatrix, getMatrix3x4As4x4(m_model)); + if (ImGui::Selectable(getPresetName(i).c_str(), isSelected)) + active.boundProjectionIx = i; - memcpy(hook.viewParameters.MVP, &modelViewProjection[0][0], sizeof(hook.viewParameters.MVP)); - memcpy(hook.viewParameters.MV, &modelView[0][0], sizeof(hook.viewParameters.MV)); - memcpy(hook.viewParameters.NormalMat, &normal[0][0], sizeof(hook.viewParameters.NormalMat)); + if (isSelected) + ImGui::SetItemDefaultFocus(); } + ImGui::EndCombo(); + } + + auto* const boundCamera = active.planar->getCamera(); + auto& boundProjection = active.planar->getPlanarProjections()[active.boundProjectionIx]; + assert(not boundProjection.isProjectionSingular()); + + auto updateParameters = boundProjection.getParameters(); + bool isLH = boundProjection.isProjectionLeftHanded().value(); + + if (useWindow) + ImGui::Checkbox("Allow axes to flip##allowAxesToFlip", &active.allowGizmoAxesToFlip); + + if (ImGui::RadioButton("LH", boundProjection.isProjectionLeftHanded().value())) + isLH = true; + + ImGui::SameLine(); + + if (ImGui::RadioButton("RH", not isLH)) + isLH = false; + + ImGui::SliderFloat("zNear", &updateParameters.m_zNear, 0.1f, 100.f, "%.2f", ImGuiSliderFlags_Logarithmic); + ImGui::SliderFloat("zFar", &updateParameters.m_zFar, 110.f, 10000.f, "%.1f", ImGuiSliderFlags_Logarithmic); + + switch (selectedProjectionType) + { + case IPlanarProjection::CProjection::Perspective: + { + ImGui::SliderFloat("Fov", &updateParameters.m_planar.perspective.fov, 20.f, 150.f, "%.1f", ImGuiSliderFlags_Logarithmic); + boundProjection.setPerspective(isLH, updateParameters.m_zNear, updateParameters.m_zFar, updateParameters.m_planar.perspective.fov, active.aspectRatio); + } break; + + case IPlanarProjection::CProjection::Orthographic: + { + ImGui::SliderFloat("Ortho width", &updateParameters.m_planar.orthographic.orthoWidth, 1.f, 20.f, "%.1f", ImGuiSliderFlags_Logarithmic); + boundProjection.setOrthographic(isLH, updateParameters.m_zNear, updateParameters.m_zFar, updateParameters.m_planar.orthographic.orthoWidth, active.aspectRatio); + } break; + + default: break; } - } - { { ImGuiIO& io = ImGui::GetIO(); @@ -1578,33 +1644,10 @@ class UISampleApp final : public examples::SimpleWindowedApplication io.WantCaptureMouse = true; } - ImGui::Begin("Cameras", nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysVerticalScrollbar); - ImGui::Checkbox("Window mode##useWindow", &useWindow); - ImGui::Text("Select Active Camera:"); - ImGui::Separator(); - - if (ImGui::BeginCombo("Active Camera", ("Camera " + std::to_string(activeCameraIndex)).c_str())) - { - for (uint32_t cameraIndex = 0; cameraIndex < CamerazCount; ++cameraIndex) - { - bool isSelected = (cameraIndex == activeCameraIndex); - std::string comboLabel = "Camera " + std::to_string(cameraIndex); - - if (ImGui::Selectable(comboLabel.c_str(), isSelected)) - activeCameraIndex = cameraIndex; - - if (isSelected) - ImGui::SetItemDefaultFocus(); - } - ImGui::EndCombo(); - } - - ImGui::Separator(); - if (enableActiveCameraMovement) - ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), "Active Camera Movement: Enabled"); + ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), "Bound Camera Movement: Enabled"); else - ImGui::TextColored(ImVec4(1.0f, 0.0f, 0.0f, 1.0f), "Active Camera Movement: Disabled"); + ImGui::TextColored(ImVec4(1.0f, 0.0f, 0.0f, 1.0f), "Bound Camera Movement: Disabled"); if (ImGui::IsItemHovered()) { @@ -1620,7 +1663,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings); - ImGui::Text("Press 'Space' to Enable/Disable selected camera movement"); + ImGui::Text("Press 'Space' to Enable/Disable bound planar camera movement"); ImGui::End(); @@ -1630,87 +1673,59 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGui::Separator(); - for (size_t cameraIndex = 0; cameraIndex < CamerazCount; ++cameraIndex) + const auto flags = ImGuiTreeNodeFlags_DefaultOpen; + if (ImGui::TreeNodeEx("Bound Camera", flags)) { - auto& camera = cameraz[cameraIndex]; - if (!camera) - continue; - - const auto flags = (activeCameraIndex == cameraIndex) ? ImGuiTreeNodeFlags_DefaultOpen : ImGuiTreeNodeFlags_None; - std::string treeNodeLabel = "Camera " + std::to_string(cameraIndex); - - if (ImGui::TreeNodeEx(treeNodeLabel.c_str(), flags)) { - ImGui::SliderFloat("Move speed factor", &moveSpeed[cameraIndex], 0.0001f, 10.f, "%.4f", ImGuiSliderFlags_Logarithmic); - ImGui::SliderFloat("Rotate speed factor", &rotateSpeed[cameraIndex], 0.0001f, 10.f, "%.4f", ImGuiSliderFlags_Logarithmic); + auto* fps = dynamic_cast(boundCamera); - if (ImGui::TreeNodeEx("Data", ImGuiTreeNodeFlags_None)) + if (fps) { - auto& gimbal = camera->getGimbal(); - const auto position = getCastedVector(gimbal.getPosition()); - const auto& orientation = gimbal.getOrientation(); - const auto viewMatrix = getCastedMatrix(gimbal.getViewMatrix()); - - ImGui::Text("Type: %s", camera->getIdentifier().data()); - ImGui::Separator(); - addMatrixTable("Position", ("PositionTable_" + std::to_string(cameraIndex)).c_str(), 1, 3, &position[0], false); - addMatrixTable("Orientation (Quaternion)", ("OrientationTable_" + std::to_string(cameraIndex)).c_str(), 1, 4, &orientation[0], false); - addMatrixTable("View Matrix", ("ViewMatrixTable_" + std::to_string(cameraIndex)).c_str(), 3, 4, &viewMatrix[0][0], false); - ImGui::TreePop(); - } + float moveSpeed = fps->getMoveSpeedScale(); + float rotationSpeed = fps->getRotationSpeedScale(); - if (ImGui::TreeNodeEx("Virtual Event Mappings", ImGuiTreeNodeFlags_DefaultOpen)) - { - displayKeyMappingsAndVirtualStatesInline(camera.get()); - ImGui::TreePop(); + ImGui::SliderFloat("Move speed factor", &moveSpeed, 0.0001f, 10.f, "%.4f", ImGuiSliderFlags_Logarithmic); + ImGui::SliderFloat("Rotate speed factor", &rotationSpeed, 0.0001f, 10.f, "%.4f", ImGuiSliderFlags_Logarithmic); + + fps->setMoveSpeedScale(moveSpeed); + fps->setRotationSpeedScale(rotationSpeed); } + } + if (ImGui::TreeNodeEx("World Data", ImGuiTreeNodeFlags_None)) + { + auto& gimbal = boundCamera->getGimbal(); + const auto position = getCastedVector(gimbal.getPosition()); + const auto& orientation = gimbal.getOrientation(); + const auto viewMatrix = getCastedMatrix(gimbal.getViewMatrix()); + + ImGui::Text("Type: %s", boundCamera->getIdentifier().data()); + ImGui::Separator(); + addMatrixTable("Position", ("PositionTable_" + activePlanarIxString).c_str(), 1, 3, &position[0], false); + addMatrixTable("Orientation (Quaternion)", ("OrientationTable_" + activePlanarIxString).c_str(), 1, 4, &orientation[0], false); + addMatrixTable("View Matrix", ("ViewMatrixTable_" + activePlanarIxString).c_str(), 3, 4, &viewMatrix[0][0], false); ImGui::TreePop(); } - } - - ImGui::End(); - } - } - - // Projections - { - ImGui::Begin("Projection"); - - ImGui::Text("Ix: %s", std::to_string(lastProjectionIx).c_str()); - - if (ImGui::RadioButton("Perspective", isPerspective[lastProjectionIx])) - isPerspective[lastProjectionIx] = true; - - ImGui::SameLine(); - if (ImGui::RadioButton("Orthographic", !isPerspective[lastProjectionIx])) - isPerspective[lastProjectionIx] = false; - - if (ImGui::RadioButton("LH", isLH[lastProjectionIx])) - isLH[lastProjectionIx] = true; - - ImGui::SameLine(); - - if (ImGui::RadioButton("RH", !isLH[lastProjectionIx])) - isLH[lastProjectionIx] = false; - - if(useWindow) - ImGui::Checkbox("Allow axes to flip##allowAxesToFlip", areAxesFlipped.data() + lastProjectionIx); + if (ImGui::TreeNodeEx("Virtual Event Mappings", flags)) + { + auto* encoder = static_cast(&boundProjection); + displayKeyMappingsAndVirtualStatesInline(encoder); + ImGui::TreePop(); + } - if (isPerspective[lastProjectionIx]) - ImGui::SliderFloat("Fov", &fov[lastProjectionIx], 20.f, 150.f, "%.1f", ImGuiSliderFlags_Logarithmic); - else - ImGui::SliderFloat("Ortho width", &viewWidth[lastProjectionIx], 1.f, 20.f, "%.1f", ImGuiSliderFlags_Logarithmic); + ImGui::TreePop(); + } - ImGui::SliderFloat("zNear", &zNear[lastProjectionIx], 0.1f, 100.f, "%.2f", ImGuiSliderFlags_Logarithmic); - ImGui::SliderFloat("zFar", &zFar[lastProjectionIx], 110.f, 10000.f, "%.1f", ImGuiSliderFlags_Logarithmic); + boundCamera->updateKeyboardMapping([&](auto& map) { map = boundProjection.getKeyboardVirtualEventMap(); }); + boundCamera->updateMouseMapping([&](auto& map) { map = boundProjection.getMouseVirtualEventMap(); }); + } ImGui::End(); } } - inline void TransformEditor(float* matrix, IGimbalController::input_imguizmo_event_t* deltaTRS = nullptr) + inline void TransformEditor(float* m16TRSmatrix, IGimbalController::input_imguizmo_event_t* deltaTRS = nullptr) { static float bounds[] = { -0.5f, -0.5f, -0.5f, 0.5f, 0.5f, 0.5f }; static float boundsSnap[] = { 0.1f, 0.1f, 0.1f }; @@ -1728,14 +1743,16 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGui::Text("Using gizmo"); ImGui::Separator(); - ImGui::Text("Object Ix: \"%s\"", std::to_string(lastManipulatedModelIx).c_str()); - ImGui::Separator(); + std::string indent; + if (boundCameraToManipulate) + indent = boundCameraToManipulate->getIdentifier(); + else + indent = "Geometry Creator Object"; - ImGui::Text("Identifier: \"%s\"", lastManipulatedModelIdentifier.c_str()); + ImGui::Text("Identifier: \"%s\"", indent.c_str()); ImGui::Separator(); - - if (!isCameraModelBound) + if (!boundCameraToManipulate) { static const char* gcObjectTypeNames[] = { "Cube", @@ -1763,8 +1780,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication } } - - addMatrixTable("Model (TRS) Matrix", "ModelMatrixTable", 4, 4, matrix); + addMatrixTable("Model (TRS) Matrix", "ModelMatrixTable", 4, 4, m16TRSmatrix); if (ImGui::RadioButton("Translate", mCurrentGizmoOperation == ImGuizmo::TRANSLATE)) mCurrentGizmoOperation = ImGuizmo::TRANSLATE; @@ -1782,14 +1798,14 @@ class UISampleApp final : public examples::SimpleWindowedApplication if (deltaTRS) *deltaTRS = IGimbalController::input_imguizmo_event_t(1); - ImGuizmo::DecomposeMatrixToComponents(matrix, &matrixTranslation[0], &matrixRotation[0], &matrixScale[0]); - decomposed = *reinterpret_cast(matrix); + ImGuizmo::DecomposeMatrixToComponents(m16TRSmatrix, &matrixTranslation[0], &matrixRotation[0], &matrixScale[0]); + decomposed = *reinterpret_cast(m16TRSmatrix); { ImGuiInputTextFlags flags = 0; ImGui::InputFloat3("Tr", &matrixTranslation[0], "%.3f", flags); - if (isCameraModelBound) // TODO: cameras are WiP here, imguizmo controller only works with translate manipulation + abs are banned currently + if (boundCameraToManipulate) // TODO: cameras are WiP here, imguizmo controller only works with translate manipulation + abs are banned currently { ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(1.0f, 0.0f, 0.0f, 0.5f)); flags |= ImGuiInputTextFlags_ReadOnly; @@ -1798,11 +1814,11 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGui::InputFloat3("Rt", &matrixRotation[0], "%.3f", flags); ImGui::InputFloat3("Sc", &matrixScale[0], "%.3f", flags); - if(isCameraModelBound) + if(boundCameraToManipulate) ImGui::PopStyleColor(); } - ImGuizmo::RecomposeMatrixFromComponents(&matrixTranslation[0], &matrixRotation[0], &matrixScale[0], matrix); - recomposed = *reinterpret_cast(matrix); + ImGuizmo::RecomposeMatrixFromComponents(&matrixTranslation[0], &matrixRotation[0], &matrixScale[0], m16TRSmatrix); + recomposed = *reinterpret_cast(m16TRSmatrix); // TODO AND NOTE: I only take care of translate part temporary! if(deltaTRS) @@ -1895,9 +1911,6 @@ class UISampleApp final : public examples::SimpleWindowedApplication //! next presentation timestamp std::chrono::microseconds m_nextPresentationTimestamp = {}; - // UI font atlas; first camera fb, second camera fb - constexpr static inline auto TotalUISampleTexturesAmount = 3u; - core::smart_refctd_ptr m_descriptorSetPool; struct CRenderUI @@ -1912,30 +1925,40 @@ class UISampleApp final : public examples::SimpleWindowedApplication core::smart_refctd_ptr descriptorSet; }; - // one model object in the world, testing 2 cameraz for which view is rendered to separate frame buffers (so what they see) with new controller API including imguizmo + // one model object in the world, testing multiuple cameraz for which view is rendered to separate frame buffers (so what they see) with new controller API including imguizmo nbl::hlsl::float32_t3x4 m_model = nbl::hlsl::float32_t3x4(1.f); - static constexpr inline auto CamerazCount = 2u; - std::array, CamerazCount> scenez; - std::array, CamerazCount> cameraz; - uint32_t activeCameraIndex = 0; + nbl::core::smart_refctd_ptr boundCameraToManipulate; + + using planar_projections_range_t = std::vector; + using planar_projection_t = CPlanarProjection; + std::vector> m_planarProjections; + bool enableActiveCameraMovement = false; + + struct SceneControlBinding + { + nbl::core::smart_refctd_ptr scene; + nbl::core::smart_refctd_ptr planar; + uint32_t boundProjectionIx = 0u; + bool allowGizmoAxesToFlip = false; + float aspectRatio = 16.f / 9.f; + }; + + static constexpr inline auto MaxSceneFBOs = 2u; + std::array sceneControlBinding; + uint32_t activePlanarIx = 0u; + + // UI font atlas + viewport FBO color attachment textures + constexpr static inline auto TotalUISampleTexturesAmount = 1u + MaxSceneFBOs; + nbl::core::smart_refctd_ptr resources; CRenderUI m_ui; video::CDumbPresentationOracle oracle; - 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 - - static constexpr inline auto ProjectionsCount = 3u; // full screen, first & second GUI windows - using linear_projections_range_t = std::array; - using linear_projection_t = CLinearProjection; - nbl::core::smart_refctd_ptr projections; + uint16_t gcIndex = {}; const bool flipGizmoY = true; - std::array isPerspective = { true, true, true }, isLH = { true, true, true }, areAxesFlipped = { false, false, false }; - std::array fov = { 60.f, 60.f, 60.f }, zNear = { 0.1f, 0.1f, 0.1f }, zFar = { 10000.f, 10000.f, 10000.f }, viewWidth = { 10.f, 10.f, 10.f }, aspectRatio = {}, invAspectRatio = {}; - std::array moveSpeed = { 0.01, 0.01 }, rotateSpeed = { 0.003, 0.003 }; - bool isCameraModelBound = false; float camYAngle = 165.f / 180.f * 3.14159f; float camXAngle = 32.f / 180.f * 3.14159f; float camDistance = 8.f; @@ -1943,14 +1966,8 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGuizmo::OPERATION mCurrentGizmoOperation = ImGuizmo::TRANSLATE; ImGuizmo::MODE mCurrentGizmoMode = ImGuizmo::LOCAL; float snap[3] = { 1.f, 1.f, 1.f }; - int lastManipulatedModelIx = 0, lastManipulatedGizmoIx = 0, lastProjectionIx = 0; - std::string lastManipulatedModelIdentifier = "Geometry Creator Object"; bool firstFrame = true; - - using planar_projections_range_t = std::vector; - using planar_projection_t = CPlanarProjection; - std::vector> m_planarProjections; }; NBL_MAIN_FUNC(UISampleApp) \ No newline at end of file diff --git a/common/include/camera/CFPSCamera.hpp b/common/include/camera/CFPSCamera.hpp index a6d169f09..6efc91f95 100644 --- a/common/include/camera/CFPSCamera.hpp +++ b/common/include/camera/CFPSCamera.hpp @@ -83,7 +83,7 @@ class CFPSCamera final : public ICamera // (***) inline void setMoveSpeedScale(double scalar) - { + { m_moveSpeedScale = scalar; } @@ -93,6 +93,9 @@ class CFPSCamera final : public ICamera m_rotationSpeedScale = scalar; } + inline double getMoveSpeedScale() const { return m_moveSpeedScale; } + inline double getRotationSpeedScale() const { return m_rotationSpeedScale; } + private: typename base_t::CGimbal m_gimbal; From d98c10b5cf38b3d7958be2bd17489515dfde32c9 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Thu, 19 Dec 2024 14:35:00 +0100 Subject: [PATCH 064/205] use `struct CPlanarProjectionWithMeta : public CPlanarProjection` in the demo with bound & last projection IXes, fix picking projection presets --- 61_UI/main.cpp | 87 ++++++++++++++++++--- common/include/camera/CPlanarProjection.hpp | 2 +- 2 files changed, 75 insertions(+), 14 deletions(-) diff --git a/61_UI/main.cpp b/61_UI/main.cpp index 109f24bdd..1dc69874c 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -11,6 +11,24 @@ using json = nlohmann::json; #include "camera/CCubeProjection.hpp" #include "glm/glm/ext/matrix_clip_space.hpp" // TODO: TESTING +using planar_projections_range_t = std::vector; +struct CPlanarProjectionWithMeta : public CPlanarProjection +{ + using base_t = CPlanarProjection; + + inline static core::smart_refctd_ptr create(core::smart_refctd_ptr&& camera) + { + if (!camera) return nullptr; + return core::smart_refctd_ptr(new CPlanarProjectionWithMeta(core::smart_refctd_ptr(camera)), core::dont_grab); + } + + std::optional boundProjectionIx = std::nullopt, lastBoundPerspectivePresetProjectionIx = std::nullopt, lastBoundOrthoPresetProjectionIx = std::nullopt; +private: + CPlanarProjectionWithMeta(core::smart_refctd_ptr&& camera) + : base_t::CPlanarProjection(core::smart_refctd_ptr(camera)) {} +}; +using planar_projection_t = CPlanarProjectionWithMeta; + constexpr IGPUImage::SSubresourceRange TripleBufferUsedSubresourceRange = { .aspectMask = IGPUImage::EAF_COLOR_BIT, @@ -681,6 +699,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication const auto cameraIx = jViewport["camera"].get(); auto& planars = m_planarProjections.emplace_back() = planar_projection_t::create(smart_refctd_ptr(cameras[cameraIx])); + planars->boundProjectionIx = 0u; // I will assume I bind first by default if (!jViewport.contains("planarControllerSet")) { @@ -1305,9 +1324,11 @@ class UISampleApp final : public examples::SimpleWindowedApplication auto& binding = sceneControlBinding[windowIx]; binding.aspectRatio = contentRegionSize.x / contentRegionSize.y; auto* planarViewCameraBound = binding.planar->getCamera(); - assert(planarViewCameraBound); - auto& projection = binding.planar->getPlanarProjections()[binding.boundProjectionIx]; + assert(planarViewCameraBound); + assert(binding.planar->boundProjectionIx.has_value()); + + auto& projection = binding.planar->getPlanarProjections()[binding.planar->boundProjectionIx.value()]; // first 0th texture is for UI texture atlas, then there are our window textures auto fboImguiTextureID = windowIx + 1u; @@ -1520,7 +1541,8 @@ class UISampleApp final : public examples::SimpleWindowedApplication viewMatrix = getCastedMatrix(boundPlanarCamera->getGimbal().getViewMatrix()); modelViewMatrix = concatenateBFollowedByA(viewMatrix, m_model); - auto& projection = binding.planar->getPlanarProjections()[binding.boundProjectionIx]; + assert(binding.planar->boundProjectionIx.has_value()); + auto& projection = binding.planar->getPlanarProjections()[binding.planar->boundProjectionIx.value()]; auto concatMatrix = mul(getCastedMatrix(projection.getProjectionMatrix()), getMatrix3x4As4x4(viewMatrix)); modelViewProjectionMatrix = mul(concatMatrix, getMatrix3x4As4x4(m_model)); @@ -1544,14 +1566,47 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGui::Text("Active Planar Ix: %s", activePlanarIxString.c_str()); auto& active = sceneControlBinding[activePlanarIx]; - const auto& params = active.planar->getPlanarProjections()[active.boundProjectionIx].getParameters(); - auto selectedProjectionType = params.m_type; + assert(active.planar->boundProjectionIx.has_value()); + + auto requestPlanarCacheInit = [&](std::optional& optionalPresetIx, IPlanarProjection::CProjection::ProjectionType requestedType) -> void + { + if (not optionalPresetIx.has_value()) + { + const auto& projections = active.planar->getPlanarProjections(); + + for (uint32_t i = 0u; i < projections.size(); ++i) + { + const auto& params = projections[i].getParameters(); + if (params.m_type == requestedType) + { + optionalPresetIx = i; + break; + } + } + } + + assert(optionalPresetIx.has_value()); + }; + + requestPlanarCacheInit(active.planar->lastBoundPerspectivePresetProjectionIx, IPlanarProjection::CProjection::Perspective); + requestPlanarCacheInit(active.planar->lastBoundOrthoPresetProjectionIx, IPlanarProjection::CProjection::Orthographic); + + auto selectedProjectionType = active.planar->getPlanarProjections()[active.planar->boundProjectionIx.value()].getParameters().m_type; { const char* labels[] = { "Perspective", "Orthographic" }; - int type = static_cast(params.m_type); + int type = static_cast(selectedProjectionType); if (ImGui::Combo("Projection Type", &type, labels, IM_ARRAYSIZE(labels))) + { selectedProjectionType = static_cast(type); + + switch (selectedProjectionType) + { + case IPlanarProjection::CProjection::Perspective: active.planar->boundProjectionIx = active.planar->lastBoundPerspectivePresetProjectionIx.value(); break; + case IPlanarProjection::CProjection::Orthographic: active.planar->boundProjectionIx = active.planar->lastBoundOrthoPresetProjectionIx.value(); break; + default: active.planar->boundProjectionIx = std::nullopt; assert(false); break; + } + } } auto getPresetName = [&](auto ix) -> std::string @@ -1564,7 +1619,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication } }; - if (ImGui::BeginCombo("Projection Preset", getPresetName(active.boundProjectionIx).c_str())) + if (ImGui::BeginCombo("Projection Preset", getPresetName(active.planar->boundProjectionIx.value()).c_str())) { auto& projections = active.planar->getPlanarProjections(); @@ -1576,10 +1631,19 @@ class UISampleApp final : public examples::SimpleWindowedApplication if (params.m_type != selectedProjectionType) continue; - bool isSelected = (i == active.boundProjectionIx); + bool isSelected = (i == active.planar->boundProjectionIx.value()); if (ImGui::Selectable(getPresetName(i).c_str(), isSelected)) - active.boundProjectionIx = i; + { + active.planar->boundProjectionIx = i; + + switch (selectedProjectionType) + { + case IPlanarProjection::CProjection::Perspective: active.planar->lastBoundPerspectivePresetProjectionIx = active.planar->boundProjectionIx.value(); break; + case IPlanarProjection::CProjection::Orthographic: active.planar->lastBoundOrthoPresetProjectionIx = active.planar->boundProjectionIx.value(); break; + default: assert(false); break; + } + } if (isSelected) ImGui::SetItemDefaultFocus(); @@ -1588,7 +1652,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication } auto* const boundCamera = active.planar->getCamera(); - auto& boundProjection = active.planar->getPlanarProjections()[active.boundProjectionIx]; + auto& boundProjection = active.planar->getPlanarProjections()[active.planar->boundProjectionIx.value()]; assert(not boundProjection.isProjectionSingular()); auto updateParameters = boundProjection.getParameters(); @@ -1929,8 +1993,6 @@ class UISampleApp final : public examples::SimpleWindowedApplication nbl::hlsl::float32_t3x4 m_model = nbl::hlsl::float32_t3x4(1.f); nbl::core::smart_refctd_ptr boundCameraToManipulate; - using planar_projections_range_t = std::vector; - using planar_projection_t = CPlanarProjection; std::vector> m_planarProjections; bool enableActiveCameraMovement = false; @@ -1939,7 +2001,6 @@ class UISampleApp final : public examples::SimpleWindowedApplication { nbl::core::smart_refctd_ptr scene; nbl::core::smart_refctd_ptr planar; - uint32_t boundProjectionIx = 0u; bool allowGizmoAxesToFlip = false; float aspectRatio = 16.f / 9.f; }; diff --git a/common/include/camera/CPlanarProjection.hpp b/common/include/camera/CPlanarProjection.hpp index f726ea959..a85cc39de 100644 --- a/common/include/camera/CPlanarProjection.hpp +++ b/common/include/camera/CPlanarProjection.hpp @@ -30,7 +30,7 @@ namespace nbl::hlsl return m_projections; } - private: + protected: CPlanarProjection(core::smart_refctd_ptr&& camera) : IPlanarProjection(core::smart_refctd_ptr(camera)) {} From 263e60b4c1ed159e35eb0c65c2d1a956aaf41d4a Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Thu, 19 Dec 2024 16:37:01 +0100 Subject: [PATCH 065/205] fix gizmo ID stack usage --- 61_UI/main.cpp | 120 ++++++------------------------------------------- 1 file changed, 13 insertions(+), 107 deletions(-) diff --git a/61_UI/main.cpp b/61_UI/main.cpp index 1dc69874c..cbff869cc 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -1119,81 +1119,6 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGuiIO& io = ImGui::GetIO(); ImGuizmo::BeginFrame(); - - //auto projectionMatrices = projections->getLinearProjections(); - { - - /* - TODO: update it - - auto mutableRange = smart_refctd_ptr_static_cast(projections)->getLinearProjections(); - for (uint32_t i = 0u; i < mutableRange.size(); ++i) - { - auto projection = mutableRange.begin() + i; - - if (isPerspective[i]) - { - if (isLH[i]) - projection->setProjectionMatrix(buildProjectionMatrixPerspectiveFovLH(glm::radians(fov[i]), aspectRatio[i], zNear[i], zFar[i])); - else - projection->setProjectionMatrix(buildProjectionMatrixPerspectiveFovRH(glm::radians(fov[i]), aspectRatio[i], zNear[i], zFar[i])); - } - else - { - float viewHeight = viewWidth[i] * invAspectRatio[i]; - - if (isLH[i]) - projection->setProjectionMatrix(buildProjectionMatrixOrthoLH(viewWidth[i], viewHeight, zNear[i], zFar[i])); - else - projection->setProjectionMatrix(buildProjectionMatrixOrthoRH(viewWidth[i], viewHeight, zNear[i], zFar[i])); - } - } - */ - - - } - - /* - * 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() - - * - */ // TODO: need to inspect where I'm wrong, workaround auto gimbalToImguizmoTRS = [&](const float32_t3x4& nblGimbalTrs) -> float32_t4x4 @@ -1208,31 +1133,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication return trs; }; - - - ImGuizmo::SetID(0u); - - /* - - imguizmoM16InOut.view[0u] = getCastedMatrix(transpose(getMatrix3x4As4x4(firstcamera->getGimbal().getViewMatrix()))); - imguizmoM16InOut.view[1u] = getCastedMatrix(transpose(getMatrix3x4As4x4(secondcamera->getGimbal().getViewMatrix()))); - - for (uint32_t i = 0u; i < ProjectionsCount; ++i) - imguizmoM16InOut.projection[i] = getCastedMatrix(transpose(projectionMatrices[i].getProjectionMatrix())); - - const auto secondCameraGimbalModel = secondcamera->getGimbal()(); - - // we will transform a scene object's model - imguizmoM16InOut.inModel[0] = transpose(getMatrix3x4As4x4(m_model)); - // and second camera's model too - imguizmoM16InOut.inModel[1] = gimbalToImguizmoTRS(getCastedMatrix(secondCameraGimbalModel)); - - */ - { - ImGuizmo::AllowAxisFlip(false); - ImGuizmo::Enable(false); - SImResourceInfo info; info.samplerIx = (uint16_t)nbl::ext::imgui::UI::DefaultSamplerIx::USER; @@ -1310,6 +1211,12 @@ class UISampleApp final : public examples::SimpleWindowedApplication } //////////////////////////////////////////////////////////////////////////// + if(enableActiveCameraMovement) + ImGuizmo::Enable(false); + else + ImGuizmo::Enable(true); + + size_t gizmoIx = {}; for (uint32_t windowIx = 0; windowIx < sceneControlBinding.size(); ++windowIx) { // setup imgui window @@ -1344,18 +1251,14 @@ class UISampleApp final : public examples::SimpleWindowedApplication else ImGuizmo::SetOrthographic(false); - // set imguizmo draw lists - ImGuizmo::SetDrawlist(); - ImGui::Image(info, contentRegionSize); ImGuizmo::SetRect(cursorPos.x, cursorPos.y, contentRegionSize.x, contentRegionSize.y); // I will assume we need to focus a window to start manipulating objects from it if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) activePlanarIx = windowIx; - - if (windowIx == activePlanarIx && not enableActiveCameraMovement) - ImGuizmo::Enable(true); + + ImGuizmo::SetDrawlist(); ImGuizmoPlanarM16InOut imguizmoPlanar; imguizmoPlanar.view = getCastedMatrix(transpose(getMatrix3x4As4x4(planarViewCameraBound->getGimbal().getViewMatrix()))); @@ -1366,13 +1269,18 @@ class UISampleApp final : public examples::SimpleWindowedApplication for (uint32_t modelIx = 0; modelIx < 1u + m_planarProjections.size(); modelIx++) { + ImGuizmo::PushID(gizmoIx); ++gizmoIx; + const bool isCameraGimbalTarget = modelIx; // I assume scene demo model is 0th ix then cameras ICamera* const targetGimbalManipulationCamera = isCameraGimbalTarget ? m_planarProjections[modelIx - 1u]->getCamera() : nullptr; const bool discard = isCameraGimbalTarget && mCurrentGizmoOperation != ImGuizmo::TRANSLATE; // discard WiP stuff // if we try to manipulate a camera which appears to be the same camera we see scene from then obvsly it doesn't make sense to manipulate its gizmo so we skip it if (targetGimbalManipulationCamera == planarViewCameraBound) + { + ImGuizmo::PopID(); continue; + } ImGuizmoModelM16InOut imguizmoModel; @@ -1385,8 +1293,6 @@ class UISampleApp final : public examples::SimpleWindowedApplication imguizmoModel.inTRS = transpose(getMatrix3x4As4x4(m_model)); imguizmoModel.outTRS = imguizmoModel.inTRS; - - ImGuizmo::PushID(modelIx); { const bool success = ImGuizmo::Manipulate(&imguizmoPlanar.view[0][0], &imguizmoPlanar.projection[0][0], mCurrentGizmoOperation, mCurrentGizmoMode, &imguizmoModel.outTRS[0][0], &imguizmoModel.outDeltaTRS[0][0], useSnap ? &snap[0] : nullptr); From 4a66fecb3ac4f678fe0a94bd35681e9f4cdf3a63 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Fri, 20 Dec 2024 10:42:06 +0100 Subject: [PATCH 066/205] set render window constraints to avoid aspect ratio glitches & division by 0, fix object-to-gizmo visual alignment in a window & camera gizmo manipulation controller bugs after API updates --- 61_UI/main.cpp | 97 +++++++++++++++++----------- common/include/camera/CFPSCamera.hpp | 9 ++- 2 files changed, 66 insertions(+), 40 deletions(-) diff --git a/61_UI/main.cpp b/61_UI/main.cpp index cbff869cc..091af65e9 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -526,7 +526,11 @@ class UISampleApp final : public examples::SimpleWindowedApplication auto orientation = [&]() { auto jret = jCamera["orientation"].get>(); - return glm::quat(jret[0], jret[1], jret[2], jret[3]); + + // order important for glm::quat, + // the ctor is GLM_FUNC_QUALIFIER GLM_CONSTEXPR qua::qua(T _w, T _x, T _y, T _z) + // but memory layout (and json) is x,y,z,w + return glm::quat(jret[3], jret[0], jret[1], jret[2]); }(); cameras.emplace_back() = make_smart_refctd_ptr(position, orientation); @@ -1172,7 +1176,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication { static std::vector virtualEvents(0x45); - uint32_t vCount; + uint32_t vCount = {}; boundCameraToManipulate->beginInputProcessing(m_nextPresentationTimestamp); { @@ -1192,15 +1196,27 @@ class UISampleApp final : public examples::SimpleWindowedApplication auto* fps = dynamic_cast(boundCameraToManipulate.get()); + float pmSpeed = 1.f; + float prSpeed = 1.f; + if (fps) { - smart_refctd_ptr_static_cast(boundCameraToManipulate)->setMoveSpeedScale(1); - smart_refctd_ptr_static_cast(boundCameraToManipulate)->setRotationSpeedScale(1); + float pmSpeed = fps->getMoveSpeedScale(); + float prSpeed = fps->getRotationSpeedScale(); + + fps->setMoveSpeedScale(1); + fps->setRotationSpeedScale(1); } // NOTE: generated events from ImGuizmo controller are always in world space! if (vCount) boundCameraToManipulate->manipulate({ virtualEvents.data(), vCount }, ICamera::World); + + if (fps) + { + fps->setMoveSpeedScale(pmSpeed); + fps->setRotationSpeedScale(prSpeed); + } } } else @@ -1217,15 +1233,18 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGuizmo::Enable(true); size_t gizmoIx = {}; + size_t manipulationCounter = {}; for (uint32_t windowIx = 0; windowIx < sceneControlBinding.size(); ++windowIx) { // setup imgui window ImGui::SetNextWindowSize(ImVec2(800, 400), ImGuiCond_Appearing); ImGui::SetNextWindowPos(ImVec2(400, 20 + windowIx * 420), ImGuiCond_Appearing); + ImGui::SetNextWindowSizeConstraints(ImVec2(0x45, 0x45), ImVec2(7680, 4320)); + ImGui::PushStyleColor(ImGuiCol_WindowBg, (ImVec4)ImColor(0.35f, 0.3f, 0.3f)); const std::string ident = "Render Window \"" + std::to_string(windowIx) + "\""; ImGui::Begin(ident.data(), 0, ImGuiWindowFlags_NoMove); - const ImVec2 contentRegionSize = ImGui::GetContentRegionAvail(), windowPos = ImGui::GetCursorScreenPos(), cursorPos = ImGui::GetWindowPos(); + const ImVec2 contentRegionSize = ImGui::GetContentRegionAvail(), windowPos = ImGui::GetWindowPos(), cursorPos = ImGui::GetCursorScreenPos(); // setup bound entities for the window like camera & projections auto& binding = sceneControlBinding[windowIx]; @@ -1260,6 +1279,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGuizmo::SetDrawlist(); + // we render a scene from view of a camera bound to planar window ImGuizmoPlanarM16InOut imguizmoPlanar; imguizmoPlanar.view = getCastedMatrix(transpose(getMatrix3x4As4x4(planarViewCameraBound->getGimbal().getViewMatrix()))); imguizmoPlanar.projection = getCastedMatrix(transpose(projection.getProjectionMatrix())); @@ -1271,9 +1291,9 @@ class UISampleApp final : public examples::SimpleWindowedApplication { ImGuizmo::PushID(gizmoIx); ++gizmoIx; - const bool isCameraGimbalTarget = modelIx; // I assume scene demo model is 0th ix then cameras + const bool isCameraGimbalTarget = modelIx; // I assume scene demo model is 0th ix, left are cameras ICamera* const targetGimbalManipulationCamera = isCameraGimbalTarget ? m_planarProjections[modelIx - 1u]->getCamera() : nullptr; - const bool discard = isCameraGimbalTarget && mCurrentGizmoOperation != ImGuizmo::TRANSLATE; // discard WiP stuff + bool discard = isCameraGimbalTarget && mCurrentGizmoOperation != ImGuizmo::TRANSLATE; // discard WiP stuff // if we try to manipulate a camera which appears to be the same camera we see scene from then obvsly it doesn't make sense to manipulate its gizmo so we skip it if (targetGimbalManipulationCamera == planarViewCameraBound) @@ -1298,6 +1318,9 @@ class UISampleApp final : public examples::SimpleWindowedApplication if (success) { + ++manipulationCounter; + discard &= manipulationCounter > 1; + // TMP WIP, our imguizmo controller doesnt support rotation & scale yet and its because // - there are numerical issues with imguizmo decompose/recompose TRS (and the author also says it) // - in rotate mode delta TRS matrix contains translate part (how? no idea, maybe imguizmo bug) and it glitches everything @@ -1314,48 +1337,45 @@ class UISampleApp final : public examples::SimpleWindowedApplication { static std::vector virtualEvents(0x45); + uint32_t vCount = {}; - if (ImGuizmo::IsUsingAny()) + targetGimbalManipulationCamera->beginInputProcessing(m_nextPresentationTimestamp); { - uint32_t vCount; + targetGimbalManipulationCamera->process(nullptr, vCount); - targetGimbalManipulationCamera->beginInputProcessing(m_nextPresentationTimestamp); - { - targetGimbalManipulationCamera->process(nullptr, vCount); + if (virtualEvents.size() < vCount) + virtualEvents.resize(vCount); - if (virtualEvents.size() < vCount) - virtualEvents.resize(vCount); - - IGimbalController::SUpdateParameters params; - params.imguizmoEvents = { { imguizmoModel.outTRS } }; - targetGimbalManipulationCamera->process(virtualEvents.data(), vCount, params); - } - targetGimbalManipulationCamera->endInputProcessing(); + IGimbalController::SUpdateParameters params; + params.imguizmoEvents = { { imguizmoModel.outDeltaTRS } }; + targetGimbalManipulationCamera->process(virtualEvents.data(), vCount, params); + } + targetGimbalManipulationCamera->endInputProcessing(); - auto* fps = dynamic_cast(targetGimbalManipulationCamera); + auto* fps = dynamic_cast(targetGimbalManipulationCamera); - float pMoveSpeed = 1.f, pRotationSpeed = 1.f; + float pMoveSpeed = 1.f, pRotationSpeed = 1.f; - if (fps) - { - pMoveSpeed = fps->getMoveSpeedScale(); - pRotationSpeed = fps->getRotationSpeedScale(); + if (fps) + { + pMoveSpeed = fps->getMoveSpeedScale(); + pRotationSpeed = fps->getRotationSpeedScale(); - // I start to think controller should be able to set sensitivity to scale magnitudes of generated events - // in order for camera to not keep any magnitude scalars like move or rotation speed scales + // I start to think controller should be able to set sensitivity to scale magnitudes of generated events + // in order for camera to not keep any magnitude scalars like move or rotation speed scales - fps->setMoveSpeedScale(1); - fps->setRotationSpeedScale(1); - } + fps->setMoveSpeedScale(1); + fps->setRotationSpeedScale(1); + } - // NOTE: generated events from ImGuizmo controller are always in world space! + // NOTE: generated events from ImGuizmo controller are always in world space! + if (vCount) targetGimbalManipulationCamera->manipulate({ virtualEvents.data(), vCount }, ICamera::World); - if (fps) - { - fps->setMoveSpeedScale(pMoveSpeed); - fps->setRotationSpeedScale(pRotationSpeed); - } + if (fps) + { + fps->setMoveSpeedScale(pMoveSpeed); + fps->setRotationSpeedScale(pRotationSpeed); } } } @@ -1405,6 +1425,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGui::End(); ImGui::PopStyleColor(1); } + assert(manipulationCounter <= 1u); } // render selected camera view onto full screen else @@ -1415,7 +1436,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication 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); - const ImVec2 contentRegionSize = ImGui::GetContentRegionAvail(), windowPos = ImGui::GetCursorScreenPos(), cursorPos = ImGui::GetWindowPos(); + const ImVec2 contentRegionSize = ImGui::GetContentRegionAvail(), windowPos = ImGui::GetWindowPos(), cursorPos = ImGui::GetCursorScreenPos(); ImGui::Image(info, contentRegionSize); ImGuizmo::SetRect(cursorPos.x, cursorPos.y, contentRegionSize.x, contentRegionSize.y); diff --git a/common/include/camera/CFPSCamera.hpp b/common/include/camera/CFPSCamera.hpp index 6efc91f95..3889e5921 100644 --- a/common/include/camera/CFPSCamera.hpp +++ b/common/include/camera/CFPSCamera.hpp @@ -17,7 +17,12 @@ class CFPSCamera final : public ICamera using base_t = ICamera; CFPSCamera(const float64_t3& position, const glm::quat& orientation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f)) - : base_t(), m_gimbal({ .position = position, .orientation = orientation }) {} + : base_t(), m_gimbal({ .position = position, .orientation = orientation }) + { + updateKeyboardMapping([](auto& map) { map = m_keyboard_to_virtual_events_preset; }); + updateMouseMapping([](auto& map) { map = m_mouse_to_virtual_events_preset; }); + updateImguizmoMapping([](auto& map) { map = m_imguizmo_to_virtual_events_preset; }); + } ~CFPSCamera() = default; const base_t::keyboard_to_virtual_events_t getKeyboardMappingPreset() const override { return m_keyboard_to_virtual_events_preset; } @@ -101,7 +106,7 @@ class CFPSCamera final : public ICamera // (***) TODO: I need to think whether a camera should own this or controllers should be able // to set sensitivity to scale magnitudes of generated events we put into manipulate method - double m_moveSpeedScale = 1, m_rotationSpeedScale = 1; + double m_moveSpeedScale = 0.01, m_rotationSpeedScale = 0.003; static inline constexpr auto AllowedLocalVirtualEvents = CVirtualGimbalEvent::Translate | CVirtualGimbalEvent::TiltUp | CVirtualGimbalEvent::TiltDown | CVirtualGimbalEvent::PanRight | CVirtualGimbalEvent::PanLeft; static inline constexpr auto AllowedWorldVirtualEvents = AllowedLocalVirtualEvents; From 2f9c5f264f1420684f899df6e7a208352711732a Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Fri, 20 Dec 2024 16:50:23 +0100 Subject: [PATCH 067/205] control active render window by switching between bound planar & projection presets, add third camera to the scene - a few minor fixes need to be applied and we start adding new type of camera (Maya incoming) --- 61_UI/cameras.json | 26 ++++++- 61_UI/main.cpp | 185 +++++++++++++++++++++++++++------------------ 2 files changed, 138 insertions(+), 73 deletions(-) diff --git a/61_UI/cameras.json b/61_UI/cameras.json index bd4d7aaa9..105a3ec7e 100644 --- a/61_UI/cameras.json +++ b/61_UI/cameras.json @@ -3,12 +3,17 @@ { "type": "FPS", "position": [-2.238, 1.438, -1.558], - "orientation": [0.253, 0.368, -0.105, 0.888] + "orientation": [0.204, 0.385, -0.088, 0.896] }, { "type": "FPS", "position": [-2.017, 0.386, 0.684], "orientation": [0.047, 0.830, -0.072, 0.55] + }, + { + "type": "FPS", + "position": [2.116, 0.826, 1.152], + "orientation": [0.095, -0.835, 0.152, 0.521] } ], "projections": [ @@ -65,6 +70,25 @@ } } ] + }, + { + "camera": 2, + "planarControllerSet": [ + { + "projection": 0, + "controllers": { + "keyboard": 0, + "mouse": 0 + } + }, + { + "projection": 1, + "controllers": { + "keyboard": 1, + "mouse": 0 + } + } + ] } ], "controllers": { diff --git a/61_UI/main.cpp b/61_UI/main.cpp index 091af65e9..e0031333b 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -12,22 +12,7 @@ using json = nlohmann::json; #include "glm/glm/ext/matrix_clip_space.hpp" // TODO: TESTING using planar_projections_range_t = std::vector; -struct CPlanarProjectionWithMeta : public CPlanarProjection -{ - using base_t = CPlanarProjection; - - inline static core::smart_refctd_ptr create(core::smart_refctd_ptr&& camera) - { - if (!camera) return nullptr; - return core::smart_refctd_ptr(new CPlanarProjectionWithMeta(core::smart_refctd_ptr(camera)), core::dont_grab); - } - - std::optional boundProjectionIx = std::nullopt, lastBoundPerspectivePresetProjectionIx = std::nullopt, lastBoundOrthoPresetProjectionIx = std::nullopt; -private: - CPlanarProjectionWithMeta(core::smart_refctd_ptr&& camera) - : base_t::CPlanarProjection(core::smart_refctd_ptr(camera)) {} -}; -using planar_projection_t = CPlanarProjectionWithMeta; +using planar_projection_t = CPlanarProjection; constexpr IGPUImage::SSubresourceRange TripleBufferUsedSubresourceRange = { @@ -703,7 +688,6 @@ class UISampleApp final : public examples::SimpleWindowedApplication const auto cameraIx = jViewport["camera"].get(); auto& planars = m_planarProjections.emplace_back() = planar_projection_t::create(smart_refctd_ptr(cameras[cameraIx])); - planars->boundProjectionIx = 0u; // I will assume I bind first by default if (!jViewport.contains("planarControllerSet")) { @@ -743,15 +727,22 @@ class UISampleApp final : public examples::SimpleWindowedApplication if (m_planarProjections.size() < sceneControlBinding.size()) { - // TODO, temporary assuming it + // TODO, temporary assuming it, I'm not going to implement each possible case now std::cerr << "Expected at least " << std::to_string(sceneControlBinding.size()) << " planars!" << std::endl; return false; } for (uint32_t i = 0u; i < sceneControlBinding.size(); ++i) { - auto& planar = sceneControlBinding[i].planar; - planar = smart_refctd_ptr(m_planarProjections[i]); + auto& binding = sceneControlBinding[i]; + + const auto& projections = m_planarProjections[binding.activePlanarIx = 0]->getPlanarProjections(); + binding.pickDefaultProjections(projections); + + if (i) + binding.boundProjectionIx = binding.lastBoundOrthoPresetProjectionIx.value(); + else + binding.boundProjectionIx = binding.lastBoundPerspectivePresetProjectionIx.value(); } } @@ -1003,8 +994,8 @@ class UISampleApp final : public examples::SimpleWindowedApplication { // wait for picked planar only { - .semaphore = sceneControlBinding[activePlanarIx].scene->semaphore.progress.get(), - .value = sceneControlBinding[activePlanarIx].scene->semaphore.finishedValue + .semaphore = sceneControlBinding[activeRenderWindowIx].scene->semaphore.progress.get(), + .value = sceneControlBinding[activeRenderWindowIx].scene->semaphore.finishedValue } } )); @@ -1096,7 +1087,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication if (enableActiveCameraMovement) { - auto* camera = m_planarProjections[activePlanarIx]->getCamera(); + auto* camera = m_planarProjections[sceneControlBinding[activeRenderWindowIx].activePlanarIx]->getCamera(); static std::vector virtualEvents(0x45); uint32_t vCount; @@ -1111,7 +1102,9 @@ class UISampleApp final : public examples::SimpleWindowedApplication camera->process(virtualEvents.data(), vCount, { params.keyboardEvents, params.mouseEvents }); } camera->endInputProcessing(); - camera->manipulate({ virtualEvents.data(), vCount }, ICamera::Local); + + if(vCount) + camera->manipulate({ virtualEvents.data(), vCount }, ICamera::Local); } m_ui.manager->update(params); @@ -1248,13 +1241,16 @@ class UISampleApp final : public examples::SimpleWindowedApplication // setup bound entities for the window like camera & projections auto& binding = sceneControlBinding[windowIx]; + auto& planarBound = m_planarProjections[binding.activePlanarIx]; + assert(planarBound); + binding.aspectRatio = contentRegionSize.x / contentRegionSize.y; - auto* planarViewCameraBound = binding.planar->getCamera(); + auto* planarViewCameraBound = planarBound->getCamera(); assert(planarViewCameraBound); - assert(binding.planar->boundProjectionIx.has_value()); + assert(binding.boundProjectionIx.has_value()); - auto& projection = binding.planar->getPlanarProjections()[binding.planar->boundProjectionIx.value()]; + auto& projection = planarBound->getPlanarProjections()[binding.boundProjectionIx.value()]; // first 0th texture is for UI texture atlas, then there are our window textures auto fboImguiTextureID = windowIx + 1u; @@ -1270,14 +1266,13 @@ class UISampleApp final : public examples::SimpleWindowedApplication else ImGuizmo::SetOrthographic(false); + ImGuizmo::SetDrawlist(); ImGui::Image(info, contentRegionSize); ImGuizmo::SetRect(cursorPos.x, cursorPos.y, contentRegionSize.x, contentRegionSize.y); // I will assume we need to focus a window to start manipulating objects from it if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) - activePlanarIx = windowIx; - - ImGuizmo::SetDrawlist(); + activeRenderWindowIx = windowIx; // we render a scene from view of a camera bound to planar window ImGuizmoPlanarM16InOut imguizmoPlanar; @@ -1287,6 +1282,17 @@ class UISampleApp final : public examples::SimpleWindowedApplication if (flipGizmoY) // note we allow to flip gizmo just to match our coordinates imguizmoPlanar.projection[1][1] *= -1.f; // https://johannesugb.github.io/gpu-programming/why-do-opengl-proj-matrices-fail-in-vulkan/ + static constexpr float identityMatrix[] = + { + 1.f, 0.f, 0.f, 0.f, + 0.f, 1.f, 0.f, 0.f, + 0.f, 0.f, 1.f, 0.f, + 0.f, 0.f, 0.f, 1.f + }; + + if(binding.enableDebugGridDraw) + ImGuizmo::DrawGrid(&imguizmoPlanar.view[0][0], &imguizmoPlanar.projection[0][0], identityMatrix, 100.f); + for (uint32_t modelIx = 0; modelIx < 1u + m_planarProjections.size(); modelIx++) { ImGuizmo::PushID(gizmoIx); ++gizmoIx; @@ -1430,7 +1436,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication // render selected camera view onto full screen else { - info.textureID = 1u + activePlanarIx; + info.textureID = 1u + activeRenderWindowIx; ImGui::SetNextWindowPos(ImVec2(0, 0)); ImGui::SetNextWindowSize(io.DisplaySize); @@ -1457,7 +1463,9 @@ class UISampleApp final : public examples::SimpleWindowedApplication auto& binding = sceneControlBinding[i]; auto& hook = binding.scene->object; - auto* boundPlanarCamera = binding.planar->getCamera(); + auto& planarBound = m_planarProjections[binding.activePlanarIx]; + assert(planarBound); + auto* boundPlanarCamera = planarBound->getCamera(); hook.meta.type = type; hook.meta.name = meta.name; @@ -1468,8 +1476,8 @@ class UISampleApp final : public examples::SimpleWindowedApplication viewMatrix = getCastedMatrix(boundPlanarCamera->getGimbal().getViewMatrix()); modelViewMatrix = concatenateBFollowedByA(viewMatrix, m_model); - assert(binding.planar->boundProjectionIx.has_value()); - auto& projection = binding.planar->getPlanarProjections()[binding.planar->boundProjectionIx.value()]; + assert(binding.boundProjectionIx.has_value()); + auto& projection = planarBound->getPlanarProjections()[binding.boundProjectionIx.value()]; auto concatMatrix = mul(getCastedMatrix(projection.getProjectionMatrix()), getMatrix3x4As4x4(viewMatrix)); modelViewProjectionMatrix = mul(concatMatrix, getMatrix3x4As4x4(m_model)); @@ -1484,41 +1492,44 @@ class UISampleApp final : public examples::SimpleWindowedApplication // Planars { ImGui::Begin("Projection"); - ImGui::Checkbox("Window mode##useWindow", &useWindow); - ImGui::Separator(); - const auto activePlanarIxString = std::to_string(activePlanarIx); - ImGui::Text("Active Planar Ix: %s", activePlanarIxString.c_str()); - - auto& active = sceneControlBinding[activePlanarIx]; - assert(active.planar->boundProjectionIx.has_value()); + const auto activeRenderWindowIxString = std::to_string(activeRenderWindowIx); - auto requestPlanarCacheInit = [&](std::optional& optionalPresetIx, IPlanarProjection::CProjection::ProjectionType requestedType) -> void + ImGui::Text("Active Render Window: %s", activeRenderWindowIxString.c_str()); { - if (not optionalPresetIx.has_value()) - { - const auto& projections = active.planar->getPlanarProjections(); + const size_t planarsCount = m_planarProjections.size(); + assert(planarsCount); - for (uint32_t i = 0u; i < projections.size(); ++i) - { - const auto& params = projections[i].getParameters(); - if (params.m_type == requestedType) - { - optionalPresetIx = i; - break; - } - } + std::vector sbels(planarsCount); + for (size_t i = 0; i < planarsCount; ++i) + sbels[i] = "Planar " + std::to_string(i); + + std::vector labels(planarsCount); + for (size_t i = 0; i < planarsCount; ++i) + labels[i] = sbels[i].c_str(); + + auto& active = sceneControlBinding[activeRenderWindowIx]; + int currentPlanarIx = static_cast(active.activePlanarIx); + if (ImGui::Combo("Active Planar", ¤tPlanarIx, labels.data(), static_cast(labels.size()))) + { + active.activePlanarIx = static_cast(currentPlanarIx); + active.pickDefaultProjections(m_planarProjections[active.activePlanarIx]->getPlanarProjections()); } + } - assert(optionalPresetIx.has_value()); - }; + auto& active = sceneControlBinding[activeRenderWindowIx]; + + assert(active.boundProjectionIx.has_value()); + assert(active.lastBoundPerspectivePresetProjectionIx.has_value()); + assert(active.lastBoundOrthoPresetProjectionIx.has_value()); - requestPlanarCacheInit(active.planar->lastBoundPerspectivePresetProjectionIx, IPlanarProjection::CProjection::Perspective); - requestPlanarCacheInit(active.planar->lastBoundOrthoPresetProjectionIx, IPlanarProjection::CProjection::Orthographic); + const auto activePlanarIxString = std::to_string(active.activePlanarIx); + auto& planarBound = m_planarProjections[active.activePlanarIx]; + assert(planarBound); - auto selectedProjectionType = active.planar->getPlanarProjections()[active.planar->boundProjectionIx.value()].getParameters().m_type; + auto selectedProjectionType = planarBound->getPlanarProjections()[active.boundProjectionIx.value()].getParameters().m_type; { const char* labels[] = { "Perspective", "Orthographic" }; int type = static_cast(selectedProjectionType); @@ -1529,9 +1540,9 @@ class UISampleApp final : public examples::SimpleWindowedApplication switch (selectedProjectionType) { - case IPlanarProjection::CProjection::Perspective: active.planar->boundProjectionIx = active.planar->lastBoundPerspectivePresetProjectionIx.value(); break; - case IPlanarProjection::CProjection::Orthographic: active.planar->boundProjectionIx = active.planar->lastBoundOrthoPresetProjectionIx.value(); break; - default: active.planar->boundProjectionIx = std::nullopt; assert(false); break; + case IPlanarProjection::CProjection::Perspective: active.boundProjectionIx = active.lastBoundPerspectivePresetProjectionIx.value(); break; + case IPlanarProjection::CProjection::Orthographic: active.boundProjectionIx = active.lastBoundOrthoPresetProjectionIx.value(); break; + default: active.boundProjectionIx = std::nullopt; assert(false); break; } } } @@ -1546,9 +1557,9 @@ class UISampleApp final : public examples::SimpleWindowedApplication } }; - if (ImGui::BeginCombo("Projection Preset", getPresetName(active.planar->boundProjectionIx.value()).c_str())) + if (ImGui::BeginCombo("Projection Preset", getPresetName(active.boundProjectionIx.value()).c_str())) { - auto& projections = active.planar->getPlanarProjections(); + auto& projections = planarBound->getPlanarProjections(); for (uint32_t i = 0; i < projections.size(); ++i) { @@ -1558,16 +1569,16 @@ class UISampleApp final : public examples::SimpleWindowedApplication if (params.m_type != selectedProjectionType) continue; - bool isSelected = (i == active.planar->boundProjectionIx.value()); + bool isSelected = (i == active.boundProjectionIx.value()); if (ImGui::Selectable(getPresetName(i).c_str(), isSelected)) { - active.planar->boundProjectionIx = i; + active.boundProjectionIx = i; switch (selectedProjectionType) { - case IPlanarProjection::CProjection::Perspective: active.planar->lastBoundPerspectivePresetProjectionIx = active.planar->boundProjectionIx.value(); break; - case IPlanarProjection::CProjection::Orthographic: active.planar->lastBoundOrthoPresetProjectionIx = active.planar->boundProjectionIx.value(); break; + case IPlanarProjection::CProjection::Perspective: active.lastBoundPerspectivePresetProjectionIx = active.boundProjectionIx.value(); break; + case IPlanarProjection::CProjection::Orthographic: active.lastBoundOrthoPresetProjectionIx = active.boundProjectionIx.value(); break; default: assert(false); break; } } @@ -1578,8 +1589,8 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGui::EndCombo(); } - auto* const boundCamera = active.planar->getCamera(); - auto& boundProjection = active.planar->getPlanarProjections()[active.planar->boundProjectionIx.value()]; + auto* const boundCamera = planarBound->getCamera(); + auto& boundProjection = planarBound->getPlanarProjections()[active.boundProjectionIx.value()]; assert(not boundProjection.isProjectionSingular()); auto updateParameters = boundProjection.getParameters(); @@ -1588,6 +1599,8 @@ class UISampleApp final : public examples::SimpleWindowedApplication if (useWindow) ImGui::Checkbox("Allow axes to flip##allowAxesToFlip", &active.allowGizmoAxesToFlip); + ImGui::Checkbox("Draw debug grid##drawDebugGrid", &active.enableDebugGridDraw); + if (ImGui::RadioButton("LH", boundProjection.isProjectionLeftHanded().value())) isLH = true; @@ -1918,6 +1931,8 @@ class UISampleApp final : public examples::SimpleWindowedApplication // one model object in the world, testing multiuple cameraz for which view is rendered to separate frame buffers (so what they see) with new controller API including imguizmo nbl::hlsl::float32_t3x4 m_model = nbl::hlsl::float32_t3x4(1.f); + + // if we had working IObjectTransform or something similar then it would be it instead, it is "last manipulated object" I need for TRS editor nbl::core::smart_refctd_ptr boundCameraToManipulate; std::vector> m_planarProjections; @@ -1927,14 +1942,40 @@ class UISampleApp final : public examples::SimpleWindowedApplication struct SceneControlBinding { nbl::core::smart_refctd_ptr scene; - nbl::core::smart_refctd_ptr planar; + + uint32_t activePlanarIx = 0u; bool allowGizmoAxesToFlip = false; + bool enableDebugGridDraw = true; float aspectRatio = 16.f / 9.f; + + std::optional boundProjectionIx = std::nullopt, lastBoundPerspectivePresetProjectionIx = std::nullopt, lastBoundOrthoPresetProjectionIx = std::nullopt; + + 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(); + } }; static constexpr inline auto MaxSceneFBOs = 2u; std::array sceneControlBinding; - uint32_t activePlanarIx = 0u; + uint32_t activeRenderWindowIx = 0u; // UI font atlas + viewport FBO color attachment textures constexpr static inline auto TotalUISampleTexturesAmount = 1u + MaxSceneFBOs; From a0abbe2f3dbe0943d771a5aa979c09da040879c6 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Sat, 21 Dec 2024 11:33:58 +0100 Subject: [PATCH 068/205] preserve virtual active event state when required & use bound projection to manipulate active camera in move mode --- 61_UI/include/keysmapping.hpp | 18 ++++++++--- 61_UI/main.cpp | 34 ++++++++++++--------- common/include/camera/IPlanarProjection.hpp | 13 +------- 3 files changed, 34 insertions(+), 31 deletions(-) diff --git a/61_UI/include/keysmapping.hpp b/61_UI/include/keysmapping.hpp index 83ec22c32..46153660c 100644 --- a/61_UI/include/keysmapping.hpp +++ b/61_UI/include/keysmapping.hpp @@ -3,8 +3,9 @@ #include "common.hpp" -void handleAddMapping(const char* tableID, IGimbalManipulateEncoder* encoder, IGimbalManipulateEncoder::EncoderType activeController, CVirtualGimbalEvent::VirtualEventType& selectedEventType, ui::E_KEY_CODE& newKey, ui::E_MOUSE_CODE& newMouseCode, bool& addMode) +bool handleAddMapping(const char* tableID, IGimbalManipulateEncoder* encoder, IGimbalManipulateEncoder::EncoderType activeController, 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, 0.33f); ImGui::TableSetupColumn("Key", ImGuiTableColumnFlags_WidthStretch, 0.33f); @@ -64,6 +65,7 @@ void handleAddMapping(const char* tableID, IGimbalManipulateEncoder* encoder, IG ImGui::TableSetColumnIndex(2); if (ImGui::Button("Confirm Add", ImVec2(100, 30))) { + anyMapUpdated |= true; if (activeController == IGimbalManipulateEncoder::Keyboard) encoder->updateKeyboardMapping([&](auto& keys) { keys[newKey] = selectedEventType; }); else @@ -72,11 +74,15 @@ void handleAddMapping(const char* tableID, IGimbalManipulateEncoder* encoder, IG } ImGui::EndTable(); + + return anyMapUpdated; } -void displayKeyMappingsAndVirtualStatesInline(IGimbalManipulateEncoder* encoder, bool spawnWindow = false) +bool displayKeyMappingsAndVirtualStatesInline(IGimbalManipulateEncoder* encoder, bool spawnWindow = false) { - if (!encoder) return; + bool anyMapUpdated = false; + + if (!encoder) return anyMapUpdated; struct MappingState { @@ -143,6 +149,7 @@ void displayKeyMappingsAndVirtualStatesInline(IGimbalManipulateEncoder* encoder, ImGui::TableSetColumnIndex(4); if (ImGui::Button(("Delete##deleteKey" + std::to_string(static_cast(keyboardCode))).c_str())) { + anyMapUpdated |= true; encoder->updateKeyboardMapping([keyboardCode](auto& keys) { keys.erase(keyboardCode); }); break; } @@ -152,7 +159,7 @@ void displayKeyMappingsAndVirtualStatesInline(IGimbalManipulateEncoder* encoder, if (state.addMode) { ImGui::Separator(); - handleAddMapping("AddKeyboardMappingTable", encoder, state.activeController, state.selectedEventType, state.newKey, state.newMouseCode, state.addMode); + anyMapUpdated |= handleAddMapping("AddKeyboardMappingTable", encoder, state.activeController, state.selectedEventType, state.newKey, state.newMouseCode, state.addMode); } ImGui::EndTabItem(); @@ -200,6 +207,7 @@ void displayKeyMappingsAndVirtualStatesInline(IGimbalManipulateEncoder* encoder, ImGui::TableSetColumnIndex(4); if (ImGui::Button(("Delete##deleteMouse" + std::to_string(static_cast(mouseCode))).c_str())) { + anyMapUpdated |= true; encoder->updateMouseMapping([mouseCode](auto& mouse) { mouse.erase(mouseCode); }); break; } @@ -219,6 +227,8 @@ void displayKeyMappingsAndVirtualStatesInline(IGimbalManipulateEncoder* encoder, if (spawnWindow) ImGui::End(); + + return anyMapUpdated; } #endif // __NBL_KEYSMAPPING_H_INCLUDED__ \ No newline at end of file diff --git a/61_UI/main.cpp b/61_UI/main.cpp index e0031333b..796242223 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -732,12 +732,14 @@ class UISampleApp final : public examples::SimpleWindowedApplication return false; } + // init render window planar references - we make all render windows start with focus on first + // planar but in a way that first window has the planar's perspective preset bound & second orthographic for (uint32_t i = 0u; i < sceneControlBinding.size(); ++i) { auto& binding = sceneControlBinding[i]; - const auto& projections = m_planarProjections[binding.activePlanarIx = 0]->getPlanarProjections(); - binding.pickDefaultProjections(projections); + auto& planar = m_planarProjections[binding.activePlanarIx = 0]; + binding.pickDefaultProjections(planar->getPlanarProjections()); if (i) binding.boundProjectionIx = binding.lastBoundOrthoPresetProjectionIx.value(); @@ -1087,21 +1089,26 @@ class UISampleApp final : public examples::SimpleWindowedApplication if (enableActiveCameraMovement) { - auto* camera = m_planarProjections[sceneControlBinding[activeRenderWindowIx].activePlanarIx]->getCamera(); + auto& binding = sceneControlBinding[activeRenderWindowIx]; + auto& planar = m_planarProjections[binding.activePlanarIx]; + auto* camera = planar->getCamera(); + + assert(binding.boundProjectionIx.has_value()); + auto& projection = planar->getPlanarProjections()[binding.boundProjectionIx.value()]; static std::vector virtualEvents(0x45); uint32_t vCount; - camera->beginInputProcessing(m_nextPresentationTimestamp); + projection.beginInputProcessing(m_nextPresentationTimestamp); { - camera->process(nullptr, vCount); + projection.process(nullptr, vCount); if (virtualEvents.size() < vCount) virtualEvents.resize(vCount); - camera->process(virtualEvents.data(), vCount, { params.keyboardEvents, params.mouseEvents }); + projection.process(virtualEvents.data(), vCount, { params.keyboardEvents, params.mouseEvents }); } - camera->endInputProcessing(); + projection.endInputProcessing(); if(vCount) camera->manipulate({ virtualEvents.data(), vCount }, ICamera::Local); @@ -1495,6 +1502,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGui::Checkbox("Window mode##useWindow", &useWindow); ImGui::Separator(); + auto& active = sceneControlBinding[activeRenderWindowIx]; const auto activeRenderWindowIxString = std::to_string(activeRenderWindowIx); ImGui::Text("Active Render Window: %s", activeRenderWindowIxString.c_str()); @@ -1510,7 +1518,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication for (size_t i = 0; i < planarsCount; ++i) labels[i] = sbels[i].c_str(); - auto& active = sceneControlBinding[activeRenderWindowIx]; + int currentPlanarIx = static_cast(active.activePlanarIx); if (ImGui::Combo("Active Planar", ¤tPlanarIx, labels.data(), static_cast(labels.size()))) { @@ -1519,8 +1527,6 @@ class UISampleApp final : public examples::SimpleWindowedApplication } } - auto& active = sceneControlBinding[activeRenderWindowIx]; - assert(active.boundProjectionIx.has_value()); assert(active.lastBoundPerspectivePresetProjectionIx.has_value()); assert(active.lastBoundOrthoPresetProjectionIx.has_value()); @@ -1557,6 +1563,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication } }; + bool updateBoundVirtualMaps = false; if (ImGui::BeginCombo("Projection Preset", getPresetName(active.boundProjectionIx.value()).c_str())) { auto& projections = planarBound->getPlanarProjections(); @@ -1574,6 +1581,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication if (ImGui::Selectable(getPresetName(i).c_str(), isSelected)) { active.boundProjectionIx = i; + updateBoundVirtualMaps |= true; switch (selectedProjectionType) { @@ -1713,16 +1721,12 @@ class UISampleApp final : public examples::SimpleWindowedApplication if (ImGui::TreeNodeEx("Virtual Event Mappings", flags)) { - auto* encoder = static_cast(&boundProjection); - displayKeyMappingsAndVirtualStatesInline(encoder); + displayKeyMappingsAndVirtualStatesInline(&boundProjection); ImGui::TreePop(); } ImGui::TreePop(); } - - boundCamera->updateKeyboardMapping([&](auto& map) { map = boundProjection.getKeyboardVirtualEventMap(); }); - boundCamera->updateMouseMapping([&](auto& map) { map = boundProjection.getMouseVirtualEventMap(); }); } ImGui::End(); diff --git a/common/include/camera/IPlanarProjection.hpp b/common/include/camera/IPlanarProjection.hpp index 4ca9a0475..731b9fd5b 100644 --- a/common/include/camera/IPlanarProjection.hpp +++ b/common/include/camera/IPlanarProjection.hpp @@ -9,7 +9,7 @@ namespace nbl::hlsl class IPlanarProjection : public ILinearProjection { public: - struct CProjection : public ILinearProjection::CProjection, public IGimbalManipulateEncoder + struct CProjection : public ILinearProjection::CProjection, public IGimbalController { using base_t = ILinearProjection::CProjection; @@ -88,21 +88,10 @@ class IPlanarProjection : public ILinearProjection base_t::setProjectionMatrix(buildProjectionMatrixOrthoRH(orthoWidth, viewHeight, zNear, zFar)); } - virtual void updateKeyboardMapping(const std::function& mapKeys) override { mapKeys(m_keyboardVirtualEventMap); } - virtual void updateMouseMapping(const std::function& mapKeys) override { mapKeys(m_mouseVirtualEventMap); } - virtual void updateImguizmoMapping(const std::function& mapKeys) override { mapKeys(m_imguizmoVirtualEventMap); } - - virtual const keyboard_to_virtual_events_t& getKeyboardVirtualEventMap() const override { return m_keyboardVirtualEventMap; } - virtual const mouse_to_virtual_events_t& getMouseVirtualEventMap() const override { return m_mouseVirtualEventMap; } - virtual const imguizmo_to_virtual_events_t& getImguizmoVirtualEventMap() const override { return m_imguizmoVirtualEventMap; } inline const ProjectionParameters& getParameters() const { return m_parameters; } private: CProjection() = default; ProjectionParameters m_parameters; - - keyboard_to_virtual_events_t m_keyboardVirtualEventMap; - mouse_to_virtual_events_t m_mouseVirtualEventMap; - imguizmo_to_virtual_events_t m_imguizmoVirtualEventMap; }; protected: From b9327d4a40a038bbca6d0e34cfd2e58e6fb02ab8 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Sat, 21 Dec 2024 13:01:02 +0100 Subject: [PATCH 069/205] correct projection updates --- 61_UI/cameras.json | 6 +-- 61_UI/main.cpp | 27 +++++-------- common/include/camera/IPlanarProjection.hpp | 43 ++++++++++++++------- 3 files changed, 41 insertions(+), 35 deletions(-) diff --git a/61_UI/cameras.json b/61_UI/cameras.json index 105a3ec7e..80e09d059 100644 --- a/61_UI/cameras.json +++ b/61_UI/cameras.json @@ -21,15 +21,13 @@ "type": "perspective", "fov": 60.0, "zNear": 0.1, - "zFar": 100.0, - "leftHanded": true + "zFar": 100.0 }, { "type": "orthographic", "orthoWidth": 10.0, "zNear": 0.1, - "zFar": 100.0, - "leftHanded": true + "zFar": 100.0 } ], "viewports": [ diff --git a/61_UI/main.cpp b/61_UI/main.cpp index 796242223..8aad11ab4 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -539,7 +539,6 @@ class UISampleApp final : public examples::SimpleWindowedApplication if (jProjection.contains("type")) { float zNear, zFar; - bool leftHanded; if (!jProjection.contains("zNear")) { @@ -553,15 +552,8 @@ class UISampleApp final : public examples::SimpleWindowedApplication return false; } - if (!jProjection.contains("leftHanded")) - { - "Expected \"leftHanded\" keyword for planar projection definition!"; - return false; - } - zNear = jProjection["zNear"].get(); zFar = jProjection["zFar"].get(); - leftHanded = jProjection["leftHanded"].get(); if (jProjection["type"] == "perspective") { @@ -572,7 +564,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication } float fov = jProjection["fov"].get(); - projections.emplace_back(IPlanarProjection::CProjection::create(leftHanded, zNear, zFar, fov)); + projections.emplace_back(IPlanarProjection::CProjection::create(zNear, zFar, fov)); } else if (jProjection["type"] == "orthographic") @@ -584,7 +576,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication } float orthoWidth = jProjection["orthoWidth"].get(); - projections.emplace_back(IPlanarProjection::CProjection::create(leftHanded, zNear, zFar, orthoWidth)); + projections.emplace_back(IPlanarProjection::CProjection::create(zNear, zFar, orthoWidth)); } else { @@ -1258,6 +1250,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication assert(binding.boundProjectionIx.has_value()); auto& projection = planarBound->getPlanarProjections()[binding.boundProjectionIx.value()]; + projection.update(binding.leftHandedProjection, binding.aspectRatio); // first 0th texture is for UI texture atlas, then there are our window textures auto fboImguiTextureID = windowIx + 1u; @@ -1602,20 +1595,19 @@ class UISampleApp final : public examples::SimpleWindowedApplication assert(not boundProjection.isProjectionSingular()); auto updateParameters = boundProjection.getParameters(); - bool isLH = boundProjection.isProjectionLeftHanded().value(); if (useWindow) ImGui::Checkbox("Allow axes to flip##allowAxesToFlip", &active.allowGizmoAxesToFlip); ImGui::Checkbox("Draw debug grid##drawDebugGrid", &active.enableDebugGridDraw); - if (ImGui::RadioButton("LH", boundProjection.isProjectionLeftHanded().value())) - isLH = true; + if (ImGui::RadioButton("LH", active.leftHandedProjection)) + active.leftHandedProjection = true; ImGui::SameLine(); - if (ImGui::RadioButton("RH", not isLH)) - isLH = false; + if (ImGui::RadioButton("RH", not active.leftHandedProjection)) + active.leftHandedProjection = false; ImGui::SliderFloat("zNear", &updateParameters.m_zNear, 0.1f, 100.f, "%.2f", ImGuiSliderFlags_Logarithmic); ImGui::SliderFloat("zFar", &updateParameters.m_zFar, 110.f, 10000.f, "%.1f", ImGuiSliderFlags_Logarithmic); @@ -1625,13 +1617,13 @@ class UISampleApp final : public examples::SimpleWindowedApplication case IPlanarProjection::CProjection::Perspective: { ImGui::SliderFloat("Fov", &updateParameters.m_planar.perspective.fov, 20.f, 150.f, "%.1f", ImGuiSliderFlags_Logarithmic); - boundProjection.setPerspective(isLH, updateParameters.m_zNear, updateParameters.m_zFar, updateParameters.m_planar.perspective.fov, active.aspectRatio); + boundProjection.setPerspective(updateParameters.m_zNear, updateParameters.m_zFar, updateParameters.m_planar.perspective.fov); } break; case IPlanarProjection::CProjection::Orthographic: { ImGui::SliderFloat("Ortho width", &updateParameters.m_planar.orthographic.orthoWidth, 1.f, 20.f, "%.1f", ImGuiSliderFlags_Logarithmic); - boundProjection.setOrthographic(isLH, updateParameters.m_zNear, updateParameters.m_zFar, updateParameters.m_planar.orthographic.orthoWidth, active.aspectRatio); + boundProjection.setOrthographic(updateParameters.m_zNear, updateParameters.m_zFar, updateParameters.m_planar.orthographic.orthoWidth); } break; default: break; @@ -1951,6 +1943,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication bool allowGizmoAxesToFlip = false; bool enableDebugGridDraw = true; float aspectRatio = 16.f / 9.f; + bool leftHandedProjection = true; std::optional boundProjectionIx = std::nullopt, lastBoundPerspectivePresetProjectionIx = std::nullopt, lastBoundOrthoPresetProjectionIx = std::nullopt; diff --git a/common/include/camera/IPlanarProjection.hpp b/common/include/camera/IPlanarProjection.hpp index 731b9fd5b..1fccf0ef4 100644 --- a/common/include/camera/IPlanarProjection.hpp +++ b/common/include/camera/IPlanarProjection.hpp @@ -60,32 +60,47 @@ class IPlanarProjection : public ILinearProjection float m_zFar; }; - inline void setPerspective(bool leftHanded = true, float zNear = 0.1f, float zFar = 100.f, float fov = 60.f, float aspectRatio = 16.f / 9.f) + inline void update(bool leftHanded, float aspectRatio) + { + switch (m_parameters.m_type) + { + case Perspective: + { + const auto& fov = m_parameters.m_planar.perspective.fov; + + if (leftHanded) + base_t::setProjectionMatrix(buildProjectionMatrixPerspectiveFovLH(glm::radians(fov), aspectRatio, m_parameters.m_zNear, m_parameters.m_zFar)); + else + base_t::setProjectionMatrix(buildProjectionMatrixPerspectiveFovRH(glm::radians(fov), aspectRatio, m_parameters.m_zNear, m_parameters.m_zFar)); + } break; + + case Orthographic: + { + const auto& orthoW = m_parameters.m_planar.orthographic.orthoWidth; + const auto viewHeight = orthoW * core::reciprocal(aspectRatio); + + if (leftHanded) + base_t::setProjectionMatrix(buildProjectionMatrixOrthoLH(orthoW, viewHeight, m_parameters.m_zNear, m_parameters.m_zFar)); + else + base_t::setProjectionMatrix(buildProjectionMatrixOrthoRH(orthoW, viewHeight, m_parameters.m_zNear, m_parameters.m_zFar)); + } break; + } + } + + inline void setPerspective(float zNear = 0.1f, float zFar = 100.f, float fov = 60.f) { m_parameters.m_type = Perspective; m_parameters.m_planar.perspective.fov = fov; m_parameters.m_zNear = zNear; m_parameters.m_zFar = zFar; - - if (leftHanded) - base_t::setProjectionMatrix(buildProjectionMatrixPerspectiveFovLH(glm::radians(fov), aspectRatio, zNear, zFar)); - else - base_t::setProjectionMatrix(buildProjectionMatrixPerspectiveFovRH(glm::radians(fov), aspectRatio, zNear, zFar)); } - inline void setOrthographic(bool leftHanded = true, float zNear = 0.1f, float zFar = 100.f, float orthoWidth = 10.f, float aspectRatio = 16.f / 9.f) + inline void setOrthographic(float zNear = 0.1f, float zFar = 100.f, float orthoWidth = 10.f) { m_parameters.m_type = Orthographic; m_parameters.m_planar.orthographic.orthoWidth = orthoWidth; m_parameters.m_zNear = zNear; m_parameters.m_zFar = zFar; - - const auto viewHeight = orthoWidth * core::reciprocal(aspectRatio); - - if (leftHanded) - base_t::setProjectionMatrix(buildProjectionMatrixOrthoLH(orthoWidth, viewHeight, zNear, zFar)); - else - base_t::setProjectionMatrix(buildProjectionMatrixOrthoRH(orthoWidth, viewHeight, zNear, zFar)); } inline const ProjectionParameters& getParameters() const { return m_parameters; } From b66723bd88cd06b56e3c2f5b8156dea9a8741f23 Mon Sep 17 00:00:00 2001 From: YasInvolved Date: Sun, 22 Dec 2024 12:23:24 +0100 Subject: [PATCH 070/205] added cameraz.json to builtin resources --- 61_UI/CMakeLists.txt | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/61_UI/CMakeLists.txt b/61_UI/CMakeLists.txt index 1930cb17f..87e1aa646 100644 --- a/61_UI/CMakeLists.txt +++ b/61_UI/CMakeLists.txt @@ -17,5 +17,19 @@ if(NBL_BUILD_IMGUI) LINK_BUILTIN_RESOURCES_TO_TARGET(${EXECUTABLE_NAME} geometryCreatorSpirvBRD) endif() +if(NBL_EMBED_BUILTIN_RESOURCES) + set(_BR_TARGET_ ${EXECUTABLE_NAME}_builtinResourceData) + + get_filename_component(_SEARCH_DIRECTORIES_ "${CMAKE_CURRENT_SOURCE_DIR}" ABSOLUTE) + get_filename_component(_OUTPUT_DIRECTORY_SOURCE_ "${CMAKE_CURRENT_BINARY_DIR}/src" ABSOLUTE) + get_filename_component(_OUTPUT_DIRECTORY_HEADER_ "${CMAKE_CURRENT_BINARY_DIR}/include" ABSOLUTE) + + LIST_BUILTIN_RESOURCE(RESOURCES_TO_EMBED "${RES_FILE}") + + ADD_CUSTOM_BUILTIN_RESOURCES(${_BR_TARGET_} RESOURCES_TO_EMBED "${_SEARCH_DIRECTORIES_}" "" "nbl::this_example::builtin" "${_OUTPUT_DIRECTORY_HEADER_}" "${_OUTPUT_DIRECTORY_SOURCE_}") + + LINK_BUILTIN_RESOURCES_TO_TARGET(${EXECUTABLE_NAME} ${_BR_TARGET_}) +endif() + add_dependencies(${EXECUTABLE_NAME} argparse) target_include_directories(${EXECUTABLE_NAME} PUBLIC $) \ No newline at end of file From 0e4456fb696ebe9937c8736c25585daa47ff020a Mon Sep 17 00:00:00 2001 From: YasInvolved Date: Sun, 22 Dec 2024 12:43:41 +0100 Subject: [PATCH 071/205] move default config to app_resources --- 61_UI/CMakeLists.txt | 8 ++++++-- 61_UI/{ => app_resources}/cameras.json | 0 61_UI/main.cpp | 11 +++++++---- 3 files changed, 13 insertions(+), 6 deletions(-) rename 61_UI/{ => app_resources}/cameras.json (100%) diff --git a/61_UI/CMakeLists.txt b/61_UI/CMakeLists.txt index 87e1aa646..42425b0a8 100644 --- a/61_UI/CMakeLists.txt +++ b/61_UI/CMakeLists.txt @@ -19,14 +19,18 @@ endif() if(NBL_EMBED_BUILTIN_RESOURCES) set(_BR_TARGET_ ${EXECUTABLE_NAME}_builtinResourceData) + set(RESOURCE_DIR "app_resources") get_filename_component(_SEARCH_DIRECTORIES_ "${CMAKE_CURRENT_SOURCE_DIR}" ABSOLUTE) get_filename_component(_OUTPUT_DIRECTORY_SOURCE_ "${CMAKE_CURRENT_BINARY_DIR}/src" ABSOLUTE) get_filename_component(_OUTPUT_DIRECTORY_HEADER_ "${CMAKE_CURRENT_BINARY_DIR}/include" ABSOLUTE) - LIST_BUILTIN_RESOURCE(RESOURCES_TO_EMBED "${RES_FILE}") + file(GLOB_RECURSE BUILTIN_RESOURCE_FILES RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}/${RESOURCE_DIR}" CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/${RESOURCE_DIR}/*") + foreach(RES_FILE ${BUILTIN_RESOURCE_FILES}) + LIST_BUILTIN_RESOURCE(RESOURCES_TO_EMBED "${RES_FILE}") + endforeach() - ADD_CUSTOM_BUILTIN_RESOURCES(${_BR_TARGET_} RESOURCES_TO_EMBED "${_SEARCH_DIRECTORIES_}" "" "nbl::this_example::builtin" "${_OUTPUT_DIRECTORY_HEADER_}" "${_OUTPUT_DIRECTORY_SOURCE_}") + ADD_CUSTOM_BUILTIN_RESOURCES(${_BR_TARGET_} RESOURCES_TO_EMBED "${_SEARCH_DIRECTORIES_}" "${RESOURCE_DIR}" "nbl::this_example::builtin" "${_OUTPUT_DIRECTORY_HEADER_}" "${_OUTPUT_DIRECTORY_SOURCE_}") LINK_BUILTIN_RESOURCES_TO_TARGET(${EXECUTABLE_NAME} ${_BR_TARGET_}) endif() diff --git a/61_UI/cameras.json b/61_UI/app_resources/cameras.json similarity index 100% rename from 61_UI/cameras.json rename to 61_UI/app_resources/cameras.json diff --git a/61_UI/main.cpp b/61_UI/main.cpp index 8aad11ab4..3a45eb7a1 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -10,6 +10,7 @@ using json = nlohmann::json; #include "keysmapping.hpp" #include "camera/CCubeProjection.hpp" #include "glm/glm/ext/matrix_clip_space.hpp" // TODO: TESTING +#include "nbl/builtin/CArchive.h" using planar_projections_range_t = std::vector; using planar_projection_t = CPlanarProjection; @@ -473,15 +474,17 @@ class UISampleApp final : public examples::SimpleWindowedApplication { const auto cameraJsonFile = program.get("--file"); + json j; std::ifstream file(cameraJsonFile.c_str()); if (!file.is_open()) { - std::cerr << "Error: Cannot open input \"" << cameraJsonFile.c_str() << "\" json file."; + m_logger->log("Cannot open input \"%s\" json file. Switching to default config."); return false; } - - json j; - file >> j; + else + { + file >> j; + } std::vector> cameras; for (const auto& jCamera : j["cameras"]) From 20214bfb8e78994a45ffde975f49dd99db95f893 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Sun, 22 Dec 2024 12:48:29 +0100 Subject: [PATCH 072/205] allow to pick object from the scene in TRS editor, add more help-texts --- 61_UI/main.cpp | 368 ++++++++++++++++++++++++++++--------------------- 1 file changed, 214 insertions(+), 154 deletions(-) diff --git a/61_UI/main.cpp b/61_UI/main.cpp index 8aad11ab4..46c0c2040 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -14,6 +14,17 @@ using json = nlohmann::json; using planar_projections_range_t = std::vector; using planar_projection_t = CPlanarProjection; +// the only reason for those is to remind we must go with transpose & 4x4 matrices +struct ImGuizmoPlanarM16InOut +{ + float32_t4x4 view, projection; +}; + +struct ImGuizmoModelM16InOut +{ + float32_t4x4 inTRS, outTRS, outDeltaTRS; +}; + constexpr IGPUImage::SSubresourceRange TripleBufferUsedSubresourceRange = { .aspectMask = IGPUImage::EAF_COLOR_BIT, @@ -453,9 +464,9 @@ class UISampleApp final : public examples::SimpleWindowedApplication const auto dpyInfo = m_winMgr->getPrimaryDisplayInfo(); - for (uint32_t i = 0u; i < sceneControlBinding.size(); ++i) + for (uint32_t i = 0u; i < windowControlBinding.size(); ++i) { - auto& scene = sceneControlBinding[i].scene; + auto& scene = windowControlBinding[i].scene; scene = CScene::create(smart_refctd_ptr(m_device), smart_refctd_ptr(m_logger), getGraphicsQueue(), smart_refctd_ptr(resources), dpyInfo.resX, dpyInfo.resY); if (!scene) @@ -717,18 +728,18 @@ class UISampleApp final : public examples::SimpleWindowedApplication return false; } - if (m_planarProjections.size() < sceneControlBinding.size()) + if (m_planarProjections.size() < windowControlBinding.size()) { // TODO, temporary assuming it, I'm not going to implement each possible case now - std::cerr << "Expected at least " << std::to_string(sceneControlBinding.size()) << " planars!" << std::endl; + std::cerr << "Expected at least " << std::to_string(windowControlBinding.size()) << " planars!" << std::endl; return false; } // init render window planar references - we make all render windows start with focus on first // planar but in a way that first window has the planar's perspective preset bound & second orthographic - for (uint32_t i = 0u; i < sceneControlBinding.size(); ++i) + for (uint32_t i = 0u; i < windowControlBinding.size(); ++i) { - auto& binding = sceneControlBinding[i]; + auto& binding = windowControlBinding[i]; auto& planar = m_planarProjections[binding.activePlanarIx = 0]; binding.pickDefaultProjections(planar->getPlanarProjections()); @@ -756,12 +767,12 @@ class UISampleApp final : public examples::SimpleWindowedApplication descriptorInfo[nbl::ext::imgui::UI::FontAtlasTexId].desc = core::smart_refctd_ptr(m_ui.manager->getFontAtlasView()); writes[nbl::ext::imgui::UI::FontAtlasTexId].info = descriptorInfo.data() + nbl::ext::imgui::UI::FontAtlasTexId; - for (uint32_t i = 0; i < sceneControlBinding.size(); ++i) + for (uint32_t i = 0; i < windowControlBinding.size(); ++i) { const auto textureIx = i + 1u; descriptorInfo[textureIx].info.image.imageLayout = IImage::LAYOUT::READ_ONLY_OPTIMAL; - descriptorInfo[textureIx].desc = sceneControlBinding[i].scene->getColorAttachment(); + descriptorInfo[textureIx].desc = windowControlBinding[i].scene->getColorAttachment(); writes[textureIx].info = descriptorInfo.data() + textureIx; writes[textureIx].info = descriptorInfo.data() + textureIx; @@ -836,10 +847,10 @@ class UISampleApp final : public examples::SimpleWindowedApplication }; if (useWindow) - for (auto binding : sceneControlBinding) + for (auto binding : windowControlBinding) renderOfflineScene(binding.scene); else - renderOfflineScene(sceneControlBinding.front().scene.get()); // just to not render to all at once + renderOfflineScene(windowControlBinding.front().scene.get()); // just to not render to all at once const IGPUCommandBuffer::SClearColorValue clearValue = { .float32 = {0.f,0.f,0.f,1.f} }; const IGPUCommandBuffer::SRenderpassBeginInfo info = { @@ -970,13 +981,13 @@ class UISampleApp final : public examples::SimpleWindowedApplication { // wait for first planar scene view fb { - .semaphore = sceneControlBinding[0].scene->semaphore.progress.get(), - .value = sceneControlBinding[0].scene->semaphore.finishedValue + .semaphore = windowControlBinding[0].scene->semaphore.progress.get(), + .value = windowControlBinding[0].scene->semaphore.finishedValue }, // and second one too { - .semaphore = sceneControlBinding[1].scene->semaphore.progress.get(), - .value = sceneControlBinding[1].scene->semaphore.finishedValue + .semaphore = windowControlBinding[1].scene->semaphore.progress.get(), + .value = windowControlBinding[1].scene->semaphore.finishedValue } } )); @@ -988,8 +999,8 @@ class UISampleApp final : public examples::SimpleWindowedApplication { // wait for picked planar only { - .semaphore = sceneControlBinding[activeRenderWindowIx].scene->semaphore.progress.get(), - .value = sceneControlBinding[activeRenderWindowIx].scene->semaphore.finishedValue + .semaphore = windowControlBinding[activeRenderWindowIx].scene->semaphore.progress.get(), + .value = windowControlBinding[activeRenderWindowIx].scene->semaphore.finishedValue } } )); @@ -1081,7 +1092,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication if (enableActiveCameraMovement) { - auto& binding = sceneControlBinding[activeRenderWindowIx]; + auto& binding = windowControlBinding[activeRenderWindowIx]; auto& planar = m_planarProjections[binding.activePlanarIx]; auto* camera = planar->getCamera(); @@ -1115,109 +1126,15 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGuiIO& io = ImGui::GetIO(); ImGuizmo::BeginFrame(); - - // TODO: need to inspect where I'm wrong, workaround - auto gimbalToImguizmoTRS = [&](const float32_t3x4& nblGimbalTrs) -> float32_t4x4 - { - // *do not transpose whole matrix*, only the translate part - float32_t4x4 trs = getMatrix3x4As4x4(nblGimbalTrs); - trs[3] = float32_t4(nblGimbalTrs[0][3], nblGimbalTrs[1][3], nblGimbalTrs[2][3], 1.f); - trs[0][3] = 0.f; - trs[1][3] = 0.f; - trs[2][3] = 0.f; - - return trs; - }; - { SImResourceInfo info; info.samplerIx = (uint16_t)nbl::ext::imgui::UI::DefaultSamplerIx::USER; - // render camera views onto GUIs + // render bound planar camera views onto GUI windows if (useWindow) { - // the only reason for those is to remind we must go with transpose & 4x4 matrices - struct ImGuizmoPlanarM16InOut - { - float32_t4x4 view, projection; - }; - - struct ImGuizmoModelM16InOut - { - float32_t4x4 inTRS, outTRS, outDeltaTRS; - }; - - //////////////////////////////////////////////////////////////////////////// - // ABS TRS control with editor, only single object can be manipulated at time - { - ImGuizmoModelM16InOut imguizmoModel; - - if (boundCameraToManipulate) - imguizmoModel.inTRS = gimbalToImguizmoTRS(getCastedMatrix(boundCameraToManipulate->getGimbal()())); - else - imguizmoModel.inTRS = transpose(getMatrix3x4As4x4(m_model)); - - imguizmoModel.outTRS = imguizmoModel.inTRS; - - // TODO! DELTA TRS IS ONLY TRANSLATE TEMPORARY - TransformEditor(&imguizmoModel.outTRS[0][0], &imguizmoModel.outDeltaTRS); - - // generate virtual events given delta TRS matrix - if (boundCameraToManipulate) - { - { - static std::vector virtualEvents(0x45); - - uint32_t vCount = {}; - - boundCameraToManipulate->beginInputProcessing(m_nextPresentationTimestamp); - { - boundCameraToManipulate->process(nullptr, vCount); - - if (virtualEvents.size() < vCount) - virtualEvents.resize(vCount); - - IGimbalController::SUpdateParameters params; - params.imguizmoEvents = { { imguizmoModel.outDeltaTRS } }; - boundCameraToManipulate->process(virtualEvents.data(), vCount, params); - } - boundCameraToManipulate->endInputProcessing(); - - // I start to think controller should be able to set sensitivity to scale magnitudes of generated events - // in order for camera to not keep any magnitude scalars like move or rotation speed scales - - auto* fps = dynamic_cast(boundCameraToManipulate.get()); - - float pmSpeed = 1.f; - float prSpeed = 1.f; - - if (fps) - { - float pmSpeed = fps->getMoveSpeedScale(); - float prSpeed = fps->getRotationSpeedScale(); - - fps->setMoveSpeedScale(1); - fps->setRotationSpeedScale(1); - } - - // NOTE: generated events from ImGuizmo controller are always in world space! - if (vCount) - boundCameraToManipulate->manipulate({ virtualEvents.data(), vCount }, ICamera::World); - - if (fps) - { - fps->setMoveSpeedScale(pmSpeed); - fps->setRotationSpeedScale(prSpeed); - } - } - } - else - { - // for scene demo model full affine transformation without limits is assumed - m_model = float32_t3x4(transpose(imguizmoModel.outTRS)); - } - } - //////////////////////////////////////////////////////////////////////////// + // ABS TRS editor to manipulate bound object + TransformEditor(); if(enableActiveCameraMovement) ImGuizmo::Enable(false); @@ -1226,7 +1143,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication size_t gizmoIx = {}; size_t manipulationCounter = {}; - for (uint32_t windowIx = 0; windowIx < sceneControlBinding.size(); ++windowIx) + for (uint32_t windowIx = 0; windowIx < windowControlBinding.size(); ++windowIx) { // setup imgui window ImGui::SetNextWindowSize(ImVec2(800, 400), ImGuiCond_Appearing); @@ -1239,7 +1156,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication const ImVec2 contentRegionSize = ImGui::GetContentRegionAvail(), windowPos = ImGui::GetWindowPos(), cursorPos = ImGui::GetCursorScreenPos(); // setup bound entities for the window like camera & projections - auto& binding = sceneControlBinding[windowIx]; + auto& binding = windowControlBinding[windowIx]; auto& planarBound = m_planarProjections[binding.activePlanarIx]; assert(planarBound); @@ -1336,6 +1253,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication if (targetGimbalManipulationCamera) { boundCameraToManipulate = smart_refctd_ptr(targetGimbalManipulationCamera); + boundPlanarCameraIxToManipulate = modelIx - 1u; /* testing imguizmo controller for target camera, we use delta world imguizmo TRS matrix to generate virtual events @@ -1358,30 +1276,33 @@ class UISampleApp final : public examples::SimpleWindowedApplication } targetGimbalManipulationCamera->endInputProcessing(); - auto* fps = dynamic_cast(targetGimbalManipulationCamera); - float pMoveSpeed = 1.f, pRotationSpeed = 1.f; - - if (fps) + if (vCount) { - pMoveSpeed = fps->getMoveSpeedScale(); - pRotationSpeed = fps->getRotationSpeedScale(); + auto* fps = dynamic_cast(targetGimbalManipulationCamera); - // I start to think controller should be able to set sensitivity to scale magnitudes of generated events - // in order for camera to not keep any magnitude scalars like move or rotation speed scales + float pMoveSpeed = 1.f, pRotationSpeed = 1.f; - fps->setMoveSpeedScale(1); - fps->setRotationSpeedScale(1); - } + if (fps) + { + pMoveSpeed = fps->getMoveSpeedScale(); + pRotationSpeed = fps->getRotationSpeedScale(); - // NOTE: generated events from ImGuizmo controller are always in world space! - if (vCount) + // I start to think controller should be able to set sensitivity to scale magnitudes of generated events + // in order for camera to not keep any magnitude scalars like move or rotation speed scales + + fps->setMoveSpeedScale(1); + fps->setRotationSpeedScale(1); + } + + // NOTE: generated events from ImGuizmo controller are always in world space! targetGimbalManipulationCamera->manipulate({ virtualEvents.data(), vCount }, ICamera::World); - if (fps) - { - fps->setMoveSpeedScale(pMoveSpeed); - fps->setRotationSpeedScale(pRotationSpeed); + if (fps) + { + fps->setMoveSpeedScale(pMoveSpeed); + fps->setRotationSpeedScale(pRotationSpeed); + } } } } @@ -1390,11 +1311,12 @@ class UISampleApp final : public examples::SimpleWindowedApplication // again, for scene demo model full affine transformation without limits is assumed m_model = float32_t3x4(transpose(imguizmoModel.outTRS)); boundCameraToManipulate = nullptr; + boundPlanarCameraIxToManipulate = std::nullopt; } } } - if (ImGuizmo::IsOver()) + if (ImGuizmo::IsOver() and not ImGuizmo::IsUsingAny() && not enableActiveCameraMovement) { ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.2f, 0.2f, 0.2f, 0.8f)); ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); @@ -1458,9 +1380,9 @@ class UISampleApp final : public examples::SimpleWindowedApplication const auto type = static_cast(gcIndex); const auto& [gpu, meta] = references[type]; - for (uint32_t i = 0u; i < sceneControlBinding.size(); ++i) + for (uint32_t i = 0u; i < windowControlBinding.size(); ++i) { - auto& binding = sceneControlBinding[i]; + auto& binding = windowControlBinding[i]; auto& hook = binding.scene->object; auto& planarBound = m_planarProjections[binding.activePlanarIx]; @@ -1495,7 +1417,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGui::Checkbox("Window mode##useWindow", &useWindow); ImGui::Separator(); - auto& active = sceneControlBinding[activeRenderWindowIx]; + auto& active = windowControlBinding[activeRenderWindowIx]; const auto activeRenderWindowIxString = std::to_string(activeRenderWindowIx); ImGui::Text("Active Render Window: %s", activeRenderWindowIxString.c_str()); @@ -1511,7 +1433,6 @@ class UISampleApp final : public examples::SimpleWindowedApplication for (size_t i = 0; i < planarsCount; ++i) labels[i] = sbels[i].c_str(); - int currentPlanarIx = static_cast(active.activePlanarIx); if (ImGui::Combo("Active Planar", ¤tPlanarIx, labels.data(), static_cast(labels.size()))) { @@ -1680,6 +1601,9 @@ class UISampleApp final : public examples::SimpleWindowedApplication const auto flags = ImGuiTreeNodeFlags_DefaultOpen; if (ImGui::TreeNodeEx("Bound Camera", flags)) { + ImGui::Text("Type: %s", boundCamera->getIdentifier().data()); + ImGui::Text("Object Ix: %s", std::to_string(active.activePlanarIx + 1u).c_str()); + ImGui::Separator(); { auto* fps = dynamic_cast(boundCamera); @@ -1703,8 +1627,6 @@ class UISampleApp final : public examples::SimpleWindowedApplication const auto& orientation = gimbal.getOrientation(); const auto viewMatrix = getCastedMatrix(gimbal.getViewMatrix()); - ImGui::Text("Type: %s", boundCamera->getIdentifier().data()); - ImGui::Separator(); addMatrixTable("Position", ("PositionTable_" + activePlanarIxString).c_str(), 1, 3, &position[0], false); addMatrixTable("Orientation (Quaternion)", ("OrientationTable_" + activePlanarIxString).c_str(), 1, 4, &orientation[0], false); addMatrixTable("View Matrix", ("ViewMatrixTable_" + activePlanarIxString).c_str(), 3, 4, &viewMatrix[0][0], false); @@ -1725,7 +1647,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication } } - inline void TransformEditor(float* m16TRSmatrix, IGimbalController::input_imguizmo_event_t* deltaTRS = nullptr) + inline void TransformEditor() { static float bounds[] = { -0.5f, -0.5f, -0.5f, 0.5f, 0.5f, 0.5f }; static float boundsSnap[] = { 0.1f, 0.1f, 0.1f }; @@ -1735,13 +1657,48 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGui::SetNextWindowPos(ImVec2(10, 10), ImGuiCond_Appearing); ImGui::SetNextWindowSize(ImVec2(320, 340), ImGuiCond_Appearing); ImGui::Begin("TRS Editor"); - ImGui::SameLine(); ImGuiIO& io = ImGui::GetIO(); - ImGui::Text("X: %f Y: %f", io.MousePos.x, io.MousePos.y); - if (ImGuizmo::IsUsing()) - ImGui::Text("Using gizmo"); - ImGui::Separator(); + { + const size_t objectsCount = m_planarProjections.size() + 1u; + assert(objectsCount); + + std::vector sbels(objectsCount); + for (size_t i = 0; i < objectsCount; ++i) + sbels[i] = "Object " + std::to_string(i); + + std::vector labels(objectsCount); + for (size_t i = 0; i < objectsCount; ++i) + labels[i] = sbels[i].c_str(); + + int activeObject = boundCameraToManipulate ? static_cast(boundPlanarCameraIxToManipulate.value() + 1u) : 0; + if (ImGui::Combo("Active Object", &activeObject, labels.data(), static_cast(labels.size()))) + { + const auto newActiveObject = static_cast(activeObject); + + if (newActiveObject) // camera + { + boundPlanarCameraIxToManipulate = newActiveObject - 1u; + ICamera* const targetGimbalManipulationCamera = m_planarProjections[boundPlanarCameraIxToManipulate.value()]->getCamera(); + boundCameraToManipulate = smart_refctd_ptr(targetGimbalManipulationCamera); + } + else // gc model + { + boundPlanarCameraIxToManipulate = std::nullopt; + boundCameraToManipulate = nullptr; + } + } + } + + ImGuizmoModelM16InOut imguizmoModel; + + if (boundCameraToManipulate) + imguizmoModel.inTRS = gimbalToImguizmoTRS(getCastedMatrix(boundCameraToManipulate->getGimbal()())); + else + imguizmoModel.inTRS = transpose(getMatrix3x4As4x4(m_model)); + + imguizmoModel.outTRS = imguizmoModel.inTRS; + float* m16TRSmatrix = &imguizmoModel.outTRS[0][0]; std::string indent; if (boundCameraToManipulate) @@ -1750,6 +1707,35 @@ class UISampleApp final : public examples::SimpleWindowedApplication indent = "Geometry Creator Object"; ImGui::Text("Identifier: \"%s\"", indent.c_str()); + { + if (ImGuizmo::IsUsingAny()) + ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), "Gizmo: In Use"); + else + ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.0f, 1.0f), "Gizmo: Idle"); + + if (ImGui::IsItemHovered()) + { + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.2f, 0.2f, 0.2f, 0.8f)); + ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.5f); + + ImVec2 mousePos = ImGui::GetMousePos(); + ImGui::SetNextWindowPos(ImVec2(mousePos.x + 10, mousePos.y + 10), ImGuiCond_Always); + + ImGui::Begin("HoverOverlay", nullptr, + ImGuiWindowFlags_NoDecoration | + ImGuiWindowFlags_AlwaysAutoResize | + ImGuiWindowFlags_NoSavedSettings); + + ImGui::Text("Right-click and drag on the gizmo to manipulate the object."); + + ImGui::End(); + + ImGui::PopStyleVar(); + ImGui::PopStyleColor(2); + } + } + ImGui::Separator(); if (!boundCameraToManipulate) @@ -1794,9 +1780,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication float32_t3 matrixTranslation, matrixRotation, matrixScale; IGimbalController::input_imguizmo_event_t decomposed, recomposed; - - if (deltaTRS) - *deltaTRS = IGimbalController::input_imguizmo_event_t(1); + imguizmoModel.outDeltaTRS = IGimbalController::input_imguizmo_event_t(1); ImGuizmo::DecomposeMatrixToComponents(m16TRSmatrix, &matrixTranslation[0], &matrixRotation[0], &matrixScale[0]); decomposed = *reinterpret_cast(m16TRSmatrix); @@ -1821,8 +1805,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication recomposed = *reinterpret_cast(m16TRSmatrix); // TODO AND NOTE: I only take care of translate part temporary! - if(deltaTRS) - deltaTRS->operator[](3) = recomposed[3] - decomposed[3]; + imguizmoModel.outDeltaTRS[3] = recomposed[3] - decomposed[3]; if (mCurrentGizmoOperation != ImGuizmo::SCALE) { @@ -1849,6 +1832,68 @@ class UISampleApp final : public examples::SimpleWindowedApplication } ImGui::End(); + + { + // generate virtual events given delta TRS matrix + if (boundCameraToManipulate) + { + { + static std::vector virtualEvents(0x45); + + if (not enableActiveCameraMovement) + { + uint32_t vCount = {}; + + boundCameraToManipulate->beginInputProcessing(m_nextPresentationTimestamp); + { + boundCameraToManipulate->process(nullptr, vCount); + + if (virtualEvents.size() < vCount) + virtualEvents.resize(vCount); + + IGimbalController::SUpdateParameters params; + params.imguizmoEvents = { { imguizmoModel.outDeltaTRS } }; + boundCameraToManipulate->process(virtualEvents.data(), vCount, params); + } + boundCameraToManipulate->endInputProcessing(); + + // I start to think controller should be able to set sensitivity to scale magnitudes of generated events + // in order for camera to not keep any magnitude scalars like move or rotation speed scales + + if (vCount) + { + auto* fps = dynamic_cast(boundCameraToManipulate.get()); + + float pmSpeed = 1.f; + float prSpeed = 1.f; + + if (fps) + { + pmSpeed = fps->getMoveSpeedScale(); + prSpeed = fps->getRotationSpeedScale(); + + fps->setMoveSpeedScale(1); + fps->setRotationSpeedScale(1); + } + + // NOTE: generated events from ImGuizmo controller are always in world space! + boundCameraToManipulate->manipulate({ virtualEvents.data(), vCount }, ICamera::World); + + if (fps) + { + fps->setMoveSpeedScale(pmSpeed); + fps->setRotationSpeedScale(prSpeed); + } + } + } + } + } + else + { + // for scene demo model full affine transformation without limits is assumed + m_model = float32_t3x4(transpose(imguizmoModel.outTRS)); + } + } } inline void addMatrixTable(const char* topText, const char* tableName, int rows, int columns, const float* pointer, bool withSeparator = true) @@ -1877,6 +1922,19 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGui::Separator(); } + // TODO: need to inspect where I'm wrong, workaround + inline float32_t4x4 gimbalToImguizmoTRS(const float32_t3x4& nblGimbalTrs) + { + // *do not transpose whole matrix*, only the translate part + float32_t4x4 trs = getMatrix3x4As4x4(nblGimbalTrs); + trs[3] = float32_t4(nblGimbalTrs[0][3], nblGimbalTrs[1][3], nblGimbalTrs[2][3], 1.f); + trs[0][3] = 0.f; + trs[1][3] = 0.f; + trs[2][3] = 0.f; + + return trs; + }; + std::chrono::seconds timeout = std::chrono::seconds(0x7fffFFFFu); clock_t::time_point start; @@ -1929,13 +1987,15 @@ class UISampleApp final : public examples::SimpleWindowedApplication nbl::hlsl::float32_t3x4 m_model = nbl::hlsl::float32_t3x4(1.f); // if we had working IObjectTransform or something similar then it would be it instead, it is "last manipulated object" I need for TRS editor - nbl::core::smart_refctd_ptr boundCameraToManipulate; + // in reality we should store range of those IObjectTransforem interface range & index to object representing last manipulated one + nbl::core::smart_refctd_ptr boundCameraToManipulate = nullptr; + std::optional boundPlanarCameraIxToManipulate = std::nullopt; std::vector> m_planarProjections; bool enableActiveCameraMovement = false; - struct SceneControlBinding + struct windowControlBinding { nbl::core::smart_refctd_ptr scene; @@ -1971,7 +2031,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication }; static constexpr inline auto MaxSceneFBOs = 2u; - std::array sceneControlBinding; + std::array windowControlBinding; uint32_t activeRenderWindowIx = 0u; // UI font atlas + viewport FBO color attachment textures From a6ac63556a9f2c92ebb3b7a2eaeac64fd6cdd86d Mon Sep 17 00:00:00 2001 From: YasInvolved Date: Sun, 22 Dec 2024 13:06:41 +0100 Subject: [PATCH 073/205] load default config if file specified in parameter is not found --- 61_UI/main.cpp | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/61_UI/main.cpp b/61_UI/main.cpp index 3a45eb7a1..e125206d3 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -10,7 +10,7 @@ using json = nlohmann::json; #include "keysmapping.hpp" #include "camera/CCubeProjection.hpp" #include "glm/glm/ext/matrix_clip_space.hpp" // TODO: TESTING -#include "nbl/builtin/CArchive.h" +#include "nbl/this_example/builtin/CArchive.h" using planar_projections_range_t = std::vector; using planar_projection_t = CPlanarProjection; @@ -478,8 +478,16 @@ class UISampleApp final : public examples::SimpleWindowedApplication std::ifstream file(cameraJsonFile.c_str()); if (!file.is_open()) { - m_logger->log("Cannot open input \"%s\" json file. Switching to default config."); - return false; + m_logger->log("Cannot open input \"%s\" json file. Switching to default config.", ILogger::ELL_WARNING, cameraJsonFile.c_str()); + auto assets = make_smart_refctd_ptr(smart_refctd_ptr(m_logger)); + auto pFile = assets->getFile("cameras.json", IFile::ECF_READ, ""); + + string config; + IFile::success_t result; + config.resize(pFile->getSize()); + pFile->read(result, config.data(), 0, pFile->getSize()); + + j = json::parse(config); } else { From 5f9ff5ba2d93a85ec06511fff35c1e0bad953412 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Sun, 22 Dec 2024 13:23:25 +0100 Subject: [PATCH 074/205] make DepthFboAttachmentFormat (gc scene fbo) EF_D32_SFLOAT to increase precision --- common/include/CGeomtryCreatorScene.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/include/CGeomtryCreatorScene.hpp b/common/include/CGeomtryCreatorScene.hpp index 444a58b4d..7180d4019 100644 --- a/common/include/CGeomtryCreatorScene.hpp +++ b/common/include/CGeomtryCreatorScene.hpp @@ -20,7 +20,7 @@ using namespace system struct Traits { static constexpr auto DefaultFramebufferW = 1280u, DefaultFramebufferH = 720u; - static constexpr auto ColorFboAttachmentFormat = nbl::asset::EF_R8G8B8A8_SRGB, DepthFboAttachmentFormat = nbl::asset::EF_D16_UNORM; + static constexpr auto ColorFboAttachmentFormat = nbl::asset::EF_R8G8B8A8_SRGB, DepthFboAttachmentFormat = nbl::asset::EF_D32_SFLOAT; static constexpr auto Samples = nbl::video::IGPUImage::ESCF_1_BIT; static constexpr nbl::video::IGPUCommandBuffer::SClearColorValue clearColor = { .float32 = {0.f,0.f,0.f,1.f} }; static constexpr nbl::video::IGPUCommandBuffer::SClearDepthStencilValue clearDepth = { .depth = 0.f }; From 5f3fd6f9213672bdb4f0174f5bad842a815aaceb Mon Sep 17 00:00:00 2001 From: YasInvolved Date: Sun, 22 Dec 2024 15:24:34 +0100 Subject: [PATCH 075/205] split viewports and planars in JSON --- 61_UI/app_resources/cameras.json | 67 +++++++++----------------------- 61_UI/main.cpp | 62 ++++++++++++++++++++++++++++- 2 files changed, 80 insertions(+), 49 deletions(-) diff --git a/61_UI/app_resources/cameras.json b/61_UI/app_resources/cameras.json index 80e09d059..5f8d5b489 100644 --- a/61_UI/app_resources/cameras.json +++ b/61_UI/app_resources/cameras.json @@ -31,62 +31,33 @@ } ], "viewports": [ + { + "projection": 0, + "controllers": { + "keyboard": 0, + "mouse": 0 + } + }, + { + "projection": 1, + "controllers": { + "keyboard": 1, + "mouse": 0 + } + } + ], + "planars": [ { "camera": 0, - "planarControllerSet": [ - { - "projection": 0, - "controllers": { - "keyboard": 0, - "mouse": 0 - } - }, - { - "projection": 1, - "controllers": { - "keyboard": 1, - "mouse": 0 - } - } - ] + "viewports": [0, 1] }, { "camera": 1, - "planarControllerSet": [ - { - "projection": 0, - "controllers": { - "keyboard": 0, - "mouse": 0 - } - }, - { - "projection": 1, - "controllers": { - "keyboard": 1, - "mouse": 0 - } - } - ] + "viewports": [0, 1] }, { "camera": 2, - "planarControllerSet": [ - { - "projection": 0, - "controllers": { - "keyboard": 0, - "mouse": 0 - } - }, - { - "projection": 1, - "controllers": { - "keyboard": 1, - "mouse": 0 - } - } - ] + "viewports": [0, 1] } ], "controllers": { diff --git a/61_UI/main.cpp b/61_UI/main.cpp index 0233717fd..07d981826 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -690,7 +690,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication return false; } - if (j.contains("viewports")) + /*if (j.contains("viewports")) { for (const auto& jViewport : j["viewports"]) { @@ -737,6 +737,66 @@ class UISampleApp final : public examples::SimpleWindowedApplication { std::cerr << "Expected \"viewports\" keyword in JSON!" << std::endl; return false; + }*/ + + if (j.contains("viewports") && j.contains("planars")) + { + for (const auto& jPlanar : j["planars"]) + { + if (!jPlanar.contains("camera")) + { + logFail("Expected \"camera\" value in planar object"); + return false; + } + + if (!jPlanar.contains("viewports")) + { + logFail("Expected \"viewports\" list in planar object"); + return false; + } + + const auto cameraIx = jPlanar["camera"].get(); + auto boundViewports = jPlanar["viewports"].get>(); + + auto& planars = m_planarProjections.emplace_back() = planar_projection_t::create(smart_refctd_ptr(cameras[cameraIx])); + for (const auto viewportIx : boundViewports) + { + auto& viewport = j["viewports"][viewportIx]; + if (!viewport.contains("projection") || !viewport.contains("controllers")) + { + logFail("\"projection\" or \"controllers\" missing in viewport object index %d", viewportIx); + return false; + } + + if (!viewport["controllers"].contains("keyboard")) + { + logFail("\"keyboard\" value missing in controllers in viewport object index %d", viewportIx); + return false; + } + + if (!viewport["controllers"].contains("mouse")) + { + logFail("\"mouse\" value missing in controllers in viewport object index %d", viewportIx); + return false; + } + + auto keyboardIx = viewport["controllers"]["keyboard"].get(); + auto mouseIx = viewport["controllers"]["mouse"].get(); + + auto projectionIx = viewport["projection"].get(); + auto keyboardControllerIx = viewport["controllers"]["keyboard"].get(); + auto mouseControllerIx = viewport["controllers"]["mouse"].get(); + + auto& projection = planars->getPlanarProjections().emplace_back(projections[projectionIx]); + projection.updateKeyboardMapping([&](auto& map) { map = controllers.keyboard[keyboardControllerIx]; }); + projection.updateMouseMapping([&](auto& map) { map = controllers.mouse[mouseControllerIx]; }); + } + } + } + else + { + logFail("Expected \"viewports\" and \"planars\" lists in JSON"); + return false; } if (m_planarProjections.size() < windowControlBinding.size()) From ecb14f5c7dabe1a78005f56393b80a25d78f4c5b Mon Sep 17 00:00:00 2001 From: YasInvolved Date: Sun, 22 Dec 2024 15:41:13 +0100 Subject: [PATCH 076/205] replace cerr with logFail to improve logging --- 61_UI/main.cpp | 87 +++++++++++--------------------------------------- 1 file changed, 19 insertions(+), 68 deletions(-) diff --git a/61_UI/main.cpp b/61_UI/main.cpp index 07d981826..d142192d6 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -459,7 +459,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication if (!resources) { - m_logger->log("Could not create geometry creator gpu resources!", ILogger::ELL_ERROR); + logFail("Could not create geometry creator gpu resources!"); return false; } @@ -472,7 +472,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication if (!scene) { - m_logger->log("Could not create geometry creator scene[%d]!", ILogger::ELL_ERROR, i); + logFail("Could not create geometry creator scene[%d]!", i); return false; } } @@ -514,13 +514,13 @@ class UISampleApp final : public examples::SimpleWindowedApplication { if (!jCamera.contains("position")) { - std::cerr << "Expected \"position\" keyword for camera definition!"; + logFail("Expected \"position\" keyword for camera definition!"); return false; } if (!jCamera.contains("orientation")) { - std::cerr << "Expected \"orientation\" keyword for camera definition!"; + logFail("Expected \"orientation\" keyword for camera definition!"); return false; } @@ -544,13 +544,13 @@ class UISampleApp final : public examples::SimpleWindowedApplication } else { - std::cerr << "Unsupported camera type!"; + logFail("Unsupported camera type!"); return false; } } else { - std::cerr << "Expected \"type\" keyword for camera definition!"; + logFail("Expected \"type\" keyword for camera definition!"); return false; } } @@ -564,13 +564,13 @@ class UISampleApp final : public examples::SimpleWindowedApplication if (!jProjection.contains("zNear")) { - "Expected \"zNear\" keyword for planar projection definition!"; + logFail("Expected \"zNear\" keyword for planar projection definition!"); return false; } if (!jProjection.contains("zFar")) { - "Expected \"zFar\" keyword for planar projection definition!"; + logFail("Expected \"zFar\" keyword for planar projection definition!"); return false; } @@ -581,7 +581,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication { if (!jProjection.contains("fov")) { - "Expected \"fov\" keyword for planar perspective projection definition!"; + logFail("Expected \"fov\" keyword for planar perspective projection definition!"); return false; } @@ -593,7 +593,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication { if (!jProjection.contains("orthoWidth")) { - "Expected \"orthoWidth\" keyword for planar orthographic projection definition!"; + logFail("Expected \"orthoWidth\" keyword for planar orthographic projection definition!"); return false; } @@ -602,7 +602,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication } else { - std::cerr << "Unsupported projection!"; + logFail("Unsupported projection!"); return false; } } @@ -631,7 +631,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication if (nativeCode == EKC_NONE) { - std::cerr << "Invalid native key \"" << key.c_str() << "\" code mapping for keyboard controller!" << std::endl; + logFail("Invalid native key \"%s\" code mapping for keyboard controller", key.c_str()); return false; } @@ -640,14 +640,14 @@ class UISampleApp final : public examples::SimpleWindowedApplication } else { - std::cerr << "Expected \"mappings\" keyword for keyboard controller definition!" << std::endl; + logFail("Expected \"mappings\" keyword for keyboard controller definition!"); return false; } } } else { - std::cerr << "Expected \"keyboard\" keyword in controllers definition!" << std::endl; + logFail("Expected \"keyboard\" keyword in controllers definition!"); return false; } @@ -664,7 +664,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication if (nativeCode == EMC_NONE) { - std::cerr << "Invalid native key \"" << key.c_str() << "\" code mapping for mouse controller!" << std::endl; + logFail("Invalid native key \"%s\" code mapping for mouse controller", key.c_str()); return false; } @@ -673,72 +673,23 @@ class UISampleApp final : public examples::SimpleWindowedApplication } else { - std::cerr << "Expected \"mappings\" keyword for mouse controller definition!" << std::endl; + logFail("Expected \"mappings\" keyword for mouse controller definition!"); return false; } } } else { - std::cerr << "Expected \"mouse\" keyword in controllers definition!" << std::endl; + logFail("Expected \"mouse\" keyword in controllers definition"); return false; } } else { - std::cerr << "Expected \"controllers\" keyword in JSON!" << std::endl; + logFail("Expected \"controllers\" keyword in controllers JSON"); return false; } - /*if (j.contains("viewports")) - { - for (const auto& jViewport : j["viewports"]) - { - if (!jViewport.contains("camera")) - { - std::cerr << "Expected \"camera\" keyword in viewport definition!" << std::endl; - return false; - } - - const auto cameraIx = jViewport["camera"].get(); - auto& planars = m_planarProjections.emplace_back() = planar_projection_t::create(smart_refctd_ptr(cameras[cameraIx])); - - if (!jViewport.contains("planarControllerSet")) - { - std::cerr << "Expected \"planarControllerSet\" keyword in viewport definition!" << std::endl; - return false; - } - - for (const auto& jPlanarController : jViewport["planarControllerSet"]) - { - if (!jPlanarController.contains("projection")) - { - std::cerr << "Expected \"projection\" keyword in planarControllerSet!" << std::endl; - return false; - } - - if (!jPlanarController.contains("controllers")) - { - std::cerr << "Expected \"controllers\" keyword in planarControllerSet!" << std::endl; - return false; - } - - auto projectionIx = jPlanarController["projection"].get(); - auto keyboardControllerIx = jPlanarController["controllers"]["keyboard"].get(); - auto mouseControllerIx = jPlanarController["controllers"]["mouse"].get(); - - auto& projection = planars->getPlanarProjections().emplace_back(projections[projectionIx]); - projection.updateKeyboardMapping([&](auto& map) { map = controllers.keyboard[keyboardControllerIx]; }); - projection.updateMouseMapping([&](auto& map) { map = controllers.mouse[mouseControllerIx]; }); - } - } - } - else - { - std::cerr << "Expected \"viewports\" keyword in JSON!" << std::endl; - return false; - }*/ - if (j.contains("viewports") && j.contains("planars")) { for (const auto& jPlanar : j["planars"]) @@ -802,7 +753,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication if (m_planarProjections.size() < windowControlBinding.size()) { // TODO, temporary assuming it, I'm not going to implement each possible case now - std::cerr << "Expected at least " << std::to_string(windowControlBinding.size()) << " planars!" << std::endl; + logFail("Expected at least %d planars", windowControlBinding.size()); return false; } From ed4278903d123d0291ed1906f7e7e924870224a5 Mon Sep 17 00:00:00 2001 From: YasInvolved Date: Sun, 22 Dec 2024 15:49:41 +0100 Subject: [PATCH 077/205] moved data parsing to before gfx initialization --- 61_UI/main.cpp | 470 ++++++++++++++++++++++++------------------------- 1 file changed, 234 insertions(+), 236 deletions(-) diff --git a/61_UI/main.cpp b/61_UI/main.cpp index d142192d6..195505e11 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -275,219 +275,12 @@ class UISampleApp final : public examples::SimpleWindowedApplication if (!base_t::onAppInitialized(std::move(system))) return false; - // Create asset manager - m_assetManager = make_smart_refctd_ptr(smart_refctd_ptr(m_system)); - - // First create the resources that don't depend on a swapchain - m_semaphore = m_device->createSemaphore(m_realFrameIx); - if (!m_semaphore) - return logFail("Failed to Create a Semaphore!"); - - // The nice thing about having a triple buffer is that you don't need to do acrobatics to account for the formats available to the surface. - // You can transcode to the swapchain's format while copying, and I actually recommend to do surface rotation, tonemapping and OETF application there. - const auto format = asset::EF_R8G8B8A8_SRGB; - // Could be more clever and use the copy Triple Buffer to Swapchain as an opportunity to do a MSAA resolve or something - const auto samples = IGPUImage::ESCF_1_BIT; - - // Create the renderpass - { - 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, // because we clear we don't care about contents when we grab the triple buffer img again - /*.finalLayout = */IGPUImage::LAYOUT::TRANSFER_SRC_OPTIMAL // put it already in the correct layout for the blit operation - }}, - IGPURenderpass::SCreationParams::ColorAttachmentsEnd - }; - IGPURenderpass::SCreationParams::SSubpassDescription subpasses[] = { - {}, - IGPURenderpass::SCreationParams::SubpassesEnd - }; - subpasses[0].colorAttachments[0] = { .render = {.attachmentIndex = 0,.layout = IGPUImage::LAYOUT::ATTACHMENT_OPTIMAL} }; - // We actually need external dependencies to ensure ordering of the Implicit Layout Transitions relative to the semaphore signals - IGPURenderpass::SCreationParams::SSubpassDependency dependencies[] = { - // wipe-transition to ATTACHMENT_OPTIMAL - { - .srcSubpass = IGPURenderpass::SCreationParams::SSubpassDependency::External, - .dstSubpass = 0, - .memoryBarrier = { - // we can have NONE as Sources because the semaphore wait is ALL_COMMANDS - // https://github.com/KhronosGroup/Vulkan-Docs/issues/2319 - .dstStageMask = asset::PIPELINE_STAGE_FLAGS::COLOR_ATTACHMENT_OUTPUT_BIT, - .dstAccessMask = asset::ACCESS_FLAGS::COLOR_ATTACHMENT_WRITE_BIT - } - // leave view offsets and flags default - }, - // ATTACHMENT_OPTIMAL to PRESENT_SRC - { - .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 - // we can have NONE as the Destinations because the semaphore signal is ALL_COMMANDS - // https://github.com/KhronosGroup/Vulkan-Docs/issues/2319 - } - // leave view offsets and flags default - }, - IGPURenderpass::SCreationParams::DependenciesEnd - }; - - IGPURenderpass::SCreationParams params = {}; - params.colorAttachments = colorAttachments; - params.subpasses = subpasses; - params.dependencies = dependencies; - m_renderpass = m_device->createRenderpass(params); - if (!m_renderpass) - return logFail("Failed to Create a Renderpass!"); - } - - // We just live life in easy mode and have the Swapchain Creation Parameters get deduced from the surface. - // We don't need any control over the format of the swapchain because we'll be only using Renderpasses this time! - // TODO: improve the queue allocation/choice and allocate a dedicated presentation queue to improve responsiveness and race to present. - if (!m_surface || !m_surface->init(m_surface->pickQueue(m_device.get()), std::make_unique(), {})) - return logFail("Failed to Create a Swapchain!"); - - // Normally you'd want to recreate these images whenever the swapchain is resized in some increment, like 64 pixels or something. - // But I'm super lazy here and will just create "worst case sized images" and waste all the VRAM I can get. - const auto dpyInfo = m_winMgr->getPrimaryDisplayInfo(); - for (auto i = 0; i < MaxFramesInFlight; i++) - { - auto& image = m_tripleBuffers[i]; - { - IGPUImage::SCreationParams params = {}; - params = asset::IImage::SCreationParams{ - .type = IGPUImage::ET_2D, - .samples = samples, - .format = format, - .extent = {dpyInfo.resX,dpyInfo.resY,1}, - .mipLevels = 1, - .arrayLayers = 1, - .flags = IGPUImage::ECF_NONE, - // in this example I'll be using a renderpass to clear the image, and then a blit to copy it to the swapchain - .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!"); - - // use dedicated allocations, we have plenty of allocations left, even on Win32 - 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()); - - // create framebuffers for the images - { - auto imageView = m_device->createImageView({ - .flags = IGPUImageView::ECF_NONE, - // give it a Transfer SRC usage flag so we can transition to the Tranfer SRC layout with End Renderpass - .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); - } - } - - // This time we'll create all CommandBuffers from one CommandPool, to keep life simple. However the Pool must support individually resettable CommandBuffers - // because they cannot be pre-recorded because the fraembuffers/swapchain images they use will change when a swapchain recreates. - 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!"); - - // UI - { - { - nbl::ext::imgui::UI::SCreationParameters params; - params.resources.texturesInfo = { .setIx = 0u, .bindingIx = 0u }; - params.resources.samplersInfo = { .setIx = 0u, .bindingIx = 1u }; - params.assetManager = m_assetManager; - 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.streamingBuffer = nullptr; - params.subpassIx = 0u; - params.transfer = getTransferUpQueue(); - params.utilities = m_utils; - - m_ui.manager = nbl::ext::imgui::UI::create(std::move(params)); - } - - if (!m_ui.manager) - return false; - - // note that we use default layout provided by our extension, but you are free to create your own by filling nbl::ext::imgui::UI::S_CREATION_PARAMETERS::resources - const auto* descriptorSetLayout = m_ui.manager->getPipeline()->getLayout()->getDescriptorSetLayout(0u); - - IDescriptorPool::SCreateInfo descriptorPoolInfo = {}; - descriptorPoolInfo.maxDescriptorCount[static_cast(asset::IDescriptor::E_TYPE::ET_SAMPLER)] = (uint32_t)nbl::ext::imgui::UI::DefaultSamplerIx::COUNT; - descriptorPoolInfo.maxDescriptorCount[static_cast(asset::IDescriptor::E_TYPE::ET_SAMPLED_IMAGE)] = TotalUISampleTexturesAmount; - descriptorPoolInfo.maxSets = 1u; - descriptorPoolInfo.flags = IDescriptorPool::E_CREATE_FLAGS::ECF_UPDATE_AFTER_BIND_BIT; - - m_descriptorSetPool = m_device->createDescriptorPool(std::move(descriptorPoolInfo)); - assert(m_descriptorSetPool); - - m_descriptorSetPool->createDescriptorSets(1u, &descriptorSetLayout, &m_ui.descriptorSet); - assert(m_ui.descriptorSet); - - m_ui.manager->registerListener([this]() -> void { imguiListen(); }); - } - - // Geometry Creator Render Scene FBOs - { - resources = ResourcesBundle::create(m_device.get(), m_logger.get(), getGraphicsQueue(), m_assetManager->getGeometryCreator()); - - if (!resources) - { - logFail("Could not create geometry creator gpu resources!"); - return false; - } - - const auto dpyInfo = m_winMgr->getPrimaryDisplayInfo(); - - for (uint32_t i = 0u; i < windowControlBinding.size(); ++i) - { - auto& scene = windowControlBinding[i].scene; - scene = CScene::create(smart_refctd_ptr(m_device), smart_refctd_ptr(m_logger), getGraphicsQueue(), smart_refctd_ptr(resources), dpyInfo.resX, dpyInfo.resY); - - if (!scene) - { - logFail("Could not create geometry creator scene[%d]!", i); - return false; - } - } - } - - oracle.reportBeginFrameRecord(); - - // json file with camera inputs - // @Yas: TODO: THIS NEEDS BETTER VALIDATION AND LOGS ON FAILURE (+ status logs would be nice)!!! { const auto cameraJsonFile = program.get("--file"); json j; std::ifstream file(cameraJsonFile.c_str()); - if (!file.is_open()) + if (!file.is_open()) { m_logger->log("Cannot open input \"%s\" json file. Switching to default config.", ILogger::ELL_WARNING, cameraJsonFile.c_str()); auto assets = make_smart_refctd_ptr(smart_refctd_ptr(m_logger)); @@ -506,7 +299,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication } std::vector> cameras; - for (const auto& jCamera : j["cameras"]) + for (const auto& jCamera : j["cameras"]) { if (jCamera.contains("type")) { @@ -524,21 +317,21 @@ class UISampleApp final : public examples::SimpleWindowedApplication return false; } - auto position = [&]() - { - auto jret = jCamera["position"].get>(); - return float32_t3(jret[0], jret[1], jret[2]); - }(); + auto position = [&]() + { + auto jret = jCamera["position"].get>(); + return float32_t3(jret[0], jret[1], jret[2]); + }(); auto orientation = [&]() - { - auto jret = jCamera["orientation"].get>(); + { + auto jret = jCamera["orientation"].get>(); - // order important for glm::quat, - // the ctor is GLM_FUNC_QUALIFIER GLM_CONSTEXPR qua::qua(T _w, T _x, T _y, T _z) - // but memory layout (and json) is x,y,z,w - return glm::quat(jret[3], jret[0], jret[1], jret[2]); - }(); + // order important for glm::quat, + // the ctor is GLM_FUNC_QUALIFIER GLM_CONSTEXPR qua::qua(T _w, T _x, T _y, T _z) + // but memory layout (and json) is x,y,z,w + return glm::quat(jret[3], jret[0], jret[1], jret[2]); + }(); cameras.emplace_back() = make_smart_refctd_ptr(position, orientation); } @@ -614,18 +407,18 @@ class UISampleApp final : public examples::SimpleWindowedApplication std::vector mouse; } controllers; - if (j.contains("controllers")) + if (j.contains("controllers")) { const auto& jControllers = j["controllers"]; - if (jControllers.contains("keyboard")) + if (jControllers.contains("keyboard")) { - for (const auto& jKeyboard : jControllers["keyboard"]) + for (const auto& jKeyboard : jControllers["keyboard"]) { - if (jKeyboard.contains("mappings")) + if (jKeyboard.contains("mappings")) { auto& controller = controllers.keyboard.emplace_back(); - for (const auto& [key, value] : jKeyboard["mappings"].items()) + for (const auto& [key, value] : jKeyboard["mappings"].items()) { const auto nativeCode = stringToKeyCode(key.c_str()); @@ -638,27 +431,27 @@ class UISampleApp final : public examples::SimpleWindowedApplication controller[nativeCode] = CVirtualGimbalEvent::stringToVirtualEvent(value.get()); } } - else + else { logFail("Expected \"mappings\" keyword for keyboard controller definition!"); return false; } } } - else + else { logFail("Expected \"keyboard\" keyword in controllers definition!"); return false; } - if (jControllers.contains("mouse")) + if (jControllers.contains("mouse")) { - for (const auto& jMouse : jControllers["mouse"]) + for (const auto& jMouse : jControllers["mouse"]) { - if (jMouse.contains("mappings")) + if (jMouse.contains("mappings")) { auto& controller = controllers.mouse.emplace_back(); - for (const auto& [key, value] : jMouse["mappings"].items()) + for (const auto& [key, value] : jMouse["mappings"].items()) { const auto nativeCode = stringToMouseCode(key.c_str()); @@ -671,20 +464,20 @@ class UISampleApp final : public examples::SimpleWindowedApplication controller[nativeCode] = CVirtualGimbalEvent::stringToVirtualEvent(value.get()); } } - else + else { logFail("Expected \"mappings\" keyword for mouse controller definition!"); return false; } } } - else + else { logFail("Expected \"mouse\" keyword in controllers definition"); return false; } } - else + else { logFail("Expected \"controllers\" keyword in controllers JSON"); return false; @@ -762,7 +555,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication for (uint32_t i = 0u; i < windowControlBinding.size(); ++i) { auto& binding = windowControlBinding[i]; - + auto& planar = m_planarProjections[binding.activePlanarIx = 0]; binding.pickDefaultProjections(planar->getPlanarProjections()); @@ -773,6 +566,211 @@ class UISampleApp final : public examples::SimpleWindowedApplication } } + // Create asset manager + m_assetManager = make_smart_refctd_ptr(smart_refctd_ptr(m_system)); + + // First create the resources that don't depend on a swapchain + m_semaphore = m_device->createSemaphore(m_realFrameIx); + if (!m_semaphore) + return logFail("Failed to Create a Semaphore!"); + + // The nice thing about having a triple buffer is that you don't need to do acrobatics to account for the formats available to the surface. + // You can transcode to the swapchain's format while copying, and I actually recommend to do surface rotation, tonemapping and OETF application there. + const auto format = asset::EF_R8G8B8A8_SRGB; + // Could be more clever and use the copy Triple Buffer to Swapchain as an opportunity to do a MSAA resolve or something + const auto samples = IGPUImage::ESCF_1_BIT; + + // Create the renderpass + { + 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, // because we clear we don't care about contents when we grab the triple buffer img again + /*.finalLayout = */IGPUImage::LAYOUT::TRANSFER_SRC_OPTIMAL // put it already in the correct layout for the blit operation + }}, + IGPURenderpass::SCreationParams::ColorAttachmentsEnd + }; + IGPURenderpass::SCreationParams::SSubpassDescription subpasses[] = { + {}, + IGPURenderpass::SCreationParams::SubpassesEnd + }; + subpasses[0].colorAttachments[0] = { .render = {.attachmentIndex = 0,.layout = IGPUImage::LAYOUT::ATTACHMENT_OPTIMAL} }; + // We actually need external dependencies to ensure ordering of the Implicit Layout Transitions relative to the semaphore signals + IGPURenderpass::SCreationParams::SSubpassDependency dependencies[] = { + // wipe-transition to ATTACHMENT_OPTIMAL + { + .srcSubpass = IGPURenderpass::SCreationParams::SSubpassDependency::External, + .dstSubpass = 0, + .memoryBarrier = { + // we can have NONE as Sources because the semaphore wait is ALL_COMMANDS + // https://github.com/KhronosGroup/Vulkan-Docs/issues/2319 + .dstStageMask = asset::PIPELINE_STAGE_FLAGS::COLOR_ATTACHMENT_OUTPUT_BIT, + .dstAccessMask = asset::ACCESS_FLAGS::COLOR_ATTACHMENT_WRITE_BIT + } + // leave view offsets and flags default + }, + // ATTACHMENT_OPTIMAL to PRESENT_SRC + { + .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 + // we can have NONE as the Destinations because the semaphore signal is ALL_COMMANDS + // https://github.com/KhronosGroup/Vulkan-Docs/issues/2319 + } + // leave view offsets and flags default + }, + IGPURenderpass::SCreationParams::DependenciesEnd + }; + + IGPURenderpass::SCreationParams params = {}; + params.colorAttachments = colorAttachments; + params.subpasses = subpasses; + params.dependencies = dependencies; + m_renderpass = m_device->createRenderpass(params); + if (!m_renderpass) + return logFail("Failed to Create a Renderpass!"); + } + + // We just live life in easy mode and have the Swapchain Creation Parameters get deduced from the surface. + // We don't need any control over the format of the swapchain because we'll be only using Renderpasses this time! + // TODO: improve the queue allocation/choice and allocate a dedicated presentation queue to improve responsiveness and race to present. + if (!m_surface || !m_surface->init(m_surface->pickQueue(m_device.get()), std::make_unique(), {})) + return logFail("Failed to Create a Swapchain!"); + + // Normally you'd want to recreate these images whenever the swapchain is resized in some increment, like 64 pixels or something. + // But I'm super lazy here and will just create "worst case sized images" and waste all the VRAM I can get. + const auto dpyInfo = m_winMgr->getPrimaryDisplayInfo(); + for (auto i = 0; i < MaxFramesInFlight; i++) + { + auto& image = m_tripleBuffers[i]; + { + IGPUImage::SCreationParams params = {}; + params = asset::IImage::SCreationParams{ + .type = IGPUImage::ET_2D, + .samples = samples, + .format = format, + .extent = {dpyInfo.resX,dpyInfo.resY,1}, + .mipLevels = 1, + .arrayLayers = 1, + .flags = IGPUImage::ECF_NONE, + // in this example I'll be using a renderpass to clear the image, and then a blit to copy it to the swapchain + .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!"); + + // use dedicated allocations, we have plenty of allocations left, even on Win32 + 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()); + + // create framebuffers for the images + { + auto imageView = m_device->createImageView({ + .flags = IGPUImageView::ECF_NONE, + // give it a Transfer SRC usage flag so we can transition to the Tranfer SRC layout with End Renderpass + .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); + } + } + + // This time we'll create all CommandBuffers from one CommandPool, to keep life simple. However the Pool must support individually resettable CommandBuffers + // because they cannot be pre-recorded because the fraembuffers/swapchain images they use will change when a swapchain recreates. + 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!"); + + // UI + { + { + nbl::ext::imgui::UI::SCreationParameters params; + params.resources.texturesInfo = { .setIx = 0u, .bindingIx = 0u }; + params.resources.samplersInfo = { .setIx = 0u, .bindingIx = 1u }; + params.assetManager = m_assetManager; + 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.streamingBuffer = nullptr; + params.subpassIx = 0u; + params.transfer = getTransferUpQueue(); + params.utilities = m_utils; + + m_ui.manager = nbl::ext::imgui::UI::create(std::move(params)); + } + + if (!m_ui.manager) + return false; + + // note that we use default layout provided by our extension, but you are free to create your own by filling nbl::ext::imgui::UI::S_CREATION_PARAMETERS::resources + const auto* descriptorSetLayout = m_ui.manager->getPipeline()->getLayout()->getDescriptorSetLayout(0u); + + IDescriptorPool::SCreateInfo descriptorPoolInfo = {}; + descriptorPoolInfo.maxDescriptorCount[static_cast(asset::IDescriptor::E_TYPE::ET_SAMPLER)] = (uint32_t)nbl::ext::imgui::UI::DefaultSamplerIx::COUNT; + descriptorPoolInfo.maxDescriptorCount[static_cast(asset::IDescriptor::E_TYPE::ET_SAMPLED_IMAGE)] = TotalUISampleTexturesAmount; + descriptorPoolInfo.maxSets = 1u; + descriptorPoolInfo.flags = IDescriptorPool::E_CREATE_FLAGS::ECF_UPDATE_AFTER_BIND_BIT; + + m_descriptorSetPool = m_device->createDescriptorPool(std::move(descriptorPoolInfo)); + assert(m_descriptorSetPool); + + m_descriptorSetPool->createDescriptorSets(1u, &descriptorSetLayout, &m_ui.descriptorSet); + assert(m_ui.descriptorSet); + + m_ui.manager->registerListener([this]() -> void { imguiListen(); }); + } + + // Geometry Creator Render Scene FBOs + { + resources = ResourcesBundle::create(m_device.get(), m_logger.get(), getGraphicsQueue(), m_assetManager->getGeometryCreator()); + + if (!resources) + { + logFail("Could not create geometry creator gpu resources!"); + return false; + } + + const auto dpyInfo = m_winMgr->getPrimaryDisplayInfo(); + + for (uint32_t i = 0u; i < windowControlBinding.size(); ++i) + { + auto& scene = windowControlBinding[i].scene; + scene = CScene::create(smart_refctd_ptr(m_device), smart_refctd_ptr(m_logger), getGraphicsQueue(), smart_refctd_ptr(resources), dpyInfo.resX, dpyInfo.resY); + + if (!scene) + { + logFail("Could not create geometry creator scene[%d]!", i); + 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(); From 3afb9c856ac966ee081917365a2848539bd82c74 Mon Sep 17 00:00:00 2001 From: YasInvolved Date: Sun, 22 Dec 2024 21:36:54 +0100 Subject: [PATCH 078/205] switch to system cursor when outside of viewport --- 61_UI/main.cpp | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/61_UI/main.cpp b/61_UI/main.cpp index 195505e11..e047505df 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -1663,6 +1663,22 @@ class UISampleApp final : public examples::SimpleWindowedApplication } } + { + ImGuiIO& io = ImGui::GetIO(); + ImVec2 mousePos = ImGui::GetMousePos(); + ImVec2 viewportSize = io.DisplaySize; + auto* cc = m_window->getCursorControl(); + + if (mousePos.x < 0.0f || mousePos.y < 0.0f || mousePos.x > viewportSize.x || mousePos.y > viewportSize.y) + { + cc->setVisible(true); + } + else + { + cc->setVisible(false); + } + } + ImGui::End(); } } From 3698f02a2953ec04fb80acfe7429fcded6553ec1 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Mon, 23 Dec 2024 08:29:01 +0100 Subject: [PATCH 079/205] ImGuizmo last invocation cache needs to be inspected, add a few comments --- 61_UI/main.cpp | 42 ++++++++++++++++++--- common/include/camera/IGimbalController.hpp | 16 ++++---- 2 files changed, 43 insertions(+), 15 deletions(-) diff --git a/61_UI/main.cpp b/61_UI/main.cpp index 46c0c2040..13e34404b 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -1143,6 +1143,8 @@ class UISampleApp final : public examples::SimpleWindowedApplication size_t gizmoIx = {}; size_t manipulationCounter = {}; + const std::optional modelInUseIx = ImGuizmo::IsUsingAny() ? std::optional(boundPlanarCameraIxToManipulate.has_value() ? 1u + boundPlanarCameraIxToManipulate.value() : 0u) : std::optional(std::nullopt); + for (uint32_t windowIx = 0; windowIx < windowControlBinding.size(); ++windowIx) { // setup imgui window @@ -1214,11 +1216,13 @@ class UISampleApp final : public examples::SimpleWindowedApplication { ImGuizmo::PushID(gizmoIx); ++gizmoIx; - const bool isCameraGimbalTarget = modelIx; // I assume scene demo model is 0th ix, left are cameras + const bool isCameraGimbalTarget = modelIx; // I assume scene demo model is 0th ix, left are planar cameras ICamera* const targetGimbalManipulationCamera = isCameraGimbalTarget ? m_planarProjections[modelIx - 1u]->getCamera() : nullptr; bool discard = isCameraGimbalTarget && mCurrentGizmoOperation != ImGuizmo::TRANSLATE; // discard WiP stuff // if we try to manipulate a camera which appears to be the same camera we see scene from then obvsly it doesn't make sense to manipulate its gizmo so we skip it + // EDIT: it actually makes some sense if you assume render planar view is rendered with ortho projection, but we would need to add imguizmo controller virtual map + // to ban forward/backward in this mode if this condition is true if (targetGimbalManipulationCamera == planarViewCameraBound) { ImGuizmo::PopID(); @@ -1242,11 +1246,38 @@ class UISampleApp final : public examples::SimpleWindowedApplication if (success) { ++manipulationCounter; - discard &= manipulationCounter > 1; + discard |= manipulationCounter > 1; + + /* + + NOTE to self & TODO: I must be killing ImGuizmo last invocation cache or something with my delta events (or at least I dont + see now where I'm *maybe* using its API incorrectly, the problem is I get translation parts in delta matrix when doing + rotations hence it glitches my cameras -> on the other hand I can see it kinda correctly outputs the delta matrix when + gc model is bound - // TMP WIP, our imguizmo controller doesnt support rotation & scale yet and its because - // - there are numerical issues with imguizmo decompose/recompose TRS (and the author also says it) - // - in rotate mode delta TRS matrix contains translate part (how? no idea, maybe imguizmo bug) and it glitches everything + auto postprocessDeltaManipulation = [](const float32_t4x4& inDeltaMatrix, float32_t4x4& outDeltaMatrix, ImGuizmo::OPERATION operation) -> void + { + struct + { + float32_t3 dTranslation, dRotation, dScale; + } world; + + ImGuizmo::DecomposeMatrixToComponents(&inDeltaMatrix[0][0], &world.dTranslation[0], &world.dRotation[0], &world.dScale[0]); + + if (operation == ImGuizmo::TRANSLATE) + { + world.dRotation = float32_t3(0, 0, 0); + } + else if (operation == ImGuizmo::ROTATE) + { + world.dTranslation = float32_t3(0, 0, 0); + } + + ImGuizmo::RecomposeMatrixFromComponents(&world.dTranslation[0], &world.dRotation[0], &world.dScale[0], &outDeltaMatrix[0][0]); + }; + + postprocessDeltaManipulation(imguizmoModel.outDeltaTRS, imguizmoModel.outDeltaTRS, mCurrentGizmoOperation); + */ if (!discard) { @@ -1832,7 +1863,6 @@ class UISampleApp final : public examples::SimpleWindowedApplication } ImGui::End(); - { // generate virtual events given delta TRS matrix if (boundCameraToManipulate) diff --git a/common/include/camera/IGimbalController.hpp b/common/include/camera/IGimbalController.hpp index c7a0c507a..841a5291b 100644 --- a/common/include/camera/IGimbalController.hpp +++ b/common/include/camera/IGimbalController.hpp @@ -387,9 +387,7 @@ class IGimbalController : public IGimbalManipulateEncoder float32_t3 dTranslation, dRotation, dScale; } world; - // TODO: since I assume the delta matrix is from imguizmo I must follow its API (but this one I could actually steal & rewrite to Nabla to not call imguizmo stuff here <- its TEMP!!!!!!!) - // - there are numerical issues with imguizmo decompose/recompose TRS (and the author also says it, we should have this util in Nabla and work with doubles not floats) - // - in rotate mode delta TRS matrix contains translate part (how? no idea, maybe imguizmo bug) and it glitches everything, I get translations e-07 or something + // TODO: write it in Nabla, this is temp ImGuizmo::DecomposeMatrixToComponents(&deltaWorldTRS[0][0], &world.dTranslation[0], &world.dRotation[0], &world.dScale[0]); // Delta translation impulse @@ -398,14 +396,14 @@ class IGimbalController : public IGimbalManipulateEncoder requestMagnitudeUpdateWithScalar(0.f, world.dTranslation[2], std::abs(world.dTranslation[2]), gimbal_event_t::MoveForward, gimbal_event_t::MoveBackward, m_imguizmoVirtualEventMap); // Delta rotation impulse - //requestMagnitudeUpdateWithScalar(0.f, world.dRotation[0], std::abs(world.dRotation[0]), gimbal_event_t::TiltUp , gimbal_event_t::TiltDown, m_imguizmoVirtualEventMap); - //requestMagnitudeUpdateWithScalar(0.f, world.dRotation[1], std::abs(world.dRotation[1]), gimbal_event_t::PanRight, gimbal_event_t::PanLeft, m_imguizmoVirtualEventMap); - //requestMagnitudeUpdateWithScalar(0.f, world.dRotation[2], std::abs(world.dRotation[2]), gimbal_event_t::RollRight, gimbal_event_t::RollLeft, m_imguizmoVirtualEventMap); + requestMagnitudeUpdateWithScalar(0.f, world.dRotation[0], std::abs(world.dRotation[0]), gimbal_event_t::TiltUp , gimbal_event_t::TiltDown, m_imguizmoVirtualEventMap); + requestMagnitudeUpdateWithScalar(0.f, world.dRotation[1], std::abs(world.dRotation[1]), gimbal_event_t::PanRight, gimbal_event_t::PanLeft, m_imguizmoVirtualEventMap); + requestMagnitudeUpdateWithScalar(0.f, world.dRotation[2], std::abs(world.dRotation[2]), gimbal_event_t::RollRight, gimbal_event_t::RollLeft, m_imguizmoVirtualEventMap); // Delta scale impulse - //requestMagnitudeUpdateWithScalar(1.f, world.dScale[0], std::abs(world.dScale[0]), gimbal_event_t::ScaleXInc, gimbal_event_t::ScaleXDec, m_imguizmoVirtualEventMap); - //requestMagnitudeUpdateWithScalar(1.f, world.dScale[1], std::abs(world.dScale[1]), gimbal_event_t::ScaleYInc, gimbal_event_t::ScaleYDec, m_imguizmoVirtualEventMap); - //requestMagnitudeUpdateWithScalar(1.f, world.dScale[2], std::abs(world.dScale[2]), gimbal_event_t::ScaleZInc, gimbal_event_t::ScaleZDec, m_imguizmoVirtualEventMap); + requestMagnitudeUpdateWithScalar(1.f, world.dScale[0], std::abs(world.dScale[0]), gimbal_event_t::ScaleXInc, gimbal_event_t::ScaleXDec, m_imguizmoVirtualEventMap); + requestMagnitudeUpdateWithScalar(1.f, world.dScale[1], std::abs(world.dScale[1]), gimbal_event_t::ScaleYInc, gimbal_event_t::ScaleYDec, m_imguizmoVirtualEventMap); + requestMagnitudeUpdateWithScalar(1.f, world.dScale[2], std::abs(world.dScale[2]), gimbal_event_t::ScaleZInc, gimbal_event_t::ScaleZDec, m_imguizmoVirtualEventMap); } postprocess(m_imguizmoVirtualEventMap, output, count); From ddfcac545cd4f3d427399f34c548f6786b903cb8 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Mon, 23 Dec 2024 08:55:20 +0100 Subject: [PATCH 080/205] fix a little loading bug after the merge --- 61_UI/main.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/61_UI/main.cpp b/61_UI/main.cpp index 12b914614..a32c6aff8 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -255,7 +255,6 @@ class UISampleApp final : public examples::SimpleWindowedApplication argparse::ArgumentParser program("Virtual camera event system demo"); program.add_argument("--file") - .required() .help("Path to json file with camera inputs"); try @@ -276,13 +275,17 @@ class UISampleApp final : public examples::SimpleWindowedApplication return false; { - const auto cameraJsonFile = program.get("--file"); + const std::optional cameraJsonFile = program.is_used("--file") ? program.get("--file") : std::optional(std::nullopt); json j; - std::ifstream file(cameraJsonFile.c_str()); + auto file = cameraJsonFile.has_value() ? std::ifstream(cameraJsonFile.value()) : std::ifstream(); if (!file.is_open()) { - m_logger->log("Cannot open input \"%s\" json file. Switching to default config.", ILogger::ELL_WARNING, cameraJsonFile.c_str()); + if (cameraJsonFile.has_value()) + m_logger->log("Cannot open input \"%s\" json file. Switching to default config.", ILogger::ELL_WARNING, cameraJsonFile.value().c_str()); + else + m_logger->log("No input json file provided. Switching to default config.", ILogger::ELL_WARNING); + auto assets = make_smart_refctd_ptr(smart_refctd_ptr(m_logger)); auto pFile = assets->getFile("cameras.json", IFile::ECF_READ, ""); From bbce80ebbb30345ba4320b5b065a5e0ccf06a587 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Mon, 23 Dec 2024 09:14:56 +0100 Subject: [PATCH 081/205] minor visual bug with cursor --- 61_UI/main.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/61_UI/main.cpp b/61_UI/main.cpp index a32c6aff8..ad4801aa9 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -1705,7 +1705,8 @@ class UISampleApp final : public examples::SimpleWindowedApplication if (mousePos.x < 0.0f || mousePos.y < 0.0f || mousePos.x > viewportSize.x || mousePos.y > viewportSize.y) { - cc->setVisible(true); + if (not enableActiveCameraMovement) + cc->setVisible(true); } else { From 1dcd89938cc2a63f54f3f37b726bee1d024367ed Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Mon, 23 Dec 2024 12:16:08 +0100 Subject: [PATCH 082/205] add new Free Lock camera to the scene, update cameras.json input, fix active window FBO rendering bugs in full screen mode --- 61_UI/app_resources/cameras.json | 36 +++- 61_UI/include/common.hpp | 1 + 61_UI/main.cpp | 188 ++++++++++---------- common/include/camera/CFPSCamera.hpp | 26 +-- common/include/camera/CFreeLockCamera.hpp | 143 +++++++++++++++ common/include/camera/ICamera.hpp | 22 ++- common/include/camera/IGimbalController.hpp | 5 - 7 files changed, 292 insertions(+), 129 deletions(-) create mode 100644 common/include/camera/CFreeLockCamera.hpp diff --git a/61_UI/app_resources/cameras.json b/61_UI/app_resources/cameras.json index 5f8d5b489..18ab7d5b6 100644 --- a/61_UI/app_resources/cameras.json +++ b/61_UI/app_resources/cameras.json @@ -11,7 +11,7 @@ "orientation": [0.047, 0.830, -0.072, 0.55] }, { - "type": "FPS", + "type": "Free", "position": [2.116, 0.826, 1.152], "orientation": [0.095, -0.835, 0.152, 0.521] } @@ -21,13 +21,13 @@ "type": "perspective", "fov": 60.0, "zNear": 0.1, - "zFar": 100.0 + "zFar": 110.0 }, { "type": "orthographic", "orthoWidth": 10.0, "zNear": 0.1, - "zFar": 100.0 + "zFar": 110.0 } ], "viewports": [ @@ -44,6 +44,18 @@ "keyboard": 1, "mouse": 0 } + }, + { + "projection": 0, + "controllers": { + "keyboard": 2 + } + }, + { + "projection": 1, + "controllers": { + "keyboard": 1 + } } ], "planars": [ @@ -57,7 +69,7 @@ }, { "camera": 2, - "viewports": [0, 1] + "viewports": [2, 3] } ], "controllers": { @@ -81,6 +93,22 @@ "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" + } } ], "mouse": [ diff --git a/61_UI/include/common.hpp b/61_UI/include/common.hpp index 4033afc14..203bd4ed9 100644 --- a/61_UI/include/common.hpp +++ b/61_UI/include/common.hpp @@ -7,6 +7,7 @@ // common api #include "camera/CFPSCamera.hpp" +#include "camera/CFreeLockCamera.hpp" #include "SimpleWindowedApplication.hpp" #include "InputSystem.hpp" diff --git a/61_UI/main.cpp b/61_UI/main.cpp index ad4801aa9..eb8e7e196 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -306,38 +306,38 @@ class UISampleApp final : public examples::SimpleWindowedApplication { if (jCamera.contains("type")) { - if (jCamera["type"] == "FPS") + if (!jCamera.contains("position")) { - if (!jCamera.contains("position")) - { - logFail("Expected \"position\" keyword for camera definition!"); - return false; - } + logFail("Expected \"position\" keyword for camera definition!"); + return false; + } - if (!jCamera.contains("orientation")) - { - logFail("Expected \"orientation\" keyword for camera definition!"); - return false; - } + if (!jCamera.contains("orientation")) + { + logFail("Expected \"orientation\" keyword for camera definition!"); + return false; + } - auto position = [&]() - { - auto jret = jCamera["position"].get>(); - return float32_t3(jret[0], jret[1], jret[2]); - }(); + auto position = [&]() + { + auto jret = jCamera["position"].get>(); + return float32_t3(jret[0], jret[1], jret[2]); + }(); - auto orientation = [&]() - { - auto jret = jCamera["orientation"].get>(); + auto orientation = [&]() + { + auto jret = jCamera["orientation"].get>(); - // order important for glm::quat, - // the ctor is GLM_FUNC_QUALIFIER GLM_CONSTEXPR qua::qua(T _w, T _x, T _y, T _z) - // but memory layout (and json) is x,y,z,w - return glm::quat(jret[3], jret[0], jret[1], jret[2]); - }(); + // order important for glm::quat, + // the ctor is GLM_FUNC_QUALIFIER GLM_CONSTEXPR qua::qua(T _w, T _x, T _y, T _z) + // but memory layout (and json) is x,y,z,w + return glm::quat(jret[3], jret[0], jret[1], jret[2]); + }(); + if (jCamera["type"] == "FPS") cameras.emplace_back() = make_smart_refctd_ptr(position, orientation); - } + else if (jCamera["type"] == "Free") + cameras.emplace_back() = make_smart_refctd_ptr(position, orientation); else { logFail("Unsupported camera type!"); @@ -383,7 +383,6 @@ class UISampleApp final : public examples::SimpleWindowedApplication float fov = jProjection["fov"].get(); projections.emplace_back(IPlanarProjection::CProjection::create(zNear, zFar, fov)); - } else if (jProjection["type"] == "orthographic") { @@ -505,7 +504,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication const auto cameraIx = jPlanar["camera"].get(); auto boundViewports = jPlanar["viewports"].get>(); - auto& planars = m_planarProjections.emplace_back() = planar_projection_t::create(smart_refctd_ptr(cameras[cameraIx])); + auto& planar = m_planarProjections.emplace_back() = planar_projection_t::create(smart_refctd_ptr(cameras[cameraIx])); for (const auto viewportIx : boundViewports) { auto& viewport = j["viewports"][viewportIx]; @@ -515,28 +514,36 @@ class UISampleApp final : public examples::SimpleWindowedApplication return false; } - if (!viewport["controllers"].contains("keyboard")) + const auto projectionIx = viewport["projection"].get(); + auto& projection = planar->getPlanarProjections().emplace_back(projections[projectionIx]); + + const bool hasKeyboardBound = viewport["controllers"].contains("keyboard"); + const bool hasMouseBound = viewport["controllers"].contains("mouse"); + + if (hasKeyboardBound) { - logFail("\"keyboard\" value missing in controllers in viewport object index %d", viewportIx); - return false; + auto keyboardControllerIx = viewport["controllers"]["keyboard"].get(); + projection.updateKeyboardMapping([&](auto& map) { map = controllers.keyboard[keyboardControllerIx]; }); } + else + projection.updateKeyboardMapping([&](auto& map) { map = {}; }); // clean the map if not bound - if (!viewport["controllers"].contains("mouse")) + if (hasMouseBound) { - logFail("\"mouse\" value missing in controllers in viewport object index %d", viewportIx); - return false; + auto mouseControllerIx = viewport["controllers"]["mouse"].get(); + projection.updateMouseMapping([&](auto& map) { map = controllers.mouse[mouseControllerIx]; }); } + else + projection.updateMouseMapping([&](auto& map) { map = {}; }); // clean the map if not bound + } - auto keyboardIx = viewport["controllers"]["keyboard"].get(); - auto mouseIx = viewport["controllers"]["mouse"].get(); - - auto projectionIx = viewport["projection"].get(); - auto keyboardControllerIx = viewport["controllers"]["keyboard"].get(); - auto mouseControllerIx = viewport["controllers"]["mouse"].get(); - - auto& projection = planars->getPlanarProjections().emplace_back(projections[projectionIx]); - projection.updateKeyboardMapping([&](auto& map) { map = controllers.keyboard[keyboardControllerIx]; }); - projection.updateMouseMapping([&](auto& map) { map = controllers.mouse[mouseControllerIx]; }); + { + auto* camera = planar->getCamera(); + { + camera->updateKeyboardMapping([&](auto& map) { map = camera->getKeyboardMappingPreset(); }); + camera->updateMouseMapping([&](auto& map) { map = camera->getMouseMappingPreset(); }); + camera->updateImguizmoMapping([&](auto& map) { map = camera->getImguizmoMappingPreset(); }); + } } } } @@ -873,7 +880,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication for (auto binding : windowControlBinding) renderOfflineScene(binding.scene); else - renderOfflineScene(windowControlBinding.front().scene.get()); // just to not render to all at once + renderOfflineScene(windowControlBinding[activeRenderWindowIx].scene.get()); const IGPUCommandBuffer::SClearColorValue clearValue = { .float32 = {0.f,0.f,0.f,1.f} }; const IGPUCommandBuffer::SRenderpassBeginInfo info = { @@ -1213,7 +1220,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGuizmo::SetRect(cursorPos.x, cursorPos.y, contentRegionSize.x, contentRegionSize.y); // I will assume we need to focus a window to start manipulating objects from it - if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) + if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) activeRenderWindowIx = windowIx; // we render a scene from view of a camera bound to planar window @@ -1330,33 +1337,22 @@ class UISampleApp final : public examples::SimpleWindowedApplication } targetGimbalManipulationCamera->endInputProcessing(); - if (vCount) { - auto* fps = dynamic_cast(targetGimbalManipulationCamera); - - float pMoveSpeed = 1.f, pRotationSpeed = 1.f; + const float pMoveSpeed = targetGimbalManipulationCamera->getMoveSpeedScale(); + const float pRotationSpeed = targetGimbalManipulationCamera->getRotationSpeedScale(); - if (fps) - { - pMoveSpeed = fps->getMoveSpeedScale(); - pRotationSpeed = fps->getRotationSpeedScale(); + // I start to think controller should be able to set sensitivity to scale magnitudes of generated events + // in order for camera to not keep any magnitude scalars like move or rotation speed scales - // I start to think controller should be able to set sensitivity to scale magnitudes of generated events - // in order for camera to not keep any magnitude scalars like move or rotation speed scales - - fps->setMoveSpeedScale(1); - fps->setRotationSpeedScale(1); - } + targetGimbalManipulationCamera->setMoveSpeedScale(1); + targetGimbalManipulationCamera->setRotationSpeedScale(1); // NOTE: generated events from ImGuizmo controller are always in world space! targetGimbalManipulationCamera->manipulate({ virtualEvents.data(), vCount }, ICamera::World); - if (fps) - { - fps->setMoveSpeedScale(pMoveSpeed); - fps->setRotationSpeedScale(pRotationSpeed); - } + targetGimbalManipulationCamera->setMoveSpeedScale(pMoveSpeed); + targetGimbalManipulationCamera->setRotationSpeedScale(pRotationSpeed); } } } @@ -1419,10 +1415,26 @@ class UISampleApp final : public examples::SimpleWindowedApplication 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); const ImVec2 contentRegionSize = ImGui::GetContentRegionAvail(), windowPos = ImGui::GetWindowPos(), cursorPos = ImGui::GetCursorScreenPos(); + { + auto& binding = windowControlBinding[activeRenderWindowIx]; + auto& planarBound = m_planarProjections[binding.activePlanarIx]; + assert(planarBound); + + binding.aspectRatio = contentRegionSize.x / contentRegionSize.y; + auto* planarViewCameraBound = planarBound->getCamera(); + + assert(planarViewCameraBound); + assert(binding.boundProjectionIx.has_value()); + + auto& projection = planarBound->getPlanarProjections()[binding.boundProjectionIx.value()]; + projection.update(binding.leftHandedProjection, binding.aspectRatio); + } ImGui::Image(info, contentRegionSize); ImGuizmo::SetRect(cursorPos.x, cursorPos.y, contentRegionSize.x, contentRegionSize.y); + + ImGui::End(); ImGui::PopStyleColor(1); } @@ -1574,7 +1586,8 @@ class UISampleApp final : public examples::SimpleWindowedApplication if (useWindow) ImGui::Checkbox("Allow axes to flip##allowAxesToFlip", &active.allowGizmoAxesToFlip); - ImGui::Checkbox("Draw debug grid##drawDebugGrid", &active.enableDebugGridDraw); + if(useWindow) + ImGui::Checkbox("Draw debug grid##drawDebugGrid", &active.enableDebugGridDraw); if (ImGui::RadioButton("LH", active.leftHandedProjection)) active.leftHandedProjection = true; @@ -1584,6 +1597,9 @@ class UISampleApp final : public examples::SimpleWindowedApplication if (ImGui::RadioButton("RH", not active.leftHandedProjection)) active.leftHandedProjection = false; + updateParameters.m_zNear = std::clamp(updateParameters.m_zNear, 0.1f, 100.f); + updateParameters.m_zFar = std::clamp(updateParameters.m_zFar, 110.f, 10000.f); + ImGui::SliderFloat("zNear", &updateParameters.m_zNear, 0.1f, 100.f, "%.2f", ImGuiSliderFlags_Logarithmic); ImGui::SliderFloat("zFar", &updateParameters.m_zFar, 110.f, 10000.f, "%.1f", ImGuiSliderFlags_Logarithmic); @@ -1659,22 +1675,17 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGui::Text("Object Ix: %s", std::to_string(active.activePlanarIx + 1u).c_str()); ImGui::Separator(); { - auto* fps = dynamic_cast(boundCamera); - - if (fps) - { - float moveSpeed = fps->getMoveSpeedScale(); - float rotationSpeed = fps->getRotationSpeedScale(); + float moveSpeed = boundCamera->getMoveSpeedScale(); + float rotationSpeed = boundCamera->getRotationSpeedScale(); - ImGui::SliderFloat("Move speed factor", &moveSpeed, 0.0001f, 10.f, "%.4f", ImGuiSliderFlags_Logarithmic); - ImGui::SliderFloat("Rotate speed factor", &rotationSpeed, 0.0001f, 10.f, "%.4f", ImGuiSliderFlags_Logarithmic); + ImGui::SliderFloat("Move speed factor", &moveSpeed, 0.0001f, 10.f, "%.4f", ImGuiSliderFlags_Logarithmic); + ImGui::SliderFloat("Rotate speed factor", &rotationSpeed, 0.0001f, 10.f, "%.4f", ImGuiSliderFlags_Logarithmic); - fps->setMoveSpeedScale(moveSpeed); - fps->setRotationSpeedScale(rotationSpeed); - } + boundCamera->setMoveSpeedScale(moveSpeed); + boundCamera->setRotationSpeedScale(rotationSpeed); } - if (ImGui::TreeNodeEx("World Data", ImGuiTreeNodeFlags_None)) + if (ImGui::TreeNodeEx("World Data", flags)) { auto& gimbal = boundCamera->getGimbal(); const auto position = getCastedVector(gimbal.getPosition()); @@ -1932,28 +1943,17 @@ class UISampleApp final : public examples::SimpleWindowedApplication if (vCount) { - auto* fps = dynamic_cast(boundCameraToManipulate.get()); - - float pmSpeed = 1.f; - float prSpeed = 1.f; + const float pmSpeed = boundCameraToManipulate->getMoveSpeedScale(); + const float prSpeed = boundCameraToManipulate->getRotationSpeedScale(); - if (fps) - { - pmSpeed = fps->getMoveSpeedScale(); - prSpeed = fps->getRotationSpeedScale(); - - fps->setMoveSpeedScale(1); - fps->setRotationSpeedScale(1); - } + boundCameraToManipulate->setMoveSpeedScale(1); + boundCameraToManipulate->setRotationSpeedScale(1); // NOTE: generated events from ImGuizmo controller are always in world space! boundCameraToManipulate->manipulate({ virtualEvents.data(), vCount }, ICamera::World); - if (fps) - { - fps->setMoveSpeedScale(pmSpeed); - fps->setRotationSpeedScale(prSpeed); - } + boundCameraToManipulate->setMoveSpeedScale(pmSpeed); + boundCameraToManipulate->setRotationSpeedScale(prSpeed); } } } diff --git a/common/include/camera/CFPSCamera.hpp b/common/include/camera/CFPSCamera.hpp index 3889e5921..cb00fe442 100644 --- a/common/include/camera/CFPSCamera.hpp +++ b/common/include/camera/CFPSCamera.hpp @@ -17,12 +17,7 @@ class CFPSCamera final : public ICamera using base_t = ICamera; CFPSCamera(const float64_t3& position, const glm::quat& orientation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f)) - : base_t(), m_gimbal({ .position = position, .orientation = orientation }) - { - updateKeyboardMapping([](auto& map) { map = m_keyboard_to_virtual_events_preset; }); - updateMouseMapping([](auto& map) { map = m_mouse_to_virtual_events_preset; }); - updateImguizmoMapping([](auto& map) { map = m_imguizmo_to_virtual_events_preset; }); - } + : base_t(), m_gimbal({ .position = position, .orientation = orientation }) {} ~CFPSCamera() = default; const base_t::keyboard_to_virtual_events_t getKeyboardMappingPreset() const override { return m_keyboard_to_virtual_events_preset; } @@ -86,28 +81,9 @@ class CFPSCamera final : public ICamera return "FPS Camera"; } - // (***) - inline void setMoveSpeedScale(double scalar) - { - m_moveSpeedScale = scalar; - } - - // (***) - inline void setRotationSpeedScale(double scalar) - { - m_rotationSpeedScale = scalar; - } - - inline double getMoveSpeedScale() const { return m_moveSpeedScale; } - inline double getRotationSpeedScale() const { return m_rotationSpeedScale; } - private: typename base_t::CGimbal m_gimbal; - // (***) TODO: I need to think whether a camera should own this or controllers should be able - // to set sensitivity to scale magnitudes of generated events we put into manipulate method - double m_moveSpeedScale = 0.01, m_rotationSpeedScale = 0.003; - static inline constexpr auto AllowedLocalVirtualEvents = CVirtualGimbalEvent::Translate | CVirtualGimbalEvent::TiltUp | CVirtualGimbalEvent::TiltDown | CVirtualGimbalEvent::PanRight | CVirtualGimbalEvent::PanLeft; static inline constexpr auto AllowedWorldVirtualEvents = AllowedLocalVirtualEvents; diff --git a/common/include/camera/CFreeLockCamera.hpp b/common/include/camera/CFreeLockCamera.hpp new file mode 100644 index 000000000..0788bb048 --- /dev/null +++ b/common/include/camera/CFreeLockCamera.hpp @@ -0,0 +1,143 @@ +// Copyright (C) 2018-2024 - 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_FREE_CAMERA_HPP_ +#define _C_FREE_CAMERA_HPP_ + +#include "ICamera.hpp" +#include + +namespace nbl::hlsl // TODO: DIFFERENT NAMESPACE +{ + +// Free Lock Camera +class CFreeCamera final : public ICamera +{ +public: + using base_t = ICamera; + + CFreeCamera(const float64_t3& position, const glm::quat& orientation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f)) + : base_t(), m_gimbal({ .position = position, .orientation = orientation }) {} + ~CFreeCamera() = default; + + const base_t::keyboard_to_virtual_events_t getKeyboardMappingPreset() const override { return m_keyboard_to_virtual_events_preset; } + const base_t::mouse_to_virtual_events_t getMouseMappingPreset() const override { return m_mouse_to_virtual_events_preset; } + const base_t::imguizmo_to_virtual_events_t getImguizmoMappingPreset() const override { return m_imguizmo_to_virtual_events_preset; } + + const typename base_t::CGimbal& getGimbal() override + { + return m_gimbal; + } + + virtual bool manipulate(std::span virtualEvents, base_t::ManipulationMode mode) override + { + if (virtualEvents.empty()) + return false; + + const auto gOrientation = m_gimbal.getOrientation(); + + // TODO: I make assumption what world base is now (at least temporary), I need to think of this but maybe each ITransformObject should know what its world is + // in ideal scenario we would define this crazy enum with all possible standard bases + const auto impulse = mode == base_t::Local ? m_gimbal.accumulate(virtualEvents) + : m_gimbal.accumulate(virtualEvents, { 1, 0, 0 }, { 0, 1, 0 }, { 0, 0, 1 }); + + const auto pitchQuat = glm::angleAxis(impulse.dVirtualRotation.x * m_rotationSpeedScale, glm::vec3(1.0f, 0.0f, 0.0f)); + const auto yawQuat = glm::angleAxis(impulse.dVirtualRotation.y * m_rotationSpeedScale, glm::vec3(0.0f, 1.0f, 0.0f)); + const auto rollQuat = glm::angleAxis(impulse.dVirtualRotation.z * m_rotationSpeedScale, glm::vec3(0.0f, 0.0f, 1.0f)); + + const auto newOrientation = glm::normalize(gOrientation * yawQuat * pitchQuat * rollQuat); + const float64_t3 newPosition = m_gimbal.getPosition() + impulse.dVirtualTranslate * m_moveSpeedScale; + + bool manipulated = true; + + m_gimbal.begin(); + { + m_gimbal.setOrientation(newOrientation); + m_gimbal.setPosition(newPosition); + } + m_gimbal.end(); + + manipulated &= bool(m_gimbal.getManipulationCounter()); + + if (manipulated) + m_gimbal.updateView(); + + return manipulated; + } + + virtual const uint32_t getAllowedVirtualEvents(base_t::ManipulationMode mode) override + { + switch (mode) + { + case base_t::Local: return AllowedLocalVirtualEvents; + case base_t::World: return AllowedWorldVirtualEvents; + default: return CVirtualGimbalEvent::None; + } + } + + virtual const std::string_view getIdentifier() override + { + return "Free-Look Camera"; + } + +private: + typename base_t::CGimbal m_gimbal; + + static inline constexpr auto AllowedLocalVirtualEvents = CVirtualGimbalEvent::Translate | CVirtualGimbalEvent::Rotate; + static inline constexpr auto AllowedWorldVirtualEvents = AllowedLocalVirtualEvents; + + static inline const auto m_keyboard_to_virtual_events_preset = []() + { + typename base_t::keyboard_to_virtual_events_t preset; + + preset[ui::E_KEY_CODE::EKC_W] = CVirtualGimbalEvent::MoveForward; + preset[ui::E_KEY_CODE::EKC_S] = CVirtualGimbalEvent::MoveBackward; + preset[ui::E_KEY_CODE::EKC_A] = CVirtualGimbalEvent::MoveLeft; + preset[ui::E_KEY_CODE::EKC_D] = CVirtualGimbalEvent::MoveRight; + preset[ui::E_KEY_CODE::EKC_I] = CVirtualGimbalEvent::TiltDown; + preset[ui::E_KEY_CODE::EKC_K] = CVirtualGimbalEvent::TiltUp; + preset[ui::E_KEY_CODE::EKC_J] = CVirtualGimbalEvent::PanLeft; + preset[ui::E_KEY_CODE::EKC_L] = CVirtualGimbalEvent::PanRight; + preset[ui::E_KEY_CODE::EKC_Q] = CVirtualGimbalEvent::RollLeft; + preset[ui::E_KEY_CODE::EKC_E] = CVirtualGimbalEvent::RollRight; + + return preset; + }(); + + static inline const auto m_mouse_to_virtual_events_preset = []() + { + typename base_t::mouse_to_virtual_events_t preset; + + preset[ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_X] = CVirtualGimbalEvent::PanRight; + preset[ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_X] = CVirtualGimbalEvent::PanLeft; + preset[ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_Y] = CVirtualGimbalEvent::TiltUp; + preset[ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_Y] = CVirtualGimbalEvent::TiltDown; + + return preset; + }(); + + static inline const auto m_imguizmo_to_virtual_events_preset = []() + { + typename base_t::imguizmo_to_virtual_events_t preset; + + preset[CVirtualGimbalEvent::MoveForward] = CVirtualGimbalEvent::MoveForward; + preset[CVirtualGimbalEvent::MoveBackward] = CVirtualGimbalEvent::MoveBackward; + preset[CVirtualGimbalEvent::MoveLeft] = CVirtualGimbalEvent::MoveLeft; + preset[CVirtualGimbalEvent::MoveRight] = CVirtualGimbalEvent::MoveRight; + preset[CVirtualGimbalEvent::MoveUp] = CVirtualGimbalEvent::MoveUp; + preset[CVirtualGimbalEvent::MoveDown] = CVirtualGimbalEvent::MoveDown; + preset[CVirtualGimbalEvent::TiltDown] = CVirtualGimbalEvent::TiltDown; + preset[CVirtualGimbalEvent::TiltUp] = CVirtualGimbalEvent::TiltUp; + preset[CVirtualGimbalEvent::PanLeft] = CVirtualGimbalEvent::PanLeft; + preset[CVirtualGimbalEvent::PanRight] = CVirtualGimbalEvent::PanRight; + preset[CVirtualGimbalEvent::RollLeft] = CVirtualGimbalEvent::RollLeft; + preset[CVirtualGimbalEvent::RollRight] = CVirtualGimbalEvent::RollRight; + + return preset; + }(); +}; + +} + +#endif // _C_FREE_CAMERA_HPP_ \ No newline at end of file diff --git a/common/include/camera/ICamera.hpp b/common/include/camera/ICamera.hpp index 33890fef1..9af3e27d9 100644 --- a/common/include/camera/ICamera.hpp +++ b/common/include/camera/ICamera.hpp @@ -71,7 +71,7 @@ class ICamera : public IGimbalController, virtual public core::IReferenceCounted }; ICamera() {} - ~ICamera() = default; + virtual ~ICamera() = default; // Returns a gimbal which *models the camera view* virtual const CGimbal& getGimbal() = 0u; @@ -85,6 +85,26 @@ class ICamera : public IGimbalController, virtual public core::IReferenceCounted // Identifier of a camera type virtual const std::string_view getIdentifier() = 0u; + + // (***) + inline void setMoveSpeedScale(double scalar) + { + m_moveSpeedScale = scalar; + } + + // (***) + inline void setRotationSpeedScale(double scalar) + { + m_rotationSpeedScale = scalar; + } + + inline double getMoveSpeedScale() const { return m_moveSpeedScale; } + inline double getRotationSpeedScale() const { return m_rotationSpeedScale; } + +protected: + // (***) TODO: I need to think whether a camera should own this or controllers should be able + // to set sensitivity to scale magnitudes of generated events we put into manipulate method + double m_moveSpeedScale = 0.01, m_rotationSpeedScale = 0.003; }; } diff --git a/common/include/camera/IGimbalController.hpp b/common/include/camera/IGimbalController.hpp index 841a5291b..770b5ed16 100644 --- a/common/include/camera/IGimbalController.hpp +++ b/common/include/camera/IGimbalController.hpp @@ -76,13 +76,8 @@ struct IGimbalManipulateEncoder using mouse_to_virtual_events_t = std::unordered_map; using imguizmo_to_virtual_events_t = std::unordered_map; - //! default preset with encode_keyboard_code_t to gimbal_virtual_event_t map virtual const keyboard_to_virtual_events_t getKeyboardMappingPreset() const { return {}; } - - //! default preset with encode_keyboard_code_t to gimbal_virtual_event_t map virtual const mouse_to_virtual_events_t getMouseMappingPreset() const { return {}; } - - //! default preset with encode_keyboard_code_t to gimbal_virtual_event_t map virtual const imguizmo_to_virtual_events_t getImguizmoMappingPreset() const { return {}; } virtual const keyboard_to_virtual_events_t& getKeyboardVirtualEventMap() const = 0; From 0a638b4a656a7f9abdc04fcdba0dc3da4fe7f398 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Mon, 23 Dec 2024 15:04:11 +0100 Subject: [PATCH 083/205] make default positioning sexy, update input constants --- 61_UI/app_resources/cameras.json | 8 +-- 61_UI/main.cpp | 83 ++++++++++++++++++++++++++++---- 2 files changed, 77 insertions(+), 14 deletions(-) diff --git a/61_UI/app_resources/cameras.json b/61_UI/app_resources/cameras.json index 18ab7d5b6..3e9769e73 100644 --- a/61_UI/app_resources/cameras.json +++ b/61_UI/app_resources/cameras.json @@ -2,8 +2,8 @@ "cameras": [ { "type": "FPS", - "position": [-2.238, 1.438, -1.558], - "orientation": [0.204, 0.385, -0.088, 0.896] + "position": [-2.438, 1.995, -3.130], + "orientation": [0.195, 0.311, -0.065, 0.928] }, { "type": "FPS", @@ -19,13 +19,13 @@ "projections": [ { "type": "perspective", - "fov": 60.0, + "fov": 40.0, "zNear": 0.1, "zFar": 110.0 }, { "type": "orthographic", - "orthoWidth": 10.0, + "orthoWidth": 16.0, "zNear": 0.1, "zFar": 110.0 } diff --git a/61_UI/main.cpp b/61_UI/main.cpp index eb8e7e196..2d630b8d1 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -752,6 +752,28 @@ class UISampleApp final : public examples::SimpleWindowedApplication assert(m_ui.descriptorSet); m_ui.manager->registerListener([this]() -> void { imguiListen(); }); + { + const auto ds = float32_t2{ m_window->getWidth(), m_window->getHeight() }; + + wInit.trsEditor.iPos = iPaddingOffset; + wInit.trsEditor.iSize = { ds.x * 0.1, ds.y - wInit.trsEditor.iPos.y * 2 }; + + wInit.planars.iSize = { ds.x * 0.2, ds.y - iPaddingOffset.y * 2 }; + wInit.planars.iPos = { ds.x - wInit.planars.iSize.x - iPaddingOffset.x, 0 + iPaddingOffset.y }; + + { + float leftX = wInit.trsEditor.iPos.x + wInit.trsEditor.iSize.x + iPaddingOffset.x; + float eachXSize = wInit.planars.iPos.x - (wInit.trsEditor.iPos.x + wInit.trsEditor.iSize.x) - 2*iPaddingOffset.x; + float eachYSize = (ds.y - 2 * iPaddingOffset.y - (wInit.renderWindows.size() - 1) * iPaddingOffset.y) / wInit.renderWindows.size(); + + for (size_t i = 0; i < wInit.renderWindows.size(); ++i) + { + auto& rw = wInit.renderWindows[i]; + rw.iPos = { leftX, (1+i) * iPaddingOffset.y + i * eachYSize }; + rw.iSize = { eachXSize, eachYSize }; + } + } + } } // Geometry Creator Render Scene FBOs @@ -1177,16 +1199,30 @@ class UISampleApp final : public examples::SimpleWindowedApplication for (uint32_t windowIx = 0; windowIx < windowControlBinding.size(); ++windowIx) { - // setup imgui window - ImGui::SetNextWindowSize(ImVec2(800, 400), ImGuiCond_Appearing); - ImGui::SetNextWindowPos(ImVec2(400, 20 + windowIx * 420), ImGuiCond_Appearing); + // setup + { + const auto& rw = wInit.renderWindows[windowIx]; + ImGui::SetNextWindowPos({ rw.iPos.x, rw.iPos.y }, ImGuiCond_Appearing); + ImGui::SetNextWindowSize({ rw.iSize.x, rw.iSize.y }, ImGuiCond_Appearing); + } ImGui::SetNextWindowSizeConstraints(ImVec2(0x45, 0x45), ImVec2(7680, 4320)); ImGui::PushStyleColor(ImGuiCol_WindowBg, (ImVec4)ImColor(0.35f, 0.3f, 0.3f)); const std::string ident = "Render Window \"" + std::to_string(windowIx) + "\""; - ImGui::Begin(ident.data(), 0, ImGuiWindowFlags_NoMove); + + ImGui::Begin(ident.data(), 0); const ImVec2 contentRegionSize = ImGui::GetContentRegionAvail(), windowPos = ImGui::GetWindowPos(), cursorPos = ImGui::GetCursorScreenPos(); + ImGuiWindow* window = ImGui::GetCurrentWindow(); + { + const auto mPos = ImGui::GetMousePos(); + + if (mPos.x < cursorPos.x || mPos.y < cursorPos.y || mPos.x > cursorPos.x + contentRegionSize.x || mPos.y > cursorPos.y + contentRegionSize.y) + window->Flags = ImGuiWindowFlags_None; + else + window->Flags = ImGuiWindowFlags_NoMove; + } + // setup bound entities for the window like camera & projections auto& binding = windowControlBinding[windowIx]; auto& planarBound = m_planarProjections[binding.activePlanarIx]; @@ -1201,6 +1237,9 @@ class UISampleApp final : public examples::SimpleWindowedApplication auto& projection = planarBound->getPlanarProjections()[binding.boundProjectionIx.value()]; projection.update(binding.leftHandedProjection, binding.aspectRatio); + // TODO: + // would be nice to normalize imguizmo visual vectors (possible with styles) + // first 0th texture is for UI texture atlas, then there are our window textures auto fboImguiTextureID = windowIx + 1u; info.textureID = fboImguiTextureID; @@ -1466,6 +1505,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication assert(binding.boundProjectionIx.has_value()); auto& projection = planarBound->getPlanarProjections()[binding.boundProjectionIx.value()]; + projection.update(binding.leftHandedProjection, binding.aspectRatio); auto concatMatrix = mul(getCastedMatrix(projection.getProjectionMatrix()), getMatrix3x4As4x4(viewMatrix)); modelViewProjectionMatrix = mul(concatMatrix, getMatrix3x4As4x4(m_model)); @@ -1479,7 +1519,13 @@ class UISampleApp final : public examples::SimpleWindowedApplication // Planars { - ImGui::Begin("Projection"); + // setup + { + ImGui::SetNextWindowPos({ wInit.planars.iPos.x, wInit.planars.iPos.y }, ImGuiCond_Appearing); + ImGui::SetNextWindowSize({ wInit.planars.iSize.x, wInit.planars.iSize.y }, ImGuiCond_Appearing); + } + + ImGui::Begin("Planar projection"); ImGui::Checkbox("Window mode##useWindow", &useWindow); ImGui::Separator(); @@ -1613,7 +1659,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication case IPlanarProjection::CProjection::Orthographic: { - ImGui::SliderFloat("Ortho width", &updateParameters.m_planar.orthographic.orthoWidth, 1.f, 20.f, "%.1f", ImGuiSliderFlags_Logarithmic); + ImGui::SliderFloat("Ortho width", &updateParameters.m_planar.orthographic.orthoWidth, 1.f, 30.f, "%.1f", ImGuiSliderFlags_Logarithmic); boundProjection.setOrthographic(updateParameters.m_zNear, updateParameters.m_zFar, updateParameters.m_planar.orthographic.orthoWidth); } break; @@ -1736,11 +1782,15 @@ class UISampleApp final : public examples::SimpleWindowedApplication static bool boundSizing = false; static bool boundSizingSnap = false; - ImGui::SetNextWindowPos(ImVec2(10, 10), ImGuiCond_Appearing); - ImGui::SetNextWindowSize(ImVec2(320, 340), ImGuiCond_Appearing); - ImGui::Begin("TRS Editor"); - ImGuiIO& io = ImGui::GetIO(); + + // setup + { + ImGui::SetNextWindowPos({ wInit.trsEditor.iPos.x, wInit.trsEditor.iPos.y }, ImGuiCond_Appearing); + ImGui::SetNextWindowSize({ wInit.trsEditor.iSize.x, wInit.trsEditor.iSize.y }, ImGuiCond_Appearing); + } + + ImGui::Begin("TRS Editor"); { const size_t objectsCount = m_planarProjections.size() + 1u; assert(objectsCount); @@ -2124,6 +2174,19 @@ class UISampleApp final : public examples::SimpleWindowedApplication float snap[3] = { 1.f, 1.f, 1.f }; bool firstFrame = true; + const float32_t2 iPaddingOffset = float32_t2(10, 10); + + struct ImWindowInit + { + float32_t2 iPos, iSize; + }; + + struct + { + ImWindowInit trsEditor; + ImWindowInit planars; + std::array renderWindows; + } wInit; }; NBL_MAIN_FUNC(UISampleApp) \ No newline at end of file From 195f139490611461c7c9ded84611e3fa5739514b Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Mon, 30 Dec 2024 17:01:56 +0100 Subject: [PATCH 084/205] create COrbitCamera.hpp & add to the demo example --- 61_UI/app_resources/cameras.json | 42 +++- 61_UI/include/common.hpp | 1 + 61_UI/main.cpp | 124 ++++++++--- common/include/camera/COrbitCamera.hpp | 221 ++++++++++++++++++++ common/include/camera/IGimbal.hpp | 2 +- common/include/camera/IGimbalController.hpp | 3 +- 6 files changed, 363 insertions(+), 30 deletions(-) create mode 100644 common/include/camera/COrbitCamera.hpp diff --git a/61_UI/app_resources/cameras.json b/61_UI/app_resources/cameras.json index 3e9769e73..7e76aef83 100644 --- a/61_UI/app_resources/cameras.json +++ b/61_UI/app_resources/cameras.json @@ -6,9 +6,9 @@ "orientation": [0.195, 0.311, -0.065, 0.928] }, { - "type": "FPS", + "type": "Orbit", "position": [-2.017, 0.386, 0.684], - "orientation": [0.047, 0.830, -0.072, 0.55] + "target": [0, 0, 0] }, { "type": "Free", @@ -56,6 +56,20 @@ "controllers": { "keyboard": 1 } + }, + { + "projection": 0, + "controllers": { + "keyboard": 3, + "mouse": 1 + } + }, + { + "projection": 1, + "controllers": { + "keyboard": 3, + "mouse": 1 + } } ], "planars": [ @@ -65,7 +79,7 @@ }, { "camera": 1, - "viewports": [0, 1] + "viewports": [4, 5] }, { "camera": 2, @@ -109,6 +123,16 @@ "U": "RollRight", "O": "RollLeft" } + }, + { + "mappings": { + "W": "MoveRight", + "S": "MoveLeft", + "A": "MoveDown", + "D": "MoveUp", + "E": "MoveForward", + "Q": "MoveBackward" + } } ], "mouse": [ @@ -119,6 +143,18 @@ "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/include/common.hpp b/61_UI/include/common.hpp index 203bd4ed9..0a709d5b4 100644 --- a/61_UI/include/common.hpp +++ b/61_UI/include/common.hpp @@ -8,6 +8,7 @@ // common api #include "camera/CFPSCamera.hpp" #include "camera/CFreeLockCamera.hpp" +#include "camera/COrbitCamera.hpp" #include "SimpleWindowedApplication.hpp" #include "InputSystem.hpp" diff --git a/61_UI/main.cpp b/61_UI/main.cpp index 2d630b8d1..3d52fb14c 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -312,32 +312,55 @@ class UISampleApp final : public examples::SimpleWindowedApplication return false; } - if (!jCamera.contains("orientation")) - { - logFail("Expected \"orientation\" keyword for camera definition!"); - return false; - } + const bool withOrientation = jCamera.contains("orientation"); auto position = [&]() - { - auto jret = jCamera["position"].get>(); - return float32_t3(jret[0], jret[1], jret[2]); - }(); + { + auto jret = jCamera["position"].get>(); + return float32_t3(jret[0], jret[1], jret[2]); + }(); - auto orientation = [&]() - { - auto jret = jCamera["orientation"].get>(); + auto getOrientation = [&]() + { + auto jret = jCamera["orientation"].get>(); - // order important for glm::quat, - // the ctor is GLM_FUNC_QUALIFIER GLM_CONSTEXPR qua::qua(T _w, T _x, T _y, T _z) - // but memory layout (and json) is x,y,z,w - return glm::quat(jret[3], jret[0], jret[1], jret[2]); - }(); + // order important for glm::quat, + // the ctor is GLM_FUNC_QUALIFIER GLM_CONSTEXPR qua::qua(T _w, T _x, T _y, T _z) + // but memory layout (and json) is x,y,z,w + return glm::quat(jret[3], jret[0], jret[1], jret[2]); + }; + + auto getTarget = [&]() + { + auto jret = jCamera["target"].get>(); + return float32_t3(jret[0], jret[1], jret[2]); + }; if (jCamera["type"] == "FPS") - cameras.emplace_back() = make_smart_refctd_ptr(position, orientation); + { + if (!withOrientation) + { + logFail("Expected \"orientation\" keyword for FPS camera definition!"); + return false; + } + + cameras.emplace_back() = make_smart_refctd_ptr(position, getOrientation()); + } else if (jCamera["type"] == "Free") - cameras.emplace_back() = make_smart_refctd_ptr(position, orientation); + { + if (!withOrientation) + { + logFail("Expected \"orientation\" keyword for Free camera definition!"); + return false; + } + + cameras.emplace_back() = make_smart_refctd_ptr(position, getOrientation()); + } + else if (jCamera["type"] == "Orbit") + { + auto& camera = cameras.emplace_back() = make_smart_refctd_ptr(position, getTarget()); + camera->setMoveSpeedScale(0.2); + } else { logFail("Unsupported camera type!"); @@ -1152,7 +1175,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication auto& projection = planar->getPlanarProjections()[binding.boundProjectionIx.value()]; static std::vector virtualEvents(0x45); - uint32_t vCount; + uint32_t vCount = {}; projection.beginInputProcessing(m_nextPresentationTimestamp); { @@ -1161,11 +1184,33 @@ class UISampleApp final : public examples::SimpleWindowedApplication if (virtualEvents.size() < vCount) virtualEvents.resize(vCount); - projection.process(virtualEvents.data(), vCount, { params.keyboardEvents, params.mouseEvents }); + auto* orbit = dynamic_cast(camera); + + if (orbit) + { + uint32_t vKeyboardEventsCount = {}, vMouseEventsCount = {}; + + projection.processKeyboard(nullptr, vKeyboardEventsCount, {}); + projection.processMouse(nullptr, vMouseEventsCount, {}); + + auto* output = virtualEvents.data(); + + projection.processKeyboard(output, vKeyboardEventsCount, params.keyboardEvents); + output += vKeyboardEventsCount; + + if (ImGui::IsMouseDown(ImGuiMouseButton_Left)) + projection.processMouse(output, vMouseEventsCount, params.mouseEvents); + else + vMouseEventsCount = 0; + + vCount = vKeyboardEventsCount + vMouseEventsCount; + } + else + projection.process(virtualEvents.data(), vCount, { params.keyboardEvents, params.mouseEvents }); } projection.endInputProcessing(); - if(vCount) + if (vCount) camera->manipulate({ virtualEvents.data(), vCount }, ICamera::Local); } @@ -1182,6 +1227,24 @@ class UISampleApp final : public examples::SimpleWindowedApplication SImResourceInfo info; info.samplerIx = (uint16_t)nbl::ext::imgui::UI::DefaultSamplerIx::USER; + + // ORBIT CAMERA TEST + { + for (auto& planar : m_planarProjections) + { + auto* camera = planar->getCamera(); + + auto* orbit = dynamic_cast(camera); + + if (orbit) + { + auto targetPostion = transpose(getMatrix3x4As4x4(m_model))[3]; + orbit->target(targetPostion); + orbit->manipulate({}, {}); + } + } + } + // render bound planar camera views onto GUI windows if (useWindow) { @@ -1472,8 +1535,6 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGui::Image(info, contentRegionSize); ImGuizmo::SetRect(cursorPos.x, cursorPos.y, contentRegionSize.x, contentRegionSize.y); - - ImGui::End(); ImGui::PopStyleColor(1); } @@ -1721,14 +1782,27 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGui::Text("Object Ix: %s", std::to_string(active.activePlanarIx + 1u).c_str()); ImGui::Separator(); { + auto* orbit = dynamic_cast(boundCamera); + float moveSpeed = boundCamera->getMoveSpeedScale(); float rotationSpeed = boundCamera->getRotationSpeedScale(); ImGui::SliderFloat("Move speed factor", &moveSpeed, 0.0001f, 10.f, "%.4f", ImGuiSliderFlags_Logarithmic); - ImGui::SliderFloat("Rotate speed factor", &rotationSpeed, 0.0001f, 10.f, "%.4f", ImGuiSliderFlags_Logarithmic); + + if(not orbit) + ImGui::SliderFloat("Rotate speed factor", &rotationSpeed, 0.0001f, 10.f, "%.4f", ImGuiSliderFlags_Logarithmic); boundCamera->setMoveSpeedScale(moveSpeed); boundCamera->setRotationSpeedScale(rotationSpeed); + + { + if (orbit) + { + float distance = orbit->getDistance(); + ImGui::SliderFloat("Distance", &distance, COrbitCamera::MinDistance, COrbitCamera::MaxDistance, "%.4f", ImGuiSliderFlags_Logarithmic); + orbit->setDistance(distance); + } + } } if (ImGui::TreeNodeEx("World Data", flags)) diff --git a/common/include/camera/COrbitCamera.hpp b/common/include/camera/COrbitCamera.hpp new file mode 100644 index 000000000..936b1e51f --- /dev/null +++ b/common/include/camera/COrbitCamera.hpp @@ -0,0 +1,221 @@ +#ifndef _C_ORBIT_CAMERA_HPP_ +#define _C_ORBIT_CAMERA_HPP_ + +#include "ICamera.hpp" +#include + +namespace nbl::hlsl +{ + +class COrbitCamera final : public ICamera +{ +public: + using base_t = ICamera; + + COrbitCamera(const float64_t3& position, const float64_t3& target) + : base_t(), m_targetPosition(target), m_distance(length(m_targetPosition - position)), m_gimbal({ .position = position, .orientation = glm::quat(glm::vec3(0, 0, 0)) }) {} + ~COrbitCamera() = default; + + const base_t::keyboard_to_virtual_events_t getKeyboardMappingPreset() const override { return m_keyboard_to_virtual_events_preset; } + const base_t::mouse_to_virtual_events_t getMouseMappingPreset() const override { return m_mouse_to_virtual_events_preset; } + const base_t::imguizmo_to_virtual_events_t getImguizmoMappingPreset() const override { return m_imguizmo_to_virtual_events_preset; } + + const typename base_t::CGimbal& getGimbal() override { return m_gimbal; } + + inline bool setDistance(float d) + { + const auto clamped = std::clamp(d, MinDistance, MaxDistance); + const bool ok = clamped == d; + + m_distance = clamped; + + return ok; + } + + inline void target(const float64_t3& p) + { + m_targetPosition = p; + } + + virtual bool manipulate(std::span virtualEvents, base_t::ManipulationMode mode) override + { + // position on the sphere + auto S = [&](double u, double v) -> float64_t3 + { + return float64_t3 + { + std::cos(v) * std::cos(u), + std::cos(v) * std::sin(u), + std::sin(v) + } * (double) m_distance; + }; + + /* + // partial derivative of S with respect to u + auto Sdu = [&](double u, double v) -> float64_t3 + { + return float64_t3 + { + -std::cos(v) * std::sin(u), + std::cos(v)* std::cos(u), + 0 + } * (double) m_distance; + }; + */ + + double deltaU = {}, deltaV = {}, deltaDistance = {}; + + for (const auto& it : virtualEvents) + { + if (it.type == it.MoveForward) + deltaDistance += it.magnitude; + else if (it.type == it.MoveBackward) + deltaDistance -= it.magnitude; + else if (it.type == it.MoveUp) + deltaU += it.magnitude; + else if (it.type == it.MoveDown) + deltaU -= it.magnitude; + else if (it.type == it.MoveRight) + deltaV += it.magnitude; + else if (it.type == it.MoveLeft) + deltaV -= it.magnitude; + } + + // TODO! + constexpr auto nastyScalar = 0.01; + deltaU *= nastyScalar * m_moveSpeedScale; + deltaV *= nastyScalar * m_moveSpeedScale; + + u += deltaU; + v += deltaV; + + m_distance = std::clamp(m_distance += deltaDistance * nastyScalar, MinDistance, MaxDistance); + + // partial derivative of S with respect to v + auto Sdv = [&](double u, double v) -> float64_t3 + { + return float64_t3 + { + -std::sin(v) * std::cos(u), + -std::sin(v) * std::sin(u), + std::cos(v) + } * (double) m_distance; + }; + + const auto localSpherePostion = S(u, v); + const auto newPosition = localSpherePostion + m_targetPosition; + + // note we are not using Sdu (though we could!) + // instead we benefit from forward we have for free when moving on sphere surface + // and given up vector obtained from partial derivative we can easily get right vector, this way + // we don't have frenet frame flip we would have with Sdu, however it could be adjusted anyway, less code + + const auto newUp = normalize(Sdv(u, v)); + const auto newForward = normalize(-localSpherePostion); + const auto newRight = normalize(cross(newUp, newForward)); + + const auto newOrientation = glm::quat_cast + ( + glm::dmat3 + { + newRight, + newUp, + newForward + } + ); + + m_gimbal.begin(); + { + m_gimbal.setPosition(newPosition); + m_gimbal.setOrientation(newOrientation); + } + m_gimbal.end(); + + bool manipulated = bool(m_gimbal.getManipulationCounter()); + + if (manipulated) + m_gimbal.updateView(); + + return manipulated; + } + + virtual const uint32_t getAllowedVirtualEvents(base_t::ManipulationMode mode) override + { + switch (mode) + { + case base_t::Local: return AllowedLocalVirtualEvents; + case base_t::World: return AllowedWorldVirtualEvents; + default: return CVirtualGimbalEvent::None; + } + } + + virtual const std::string_view getIdentifier() override + { + return "Orbit Camera"; + } + + inline float getDistance() { return m_distance; } + inline double getU() { return u; } + inline double getV() { return v; } + + static inline constexpr float MinDistance = 0.1f; + static inline constexpr float MaxDistance = 10000.f; + +private: + float64_t3 m_targetPosition; + float m_distance; + typename base_t::CGimbal m_gimbal; + + double u = {}, v = {}; + + static inline constexpr auto AllowedLocalVirtualEvents = CVirtualGimbalEvent::Translate; + static inline constexpr auto AllowedWorldVirtualEvents = CVirtualGimbalEvent::None; + + static inline const auto m_keyboard_to_virtual_events_preset = []() + { + typename base_t::keyboard_to_virtual_events_t preset; + + preset[ui::E_KEY_CODE::EKC_W] = CVirtualGimbalEvent::MoveUp; + preset[ui::E_KEY_CODE::EKC_S] = CVirtualGimbalEvent::MoveDown; + preset[ui::E_KEY_CODE::EKC_A] = CVirtualGimbalEvent::MoveLeft; + preset[ui::E_KEY_CODE::EKC_D] = CVirtualGimbalEvent::MoveRight; + preset[ui::E_KEY_CODE::EKC_E] = CVirtualGimbalEvent::MoveForward; + preset[ui::E_KEY_CODE::EKC_Q] = CVirtualGimbalEvent::MoveBackward; + + return preset; + }(); + + static inline const auto m_mouse_to_virtual_events_preset = []() + { + typename base_t::mouse_to_virtual_events_t preset; + + preset[ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_X] = CVirtualGimbalEvent::MoveRight; + preset[ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_X] = CVirtualGimbalEvent::MoveLeft; + preset[ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_Y] = CVirtualGimbalEvent::MoveUp; + preset[ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_Y] = CVirtualGimbalEvent::MoveDown; + preset[ui::E_MOUSE_CODE::EMC_VERTICAL_POSITIVE_SCROLL] = CVirtualGimbalEvent::MoveForward; + preset[ui::E_MOUSE_CODE::EMC_HORIZONTAL_POSITIVE_SCROLL] = CVirtualGimbalEvent::MoveForward; + preset[ui::E_MOUSE_CODE::EMC_VERTICAL_NEGATIVE_SCROLL] = CVirtualGimbalEvent::MoveBackward; + preset[ui::E_MOUSE_CODE::EMC_HORIZONTAL_NEGATIVE_SCROLL] = CVirtualGimbalEvent::MoveBackward; + + return preset; + }(); + + static inline const auto m_imguizmo_to_virtual_events_preset = []() + { + typename base_t::imguizmo_to_virtual_events_t preset; + + preset[CVirtualGimbalEvent::MoveForward] = CVirtualGimbalEvent::MoveForward; + preset[CVirtualGimbalEvent::MoveBackward] = CVirtualGimbalEvent::MoveBackward; + preset[CVirtualGimbalEvent::MoveLeft] = CVirtualGimbalEvent::MoveLeft; + preset[CVirtualGimbalEvent::MoveRight] = CVirtualGimbalEvent::MoveRight; + preset[CVirtualGimbalEvent::MoveUp] = CVirtualGimbalEvent::MoveUp; + preset[CVirtualGimbalEvent::MoveDown] = CVirtualGimbalEvent::MoveDown; + + return preset; + }(); +}; + +} + +#endif // _C_ORBIT_CAMERA_HPP_ \ No newline at end of file diff --git a/common/include/camera/IGimbal.hpp b/common/include/camera/IGimbal.hpp index 1a94affc5..c70c2bbab 100644 --- a/common/include/camera/IGimbal.hpp +++ b/common/include/camera/IGimbal.hpp @@ -317,7 +317,7 @@ namespace nbl::hlsl move(getZAxis() * distance); } - void end() + inline void end() { m_isManipulating = false; } diff --git a/common/include/camera/IGimbalController.hpp b/common/include/camera/IGimbalController.hpp index 770b5ed16..7b4ba365e 100644 --- a/common/include/camera/IGimbalController.hpp +++ b/common/include/camera/IGimbalController.hpp @@ -182,7 +182,6 @@ class IGimbalController : public IGimbalManipulateEncoder virtual const mouse_to_virtual_events_t& getMouseVirtualEventMap() const override { return m_mouseVirtualEventMap; } virtual const imguizmo_to_virtual_events_t& getImguizmoVirtualEventMap() const override { return m_imguizmoVirtualEventMap; } -private: /** * @brief Processes keyboard events to generate virtual manipulation events. * @@ -405,6 +404,8 @@ class IGimbalController : public IGimbalManipulateEncoder } } +private: + //! helper utility, for any controller this should be called before any update of hash map void preprocess(auto& map) { From 1810e70801e1a1d80e266671d51093a1a1860279 Mon Sep 17 00:00:00 2001 From: YasInvolved Date: Tue, 31 Dec 2024 17:35:08 +0100 Subject: [PATCH 085/205] make mouse stay in window area when camera movement is enabled --- 61_UI/main.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/61_UI/main.cpp b/61_UI/main.cpp index 2d630b8d1..8dfbb603b 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -1677,6 +1677,15 @@ class UISampleApp final : public examples::SimpleWindowedApplication io.ConfigFlags |= ImGuiConfigFlags_NoMouse; io.MouseDrawCursor = false; io.WantCaptureMouse = false; + + ImVec2 cursorPos = ImGui::GetMousePos(); + ImVec2 viewportSize = io.DisplaySize; + auto* cc = m_window->getCursorControl(); + int32_t posX = m_window->getX(); + int32_t posY = m_window->getY(); + + const ICursorControl::SPosition middle{ static_cast(viewportSize.x/2 + posX), static_cast(viewportSize.y/2 + posY) }; + cc->setPosition(middle); } else { From e0c284a82ec7b41b5b2be4222579604665e5f896 Mon Sep 17 00:00:00 2001 From: YasInvolved Date: Mon, 13 Jan 2025 21:52:21 +0100 Subject: [PATCH 086/205] small optimization --- 61_UI/main.cpp | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/61_UI/main.cpp b/61_UI/main.cpp index 8dfbb603b..db8955320 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -1674,6 +1674,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication if (enableActiveCameraMovement) { + ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), "Bound Camera Movement: Enabled"); io.ConfigFlags |= ImGuiConfigFlags_NoMouse; io.MouseDrawCursor = false; io.WantCaptureMouse = false; @@ -1689,15 +1690,11 @@ class UISampleApp final : public examples::SimpleWindowedApplication } else { + ImGui::TextColored(ImVec4(1.0f, 0.0f, 0.0f, 1.0f), "Bound Camera Movement: Disabled"); io.ConfigFlags &= ~ImGuiConfigFlags_NoMouse; io.MouseDrawCursor = true; io.WantCaptureMouse = true; - } - - if (enableActiveCameraMovement) - ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), "Bound Camera Movement: Enabled"); - else - ImGui::TextColored(ImVec4(1.0f, 0.0f, 0.0f, 1.0f), "Bound Camera Movement: Disabled"); + } if (ImGui::IsItemHovered()) { From 2316f8fcb55ebe136343e4e726241c06af250947 Mon Sep 17 00:00:00 2001 From: YasInvolved Date: Mon, 13 Jan 2025 22:32:57 +0100 Subject: [PATCH 087/205] cursor behaviour choice menu --- 61_UI/main.cpp | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/61_UI/main.cpp b/61_UI/main.cpp index db8955320..85aca2a63 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -1666,6 +1666,17 @@ class UISampleApp final : public examples::SimpleWindowedApplication default: break; } + { + if (ImGui::TreeNodeEx("Cursor Behaviour")) + { + if (ImGui::RadioButton("Clamp to the window", !resetCursorToCenter)) + resetCursorToCenter = false; + if (ImGui::RadioButton("Reset to the window center", resetCursorToCenter)) + resetCursorToCenter = true; + ImGui::TreePop(); + } + } + { ImGuiIO& io = ImGui::GetIO(); @@ -1685,8 +1696,19 @@ class UISampleApp final : public examples::SimpleWindowedApplication int32_t posX = m_window->getX(); int32_t posY = m_window->getY(); - const ICursorControl::SPosition middle{ static_cast(viewportSize.x/2 + posX), static_cast(viewportSize.y/2 + posY) }; - cc->setPosition(middle); + if (resetCursorToCenter) + { + const ICursorControl::SPosition middle{ static_cast(viewportSize.x / 2 + posX), static_cast(viewportSize.y / 2 + posY) }; + cc->setPosition(middle); + } + else + { + auto currentCursorPos = cc->getPosition(); + ICursorControl::SPosition newPos{}; + newPos.x = std::clamp(currentCursorPos.x, posX, viewportSize.x + posX); + newPos.y = std::clamp(currentCursorPos.y, posY, viewportSize.y + posY); + cc->setPosition(newPos); + } } else { @@ -1694,7 +1716,8 @@ class UISampleApp final : public examples::SimpleWindowedApplication io.ConfigFlags &= ~ImGuiConfigFlags_NoMouse; io.MouseDrawCursor = true; io.WantCaptureMouse = true; - } + } + if (ImGui::IsItemHovered()) { @@ -2121,6 +2144,8 @@ class UISampleApp final : public examples::SimpleWindowedApplication bool enableActiveCameraMovement = false; + bool resetCursorToCenter = false; + struct windowControlBinding { nbl::core::smart_refctd_ptr scene; From c43fa03418a7e9cf6aa641c92a677a1e0ffd86cc Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Fri, 24 Jan 2025 12:39:48 +0100 Subject: [PATCH 088/205] wooops thats funny, correct scope target definitions for UI ex --- 61_UI/CMakeLists.txt | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/61_UI/CMakeLists.txt b/61_UI/CMakeLists.txt index 42425b0a8..d8fe0da7f 100644 --- a/61_UI/CMakeLists.txt +++ b/61_UI/CMakeLists.txt @@ -15,25 +15,25 @@ if(NBL_BUILD_IMGUI) nbl_create_executable_project("${NBL_EXTRA_SOURCES}" "" "${NBL_INCLUDE_SERACH_DIRECTORIES}" "${NBL_LIBRARIES}" "${NBL_EXECUTABLE_PROJECT_CREATION_PCH_TARGET}") LINK_BUILTIN_RESOURCES_TO_TARGET(${EXECUTABLE_NAME} geometryCreatorSpirvBRD) -endif() -if(NBL_EMBED_BUILTIN_RESOURCES) - set(_BR_TARGET_ ${EXECUTABLE_NAME}_builtinResourceData) - set(RESOURCE_DIR "app_resources") + if(NBL_EMBED_BUILTIN_RESOURCES) + set(_BR_TARGET_ ${EXECUTABLE_NAME}_builtinResourceData) + set(RESOURCE_DIR "app_resources") - get_filename_component(_SEARCH_DIRECTORIES_ "${CMAKE_CURRENT_SOURCE_DIR}" ABSOLUTE) - get_filename_component(_OUTPUT_DIRECTORY_SOURCE_ "${CMAKE_CURRENT_BINARY_DIR}/src" ABSOLUTE) - get_filename_component(_OUTPUT_DIRECTORY_HEADER_ "${CMAKE_CURRENT_BINARY_DIR}/include" ABSOLUTE) - - file(GLOB_RECURSE BUILTIN_RESOURCE_FILES RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}/${RESOURCE_DIR}" CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/${RESOURCE_DIR}/*") - foreach(RES_FILE ${BUILTIN_RESOURCE_FILES}) - LIST_BUILTIN_RESOURCE(RESOURCES_TO_EMBED "${RES_FILE}") - endforeach() + get_filename_component(_SEARCH_DIRECTORIES_ "${CMAKE_CURRENT_SOURCE_DIR}" ABSOLUTE) + get_filename_component(_OUTPUT_DIRECTORY_SOURCE_ "${CMAKE_CURRENT_BINARY_DIR}/src" ABSOLUTE) + get_filename_component(_OUTPUT_DIRECTORY_HEADER_ "${CMAKE_CURRENT_BINARY_DIR}/include" ABSOLUTE) + + file(GLOB_RECURSE BUILTIN_RESOURCE_FILES RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}/${RESOURCE_DIR}" CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/${RESOURCE_DIR}/*") + foreach(RES_FILE ${BUILTIN_RESOURCE_FILES}) + LIST_BUILTIN_RESOURCE(RESOURCES_TO_EMBED "${RES_FILE}") + endforeach() - ADD_CUSTOM_BUILTIN_RESOURCES(${_BR_TARGET_} RESOURCES_TO_EMBED "${_SEARCH_DIRECTORIES_}" "${RESOURCE_DIR}" "nbl::this_example::builtin" "${_OUTPUT_DIRECTORY_HEADER_}" "${_OUTPUT_DIRECTORY_SOURCE_}") + ADD_CUSTOM_BUILTIN_RESOURCES(${_BR_TARGET_} RESOURCES_TO_EMBED "${_SEARCH_DIRECTORIES_}" "${RESOURCE_DIR}" "nbl::this_example::builtin" "${_OUTPUT_DIRECTORY_HEADER_}" "${_OUTPUT_DIRECTORY_SOURCE_}") - LINK_BUILTIN_RESOURCES_TO_TARGET(${EXECUTABLE_NAME} ${_BR_TARGET_}) -endif() + LINK_BUILTIN_RESOURCES_TO_TARGET(${EXECUTABLE_NAME} ${_BR_TARGET_}) + endif() -add_dependencies(${EXECUTABLE_NAME} argparse) -target_include_directories(${EXECUTABLE_NAME} PUBLIC $) \ No newline at end of file + add_dependencies(${EXECUTABLE_NAME} argparse) + target_include_directories(${EXECUTABLE_NAME} PUBLIC $) +endif() \ No newline at end of file From 09e882dc64bb29c457c59c40328521406f16551d Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Fri, 24 Jan 2025 12:58:11 +0100 Subject: [PATCH 089/205] a few transpose updates due to ambiguity, need Ali changes to apply and pull master --- 61_UI/main.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/61_UI/main.cpp b/61_UI/main.cpp index b6def6285..f56f45390 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -1238,7 +1238,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication if (orbit) { - auto targetPostion = transpose(getMatrix3x4As4x4(m_model))[3]; + auto targetPostion = core::transpose(getMatrix3x4As4x4(m_model))[3]; orbit->target(targetPostion); orbit->manipulate({}, {}); } @@ -1327,8 +1327,8 @@ class UISampleApp final : public examples::SimpleWindowedApplication // we render a scene from view of a camera bound to planar window ImGuizmoPlanarM16InOut imguizmoPlanar; - imguizmoPlanar.view = getCastedMatrix(transpose(getMatrix3x4As4x4(planarViewCameraBound->getGimbal().getViewMatrix()))); - imguizmoPlanar.projection = getCastedMatrix(transpose(projection.getProjectionMatrix())); + imguizmoPlanar.view = getCastedMatrix(core::transpose(getMatrix3x4As4x4(planarViewCameraBound->getGimbal().getViewMatrix()))); + imguizmoPlanar.projection = getCastedMatrix(core::transpose(projection.getProjectionMatrix())); if (flipGizmoY) // note we allow to flip gizmo just to match our coordinates imguizmoPlanar.projection[1][1] *= -1.f; // https://johannesugb.github.io/gpu-programming/why-do-opengl-proj-matrices-fail-in-vulkan/ @@ -1369,7 +1369,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication imguizmoModel.inTRS = gimbalToImguizmoTRS(getCastedMatrix(targetGimbalManipulationCamera->getGimbal()())); } else - imguizmoModel.inTRS = transpose(getMatrix3x4As4x4(m_model)); + imguizmoModel.inTRS = core::transpose(getMatrix3x4As4x4(m_model)); imguizmoModel.outTRS = imguizmoModel.inTRS; { @@ -1461,7 +1461,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication else { // again, for scene demo model full affine transformation without limits is assumed - m_model = float32_t3x4(transpose(imguizmoModel.outTRS)); + m_model = float32_t3x4(core::transpose(imguizmoModel.outTRS)); boundCameraToManipulate = nullptr; boundPlanarCameraIxToManipulate = std::nullopt; } @@ -1930,7 +1930,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication if (boundCameraToManipulate) imguizmoModel.inTRS = gimbalToImguizmoTRS(getCastedMatrix(boundCameraToManipulate->getGimbal()())); else - imguizmoModel.inTRS = transpose(getMatrix3x4As4x4(m_model)); + imguizmoModel.inTRS = core::transpose(getMatrix3x4As4x4(m_model)); imguizmoModel.outTRS = imguizmoModel.inTRS; float* m16TRSmatrix = &imguizmoModel.outTRS[0][0]; @@ -2114,7 +2114,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication else { // for scene demo model full affine transformation without limits is assumed - m_model = float32_t3x4(transpose(imguizmoModel.outTRS)); + m_model = float32_t3x4(core::transpose(imguizmoModel.outTRS)); } } } From f1e87aecb779eb584cf8c1196c7d589f24afb53a Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Fri, 24 Jan 2025 13:58:24 +0100 Subject: [PATCH 090/205] core::transpose to hlsl::transpose --- 61_UI/main.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/61_UI/main.cpp b/61_UI/main.cpp index f56f45390..f90b2610e 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -1238,7 +1238,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication if (orbit) { - auto targetPostion = core::transpose(getMatrix3x4As4x4(m_model))[3]; + auto targetPostion = hlsl::transpose(getMatrix3x4As4x4(m_model))[3]; orbit->target(targetPostion); orbit->manipulate({}, {}); } @@ -1327,8 +1327,8 @@ class UISampleApp final : public examples::SimpleWindowedApplication // we render a scene from view of a camera bound to planar window ImGuizmoPlanarM16InOut imguizmoPlanar; - imguizmoPlanar.view = getCastedMatrix(core::transpose(getMatrix3x4As4x4(planarViewCameraBound->getGimbal().getViewMatrix()))); - imguizmoPlanar.projection = getCastedMatrix(core::transpose(projection.getProjectionMatrix())); + imguizmoPlanar.view = getCastedMatrix(hlsl::transpose(getMatrix3x4As4x4(planarViewCameraBound->getGimbal().getViewMatrix()))); + imguizmoPlanar.projection = getCastedMatrix(hlsl::transpose(projection.getProjectionMatrix())); if (flipGizmoY) // note we allow to flip gizmo just to match our coordinates imguizmoPlanar.projection[1][1] *= -1.f; // https://johannesugb.github.io/gpu-programming/why-do-opengl-proj-matrices-fail-in-vulkan/ @@ -1369,7 +1369,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication imguizmoModel.inTRS = gimbalToImguizmoTRS(getCastedMatrix(targetGimbalManipulationCamera->getGimbal()())); } else - imguizmoModel.inTRS = core::transpose(getMatrix3x4As4x4(m_model)); + imguizmoModel.inTRS = hlsl::transpose(getMatrix3x4As4x4(m_model)); imguizmoModel.outTRS = imguizmoModel.inTRS; { @@ -1461,7 +1461,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication else { // again, for scene demo model full affine transformation without limits is assumed - m_model = float32_t3x4(core::transpose(imguizmoModel.outTRS)); + m_model = float32_t3x4(hlsl::transpose(imguizmoModel.outTRS)); boundCameraToManipulate = nullptr; boundPlanarCameraIxToManipulate = std::nullopt; } @@ -1930,7 +1930,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication if (boundCameraToManipulate) imguizmoModel.inTRS = gimbalToImguizmoTRS(getCastedMatrix(boundCameraToManipulate->getGimbal()())); else - imguizmoModel.inTRS = core::transpose(getMatrix3x4As4x4(m_model)); + imguizmoModel.inTRS = hlsl::transpose(getMatrix3x4As4x4(m_model)); imguizmoModel.outTRS = imguizmoModel.inTRS; float* m16TRSmatrix = &imguizmoModel.outTRS[0][0]; @@ -2114,7 +2114,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication else { // for scene demo model full affine transformation without limits is assumed - m_model = float32_t3x4(core::transpose(imguizmoModel.outTRS)); + m_model = float32_t3x4(hlsl::transpose(imguizmoModel.outTRS)); } } } From c50beb0566a6edcdf4eaef5137424f6794275f88 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Fri, 31 Jan 2025 17:14:55 +0100 Subject: [PATCH 091/205] correct bugs, save changes with tests, allow gizmos to controller cameras & adjust to changes - take reference frame with ::manipulate parameter; I need to inspect a few issues with reference events --- 61_UI/main.cpp | 146 +++++++++++----------- common/include/camera/CFPSCamera.hpp | 76 +++++++---- common/include/camera/CFreeLockCamera.hpp | 35 ++---- common/include/camera/COrbitCamera.hpp | 14 +-- common/include/camera/ICamera.hpp | 32 +---- common/include/camera/IGimbal.hpp | 79 ++++++++---- 6 files changed, 192 insertions(+), 190 deletions(-) diff --git a/61_UI/main.cpp b/61_UI/main.cpp index f90b2610e..0c051fb60 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -1211,7 +1211,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication projection.endInputProcessing(); if (vCount) - camera->manipulate({ virtualEvents.data(), vCount }, ICamera::Local); + camera->manipulate({ virtualEvents.data(), vCount }); } m_ui.manager->update(params); @@ -1350,7 +1350,6 @@ class UISampleApp final : public examples::SimpleWindowedApplication const bool isCameraGimbalTarget = modelIx; // I assume scene demo model is 0th ix, left are planar cameras ICamera* const targetGimbalManipulationCamera = isCameraGimbalTarget ? m_planarProjections[modelIx - 1u]->getCamera() : nullptr; - bool discard = isCameraGimbalTarget && mCurrentGizmoOperation != ImGuizmo::TRANSLATE; // discard WiP stuff // if we try to manipulate a camera which appears to be the same camera we see scene from then obvsly it doesn't make sense to manipulate its gizmo so we skip it // EDIT: it actually makes some sense if you assume render planar view is rendered with ortho projection, but we would need to add imguizmo controller virtual map @@ -1373,98 +1372,94 @@ class UISampleApp final : public examples::SimpleWindowedApplication imguizmoModel.outTRS = imguizmoModel.inTRS; { - const bool success = ImGuizmo::Manipulate(&imguizmoPlanar.view[0][0], &imguizmoPlanar.projection[0][0], mCurrentGizmoOperation, mCurrentGizmoMode, &imguizmoModel.outTRS[0][0], &imguizmoModel.outDeltaTRS[0][0], useSnap ? &snap[0] : nullptr); + const bool success = ImGuizmo::Manipulate(&imguizmoPlanar.view[0][0], &imguizmoPlanar.projection[0][0], ImGuizmo::OPERATION::UNIVERSAL, mCurrentGizmoMode, &imguizmoModel.outTRS[0][0], &imguizmoModel.outDeltaTRS[0][0], useSnap ? &snap[0] : nullptr); if (success) { - ++manipulationCounter; - discard |= manipulationCounter > 1; - - /* - - NOTE to self & TODO: I must be killing ImGuizmo last invocation cache or something with my delta events (or at least I dont - see now where I'm *maybe* using its API incorrectly, the problem is I get translation parts in delta matrix when doing - rotations hence it glitches my cameras -> on the other hand I can see it kinda correctly outputs the delta matrix when - gc model is bound - - auto postprocessDeltaManipulation = [](const float32_t4x4& inDeltaMatrix, float32_t4x4& outDeltaMatrix, ImGuizmo::OPERATION operation) -> void + if (targetGimbalManipulationCamera) { - struct - { - float32_t3 dTranslation, dRotation, dScale; - } world; + boundCameraToManipulate = smart_refctd_ptr(targetGimbalManipulationCamera); + boundPlanarCameraIxToManipulate = modelIx - 1u; - ImGuizmo::DecomposeMatrixToComponents(&inDeltaMatrix[0][0], &world.dTranslation[0], &world.dRotation[0], &world.dScale[0]); + auto& gimbal = (ICamera::CGimbal&)targetGimbalManipulationCamera->getGimbal(); - if (operation == ImGuizmo::TRANSLATE) - { - world.dRotation = float32_t3(0, 0, 0); - } - else if (operation == ImGuizmo::ROTATE) + struct { - world.dTranslation = float32_t3(0, 0, 0); - } - - ImGuizmo::RecomposeMatrixFromComponents(&world.dTranslation[0], &world.dRotation[0], &world.dScale[0], &outDeltaMatrix[0][0]); - }; - - postprocessDeltaManipulation(imguizmoModel.outDeltaTRS, imguizmoModel.outDeltaTRS, mCurrentGizmoOperation); - */ + float32_t3 t, r, s; + } out, delta; - if (!discard) - { - if (targetGimbalManipulationCamera) + ImGuizmo::DecomposeMatrixToComponents(&imguizmoModel.outTRS[0][0], &out.t[0], &out.r[0], &out.s[0]); + ImGuizmo::DecomposeMatrixToComponents(&imguizmoModel.outDeltaTRS[0][0], &delta.t[0], &delta.r[0], &delta.s[0]); { - boundCameraToManipulate = smart_refctd_ptr(targetGimbalManipulationCamera); - boundPlanarCameraIxToManipulate = modelIx - 1u; - - /* - testing imguizmo controller for target camera, we use delta world imguizmo TRS matrix to generate virtual events - */ - + std::vector virtualEvents; + + auto requestMagnitudeUpdateWithScalar = [&](float signPivot, float dScalar, float dMagnitude, auto positive, auto negative) { - static std::vector virtualEvents(0x45); - uint32_t vCount = {}; - - targetGimbalManipulationCamera->beginInputProcessing(m_nextPresentationTimestamp); + if (dScalar != signPivot) { - targetGimbalManipulationCamera->process(nullptr, vCount); - - if (virtualEvents.size() < vCount) - virtualEvents.resize(vCount); + auto& ev = virtualEvents.emplace_back(); + auto code = (dScalar > signPivot) ? positive : negative; - IGimbalController::SUpdateParameters params; - params.imguizmoEvents = { { imguizmoModel.outDeltaTRS } }; - targetGimbalManipulationCamera->process(virtualEvents.data(), vCount, params); + ev.type = code; + ev.magnitude += dMagnitude; } - targetGimbalManipulationCamera->endInputProcessing(); + }; + + // Delta translation impulse + requestMagnitudeUpdateWithScalar(0.f, delta.t[0], std::abs(delta.t[0]), CVirtualGimbalEvent::VirtualEventType::MoveRight, CVirtualGimbalEvent::VirtualEventType::MoveLeft); + requestMagnitudeUpdateWithScalar(0.f, delta.t[1], std::abs(delta.t[1]), CVirtualGimbalEvent::VirtualEventType::MoveUp, CVirtualGimbalEvent::VirtualEventType::MoveDown); + requestMagnitudeUpdateWithScalar(0.f, delta.t[2], std::abs(delta.t[2]), CVirtualGimbalEvent::VirtualEventType::MoveForward, CVirtualGimbalEvent::VirtualEventType::MoveBackward); + + // Delta rotation impulse + requestMagnitudeUpdateWithScalar(0.f, delta.r[0], std::abs(delta.r[0]), CVirtualGimbalEvent::VirtualEventType::TiltUp, CVirtualGimbalEvent::VirtualEventType::TiltDown); + requestMagnitudeUpdateWithScalar(0.f, delta.r[1], std::abs(delta.r[1]), CVirtualGimbalEvent::VirtualEventType::PanRight, CVirtualGimbalEvent::VirtualEventType::PanLeft); + requestMagnitudeUpdateWithScalar(0.f, delta.r[2], std::abs(delta.r[2]), CVirtualGimbalEvent::VirtualEventType::RollRight, CVirtualGimbalEvent::VirtualEventType::RollLeft); + + // Delta scale impulse + requestMagnitudeUpdateWithScalar(1.f, delta.s[0], std::abs(delta.s[0]), CVirtualGimbalEvent::VirtualEventType::ScaleXInc, CVirtualGimbalEvent::VirtualEventType::ScaleXDec); + requestMagnitudeUpdateWithScalar(1.f, delta.s[1], std::abs(delta.s[1]), CVirtualGimbalEvent::VirtualEventType::ScaleYInc, CVirtualGimbalEvent::VirtualEventType::ScaleYDec); + requestMagnitudeUpdateWithScalar(1.f, delta.s[2], std::abs(delta.s[2]), CVirtualGimbalEvent::VirtualEventType::ScaleZInc, CVirtualGimbalEvent::VirtualEventType::ScaleZDec); + + constexpr auto TESSTTSS = CVirtualGimbalEvent::All; + const auto impulse = gimbal.accumulate(virtualEvents); + + assert(impulse.dVirtualTranslate == float64_t3(delta.t)); + assert(impulse.dVirtualRotation == float64_t3(delta.r)); + assert(impulse.dVirtualScale == float64_t3(delta.s)); - if (vCount) - { - const float pMoveSpeed = targetGimbalManipulationCamera->getMoveSpeedScale(); - const float pRotationSpeed = targetGimbalManipulationCamera->getRotationSpeedScale(); + const auto vCount = virtualEvents.size(); - // I start to think controller should be able to set sensitivity to scale magnitudes of generated events - // in order for camera to not keep any magnitude scalars like move or rotation speed scales + if (vCount) + { + const float pMoveSpeed = targetGimbalManipulationCamera->getMoveSpeedScale(); + const float pRotationSpeed = targetGimbalManipulationCamera->getRotationSpeedScale(); - targetGimbalManipulationCamera->setMoveSpeedScale(1); - targetGimbalManipulationCamera->setRotationSpeedScale(1); + // I start to think controller should be able to set sensitivity to scale magnitudes of generated events + // in order for camera to not keep any magnitude scalars like move or rotation speed scales - // NOTE: generated events from ImGuizmo controller are always in world space! - targetGimbalManipulationCamera->manipulate({ virtualEvents.data(), vCount }, ICamera::World); + targetGimbalManipulationCamera->setMoveSpeedScale(1); + targetGimbalManipulationCamera->setRotationSpeedScale(1); - targetGimbalManipulationCamera->setMoveSpeedScale(pMoveSpeed); - targetGimbalManipulationCamera->setRotationSpeedScale(pRotationSpeed); - } + /* + new = old * delta + delta^(-1) * new = old + */ + + const auto referenceFrame = getCastedMatrix(mul(inverse(imguizmoModel.outDeltaTRS), imguizmoModel.outTRS)); + targetGimbalManipulationCamera->manipulate({ virtualEvents.data(), vCount }, &referenceFrame); + + targetGimbalManipulationCamera->setMoveSpeedScale(pMoveSpeed); + targetGimbalManipulationCamera->setRotationSpeedScale(pRotationSpeed); } + } - else - { - // again, for scene demo model full affine transformation without limits is assumed - m_model = float32_t3x4(hlsl::transpose(imguizmoModel.outTRS)); - boundCameraToManipulate = nullptr; - boundPlanarCameraIxToManipulate = std::nullopt; - } + } + else + { + // again, for scene demo model full affine transformation without limits is assumed + m_model = float32_t3x4(hlsl::transpose(imguizmoModel.outTRS)); + boundCameraToManipulate = nullptr; + boundPlanarCameraIxToManipulate = std::nullopt; } } @@ -2102,8 +2097,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication boundCameraToManipulate->setMoveSpeedScale(1); boundCameraToManipulate->setRotationSpeedScale(1); - // NOTE: generated events from ImGuizmo controller are always in world space! - boundCameraToManipulate->manipulate({ virtualEvents.data(), vCount }, ICamera::World); + boundCameraToManipulate->manipulate({ virtualEvents.data(), vCount }); boundCameraToManipulate->setMoveSpeedScale(pmSpeed); boundCameraToManipulate->setRotationSpeedScale(prSpeed); diff --git a/common/include/camera/CFPSCamera.hpp b/common/include/camera/CFPSCamera.hpp index cb00fe442..a99d30f5c 100644 --- a/common/include/camera/CFPSCamera.hpp +++ b/common/include/camera/CFPSCamera.hpp @@ -1,4 +1,4 @@ -// Copyright (C) 2018-2020 - DevSH Graphics Programming Sp. z O.O. +// 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 @@ -17,7 +17,24 @@ class CFPSCamera final : public ICamera using base_t = ICamera; CFPSCamera(const float64_t3& position, const glm::quat& orientation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f)) - : base_t(), m_gimbal({ .position = position, .orientation = orientation }) {} + : base_t(), m_gimbal({ .position = position, .orientation = orientation }) + { + m_gimbal.begin(); + { + const auto& gForward = m_gimbal.getZAxis(); + const float gPitch = atan2(glm::length(glm::vec2(gForward.x, gForward.z)), gForward.y) - glm::half_pi(), gYaw = atan2(gForward.x, gForward.z); + + + + + auto test = glm::quat(glm::vec3(gPitch, gYaw, 0.0f)); + + glm::vec3 euler = glm::eulerAngles(test); + + m_gimbal.setOrientation(test); + } + m_gimbal.end(); + } ~CFPSCamera() = default; const base_t::keyboard_to_virtual_events_t getKeyboardMappingPreset() const override { return m_keyboard_to_virtual_events_preset; } @@ -29,51 +46,55 @@ class CFPSCamera final : public ICamera return m_gimbal; } - virtual bool manipulate(std::span virtualEvents, base_t::ManipulationMode mode) override + virtual bool manipulate(std::span virtualEvents, const float64_t4x4 const* referenceFrame = nullptr) override { - constexpr double MaxVerticalAngle = glm::radians(88.0f), MinVerticalAngle = -MaxVerticalAngle; - if (!virtualEvents.size()) return false; - const auto& gForward = m_gimbal.getZAxis(); - const float gPitch = atan2(glm::length(glm::vec2(gForward.x, gForward.z)), gForward.y) - glm::half_pi(), gYaw = atan2(gForward.x, gForward.z); - - glm::quat newOrientation; float64_t3 newPosition; + //! true if virtual events are with respect to the moving gimbal frame, otherwise it is assumed deltas are generated for fixed reference frame + const bool isMovingReference = not referenceFrame; - // TODO: I make assumption what world base is now (at least temporary), I need to think of this but maybe each ITransformObject should know what its world is - // in ideal scenario we would define this crazy enum with all possible standard bases - const auto impulse = mode == base_t::Local ? m_gimbal.accumulate(virtualEvents) - : m_gimbal.accumulate(virtualEvents, { 1, 0, 0 }, { 0, 1, 0 }, { 0, 0, 1 }); + CReferenceTransform reference; + if (not m_gimbal.extractReferenceTransform(&reference, referenceFrame)) + return false; - const auto newPitch = std::clamp(gPitch + impulse.dVirtualRotation.x * m_rotationSpeedScale, MinVerticalAngle, MaxVerticalAngle), newYaw = gYaw + impulse.dVirtualRotation.y * m_rotationSpeedScale; - newOrientation = glm::quat(glm::vec3(newPitch, newYaw, 0.0f)); newPosition = m_gimbal.getPosition() + impulse.dVirtualTranslate * m_moveSpeedScale; + auto impulse = m_gimbal.accumulate(virtualEvents); bool manipulated = true; m_gimbal.begin(); { - m_gimbal.setOrientation(newOrientation); - m_gimbal.setPosition(newPosition); + if (isMovingReference) + { + const auto& gForward = m_gimbal.getZAxis(); + const float gPitch = atan2(glm::length(glm::vec2(gForward.x, gForward.z)), gForward.y) - glm::half_pi(), gYaw = atan2(gForward.x, gForward.z); + const float newPitch = std::clamp(gPitch + impulse.dVirtualRotation.x * m_rotationSpeedScale, MinVerticalAngle, MaxVerticalAngle), newYaw = gYaw + impulse.dVirtualRotation.y * m_rotationSpeedScale; + + m_gimbal.setOrientation(glm::quat(glm::vec3(newPitch, newYaw, 0.0f))); + m_gimbal.setPosition(m_gimbal.getPosition() + mul(impulse.dVirtualTranslate * m_moveSpeedScale, m_gimbal.getOrthonornalMatrix())); + } + else + { + // TODO: I need to think of this, the problem is that since reference frame may not be aligned with world nicely it means events must be transformed + // what FPS camera really does is: + // tilt around FIXED world (0,1,0) vector + // pitch around REFERENCE right vector + m_gimbal.transform(reference, impulse); + } } m_gimbal.end(); manipulated &= bool(m_gimbal.getManipulationCounter()); - if(manipulated) + if (manipulated) m_gimbal.updateView(); return manipulated; } - virtual const uint32_t getAllowedVirtualEvents(base_t::ManipulationMode mode) override + virtual const uint32_t getAllowedVirtualEvents() override { - switch (mode) - { - case base_t::Local: return AllowedLocalVirtualEvents; - case base_t::World: return AllowedWorldVirtualEvents; - default: return CVirtualGimbalEvent::None; - } + return AllowedVirtualEvents; } virtual const std::string_view getIdentifier() override @@ -82,10 +103,11 @@ class CFPSCamera final : public ICamera } private: + typename base_t::CGimbal m_gimbal; - static inline constexpr auto AllowedLocalVirtualEvents = CVirtualGimbalEvent::Translate | CVirtualGimbalEvent::TiltUp | CVirtualGimbalEvent::TiltDown | CVirtualGimbalEvent::PanRight | CVirtualGimbalEvent::PanLeft; - static inline constexpr auto AllowedWorldVirtualEvents = AllowedLocalVirtualEvents; + static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate | CVirtualGimbalEvent::Rotate; + static inline constexpr float MaxVerticalAngle = glm::radians(88.0f), MinVerticalAngle = -MaxVerticalAngle; static inline const auto m_keyboard_to_virtual_events_preset = []() { diff --git a/common/include/camera/CFreeLockCamera.hpp b/common/include/camera/CFreeLockCamera.hpp index 0788bb048..0ab3351b7 100644 --- a/common/include/camera/CFreeLockCamera.hpp +++ b/common/include/camera/CFreeLockCamera.hpp @@ -30,31 +30,22 @@ class CFreeCamera final : public ICamera return m_gimbal; } - virtual bool manipulate(std::span virtualEvents, base_t::ManipulationMode mode) override + virtual bool manipulate(std::span virtualEvents, const float64_t4x4 const* referenceFrame = nullptr) override { - if (virtualEvents.empty()) + if (!virtualEvents.size()) return false; - const auto gOrientation = m_gimbal.getOrientation(); - - // TODO: I make assumption what world base is now (at least temporary), I need to think of this but maybe each ITransformObject should know what its world is - // in ideal scenario we would define this crazy enum with all possible standard bases - const auto impulse = mode == base_t::Local ? m_gimbal.accumulate(virtualEvents) - : m_gimbal.accumulate(virtualEvents, { 1, 0, 0 }, { 0, 1, 0 }, { 0, 0, 1 }); - - const auto pitchQuat = glm::angleAxis(impulse.dVirtualRotation.x * m_rotationSpeedScale, glm::vec3(1.0f, 0.0f, 0.0f)); - const auto yawQuat = glm::angleAxis(impulse.dVirtualRotation.y * m_rotationSpeedScale, glm::vec3(0.0f, 1.0f, 0.0f)); - const auto rollQuat = glm::angleAxis(impulse.dVirtualRotation.z * m_rotationSpeedScale, glm::vec3(0.0f, 0.0f, 1.0f)); + CReferenceTransform reference; + if (not m_gimbal.extractReferenceTransform(&reference, referenceFrame)) + return false; - const auto newOrientation = glm::normalize(gOrientation * yawQuat * pitchQuat * rollQuat); - const float64_t3 newPosition = m_gimbal.getPosition() + impulse.dVirtualTranslate * m_moveSpeedScale; + auto impulse = m_gimbal.accumulate(virtualEvents); bool manipulated = true; m_gimbal.begin(); { - m_gimbal.setOrientation(newOrientation); - m_gimbal.setPosition(newPosition); + m_gimbal.transform(reference, impulse); } m_gimbal.end(); @@ -66,14 +57,9 @@ class CFreeCamera final : public ICamera return manipulated; } - virtual const uint32_t getAllowedVirtualEvents(base_t::ManipulationMode mode) override + virtual const uint32_t getAllowedVirtualEvents() override { - switch (mode) - { - case base_t::Local: return AllowedLocalVirtualEvents; - case base_t::World: return AllowedWorldVirtualEvents; - default: return CVirtualGimbalEvent::None; - } + return AllowedVirtualEvents; } virtual const std::string_view getIdentifier() override @@ -84,8 +70,7 @@ class CFreeCamera final : public ICamera private: typename base_t::CGimbal m_gimbal; - static inline constexpr auto AllowedLocalVirtualEvents = CVirtualGimbalEvent::Translate | CVirtualGimbalEvent::Rotate; - static inline constexpr auto AllowedWorldVirtualEvents = AllowedLocalVirtualEvents; + static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate | CVirtualGimbalEvent::Rotate; static inline const auto m_keyboard_to_virtual_events_preset = []() { diff --git a/common/include/camera/COrbitCamera.hpp b/common/include/camera/COrbitCamera.hpp index 936b1e51f..46ef938d1 100644 --- a/common/include/camera/COrbitCamera.hpp +++ b/common/include/camera/COrbitCamera.hpp @@ -37,7 +37,7 @@ class COrbitCamera final : public ICamera m_targetPosition = p; } - virtual bool manipulate(std::span virtualEvents, base_t::ManipulationMode mode) override + virtual bool manipulate(std::span virtualEvents, const float64_t4x4 const* referenceFrame = nullptr) override { // position on the sphere auto S = [&](double u, double v) -> float64_t3 @@ -139,14 +139,9 @@ class COrbitCamera final : public ICamera return manipulated; } - virtual const uint32_t getAllowedVirtualEvents(base_t::ManipulationMode mode) override + virtual const uint32_t getAllowedVirtualEvents() override { - switch (mode) - { - case base_t::Local: return AllowedLocalVirtualEvents; - case base_t::World: return AllowedWorldVirtualEvents; - default: return CVirtualGimbalEvent::None; - } + return AllowedVirtualEvents; } virtual const std::string_view getIdentifier() override @@ -168,8 +163,7 @@ class COrbitCamera final : public ICamera double u = {}, v = {}; - static inline constexpr auto AllowedLocalVirtualEvents = CVirtualGimbalEvent::Translate; - static inline constexpr auto AllowedWorldVirtualEvents = CVirtualGimbalEvent::None; + static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate; static inline const auto m_keyboard_to_virtual_events_preset = []() { diff --git a/common/include/camera/ICamera.hpp b/common/include/camera/ICamera.hpp index 9af3e27d9..aceec1bc2 100644 --- a/common/include/camera/ICamera.hpp +++ b/common/include/camera/ICamera.hpp @@ -15,17 +15,6 @@ class ICamera : public IGimbalController, virtual public core::IReferenceCounted public: using IGimbalController::IGimbalController; - //! Manipulation mode for virtual events - //! TODO: this should belong to IObjectTransform or something - enum ManipulationMode - { - // Interpret virtual events as accumulated impulse representing relative manipulation with respect to view gimbal base - Local, - - // Interpret virtual events as accumulated impulse representing relative manipulation with respect to world base - World - }; - // Gimbal with view parameters representing a camera in world space class CGimbal : public IGimbal { @@ -39,22 +28,6 @@ class ICamera : public IGimbalController, virtual public core::IReferenceCounted { const auto& gRight = base_t::getXAxis(), gUp = base_t::getYAxis(), gForward = base_t::getZAxis(); - auto isNormalized = [](const auto& v, precision_t epsilon) -> bool - { - return glm::epsilonEqual(glm::length(v), 1.0, epsilon); - }; - - auto isOrthogonal = [](const auto& a, const auto& b, precision_t epsilon) -> bool - { - return glm::epsilonEqual(glm::dot(a, b), 0.0, epsilon); - }; - - auto isOrthoBase = [&](const auto& x, const auto& y, const auto& z, precision_t epsilon = 1e-6) -> bool - { - return isNormalized(x, epsilon) && isNormalized(y, epsilon) && isNormalized(z, epsilon) && - isOrthogonal(x, y, epsilon) && isOrthogonal(x, z, epsilon) && isOrthogonal(y, z, epsilon); - }; - assert(isOrthoBase(gRight, gUp, gForward)); const auto& position = base_t::getPosition(); @@ -78,10 +51,10 @@ class ICamera : public IGimbalController, virtual public core::IReferenceCounted // Manipulates camera with virtual events, returns true if *any* manipulation happens, it may fail partially or fully because each camera type has certain constraints which determine how it actually works // TODO: this really needs to be moved to more abstract interface, eg. IObjectTransform or something and ICamera should inherit it (its also an object!) - virtual bool manipulate(std::span virtualEvents, ManipulationMode mode) = 0; + virtual bool manipulate(std::span virtualEvents, const float64_t4x4 const* referenceFrame = nullptr) = 0; // VirtualEventType bitmask for a camera view gimbal manipulation requests filtering - virtual const uint32_t getAllowedVirtualEvents(ManipulationMode mode) = 0u; + virtual const uint32_t getAllowedVirtualEvents() = 0u; // Identifier of a camera type virtual const std::string_view getIdentifier() = 0u; @@ -102,6 +75,7 @@ class ICamera : public IGimbalController, virtual public core::IReferenceCounted inline double getRotationSpeedScale() const { return m_rotationSpeedScale; } protected: + // (***) TODO: I need to think whether a camera should own this or controllers should be able // to set sensitivity to scale magnitudes of generated events we put into manipulate method double m_moveSpeedScale = 0.01, m_rotationSpeedScale = 0.003; diff --git a/common/include/camera/IGimbal.hpp b/common/include/camera/IGimbal.hpp index c70c2bbab..bbf2ff478 100644 --- a/common/include/camera/IGimbal.hpp +++ b/common/include/camera/IGimbal.hpp @@ -117,6 +117,12 @@ namespace nbl::hlsl }(); }; + struct CReferenceTransform + { + float64_t4x4 frame; + glm::quat orientation; + }; + template requires is_any_of_v class IGimbal @@ -128,43 +134,43 @@ namespace nbl::hlsl struct VirtualImpulse { - vector dVirtualTranslate { 0.0f }, dVirtualRotation { 0.0f }, dVirtualScale { 0.0f }; + vector dVirtualTranslate { 0.0f }, dVirtualRotation { 0.0f }, dVirtualScale { 1.0f }; }; - // TODO: document it + //! Accumulates virtual impulse given allowed virtual event bitmap. Input virtual events are already deltas with respect to some base frame, the utility filters the events & outputs the impulse template VirtualImpulse accumulate(std::span virtualEvents, const vector& gRightOverride, const vector& gUpOverride, const vector& gForwardOverride) { VirtualImpulse impulse; - const auto& gRight = gRightOverride, gUp = gUpOverride, gForward = gForwardOverride; - for (const auto& event : virtualEvents) { - // translation events - if constexpr (AllowedEvents & CVirtualGimbalEvent::MoveForward) - if (event.type == CVirtualGimbalEvent::MoveForward) - impulse.dVirtualTranslate += gForward * static_cast(event.magnitude); - - if constexpr (AllowedEvents & CVirtualGimbalEvent::MoveBackward) - if (event.type == CVirtualGimbalEvent::MoveBackward) - impulse.dVirtualTranslate -= gForward * static_cast(event.magnitude); + assert(event.magnitude >= 0); + // translation events if constexpr (AllowedEvents & CVirtualGimbalEvent::MoveRight) if (event.type == CVirtualGimbalEvent::MoveRight) - impulse.dVirtualTranslate += gRight * static_cast(event.magnitude); + impulse.dVirtualTranslate.x += static_cast(event.magnitude); if constexpr (AllowedEvents & CVirtualGimbalEvent::MoveLeft) if (event.type == CVirtualGimbalEvent::MoveLeft) - impulse.dVirtualTranslate -= gRight * static_cast(event.magnitude); + impulse.dVirtualTranslate.x -= static_cast(event.magnitude); if constexpr (AllowedEvents & CVirtualGimbalEvent::MoveUp) if (event.type == CVirtualGimbalEvent::MoveUp) - impulse.dVirtualTranslate += gUp * static_cast(event.magnitude); + impulse.dVirtualTranslate.y += static_cast(event.magnitude); if constexpr (AllowedEvents & CVirtualGimbalEvent::MoveDown) if (event.type == CVirtualGimbalEvent::MoveDown) - impulse.dVirtualTranslate -= gUp * static_cast(event.magnitude); + impulse.dVirtualTranslate.y -= static_cast(event.magnitude); + + if constexpr (AllowedEvents & CVirtualGimbalEvent::MoveForward) + if (event.type == CVirtualGimbalEvent::MoveForward) + impulse.dVirtualTranslate.z += static_cast(event.magnitude); + + if constexpr (AllowedEvents & CVirtualGimbalEvent::MoveBackward) + if (event.type == CVirtualGimbalEvent::MoveBackward) + impulse.dVirtualTranslate.z -= static_cast(event.magnitude); // rotation events if constexpr (AllowedEvents & CVirtualGimbalEvent::TiltUp) @@ -194,27 +200,27 @@ namespace nbl::hlsl // scaling events if constexpr (AllowedEvents & CVirtualGimbalEvent::ScaleXInc) if (event.type == CVirtualGimbalEvent::ScaleXInc) - impulse.dVirtualScale.x += static_cast(event.magnitude); + impulse.dVirtualScale.x *= static_cast(event.magnitude); if constexpr (AllowedEvents & CVirtualGimbalEvent::ScaleXDec) if (event.type == CVirtualGimbalEvent::ScaleXDec) - impulse.dVirtualScale.x -= static_cast(event.magnitude); + impulse.dVirtualScale.x *= static_cast(event.magnitude); if constexpr (AllowedEvents & CVirtualGimbalEvent::ScaleYInc) if (event.type == CVirtualGimbalEvent::ScaleYInc) - impulse.dVirtualScale.y += static_cast(event.magnitude); + impulse.dVirtualScale.y *= static_cast(event.magnitude); if constexpr (AllowedEvents & CVirtualGimbalEvent::ScaleYDec) if (event.type == CVirtualGimbalEvent::ScaleYDec) - impulse.dVirtualScale.y -= static_cast(event.magnitude); + impulse.dVirtualScale.y *= static_cast(event.magnitude); if constexpr (AllowedEvents & CVirtualGimbalEvent::ScaleZInc) if (event.type == CVirtualGimbalEvent::ScaleZInc) - impulse.dVirtualScale.z += static_cast(event.magnitude); + impulse.dVirtualScale.z *= static_cast(event.magnitude); if constexpr (AllowedEvents & CVirtualGimbalEvent::ScaleZDec) if (event.type == CVirtualGimbalEvent::ScaleZDec) - impulse.dVirtualScale.z -= static_cast(event.magnitude); + impulse.dVirtualScale.z *= static_cast(event.magnitude); } return impulse; @@ -263,7 +269,6 @@ namespace nbl::hlsl inline void setScale(const vector& scale) { - // we are not consider it a manipulation because it only impacts TRS world matrix we do not store m_scale = scale; } @@ -278,6 +283,12 @@ namespace nbl::hlsl updateOrthonormalOrientationBase(); } + inline void transform(const CReferenceTransform& reference, const VirtualImpulse& impulse) + { + setOrientation(reference.orientation * glm::quat(glm::radians(impulse.dVirtualRotation))); + setPosition(mul(float64_t4(impulse.dVirtualTranslate, 1), reference.frame).xyz); + } + inline void rotate(const vector& axis, float dRadians) { assert(m_isManipulating); // TODO: log error and return without doing nothing @@ -370,6 +381,28 @@ namespace nbl::hlsl //! Returns true if gimbal records a manipulation inline bool isManipulating() const { return m_isManipulating; } + bool extractReferenceTransform(CReferenceTransform* out, const float64_t4x4 const* referenceFrame = nullptr) + { + if (not out) + return false; + + if (referenceFrame) + { + out->frame = *referenceFrame; + if (not isOrthoBase(float64_t3(out->frame[0]), float64_t3(out->frame[1]), float64_t3(out->frame[2]))) + return false; + } + else + { + out->frame = getMatrix3x3As4x4(getOrthonornalMatrix()); + out->frame[3] = float64_t4(getPosition(), 1); + } + + out->orientation = glm::quat_cast(glm::dmat3{ out->frame[0], out->frame[1], out->frame[2] }); + + return true; + } + private: inline void updateOrthonormalOrientationBase() { From 491aa0bb26c05ad121e894ae8fb6e6b2284190b8 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Sun, 2 Feb 2025 20:50:08 +0100 Subject: [PATCH 092/205] finally managed to get reference frame virtual events from imguizmo, FPS Camera & Free lock tested - leaving some debug windows, save work --- 61_UI/main.cpp | 115 ++++++++++++++-------- common/include/camera/CFPSCamera.hpp | 27 ++--- common/include/camera/CFreeLockCamera.hpp | 76 +++++++++++++- common/include/camera/IGimbal.hpp | 27 +++-- 4 files changed, 178 insertions(+), 67 deletions(-) diff --git a/61_UI/main.cpp b/61_UI/main.cpp index 0c051fb60..ff23ac3df 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -1224,10 +1224,13 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGuizmo::BeginFrame(); { + nbl::hlsl::ShowDebugWindow(); + + ImGuizmo::ShowDebugImguizmoWindow(); + SImResourceInfo info; info.samplerIx = (uint16_t)nbl::ext::imgui::UI::DefaultSamplerIx::USER; - // ORBIT CAMERA TEST { for (auto& planar : m_planarProjections) @@ -1365,7 +1368,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication if (isCameraGimbalTarget) { assert(targetGimbalManipulationCamera); - imguizmoModel.inTRS = gimbalToImguizmoTRS(getCastedMatrix(targetGimbalManipulationCamera->getGimbal()())); + imguizmoModel.inTRS = getCastedMatrix(targetGimbalManipulationCamera->getGimbal().template operator() < float64_t4x4 > ()); } else imguizmoModel.inTRS = hlsl::transpose(getMatrix3x4As4x4(m_model)); @@ -1378,10 +1381,13 @@ class UISampleApp final : public examples::SimpleWindowedApplication { if (targetGimbalManipulationCamera) { + const auto referenceFrame = getCastedMatrix(*reinterpret_cast(ImGuizmo::GetReferenceFrame())); + boundCameraToManipulate = smart_refctd_ptr(targetGimbalManipulationCamera); boundPlanarCameraIxToManipulate = modelIx - 1u; - auto& gimbal = (ICamera::CGimbal&)targetGimbalManipulationCamera->getGimbal(); + // TODO: TO BE REMOVED, ONLY FOR TESTING ITS INCOMPLETE TYPE! + const auto& imguizmoCtx = ImGuizmo::GetContext(); struct { @@ -1404,28 +1410,72 @@ class UISampleApp final : public examples::SimpleWindowedApplication ev.magnitude += dMagnitude; } }; + + // TODO TESTING STUFF WITH MY IMGUIZMO UPDATES + // IT WILL BE REMOVED ONCE ALL TESTS ARE DONE + // AND CONTROLLER API WILL BE USED INSTEAD - // Delta translation impulse - requestMagnitudeUpdateWithScalar(0.f, delta.t[0], std::abs(delta.t[0]), CVirtualGimbalEvent::VirtualEventType::MoveRight, CVirtualGimbalEvent::VirtualEventType::MoveLeft); - requestMagnitudeUpdateWithScalar(0.f, delta.t[1], std::abs(delta.t[1]), CVirtualGimbalEvent::VirtualEventType::MoveUp, CVirtualGimbalEvent::VirtualEventType::MoveDown); - requestMagnitudeUpdateWithScalar(0.f, delta.t[2], std::abs(delta.t[2]), CVirtualGimbalEvent::VirtualEventType::MoveForward, CVirtualGimbalEvent::VirtualEventType::MoveBackward); + // translations + { + ImGuizmo::OPERATION ioType; + const auto dScalar = ImGuizmo::GetTranslationDeltaScalar(&ioType); - // Delta rotation impulse - requestMagnitudeUpdateWithScalar(0.f, delta.r[0], std::abs(delta.r[0]), CVirtualGimbalEvent::VirtualEventType::TiltUp, CVirtualGimbalEvent::VirtualEventType::TiltDown); - requestMagnitudeUpdateWithScalar(0.f, delta.r[1], std::abs(delta.r[1]), CVirtualGimbalEvent::VirtualEventType::PanRight, CVirtualGimbalEvent::VirtualEventType::PanLeft); - requestMagnitudeUpdateWithScalar(0.f, delta.r[2], std::abs(delta.r[2]), CVirtualGimbalEvent::VirtualEventType::RollRight, CVirtualGimbalEvent::VirtualEventType::RollLeft); + if (dScalar) + { + switch (ioType) + { + case ImGuizmo::OPERATION::TRANSLATE_X: + { + requestMagnitudeUpdateWithScalar(0.f, dScalar, std::abs(dScalar), CVirtualGimbalEvent::VirtualEventType::MoveRight, CVirtualGimbalEvent::VirtualEventType::MoveLeft); + } break; + + case ImGuizmo::OPERATION::TRANSLATE_Y: + { + requestMagnitudeUpdateWithScalar(0.f, dScalar, std::abs(dScalar), CVirtualGimbalEvent::VirtualEventType::MoveUp, CVirtualGimbalEvent::VirtualEventType::MoveDown); + } break; + + case ImGuizmo::OPERATION::TRANSLATE_Z: + { + requestMagnitudeUpdateWithScalar(0.f, dScalar, std::abs(dScalar), CVirtualGimbalEvent::VirtualEventType::MoveForward, CVirtualGimbalEvent::VirtualEventType::MoveBackward); + } break; + + default: break; + } + } + } - // Delta scale impulse - requestMagnitudeUpdateWithScalar(1.f, delta.s[0], std::abs(delta.s[0]), CVirtualGimbalEvent::VirtualEventType::ScaleXInc, CVirtualGimbalEvent::VirtualEventType::ScaleXDec); - requestMagnitudeUpdateWithScalar(1.f, delta.s[1], std::abs(delta.s[1]), CVirtualGimbalEvent::VirtualEventType::ScaleYInc, CVirtualGimbalEvent::VirtualEventType::ScaleYDec); - requestMagnitudeUpdateWithScalar(1.f, delta.s[2], std::abs(delta.s[2]), CVirtualGimbalEvent::VirtualEventType::ScaleZInc, CVirtualGimbalEvent::VirtualEventType::ScaleZDec); - - constexpr auto TESSTTSS = CVirtualGimbalEvent::All; - const auto impulse = gimbal.accumulate(virtualEvents); - - assert(impulse.dVirtualTranslate == float64_t3(delta.t)); - assert(impulse.dVirtualRotation == float64_t3(delta.r)); - assert(impulse.dVirtualScale == float64_t3(delta.s)); + // TODO: ok becuase I have only one reference from imguizmo I must do it differently when + // I have local base && want to do rotation with respect to world instead; we almost there + + // rotations + { + ImGuizmo::OPERATION ioType; + float dRadians = ImGuizmo::GetRotationDeltaRadians(&ioType); + + if (dRadians) + { + switch (ioType) + { + case ImGuizmo::OPERATION::ROTATE_X: + { + requestMagnitudeUpdateWithScalar(0.f, dRadians, std::abs(dRadians), CVirtualGimbalEvent::VirtualEventType::TiltUp, CVirtualGimbalEvent::VirtualEventType::TiltDown); + } break; + + case ImGuizmo::OPERATION::ROTATE_Y: + { + requestMagnitudeUpdateWithScalar(0.f, dRadians, std::abs(dRadians), CVirtualGimbalEvent::VirtualEventType::PanRight, CVirtualGimbalEvent::VirtualEventType::PanLeft); + } break; + + case ImGuizmo::OPERATION::ROTATE_Z: + { + requestMagnitudeUpdateWithScalar(0.f, dRadians, std::abs(dRadians), CVirtualGimbalEvent::VirtualEventType::RollRight, CVirtualGimbalEvent::VirtualEventType::RollLeft); + } break; + + default: + assert(false); break; // should never be hit + } + } + } const auto vCount = virtualEvents.size(); @@ -1440,12 +1490,6 @@ class UISampleApp final : public examples::SimpleWindowedApplication targetGimbalManipulationCamera->setMoveSpeedScale(1); targetGimbalManipulationCamera->setRotationSpeedScale(1); - /* - new = old * delta - delta^(-1) * new = old - */ - - const auto referenceFrame = getCastedMatrix(mul(inverse(imguizmoModel.outDeltaTRS), imguizmoModel.outTRS)); targetGimbalManipulationCamera->manipulate({ virtualEvents.data(), vCount }, &referenceFrame); targetGimbalManipulationCamera->setMoveSpeedScale(pMoveSpeed); @@ -1923,7 +1967,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGuizmoModelM16InOut imguizmoModel; if (boundCameraToManipulate) - imguizmoModel.inTRS = gimbalToImguizmoTRS(getCastedMatrix(boundCameraToManipulate->getGimbal()())); + imguizmoModel.inTRS = getCastedMatrix(boundCameraToManipulate->getGimbal().template operator() < float64_t4x4 > ()); else imguizmoModel.inTRS = hlsl::transpose(getMatrix3x4As4x4(m_model)); @@ -2139,19 +2183,6 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGui::Separator(); } - // TODO: need to inspect where I'm wrong, workaround - inline float32_t4x4 gimbalToImguizmoTRS(const float32_t3x4& nblGimbalTrs) - { - // *do not transpose whole matrix*, only the translate part - float32_t4x4 trs = getMatrix3x4As4x4(nblGimbalTrs); - trs[3] = float32_t4(nblGimbalTrs[0][3], nblGimbalTrs[1][3], nblGimbalTrs[2][3], 1.f); - trs[0][3] = 0.f; - trs[1][3] = 0.f; - trs[2][3] = 0.f; - - return trs; - }; - std::chrono::seconds timeout = std::chrono::seconds(0x7fffFFFFu); clock_t::time_point start; diff --git a/common/include/camera/CFPSCamera.hpp b/common/include/camera/CFPSCamera.hpp index a99d30f5c..02b49c32f 100644 --- a/common/include/camera/CFPSCamera.hpp +++ b/common/include/camera/CFPSCamera.hpp @@ -46,14 +46,13 @@ class CFPSCamera final : public ICamera return m_gimbal; } + // rotation events IN RADIANS + virtual bool manipulate(std::span virtualEvents, const float64_t4x4 const* referenceFrame = nullptr) override { if (!virtualEvents.size()) return false; - //! true if virtual events are with respect to the moving gimbal frame, otherwise it is assumed deltas are generated for fixed reference frame - const bool isMovingReference = not referenceFrame; - CReferenceTransform reference; if (not m_gimbal.extractReferenceTransform(&reference, referenceFrame)) return false; @@ -64,23 +63,17 @@ class CFPSCamera final : public ICamera m_gimbal.begin(); { - if (isMovingReference) + if (impulse.dVirtualRotation.x or impulse.dVirtualRotation.y or impulse.dVirtualRotation.z) { - const auto& gForward = m_gimbal.getZAxis(); - const float gPitch = atan2(glm::length(glm::vec2(gForward.x, gForward.z)), gForward.y) - glm::half_pi(), gYaw = atan2(gForward.x, gForward.z); - const float newPitch = std::clamp(gPitch + impulse.dVirtualRotation.x * m_rotationSpeedScale, MinVerticalAngle, MaxVerticalAngle), newYaw = gYaw + impulse.dVirtualRotation.y * m_rotationSpeedScale; - + const auto rForward = glm::vec3(reference.frame[2]); + const float rPitch = atan2(glm::length(glm::vec2(rForward.x, rForward.z)), rForward.y) - glm::half_pi(), gYaw = atan2(rForward.x, rForward.z); + const float newPitch = std::clamp(rPitch + impulse.dVirtualRotation.x * m_rotationSpeedScale, MinVerticalAngle, MaxVerticalAngle), newYaw = gYaw + impulse.dVirtualRotation.y * m_rotationSpeedScale; + m_gimbal.setOrientation(glm::quat(glm::vec3(newPitch, newYaw, 0.0f))); - m_gimbal.setPosition(m_gimbal.getPosition() + mul(impulse.dVirtualTranslate * m_moveSpeedScale, m_gimbal.getOrthonornalMatrix())); - } - else - { - // TODO: I need to think of this, the problem is that since reference frame may not be aligned with world nicely it means events must be transformed - // what FPS camera really does is: - // tilt around FIXED world (0,1,0) vector - // pitch around REFERENCE right vector - m_gimbal.transform(reference, impulse); } + + if (impulse.dVirtualTranslate.x or impulse.dVirtualTranslate.y or impulse.dVirtualTranslate.z) + m_gimbal.setPosition(glm::vec3(reference.frame[3]) + reference.orientation * glm::vec3(impulse.dVirtualTranslate)); } m_gimbal.end(); diff --git a/common/include/camera/CFreeLockCamera.hpp b/common/include/camera/CFreeLockCamera.hpp index 0ab3351b7..494d156d8 100644 --- a/common/include/camera/CFreeLockCamera.hpp +++ b/common/include/camera/CFreeLockCamera.hpp @@ -1,4 +1,4 @@ -// Copyright (C) 2018-2024 - DevSH Graphics Programming Sp. z O.O. +// Copyright (C) 2018-2024 - 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 @@ -8,8 +8,62 @@ #include "ICamera.hpp" #include +#include "nbl/ext/ImGui/ImGui.h" +#include "imgui/imgui_internal.h" + namespace nbl::hlsl // TODO: DIFFERENT NAMESPACE { + static inline IGimbal::VirtualImpulse sVirtualImpulse = {}; + static inline glm::mat4 sReferenceFrame = glm::mat4(1.0f); + static inline glm::quat sReferenceOrientation = {}; + + // TODO: DEBUG AND TEMPORARY + void ShowDebugWindow() + { + ImGui::Begin("Debug Window"); + + ImGui::Text("Translate deltas:"); + ImGui::Text(" x: %.3f", sVirtualImpulse.dVirtualTranslate.x); + ImGui::Text(" y: %.3f", sVirtualImpulse.dVirtualTranslate.y); + ImGui::Text(" z: %.3f", sVirtualImpulse.dVirtualTranslate.z); + + ImGui::Separator(); + + ImGui::Text("Rotation deltas:"); + ImGui::Text(" x: %.3f", sVirtualImpulse.dVirtualRotation.x); + ImGui::Text(" y: %.3f", sVirtualImpulse.dVirtualRotation.y); + ImGui::Text(" z: %.3f", sVirtualImpulse.dVirtualRotation.z); + + ImGui::Separator(); + + ImGui::Text("Scale deltas:"); + ImGui::Text(" x: %.3f", sVirtualImpulse.dVirtualScale.x); + ImGui::Text(" y: %.3f", sVirtualImpulse.dVirtualScale.y); + ImGui::Text(" z: %.3f", sVirtualImpulse.dVirtualScale.z); + + ImGui::Separator(); + + ImGui::Text("Reference frame:"); + + for (int row = 0; row < 4; ++row) + { + ImGui::Text("%.3f %.3f %.3f %.3f", + sReferenceFrame[0][row], + sReferenceFrame[1][row], + sReferenceFrame[2][row], + sReferenceFrame[3][row]); + } + + ImGui::Text("Reference orientation:"); + + ImGui::Text("%.3f %.3f %.3f %.3f", + sReferenceOrientation.x, + sReferenceOrientation.y, + sReferenceOrientation.z, + sReferenceOrientation.w); + + ImGui::End(); + } // Free Lock Camera class CFreeCamera final : public ICamera @@ -43,9 +97,27 @@ class CFreeCamera final : public ICamera bool manipulated = true; + // TODO: DEBUG AND TEMPORARY + { + sVirtualImpulse = impulse; + auto cast = getCastedMatrix(reference.frame);; + memcpy(&sReferenceFrame, &cast, sizeof(sReferenceFrame)); + sReferenceOrientation = reference.orientation; + } + m_gimbal.begin(); { - m_gimbal.transform(reference, impulse); + if (impulse.dVirtualRotation.x or impulse.dVirtualRotation.y or impulse.dVirtualRotation.z) + { + glm::quat pitch = glm::angleAxis(impulse.dVirtualRotation.x, glm::vec3(reference.frame[0])); + glm::quat yaw = glm::angleAxis(impulse.dVirtualRotation.y, glm::vec3(reference.frame[1])); + glm::quat roll = glm::angleAxis(impulse.dVirtualRotation.z, glm::vec3(reference.frame[2])); + + m_gimbal.setOrientation(yaw * pitch * roll * reference.orientation); + } + + if(impulse.dVirtualTranslate.x or impulse.dVirtualTranslate.y or impulse.dVirtualTranslate.z) + m_gimbal.setPosition(glm::vec3(reference.frame[3]) + reference.orientation * glm::vec3(impulse.dVirtualTranslate)); } m_gimbal.end(); diff --git a/common/include/camera/IGimbal.hpp b/common/include/camera/IGimbal.hpp index bbf2ff478..384786b46 100644 --- a/common/include/camera/IGimbal.hpp +++ b/common/include/camera/IGimbal.hpp @@ -343,18 +343,33 @@ namespace nbl::hlsl inline const auto& getScale() const { return m_scale; } //! World matrix (TRS) - inline const model_matrix_t operator()() const + template + requires is_any_of_v> + const TRS operator()() const { const auto& position = getPosition(); const auto& rotation = getOrthonornalMatrix(); const auto& scale = getScale(); - return + if constexpr (is_same_v) { - vector(rotation[0] * scale.x, position.x), - vector(rotation[1] * scale.y, position.y), - vector(rotation[2] * scale.z, position.z) - }; + return + { + vector(rotation[0] * scale.x, position.x), + vector(rotation[1] * scale.y, position.y), + vector(rotation[2] * scale.z, position.z) + }; + } + else + { + return + { + vector(rotation[0] * scale.x, T(0)), + vector(rotation[1] * scale.y, T(0)), + vector(rotation[2] * scale.z, T(0)), + vector(position, T(1)) + }; + } } //! Orthonormal [getXAxis(), getYAxis(), getZAxis()] orientation matrix From 2b75a10d1a37e6fe14c37fb282d8ab2795a38220 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Thu, 6 Feb 2025 16:36:37 +0100 Subject: [PATCH 093/205] allow cameras orientation to be controlled with raw input pitch/yaw/roll degrees world scalars & still respect manipulate constraints; mark TODOs for orbit camera, we need another gimbal to control its target nicely --- 61_UI/main.cpp | 30 +++++++++-------- common/include/camera/CFPSCamera.hpp | 36 ++++++++++++++------ common/include/camera/CFreeLockCamera.hpp | 17 ++++------ common/include/camera/COrbitCamera.hpp | 41 ++++++++--------------- 4 files changed, 61 insertions(+), 63 deletions(-) diff --git a/61_UI/main.cpp b/61_UI/main.cpp index ff23ac3df..f4c51c31d 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -2062,25 +2062,12 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGuiInputTextFlags flags = 0; ImGui::InputFloat3("Tr", &matrixTranslation[0], "%.3f", flags); - - if (boundCameraToManipulate) // TODO: cameras are WiP here, imguizmo controller only works with translate manipulation + abs are banned currently - { - ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(1.0f, 0.0f, 0.0f, 0.5f)); - flags |= ImGuiInputTextFlags_ReadOnly; - } - ImGui::InputFloat3("Rt", &matrixRotation[0], "%.3f", flags); ImGui::InputFloat3("Sc", &matrixScale[0], "%.3f", flags); - - if(boundCameraToManipulate) - ImGui::PopStyleColor(); } ImGuizmo::RecomposeMatrixFromComponents(&matrixTranslation[0], &matrixRotation[0], &matrixScale[0], m16TRSmatrix); recomposed = *reinterpret_cast(m16TRSmatrix); - // TODO AND NOTE: I only take care of translate part temporary! - imguizmoModel.outDeltaTRS[3] = recomposed[3] - decomposed[3]; - if (mCurrentGizmoOperation != ImGuizmo::SCALE) { if (ImGui::RadioButton("Local", mCurrentGizmoMode == ImGuizmo::LOCAL)) @@ -2110,6 +2097,19 @@ class UISampleApp final : public examples::SimpleWindowedApplication // generate virtual events given delta TRS matrix if (boundCameraToManipulate) { + const float pmSpeed = boundCameraToManipulate->getMoveSpeedScale(); + const float prSpeed = boundCameraToManipulate->getRotationSpeedScale(); + + boundCameraToManipulate->setMoveSpeedScale(1); + boundCameraToManipulate->setRotationSpeedScale(1); + + auto referenceFrame = getCastedMatrix(imguizmoModel.outTRS); + boundCameraToManipulate->manipulate({}, &referenceFrame); + + boundCameraToManipulate->setMoveSpeedScale(pmSpeed); + boundCameraToManipulate->setRotationSpeedScale(prSpeed); + + /* { static std::vector virtualEvents(0x45); @@ -2141,13 +2141,15 @@ class UISampleApp final : public examples::SimpleWindowedApplication boundCameraToManipulate->setMoveSpeedScale(1); boundCameraToManipulate->setRotationSpeedScale(1); - boundCameraToManipulate->manipulate({ virtualEvents.data(), vCount }); + auto referenceFrame = getCastedMatrix(imguizmoModel.outTRS); + boundCameraToManipulate->manipulate({ virtualEvents.data(), vCount }, &referenceFrame); boundCameraToManipulate->setMoveSpeedScale(pmSpeed); boundCameraToManipulate->setRotationSpeedScale(prSpeed); } } } + */ } else { diff --git a/common/include/camera/CFPSCamera.hpp b/common/include/camera/CFPSCamera.hpp index 02b49c32f..5e8d049dd 100644 --- a/common/include/camera/CFPSCamera.hpp +++ b/common/include/camera/CFPSCamera.hpp @@ -50,30 +50,44 @@ class CFPSCamera final : public ICamera virtual bool manipulate(std::span virtualEvents, const float64_t4x4 const* referenceFrame = nullptr) override { - if (!virtualEvents.size()) + // TODO: note, for FPS camera its assumed tilt is performed with respect to "world" up vector which is (0,1,0) + // but in reality its all about where -(gravity force) vector is, we can just add it and construct yaw quat with respect to this new custom vector instead + + if (not virtualEvents.size() and not referenceFrame) return false; CReferenceTransform reference; if (not m_gimbal.extractReferenceTransform(&reference, referenceFrame)) return false; + auto validateReference = [&]() + { + if (referenceFrame) + { + // invalidate roll + auto euler = glm::eulerAngles(reference.orientation) * 180.f / core::PI(); + auto roll = std::abs(euler.z); + constexpr float epsilon = 1.e-4f; + + if (not (glm::epsilonEqual(roll, 0.f, epsilon) || glm::epsilonEqual(roll, 180.f, epsilon))) + return false; + } + + return true; + }; + auto impulse = m_gimbal.accumulate(virtualEvents); bool manipulated = true; m_gimbal.begin(); { - if (impulse.dVirtualRotation.x or impulse.dVirtualRotation.y or impulse.dVirtualRotation.z) - { - const auto rForward = glm::vec3(reference.frame[2]); - const float rPitch = atan2(glm::length(glm::vec2(rForward.x, rForward.z)), rForward.y) - glm::half_pi(), gYaw = atan2(rForward.x, rForward.z); - const float newPitch = std::clamp(rPitch + impulse.dVirtualRotation.x * m_rotationSpeedScale, MinVerticalAngle, MaxVerticalAngle), newYaw = gYaw + impulse.dVirtualRotation.y * m_rotationSpeedScale; - - m_gimbal.setOrientation(glm::quat(glm::vec3(newPitch, newYaw, 0.0f))); - } + const auto rForward = glm::vec3(reference.frame[2]); + const float rPitch = atan2(glm::length(glm::vec2(rForward.x, rForward.z)), rForward.y) - glm::half_pi(), gYaw = atan2(rForward.x, rForward.z); + const float newPitch = std::clamp(rPitch + impulse.dVirtualRotation.x * m_rotationSpeedScale, MinVerticalAngle, MaxVerticalAngle), newYaw = gYaw + impulse.dVirtualRotation.y * m_rotationSpeedScale; - if (impulse.dVirtualTranslate.x or impulse.dVirtualTranslate.y or impulse.dVirtualTranslate.z) - m_gimbal.setPosition(glm::vec3(reference.frame[3]) + reference.orientation * glm::vec3(impulse.dVirtualTranslate)); + if(validateReference()) m_gimbal.setOrientation(glm::quat(glm::vec3(newPitch, newYaw, 0.0f))); + m_gimbal.setPosition(glm::vec3(reference.frame[3]) + reference.orientation * glm::vec3(impulse.dVirtualTranslate)); } m_gimbal.end(); diff --git a/common/include/camera/CFreeLockCamera.hpp b/common/include/camera/CFreeLockCamera.hpp index 494d156d8..c684a6fb3 100644 --- a/common/include/camera/CFreeLockCamera.hpp +++ b/common/include/camera/CFreeLockCamera.hpp @@ -86,7 +86,7 @@ class CFreeCamera final : public ICamera virtual bool manipulate(std::span virtualEvents, const float64_t4x4 const* referenceFrame = nullptr) override { - if (!virtualEvents.size()) + if (not virtualEvents.size() and not referenceFrame) return false; CReferenceTransform reference; @@ -107,17 +107,12 @@ class CFreeCamera final : public ICamera m_gimbal.begin(); { - if (impulse.dVirtualRotation.x or impulse.dVirtualRotation.y or impulse.dVirtualRotation.z) - { - glm::quat pitch = glm::angleAxis(impulse.dVirtualRotation.x, glm::vec3(reference.frame[0])); - glm::quat yaw = glm::angleAxis(impulse.dVirtualRotation.y, glm::vec3(reference.frame[1])); - glm::quat roll = glm::angleAxis(impulse.dVirtualRotation.z, glm::vec3(reference.frame[2])); + glm::quat pitch = glm::angleAxis(impulse.dVirtualRotation.x, glm::vec3(reference.frame[0])); + glm::quat yaw = glm::angleAxis(impulse.dVirtualRotation.y, glm::vec3(reference.frame[1])); + glm::quat roll = glm::angleAxis(impulse.dVirtualRotation.z, glm::vec3(reference.frame[2])); - m_gimbal.setOrientation(yaw * pitch * roll * reference.orientation); - } - - if(impulse.dVirtualTranslate.x or impulse.dVirtualTranslate.y or impulse.dVirtualTranslate.z) - m_gimbal.setPosition(glm::vec3(reference.frame[3]) + reference.orientation * glm::vec3(impulse.dVirtualTranslate)); + m_gimbal.setOrientation(yaw * pitch * roll * reference.orientation); + m_gimbal.setPosition(glm::vec3(reference.frame[3]) + reference.orientation * glm::vec3(impulse.dVirtualTranslate)); } m_gimbal.end(); diff --git a/common/include/camera/COrbitCamera.hpp b/common/include/camera/COrbitCamera.hpp index 46ef938d1..694459c2d 100644 --- a/common/include/camera/COrbitCamera.hpp +++ b/common/include/camera/COrbitCamera.hpp @@ -39,6 +39,8 @@ class COrbitCamera final : public ICamera virtual bool manipulate(std::span virtualEvents, const float64_t4x4 const* referenceFrame = nullptr) override { + // TODO: it must work differently, we should take another gimbal to control target + // position on the sphere auto S = [&](double u, double v) -> float64_t3 { @@ -63,23 +65,19 @@ class COrbitCamera final : public ICamera }; */ - double deltaU = {}, deltaV = {}, deltaDistance = {}; - - for (const auto& it : virtualEvents) + // partial derivative of S with respect to v + auto Sdv = [&](double u, double v) -> float64_t3 { - if (it.type == it.MoveForward) - deltaDistance += it.magnitude; - else if (it.type == it.MoveBackward) - deltaDistance -= it.magnitude; - else if (it.type == it.MoveUp) - deltaU += it.magnitude; - else if (it.type == it.MoveDown) - deltaU -= it.magnitude; - else if (it.type == it.MoveRight) - deltaV += it.magnitude; - else if (it.type == it.MoveLeft) - deltaV -= it.magnitude; - } + return float64_t3 + { + -std::sin(v) * std::cos(u), + -std::sin(v) * std::sin(u), + std::cos(v) + } *(double)m_distance; + }; + + auto impulse = m_gimbal.accumulate(virtualEvents); + double deltaU = impulse.dVirtualTranslate.y, deltaV = impulse.dVirtualTranslate.x, deltaDistance = impulse.dVirtualTranslate.z; // TODO! constexpr auto nastyScalar = 0.01; @@ -91,17 +89,6 @@ class COrbitCamera final : public ICamera m_distance = std::clamp(m_distance += deltaDistance * nastyScalar, MinDistance, MaxDistance); - // partial derivative of S with respect to v - auto Sdv = [&](double u, double v) -> float64_t3 - { - return float64_t3 - { - -std::sin(v) * std::cos(u), - -std::sin(v) * std::sin(u), - std::cos(v) - } * (double) m_distance; - }; - const auto localSpherePostion = S(u, v); const auto newPosition = localSpherePostion + m_targetPosition; From c936ded9451a2e1ce741e9fa23656f50d9449c9e Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Tue, 3 Feb 2026 11:29:22 +0100 Subject: [PATCH 094/205] adjust to code, precompile remaining runtime shaders --- 61_UI/CMakeLists.txt | 44 +++++++++++++++++++++++++ 61_UI/app_resources/imgui_fragment.hlsl | 7 ++++ 61_UI/app_resources/imgui_vertex.hlsl | 1 + 61_UI/main.cpp | 25 ++++++++++++++ 4 files changed, 77 insertions(+) create mode 100644 61_UI/app_resources/imgui_fragment.hlsl create mode 100644 61_UI/app_resources/imgui_vertex.hlsl diff --git a/61_UI/CMakeLists.txt b/61_UI/CMakeLists.txt index 751e3c417..bbcdacf08 100644 --- a/61_UI/CMakeLists.txt +++ b/61_UI/CMakeLists.txt @@ -33,6 +33,50 @@ if(NBL_BUILD_IMGUI) LINK_BUILTIN_RESOURCES_TO_TARGET(${EXECUTABLE_NAME} ${_BR_TARGET_}) endif() + set(OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/auto-gen") + + set(JSON [=[ +[ + { + "INPUT": "app_resources/imgui_vertex.hlsl", + "KEY": "imgui_vertex", + "COMPILE_OPTIONS": ["-T", "vs_6_7", "-E", "VSMain", "-O3"] + }, + { + "INPUT": "app_resources/imgui_fragment.hlsl", + "KEY": "imgui_fragment", + "COMPILE_OPTIONS": ["-T", "ps_6_7", "-E", "PSMain", "-O3"] + } +] +]=]) + string(CONFIGURE "${JSON}" JSON) + + set(COMPILE_OPTIONS + -I "${CMAKE_CURRENT_SOURCE_DIR}" + -I "${NBL_ROOT_PATH}/include" + -I "${NBL_ROOT_PATH}/include/nbl/ext/ImGui/builtin/hlsl" + ) + + 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} + ) + add_dependencies(${EXECUTABLE_NAME} argparse) target_include_directories(${EXECUTABLE_NAME} PUBLIC $) endif() 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/main.cpp b/61_UI/main.cpp index 907f1ce4a..89a7c698d 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -11,6 +11,7 @@ using json = nlohmann::json; #include "camera/CCubeProjection.hpp" #include "glm/glm/ext/matrix_clip_space.hpp" // TODO: TESTING #include "nbl/ext/ScreenShot/ScreenShot.h" +#include "nbl/this_example/builtin/build/spirv/keys.hpp" #if __has_include("nbl/this_example/builtin/CArchive.h") #include "nbl/this_example/builtin/CArchive.h" #endif @@ -883,6 +884,30 @@ class UISampleApp final : public examples::SimpleWindowedApplication params.transfer = getTransferUpQueue(); params.utilities = m_utils; + auto loadPrecompiledShader = [&](const std::string_view key) -> smart_refctd_ptr + { + IAssetLoader::SAssetLoadParams loadParams = {}; + loadParams.logger = m_logger.get(); + loadParams.workingDirectory = "app_resources"; + auto bundle = m_assetManager->getAsset(key.data(), loadParams); + const auto& contents = bundle.getContents(); + if (contents.empty()) + return nullptr; + return IAsset::castDown(contents[0]); + }; + + const auto vertexKey = nbl::this_example::builtin::build::get_spirv_key<"imgui_vertex">(m_device.get()); + const auto fragmentKey = nbl::this_example::builtin::build::get_spirv_key<"imgui_fragment">(m_device.get()); + auto vertexShader = loadPrecompiledShader(vertexKey.data()); + auto fragmentShader = loadPrecompiledShader(fragmentKey.data()); + if (!vertexShader || !fragmentShader) + return logFail("Failed to load precompiled ImGui shaders."); + + params.spirv = nbl::ext::imgui::UI::SCreationParameters::PrecompiledShaders{ + .vertex = std::move(vertexShader), + .fragment = std::move(fragmentShader) + }; + m_ui.manager = nbl::ext::imgui::UI::create(std::move(params)); } From 26875f5aefc3e7e09b0cb16e7f43721411e28a26 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Tue, 3 Feb 2026 17:55:30 +0100 Subject: [PATCH 095/205] unit tests for virtual events --- 61_UI/main.cpp | 926 +++++++++++++++++++- common/include/camera/IGimbal.hpp | 4 +- common/include/camera/IGimbalController.hpp | 38 +- 3 files changed, 946 insertions(+), 22 deletions(-) diff --git a/61_UI/main.cpp b/61_UI/main.cpp index 89a7c698d..bba9c96bd 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -2,6 +2,10 @@ // 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 "nlohmann/json.hpp" #include "argparse/argparse.hpp" using json = nlohmann::json; @@ -331,6 +335,12 @@ class UISampleApp final : public examples::SimpleWindowedApplication .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); try { @@ -345,6 +355,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication m_ciMode = program.get("--ci"); if (m_ciMode) m_ciScreenshotPath = localOutputCWD / "cameraz_ci.png"; + m_scriptedInput.log = program.get("--script-log"); // Create imput system m_inputSystem = make_smart_refctd_ptr(logger_opt_smart_ptr(smart_refctd_ptr(m_logger))); @@ -429,6 +440,395 @@ class UISampleApp final : public examples::SimpleWindowedApplication file >> j; } + auto loadScriptJson = [&](const std::string& path, json& out) -> bool + { + std::ifstream sfile(path); + if (!sfile.is_open()) + { + m_logger->log("Cannot open scripted input file \"%s\".", ILogger::ELL_ERROR, path.c_str()); + return false; + } + sfile >> out; + return true; + }; + + auto parseScriptedInput = [&](const json& script) -> void + { + m_scriptedInput.events.clear(); + m_scriptedInput.checks.clear(); + m_scriptedInput.nextEventIndex = 0; + m_scriptedInput.nextCheckIndex = 0; + m_scriptedInput.failed = false; + m_scriptedInput.summaryReported = false; + m_scriptedInput.baselineValid = false; + m_scriptedInput.exclusive = false; + m_scriptedInput.hardFail = false; + + if (script.contains("enabled")) + m_scriptedInput.enabled = script["enabled"].get(); + else + m_scriptedInput.enabled = true; + + if (script.contains("log")) + m_scriptedInput.log = script["log"].get() || m_scriptedInput.log; + + if (script.contains("hard_fail")) + m_scriptedInput.hardFail = script["hard_fail"].get(); + + if (script.contains("enableActiveCameraMovement")) + enableActiveCameraMovement = script["enableActiveCameraMovement"].get(); + else if (m_scriptedInput.enabled) + enableActiveCameraMovement = true; + + if (script.contains("exclusive_input")) + m_scriptedInput.exclusive = script["exclusive_input"].get() || m_scriptedInput.exclusive; + if (script.contains("exclusive")) + m_scriptedInput.exclusive = script["exclusive"].get() || m_scriptedInput.exclusive; + + if (script.contains("events")) + for (const auto& ev : script["events"]) + { + if (!ev.contains("frame") || !ev.contains("type")) + { + m_logger->log("Scripted input event missing \"frame\" or \"type\".", ILogger::ELL_WARNING); + continue; + } + + const auto frame = ev["frame"].get(); + const auto type = ev["type"].get(); + + if (type == "keyboard") + { + if (!ev.contains("key") || !ev.contains("action")) + { + m_logger->log("Scripted keyboard event missing \"key\" or \"action\".", ILogger::ELL_WARNING); + continue; + } + + const auto keyStr = ev["key"].get(); + const auto actionStr = ev["action"].get(); + const auto key = ui::stringToKeyCode(keyStr); + if (key == ui::EKC_NONE) + { + m_logger->log("Scripted keyboard event has invalid key \"%s\".", ILogger::ELL_WARNING, keyStr.c_str()); + continue; + } + + ui::SKeyboardEvent::E_KEY_ACTION action = ui::SKeyboardEvent::ECA_UNITIALIZED; + if (actionStr == "pressed" || actionStr == "press") + action = ui::SKeyboardEvent::ECA_PRESSED; + else if (actionStr == "released" || actionStr == "release") + action = ui::SKeyboardEvent::ECA_RELEASED; + + if (action == ui::SKeyboardEvent::ECA_UNITIALIZED) + { + m_logger->log("Scripted keyboard event has invalid action \"%s\".", ILogger::ELL_WARNING, actionStr.c_str()); + continue; + } + + ScriptedInputEvent entry; + entry.frame = frame; + entry.type = ScriptedInputEvent::Type::Keyboard; + entry.keyboard.key = key; + entry.keyboard.action = action; + m_scriptedInput.events.emplace_back(entry); + } + else if (type == "mouse") + { + if (!ev.contains("kind")) + { + m_logger->log("Scripted mouse event missing \"kind\".", ILogger::ELL_WARNING); + continue; + } + + const auto kind = ev["kind"].get(); + ScriptedInputEvent entry; + entry.frame = frame; + entry.type = ScriptedInputEvent::Type::Mouse; + + if (kind == "move") + { + entry.mouse.type = ui::SMouseEvent::EET_MOVEMENT; + entry.mouse.dx = ev.value("dx", 0); + entry.mouse.dy = ev.value("dy", 0); + } + else if (kind == "scroll") + { + entry.mouse.type = ui::SMouseEvent::EET_SCROLL; + entry.mouse.v = ev.value("v", 0); + entry.mouse.h = ev.value("h", 0); + } + else if (kind == "click") + { + if (!ev.contains("button") || !ev.contains("action")) + { + m_logger->log("Scripted click event missing \"button\" or \"action\".", ILogger::ELL_WARNING); + continue; + } + + const auto buttonStr = ev["button"].get(); + const auto actionStr = ev["action"].get(); + + ui::E_MOUSE_BUTTON button = ui::EMB_LEFT_BUTTON; + if (buttonStr == "LEFT_BUTTON") button = ui::EMB_LEFT_BUTTON; + else if (buttonStr == "RIGHT_BUTTON") button = ui::EMB_RIGHT_BUTTON; + else if (buttonStr == "MIDDLE_BUTTON") button = ui::EMB_MIDDLE_BUTTON; + else if (buttonStr == "BUTTON_4") button = ui::EMB_BUTTON_4; + else if (buttonStr == "BUTTON_5") button = ui::EMB_BUTTON_5; + else + { + m_logger->log("Scripted click event has invalid button \"%s\".", ILogger::ELL_WARNING, buttonStr.c_str()); + continue; + } + + ui::SMouseEvent::SClickEvent::E_ACTION action = ui::SMouseEvent::SClickEvent::EA_UNITIALIZED; + if (actionStr == "pressed" || actionStr == "press") + action = ui::SMouseEvent::SClickEvent::EA_PRESSED; + else if (actionStr == "released" || actionStr == "release") + action = ui::SMouseEvent::SClickEvent::EA_RELEASED; + + if (action == ui::SMouseEvent::SClickEvent::EA_UNITIALIZED) + { + m_logger->log("Scripted click event has invalid action \"%s\".", ILogger::ELL_WARNING, actionStr.c_str()); + continue; + } + + entry.mouse.type = ui::SMouseEvent::EET_CLICK; + entry.mouse.button = button; + entry.mouse.action = action; + entry.mouse.x = ev.value("x", 0); + entry.mouse.y = ev.value("y", 0); + } + else + { + m_logger->log("Scripted mouse event has invalid kind \"%s\".", ILogger::ELL_WARNING, kind.c_str()); + continue; + } + + m_scriptedInput.events.emplace_back(entry); + } + else if (type == "imguizmo") + { + ScriptedInputEvent entry; + entry.frame = frame; + entry.type = ScriptedInputEvent::Type::Imguizmo; + + if (ev.contains("delta_trs")) + { + const auto arr = ev["delta_trs"].get>(); + float m16[16]; + for (size_t i = 0u; i < 16u; ++i) + m16[i] = arr[i]; + entry.imguizmo = *reinterpret_cast(m16); + } + else + { + const auto t = ev.contains("translation") ? ev["translation"].get>() : std::array{0.f, 0.f, 0.f}; + const auto r = ev.contains("rotation_deg") ? ev["rotation_deg"].get>() : std::array{0.f, 0.f, 0.f}; + const auto s = ev.contains("scale") ? ev["scale"].get>() : std::array{1.f, 1.f, 1.f}; + + float m16[16]; + float tr[3] = { t[0], t[1], t[2] }; + float rot[3] = { r[0], r[1], r[2] }; + float sc[3] = { s[0], s[1], s[2] }; + + ImGuizmo::RecomposeMatrixFromComponents(tr, rot, sc, m16); + entry.imguizmo = *reinterpret_cast(m16); + } + + m_scriptedInput.events.emplace_back(entry); + } + else if (type == "action") + { + if (!ev.contains("action")) + { + m_logger->log("Scripted action event missing \"action\".", ILogger::ELL_WARNING); + continue; + } + + const auto actionStr = ev["action"].get(); + ScriptedInputEvent entry; + entry.frame = frame; + entry.type = ScriptedInputEvent::Type::Action; + + auto getValueInt = [&]() -> int32_t + { + if (ev.contains("value")) + return ev["value"].get(); + if (ev.contains("index")) + return ev["index"].get(); + return 0; + }; + + if (actionStr == "set_active_render_window") + { + entry.action.kind = ScriptedInputEvent::ActionData::Kind::SetActiveRenderWindow; + entry.action.value = getValueInt(); + } + else if (actionStr == "set_active_planar") + { + entry.action.kind = ScriptedInputEvent::ActionData::Kind::SetActivePlanar; + entry.action.value = getValueInt(); + } + else if (actionStr == "set_projection_type") + { + entry.action.kind = ScriptedInputEvent::ActionData::Kind::SetProjectionType; + if (ev.contains("value") && ev["value"].is_string()) + { + const auto valueStr = ev["value"].get(); + if (valueStr == "perspective") + entry.action.value = static_cast(IPlanarProjection::CProjection::Perspective); + else if (valueStr == "orthographic") + entry.action.value = static_cast(IPlanarProjection::CProjection::Orthographic); + else + { + m_logger->log("Scripted action projection type has invalid value \"%s\".", ILogger::ELL_WARNING, valueStr.c_str()); + continue; + } + } + else + { + entry.action.value = getValueInt(); + } + } + else if (actionStr == "set_projection_index") + { + entry.action.kind = ScriptedInputEvent::ActionData::Kind::SetProjectionIndex; + entry.action.value = getValueInt(); + } + else if (actionStr == "set_use_window") + { + entry.action.kind = ScriptedInputEvent::ActionData::Kind::SetUseWindow; + entry.action.value = ev.value("value", false) ? 1 : 0; + } + else if (actionStr == "set_left_handed") + { + entry.action.kind = ScriptedInputEvent::ActionData::Kind::SetLeftHanded; + entry.action.value = ev.value("value", false) ? 1 : 0; + } + else + { + m_logger->log("Scripted action event has invalid action \"%s\".", ILogger::ELL_WARNING, actionStr.c_str()); + continue; + } + + m_scriptedInput.events.emplace_back(entry); + } + else + { + m_logger->log("Scripted input event has invalid type \"%s\".", ILogger::ELL_WARNING, type.c_str()); + } + } + + if (script.contains("checks")) + { + for (const auto& chk : script["checks"]) + { + if (!chk.contains("frame") || !chk.contains("kind")) + { + m_logger->log("Scripted check missing \"frame\" or \"kind\".", ILogger::ELL_WARNING); + continue; + } + + const auto frame = chk["frame"].get(); + const auto kind = chk["kind"].get(); + + ScriptedInputCheck entry; + entry.frame = frame; + + if (kind == "baseline") + { + entry.kind = ScriptedInputCheck::Kind::Baseline; + } + else if (kind == "imguizmo_virtual") + { + entry.kind = ScriptedInputCheck::Kind::ImguizmoVirtual; + entry.tolerance = chk.value("tolerance", entry.tolerance); + + if (!chk.contains("events")) + { + m_logger->log("Imguizmo virtual check missing \"events\".", ILogger::ELL_WARNING); + continue; + } + + for (const auto& ev : chk["events"]) + { + if (!ev.contains("type") || !ev.contains("magnitude")) + { + m_logger->log("Imguizmo virtual check event missing \"type\" or \"magnitude\".", ILogger::ELL_WARNING); + continue; + } + + const auto typeStr = ev["type"].get(); + const auto type = CVirtualGimbalEvent::stringToVirtualEvent(typeStr); + if (type == CVirtualGimbalEvent::None) + { + m_logger->log("Imguizmo virtual check event has invalid type \"%s\".", ILogger::ELL_WARNING, typeStr.c_str()); + continue; + } + + ScriptedInputCheck::ExpectedVirtualEvent expected; + expected.type = type; + expected.magnitude = ev["magnitude"].get(); + entry.expectedVirtualEvents.emplace_back(expected); + } + } + else if (kind == "gimbal_near") + { + entry.kind = ScriptedInputCheck::Kind::GimbalNear; + entry.posTolerance = chk.value("pos_tolerance", entry.posTolerance); + entry.eulerToleranceDeg = chk.value("euler_tolerance_deg", entry.eulerToleranceDeg); + + if (chk.contains("position")) + { + const auto pos = chk["position"].get>(); + entry.expectedPos = float32_t3(pos[0], pos[1], pos[2]); + entry.hasExpectedPos = true; + } + if (chk.contains("euler_deg")) + { + const auto euler = chk["euler_deg"].get>(); + entry.expectedEulerDeg = float32_t3(euler[0], euler[1], euler[2]); + entry.hasExpectedEuler = true; + } + } + else if (kind == "gimbal_delta") + { + entry.kind = ScriptedInputCheck::Kind::GimbalDelta; + entry.posTolerance = chk.value("pos_tolerance", entry.posTolerance); + entry.eulerToleranceDeg = chk.value("euler_tolerance_deg", entry.eulerToleranceDeg); + } + else + { + m_logger->log("Scripted check has invalid kind \"%s\".", ILogger::ELL_WARNING, kind.c_str()); + continue; + } + + m_scriptedInput.checks.emplace_back(entry); + } + } + + std::sort(m_scriptedInput.events.begin(), m_scriptedInput.events.end(), + [](const ScriptedInputEvent& a, const ScriptedInputEvent& b) { return a.frame < b.frame; }); + std::sort(m_scriptedInput.checks.begin(), m_scriptedInput.checks.end(), + [](const ScriptedInputCheck& a, const ScriptedInputCheck& b) { return a.frame < b.frame; }); + }; + + if (program.is_used("--script")) + { + system::path scriptPath = program.get("--script"); + if (scriptPath.is_relative()) + scriptPath = localInputCWD / scriptPath; + json scriptJson; + if (!loadScriptJson(scriptPath.string(), scriptJson)) + return false; + parseScriptedInput(scriptJson); + } + else if (j.contains("scripted_input")) + { + parseScriptedInput(j["scripted_input"]); + } + std::vector> cameras; for (const auto& jCamera : j["cameras"]) { @@ -1180,6 +1580,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication const auto viewParams = CSimpleDebugRenderer::SViewParams(binding.viewMatrix, binding.viewProjMatrix); m_renderer->render(cmdbuf, viewParams); + } willSubmit &= cmdbuf->endRenderPass(); }; @@ -1407,6 +1808,11 @@ class UISampleApp final : public examples::SimpleWindowedApplication inline bool keepRunning() override { + if (m_scriptedInput.enabled && m_scriptedInput.hardFail && m_scriptedInput.failed) + { + if (!m_ciMode || m_ciScreenshotDone) + std::exit(EXIT_FAILURE); + } if (m_ciMode && m_ciScreenshotDone) return false; if (m_surface->irrecoverable()) @@ -1457,6 +1863,169 @@ class UISampleApp final : public examples::SimpleWindowedApplication }, m_logger.get()); } + if (m_scriptedInput.enabled && m_scriptedInput.exclusive) + { + capturedEvents.mouse.clear(); + capturedEvents.keyboard.clear(); + } + + std::vector scriptedMouse; + std::vector scriptedKeyboard; + std::vector scriptedImguizmo; + std::vector scriptedActions; + const CVirtualGimbalEvent* scriptedImguizmoVirtual = nullptr; + uint32_t scriptedImguizmoVirtualCount = 0u; + + if (m_scriptedInput.enabled && m_scriptedInput.nextEventIndex < m_scriptedInput.events.size()) + { + const auto frame = m_realFrameIx; + while (m_scriptedInput.nextEventIndex < m_scriptedInput.events.size() && + m_scriptedInput.events[m_scriptedInput.nextEventIndex].frame == frame) + { + const auto& ev = m_scriptedInput.events[m_scriptedInput.nextEventIndex]; + + if (ev.type == ScriptedInputEvent::Type::Keyboard) + { + SKeyboardEvent e(m_nextPresentationTimestamp); + e.keyCode = ev.keyboard.key; + e.action = ev.keyboard.action; + e.window = m_window.get(); + scriptedKeyboard.emplace_back(e); + } + else if (ev.type == ScriptedInputEvent::Type::Mouse) + { + SMouseEvent e(m_nextPresentationTimestamp); + e.window = m_window.get(); + e.type = ev.mouse.type; + if (ev.mouse.type == ui::SMouseEvent::EET_CLICK) + { + e.clickEvent.mouseButton = ev.mouse.button; + e.clickEvent.action = ev.mouse.action; + e.clickEvent.clickPosX = ev.mouse.x; + e.clickEvent.clickPosY = ev.mouse.y; + } + else if (ev.mouse.type == ui::SMouseEvent::EET_SCROLL) + { + e.scrollEvent.verticalScroll = ev.mouse.v; + e.scrollEvent.horizontalScroll = ev.mouse.h; + } + else if (ev.mouse.type == ui::SMouseEvent::EET_MOVEMENT) + { + e.movementEvent.relativeMovementX = ev.mouse.dx; + e.movementEvent.relativeMovementY = ev.mouse.dy; + } + scriptedMouse.emplace_back(e); + } + else if (ev.type == ScriptedInputEvent::Type::Imguizmo) + { + scriptedImguizmo.emplace_back(ev.imguizmo); + } + else if (ev.type == ScriptedInputEvent::Type::Action) + { + scriptedActions.emplace_back(ev.action); + } + + ++m_scriptedInput.nextEventIndex; + } + } + + if (m_scriptedInput.enabled && scriptedActions.size()) + { + auto applyAction = [&](const ScriptedInputEvent::ActionData& action) -> void + { + switch (action.kind) + { + case ScriptedInputEvent::ActionData::Kind::SetActiveRenderWindow: + { + if (action.value < 0 || static_cast(action.value) >= windowBindings.size()) + { + m_logger->log("[script][warn] action set_active_render_window out of range: %d", ILogger::ELL_WARNING, action.value); + return; + } + activeRenderWindowIx = static_cast(action.value); + } break; + + case ScriptedInputEvent::ActionData::Kind::SetActivePlanar: + { + if (action.value < 0 || static_cast(action.value) >= m_planarProjections.size()) + { + m_logger->log("[script][warn] action set_active_planar out of range: %d", ILogger::ELL_WARNING, action.value); + return; + } + auto& binding = windowBindings[activeRenderWindowIx]; + binding.activePlanarIx = static_cast(action.value); + binding.pickDefaultProjections(m_planarProjections[binding.activePlanarIx]->getPlanarProjections()); + } break; + + case ScriptedInputEvent::ActionData::Kind::SetProjectionType: + { + auto& binding = windowBindings[activeRenderWindowIx]; + if (!binding.lastBoundPerspectivePresetProjectionIx.has_value() || !binding.lastBoundOrthoPresetProjectionIx.has_value()) + binding.pickDefaultProjections(m_planarProjections[binding.activePlanarIx]->getPlanarProjections()); + + const auto type = static_cast(action.value); + switch (type) + { + case IPlanarProjection::CProjection::Perspective: + binding.boundProjectionIx = binding.lastBoundPerspectivePresetProjectionIx.value(); + break; + case IPlanarProjection::CProjection::Orthographic: + binding.boundProjectionIx = binding.lastBoundOrthoPresetProjectionIx.value(); + break; + default: + m_logger->log("[script][warn] action set_projection_type invalid value: %d", ILogger::ELL_WARNING, action.value); + break; + } + } break; + + case ScriptedInputEvent::ActionData::Kind::SetProjectionIndex: + { + auto& binding = windowBindings[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; + } + const auto ix = static_cast(action.value); + const auto type = projections[ix].getParameters().m_type; + binding.boundProjectionIx = ix; + if (type == IPlanarProjection::CProjection::Perspective) + binding.lastBoundPerspectivePresetProjectionIx = ix; + else if (type == IPlanarProjection::CProjection::Orthographic) + binding.lastBoundOrthoPresetProjectionIx = ix; + } break; + + case ScriptedInputEvent::ActionData::Kind::SetUseWindow: + { + useWindow = action.value != 0; + } break; + + case ScriptedInputEvent::ActionData::Kind::SetLeftHanded: + { + auto& binding = windowBindings[activeRenderWindowIx]; + binding.leftHandedProjection = action.value != 0; + } break; + } + }; + + for (const auto& action : scriptedActions) + if (action.kind == ScriptedInputEvent::ActionData::Kind::SetActiveRenderWindow) + applyAction(action); + + for (const auto& action : scriptedActions) + if (action.kind != ScriptedInputEvent::ActionData::Kind::SetActiveRenderWindow) + applyAction(action); + + if (m_scriptedInput.log) + m_logger->log("[script] frame %llu actions=%zu", ILogger::ELL_INFO, static_cast(m_realFrameIx), scriptedActions.size()); + } + + if (!scriptedMouse.empty()) + capturedEvents.mouse.insert(capturedEvents.mouse.end(), scriptedMouse.begin(), scriptedMouse.end()); + if (!scriptedKeyboard.empty()) + capturedEvents.keyboard.insert(capturedEvents.keyboard.end(), scriptedKeyboard.begin(), scriptedKeyboard.end()); + const auto cursorPosition = m_window->getCursorControl()->getPosition(); nbl::ext::imgui::UI::SUpdateParameters params = @@ -1467,6 +2036,15 @@ class UISampleApp final : public examples::SimpleWindowedApplication .keyboardEvents = { capturedEvents.keyboard.data(), capturedEvents.keyboard.size() } }; + if (m_scriptedInput.log && (scriptedKeyboard.size() || scriptedMouse.size() || scriptedImguizmo.size())) + { + m_logger->log("[script] frame %llu input kb=%zu mouse=%zu imguizmo=%zu", ILogger::ELL_INFO, + static_cast(m_realFrameIx), + scriptedKeyboard.size(), + scriptedMouse.size(), + scriptedImguizmo.size()); + } + if (enableActiveCameraMovement) { auto& binding = windowBindings[activeRenderWindowIx]; @@ -1513,10 +2091,253 @@ class UISampleApp final : public examples::SimpleWindowedApplication projection.endInputProcessing(); if (vCount) + { camera->manipulate({ virtualEvents.data(), vCount }); + if (m_scriptedInput.log) + { + for (uint32_t i = 0u; i < vCount; ++i) + { + const auto& ev = virtualEvents[i]; + m_logger->log("[script] virtual %s magnitude=%.6f", ILogger::ELL_INFO, CVirtualGimbalEvent::virtualEventToString(ev.type).data(), ev.magnitude); + } + + const auto& gimbal = camera->getGimbal(); + const auto pos = gimbal.getPosition(); + const auto euler = glm::degrees(glm::eulerAngles(gimbal.getOrientation())); + m_logger->log("[script] gimbal pos=(%.3f, %.3f, %.3f) euler_deg=(%.3f, %.3f, %.3f)", ILogger::ELL_INFO, + pos.x, pos.y, pos.z, euler.x, euler.y, euler.z); + } + } + } + + if (m_scriptedInput.enabled && scriptedImguizmo.size()) + { + auto& binding = windowBindings[activeRenderWindowIx]; + auto& planar = m_planarProjections[binding.activePlanarIx]; + auto* camera = planar->getCamera(); + + static std::vector imguizmoEvents(0x20); + uint32_t vCount = 0u; + + camera->beginInputProcessing(m_nextPresentationTimestamp); + { + camera->processImguizmo(nullptr, vCount, {}); + if (imguizmoEvents.size() < vCount) + imguizmoEvents.resize(vCount); + + camera->processImguizmo(imguizmoEvents.data(), vCount, { scriptedImguizmo.data(), scriptedImguizmo.size() }); + } + camera->endInputProcessing(); + + if (vCount) + { + camera->manipulate({ imguizmoEvents.data(), vCount }); + + if (m_scriptedInput.log) + { + for (uint32_t i = 0u; i < vCount; ++i) + { + const auto& ev = imguizmoEvents[i]; + m_logger->log("[script] imguizmo virtual %s magnitude=%.6f", ILogger::ELL_INFO, CVirtualGimbalEvent::virtualEventToString(ev.type).data(), ev.magnitude); + } + + const auto& gimbal = camera->getGimbal(); + const auto pos = gimbal.getPosition(); + const auto euler = glm::degrees(glm::eulerAngles(gimbal.getOrientation())); + m_logger->log("[script] imguizmo gimbal pos=(%.3f, %.3f, %.3f) euler_deg=(%.3f, %.3f, %.3f)", ILogger::ELL_INFO, + pos.x, pos.y, pos.z, euler.x, euler.y, euler.z); + } + } + + scriptedImguizmoVirtual = vCount ? imguizmoEvents.data() : nullptr; + scriptedImguizmoVirtualCount = vCount; + } + + if (m_scriptedInput.enabled && m_scriptedInput.nextCheckIndex < m_scriptedInput.checks.size()) + { + auto* camera = [&]() -> ICamera* + { + if (m_planarProjections.empty()) + return nullptr; + auto& binding = windowBindings[activeRenderWindowIx]; + if (binding.activePlanarIx >= m_planarProjections.size()) + return nullptr; + return m_planarProjections[binding.activePlanarIx]->getCamera(); + }(); + + 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)...); + }; + + auto angleDiffDeg = [](float a, float b) -> float + { + float d = std::fmod(a - b + 180.0f, 360.0f); + if (d < 0.0f) + d += 360.0f; + return std::abs(d - 180.0f); + }; + + auto isFinite3 = [](const float32_t3& v) -> bool + { + return std::isfinite(v.x) && std::isfinite(v.y) && std::isfinite(v.z); + }; + + const auto frame = m_realFrameIx; + while (m_scriptedInput.nextCheckIndex < m_scriptedInput.checks.size() && + m_scriptedInput.checks[m_scriptedInput.nextCheckIndex].frame == frame) + { + const auto& check = m_scriptedInput.checks[m_scriptedInput.nextCheckIndex]; + + if (!camera) + { + logFail("[script][fail] check frame=%llu no active camera", static_cast(frame)); + ++m_scriptedInput.nextCheckIndex; + continue; + } + + const auto& gimbal = camera->getGimbal(); + const auto pos = gimbal.getPosition(); + const auto euler = glm::degrees(glm::eulerAngles(gimbal.getOrientation())); + + if (!isFinite3(pos) || !isFinite3(euler)) + { + logFail("[script][fail] check frame=%llu non-finite gimbal state", static_cast(frame)); + ++m_scriptedInput.nextCheckIndex; + continue; + } + + if (check.kind == ScriptedInputCheck::Kind::Baseline) + { + m_scriptedInput.baselineValid = true; + m_scriptedInput.baselinePos = pos; + m_scriptedInput.baselineEulerDeg = euler; + logPass("[script][pass] baseline frame=%llu pos=(%.3f, %.3f, %.3f) euler_deg=(%.3f, %.3f, %.3f)", + static_cast(frame), + pos.x, pos.y, pos.z, euler.x, euler.y, euler.z); + } + else if (check.kind == ScriptedInputCheck::Kind::ImguizmoVirtual) + { + bool ok = true; + if (!scriptedImguizmoVirtual || scriptedImguizmoVirtualCount == 0u) + { + ok = false; + } + else + { + for (const auto& expected : check.expectedVirtualEvents) + { + bool found = false; + double actual = 0.0; + for (uint32_t i = 0u; i < scriptedImguizmoVirtualCount; ++i) + { + if (scriptedImguizmoVirtual[i].type == expected.type) + { + found = true; + actual = scriptedImguizmoVirtual[i].magnitude; + break; + } + } + if (!found || std::abs(actual - expected.magnitude) > check.tolerance) + { + ok = false; + logFail("[script][fail] imguizmo_virtual frame=%llu type=%s expected=%.6f actual=%.6f tol=%.6f", + static_cast(frame), + CVirtualGimbalEvent::virtualEventToString(expected.type).data(), + expected.magnitude, + actual, + check.tolerance); + } + } + } + + if (ok) + logPass("[script][pass] imguizmo_virtual frame=%llu events=%zu", static_cast(frame), check.expectedVirtualEvents.size()); + } + else if (check.kind == ScriptedInputCheck::Kind::GimbalNear) + { + bool ok = true; + if (check.hasExpectedPos) + { + const auto diff = float32_t3(pos.x - check.expectedPos.x, pos.y - check.expectedPos.y, pos.z - check.expectedPos.z); + const auto d = std::sqrt(diff.x * diff.x + diff.y * diff.y + diff.z * diff.z); + if (d > check.posTolerance) + { + ok = false; + logFail("[script][fail] gimbal_near frame=%llu pos_diff=%.6f tol=%.6f", + static_cast(frame), d, check.posTolerance); + } + } + if (check.hasExpectedEuler) + { + const auto dx = angleDiffDeg(euler.x, check.expectedEulerDeg.x); + const auto dy = angleDiffDeg(euler.y, check.expectedEulerDeg.y); + const auto dz = angleDiffDeg(euler.z, check.expectedEulerDeg.z); + const auto dmax = std::max(dx, std::max(dy, dz)); + if (dmax > check.eulerToleranceDeg) + { + ok = false; + logFail("[script][fail] gimbal_near frame=%llu euler_diff=%.6f tol=%.6f", + static_cast(frame), dmax, check.eulerToleranceDeg); + } + } + + if (ok) + logPass("[script][pass] gimbal_near frame=%llu", static_cast(frame)); + } + else if (check.kind == ScriptedInputCheck::Kind::GimbalDelta) + { + if (!m_scriptedInput.baselineValid) + { + logFail("[script][fail] gimbal_delta frame=%llu missing baseline", static_cast(frame)); + } + else + { + const auto diff = float32_t3(pos.x - m_scriptedInput.baselinePos.x, pos.y - m_scriptedInput.baselinePos.y, pos.z - m_scriptedInput.baselinePos.z); + const auto dpos = std::sqrt(diff.x * diff.x + diff.y * diff.y + diff.z * diff.z); + const auto dx = angleDiffDeg(euler.x, m_scriptedInput.baselineEulerDeg.x); + const auto dy = angleDiffDeg(euler.y, m_scriptedInput.baselineEulerDeg.y); + const auto dz = angleDiffDeg(euler.z, m_scriptedInput.baselineEulerDeg.z); + const auto dmax = std::max(dx, std::max(dy, dz)); + + if (dpos > check.posTolerance || dmax > check.eulerToleranceDeg) + { + logFail("[script][fail] gimbal_delta frame=%llu pos_diff=%.6f tol=%.6f euler_diff=%.6f tol=%.6f", + static_cast(frame), + dpos, check.posTolerance, + dmax, check.eulerToleranceDeg); + } + else + { + logPass("[script][pass] gimbal_delta frame=%llu pos_diff=%.6f euler_diff=%.6f", + static_cast(frame), dpos, dmax); + } + } + } + + ++m_scriptedInput.nextCheckIndex; + } + + if (!m_scriptedInput.summaryReported && m_scriptedInput.nextCheckIndex >= m_scriptedInput.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); + } } m_ui.manager->update(params); + } private: @@ -1636,7 +2457,10 @@ class UISampleApp final : public examples::SimpleWindowedApplication // I will assume we need to focus a window to start manipulating objects from it if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) - activeRenderWindowIx = windowIx; + { + if (!(m_scriptedInput.enabled && m_scriptedInput.exclusive)) + activeRenderWindowIx = windowIx; + } // we render a scene from view of a camera bound to planar window ImGuizmoPlanarM16InOut imguizmoPlanar; @@ -2578,6 +3402,105 @@ class UISampleApp final : public examples::SimpleWindowedApplication } }; + struct ScriptedInputEvent + { + enum class Type : uint8_t + { + Keyboard, + Mouse, + Imguizmo, + Action + }; + + struct KeyboardData + { + ui::E_KEY_CODE key = ui::EKC_NONE; + ui::SKeyboardEvent::E_KEY_ACTION action = ui::SKeyboardEvent::ECA_UNITIALIZED; + }; + + struct MouseData + { + ui::SMouseEvent::E_EVENT_TYPE type = ui::SMouseEvent::EET_UNITIALIZED; + ui::E_MOUSE_BUTTON button = ui::EMB_LEFT_BUTTON; + ui::SMouseEvent::SClickEvent::E_ACTION action = ui::SMouseEvent::SClickEvent::EA_UNITIALIZED; + int16_t x = 0; + int16_t y = 0; + int16_t dx = 0; + int16_t dy = 0; + int16_t v = 0; + int16_t h = 0; + }; + + struct ActionData + { + enum class Kind : uint8_t + { + SetActiveRenderWindow, + SetActivePlanar, + SetProjectionType, + SetProjectionIndex, + SetUseWindow, + SetLeftHanded + }; + + Kind kind = Kind::SetActiveRenderWindow; + int32_t value = 0; + }; + + uint64_t frame = 0; + Type type = Type::Keyboard; + KeyboardData keyboard; + MouseData mouse; + float32_t4x4 imguizmo = float32_t4x4(1.f); + ActionData action; + }; + + struct ScriptedInputCheck + { + enum class Kind : uint8_t + { + Baseline, + ImguizmoVirtual, + GimbalNear, + GimbalDelta + }; + + struct ExpectedVirtualEvent + { + CVirtualGimbalEvent::VirtualEventType type = CVirtualGimbalEvent::None; + float64_t magnitude = 0.0; + }; + + uint64_t frame = 0; + Kind kind = Kind::Baseline; + float tolerance = 1e-3f; + std::vector expectedVirtualEvents; + + float32_t3 expectedPos = float32_t3(0.f); + float32_t3 expectedEulerDeg = float32_t3(0.f); + bool hasExpectedPos = false; + bool hasExpectedEuler = false; + float posTolerance = 0.05f; + float eulerToleranceDeg = 1.0f; + }; + + struct ScriptedInputState + { + bool enabled = false; + bool log = false; + bool exclusive = false; + bool hardFail = false; + std::vector events; + size_t nextEventIndex = 0; + std::vector checks; + size_t nextCheckIndex = 0; + bool failed = false; + bool summaryReported = false; + bool baselineValid = false; + float32_t3 baselinePos = float32_t3(0.f); + float32_t3 baselineEulerDeg = float32_t3(0.f); + }; + static constexpr inline auto MaxSceneFBOs = 2u; std::array windowBindings; uint32_t activeRenderWindowIx = 0u; @@ -2598,6 +3521,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication bool m_ciScreenshotDone = false; uint32_t m_ciFrameCounter = 0u; system::path m_ciScreenshotPath; + ScriptedInputState m_scriptedInput; const bool flipGizmoY = true; diff --git a/common/include/camera/IGimbal.hpp b/common/include/camera/IGimbal.hpp index 384786b46..b4114570e 100644 --- a/common/include/camera/IGimbal.hpp +++ b/common/include/camera/IGimbal.hpp @@ -285,7 +285,7 @@ namespace nbl::hlsl inline void transform(const CReferenceTransform& reference, const VirtualImpulse& impulse) { - setOrientation(reference.orientation * glm::quat(glm::radians(impulse.dVirtualRotation))); + setOrientation(reference.orientation * glm::quat(impulse.dVirtualRotation)); setPosition(mul(float64_t4(impulse.dVirtualTranslate, 1), reference.frame).xyz); } @@ -448,4 +448,4 @@ namespace nbl::hlsl }; } // namespace nbl::hlsl -#endif // _NBL_IGIMBAL_HPP_ \ No newline at end of file +#endif // _NBL_IGIMBAL_HPP_ diff --git a/common/include/camera/IGimbalController.hpp b/common/include/camera/IGimbalController.hpp index 7b4ba365e..536fdd6f7 100644 --- a/common/include/camera/IGimbalController.hpp +++ b/common/include/camera/IGimbalController.hpp @@ -114,8 +114,14 @@ class IGimbalController : public IGimbalManipulateEncoder void beginInputProcessing(const std::chrono::microseconds nextPresentationTimeStamp) { m_nextPresentationTimeStamp = nextPresentationTimeStamp; - m_frameDeltaTime = std::chrono::duration_cast(m_nextPresentationTimeStamp - m_lastVirtualUpTimeStamp).count(); - assert(m_frameDeltaTime >= 0.f); + const auto deltaMs = std::chrono::duration_cast(m_nextPresentationTimeStamp - m_lastVirtualUpTimeStamp).count(); + constexpr double MaxFrameDeltaMs = 200.0; + if (deltaMs < 0) + m_frameDeltaTime = 0.0; + else if (static_cast(deltaMs) > MaxFrameDeltaMs) + m_frameDeltaTime = MaxFrameDeltaMs; + else + m_frameDeltaTime = static_cast(deltaMs); } void endInputProcessing() @@ -226,13 +232,7 @@ class IGimbalController : public IGimbalManipulateEncoder if (keyboardEvent.action == input_keyboard_event_t::ECA_PRESSED) { if (!hash.active) - { - const auto keyDeltaTime = std::chrono::duration_cast(m_nextPresentationTimeStamp - keyboardEvent.timeStamp).count(); - assert(keyDeltaTime >= 0); - hash.active = true; - hash.event.magnitude = keyDeltaTime; - } } else if (keyboardEvent.action == input_keyboard_event_t::ECA_RELEASED) hash.active = false; @@ -303,13 +303,7 @@ class IGimbalController : public IGimbalManipulateEncoder if (mouseEvent.clickEvent.action == input_mouse_event_t::SClickEvent::EA_PRESSED) { if (!hash.active) - { - const auto keyDeltaTime = std::chrono::duration_cast(m_nextPresentationTimeStamp - mouseEvent.timeStamp).count(); - assert(keyDeltaTime >= 0); - hash.active = true; - hash.event.magnitude += keyDeltaTime; - } } else if (mouseEvent.clickEvent.action == input_mouse_event_t::SClickEvent::EA_RELEASED) hash.active = false; @@ -390,9 +384,15 @@ class IGimbalController : public IGimbalManipulateEncoder requestMagnitudeUpdateWithScalar(0.f, world.dTranslation[2], std::abs(world.dTranslation[2]), gimbal_event_t::MoveForward, gimbal_event_t::MoveBackward, m_imguizmoVirtualEventMap); // Delta rotation impulse - requestMagnitudeUpdateWithScalar(0.f, world.dRotation[0], std::abs(world.dRotation[0]), gimbal_event_t::TiltUp , gimbal_event_t::TiltDown, m_imguizmoVirtualEventMap); - requestMagnitudeUpdateWithScalar(0.f, world.dRotation[1], std::abs(world.dRotation[1]), gimbal_event_t::PanRight, gimbal_event_t::PanLeft, m_imguizmoVirtualEventMap); - requestMagnitudeUpdateWithScalar(0.f, world.dRotation[2], std::abs(world.dRotation[2]), gimbal_event_t::RollRight, gimbal_event_t::RollLeft, m_imguizmoVirtualEventMap); + const float32_t3 dRotationRad = + { + glm::radians(world.dRotation[0]), + glm::radians(world.dRotation[1]), + glm::radians(world.dRotation[2]) + }; + requestMagnitudeUpdateWithScalar(0.f, dRotationRad[0], std::abs(dRotationRad[0]), gimbal_event_t::TiltUp , gimbal_event_t::TiltDown, m_imguizmoVirtualEventMap); + requestMagnitudeUpdateWithScalar(0.f, dRotationRad[1], std::abs(dRotationRad[1]), gimbal_event_t::PanRight, gimbal_event_t::PanLeft, m_imguizmoVirtualEventMap); + requestMagnitudeUpdateWithScalar(0.f, dRotationRad[2], std::abs(dRotationRad[2]), gimbal_event_t::RollRight, gimbal_event_t::RollLeft, m_imguizmoVirtualEventMap); // Delta scale impulse requestMagnitudeUpdateWithScalar(1.f, world.dScale[0], std::abs(world.dScale[0]), gimbal_event_t::ScaleXInc, gimbal_event_t::ScaleXDec, m_imguizmoVirtualEventMap); @@ -448,10 +448,10 @@ class IGimbalController : public IGimbalManipulateEncoder mouse_to_virtual_events_t m_mouseVirtualEventMap; imguizmo_to_virtual_events_t m_imguizmoVirtualEventMap; - size_t m_frameDeltaTime = {}; + double m_frameDeltaTime = {}; std::chrono::microseconds m_nextPresentationTimeStamp = {}, m_lastVirtualUpTimeStamp = {}; }; } // nbl::hlsl namespace -#endif // _NBL_I_CAMERA_CONTROLLER_HPP_ \ No newline at end of file +#endif // _NBL_I_CAMERA_CONTROLLER_HPP_ From 893106ea86859713f35f8a7a80848dfa1d6785dc Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Thu, 5 Feb 2026 13:09:36 +0100 Subject: [PATCH 096/205] camera types and control updates --- 61_UI/app_resources/cameras.json | 54 +- 61_UI/include/common.hpp | 8 + 61_UI/main.cpp | 2521 ++++++++++++++--- common/include/camera/CArcballCamera.hpp | 208 ++ common/include/camera/CFreeLockCamera.hpp | 63 - common/include/camera/COrbitCamera.hpp | 7 + .../include/camera/CTargetPoseController.hpp | 219 ++ common/include/camera/CTurntableCamera.hpp | 195 ++ 8 files changed, 2739 insertions(+), 536 deletions(-) create mode 100644 common/include/camera/CArcballCamera.hpp create mode 100644 common/include/camera/CTargetPoseController.hpp create mode 100644 common/include/camera/CTurntableCamera.hpp diff --git a/61_UI/app_resources/cameras.json b/61_UI/app_resources/cameras.json index 7e76aef83..418be8ffa 100644 --- a/61_UI/app_resources/cameras.json +++ b/61_UI/app_resources/cameras.json @@ -14,6 +14,16 @@ "type": "Free", "position": [2.116, 0.826, 1.152], "orientation": [0.095, -0.835, 0.152, 0.521] + }, + { + "type": "Arcball", + "position": [1.500, 1.200, -1.800], + "target": [0, 0, 0] + }, + { + "type": "Turntable", + "position": [-1.400, 1.100, 1.600], + "target": [0, 0, 0] } ], "projections": [ @@ -48,13 +58,15 @@ { "projection": 0, "controllers": { - "keyboard": 2 + "keyboard": 2, + "mouse": 0 } }, { "projection": 1, "controllers": { - "keyboard": 1 + "keyboard": 1, + "mouse": 0 } }, { @@ -70,6 +82,34 @@ "keyboard": 3, "mouse": 1 } + }, + { + "projection": 0, + "controllers": { + "keyboard": 2, + "mouse": 0 + } + }, + { + "projection": 1, + "controllers": { + "keyboard": 2, + "mouse": 0 + } + }, + { + "projection": 0, + "controllers": { + "keyboard": 2, + "mouse": 0 + } + }, + { + "projection": 1, + "controllers": { + "keyboard": 2, + "mouse": 0 + } } ], "planars": [ @@ -84,6 +124,14 @@ { "camera": 2, "viewports": [2, 3] + }, + { + "camera": 3, + "viewports": [6, 7] + }, + { + "camera": 4, + "viewports": [8, 9] } ], "controllers": { @@ -159,4 +207,4 @@ ] } } - \ No newline at end of file + diff --git a/61_UI/include/common.hpp b/61_UI/include/common.hpp index 6b5c64e13..ca3a5fa9f 100644 --- a/61_UI/include/common.hpp +++ b/61_UI/include/common.hpp @@ -9,6 +9,9 @@ #include "camera/CFPSCamera.hpp" #include "camera/CFreeLockCamera.hpp" #include "camera/COrbitCamera.hpp" +#include "camera/CArcballCamera.hpp" +#include "camera/CTurntableCamera.hpp" +#include "camera/CTargetPoseController.hpp" #include "camera/CCubeProjection.hpp" #include "camera/CLinearProjection.hpp" @@ -33,6 +36,10 @@ using nbl::hlsl::ICamera; using nbl::hlsl::CFPSCamera; using nbl::hlsl::CFreeCamera; using nbl::hlsl::COrbitCamera; +using nbl::hlsl::CArcballCamera; +using nbl::hlsl::CTurntableCamera; +using nbl::hlsl::CTargetPose; +using nbl::hlsl::CTargetPoseController; using nbl::hlsl::IPlanarProjection; using nbl::hlsl::CPlanarProjection; using nbl::hlsl::IGimbalController; @@ -46,6 +53,7 @@ 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_t4x4; using nbl::hlsl::uint16_t2; using nbl::hlsl::getCastedMatrix; diff --git a/61_UI/main.cpp b/61_UI/main.cpp index bba9c96bd..76fa5807c 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -5,6 +5,10 @@ #include #include #include +#include +#include +#include +#include #include #include "nlohmann/json.hpp" #include "argparse/argparse.hpp" @@ -14,6 +18,7 @@ using json = nlohmann::json; #include "keysmapping.hpp" #include "camera/CCubeProjection.hpp" #include "glm/glm/ext/matrix_clip_space.hpp" // TODO: TESTING +#include "glm/gtc/quaternion.hpp" #include "nbl/ext/ScreenShot/ScreenShot.h" #include "nbl/this_example/builtin/build/spirv/keys.hpp" #if __has_include("nbl/this_example/builtin/CArchive.h") @@ -456,13 +461,17 @@ class UISampleApp final : public examples::SimpleWindowedApplication { m_scriptedInput.events.clear(); m_scriptedInput.checks.clear(); + m_scriptedInput.captureFrames.clear(); m_scriptedInput.nextEventIndex = 0; m_scriptedInput.nextCheckIndex = 0; + m_scriptedInput.nextCaptureIndex = 0; m_scriptedInput.failed = false; m_scriptedInput.summaryReported = false; m_scriptedInput.baselineValid = false; m_scriptedInput.exclusive = false; m_scriptedInput.hardFail = false; + m_scriptedInput.capturePrefix = "script"; + m_scriptedInput.captureOutputDir = localOutputCWD; if (script.contains("enabled")) m_scriptedInput.enabled = script["enabled"].get(); @@ -485,6 +494,29 @@ class UISampleApp final : public examples::SimpleWindowedApplication if (script.contains("exclusive")) m_scriptedInput.exclusive = script["exclusive"].get() || m_scriptedInput.exclusive; + if (script.contains("capture_prefix")) + m_scriptedInput.capturePrefix = script["capture_prefix"].get(); + if (m_scriptedInput.capturePrefix.empty()) + m_scriptedInput.capturePrefix = "script"; + if (script.contains("capture_frames")) + for (const auto& frame : script["capture_frames"]) + m_scriptedInput.captureFrames.emplace_back(frame.get()); + + if (script.contains("camera_controls")) + { + const auto& controls = script["camera_controls"]; + if (controls.contains("keyboard_scale")) + m_cameraControls.keyboardScale = controls["keyboard_scale"].get(); + if (controls.contains("mouse_move_scale")) + m_cameraControls.mouseMoveScale = controls["mouse_move_scale"].get(); + if (controls.contains("mouse_scroll_scale")) + m_cameraControls.mouseScrollScale = controls["mouse_scroll_scale"].get(); + if (controls.contains("translation_scale")) + m_cameraControls.translationScale = controls["translation_scale"].get(); + if (controls.contains("rotation_scale")) + m_cameraControls.rotationScale = controls["rotation_scale"].get(); + } + if (script.contains("events")) for (const auto& ev : script["events"]) { @@ -496,6 +528,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication const auto frame = ev["frame"].get(); const auto type = ev["type"].get(); + const bool captureFrame = ev.value("capture", false); if (type == "keyboard") { @@ -532,6 +565,8 @@ class UISampleApp final : public examples::SimpleWindowedApplication entry.keyboard.key = key; entry.keyboard.action = action; m_scriptedInput.events.emplace_back(entry); + if (captureFrame) + m_scriptedInput.captureFrames.emplace_back(frame); } else if (type == "mouse") { @@ -606,6 +641,8 @@ class UISampleApp final : public examples::SimpleWindowedApplication } m_scriptedInput.events.emplace_back(entry); + if (captureFrame) + m_scriptedInput.captureFrames.emplace_back(frame); } else if (type == "imguizmo") { @@ -637,6 +674,8 @@ class UISampleApp final : public examples::SimpleWindowedApplication } m_scriptedInput.events.emplace_back(entry); + if (captureFrame) + m_scriptedInput.captureFrames.emplace_back(frame); } else if (type == "action") { @@ -713,6 +752,8 @@ class UISampleApp final : public examples::SimpleWindowedApplication } m_scriptedInput.events.emplace_back(entry); + if (captureFrame) + m_scriptedInput.captureFrames.emplace_back(frame); } else { @@ -812,6 +853,11 @@ class UISampleApp final : public examples::SimpleWindowedApplication [](const ScriptedInputEvent& a, const ScriptedInputEvent& b) { return a.frame < b.frame; }); std::sort(m_scriptedInput.checks.begin(), m_scriptedInput.checks.end(), [](const ScriptedInputCheck& a, const ScriptedInputCheck& b) { return a.frame < b.frame; }); + if (!m_scriptedInput.captureFrames.empty()) + { + std::sort(m_scriptedInput.captureFrames.begin(), m_scriptedInput.captureFrames.end()); + m_scriptedInput.captureFrames.erase(std::unique(m_scriptedInput.captureFrames.begin(), m_scriptedInput.captureFrames.end()), m_scriptedInput.captureFrames.end()); + } }; if (program.is_used("--script")) @@ -864,6 +910,10 @@ class UISampleApp final : public examples::SimpleWindowedApplication return float32_t3(jret[0], jret[1], jret[2]); }; + constexpr float DefaultMoveScale = 0.01f; + constexpr float DefaultRotateScale = 0.003f; + constexpr float OrbitMoveScale = 0.5f; + if (jCamera["type"] == "FPS") { if (!withOrientation) @@ -872,7 +922,10 @@ class UISampleApp final : public examples::SimpleWindowedApplication return false; } - cameras.emplace_back() = make_smart_refctd_ptr(position, getOrientation()); + auto camera = make_smart_refctd_ptr(position, getOrientation()); + camera->setMoveSpeedScale(DefaultMoveScale); + camera->setRotationSpeedScale(DefaultRotateScale); + cameras.emplace_back(std::move(camera)); } else if (jCamera["type"] == "Free") { @@ -882,12 +935,31 @@ class UISampleApp final : public examples::SimpleWindowedApplication return false; } - cameras.emplace_back() = make_smart_refctd_ptr(position, getOrientation()); + auto camera = make_smart_refctd_ptr(position, getOrientation()); + camera->setMoveSpeedScale(DefaultMoveScale); + camera->setRotationSpeedScale(DefaultRotateScale); + cameras.emplace_back(std::move(camera)); } else if (jCamera["type"] == "Orbit") { - auto& camera = cameras.emplace_back() = make_smart_refctd_ptr(position, getTarget()); - camera->setMoveSpeedScale(0.2); + auto camera = make_smart_refctd_ptr(position, getTarget()); + camera->setMoveSpeedScale(OrbitMoveScale); + camera->setRotationSpeedScale(DefaultRotateScale); + cameras.emplace_back(std::move(camera)); + } + else if (jCamera["type"] == "Arcball") + { + auto camera = make_smart_refctd_ptr(position, getTarget()); + camera->setMoveSpeedScale(OrbitMoveScale); + camera->setRotationSpeedScale(DefaultRotateScale); + cameras.emplace_back(std::move(camera)); + } + else if (jCamera["type"] == "Turntable") + { + auto camera = make_smart_refctd_ptr(position, getTarget()); + camera->setMoveSpeedScale(OrbitMoveScale); + camera->setRotationSpeedScale(DefaultRotateScale); + cameras.emplace_back(std::move(camera)); } else { @@ -1272,6 +1344,59 @@ class UISampleApp final : public examples::SimpleWindowedApplication // UI { { + constexpr size_t ImGuiStreamingBufferSize = 32ull * 1024ull * 1024ull; + auto createImGuiStreamingBuffer = [&](size_t size) -> smart_refctd_ptr + { + constexpr uint32_t minStreamingBufferAllocationSize = 128u; + constexpr uint32_t maxStreamingBufferAllocationAlignment = 4096u; + + auto getRequiredAccessFlags = [&](const bitflag& properties) + { + bitflag flags(IDeviceMemoryAllocation::EMCAF_NO_MAPPING_ACCESS); + + if (properties.hasFlags(IDeviceMemoryAllocation::EMPF_HOST_READABLE_BIT)) + flags |= IDeviceMemoryAllocation::EMCAF_READ; + if (properties.hasFlags(IDeviceMemoryAllocation::EMPF_HOST_WRITABLE_BIT)) + flags |= IDeviceMemoryAllocation::EMCAF_WRITE; + + return flags; + }; + + IGPUBuffer::SCreationParams mdiCreationParams = {}; + mdiCreationParams.usage = nbl::ext::imgui::UI::SCachedCreationParams::RequiredUsageFlags; + mdiCreationParams.size = size; + + auto buffer = m_utils->getLogicalDevice()->createBuffer(std::move(mdiCreationParams)); + if (!buffer) + return nullptr; + + buffer->setObjectDebugName("ImGui MDI Upstream Buffer"); + + auto memoryReqs = buffer->getMemoryReqs(); + memoryReqs.memoryTypeBits &= m_utils->getLogicalDevice()->getPhysicalDevice()->getUpStreamingMemoryTypeBits(); + + auto allocation = m_utils->getLogicalDevice()->allocate(memoryReqs, buffer.get(), nbl::ext::imgui::UI::SCachedCreationParams::RequiredAllocateFlags); + if (!allocation.isValid()) + return nullptr; + + auto memory = allocation.memory; + + if (!memory->map({ 0ull, memoryReqs.size }, getRequiredAccessFlags(memory->getMemoryPropertyFlags()))) + { + m_logger->log("Could not map ImGui streaming buffer memory!", ILogger::ELL_ERROR); + return nullptr; + } + + return make_smart_refctd_ptr( + SBufferRange{0ull, mdiCreationParams.size, std::move(buffer)}, + maxStreamingBufferAllocationAlignment, + minStreamingBufferAllocationSize); + }; + + auto imguiStreamingBuffer = createImGuiStreamingBuffer(ImGuiStreamingBufferSize); + if (!imguiStreamingBuffer) + return logFail("Failed to create ImGui streaming buffer."); + nbl::ext::imgui::UI::SCreationParameters params; params.resources.texturesInfo = { .setIx = 0u, .bindingIx = 0u }; params.resources.samplersInfo = { .setIx = 0u, .bindingIx = 1u }; @@ -1279,7 +1404,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication 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.streamingBuffer = nullptr; + params.streamingBuffer = std::move(imguiStreamingBuffer); params.subpassIx = 0u; params.transfer = getTransferUpQueue(); params.utilities = m_utils; @@ -1334,20 +1459,24 @@ class UISampleApp final : public examples::SimpleWindowedApplication const auto ds = float32_t2{ m_window->getWidth(), m_window->getHeight() }; wInit.trsEditor.iPos = iPaddingOffset; - wInit.trsEditor.iSize = { ds.x * 0.1, ds.y - wInit.trsEditor.iPos.y * 2 }; + wInit.trsEditor.iSize = { 0.0f, ds.y - wInit.trsEditor.iPos.y * 2 }; - wInit.planars.iSize = { ds.x * 0.2, ds.y - iPaddingOffset.y * 2 }; + const float panelWidth = std::clamp(ds.x * 0.33f, 380.0f, ds.x * 0.48f); + wInit.planars.iSize = { panelWidth, ds.y - iPaddingOffset.y * 2 }; wInit.planars.iPos = { ds.x - wInit.planars.iSize.x - iPaddingOffset.x, 0 + iPaddingOffset.y }; { - float leftX = wInit.trsEditor.iPos.x + wInit.trsEditor.iSize.x + iPaddingOffset.x; - float eachXSize = wInit.planars.iPos.x - (wInit.trsEditor.iPos.x + wInit.trsEditor.iSize.x) - 2*iPaddingOffset.x; - float eachYSize = (ds.y - 2 * iPaddingOffset.y - (wInit.renderWindows.size() - 1) * iPaddingOffset.y) / wInit.renderWindows.size(); + const float renderPaddingX = 0.0f; + const float renderPaddingY = 0.0f; + const float splitGap = 4.0f; + float leftX = renderPaddingX; + float eachXSize = std::max(0.0f, ds.x - 2.0f * renderPaddingX); + float eachYSize = (ds.y - 2.0f * renderPaddingY - (wInit.renderWindows.size() - 1) * splitGap) / wInit.renderWindows.size(); for (size_t i = 0; i < wInit.renderWindows.size(); ++i) { auto& rw = wInit.renderWindows[i]; - rw.iPos = { leftX, (1+i) * iPaddingOffset.y + i * eachYSize }; + rw.iPos = { leftX, renderPaddingY + i * (eachYSize + splitGap) }; rw.iSize = { eachXSize, eachYSize }; } } @@ -1731,6 +1860,61 @@ class UISampleApp final : public examples::SimpleWindowedApplication m_realFrameIx++; + const uint64_t renderedFrameIx = m_realFrameIx - 1u; + auto captureScreenshot = [&](const system::path& outPath, const char* tag) -> void + { + if (!m_device || !m_assetManager || !m_surface) + 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; + } + + if (!frame) + { + m_logger->log("%s screenshot failed: missing frame image.", 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_assetManager.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); + }; + // only present if there's successful content to show const ISmoothResizeSurface::SPresentInfo presentInfo = { { @@ -1748,56 +1932,19 @@ class UISampleApp final : public examples::SimpleWindowedApplication if (m_ciFrameCounter >= CiFramesBeforeCapture) { m_ciScreenshotDone = true; - if (!m_device || !m_assetManager || !m_surface) - return; - - m_logger->log("CI screenshot capture start (frame %u).", ILogger::ELL_INFO, m_ciFrameCounter); - 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("CI screenshot failed: wait for render finished.", ILogger::ELL_ERROR); - return; - } - - if (!frame) - { - m_logger->log("CI screenshot failed: missing frame image.", ILogger::ELL_ERROR); - 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("CI screenshot failed: could not create frame view.", ILogger::ELL_ERROR); - return; - } - - m_logger->log("CI screenshot capture: calling createScreenShot.", ILogger::ELL_INFO); - const bool ok = ext::ScreenShot::createScreenShot( - m_device.get(), - getGraphicsQueue(), - nullptr, - frameView.get(), - m_assetManager.get(), - m_ciScreenshotPath, - asset::IImage::LAYOUT::TRANSFER_SRC_OPTIMAL, - asset::ACCESS_FLAGS::COLOR_ATTACHMENT_WRITE_BIT); + captureScreenshot(m_ciScreenshotPath, "CI"); + } + } - if (ok) - m_logger->log("CI screenshot saved to \"%s\".", ILogger::ELL_INFO, m_ciScreenshotPath.string().c_str()); - else - m_logger->log("CI screenshot failed to save.", ILogger::ELL_ERROR); + if (m_scriptedInput.enabled && !m_scriptedInput.captureFrames.empty()) + { + while (m_scriptedInput.nextCaptureIndex < m_scriptedInput.captureFrames.size() && + m_scriptedInput.captureFrames[m_scriptedInput.nextCaptureIndex] == renderedFrameIx) + { + const auto outPath = m_scriptedInput.captureOutputDir / + (m_scriptedInput.capturePrefix + "_" + std::to_string(renderedFrameIx) + ".png"); + captureScreenshot(outPath, "Script"); + ++m_scriptedInput.nextCaptureIndex; } } @@ -1814,7 +1961,11 @@ class UISampleApp final : public examples::SimpleWindowedApplication std::exit(EXIT_FAILURE); } if (m_ciMode && m_ciScreenshotDone) + { + if (m_scriptedInput.enabled && m_scriptedInput.nextCaptureIndex < m_scriptedInput.captureFrames.size()) + return true; return false; + } if (m_surface->irrecoverable()) return false; @@ -1841,6 +1992,19 @@ class UISampleApp final : public examples::SimpleWindowedApplication }; m_nextPresentationTimestamp = updatePresentationTimestamp(); + if (m_haveLastPresentationTimestamp) + { + const auto delta = m_nextPresentationTimestamp - m_lastPresentationTimestamp; + if (delta.count() < 0) + m_frameDeltaSec = 0.0; + else + m_frameDeltaSec = static_cast(delta.count()) / 1000000.0; + } + m_lastPresentationTimestamp = m_nextPresentationTimestamp; + m_haveLastPresentationTimestamp = true; + + updatePlayback(m_frameDeltaSec); + const bool skipCameraInput = m_playback.playing && m_playback.overrideInput; struct { @@ -2026,6 +2190,24 @@ class UISampleApp final : public examples::SimpleWindowedApplication if (!scriptedKeyboard.empty()) capturedEvents.keyboard.insert(capturedEvents.keyboard.end(), scriptedKeyboard.begin(), scriptedKeyboard.end()); + m_uiInputEventsThisFrame = static_cast(capturedEvents.mouse.size() + capturedEvents.keyboard.size()); + + auto cameraKeyboardEvents = capturedEvents.keyboard; + auto cameraMouseEvents = capturedEvents.mouse; + for (auto& ev : cameraMouseEvents) + { + if (ev.type == ui::SMouseEvent::EET_SCROLL) + { + ev.scrollEvent.verticalScroll *= m_cameraControls.mouseScrollScale; + ev.scrollEvent.horizontalScroll *= m_cameraControls.mouseScrollScale; + } + else if (ev.type == ui::SMouseEvent::EET_MOVEMENT) + { + ev.movementEvent.relativeMovementX *= m_cameraControls.mouseMoveScale; + ev.movementEvent.relativeMovementY *= m_cameraControls.mouseMoveScale; + } + } + const auto cursorPosition = m_window->getCursorControl()->getPosition(); nbl::ext::imgui::UI::SUpdateParameters params = @@ -2045,54 +2227,101 @@ class UISampleApp final : public examples::SimpleWindowedApplication scriptedImguizmo.size()); } - if (enableActiveCameraMovement) + if (enableActiveCameraMovement && !skipCameraInput) { auto& binding = windowBindings[activeRenderWindowIx]; auto& planar = m_planarProjections[binding.activePlanarIx]; auto* camera = planar->getCamera(); - + assert(binding.boundProjectionIx.has_value()); auto& projection = planar->getPlanarProjections()[binding.boundProjectionIx.value()]; static std::vector virtualEvents(0x45); uint32_t vCount = {}; + uint32_t vKeyboardEventsCount = {}; + uint32_t vMouseEventsCount = {}; projection.beginInputProcessing(m_nextPresentationTimestamp); { - projection.process(nullptr, vCount); + projection.processKeyboard(nullptr, vKeyboardEventsCount, {}); + projection.processMouse(nullptr, vMouseEventsCount, {}); - if (virtualEvents.size() < vCount) - virtualEvents.resize(vCount); + const auto totalCount = vKeyboardEventsCount + vMouseEventsCount; + if (virtualEvents.size() < totalCount) + virtualEvents.resize(totalCount); - auto* orbit = dynamic_cast(camera); + auto* output = virtualEvents.data(); + projection.processKeyboard(output, vKeyboardEventsCount, { cameraKeyboardEvents.data(), cameraKeyboardEvents.size() }); + for (uint32_t i = 0u; i < vKeyboardEventsCount; ++i) + output[i].magnitude *= m_cameraControls.keyboardScale; + output += vKeyboardEventsCount; - if (orbit) + if (isOrbitLikeCamera(camera)) { - uint32_t vKeyboardEventsCount = {}, vMouseEventsCount = {}; - - projection.processKeyboard(nullptr, vKeyboardEventsCount, {}); - projection.processMouse(nullptr, vMouseEventsCount, {}); - - auto* output = virtualEvents.data(); - - projection.processKeyboard(output, vKeyboardEventsCount, params.keyboardEvents); - output += vKeyboardEventsCount; - if (ImGui::IsMouseDown(ImGuiMouseButton_Left)) - projection.processMouse(output, vMouseEventsCount, params.mouseEvents); + projection.processMouse(output, vMouseEventsCount, { cameraMouseEvents.data(), cameraMouseEvents.size() }); else vMouseEventsCount = 0; - - vCount = vKeyboardEventsCount + vMouseEventsCount; } else - projection.process(virtualEvents.data(), vCount, { params.keyboardEvents, params.mouseEvents }); + { + projection.processMouse(output, vMouseEventsCount, { cameraMouseEvents.data(), cameraMouseEvents.size() }); + } + + vCount = vKeyboardEventsCount + vMouseEventsCount; } projection.endInputProcessing(); if (vCount) { - camera->manipulate({ virtualEvents.data(), vCount }); + applyVirtualEventScaling(virtualEvents, vCount); + + const char* controllerLabel = "Keyboard/Mouse"; + auto applyEventsToCamera = [&](ICamera* target, uint32_t planarIx) + { + if (!target) + return; + + if (m_cameraControls.worldTranslate) + { + std::vector perCameraEvents(virtualEvents.begin(), virtualEvents.begin() + vCount); + uint32_t perCount = vCount; + remapTranslationToWorld(target, perCameraEvents, perCount); + if (perCount) + target->manipulate({ perCameraEvents.data(), perCount }); + } + else + { + target->manipulate({ virtualEvents.data(), vCount }); + } + + applyConstraintsToCamera(target); + appendVirtualEventLog("input", controllerLabel, planarIx, target, virtualEvents.data(), vCount); + }; + + if (m_cameraControls.mirrorInput) + { + std::unordered_set visited; + for (size_t bindingIx = 0u; bindingIx < windowBindings.size(); ++bindingIx) + { + auto& bindingIt = windowBindings[bindingIx]; + auto& planarIt = m_planarProjections[bindingIt.activePlanarIx]; + if (!planarIt) + continue; + auto* target = planarIt->getCamera(); + if (!target) + continue; + const auto id = target->getGimbal().getID(); + if (!visited.insert(id).second) + continue; + applyEventsToCamera(target, bindingIt.activePlanarIx); + } + } + else + { + applyEventsToCamera(camera, binding.activePlanarIx); + } + if (m_scriptedInput.log) { for (uint32_t i = 0u; i < vCount; ++i) @@ -2110,7 +2339,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication } } - if (m_scriptedInput.enabled && scriptedImguizmo.size()) + if (m_scriptedInput.enabled && scriptedImguizmo.size() && !skipCameraInput) { auto& binding = windowBindings[activeRenderWindowIx]; auto& planar = m_planarProjections[binding.activePlanarIx]; @@ -2132,6 +2361,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication if (vCount) { camera->manipulate({ imguizmoEvents.data(), vCount }); + appendVirtualEventLog("imguizmo", "ImGuizmo", binding.activePlanarIx, camera, imguizmoEvents.data(), vCount); if (m_scriptedInput.log) { @@ -2336,134 +2566,691 @@ class UISampleApp final : public examples::SimpleWindowedApplication } } + UpdateUiMetrics(); m_ui.manager->update(params); - } + } - private: - inline void imguiListen() + private: + struct CUILogFormatter final : public nbl::system::ILogger { - ImGuiIO& io = ImGui::GetIO(); - if (m_ciMode) + CUILogFormatter() : ILogger(ILogger::DefaultLogMask()) {} + + std::string format(E_LOG_LEVEL level, std::string_view fmt, ...) { - io.IniFilename = nullptr; - useWindow = true; + 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; } - - ImGuizmo::BeginFrame(); - { - if (!m_ciMode) - { - nbl::hlsl::ShowDebugWindow(); - ImGuizmo::ShowDebugImguizmoWindow(); - } - SImResourceInfo info; - info.samplerIx = (uint16_t)nbl::ext::imgui::UI::DefaultSamplerIx::USER; + protected: + void log_impl(const std::string_view&, E_LOG_LEVEL, va_list) override {} + }; - // ORBIT CAMERA TEST - { - for (auto& planar : m_planarProjections) - { - auto* camera = planar->getCamera(); + struct VirtualEventLogEntry + { + uint64_t frame = 0; + CVirtualGimbalEvent::VirtualEventType type = CVirtualGimbalEvent::None; + float64_t magnitude = 0.0; + std::string source; + std::string controller; + std::string camera; + uint32_t planarIx = 0u; + std::string line; + }; - auto* orbit = dynamic_cast(camera); + struct CameraPreset + { + std::string name; + std::string identifier; + float64_t3 position = float64_t3(0.0); + glm::quat orientation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f); + float distance = 0.f; + bool hasDistance = false; + double orbitU = 0.0; + double orbitV = 0.0; + float orbitDistance = 0.f; + bool hasOrbitState = false; + }; - if (orbit) - { - auto targetPostion = hlsl::transpose(getMatrix3x4As4x4(m_model))[3]; - orbit->target(targetPostion); - orbit->manipulate({}, {}); - } - } - } + struct CameraKeyframe + { + CameraPreset preset; + float time = 0.f; + }; - // render bound planar camera views onto GUI windows - if (useWindow) - { - // ABS TRS editor to manipulate bound object - TransformEditor(); + struct CameraPlaybackState + { + bool playing = false; + bool loop = true; + bool overrideInput = true; + float speed = 1.f; + float time = 0.f; + }; - if(enableActiveCameraMovement) - ImGuizmo::Enable(false); - else - ImGuizmo::Enable(true); + struct CameraControlSettings + { + bool mirrorInput = false; + bool worldTranslate = false; + float keyboardScale = 0.00625f; + float mouseMoveScale = 1.0f; + float mouseScrollScale = 1.0f; + float translationScale = 1.0f; + float rotationScale = 1.0f; + }; - size_t gizmoIx = {}; - size_t manipulationCounter = {}; - const std::optional modelInUseIx = ImGuizmo::IsUsingAny() ? std::optional(boundPlanarCameraIxToManipulate.has_value() ? 1u + boundPlanarCameraIxToManipulate.value() : 0u) : std::optional(std::nullopt); + struct CameraConstraintSettings + { + bool enabled = false; + bool clampPitch = false; + bool clampYaw = false; + bool clampRoll = false; + bool clampDistance = false; + float pitchMinDeg = -80.f; + float pitchMaxDeg = 80.f; + float yawMinDeg = -180.f; + float yawMaxDeg = 180.f; + float rollMinDeg = -180.f; + float rollMaxDeg = 180.f; + float minDistance = 0.1f; + float maxDistance = 1000.f; + }; - for (uint32_t windowIx = 0; windowIx < windowBindings.size(); ++windowIx) - { - // setup - { - const auto& rw = wInit.renderWindows[windowIx]; - const ImGuiCond windowCond = m_ciMode ? ImGuiCond_Always : ImGuiCond_Appearing; - ImGui::SetNextWindowPos({ rw.iPos.x, rw.iPos.y }, windowCond); - ImGui::SetNextWindowSize({ rw.iSize.x, rw.iSize.y }, windowCond); - } - ImGui::SetNextWindowSizeConstraints(ImVec2(0x45, 0x45), ImVec2(7680, 4320)); + inline ICamera* getActiveCamera() + { + auto& binding = windowBindings[activeRenderWindowIx]; + auto& planar = m_planarProjections[binding.activePlanarIx]; + return planar ? planar->getCamera() : nullptr; + } - ImGui::PushStyleColor(ImGuiCol_WindowBg, (ImVec4)ImColor(0.35f, 0.3f, 0.3f)); - const std::string ident = "Render Window \"" + std::to_string(windowIx) + "\""; + inline bool isOrbitLikeCamera(ICamera* camera) + { + return dynamic_cast(camera) || dynamic_cast(camera) || dynamic_cast(camera); + } - ImGui::Begin(ident.data(), 0); - const ImVec2 contentRegionSize = ImGui::GetContentRegionAvail(), windowPos = ImGui::GetWindowPos(), cursorPos = ImGui::GetCursorScreenPos(); + template + inline bool withOrbitLikeCamera(ICamera* camera, Fn&& fn) + { + if (auto* orbit = dynamic_cast(camera)) + { + fn(orbit); + return true; + } + if (auto* arcball = dynamic_cast(camera)) + { + fn(arcball); + return true; + } + if (auto* turntable = dynamic_cast(camera)) + { + fn(turntable); + return true; + } + return false; + } - ImGuiWindow* window = ImGui::GetCurrentWindow(); - { - const auto mPos = ImGui::GetMousePos(); + inline std::string_view getCameraTypeLabel(const ICamera* camera) const + { + if (dynamic_cast(camera)) + return "FPS"; + if (dynamic_cast(camera)) + return "Free"; + if (dynamic_cast(camera)) + return "Orbit"; + if (dynamic_cast(camera)) + return "Arcball"; + if (dynamic_cast(camera)) + return "Turntable"; + return "Unknown"; + } - if (mPos.x < cursorPos.x || mPos.y < cursorPos.y || mPos.x > cursorPos.x + contentRegionSize.x || mPos.y > cursorPos.y + contentRegionSize.y) - window->Flags = ImGuiWindowFlags_None; - else - window->Flags = ImGuiWindowFlags_NoMove; - } + inline std::string_view getCameraTypeDescription(const ICamera* camera) const + { + if (dynamic_cast(camera)) + return "First-person WASD + mouse look"; + if (dynamic_cast(camera)) + return "Free-fly 6DOF with full rotation"; + if (dynamic_cast(camera)) + return "Orbit around target with dolly"; + if (dynamic_cast(camera)) + return "Arcball trackball around target"; + if (dynamic_cast(camera)) + return "Turntable yaw/pitch around target"; + return "Unspecified camera behavior"; + } - // setup bound entities for the window like camera & projections - auto& binding = windowBindings[windowIx]; - auto& planarBound = m_planarProjections[binding.activePlanarIx]; - assert(planarBound); + inline CameraPreset capturePreset(ICamera* camera, const std::string& name) + { + CameraPreset preset; + preset.name = name; + if (!camera) + return preset; - binding.aspectRatio = contentRegionSize.x / contentRegionSize.y; - auto* planarViewCameraBound = planarBound->getCamera(); + preset.identifier = std::string(camera->getIdentifier()); + const auto& gimbal = camera->getGimbal(); + preset.position = gimbal.getPosition(); + preset.orientation = gimbal.getOrientation(); - assert(planarViewCameraBound); - assert(binding.boundProjectionIx.has_value()); - - auto& projection = planarBound->getPlanarProjections()[binding.boundProjectionIx.value()]; - projection.update(binding.leftHandedProjection, binding.aspectRatio); + auto captureOrbit = [&](auto* orbit) + { + preset.distance = orbit->getDistance(); + preset.hasDistance = true; + preset.orbitDistance = orbit->getDistance(); + preset.orbitU = orbit->getU(); + preset.orbitV = orbit->getV(); + preset.hasOrbitState = true; + }; - // TODO: - // would be nice to normalize imguizmo visual vectors (possible with styles) + withOrbitLikeCamera(camera, captureOrbit); - // first 0th texture is for UI texture atlas, then there are our window textures - auto fboImguiTextureID = windowIx + 1u; - info.textureID = fboImguiTextureID; + return preset; + } - if(binding.allowGizmoAxesToFlip) - ImGuizmo::AllowAxisFlip(true); - else - ImGuizmo::AllowAxisFlip(false); + inline bool applyPresetToCamera(ICamera* camera, const CameraPreset& preset) + { + if (!camera) + return false; - if(projection.getParameters().m_type == IPlanarProjection::CProjection::Orthographic) - ImGuizmo::SetOrthographic(true); - else - ImGuizmo::SetOrthographic(false); + CTargetPose target; + target.position = preset.position; + target.orientation = preset.orientation; + target.hasDistance = preset.hasDistance; + target.distance = preset.distance; + target.hasOrbitState = preset.hasOrbitState; + target.orbitU = preset.orbitU; + target.orbitV = preset.orbitV; + target.orbitDistance = preset.orbitDistance; + + return m_targetPoseController.apply(camera, target); + } - ImGuizmo::SetDrawlist(); - ImGui::Image(info, contentRegionSize); - ImGuizmo::SetRect(cursorPos.x, cursorPos.y, contentRegionSize.x, contentRegionSize.y); + inline void appendVirtualEventLog(std::string_view source, std::string_view controller, uint32_t planarIx, ICamera* camera, const CVirtualGimbalEvent* events, uint32_t count) + { + m_uiVirtualEventsThisFrame += count; + const std::string sourceStr(source); + const std::string controllerStr(controller); + 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 ctrl=%s cam=%s planar=%u event=%s mag=%.6f", + static_cast(m_realFrameIx), + sourceStr.c_str(), + controllerStr.c_str(), + cameraName.c_str(), + planarIx, + eventName, + events[i].magnitude); + m_virtualEventLog.push_back({ + m_realFrameIx, + events[i].type, + events[i].magnitude, + sourceStr, + controllerStr, + cameraName, + planarIx, + std::move(line) + }); + } - // I will assume we need to focus a window to start manipulating objects from it - if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) - { - if (!(m_scriptedInput.enabled && m_scriptedInput.exclusive)) - activeRenderWindowIx = windowIx; - } + while (m_virtualEventLog.size() > m_virtualEventLogMax) + m_virtualEventLog.pop_front(); + } - // we render a scene from view of a camera bound to planar window - ImGuizmoPlanarM16InOut imguizmoPlanar; + inline void applyConstraintsToCamera(ICamera* camera) + { + if (!m_cameraConstraints.enabled || !camera) + return; + + auto clampOrbitDistance = [&](auto* orbit) + { + if (m_cameraConstraints.clampDistance) + { + const float clamped = std::clamp(orbit->getDistance(), m_cameraConstraints.minDistance, m_cameraConstraints.maxDistance); + orbit->setDistance(clamped); + } + }; + + if (withOrbitLikeCamera(camera, clampOrbitDistance)) + return; + + if (!(m_cameraConstraints.clampPitch || m_cameraConstraints.clampYaw || m_cameraConstraints.clampRoll)) + return; + + const auto& gimbal = camera->getGimbal(); + const auto pos = gimbal.getPosition(); + const auto eulerDeg = glm::degrees(glm::eulerAngles(gimbal.getOrientation())); + + auto clamped = eulerDeg; + if (m_cameraConstraints.clampPitch) + clamped.x = std::clamp(clamped.x, m_cameraConstraints.pitchMinDeg, m_cameraConstraints.pitchMaxDeg); + if (m_cameraConstraints.clampYaw) + clamped.y = std::clamp(clamped.y, m_cameraConstraints.yawMinDeg, m_cameraConstraints.yawMaxDeg); + if (m_cameraConstraints.clampRoll) + clamped.z = std::clamp(clamped.z, m_cameraConstraints.rollMinDeg, m_cameraConstraints.rollMaxDeg); + + if (clamped.x == eulerDeg.x && clamped.y == eulerDeg.y && clamped.z == eulerDeg.z) + return; + + CameraPreset preset; + preset.position = pos; + preset.orientation = glm::quat(glm::radians(clamped)); + applyPresetToCamera(camera, preset); + } + + inline void applyVirtualEventScaling(std::vector& events, uint32_t count) + { + for (uint32_t i = 0u; i < count; ++i) + { + auto& ev = events[i]; + const auto type = ev.type; + + if (type == CVirtualGimbalEvent::MoveForward || type == CVirtualGimbalEvent::MoveBackward || + type == CVirtualGimbalEvent::MoveLeft || type == CVirtualGimbalEvent::MoveRight || + type == CVirtualGimbalEvent::MoveUp || type == CVirtualGimbalEvent::MoveDown) + { + ev.magnitude *= m_cameraControls.translationScale; + } + else if (type == CVirtualGimbalEvent::TiltUp || type == CVirtualGimbalEvent::TiltDown || + type == CVirtualGimbalEvent::PanLeft || type == CVirtualGimbalEvent::PanRight || + type == CVirtualGimbalEvent::RollLeft || type == CVirtualGimbalEvent::RollRight) + { + ev.magnitude *= m_cameraControls.rotationScale; + } + } + } + + inline void remapTranslationToWorld(ICamera* camera, std::vector& events, uint32_t& count) + { + if (!camera) + return; + + float64_t3 worldDelta = float64_t3(0.0); + std::vector filtered; + filtered.reserve(events.size()); + + for (uint32_t i = 0u; i < count; ++i) + { + const auto& ev = events[i]; + switch (ev.type) + { + case CVirtualGimbalEvent::MoveRight: worldDelta.x += ev.magnitude; break; + case CVirtualGimbalEvent::MoveLeft: worldDelta.x -= ev.magnitude; break; + case CVirtualGimbalEvent::MoveUp: worldDelta.y += ev.magnitude; break; + case CVirtualGimbalEvent::MoveDown: worldDelta.y -= ev.magnitude; break; + case CVirtualGimbalEvent::MoveForward: worldDelta.z += ev.magnitude; break; + case CVirtualGimbalEvent::MoveBackward: worldDelta.z -= ev.magnitude; break; + default: + filtered.emplace_back(ev); + break; + } + } + + if (worldDelta.x == 0.0 && worldDelta.y == 0.0 && worldDelta.z == 0.0) + { + events = std::move(filtered); + count = static_cast(events.size()); + return; + } + + const auto& gimbal = camera->getGimbal(); + const auto right = gimbal.getXAxis(); + const auto up = gimbal.getYAxis(); + const auto forward = gimbal.getZAxis(); + + const float64_t3 localDelta = float64_t3( + glm::dot(worldDelta, right), + glm::dot(worldDelta, up), + glm::dot(worldDelta, forward) + ); + + auto emitAxis = [&](double v, CVirtualGimbalEvent::VirtualEventType pos, CVirtualGimbalEvent::VirtualEventType neg) + { + if (v == 0.0) + return; + auto& ev = filtered.emplace_back(); + ev.type = (v > 0.0) ? pos : neg; + ev.magnitude = std::abs(v); + }; + + emitAxis(localDelta.x, CVirtualGimbalEvent::MoveRight, CVirtualGimbalEvent::MoveLeft); + emitAxis(localDelta.y, CVirtualGimbalEvent::MoveUp, CVirtualGimbalEvent::MoveDown); + emitAxis(localDelta.z, CVirtualGimbalEvent::MoveForward, CVirtualGimbalEvent::MoveBackward); + + events = std::move(filtered); + count = static_cast(events.size()); + } + + inline void applyPresetToTargets(const CameraPreset& preset) + { + if (!m_playbackAffectsAll) + { + applyPresetToCamera(getActiveCamera(), preset); + return; + } + + std::unordered_set visited; + for (auto& binding : windowBindings) + { + auto& planar = m_planarProjections[binding.activePlanarIx]; + if (!planar) + continue; + auto* camera = planar->getCamera(); + if (!camera) + continue; + const auto id = camera->getGimbal().getID(); + if (visited.insert(id).second) + applyPresetToCamera(camera, preset); + } + } + + inline void updatePlayback(double dtSec) + { + if (!m_playback.playing || m_keyframes.empty()) + return; + + m_playback.time += static_cast(dtSec * m_playback.speed); + + const float duration = m_keyframes.back().time; + if (duration <= 0.f) + { + applyPresetToTargets(m_keyframes.back().preset); + return; + } + + if (m_playback.loop) + { + while (m_playback.time > duration) + m_playback.time -= duration; + } + else if (m_playback.time > duration) + { + m_playback.time = duration; + m_playback.playing = false; + } + + const auto time = m_playback.time; + if (m_keyframes.size() == 1) + { + applyPresetToTargets(m_keyframes.front().preset); + return; + } + + size_t idx = 0u; + while (idx + 1u < m_keyframes.size() && m_keyframes[idx + 1u].time < time) + ++idx; + + const auto& a = m_keyframes[idx]; + const auto& b = m_keyframes[std::min(idx + 1u, m_keyframes.size() - 1u)]; + + if (b.time <= a.time) + { + applyPresetToTargets(a.preset); + return; + } + + const double alpha = static_cast(time - a.time) / static_cast(b.time - a.time); + + CameraPreset blended; + blended.position = a.preset.position + (b.preset.position - a.preset.position) * alpha; + blended.orientation = glm::slerp(a.preset.orientation, b.preset.orientation, static_cast(alpha)); + blended.hasDistance = a.preset.hasDistance || b.preset.hasDistance; + if (blended.hasDistance) + { + const float da = a.preset.hasDistance ? a.preset.distance : b.preset.distance; + const float db = b.preset.hasDistance ? b.preset.distance : a.preset.distance; + blended.distance = da + (db - da) * static_cast(alpha); + } + blended.hasOrbitState = a.preset.hasOrbitState || b.preset.hasOrbitState; + if (blended.hasOrbitState) + { + const double ua = a.preset.hasOrbitState ? a.preset.orbitU : b.preset.orbitU; + const double ub = b.preset.hasOrbitState ? b.preset.orbitU : a.preset.orbitU; + const double va = a.preset.hasOrbitState ? a.preset.orbitV : b.preset.orbitV; + const double vb = b.preset.hasOrbitState ? b.preset.orbitV : a.preset.orbitV; + const float da = a.preset.hasOrbitState ? a.preset.orbitDistance : b.preset.orbitDistance; + const float db = b.preset.hasOrbitState ? b.preset.orbitDistance : a.preset.orbitDistance; + + blended.orbitU = ua + (ub - ua) * alpha; + blended.orbitV = va + (vb - va) * alpha; + blended.orbitDistance = da + (db - da) * static_cast(alpha); + } + + applyPresetToTargets(blended); + } + + inline bool savePresetsToFile(const system::path& path) + { + json root; + root["presets"] = json::array(); + + for (const auto& preset : m_presets) + { + json j; + j["name"] = preset.name; + j["identifier"] = preset.identifier; + j["position"] = { preset.position.x, preset.position.y, preset.position.z }; + j["orientation"] = { preset.orientation.x, preset.orientation.y, preset.orientation.z, preset.orientation.w }; + if (preset.hasDistance) + j["distance"] = preset.distance; + if (preset.hasOrbitState) + { + j["orbit_u"] = preset.orbitU; + j["orbit_v"] = preset.orbitV; + j["orbit_distance"] = preset.orbitDistance; + } + root["presets"].push_back(std::move(j)); + } + + std::ofstream out(path.string(), std::ios::binary); + if (!out) + return false; + out << root.dump(2); + return true; + } + + inline bool loadPresetsFromFile(const system::path& path) + { + std::ifstream in(path.string(), std::ios::binary); + if (!in) + return false; + + json root; + in >> root; + if (!root.contains("presets")) + return false; + + m_presets.clear(); + for (const auto& entry : root["presets"]) + { + CameraPreset preset; + if (entry.contains("name")) + preset.name = entry["name"].get(); + if (entry.contains("identifier")) + preset.identifier = entry["identifier"].get(); + if (entry.contains("position") && entry["position"].is_array()) + { + auto arr = entry["position"]; + preset.position = float64_t3(arr[0].get(), arr[1].get(), arr[2].get()); + } + if (entry.contains("orientation") && entry["orientation"].is_array()) + { + auto arr = entry["orientation"]; + preset.orientation = glm::quat( + arr[3].get(), + arr[0].get(), + arr[1].get(), + arr[2].get() + ); + } + if (entry.contains("distance")) + { + preset.distance = entry["distance"].get(); + preset.hasDistance = true; + } + if (entry.contains("orbit_u")) + { + preset.orbitU = entry["orbit_u"].get(); + preset.hasOrbitState = true; + } + if (entry.contains("orbit_v")) + { + preset.orbitV = entry["orbit_v"].get(); + preset.hasOrbitState = true; + } + if (entry.contains("orbit_distance")) + { + preset.orbitDistance = entry["orbit_distance"].get(); + preset.hasOrbitState = true; + } + m_presets.emplace_back(std::move(preset)); + } + + return true; + } + + inline void imguiListen() + { + ImGuiIO& io = ImGui::GetIO(); + if (m_ciMode) + { + io.IniFilename = nullptr; + useWindow = true; + } + + ImGuizmo::BeginFrame(); + { + if (!m_ciMode) + { + } + + SImResourceInfo info; + info.samplerIx = (uint16_t)nbl::ext::imgui::UI::DefaultSamplerIx::USER; + + // ORBIT CAMERA TEST + { + for (auto& planar : m_planarProjections) + { + auto* camera = planar->getCamera(); + withOrbitLikeCamera(camera, [&](auto* orbit) + { + auto targetPostion = hlsl::transpose(getMatrix3x4As4x4(m_model))[3]; + orbit->target(targetPostion); + orbit->manipulate({}, {}); + }); + } + } + + // render bound planar camera views onto GUI windows + if (useWindow) + { + if(enableActiveCameraMovement) + ImGuizmo::Enable(false); + else + ImGuizmo::Enable(true); + + size_t gizmoIx = {}; + size_t manipulationCounter = {}; + const std::optional modelInUseIx = ImGuizmo::IsUsingAny() ? std::optional(boundPlanarCameraIxToManipulate.has_value() ? 1u + boundPlanarCameraIxToManipulate.value() : 0u) : std::optional(std::nullopt); + + for (uint32_t windowIx = 0; windowIx < windowBindings.size(); ++windowIx) + { + // setup + { + const auto& rw = wInit.renderWindows[windowIx]; + const ImGuiCond windowCond = m_ciMode ? ImGuiCond_Always : ImGuiCond_Appearing; + ImGui::SetNextWindowPos({ rw.iPos.x, rw.iPos.y }, windowCond); + ImGui::SetNextWindowSize({ rw.iSize.x, rw.iSize.y }, windowCond); + } + ImGui::SetNextWindowSizeConstraints(ImVec2(0x45, 0x45), ImVec2(7680, 4320)); + + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f)); + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.0f, 0.0f, 0.0f, 0.0f)); + const std::string ident = "Render Window \"" + std::to_string(windowIx) + "\""; + + ImGui::Begin(ident.data(), 0, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoBringToFrontOnFocus); + const ImVec2 contentRegionSize = ImGui::GetContentRegionAvail(), windowPos = ImGui::GetWindowPos(), cursorPos = ImGui::GetCursorScreenPos(); + + ImGuiWindow* window = ImGui::GetCurrentWindow(); + { + const auto mPos = ImGui::GetMousePos(); + + if (mPos.x < cursorPos.x || mPos.y < cursorPos.y || mPos.x > cursorPos.x + contentRegionSize.x || mPos.y > cursorPos.y + contentRegionSize.y) + window->Flags &= ~ImGuiWindowFlags_NoMove; + else + window->Flags |= ImGuiWindowFlags_NoMove; + } + + // setup bound entities for the window like camera & projections + auto& binding = windowBindings[windowIx]; + auto& planarBound = m_planarProjections[binding.activePlanarIx]; + assert(planarBound); + + binding.aspectRatio = contentRegionSize.x / contentRegionSize.y; + auto* planarViewCameraBound = planarBound->getCamera(); + + assert(planarViewCameraBound); + assert(binding.boundProjectionIx.has_value()); + + auto& projection = planarBound->getPlanarProjections()[binding.boundProjectionIx.value()]; + projection.update(binding.leftHandedProjection, binding.aspectRatio); + + // TODO: + // would be nice to normalize imguizmo visual vectors (possible with styles) + + // first 0th texture is for UI texture atlas, then there are our window textures + auto fboImguiTextureID = windowIx + 1u; + info.textureID = fboImguiTextureID; + + if(binding.allowGizmoAxesToFlip) + ImGuizmo::AllowAxisFlip(true); + else + ImGuizmo::AllowAxisFlip(false); + + if(projection.getParameters().m_type == IPlanarProjection::CProjection::Orthographic) + ImGuizmo::SetOrthographic(true); + else + ImGuizmo::SetOrthographic(false); + + ImGuizmo::SetDrawlist(); + ImGui::Image(info, contentRegionSize); + ImGuizmo::SetRect(cursorPos.x, cursorPos.y, contentRegionSize.x, contentRegionSize.y); + { + const char* projLabel = projection.getParameters().m_type == IPlanarProjection::CProjection::Perspective ? "Persp" : "Ortho"; + const std::string overlayText = "Planar " + std::to_string(binding.activePlanarIx) + " | " + projLabel + " | W" + std::to_string(windowIx); + const std::string cameraText = std::string(getCameraTypeLabel(planarViewCameraBound)) + ": " + std::string(getCameraTypeDescription(planarViewCameraBound)); + const ImVec2 textSize = ImGui::CalcTextSize(overlayText.c_str()); + const ImVec2 descSize = ImGui::CalcTextSize(cameraText.c_str()); + const ImVec2 pad = ImVec2(6.0f, 4.0f); + const float lineGap = 2.0f; + const float width = std::max(textSize.x, descSize.x); + const float height = textSize.y + descSize.y + lineGap + pad.y * 2.0f; + ImVec2 overlayPos = ImVec2(cursorPos.x + contentRegionSize.x - width - pad.x * 2.0f - 6.0f, cursorPos.y + 6.0f); + overlayPos.x = std::max(overlayPos.x, cursorPos.x + 6.0f); + ImVec2 overlayMax = ImVec2(overlayPos.x + width + pad.x * 2.0f, overlayPos.y + height); + auto* drawList = ImGui::GetWindowDrawList(); + drawList->AddRectFilled(overlayPos, overlayMax, ImGui::ColorConvertFloat4ToU32(ImVec4(0.05f, 0.06f, 0.08f, 0.80f)), 6.0f); + drawList->AddRect(overlayPos, overlayMax, ImGui::ColorConvertFloat4ToU32(ImVec4(0.60f, 0.66f, 0.76f, 0.80f)), 6.0f); + drawList->AddText(ImVec2(overlayPos.x + pad.x, overlayPos.y + pad.y), ImGui::ColorConvertFloat4ToU32(ImVec4(0.96f, 0.98f, 1.0f, 1.0f)), overlayText.c_str()); + drawList->AddText(ImVec2(overlayPos.x + pad.x, overlayPos.y + pad.y + textSize.y + lineGap), ImGui::ColorConvertFloat4ToU32(ImVec4(0.78f, 0.82f, 0.90f, 1.0f)), cameraText.c_str()); + } + + // I will assume we need to focus a window to start manipulating objects from it + if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) + { + if (!(m_scriptedInput.enabled && m_scriptedInput.exclusive)) + activeRenderWindowIx = windowIx; + } + + // we render a scene from view of a camera bound to planar window + ImGuizmoPlanarM16InOut imguizmoPlanar; imguizmoPlanar.view = getCastedMatrix(hlsl::transpose(getMatrix3x4As4x4(planarViewCameraBound->getGimbal().getViewMatrix()))); imguizmoPlanar.projection = getCastedMatrix(hlsl::transpose(projection.getProjectionMatrix())); @@ -2507,6 +3294,9 @@ class UISampleApp final : public examples::SimpleWindowedApplication else imguizmoModel.inTRS = hlsl::transpose(getMatrix3x4As4x4(m_model)); + float gizmoSizeClip = 0.1f; + ImGuizmo::SetGizmoSizeClipSpace(gizmoSizeClip); + imguizmoModel.outTRS = imguizmoModel.inTRS; { const bool success = ImGuizmo::Manipulate(&imguizmoPlanar.view[0][0], &imguizmoPlanar.projection[0][0], ImGuizmo::OPERATION::UNIVERSAL, mCurrentGizmoMode, &imguizmoModel.outTRS[0][0], &imguizmoModel.outDeltaTRS[0][0], useSnap ? &snap[0] : nullptr); @@ -2643,6 +3433,18 @@ class UISampleApp final : public examples::SimpleWindowedApplication if (ImGuizmo::IsOver() and not ImGuizmo::IsUsingAny() && not enableActiveCameraMovement) { + if (targetGimbalManipulationCamera && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) + { + const uint32_t newPlanarIx = modelIx - 1u; + if (newPlanarIx < m_planarProjections.size()) + { + binding.activePlanarIx = newPlanarIx; + binding.pickDefaultProjections(m_planarProjections[binding.activePlanarIx]->getPlanarProjections()); + if (!(m_scriptedInput.enabled && m_scriptedInput.exclusive)) + activeRenderWindowIx = windowIx; + } + } + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.2f, 0.2f, 0.2f, 0.8f)); ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.5f); @@ -2665,6 +3467,13 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGui::Text("Identifier: %s", ident.c_str()); ImGui::Text("Object Ix: %u", modelIx); + if (targetGimbalManipulationCamera) + { + ImGui::Separator(); + ImGui::TextDisabled("RMB: switch view to this camera"); + ImGui::TextDisabled("LMB drag: manipulate gizmo"); + ImGui::TextDisabled("SPACE: toggle move mode"); + } ImGui::End(); @@ -2677,6 +3486,22 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGui::End(); ImGui::PopStyleColor(1); + ImGui::PopStyleVar(3); + } + if (windowBindings.size() > 1u) + { + const auto& topRw = wInit.renderWindows[0]; + const float splitY = topRw.iPos.y + topRw.iSize.y; + const float gap = std::max(0.0f, wInit.renderWindows[1].iPos.y - splitY); + ImGui::SetNextWindowPos(ImVec2(0.0f, 0.0f), ImGuiCond_Always); + ImGui::SetNextWindowSize(io.DisplaySize, ImGuiCond_Always); + ImGui::Begin("SplitOverlay", nullptr, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoBringToFrontOnFocus); + auto* drawList = ImGui::GetWindowDrawList(); + if (gap >= 2.0f) + drawList->AddRectFilled(ImVec2(0.0f, splitY), ImVec2(io.DisplaySize.x, splitY + gap), ImGui::ColorConvertFloat4ToU32(ImVec4(0.05f, 0.06f, 0.08f, 0.85f))); + else + drawList->AddLine(ImVec2(0.0f, splitY), ImVec2(io.DisplaySize.x, splitY), ImGui::ColorConvertFloat4ToU32(ImVec4(0.80f, 0.84f, 0.92f, 0.75f)), 2.0f); + ImGui::End(); } assert(manipulationCounter <= 1u); } @@ -2713,6 +3538,10 @@ class UISampleApp final : public examples::SimpleWindowedApplication } } + DrawControlPanel(); + UpdateBoundCameraMovement(); + UpdateCursorVisibility(); + // update camera matrices for scene rendering { for (uint32_t i = 0u; i < windowBindings.size(); ++i) @@ -2735,352 +3564,1072 @@ class UISampleApp final : public examples::SimpleWindowedApplication } } - // Planars - { - // setup - { - const ImGuiCond windowCond = m_ciMode ? ImGuiCond_Always : ImGuiCond_Appearing; - ImGui::SetNextWindowPos({ wInit.planars.iPos.x, wInit.planars.iPos.y }, windowCond); - ImGui::SetNextWindowSize({ wInit.planars.iSize.x, wInit.planars.iSize.y }, windowCond); - } - ImGui::Begin("Planar projection"); - ImGui::Checkbox("Window mode##useWindow", &useWindow); - ImGui::Separator(); + } + + inline void UpdateBoundCameraMovement() + { + ImGuiIO& io = ImGui::GetIO(); + + if (ImGui::IsKeyPressed(ImGuiKey_Space)) + enableActiveCameraMovement = !enableActiveCameraMovement; - auto& active = windowBindings[activeRenderWindowIx]; - const auto activeRenderWindowIxString = std::to_string(activeRenderWindowIx); + if (enableActiveCameraMovement) + { + io.ConfigFlags |= ImGuiConfigFlags_NoMouse; + io.MouseDrawCursor = false; + io.WantCaptureMouse = false; - ImGui::Text("Active Render Window: %s", activeRenderWindowIxString.c_str()); + ImVec2 viewportSize = io.DisplaySize; + auto* cc = m_window->getCursorControl(); + int32_t posX = m_window->getX(); + int32_t posY = m_window->getY(); + + if (resetCursorToCenter) { - const size_t planarsCount = m_planarProjections.size(); - assert(planarsCount); + const ICursorControl::SPosition middle{ static_cast(viewportSize.x / 2 + posX), static_cast(viewportSize.y / 2 + posY) }; + cc->setPosition(middle); + } + else + { + auto currentCursorPos = cc->getPosition(); + ICursorControl::SPosition newPos{}; + newPos.x = std::clamp(currentCursorPos.x, posX, viewportSize.x + posX); + newPos.y = std::clamp(currentCursorPos.y, posY, viewportSize.y + posY); + cc->setPosition(newPos); + } + } + else + { + io.ConfigFlags &= ~ImGuiConfigFlags_NoMouse; + io.MouseDrawCursor = true; + io.WantCaptureMouse = true; + } + } + + inline void UpdateCursorVisibility() + { + ImGuiIO& io = ImGui::GetIO(); + ImVec2 mousePos = ImGui::GetMousePos(); + ImVec2 viewportSize = io.DisplaySize; + auto* cc = m_window->getCursorControl(); + + if (mousePos.x < 0.0f || mousePos.y < 0.0f || mousePos.x > viewportSize.x || mousePos.y > viewportSize.y) + { + if (not enableActiveCameraMovement) + cc->setVisible(true); + } + else + { + cc->setVisible(false); + } + } + + inline void UpdateUiMetrics() + { + m_uiLastFrameMs = static_cast(m_frameDeltaSec * 1000.0); + m_uiLastInputEvents = m_uiInputEventsThisFrame; + m_uiLastVirtualEvents = m_uiVirtualEventsThisFrame; + + m_uiFrameMs[m_uiMetricIndex] = m_uiLastFrameMs; + m_uiInputCounts[m_uiMetricIndex] = static_cast(m_uiInputEventsThisFrame); + m_uiVirtualCounts[m_uiMetricIndex] = static_cast(m_uiVirtualEventsThisFrame); + + m_uiMetricIndex = (m_uiMetricIndex + 1u) % UiMetricSamples; + m_uiInputEventsThisFrame = 0u; + m_uiVirtualEventsThisFrame = 0u; + } + + inline void DrawBadge(const char* label, const ImVec4& bg, const ImVec4& fg) + { + 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(6.0f, 2.0f)); + ImGui::Button(label); + ImGui::PopStyleVar(); + ImGui::PopStyleColor(4); + } + + inline void DrawKeyHint(const char* label, const ImVec4& bg, const ImVec4& fg) + { + 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(4.0f, 1.0f)); + ImGui::SmallButton(label); + ImGui::PopStyleVar(); + ImGui::PopStyleColor(4); + } + + inline void DrawHoverHint(const char* text) + { + if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayShort)) + { + ImGui::BeginTooltip(); + ImGui::TextUnformatted(text); + ImGui::EndTooltip(); + } + } + + inline void DrawDot(const ImVec4& color) + { + ImVec2 p = ImGui::GetCursorScreenPos(); + const float radius = 3.5f; + ImGui::GetWindowDrawList()->AddCircleFilled(ImVec2(p.x + radius, p.y + radius + 1.0f), radius, ImGui::ColorConvertFloat4ToU32(color)); + ImGui::Dummy(ImVec2(radius * 2.0f + 2.0f, radius * 2.0f)); + ImGui::SameLine(0, 6.0f); + } + + inline void DrawSectionHeader(const char* id, const char* label, const ImVec4& accent) + { + ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 4.0f); + ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.14f, 0.18f, 0.22f, 0.52f)); + if (ImGui::BeginChild(id, ImVec2(0, 20), true, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse)) + { + ImVec2 p = ImGui::GetWindowPos(); + ImVec2 s = ImGui::GetWindowSize(); + ImGui::GetWindowDrawList()->AddRectFilled(p, ImVec2(p.x + 2.0f, p.y + s.y), ImGui::ColorConvertFloat4ToU32(accent), 4.0f); + ImGui::SetCursorPosX(8.0f); + ImGui::AlignTextToFramePadding(); + ImGui::TextColored(accent, "%s", label); + } + ImGui::EndChild(); + ImGui::PopStyleColor(); + ImGui::PopStyleVar(); + ImGui::Spacing(); + } + + inline float CalcCardHeight(int rows) const + { + return ImGui::GetFrameHeightWithSpacing() * (static_cast(rows) + 1.0f) + 10.0f; + } + + inline bool BeginCard(const char* id, float height, const ImVec4& top, const ImVec4& bottom, const ImVec4& border) + { + ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 6.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(10.0f, 8.0f)); + ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0, 0, 0, 0)); + const bool open = ImGui::BeginChild(id, ImVec2(0, height), true, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse); + ImVec2 p = ImGui::GetWindowPos(); + ImVec2 s = ImGui::GetWindowSize(); + const ImU32 colTop = ImGui::ColorConvertFloat4ToU32(top); + const ImU32 colBottom = ImGui::ColorConvertFloat4ToU32(bottom); + ImGui::GetWindowDrawList()->AddRectFilledMultiColor( + p, + ImVec2(p.x + s.x, p.y + s.y), + colTop, + colTop, + colBottom, + colBottom + ); + ImGui::GetWindowDrawList()->AddRect(p, ImVec2(p.x + s.x, p.y + s.y), ImGui::ColorConvertFloat4ToU32(border), 6.0f); + return open; + } + + inline void EndCard() + { + ImGui::EndChild(); + ImGui::PopStyleColor(); + ImGui::PopStyleVar(2); + } + - std::vector sbels(planarsCount); - for (size_t i = 0; i < planarsCount; ++i) - sbels[i] = "Planar " + std::to_string(i); + inline void DrawControlPanel() + { + const ImVec2 displaySize = ImGui::GetIO().DisplaySize; + const float panelWidth = std::clamp(displaySize.x * 0.19f, 200.0f, displaySize.x * 0.25f); + const float panelHeight = std::clamp(displaySize.y * 0.34f, 200.0f, displaySize.y * 0.50f); + const ImVec2 panelSize = { panelWidth, panelHeight }; + const ImVec2 panelPos = { 0.0f, 0.0f }; + ImGui::SetNextWindowPos(panelPos, ImGuiCond_Always); + ImGui::SetNextWindowSize(panelSize, ImGuiCond_Always); + + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(5.0f, 4.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(4.0f, 1.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(3.0f, 2.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f); + ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f); + ImGui::PushStyleVar(ImGuiStyleVar_TabRounding, 3.0f); + ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarRounding, 4.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f); + ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2(3.0f, 2.0f)); + + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.05f, 0.06f, 0.08f, 0.0f)); + ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.10f, 0.12f, 0.16f, 0.44f)); + ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.64f, 0.72f, 0.84f, 0.55f)); + ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.16f, 0.19f, 0.24f, 0.54f)); + ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, ImVec4(0.26f, 0.32f, 0.40f, 0.64f)); + ImGui::PushStyleColor(ImGuiCol_FrameBgActive, ImVec4(0.30f, 0.36f, 0.45f, 0.70f)); + ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.14f, 0.18f, 0.24f, 0.60f)); + ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(0.24f, 0.30f, 0.40f, 0.70f)); + ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(0.28f, 0.36f, 0.46f, 0.78f)); + ImGui::PushStyleColor(ImGuiCol_Tab, ImVec4(0.14f, 0.18f, 0.24f, 0.60f)); + ImGui::PushStyleColor(ImGuiCol_TabHovered, ImVec4(0.24f, 0.30f, 0.40f, 0.70f)); + ImGui::PushStyleColor(ImGuiCol_TabActive, ImVec4(0.20f, 0.26f, 0.36f, 0.78f)); + ImGui::PushStyleColor(ImGuiCol_TableRowBg, ImVec4(0.12f, 0.14f, 0.18f, 0.50f)); + ImGui::PushStyleColor(ImGuiCol_TableRowBgAlt, ImVec4(0.16f, 0.18f, 0.22f, 0.50f)); + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.98f, 0.99f, 1.0f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_TextDisabled, ImVec4(0.82f, 0.86f, 0.90f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_Separator, ImVec4(0.54f, 0.60f, 0.70f, 0.80f)); + ImGui::PushStyleColor(ImGuiCol_SeparatorHovered, ImVec4(0.68f, 0.76f, 0.88f, 0.90f)); + ImGui::PushStyleColor(ImGuiCol_SeparatorActive, ImVec4(0.82f, 0.90f, 1.0f, 0.96f)); + + ImGui::SetNextWindowCollapsed(false, ImGuiCond_Always); + ImGui::SetNextWindowBgAlpha(0.0f); + if (m_ciMode) + ImGui::SetNextWindowFocus(); + ImGui::Begin("Control Panel", nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar); + + const ImVec4 accent = ImVec4(0.60f, 0.82f, 1.0f, 1.0f); + const ImVec4 good = ImVec4(0.45f, 0.90f, 0.60f, 1.0f); + const ImVec4 bad = ImVec4(1.0f, 0.50f, 0.45f, 1.0f); + const ImVec4 warn = ImVec4(0.95f, 0.80f, 0.45f, 1.0f); + const ImVec4 muted = ImVec4(0.92f, 0.93f, 0.95f, 1.0f); + const ImVec4 badgeText = ImVec4(0.10f, 0.11f, 0.13f, 1.0f); + const ImVec4 keyBg = ImVec4(0.20f, 0.22f, 0.25f, 1.0f); + const ImVec4 keyFg = ImVec4(0.92f, 0.94f, 0.96f, 1.0f); + const ImGuiTableFlags tableFlags = ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_RowBg | ImGuiTableFlags_PadOuterX; + const ImVec4 panelBg = ImVec4(0.03f, 0.04f, 0.05f, 0.50f); + const ImVec4 panelEdge = ImVec4(0.62f, 0.70f, 0.84f, 0.60f); + const ImVec4 panelStripe = ImVec4(0.28f, 0.56f, 0.90f, 0.70f); + const ImVec4 panelShadow = ImVec4(0.0f, 0.0f, 0.0f, 0.12f); - std::vector labels(planarsCount); - for (size_t i = 0; i < planarsCount; ++i) - labels[i] = sbels[i].c_str(); + { + const ImVec2 panelPos = ImGui::GetWindowPos(); + const ImVec2 panelSize = ImGui::GetWindowSize(); + auto* drawList = ImGui::GetWindowDrawList(); + drawList->AddRectFilled(ImVec2(panelPos.x + 2.0f, panelPos.y + 3.0f), ImVec2(panelPos.x + panelSize.x + 4.0f, panelPos.y + panelSize.y + 5.0f), ImGui::ColorConvertFloat4ToU32(panelShadow), 8.0f); + drawList->AddRectFilled(panelPos, ImVec2(panelPos.x + panelSize.x, panelPos.y + panelSize.y), ImGui::ColorConvertFloat4ToU32(panelBg), 6.0f); + drawList->AddRect(panelPos, ImVec2(panelPos.x + panelSize.x, panelPos.y + panelSize.y), ImGui::ColorConvertFloat4ToU32(panelEdge), 6.0f); + drawList->AddRectFilled(panelPos, ImVec2(panelPos.x + 4.0f, panelPos.y + panelSize.y), ImGui::ColorConvertFloat4ToU32(panelStripe), 6.0f); + } - int currentPlanarIx = static_cast(active.activePlanarIx); - if (ImGui::Combo("Active Planar", ¤tPlanarIx, labels.data(), static_cast(labels.size()))) + auto row = [&](const char* label, auto&& drawValue) + { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::TextUnformatted(label); + ImGui::TableSetColumnIndex(1); + drawValue(); + }; + + auto metricMax = [&](const std::array& values, float minValue) -> float + { + float maxValue = minValue; + for (const float v : values) + maxValue = std::max(maxValue, v); + return maxValue; + }; + + auto miniStat = [&](const char* id, const char* label, const ImVec4& color, const std::array& series, float minValue, auto&& drawValue) + { + ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.14f, 0.16f, 0.19f, 0.75f)); + ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 6.0f); + if (ImGui::BeginChild(id, ImVec2(0, 56), true, ImGuiWindowFlags_NoScrollbar)) + { + ImGui::TextDisabled("%s", label); + ImGui::SetWindowFontScale(1.05f); + drawValue(); + ImGui::SetWindowFontScale(1.0f); + ImGui::PushStyleColor(ImGuiCol_PlotLines, color); + const float maxValue = metricMax(series, minValue); + ImGui::PlotLines("##plot", series.data(), static_cast(UiMetricSamples), static_cast(m_uiMetricIndex), nullptr, 0.0f, maxValue, ImVec2(0, 24)); + ImGui::PopStyleColor(); + } + ImGui::EndChild(); + ImGui::PopStyleVar(); + ImGui::PopStyleColor(); + }; + + auto calcPillWidth = [&](const char* label, const ImVec2& pad) + { + return ImGui::CalcTextSize(label).x + pad.x * 2.0f; + }; + + auto drawTogglePill = [&](const char* label, bool& value, const ImVec4& onCol, const ImVec4& offCol, const ImVec2& pad) + { + ImGui::PushStyleColor(ImGuiCol_Button, value ? onCol : offCol); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, value ? onCol : offCol); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, value ? onCol : offCol); + ImGui::PushStyleColor(ImGuiCol_Text, badgeText); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, pad); + if (ImGui::Button(label)) + value = !value; + ImGui::PopStyleVar(); + ImGui::PopStyleColor(4); + }; + + ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 6.0f); + if (ImGui::BeginChild("PanelHeader", ImVec2(0, 64), true, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse)) + { + ImGui::Dummy(ImVec2(0.0f, 1.0f)); + ImGui::SetWindowFontScale(1.08f); + ImGui::TextColored(accent, "Control Panel"); + ImGui::SetWindowFontScale(1.0f); + { + const ImVec2 badgePad = ImVec2(6.0f, 2.0f); + const float gap = ImGui::GetStyle().ItemSpacing.x; + const char* badgeWindow = useWindow ? "WINDOW" : "FULL"; + const char* badgeMove = enableActiveCameraMovement ? "MOVE ON" : "MOVE OFF"; + const char* badgeScript = m_scriptedInput.enabled ? (m_scriptedInput.exclusive ? "SCRIPT EXCL" : "SCRIPT") : "SCRIPT OFF"; + const float badgeRowWidth = calcPillWidth(badgeWindow, badgePad) + + gap + calcPillWidth(badgeMove, badgePad) + + gap + calcPillWidth(badgeScript, badgePad) + + (m_ciMode ? (gap + calcPillWidth("CI", badgePad)) : 0.0f); + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + std::max(0.0f, (ImGui::GetContentRegionAvail().x - badgeRowWidth) * 0.5f)); + + DrawBadge(badgeWindow, accent, badgeText); + ImGui::SameLine(0.0f, gap); + DrawBadge(badgeMove, enableActiveCameraMovement ? good : bad, badgeText); + ImGui::SameLine(0.0f, gap); + DrawBadge(badgeScript, m_scriptedInput.enabled ? accent : ImVec4(0.35f, 0.36f, 0.38f, 1.0f), badgeText); + if (m_ciMode) { - active.activePlanarIx = static_cast(currentPlanarIx); - active.pickDefaultProjections(m_planarProjections[active.activePlanarIx]->getPlanarProjections()); + ImGui::SameLine(0.0f, gap); + DrawBadge("CI", warn, badgeText); } } - assert(active.boundProjectionIx.has_value()); - assert(active.lastBoundPerspectivePresetProjectionIx.has_value()); - assert(active.lastBoundOrthoPresetProjectionIx.has_value()); - - const auto activePlanarIxString = std::to_string(active.activePlanarIx); - auto& planarBound = m_planarProjections[active.activePlanarIx]; - assert(planarBound); + ImGui::Dummy(ImVec2(0.0f, 2.0f)); + { + const ImVec2 keyPad = ImVec2(4.0f, 1.0f); + const float gap = ImGui::GetStyle().ItemSpacing.x; + const float groupGap = gap * 2.0f; + const float moveWidth = ImGui::CalcTextSize("Move").x + gap + + calcPillWidth("W", keyPad) + gap + + calcPillWidth("A", keyPad) + gap + + calcPillWidth("S", keyPad) + gap + + calcPillWidth("D", keyPad); + const float lookWidth = ImGui::CalcTextSize("Look").x + gap + calcPillWidth("RMB", keyPad); + const float zoomWidth = ImGui::CalcTextSize("Zoom").x + gap + calcPillWidth("MW", keyPad); + const float rowWidth = moveWidth + groupGap + lookWidth + groupGap + zoomWidth; + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + std::max(0.0f, (ImGui::GetContentRegionAvail().x - rowWidth) * 0.5f)); + + ImGui::TextDisabled("Move"); + ImGui::SameLine(); + DrawKeyHint("W", keyBg, keyFg); + ImGui::SameLine(); + DrawKeyHint("A", keyBg, keyFg); + ImGui::SameLine(); + DrawKeyHint("S", keyBg, keyFg); + ImGui::SameLine(); + DrawKeyHint("D", keyBg, keyFg); + + ImGui::SameLine(0.0f, groupGap); + ImGui::TextDisabled("Look"); + ImGui::SameLine(); + DrawKeyHint("RMB", keyBg, keyFg); + + ImGui::SameLine(0.0f, groupGap); + ImGui::TextDisabled("Zoom"); + ImGui::SameLine(); + DrawKeyHint("MW", keyBg, keyFg); + } - auto selectedProjectionType = planarBound->getPlanarProjections()[active.boundProjectionIx.value()].getParameters().m_type; + ImGui::Dummy(ImVec2(0.0f, 2.0f)); + if (ImGui::BeginTable("HeaderMetrics", 3, ImGuiTableFlags_SizingStretchProp)) { - const char* labels[] = { "Perspective", "Orthographic" }; - int type = static_cast(selectedProjectionType); + const float frameMs = std::max(0.0f, m_uiLastFrameMs); + const float fps = frameMs > 0.0f ? (1000.0f / frameMs) : 0.0f; - if (ImGui::Combo("Projection Type", &type, labels, IM_ARRAYSIZE(labels))) + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + miniStat("FrameStat", "Frame", accent, m_uiFrameMs, 16.0f, [&] { - selectedProjectionType = static_cast(type); + ImGui::TextColored(accent, "%.1f ms %.0f fps", frameMs, fps); + }); - switch (selectedProjectionType) + ImGui::TableSetColumnIndex(1); + miniStat("InputStat", "Input", accent, m_uiInputCounts, 4.0f, [&] + { + ImGui::TextColored(accent, "%u ev", m_uiLastInputEvents); + }); + + ImGui::TableSetColumnIndex(2); + miniStat("VirtualStat", "Virtual", accent, m_uiVirtualCounts, 4.0f, [&] + { + ImGui::TextColored(accent, "%u ev", m_uiLastVirtualEvents); + }); + ImGui::EndTable(); + } + } + ImGui::EndChild(); + ImGui::PopStyleVar(); + + ImGui::Spacing(); + + { + const ImVec2 togglePad = ImVec2(6.0f, 2.0f); + const float gap = ImGui::GetStyle().ItemSpacing.x; + const float rowWidth = calcPillWidth("WINDOW", togglePad) + + gap + calcPillWidth("STATUS", togglePad) + + gap + calcPillWidth("EVENT LOG", togglePad); + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + std::max(0.0f, (ImGui::GetContentRegionAvail().x - rowWidth) * 0.5f)); + drawTogglePill("WINDOW", useWindow, accent, ImVec4(0.35f, 0.36f, 0.38f, 1.0f), togglePad); + DrawHoverHint("Toggle split render windows"); + ImGui::SameLine(0.0f, gap); + drawTogglePill("STATUS", m_showHud, accent, ImVec4(0.35f, 0.36f, 0.38f, 1.0f), togglePad); + DrawHoverHint("Show system and camera status panel"); + ImGui::SameLine(0.0f, gap); + drawTogglePill("EVENT LOG", m_showEventLog, accent, ImVec4(0.35f, 0.36f, 0.38f, 1.0f), togglePad); + DrawHoverHint("Show virtual event log"); + } + + ImGui::Separator(); + + if (ImGui::BeginTabBar("ControlTabs")) + { + if (m_showHud && ImGui::BeginTabItem("Status")) + { + ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 4.0f); + if (ImGui::BeginChild("StatusPanel", ImVec2(0, 0), true)) + { + ImGui::PushItemWidth(-1.0f); + const ImVec4 cardTop = ImVec4(0.20f, 0.22f, 0.26f, 0.98f); + const ImVec4 cardBottom = ImVec4(0.12f, 0.13f, 0.15f, 0.98f); + const ImVec4 cardBorder = ImVec4(0.45f, 0.48f, 0.54f, 1.0f); + + DrawSectionHeader("SessionHeader", "Session", accent); + if (BeginCard("SessionCard", CalcCardHeight(3), cardTop, cardBottom, cardBorder)) { - case IPlanarProjection::CProjection::Perspective: active.boundProjectionIx = active.lastBoundPerspectivePresetProjectionIx.value(); break; - case IPlanarProjection::CProjection::Orthographic: active.boundProjectionIx = active.lastBoundOrthoPresetProjectionIx.value(); break; - default: active.boundProjectionIx = std::nullopt; assert(false); break; + if (ImGui::BeginTable("SessionTable", 2, tableFlags)) + { + ImGui::TableSetupColumn("Label", ImGuiTableColumnFlags_WidthFixed, 120.0f); + ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch); + row("Mode", [&] { DrawDot(accent); ImGui::TextColored(accent, "%s", useWindow ? "Window" : "Fullscreen"); }); + row("Active window", [&] { DrawDot(accent); ImGui::TextColored(accent, "%u", activeRenderWindowIx); }); + row("Movement", [&] { const ImVec4 c = enableActiveCameraMovement ? good : bad; DrawDot(c); ImGui::TextColored(c, "%s", enableActiveCameraMovement ? "Enabled" : "Disabled"); }); + ImGui::EndTable(); + } } + EndCard(); + + DrawSectionHeader("CameraHeader", "Camera", accent); + + auto* activeCamera = getActiveCamera(); + if (activeCamera) + { + const auto& gimbal = activeCamera->getGimbal(); + const auto pos = gimbal.getPosition(); + const auto euler = glm::degrees(glm::eulerAngles(gimbal.getOrientation())); + + if (BeginCard("CameraCard", CalcCardHeight(5), cardTop, cardBottom, cardBorder)) + { + if (ImGui::BeginTable("CameraTable", 2, tableFlags)) + { + ImGui::TableSetupColumn("Label", ImGuiTableColumnFlags_WidthFixed, 120.0f); + ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch); + row("Name", [&] { DrawDot(accent); ImGui::TextColored(muted, "%s", activeCamera->getIdentifier().data()); }); + row("Position", [&] { DrawDot(muted); ImGui::TextColored(muted, "%.2f %.2f %.2f", pos.x, pos.y, pos.z); }); + row("Euler", [&] { DrawDot(muted); ImGui::TextColored(muted, "%.1f %.1f %.1f", euler.x, euler.y, euler.z); }); + row("Move scale", [&] { DrawDot(muted); ImGui::TextColored(muted, "%.4f", activeCamera->getMoveSpeedScale()); }); + row("Rotate scale", [&] { DrawDot(muted); ImGui::TextColored(muted, "%.4f", activeCamera->getRotationSpeedScale()); }); + ImGui::EndTable(); + } + } + EndCard(); + } + else + { + if (BeginCard("CameraCard", CalcCardHeight(2), cardTop, cardBottom, cardBorder)) + ImGui::TextDisabled("No active camera"); + EndCard(); + } + + DrawSectionHeader("ProjectionHeader", "Projection", accent); + + auto& binding = windowBindings[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 (BeginCard("ProjectionCard", CalcCardHeight(4), cardTop, cardBottom, cardBorder)) + { + if (ImGui::BeginTable("ProjectionTable", 2, tableFlags)) + { + ImGui::TableSetupColumn("Label", ImGuiTableColumnFlags_WidthFixed, 120.0f); + ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch); + row("Type", [&] { DrawDot(accent); ImGui::TextColored(muted, "%s", params.m_type == IPlanarProjection::CProjection::Perspective ? "Perspective" : "Orthographic"); }); + row("zNear", [&] { DrawDot(muted); ImGui::TextColored(muted, "%.2f", params.m_zNear); }); + row("zFar", [&] { DrawDot(muted); ImGui::TextColored(muted, "%.2f", params.m_zFar); }); + if (params.m_type == IPlanarProjection::CProjection::Perspective) + row("Fov", [&] { DrawDot(muted); ImGui::TextColored(muted, "%.1f", params.m_planar.perspective.fov); }); + else + row("Ortho width", [&] { DrawDot(muted); ImGui::TextColored(muted, "%.1f", params.m_planar.orthographic.orthoWidth); }); + ImGui::EndTable(); + } + } + EndCard(); + } + else + { + if (BeginCard("ProjectionCard", CalcCardHeight(2), cardTop, cardBottom, cardBorder)) + ImGui::TextDisabled("No projection bound"); + EndCard(); + } + ImGui::PopItemWidth(); } + ImGui::EndChild(); + ImGui::PopStyleVar(); + ImGui::EndTabItem(); } - auto getPresetName = [&](auto ix) -> std::string + if (ImGui::BeginTabItem("Projection")) { - switch (selectedProjectionType) + ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 4.0f); + if (ImGui::BeginChild("ProjectionPanel", ImVec2(0, 0), true)) { - case IPlanarProjection::CProjection::Perspective: return "Perspective Projection Preset " + std::to_string(ix); - case IPlanarProjection::CProjection::Orthographic: return "Orthographic Projection Preset " + std::to_string(ix); - default: return "Unknown Projection Preset " + std::to_string(ix); - } - }; + ImGui::PushItemWidth(-1.0f); + auto& active = windowBindings[activeRenderWindowIx]; + const auto activeRenderWindowIxString = std::to_string(activeRenderWindowIx); - bool updateBoundVirtualMaps = false; - if (ImGui::BeginCombo("Projection Preset", getPresetName(active.boundProjectionIx.value()).c_str())) - { - auto& projections = planarBound->getPlanarProjections(); + DrawSectionHeader("PlanarSelectHeader", "Planar Selection", accent); + ImGui::Text("Active Render Window: %s", activeRenderWindowIxString.c_str()); + DrawHoverHint("Window that receives input and camera switching"); + { + const size_t planarsCount = m_planarProjections.size(); + assert(planarsCount); - for (uint32_t i = 0; i < projections.size(); ++i) - { - const auto& projection = projections[i]; - const auto& params = projection.getParameters(); + std::vector sbels(planarsCount); + for (size_t i = 0; i < planarsCount; ++i) + sbels[i] = "Planar " + std::to_string(i); - if (params.m_type != selectedProjectionType) - continue; + std::vector labels(planarsCount); + for (size_t i = 0; i < planarsCount; ++i) + labels[i] = sbels[i].c_str(); + + int currentPlanarIx = static_cast(active.activePlanarIx); + if (ImGui::Combo("Active Planar", ¤tPlanarIx, labels.data(), static_cast(labels.size()))) + { + active.activePlanarIx = static_cast(currentPlanarIx); + active.pickDefaultProjections(m_planarProjections[active.activePlanarIx]->getPlanarProjections()); + } + DrawHoverHint("Select which camera the window renders"); + } + + assert(active.boundProjectionIx.has_value()); + assert(active.lastBoundPerspectivePresetProjectionIx.has_value()); + assert(active.lastBoundOrthoPresetProjectionIx.has_value()); + + const auto activePlanarIxString = std::to_string(active.activePlanarIx); + auto& planarBound = m_planarProjections[active.activePlanarIx]; + assert(planarBound); - bool isSelected = (i == active.boundProjectionIx.value()); + DrawSectionHeader("ProjectionParamsHeader", "Projection Parameters", accent); - if (ImGui::Selectable(getPresetName(i).c_str(), isSelected)) + auto selectedProjectionType = planarBound->getPlanarProjections()[active.boundProjectionIx.value()].getParameters().m_type; { - active.boundProjectionIx = i; - updateBoundVirtualMaps |= true; + const char* labels[] = { "Perspective", "Orthographic" }; + int type = static_cast(selectedProjectionType); - switch (selectedProjectionType) + if (ImGui::Combo("Projection Type", &type, labels, IM_ARRAYSIZE(labels))) { - case IPlanarProjection::CProjection::Perspective: active.lastBoundPerspectivePresetProjectionIx = active.boundProjectionIx.value(); break; - case IPlanarProjection::CProjection::Orthographic: active.lastBoundOrthoPresetProjectionIx = active.boundProjectionIx.value(); break; - default: assert(false); break; + selectedProjectionType = static_cast(type); + + switch (selectedProjectionType) + { + case IPlanarProjection::CProjection::Perspective: active.boundProjectionIx = active.lastBoundPerspectivePresetProjectionIx.value(); break; + case IPlanarProjection::CProjection::Orthographic: active.boundProjectionIx = active.lastBoundOrthoPresetProjectionIx.value(); break; + default: active.boundProjectionIx = std::nullopt; assert(false); break; + } } + DrawHoverHint("Switch projection type for this planar"); } - if (isSelected) - ImGui::SetItemDefaultFocus(); - } - ImGui::EndCombo(); - } - - auto* const boundCamera = planarBound->getCamera(); - auto& boundProjection = planarBound->getPlanarProjections()[active.boundProjectionIx.value()]; - assert(not boundProjection.isProjectionSingular()); + auto getPresetName = [&](auto ix) -> std::string + { + switch (selectedProjectionType) + { + case IPlanarProjection::CProjection::Perspective: return "Perspective Projection Preset " + std::to_string(ix); + case IPlanarProjection::CProjection::Orthographic: return "Orthographic Projection Preset " + std::to_string(ix); + default: return "Unknown Projection Preset " + std::to_string(ix); + } + }; - auto updateParameters = boundProjection.getParameters(); + bool updateBoundVirtualMaps = false; + if (ImGui::BeginCombo("Projection Preset", getPresetName(active.boundProjectionIx.value()).c_str())) + { + auto& projections = planarBound->getPlanarProjections(); - if (useWindow) - ImGui::Checkbox("Allow axes to flip##allowAxesToFlip", &active.allowGizmoAxesToFlip); + for (uint32_t i = 0; i < projections.size(); ++i) + { + const auto& projection = projections[i]; + const auto& params = projection.getParameters(); - if(useWindow) - ImGui::Checkbox("Draw debug grid##drawDebugGrid", &active.enableDebugGridDraw); + if (params.m_type != selectedProjectionType) + continue; - if (ImGui::RadioButton("LH", active.leftHandedProjection)) - active.leftHandedProjection = true; + bool isSelected = (i == active.boundProjectionIx.value()); - ImGui::SameLine(); + if (ImGui::Selectable(getPresetName(i).c_str(), isSelected)) + { + active.boundProjectionIx = i; + updateBoundVirtualMaps |= true; - if (ImGui::RadioButton("RH", not active.leftHandedProjection)) - active.leftHandedProjection = false; + switch (selectedProjectionType) + { + case IPlanarProjection::CProjection::Perspective: active.lastBoundPerspectivePresetProjectionIx = active.boundProjectionIx.value(); break; + case IPlanarProjection::CProjection::Orthographic: active.lastBoundOrthoPresetProjectionIx = active.boundProjectionIx.value(); break; + default: assert(false); break; + } + } - updateParameters.m_zNear = std::clamp(updateParameters.m_zNear, 0.1f, 100.f); - updateParameters.m_zFar = std::clamp(updateParameters.m_zFar, 110.f, 10000.f); + if (isSelected) + ImGui::SetItemDefaultFocus(); + } + ImGui::EndCombo(); + } + DrawHoverHint("Switch preset projection for this planar"); - ImGui::SliderFloat("zNear", &updateParameters.m_zNear, 0.1f, 100.f, "%.2f", ImGuiSliderFlags_Logarithmic); - ImGui::SliderFloat("zFar", &updateParameters.m_zFar, 110.f, 10000.f, "%.1f", ImGuiSliderFlags_Logarithmic); + auto* const boundCamera = planarBound->getCamera(); + auto& boundProjection = planarBound->getPlanarProjections()[active.boundProjectionIx.value()]; + assert(not boundProjection.isProjectionSingular()); - switch (selectedProjectionType) - { - case IPlanarProjection::CProjection::Perspective: - { - ImGui::SliderFloat("Fov", &updateParameters.m_planar.perspective.fov, 20.f, 150.f, "%.1f", ImGuiSliderFlags_Logarithmic); - boundProjection.setPerspective(updateParameters.m_zNear, updateParameters.m_zFar, updateParameters.m_planar.perspective.fov); - } break; + auto updateParameters = boundProjection.getParameters(); - case IPlanarProjection::CProjection::Orthographic: - { - ImGui::SliderFloat("Ortho width", &updateParameters.m_planar.orthographic.orthoWidth, 1.f, 30.f, "%.1f", ImGuiSliderFlags_Logarithmic); - boundProjection.setOrthographic(updateParameters.m_zNear, updateParameters.m_zFar, updateParameters.m_planar.orthographic.orthoWidth); - } break; + if (useWindow) + ImGui::Checkbox("Allow axes to flip##allowAxesToFlip", &active.allowGizmoAxesToFlip); + DrawHoverHint("Allow ImGuizmo axes to flip based on view"); - default: break; - } + if(useWindow) + ImGui::Checkbox("Draw debug grid##drawDebugGrid", &active.enableDebugGridDraw); + DrawHoverHint("Toggle debug grid in the render window"); - { - if (ImGui::TreeNodeEx("Cursor Behaviour")) - { - if (ImGui::RadioButton("Clamp to the window", !resetCursorToCenter)) - resetCursorToCenter = false; - if (ImGui::RadioButton("Reset to the window center", resetCursorToCenter)) - resetCursorToCenter = true; - ImGui::TreePop(); - } - } + if (ImGui::RadioButton("LH", active.leftHandedProjection)) + active.leftHandedProjection = true; - { - ImGuiIO& io = ImGui::GetIO(); + ImGui::SameLine(); - if (ImGui::IsKeyPressed(ImGuiKey_Space)) - enableActiveCameraMovement = !enableActiveCameraMovement; + if (ImGui::RadioButton("RH", not active.leftHandedProjection)) + active.leftHandedProjection = false; + DrawHoverHint("Toggle left or right handed projection"); - if (enableActiveCameraMovement) - { - ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), "Bound Camera Movement: Enabled"); - io.ConfigFlags |= ImGuiConfigFlags_NoMouse; - io.MouseDrawCursor = false; - io.WantCaptureMouse = false; + updateParameters.m_zNear = std::clamp(updateParameters.m_zNear, 0.1f, 100.f); + updateParameters.m_zFar = std::clamp(updateParameters.m_zFar, 110.f, 10000.f); - ImVec2 cursorPos = ImGui::GetMousePos(); - ImVec2 viewportSize = io.DisplaySize; - auto* cc = m_window->getCursorControl(); - int32_t posX = m_window->getX(); - int32_t posY = m_window->getY(); + ImGui::SliderFloat("zNear", &updateParameters.m_zNear, 0.1f, 100.f, "%.2f", ImGuiSliderFlags_Logarithmic); + DrawHoverHint("Near clip plane"); + ImGui::SliderFloat("zFar", &updateParameters.m_zFar, 110.f, 10000.f, "%.1f", ImGuiSliderFlags_Logarithmic); + DrawHoverHint("Far clip plane"); - if (resetCursorToCenter) + switch (selectedProjectionType) { - const ICursorControl::SPosition middle{ static_cast(viewportSize.x / 2 + posX), static_cast(viewportSize.y / 2 + posY) }; - cc->setPosition(middle); + case IPlanarProjection::CProjection::Perspective: + { + ImGui::SliderFloat("Fov", &updateParameters.m_planar.perspective.fov, 20.f, 150.f, "%.1f", ImGuiSliderFlags_Logarithmic); + DrawHoverHint("Perspective field of view"); + boundProjection.setPerspective(updateParameters.m_zNear, updateParameters.m_zFar, updateParameters.m_planar.perspective.fov); + } break; + + case IPlanarProjection::CProjection::Orthographic: + { + ImGui::SliderFloat("Ortho width", &updateParameters.m_planar.orthographic.orthoWidth, 1.f, 30.f, "%.1f", ImGuiSliderFlags_Logarithmic); + DrawHoverHint("Orthographic width"); + boundProjection.setOrthographic(updateParameters.m_zNear, updateParameters.m_zFar, updateParameters.m_planar.orthographic.orthoWidth); + } break; + + default: break; } - else + + DrawSectionHeader("CursorHeader", "Cursor Behaviour", accent); + if (ImGui::TreeNodeEx("Cursor Behaviour")) { - auto currentCursorPos = cc->getPosition(); - ICursorControl::SPosition newPos{}; - newPos.x = std::clamp(currentCursorPos.x, posX, viewportSize.x + posX); - newPos.y = std::clamp(currentCursorPos.y, posY, viewportSize.y + posY); - cc->setPosition(newPos); + if (ImGui::RadioButton("Clamp to the window", !resetCursorToCenter)) + resetCursorToCenter = false; + if (ImGui::RadioButton("Reset to the window center", resetCursorToCenter)) + resetCursorToCenter = true; + ImGui::TreePop(); } - } - else - { - ImGui::TextColored(ImVec4(1.0f, 0.0f, 0.0f, 1.0f), "Bound Camera Movement: Disabled"); - io.ConfigFlags &= ~ImGuiConfigFlags_NoMouse; - io.MouseDrawCursor = true; - io.WantCaptureMouse = true; - } - - if (ImGui::IsItemHovered()) - { - ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.2f, 0.2f, 0.2f, 0.8f)); - ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); - ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.5f); + if (enableActiveCameraMovement) + ImGui::TextColored(good, "Bound Camera Movement: Enabled"); + else + ImGui::TextColored(bad, "Bound Camera Movement: Disabled"); - ImVec2 mousePos = ImGui::GetMousePos(); - ImGui::SetNextWindowPos(ImVec2(mousePos.x + 10, mousePos.y + 10), ImGuiCond_Always); + ImGui::Separator(); - ImGui::Begin("HoverOverlay", nullptr, - ImGuiWindowFlags_NoDecoration | - ImGuiWindowFlags_AlwaysAutoResize | - ImGuiWindowFlags_NoSavedSettings); + DrawSectionHeader("BoundCameraHeader", "Bound Camera", accent); + const auto flags = ImGuiTreeNodeFlags_DefaultOpen; + if (ImGui::TreeNodeEx("Bound Camera", flags)) + { + ImGui::Text("Type: %s", boundCamera->getIdentifier().data()); + ImGui::Text("Object Ix: %s", std::to_string(active.activePlanarIx + 1u).c_str()); + ImGui::Separator(); + { + auto* orbit = dynamic_cast(boundCamera); + auto* arcball = dynamic_cast(boundCamera); + auto* turntable = dynamic_cast(boundCamera); + const bool isOrbitLike = orbit || arcball || turntable; - ImGui::Text("Press 'Space' to Enable/Disable bound planar camera movement"); + float moveSpeed = boundCamera->getMoveSpeedScale(); + float rotationSpeed = boundCamera->getRotationSpeedScale(); - ImGui::End(); + ImGui::SliderFloat("Move speed factor", &moveSpeed, 0.0001f, 10.f, "%.4f", ImGuiSliderFlags_Logarithmic); + DrawHoverHint("Scale translation speed for this camera"); - ImGui::PopStyleVar(); - ImGui::PopStyleColor(2); - } + if (not orbit) + ImGui::SliderFloat("Rotate speed factor", &rotationSpeed, 0.0001f, 10.f, "%.4f", ImGuiSliderFlags_Logarithmic); + DrawHoverHint("Scale rotation speed for this camera"); - ImGui::Separator(); + boundCamera->setMoveSpeedScale(moveSpeed); + boundCamera->setRotationSpeedScale(rotationSpeed); - const auto flags = ImGuiTreeNodeFlags_DefaultOpen; - if (ImGui::TreeNodeEx("Bound Camera", flags)) + if (isOrbitLike) + { + auto applyDistance = [&](auto* cam) + { + float distance = cam->getDistance(); + ImGui::SliderFloat("Distance", &distance, cam->MinDistance, cam->MaxDistance, "%.4f", ImGuiSliderFlags_Logarithmic); + DrawHoverHint("Current orbit distance"); + cam->setDistance(distance); + }; + + if (orbit) + applyDistance(orbit); + else if (arcball) + applyDistance(arcball); + else if (turntable) + applyDistance(turntable); + } + } + + if (ImGui::TreeNodeEx("World Data", flags)) + { + auto& gimbal = boundCamera->getGimbal(); + const auto position = getCastedVector(gimbal.getPosition()); + const auto& orientation = gimbal.getOrientation(); + const auto viewMatrix = getCastedMatrix(gimbal.getViewMatrix()); + + addMatrixTable("Position", ("PositionTable_" + activePlanarIxString).c_str(), 1, 3, &position[0], false); + addMatrixTable("Orientation (Quaternion)", ("OrientationTable_" + activePlanarIxString).c_str(), 1, 4, &orientation[0], false); + addMatrixTable("View Matrix", ("ViewMatrixTable_" + activePlanarIxString).c_str(), 3, 4, &viewMatrix[0][0], false); + ImGui::TreePop(); + } + + if (ImGui::TreeNodeEx("Virtual Event Mappings", flags)) + { + displayKeyMappingsAndVirtualStatesInline(&boundProjection); + ImGui::TreePop(); + } + + ImGui::TreePop(); + } + ImGui::PopItemWidth(); + } + ImGui::EndChild(); + ImGui::PopStyleVar(); + ImGui::EndTabItem(); + } + + if (ImGui::BeginTabItem("Camera")) + { + ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 4.0f); + if (ImGui::BeginChild("CameraPanel", ImVec2(0, 0), true)) { - ImGui::Text("Type: %s", boundCamera->getIdentifier().data()); - ImGui::Text("Object Ix: %s", std::to_string(active.activePlanarIx + 1u).c_str()); + ImGui::PushItemWidth(-1.0f); + DrawSectionHeader("CameraInputHeader", "Input", accent); + ImGui::Checkbox("Mirror input to all cameras", &m_cameraControls.mirrorInput); + DrawHoverHint("Apply keyboard and mouse input to every camera"); + ImGui::Checkbox("World translate", &m_cameraControls.worldTranslate); + DrawHoverHint("Translate in world space instead of camera space"); + ImGui::SliderFloat("Keyboard scale", &m_cameraControls.keyboardScale, 0.01f, 10.f, "%.2f"); + DrawHoverHint("Scale keyboard movement magnitudes"); + ImGui::SliderFloat("Mouse move scale", &m_cameraControls.mouseMoveScale, 0.01f, 10.f, "%.2f"); + DrawHoverHint("Scale mouse move magnitudes"); + ImGui::SliderFloat("Mouse scroll scale", &m_cameraControls.mouseScrollScale, 0.01f, 10.f, "%.2f"); + DrawHoverHint("Scale mouse wheel magnitudes"); + ImGui::SliderFloat("Translate scale", &m_cameraControls.translationScale, 0.01f, 10.f, "%.2f"); + DrawHoverHint("Overall translation scale for virtual events"); + ImGui::SliderFloat("Rotate scale", &m_cameraControls.rotationScale, 0.01f, 10.f, "%.2f"); + DrawHoverHint("Overall rotation scale for virtual events"); + + DrawSectionHeader("CameraConstraintsHeader", "Constraints", accent); + ImGui::Checkbox("Enable constraints", &m_cameraConstraints.enabled); + DrawHoverHint("Enable or disable all camera constraints"); + ImGui::Checkbox("Clamp distance", &m_cameraConstraints.clampDistance); + DrawHoverHint("Clamp orbit distance to min/max"); + ImGui::SliderFloat("Min distance", &m_cameraConstraints.minDistance, 0.01f, 1000.f, "%.3f", ImGuiSliderFlags_Logarithmic); + DrawHoverHint("Minimum orbit distance"); + ImGui::SliderFloat("Max distance", &m_cameraConstraints.maxDistance, 0.01f, 10000.f, "%.3f", ImGuiSliderFlags_Logarithmic); + DrawHoverHint("Maximum orbit distance"); ImGui::Separator(); + ImGui::Checkbox("Clamp pitch", &m_cameraConstraints.clampPitch); + DrawHoverHint("Clamp pitch angle"); + ImGui::SliderFloat("Pitch min", &m_cameraConstraints.pitchMinDeg, -180.f, 180.f, "%.1f"); + DrawHoverHint("Minimum pitch in degrees"); + ImGui::SliderFloat("Pitch max", &m_cameraConstraints.pitchMaxDeg, -180.f, 180.f, "%.1f"); + DrawHoverHint("Maximum pitch in degrees"); + ImGui::Checkbox("Clamp yaw", &m_cameraConstraints.clampYaw); + DrawHoverHint("Clamp yaw angle"); + ImGui::SliderFloat("Yaw min", &m_cameraConstraints.yawMinDeg, -180.f, 180.f, "%.1f"); + DrawHoverHint("Minimum yaw in degrees"); + ImGui::SliderFloat("Yaw max", &m_cameraConstraints.yawMaxDeg, -180.f, 180.f, "%.1f"); + DrawHoverHint("Maximum yaw in degrees"); + ImGui::Checkbox("Clamp roll", &m_cameraConstraints.clampRoll); + DrawHoverHint("Clamp roll angle"); + ImGui::SliderFloat("Roll min", &m_cameraConstraints.rollMinDeg, -180.f, 180.f, "%.1f"); + DrawHoverHint("Minimum roll in degrees"); + ImGui::SliderFloat("Roll max", &m_cameraConstraints.rollMaxDeg, -180.f, 180.f, "%.1f"); + DrawHoverHint("Maximum roll in degrees"); + + DrawSectionHeader("OrbitHeader", "Orbit Target", accent); + + auto* activeCamera = getActiveCamera(); + const bool hasOrbitTarget = withOrbitLikeCamera(activeCamera, [&](auto* orbit) { - auto* orbit = dynamic_cast(boundCamera); + auto target = getCastedVector(orbit->getTarget()); + if (ImGui::InputFloat3("Target", &target[0])) + orbit->target(getCastedVector(target)); - float moveSpeed = boundCamera->getMoveSpeedScale(); - float rotationSpeed = boundCamera->getRotationSpeedScale(); + if (ImGui::Button("Target model")) + { + auto targetPos = hlsl::transpose(getMatrix3x4As4x4(m_model))[3]; + orbit->target(targetPos); + } + DrawHoverHint("Set orbit target to the model position"); + ImGui::SameLine(); + if (ImGui::Button("Target origin")) + orbit->target(float64_t3(0.0)); + DrawHoverHint("Set orbit target to world origin"); + }); + if (!hasOrbitTarget) + { + ImGui::TextDisabled("Active camera is not orbit."); + } + ImGui::PopItemWidth(); + } + ImGui::EndChild(); + ImGui::PopStyleVar(); + ImGui::EndTabItem(); + } - ImGui::SliderFloat("Move speed factor", &moveSpeed, 0.0001f, 10.f, "%.4f", ImGuiSliderFlags_Logarithmic); + if (ImGui::BeginTabItem("Presets")) + { + ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 4.0f); + if (ImGui::BeginChild("PresetsPanel", ImVec2(0, 0), true)) + { + ImGui::PushItemWidth(-1.0f); + DrawSectionHeader("PresetsHeader", "Presets", accent); + ImGui::InputText("Preset name", m_presetName, IM_ARRAYSIZE(m_presetName)); + if (ImGui::Button("Add preset")) + { + auto* activeCamera = getActiveCamera(); + m_presets.emplace_back(capturePreset(activeCamera, m_presetName)); + } + DrawHoverHint("Store current camera as a preset"); + ImGui::SameLine(); + if (ImGui::Button("Clear presets")) + m_presets.clear(); + DrawHoverHint("Remove all presets"); - if(not orbit) - ImGui::SliderFloat("Rotate speed factor", &rotationSpeed, 0.0001f, 10.f, "%.4f", ImGuiSliderFlags_Logarithmic); + if (!m_presets.empty()) + { + std::vector names; + names.reserve(m_presets.size()); + for (const auto& preset : m_presets) + names.push_back(preset.name.c_str()); - boundCamera->setMoveSpeedScale(moveSpeed); - boundCamera->setRotationSpeedScale(rotationSpeed); + static int selectedPreset = -1; + ImGui::ListBox("Preset list", &selectedPreset, names.data(), static_cast(names.size()), 6); + if (selectedPreset >= 0 && static_cast(selectedPreset) < m_presets.size()) { - if (orbit) - { - float distance = orbit->getDistance(); - ImGui::SliderFloat("Distance", &distance, COrbitCamera::MinDistance, COrbitCamera::MaxDistance, "%.4f", ImGuiSliderFlags_Logarithmic); - orbit->setDistance(distance); - } + if (ImGui::Button("Apply preset")) + applyPresetToCamera(getActiveCamera(), m_presets[static_cast(selectedPreset)]); + DrawHoverHint("Apply selected preset to the active camera"); + ImGui::SameLine(); + if (ImGui::Button("Remove preset")) + m_presets.erase(m_presets.begin() + selectedPreset); + DrawHoverHint("Remove selected preset"); } } - if (ImGui::TreeNodeEx("World Data", flags)) + DrawSectionHeader("PresetsStorageHeader", "Storage", accent); + ImGui::InputText("Preset file", m_presetPath, IM_ARRAYSIZE(m_presetPath)); + if (ImGui::Button("Save presets")) + { + if (!savePresetsToFile(system::path(m_presetPath))) + m_logger->log("Failed to save presets to \"%s\".", ILogger::ELL_ERROR, m_presetPath); + } + DrawHoverHint("Save presets to JSON file"); + ImGui::SameLine(); + if (ImGui::Button("Load presets")) { - auto& gimbal = boundCamera->getGimbal(); - const auto position = getCastedVector(gimbal.getPosition()); - const auto& orientation = gimbal.getOrientation(); - const auto viewMatrix = getCastedMatrix(gimbal.getViewMatrix()); + if (!loadPresetsFromFile(system::path(m_presetPath))) + m_logger->log("Failed to load presets from \"%s\".", ILogger::ELL_ERROR, m_presetPath); + } + DrawHoverHint("Load presets from JSON file"); + ImGui::PopItemWidth(); + } + ImGui::EndChild(); + ImGui::PopStyleVar(); + ImGui::EndTabItem(); + } - addMatrixTable("Position", ("PositionTable_" + activePlanarIxString).c_str(), 1, 3, &position[0], false); - addMatrixTable("Orientation (Quaternion)", ("OrientationTable_" + activePlanarIxString).c_str(), 1, 4, &orientation[0], false); - addMatrixTable("View Matrix", ("ViewMatrixTable_" + activePlanarIxString).c_str(), 3, 4, &viewMatrix[0][0], false); - ImGui::TreePop(); + if (ImGui::BeginTabItem("Playback")) + { + ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 4.0f); + if (ImGui::BeginChild("PlaybackPanel", ImVec2(0, 0), true)) + { + ImGui::PushItemWidth(-1.0f); + DrawSectionHeader("PlaybackHeader", "Playback", accent); + ImGui::Checkbox("Loop", &m_playback.loop); + DrawHoverHint("Loop playback when it reaches the end"); + ImGui::Checkbox("Override input", &m_playback.overrideInput); + DrawHoverHint("Ignore manual input during playback"); + ImGui::Checkbox("Affect all cameras", &m_playbackAffectsAll); + DrawHoverHint("Apply playback to all cameras"); + ImGui::SliderFloat("Speed", &m_playback.speed, 0.1f, 4.f, "%.2f"); + DrawHoverHint("Playback speed multiplier"); + + if (ImGui::Button(m_playback.playing ? "Pause" : "Play")) + m_playback.playing = !m_playback.playing; + DrawHoverHint("Start or pause playback"); + ImGui::SameLine(); + if (ImGui::Button("Stop")) + { + m_playback.playing = false; + m_playback.time = 0.f; } + DrawHoverHint("Stop playback and reset time"); - if (ImGui::TreeNodeEx("Virtual Event Mappings", flags)) + if (!m_keyframes.empty()) { - displayKeyMappingsAndVirtualStatesInline(&boundProjection); - ImGui::TreePop(); + const float duration = m_keyframes.back().time; + ImGui::SliderFloat("Time", &m_playback.time, 0.f, duration, "%.3f"); + } + + DrawSectionHeader("KeyframesHeader", "Keyframes", accent); + ImGui::InputFloat("New keyframe time", &m_newKeyframeTime, 0.1f, 1.f, "%.3f"); + DrawHoverHint("Time value for new keyframe"); + if (ImGui::Button("Add keyframe")) + { + auto* activeCamera = getActiveCamera(); + CameraKeyframe keyframe; + keyframe.time = m_newKeyframeTime; + keyframe.preset = capturePreset(activeCamera, "Keyframe"); + m_keyframes.emplace_back(std::move(keyframe)); + std::sort(m_keyframes.begin(), m_keyframes.end(), [](const auto& a, const auto& b) { return a.time < b.time; }); } + DrawHoverHint("Add keyframe from current camera"); + ImGui::SameLine(); + if (ImGui::Button("Clear keyframes")) + m_keyframes.clear(); + DrawHoverHint("Remove all keyframes"); - ImGui::TreePop(); + if (!m_keyframes.empty()) + { + if (ImGui::BeginChild("KeyframeList", ImVec2(0, 120), true)) + { + for (size_t i = 0; i < m_keyframes.size(); ++i) + { + ImGui::Text("[%zu] t=%.3f", i, m_keyframes[i].time); + } + } + ImGui::EndChild(); + } + ImGui::PopItemWidth(); } + ImGui::EndChild(); + ImGui::PopStyleVar(); + ImGui::EndTabItem(); } + if (ImGui::BeginTabItem("Gizmo")) { - ImGuiIO& io = ImGui::GetIO(); - ImVec2 mousePos = ImGui::GetMousePos(); - ImVec2 viewportSize = io.DisplaySize; - auto* cc = m_window->getCursorControl(); - - if (mousePos.x < 0.0f || mousePos.y < 0.0f || mousePos.x > viewportSize.x || mousePos.y > viewportSize.y) + ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 4.0f); + if (ImGui::BeginChild("GizmoPanel", ImVec2(0, 0), true)) { - if (not enableActiveCameraMovement) - cc->setVisible(true); + DrawSectionHeader("GizmoHeader", "Gizmo", accent); + TransformEditorContents(); } - else + ImGui::EndChild(); + ImGui::PopStyleVar(); + ImGui::EndTabItem(); + } + + if (m_showEventLog && ImGui::BeginTabItem("Log")) + { + ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 4.0f); + if (ImGui::BeginChild("LogPanel", ImVec2(0, 0), true)) { - cc->setVisible(false); + DrawSectionHeader("LogHeader", "Virtual Events", accent); + ImGui::Checkbox("Auto-scroll", &m_logAutoScroll); + ImGui::SameLine(); + ImGui::Checkbox("Wrap", &m_logWrap); + ImGui::Separator(); + + ImGuiWindowFlags logFlags = m_logWrap ? ImGuiWindowFlags_None : ImGuiWindowFlags_HorizontalScrollbar; + if (ImGui::BeginChild("LogList", ImVec2(0, 0), false, logFlags)) + { + const float scrollY = ImGui::GetScrollY(); + const float scrollMax = ImGui::GetScrollMaxY(); + const bool wasAtBottom = scrollY >= scrollMax - 5.0f; + const size_t start = m_virtualEventLog.size() > 200 ? m_virtualEventLog.size() - 200 : 0; + if (m_logWrap) + ImGui::PushTextWrapPos(0.0f); + for (size_t i = start; i < m_virtualEventLog.size(); ++i) + { + const auto& entry = m_virtualEventLog[i]; + ImGui::TextUnformatted(entry.line.c_str()); + } + if (m_logWrap) + ImGui::PopTextWrapPos(); + if (m_logAutoScroll && wasAtBottom && !m_virtualEventLog.empty()) + ImGui::SetScrollHereY(1.0f); + } + ImGui::EndChild(); } + ImGui::EndChild(); + ImGui::PopStyleVar(); + ImGui::EndTabItem(); } - ImGui::End(); + ImGui::EndTabBar(); } + + ImGui::End(); + ImGui::PopStyleColor(19); + ImGui::PopStyleVar(9); } - inline void TransformEditor() + inline void TransformEditorContents() { 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; - ImGuiIO& io = ImGui::GetIO(); + const size_t objectsCount = m_planarProjections.size() + 1u; + assert(objectsCount); - // setup - { - const ImGuiCond windowCond = m_ciMode ? ImGuiCond_Always : ImGuiCond_Appearing; - ImGui::SetNextWindowPos({ wInit.trsEditor.iPos.x, wInit.trsEditor.iPos.y }, windowCond); - ImGui::SetNextWindowSize({ wInit.trsEditor.iSize.x, wInit.trsEditor.iSize.y }, windowCond); - } - - ImGui::Begin("TRS Editor"); - { - const size_t objectsCount = m_planarProjections.size() + 1u; - assert(objectsCount); + std::vector sbels(objectsCount); + for (size_t i = 0; i < objectsCount; ++i) + sbels[i] = "Object " + std::to_string(i); - std::vector sbels(objectsCount); - for (size_t i = 0; i < objectsCount; ++i) - sbels[i] = "Object " + std::to_string(i); + std::vector labels(objectsCount); + for (size_t i = 0; i < objectsCount; ++i) + labels[i] = sbels[i].c_str(); - std::vector labels(objectsCount); - for (size_t i = 0; i < objectsCount; ++i) - labels[i] = sbels[i].c_str(); + int activeObject = boundCameraToManipulate ? static_cast(boundPlanarCameraIxToManipulate.value() + 1u) : 0; + if (ImGui::Combo("Active Object", &activeObject, labels.data(), static_cast(labels.size()))) + { + const auto newActiveObject = static_cast(activeObject); - int activeObject = boundCameraToManipulate ? static_cast(boundPlanarCameraIxToManipulate.value() + 1u) : 0; - if (ImGui::Combo("Active Object", &activeObject, labels.data(), static_cast(labels.size()))) + if (newActiveObject) // camera { - const auto newActiveObject = static_cast(activeObject); - - if (newActiveObject) // camera - { - boundPlanarCameraIxToManipulate = newActiveObject - 1u; - ICamera* const targetGimbalManipulationCamera = m_planarProjections[boundPlanarCameraIxToManipulate.value()]->getCamera(); - boundCameraToManipulate = smart_refctd_ptr(targetGimbalManipulationCamera); - } - else // gc model - { - boundPlanarCameraIxToManipulate = std::nullopt; - boundCameraToManipulate = nullptr; - } + boundPlanarCameraIxToManipulate = newActiveObject - 1u; + ICamera* const targetGimbalManipulationCamera = m_planarProjections[boundPlanarCameraIxToManipulate.value()]->getCamera(); + boundCameraToManipulate = smart_refctd_ptr(targetGimbalManipulationCamera); + } + else // gc model + { + boundPlanarCameraIxToManipulate = std::nullopt; + boundCameraToManipulate = nullptr; } } @@ -3154,6 +4703,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGui::EndCombo(); } } + } addMatrixTable("Model (TRS) Matrix", "ModelMatrixTable", 4, 4, m16TRSmatrix); @@ -3208,70 +4758,67 @@ class UISampleApp final : public examples::SimpleWindowedApplication break; } - ImGui::End(); + // generate virtual events given delta TRS matrix + if (boundCameraToManipulate) { - // generate virtual events given delta TRS matrix - if (boundCameraToManipulate) - { - const float pmSpeed = boundCameraToManipulate->getMoveSpeedScale(); - const float prSpeed = boundCameraToManipulate->getRotationSpeedScale(); + const float pmSpeed = boundCameraToManipulate->getMoveSpeedScale(); + const float prSpeed = boundCameraToManipulate->getRotationSpeedScale(); - boundCameraToManipulate->setMoveSpeedScale(1); - boundCameraToManipulate->setRotationSpeedScale(1); + boundCameraToManipulate->setMoveSpeedScale(1); + boundCameraToManipulate->setRotationSpeedScale(1); - auto referenceFrame = getCastedMatrix(imguizmoModel.outTRS); - boundCameraToManipulate->manipulate({}, &referenceFrame); + auto referenceFrame = getCastedMatrix(imguizmoModel.outTRS); + boundCameraToManipulate->manipulate({}, &referenceFrame); - boundCameraToManipulate->setMoveSpeedScale(pmSpeed); - boundCameraToManipulate->setRotationSpeedScale(prSpeed); + boundCameraToManipulate->setMoveSpeedScale(pmSpeed); + boundCameraToManipulate->setRotationSpeedScale(prSpeed); + + /* + { + static std::vector virtualEvents(0x45); - /* + if (not enableActiveCameraMovement) { - static std::vector virtualEvents(0x45); + uint32_t vCount = {}; - if (not enableActiveCameraMovement) + boundCameraToManipulate->beginInputProcessing(m_nextPresentationTimestamp); { - uint32_t vCount = {}; + boundCameraToManipulate->process(nullptr, vCount); - boundCameraToManipulate->beginInputProcessing(m_nextPresentationTimestamp); - { - boundCameraToManipulate->process(nullptr, vCount); - - if (virtualEvents.size() < vCount) - virtualEvents.resize(vCount); + if (virtualEvents.size() < vCount) + virtualEvents.resize(vCount); - IGimbalController::SUpdateParameters params; - params.imguizmoEvents = { { imguizmoModel.outDeltaTRS } }; - boundCameraToManipulate->process(virtualEvents.data(), vCount, params); - } - boundCameraToManipulate->endInputProcessing(); + IGimbalController::SUpdateParameters params; + params.imguizmoEvents = { { imguizmoModel.outDeltaTRS } }; + boundCameraToManipulate->process(virtualEvents.data(), vCount, params); + } + boundCameraToManipulate->endInputProcessing(); - // I start to think controller should be able to set sensitivity to scale magnitudes of generated events - // in order for camera to not keep any magnitude scalars like move or rotation speed scales + // I start to think controller should be able to set sensitivity to scale magnitudes of generated events + // in order for camera to not keep any magnitude scalars like move or rotation speed scales - if (vCount) - { - const float pmSpeed = boundCameraToManipulate->getMoveSpeedScale(); - const float prSpeed = boundCameraToManipulate->getRotationSpeedScale(); + if (vCount) + { + const float pmSpeed = boundCameraToManipulate->getMoveSpeedScale(); + const float prSpeed = boundCameraToManipulate->getRotationSpeedScale(); - boundCameraToManipulate->setMoveSpeedScale(1); - boundCameraToManipulate->setRotationSpeedScale(1); + boundCameraToManipulate->setMoveSpeedScale(1); + boundCameraToManipulate->setRotationSpeedScale(1); - auto referenceFrame = getCastedMatrix(imguizmoModel.outTRS); - boundCameraToManipulate->manipulate({ virtualEvents.data(), vCount }, &referenceFrame); + auto referenceFrame = getCastedMatrix(imguizmoModel.outTRS); + boundCameraToManipulate->manipulate({ virtualEvents.data(), vCount }, &referenceFrame); - boundCameraToManipulate->setMoveSpeedScale(pmSpeed); - boundCameraToManipulate->setRotationSpeedScale(prSpeed); - } + boundCameraToManipulate->setMoveSpeedScale(pmSpeed); + boundCameraToManipulate->setRotationSpeedScale(prSpeed); } } - */ - } - else - { - // for scene demo model full affine transformation without limits is assumed - m_model = float32_t3x4(hlsl::transpose(imguizmoModel.outTRS)); } + */ + } + else + { + // for scene demo model full affine transformation without limits is assumed + m_model = float32_t3x4(hlsl::transpose(imguizmoModel.outTRS)); } } @@ -3361,7 +4908,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication bool enableActiveCameraMovement = false; - bool resetCursorToCenter = false; + bool resetCursorToCenter = true; struct windowControlBinding { @@ -3494,6 +5041,10 @@ class UISampleApp final : public examples::SimpleWindowedApplication size_t nextEventIndex = 0; std::vector checks; size_t nextCheckIndex = 0; + std::vector captureFrames; + size_t nextCaptureIndex = 0; + std::string capturePrefix = "script"; + system::path captureOutputDir; bool failed = false; bool summaryReported = false; bool baselineValid = false; @@ -3522,6 +5073,36 @@ class UISampleApp final : public examples::SimpleWindowedApplication uint32_t m_ciFrameCounter = 0u; system::path m_ciScreenshotPath; ScriptedInputState m_scriptedInput; + CameraControlSettings m_cameraControls; + CameraConstraintSettings m_cameraConstraints; + CUILogFormatter m_logFormatter; + std::deque m_virtualEventLog; + size_t m_virtualEventLogMax = 128u; + bool m_showHud = true; + bool m_showEventLog = false; + bool m_logAutoScroll = true; + bool m_logWrap = true; + std::vector m_presets; + std::vector m_keyframes; + CameraPlaybackState m_playback; + CTargetPoseController m_targetPoseController; + bool m_playbackAffectsAll = false; + float m_newKeyframeTime = 0.f; + char m_presetName[64] = "Preset"; + char m_presetPath[260] = "camera_presets.json"; + std::chrono::microseconds m_lastPresentationTimestamp = {}; + bool m_haveLastPresentationTimestamp = false; + double m_frameDeltaSec = 0.0; + static constexpr size_t UiMetricSamples = 96u; + std::array m_uiFrameMs = {}; + std::array m_uiInputCounts = {}; + std::array m_uiVirtualCounts = {}; + uint32_t m_uiMetricIndex = 0u; + uint32_t m_uiVirtualEventsThisFrame = 0u; + uint32_t m_uiInputEventsThisFrame = 0u; + uint32_t m_uiLastInputEvents = 0u; + uint32_t m_uiLastVirtualEvents = 0u; + float m_uiLastFrameMs = 0.0f; const bool flipGizmoY = true; diff --git a/common/include/camera/CArcballCamera.hpp b/common/include/camera/CArcballCamera.hpp new file mode 100644 index 000000000..725591aad --- /dev/null +++ b/common/include/camera/CArcballCamera.hpp @@ -0,0 +1,208 @@ +// Copyright (C) 2018-2024 - 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_ARCBALL_CAMERA_HPP_ +#define _C_ARCBALL_CAMERA_HPP_ + +#include +#include + +#include "ICamera.hpp" + +namespace nbl::hlsl +{ + +class CArcballCamera final : public ICamera +{ +public: + using base_t = ICamera; + + CArcballCamera(const float64_t3& position, const float64_t3& target) + : base_t(), m_targetPosition(target), m_distance(1.0f), m_gimbal({ .position = position, .orientation = glm::quat(glm::vec3(0.0f)) }) + { + initFromPosition(position); + applyPose(); + } + ~CArcballCamera() = default; + + const base_t::keyboard_to_virtual_events_t getKeyboardMappingPreset() const override { return m_keyboard_to_virtual_events_preset; } + const base_t::mouse_to_virtual_events_t getMouseMappingPreset() const override { return m_mouse_to_virtual_events_preset; } + const base_t::imguizmo_to_virtual_events_t getImguizmoMappingPreset() const override { return m_imguizmo_to_virtual_events_preset; } + + const typename base_t::CGimbal& getGimbal() override { return m_gimbal; } + + inline bool setDistance(float d) + { + const auto clamped = std::clamp(d, MinDistance, MaxDistance); + const bool ok = clamped == d; + m_distance = clamped; + return ok; + } + + inline void target(const float64_t3& p) { m_targetPosition = p; } + inline float64_t3 getTarget() const { return m_targetPosition; } + + inline float getDistance() { return m_distance; } + inline double getU() { return u; } + inline double getV() { return v; } + + virtual bool manipulate(std::span virtualEvents, const float64_t4x4 const* referenceFrame = nullptr) override + { + if (not virtualEvents.size() and not referenceFrame) + return false; + + auto impulse = m_gimbal.accumulate(virtualEvents); + + const double deltaYaw = impulse.dVirtualRotation.y * m_rotationSpeedScale; + const double deltaPitch = impulse.dVirtualRotation.x * m_rotationSpeedScale; + + constexpr double translateScalar = 0.01; + const double panScalar = translateScalar * m_moveSpeedScale; + const double deltaPanX = impulse.dVirtualTranslate.x * panScalar; + const double deltaPanY = impulse.dVirtualTranslate.y * panScalar; + const double deltaDistance = impulse.dVirtualTranslate.z * translateScalar; + + u += deltaYaw; + v = std::clamp(v + deltaPitch, MinPitch, MaxPitch); + m_distance = std::clamp(m_distance + static_cast(deltaDistance), MinDistance, MaxDistance); + + const auto localSpherePosition = S(u, v) * static_cast(m_distance); + const auto newForward = normalize(-localSpherePosition); + const auto newUp = normalize(Sdv(u, v)); + const auto newRight = normalize(cross(newUp, newForward)); + + if (deltaPanX != 0.0 || deltaPanY != 0.0) + m_targetPosition += newRight * deltaPanX + newUp * deltaPanY; + + return applyPose(); + } + + virtual const uint32_t getAllowedVirtualEvents() override { return AllowedVirtualEvents; } + virtual const std::string_view getIdentifier() override { return "Arcball Camera"; } + + static inline constexpr float MinDistance = 0.1f; + static inline constexpr float MaxDistance = 10000.f; + +private: + float64_t3 m_targetPosition; + float m_distance; + typename base_t::CGimbal m_gimbal; + double u = {}; + double v = {}; + + static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate | CVirtualGimbalEvent::Rotate; + static inline constexpr double MaxPitch = glm::radians(89.0); + static inline constexpr double MinPitch = -MaxPitch; + + inline float64_t3 S(double su, double sv) const + { + return float64_t3 + { + std::cos(sv) * std::cos(su), + std::cos(sv) * std::sin(su), + std::sin(sv) + }; + } + + inline float64_t3 Sdv(double su, double sv) const + { + return float64_t3 + { + -std::sin(sv) * std::cos(su), + -std::sin(sv) * std::sin(su), + std::cos(sv) + }; + } + + inline void initFromPosition(const float64_t3& position) + { + const auto offset = position - m_targetPosition; + const double dist = length(offset); + const double safeDist = std::isfinite(dist) && dist > 0.0 ? dist : static_cast(MinDistance); + m_distance = std::clamp(static_cast(safeDist), MinDistance, MaxDistance); + const auto local = offset / static_cast(m_distance); + u = std::atan2(local.y, local.x); + v = std::asin(std::clamp(local.z, -1.0, 1.0)); + v = std::clamp(v, MinPitch, MaxPitch); + } + + inline bool applyPose() + { + const auto localSpherePosition = S(u, v) * static_cast(m_distance); + const auto newPosition = localSpherePosition + m_targetPosition; + const auto newForward = normalize(-localSpherePosition); + const auto newUp = normalize(Sdv(u, v)); + const auto newRight = normalize(cross(newUp, newForward)); + const auto newOrientation = glm::quat_cast(glm::dmat3{ newRight, newUp, newForward }); + + m_gimbal.begin(); + { + m_gimbal.setPosition(newPosition); + m_gimbal.setOrientation(newOrientation); + } + m_gimbal.end(); + + const bool manipulated = bool(m_gimbal.getManipulationCounter()); + if (manipulated) + m_gimbal.updateView(); + + return manipulated; + } + + static inline const auto m_keyboard_to_virtual_events_preset = []() + { + typename base_t::keyboard_to_virtual_events_t preset; + + preset[ui::E_KEY_CODE::EKC_W] = CVirtualGimbalEvent::MoveForward; + preset[ui::E_KEY_CODE::EKC_S] = CVirtualGimbalEvent::MoveBackward; + preset[ui::E_KEY_CODE::EKC_A] = CVirtualGimbalEvent::MoveLeft; + preset[ui::E_KEY_CODE::EKC_D] = CVirtualGimbalEvent::MoveRight; + preset[ui::E_KEY_CODE::EKC_Q] = CVirtualGimbalEvent::MoveDown; + preset[ui::E_KEY_CODE::EKC_E] = CVirtualGimbalEvent::MoveUp; + preset[ui::E_KEY_CODE::EKC_I] = CVirtualGimbalEvent::TiltDown; + preset[ui::E_KEY_CODE::EKC_K] = CVirtualGimbalEvent::TiltUp; + preset[ui::E_KEY_CODE::EKC_J] = CVirtualGimbalEvent::PanLeft; + preset[ui::E_KEY_CODE::EKC_L] = CVirtualGimbalEvent::PanRight; + + return preset; + }(); + + static inline const auto m_mouse_to_virtual_events_preset = []() + { + typename base_t::mouse_to_virtual_events_t preset; + + preset[ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_X] = CVirtualGimbalEvent::PanRight; + preset[ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_X] = CVirtualGimbalEvent::PanLeft; + preset[ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_Y] = CVirtualGimbalEvent::TiltUp; + preset[ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_Y] = CVirtualGimbalEvent::TiltDown; + preset[ui::E_MOUSE_CODE::EMC_VERTICAL_POSITIVE_SCROLL] = CVirtualGimbalEvent::MoveForward; + preset[ui::E_MOUSE_CODE::EMC_HORIZONTAL_POSITIVE_SCROLL] = CVirtualGimbalEvent::MoveForward; + preset[ui::E_MOUSE_CODE::EMC_VERTICAL_NEGATIVE_SCROLL] = CVirtualGimbalEvent::MoveBackward; + preset[ui::E_MOUSE_CODE::EMC_HORIZONTAL_NEGATIVE_SCROLL] = CVirtualGimbalEvent::MoveBackward; + + return preset; + }(); + + static inline const auto m_imguizmo_to_virtual_events_preset = []() + { + typename base_t::imguizmo_to_virtual_events_t preset; + + preset[CVirtualGimbalEvent::MoveForward] = CVirtualGimbalEvent::MoveForward; + preset[CVirtualGimbalEvent::MoveBackward] = CVirtualGimbalEvent::MoveBackward; + preset[CVirtualGimbalEvent::MoveLeft] = CVirtualGimbalEvent::MoveLeft; + preset[CVirtualGimbalEvent::MoveRight] = CVirtualGimbalEvent::MoveRight; + preset[CVirtualGimbalEvent::MoveUp] = CVirtualGimbalEvent::MoveUp; + preset[CVirtualGimbalEvent::MoveDown] = CVirtualGimbalEvent::MoveDown; + preset[CVirtualGimbalEvent::TiltDown] = CVirtualGimbalEvent::TiltDown; + preset[CVirtualGimbalEvent::TiltUp] = CVirtualGimbalEvent::TiltUp; + preset[CVirtualGimbalEvent::PanLeft] = CVirtualGimbalEvent::PanLeft; + preset[CVirtualGimbalEvent::PanRight] = CVirtualGimbalEvent::PanRight; + + return preset; + }(); +}; + +} + +#endif diff --git a/common/include/camera/CFreeLockCamera.hpp b/common/include/camera/CFreeLockCamera.hpp index 2ca94c2af..628b0005e 100644 --- a/common/include/camera/CFreeLockCamera.hpp +++ b/common/include/camera/CFreeLockCamera.hpp @@ -7,63 +7,8 @@ #include "ICamera.hpp" -#include "nbl/ext/ImGui/ImGui.h" -#include "imgui/imgui_internal.h" - namespace nbl::hlsl // TODO: DIFFERENT NAMESPACE { - static inline IGimbal::VirtualImpulse sVirtualImpulse = {}; - static inline glm::mat4 sReferenceFrame = glm::mat4(1.0f); - static inline glm::quat sReferenceOrientation = {}; - - // TODO: DEBUG AND TEMPORARY - void ShowDebugWindow() - { - ImGui::Begin("Debug Window"); - - ImGui::Text("Translate deltas:"); - ImGui::Text(" x: %.3f", sVirtualImpulse.dVirtualTranslate.x); - ImGui::Text(" y: %.3f", sVirtualImpulse.dVirtualTranslate.y); - ImGui::Text(" z: %.3f", sVirtualImpulse.dVirtualTranslate.z); - - ImGui::Separator(); - - ImGui::Text("Rotation deltas:"); - ImGui::Text(" x: %.3f", sVirtualImpulse.dVirtualRotation.x); - ImGui::Text(" y: %.3f", sVirtualImpulse.dVirtualRotation.y); - ImGui::Text(" z: %.3f", sVirtualImpulse.dVirtualRotation.z); - - ImGui::Separator(); - - ImGui::Text("Scale deltas:"); - ImGui::Text(" x: %.3f", sVirtualImpulse.dVirtualScale.x); - ImGui::Text(" y: %.3f", sVirtualImpulse.dVirtualScale.y); - ImGui::Text(" z: %.3f", sVirtualImpulse.dVirtualScale.z); - - ImGui::Separator(); - - ImGui::Text("Reference frame:"); - - for (int row = 0; row < 4; ++row) - { - ImGui::Text("%.3f %.3f %.3f %.3f", - sReferenceFrame[0][row], - sReferenceFrame[1][row], - sReferenceFrame[2][row], - sReferenceFrame[3][row]); - } - - ImGui::Text("Reference orientation:"); - - ImGui::Text("%.3f %.3f %.3f %.3f", - sReferenceOrientation.x, - sReferenceOrientation.y, - sReferenceOrientation.z, - sReferenceOrientation.w); - - ImGui::End(); - } - // Free Lock Camera class CFreeCamera final : public ICamera { @@ -96,14 +41,6 @@ class CFreeCamera final : public ICamera bool manipulated = true; - // TODO: DEBUG AND TEMPORARY - { - sVirtualImpulse = impulse; - auto cast = getCastedMatrix(reference.frame);; - memcpy(&sReferenceFrame, &cast, sizeof(sReferenceFrame)); - sReferenceOrientation = reference.orientation; - } - m_gimbal.begin(); { glm::quat pitch = glm::angleAxis(impulse.dVirtualRotation.x, glm::vec3(reference.frame[0])); diff --git a/common/include/camera/COrbitCamera.hpp b/common/include/camera/COrbitCamera.hpp index 24b0e0b9d..cafcb8a85 100644 --- a/common/include/camera/COrbitCamera.hpp +++ b/common/include/camera/COrbitCamera.hpp @@ -1,6 +1,8 @@ #ifndef _C_ORBIT_CAMERA_HPP_ #define _C_ORBIT_CAMERA_HPP_ +#include +#include #include "ICamera.hpp" namespace nbl::hlsl @@ -36,6 +38,11 @@ class COrbitCamera final : public ICamera m_targetPosition = p; } + inline float64_t3 getTarget() const + { + return m_targetPosition; + } + virtual bool manipulate(std::span virtualEvents, const float64_t4x4 const* referenceFrame = nullptr) override { // TODO: it must work differently, we should take another gimbal to control target diff --git a/common/include/camera/CTargetPoseController.hpp b/common/include/camera/CTargetPoseController.hpp new file mode 100644 index 000000000..0d59eedbf --- /dev/null +++ b/common/include/camera/CTargetPoseController.hpp @@ -0,0 +1,219 @@ +#ifndef _C_TARGET_POSE_CONTROLLER_HPP_ +#define _C_TARGET_POSE_CONTROLLER_HPP_ + +#include +#include +#include +#include + +#include "ICamera.hpp" +#include "CFPSCamera.hpp" +#include "CFreeLockCamera.hpp" +#include "COrbitCamera.hpp" +#include "CArcballCamera.hpp" +#include "CTurntableCamera.hpp" +#include "glm/glm/gtc/quaternion.hpp" + +namespace nbl::hlsl +{ + +struct CTargetPose +{ + float64_t3 position = float64_t3(0.0); + glm::quat orientation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f); + bool hasDistance = false; + float distance = 0.f; + bool hasOrbitState = false; + double orbitU = 0.0; + double orbitV = 0.0; + float orbitDistance = 0.f; +}; + +class CTargetPoseController +{ +public: + bool buildEvents(ICamera* camera, const CTargetPose& target, std::vector& out) const + { + out.clear(); + if (!camera) + return false; + + if (auto* orbit = dynamic_cast(camera)) + return buildOrbitEvents(orbit, target, out); + if (auto* arcball = dynamic_cast(camera)) + return buildOrbitEvents(arcball, target, out); + if (auto* turntable = dynamic_cast(camera)) + return buildOrbitEvents(turntable, target, out); + + return buildFreeEvents(camera, target, out); + } + + bool apply(ICamera* camera, const CTargetPose& target) const + { + std::vector events; + if (!buildEvents(camera, target, events)) + return false; + return camera->manipulate({ events.data(), events.size() }); + } + +private: + static constexpr double Pi = 3.14159265358979323846; + static constexpr double HalfPi = 0.5 * Pi; + + inline double wrapAngleRad(double angle) const + { + while (angle > Pi) + angle -= 2.0 * Pi; + while (angle < -Pi) + angle += 2.0 * Pi; + return angle; + } + + inline void appendSignedEvent(std::vector& events, double value, + CVirtualGimbalEvent::VirtualEventType positive, CVirtualGimbalEvent::VirtualEventType negative) const + { + if (value == 0.0) + return; + auto& ev = events.emplace_back(); + ev.type = (value > 0.0) ? positive : negative; + ev.magnitude = std::abs(value); + } + + inline std::pair computePitchYawFromOrientation(const glm::quat& orientation) const + { + const auto mat = glm::mat3_cast(orientation); + const auto forward = float64_t3(mat[2][0], mat[2][1], mat[2][2]); + const double pitch = std::atan2(std::sqrt(forward.x * forward.x + forward.z * forward.z), forward.y) - HalfPi; + const double yaw = std::atan2(forward.x, forward.z); + return { pitch, yaw }; + } + + inline float64_t3 extractYawPitchRollYXZ(const glm::quat& delta) const + { + const auto m = glm::mat3_cast(delta); + const double sp = std::clamp(-static_cast(m[1][2]), -1.0, 1.0); + const double pitch = std::asin(sp); + const double cp = std::cos(pitch); + + double yaw = 0.0; + double roll = 0.0; + if (std::abs(cp) > 1e-6) + { + yaw = std::atan2(static_cast(m[0][2]), static_cast(m[2][2])); + roll = std::atan2(static_cast(m[1][0]), static_cast(m[1][1])); + } + else + { + yaw = std::atan2(-static_cast(m[2][0]), static_cast(m[0][0])); + roll = 0.0; + } + + return float64_t3(pitch, yaw, roll); + } + + inline bool computeOrbitStateFromPositionTarget(const float64_t3& position, const float64_t3& target, + double& outU, double& outV, float& outDistance, float minDistance, float maxDistance) const + { + const auto localSpherePosition = position - target; + const double dist = length(localSpherePosition); + if (!std::isfinite(dist)) + return false; + + const double clamped = std::clamp(dist, static_cast(minDistance), static_cast(maxDistance)); + outDistance = static_cast(clamped); + + if (clamped > 0.0) + { + const auto localUnit = localSpherePosition / clamped; + outU = std::atan2(localUnit.y, localUnit.x); + outV = std::asin(localUnit.z); + } + + return true; + } + + template + inline bool buildOrbitEvents(T* orbit, const CTargetPose& target, std::vector& out) const + { + double targetU = orbit->getU(); + double targetV = orbit->getV(); + float targetDistance = orbit->getDistance(); + + if (target.hasOrbitState) + { + targetU = target.orbitU; + targetV = target.orbitV; + targetDistance = target.orbitDistance; + } + else + { + const auto orbitTarget = orbit->getTarget(); + if (!computeOrbitStateFromPositionTarget(target.position, orbitTarget, targetU, targetV, targetDistance, T::MinDistance, T::MaxDistance)) + return false; + } + + targetDistance = std::clamp(targetDistance, T::MinDistance, T::MaxDistance); + + const double deltaU = targetU - orbit->getU(); + const double deltaV = targetV - orbit->getV(); + const double deltaDistance = static_cast(targetDistance - orbit->getDistance()); + + const double moveScale = orbit->getMoveSpeedScale(); + const double moveDenom = 0.01 * (moveScale == 0.0 ? 1.0 : moveScale); + + appendSignedEvent(out, deltaV / moveDenom, CVirtualGimbalEvent::MoveRight, CVirtualGimbalEvent::MoveLeft); + appendSignedEvent(out, deltaU / moveDenom, CVirtualGimbalEvent::MoveUp, CVirtualGimbalEvent::MoveDown); + appendSignedEvent(out, deltaDistance / 0.01, CVirtualGimbalEvent::MoveForward, CVirtualGimbalEvent::MoveBackward); + + return !out.empty(); + } + + inline bool buildFreeEvents(ICamera* camera, const CTargetPose& target, std::vector& out) const + { + const auto& gimbal = camera->getGimbal(); + const auto currentPos = gimbal.getPosition(); + const auto right = gimbal.getXAxis(); + const auto up = gimbal.getYAxis(); + const auto forward = gimbal.getZAxis(); + + const auto deltaWorld = target.position - currentPos; + const float64_t3 localDelta( + glm::dot(deltaWorld, right), + glm::dot(deltaWorld, up), + glm::dot(deltaWorld, forward)); + + appendSignedEvent(out, localDelta.x, CVirtualGimbalEvent::MoveRight, CVirtualGimbalEvent::MoveLeft); + appendSignedEvent(out, localDelta.y, CVirtualGimbalEvent::MoveUp, CVirtualGimbalEvent::MoveDown); + appendSignedEvent(out, localDelta.z, CVirtualGimbalEvent::MoveForward, CVirtualGimbalEvent::MoveBackward); + + if (auto* fps = dynamic_cast(camera)) + { + const auto [curPitch, curYaw] = computePitchYawFromOrientation(gimbal.getOrientation()); + const auto [tgtPitch, tgtYaw] = computePitchYawFromOrientation(target.orientation); + + const double rotScale = fps->getRotationSpeedScale(); + const double invScale = rotScale == 0.0 ? 1.0 : (1.0 / rotScale); + + const double deltaPitch = wrapAngleRad(tgtPitch - curPitch) * invScale; + const double deltaYaw = wrapAngleRad(tgtYaw - curYaw) * invScale; + + appendSignedEvent(out, deltaPitch, CVirtualGimbalEvent::TiltUp, CVirtualGimbalEvent::TiltDown); + appendSignedEvent(out, deltaYaw, CVirtualGimbalEvent::PanRight, CVirtualGimbalEvent::PanLeft); + } + else if (auto* freeCam = dynamic_cast(camera)) + { + const auto deltaQuat = glm::normalize(target.orientation) * glm::inverse(gimbal.getOrientation()); + const auto angles = extractYawPitchRollYXZ(deltaQuat); + + appendSignedEvent(out, angles.x, CVirtualGimbalEvent::TiltUp, CVirtualGimbalEvent::TiltDown); + appendSignedEvent(out, angles.y, CVirtualGimbalEvent::PanRight, CVirtualGimbalEvent::PanLeft); + appendSignedEvent(out, angles.z, CVirtualGimbalEvent::RollRight, CVirtualGimbalEvent::RollLeft); + } + + return !out.empty(); + } +}; + +} // namespace nbl::hlsl + +#endif // _C_TARGET_POSE_CONTROLLER_HPP_ diff --git a/common/include/camera/CTurntableCamera.hpp b/common/include/camera/CTurntableCamera.hpp new file mode 100644 index 000000000..ca1fc9b9c --- /dev/null +++ b/common/include/camera/CTurntableCamera.hpp @@ -0,0 +1,195 @@ +// Copyright (C) 2018-2024 - 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_TURNTABLE_CAMERA_HPP_ +#define _C_TURNTABLE_CAMERA_HPP_ + +#include +#include + +#include "ICamera.hpp" + +namespace nbl::hlsl +{ + +class CTurntableCamera final : public ICamera +{ +public: + using base_t = ICamera; + + CTurntableCamera(const float64_t3& position, const float64_t3& target) + : base_t(), m_targetPosition(target), m_distance(1.0f), m_gimbal({ .position = position, .orientation = glm::quat(glm::vec3(0.0f)) }) + { + initFromPosition(position); + applyPose(); + } + ~CTurntableCamera() = default; + + const base_t::keyboard_to_virtual_events_t getKeyboardMappingPreset() const override { return m_keyboard_to_virtual_events_preset; } + const base_t::mouse_to_virtual_events_t getMouseMappingPreset() const override { return m_mouse_to_virtual_events_preset; } + const base_t::imguizmo_to_virtual_events_t getImguizmoMappingPreset() const override { return m_imguizmo_to_virtual_events_preset; } + + const typename base_t::CGimbal& getGimbal() override { return m_gimbal; } + + inline bool setDistance(float d) + { + const auto clamped = std::clamp(d, MinDistance, MaxDistance); + const bool ok = clamped == d; + m_distance = clamped; + return ok; + } + + inline void target(const float64_t3& p) { m_targetPosition = p; } + inline float64_t3 getTarget() const { return m_targetPosition; } + + inline float getDistance() { return m_distance; } + inline double getU() { return u; } + inline double getV() { return v; } + + virtual bool manipulate(std::span virtualEvents, const float64_t4x4 const* referenceFrame = nullptr) override + { + if (not virtualEvents.size() and not referenceFrame) + return false; + + auto impulse = m_gimbal.accumulate(virtualEvents); + + const double deltaYaw = impulse.dVirtualRotation.y * m_rotationSpeedScale; + const double deltaPitch = impulse.dVirtualRotation.x * m_rotationSpeedScale; + + constexpr double translateScalar = 0.01; + const double deltaDistance = impulse.dVirtualTranslate.z * translateScalar; + + u += deltaYaw; + v = std::clamp(v + deltaPitch, MinPitch, MaxPitch); + m_distance = std::clamp(m_distance + static_cast(deltaDistance), MinDistance, MaxDistance); + + return applyPose(); + } + + virtual const uint32_t getAllowedVirtualEvents() override { return AllowedVirtualEvents; } + virtual const std::string_view getIdentifier() override { return "Turntable Camera"; } + + static inline constexpr float MinDistance = 0.1f; + static inline constexpr float MaxDistance = 10000.f; + +private: + float64_t3 m_targetPosition; + float m_distance; + typename base_t::CGimbal m_gimbal; + double u = {}; + double v = {}; + + static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate | CVirtualGimbalEvent::Rotate; + static inline constexpr double MaxPitch = glm::radians(89.0); + static inline constexpr double MinPitch = -MaxPitch; + + inline float64_t3 S(double su, double sv) const + { + return float64_t3 + { + std::cos(sv) * std::cos(su), + std::cos(sv) * std::sin(su), + std::sin(sv) + }; + } + + inline float64_t3 Sdv(double su, double sv) const + { + return float64_t3 + { + -std::sin(sv) * std::cos(su), + -std::sin(sv) * std::sin(su), + std::cos(sv) + }; + } + + inline void initFromPosition(const float64_t3& position) + { + const auto offset = position - m_targetPosition; + const double dist = length(offset); + const double safeDist = std::isfinite(dist) && dist > 0.0 ? dist : static_cast(MinDistance); + m_distance = std::clamp(static_cast(safeDist), MinDistance, MaxDistance); + const auto local = offset / static_cast(m_distance); + u = std::atan2(local.y, local.x); + v = std::asin(std::clamp(local.z, -1.0, 1.0)); + v = std::clamp(v, MinPitch, MaxPitch); + } + + inline bool applyPose() + { + const auto localSpherePosition = S(u, v) * static_cast(m_distance); + const auto newPosition = localSpherePosition + m_targetPosition; + const auto newForward = normalize(-localSpherePosition); + const auto newUp = normalize(Sdv(u, v)); + const auto newRight = normalize(cross(newUp, newForward)); + const auto newOrientation = glm::quat_cast(glm::dmat3{ newRight, newUp, newForward }); + + m_gimbal.begin(); + { + m_gimbal.setPosition(newPosition); + m_gimbal.setOrientation(newOrientation); + } + m_gimbal.end(); + + const bool manipulated = bool(m_gimbal.getManipulationCounter()); + if (manipulated) + m_gimbal.updateView(); + + return manipulated; + } + + static inline const auto m_keyboard_to_virtual_events_preset = []() + { + typename base_t::keyboard_to_virtual_events_t preset; + + preset[ui::E_KEY_CODE::EKC_W] = CVirtualGimbalEvent::MoveForward; + preset[ui::E_KEY_CODE::EKC_S] = CVirtualGimbalEvent::MoveBackward; + preset[ui::E_KEY_CODE::EKC_A] = CVirtualGimbalEvent::PanLeft; + preset[ui::E_KEY_CODE::EKC_D] = CVirtualGimbalEvent::PanRight; + preset[ui::E_KEY_CODE::EKC_I] = CVirtualGimbalEvent::TiltDown; + preset[ui::E_KEY_CODE::EKC_K] = CVirtualGimbalEvent::TiltUp; + preset[ui::E_KEY_CODE::EKC_J] = CVirtualGimbalEvent::PanLeft; + preset[ui::E_KEY_CODE::EKC_L] = CVirtualGimbalEvent::PanRight; + + return preset; + }(); + + static inline const auto m_mouse_to_virtual_events_preset = []() + { + typename base_t::mouse_to_virtual_events_t preset; + + preset[ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_X] = CVirtualGimbalEvent::PanRight; + preset[ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_X] = CVirtualGimbalEvent::PanLeft; + preset[ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_Y] = CVirtualGimbalEvent::TiltUp; + preset[ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_Y] = CVirtualGimbalEvent::TiltDown; + preset[ui::E_MOUSE_CODE::EMC_VERTICAL_POSITIVE_SCROLL] = CVirtualGimbalEvent::MoveForward; + preset[ui::E_MOUSE_CODE::EMC_HORIZONTAL_POSITIVE_SCROLL] = CVirtualGimbalEvent::MoveForward; + preset[ui::E_MOUSE_CODE::EMC_VERTICAL_NEGATIVE_SCROLL] = CVirtualGimbalEvent::MoveBackward; + preset[ui::E_MOUSE_CODE::EMC_HORIZONTAL_NEGATIVE_SCROLL] = CVirtualGimbalEvent::MoveBackward; + + return preset; + }(); + + static inline const auto m_imguizmo_to_virtual_events_preset = []() + { + typename base_t::imguizmo_to_virtual_events_t preset; + + preset[CVirtualGimbalEvent::MoveForward] = CVirtualGimbalEvent::MoveForward; + preset[CVirtualGimbalEvent::MoveBackward] = CVirtualGimbalEvent::MoveBackward; + preset[CVirtualGimbalEvent::MoveLeft] = CVirtualGimbalEvent::MoveLeft; + preset[CVirtualGimbalEvent::MoveRight] = CVirtualGimbalEvent::MoveRight; + preset[CVirtualGimbalEvent::MoveUp] = CVirtualGimbalEvent::MoveUp; + preset[CVirtualGimbalEvent::MoveDown] = CVirtualGimbalEvent::MoveDown; + preset[CVirtualGimbalEvent::TiltDown] = CVirtualGimbalEvent::TiltDown; + preset[CVirtualGimbalEvent::TiltUp] = CVirtualGimbalEvent::TiltUp; + preset[CVirtualGimbalEvent::PanLeft] = CVirtualGimbalEvent::PanLeft; + preset[CVirtualGimbalEvent::PanRight] = CVirtualGimbalEvent::PanRight; + + return preset; + }(); +}; + +} + +#endif From fc6dc1c7fa9663662378a2e425f17dbb3842d35b Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 7 Feb 2026 10:37:05 +0100 Subject: [PATCH 097/205] Add camera workflows and continuity tests --- 61_UI/CMakeLists.txt | 24 + 61_UI/README.md | 119 +- 61_UI/app_resources/cameras.json | 155 +- 61_UI/app_resources/cameraz_continuity.json | 49486 ++++++++++++++++ 61_UI/app_resources/cameraz_smoke_all.json | 532 + 61_UI/include/common.hpp | 14 + 61_UI/main.cpp | 951 +- common/include/camera/CArcballCamera.hpp | 102 +- common/include/camera/CChaseCamera.hpp | 133 + common/include/camera/CDollyCamera.hpp | 117 + common/include/camera/CDollyZoomCamera.hpp | 122 + common/include/camera/CFPSCamera.hpp | 4 +- common/include/camera/CFreeLockCamera.hpp | 4 +- common/include/camera/CIsometricCamera.hpp | 111 + common/include/camera/COrbitCamera.hpp | 124 +- common/include/camera/CPathCamera.hpp | 120 + .../include/camera/CSphericalTargetCamera.hpp | 122 + .../include/camera/CTargetPoseController.hpp | 10 +- common/include/camera/CTopDownCamera.hpp | 114 + common/include/camera/CTurntableCamera.hpp | 95 +- common/include/camera/ICamera.hpp | 4 +- common/include/camera/IGimbal.hpp | 2 +- 22 files changed, 52000 insertions(+), 465 deletions(-) create mode 100644 61_UI/app_resources/cameraz_continuity.json create mode 100644 61_UI/app_resources/cameraz_smoke_all.json create mode 100644 common/include/camera/CChaseCamera.hpp create mode 100644 common/include/camera/CDollyCamera.hpp create mode 100644 common/include/camera/CDollyZoomCamera.hpp create mode 100644 common/include/camera/CIsometricCamera.hpp create mode 100644 common/include/camera/CPathCamera.hpp create mode 100644 common/include/camera/CSphericalTargetCamera.hpp create mode 100644 common/include/camera/CTopDownCamera.hpp diff --git a/61_UI/CMakeLists.txt b/61_UI/CMakeLists.txt index bbcdacf08..e49af2205 100644 --- a/61_UI/CMakeLists.txt +++ b/61_UI/CMakeLists.txt @@ -79,4 +79,28 @@ if(NBL_BUILD_IMGUI) add_dependencies(${EXECUTABLE_NAME} argparse) target_include_directories(${EXECUTABLE_NAME} PUBLIC $) + + enable_testing() + + set(CAMERA_SMOKE_SCRIPT "${CMAKE_CURRENT_SOURCE_DIR}/app_resources/cameraz_smoke_all.json") + set(CAMERA_CONTINUITY_SCRIPT "${CMAKE_CURRENT_SOURCE_DIR}/app_resources/cameraz_continuity.json") + + add_test(NAME NBL_61_UI_CAMERA_SMOKE + COMMAND "$" --ci --script "${CAMERA_SMOKE_SCRIPT}" --script-log + WORKING_DIRECTORY "$" + COMMAND_EXPAND_LISTS + ) + add_test(NAME NBL_61_UI_CAMERA_CONTINUITY + COMMAND "$" --ci --script "${CAMERA_CONTINUITY_SCRIPT}" --script-visual-debug + 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..53dffefac 100644 --- a/61_UI/README.md +++ b/61_UI/README.md @@ -1,2 +1,119 @@ -https://github.com/user-attachments/assets/6f779700-e6d4-4e11-95fb-7a7fddc47255 +# 61_UI Cameraz +This example demonstrates interactive camera control in the ImGui-based UI sample. +It contains a scripted-input harness that can drive camera actions in CI and validate behavior with frame-based checks. + +## Cameras in this scene + +`app_resources/cameras.json` defines 11 camera types: + +- FPS +- Orbit +- Free +- Arcball +- Turntable +- TopDown +- Isometric +- Chase +- Dolly +- DollyZoom +- Path + +Each planar uses one of the configured controller mappings and can be switched at runtime by scripted `action` events. + +## Short math context + +Each camera is represented by a gimbal pose `(R, p)` and produces a view matrix from camera basis vectors and position. + +For a world-space point `x_w`, clip-space projection is: + +`x_c = P * V * x_w` + +where: + +- `V` is camera view transform +- `P` is selected planar projection (perspective or orthographic) + +The scripted smoothness checks use per-step deltas: + +- position delta: `d_pos = ||p_t - p_{t-1}||` +- rotation delta: `d_rot = max(angleDiff(euler_t, euler_{t-1}))` + +and validate them against configured `[min, max]` ranges. + +## Scripted test assets + +- `app_resources/cameraz_smoke_all.json` +- `app_resources/cameraz_continuity.json` + +### Smoke script + +Goal: verify that every camera can be selected and responds to scripted input. + +Per camera sequence: + +1. select planar +2. store `baseline` +3. apply one `imguizmo` movement step +4. run `gimbal_step` check + +PASS means each camera produced a finite and expected movement delta. +FAIL means missing movement, out-of-range movement, invalid state, or missing reference. + +### Continuity script + +Goal: verify smooth frame-to-frame behavior (no visible teleport-like jumps). + +Per camera sequence: + +1. select planar +2. store `baseline` +3. hold camera segment for `3.0 s` (`180` frames at `60 FPS`) +4. apply 3 small `imguizmo` movement steps spread over the segment +5. run `gimbal_step` after each step + +PASS means every step delta stayed inside configured continuity ranges. +FAIL means any step exceeded max range or failed minimum expected motion. + +Continuity also supports visual debug mode: + +- large top-center overlay with active camera type and segment progress +- fixed frame pacing (`visual_debug_target_fps`) so camera time is human-readable + +## Build and run + +Build this example first: + +```powershell +cmake --build build_vs2026/examples_tests/61_UI --config Debug --target 61_ui -- /m:1 +``` + +Run manually from executable directory: + +```powershell +./61_ui_d.exe --script app_resources/cameraz_smoke_all.json --script-log +``` + +For CI-style exit with automatic screenshot/capture behavior: + +```powershell +./61_ui_d.exe --ci --script app_resources/cameraz_continuity.json --script-log --script-visual-debug +``` + +Notes: + +- continuity visual run takes about `33 s` (`11` cameras × `3 s`) +- if `visual_debug` is present in json, CLI flag is optional + +## CTest entries + +`CMakeLists.txt` registers two dedicated tests: + +- `NBL_61_UI_CAMERA_SMOKE` +- `NBL_61_UI_CAMERA_CONTINUITY` + +Run from `build_vs2026/examples_tests`: + +```powershell +ctest -C Debug --output-on-failure -R NBL_61_UI_CAMERA_ +``` diff --git a/61_UI/app_resources/cameras.json b/61_UI/app_resources/cameras.json index 418be8ffa..6f078ec22 100644 --- a/61_UI/app_resources/cameras.json +++ b/61_UI/app_resources/cameras.json @@ -7,22 +7,53 @@ }, { "type": "Orbit", - "position": [-2.017, 0.386, 0.684], + "position": [9.000, 1.500, 0.000], "target": [0, 0, 0] }, { "type": "Free", - "position": [2.116, 0.826, 1.152], + "position": [0.000, 1.500, -9.000], "orientation": [0.095, -0.835, 0.152, 0.521] }, { "type": "Arcball", - "position": [1.500, 1.200, -1.800], + "position": [0.000, 1.500, 9.000], "target": [0, 0, 0] }, { "type": "Turntable", - "position": [-1.400, 1.100, 1.600], + "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": "Path", + "position": [12.000, 1.500, 12.000], "target": [0, 0, 0] } ], @@ -43,97 +74,106 @@ "viewports": [ { "projection": 0, - "controllers": { - "keyboard": 0, - "mouse": 0 - } + "controllers": { "keyboard": 0, "mouse": 0 } }, { "projection": 1, - "controllers": { - "keyboard": 1, - "mouse": 0 - } + "controllers": { "keyboard": 0, "mouse": 0 } }, { "projection": 0, - "controllers": { - "keyboard": 2, - "mouse": 0 - } + "controllers": { "keyboard": 3, "mouse": 1 } }, { "projection": 1, - "controllers": { - "keyboard": 1, - "mouse": 0 - } + "controllers": { "keyboard": 3, "mouse": 1 } }, { "projection": 0, - "controllers": { - "keyboard": 3, - "mouse": 1 - } + "controllers": { "keyboard": 2, "mouse": 0 } }, { "projection": 1, - "controllers": { - "keyboard": 3, - "mouse": 1 - } + "controllers": { "keyboard": 2, "mouse": 0 } }, { "projection": 0, - "controllers": { - "keyboard": 2, - "mouse": 0 - } + "controllers": { "keyboard": 2, "mouse": 0 } }, { "projection": 1, - "controllers": { - "keyboard": 2, - "mouse": 0 - } + "controllers": { "keyboard": 2, "mouse": 0 } }, { "projection": 0, - "controllers": { - "keyboard": 2, - "mouse": 0 - } + "controllers": { "keyboard": 2, "mouse": 0 } }, { "projection": 1, - "controllers": { - "keyboard": 2, - "mouse": 0 - } - } - ], - "planars": [ + "controllers": { "keyboard": 2, "mouse": 0 } + }, + { + "projection": 0, + "controllers": { "keyboard": 0, "mouse": 1 } + }, { - "camera": 0, - "viewports": [0, 1] + "projection": 1, + "controllers": { "keyboard": 0, "mouse": 1 } }, { - "camera": 1, - "viewports": [4, 5] + "projection": 0, + "controllers": { "keyboard": 1, "mouse": 1 } }, { - "camera": 2, - "viewports": [2, 3] + "projection": 1, + "controllers": { "keyboard": 1, "mouse": 1 } }, { - "camera": 3, - "viewports": [6, 7] + "projection": 0, + "controllers": { "keyboard": 2, "mouse": 0 } }, { - "camera": 4, - "viewports": [8, 9] + "projection": 1, + "controllers": { "keyboard": 2, "mouse": 0 } + }, + { + "projection": 0, + "controllers": { "keyboard": 2, "mouse": 0 } + }, + { + "projection": 1, + "controllers": { "keyboard": 2, "mouse": 0 } + }, + { + "projection": 0, + "controllers": { "keyboard": 3, "mouse": 1 } + }, + { + "projection": 1, + "controllers": { "keyboard": 3, "mouse": 1 } + }, + { + "projection": 0, + "controllers": { "keyboard": 2, "mouse": 1 } + }, + { + "projection": 1, + "controllers": { "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] } + ], "controllers": { "keyboard": [ { @@ -207,4 +247,3 @@ ] } } - diff --git a/61_UI/app_resources/cameraz_continuity.json b/61_UI/app_resources/cameraz_continuity.json new file mode 100644 index 000000000..b086cabd3 --- /dev/null +++ b/61_UI/app_resources/cameraz_continuity.json @@ -0,0 +1,49486 @@ +{ + "enabled": true, + "log": false, + "exclusive": true, + "hard_fail": true, + "enableActiveCameraMovement": true, + "visual_debug": true, + "visual_debug_target_fps": 60.0, + "visual_debug_hold_seconds": 3.0, + "capture_prefix": "camera_continuity", + "capture_frames": [ + 1979 + ], + "events": [ + { + "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": 1, + "type": "imguizmo", + "translation": [ + 0.04, + 0.0, + 0.06 + ], + "rotation_deg": [ + 0.0, + 12.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 2, + "type": "imguizmo", + "translation": [ + 0.039981162362802226, + 0.0015998235910796123, + 0.06 + ], + "rotation_deg": [ + 0.16939902674809185, + 11.995514848286243, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 3, + "type": "imguizmo", + "translation": [ + 0.039926179931534925, + 0.003063691054215668, + 0.06 + ], + "rotation_deg": [ + 0.33858700340309256, + 11.9820649810855, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 4, + "type": "imguizmo", + "translation": [ + 0.039836507657558584, + 0.00452253423361087, + 0.06 + ], + "rotation_deg": [ + 0.5073531428140463, + 11.95966715525747, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 5, + "type": "imguizmo", + "translation": [ + 0.03971214634535241, + 0.005958027265265972, + 0.06 + ], + "rotation_deg": [ + 0.6754871833866745, + 11.928349275704072, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 6, + "type": "imguizmo", + "translation": [ + 0.03955325980690609, + 0.007363903233557683, + 0.06 + ], + "rotation_deg": [ + 0.8427796510431396, + 11.888150360603401, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 7, + "type": "imguizmo", + "translation": [ + 0.039360045285198875, + 0.008733087364053448, + 0.06 + ], + "rotation_deg": [ + 1.009022120200661, + 11.839120492797944, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 8, + "type": "imguizmo", + "translation": [ + 0.03913274355825398, + 0.010058764111973288, + 0.06 + ], + "rotation_deg": [ + 1.1740074734438395, + 11.781320757397596, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 9, + "type": "imguizmo", + "translation": [ + 0.03887163781117556, + 0.011334328569507748, + 0.06 + ], + "rotation_deg": [ + 1.3375301595671623, + 11.714823165675211, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 10, + "type": "imguizmo", + "translation": [ + 0.038577053349573995, + 0.012553425974927018, + 0.06 + ], + "rotation_deg": [ + 1.4993864496662117, + 11.639710565349535, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 11, + "type": "imguizmo", + "translation": [ + 0.03824935718897653, + 0.01370998284338098, + 0.06 + ], + "rotation_deg": [ + 1.6593746909585103, + 11.556076537367254, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 12, + "type": "imguizmo", + "translation": [ + 0.03788895759799559, + 0.01479823726695948, + 0.06 + ], + "rotation_deg": [ + 1.8172955580177759, + 11.464025279312793, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 13, + "type": "imguizmo", + "translation": [ + 0.03749630358964297, + 0.015812767616846555, + 0.06 + ], + "rotation_deg": [ + 1.9729523011085845, + 11.363671475591103, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 14, + "type": "imguizmo", + "translation": [ + 0.037071884361918245, + 0.01674851955387298, + 0.06 + ], + "rotation_deg": [ + 2.126150991312038, + 11.255140154545167, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 15, + "type": "imguizmo", + "translation": [ + 0.036616228688328804, + 0.017600831208958112, + 0.06 + ], + "rotation_deg": [ + 2.2767007621370507, + 11.138566532686259, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 16, + "type": "imguizmo", + "translation": [ + 0.0361299042591039, + 0.018365456408308924, + 0.06 + ], + "rotation_deg": [ + 2.424414047316227, + 11.014095846231006, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 17, + "type": "imguizmo", + "translation": [ + 0.0356135169739233, + 0.019038585827644047, + 0.06 + ], + "rotation_deg": [ + 2.569106814490077, + 10.881883170155152, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 18, + "type": "imguizmo", + "translation": [ + 0.03506771018704176, + 0.0196168659700558, + 0.06 + ], + "rotation_deg": [ + 2.7105987944884165, + 10.74209322498944, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 19, + "type": "imguizmo", + "translation": [ + 0.03449316390574969, + 0.02009741587296306, + 0.06 + ], + "rotation_deg": [ + 2.84871370592331, + 10.594900171598347, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 20, + "type": "imguizmo", + "translation": [ + 0.03389059394316883, + 0.02047784146092194, + 0.06 + ], + "rotation_deg": [ + 2.983279474813725, + 10.44048739419734, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 21, + "type": "imguizmo", + "translation": [ + 0.03326075102643814, + 0.02075624747278922, + 0.06 + ], + "rotation_deg": [ + 3.1141284489682852, + 10.279047271878971, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 22, + "type": "imguizmo", + "translation": [ + 0.03260441986140135, + 0.02093124690381801, + 0.06 + ], + "rotation_deg": [ + 3.2410976068590265, + 10.110780938932484, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 23, + "type": "imguizmo", + "translation": [ + 0.03192241815496113, + 0.02100196791564568, + 0.06 + ], + "rotation_deg": [ + 3.3640287607259185, + 9.93589803425554, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 24, + "type": "imguizmo", + "translation": [ + 0.031215595596318157, + 0.020968058179748672, + 0.06 + ], + "rotation_deg": [ + 3.4827687536591174, + 9.754616440170258, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 25, + "type": "imguizmo", + "translation": [ + 0.030484832798364173, + 0.020829686632725518, + 0.06 + ], + "rotation_deg": [ + 3.5971696504134014, + 9.567162010968968, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 26, + "type": "imguizmo", + "translation": [ + 0.02973104020054793, + 0.02058754263466318, + 0.06 + ], + "rotation_deg": [ + 3.7070889217170673, + 9.373768291527881, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 27, + "type": "imguizmo", + "translation": [ + 0.02895515693458109, + 0.020242832534779792, + 0.06 + ], + "rotation_deg": [ + 3.812389621845653, + 9.174676226339276, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 28, + "type": "imguizmo", + "translation": [ + 0.028158149654396978, + 0.019797273661453535, + 0.06 + ], + "rotation_deg": [ + 3.9129405592392583, + 8.970133859324644, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 29, + "type": "imguizmo", + "translation": [ + 0.027341011331820222, + 0.019253085766579026, + 0.06 + ], + "rotation_deg": [ + 4.008616459950891, + 8.760396024802862, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 30, + "type": "imguizmo", + "translation": [ + 0.026504760019447435, + 0.018612979966875037, + 0.06 + ], + "rotation_deg": [ + 4.099298123722207, + 8.545724029998356, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 31, + "type": "imguizmo", + "translation": [ + 0.025650437582280528, + 0.017880145237237417, + 0.06 + ], + "rotation_deg": [ + 4.184872572492186, + 8.326385329484848, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 32, + "type": "imguizmo", + "translation": [ + 0.02477910839969259, + 0.01705823252342649, + 0.06 + ], + "rotation_deg": [ + 4.26523319115373, + 8.102653191970255, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 33, + "type": "imguizmo", + "translation": [ + 0.02389185803934378, + 0.016151336553239016, + 0.06 + ], + "rotation_deg": [ + 4.340279860382808, + 7.874806359837911, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 34, + "type": "imguizmo", + "translation": [ + 0.022989791904699155, + 0.015163975436780198, + 0.06 + ], + "rotation_deg": [ + 4.40991908137467, + 7.643128701868261, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 35, + "type": "imguizmo", + "translation": [ + 0.022074033857833648, + 0.01410106815746648, + 0.06 + ], + "rotation_deg": [ + 4.4740640923317105, + 7.407908859573719, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 36, + "type": "imguizmo", + "translation": [ + 0.021145724819239826, + 0.012967910065897524, + 0.06 + ], + "rotation_deg": [ + 4.532634976557868, + 7.169439887587275, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 37, + "type": "imguizmo", + "translation": [ + 0.02020602134638308, + 0.011770146498685968, + 0.06 + ], + "rotation_deg": [ + 4.585558762024875, + 6.928018888552929, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 38, + "type": "imguizmo", + "translation": [ + 0.019256094192774978, + 0.010513744653674292, + 0.06 + ], + "rotation_deg": [ + 4.632769512286319, + 6.683946642972776, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 39, + "type": "imguizmo", + "translation": [ + 0.018297126849360242, + 0.009204963861655441, + 0.06 + ], + "rotation_deg": [ + 4.674208408626242, + 6.43752723447197, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 40, + "type": "imguizmo", + "translation": [ + 0.017330314070034346, + 0.007850324402701557, + 0.06 + ], + "rotation_deg": [ + 4.709823823339943, + 6.189067670948381, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 41, + "type": "imguizmo", + "translation": [ + 0.01635686038312898, + 0.006456575022456723, + 0.06 + ], + "rotation_deg": [ + 4.739571384055658, + 5.938877502078992, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 42, + "type": "imguizmo", + "translation": [ + 0.015377978590719736, + 0.005030659310225598, + 0.06 + ], + "rotation_deg": [ + 4.76341402901702, + 5.687268433659546, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 43, + "type": "imguizmo", + "translation": [ + 0.014394888257625716, + 0.003579681106360786, + 0.06 + ], + "rotation_deg": [ + 4.781322053257388, + 5.4345539392579365, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 44, + "type": "imguizmo", + "translation": [ + 0.013408814191983651, + 0.0021108691112875022, + 0.06 + ], + "rotation_deg": [ + 4.793273145608537, + 5.181048869665178, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 45, + "type": "imguizmo", + "translation": [ + 0.012420984919289496, + 0.0006315408724817354, + 0.06 + ], + "rotation_deg": [ + 4.799252416497594, + 4.92706906063053, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 46, + "type": "imguizmo", + "translation": [ + 0.01143263115180861, + -0.0008509336711832751, + 0.06 + ], + "rotation_deg": [ + 4.799252416497594, + 4.672930939369478, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 47, + "type": "imguizmo", + "translation": [ + 0.010444984255261554, + -0.002329168906101713, + 0.06 + ], + "rotation_deg": [ + 4.793273145608537, + 4.418951130334829, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 48, + "type": "imguizmo", + "translation": [ + 0.009459274714695744, + -0.003795800338690687, + 0.06 + ], + "rotation_deg": [ + 4.781322053257388, + 4.165446060742073, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 49, + "type": "imguizmo", + "translation": [ + 0.008476730601454242, + -0.005243521284926882, + 0.06 + ], + "rotation_deg": [ + 4.763414029017019, + 3.912731566340462, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 50, + "type": "imguizmo", + "translation": [ + 0.007498576043151886, + -0.006665119271878699, + 0.06 + ], + "rotation_deg": [ + 4.739571384055657, + 3.6611224979210153, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 51, + "type": "imguizmo", + "translation": [ + 0.006526029698564641, + -0.008053511969883697, + 0.06 + ], + "rotation_deg": [ + 4.709823823339942, + 3.4109323290516267, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 52, + "type": "imguizmo", + "translation": [ + 0.005560303239332553, + -0.009401782476358683, + 0.06 + ], + "rotation_deg": [ + 4.674208408626242, + 3.162472765528039, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 53, + "type": "imguizmo", + "translation": [ + 0.004602599840367727, + -0.010703213775460477, + 0.06 + ], + "rotation_deg": [ + 4.632769512286319, + 2.916053357027231, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 54, + "type": "imguizmo", + "translation": [ + 0.003654112680848272, + -0.011951322201920933, + 0.06 + ], + "rotation_deg": [ + 4.585558762024875, + 2.6719811114470784, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 55, + "type": "imguizmo", + "translation": [ + 0.00271602345766557, + -0.013139889742341182, + 0.06 + ], + "rotation_deg": [ + 4.532634976557868, + 2.430560112412731, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 56, + "type": "imguizmo", + "translation": [ + 0.0017895009131771375, + -0.014262995013021596, + 0.06 + ], + "rotation_deg": [ + 4.474064092331711, + 2.192091140426291, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 57, + "type": "imguizmo", + "translation": [ + 0.0008756993790991278, + -0.015315042759997595, + 0.06 + ], + "rotation_deg": [ + 4.409919081374671, + 1.9568712981317473, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 58, + "type": "imguizmo", + "translation": [ + -2.424266164716745E-05, + -0.016290791734313338, + 0.06 + ], + "rotation_deg": [ + 4.340279860382808, + 1.725193640162095, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 59, + "type": "imguizmo", + "translation": [ + -0.0009092039933433156, + -0.017185380803660433, + 0.06 + ], + "rotation_deg": [ + 4.26523319115373, + 1.49734680802975, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 60, + "type": "imguizmo", + "translation": [ + -0.0017780820643689015, + -0.01799435317029459, + 0.06 + ], + "rotation_deg": [ + 4.1848725724921865, + 1.2736146705151596, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 61, + "type": "imguizmo", + "translation": [ + -0.002629794360843588, + -0.0187136785745778, + 0.06 + ], + "rotation_deg": [ + 4.099298123722207, + 1.0542759700016522, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 62, + "type": "imguizmo", + "translation": [ + -0.003463279755304539, + -0.019339773373529087, + 0.06 + ], + "rotation_deg": [ + 4.008616459950891, + 0.8396039751971452, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 63, + "type": "imguizmo", + "translation": [ + -0.004277499828739157, + -0.019869518394353594, + 0.06 + ], + "rotation_deg": [ + 3.9129405592392574, + 0.6298661406753615, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 64, + "type": "imguizmo", + "translation": [ + -0.005071440164325831, + -0.020300274474004554, + 0.06 + ], + "rotation_deg": [ + 3.812389621845653, + 0.42532377366073215, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 65, + "type": "imguizmo", + "translation": [ + -0.005844111611271043, + -0.02062989560736085, + 0.06 + ], + "rotation_deg": [ + 3.7070889217170673, + 0.2262317084721259, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 66, + "type": "imguizmo", + "translation": [ + -0.006594551517167985, + -0.02085673963851654, + 0.06 + ], + "rotation_deg": [ + 3.5971696504134014, + 0.03283798903104068, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 67, + "type": "imguizmo", + "translation": [ + -0.007321824927341658, + -0.020979676441919004, + 0.06 + ], + "rotation_deg": [ + 3.482768753659117, + -0.1546164401702514, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 68, + "type": "imguizmo", + "translation": [ + -0.008025025749685942, + -0.020998093552597605, + 0.06 + ], + "rotation_deg": [ + 3.3640287607259194, + -0.33589803425553255, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 69, + "type": "imguizmo", + "translation": [ + -0.008703277883541563, + -0.020911899217433343, + 0.06 + ], + "rotation_deg": [ + 3.2410976068590274, + -0.5107809389324762, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 70, + "type": "imguizmo", + "translation": [ + -0.009355736311208363, + -0.020721522852268307, + 0.06 + ], + "rotation_deg": [ + 3.1141284489682857, + -0.6790472718789642, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 71, + "type": "imguizmo", + "translation": [ + -0.009981588150732176, + -0.020427912902577526, + 0.06 + ], + "rotation_deg": [ + 2.9832794748137252, + -0.8404873941973343, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 72, + "type": "imguizmo", + "translation": [ + -0.01058005366865449, + -0.020032532118361326, + 0.06 + ], + "rotation_deg": [ + 2.848713705923312, + -0.9949001715983393, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 73, + "type": "imguizmo", + "translation": [ + -0.011150387251463296, + -0.019537350266798426, + 0.06 + ], + "rotation_deg": [ + 2.7105987944884182, + -1.1420932249894329, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 74, + "type": "imguizmo", + "translation": [ + -0.01169187833453459, + -0.01894483431896489, + 0.06 + ], + "rotation_deg": [ + 2.5691068144900777, + -1.2818831701551465, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 75, + "type": "imguizmo", + "translation": [ + -0.012203852287407481, + -0.018257936159508385, + 0.06 + ], + "rotation_deg": [ + 2.424414047316228, + -1.4140958462310023, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 76, + "type": "imguizmo", + "translation": [ + -0.012685671254289698, + -0.017480077880507296, + 0.06 + ], + "rotation_deg": [ + 2.276700762137053, + -1.5385665326862532, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 77, + "type": "imguizmo", + "translation": [ + -0.013136734948746533, + -0.01661513473278013, + 0.06 + ], + "rotation_deg": [ + 2.1261509913120404, + -1.6551401545451607, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 78, + "type": "imguizmo", + "translation": [ + -0.013556481401582922, + -0.015667415819580914, + 0.06 + ], + "rotation_deg": [ + 1.972952301108586, + -1.7636714755910972, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 79, + "type": "imguizmo", + "translation": [ + -0.013944387660987113, + -0.014641642628863492, + 0.06 + ], + "rotation_deg": [ + 1.8172955580177772, + -1.8640252793127874, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 80, + "type": "imguizmo", + "translation": [ + -0.01429997044406351, + -0.013542925511066067, + 0.06 + ], + "rotation_deg": [ + 1.659374690958511, + -1.956076537367249, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 81, + "type": "imguizmo", + "translation": [ + -0.014622786738942985, + -0.012376738219602352, + 0.06 + ], + "rotation_deg": [ + 1.4993864496662122, + -2.03971056534953, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 82, + "type": "imguizmo", + "translation": [ + -0.01491243435672047, + -0.011148890640897417, + 0.06 + ], + "rotation_deg": [ + 1.3375301595671625, + -2.1148231656752072, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 83, + "type": "imguizmo", + "translation": [ + -0.015168552432532281, + -0.00986549984982576, + 0.06 + ], + "rotation_deg": [ + 1.174007473443839, + -2.1813207573975917, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 84, + "type": "imguizmo", + "translation": [ + -0.015390821875148803, + -0.008532959634752008, + 0.06 + ], + "rotation_deg": [ + 1.0090221202006624, + -2.23912049279794, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 85, + "type": "imguizmo", + "translation": [ + -0.015578965764522399, + -0.0071579086439988015, + 0.06 + ], + "rotation_deg": [ + 0.8427796510431402, + -2.288150360603397, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 86, + "type": "imguizmo", + "translation": [ + -0.01573274969679533, + -0.005747197312434936, + 0.06 + ], + "rotation_deg": [ + 0.6754871833866748, + -2.328349275704067, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 87, + "type": "imguizmo", + "translation": [ + -0.01585198207633777, + -0.004307853732953323, + 0.06 + ], + "rotation_deg": [ + 0.5073531428140463, + -2.3596671552574664, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 88, + "type": "imguizmo", + "translation": [ + -0.015936514354452102, + -0.002847048642865782, + 0.06 + ], + "rotation_deg": [ + 0.3385870034030942, + -2.382064981085496, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 89, + "type": "imguizmo", + "translation": [ + -0.01598624121444608, + -0.0013720596996503569, + 0.06 + ], + "rotation_deg": [ + 0.169399026748093, + -2.39551484828624, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 90, + "type": "imguizmo", + "translation": [ + -0.01600110070284434, + 0.00010976477597202561, + 0.06 + ], + "rotation_deg": [ + 7.494005416219807E-16, + -2.399999999999997, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 91, + "type": "imguizmo", + "translation": [ + -0.0159810743065747, + 0.0015910424090018512, + 0.06 + ], + "rotation_deg": [ + -0.16939902674809154, + -2.39551484828624, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 92, + "type": "imguizmo", + "translation": [ + -0.015926186976033145, + 0.0030643935487818935, + 0.06 + ], + "rotation_deg": [ + -0.33858700340309267, + -2.382064981085496, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 93, + "type": "imguizmo", + "translation": [ + -0.015836507093998746, + 0.004522478034045581, + 0.06 + ], + "rotation_deg": [ + -0.5073531428140469, + -2.3596671552574664, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 94, + "type": "imguizmo", + "translation": [ + -0.01571214639043722, + 0.005958031761231207, + 0.06 + ], + "rotation_deg": [ + -0.6754871833866755, + -2.328349275704067, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 95, + "type": "imguizmo", + "translation": [ + -0.015553259803299331, + 0.007363902873880442, + 0.06 + ], + "rotation_deg": [ + -0.8427796510431365, + -2.2881503606033977, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 96, + "type": "imguizmo", + "translation": [ + -0.015360045285487447, + 0.00873308739282761, + 0.06 + ], + "rotation_deg": [ + -1.0090221202006586, + -2.239120492797941, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 97, + "type": "imguizmo", + "translation": [ + -0.01513274355823093, + 0.010058764109671341, + 0.06 + ], + "rotation_deg": [ + -1.1740074734438375, + -2.1813207573975917, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 98, + "type": "imguizmo", + "translation": [ + -0.014871637811177441, + 0.011334328569691894, + 0.06 + ], + "rotation_deg": [ + -1.3375301595671605, + -2.114823165675208, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 99, + "type": "imguizmo", + "translation": [ + -0.014577053349573875, + 0.012553425974912283, + 0.06 + ], + "rotation_deg": [ + -1.4993864496662104, + -2.039710565349531, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 100, + "type": "imguizmo", + "translation": [ + -0.014249357188976578, + 0.01370998284338216, + 0.06 + ], + "rotation_deg": [ + -1.6593746909585092, + -1.9560765373672486, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 101, + "type": "imguizmo", + "translation": [ + -0.013888957597995627, + 0.01479823726695939, + 0.06 + ], + "rotation_deg": [ + -1.8172955580177754, + -1.864025279312788, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 102, + "type": "imguizmo", + "translation": [ + -0.013496303589642999, + 0.01581276761684657, + 0.06 + ], + "rotation_deg": [ + -1.9729523011085843, + -1.7636714755910978, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 103, + "type": "imguizmo", + "translation": [ + -0.013071884361918286, + 0.01674851955387297, + 0.06 + ], + "rotation_deg": [ + -2.126150991312037, + -1.6551401545451625, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 104, + "type": "imguizmo", + "translation": [ + -0.012616228688328849, + 0.017600831208958116, + 0.06 + ], + "rotation_deg": [ + -2.2767007621370494, + -1.538566532686255, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 105, + "type": "imguizmo", + "translation": [ + -0.012129904259103944, + 0.018365456408308927, + 0.06 + ], + "rotation_deg": [ + -2.424414047316226, + -1.4140958462310032, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 106, + "type": "imguizmo", + "translation": [ + -0.011613516973923342, + 0.01903858582764405, + 0.06 + ], + "rotation_deg": [ + -2.5691068144900764, + -1.2818831701551474, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 107, + "type": "imguizmo", + "translation": [ + -0.011067710187041791, + 0.019616865970055807, + 0.06 + ], + "rotation_deg": [ + -2.7105987944884165, + -1.1420932249894329, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 108, + "type": "imguizmo", + "translation": [ + -0.01049316390574973, + 0.020097415872963067, + 0.06 + ], + "rotation_deg": [ + -2.8487137059233105, + -0.9949001715983414, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 109, + "type": "imguizmo", + "translation": [ + -0.009890593943168866, + 0.020477841460921947, + 0.06 + ], + "rotation_deg": [ + -2.9832794748137257, + -0.8404873941973332, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 110, + "type": "imguizmo", + "translation": [ + -0.009260751026438174, + 0.020756247472789223, + 0.06 + ], + "rotation_deg": [ + -3.114128448968286, + -0.6790472718789631, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 111, + "type": "imguizmo", + "translation": [ + -0.008604419861401402, + 0.020931246903818014, + 0.06 + ], + "rotation_deg": [ + -3.241097606859024, + -0.5107809389324793, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 112, + "type": "imguizmo", + "translation": [ + -0.007922418154961182, + 0.021001967915645684, + 0.06 + ], + "rotation_deg": [ + -3.3640287607259163, + -0.33589803425553566, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 113, + "type": "imguizmo", + "translation": [ + -0.007215595596318212, + 0.020968058179748676, + 0.06 + ], + "rotation_deg": [ + -3.482768753659115, + -0.15461644017025347, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 114, + "type": "imguizmo", + "translation": [ + -0.006484832798364218, + 0.02082968663272552, + 0.06 + ], + "rotation_deg": [ + -3.5971696504133996, + 0.032837989031039655, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 115, + "type": "imguizmo", + "translation": [ + -0.005731040200547969, + 0.02058754263466318, + 0.06 + ], + "rotation_deg": [ + -3.7070889217170673, + 0.22623170847212754, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 116, + "type": "imguizmo", + "translation": [ + -0.004955156934581126, + 0.02024283253477979, + 0.06 + ], + "rotation_deg": [ + -3.812389621845653, + 0.4253237736607327, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 117, + "type": "imguizmo", + "translation": [ + -0.0041581496543970165, + 0.019797273661453525, + 0.06 + ], + "rotation_deg": [ + -3.9129405592392588, + 0.6298661406753658, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 118, + "type": "imguizmo", + "translation": [ + -0.0033410113318202523, + 0.019253085766579015, + 0.06 + ], + "rotation_deg": [ + -4.0086164599508916, + 0.8396039751971496, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 119, + "type": "imguizmo", + "translation": [ + -0.002504760019447488, + 0.01861297996687505, + 0.06 + ], + "rotation_deg": [ + -4.099298123722205, + 1.0542759700016506, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 120, + "type": "imguizmo", + "translation": [ + -0.0016504375822805786, + 0.017880145237237424, + 0.06 + ], + "rotation_deg": [ + -4.184872572492185, + 1.2736146705151585, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 121, + "type": "imguizmo", + "translation": [ + -0.0007791083996926406, + 0.0170582325234265, + 0.06 + ], + "rotation_deg": [ + -4.265233191153729, + 1.497346808029751, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 122, + "type": "imguizmo", + "translation": [ + 0.00010814196065617621, + 0.016151336553239016, + 0.06 + ], + "rotation_deg": [ + -4.340279860382807, + 1.725193640162097, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 123, + "type": "imguizmo", + "translation": [ + 0.0010102080953008018, + 0.015163975436780202, + 0.06 + ], + "rotation_deg": [ + -4.409919081374669, + 1.9568712981317473, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 124, + "type": "imguizmo", + "translation": [ + 0.0019259661421663146, + 0.014101068157466475, + 0.06 + ], + "rotation_deg": [ + -4.47406409233171, + 2.1920911404262906, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 125, + "type": "imguizmo", + "translation": [ + 0.0028542751807601355, + 0.01296791006589752, + 0.06 + ], + "rotation_deg": [ + -4.532634976557867, + 2.430560112412734, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 126, + "type": "imguizmo", + "translation": [ + 0.003793978653616888, + 0.01177014649868595, + 0.06 + ], + "rotation_deg": [ + -4.585558762024874, + 2.671981111447081, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 127, + "type": "imguizmo", + "translation": [ + 0.0047439058072249645, + 0.010513744653674311, + 0.06 + ], + "rotation_deg": [ + -4.632769512286317, + 2.916053357027228, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 128, + "type": "imguizmo", + "translation": [ + 0.005702873150639708, + 0.009204963861655448, + 0.06 + ], + "rotation_deg": [ + -4.6742084086262405, + 3.162472765528036, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 129, + "type": "imguizmo", + "translation": [ + 0.0066696859299656035, + 0.007850324402701564, + 0.06 + ], + "rotation_deg": [ + -4.70982382333994, + 3.410932329051625, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 130, + "type": "imguizmo", + "translation": [ + 0.007643139616870965, + 0.00645657502245673, + 0.06 + ], + "rotation_deg": [ + -4.7395713840556555, + 3.6611224979210135, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 131, + "type": "imguizmo", + "translation": [ + 0.008622021409280213, + 0.0050306593102256054, + 0.06 + ], + "rotation_deg": [ + -4.763414029017018, + 3.9127315663404603, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 132, + "type": "imguizmo", + "translation": [ + 0.00960511174237424, + 0.0035796811063607834, + 0.06 + ], + "rotation_deg": [ + -4.781322053257387, + 4.165446060742071, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 133, + "type": "imguizmo", + "translation": [ + 0.010591185808016304, + 0.0021108691112875005, + 0.06 + ], + "rotation_deg": [ + -4.793273145608536, + 4.41895113033483, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 134, + "type": "imguizmo", + "translation": [ + 0.011579015080710466, + 0.0006315408724817239, + 0.06 + ], + "rotation_deg": [ + -4.799252416497593, + 4.672930939369479, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 135, + "type": "imguizmo", + "translation": [ + 0.012567368848191327, + -0.0008509336711832456, + 0.06 + ], + "rotation_deg": [ + -4.799252416497594, + 4.927069060630525, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 136, + "type": "imguizmo", + "translation": [ + 0.013555015744738384, + -0.002329168906101687, + 0.06 + ], + "rotation_deg": [ + -4.793273145608536, + 5.181048869665174, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 137, + "type": "imguizmo", + "translation": [ + 0.014540725285304208, + -0.0037958003386906808, + 0.06 + ], + "rotation_deg": [ + -4.781322053257387, + 5.434553939257933, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 138, + "type": "imguizmo", + "translation": [ + 0.015523269398545708, + -0.005243521284926873, + 0.06 + ], + "rotation_deg": [ + -4.763414029017019, + 5.687268433659543, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 139, + "type": "imguizmo", + "translation": [ + 0.016501423956848067, + -0.00666511927187869, + 0.06 + ], + "rotation_deg": [ + -4.739571384055656, + 5.938877502078991, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 140, + "type": "imguizmo", + "translation": [ + 0.017473970301435313, + -0.00805351196988369, + 0.06 + ], + "rotation_deg": [ + -4.709823823339941, + 6.189067670948379, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 141, + "type": "imguizmo", + "translation": [ + 0.018439696760667406, + -0.009401782476358683, + 0.06 + ], + "rotation_deg": [ + -4.674208408626241, + 6.437527234471968, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 142, + "type": "imguizmo", + "translation": [ + 0.019397400159632226, + -0.010703213775460477, + 0.06 + ], + "rotation_deg": [ + -4.632769512286318, + 6.683946642972776, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 143, + "type": "imguizmo", + "translation": [ + 0.020345887319151656, + -0.011951322201920899, + 0.06 + ], + "rotation_deg": [ + -4.585558762024875, + 6.928018888552923, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 144, + "type": "imguizmo", + "translation": [ + 0.021283976542334365, + -0.01313988974234115, + 0.06 + ], + "rotation_deg": [ + -4.532634976557868, + 7.16943988758727, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 145, + "type": "imguizmo", + "translation": [ + 0.022210499086822807, + -0.014262995013021584, + 0.06 + ], + "rotation_deg": [ + -4.4740640923317105, + 7.4079088595737135, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 146, + "type": "imguizmo", + "translation": [ + 0.023124300620900814, + -0.015315042759997581, + 0.06 + ], + "rotation_deg": [ + -4.40991908137467, + 7.643128701868257, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 147, + "type": "imguizmo", + "translation": [ + 0.024024242661647103, + -0.016290791734313317, + 0.06 + ], + "rotation_deg": [ + -4.340279860382808, + 7.8748063598379066, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 148, + "type": "imguizmo", + "translation": [ + 0.024909203993343253, + -0.017185380803660416, + 0.06 + ], + "rotation_deg": [ + -4.26523319115373, + 8.102653191970251, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 149, + "type": "imguizmo", + "translation": [ + 0.02577808206436885, + -0.017994353170294585, + 0.06 + ], + "rotation_deg": [ + -4.184872572492186, + 8.326385329484845, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 150, + "type": "imguizmo", + "translation": [ + 0.026629794360843537, + -0.018713678574577796, + 0.06 + ], + "rotation_deg": [ + -4.099298123722206, + 8.545724029998354, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 151, + "type": "imguizmo", + "translation": [ + 0.02746327975530446, + -0.01933977337352907, + 0.06 + ], + "rotation_deg": [ + -4.008616459950892, + 8.760396024802855, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 152, + "type": "imguizmo", + "translation": [ + 0.028277499828739082, + -0.01986951839435358, + 0.06 + ], + "rotation_deg": [ + -3.9129405592392597, + 8.970133859324639, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 153, + "type": "imguizmo", + "translation": [ + 0.02907144016432577, + -0.02030027447400455, + 0.06 + ], + "rotation_deg": [ + -3.812389621845654, + 9.17467622633927, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 154, + "type": "imguizmo", + "translation": [ + 0.029844111611270978, + -0.020629895607360848, + 0.06 + ], + "rotation_deg": [ + -3.707088921717068, + 9.373768291527877, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 155, + "type": "imguizmo", + "translation": [ + 0.03059455151716792, + -0.020856739638516537, + 0.06 + ], + "rotation_deg": [ + -3.5971696504134023, + 9.567162010968962, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 156, + "type": "imguizmo", + "translation": [ + 0.03132182492734159, + -0.020979676441919004, + 0.06 + ], + "rotation_deg": [ + -3.482768753659118, + 9.754616440170256, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 157, + "type": "imguizmo", + "translation": [ + 0.032025025749685895, + -0.020998093552597605, + 0.06 + ], + "rotation_deg": [ + -3.364028760725919, + 9.935898034255539, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 158, + "type": "imguizmo", + "translation": [ + 0.032703277883541514, + -0.020911899217433343, + 0.06 + ], + "rotation_deg": [ + -3.241097606859027, + 10.110780938932482, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 159, + "type": "imguizmo", + "translation": [ + 0.03335573631120831, + -0.020721522852268307, + 0.06 + ], + "rotation_deg": [ + -3.1141284489682852, + 10.27904727187897, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 160, + "type": "imguizmo", + "translation": [ + 0.03398158815073212, + -0.020427912902577526, + 0.06 + ], + "rotation_deg": [ + -2.983279474813725, + 10.44048739419734, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 161, + "type": "imguizmo", + "translation": [ + 0.03458005366865445, + -0.020032532118361323, + 0.06 + ], + "rotation_deg": [ + -2.8487137059233096, + 10.594900171598347, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 162, + "type": "imguizmo", + "translation": [ + 0.03515038725146326, + -0.01953735026679842, + 0.06 + ], + "rotation_deg": [ + -2.7105987944884156, + 10.74209322498944, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 163, + "type": "imguizmo", + "translation": [ + 0.03569187833453455, + -0.018944834318964883, + 0.06 + ], + "rotation_deg": [ + -2.5691068144900755, + 10.881883170155152, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 164, + "type": "imguizmo", + "translation": [ + 0.03620385228740743, + -0.018257936159508374, + 0.06 + ], + "rotation_deg": [ + -2.4244140473162257, + 11.014095846231008, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 165, + "type": "imguizmo", + "translation": [ + 0.03668567125428966, + -0.017480077880507275, + 0.06 + ], + "rotation_deg": [ + -2.276700762137049, + 11.13856653268626, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 166, + "type": "imguizmo", + "translation": [ + 0.0371367349487465, + -0.016615134732780104, + 0.06 + ], + "rotation_deg": [ + -2.126150991312036, + 11.255140154545169, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 167, + "type": "imguizmo", + "translation": [ + 0.037556481401582874, + -0.015667415819580914, + 0.06 + ], + "rotation_deg": [ + -1.9729523011085857, + 11.363671475591103, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 168, + "type": "imguizmo", + "translation": [ + 0.03794438766098707, + -0.014641642628863494, + 0.06 + ], + "rotation_deg": [ + -1.8172955580177768, + 11.464025279312793, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 169, + "type": "imguizmo", + "translation": [ + 0.03829997044406347, + -0.013542925511066069, + 0.06 + ], + "rotation_deg": [ + -1.6593746909585105, + 11.556076537367254, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 170, + "type": "imguizmo", + "translation": [ + 0.03862278673894294, + -0.012376738219602354, + 0.06 + ], + "rotation_deg": [ + -1.4993864496662117, + 11.639710565349535, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 171, + "type": "imguizmo", + "translation": [ + 0.03891243435672043, + -0.011148890640897418, + 0.06 + ], + "rotation_deg": [ + -1.337530159567162, + 11.714823165675211, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 172, + "type": "imguizmo", + "translation": [ + 0.03916855243253224, + -0.009865499849825762, + 0.06 + ], + "rotation_deg": [ + -1.1740074734438386, + 11.781320757397596, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 173, + "type": "imguizmo", + "translation": [ + 0.039390821875148765, + -0.00853295963475199, + 0.06 + ], + "rotation_deg": [ + -1.00902212020066, + 11.839120492797944, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 174, + "type": "imguizmo", + "translation": [ + 0.03957896576452236, + -0.007157908643998786, + 0.06 + ], + "rotation_deg": [ + -0.8427796510431377, + 11.888150360603401, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 175, + "type": "imguizmo", + "translation": [ + 0.03973274969679529, + -0.005747197312434959, + 0.06 + ], + "rotation_deg": [ + -0.6754871833866767, + 11.928349275704072, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 176, + "type": "imguizmo", + "translation": [ + 0.039851982076337736, + -0.004307853732953343, + 0.06 + ], + "rotation_deg": [ + -0.5073531428140481, + 11.95966715525747, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 177, + "type": "imguizmo", + "translation": [ + 0.039936514354452064, + -0.002847048642865782, + 0.06 + ], + "rotation_deg": [ + -0.33858700340309394, + 11.9820649810855, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 178, + "type": "imguizmo", + "translation": [ + 0.03998624121444605, + -0.0013720596996503584, + 0.06 + ], + "rotation_deg": [ + -0.16939902674809273, + 11.995514848286243, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 179, + "type": "imguizmo", + "translation": [ + 0.0400011007028443, + 0.00010976477597202409, + 0.06 + ], + "rotation_deg": [ + -4.440892098500626E-16, + 12.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 180, + "type": "action", + "action": "set_active_render_window", + "value": 0 + }, + { + "frame": 180, + "type": "action", + "action": "set_active_planar", + "value": 1 + }, + { + "frame": 180, + "type": "action", + "action": "set_projection_type", + "value": "perspective" + }, + { + "frame": 181, + "type": "imguizmo", + "translation": [ + 0.0, + 5.739999999999999, + 0.0 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 182, + "type": "imguizmo", + "translation": [ + 0.05469471076129016, + 5.739999999999999, + 0.07871132068111694 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 183, + "type": "imguizmo", + "translation": [ + 0.1049457018628703, + 5.739999999999999, + 0.15073359986741086 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 184, + "type": "imguizmo", + "translation": [ + 0.1554159898370556, + 5.739999999999999, + 0.22250868429365483 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 185, + "type": "imguizmo", + "translation": [ + 0.2056646451490081, + 5.739999999999999, + 0.2931349414510858 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 186, + "type": "imguizmo", + "translation": [ + 0.25565930821863303, + 5.739999999999999, + 0.362304039091038 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 187, + "type": "imguizmo", + "translation": [ + 0.3053352724022978, + 5.739999999999999, + 0.4296678983114296 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 188, + "type": "imguizmo", + "translation": [ + 0.3546308411959958, + 5.739999999999999, + 0.4948911943090857 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 189, + "type": "imguizmo", + "translation": [ + 0.40348458297456774, + 5.739999999999999, + 0.5576489656197812 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 190, + "type": "imguizmo", + "translation": [ + 0.4518356332980126, + 5.739999999999999, + 0.6176285579664093 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 191, + "type": "imguizmo", + "translation": [ + 0.4996237526793879, + 5.739999999999999, + 0.6745311558943443 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 192, + "type": "imguizmo", + "translation": [ + 0.5467894030806382, + 5.739999999999999, + 0.7280732735344067 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 193, + "type": "imguizmo", + "translation": [ + 0.5932738219739832, + 5.739999999999999, + 0.7779881667488505 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 194, + "type": "imguizmo", + "translation": [ + 0.6390190955619556, + 5.739999999999999, + 0.8240271620505506 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 195, + "type": "imguizmo", + "translation": [ + 0.6839682309300437, + 5.739999999999999, + 0.8659608954807391 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 196, + "type": "imguizmo", + "translation": [ + 0.7280652270528233, + 5.739999999999999, + 0.9035804552887989 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 197, + "type": "imguizmo", + "translation": [ + 0.7712551445642576, + 5.739999999999999, + 0.9366984227200871 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 198, + "type": "imguizmo", + "translation": [ + 0.8134841742053067, + 5.739999999999999, + 0.9651498057267452 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 199, + "type": "imguizmo", + "translation": [ + 0.854699703863564, + 5.739999999999999, + 0.9887928609497825 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 200, + "type": "imguizmo", + "translation": [ + 0.894850384121396, + 5.739999999999999, + 1.0075097998773594 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 201, + "type": "imguizmo", + "translation": [ + 0.9338861922309233, + 5.739999999999999, + 1.0212073756612294 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 202, + "type": "imguizmo", + "translation": [ + 0.9717584944361342, + 5.739999999999999, + 1.029817347667846 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 203, + "type": "imguizmo", + "translation": [ + 1.00842010656449, + 5.739999999999999, + 1.0332968214497675 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 204, + "type": "imguizmo", + "translation": [ + 1.0438253528125279, + 5.739999999999999, + 1.0316284624436345 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 205, + "type": "imguizmo", + "translation": [ + 1.0779301226522244, + 5.739999999999999, + 1.0248205823300953 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 206, + "type": "imguizmo", + "translation": [ + 1.1106919257872196, + 5.739999999999999, + 1.0129070976254282 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 207, + "type": "imguizmo", + "translation": [ + 1.142069945090437, + 5.739999999999999, + 0.9959473607111657 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 208, + "type": "imguizmo", + "translation": [ + 1.17202508745714, + 5.739999999999999, + 0.9740258641435138 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 209, + "type": "imguizmo", + "translation": [ + 1.200520032510072, + 5.739999999999999, + 0.947251819715688 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 210, + "type": "imguizmo", + "translation": [ + 1.2275192790960014, + 5.739999999999999, + 0.915758614370252 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 211, + "type": "imguizmo", + "translation": [ + 1.2529891895157341, + 5.739999999999999, + 0.8797031456720809 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 212, + "type": "imguizmo", + "translation": [ + 1.2768980314325014, + 5.739999999999999, + 0.8392650401525831 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 213, + "type": "imguizmo", + "translation": [ + 1.2992160174064986, + 5.739999999999999, + 0.7946457584193596 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 214, + "type": "imguizmo", + "translation": [ + 1.3199153420063263, + 5.739999999999999, + 0.7460675914895857 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 215, + "type": "imguizmo", + "translation": [ + 1.3389702164510942, + 5.739999999999999, + 0.6937725533473507 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 216, + "type": "imguizmo", + "translation": [ + 1.3563569007400338, + 5.739999999999999, + 0.6380211752421582 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 217, + "type": "imguizmo", + "translation": [ + 1.3720537332295781, + 5.739999999999999, + 0.5790912077353495 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 218, + "type": "imguizmo", + "translation": [ + 1.3860411576210785, + 5.739999999999999, + 0.5172762369607751 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 219, + "type": "imguizmo", + "translation": [ + 1.3983017473255113, + 5.739999999999999, + 0.4528842219934477 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 220, + "type": "imguizmo", + "translation": [ + 1.4088202271748425, + 5.739999999999999, + 0.3862359606129166 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 221, + "type": "imguizmo", + "translation": [ + 1.4175834924529824, + 5.739999999999999, + 0.3176634911048708 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 222, + "type": "imguizmo", + "translation": [ + 1.4245806252226312, + 5.739999999999999, + 0.24750843806309936 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 223, + "type": "imguizmo", + "translation": [ + 1.4298029079276682, + 5.739999999999999, + 0.17612031043295068 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 224, + "type": "imguizmo", + "translation": [ + 1.4332438342541423, + 5.739999999999999, + 0.10385476027534508 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 225, + "type": "imguizmo", + "translation": [ + 1.4348991172363286, + 5.739999999999999, + 0.03107181092610138 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 226, + "type": "imguizmo", + "translation": [ + 1.4347666945977537, + 5.739999999999999, + -0.04186593662221716 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 227, + "type": "imguizmo", + "translation": [ + 1.4328467313205353, + 5.739999999999999, + -0.11459511018020431 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 228, + "type": "imguizmo", + "translation": [ + 1.4291416194398359, + 5.739999999999999, + -0.1867533766635818 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 229, + "type": "imguizmo", + "translation": [ + 1.4236559750636828, + 5.739999999999999, + -0.25798124721840254 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 230, + "type": "imguizmo", + "translation": [ + 1.4163966326218753, + 5.739999999999999, + -0.32792386817643193 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 231, + "type": "imguizmo", + "translation": [ + 1.4073726363511332, + 5.739999999999999, + -0.39623278891827785 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 232, + "type": "imguizmo", + "translation": [ + 1.3965952290271069, + 5.739999999999999, + -0.4625676978368471 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 233, + "type": "imguizmo", + "translation": [ + 1.3840778379572762, + 5.739999999999999, + -0.5265981177526554 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 234, + "type": "imguizmo", + "translation": [ + 1.3698360582521987, + 5.739999999999999, + -0.5880050523345098 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 235, + "type": "imguizmo", + "translation": [ + 1.3538876333959453, + 5.739999999999999, + -0.6464825753231862 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 236, + "type": "imguizmo", + "translation": [ + 1.336252433139925, + 5.739999999999999, + -0.7017393546406625 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 237, + "type": "imguizmo", + "translation": [ + 1.3169524287476522, + 5.739999999999999, + -0.7535001037918817 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 238, + "type": "imguizmo", + "translation": [ + 1.2960116656212863, + 5.739999999999999, + -0.8015069533282162 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 239, + "type": "imguizmo", + "translation": [ + 1.2734562333440567, + 5.739999999999999, + -0.8455207355400934 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 240, + "type": "imguizmo", + "translation": [ + 1.2493142331758895, + 5.739999999999999, + -0.8853221759784938 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 241, + "type": "imguizmo", + "translation": [ + 1.223615743042736, + 5.739999999999999, + -0.9207129858692282 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 242, + "type": "imguizmo", + "translation": [ + 1.1963927800632244, + 5.739999999999999, + -0.9515168499776314 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 243, + "type": "imguizmo", + "translation": [ + 1.1676792606593167, + 5.739999999999999, + -0.9775803050021973 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 244, + "type": "imguizmo", + "translation": [ + 1.1375109583006695, + 5.739999999999999, + -0.9987735041210246 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 245, + "type": "imguizmo", + "translation": [ + 1.105925458935344, + 5.739999999999999, + -1.0149908638821543 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 246, + "type": "imguizmo", + "translation": [ + 1.072962114162399, + 5.739999999999999, + -1.0261515902150142 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 247, + "type": "imguizmo", + "translation": [ + 1.038661992204695, + 5.739999999999999, + -1.0322000809424152 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 248, + "type": "imguizmo", + "translation": [ + 1.003067826743005, + 5.739999999999999, + -1.0331062027878024 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 249, + "type": "imguizmo", + "translation": [ + 0.9662239636751674, + 5.739999999999999, + -1.028865441497721 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 250, + "type": "imguizmo", + "translation": [ + 0.9281763058666213, + 5.739999999999999, + -1.019498924331601 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 251, + "type": "imguizmo", + "translation": [ + 0.8889722559611514, + 5.739999999999999, + -1.0050533148068148 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 252, + "type": "imguizmo", + "translation": [ + 0.8486606573230967, + 5.739999999999999, + -0.9856005802233779 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 253, + "type": "imguizmo", + "translation": [ + 0.8072917331845997, + 5.739999999999999, + -0.9612376331264829 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 254, + "type": "imguizmo", + "translation": [ + 0.7649170240737152, + 5.739999999999999, + -0.9320858484930732 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 255, + "type": "imguizmo", + "translation": [ + 0.7215893236013293, + 5.739999999999999, + -0.8982904590478129 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 256, + "type": "imguizmo", + "translation": [ + 0.6773626126868942, + 5.739999999999999, + -0.8600198317209593 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 257, + "type": "imguizmo", + "translation": [ + 0.6322919923049231, + 5.739999999999999, + -0.8174646288527828 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 258, + "type": "imguizmo", + "translation": [ + 0.5864336148360403, + 5.739999999999999, + -0.7708368583233813 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 259, + "type": "imguizmo", + "translation": [ + 0.5398446141081061, + 5.739999999999999, + -0.7203688173400843 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 260, + "type": "imguizmo", + "translation": [ + 0.4925830342145803, + 5.739999999999999, + -0.6663119351444511 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 261, + "type": "imguizmo", + "translation": [ + 0.44470775719881134, + 5.739999999999999, + -0.6089355204044361 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 262, + "type": "imguizmo", + "translation": [ + 0.39627842969434224, + 5.739999999999999, + -0.5485254195321535 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 263, + "type": "imguizmo", + "translation": [ + 0.34735538861263177, + 5.739999999999999, + -0.485382592611428 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 264, + "type": "imguizmo", + "translation": [ + 0.2979995859707779, + 5.739999999999999, + -0.4198216140297991 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 265, + "type": "imguizmo", + "translation": [ + 0.24827251295289124, + 5.739999999999999, + -0.35216910528474155 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 266, + "type": "imguizmo", + "translation": [ + 0.19823612329974089, + 5.739999999999999, + -0.2827621077717993 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 267, + "type": "imguizmo", + "translation": [ + 0.1479527561221055, + 5.739999999999999, + -0.21194640366130396 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 268, + "type": "imguizmo", + "translation": [ + 0.09748505823400519, + 5.739999999999999, + -0.14007479322899688 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 269, + "type": "imguizmo", + "translation": [ + 0.04689590610256971, + 5.739999999999999, + -0.06750533722279796 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 270, + "type": "imguizmo", + "translation": [ + -0.0037516724882057442, + 5.739999999999999, + 0.005400426977823267 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 271, + "type": "imguizmo", + "translation": [ + -0.054394576962234, + 5.739999999999999, + 0.07827928652289069 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 272, + "type": "imguizmo", + "translation": [ + -0.10496971256679523, + 5.739999999999999, + 0.1507681626000688 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 273, + "type": "imguizmo", + "translation": [ + -0.15541406898074217, + 5.739999999999999, + 0.2225059192750422 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 274, + "type": "imguizmo", + "translation": [ + -0.2056647988175139, + 5.739999999999999, + 0.29313516265257505 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 275, + "type": "imguizmo", + "translation": [ + -0.255659295925152, + 5.739999999999999, + 0.3623040213949174 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 276, + "type": "imguizmo", + "translation": [ + -0.30533527338577593, + 5.739999999999999, + 0.42966789972711805 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 277, + "type": "imguizmo", + "translation": [ + -0.35463084111731735, + 5.739999999999999, + 0.4948911941958297 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 278, + "type": "imguizmo", + "translation": [ + -0.40348458298086204, + 5.739999999999999, + 0.5576489656288408 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 279, + "type": "imguizmo", + "translation": [ + -0.45183563329750914, + 5.739999999999999, + 0.617628557965684 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 280, + "type": "imguizmo", + "translation": [ + -0.4996237526794284, + 5.739999999999999, + 0.674531155894402 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 281, + "type": "imguizmo", + "translation": [ + -0.5467894030806353, + 5.739999999999999, + 0.7280732735344018 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 282, + "type": "imguizmo", + "translation": [ + -0.5932738219739838, + 5.739999999999999, + 0.7779881667488509 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 283, + "type": "imguizmo", + "translation": [ + -0.6390190955619557, + 5.739999999999999, + 0.8240271620505499 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 284, + "type": "imguizmo", + "translation": [ + -0.6839682309300439, + 5.739999999999999, + 0.865960895480739 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 285, + "type": "imguizmo", + "translation": [ + -0.7280652270528234, + 5.739999999999999, + 0.9035804552887987 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 286, + "type": "imguizmo", + "translation": [ + -0.7712551445642579, + 5.739999999999999, + 0.9366984227200869 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 287, + "type": "imguizmo", + "translation": [ + -0.8134841742053073, + 5.739999999999999, + 0.9651498057267454 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 288, + "type": "imguizmo", + "translation": [ + -0.8546997038635649, + 5.739999999999999, + 0.9887928609497827 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 289, + "type": "imguizmo", + "translation": [ + -0.8948503841213968, + 5.739999999999999, + 1.0075097998773594 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 290, + "type": "imguizmo", + "translation": [ + -0.9338861922309241, + 5.739999999999999, + 1.0212073756612292 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 291, + "type": "imguizmo", + "translation": [ + -0.9717584944361338, + 5.739999999999999, + 1.0298173476678458 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 292, + "type": "imguizmo", + "translation": [ + -1.0084201065644902, + 5.739999999999999, + 1.0332968214497673 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 293, + "type": "imguizmo", + "translation": [ + -1.043825352812528, + 5.739999999999999, + 1.0316284624436345 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 294, + "type": "imguizmo", + "translation": [ + -1.0779301226522249, + 5.739999999999999, + 1.0248205823300953 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 295, + "type": "imguizmo", + "translation": [ + -1.1106919257872208, + 5.739999999999999, + 1.012907097625428 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 296, + "type": "imguizmo", + "translation": [ + -1.1420699450904381, + 5.739999999999999, + 0.9959473607111654 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 297, + "type": "imguizmo", + "translation": [ + -1.1720250874571414, + 5.739999999999999, + 0.9740258641435133 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 298, + "type": "imguizmo", + "translation": [ + -1.2005200325100736, + 5.739999999999999, + 0.9472518197156874 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 299, + "type": "imguizmo", + "translation": [ + -1.2275192790960017, + 5.739999999999999, + 0.9157586143702521 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 300, + "type": "imguizmo", + "translation": [ + -1.2529891895157348, + 5.739999999999999, + 0.879703145672081 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 301, + "type": "imguizmo", + "translation": [ + -1.276898031432502, + 5.739999999999999, + 0.8392650401525834 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 302, + "type": "imguizmo", + "translation": [ + -1.2992160174064993, + 5.739999999999999, + 0.7946457584193591 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 303, + "type": "imguizmo", + "translation": [ + -1.319915342006327, + 5.739999999999999, + 0.7460675914895857 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 304, + "type": "imguizmo", + "translation": [ + -1.338970216451095, + 5.739999999999999, + 0.6937725533473503 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 305, + "type": "imguizmo", + "translation": [ + -1.3563569007400345, + 5.739999999999999, + 0.6380211752421577 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 306, + "type": "imguizmo", + "translation": [ + -1.3720537332295792, + 5.739999999999999, + 0.5790912077353486 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 307, + "type": "imguizmo", + "translation": [ + -1.386041157621079, + 5.739999999999999, + 0.517276236960776 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 308, + "type": "imguizmo", + "translation": [ + -1.398301747325512, + 5.739999999999999, + 0.45288422199344786 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 309, + "type": "imguizmo", + "translation": [ + -1.4088202271748431, + 5.739999999999999, + 0.38623596061291676 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 310, + "type": "imguizmo", + "translation": [ + -1.417583492452983, + 5.739999999999999, + 0.31766349110487097 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 311, + "type": "imguizmo", + "translation": [ + -1.4245806252226318, + 5.739999999999999, + 0.24750843806309958 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 312, + "type": "imguizmo", + "translation": [ + -1.4298029079276688, + 5.739999999999999, + 0.1761203104329504 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 313, + "type": "imguizmo", + "translation": [ + -1.433243834254143, + 5.739999999999999, + 0.10385476027534485 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 314, + "type": "imguizmo", + "translation": [ + -1.4348991172363292, + 5.739999999999999, + 0.03107181092610066 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 315, + "type": "imguizmo", + "translation": [ + -1.4347666945977546, + 5.739999999999999, + -0.041865936622215855 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 316, + "type": "imguizmo", + "translation": [ + -1.432846731320536, + 5.739999999999999, + -0.11459511018020317 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 317, + "type": "imguizmo", + "translation": [ + -1.4291416194398365, + 5.739999999999999, + -0.18675337666358166 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 318, + "type": "imguizmo", + "translation": [ + -1.4236559750636835, + 5.739999999999999, + -0.25798124721840227 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 319, + "type": "imguizmo", + "translation": [ + -1.416396632621876, + 5.739999999999999, + -0.3279238681764317 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 320, + "type": "imguizmo", + "translation": [ + -1.4073726363511339, + 5.739999999999999, + -0.3962327889182776 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 321, + "type": "imguizmo", + "translation": [ + -1.3965952290271075, + 5.739999999999999, + -0.46256769783684737 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 322, + "type": "imguizmo", + "translation": [ + -1.384077837957277, + 5.739999999999999, + -0.5265981177526557 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 323, + "type": "imguizmo", + "translation": [ + -1.3698360582521996, + 5.739999999999999, + -0.5880050523345084 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 324, + "type": "imguizmo", + "translation": [ + -1.353887633395946, + 5.739999999999999, + -0.6464825753231849 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 325, + "type": "imguizmo", + "translation": [ + -1.3362524331399257, + 5.739999999999999, + -0.7017393546406622 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 326, + "type": "imguizmo", + "translation": [ + -1.3169524287476526, + 5.739999999999999, + -0.7535001037918813 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 327, + "type": "imguizmo", + "translation": [ + -1.2960116656212868, + 5.739999999999999, + -0.8015069533282154 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 328, + "type": "imguizmo", + "translation": [ + -1.2734562333440576, + 5.739999999999999, + -0.8455207355400927 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 329, + "type": "imguizmo", + "translation": [ + -1.2493142331758897, + 5.739999999999999, + -0.8853221759784939 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 330, + "type": "imguizmo", + "translation": [ + -1.2236157430427361, + 5.739999999999999, + -0.920712985869228 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 331, + "type": "imguizmo", + "translation": [ + -1.1963927800632257, + 5.739999999999999, + -0.9515168499776305 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 332, + "type": "imguizmo", + "translation": [ + -1.167679260659318, + 5.739999999999999, + -0.9775803050021965 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 333, + "type": "imguizmo", + "translation": [ + -1.1375109583006704, + 5.739999999999999, + -0.9987735041210243 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 334, + "type": "imguizmo", + "translation": [ + -1.1059254589353449, + 5.739999999999999, + -1.014990863882154 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 335, + "type": "imguizmo", + "translation": [ + -1.0729621141624, + 5.739999999999999, + -1.0261515902150142 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 336, + "type": "imguizmo", + "translation": [ + -1.0386619922046958, + 5.739999999999999, + -1.0322000809424152 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 337, + "type": "imguizmo", + "translation": [ + -1.0030678267430058, + 5.739999999999999, + -1.0331062027878024 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 338, + "type": "imguizmo", + "translation": [ + -0.9662239636751683, + 5.739999999999999, + -1.028865441497721 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 339, + "type": "imguizmo", + "translation": [ + -0.928176305866622, + 5.739999999999999, + -1.019498924331601 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 340, + "type": "imguizmo", + "translation": [ + -0.8889722559611519, + 5.739999999999999, + -1.0050533148068148 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 341, + "type": "imguizmo", + "translation": [ + -0.8486606573230968, + 5.739999999999999, + -0.9856005802233777 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 342, + "type": "imguizmo", + "translation": [ + -0.8072917331845998, + 5.739999999999999, + -0.9612376331264826 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 343, + "type": "imguizmo", + "translation": [ + -0.7649170240737154, + 5.739999999999999, + -0.9320858484930729 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 344, + "type": "imguizmo", + "translation": [ + -0.7215893236013295, + 5.739999999999999, + -0.8982904590478125 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 345, + "type": "imguizmo", + "translation": [ + -0.6773626126868937, + 5.739999999999999, + -0.8600198317209585 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 346, + "type": "imguizmo", + "translation": [ + -0.6322919923049226, + 5.739999999999999, + -0.8174646288527819 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 347, + "type": "imguizmo", + "translation": [ + -0.5864336148360412, + 5.739999999999999, + -0.7708368583233816 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 348, + "type": "imguizmo", + "translation": [ + -0.5398446141081069, + 5.739999999999999, + -0.7203688173400845 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 349, + "type": "imguizmo", + "translation": [ + -0.49258303421458105, + 5.739999999999999, + -0.6663119351444512 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 350, + "type": "imguizmo", + "translation": [ + -0.4447077571988121, + 5.739999999999999, + -0.6089355204044363 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 351, + "type": "imguizmo", + "translation": [ + -0.3962784296943429, + 5.739999999999999, + -0.5485254195321534 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 352, + "type": "imguizmo", + "translation": [ + -0.3473553886126325, + 5.739999999999999, + -0.485382592611428 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 353, + "type": "imguizmo", + "translation": [ + -0.297999585970778, + 5.739999999999999, + -0.41982161402979834 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 354, + "type": "imguizmo", + "translation": [ + -0.2482725129528914, + 5.739999999999999, + -0.35216910528474077 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 355, + "type": "imguizmo", + "translation": [ + -0.19823612329974252, + 5.739999999999999, + -0.28276210777180033 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 356, + "type": "imguizmo", + "translation": [ + -0.14795275612210698, + 5.739999999999999, + -0.21194640366130502 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 357, + "type": "imguizmo", + "translation": [ + -0.097485058234006, + 5.739999999999999, + -0.1400747932289969 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 358, + "type": "imguizmo", + "translation": [ + -0.04689590610257057, + 5.739999999999999, + -0.0675053372227981 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 359, + "type": "imguizmo", + "translation": [ + 0.003751672488204884, + 5.739999999999999, + 0.005400426977823114 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 360, + "type": "action", + "action": "set_active_render_window", + "value": 0 + }, + { + "frame": 360, + "type": "action", + "action": "set_active_planar", + "value": 2 + }, + { + "frame": 360, + "type": "action", + "action": "set_projection_type", + "value": "perspective" + }, + { + "frame": 361, + "type": "imguizmo", + "translation": [ + 0.037500000000000006, + 0.0, + 0.0 + ], + "rotation_deg": [ + 0.0, + 0.7, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 362, + "type": "imguizmo", + "translation": [ + 0.037482339715127086, + 0.0008004104013847341, + 0.0019997794888495157 + ], + "rotation_deg": [ + 0.014822414840458037, + 0.6997383661500308, + 0.02370109023821648 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 363, + "type": "imguizmo", + "translation": [ + 0.03743079368581399, + 0.001535790758968834, + 0.0038296138177695855 + ], + "rotation_deg": [ + 0.0296263627977706, + 0.6989537905633207, + 0.047284102837067216 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 364, + "type": "imguizmo", + "translation": [ + 0.03734672592896117, + 0.0022743803390788625, + 0.005653167792013589 + ], + "rotation_deg": [ + 0.04439339999622906, + 0.6976472507233524, + 0.07063154841404629 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 365, + "type": "imguizmo", + "translation": [ + 0.037230137198767894, + 0.003009726514375728, + 0.007447534081582464 + ], + "rotation_deg": [ + 0.059105128546334026, + 0.6958203744160708, + 0.09362711116970135 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 366, + "type": "imguizmo", + "translation": [ + 0.03708118106897446, + 0.003741355730028776, + 0.009204879041947101 + ], + "rotation_deg": [ + 0.0737432194662747, + 0.6934754377018649, + 0.1161562283670957 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 367, + "type": "imguizmo", + "translation": [ + 0.036900042454873945, + 0.004468321059545822, + 0.010916359205066808 + ], + "rotation_deg": [ + 0.08828943551755786, + 0.69061536207988, + 0.13810666107760092 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 368, + "type": "imguizmo", + "translation": [ + 0.03668694708586311, + 0.005189719627258477, + 0.012573455139966608 + ], + "rotation_deg": [ + 0.10272565392633597, + 0.6872437108481929, + 0.15936905334959353 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 369, + "type": "imguizmo", + "translation": [ + 0.036442160447977094, + 0.005904652433774164, + 0.014167910711884685 + ], + "rotation_deg": [ + 0.11703388896212669, + 0.6833646846643872, + 0.17983747701430533 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 370, + "type": "imguizmo", + "translation": [ + 0.03616598751522562, + 0.006612228779970917, + 0.015691782468658773 + ], + "rotation_deg": [ + 0.13119631434579354, + 0.678983116312056, + 0.19940995941463166 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 371, + "type": "imguizmo", + "translation": [ + 0.03585877236466551, + 0.007311567112381287, + 0.017137478554226232 + ], + "rotation_deg": [ + 0.14519528545886964, + 0.6741044646797563, + 0.21798899142777994 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 372, + "type": "imguizmo", + "translation": [ + 0.03552089774812088, + 0.008001796142643488, + 0.018497796583699353 + ], + "rotation_deg": [ + 0.15901336132655539, + 0.6687348079599128, + 0.23548201325081422 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 373, + "type": "imguizmo", + "translation": [ + 0.03515278461529029, + 0.008682055931326585, + 0.0197659595210582 + ], + "rotation_deg": [ + 0.17263332634700115, + 0.6628808360761476, + 0.25180187552893807 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 374, + "type": "imguizmo", + "translation": [ + 0.03475489158929837, + 0.009351498959443256, + 0.020935649442341227 + ], + "rotation_deg": [ + 0.18603821173980334, + 0.656549842348468, + 0.26686727352919565 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 375, + "type": "imguizmo", + "translation": [ + 0.034327714395308274, + 0.010009291184342104, + 0.022001039011197647 + ], + "rotation_deg": [ + 0.1992113166869919, + 0.6497497144066984, + 0.2806031521965623 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 376, + "type": "imguizmo", + "translation": [ + 0.03387178524290992, + 0.010654613078821805, + 0.022956820510386156 + ], + "rotation_deg": [ + 0.21213622914016986, + 0.6424889243634754, + 0.29294108007445296 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 377, + "type": "imguizmo", + "translation": [ + 0.03338767216305311, + 0.011286660652159872, + 0.02379823228455507 + ], + "rotation_deg": [ + 0.22479684626788168, + 0.6347765182590505, + 0.3038195902267965 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 378, + "type": "imguizmo", + "translation": [ + 0.03287597830035166, + 0.01190464645178498, + 0.024521082462569756 + ], + "rotation_deg": [ + 0.2371773945177364, + 0.6266221047910505, + 0.3131844864632196 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 379, + "type": "imguizmo", + "translation": [ + 0.03233734116164035, + 0.012507800544344843, + 0.02512176984120383 + ], + "rotation_deg": [ + 0.24926244926828958, + 0.6180358433432368, + 0.32098911334174113 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 380, + "type": "imguizmo", + "translation": [ + 0.03177243182172079, + 0.013095371474947265, + 0.025597301826152427 + ], + "rotation_deg": [ + 0.26103695404620086, + 0.6090284313281781, + 0.3271945886038368 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 381, + "type": "imguizmo", + "translation": [ + 0.031181954087285777, + 0.01366662720337937, + 0.025945309340986527 + ], + "rotation_deg": [ + 0.272486239284725, + 0.5996110908596065, + 0.3317699968838959 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 382, + "type": "imguizmo", + "translation": [ + 0.030566643620063783, + 0.014220856016138551, + 0.026164058629772516 + ], + "rotation_deg": [ + 0.2835960406001648, + 0.5897955547710614, + 0.334692543728017 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 383, + "type": "imguizmo", + "translation": [ + 0.029927267020276076, + 0.014757367413138881, + 0.0262524598945571 + ], + "rotation_deg": [ + 0.2943525165635178, + 0.5795940519982398, + 0.33594766915483143 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 384, + "type": "imguizmo", + "translation": [ + 0.029264620871548297, + 0.01527549296798822, + 0.026210072724685844 + ], + "rotation_deg": [ + 0.30474226594517273, + 0.569019292343265, + 0.3355291201925975 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 385, + "type": "imguizmo", + "translation": [ + 0.028579530748466433, + 0.015774587160764266, + 0.0260371082909069 + ], + "rotation_deg": [ + 0.3147523444111726, + 0.5580844506398562, + 0.33343898203119127 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 386, + "type": "imguizmo", + "translation": [ + 0.027872850188013703, + 0.016254028182252002, + 0.02573442829332898 + ], + "rotation_deg": [ + 0.3243702806502433, + 0.5468031503391262, + 0.32968766763379587 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 387, + "type": "imguizmo", + "translation": [ + 0.02714545962616979, + 0.01671321870864055, + 0.025303540668474744 + ], + "rotation_deg": [ + 0.33358409191149463, + 0.5351894465364576, + 0.32429386586004216 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 388, + "type": "imguizmo", + "translation": [ + 0.026398265300997186, + 0.017151586645714253, + 0.024746592076816922 + ], + "rotation_deg": [ + 0.3423822989334351, + 0.5232578084606041, + 0.3172844483590506 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 389, + "type": "imguizmo", + "translation": [ + 0.025632198123581477, + 0.01756858584161082, + 0.02406635720822379 + ], + "rotation_deg": [ + 0.35075394024570283, + 0.5110231014468334, + 0.3086943356962268 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 390, + "type": "imguizmo", + "translation": [ + 0.02484821251823199, + 0.017963696767258567, + 0.023266224958593804 + ], + "rotation_deg": [ + 0.3586885858256931, + 0.4985005684165705, + 0.2985663233807609 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 391, + "type": "imguizmo", + "translation": [ + 0.024047285233388015, + 0.0183364271636449, + 0.022350181546546777 + ], + "rotation_deg": [ + 0.3661763500930662, + 0.48570581088661596, + 0.28695086866055436 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 392, + "type": "imguizmo", + "translation": [ + 0.023230414124711826, + 0.018686312655109782, + 0.021322790654283122 + ], + "rotation_deg": [ + 0.37320790422595135, + 0.47265476953159796, + 0.2739058391467479 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 393, + "type": "imguizmo", + "translation": [ + 0.022398616911884814, + 0.019012917327899993, + 0.020189170691548783 + ], + "rotation_deg": [ + 0.3797744877834957, + 0.4593637043238779, + 0.2594962245201946 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 394, + "type": "imguizmo", + "translation": [ + 0.021552929910655477, + 0.01931583427326332, + 0.018954969295975253 + ], + "rotation_deg": [ + 0.3858679196202836, + 0.44584917427564835, + 0.24379381275613804 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 395, + "type": "imguizmo", + "translation": [ + 0.02069440674171906, + 0.01959468609440627, + 0.01762633519683311 + ], + "rotation_deg": [ + 0.39148060807902463, + 0.43212801680846674, + 0.22687683248013177 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 396, + "type": "imguizmo", + "translation": [ + 0.01982411701803736, + 0.01984912537668343, + 0.016209887582371914 + ], + "rotation_deg": [ + 0.39660556044881345, + 0.4182173267759242, + 0.2088295632369606 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 397, + "type": "imguizmo", + "translation": [ + 0.018943145012234153, + 0.02007883512043287, + 0.014712683123357464 + ], + "rotation_deg": [ + 0.40123639167717656, + 0.4041344351655874, + 0.18974191561418913 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 398, + "type": "imguizmo", + "translation": [ + 0.01805258830572656, + 0.020283529135918237, + 0.013142180817092872 + ], + "rotation_deg": [ + 0.4053673323250529, + 0.3898968875067451, + 0.1697089833121358 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 399, + "type": "imguizmo", + "translation": [ + 0.017153556421275244, + 0.02046295239988554, + 0.011506204827069306 + ], + "rotation_deg": [ + 0.40899323575479624, + 0.3755224220108648, + 0.1488305693918427 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 400, + "type": "imguizmo", + "translation": [ + 0.016247169440657218, + 0.020616881373290392, + 0.00981290550337695 + ], + "rotation_deg": [ + 0.412109584542245, + 0.3610289474719887, + 0.12721068906124425 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 401, + "type": "imguizmo", + "translation": [ + 0.015334556609183439, + 0.02074512427979976, + 0.008070718778070907 + ], + "rotation_deg": [ + 0.4147124961048701, + 0.3464345209546077, + 0.1049570514766347 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 402, + "type": "imguizmo", + "translation": [ + 0.014416854928799771, + 0.020847521344721443, + 0.006288324137782001 + ], + "rotation_deg": [ + 0.4167987275389892, + 0.3317573252968066, + 0.08218052314106859 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 403, + "type": "imguizmo", + "translation": [ + 0.013495207741524129, + 0.02092394499406345, + 0.004474601382950985 + ], + "rotation_deg": [ + 0.4183656796600214, + 0.31701564645671276, + 0.058994575573019674 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 404, + "type": "imguizmo", + "translation": [ + 0.012570763304984692, + 0.020974300013475267, + 0.0026385863891093806 + ], + "rotation_deg": [ + 0.41941140024074697, + 0.3022278507304685, + 0.0355147199969831 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 405, + "type": "imguizmo", + "translation": [ + 0.011644673361833922, + 0.020998523666873114, + 0.0007894260906021722 + ], + "rotation_deg": [ + 0.4199345864435394, + 0.28741236187011404, + 0.011857931872366372 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 406, + "type": "imguizmo", + "translation": [ + 0.01071809170482059, + 0.020996585774601288, + -0.0010636670889790912 + ], + "rotation_deg": [ + 0.4199345864435394, + 0.27258763812988596, + -0.011857931872366542 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 407, + "type": "imguizmo", + "translation": [ + 0.009792172739307726, + 0.02096848875103224, + -0.002911461132627139 + ], + "rotation_deg": [ + 0.41941140024074697, + 0.2577721492695314, + -0.03551471999698341 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 408, + "type": "imguizmo", + "translation": [ + 0.00886807004502728, + 0.020914267601558588, + -0.0047447504233633555 + ], + "rotation_deg": [ + 0.4183656796600214, + 0.24298435354328732, + -0.058994575573019695 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 409, + "type": "imguizmo", + "translation": [ + 0.007946934938863372, + 0.02083398987898074, + -0.006554401606158599 + ], + "rotation_deg": [ + 0.4167987275389892, + 0.22824267470319334, + -0.08218052314106876 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 410, + "type": "imguizmo", + "translation": [ + 0.007029915040454912, + 0.020727755599344533, + -0.008331399089848372 + ], + "rotation_deg": [ + 0.4147124961048701, + 0.21356547904539228, + -0.10495705147663488 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 411, + "type": "imguizmo", + "translation": [ + 0.00611815284240437, + 0.020595697117333674, + -0.010066889962354619 + ], + "rotation_deg": [ + 0.412109584542245, + 0.19897105252801126, + -0.12721068906124441 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 412, + "type": "imguizmo", + "translation": [ + 0.005212784286874287, + 0.02043797896137231, + -0.011752228095448349 + ], + "rotation_deg": [ + 0.40899323575479624, + 0.1844775779891353, + -0.14883056939184272 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 413, + "type": "imguizmo", + "translation": [ + 0.004314937350344763, + 0.02025479762864308, + -0.013379017219325592 + ], + "rotation_deg": [ + 0.4053673323250529, + 0.17010311249325488, + -0.169708983312136 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 414, + "type": "imguizmo", + "translation": [ + 0.0034257306382952735, + 0.020046381340276094, + -0.014939152752401161 + ], + "rotation_deg": [ + 0.40123639167717656, + 0.15586556483441263, + -0.18974191561418932 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 415, + "type": "imguizmo", + "translation": [ + 0.0025462719915614897, + 0.019812989757013844, + -0.016424862177926474 + ], + "rotation_deg": [ + 0.39660556044881345, + 0.1417826732240757, + -0.20882956323696095 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 416, + "type": "imguizmo", + "translation": [ + 0.0016776571061035855, + 0.019554913655706233, + -0.01782874376627699 + ], + "rotation_deg": [ + 0.3914806080790247, + 0.12787198319153337, + -0.22687683248013185 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 417, + "type": "imguizmo", + "translation": [ + 0.0008209681679054502, + 0.019272474567038823, + -0.01914380344999699 + ], + "rotation_deg": [ + 0.3858679196202836, + 0.11415082572435162, + -0.24379381275613823 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 418, + "type": "imguizmo", + "translation": [ + -2.2727495294201E-05, + 0.018966024374945666, + -0.020363489667891664 + ], + "rotation_deg": [ + 0.37977448778349565, + 0.10063629567612194, + -0.2594962245201949 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 419, + "type": "imguizmo", + "translation": [ + -0.0008523787437593401, + 0.01863594487820572, + -0.021481726004575534 + ], + "rotation_deg": [ + 0.37320790422595124, + 0.0873452304684018, + -0.27390583914674826 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 420, + "type": "imguizmo", + "translation": [ + -0.0016669519353458278, + 0.018282647314769127, + -0.022492941462868236 + ], + "rotation_deg": [ + 0.3661763500930662, + 0.07429418911338405, + -0.2869508686605546 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 421, + "type": "imguizmo", + "translation": [ + -0.002465432213290845, + 0.017906571849405903, + -0.02339209821822225 + ], + "rotation_deg": [ + 0.3586885858256931, + 0.06149943158342944, + -0.29856632338076117 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 422, + "type": "imguizmo", + "translation": [ + -0.003246824770597988, + 0.01750818702531549, + -0.02417471671691136 + ], + "rotation_deg": [ + 0.35075394024570283, + 0.048976898553166545, + -0.308694335696227 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 423, + "type": "imguizmo", + "translation": [ + -0.004010156089442942, + 0.017087989180380257, + -0.024836897992941996 + ], + "rotation_deg": [ + 0.3423822989334349, + 0.036742191539395806, + -0.31728444835905084 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 424, + "type": "imguizmo", + "translation": [ + -0.004754475154055448, + 0.0166465018287903, + -0.0253753430925057 + ], + "rotation_deg": [ + 0.3335840919114946, + 0.024810553463542426, + -0.32429386586004233 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 425, + "type": "imguizmo", + "translation": [ + -0.005478854635566584, + 0.016184275008809926, + -0.02578736950920107 + ], + "rotation_deg": [ + 0.3243702806502433, + 0.013196849660873741, + -0.32968766763379603 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 426, + "type": "imguizmo", + "translation": [ + -0.006182392047344966, + 0.015701884597498536, + -0.026070924548145677 + ], + "rotation_deg": [ + 0.3147523444111725, + 0.0019155493601436951, + -0.3334389820311915 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 427, + "type": "imguizmo", + "translation": [ + -0.006864210869382786, + 0.015199931593239452, + -0.026224595552398755 + ], + "rotation_deg": [ + 0.3047422659451726, + -0.009019292343265031, + -0.33552912019259773 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 428, + "type": "imguizmo", + "translation": [ + -0.007523461640330552, + 0.014679041366970819, + -0.026247616940747006 + ], + "rotation_deg": [ + 0.29435251656351785, + -0.019594051998239652, + -0.33594766915483176 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 429, + "type": "imguizmo", + "translation": [ + -0.008159323015820195, + 0.014139862883051245, + -0.02613987402179168 + ], + "rotation_deg": [ + 0.28359604060016474, + -0.02979555477106141, + -0.3346925437280173 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 430, + "type": "imguizmo", + "translation": [ + -0.008771002791757822, + 0.013583067890731057, + -0.025901903565335387 + ], + "rotation_deg": [ + 0.27248623928472493, + -0.03961109085960652, + -0.3317699968838962 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 431, + "type": "imguizmo", + "translation": [ + -0.009357738891311397, + 0.013009350087236372, + -0.025534891128221906 + ], + "rotation_deg": [ + 0.2610369540462009, + -0.0490284313281782, + -0.3271945886038371 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 432, + "type": "imguizmo", + "translation": [ + -0.009918800314363567, + 0.012419424253508744, + -0.025040665147951665 + ], + "rotation_deg": [ + 0.2492624492682897, + -0.05803584334323679, + -0.32098911334174146 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 433, + "type": "imguizmo", + "translation": [ + -0.010453488048246824, + 0.011814025363677084, + -0.024421687833498032 + ], + "rotation_deg": [ + 0.2371773945177365, + -0.06662210479105055, + -0.31318448646322 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 434, + "type": "imguizmo", + "translation": [ + -0.010961135938626158, + 0.011193907669371459, + -0.023681042898706112 + ], + "rotation_deg": [ + 0.22479684626788177, + -0.07477651825905052, + -0.3038195902267967 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 435, + "type": "imguizmo", + "translation": [ + -0.011441111519444495, + 0.010559843760019469, + -0.022822420199385483 + ], + "rotation_deg": [ + 0.21213622914016986, + -0.08248892436347544, + -0.2929410800744532 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 436, + "type": "imguizmo", + "translation": [ + -0.011892816800896579, + 0.009912623600296027, + -0.02185009735063412 + ], + "rotation_deg": [ + 0.1992113166869921, + -0.08974971440669835, + -0.2806031521965627 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 437, + "type": "imguizmo", + "translation": [ + -0.012315689014449863, + 0.009253053545925717, + -0.020768918415975165 + ], + "rotation_deg": [ + 0.1860382117398035, + -0.09654984234846804, + -0.266867273529196 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 438, + "type": "imguizmo", + "translation": [ + -0.012709201313983974, + 0.008581955339064019, + -0.019584269774476144 + ], + "rotation_deg": [ + 0.1726333263470012, + -0.10288083607614762, + -0.2518018755289384 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 439, + "type": "imguizmo", + "translation": [ + -0.0130728634321754, + 0.007900165084508881, + -0.018302053286079367 + ], + "rotation_deg": [ + 0.15901336132655544, + -0.10873480795991293, + -0.2354820132508146 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 440, + "type": "imguizmo", + "translation": [ + -0.013406222291309527, + 0.007208532208018262, + -0.016928656888832584 + ], + "rotation_deg": [ + 0.14519528545886967, + -0.11410446467975643, + -0.2179889914277802 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 441, + "type": "imguizmo", + "translation": [ + -0.013708862567759031, + 0.006507918398031398, + -0.015470922774502942 + ], + "rotation_deg": [ + 0.1311963143457935, + -0.1189831163120562, + -0.1994099594146319 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 442, + "type": "imguizmo", + "translation": [ + -0.013980407209425422, + 0.005799196532112338, + -0.013936113301121775 + ], + "rotation_deg": [ + 0.11703388896212667, + -0.1233646846643874, + -0.17983747701430552 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 443, + "type": "imguizmo", + "translation": [ + -0.014220517905498997, + 0.0050832495894531605, + -0.012331874812282206 + ], + "rotation_deg": [ + 0.10272565392633588, + -0.12724371084819314, + -0.15936905334959364 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 444, + "type": "imguizmo", + "translation": [ + -0.014428895507951985, + 0.0043609695507918845, + -0.010666199543440011 + ], + "rotation_deg": [ + 0.08828943551755793, + -0.13061536207988014, + -0.13810666107760122 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 445, + "type": "imguizmo", + "translation": [ + -0.014605280404239732, + 0.003633256287115495, + -0.008947385804998506 + ], + "rotation_deg": [ + 0.07374321946627474, + -0.1334754377018651, + -0.11615622836709598 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 446, + "type": "imguizmo", + "translation": [ + -0.014749452840745607, + 0.0029010164385328066, + -0.007183996640543674 + ], + "rotation_deg": [ + 0.05910512854633401, + -0.13582037441607092, + -0.09362711116970159 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 447, + "type": "imguizmo", + "translation": [ + -0.014861233196566641, + 0.002165162284713752, + -0.005384817166191659 + ], + "rotation_deg": [ + 0.04439339999622902, + -0.1376472507233525, + -0.07063154841404642 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 448, + "type": "imguizmo", + "translation": [ + -0.014940482207298825, + 0.0014266106083025282, + -0.003558810803582231 + ], + "rotation_deg": [ + 0.029626362797770714, + -0.13895379056332086, + -0.0472841028370676 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 449, + "type": "imguizmo", + "translation": [ + -0.014987101138543183, + 0.0006862815527205454, + -0.0017150746245629493 + ], + "rotation_deg": [ + 0.014822414840458112, + -0.13973836615003088, + -0.023701090238216805 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 450, + "type": "imguizmo", + "translation": [ + -0.015001031908916553, + -5.490252421763199E-05, + 0.00013720596996502914 + ], + "rotation_deg": [ + 3.8163916471489756E-17, + -0.14000000000000004, + -2.636779683484747E-16 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 451, + "type": "imguizmo", + "translation": [ + -0.014982257162413764, + -0.0007960181994473137, + 0.0019888030112523115 + ], + "rotation_deg": [ + -0.014822414840458036, + -0.13973836615003088, + 0.023701090238216278 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 452, + "type": "imguizmo", + "translation": [ + -0.014930800290031056, + -0.0015361421351238196, + 0.0038304919359773647 + ], + "rotation_deg": [ + -0.029626362797770638, + -0.13895379056332086, + 0.04728410283706707 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 453, + "type": "imguizmo", + "translation": [ + -0.014846725400623805, + -0.0022743522289864577, + 0.005653097542556974 + ], + "rotation_deg": [ + -0.04439339999622913, + -0.1376472507233525, + 0.0706315484140462 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 454, + "type": "imguizmo", + "translation": [ + -0.014730137241034882, + -0.003009728763183117, + 0.007447539701539008 + ], + "rotation_deg": [ + -0.05910512854633413, + -0.13582037441607092, + 0.09362711116970135 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 455, + "type": "imguizmo", + "translation": [ + -0.014581181065593106, + -0.003741355550124163, + 0.009204878592350548 + ], + "rotation_deg": [ + -0.07374321946627448, + -0.13347543770186515, + 0.11615622836709519 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 456, + "type": "imguizmo", + "translation": [ + -0.014400042455144462, + -0.004468321073938172, + 0.010916359241034509 + ], + "rotation_deg": [ + -0.08828943551755766, + -0.13061536207988012, + 0.13810666107760047 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 457, + "type": "imguizmo", + "translation": [ + -0.01418694708584148, + -0.005189719626107071, + 0.012573455137089174 + ], + "rotation_deg": [ + -0.10272565392633581, + -0.12724371084819316, + 0.15936905334959314 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 458, + "type": "imguizmo", + "translation": [ + -0.013942160447978831, + -0.0059046524338662604, + 0.014167910712114862 + ], + "rotation_deg": [ + -0.11703388896212659, + -0.12336468466438734, + 0.17983747701430502 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 459, + "type": "imguizmo", + "translation": [ + -0.013665987515225492, + -0.006612228779963537, + 0.015691782468640354 + ], + "rotation_deg": [ + -0.13119631434579346, + -0.11898311631205628, + 0.19940995941463147 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 460, + "type": "imguizmo", + "translation": [ + -0.013358772364665527, + -0.007311567112381866, + 0.0171374785542277 + ], + "rotation_deg": [ + -0.14519528545886964, + -0.11410446467975652, + 0.21798899142777975 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 461, + "type": "imguizmo", + "translation": [ + -0.013020897748120881, + -0.008001796142643432, + 0.018497796583699235 + ], + "rotation_deg": [ + -0.15901336132655539, + -0.10873480795991287, + 0.23548201325081408 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 462, + "type": "imguizmo", + "translation": [ + -0.012652784615290293, + -0.00868205593132658, + 0.019765959521058214 + ], + "rotation_deg": [ + -0.1726333263470012, + -0.10288083607614763, + 0.25180187552893796 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 463, + "type": "imguizmo", + "translation": [ + -0.012254891589298376, + -0.00935149895944324, + 0.02093564944234122 + ], + "rotation_deg": [ + -0.1860382117398033, + -0.09654984234846813, + 0.2668672735291954 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 464, + "type": "imguizmo", + "translation": [ + -0.011827714395308275, + -0.01000929118434209, + 0.022001039011197644 + ], + "rotation_deg": [ + -0.1992113166869919, + -0.08974971440669843, + 0.2806031521965621 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 465, + "type": "imguizmo", + "translation": [ + -0.01137178524290993, + -0.010654613078821791, + 0.022956820510386156 + ], + "rotation_deg": [ + -0.21213622914016989, + -0.0824889243634754, + 0.2929410800744528 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 466, + "type": "imguizmo", + "translation": [ + -0.010887672163053115, + -0.01128666065215986, + 0.02379823228455506 + ], + "rotation_deg": [ + -0.2247968462678817, + -0.07477651825905048, + 0.3038195902267964 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 467, + "type": "imguizmo", + "translation": [ + -0.010375978300351661, + -0.01190464645178497, + 0.024521082462569756 + ], + "rotation_deg": [ + -0.23717739451773645, + -0.06662210479105057, + 0.3131844864632196 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 468, + "type": "imguizmo", + "translation": [ + -0.009837341161640353, + -0.012507800544344838, + 0.025121769841203834 + ], + "rotation_deg": [ + -0.24926244926828975, + -0.05803584334323682, + 0.32098911334174113 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 469, + "type": "imguizmo", + "translation": [ + -0.009272431821720788, + -0.013095371474947257, + 0.02559730182615243 + ], + "rotation_deg": [ + -0.26103695404620103, + -0.049028431328178024, + 0.3271945886038368 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 470, + "type": "imguizmo", + "translation": [ + -0.008681954087285768, + -0.01366662720337936, + 0.025945309340986527 + ], + "rotation_deg": [ + -0.27248623928472504, + -0.039611090859606415, + 0.33176999688389586 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 471, + "type": "imguizmo", + "translation": [ + -0.00806664362006379, + -0.01422085601613853, + 0.026164058629772516 + ], + "rotation_deg": [ + -0.28359604060016463, + -0.029795554771061578, + 0.33469254372801693 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 472, + "type": "imguizmo", + "translation": [ + -0.007427267020276088, + -0.014757367413138864, + 0.0262524598945571 + ], + "rotation_deg": [ + -0.2943525165635178, + -0.019594051998239922, + 0.3359476691548314 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 473, + "type": "imguizmo", + "translation": [ + -0.0067646208715482995, + -0.0152754929679882, + 0.026210072724685847 + ], + "rotation_deg": [ + -0.3047422659451727, + -0.009019292343265038, + 0.33552912019259745 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 474, + "type": "imguizmo", + "translation": [ + -0.006079530748466435, + -0.015774587160764245, + 0.026037108290906903 + ], + "rotation_deg": [ + -0.31475234441117256, + 0.0019155493601437212, + 0.3334389820311912 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 475, + "type": "imguizmo", + "translation": [ + -0.0053728501880136986, + -0.01625402818225199, + 0.025734428293328972 + ], + "rotation_deg": [ + -0.3243702806502435, + 0.0131968496608738, + 0.32968766763379576 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 476, + "type": "imguizmo", + "translation": [ + -0.004645459626169784, + -0.01671321870864054, + 0.025303540668474733 + ], + "rotation_deg": [ + -0.33358409191149474, + 0.024810553463542485, + 0.324293865860042 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 477, + "type": "imguizmo", + "translation": [ + -0.0038982653009971835, + -0.017151586645714246, + 0.024746592076816912 + ], + "rotation_deg": [ + -0.3423822989334353, + 0.03674219153939606, + 0.31728444835905045 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 478, + "type": "imguizmo", + "translation": [ + -0.003132198123581465, + -0.017568585841610813, + 0.024066357208223768 + ], + "rotation_deg": [ + -0.3507539402457032, + 0.0489768985531668, + 0.3086943356962265 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 479, + "type": "imguizmo", + "translation": [ + -0.002348212518232, + -0.017963696767258543, + 0.02326622495859381 + ], + "rotation_deg": [ + -0.35868858582569313, + 0.06149943158342936, + 0.29856632338076095 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 480, + "type": "imguizmo", + "translation": [ + -0.0015472852333880207, + -0.018336427163644878, + 0.022350181546546777 + ], + "rotation_deg": [ + -0.3661763500930663, + 0.07429418911338394, + 0.28695086866055436 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 481, + "type": "imguizmo", + "translation": [ + -0.0007304141247118297, + -0.01868631265510976, + 0.021322790654283122 + ], + "rotation_deg": [ + -0.37320790422595146, + 0.08734523046840188, + 0.2739058391467479 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 482, + "type": "imguizmo", + "translation": [ + 0.00010138308811518618, + -0.019012917327899972, + 0.020189170691548766 + ], + "rotation_deg": [ + -0.3797744877834958, + 0.10063629567612202, + 0.25949622452019444 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 483, + "type": "imguizmo", + "translation": [ + 0.0009470700893445218, + -0.0193158342732633, + 0.01895496929597525 + ], + "rotation_deg": [ + -0.3858679196202837, + 0.11415082572435163, + 0.24379381275613798 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 484, + "type": "imguizmo", + "translation": [ + 0.0018055932582809425, + -0.019594686094406254, + 0.01762633519683309 + ], + "rotation_deg": [ + -0.3914806080790248, + 0.12787198319153337, + 0.22687683248013157 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 485, + "type": "imguizmo", + "translation": [ + 0.0026758829819626493, + -0.01984912537668341, + 0.0162098875823719 + ], + "rotation_deg": [ + -0.39660556044881357, + 0.14178267322407587, + 0.2088295632369604 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 486, + "type": "imguizmo", + "translation": [ + 0.0035568549877658525, + -0.020078835120432847, + 0.014712683123357438 + ], + "rotation_deg": [ + -0.40123639167717673, + 0.1558655648344128, + 0.18974191561418877 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 487, + "type": "imguizmo", + "translation": [ + 0.004447411694273425, + -0.020283529135918205, + 0.013142180817092886 + ], + "rotation_deg": [ + -0.40536733232505295, + 0.17010311249325472, + 0.1697089833121359 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 488, + "type": "imguizmo", + "translation": [ + 0.005346443578724748, + -0.020462952399885517, + 0.01150620482706931 + ], + "rotation_deg": [ + -0.40899323575479635, + 0.18447757798913514, + 0.1488305693918427 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 489, + "type": "imguizmo", + "translation": [ + 0.006252830559342776, + -0.020616881373290364, + 0.009812905503376955 + ], + "rotation_deg": [ + -0.41210958454224506, + 0.19897105252801117, + 0.12721068906124428 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 490, + "type": "imguizmo", + "translation": [ + 0.007165443390816552, + -0.02074512427979973, + 0.00807071877807091 + ], + "rotation_deg": [ + -0.41471249610487015, + 0.2135654790453922, + 0.1049570514766347 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 491, + "type": "imguizmo", + "translation": [ + 0.008083145071200221, + -0.020847521344721422, + 0.006288324137782004 + ], + "rotation_deg": [ + -0.4167987275389894, + 0.22824267470319326, + 0.0821805231410686 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 492, + "type": "imguizmo", + "translation": [ + 0.00900479225847587, + -0.020923944994063425, + 0.004474601382950978 + ], + "rotation_deg": [ + -0.4183656796600216, + 0.24298435354328723, + 0.05899457557301954 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 493, + "type": "imguizmo", + "translation": [ + 0.009929236695015307, + -0.020974300013475243, + 0.0026385863891093737 + ], + "rotation_deg": [ + -0.41941140024074713, + 0.2577721492695315, + 0.035514719996982955 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 494, + "type": "imguizmo", + "translation": [ + 0.010855326638166083, + -0.02099852366687309, + 0.0007894260906021531 + ], + "rotation_deg": [ + -0.4199345864435396, + 0.272587638129886, + 0.01185793187236608 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 495, + "type": "imguizmo", + "translation": [ + 0.01178190829517939, + -0.020996585774601267, + -0.001063667088979059 + ], + "rotation_deg": [ + -0.41993458644353965, + 0.2874123618701137, + -0.011857931872366244 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 496, + "type": "imguizmo", + "translation": [ + 0.012707827260692256, + -0.02096848875103222, + -0.0029114611326271113 + ], + "rotation_deg": [ + -0.4194114002407471, + 0.3022278507304682, + -0.03551471999698313 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 497, + "type": "imguizmo", + "translation": [ + 0.013631929954972716, + -0.020914267601558567, + -0.004744750423363354 + ], + "rotation_deg": [ + -0.4183656796600215, + 0.3170156464567125, + -0.0589945755730197 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 498, + "type": "imguizmo", + "translation": [ + 0.01455306506113662, + -0.020833989878980722, + -0.0065544016061585934 + ], + "rotation_deg": [ + -0.4167987275389894, + 0.33175732529680646, + -0.08218052314106876 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 499, + "type": "imguizmo", + "translation": [ + 0.01547008495954508, + -0.02072775559934451, + -0.008331399089848368 + ], + "rotation_deg": [ + -0.41471249610487015, + 0.3464345209546075, + -0.10495705147663488 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 500, + "type": "imguizmo", + "translation": [ + 0.016381847157595622, + -0.02059569711733365, + -0.010066889962354615 + ], + "rotation_deg": [ + -0.41210958454224506, + 0.36102894747198855, + -0.12721068906124444 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 501, + "type": "imguizmo", + "translation": [ + 0.017287215713125713, + -0.02043797896137229, + -0.011752228095448358 + ], + "rotation_deg": [ + -0.4089932357547963, + 0.3755224220108646, + -0.14883056939184286 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 502, + "type": "imguizmo", + "translation": [ + 0.018185062649655238, + -0.02025479762864306, + -0.013379017219325605 + ], + "rotation_deg": [ + -0.40536733232505295, + 0.389896887506745, + -0.16970898331213613 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 503, + "type": "imguizmo", + "translation": [ + 0.019074269361704702, + -0.020046381340276077, + -0.014939152752401128 + ], + "rotation_deg": [ + -0.4012363916771767, + 0.40413443516558695, + -0.18974191561418893 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 504, + "type": "imguizmo", + "translation": [ + 0.019953728008438486, + -0.01981298975701383, + -0.016424862177926446 + ], + "rotation_deg": [ + -0.3966055604488136, + 0.4182173267759239, + -0.20882956323696059 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 505, + "type": "imguizmo", + "translation": [ + 0.020822342893896405, + -0.019554913655706215, + -0.01782874376627699 + ], + "rotation_deg": [ + -0.3914806080790248, + 0.43212801680846646, + -0.22687683248013177 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 506, + "type": "imguizmo", + "translation": [ + 0.021679031832094537, + -0.019272474567038806, + -0.019143803449996985 + ], + "rotation_deg": [ + -0.3858679196202838, + 0.44584917427564813, + -0.24379381275613818 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 507, + "type": "imguizmo", + "translation": [ + 0.022522727495294187, + -0.018966024374945652, + -0.020363489667891654 + ], + "rotation_deg": [ + -0.37977448778349576, + 0.45936370432387774, + -0.2594962245201947 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 508, + "type": "imguizmo", + "translation": [ + 0.023352378743759325, + -0.018635944878205708, + -0.021481726004575527 + ], + "rotation_deg": [ + -0.3732079042259514, + 0.4726547695315979, + -0.27390583914674815 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 509, + "type": "imguizmo", + "translation": [ + 0.024166951935345824, + -0.018282647314769102, + -0.022492941462868236 + ], + "rotation_deg": [ + -0.36617635009306626, + 0.4857058108866158, + -0.28695086866055464 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 510, + "type": "imguizmo", + "translation": [ + 0.024965432213290846, + -0.017906571849405882, + -0.02339209821822225 + ], + "rotation_deg": [ + -0.35868858582569313, + 0.4985005684165704, + -0.2985663233807612 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 511, + "type": "imguizmo", + "translation": [ + 0.025746824770597965, + -0.01750818702531548, + -0.02417471671691134 + ], + "rotation_deg": [ + -0.35075394024570317, + 0.5110231014468329, + -0.30869433569622684 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 512, + "type": "imguizmo", + "translation": [ + 0.02651015608944292, + -0.017087989180380247, + -0.024836897992941982 + ], + "rotation_deg": [ + -0.3423822989334353, + 0.5232578084606037, + -0.3172844483590508 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 513, + "type": "imguizmo", + "translation": [ + 0.02725447515405544, + -0.01664650182879028, + -0.025375343092505692 + ], + "rotation_deg": [ + -0.33358409191149485, + 0.5351894465364573, + -0.3242938658600424 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 514, + "type": "imguizmo", + "translation": [ + 0.027978854635566573, + -0.016184275008809906, + -0.025787369509201064 + ], + "rotation_deg": [ + -0.32437028065024354, + 0.546803150339126, + -0.3296876676337961 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 515, + "type": "imguizmo", + "translation": [ + 0.028682392047344955, + -0.015701884597498515, + -0.02607092454814568 + ], + "rotation_deg": [ + -0.3147523444111727, + 0.558084450639856, + -0.3334389820311916 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 516, + "type": "imguizmo", + "translation": [ + 0.029364210869382772, + -0.015199931593239431, + -0.02622459555239876 + ], + "rotation_deg": [ + -0.30474226594517284, + 0.5690192923432646, + -0.33552912019259784 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 517, + "type": "imguizmo", + "translation": [ + 0.030023461640330554, + -0.014679041366970793, + -0.02624761694074701 + ], + "rotation_deg": [ + -0.29435251656351796, + 0.5795940519982395, + -0.3359476691548319 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 518, + "type": "imguizmo", + "translation": [ + 0.030659323015820188, + -0.014139862883051219, + -0.026139874021791683 + ], + "rotation_deg": [ + -0.28359604060016497, + 0.5897955547710613, + -0.33469254372801743 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 519, + "type": "imguizmo", + "translation": [ + 0.03127100279175782, + -0.013583067890731031, + -0.02590190356533539 + ], + "rotation_deg": [ + -0.27248623928472504, + 0.5996110908596064, + -0.3317699968838963 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 520, + "type": "imguizmo", + "translation": [ + 0.03185773889131139, + -0.013009350087236346, + -0.02553489112822191 + ], + "rotation_deg": [ + -0.26103695404620103, + 0.609028431328178, + -0.3271945886038372 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 521, + "type": "imguizmo", + "translation": [ + 0.03241880031436357, + -0.012419424253508709, + -0.025040665147951665 + ], + "rotation_deg": [ + -0.24926244926828967, + 0.6180358433432367, + -0.32098911334174157 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 522, + "type": "imguizmo", + "translation": [ + 0.03295348804824682, + -0.011814025363677046, + -0.024421687833498032 + ], + "rotation_deg": [ + -0.23717739451773645, + 0.6266221047910504, + -0.31318448646322 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 523, + "type": "imguizmo", + "translation": [ + 0.03346113593862615, + -0.01119390766937142, + -0.023681042898706112 + ], + "rotation_deg": [ + -0.22479684626788166, + 0.6347765182590503, + -0.3038195902267969 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 524, + "type": "imguizmo", + "translation": [ + 0.03394111151944449, + -0.010559843760019432, + -0.022822420199385476 + ], + "rotation_deg": [ + -0.21213622914016989, + 0.6424889243634753, + -0.29294108007445324 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 525, + "type": "imguizmo", + "translation": [ + 0.03439281680089658, + -0.00991262360029598, + -0.021850097350634104 + ], + "rotation_deg": [ + -0.19921131668699182, + 0.6497497144066983, + -0.2806031521965626 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 526, + "type": "imguizmo", + "translation": [ + 0.03481568901444986, + -0.00925305354592567, + -0.020768918415975144 + ], + "rotation_deg": [ + -0.18603821173980326, + 0.6565498423484679, + -0.26686727352919587 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 527, + "type": "imguizmo", + "translation": [ + 0.03520920131398396, + -0.008581955339063991, + -0.01958426977447615 + ], + "rotation_deg": [ + -0.17263332634700132, + 0.6628808360761473, + -0.25180187552893857 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 528, + "type": "imguizmo", + "translation": [ + 0.03557286343217538, + -0.007900165084508852, + -0.018302053286079374 + ], + "rotation_deg": [ + -0.15901336132655555, + 0.6687348079599127, + -0.23548201325081478 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 529, + "type": "imguizmo", + "translation": [ + 0.03590622229130951, + -0.007208532208018232, + -0.016928656888832595 + ], + "rotation_deg": [ + -0.14519528545886976, + 0.6741044646797562, + -0.21798899142778044 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 530, + "type": "imguizmo", + "translation": [ + 0.036208862567759015, + -0.006507918398031371, + -0.01547092277450295 + ], + "rotation_deg": [ + -0.1311963143457936, + 0.6789831163120559, + -0.19940995941463213 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 531, + "type": "imguizmo", + "translation": [ + 0.03648040720942541, + -0.005799196532112308, + -0.01393611330112178 + ], + "rotation_deg": [ + -0.11703388896212674, + 0.683364684664387, + -0.17983747701430575 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 532, + "type": "imguizmo", + "translation": [ + 0.03672051790549898, + -0.00508324958945313, + -0.01233187481228221 + ], + "rotation_deg": [ + -0.10272565392633595, + 0.6872437108481928, + -0.15936905334959386 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 533, + "type": "imguizmo", + "translation": [ + 0.03692889550795197, + -0.004360969550791845, + -0.010666199543439992 + ], + "rotation_deg": [ + -0.08828943551755784, + 0.6906153620798798, + -0.1381066610776012 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 534, + "type": "imguizmo", + "translation": [ + 0.03710528040423972, + -0.0036332562871154556, + -0.008947385804998487 + ], + "rotation_deg": [ + -0.07374321946627463, + 0.6934754377018648, + -0.11615622836709592 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 535, + "type": "imguizmo", + "translation": [ + 0.037249452840745594, + -0.0029010164385327884, + -0.007183996640543702 + ], + "rotation_deg": [ + -0.05910512854633429, + 0.6958203744160707, + -0.0936271111697021 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 536, + "type": "imguizmo", + "translation": [ + 0.037361233196566625, + -0.0021651622847137317, + -0.005384817166191684 + ], + "rotation_deg": [ + -0.044393399996229294, + 0.6976472507233523, + -0.07063154841404697 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 537, + "type": "imguizmo", + "translation": [ + 0.037440482207298814, + -0.0014266106083024978, + -0.0035588108035822306 + ], + "rotation_deg": [ + -0.029626362797770804, + 0.6989537905633206, + -0.04728410283706784 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 538, + "type": "imguizmo", + "translation": [ + 0.03748710113854317, + -0.000686281552720516, + -0.0017150746245629517 + ], + "rotation_deg": [ + -0.014822414840458199, + 0.6997383661500307, + -0.023701090238217055 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 539, + "type": "imguizmo", + "translation": [ + 0.037501031908916536, + 5.490252421766148E-05, + 0.00013720596996502697 + ], + "rotation_deg": [ + -1.249000902703301E-16, + 0.6999999999999998, + -5.169475958410885E-16 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 540, + "type": "action", + "action": "set_active_render_window", + "value": 0 + }, + { + "frame": 540, + "type": "action", + "action": "set_active_planar", + "value": 3 + }, + { + "frame": 540, + "type": "action", + "action": "set_projection_type", + "value": "perspective" + }, + { + "frame": 541, + "type": "imguizmo", + "translation": [ + 0.0, + 2.0, + 0.0 + ], + "rotation_deg": [ + 0.0, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 542, + "type": "imguizmo", + "translation": [ + 0.06670086678206116, + 1.999058118140111, + 0.0639929436431845 + ], + "rotation_deg": [ + 1.270492700610689, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 543, + "type": "imguizmo", + "translation": [ + 0.12798256324740281, + 1.996308996576746, + 0.12254764216862671 + ], + "rotation_deg": [ + 2.5394025255231947, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 544, + "type": "imguizmo", + "translation": [ + 0.18953169492323854, + 1.991825382877929, + 0.1809013693444348 + ], + "rotation_deg": [ + 3.8051485711053474, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 545, + "type": "imguizmo", + "translation": [ + 0.250810542864644, + 1.9856073172676207, + 0.23832109061063889 + ], + "rotation_deg": [ + 5.066153875400059, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 546, + "type": "imguizmo", + "translation": [ + 0.3117796441690647, + 1.9776629903453047, + 0.29455612934230735 + ], + "rotation_deg": [ + 6.320847382823546, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 547, + "type": "imguizmo", + "translation": [ + 0.37236008829548517, + 1.9680022642599442, + 0.34932349456213785 + ], + "rotation_deg": [ + 7.5676659015049585, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 548, + "type": "imguizmo", + "translation": [ + 0.43247663560487304, + 1.9566371779126996, + 0.40235056447893147 + ], + "rotation_deg": [ + 8.805056050828796, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 549, + "type": "imguizmo", + "translation": [ + 0.4920543694811803, + 1.9435818905587785, + 0.4533731427803099 + ], + "rotation_deg": [ + 10.031476196753717, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 550, + "type": "imguizmo", + "translation": [ + 0.5510190649975765, + 1.9288526674787003, + 0.5021370389970806 + ], + "rotation_deg": [ + 11.245398372496588, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 551, + "type": "imguizmo", + "translation": [ + 0.6092972593651071, + 1.9124678594488274, + 0.5483993137352393 + ], + "rotation_deg": [ + 12.445310182188825, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 552, + "type": "imguizmo", + "translation": [ + 0.6668163452202907, + 1.8944478798997806, + 0.5919294906783793 + ], + "rotation_deg": [ + 13.62971668513332, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 553, + "type": "imguizmo", + "translation": [ + 0.7235046609438818, + 1.8748151794821493, + 0.6325107046738623 + ], + "rotation_deg": [ + 14.797142258314384, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 554, + "type": "imguizmo", + "translation": [ + 0.7792915799536043, + 1.8535942180959133, + 0.6699407821549191 + ], + "rotation_deg": [ + 15.946132434840287, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 555, + "type": "imguizmo", + "translation": [ + 0.834107598695175, + 1.8308114344164415, + 0.7040332483583245 + ], + "rotation_deg": [ + 17.07525571602788, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 556, + "type": "imguizmo", + "translation": [ + 0.8878844232351503, + 1.806495212955196, + 0.7346182563323569 + ], + "rotation_deg": [ + 18.1831053548717, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 557, + "type": "imguizmo", + "translation": [ + 0.9405550543466554, + 1.7806758486961665, + 0.7615434331057618 + ], + "rotation_deg": [ + 19.268301108675573, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 558, + "type": "imguizmo", + "translation": [ + 0.9920538709820813, + 1.7533855093520891, + 0.7846746388022319 + ], + "rotation_deg": [ + 20.329490958663122, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 559, + "type": "imguizmo", + "translation": [ + 1.0423167120287367, + 1.724658195287486, + 0.8038966349185224 + ], + "rotation_deg": [ + 21.365352794424826, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 560, + "type": "imguizmo", + "translation": [ + 1.091280956245605, + 1.694529697158443, + 0.8191136584368774 + ], + "rotation_deg": [ + 22.374596061102935, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 561, + "type": "imguizmo", + "translation": [ + 1.1388856002816137, + 1.6630375513219087, + 0.8302498989115686 + ], + "rotation_deg": [ + 23.355963367262138, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 562, + "type": "imguizmo", + "translation": [ + 1.1850713346782122, + 1.630220993070069, + 0.8372498761527203 + ], + "rotation_deg": [ + 24.308232051442694, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 563, + "type": "imguizmo", + "translation": [ + 1.2297806177615729, + 1.5961209077480583, + 0.840078716625827 + ], + "rotation_deg": [ + 25.230215705444387, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 564, + "type": "imguizmo", + "translation": [ + 1.272957747332351, + 1.56077977981591, + 0.8387223271899468 + ], + "rotation_deg": [ + 26.120765652443374, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 565, + "type": "imguizmo", + "translation": [ + 1.3145489300636883, + 1.5242416399182106, + 0.8331874653090205 + ], + "rotation_deg": [ + 26.978772378100505, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 566, + "type": "imguizmo", + "translation": [ + 1.3545023485209997, + 1.4865520100273983, + 0.8235017053865271 + ], + "rotation_deg": [ + 27.803166912878, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 567, + "type": "imguizmo", + "translation": [ + 1.3927682257200455, + 1.4477578467290564, + 0.8097133013911916 + ], + "rotation_deg": [ + 28.592922163842395, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 568, + "type": "imguizmo", + "translation": [ + 1.429298887142854, + 1.4079074827198508, + 0.7918909464581413 + ], + "rotation_deg": [ + 29.347054194294437, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 569, + "type": "imguizmo", + "translation": [ + 1.4640488201342348, + 1.367050566591013, + 0.770123430663161 + ], + "rotation_deg": [ + 30.06462344963168, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 570, + "type": "imguizmo", + "translation": [ + 1.49697473060488, + 1.3252380009723737, + 0.7445191986750015 + ], + "rotation_deg": [ + 30.744735927916555, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 571, + "type": "imguizmo", + "translation": [ + 1.5280355969704076, + 1.2825218791140283, + 0.7152058094894966 + ], + "rotation_deg": [ + 31.386544293691394, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 572, + "type": "imguizmo", + "translation": [ + 1.5571927212591483, + 1.2389554199846315, + 0.6823293009370596 + ], + "rotation_deg": [ + 31.989248933652974, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 573, + "type": "imguizmo", + "translation": [ + 1.5844097773249985, + 1.1945929019671908, + 0.6460534621295606 + ], + "rotation_deg": [ + 32.55209895287106, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 574, + "type": "imguizmo", + "translation": [ + 1.609652856105276, + 1.1494895952349595, + 0.606559017471208 + ], + "rotation_deg": [ + 33.07439311031002, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 575, + "type": "imguizmo", + "translation": [ + 1.6328905078671885, + 1.103701692891684, + 0.5640427262986593 + ], + "rotation_deg": [ + 33.555480692487826, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 576, + "type": "imguizmo", + "translation": [ + 1.6540937813902852, + 1.0572862409619932, + 0.518716402635901 + ], + "rotation_deg": [ + 33.99476232418401, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 577, + "type": "imguizmo", + "translation": [ + 1.6732362600360713, + 1.010301067319156, + 0.4708058599474386 + ], + "rotation_deg": [ + 34.391690715186556, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 578, + "type": "imguizmo", + "translation": [ + 1.6902940946598521, + 0.9628047096387508, + 0.4205497861469717 + ], + "rotation_deg": [ + 34.745771342147385, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 579, + "type": "imguizmo", + "translation": [ + 1.7052460333237944, + 0.914856342468014, + 0.3681985544662176 + ], + "rotation_deg": [ + 35.05656306469681, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 580, + "type": "imguizmo", + "translation": [ + 1.7180734477741986, + 0.8665157035017191, + 0.3140129761080622 + ], + "rotation_deg": [ + 35.323678675049564, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 581, + "type": "imguizmo", + "translation": [ + 1.728760356649979, + 0.817843019156451, + 0.25826300089826887 + ], + "rotation_deg": [ + 35.54678538041743, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 582, + "type": "imguizmo", + "translation": [ + 1.7372934453934528, + 0.7688989295359886, + 0.20122637240902386 + ], + "rotation_deg": [ + 35.725605217627646, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 583, + "type": "imguizmo", + "translation": [ + 1.7436620828386202, + 0.7197444128812878, + 0.1431872442544314 + ], + "rotation_deg": [ + 35.859915399430406, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 584, + "type": "imguizmo", + "translation": [ + 1.7478583344562715, + 0.6704407095991844, + 0.08443476445150004 + ], + "rotation_deg": [ + 35.949548592064026, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 585, + "type": "imguizmo", + "translation": [ + 1.7498769722394254, + 0.6210492459644767, + 0.025261634899269386 + ], + "rotation_deg": [ + 35.99439312373195, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 586, + "type": "imguizmo", + "translation": [ + 1.7497154812167732, + 0.5716315575904323, + -0.034037346847331035 + ], + "rotation_deg": [ + 35.99439312373195, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 587, + "type": "imguizmo", + "translation": [ + 1.7473740625860192, + 0.5222492127630796, + -0.09316675624406856 + ], + "rotation_deg": [ + 35.949548592064026, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 588, + "type": "imguizmo", + "translation": [ + 1.7428556334632148, + 0.472963735734789, + -0.15183201354762751 + ], + "rotation_deg": [ + 35.85991539943041, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 589, + "type": "imguizmo", + "translation": [ + 1.7361658232483939, + 0.42383653007271394, + -0.20974085139707527 + ], + "rotation_deg": [ + 35.725605217627646, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 590, + "type": "imguizmo", + "translation": [ + 1.7273129666120435, + 0.37492880215759616, + -0.2666047708751479 + ], + "rotation_deg": [ + 35.54678538041743, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 591, + "type": "imguizmo", + "translation": [ + 1.7163080931111387, + 0.3263014849282339, + -0.32214047879534785 + ], + "rotation_deg": [ + 35.323678675049564, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 592, + "type": "imguizmo", + "translation": [ + 1.7031649134476914, + 0.27801516196662945, + -0.3760712990543473 + ], + "rotation_deg": [ + 35.05656306469682, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 593, + "type": "imguizmo", + "translation": [ + 1.6878998023869225, + 0.23012999201838824, + -0.42812855101841907 + ], + "rotation_deg": [ + 34.745771342147385, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 594, + "type": "imguizmo", + "translation": [ + 1.6705317783563403, + 0.18270563404241544, + -0.47805288807683727 + ], + "rotation_deg": [ + 34.391690715186556, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 595, + "type": "imguizmo", + "translation": [ + 1.6510824797511532, + 0.13580117288328034, + -0.5255955896936473 + ], + "rotation_deg": [ + 33.99476232418401, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 596, + "type": "imguizmo", + "translation": [ + 1.6295761379755185, + 0.08947504565885875, + -0.5705198005208638 + ], + "rotation_deg": [ + 33.555480692487826, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 597, + "type": "imguizmo", + "translation": [ + 1.6060395472532345, + 0.04378496895495825, + -0.6126017103999037 + ], + "rotation_deg": [ + 33.07439311031002, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 598, + "type": "imguizmo", + "translation": [ + 1.5805020312454716, + -0.0012121330823565701, + -0.6516316693725335 + ], + "rotation_deg": [ + 32.552098952871056, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 599, + "type": "imguizmo", + "translation": [ + 1.5529954065171427, + -0.04546019966716389, + -0.6874152321464173 + ], + "rotation_deg": [ + 31.989248933652966, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 600, + "type": "imguizmo", + "translation": [ + 1.5235539428974265, + -0.08890410321844325, + -0.7197741268117835 + ], + "rotation_deg": [ + 31.386544293691394, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 601, + "type": "imguizmo", + "translation": [ + 1.4922143207838245, + -0.13148971804217752, + -0.748547142983112 + ], + "rotation_deg": [ + 30.74473592791655, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 602, + "type": "imguizmo", + "translation": [ + 1.459015585442957, + -0.17316398776522515, + -0.7735909349411636 + ], + "rotation_deg": [ + 30.064623449631675, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 603, + "type": "imguizmo", + "translation": [ + 1.4239990983650208, + -0.21387499143695599, + -0.794780735774144 + ], + "rotation_deg": [ + 29.34705419429443, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 604, + "type": "imguizmo", + "translation": [ + 1.387208485732524, + -0.25357200821628967, + -0.8120109789601824 + ], + "rotation_deg": [ + 28.5929221638424, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 605, + "type": "imguizmo", + "translation": [ + 1.3486895840674933, + -0.2922055805635503, + -0.8251958242944343 + ], + "rotation_deg": [ + 27.803166912878005, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 606, + "type": "imguizmo", + "translation": [ + 1.308490383124877, + -0.3297275758583974, + -0.8342695855406619 + ], + "rotation_deg": [ + 26.97877237810051, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 607, + "type": "imguizmo", + "translation": [ + 1.266660966103287, + -0.366091246367081, + -0.8391870576767604 + ], + "rotation_deg": [ + 26.120765652443374, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 608, + "type": "imguizmo", + "translation": [ + 1.2232534472475676, + -0.4012512874842952, + -0.8399237421039044 + ], + "rotation_deg": [ + 25.230215705444394, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 609, + "type": "imguizmo", + "translation": [ + 1.1783219069209365, + -0.4351638941770763, + -0.8364759686973339 + ], + "rotation_deg": [ + 24.3082320514427, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 610, + "type": "imguizmo", + "translation": [ + 1.1319223242275875, + -0.4677868155604163, + -0.8288609140907326 + ], + "rotation_deg": [ + 23.35596336726214, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 611, + "type": "imguizmo", + "translation": [ + 1.084112507269697, + -0.49907940753660696, + -0.8171165161031011 + ], + "rotation_deg": [ + 22.374596061102938, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 612, + "type": "imguizmo", + "translation": [ + 1.0349520211257281, + -0.5290026834327227, + -0.8013012847344534 + ], + "rotation_deg": [ + 21.365352794424837, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 613, + "type": "imguizmo", + "translation": [ + 0.9845021136397563, + -0.5575193625731629, + -0.7814940106719371 + ], + "rotation_deg": [ + 20.329490958663133, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 614, + "type": "imguizmo", + "translation": [ + 0.9328256391142874, + -0.5845939167267277, + -0.757793372758596 + ], + "rotation_deg": [ + 19.268301108675576, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 615, + "type": "imguizmo", + "translation": [ + 0.8799869800016216, + -0.6101926143703722, + -0.7303174463803356 + ], + "rotation_deg": [ + 18.183105354871703, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 616, + "type": "imguizmo", + "translation": [ + 0.8260519666913348, + -0.6342835627144832, + -0.699203115220292 + ], + "rotation_deg": [ + 17.075255716027893, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 617, + "type": "imguizmo", + "translation": [ + 0.771087795493809, + -0.6568367474373251, + -0.6646053893112055 + ], + "rotation_deg": [ + 15.946132434840298, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 618, + "type": "imguizmo", + "translation": [ + 0.7151629449220009, + -0.6778240700791444, + -0.6266966327832368 + ], + "rotation_deg": [ + 14.797142258314391, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 619, + "type": "imguizmo", + "translation": [ + 0.6583470903757396, + -0.6972193830493539, + -0.58566570515454 + ], + "rotation_deg": [ + 13.629716685133324, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 620, + "type": "imguizmo", + "translation": [ + 0.6007110173348545, + -0.7149985222031738, + -0.541717020442643 + ], + "rotation_deg": [ + 12.445310182188827, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 621, + "type": "imguizmo", + "translation": [ + 0.5423265331692826, + -0.7311393369471475, + -0.4950695287840944 + ], + "rotation_deg": [ + 11.245398372496588, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 622, + "type": "imguizmo", + "translation": [ + 0.48326637767602754, + -0.7456217178360217, + -0.44595562563589697 + ], + "rotation_deg": [ + 10.031476196753715, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 623, + "type": "imguizmo", + "translation": [ + 0.4236041324544294, + -0.7584276216266125, + -0.3946199939930307 + ], + "rotation_deg": [ + 8.805056050828789, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 624, + "type": "imguizmo", + "translation": [ + 0.3634141292326564, + -0.7695410937574385, + -0.34131838539008047 + ], + "rotation_deg": [ + 7.567665901504963, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 625, + "type": "imguizmo", + "translation": [ + 0.3027713572596239, + -0.7789482882261183, + -0.2863163457599523 + ], + "rotation_deg": [ + 6.320847382823548, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 626, + "type": "imguizmo", + "translation": [ + 0.2417513698777332, + -0.7866374848397648, + -0.2298878924973977 + ], + "rotation_deg": [ + 5.066153875400057, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 627, + "type": "imguizmo", + "translation": [ + 0.18043019039281202, + -0.7925991038168868, + -0.1723141493181332 + ], + "rotation_deg": [ + 3.805148571105343, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 628, + "type": "imguizmo", + "translation": [ + 0.11888421735854332, + -0.7968257177226031, + -0.11388194571463153 + ], + "rotation_deg": [ + 2.5394025255232027, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 629, + "type": "imguizmo", + "translation": [ + 0.0571901293933781, + -0.7993120607223021, + -0.05488238798601452 + ], + "rotation_deg": [ + 1.2704927006106939, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 630, + "type": "imguizmo", + "translation": [ + -0.004575210351470023, + -0.8000550351422152, + 0.004390591038880794 + ], + "rotation_deg": [ + 1.9984014443252818E-15, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 631, + "type": "imguizmo", + "translation": [ + -0.06633484995394351, + -0.7990537153287331, + 0.06364169636007383 + ], + "rotation_deg": [ + -1.27049270061069, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 632, + "type": "imguizmo", + "translation": [ + -0.12801184459365234, + -0.7963093488016555, + 0.1225757419512755 + ], + "rotation_deg": [ + -2.5394025255231987, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 633, + "type": "imguizmo", + "translation": [ + -0.1895293524155388, + -0.7918253546999356, + 0.180899121361823 + ], + "rotation_deg": [ + -3.805148571105356, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 634, + "type": "imguizmo", + "translation": [ + -0.25081073026526046, + -0.7856073195218594, + 0.2383212704492481 + ], + "rotation_deg": [ + -5.066153875400071, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 635, + "type": "imguizmo", + "translation": [ + -0.31177962917701424, + -0.7776629901649649, + 0.29455611495521744 + ], + "rotation_deg": [ + -6.320847382823529, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 636, + "type": "imguizmo", + "translation": [ + -0.3723600894948483, + -0.7680022642743706, + 0.34932349571310417 + ], + "rotation_deg": [ + -7.567665901504945, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 637, + "type": "imguizmo", + "translation": [ + -0.4324766355089231, + -0.7566371779115448, + 0.4023505643868534 + ], + "rotation_deg": [ + -8.805056050828787, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 638, + "type": "imguizmo", + "translation": [ + -0.49205436948885567, + -0.7435818905588701, + 0.4533731427876755 + ], + "rotation_deg": [ + -10.03147619675371, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 639, + "type": "imguizmo", + "translation": [ + -0.5510190649969621, + -0.728852667478692, + 0.5021370389964912 + ], + "rotation_deg": [ + -11.245398372496584, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 640, + "type": "imguizmo", + "translation": [ + -0.6092972593651562, + -0.7124678594488271, + 0.5483993137352863 + ], + "rotation_deg": [ + -12.445310182188827, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 641, + "type": "imguizmo", + "translation": [ + -0.6668163452202867, + -0.6944478798997795, + 0.5919294906783755 + ], + "rotation_deg": [ + -13.629716685133323, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 642, + "type": "imguizmo", + "translation": [ + -0.7235046609438823, + -0.6748151794821482, + 0.6325107046738627 + ], + "rotation_deg": [ + -14.79714225831439, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 643, + "type": "imguizmo", + "translation": [ + -0.7792915799536041, + -0.6535942180959124, + 0.6699407821549189 + ], + "rotation_deg": [ + -15.946132434840282, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 644, + "type": "imguizmo", + "translation": [ + -0.834107598695175, + -0.6308114344164406, + 0.7040332483583246 + ], + "rotation_deg": [ + -17.075255716027876, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 645, + "type": "imguizmo", + "translation": [ + -0.8878844232351503, + -0.6064952129551954, + 0.7346182563323569 + ], + "rotation_deg": [ + -18.1831053548717, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 646, + "type": "imguizmo", + "translation": [ + -0.9405550543466555, + -0.5806758486961653, + 0.761543433105762 + ], + "rotation_deg": [ + -19.268301108675576, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 647, + "type": "imguizmo", + "translation": [ + -0.9920538709820816, + -0.5533855093520879, + 0.7846746388022323 + ], + "rotation_deg": [ + -20.32949095866313, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 648, + "type": "imguizmo", + "translation": [ + -1.042316712028737, + -0.5246581952874846, + 0.8038966349185228 + ], + "rotation_deg": [ + -21.365352794424837, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 649, + "type": "imguizmo", + "translation": [ + -1.0912809562456054, + -0.4945296971584412, + 0.8191136584368779 + ], + "rotation_deg": [ + -22.374596061102945, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 650, + "type": "imguizmo", + "translation": [ + -1.1388856002816141, + -0.4630375513219068, + 0.830249898911569 + ], + "rotation_deg": [ + -23.35596336726215, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 651, + "type": "imguizmo", + "translation": [ + -1.1850713346782116, + -0.43022099307006806, + 0.8372498761527206 + ], + "rotation_deg": [ + -24.308232051442683, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 652, + "type": "imguizmo", + "translation": [ + -1.2297806177615727, + -0.3961209077480571, + 0.8400787166258273 + ], + "rotation_deg": [ + -25.23021570544438, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 653, + "type": "imguizmo", + "translation": [ + -1.272957747332351, + -0.3607797798159086, + 0.8387223271899472 + ], + "rotation_deg": [ + -26.120765652443367, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 654, + "type": "imguizmo", + "translation": [ + -1.3145489300636881, + -0.324241639918209, + 0.8331874653090208 + ], + "rotation_deg": [ + -26.9787723781005, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 655, + "type": "imguizmo", + "translation": [ + -1.3545023485210004, + -0.28655201002739644, + 0.8235017053865272 + ], + "rotation_deg": [ + -27.80316691287801, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 656, + "type": "imguizmo", + "translation": [ + -1.392768225720046, + -0.24775784672905432, + 0.8097133013911917 + ], + "rotation_deg": [ + -28.592922163842402, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 657, + "type": "imguizmo", + "translation": [ + -1.4292988871428547, + -0.20790748271984888, + 0.7918909464581413 + ], + "rotation_deg": [ + -29.347054194294444, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 658, + "type": "imguizmo", + "translation": [ + -1.4640488201342357, + -0.16705056659101067, + 0.7701234306631608 + ], + "rotation_deg": [ + -30.064623449631696, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 659, + "type": "imguizmo", + "translation": [ + -1.4969747306048795, + -0.1252380009723725, + 0.7445191986750022 + ], + "rotation_deg": [ + -30.74473592791655, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 660, + "type": "imguizmo", + "translation": [ + -1.5280355969704076, + -0.08252187911402695, + 0.7152058094894969 + ], + "rotation_deg": [ + -31.386544293691394, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 661, + "type": "imguizmo", + "translation": [ + -1.5571927212591483, + -0.03895541998463006, + 0.6823293009370599 + ], + "rotation_deg": [ + -31.989248933652974, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 662, + "type": "imguizmo", + "translation": [ + -1.5844097773249985, + 0.005407098032810788, + 0.6460534621295605 + ], + "rotation_deg": [ + -32.55209895287106, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 663, + "type": "imguizmo", + "translation": [ + -1.609652856105276, + 0.050510404765042045, + 0.606559017471208 + ], + "rotation_deg": [ + -33.07439311031003, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 664, + "type": "imguizmo", + "translation": [ + -1.6328905078671887, + 0.09629830710831769, + 0.5640427262986589 + ], + "rotation_deg": [ + -33.55548069248783, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 665, + "type": "imguizmo", + "translation": [ + -1.6540937813902852, + 0.1427137590380087, + 0.5187164026359008 + ], + "rotation_deg": [ + -33.994762324184016, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 666, + "type": "imguizmo", + "translation": [ + -1.6732362600360717, + 0.18969893268084637, + 0.470805859947438 + ], + "rotation_deg": [ + -34.39169071518656, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 667, + "type": "imguizmo", + "translation": [ + -1.690294094659852, + 0.2371952903612502, + 0.42054978614697236 + ], + "rotation_deg": [ + -34.745771342147385, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 668, + "type": "imguizmo", + "translation": [ + -1.7052460333237944, + 0.28514365753198745, + 0.36819855446621796 + ], + "rotation_deg": [ + -35.05656306469681, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 669, + "type": "imguizmo", + "translation": [ + -1.7180734477741983, + 0.3334842964982822, + 0.3140129761080625 + ], + "rotation_deg": [ + -35.32367867504956, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 670, + "type": "imguizmo", + "translation": [ + -1.7287603566499787, + 0.38215698084355026, + 0.25826300089826915 + ], + "rotation_deg": [ + -35.546785380417425, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 671, + "type": "imguizmo", + "translation": [ + -1.7372934453934528, + 0.43110107046401264, + 0.20122637240902413 + ], + "rotation_deg": [ + -35.72560521762764, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 672, + "type": "imguizmo", + "translation": [ + -1.7436620828386202, + 0.4802555871187139, + 0.14318724425443127 + ], + "rotation_deg": [ + -35.859915399430406, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 673, + "type": "imguizmo", + "translation": [ + -1.7478583344562715, + 0.5295592904008172, + 0.08443476445149993 + ], + "rotation_deg": [ + -35.94954859206402, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 674, + "type": "imguizmo", + "translation": [ + -1.7498769722394254, + 0.5789507540355253, + 0.025261634899268894 + ], + "rotation_deg": [ + -35.994393123731946, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 675, + "type": "imguizmo", + "translation": [ + -1.7497154812167734, + 0.6283684424095682, + -0.0340373468473299 + ], + "rotation_deg": [ + -35.99439312373195, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 676, + "type": "imguizmo", + "translation": [ + -1.7473740625860192, + 0.6777507872369212, + -0.09316675624406756 + ], + "rotation_deg": [ + -35.94954859206402, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 677, + "type": "imguizmo", + "translation": [ + -1.7428556334632148, + 0.7270362642652123, + -0.15183201354762732 + ], + "rotation_deg": [ + -35.859915399430406, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 678, + "type": "imguizmo", + "translation": [ + -1.7361658232483943, + 0.7761634699272872, + -0.209740851397075 + ], + "rotation_deg": [ + -35.725605217627646, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 679, + "type": "imguizmo", + "translation": [ + -1.7273129666120437, + 0.825071197842405, + -0.26660477087514767 + ], + "rotation_deg": [ + -35.54678538041743, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 680, + "type": "imguizmo", + "translation": [ + -1.716308093111139, + 0.8736985150717673, + -0.32214047879534763 + ], + "rotation_deg": [ + -35.323678675049564, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 681, + "type": "imguizmo", + "translation": [ + -1.7031649134476916, + 0.9219848380333722, + -0.37607129905434733 + ], + "rotation_deg": [ + -35.05656306469682, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 682, + "type": "imguizmo", + "translation": [ + -1.6878998023869227, + 0.9698700079816133, + -0.42812855101841923 + ], + "rotation_deg": [ + -34.745771342147385, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 683, + "type": "imguizmo", + "translation": [ + -1.670531778356341, + 1.0172943659575848, + -0.478052888076836 + ], + "rotation_deg": [ + -34.39169071518656, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 684, + "type": "imguizmo", + "translation": [ + -1.6510824797511539, + 1.0641988271167202, + -0.5255955896936462 + ], + "rotation_deg": [ + -33.994762324184016, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 685, + "type": "imguizmo", + "translation": [ + -1.6295761379755191, + 1.1105249543411424, + -0.5705198005208636 + ], + "rotation_deg": [ + -33.55548069248783, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 686, + "type": "imguizmo", + "translation": [ + -1.6060395472532352, + 1.1562150310450428, + -0.6126017103999034 + ], + "rotation_deg": [ + -33.07439311031003, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 687, + "type": "imguizmo", + "translation": [ + -1.5805020312454723, + 1.2012121330823573, + -0.6516316693725328 + ], + "rotation_deg": [ + -32.55209895287106, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 688, + "type": "imguizmo", + "translation": [ + -1.5529954065171434, + 1.2454601996671646, + -0.6874152321464166 + ], + "rotation_deg": [ + -31.989248933652974, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 689, + "type": "imguizmo", + "translation": [ + -1.523553942897427, + 1.2889041032184445, + -0.7197741268117833 + ], + "rotation_deg": [ + -31.386544293691394, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 690, + "type": "imguizmo", + "translation": [ + -1.492214320783825, + 1.331489718042179, + -0.748547142983112 + ], + "rotation_deg": [ + -30.74473592791655, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 691, + "type": "imguizmo", + "translation": [ + -1.4590155854429585, + 1.373163987765225, + -0.7735909349411628 + ], + "rotation_deg": [ + -30.064623449631696, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 692, + "type": "imguizmo", + "translation": [ + -1.4239990983650224, + 1.4138749914369562, + -0.7947807357741433 + ], + "rotation_deg": [ + -29.34705419429445, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 693, + "type": "imguizmo", + "translation": [ + -1.387208485732525, + 1.4535720082162906, + -0.8120109789601822 + ], + "rotation_deg": [ + -28.59292216384241, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 694, + "type": "imguizmo", + "translation": [ + -1.3486895840674944, + 1.492205580563551, + -0.825195824294434 + ], + "rotation_deg": [ + -27.803166912878016, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 695, + "type": "imguizmo", + "translation": [ + -1.3084903831248782, + 1.5297275758583981, + -0.8342695855406617 + ], + "rotation_deg": [ + -26.97877237810052, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 696, + "type": "imguizmo", + "translation": [ + -1.2666609661032882, + 1.5660912463670817, + -0.8391870576767603 + ], + "rotation_deg": [ + -26.120765652443385, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 697, + "type": "imguizmo", + "translation": [ + -1.2232534472475682, + 1.6012512874842966, + -0.8399237421039043 + ], + "rotation_deg": [ + -25.230215705444397, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 698, + "type": "imguizmo", + "translation": [ + -1.1783219069209374, + 1.6351638941770774, + -0.8364759686973338 + ], + "rotation_deg": [ + -24.308232051442705, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 699, + "type": "imguizmo", + "translation": [ + -1.1319223242275882, + 1.6677868155604172, + -0.8288609140907325 + ], + "rotation_deg": [ + -23.35596336726214, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 700, + "type": "imguizmo", + "translation": [ + -1.0841125072696978, + 1.6990794075366078, + -0.817116516103101 + ], + "rotation_deg": [ + -22.37459606110294, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 701, + "type": "imguizmo", + "translation": [ + -1.0349520211257281, + 1.7290026834327241, + -0.8013012847344533 + ], + "rotation_deg": [ + -21.36535279442483, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 702, + "type": "imguizmo", + "translation": [ + -0.9845021136397561, + 1.7575193625731642, + -0.7814940106719368 + ], + "rotation_deg": [ + -20.329490958663122, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 703, + "type": "imguizmo", + "translation": [ + -0.9328256391142875, + 1.7845939167267286, + -0.7577933727585957 + ], + "rotation_deg": [ + -19.26830110867557, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 704, + "type": "imguizmo", + "translation": [ + -0.8799869800016217, + 1.8101926143703733, + -0.7303174463803351 + ], + "rotation_deg": [ + -18.183105354871696, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 705, + "type": "imguizmo", + "translation": [ + -0.826051966691334, + 1.8342835627144847, + -0.6992031152202912 + ], + "rotation_deg": [ + -17.07525571602787, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 706, + "type": "imguizmo", + "translation": [ + -0.7710877954938082, + 1.8568367474373264, + -0.6646053893112046 + ], + "rotation_deg": [ + -15.94613243484027, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 707, + "type": "imguizmo", + "translation": [ + -0.7151629449220016, + 1.877824070079145, + -0.6266966327832368 + ], + "rotation_deg": [ + -14.797142258314395, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 708, + "type": "imguizmo", + "translation": [ + -0.6583470903757402, + 1.8972193830493547, + -0.5856657051545401 + ], + "rotation_deg": [ + -13.629716685133328, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 709, + "type": "imguizmo", + "translation": [ + -0.6007110173348551, + 1.9149985222031747, + -0.5417170204426431 + ], + "rotation_deg": [ + -12.44531018218883, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 710, + "type": "imguizmo", + "translation": [ + -0.5423265331692831, + 1.9311393369471483, + -0.4950695287840945 + ], + "rotation_deg": [ + -11.245398372496588, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 711, + "type": "imguizmo", + "translation": [ + -0.4832663776760281, + 1.9456217178360224, + -0.44595562563589713 + ], + "rotation_deg": [ + -10.031476196753715, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 712, + "type": "imguizmo", + "translation": [ + -0.42360413245442996, + 1.9584276216266132, + -0.39461999399303094 + ], + "rotation_deg": [ + -8.80505605082879, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 713, + "type": "imguizmo", + "translation": [ + -0.36341412923265626, + 1.9695410937574391, + -0.34131838539007997 + ], + "rotation_deg": [ + -7.5676659015049506, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 714, + "type": "imguizmo", + "translation": [ + -0.3027713572596238, + 1.978948288226119, + -0.2863163457599518 + ], + "rotation_deg": [ + -6.320847382823533, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 715, + "type": "imguizmo", + "translation": [ + -0.24175136987773482, + 1.9866374848397654, + -0.2298878924973987 + ], + "rotation_deg": [ + -5.066153875400075, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 716, + "type": "imguizmo", + "translation": [ + -0.18043019039281344, + 1.9925991038168873, + -0.1723141493181341 + ], + "rotation_deg": [ + -3.8051485711053608, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 717, + "type": "imguizmo", + "translation": [ + -0.11888421735854393, + 1.9968257177226036, + -0.11388194571463164 + ], + "rotation_deg": [ + -2.5394025255232044, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 718, + "type": "imguizmo", + "translation": [ + -0.05719012939337879, + 1.9993120607223025, + -0.05488238798601471 + ], + "rotation_deg": [ + -1.2704927006106954, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 719, + "type": "imguizmo", + "translation": [ + 0.004575210351469343, + 2.0000550351422155, + 0.004390591038880599 + ], + "rotation_deg": [ + -3.3306690738754696E-15, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 720, + "type": "action", + "action": "set_active_render_window", + "value": 0 + }, + { + "frame": 720, + "type": "action", + "action": "set_active_planar", + "value": 4 + }, + { + "frame": 720, + "type": "action", + "action": "set_projection_type", + "value": "perspective" + }, + { + "frame": 721, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.0 + ], + "rotation_deg": [ + 0.0, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 722, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.040020520069236704 + ], + "rotation_deg": [ + 1.270492700610689, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 723, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.07678953794844168 + ], + "rotation_deg": [ + 2.5394025255231947, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 724, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.1137190169539431 + ], + "rotation_deg": [ + 3.8051485711053474, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 725, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.15048632571878637 + ], + "rotation_deg": [ + 5.066153875400059, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 726, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.1870677865014388 + ], + "rotation_deg": [ + 6.320847382823546, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 727, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.22341605297729109 + ], + "rotation_deg": [ + 7.5676659015049585, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 728, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.25948598136292383 + ], + "rotation_deg": [ + 8.805056050828796, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 729, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.2952326216887082 + ], + "rotation_deg": [ + 10.031476196753717, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 730, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.33061143899854584 + ], + "rotation_deg": [ + 11.245398372496588, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 731, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.3655783556190644 + ], + "rotation_deg": [ + 12.445310182188825, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 732, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.40008980713217435 + ], + "rotation_deg": [ + 13.62971668513332, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 733, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.43410279656632905 + ], + "rotation_deg": [ + 14.797142258314384, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 734, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.46757494797216265 + ], + "rotation_deg": [ + 15.946132434840287, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 735, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.5004645592171051 + ], + "rotation_deg": [ + 17.07525571602788, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 736, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.5327306539410901 + ], + "rotation_deg": [ + 18.1831053548717, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 737, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.5643330326079934 + ], + "rotation_deg": [ + 19.268301108675573, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 738, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.5952323225892489 + ], + "rotation_deg": [ + 20.329490958663122, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 739, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.625390027217242 + ], + "rotation_deg": [ + 21.365352794424826, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 740, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.6547685737473631 + ], + "rotation_deg": [ + 22.374596061102935, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 741, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.6833313601689682 + ], + "rotation_deg": [ + 23.355963367262138, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 742, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.7110428008069274 + ], + "rotation_deg": [ + 24.308232051442694, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 743, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.7378683706569439 + ], + "rotation_deg": [ + 25.230215705444387, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 744, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.7637746483994108 + ], + "rotation_deg": [ + 26.120765652443374, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 745, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.788729358038213 + ], + "rotation_deg": [ + 26.978772378100505, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 746, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.8127014091125998 + ], + "rotation_deg": [ + 27.803166912878, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 747, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.8356609354320272 + ], + "rotation_deg": [ + 28.592922163842395, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 748, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.8575793322857121 + ], + "rotation_deg": [ + 29.347054194294437, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 749, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.8784292920805408 + ], + "rotation_deg": [ + 30.06462344963168, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 750, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.898184838362928 + ], + "rotation_deg": [ + 30.744735927916555, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 751, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.9168213581822445 + ], + "rotation_deg": [ + 31.386544293691394, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 752, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.9343156327554888 + ], + "rotation_deg": [ + 31.989248933652974, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 753, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.950645866394999 + ], + "rotation_deg": [ + 32.55209895287106, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 754, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.9657917136631656 + ], + "rotation_deg": [ + 33.07439311031002, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 755, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.9797343047203132 + ], + "rotation_deg": [ + 33.555480692487826, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 756, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.992456268834171 + ], + "rotation_deg": [ + 33.99476232418401, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 757, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 1.0039417560216426 + ], + "rotation_deg": [ + 34.391690715186556, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 758, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 1.014176456795911 + ], + "rotation_deg": [ + 34.745771342147385, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 759, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 1.0231476199942764 + ], + "rotation_deg": [ + 35.05656306469681, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 760, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 1.0308440686645188 + ], + "rotation_deg": [ + 35.323678675049564, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 761, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 1.037256213989987 + ], + "rotation_deg": [ + 35.54678538041743, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 762, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 1.0423760672360716 + ], + "rotation_deg": [ + 35.725605217627646, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 763, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 1.0461972497031717 + ], + "rotation_deg": [ + 35.859915399430406, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 764, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 1.0487150006737627 + ], + "rotation_deg": [ + 35.949548592064026, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 765, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 1.049926183343655 + ], + "rotation_deg": [ + 35.99439312373195, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 766, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 1.0498292887300635 + ], + "rotation_deg": [ + 35.99439312373195, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 767, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 1.0484244375516112 + ], + "rotation_deg": [ + 35.949548592064026, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 768, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 1.0457133800779288 + ], + "rotation_deg": [ + 35.85991539943041, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 769, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 1.041699493949036 + ], + "rotation_deg": [ + 35.725605217627646, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 770, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 1.0363877799672256 + ], + "rotation_deg": [ + 35.54678538041743, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 771, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 1.029784855866683 + ], + "rotation_deg": [ + 35.323678675049564, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 772, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 1.021898948068615 + ], + "rotation_deg": [ + 35.05656306469682, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 773, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 1.0127398814321533 + ], + "rotation_deg": [ + 34.745771342147385, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 774, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 1.002319067013804 + ], + "rotation_deg": [ + 34.391690715186556, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 775, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.9906494878506916 + ], + "rotation_deg": [ + 33.99476232418401, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 776, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.9777456827853109 + ], + "rotation_deg": [ + 33.555480692487826, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 777, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.9636237283519408 + ], + "rotation_deg": [ + 33.07439311031002, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 778, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.9483012187472828 + ], + "rotation_deg": [ + 32.552098952871056, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 779, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.9317972439102856 + ], + "rotation_deg": [ + 31.989248933652966, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 780, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.914132365738456 + ], + "rotation_deg": [ + 31.386544293691394, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 781, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.8953285924702947 + ], + "rotation_deg": [ + 30.74473592791655, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 782, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.8754093512657741 + ], + "rotation_deg": [ + 30.064623449631675, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 783, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.8543994590190124 + ], + "rotation_deg": [ + 29.34705419429443, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 784, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.8323250914395143 + ], + "rotation_deg": [ + 28.5929221638424, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 785, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.8092137504404958 + ], + "rotation_deg": [ + 27.803166912878005, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 786, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.7850942298749263 + ], + "rotation_deg": [ + 26.97877237810051, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 787, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.7599965796619721 + ], + "rotation_deg": [ + 26.120765652443374, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 788, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.7339520683485403 + ], + "rotation_deg": [ + 25.230215705444394, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 789, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.7069931441525616 + ], + "rotation_deg": [ + 24.3082320514427, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 790, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.6791533945365522 + ], + "rotation_deg": [ + 23.35596336726214, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 791, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.6504675043618181 + ], + "rotation_deg": [ + 22.374596061102938, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 792, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.6209712126754365 + ], + "rotation_deg": [ + 21.365352794424837, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 793, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.5907012681838535 + ], + "rotation_deg": [ + 20.329490958663133, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 794, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.5596953834685723 + ], + "rotation_deg": [ + 19.268301108675576, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 795, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.5279921880009728 + ], + "rotation_deg": [ + 18.183105354871703, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 796, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.49563118001480067 + ], + "rotation_deg": [ + 17.075255716027893, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 797, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.46265267729628523 + ], + "rotation_deg": [ + 15.946132434840298, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 798, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.4290977669532004 + ], + "rotation_deg": [ + 14.797142258314391, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 799, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.3950082542254436 + ], + "rotation_deg": [ + 13.629716685133324, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 800, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.36042661040091245 + ], + "rotation_deg": [ + 12.445310182188827, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 801, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.3253959199015694 + ], + "rotation_deg": [ + 11.245398372496588, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 802, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.28995982660561637 + ], + "rotation_deg": [ + 10.031476196753715, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 803, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.25416247947265747 + ], + "rotation_deg": [ + 8.805056050828789, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 804, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.21804847753959367 + ], + "rotation_deg": [ + 7.567665901504963, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 805, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.18166281435577417 + ], + "rotation_deg": [ + 6.320847382823548, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 806, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.14505082192663976 + ], + "rotation_deg": [ + 5.066153875400057, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 807, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.10825811423568706 + ], + "rotation_deg": [ + 3.805148571105343, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 808, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.07133053041512583 + ], + "rotation_deg": [ + 2.5394025255232027, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 809, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.03431407763602671 + ], + "rotation_deg": [ + 1.2704927006106939, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 810, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.0027451262108821595 + ], + "rotation_deg": [ + 1.9984014443252818E-15, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 811, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.03980090997236625 + ], + "rotation_deg": [ + -1.27049270061069, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 812, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.07680710675619157 + ], + "rotation_deg": [ + -2.5394025255231987, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 813, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.11371761144932346 + ], + "rotation_deg": [ + -3.805148571105356, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 814, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.1504864381591564 + ], + "rotation_deg": [ + -5.066153875400071, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 815, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.1870677775062087 + ], + "rotation_deg": [ + -6.320847382823529, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 816, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.22341605369690914 + ], + "rotation_deg": [ + -7.567665901504945, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 817, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.2594859813053541 + ], + "rotation_deg": [ + -8.805056050828787, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 818, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.29523262169331355 + ], + "rotation_deg": [ + -10.03147619675371, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 819, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.33061143899817735 + ], + "rotation_deg": [ + -11.245398372496584, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 820, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.3655783556190938 + ], + "rotation_deg": [ + -12.445310182188827, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 821, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.4000898071321721 + ], + "rotation_deg": [ + -13.629716685133323, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 822, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.43410279656632944 + ], + "rotation_deg": [ + -14.79714225831439, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 823, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.4675749479721625 + ], + "rotation_deg": [ + -15.946132434840282, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 824, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.5004645592171051 + ], + "rotation_deg": [ + -17.075255716027876, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 825, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.5327306539410901 + ], + "rotation_deg": [ + -18.1831053548717, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 826, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.5643330326079934 + ], + "rotation_deg": [ + -19.268301108675576, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 827, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.5952323225892491 + ], + "rotation_deg": [ + -20.32949095866313, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 828, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.6253900272172424 + ], + "rotation_deg": [ + -21.365352794424837, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 829, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.6547685737473635 + ], + "rotation_deg": [ + -22.374596061102945, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 830, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.6833313601689687 + ], + "rotation_deg": [ + -23.35596336726215, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 831, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.7110428008069271 + ], + "rotation_deg": [ + -24.308232051442683, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 832, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.7378683706569437 + ], + "rotation_deg": [ + -25.23021570544438, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 833, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.7637746483994106 + ], + "rotation_deg": [ + -26.120765652443367, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 834, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.788729358038213 + ], + "rotation_deg": [ + -26.9787723781005, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 835, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.8127014091126001 + ], + "rotation_deg": [ + -27.80316691287801, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 836, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.8356609354320277 + ], + "rotation_deg": [ + -28.592922163842402, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 837, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.8575793322857128 + ], + "rotation_deg": [ + -29.347054194294444, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 838, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.8784292920805414 + ], + "rotation_deg": [ + -30.064623449631696, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 839, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.898184838362928 + ], + "rotation_deg": [ + -30.74473592791655, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 840, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.9168213581822446 + ], + "rotation_deg": [ + -31.386544293691394, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 841, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.9343156327554889 + ], + "rotation_deg": [ + -31.989248933652974, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 842, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.9506458663949992 + ], + "rotation_deg": [ + -32.55209895287106, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 843, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.965791713663166 + ], + "rotation_deg": [ + -33.07439311031003, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 844, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.9797343047203133 + ], + "rotation_deg": [ + -33.55548069248783, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 845, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.9924562688341714 + ], + "rotation_deg": [ + -33.994762324184016, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 846, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -1.003941756021643 + ], + "rotation_deg": [ + -34.39169071518656, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 847, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -1.0141764567959113 + ], + "rotation_deg": [ + -34.745771342147385, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 848, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -1.0231476199942768 + ], + "rotation_deg": [ + -35.05656306469681, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 849, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -1.030844068664519 + ], + "rotation_deg": [ + -35.32367867504956, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 850, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -1.0372562139899875 + ], + "rotation_deg": [ + -35.546785380417425, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 851, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -1.042376067236072 + ], + "rotation_deg": [ + -35.72560521762764, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 852, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -1.0461972497031724 + ], + "rotation_deg": [ + -35.859915399430406, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 853, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -1.048715000673763 + ], + "rotation_deg": [ + -35.94954859206402, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 854, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -1.0499261833436555 + ], + "rotation_deg": [ + -35.994393123731946, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 855, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -1.049829288730064 + ], + "rotation_deg": [ + -35.99439312373195, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 856, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -1.0484244375516116 + ], + "rotation_deg": [ + -35.94954859206402, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 857, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -1.0457133800779292 + ], + "rotation_deg": [ + -35.859915399430406, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 858, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -1.0416994939490365 + ], + "rotation_deg": [ + -35.725605217627646, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 859, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -1.036387779967226 + ], + "rotation_deg": [ + -35.54678538041743, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 860, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -1.0297848558666833 + ], + "rotation_deg": [ + -35.323678675049564, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 861, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -1.0218989480686154 + ], + "rotation_deg": [ + -35.05656306469682, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 862, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -1.0127398814321538 + ], + "rotation_deg": [ + -34.745771342147385, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 863, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -1.0023190670138047 + ], + "rotation_deg": [ + -34.39169071518656, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 864, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.9906494878506922 + ], + "rotation_deg": [ + -33.994762324184016, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 865, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.9777456827853115 + ], + "rotation_deg": [ + -33.55548069248783, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 866, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.9636237283519411 + ], + "rotation_deg": [ + -33.07439311031003, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 867, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.9483012187472833 + ], + "rotation_deg": [ + -32.55209895287106, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 868, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.9317972439102862 + ], + "rotation_deg": [ + -31.989248933652974, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 869, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.9141323657384564 + ], + "rotation_deg": [ + -31.386544293691394, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 870, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.8953285924702951 + ], + "rotation_deg": [ + -30.74473592791655, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 871, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.8754093512657752 + ], + "rotation_deg": [ + -30.064623449631696, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 872, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.8543994590190134 + ], + "rotation_deg": [ + -29.34705419429445, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 873, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.8323250914395149 + ], + "rotation_deg": [ + -28.59292216384241, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 874, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.8092137504404965 + ], + "rotation_deg": [ + -27.803166912878016, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 875, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.7850942298749269 + ], + "rotation_deg": [ + -26.97877237810052, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 876, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.7599965796619728 + ], + "rotation_deg": [ + -26.120765652443385, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 877, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.7339520683485409 + ], + "rotation_deg": [ + -25.230215705444397, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 878, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.7069931441525621 + ], + "rotation_deg": [ + -24.308232051442705, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 879, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.6791533945365528 + ], + "rotation_deg": [ + -23.35596336726214, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 880, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.6504675043618187 + ], + "rotation_deg": [ + -22.37459606110294, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 881, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.6209712126754369 + ], + "rotation_deg": [ + -21.36535279442483, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 882, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.5907012681838537 + ], + "rotation_deg": [ + -20.329490958663122, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 883, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.5596953834685725 + ], + "rotation_deg": [ + -19.26830110867557, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 884, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.527992188000973 + ], + "rotation_deg": [ + -18.183105354871696, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 885, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.4956311800148004 + ], + "rotation_deg": [ + -17.07525571602787, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 886, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.46265267729628495 + ], + "rotation_deg": [ + -15.94613243484027, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 887, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.429097766953201 + ], + "rotation_deg": [ + -14.797142258314395, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 888, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.3950082542254441 + ], + "rotation_deg": [ + -13.629716685133328, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 889, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.36042661040091306 + ], + "rotation_deg": [ + -12.44531018218883, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 890, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.32539591990156996 + ], + "rotation_deg": [ + -11.245398372496588, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 891, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.2899598266056169 + ], + "rotation_deg": [ + -10.031476196753715, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 892, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.254162479472658 + ], + "rotation_deg": [ + -8.80505605082879, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 893, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.21804847753959372 + ], + "rotation_deg": [ + -7.5676659015049506, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 894, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.18166281435577425 + ], + "rotation_deg": [ + -6.320847382823533, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 895, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.1450508219266409 + ], + "rotation_deg": [ + -5.066153875400075, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 896, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.10825811423568805 + ], + "rotation_deg": [ + -3.8051485711053608, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 897, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.07133053041512637 + ], + "rotation_deg": [ + -2.5394025255232044, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 898, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + -0.03431407763602727 + ], + "rotation_deg": [ + -1.2704927006106954, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 899, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0, + 0.0027451262108815974 + ], + "rotation_deg": [ + -3.3306690738754696E-15, + 150.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 900, + "type": "action", + "action": "set_active_render_window", + "value": 0 + }, + { + "frame": 900, + "type": "action", + "action": "set_active_planar", + "value": 5 + }, + { + "frame": 900, + "type": "action", + "action": "set_projection_type", + "value": "perspective" + }, + { + "frame": 901, + "type": "imguizmo", + "translation": [ + 3.5, + 0.0, + 0.0 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 902, + "type": "imguizmo", + "translation": [ + 3.498351706745195, + 0.09338121349488564, + 0.09598941546477674 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 903, + "type": "imguizmo", + "translation": [ + 3.493540744009305, + 0.17917558854636395, + 0.1838214632529401 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 904, + "type": "imguizmo", + "translation": [ + 3.485694420036376, + 0.2653443728925339, + 0.27135205401665224 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 905, + "type": "imguizmo", + "translation": [ + 3.474812805218336, + 0.3511347600105016, + 0.3574816359159584 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 906, + "type": "imguizmo", + "translation": [ + 3.460910233104283, + 0.4364915018366905, + 0.441834194013461 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 907, + "type": "imguizmo", + "translation": [ + 3.444003962454902, + 0.5213041236136793, + 0.5239852418432069 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 908, + "type": "imguizmo", + "translation": [ + 3.424115061347224, + 0.6054672898468222, + 0.6035258467183974 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 909, + "type": "imguizmo", + "translation": [ + 3.4012683084778628, + 0.6888761172736524, + 0.6800597141704652 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 910, + "type": "imguizmo", + "translation": [ + 3.375492168087725, + 0.771426690996607, + 0.7532055584956212 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 911, + "type": "imguizmo", + "translation": [ + 3.3468187540354477, + 0.8530161631111502, + 0.8225989706028591 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 912, + "type": "imguizmo", + "translation": [ + 3.3152837898246155, + 0.933542883308407, + 0.8878942360175692 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 913, + "type": "imguizmo", + "translation": [ + 3.2809265640937606, + 1.0129065253214347, + 0.9487660570107936 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 914, + "type": "imguizmo", + "translation": [ + 3.243789881667847, + 1.0910082119350464, + 1.004911173232379 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 915, + "type": "imguizmo", + "translation": [ + 3.203920010228772, + 1.1677506381732456, + 1.056049872537487 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 916, + "type": "imguizmo", + "translation": [ + 3.1613666226715935, + 1.2430381925292104, + 1.1019273844985356 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 917, + "type": "imguizmo", + "translation": [ + 3.116182735218291, + 1.316777076085318, + 1.142315149658643 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 918, + "type": "imguizmo", + "translation": [ + 3.068424641366156, + 1.388875419374914, + 1.1770119582033483 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 919, + "type": "imguizmo", + "translation": [ + 3.0181518417531, + 1.4592433968402316, + 1.2058449523777839 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 920, + "type": "imguizmo", + "translation": [ + 2.9654269700272744, + 1.527793338743847, + 1.2286704876553167 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 921, + "type": "imguizmo", + "translation": [ + 2.9103157148133394, + 1.594439840394259, + 1.2453748483673535 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 922, + "type": "imguizmo", + "translation": [ + 2.85288673787262, + 1.6590998685494973, + 1.255874814229081 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 923, + "type": "imguizmo", + "translation": [ + 2.793211588559101, + 1.7216928648662022, + 1.260118074938741 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 924, + "type": "imguizmo", + "translation": [ + 2.731364614677841, + 1.7821408462652917, + 1.2580834907849205 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 925, + "type": "imguizmo", + "translation": [ + 2.6674228698568667, + 1.8403685020891636, + 1.2497811979635312 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 926, + "type": "imguizmo", + "translation": [ + 2.601466017547946, + 1.8963032879294, + 1.2352525580797908 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 927, + "type": "imguizmo", + "translation": [ + 2.5335762317758475, + 1.9498755160080639, + 1.2145699520867876 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 928, + "type": "imguizmo", + "translation": [ + 2.4638380947597374, + 2.0010184419999955, + 1.1878364196872122 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 929, + "type": "imguizmo", + "translation": [ + 2.3923384915342716, + 2.0496683481879283, + 1.1551851459947415 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 930, + "type": "imguizmo", + "translation": [ + 2.3191665017016527, + 2.095764622846832, + 1.1167787980125026 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 931, + "type": "imguizmo", + "translation": [ + 2.244413288449548, + 2.1392498357585708, + 1.0728087142342453 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 932, + "type": "imguizmo", + "translation": [ + 2.168171984973103, + 2.180069809762807, + 1.0234939514055896 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 933, + "type": "imguizmo", + "translation": [ + 2.0905375784425826, + 2.2181736882549976, + 0.9690801931943411 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 934, + "type": "imguizmo", + "translation": [ + 2.011606791661178, + 2.2535139985473864, + 0.9098385262068122 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 935, + "type": "imguizmo", + "translation": [ + 1.9314779625604461, + 2.286046711014064, + 0.8460640894479889 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 936, + "type": "imguizmo", + "translation": [ + 1.8502509216834866, + 2.315731293946399, + 0.7780746039538516 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 937, + "type": "imguizmo", + "translation": [ + 1.7680268678085211, + 2.3425307640504998, + 0.7062087899211581 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 938, + "type": "imguizmo", + "translation": [ + 1.6849082418678123, + 2.3664117325237926, + 0.6308246792204576 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 939, + "type": "imguizmo", + "translation": [ + 1.6009985993190228, + 2.3873444466533114, + 0.5522978316993266 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 940, + "type": "imguizmo", + "translation": [ + 1.516402481128007, + 2.405302826883877, + 0.4710194641620934 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 941, + "type": "imguizmo", + "translation": [ + 1.4312252835237878, + 2.42026449930997, + 0.3873945013474034 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 942, + "type": "imguizmo", + "translation": [ + 1.3455731266879787, + 2.4322108235508333, + 0.30183955861353584 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 943, + "type": "imguizmo", + "translation": [ + 1.259552722542252, + 2.441126915974067, + 0.21478086638164712 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 944, + "type": "imguizmo", + "translation": [ + 1.1732712417985711, + 2.447001668238779, + 0.12665214667725008 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 945, + "type": "imguizmo", + "translation": [ + 1.0868361804378328, + 2.449827761135195, + 0.03789245234890409 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 946, + "type": "imguizmo", + "translation": [ + 1.000355225783255, + 2.4496016737034814, + -0.05105602027099654 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 947, + "type": "imguizmo", + "translation": [ + 0.9139361223353878, + 2.446323687620426, + -0.13975013436610284 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 948, + "type": "imguizmo", + "translation": [ + 0.8276865375358794, + 2.4399978868485, + -0.22774802032144126 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 949, + "type": "imguizmo", + "translation": [ + 0.7417139276272481, + 2.4306321525477506, + -0.314611277095613 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 950, + "type": "imguizmo", + "translation": [ + 0.6561254037757919, + 2.4182381532568598, + -0.399907156312722 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 951, + "type": "imguizmo", + "translation": [ + 0.5710275986244079, + 2.4028313303555935, + -0.4832107181930219 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 952, + "type": "imguizmo", + "translation": [ + 0.48652653344160013, + 2.3844308788267674, + -0.564106948581521 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 953, + "type": "imguizmo", + "translation": [ + 0.402727486032178, + 2.3630597233416903, + -0.6421928265276287 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 954, + "type": "imguizmo", + "translation": [ + 0.3197348595742256, + 2.338744489698876, + -0.7170793321152561 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 955, + "type": "imguizmo", + "translation": [ + 0.2376520525457393, + 2.3115154716516138, + -0.7883933845404711 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 956, + "type": "imguizmo", + "translation": [ + 0.1565813299030015, + 2.2814065931657255, + -0.855779700781296 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 957, + "type": "imguizmo", + "translation": [ + 0.0766236956711756, + 2.248455366154528, + -0.9189025655998558 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 958, + "type": "imguizmo", + "translation": [ + -0.0021212328941254133, + 2.2127028437436596, + -0.9774475040588004 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 959, + "type": "imguizmo", + "translation": [ + -0.07955534941753811, + 2.1741935691239997, + -1.0311228482196264 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 960, + "type": "imguizmo", + "translation": [ + -0.1555821806322771, + 2.132975520056397, + -1.0796611902176756 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 961, + "type": "imguizmo", + "translation": [ + -0.23010700657381208, + 2.089100049097354, + -1.1228207144746687 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 962, + "type": "imguizmo", + "translation": [ + -0.30303697858914547, + 2.0426218196201393, + -1.1603864024117458 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 963, + "type": "imguizmo", + "translation": [ + -0.3742812350146743, + 1.9935987377110287, + -1.1921711036612164 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 964, + "type": "imguizmo", + "translation": [ + -0.44375101437850833, + 1.9420918800255336, + -1.2180164684402743 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 965, + "type": "imguizmo", + "translation": [ + -0.5113597659862144, + 1.88816541769449, + -1.237793736441652 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 966, + "type": "imguizmo", + "translation": [ + -0.5770232577521969, + 1.8318865363748278, + -1.2514043783109934 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 967, + "type": "imguizmo", + "translation": [ + -0.6406596811423929, + 1.7733253525446013, + -1.2587805865151411 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 968, + "type": "imguizmo", + "translation": [ + -0.7021897530975179, + 1.7125548261465942, + -1.2598856131558573 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 969, + "type": "imguizmo", + "translation": [ + -0.7615368148098847, + 1.6496506696893103, + -1.2547139530460016 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 970, + "type": "imguizmo", + "translation": [ + -0.8186269272307299, + 1.5846912539186218, + -1.2432913711360996 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 971, + "type": "imguizmo", + "translation": [ + -0.8733889631890633, + 1.5177575101775755, + -1.2256747741546528 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 972, + "type": "imguizmo", + "translation": [ + -0.9257546960072662, + 1.4489328295760187, + -1.201951927101681 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 973, + "type": "imguizmo", + "translation": [ + -0.9756588845030363, + 1.3783029590956584, + -1.1722410160079066 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 974, + "type": "imguizmo", + "translation": [ + -1.0230393542717746, + 1.3059558947600016, + -1.1366900591378948 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 975, + "type": "imguizmo", + "translation": [ + -1.0678370751481527, + 1.2319817720022697, + -1.0954761695705044 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 976, + "type": "imguizmo", + "translation": [ + -1.109996234750347, + 1.1564727533678683, + -1.048804672830439 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 977, + "type": "imguizmo", + "translation": [ + -1.1494643080153202, + 1.079522913691332, + -0.9969080839668093 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 978, + "type": "imguizmo", + "translation": [ + -1.186192122638504, + 1.0012281228908007, + -0.9400449491748561 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 979, + "type": "imguizmo", + "translation": [ + -1.2201339203363704, + 0.9216859265260348, + -0.878498557731811 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 980, + "type": "imguizmo", + "translation": [ + -1.2512474138555556, + 0.8409954242687956, + -0.8125755306639656 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 981, + "type": "imguizmo", + "translation": [ + -1.2794938396575095, + 0.759257146436995, + -0.7426042931761426 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 982, + "type": "imguizmo", + "translation": [ + -1.3048380062130394, + 0.6765729287464379, + -0.6689334384538466 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 983, + "type": "imguizmo", + "translation": [ + -1.3272483378465731, + 0.5930457854362006, + -0.5919299909895472 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 984, + "type": "imguizmo", + "translation": [ + -1.3466969140755187, + 0.5087797809257184, + -0.5119775780851219 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 985, + "type": "imguizmo", + "translation": [ + -1.3631595043957085, + 0.4238799001634729, + -0.4294745186399297 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 986, + "type": "imguizmo", + "translation": [ + -1.3766155984695898, + 0.33845191782882594, + -0.3448318387460978 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 987, + "type": "imguizmo", + "translation": [ + -1.3870484316795537, + 0.2526022665499362, + -0.258471223977201 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 988, + "type": "imguizmo", + "translation": [ + -1.3944450060145572, + 0.1664379043019601, + -0.17082291857194848 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 989, + "type": "imguizmo", + "translation": [ + -1.3987961062640304, + 0.08006618115072876, + -0.08232358197902295 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 990, + "type": "imguizmo", + "translation": [ + -1.4000963114988783, + -0.00640529449205858, + 0.0065858865583200005 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 991, + "type": "imguizmo", + "translation": [ + -1.3983440018252848, + -0.09286878993552146, + 0.09546254454010955 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 992, + "type": "imguizmo", + "translation": [ + -1.3935413604028986, + -0.17921658243111382, + 0.1838636129269121 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 993, + "type": "imguizmo", + "translation": [ + -1.385694370724889, + -0.26534109338175493, + 0.2713486820427333 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 994, + "type": "imguizmo", + "translation": [ + -1.3748128091632554, + -0.3511350223713651, + 0.35748190567387095 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 995, + "type": "imguizmo", + "translation": [ + -1.36091023278869, + -0.4364914808478205, + 0.441834172432825 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 996, + "type": "imguizmo", + "translation": [ + -1.3440039624801499, + -0.5213041252927882, + 0.5239852435696551 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 997, + "type": "imguizmo", + "translation": [ + -1.3241150613452046, + -0.6054672897124932, + 0.6035258465802791 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 998, + "type": "imguizmo", + "translation": [ + -1.301268308478024, + -0.6888761172843985, + 0.6800597141815122 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 999, + "type": "imguizmo", + "translation": [ + -1.2754921680877125, + -0.7714266909957476, + 0.7532055584947357 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1000, + "type": "imguizmo", + "translation": [ + -1.2468187540354487, + -0.8530161631112192, + 0.8225989706029284 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1001, + "type": "imguizmo", + "translation": [ + -1.2152837898246154, + -0.9335428833084021, + 0.8878942360175622 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1002, + "type": "imguizmo", + "translation": [ + -1.1809265640937605, + -1.0129065253214358, + 0.9487660570107929 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1003, + "type": "imguizmo", + "translation": [ + -1.143789881667848, + -1.0910082119350462, + 1.0049111732323772 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1004, + "type": "imguizmo", + "translation": [ + -1.1039200102287723, + -1.1677506381732456, + 1.0560498725374856 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1005, + "type": "imguizmo", + "translation": [ + -1.0613666226715934, + -1.2430381925292107, + 1.1019273844985342 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1006, + "type": "imguizmo", + "translation": [ + -1.0161827352182906, + -1.3167770760853184, + 1.1423151496586417 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1007, + "type": "imguizmo", + "translation": [ + -0.9684246413661551, + -1.3888754193749149, + 1.1770119582033471 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1008, + "type": "imguizmo", + "translation": [ + -0.9181518417530996, + -1.4592433968402323, + 1.2058449523777828 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1009, + "type": "imguizmo", + "translation": [ + -0.8654269700272739, + -1.5277933387438483, + 1.2286704876553156 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1010, + "type": "imguizmo", + "translation": [ + -0.8103157148133387, + -1.5944398403942603, + 1.245374848367352 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1011, + "type": "imguizmo", + "translation": [ + -0.752886737872621, + -1.6590998685494966, + 1.2558748142290797 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1012, + "type": "imguizmo", + "translation": [ + -0.6932115885591016, + -1.7216928648662024, + 1.2601180749387397 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1013, + "type": "imguizmo", + "translation": [ + -0.6313646146778419, + -1.7821408462652917, + 1.2580834907849197 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1014, + "type": "imguizmo", + "translation": [ + -0.5674228698568673, + -1.8403685020891636, + 1.24978119796353 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1015, + "type": "imguizmo", + "translation": [ + -0.5014660175479455, + -1.8963032879294006, + 1.2352525580797895 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1016, + "type": "imguizmo", + "translation": [ + -0.43357623177584687, + -1.9498755160080645, + 1.2145699520867863 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1017, + "type": "imguizmo", + "translation": [ + -0.3638380947597374, + -2.001018441999997, + 1.1878364196872109 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1018, + "type": "imguizmo", + "translation": [ + -0.29233849153427044, + -2.0496683481879296, + 1.15518514599474 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1019, + "type": "imguizmo", + "translation": [ + -0.21916650170165367, + -2.095764622846832, + 1.1167787980125021 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1020, + "type": "imguizmo", + "translation": [ + -0.1444132884495489, + -2.1392498357585708, + 1.0728087142342444 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1021, + "type": "imguizmo", + "translation": [ + -0.06817198497310444, + -2.180069809762807, + 1.023493951405589 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1022, + "type": "imguizmo", + "translation": [ + 0.009462421557417125, + -2.2181736882549976, + 0.9690801931943398 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1023, + "type": "imguizmo", + "translation": [ + 0.08839320833882182, + -2.253513998547387, + 0.909838526206811 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1024, + "type": "imguizmo", + "translation": [ + 0.16852203743955418, + -2.2860467110140643, + 0.8460640894479874 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1025, + "type": "imguizmo", + "translation": [ + 0.24974907831651344, + -2.3157312939463996, + 0.7780746039538501 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1026, + "type": "imguizmo", + "translation": [ + 0.33197313219147934, + -2.3425307640505, + 0.706208789921156 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1027, + "type": "imguizmo", + "translation": [ + 0.41509175813218596, + -2.366411732523793, + 0.6308246792204575 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1028, + "type": "imguizmo", + "translation": [ + 0.4990014006809761, + -2.3873444466533127, + 0.5522978316993259 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1029, + "type": "imguizmo", + "translation": [ + 0.583597518871992, + -2.405302826883878, + 0.47101946416209284 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1030, + "type": "imguizmo", + "translation": [ + 0.6687747164762112, + -2.4202644993099707, + 0.38739450134740283 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1031, + "type": "imguizmo", + "translation": [ + 0.7544268733120203, + -2.4322108235508346, + 0.30183955861353534 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1032, + "type": "imguizmo", + "translation": [ + 0.8404472774577475, + -2.4411269159740687, + 0.21478086638164598 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1033, + "type": "imguizmo", + "translation": [ + 0.9267287582014283, + -2.4470016682387805, + 0.126652146677249 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1034, + "type": "imguizmo", + "translation": [ + 1.0131638195621673, + -2.449827761135196, + 0.03789245234890243 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1035, + "type": "imguizmo", + "translation": [ + 1.0996447742167426, + -2.4496016737034836, + -0.05105602027099576 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1036, + "type": "imguizmo", + "translation": [ + 1.1860638776646102, + -2.4463236876204273, + -0.13975013436610229 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1037, + "type": "imguizmo", + "translation": [ + 1.2723134624641195, + -2.4399978868485013, + -0.2277480203214419 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1038, + "type": "imguizmo", + "translation": [ + 1.3582860723727506, + -2.4306321525477523, + -0.31461127709561343 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1039, + "type": "imguizmo", + "translation": [ + 1.443874596224207, + -2.418238153256861, + -0.3999071563127225 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1040, + "type": "imguizmo", + "translation": [ + 1.5289724013755908, + -2.402831330355595, + -0.4832107181930224 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1041, + "type": "imguizmo", + "translation": [ + 1.6134734665583992, + -2.3844308788267687, + -0.564106948581522 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1042, + "type": "imguizmo", + "translation": [ + 1.697272513967821, + -2.3630597233416917, + -0.6421928265276298 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1043, + "type": "imguizmo", + "translation": [ + 1.7802651404257712, + -2.3387444896988776, + -0.717079332115255 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1044, + "type": "imguizmo", + "translation": [ + 1.862347947454258, + -2.3115154716516155, + -0.7883933845404703 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1045, + "type": "imguizmo", + "translation": [ + 1.943418670096997, + -2.281406593165727, + -0.8557797007812963 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1046, + "type": "imguizmo", + "translation": [ + 2.023376304328823, + -2.2484553661545297, + -0.9189025655998562 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1047, + "type": "imguizmo", + "translation": [ + 2.102121232894123, + -2.2127028437436613, + -0.9774475040588003 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1048, + "type": "imguizmo", + "translation": [ + 2.179555349417536, + -2.1741935691240015, + -1.0311228482196262 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1049, + "type": "imguizmo", + "translation": [ + 2.255582180632276, + -2.1329755200563985, + -1.0796611902176763 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1050, + "type": "imguizmo", + "translation": [ + 2.330107006573811, + -2.0891000490973552, + -1.1228207144746691 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1051, + "type": "imguizmo", + "translation": [ + 2.4030369785891423, + -2.042621819620142, + -1.1603864024117454 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1052, + "type": "imguizmo", + "translation": [ + 2.4742812350146717, + -1.9935987377110316, + -1.192171103661216 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1053, + "type": "imguizmo", + "translation": [ + 2.5437510143785067, + -1.9420918800255351, + -1.2180164684402741 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1054, + "type": "imguizmo", + "translation": [ + 2.611359765986213, + -1.888165417694492, + -1.237793736441652 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1055, + "type": "imguizmo", + "translation": [ + 2.677023257752195, + -1.8318865363748296, + -1.2514043783109934 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1056, + "type": "imguizmo", + "translation": [ + 2.7406596811423913, + -1.7733253525446033, + -1.2587805865151411 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1057, + "type": "imguizmo", + "translation": [ + 2.802189753097517, + -1.7125548261465957, + -1.2598856131558573 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1058, + "type": "imguizmo", + "translation": [ + 2.861536814809883, + -1.6496506696893118, + -1.2547139530460016 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1059, + "type": "imguizmo", + "translation": [ + 2.9186269272307284, + -1.5846912539186233, + -1.2432913711360996 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1060, + "type": "imguizmo", + "translation": [ + 2.9733889631890618, + -1.517757510177577, + -1.2256747741546528 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1061, + "type": "imguizmo", + "translation": [ + 3.025754696007265, + -1.4489328295760193, + -1.2019519271016808 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1062, + "type": "imguizmo", + "translation": [ + 3.0756588845030355, + -1.3783029590956586, + -1.1722410160079064 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1063, + "type": "imguizmo", + "translation": [ + 3.123039354271773, + -1.3059558947600025, + -1.1366900591378946 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1064, + "type": "imguizmo", + "translation": [ + 3.1678370751481513, + -1.2319817720022703, + -1.095476169570504 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1065, + "type": "imguizmo", + "translation": [ + 3.2099962347503466, + -1.1564727533678676, + -1.0488046728304379 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1066, + "type": "imguizmo", + "translation": [ + 3.2494643080153196, + -1.0795229136913314, + -0.996908083966808 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1067, + "type": "imguizmo", + "translation": [ + 3.2861921226385027, + -1.0012281228908024, + -0.9400449491748564 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1068, + "type": "imguizmo", + "translation": [ + 3.320133920336369, + -0.9216859265260363, + -0.8784985577318112 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1069, + "type": "imguizmo", + "translation": [ + 3.351247413855554, + -0.840995424268797, + -0.8125755306639658 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1070, + "type": "imguizmo", + "translation": [ + 3.3794938396575076, + -0.7592571464369966, + -0.7426042931761428 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1071, + "type": "imguizmo", + "translation": [ + 3.404838006213038, + -0.6765729287464395, + -0.6689334384538467 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1072, + "type": "imguizmo", + "translation": [ + 3.4272483378465712, + -0.5930457854362021, + -0.5919299909895473 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1073, + "type": "imguizmo", + "translation": [ + 3.4466969140755173, + -0.5087797809257188, + -0.5119775780851209 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1074, + "type": "imguizmo", + "translation": [ + 3.4631595043957066, + -0.42387990016347343, + -0.4294745186399287 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1075, + "type": "imguizmo", + "translation": [ + 3.476615598469588, + -0.3384519178288289, + -0.344831838746099 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1076, + "type": "imguizmo", + "translation": [ + 3.4870484316795514, + -0.25260226654993895, + -0.2584712239772022 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1077, + "type": "imguizmo", + "translation": [ + 3.494445006014555, + -0.1664379043019617, + -0.17082291857194842 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1078, + "type": "imguizmo", + "translation": [ + 3.4987961062640283, + -0.08006618115073047, + -0.08232358197902302 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1079, + "type": "imguizmo", + "translation": [ + 3.5000963114988757, + 0.006405294492056901, + 0.006585886558319931 + ], + "rotation_deg": [ + 0.0, + 100.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1080, + "type": "action", + "action": "set_active_render_window", + "value": 0 + }, + { + "frame": 1080, + "type": "action", + "action": "set_active_planar", + "value": 6 + }, + { + "frame": 1080, + "type": "action", + "action": "set_projection_type", + "value": "perspective" + }, + { + "frame": 1081, + "type": "imguizmo", + "translation": [ + 3.5, + 0.0, + 0.0 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1082, + "type": "imguizmo", + "translation": [ + 3.498351706745195, + 0.09338121349488564, + 0.09598941546477674 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1083, + "type": "imguizmo", + "translation": [ + 3.493540744009305, + 0.17917558854636395, + 0.1838214632529401 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1084, + "type": "imguizmo", + "translation": [ + 3.485694420036376, + 0.2653443728925339, + 0.27135205401665224 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1085, + "type": "imguizmo", + "translation": [ + 3.474812805218336, + 0.3511347600105016, + 0.3574816359159584 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1086, + "type": "imguizmo", + "translation": [ + 3.460910233104283, + 0.4364915018366905, + 0.441834194013461 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1087, + "type": "imguizmo", + "translation": [ + 3.444003962454902, + 0.5213041236136793, + 0.5239852418432069 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1088, + "type": "imguizmo", + "translation": [ + 3.424115061347224, + 0.6054672898468222, + 0.6035258467183974 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1089, + "type": "imguizmo", + "translation": [ + 3.4012683084778628, + 0.6888761172736524, + 0.6800597141704652 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1090, + "type": "imguizmo", + "translation": [ + 3.375492168087725, + 0.771426690996607, + 0.7532055584956212 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1091, + "type": "imguizmo", + "translation": [ + 3.3468187540354477, + 0.8530161631111502, + 0.8225989706028591 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1092, + "type": "imguizmo", + "translation": [ + 3.3152837898246155, + 0.933542883308407, + 0.8878942360175692 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1093, + "type": "imguizmo", + "translation": [ + 3.2809265640937606, + 1.0129065253214347, + 0.9487660570107936 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1094, + "type": "imguizmo", + "translation": [ + 3.243789881667847, + 1.0910082119350464, + 1.004911173232379 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1095, + "type": "imguizmo", + "translation": [ + 3.203920010228772, + 1.1677506381732456, + 1.056049872537487 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1096, + "type": "imguizmo", + "translation": [ + 3.1613666226715935, + 1.2430381925292104, + 1.1019273844985356 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1097, + "type": "imguizmo", + "translation": [ + 3.116182735218291, + 1.316777076085318, + 1.142315149658643 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1098, + "type": "imguizmo", + "translation": [ + 3.068424641366156, + 1.388875419374914, + 1.1770119582033483 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1099, + "type": "imguizmo", + "translation": [ + 3.0181518417531, + 1.4592433968402316, + 1.2058449523777839 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1100, + "type": "imguizmo", + "translation": [ + 2.9654269700272744, + 1.527793338743847, + 1.2286704876553167 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1101, + "type": "imguizmo", + "translation": [ + 2.9103157148133394, + 1.594439840394259, + 1.2453748483673535 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1102, + "type": "imguizmo", + "translation": [ + 2.85288673787262, + 1.6590998685494973, + 1.255874814229081 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1103, + "type": "imguizmo", + "translation": [ + 2.793211588559101, + 1.7216928648662022, + 1.260118074938741 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1104, + "type": "imguizmo", + "translation": [ + 2.731364614677841, + 1.7821408462652917, + 1.2580834907849205 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1105, + "type": "imguizmo", + "translation": [ + 2.6674228698568667, + 1.8403685020891636, + 1.2497811979635312 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1106, + "type": "imguizmo", + "translation": [ + 2.601466017547946, + 1.8963032879294, + 1.2352525580797908 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1107, + "type": "imguizmo", + "translation": [ + 2.5335762317758475, + 1.9498755160080639, + 1.2145699520867876 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1108, + "type": "imguizmo", + "translation": [ + 2.4638380947597374, + 2.0010184419999955, + 1.1878364196872122 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1109, + "type": "imguizmo", + "translation": [ + 2.3923384915342716, + 2.0496683481879283, + 1.1551851459947415 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1110, + "type": "imguizmo", + "translation": [ + 2.3191665017016527, + 2.095764622846832, + 1.1167787980125026 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1111, + "type": "imguizmo", + "translation": [ + 2.244413288449548, + 2.1392498357585708, + 1.0728087142342453 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1112, + "type": "imguizmo", + "translation": [ + 2.168171984973103, + 2.180069809762807, + 1.0234939514055896 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1113, + "type": "imguizmo", + "translation": [ + 2.0905375784425826, + 2.2181736882549976, + 0.9690801931943411 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1114, + "type": "imguizmo", + "translation": [ + 2.011606791661178, + 2.2535139985473864, + 0.9098385262068122 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1115, + "type": "imguizmo", + "translation": [ + 1.9314779625604461, + 2.286046711014064, + 0.8460640894479889 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1116, + "type": "imguizmo", + "translation": [ + 1.8502509216834866, + 2.315731293946399, + 0.7780746039538516 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1117, + "type": "imguizmo", + "translation": [ + 1.7680268678085211, + 2.3425307640504998, + 0.7062087899211581 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1118, + "type": "imguizmo", + "translation": [ + 1.6849082418678123, + 2.3664117325237926, + 0.6308246792204576 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1119, + "type": "imguizmo", + "translation": [ + 1.6009985993190228, + 2.3873444466533114, + 0.5522978316993266 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1120, + "type": "imguizmo", + "translation": [ + 1.516402481128007, + 2.405302826883877, + 0.4710194641620934 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1121, + "type": "imguizmo", + "translation": [ + 1.4312252835237878, + 2.42026449930997, + 0.3873945013474034 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1122, + "type": "imguizmo", + "translation": [ + 1.3455731266879787, + 2.4322108235508333, + 0.30183955861353584 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1123, + "type": "imguizmo", + "translation": [ + 1.259552722542252, + 2.441126915974067, + 0.21478086638164712 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1124, + "type": "imguizmo", + "translation": [ + 1.1732712417985711, + 2.447001668238779, + 0.12665214667725008 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1125, + "type": "imguizmo", + "translation": [ + 1.0868361804378328, + 2.449827761135195, + 0.03789245234890409 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1126, + "type": "imguizmo", + "translation": [ + 1.000355225783255, + 2.4496016737034814, + -0.05105602027099654 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1127, + "type": "imguizmo", + "translation": [ + 0.9139361223353878, + 2.446323687620426, + -0.13975013436610284 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1128, + "type": "imguizmo", + "translation": [ + 0.8276865375358794, + 2.4399978868485, + -0.22774802032144126 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1129, + "type": "imguizmo", + "translation": [ + 0.7417139276272481, + 2.4306321525477506, + -0.314611277095613 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1130, + "type": "imguizmo", + "translation": [ + 0.6561254037757919, + 2.4182381532568598, + -0.399907156312722 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1131, + "type": "imguizmo", + "translation": [ + 0.5710275986244079, + 2.4028313303555935, + -0.4832107181930219 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1132, + "type": "imguizmo", + "translation": [ + 0.48652653344160013, + 2.3844308788267674, + -0.564106948581521 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1133, + "type": "imguizmo", + "translation": [ + 0.402727486032178, + 2.3630597233416903, + -0.6421928265276287 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1134, + "type": "imguizmo", + "translation": [ + 0.3197348595742256, + 2.338744489698876, + -0.7170793321152561 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1135, + "type": "imguizmo", + "translation": [ + 0.2376520525457393, + 2.3115154716516138, + -0.7883933845404711 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1136, + "type": "imguizmo", + "translation": [ + 0.1565813299030015, + 2.2814065931657255, + -0.855779700781296 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1137, + "type": "imguizmo", + "translation": [ + 0.0766236956711756, + 2.248455366154528, + -0.9189025655998558 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1138, + "type": "imguizmo", + "translation": [ + -0.0021212328941254133, + 2.2127028437436596, + -0.9774475040588004 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1139, + "type": "imguizmo", + "translation": [ + -0.07955534941753811, + 2.1741935691239997, + -1.0311228482196264 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1140, + "type": "imguizmo", + "translation": [ + -0.1555821806322771, + 2.132975520056397, + -1.0796611902176756 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1141, + "type": "imguizmo", + "translation": [ + -0.23010700657381208, + 2.089100049097354, + -1.1228207144746687 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1142, + "type": "imguizmo", + "translation": [ + -0.30303697858914547, + 2.0426218196201393, + -1.1603864024117458 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1143, + "type": "imguizmo", + "translation": [ + -0.3742812350146743, + 1.9935987377110287, + -1.1921711036612164 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1144, + "type": "imguizmo", + "translation": [ + -0.44375101437850833, + 1.9420918800255336, + -1.2180164684402743 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1145, + "type": "imguizmo", + "translation": [ + -0.5113597659862144, + 1.88816541769449, + -1.237793736441652 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1146, + "type": "imguizmo", + "translation": [ + -0.5770232577521969, + 1.8318865363748278, + -1.2514043783109934 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1147, + "type": "imguizmo", + "translation": [ + -0.6406596811423929, + 1.7733253525446013, + -1.2587805865151411 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1148, + "type": "imguizmo", + "translation": [ + -0.7021897530975179, + 1.7125548261465942, + -1.2598856131558573 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1149, + "type": "imguizmo", + "translation": [ + -0.7615368148098847, + 1.6496506696893103, + -1.2547139530460016 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1150, + "type": "imguizmo", + "translation": [ + -0.8186269272307299, + 1.5846912539186218, + -1.2432913711360996 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1151, + "type": "imguizmo", + "translation": [ + -0.8733889631890633, + 1.5177575101775755, + -1.2256747741546528 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1152, + "type": "imguizmo", + "translation": [ + -0.9257546960072662, + 1.4489328295760187, + -1.201951927101681 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1153, + "type": "imguizmo", + "translation": [ + -0.9756588845030363, + 1.3783029590956584, + -1.1722410160079066 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1154, + "type": "imguizmo", + "translation": [ + -1.0230393542717746, + 1.3059558947600016, + -1.1366900591378948 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1155, + "type": "imguizmo", + "translation": [ + -1.0678370751481527, + 1.2319817720022697, + -1.0954761695705044 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1156, + "type": "imguizmo", + "translation": [ + -1.109996234750347, + 1.1564727533678683, + -1.048804672830439 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1157, + "type": "imguizmo", + "translation": [ + -1.1494643080153202, + 1.079522913691332, + -0.9969080839668093 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1158, + "type": "imguizmo", + "translation": [ + -1.186192122638504, + 1.0012281228908007, + -0.9400449491748561 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1159, + "type": "imguizmo", + "translation": [ + -1.2201339203363704, + 0.9216859265260348, + -0.878498557731811 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1160, + "type": "imguizmo", + "translation": [ + -1.2512474138555556, + 0.8409954242687956, + -0.8125755306639656 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1161, + "type": "imguizmo", + "translation": [ + -1.2794938396575095, + 0.759257146436995, + -0.7426042931761426 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1162, + "type": "imguizmo", + "translation": [ + -1.3048380062130394, + 0.6765729287464379, + -0.6689334384538466 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1163, + "type": "imguizmo", + "translation": [ + -1.3272483378465731, + 0.5930457854362006, + -0.5919299909895472 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1164, + "type": "imguizmo", + "translation": [ + -1.3466969140755187, + 0.5087797809257184, + -0.5119775780851219 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1165, + "type": "imguizmo", + "translation": [ + -1.3631595043957085, + 0.4238799001634729, + -0.4294745186399297 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1166, + "type": "imguizmo", + "translation": [ + -1.3766155984695898, + 0.33845191782882594, + -0.3448318387460978 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1167, + "type": "imguizmo", + "translation": [ + -1.3870484316795537, + 0.2526022665499362, + -0.258471223977201 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1168, + "type": "imguizmo", + "translation": [ + -1.3944450060145572, + 0.1664379043019601, + -0.17082291857194848 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1169, + "type": "imguizmo", + "translation": [ + -1.3987961062640304, + 0.08006618115072876, + -0.08232358197902295 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1170, + "type": "imguizmo", + "translation": [ + -1.4000963114988783, + -0.00640529449205858, + 0.0065858865583200005 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1171, + "type": "imguizmo", + "translation": [ + -1.3983440018252848, + -0.09286878993552146, + 0.09546254454010955 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1172, + "type": "imguizmo", + "translation": [ + -1.3935413604028986, + -0.17921658243111382, + 0.1838636129269121 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1173, + "type": "imguizmo", + "translation": [ + -1.385694370724889, + -0.26534109338175493, + 0.2713486820427333 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1174, + "type": "imguizmo", + "translation": [ + -1.3748128091632554, + -0.3511350223713651, + 0.35748190567387095 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1175, + "type": "imguizmo", + "translation": [ + -1.36091023278869, + -0.4364914808478205, + 0.441834172432825 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1176, + "type": "imguizmo", + "translation": [ + -1.3440039624801499, + -0.5213041252927882, + 0.5239852435696551 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1177, + "type": "imguizmo", + "translation": [ + -1.3241150613452046, + -0.6054672897124932, + 0.6035258465802791 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1178, + "type": "imguizmo", + "translation": [ + -1.301268308478024, + -0.6888761172843985, + 0.6800597141815122 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1179, + "type": "imguizmo", + "translation": [ + -1.2754921680877125, + -0.7714266909957476, + 0.7532055584947357 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1180, + "type": "imguizmo", + "translation": [ + -1.2468187540354487, + -0.8530161631112192, + 0.8225989706029284 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1181, + "type": "imguizmo", + "translation": [ + -1.2152837898246154, + -0.9335428833084021, + 0.8878942360175622 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1182, + "type": "imguizmo", + "translation": [ + -1.1809265640937605, + -1.0129065253214358, + 0.9487660570107929 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1183, + "type": "imguizmo", + "translation": [ + -1.143789881667848, + -1.0910082119350462, + 1.0049111732323772 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1184, + "type": "imguizmo", + "translation": [ + -1.1039200102287723, + -1.1677506381732456, + 1.0560498725374856 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1185, + "type": "imguizmo", + "translation": [ + -1.0613666226715934, + -1.2430381925292107, + 1.1019273844985342 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1186, + "type": "imguizmo", + "translation": [ + -1.0161827352182906, + -1.3167770760853184, + 1.1423151496586417 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1187, + "type": "imguizmo", + "translation": [ + -0.9684246413661551, + -1.3888754193749149, + 1.1770119582033471 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1188, + "type": "imguizmo", + "translation": [ + -0.9181518417530996, + -1.4592433968402323, + 1.2058449523777828 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1189, + "type": "imguizmo", + "translation": [ + -0.8654269700272739, + -1.5277933387438483, + 1.2286704876553156 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1190, + "type": "imguizmo", + "translation": [ + -0.8103157148133387, + -1.5944398403942603, + 1.245374848367352 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1191, + "type": "imguizmo", + "translation": [ + -0.752886737872621, + -1.6590998685494966, + 1.2558748142290797 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1192, + "type": "imguizmo", + "translation": [ + -0.6932115885591016, + -1.7216928648662024, + 1.2601180749387397 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1193, + "type": "imguizmo", + "translation": [ + -0.6313646146778419, + -1.7821408462652917, + 1.2580834907849197 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1194, + "type": "imguizmo", + "translation": [ + -0.5674228698568673, + -1.8403685020891636, + 1.24978119796353 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1195, + "type": "imguizmo", + "translation": [ + -0.5014660175479455, + -1.8963032879294006, + 1.2352525580797895 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1196, + "type": "imguizmo", + "translation": [ + -0.43357623177584687, + -1.9498755160080645, + 1.2145699520867863 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1197, + "type": "imguizmo", + "translation": [ + -0.3638380947597374, + -2.001018441999997, + 1.1878364196872109 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1198, + "type": "imguizmo", + "translation": [ + -0.29233849153427044, + -2.0496683481879296, + 1.15518514599474 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1199, + "type": "imguizmo", + "translation": [ + -0.21916650170165367, + -2.095764622846832, + 1.1167787980125021 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1200, + "type": "imguizmo", + "translation": [ + -0.1444132884495489, + -2.1392498357585708, + 1.0728087142342444 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1201, + "type": "imguizmo", + "translation": [ + -0.06817198497310444, + -2.180069809762807, + 1.023493951405589 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1202, + "type": "imguizmo", + "translation": [ + 0.009462421557417125, + -2.2181736882549976, + 0.9690801931943398 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1203, + "type": "imguizmo", + "translation": [ + 0.08839320833882182, + -2.253513998547387, + 0.909838526206811 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1204, + "type": "imguizmo", + "translation": [ + 0.16852203743955418, + -2.2860467110140643, + 0.8460640894479874 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1205, + "type": "imguizmo", + "translation": [ + 0.24974907831651344, + -2.3157312939463996, + 0.7780746039538501 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1206, + "type": "imguizmo", + "translation": [ + 0.33197313219147934, + -2.3425307640505, + 0.706208789921156 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1207, + "type": "imguizmo", + "translation": [ + 0.41509175813218596, + -2.366411732523793, + 0.6308246792204575 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1208, + "type": "imguizmo", + "translation": [ + 0.4990014006809761, + -2.3873444466533127, + 0.5522978316993259 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1209, + "type": "imguizmo", + "translation": [ + 0.583597518871992, + -2.405302826883878, + 0.47101946416209284 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1210, + "type": "imguizmo", + "translation": [ + 0.6687747164762112, + -2.4202644993099707, + 0.38739450134740283 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1211, + "type": "imguizmo", + "translation": [ + 0.7544268733120203, + -2.4322108235508346, + 0.30183955861353534 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1212, + "type": "imguizmo", + "translation": [ + 0.8404472774577475, + -2.4411269159740687, + 0.21478086638164598 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1213, + "type": "imguizmo", + "translation": [ + 0.9267287582014283, + -2.4470016682387805, + 0.126652146677249 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1214, + "type": "imguizmo", + "translation": [ + 1.0131638195621673, + -2.449827761135196, + 0.03789245234890243 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1215, + "type": "imguizmo", + "translation": [ + 1.0996447742167426, + -2.4496016737034836, + -0.05105602027099576 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1216, + "type": "imguizmo", + "translation": [ + 1.1860638776646102, + -2.4463236876204273, + -0.13975013436610229 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1217, + "type": "imguizmo", + "translation": [ + 1.2723134624641195, + -2.4399978868485013, + -0.2277480203214419 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1218, + "type": "imguizmo", + "translation": [ + 1.3582860723727506, + -2.4306321525477523, + -0.31461127709561343 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1219, + "type": "imguizmo", + "translation": [ + 1.443874596224207, + -2.418238153256861, + -0.3999071563127225 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1220, + "type": "imguizmo", + "translation": [ + 1.5289724013755908, + -2.402831330355595, + -0.4832107181930224 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1221, + "type": "imguizmo", + "translation": [ + 1.6134734665583992, + -2.3844308788267687, + -0.564106948581522 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1222, + "type": "imguizmo", + "translation": [ + 1.697272513967821, + -2.3630597233416917, + -0.6421928265276298 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1223, + "type": "imguizmo", + "translation": [ + 1.7802651404257712, + -2.3387444896988776, + -0.717079332115255 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1224, + "type": "imguizmo", + "translation": [ + 1.862347947454258, + -2.3115154716516155, + -0.7883933845404703 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1225, + "type": "imguizmo", + "translation": [ + 1.943418670096997, + -2.281406593165727, + -0.8557797007812963 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1226, + "type": "imguizmo", + "translation": [ + 2.023376304328823, + -2.2484553661545297, + -0.9189025655998562 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1227, + "type": "imguizmo", + "translation": [ + 2.102121232894123, + -2.2127028437436613, + -0.9774475040588003 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1228, + "type": "imguizmo", + "translation": [ + 2.179555349417536, + -2.1741935691240015, + -1.0311228482196262 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1229, + "type": "imguizmo", + "translation": [ + 2.255582180632276, + -2.1329755200563985, + -1.0796611902176763 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1230, + "type": "imguizmo", + "translation": [ + 2.330107006573811, + -2.0891000490973552, + -1.1228207144746691 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1231, + "type": "imguizmo", + "translation": [ + 2.4030369785891423, + -2.042621819620142, + -1.1603864024117454 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1232, + "type": "imguizmo", + "translation": [ + 2.4742812350146717, + -1.9935987377110316, + -1.192171103661216 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1233, + "type": "imguizmo", + "translation": [ + 2.5437510143785067, + -1.9420918800255351, + -1.2180164684402741 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1234, + "type": "imguizmo", + "translation": [ + 2.611359765986213, + -1.888165417694492, + -1.237793736441652 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1235, + "type": "imguizmo", + "translation": [ + 2.677023257752195, + -1.8318865363748296, + -1.2514043783109934 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1236, + "type": "imguizmo", + "translation": [ + 2.7406596811423913, + -1.7733253525446033, + -1.2587805865151411 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1237, + "type": "imguizmo", + "translation": [ + 2.802189753097517, + -1.7125548261465957, + -1.2598856131558573 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1238, + "type": "imguizmo", + "translation": [ + 2.861536814809883, + -1.6496506696893118, + -1.2547139530460016 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1239, + "type": "imguizmo", + "translation": [ + 2.9186269272307284, + -1.5846912539186233, + -1.2432913711360996 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1240, + "type": "imguizmo", + "translation": [ + 2.9733889631890618, + -1.517757510177577, + -1.2256747741546528 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1241, + "type": "imguizmo", + "translation": [ + 3.025754696007265, + -1.4489328295760193, + -1.2019519271016808 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1242, + "type": "imguizmo", + "translation": [ + 3.0756588845030355, + -1.3783029590956586, + -1.1722410160079064 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1243, + "type": "imguizmo", + "translation": [ + 3.123039354271773, + -1.3059558947600025, + -1.1366900591378946 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1244, + "type": "imguizmo", + "translation": [ + 3.1678370751481513, + -1.2319817720022703, + -1.095476169570504 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1245, + "type": "imguizmo", + "translation": [ + 3.2099962347503466, + -1.1564727533678676, + -1.0488046728304379 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1246, + "type": "imguizmo", + "translation": [ + 3.2494643080153196, + -1.0795229136913314, + -0.996908083966808 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1247, + "type": "imguizmo", + "translation": [ + 3.2861921226385027, + -1.0012281228908024, + -0.9400449491748564 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1248, + "type": "imguizmo", + "translation": [ + 3.320133920336369, + -0.9216859265260363, + -0.8784985577318112 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1249, + "type": "imguizmo", + "translation": [ + 3.351247413855554, + -0.840995424268797, + -0.8125755306639658 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1250, + "type": "imguizmo", + "translation": [ + 3.3794938396575076, + -0.7592571464369966, + -0.7426042931761428 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1251, + "type": "imguizmo", + "translation": [ + 3.404838006213038, + -0.6765729287464395, + -0.6689334384538467 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1252, + "type": "imguizmo", + "translation": [ + 3.4272483378465712, + -0.5930457854362021, + -0.5919299909895473 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1253, + "type": "imguizmo", + "translation": [ + 3.4466969140755173, + -0.5087797809257188, + -0.5119775780851209 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1254, + "type": "imguizmo", + "translation": [ + 3.4631595043957066, + -0.42387990016347343, + -0.4294745186399287 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1255, + "type": "imguizmo", + "translation": [ + 3.476615598469588, + -0.3384519178288289, + -0.344831838746099 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1256, + "type": "imguizmo", + "translation": [ + 3.4870484316795514, + -0.25260226654993895, + -0.2584712239772022 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1257, + "type": "imguizmo", + "translation": [ + 3.494445006014555, + -0.1664379043019617, + -0.17082291857194842 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1258, + "type": "imguizmo", + "translation": [ + 3.4987961062640283, + -0.08006618115073047, + -0.08232358197902302 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1259, + "type": "imguizmo", + "translation": [ + 3.5000963114988757, + 0.006405294492056901, + 0.006585886558319931 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1260, + "type": "action", + "action": "set_active_render_window", + "value": 0 + }, + { + "frame": 1260, + "type": "action", + "action": "set_active_planar", + "value": 7 + }, + { + "frame": 1260, + "type": "action", + "action": "set_projection_type", + "value": "perspective" + }, + { + "frame": 1261, + "type": "imguizmo", + "translation": [ + 1.64, + 0.0, + 1.64 + ], + "rotation_deg": [ + 0.0, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1262, + "type": "imguizmo", + "translation": [ + 1.639227656874891, + 0.03281682645677409, + 1.64 + ], + "rotation_deg": [ + 0.9528695254580167, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1263, + "type": "imguizmo", + "translation": [ + 1.6369733771929313, + 0.06296742111772219, + 1.64 + ], + "rotation_deg": [ + 1.9045518941423958, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1264, + "type": "imguizmo", + "translation": [ + 1.6332968139599016, + 0.09324959390223336, + 1.64 + ], + "rotation_deg": [ + 2.8538614283290102, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1265, + "type": "imguizmo", + "translation": [ + 1.6281980001594487, + 0.12339878708940485, + 1.64 + ], + "rotation_deg": [ + 3.7996154065500436, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1266, + "type": "imguizmo", + "translation": [ + 1.6216836520831495, + 0.15339558493117983, + 1.64 + ], + "rotation_deg": [ + 4.74063553711766, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1267, + "type": "imguizmo", + "translation": [ + 1.6137618566931538, + 0.18320116344137866, + 1.64 + ], + "rotation_deg": [ + 5.675749426128718, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1268, + "type": "imguizmo", + "translation": [ + 1.6044424858884132, + 0.21277850471759752, + 1.64 + ], + "rotation_deg": [ + 6.603792038121597, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1269, + "type": "imguizmo", + "translation": [ + 1.5937371502581978, + 0.24209074978474063, + 1.64 + ], + "rotation_deg": [ + 7.523607147565286, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1270, + "type": "imguizmo", + "translation": [ + 1.5816591873325336, + 0.27110137997880757, + 1.64 + ], + "rotation_deg": [ + 8.43404877937244, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1271, + "type": "imguizmo", + "translation": [ + 1.5682236447480375, + 0.29977425160763277, + 1.64 + ], + "rotation_deg": [ + 9.333982636641618, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1272, + "type": "imguizmo", + "translation": [ + 1.5534472615178194, + 0.328073641848383, + 1.64 + ], + "rotation_deg": [ + 10.222287513849988, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1273, + "type": "imguizmo", + "translation": [ + 1.5373484471753618, + 0.35596429318438993, + 1.64 + ], + "rotation_deg": [ + 11.097856693735785, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1274, + "type": "imguizmo", + "translation": [ + 1.519947258838648, + 0.3834114573371734, + 1.64 + ], + "rotation_deg": [ + 11.959599326130213, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1275, + "type": "imguizmo", + "translation": [ + 1.5012653762214814, + 0.41038093855802626, + 1.64 + ], + "rotation_deg": [ + 12.806441787020908, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1276, + "type": "imguizmo", + "translation": [ + 1.4813260746232602, + 0.43683913623169396, + 1.64 + ], + "rotation_deg": [ + 13.637329016153773, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1277, + "type": "imguizmo", + "translation": [ + 1.4601541959308557, + 0.4627530867385546, + 1.64 + ], + "rotation_deg": [ + 14.451225831506678, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1278, + "type": "imguizmo", + "translation": [ + 1.4377761176687125, + 0.48809050452318403, + 1.64 + ], + "rotation_deg": [ + 15.247118218997338, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1279, + "type": "imguizmo", + "translation": [ + 1.4142197201357378, + 0.5128198223181384, + 1.64 + ], + "rotation_deg": [ + 16.024014595818613, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1280, + "type": "imguizmo", + "translation": [ + 1.3895143516699222, + 0.5369102304728377, + 1.64 + ], + "rotation_deg": [ + 16.780947045827197, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1281, + "type": "imguizmo", + "translation": [ + 1.3636907920839643, + 0.560331715338554, + 1.64 + ], + "rotation_deg": [ + 17.516972525446597, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1282, + "type": "imguizmo", + "translation": [ + 1.3367812143174558, + 0.5830550966616804, + 1.64 + ], + "rotation_deg": [ + 18.23117403858202, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1283, + "type": "imguizmo", + "translation": [ + 1.308819144353407, + 0.605052063938694, + 1.64 + ], + "rotation_deg": [ + 18.922661779083285, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1284, + "type": "imguizmo", + "translation": [ + 1.2798394194490452, + 0.6262952116875169, + 1.64 + ], + "rotation_deg": [ + 19.59057423933253, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1285, + "type": "imguizmo", + "translation": [ + 1.2498781447329317, + 0.6467580735913346, + 1.64 + ], + "rotation_deg": [ + 20.234079283575376, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1286, + "type": "imguizmo", + "translation": [ + 1.2189726482224659, + 0.666415155472332, + 1.64 + ], + "rotation_deg": [ + 20.852375184658495, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1287, + "type": "imguizmo", + "translation": [ + 1.1871614343178254, + 0.6852419670542624, + 1.64 + ], + "rotation_deg": [ + 21.44469162288179, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1288, + "type": "imguizmo", + "translation": [ + 1.154484135830277, + 0.7032150524742842, + 1.64 + ], + "rotation_deg": [ + 22.01029064572082, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1289, + "type": "imguizmo", + "translation": [ + 1.12098146460463, + 0.7203120195060435, + 1.64 + ], + "rotation_deg": [ + 22.548467587223755, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1290, + "type": "imguizmo", + "translation": [ + 1.0866951607973458, + 0.7365115674576009, + 1.64 + ], + "rotation_deg": [ + 23.05855194593741, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1291, + "type": "imguizmo", + "translation": [ + 1.0516679408735028, + 0.7517935137094406, + 1.64 + ], + "rotation_deg": [ + 23.539908220268543, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1292, + "type": "imguizmo", + "translation": [ + 1.0159434443873971, + 0.7661388188595009, + 1.64 + ], + "rotation_deg": [ + 23.991936700239727, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1293, + "type": "imguizmo", + "translation": [ + 0.979566179613096, + 0.7795296104438992, + 1.64 + ], + "rotation_deg": [ + 24.414074214653297, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1294, + "type": "imguizmo", + "translation": [ + 0.9425814680926663, + 0.7919492052037957, + 1.64 + ], + "rotation_deg": [ + 24.805794832732513, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1295, + "type": "imguizmo", + "translation": [ + 0.9050353881711805, + 0.8033821298706567, + 1.64 + ], + "rotation_deg": [ + 25.166610519365864, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1296, + "type": "imguizmo", + "translation": [ + 0.8669747175888339, + 0.8138141404440203, + 1.64 + ], + "rotation_deg": [ + 25.496071743138003, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1297, + "type": "imguizmo", + "translation": [ + 0.8284468752017072, + 0.823232239937747, + 1.64 + ], + "rotation_deg": [ + 25.793768036389917, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1298, + "type": "imguizmo", + "translation": [ + 0.7894998619037751, + 0.8316246945726472, + 1.64 + ], + "rotation_deg": [ + 26.059328506610537, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1299, + "type": "imguizmo", + "translation": [ + 0.750182200823771, + 0.8389810483953068, + 1.64 + ], + "rotation_deg": [ + 26.292422298522606, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1300, + "type": "imguizmo", + "translation": [ + 0.7105428768714092, + 0.8452921363049057, + 1.64 + ], + "rotation_deg": [ + 26.492759006287177, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1301, + "type": "imguizmo", + "translation": [ + 0.6706312757082894, + 0.8505500954717897, + 1.64 + ], + "rotation_deg": [ + 26.66008903531307, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1302, + "type": "imguizmo", + "translation": [ + 0.6304971222195103, + 0.8547483751335789, + 1.64 + ], + "rotation_deg": [ + 26.794203913220734, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1303, + "type": "imguizmo", + "translation": [ + 0.5901904185626555, + 0.8578817447566012, + 1.64 + ], + "rotation_deg": [ + 26.894936549572805, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1304, + "type": "imguizmo", + "translation": [ + 0.5497613818713308, + 0.8599463005524858, + 1.64 + ], + "rotation_deg": [ + 26.962161444048018, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1305, + "type": "imguizmo", + "translation": [ + 0.5092603816908705, + 0.8609394703417974, + 1.64 + ], + "rotation_deg": [ + 26.99579484279896, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1306, + "type": "imguizmo", + "translation": [ + 0.4687378772241541, + 0.8608600167586525, + 1.64 + ], + "rotation_deg": [ + 26.99579484279896, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1307, + "type": "imguizmo", + "translation": [ + 0.4282443544657249, + 0.8597080387923216, + 1.64 + ], + "rotation_deg": [ + 26.962161444048018, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1308, + "type": "imguizmo", + "translation": [ + 0.3878302633025267, + 0.8574849716639017, + 1.64 + ], + "rotation_deg": [ + 26.894936549572808, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1309, + "type": "imguizmo", + "translation": [ + 0.3475459546596252, + 0.85419358503821, + 1.64 + ], + "rotation_deg": [ + 26.794203913220734, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1310, + "type": "imguizmo", + "translation": [ + 0.30744161776922857, + 0.8498379795731253, + 1.64 + ], + "rotation_deg": [ + 26.66008903531307, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1311, + "type": "imguizmo", + "translation": [ + 0.26756721764115154, + 0.8444235818106802, + 1.64 + ], + "rotation_deg": [ + 26.492759006287177, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1312, + "type": "imguizmo", + "translation": [ + 0.2279724328126359, + 0.8379571374162643, + 1.64 + ], + "rotation_deg": [ + 26.29242229852261, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1313, + "type": "imguizmo", + "translation": [ + 0.18870659345507812, + 0.8304467027743658, + 1.64 + ], + "rotation_deg": [ + 26.059328506610537, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1314, + "type": "imguizmo", + "translation": [ + 0.1498186199147805, + 0.8219016349513193, + 1.64 + ], + "rotation_deg": [ + 25.793768036389917, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1315, + "type": "imguizmo", + "translation": [ + 0.11135696176428965, + 0.8123325800375673, + 1.64 + ], + "rotation_deg": [ + 25.496071743138003, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1316, + "type": "imguizmo", + "translation": [ + 0.07336953744026398, + 0.801751459883955, + 1.64 + ], + "rotation_deg": [ + 25.166610519365868, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1317, + "type": "imguizmo", + "translation": [ + 0.03590367454306552, + 0.7901714572485914, + 1.64 + ], + "rotation_deg": [ + 24.805794832732516, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1318, + "type": "imguizmo", + "translation": [ + -0.0009939491275325688, + 0.7776069993727719, + 1.64 + ], + "rotation_deg": [ + 24.41407421465329, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1319, + "type": "imguizmo", + "translation": [ + -0.03727736372707465, + 0.7640737400064342, + 1.64 + ], + "rotation_deg": [ + 23.99193670023972, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1320, + "type": "imguizmo", + "translation": [ + -0.07290136463912357, + 0.749588539905534, + 1.64 + ], + "rotation_deg": [ + 23.539908220268543, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1321, + "type": "imguizmo", + "translation": [ + -0.10782156879458578, + 0.7341694458256416, + 1.64 + ], + "rotation_deg": [ + 23.05855194593741, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1322, + "type": "imguizmo", + "translation": [ + -0.14199446996748483, + 0.7178356680379347, + 1.64 + ], + "rotation_deg": [ + 22.548467587223755, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1323, + "type": "imguizmo", + "translation": [ + -0.1753774929783042, + 0.7006075563955902, + 1.64 + ], + "rotation_deg": [ + 22.01029064572082, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1324, + "type": "imguizmo", + "translation": [ + -0.20792904673735776, + 0.6825065749804016, + 1.64 + ], + "rotation_deg": [ + 21.444691622881795, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1325, + "type": "imguizmo", + "translation": [ + -0.23960857606211147, + 0.6635552753612066, + 1.64 + ], + "rotation_deg": [ + 20.8523751846585, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1326, + "type": "imguizmo", + "translation": [ + -0.270376612203886, + 0.6437772684974393, + 1.64 + ], + "rotation_deg": [ + 20.23407928357538, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1327, + "type": "imguizmo", + "translation": [ + -0.3001948220210066, + 0.623197195322817, + 1.64 + ], + "rotation_deg": [ + 19.59057423933253, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1328, + "type": "imguizmo", + "translation": [ + -0.3290260557371222, + 0.6018406960458029, + 1.64 + ], + "rotation_deg": [ + 18.92266177908329, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1329, + "type": "imguizmo", + "translation": [ + -0.35683439322520266, + 0.5797343782051005, + 1.64 + ], + "rotation_deg": [ + 18.231174038582026, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1330, + "type": "imguizmo", + "translation": [ + -0.3835851887595415, + 0.5569057835199728, + 1.64 + ], + "rotation_deg": [ + 17.516972525446604, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1331, + "type": "imguizmo", + "translation": [ + -0.40924511418001785, + 0.5333833535766906, + 1.64 + ], + "rotation_deg": [ + 16.780947045827197, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1332, + "type": "imguizmo", + "translation": [ + -0.4337822004148328, + 0.509196394393858, + 1.64 + ], + "rotation_deg": [ + 16.024014595818624, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1333, + "type": "imguizmo", + "translation": [ + -0.4571658773099938, + 0.48437503991075975, + 1.64 + ], + "rotation_deg": [ + 15.247118218997347, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1334, + "type": "imguizmo", + "translation": [ + -0.4793670117159167, + 0.4589502144442291, + 1.64 + ], + "rotation_deg": [ + 14.45122583150668, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1335, + "type": "imguizmo", + "translation": [ + -0.5003579437837052, + 0.43295359416079754, + 1.64 + ], + "rotation_deg": [ + 13.637329016153776, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1336, + "type": "imguizmo", + "translation": [ + -0.5201125214258765, + 0.40641756761213643, + 1.64 + ], + "rotation_deg": [ + 12.806441787020917, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1337, + "type": "imguizmo", + "translation": [ + -0.5386061328986069, + 0.3793751953829538, + 1.64 + ], + "rotation_deg": [ + 11.959599326130222, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1338, + "type": "imguizmo", + "translation": [ + -0.5558157374648987, + 0.35186016890162425, + 1.64 + ], + "rotation_deg": [ + 11.09785669373579, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1339, + "type": "imguizmo", + "translation": [ + -0.5717198941004705, + 0.32390676846486366, + 1.64 + ], + "rotation_deg": [ + 10.222287513849992, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1340, + "type": "imguizmo", + "translation": [ + -0.5862987882066028, + 0.2955498205287481, + 1.64 + ], + "rotation_deg": [ + 9.33398263664162, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1341, + "type": "imguizmo", + "translation": [ + -0.5995342562966611, + 0.26682465431928676, + 1.64 + ], + "rotation_deg": [ + 8.434048779372437, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1342, + "type": "imguizmo", + "translation": [ + -0.611409808625538, + 0.2377670578166053, + 1.64 + ], + "rotation_deg": [ + 7.523607147565284, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1343, + "type": "imguizmo", + "translation": [ + -0.6219106497338223, + 0.20841323316757898, + 1.64 + ], + "rotation_deg": [ + 6.60379203812159, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1344, + "type": "imguizmo", + "translation": [ + -0.6310236968810998, + 0.17879975158246672, + 1.64 + ], + "rotation_deg": [ + 5.675749426128722, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1345, + "type": "imguizmo", + "translation": [ + -0.6387375963454169, + 0.14896350777173473, + 1.64 + ], + "rotation_deg": [ + 4.74063553711766, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1346, + "type": "imguizmo", + "translation": [ + -0.6450427375686073, + 0.11894167397984451, + 1.64 + ], + "rotation_deg": [ + 3.799615406550042, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1347, + "type": "imguizmo", + "translation": [ + -0.6499312651298472, + 0.08877165367326328, + 1.64 + ], + "rotation_deg": [ + 2.8538614283290062, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1348, + "type": "imguizmo", + "translation": [ + -0.6533970885325346, + 0.05849103494040307, + 1.64 + ], + "rotation_deg": [ + 1.9045518941424011, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1349, + "type": "imguizmo", + "translation": [ + -0.655435889792288, + 0.02813754366154179, + 1.64 + ], + "rotation_deg": [ + 0.9528695254580194, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1350, + "type": "imguizmo", + "translation": [ + -0.6560451288166165, + -0.0022510034929234743, + 1.64 + ], + "rotation_deg": [ + 3.3306690738754696E-16, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1351, + "type": "imguizmo", + "translation": [ + -0.6552240465695613, + -0.03263674617734043, + 1.64 + ], + "rotation_deg": [ + -0.9528695254580187, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1352, + "type": "imguizmo", + "translation": [ + -0.6529736660173575, + -0.06298182754007718, + 1.64 + ], + "rotation_deg": [ + -1.9045518941424004, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1353, + "type": "imguizmo", + "translation": [ + -0.6492967908539472, + -0.09324844138844533, + 1.64 + ], + "rotation_deg": [ + -2.853861428329018, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1354, + "type": "imguizmo", + "translation": [ + -0.6441980020079249, + -0.12339887929050834, + 1.64 + ], + "rotation_deg": [ + -3.7996154065500534, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1355, + "type": "imguizmo", + "translation": [ + -0.637683651935271, + -0.1533955775550912, + 1.64 + ], + "rotation_deg": [ + -4.740635537117647, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1356, + "type": "imguizmo", + "translation": [ + -0.6297618567049839, + -0.18320116403146564, + 1.64 + ], + "rotation_deg": [ + -5.675749426128711, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1357, + "type": "imguizmo", + "translation": [ + -0.620442485887467, + -0.21277850467039044, + 1.64 + ], + "rotation_deg": [ + -6.6037920381215915, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1358, + "type": "imguizmo", + "translation": [ + -0.6097371502582737, + -0.2420907497885172, + 1.64 + ], + "rotation_deg": [ + -7.523607147565285, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1359, + "type": "imguizmo", + "translation": [ + -0.5976591873325274, + -0.27110137997850553, + 1.64 + ], + "rotation_deg": [ + -8.43404877937244, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1360, + "type": "imguizmo", + "translation": [ + -0.5842236447480383, + -0.299774251607657, + 1.64 + ], + "rotation_deg": [ + -9.333982636641622, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1361, + "type": "imguizmo", + "translation": [ + -0.5694472615178193, + -0.3280736418483813, + 1.64 + ], + "rotation_deg": [ + -10.222287513849993, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1362, + "type": "imguizmo", + "translation": [ + -0.5533484471753617, + -0.3559642931843903, + 1.64 + ], + "rotation_deg": [ + -11.097856693735793, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1363, + "type": "imguizmo", + "translation": [ + -0.5359472588386486, + -0.3834114573371734, + 1.64 + ], + "rotation_deg": [ + -11.959599326130213, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1364, + "type": "imguizmo", + "translation": [ + -0.5172653762214814, + -0.41038093855802643, + 1.64 + ], + "rotation_deg": [ + -12.806441787020908, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1365, + "type": "imguizmo", + "translation": [ + -0.49732607462326034, + -0.4368391362316941, + 1.64 + ], + "rotation_deg": [ + -13.637329016153778, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1366, + "type": "imguizmo", + "translation": [ + -0.4761541959308557, + -0.4627530867385548, + 1.64 + ], + "rotation_deg": [ + -14.451225831506685, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1367, + "type": "imguizmo", + "translation": [ + -0.4537761176687122, + -0.4880905045231844, + 1.64 + ], + "rotation_deg": [ + -15.247118218997349, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1368, + "type": "imguizmo", + "translation": [ + -0.4302197201357375, + -0.5128198223181389, + 1.64 + ], + "rotation_deg": [ + -16.024014595818628, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1369, + "type": "imguizmo", + "translation": [ + -0.40551435166992184, + -0.5369102304728384, + 1.64 + ], + "rotation_deg": [ + -16.78094704582721, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1370, + "type": "imguizmo", + "translation": [ + -0.37969079208396367, + -0.5603317153385546, + 1.64 + ], + "rotation_deg": [ + -17.516972525446615, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1371, + "type": "imguizmo", + "translation": [ + -0.35278121431745607, + -0.5830550966616804, + 1.64 + ], + "rotation_deg": [ + -18.231174038582015, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1372, + "type": "imguizmo", + "translation": [ + -0.324819144353407, + -0.6050520639386942, + 1.64 + ], + "rotation_deg": [ + -18.922661779083285, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1373, + "type": "imguizmo", + "translation": [ + -0.2958394194490453, + -0.6262952116875171, + 1.64 + ], + "rotation_deg": [ + -19.590574239332533, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1374, + "type": "imguizmo", + "translation": [ + -0.2658781447329314, + -0.646758073591335, + 1.64 + ], + "rotation_deg": [ + -20.23407928357538, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1375, + "type": "imguizmo", + "translation": [ + -0.23497264822246525, + -0.6664151554723325, + 1.64 + ], + "rotation_deg": [ + -20.85237518465851, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1376, + "type": "imguizmo", + "translation": [ + -0.20316143431782474, + -0.6852419670542631, + 1.64 + ], + "rotation_deg": [ + -21.444691622881802, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1377, + "type": "imguizmo", + "translation": [ + -0.17048413583027625, + -0.703215052474285, + 1.64 + ], + "rotation_deg": [ + -22.010290645720836, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1378, + "type": "imguizmo", + "translation": [ + -0.13698146460462893, + -0.7203120195060444, + 1.64 + ], + "rotation_deg": [ + -22.548467587223772, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1379, + "type": "imguizmo", + "translation": [ + -0.10269516079734561, + -0.7365115674576013, + 1.64 + ], + "rotation_deg": [ + -23.058551945937413, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1380, + "type": "imguizmo", + "translation": [ + -0.06766794087350216, + -0.751793513709441, + 1.64 + ], + "rotation_deg": [ + -23.539908220268547, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1381, + "type": "imguizmo", + "translation": [ + -0.031943444387396845, + -0.7661388188595013, + 1.64 + ], + "rotation_deg": [ + -23.99193670023973, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1382, + "type": "imguizmo", + "translation": [ + 0.004433820386904652, + -0.7795296104438997, + 1.64 + ], + "rotation_deg": [ + -24.4140742146533, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1383, + "type": "imguizmo", + "translation": [ + 0.04141853190733425, + -0.7919492052037964, + 1.64 + ], + "rotation_deg": [ + -24.80579483273252, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1384, + "type": "imguizmo", + "translation": [ + 0.07896461182882031, + -0.8033821298706573, + 1.64 + ], + "rotation_deg": [ + -25.16661051936587, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1385, + "type": "imguizmo", + "translation": [ + 0.11702528241116694, + -0.813814140444021, + 1.64 + ], + "rotation_deg": [ + -25.49607174313801, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1386, + "type": "imguizmo", + "translation": [ + 0.1555531247982938, + -0.8232322399377476, + 1.64 + ], + "rotation_deg": [ + -25.79376803638992, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1387, + "type": "imguizmo", + "translation": [ + 0.19450013809622493, + -0.8316246945726475, + 1.64 + ], + "rotation_deg": [ + -26.05932850661054, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1388, + "type": "imguizmo", + "translation": [ + 0.23381779917622947, + -0.8389810483953073, + 1.64 + ], + "rotation_deg": [ + -26.29242229852261, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1389, + "type": "imguizmo", + "translation": [ + 0.2734571231285912, + -0.8452921363049061, + 1.64 + ], + "rotation_deg": [ + -26.492759006287177, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1390, + "type": "imguizmo", + "translation": [ + 0.313368724291711, + -0.8505500954717902, + 1.64 + ], + "rotation_deg": [ + -26.66008903531307, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1391, + "type": "imguizmo", + "translation": [ + 0.35350287778049017, + -0.8547483751335794, + 1.64 + ], + "rotation_deg": [ + -26.794203913220738, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1392, + "type": "imguizmo", + "translation": [ + 0.3938095814373452, + -0.8578817447566016, + 1.64 + ], + "rotation_deg": [ + -26.89493654957281, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1393, + "type": "imguizmo", + "translation": [ + 0.43423861812866993, + -0.8599463005524862, + 1.64 + ], + "rotation_deg": [ + -26.96216144404802, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1394, + "type": "imguizmo", + "translation": [ + 0.4747396183091305, + -0.8609394703417979, + 1.64 + ], + "rotation_deg": [ + -26.995794842798965, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1395, + "type": "imguizmo", + "translation": [ + 0.5152621227758457, + -0.860860016758653, + 1.64 + ], + "rotation_deg": [ + -26.99579484279897, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1396, + "type": "imguizmo", + "translation": [ + 0.555755645534275, + -0.859708038792322, + 1.64 + ], + "rotation_deg": [ + -26.96216144404802, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1397, + "type": "imguizmo", + "translation": [ + 0.5961697366974738, + -0.8574849716639021, + 1.64 + ], + "rotation_deg": [ + -26.89493654957281, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1398, + "type": "imguizmo", + "translation": [ + 0.6364540453403752, + -0.8541935850382104, + 1.64 + ], + "rotation_deg": [ + -26.79420391322074, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1399, + "type": "imguizmo", + "translation": [ + 0.6765583822307718, + -0.8498379795731258, + 1.64 + ], + "rotation_deg": [ + -26.660089035313074, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1400, + "type": "imguizmo", + "translation": [ + 0.7164327823588489, + -0.8444235818106807, + 1.64 + ], + "rotation_deg": [ + -26.49275900628718, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1401, + "type": "imguizmo", + "translation": [ + 0.7560275671873647, + -0.8379571374162648, + 1.64 + ], + "rotation_deg": [ + -26.292422298522613, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1402, + "type": "imguizmo", + "translation": [ + 0.7952934065449225, + -0.8304467027743663, + 1.64 + ], + "rotation_deg": [ + -26.05932850661054, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1403, + "type": "imguizmo", + "translation": [ + 0.8341813800852191, + -0.8219016349513202, + 1.64 + ], + "rotation_deg": [ + -25.793768036389928, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1404, + "type": "imguizmo", + "translation": [ + 0.8726430382357101, + -0.8123325800375678, + 1.64 + ], + "rotation_deg": [ + -25.496071743138017, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1405, + "type": "imguizmo", + "translation": [ + 0.9106304625597363, + -0.8017514598839556, + 1.64 + ], + "rotation_deg": [ + -25.16661051936588, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1406, + "type": "imguizmo", + "translation": [ + 0.9480963254569347, + -0.7901714572485918, + 1.64 + ], + "rotation_deg": [ + -24.805794832732527, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1407, + "type": "imguizmo", + "translation": [ + 0.9849939491275326, + -0.7776069993727726, + 1.64 + ], + "rotation_deg": [ + -24.4140742146533, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1408, + "type": "imguizmo", + "translation": [ + 1.0212773637270747, + -0.7640737400064349, + 1.64 + ], + "rotation_deg": [ + -23.99193670023973, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1409, + "type": "imguizmo", + "translation": [ + 1.056901364639124, + -0.7495885399055345, + 1.64 + ], + "rotation_deg": [ + -23.539908220268547, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1410, + "type": "imguizmo", + "translation": [ + 1.0918215687945865, + -0.7341694458256421, + 1.64 + ], + "rotation_deg": [ + -23.058551945937413, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1411, + "type": "imguizmo", + "translation": [ + 1.1259944699674846, + -0.7178356680379359, + 1.64 + ], + "rotation_deg": [ + -22.548467587223776, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1412, + "type": "imguizmo", + "translation": [ + 1.1593774929783038, + -0.7006075563955912, + 1.64 + ], + "rotation_deg": [ + -22.01029064572084, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1413, + "type": "imguizmo", + "translation": [ + 1.191929046737358, + -0.6825065749804025, + 1.64 + ], + "rotation_deg": [ + -21.44469162288181, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1414, + "type": "imguizmo", + "translation": [ + 1.2236085760621118, + -0.6635552753612074, + 1.64 + ], + "rotation_deg": [ + -20.852375184658513, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1415, + "type": "imguizmo", + "translation": [ + 1.2543766122038864, + -0.6437772684974403, + 1.64 + ], + "rotation_deg": [ + -20.234079283575394, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1416, + "type": "imguizmo", + "translation": [ + 1.284194822021007, + -0.6231971953228179, + 1.64 + ], + "rotation_deg": [ + -19.590574239332543, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1417, + "type": "imguizmo", + "translation": [ + 1.3130260557371232, + -0.6018406960458037, + 1.64 + ], + "rotation_deg": [ + -18.922661779083295, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1418, + "type": "imguizmo", + "translation": [ + 1.3408343932252036, + -0.5797343782051012, + 1.64 + ], + "rotation_deg": [ + -18.23117403858203, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1419, + "type": "imguizmo", + "translation": [ + 1.3675851887595423, + -0.5569057835199734, + 1.64 + ], + "rotation_deg": [ + -17.516972525446608, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1420, + "type": "imguizmo", + "translation": [ + 1.3932451141800186, + -0.5333833535766914, + 1.64 + ], + "rotation_deg": [ + -16.780947045827208, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1421, + "type": "imguizmo", + "translation": [ + 1.417782200414834, + -0.5091963943938584, + 1.64 + ], + "rotation_deg": [ + -16.02401459581862, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1422, + "type": "imguizmo", + "translation": [ + 1.4411658773099951, + -0.4843750399107602, + 1.64 + ], + "rotation_deg": [ + -15.247118218997342, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1423, + "type": "imguizmo", + "translation": [ + 1.4633670117159179, + -0.45895021444422957, + 1.64 + ], + "rotation_deg": [ + -14.451225831506678, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1424, + "type": "imguizmo", + "translation": [ + 1.4843579437837064, + -0.43295359416079804, + 1.64 + ], + "rotation_deg": [ + -13.637329016153773, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1425, + "type": "imguizmo", + "translation": [ + 1.5041125214258777, + -0.4064175676121365, + 1.64 + ], + "rotation_deg": [ + -12.806441787020901, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1426, + "type": "imguizmo", + "translation": [ + 1.5226061328986082, + -0.3793751953829538, + 1.64 + ], + "rotation_deg": [ + -11.959599326130203, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1427, + "type": "imguizmo", + "translation": [ + 1.5398157374648995, + -0.35186016890162497, + 1.64 + ], + "rotation_deg": [ + -11.097856693735796, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1428, + "type": "imguizmo", + "translation": [ + 1.5557198941004713, + -0.3239067684648643, + 1.64 + ], + "rotation_deg": [ + -10.222287513849997, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1429, + "type": "imguizmo", + "translation": [ + 1.5702987882066035, + -0.29554982052874884, + 1.64 + ], + "rotation_deg": [ + -9.333982636641625, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1430, + "type": "imguizmo", + "translation": [ + 1.5835342562966621, + -0.2668246543192875, + 1.64 + ], + "rotation_deg": [ + -8.434048779372445, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1431, + "type": "imguizmo", + "translation": [ + 1.5954098086255388, + -0.237767057816606, + 1.64 + ], + "rotation_deg": [ + -7.52360714756529, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1432, + "type": "imguizmo", + "translation": [ + 1.6059106497338234, + -0.2084132331675797, + 1.64 + ], + "rotation_deg": [ + -6.603792038121595, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1433, + "type": "imguizmo", + "translation": [ + 1.6150236968811007, + -0.17879975158246703, + 1.64 + ], + "rotation_deg": [ + -5.675749426128716, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1434, + "type": "imguizmo", + "translation": [ + 1.622737596345418, + -0.14896350777173506, + 1.64 + ], + "rotation_deg": [ + -4.740635537117653, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1435, + "type": "imguizmo", + "translation": [ + 1.6290427375686083, + -0.11894167397984569, + 1.64 + ], + "rotation_deg": [ + -3.7996154065500596, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1436, + "type": "imguizmo", + "translation": [ + 1.6339312651298483, + -0.08877165367326437, + 1.64 + ], + "rotation_deg": [ + -2.853861428329023, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1437, + "type": "imguizmo", + "translation": [ + 1.6373970885325355, + -0.05849103494040378, + 1.64 + ], + "rotation_deg": [ + -1.9045518941424064, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1438, + "type": "imguizmo", + "translation": [ + 1.6394358897922887, + -0.028137543661542527, + 1.64 + ], + "rotation_deg": [ + -0.9528695254580246, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1439, + "type": "imguizmo", + "translation": [ + 1.6400451288166176, + 0.0022510034929227526, + 1.64 + ], + "rotation_deg": [ + -5.551115123125783E-15, + 120.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1440, + "type": "action", + "action": "set_active_render_window", + "value": 0 + }, + { + "frame": 1440, + "type": "action", + "action": "set_active_planar", + "value": 8 + }, + { + "frame": 1440, + "type": "action", + "action": "set_projection_type", + "value": "perspective" + }, + { + "frame": 1441, + "type": "imguizmo", + "translation": [ + 1.64, + 0.0, + 2.05 + ], + "rotation_deg": [ + 0.0, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1442, + "type": "imguizmo", + "translation": [ + 1.639227656874891, + 0.03281682645677409, + 2.1374570229790186 + ], + "rotation_deg": [ + 0.9528695254580167, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1443, + "type": "imguizmo", + "translation": [ + 1.6369733771929313, + 0.06296742111772219, + 2.2174817776304567 + ], + "rotation_deg": [ + 1.9045518941423958, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1444, + "type": "imguizmo", + "translation": [ + 1.6332968139599016, + 0.09324959390223336, + 2.2972318714373943 + ], + "rotation_deg": [ + 2.8538614283290102, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1445, + "type": "imguizmo", + "translation": [ + 1.6281980001594487, + 0.12339878708940485, + 2.3757054905012067 + ], + "rotation_deg": [ + 3.7996154065500436, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1446, + "type": "imguizmo", + "translation": [ + 1.6216836520831495, + 0.15339558493117983, + 2.4525600434344867 + ], + "rotation_deg": [ + 4.74063553711766, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1447, + "type": "imguizmo", + "translation": [ + 1.6137618566931538, + 0.18320116344137866, + 2.5274087759015886 + ], + "rotation_deg": [ + 5.675749426128718, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1448, + "type": "imguizmo", + "translation": [ + 1.6044424858884132, + 0.21277850471759752, + 2.5998791047878735 + ], + "rotation_deg": [ + 6.603792038121597, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1449, + "type": "imguizmo", + "translation": [ + 1.5937371502581978, + 0.24209074978474063, + 2.6696099617997566 + ], + "rotation_deg": [ + 7.523607147565286, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1450, + "type": "imguizmo", + "translation": [ + 1.5816591873325336, + 0.27110137997880757, + 2.73625395329601 + ], + "rotation_deg": [ + 8.43404877937244, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1451, + "type": "imguizmo", + "translation": [ + 1.5682236447480375, + 0.29977425160763277, + 2.799479062104827 + ], + "rotation_deg": [ + 9.333982636641618, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1452, + "type": "imguizmo", + "translation": [ + 1.5534472615178194, + 0.328073641848383, + 2.8589703039271184 + ], + "rotation_deg": [ + 10.222287513849988, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1453, + "type": "imguizmo", + "translation": [ + 1.5373484471753618, + 0.35596429318438993, + 2.914431296387612 + ], + "rotation_deg": [ + 11.097856693735785, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1454, + "type": "imguizmo", + "translation": [ + 1.519947258838648, + 0.3834114573371734, + 2.9655857356117235 + ], + "rotation_deg": [ + 11.959599326130213, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1455, + "type": "imguizmo", + "translation": [ + 1.5012653762214814, + 0.41038093855802626, + 3.0121787727563767 + ], + "rotation_deg": [ + 12.806441787020908, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1456, + "type": "imguizmo", + "translation": [ + 1.4813260746232602, + 0.43683913623169396, + 3.0539782836542213 + ], + "rotation_deg": [ + 13.637329016153773, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1457, + "type": "imguizmo", + "translation": [ + 1.4601541959308557, + 0.4627530867385546, + 3.0907760252445415 + ], + "rotation_deg": [ + 14.451225831506678, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1458, + "type": "imguizmo", + "translation": [ + 1.4377761176687125, + 0.48809050452318403, + 3.122388673029717 + ], + "rotation_deg": [ + 15.247118218997338, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1459, + "type": "imguizmo", + "translation": [ + 1.4142197201357378, + 0.5128198223181384, + 3.148658734388647 + ], + "rotation_deg": [ + 16.024014595818613, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1460, + "type": "imguizmo", + "translation": [ + 1.3895143516699222, + 0.5369102304728377, + 3.1694553331970656 + ], + "rotation_deg": [ + 16.780947045827197, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1461, + "type": "imguizmo", + "translation": [ + 1.3636907920839643, + 0.560331715338554, + 3.184674861845811 + ], + "rotation_deg": [ + 17.516972525446597, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1462, + "type": "imguizmo", + "translation": [ + 1.3367812143174558, + 0.5830550966616804, + 3.194241497408718 + ], + "rotation_deg": [ + 18.23117403858202, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1463, + "type": "imguizmo", + "translation": [ + 1.308819144353407, + 0.605052063938694, + 3.1981075793886293 + ], + "rotation_deg": [ + 18.922661779083285, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1464, + "type": "imguizmo", + "translation": [ + 1.2798394194490452, + 0.6262952116875169, + 3.1962538471595936 + ], + "rotation_deg": [ + 19.59057423933253, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1465, + "type": "imguizmo", + "translation": [ + 1.2498781447329317, + 0.6467580735913346, + 3.188689535922328 + ], + "rotation_deg": [ + 20.234079283575376, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1466, + "type": "imguizmo", + "translation": [ + 1.2189726482224659, + 0.666415155472332, + 3.17545233069492 + ], + "rotation_deg": [ + 20.852375184658495, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1467, + "type": "imguizmo", + "translation": [ + 1.1871614343178254, + 0.6852419670542624, + 3.1566081785679616 + ], + "rotation_deg": [ + 21.44469162288179, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1468, + "type": "imguizmo", + "translation": [ + 1.154484135830277, + 0.7032150524742842, + 3.1322509601594595 + ], + "rotation_deg": [ + 22.01029064572082, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1469, + "type": "imguizmo", + "translation": [ + 1.12098146460463, + 0.7203120195060435, + 3.1025020219063193 + ], + "rotation_deg": [ + 22.548467587223755, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1470, + "type": "imguizmo", + "translation": [ + 1.0866951607973458, + 0.7365115674576009, + 3.0675095715225016 + ], + "rotation_deg": [ + 23.05855194593741, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1471, + "type": "imguizmo", + "translation": [ + 1.0516679408735028, + 0.7517935137094406, + 3.0274479396356453 + ], + "rotation_deg": [ + 23.539908220268543, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1472, + "type": "imguizmo", + "translation": [ + 1.0159434443873971, + 0.7661388188595009, + 2.9825167112806477 + ], + "rotation_deg": [ + 23.991936700239727, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1473, + "type": "imguizmo", + "translation": [ + 0.979566179613096, + 0.7795296104438992, + 2.9329397315770662 + ], + "rotation_deg": [ + 24.414074214653297, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1474, + "type": "imguizmo", + "translation": [ + 0.9425814680926663, + 0.7919492052037957, + 2.878963990543984 + ], + "rotation_deg": [ + 24.805794832732513, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1475, + "type": "imguizmo", + "translation": [ + 0.9050353881711805, + 0.8033821298706567, + 2.8208583926081676 + ], + "rotation_deg": [ + 25.166610519365864, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1476, + "type": "imguizmo", + "translation": [ + 0.8669747175888339, + 0.8138141404440203, + 2.7589124169357313 + ], + "rotation_deg": [ + 25.496071743138003, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1477, + "type": "imguizmo", + "translation": [ + 0.8284468752017072, + 0.823232239937747, + 2.693434675261499 + ], + "rotation_deg": [ + 25.793768036389917, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1478, + "type": "imguizmo", + "translation": [ + 0.7894998619037751, + 0.8316246945726472, + 2.6247513744008613 + ], + "rotation_deg": [ + 26.059328506610537, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1479, + "type": "imguizmo", + "translation": [ + 0.750182200823771, + 0.8389810483953068, + 2.5532046911038306 + ], + "rotation_deg": [ + 26.292422298522606, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1480, + "type": "imguizmo", + "translation": [ + 0.7105428768714092, + 0.8452921363049057, + 2.479151067347685 + ], + "rotation_deg": [ + 26.492759006287177, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1481, + "type": "imguizmo", + "translation": [ + 0.6706312757082894, + 0.8505500954717897, + 2.4029594345609673 + ], + "rotation_deg": [ + 26.66008903531307, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1482, + "type": "imguizmo", + "translation": [ + 0.6304971222195103, + 0.8547483751335789, + 2.3250093756256662 + ], + "rotation_deg": [ + 26.794203913220734, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1483, + "type": "imguizmo", + "translation": [ + 0.5901904185626555, + 0.8578817447566012, + 2.2456892338143897 + ], + "rotation_deg": [ + 26.894936549572805, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1484, + "type": "imguizmo", + "translation": [ + 0.5497613818713308, + 0.8599463005524858, + 2.1653941780837167 + ], + "rotation_deg": [ + 26.962161444048018, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1485, + "type": "imguizmo", + "translation": [ + 0.5092603816908705, + 0.8609394703417974, + 2.0845242343623345 + ], + "rotation_deg": [ + 26.99579484279896, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1486, + "type": "imguizmo", + "translation": [ + 0.4687378772241541, + 0.8608600167586525, + 2.0034822926419804 + ], + "rotation_deg": [ + 26.99579484279896, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1487, + "type": "imguizmo", + "translation": [ + 0.4282443544657249, + 0.8597080387923216, + 1.9226720997997726 + ], + "rotation_deg": [ + 26.962161444048018, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1488, + "type": "imguizmo", + "translation": [ + 0.3878302633025267, + 0.8574849716639017, + 1.8424962481515754 + ], + "rotation_deg": [ + 26.894936549572808, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1489, + "type": "imguizmo", + "translation": [ + 0.3475459546596252, + 0.85419358503821, + 1.7633541697573303 + ], + "rotation_deg": [ + 26.794203913220734, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1490, + "type": "imguizmo", + "translation": [ + 0.30744161776922857, + 0.8498379795731253, + 1.6856401464706305 + ], + "rotation_deg": [ + 26.66008903531307, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1491, + "type": "imguizmo", + "translation": [ + 0.26756721764115154, + 0.8444235818106802, + 1.6097413456463574 + ], + "rotation_deg": [ + 26.492759006287177, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1492, + "type": "imguizmo", + "translation": [ + 0.2279724328126359, + 0.8379571374162643, + 1.5360358912923917 + ], + "rotation_deg": [ + 26.29242229852261, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1493, + "type": "imguizmo", + "translation": [ + 0.18870659345507812, + 0.8304467027743658, + 1.4648909802748267 + ], + "rotation_deg": [ + 26.059328506610537, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1494, + "type": "imguizmo", + "translation": [ + 0.1498186199147805, + 0.8219016349513193, + 1.3966610529616552 + ], + "rotation_deg": [ + 25.793768036389917, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1495, + "type": "imguizmo", + "translation": [ + 0.11135696176428965, + 0.8123325800375673, + 1.3316860274186815 + ], + "rotation_deg": [ + 25.496071743138003, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1496, + "type": "imguizmo", + "translation": [ + 0.07336953744026398, + 0.801751459883955, + 1.2702896059548188 + ], + "rotation_deg": [ + 25.166610519365868, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1497, + "type": "imguizmo", + "translation": [ + 0.03590367454306552, + 0.7901714572485914, + 1.212777662453464 + ], + "rotation_deg": [ + 24.805794832732516, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1498, + "type": "imguizmo", + "translation": [ + -0.0009939491275325688, + 0.7776069993727719, + 1.1594367185242036 + ], + "rotation_deg": [ + 24.41407421465329, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1499, + "type": "imguizmo", + "translation": [ + -0.03727736372707465, + 0.7640737400064342, + 1.1105325160665624 + ], + "rotation_deg": [ + 23.99193670023972, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1500, + "type": "imguizmo", + "translation": [ + -0.07290136463912357, + 0.749588539905534, + 1.0663086933572283 + ], + "rotation_deg": [ + 23.539908220268543, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1501, + "type": "imguizmo", + "translation": [ + -0.10782156879458578, + 0.7341694458256416, + 1.0269855712564127 + ], + "rotation_deg": [ + 23.05855194593741, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1502, + "type": "imguizmo", + "translation": [ + -0.14199446996748483, + 0.7178356680379347, + 0.992759055580409 + ], + "rotation_deg": [ + 22.548467587223755, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1503, + "type": "imguizmo", + "translation": [ + -0.1753774929783042, + 0.7006075563955902, + 0.9637996611086691 + ], + "rotation_deg": [ + 22.01029064572082, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1504, + "type": "imguizmo", + "translation": [ + -0.20792904673735776, + 0.6825065749804016, + 0.9402516620877498 + ], + "rotation_deg": [ + 21.444691622881795, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1505, + "type": "imguizmo", + "translation": [ + -0.23960857606211147, + 0.6635552753612066, + 0.9222323734642723 + ], + "rotation_deg": [ + 20.8523751846585, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1506, + "type": "imguizmo", + "translation": [ + -0.270376612203886, + 0.6437772684974393, + 0.9098315664277614 + ], + "rotation_deg": [ + 20.23407928357538, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1507, + "type": "imguizmo", + "translation": [ + -0.3001948220210066, + 0.623197195322817, + 0.9031110211750932 + ], + "rotation_deg": [ + 19.59057423933253, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1508, + "type": "imguizmo", + "translation": [ + -0.3290260557371222, + 0.6018406960458029, + 0.9021042191246632 + ], + "rotation_deg": [ + 18.92266177908329, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1509, + "type": "imguizmo", + "translation": [ + -0.35683439322520266, + 0.5797343782051005, + 0.9068161761136428 + ], + "rotation_deg": [ + 18.231174038582026, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1510, + "type": "imguizmo", + "translation": [ + -0.3835851887595415, + 0.5569057835199728, + 0.9172234174093314 + ], + "rotation_deg": [ + 17.516972525446604, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1511, + "type": "imguizmo", + "translation": [ + -0.40924511418001785, + 0.5333833535766906, + 0.9332740946590943 + ], + "rotation_deg": [ + 16.780947045827197, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1512, + "type": "imguizmo", + "translation": [ + -0.4337822004148328, + 0.509196394393858, + 0.9548882441962462 + ], + "rotation_deg": [ + 16.024014595818624, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1513, + "type": "imguizmo", + "translation": [ + -0.4571658773099938, + 0.48437503991075975, + 0.9819581854150184 + ], + "rotation_deg": [ + 15.247118218997347, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1514, + "type": "imguizmo", + "translation": [ + -0.4793670117159167, + 0.4589502144442291, + 1.0143490572299183 + ], + "rotation_deg": [ + 14.45122583150668, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1515, + "type": "imguizmo", + "translation": [ + -0.5003579437837052, + 0.43295359416079754, + 1.0518994899468739 + ], + "rotation_deg": [ + 13.637329016153776, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1516, + "type": "imguizmo", + "translation": [ + -0.5201125214258765, + 0.40641756761213643, + 1.0944224091989336 + ], + "rotation_deg": [ + 12.806441787020917, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1517, + "type": "imguizmo", + "translation": [ + -0.5386061328986069, + 0.3793751953829538, + 1.141705967941352 + ], + "rotation_deg": [ + 11.959599326130222, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1518, + "type": "imguizmo", + "translation": [ + -0.5558157374648987, + 0.35186016890162425, + 1.193514601862909 + ], + "rotation_deg": [ + 11.09785669373579, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1519, + "type": "imguizmo", + "translation": [ + -0.5717198941004705, + 0.32390676846486366, + 1.2495902029554613 + ], + "rotation_deg": [ + 10.222287513849992, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1520, + "type": "imguizmo", + "translation": [ + -0.5862987882066028, + 0.2955498205287481, + 1.3096534053950537 + ], + "rotation_deg": [ + 9.33398263664162, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1521, + "type": "imguizmo", + "translation": [ + -0.5995342562966611, + 0.26682465431928676, + 1.3734049773284036 + ], + "rotation_deg": [ + 8.434048779372437, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1522, + "type": "imguizmo", + "translation": [ + -0.611409808625538, + 0.2377670578166053, + 1.44052731163094 + ], + "rotation_deg": [ + 7.523607147565284, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1523, + "type": "imguizmo", + "translation": [ + -0.6219106497338223, + 0.20841323316757898, + 1.5106860082095237 + ], + "rotation_deg": [ + 6.60379203812159, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1524, + "type": "imguizmo", + "translation": [ + -0.6310236968810998, + 0.17879975158246672, + 1.583531539966889 + ], + "rotation_deg": [ + 5.675749426128722, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1525, + "type": "imguizmo", + "translation": [ + -0.6387375963454169, + 0.14896350777173473, + 1.6587009941280644 + ], + "rotation_deg": [ + 4.74063553711766, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1526, + "type": "imguizmo", + "translation": [ + -0.6450427375686073, + 0.11894167397984451, + 1.7358198802535556 + ], + "rotation_deg": [ + 3.799615406550042, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1527, + "type": "imguizmo", + "translation": [ + -0.6499312651298472, + 0.08877165367326328, + 1.8145039959318838 + ], + "rotation_deg": [ + 2.8538614283290062, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1528, + "type": "imguizmo", + "translation": [ + -0.6533970885325346, + 0.05849103494040307, + 1.894361340856669 + ], + "rotation_deg": [ + 1.9045518941424011, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1529, + "type": "imguizmo", + "translation": [ + -0.655435889792288, + 0.02813754366154179, + 1.9749940697524457 + ], + "rotation_deg": [ + 0.9528695254580194, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1530, + "type": "imguizmo", + "translation": [ + -0.6560451288166165, + -0.0022510034929234743, + 2.0560004744198026 + ], + "rotation_deg": [ + 3.3306690738754696E-16, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1531, + "type": "imguizmo", + "translation": [ + -0.6552240465695613, + -0.03263674617734043, + 2.136976985025433 + ], + "rotation_deg": [ + -0.9528695254580187, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1532, + "type": "imguizmo", + "translation": [ + -0.6529736660173575, + -0.06298182754007718, + 2.217520180666742 + ], + "rotation_deg": [ + -1.9045518941424004, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1533, + "type": "imguizmo", + "translation": [ + -0.6492967908539472, + -0.09324844138844533, + 2.29722879919449 + ], + "rotation_deg": [ + -2.853861428329018, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1534, + "type": "imguizmo", + "translation": [ + -0.6441980020079249, + -0.12339887929050834, + 2.375705736280638 + ], + "rotation_deg": [ + -3.7996154065500534, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1535, + "type": "imguizmo", + "translation": [ + -0.637683651935271, + -0.1533955775550912, + 2.45256002377213 + ], + "rotation_deg": [ + -4.740635537117647, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1536, + "type": "imguizmo", + "translation": [ + -0.6297618567049839, + -0.18320116403146564, + 2.5274087774745744 + ], + "rotation_deg": [ + -5.675749426128711, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1537, + "type": "imguizmo", + "translation": [ + -0.620442485887467, + -0.21277850467039044, + 2.599879104662032 + ], + "rotation_deg": [ + -6.6037920381215915, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1538, + "type": "imguizmo", + "translation": [ + -0.6097371502582737, + -0.2420907497885172, + 2.669609961809822 + ], + "rotation_deg": [ + -7.523607147565285, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1539, + "type": "imguizmo", + "translation": [ + -0.5976591873325274, + -0.27110137997850553, + 2.736253953295204 + ], + "rotation_deg": [ + -8.43404877937244, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1540, + "type": "imguizmo", + "translation": [ + -0.5842236447480383, + -0.299774251607657, + 2.7994790621048904 + ], + "rotation_deg": [ + -9.333982636641622, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1541, + "type": "imguizmo", + "translation": [ + -0.5694472615178193, + -0.3280736418483813, + 2.858970303927112 + ], + "rotation_deg": [ + -10.222287513849993, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1542, + "type": "imguizmo", + "translation": [ + -0.5533484471753617, + -0.3559642931843903, + 2.9144312963876113 + ], + "rotation_deg": [ + -11.097856693735793, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1543, + "type": "imguizmo", + "translation": [ + -0.5359472588386486, + -0.3834114573371734, + 2.965585735611721 + ], + "rotation_deg": [ + -11.959599326130213, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1544, + "type": "imguizmo", + "translation": [ + -0.5172653762214814, + -0.41038093855802643, + 3.012178772756376 + ], + "rotation_deg": [ + -12.806441787020908, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1545, + "type": "imguizmo", + "translation": [ + -0.49732607462326034, + -0.4368391362316941, + 3.0539782836542195 + ], + "rotation_deg": [ + -13.637329016153778, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1546, + "type": "imguizmo", + "translation": [ + -0.4761541959308557, + -0.4627530867385548, + 3.0907760252445398 + ], + "rotation_deg": [ + -14.451225831506685, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1547, + "type": "imguizmo", + "translation": [ + -0.4537761176687122, + -0.4880905045231844, + 3.1223886730297163 + ], + "rotation_deg": [ + -15.247118218997349, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1548, + "type": "imguizmo", + "translation": [ + -0.4302197201357375, + -0.5128198223181389, + 3.1486587343886456 + ], + "rotation_deg": [ + -16.024014595818628, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1549, + "type": "imguizmo", + "translation": [ + -0.40551435166992184, + -0.5369102304728384, + 3.169455333197065 + ], + "rotation_deg": [ + -16.78094704582721, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1550, + "type": "imguizmo", + "translation": [ + -0.37969079208396367, + -0.5603317153385546, + 3.184674861845809 + ], + "rotation_deg": [ + -17.516972525446615, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1551, + "type": "imguizmo", + "translation": [ + -0.35278121431745607, + -0.5830550966616804, + 3.194241497408717 + ], + "rotation_deg": [ + -18.231174038582015, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1552, + "type": "imguizmo", + "translation": [ + -0.324819144353407, + -0.6050520639386942, + 3.1981075793886284 + ], + "rotation_deg": [ + -18.922661779083285, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1553, + "type": "imguizmo", + "translation": [ + -0.2958394194490453, + -0.6262952116875171, + 3.1962538471595927 + ], + "rotation_deg": [ + -19.590574239332533, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1554, + "type": "imguizmo", + "translation": [ + -0.2658781447329314, + -0.646758073591335, + 3.188689535922327 + ], + "rotation_deg": [ + -20.23407928357538, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1555, + "type": "imguizmo", + "translation": [ + -0.23497264822246525, + -0.6664151554723325, + 3.1754523306949185 + ], + "rotation_deg": [ + -20.85237518465851, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1556, + "type": "imguizmo", + "translation": [ + -0.20316143431782474, + -0.6852419670542631, + 3.1566081785679603 + ], + "rotation_deg": [ + -21.444691622881802, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1557, + "type": "imguizmo", + "translation": [ + -0.17048413583027625, + -0.703215052474285, + 3.132250960159458 + ], + "rotation_deg": [ + -22.010290645720836, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1558, + "type": "imguizmo", + "translation": [ + -0.13698146460462893, + -0.7203120195060444, + 3.102502021906318 + ], + "rotation_deg": [ + -22.548467587223772, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1559, + "type": "imguizmo", + "translation": [ + -0.10269516079734561, + -0.7365115674576013, + 3.0675095715225003 + ], + "rotation_deg": [ + -23.058551945937413, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1560, + "type": "imguizmo", + "translation": [ + -0.06766794087350216, + -0.751793513709441, + 3.027447939635644 + ], + "rotation_deg": [ + -23.539908220268547, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1561, + "type": "imguizmo", + "translation": [ + -0.031943444387396845, + -0.7661388188595013, + 2.9825167112806463 + ], + "rotation_deg": [ + -23.99193670023973, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1562, + "type": "imguizmo", + "translation": [ + 0.004433820386904652, + -0.7795296104438997, + 2.9329397315770644 + ], + "rotation_deg": [ + -24.4140742146533, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1563, + "type": "imguizmo", + "translation": [ + 0.04141853190733425, + -0.7919492052037964, + 2.878963990543982 + ], + "rotation_deg": [ + -24.80579483273252, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1564, + "type": "imguizmo", + "translation": [ + 0.07896461182882031, + -0.8033821298706573, + 2.820858392608165 + ], + "rotation_deg": [ + -25.16661051936587, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1565, + "type": "imguizmo", + "translation": [ + 0.11702528241116694, + -0.813814140444021, + 2.7589124169357295 + ], + "rotation_deg": [ + -25.49607174313801, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1566, + "type": "imguizmo", + "translation": [ + 0.1555531247982938, + -0.8232322399377476, + 2.693434675261497 + ], + "rotation_deg": [ + -25.79376803638992, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1567, + "type": "imguizmo", + "translation": [ + 0.19450013809622493, + -0.8316246945726475, + 2.6247513744008604 + ], + "rotation_deg": [ + -26.05932850661054, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1568, + "type": "imguizmo", + "translation": [ + 0.23381779917622947, + -0.8389810483953073, + 2.5532046911038293 + ], + "rotation_deg": [ + -26.29242229852261, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1569, + "type": "imguizmo", + "translation": [ + 0.2734571231285912, + -0.8452921363049061, + 2.4791510673476833 + ], + "rotation_deg": [ + -26.492759006287177, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1570, + "type": "imguizmo", + "translation": [ + 0.313368724291711, + -0.8505500954717902, + 2.402959434560966 + ], + "rotation_deg": [ + -26.66008903531307, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1571, + "type": "imguizmo", + "translation": [ + 0.35350287778049017, + -0.8547483751335794, + 2.3250093756256645 + ], + "rotation_deg": [ + -26.794203913220738, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1572, + "type": "imguizmo", + "translation": [ + 0.3938095814373452, + -0.8578817447566016, + 2.245689233814388 + ], + "rotation_deg": [ + -26.89493654957281, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1573, + "type": "imguizmo", + "translation": [ + 0.43423861812866993, + -0.8599463005524862, + 2.1653941780837145 + ], + "rotation_deg": [ + -26.96216144404802, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1574, + "type": "imguizmo", + "translation": [ + 0.4747396183091305, + -0.8609394703417979, + 2.0845242343623323 + ], + "rotation_deg": [ + -26.995794842798965, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1575, + "type": "imguizmo", + "translation": [ + 0.5152621227758457, + -0.860860016758653, + 2.003482292641981 + ], + "rotation_deg": [ + -26.99579484279897, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1576, + "type": "imguizmo", + "translation": [ + 0.555755645534275, + -0.859708038792322, + 1.9226720997997726 + ], + "rotation_deg": [ + -26.96216144404802, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1577, + "type": "imguizmo", + "translation": [ + 0.5961697366974738, + -0.8574849716639021, + 1.8424962481515743 + ], + "rotation_deg": [ + -26.89493654957281, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1578, + "type": "imguizmo", + "translation": [ + 0.6364540453403752, + -0.8541935850382104, + 1.763354169757329 + ], + "rotation_deg": [ + -26.79420391322074, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1579, + "type": "imguizmo", + "translation": [ + 0.6765583822307718, + -0.8498379795731258, + 1.6856401464706297 + ], + "rotation_deg": [ + -26.660089035313074, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1580, + "type": "imguizmo", + "translation": [ + 0.7164327823588489, + -0.8444235818106807, + 1.6097413456463563 + ], + "rotation_deg": [ + -26.49275900628718, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1581, + "type": "imguizmo", + "translation": [ + 0.7560275671873647, + -0.8379571374162648, + 1.53603589129239 + ], + "rotation_deg": [ + -26.292422298522613, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1582, + "type": "imguizmo", + "translation": [ + 0.7952934065449225, + -0.8304467027743663, + 1.4648909802748251 + ], + "rotation_deg": [ + -26.05932850661054, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1583, + "type": "imguizmo", + "translation": [ + 0.8341813800852191, + -0.8219016349513202, + 1.3966610529616557 + ], + "rotation_deg": [ + -25.793768036389928, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1584, + "type": "imguizmo", + "translation": [ + 0.8726430382357101, + -0.8123325800375678, + 1.3316860274186817 + ], + "rotation_deg": [ + -25.496071743138017, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1585, + "type": "imguizmo", + "translation": [ + 0.9106304625597363, + -0.8017514598839556, + 1.2702896059548179 + ], + "rotation_deg": [ + -25.16661051936588, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1586, + "type": "imguizmo", + "translation": [ + 0.9480963254569347, + -0.7901714572485918, + 1.2127776624534636 + ], + "rotation_deg": [ + -24.805794832732527, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1587, + "type": "imguizmo", + "translation": [ + 0.9849939491275326, + -0.7776069993727726, + 1.1594367185242034 + ], + "rotation_deg": [ + -24.4140742146533, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1588, + "type": "imguizmo", + "translation": [ + 1.0212773637270747, + -0.7640737400064349, + 1.110532516066562 + ], + "rotation_deg": [ + -23.99193670023973, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1589, + "type": "imguizmo", + "translation": [ + 1.056901364639124, + -0.7495885399055345, + 1.0663086933572274 + ], + "rotation_deg": [ + -23.539908220268547, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1590, + "type": "imguizmo", + "translation": [ + 1.0918215687945865, + -0.7341694458256421, + 1.0269855712564118 + ], + "rotation_deg": [ + -23.058551945937413, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1591, + "type": "imguizmo", + "translation": [ + 1.1259944699674846, + -0.7178356680379359, + 0.9927590555804089 + ], + "rotation_deg": [ + -22.548467587223776, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1592, + "type": "imguizmo", + "translation": [ + 1.1593774929783038, + -0.7006075563955912, + 0.9637996611086689 + ], + "rotation_deg": [ + -22.01029064572084, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1593, + "type": "imguizmo", + "translation": [ + 1.191929046737358, + -0.6825065749804025, + 0.9402516620877491 + ], + "rotation_deg": [ + -21.44469162288181, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1594, + "type": "imguizmo", + "translation": [ + 1.2236085760621118, + -0.6635552753612074, + 0.9222323734642716 + ], + "rotation_deg": [ + -20.852375184658513, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1595, + "type": "imguizmo", + "translation": [ + 1.2543766122038864, + -0.6437772684974403, + 0.9098315664277605 + ], + "rotation_deg": [ + -20.234079283575394, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1596, + "type": "imguizmo", + "translation": [ + 1.284194822021007, + -0.6231971953228179, + 0.9031110211750922 + ], + "rotation_deg": [ + -19.590574239332543, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1597, + "type": "imguizmo", + "translation": [ + 1.3130260557371232, + -0.6018406960458037, + 0.9021042191246622 + ], + "rotation_deg": [ + -18.922661779083295, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1598, + "type": "imguizmo", + "translation": [ + 1.3408343932252036, + -0.5797343782051012, + 0.9068161761136418 + ], + "rotation_deg": [ + -18.23117403858203, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1599, + "type": "imguizmo", + "translation": [ + 1.3675851887595423, + -0.5569057835199734, + 0.9172234174093304 + ], + "rotation_deg": [ + -17.516972525446608, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1600, + "type": "imguizmo", + "translation": [ + 1.3932451141800186, + -0.5333833535766914, + 0.9332740946590933 + ], + "rotation_deg": [ + -16.780947045827208, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1601, + "type": "imguizmo", + "translation": [ + 1.417782200414834, + -0.5091963943938584, + 0.9548882441962453 + ], + "rotation_deg": [ + -16.02401459581862, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1602, + "type": "imguizmo", + "translation": [ + 1.4411658773099951, + -0.4843750399107602, + 0.9819581854150177 + ], + "rotation_deg": [ + -15.247118218997342, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1603, + "type": "imguizmo", + "translation": [ + 1.4633670117159179, + -0.45895021444422957, + 1.0143490572299174 + ], + "rotation_deg": [ + -14.451225831506678, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1604, + "type": "imguizmo", + "translation": [ + 1.4843579437837064, + -0.43295359416079804, + 1.0518994899468734 + ], + "rotation_deg": [ + -13.637329016153773, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1605, + "type": "imguizmo", + "translation": [ + 1.5041125214258777, + -0.4064175676121365, + 1.0944224091989334 + ], + "rotation_deg": [ + -12.806441787020901, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1606, + "type": "imguizmo", + "translation": [ + 1.5226061328986082, + -0.3793751953829538, + 1.1417059679413517 + ], + "rotation_deg": [ + -11.959599326130203, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1607, + "type": "imguizmo", + "translation": [ + 1.5398157374648995, + -0.35186016890162497, + 1.1935146018629077 + ], + "rotation_deg": [ + -11.097856693735796, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1608, + "type": "imguizmo", + "translation": [ + 1.5557198941004713, + -0.3239067684648643, + 1.2495902029554598 + ], + "rotation_deg": [ + -10.222287513849997, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1609, + "type": "imguizmo", + "translation": [ + 1.5702987882066035, + -0.29554982052874884, + 1.3096534053950526 + ], + "rotation_deg": [ + -9.333982636641625, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1610, + "type": "imguizmo", + "translation": [ + 1.5835342562966621, + -0.2668246543192875, + 1.3734049773284023 + ], + "rotation_deg": [ + -8.434048779372445, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1611, + "type": "imguizmo", + "translation": [ + 1.5954098086255388, + -0.237767057816606, + 1.4405273116309387 + ], + "rotation_deg": [ + -7.52360714756529, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1612, + "type": "imguizmo", + "translation": [ + 1.6059106497338234, + -0.2084132331675797, + 1.5106860082095224 + ], + "rotation_deg": [ + -6.603792038121595, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1613, + "type": "imguizmo", + "translation": [ + 1.6150236968811007, + -0.17879975158246703, + 1.583531539966889 + ], + "rotation_deg": [ + -5.675749426128716, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1614, + "type": "imguizmo", + "translation": [ + 1.622737596345418, + -0.14896350777173506, + 1.658700994128064 + ], + "rotation_deg": [ + -4.740635537117653, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1615, + "type": "imguizmo", + "translation": [ + 1.6290427375686083, + -0.11894167397984569, + 1.7358198802535532 + ], + "rotation_deg": [ + -3.7996154065500596, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1616, + "type": "imguizmo", + "translation": [ + 1.6339312651298483, + -0.08877165367326437, + 1.8145039959318814 + ], + "rotation_deg": [ + -2.853861428329023, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1617, + "type": "imguizmo", + "translation": [ + 1.6373970885325355, + -0.05849103494040378, + 1.8943613408566682 + ], + "rotation_deg": [ + -1.9045518941424064, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1618, + "type": "imguizmo", + "translation": [ + 1.6394358897922887, + -0.028137543661542527, + 1.9749940697524448 + ], + "rotation_deg": [ + -0.9528695254580246, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1619, + "type": "imguizmo", + "translation": [ + 1.6400451288166176, + 0.0022510034929227526, + 2.0560004744198013 + ], + "rotation_deg": [ + -5.551115123125783E-15, + 140.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1620, + "type": "action", + "action": "set_active_render_window", + "value": 0 + }, + { + "frame": 1620, + "type": "action", + "action": "set_active_planar", + "value": 9 + }, + { + "frame": 1620, + "type": "action", + "action": "set_projection_type", + "value": "perspective" + }, + { + "frame": 1621, + "type": "imguizmo", + "translation": [ + 0.0, + 1.0395, + 0.0 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1622, + "type": "imguizmo", + "translation": [ + 0.006339250378967094, + 1.0395, + 0.019797816939610205 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1623, + "type": "imguizmo", + "translation": [ + 0.012163462811033164, + 1.0395, + 0.03791317679591889 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1624, + "type": "imguizmo", + "translation": [ + 0.01801309228550459, + 1.0395, + 0.05596636114093453 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1625, + "type": "imguizmo", + "translation": [ + 0.02383703399385577, + 1.0395, + 0.07373058740766639 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1626, + "type": "imguizmo", + "translation": [ + 0.029631537381827902, + 1.0395, + 0.09112830251527632 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1627, + "type": "imguizmo", + "translation": [ + 0.035389102791602914, + 1.0395, + 0.1080719561301614 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1628, + "type": "imguizmo", + "translation": [ + 0.04110257944788714, + 1.0395, + 0.12447720588566943 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1629, + "type": "imguizmo", + "translation": [ + 0.04676484727549138, + 1.0395, + 0.1402623160476584 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1630, + "type": "imguizmo", + "translation": [ + 0.05236885193736966, + 1.0395, + 0.15534864643972185 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1631, + "type": "imguizmo", + "translation": [ + 0.0579076115300598, + 1.0395, + 0.16966103768683968 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1632, + "type": "imguizmo", + "translation": [ + 0.06337422544973643, + 1.0395, + 0.18312818617862361 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1633, + "type": "imguizmo", + "translation": [ + 0.06876188297610654, + 1.0395, + 0.19568299925847615 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1634, + "type": "imguizmo", + "translation": [ + 0.07406387175879058, + 1.0395, + 0.20726292947917818 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1635, + "type": "imguizmo", + "translation": [ + 0.07927358617998946, + 1.0395, + 0.21781028621085674 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1636, + "type": "imguizmo", + "translation": [ + 0.08438453558426869, + 1.0395, + 0.227272523052823 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1637, + "type": "imguizmo", + "translation": [ + 0.08939035236510615, + 1.0395, + 0.2356024996170952 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1638, + "type": "imguizmo", + "translation": [ + 0.09428479989813704, + 1.0395, + 0.24275871637944063 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1639, + "type": "imguizmo", + "translation": [ + 0.09906178031121114, + 1.0395, + 0.24870552142791796 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1640, + "type": "imguizmo", + "translation": [ + 0.10371534208158233, + 1.0395, + 0.2534132880789091 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1641, + "type": "imguizmo", + "translation": [ + 0.1082396874507646, + 1.0395, + 0.25685856247576666 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1642, + "type": "imguizmo", + "translation": [ + 0.11262917964781731, + 1.0395, + 0.2590241804347479 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1643, + "type": "imguizmo", + "translation": [ + 0.11687834991205992, + 1.0395, + 0.2598993529561153 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1644, + "type": "imguizmo", + "translation": [ + 0.12098190430646669, + 1.0395, + 0.25947971997438984 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1645, + "type": "imguizmo", + "translation": [ + 0.12493473031325295, + 1.0395, + 0.25776737207997824 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1646, + "type": "imguizmo", + "translation": [ + 0.12873190320343583, + 1.0395, + 0.25477084010395684 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1647, + "type": "imguizmo", + "translation": [ + 0.1323686921724331, + 1.0395, + 0.25050505261789996 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1648, + "type": "imguizmo", + "translation": [ + 0.13584056623405685, + 1.0395, + 0.24499126156048748 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1649, + "type": "imguizmo", + "translation": [ + 0.13914319986555768, + 1.0395, + 0.23825693636141546 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1650, + "type": "imguizmo", + "translation": [ + 0.14227247839668783, + 1.0395, + 0.23033562709007863 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1651, + "type": "imguizmo", + "translation": [ + 0.14522450313606755, + 1.0395, + 0.22126679731081303 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1652, + "type": "imguizmo", + "translation": [ + 0.14799559622846944, + 1.0395, + 0.21109562747740282 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1653, + "type": "imguizmo", + "translation": [ + 0.1505823052369679, + 1.0395, + 0.1998727898463328 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1654, + "type": "imguizmo", + "translation": [ + 0.1529814074442455, + 1.0395, + 0.18765419603015496 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1655, + "type": "imguizmo", + "translation": [ + 0.15518991386769762, + 1.0395, + 0.17450071844864767 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1656, + "type": "imguizmo", + "translation": [ + 0.15720507298333272, + 1.0395, + 0.16047788706548186 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1657, + "type": "imguizmo", + "translation": [ + 0.15902437415382825, + 1.0395, + 0.14565556292123882 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1658, + "type": "imguizmo", + "translation": [ + 0.16064555075647236, + 1.0395, + 0.13010759008921935 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1659, + "type": "imguizmo", + "translation": [ + 0.16206658300709345, + 1.0395, + 0.11391142778798606 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1660, + "type": "imguizmo", + "translation": [ + 0.16328570047645988, + 1.0395, + 0.09714776448343171 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1661, + "type": "imguizmo", + "translation": [ + 0.164301384296014, + 1.0395, + 0.0799001159029019 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1662, + "type": "imguizmo", + "translation": [ + 0.16511236905019375, + 1.0395, + 0.06225440896404172 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1663, + "type": "imguizmo", + "translation": [ + 0.16571764435298242, + 1.0395, + 0.04429855369121467 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1664, + "type": "imguizmo", + "translation": [ + 0.16611645610672401, + 1.0395, + 0.026122005252182785 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1665, + "type": "imguizmo", + "translation": [ + 0.16630830744163497, + 1.0395, + 0.007815318296961419 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1666, + "type": "imguizmo", + "translation": [ + 0.1662929593348421, + 1.0395, + -0.01053030418089309 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1667, + "type": "imguizmo", + "translation": [ + 0.16607043090817525, + 1.0395, + -0.028823465213008756 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1668, + "type": "imguizmo", + "translation": [ + 0.1656409994043439, + 1.0395, + -0.04697302919129732 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1669, + "type": "imguizmo", + "translation": [ + 0.16500519984152734, + 1.0395, + -0.06488857590097022 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1670, + "type": "imguizmo", + "translation": [ + 0.1641638243468086, + 1.0395, + -0.08248085098949896 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1671, + "type": "imguizmo", + "translation": [ + 0.16311792116928261, + 1.0395, + -0.09966221062731084 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1672, + "type": "imguizmo", + "translation": [ + 0.1618687933740686, + 1.0395, + -0.11634705814493879 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1673, + "type": "imguizmo", + "translation": [ + 0.1604179972188531, + 1.0395, + -0.1324522704713235 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1674, + "type": "imguizmo", + "translation": [ + 0.1587673402149866, + 1.0395, + -0.14789761224877163 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1675, + "type": "imguizmo", + "translation": [ + 0.15691887887554956, + 1.0395, + -0.16260613556147221 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1676, + "type": "imguizmo", + "translation": [ + 0.1548749161531933, + 1.0395, + -0.17650456328614236 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1677, + "type": "imguizmo", + "translation": [ + 0.15263799857094745, + 1.0395, + -0.18952365415497036 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1678, + "type": "imguizmo", + "translation": [ + 0.15021091304956963, + 1.0395, + -0.20159854771212768 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1679, + "type": "imguizmo", + "translation": [ + 0.14759668343538926, + 1.0395, + -0.21266908744529797 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1680, + "type": "imguizmo", + "translation": [ + 0.14479856673297145, + 1.0395, + -0.22268012048239563 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1681, + "type": "imguizmo", + "translation": [ + 0.14182004904729473, + 1.0395, + -0.23158177236040042 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1682, + "type": "imguizmo", + "translation": [ + 0.13866484124049866, + 1.0395, + -0.23932969549742264 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1683, + "type": "imguizmo", + "translation": [ + 0.1353368743086116, + 1.0395, + -0.2458852901301259 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1684, + "type": "imguizmo", + "translation": [ + 0.13184029448401913, + 1.0395, + -0.2512158966158066 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1685, + "type": "imguizmo", + "translation": [ + 0.1281794580697746, + 1.0395, + -0.25529495814109077 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1686, + "type": "imguizmo", + "translation": [ + 0.12435892601218837, + 1.0395, + -0.25810215302664236 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1687, + "type": "imguizmo", + "translation": [ + 0.12038345821845643, + 1.0395, + -0.25962349596874784 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1688, + "type": "imguizmo", + "translation": [ + 0.11625800762640887, + 1.0395, + -0.2598514077133955 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1689, + "type": "imguizmo", + "translation": [ + 0.11198771403376584, + 1.0395, + -0.25878475281573776 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1690, + "type": "imguizmo", + "translation": [ + 0.10757789769458996, + 1.0395, + -0.25642884529682053 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1691, + "type": "imguizmo", + "translation": [ + 0.10303405269091205, + 1.0395, + -0.25279542216939704 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1692, + "type": "imguizmo", + "translation": [ + 0.09836184008778923, + 1.0395, + -0.2479025849647216 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1693, + "type": "imguizmo", + "translation": [ + 0.09356708088032245, + 1.0395, + -0.24177470955163066 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1694, + "type": "imguizmo", + "translation": [ + 0.08865574874142189, + 1.0395, + -0.2344423246971907 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1695, + "type": "imguizmo", + "translation": [ + 0.08363396257935415, + 1.0395, + -0.22594195997391642 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1696, + "type": "imguizmo", + "translation": [ + 0.07850797891434448, + 1.0395, + -0.21631596377127793 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1697, + "type": "imguizmo", + "translation": [ + 0.07328418408373165, + 1.0395, + -0.20561229231815425 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1698, + "type": "imguizmo", + "translation": [ + 0.067969086285387, + 1.0395, + -0.193884270767314 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1699, + "type": "imguizmo", + "translation": [ + 0.06256930746931033, + 1.0395, + -0.1811903275321859 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1700, + "type": "imguizmo", + "translation": [ + 0.057091575087504606, + 1.0395, + -0.16759370319944272 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1701, + "type": "imguizmo", + "translation": [ + 0.05154271371240866, + 1.0395, + -0.15316213546757926 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1702, + "type": "imguizmo", + "translation": [ + 0.045929636534329694, + 1.0395, + -0.1379675216811057 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1703, + "type": "imguizmo", + "translation": [ + 0.04025933674846901, + 1.0395, + -0.12208556064159394 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1704, + "type": "imguizmo", + "translation": [ + 0.03453887884227171, + 1.0395, + -0.10559537548005621 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1705, + "type": "imguizmo", + "translation": [ + 0.0287753897939547, + 1.0395, + -0.08857911946948532 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1706, + "type": "imguizmo", + "translation": [ + 0.022976050193179807, + 1.0395, + -0.07112156674138248 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1707, + "type": "imguizmo", + "translation": [ + 0.017148085294932898, + 1.0395, + -0.05330968994529752 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1708, + "type": "imguizmo", + "translation": [ + 0.011298756017756002, + 1.0395, + -0.035232226955464185 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1709, + "type": "imguizmo", + "translation": [ + 0.005435349897546697, + 1.0395, + -0.016979238783173305 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1710, + "type": "imguizmo", + "translation": [ + -0.00043482799180366745, + 1.0395, + 0.0013583391026536842 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1711, + "type": "imguizmo", + "translation": [ + -0.006304464139622748, + 1.0395, + 0.01968914981139778 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1712, + "type": "imguizmo", + "translation": [ + -0.012166245710180674, + 1.0395, + 0.03792187016617581 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1713, + "type": "imguizmo", + "translation": [ + -0.018012869653572766, + 1.0395, + 0.05596566567131397 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1714, + "type": "imguizmo", + "translation": [ + -0.02383705180441031, + 1.0395, + 0.07373064304523608 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1715, + "type": "imguizmo", + "translation": [ + -0.02963153595698339, + 1.0395, + 0.09112829806427036 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1716, + "type": "imguizmo", + "translation": [ + -0.03538910290559035, + 1.0395, + 0.10807195648624157 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1717, + "type": "imguizmo", + "translation": [ + -0.04110257943876802, + 1.0395, + 0.12447720585718276 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1718, + "type": "imguizmo", + "translation": [ + -0.0467648472762208, + 1.0395, + 0.14026231604993705 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1719, + "type": "imguizmo", + "translation": [ + -0.052368851937311243, + 1.0395, + 0.15534864643953938 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1720, + "type": "imguizmo", + "translation": [ + -0.057907611530064414, + 1.0395, + 0.16966103768685414 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1721, + "type": "imguizmo", + "translation": [ + -0.063374225449736, + 1.0395, + 0.18312818617862237 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1722, + "type": "imguizmo", + "translation": [ + -0.06876188297610651, + 1.0395, + 0.1956829992584762 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1723, + "type": "imguizmo", + "translation": [ + -0.07406387175879049, + 1.0395, + 0.207262929479178 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1724, + "type": "imguizmo", + "translation": [ + -0.07927358617998938, + 1.0395, + 0.21781028621085663 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1725, + "type": "imguizmo", + "translation": [ + -0.08438453558426863, + 1.0395, + 0.22727252305282283 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1726, + "type": "imguizmo", + "translation": [ + -0.0893903523651061, + 1.0395, + 0.23560249961709503 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1727, + "type": "imguizmo", + "translation": [ + -0.09428479989813698, + 1.0395, + 0.24275871637944055 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1728, + "type": "imguizmo", + "translation": [ + -0.09906178031121114, + 1.0395, + 0.24870552142791794 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1729, + "type": "imguizmo", + "translation": [ + -0.10371534208158233, + 1.0395, + 0.25341328807890906 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1730, + "type": "imguizmo", + "translation": [ + -0.1082396874507646, + 1.0395, + 0.2568585624757666 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1731, + "type": "imguizmo", + "translation": [ + -0.11262917964781721, + 1.0395, + 0.25902418043474795 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1732, + "type": "imguizmo", + "translation": [ + -0.11687834991205986, + 1.0395, + 0.2598993529561153 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1733, + "type": "imguizmo", + "translation": [ + -0.12098190430646662, + 1.0395, + 0.2594797199743899 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1734, + "type": "imguizmo", + "translation": [ + -0.1249347303132529, + 1.0395, + 0.25776737207997824 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1735, + "type": "imguizmo", + "translation": [ + -0.12873190320343583, + 1.0395, + 0.25477084010395684 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1736, + "type": "imguizmo", + "translation": [ + -0.1323686921724332, + 1.0395, + 0.2505050526178999 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1737, + "type": "imguizmo", + "translation": [ + -0.1358405662340569, + 1.0395, + 0.24499126156048742 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1738, + "type": "imguizmo", + "translation": [ + -0.13914319986555773, + 1.0395, + 0.23825693636141534 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1739, + "type": "imguizmo", + "translation": [ + -0.14227247839668777, + 1.0395, + 0.23033562709007874 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1740, + "type": "imguizmo", + "translation": [ + -0.14522450313606755, + 1.0395, + 0.22126679731081308 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1741, + "type": "imguizmo", + "translation": [ + -0.14799559622846944, + 1.0395, + 0.21109562747740285 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1742, + "type": "imguizmo", + "translation": [ + -0.1505823052369679, + 1.0395, + 0.1998727898463328 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1743, + "type": "imguizmo", + "translation": [ + -0.1529814074442455, + 1.0395, + 0.18765419603015498 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1744, + "type": "imguizmo", + "translation": [ + -0.15518991386769762, + 1.0395, + 0.1745007184486476 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1745, + "type": "imguizmo", + "translation": [ + -0.15720507298333275, + 1.0395, + 0.16047788706548174 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1746, + "type": "imguizmo", + "translation": [ + -0.15902437415382822, + 1.0395, + 0.1456555629212386 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1747, + "type": "imguizmo", + "translation": [ + -0.1606455507564723, + 1.0395, + 0.13010759008921954 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1748, + "type": "imguizmo", + "translation": [ + -0.1620665830070934, + 1.0395, + 0.11391142778798613 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1749, + "type": "imguizmo", + "translation": [ + -0.1632857004764598, + 1.0395, + 0.0971477644834318 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1750, + "type": "imguizmo", + "translation": [ + -0.16430138429601396, + 1.0395, + 0.07990011590290198 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1751, + "type": "imguizmo", + "translation": [ + -0.16511236905019372, + 1.0395, + 0.06225440896404179 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1752, + "type": "imguizmo", + "translation": [ + -0.16571764435298242, + 1.0395, + 0.04429855369121462 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1753, + "type": "imguizmo", + "translation": [ + -0.16611645610672401, + 1.0395, + 0.026122005252182733 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1754, + "type": "imguizmo", + "translation": [ + -0.16630830744163497, + 1.0395, + 0.007815318296961249 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1755, + "type": "imguizmo", + "translation": [ + -0.16629295933484212, + 1.0395, + -0.01053030418089275 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1756, + "type": "imguizmo", + "translation": [ + -0.16607043090817525, + 1.0395, + -0.028823465213008465 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1757, + "type": "imguizmo", + "translation": [ + -0.1656409994043439, + 1.0395, + -0.04697302919129727 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1758, + "type": "imguizmo", + "translation": [ + -0.16500519984152737, + 1.0395, + -0.06488857590097014 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1759, + "type": "imguizmo", + "translation": [ + -0.1641638243468086, + 1.0395, + -0.08248085098949888 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1760, + "type": "imguizmo", + "translation": [ + -0.16311792116928261, + 1.0395, + -0.09966221062731076 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1761, + "type": "imguizmo", + "translation": [ + -0.1618687933740686, + 1.0395, + -0.11634705814493881 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1762, + "type": "imguizmo", + "translation": [ + -0.1604179972188531, + 1.0395, + -0.13245227047132352 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1763, + "type": "imguizmo", + "translation": [ + -0.15876734021498665, + 1.0395, + -0.14789761224877124 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1764, + "type": "imguizmo", + "translation": [ + -0.15691887887554964, + 1.0395, + -0.16260613556147185 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1765, + "type": "imguizmo", + "translation": [ + -0.15487491615319332, + 1.0395, + -0.17650456328614222 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1766, + "type": "imguizmo", + "translation": [ + -0.15263799857094748, + 1.0395, + -0.18952365415497022 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1767, + "type": "imguizmo", + "translation": [ + -0.15021091304956966, + 1.0395, + -0.20159854771212746 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1768, + "type": "imguizmo", + "translation": [ + -0.14759668343538931, + 1.0395, + -0.21266908744529775 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1769, + "type": "imguizmo", + "translation": [ + -0.14479856673297145, + 1.0395, + -0.22268012048239563 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1770, + "type": "imguizmo", + "translation": [ + -0.14182004904729473, + 1.0395, + -0.2315817723604004 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1771, + "type": "imguizmo", + "translation": [ + -0.13866484124049874, + 1.0395, + -0.23932969549742236 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1772, + "type": "imguizmo", + "translation": [ + -0.1353368743086117, + 1.0395, + -0.24588529013012572 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1773, + "type": "imguizmo", + "translation": [ + -0.13184029448401913, + 1.0395, + -0.2512158966158065 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1774, + "type": "imguizmo", + "translation": [ + -0.12817945806977463, + 1.0395, + -0.2552949581410907 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1775, + "type": "imguizmo", + "translation": [ + -0.12435892601218838, + 1.0395, + -0.25810215302664236 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1776, + "type": "imguizmo", + "translation": [ + -0.12038345821845645, + 1.0395, + -0.25962349596874784 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1777, + "type": "imguizmo", + "translation": [ + -0.11625800762640885, + 1.0395, + -0.2598514077133955 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1778, + "type": "imguizmo", + "translation": [ + -0.11198771403376581, + 1.0395, + -0.25878475281573776 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1779, + "type": "imguizmo", + "translation": [ + -0.10757789769458992, + 1.0395, + -0.25642884529682053 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1780, + "type": "imguizmo", + "translation": [ + -0.10303405269091202, + 1.0395, + -0.25279542216939704 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1781, + "type": "imguizmo", + "translation": [ + -0.09836184008778914, + 1.0395, + -0.2479025849647216 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1782, + "type": "imguizmo", + "translation": [ + -0.09356708088032237, + 1.0395, + -0.2417747095516306 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1783, + "type": "imguizmo", + "translation": [ + -0.0886557487414218, + 1.0395, + -0.2344423246971906 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1784, + "type": "imguizmo", + "translation": [ + -0.08363396257935407, + 1.0395, + -0.22594195997391628 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1785, + "type": "imguizmo", + "translation": [ + -0.07850797891434433, + 1.0395, + -0.21631596377127768 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1786, + "type": "imguizmo", + "translation": [ + -0.07328418408373147, + 1.0395, + -0.205612292318154 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1787, + "type": "imguizmo", + "translation": [ + -0.06796908628538698, + 1.0395, + -0.193884270767314 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1788, + "type": "imguizmo", + "translation": [ + -0.06256930746931029, + 1.0395, + -0.18119032753218592 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1789, + "type": "imguizmo", + "translation": [ + -0.05709157508750457, + 1.0395, + -0.16759370319944278 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1790, + "type": "imguizmo", + "translation": [ + -0.051542713712408615, + 1.0395, + -0.1531621354675793 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1791, + "type": "imguizmo", + "translation": [ + -0.04592963653432965, + 1.0395, + -0.13796752168110576 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1792, + "type": "imguizmo", + "translation": [ + -0.04025933674846897, + 1.0395, + -0.12208556064159401 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1793, + "type": "imguizmo", + "translation": [ + -0.03453887884227159, + 1.0395, + -0.1055953754800561 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1794, + "type": "imguizmo", + "translation": [ + -0.028775389793954588, + 1.0395, + -0.08857911946948518 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1795, + "type": "imguizmo", + "translation": [ + -0.022976050193179855, + 1.0395, + -0.07112156674138281 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1796, + "type": "imguizmo", + "translation": [ + -0.017148085294932922, + 1.0395, + -0.05330968994529783 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1797, + "type": "imguizmo", + "translation": [ + -0.011298756017755951, + 1.0395, + -0.035232226955464255 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1798, + "type": "imguizmo", + "translation": [ + -0.005435349897546653, + 1.0395, + -0.01697923878317339 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1799, + "type": "imguizmo", + "translation": [ + 0.0004348279918037117, + 1.0395, + 0.001358339102653594 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1800, + "type": "action", + "action": "set_active_render_window", + "value": 0 + }, + { + "frame": 1800, + "type": "action", + "action": "set_active_planar", + "value": 10 + }, + { + "frame": 1800, + "type": "action", + "action": "set_projection_type", + "value": "perspective" + }, + { + "frame": 1801, + "type": "imguizmo", + "translation": [ + 0.0, + 0.0792, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1802, + "type": "imguizmo", + "translation": [ + 0.002113083459655698, + 0.07916270147834842, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1803, + "type": "imguizmo", + "translation": [ + 0.004054487603677721, + 0.07905383626443915, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1804, + "type": "imguizmo", + "translation": [ + 0.006004364095168197, + 0.078876285161966, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1805, + "type": "imguizmo", + "translation": [ + 0.007945677997951922, + 0.0786300497637978, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1806, + "type": "imguizmo", + "translation": [ + 0.009877179127275969, + 0.07831545441767407, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1807, + "type": "imguizmo", + "translation": [ + 0.011796367597200971, + 0.07793288966469379, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1808, + "type": "imguizmo", + "translation": [ + 0.013700859815962379, + 0.07748283224534291, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1809, + "type": "imguizmo", + "translation": [ + 0.015588282425163793, + 0.07696584286612763, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1810, + "type": "imguizmo", + "translation": [ + 0.017456283979123223, + 0.07638256563215652, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1811, + "type": "imguizmo", + "translation": [ + 0.019302537176686597, + 0.07573372723417356, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1812, + "type": "imguizmo", + "translation": [ + 0.02112474181657881, + 0.07502013604403128, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1813, + "type": "imguizmo", + "translation": [ + 0.02292062765870218, + 0.07424268110749309, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1814, + "type": "imguizmo", + "translation": [ + 0.02468795725293019, + 0.07340233103659814, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1815, + "type": "imguizmo", + "translation": [ + 0.026424528726663156, + 0.07250013280289105, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1816, + "type": "imguizmo", + "translation": [ + 0.028128178528089562, + 0.07153721043302574, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1817, + "type": "imguizmo", + "translation": [ + 0.029796784121702048, + 0.07051476360836817, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1818, + "type": "imguizmo", + "translation": [ + 0.031428266632712344, + 0.06943406617034271, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1819, + "type": "imguizmo", + "translation": [ + 0.03302059343707038, + 0.06829646453338442, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1820, + "type": "imguizmo", + "translation": [ + 0.03457178069386077, + 0.06710337600747432, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1821, + "type": "imguizmo", + "translation": [ + 0.036079895816921524, + 0.06585628703234757, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1822, + "type": "imguizmo", + "translation": [ + 0.03754305988260577, + 0.06455675132557472, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1823, + "type": "imguizmo", + "translation": [ + 0.03895944997068664, + 0.06320638794682307, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1824, + "type": "imguizmo", + "translation": [ + 0.0403273014354889, + 0.06180687928071, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1825, + "type": "imguizmo", + "translation": [ + 0.041644910104417655, + 0.0603599689407611, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1826, + "type": "imguizmo", + "translation": [ + 0.04291063440114529, + 0.05886745959708494, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1827, + "type": "imguizmo", + "translation": [ + 0.044122897390811044, + 0.05733121073047059, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1828, + "type": "imguizmo", + "translation": [ + 0.04528018874468562, + 0.05575313631570605, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1829, + "type": "imguizmo", + "translation": [ + 0.04638106662185256, + 0.05413520243700407, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1830, + "type": "imguizmo", + "translation": [ + 0.04742415946556261, + 0.052479424838505956, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1831, + "type": "imguizmo", + "translation": [ + 0.048408167712022526, + 0.05078786641291548, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1832, + "type": "imguizmo", + "translation": [ + 0.04933186540948983, + 0.049062634631391364, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1833, + "type": "imguizmo", + "translation": [ + 0.050194101745655965, + 0.047305878917900716, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1834, + "type": "imguizmo", + "translation": [ + 0.05099380248141516, + 0.045519787971304355, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1835, + "type": "imguizmo", + "translation": [ + 0.05172997128923255, + 0.04370658703851066, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1836, + "type": "imguizmo", + "translation": [ + 0.05240169099444424, + 0.04186853514209489, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1837, + "type": "imguizmo", + "translation": [ + 0.05300812471794276, + 0.04000792226583853, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1838, + "type": "imguizmo", + "translation": [ + 0.05354851691882413, + 0.03812706650169449, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1839, + "type": "imguizmo", + "translation": [ + 0.05402219433569782, + 0.03622831116173331, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1840, + "type": "imguizmo", + "translation": [ + 0.05442856682548663, + 0.03431402185866804, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1841, + "type": "imguizmo", + "translation": [ + 0.05476712809867136, + 0.03238658355859542, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1842, + "type": "imguizmo", + "translation": [ + 0.055037456350064594, + 0.030448397609625116, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1843, + "type": "imguizmo", + "translation": [ + 0.05523921478432749, + 0.02850187875009896, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1844, + "type": "imguizmo", + "translation": [ + 0.0553721520355747, + 0.026549452100127668, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1845, + "type": "imguizmo", + "translation": [ + 0.05543610248054502, + 0.024593550140193245, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1846, + "type": "imguizmo", + "translation": [ + 0.05543098644494739, + 0.022636609680581087, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1847, + "type": "imguizmo", + "translation": [ + 0.0553568103027251, + 0.020681068825417915, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1848, + "type": "imguizmo", + "translation": [ + 0.05521366646811466, + 0.018729363935097613, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1849, + "type": "imguizmo", + "translation": [ + 0.055001733280509134, + 0.01678392659087944, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1850, + "type": "imguizmo", + "translation": [ + 0.054721274782269555, + 0.014847180565440777, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1851, + "type": "imguizmo", + "translation": [ + 0.05437264038976089, + 0.012921538803158033, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1852, + "type": "imguizmo", + "translation": [ + 0.05395626445802288, + 0.011009400413878496, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1853, + "type": "imguizmo", + "translation": [ + 0.05347266573961772, + 0.009113147683928144, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1854, + "type": "imguizmo", + "translation": [ + 0.052922446738328875, + 0.007235143108079623, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1855, + "type": "imguizmo", + "translation": [ + 0.052306292958516534, + 0.0053777264461778735, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1856, + "type": "imguizmo", + "translation": [ + 0.05162497205106445, + 0.003543211808090775, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1857, + "type": "imguizmo", + "translation": [ + 0.05087933285698249, + 0.0017338847706163177, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1858, + "type": "imguizmo", + "translation": [ + 0.05007030434985655, + -4.800047006135017E-05, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1859, + "type": "imguizmo", + "translation": [ + 0.049198894478463095, + -0.0018002239068197257, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1860, + "type": "imguizmo", + "translation": [ + 0.04826618891099049, + -0.0035206024874503846, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1861, + "type": "imguizmo", + "translation": [ + 0.047273349682431576, + -0.0052069928344702626, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1862, + "type": "imguizmo", + "translation": [ + 0.04622161374683289, + -0.006857293915502945, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1863, + "type": "imguizmo", + "translation": [ + 0.04511229143620387, + -0.00846944966090349, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1864, + "type": "imguizmo", + "translation": [ + 0.04394676482800637, + -0.010041451525365102, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1865, + "type": "imguizmo", + "translation": [ + 0.042726486023258195, + -0.011571340990316627, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1866, + "type": "imguizmo", + "translation": [ + 0.04145297533739612, + -0.013057212003992567, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1867, + "type": "imguizmo", + "translation": [ + 0.04012781940615214, + -0.014497213356136438, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1868, + "type": "imguizmo", + "translation": [ + 0.03875266920880295, + -0.015889550984378116, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1869, + "type": "imguizmo", + "translation": [ + 0.03732923801125527, + -0.01723249020941225, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1870, + "type": "imguizmo", + "translation": [ + 0.03585929923152997, + -0.018524357896192518, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1871, + "type": "imguizmo", + "translation": [ + 0.034344684230304005, + -0.019763544538449664, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1872, + "type": "imguizmo", + "translation": [ + 0.03278728002926307, + -0.020948506263935844, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1873, + "type": "imguizmo", + "translation": [ + 0.03118902696010748, + -0.022077766757897282, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1874, + "type": "imguizmo", + "translation": [ + 0.029551916247140632, + -0.02314991910237844, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1875, + "type": "imguizmo", + "translation": [ + 0.027877987526451382, + -0.024163627529066763, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1876, + "type": "imguizmo", + "translation": [ + 0.026169326304781488, + -0.025117629083493558, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1877, + "type": "imguizmo", + "translation": [ + 0.024428061361243876, + -0.026010735198518105, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1878, + "type": "imguizmo", + "translation": [ + 0.022656362095128994, + -0.026841833175134142, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1879, + "type": "imguizmo", + "translation": [ + 0.020856435823103436, + -0.027609887568754436, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1880, + "type": "imguizmo", + "translation": [ + 0.01903052502916819, + -0.02831394147924571, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1881, + "type": "imguizmo", + "translation": [ + 0.017180904570802874, + -0.028953117743107066, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1882, + "type": "imguizmo", + "translation": [ + 0.015309878844776557, + -0.02952662002630648, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1883, + "type": "imguizmo", + "translation": [ + 0.013419778916156328, + -0.030033733816413877, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1884, + "type": "imguizmo", + "translation": [ + 0.011512959614090561, + -0.030473827312794587, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1885, + "type": "imguizmo", + "translation": [ + 0.00959179659798489, + -0.030846352213754303, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1886, + "type": "imguizmo", + "translation": [ + 0.007658683397726593, + -0.031150844399654706, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1887, + "type": "imguizmo", + "translation": [ + 0.00571602843164429, + -0.031386924511148745, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1888, + "type": "imguizmo", + "translation": [ + 0.003766252005918659, + -0.03155429842181511, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1889, + "type": "imguizmo", + "translation": [ + 0.001811783299182224, + -0.03165275760460319, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1890, + "type": "imguizmo", + "translation": [ + -0.0001449426639345639, + -0.031682179391631746, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1891, + "type": "imguizmo", + "translation": [ + -0.0021014880465409238, + -0.031642527127017865, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1892, + "type": "imguizmo", + "translation": [ + -0.0040554152367269, + -0.03153385021254559, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1893, + "type": "imguizmo", + "translation": [ + -0.006004289884524264, + -0.03135628404611747, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1894, + "type": "imguizmo", + "translation": [ + -0.007945683934803445, + -0.031110049853065657, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1895, + "type": "imguizmo", + "translation": [ + -0.009877178652327803, + -0.03079545441053262, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1896, + "type": "imguizmo", + "translation": [ + -0.01179636763519679, + -0.030412889665265102, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1897, + "type": "imguizmo", + "translation": [ + -0.013700859812922682, + -0.029962832245297204, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1898, + "type": "imguizmo", + "translation": [ + -0.01558828242540694, + -0.029445842866131274, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1899, + "type": "imguizmo", + "translation": [ + -0.017456283979103752, + -0.028862565632156233, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1900, + "type": "imguizmo", + "translation": [ + -0.019302537176688145, + -0.028213727234173577, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1901, + "type": "imguizmo", + "translation": [ + -0.02112474181657868, + -0.02750013604403128, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1902, + "type": "imguizmo", + "translation": [ + -0.022920627658702182, + -0.02672268110749309, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1903, + "type": "imguizmo", + "translation": [ + -0.024687957252930173, + -0.02588233103659816, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1904, + "type": "imguizmo", + "translation": [ + -0.02642452872666314, + -0.024980132802891065, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1905, + "type": "imguizmo", + "translation": [ + -0.028128178528089555, + -0.02401721043302576, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1906, + "type": "imguizmo", + "translation": [ + -0.029796784121702048, + -0.02299476360836817, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1907, + "type": "imguizmo", + "translation": [ + -0.03142826663271235, + -0.0219140661703427, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1908, + "type": "imguizmo", + "translation": [ + -0.033020593437070395, + -0.020776464533384414, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1909, + "type": "imguizmo", + "translation": [ + -0.034571780693860785, + -0.019583376007474285, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1910, + "type": "imguizmo", + "translation": [ + -0.03607989581692154, + -0.018336287032347527, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1911, + "type": "imguizmo", + "translation": [ + -0.03754305988260574, + -0.017036751325574702, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1912, + "type": "imguizmo", + "translation": [ + -0.03895944997068662, + -0.015686387946823085, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1913, + "type": "imguizmo", + "translation": [ + -0.04032730143548888, + -0.014286879280709996, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1914, + "type": "imguizmo", + "translation": [ + -0.041644910104417635, + -0.012839968940761089, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1915, + "type": "imguizmo", + "translation": [ + -0.04291063440114529, + -0.011347459597084913, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1916, + "type": "imguizmo", + "translation": [ + -0.044122897390811065, + -0.009811210730470568, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1917, + "type": "imguizmo", + "translation": [ + -0.04528018874468563, + -0.008233136315706035, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1918, + "type": "imguizmo", + "translation": [ + -0.046381066621852575, + -0.006615202437004037, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1919, + "type": "imguizmo", + "translation": [ + -0.0474241594655626, + -0.0049594248385059665, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1920, + "type": "imguizmo", + "translation": [ + -0.04840816771202252, + -0.0032678664129154864, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1921, + "type": "imguizmo", + "translation": [ + -0.049331865409489827, + -0.0015426346313913643, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1922, + "type": "imguizmo", + "translation": [ + -0.05019410174565596, + 0.00021412108209928945, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1923, + "type": "imguizmo", + "translation": [ + -0.05099380248141517, + 0.0020002120286956512, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1924, + "type": "imguizmo", + "translation": [ + -0.051729971289232554, + 0.003813412961489367, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1925, + "type": "imguizmo", + "translation": [ + -0.05240169099444425, + 0.005651464857905129, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1926, + "type": "imguizmo", + "translation": [ + -0.05300812471794276, + 0.0075120777341615005, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1927, + "type": "imguizmo", + "translation": [ + -0.053548516918824125, + 0.00939293349830549, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1928, + "type": "imguizmo", + "translation": [ + -0.05402219433569782, + 0.011291688838266686, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1929, + "type": "imguizmo", + "translation": [ + -0.05442856682548662, + 0.01320597814133196, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1930, + "type": "imguizmo", + "translation": [ + -0.054767128098671354, + 0.015133416441404573, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1931, + "type": "imguizmo", + "translation": [ + -0.055037456350064594, + 0.017071602390374883, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1932, + "type": "imguizmo", + "translation": [ + -0.0552392147843275, + 0.01901812124990105, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1933, + "type": "imguizmo", + "translation": [ + -0.0553721520355747, + 0.020970547899872342, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1934, + "type": "imguizmo", + "translation": [ + -0.05543610248054502, + 0.022926449859806776, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1935, + "type": "imguizmo", + "translation": [ + -0.05543098644494739, + 0.024883390319418882, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1936, + "type": "imguizmo", + "translation": [ + -0.0553568103027251, + 0.026838931174582057, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1937, + "type": "imguizmo", + "translation": [ + -0.05521366646811466, + 0.028790636064902387, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1938, + "type": "imguizmo", + "translation": [ + -0.05500173328050915, + 0.030736073409120556, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1939, + "type": "imguizmo", + "translation": [ + -0.05472127478226956, + 0.03267281943455922, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1940, + "type": "imguizmo", + "translation": [ + -0.0543726403897609, + 0.03459846119684196, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1941, + "type": "imguizmo", + "translation": [ + -0.05395626445802289, + 0.03651059958612152, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1942, + "type": "imguizmo", + "translation": [ + -0.053472665739617724, + 0.03840685231607186, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1943, + "type": "imguizmo", + "translation": [ + -0.05292244673832889, + 0.04028485689192034, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1944, + "type": "imguizmo", + "translation": [ + -0.05230629295851656, + 0.04214227355382209, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1945, + "type": "imguizmo", + "translation": [ + -0.05162497205106446, + 0.04397678819190922, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1946, + "type": "imguizmo", + "translation": [ + -0.050879332856982505, + 0.045786115229383674, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1947, + "type": "imguizmo", + "translation": [ + -0.050070304349856556, + 0.04756800047006132, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1948, + "type": "imguizmo", + "translation": [ + -0.04919889447846311, + 0.049320223906819706, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1949, + "type": "imguizmo", + "translation": [ + -0.0482661889109905, + 0.05104060248745038, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1950, + "type": "imguizmo", + "translation": [ + -0.04727334968243158, + 0.05272699283447027, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1951, + "type": "imguizmo", + "translation": [ + -0.046221613746832924, + 0.0543772939155029, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1952, + "type": "imguizmo", + "translation": [ + -0.045112291436203905, + 0.05598944966090345, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1953, + "type": "imguizmo", + "translation": [ + -0.0439467648280064, + 0.05756145152536509, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1954, + "type": "imguizmo", + "translation": [ + -0.042726486023258216, + 0.05909134099031661, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1955, + "type": "imguizmo", + "translation": [ + -0.04145297533739614, + 0.060577212003992555, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1956, + "type": "imguizmo", + "translation": [ + -0.04012781940615216, + 0.06201721335613643, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1957, + "type": "imguizmo", + "translation": [ + -0.03875266920880297, + 0.06340955098437813, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1958, + "type": "imguizmo", + "translation": [ + -0.03732923801125529, + 0.06475249020941225, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1959, + "type": "imguizmo", + "translation": [ + -0.03585929923152999, + 0.06604435789619251, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1960, + "type": "imguizmo", + "translation": [ + -0.03434468423030403, + 0.06728354453844967, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1961, + "type": "imguizmo", + "translation": [ + -0.03278728002926307, + 0.06846850626393589, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1962, + "type": "imguizmo", + "translation": [ + -0.03118902696010748, + 0.06959776675789732, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1963, + "type": "imguizmo", + "translation": [ + -0.029551916247140632, + 0.07066991910237846, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1964, + "type": "imguizmo", + "translation": [ + -0.027877987526451382, + 0.07168362752906678, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1965, + "type": "imguizmo", + "translation": [ + -0.026169326304781467, + 0.07263762908349358, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1966, + "type": "imguizmo", + "translation": [ + -0.024428061361243845, + 0.07353073519851812, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1967, + "type": "imguizmo", + "translation": [ + -0.02265636209512902, + 0.07436183317513415, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1968, + "type": "imguizmo", + "translation": [ + -0.020856435823103456, + 0.07512988756875444, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1969, + "type": "imguizmo", + "translation": [ + -0.019030525029168216, + 0.07583394147924571, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1970, + "type": "imguizmo", + "translation": [ + -0.017180904570802895, + 0.07647311774310708, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1971, + "type": "imguizmo", + "translation": [ + -0.015309878844776578, + 0.07704662002630651, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1972, + "type": "imguizmo", + "translation": [ + -0.013419778916156347, + 0.0775537338164139, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1973, + "type": "imguizmo", + "translation": [ + -0.011512959614090556, + 0.07799382731279461, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1974, + "type": "imguizmo", + "translation": [ + -0.009591796597984887, + 0.07836635221375433, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1975, + "type": "imguizmo", + "translation": [ + -0.0076586833977266445, + 0.07867084439965473, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1976, + "type": "imguizmo", + "translation": [ + -0.005716028431644335, + 0.07890692451114875, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1977, + "type": "imguizmo", + "translation": [ + -0.0037662520059186773, + 0.07907429842181513, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1978, + "type": "imguizmo", + "translation": [ + -0.0018117832991822454, + 0.07917275760460321, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 1979, + "type": "imguizmo", + "translation": [ + 0.0001449426639345431, + 0.07920217939163175, + 0.5940000000000001 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + ], + "checks": [ + { + "frame": 0, + "kind": "baseline" + }, + { + "frame": 1, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 2, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 3, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 4, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 5, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 6, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 7, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 8, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 9, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 10, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 11, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 12, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 13, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 14, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 15, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 16, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 17, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 18, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 19, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 20, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 21, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 22, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 23, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 24, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 25, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 26, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 27, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 28, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 29, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 30, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 31, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 32, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 33, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 34, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 35, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 36, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 37, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 38, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 39, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 40, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 41, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 42, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 43, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 44, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 45, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 46, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 47, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 48, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 49, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 50, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 51, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 52, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 53, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 54, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 55, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 56, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 57, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 58, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 59, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 60, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 61, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 62, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 63, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 64, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 65, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 66, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 67, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 68, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 69, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 70, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 71, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 72, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 73, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 74, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 75, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 76, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 77, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 78, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 79, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 80, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 81, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 82, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 83, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 84, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 85, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 86, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 87, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 88, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 89, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 90, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 91, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 92, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 93, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 94, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 95, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 96, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 97, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 98, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 99, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 100, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 101, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 102, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 103, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 104, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 105, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 106, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 107, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 108, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 109, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 110, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 111, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 112, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 113, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 114, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 115, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 116, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 117, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 118, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 119, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 120, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 121, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 122, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 123, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 124, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 125, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 126, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 127, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 128, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 129, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 130, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 131, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 132, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 133, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 134, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 135, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 136, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 137, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 138, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 139, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 140, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 141, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 142, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 143, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 144, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 145, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 146, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 147, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 148, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 149, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 150, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 151, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 152, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 153, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 154, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 155, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 156, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 157, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 158, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 159, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 160, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 161, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 162, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 163, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 164, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 165, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 166, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 167, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 168, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 169, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 170, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 171, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 172, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 173, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 174, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 175, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 176, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 177, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 178, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 179, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 180, + "kind": "baseline" + }, + { + "frame": 181, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 182, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 183, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 184, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 185, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 186, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 187, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 188, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 189, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 190, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 191, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 192, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 193, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 194, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 195, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 196, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 197, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 198, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 199, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 200, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 201, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 202, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 203, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 204, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 205, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 206, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 207, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 208, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 209, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 210, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 211, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 212, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 213, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 214, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 215, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 216, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 217, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 218, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 219, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 220, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 221, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 222, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 223, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 224, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 225, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 226, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 227, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 228, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 229, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 230, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 231, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 232, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 233, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 234, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 235, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 236, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 237, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 238, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 239, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 240, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 241, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 242, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 243, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 244, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 245, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 246, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 247, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 248, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 249, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 250, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 251, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 252, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 253, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 254, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 255, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 256, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 257, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 258, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 259, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 260, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 261, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 262, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 263, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 264, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 265, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 266, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 267, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 268, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 269, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 270, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 271, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 272, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 273, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 274, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 275, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 276, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 277, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 278, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 279, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 280, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 281, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 282, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 283, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 284, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 285, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 286, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 287, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 288, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 289, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 290, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 291, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 292, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 293, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 294, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 295, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 296, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 297, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 298, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 299, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 300, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 301, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 302, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 303, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 304, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 305, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 306, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 307, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 308, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 309, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 310, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 311, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 312, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 313, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 314, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 315, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 316, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 317, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 318, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 319, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 320, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 321, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 322, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 323, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 324, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 325, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 326, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 327, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 328, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 329, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 330, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 331, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 332, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 333, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 334, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 335, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 336, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 337, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 338, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 339, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 340, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 341, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 342, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 343, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 344, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 345, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 346, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 347, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 348, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 349, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 350, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 351, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 352, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 353, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 354, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 355, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 356, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 357, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 358, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 359, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 360, + "kind": "baseline" + }, + { + "frame": 361, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 362, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 363, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 364, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 365, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 366, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 367, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 368, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 369, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 370, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 371, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 372, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 373, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 374, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 375, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 376, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 377, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 378, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 379, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 380, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 381, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 382, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 383, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 384, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 385, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 386, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 387, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 388, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 389, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 390, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 391, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 392, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 393, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 394, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 395, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 396, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 397, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 398, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 399, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 400, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 401, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 402, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 403, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 404, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 405, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 406, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 407, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 408, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 409, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 410, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 411, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 412, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 413, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 414, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 415, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 416, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 417, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 418, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 419, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 420, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 421, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 422, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 423, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 424, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 425, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 426, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 427, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 428, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 429, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 430, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 431, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 432, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 433, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 434, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 435, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 436, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 437, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 438, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 439, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 440, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 441, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 442, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 443, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 444, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 445, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 446, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 447, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 448, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 449, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 450, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 451, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 452, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 453, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 454, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 455, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 456, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 457, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 458, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 459, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 460, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 461, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 462, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 463, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 464, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 465, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 466, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 467, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 468, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 469, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 470, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 471, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 472, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 473, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 474, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 475, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 476, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 477, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 478, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 479, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 480, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 481, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 482, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 483, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 484, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 485, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 486, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 487, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 488, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 489, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 490, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 491, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 492, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 493, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 494, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 495, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 496, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 497, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 498, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 499, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 500, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 501, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 502, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 503, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 504, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 505, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 506, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 507, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 508, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 509, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 510, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 511, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 512, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 513, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 514, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 515, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 516, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 517, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 518, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 519, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 520, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 521, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 522, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 523, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 524, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 525, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 526, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 527, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 528, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 529, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 530, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 531, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 532, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 533, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 534, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 535, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 536, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 537, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 538, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 539, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 540, + "kind": "baseline" + }, + { + "frame": 541, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 542, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 543, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 544, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 545, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 546, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 547, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 548, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 549, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 550, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 551, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 552, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 553, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 554, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 555, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 556, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 557, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 558, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 559, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 560, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 561, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 562, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 563, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 564, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 565, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 566, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 567, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 568, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 569, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 570, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 571, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 572, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 573, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 574, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 575, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 576, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 577, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 578, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 579, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 580, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 581, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 582, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 583, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 584, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 585, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 586, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 587, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 588, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 589, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 590, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 591, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 592, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 593, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 594, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 595, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 596, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 597, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 598, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 599, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 600, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 601, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 602, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 603, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 604, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 605, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 606, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 607, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 608, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 609, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 610, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 611, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 612, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 613, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 614, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 615, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 616, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 617, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 618, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 619, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 620, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 621, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 622, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 623, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 624, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 625, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 626, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 627, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 628, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 629, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 630, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 631, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 632, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 633, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 634, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 635, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 636, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 637, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 638, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 639, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 640, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 641, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 642, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 643, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 644, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 645, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 646, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 647, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 648, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 649, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 650, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 651, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 652, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 653, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 654, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 655, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 656, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 657, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 658, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 659, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 660, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 661, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 662, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 663, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 664, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 665, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 666, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 667, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 668, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 669, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 670, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 671, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 672, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 673, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 674, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 675, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 676, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 677, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 678, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 679, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 680, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 681, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 682, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 683, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 684, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 685, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 686, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 687, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 688, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 689, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 690, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 691, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 692, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 693, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 694, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 695, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 696, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 697, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 698, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 699, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 700, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 701, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 702, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 703, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 704, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 705, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 706, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 707, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 708, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 709, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 710, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 711, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 712, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 713, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 714, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 715, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 716, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 717, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 718, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 719, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 720, + "kind": "baseline" + }, + { + "frame": 721, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 722, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 723, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 724, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 725, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 726, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 727, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 728, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 729, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 730, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 731, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 732, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 733, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 734, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 735, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 736, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 737, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 738, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 739, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 740, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 741, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 742, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 743, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 744, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 745, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 746, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 747, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 748, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 749, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 750, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 751, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 752, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 753, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 754, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 755, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 756, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 757, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 758, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 759, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 760, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 761, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 762, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 763, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 764, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 765, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 766, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 767, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 768, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 769, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 770, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 771, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 772, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 773, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 774, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 775, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 776, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 777, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 778, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 779, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 780, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 781, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 782, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 783, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 784, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 785, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 786, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 787, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 788, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 789, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 790, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 791, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 792, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 793, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 794, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 795, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 796, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 797, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 798, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 799, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 800, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 801, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 802, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 803, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 804, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 805, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 806, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 807, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 808, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 809, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 810, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 811, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 812, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 813, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 814, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 815, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 816, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 817, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 818, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 819, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 820, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 821, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 822, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 823, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 824, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 825, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 826, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 827, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 828, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 829, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 830, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 831, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 832, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 833, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 834, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 835, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 836, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 837, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 838, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 839, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 840, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 841, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 842, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 843, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 844, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 845, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 846, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 847, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 848, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 849, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 850, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 851, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 852, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 853, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 854, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 855, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 856, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 857, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 858, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 859, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 860, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 861, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 862, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 863, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 864, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 865, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 866, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 867, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 868, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 869, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 870, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 871, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 872, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 873, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 874, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 875, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 876, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 877, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 878, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 879, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 880, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 881, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 882, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 883, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 884, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 885, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 886, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 887, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 888, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 889, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 890, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 891, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 892, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 893, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 894, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 895, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 896, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 897, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 898, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 899, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 900, + "kind": "baseline" + }, + { + "frame": 901, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 902, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 903, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 904, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 905, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 906, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 907, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 908, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 909, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 910, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 911, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 912, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 913, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 914, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 915, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 916, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 917, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 918, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 919, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 920, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 921, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 922, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 923, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 924, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 925, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 926, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 927, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 928, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 929, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 930, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 931, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 932, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 933, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 934, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 935, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 936, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 937, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 938, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 939, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 940, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 941, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 942, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 943, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 944, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 945, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 946, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 947, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 948, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 949, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 950, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 951, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 952, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 953, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 954, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 955, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 956, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 957, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 958, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 959, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 960, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 961, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 962, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 963, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 964, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 965, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 966, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 967, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 968, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 969, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 970, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 971, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 972, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 973, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 974, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 975, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 976, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 977, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 978, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 979, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 980, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 981, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 982, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 983, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 984, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 985, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 986, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 987, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 988, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 989, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 990, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 991, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 992, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 993, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 994, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 995, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 996, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 997, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 998, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 999, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1000, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1001, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1002, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1003, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1004, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1005, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1006, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1007, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1008, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1009, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1010, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1011, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1012, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1013, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1014, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1015, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1016, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1017, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1018, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1019, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1020, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1021, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1022, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1023, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1024, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1025, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1026, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1027, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1028, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1029, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1030, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1031, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1032, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1033, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1034, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1035, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1036, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1037, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1038, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1039, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1040, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1041, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1042, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1043, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1044, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1045, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1046, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1047, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1048, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1049, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1050, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1051, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1052, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1053, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1054, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1055, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1056, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1057, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1058, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1059, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1060, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1061, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1062, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1063, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1064, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1065, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1066, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1067, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1068, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1069, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1070, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1071, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1072, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1073, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1074, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1075, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1076, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1077, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1078, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1079, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1080, + "kind": "baseline" + }, + { + "frame": 1081, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1082, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1083, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1084, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1085, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1086, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1087, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1088, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1089, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1090, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1091, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1092, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1093, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1094, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1095, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1096, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1097, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1098, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1099, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1100, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1101, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1102, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1103, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1104, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1105, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1106, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1107, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1108, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1109, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1110, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1111, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1112, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1113, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1114, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1115, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1116, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1117, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1118, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1119, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1120, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1121, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1122, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1123, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1124, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1125, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1126, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1127, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1128, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1129, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1130, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1131, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1132, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1133, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1134, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1135, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1136, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1137, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1138, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1139, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1140, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1141, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1142, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1143, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1144, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1145, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1146, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1147, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1148, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1149, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1150, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1151, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1152, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1153, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1154, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1155, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1156, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1157, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1158, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1159, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1160, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1161, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1162, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1163, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1164, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1165, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1166, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1167, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1168, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1169, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1170, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1171, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1172, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1173, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1174, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1175, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1176, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1177, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1178, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1179, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1180, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1181, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1182, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1183, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1184, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1185, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1186, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1187, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1188, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1189, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1190, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1191, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1192, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1193, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1194, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1195, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1196, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1197, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1198, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1199, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1200, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1201, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1202, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1203, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1204, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1205, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1206, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1207, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1208, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1209, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1210, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1211, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1212, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1213, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1214, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1215, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1216, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1217, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1218, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1219, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1220, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1221, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1222, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1223, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1224, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1225, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1226, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1227, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1228, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1229, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1230, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1231, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1232, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1233, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1234, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1235, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1236, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1237, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1238, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1239, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1240, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1241, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1242, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1243, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1244, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1245, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1246, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1247, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1248, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1249, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1250, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1251, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1252, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1253, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1254, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1255, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1256, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1257, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1258, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1259, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1260, + "kind": "baseline" + }, + { + "frame": 1261, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1262, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1263, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1264, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1265, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1266, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1267, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1268, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1269, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1270, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1271, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1272, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1273, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1274, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1275, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1276, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1277, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1278, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1279, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1280, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1281, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1282, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1283, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1284, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1285, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1286, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1287, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1288, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1289, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1290, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1291, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1292, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1293, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1294, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1295, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1296, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1297, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1298, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1299, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1300, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1301, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1302, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1303, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1304, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1305, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1306, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1307, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1308, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1309, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1310, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1311, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1312, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1313, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1314, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1315, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1316, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1317, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1318, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1319, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1320, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1321, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1322, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1323, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1324, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1325, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1326, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1327, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1328, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1329, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1330, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1331, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1332, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1333, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1334, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1335, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1336, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1337, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1338, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1339, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1340, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1341, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1342, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1343, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1344, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1345, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1346, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1347, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1348, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1349, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1350, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1351, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1352, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1353, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1354, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1355, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1356, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1357, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1358, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1359, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1360, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1361, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1362, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1363, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1364, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1365, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1366, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1367, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1368, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1369, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1370, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1371, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1372, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1373, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1374, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1375, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1376, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1377, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1378, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1379, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1380, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1381, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1382, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1383, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1384, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1385, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1386, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1387, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1388, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1389, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1390, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1391, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1392, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1393, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1394, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1395, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1396, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1397, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1398, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1399, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1400, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1401, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1402, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1403, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1404, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1405, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1406, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1407, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1408, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1409, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1410, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1411, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1412, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1413, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1414, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1415, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1416, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1417, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1418, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1419, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1420, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1421, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1422, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1423, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1424, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1425, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1426, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1427, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1428, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1429, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1430, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1431, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1432, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1433, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1434, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1435, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1436, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1437, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1438, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1439, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1440, + "kind": "baseline" + }, + { + "frame": 1441, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1442, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1443, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1444, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1445, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1446, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1447, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1448, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1449, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1450, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1451, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1452, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1453, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1454, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1455, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1456, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1457, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1458, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1459, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1460, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1461, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1462, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1463, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1464, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1465, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1466, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1467, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1468, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1469, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1470, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1471, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1472, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1473, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1474, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1475, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1476, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1477, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1478, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1479, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1480, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1481, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1482, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1483, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1484, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1485, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1486, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1487, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1488, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1489, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1490, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1491, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1492, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1493, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1494, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1495, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1496, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1497, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1498, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1499, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1500, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1501, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1502, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1503, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1504, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1505, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1506, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1507, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1508, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1509, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1510, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1511, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1512, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1513, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1514, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1515, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1516, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1517, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1518, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1519, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1520, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1521, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1522, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1523, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1524, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1525, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1526, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1527, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1528, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1529, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1530, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1531, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1532, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1533, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1534, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1535, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1536, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1537, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1538, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1539, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1540, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1541, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1542, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1543, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1544, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1545, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1546, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1547, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1548, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1549, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1550, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1551, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1552, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1553, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1554, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1555, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1556, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1557, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1558, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1559, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1560, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1561, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1562, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1563, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1564, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1565, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1566, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1567, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1568, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1569, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1570, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1571, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1572, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1573, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1574, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1575, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1576, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1577, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1578, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1579, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1580, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1581, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1582, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1583, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1584, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1585, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1586, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1587, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1588, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1589, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1590, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1591, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1592, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1593, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1594, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1595, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1596, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1597, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1598, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1599, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1600, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1601, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1602, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1603, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1604, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1605, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1606, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1607, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1608, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1609, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1610, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1611, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1612, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1613, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1614, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1615, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1616, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1617, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1618, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1619, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1620, + "kind": "baseline" + }, + { + "frame": 1621, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1622, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1623, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1624, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1625, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1626, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1627, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1628, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1629, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1630, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1631, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1632, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1633, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1634, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1635, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1636, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1637, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1638, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1639, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1640, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1641, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1642, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1643, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1644, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1645, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1646, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1647, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1648, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1649, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1650, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1651, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1652, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1653, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1654, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1655, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1656, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1657, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1658, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1659, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1660, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1661, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1662, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1663, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1664, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1665, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1666, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1667, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1668, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1669, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1670, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1671, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1672, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1673, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1674, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1675, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1676, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1677, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1678, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1679, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1680, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1681, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1682, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1683, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1684, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1685, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1686, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1687, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1688, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1689, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1690, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1691, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1692, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1693, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1694, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1695, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1696, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1697, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1698, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1699, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1700, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1701, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1702, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1703, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1704, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1705, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1706, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1707, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1708, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1709, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1710, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1711, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1712, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1713, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1714, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1715, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1716, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1717, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1718, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1719, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1720, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1721, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1722, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1723, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1724, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1725, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1726, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1727, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1728, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1729, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1730, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1731, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1732, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1733, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1734, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1735, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1736, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1737, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1738, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1739, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1740, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1741, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1742, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1743, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1744, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1745, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1746, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1747, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1748, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1749, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1750, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1751, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1752, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1753, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1754, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1755, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1756, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1757, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1758, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1759, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1760, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1761, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1762, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1763, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1764, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1765, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1766, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1767, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1768, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1769, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1770, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1771, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1772, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1773, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1774, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1775, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1776, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1777, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1778, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1779, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1780, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1781, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1782, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1783, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1784, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1785, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1786, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1787, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1788, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1789, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1790, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1791, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1792, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1793, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1794, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1795, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1796, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1797, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1798, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1799, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1800, + "kind": "baseline" + }, + { + "frame": 1801, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1802, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1803, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1804, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1805, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1806, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1807, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1808, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1809, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1810, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1811, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1812, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1813, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1814, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1815, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1816, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1817, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1818, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1819, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1820, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1821, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1822, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1823, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1824, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1825, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1826, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1827, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1828, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1829, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1830, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1831, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1832, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1833, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1834, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1835, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1836, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1837, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1838, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1839, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1840, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1841, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1842, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1843, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1844, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1845, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1846, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1847, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1848, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1849, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1850, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1851, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1852, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1853, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1854, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1855, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1856, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1857, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1858, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1859, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1860, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1861, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1862, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1863, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1864, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1865, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1866, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1867, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1868, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1869, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1870, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1871, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1872, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1873, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1874, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1875, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1876, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1877, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1878, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1879, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1880, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1881, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1882, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1883, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1884, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1885, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1886, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1887, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1888, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1889, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1890, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1891, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1892, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1893, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1894, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1895, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1896, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1897, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1898, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1899, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1900, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1901, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1902, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1903, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1904, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1905, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1906, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1907, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1908, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1909, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1910, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1911, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1912, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1913, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1914, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1915, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1916, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1917, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1918, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1919, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1920, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1921, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1922, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1923, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1924, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1925, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1926, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1927, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1928, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1929, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1930, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1931, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1932, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1933, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1934, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1935, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1936, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1937, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1938, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1939, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1940, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1941, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1942, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1943, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1944, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1945, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1946, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1947, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1948, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1949, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1950, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1951, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1952, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1953, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1954, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1955, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1956, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1957, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1958, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1959, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1960, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1961, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1962, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1963, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1964, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1965, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1966, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1967, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1968, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1969, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1970, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1971, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1972, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1973, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1974, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1975, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1976, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1977, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1978, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + }, + { + "frame": 1979, + "kind": "gimbal_step", + "min_pos_delta": 0.0004, + "max_pos_delta": 1.5 + } + ] +} + 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..cfa34d543 --- /dev/null +++ b/61_UI/app_resources/cameraz_smoke_all.json @@ -0,0 +1,532 @@ +{ + "enabled": true, + "log": true, + "exclusive": true, + "hard_fail": true, + "enableActiveCameraMovement": true, + "capture_prefix": "camera_smoke", + "capture_frames": [ + 31 + ], + "events": [ + { + "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": 1, + "type": "imguizmo", + "translation": [ + 4.0, + 2.0, + 1.0 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 3, + "type": "action", + "action": "set_active_render_window", + "value": 0 + }, + { + "frame": 3, + "type": "action", + "action": "set_active_planar", + "value": 1 + }, + { + "frame": 3, + "type": "action", + "action": "set_projection_type", + "value": "perspective" + }, + { + "frame": 4, + "type": "imguizmo", + "translation": [ + 4.0, + 2.0, + 1.0 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.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": 2 + }, + { + "frame": 6, + "type": "action", + "action": "set_projection_type", + "value": "perspective" + }, + { + "frame": 7, + "type": "imguizmo", + "translation": [ + 4.0, + 2.0, + 1.0 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 9, + "type": "action", + "action": "set_active_render_window", + "value": 0 + }, + { + "frame": 9, + "type": "action", + "action": "set_active_planar", + "value": 3 + }, + { + "frame": 9, + "type": "action", + "action": "set_projection_type", + "value": "perspective" + }, + { + "frame": 10, + "type": "imguizmo", + "translation": [ + 4.0, + 2.0, + 1.0 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.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": 4 + }, + { + "frame": 12, + "type": "action", + "action": "set_projection_type", + "value": "perspective" + }, + { + "frame": 13, + "type": "imguizmo", + "translation": [ + 4.0, + 2.0, + 1.0 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 15, + "type": "action", + "action": "set_active_render_window", + "value": 0 + }, + { + "frame": 15, + "type": "action", + "action": "set_active_planar", + "value": 5 + }, + { + "frame": 15, + "type": "action", + "action": "set_projection_type", + "value": "perspective" + }, + { + "frame": 16, + "type": "imguizmo", + "translation": [ + 4.0, + 2.0, + 1.0 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.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": 6 + }, + { + "frame": 18, + "type": "action", + "action": "set_projection_type", + "value": "perspective" + }, + { + "frame": 19, + "type": "imguizmo", + "translation": [ + 4.0, + 2.0, + 1.0 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 21, + "type": "action", + "action": "set_active_render_window", + "value": 0 + }, + { + "frame": 21, + "type": "action", + "action": "set_active_planar", + "value": 7 + }, + { + "frame": 21, + "type": "action", + "action": "set_projection_type", + "value": "perspective" + }, + { + "frame": 22, + "type": "imguizmo", + "translation": [ + 4.0, + 2.0, + 1.0 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.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": 8 + }, + { + "frame": 24, + "type": "action", + "action": "set_projection_type", + "value": "perspective" + }, + { + "frame": 25, + "type": "imguizmo", + "translation": [ + 4.0, + 2.0, + 1.0 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + { + "frame": 27, + "type": "action", + "action": "set_active_render_window", + "value": 0 + }, + { + "frame": 27, + "type": "action", + "action": "set_active_planar", + "value": 9 + }, + { + "frame": 27, + "type": "action", + "action": "set_projection_type", + "value": "perspective" + }, + { + "frame": 28, + "type": "imguizmo", + "translation": [ + 4.0, + 2.0, + 1.0 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.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": 10 + }, + { + "frame": 30, + "type": "action", + "action": "set_projection_type", + "value": "perspective" + }, + { + "frame": 31, + "type": "imguizmo", + "translation": [ + 4.0, + 2.0, + 1.0 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + ], + "checks": [ + { + "frame": 0, + "kind": "baseline" + }, + { + "frame": 1, + "kind": "gimbal_step", + "min_pos_delta": 0.009, + "max_pos_delta": 8.0 + }, + { + "frame": 3, + "kind": "baseline" + }, + { + "frame": 4, + "kind": "gimbal_step", + "min_pos_delta": 0.009, + "max_pos_delta": 8.0 + }, + { + "frame": 6, + "kind": "baseline" + }, + { + "frame": 7, + "kind": "gimbal_step", + "min_pos_delta": 0.009, + "max_pos_delta": 8.0 + }, + { + "frame": 9, + "kind": "baseline" + }, + { + "frame": 10, + "kind": "gimbal_step", + "min_pos_delta": 0.009, + "max_pos_delta": 8.0 + }, + { + "frame": 12, + "kind": "baseline" + }, + { + "frame": 13, + "kind": "gimbal_step", + "min_pos_delta": 0.009, + "max_pos_delta": 8.0 + }, + { + "frame": 15, + "kind": "baseline" + }, + { + "frame": 16, + "kind": "gimbal_step", + "min_pos_delta": 0.009, + "max_pos_delta": 8.0 + }, + { + "frame": 18, + "kind": "baseline" + }, + { + "frame": 19, + "kind": "gimbal_step", + "min_pos_delta": 0.009, + "max_pos_delta": 8.0 + }, + { + "frame": 21, + "kind": "baseline" + }, + { + "frame": 22, + "kind": "gimbal_step", + "min_pos_delta": 0.009, + "max_pos_delta": 8.0 + }, + { + "frame": 24, + "kind": "baseline" + }, + { + "frame": 25, + "kind": "gimbal_step", + "min_pos_delta": 0.009, + "max_pos_delta": 8.0 + }, + { + "frame": 27, + "kind": "baseline" + }, + { + "frame": 28, + "kind": "gimbal_step", + "min_pos_delta": 0.009, + "max_pos_delta": 8.0 + }, + { + "frame": 30, + "kind": "baseline" + }, + { + "frame": 31, + "kind": "gimbal_step", + "min_pos_delta": 0.009, + "max_pos_delta": 8.0 + } + ] +} diff --git a/61_UI/include/common.hpp b/61_UI/include/common.hpp index ca3a5fa9f..6ea91a645 100644 --- a/61_UI/include/common.hpp +++ b/61_UI/include/common.hpp @@ -8,9 +8,16 @@ // common api #include "camera/CFPSCamera.hpp" #include "camera/CFreeLockCamera.hpp" +#include "camera/CSphericalTargetCamera.hpp" #include "camera/COrbitCamera.hpp" #include "camera/CArcballCamera.hpp" #include "camera/CTurntableCamera.hpp" +#include "camera/CTopDownCamera.hpp" +#include "camera/CIsometricCamera.hpp" +#include "camera/CChaseCamera.hpp" +#include "camera/CDollyCamera.hpp" +#include "camera/CDollyZoomCamera.hpp" +#include "camera/CPathCamera.hpp" #include "camera/CTargetPoseController.hpp" #include "camera/CCubeProjection.hpp" @@ -35,9 +42,16 @@ namespace hlsl = nbl::hlsl; using nbl::hlsl::ICamera; using nbl::hlsl::CFPSCamera; using nbl::hlsl::CFreeCamera; +using nbl::hlsl::CSphericalTargetCamera; using nbl::hlsl::COrbitCamera; using nbl::hlsl::CArcballCamera; using nbl::hlsl::CTurntableCamera; +using nbl::hlsl::CTopDownCamera; +using nbl::hlsl::CIsometricCamera; +using nbl::hlsl::CChaseCamera; +using nbl::hlsl::CDollyCamera; +using nbl::hlsl::CDollyZoomCamera; +using nbl::hlsl::CPathCamera; using nbl::hlsl::CTargetPose; using nbl::hlsl::CTargetPoseController; using nbl::hlsl::IPlanarProjection; diff --git a/61_UI/main.cpp b/61_UI/main.cpp index 76fa5807c..25e93b249 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -3,11 +3,15 @@ // For conditions of distribution and use, see copyright notice in nabla.h #include +#include #include +#include #include #include #include +#include #include +#include #include #include #include "nlohmann/json.hpp" @@ -283,7 +287,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication constexpr static inline clock_t::duration DisplayImageDuration = std::chrono::milliseconds(900); constexpr static inline auto sceneRenderDepthFormat = EF_D32_SFLOAT; constexpr static inline auto finalSceneRenderFormat = EF_R8G8B8A8_SRGB; - constexpr static inline IGPUCommandBuffer::SClearColorValue SceneClearColor = { .float32 = {0.f,0.f,0.f,1.f} }; + constexpr static inline IGPUCommandBuffer::SClearColorValue SceneClearColor = { .float32 = {0.014f,0.018f,0.030f,1.f} }; constexpr static inline IGPUCommandBuffer::SClearDepthStencilValue SceneClearDepth = { .depth = 0.f }; public: @@ -322,7 +326,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication { m_window->getManager()->maximize(m_window.get()); auto* cc = m_window->getCursorControl(); - cc->setVisible(false); + cc->setVisible(true); return { {m_surface->getSurface()/*,EQF_NONE*/} }; } @@ -346,6 +350,10 @@ class UISampleApp final : public examples::SimpleWindowedApplication .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); try { @@ -359,11 +367,16 @@ class UISampleApp final : public examples::SimpleWindowedApplication m_ciMode = program.get("--ci"); if (m_ciMode) + { m_ciScreenshotPath = localOutputCWD / "cameraz_ci.png"; + m_ciStartedAt = clock_t::now(); + } m_scriptedInput.log = program.get("--script-log"); + m_scriptVisualDebugCli = program.get("--script-visual-debug"); // Create imput system m_inputSystem = make_smart_refctd_ptr(logger_opt_smart_ptr(smart_refctd_ptr(m_logger))); + m_logFormatter = core::make_smart_refctd_ptr(); // Remember to call the base class initialization! if (!base_t::onAppInitialized(std::move(system))) @@ -435,7 +448,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication if (cameraJsonFile.has_value()) m_logger->log("Cannot open input \"%s\" json file. Switching to default config.", ILogger::ELL_WARNING, cameraJsonFile.value().c_str()); else - m_logger->log("No input json file provided. Switching to default config.", ILogger::ELL_WARNING); + m_logger->log("No input json file provided. Switching to default config.", ILogger::ELL_INFO); if (!loadDefaultConfig()) return false; @@ -468,8 +481,16 @@ class UISampleApp final : public examples::SimpleWindowedApplication m_scriptedInput.failed = false; m_scriptedInput.summaryReported = false; m_scriptedInput.baselineValid = false; + m_scriptedInput.stepValid = false; m_scriptedInput.exclusive = false; m_scriptedInput.hardFail = false; + m_scriptedInput.visualDebug = false; + m_scriptedInput.visualTargetFps = 0.f; + m_scriptedInput.visualCameraHoldSeconds = 0.f; + m_scriptedInput.visualActivePlanarValid = false; + m_scriptedInput.visualActivePlanarIx = 0u; + m_scriptedInput.visualActivePlanarStartFrame = 0u; + m_scriptedInput.framePacerInitialized = false; m_scriptedInput.capturePrefix = "script"; m_scriptedInput.captureOutputDir = localOutputCWD; @@ -484,6 +505,22 @@ class UISampleApp final : public examples::SimpleWindowedApplication if (script.contains("hard_fail")) m_scriptedInput.hardFail = script["hard_fail"].get(); + if (script.contains("visual_debug")) + m_scriptedInput.visualDebug = script["visual_debug"].get(); + if (script.contains("visual_debug_target_fps")) + m_scriptedInput.visualTargetFps = script["visual_debug_target_fps"].get(); + if (script.contains("visual_debug_hold_seconds")) + m_scriptedInput.visualCameraHoldSeconds = script["visual_debug_hold_seconds"].get(); + if (m_scriptVisualDebugCli) + m_scriptedInput.visualDebug = true; + if (m_scriptedInput.visualDebug) + { + if (m_scriptedInput.visualTargetFps <= 0.f) + m_scriptedInput.visualTargetFps = 60.f; + if (m_scriptedInput.visualCameraHoldSeconds <= 0.f) + m_scriptedInput.visualCameraHoldSeconds = 3.f; + } + if (script.contains("enableActiveCameraMovement")) enableActiveCameraMovement = script["enableActiveCameraMovement"].get(); else if (m_scriptedInput.enabled) @@ -839,6 +876,48 @@ class UISampleApp final : public examples::SimpleWindowedApplication entry.posTolerance = chk.value("pos_tolerance", entry.posTolerance); entry.eulerToleranceDeg = chk.value("euler_tolerance_deg", entry.eulerToleranceDeg); } + else if (kind == "gimbal_step") + { + entry.kind = ScriptedInputCheck::Kind::GimbalStep; + + if (chk.contains("min_pos_delta")) + { + entry.minPosDelta = chk["min_pos_delta"].get(); + entry.hasPosDeltaConstraint = true; + } + if (chk.contains("max_pos_delta")) + { + entry.posTolerance = chk["max_pos_delta"].get(); + entry.hasPosDeltaConstraint = true; + } + else if (chk.contains("pos_tolerance")) + { + entry.posTolerance = chk["pos_tolerance"].get(); + entry.hasPosDeltaConstraint = true; + } + + if (chk.contains("min_euler_delta_deg")) + { + entry.minEulerDeltaDeg = chk["min_euler_delta_deg"].get(); + entry.hasEulerDeltaConstraint = true; + } + if (chk.contains("max_euler_delta_deg")) + { + entry.eulerToleranceDeg = chk["max_euler_delta_deg"].get(); + entry.hasEulerDeltaConstraint = true; + } + else if (chk.contains("euler_tolerance_deg")) + { + entry.eulerToleranceDeg = chk["euler_tolerance_deg"].get(); + entry.hasEulerDeltaConstraint = true; + } + + if (!entry.hasPosDeltaConstraint && !entry.hasEulerDeltaConstraint) + { + m_logger->log("gimbal_step check requires at least one delta constraint.", ILogger::ELL_WARNING); + continue; + } + } else { m_logger->log("Scripted check has invalid kind \"%s\".", ILogger::ELL_WARNING, kind.c_str()); @@ -961,6 +1040,52 @@ class UISampleApp final : public examples::SimpleWindowedApplication camera->setRotationSpeedScale(DefaultRotateScale); cameras.emplace_back(std::move(camera)); } + else if (jCamera["type"] == "TopDown") + { + auto camera = make_smart_refctd_ptr(position, getTarget()); + camera->setMoveSpeedScale(OrbitMoveScale); + camera->setRotationSpeedScale(DefaultRotateScale); + cameras.emplace_back(std::move(camera)); + } + else if (jCamera["type"] == "Isometric") + { + auto camera = make_smart_refctd_ptr(position, getTarget()); + camera->setMoveSpeedScale(OrbitMoveScale); + camera->setRotationSpeedScale(DefaultRotateScale); + cameras.emplace_back(std::move(camera)); + } + else if (jCamera["type"] == "Chase") + { + auto camera = make_smart_refctd_ptr(position, getTarget()); + camera->setMoveSpeedScale(OrbitMoveScale); + camera->setRotationSpeedScale(DefaultRotateScale); + cameras.emplace_back(std::move(camera)); + } + else if (jCamera["type"] == "Dolly") + { + auto camera = make_smart_refctd_ptr(position, getTarget()); + camera->setMoveSpeedScale(OrbitMoveScale); + camera->setRotationSpeedScale(DefaultRotateScale); + cameras.emplace_back(std::move(camera)); + } + else if (jCamera["type"] == "DollyZoom") + { + float baseFov = 40.0f; + if (jCamera.contains("baseFov")) + baseFov = jCamera["baseFov"].get(); + + auto camera = make_smart_refctd_ptr(position, getTarget(), baseFov); + camera->setMoveSpeedScale(OrbitMoveScale); + camera->setRotationSpeedScale(DefaultRotateScale); + cameras.emplace_back(std::move(camera)); + } + else if (jCamera["type"] == "Path") + { + auto camera = make_smart_refctd_ptr(position, getTarget()); + camera->setMoveSpeedScale(OrbitMoveScale); + camera->setRotationSpeedScale(DefaultRotateScale); + cameras.emplace_back(std::move(camera)); + } else { logFail("Unsupported camera type!"); @@ -1176,10 +1301,9 @@ class UISampleApp final : public examples::SimpleWindowedApplication return false; } - if (m_planarProjections.size() < windowBindings.size()) + if (m_planarProjections.empty()) { - // TODO, temporary assuming it, I'm not going to implement each possible case now - logFail("Expected at least %d planars", windowBindings.size()); + logFail("Expected at least 1 planar"); return false; } @@ -1344,7 +1468,13 @@ class UISampleApp final : public examples::SimpleWindowedApplication // UI { { - constexpr size_t ImGuiStreamingBufferSize = 32ull * 1024ull * 1024ull; + constexpr std::array ImGuiStreamingBufferSizes = { + 32ull * 1024ull * 1024ull, + 16ull * 1024ull * 1024ull, + 8ull * 1024ull * 1024ull, + 4ull * 1024ull * 1024ull, + 2ull * 1024ull * 1024ull + }; auto createImGuiStreamingBuffer = [&](size_t size) -> smart_refctd_ptr { constexpr uint32_t minStreamingBufferAllocationSize = 128u; @@ -1368,22 +1498,35 @@ class UISampleApp final : public examples::SimpleWindowedApplication auto buffer = m_utils->getLogicalDevice()->createBuffer(std::move(mdiCreationParams)); if (!buffer) + { + m_logger->log("Failed to create ImGui streaming buffer object for size=%zu.", ILogger::ELL_WARNING, size); return nullptr; + } buffer->setObjectDebugName("ImGui MDI Upstream Buffer"); auto memoryReqs = buffer->getMemoryReqs(); - memoryReqs.memoryTypeBits &= m_utils->getLogicalDevice()->getPhysicalDevice()->getUpStreamingMemoryTypeBits(); + const auto upStreamingBits = m_utils->getLogicalDevice()->getPhysicalDevice()->getUpStreamingMemoryTypeBits(); + const auto reqMemoryTypeBits = memoryReqs.memoryTypeBits; + memoryReqs.memoryTypeBits &= upStreamingBits; + if (!memoryReqs.memoryTypeBits) + { + m_logger->log("No compatible up-streaming memory type for ImGui buffer size=%zu reqBits=0x%08x upBits=0x%08x.", ILogger::ELL_WARNING, size, reqMemoryTypeBits, upStreamingBits); + return nullptr; + } auto allocation = m_utils->getLogicalDevice()->allocate(memoryReqs, buffer.get(), nbl::ext::imgui::UI::SCachedCreationParams::RequiredAllocateFlags); if (!allocation.isValid()) + { + m_logger->log("Failed to allocate ImGui streaming buffer memory for size=%zu reqBits=0x%08x upBits=0x%08x filteredBits=0x%08x sizeReq=%llu.", ILogger::ELL_WARNING, size, reqMemoryTypeBits, upStreamingBits, memoryReqs.memoryTypeBits, memoryReqs.size); return nullptr; + } auto memory = allocation.memory; if (!memory->map({ 0ull, memoryReqs.size }, getRequiredAccessFlags(memory->getMemoryPropertyFlags()))) { - m_logger->log("Could not map ImGui streaming buffer memory!", ILogger::ELL_ERROR); + m_logger->log("Could not map ImGui streaming buffer memory for size=%zu.", ILogger::ELL_WARNING, size); return nullptr; } @@ -1393,7 +1536,13 @@ class UISampleApp final : public examples::SimpleWindowedApplication minStreamingBufferAllocationSize); }; - auto imguiStreamingBuffer = createImGuiStreamingBuffer(ImGuiStreamingBufferSize); + smart_refctd_ptr imguiStreamingBuffer = nullptr; + for (const auto candidateSize : ImGuiStreamingBufferSizes) + { + imguiStreamingBuffer = createImGuiStreamingBuffer(candidateSize); + if (imguiStreamingBuffer) + break; + } if (!imguiStreamingBuffer) return logFail("Failed to create ImGui streaming buffer."); @@ -1640,6 +1789,8 @@ class UISampleApp final : public examples::SimpleWindowedApplication inline void workLoopBody() override { + paceScriptedVisualDebugFrame(); + // framesInFlight: ensuring safe execution of command buffers and acquires, `framesInFlight` only affect semaphore waits, don't use this to index your resources because it can change with swapchain recreation. const uint32_t framesInFlight = core::min(MaxFramesInFlight, m_surface->getMaxAcquiresInFlight()); // We block for semaphores for 2 reasons here: @@ -1953,6 +2104,36 @@ class UISampleApp final : public examples::SimpleWindowedApplication firstFrame = false; } + inline void paceScriptedVisualDebugFrame() + { + if (!(m_scriptedInput.enabled && m_scriptedInput.visualDebug)) + { + m_scriptedInput.framePacerInitialized = 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.framePacerInitialized) + { + m_scriptedInput.framePacerInitialized = true; + m_scriptedInput.framePacerNext = now + frameDuration; + return; + } + + if (now < m_scriptedInput.framePacerNext) + std::this_thread::sleep_until(m_scriptedInput.framePacerNext); + + auto postSleepNow = std::chrono::steady_clock::now(); + while (m_scriptedInput.framePacerNext < postSleepNow) + m_scriptedInput.framePacerNext += frameDuration; + } + inline bool keepRunning() override { if (m_scriptedInput.enabled && m_scriptedInput.hardFail && m_scriptedInput.failed) @@ -1960,6 +2141,16 @@ class UISampleApp final : public examples::SimpleWindowedApplication if (!m_ciMode || m_ciScreenshotDone) std::exit(EXIT_FAILURE); } + if (m_ciMode && m_ciStartedAt != clock_t::time_point::min()) + { + const auto elapsed = clock_t::now() - m_ciStartedAt; + if (elapsed > 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_ciMode && m_ciScreenshotDone) { if (m_scriptedInput.enabled && m_scriptedInput.nextCaptureIndex < m_scriptedInput.captureFrames.size()) @@ -2119,6 +2310,9 @@ class UISampleApp final : public examples::SimpleWindowedApplication auto& binding = windowBindings[activeRenderWindowIx]; binding.activePlanarIx = static_cast(action.value); binding.pickDefaultProjections(m_planarProjections[binding.activePlanarIx]->getPlanarProjections()); + m_scriptedInput.visualActivePlanarValid = true; + m_scriptedInput.visualActivePlanarIx = binding.activePlanarIx; + m_scriptedInput.visualActivePlanarStartFrame = m_realFrameIx; } break; case ScriptedInputEvent::ActionData::Kind::SetProjectionType: @@ -2185,6 +2379,16 @@ class UISampleApp final : public examples::SimpleWindowedApplication m_logger->log("[script] frame %llu actions=%zu", ILogger::ELL_INFO, static_cast(m_realFrameIx), scriptedActions.size()); } + if (m_scriptedInput.enabled && m_scriptedInput.visualDebug && !m_scriptedInput.visualActivePlanarValid) + { + if (activeRenderWindowIx < windowBindings.size()) + { + m_scriptedInput.visualActivePlanarValid = true; + m_scriptedInput.visualActivePlanarIx = windowBindings[activeRenderWindowIx].activePlanarIx; + m_scriptedInput.visualActivePlanarStartFrame = m_realFrameIx; + } + } + if (!scriptedMouse.empty()) capturedEvents.mouse.insert(capturedEvents.mouse.end(), scriptedMouse.begin(), scriptedMouse.end()); if (!scriptedKeyboard.empty()) @@ -2420,6 +2624,12 @@ class UISampleApp final : public examples::SimpleWindowedApplication { return std::isfinite(v.x) && std::isfinite(v.y) && std::isfinite(v.z); }; + auto setStepReference = [&](const float32_t3& newPos, const float32_t3& newEuler) -> void + { + m_scriptedInput.stepValid = true; + m_scriptedInput.stepPos = newPos; + m_scriptedInput.stepEulerDeg = newEuler; + }; const auto frame = m_realFrameIx; while (m_scriptedInput.nextCheckIndex < m_scriptedInput.checks.size() && @@ -2450,6 +2660,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication m_scriptedInput.baselineValid = true; m_scriptedInput.baselinePos = pos; m_scriptedInput.baselineEulerDeg = euler; + setStepReference(pos, euler); logPass("[script][pass] baseline frame=%llu pos=(%.3f, %.3f, %.3f) euler_deg=(%.3f, %.3f, %.3f)", static_cast(frame), pos.x, pos.y, pos.z, euler.x, euler.y, euler.z); @@ -2552,6 +2763,53 @@ class UISampleApp final : public examples::SimpleWindowedApplication } } } + else if (check.kind == ScriptedInputCheck::Kind::GimbalStep) + { + if (!m_scriptedInput.stepValid) + { + if (m_scriptedInput.baselineValid) + setStepReference(m_scriptedInput.baselinePos, m_scriptedInput.baselineEulerDeg); + else + { + logFail("[script][fail] gimbal_step frame=%llu missing step reference", static_cast(frame)); + setStepReference(pos, euler); + ++m_scriptedInput.nextCheckIndex; + continue; + } + } + + const auto diff = float32_t3(pos.x - m_scriptedInput.stepPos.x, pos.y - m_scriptedInput.stepPos.y, pos.z - m_scriptedInput.stepPos.z); + const auto dpos = std::sqrt(diff.x * diff.x + diff.y * diff.y + diff.z * diff.z); + const auto dx = angleDiffDeg(euler.x, m_scriptedInput.stepEulerDeg.x); + const auto dy = angleDiffDeg(euler.y, m_scriptedInput.stepEulerDeg.y); + const auto dz = angleDiffDeg(euler.z, m_scriptedInput.stepEulerDeg.z); + const auto dmax = std::max(dx, std::max(dy, dz)); + + bool ok = true; + if (check.hasPosDeltaConstraint) + { + if (dpos < check.minPosDelta || dpos > check.posTolerance) + { + ok = false; + logFail("[script][fail] gimbal_step frame=%llu pos_delta=%.6f expected=[%.6f, %.6f]", + static_cast(frame), dpos, check.minPosDelta, check.posTolerance); + } + } + if (check.hasEulerDeltaConstraint) + { + if (dmax < check.minEulerDeltaDeg || dmax > check.eulerToleranceDeg) + { + ok = false; + logFail("[script][fail] gimbal_step frame=%llu euler_delta=%.6f expected=[%.6f, %.6f]", + static_cast(frame), dmax, check.minEulerDeltaDeg, check.eulerToleranceDeg); + } + } + + if (ok) + logPass("[script][pass] gimbal_step frame=%llu pos_delta=%.6f euler_delta=%.6f", + static_cast(frame), dpos, dmax); + setStepReference(pos, euler); + } ++m_scriptedInput.nextCheckIndex; } @@ -2669,27 +2927,17 @@ class UISampleApp final : public examples::SimpleWindowedApplication inline bool isOrbitLikeCamera(ICamera* camera) { - return dynamic_cast(camera) || dynamic_cast(camera) || dynamic_cast(camera); + return dynamic_cast(camera); } template inline bool withOrbitLikeCamera(ICamera* camera, Fn&& fn) { - if (auto* orbit = dynamic_cast(camera)) + if (auto* orbit = dynamic_cast(camera)) { fn(orbit); return true; } - if (auto* arcball = dynamic_cast(camera)) - { - fn(arcball); - return true; - } - if (auto* turntable = dynamic_cast(camera)) - { - fn(turntable); - return true; - } return false; } @@ -2705,6 +2953,18 @@ class UISampleApp final : public examples::SimpleWindowedApplication return "Arcball"; if (dynamic_cast(camera)) return "Turntable"; + if (dynamic_cast(camera)) + return "TopDown"; + if (dynamic_cast(camera)) + return "Isometric"; + if (dynamic_cast(camera)) + return "Chase"; + if (dynamic_cast(camera)) + return "Dolly"; + if (dynamic_cast(camera)) + return "Dolly Zoom"; + if (dynamic_cast(camera)) + return "Path"; return "Unknown"; } @@ -2720,9 +2980,435 @@ class UISampleApp final : public examples::SimpleWindowedApplication return "Arcball trackball around target"; if (dynamic_cast(camera)) return "Turntable yaw/pitch around target"; + if (dynamic_cast(camera)) + return "Fixed pitch top-down pan"; + if (dynamic_cast(camera)) + return "Fixed isometric view with pan"; + if (dynamic_cast(camera)) + return "Target follow with chase controls"; + if (dynamic_cast(camera)) + return "Rig truck/dolly with look-at"; + if (dynamic_cast(camera)) + return "Orbit with dolly-zoom FOV"; + if (dynamic_cast(camera)) + return "Move along a target path"; return "Unspecified camera behavior"; } + inline void syncVisualDebugWindowBindings() + { + if (!(m_scriptedInput.enabled && m_scriptedInput.visualDebug)) + return; + if (windowBindings.size() < 2u || m_planarProjections.empty()) + return; + + auto& perspectiveBinding = windowBindings[0u]; + if (perspectiveBinding.activePlanarIx >= m_planarProjections.size()) + return; + auto& perspectivePlanar = m_planarProjections[perspectiveBinding.activePlanarIx]; + if (!perspectivePlanar) + return; + if (!perspectiveBinding.lastBoundPerspectivePresetProjectionIx.has_value()) + perspectiveBinding.pickDefaultProjections(perspectivePlanar->getPlanarProjections()); + if (perspectiveBinding.lastBoundPerspectivePresetProjectionIx.has_value()) + perspectiveBinding.boundProjectionIx = perspectiveBinding.lastBoundPerspectivePresetProjectionIx.value(); + + auto& orthoBinding = windowBindings[1u]; + if (orthoBinding.activePlanarIx != perspectiveBinding.activePlanarIx) + { + orthoBinding.activePlanarIx = perspectiveBinding.activePlanarIx; + auto& orthoPlanar = m_planarProjections[orthoBinding.activePlanarIx]; + if (!orthoPlanar) + return; + orthoBinding.pickDefaultProjections(orthoPlanar->getPlanarProjections()); + } + if (orthoBinding.activePlanarIx >= m_planarProjections.size()) + return; + auto& orthoPlanar = m_planarProjections[orthoBinding.activePlanarIx]; + if (!orthoPlanar) + return; + if (!orthoBinding.lastBoundOrthoPresetProjectionIx.has_value()) + orthoBinding.pickDefaultProjections(orthoPlanar->getPlanarProjections()); + if (orthoBinding.lastBoundOrthoPresetProjectionIx.has_value()) + orthoBinding.boundProjectionIx = orthoBinding.lastBoundOrthoPresetProjectionIx.value(); + } + + inline bool projectWorldPointToViewport( + const float32_t4x4& viewProjMatrix, + const float32_t3& worldPoint, + const ImVec2& viewportPos, + const ImVec2& viewportSize, + ImVec2& outScreen) const + { + if (viewportSize.x <= 1.0f || viewportSize.y <= 1.0f) + return false; + + const auto clip = mul(viewProjMatrix, float32_t4(worldPoint.x, worldPoint.y, worldPoint.z, 1.0f)); + if (!std::isfinite(clip.x) || !std::isfinite(clip.y) || !std::isfinite(clip.z) || !std::isfinite(clip.w)) + return false; + + const float absW = std::abs(clip.w); + if (absW < 1e-5f) + return false; + + const float invW = 1.0f / clip.w; + const float ndcX = clip.x * invW; + const float ndcY = clip.y * invW; + const float ndcZ = clip.z * invW; + + if (!std::isfinite(ndcX) || !std::isfinite(ndcY) || !std::isfinite(ndcZ)) + return false; + if (std::abs(ndcX) > 100.0f || std::abs(ndcY) > 100.0f || std::abs(ndcZ) > 100.0f) + return false; + + outScreen.x = viewportPos.x + (ndcX * 0.5f + 0.5f) * viewportSize.x; + outScreen.y = viewportPos.y + (-ndcY * 0.5f + 0.5f) * viewportSize.y; + return std::isfinite(outScreen.x) && std::isfinite(outScreen.y); + } + + inline void drawWorldReferenceOverlay( + const ImVec2& viewportPos, + const ImVec2& viewportSize, + const float32_t4x4& viewMatrix, + const float32_t4x4& projectionMatrix, + bool leftHandedProjection, + float nearPlane, + float farPlane) + { + if (!(m_scriptedInput.enabled && m_scriptedInput.visualDebug)) + return; + if (viewportSize.x <= 1.0f || viewportSize.y <= 1.0f) + return; + + auto* drawList = ImGui::GetWindowDrawList(); + if (!drawList) + return; + + const float safeNear = std::max(nearPlane, 0.001f); + const float safeFar = std::max(farPlane, safeNear + 0.001f); + const auto depthOfViewPoint = [&](const float32_t4& viewPoint) -> float + { + return leftHandedProjection ? viewPoint.z : -viewPoint.z; + }; + const auto ndcToViewport = [&](const ImVec2& ndc) -> ImVec2 + { + return ImVec2( + viewportPos.x + (ndc.x * 0.5f + 0.5f) * viewportSize.x, + viewportPos.y + (-ndc.y * 0.5f + 0.5f) * viewportSize.y); + }; + const auto clipSegmentByDepthRange = [&](float32_t4& viewA, float32_t4& viewB) -> bool + { + const float32_t4 a0 = viewA; + const float32_t4 b0 = viewB; + const float32_t4 delta = b0 - a0; + const float depthA = depthOfViewPoint(a0); + const float depthB = depthOfViewPoint(b0); + + float tEnter = 0.0f; + float tExit = 1.0f; + const auto clipByConstraint = [&](float fa, float fb) -> bool + { + if (fa < 0.0f && fb < 0.0f) + return false; + if (fa >= 0.0f && fb >= 0.0f) + return true; + + const float denom = fa - fb; + if (std::abs(denom) < 1e-6f) + return false; + const float t = std::clamp(fa / denom, 0.0f, 1.0f); + + if (fa < 0.0f) + tEnter = std::max(tEnter, t); + else + tExit = std::min(tExit, t); + + return tEnter <= tExit; + }; + + if (!clipByConstraint(depthA - safeNear, depthB - safeNear)) + return false; + if (!clipByConstraint(safeFar - depthA, safeFar - depthB)) + return false; + + viewA = a0 + delta * tEnter; + viewB = a0 + delta * tExit; + return true; + }; + const auto projectViewPointToNdc = [&](const float32_t4& viewPoint, ImVec2& outNdc) -> bool + { + const auto clip = mul(projectionMatrix, viewPoint); + if (!std::isfinite(clip.x) || !std::isfinite(clip.y) || !std::isfinite(clip.z) || !std::isfinite(clip.w)) + return false; + + const float absW = std::abs(clip.w); + if (absW < 1e-6f) + return false; + + const float invW = 1.0f / clip.w; + const float ndcX = clip.x * invW; + const float ndcY = clip.y * invW; + const float ndcZ = clip.z * invW; + if (!std::isfinite(ndcX) || !std::isfinite(ndcY) || !std::isfinite(ndcZ)) + return false; + if (std::abs(ndcX) > 1e4f || std::abs(ndcY) > 1e4f || std::abs(ndcZ) > 1e4f) + return false; + + outNdc = ImVec2(ndcX, ndcY); + return true; + }; + const auto clipNdcSegmentToViewport = [&](ImVec2& ndcA, ImVec2& ndcB) -> bool + { + float tEnter = 0.0f; + float tExit = 1.0f; + const float dx = ndcB.x - ndcA.x; + const float dy = ndcB.y - ndcA.y; + const auto clipTest = [&](float p, float q) -> bool + { + if (std::abs(p) < 1e-6f) + return q >= 0.0f; + + const float r = q / p; + if (p < 0.0f) + { + if (r > tExit) + return false; + tEnter = std::max(tEnter, r); + } + else + { + if (r < tEnter) + return false; + tExit = std::min(tExit, r); + } + return tEnter <= tExit; + }; + + if (!clipTest(-dx, ndcA.x + 1.0f)) + return false; + if (!clipTest(dx, 1.0f - ndcA.x)) + return false; + if (!clipTest(-dy, ndcA.y + 1.0f)) + return false; + if (!clipTest(dy, 1.0f - ndcA.y)) + return false; + + const ImVec2 a0 = ndcA; + ndcA = ImVec2(a0.x + dx * tEnter, a0.y + dy * tEnter); + ndcB = ImVec2(a0.x + dx * tExit, a0.y + dy * tExit); + return true; + }; + const auto projectWorldPointToViewportClipped = [&](const float32_t3& worldPoint, ImVec2& outScreen) -> bool + { + const auto viewPoint = mul(viewMatrix, float32_t4(worldPoint.x, worldPoint.y, worldPoint.z, 1.0f)); + if (!std::isfinite(viewPoint.x) || !std::isfinite(viewPoint.y) || !std::isfinite(viewPoint.z) || !std::isfinite(viewPoint.w)) + return false; + + const float depth = depthOfViewPoint(viewPoint); + if (depth < safeNear || depth > safeFar) + return false; + + ImVec2 ndcPoint = {}; + if (!projectViewPointToNdc(viewPoint, ndcPoint)) + return false; + if (ndcPoint.x < -1.0f || ndcPoint.x > 1.0f || ndcPoint.y < -1.0f || ndcPoint.y > 1.0f) + return false; + + outScreen = ndcToViewport(ndcPoint); + return std::isfinite(outScreen.x) && std::isfinite(outScreen.y); + }; + + auto drawWorldLine = [&](const float32_t3& aWorld, const float32_t3& bWorld, ImU32 color, float thickness) -> void + { + float32_t4 viewA = mul(viewMatrix, float32_t4(aWorld.x, aWorld.y, aWorld.z, 1.0f)); + float32_t4 viewB = mul(viewMatrix, float32_t4(bWorld.x, bWorld.y, bWorld.z, 1.0f)); + if (!std::isfinite(viewA.x) || !std::isfinite(viewA.y) || !std::isfinite(viewA.z) || !std::isfinite(viewA.w) || + !std::isfinite(viewB.x) || !std::isfinite(viewB.y) || !std::isfinite(viewB.z) || !std::isfinite(viewB.w)) + return; + if (!clipSegmentByDepthRange(viewA, viewB)) + return; + + ImVec2 ndcA = {}; + ImVec2 ndcB = {}; + if (!projectViewPointToNdc(viewA, ndcA)) + return; + if (!projectViewPointToNdc(viewB, ndcB)) + return; + if (!clipNdcSegmentToViewport(ndcA, ndcB)) + return; + + const ImVec2 screenA = ndcToViewport(ndcA); + const ImVec2 screenB = ndcToViewport(ndcB); + drawList->AddLine(screenA, screenB, color, thickness); + }; + + constexpr int gridHalfSteps = 8; + constexpr float gridStep = 2.0f; + const float gridHalfSize = static_cast(gridHalfSteps) * gridStep; + constexpr int gridMajorModulo = 1; + const ImU32 gridMajor = IM_COL32(136, 160, 194, 95); + + for (int i = -gridHalfSteps; i <= gridHalfSteps; ++i) + { + if (i == 0) + continue; + if ((i % gridMajorModulo) != 0) + continue; + const float c = static_cast(i) * gridStep; + drawWorldLine(float32_t3(c, 0.0f, -gridHalfSize), float32_t3(c, 0.0f, gridHalfSize), gridMajor, 1.3f); + drawWorldLine(float32_t3(-gridHalfSize, 0.0f, c), float32_t3(gridHalfSize, 0.0f, c), gridMajor, 1.3f); + } + + drawWorldLine(float32_t3(-gridHalfSize, 0.0f, 0.0f), float32_t3(gridHalfSize, 0.0f, 0.0f), IM_COL32(184, 204, 232, 170), 2.0f); + drawWorldLine(float32_t3(0.0f, 0.0f, -gridHalfSize), float32_t3(0.0f, 0.0f, gridHalfSize), IM_COL32(184, 204, 232, 170), 2.0f); + + constexpr float axisLength = 7.5f; + const float32_t3 origin = float32_t3(0.0f); + const float32_t3 xPos = float32_t3(axisLength, 0.0f, 0.0f); + const float32_t3 yPos = float32_t3(0.0f, axisLength, 0.0f); + const float32_t3 zPos = float32_t3(0.0f, 0.0f, axisLength); + const float32_t3 xNeg = float32_t3(-axisLength * 0.4f, 0.0f, 0.0f); + const float32_t3 yNeg = float32_t3(0.0f, -axisLength * 0.3f, 0.0f); + const float32_t3 zNeg = float32_t3(0.0f, 0.0f, -axisLength * 0.4f); + + drawWorldLine(origin, xPos, IM_COL32(244, 92, 92, 245), 3.2f); + drawWorldLine(origin, yPos, IM_COL32(124, 236, 132, 245), 3.2f); + drawWorldLine(origin, zPos, IM_COL32(106, 166, 255, 245), 3.2f); + drawWorldLine(origin, xNeg, IM_COL32(128, 74, 74, 180), 1.6f); + drawWorldLine(origin, yNeg, IM_COL32(74, 128, 78, 180), 1.6f); + drawWorldLine(origin, zNeg, IM_COL32(70, 88, 124, 180), 1.6f); + + auto drawAxisLabel = [&](const char* label, const float32_t3& worldPoint, ImU32 color) -> void + { + ImVec2 screenPos = {}; + if (!projectWorldPointToViewportClipped(worldPoint, screenPos)) + return; + drawList->AddText(ImVec2(screenPos.x + 4.0f, screenPos.y + 3.0f), color, label); + }; + + ImVec2 originScreen = {}; + if (projectWorldPointToViewportClipped(origin, originScreen)) + drawList->AddCircleFilled(originScreen, 4.0f, IM_COL32(240, 248, 255, 220), 16); + + drawAxisLabel("X", xPos, IM_COL32(255, 152, 152, 255)); + drawAxisLabel("Y", yPos, IM_COL32(172, 255, 178, 255)); + drawAxisLabel("Z", zPos, IM_COL32(172, 210, 255, 255)); + } + + inline void drawScriptVisualDebugOverlay(const ImVec2& displaySize) + { + if (!(m_scriptedInput.enabled && m_scriptedInput.visualDebug)) + return; + if (windowBindings.empty() || m_planarProjections.empty()) + return; + if (activeRenderWindowIx >= windowBindings.size()) + return; + + const auto& binding = windowBindings[activeRenderWindowIx]; + if (binding.activePlanarIx >= m_planarProjections.size()) + return; + + auto& planar = m_planarProjections[binding.activePlanarIx]; + if (!planar) + return; + auto* camera = planar->getCamera(); + if (!camera) + return; + + if (!m_scriptedInput.visualActivePlanarValid) + { + m_scriptedInput.visualActivePlanarValid = true; + m_scriptedInput.visualActivePlanarIx = binding.activePlanarIx; + m_scriptedInput.visualActivePlanarStartFrame = m_realFrameIx; + } + + const uint64_t elapsedFrames = (m_realFrameIx >= m_scriptedInput.visualActivePlanarStartFrame) ? + (m_realFrameIx - m_scriptedInput.visualActivePlanarStartFrame) : 0ull; + const float fps = std::max(1.f, m_scriptedInput.visualTargetFps); + const uint64_t holdFrames = static_cast(std::round(std::max(0.f, m_scriptedInput.visualCameraHoldSeconds) * fps)); + const uint64_t progressFrames = holdFrames ? std::min(elapsedFrames, holdFrames) : elapsedFrames; + + const auto cameraLabel = getCameraTypeLabel(camera); + std::string lineTop = "SCRIPT VISUAL DEBUG"; + std::string lineMid = "Camera " + std::to_string(binding.activePlanarIx + 1u) + "/" + std::to_string(m_planarProjections.size()) + " " + std::string(cameraLabel); + + char lineBottomBuffer[256] = {}; + if (holdFrames) + { + const double elapsedSeconds = static_cast(progressFrames) / static_cast(fps); + const double holdSeconds = static_cast(holdFrames) / static_cast(fps); + std::snprintf( + lineBottomBuffer, + sizeof(lineBottomBuffer), + "Planar %u Segment %.1f/%.1f s Frame %llu/%llu", + binding.activePlanarIx, + elapsedSeconds, + holdSeconds, + static_cast(progressFrames), + static_cast(holdFrames)); + } + else + { + std::snprintf( + lineBottomBuffer, + sizeof(lineBottomBuffer), + "Planar %u Frame %llu", + binding.activePlanarIx, + static_cast(m_realFrameIx)); + } + const std::string lineBottom(lineBottomBuffer); + + const float topSize = 50.f; + const float midSize = 38.f; + const float bottomSize = 28.f; + const float marginTop = 18.f; + const float padX = 24.f; + const float padY = 16.f; + const float gap = 6.f; + + ImFont* font = ImGui::GetFont(); + if (!font) + return; + + const float textWrap = std::numeric_limits::max(); + const ImVec2 topTextSize = font->CalcTextSizeA(topSize, textWrap, 0.0f, lineTop.c_str()); + const ImVec2 midTextSize = font->CalcTextSizeA(midSize, textWrap, 0.0f, lineMid.c_str()); + const ImVec2 bottomTextSize = font->CalcTextSizeA(bottomSize, textWrap, 0.0f, lineBottom.c_str()); + const float panelWidth = std::max(topTextSize.x, std::max(midTextSize.x, bottomTextSize.x)) + padX * 2.0f; + const float panelHeight = topTextSize.y + midTextSize.y + bottomTextSize.y + gap * 2.0f + padY * 2.0f; + const ImVec2 panelMin((displaySize.x - panelWidth) * 0.5f, marginTop); + const ImVec2 panelMax(panelMin.x + panelWidth, panelMin.y + panelHeight); + + auto* drawList = ImGui::GetForegroundDrawList(); + if (!drawList) + return; + + drawList->AddRectFilled(panelMin, panelMax, IM_COL32(6, 8, 12, 232), 14.0f); + drawList->AddRect(panelMin, panelMax, IM_COL32(255, 166, 64, 255), 14.0f, 0, 2.5f); + + const float topX = panelMin.x + (panelWidth - topTextSize.x) * 0.5f; + const float midX = panelMin.x + (panelWidth - midTextSize.x) * 0.5f; + const float bottomX = panelMin.x + (panelWidth - bottomTextSize.x) * 0.5f; + const float topY = panelMin.y + padY; + const float midY = topY + topTextSize.y + gap; + const float bottomY = midY + midTextSize.y + gap; + + drawList->AddText(font, topSize, ImVec2(topX, topY), IM_COL32(255, 206, 120, 255), lineTop.c_str()); + drawList->AddText(font, midSize, ImVec2(midX, midY), IM_COL32(255, 244, 224, 255), lineMid.c_str()); + drawList->AddText(font, bottomSize, ImVec2(bottomX, bottomY), IM_COL32(202, 222, 255, 255), lineBottom.c_str()); + } + + inline void applyDollyZoomProjection(ICamera* camera, IPlanarProjection::CProjection& projection) + { + auto* dolly = dynamic_cast(camera); + if (!dolly) + return; + const auto& params = projection.getParameters(); + if (params.m_type != IPlanarProjection::CProjection::Perspective) + return; + projection.setPerspective(params.m_zNear, params.m_zFar, dolly->computeDollyFov()); + } + inline CameraPreset capturePreset(ICamera* camera, const std::string& name) { CameraPreset preset; @@ -2777,7 +3463,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication 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, + auto line = m_logFormatter->format(ILogger::ELL_INFO, "virtual frame=%llu src=%s ctrl=%s cam=%s planar=%u event=%s mag=%.6f", static_cast(m_realFrameIx), sourceStr.c_str(), @@ -3149,7 +3835,9 @@ class UISampleApp final : public examples::SimpleWindowedApplication // render bound planar camera views onto GUI windows if (useWindow) { - if(enableActiveCameraMovement) + syncVisualDebugWindowBindings(); + const bool hideSceneGizmos = enableActiveCameraMovement || (m_scriptedInput.enabled && m_scriptedInput.visualDebug); + if(hideSceneGizmos) ImGuizmo::Enable(false); else ImGuizmo::Enable(true); @@ -3200,6 +3888,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication assert(binding.boundProjectionIx.has_value()); auto& projection = planarBound->getPlanarProjections()[binding.boundProjectionIx.value()]; + applyDollyZoomProjection(planarViewCameraBound, projection); projection.update(binding.leftHandedProjection, binding.aspectRatio); // TODO: @@ -3253,6 +3942,10 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGuizmoPlanarM16InOut imguizmoPlanar; imguizmoPlanar.view = getCastedMatrix(hlsl::transpose(getMatrix3x4As4x4(planarViewCameraBound->getGimbal().getViewMatrix()))); imguizmoPlanar.projection = getCastedMatrix(hlsl::transpose(projection.getProjectionMatrix())); + const auto viewMatrix = getMatrix3x4As4x4(getCastedMatrix(planarViewCameraBound->getGimbal().getViewMatrix())); + const auto projectionMatrix = getCastedMatrix(projection.getProjectionMatrix()); + const auto& projectionParams = projection.getParameters(); + drawWorldReferenceOverlay(cursorPos, contentRegionSize, viewMatrix, projectionMatrix, binding.leftHandedProjection, projectionParams.m_zNear, projectionParams.m_zFar); if (flipGizmoY) // note we allow to flip gizmo just to match our coordinates imguizmoPlanar.projection[1][1] *= -1.f; // https://johannesugb.github.io/gpu-programming/why-do-opengl-proj-matrices-fail-in-vulkan/ @@ -3265,41 +3958,59 @@ class UISampleApp final : public examples::SimpleWindowedApplication 0.f, 0.f, 0.f, 1.f }; - if(binding.enableDebugGridDraw) + if(!hideSceneGizmos && binding.enableDebugGridDraw) ImGuizmo::DrawGrid(&imguizmoPlanar.view[0][0], &imguizmoPlanar.projection[0][0], identityMatrix, 100.f); - for (uint32_t modelIx = 0; modelIx < 1u + m_planarProjections.size(); modelIx++) + if (!hideSceneGizmos) { - ImGuizmo::PushID(gizmoIx); ++gizmoIx; + for (uint32_t modelIx = 0; modelIx < 1u + m_planarProjections.size(); modelIx++) + { + ImGuizmo::PushID(gizmoIx); ++gizmoIx; - const bool isCameraGimbalTarget = modelIx; // I assume scene demo model is 0th ix, left are planar cameras - ICamera* const targetGimbalManipulationCamera = isCameraGimbalTarget ? m_planarProjections[modelIx - 1u]->getCamera() : nullptr; + const bool isCameraGimbalTarget = modelIx; // I assume scene demo model is 0th ix, left are planar cameras + ICamera* const targetGimbalManipulationCamera = isCameraGimbalTarget ? m_planarProjections[modelIx - 1u]->getCamera() : nullptr; // if we try to manipulate a camera which appears to be the same camera we see scene from then obvsly it doesn't make sense to manipulate its gizmo so we skip it // EDIT: it actually makes some sense if you assume render planar view is rendered with ortho projection, but we would need to add imguizmo controller virtual map // to ban forward/backward in this mode if this condition is true - if (targetGimbalManipulationCamera == planarViewCameraBound) - { - ImGuizmo::PopID(); - continue; - } + if (targetGimbalManipulationCamera == planarViewCameraBound) + { + ImGuizmo::PopID(); + continue; + } - ImGuizmoModelM16InOut imguizmoModel; + ImGuizmoModelM16InOut imguizmoModel; - if (isCameraGimbalTarget) - { - assert(targetGimbalManipulationCamera); - imguizmoModel.inTRS = getCastedMatrix(targetGimbalManipulationCamera->getGimbal().template operator() < float64_t4x4 > ()); - } - else - imguizmoModel.inTRS = hlsl::transpose(getMatrix3x4As4x4(m_model)); + if (isCameraGimbalTarget) + { + assert(targetGimbalManipulationCamera); + imguizmoModel.inTRS = getCastedMatrix(targetGimbalManipulationCamera->getGimbal().template operator() < float64_t4x4 > ()); + } + else + imguizmoModel.inTRS = hlsl::transpose(getMatrix3x4As4x4(m_model)); - float gizmoSizeClip = 0.1f; - ImGuizmo::SetGizmoSizeClipSpace(gizmoSizeClip); + const float gizmoWorldRadius = 0.22f; + float32_t3 gizmoWorldPos = {}; + if (isCameraGimbalTarget) + gizmoWorldPos = getCastedVector(targetGimbalManipulationCamera->getGimbal().getPosition()); + else + { + const auto modelPos = hlsl::transpose(getMatrix3x4As4x4(m_model))[3]; + gizmoWorldPos = float32_t3(modelPos.x, modelPos.y, modelPos.z); + } - imguizmoModel.outTRS = imguizmoModel.inTRS; - { - const bool success = ImGuizmo::Manipulate(&imguizmoPlanar.view[0][0], &imguizmoPlanar.projection[0][0], ImGuizmo::OPERATION::UNIVERSAL, mCurrentGizmoMode, &imguizmoModel.outTRS[0][0], &imguizmoModel.outDeltaTRS[0][0], useSnap ? &snap[0] : nullptr); + const auto viewPos = mul(viewMatrix, float32_t4(gizmoWorldPos, 1.0f)); + const float depth = std::max(0.001f, std::abs(viewPos.z)); + float gizmoSizeClip = 0.1f; + if (projection.getParameters().m_type == IPlanarProjection::CProjection::Perspective) + gizmoSizeClip = (gizmoWorldRadius * projectionMatrix[1][1]) / depth; + else + gizmoSizeClip = gizmoWorldRadius * projectionMatrix[1][1]; + ImGuizmo::SetGizmoSizeClipSpace(gizmoSizeClip); + + imguizmoModel.outTRS = imguizmoModel.inTRS; + { + const bool success = ImGuizmo::Manipulate(&imguizmoPlanar.view[0][0], &imguizmoPlanar.projection[0][0], ImGuizmo::OPERATION::UNIVERSAL, mCurrentGizmoMode, &imguizmoModel.outTRS[0][0], &imguizmoModel.outDeltaTRS[0][0], useSnap ? &snap[0] : nullptr); if (success) { @@ -3431,8 +4142,8 @@ class UISampleApp final : public examples::SimpleWindowedApplication } } - if (ImGuizmo::IsOver() and not ImGuizmo::IsUsingAny() && not enableActiveCameraMovement) - { + if (ImGuizmo::IsOver() and not ImGuizmo::IsUsingAny() && not enableActiveCameraMovement) + { if (targetGimbalManipulationCamera && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) { const uint32_t newPlanarIx = modelIx - 1u; @@ -3479,9 +4190,10 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGui::PopStyleVar(); ImGui::PopStyleColor(2); + } } + ImGuizmo::PopID(); } - ImGuizmo::PopID(); } ImGui::End(); @@ -3527,6 +4239,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication assert(binding.boundProjectionIx.has_value()); auto& projection = planarBound->getPlanarProjections()[binding.boundProjectionIx.value()]; + applyDollyZoomProjection(planarViewCameraBound, projection); projection.update(binding.leftHandedProjection, binding.aspectRatio); } @@ -3538,6 +4251,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication } } + drawScriptVisualDebugOverlay(io.DisplaySize); DrawControlPanel(); UpdateBoundCameraMovement(); UpdateCursorVisibility(); @@ -3554,6 +4268,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication assert(binding.boundProjectionIx.has_value()); auto& projection = planarBound->getPlanarProjections()[binding.boundProjectionIx.value()]; + applyDollyZoomProjection(boundPlanarCamera, projection); projection.update(binding.leftHandedProjection, binding.aspectRatio); auto viewMatrix = getCastedMatrix(boundPlanarCamera->getGimbal().getViewMatrix()); @@ -3567,6 +4282,17 @@ class UISampleApp final : public examples::SimpleWindowedApplication } + inline bool shouldCaptureOSCursor() + { + if (!enableActiveCameraMovement || !captureCursorInMoveMode) + return false; + if (m_ciMode || m_scriptedInput.enabled) + return false; + if (!m_window || !m_window->hasInputFocus() || !m_window->hasMouseFocus()) + return false; + return true; + } + inline void UpdateBoundCameraMovement() { ImGuiIO& io = ImGui::GetIO(); @@ -3580,49 +4306,45 @@ class UISampleApp final : public examples::SimpleWindowedApplication io.MouseDrawCursor = false; io.WantCaptureMouse = false; - ImVec2 viewportSize = io.DisplaySize; - auto* cc = m_window->getCursorControl(); - int32_t posX = m_window->getX(); - int32_t posY = m_window->getY(); - - if (resetCursorToCenter) + if (shouldCaptureOSCursor()) { - const ICursorControl::SPosition middle{ static_cast(viewportSize.x / 2 + posX), static_cast(viewportSize.y / 2 + posY) }; - cc->setPosition(middle); - } - else - { - auto currentCursorPos = cc->getPosition(); - ICursorControl::SPosition newPos{}; - newPos.x = std::clamp(currentCursorPos.x, posX, viewportSize.x + posX); - newPos.y = std::clamp(currentCursorPos.y, posY, viewportSize.y + posY); - cc->setPosition(newPos); + ImVec2 viewportSize = io.DisplaySize; + auto* cc = m_window->getCursorControl(); + if (cc) + { + int32_t posX = m_window->getX(); + int32_t posY = m_window->getY(); + + if (resetCursorToCenter) + { + const ICursorControl::SPosition middle{ static_cast(viewportSize.x / 2 + posX), static_cast(viewportSize.y / 2 + posY) }; + cc->setPosition(middle); + } + else + { + auto currentCursorPos = cc->getPosition(); + ICursorControl::SPosition newPos{}; + newPos.x = std::clamp(currentCursorPos.x, posX, viewportSize.x + posX); + newPos.y = std::clamp(currentCursorPos.y, posY, viewportSize.y + posY); + cc->setPosition(newPos); + } + } } } else { io.ConfigFlags &= ~ImGuiConfigFlags_NoMouse; - io.MouseDrawCursor = true; + io.MouseDrawCursor = false; io.WantCaptureMouse = true; } } inline void UpdateCursorVisibility() { - ImGuiIO& io = ImGui::GetIO(); - ImVec2 mousePos = ImGui::GetMousePos(); - ImVec2 viewportSize = io.DisplaySize; - auto* cc = m_window->getCursorControl(); - - if (mousePos.x < 0.0f || mousePos.y < 0.0f || mousePos.x > viewportSize.x || mousePos.y > viewportSize.y) - { - if (not enableActiveCameraMovement) - cc->setVisible(true); - } - else - { - cc->setVisible(false); - } + auto* cc = m_window ? m_window->getCursorControl() : nullptr; + if (!cc) + return; + cc->setVisible(!shouldCaptureOSCursor()); } inline void UpdateUiMetrics() @@ -4238,10 +4960,19 @@ class UISampleApp final : public examples::SimpleWindowedApplication DrawSectionHeader("CursorHeader", "Cursor Behaviour", accent); if (ImGui::TreeNodeEx("Cursor Behaviour")) { - if (ImGui::RadioButton("Clamp to the window", !resetCursorToCenter)) - resetCursorToCenter = false; - if (ImGui::RadioButton("Reset to the window center", resetCursorToCenter)) - resetCursorToCenter = true; + ImGui::Checkbox("Capture OS cursor in move mode", &captureCursorInMoveMode); + DrawHoverHint("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(); } @@ -4260,10 +4991,8 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGui::Text("Object Ix: %s", std::to_string(active.activePlanarIx + 1u).c_str()); ImGui::Separator(); { - auto* orbit = dynamic_cast(boundCamera); - auto* arcball = dynamic_cast(boundCamera); - auto* turntable = dynamic_cast(boundCamera); - const bool isOrbitLike = orbit || arcball || turntable; + auto* orbit = dynamic_cast(boundCamera); + const bool isOrbitLike = orbit != nullptr; float moveSpeed = boundCamera->getMoveSpeedScale(); float rotationSpeed = boundCamera->getRotationSpeedScale(); @@ -4271,7 +5000,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication ImGui::SliderFloat("Move speed factor", &moveSpeed, 0.0001f, 10.f, "%.4f", ImGuiSliderFlags_Logarithmic); DrawHoverHint("Scale translation speed for this camera"); - if (not orbit) + if (boundCamera->getAllowedVirtualEvents() & CVirtualGimbalEvent::Rotate) ImGui::SliderFloat("Rotate speed factor", &rotationSpeed, 0.0001f, 10.f, "%.4f", ImGuiSliderFlags_Logarithmic); DrawHoverHint("Scale rotation speed for this camera"); @@ -4280,20 +5009,10 @@ class UISampleApp final : public examples::SimpleWindowedApplication if (isOrbitLike) { - auto applyDistance = [&](auto* cam) - { - float distance = cam->getDistance(); - ImGui::SliderFloat("Distance", &distance, cam->MinDistance, cam->MaxDistance, "%.4f", ImGuiSliderFlags_Logarithmic); - DrawHoverHint("Current orbit distance"); - cam->setDistance(distance); - }; - - if (orbit) - applyDistance(orbit); - else if (arcball) - applyDistance(arcball); - else if (turntable) - applyDistance(turntable); + float distance = orbit->getDistance(); + ImGui::SliderFloat("Distance", &distance, orbit->MinDistance, orbit->MaxDistance, "%.4f", ImGuiSliderFlags_Logarithmic); + DrawHoverHint("Current orbit distance"); + orbit->setDistance(distance); } } @@ -4907,6 +5626,7 @@ class UISampleApp final : public examples::SimpleWindowedApplication std::vector> m_planarProjections; bool enableActiveCameraMovement = false; + bool captureCursorInMoveMode = false; bool resetCursorToCenter = true; @@ -5009,7 +5729,8 @@ class UISampleApp final : public examples::SimpleWindowedApplication Baseline, ImguizmoVirtual, GimbalNear, - GimbalDelta + GimbalDelta, + GimbalStep }; struct ExpectedVirtualEvent @@ -5029,6 +5750,10 @@ class UISampleApp final : public examples::SimpleWindowedApplication bool hasExpectedEuler = false; float posTolerance = 0.05f; float eulerToleranceDeg = 1.0f; + float minPosDelta = 0.0f; + float minEulerDeltaDeg = 0.0f; + bool hasPosDeltaConstraint = false; + bool hasEulerDeltaConstraint = false; }; struct ScriptedInputState @@ -5037,6 +5762,9 @@ class UISampleApp final : public examples::SimpleWindowedApplication bool log = false; bool exclusive = false; bool hardFail = false; + bool visualDebug = false; + float visualTargetFps = 0.f; + float visualCameraHoldSeconds = 0.f; std::vector events; size_t nextEventIndex = 0; std::vector checks; @@ -5050,6 +5778,14 @@ class UISampleApp final : public examples::SimpleWindowedApplication bool baselineValid = false; float32_t3 baselinePos = float32_t3(0.f); float32_t3 baselineEulerDeg = float32_t3(0.f); + bool stepValid = false; + float32_t3 stepPos = float32_t3(0.f); + float32_t3 stepEulerDeg = float32_t3(0.f); + bool visualActivePlanarValid = false; + uint32_t visualActivePlanarIx = 0u; + uint64_t visualActivePlanarStartFrame = 0u; + bool framePacerInitialized = false; + std::chrono::steady_clock::time_point framePacerNext = {}; }; static constexpr inline auto MaxSceneFBOs = 2u; @@ -5068,14 +5804,17 @@ class UISampleApp final : public examples::SimpleWindowedApplication uint16_t gcIndex = {}; static constexpr uint32_t CiFramesBeforeCapture = 10u; + static constexpr auto CiMaxRuntime = std::chrono::minutes(2); bool m_ciMode = false; bool m_ciScreenshotDone = false; uint32_t m_ciFrameCounter = 0u; system::path m_ciScreenshotPath; + clock_t::time_point m_ciStartedAt = clock_t::time_point::min(); + bool m_scriptVisualDebugCli = false; ScriptedInputState m_scriptedInput; CameraControlSettings m_cameraControls; CameraConstraintSettings m_cameraConstraints; - CUILogFormatter m_logFormatter; + core::smart_refctd_ptr m_logFormatter; std::deque m_virtualEventLog; size_t m_virtualEventLogMax = 128u; bool m_showHud = true; diff --git a/common/include/camera/CArcballCamera.hpp b/common/include/camera/CArcballCamera.hpp index 725591aad..f68add426 100644 --- a/common/include/camera/CArcballCamera.hpp +++ b/common/include/camera/CArcballCamera.hpp @@ -8,20 +8,20 @@ #include #include -#include "ICamera.hpp" +#include "CSphericalTargetCamera.hpp" namespace nbl::hlsl { -class CArcballCamera final : public ICamera +class CArcballCamera final : public CSphericalTargetCamera { public: - using base_t = ICamera; + using base_t = CSphericalTargetCamera; CArcballCamera(const float64_t3& position, const float64_t3& target) - : base_t(), m_targetPosition(target), m_distance(1.0f), m_gimbal({ .position = position, .orientation = glm::quat(glm::vec3(0.0f)) }) + : base_t(position, target) { - initFromPosition(position); + m_v = std::clamp(m_v, MinPitch, MaxPitch); applyPose(); } ~CArcballCamera() = default; @@ -32,22 +32,7 @@ class CArcballCamera final : public ICamera const typename base_t::CGimbal& getGimbal() override { return m_gimbal; } - inline bool setDistance(float d) - { - const auto clamped = std::clamp(d, MinDistance, MaxDistance); - const bool ok = clamped == d; - m_distance = clamped; - return ok; - } - - inline void target(const float64_t3& p) { m_targetPosition = p; } - inline float64_t3 getTarget() const { return m_targetPosition; } - - inline float getDistance() { return m_distance; } - inline double getU() { return u; } - inline double getV() { return v; } - - virtual bool manipulate(std::span virtualEvents, const float64_t4x4 const* referenceFrame = nullptr) override + virtual bool manipulate(std::span virtualEvents, const float64_t4x4* referenceFrame = nullptr) override { if (not virtualEvents.size() and not referenceFrame) return false; @@ -63,17 +48,14 @@ class CArcballCamera final : public ICamera const double deltaPanY = impulse.dVirtualTranslate.y * panScalar; const double deltaDistance = impulse.dVirtualTranslate.z * translateScalar; - u += deltaYaw; - v = std::clamp(v + deltaPitch, MinPitch, MaxPitch); + m_u += deltaYaw; + m_v = std::clamp(m_v + deltaPitch, MinPitch, MaxPitch); m_distance = std::clamp(m_distance + static_cast(deltaDistance), MinDistance, MaxDistance); - const auto localSpherePosition = S(u, v) * static_cast(m_distance); - const auto newForward = normalize(-localSpherePosition); - const auto newUp = normalize(Sdv(u, v)); - const auto newRight = normalize(cross(newUp, newForward)); + const auto basis = computeBasis(m_u, m_v, m_distance); if (deltaPanX != 0.0 || deltaPanY != 0.0) - m_targetPosition += newRight * deltaPanX + newUp * deltaPanY; + m_targetPosition += basis.right * deltaPanX + basis.up * deltaPanY; return applyPose(); } @@ -81,75 +63,15 @@ class CArcballCamera final : public ICamera virtual const uint32_t getAllowedVirtualEvents() override { return AllowedVirtualEvents; } virtual const std::string_view getIdentifier() override { return "Arcball Camera"; } - static inline constexpr float MinDistance = 0.1f; - static inline constexpr float MaxDistance = 10000.f; + static inline constexpr float MinDistance = base_t::MinDistance; + static inline constexpr float MaxDistance = base_t::MaxDistance; private: - float64_t3 m_targetPosition; - float m_distance; - typename base_t::CGimbal m_gimbal; - double u = {}; - double v = {}; static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate | CVirtualGimbalEvent::Rotate; static inline constexpr double MaxPitch = glm::radians(89.0); static inline constexpr double MinPitch = -MaxPitch; - inline float64_t3 S(double su, double sv) const - { - return float64_t3 - { - std::cos(sv) * std::cos(su), - std::cos(sv) * std::sin(su), - std::sin(sv) - }; - } - - inline float64_t3 Sdv(double su, double sv) const - { - return float64_t3 - { - -std::sin(sv) * std::cos(su), - -std::sin(sv) * std::sin(su), - std::cos(sv) - }; - } - - inline void initFromPosition(const float64_t3& position) - { - const auto offset = position - m_targetPosition; - const double dist = length(offset); - const double safeDist = std::isfinite(dist) && dist > 0.0 ? dist : static_cast(MinDistance); - m_distance = std::clamp(static_cast(safeDist), MinDistance, MaxDistance); - const auto local = offset / static_cast(m_distance); - u = std::atan2(local.y, local.x); - v = std::asin(std::clamp(local.z, -1.0, 1.0)); - v = std::clamp(v, MinPitch, MaxPitch); - } - - inline bool applyPose() - { - const auto localSpherePosition = S(u, v) * static_cast(m_distance); - const auto newPosition = localSpherePosition + m_targetPosition; - const auto newForward = normalize(-localSpherePosition); - const auto newUp = normalize(Sdv(u, v)); - const auto newRight = normalize(cross(newUp, newForward)); - const auto newOrientation = glm::quat_cast(glm::dmat3{ newRight, newUp, newForward }); - - m_gimbal.begin(); - { - m_gimbal.setPosition(newPosition); - m_gimbal.setOrientation(newOrientation); - } - m_gimbal.end(); - - const bool manipulated = bool(m_gimbal.getManipulationCounter()); - if (manipulated) - m_gimbal.updateView(); - - return manipulated; - } - static inline const auto m_keyboard_to_virtual_events_preset = []() { typename base_t::keyboard_to_virtual_events_t preset; diff --git a/common/include/camera/CChaseCamera.hpp b/common/include/camera/CChaseCamera.hpp new file mode 100644 index 000000000..71129b5a7 --- /dev/null +++ b/common/include/camera/CChaseCamera.hpp @@ -0,0 +1,133 @@ +#ifndef _C_CHASE_CAMERA_HPP_ +#define _C_CHASE_CAMERA_HPP_ + +#include +#include + +#include "CSphericalTargetCamera.hpp" + +namespace nbl::hlsl +{ + +class CChaseCamera final : public CSphericalTargetCamera +{ +public: + using base_t = CSphericalTargetCamera; + + CChaseCamera(const float64_t3& position, const float64_t3& target) + : base_t(position, target) + { + m_v = std::clamp(m_v, MinPitch, MaxPitch); + applyPose(); + } + ~CChaseCamera() = default; + + const base_t::keyboard_to_virtual_events_t getKeyboardMappingPreset() const override { return m_keyboard_to_virtual_events_preset; } + const base_t::mouse_to_virtual_events_t getMouseMappingPreset() const override { return m_mouse_to_virtual_events_preset; } + const base_t::imguizmo_to_virtual_events_t getImguizmoMappingPreset() const override { return m_imguizmo_to_virtual_events_preset; } + + const typename base_t::CGimbal& getGimbal() override { return m_gimbal; } + + virtual bool manipulate(std::span virtualEvents, const float64_t4x4* referenceFrame = nullptr) override + { + if (not virtualEvents.size() and not referenceFrame) + return false; + + auto impulse = m_gimbal.accumulate(virtualEvents); + + const double deltaYaw = impulse.dVirtualRotation.y * m_rotationSpeedScale; + const double deltaPitch = impulse.dVirtualRotation.x * m_rotationSpeedScale; + + constexpr double translateScalar = 0.01; + const double moveScalar = translateScalar * m_moveSpeedScale; + + const auto basis = computeBasis(m_u, m_v, m_distance); + + float64_t3 planarForward = float64_t3(basis.forward.x, 0.0, basis.forward.z); + float64_t3 planarRight = float64_t3(basis.right.x, 0.0, basis.right.z); + + const double forwardLen = length(planarForward); + if (forwardLen > 0.0) + planarForward /= forwardLen; + else + planarForward = float64_t3(0.0, 0.0, 1.0); + + const double rightLen = length(planarRight); + if (rightLen > 0.0) + planarRight /= rightLen; + else + planarRight = float64_t3(1.0, 0.0, 0.0); + + m_targetPosition += (planarRight * impulse.dVirtualTranslate.x + planarForward * impulse.dVirtualTranslate.z) * moveScalar; + m_distance = std::clamp(m_distance + static_cast(impulse.dVirtualTranslate.y * translateScalar), MinDistance, MaxDistance); + + m_u += deltaYaw; + m_v = std::clamp(m_v + deltaPitch, MinPitch, MaxPitch); + + return applyPose(); + } + + virtual const uint32_t getAllowedVirtualEvents() override { return AllowedVirtualEvents; } + virtual const std::string_view getIdentifier() override { return "Chase Camera"; } + +private: + static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate | CVirtualGimbalEvent::Rotate; + static inline constexpr double MaxPitch = glm::radians(70.0); + static inline constexpr double MinPitch = glm::radians(-60.0); + + static inline const auto m_keyboard_to_virtual_events_preset = []() + { + typename base_t::keyboard_to_virtual_events_t preset; + + preset[ui::E_KEY_CODE::EKC_W] = CVirtualGimbalEvent::MoveForward; + preset[ui::E_KEY_CODE::EKC_S] = CVirtualGimbalEvent::MoveBackward; + preset[ui::E_KEY_CODE::EKC_A] = CVirtualGimbalEvent::MoveLeft; + preset[ui::E_KEY_CODE::EKC_D] = CVirtualGimbalEvent::MoveRight; + preset[ui::E_KEY_CODE::EKC_Q] = CVirtualGimbalEvent::MoveDown; + preset[ui::E_KEY_CODE::EKC_E] = CVirtualGimbalEvent::MoveUp; + preset[ui::E_KEY_CODE::EKC_I] = CVirtualGimbalEvent::TiltDown; + preset[ui::E_KEY_CODE::EKC_K] = CVirtualGimbalEvent::TiltUp; + preset[ui::E_KEY_CODE::EKC_J] = CVirtualGimbalEvent::PanLeft; + preset[ui::E_KEY_CODE::EKC_L] = CVirtualGimbalEvent::PanRight; + + return preset; + }(); + + static inline const auto m_mouse_to_virtual_events_preset = []() + { + typename base_t::mouse_to_virtual_events_t preset; + + preset[ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_X] = CVirtualGimbalEvent::PanRight; + preset[ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_X] = CVirtualGimbalEvent::PanLeft; + preset[ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_Y] = CVirtualGimbalEvent::TiltUp; + preset[ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_Y] = CVirtualGimbalEvent::TiltDown; + preset[ui::E_MOUSE_CODE::EMC_VERTICAL_POSITIVE_SCROLL] = CVirtualGimbalEvent::MoveUp; + preset[ui::E_MOUSE_CODE::EMC_HORIZONTAL_POSITIVE_SCROLL] = CVirtualGimbalEvent::MoveUp; + preset[ui::E_MOUSE_CODE::EMC_VERTICAL_NEGATIVE_SCROLL] = CVirtualGimbalEvent::MoveDown; + preset[ui::E_MOUSE_CODE::EMC_HORIZONTAL_NEGATIVE_SCROLL] = CVirtualGimbalEvent::MoveDown; + + return preset; + }(); + + static inline const auto m_imguizmo_to_virtual_events_preset = []() + { + typename base_t::imguizmo_to_virtual_events_t preset; + + preset[CVirtualGimbalEvent::MoveForward] = CVirtualGimbalEvent::MoveForward; + preset[CVirtualGimbalEvent::MoveBackward] = CVirtualGimbalEvent::MoveBackward; + preset[CVirtualGimbalEvent::MoveLeft] = CVirtualGimbalEvent::MoveLeft; + preset[CVirtualGimbalEvent::MoveRight] = CVirtualGimbalEvent::MoveRight; + preset[CVirtualGimbalEvent::MoveUp] = CVirtualGimbalEvent::MoveUp; + preset[CVirtualGimbalEvent::MoveDown] = CVirtualGimbalEvent::MoveDown; + preset[CVirtualGimbalEvent::TiltDown] = CVirtualGimbalEvent::TiltDown; + preset[CVirtualGimbalEvent::TiltUp] = CVirtualGimbalEvent::TiltUp; + preset[CVirtualGimbalEvent::PanLeft] = CVirtualGimbalEvent::PanLeft; + preset[CVirtualGimbalEvent::PanRight] = CVirtualGimbalEvent::PanRight; + + return preset; + }(); +}; + +} + +#endif diff --git a/common/include/camera/CDollyCamera.hpp b/common/include/camera/CDollyCamera.hpp new file mode 100644 index 000000000..666e4aa71 --- /dev/null +++ b/common/include/camera/CDollyCamera.hpp @@ -0,0 +1,117 @@ +#ifndef _C_DOLLY_CAMERA_HPP_ +#define _C_DOLLY_CAMERA_HPP_ + +#include +#include + +#include "CSphericalTargetCamera.hpp" + +namespace nbl::hlsl +{ + +class CDollyCamera final : public CSphericalTargetCamera +{ +public: + using base_t = CSphericalTargetCamera; + + CDollyCamera(const float64_t3& position, const float64_t3& target) + : base_t(position, target) + { + m_v = std::clamp(m_v, MinPitch, MaxPitch); + applyPose(); + } + ~CDollyCamera() = default; + + const base_t::keyboard_to_virtual_events_t getKeyboardMappingPreset() const override { return m_keyboard_to_virtual_events_preset; } + const base_t::mouse_to_virtual_events_t getMouseMappingPreset() const override { return m_mouse_to_virtual_events_preset; } + const base_t::imguizmo_to_virtual_events_t getImguizmoMappingPreset() const override { return m_imguizmo_to_virtual_events_preset; } + + const typename base_t::CGimbal& getGimbal() override { return m_gimbal; } + + virtual bool manipulate(std::span virtualEvents, const float64_t4x4* referenceFrame = nullptr) override + { + if (not virtualEvents.size() and not referenceFrame) + return false; + + auto impulse = m_gimbal.accumulate(virtualEvents); + + const double deltaYaw = impulse.dVirtualRotation.y * m_rotationSpeedScale; + const double deltaPitch = impulse.dVirtualRotation.x * m_rotationSpeedScale; + + constexpr double translateScalar = 0.01; + const double moveScalar = translateScalar * m_moveSpeedScale; + + const auto basis = computeBasis(m_u, m_v, m_distance); + const auto delta = (basis.right * impulse.dVirtualTranslate.x + basis.up * impulse.dVirtualTranslate.y + basis.forward * impulse.dVirtualTranslate.z) * moveScalar; + + m_targetPosition += delta; + m_u += deltaYaw; + m_v = std::clamp(m_v + deltaPitch, MinPitch, MaxPitch); + + return applyPose(); + } + + virtual const uint32_t getAllowedVirtualEvents() override { return AllowedVirtualEvents; } + virtual const std::string_view getIdentifier() override { return "Dolly Camera"; } + +private: + static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate | CVirtualGimbalEvent::Rotate; + static inline constexpr double MaxPitch = glm::radians(85.0); + static inline constexpr double MinPitch = glm::radians(-85.0); + + static inline const auto m_keyboard_to_virtual_events_preset = []() + { + typename base_t::keyboard_to_virtual_events_t preset; + + preset[ui::E_KEY_CODE::EKC_W] = CVirtualGimbalEvent::MoveForward; + preset[ui::E_KEY_CODE::EKC_S] = CVirtualGimbalEvent::MoveBackward; + preset[ui::E_KEY_CODE::EKC_A] = CVirtualGimbalEvent::MoveLeft; + preset[ui::E_KEY_CODE::EKC_D] = CVirtualGimbalEvent::MoveRight; + preset[ui::E_KEY_CODE::EKC_Q] = CVirtualGimbalEvent::MoveDown; + preset[ui::E_KEY_CODE::EKC_E] = CVirtualGimbalEvent::MoveUp; + preset[ui::E_KEY_CODE::EKC_I] = CVirtualGimbalEvent::TiltDown; + preset[ui::E_KEY_CODE::EKC_K] = CVirtualGimbalEvent::TiltUp; + preset[ui::E_KEY_CODE::EKC_J] = CVirtualGimbalEvent::PanLeft; + preset[ui::E_KEY_CODE::EKC_L] = CVirtualGimbalEvent::PanRight; + + return preset; + }(); + + static inline const auto m_mouse_to_virtual_events_preset = []() + { + typename base_t::mouse_to_virtual_events_t preset; + + preset[ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_X] = CVirtualGimbalEvent::PanRight; + preset[ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_X] = CVirtualGimbalEvent::PanLeft; + preset[ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_Y] = CVirtualGimbalEvent::TiltUp; + preset[ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_Y] = CVirtualGimbalEvent::TiltDown; + preset[ui::E_MOUSE_CODE::EMC_VERTICAL_POSITIVE_SCROLL] = CVirtualGimbalEvent::MoveForward; + preset[ui::E_MOUSE_CODE::EMC_HORIZONTAL_POSITIVE_SCROLL] = CVirtualGimbalEvent::MoveForward; + preset[ui::E_MOUSE_CODE::EMC_VERTICAL_NEGATIVE_SCROLL] = CVirtualGimbalEvent::MoveBackward; + preset[ui::E_MOUSE_CODE::EMC_HORIZONTAL_NEGATIVE_SCROLL] = CVirtualGimbalEvent::MoveBackward; + + return preset; + }(); + + static inline const auto m_imguizmo_to_virtual_events_preset = []() + { + typename base_t::imguizmo_to_virtual_events_t preset; + + preset[CVirtualGimbalEvent::MoveForward] = CVirtualGimbalEvent::MoveForward; + preset[CVirtualGimbalEvent::MoveBackward] = CVirtualGimbalEvent::MoveBackward; + preset[CVirtualGimbalEvent::MoveLeft] = CVirtualGimbalEvent::MoveLeft; + preset[CVirtualGimbalEvent::MoveRight] = CVirtualGimbalEvent::MoveRight; + preset[CVirtualGimbalEvent::MoveUp] = CVirtualGimbalEvent::MoveUp; + preset[CVirtualGimbalEvent::MoveDown] = CVirtualGimbalEvent::MoveDown; + preset[CVirtualGimbalEvent::TiltDown] = CVirtualGimbalEvent::TiltDown; + preset[CVirtualGimbalEvent::TiltUp] = CVirtualGimbalEvent::TiltUp; + preset[CVirtualGimbalEvent::PanLeft] = CVirtualGimbalEvent::PanLeft; + preset[CVirtualGimbalEvent::PanRight] = CVirtualGimbalEvent::PanRight; + + return preset; + }(); +}; + +} + +#endif diff --git a/common/include/camera/CDollyZoomCamera.hpp b/common/include/camera/CDollyZoomCamera.hpp new file mode 100644 index 000000000..1fe36594a --- /dev/null +++ b/common/include/camera/CDollyZoomCamera.hpp @@ -0,0 +1,122 @@ +#ifndef _C_DOLLY_ZOOM_CAMERA_HPP_ +#define _C_DOLLY_ZOOM_CAMERA_HPP_ + +#include +#include + +#include "CSphericalTargetCamera.hpp" + +namespace nbl::hlsl +{ + +class CDollyZoomCamera final : public CSphericalTargetCamera +{ +public: + using base_t = CSphericalTargetCamera; + + CDollyZoomCamera(const float64_t3& position, const float64_t3& target, float baseFov = 40.0f) + : base_t(position, target), m_baseFov(baseFov), m_referenceDistance(m_distance) + { + applyPose(); + } + ~CDollyZoomCamera() = default; + + const base_t::keyboard_to_virtual_events_t getKeyboardMappingPreset() const override { return m_keyboard_to_virtual_events_preset; } + const base_t::mouse_to_virtual_events_t getMouseMappingPreset() const override { return m_mouse_to_virtual_events_preset; } + const base_t::imguizmo_to_virtual_events_t getImguizmoMappingPreset() const override { return m_imguizmo_to_virtual_events_preset; } + + const typename base_t::CGimbal& getGimbal() override { return m_gimbal; } + + float getBaseFov() const { return m_baseFov; } + void setBaseFov(float fov) { m_baseFov = fov; } + + float getReferenceDistance() const { return m_referenceDistance; } + void setReferenceDistance(float distance) { m_referenceDistance = distance; } + + float computeDollyFov() const + { + const double base = std::tan(glm::radians(static_cast(m_baseFov)) * 0.5); + const double ratio = static_cast(m_referenceDistance) / std::max(static_cast(m_distance), static_cast(MinDistance)); + const double fov = 2.0 * std::atan(base * ratio); + const double fovDeg = glm::degrees(fov); + return static_cast(std::clamp(fovDeg, 10.0, 150.0)); + } + + virtual bool manipulate(std::span virtualEvents, const float64_t4x4* referenceFrame = nullptr) override + { + if (not virtualEvents.size() and not referenceFrame) + return false; + + auto impulse = m_gimbal.accumulate(virtualEvents); + double deltaU = impulse.dVirtualTranslate.y; + double deltaV = impulse.dVirtualTranslate.x; + double deltaDistance = impulse.dVirtualTranslate.z; + + constexpr auto scalar = 0.01; + deltaU *= scalar * m_moveSpeedScale; + deltaV *= scalar * m_moveSpeedScale; + + m_u += deltaU; + m_v += deltaV; + m_distance = std::clamp(m_distance + static_cast(deltaDistance * scalar), MinDistance, MaxDistance); + + return applyPose(); + } + + virtual const uint32_t getAllowedVirtualEvents() override { return AllowedVirtualEvents; } + virtual const std::string_view getIdentifier() override { return "Dolly Zoom Camera"; } + +private: + static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate; + + float m_baseFov = 40.0f; + float m_referenceDistance = 1.0f; + + static inline const auto m_keyboard_to_virtual_events_preset = []() + { + typename base_t::keyboard_to_virtual_events_t preset; + + preset[ui::E_KEY_CODE::EKC_W] = CVirtualGimbalEvent::MoveUp; + preset[ui::E_KEY_CODE::EKC_S] = CVirtualGimbalEvent::MoveDown; + preset[ui::E_KEY_CODE::EKC_A] = CVirtualGimbalEvent::MoveLeft; + preset[ui::E_KEY_CODE::EKC_D] = CVirtualGimbalEvent::MoveRight; + preset[ui::E_KEY_CODE::EKC_E] = CVirtualGimbalEvent::MoveForward; + preset[ui::E_KEY_CODE::EKC_Q] = CVirtualGimbalEvent::MoveBackward; + + return preset; + }(); + + static inline const auto m_mouse_to_virtual_events_preset = []() + { + typename base_t::mouse_to_virtual_events_t preset; + + preset[ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_X] = CVirtualGimbalEvent::MoveRight; + preset[ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_X] = CVirtualGimbalEvent::MoveLeft; + preset[ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_Y] = CVirtualGimbalEvent::MoveUp; + preset[ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_Y] = CVirtualGimbalEvent::MoveDown; + preset[ui::E_MOUSE_CODE::EMC_VERTICAL_POSITIVE_SCROLL] = CVirtualGimbalEvent::MoveForward; + preset[ui::E_MOUSE_CODE::EMC_HORIZONTAL_POSITIVE_SCROLL] = CVirtualGimbalEvent::MoveForward; + preset[ui::E_MOUSE_CODE::EMC_VERTICAL_NEGATIVE_SCROLL] = CVirtualGimbalEvent::MoveBackward; + preset[ui::E_MOUSE_CODE::EMC_HORIZONTAL_NEGATIVE_SCROLL] = CVirtualGimbalEvent::MoveBackward; + + return preset; + }(); + + static inline const auto m_imguizmo_to_virtual_events_preset = []() + { + typename base_t::imguizmo_to_virtual_events_t preset; + + preset[CVirtualGimbalEvent::MoveForward] = CVirtualGimbalEvent::MoveForward; + preset[CVirtualGimbalEvent::MoveBackward] = CVirtualGimbalEvent::MoveBackward; + preset[CVirtualGimbalEvent::MoveLeft] = CVirtualGimbalEvent::MoveLeft; + preset[CVirtualGimbalEvent::MoveRight] = CVirtualGimbalEvent::MoveRight; + preset[CVirtualGimbalEvent::MoveUp] = CVirtualGimbalEvent::MoveUp; + preset[CVirtualGimbalEvent::MoveDown] = CVirtualGimbalEvent::MoveDown; + + return preset; + }(); +}; + +} + +#endif diff --git a/common/include/camera/CFPSCamera.hpp b/common/include/camera/CFPSCamera.hpp index 95a750a08..dc2908e72 100644 --- a/common/include/camera/CFPSCamera.hpp +++ b/common/include/camera/CFPSCamera.hpp @@ -1,4 +1,4 @@ -// Copyright (C) 2018-2020 - DevSH Graphics Programming Sp. z O.O. +// 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 @@ -47,7 +47,7 @@ class CFPSCamera final : public ICamera // rotation events IN RADIANS - virtual bool manipulate(std::span virtualEvents, const float64_t4x4 const* referenceFrame = nullptr) override + virtual bool manipulate(std::span virtualEvents, const float64_t4x4* referenceFrame = nullptr) override { // TODO: note, for FPS camera its assumed tilt is performed with respect to "world" up vector which is (0,1,0) // but in reality its all about where -(gravity force) vector is, we can just add it and construct yaw quat with respect to this new custom vector instead diff --git a/common/include/camera/CFreeLockCamera.hpp b/common/include/camera/CFreeLockCamera.hpp index 628b0005e..9d4a7a0f0 100644 --- a/common/include/camera/CFreeLockCamera.hpp +++ b/common/include/camera/CFreeLockCamera.hpp @@ -1,4 +1,4 @@ -// Copyright (C) 2018-2024 - DevSH Graphics Programming Sp. z O.O. +// Copyright (C) 2018-2024 - 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 @@ -28,7 +28,7 @@ class CFreeCamera final : public ICamera return m_gimbal; } - virtual bool manipulate(std::span virtualEvents, const float64_t4x4 const* referenceFrame = nullptr) override + virtual bool manipulate(std::span virtualEvents, const float64_t4x4* referenceFrame = nullptr) override { if (not virtualEvents.size() and not referenceFrame) return false; diff --git a/common/include/camera/CIsometricCamera.hpp b/common/include/camera/CIsometricCamera.hpp new file mode 100644 index 000000000..3fb2de1f1 --- /dev/null +++ b/common/include/camera/CIsometricCamera.hpp @@ -0,0 +1,111 @@ +#ifndef _C_ISOMETRIC_CAMERA_HPP_ +#define _C_ISOMETRIC_CAMERA_HPP_ + +#include +#include + +#include "CSphericalTargetCamera.hpp" + +namespace nbl::hlsl +{ + +class CIsometricCamera final : public CSphericalTargetCamera +{ +public: + using base_t = CSphericalTargetCamera; + + CIsometricCamera(const float64_t3& position, const float64_t3& target) + : base_t(position, target) + { + m_u = IsoYaw; + m_v = IsoPitch; + applyPose(); + } + ~CIsometricCamera() = default; + + const base_t::keyboard_to_virtual_events_t getKeyboardMappingPreset() const override { return m_keyboard_to_virtual_events_preset; } + const base_t::mouse_to_virtual_events_t getMouseMappingPreset() const override { return m_mouse_to_virtual_events_preset; } + const base_t::imguizmo_to_virtual_events_t getImguizmoMappingPreset() const override { return m_imguizmo_to_virtual_events_preset; } + + const typename base_t::CGimbal& getGimbal() override { return m_gimbal; } + + virtual bool manipulate(std::span virtualEvents, const float64_t4x4* referenceFrame = nullptr) override + { + if (not virtualEvents.size() and not referenceFrame) + return false; + + auto impulse = m_gimbal.accumulate(virtualEvents); + + constexpr double translateScalar = 0.01; + const double panScalar = translateScalar * m_moveSpeedScale; + const double deltaPanX = impulse.dVirtualTranslate.x * panScalar; + const double deltaPanY = impulse.dVirtualTranslate.y * panScalar; + const double deltaDistance = impulse.dVirtualTranslate.z * translateScalar; + + m_u = IsoYaw; + m_v = IsoPitch; + m_distance = std::clamp(m_distance + static_cast(deltaDistance), MinDistance, MaxDistance); + + const auto basis = computeBasis(m_u, m_v, m_distance); + if (deltaPanX != 0.0 || deltaPanY != 0.0) + m_targetPosition += basis.right * deltaPanX + basis.up * deltaPanY; + + return applyPose(); + } + + virtual const uint32_t getAllowedVirtualEvents() override { return AllowedVirtualEvents; } + virtual const std::string_view getIdentifier() override { return "Isometric Camera"; } + +private: + static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate; + static inline constexpr double IsoYaw = 0.7853981633974483; + static inline constexpr double IsoPitch = 0.6154797086703873; + + static inline const auto m_keyboard_to_virtual_events_preset = []() + { + typename base_t::keyboard_to_virtual_events_t preset; + + preset[ui::E_KEY_CODE::EKC_W] = CVirtualGimbalEvent::MoveUp; + preset[ui::E_KEY_CODE::EKC_S] = CVirtualGimbalEvent::MoveDown; + preset[ui::E_KEY_CODE::EKC_A] = CVirtualGimbalEvent::MoveLeft; + preset[ui::E_KEY_CODE::EKC_D] = CVirtualGimbalEvent::MoveRight; + preset[ui::E_KEY_CODE::EKC_Q] = CVirtualGimbalEvent::MoveBackward; + preset[ui::E_KEY_CODE::EKC_E] = CVirtualGimbalEvent::MoveForward; + + return preset; + }(); + + static inline const auto m_mouse_to_virtual_events_preset = []() + { + typename base_t::mouse_to_virtual_events_t preset; + + preset[ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_X] = CVirtualGimbalEvent::MoveRight; + preset[ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_X] = CVirtualGimbalEvent::MoveLeft; + preset[ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_Y] = CVirtualGimbalEvent::MoveUp; + preset[ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_Y] = CVirtualGimbalEvent::MoveDown; + preset[ui::E_MOUSE_CODE::EMC_VERTICAL_POSITIVE_SCROLL] = CVirtualGimbalEvent::MoveForward; + preset[ui::E_MOUSE_CODE::EMC_HORIZONTAL_POSITIVE_SCROLL] = CVirtualGimbalEvent::MoveForward; + preset[ui::E_MOUSE_CODE::EMC_VERTICAL_NEGATIVE_SCROLL] = CVirtualGimbalEvent::MoveBackward; + preset[ui::E_MOUSE_CODE::EMC_HORIZONTAL_NEGATIVE_SCROLL] = CVirtualGimbalEvent::MoveBackward; + + return preset; + }(); + + static inline const auto m_imguizmo_to_virtual_events_preset = []() + { + typename base_t::imguizmo_to_virtual_events_t preset; + + preset[CVirtualGimbalEvent::MoveForward] = CVirtualGimbalEvent::MoveForward; + preset[CVirtualGimbalEvent::MoveBackward] = CVirtualGimbalEvent::MoveBackward; + preset[CVirtualGimbalEvent::MoveLeft] = CVirtualGimbalEvent::MoveLeft; + preset[CVirtualGimbalEvent::MoveRight] = CVirtualGimbalEvent::MoveRight; + preset[CVirtualGimbalEvent::MoveUp] = CVirtualGimbalEvent::MoveUp; + preset[CVirtualGimbalEvent::MoveDown] = CVirtualGimbalEvent::MoveDown; + + return preset; + }(); +}; + +} + +#endif diff --git a/common/include/camera/COrbitCamera.hpp b/common/include/camera/COrbitCamera.hpp index cafcb8a85..8df9c32d0 100644 --- a/common/include/camera/COrbitCamera.hpp +++ b/common/include/camera/COrbitCamera.hpp @@ -3,18 +3,22 @@ #include #include -#include "ICamera.hpp" +#include "CSphericalTargetCamera.hpp" namespace nbl::hlsl { -class COrbitCamera final : public ICamera +class COrbitCamera final : public CSphericalTargetCamera { public: - using base_t = ICamera; + using base_t = CSphericalTargetCamera; COrbitCamera(const float64_t3& position, const float64_t3& target) - : base_t(), m_targetPosition(target), m_distance(length(m_targetPosition - position)), m_gimbal({ .position = position, .orientation = glm::quat(glm::vec3(0, 0, 0)) }) {} + : base_t(position, target) + { + m_distance = std::clamp(length(m_targetPosition - position), MinDistance, MaxDistance); + applyPose(); + } ~COrbitCamera() = default; const base_t::keyboard_to_virtual_events_t getKeyboardMappingPreset() const override { return m_keyboard_to_virtual_events_preset; } @@ -23,65 +27,10 @@ class COrbitCamera final : public ICamera const typename base_t::CGimbal& getGimbal() override { return m_gimbal; } - inline bool setDistance(float d) - { - const auto clamped = std::clamp(d, MinDistance, MaxDistance); - const bool ok = clamped == d; - - m_distance = clamped; - - return ok; - } - - inline void target(const float64_t3& p) - { - m_targetPosition = p; - } - - inline float64_t3 getTarget() const - { - return m_targetPosition; - } - - virtual bool manipulate(std::span virtualEvents, const float64_t4x4 const* referenceFrame = nullptr) override + virtual bool manipulate(std::span virtualEvents, const float64_t4x4* referenceFrame = nullptr) override { // TODO: it must work differently, we should take another gimbal to control target - // position on the sphere - auto S = [&](double u, double v) -> float64_t3 - { - return float64_t3 - { - std::cos(v) * std::cos(u), - std::cos(v) * std::sin(u), - std::sin(v) - } * (double) m_distance; - }; - - /* - // partial derivative of S with respect to u - auto Sdu = [&](double u, double v) -> float64_t3 - { - return float64_t3 - { - -std::cos(v) * std::sin(u), - std::cos(v)* std::cos(u), - 0 - } * (double) m_distance; - }; - */ - - // partial derivative of S with respect to v - auto Sdv = [&](double u, double v) -> float64_t3 - { - return float64_t3 - { - -std::sin(v) * std::cos(u), - -std::sin(v) * std::sin(u), - std::cos(v) - } *(double)m_distance; - }; - auto impulse = m_gimbal.accumulate(virtualEvents); double deltaU = impulse.dVirtualTranslate.y, deltaV = impulse.dVirtualTranslate.x, deltaDistance = impulse.dVirtualTranslate.z; @@ -90,46 +39,12 @@ class COrbitCamera final : public ICamera deltaU *= nastyScalar * m_moveSpeedScale; deltaV *= nastyScalar * m_moveSpeedScale; - u += deltaU; - v += deltaV; + m_u += deltaU; + m_v += deltaV; m_distance = std::clamp(m_distance += deltaDistance * nastyScalar, MinDistance, MaxDistance); - const auto localSpherePostion = S(u, v); - const auto newPosition = localSpherePostion + m_targetPosition; - - // note we are not using Sdu (though we could!) - // instead we benefit from forward we have for free when moving on sphere surface - // and given up vector obtained from partial derivative we can easily get right vector, this way - // we don't have frenet frame flip we would have with Sdu, however it could be adjusted anyway, less code - - const auto newUp = normalize(Sdv(u, v)); - const auto newForward = normalize(-localSpherePostion); - const auto newRight = normalize(cross(newUp, newForward)); - - const auto newOrientation = glm::quat_cast - ( - glm::dmat3 - { - newRight, - newUp, - newForward - } - ); - - m_gimbal.begin(); - { - m_gimbal.setPosition(newPosition); - m_gimbal.setOrientation(newOrientation); - } - m_gimbal.end(); - - bool manipulated = bool(m_gimbal.getManipulationCounter()); - - if (manipulated) - m_gimbal.updateView(); - - return manipulated; + return applyPose(); } virtual const uint32_t getAllowedVirtualEvents() override @@ -142,19 +57,8 @@ class COrbitCamera final : public ICamera return "Orbit Camera"; } - inline float getDistance() { return m_distance; } - inline double getU() { return u; } - inline double getV() { return v; } - - static inline constexpr float MinDistance = 0.1f; - static inline constexpr float MaxDistance = 10000.f; - -private: - float64_t3 m_targetPosition; - float m_distance; - typename base_t::CGimbal m_gimbal; - - double u = {}, v = {}; + static inline constexpr float MinDistance = base_t::MinDistance; + static inline constexpr float MaxDistance = base_t::MaxDistance; static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate; diff --git a/common/include/camera/CPathCamera.hpp b/common/include/camera/CPathCamera.hpp new file mode 100644 index 000000000..4277e4d92 --- /dev/null +++ b/common/include/camera/CPathCamera.hpp @@ -0,0 +1,120 @@ +#ifndef _C_PATH_CAMERA_HPP_ +#define _C_PATH_CAMERA_HPP_ + +#include +#include + +#include "CSphericalTargetCamera.hpp" + +namespace nbl::hlsl +{ + +class CPathCamera final : public CSphericalTargetCamera +{ +public: + using base_t = CSphericalTargetCamera; + + CPathCamera(const float64_t3& position, const float64_t3& target) + : base_t(position, target) + { + const auto offset = position - target; + m_pathRadius = std::sqrt(offset.x * offset.x + offset.z * offset.z); + if (m_pathRadius < MinPathRadius) + m_pathRadius = MinPathRadius; + m_pathHeight = offset.y; + m_pathAngle = std::atan2(offset.z, offset.x); + updateFromPath(); + } + ~CPathCamera() = default; + + const base_t::keyboard_to_virtual_events_t getKeyboardMappingPreset() const override { return m_keyboard_to_virtual_events_preset; } + const base_t::mouse_to_virtual_events_t getMouseMappingPreset() const override { return m_mouse_to_virtual_events_preset; } + const base_t::imguizmo_to_virtual_events_t getImguizmoMappingPreset() const override { return m_imguizmo_to_virtual_events_preset; } + + const typename base_t::CGimbal& getGimbal() override { return m_gimbal; } + + virtual bool manipulate(std::span virtualEvents, const float64_t4x4* referenceFrame = nullptr) override + { + if (not virtualEvents.size() and not referenceFrame) + return false; + + auto impulse = m_gimbal.accumulate(virtualEvents); + + constexpr double translateScalar = 0.01; + const double moveScalar = translateScalar * m_moveSpeedScale; + + m_pathAngle += impulse.dVirtualTranslate.z * moveScalar; + m_pathRadius = std::max(MinPathRadius, m_pathRadius + impulse.dVirtualTranslate.x * moveScalar); + m_pathHeight += impulse.dVirtualTranslate.y * moveScalar; + + return updateFromPath(); + } + + virtual const uint32_t getAllowedVirtualEvents() override { return AllowedVirtualEvents; } + virtual const std::string_view getIdentifier() override { return "Path Camera"; } + +private: + static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate; + static inline constexpr double MinPathRadius = 0.1; + + double m_pathAngle = 0.0; + double m_pathRadius = 1.0; + double m_pathHeight = 0.0; + + bool updateFromPath() + { + const double x = std::cos(m_pathAngle) * m_pathRadius; + const double z = std::sin(m_pathAngle) * m_pathRadius; + const float64_t3 position = m_targetPosition + float64_t3(x, m_pathHeight, z); + initFromPosition(position); + return applyPose(); + } + + static inline const auto m_keyboard_to_virtual_events_preset = []() + { + typename base_t::keyboard_to_virtual_events_t preset; + + preset[ui::E_KEY_CODE::EKC_W] = CVirtualGimbalEvent::MoveForward; + preset[ui::E_KEY_CODE::EKC_S] = CVirtualGimbalEvent::MoveBackward; + preset[ui::E_KEY_CODE::EKC_A] = CVirtualGimbalEvent::MoveLeft; + preset[ui::E_KEY_CODE::EKC_D] = CVirtualGimbalEvent::MoveRight; + preset[ui::E_KEY_CODE::EKC_Q] = CVirtualGimbalEvent::MoveDown; + preset[ui::E_KEY_CODE::EKC_E] = CVirtualGimbalEvent::MoveUp; + + return preset; + }(); + + static inline const auto m_mouse_to_virtual_events_preset = []() + { + typename base_t::mouse_to_virtual_events_t preset; + + preset[ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_X] = CVirtualGimbalEvent::MoveRight; + preset[ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_X] = CVirtualGimbalEvent::MoveLeft; + preset[ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_Y] = CVirtualGimbalEvent::MoveUp; + preset[ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_Y] = CVirtualGimbalEvent::MoveDown; + preset[ui::E_MOUSE_CODE::EMC_VERTICAL_POSITIVE_SCROLL] = CVirtualGimbalEvent::MoveForward; + preset[ui::E_MOUSE_CODE::EMC_HORIZONTAL_POSITIVE_SCROLL] = CVirtualGimbalEvent::MoveForward; + preset[ui::E_MOUSE_CODE::EMC_VERTICAL_NEGATIVE_SCROLL] = CVirtualGimbalEvent::MoveBackward; + preset[ui::E_MOUSE_CODE::EMC_HORIZONTAL_NEGATIVE_SCROLL] = CVirtualGimbalEvent::MoveBackward; + + return preset; + }(); + + static inline const auto m_imguizmo_to_virtual_events_preset = []() + { + typename base_t::imguizmo_to_virtual_events_t preset; + + preset[CVirtualGimbalEvent::MoveForward] = CVirtualGimbalEvent::MoveForward; + preset[CVirtualGimbalEvent::MoveBackward] = CVirtualGimbalEvent::MoveBackward; + preset[CVirtualGimbalEvent::MoveLeft] = CVirtualGimbalEvent::MoveLeft; + preset[CVirtualGimbalEvent::MoveRight] = CVirtualGimbalEvent::MoveRight; + preset[CVirtualGimbalEvent::MoveUp] = CVirtualGimbalEvent::MoveUp; + preset[CVirtualGimbalEvent::MoveDown] = CVirtualGimbalEvent::MoveDown; + + return preset; + }(); +}; + +} + +#endif diff --git a/common/include/camera/CSphericalTargetCamera.hpp b/common/include/camera/CSphericalTargetCamera.hpp new file mode 100644 index 000000000..0ddcf747e --- /dev/null +++ b/common/include/camera/CSphericalTargetCamera.hpp @@ -0,0 +1,122 @@ +#ifndef _C_SPHERICAL_TARGET_CAMERA_HPP_ +#define _C_SPHERICAL_TARGET_CAMERA_HPP_ + +#include +#include + +#include "ICamera.hpp" + +namespace nbl::hlsl +{ + +class CSphericalTargetCamera : public ICamera +{ +public: + using base_t = ICamera; + + CSphericalTargetCamera(const float64_t3& position, const float64_t3& target) + : base_t(), m_targetPosition(target), m_distance(1.0f), + m_gimbal({ .position = position, .orientation = glm::quat(glm::vec3(0.0f)) }) + { + initFromPosition(position); + } + ~CSphericalTargetCamera() = default; + + inline bool setDistance(float d) + { + const auto clamped = std::clamp(d, MinDistance, MaxDistance); + const bool ok = clamped == d; + m_distance = clamped; + return ok; + } + + inline void target(const float64_t3& p) { m_targetPosition = p; } + inline float64_t3 getTarget() const { return m_targetPosition; } + + inline float getDistance() const { return m_distance; } + inline double getU() const { return m_u; } + inline double getV() const { return m_v; } + + static inline constexpr float MinDistance = 0.1f; + static inline constexpr float MaxDistance = 10000.f; + +protected: + struct SphericalBasis + { + float64_t3 localSpherePosition; + float64_t3 right; + float64_t3 up; + float64_t3 forward; + }; + + inline float64_t3 S(double su, double sv) const + { + return float64_t3 + { + std::cos(sv) * std::cos(su), + std::cos(sv) * std::sin(su), + std::sin(sv) + }; + } + + inline float64_t3 Sdv(double su, double sv) const + { + return float64_t3 + { + -std::sin(sv) * std::cos(su), + -std::sin(sv) * std::sin(su), + std::cos(sv) + }; + } + + inline SphericalBasis computeBasis(double su, double sv, float distance) const + { + const auto localSpherePosition = S(su, sv) * static_cast(distance); + const auto forward = normalize(-localSpherePosition); + const auto up = normalize(Sdv(su, sv)); + const auto right = normalize(cross(up, forward)); + + return { localSpherePosition, right, up, forward }; + } + + inline void initFromPosition(const float64_t3& position) + { + const auto offset = position - m_targetPosition; + const double dist = length(offset); + const double safeDist = std::isfinite(dist) && dist > 0.0 ? dist : static_cast(MinDistance); + m_distance = std::clamp(static_cast(safeDist), MinDistance, MaxDistance); + const auto local = offset / static_cast(m_distance); + m_u = std::atan2(local.y, local.x); + m_v = std::asin(std::clamp(local.z, -1.0, 1.0)); + } + + inline bool applyPose() + { + const auto basis = computeBasis(m_u, m_v, m_distance); + const auto newPosition = basis.localSpherePosition + m_targetPosition; + const auto newOrientation = glm::quat_cast(glm::dmat3{ basis.right, basis.up, basis.forward }); + + m_gimbal.begin(); + { + m_gimbal.setPosition(newPosition); + m_gimbal.setOrientation(newOrientation); + } + m_gimbal.end(); + + const bool manipulated = bool(m_gimbal.getManipulationCounter()); + if (manipulated) + m_gimbal.updateView(); + + return manipulated; + } + + float64_t3 m_targetPosition; + float m_distance; + typename base_t::CGimbal m_gimbal; + double m_u = {}; + double m_v = {}; +}; + +} + +#endif diff --git a/common/include/camera/CTargetPoseController.hpp b/common/include/camera/CTargetPoseController.hpp index 0d59eedbf..5ae44ed07 100644 --- a/common/include/camera/CTargetPoseController.hpp +++ b/common/include/camera/CTargetPoseController.hpp @@ -9,9 +9,7 @@ #include "ICamera.hpp" #include "CFPSCamera.hpp" #include "CFreeLockCamera.hpp" -#include "COrbitCamera.hpp" -#include "CArcballCamera.hpp" -#include "CTurntableCamera.hpp" +#include "CSphericalTargetCamera.hpp" #include "glm/glm/gtc/quaternion.hpp" namespace nbl::hlsl @@ -38,12 +36,8 @@ class CTargetPoseController if (!camera) return false; - if (auto* orbit = dynamic_cast(camera)) + if (auto* orbit = dynamic_cast(camera)) return buildOrbitEvents(orbit, target, out); - if (auto* arcball = dynamic_cast(camera)) - return buildOrbitEvents(arcball, target, out); - if (auto* turntable = dynamic_cast(camera)) - return buildOrbitEvents(turntable, target, out); return buildFreeEvents(camera, target, out); } diff --git a/common/include/camera/CTopDownCamera.hpp b/common/include/camera/CTopDownCamera.hpp new file mode 100644 index 000000000..baa178b3c --- /dev/null +++ b/common/include/camera/CTopDownCamera.hpp @@ -0,0 +1,114 @@ +#ifndef _C_TOPDOWN_CAMERA_HPP_ +#define _C_TOPDOWN_CAMERA_HPP_ + +#include +#include + +#include "CSphericalTargetCamera.hpp" + +namespace nbl::hlsl +{ + +class CTopDownCamera final : public CSphericalTargetCamera +{ +public: + using base_t = CSphericalTargetCamera; + + CTopDownCamera(const float64_t3& position, const float64_t3& target) + : base_t(position, target) + { + m_v = TopDownPitch; + applyPose(); + } + ~CTopDownCamera() = default; + + const base_t::keyboard_to_virtual_events_t getKeyboardMappingPreset() const override { return m_keyboard_to_virtual_events_preset; } + const base_t::mouse_to_virtual_events_t getMouseMappingPreset() const override { return m_mouse_to_virtual_events_preset; } + const base_t::imguizmo_to_virtual_events_t getImguizmoMappingPreset() const override { return m_imguizmo_to_virtual_events_preset; } + + const typename base_t::CGimbal& getGimbal() override { return m_gimbal; } + + virtual bool manipulate(std::span virtualEvents, const float64_t4x4* referenceFrame = nullptr) override + { + if (not virtualEvents.size() and not referenceFrame) + return false; + + auto impulse = m_gimbal.accumulate(virtualEvents); + + const double deltaYaw = impulse.dVirtualRotation.y * m_rotationSpeedScale; + constexpr double translateScalar = 0.01; + const double panScalar = translateScalar * m_moveSpeedScale; + const double deltaPanX = impulse.dVirtualTranslate.x * panScalar; + const double deltaPanY = impulse.dVirtualTranslate.y * panScalar; + const double deltaDistance = impulse.dVirtualTranslate.z * translateScalar; + + m_u += deltaYaw; + m_v = TopDownPitch; + m_distance = std::clamp(m_distance + static_cast(deltaDistance), MinDistance, MaxDistance); + + const auto basis = computeBasis(m_u, m_v, m_distance); + if (deltaPanX != 0.0 || deltaPanY != 0.0) + m_targetPosition += basis.right * deltaPanX + basis.up * deltaPanY; + + return applyPose(); + } + + virtual const uint32_t getAllowedVirtualEvents() override { return AllowedVirtualEvents; } + virtual const std::string_view getIdentifier() override { return "Top-Down Camera"; } + +private: + static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate | CVirtualGimbalEvent::Rotate; + static inline constexpr double TopDownPitch = -1.5707963267948966; + + static inline const auto m_keyboard_to_virtual_events_preset = []() + { + typename base_t::keyboard_to_virtual_events_t preset; + + preset[ui::E_KEY_CODE::EKC_W] = CVirtualGimbalEvent::MoveUp; + preset[ui::E_KEY_CODE::EKC_S] = CVirtualGimbalEvent::MoveDown; + preset[ui::E_KEY_CODE::EKC_A] = CVirtualGimbalEvent::MoveLeft; + preset[ui::E_KEY_CODE::EKC_D] = CVirtualGimbalEvent::MoveRight; + preset[ui::E_KEY_CODE::EKC_Q] = CVirtualGimbalEvent::MoveBackward; + preset[ui::E_KEY_CODE::EKC_E] = CVirtualGimbalEvent::MoveForward; + preset[ui::E_KEY_CODE::EKC_J] = CVirtualGimbalEvent::PanLeft; + preset[ui::E_KEY_CODE::EKC_L] = CVirtualGimbalEvent::PanRight; + + return preset; + }(); + + static inline const auto m_mouse_to_virtual_events_preset = []() + { + typename base_t::mouse_to_virtual_events_t preset; + + preset[ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_X] = CVirtualGimbalEvent::PanRight; + preset[ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_X] = CVirtualGimbalEvent::PanLeft; + preset[ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_Y] = CVirtualGimbalEvent::MoveUp; + preset[ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_Y] = CVirtualGimbalEvent::MoveDown; + preset[ui::E_MOUSE_CODE::EMC_VERTICAL_POSITIVE_SCROLL] = CVirtualGimbalEvent::MoveForward; + preset[ui::E_MOUSE_CODE::EMC_HORIZONTAL_POSITIVE_SCROLL] = CVirtualGimbalEvent::MoveForward; + preset[ui::E_MOUSE_CODE::EMC_VERTICAL_NEGATIVE_SCROLL] = CVirtualGimbalEvent::MoveBackward; + preset[ui::E_MOUSE_CODE::EMC_HORIZONTAL_NEGATIVE_SCROLL] = CVirtualGimbalEvent::MoveBackward; + + return preset; + }(); + + static inline const auto m_imguizmo_to_virtual_events_preset = []() + { + typename base_t::imguizmo_to_virtual_events_t preset; + + preset[CVirtualGimbalEvent::MoveForward] = CVirtualGimbalEvent::MoveForward; + preset[CVirtualGimbalEvent::MoveBackward] = CVirtualGimbalEvent::MoveBackward; + preset[CVirtualGimbalEvent::MoveLeft] = CVirtualGimbalEvent::MoveLeft; + preset[CVirtualGimbalEvent::MoveRight] = CVirtualGimbalEvent::MoveRight; + preset[CVirtualGimbalEvent::MoveUp] = CVirtualGimbalEvent::MoveUp; + preset[CVirtualGimbalEvent::MoveDown] = CVirtualGimbalEvent::MoveDown; + preset[CVirtualGimbalEvent::PanLeft] = CVirtualGimbalEvent::PanLeft; + preset[CVirtualGimbalEvent::PanRight] = CVirtualGimbalEvent::PanRight; + + return preset; + }(); +}; + +} + +#endif diff --git a/common/include/camera/CTurntableCamera.hpp b/common/include/camera/CTurntableCamera.hpp index ca1fc9b9c..3cd0f750c 100644 --- a/common/include/camera/CTurntableCamera.hpp +++ b/common/include/camera/CTurntableCamera.hpp @@ -8,20 +8,20 @@ #include #include -#include "ICamera.hpp" +#include "CSphericalTargetCamera.hpp" namespace nbl::hlsl { -class CTurntableCamera final : public ICamera +class CTurntableCamera final : public CSphericalTargetCamera { public: - using base_t = ICamera; + using base_t = CSphericalTargetCamera; CTurntableCamera(const float64_t3& position, const float64_t3& target) - : base_t(), m_targetPosition(target), m_distance(1.0f), m_gimbal({ .position = position, .orientation = glm::quat(glm::vec3(0.0f)) }) + : base_t(position, target) { - initFromPosition(position); + m_v = std::clamp(m_v, MinPitch, MaxPitch); applyPose(); } ~CTurntableCamera() = default; @@ -32,22 +32,7 @@ class CTurntableCamera final : public ICamera const typename base_t::CGimbal& getGimbal() override { return m_gimbal; } - inline bool setDistance(float d) - { - const auto clamped = std::clamp(d, MinDistance, MaxDistance); - const bool ok = clamped == d; - m_distance = clamped; - return ok; - } - - inline void target(const float64_t3& p) { m_targetPosition = p; } - inline float64_t3 getTarget() const { return m_targetPosition; } - - inline float getDistance() { return m_distance; } - inline double getU() { return u; } - inline double getV() { return v; } - - virtual bool manipulate(std::span virtualEvents, const float64_t4x4 const* referenceFrame = nullptr) override + virtual bool manipulate(std::span virtualEvents, const float64_t4x4* referenceFrame = nullptr) override { if (not virtualEvents.size() and not referenceFrame) return false; @@ -60,8 +45,8 @@ class CTurntableCamera final : public ICamera constexpr double translateScalar = 0.01; const double deltaDistance = impulse.dVirtualTranslate.z * translateScalar; - u += deltaYaw; - v = std::clamp(v + deltaPitch, MinPitch, MaxPitch); + m_u += deltaYaw; + m_v = std::clamp(m_v + deltaPitch, MinPitch, MaxPitch); m_distance = std::clamp(m_distance + static_cast(deltaDistance), MinDistance, MaxDistance); return applyPose(); @@ -70,75 +55,15 @@ class CTurntableCamera final : public ICamera virtual const uint32_t getAllowedVirtualEvents() override { return AllowedVirtualEvents; } virtual const std::string_view getIdentifier() override { return "Turntable Camera"; } - static inline constexpr float MinDistance = 0.1f; - static inline constexpr float MaxDistance = 10000.f; + static inline constexpr float MinDistance = base_t::MinDistance; + static inline constexpr float MaxDistance = base_t::MaxDistance; private: - float64_t3 m_targetPosition; - float m_distance; - typename base_t::CGimbal m_gimbal; - double u = {}; - double v = {}; static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate | CVirtualGimbalEvent::Rotate; static inline constexpr double MaxPitch = glm::radians(89.0); static inline constexpr double MinPitch = -MaxPitch; - inline float64_t3 S(double su, double sv) const - { - return float64_t3 - { - std::cos(sv) * std::cos(su), - std::cos(sv) * std::sin(su), - std::sin(sv) - }; - } - - inline float64_t3 Sdv(double su, double sv) const - { - return float64_t3 - { - -std::sin(sv) * std::cos(su), - -std::sin(sv) * std::sin(su), - std::cos(sv) - }; - } - - inline void initFromPosition(const float64_t3& position) - { - const auto offset = position - m_targetPosition; - const double dist = length(offset); - const double safeDist = std::isfinite(dist) && dist > 0.0 ? dist : static_cast(MinDistance); - m_distance = std::clamp(static_cast(safeDist), MinDistance, MaxDistance); - const auto local = offset / static_cast(m_distance); - u = std::atan2(local.y, local.x); - v = std::asin(std::clamp(local.z, -1.0, 1.0)); - v = std::clamp(v, MinPitch, MaxPitch); - } - - inline bool applyPose() - { - const auto localSpherePosition = S(u, v) * static_cast(m_distance); - const auto newPosition = localSpherePosition + m_targetPosition; - const auto newForward = normalize(-localSpherePosition); - const auto newUp = normalize(Sdv(u, v)); - const auto newRight = normalize(cross(newUp, newForward)); - const auto newOrientation = glm::quat_cast(glm::dmat3{ newRight, newUp, newForward }); - - m_gimbal.begin(); - { - m_gimbal.setPosition(newPosition); - m_gimbal.setOrientation(newOrientation); - } - m_gimbal.end(); - - const bool manipulated = bool(m_gimbal.getManipulationCounter()); - if (manipulated) - m_gimbal.updateView(); - - return manipulated; - } - static inline const auto m_keyboard_to_virtual_events_preset = []() { typename base_t::keyboard_to_virtual_events_t preset; diff --git a/common/include/camera/ICamera.hpp b/common/include/camera/ICamera.hpp index aceec1bc2..6560aa01f 100644 --- a/common/include/camera/ICamera.hpp +++ b/common/include/camera/ICamera.hpp @@ -51,7 +51,7 @@ class ICamera : public IGimbalController, virtual public core::IReferenceCounted // Manipulates camera with virtual events, returns true if *any* manipulation happens, it may fail partially or fully because each camera type has certain constraints which determine how it actually works // TODO: this really needs to be moved to more abstract interface, eg. IObjectTransform or something and ICamera should inherit it (its also an object!) - virtual bool manipulate(std::span virtualEvents, const float64_t4x4 const* referenceFrame = nullptr) = 0; + virtual bool manipulate(std::span virtualEvents, const float64_t4x4* referenceFrame = nullptr) = 0; // VirtualEventType bitmask for a camera view gimbal manipulation requests filtering virtual const uint32_t getAllowedVirtualEvents() = 0u; @@ -83,4 +83,4 @@ class ICamera : public IGimbalController, virtual public core::IReferenceCounted } -#endif // _I_CAMERA_HPP_ \ No newline at end of file +#endif // _I_CAMERA_HPP_ diff --git a/common/include/camera/IGimbal.hpp b/common/include/camera/IGimbal.hpp index b4114570e..84a7e3750 100644 --- a/common/include/camera/IGimbal.hpp +++ b/common/include/camera/IGimbal.hpp @@ -396,7 +396,7 @@ namespace nbl::hlsl //! Returns true if gimbal records a manipulation inline bool isManipulating() const { return m_isManipulating; } - bool extractReferenceTransform(CReferenceTransform* out, const float64_t4x4 const* referenceFrame = nullptr) + bool extractReferenceTransform(CReferenceTransform* out, const float64_t4x4* referenceFrame = nullptr) { if (not out) return false; From f74c70123618e66924d39366d2d28e6ebc1269dd Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 14 Feb 2026 20:22:57 +0100 Subject: [PATCH 098/205] Refactor UI app and camera math internals --- 61_UI/CMakeLists.txt | 1 + 61_UI/include/app/App.hpp | 1922 ++++++ 61_UI/include/app/AppTypes.hpp | 28 + 61_UI/include/app/impl/AppControlPanel.inl | 853 +++ 61_UI/include/app/impl/AppImGuiListen.inl | 478 ++ 61_UI/include/app/impl/AppInit.inl | 1417 ++++ 61_UI/include/app/impl/AppTransformEditor.inl | 221 + 61_UI/include/app/impl/AppUpdate.inl | 657 ++ 61_UI/include/app/impl/AppWorkLoop.inl | 313 + 61_UI/include/keysmapping.hpp | 231 +- 61_UI/include/transform.hpp | 149 +- 61_UI/main.cpp | 5872 +---------------- 61_UI/src/keysmapping.cpp | 232 + 61_UI/src/transform.cpp | 144 + common/include/camera/CArcballCamera.hpp | 2 +- common/include/camera/CChaseCamera.hpp | 4 +- common/include/camera/CDollyCamera.hpp | 4 +- common/include/camera/CDollyZoomCamera.hpp | 4 +- common/include/camera/CFPSCamera.hpp | 24 +- .../include/camera/CTargetPoseController.hpp | 6 +- common/include/camera/CTurntableCamera.hpp | 2 +- common/include/camera/ICamera.hpp | 6 +- common/include/camera/IGimbalController.hpp | 6 +- common/include/camera/IPlanarProjection.hpp | 6 +- 24 files changed, 6309 insertions(+), 6273 deletions(-) create mode 100644 61_UI/include/app/App.hpp create mode 100644 61_UI/include/app/AppTypes.hpp create mode 100644 61_UI/include/app/impl/AppControlPanel.inl create mode 100644 61_UI/include/app/impl/AppImGuiListen.inl create mode 100644 61_UI/include/app/impl/AppInit.inl create mode 100644 61_UI/include/app/impl/AppTransformEditor.inl create mode 100644 61_UI/include/app/impl/AppUpdate.inl create mode 100644 61_UI/include/app/impl/AppWorkLoop.inl create mode 100644 61_UI/src/keysmapping.cpp diff --git a/61_UI/CMakeLists.txt b/61_UI/CMakeLists.txt index e49af2205..ebf353c0a 100644 --- a/61_UI/CMakeLists.txt +++ b/61_UI/CMakeLists.txt @@ -1,5 +1,6 @@ if(NBL_BUILD_IMGUI) set(NBL_EXTRA_SOURCES + "${CMAKE_CURRENT_SOURCE_DIR}/src/keysmapping.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/transform.cpp" ) diff --git a/61_UI/include/app/App.hpp b/61_UI/include/app/App.hpp new file mode 100644 index 000000000..f6d43f6ab --- /dev/null +++ b/61_UI/include/app/App.hpp @@ -0,0 +1,1922 @@ +#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 +#include +#include "nlohmann/json.hpp" +#include "argparse/argparse.hpp" +using json = nlohmann::json; + +#include "common.hpp" +#include "keysmapping.hpp" +#include "app/AppTypes.hpp" +#include "camera/CCubeProjection.hpp" +#include "glm/glm/ext/matrix_clip_space.hpp" // TODO: TESTING +#include "glm/gtc/quaternion.hpp" +#include "nbl/ext/ScreenShot/ScreenShot.h" +#include "nbl/this_example/builtin/build/spirv/keys.hpp" +#if __has_include("nbl/this_example/builtin/CArchive.h") +#include "nbl/this_example/builtin/CArchive.h" +#endif +#if __has_include("nbl/this_example/builtin/build/CArchive.h") +#include "nbl/this_example/builtin/build/CArchive.h" +#endif + +class CUIEventCallback : public nbl::video::ISmoothResizeSurface::ICallback // I cannot use common CEventCallback because I MUST inherit this callback in order to use smooth resize surface with window callback (for my input events) +{ +public: + CUIEventCallback(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)) {} + CUIEventCallback() {} + + void setLogger(nbl::system::logger_opt_smart_ptr& logger) + { + m_logger = logger; + } + void setInputSystem(nbl::core::smart_refctd_ptr&& m_inputSystem) + { + m_inputSystem = std::move(m_inputSystem); + } +private: + + void onMouseConnected_impl(nbl::core::smart_refctd_ptr&& mch) override + { + m_logger.log("A mouse %p has been connected", nbl::system::ILogger::ELL_INFO, mch.get()); + m_inputSystem.get()->add(m_inputSystem.get()->m_mouse, std::move(mch)); + } + void onMouseDisconnected_impl(nbl::ui::IMouseEventChannel* mch) override + { + m_logger.log("A mouse %p has been disconnected", nbl::system::ILogger::ELL_INFO, mch); + m_inputSystem.get()->remove(m_inputSystem.get()->m_mouse, mch); + } + void onKeyboardConnected_impl(nbl::core::smart_refctd_ptr&& kbch) override + { + m_logger.log("A keyboard %p has been connected", nbl::system::ILogger::ELL_INFO, kbch.get()); + m_inputSystem.get()->add(m_inputSystem.get()->m_keyboard, std::move(kbch)); + } + void onKeyboardDisconnected_impl(nbl::ui::IKeyboardEventChannel* kbch) override + { + m_logger.log("A keyboard %p has been disconnected", nbl::system::ILogger::ELL_INFO, kbch); + m_inputSystem.get()->remove(m_inputSystem.get()->m_keyboard, kbch); + } + +private: + nbl::core::smart_refctd_ptr m_inputSystem = nullptr; + nbl::system::logger_opt_smart_ptr m_logger = nullptr; +}; + +class CSwapchainResources final : public ISmoothResizeSurface::ISwapchainResources +{ +public: + // Because we blit to the swapchain image asynchronously, we need a queue which can not only present but also perform graphics commands. + // If we for example used a compute shader to tonemap and MSAA resolve, we'd request the COMPUTE_BIT here. + constexpr static inline IQueue::FAMILY_FLAGS RequiredQueueFlags = IQueue::FAMILY_FLAGS::GRAPHICS_BIT; + + inline uint8_t getLastImageIndex() const { return m_lastImageIndex; } + +protected: + // We can return `BLIT_BIT` here, because the Source Image will be already in the correct layout to be used for the present + 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; + + // Ownership of the Source Blit Image, not the Swapchain Image + const bool needToAcquireSrcOwnership = qFamToAcquireSrcFrom != IQueue::FamilyIgnored; + // Should never get asked to transfer ownership if the source is concurrent sharing + assert(!source.image->getCachedCreationParams().isConcurrentSharing() || !needToAcquireSrcOwnership); + + const auto blitDstLayout = IGPUImage::LAYOUT::TRANSFER_DST_OPTIMAL; + IGPUCommandBuffer::SPipelineBarrierDependencyInfo depInfo = {}; + + // barrier before to transition the swapchain image layout + using image_barrier_t = decltype(depInfo.imgBarriers)::element_type; + const image_barrier_t preBarriers[2] = { + { + .barrier = { + .dep = { + .srcStageMask = asset::PIPELINE_STAGE_FLAGS::NONE, // acquire isn't a stage + .srcAccessMask = asset::ACCESS_FLAGS::NONE, // performs no accesses + .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, // I do not care about previous contents of the swapchain + .newLayout = blitDstLayout + }, + { + .barrier = { + .dep = { + // when acquiring ownership the source access masks don't matter + .srcStageMask = asset::PIPELINE_STAGE_FLAGS::NONE, + // Acquire must Happen-Before Semaphore wait, but neither has a true stage so NONE here + // https://github.com/KhronosGroup/Vulkan-Docs/issues/2319 + // If no ownership acquire needed then this dep info won't be used at all + .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 + // no layout transition, already in the correct layout for the blit + } + }; + // We only barrier the source image if we need to acquire ownership, otherwise thanks to Timeline Semaphores all sync is good + depInfo.imgBarriers = { preBarriers,needToAcquireSrcOwnership ? 2ull : 1ull }; + success &= cmdbuf->pipelineBarrier(asset::EDF_NONE, depInfo); + + // TODO: Implement scaling modes other than a plain STRETCH, and allow for using subrectangles of the initial contents + { + 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); + } + + // Barrier after, note that I don't care about preserving the contents of the Triple Buffer when the Render queue starts writing to it again. + // Therefore no ownership release, and no layout transition. + const image_barrier_t postBarrier[1] = { + { + .barrier = { + // When transitioning the image to VK_IMAGE_LAYOUT_SHARED_PRESENT_KHR or VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, there is no need to delay subsequent processing, + // or perform any visibility operations (as vkQueuePresentKHR performs automatic visibility operations). + // To achieve this, the dstAccessMask member of the VkImageMemoryBarrier should be set to 0, and the dstStageMask parameter should be set to VK_PIPELINE_STAGE_2_NONE + .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; +}; + +static smart_refctd_ptr createAttachmentView(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)); +} + +static 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)); +} + +/* + Renders scene texture to an offline + framebuffer which color attachment + is then sampled into a imgui window. + + Written with Nabla, it's UI extension + and got integrated with ImGuizmo to + handle scene's object translations. +*/ + +class App final : public examples::SimpleWindowedApplication +{ + using base_t = examples::SimpleWindowedApplication; + using clock_t = std::chrono::steady_clock; + + constexpr static inline clock_t::duration DisplayImageDuration = std::chrono::milliseconds(900); + constexpr static inline auto sceneRenderDepthFormat = EF_D32_SFLOAT; + constexpr static inline auto finalSceneRenderFormat = EF_R8G8B8A8_SRGB; + constexpr static inline IGPUCommandBuffer::SClearColorValue SceneClearColor = { .float32 = {0.014f,0.018f,0.030f,1.f} }; + constexpr static inline IGPUCommandBuffer::SClearDepthStencilValue SceneClearDepth = { .depth = 0.f }; + + 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 + { + // So let's create our Window and Surface then! + if (!m_surface) + { + { + const auto dpyInfo = m_winMgr->getPrimaryDisplayInfo(); + auto windowCallback = core::make_smart_refctd_ptr(smart_refctd_ptr(m_inputSystem), smart_refctd_ptr(m_logger)); + + IWindow::SCreationParams params = {}; + params.callback = core::make_smart_refctd_ptr(); + params.width = dpyInfo.resX; + params.height = dpyInfo.resY; + params.x = 32; + params.y = 32; + params.flags = IWindow::ECF_INPUT_FOCUS | IWindow::ECF_CAN_RESIZE | IWindow::ECF_CAN_MAXIMIZE | IWindow::ECF_CAN_MINIMIZE; + params.windowCaption = "[Nabla Engine] UI App"; + params.callback = windowCallback; + + 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) + { + m_window->getManager()->maximize(m_window.get()); + auto* cc = m_window->getCursorControl(); + cc->setVisible(true); + + return { {m_surface->getSurface()/*,EQF_NONE*/} }; + } + + return {}; + } + + inline bool onAppInitialized(smart_refctd_ptr&& system) override + { + #include "app/impl/AppInit.inl" + } + + bool updateGUIDescriptorSet() + { + // UI texture atlas + our camera scene textures, note we don't create info & write pair for the font sampler because UI extension's is immutable and baked into DS layout + static std::array descriptorInfo; + static IGPUDescriptorSet::SWriteDescriptorSet writes[TotalUISampleTexturesAmount]; + + descriptorInfo[nbl::ext::imgui::UI::FontAtlasTexId].info.image.imageLayout = IImage::LAYOUT::READ_ONLY_OPTIMAL; + descriptorInfo[nbl::ext::imgui::UI::FontAtlasTexId].desc = core::smart_refctd_ptr(m_ui.manager->getFontAtlasView()); + writes[nbl::ext::imgui::UI::FontAtlasTexId].info = descriptorInfo.data() + nbl::ext::imgui::UI::FontAtlasTexId; + + for (uint32_t i = 0; i < windowBindings.size(); ++i) + { + const auto textureIx = i + 1u; + + descriptorInfo[textureIx].info.image.imageLayout = IImage::LAYOUT::READ_ONLY_OPTIMAL; + descriptorInfo[textureIx].desc = windowBindings[i].sceneColorView; + + writes[textureIx].info = descriptorInfo.data() + textureIx; + writes[textureIx].info = descriptorInfo.data() + textureIx; + } + + for (uint32_t i = 0; i < descriptorInfo.size(); ++i) + { + writes[i].dstSet = m_ui.descriptorSet.get(); + writes[i].binding = 0u; + writes[i].arrayElement = i; + writes[i].count = 1u; + } + + return m_device->updateDescriptorSets(writes, {}); + } + + inline void workLoopBody() override + { + #include "app/impl/AppWorkLoop.inl" + } + + inline void paceScriptedVisualDebugFrame() + { + if (!(m_scriptedInput.enabled && m_scriptedInput.visualDebug)) + { + m_scriptedInput.framePacerInitialized = 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.framePacerInitialized) + { + m_scriptedInput.framePacerInitialized = true; + m_scriptedInput.framePacerNext = now + frameDuration; + return; + } + + if (now < m_scriptedInput.framePacerNext) + std::this_thread::sleep_until(m_scriptedInput.framePacerNext); + + auto postSleepNow = std::chrono::steady_clock::now(); + while (m_scriptedInput.framePacerNext < postSleepNow) + m_scriptedInput.framePacerNext += frameDuration; + } + + inline bool keepRunning() override + { + if (m_scriptedInput.enabled && m_scriptedInput.hardFail && m_scriptedInput.failed) + { + if (!m_ciMode || m_ciScreenshotDone) + std::exit(EXIT_FAILURE); + } + if (m_ciMode && m_ciStartedAt != clock_t::time_point::min()) + { + const auto elapsed = clock_t::now() - m_ciStartedAt; + if (elapsed > 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_ciMode && m_ciScreenshotDone) + { + if (m_scriptedInput.enabled && m_scriptedInput.nextCaptureIndex < m_scriptedInput.captureFrames.size()) + return true; + return false; + } + if (m_surface->irrecoverable()) + return false; + + return true; + } + + inline bool onAppTerminated() override + { + return base_t::onAppTerminated(); + } + + inline void update() + { + #include "app/impl/AppUpdate.inl" + } + + private: + struct CUILogFormatter final : public nbl::system::ILogger + { + CUILogFormatter() : ILogger(ILogger::DefaultLogMask()) {} + + std::string format(E_LOG_LEVEL level, 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&, E_LOG_LEVEL, va_list) override {} + }; + + struct VirtualEventLogEntry + { + uint64_t frame = 0; + CVirtualGimbalEvent::VirtualEventType type = CVirtualGimbalEvent::None; + float64_t magnitude = 0.0; + std::string source; + std::string controller; + std::string camera; + uint32_t planarIx = 0u; + std::string line; + }; + + struct CameraPreset + { + std::string name; + std::string identifier; + float64_t3 position = float64_t3(0.0); + glm::quat orientation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f); + float distance = 0.f; + bool hasDistance = false; + double orbitU = 0.0; + double orbitV = 0.0; + float orbitDistance = 0.f; + bool hasOrbitState = false; + }; + + struct CameraKeyframe + { + CameraPreset preset; + float time = 0.f; + }; + + struct CameraPlaybackState + { + bool playing = false; + bool loop = true; + bool overrideInput = true; + float speed = 1.f; + float time = 0.f; + }; + + struct CameraControlSettings + { + bool mirrorInput = false; + bool worldTranslate = false; + float keyboardScale = 0.00625f; + float mouseMoveScale = 1.0f; + float mouseScrollScale = 1.0f; + float translationScale = 1.0f; + float rotationScale = 1.0f; + }; + + struct CameraConstraintSettings + { + bool enabled = false; + bool clampPitch = false; + bool clampYaw = false; + bool clampRoll = false; + bool clampDistance = false; + float pitchMinDeg = -80.f; + float pitchMaxDeg = 80.f; + float yawMinDeg = -180.f; + float yawMaxDeg = 180.f; + float rollMinDeg = -180.f; + float rollMaxDeg = 180.f; + float minDistance = 0.1f; + float maxDistance = 1000.f; + }; + + inline ICamera* getActiveCamera() + { + auto& binding = windowBindings[activeRenderWindowIx]; + auto& planar = m_planarProjections[binding.activePlanarIx]; + return planar ? planar->getCamera() : nullptr; + } + + inline bool isOrbitLikeCamera(ICamera* camera) + { + return dynamic_cast(camera); + } + + template + inline bool withOrbitLikeCamera(ICamera* camera, Fn&& fn) + { + if (auto* orbit = dynamic_cast(camera)) + { + fn(orbit); + return true; + } + return false; + } + + inline std::string_view getCameraTypeLabel(const ICamera* camera) const + { + if (dynamic_cast(camera)) + return "FPS"; + if (dynamic_cast(camera)) + return "Free"; + if (dynamic_cast(camera)) + return "Orbit"; + if (dynamic_cast(camera)) + return "Arcball"; + if (dynamic_cast(camera)) + return "Turntable"; + if (dynamic_cast(camera)) + return "TopDown"; + if (dynamic_cast(camera)) + return "Isometric"; + if (dynamic_cast(camera)) + return "Chase"; + if (dynamic_cast(camera)) + return "Dolly"; + if (dynamic_cast(camera)) + return "Dolly Zoom"; + if (dynamic_cast(camera)) + return "Path"; + return "Unknown"; + } + + inline std::string_view getCameraTypeDescription(const ICamera* camera) const + { + if (dynamic_cast(camera)) + return "First-person WASD + mouse look"; + if (dynamic_cast(camera)) + return "Free-fly 6DOF with full rotation"; + if (dynamic_cast(camera)) + return "Orbit around target with dolly"; + if (dynamic_cast(camera)) + return "Arcball trackball around target"; + if (dynamic_cast(camera)) + return "Turntable yaw/pitch around target"; + if (dynamic_cast(camera)) + return "Fixed pitch top-down pan"; + if (dynamic_cast(camera)) + return "Fixed isometric view with pan"; + if (dynamic_cast(camera)) + return "Target follow with chase controls"; + if (dynamic_cast(camera)) + return "Rig truck/dolly with look-at"; + if (dynamic_cast(camera)) + return "Orbit with dolly-zoom FOV"; + if (dynamic_cast(camera)) + return "Move along a target path"; + return "Unspecified camera behavior"; + } + + inline void syncVisualDebugWindowBindings() + { + if (!(m_scriptedInput.enabled && m_scriptedInput.visualDebug)) + return; + if (windowBindings.size() < 2u || m_planarProjections.empty()) + return; + + auto& perspectiveBinding = windowBindings[0u]; + if (perspectiveBinding.activePlanarIx >= m_planarProjections.size()) + return; + auto& perspectivePlanar = m_planarProjections[perspectiveBinding.activePlanarIx]; + if (!perspectivePlanar) + return; + if (!perspectiveBinding.lastBoundPerspectivePresetProjectionIx.has_value()) + perspectiveBinding.pickDefaultProjections(perspectivePlanar->getPlanarProjections()); + if (perspectiveBinding.lastBoundPerspectivePresetProjectionIx.has_value()) + perspectiveBinding.boundProjectionIx = perspectiveBinding.lastBoundPerspectivePresetProjectionIx.value(); + + auto& orthoBinding = windowBindings[1u]; + if (orthoBinding.activePlanarIx != perspectiveBinding.activePlanarIx) + { + orthoBinding.activePlanarIx = perspectiveBinding.activePlanarIx; + auto& orthoPlanar = m_planarProjections[orthoBinding.activePlanarIx]; + if (!orthoPlanar) + return; + orthoBinding.pickDefaultProjections(orthoPlanar->getPlanarProjections()); + } + if (orthoBinding.activePlanarIx >= m_planarProjections.size()) + return; + auto& orthoPlanar = m_planarProjections[orthoBinding.activePlanarIx]; + if (!orthoPlanar) + return; + if (!orthoBinding.lastBoundOrthoPresetProjectionIx.has_value()) + orthoBinding.pickDefaultProjections(orthoPlanar->getPlanarProjections()); + if (orthoBinding.lastBoundOrthoPresetProjectionIx.has_value()) + orthoBinding.boundProjectionIx = orthoBinding.lastBoundOrthoPresetProjectionIx.value(); + } + + inline bool projectWorldPointToViewport( + const float32_t4x4& viewProjMatrix, + const float32_t3& worldPoint, + const ImVec2& viewportPos, + const ImVec2& viewportSize, + ImVec2& outScreen) const + { + if (viewportSize.x <= 1.0f || viewportSize.y <= 1.0f) + return false; + + const auto clip = mul(viewProjMatrix, float32_t4(worldPoint.x, worldPoint.y, worldPoint.z, 1.0f)); + if (!std::isfinite(clip.x) || !std::isfinite(clip.y) || !std::isfinite(clip.z) || !std::isfinite(clip.w)) + return false; + + const float absW = std::abs(clip.w); + if (absW < 1e-5f) + return false; + + const float invW = 1.0f / clip.w; + const float ndcX = clip.x * invW; + const float ndcY = clip.y * invW; + const float ndcZ = clip.z * invW; + + if (!std::isfinite(ndcX) || !std::isfinite(ndcY) || !std::isfinite(ndcZ)) + return false; + if (std::abs(ndcX) > 100.0f || std::abs(ndcY) > 100.0f || std::abs(ndcZ) > 100.0f) + return false; + + outScreen.x = viewportPos.x + (ndcX * 0.5f + 0.5f) * viewportSize.x; + outScreen.y = viewportPos.y + (-ndcY * 0.5f + 0.5f) * viewportSize.y; + return std::isfinite(outScreen.x) && std::isfinite(outScreen.y); + } + + inline void drawWorldReferenceOverlay( + const ImVec2& viewportPos, + const ImVec2& viewportSize, + const float32_t4x4& viewMatrix, + const float32_t4x4& projectionMatrix, + bool leftHandedProjection, + float nearPlane, + float farPlane) + { + if (!(m_scriptedInput.enabled && m_scriptedInput.visualDebug)) + return; + if (viewportSize.x <= 1.0f || viewportSize.y <= 1.0f) + return; + + auto* drawList = ImGui::GetWindowDrawList(); + if (!drawList) + return; + + const float safeNear = std::max(nearPlane, 0.001f); + const float safeFar = std::max(farPlane, safeNear + 0.001f); + const auto depthOfViewPoint = [&](const float32_t4& viewPoint) -> float + { + return leftHandedProjection ? viewPoint.z : -viewPoint.z; + }; + const auto ndcToViewport = [&](const ImVec2& ndc) -> ImVec2 + { + return ImVec2( + viewportPos.x + (ndc.x * 0.5f + 0.5f) * viewportSize.x, + viewportPos.y + (-ndc.y * 0.5f + 0.5f) * viewportSize.y); + }; + const auto clipSegmentByDepthRange = [&](float32_t4& viewA, float32_t4& viewB) -> bool + { + const float32_t4 a0 = viewA; + const float32_t4 b0 = viewB; + const float32_t4 delta = b0 - a0; + const float depthA = depthOfViewPoint(a0); + const float depthB = depthOfViewPoint(b0); + + float tEnter = 0.0f; + float tExit = 1.0f; + const auto clipByConstraint = [&](float fa, float fb) -> bool + { + if (fa < 0.0f && fb < 0.0f) + return false; + if (fa >= 0.0f && fb >= 0.0f) + return true; + + const float denom = fa - fb; + if (std::abs(denom) < 1e-6f) + return false; + const float t = std::clamp(fa / denom, 0.0f, 1.0f); + + if (fa < 0.0f) + tEnter = std::max(tEnter, t); + else + tExit = std::min(tExit, t); + + return tEnter <= tExit; + }; + + if (!clipByConstraint(depthA - safeNear, depthB - safeNear)) + return false; + if (!clipByConstraint(safeFar - depthA, safeFar - depthB)) + return false; + + viewA = a0 + delta * tEnter; + viewB = a0 + delta * tExit; + return true; + }; + const auto projectViewPointToNdc = [&](const float32_t4& viewPoint, ImVec2& outNdc) -> bool + { + const auto clip = mul(projectionMatrix, viewPoint); + if (!std::isfinite(clip.x) || !std::isfinite(clip.y) || !std::isfinite(clip.z) || !std::isfinite(clip.w)) + return false; + + const float absW = std::abs(clip.w); + if (absW < 1e-6f) + return false; + + const float invW = 1.0f / clip.w; + const float ndcX = clip.x * invW; + const float ndcY = clip.y * invW; + const float ndcZ = clip.z * invW; + if (!std::isfinite(ndcX) || !std::isfinite(ndcY) || !std::isfinite(ndcZ)) + return false; + if (std::abs(ndcX) > 1e4f || std::abs(ndcY) > 1e4f || std::abs(ndcZ) > 1e4f) + return false; + + outNdc = ImVec2(ndcX, ndcY); + return true; + }; + const auto clipNdcSegmentToViewport = [&](ImVec2& ndcA, ImVec2& ndcB) -> bool + { + float tEnter = 0.0f; + float tExit = 1.0f; + const float dx = ndcB.x - ndcA.x; + const float dy = ndcB.y - ndcA.y; + const auto clipTest = [&](float p, float q) -> bool + { + if (std::abs(p) < 1e-6f) + return q >= 0.0f; + + const float r = q / p; + if (p < 0.0f) + { + if (r > tExit) + return false; + tEnter = std::max(tEnter, r); + } + else + { + if (r < tEnter) + return false; + tExit = std::min(tExit, r); + } + return tEnter <= tExit; + }; + + if (!clipTest(-dx, ndcA.x + 1.0f)) + return false; + if (!clipTest(dx, 1.0f - ndcA.x)) + return false; + if (!clipTest(-dy, ndcA.y + 1.0f)) + return false; + if (!clipTest(dy, 1.0f - ndcA.y)) + return false; + + const ImVec2 a0 = ndcA; + ndcA = ImVec2(a0.x + dx * tEnter, a0.y + dy * tEnter); + ndcB = ImVec2(a0.x + dx * tExit, a0.y + dy * tExit); + return true; + }; + const auto projectWorldPointToViewportClipped = [&](const float32_t3& worldPoint, ImVec2& outScreen) -> bool + { + const auto viewPoint = mul(viewMatrix, float32_t4(worldPoint.x, worldPoint.y, worldPoint.z, 1.0f)); + if (!std::isfinite(viewPoint.x) || !std::isfinite(viewPoint.y) || !std::isfinite(viewPoint.z) || !std::isfinite(viewPoint.w)) + return false; + + const float depth = depthOfViewPoint(viewPoint); + if (depth < safeNear || depth > safeFar) + return false; + + ImVec2 ndcPoint = {}; + if (!projectViewPointToNdc(viewPoint, ndcPoint)) + return false; + if (ndcPoint.x < -1.0f || ndcPoint.x > 1.0f || ndcPoint.y < -1.0f || ndcPoint.y > 1.0f) + return false; + + outScreen = ndcToViewport(ndcPoint); + return std::isfinite(outScreen.x) && std::isfinite(outScreen.y); + }; + + auto drawWorldLine = [&](const float32_t3& aWorld, const float32_t3& bWorld, ImU32 color, float thickness) -> void + { + float32_t4 viewA = mul(viewMatrix, float32_t4(aWorld.x, aWorld.y, aWorld.z, 1.0f)); + float32_t4 viewB = mul(viewMatrix, float32_t4(bWorld.x, bWorld.y, bWorld.z, 1.0f)); + if (!std::isfinite(viewA.x) || !std::isfinite(viewA.y) || !std::isfinite(viewA.z) || !std::isfinite(viewA.w) || + !std::isfinite(viewB.x) || !std::isfinite(viewB.y) || !std::isfinite(viewB.z) || !std::isfinite(viewB.w)) + return; + if (!clipSegmentByDepthRange(viewA, viewB)) + return; + + ImVec2 ndcA = {}; + ImVec2 ndcB = {}; + if (!projectViewPointToNdc(viewA, ndcA)) + return; + if (!projectViewPointToNdc(viewB, ndcB)) + return; + if (!clipNdcSegmentToViewport(ndcA, ndcB)) + return; + + const ImVec2 screenA = ndcToViewport(ndcA); + const ImVec2 screenB = ndcToViewport(ndcB); + drawList->AddLine(screenA, screenB, color, thickness); + }; + + constexpr int gridHalfSteps = 8; + constexpr float gridStep = 2.0f; + const float gridHalfSize = static_cast(gridHalfSteps) * gridStep; + constexpr int gridMajorModulo = 1; + const ImU32 gridMajor = IM_COL32(136, 160, 194, 95); + + for (int i = -gridHalfSteps; i <= gridHalfSteps; ++i) + { + if (i == 0) + continue; + if ((i % gridMajorModulo) != 0) + continue; + const float c = static_cast(i) * gridStep; + drawWorldLine(float32_t3(c, 0.0f, -gridHalfSize), float32_t3(c, 0.0f, gridHalfSize), gridMajor, 1.3f); + drawWorldLine(float32_t3(-gridHalfSize, 0.0f, c), float32_t3(gridHalfSize, 0.0f, c), gridMajor, 1.3f); + } + + drawWorldLine(float32_t3(-gridHalfSize, 0.0f, 0.0f), float32_t3(gridHalfSize, 0.0f, 0.0f), IM_COL32(184, 204, 232, 170), 2.0f); + drawWorldLine(float32_t3(0.0f, 0.0f, -gridHalfSize), float32_t3(0.0f, 0.0f, gridHalfSize), IM_COL32(184, 204, 232, 170), 2.0f); + + constexpr float axisLength = 7.5f; + const float32_t3 origin = float32_t3(0.0f); + const float32_t3 xPos = float32_t3(axisLength, 0.0f, 0.0f); + const float32_t3 yPos = float32_t3(0.0f, axisLength, 0.0f); + const float32_t3 zPos = float32_t3(0.0f, 0.0f, axisLength); + const float32_t3 xNeg = float32_t3(-axisLength * 0.4f, 0.0f, 0.0f); + const float32_t3 yNeg = float32_t3(0.0f, -axisLength * 0.3f, 0.0f); + const float32_t3 zNeg = float32_t3(0.0f, 0.0f, -axisLength * 0.4f); + + drawWorldLine(origin, xPos, IM_COL32(244, 92, 92, 245), 3.2f); + drawWorldLine(origin, yPos, IM_COL32(124, 236, 132, 245), 3.2f); + drawWorldLine(origin, zPos, IM_COL32(106, 166, 255, 245), 3.2f); + drawWorldLine(origin, xNeg, IM_COL32(128, 74, 74, 180), 1.6f); + drawWorldLine(origin, yNeg, IM_COL32(74, 128, 78, 180), 1.6f); + drawWorldLine(origin, zNeg, IM_COL32(70, 88, 124, 180), 1.6f); + + auto drawAxisLabel = [&](const char* label, const float32_t3& worldPoint, ImU32 color) -> void + { + ImVec2 screenPos = {}; + if (!projectWorldPointToViewportClipped(worldPoint, screenPos)) + return; + drawList->AddText(ImVec2(screenPos.x + 4.0f, screenPos.y + 3.0f), color, label); + }; + + ImVec2 originScreen = {}; + if (projectWorldPointToViewportClipped(origin, originScreen)) + drawList->AddCircleFilled(originScreen, 4.0f, IM_COL32(240, 248, 255, 220), 16); + + drawAxisLabel("X", xPos, IM_COL32(255, 152, 152, 255)); + drawAxisLabel("Y", yPos, IM_COL32(172, 255, 178, 255)); + drawAxisLabel("Z", zPos, IM_COL32(172, 210, 255, 255)); + } + + inline void drawScriptVisualDebugOverlay(const ImVec2& displaySize) + { + if (!(m_scriptedInput.enabled && m_scriptedInput.visualDebug)) + return; + if (windowBindings.empty() || m_planarProjections.empty()) + return; + if (activeRenderWindowIx >= windowBindings.size()) + return; + + const auto& binding = windowBindings[activeRenderWindowIx]; + if (binding.activePlanarIx >= m_planarProjections.size()) + return; + + auto& planar = m_planarProjections[binding.activePlanarIx]; + if (!planar) + return; + auto* camera = planar->getCamera(); + if (!camera) + return; + + if (!m_scriptedInput.visualActivePlanarValid) + { + m_scriptedInput.visualActivePlanarValid = true; + m_scriptedInput.visualActivePlanarIx = binding.activePlanarIx; + m_scriptedInput.visualActivePlanarStartFrame = m_realFrameIx; + } + + const uint64_t elapsedFrames = (m_realFrameIx >= m_scriptedInput.visualActivePlanarStartFrame) ? + (m_realFrameIx - m_scriptedInput.visualActivePlanarStartFrame) : 0ull; + const float fps = std::max(1.f, m_scriptedInput.visualTargetFps); + const uint64_t holdFrames = static_cast(std::round(std::max(0.f, m_scriptedInput.visualCameraHoldSeconds) * fps)); + const uint64_t progressFrames = holdFrames ? std::min(elapsedFrames, holdFrames) : elapsedFrames; + + const auto cameraLabel = getCameraTypeLabel(camera); + std::string lineTop = "SCRIPT VISUAL DEBUG"; + std::string lineMid = "Camera " + std::to_string(binding.activePlanarIx + 1u) + "/" + std::to_string(m_planarProjections.size()) + " " + std::string(cameraLabel); + + char lineBottomBuffer[256] = {}; + if (holdFrames) + { + const double elapsedSeconds = static_cast(progressFrames) / static_cast(fps); + const double holdSeconds = static_cast(holdFrames) / static_cast(fps); + std::snprintf( + lineBottomBuffer, + sizeof(lineBottomBuffer), + "Planar %u Segment %.1f/%.1f s Frame %llu/%llu", + binding.activePlanarIx, + elapsedSeconds, + holdSeconds, + static_cast(progressFrames), + static_cast(holdFrames)); + } + else + { + std::snprintf( + lineBottomBuffer, + sizeof(lineBottomBuffer), + "Planar %u Frame %llu", + binding.activePlanarIx, + static_cast(m_realFrameIx)); + } + const std::string lineBottom(lineBottomBuffer); + + const float topSize = 50.f; + const float midSize = 38.f; + const float bottomSize = 28.f; + const float marginTop = 18.f; + const float padX = 24.f; + const float padY = 16.f; + const float gap = 6.f; + + ImFont* font = ImGui::GetFont(); + if (!font) + return; + + const float textWrap = std::numeric_limits::max(); + const ImVec2 topTextSize = font->CalcTextSizeA(topSize, textWrap, 0.0f, lineTop.c_str()); + const ImVec2 midTextSize = font->CalcTextSizeA(midSize, textWrap, 0.0f, lineMid.c_str()); + const ImVec2 bottomTextSize = font->CalcTextSizeA(bottomSize, textWrap, 0.0f, lineBottom.c_str()); + const float panelWidth = std::max(topTextSize.x, std::max(midTextSize.x, bottomTextSize.x)) + padX * 2.0f; + const float panelHeight = topTextSize.y + midTextSize.y + bottomTextSize.y + gap * 2.0f + padY * 2.0f; + const ImVec2 panelMin((displaySize.x - panelWidth) * 0.5f, marginTop); + const ImVec2 panelMax(panelMin.x + panelWidth, panelMin.y + panelHeight); + + auto* drawList = ImGui::GetForegroundDrawList(); + if (!drawList) + return; + + drawList->AddRectFilled(panelMin, panelMax, IM_COL32(6, 8, 12, 232), 14.0f); + drawList->AddRect(panelMin, panelMax, IM_COL32(255, 166, 64, 255), 14.0f, 0, 2.5f); + + const float topX = panelMin.x + (panelWidth - topTextSize.x) * 0.5f; + const float midX = panelMin.x + (panelWidth - midTextSize.x) * 0.5f; + const float bottomX = panelMin.x + (panelWidth - bottomTextSize.x) * 0.5f; + const float topY = panelMin.y + padY; + const float midY = topY + topTextSize.y + gap; + const float bottomY = midY + midTextSize.y + gap; + + drawList->AddText(font, topSize, ImVec2(topX, topY), IM_COL32(255, 206, 120, 255), lineTop.c_str()); + drawList->AddText(font, midSize, ImVec2(midX, midY), IM_COL32(255, 244, 224, 255), lineMid.c_str()); + drawList->AddText(font, bottomSize, ImVec2(bottomX, bottomY), IM_COL32(202, 222, 255, 255), lineBottom.c_str()); + } + + inline void applyDollyZoomProjection(ICamera* camera, IPlanarProjection::CProjection& projection) + { + auto* dolly = dynamic_cast(camera); + if (!dolly) + return; + const auto& params = projection.getParameters(); + if (params.m_type != IPlanarProjection::CProjection::Perspective) + return; + projection.setPerspective(params.m_zNear, params.m_zFar, dolly->computeDollyFov()); + } + + inline CameraPreset capturePreset(ICamera* camera, const std::string& name) + { + CameraPreset preset; + preset.name = name; + if (!camera) + return preset; + + preset.identifier = std::string(camera->getIdentifier()); + const auto& gimbal = camera->getGimbal(); + preset.position = gimbal.getPosition(); + preset.orientation = gimbal.getOrientation(); + + auto captureOrbit = [&](auto* orbit) + { + preset.distance = orbit->getDistance(); + preset.hasDistance = true; + preset.orbitDistance = orbit->getDistance(); + preset.orbitU = orbit->getU(); + preset.orbitV = orbit->getV(); + preset.hasOrbitState = true; + }; + + withOrbitLikeCamera(camera, captureOrbit); + + return preset; + } + + inline bool applyPresetToCamera(ICamera* camera, const CameraPreset& preset) + { + if (!camera) + return false; + + CTargetPose target; + target.position = preset.position; + target.orientation = preset.orientation; + target.hasDistance = preset.hasDistance; + target.distance = preset.distance; + target.hasOrbitState = preset.hasOrbitState; + target.orbitU = preset.orbitU; + target.orbitV = preset.orbitV; + target.orbitDistance = preset.orbitDistance; + + return m_targetPoseController.apply(camera, target); + } + + inline void appendVirtualEventLog(std::string_view source, std::string_view controller, uint32_t planarIx, ICamera* camera, const CVirtualGimbalEvent* events, uint32_t count) + { + m_uiVirtualEventsThisFrame += count; + const std::string sourceStr(source); + const std::string controllerStr(controller); + 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 ctrl=%s cam=%s planar=%u event=%s mag=%.6f", + static_cast(m_realFrameIx), + sourceStr.c_str(), + controllerStr.c_str(), + cameraName.c_str(), + planarIx, + eventName, + events[i].magnitude); + m_virtualEventLog.push_back({ + m_realFrameIx, + events[i].type, + events[i].magnitude, + sourceStr, + controllerStr, + cameraName, + planarIx, + std::move(line) + }); + } + + while (m_virtualEventLog.size() > m_virtualEventLogMax) + m_virtualEventLog.pop_front(); + } + + inline void applyConstraintsToCamera(ICamera* camera) + { + if (!m_cameraConstraints.enabled || !camera) + return; + + auto clampOrbitDistance = [&](auto* orbit) + { + if (m_cameraConstraints.clampDistance) + { + const float clamped = std::clamp(orbit->getDistance(), m_cameraConstraints.minDistance, m_cameraConstraints.maxDistance); + orbit->setDistance(clamped); + } + }; + + if (withOrbitLikeCamera(camera, clampOrbitDistance)) + return; + + if (!(m_cameraConstraints.clampPitch || m_cameraConstraints.clampYaw || m_cameraConstraints.clampRoll)) + return; + + const auto& gimbal = camera->getGimbal(); + const auto pos = gimbal.getPosition(); + const auto eulerDeg = glm::degrees(glm::eulerAngles(gimbal.getOrientation())); + + auto clamped = eulerDeg; + if (m_cameraConstraints.clampPitch) + clamped.x = std::clamp(clamped.x, m_cameraConstraints.pitchMinDeg, m_cameraConstraints.pitchMaxDeg); + if (m_cameraConstraints.clampYaw) + clamped.y = std::clamp(clamped.y, m_cameraConstraints.yawMinDeg, m_cameraConstraints.yawMaxDeg); + if (m_cameraConstraints.clampRoll) + clamped.z = std::clamp(clamped.z, m_cameraConstraints.rollMinDeg, m_cameraConstraints.rollMaxDeg); + + if (clamped.x == eulerDeg.x && clamped.y == eulerDeg.y && clamped.z == eulerDeg.z) + return; + + CameraPreset preset; + preset.position = pos; + preset.orientation = glm::quat(hlsl::radians(clamped)); + applyPresetToCamera(camera, preset); + } + + inline void applyVirtualEventScaling(std::vector& events, uint32_t count) + { + for (uint32_t i = 0u; i < count; ++i) + { + auto& ev = events[i]; + const auto type = ev.type; + + if (type == CVirtualGimbalEvent::MoveForward || type == CVirtualGimbalEvent::MoveBackward || + type == CVirtualGimbalEvent::MoveLeft || type == CVirtualGimbalEvent::MoveRight || + type == CVirtualGimbalEvent::MoveUp || type == CVirtualGimbalEvent::MoveDown) + { + ev.magnitude *= m_cameraControls.translationScale; + } + else if (type == CVirtualGimbalEvent::TiltUp || type == CVirtualGimbalEvent::TiltDown || + type == CVirtualGimbalEvent::PanLeft || type == CVirtualGimbalEvent::PanRight || + type == CVirtualGimbalEvent::RollLeft || type == CVirtualGimbalEvent::RollRight) + { + ev.magnitude *= m_cameraControls.rotationScale; + } + } + } + + inline void remapTranslationToWorld(ICamera* camera, std::vector& events, uint32_t& count) + { + if (!camera) + return; + + float64_t3 worldDelta = float64_t3(0.0); + std::vector filtered; + filtered.reserve(events.size()); + + for (uint32_t i = 0u; i < count; ++i) + { + const auto& ev = events[i]; + switch (ev.type) + { + case CVirtualGimbalEvent::MoveRight: worldDelta.x += ev.magnitude; break; + case CVirtualGimbalEvent::MoveLeft: worldDelta.x -= ev.magnitude; break; + case CVirtualGimbalEvent::MoveUp: worldDelta.y += ev.magnitude; break; + case CVirtualGimbalEvent::MoveDown: worldDelta.y -= ev.magnitude; break; + case CVirtualGimbalEvent::MoveForward: worldDelta.z += ev.magnitude; break; + case CVirtualGimbalEvent::MoveBackward: worldDelta.z -= ev.magnitude; break; + default: + filtered.emplace_back(ev); + break; + } + } + + if (worldDelta.x == 0.0 && worldDelta.y == 0.0 && worldDelta.z == 0.0) + { + events = std::move(filtered); + count = static_cast(events.size()); + return; + } + + const auto& gimbal = camera->getGimbal(); + const auto right = gimbal.getXAxis(); + const auto up = gimbal.getYAxis(); + const auto forward = gimbal.getZAxis(); + + const float64_t3 localDelta = float64_t3( + hlsl::dot(worldDelta, right), + hlsl::dot(worldDelta, up), + hlsl::dot(worldDelta, forward) + ); + + auto emitAxis = [&](double v, CVirtualGimbalEvent::VirtualEventType pos, CVirtualGimbalEvent::VirtualEventType neg) + { + if (v == 0.0) + return; + auto& ev = filtered.emplace_back(); + ev.type = (v > 0.0) ? pos : neg; + ev.magnitude = std::abs(v); + }; + + emitAxis(localDelta.x, CVirtualGimbalEvent::MoveRight, CVirtualGimbalEvent::MoveLeft); + emitAxis(localDelta.y, CVirtualGimbalEvent::MoveUp, CVirtualGimbalEvent::MoveDown); + emitAxis(localDelta.z, CVirtualGimbalEvent::MoveForward, CVirtualGimbalEvent::MoveBackward); + + events = std::move(filtered); + count = static_cast(events.size()); + } + + inline void applyPresetToTargets(const CameraPreset& preset) + { + if (!m_playbackAffectsAll) + { + applyPresetToCamera(getActiveCamera(), preset); + return; + } + + std::unordered_set visited; + for (auto& binding : windowBindings) + { + auto& planar = m_planarProjections[binding.activePlanarIx]; + if (!planar) + continue; + auto* camera = planar->getCamera(); + if (!camera) + continue; + const auto id = camera->getGimbal().getID(); + if (visited.insert(id).second) + applyPresetToCamera(camera, preset); + } + } + + inline void updatePlayback(double dtSec) + { + if (!m_playback.playing || m_keyframes.empty()) + return; + + m_playback.time += static_cast(dtSec * m_playback.speed); + + const float duration = m_keyframes.back().time; + if (duration <= 0.f) + { + applyPresetToTargets(m_keyframes.back().preset); + return; + } + + if (m_playback.loop) + { + while (m_playback.time > duration) + m_playback.time -= duration; + } + else if (m_playback.time > duration) + { + m_playback.time = duration; + m_playback.playing = false; + } + + const auto time = m_playback.time; + if (m_keyframes.size() == 1) + { + applyPresetToTargets(m_keyframes.front().preset); + return; + } + + size_t idx = 0u; + while (idx + 1u < m_keyframes.size() && m_keyframes[idx + 1u].time < time) + ++idx; + + const auto& a = m_keyframes[idx]; + const auto& b = m_keyframes[std::min(idx + 1u, m_keyframes.size() - 1u)]; + + if (b.time <= a.time) + { + applyPresetToTargets(a.preset); + return; + } + + const double alpha = static_cast(time - a.time) / static_cast(b.time - a.time); + + CameraPreset blended; + blended.position = a.preset.position + (b.preset.position - a.preset.position) * alpha; + blended.orientation = glm::slerp(a.preset.orientation, b.preset.orientation, static_cast(alpha)); + blended.hasDistance = a.preset.hasDistance || b.preset.hasDistance; + if (blended.hasDistance) + { + const float da = a.preset.hasDistance ? a.preset.distance : b.preset.distance; + const float db = b.preset.hasDistance ? b.preset.distance : a.preset.distance; + blended.distance = da + (db - da) * static_cast(alpha); + } + blended.hasOrbitState = a.preset.hasOrbitState || b.preset.hasOrbitState; + if (blended.hasOrbitState) + { + const double ua = a.preset.hasOrbitState ? a.preset.orbitU : b.preset.orbitU; + const double ub = b.preset.hasOrbitState ? b.preset.orbitU : a.preset.orbitU; + const double va = a.preset.hasOrbitState ? a.preset.orbitV : b.preset.orbitV; + const double vb = b.preset.hasOrbitState ? b.preset.orbitV : a.preset.orbitV; + const float da = a.preset.hasOrbitState ? a.preset.orbitDistance : b.preset.orbitDistance; + const float db = b.preset.hasOrbitState ? b.preset.orbitDistance : a.preset.orbitDistance; + + blended.orbitU = ua + (ub - ua) * alpha; + blended.orbitV = va + (vb - va) * alpha; + blended.orbitDistance = da + (db - da) * static_cast(alpha); + } + + applyPresetToTargets(blended); + } + + inline bool savePresetsToFile(const system::path& path) + { + json root; + root["presets"] = json::array(); + + for (const auto& preset : m_presets) + { + json j; + j["name"] = preset.name; + j["identifier"] = preset.identifier; + j["position"] = { preset.position.x, preset.position.y, preset.position.z }; + j["orientation"] = { preset.orientation.x, preset.orientation.y, preset.orientation.z, preset.orientation.w }; + if (preset.hasDistance) + j["distance"] = preset.distance; + if (preset.hasOrbitState) + { + j["orbit_u"] = preset.orbitU; + j["orbit_v"] = preset.orbitV; + j["orbit_distance"] = preset.orbitDistance; + } + root["presets"].push_back(std::move(j)); + } + + std::ofstream out(path.string(), std::ios::binary); + if (!out) + return false; + out << root.dump(2); + return true; + } + + inline bool loadPresetsFromFile(const system::path& path) + { + std::ifstream in(path.string(), std::ios::binary); + if (!in) + return false; + + json root; + in >> root; + if (!root.contains("presets")) + return false; + + m_presets.clear(); + for (const auto& entry : root["presets"]) + { + CameraPreset preset; + if (entry.contains("name")) + preset.name = entry["name"].get(); + if (entry.contains("identifier")) + preset.identifier = entry["identifier"].get(); + if (entry.contains("position") && entry["position"].is_array()) + { + auto arr = entry["position"]; + preset.position = float64_t3(arr[0].get(), arr[1].get(), arr[2].get()); + } + if (entry.contains("orientation") && entry["orientation"].is_array()) + { + auto arr = entry["orientation"]; + preset.orientation = glm::quat( + arr[3].get(), + arr[0].get(), + arr[1].get(), + arr[2].get() + ); + } + if (entry.contains("distance")) + { + preset.distance = entry["distance"].get(); + preset.hasDistance = true; + } + if (entry.contains("orbit_u")) + { + preset.orbitU = entry["orbit_u"].get(); + preset.hasOrbitState = true; + } + if (entry.contains("orbit_v")) + { + preset.orbitV = entry["orbit_v"].get(); + preset.hasOrbitState = true; + } + if (entry.contains("orbit_distance")) + { + preset.orbitDistance = entry["orbit_distance"].get(); + preset.hasOrbitState = true; + } + m_presets.emplace_back(std::move(preset)); + } + + return true; + } + + inline void imguiListen() + { + #include "app/impl/AppImGuiListen.inl" + } + + inline bool shouldCaptureOSCursor() + { + if (!enableActiveCameraMovement || !captureCursorInMoveMode) + return false; + if (m_ciMode || m_scriptedInput.enabled) + return false; + if (!m_window || !m_window->hasInputFocus() || !m_window->hasMouseFocus()) + return false; + return true; + } + + inline void UpdateBoundCameraMovement() + { + ImGuiIO& io = ImGui::GetIO(); + + if (ImGui::IsKeyPressed(ImGuiKey_Space)) + enableActiveCameraMovement = !enableActiveCameraMovement; + + if (enableActiveCameraMovement) + { + io.ConfigFlags |= ImGuiConfigFlags_NoMouse; + io.MouseDrawCursor = false; + io.WantCaptureMouse = false; + + if (shouldCaptureOSCursor()) + { + ImVec2 viewportSize = io.DisplaySize; + auto* cc = m_window->getCursorControl(); + if (cc) + { + int32_t posX = m_window->getX(); + int32_t posY = m_window->getY(); + + if (resetCursorToCenter) + { + const ICursorControl::SPosition middle{ static_cast(viewportSize.x / 2 + posX), static_cast(viewportSize.y / 2 + posY) }; + cc->setPosition(middle); + } + else + { + auto currentCursorPos = cc->getPosition(); + ICursorControl::SPosition newPos{}; + newPos.x = std::clamp(currentCursorPos.x, posX, viewportSize.x + posX); + newPos.y = std::clamp(currentCursorPos.y, posY, viewportSize.y + posY); + cc->setPosition(newPos); + } + } + } + } + else + { + io.ConfigFlags &= ~ImGuiConfigFlags_NoMouse; + io.MouseDrawCursor = false; + io.WantCaptureMouse = true; + } + } + + inline void UpdateCursorVisibility() + { + auto* cc = m_window ? m_window->getCursorControl() : nullptr; + if (!cc) + return; + cc->setVisible(!shouldCaptureOSCursor()); + } + + inline void UpdateUiMetrics() + { + m_uiLastFrameMs = static_cast(m_frameDeltaSec * 1000.0); + m_uiLastInputEvents = m_uiInputEventsThisFrame; + m_uiLastVirtualEvents = m_uiVirtualEventsThisFrame; + + m_uiFrameMs[m_uiMetricIndex] = m_uiLastFrameMs; + m_uiInputCounts[m_uiMetricIndex] = static_cast(m_uiInputEventsThisFrame); + m_uiVirtualCounts[m_uiMetricIndex] = static_cast(m_uiVirtualEventsThisFrame); + + m_uiMetricIndex = (m_uiMetricIndex + 1u) % UiMetricSamples; + m_uiInputEventsThisFrame = 0u; + m_uiVirtualEventsThisFrame = 0u; + } + + inline void DrawBadge(const char* label, const ImVec4& bg, const ImVec4& fg) + { + 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(6.0f, 2.0f)); + ImGui::Button(label); + ImGui::PopStyleVar(); + ImGui::PopStyleColor(4); + } + + inline void DrawKeyHint(const char* label, const ImVec4& bg, const ImVec4& fg) + { + 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(4.0f, 1.0f)); + ImGui::SmallButton(label); + ImGui::PopStyleVar(); + ImGui::PopStyleColor(4); + } + + inline void DrawHoverHint(const char* text) + { + if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayShort)) + { + ImGui::BeginTooltip(); + ImGui::TextUnformatted(text); + ImGui::EndTooltip(); + } + } + + inline void DrawDot(const ImVec4& color) + { + ImVec2 p = ImGui::GetCursorScreenPos(); + const float radius = 3.5f; + ImGui::GetWindowDrawList()->AddCircleFilled(ImVec2(p.x + radius, p.y + radius + 1.0f), radius, ImGui::ColorConvertFloat4ToU32(color)); + ImGui::Dummy(ImVec2(radius * 2.0f + 2.0f, radius * 2.0f)); + ImGui::SameLine(0, 6.0f); + } + + inline void DrawSectionHeader(const char* id, const char* label, const ImVec4& accent) + { + ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 4.0f); + ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.14f, 0.18f, 0.22f, 0.52f)); + if (ImGui::BeginChild(id, ImVec2(0, 20), true, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse)) + { + ImVec2 p = ImGui::GetWindowPos(); + ImVec2 s = ImGui::GetWindowSize(); + ImGui::GetWindowDrawList()->AddRectFilled(p, ImVec2(p.x + 2.0f, p.y + s.y), ImGui::ColorConvertFloat4ToU32(accent), 4.0f); + ImGui::SetCursorPosX(8.0f); + ImGui::AlignTextToFramePadding(); + ImGui::TextColored(accent, "%s", label); + } + ImGui::EndChild(); + ImGui::PopStyleColor(); + ImGui::PopStyleVar(); + ImGui::Spacing(); + } + + inline float CalcCardHeight(int rows) const + { + return ImGui::GetFrameHeightWithSpacing() * (static_cast(rows) + 1.0f) + 10.0f; + } + + inline bool BeginCard(const char* id, float height, const ImVec4& top, const ImVec4& bottom, const ImVec4& border) + { + ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 6.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(10.0f, 8.0f)); + ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0, 0, 0, 0)); + const bool open = ImGui::BeginChild(id, ImVec2(0, height), true, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse); + ImVec2 p = ImGui::GetWindowPos(); + ImVec2 s = ImGui::GetWindowSize(); + const ImU32 colTop = ImGui::ColorConvertFloat4ToU32(top); + const ImU32 colBottom = ImGui::ColorConvertFloat4ToU32(bottom); + ImGui::GetWindowDrawList()->AddRectFilledMultiColor( + p, + ImVec2(p.x + s.x, p.y + s.y), + colTop, + colTop, + colBottom, + colBottom + ); + ImGui::GetWindowDrawList()->AddRect(p, ImVec2(p.x + s.x, p.y + s.y), ImGui::ColorConvertFloat4ToU32(border), 6.0f); + return open; + } + + inline void EndCard() + { + ImGui::EndChild(); + ImGui::PopStyleColor(); + ImGui::PopStyleVar(2); + } + + + inline void DrawControlPanel() + { + #include "app/impl/AppControlPanel.inl" + } + + inline void TransformEditorContents() + { + #include "app/impl/AppTransformEditor.inl" + } + + inline void addMatrixTable(const char* topText, const char* tableName, int rows, int columns, const float* pointer, bool withSeparator = true) + { + 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(); + } + + std::chrono::seconds timeout = std::chrono::seconds(0x7fffFFFFu); + clock_t::time_point start; + + //! One window & 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 = 3u; + // 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 = {}; + // We will use it to get some asset stuff like geometry creator + smart_refctd_ptr m_assetManager; + // Input system for capturing system events + core::smart_refctd_ptr m_inputSystem; + // Handles mouse events + InputSystem::ChannelReader mouse; + // Handles keyboard events + InputSystem::ChannelReader keyboard; + //! 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; + }; + + // one model object in the world, testing multiuple cameraz for which view is rendered to separate frame buffers (so what they see) with new controller API including imguizmo + nbl::hlsl::float32_t3x4 m_model = nbl::hlsl::float32_t3x4(1.f); + + // if we had working IObjectTransform or something similar then it would be it instead, it is "last manipulated object" I need for TRS editor + // in reality we should store range of those IObjectTransforem interface range & index to object representing last manipulated one + nbl::core::smart_refctd_ptr boundCameraToManipulate = nullptr; + std::optional boundPlanarCameraIxToManipulate = std::nullopt; + + std::vector> m_planarProjections; + + bool enableActiveCameraMovement = false; + bool captureCursorInMoveMode = false; + + bool resetCursorToCenter = true; + + struct windowControlBinding + { + 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 viewProjMatrix = float32_t4x4(1.f); + + uint32_t activePlanarIx = 0u; + bool allowGizmoAxesToFlip = false; + bool enableDebugGridDraw = true; + float aspectRatio = 16.f / 9.f; + bool leftHandedProjection = true; + + std::optional boundProjectionIx = std::nullopt, lastBoundPerspectivePresetProjectionIx = std::nullopt, lastBoundOrthoPresetProjectionIx = std::nullopt; + + 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(); + } + }; + + struct ScriptedInputEvent + { + enum class Type : uint8_t + { + Keyboard, + Mouse, + Imguizmo, + Action + }; + + struct KeyboardData + { + ui::E_KEY_CODE key = ui::EKC_NONE; + ui::SKeyboardEvent::E_KEY_ACTION action = ui::SKeyboardEvent::ECA_UNITIALIZED; + }; + + struct MouseData + { + ui::SMouseEvent::E_EVENT_TYPE type = ui::SMouseEvent::EET_UNITIALIZED; + ui::E_MOUSE_BUTTON button = ui::EMB_LEFT_BUTTON; + ui::SMouseEvent::SClickEvent::E_ACTION action = ui::SMouseEvent::SClickEvent::EA_UNITIALIZED; + int16_t x = 0; + int16_t y = 0; + int16_t dx = 0; + int16_t dy = 0; + int16_t v = 0; + int16_t h = 0; + }; + + struct ActionData + { + enum class Kind : uint8_t + { + SetActiveRenderWindow, + SetActivePlanar, + SetProjectionType, + SetProjectionIndex, + SetUseWindow, + SetLeftHanded + }; + + Kind kind = Kind::SetActiveRenderWindow; + int32_t value = 0; + }; + + uint64_t frame = 0; + Type type = Type::Keyboard; + KeyboardData keyboard; + MouseData mouse; + float32_t4x4 imguizmo = float32_t4x4(1.f); + ActionData action; + }; + + struct ScriptedInputCheck + { + enum class Kind : uint8_t + { + Baseline, + ImguizmoVirtual, + GimbalNear, + GimbalDelta, + GimbalStep + }; + + struct ExpectedVirtualEvent + { + CVirtualGimbalEvent::VirtualEventType type = CVirtualGimbalEvent::None; + float64_t magnitude = 0.0; + }; + + uint64_t frame = 0; + Kind kind = Kind::Baseline; + float tolerance = 1e-3f; + std::vector expectedVirtualEvents; + + float32_t3 expectedPos = float32_t3(0.f); + float32_t3 expectedEulerDeg = float32_t3(0.f); + bool hasExpectedPos = false; + bool hasExpectedEuler = false; + float posTolerance = 0.05f; + float eulerToleranceDeg = 1.0f; + float minPosDelta = 0.0f; + float minEulerDeltaDeg = 0.0f; + bool hasPosDeltaConstraint = false; + bool hasEulerDeltaConstraint = false; + }; + + struct ScriptedInputState + { + bool enabled = false; + bool log = false; + bool exclusive = false; + bool hardFail = false; + bool visualDebug = false; + float visualTargetFps = 0.f; + float visualCameraHoldSeconds = 0.f; + std::vector events; + size_t nextEventIndex = 0; + std::vector checks; + size_t nextCheckIndex = 0; + std::vector captureFrames; + size_t nextCaptureIndex = 0; + std::string capturePrefix = "script"; + system::path captureOutputDir; + bool failed = false; + bool summaryReported = false; + bool baselineValid = false; + float32_t3 baselinePos = float32_t3(0.f); + float32_t3 baselineEulerDeg = float32_t3(0.f); + bool stepValid = false; + float32_t3 stepPos = float32_t3(0.f); + float32_t3 stepEulerDeg = float32_t3(0.f); + bool visualActivePlanarValid = false; + uint32_t visualActivePlanarIx = 0u; + uint64_t visualActivePlanarStartFrame = 0u; + bool framePacerInitialized = false; + std::chrono::steady_clock::time_point framePacerNext = {}; + }; + + static constexpr inline auto MaxSceneFBOs = 2u; + std::array windowBindings; + uint32_t activeRenderWindowIx = 0u; + + // UI font atlas + viewport FBO color attachment textures + constexpr static inline auto TotalUISampleTexturesAmount = 1u + MaxSceneFBOs; + + nbl::core::smart_refctd_ptr m_scene; + nbl::core::smart_refctd_ptr m_sceneRenderpass; + nbl::core::smart_refctd_ptr m_renderer; + + CRenderUI m_ui; + video::CDumbPresentationOracle oracle; + uint16_t gcIndex = {}; + + static constexpr uint32_t CiFramesBeforeCapture = 10u; + static constexpr auto CiMaxRuntime = std::chrono::minutes(2); + bool m_ciMode = false; + bool m_ciScreenshotDone = false; + uint32_t m_ciFrameCounter = 0u; + system::path m_ciScreenshotPath; + clock_t::time_point m_ciStartedAt = clock_t::time_point::min(); + bool m_scriptVisualDebugCli = false; + ScriptedInputState m_scriptedInput; + CameraControlSettings m_cameraControls; + CameraConstraintSettings m_cameraConstraints; + core::smart_refctd_ptr m_logFormatter; + std::deque m_virtualEventLog; + size_t m_virtualEventLogMax = 128u; + bool m_showHud = true; + bool m_showEventLog = false; + bool m_logAutoScroll = true; + bool m_logWrap = true; + std::vector m_presets; + std::vector m_keyframes; + CameraPlaybackState m_playback; + CTargetPoseController m_targetPoseController; + bool m_playbackAffectsAll = false; + float m_newKeyframeTime = 0.f; + char m_presetName[64] = "Preset"; + char m_presetPath[260] = "camera_presets.json"; + std::chrono::microseconds m_lastPresentationTimestamp = {}; + bool m_haveLastPresentationTimestamp = false; + double m_frameDeltaSec = 0.0; + static constexpr size_t UiMetricSamples = 96u; + std::array m_uiFrameMs = {}; + std::array m_uiInputCounts = {}; + std::array m_uiVirtualCounts = {}; + uint32_t m_uiMetricIndex = 0u; + uint32_t m_uiVirtualEventsThisFrame = 0u; + uint32_t m_uiInputEventsThisFrame = 0u; + uint32_t m_uiLastInputEvents = 0u; + uint32_t m_uiLastVirtualEvents = 0u; + float m_uiLastFrameMs = 0.0f; + + const bool flipGizmoY = true; + + float camYAngle = 165.f / 180.f * 3.14159f; + float camXAngle = 32.f / 180.f * 3.14159f; + float camDistance = 8.f; + bool useWindow = true, useSnap = false; + ImGuizmo::OPERATION mCurrentGizmoOperation = ImGuizmo::TRANSLATE; + ImGuizmo::MODE mCurrentGizmoMode = ImGuizmo::LOCAL; + float snap[3] = { 1.f, 1.f, 1.f }; + + bool firstFrame = true; + const float32_t2 iPaddingOffset = float32_t2(10, 10); + + struct ImWindowInit + { + float32_t2 iPos, iSize; + }; + + struct + { + ImWindowInit trsEditor; + ImWindowInit planars; + std::array renderWindows; + } wInit; +}; + + +#endif // _NBL_THIS_EXAMPLE_APP_HPP_ + diff --git a/61_UI/include/app/AppTypes.hpp b/61_UI/include/app/AppTypes.hpp new file mode 100644 index 000000000..436898cf6 --- /dev/null +++ b/61_UI/include/app/AppTypes.hpp @@ -0,0 +1,28 @@ +#ifndef _NBL_THIS_EXAMPLE_APP_TYPES_HPP_ +#define _NBL_THIS_EXAMPLE_APP_TYPES_HPP_ + +#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; +}; + +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/impl/AppControlPanel.inl b/61_UI/include/app/impl/AppControlPanel.inl new file mode 100644 index 000000000..a9a311725 --- /dev/null +++ b/61_UI/include/app/impl/AppControlPanel.inl @@ -0,0 +1,853 @@ + const ImVec2 displaySize = ImGui::GetIO().DisplaySize; + const float panelWidth = std::clamp(displaySize.x * 0.19f, 200.0f, displaySize.x * 0.25f); + const float panelHeight = std::clamp(displaySize.y * 0.34f, 200.0f, displaySize.y * 0.50f); + const ImVec2 panelSize = { panelWidth, panelHeight }; + const ImVec2 panelPos = { 0.0f, 0.0f }; + ImGui::SetNextWindowPos(panelPos, ImGuiCond_Always); + ImGui::SetNextWindowSize(panelSize, ImGuiCond_Always); + + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(5.0f, 4.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(4.0f, 1.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(3.0f, 2.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f); + ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f); + ImGui::PushStyleVar(ImGuiStyleVar_TabRounding, 3.0f); + ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarRounding, 4.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f); + ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2(3.0f, 2.0f)); + + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.05f, 0.06f, 0.08f, 0.0f)); + ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.10f, 0.12f, 0.16f, 0.44f)); + ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.64f, 0.72f, 0.84f, 0.55f)); + ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.16f, 0.19f, 0.24f, 0.54f)); + ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, ImVec4(0.26f, 0.32f, 0.40f, 0.64f)); + ImGui::PushStyleColor(ImGuiCol_FrameBgActive, ImVec4(0.30f, 0.36f, 0.45f, 0.70f)); + ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.14f, 0.18f, 0.24f, 0.60f)); + ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(0.24f, 0.30f, 0.40f, 0.70f)); + ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(0.28f, 0.36f, 0.46f, 0.78f)); + ImGui::PushStyleColor(ImGuiCol_Tab, ImVec4(0.14f, 0.18f, 0.24f, 0.60f)); + ImGui::PushStyleColor(ImGuiCol_TabHovered, ImVec4(0.24f, 0.30f, 0.40f, 0.70f)); + ImGui::PushStyleColor(ImGuiCol_TabActive, ImVec4(0.20f, 0.26f, 0.36f, 0.78f)); + ImGui::PushStyleColor(ImGuiCol_TableRowBg, ImVec4(0.12f, 0.14f, 0.18f, 0.50f)); + ImGui::PushStyleColor(ImGuiCol_TableRowBgAlt, ImVec4(0.16f, 0.18f, 0.22f, 0.50f)); + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.98f, 0.99f, 1.0f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_TextDisabled, ImVec4(0.82f, 0.86f, 0.90f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_Separator, ImVec4(0.54f, 0.60f, 0.70f, 0.80f)); + ImGui::PushStyleColor(ImGuiCol_SeparatorHovered, ImVec4(0.68f, 0.76f, 0.88f, 0.90f)); + ImGui::PushStyleColor(ImGuiCol_SeparatorActive, ImVec4(0.82f, 0.90f, 1.0f, 0.96f)); + + ImGui::SetNextWindowCollapsed(false, ImGuiCond_Always); + ImGui::SetNextWindowBgAlpha(0.0f); + if (m_ciMode) + ImGui::SetNextWindowFocus(); + ImGui::Begin("Control Panel", nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar); + + const ImVec4 accent = ImVec4(0.60f, 0.82f, 1.0f, 1.0f); + const ImVec4 good = ImVec4(0.45f, 0.90f, 0.60f, 1.0f); + const ImVec4 bad = ImVec4(1.0f, 0.50f, 0.45f, 1.0f); + const ImVec4 warn = ImVec4(0.95f, 0.80f, 0.45f, 1.0f); + const ImVec4 muted = ImVec4(0.92f, 0.93f, 0.95f, 1.0f); + const ImVec4 badgeText = ImVec4(0.10f, 0.11f, 0.13f, 1.0f); + const ImVec4 keyBg = ImVec4(0.20f, 0.22f, 0.25f, 1.0f); + const ImVec4 keyFg = ImVec4(0.92f, 0.94f, 0.96f, 1.0f); + const ImGuiTableFlags tableFlags = ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_RowBg | ImGuiTableFlags_PadOuterX; + const ImVec4 panelBg = ImVec4(0.03f, 0.04f, 0.05f, 0.50f); + const ImVec4 panelEdge = ImVec4(0.62f, 0.70f, 0.84f, 0.60f); + const ImVec4 panelStripe = ImVec4(0.28f, 0.56f, 0.90f, 0.70f); + const ImVec4 panelShadow = ImVec4(0.0f, 0.0f, 0.0f, 0.12f); + + { + const ImVec2 panelPos = ImGui::GetWindowPos(); + const ImVec2 panelSize = ImGui::GetWindowSize(); + auto* drawList = ImGui::GetWindowDrawList(); + drawList->AddRectFilled(ImVec2(panelPos.x + 2.0f, panelPos.y + 3.0f), ImVec2(panelPos.x + panelSize.x + 4.0f, panelPos.y + panelSize.y + 5.0f), ImGui::ColorConvertFloat4ToU32(panelShadow), 8.0f); + drawList->AddRectFilled(panelPos, ImVec2(panelPos.x + panelSize.x, panelPos.y + panelSize.y), ImGui::ColorConvertFloat4ToU32(panelBg), 6.0f); + drawList->AddRect(panelPos, ImVec2(panelPos.x + panelSize.x, panelPos.y + panelSize.y), ImGui::ColorConvertFloat4ToU32(panelEdge), 6.0f); + drawList->AddRectFilled(panelPos, ImVec2(panelPos.x + 4.0f, panelPos.y + panelSize.y), ImGui::ColorConvertFloat4ToU32(panelStripe), 6.0f); + } + + auto row = [&](const char* label, auto&& drawValue) + { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::TextUnformatted(label); + ImGui::TableSetColumnIndex(1); + drawValue(); + }; + + auto metricMax = [&](const std::array& values, float minValue) -> float + { + float maxValue = minValue; + for (const float v : values) + maxValue = std::max(maxValue, v); + return maxValue; + }; + + auto miniStat = [&](const char* id, const char* label, const ImVec4& color, const std::array& series, float minValue, auto&& drawValue) + { + ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.14f, 0.16f, 0.19f, 0.75f)); + ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 6.0f); + if (ImGui::BeginChild(id, ImVec2(0, 56), true, ImGuiWindowFlags_NoScrollbar)) + { + ImGui::TextDisabled("%s", label); + ImGui::SetWindowFontScale(1.05f); + drawValue(); + ImGui::SetWindowFontScale(1.0f); + ImGui::PushStyleColor(ImGuiCol_PlotLines, color); + const float maxValue = metricMax(series, minValue); + ImGui::PlotLines("##plot", series.data(), static_cast(UiMetricSamples), static_cast(m_uiMetricIndex), nullptr, 0.0f, maxValue, ImVec2(0, 24)); + ImGui::PopStyleColor(); + } + ImGui::EndChild(); + ImGui::PopStyleVar(); + ImGui::PopStyleColor(); + }; + + auto calcPillWidth = [&](const char* label, const ImVec2& pad) + { + return ImGui::CalcTextSize(label).x + pad.x * 2.0f; + }; + + auto drawTogglePill = [&](const char* label, bool& value, const ImVec4& onCol, const ImVec4& offCol, const ImVec2& pad) + { + ImGui::PushStyleColor(ImGuiCol_Button, value ? onCol : offCol); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, value ? onCol : offCol); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, value ? onCol : offCol); + ImGui::PushStyleColor(ImGuiCol_Text, badgeText); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, pad); + if (ImGui::Button(label)) + value = !value; + ImGui::PopStyleVar(); + ImGui::PopStyleColor(4); + }; + + ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 6.0f); + if (ImGui::BeginChild("PanelHeader", ImVec2(0, 64), true, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse)) + { + ImGui::Dummy(ImVec2(0.0f, 1.0f)); + ImGui::SetWindowFontScale(1.08f); + ImGui::TextColored(accent, "Control Panel"); + ImGui::SetWindowFontScale(1.0f); + { + const ImVec2 badgePad = ImVec2(6.0f, 2.0f); + const float gap = ImGui::GetStyle().ItemSpacing.x; + const char* badgeWindow = useWindow ? "WINDOW" : "FULL"; + const char* badgeMove = enableActiveCameraMovement ? "MOVE ON" : "MOVE OFF"; + const char* badgeScript = m_scriptedInput.enabled ? (m_scriptedInput.exclusive ? "SCRIPT EXCL" : "SCRIPT") : "SCRIPT OFF"; + const float badgeRowWidth = calcPillWidth(badgeWindow, badgePad) + + gap + calcPillWidth(badgeMove, badgePad) + + gap + calcPillWidth(badgeScript, badgePad) + + (m_ciMode ? (gap + calcPillWidth("CI", badgePad)) : 0.0f); + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + std::max(0.0f, (ImGui::GetContentRegionAvail().x - badgeRowWidth) * 0.5f)); + + DrawBadge(badgeWindow, accent, badgeText); + ImGui::SameLine(0.0f, gap); + DrawBadge(badgeMove, enableActiveCameraMovement ? good : bad, badgeText); + ImGui::SameLine(0.0f, gap); + DrawBadge(badgeScript, m_scriptedInput.enabled ? accent : ImVec4(0.35f, 0.36f, 0.38f, 1.0f), badgeText); + if (m_ciMode) + { + ImGui::SameLine(0.0f, gap); + DrawBadge("CI", warn, badgeText); + } + } + + ImGui::Dummy(ImVec2(0.0f, 2.0f)); + { + const ImVec2 keyPad = ImVec2(4.0f, 1.0f); + const float gap = ImGui::GetStyle().ItemSpacing.x; + const float groupGap = gap * 2.0f; + const float moveWidth = ImGui::CalcTextSize("Move").x + gap + + calcPillWidth("W", keyPad) + gap + + calcPillWidth("A", keyPad) + gap + + calcPillWidth("S", keyPad) + gap + + calcPillWidth("D", keyPad); + const float lookWidth = ImGui::CalcTextSize("Look").x + gap + calcPillWidth("RMB", keyPad); + const float zoomWidth = ImGui::CalcTextSize("Zoom").x + gap + calcPillWidth("MW", keyPad); + const float rowWidth = moveWidth + groupGap + lookWidth + groupGap + zoomWidth; + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + std::max(0.0f, (ImGui::GetContentRegionAvail().x - rowWidth) * 0.5f)); + + ImGui::TextDisabled("Move"); + ImGui::SameLine(); + DrawKeyHint("W", keyBg, keyFg); + ImGui::SameLine(); + DrawKeyHint("A", keyBg, keyFg); + ImGui::SameLine(); + DrawKeyHint("S", keyBg, keyFg); + ImGui::SameLine(); + DrawKeyHint("D", keyBg, keyFg); + + ImGui::SameLine(0.0f, groupGap); + ImGui::TextDisabled("Look"); + ImGui::SameLine(); + DrawKeyHint("RMB", keyBg, keyFg); + + ImGui::SameLine(0.0f, groupGap); + ImGui::TextDisabled("Zoom"); + ImGui::SameLine(); + DrawKeyHint("MW", keyBg, keyFg); + } + + ImGui::Dummy(ImVec2(0.0f, 2.0f)); + if (ImGui::BeginTable("HeaderMetrics", 3, ImGuiTableFlags_SizingStretchProp)) + { + const float frameMs = std::max(0.0f, m_uiLastFrameMs); + const float fps = frameMs > 0.0f ? (1000.0f / frameMs) : 0.0f; + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + miniStat("FrameStat", "Frame", accent, m_uiFrameMs, 16.0f, [&] + { + ImGui::TextColored(accent, "%.1f ms %.0f fps", frameMs, fps); + }); + + ImGui::TableSetColumnIndex(1); + miniStat("InputStat", "Input", accent, m_uiInputCounts, 4.0f, [&] + { + ImGui::TextColored(accent, "%u ev", m_uiLastInputEvents); + }); + + ImGui::TableSetColumnIndex(2); + miniStat("VirtualStat", "Virtual", accent, m_uiVirtualCounts, 4.0f, [&] + { + ImGui::TextColored(accent, "%u ev", m_uiLastVirtualEvents); + }); + ImGui::EndTable(); + } + } + ImGui::EndChild(); + ImGui::PopStyleVar(); + + ImGui::Spacing(); + + { + const ImVec2 togglePad = ImVec2(6.0f, 2.0f); + const float gap = ImGui::GetStyle().ItemSpacing.x; + const float rowWidth = calcPillWidth("WINDOW", togglePad) + + gap + calcPillWidth("STATUS", togglePad) + + gap + calcPillWidth("EVENT LOG", togglePad); + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + std::max(0.0f, (ImGui::GetContentRegionAvail().x - rowWidth) * 0.5f)); + drawTogglePill("WINDOW", useWindow, accent, ImVec4(0.35f, 0.36f, 0.38f, 1.0f), togglePad); + DrawHoverHint("Toggle split render windows"); + ImGui::SameLine(0.0f, gap); + drawTogglePill("STATUS", m_showHud, accent, ImVec4(0.35f, 0.36f, 0.38f, 1.0f), togglePad); + DrawHoverHint("Show system and camera status panel"); + ImGui::SameLine(0.0f, gap); + drawTogglePill("EVENT LOG", m_showEventLog, accent, ImVec4(0.35f, 0.36f, 0.38f, 1.0f), togglePad); + DrawHoverHint("Show virtual event log"); + } + + ImGui::Separator(); + + if (ImGui::BeginTabBar("ControlTabs")) + { + if (m_showHud && ImGui::BeginTabItem("Status")) + { + ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 4.0f); + if (ImGui::BeginChild("StatusPanel", ImVec2(0, 0), true)) + { + ImGui::PushItemWidth(-1.0f); + const ImVec4 cardTop = ImVec4(0.20f, 0.22f, 0.26f, 0.98f); + const ImVec4 cardBottom = ImVec4(0.12f, 0.13f, 0.15f, 0.98f); + const ImVec4 cardBorder = ImVec4(0.45f, 0.48f, 0.54f, 1.0f); + + DrawSectionHeader("SessionHeader", "Session", accent); + if (BeginCard("SessionCard", CalcCardHeight(3), cardTop, cardBottom, cardBorder)) + { + if (ImGui::BeginTable("SessionTable", 2, tableFlags)) + { + ImGui::TableSetupColumn("Label", ImGuiTableColumnFlags_WidthFixed, 120.0f); + ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch); + row("Mode", [&] { DrawDot(accent); ImGui::TextColored(accent, "%s", useWindow ? "Window" : "Fullscreen"); }); + row("Active window", [&] { DrawDot(accent); ImGui::TextColored(accent, "%u", activeRenderWindowIx); }); + row("Movement", [&] { const ImVec4 c = enableActiveCameraMovement ? good : bad; DrawDot(c); ImGui::TextColored(c, "%s", enableActiveCameraMovement ? "Enabled" : "Disabled"); }); + ImGui::EndTable(); + } + } + EndCard(); + + DrawSectionHeader("CameraHeader", "Camera", accent); + + auto* activeCamera = getActiveCamera(); + if (activeCamera) + { + const auto& gimbal = activeCamera->getGimbal(); + const auto pos = gimbal.getPosition(); + const auto euler = glm::degrees(glm::eulerAngles(gimbal.getOrientation())); + + if (BeginCard("CameraCard", CalcCardHeight(5), cardTop, cardBottom, cardBorder)) + { + if (ImGui::BeginTable("CameraTable", 2, tableFlags)) + { + ImGui::TableSetupColumn("Label", ImGuiTableColumnFlags_WidthFixed, 120.0f); + ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch); + row("Name", [&] { DrawDot(accent); ImGui::TextColored(muted, "%s", activeCamera->getIdentifier().data()); }); + row("Position", [&] { DrawDot(muted); ImGui::TextColored(muted, "%.2f %.2f %.2f", pos.x, pos.y, pos.z); }); + row("Euler", [&] { DrawDot(muted); ImGui::TextColored(muted, "%.1f %.1f %.1f", euler.x, euler.y, euler.z); }); + row("Move scale", [&] { DrawDot(muted); ImGui::TextColored(muted, "%.4f", activeCamera->getMoveSpeedScale()); }); + row("Rotate scale", [&] { DrawDot(muted); ImGui::TextColored(muted, "%.4f", activeCamera->getRotationSpeedScale()); }); + ImGui::EndTable(); + } + } + EndCard(); + } + else + { + if (BeginCard("CameraCard", CalcCardHeight(2), cardTop, cardBottom, cardBorder)) + ImGui::TextDisabled("No active camera"); + EndCard(); + } + + DrawSectionHeader("ProjectionHeader", "Projection", accent); + + auto& binding = windowBindings[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 (BeginCard("ProjectionCard", CalcCardHeight(4), cardTop, cardBottom, cardBorder)) + { + if (ImGui::BeginTable("ProjectionTable", 2, tableFlags)) + { + ImGui::TableSetupColumn("Label", ImGuiTableColumnFlags_WidthFixed, 120.0f); + ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch); + row("Type", [&] { DrawDot(accent); ImGui::TextColored(muted, "%s", params.m_type == IPlanarProjection::CProjection::Perspective ? "Perspective" : "Orthographic"); }); + row("zNear", [&] { DrawDot(muted); ImGui::TextColored(muted, "%.2f", params.m_zNear); }); + row("zFar", [&] { DrawDot(muted); ImGui::TextColored(muted, "%.2f", params.m_zFar); }); + if (params.m_type == IPlanarProjection::CProjection::Perspective) + row("Fov", [&] { DrawDot(muted); ImGui::TextColored(muted, "%.1f", params.m_planar.perspective.fov); }); + else + row("Ortho width", [&] { DrawDot(muted); ImGui::TextColored(muted, "%.1f", params.m_planar.orthographic.orthoWidth); }); + ImGui::EndTable(); + } + } + EndCard(); + } + else + { + if (BeginCard("ProjectionCard", CalcCardHeight(2), cardTop, cardBottom, cardBorder)) + ImGui::TextDisabled("No projection bound"); + EndCard(); + } + ImGui::PopItemWidth(); + } + ImGui::EndChild(); + ImGui::PopStyleVar(); + ImGui::EndTabItem(); + } + + if (ImGui::BeginTabItem("Projection")) + { + ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 4.0f); + if (ImGui::BeginChild("ProjectionPanel", ImVec2(0, 0), true)) + { + ImGui::PushItemWidth(-1.0f); + auto& active = windowBindings[activeRenderWindowIx]; + const auto activeRenderWindowIxString = std::to_string(activeRenderWindowIx); + + DrawSectionHeader("PlanarSelectHeader", "Planar Selection", accent); + ImGui::Text("Active Render Window: %s", activeRenderWindowIxString.c_str()); + DrawHoverHint("Window that receives input and camera switching"); + { + const size_t planarsCount = m_planarProjections.size(); + assert(planarsCount); + + std::vector sbels(planarsCount); + for (size_t i = 0; i < planarsCount; ++i) + sbels[i] = "Planar " + std::to_string(i); + + std::vector labels(planarsCount); + for (size_t i = 0; i < planarsCount; ++i) + labels[i] = sbels[i].c_str(); + + int currentPlanarIx = static_cast(active.activePlanarIx); + if (ImGui::Combo("Active Planar", ¤tPlanarIx, labels.data(), static_cast(labels.size()))) + { + active.activePlanarIx = static_cast(currentPlanarIx); + active.pickDefaultProjections(m_planarProjections[active.activePlanarIx]->getPlanarProjections()); + } + DrawHoverHint("Select which camera the window renders"); + } + + assert(active.boundProjectionIx.has_value()); + assert(active.lastBoundPerspectivePresetProjectionIx.has_value()); + assert(active.lastBoundOrthoPresetProjectionIx.has_value()); + + const auto activePlanarIxString = std::to_string(active.activePlanarIx); + auto& planarBound = m_planarProjections[active.activePlanarIx]; + assert(planarBound); + + DrawSectionHeader("ProjectionParamsHeader", "Projection Parameters", accent); + + auto selectedProjectionType = planarBound->getPlanarProjections()[active.boundProjectionIx.value()].getParameters().m_type; + { + const char* labels[] = { "Perspective", "Orthographic" }; + int type = static_cast(selectedProjectionType); + + if (ImGui::Combo("Projection Type", &type, labels, IM_ARRAYSIZE(labels))) + { + selectedProjectionType = static_cast(type); + + switch (selectedProjectionType) + { + case IPlanarProjection::CProjection::Perspective: active.boundProjectionIx = active.lastBoundPerspectivePresetProjectionIx.value(); break; + case IPlanarProjection::CProjection::Orthographic: active.boundProjectionIx = active.lastBoundOrthoPresetProjectionIx.value(); break; + default: active.boundProjectionIx = std::nullopt; assert(false); break; + } + } + DrawHoverHint("Switch projection type for this planar"); + } + + auto getPresetName = [&](auto ix) -> std::string + { + switch (selectedProjectionType) + { + case IPlanarProjection::CProjection::Perspective: return "Perspective Projection Preset " + std::to_string(ix); + case IPlanarProjection::CProjection::Orthographic: return "Orthographic Projection Preset " + std::to_string(ix); + default: return "Unknown Projection Preset " + std::to_string(ix); + } + }; + + bool updateBoundVirtualMaps = false; + if (ImGui::BeginCombo("Projection Preset", getPresetName(active.boundProjectionIx.value()).c_str())) + { + auto& projections = planarBound->getPlanarProjections(); + + for (uint32_t i = 0; i < projections.size(); ++i) + { + const auto& projection = projections[i]; + const auto& params = projection.getParameters(); + + if (params.m_type != selectedProjectionType) + continue; + + bool isSelected = (i == active.boundProjectionIx.value()); + + if (ImGui::Selectable(getPresetName(i).c_str(), isSelected)) + { + active.boundProjectionIx = i; + updateBoundVirtualMaps |= true; + + switch (selectedProjectionType) + { + case IPlanarProjection::CProjection::Perspective: active.lastBoundPerspectivePresetProjectionIx = active.boundProjectionIx.value(); break; + case IPlanarProjection::CProjection::Orthographic: active.lastBoundOrthoPresetProjectionIx = active.boundProjectionIx.value(); break; + default: assert(false); break; + } + } + + if (isSelected) + ImGui::SetItemDefaultFocus(); + } + ImGui::EndCombo(); + } + DrawHoverHint("Switch preset projection for this planar"); + + auto* const boundCamera = planarBound->getCamera(); + auto& boundProjection = planarBound->getPlanarProjections()[active.boundProjectionIx.value()]; + assert(not boundProjection.isProjectionSingular()); + + auto updateParameters = boundProjection.getParameters(); + + if (useWindow) + ImGui::Checkbox("Allow axes to flip##allowAxesToFlip", &active.allowGizmoAxesToFlip); + DrawHoverHint("Allow ImGuizmo axes to flip based on view"); + + if(useWindow) + ImGui::Checkbox("Draw debug grid##drawDebugGrid", &active.enableDebugGridDraw); + DrawHoverHint("Toggle debug grid in the render window"); + + if (ImGui::RadioButton("LH", active.leftHandedProjection)) + active.leftHandedProjection = true; + + ImGui::SameLine(); + + if (ImGui::RadioButton("RH", not active.leftHandedProjection)) + active.leftHandedProjection = false; + DrawHoverHint("Toggle left or right handed projection"); + + updateParameters.m_zNear = std::clamp(updateParameters.m_zNear, 0.1f, 100.f); + updateParameters.m_zFar = std::clamp(updateParameters.m_zFar, 110.f, 10000.f); + + ImGui::SliderFloat("zNear", &updateParameters.m_zNear, 0.1f, 100.f, "%.2f", ImGuiSliderFlags_Logarithmic); + DrawHoverHint("Near clip plane"); + ImGui::SliderFloat("zFar", &updateParameters.m_zFar, 110.f, 10000.f, "%.1f", ImGuiSliderFlags_Logarithmic); + DrawHoverHint("Far clip plane"); + + switch (selectedProjectionType) + { + case IPlanarProjection::CProjection::Perspective: + { + ImGui::SliderFloat("Fov", &updateParameters.m_planar.perspective.fov, 20.f, 150.f, "%.1f", ImGuiSliderFlags_Logarithmic); + DrawHoverHint("Perspective field of view"); + boundProjection.setPerspective(updateParameters.m_zNear, updateParameters.m_zFar, updateParameters.m_planar.perspective.fov); + } break; + + case IPlanarProjection::CProjection::Orthographic: + { + ImGui::SliderFloat("Ortho width", &updateParameters.m_planar.orthographic.orthoWidth, 1.f, 30.f, "%.1f", ImGuiSliderFlags_Logarithmic); + DrawHoverHint("Orthographic width"); + boundProjection.setOrthographic(updateParameters.m_zNear, updateParameters.m_zFar, updateParameters.m_planar.orthographic.orthoWidth); + } break; + + default: break; + } + + DrawSectionHeader("CursorHeader", "Cursor Behaviour", accent); + if (ImGui::TreeNodeEx("Cursor Behaviour")) + { + ImGui::Checkbox("Capture OS cursor in move mode", &captureCursorInMoveMode); + DrawHoverHint("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(); + } + + if (enableActiveCameraMovement) + ImGui::TextColored(good, "Bound Camera Movement: Enabled"); + else + ImGui::TextColored(bad, "Bound Camera Movement: Disabled"); + + ImGui::Separator(); + + DrawSectionHeader("BoundCameraHeader", "Bound Camera", accent); + const auto flags = ImGuiTreeNodeFlags_DefaultOpen; + if (ImGui::TreeNodeEx("Bound Camera", flags)) + { + ImGui::Text("Type: %s", boundCamera->getIdentifier().data()); + ImGui::Text("Object Ix: %s", std::to_string(active.activePlanarIx + 1u).c_str()); + ImGui::Separator(); + { + auto* orbit = dynamic_cast(boundCamera); + const bool isOrbitLike = orbit != nullptr; + + float moveSpeed = boundCamera->getMoveSpeedScale(); + float rotationSpeed = boundCamera->getRotationSpeedScale(); + + ImGui::SliderFloat("Move speed factor", &moveSpeed, 0.0001f, 10.f, "%.4f", ImGuiSliderFlags_Logarithmic); + DrawHoverHint("Scale translation speed for this camera"); + + if (boundCamera->getAllowedVirtualEvents() & CVirtualGimbalEvent::Rotate) + ImGui::SliderFloat("Rotate speed factor", &rotationSpeed, 0.0001f, 10.f, "%.4f", ImGuiSliderFlags_Logarithmic); + DrawHoverHint("Scale rotation speed for this camera"); + + boundCamera->setMoveSpeedScale(moveSpeed); + boundCamera->setRotationSpeedScale(rotationSpeed); + + if (isOrbitLike) + { + float distance = orbit->getDistance(); + ImGui::SliderFloat("Distance", &distance, orbit->MinDistance, orbit->MaxDistance, "%.4f", ImGuiSliderFlags_Logarithmic); + DrawHoverHint("Current orbit distance"); + orbit->setDistance(distance); + } + } + + if (ImGui::TreeNodeEx("World Data", flags)) + { + auto& gimbal = boundCamera->getGimbal(); + const auto position = getCastedVector(gimbal.getPosition()); + const auto& orientation = gimbal.getOrientation(); + const auto viewMatrix = getCastedMatrix(gimbal.getViewMatrix()); + + addMatrixTable("Position", ("PositionTable_" + activePlanarIxString).c_str(), 1, 3, &position[0], false); + addMatrixTable("Orientation (Quaternion)", ("OrientationTable_" + activePlanarIxString).c_str(), 1, 4, &orientation[0], false); + addMatrixTable("View Matrix", ("ViewMatrixTable_" + activePlanarIxString).c_str(), 3, 4, &viewMatrix[0][0], false); + ImGui::TreePop(); + } + + if (ImGui::TreeNodeEx("Virtual Event Mappings", flags)) + { + displayKeyMappingsAndVirtualStatesInline(&boundProjection); + ImGui::TreePop(); + } + + ImGui::TreePop(); + } + ImGui::PopItemWidth(); + } + ImGui::EndChild(); + ImGui::PopStyleVar(); + ImGui::EndTabItem(); + } + + if (ImGui::BeginTabItem("Camera")) + { + ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 4.0f); + if (ImGui::BeginChild("CameraPanel", ImVec2(0, 0), true)) + { + ImGui::PushItemWidth(-1.0f); + DrawSectionHeader("CameraInputHeader", "Input", accent); + ImGui::Checkbox("Mirror input to all cameras", &m_cameraControls.mirrorInput); + DrawHoverHint("Apply keyboard and mouse input to every camera"); + ImGui::Checkbox("World translate", &m_cameraControls.worldTranslate); + DrawHoverHint("Translate in world space instead of camera space"); + ImGui::SliderFloat("Keyboard scale", &m_cameraControls.keyboardScale, 0.01f, 10.f, "%.2f"); + DrawHoverHint("Scale keyboard movement magnitudes"); + ImGui::SliderFloat("Mouse move scale", &m_cameraControls.mouseMoveScale, 0.01f, 10.f, "%.2f"); + DrawHoverHint("Scale mouse move magnitudes"); + ImGui::SliderFloat("Mouse scroll scale", &m_cameraControls.mouseScrollScale, 0.01f, 10.f, "%.2f"); + DrawHoverHint("Scale mouse wheel magnitudes"); + ImGui::SliderFloat("Translate scale", &m_cameraControls.translationScale, 0.01f, 10.f, "%.2f"); + DrawHoverHint("Overall translation scale for virtual events"); + ImGui::SliderFloat("Rotate scale", &m_cameraControls.rotationScale, 0.01f, 10.f, "%.2f"); + DrawHoverHint("Overall rotation scale for virtual events"); + + DrawSectionHeader("CameraConstraintsHeader", "Constraints", accent); + ImGui::Checkbox("Enable constraints", &m_cameraConstraints.enabled); + DrawHoverHint("Enable or disable all camera constraints"); + ImGui::Checkbox("Clamp distance", &m_cameraConstraints.clampDistance); + DrawHoverHint("Clamp orbit distance to min/max"); + ImGui::SliderFloat("Min distance", &m_cameraConstraints.minDistance, 0.01f, 1000.f, "%.3f", ImGuiSliderFlags_Logarithmic); + DrawHoverHint("Minimum orbit distance"); + ImGui::SliderFloat("Max distance", &m_cameraConstraints.maxDistance, 0.01f, 10000.f, "%.3f", ImGuiSliderFlags_Logarithmic); + DrawHoverHint("Maximum orbit distance"); + ImGui::Separator(); + ImGui::Checkbox("Clamp pitch", &m_cameraConstraints.clampPitch); + DrawHoverHint("Clamp pitch angle"); + ImGui::SliderFloat("Pitch min", &m_cameraConstraints.pitchMinDeg, -180.f, 180.f, "%.1f"); + DrawHoverHint("Minimum pitch in degrees"); + ImGui::SliderFloat("Pitch max", &m_cameraConstraints.pitchMaxDeg, -180.f, 180.f, "%.1f"); + DrawHoverHint("Maximum pitch in degrees"); + ImGui::Checkbox("Clamp yaw", &m_cameraConstraints.clampYaw); + DrawHoverHint("Clamp yaw angle"); + ImGui::SliderFloat("Yaw min", &m_cameraConstraints.yawMinDeg, -180.f, 180.f, "%.1f"); + DrawHoverHint("Minimum yaw in degrees"); + ImGui::SliderFloat("Yaw max", &m_cameraConstraints.yawMaxDeg, -180.f, 180.f, "%.1f"); + DrawHoverHint("Maximum yaw in degrees"); + ImGui::Checkbox("Clamp roll", &m_cameraConstraints.clampRoll); + DrawHoverHint("Clamp roll angle"); + ImGui::SliderFloat("Roll min", &m_cameraConstraints.rollMinDeg, -180.f, 180.f, "%.1f"); + DrawHoverHint("Minimum roll in degrees"); + ImGui::SliderFloat("Roll max", &m_cameraConstraints.rollMaxDeg, -180.f, 180.f, "%.1f"); + DrawHoverHint("Maximum roll in degrees"); + + DrawSectionHeader("OrbitHeader", "Orbit Target", accent); + + auto* activeCamera = getActiveCamera(); + const bool hasOrbitTarget = withOrbitLikeCamera(activeCamera, [&](auto* orbit) + { + auto target = getCastedVector(orbit->getTarget()); + if (ImGui::InputFloat3("Target", &target[0])) + orbit->target(getCastedVector(target)); + + if (ImGui::Button("Target model")) + { + auto targetPos = hlsl::transpose(getMatrix3x4As4x4(m_model))[3]; + orbit->target(targetPos); + } + DrawHoverHint("Set orbit target to the model position"); + ImGui::SameLine(); + if (ImGui::Button("Target origin")) + orbit->target(float64_t3(0.0)); + DrawHoverHint("Set orbit target to world origin"); + }); + if (!hasOrbitTarget) + { + ImGui::TextDisabled("Active camera is not orbit."); + } + ImGui::PopItemWidth(); + } + ImGui::EndChild(); + ImGui::PopStyleVar(); + ImGui::EndTabItem(); + } + + if (ImGui::BeginTabItem("Presets")) + { + ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 4.0f); + if (ImGui::BeginChild("PresetsPanel", ImVec2(0, 0), true)) + { + ImGui::PushItemWidth(-1.0f); + DrawSectionHeader("PresetsHeader", "Presets", accent); + ImGui::InputText("Preset name", m_presetName, IM_ARRAYSIZE(m_presetName)); + if (ImGui::Button("Add preset")) + { + auto* activeCamera = getActiveCamera(); + m_presets.emplace_back(capturePreset(activeCamera, m_presetName)); + } + DrawHoverHint("Store current camera as a preset"); + ImGui::SameLine(); + if (ImGui::Button("Clear presets")) + m_presets.clear(); + DrawHoverHint("Remove all presets"); + + if (!m_presets.empty()) + { + std::vector names; + names.reserve(m_presets.size()); + for (const auto& preset : m_presets) + names.push_back(preset.name.c_str()); + + static int selectedPreset = -1; + ImGui::ListBox("Preset list", &selectedPreset, names.data(), static_cast(names.size()), 6); + + if (selectedPreset >= 0 && static_cast(selectedPreset) < m_presets.size()) + { + if (ImGui::Button("Apply preset")) + applyPresetToCamera(getActiveCamera(), m_presets[static_cast(selectedPreset)]); + DrawHoverHint("Apply selected preset to the active camera"); + ImGui::SameLine(); + if (ImGui::Button("Remove preset")) + m_presets.erase(m_presets.begin() + selectedPreset); + DrawHoverHint("Remove selected preset"); + } + } + + DrawSectionHeader("PresetsStorageHeader", "Storage", accent); + ImGui::InputText("Preset file", m_presetPath, IM_ARRAYSIZE(m_presetPath)); + if (ImGui::Button("Save presets")) + { + if (!savePresetsToFile(system::path(m_presetPath))) + m_logger->log("Failed to save presets to \"%s\".", ILogger::ELL_ERROR, m_presetPath); + } + DrawHoverHint("Save presets to JSON file"); + ImGui::SameLine(); + if (ImGui::Button("Load presets")) + { + if (!loadPresetsFromFile(system::path(m_presetPath))) + m_logger->log("Failed to load presets from \"%s\".", ILogger::ELL_ERROR, m_presetPath); + } + DrawHoverHint("Load presets from JSON file"); + ImGui::PopItemWidth(); + } + ImGui::EndChild(); + ImGui::PopStyleVar(); + ImGui::EndTabItem(); + } + + if (ImGui::BeginTabItem("Playback")) + { + ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 4.0f); + if (ImGui::BeginChild("PlaybackPanel", ImVec2(0, 0), true)) + { + ImGui::PushItemWidth(-1.0f); + DrawSectionHeader("PlaybackHeader", "Playback", accent); + ImGui::Checkbox("Loop", &m_playback.loop); + DrawHoverHint("Loop playback when it reaches the end"); + ImGui::Checkbox("Override input", &m_playback.overrideInput); + DrawHoverHint("Ignore manual input during playback"); + ImGui::Checkbox("Affect all cameras", &m_playbackAffectsAll); + DrawHoverHint("Apply playback to all cameras"); + ImGui::SliderFloat("Speed", &m_playback.speed, 0.1f, 4.f, "%.2f"); + DrawHoverHint("Playback speed multiplier"); + + if (ImGui::Button(m_playback.playing ? "Pause" : "Play")) + m_playback.playing = !m_playback.playing; + DrawHoverHint("Start or pause playback"); + ImGui::SameLine(); + if (ImGui::Button("Stop")) + { + m_playback.playing = false; + m_playback.time = 0.f; + } + DrawHoverHint("Stop playback and reset time"); + + if (!m_keyframes.empty()) + { + const float duration = m_keyframes.back().time; + ImGui::SliderFloat("Time", &m_playback.time, 0.f, duration, "%.3f"); + } + + DrawSectionHeader("KeyframesHeader", "Keyframes", accent); + ImGui::InputFloat("New keyframe time", &m_newKeyframeTime, 0.1f, 1.f, "%.3f"); + DrawHoverHint("Time value for new keyframe"); + if (ImGui::Button("Add keyframe")) + { + auto* activeCamera = getActiveCamera(); + CameraKeyframe keyframe; + keyframe.time = m_newKeyframeTime; + keyframe.preset = capturePreset(activeCamera, "Keyframe"); + m_keyframes.emplace_back(std::move(keyframe)); + std::sort(m_keyframes.begin(), m_keyframes.end(), [](const auto& a, const auto& b) { return a.time < b.time; }); + } + DrawHoverHint("Add keyframe from current camera"); + ImGui::SameLine(); + if (ImGui::Button("Clear keyframes")) + m_keyframes.clear(); + DrawHoverHint("Remove all keyframes"); + + if (!m_keyframes.empty()) + { + if (ImGui::BeginChild("KeyframeList", ImVec2(0, 120), true)) + { + for (size_t i = 0; i < m_keyframes.size(); ++i) + { + ImGui::Text("[%zu] t=%.3f", i, m_keyframes[i].time); + } + } + ImGui::EndChild(); + } + ImGui::PopItemWidth(); + } + ImGui::EndChild(); + ImGui::PopStyleVar(); + ImGui::EndTabItem(); + } + + if (ImGui::BeginTabItem("Gizmo")) + { + ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 4.0f); + if (ImGui::BeginChild("GizmoPanel", ImVec2(0, 0), true)) + { + DrawSectionHeader("GizmoHeader", "Gizmo", accent); + TransformEditorContents(); + } + ImGui::EndChild(); + ImGui::PopStyleVar(); + ImGui::EndTabItem(); + } + + if (m_showEventLog && ImGui::BeginTabItem("Log")) + { + ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 4.0f); + if (ImGui::BeginChild("LogPanel", ImVec2(0, 0), true)) + { + DrawSectionHeader("LogHeader", "Virtual Events", accent); + ImGui::Checkbox("Auto-scroll", &m_logAutoScroll); + ImGui::SameLine(); + ImGui::Checkbox("Wrap", &m_logWrap); + ImGui::Separator(); + + ImGuiWindowFlags logFlags = m_logWrap ? ImGuiWindowFlags_None : ImGuiWindowFlags_HorizontalScrollbar; + if (ImGui::BeginChild("LogList", ImVec2(0, 0), false, logFlags)) + { + const float scrollY = ImGui::GetScrollY(); + const float scrollMax = ImGui::GetScrollMaxY(); + const bool wasAtBottom = scrollY >= scrollMax - 5.0f; + const size_t start = m_virtualEventLog.size() > 200 ? m_virtualEventLog.size() - 200 : 0; + if (m_logWrap) + ImGui::PushTextWrapPos(0.0f); + for (size_t i = start; i < m_virtualEventLog.size(); ++i) + { + const auto& entry = m_virtualEventLog[i]; + ImGui::TextUnformatted(entry.line.c_str()); + } + if (m_logWrap) + ImGui::PopTextWrapPos(); + if (m_logAutoScroll && wasAtBottom && !m_virtualEventLog.empty()) + ImGui::SetScrollHereY(1.0f); + } + ImGui::EndChild(); + } + ImGui::EndChild(); + ImGui::PopStyleVar(); + ImGui::EndTabItem(); + } + + ImGui::EndTabBar(); + } + + ImGui::End(); + ImGui::PopStyleColor(19); + ImGui::PopStyleVar(9); diff --git a/61_UI/include/app/impl/AppImGuiListen.inl b/61_UI/include/app/impl/AppImGuiListen.inl new file mode 100644 index 000000000..161040f3b --- /dev/null +++ b/61_UI/include/app/impl/AppImGuiListen.inl @@ -0,0 +1,478 @@ + ImGuiIO& io = ImGui::GetIO(); + if (m_ciMode) + { + io.IniFilename = nullptr; + useWindow = true; + } + + ImGuizmo::BeginFrame(); + { + if (!m_ciMode) + { + } + + SImResourceInfo info; + info.samplerIx = (uint16_t)nbl::ext::imgui::UI::DefaultSamplerIx::USER; + + // ORBIT CAMERA TEST + { + for (auto& planar : m_planarProjections) + { + auto* camera = planar->getCamera(); + withOrbitLikeCamera(camera, [&](auto* orbit) + { + auto targetPostion = hlsl::transpose(getMatrix3x4As4x4(m_model))[3]; + orbit->target(targetPostion); + orbit->manipulate({}, {}); + }); + } + } + + // render bound planar camera views onto GUI windows + if (useWindow) + { + syncVisualDebugWindowBindings(); + const bool hideSceneGizmos = enableActiveCameraMovement || (m_scriptedInput.enabled && m_scriptedInput.visualDebug); + if(hideSceneGizmos) + ImGuizmo::Enable(false); + else + ImGuizmo::Enable(true); + + size_t gizmoIx = {}; + size_t manipulationCounter = {}; + const std::optional modelInUseIx = ImGuizmo::IsUsingAny() ? std::optional(boundPlanarCameraIxToManipulate.has_value() ? 1u + boundPlanarCameraIxToManipulate.value() : 0u) : std::optional(std::nullopt); + + for (uint32_t windowIx = 0; windowIx < windowBindings.size(); ++windowIx) + { + // setup + { + const auto& rw = wInit.renderWindows[windowIx]; + const ImGuiCond windowCond = m_ciMode ? ImGuiCond_Always : ImGuiCond_Appearing; + ImGui::SetNextWindowPos({ rw.iPos.x, rw.iPos.y }, windowCond); + ImGui::SetNextWindowSize({ rw.iSize.x, rw.iSize.y }, windowCond); + } + ImGui::SetNextWindowSizeConstraints(ImVec2(0x45, 0x45), ImVec2(7680, 4320)); + + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f)); + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.0f, 0.0f, 0.0f, 0.0f)); + const std::string ident = "Render Window \"" + std::to_string(windowIx) + "\""; + + ImGui::Begin(ident.data(), 0, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoBringToFrontOnFocus); + const ImVec2 contentRegionSize = ImGui::GetContentRegionAvail(), windowPos = ImGui::GetWindowPos(), cursorPos = ImGui::GetCursorScreenPos(); + + ImGuiWindow* window = ImGui::GetCurrentWindow(); + { + const auto mPos = ImGui::GetMousePos(); + + if (mPos.x < cursorPos.x || mPos.y < cursorPos.y || mPos.x > cursorPos.x + contentRegionSize.x || mPos.y > cursorPos.y + contentRegionSize.y) + window->Flags &= ~ImGuiWindowFlags_NoMove; + else + window->Flags |= ImGuiWindowFlags_NoMove; + } + + // setup bound entities for the window like camera & projections + auto& binding = windowBindings[windowIx]; + auto& planarBound = m_planarProjections[binding.activePlanarIx]; + assert(planarBound); + + binding.aspectRatio = contentRegionSize.x / contentRegionSize.y; + auto* planarViewCameraBound = planarBound->getCamera(); + + assert(planarViewCameraBound); + assert(binding.boundProjectionIx.has_value()); + + auto& projection = planarBound->getPlanarProjections()[binding.boundProjectionIx.value()]; + applyDollyZoomProjection(planarViewCameraBound, projection); + projection.update(binding.leftHandedProjection, binding.aspectRatio); + + // TODO: + // would be nice to normalize imguizmo visual vectors (possible with styles) + + // first 0th texture is for UI texture atlas, then there are our window textures + auto fboImguiTextureID = windowIx + 1u; + info.textureID = fboImguiTextureID; + + if(binding.allowGizmoAxesToFlip) + ImGuizmo::AllowAxisFlip(true); + else + ImGuizmo::AllowAxisFlip(false); + + if(projection.getParameters().m_type == IPlanarProjection::CProjection::Orthographic) + ImGuizmo::SetOrthographic(true); + else + ImGuizmo::SetOrthographic(false); + + ImGuizmo::SetDrawlist(); + ImGui::Image(info, contentRegionSize); + ImGuizmo::SetRect(cursorPos.x, cursorPos.y, contentRegionSize.x, contentRegionSize.y); + { + const char* projLabel = projection.getParameters().m_type == IPlanarProjection::CProjection::Perspective ? "Persp" : "Ortho"; + const std::string overlayText = "Planar " + std::to_string(binding.activePlanarIx) + " | " + projLabel + " | W" + std::to_string(windowIx); + const std::string cameraText = std::string(getCameraTypeLabel(planarViewCameraBound)) + ": " + std::string(getCameraTypeDescription(planarViewCameraBound)); + const ImVec2 textSize = ImGui::CalcTextSize(overlayText.c_str()); + const ImVec2 descSize = ImGui::CalcTextSize(cameraText.c_str()); + const ImVec2 pad = ImVec2(6.0f, 4.0f); + const float lineGap = 2.0f; + const float width = std::max(textSize.x, descSize.x); + const float height = textSize.y + descSize.y + lineGap + pad.y * 2.0f; + ImVec2 overlayPos = ImVec2(cursorPos.x + contentRegionSize.x - width - pad.x * 2.0f - 6.0f, cursorPos.y + 6.0f); + overlayPos.x = std::max(overlayPos.x, cursorPos.x + 6.0f); + ImVec2 overlayMax = ImVec2(overlayPos.x + width + pad.x * 2.0f, overlayPos.y + height); + auto* drawList = ImGui::GetWindowDrawList(); + drawList->AddRectFilled(overlayPos, overlayMax, ImGui::ColorConvertFloat4ToU32(ImVec4(0.05f, 0.06f, 0.08f, 0.80f)), 6.0f); + drawList->AddRect(overlayPos, overlayMax, ImGui::ColorConvertFloat4ToU32(ImVec4(0.60f, 0.66f, 0.76f, 0.80f)), 6.0f); + drawList->AddText(ImVec2(overlayPos.x + pad.x, overlayPos.y + pad.y), ImGui::ColorConvertFloat4ToU32(ImVec4(0.96f, 0.98f, 1.0f, 1.0f)), overlayText.c_str()); + drawList->AddText(ImVec2(overlayPos.x + pad.x, overlayPos.y + pad.y + textSize.y + lineGap), ImGui::ColorConvertFloat4ToU32(ImVec4(0.78f, 0.82f, 0.90f, 1.0f)), cameraText.c_str()); + } + + // I will assume we need to focus a window to start manipulating objects from it + if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) + { + if (!(m_scriptedInput.enabled && m_scriptedInput.exclusive)) + activeRenderWindowIx = windowIx; + } + + // we render a scene from view of a camera bound to planar window + ImGuizmoPlanarM16InOut imguizmoPlanar; + imguizmoPlanar.view = getCastedMatrix(hlsl::transpose(getMatrix3x4As4x4(planarViewCameraBound->getGimbal().getViewMatrix()))); + imguizmoPlanar.projection = getCastedMatrix(hlsl::transpose(projection.getProjectionMatrix())); + const auto viewMatrix = getMatrix3x4As4x4(getCastedMatrix(planarViewCameraBound->getGimbal().getViewMatrix())); + const auto projectionMatrix = getCastedMatrix(projection.getProjectionMatrix()); + const auto& projectionParams = projection.getParameters(); + drawWorldReferenceOverlay(cursorPos, contentRegionSize, viewMatrix, projectionMatrix, binding.leftHandedProjection, projectionParams.m_zNear, projectionParams.m_zFar); + + if (flipGizmoY) // note we allow to flip gizmo just to match our coordinates + imguizmoPlanar.projection[1][1] *= -1.f; // https://johannesugb.github.io/gpu-programming/why-do-opengl-proj-matrices-fail-in-vulkan/ + + static constexpr float identityMatrix[] = + { + 1.f, 0.f, 0.f, 0.f, + 0.f, 1.f, 0.f, 0.f, + 0.f, 0.f, 1.f, 0.f, + 0.f, 0.f, 0.f, 1.f + }; + + if(!hideSceneGizmos && binding.enableDebugGridDraw) + ImGuizmo::DrawGrid(&imguizmoPlanar.view[0][0], &imguizmoPlanar.projection[0][0], identityMatrix, 100.f); + + if (!hideSceneGizmos) + { + for (uint32_t modelIx = 0; modelIx < 1u + m_planarProjections.size(); modelIx++) + { + ImGuizmo::PushID(gizmoIx); ++gizmoIx; + + const bool isCameraGimbalTarget = modelIx; // I assume scene demo model is 0th ix, left are planar cameras + ICamera* const targetGimbalManipulationCamera = isCameraGimbalTarget ? m_planarProjections[modelIx - 1u]->getCamera() : nullptr; + + // if we try to manipulate a camera which appears to be the same camera we see scene from then obvsly it doesn't make sense to manipulate its gizmo so we skip it + // EDIT: it actually makes some sense if you assume render planar view is rendered with ortho projection, but we would need to add imguizmo controller virtual map + // to ban forward/backward in this mode if this condition is true + if (targetGimbalManipulationCamera == planarViewCameraBound) + { + ImGuizmo::PopID(); + continue; + } + + ImGuizmoModelM16InOut imguizmoModel; + + if (isCameraGimbalTarget) + { + assert(targetGimbalManipulationCamera); + imguizmoModel.inTRS = getCastedMatrix(targetGimbalManipulationCamera->getGimbal().template operator() < float64_t4x4 > ()); + } + else + imguizmoModel.inTRS = hlsl::transpose(getMatrix3x4As4x4(m_model)); + + const float gizmoWorldRadius = 0.22f; + float32_t3 gizmoWorldPos = {}; + if (isCameraGimbalTarget) + gizmoWorldPos = getCastedVector(targetGimbalManipulationCamera->getGimbal().getPosition()); + else + { + const auto modelPos = hlsl::transpose(getMatrix3x4As4x4(m_model))[3]; + gizmoWorldPos = float32_t3(modelPos.x, modelPos.y, modelPos.z); + } + + const auto viewPos = mul(viewMatrix, float32_t4(gizmoWorldPos, 1.0f)); + const float depth = std::max(0.001f, std::abs(viewPos.z)); + float gizmoSizeClip = 0.1f; + if (projection.getParameters().m_type == IPlanarProjection::CProjection::Perspective) + gizmoSizeClip = (gizmoWorldRadius * projectionMatrix[1][1]) / depth; + else + gizmoSizeClip = gizmoWorldRadius * projectionMatrix[1][1]; + ImGuizmo::SetGizmoSizeClipSpace(gizmoSizeClip); + + imguizmoModel.outTRS = imguizmoModel.inTRS; + { + const bool success = ImGuizmo::Manipulate(&imguizmoPlanar.view[0][0], &imguizmoPlanar.projection[0][0], ImGuizmo::OPERATION::UNIVERSAL, mCurrentGizmoMode, &imguizmoModel.outTRS[0][0], &imguizmoModel.outDeltaTRS[0][0], useSnap ? &snap[0] : nullptr); + + if (success) + { + if (targetGimbalManipulationCamera) + { + const auto referenceFrame = getCastedMatrix(*reinterpret_cast(ImGuizmo::GetReferenceFrame())); + + boundCameraToManipulate = smart_refctd_ptr(targetGimbalManipulationCamera); + boundPlanarCameraIxToManipulate = modelIx - 1u; + + // TODO: TO BE REMOVED, ONLY FOR TESTING ITS INCOMPLETE TYPE! + const auto& imguizmoCtx = ImGuizmo::GetContext(); + + struct + { + float32_t3 t, r, s; + } out, delta; + + ImGuizmo::DecomposeMatrixToComponents(&imguizmoModel.outTRS[0][0], &out.t[0], &out.r[0], &out.s[0]); + ImGuizmo::DecomposeMatrixToComponents(&imguizmoModel.outDeltaTRS[0][0], &delta.t[0], &delta.r[0], &delta.s[0]); + { + std::vector virtualEvents; + + auto requestMagnitudeUpdateWithScalar = [&](float signPivot, float dScalar, float dMagnitude, auto positive, auto negative) + { + if (dScalar != signPivot) + { + auto& ev = virtualEvents.emplace_back(); + auto code = (dScalar > signPivot) ? positive : negative; + + ev.type = code; + ev.magnitude += dMagnitude; + } + }; + + // TODO TESTING STUFF WITH MY IMGUIZMO UPDATES + // IT WILL BE REMOVED ONCE ALL TESTS ARE DONE + // AND CONTROLLER API WILL BE USED INSTEAD + + // translations + { + ImGuizmo::OPERATION ioType; + const auto dScalar = ImGuizmo::GetTranslationDeltaScalar(&ioType); + + if (dScalar) + { + switch (ioType) + { + case ImGuizmo::OPERATION::TRANSLATE_X: + { + requestMagnitudeUpdateWithScalar(0.f, dScalar, std::abs(dScalar), CVirtualGimbalEvent::VirtualEventType::MoveRight, CVirtualGimbalEvent::VirtualEventType::MoveLeft); + } break; + + case ImGuizmo::OPERATION::TRANSLATE_Y: + { + requestMagnitudeUpdateWithScalar(0.f, dScalar, std::abs(dScalar), CVirtualGimbalEvent::VirtualEventType::MoveUp, CVirtualGimbalEvent::VirtualEventType::MoveDown); + } break; + + case ImGuizmo::OPERATION::TRANSLATE_Z: + { + requestMagnitudeUpdateWithScalar(0.f, dScalar, std::abs(dScalar), CVirtualGimbalEvent::VirtualEventType::MoveForward, CVirtualGimbalEvent::VirtualEventType::MoveBackward); + } break; + + default: break; + } + } + } + + // TODO: ok becuase I have only one reference from imguizmo I must do it differently when + // I have local base && want to do rotation with respect to world instead; we almost there + + // rotations + { + ImGuizmo::OPERATION ioType; + float dRadians = ImGuizmo::GetRotationDeltaRadians(&ioType); + + if (dRadians) + { + switch (ioType) + { + case ImGuizmo::OPERATION::ROTATE_X: + { + requestMagnitudeUpdateWithScalar(0.f, dRadians, std::abs(dRadians), CVirtualGimbalEvent::VirtualEventType::TiltUp, CVirtualGimbalEvent::VirtualEventType::TiltDown); + } break; + + case ImGuizmo::OPERATION::ROTATE_Y: + { + requestMagnitudeUpdateWithScalar(0.f, dRadians, std::abs(dRadians), CVirtualGimbalEvent::VirtualEventType::PanRight, CVirtualGimbalEvent::VirtualEventType::PanLeft); + } break; + + case ImGuizmo::OPERATION::ROTATE_Z: + { + requestMagnitudeUpdateWithScalar(0.f, dRadians, std::abs(dRadians), CVirtualGimbalEvent::VirtualEventType::RollRight, CVirtualGimbalEvent::VirtualEventType::RollLeft); + } break; + + default: + assert(false); break; // should never be hit + } + } + } + + const auto vCount = virtualEvents.size(); + + if (vCount) + { + const float pMoveSpeed = targetGimbalManipulationCamera->getMoveSpeedScale(); + const float pRotationSpeed = targetGimbalManipulationCamera->getRotationSpeedScale(); + + // I start to think controller should be able to set sensitivity to scale magnitudes of generated events + // in order for camera to not keep any magnitude scalars like move or rotation speed scales + + targetGimbalManipulationCamera->setMoveSpeedScale(1); + targetGimbalManipulationCamera->setRotationSpeedScale(1); + + targetGimbalManipulationCamera->manipulate({ virtualEvents.data(), vCount }, &referenceFrame); + + targetGimbalManipulationCamera->setMoveSpeedScale(pMoveSpeed); + targetGimbalManipulationCamera->setRotationSpeedScale(pRotationSpeed); + } + + } + } + else + { + // again, for scene demo model full affine transformation without limits is assumed + m_model = float32_t3x4(hlsl::transpose(imguizmoModel.outTRS)); + boundCameraToManipulate = nullptr; + boundPlanarCameraIxToManipulate = std::nullopt; + } + } + + if (ImGuizmo::IsOver() and not ImGuizmo::IsUsingAny() && not enableActiveCameraMovement) + { + if (targetGimbalManipulationCamera && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) + { + const uint32_t newPlanarIx = modelIx - 1u; + if (newPlanarIx < m_planarProjections.size()) + { + binding.activePlanarIx = newPlanarIx; + binding.pickDefaultProjections(m_planarProjections[binding.activePlanarIx]->getPlanarProjections()); + if (!(m_scriptedInput.enabled && m_scriptedInput.exclusive)) + activeRenderWindowIx = windowIx; + } + } + + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.2f, 0.2f, 0.2f, 0.8f)); + ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.5f); + + ImGuiIO& io = ImGui::GetIO(); + ImVec2 mousePos = io.MousePos; + ImGui::SetNextWindowPos(ImVec2(mousePos.x + 10, mousePos.y + 10), ImGuiCond_Always); + + ImGui::Begin("InfoOverlay", nullptr, + ImGuiWindowFlags_NoDecoration | + ImGuiWindowFlags_AlwaysAutoResize | + ImGuiWindowFlags_NoSavedSettings); + + std::string ident; + + if (targetGimbalManipulationCamera) + ident = targetGimbalManipulationCamera->getIdentifier(); + else + ident = "Geometry Creator Object"; + + ImGui::Text("Identifier: %s", ident.c_str()); + ImGui::Text("Object Ix: %u", modelIx); + if (targetGimbalManipulationCamera) + { + ImGui::Separator(); + ImGui::TextDisabled("RMB: switch view to this camera"); + ImGui::TextDisabled("LMB drag: manipulate gizmo"); + ImGui::TextDisabled("SPACE: toggle move mode"); + } + + ImGui::End(); + + ImGui::PopStyleVar(); + ImGui::PopStyleColor(2); + } + } + ImGuizmo::PopID(); + } + } + + ImGui::End(); + ImGui::PopStyleColor(1); + ImGui::PopStyleVar(3); + } + if (windowBindings.size() > 1u) + { + const auto& topRw = wInit.renderWindows[0]; + const float splitY = topRw.iPos.y + topRw.iSize.y; + const float gap = std::max(0.0f, wInit.renderWindows[1].iPos.y - splitY); + ImGui::SetNextWindowPos(ImVec2(0.0f, 0.0f), ImGuiCond_Always); + ImGui::SetNextWindowSize(io.DisplaySize, ImGuiCond_Always); + ImGui::Begin("SplitOverlay", nullptr, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoBringToFrontOnFocus); + auto* drawList = ImGui::GetWindowDrawList(); + if (gap >= 2.0f) + drawList->AddRectFilled(ImVec2(0.0f, splitY), ImVec2(io.DisplaySize.x, splitY + gap), ImGui::ColorConvertFloat4ToU32(ImVec4(0.05f, 0.06f, 0.08f, 0.85f))); + else + drawList->AddLine(ImVec2(0.0f, splitY), ImVec2(io.DisplaySize.x, splitY), ImGui::ColorConvertFloat4ToU32(ImVec4(0.80f, 0.84f, 0.92f, 0.75f)), 2.0f); + ImGui::End(); + } + assert(manipulationCounter <= 1u); + } + // render selected camera view onto full screen + else + { + info.textureID = 1u + activeRenderWindowIx; + + 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); + const ImVec2 contentRegionSize = ImGui::GetContentRegionAvail(), windowPos = ImGui::GetWindowPos(), cursorPos = ImGui::GetCursorScreenPos(); + { + auto& binding = windowBindings[activeRenderWindowIx]; + auto& planarBound = m_planarProjections[binding.activePlanarIx]; + assert(planarBound); + + binding.aspectRatio = contentRegionSize.x / contentRegionSize.y; + auto* planarViewCameraBound = planarBound->getCamera(); + + assert(planarViewCameraBound); + assert(binding.boundProjectionIx.has_value()); + + auto& projection = planarBound->getPlanarProjections()[binding.boundProjectionIx.value()]; + applyDollyZoomProjection(planarViewCameraBound, projection); + projection.update(binding.leftHandedProjection, binding.aspectRatio); + } + + ImGui::Image(info, contentRegionSize); + ImGuizmo::SetRect(cursorPos.x, cursorPos.y, contentRegionSize.x, contentRegionSize.y); + + ImGui::End(); + ImGui::PopStyleColor(1); + } + } + + drawScriptVisualDebugOverlay(io.DisplaySize); + DrawControlPanel(); + UpdateBoundCameraMovement(); + UpdateCursorVisibility(); + + // update camera matrices for scene rendering + { + for (uint32_t i = 0u; i < windowBindings.size(); ++i) + { + auto& binding = windowBindings[i]; + + auto& planarBound = m_planarProjections[binding.activePlanarIx]; + assert(planarBound); + auto* boundPlanarCamera = planarBound->getCamera(); + + assert(binding.boundProjectionIx.has_value()); + auto& projection = planarBound->getPlanarProjections()[binding.boundProjectionIx.value()]; + applyDollyZoomProjection(boundPlanarCamera, projection); + projection.update(binding.leftHandedProjection, binding.aspectRatio); + + auto viewMatrix = getCastedMatrix(boundPlanarCamera->getGimbal().getViewMatrix()); + auto viewProjMatrix = mul(getCastedMatrix(projection.getProjectionMatrix()), getMatrix3x4As4x4(viewMatrix)); + + binding.viewMatrix = viewMatrix; + binding.viewProjMatrix = viewProjMatrix; + } + } + + diff --git a/61_UI/include/app/impl/AppInit.inl b/61_UI/include/app/impl/AppInit.inl new file mode 100644 index 000000000..f0a740303 --- /dev/null +++ b/61_UI/include/app/impl/AppInit.inl @@ -0,0 +1,1417 @@ + 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); + + 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_ciMode = program.get("--ci"); + if (m_ciMode) + { + m_ciScreenshotPath = localOutputCWD / "cameraz_ci.png"; + m_ciStartedAt = clock_t::now(); + } + m_scriptedInput.log = program.get("--script-log"); + m_scriptVisualDebugCli = program.get("--script-visual-debug"); + + // Create imput system + m_inputSystem = make_smart_refctd_ptr(logger_opt_smart_ptr(smart_refctd_ptr(m_logger))); + m_logFormatter = core::make_smart_refctd_ptr(); + + // Remember to call the base class initialization! + if (!base_t::onAppInitialized(std::move(system))) + return false; + + { + smart_refctd_ptr examplesHeaderArch, examplesSourceArch, examplesBuildArch, thisExampleArch, thisExampleBuildArch; +#ifdef NBL_EMBED_BUILTIN_RESOURCES + examplesHeaderArch = core::make_smart_refctd_ptr(smart_refctd_ptr(m_logger)); + examplesSourceArch = core::make_smart_refctd_ptr(smart_refctd_ptr(m_logger)); + examplesBuildArch = core::make_smart_refctd_ptr(smart_refctd_ptr(m_logger)); + + #ifdef _NBL_THIS_EXAMPLE_BUILTIN_C_ARCHIVE_H_ + thisExampleArch = make_smart_refctd_ptr(smart_refctd_ptr(m_logger)); + #endif + + #ifdef _NBL_THIS_EXAMPLE_BUILTIN_BUILD_C_ARCHIVE_H_ + thisExampleBuildArch = make_smart_refctd_ptr(smart_refctd_ptr(m_logger)); + #endif +#else + examplesHeaderArch = make_smart_refctd_ptr(localInputCWD/"../common/include/nbl/examples", smart_refctd_ptr(m_logger), m_system.get()); + examplesSourceArch = make_smart_refctd_ptr(localInputCWD/"../common/src/nbl/examples", smart_refctd_ptr(m_logger), m_system.get()); + examplesBuildArch = make_smart_refctd_ptr(NBL_EXAMPLES_BUILD_MOUNT_POINT, smart_refctd_ptr(m_logger), m_system.get()); + thisExampleArch = make_smart_refctd_ptr(localInputCWD/"app_resources", smart_refctd_ptr(m_logger), m_system.get()); + #ifdef NBL_THIS_EXAMPLE_BUILD_MOUNT_POINT + thisExampleBuildArch = make_smart_refctd_ptr(NBL_THIS_EXAMPLE_BUILD_MOUNT_POINT, smart_refctd_ptr(m_logger), m_system.get()); + #endif +#endif + m_system->mount(std::move(examplesHeaderArch),"nbl/examples"); + m_system->mount(std::move(examplesSourceArch),"nbl/examples"); + m_system->mount(std::move(examplesBuildArch),"nbl/examples"); + if (thisExampleArch) + m_system->mount(std::move(thisExampleArch),"app_resources"); + if (thisExampleBuildArch) + m_system->mount(std::move(thisExampleBuildArch),"app_resources"); + } + + { + const std::optional cameraJsonFile = program.is_used("--file") ? program.get("--file") : std::optional(std::nullopt); + + json j; + auto loadDefaultConfig = [&]() -> bool + { +#ifdef _NBL_THIS_EXAMPLE_BUILTIN_C_ARCHIVE_H_ + auto assets = make_smart_refctd_ptr(smart_refctd_ptr(m_logger)); + auto pFile = assets->getFile("cameras.json", IFile::ECF_READ, ""); + if (!pFile) + return logFail("Could not open builtin cameras.json!"); + + string config; + IFile::success_t result; + config.resize(pFile->getSize()); + pFile->read(result, config.data(), 0, pFile->getSize()); + j = json::parse(config); + return true; +#else + const auto fallbackPath = localInputCWD / "app_resources" / "cameras.json"; + std::ifstream fallbackFile(fallbackPath); + if (!fallbackFile.is_open()) + return logFail("Cannot open default config \"%s\".", fallbackPath.string().c_str()); + fallbackFile >> j; + return true; +#endif + }; + + auto file = cameraJsonFile.has_value() ? std::ifstream(cameraJsonFile.value()) : std::ifstream(); + if (!file.is_open()) + { + if (cameraJsonFile.has_value()) + m_logger->log("Cannot open input \"%s\" json file. Switching to default config.", ILogger::ELL_WARNING, cameraJsonFile.value().c_str()); + else + m_logger->log("No input json file provided. Switching to default config.", ILogger::ELL_INFO); + + if (!loadDefaultConfig()) + return false; + } + else + { + file >> j; + } + + auto loadScriptJson = [&](const std::string& path, json& out) -> bool + { + std::ifstream sfile(path); + if (!sfile.is_open()) + { + m_logger->log("Cannot open scripted input file \"%s\".", ILogger::ELL_ERROR, path.c_str()); + return false; + } + sfile >> out; + return true; + }; + + auto parseScriptedInput = [&](const json& script) -> void + { + m_scriptedInput.events.clear(); + m_scriptedInput.checks.clear(); + m_scriptedInput.captureFrames.clear(); + m_scriptedInput.nextEventIndex = 0; + m_scriptedInput.nextCheckIndex = 0; + m_scriptedInput.nextCaptureIndex = 0; + m_scriptedInput.failed = false; + m_scriptedInput.summaryReported = false; + m_scriptedInput.baselineValid = false; + m_scriptedInput.stepValid = false; + m_scriptedInput.exclusive = false; + m_scriptedInput.hardFail = false; + m_scriptedInput.visualDebug = false; + m_scriptedInput.visualTargetFps = 0.f; + m_scriptedInput.visualCameraHoldSeconds = 0.f; + m_scriptedInput.visualActivePlanarValid = false; + m_scriptedInput.visualActivePlanarIx = 0u; + m_scriptedInput.visualActivePlanarStartFrame = 0u; + m_scriptedInput.framePacerInitialized = false; + m_scriptedInput.capturePrefix = "script"; + m_scriptedInput.captureOutputDir = localOutputCWD; + + if (script.contains("enabled")) + m_scriptedInput.enabled = script["enabled"].get(); + else + m_scriptedInput.enabled = true; + + if (script.contains("log")) + m_scriptedInput.log = script["log"].get() || m_scriptedInput.log; + + if (script.contains("hard_fail")) + m_scriptedInput.hardFail = script["hard_fail"].get(); + + if (script.contains("visual_debug")) + m_scriptedInput.visualDebug = script["visual_debug"].get(); + if (script.contains("visual_debug_target_fps")) + m_scriptedInput.visualTargetFps = script["visual_debug_target_fps"].get(); + if (script.contains("visual_debug_hold_seconds")) + m_scriptedInput.visualCameraHoldSeconds = script["visual_debug_hold_seconds"].get(); + if (m_scriptVisualDebugCli) + m_scriptedInput.visualDebug = true; + if (m_scriptedInput.visualDebug) + { + if (m_scriptedInput.visualTargetFps <= 0.f) + m_scriptedInput.visualTargetFps = 60.f; + if (m_scriptedInput.visualCameraHoldSeconds <= 0.f) + m_scriptedInput.visualCameraHoldSeconds = 3.f; + } + + if (script.contains("enableActiveCameraMovement")) + enableActiveCameraMovement = script["enableActiveCameraMovement"].get(); + else if (m_scriptedInput.enabled) + enableActiveCameraMovement = true; + + if (script.contains("exclusive_input")) + m_scriptedInput.exclusive = script["exclusive_input"].get() || m_scriptedInput.exclusive; + if (script.contains("exclusive")) + m_scriptedInput.exclusive = script["exclusive"].get() || m_scriptedInput.exclusive; + + if (script.contains("capture_prefix")) + m_scriptedInput.capturePrefix = script["capture_prefix"].get(); + if (m_scriptedInput.capturePrefix.empty()) + m_scriptedInput.capturePrefix = "script"; + if (script.contains("capture_frames")) + for (const auto& frame : script["capture_frames"]) + m_scriptedInput.captureFrames.emplace_back(frame.get()); + + if (script.contains("camera_controls")) + { + const auto& controls = script["camera_controls"]; + if (controls.contains("keyboard_scale")) + m_cameraControls.keyboardScale = controls["keyboard_scale"].get(); + if (controls.contains("mouse_move_scale")) + m_cameraControls.mouseMoveScale = controls["mouse_move_scale"].get(); + if (controls.contains("mouse_scroll_scale")) + m_cameraControls.mouseScrollScale = controls["mouse_scroll_scale"].get(); + if (controls.contains("translation_scale")) + m_cameraControls.translationScale = controls["translation_scale"].get(); + if (controls.contains("rotation_scale")) + m_cameraControls.rotationScale = controls["rotation_scale"].get(); + } + + if (script.contains("events")) + for (const auto& ev : script["events"]) + { + if (!ev.contains("frame") || !ev.contains("type")) + { + m_logger->log("Scripted input event missing \"frame\" or \"type\".", ILogger::ELL_WARNING); + continue; + } + + const auto frame = ev["frame"].get(); + const auto type = ev["type"].get(); + const bool captureFrame = ev.value("capture", false); + + if (type == "keyboard") + { + if (!ev.contains("key") || !ev.contains("action")) + { + m_logger->log("Scripted keyboard event missing \"key\" or \"action\".", ILogger::ELL_WARNING); + continue; + } + + const auto keyStr = ev["key"].get(); + const auto actionStr = ev["action"].get(); + const auto key = ui::stringToKeyCode(keyStr); + if (key == ui::EKC_NONE) + { + m_logger->log("Scripted keyboard event has invalid key \"%s\".", ILogger::ELL_WARNING, keyStr.c_str()); + continue; + } + + ui::SKeyboardEvent::E_KEY_ACTION action = ui::SKeyboardEvent::ECA_UNITIALIZED; + if (actionStr == "pressed" || actionStr == "press") + action = ui::SKeyboardEvent::ECA_PRESSED; + else if (actionStr == "released" || actionStr == "release") + action = ui::SKeyboardEvent::ECA_RELEASED; + + if (action == ui::SKeyboardEvent::ECA_UNITIALIZED) + { + m_logger->log("Scripted keyboard event has invalid action \"%s\".", ILogger::ELL_WARNING, actionStr.c_str()); + continue; + } + + ScriptedInputEvent entry; + entry.frame = frame; + entry.type = ScriptedInputEvent::Type::Keyboard; + entry.keyboard.key = key; + entry.keyboard.action = action; + m_scriptedInput.events.emplace_back(entry); + if (captureFrame) + m_scriptedInput.captureFrames.emplace_back(frame); + } + else if (type == "mouse") + { + if (!ev.contains("kind")) + { + m_logger->log("Scripted mouse event missing \"kind\".", ILogger::ELL_WARNING); + continue; + } + + const auto kind = ev["kind"].get(); + ScriptedInputEvent entry; + entry.frame = frame; + entry.type = ScriptedInputEvent::Type::Mouse; + + if (kind == "move") + { + entry.mouse.type = ui::SMouseEvent::EET_MOVEMENT; + entry.mouse.dx = ev.value("dx", 0); + entry.mouse.dy = ev.value("dy", 0); + } + else if (kind == "scroll") + { + entry.mouse.type = ui::SMouseEvent::EET_SCROLL; + entry.mouse.v = ev.value("v", 0); + entry.mouse.h = ev.value("h", 0); + } + else if (kind == "click") + { + if (!ev.contains("button") || !ev.contains("action")) + { + m_logger->log("Scripted click event missing \"button\" or \"action\".", ILogger::ELL_WARNING); + continue; + } + + const auto buttonStr = ev["button"].get(); + const auto actionStr = ev["action"].get(); + + ui::E_MOUSE_BUTTON button = ui::EMB_LEFT_BUTTON; + if (buttonStr == "LEFT_BUTTON") button = ui::EMB_LEFT_BUTTON; + else if (buttonStr == "RIGHT_BUTTON") button = ui::EMB_RIGHT_BUTTON; + else if (buttonStr == "MIDDLE_BUTTON") button = ui::EMB_MIDDLE_BUTTON; + else if (buttonStr == "BUTTON_4") button = ui::EMB_BUTTON_4; + else if (buttonStr == "BUTTON_5") button = ui::EMB_BUTTON_5; + else + { + m_logger->log("Scripted click event has invalid button \"%s\".", ILogger::ELL_WARNING, buttonStr.c_str()); + continue; + } + + ui::SMouseEvent::SClickEvent::E_ACTION action = ui::SMouseEvent::SClickEvent::EA_UNITIALIZED; + if (actionStr == "pressed" || actionStr == "press") + action = ui::SMouseEvent::SClickEvent::EA_PRESSED; + else if (actionStr == "released" || actionStr == "release") + action = ui::SMouseEvent::SClickEvent::EA_RELEASED; + + if (action == ui::SMouseEvent::SClickEvent::EA_UNITIALIZED) + { + m_logger->log("Scripted click event has invalid action \"%s\".", ILogger::ELL_WARNING, actionStr.c_str()); + continue; + } + + entry.mouse.type = ui::SMouseEvent::EET_CLICK; + entry.mouse.button = button; + entry.mouse.action = action; + entry.mouse.x = ev.value("x", 0); + entry.mouse.y = ev.value("y", 0); + } + else + { + m_logger->log("Scripted mouse event has invalid kind \"%s\".", ILogger::ELL_WARNING, kind.c_str()); + continue; + } + + m_scriptedInput.events.emplace_back(entry); + if (captureFrame) + m_scriptedInput.captureFrames.emplace_back(frame); + } + else if (type == "imguizmo") + { + ScriptedInputEvent entry; + entry.frame = frame; + entry.type = ScriptedInputEvent::Type::Imguizmo; + + if (ev.contains("delta_trs")) + { + const auto arr = ev["delta_trs"].get>(); + float m16[16]; + for (size_t i = 0u; i < 16u; ++i) + m16[i] = arr[i]; + entry.imguizmo = *reinterpret_cast(m16); + } + else + { + const auto t = ev.contains("translation") ? ev["translation"].get>() : std::array{0.f, 0.f, 0.f}; + const auto r = ev.contains("rotation_deg") ? ev["rotation_deg"].get>() : std::array{0.f, 0.f, 0.f}; + const auto s = ev.contains("scale") ? ev["scale"].get>() : std::array{1.f, 1.f, 1.f}; + + float m16[16]; + float tr[3] = { t[0], t[1], t[2] }; + float rot[3] = { r[0], r[1], r[2] }; + float sc[3] = { s[0], s[1], s[2] }; + + ImGuizmo::RecomposeMatrixFromComponents(tr, rot, sc, m16); + entry.imguizmo = *reinterpret_cast(m16); + } + + m_scriptedInput.events.emplace_back(entry); + if (captureFrame) + m_scriptedInput.captureFrames.emplace_back(frame); + } + else if (type == "action") + { + if (!ev.contains("action")) + { + m_logger->log("Scripted action event missing \"action\".", ILogger::ELL_WARNING); + continue; + } + + const auto actionStr = ev["action"].get(); + ScriptedInputEvent entry; + entry.frame = frame; + entry.type = ScriptedInputEvent::Type::Action; + + auto getValueInt = [&]() -> int32_t + { + if (ev.contains("value")) + return ev["value"].get(); + if (ev.contains("index")) + return ev["index"].get(); + return 0; + }; + + if (actionStr == "set_active_render_window") + { + entry.action.kind = ScriptedInputEvent::ActionData::Kind::SetActiveRenderWindow; + entry.action.value = getValueInt(); + } + else if (actionStr == "set_active_planar") + { + entry.action.kind = ScriptedInputEvent::ActionData::Kind::SetActivePlanar; + entry.action.value = getValueInt(); + } + else if (actionStr == "set_projection_type") + { + entry.action.kind = ScriptedInputEvent::ActionData::Kind::SetProjectionType; + if (ev.contains("value") && ev["value"].is_string()) + { + const auto valueStr = ev["value"].get(); + if (valueStr == "perspective") + entry.action.value = static_cast(IPlanarProjection::CProjection::Perspective); + else if (valueStr == "orthographic") + entry.action.value = static_cast(IPlanarProjection::CProjection::Orthographic); + else + { + m_logger->log("Scripted action projection type has invalid value \"%s\".", ILogger::ELL_WARNING, valueStr.c_str()); + continue; + } + } + else + { + entry.action.value = getValueInt(); + } + } + else if (actionStr == "set_projection_index") + { + entry.action.kind = ScriptedInputEvent::ActionData::Kind::SetProjectionIndex; + entry.action.value = getValueInt(); + } + else if (actionStr == "set_use_window") + { + entry.action.kind = ScriptedInputEvent::ActionData::Kind::SetUseWindow; + entry.action.value = ev.value("value", false) ? 1 : 0; + } + else if (actionStr == "set_left_handed") + { + entry.action.kind = ScriptedInputEvent::ActionData::Kind::SetLeftHanded; + entry.action.value = ev.value("value", false) ? 1 : 0; + } + else + { + m_logger->log("Scripted action event has invalid action \"%s\".", ILogger::ELL_WARNING, actionStr.c_str()); + continue; + } + + m_scriptedInput.events.emplace_back(entry); + if (captureFrame) + m_scriptedInput.captureFrames.emplace_back(frame); + } + else + { + m_logger->log("Scripted input event has invalid type \"%s\".", ILogger::ELL_WARNING, type.c_str()); + } + } + + if (script.contains("checks")) + { + for (const auto& chk : script["checks"]) + { + if (!chk.contains("frame") || !chk.contains("kind")) + { + m_logger->log("Scripted check missing \"frame\" or \"kind\".", ILogger::ELL_WARNING); + continue; + } + + const auto frame = chk["frame"].get(); + const auto kind = chk["kind"].get(); + + ScriptedInputCheck entry; + entry.frame = frame; + + if (kind == "baseline") + { + entry.kind = ScriptedInputCheck::Kind::Baseline; + } + else if (kind == "imguizmo_virtual") + { + entry.kind = ScriptedInputCheck::Kind::ImguizmoVirtual; + entry.tolerance = chk.value("tolerance", entry.tolerance); + + if (!chk.contains("events")) + { + m_logger->log("Imguizmo virtual check missing \"events\".", ILogger::ELL_WARNING); + continue; + } + + for (const auto& ev : chk["events"]) + { + if (!ev.contains("type") || !ev.contains("magnitude")) + { + m_logger->log("Imguizmo virtual check event missing \"type\" or \"magnitude\".", ILogger::ELL_WARNING); + continue; + } + + const auto typeStr = ev["type"].get(); + const auto type = CVirtualGimbalEvent::stringToVirtualEvent(typeStr); + if (type == CVirtualGimbalEvent::None) + { + m_logger->log("Imguizmo virtual check event has invalid type \"%s\".", ILogger::ELL_WARNING, typeStr.c_str()); + continue; + } + + ScriptedInputCheck::ExpectedVirtualEvent expected; + expected.type = type; + expected.magnitude = ev["magnitude"].get(); + entry.expectedVirtualEvents.emplace_back(expected); + } + } + else if (kind == "gimbal_near") + { + entry.kind = ScriptedInputCheck::Kind::GimbalNear; + entry.posTolerance = chk.value("pos_tolerance", entry.posTolerance); + entry.eulerToleranceDeg = chk.value("euler_tolerance_deg", entry.eulerToleranceDeg); + + if (chk.contains("position")) + { + const auto pos = chk["position"].get>(); + entry.expectedPos = float32_t3(pos[0], pos[1], pos[2]); + entry.hasExpectedPos = true; + } + if (chk.contains("euler_deg")) + { + const auto euler = chk["euler_deg"].get>(); + entry.expectedEulerDeg = float32_t3(euler[0], euler[1], euler[2]); + entry.hasExpectedEuler = true; + } + } + else if (kind == "gimbal_delta") + { + entry.kind = ScriptedInputCheck::Kind::GimbalDelta; + entry.posTolerance = chk.value("pos_tolerance", entry.posTolerance); + entry.eulerToleranceDeg = chk.value("euler_tolerance_deg", entry.eulerToleranceDeg); + } + else if (kind == "gimbal_step") + { + entry.kind = ScriptedInputCheck::Kind::GimbalStep; + + if (chk.contains("min_pos_delta")) + { + entry.minPosDelta = chk["min_pos_delta"].get(); + entry.hasPosDeltaConstraint = true; + } + if (chk.contains("max_pos_delta")) + { + entry.posTolerance = chk["max_pos_delta"].get(); + entry.hasPosDeltaConstraint = true; + } + else if (chk.contains("pos_tolerance")) + { + entry.posTolerance = chk["pos_tolerance"].get(); + entry.hasPosDeltaConstraint = true; + } + + if (chk.contains("min_euler_delta_deg")) + { + entry.minEulerDeltaDeg = chk["min_euler_delta_deg"].get(); + entry.hasEulerDeltaConstraint = true; + } + if (chk.contains("max_euler_delta_deg")) + { + entry.eulerToleranceDeg = chk["max_euler_delta_deg"].get(); + entry.hasEulerDeltaConstraint = true; + } + else if (chk.contains("euler_tolerance_deg")) + { + entry.eulerToleranceDeg = chk["euler_tolerance_deg"].get(); + entry.hasEulerDeltaConstraint = true; + } + + if (!entry.hasPosDeltaConstraint && !entry.hasEulerDeltaConstraint) + { + m_logger->log("gimbal_step check requires at least one delta constraint.", ILogger::ELL_WARNING); + continue; + } + } + else + { + m_logger->log("Scripted check has invalid kind \"%s\".", ILogger::ELL_WARNING, kind.c_str()); + continue; + } + + m_scriptedInput.checks.emplace_back(entry); + } + } + + std::sort(m_scriptedInput.events.begin(), m_scriptedInput.events.end(), + [](const ScriptedInputEvent& a, const ScriptedInputEvent& b) { return a.frame < b.frame; }); + std::sort(m_scriptedInput.checks.begin(), m_scriptedInput.checks.end(), + [](const ScriptedInputCheck& a, const ScriptedInputCheck& b) { return a.frame < b.frame; }); + if (!m_scriptedInput.captureFrames.empty()) + { + std::sort(m_scriptedInput.captureFrames.begin(), m_scriptedInput.captureFrames.end()); + m_scriptedInput.captureFrames.erase(std::unique(m_scriptedInput.captureFrames.begin(), m_scriptedInput.captureFrames.end()), m_scriptedInput.captureFrames.end()); + } + }; + + if (program.is_used("--script")) + { + system::path scriptPath = program.get("--script"); + if (scriptPath.is_relative()) + scriptPath = localInputCWD / scriptPath; + json scriptJson; + if (!loadScriptJson(scriptPath.string(), scriptJson)) + return false; + parseScriptedInput(scriptJson); + } + else if (j.contains("scripted_input")) + { + parseScriptedInput(j["scripted_input"]); + } + + std::vector> cameras; + for (const auto& jCamera : j["cameras"]) + { + if (jCamera.contains("type")) + { + if (!jCamera.contains("position")) + { + logFail("Expected \"position\" keyword for camera definition!"); + return false; + } + + const bool withOrientation = jCamera.contains("orientation"); + + auto position = [&]() + { + auto jret = jCamera["position"].get>(); + return float32_t3(jret[0], jret[1], jret[2]); + }(); + + auto getOrientation = [&]() + { + auto jret = jCamera["orientation"].get>(); + + // order important for glm::quat, + // the ctor is GLM_FUNC_QUALIFIER GLM_CONSTEXPR qua::qua(T _w, T _x, T _y, T _z) + // but memory layout (and json) is x,y,z,w + return glm::quat(jret[3], jret[0], jret[1], jret[2]); + }; + + auto getTarget = [&]() + { + auto jret = jCamera["target"].get>(); + return float32_t3(jret[0], jret[1], jret[2]); + }; + + constexpr float DefaultMoveScale = 0.01f; + constexpr float DefaultRotateScale = 0.003f; + constexpr float OrbitMoveScale = 0.5f; + + if (jCamera["type"] == "FPS") + { + if (!withOrientation) + { + logFail("Expected \"orientation\" keyword for FPS camera definition!"); + return false; + } + + auto camera = make_smart_refctd_ptr(position, getOrientation()); + camera->setMoveSpeedScale(DefaultMoveScale); + camera->setRotationSpeedScale(DefaultRotateScale); + cameras.emplace_back(std::move(camera)); + } + else if (jCamera["type"] == "Free") + { + if (!withOrientation) + { + logFail("Expected \"orientation\" keyword for Free camera definition!"); + return false; + } + + auto camera = make_smart_refctd_ptr(position, getOrientation()); + camera->setMoveSpeedScale(DefaultMoveScale); + camera->setRotationSpeedScale(DefaultRotateScale); + cameras.emplace_back(std::move(camera)); + } + else if (jCamera["type"] == "Orbit") + { + auto camera = make_smart_refctd_ptr(position, getTarget()); + camera->setMoveSpeedScale(OrbitMoveScale); + camera->setRotationSpeedScale(DefaultRotateScale); + cameras.emplace_back(std::move(camera)); + } + else if (jCamera["type"] == "Arcball") + { + auto camera = make_smart_refctd_ptr(position, getTarget()); + camera->setMoveSpeedScale(OrbitMoveScale); + camera->setRotationSpeedScale(DefaultRotateScale); + cameras.emplace_back(std::move(camera)); + } + else if (jCamera["type"] == "Turntable") + { + auto camera = make_smart_refctd_ptr(position, getTarget()); + camera->setMoveSpeedScale(OrbitMoveScale); + camera->setRotationSpeedScale(DefaultRotateScale); + cameras.emplace_back(std::move(camera)); + } + else if (jCamera["type"] == "TopDown") + { + auto camera = make_smart_refctd_ptr(position, getTarget()); + camera->setMoveSpeedScale(OrbitMoveScale); + camera->setRotationSpeedScale(DefaultRotateScale); + cameras.emplace_back(std::move(camera)); + } + else if (jCamera["type"] == "Isometric") + { + auto camera = make_smart_refctd_ptr(position, getTarget()); + camera->setMoveSpeedScale(OrbitMoveScale); + camera->setRotationSpeedScale(DefaultRotateScale); + cameras.emplace_back(std::move(camera)); + } + else if (jCamera["type"] == "Chase") + { + auto camera = make_smart_refctd_ptr(position, getTarget()); + camera->setMoveSpeedScale(OrbitMoveScale); + camera->setRotationSpeedScale(DefaultRotateScale); + cameras.emplace_back(std::move(camera)); + } + else if (jCamera["type"] == "Dolly") + { + auto camera = make_smart_refctd_ptr(position, getTarget()); + camera->setMoveSpeedScale(OrbitMoveScale); + camera->setRotationSpeedScale(DefaultRotateScale); + cameras.emplace_back(std::move(camera)); + } + else if (jCamera["type"] == "DollyZoom") + { + float baseFov = 40.0f; + if (jCamera.contains("baseFov")) + baseFov = jCamera["baseFov"].get(); + + auto camera = make_smart_refctd_ptr(position, getTarget(), baseFov); + camera->setMoveSpeedScale(OrbitMoveScale); + camera->setRotationSpeedScale(DefaultRotateScale); + cameras.emplace_back(std::move(camera)); + } + else if (jCamera["type"] == "Path") + { + auto camera = make_smart_refctd_ptr(position, getTarget()); + camera->setMoveSpeedScale(OrbitMoveScale); + camera->setRotationSpeedScale(DefaultRotateScale); + cameras.emplace_back(std::move(camera)); + } + else + { + logFail("Unsupported camera type!"); + return false; + } + } + else + { + logFail("Expected \"type\" keyword for camera definition!"); + return false; + } + } + + std::vector projections; + for (const auto& jProjection : j["projections"]) + { + if (jProjection.contains("type")) + { + float zNear, zFar; + + if (!jProjection.contains("zNear")) + { + logFail("Expected \"zNear\" keyword for planar projection definition!"); + return false; + } + + if (!jProjection.contains("zFar")) + { + logFail("Expected \"zFar\" keyword for planar projection definition!"); + return false; + } + + zNear = jProjection["zNear"].get(); + zFar = jProjection["zFar"].get(); + + if (jProjection["type"] == "perspective") + { + if (!jProjection.contains("fov")) + { + logFail("Expected \"fov\" keyword for planar perspective projection definition!"); + return false; + } + + float fov = jProjection["fov"].get(); + projections.emplace_back(IPlanarProjection::CProjection::create(zNear, zFar, fov)); + } + else if (jProjection["type"] == "orthographic") + { + if (!jProjection.contains("orthoWidth")) + { + logFail("Expected \"orthoWidth\" keyword for planar orthographic projection definition!"); + return false; + } + + float orthoWidth = jProjection["orthoWidth"].get(); + projections.emplace_back(IPlanarProjection::CProjection::create(zNear, zFar, orthoWidth)); + } + else + { + logFail("Unsupported projection!"); + return false; + } + } + } + + struct + { + std::vector keyboard; + std::vector mouse; + } controllers; + + if (j.contains("controllers")) + { + const auto& jControllers = j["controllers"]; + + if (jControllers.contains("keyboard")) + { + for (const auto& jKeyboard : jControllers["keyboard"]) + { + if (jKeyboard.contains("mappings")) + { + auto& controller = controllers.keyboard.emplace_back(); + for (const auto& [key, value] : jKeyboard["mappings"].items()) + { + const auto nativeCode = stringToKeyCode(key.c_str()); + + if (nativeCode == EKC_NONE) + { + logFail("Invalid native key \"%s\" code mapping for keyboard controller", key.c_str()); + return false; + } + + controller[nativeCode] = CVirtualGimbalEvent::stringToVirtualEvent(value.get()); + } + } + else + { + logFail("Expected \"mappings\" keyword for keyboard controller definition!"); + return false; + } + } + } + else + { + logFail("Expected \"keyboard\" keyword in controllers definition!"); + return false; + } + + if (jControllers.contains("mouse")) + { + for (const auto& jMouse : jControllers["mouse"]) + { + if (jMouse.contains("mappings")) + { + auto& controller = controllers.mouse.emplace_back(); + for (const auto& [key, value] : jMouse["mappings"].items()) + { + const auto nativeCode = stringToMouseCode(key.c_str()); + + if (nativeCode == EMC_NONE) + { + logFail("Invalid native key \"%s\" code mapping for mouse controller", key.c_str()); + return false; + } + + controller[nativeCode] = CVirtualGimbalEvent::stringToVirtualEvent(value.get()); + } + } + else + { + logFail("Expected \"mappings\" keyword for mouse controller definition!"); + return false; + } + } + } + else + { + logFail("Expected \"mouse\" keyword in controllers definition"); + return false; + } + } + else + { + logFail("Expected \"controllers\" keyword in controllers JSON"); + return false; + } + + if (j.contains("viewports") && j.contains("planars")) + { + for (const auto& jPlanar : j["planars"]) + { + if (!jPlanar.contains("camera")) + { + logFail("Expected \"camera\" value in planar object"); + return false; + } + + if (!jPlanar.contains("viewports")) + { + logFail("Expected \"viewports\" list in planar object"); + return false; + } + + const auto cameraIx = jPlanar["camera"].get(); + auto boundViewports = jPlanar["viewports"].get>(); + + auto& planar = m_planarProjections.emplace_back() = planar_projection_t::create(smart_refctd_ptr(cameras[cameraIx])); + for (const auto viewportIx : boundViewports) + { + auto& viewport = j["viewports"][viewportIx]; + if (!viewport.contains("projection") || !viewport.contains("controllers")) + { + logFail("\"projection\" or \"controllers\" missing in viewport object index %d", viewportIx); + return false; + } + + const auto projectionIx = viewport["projection"].get(); + auto& projection = planar->getPlanarProjections().emplace_back(projections[projectionIx]); + + const bool hasKeyboardBound = viewport["controllers"].contains("keyboard"); + const bool hasMouseBound = viewport["controllers"].contains("mouse"); + + if (hasKeyboardBound) + { + auto keyboardControllerIx = viewport["controllers"]["keyboard"].get(); + projection.updateKeyboardMapping([&](auto& map) { map = controllers.keyboard[keyboardControllerIx]; }); + } + else + projection.updateKeyboardMapping([&](auto& map) { map = {}; }); // clean the map if not bound + + if (hasMouseBound) + { + auto mouseControllerIx = viewport["controllers"]["mouse"].get(); + projection.updateMouseMapping([&](auto& map) { map = controllers.mouse[mouseControllerIx]; }); + } + else + projection.updateMouseMapping([&](auto& map) { map = {}; }); // clean the map if not bound + } + + { + auto* camera = planar->getCamera(); + { + camera->updateKeyboardMapping([&](auto& map) { map = camera->getKeyboardMappingPreset(); }); + camera->updateMouseMapping([&](auto& map) { map = camera->getMouseMappingPreset(); }); + camera->updateImguizmoMapping([&](auto& map) { map = camera->getImguizmoMappingPreset(); }); + } + } + } + } + else + { + logFail("Expected \"viewports\" and \"planars\" lists in JSON"); + return false; + } + + if (m_planarProjections.empty()) + { + logFail("Expected at least 1 planar"); + return false; + } + + // init render window planar references - we make all render windows start with focus on first + // planar but in a way that first window has the planar's perspective preset bound & second orthographic + for (uint32_t i = 0u; i < windowBindings.size(); ++i) + { + auto& binding = windowBindings[i]; + + auto& planar = m_planarProjections[binding.activePlanarIx = 0]; + binding.pickDefaultProjections(planar->getPlanarProjections()); + + if (i) + binding.boundProjectionIx = binding.lastBoundOrthoPresetProjectionIx.value(); + else + binding.boundProjectionIx = binding.lastBoundPerspectivePresetProjectionIx.value(); + } + } + + // Create asset manager + m_assetManager = make_smart_refctd_ptr(smart_refctd_ptr(m_system)); + + // First create the resources that don't depend on a swapchain + m_semaphore = m_device->createSemaphore(m_realFrameIx); + if (!m_semaphore) + return logFail("Failed to Create a Semaphore!"); + + // The nice thing about having a triple buffer is that you don't need to do acrobatics to account for the formats available to the surface. + // You can transcode to the swapchain's format while copying, and I actually recommend to do surface rotation, tonemapping and OETF application there. + const auto format = asset::EF_R8G8B8A8_SRGB; + // Could be more clever and use the copy Triple Buffer to Swapchain as an opportunity to do a MSAA resolve or something + const auto samples = IGPUImage::ESCF_1_BIT; + + // Create the renderpass + { + 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, // because we clear we don't care about contents when we grab the triple buffer img again + /*.finalLayout = */IGPUImage::LAYOUT::TRANSFER_SRC_OPTIMAL // put it already in the correct layout for the blit operation + }}, + IGPURenderpass::SCreationParams::ColorAttachmentsEnd + }; + IGPURenderpass::SCreationParams::SSubpassDescription subpasses[] = { + {}, + IGPURenderpass::SCreationParams::SubpassesEnd + }; + subpasses[0].colorAttachments[0] = { .render = {.attachmentIndex = 0,.layout = IGPUImage::LAYOUT::ATTACHMENT_OPTIMAL} }; + // We actually need external dependencies to ensure ordering of the Implicit Layout Transitions relative to the semaphore signals + IGPURenderpass::SCreationParams::SSubpassDependency dependencies[] = { + // wipe-transition to ATTACHMENT_OPTIMAL + { + .srcSubpass = IGPURenderpass::SCreationParams::SSubpassDependency::External, + .dstSubpass = 0, + .memoryBarrier = { + // we can have NONE as Sources because the semaphore wait is ALL_COMMANDS + // https://github.com/KhronosGroup/Vulkan-Docs/issues/2319 + .dstStageMask = asset::PIPELINE_STAGE_FLAGS::COLOR_ATTACHMENT_OUTPUT_BIT, + .dstAccessMask = asset::ACCESS_FLAGS::COLOR_ATTACHMENT_WRITE_BIT + } + // leave view offsets and flags default + }, + // ATTACHMENT_OPTIMAL to PRESENT_SRC + { + .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 + // we can have NONE as the Destinations because the semaphore signal is ALL_COMMANDS + // https://github.com/KhronosGroup/Vulkan-Docs/issues/2319 + } + // leave view offsets and flags default + }, + IGPURenderpass::SCreationParams::DependenciesEnd + }; + + IGPURenderpass::SCreationParams params = {}; + params.colorAttachments = colorAttachments; + params.subpasses = subpasses; + params.dependencies = dependencies; + m_renderpass = m_device->createRenderpass(params); + if (!m_renderpass) + return logFail("Failed to Create a Renderpass!"); + } + + // We just live life in easy mode and have the Swapchain Creation Parameters get deduced from the surface. + // We don't need any control over the format of the swapchain because we'll be only using Renderpasses this time! + // TODO: improve the queue allocation/choice and allocate a dedicated presentation queue to improve responsiveness and race to present. + ISwapchain::SSharedCreationParams sharedParams = {}; + sharedParams.imageUsage |= IGPUImage::EUF_TRANSFER_SRC_BIT; + auto swapchainResources = std::make_unique(); + if (!m_surface || !m_surface->init(m_surface->pickQueue(m_device.get()), std::move(swapchainResources), sharedParams)) + return logFail("Failed to Create a Swapchain!"); + + // Normally you'd want to recreate these images whenever the swapchain is resized in some increment, like 64 pixels or something. + // But I'm super lazy here and will just create "worst case sized images" and waste all the VRAM I can get. + const auto dpyInfo = m_winMgr->getPrimaryDisplayInfo(); + for (auto i = 0; i < MaxFramesInFlight; i++) + { + auto& image = m_tripleBuffers[i]; + { + IGPUImage::SCreationParams params = {}; + params = asset::IImage::SCreationParams{ + .type = IGPUImage::ET_2D, + .samples = samples, + .format = format, + .extent = {dpyInfo.resX,dpyInfo.resY,1}, + .mipLevels = 1, + .arrayLayers = 1, + .flags = IGPUImage::ECF_NONE, + // in this example I'll be using a renderpass to clear the image, and then a blit to copy it to the swapchain + .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!"); + + // use dedicated allocations, we have plenty of allocations left, even on Win32 + 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()); + + // create framebuffers for the images + { + auto imageView = m_device->createImageView({ + .flags = IGPUImageView::ECF_NONE, + // give it a Transfer SRC usage flag so we can transition to the Tranfer SRC layout with End Renderpass + .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); + } + } + + // This time we'll create all CommandBuffers from one CommandPool, to keep life simple. However the Pool must support individually resettable CommandBuffers + // because they cannot be pre-recorded because the fraembuffers/swapchain images they use will change when a swapchain recreates. + 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!"); + + // UI + { + { + constexpr std::array ImGuiStreamingBufferSizes = { + 32ull * 1024ull * 1024ull, + 16ull * 1024ull * 1024ull, + 8ull * 1024ull * 1024ull, + 4ull * 1024ull * 1024ull, + 2ull * 1024ull * 1024ull + }; + auto createImGuiStreamingBuffer = [&](size_t size) -> smart_refctd_ptr + { + constexpr uint32_t minStreamingBufferAllocationSize = 128u; + constexpr uint32_t maxStreamingBufferAllocationAlignment = 4096u; + + auto getRequiredAccessFlags = [&](const bitflag& properties) + { + bitflag flags(IDeviceMemoryAllocation::EMCAF_NO_MAPPING_ACCESS); + + if (properties.hasFlags(IDeviceMemoryAllocation::EMPF_HOST_READABLE_BIT)) + flags |= IDeviceMemoryAllocation::EMCAF_READ; + if (properties.hasFlags(IDeviceMemoryAllocation::EMPF_HOST_WRITABLE_BIT)) + flags |= IDeviceMemoryAllocation::EMCAF_WRITE; + + return flags; + }; + + IGPUBuffer::SCreationParams mdiCreationParams = {}; + mdiCreationParams.usage = nbl::ext::imgui::UI::SCachedCreationParams::RequiredUsageFlags; + mdiCreationParams.size = size; + + auto buffer = m_utils->getLogicalDevice()->createBuffer(std::move(mdiCreationParams)); + if (!buffer) + { + m_logger->log("Failed to create ImGui streaming buffer object for size=%zu.", ILogger::ELL_WARNING, size); + return nullptr; + } + + buffer->setObjectDebugName("ImGui MDI Upstream Buffer"); + + auto memoryReqs = buffer->getMemoryReqs(); + const auto upStreamingBits = m_utils->getLogicalDevice()->getPhysicalDevice()->getUpStreamingMemoryTypeBits(); + const auto reqMemoryTypeBits = memoryReqs.memoryTypeBits; + memoryReqs.memoryTypeBits &= upStreamingBits; + if (!memoryReqs.memoryTypeBits) + { + m_logger->log("No compatible up-streaming memory type for ImGui buffer size=%zu reqBits=0x%08x upBits=0x%08x.", ILogger::ELL_WARNING, size, reqMemoryTypeBits, upStreamingBits); + return nullptr; + } + + auto allocation = m_utils->getLogicalDevice()->allocate(memoryReqs, buffer.get(), nbl::ext::imgui::UI::SCachedCreationParams::RequiredAllocateFlags); + if (!allocation.isValid()) + { + m_logger->log("Failed to allocate ImGui streaming buffer memory for size=%zu reqBits=0x%08x upBits=0x%08x filteredBits=0x%08x sizeReq=%llu.", ILogger::ELL_WARNING, size, reqMemoryTypeBits, upStreamingBits, memoryReqs.memoryTypeBits, memoryReqs.size); + return nullptr; + } + + auto memory = allocation.memory; + + if (!memory->map({ 0ull, memoryReqs.size }, getRequiredAccessFlags(memory->getMemoryPropertyFlags()))) + { + m_logger->log("Could not map ImGui streaming buffer memory for size=%zu.", ILogger::ELL_WARNING, size); + return nullptr; + } + + return make_smart_refctd_ptr( + SBufferRange{0ull, mdiCreationParams.size, std::move(buffer)}, + maxStreamingBufferAllocationAlignment, + minStreamingBufferAllocationSize); + }; + + smart_refctd_ptr imguiStreamingBuffer = nullptr; + for (const auto candidateSize : ImGuiStreamingBufferSizes) + { + imguiStreamingBuffer = createImGuiStreamingBuffer(candidateSize); + if (imguiStreamingBuffer) + break; + } + if (!imguiStreamingBuffer) + return logFail("Failed to create ImGui streaming buffer."); + + nbl::ext::imgui::UI::SCreationParameters params; + params.resources.texturesInfo = { .setIx = 0u, .bindingIx = 0u }; + params.resources.samplersInfo = { .setIx = 0u, .bindingIx = 1u }; + params.assetManager = m_assetManager; + 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.streamingBuffer = std::move(imguiStreamingBuffer); + params.subpassIx = 0u; + params.transfer = getTransferUpQueue(); + params.utilities = m_utils; + + auto loadPrecompiledShader = [&](const std::string_view key) -> smart_refctd_ptr + { + IAssetLoader::SAssetLoadParams loadParams = {}; + loadParams.logger = m_logger.get(); + loadParams.workingDirectory = "app_resources"; + auto bundle = m_assetManager->getAsset(key.data(), loadParams); + const auto& contents = bundle.getContents(); + if (contents.empty()) + return nullptr; + return IAsset::castDown(contents[0]); + }; + + const auto vertexKey = nbl::this_example::builtin::build::get_spirv_key<"imgui_vertex">(m_device.get()); + const auto fragmentKey = nbl::this_example::builtin::build::get_spirv_key<"imgui_fragment">(m_device.get()); + auto vertexShader = loadPrecompiledShader(vertexKey.data()); + auto fragmentShader = loadPrecompiledShader(fragmentKey.data()); + if (!vertexShader || !fragmentShader) + return logFail("Failed to load precompiled ImGui shaders."); + + params.spirv = nbl::ext::imgui::UI::SCreationParameters::PrecompiledShaders{ + .vertex = std::move(vertexShader), + .fragment = std::move(fragmentShader) + }; + + m_ui.manager = nbl::ext::imgui::UI::create(std::move(params)); + } + + if (!m_ui.manager) + return false; + + // note that we use default layout provided by our extension, but you are free to create your own by filling nbl::ext::imgui::UI::S_CREATION_PARAMETERS::resources + const auto* descriptorSetLayout = m_ui.manager->getPipeline()->getLayout()->getDescriptorSetLayout(0u); + + IDescriptorPool::SCreateInfo descriptorPoolInfo = {}; + descriptorPoolInfo.maxDescriptorCount[static_cast(asset::IDescriptor::E_TYPE::ET_SAMPLER)] = (uint32_t)nbl::ext::imgui::UI::DefaultSamplerIx::COUNT; + descriptorPoolInfo.maxDescriptorCount[static_cast(asset::IDescriptor::E_TYPE::ET_SAMPLED_IMAGE)] = TotalUISampleTexturesAmount; + descriptorPoolInfo.maxSets = 1u; + descriptorPoolInfo.flags = IDescriptorPool::E_CREATE_FLAGS::ECF_UPDATE_AFTER_BIND_BIT; + + m_descriptorSetPool = m_device->createDescriptorPool(std::move(descriptorPoolInfo)); + assert(m_descriptorSetPool); + + m_descriptorSetPool->createDescriptorSets(1u, &descriptorSetLayout, &m_ui.descriptorSet); + assert(m_ui.descriptorSet); + + m_ui.manager->registerListener([this]() -> void { imguiListen(); }); + { + const auto ds = float32_t2{ m_window->getWidth(), m_window->getHeight() }; + + wInit.trsEditor.iPos = iPaddingOffset; + wInit.trsEditor.iSize = { 0.0f, ds.y - wInit.trsEditor.iPos.y * 2 }; + + const float panelWidth = std::clamp(ds.x * 0.33f, 380.0f, ds.x * 0.48f); + wInit.planars.iSize = { panelWidth, ds.y - iPaddingOffset.y * 2 }; + wInit.planars.iPos = { ds.x - wInit.planars.iSize.x - iPaddingOffset.x, 0 + iPaddingOffset.y }; + + { + const float renderPaddingX = 0.0f; + const float renderPaddingY = 0.0f; + const float splitGap = 4.0f; + float leftX = renderPaddingX; + float eachXSize = std::max(0.0f, ds.x - 2.0f * renderPaddingX); + float eachYSize = (ds.y - 2.0f * renderPaddingY - (wInit.renderWindows.size() - 1) * splitGap) / wInit.renderWindows.size(); + + for (size_t i = 0; i < wInit.renderWindows.size(); ++i) + { + auto& rw = wInit.renderWindows[i]; + rw.iPos = { leftX, renderPaddingY + i * (eachYSize + splitGap) }; + rw.iSize = { eachXSize, eachYSize }; + } + } + } + } + + // Geometry Creator Render Scene FBOs + { + const uint32_t addtionalBufferOwnershipFamilies[] = { getGraphicsQueue()->getFamilyIndex() }; + m_scene = CGeometryCreatorScene::create( + { + .transferQueue = getTransferUpQueue(), + .utilities = m_utils.get(), + .logger = m_logger.get(), + .addtionalBufferOwnershipFamilies = addtionalBufferOwnershipFamilies + }, + CSimpleDebugRenderer::DefaultPolygonGeometryPatch + ); + + if (!m_scene) + return logFail("Could not create geometry creator scene!"); + + { + 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 + }}, + 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[] = { + { + .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 = {}; + m_sceneRenderpass = m_device->createRenderpass(std::move(params)); + if (!m_sceneRenderpass) + return logFail("Failed to create Scene Renderpass!"); + } + + const auto& geometries = m_scene->getInitParams().geometries; + if (geometries.empty()) + return logFail("No geometries found for scene!"); + m_renderer = CSimpleDebugRenderer::create(m_assetManager.get(), m_sceneRenderpass.get(), 0, { &geometries.front().get(), geometries.size() }); + if (!m_renderer) + return logFail("Failed to create debug renderer!"); + + { + 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++; + } + } + m_renderer->m_instances.resize(1); + + const auto dpyInfo = m_winMgr->getPrimaryDisplayInfo(); + for (uint32_t i = 0u; i < windowBindings.size(); ++i) + { + auto& binding = windowBindings[i]; + binding.sceneColorView = createAttachmentView(m_device.get(), finalSceneRenderFormat, dpyInfo.resX, dpyInfo.resY, "UI Scene Color Attachment"); + binding.sceneDepthView = createAttachmentView(m_device.get(), sceneRenderDepthFormat, dpyInfo.resX, dpyInfo.resY, "UI Scene Depth Attachment"); + binding.sceneFramebuffer = createSceneFramebuffer(m_device.get(), m_sceneRenderpass.get(), binding.sceneColorView.get(), binding.sceneDepthView.get()); + if (!binding.sceneFramebuffer) + return logFail("Could not create geometry creator scene[%d]!", i); + } + } + + 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/include/app/impl/AppTransformEditor.inl b/61_UI/include/app/impl/AppTransformEditor.inl new file mode 100644 index 000000000..4bbb4a21f --- /dev/null +++ b/61_UI/include/app/impl/AppTransformEditor.inl @@ -0,0 +1,221 @@ + 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; + + const size_t objectsCount = m_planarProjections.size() + 1u; + assert(objectsCount); + + std::vector sbels(objectsCount); + for (size_t i = 0; i < objectsCount; ++i) + sbels[i] = "Object " + std::to_string(i); + + std::vector labels(objectsCount); + for (size_t i = 0; i < objectsCount; ++i) + labels[i] = sbels[i].c_str(); + + int activeObject = boundCameraToManipulate ? static_cast(boundPlanarCameraIxToManipulate.value() + 1u) : 0; + if (ImGui::Combo("Active Object", &activeObject, labels.data(), static_cast(labels.size()))) + { + const auto newActiveObject = static_cast(activeObject); + + if (newActiveObject) // camera + { + boundPlanarCameraIxToManipulate = newActiveObject - 1u; + ICamera* const targetGimbalManipulationCamera = m_planarProjections[boundPlanarCameraIxToManipulate.value()]->getCamera(); + boundCameraToManipulate = smart_refctd_ptr(targetGimbalManipulationCamera); + } + else // gc model + { + boundPlanarCameraIxToManipulate = std::nullopt; + boundCameraToManipulate = nullptr; + } + } + + ImGuizmoModelM16InOut imguizmoModel; + + if (boundCameraToManipulate) + imguizmoModel.inTRS = getCastedMatrix(boundCameraToManipulate->getGimbal().template operator() < float64_t4x4 > ()); + else + imguizmoModel.inTRS = hlsl::transpose(getMatrix3x4As4x4(m_model)); + + imguizmoModel.outTRS = imguizmoModel.inTRS; + float* m16TRSmatrix = &imguizmoModel.outTRS[0][0]; + + std::string indent; + if (boundCameraToManipulate) + indent = boundCameraToManipulate->getIdentifier(); + else + indent = "Geometry Creator Object"; + + ImGui::Text("Identifier: \"%s\"", indent.c_str()); + { + if (ImGuizmo::IsUsingAny()) + ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), "Gizmo: In Use"); + else + ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.0f, 1.0f), "Gizmo: Idle"); + + if (ImGui::IsItemHovered()) + { + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.2f, 0.2f, 0.2f, 0.8f)); + ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.5f); + + ImVec2 mousePos = ImGui::GetMousePos(); + ImGui::SetNextWindowPos(ImVec2(mousePos.x + 10, mousePos.y + 10), ImGuiCond_Always); + + ImGui::Begin("HoverOverlay", nullptr, + ImGuiWindowFlags_NoDecoration | + ImGuiWindowFlags_AlwaysAutoResize | + ImGuiWindowFlags_NoSavedSettings); + + ImGui::Text("Right-click and drag on the gizmo to manipulate the object."); + + ImGui::End(); + + ImGui::PopStyleVar(); + ImGui::PopStyleColor(2); + } + } + + ImGui::Separator(); + + if (!boundCameraToManipulate) + { + const auto& names = m_scene->getInitParams().geometryNames; + if (!names.empty()) + { + if (gcIndex >= names.size()) + gcIndex = 0; + + if (ImGui::BeginCombo("Object Type", names[gcIndex].c_str())) + { + for (uint32_t i = 0u; i < names.size(); ++i) + { + const bool isSelected = (gcIndex == i); + if (ImGui::Selectable(names[i].c_str(), isSelected)) + gcIndex = static_cast(i); + + if (isSelected) + ImGui::SetItemDefaultFocus(); + } + ImGui::EndCombo(); + } + } + + } + + addMatrixTable("Model (TRS) Matrix", "ModelMatrixTable", 4, 4, m16TRSmatrix); + + 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; + + float32_t3 matrixTranslation, matrixRotation, matrixScale; + IGimbalController::input_imguizmo_event_t decomposed, recomposed; + imguizmoModel.outDeltaTRS = IGimbalController::input_imguizmo_event_t(1); + + ImGuizmo::DecomposeMatrixToComponents(m16TRSmatrix, &matrixTranslation[0], &matrixRotation[0], &matrixScale[0]); + decomposed = *reinterpret_cast(m16TRSmatrix); + { + ImGuiInputTextFlags flags = 0; + + ImGui::InputFloat3("Tr", &matrixTranslation[0], "%.3f", flags); + ImGui::InputFloat3("Rt", &matrixRotation[0], "%.3f", flags); + ImGui::InputFloat3("Sc", &matrixScale[0], "%.3f", flags); + } + ImGuizmo::RecomposeMatrixFromComponents(&matrixTranslation[0], &matrixRotation[0], &matrixScale[0], m16TRSmatrix); + recomposed = *reinterpret_cast(m16TRSmatrix); + + 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; + } + + ImGui::Checkbox(" ", &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; + } + + // generate virtual events given delta TRS matrix + if (boundCameraToManipulate) + { + const float pmSpeed = boundCameraToManipulate->getMoveSpeedScale(); + const float prSpeed = boundCameraToManipulate->getRotationSpeedScale(); + + boundCameraToManipulate->setMoveSpeedScale(1); + boundCameraToManipulate->setRotationSpeedScale(1); + + auto referenceFrame = getCastedMatrix(imguizmoModel.outTRS); + boundCameraToManipulate->manipulate({}, &referenceFrame); + + boundCameraToManipulate->setMoveSpeedScale(pmSpeed); + boundCameraToManipulate->setRotationSpeedScale(prSpeed); + + /* + { + static std::vector virtualEvents(0x45); + + if (not enableActiveCameraMovement) + { + uint32_t vCount = {}; + + boundCameraToManipulate->beginInputProcessing(m_nextPresentationTimestamp); + { + boundCameraToManipulate->process(nullptr, vCount); + + if (virtualEvents.size() < vCount) + virtualEvents.resize(vCount); + + IGimbalController::SUpdateParameters params; + params.imguizmoEvents = { { imguizmoModel.outDeltaTRS } }; + boundCameraToManipulate->process(virtualEvents.data(), vCount, params); + } + boundCameraToManipulate->endInputProcessing(); + + // I start to think controller should be able to set sensitivity to scale magnitudes of generated events + // in order for camera to not keep any magnitude scalars like move or rotation speed scales + + if (vCount) + { + const float pmSpeed = boundCameraToManipulate->getMoveSpeedScale(); + const float prSpeed = boundCameraToManipulate->getRotationSpeedScale(); + + boundCameraToManipulate->setMoveSpeedScale(1); + boundCameraToManipulate->setRotationSpeedScale(1); + + auto referenceFrame = getCastedMatrix(imguizmoModel.outTRS); + boundCameraToManipulate->manipulate({ virtualEvents.data(), vCount }, &referenceFrame); + + boundCameraToManipulate->setMoveSpeedScale(pmSpeed); + boundCameraToManipulate->setRotationSpeedScale(prSpeed); + } + } + } + */ + } + else + { + // for scene demo model full affine transformation without limits is assumed + m_model = float32_t3x4(hlsl::transpose(imguizmoModel.outTRS)); + } diff --git a/61_UI/include/app/impl/AppUpdate.inl b/61_UI/include/app/impl/AppUpdate.inl new file mode 100644 index 000000000..b1f798b0e --- /dev/null +++ b/61_UI/include/app/impl/AppUpdate.inl @@ -0,0 +1,657 @@ + m_inputSystem->getDefaultMouse(&mouse); + m_inputSystem->getDefaultKeyboard(&keyboard); + + auto updatePresentationTimestamp = [&]() + { + oracle.reportEndFrameRecord(); + const auto timestamp = oracle.getNextPresentationTimeStamp(); + oracle.reportBeginFrameRecord(); + + return timestamp; + }; + + m_nextPresentationTimestamp = updatePresentationTimestamp(); + if (m_haveLastPresentationTimestamp) + { + const auto delta = m_nextPresentationTimestamp - m_lastPresentationTimestamp; + if (delta.count() < 0) + m_frameDeltaSec = 0.0; + else + m_frameDeltaSec = static_cast(delta.count()) / 1000000.0; + } + m_lastPresentationTimestamp = m_nextPresentationTimestamp; + m_haveLastPresentationTimestamp = true; + + updatePlayback(m_frameDeltaSec); + const bool skipCameraInput = m_playback.playing && m_playback.overrideInput; + + struct + { + std::vector mouse {}; + std::vector keyboard {}; + } capturedEvents; + { + mouse.consumeEvents([&](const IMouseEventChannel::range_t& events) -> void + { + if (m_window->hasInputFocus()) + for (const auto& e : events) + capturedEvents.mouse.emplace_back(e); + }, m_logger.get()); + + keyboard.consumeEvents([&](const IKeyboardEventChannel::range_t& events) -> void + { + if (m_window->hasInputFocus()) + for (const auto& e : events) + capturedEvents.keyboard.emplace_back(e); + }, m_logger.get()); + } + + if (m_scriptedInput.enabled && m_scriptedInput.exclusive) + { + capturedEvents.mouse.clear(); + capturedEvents.keyboard.clear(); + } + + std::vector scriptedMouse; + std::vector scriptedKeyboard; + std::vector scriptedImguizmo; + std::vector scriptedActions; + const CVirtualGimbalEvent* scriptedImguizmoVirtual = nullptr; + uint32_t scriptedImguizmoVirtualCount = 0u; + + if (m_scriptedInput.enabled && m_scriptedInput.nextEventIndex < m_scriptedInput.events.size()) + { + const auto frame = m_realFrameIx; + while (m_scriptedInput.nextEventIndex < m_scriptedInput.events.size() && + m_scriptedInput.events[m_scriptedInput.nextEventIndex].frame == frame) + { + const auto& ev = m_scriptedInput.events[m_scriptedInput.nextEventIndex]; + + if (ev.type == ScriptedInputEvent::Type::Keyboard) + { + SKeyboardEvent e(m_nextPresentationTimestamp); + e.keyCode = ev.keyboard.key; + e.action = ev.keyboard.action; + e.window = m_window.get(); + scriptedKeyboard.emplace_back(e); + } + else if (ev.type == ScriptedInputEvent::Type::Mouse) + { + SMouseEvent e(m_nextPresentationTimestamp); + e.window = m_window.get(); + e.type = ev.mouse.type; + if (ev.mouse.type == ui::SMouseEvent::EET_CLICK) + { + e.clickEvent.mouseButton = ev.mouse.button; + e.clickEvent.action = ev.mouse.action; + e.clickEvent.clickPosX = ev.mouse.x; + e.clickEvent.clickPosY = ev.mouse.y; + } + else if (ev.mouse.type == ui::SMouseEvent::EET_SCROLL) + { + e.scrollEvent.verticalScroll = ev.mouse.v; + e.scrollEvent.horizontalScroll = ev.mouse.h; + } + else if (ev.mouse.type == ui::SMouseEvent::EET_MOVEMENT) + { + e.movementEvent.relativeMovementX = ev.mouse.dx; + e.movementEvent.relativeMovementY = ev.mouse.dy; + } + scriptedMouse.emplace_back(e); + } + else if (ev.type == ScriptedInputEvent::Type::Imguizmo) + { + scriptedImguizmo.emplace_back(ev.imguizmo); + } + else if (ev.type == ScriptedInputEvent::Type::Action) + { + scriptedActions.emplace_back(ev.action); + } + + ++m_scriptedInput.nextEventIndex; + } + } + + if (m_scriptedInput.enabled && scriptedActions.size()) + { + auto applyAction = [&](const ScriptedInputEvent::ActionData& action) -> void + { + switch (action.kind) + { + case ScriptedInputEvent::ActionData::Kind::SetActiveRenderWindow: + { + if (action.value < 0 || static_cast(action.value) >= windowBindings.size()) + { + m_logger->log("[script][warn] action set_active_render_window out of range: %d", ILogger::ELL_WARNING, action.value); + return; + } + activeRenderWindowIx = static_cast(action.value); + } break; + + case ScriptedInputEvent::ActionData::Kind::SetActivePlanar: + { + if (action.value < 0 || static_cast(action.value) >= m_planarProjections.size()) + { + m_logger->log("[script][warn] action set_active_planar out of range: %d", ILogger::ELL_WARNING, action.value); + return; + } + auto& binding = windowBindings[activeRenderWindowIx]; + binding.activePlanarIx = static_cast(action.value); + binding.pickDefaultProjections(m_planarProjections[binding.activePlanarIx]->getPlanarProjections()); + m_scriptedInput.visualActivePlanarValid = true; + m_scriptedInput.visualActivePlanarIx = binding.activePlanarIx; + m_scriptedInput.visualActivePlanarStartFrame = m_realFrameIx; + } break; + + case ScriptedInputEvent::ActionData::Kind::SetProjectionType: + { + auto& binding = windowBindings[activeRenderWindowIx]; + if (!binding.lastBoundPerspectivePresetProjectionIx.has_value() || !binding.lastBoundOrthoPresetProjectionIx.has_value()) + binding.pickDefaultProjections(m_planarProjections[binding.activePlanarIx]->getPlanarProjections()); + + const auto type = static_cast(action.value); + switch (type) + { + case IPlanarProjection::CProjection::Perspective: + binding.boundProjectionIx = binding.lastBoundPerspectivePresetProjectionIx.value(); + break; + case IPlanarProjection::CProjection::Orthographic: + binding.boundProjectionIx = binding.lastBoundOrthoPresetProjectionIx.value(); + break; + default: + m_logger->log("[script][warn] action set_projection_type invalid value: %d", ILogger::ELL_WARNING, action.value); + break; + } + } break; + + case ScriptedInputEvent::ActionData::Kind::SetProjectionIndex: + { + auto& binding = windowBindings[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; + } + const auto ix = static_cast(action.value); + const auto type = projections[ix].getParameters().m_type; + binding.boundProjectionIx = ix; + if (type == IPlanarProjection::CProjection::Perspective) + binding.lastBoundPerspectivePresetProjectionIx = ix; + else if (type == IPlanarProjection::CProjection::Orthographic) + binding.lastBoundOrthoPresetProjectionIx = ix; + } break; + + case ScriptedInputEvent::ActionData::Kind::SetUseWindow: + { + useWindow = action.value != 0; + } break; + + case ScriptedInputEvent::ActionData::Kind::SetLeftHanded: + { + auto& binding = windowBindings[activeRenderWindowIx]; + binding.leftHandedProjection = action.value != 0; + } break; + } + }; + + for (const auto& action : scriptedActions) + if (action.kind == ScriptedInputEvent::ActionData::Kind::SetActiveRenderWindow) + applyAction(action); + + for (const auto& action : scriptedActions) + if (action.kind != ScriptedInputEvent::ActionData::Kind::SetActiveRenderWindow) + applyAction(action); + + if (m_scriptedInput.log) + m_logger->log("[script] frame %llu actions=%zu", ILogger::ELL_INFO, static_cast(m_realFrameIx), scriptedActions.size()); + } + + if (m_scriptedInput.enabled && m_scriptedInput.visualDebug && !m_scriptedInput.visualActivePlanarValid) + { + if (activeRenderWindowIx < windowBindings.size()) + { + m_scriptedInput.visualActivePlanarValid = true; + m_scriptedInput.visualActivePlanarIx = windowBindings[activeRenderWindowIx].activePlanarIx; + m_scriptedInput.visualActivePlanarStartFrame = m_realFrameIx; + } + } + + if (!scriptedMouse.empty()) + capturedEvents.mouse.insert(capturedEvents.mouse.end(), scriptedMouse.begin(), scriptedMouse.end()); + if (!scriptedKeyboard.empty()) + capturedEvents.keyboard.insert(capturedEvents.keyboard.end(), scriptedKeyboard.begin(), scriptedKeyboard.end()); + + m_uiInputEventsThisFrame = static_cast(capturedEvents.mouse.size() + capturedEvents.keyboard.size()); + + auto cameraKeyboardEvents = capturedEvents.keyboard; + auto cameraMouseEvents = capturedEvents.mouse; + for (auto& ev : cameraMouseEvents) + { + if (ev.type == ui::SMouseEvent::EET_SCROLL) + { + ev.scrollEvent.verticalScroll *= m_cameraControls.mouseScrollScale; + ev.scrollEvent.horizontalScroll *= m_cameraControls.mouseScrollScale; + } + else if (ev.type == ui::SMouseEvent::EET_MOVEMENT) + { + ev.movementEvent.relativeMovementX *= m_cameraControls.mouseMoveScale; + ev.movementEvent.relativeMovementY *= m_cameraControls.mouseMoveScale; + } + } + + const auto cursorPosition = m_window->getCursorControl()->getPosition(); + + nbl::ext::imgui::UI::SUpdateParameters params = + { + .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() } + }; + + if (m_scriptedInput.log && (scriptedKeyboard.size() || scriptedMouse.size() || scriptedImguizmo.size())) + { + m_logger->log("[script] frame %llu input kb=%zu mouse=%zu imguizmo=%zu", ILogger::ELL_INFO, + static_cast(m_realFrameIx), + scriptedKeyboard.size(), + scriptedMouse.size(), + scriptedImguizmo.size()); + } + + if (enableActiveCameraMovement && !skipCameraInput) + { + auto& binding = windowBindings[activeRenderWindowIx]; + auto& planar = m_planarProjections[binding.activePlanarIx]; + auto* camera = planar->getCamera(); + + assert(binding.boundProjectionIx.has_value()); + auto& projection = planar->getPlanarProjections()[binding.boundProjectionIx.value()]; + + static std::vector virtualEvents(0x45); + uint32_t vCount = {}; + uint32_t vKeyboardEventsCount = {}; + uint32_t vMouseEventsCount = {}; + + projection.beginInputProcessing(m_nextPresentationTimestamp); + { + projection.processKeyboard(nullptr, vKeyboardEventsCount, {}); + projection.processMouse(nullptr, vMouseEventsCount, {}); + + const auto totalCount = vKeyboardEventsCount + vMouseEventsCount; + if (virtualEvents.size() < totalCount) + virtualEvents.resize(totalCount); + + auto* output = virtualEvents.data(); + projection.processKeyboard(output, vKeyboardEventsCount, { cameraKeyboardEvents.data(), cameraKeyboardEvents.size() }); + for (uint32_t i = 0u; i < vKeyboardEventsCount; ++i) + output[i].magnitude *= m_cameraControls.keyboardScale; + output += vKeyboardEventsCount; + + if (isOrbitLikeCamera(camera)) + { + if (ImGui::IsMouseDown(ImGuiMouseButton_Left)) + projection.processMouse(output, vMouseEventsCount, { cameraMouseEvents.data(), cameraMouseEvents.size() }); + else + vMouseEventsCount = 0; + } + else + { + projection.processMouse(output, vMouseEventsCount, { cameraMouseEvents.data(), cameraMouseEvents.size() }); + } + + vCount = vKeyboardEventsCount + vMouseEventsCount; + } + projection.endInputProcessing(); + + if (vCount) + { + applyVirtualEventScaling(virtualEvents, vCount); + + const char* controllerLabel = "Keyboard/Mouse"; + auto applyEventsToCamera = [&](ICamera* target, uint32_t planarIx) + { + if (!target) + return; + + if (m_cameraControls.worldTranslate) + { + std::vector perCameraEvents(virtualEvents.begin(), virtualEvents.begin() + vCount); + uint32_t perCount = vCount; + remapTranslationToWorld(target, perCameraEvents, perCount); + if (perCount) + target->manipulate({ perCameraEvents.data(), perCount }); + } + else + { + target->manipulate({ virtualEvents.data(), vCount }); + } + + applyConstraintsToCamera(target); + appendVirtualEventLog("input", controllerLabel, planarIx, target, virtualEvents.data(), vCount); + }; + + if (m_cameraControls.mirrorInput) + { + std::unordered_set visited; + for (size_t bindingIx = 0u; bindingIx < windowBindings.size(); ++bindingIx) + { + auto& bindingIt = windowBindings[bindingIx]; + auto& planarIt = m_planarProjections[bindingIt.activePlanarIx]; + if (!planarIt) + continue; + auto* target = planarIt->getCamera(); + if (!target) + continue; + const auto id = target->getGimbal().getID(); + if (!visited.insert(id).second) + continue; + applyEventsToCamera(target, bindingIt.activePlanarIx); + } + } + else + { + applyEventsToCamera(camera, binding.activePlanarIx); + } + + if (m_scriptedInput.log) + { + for (uint32_t i = 0u; i < vCount; ++i) + { + const auto& ev = virtualEvents[i]; + m_logger->log("[script] virtual %s magnitude=%.6f", ILogger::ELL_INFO, CVirtualGimbalEvent::virtualEventToString(ev.type).data(), ev.magnitude); + } + + const auto& gimbal = camera->getGimbal(); + const auto pos = gimbal.getPosition(); + const auto euler = glm::degrees(glm::eulerAngles(gimbal.getOrientation())); + m_logger->log("[script] gimbal pos=(%.3f, %.3f, %.3f) euler_deg=(%.3f, %.3f, %.3f)", ILogger::ELL_INFO, + pos.x, pos.y, pos.z, euler.x, euler.y, euler.z); + } + } + } + + if (m_scriptedInput.enabled && scriptedImguizmo.size() && !skipCameraInput) + { + auto& binding = windowBindings[activeRenderWindowIx]; + auto& planar = m_planarProjections[binding.activePlanarIx]; + auto* camera = planar->getCamera(); + + static std::vector imguizmoEvents(0x20); + uint32_t vCount = 0u; + + camera->beginInputProcessing(m_nextPresentationTimestamp); + { + camera->processImguizmo(nullptr, vCount, {}); + if (imguizmoEvents.size() < vCount) + imguizmoEvents.resize(vCount); + + camera->processImguizmo(imguizmoEvents.data(), vCount, { scriptedImguizmo.data(), scriptedImguizmo.size() }); + } + camera->endInputProcessing(); + + if (vCount) + { + camera->manipulate({ imguizmoEvents.data(), vCount }); + appendVirtualEventLog("imguizmo", "ImGuizmo", binding.activePlanarIx, camera, imguizmoEvents.data(), vCount); + + if (m_scriptedInput.log) + { + for (uint32_t i = 0u; i < vCount; ++i) + { + const auto& ev = imguizmoEvents[i]; + m_logger->log("[script] imguizmo virtual %s magnitude=%.6f", ILogger::ELL_INFO, CVirtualGimbalEvent::virtualEventToString(ev.type).data(), ev.magnitude); + } + + const auto& gimbal = camera->getGimbal(); + const auto pos = gimbal.getPosition(); + const auto euler = glm::degrees(glm::eulerAngles(gimbal.getOrientation())); + m_logger->log("[script] imguizmo gimbal pos=(%.3f, %.3f, %.3f) euler_deg=(%.3f, %.3f, %.3f)", ILogger::ELL_INFO, + pos.x, pos.y, pos.z, euler.x, euler.y, euler.z); + } + } + + scriptedImguizmoVirtual = vCount ? imguizmoEvents.data() : nullptr; + scriptedImguizmoVirtualCount = vCount; + } + + if (m_scriptedInput.enabled && m_scriptedInput.nextCheckIndex < m_scriptedInput.checks.size()) + { + auto* camera = [&]() -> ICamera* + { + if (m_planarProjections.empty()) + return nullptr; + auto& binding = windowBindings[activeRenderWindowIx]; + if (binding.activePlanarIx >= m_planarProjections.size()) + return nullptr; + return m_planarProjections[binding.activePlanarIx]->getCamera(); + }(); + + 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)...); + }; + + auto angleDiffDeg = [](float a, float b) -> float + { + float d = std::fmod(a - b + 180.0f, 360.0f); + if (d < 0.0f) + d += 360.0f; + return std::abs(d - 180.0f); + }; + + auto isFinite3 = [](const float32_t3& v) -> bool + { + return std::isfinite(v.x) && std::isfinite(v.y) && std::isfinite(v.z); + }; + auto setStepReference = [&](const float32_t3& newPos, const float32_t3& newEuler) -> void + { + m_scriptedInput.stepValid = true; + m_scriptedInput.stepPos = newPos; + m_scriptedInput.stepEulerDeg = newEuler; + }; + + const auto frame = m_realFrameIx; + while (m_scriptedInput.nextCheckIndex < m_scriptedInput.checks.size() && + m_scriptedInput.checks[m_scriptedInput.nextCheckIndex].frame == frame) + { + const auto& check = m_scriptedInput.checks[m_scriptedInput.nextCheckIndex]; + + if (!camera) + { + logFail("[script][fail] check frame=%llu no active camera", static_cast(frame)); + ++m_scriptedInput.nextCheckIndex; + continue; + } + + const auto& gimbal = camera->getGimbal(); + const auto pos = gimbal.getPosition(); + const auto euler = glm::degrees(glm::eulerAngles(gimbal.getOrientation())); + + if (!isFinite3(pos) || !isFinite3(euler)) + { + logFail("[script][fail] check frame=%llu non-finite gimbal state", static_cast(frame)); + ++m_scriptedInput.nextCheckIndex; + continue; + } + + if (check.kind == ScriptedInputCheck::Kind::Baseline) + { + m_scriptedInput.baselineValid = true; + m_scriptedInput.baselinePos = pos; + m_scriptedInput.baselineEulerDeg = euler; + setStepReference(pos, euler); + logPass("[script][pass] baseline frame=%llu pos=(%.3f, %.3f, %.3f) euler_deg=(%.3f, %.3f, %.3f)", + static_cast(frame), + pos.x, pos.y, pos.z, euler.x, euler.y, euler.z); + } + else if (check.kind == ScriptedInputCheck::Kind::ImguizmoVirtual) + { + bool ok = true; + if (!scriptedImguizmoVirtual || scriptedImguizmoVirtualCount == 0u) + { + ok = false; + } + else + { + for (const auto& expected : check.expectedVirtualEvents) + { + bool found = false; + double actual = 0.0; + for (uint32_t i = 0u; i < scriptedImguizmoVirtualCount; ++i) + { + if (scriptedImguizmoVirtual[i].type == expected.type) + { + found = true; + actual = scriptedImguizmoVirtual[i].magnitude; + break; + } + } + if (!found || std::abs(actual - expected.magnitude) > check.tolerance) + { + ok = false; + logFail("[script][fail] imguizmo_virtual frame=%llu type=%s expected=%.6f actual=%.6f tol=%.6f", + static_cast(frame), + CVirtualGimbalEvent::virtualEventToString(expected.type).data(), + expected.magnitude, + actual, + check.tolerance); + } + } + } + + if (ok) + logPass("[script][pass] imguizmo_virtual frame=%llu events=%zu", static_cast(frame), check.expectedVirtualEvents.size()); + } + else if (check.kind == ScriptedInputCheck::Kind::GimbalNear) + { + bool ok = true; + if (check.hasExpectedPos) + { + const auto diff = float32_t3(pos.x - check.expectedPos.x, pos.y - check.expectedPos.y, pos.z - check.expectedPos.z); + const auto d = std::sqrt(diff.x * diff.x + diff.y * diff.y + diff.z * diff.z); + if (d > check.posTolerance) + { + ok = false; + logFail("[script][fail] gimbal_near frame=%llu pos_diff=%.6f tol=%.6f", + static_cast(frame), d, check.posTolerance); + } + } + if (check.hasExpectedEuler) + { + const auto dx = angleDiffDeg(euler.x, check.expectedEulerDeg.x); + const auto dy = angleDiffDeg(euler.y, check.expectedEulerDeg.y); + const auto dz = angleDiffDeg(euler.z, check.expectedEulerDeg.z); + const auto dmax = std::max(dx, std::max(dy, dz)); + if (dmax > check.eulerToleranceDeg) + { + ok = false; + logFail("[script][fail] gimbal_near frame=%llu euler_diff=%.6f tol=%.6f", + static_cast(frame), dmax, check.eulerToleranceDeg); + } + } + + if (ok) + logPass("[script][pass] gimbal_near frame=%llu", static_cast(frame)); + } + else if (check.kind == ScriptedInputCheck::Kind::GimbalDelta) + { + if (!m_scriptedInput.baselineValid) + { + logFail("[script][fail] gimbal_delta frame=%llu missing baseline", static_cast(frame)); + } + else + { + const auto diff = float32_t3(pos.x - m_scriptedInput.baselinePos.x, pos.y - m_scriptedInput.baselinePos.y, pos.z - m_scriptedInput.baselinePos.z); + const auto dpos = std::sqrt(diff.x * diff.x + diff.y * diff.y + diff.z * diff.z); + const auto dx = angleDiffDeg(euler.x, m_scriptedInput.baselineEulerDeg.x); + const auto dy = angleDiffDeg(euler.y, m_scriptedInput.baselineEulerDeg.y); + const auto dz = angleDiffDeg(euler.z, m_scriptedInput.baselineEulerDeg.z); + const auto dmax = std::max(dx, std::max(dy, dz)); + + if (dpos > check.posTolerance || dmax > check.eulerToleranceDeg) + { + logFail("[script][fail] gimbal_delta frame=%llu pos_diff=%.6f tol=%.6f euler_diff=%.6f tol=%.6f", + static_cast(frame), + dpos, check.posTolerance, + dmax, check.eulerToleranceDeg); + } + else + { + logPass("[script][pass] gimbal_delta frame=%llu pos_diff=%.6f euler_diff=%.6f", + static_cast(frame), dpos, dmax); + } + } + } + else if (check.kind == ScriptedInputCheck::Kind::GimbalStep) + { + if (!m_scriptedInput.stepValid) + { + if (m_scriptedInput.baselineValid) + setStepReference(m_scriptedInput.baselinePos, m_scriptedInput.baselineEulerDeg); + else + { + logFail("[script][fail] gimbal_step frame=%llu missing step reference", static_cast(frame)); + setStepReference(pos, euler); + ++m_scriptedInput.nextCheckIndex; + continue; + } + } + + const auto diff = float32_t3(pos.x - m_scriptedInput.stepPos.x, pos.y - m_scriptedInput.stepPos.y, pos.z - m_scriptedInput.stepPos.z); + const auto dpos = std::sqrt(diff.x * diff.x + diff.y * diff.y + diff.z * diff.z); + const auto dx = angleDiffDeg(euler.x, m_scriptedInput.stepEulerDeg.x); + const auto dy = angleDiffDeg(euler.y, m_scriptedInput.stepEulerDeg.y); + const auto dz = angleDiffDeg(euler.z, m_scriptedInput.stepEulerDeg.z); + const auto dmax = std::max(dx, std::max(dy, dz)); + + bool ok = true; + if (check.hasPosDeltaConstraint) + { + if (dpos < check.minPosDelta || dpos > check.posTolerance) + { + ok = false; + logFail("[script][fail] gimbal_step frame=%llu pos_delta=%.6f expected=[%.6f, %.6f]", + static_cast(frame), dpos, check.minPosDelta, check.posTolerance); + } + } + if (check.hasEulerDeltaConstraint) + { + if (dmax < check.minEulerDeltaDeg || dmax > check.eulerToleranceDeg) + { + ok = false; + logFail("[script][fail] gimbal_step frame=%llu euler_delta=%.6f expected=[%.6f, %.6f]", + static_cast(frame), dmax, check.minEulerDeltaDeg, check.eulerToleranceDeg); + } + } + + if (ok) + logPass("[script][pass] gimbal_step frame=%llu pos_delta=%.6f euler_delta=%.6f", + static_cast(frame), dpos, dmax); + setStepReference(pos, euler); + } + + ++m_scriptedInput.nextCheckIndex; + } + + if (!m_scriptedInput.summaryReported && m_scriptedInput.nextCheckIndex >= m_scriptedInput.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); + } + } + + UpdateUiMetrics(); + m_ui.manager->update(params); + diff --git a/61_UI/include/app/impl/AppWorkLoop.inl b/61_UI/include/app/impl/AppWorkLoop.inl new file mode 100644 index 000000000..4976bc589 --- /dev/null +++ b/61_UI/include/app/impl/AppWorkLoop.inl @@ -0,0 +1,313 @@ + paceScriptedVisualDebugFrame(); + + // framesInFlight: ensuring safe execution of command buffers and acquires, `framesInFlight` only affect semaphore waits, don't use this to index your resources because it can change with swapchain recreation. + const uint32_t framesInFlight = core::min(MaxFramesInFlight, m_surface->getMaxAcquiresInFlight()); + // We block for semaphores for 2 reasons here: + // A) Resource: Can't use resource like a command buffer BEFORE previous use is finished! [MaxFramesInFlight] + // B) Acquire: Can't have more acquires in flight than a certain threshold returned by swapchain or your surface helper class. [MaxAcquiresInFlight] + if (m_realFrameIx >= framesInFlight) + { + const ISemaphore::SWaitInfo cmdbufDonePending[] = { + { + .semaphore = m_semaphore.get(), + .value = m_realFrameIx + 1 - framesInFlight + } + }; + if (m_device->blockForSemaphores(cmdbufDonePending) != ISemaphore::WAIT_RESULT::SUCCESS) + return; + } + + // Predict size of next render, and bail if nothing to do + const auto currentSwapchainExtent = m_surface->getCurrentExtent(); + if (currentSwapchainExtent.width * currentSwapchainExtent.height <= 0) + return; + // The extent of the swapchain might change between now and `present` but the blit should adapt nicely + const VkRect2D currentRenderArea = { .offset = {0,0},.extent = currentSwapchainExtent }; + + // You explicitly should not use `getAcquireCount()` see the comment on `m_realFrameIx` + const auto resourceIx = m_realFrameIx % MaxFramesInFlight; + + // We will be using this command buffer to produce the frame + auto frame = m_tripleBuffers[resourceIx].get(); + auto cmdbuf = m_cmdBufs[resourceIx].get(); + + // update CPU stuff - controllers, events, UI state + update(); + + bool willSubmit = true; + { + willSubmit &= cmdbuf->reset(IGPUCommandBuffer::RESET_FLAGS::RELEASE_RESOURCES_BIT); + willSubmit &= cmdbuf->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT); + willSubmit &= cmdbuf->beginDebugMarker("UIApp Frame"); + + auto renderScene = [&](windowControlBinding& binding) + { + if (!binding.sceneFramebuffer) + return; + + const auto& fbParams = binding.sceneFramebuffer->getCreationParameters(); + const VkRect2D renderArea = { .offset = {0,0}, .extent = {fbParams.width, fbParams.height} }; + const IGPUCommandBuffer::SRenderpassBeginInfo info = { + .framebuffer = binding.sceneFramebuffer.get(), + .colorClearValues = &SceneClearColor, + .depthStencilClearValues = &SceneClearDepth, + .renderArea = renderArea + }; + + willSubmit &= cmdbuf->beginRenderPass(info, IGPUCommandBuffer::SUBPASS_CONTENTS::INLINE); + { + asset::SViewport viewport = {}; + viewport.minDepth = 1.f; + viewport.maxDepth = 0.f; + viewport.x = 0u; + viewport.y = 0u; + viewport.width = fbParams.width; + viewport.height = fbParams.height; + + willSubmit &= cmdbuf->setViewport(0u, 1u, &viewport); + willSubmit &= cmdbuf->setScissor(0u, 1u, &renderArea); + + const auto viewParams = CSimpleDebugRenderer::SViewParams(binding.viewMatrix, binding.viewProjMatrix); + m_renderer->render(cmdbuf, viewParams); + + } + willSubmit &= cmdbuf->endRenderPass(); + }; + + if (m_renderer && !m_renderer->m_instances.empty()) + { + auto& instance = m_renderer->m_instances[0]; + instance.world = m_model; + const auto geomCount = m_renderer->getGeometries().size(); + if (geomCount) + { + if (gcIndex >= geomCount) + gcIndex = 0; + instance.packedGeo = m_renderer->getGeometries().data() + gcIndex; + } + } + + if (useWindow) + for (auto& binding : windowBindings) + renderScene(binding); + else + renderScene(windowBindings[activeRenderWindowIx]); + + const IGPUCommandBuffer::SClearColorValue clearValue = { .float32 = {0.f,0.f,0.f,1.f} }; + const IGPUCommandBuffer::SRenderpassBeginInfo info = { + .framebuffer = m_framebuffers[resourceIx].get(), + .colorClearValues = &clearValue, + .depthStencilClearValues = nullptr, + .renderArea = currentRenderArea + }; + + // UI renderpass + willSubmit &= cmdbuf->beginRenderPass(info, IGPUCommandBuffer::SUBPASS_CONTENTS::INLINE); + { + asset::SViewport viewport; + { + viewport.minDepth = 1.f; + viewport.maxDepth = 0.f; + viewport.x = 0u; + viewport.y = 0u; + viewport.width = m_window->getWidth(); + viewport.height = m_window->getHeight(); + } + + willSubmit &= cmdbuf->setViewport(0u, 1u, &viewport); + + const VkRect2D currentRenderArea = + { + .offset = {0,0}, + .extent = {m_window->getWidth(),m_window->getHeight()} + }; + + IQueue::SSubmitInfo::SCommandBufferInfo commandBuffersInfo[] = { {.cmdbuf = cmdbuf } }; + + const IGPUCommandBuffer::SRenderpassBeginInfo info = + { + .framebuffer = m_framebuffers[resourceIx].get(), + .colorClearValues = &clearValue, + .depthStencilClearValues = nullptr, + .renderArea = currentRenderArea + }; + + nbl::video::ISemaphore::SWaitInfo waitInfo = { .semaphore = m_semaphore.get(), .value = m_realFrameIx + 1u }; + const auto uiParams = m_ui.manager->getCreationParameters(); + auto* pipeline = m_ui.manager->getPipeline(); + + cmdbuf->bindGraphicsPipeline(pipeline); + cmdbuf->bindDescriptorSets(EPBP_GRAPHICS, pipeline->getLayout(), uiParams.resources.texturesInfo.setIx, 1u, &m_ui.descriptorSet.get()); // note that we use default UI pipeline layout where uiParams.resources.textures.setIx == uiParams.resources.samplers.setIx + + if (!keepRunning()) + return; + + willSubmit &= m_ui.manager->render(cmdbuf, waitInfo); + } + willSubmit &= cmdbuf->endRenderPass(); + + // If the Rendering and Blit/Present Queues don't come from the same family we need to transfer ownership, because we need to preserve contents between them. + auto blitQueueFamily = m_surface->getAssignedQueue()->getFamilyIndex(); + // Also should crash/error if concurrent sharing enabled but would-be-user-queue is not in the share set, but oh well. + const bool needOwnershipRelease = cmdbuf->getQueueFamilyIndex() != blitQueueFamily && !frame->getCachedCreationParams().isConcurrentSharing(); + if (needOwnershipRelease) + { + const IGPUCommandBuffer::SPipelineBarrierDependencyInfo::image_barrier_t barrier[] = { { + .barrier = { + .dep = { + // Normally I'd put `COLOR_ATTACHMENT` on the masks, but we want this to happen after Layout Transition :( + // https://github.com/KhronosGroup/Vulkan-Docs/issues/2319 + .srcStageMask = asset::PIPELINE_STAGE_FLAGS::ALL_COMMANDS_BITS, + .srcAccessMask = asset::ACCESS_FLAGS::MEMORY_READ_BITS | asset::ACCESS_FLAGS::MEMORY_WRITE_BITS, + // For a Queue Family Ownership Release the destination access masks are irrelevant + // and source stage mask can be NONE as long as the semaphore signals ALL_COMMANDS_BIT + .dstStageMask = asset::PIPELINE_STAGE_FLAGS::NONE, + .dstAccessMask = asset::ACCESS_FLAGS::NONE + }, + .ownershipOp = IGPUCommandBuffer::SOwnershipTransferBarrier::OWNERSHIP_OP::RELEASE, + .otherQueueFamilyIndex = blitQueueFamily + }, + .image = frame, + .subresourceRange = TripleBufferUsedSubresourceRange + // there will be no layout transition, already done by the Renderpass End + } }; + const IGPUCommandBuffer::SPipelineBarrierDependencyInfo depInfo = { .imgBarriers = barrier }; + willSubmit &= cmdbuf->pipelineBarrier(asset::EDF_NONE, depInfo); + } + } + willSubmit &= cmdbuf->end(); + + // submit and present under a mutex ASAP + if (willSubmit) + { + // We will signal a semaphore in the rendering queue, and await it with the presentation/blit queue + const IQueue::SSubmitInfo::SSemaphoreInfo rendered = + { + .semaphore = m_semaphore.get(), + .value = m_realFrameIx + 1, + // Normally I'd put `COLOR_ATTACHMENT` on the masks, but we want to signal after Layout Transitions and optional Ownership Release + // https://github.com/KhronosGroup/Vulkan-Docs/issues/2319 + .stageMask = asset::PIPELINE_STAGE_FLAGS::ALL_COMMANDS_BITS + }; + const IQueue::SSubmitInfo::SCommandBufferInfo cmdbufs[1] = + { { + .cmdbuf = cmdbuf + } }; + // We need to wait on previous triple buffer blits/presents from our source image to complete + auto* pBlitWaitValue = m_blitWaitValues.data() + resourceIx; + auto swapchainLock = m_surface->pseudoAcquire(pBlitWaitValue); + const IQueue::SSubmitInfo::SSemaphoreInfo blitted = + { + .semaphore = m_surface->getPresentSemaphore(), + .value = pBlitWaitValue->load(), + // Normally I'd put `BLIT` on the masks, but we want to wait before Implicit Layout Transitions and optional Implicit Ownership Acquire + // https://github.com/KhronosGroup/Vulkan-Docs/issues/2319 + .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; + + m_realFrameIx++; + + const uint64_t renderedFrameIx = m_realFrameIx - 1u; + auto captureScreenshot = [&](const system::path& outPath, const char* tag) -> void + { + if (!m_device || !m_assetManager || !m_surface) + 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; + } + + if (!frame) + { + m_logger->log("%s screenshot failed: missing frame image.", 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_assetManager.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); + }; + + // only present if there's successful content to show + const ISmoothResizeSurface::SPresentInfo presentInfo = { + { + .source = {.image = frame,.rect = currentRenderArea}, + .waitSemaphore = rendered.semaphore, + .waitValue = rendered.value, + .pPresentSemaphoreWaitValue = pBlitWaitValue, + }, + // The Graphics Queue will be the the most recent owner just before it releases ownership + cmdbuf->getQueueFamilyIndex() + }; + if (m_ciMode && !m_ciScreenshotDone) + { + ++m_ciFrameCounter; + if (m_ciFrameCounter >= CiFramesBeforeCapture) + { + m_ciScreenshotDone = true; + captureScreenshot(m_ciScreenshotPath, "CI"); + } + } + + if (m_scriptedInput.enabled && !m_scriptedInput.captureFrames.empty()) + { + while (m_scriptedInput.nextCaptureIndex < m_scriptedInput.captureFrames.size() && + m_scriptedInput.captureFrames[m_scriptedInput.nextCaptureIndex] == renderedFrameIx) + { + const auto outPath = m_scriptedInput.captureOutputDir / + (m_scriptedInput.capturePrefix + "_" + std::to_string(renderedFrameIx) + ".png"); + captureScreenshot(outPath, "Script"); + ++m_scriptedInput.nextCaptureIndex; + } + } + + m_surface->present(std::move(swapchainLock), presentInfo); + } + firstFrame = false; diff --git a/61_UI/include/keysmapping.hpp b/61_UI/include/keysmapping.hpp index 46153660c..ef37bfd89 100644 --- a/61_UI/include/keysmapping.hpp +++ b/61_UI/include/keysmapping.hpp @@ -3,232 +3,7 @@ #include "common.hpp" -bool handleAddMapping(const char* tableID, IGimbalManipulateEncoder* encoder, IGimbalManipulateEncoder::EncoderType activeController, 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, 0.33f); - ImGui::TableSetupColumn("Key", ImGuiTableColumnFlags_WidthStretch, 0.33f); - ImGui::TableSetupColumn("Actions", ImGuiTableColumnFlags_WidthStretch, 0.33f); - ImGui::TableHeadersRow(); +bool handleAddMapping(const char* tableID, IGimbalManipulateEncoder* encoder, IGimbalManipulateEncoder::EncoderType activeController, CVirtualGimbalEvent::VirtualEventType& selectedEventType, ui::E_KEY_CODE& newKey, ui::E_MOUSE_CODE& newMouseCode, bool& addMode); +bool displayKeyMappingsAndVirtualStatesInline(IGimbalManipulateEncoder* encoder, bool spawnWindow = false); - 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 (activeController == IGimbalManipulateEncoder::Keyboard) - { - char newKeyDisplay[2] = { ui::keyCodeToChar(newKey, true), '\0' }; - if (ImGui::BeginCombo("##selectKey", newKeyDisplay)) - { - for (int i = ui::E_KEY_CODE::EKC_A; i <= ui::E_KEY_CODE::EKC_Z; ++i) - { - bool isSelected = (newKey == static_cast(i)); - char label[2] = { ui::keyCodeToChar(static_cast(i), true), '\0' }; - if (ImGui::Selectable(label, 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", ImVec2(100, 30))) - { - anyMapUpdated |= true; - if (activeController == IGimbalManipulateEncoder::Keyboard) - encoder->updateKeyboardMapping([&](auto& keys) { keys[newKey] = selectedEventType; }); - else - encoder->updateMouseMapping([&](auto& mouse) { mouse[newMouseCode] = selectedEventType; }); - addMode = false; - } - - ImGui::EndTable(); - - return anyMapUpdated; -} - -bool displayKeyMappingsAndVirtualStatesInline(IGimbalManipulateEncoder* encoder, bool spawnWindow = false) -{ - bool anyMapUpdated = false; - - if (!encoder) 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; - IGimbalManipulateEncoder::EncoderType activeController = IGimbalManipulateEncoder::Keyboard; - }; - - static std::unordered_map cameraStates; - auto& state = cameraStates[encoder]; - - const auto& keyboardMappings = encoder->getKeyboardVirtualEventMap(); - const auto& mouseMappings = encoder->getMouseVirtualEventMap(); - - if (spawnWindow) - { - ImGui::SetNextWindowSize(ImVec2(600, 400), ImGuiCond_FirstUseEver); - ImGui::Begin("Controller Mappings & Virtual States", nullptr, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_AlwaysVerticalScrollbar); - } - - if (ImGui::BeginTabBar("ControllersTabBar")) - { - if (ImGui::BeginTabItem("Keyboard")) - { - state.activeController = IGimbalManipulateEncoder::Keyboard; - ImGui::Separator(); - - if (ImGui::Button("Add Key", ImVec2(100, 30))) - 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); - std::string keyString(1, ui::keyCodeToChar(keyboardCode, true)); - ImGui::AlignTextToFramePadding(); - ImGui::TextWrapped("%s", keyString.c_str()); - - ImGui::TableSetColumnIndex(2); - bool isActive = (hash.event.magnitude > 0); - ImVec4 statusColor = isActive ? ImVec4(0.0f, 1.0f, 0.0f, 1.0f) : ImVec4(1.0f, 0.0f, 0.0f, 1.0f); - 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; - encoder->updateKeyboardMapping([keyboardCode](auto& keys) { keys.erase(keyboardCode); }); - break; - } - } - ImGui::EndTable(); - - if (state.addMode) - { - ImGui::Separator(); - anyMapUpdated |= handleAddMapping("AddKeyboardMappingTable", encoder, state.activeController, state.selectedEventType, state.newKey, state.newMouseCode, state.addMode); - } - - ImGui::EndTabItem(); - } - - if (ImGui::BeginTabItem("Mouse")) - { - state.activeController = IGimbalManipulateEncoder::Mouse; - ImGui::Separator(); - - if (ImGui::Button("Add Key", ImVec2(100, 30))) - 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); - ImVec4 statusColor = isActive ? ImVec4(0.0f, 1.0f, 0.0f, 1.0f) : ImVec4(1.0f, 0.0f, 0.0f, 1.0f); - 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; - encoder->updateMouseMapping([mouseCode](auto& mouse) { mouse.erase(mouseCode); }); - break; - } - } - ImGui::EndTable(); - - if (state.addMode) - { - ImGui::Separator(); - handleAddMapping("AddMouseMappingTable", encoder, state.activeController, state.selectedEventType, state.newKey, state.newMouseCode, state.addMode); - } - ImGui::EndTabItem(); - } - - ImGui::EndTabBar(); - } - - if (spawnWindow) - ImGui::End(); - - return anyMapUpdated; -} - -#endif // __NBL_KEYSMAPPING_H_INCLUDED__ \ No newline at end of file +#endif // __NBL_KEYSMAPPING_H_INCLUDED__ diff --git a/61_UI/include/transform.hpp b/61_UI/include/transform.hpp index fb1672c2f..936a5af40 100644 --- a/61_UI/include/transform.hpp +++ b/61_UI/include/transform.hpp @@ -1,15 +1,12 @@ #ifndef _NBL_THIS_EXAMPLE_TRANSFORM_H_INCLUDED_ #define _NBL_THIS_EXAMPLE_TRANSFORM_H_INCLUDED_ - +#include #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; @@ -17,146 +14,6 @@ struct TransformRequestParams 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; -} +nbl::hlsl::uint16_t2 EditTransform(float* cameraView, const float* cameraProjection, float* matrix, const TransformRequestParams& params); -#endif // __NBL_THIS_EXAMPLE_TRANSFORM_H_INCLUDED__ \ No newline at end of file +#endif // _NBL_THIS_EXAMPLE_TRANSFORM_H_INCLUDED_ diff --git a/61_UI/main.cpp b/61_UI/main.cpp index 25e93b249..02a6bb746 100644 --- a/61_UI/main.cpp +++ b/61_UI/main.cpp @@ -1,5872 +1,4 @@ -// 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 "app/App.hpp" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "nlohmann/json.hpp" -#include "argparse/argparse.hpp" -using json = nlohmann::json; +NBL_MAIN_FUNC(App) -#include "common.hpp" -#include "keysmapping.hpp" -#include "camera/CCubeProjection.hpp" -#include "glm/glm/ext/matrix_clip_space.hpp" // TODO: TESTING -#include "glm/gtc/quaternion.hpp" -#include "nbl/ext/ScreenShot/ScreenShot.h" -#include "nbl/this_example/builtin/build/spirv/keys.hpp" -#if __has_include("nbl/this_example/builtin/CArchive.h") -#include "nbl/this_example/builtin/CArchive.h" -#endif -#if __has_include("nbl/this_example/builtin/build/CArchive.h") -#include "nbl/this_example/builtin/build/CArchive.h" -#endif - -using planar_projections_range_t = std::vector; -using planar_projection_t = CPlanarProjection; - -// the only reason for those is to remind we must go with transpose & 4x4 matrices -struct ImGuizmoPlanarM16InOut -{ - float32_t4x4 view, projection; -}; - -struct ImGuizmoModelM16InOut -{ - float32_t4x4 inTRS, outTRS, outDeltaTRS; -}; - -constexpr IGPUImage::SSubresourceRange TripleBufferUsedSubresourceRange = -{ - .aspectMask = IGPUImage::EAF_COLOR_BIT, - .baseMipLevel = 0, - .levelCount = 1, - .baseArrayLayer = 0, - .layerCount = 1 -}; - -class CUIEventCallback : public nbl::video::ISmoothResizeSurface::ICallback // I cannot use common CEventCallback because I MUST inherit this callback in order to use smooth resize surface with window callback (for my input events) -{ -public: - CUIEventCallback(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)) {} - CUIEventCallback() {} - - void setLogger(nbl::system::logger_opt_smart_ptr& logger) - { - m_logger = logger; - } - void setInputSystem(nbl::core::smart_refctd_ptr&& m_inputSystem) - { - m_inputSystem = std::move(m_inputSystem); - } -private: - - void onMouseConnected_impl(nbl::core::smart_refctd_ptr&& mch) override - { - m_logger.log("A mouse %p has been connected", nbl::system::ILogger::ELL_INFO, mch.get()); - m_inputSystem.get()->add(m_inputSystem.get()->m_mouse, std::move(mch)); - } - void onMouseDisconnected_impl(nbl::ui::IMouseEventChannel* mch) override - { - m_logger.log("A mouse %p has been disconnected", nbl::system::ILogger::ELL_INFO, mch); - m_inputSystem.get()->remove(m_inputSystem.get()->m_mouse, mch); - } - void onKeyboardConnected_impl(nbl::core::smart_refctd_ptr&& kbch) override - { - m_logger.log("A keyboard %p has been connected", nbl::system::ILogger::ELL_INFO, kbch.get()); - m_inputSystem.get()->add(m_inputSystem.get()->m_keyboard, std::move(kbch)); - } - void onKeyboardDisconnected_impl(nbl::ui::IKeyboardEventChannel* kbch) override - { - m_logger.log("A keyboard %p has been disconnected", nbl::system::ILogger::ELL_INFO, kbch); - m_inputSystem.get()->remove(m_inputSystem.get()->m_keyboard, kbch); - } - -private: - nbl::core::smart_refctd_ptr m_inputSystem = nullptr; - nbl::system::logger_opt_smart_ptr m_logger = nullptr; -}; - -class CSwapchainResources final : public ISmoothResizeSurface::ISwapchainResources -{ -public: - // Because we blit to the swapchain image asynchronously, we need a queue which can not only present but also perform graphics commands. - // If we for example used a compute shader to tonemap and MSAA resolve, we'd request the COMPUTE_BIT here. - constexpr static inline IQueue::FAMILY_FLAGS RequiredQueueFlags = IQueue::FAMILY_FLAGS::GRAPHICS_BIT; - - inline uint8_t getLastImageIndex() const { return m_lastImageIndex; } - -protected: - // We can return `BLIT_BIT` here, because the Source Image will be already in the correct layout to be used for the present - 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; - - // Ownership of the Source Blit Image, not the Swapchain Image - const bool needToAcquireSrcOwnership = qFamToAcquireSrcFrom != IQueue::FamilyIgnored; - // Should never get asked to transfer ownership if the source is concurrent sharing - assert(!source.image->getCachedCreationParams().isConcurrentSharing() || !needToAcquireSrcOwnership); - - const auto blitDstLayout = IGPUImage::LAYOUT::TRANSFER_DST_OPTIMAL; - IGPUCommandBuffer::SPipelineBarrierDependencyInfo depInfo = {}; - - // barrier before to transition the swapchain image layout - using image_barrier_t = decltype(depInfo.imgBarriers)::element_type; - const image_barrier_t preBarriers[2] = { - { - .barrier = { - .dep = { - .srcStageMask = asset::PIPELINE_STAGE_FLAGS::NONE, // acquire isn't a stage - .srcAccessMask = asset::ACCESS_FLAGS::NONE, // performs no accesses - .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, // I do not care about previous contents of the swapchain - .newLayout = blitDstLayout - }, - { - .barrier = { - .dep = { - // when acquiring ownership the source access masks don't matter - .srcStageMask = asset::PIPELINE_STAGE_FLAGS::NONE, - // Acquire must Happen-Before Semaphore wait, but neither has a true stage so NONE here - // https://github.com/KhronosGroup/Vulkan-Docs/issues/2319 - // If no ownership acquire needed then this dep info won't be used at all - .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 - // no layout transition, already in the correct layout for the blit - } - }; - // We only barrier the source image if we need to acquire ownership, otherwise thanks to Timeline Semaphores all sync is good - depInfo.imgBarriers = { preBarriers,needToAcquireSrcOwnership ? 2ull : 1ull }; - success &= cmdbuf->pipelineBarrier(asset::EDF_NONE, depInfo); - - // TODO: Implement scaling modes other than a plain STRETCH, and allow for using subrectangles of the initial contents - { - 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); - } - - // Barrier after, note that I don't care about preserving the contents of the Triple Buffer when the Render queue starts writing to it again. - // Therefore no ownership release, and no layout transition. - const image_barrier_t postBarrier[1] = { - { - .barrier = { - // When transitioning the image to VK_IMAGE_LAYOUT_SHARED_PRESENT_KHR or VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, there is no need to delay subsequent processing, - // or perform any visibility operations (as vkQueuePresentKHR performs automatic visibility operations). - // To achieve this, the dstAccessMask member of the VkImageMemoryBarrier should be set to 0, and the dstStageMask parameter should be set to VK_PIPELINE_STAGE_2_NONE - .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; -}; - -static smart_refctd_ptr createAttachmentView(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)); -} - -static 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)); -} - -/* - Renders scene texture to an offline - framebuffer which color attachment - is then sampled into a imgui window. - - Written with Nabla, it's UI extension - and got integrated with ImGuizmo to - handle scene's object translations. -*/ - -class UISampleApp final : public examples::SimpleWindowedApplication -{ - using base_t = examples::SimpleWindowedApplication; - using clock_t = std::chrono::steady_clock; - - constexpr static inline clock_t::duration DisplayImageDuration = std::chrono::milliseconds(900); - constexpr static inline auto sceneRenderDepthFormat = EF_D32_SFLOAT; - constexpr static inline auto finalSceneRenderFormat = EF_R8G8B8A8_SRGB; - constexpr static inline IGPUCommandBuffer::SClearColorValue SceneClearColor = { .float32 = {0.014f,0.018f,0.030f,1.f} }; - constexpr static inline IGPUCommandBuffer::SClearDepthStencilValue SceneClearDepth = { .depth = 0.f }; - - public: - using base_t::base_t; - - inline UISampleApp(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 - { - // So let's create our Window and Surface then! - if (!m_surface) - { - { - const auto dpyInfo = m_winMgr->getPrimaryDisplayInfo(); - auto windowCallback = core::make_smart_refctd_ptr(smart_refctd_ptr(m_inputSystem), smart_refctd_ptr(m_logger)); - - IWindow::SCreationParams params = {}; - params.callback = core::make_smart_refctd_ptr(); - params.width = dpyInfo.resX; - params.height = dpyInfo.resY; - params.x = 32; - params.y = 32; - params.flags = IWindow::ECF_INPUT_FOCUS | IWindow::ECF_CAN_RESIZE | IWindow::ECF_CAN_MAXIMIZE | IWindow::ECF_CAN_MINIMIZE; - params.windowCaption = "[Nabla Engine] UI App"; - params.callback = windowCallback; - - 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) - { - m_window->getManager()->maximize(m_window.get()); - auto* cc = m_window->getCursorControl(); - cc->setVisible(true); - - return { {m_surface->getSurface()/*,EQF_NONE*/} }; - } - - return {}; - } - - inline bool onAppInitialized(smart_refctd_ptr&& system) override - { - 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); - - 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_ciMode = program.get("--ci"); - if (m_ciMode) - { - m_ciScreenshotPath = localOutputCWD / "cameraz_ci.png"; - m_ciStartedAt = clock_t::now(); - } - m_scriptedInput.log = program.get("--script-log"); - m_scriptVisualDebugCli = program.get("--script-visual-debug"); - - // Create imput system - m_inputSystem = make_smart_refctd_ptr(logger_opt_smart_ptr(smart_refctd_ptr(m_logger))); - m_logFormatter = core::make_smart_refctd_ptr(); - - // Remember to call the base class initialization! - if (!base_t::onAppInitialized(std::move(system))) - return false; - - { - smart_refctd_ptr examplesHeaderArch, examplesSourceArch, examplesBuildArch, thisExampleArch, thisExampleBuildArch; -#ifdef NBL_EMBED_BUILTIN_RESOURCES - examplesHeaderArch = core::make_smart_refctd_ptr(smart_refctd_ptr(m_logger)); - examplesSourceArch = core::make_smart_refctd_ptr(smart_refctd_ptr(m_logger)); - examplesBuildArch = core::make_smart_refctd_ptr(smart_refctd_ptr(m_logger)); - - #ifdef _NBL_THIS_EXAMPLE_BUILTIN_C_ARCHIVE_H_ - thisExampleArch = make_smart_refctd_ptr(smart_refctd_ptr(m_logger)); - #endif - - #ifdef _NBL_THIS_EXAMPLE_BUILTIN_BUILD_C_ARCHIVE_H_ - thisExampleBuildArch = make_smart_refctd_ptr(smart_refctd_ptr(m_logger)); - #endif -#else - examplesHeaderArch = make_smart_refctd_ptr(localInputCWD/"../common/include/nbl/examples", smart_refctd_ptr(m_logger), m_system.get()); - examplesSourceArch = make_smart_refctd_ptr(localInputCWD/"../common/src/nbl/examples", smart_refctd_ptr(m_logger), m_system.get()); - examplesBuildArch = make_smart_refctd_ptr(NBL_EXAMPLES_BUILD_MOUNT_POINT, smart_refctd_ptr(m_logger), m_system.get()); - thisExampleArch = make_smart_refctd_ptr(localInputCWD/"app_resources", smart_refctd_ptr(m_logger), m_system.get()); - #ifdef NBL_THIS_EXAMPLE_BUILD_MOUNT_POINT - thisExampleBuildArch = make_smart_refctd_ptr(NBL_THIS_EXAMPLE_BUILD_MOUNT_POINT, smart_refctd_ptr(m_logger), m_system.get()); - #endif -#endif - m_system->mount(std::move(examplesHeaderArch),"nbl/examples"); - m_system->mount(std::move(examplesSourceArch),"nbl/examples"); - m_system->mount(std::move(examplesBuildArch),"nbl/examples"); - if (thisExampleArch) - m_system->mount(std::move(thisExampleArch),"app_resources"); - if (thisExampleBuildArch) - m_system->mount(std::move(thisExampleBuildArch),"app_resources"); - } - - { - const std::optional cameraJsonFile = program.is_used("--file") ? program.get("--file") : std::optional(std::nullopt); - - json j; - auto loadDefaultConfig = [&]() -> bool - { -#ifdef _NBL_THIS_EXAMPLE_BUILTIN_C_ARCHIVE_H_ - auto assets = make_smart_refctd_ptr(smart_refctd_ptr(m_logger)); - auto pFile = assets->getFile("cameras.json", IFile::ECF_READ, ""); - if (!pFile) - return logFail("Could not open builtin cameras.json!"); - - string config; - IFile::success_t result; - config.resize(pFile->getSize()); - pFile->read(result, config.data(), 0, pFile->getSize()); - j = json::parse(config); - return true; -#else - const auto fallbackPath = localInputCWD / "app_resources" / "cameras.json"; - std::ifstream fallbackFile(fallbackPath); - if (!fallbackFile.is_open()) - return logFail("Cannot open default config \"%s\".", fallbackPath.string().c_str()); - fallbackFile >> j; - return true; -#endif - }; - - auto file = cameraJsonFile.has_value() ? std::ifstream(cameraJsonFile.value()) : std::ifstream(); - if (!file.is_open()) - { - if (cameraJsonFile.has_value()) - m_logger->log("Cannot open input \"%s\" json file. Switching to default config.", ILogger::ELL_WARNING, cameraJsonFile.value().c_str()); - else - m_logger->log("No input json file provided. Switching to default config.", ILogger::ELL_INFO); - - if (!loadDefaultConfig()) - return false; - } - else - { - file >> j; - } - - auto loadScriptJson = [&](const std::string& path, json& out) -> bool - { - std::ifstream sfile(path); - if (!sfile.is_open()) - { - m_logger->log("Cannot open scripted input file \"%s\".", ILogger::ELL_ERROR, path.c_str()); - return false; - } - sfile >> out; - return true; - }; - - auto parseScriptedInput = [&](const json& script) -> void - { - m_scriptedInput.events.clear(); - m_scriptedInput.checks.clear(); - m_scriptedInput.captureFrames.clear(); - m_scriptedInput.nextEventIndex = 0; - m_scriptedInput.nextCheckIndex = 0; - m_scriptedInput.nextCaptureIndex = 0; - m_scriptedInput.failed = false; - m_scriptedInput.summaryReported = false; - m_scriptedInput.baselineValid = false; - m_scriptedInput.stepValid = false; - m_scriptedInput.exclusive = false; - m_scriptedInput.hardFail = false; - m_scriptedInput.visualDebug = false; - m_scriptedInput.visualTargetFps = 0.f; - m_scriptedInput.visualCameraHoldSeconds = 0.f; - m_scriptedInput.visualActivePlanarValid = false; - m_scriptedInput.visualActivePlanarIx = 0u; - m_scriptedInput.visualActivePlanarStartFrame = 0u; - m_scriptedInput.framePacerInitialized = false; - m_scriptedInput.capturePrefix = "script"; - m_scriptedInput.captureOutputDir = localOutputCWD; - - if (script.contains("enabled")) - m_scriptedInput.enabled = script["enabled"].get(); - else - m_scriptedInput.enabled = true; - - if (script.contains("log")) - m_scriptedInput.log = script["log"].get() || m_scriptedInput.log; - - if (script.contains("hard_fail")) - m_scriptedInput.hardFail = script["hard_fail"].get(); - - if (script.contains("visual_debug")) - m_scriptedInput.visualDebug = script["visual_debug"].get(); - if (script.contains("visual_debug_target_fps")) - m_scriptedInput.visualTargetFps = script["visual_debug_target_fps"].get(); - if (script.contains("visual_debug_hold_seconds")) - m_scriptedInput.visualCameraHoldSeconds = script["visual_debug_hold_seconds"].get(); - if (m_scriptVisualDebugCli) - m_scriptedInput.visualDebug = true; - if (m_scriptedInput.visualDebug) - { - if (m_scriptedInput.visualTargetFps <= 0.f) - m_scriptedInput.visualTargetFps = 60.f; - if (m_scriptedInput.visualCameraHoldSeconds <= 0.f) - m_scriptedInput.visualCameraHoldSeconds = 3.f; - } - - if (script.contains("enableActiveCameraMovement")) - enableActiveCameraMovement = script["enableActiveCameraMovement"].get(); - else if (m_scriptedInput.enabled) - enableActiveCameraMovement = true; - - if (script.contains("exclusive_input")) - m_scriptedInput.exclusive = script["exclusive_input"].get() || m_scriptedInput.exclusive; - if (script.contains("exclusive")) - m_scriptedInput.exclusive = script["exclusive"].get() || m_scriptedInput.exclusive; - - if (script.contains("capture_prefix")) - m_scriptedInput.capturePrefix = script["capture_prefix"].get(); - if (m_scriptedInput.capturePrefix.empty()) - m_scriptedInput.capturePrefix = "script"; - if (script.contains("capture_frames")) - for (const auto& frame : script["capture_frames"]) - m_scriptedInput.captureFrames.emplace_back(frame.get()); - - if (script.contains("camera_controls")) - { - const auto& controls = script["camera_controls"]; - if (controls.contains("keyboard_scale")) - m_cameraControls.keyboardScale = controls["keyboard_scale"].get(); - if (controls.contains("mouse_move_scale")) - m_cameraControls.mouseMoveScale = controls["mouse_move_scale"].get(); - if (controls.contains("mouse_scroll_scale")) - m_cameraControls.mouseScrollScale = controls["mouse_scroll_scale"].get(); - if (controls.contains("translation_scale")) - m_cameraControls.translationScale = controls["translation_scale"].get(); - if (controls.contains("rotation_scale")) - m_cameraControls.rotationScale = controls["rotation_scale"].get(); - } - - if (script.contains("events")) - for (const auto& ev : script["events"]) - { - if (!ev.contains("frame") || !ev.contains("type")) - { - m_logger->log("Scripted input event missing \"frame\" or \"type\".", ILogger::ELL_WARNING); - continue; - } - - const auto frame = ev["frame"].get(); - const auto type = ev["type"].get(); - const bool captureFrame = ev.value("capture", false); - - if (type == "keyboard") - { - if (!ev.contains("key") || !ev.contains("action")) - { - m_logger->log("Scripted keyboard event missing \"key\" or \"action\".", ILogger::ELL_WARNING); - continue; - } - - const auto keyStr = ev["key"].get(); - const auto actionStr = ev["action"].get(); - const auto key = ui::stringToKeyCode(keyStr); - if (key == ui::EKC_NONE) - { - m_logger->log("Scripted keyboard event has invalid key \"%s\".", ILogger::ELL_WARNING, keyStr.c_str()); - continue; - } - - ui::SKeyboardEvent::E_KEY_ACTION action = ui::SKeyboardEvent::ECA_UNITIALIZED; - if (actionStr == "pressed" || actionStr == "press") - action = ui::SKeyboardEvent::ECA_PRESSED; - else if (actionStr == "released" || actionStr == "release") - action = ui::SKeyboardEvent::ECA_RELEASED; - - if (action == ui::SKeyboardEvent::ECA_UNITIALIZED) - { - m_logger->log("Scripted keyboard event has invalid action \"%s\".", ILogger::ELL_WARNING, actionStr.c_str()); - continue; - } - - ScriptedInputEvent entry; - entry.frame = frame; - entry.type = ScriptedInputEvent::Type::Keyboard; - entry.keyboard.key = key; - entry.keyboard.action = action; - m_scriptedInput.events.emplace_back(entry); - if (captureFrame) - m_scriptedInput.captureFrames.emplace_back(frame); - } - else if (type == "mouse") - { - if (!ev.contains("kind")) - { - m_logger->log("Scripted mouse event missing \"kind\".", ILogger::ELL_WARNING); - continue; - } - - const auto kind = ev["kind"].get(); - ScriptedInputEvent entry; - entry.frame = frame; - entry.type = ScriptedInputEvent::Type::Mouse; - - if (kind == "move") - { - entry.mouse.type = ui::SMouseEvent::EET_MOVEMENT; - entry.mouse.dx = ev.value("dx", 0); - entry.mouse.dy = ev.value("dy", 0); - } - else if (kind == "scroll") - { - entry.mouse.type = ui::SMouseEvent::EET_SCROLL; - entry.mouse.v = ev.value("v", 0); - entry.mouse.h = ev.value("h", 0); - } - else if (kind == "click") - { - if (!ev.contains("button") || !ev.contains("action")) - { - m_logger->log("Scripted click event missing \"button\" or \"action\".", ILogger::ELL_WARNING); - continue; - } - - const auto buttonStr = ev["button"].get(); - const auto actionStr = ev["action"].get(); - - ui::E_MOUSE_BUTTON button = ui::EMB_LEFT_BUTTON; - if (buttonStr == "LEFT_BUTTON") button = ui::EMB_LEFT_BUTTON; - else if (buttonStr == "RIGHT_BUTTON") button = ui::EMB_RIGHT_BUTTON; - else if (buttonStr == "MIDDLE_BUTTON") button = ui::EMB_MIDDLE_BUTTON; - else if (buttonStr == "BUTTON_4") button = ui::EMB_BUTTON_4; - else if (buttonStr == "BUTTON_5") button = ui::EMB_BUTTON_5; - else - { - m_logger->log("Scripted click event has invalid button \"%s\".", ILogger::ELL_WARNING, buttonStr.c_str()); - continue; - } - - ui::SMouseEvent::SClickEvent::E_ACTION action = ui::SMouseEvent::SClickEvent::EA_UNITIALIZED; - if (actionStr == "pressed" || actionStr == "press") - action = ui::SMouseEvent::SClickEvent::EA_PRESSED; - else if (actionStr == "released" || actionStr == "release") - action = ui::SMouseEvent::SClickEvent::EA_RELEASED; - - if (action == ui::SMouseEvent::SClickEvent::EA_UNITIALIZED) - { - m_logger->log("Scripted click event has invalid action \"%s\".", ILogger::ELL_WARNING, actionStr.c_str()); - continue; - } - - entry.mouse.type = ui::SMouseEvent::EET_CLICK; - entry.mouse.button = button; - entry.mouse.action = action; - entry.mouse.x = ev.value("x", 0); - entry.mouse.y = ev.value("y", 0); - } - else - { - m_logger->log("Scripted mouse event has invalid kind \"%s\".", ILogger::ELL_WARNING, kind.c_str()); - continue; - } - - m_scriptedInput.events.emplace_back(entry); - if (captureFrame) - m_scriptedInput.captureFrames.emplace_back(frame); - } - else if (type == "imguizmo") - { - ScriptedInputEvent entry; - entry.frame = frame; - entry.type = ScriptedInputEvent::Type::Imguizmo; - - if (ev.contains("delta_trs")) - { - const auto arr = ev["delta_trs"].get>(); - float m16[16]; - for (size_t i = 0u; i < 16u; ++i) - m16[i] = arr[i]; - entry.imguizmo = *reinterpret_cast(m16); - } - else - { - const auto t = ev.contains("translation") ? ev["translation"].get>() : std::array{0.f, 0.f, 0.f}; - const auto r = ev.contains("rotation_deg") ? ev["rotation_deg"].get>() : std::array{0.f, 0.f, 0.f}; - const auto s = ev.contains("scale") ? ev["scale"].get>() : std::array{1.f, 1.f, 1.f}; - - float m16[16]; - float tr[3] = { t[0], t[1], t[2] }; - float rot[3] = { r[0], r[1], r[2] }; - float sc[3] = { s[0], s[1], s[2] }; - - ImGuizmo::RecomposeMatrixFromComponents(tr, rot, sc, m16); - entry.imguizmo = *reinterpret_cast(m16); - } - - m_scriptedInput.events.emplace_back(entry); - if (captureFrame) - m_scriptedInput.captureFrames.emplace_back(frame); - } - else if (type == "action") - { - if (!ev.contains("action")) - { - m_logger->log("Scripted action event missing \"action\".", ILogger::ELL_WARNING); - continue; - } - - const auto actionStr = ev["action"].get(); - ScriptedInputEvent entry; - entry.frame = frame; - entry.type = ScriptedInputEvent::Type::Action; - - auto getValueInt = [&]() -> int32_t - { - if (ev.contains("value")) - return ev["value"].get(); - if (ev.contains("index")) - return ev["index"].get(); - return 0; - }; - - if (actionStr == "set_active_render_window") - { - entry.action.kind = ScriptedInputEvent::ActionData::Kind::SetActiveRenderWindow; - entry.action.value = getValueInt(); - } - else if (actionStr == "set_active_planar") - { - entry.action.kind = ScriptedInputEvent::ActionData::Kind::SetActivePlanar; - entry.action.value = getValueInt(); - } - else if (actionStr == "set_projection_type") - { - entry.action.kind = ScriptedInputEvent::ActionData::Kind::SetProjectionType; - if (ev.contains("value") && ev["value"].is_string()) - { - const auto valueStr = ev["value"].get(); - if (valueStr == "perspective") - entry.action.value = static_cast(IPlanarProjection::CProjection::Perspective); - else if (valueStr == "orthographic") - entry.action.value = static_cast(IPlanarProjection::CProjection::Orthographic); - else - { - m_logger->log("Scripted action projection type has invalid value \"%s\".", ILogger::ELL_WARNING, valueStr.c_str()); - continue; - } - } - else - { - entry.action.value = getValueInt(); - } - } - else if (actionStr == "set_projection_index") - { - entry.action.kind = ScriptedInputEvent::ActionData::Kind::SetProjectionIndex; - entry.action.value = getValueInt(); - } - else if (actionStr == "set_use_window") - { - entry.action.kind = ScriptedInputEvent::ActionData::Kind::SetUseWindow; - entry.action.value = ev.value("value", false) ? 1 : 0; - } - else if (actionStr == "set_left_handed") - { - entry.action.kind = ScriptedInputEvent::ActionData::Kind::SetLeftHanded; - entry.action.value = ev.value("value", false) ? 1 : 0; - } - else - { - m_logger->log("Scripted action event has invalid action \"%s\".", ILogger::ELL_WARNING, actionStr.c_str()); - continue; - } - - m_scriptedInput.events.emplace_back(entry); - if (captureFrame) - m_scriptedInput.captureFrames.emplace_back(frame); - } - else - { - m_logger->log("Scripted input event has invalid type \"%s\".", ILogger::ELL_WARNING, type.c_str()); - } - } - - if (script.contains("checks")) - { - for (const auto& chk : script["checks"]) - { - if (!chk.contains("frame") || !chk.contains("kind")) - { - m_logger->log("Scripted check missing \"frame\" or \"kind\".", ILogger::ELL_WARNING); - continue; - } - - const auto frame = chk["frame"].get(); - const auto kind = chk["kind"].get(); - - ScriptedInputCheck entry; - entry.frame = frame; - - if (kind == "baseline") - { - entry.kind = ScriptedInputCheck::Kind::Baseline; - } - else if (kind == "imguizmo_virtual") - { - entry.kind = ScriptedInputCheck::Kind::ImguizmoVirtual; - entry.tolerance = chk.value("tolerance", entry.tolerance); - - if (!chk.contains("events")) - { - m_logger->log("Imguizmo virtual check missing \"events\".", ILogger::ELL_WARNING); - continue; - } - - for (const auto& ev : chk["events"]) - { - if (!ev.contains("type") || !ev.contains("magnitude")) - { - m_logger->log("Imguizmo virtual check event missing \"type\" or \"magnitude\".", ILogger::ELL_WARNING); - continue; - } - - const auto typeStr = ev["type"].get(); - const auto type = CVirtualGimbalEvent::stringToVirtualEvent(typeStr); - if (type == CVirtualGimbalEvent::None) - { - m_logger->log("Imguizmo virtual check event has invalid type \"%s\".", ILogger::ELL_WARNING, typeStr.c_str()); - continue; - } - - ScriptedInputCheck::ExpectedVirtualEvent expected; - expected.type = type; - expected.magnitude = ev["magnitude"].get(); - entry.expectedVirtualEvents.emplace_back(expected); - } - } - else if (kind == "gimbal_near") - { - entry.kind = ScriptedInputCheck::Kind::GimbalNear; - entry.posTolerance = chk.value("pos_tolerance", entry.posTolerance); - entry.eulerToleranceDeg = chk.value("euler_tolerance_deg", entry.eulerToleranceDeg); - - if (chk.contains("position")) - { - const auto pos = chk["position"].get>(); - entry.expectedPos = float32_t3(pos[0], pos[1], pos[2]); - entry.hasExpectedPos = true; - } - if (chk.contains("euler_deg")) - { - const auto euler = chk["euler_deg"].get>(); - entry.expectedEulerDeg = float32_t3(euler[0], euler[1], euler[2]); - entry.hasExpectedEuler = true; - } - } - else if (kind == "gimbal_delta") - { - entry.kind = ScriptedInputCheck::Kind::GimbalDelta; - entry.posTolerance = chk.value("pos_tolerance", entry.posTolerance); - entry.eulerToleranceDeg = chk.value("euler_tolerance_deg", entry.eulerToleranceDeg); - } - else if (kind == "gimbal_step") - { - entry.kind = ScriptedInputCheck::Kind::GimbalStep; - - if (chk.contains("min_pos_delta")) - { - entry.minPosDelta = chk["min_pos_delta"].get(); - entry.hasPosDeltaConstraint = true; - } - if (chk.contains("max_pos_delta")) - { - entry.posTolerance = chk["max_pos_delta"].get(); - entry.hasPosDeltaConstraint = true; - } - else if (chk.contains("pos_tolerance")) - { - entry.posTolerance = chk["pos_tolerance"].get(); - entry.hasPosDeltaConstraint = true; - } - - if (chk.contains("min_euler_delta_deg")) - { - entry.minEulerDeltaDeg = chk["min_euler_delta_deg"].get(); - entry.hasEulerDeltaConstraint = true; - } - if (chk.contains("max_euler_delta_deg")) - { - entry.eulerToleranceDeg = chk["max_euler_delta_deg"].get(); - entry.hasEulerDeltaConstraint = true; - } - else if (chk.contains("euler_tolerance_deg")) - { - entry.eulerToleranceDeg = chk["euler_tolerance_deg"].get(); - entry.hasEulerDeltaConstraint = true; - } - - if (!entry.hasPosDeltaConstraint && !entry.hasEulerDeltaConstraint) - { - m_logger->log("gimbal_step check requires at least one delta constraint.", ILogger::ELL_WARNING); - continue; - } - } - else - { - m_logger->log("Scripted check has invalid kind \"%s\".", ILogger::ELL_WARNING, kind.c_str()); - continue; - } - - m_scriptedInput.checks.emplace_back(entry); - } - } - - std::sort(m_scriptedInput.events.begin(), m_scriptedInput.events.end(), - [](const ScriptedInputEvent& a, const ScriptedInputEvent& b) { return a.frame < b.frame; }); - std::sort(m_scriptedInput.checks.begin(), m_scriptedInput.checks.end(), - [](const ScriptedInputCheck& a, const ScriptedInputCheck& b) { return a.frame < b.frame; }); - if (!m_scriptedInput.captureFrames.empty()) - { - std::sort(m_scriptedInput.captureFrames.begin(), m_scriptedInput.captureFrames.end()); - m_scriptedInput.captureFrames.erase(std::unique(m_scriptedInput.captureFrames.begin(), m_scriptedInput.captureFrames.end()), m_scriptedInput.captureFrames.end()); - } - }; - - if (program.is_used("--script")) - { - system::path scriptPath = program.get("--script"); - if (scriptPath.is_relative()) - scriptPath = localInputCWD / scriptPath; - json scriptJson; - if (!loadScriptJson(scriptPath.string(), scriptJson)) - return false; - parseScriptedInput(scriptJson); - } - else if (j.contains("scripted_input")) - { - parseScriptedInput(j["scripted_input"]); - } - - std::vector> cameras; - for (const auto& jCamera : j["cameras"]) - { - if (jCamera.contains("type")) - { - if (!jCamera.contains("position")) - { - logFail("Expected \"position\" keyword for camera definition!"); - return false; - } - - const bool withOrientation = jCamera.contains("orientation"); - - auto position = [&]() - { - auto jret = jCamera["position"].get>(); - return float32_t3(jret[0], jret[1], jret[2]); - }(); - - auto getOrientation = [&]() - { - auto jret = jCamera["orientation"].get>(); - - // order important for glm::quat, - // the ctor is GLM_FUNC_QUALIFIER GLM_CONSTEXPR qua::qua(T _w, T _x, T _y, T _z) - // but memory layout (and json) is x,y,z,w - return glm::quat(jret[3], jret[0], jret[1], jret[2]); - }; - - auto getTarget = [&]() - { - auto jret = jCamera["target"].get>(); - return float32_t3(jret[0], jret[1], jret[2]); - }; - - constexpr float DefaultMoveScale = 0.01f; - constexpr float DefaultRotateScale = 0.003f; - constexpr float OrbitMoveScale = 0.5f; - - if (jCamera["type"] == "FPS") - { - if (!withOrientation) - { - logFail("Expected \"orientation\" keyword for FPS camera definition!"); - return false; - } - - auto camera = make_smart_refctd_ptr(position, getOrientation()); - camera->setMoveSpeedScale(DefaultMoveScale); - camera->setRotationSpeedScale(DefaultRotateScale); - cameras.emplace_back(std::move(camera)); - } - else if (jCamera["type"] == "Free") - { - if (!withOrientation) - { - logFail("Expected \"orientation\" keyword for Free camera definition!"); - return false; - } - - auto camera = make_smart_refctd_ptr(position, getOrientation()); - camera->setMoveSpeedScale(DefaultMoveScale); - camera->setRotationSpeedScale(DefaultRotateScale); - cameras.emplace_back(std::move(camera)); - } - else if (jCamera["type"] == "Orbit") - { - auto camera = make_smart_refctd_ptr(position, getTarget()); - camera->setMoveSpeedScale(OrbitMoveScale); - camera->setRotationSpeedScale(DefaultRotateScale); - cameras.emplace_back(std::move(camera)); - } - else if (jCamera["type"] == "Arcball") - { - auto camera = make_smart_refctd_ptr(position, getTarget()); - camera->setMoveSpeedScale(OrbitMoveScale); - camera->setRotationSpeedScale(DefaultRotateScale); - cameras.emplace_back(std::move(camera)); - } - else if (jCamera["type"] == "Turntable") - { - auto camera = make_smart_refctd_ptr(position, getTarget()); - camera->setMoveSpeedScale(OrbitMoveScale); - camera->setRotationSpeedScale(DefaultRotateScale); - cameras.emplace_back(std::move(camera)); - } - else if (jCamera["type"] == "TopDown") - { - auto camera = make_smart_refctd_ptr(position, getTarget()); - camera->setMoveSpeedScale(OrbitMoveScale); - camera->setRotationSpeedScale(DefaultRotateScale); - cameras.emplace_back(std::move(camera)); - } - else if (jCamera["type"] == "Isometric") - { - auto camera = make_smart_refctd_ptr(position, getTarget()); - camera->setMoveSpeedScale(OrbitMoveScale); - camera->setRotationSpeedScale(DefaultRotateScale); - cameras.emplace_back(std::move(camera)); - } - else if (jCamera["type"] == "Chase") - { - auto camera = make_smart_refctd_ptr(position, getTarget()); - camera->setMoveSpeedScale(OrbitMoveScale); - camera->setRotationSpeedScale(DefaultRotateScale); - cameras.emplace_back(std::move(camera)); - } - else if (jCamera["type"] == "Dolly") - { - auto camera = make_smart_refctd_ptr(position, getTarget()); - camera->setMoveSpeedScale(OrbitMoveScale); - camera->setRotationSpeedScale(DefaultRotateScale); - cameras.emplace_back(std::move(camera)); - } - else if (jCamera["type"] == "DollyZoom") - { - float baseFov = 40.0f; - if (jCamera.contains("baseFov")) - baseFov = jCamera["baseFov"].get(); - - auto camera = make_smart_refctd_ptr(position, getTarget(), baseFov); - camera->setMoveSpeedScale(OrbitMoveScale); - camera->setRotationSpeedScale(DefaultRotateScale); - cameras.emplace_back(std::move(camera)); - } - else if (jCamera["type"] == "Path") - { - auto camera = make_smart_refctd_ptr(position, getTarget()); - camera->setMoveSpeedScale(OrbitMoveScale); - camera->setRotationSpeedScale(DefaultRotateScale); - cameras.emplace_back(std::move(camera)); - } - else - { - logFail("Unsupported camera type!"); - return false; - } - } - else - { - logFail("Expected \"type\" keyword for camera definition!"); - return false; - } - } - - std::vector projections; - for (const auto& jProjection : j["projections"]) - { - if (jProjection.contains("type")) - { - float zNear, zFar; - - if (!jProjection.contains("zNear")) - { - logFail("Expected \"zNear\" keyword for planar projection definition!"); - return false; - } - - if (!jProjection.contains("zFar")) - { - logFail("Expected \"zFar\" keyword for planar projection definition!"); - return false; - } - - zNear = jProjection["zNear"].get(); - zFar = jProjection["zFar"].get(); - - if (jProjection["type"] == "perspective") - { - if (!jProjection.contains("fov")) - { - logFail("Expected \"fov\" keyword for planar perspective projection definition!"); - return false; - } - - float fov = jProjection["fov"].get(); - projections.emplace_back(IPlanarProjection::CProjection::create(zNear, zFar, fov)); - } - else if (jProjection["type"] == "orthographic") - { - if (!jProjection.contains("orthoWidth")) - { - logFail("Expected \"orthoWidth\" keyword for planar orthographic projection definition!"); - return false; - } - - float orthoWidth = jProjection["orthoWidth"].get(); - projections.emplace_back(IPlanarProjection::CProjection::create(zNear, zFar, orthoWidth)); - } - else - { - logFail("Unsupported projection!"); - return false; - } - } - } - - struct - { - std::vector keyboard; - std::vector mouse; - } controllers; - - if (j.contains("controllers")) - { - const auto& jControllers = j["controllers"]; - - if (jControllers.contains("keyboard")) - { - for (const auto& jKeyboard : jControllers["keyboard"]) - { - if (jKeyboard.contains("mappings")) - { - auto& controller = controllers.keyboard.emplace_back(); - for (const auto& [key, value] : jKeyboard["mappings"].items()) - { - const auto nativeCode = stringToKeyCode(key.c_str()); - - if (nativeCode == EKC_NONE) - { - logFail("Invalid native key \"%s\" code mapping for keyboard controller", key.c_str()); - return false; - } - - controller[nativeCode] = CVirtualGimbalEvent::stringToVirtualEvent(value.get()); - } - } - else - { - logFail("Expected \"mappings\" keyword for keyboard controller definition!"); - return false; - } - } - } - else - { - logFail("Expected \"keyboard\" keyword in controllers definition!"); - return false; - } - - if (jControllers.contains("mouse")) - { - for (const auto& jMouse : jControllers["mouse"]) - { - if (jMouse.contains("mappings")) - { - auto& controller = controllers.mouse.emplace_back(); - for (const auto& [key, value] : jMouse["mappings"].items()) - { - const auto nativeCode = stringToMouseCode(key.c_str()); - - if (nativeCode == EMC_NONE) - { - logFail("Invalid native key \"%s\" code mapping for mouse controller", key.c_str()); - return false; - } - - controller[nativeCode] = CVirtualGimbalEvent::stringToVirtualEvent(value.get()); - } - } - else - { - logFail("Expected \"mappings\" keyword for mouse controller definition!"); - return false; - } - } - } - else - { - logFail("Expected \"mouse\" keyword in controllers definition"); - return false; - } - } - else - { - logFail("Expected \"controllers\" keyword in controllers JSON"); - return false; - } - - if (j.contains("viewports") && j.contains("planars")) - { - for (const auto& jPlanar : j["planars"]) - { - if (!jPlanar.contains("camera")) - { - logFail("Expected \"camera\" value in planar object"); - return false; - } - - if (!jPlanar.contains("viewports")) - { - logFail("Expected \"viewports\" list in planar object"); - return false; - } - - const auto cameraIx = jPlanar["camera"].get(); - auto boundViewports = jPlanar["viewports"].get>(); - - auto& planar = m_planarProjections.emplace_back() = planar_projection_t::create(smart_refctd_ptr(cameras[cameraIx])); - for (const auto viewportIx : boundViewports) - { - auto& viewport = j["viewports"][viewportIx]; - if (!viewport.contains("projection") || !viewport.contains("controllers")) - { - logFail("\"projection\" or \"controllers\" missing in viewport object index %d", viewportIx); - return false; - } - - const auto projectionIx = viewport["projection"].get(); - auto& projection = planar->getPlanarProjections().emplace_back(projections[projectionIx]); - - const bool hasKeyboardBound = viewport["controllers"].contains("keyboard"); - const bool hasMouseBound = viewport["controllers"].contains("mouse"); - - if (hasKeyboardBound) - { - auto keyboardControllerIx = viewport["controllers"]["keyboard"].get(); - projection.updateKeyboardMapping([&](auto& map) { map = controllers.keyboard[keyboardControllerIx]; }); - } - else - projection.updateKeyboardMapping([&](auto& map) { map = {}; }); // clean the map if not bound - - if (hasMouseBound) - { - auto mouseControllerIx = viewport["controllers"]["mouse"].get(); - projection.updateMouseMapping([&](auto& map) { map = controllers.mouse[mouseControllerIx]; }); - } - else - projection.updateMouseMapping([&](auto& map) { map = {}; }); // clean the map if not bound - } - - { - auto* camera = planar->getCamera(); - { - camera->updateKeyboardMapping([&](auto& map) { map = camera->getKeyboardMappingPreset(); }); - camera->updateMouseMapping([&](auto& map) { map = camera->getMouseMappingPreset(); }); - camera->updateImguizmoMapping([&](auto& map) { map = camera->getImguizmoMappingPreset(); }); - } - } - } - } - else - { - logFail("Expected \"viewports\" and \"planars\" lists in JSON"); - return false; - } - - if (m_planarProjections.empty()) - { - logFail("Expected at least 1 planar"); - return false; - } - - // init render window planar references - we make all render windows start with focus on first - // planar but in a way that first window has the planar's perspective preset bound & second orthographic - for (uint32_t i = 0u; i < windowBindings.size(); ++i) - { - auto& binding = windowBindings[i]; - - auto& planar = m_planarProjections[binding.activePlanarIx = 0]; - binding.pickDefaultProjections(planar->getPlanarProjections()); - - if (i) - binding.boundProjectionIx = binding.lastBoundOrthoPresetProjectionIx.value(); - else - binding.boundProjectionIx = binding.lastBoundPerspectivePresetProjectionIx.value(); - } - } - - // Create asset manager - m_assetManager = make_smart_refctd_ptr(smart_refctd_ptr(m_system)); - - // First create the resources that don't depend on a swapchain - m_semaphore = m_device->createSemaphore(m_realFrameIx); - if (!m_semaphore) - return logFail("Failed to Create a Semaphore!"); - - // The nice thing about having a triple buffer is that you don't need to do acrobatics to account for the formats available to the surface. - // You can transcode to the swapchain's format while copying, and I actually recommend to do surface rotation, tonemapping and OETF application there. - const auto format = asset::EF_R8G8B8A8_SRGB; - // Could be more clever and use the copy Triple Buffer to Swapchain as an opportunity to do a MSAA resolve or something - const auto samples = IGPUImage::ESCF_1_BIT; - - // Create the renderpass - { - 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, // because we clear we don't care about contents when we grab the triple buffer img again - /*.finalLayout = */IGPUImage::LAYOUT::TRANSFER_SRC_OPTIMAL // put it already in the correct layout for the blit operation - }}, - IGPURenderpass::SCreationParams::ColorAttachmentsEnd - }; - IGPURenderpass::SCreationParams::SSubpassDescription subpasses[] = { - {}, - IGPURenderpass::SCreationParams::SubpassesEnd - }; - subpasses[0].colorAttachments[0] = { .render = {.attachmentIndex = 0,.layout = IGPUImage::LAYOUT::ATTACHMENT_OPTIMAL} }; - // We actually need external dependencies to ensure ordering of the Implicit Layout Transitions relative to the semaphore signals - IGPURenderpass::SCreationParams::SSubpassDependency dependencies[] = { - // wipe-transition to ATTACHMENT_OPTIMAL - { - .srcSubpass = IGPURenderpass::SCreationParams::SSubpassDependency::External, - .dstSubpass = 0, - .memoryBarrier = { - // we can have NONE as Sources because the semaphore wait is ALL_COMMANDS - // https://github.com/KhronosGroup/Vulkan-Docs/issues/2319 - .dstStageMask = asset::PIPELINE_STAGE_FLAGS::COLOR_ATTACHMENT_OUTPUT_BIT, - .dstAccessMask = asset::ACCESS_FLAGS::COLOR_ATTACHMENT_WRITE_BIT - } - // leave view offsets and flags default - }, - // ATTACHMENT_OPTIMAL to PRESENT_SRC - { - .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 - // we can have NONE as the Destinations because the semaphore signal is ALL_COMMANDS - // https://github.com/KhronosGroup/Vulkan-Docs/issues/2319 - } - // leave view offsets and flags default - }, - IGPURenderpass::SCreationParams::DependenciesEnd - }; - - IGPURenderpass::SCreationParams params = {}; - params.colorAttachments = colorAttachments; - params.subpasses = subpasses; - params.dependencies = dependencies; - m_renderpass = m_device->createRenderpass(params); - if (!m_renderpass) - return logFail("Failed to Create a Renderpass!"); - } - - // We just live life in easy mode and have the Swapchain Creation Parameters get deduced from the surface. - // We don't need any control over the format of the swapchain because we'll be only using Renderpasses this time! - // TODO: improve the queue allocation/choice and allocate a dedicated presentation queue to improve responsiveness and race to present. - ISwapchain::SSharedCreationParams sharedParams = {}; - sharedParams.imageUsage |= IGPUImage::EUF_TRANSFER_SRC_BIT; - auto swapchainResources = std::make_unique(); - if (!m_surface || !m_surface->init(m_surface->pickQueue(m_device.get()), std::move(swapchainResources), sharedParams)) - return logFail("Failed to Create a Swapchain!"); - - // Normally you'd want to recreate these images whenever the swapchain is resized in some increment, like 64 pixels or something. - // But I'm super lazy here and will just create "worst case sized images" and waste all the VRAM I can get. - const auto dpyInfo = m_winMgr->getPrimaryDisplayInfo(); - for (auto i = 0; i < MaxFramesInFlight; i++) - { - auto& image = m_tripleBuffers[i]; - { - IGPUImage::SCreationParams params = {}; - params = asset::IImage::SCreationParams{ - .type = IGPUImage::ET_2D, - .samples = samples, - .format = format, - .extent = {dpyInfo.resX,dpyInfo.resY,1}, - .mipLevels = 1, - .arrayLayers = 1, - .flags = IGPUImage::ECF_NONE, - // in this example I'll be using a renderpass to clear the image, and then a blit to copy it to the swapchain - .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!"); - - // use dedicated allocations, we have plenty of allocations left, even on Win32 - 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()); - - // create framebuffers for the images - { - auto imageView = m_device->createImageView({ - .flags = IGPUImageView::ECF_NONE, - // give it a Transfer SRC usage flag so we can transition to the Tranfer SRC layout with End Renderpass - .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); - } - } - - // This time we'll create all CommandBuffers from one CommandPool, to keep life simple. However the Pool must support individually resettable CommandBuffers - // because they cannot be pre-recorded because the fraembuffers/swapchain images they use will change when a swapchain recreates. - 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!"); - - // UI - { - { - constexpr std::array ImGuiStreamingBufferSizes = { - 32ull * 1024ull * 1024ull, - 16ull * 1024ull * 1024ull, - 8ull * 1024ull * 1024ull, - 4ull * 1024ull * 1024ull, - 2ull * 1024ull * 1024ull - }; - auto createImGuiStreamingBuffer = [&](size_t size) -> smart_refctd_ptr - { - constexpr uint32_t minStreamingBufferAllocationSize = 128u; - constexpr uint32_t maxStreamingBufferAllocationAlignment = 4096u; - - auto getRequiredAccessFlags = [&](const bitflag& properties) - { - bitflag flags(IDeviceMemoryAllocation::EMCAF_NO_MAPPING_ACCESS); - - if (properties.hasFlags(IDeviceMemoryAllocation::EMPF_HOST_READABLE_BIT)) - flags |= IDeviceMemoryAllocation::EMCAF_READ; - if (properties.hasFlags(IDeviceMemoryAllocation::EMPF_HOST_WRITABLE_BIT)) - flags |= IDeviceMemoryAllocation::EMCAF_WRITE; - - return flags; - }; - - IGPUBuffer::SCreationParams mdiCreationParams = {}; - mdiCreationParams.usage = nbl::ext::imgui::UI::SCachedCreationParams::RequiredUsageFlags; - mdiCreationParams.size = size; - - auto buffer = m_utils->getLogicalDevice()->createBuffer(std::move(mdiCreationParams)); - if (!buffer) - { - m_logger->log("Failed to create ImGui streaming buffer object for size=%zu.", ILogger::ELL_WARNING, size); - return nullptr; - } - - buffer->setObjectDebugName("ImGui MDI Upstream Buffer"); - - auto memoryReqs = buffer->getMemoryReqs(); - const auto upStreamingBits = m_utils->getLogicalDevice()->getPhysicalDevice()->getUpStreamingMemoryTypeBits(); - const auto reqMemoryTypeBits = memoryReqs.memoryTypeBits; - memoryReqs.memoryTypeBits &= upStreamingBits; - if (!memoryReqs.memoryTypeBits) - { - m_logger->log("No compatible up-streaming memory type for ImGui buffer size=%zu reqBits=0x%08x upBits=0x%08x.", ILogger::ELL_WARNING, size, reqMemoryTypeBits, upStreamingBits); - return nullptr; - } - - auto allocation = m_utils->getLogicalDevice()->allocate(memoryReqs, buffer.get(), nbl::ext::imgui::UI::SCachedCreationParams::RequiredAllocateFlags); - if (!allocation.isValid()) - { - m_logger->log("Failed to allocate ImGui streaming buffer memory for size=%zu reqBits=0x%08x upBits=0x%08x filteredBits=0x%08x sizeReq=%llu.", ILogger::ELL_WARNING, size, reqMemoryTypeBits, upStreamingBits, memoryReqs.memoryTypeBits, memoryReqs.size); - return nullptr; - } - - auto memory = allocation.memory; - - if (!memory->map({ 0ull, memoryReqs.size }, getRequiredAccessFlags(memory->getMemoryPropertyFlags()))) - { - m_logger->log("Could not map ImGui streaming buffer memory for size=%zu.", ILogger::ELL_WARNING, size); - return nullptr; - } - - return make_smart_refctd_ptr( - SBufferRange{0ull, mdiCreationParams.size, std::move(buffer)}, - maxStreamingBufferAllocationAlignment, - minStreamingBufferAllocationSize); - }; - - smart_refctd_ptr imguiStreamingBuffer = nullptr; - for (const auto candidateSize : ImGuiStreamingBufferSizes) - { - imguiStreamingBuffer = createImGuiStreamingBuffer(candidateSize); - if (imguiStreamingBuffer) - break; - } - if (!imguiStreamingBuffer) - return logFail("Failed to create ImGui streaming buffer."); - - nbl::ext::imgui::UI::SCreationParameters params; - params.resources.texturesInfo = { .setIx = 0u, .bindingIx = 0u }; - params.resources.samplersInfo = { .setIx = 0u, .bindingIx = 1u }; - params.assetManager = m_assetManager; - 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.streamingBuffer = std::move(imguiStreamingBuffer); - params.subpassIx = 0u; - params.transfer = getTransferUpQueue(); - params.utilities = m_utils; - - auto loadPrecompiledShader = [&](const std::string_view key) -> smart_refctd_ptr - { - IAssetLoader::SAssetLoadParams loadParams = {}; - loadParams.logger = m_logger.get(); - loadParams.workingDirectory = "app_resources"; - auto bundle = m_assetManager->getAsset(key.data(), loadParams); - const auto& contents = bundle.getContents(); - if (contents.empty()) - return nullptr; - return IAsset::castDown(contents[0]); - }; - - const auto vertexKey = nbl::this_example::builtin::build::get_spirv_key<"imgui_vertex">(m_device.get()); - const auto fragmentKey = nbl::this_example::builtin::build::get_spirv_key<"imgui_fragment">(m_device.get()); - auto vertexShader = loadPrecompiledShader(vertexKey.data()); - auto fragmentShader = loadPrecompiledShader(fragmentKey.data()); - if (!vertexShader || !fragmentShader) - return logFail("Failed to load precompiled ImGui shaders."); - - params.spirv = nbl::ext::imgui::UI::SCreationParameters::PrecompiledShaders{ - .vertex = std::move(vertexShader), - .fragment = std::move(fragmentShader) - }; - - m_ui.manager = nbl::ext::imgui::UI::create(std::move(params)); - } - - if (!m_ui.manager) - return false; - - // note that we use default layout provided by our extension, but you are free to create your own by filling nbl::ext::imgui::UI::S_CREATION_PARAMETERS::resources - const auto* descriptorSetLayout = m_ui.manager->getPipeline()->getLayout()->getDescriptorSetLayout(0u); - - IDescriptorPool::SCreateInfo descriptorPoolInfo = {}; - descriptorPoolInfo.maxDescriptorCount[static_cast(asset::IDescriptor::E_TYPE::ET_SAMPLER)] = (uint32_t)nbl::ext::imgui::UI::DefaultSamplerIx::COUNT; - descriptorPoolInfo.maxDescriptorCount[static_cast(asset::IDescriptor::E_TYPE::ET_SAMPLED_IMAGE)] = TotalUISampleTexturesAmount; - descriptorPoolInfo.maxSets = 1u; - descriptorPoolInfo.flags = IDescriptorPool::E_CREATE_FLAGS::ECF_UPDATE_AFTER_BIND_BIT; - - m_descriptorSetPool = m_device->createDescriptorPool(std::move(descriptorPoolInfo)); - assert(m_descriptorSetPool); - - m_descriptorSetPool->createDescriptorSets(1u, &descriptorSetLayout, &m_ui.descriptorSet); - assert(m_ui.descriptorSet); - - m_ui.manager->registerListener([this]() -> void { imguiListen(); }); - { - const auto ds = float32_t2{ m_window->getWidth(), m_window->getHeight() }; - - wInit.trsEditor.iPos = iPaddingOffset; - wInit.trsEditor.iSize = { 0.0f, ds.y - wInit.trsEditor.iPos.y * 2 }; - - const float panelWidth = std::clamp(ds.x * 0.33f, 380.0f, ds.x * 0.48f); - wInit.planars.iSize = { panelWidth, ds.y - iPaddingOffset.y * 2 }; - wInit.planars.iPos = { ds.x - wInit.planars.iSize.x - iPaddingOffset.x, 0 + iPaddingOffset.y }; - - { - const float renderPaddingX = 0.0f; - const float renderPaddingY = 0.0f; - const float splitGap = 4.0f; - float leftX = renderPaddingX; - float eachXSize = std::max(0.0f, ds.x - 2.0f * renderPaddingX); - float eachYSize = (ds.y - 2.0f * renderPaddingY - (wInit.renderWindows.size() - 1) * splitGap) / wInit.renderWindows.size(); - - for (size_t i = 0; i < wInit.renderWindows.size(); ++i) - { - auto& rw = wInit.renderWindows[i]; - rw.iPos = { leftX, renderPaddingY + i * (eachYSize + splitGap) }; - rw.iSize = { eachXSize, eachYSize }; - } - } - } - } - - // Geometry Creator Render Scene FBOs - { - const uint32_t addtionalBufferOwnershipFamilies[] = { getGraphicsQueue()->getFamilyIndex() }; - m_scene = CGeometryCreatorScene::create( - { - .transferQueue = getTransferUpQueue(), - .utilities = m_utils.get(), - .logger = m_logger.get(), - .addtionalBufferOwnershipFamilies = addtionalBufferOwnershipFamilies - }, - CSimpleDebugRenderer::DefaultPolygonGeometryPatch - ); - - if (!m_scene) - return logFail("Could not create geometry creator scene!"); - - { - 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 - }}, - 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[] = { - { - .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 = {}; - m_sceneRenderpass = m_device->createRenderpass(std::move(params)); - if (!m_sceneRenderpass) - return logFail("Failed to create Scene Renderpass!"); - } - - const auto& geometries = m_scene->getInitParams().geometries; - if (geometries.empty()) - return logFail("No geometries found for scene!"); - m_renderer = CSimpleDebugRenderer::create(m_assetManager.get(), m_sceneRenderpass.get(), 0, { &geometries.front().get(), geometries.size() }); - if (!m_renderer) - return logFail("Failed to create debug renderer!"); - - { - 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++; - } - } - m_renderer->m_instances.resize(1); - - const auto dpyInfo = m_winMgr->getPrimaryDisplayInfo(); - for (uint32_t i = 0u; i < windowBindings.size(); ++i) - { - auto& binding = windowBindings[i]; - binding.sceneColorView = createAttachmentView(m_device.get(), finalSceneRenderFormat, dpyInfo.resX, dpyInfo.resY, "UI Scene Color Attachment"); - binding.sceneDepthView = createAttachmentView(m_device.get(), sceneRenderDepthFormat, dpyInfo.resX, dpyInfo.resY, "UI Scene Depth Attachment"); - binding.sceneFramebuffer = createSceneFramebuffer(m_device.get(), m_sceneRenderpass.get(), binding.sceneColorView.get(), binding.sceneDepthView.get()); - if (!binding.sceneFramebuffer) - return logFail("Could not create geometry creator scene[%d]!", i); - } - } - - 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; - } - - bool updateGUIDescriptorSet() - { - // UI texture atlas + our camera scene textures, note we don't create info & write pair for the font sampler because UI extension's is immutable and baked into DS layout - static std::array descriptorInfo; - static IGPUDescriptorSet::SWriteDescriptorSet writes[TotalUISampleTexturesAmount]; - - descriptorInfo[nbl::ext::imgui::UI::FontAtlasTexId].info.image.imageLayout = IImage::LAYOUT::READ_ONLY_OPTIMAL; - descriptorInfo[nbl::ext::imgui::UI::FontAtlasTexId].desc = core::smart_refctd_ptr(m_ui.manager->getFontAtlasView()); - writes[nbl::ext::imgui::UI::FontAtlasTexId].info = descriptorInfo.data() + nbl::ext::imgui::UI::FontAtlasTexId; - - for (uint32_t i = 0; i < windowBindings.size(); ++i) - { - const auto textureIx = i + 1u; - - descriptorInfo[textureIx].info.image.imageLayout = IImage::LAYOUT::READ_ONLY_OPTIMAL; - descriptorInfo[textureIx].desc = windowBindings[i].sceneColorView; - - writes[textureIx].info = descriptorInfo.data() + textureIx; - writes[textureIx].info = descriptorInfo.data() + textureIx; - } - - for (uint32_t i = 0; i < descriptorInfo.size(); ++i) - { - writes[i].dstSet = m_ui.descriptorSet.get(); - writes[i].binding = 0u; - writes[i].arrayElement = i; - writes[i].count = 1u; - } - - return m_device->updateDescriptorSets(writes, {}); - } - - inline void workLoopBody() override - { - paceScriptedVisualDebugFrame(); - - // framesInFlight: ensuring safe execution of command buffers and acquires, `framesInFlight` only affect semaphore waits, don't use this to index your resources because it can change with swapchain recreation. - const uint32_t framesInFlight = core::min(MaxFramesInFlight, m_surface->getMaxAcquiresInFlight()); - // We block for semaphores for 2 reasons here: - // A) Resource: Can't use resource like a command buffer BEFORE previous use is finished! [MaxFramesInFlight] - // B) Acquire: Can't have more acquires in flight than a certain threshold returned by swapchain or your surface helper class. [MaxAcquiresInFlight] - if (m_realFrameIx >= framesInFlight) - { - const ISemaphore::SWaitInfo cmdbufDonePending[] = { - { - .semaphore = m_semaphore.get(), - .value = m_realFrameIx + 1 - framesInFlight - } - }; - if (m_device->blockForSemaphores(cmdbufDonePending) != ISemaphore::WAIT_RESULT::SUCCESS) - return; - } - - // Predict size of next render, and bail if nothing to do - const auto currentSwapchainExtent = m_surface->getCurrentExtent(); - if (currentSwapchainExtent.width * currentSwapchainExtent.height <= 0) - return; - // The extent of the swapchain might change between now and `present` but the blit should adapt nicely - const VkRect2D currentRenderArea = { .offset = {0,0},.extent = currentSwapchainExtent }; - - // You explicitly should not use `getAcquireCount()` see the comment on `m_realFrameIx` - const auto resourceIx = m_realFrameIx % MaxFramesInFlight; - - // We will be using this command buffer to produce the frame - auto frame = m_tripleBuffers[resourceIx].get(); - auto cmdbuf = m_cmdBufs[resourceIx].get(); - - // update CPU stuff - controllers, events, UI state - update(); - - bool willSubmit = true; - { - willSubmit &= cmdbuf->reset(IGPUCommandBuffer::RESET_FLAGS::RELEASE_RESOURCES_BIT); - willSubmit &= cmdbuf->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT); - willSubmit &= cmdbuf->beginDebugMarker("UIApp Frame"); - - auto renderScene = [&](windowControlBinding& binding) - { - if (!binding.sceneFramebuffer) - return; - - const auto& fbParams = binding.sceneFramebuffer->getCreationParameters(); - const VkRect2D renderArea = { .offset = {0,0}, .extent = {fbParams.width, fbParams.height} }; - const IGPUCommandBuffer::SRenderpassBeginInfo info = { - .framebuffer = binding.sceneFramebuffer.get(), - .colorClearValues = &SceneClearColor, - .depthStencilClearValues = &SceneClearDepth, - .renderArea = renderArea - }; - - willSubmit &= cmdbuf->beginRenderPass(info, IGPUCommandBuffer::SUBPASS_CONTENTS::INLINE); - { - asset::SViewport viewport = {}; - viewport.minDepth = 1.f; - viewport.maxDepth = 0.f; - viewport.x = 0u; - viewport.y = 0u; - viewport.width = fbParams.width; - viewport.height = fbParams.height; - - willSubmit &= cmdbuf->setViewport(0u, 1u, &viewport); - willSubmit &= cmdbuf->setScissor(0u, 1u, &renderArea); - - const auto viewParams = CSimpleDebugRenderer::SViewParams(binding.viewMatrix, binding.viewProjMatrix); - m_renderer->render(cmdbuf, viewParams); - - } - willSubmit &= cmdbuf->endRenderPass(); - }; - - if (m_renderer && !m_renderer->m_instances.empty()) - { - auto& instance = m_renderer->m_instances[0]; - instance.world = m_model; - const auto geomCount = m_renderer->getGeometries().size(); - if (geomCount) - { - if (gcIndex >= geomCount) - gcIndex = 0; - instance.packedGeo = m_renderer->getGeometries().data() + gcIndex; - } - } - - if (useWindow) - for (auto& binding : windowBindings) - renderScene(binding); - else - renderScene(windowBindings[activeRenderWindowIx]); - - const IGPUCommandBuffer::SClearColorValue clearValue = { .float32 = {0.f,0.f,0.f,1.f} }; - const IGPUCommandBuffer::SRenderpassBeginInfo info = { - .framebuffer = m_framebuffers[resourceIx].get(), - .colorClearValues = &clearValue, - .depthStencilClearValues = nullptr, - .renderArea = currentRenderArea - }; - - // UI renderpass - willSubmit &= cmdbuf->beginRenderPass(info, IGPUCommandBuffer::SUBPASS_CONTENTS::INLINE); - { - asset::SViewport viewport; - { - viewport.minDepth = 1.f; - viewport.maxDepth = 0.f; - viewport.x = 0u; - viewport.y = 0u; - viewport.width = m_window->getWidth(); - viewport.height = m_window->getHeight(); - } - - willSubmit &= cmdbuf->setViewport(0u, 1u, &viewport); - - const VkRect2D currentRenderArea = - { - .offset = {0,0}, - .extent = {m_window->getWidth(),m_window->getHeight()} - }; - - IQueue::SSubmitInfo::SCommandBufferInfo commandBuffersInfo[] = { {.cmdbuf = cmdbuf } }; - - const IGPUCommandBuffer::SRenderpassBeginInfo info = - { - .framebuffer = m_framebuffers[resourceIx].get(), - .colorClearValues = &clearValue, - .depthStencilClearValues = nullptr, - .renderArea = currentRenderArea - }; - - nbl::video::ISemaphore::SWaitInfo waitInfo = { .semaphore = m_semaphore.get(), .value = m_realFrameIx + 1u }; - const auto uiParams = m_ui.manager->getCreationParameters(); - auto* pipeline = m_ui.manager->getPipeline(); - - cmdbuf->bindGraphicsPipeline(pipeline); - cmdbuf->bindDescriptorSets(EPBP_GRAPHICS, pipeline->getLayout(), uiParams.resources.texturesInfo.setIx, 1u, &m_ui.descriptorSet.get()); // note that we use default UI pipeline layout where uiParams.resources.textures.setIx == uiParams.resources.samplers.setIx - - if (!keepRunning()) - return; - - willSubmit &= m_ui.manager->render(cmdbuf, waitInfo); - } - willSubmit &= cmdbuf->endRenderPass(); - - // If the Rendering and Blit/Present Queues don't come from the same family we need to transfer ownership, because we need to preserve contents between them. - auto blitQueueFamily = m_surface->getAssignedQueue()->getFamilyIndex(); - // Also should crash/error if concurrent sharing enabled but would-be-user-queue is not in the share set, but oh well. - const bool needOwnershipRelease = cmdbuf->getQueueFamilyIndex() != blitQueueFamily && !frame->getCachedCreationParams().isConcurrentSharing(); - if (needOwnershipRelease) - { - const IGPUCommandBuffer::SPipelineBarrierDependencyInfo::image_barrier_t barrier[] = { { - .barrier = { - .dep = { - // Normally I'd put `COLOR_ATTACHMENT` on the masks, but we want this to happen after Layout Transition :( - // https://github.com/KhronosGroup/Vulkan-Docs/issues/2319 - .srcStageMask = asset::PIPELINE_STAGE_FLAGS::ALL_COMMANDS_BITS, - .srcAccessMask = asset::ACCESS_FLAGS::MEMORY_READ_BITS | asset::ACCESS_FLAGS::MEMORY_WRITE_BITS, - // For a Queue Family Ownership Release the destination access masks are irrelevant - // and source stage mask can be NONE as long as the semaphore signals ALL_COMMANDS_BIT - .dstStageMask = asset::PIPELINE_STAGE_FLAGS::NONE, - .dstAccessMask = asset::ACCESS_FLAGS::NONE - }, - .ownershipOp = IGPUCommandBuffer::SOwnershipTransferBarrier::OWNERSHIP_OP::RELEASE, - .otherQueueFamilyIndex = blitQueueFamily - }, - .image = frame, - .subresourceRange = TripleBufferUsedSubresourceRange - // there will be no layout transition, already done by the Renderpass End - } }; - const IGPUCommandBuffer::SPipelineBarrierDependencyInfo depInfo = { .imgBarriers = barrier }; - willSubmit &= cmdbuf->pipelineBarrier(asset::EDF_NONE, depInfo); - } - } - willSubmit &= cmdbuf->end(); - - // submit and present under a mutex ASAP - if (willSubmit) - { - // We will signal a semaphore in the rendering queue, and await it with the presentation/blit queue - const IQueue::SSubmitInfo::SSemaphoreInfo rendered = - { - .semaphore = m_semaphore.get(), - .value = m_realFrameIx + 1, - // Normally I'd put `COLOR_ATTACHMENT` on the masks, but we want to signal after Layout Transitions and optional Ownership Release - // https://github.com/KhronosGroup/Vulkan-Docs/issues/2319 - .stageMask = asset::PIPELINE_STAGE_FLAGS::ALL_COMMANDS_BITS - }; - const IQueue::SSubmitInfo::SCommandBufferInfo cmdbufs[1] = - { { - .cmdbuf = cmdbuf - } }; - // We need to wait on previous triple buffer blits/presents from our source image to complete - auto* pBlitWaitValue = m_blitWaitValues.data() + resourceIx; - auto swapchainLock = m_surface->pseudoAcquire(pBlitWaitValue); - const IQueue::SSubmitInfo::SSemaphoreInfo blitted = - { - .semaphore = m_surface->getPresentSemaphore(), - .value = pBlitWaitValue->load(), - // Normally I'd put `BLIT` on the masks, but we want to wait before Implicit Layout Transitions and optional Implicit Ownership Acquire - // https://github.com/KhronosGroup/Vulkan-Docs/issues/2319 - .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; - - m_realFrameIx++; - - const uint64_t renderedFrameIx = m_realFrameIx - 1u; - auto captureScreenshot = [&](const system::path& outPath, const char* tag) -> void - { - if (!m_device || !m_assetManager || !m_surface) - 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; - } - - if (!frame) - { - m_logger->log("%s screenshot failed: missing frame image.", 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_assetManager.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); - }; - - // only present if there's successful content to show - const ISmoothResizeSurface::SPresentInfo presentInfo = { - { - .source = {.image = frame,.rect = currentRenderArea}, - .waitSemaphore = rendered.semaphore, - .waitValue = rendered.value, - .pPresentSemaphoreWaitValue = pBlitWaitValue, - }, - // The Graphics Queue will be the the most recent owner just before it releases ownership - cmdbuf->getQueueFamilyIndex() - }; - if (m_ciMode && !m_ciScreenshotDone) - { - ++m_ciFrameCounter; - if (m_ciFrameCounter >= CiFramesBeforeCapture) - { - m_ciScreenshotDone = true; - captureScreenshot(m_ciScreenshotPath, "CI"); - } - } - - if (m_scriptedInput.enabled && !m_scriptedInput.captureFrames.empty()) - { - while (m_scriptedInput.nextCaptureIndex < m_scriptedInput.captureFrames.size() && - m_scriptedInput.captureFrames[m_scriptedInput.nextCaptureIndex] == renderedFrameIx) - { - const auto outPath = m_scriptedInput.captureOutputDir / - (m_scriptedInput.capturePrefix + "_" + std::to_string(renderedFrameIx) + ".png"); - captureScreenshot(outPath, "Script"); - ++m_scriptedInput.nextCaptureIndex; - } - } - - m_surface->present(std::move(swapchainLock), presentInfo); - } - firstFrame = false; - } - - inline void paceScriptedVisualDebugFrame() - { - if (!(m_scriptedInput.enabled && m_scriptedInput.visualDebug)) - { - m_scriptedInput.framePacerInitialized = 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.framePacerInitialized) - { - m_scriptedInput.framePacerInitialized = true; - m_scriptedInput.framePacerNext = now + frameDuration; - return; - } - - if (now < m_scriptedInput.framePacerNext) - std::this_thread::sleep_until(m_scriptedInput.framePacerNext); - - auto postSleepNow = std::chrono::steady_clock::now(); - while (m_scriptedInput.framePacerNext < postSleepNow) - m_scriptedInput.framePacerNext += frameDuration; - } - - inline bool keepRunning() override - { - if (m_scriptedInput.enabled && m_scriptedInput.hardFail && m_scriptedInput.failed) - { - if (!m_ciMode || m_ciScreenshotDone) - std::exit(EXIT_FAILURE); - } - if (m_ciMode && m_ciStartedAt != clock_t::time_point::min()) - { - const auto elapsed = clock_t::now() - m_ciStartedAt; - if (elapsed > 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_ciMode && m_ciScreenshotDone) - { - if (m_scriptedInput.enabled && m_scriptedInput.nextCaptureIndex < m_scriptedInput.captureFrames.size()) - return true; - return false; - } - if (m_surface->irrecoverable()) - return false; - - return true; - } - - inline bool onAppTerminated() override - { - return base_t::onAppTerminated(); - } - - inline void update() - { - m_inputSystem->getDefaultMouse(&mouse); - m_inputSystem->getDefaultKeyboard(&keyboard); - - auto updatePresentationTimestamp = [&]() - { - oracle.reportEndFrameRecord(); - const auto timestamp = oracle.getNextPresentationTimeStamp(); - oracle.reportBeginFrameRecord(); - - return timestamp; - }; - - m_nextPresentationTimestamp = updatePresentationTimestamp(); - if (m_haveLastPresentationTimestamp) - { - const auto delta = m_nextPresentationTimestamp - m_lastPresentationTimestamp; - if (delta.count() < 0) - m_frameDeltaSec = 0.0; - else - m_frameDeltaSec = static_cast(delta.count()) / 1000000.0; - } - m_lastPresentationTimestamp = m_nextPresentationTimestamp; - m_haveLastPresentationTimestamp = true; - - updatePlayback(m_frameDeltaSec); - const bool skipCameraInput = m_playback.playing && m_playback.overrideInput; - - struct - { - std::vector mouse {}; - std::vector keyboard {}; - } capturedEvents; - { - mouse.consumeEvents([&](const IMouseEventChannel::range_t& events) -> void - { - if (m_window->hasInputFocus()) - for (const auto& e : events) - capturedEvents.mouse.emplace_back(e); - }, m_logger.get()); - - keyboard.consumeEvents([&](const IKeyboardEventChannel::range_t& events) -> void - { - if (m_window->hasInputFocus()) - for (const auto& e : events) - capturedEvents.keyboard.emplace_back(e); - }, m_logger.get()); - } - - if (m_scriptedInput.enabled && m_scriptedInput.exclusive) - { - capturedEvents.mouse.clear(); - capturedEvents.keyboard.clear(); - } - - std::vector scriptedMouse; - std::vector scriptedKeyboard; - std::vector scriptedImguizmo; - std::vector scriptedActions; - const CVirtualGimbalEvent* scriptedImguizmoVirtual = nullptr; - uint32_t scriptedImguizmoVirtualCount = 0u; - - if (m_scriptedInput.enabled && m_scriptedInput.nextEventIndex < m_scriptedInput.events.size()) - { - const auto frame = m_realFrameIx; - while (m_scriptedInput.nextEventIndex < m_scriptedInput.events.size() && - m_scriptedInput.events[m_scriptedInput.nextEventIndex].frame == frame) - { - const auto& ev = m_scriptedInput.events[m_scriptedInput.nextEventIndex]; - - if (ev.type == ScriptedInputEvent::Type::Keyboard) - { - SKeyboardEvent e(m_nextPresentationTimestamp); - e.keyCode = ev.keyboard.key; - e.action = ev.keyboard.action; - e.window = m_window.get(); - scriptedKeyboard.emplace_back(e); - } - else if (ev.type == ScriptedInputEvent::Type::Mouse) - { - SMouseEvent e(m_nextPresentationTimestamp); - e.window = m_window.get(); - e.type = ev.mouse.type; - if (ev.mouse.type == ui::SMouseEvent::EET_CLICK) - { - e.clickEvent.mouseButton = ev.mouse.button; - e.clickEvent.action = ev.mouse.action; - e.clickEvent.clickPosX = ev.mouse.x; - e.clickEvent.clickPosY = ev.mouse.y; - } - else if (ev.mouse.type == ui::SMouseEvent::EET_SCROLL) - { - e.scrollEvent.verticalScroll = ev.mouse.v; - e.scrollEvent.horizontalScroll = ev.mouse.h; - } - else if (ev.mouse.type == ui::SMouseEvent::EET_MOVEMENT) - { - e.movementEvent.relativeMovementX = ev.mouse.dx; - e.movementEvent.relativeMovementY = ev.mouse.dy; - } - scriptedMouse.emplace_back(e); - } - else if (ev.type == ScriptedInputEvent::Type::Imguizmo) - { - scriptedImguizmo.emplace_back(ev.imguizmo); - } - else if (ev.type == ScriptedInputEvent::Type::Action) - { - scriptedActions.emplace_back(ev.action); - } - - ++m_scriptedInput.nextEventIndex; - } - } - - if (m_scriptedInput.enabled && scriptedActions.size()) - { - auto applyAction = [&](const ScriptedInputEvent::ActionData& action) -> void - { - switch (action.kind) - { - case ScriptedInputEvent::ActionData::Kind::SetActiveRenderWindow: - { - if (action.value < 0 || static_cast(action.value) >= windowBindings.size()) - { - m_logger->log("[script][warn] action set_active_render_window out of range: %d", ILogger::ELL_WARNING, action.value); - return; - } - activeRenderWindowIx = static_cast(action.value); - } break; - - case ScriptedInputEvent::ActionData::Kind::SetActivePlanar: - { - if (action.value < 0 || static_cast(action.value) >= m_planarProjections.size()) - { - m_logger->log("[script][warn] action set_active_planar out of range: %d", ILogger::ELL_WARNING, action.value); - return; - } - auto& binding = windowBindings[activeRenderWindowIx]; - binding.activePlanarIx = static_cast(action.value); - binding.pickDefaultProjections(m_planarProjections[binding.activePlanarIx]->getPlanarProjections()); - m_scriptedInput.visualActivePlanarValid = true; - m_scriptedInput.visualActivePlanarIx = binding.activePlanarIx; - m_scriptedInput.visualActivePlanarStartFrame = m_realFrameIx; - } break; - - case ScriptedInputEvent::ActionData::Kind::SetProjectionType: - { - auto& binding = windowBindings[activeRenderWindowIx]; - if (!binding.lastBoundPerspectivePresetProjectionIx.has_value() || !binding.lastBoundOrthoPresetProjectionIx.has_value()) - binding.pickDefaultProjections(m_planarProjections[binding.activePlanarIx]->getPlanarProjections()); - - const auto type = static_cast(action.value); - switch (type) - { - case IPlanarProjection::CProjection::Perspective: - binding.boundProjectionIx = binding.lastBoundPerspectivePresetProjectionIx.value(); - break; - case IPlanarProjection::CProjection::Orthographic: - binding.boundProjectionIx = binding.lastBoundOrthoPresetProjectionIx.value(); - break; - default: - m_logger->log("[script][warn] action set_projection_type invalid value: %d", ILogger::ELL_WARNING, action.value); - break; - } - } break; - - case ScriptedInputEvent::ActionData::Kind::SetProjectionIndex: - { - auto& binding = windowBindings[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; - } - const auto ix = static_cast(action.value); - const auto type = projections[ix].getParameters().m_type; - binding.boundProjectionIx = ix; - if (type == IPlanarProjection::CProjection::Perspective) - binding.lastBoundPerspectivePresetProjectionIx = ix; - else if (type == IPlanarProjection::CProjection::Orthographic) - binding.lastBoundOrthoPresetProjectionIx = ix; - } break; - - case ScriptedInputEvent::ActionData::Kind::SetUseWindow: - { - useWindow = action.value != 0; - } break; - - case ScriptedInputEvent::ActionData::Kind::SetLeftHanded: - { - auto& binding = windowBindings[activeRenderWindowIx]; - binding.leftHandedProjection = action.value != 0; - } break; - } - }; - - for (const auto& action : scriptedActions) - if (action.kind == ScriptedInputEvent::ActionData::Kind::SetActiveRenderWindow) - applyAction(action); - - for (const auto& action : scriptedActions) - if (action.kind != ScriptedInputEvent::ActionData::Kind::SetActiveRenderWindow) - applyAction(action); - - if (m_scriptedInput.log) - m_logger->log("[script] frame %llu actions=%zu", ILogger::ELL_INFO, static_cast(m_realFrameIx), scriptedActions.size()); - } - - if (m_scriptedInput.enabled && m_scriptedInput.visualDebug && !m_scriptedInput.visualActivePlanarValid) - { - if (activeRenderWindowIx < windowBindings.size()) - { - m_scriptedInput.visualActivePlanarValid = true; - m_scriptedInput.visualActivePlanarIx = windowBindings[activeRenderWindowIx].activePlanarIx; - m_scriptedInput.visualActivePlanarStartFrame = m_realFrameIx; - } - } - - if (!scriptedMouse.empty()) - capturedEvents.mouse.insert(capturedEvents.mouse.end(), scriptedMouse.begin(), scriptedMouse.end()); - if (!scriptedKeyboard.empty()) - capturedEvents.keyboard.insert(capturedEvents.keyboard.end(), scriptedKeyboard.begin(), scriptedKeyboard.end()); - - m_uiInputEventsThisFrame = static_cast(capturedEvents.mouse.size() + capturedEvents.keyboard.size()); - - auto cameraKeyboardEvents = capturedEvents.keyboard; - auto cameraMouseEvents = capturedEvents.mouse; - for (auto& ev : cameraMouseEvents) - { - if (ev.type == ui::SMouseEvent::EET_SCROLL) - { - ev.scrollEvent.verticalScroll *= m_cameraControls.mouseScrollScale; - ev.scrollEvent.horizontalScroll *= m_cameraControls.mouseScrollScale; - } - else if (ev.type == ui::SMouseEvent::EET_MOVEMENT) - { - ev.movementEvent.relativeMovementX *= m_cameraControls.mouseMoveScale; - ev.movementEvent.relativeMovementY *= m_cameraControls.mouseMoveScale; - } - } - - const auto cursorPosition = m_window->getCursorControl()->getPosition(); - - nbl::ext::imgui::UI::SUpdateParameters params = - { - .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() } - }; - - if (m_scriptedInput.log && (scriptedKeyboard.size() || scriptedMouse.size() || scriptedImguizmo.size())) - { - m_logger->log("[script] frame %llu input kb=%zu mouse=%zu imguizmo=%zu", ILogger::ELL_INFO, - static_cast(m_realFrameIx), - scriptedKeyboard.size(), - scriptedMouse.size(), - scriptedImguizmo.size()); - } - - if (enableActiveCameraMovement && !skipCameraInput) - { - auto& binding = windowBindings[activeRenderWindowIx]; - auto& planar = m_planarProjections[binding.activePlanarIx]; - auto* camera = planar->getCamera(); - - assert(binding.boundProjectionIx.has_value()); - auto& projection = planar->getPlanarProjections()[binding.boundProjectionIx.value()]; - - static std::vector virtualEvents(0x45); - uint32_t vCount = {}; - uint32_t vKeyboardEventsCount = {}; - uint32_t vMouseEventsCount = {}; - - projection.beginInputProcessing(m_nextPresentationTimestamp); - { - projection.processKeyboard(nullptr, vKeyboardEventsCount, {}); - projection.processMouse(nullptr, vMouseEventsCount, {}); - - const auto totalCount = vKeyboardEventsCount + vMouseEventsCount; - if (virtualEvents.size() < totalCount) - virtualEvents.resize(totalCount); - - auto* output = virtualEvents.data(); - projection.processKeyboard(output, vKeyboardEventsCount, { cameraKeyboardEvents.data(), cameraKeyboardEvents.size() }); - for (uint32_t i = 0u; i < vKeyboardEventsCount; ++i) - output[i].magnitude *= m_cameraControls.keyboardScale; - output += vKeyboardEventsCount; - - if (isOrbitLikeCamera(camera)) - { - if (ImGui::IsMouseDown(ImGuiMouseButton_Left)) - projection.processMouse(output, vMouseEventsCount, { cameraMouseEvents.data(), cameraMouseEvents.size() }); - else - vMouseEventsCount = 0; - } - else - { - projection.processMouse(output, vMouseEventsCount, { cameraMouseEvents.data(), cameraMouseEvents.size() }); - } - - vCount = vKeyboardEventsCount + vMouseEventsCount; - } - projection.endInputProcessing(); - - if (vCount) - { - applyVirtualEventScaling(virtualEvents, vCount); - - const char* controllerLabel = "Keyboard/Mouse"; - auto applyEventsToCamera = [&](ICamera* target, uint32_t planarIx) - { - if (!target) - return; - - if (m_cameraControls.worldTranslate) - { - std::vector perCameraEvents(virtualEvents.begin(), virtualEvents.begin() + vCount); - uint32_t perCount = vCount; - remapTranslationToWorld(target, perCameraEvents, perCount); - if (perCount) - target->manipulate({ perCameraEvents.data(), perCount }); - } - else - { - target->manipulate({ virtualEvents.data(), vCount }); - } - - applyConstraintsToCamera(target); - appendVirtualEventLog("input", controllerLabel, planarIx, target, virtualEvents.data(), vCount); - }; - - if (m_cameraControls.mirrorInput) - { - std::unordered_set visited; - for (size_t bindingIx = 0u; bindingIx < windowBindings.size(); ++bindingIx) - { - auto& bindingIt = windowBindings[bindingIx]; - auto& planarIt = m_planarProjections[bindingIt.activePlanarIx]; - if (!planarIt) - continue; - auto* target = planarIt->getCamera(); - if (!target) - continue; - const auto id = target->getGimbal().getID(); - if (!visited.insert(id).second) - continue; - applyEventsToCamera(target, bindingIt.activePlanarIx); - } - } - else - { - applyEventsToCamera(camera, binding.activePlanarIx); - } - - if (m_scriptedInput.log) - { - for (uint32_t i = 0u; i < vCount; ++i) - { - const auto& ev = virtualEvents[i]; - m_logger->log("[script] virtual %s magnitude=%.6f", ILogger::ELL_INFO, CVirtualGimbalEvent::virtualEventToString(ev.type).data(), ev.magnitude); - } - - const auto& gimbal = camera->getGimbal(); - const auto pos = gimbal.getPosition(); - const auto euler = glm::degrees(glm::eulerAngles(gimbal.getOrientation())); - m_logger->log("[script] gimbal pos=(%.3f, %.3f, %.3f) euler_deg=(%.3f, %.3f, %.3f)", ILogger::ELL_INFO, - pos.x, pos.y, pos.z, euler.x, euler.y, euler.z); - } - } - } - - if (m_scriptedInput.enabled && scriptedImguizmo.size() && !skipCameraInput) - { - auto& binding = windowBindings[activeRenderWindowIx]; - auto& planar = m_planarProjections[binding.activePlanarIx]; - auto* camera = planar->getCamera(); - - static std::vector imguizmoEvents(0x20); - uint32_t vCount = 0u; - - camera->beginInputProcessing(m_nextPresentationTimestamp); - { - camera->processImguizmo(nullptr, vCount, {}); - if (imguizmoEvents.size() < vCount) - imguizmoEvents.resize(vCount); - - camera->processImguizmo(imguizmoEvents.data(), vCount, { scriptedImguizmo.data(), scriptedImguizmo.size() }); - } - camera->endInputProcessing(); - - if (vCount) - { - camera->manipulate({ imguizmoEvents.data(), vCount }); - appendVirtualEventLog("imguizmo", "ImGuizmo", binding.activePlanarIx, camera, imguizmoEvents.data(), vCount); - - if (m_scriptedInput.log) - { - for (uint32_t i = 0u; i < vCount; ++i) - { - const auto& ev = imguizmoEvents[i]; - m_logger->log("[script] imguizmo virtual %s magnitude=%.6f", ILogger::ELL_INFO, CVirtualGimbalEvent::virtualEventToString(ev.type).data(), ev.magnitude); - } - - const auto& gimbal = camera->getGimbal(); - const auto pos = gimbal.getPosition(); - const auto euler = glm::degrees(glm::eulerAngles(gimbal.getOrientation())); - m_logger->log("[script] imguizmo gimbal pos=(%.3f, %.3f, %.3f) euler_deg=(%.3f, %.3f, %.3f)", ILogger::ELL_INFO, - pos.x, pos.y, pos.z, euler.x, euler.y, euler.z); - } - } - - scriptedImguizmoVirtual = vCount ? imguizmoEvents.data() : nullptr; - scriptedImguizmoVirtualCount = vCount; - } - - if (m_scriptedInput.enabled && m_scriptedInput.nextCheckIndex < m_scriptedInput.checks.size()) - { - auto* camera = [&]() -> ICamera* - { - if (m_planarProjections.empty()) - return nullptr; - auto& binding = windowBindings[activeRenderWindowIx]; - if (binding.activePlanarIx >= m_planarProjections.size()) - return nullptr; - return m_planarProjections[binding.activePlanarIx]->getCamera(); - }(); - - 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)...); - }; - - auto angleDiffDeg = [](float a, float b) -> float - { - float d = std::fmod(a - b + 180.0f, 360.0f); - if (d < 0.0f) - d += 360.0f; - return std::abs(d - 180.0f); - }; - - auto isFinite3 = [](const float32_t3& v) -> bool - { - return std::isfinite(v.x) && std::isfinite(v.y) && std::isfinite(v.z); - }; - auto setStepReference = [&](const float32_t3& newPos, const float32_t3& newEuler) -> void - { - m_scriptedInput.stepValid = true; - m_scriptedInput.stepPos = newPos; - m_scriptedInput.stepEulerDeg = newEuler; - }; - - const auto frame = m_realFrameIx; - while (m_scriptedInput.nextCheckIndex < m_scriptedInput.checks.size() && - m_scriptedInput.checks[m_scriptedInput.nextCheckIndex].frame == frame) - { - const auto& check = m_scriptedInput.checks[m_scriptedInput.nextCheckIndex]; - - if (!camera) - { - logFail("[script][fail] check frame=%llu no active camera", static_cast(frame)); - ++m_scriptedInput.nextCheckIndex; - continue; - } - - const auto& gimbal = camera->getGimbal(); - const auto pos = gimbal.getPosition(); - const auto euler = glm::degrees(glm::eulerAngles(gimbal.getOrientation())); - - if (!isFinite3(pos) || !isFinite3(euler)) - { - logFail("[script][fail] check frame=%llu non-finite gimbal state", static_cast(frame)); - ++m_scriptedInput.nextCheckIndex; - continue; - } - - if (check.kind == ScriptedInputCheck::Kind::Baseline) - { - m_scriptedInput.baselineValid = true; - m_scriptedInput.baselinePos = pos; - m_scriptedInput.baselineEulerDeg = euler; - setStepReference(pos, euler); - logPass("[script][pass] baseline frame=%llu pos=(%.3f, %.3f, %.3f) euler_deg=(%.3f, %.3f, %.3f)", - static_cast(frame), - pos.x, pos.y, pos.z, euler.x, euler.y, euler.z); - } - else if (check.kind == ScriptedInputCheck::Kind::ImguizmoVirtual) - { - bool ok = true; - if (!scriptedImguizmoVirtual || scriptedImguizmoVirtualCount == 0u) - { - ok = false; - } - else - { - for (const auto& expected : check.expectedVirtualEvents) - { - bool found = false; - double actual = 0.0; - for (uint32_t i = 0u; i < scriptedImguizmoVirtualCount; ++i) - { - if (scriptedImguizmoVirtual[i].type == expected.type) - { - found = true; - actual = scriptedImguizmoVirtual[i].magnitude; - break; - } - } - if (!found || std::abs(actual - expected.magnitude) > check.tolerance) - { - ok = false; - logFail("[script][fail] imguizmo_virtual frame=%llu type=%s expected=%.6f actual=%.6f tol=%.6f", - static_cast(frame), - CVirtualGimbalEvent::virtualEventToString(expected.type).data(), - expected.magnitude, - actual, - check.tolerance); - } - } - } - - if (ok) - logPass("[script][pass] imguizmo_virtual frame=%llu events=%zu", static_cast(frame), check.expectedVirtualEvents.size()); - } - else if (check.kind == ScriptedInputCheck::Kind::GimbalNear) - { - bool ok = true; - if (check.hasExpectedPos) - { - const auto diff = float32_t3(pos.x - check.expectedPos.x, pos.y - check.expectedPos.y, pos.z - check.expectedPos.z); - const auto d = std::sqrt(diff.x * diff.x + diff.y * diff.y + diff.z * diff.z); - if (d > check.posTolerance) - { - ok = false; - logFail("[script][fail] gimbal_near frame=%llu pos_diff=%.6f tol=%.6f", - static_cast(frame), d, check.posTolerance); - } - } - if (check.hasExpectedEuler) - { - const auto dx = angleDiffDeg(euler.x, check.expectedEulerDeg.x); - const auto dy = angleDiffDeg(euler.y, check.expectedEulerDeg.y); - const auto dz = angleDiffDeg(euler.z, check.expectedEulerDeg.z); - const auto dmax = std::max(dx, std::max(dy, dz)); - if (dmax > check.eulerToleranceDeg) - { - ok = false; - logFail("[script][fail] gimbal_near frame=%llu euler_diff=%.6f tol=%.6f", - static_cast(frame), dmax, check.eulerToleranceDeg); - } - } - - if (ok) - logPass("[script][pass] gimbal_near frame=%llu", static_cast(frame)); - } - else if (check.kind == ScriptedInputCheck::Kind::GimbalDelta) - { - if (!m_scriptedInput.baselineValid) - { - logFail("[script][fail] gimbal_delta frame=%llu missing baseline", static_cast(frame)); - } - else - { - const auto diff = float32_t3(pos.x - m_scriptedInput.baselinePos.x, pos.y - m_scriptedInput.baselinePos.y, pos.z - m_scriptedInput.baselinePos.z); - const auto dpos = std::sqrt(diff.x * diff.x + diff.y * diff.y + diff.z * diff.z); - const auto dx = angleDiffDeg(euler.x, m_scriptedInput.baselineEulerDeg.x); - const auto dy = angleDiffDeg(euler.y, m_scriptedInput.baselineEulerDeg.y); - const auto dz = angleDiffDeg(euler.z, m_scriptedInput.baselineEulerDeg.z); - const auto dmax = std::max(dx, std::max(dy, dz)); - - if (dpos > check.posTolerance || dmax > check.eulerToleranceDeg) - { - logFail("[script][fail] gimbal_delta frame=%llu pos_diff=%.6f tol=%.6f euler_diff=%.6f tol=%.6f", - static_cast(frame), - dpos, check.posTolerance, - dmax, check.eulerToleranceDeg); - } - else - { - logPass("[script][pass] gimbal_delta frame=%llu pos_diff=%.6f euler_diff=%.6f", - static_cast(frame), dpos, dmax); - } - } - } - else if (check.kind == ScriptedInputCheck::Kind::GimbalStep) - { - if (!m_scriptedInput.stepValid) - { - if (m_scriptedInput.baselineValid) - setStepReference(m_scriptedInput.baselinePos, m_scriptedInput.baselineEulerDeg); - else - { - logFail("[script][fail] gimbal_step frame=%llu missing step reference", static_cast(frame)); - setStepReference(pos, euler); - ++m_scriptedInput.nextCheckIndex; - continue; - } - } - - const auto diff = float32_t3(pos.x - m_scriptedInput.stepPos.x, pos.y - m_scriptedInput.stepPos.y, pos.z - m_scriptedInput.stepPos.z); - const auto dpos = std::sqrt(diff.x * diff.x + diff.y * diff.y + diff.z * diff.z); - const auto dx = angleDiffDeg(euler.x, m_scriptedInput.stepEulerDeg.x); - const auto dy = angleDiffDeg(euler.y, m_scriptedInput.stepEulerDeg.y); - const auto dz = angleDiffDeg(euler.z, m_scriptedInput.stepEulerDeg.z); - const auto dmax = std::max(dx, std::max(dy, dz)); - - bool ok = true; - if (check.hasPosDeltaConstraint) - { - if (dpos < check.minPosDelta || dpos > check.posTolerance) - { - ok = false; - logFail("[script][fail] gimbal_step frame=%llu pos_delta=%.6f expected=[%.6f, %.6f]", - static_cast(frame), dpos, check.minPosDelta, check.posTolerance); - } - } - if (check.hasEulerDeltaConstraint) - { - if (dmax < check.minEulerDeltaDeg || dmax > check.eulerToleranceDeg) - { - ok = false; - logFail("[script][fail] gimbal_step frame=%llu euler_delta=%.6f expected=[%.6f, %.6f]", - static_cast(frame), dmax, check.minEulerDeltaDeg, check.eulerToleranceDeg); - } - } - - if (ok) - logPass("[script][pass] gimbal_step frame=%llu pos_delta=%.6f euler_delta=%.6f", - static_cast(frame), dpos, dmax); - setStepReference(pos, euler); - } - - ++m_scriptedInput.nextCheckIndex; - } - - if (!m_scriptedInput.summaryReported && m_scriptedInput.nextCheckIndex >= m_scriptedInput.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); - } - } - - UpdateUiMetrics(); - m_ui.manager->update(params); - - } - - private: - struct CUILogFormatter final : public nbl::system::ILogger - { - CUILogFormatter() : ILogger(ILogger::DefaultLogMask()) {} - - std::string format(E_LOG_LEVEL level, 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&, E_LOG_LEVEL, va_list) override {} - }; - - struct VirtualEventLogEntry - { - uint64_t frame = 0; - CVirtualGimbalEvent::VirtualEventType type = CVirtualGimbalEvent::None; - float64_t magnitude = 0.0; - std::string source; - std::string controller; - std::string camera; - uint32_t planarIx = 0u; - std::string line; - }; - - struct CameraPreset - { - std::string name; - std::string identifier; - float64_t3 position = float64_t3(0.0); - glm::quat orientation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f); - float distance = 0.f; - bool hasDistance = false; - double orbitU = 0.0; - double orbitV = 0.0; - float orbitDistance = 0.f; - bool hasOrbitState = false; - }; - - struct CameraKeyframe - { - CameraPreset preset; - float time = 0.f; - }; - - struct CameraPlaybackState - { - bool playing = false; - bool loop = true; - bool overrideInput = true; - float speed = 1.f; - float time = 0.f; - }; - - struct CameraControlSettings - { - bool mirrorInput = false; - bool worldTranslate = false; - float keyboardScale = 0.00625f; - float mouseMoveScale = 1.0f; - float mouseScrollScale = 1.0f; - float translationScale = 1.0f; - float rotationScale = 1.0f; - }; - - struct CameraConstraintSettings - { - bool enabled = false; - bool clampPitch = false; - bool clampYaw = false; - bool clampRoll = false; - bool clampDistance = false; - float pitchMinDeg = -80.f; - float pitchMaxDeg = 80.f; - float yawMinDeg = -180.f; - float yawMaxDeg = 180.f; - float rollMinDeg = -180.f; - float rollMaxDeg = 180.f; - float minDistance = 0.1f; - float maxDistance = 1000.f; - }; - - inline ICamera* getActiveCamera() - { - auto& binding = windowBindings[activeRenderWindowIx]; - auto& planar = m_planarProjections[binding.activePlanarIx]; - return planar ? planar->getCamera() : nullptr; - } - - inline bool isOrbitLikeCamera(ICamera* camera) - { - return dynamic_cast(camera); - } - - template - inline bool withOrbitLikeCamera(ICamera* camera, Fn&& fn) - { - if (auto* orbit = dynamic_cast(camera)) - { - fn(orbit); - return true; - } - return false; - } - - inline std::string_view getCameraTypeLabel(const ICamera* camera) const - { - if (dynamic_cast(camera)) - return "FPS"; - if (dynamic_cast(camera)) - return "Free"; - if (dynamic_cast(camera)) - return "Orbit"; - if (dynamic_cast(camera)) - return "Arcball"; - if (dynamic_cast(camera)) - return "Turntable"; - if (dynamic_cast(camera)) - return "TopDown"; - if (dynamic_cast(camera)) - return "Isometric"; - if (dynamic_cast(camera)) - return "Chase"; - if (dynamic_cast(camera)) - return "Dolly"; - if (dynamic_cast(camera)) - return "Dolly Zoom"; - if (dynamic_cast(camera)) - return "Path"; - return "Unknown"; - } - - inline std::string_view getCameraTypeDescription(const ICamera* camera) const - { - if (dynamic_cast(camera)) - return "First-person WASD + mouse look"; - if (dynamic_cast(camera)) - return "Free-fly 6DOF with full rotation"; - if (dynamic_cast(camera)) - return "Orbit around target with dolly"; - if (dynamic_cast(camera)) - return "Arcball trackball around target"; - if (dynamic_cast(camera)) - return "Turntable yaw/pitch around target"; - if (dynamic_cast(camera)) - return "Fixed pitch top-down pan"; - if (dynamic_cast(camera)) - return "Fixed isometric view with pan"; - if (dynamic_cast(camera)) - return "Target follow with chase controls"; - if (dynamic_cast(camera)) - return "Rig truck/dolly with look-at"; - if (dynamic_cast(camera)) - return "Orbit with dolly-zoom FOV"; - if (dynamic_cast(camera)) - return "Move along a target path"; - return "Unspecified camera behavior"; - } - - inline void syncVisualDebugWindowBindings() - { - if (!(m_scriptedInput.enabled && m_scriptedInput.visualDebug)) - return; - if (windowBindings.size() < 2u || m_planarProjections.empty()) - return; - - auto& perspectiveBinding = windowBindings[0u]; - if (perspectiveBinding.activePlanarIx >= m_planarProjections.size()) - return; - auto& perspectivePlanar = m_planarProjections[perspectiveBinding.activePlanarIx]; - if (!perspectivePlanar) - return; - if (!perspectiveBinding.lastBoundPerspectivePresetProjectionIx.has_value()) - perspectiveBinding.pickDefaultProjections(perspectivePlanar->getPlanarProjections()); - if (perspectiveBinding.lastBoundPerspectivePresetProjectionIx.has_value()) - perspectiveBinding.boundProjectionIx = perspectiveBinding.lastBoundPerspectivePresetProjectionIx.value(); - - auto& orthoBinding = windowBindings[1u]; - if (orthoBinding.activePlanarIx != perspectiveBinding.activePlanarIx) - { - orthoBinding.activePlanarIx = perspectiveBinding.activePlanarIx; - auto& orthoPlanar = m_planarProjections[orthoBinding.activePlanarIx]; - if (!orthoPlanar) - return; - orthoBinding.pickDefaultProjections(orthoPlanar->getPlanarProjections()); - } - if (orthoBinding.activePlanarIx >= m_planarProjections.size()) - return; - auto& orthoPlanar = m_planarProjections[orthoBinding.activePlanarIx]; - if (!orthoPlanar) - return; - if (!orthoBinding.lastBoundOrthoPresetProjectionIx.has_value()) - orthoBinding.pickDefaultProjections(orthoPlanar->getPlanarProjections()); - if (orthoBinding.lastBoundOrthoPresetProjectionIx.has_value()) - orthoBinding.boundProjectionIx = orthoBinding.lastBoundOrthoPresetProjectionIx.value(); - } - - inline bool projectWorldPointToViewport( - const float32_t4x4& viewProjMatrix, - const float32_t3& worldPoint, - const ImVec2& viewportPos, - const ImVec2& viewportSize, - ImVec2& outScreen) const - { - if (viewportSize.x <= 1.0f || viewportSize.y <= 1.0f) - return false; - - const auto clip = mul(viewProjMatrix, float32_t4(worldPoint.x, worldPoint.y, worldPoint.z, 1.0f)); - if (!std::isfinite(clip.x) || !std::isfinite(clip.y) || !std::isfinite(clip.z) || !std::isfinite(clip.w)) - return false; - - const float absW = std::abs(clip.w); - if (absW < 1e-5f) - return false; - - const float invW = 1.0f / clip.w; - const float ndcX = clip.x * invW; - const float ndcY = clip.y * invW; - const float ndcZ = clip.z * invW; - - if (!std::isfinite(ndcX) || !std::isfinite(ndcY) || !std::isfinite(ndcZ)) - return false; - if (std::abs(ndcX) > 100.0f || std::abs(ndcY) > 100.0f || std::abs(ndcZ) > 100.0f) - return false; - - outScreen.x = viewportPos.x + (ndcX * 0.5f + 0.5f) * viewportSize.x; - outScreen.y = viewportPos.y + (-ndcY * 0.5f + 0.5f) * viewportSize.y; - return std::isfinite(outScreen.x) && std::isfinite(outScreen.y); - } - - inline void drawWorldReferenceOverlay( - const ImVec2& viewportPos, - const ImVec2& viewportSize, - const float32_t4x4& viewMatrix, - const float32_t4x4& projectionMatrix, - bool leftHandedProjection, - float nearPlane, - float farPlane) - { - if (!(m_scriptedInput.enabled && m_scriptedInput.visualDebug)) - return; - if (viewportSize.x <= 1.0f || viewportSize.y <= 1.0f) - return; - - auto* drawList = ImGui::GetWindowDrawList(); - if (!drawList) - return; - - const float safeNear = std::max(nearPlane, 0.001f); - const float safeFar = std::max(farPlane, safeNear + 0.001f); - const auto depthOfViewPoint = [&](const float32_t4& viewPoint) -> float - { - return leftHandedProjection ? viewPoint.z : -viewPoint.z; - }; - const auto ndcToViewport = [&](const ImVec2& ndc) -> ImVec2 - { - return ImVec2( - viewportPos.x + (ndc.x * 0.5f + 0.5f) * viewportSize.x, - viewportPos.y + (-ndc.y * 0.5f + 0.5f) * viewportSize.y); - }; - const auto clipSegmentByDepthRange = [&](float32_t4& viewA, float32_t4& viewB) -> bool - { - const float32_t4 a0 = viewA; - const float32_t4 b0 = viewB; - const float32_t4 delta = b0 - a0; - const float depthA = depthOfViewPoint(a0); - const float depthB = depthOfViewPoint(b0); - - float tEnter = 0.0f; - float tExit = 1.0f; - const auto clipByConstraint = [&](float fa, float fb) -> bool - { - if (fa < 0.0f && fb < 0.0f) - return false; - if (fa >= 0.0f && fb >= 0.0f) - return true; - - const float denom = fa - fb; - if (std::abs(denom) < 1e-6f) - return false; - const float t = std::clamp(fa / denom, 0.0f, 1.0f); - - if (fa < 0.0f) - tEnter = std::max(tEnter, t); - else - tExit = std::min(tExit, t); - - return tEnter <= tExit; - }; - - if (!clipByConstraint(depthA - safeNear, depthB - safeNear)) - return false; - if (!clipByConstraint(safeFar - depthA, safeFar - depthB)) - return false; - - viewA = a0 + delta * tEnter; - viewB = a0 + delta * tExit; - return true; - }; - const auto projectViewPointToNdc = [&](const float32_t4& viewPoint, ImVec2& outNdc) -> bool - { - const auto clip = mul(projectionMatrix, viewPoint); - if (!std::isfinite(clip.x) || !std::isfinite(clip.y) || !std::isfinite(clip.z) || !std::isfinite(clip.w)) - return false; - - const float absW = std::abs(clip.w); - if (absW < 1e-6f) - return false; - - const float invW = 1.0f / clip.w; - const float ndcX = clip.x * invW; - const float ndcY = clip.y * invW; - const float ndcZ = clip.z * invW; - if (!std::isfinite(ndcX) || !std::isfinite(ndcY) || !std::isfinite(ndcZ)) - return false; - if (std::abs(ndcX) > 1e4f || std::abs(ndcY) > 1e4f || std::abs(ndcZ) > 1e4f) - return false; - - outNdc = ImVec2(ndcX, ndcY); - return true; - }; - const auto clipNdcSegmentToViewport = [&](ImVec2& ndcA, ImVec2& ndcB) -> bool - { - float tEnter = 0.0f; - float tExit = 1.0f; - const float dx = ndcB.x - ndcA.x; - const float dy = ndcB.y - ndcA.y; - const auto clipTest = [&](float p, float q) -> bool - { - if (std::abs(p) < 1e-6f) - return q >= 0.0f; - - const float r = q / p; - if (p < 0.0f) - { - if (r > tExit) - return false; - tEnter = std::max(tEnter, r); - } - else - { - if (r < tEnter) - return false; - tExit = std::min(tExit, r); - } - return tEnter <= tExit; - }; - - if (!clipTest(-dx, ndcA.x + 1.0f)) - return false; - if (!clipTest(dx, 1.0f - ndcA.x)) - return false; - if (!clipTest(-dy, ndcA.y + 1.0f)) - return false; - if (!clipTest(dy, 1.0f - ndcA.y)) - return false; - - const ImVec2 a0 = ndcA; - ndcA = ImVec2(a0.x + dx * tEnter, a0.y + dy * tEnter); - ndcB = ImVec2(a0.x + dx * tExit, a0.y + dy * tExit); - return true; - }; - const auto projectWorldPointToViewportClipped = [&](const float32_t3& worldPoint, ImVec2& outScreen) -> bool - { - const auto viewPoint = mul(viewMatrix, float32_t4(worldPoint.x, worldPoint.y, worldPoint.z, 1.0f)); - if (!std::isfinite(viewPoint.x) || !std::isfinite(viewPoint.y) || !std::isfinite(viewPoint.z) || !std::isfinite(viewPoint.w)) - return false; - - const float depth = depthOfViewPoint(viewPoint); - if (depth < safeNear || depth > safeFar) - return false; - - ImVec2 ndcPoint = {}; - if (!projectViewPointToNdc(viewPoint, ndcPoint)) - return false; - if (ndcPoint.x < -1.0f || ndcPoint.x > 1.0f || ndcPoint.y < -1.0f || ndcPoint.y > 1.0f) - return false; - - outScreen = ndcToViewport(ndcPoint); - return std::isfinite(outScreen.x) && std::isfinite(outScreen.y); - }; - - auto drawWorldLine = [&](const float32_t3& aWorld, const float32_t3& bWorld, ImU32 color, float thickness) -> void - { - float32_t4 viewA = mul(viewMatrix, float32_t4(aWorld.x, aWorld.y, aWorld.z, 1.0f)); - float32_t4 viewB = mul(viewMatrix, float32_t4(bWorld.x, bWorld.y, bWorld.z, 1.0f)); - if (!std::isfinite(viewA.x) || !std::isfinite(viewA.y) || !std::isfinite(viewA.z) || !std::isfinite(viewA.w) || - !std::isfinite(viewB.x) || !std::isfinite(viewB.y) || !std::isfinite(viewB.z) || !std::isfinite(viewB.w)) - return; - if (!clipSegmentByDepthRange(viewA, viewB)) - return; - - ImVec2 ndcA = {}; - ImVec2 ndcB = {}; - if (!projectViewPointToNdc(viewA, ndcA)) - return; - if (!projectViewPointToNdc(viewB, ndcB)) - return; - if (!clipNdcSegmentToViewport(ndcA, ndcB)) - return; - - const ImVec2 screenA = ndcToViewport(ndcA); - const ImVec2 screenB = ndcToViewport(ndcB); - drawList->AddLine(screenA, screenB, color, thickness); - }; - - constexpr int gridHalfSteps = 8; - constexpr float gridStep = 2.0f; - const float gridHalfSize = static_cast(gridHalfSteps) * gridStep; - constexpr int gridMajorModulo = 1; - const ImU32 gridMajor = IM_COL32(136, 160, 194, 95); - - for (int i = -gridHalfSteps; i <= gridHalfSteps; ++i) - { - if (i == 0) - continue; - if ((i % gridMajorModulo) != 0) - continue; - const float c = static_cast(i) * gridStep; - drawWorldLine(float32_t3(c, 0.0f, -gridHalfSize), float32_t3(c, 0.0f, gridHalfSize), gridMajor, 1.3f); - drawWorldLine(float32_t3(-gridHalfSize, 0.0f, c), float32_t3(gridHalfSize, 0.0f, c), gridMajor, 1.3f); - } - - drawWorldLine(float32_t3(-gridHalfSize, 0.0f, 0.0f), float32_t3(gridHalfSize, 0.0f, 0.0f), IM_COL32(184, 204, 232, 170), 2.0f); - drawWorldLine(float32_t3(0.0f, 0.0f, -gridHalfSize), float32_t3(0.0f, 0.0f, gridHalfSize), IM_COL32(184, 204, 232, 170), 2.0f); - - constexpr float axisLength = 7.5f; - const float32_t3 origin = float32_t3(0.0f); - const float32_t3 xPos = float32_t3(axisLength, 0.0f, 0.0f); - const float32_t3 yPos = float32_t3(0.0f, axisLength, 0.0f); - const float32_t3 zPos = float32_t3(0.0f, 0.0f, axisLength); - const float32_t3 xNeg = float32_t3(-axisLength * 0.4f, 0.0f, 0.0f); - const float32_t3 yNeg = float32_t3(0.0f, -axisLength * 0.3f, 0.0f); - const float32_t3 zNeg = float32_t3(0.0f, 0.0f, -axisLength * 0.4f); - - drawWorldLine(origin, xPos, IM_COL32(244, 92, 92, 245), 3.2f); - drawWorldLine(origin, yPos, IM_COL32(124, 236, 132, 245), 3.2f); - drawWorldLine(origin, zPos, IM_COL32(106, 166, 255, 245), 3.2f); - drawWorldLine(origin, xNeg, IM_COL32(128, 74, 74, 180), 1.6f); - drawWorldLine(origin, yNeg, IM_COL32(74, 128, 78, 180), 1.6f); - drawWorldLine(origin, zNeg, IM_COL32(70, 88, 124, 180), 1.6f); - - auto drawAxisLabel = [&](const char* label, const float32_t3& worldPoint, ImU32 color) -> void - { - ImVec2 screenPos = {}; - if (!projectWorldPointToViewportClipped(worldPoint, screenPos)) - return; - drawList->AddText(ImVec2(screenPos.x + 4.0f, screenPos.y + 3.0f), color, label); - }; - - ImVec2 originScreen = {}; - if (projectWorldPointToViewportClipped(origin, originScreen)) - drawList->AddCircleFilled(originScreen, 4.0f, IM_COL32(240, 248, 255, 220), 16); - - drawAxisLabel("X", xPos, IM_COL32(255, 152, 152, 255)); - drawAxisLabel("Y", yPos, IM_COL32(172, 255, 178, 255)); - drawAxisLabel("Z", zPos, IM_COL32(172, 210, 255, 255)); - } - - inline void drawScriptVisualDebugOverlay(const ImVec2& displaySize) - { - if (!(m_scriptedInput.enabled && m_scriptedInput.visualDebug)) - return; - if (windowBindings.empty() || m_planarProjections.empty()) - return; - if (activeRenderWindowIx >= windowBindings.size()) - return; - - const auto& binding = windowBindings[activeRenderWindowIx]; - if (binding.activePlanarIx >= m_planarProjections.size()) - return; - - auto& planar = m_planarProjections[binding.activePlanarIx]; - if (!planar) - return; - auto* camera = planar->getCamera(); - if (!camera) - return; - - if (!m_scriptedInput.visualActivePlanarValid) - { - m_scriptedInput.visualActivePlanarValid = true; - m_scriptedInput.visualActivePlanarIx = binding.activePlanarIx; - m_scriptedInput.visualActivePlanarStartFrame = m_realFrameIx; - } - - const uint64_t elapsedFrames = (m_realFrameIx >= m_scriptedInput.visualActivePlanarStartFrame) ? - (m_realFrameIx - m_scriptedInput.visualActivePlanarStartFrame) : 0ull; - const float fps = std::max(1.f, m_scriptedInput.visualTargetFps); - const uint64_t holdFrames = static_cast(std::round(std::max(0.f, m_scriptedInput.visualCameraHoldSeconds) * fps)); - const uint64_t progressFrames = holdFrames ? std::min(elapsedFrames, holdFrames) : elapsedFrames; - - const auto cameraLabel = getCameraTypeLabel(camera); - std::string lineTop = "SCRIPT VISUAL DEBUG"; - std::string lineMid = "Camera " + std::to_string(binding.activePlanarIx + 1u) + "/" + std::to_string(m_planarProjections.size()) + " " + std::string(cameraLabel); - - char lineBottomBuffer[256] = {}; - if (holdFrames) - { - const double elapsedSeconds = static_cast(progressFrames) / static_cast(fps); - const double holdSeconds = static_cast(holdFrames) / static_cast(fps); - std::snprintf( - lineBottomBuffer, - sizeof(lineBottomBuffer), - "Planar %u Segment %.1f/%.1f s Frame %llu/%llu", - binding.activePlanarIx, - elapsedSeconds, - holdSeconds, - static_cast(progressFrames), - static_cast(holdFrames)); - } - else - { - std::snprintf( - lineBottomBuffer, - sizeof(lineBottomBuffer), - "Planar %u Frame %llu", - binding.activePlanarIx, - static_cast(m_realFrameIx)); - } - const std::string lineBottom(lineBottomBuffer); - - const float topSize = 50.f; - const float midSize = 38.f; - const float bottomSize = 28.f; - const float marginTop = 18.f; - const float padX = 24.f; - const float padY = 16.f; - const float gap = 6.f; - - ImFont* font = ImGui::GetFont(); - if (!font) - return; - - const float textWrap = std::numeric_limits::max(); - const ImVec2 topTextSize = font->CalcTextSizeA(topSize, textWrap, 0.0f, lineTop.c_str()); - const ImVec2 midTextSize = font->CalcTextSizeA(midSize, textWrap, 0.0f, lineMid.c_str()); - const ImVec2 bottomTextSize = font->CalcTextSizeA(bottomSize, textWrap, 0.0f, lineBottom.c_str()); - const float panelWidth = std::max(topTextSize.x, std::max(midTextSize.x, bottomTextSize.x)) + padX * 2.0f; - const float panelHeight = topTextSize.y + midTextSize.y + bottomTextSize.y + gap * 2.0f + padY * 2.0f; - const ImVec2 panelMin((displaySize.x - panelWidth) * 0.5f, marginTop); - const ImVec2 panelMax(panelMin.x + panelWidth, panelMin.y + panelHeight); - - auto* drawList = ImGui::GetForegroundDrawList(); - if (!drawList) - return; - - drawList->AddRectFilled(panelMin, panelMax, IM_COL32(6, 8, 12, 232), 14.0f); - drawList->AddRect(panelMin, panelMax, IM_COL32(255, 166, 64, 255), 14.0f, 0, 2.5f); - - const float topX = panelMin.x + (panelWidth - topTextSize.x) * 0.5f; - const float midX = panelMin.x + (panelWidth - midTextSize.x) * 0.5f; - const float bottomX = panelMin.x + (panelWidth - bottomTextSize.x) * 0.5f; - const float topY = panelMin.y + padY; - const float midY = topY + topTextSize.y + gap; - const float bottomY = midY + midTextSize.y + gap; - - drawList->AddText(font, topSize, ImVec2(topX, topY), IM_COL32(255, 206, 120, 255), lineTop.c_str()); - drawList->AddText(font, midSize, ImVec2(midX, midY), IM_COL32(255, 244, 224, 255), lineMid.c_str()); - drawList->AddText(font, bottomSize, ImVec2(bottomX, bottomY), IM_COL32(202, 222, 255, 255), lineBottom.c_str()); - } - - inline void applyDollyZoomProjection(ICamera* camera, IPlanarProjection::CProjection& projection) - { - auto* dolly = dynamic_cast(camera); - if (!dolly) - return; - const auto& params = projection.getParameters(); - if (params.m_type != IPlanarProjection::CProjection::Perspective) - return; - projection.setPerspective(params.m_zNear, params.m_zFar, dolly->computeDollyFov()); - } - - inline CameraPreset capturePreset(ICamera* camera, const std::string& name) - { - CameraPreset preset; - preset.name = name; - if (!camera) - return preset; - - preset.identifier = std::string(camera->getIdentifier()); - const auto& gimbal = camera->getGimbal(); - preset.position = gimbal.getPosition(); - preset.orientation = gimbal.getOrientation(); - - auto captureOrbit = [&](auto* orbit) - { - preset.distance = orbit->getDistance(); - preset.hasDistance = true; - preset.orbitDistance = orbit->getDistance(); - preset.orbitU = orbit->getU(); - preset.orbitV = orbit->getV(); - preset.hasOrbitState = true; - }; - - withOrbitLikeCamera(camera, captureOrbit); - - return preset; - } - - inline bool applyPresetToCamera(ICamera* camera, const CameraPreset& preset) - { - if (!camera) - return false; - - CTargetPose target; - target.position = preset.position; - target.orientation = preset.orientation; - target.hasDistance = preset.hasDistance; - target.distance = preset.distance; - target.hasOrbitState = preset.hasOrbitState; - target.orbitU = preset.orbitU; - target.orbitV = preset.orbitV; - target.orbitDistance = preset.orbitDistance; - - return m_targetPoseController.apply(camera, target); - } - - inline void appendVirtualEventLog(std::string_view source, std::string_view controller, uint32_t planarIx, ICamera* camera, const CVirtualGimbalEvent* events, uint32_t count) - { - m_uiVirtualEventsThisFrame += count; - const std::string sourceStr(source); - const std::string controllerStr(controller); - 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 ctrl=%s cam=%s planar=%u event=%s mag=%.6f", - static_cast(m_realFrameIx), - sourceStr.c_str(), - controllerStr.c_str(), - cameraName.c_str(), - planarIx, - eventName, - events[i].magnitude); - m_virtualEventLog.push_back({ - m_realFrameIx, - events[i].type, - events[i].magnitude, - sourceStr, - controllerStr, - cameraName, - planarIx, - std::move(line) - }); - } - - while (m_virtualEventLog.size() > m_virtualEventLogMax) - m_virtualEventLog.pop_front(); - } - - inline void applyConstraintsToCamera(ICamera* camera) - { - if (!m_cameraConstraints.enabled || !camera) - return; - - auto clampOrbitDistance = [&](auto* orbit) - { - if (m_cameraConstraints.clampDistance) - { - const float clamped = std::clamp(orbit->getDistance(), m_cameraConstraints.minDistance, m_cameraConstraints.maxDistance); - orbit->setDistance(clamped); - } - }; - - if (withOrbitLikeCamera(camera, clampOrbitDistance)) - return; - - if (!(m_cameraConstraints.clampPitch || m_cameraConstraints.clampYaw || m_cameraConstraints.clampRoll)) - return; - - const auto& gimbal = camera->getGimbal(); - const auto pos = gimbal.getPosition(); - const auto eulerDeg = glm::degrees(glm::eulerAngles(gimbal.getOrientation())); - - auto clamped = eulerDeg; - if (m_cameraConstraints.clampPitch) - clamped.x = std::clamp(clamped.x, m_cameraConstraints.pitchMinDeg, m_cameraConstraints.pitchMaxDeg); - if (m_cameraConstraints.clampYaw) - clamped.y = std::clamp(clamped.y, m_cameraConstraints.yawMinDeg, m_cameraConstraints.yawMaxDeg); - if (m_cameraConstraints.clampRoll) - clamped.z = std::clamp(clamped.z, m_cameraConstraints.rollMinDeg, m_cameraConstraints.rollMaxDeg); - - if (clamped.x == eulerDeg.x && clamped.y == eulerDeg.y && clamped.z == eulerDeg.z) - return; - - CameraPreset preset; - preset.position = pos; - preset.orientation = glm::quat(glm::radians(clamped)); - applyPresetToCamera(camera, preset); - } - - inline void applyVirtualEventScaling(std::vector& events, uint32_t count) - { - for (uint32_t i = 0u; i < count; ++i) - { - auto& ev = events[i]; - const auto type = ev.type; - - if (type == CVirtualGimbalEvent::MoveForward || type == CVirtualGimbalEvent::MoveBackward || - type == CVirtualGimbalEvent::MoveLeft || type == CVirtualGimbalEvent::MoveRight || - type == CVirtualGimbalEvent::MoveUp || type == CVirtualGimbalEvent::MoveDown) - { - ev.magnitude *= m_cameraControls.translationScale; - } - else if (type == CVirtualGimbalEvent::TiltUp || type == CVirtualGimbalEvent::TiltDown || - type == CVirtualGimbalEvent::PanLeft || type == CVirtualGimbalEvent::PanRight || - type == CVirtualGimbalEvent::RollLeft || type == CVirtualGimbalEvent::RollRight) - { - ev.magnitude *= m_cameraControls.rotationScale; - } - } - } - - inline void remapTranslationToWorld(ICamera* camera, std::vector& events, uint32_t& count) - { - if (!camera) - return; - - float64_t3 worldDelta = float64_t3(0.0); - std::vector filtered; - filtered.reserve(events.size()); - - for (uint32_t i = 0u; i < count; ++i) - { - const auto& ev = events[i]; - switch (ev.type) - { - case CVirtualGimbalEvent::MoveRight: worldDelta.x += ev.magnitude; break; - case CVirtualGimbalEvent::MoveLeft: worldDelta.x -= ev.magnitude; break; - case CVirtualGimbalEvent::MoveUp: worldDelta.y += ev.magnitude; break; - case CVirtualGimbalEvent::MoveDown: worldDelta.y -= ev.magnitude; break; - case CVirtualGimbalEvent::MoveForward: worldDelta.z += ev.magnitude; break; - case CVirtualGimbalEvent::MoveBackward: worldDelta.z -= ev.magnitude; break; - default: - filtered.emplace_back(ev); - break; - } - } - - if (worldDelta.x == 0.0 && worldDelta.y == 0.0 && worldDelta.z == 0.0) - { - events = std::move(filtered); - count = static_cast(events.size()); - return; - } - - const auto& gimbal = camera->getGimbal(); - const auto right = gimbal.getXAxis(); - const auto up = gimbal.getYAxis(); - const auto forward = gimbal.getZAxis(); - - const float64_t3 localDelta = float64_t3( - glm::dot(worldDelta, right), - glm::dot(worldDelta, up), - glm::dot(worldDelta, forward) - ); - - auto emitAxis = [&](double v, CVirtualGimbalEvent::VirtualEventType pos, CVirtualGimbalEvent::VirtualEventType neg) - { - if (v == 0.0) - return; - auto& ev = filtered.emplace_back(); - ev.type = (v > 0.0) ? pos : neg; - ev.magnitude = std::abs(v); - }; - - emitAxis(localDelta.x, CVirtualGimbalEvent::MoveRight, CVirtualGimbalEvent::MoveLeft); - emitAxis(localDelta.y, CVirtualGimbalEvent::MoveUp, CVirtualGimbalEvent::MoveDown); - emitAxis(localDelta.z, CVirtualGimbalEvent::MoveForward, CVirtualGimbalEvent::MoveBackward); - - events = std::move(filtered); - count = static_cast(events.size()); - } - - inline void applyPresetToTargets(const CameraPreset& preset) - { - if (!m_playbackAffectsAll) - { - applyPresetToCamera(getActiveCamera(), preset); - return; - } - - std::unordered_set visited; - for (auto& binding : windowBindings) - { - auto& planar = m_planarProjections[binding.activePlanarIx]; - if (!planar) - continue; - auto* camera = planar->getCamera(); - if (!camera) - continue; - const auto id = camera->getGimbal().getID(); - if (visited.insert(id).second) - applyPresetToCamera(camera, preset); - } - } - - inline void updatePlayback(double dtSec) - { - if (!m_playback.playing || m_keyframes.empty()) - return; - - m_playback.time += static_cast(dtSec * m_playback.speed); - - const float duration = m_keyframes.back().time; - if (duration <= 0.f) - { - applyPresetToTargets(m_keyframes.back().preset); - return; - } - - if (m_playback.loop) - { - while (m_playback.time > duration) - m_playback.time -= duration; - } - else if (m_playback.time > duration) - { - m_playback.time = duration; - m_playback.playing = false; - } - - const auto time = m_playback.time; - if (m_keyframes.size() == 1) - { - applyPresetToTargets(m_keyframes.front().preset); - return; - } - - size_t idx = 0u; - while (idx + 1u < m_keyframes.size() && m_keyframes[idx + 1u].time < time) - ++idx; - - const auto& a = m_keyframes[idx]; - const auto& b = m_keyframes[std::min(idx + 1u, m_keyframes.size() - 1u)]; - - if (b.time <= a.time) - { - applyPresetToTargets(a.preset); - return; - } - - const double alpha = static_cast(time - a.time) / static_cast(b.time - a.time); - - CameraPreset blended; - blended.position = a.preset.position + (b.preset.position - a.preset.position) * alpha; - blended.orientation = glm::slerp(a.preset.orientation, b.preset.orientation, static_cast(alpha)); - blended.hasDistance = a.preset.hasDistance || b.preset.hasDistance; - if (blended.hasDistance) - { - const float da = a.preset.hasDistance ? a.preset.distance : b.preset.distance; - const float db = b.preset.hasDistance ? b.preset.distance : a.preset.distance; - blended.distance = da + (db - da) * static_cast(alpha); - } - blended.hasOrbitState = a.preset.hasOrbitState || b.preset.hasOrbitState; - if (blended.hasOrbitState) - { - const double ua = a.preset.hasOrbitState ? a.preset.orbitU : b.preset.orbitU; - const double ub = b.preset.hasOrbitState ? b.preset.orbitU : a.preset.orbitU; - const double va = a.preset.hasOrbitState ? a.preset.orbitV : b.preset.orbitV; - const double vb = b.preset.hasOrbitState ? b.preset.orbitV : a.preset.orbitV; - const float da = a.preset.hasOrbitState ? a.preset.orbitDistance : b.preset.orbitDistance; - const float db = b.preset.hasOrbitState ? b.preset.orbitDistance : a.preset.orbitDistance; - - blended.orbitU = ua + (ub - ua) * alpha; - blended.orbitV = va + (vb - va) * alpha; - blended.orbitDistance = da + (db - da) * static_cast(alpha); - } - - applyPresetToTargets(blended); - } - - inline bool savePresetsToFile(const system::path& path) - { - json root; - root["presets"] = json::array(); - - for (const auto& preset : m_presets) - { - json j; - j["name"] = preset.name; - j["identifier"] = preset.identifier; - j["position"] = { preset.position.x, preset.position.y, preset.position.z }; - j["orientation"] = { preset.orientation.x, preset.orientation.y, preset.orientation.z, preset.orientation.w }; - if (preset.hasDistance) - j["distance"] = preset.distance; - if (preset.hasOrbitState) - { - j["orbit_u"] = preset.orbitU; - j["orbit_v"] = preset.orbitV; - j["orbit_distance"] = preset.orbitDistance; - } - root["presets"].push_back(std::move(j)); - } - - std::ofstream out(path.string(), std::ios::binary); - if (!out) - return false; - out << root.dump(2); - return true; - } - - inline bool loadPresetsFromFile(const system::path& path) - { - std::ifstream in(path.string(), std::ios::binary); - if (!in) - return false; - - json root; - in >> root; - if (!root.contains("presets")) - return false; - - m_presets.clear(); - for (const auto& entry : root["presets"]) - { - CameraPreset preset; - if (entry.contains("name")) - preset.name = entry["name"].get(); - if (entry.contains("identifier")) - preset.identifier = entry["identifier"].get(); - if (entry.contains("position") && entry["position"].is_array()) - { - auto arr = entry["position"]; - preset.position = float64_t3(arr[0].get(), arr[1].get(), arr[2].get()); - } - if (entry.contains("orientation") && entry["orientation"].is_array()) - { - auto arr = entry["orientation"]; - preset.orientation = glm::quat( - arr[3].get(), - arr[0].get(), - arr[1].get(), - arr[2].get() - ); - } - if (entry.contains("distance")) - { - preset.distance = entry["distance"].get(); - preset.hasDistance = true; - } - if (entry.contains("orbit_u")) - { - preset.orbitU = entry["orbit_u"].get(); - preset.hasOrbitState = true; - } - if (entry.contains("orbit_v")) - { - preset.orbitV = entry["orbit_v"].get(); - preset.hasOrbitState = true; - } - if (entry.contains("orbit_distance")) - { - preset.orbitDistance = entry["orbit_distance"].get(); - preset.hasOrbitState = true; - } - m_presets.emplace_back(std::move(preset)); - } - - return true; - } - - inline void imguiListen() - { - ImGuiIO& io = ImGui::GetIO(); - if (m_ciMode) - { - io.IniFilename = nullptr; - useWindow = true; - } - - ImGuizmo::BeginFrame(); - { - if (!m_ciMode) - { - } - - SImResourceInfo info; - info.samplerIx = (uint16_t)nbl::ext::imgui::UI::DefaultSamplerIx::USER; - - // ORBIT CAMERA TEST - { - for (auto& planar : m_planarProjections) - { - auto* camera = planar->getCamera(); - withOrbitLikeCamera(camera, [&](auto* orbit) - { - auto targetPostion = hlsl::transpose(getMatrix3x4As4x4(m_model))[3]; - orbit->target(targetPostion); - orbit->manipulate({}, {}); - }); - } - } - - // render bound planar camera views onto GUI windows - if (useWindow) - { - syncVisualDebugWindowBindings(); - const bool hideSceneGizmos = enableActiveCameraMovement || (m_scriptedInput.enabled && m_scriptedInput.visualDebug); - if(hideSceneGizmos) - ImGuizmo::Enable(false); - else - ImGuizmo::Enable(true); - - size_t gizmoIx = {}; - size_t manipulationCounter = {}; - const std::optional modelInUseIx = ImGuizmo::IsUsingAny() ? std::optional(boundPlanarCameraIxToManipulate.has_value() ? 1u + boundPlanarCameraIxToManipulate.value() : 0u) : std::optional(std::nullopt); - - for (uint32_t windowIx = 0; windowIx < windowBindings.size(); ++windowIx) - { - // setup - { - const auto& rw = wInit.renderWindows[windowIx]; - const ImGuiCond windowCond = m_ciMode ? ImGuiCond_Always : ImGuiCond_Appearing; - ImGui::SetNextWindowPos({ rw.iPos.x, rw.iPos.y }, windowCond); - ImGui::SetNextWindowSize({ rw.iSize.x, rw.iSize.y }, windowCond); - } - ImGui::SetNextWindowSizeConstraints(ImVec2(0x45, 0x45), ImVec2(7680, 4320)); - - ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); - ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f)); - ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.0f, 0.0f, 0.0f, 0.0f)); - const std::string ident = "Render Window \"" + std::to_string(windowIx) + "\""; - - ImGui::Begin(ident.data(), 0, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoBringToFrontOnFocus); - const ImVec2 contentRegionSize = ImGui::GetContentRegionAvail(), windowPos = ImGui::GetWindowPos(), cursorPos = ImGui::GetCursorScreenPos(); - - ImGuiWindow* window = ImGui::GetCurrentWindow(); - { - const auto mPos = ImGui::GetMousePos(); - - if (mPos.x < cursorPos.x || mPos.y < cursorPos.y || mPos.x > cursorPos.x + contentRegionSize.x || mPos.y > cursorPos.y + contentRegionSize.y) - window->Flags &= ~ImGuiWindowFlags_NoMove; - else - window->Flags |= ImGuiWindowFlags_NoMove; - } - - // setup bound entities for the window like camera & projections - auto& binding = windowBindings[windowIx]; - auto& planarBound = m_planarProjections[binding.activePlanarIx]; - assert(planarBound); - - binding.aspectRatio = contentRegionSize.x / contentRegionSize.y; - auto* planarViewCameraBound = planarBound->getCamera(); - - assert(planarViewCameraBound); - assert(binding.boundProjectionIx.has_value()); - - auto& projection = planarBound->getPlanarProjections()[binding.boundProjectionIx.value()]; - applyDollyZoomProjection(planarViewCameraBound, projection); - projection.update(binding.leftHandedProjection, binding.aspectRatio); - - // TODO: - // would be nice to normalize imguizmo visual vectors (possible with styles) - - // first 0th texture is for UI texture atlas, then there are our window textures - auto fboImguiTextureID = windowIx + 1u; - info.textureID = fboImguiTextureID; - - if(binding.allowGizmoAxesToFlip) - ImGuizmo::AllowAxisFlip(true); - else - ImGuizmo::AllowAxisFlip(false); - - if(projection.getParameters().m_type == IPlanarProjection::CProjection::Orthographic) - ImGuizmo::SetOrthographic(true); - else - ImGuizmo::SetOrthographic(false); - - ImGuizmo::SetDrawlist(); - ImGui::Image(info, contentRegionSize); - ImGuizmo::SetRect(cursorPos.x, cursorPos.y, contentRegionSize.x, contentRegionSize.y); - { - const char* projLabel = projection.getParameters().m_type == IPlanarProjection::CProjection::Perspective ? "Persp" : "Ortho"; - const std::string overlayText = "Planar " + std::to_string(binding.activePlanarIx) + " | " + projLabel + " | W" + std::to_string(windowIx); - const std::string cameraText = std::string(getCameraTypeLabel(planarViewCameraBound)) + ": " + std::string(getCameraTypeDescription(planarViewCameraBound)); - const ImVec2 textSize = ImGui::CalcTextSize(overlayText.c_str()); - const ImVec2 descSize = ImGui::CalcTextSize(cameraText.c_str()); - const ImVec2 pad = ImVec2(6.0f, 4.0f); - const float lineGap = 2.0f; - const float width = std::max(textSize.x, descSize.x); - const float height = textSize.y + descSize.y + lineGap + pad.y * 2.0f; - ImVec2 overlayPos = ImVec2(cursorPos.x + contentRegionSize.x - width - pad.x * 2.0f - 6.0f, cursorPos.y + 6.0f); - overlayPos.x = std::max(overlayPos.x, cursorPos.x + 6.0f); - ImVec2 overlayMax = ImVec2(overlayPos.x + width + pad.x * 2.0f, overlayPos.y + height); - auto* drawList = ImGui::GetWindowDrawList(); - drawList->AddRectFilled(overlayPos, overlayMax, ImGui::ColorConvertFloat4ToU32(ImVec4(0.05f, 0.06f, 0.08f, 0.80f)), 6.0f); - drawList->AddRect(overlayPos, overlayMax, ImGui::ColorConvertFloat4ToU32(ImVec4(0.60f, 0.66f, 0.76f, 0.80f)), 6.0f); - drawList->AddText(ImVec2(overlayPos.x + pad.x, overlayPos.y + pad.y), ImGui::ColorConvertFloat4ToU32(ImVec4(0.96f, 0.98f, 1.0f, 1.0f)), overlayText.c_str()); - drawList->AddText(ImVec2(overlayPos.x + pad.x, overlayPos.y + pad.y + textSize.y + lineGap), ImGui::ColorConvertFloat4ToU32(ImVec4(0.78f, 0.82f, 0.90f, 1.0f)), cameraText.c_str()); - } - - // I will assume we need to focus a window to start manipulating objects from it - if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) - { - if (!(m_scriptedInput.enabled && m_scriptedInput.exclusive)) - activeRenderWindowIx = windowIx; - } - - // we render a scene from view of a camera bound to planar window - ImGuizmoPlanarM16InOut imguizmoPlanar; - imguizmoPlanar.view = getCastedMatrix(hlsl::transpose(getMatrix3x4As4x4(planarViewCameraBound->getGimbal().getViewMatrix()))); - imguizmoPlanar.projection = getCastedMatrix(hlsl::transpose(projection.getProjectionMatrix())); - const auto viewMatrix = getMatrix3x4As4x4(getCastedMatrix(planarViewCameraBound->getGimbal().getViewMatrix())); - const auto projectionMatrix = getCastedMatrix(projection.getProjectionMatrix()); - const auto& projectionParams = projection.getParameters(); - drawWorldReferenceOverlay(cursorPos, contentRegionSize, viewMatrix, projectionMatrix, binding.leftHandedProjection, projectionParams.m_zNear, projectionParams.m_zFar); - - if (flipGizmoY) // note we allow to flip gizmo just to match our coordinates - imguizmoPlanar.projection[1][1] *= -1.f; // https://johannesugb.github.io/gpu-programming/why-do-opengl-proj-matrices-fail-in-vulkan/ - - static constexpr float identityMatrix[] = - { - 1.f, 0.f, 0.f, 0.f, - 0.f, 1.f, 0.f, 0.f, - 0.f, 0.f, 1.f, 0.f, - 0.f, 0.f, 0.f, 1.f - }; - - if(!hideSceneGizmos && binding.enableDebugGridDraw) - ImGuizmo::DrawGrid(&imguizmoPlanar.view[0][0], &imguizmoPlanar.projection[0][0], identityMatrix, 100.f); - - if (!hideSceneGizmos) - { - for (uint32_t modelIx = 0; modelIx < 1u + m_planarProjections.size(); modelIx++) - { - ImGuizmo::PushID(gizmoIx); ++gizmoIx; - - const bool isCameraGimbalTarget = modelIx; // I assume scene demo model is 0th ix, left are planar cameras - ICamera* const targetGimbalManipulationCamera = isCameraGimbalTarget ? m_planarProjections[modelIx - 1u]->getCamera() : nullptr; - - // if we try to manipulate a camera which appears to be the same camera we see scene from then obvsly it doesn't make sense to manipulate its gizmo so we skip it - // EDIT: it actually makes some sense if you assume render planar view is rendered with ortho projection, but we would need to add imguizmo controller virtual map - // to ban forward/backward in this mode if this condition is true - if (targetGimbalManipulationCamera == planarViewCameraBound) - { - ImGuizmo::PopID(); - continue; - } - - ImGuizmoModelM16InOut imguizmoModel; - - if (isCameraGimbalTarget) - { - assert(targetGimbalManipulationCamera); - imguizmoModel.inTRS = getCastedMatrix(targetGimbalManipulationCamera->getGimbal().template operator() < float64_t4x4 > ()); - } - else - imguizmoModel.inTRS = hlsl::transpose(getMatrix3x4As4x4(m_model)); - - const float gizmoWorldRadius = 0.22f; - float32_t3 gizmoWorldPos = {}; - if (isCameraGimbalTarget) - gizmoWorldPos = getCastedVector(targetGimbalManipulationCamera->getGimbal().getPosition()); - else - { - const auto modelPos = hlsl::transpose(getMatrix3x4As4x4(m_model))[3]; - gizmoWorldPos = float32_t3(modelPos.x, modelPos.y, modelPos.z); - } - - const auto viewPos = mul(viewMatrix, float32_t4(gizmoWorldPos, 1.0f)); - const float depth = std::max(0.001f, std::abs(viewPos.z)); - float gizmoSizeClip = 0.1f; - if (projection.getParameters().m_type == IPlanarProjection::CProjection::Perspective) - gizmoSizeClip = (gizmoWorldRadius * projectionMatrix[1][1]) / depth; - else - gizmoSizeClip = gizmoWorldRadius * projectionMatrix[1][1]; - ImGuizmo::SetGizmoSizeClipSpace(gizmoSizeClip); - - imguizmoModel.outTRS = imguizmoModel.inTRS; - { - const bool success = ImGuizmo::Manipulate(&imguizmoPlanar.view[0][0], &imguizmoPlanar.projection[0][0], ImGuizmo::OPERATION::UNIVERSAL, mCurrentGizmoMode, &imguizmoModel.outTRS[0][0], &imguizmoModel.outDeltaTRS[0][0], useSnap ? &snap[0] : nullptr); - - if (success) - { - if (targetGimbalManipulationCamera) - { - const auto referenceFrame = getCastedMatrix(*reinterpret_cast(ImGuizmo::GetReferenceFrame())); - - boundCameraToManipulate = smart_refctd_ptr(targetGimbalManipulationCamera); - boundPlanarCameraIxToManipulate = modelIx - 1u; - - // TODO: TO BE REMOVED, ONLY FOR TESTING ITS INCOMPLETE TYPE! - const auto& imguizmoCtx = ImGuizmo::GetContext(); - - struct - { - float32_t3 t, r, s; - } out, delta; - - ImGuizmo::DecomposeMatrixToComponents(&imguizmoModel.outTRS[0][0], &out.t[0], &out.r[0], &out.s[0]); - ImGuizmo::DecomposeMatrixToComponents(&imguizmoModel.outDeltaTRS[0][0], &delta.t[0], &delta.r[0], &delta.s[0]); - { - std::vector virtualEvents; - - auto requestMagnitudeUpdateWithScalar = [&](float signPivot, float dScalar, float dMagnitude, auto positive, auto negative) - { - if (dScalar != signPivot) - { - auto& ev = virtualEvents.emplace_back(); - auto code = (dScalar > signPivot) ? positive : negative; - - ev.type = code; - ev.magnitude += dMagnitude; - } - }; - - // TODO TESTING STUFF WITH MY IMGUIZMO UPDATES - // IT WILL BE REMOVED ONCE ALL TESTS ARE DONE - // AND CONTROLLER API WILL BE USED INSTEAD - - // translations - { - ImGuizmo::OPERATION ioType; - const auto dScalar = ImGuizmo::GetTranslationDeltaScalar(&ioType); - - if (dScalar) - { - switch (ioType) - { - case ImGuizmo::OPERATION::TRANSLATE_X: - { - requestMagnitudeUpdateWithScalar(0.f, dScalar, std::abs(dScalar), CVirtualGimbalEvent::VirtualEventType::MoveRight, CVirtualGimbalEvent::VirtualEventType::MoveLeft); - } break; - - case ImGuizmo::OPERATION::TRANSLATE_Y: - { - requestMagnitudeUpdateWithScalar(0.f, dScalar, std::abs(dScalar), CVirtualGimbalEvent::VirtualEventType::MoveUp, CVirtualGimbalEvent::VirtualEventType::MoveDown); - } break; - - case ImGuizmo::OPERATION::TRANSLATE_Z: - { - requestMagnitudeUpdateWithScalar(0.f, dScalar, std::abs(dScalar), CVirtualGimbalEvent::VirtualEventType::MoveForward, CVirtualGimbalEvent::VirtualEventType::MoveBackward); - } break; - - default: break; - } - } - } - - // TODO: ok becuase I have only one reference from imguizmo I must do it differently when - // I have local base && want to do rotation with respect to world instead; we almost there - - // rotations - { - ImGuizmo::OPERATION ioType; - float dRadians = ImGuizmo::GetRotationDeltaRadians(&ioType); - - if (dRadians) - { - switch (ioType) - { - case ImGuizmo::OPERATION::ROTATE_X: - { - requestMagnitudeUpdateWithScalar(0.f, dRadians, std::abs(dRadians), CVirtualGimbalEvent::VirtualEventType::TiltUp, CVirtualGimbalEvent::VirtualEventType::TiltDown); - } break; - - case ImGuizmo::OPERATION::ROTATE_Y: - { - requestMagnitudeUpdateWithScalar(0.f, dRadians, std::abs(dRadians), CVirtualGimbalEvent::VirtualEventType::PanRight, CVirtualGimbalEvent::VirtualEventType::PanLeft); - } break; - - case ImGuizmo::OPERATION::ROTATE_Z: - { - requestMagnitudeUpdateWithScalar(0.f, dRadians, std::abs(dRadians), CVirtualGimbalEvent::VirtualEventType::RollRight, CVirtualGimbalEvent::VirtualEventType::RollLeft); - } break; - - default: - assert(false); break; // should never be hit - } - } - } - - const auto vCount = virtualEvents.size(); - - if (vCount) - { - const float pMoveSpeed = targetGimbalManipulationCamera->getMoveSpeedScale(); - const float pRotationSpeed = targetGimbalManipulationCamera->getRotationSpeedScale(); - - // I start to think controller should be able to set sensitivity to scale magnitudes of generated events - // in order for camera to not keep any magnitude scalars like move or rotation speed scales - - targetGimbalManipulationCamera->setMoveSpeedScale(1); - targetGimbalManipulationCamera->setRotationSpeedScale(1); - - targetGimbalManipulationCamera->manipulate({ virtualEvents.data(), vCount }, &referenceFrame); - - targetGimbalManipulationCamera->setMoveSpeedScale(pMoveSpeed); - targetGimbalManipulationCamera->setRotationSpeedScale(pRotationSpeed); - } - - } - } - else - { - // again, for scene demo model full affine transformation without limits is assumed - m_model = float32_t3x4(hlsl::transpose(imguizmoModel.outTRS)); - boundCameraToManipulate = nullptr; - boundPlanarCameraIxToManipulate = std::nullopt; - } - } - - if (ImGuizmo::IsOver() and not ImGuizmo::IsUsingAny() && not enableActiveCameraMovement) - { - if (targetGimbalManipulationCamera && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) - { - const uint32_t newPlanarIx = modelIx - 1u; - if (newPlanarIx < m_planarProjections.size()) - { - binding.activePlanarIx = newPlanarIx; - binding.pickDefaultProjections(m_planarProjections[binding.activePlanarIx]->getPlanarProjections()); - if (!(m_scriptedInput.enabled && m_scriptedInput.exclusive)) - activeRenderWindowIx = windowIx; - } - } - - ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.2f, 0.2f, 0.2f, 0.8f)); - ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); - ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.5f); - - ImGuiIO& io = ImGui::GetIO(); - ImVec2 mousePos = io.MousePos; - ImGui::SetNextWindowPos(ImVec2(mousePos.x + 10, mousePos.y + 10), ImGuiCond_Always); - - ImGui::Begin("InfoOverlay", nullptr, - ImGuiWindowFlags_NoDecoration | - ImGuiWindowFlags_AlwaysAutoResize | - ImGuiWindowFlags_NoSavedSettings); - - std::string ident; - - if (targetGimbalManipulationCamera) - ident = targetGimbalManipulationCamera->getIdentifier(); - else - ident = "Geometry Creator Object"; - - ImGui::Text("Identifier: %s", ident.c_str()); - ImGui::Text("Object Ix: %u", modelIx); - if (targetGimbalManipulationCamera) - { - ImGui::Separator(); - ImGui::TextDisabled("RMB: switch view to this camera"); - ImGui::TextDisabled("LMB drag: manipulate gizmo"); - ImGui::TextDisabled("SPACE: toggle move mode"); - } - - ImGui::End(); - - ImGui::PopStyleVar(); - ImGui::PopStyleColor(2); - } - } - ImGuizmo::PopID(); - } - } - - ImGui::End(); - ImGui::PopStyleColor(1); - ImGui::PopStyleVar(3); - } - if (windowBindings.size() > 1u) - { - const auto& topRw = wInit.renderWindows[0]; - const float splitY = topRw.iPos.y + topRw.iSize.y; - const float gap = std::max(0.0f, wInit.renderWindows[1].iPos.y - splitY); - ImGui::SetNextWindowPos(ImVec2(0.0f, 0.0f), ImGuiCond_Always); - ImGui::SetNextWindowSize(io.DisplaySize, ImGuiCond_Always); - ImGui::Begin("SplitOverlay", nullptr, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoBringToFrontOnFocus); - auto* drawList = ImGui::GetWindowDrawList(); - if (gap >= 2.0f) - drawList->AddRectFilled(ImVec2(0.0f, splitY), ImVec2(io.DisplaySize.x, splitY + gap), ImGui::ColorConvertFloat4ToU32(ImVec4(0.05f, 0.06f, 0.08f, 0.85f))); - else - drawList->AddLine(ImVec2(0.0f, splitY), ImVec2(io.DisplaySize.x, splitY), ImGui::ColorConvertFloat4ToU32(ImVec4(0.80f, 0.84f, 0.92f, 0.75f)), 2.0f); - ImGui::End(); - } - assert(manipulationCounter <= 1u); - } - // render selected camera view onto full screen - else - { - info.textureID = 1u + activeRenderWindowIx; - - 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); - const ImVec2 contentRegionSize = ImGui::GetContentRegionAvail(), windowPos = ImGui::GetWindowPos(), cursorPos = ImGui::GetCursorScreenPos(); - { - auto& binding = windowBindings[activeRenderWindowIx]; - auto& planarBound = m_planarProjections[binding.activePlanarIx]; - assert(planarBound); - - binding.aspectRatio = contentRegionSize.x / contentRegionSize.y; - auto* planarViewCameraBound = planarBound->getCamera(); - - assert(planarViewCameraBound); - assert(binding.boundProjectionIx.has_value()); - - auto& projection = planarBound->getPlanarProjections()[binding.boundProjectionIx.value()]; - applyDollyZoomProjection(planarViewCameraBound, projection); - projection.update(binding.leftHandedProjection, binding.aspectRatio); - } - - ImGui::Image(info, contentRegionSize); - ImGuizmo::SetRect(cursorPos.x, cursorPos.y, contentRegionSize.x, contentRegionSize.y); - - ImGui::End(); - ImGui::PopStyleColor(1); - } - } - - drawScriptVisualDebugOverlay(io.DisplaySize); - DrawControlPanel(); - UpdateBoundCameraMovement(); - UpdateCursorVisibility(); - - // update camera matrices for scene rendering - { - for (uint32_t i = 0u; i < windowBindings.size(); ++i) - { - auto& binding = windowBindings[i]; - - auto& planarBound = m_planarProjections[binding.activePlanarIx]; - assert(planarBound); - auto* boundPlanarCamera = planarBound->getCamera(); - - assert(binding.boundProjectionIx.has_value()); - auto& projection = planarBound->getPlanarProjections()[binding.boundProjectionIx.value()]; - applyDollyZoomProjection(boundPlanarCamera, projection); - projection.update(binding.leftHandedProjection, binding.aspectRatio); - - auto viewMatrix = getCastedMatrix(boundPlanarCamera->getGimbal().getViewMatrix()); - auto viewProjMatrix = mul(getCastedMatrix(projection.getProjectionMatrix()), getMatrix3x4As4x4(viewMatrix)); - - binding.viewMatrix = viewMatrix; - binding.viewProjMatrix = viewProjMatrix; - } - } - - - } - - inline bool shouldCaptureOSCursor() - { - if (!enableActiveCameraMovement || !captureCursorInMoveMode) - return false; - if (m_ciMode || m_scriptedInput.enabled) - return false; - if (!m_window || !m_window->hasInputFocus() || !m_window->hasMouseFocus()) - return false; - return true; - } - - inline void UpdateBoundCameraMovement() - { - ImGuiIO& io = ImGui::GetIO(); - - if (ImGui::IsKeyPressed(ImGuiKey_Space)) - enableActiveCameraMovement = !enableActiveCameraMovement; - - if (enableActiveCameraMovement) - { - io.ConfigFlags |= ImGuiConfigFlags_NoMouse; - io.MouseDrawCursor = false; - io.WantCaptureMouse = false; - - if (shouldCaptureOSCursor()) - { - ImVec2 viewportSize = io.DisplaySize; - auto* cc = m_window->getCursorControl(); - if (cc) - { - int32_t posX = m_window->getX(); - int32_t posY = m_window->getY(); - - if (resetCursorToCenter) - { - const ICursorControl::SPosition middle{ static_cast(viewportSize.x / 2 + posX), static_cast(viewportSize.y / 2 + posY) }; - cc->setPosition(middle); - } - else - { - auto currentCursorPos = cc->getPosition(); - ICursorControl::SPosition newPos{}; - newPos.x = std::clamp(currentCursorPos.x, posX, viewportSize.x + posX); - newPos.y = std::clamp(currentCursorPos.y, posY, viewportSize.y + posY); - cc->setPosition(newPos); - } - } - } - } - else - { - io.ConfigFlags &= ~ImGuiConfigFlags_NoMouse; - io.MouseDrawCursor = false; - io.WantCaptureMouse = true; - } - } - - inline void UpdateCursorVisibility() - { - auto* cc = m_window ? m_window->getCursorControl() : nullptr; - if (!cc) - return; - cc->setVisible(!shouldCaptureOSCursor()); - } - - inline void UpdateUiMetrics() - { - m_uiLastFrameMs = static_cast(m_frameDeltaSec * 1000.0); - m_uiLastInputEvents = m_uiInputEventsThisFrame; - m_uiLastVirtualEvents = m_uiVirtualEventsThisFrame; - - m_uiFrameMs[m_uiMetricIndex] = m_uiLastFrameMs; - m_uiInputCounts[m_uiMetricIndex] = static_cast(m_uiInputEventsThisFrame); - m_uiVirtualCounts[m_uiMetricIndex] = static_cast(m_uiVirtualEventsThisFrame); - - m_uiMetricIndex = (m_uiMetricIndex + 1u) % UiMetricSamples; - m_uiInputEventsThisFrame = 0u; - m_uiVirtualEventsThisFrame = 0u; - } - - inline void DrawBadge(const char* label, const ImVec4& bg, const ImVec4& fg) - { - 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(6.0f, 2.0f)); - ImGui::Button(label); - ImGui::PopStyleVar(); - ImGui::PopStyleColor(4); - } - - inline void DrawKeyHint(const char* label, const ImVec4& bg, const ImVec4& fg) - { - 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(4.0f, 1.0f)); - ImGui::SmallButton(label); - ImGui::PopStyleVar(); - ImGui::PopStyleColor(4); - } - - inline void DrawHoverHint(const char* text) - { - if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayShort)) - { - ImGui::BeginTooltip(); - ImGui::TextUnformatted(text); - ImGui::EndTooltip(); - } - } - - inline void DrawDot(const ImVec4& color) - { - ImVec2 p = ImGui::GetCursorScreenPos(); - const float radius = 3.5f; - ImGui::GetWindowDrawList()->AddCircleFilled(ImVec2(p.x + radius, p.y + radius + 1.0f), radius, ImGui::ColorConvertFloat4ToU32(color)); - ImGui::Dummy(ImVec2(radius * 2.0f + 2.0f, radius * 2.0f)); - ImGui::SameLine(0, 6.0f); - } - - inline void DrawSectionHeader(const char* id, const char* label, const ImVec4& accent) - { - ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 4.0f); - ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.14f, 0.18f, 0.22f, 0.52f)); - if (ImGui::BeginChild(id, ImVec2(0, 20), true, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse)) - { - ImVec2 p = ImGui::GetWindowPos(); - ImVec2 s = ImGui::GetWindowSize(); - ImGui::GetWindowDrawList()->AddRectFilled(p, ImVec2(p.x + 2.0f, p.y + s.y), ImGui::ColorConvertFloat4ToU32(accent), 4.0f); - ImGui::SetCursorPosX(8.0f); - ImGui::AlignTextToFramePadding(); - ImGui::TextColored(accent, "%s", label); - } - ImGui::EndChild(); - ImGui::PopStyleColor(); - ImGui::PopStyleVar(); - ImGui::Spacing(); - } - - inline float CalcCardHeight(int rows) const - { - return ImGui::GetFrameHeightWithSpacing() * (static_cast(rows) + 1.0f) + 10.0f; - } - - inline bool BeginCard(const char* id, float height, const ImVec4& top, const ImVec4& bottom, const ImVec4& border) - { - ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 6.0f); - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(10.0f, 8.0f)); - ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0, 0, 0, 0)); - const bool open = ImGui::BeginChild(id, ImVec2(0, height), true, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse); - ImVec2 p = ImGui::GetWindowPos(); - ImVec2 s = ImGui::GetWindowSize(); - const ImU32 colTop = ImGui::ColorConvertFloat4ToU32(top); - const ImU32 colBottom = ImGui::ColorConvertFloat4ToU32(bottom); - ImGui::GetWindowDrawList()->AddRectFilledMultiColor( - p, - ImVec2(p.x + s.x, p.y + s.y), - colTop, - colTop, - colBottom, - colBottom - ); - ImGui::GetWindowDrawList()->AddRect(p, ImVec2(p.x + s.x, p.y + s.y), ImGui::ColorConvertFloat4ToU32(border), 6.0f); - return open; - } - - inline void EndCard() - { - ImGui::EndChild(); - ImGui::PopStyleColor(); - ImGui::PopStyleVar(2); - } - - - inline void DrawControlPanel() - { - const ImVec2 displaySize = ImGui::GetIO().DisplaySize; - const float panelWidth = std::clamp(displaySize.x * 0.19f, 200.0f, displaySize.x * 0.25f); - const float panelHeight = std::clamp(displaySize.y * 0.34f, 200.0f, displaySize.y * 0.50f); - const ImVec2 panelSize = { panelWidth, panelHeight }; - const ImVec2 panelPos = { 0.0f, 0.0f }; - ImGui::SetNextWindowPos(panelPos, ImGuiCond_Always); - ImGui::SetNextWindowSize(panelSize, ImGuiCond_Always); - - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(5.0f, 4.0f)); - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(4.0f, 1.0f)); - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(3.0f, 2.0f)); - ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f); - ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f); - ImGui::PushStyleVar(ImGuiStyleVar_TabRounding, 3.0f); - ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarRounding, 4.0f); - ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f); - ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2(3.0f, 2.0f)); - - ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.05f, 0.06f, 0.08f, 0.0f)); - ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.10f, 0.12f, 0.16f, 0.44f)); - ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.64f, 0.72f, 0.84f, 0.55f)); - ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.16f, 0.19f, 0.24f, 0.54f)); - ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, ImVec4(0.26f, 0.32f, 0.40f, 0.64f)); - ImGui::PushStyleColor(ImGuiCol_FrameBgActive, ImVec4(0.30f, 0.36f, 0.45f, 0.70f)); - ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.14f, 0.18f, 0.24f, 0.60f)); - ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(0.24f, 0.30f, 0.40f, 0.70f)); - ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(0.28f, 0.36f, 0.46f, 0.78f)); - ImGui::PushStyleColor(ImGuiCol_Tab, ImVec4(0.14f, 0.18f, 0.24f, 0.60f)); - ImGui::PushStyleColor(ImGuiCol_TabHovered, ImVec4(0.24f, 0.30f, 0.40f, 0.70f)); - ImGui::PushStyleColor(ImGuiCol_TabActive, ImVec4(0.20f, 0.26f, 0.36f, 0.78f)); - ImGui::PushStyleColor(ImGuiCol_TableRowBg, ImVec4(0.12f, 0.14f, 0.18f, 0.50f)); - ImGui::PushStyleColor(ImGuiCol_TableRowBgAlt, ImVec4(0.16f, 0.18f, 0.22f, 0.50f)); - ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.98f, 0.99f, 1.0f, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_TextDisabled, ImVec4(0.82f, 0.86f, 0.90f, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_Separator, ImVec4(0.54f, 0.60f, 0.70f, 0.80f)); - ImGui::PushStyleColor(ImGuiCol_SeparatorHovered, ImVec4(0.68f, 0.76f, 0.88f, 0.90f)); - ImGui::PushStyleColor(ImGuiCol_SeparatorActive, ImVec4(0.82f, 0.90f, 1.0f, 0.96f)); - - ImGui::SetNextWindowCollapsed(false, ImGuiCond_Always); - ImGui::SetNextWindowBgAlpha(0.0f); - if (m_ciMode) - ImGui::SetNextWindowFocus(); - ImGui::Begin("Control Panel", nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar); - - const ImVec4 accent = ImVec4(0.60f, 0.82f, 1.0f, 1.0f); - const ImVec4 good = ImVec4(0.45f, 0.90f, 0.60f, 1.0f); - const ImVec4 bad = ImVec4(1.0f, 0.50f, 0.45f, 1.0f); - const ImVec4 warn = ImVec4(0.95f, 0.80f, 0.45f, 1.0f); - const ImVec4 muted = ImVec4(0.92f, 0.93f, 0.95f, 1.0f); - const ImVec4 badgeText = ImVec4(0.10f, 0.11f, 0.13f, 1.0f); - const ImVec4 keyBg = ImVec4(0.20f, 0.22f, 0.25f, 1.0f); - const ImVec4 keyFg = ImVec4(0.92f, 0.94f, 0.96f, 1.0f); - const ImGuiTableFlags tableFlags = ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_RowBg | ImGuiTableFlags_PadOuterX; - const ImVec4 panelBg = ImVec4(0.03f, 0.04f, 0.05f, 0.50f); - const ImVec4 panelEdge = ImVec4(0.62f, 0.70f, 0.84f, 0.60f); - const ImVec4 panelStripe = ImVec4(0.28f, 0.56f, 0.90f, 0.70f); - const ImVec4 panelShadow = ImVec4(0.0f, 0.0f, 0.0f, 0.12f); - - { - const ImVec2 panelPos = ImGui::GetWindowPos(); - const ImVec2 panelSize = ImGui::GetWindowSize(); - auto* drawList = ImGui::GetWindowDrawList(); - drawList->AddRectFilled(ImVec2(panelPos.x + 2.0f, panelPos.y + 3.0f), ImVec2(panelPos.x + panelSize.x + 4.0f, panelPos.y + panelSize.y + 5.0f), ImGui::ColorConvertFloat4ToU32(panelShadow), 8.0f); - drawList->AddRectFilled(panelPos, ImVec2(panelPos.x + panelSize.x, panelPos.y + panelSize.y), ImGui::ColorConvertFloat4ToU32(panelBg), 6.0f); - drawList->AddRect(panelPos, ImVec2(panelPos.x + panelSize.x, panelPos.y + panelSize.y), ImGui::ColorConvertFloat4ToU32(panelEdge), 6.0f); - drawList->AddRectFilled(panelPos, ImVec2(panelPos.x + 4.0f, panelPos.y + panelSize.y), ImGui::ColorConvertFloat4ToU32(panelStripe), 6.0f); - } - - auto row = [&](const char* label, auto&& drawValue) - { - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::TextUnformatted(label); - ImGui::TableSetColumnIndex(1); - drawValue(); - }; - - auto metricMax = [&](const std::array& values, float minValue) -> float - { - float maxValue = minValue; - for (const float v : values) - maxValue = std::max(maxValue, v); - return maxValue; - }; - - auto miniStat = [&](const char* id, const char* label, const ImVec4& color, const std::array& series, float minValue, auto&& drawValue) - { - ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.14f, 0.16f, 0.19f, 0.75f)); - ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 6.0f); - if (ImGui::BeginChild(id, ImVec2(0, 56), true, ImGuiWindowFlags_NoScrollbar)) - { - ImGui::TextDisabled("%s", label); - ImGui::SetWindowFontScale(1.05f); - drawValue(); - ImGui::SetWindowFontScale(1.0f); - ImGui::PushStyleColor(ImGuiCol_PlotLines, color); - const float maxValue = metricMax(series, minValue); - ImGui::PlotLines("##plot", series.data(), static_cast(UiMetricSamples), static_cast(m_uiMetricIndex), nullptr, 0.0f, maxValue, ImVec2(0, 24)); - ImGui::PopStyleColor(); - } - ImGui::EndChild(); - ImGui::PopStyleVar(); - ImGui::PopStyleColor(); - }; - - auto calcPillWidth = [&](const char* label, const ImVec2& pad) - { - return ImGui::CalcTextSize(label).x + pad.x * 2.0f; - }; - - auto drawTogglePill = [&](const char* label, bool& value, const ImVec4& onCol, const ImVec4& offCol, const ImVec2& pad) - { - ImGui::PushStyleColor(ImGuiCol_Button, value ? onCol : offCol); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, value ? onCol : offCol); - ImGui::PushStyleColor(ImGuiCol_ButtonActive, value ? onCol : offCol); - ImGui::PushStyleColor(ImGuiCol_Text, badgeText); - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, pad); - if (ImGui::Button(label)) - value = !value; - ImGui::PopStyleVar(); - ImGui::PopStyleColor(4); - }; - - ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 6.0f); - if (ImGui::BeginChild("PanelHeader", ImVec2(0, 64), true, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse)) - { - ImGui::Dummy(ImVec2(0.0f, 1.0f)); - ImGui::SetWindowFontScale(1.08f); - ImGui::TextColored(accent, "Control Panel"); - ImGui::SetWindowFontScale(1.0f); - { - const ImVec2 badgePad = ImVec2(6.0f, 2.0f); - const float gap = ImGui::GetStyle().ItemSpacing.x; - const char* badgeWindow = useWindow ? "WINDOW" : "FULL"; - const char* badgeMove = enableActiveCameraMovement ? "MOVE ON" : "MOVE OFF"; - const char* badgeScript = m_scriptedInput.enabled ? (m_scriptedInput.exclusive ? "SCRIPT EXCL" : "SCRIPT") : "SCRIPT OFF"; - const float badgeRowWidth = calcPillWidth(badgeWindow, badgePad) - + gap + calcPillWidth(badgeMove, badgePad) - + gap + calcPillWidth(badgeScript, badgePad) - + (m_ciMode ? (gap + calcPillWidth("CI", badgePad)) : 0.0f); - ImGui::SetCursorPosX(ImGui::GetCursorPosX() + std::max(0.0f, (ImGui::GetContentRegionAvail().x - badgeRowWidth) * 0.5f)); - - DrawBadge(badgeWindow, accent, badgeText); - ImGui::SameLine(0.0f, gap); - DrawBadge(badgeMove, enableActiveCameraMovement ? good : bad, badgeText); - ImGui::SameLine(0.0f, gap); - DrawBadge(badgeScript, m_scriptedInput.enabled ? accent : ImVec4(0.35f, 0.36f, 0.38f, 1.0f), badgeText); - if (m_ciMode) - { - ImGui::SameLine(0.0f, gap); - DrawBadge("CI", warn, badgeText); - } - } - - ImGui::Dummy(ImVec2(0.0f, 2.0f)); - { - const ImVec2 keyPad = ImVec2(4.0f, 1.0f); - const float gap = ImGui::GetStyle().ItemSpacing.x; - const float groupGap = gap * 2.0f; - const float moveWidth = ImGui::CalcTextSize("Move").x + gap - + calcPillWidth("W", keyPad) + gap - + calcPillWidth("A", keyPad) + gap - + calcPillWidth("S", keyPad) + gap - + calcPillWidth("D", keyPad); - const float lookWidth = ImGui::CalcTextSize("Look").x + gap + calcPillWidth("RMB", keyPad); - const float zoomWidth = ImGui::CalcTextSize("Zoom").x + gap + calcPillWidth("MW", keyPad); - const float rowWidth = moveWidth + groupGap + lookWidth + groupGap + zoomWidth; - ImGui::SetCursorPosX(ImGui::GetCursorPosX() + std::max(0.0f, (ImGui::GetContentRegionAvail().x - rowWidth) * 0.5f)); - - ImGui::TextDisabled("Move"); - ImGui::SameLine(); - DrawKeyHint("W", keyBg, keyFg); - ImGui::SameLine(); - DrawKeyHint("A", keyBg, keyFg); - ImGui::SameLine(); - DrawKeyHint("S", keyBg, keyFg); - ImGui::SameLine(); - DrawKeyHint("D", keyBg, keyFg); - - ImGui::SameLine(0.0f, groupGap); - ImGui::TextDisabled("Look"); - ImGui::SameLine(); - DrawKeyHint("RMB", keyBg, keyFg); - - ImGui::SameLine(0.0f, groupGap); - ImGui::TextDisabled("Zoom"); - ImGui::SameLine(); - DrawKeyHint("MW", keyBg, keyFg); - } - - ImGui::Dummy(ImVec2(0.0f, 2.0f)); - if (ImGui::BeginTable("HeaderMetrics", 3, ImGuiTableFlags_SizingStretchProp)) - { - const float frameMs = std::max(0.0f, m_uiLastFrameMs); - const float fps = frameMs > 0.0f ? (1000.0f / frameMs) : 0.0f; - - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - miniStat("FrameStat", "Frame", accent, m_uiFrameMs, 16.0f, [&] - { - ImGui::TextColored(accent, "%.1f ms %.0f fps", frameMs, fps); - }); - - ImGui::TableSetColumnIndex(1); - miniStat("InputStat", "Input", accent, m_uiInputCounts, 4.0f, [&] - { - ImGui::TextColored(accent, "%u ev", m_uiLastInputEvents); - }); - - ImGui::TableSetColumnIndex(2); - miniStat("VirtualStat", "Virtual", accent, m_uiVirtualCounts, 4.0f, [&] - { - ImGui::TextColored(accent, "%u ev", m_uiLastVirtualEvents); - }); - ImGui::EndTable(); - } - } - ImGui::EndChild(); - ImGui::PopStyleVar(); - - ImGui::Spacing(); - - { - const ImVec2 togglePad = ImVec2(6.0f, 2.0f); - const float gap = ImGui::GetStyle().ItemSpacing.x; - const float rowWidth = calcPillWidth("WINDOW", togglePad) - + gap + calcPillWidth("STATUS", togglePad) - + gap + calcPillWidth("EVENT LOG", togglePad); - ImGui::SetCursorPosX(ImGui::GetCursorPosX() + std::max(0.0f, (ImGui::GetContentRegionAvail().x - rowWidth) * 0.5f)); - drawTogglePill("WINDOW", useWindow, accent, ImVec4(0.35f, 0.36f, 0.38f, 1.0f), togglePad); - DrawHoverHint("Toggle split render windows"); - ImGui::SameLine(0.0f, gap); - drawTogglePill("STATUS", m_showHud, accent, ImVec4(0.35f, 0.36f, 0.38f, 1.0f), togglePad); - DrawHoverHint("Show system and camera status panel"); - ImGui::SameLine(0.0f, gap); - drawTogglePill("EVENT LOG", m_showEventLog, accent, ImVec4(0.35f, 0.36f, 0.38f, 1.0f), togglePad); - DrawHoverHint("Show virtual event log"); - } - - ImGui::Separator(); - - if (ImGui::BeginTabBar("ControlTabs")) - { - if (m_showHud && ImGui::BeginTabItem("Status")) - { - ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 4.0f); - if (ImGui::BeginChild("StatusPanel", ImVec2(0, 0), true)) - { - ImGui::PushItemWidth(-1.0f); - const ImVec4 cardTop = ImVec4(0.20f, 0.22f, 0.26f, 0.98f); - const ImVec4 cardBottom = ImVec4(0.12f, 0.13f, 0.15f, 0.98f); - const ImVec4 cardBorder = ImVec4(0.45f, 0.48f, 0.54f, 1.0f); - - DrawSectionHeader("SessionHeader", "Session", accent); - if (BeginCard("SessionCard", CalcCardHeight(3), cardTop, cardBottom, cardBorder)) - { - if (ImGui::BeginTable("SessionTable", 2, tableFlags)) - { - ImGui::TableSetupColumn("Label", ImGuiTableColumnFlags_WidthFixed, 120.0f); - ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch); - row("Mode", [&] { DrawDot(accent); ImGui::TextColored(accent, "%s", useWindow ? "Window" : "Fullscreen"); }); - row("Active window", [&] { DrawDot(accent); ImGui::TextColored(accent, "%u", activeRenderWindowIx); }); - row("Movement", [&] { const ImVec4 c = enableActiveCameraMovement ? good : bad; DrawDot(c); ImGui::TextColored(c, "%s", enableActiveCameraMovement ? "Enabled" : "Disabled"); }); - ImGui::EndTable(); - } - } - EndCard(); - - DrawSectionHeader("CameraHeader", "Camera", accent); - - auto* activeCamera = getActiveCamera(); - if (activeCamera) - { - const auto& gimbal = activeCamera->getGimbal(); - const auto pos = gimbal.getPosition(); - const auto euler = glm::degrees(glm::eulerAngles(gimbal.getOrientation())); - - if (BeginCard("CameraCard", CalcCardHeight(5), cardTop, cardBottom, cardBorder)) - { - if (ImGui::BeginTable("CameraTable", 2, tableFlags)) - { - ImGui::TableSetupColumn("Label", ImGuiTableColumnFlags_WidthFixed, 120.0f); - ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch); - row("Name", [&] { DrawDot(accent); ImGui::TextColored(muted, "%s", activeCamera->getIdentifier().data()); }); - row("Position", [&] { DrawDot(muted); ImGui::TextColored(muted, "%.2f %.2f %.2f", pos.x, pos.y, pos.z); }); - row("Euler", [&] { DrawDot(muted); ImGui::TextColored(muted, "%.1f %.1f %.1f", euler.x, euler.y, euler.z); }); - row("Move scale", [&] { DrawDot(muted); ImGui::TextColored(muted, "%.4f", activeCamera->getMoveSpeedScale()); }); - row("Rotate scale", [&] { DrawDot(muted); ImGui::TextColored(muted, "%.4f", activeCamera->getRotationSpeedScale()); }); - ImGui::EndTable(); - } - } - EndCard(); - } - else - { - if (BeginCard("CameraCard", CalcCardHeight(2), cardTop, cardBottom, cardBorder)) - ImGui::TextDisabled("No active camera"); - EndCard(); - } - - DrawSectionHeader("ProjectionHeader", "Projection", accent); - - auto& binding = windowBindings[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 (BeginCard("ProjectionCard", CalcCardHeight(4), cardTop, cardBottom, cardBorder)) - { - if (ImGui::BeginTable("ProjectionTable", 2, tableFlags)) - { - ImGui::TableSetupColumn("Label", ImGuiTableColumnFlags_WidthFixed, 120.0f); - ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch); - row("Type", [&] { DrawDot(accent); ImGui::TextColored(muted, "%s", params.m_type == IPlanarProjection::CProjection::Perspective ? "Perspective" : "Orthographic"); }); - row("zNear", [&] { DrawDot(muted); ImGui::TextColored(muted, "%.2f", params.m_zNear); }); - row("zFar", [&] { DrawDot(muted); ImGui::TextColored(muted, "%.2f", params.m_zFar); }); - if (params.m_type == IPlanarProjection::CProjection::Perspective) - row("Fov", [&] { DrawDot(muted); ImGui::TextColored(muted, "%.1f", params.m_planar.perspective.fov); }); - else - row("Ortho width", [&] { DrawDot(muted); ImGui::TextColored(muted, "%.1f", params.m_planar.orthographic.orthoWidth); }); - ImGui::EndTable(); - } - } - EndCard(); - } - else - { - if (BeginCard("ProjectionCard", CalcCardHeight(2), cardTop, cardBottom, cardBorder)) - ImGui::TextDisabled("No projection bound"); - EndCard(); - } - ImGui::PopItemWidth(); - } - ImGui::EndChild(); - ImGui::PopStyleVar(); - ImGui::EndTabItem(); - } - - if (ImGui::BeginTabItem("Projection")) - { - ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 4.0f); - if (ImGui::BeginChild("ProjectionPanel", ImVec2(0, 0), true)) - { - ImGui::PushItemWidth(-1.0f); - auto& active = windowBindings[activeRenderWindowIx]; - const auto activeRenderWindowIxString = std::to_string(activeRenderWindowIx); - - DrawSectionHeader("PlanarSelectHeader", "Planar Selection", accent); - ImGui::Text("Active Render Window: %s", activeRenderWindowIxString.c_str()); - DrawHoverHint("Window that receives input and camera switching"); - { - const size_t planarsCount = m_planarProjections.size(); - assert(planarsCount); - - std::vector sbels(planarsCount); - for (size_t i = 0; i < planarsCount; ++i) - sbels[i] = "Planar " + std::to_string(i); - - std::vector labels(planarsCount); - for (size_t i = 0; i < planarsCount; ++i) - labels[i] = sbels[i].c_str(); - - int currentPlanarIx = static_cast(active.activePlanarIx); - if (ImGui::Combo("Active Planar", ¤tPlanarIx, labels.data(), static_cast(labels.size()))) - { - active.activePlanarIx = static_cast(currentPlanarIx); - active.pickDefaultProjections(m_planarProjections[active.activePlanarIx]->getPlanarProjections()); - } - DrawHoverHint("Select which camera the window renders"); - } - - assert(active.boundProjectionIx.has_value()); - assert(active.lastBoundPerspectivePresetProjectionIx.has_value()); - assert(active.lastBoundOrthoPresetProjectionIx.has_value()); - - const auto activePlanarIxString = std::to_string(active.activePlanarIx); - auto& planarBound = m_planarProjections[active.activePlanarIx]; - assert(planarBound); - - DrawSectionHeader("ProjectionParamsHeader", "Projection Parameters", accent); - - auto selectedProjectionType = planarBound->getPlanarProjections()[active.boundProjectionIx.value()].getParameters().m_type; - { - const char* labels[] = { "Perspective", "Orthographic" }; - int type = static_cast(selectedProjectionType); - - if (ImGui::Combo("Projection Type", &type, labels, IM_ARRAYSIZE(labels))) - { - selectedProjectionType = static_cast(type); - - switch (selectedProjectionType) - { - case IPlanarProjection::CProjection::Perspective: active.boundProjectionIx = active.lastBoundPerspectivePresetProjectionIx.value(); break; - case IPlanarProjection::CProjection::Orthographic: active.boundProjectionIx = active.lastBoundOrthoPresetProjectionIx.value(); break; - default: active.boundProjectionIx = std::nullopt; assert(false); break; - } - } - DrawHoverHint("Switch projection type for this planar"); - } - - auto getPresetName = [&](auto ix) -> std::string - { - switch (selectedProjectionType) - { - case IPlanarProjection::CProjection::Perspective: return "Perspective Projection Preset " + std::to_string(ix); - case IPlanarProjection::CProjection::Orthographic: return "Orthographic Projection Preset " + std::to_string(ix); - default: return "Unknown Projection Preset " + std::to_string(ix); - } - }; - - bool updateBoundVirtualMaps = false; - if (ImGui::BeginCombo("Projection Preset", getPresetName(active.boundProjectionIx.value()).c_str())) - { - auto& projections = planarBound->getPlanarProjections(); - - for (uint32_t i = 0; i < projections.size(); ++i) - { - const auto& projection = projections[i]; - const auto& params = projection.getParameters(); - - if (params.m_type != selectedProjectionType) - continue; - - bool isSelected = (i == active.boundProjectionIx.value()); - - if (ImGui::Selectable(getPresetName(i).c_str(), isSelected)) - { - active.boundProjectionIx = i; - updateBoundVirtualMaps |= true; - - switch (selectedProjectionType) - { - case IPlanarProjection::CProjection::Perspective: active.lastBoundPerspectivePresetProjectionIx = active.boundProjectionIx.value(); break; - case IPlanarProjection::CProjection::Orthographic: active.lastBoundOrthoPresetProjectionIx = active.boundProjectionIx.value(); break; - default: assert(false); break; - } - } - - if (isSelected) - ImGui::SetItemDefaultFocus(); - } - ImGui::EndCombo(); - } - DrawHoverHint("Switch preset projection for this planar"); - - auto* const boundCamera = planarBound->getCamera(); - auto& boundProjection = planarBound->getPlanarProjections()[active.boundProjectionIx.value()]; - assert(not boundProjection.isProjectionSingular()); - - auto updateParameters = boundProjection.getParameters(); - - if (useWindow) - ImGui::Checkbox("Allow axes to flip##allowAxesToFlip", &active.allowGizmoAxesToFlip); - DrawHoverHint("Allow ImGuizmo axes to flip based on view"); - - if(useWindow) - ImGui::Checkbox("Draw debug grid##drawDebugGrid", &active.enableDebugGridDraw); - DrawHoverHint("Toggle debug grid in the render window"); - - if (ImGui::RadioButton("LH", active.leftHandedProjection)) - active.leftHandedProjection = true; - - ImGui::SameLine(); - - if (ImGui::RadioButton("RH", not active.leftHandedProjection)) - active.leftHandedProjection = false; - DrawHoverHint("Toggle left or right handed projection"); - - updateParameters.m_zNear = std::clamp(updateParameters.m_zNear, 0.1f, 100.f); - updateParameters.m_zFar = std::clamp(updateParameters.m_zFar, 110.f, 10000.f); - - ImGui::SliderFloat("zNear", &updateParameters.m_zNear, 0.1f, 100.f, "%.2f", ImGuiSliderFlags_Logarithmic); - DrawHoverHint("Near clip plane"); - ImGui::SliderFloat("zFar", &updateParameters.m_zFar, 110.f, 10000.f, "%.1f", ImGuiSliderFlags_Logarithmic); - DrawHoverHint("Far clip plane"); - - switch (selectedProjectionType) - { - case IPlanarProjection::CProjection::Perspective: - { - ImGui::SliderFloat("Fov", &updateParameters.m_planar.perspective.fov, 20.f, 150.f, "%.1f", ImGuiSliderFlags_Logarithmic); - DrawHoverHint("Perspective field of view"); - boundProjection.setPerspective(updateParameters.m_zNear, updateParameters.m_zFar, updateParameters.m_planar.perspective.fov); - } break; - - case IPlanarProjection::CProjection::Orthographic: - { - ImGui::SliderFloat("Ortho width", &updateParameters.m_planar.orthographic.orthoWidth, 1.f, 30.f, "%.1f", ImGuiSliderFlags_Logarithmic); - DrawHoverHint("Orthographic width"); - boundProjection.setOrthographic(updateParameters.m_zNear, updateParameters.m_zFar, updateParameters.m_planar.orthographic.orthoWidth); - } break; - - default: break; - } - - DrawSectionHeader("CursorHeader", "Cursor Behaviour", accent); - if (ImGui::TreeNodeEx("Cursor Behaviour")) - { - ImGui::Checkbox("Capture OS cursor in move mode", &captureCursorInMoveMode); - DrawHoverHint("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(); - } - - if (enableActiveCameraMovement) - ImGui::TextColored(good, "Bound Camera Movement: Enabled"); - else - ImGui::TextColored(bad, "Bound Camera Movement: Disabled"); - - ImGui::Separator(); - - DrawSectionHeader("BoundCameraHeader", "Bound Camera", accent); - const auto flags = ImGuiTreeNodeFlags_DefaultOpen; - if (ImGui::TreeNodeEx("Bound Camera", flags)) - { - ImGui::Text("Type: %s", boundCamera->getIdentifier().data()); - ImGui::Text("Object Ix: %s", std::to_string(active.activePlanarIx + 1u).c_str()); - ImGui::Separator(); - { - auto* orbit = dynamic_cast(boundCamera); - const bool isOrbitLike = orbit != nullptr; - - float moveSpeed = boundCamera->getMoveSpeedScale(); - float rotationSpeed = boundCamera->getRotationSpeedScale(); - - ImGui::SliderFloat("Move speed factor", &moveSpeed, 0.0001f, 10.f, "%.4f", ImGuiSliderFlags_Logarithmic); - DrawHoverHint("Scale translation speed for this camera"); - - if (boundCamera->getAllowedVirtualEvents() & CVirtualGimbalEvent::Rotate) - ImGui::SliderFloat("Rotate speed factor", &rotationSpeed, 0.0001f, 10.f, "%.4f", ImGuiSliderFlags_Logarithmic); - DrawHoverHint("Scale rotation speed for this camera"); - - boundCamera->setMoveSpeedScale(moveSpeed); - boundCamera->setRotationSpeedScale(rotationSpeed); - - if (isOrbitLike) - { - float distance = orbit->getDistance(); - ImGui::SliderFloat("Distance", &distance, orbit->MinDistance, orbit->MaxDistance, "%.4f", ImGuiSliderFlags_Logarithmic); - DrawHoverHint("Current orbit distance"); - orbit->setDistance(distance); - } - } - - if (ImGui::TreeNodeEx("World Data", flags)) - { - auto& gimbal = boundCamera->getGimbal(); - const auto position = getCastedVector(gimbal.getPosition()); - const auto& orientation = gimbal.getOrientation(); - const auto viewMatrix = getCastedMatrix(gimbal.getViewMatrix()); - - addMatrixTable("Position", ("PositionTable_" + activePlanarIxString).c_str(), 1, 3, &position[0], false); - addMatrixTable("Orientation (Quaternion)", ("OrientationTable_" + activePlanarIxString).c_str(), 1, 4, &orientation[0], false); - addMatrixTable("View Matrix", ("ViewMatrixTable_" + activePlanarIxString).c_str(), 3, 4, &viewMatrix[0][0], false); - ImGui::TreePop(); - } - - if (ImGui::TreeNodeEx("Virtual Event Mappings", flags)) - { - displayKeyMappingsAndVirtualStatesInline(&boundProjection); - ImGui::TreePop(); - } - - ImGui::TreePop(); - } - ImGui::PopItemWidth(); - } - ImGui::EndChild(); - ImGui::PopStyleVar(); - ImGui::EndTabItem(); - } - - if (ImGui::BeginTabItem("Camera")) - { - ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 4.0f); - if (ImGui::BeginChild("CameraPanel", ImVec2(0, 0), true)) - { - ImGui::PushItemWidth(-1.0f); - DrawSectionHeader("CameraInputHeader", "Input", accent); - ImGui::Checkbox("Mirror input to all cameras", &m_cameraControls.mirrorInput); - DrawHoverHint("Apply keyboard and mouse input to every camera"); - ImGui::Checkbox("World translate", &m_cameraControls.worldTranslate); - DrawHoverHint("Translate in world space instead of camera space"); - ImGui::SliderFloat("Keyboard scale", &m_cameraControls.keyboardScale, 0.01f, 10.f, "%.2f"); - DrawHoverHint("Scale keyboard movement magnitudes"); - ImGui::SliderFloat("Mouse move scale", &m_cameraControls.mouseMoveScale, 0.01f, 10.f, "%.2f"); - DrawHoverHint("Scale mouse move magnitudes"); - ImGui::SliderFloat("Mouse scroll scale", &m_cameraControls.mouseScrollScale, 0.01f, 10.f, "%.2f"); - DrawHoverHint("Scale mouse wheel magnitudes"); - ImGui::SliderFloat("Translate scale", &m_cameraControls.translationScale, 0.01f, 10.f, "%.2f"); - DrawHoverHint("Overall translation scale for virtual events"); - ImGui::SliderFloat("Rotate scale", &m_cameraControls.rotationScale, 0.01f, 10.f, "%.2f"); - DrawHoverHint("Overall rotation scale for virtual events"); - - DrawSectionHeader("CameraConstraintsHeader", "Constraints", accent); - ImGui::Checkbox("Enable constraints", &m_cameraConstraints.enabled); - DrawHoverHint("Enable or disable all camera constraints"); - ImGui::Checkbox("Clamp distance", &m_cameraConstraints.clampDistance); - DrawHoverHint("Clamp orbit distance to min/max"); - ImGui::SliderFloat("Min distance", &m_cameraConstraints.minDistance, 0.01f, 1000.f, "%.3f", ImGuiSliderFlags_Logarithmic); - DrawHoverHint("Minimum orbit distance"); - ImGui::SliderFloat("Max distance", &m_cameraConstraints.maxDistance, 0.01f, 10000.f, "%.3f", ImGuiSliderFlags_Logarithmic); - DrawHoverHint("Maximum orbit distance"); - ImGui::Separator(); - ImGui::Checkbox("Clamp pitch", &m_cameraConstraints.clampPitch); - DrawHoverHint("Clamp pitch angle"); - ImGui::SliderFloat("Pitch min", &m_cameraConstraints.pitchMinDeg, -180.f, 180.f, "%.1f"); - DrawHoverHint("Minimum pitch in degrees"); - ImGui::SliderFloat("Pitch max", &m_cameraConstraints.pitchMaxDeg, -180.f, 180.f, "%.1f"); - DrawHoverHint("Maximum pitch in degrees"); - ImGui::Checkbox("Clamp yaw", &m_cameraConstraints.clampYaw); - DrawHoverHint("Clamp yaw angle"); - ImGui::SliderFloat("Yaw min", &m_cameraConstraints.yawMinDeg, -180.f, 180.f, "%.1f"); - DrawHoverHint("Minimum yaw in degrees"); - ImGui::SliderFloat("Yaw max", &m_cameraConstraints.yawMaxDeg, -180.f, 180.f, "%.1f"); - DrawHoverHint("Maximum yaw in degrees"); - ImGui::Checkbox("Clamp roll", &m_cameraConstraints.clampRoll); - DrawHoverHint("Clamp roll angle"); - ImGui::SliderFloat("Roll min", &m_cameraConstraints.rollMinDeg, -180.f, 180.f, "%.1f"); - DrawHoverHint("Minimum roll in degrees"); - ImGui::SliderFloat("Roll max", &m_cameraConstraints.rollMaxDeg, -180.f, 180.f, "%.1f"); - DrawHoverHint("Maximum roll in degrees"); - - DrawSectionHeader("OrbitHeader", "Orbit Target", accent); - - auto* activeCamera = getActiveCamera(); - const bool hasOrbitTarget = withOrbitLikeCamera(activeCamera, [&](auto* orbit) - { - auto target = getCastedVector(orbit->getTarget()); - if (ImGui::InputFloat3("Target", &target[0])) - orbit->target(getCastedVector(target)); - - if (ImGui::Button("Target model")) - { - auto targetPos = hlsl::transpose(getMatrix3x4As4x4(m_model))[3]; - orbit->target(targetPos); - } - DrawHoverHint("Set orbit target to the model position"); - ImGui::SameLine(); - if (ImGui::Button("Target origin")) - orbit->target(float64_t3(0.0)); - DrawHoverHint("Set orbit target to world origin"); - }); - if (!hasOrbitTarget) - { - ImGui::TextDisabled("Active camera is not orbit."); - } - ImGui::PopItemWidth(); - } - ImGui::EndChild(); - ImGui::PopStyleVar(); - ImGui::EndTabItem(); - } - - if (ImGui::BeginTabItem("Presets")) - { - ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 4.0f); - if (ImGui::BeginChild("PresetsPanel", ImVec2(0, 0), true)) - { - ImGui::PushItemWidth(-1.0f); - DrawSectionHeader("PresetsHeader", "Presets", accent); - ImGui::InputText("Preset name", m_presetName, IM_ARRAYSIZE(m_presetName)); - if (ImGui::Button("Add preset")) - { - auto* activeCamera = getActiveCamera(); - m_presets.emplace_back(capturePreset(activeCamera, m_presetName)); - } - DrawHoverHint("Store current camera as a preset"); - ImGui::SameLine(); - if (ImGui::Button("Clear presets")) - m_presets.clear(); - DrawHoverHint("Remove all presets"); - - if (!m_presets.empty()) - { - std::vector names; - names.reserve(m_presets.size()); - for (const auto& preset : m_presets) - names.push_back(preset.name.c_str()); - - static int selectedPreset = -1; - ImGui::ListBox("Preset list", &selectedPreset, names.data(), static_cast(names.size()), 6); - - if (selectedPreset >= 0 && static_cast(selectedPreset) < m_presets.size()) - { - if (ImGui::Button("Apply preset")) - applyPresetToCamera(getActiveCamera(), m_presets[static_cast(selectedPreset)]); - DrawHoverHint("Apply selected preset to the active camera"); - ImGui::SameLine(); - if (ImGui::Button("Remove preset")) - m_presets.erase(m_presets.begin() + selectedPreset); - DrawHoverHint("Remove selected preset"); - } - } - - DrawSectionHeader("PresetsStorageHeader", "Storage", accent); - ImGui::InputText("Preset file", m_presetPath, IM_ARRAYSIZE(m_presetPath)); - if (ImGui::Button("Save presets")) - { - if (!savePresetsToFile(system::path(m_presetPath))) - m_logger->log("Failed to save presets to \"%s\".", ILogger::ELL_ERROR, m_presetPath); - } - DrawHoverHint("Save presets to JSON file"); - ImGui::SameLine(); - if (ImGui::Button("Load presets")) - { - if (!loadPresetsFromFile(system::path(m_presetPath))) - m_logger->log("Failed to load presets from \"%s\".", ILogger::ELL_ERROR, m_presetPath); - } - DrawHoverHint("Load presets from JSON file"); - ImGui::PopItemWidth(); - } - ImGui::EndChild(); - ImGui::PopStyleVar(); - ImGui::EndTabItem(); - } - - if (ImGui::BeginTabItem("Playback")) - { - ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 4.0f); - if (ImGui::BeginChild("PlaybackPanel", ImVec2(0, 0), true)) - { - ImGui::PushItemWidth(-1.0f); - DrawSectionHeader("PlaybackHeader", "Playback", accent); - ImGui::Checkbox("Loop", &m_playback.loop); - DrawHoverHint("Loop playback when it reaches the end"); - ImGui::Checkbox("Override input", &m_playback.overrideInput); - DrawHoverHint("Ignore manual input during playback"); - ImGui::Checkbox("Affect all cameras", &m_playbackAffectsAll); - DrawHoverHint("Apply playback to all cameras"); - ImGui::SliderFloat("Speed", &m_playback.speed, 0.1f, 4.f, "%.2f"); - DrawHoverHint("Playback speed multiplier"); - - if (ImGui::Button(m_playback.playing ? "Pause" : "Play")) - m_playback.playing = !m_playback.playing; - DrawHoverHint("Start or pause playback"); - ImGui::SameLine(); - if (ImGui::Button("Stop")) - { - m_playback.playing = false; - m_playback.time = 0.f; - } - DrawHoverHint("Stop playback and reset time"); - - if (!m_keyframes.empty()) - { - const float duration = m_keyframes.back().time; - ImGui::SliderFloat("Time", &m_playback.time, 0.f, duration, "%.3f"); - } - - DrawSectionHeader("KeyframesHeader", "Keyframes", accent); - ImGui::InputFloat("New keyframe time", &m_newKeyframeTime, 0.1f, 1.f, "%.3f"); - DrawHoverHint("Time value for new keyframe"); - if (ImGui::Button("Add keyframe")) - { - auto* activeCamera = getActiveCamera(); - CameraKeyframe keyframe; - keyframe.time = m_newKeyframeTime; - keyframe.preset = capturePreset(activeCamera, "Keyframe"); - m_keyframes.emplace_back(std::move(keyframe)); - std::sort(m_keyframes.begin(), m_keyframes.end(), [](const auto& a, const auto& b) { return a.time < b.time; }); - } - DrawHoverHint("Add keyframe from current camera"); - ImGui::SameLine(); - if (ImGui::Button("Clear keyframes")) - m_keyframes.clear(); - DrawHoverHint("Remove all keyframes"); - - if (!m_keyframes.empty()) - { - if (ImGui::BeginChild("KeyframeList", ImVec2(0, 120), true)) - { - for (size_t i = 0; i < m_keyframes.size(); ++i) - { - ImGui::Text("[%zu] t=%.3f", i, m_keyframes[i].time); - } - } - ImGui::EndChild(); - } - ImGui::PopItemWidth(); - } - ImGui::EndChild(); - ImGui::PopStyleVar(); - ImGui::EndTabItem(); - } - - if (ImGui::BeginTabItem("Gizmo")) - { - ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 4.0f); - if (ImGui::BeginChild("GizmoPanel", ImVec2(0, 0), true)) - { - DrawSectionHeader("GizmoHeader", "Gizmo", accent); - TransformEditorContents(); - } - ImGui::EndChild(); - ImGui::PopStyleVar(); - ImGui::EndTabItem(); - } - - if (m_showEventLog && ImGui::BeginTabItem("Log")) - { - ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 4.0f); - if (ImGui::BeginChild("LogPanel", ImVec2(0, 0), true)) - { - DrawSectionHeader("LogHeader", "Virtual Events", accent); - ImGui::Checkbox("Auto-scroll", &m_logAutoScroll); - ImGui::SameLine(); - ImGui::Checkbox("Wrap", &m_logWrap); - ImGui::Separator(); - - ImGuiWindowFlags logFlags = m_logWrap ? ImGuiWindowFlags_None : ImGuiWindowFlags_HorizontalScrollbar; - if (ImGui::BeginChild("LogList", ImVec2(0, 0), false, logFlags)) - { - const float scrollY = ImGui::GetScrollY(); - const float scrollMax = ImGui::GetScrollMaxY(); - const bool wasAtBottom = scrollY >= scrollMax - 5.0f; - const size_t start = m_virtualEventLog.size() > 200 ? m_virtualEventLog.size() - 200 : 0; - if (m_logWrap) - ImGui::PushTextWrapPos(0.0f); - for (size_t i = start; i < m_virtualEventLog.size(); ++i) - { - const auto& entry = m_virtualEventLog[i]; - ImGui::TextUnformatted(entry.line.c_str()); - } - if (m_logWrap) - ImGui::PopTextWrapPos(); - if (m_logAutoScroll && wasAtBottom && !m_virtualEventLog.empty()) - ImGui::SetScrollHereY(1.0f); - } - ImGui::EndChild(); - } - ImGui::EndChild(); - ImGui::PopStyleVar(); - ImGui::EndTabItem(); - } - - ImGui::EndTabBar(); - } - - ImGui::End(); - ImGui::PopStyleColor(19); - ImGui::PopStyleVar(9); - } - - inline void TransformEditorContents() - { - 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; - - const size_t objectsCount = m_planarProjections.size() + 1u; - assert(objectsCount); - - std::vector sbels(objectsCount); - for (size_t i = 0; i < objectsCount; ++i) - sbels[i] = "Object " + std::to_string(i); - - std::vector labels(objectsCount); - for (size_t i = 0; i < objectsCount; ++i) - labels[i] = sbels[i].c_str(); - - int activeObject = boundCameraToManipulate ? static_cast(boundPlanarCameraIxToManipulate.value() + 1u) : 0; - if (ImGui::Combo("Active Object", &activeObject, labels.data(), static_cast(labels.size()))) - { - const auto newActiveObject = static_cast(activeObject); - - if (newActiveObject) // camera - { - boundPlanarCameraIxToManipulate = newActiveObject - 1u; - ICamera* const targetGimbalManipulationCamera = m_planarProjections[boundPlanarCameraIxToManipulate.value()]->getCamera(); - boundCameraToManipulate = smart_refctd_ptr(targetGimbalManipulationCamera); - } - else // gc model - { - boundPlanarCameraIxToManipulate = std::nullopt; - boundCameraToManipulate = nullptr; - } - } - - ImGuizmoModelM16InOut imguizmoModel; - - if (boundCameraToManipulate) - imguizmoModel.inTRS = getCastedMatrix(boundCameraToManipulate->getGimbal().template operator() < float64_t4x4 > ()); - else - imguizmoModel.inTRS = hlsl::transpose(getMatrix3x4As4x4(m_model)); - - imguizmoModel.outTRS = imguizmoModel.inTRS; - float* m16TRSmatrix = &imguizmoModel.outTRS[0][0]; - - std::string indent; - if (boundCameraToManipulate) - indent = boundCameraToManipulate->getIdentifier(); - else - indent = "Geometry Creator Object"; - - ImGui::Text("Identifier: \"%s\"", indent.c_str()); - { - if (ImGuizmo::IsUsingAny()) - ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), "Gizmo: In Use"); - else - ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.0f, 1.0f), "Gizmo: Idle"); - - if (ImGui::IsItemHovered()) - { - ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.2f, 0.2f, 0.2f, 0.8f)); - ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); - ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.5f); - - ImVec2 mousePos = ImGui::GetMousePos(); - ImGui::SetNextWindowPos(ImVec2(mousePos.x + 10, mousePos.y + 10), ImGuiCond_Always); - - ImGui::Begin("HoverOverlay", nullptr, - ImGuiWindowFlags_NoDecoration | - ImGuiWindowFlags_AlwaysAutoResize | - ImGuiWindowFlags_NoSavedSettings); - - ImGui::Text("Right-click and drag on the gizmo to manipulate the object."); - - ImGui::End(); - - ImGui::PopStyleVar(); - ImGui::PopStyleColor(2); - } - } - - ImGui::Separator(); - - if (!boundCameraToManipulate) - { - const auto& names = m_scene->getInitParams().geometryNames; - if (!names.empty()) - { - if (gcIndex >= names.size()) - gcIndex = 0; - - if (ImGui::BeginCombo("Object Type", names[gcIndex].c_str())) - { - for (uint32_t i = 0u; i < names.size(); ++i) - { - const bool isSelected = (gcIndex == i); - if (ImGui::Selectable(names[i].c_str(), isSelected)) - gcIndex = static_cast(i); - - if (isSelected) - ImGui::SetItemDefaultFocus(); - } - ImGui::EndCombo(); - } - } - - } - - addMatrixTable("Model (TRS) Matrix", "ModelMatrixTable", 4, 4, m16TRSmatrix); - - 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; - - float32_t3 matrixTranslation, matrixRotation, matrixScale; - IGimbalController::input_imguizmo_event_t decomposed, recomposed; - imguizmoModel.outDeltaTRS = IGimbalController::input_imguizmo_event_t(1); - - ImGuizmo::DecomposeMatrixToComponents(m16TRSmatrix, &matrixTranslation[0], &matrixRotation[0], &matrixScale[0]); - decomposed = *reinterpret_cast(m16TRSmatrix); - { - ImGuiInputTextFlags flags = 0; - - ImGui::InputFloat3("Tr", &matrixTranslation[0], "%.3f", flags); - ImGui::InputFloat3("Rt", &matrixRotation[0], "%.3f", flags); - ImGui::InputFloat3("Sc", &matrixScale[0], "%.3f", flags); - } - ImGuizmo::RecomposeMatrixFromComponents(&matrixTranslation[0], &matrixRotation[0], &matrixScale[0], m16TRSmatrix); - recomposed = *reinterpret_cast(m16TRSmatrix); - - 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; - } - - ImGui::Checkbox(" ", &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; - } - - // generate virtual events given delta TRS matrix - if (boundCameraToManipulate) - { - const float pmSpeed = boundCameraToManipulate->getMoveSpeedScale(); - const float prSpeed = boundCameraToManipulate->getRotationSpeedScale(); - - boundCameraToManipulate->setMoveSpeedScale(1); - boundCameraToManipulate->setRotationSpeedScale(1); - - auto referenceFrame = getCastedMatrix(imguizmoModel.outTRS); - boundCameraToManipulate->manipulate({}, &referenceFrame); - - boundCameraToManipulate->setMoveSpeedScale(pmSpeed); - boundCameraToManipulate->setRotationSpeedScale(prSpeed); - - /* - { - static std::vector virtualEvents(0x45); - - if (not enableActiveCameraMovement) - { - uint32_t vCount = {}; - - boundCameraToManipulate->beginInputProcessing(m_nextPresentationTimestamp); - { - boundCameraToManipulate->process(nullptr, vCount); - - if (virtualEvents.size() < vCount) - virtualEvents.resize(vCount); - - IGimbalController::SUpdateParameters params; - params.imguizmoEvents = { { imguizmoModel.outDeltaTRS } }; - boundCameraToManipulate->process(virtualEvents.data(), vCount, params); - } - boundCameraToManipulate->endInputProcessing(); - - // I start to think controller should be able to set sensitivity to scale magnitudes of generated events - // in order for camera to not keep any magnitude scalars like move or rotation speed scales - - if (vCount) - { - const float pmSpeed = boundCameraToManipulate->getMoveSpeedScale(); - const float prSpeed = boundCameraToManipulate->getRotationSpeedScale(); - - boundCameraToManipulate->setMoveSpeedScale(1); - boundCameraToManipulate->setRotationSpeedScale(1); - - auto referenceFrame = getCastedMatrix(imguizmoModel.outTRS); - boundCameraToManipulate->manipulate({ virtualEvents.data(), vCount }, &referenceFrame); - - boundCameraToManipulate->setMoveSpeedScale(pmSpeed); - boundCameraToManipulate->setRotationSpeedScale(prSpeed); - } - } - } - */ - } - else - { - // for scene demo model full affine transformation without limits is assumed - m_model = float32_t3x4(hlsl::transpose(imguizmoModel.outTRS)); - } - } - - inline void addMatrixTable(const char* topText, const char* tableName, int rows, int columns, const float* pointer, bool withSeparator = true) - { - 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(); - } - - std::chrono::seconds timeout = std::chrono::seconds(0x7fffFFFFu); - clock_t::time_point start; - - //! One window & 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 = 3u; - // 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 = {}; - // We will use it to get some asset stuff like geometry creator - smart_refctd_ptr m_assetManager; - // Input system for capturing system events - core::smart_refctd_ptr m_inputSystem; - // Handles mouse events - InputSystem::ChannelReader mouse; - // Handles keyboard events - InputSystem::ChannelReader keyboard; - //! 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; - }; - - // one model object in the world, testing multiuple cameraz for which view is rendered to separate frame buffers (so what they see) with new controller API including imguizmo - nbl::hlsl::float32_t3x4 m_model = nbl::hlsl::float32_t3x4(1.f); - - // if we had working IObjectTransform or something similar then it would be it instead, it is "last manipulated object" I need for TRS editor - // in reality we should store range of those IObjectTransforem interface range & index to object representing last manipulated one - nbl::core::smart_refctd_ptr boundCameraToManipulate = nullptr; - std::optional boundPlanarCameraIxToManipulate = std::nullopt; - - std::vector> m_planarProjections; - - bool enableActiveCameraMovement = false; - bool captureCursorInMoveMode = false; - - bool resetCursorToCenter = true; - - struct windowControlBinding - { - 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 viewProjMatrix = float32_t4x4(1.f); - - uint32_t activePlanarIx = 0u; - bool allowGizmoAxesToFlip = false; - bool enableDebugGridDraw = true; - float aspectRatio = 16.f / 9.f; - bool leftHandedProjection = true; - - std::optional boundProjectionIx = std::nullopt, lastBoundPerspectivePresetProjectionIx = std::nullopt, lastBoundOrthoPresetProjectionIx = std::nullopt; - - 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(); - } - }; - - struct ScriptedInputEvent - { - enum class Type : uint8_t - { - Keyboard, - Mouse, - Imguizmo, - Action - }; - - struct KeyboardData - { - ui::E_KEY_CODE key = ui::EKC_NONE; - ui::SKeyboardEvent::E_KEY_ACTION action = ui::SKeyboardEvent::ECA_UNITIALIZED; - }; - - struct MouseData - { - ui::SMouseEvent::E_EVENT_TYPE type = ui::SMouseEvent::EET_UNITIALIZED; - ui::E_MOUSE_BUTTON button = ui::EMB_LEFT_BUTTON; - ui::SMouseEvent::SClickEvent::E_ACTION action = ui::SMouseEvent::SClickEvent::EA_UNITIALIZED; - int16_t x = 0; - int16_t y = 0; - int16_t dx = 0; - int16_t dy = 0; - int16_t v = 0; - int16_t h = 0; - }; - - struct ActionData - { - enum class Kind : uint8_t - { - SetActiveRenderWindow, - SetActivePlanar, - SetProjectionType, - SetProjectionIndex, - SetUseWindow, - SetLeftHanded - }; - - Kind kind = Kind::SetActiveRenderWindow; - int32_t value = 0; - }; - - uint64_t frame = 0; - Type type = Type::Keyboard; - KeyboardData keyboard; - MouseData mouse; - float32_t4x4 imguizmo = float32_t4x4(1.f); - ActionData action; - }; - - struct ScriptedInputCheck - { - enum class Kind : uint8_t - { - Baseline, - ImguizmoVirtual, - GimbalNear, - GimbalDelta, - GimbalStep - }; - - struct ExpectedVirtualEvent - { - CVirtualGimbalEvent::VirtualEventType type = CVirtualGimbalEvent::None; - float64_t magnitude = 0.0; - }; - - uint64_t frame = 0; - Kind kind = Kind::Baseline; - float tolerance = 1e-3f; - std::vector expectedVirtualEvents; - - float32_t3 expectedPos = float32_t3(0.f); - float32_t3 expectedEulerDeg = float32_t3(0.f); - bool hasExpectedPos = false; - bool hasExpectedEuler = false; - float posTolerance = 0.05f; - float eulerToleranceDeg = 1.0f; - float minPosDelta = 0.0f; - float minEulerDeltaDeg = 0.0f; - bool hasPosDeltaConstraint = false; - bool hasEulerDeltaConstraint = false; - }; - - struct ScriptedInputState - { - bool enabled = false; - bool log = false; - bool exclusive = false; - bool hardFail = false; - bool visualDebug = false; - float visualTargetFps = 0.f; - float visualCameraHoldSeconds = 0.f; - std::vector events; - size_t nextEventIndex = 0; - std::vector checks; - size_t nextCheckIndex = 0; - std::vector captureFrames; - size_t nextCaptureIndex = 0; - std::string capturePrefix = "script"; - system::path captureOutputDir; - bool failed = false; - bool summaryReported = false; - bool baselineValid = false; - float32_t3 baselinePos = float32_t3(0.f); - float32_t3 baselineEulerDeg = float32_t3(0.f); - bool stepValid = false; - float32_t3 stepPos = float32_t3(0.f); - float32_t3 stepEulerDeg = float32_t3(0.f); - bool visualActivePlanarValid = false; - uint32_t visualActivePlanarIx = 0u; - uint64_t visualActivePlanarStartFrame = 0u; - bool framePacerInitialized = false; - std::chrono::steady_clock::time_point framePacerNext = {}; - }; - - static constexpr inline auto MaxSceneFBOs = 2u; - std::array windowBindings; - uint32_t activeRenderWindowIx = 0u; - - // UI font atlas + viewport FBO color attachment textures - constexpr static inline auto TotalUISampleTexturesAmount = 1u + MaxSceneFBOs; - - nbl::core::smart_refctd_ptr m_scene; - nbl::core::smart_refctd_ptr m_sceneRenderpass; - nbl::core::smart_refctd_ptr m_renderer; - - CRenderUI m_ui; - video::CDumbPresentationOracle oracle; - uint16_t gcIndex = {}; - - static constexpr uint32_t CiFramesBeforeCapture = 10u; - static constexpr auto CiMaxRuntime = std::chrono::minutes(2); - bool m_ciMode = false; - bool m_ciScreenshotDone = false; - uint32_t m_ciFrameCounter = 0u; - system::path m_ciScreenshotPath; - clock_t::time_point m_ciStartedAt = clock_t::time_point::min(); - bool m_scriptVisualDebugCli = false; - ScriptedInputState m_scriptedInput; - CameraControlSettings m_cameraControls; - CameraConstraintSettings m_cameraConstraints; - core::smart_refctd_ptr m_logFormatter; - std::deque m_virtualEventLog; - size_t m_virtualEventLogMax = 128u; - bool m_showHud = true; - bool m_showEventLog = false; - bool m_logAutoScroll = true; - bool m_logWrap = true; - std::vector m_presets; - std::vector m_keyframes; - CameraPlaybackState m_playback; - CTargetPoseController m_targetPoseController; - bool m_playbackAffectsAll = false; - float m_newKeyframeTime = 0.f; - char m_presetName[64] = "Preset"; - char m_presetPath[260] = "camera_presets.json"; - std::chrono::microseconds m_lastPresentationTimestamp = {}; - bool m_haveLastPresentationTimestamp = false; - double m_frameDeltaSec = 0.0; - static constexpr size_t UiMetricSamples = 96u; - std::array m_uiFrameMs = {}; - std::array m_uiInputCounts = {}; - std::array m_uiVirtualCounts = {}; - uint32_t m_uiMetricIndex = 0u; - uint32_t m_uiVirtualEventsThisFrame = 0u; - uint32_t m_uiInputEventsThisFrame = 0u; - uint32_t m_uiLastInputEvents = 0u; - uint32_t m_uiLastVirtualEvents = 0u; - float m_uiLastFrameMs = 0.0f; - - const bool flipGizmoY = true; - - float camYAngle = 165.f / 180.f * 3.14159f; - float camXAngle = 32.f / 180.f * 3.14159f; - float camDistance = 8.f; - bool useWindow = true, useSnap = false; - ImGuizmo::OPERATION mCurrentGizmoOperation = ImGuizmo::TRANSLATE; - ImGuizmo::MODE mCurrentGizmoMode = ImGuizmo::LOCAL; - float snap[3] = { 1.f, 1.f, 1.f }; - - bool firstFrame = true; - const float32_t2 iPaddingOffset = float32_t2(10, 10); - - struct ImWindowInit - { - float32_t2 iPos, iSize; - }; - - struct - { - ImWindowInit trsEditor; - ImWindowInit planars; - std::array renderWindows; - } wInit; -}; - -NBL_MAIN_FUNC(UISampleApp) diff --git a/61_UI/src/keysmapping.cpp b/61_UI/src/keysmapping.cpp new file mode 100644 index 000000000..1cd112c88 --- /dev/null +++ b/61_UI/src/keysmapping.cpp @@ -0,0 +1,232 @@ +#include "keysmapping.hpp" +#include +#include + +bool handleAddMapping(const char* tableID, IGimbalManipulateEncoder* encoder, IGimbalManipulateEncoder::EncoderType activeController, 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, 0.33f); + ImGui::TableSetupColumn("Key", ImGuiTableColumnFlags_WidthStretch, 0.33f); + ImGui::TableSetupColumn("Actions", ImGuiTableColumnFlags_WidthStretch, 0.33f); + 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 (activeController == IGimbalManipulateEncoder::Keyboard) + { + char newKeyDisplay[2] = { ui::keyCodeToChar(newKey, true), '\0' }; + if (ImGui::BeginCombo("##selectKey", newKeyDisplay)) + { + for (int i = ui::E_KEY_CODE::EKC_A; i <= ui::E_KEY_CODE::EKC_Z; ++i) + { + bool isSelected = (newKey == static_cast(i)); + char label[2] = { ui::keyCodeToChar(static_cast(i), true), '\0' }; + if (ImGui::Selectable(label, 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", ImVec2(100, 30))) + { + anyMapUpdated |= true; + if (activeController == IGimbalManipulateEncoder::Keyboard) + encoder->updateKeyboardMapping([&](auto& keys) { keys[newKey] = selectedEventType; }); + else + encoder->updateMouseMapping([&](auto& mouse) { mouse[newMouseCode] = selectedEventType; }); + addMode = false; + } + + ImGui::EndTable(); + + return anyMapUpdated; +} + +bool displayKeyMappingsAndVirtualStatesInline(IGimbalManipulateEncoder* encoder, bool spawnWindow) +{ + bool anyMapUpdated = false; + + if (!encoder) 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; + IGimbalManipulateEncoder::EncoderType activeController = IGimbalManipulateEncoder::Keyboard; + }; + + static std::unordered_map cameraStates; + auto& state = cameraStates[encoder]; + + const auto& keyboardMappings = encoder->getKeyboardVirtualEventMap(); + const auto& mouseMappings = encoder->getMouseVirtualEventMap(); + + if (spawnWindow) + { + ImGui::SetNextWindowSize(ImVec2(600, 400), ImGuiCond_FirstUseEver); + ImGui::Begin("Controller Mappings & Virtual States", nullptr, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_AlwaysVerticalScrollbar); + } + + if (ImGui::BeginTabBar("ControllersTabBar")) + { + if (ImGui::BeginTabItem("Keyboard")) + { + state.activeController = IGimbalManipulateEncoder::Keyboard; + ImGui::Separator(); + + if (ImGui::Button("Add Key", ImVec2(100, 30))) + 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); + std::string keyString(1, ui::keyCodeToChar(keyboardCode, true)); + ImGui::AlignTextToFramePadding(); + ImGui::TextWrapped("%s", keyString.c_str()); + + ImGui::TableSetColumnIndex(2); + bool isActive = (hash.event.magnitude > 0); + ImVec4 statusColor = isActive ? ImVec4(0.0f, 1.0f, 0.0f, 1.0f) : ImVec4(1.0f, 0.0f, 0.0f, 1.0f); + 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; + encoder->updateKeyboardMapping([keyboardCode](auto& keys) { keys.erase(keyboardCode); }); + break; + } + } + ImGui::EndTable(); + + if (state.addMode) + { + ImGui::Separator(); + anyMapUpdated |= handleAddMapping("AddKeyboardMappingTable", encoder, state.activeController, state.selectedEventType, state.newKey, state.newMouseCode, state.addMode); + } + + ImGui::EndTabItem(); + } + + if (ImGui::BeginTabItem("Mouse")) + { + state.activeController = IGimbalManipulateEncoder::Mouse; + ImGui::Separator(); + + if (ImGui::Button("Add Key", ImVec2(100, 30))) + 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); + ImVec4 statusColor = isActive ? ImVec4(0.0f, 1.0f, 0.0f, 1.0f) : ImVec4(1.0f, 0.0f, 0.0f, 1.0f); + 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; + encoder->updateMouseMapping([mouseCode](auto& mouse) { mouse.erase(mouseCode); }); + break; + } + } + ImGui::EndTable(); + + if (state.addMode) + { + ImGui::Separator(); + handleAddMapping("AddMouseMappingTable", encoder, state.activeController, 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 index e69de29bb..d1778ec90 100644 --- a/61_UI/src/transform.cpp +++ b/61_UI/src/transform.cpp @@ -0,0 +1,144 @@ +#include "transform.hpp" + +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, nullptr, useSnap ? &snap[0] : nullptr, boundSizing ? bounds : nullptr, boundSizingSnap ? boundsSnap : nullptr); + + if(params.enableViewManipulate) + ImGuizmo::ViewManipulate(cameraView, params.camDistance, ImVec2(viewManipulateRight - 128, viewManipulateTop), ImVec2(128, 128), 0x10101010); + + ImGui::End(); + ImGui::PopStyleColor(); + + return retval; +} + diff --git a/common/include/camera/CArcballCamera.hpp b/common/include/camera/CArcballCamera.hpp index f68add426..b8e21dcf4 100644 --- a/common/include/camera/CArcballCamera.hpp +++ b/common/include/camera/CArcballCamera.hpp @@ -69,7 +69,7 @@ class CArcballCamera final : public CSphericalTargetCamera private: static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate | CVirtualGimbalEvent::Rotate; - static inline constexpr double MaxPitch = glm::radians(89.0); + static inline constexpr double MaxPitch = 1.5533430342749532; static inline constexpr double MinPitch = -MaxPitch; static inline const auto m_keyboard_to_virtual_events_preset = []() diff --git a/common/include/camera/CChaseCamera.hpp b/common/include/camera/CChaseCamera.hpp index 71129b5a7..6b77be94d 100644 --- a/common/include/camera/CChaseCamera.hpp +++ b/common/include/camera/CChaseCamera.hpp @@ -72,8 +72,8 @@ class CChaseCamera final : public CSphericalTargetCamera private: static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate | CVirtualGimbalEvent::Rotate; - static inline constexpr double MaxPitch = glm::radians(70.0); - static inline constexpr double MinPitch = glm::radians(-60.0); + static inline constexpr double MaxPitch = 1.2217304763960306; + static inline constexpr double MinPitch = -1.0471975511965976; static inline const auto m_keyboard_to_virtual_events_preset = []() { diff --git a/common/include/camera/CDollyCamera.hpp b/common/include/camera/CDollyCamera.hpp index 666e4aa71..e74b56214 100644 --- a/common/include/camera/CDollyCamera.hpp +++ b/common/include/camera/CDollyCamera.hpp @@ -56,8 +56,8 @@ class CDollyCamera final : public CSphericalTargetCamera private: static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate | CVirtualGimbalEvent::Rotate; - static inline constexpr double MaxPitch = glm::radians(85.0); - static inline constexpr double MinPitch = glm::radians(-85.0); + static inline constexpr double MaxPitch = 1.4835298641951802; + static inline constexpr double MinPitch = -1.4835298641951802; static inline const auto m_keyboard_to_virtual_events_preset = []() { diff --git a/common/include/camera/CDollyZoomCamera.hpp b/common/include/camera/CDollyZoomCamera.hpp index 1fe36594a..d17ba27b9 100644 --- a/common/include/camera/CDollyZoomCamera.hpp +++ b/common/include/camera/CDollyZoomCamera.hpp @@ -35,10 +35,10 @@ class CDollyZoomCamera final : public CSphericalTargetCamera float computeDollyFov() const { - const double base = std::tan(glm::radians(static_cast(m_baseFov)) * 0.5); + const double base = std::tan(hlsl::radians(static_cast(m_baseFov)) * 0.5); const double ratio = static_cast(m_referenceDistance) / std::max(static_cast(m_distance), static_cast(MinDistance)); const double fov = 2.0 * std::atan(base * ratio); - const double fovDeg = glm::degrees(fov); + const double fovDeg = hlsl::degrees(fov); return static_cast(std::clamp(fovDeg, 10.0, 150.0)); } diff --git a/common/include/camera/CFPSCamera.hpp b/common/include/camera/CFPSCamera.hpp index dc2908e72..bc9f0cf21 100644 --- a/common/include/camera/CFPSCamera.hpp +++ b/common/include/camera/CFPSCamera.hpp @@ -5,6 +5,8 @@ #ifndef _C_FPS_CAMERA_HPP_ #define _C_FPS_CAMERA_HPP_ +#include + #include "ICamera.hpp" namespace nbl::hlsl // TODO: DIFFERENT NAMESPACE @@ -15,6 +17,8 @@ class CFPSCamera final : public ICamera { public: using base_t = ICamera; + static inline constexpr float HalfPi = 1.57079632679489661923f; + static inline constexpr float RadToDeg = 57.2957795130823208768f; CFPSCamera(const float64_t3& position, const glm::quat& orientation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f)) : base_t(), m_gimbal({ .position = position, .orientation = orientation }) @@ -25,8 +29,8 @@ class CFPSCamera final : public ICamera const float gForwardX = static_cast(gForward.x); const float gForwardY = static_cast(gForward.y); const float gForwardZ = static_cast(gForward.z); - const float gPitch = glm::atan(glm::length(glm::vec2(gForwardX, gForwardZ)), gForwardY) - glm::half_pi(); - const float gYaw = glm::atan(gForwardX, gForwardZ); + const float gPitch = std::atan2(std::hypot(gForwardX, gForwardZ), gForwardY) - HalfPi; + const float gYaw = std::atan2(gForwardX, gForwardZ); auto test = glm::quat(glm::vec3(gPitch, gYaw, 0.0f)); @@ -70,11 +74,11 @@ class CFPSCamera final : public ICamera const float z = static_cast(q.z); const float sinr_cosp = 2.f * (w * z + x * y); const float cosr_cosp = 1.f - 2.f * (y * y + z * z); - const float roll = glm::degrees(glm::atan(sinr_cosp, cosr_cosp)); - const float absRoll = glm::abs(roll); + const float roll = RadToDeg * std::atan2(sinr_cosp, cosr_cosp); + const float absRoll = std::abs(roll); constexpr float epsilon = 1.e-4f; - if (not (glm::epsilonEqual(absRoll, 0.f, epsilon) || glm::epsilonEqual(absRoll, 180.f, epsilon))) + if (not ((absRoll <= epsilon) || (std::abs(absRoll - 180.f) <= epsilon))) return false; } @@ -87,9 +91,11 @@ class CFPSCamera final : public ICamera m_gimbal.begin(); { - const auto rForward = glm::vec3(reference.frame[2]); - const float rPitch = glm::atan(glm::length(glm::vec2(rForward.x, rForward.z)), rForward.y) - glm::half_pi(); - const float gYaw = glm::atan(rForward.x, rForward.z); + const float rForwardX = static_cast(reference.frame[2].x); + const float rForwardY = static_cast(reference.frame[2].y); + const float rForwardZ = static_cast(reference.frame[2].z); + const float rPitch = std::atan2(std::hypot(rForwardX, rForwardZ), rForwardY) - HalfPi; + const float gYaw = std::atan2(rForwardX, rForwardZ); const float newPitch = std::clamp(rPitch + impulse.dVirtualRotation.x * m_rotationSpeedScale, MinVerticalAngle, MaxVerticalAngle), newYaw = gYaw + impulse.dVirtualRotation.y * m_rotationSpeedScale; if(validateReference()) m_gimbal.setOrientation(glm::quat(glm::vec3(newPitch, newYaw, 0.0f))); @@ -120,7 +126,7 @@ class CFPSCamera final : public ICamera typename base_t::CGimbal m_gimbal; static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate | CVirtualGimbalEvent::Rotate; - static inline constexpr float MaxVerticalAngle = glm::radians(88.0f), MinVerticalAngle = -MaxVerticalAngle; + static inline constexpr float MaxVerticalAngle = 1.53588974175501f, MinVerticalAngle = -MaxVerticalAngle; static inline const auto m_keyboard_to_virtual_events_preset = []() { diff --git a/common/include/camera/CTargetPoseController.hpp b/common/include/camera/CTargetPoseController.hpp index 5ae44ed07..fdd566ed8 100644 --- a/common/include/camera/CTargetPoseController.hpp +++ b/common/include/camera/CTargetPoseController.hpp @@ -172,9 +172,9 @@ class CTargetPoseController const auto deltaWorld = target.position - currentPos; const float64_t3 localDelta( - glm::dot(deltaWorld, right), - glm::dot(deltaWorld, up), - glm::dot(deltaWorld, forward)); + hlsl::dot(deltaWorld, right), + hlsl::dot(deltaWorld, up), + hlsl::dot(deltaWorld, forward)); appendSignedEvent(out, localDelta.x, CVirtualGimbalEvent::MoveRight, CVirtualGimbalEvent::MoveLeft); appendSignedEvent(out, localDelta.y, CVirtualGimbalEvent::MoveUp, CVirtualGimbalEvent::MoveDown); diff --git a/common/include/camera/CTurntableCamera.hpp b/common/include/camera/CTurntableCamera.hpp index 3cd0f750c..97eb6bc2f 100644 --- a/common/include/camera/CTurntableCamera.hpp +++ b/common/include/camera/CTurntableCamera.hpp @@ -61,7 +61,7 @@ class CTurntableCamera final : public CSphericalTargetCamera private: static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate | CVirtualGimbalEvent::Rotate; - static inline constexpr double MaxPitch = glm::radians(89.0); + static inline constexpr double MaxPitch = 1.5533430342749532; static inline constexpr double MinPitch = -MaxPitch; static inline const auto m_keyboard_to_virtual_events_preset = []() diff --git a/common/include/camera/ICamera.hpp b/common/include/camera/ICamera.hpp index 6560aa01f..046661b4d 100644 --- a/common/include/camera/ICamera.hpp +++ b/common/include/camera/ICamera.hpp @@ -32,9 +32,9 @@ class ICamera : public IGimbalController, virtual public core::IReferenceCounted const auto& position = base_t::getPosition(); - m_viewMatrix[0u] = float64_t4(gRight, -glm::dot(gRight, position)); - m_viewMatrix[1u] = float64_t4(gUp, -glm::dot(gUp, position)); - m_viewMatrix[2u] = float64_t4(gForward, -glm::dot(gForward, position)); + m_viewMatrix[0u] = float64_t4(gRight, -hlsl::dot(gRight, position)); + m_viewMatrix[1u] = float64_t4(gUp, -hlsl::dot(gUp, position)); + m_viewMatrix[2u] = float64_t4(gForward, -hlsl::dot(gForward, position)); } inline const float64_t3x4& getViewMatrix() const { return m_viewMatrix; } diff --git a/common/include/camera/IGimbalController.hpp b/common/include/camera/IGimbalController.hpp index 536fdd6f7..3e143a37a 100644 --- a/common/include/camera/IGimbalController.hpp +++ b/common/include/camera/IGimbalController.hpp @@ -386,9 +386,9 @@ class IGimbalController : public IGimbalManipulateEncoder // Delta rotation impulse const float32_t3 dRotationRad = { - glm::radians(world.dRotation[0]), - glm::radians(world.dRotation[1]), - glm::radians(world.dRotation[2]) + hlsl::radians(world.dRotation[0]), + hlsl::radians(world.dRotation[1]), + hlsl::radians(world.dRotation[2]) }; requestMagnitudeUpdateWithScalar(0.f, dRotationRad[0], std::abs(dRotationRad[0]), gimbal_event_t::TiltUp , gimbal_event_t::TiltDown, m_imguizmoVirtualEventMap); requestMagnitudeUpdateWithScalar(0.f, dRotationRad[1], std::abs(dRotationRad[1]), gimbal_event_t::PanRight, gimbal_event_t::PanLeft, m_imguizmoVirtualEventMap); diff --git a/common/include/camera/IPlanarProjection.hpp b/common/include/camera/IPlanarProjection.hpp index 1fccf0ef4..eac4e6b76 100644 --- a/common/include/camera/IPlanarProjection.hpp +++ b/common/include/camera/IPlanarProjection.hpp @@ -69,9 +69,9 @@ class IPlanarProjection : public ILinearProjection const auto& fov = m_parameters.m_planar.perspective.fov; if (leftHanded) - base_t::setProjectionMatrix(buildProjectionMatrixPerspectiveFovLH(glm::radians(fov), aspectRatio, m_parameters.m_zNear, m_parameters.m_zFar)); + base_t::setProjectionMatrix(buildProjectionMatrixPerspectiveFovLH(hlsl::radians(fov), aspectRatio, m_parameters.m_zNear, m_parameters.m_zFar)); else - base_t::setProjectionMatrix(buildProjectionMatrixPerspectiveFovRH(glm::radians(fov), aspectRatio, m_parameters.m_zNear, m_parameters.m_zFar)); + base_t::setProjectionMatrix(buildProjectionMatrixPerspectiveFovRH(hlsl::radians(fov), aspectRatio, m_parameters.m_zNear, m_parameters.m_zFar)); } break; case Orthographic: @@ -117,4 +117,4 @@ class IPlanarProjection : public ILinearProjection } // nbl::hlsl namespace -#endif // _NBL_I_PLANAR_PROJECTION_HPP_ \ No newline at end of file +#endif // _NBL_I_PLANAR_PROJECTION_HPP_ From 8df8db2f89b78a2c89287faa8f026176b2465f88 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 14 Feb 2026 20:28:56 +0100 Subject: [PATCH 099/205] Render debug grid with scene depth --- 61_UI/include/app/App.hpp | 3 +- 61_UI/include/app/impl/AppImGuiListen.inl | 11 ----- 61_UI/include/app/impl/AppInit.inl | 2 + 61_UI/include/app/impl/AppWorkLoop.inl | 53 +++++++++++++++++------ 4 files changed, 43 insertions(+), 26 deletions(-) diff --git a/61_UI/include/app/App.hpp b/61_UI/include/app/App.hpp index f6d43f6ab..663065857 100644 --- a/61_UI/include/app/App.hpp +++ b/61_UI/include/app/App.hpp @@ -1849,7 +1849,8 @@ class App final : public examples::SimpleWindowedApplication CRenderUI m_ui; video::CDumbPresentationOracle oracle; - uint16_t gcIndex = {}; + uint16_t gcIndex = {}; + uint16_t m_gridGeometryIx = std::numeric_limits::max(); static constexpr uint32_t CiFramesBeforeCapture = 10u; static constexpr auto CiMaxRuntime = std::chrono::minutes(2); diff --git a/61_UI/include/app/impl/AppImGuiListen.inl b/61_UI/include/app/impl/AppImGuiListen.inl index 161040f3b..7bb8897f8 100644 --- a/61_UI/include/app/impl/AppImGuiListen.inl +++ b/61_UI/include/app/impl/AppImGuiListen.inl @@ -146,17 +146,6 @@ if (flipGizmoY) // note we allow to flip gizmo just to match our coordinates imguizmoPlanar.projection[1][1] *= -1.f; // https://johannesugb.github.io/gpu-programming/why-do-opengl-proj-matrices-fail-in-vulkan/ - static constexpr float identityMatrix[] = - { - 1.f, 0.f, 0.f, 0.f, - 0.f, 1.f, 0.f, 0.f, - 0.f, 0.f, 1.f, 0.f, - 0.f, 0.f, 0.f, 1.f - }; - - if(!hideSceneGizmos && binding.enableDebugGridDraw) - ImGuizmo::DrawGrid(&imguizmoPlanar.view[0][0], &imguizmoPlanar.projection[0][0], identityMatrix, 100.f); - if (!hideSceneGizmos) { for (uint32_t modelIx = 0; modelIx < 1u + m_planarProjections.size(); modelIx++) diff --git a/61_UI/include/app/impl/AppInit.inl b/61_UI/include/app/impl/AppInit.inl index f0a740303..b0cd5b73e 100644 --- a/61_UI/include/app/impl/AppInit.inl +++ b/61_UI/include/app/impl/AppInit.inl @@ -1392,6 +1392,8 @@ { if (name == "Cone") m_renderer->getGeometry(ix).pipeline = pipelines[CSimpleDebugRenderer::SInitParams::PipelineType::Cone]; + else if (name == "Grid") + m_gridGeometryIx = static_cast(ix); ix++; } } diff --git a/61_UI/include/app/impl/AppWorkLoop.inl b/61_UI/include/app/impl/AppWorkLoop.inl index 4976bc589..9285f348a 100644 --- a/61_UI/include/app/impl/AppWorkLoop.inl +++ b/61_UI/include/app/impl/AppWorkLoop.inl @@ -42,7 +42,45 @@ auto renderScene = [&](windowControlBinding& binding) { - if (!binding.sceneFramebuffer) + if (!binding.sceneFramebuffer || !m_renderer) + return; + + const auto& geometries = m_renderer->getGeometries(); + const auto geomCount = geometries.size(); + if (!geomCount) + return; + + if (gcIndex >= geomCount) + gcIndex = 0u; + + const bool hasGridGeometry = m_gridGeometryIx < geomCount; + const bool drawDepthGrid = binding.enableDebugGridDraw && hasGridGeometry; + if (drawDepthGrid) + { + if (m_renderer->m_instances.size() < 2u) + m_renderer->m_instances.resize(2u); + + auto& gridInstance = m_renderer->m_instances[0]; + gridInstance.world = nbl::hlsl::float32_t3x4(1.f); + gridInstance.packedGeo = geometries.data() + m_gridGeometryIx; + + auto& objectInstance = m_renderer->m_instances[1]; + objectInstance.world = m_model; + objectInstance.packedGeo = geometries.data() + gcIndex; + } + else + { + if (m_renderer->m_instances.empty()) + m_renderer->m_instances.resize(1u); + if (m_renderer->m_instances.size() > 1u) + m_renderer->m_instances.resize(1u); + + auto& objectInstance = m_renderer->m_instances[0]; + objectInstance.world = m_model; + objectInstance.packedGeo = geometries.data() + gcIndex; + } + + if (m_renderer->m_instances.empty()) return; const auto& fbParams = binding.sceneFramebuffer->getCreationParameters(); @@ -74,19 +112,6 @@ willSubmit &= cmdbuf->endRenderPass(); }; - if (m_renderer && !m_renderer->m_instances.empty()) - { - auto& instance = m_renderer->m_instances[0]; - instance.world = m_model; - const auto geomCount = m_renderer->getGeometries().size(); - if (geomCount) - { - if (gcIndex >= geomCount) - gcIndex = 0; - instance.packedGeo = m_renderer->getGeometries().data() + gcIndex; - } - } - if (useWindow) for (auto& binding : windowBindings) renderScene(binding); From 6148a041833976ebad2c742b6e29de2751e46d55 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Wed, 18 Feb 2026 13:19:34 +0100 Subject: [PATCH 100/205] Move 61 UI app implementation from inl to cpp --- ...ppControlPanel.inl => AppControlPanel.cpp} | 8 ++ .../AppImGuiListen.inl => AppImGuiListen.cpp} | 11 ++- .../app/impl/AppInit.inl => AppInit.cpp} | 14 +++- ...sformEditor.inl => AppTransformEditor.cpp} | 8 ++ .../app/impl/AppUpdate.inl => AppUpdate.cpp} | 28 ++++++- .../impl/AppWorkLoop.inl => AppWorkLoop.cpp} | 78 +++++++++---------- 61_UI/CMakeLists.txt | 6 ++ 61_UI/include/app/App.hpp | 58 ++++---------- .../examples/shaders/geometry/unified.hlsl | 32 ++++++-- 9 files changed, 148 insertions(+), 95 deletions(-) rename 61_UI/{include/app/impl/AppControlPanel.inl => AppControlPanel.cpp} (99%) rename 61_UI/{include/app/impl/AppImGuiListen.inl => AppImGuiListen.cpp} (98%) rename 61_UI/{include/app/impl/AppInit.inl => AppInit.cpp} (99%) rename 61_UI/{include/app/impl/AppTransformEditor.inl => AppTransformEditor.cpp} (99%) rename 61_UI/{include/app/impl/AppUpdate.inl => AppUpdate.cpp} (96%) rename 61_UI/{include/app/impl/AppWorkLoop.inl => AppWorkLoop.cpp} (90%) diff --git a/61_UI/include/app/impl/AppControlPanel.inl b/61_UI/AppControlPanel.cpp similarity index 99% rename from 61_UI/include/app/impl/AppControlPanel.inl rename to 61_UI/AppControlPanel.cpp index a9a311725..888cb6029 100644 --- a/61_UI/include/app/impl/AppControlPanel.inl +++ b/61_UI/AppControlPanel.cpp @@ -1,3 +1,7 @@ +#include "app/App.hpp" + +void App::DrawControlPanel() +{ const ImVec2 displaySize = ImGui::GetIO().DisplaySize; const float panelWidth = std::clamp(displaySize.x * 0.19f, 200.0f, displaySize.x * 0.25f); const float panelHeight = std::clamp(displaySize.y * 0.34f, 200.0f, displaySize.y * 0.50f); @@ -851,3 +855,7 @@ ImGui::End(); ImGui::PopStyleColor(19); ImGui::PopStyleVar(9); + +} + + diff --git a/61_UI/include/app/impl/AppImGuiListen.inl b/61_UI/AppImGuiListen.cpp similarity index 98% rename from 61_UI/include/app/impl/AppImGuiListen.inl rename to 61_UI/AppImGuiListen.cpp index 7bb8897f8..e61ab0292 100644 --- a/61_UI/include/app/impl/AppImGuiListen.inl +++ b/61_UI/AppImGuiListen.cpp @@ -1,3 +1,7 @@ +#include "app/App.hpp" + +void App::imguiListen() +{ ImGuiIO& io = ImGui::GetIO(); if (m_ciMode) { @@ -457,7 +461,8 @@ projection.update(binding.leftHandedProjection, binding.aspectRatio); auto viewMatrix = getCastedMatrix(boundPlanarCamera->getGimbal().getViewMatrix()); - auto viewProjMatrix = mul(getCastedMatrix(projection.getProjectionMatrix()), getMatrix3x4As4x4(viewMatrix)); + auto projectionMatrix = getCastedMatrix(projection.getProjectionMatrix()); + auto viewProjMatrix = mul(projectionMatrix, getMatrix3x4As4x4(viewMatrix)); binding.viewMatrix = viewMatrix; binding.viewProjMatrix = viewProjMatrix; @@ -465,3 +470,7 @@ } + +} + + diff --git a/61_UI/include/app/impl/AppInit.inl b/61_UI/AppInit.cpp similarity index 99% rename from 61_UI/include/app/impl/AppInit.inl rename to 61_UI/AppInit.cpp index b0cd5b73e..e550d0600 100644 --- a/61_UI/include/app/impl/AppInit.inl +++ b/61_UI/AppInit.cpp @@ -1,3 +1,7 @@ +#include "app/App.hpp" + +bool App::onAppInitialized(smart_refctd_ptr&& system) +{ argparse::ArgumentParser program("Virtual camera event system demo"); program.add_argument("--file") @@ -152,6 +156,7 @@ m_scriptedInput.visualActivePlanarValid = false; m_scriptedInput.visualActivePlanarIx = 0u; m_scriptedInput.visualActivePlanarStartFrame = 0u; + m_scriptedInput.scriptedLeftMouseDown = false; m_scriptedInput.framePacerInitialized = false; m_scriptedInput.capturePrefix = "script"; m_scriptedInput.captureOutputDir = localOutputCWD; @@ -1387,17 +1392,18 @@ { const auto& pipelines = m_renderer->getInitParams().pipelines; + m_gridGeometryIx = std::nullopt; auto ix = 0u; for (const auto& name : m_scene->getInitParams().geometryNames) { if (name == "Cone") m_renderer->getGeometry(ix).pipeline = pipelines[CSimpleDebugRenderer::SInitParams::PipelineType::Cone]; else if (name == "Grid") - m_gridGeometryIx = static_cast(ix); + m_gridGeometryIx = ix; ix++; } } - m_renderer->m_instances.resize(1); + m_renderer->m_instances.resize(m_gridGeometryIx.has_value() ? 2u : 1u); const auto dpyInfo = m_winMgr->getPrimaryDisplayInfo(); for (uint32_t i = 0u; i < windowBindings.size(); ++i) @@ -1417,3 +1423,7 @@ timeout = std::chrono::seconds(std::atoi(argv[2].c_str())); start = clock_t::now(); return true; + +} + + diff --git a/61_UI/include/app/impl/AppTransformEditor.inl b/61_UI/AppTransformEditor.cpp similarity index 99% rename from 61_UI/include/app/impl/AppTransformEditor.inl rename to 61_UI/AppTransformEditor.cpp index 4bbb4a21f..09827bbb6 100644 --- a/61_UI/include/app/impl/AppTransformEditor.inl +++ b/61_UI/AppTransformEditor.cpp @@ -1,3 +1,7 @@ +#include "app/App.hpp" + +void App::TransformEditorContents() +{ 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; @@ -219,3 +223,7 @@ // for scene demo model full affine transformation without limits is assumed m_model = float32_t3x4(hlsl::transpose(imguizmoModel.outTRS)); } + +} + + diff --git a/61_UI/include/app/impl/AppUpdate.inl b/61_UI/AppUpdate.cpp similarity index 96% rename from 61_UI/include/app/impl/AppUpdate.inl rename to 61_UI/AppUpdate.cpp index b1f798b0e..08ab106e8 100644 --- a/61_UI/include/app/impl/AppUpdate.inl +++ b/61_UI/AppUpdate.cpp @@ -1,3 +1,7 @@ +#include "app/App.hpp" + +void App::update() +{ m_inputSystem->getDefaultMouse(&mouse); m_inputSystem->getDefaultKeyboard(&keyboard); @@ -217,6 +221,23 @@ } } + if (!m_scriptedInput.enabled) + { + m_scriptedInput.scriptedLeftMouseDown = false; + } + else + { + for (const auto& ev : scriptedMouse) + { + if (ev.type != ui::SMouseEvent::EET_CLICK || ev.clickEvent.mouseButton != ui::EMB_LEFT_BUTTON) + continue; + if (ev.clickEvent.action == ui::SMouseEvent::SClickEvent::EA_PRESSED) + m_scriptedInput.scriptedLeftMouseDown = true; + else if (ev.clickEvent.action == ui::SMouseEvent::SClickEvent::EA_RELEASED) + m_scriptedInput.scriptedLeftMouseDown = false; + } + } + if (!scriptedMouse.empty()) capturedEvents.mouse.insert(capturedEvents.mouse.end(), scriptedMouse.begin(), scriptedMouse.end()); if (!scriptedKeyboard.empty()) @@ -290,7 +311,8 @@ if (isOrbitLikeCamera(camera)) { - if (ImGui::IsMouseDown(ImGuiMouseButton_Left)) + const bool orbitMouseDown = ImGui::IsMouseDown(ImGuiMouseButton_Left) || (m_scriptedInput.enabled && m_scriptedInput.scriptedLeftMouseDown); + if (orbitMouseDown) projection.processMouse(output, vMouseEventsCount, { cameraMouseEvents.data(), cameraMouseEvents.size() }); else vMouseEventsCount = 0; @@ -655,3 +677,7 @@ UpdateUiMetrics(); m_ui.manager->update(params); + +} + + diff --git a/61_UI/include/app/impl/AppWorkLoop.inl b/61_UI/AppWorkLoop.cpp similarity index 90% rename from 61_UI/include/app/impl/AppWorkLoop.inl rename to 61_UI/AppWorkLoop.cpp index 9285f348a..69f0b7364 100644 --- a/61_UI/include/app/impl/AppWorkLoop.inl +++ b/61_UI/AppWorkLoop.cpp @@ -1,3 +1,7 @@ +#include "app/App.hpp" + +void App::workLoopBody() +{ paceScriptedVisualDebugFrame(); // framesInFlight: ensuring safe execution of command buffers and acquires, `framesInFlight` only affect semaphore waits, don't use this to index your resources because it can change with swapchain recreation. @@ -42,45 +46,7 @@ auto renderScene = [&](windowControlBinding& binding) { - if (!binding.sceneFramebuffer || !m_renderer) - return; - - const auto& geometries = m_renderer->getGeometries(); - const auto geomCount = geometries.size(); - if (!geomCount) - return; - - if (gcIndex >= geomCount) - gcIndex = 0u; - - const bool hasGridGeometry = m_gridGeometryIx < geomCount; - const bool drawDepthGrid = binding.enableDebugGridDraw && hasGridGeometry; - if (drawDepthGrid) - { - if (m_renderer->m_instances.size() < 2u) - m_renderer->m_instances.resize(2u); - - auto& gridInstance = m_renderer->m_instances[0]; - gridInstance.world = nbl::hlsl::float32_t3x4(1.f); - gridInstance.packedGeo = geometries.data() + m_gridGeometryIx; - - auto& objectInstance = m_renderer->m_instances[1]; - objectInstance.world = m_model; - objectInstance.packedGeo = geometries.data() + gcIndex; - } - else - { - if (m_renderer->m_instances.empty()) - m_renderer->m_instances.resize(1u); - if (m_renderer->m_instances.size() > 1u) - m_renderer->m_instances.resize(1u); - - auto& objectInstance = m_renderer->m_instances[0]; - objectInstance.world = m_model; - objectInstance.packedGeo = geometries.data() + gcIndex; - } - - if (m_renderer->m_instances.empty()) + if (!binding.sceneFramebuffer) return; const auto& fbParams = binding.sceneFramebuffer->getCreationParameters(); @@ -112,6 +78,36 @@ willSubmit &= cmdbuf->endRenderPass(); }; + if (m_renderer && !m_renderer->m_instances.empty()) + { + auto& instance = m_renderer->m_instances[0]; + instance.world = m_model; + const auto geomCount = m_renderer->getGeometries().size(); + if (geomCount) + { + if (gcIndex >= geomCount) + gcIndex = 0; + instance.packedGeo = m_renderer->getGeometries().data() + gcIndex; + } + + if (m_gridGeometryIx.has_value() && m_renderer->m_instances.size() > 1u) + { + const auto gridIx = m_gridGeometryIx.value(); + if (gridIx < geomCount) + { + auto& gridInstance = m_renderer->m_instances[1]; + gridInstance.packedGeo = m_renderer->getGeometries().data() + gridIx; + + constexpr float gridExtent = 32.0f; + float32_t3x4 gridWorld = float32_t3x4(1.0f); + gridWorld[0] = float32_t4(gridExtent, 0.0f, 0.0f, -0.5f * gridExtent); + gridWorld[1] = float32_t4(0.0f, 1.0f, 0.0f, -0.5f); + gridWorld[2] = float32_t4(0.0f, 0.0f, gridExtent, -0.5f * gridExtent); + gridInstance.world = gridWorld; + } + } + } + if (useWindow) for (auto& binding : windowBindings) renderScene(binding); @@ -336,3 +332,7 @@ m_surface->present(std::move(swapchainLock), presentInfo); } firstFrame = false; + +} + + diff --git a/61_UI/CMakeLists.txt b/61_UI/CMakeLists.txt index ebf353c0a..68dbaedd8 100644 --- a/61_UI/CMakeLists.txt +++ b/61_UI/CMakeLists.txt @@ -1,5 +1,11 @@ if(NBL_BUILD_IMGUI) set(NBL_EXTRA_SOURCES + "${CMAKE_CURRENT_SOURCE_DIR}/AppControlPanel.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/AppImGuiListen.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/AppInit.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/AppTransformEditor.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/AppUpdate.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/AppWorkLoop.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/keysmapping.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/transform.cpp" ) diff --git a/61_UI/include/app/App.hpp b/61_UI/include/app/App.hpp index 663065857..dbd072acc 100644 --- a/61_UI/include/app/App.hpp +++ b/61_UI/include/app/App.hpp @@ -315,10 +315,7 @@ class App final : public examples::SimpleWindowedApplication return {}; } - inline bool onAppInitialized(smart_refctd_ptr&& system) override - { - #include "app/impl/AppInit.inl" - } + bool onAppInitialized(smart_refctd_ptr&& system) override; bool updateGUIDescriptorSet() { @@ -352,10 +349,7 @@ class App final : public examples::SimpleWindowedApplication return m_device->updateDescriptorSets(writes, {}); } - inline void workLoopBody() override - { - #include "app/impl/AppWorkLoop.inl" - } + void workLoopBody() override; inline void paceScriptedVisualDebugFrame() { @@ -421,10 +415,7 @@ class App final : public examples::SimpleWindowedApplication return base_t::onAppTerminated(); } - inline void update() - { - #include "app/impl/AppUpdate.inl" - } + void update(); private: struct CUILogFormatter final : public nbl::system::ILogger @@ -596,6 +587,8 @@ class App final : public examples::SimpleWindowedApplication { if (!(m_scriptedInput.enabled && m_scriptedInput.visualDebug)) return; + if (m_scriptedInput.exclusive) + return; if (windowBindings.size() < 2u || m_planarProjections.empty()) return; @@ -815,7 +808,7 @@ class App final : public examples::SimpleWindowedApplication return std::isfinite(outScreen.x) && std::isfinite(outScreen.y); }; - auto drawWorldLine = [&](const float32_t3& aWorld, const float32_t3& bWorld, ImU32 color, float thickness) -> void + const auto drawProjectedSegment = [&](const float32_t3& aWorld, const float32_t3& bWorld, ImU32 color, float thickness) -> void { float32_t4 viewA = mul(viewMatrix, float32_t4(aWorld.x, aWorld.y, aWorld.z, 1.0f)); float32_t4 viewB = mul(viewMatrix, float32_t4(bWorld.x, bWorld.y, bWorld.z, 1.0f)); @@ -839,25 +832,10 @@ class App final : public examples::SimpleWindowedApplication drawList->AddLine(screenA, screenB, color, thickness); }; - constexpr int gridHalfSteps = 8; - constexpr float gridStep = 2.0f; - const float gridHalfSize = static_cast(gridHalfSteps) * gridStep; - constexpr int gridMajorModulo = 1; - const ImU32 gridMajor = IM_COL32(136, 160, 194, 95); - - for (int i = -gridHalfSteps; i <= gridHalfSteps; ++i) + auto drawWorldLine = [&](const float32_t3& aWorld, const float32_t3& bWorld, ImU32 color, float thickness) -> void { - if (i == 0) - continue; - if ((i % gridMajorModulo) != 0) - continue; - const float c = static_cast(i) * gridStep; - drawWorldLine(float32_t3(c, 0.0f, -gridHalfSize), float32_t3(c, 0.0f, gridHalfSize), gridMajor, 1.3f); - drawWorldLine(float32_t3(-gridHalfSize, 0.0f, c), float32_t3(gridHalfSize, 0.0f, c), gridMajor, 1.3f); - } - - drawWorldLine(float32_t3(-gridHalfSize, 0.0f, 0.0f), float32_t3(gridHalfSize, 0.0f, 0.0f), IM_COL32(184, 204, 232, 170), 2.0f); - drawWorldLine(float32_t3(0.0f, 0.0f, -gridHalfSize), float32_t3(0.0f, 0.0f, gridHalfSize), IM_COL32(184, 204, 232, 170), 2.0f); + drawProjectedSegment(aWorld, bWorld, color, thickness); + }; constexpr float axisLength = 7.5f; const float32_t3 origin = float32_t3(0.0f); @@ -1397,10 +1375,7 @@ class App final : public examples::SimpleWindowedApplication return true; } - inline void imguiListen() - { - #include "app/impl/AppImGuiListen.inl" - } + void imguiListen(); inline bool shouldCaptureOSCursor() { @@ -1579,15 +1554,9 @@ class App final : public examples::SimpleWindowedApplication } - inline void DrawControlPanel() - { - #include "app/impl/AppControlPanel.inl" - } + void DrawControlPanel(); - inline void TransformEditorContents() - { - #include "app/impl/AppTransformEditor.inl" - } + void TransformEditorContents(); inline void addMatrixTable(const char* topText, const char* tableName, int rows, int columns, const float* pointer, bool withSeparator = true) { @@ -1832,6 +1801,7 @@ class App final : public examples::SimpleWindowedApplication bool visualActivePlanarValid = false; uint32_t visualActivePlanarIx = 0u; uint64_t visualActivePlanarStartFrame = 0u; + bool scriptedLeftMouseDown = false; bool framePacerInitialized = false; std::chrono::steady_clock::time_point framePacerNext = {}; }; @@ -1846,11 +1816,11 @@ class App final : public examples::SimpleWindowedApplication nbl::core::smart_refctd_ptr m_scene; nbl::core::smart_refctd_ptr m_sceneRenderpass; nbl::core::smart_refctd_ptr m_renderer; + std::optional m_gridGeometryIx = std::nullopt; CRenderUI m_ui; video::CDumbPresentationOracle oracle; uint16_t gcIndex = {}; - uint16_t m_gridGeometryIx = std::numeric_limits::max(); static constexpr uint32_t CiFramesBeforeCapture = 10u; static constexpr auto CiMaxRuntime = std::chrono::minutes(2); 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")] From a344c2f0e1950ae867f86b9e3e52d78a2dc0e7b0 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Wed, 18 Feb 2026 13:26:17 +0100 Subject: [PATCH 101/205] Stabilize world axis overlay arrows --- 61_UI/include/app/App.hpp | 49 +++++++++++++++++++++++++++++++-------- 1 file changed, 39 insertions(+), 10 deletions(-) diff --git a/61_UI/include/app/App.hpp b/61_UI/include/app/App.hpp index dbd072acc..5789ea2a3 100644 --- a/61_UI/include/app/App.hpp +++ b/61_UI/include/app/App.hpp @@ -837,8 +837,12 @@ class App final : public examples::SimpleWindowedApplication drawProjectedSegment(aWorld, bWorld, color, thickness); }; - constexpr float axisLength = 7.5f; const float32_t3 origin = float32_t3(0.0f); + ImVec2 originScreen = {}; + if (!projectWorldPointToViewportClipped(origin, originScreen)) + return; + + constexpr float axisLength = 5.0f; const float32_t3 xPos = float32_t3(axisLength, 0.0f, 0.0f); const float32_t3 yPos = float32_t3(0.0f, axisLength, 0.0f); const float32_t3 zPos = float32_t3(0.0f, 0.0f, axisLength); @@ -846,12 +850,39 @@ class App final : public examples::SimpleWindowedApplication const float32_t3 yNeg = float32_t3(0.0f, -axisLength * 0.3f, 0.0f); const float32_t3 zNeg = float32_t3(0.0f, 0.0f, -axisLength * 0.4f); - drawWorldLine(origin, xPos, IM_COL32(244, 92, 92, 245), 3.2f); - drawWorldLine(origin, yPos, IM_COL32(124, 236, 132, 245), 3.2f); - drawWorldLine(origin, zPos, IM_COL32(106, 166, 255, 245), 3.2f); - drawWorldLine(origin, xNeg, IM_COL32(128, 74, 74, 180), 1.6f); - drawWorldLine(origin, yNeg, IM_COL32(74, 128, 78, 180), 1.6f); - drawWorldLine(origin, zNeg, IM_COL32(70, 88, 124, 180), 1.6f); + drawWorldLine(origin, xPos, IM_COL32(244, 92, 92, 245), 2.8f); + drawWorldLine(origin, yPos, IM_COL32(124, 236, 132, 245), 2.8f); + drawWorldLine(origin, zPos, IM_COL32(106, 166, 255, 245), 2.8f); + drawWorldLine(origin, xNeg, IM_COL32(128, 74, 74, 170), 1.4f); + drawWorldLine(origin, yNeg, IM_COL32(74, 128, 78, 170), 1.4f); + drawWorldLine(origin, zNeg, IM_COL32(70, 88, 124, 170), 1.4f); + + const auto drawAxisArrowHead = [&](const float32_t3& tipWorld, const float32_t3& tailWorld, ImU32 color) -> void + { + ImVec2 tipScreen = {}; + ImVec2 tailScreen = {}; + if (!projectWorldPointToViewportClipped(tipWorld, tipScreen) || !projectWorldPointToViewportClipped(tailWorld, tailScreen)) + return; + + const ImVec2 dir = ImVec2(tipScreen.x - tailScreen.x, tipScreen.y - tailScreen.y); + const float len = std::sqrt(dir.x * dir.x + dir.y * dir.y); + if (len < 1e-3f) + return; + + const ImVec2 n = ImVec2(dir.x / len, dir.y / len); + const ImVec2 ortho = ImVec2(-n.y, n.x); + const float headLength = 9.0f; + const float headHalfWidth = 4.5f; + + const ImVec2 base = ImVec2(tipScreen.x - n.x * headLength, tipScreen.y - n.y * headLength); + const ImVec2 left = ImVec2(base.x + ortho.x * headHalfWidth, base.y + ortho.y * headHalfWidth); + const ImVec2 right = ImVec2(base.x - ortho.x * headHalfWidth, base.y - ortho.y * headHalfWidth); + drawList->AddTriangleFilled(tipScreen, left, right, color); + }; + + drawAxisArrowHead(xPos, float32_t3(axisLength - 0.55f, 0.0f, 0.0f), IM_COL32(255, 162, 162, 255)); + drawAxisArrowHead(yPos, float32_t3(0.0f, axisLength - 0.55f, 0.0f), IM_COL32(186, 255, 192, 255)); + drawAxisArrowHead(zPos, float32_t3(0.0f, 0.0f, axisLength - 0.55f), IM_COL32(178, 216, 255, 255)); auto drawAxisLabel = [&](const char* label, const float32_t3& worldPoint, ImU32 color) -> void { @@ -861,9 +892,7 @@ class App final : public examples::SimpleWindowedApplication drawList->AddText(ImVec2(screenPos.x + 4.0f, screenPos.y + 3.0f), color, label); }; - ImVec2 originScreen = {}; - if (projectWorldPointToViewportClipped(origin, originScreen)) - drawList->AddCircleFilled(originScreen, 4.0f, IM_COL32(240, 248, 255, 220), 16); + drawList->AddCircleFilled(originScreen, 4.0f, IM_COL32(240, 248, 255, 220), 16); drawAxisLabel("X", xPos, IM_COL32(255, 152, 152, 255)); drawAxisLabel("Y", yPos, IM_COL32(172, 255, 178, 255)); From c30b5e4f7aee5c8e7b1bccbc492e2ab548585b78 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Wed, 18 Feb 2026 20:15:22 +0100 Subject: [PATCH 102/205] Improve camera smoke visuals and HDR sky environment --- 61_UI/AppImGuiListen.cpp | 4 +- 61_UI/AppInit.cpp | 297 ++++++++++++++++++ 61_UI/AppWorkLoop.cpp | 19 ++ 61_UI/CMakeLists.txt | 6 + 61_UI/app_resources/sky_env_fragment.hlsl | 86 +++++ 61_UI/include/app/App.hpp | 30 +- .../geometry/CSimpleDebugRenderer.hpp | 2 +- 7 files changed, 438 insertions(+), 6 deletions(-) create mode 100644 61_UI/app_resources/sky_env_fragment.hlsl diff --git a/61_UI/AppImGuiListen.cpp b/61_UI/AppImGuiListen.cpp index e61ab0292..8c3dc6bbe 100644 --- a/61_UI/AppImGuiListen.cpp +++ b/61_UI/AppImGuiListen.cpp @@ -144,8 +144,6 @@ void App::imguiListen() imguizmoPlanar.projection = getCastedMatrix(hlsl::transpose(projection.getProjectionMatrix())); const auto viewMatrix = getMatrix3x4As4x4(getCastedMatrix(planarViewCameraBound->getGimbal().getViewMatrix())); const auto projectionMatrix = getCastedMatrix(projection.getProjectionMatrix()); - const auto& projectionParams = projection.getParameters(); - drawWorldReferenceOverlay(cursorPos, contentRegionSize, viewMatrix, projectionMatrix, binding.leftHandedProjection, projectionParams.m_zNear, projectionParams.m_zFar); if (flipGizmoY) // note we allow to flip gizmo just to match our coordinates imguizmoPlanar.projection[1][1] *= -1.f; // https://johannesugb.github.io/gpu-programming/why-do-opengl-proj-matrices-fail-in-vulkan/ @@ -459,12 +457,14 @@ void App::imguiListen() auto& projection = planarBound->getPlanarProjections()[binding.boundProjectionIx.value()]; applyDollyZoomProjection(boundPlanarCamera, projection); projection.update(binding.leftHandedProjection, binding.aspectRatio); + binding.isOrthographicProjection = projection.getParameters().m_type == IPlanarProjection::CProjection::Orthographic; auto viewMatrix = getCastedMatrix(boundPlanarCamera->getGimbal().getViewMatrix()); auto projectionMatrix = getCastedMatrix(projection.getProjectionMatrix()); auto viewProjMatrix = mul(projectionMatrix, getMatrix3x4As4x4(viewMatrix)); binding.viewMatrix = viewMatrix; + binding.projectionMatrix = projectionMatrix; binding.viewProjMatrix = viewProjMatrix; } } diff --git a/61_UI/AppInit.cpp b/61_UI/AppInit.cpp index e550d0600..17eca2b4f 100644 --- a/61_UI/AppInit.cpp +++ b/61_UI/AppInit.cpp @@ -1,4 +1,50 @@ #include "app/App.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + struct SpaceEnvBlobHeader final + { + uint32_t magic = 0u; + uint32_t width = 0u; + uint32_t height = 0u; + uint32_t format = 0u; + uint64_t payloadSize = 0ull; + }; + + constexpr uint32_t SpaceEnvBlobMagic = 0x31425645u; // "EVB1" + constexpr uint32_t SpaceEnvBlobFormatRgba16Sfloat = 2u; + + bool loadSpaceEnvBlob(const std::filesystem::path& blobPath, SpaceEnvBlobHeader& outHeader, std::vector& outPayload) + { + std::ifstream in(blobPath, std::ios::binary); + if (!in.is_open()) + return false; + + in.read(reinterpret_cast(&outHeader), sizeof(outHeader)); + if (in.gcount() != sizeof(outHeader)) + return false; + + if (outHeader.magic != SpaceEnvBlobMagic || outHeader.format != 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; + + outPayload.resize(static_cast(outHeader.payloadSize)); + in.read(reinterpret_cast(outPayload.data()), static_cast(outPayload.size())); + return in.gcount() == static_cast(outPayload.size()); + } +} bool App::onAppInitialized(smart_refctd_ptr&& system) { @@ -1383,6 +1429,257 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) return logFail("Failed to create Scene Renderpass!"); } + { + constexpr std::string_view SpaceEnvBlobCandidates[] = { + "rich_blue_nebulae_1_8k.rgba16f.envblob" + }; + + SpaceEnvBlobHeader envBlobHeader = {}; + std::vector envBlobPayload; + const std::array SpaceEnvSearchRoots = { + localInputCWD / "media", + localInputCWD / "app_resources" + }; + for (const auto candidate : SpaceEnvBlobCandidates) + { + for (const auto& root : SpaceEnvSearchRoots) + { + const auto candidatePath = root / candidate; + if (loadSpaceEnvBlob(candidatePath, envBlobHeader, envBlobPayload)) + { + break; + } + } + if (!envBlobPayload.empty()) + break; + } + if (envBlobPayload.empty()) + return logFail("Failed to load space environment blob from available assets."); + + const E_FORMAT envFormat = EF_R16G16B16A16_SFLOAT; + const asset::VkExtent3D envExtent = { envBlobHeader.width, envBlobHeader.height, 1u }; + constexpr uint32_t envMipLevels = 1u; + constexpr uint32_t envArrayLayers = 1u; + const E_FORMAT envGpuFormat = envFormat; + const std::array envRegions = {{ + { + .bufferOffset = 0ull, + .bufferRowLength = 0u, + .bufferImageHeight = 0u, + .imageSubresource = { + .aspectMask = IImage::E_ASPECT_FLAGS::EAF_COLOR_BIT, + .mipLevel = 0u, + .baseArrayLayer = 0u, + .layerCount = envArrayLayers + }, + .imageOffset = { 0, 0, 0 }, + .imageExtent = envExtent + } + }}; + + IGPUImage::SCreationParams imageParams = {}; + imageParams = asset::IImage::SCreationParams{ + .type = IGPUImage::ET_2D, + .samples = IGPUImage::ESCF_1_BIT, + .format = envGpuFormat, + .extent = envExtent, + .mipLevels = envMipLevels, + .arrayLayers = envArrayLayers, + .flags = IGPUImage::ECF_NONE, + .usage = IGPUImage::EUF_SAMPLED_BIT | IGPUImage::EUF_TRANSFER_DST_BIT + }; + m_spaceEnvImage = m_device->createImage(std::move(imageParams)); + if (!m_spaceEnvImage) + return logFail("Failed to create space environment image."); + m_spaceEnvImage->setObjectDebugName("61_UI Space Environment"); + + auto memReqs = m_spaceEnvImage->getMemoryReqs(); + memReqs.memoryTypeBits &= m_physicalDevice->getDeviceLocalMemoryTypeBits(); + if (!m_device->allocate(memReqs, m_spaceEnvImage.get()).isValid()) + return logFail("Failed to allocate memory for space environment image."); + + 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_spaceEnvImage.get(), + .subresourceRange = { + .aspectMask = IGPUImage::EAF_COLOR_BIT, + .baseMipLevel = 0u, + .levelCount = envMipLevels, + .baseArrayLayer = 0u, + .layerCount = envArrayLayers + }, + .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(), + envFormat, + m_spaceEnvImage.get(), + IGPUImage::LAYOUT::TRANSFER_DST_OPTIMAL, + std::span(envRegions)); + 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_spaceEnvImage.get(), + .subresourceRange = { + .aspectMask = IGPUImage::EAF_COLOR_BIT, + .baseMipLevel = 0u, + .levelCount = envMipLevels, + .baseArrayLayer = 0u, + .layerCount = envArrayLayers + }, + .oldLayout = IGPUImage::LAYOUT::TRANSFER_DST_OPTIMAL, + .newLayout = IGPUImage::LAYOUT::READ_ONLY_OPTIMAL + } + }; + const IGPUCommandBuffer::SPipelineBarrierDependencyInfo postDep = { .imgBarriers = postBarrier }; + success = success && cmdbuf->pipelineBarrier(asset::EDF_NONE, postDep); + return success; + }); + if (uploadResult.copy() != IQueue::RESULT::SUCCESS) + return logFail("Failed to upload space environment map."); + + IGPUImageView::SCreationParams viewParams = {}; + viewParams.subUsages = IGPUImage::EUF_SAMPLED_BIT; + viewParams.image = core::smart_refctd_ptr(m_spaceEnvImage); + viewParams.viewType = IGPUImageView::ET_2D; + viewParams.format = envGpuFormat; + viewParams.subresourceRange.aspectMask = IGPUImage::EAF_COLOR_BIT; + viewParams.subresourceRange.baseMipLevel = 0u; + viewParams.subresourceRange.levelCount = envMipLevels; + viewParams.subresourceRange.baseArrayLayer = 0u; + viewParams.subresourceRange.layerCount = envArrayLayers; + m_spaceEnvImageView = m_device->createImageView(std::move(viewParams)); + if (!m_spaceEnvImageView) + return logFail("Failed to create space environment image view."); + + 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_spaceEnvSampler = m_device->createSampler(samplerParams); + if (!m_spaceEnvSampler) + return logFail("Failed to create space environment sampler."); + + 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_spaceEnvSampler + } + }; + m_spaceEnvDescriptorSetLayout = m_device->createDescriptorSetLayout(bindings); + if (!m_spaceEnvDescriptorSetLayout) + return logFail("Failed to create space environment descriptor set layout."); + + 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_spaceEnvDescriptorSetLayout), + nullptr, + nullptr, + nullptr); + if (!pipelineLayout) + return logFail("Failed to create space environment pipeline layout."); + + auto loadPrecompiledShader = [&](const std::string_view key) -> smart_refctd_ptr + { + IAssetLoader::SAssetLoadParams loadParams = {}; + loadParams.logger = m_logger.get(); + loadParams.workingDirectory = "app_resources"; + auto bundle = m_assetManager->getAsset(key.data(), loadParams); + const auto& contents = bundle.getContents(); + if (contents.empty()) + return nullptr; + return IAsset::castDown(contents[0]); + }; + const auto spaceFragKey = nbl::this_example::builtin::build::get_spirv_key<"sky_env_fragment">(m_device.get()); + auto fragmentShader = loadPrecompiledShader(spaceFragKey.data()); + if (!fragmentShader) + return logFail("Failed to load space environment fragment shader."); + + nbl::ext::FullScreenTriangle::ProtoPipeline fsTriProto(m_assetManager.get(), m_device.get(), m_logger.get()); + if (!fsTriProto) + return logFail("Failed to create FullScreenTriangle prototype pipeline."); + + const IGPUPipelineBase::SShaderSpecInfo fragmentSpec = { + .shader = fragmentShader.get(), + .entryPoint = "main" + }; + m_spaceEnvPipeline = fsTriProto.createPipeline(fragmentSpec, pipelineLayout.get(), m_sceneRenderpass.get()); + if (!m_spaceEnvPipeline) + return logFail("Failed to create space environment pipeline."); + + uint32_t setCount = 1u; + const IGPUDescriptorSetLayout* setLayouts[] = { m_spaceEnvDescriptorSetLayout.get() }; + m_spaceEnvDescriptorPool = m_device->createDescriptorPoolForDSLayouts(IDescriptorPool::E_CREATE_FLAGS::ECF_NONE, setLayouts, &setCount); + if (!m_spaceEnvDescriptorPool) + return logFail("Failed to create space environment descriptor pool."); + m_spaceEnvDescriptorSet = m_spaceEnvDescriptorPool->createDescriptorSet(core::smart_refctd_ptr(m_spaceEnvDescriptorSetLayout)); + if (!m_spaceEnvDescriptorSet) + return logFail("Failed to create space environment descriptor set."); + + IGPUDescriptorSet::SDescriptorInfo info = {}; + info.desc = m_spaceEnvImageView; + info.info.image.imageLayout = IImage::LAYOUT::READ_ONLY_OPTIMAL; + + IGPUDescriptorSet::SWriteDescriptorSet write = {}; + write.dstSet = m_spaceEnvDescriptorSet.get(); + write.binding = 0u; + write.arrayElement = 0u; + write.count = 1u; + write.info = &info; + if (!m_device->updateDescriptorSets({ &write, 1u }, {})) + return logFail("Failed to update space environment descriptor set."); + } + const auto& geometries = m_scene->getInitParams().geometries; if (geometries.empty()) return logFail("No geometries found for scene!"); diff --git a/61_UI/AppWorkLoop.cpp b/61_UI/AppWorkLoop.cpp index 69f0b7364..5da6e681e 100644 --- a/61_UI/AppWorkLoop.cpp +++ b/61_UI/AppWorkLoop.cpp @@ -71,6 +71,25 @@ void App::workLoopBody() willSubmit &= cmdbuf->setViewport(0u, 1u, &viewport); willSubmit &= cmdbuf->setScissor(0u, 1u, &renderArea); + if (m_spaceEnvPipeline && m_spaceEnvDescriptorSet) + { + auto* pipelineLayout = m_spaceEnvPipeline->getLayout(); + const IGPUDescriptorSet* descriptorSets[] = { m_spaceEnvDescriptorSet.get() }; + SpaceEnvPushConstants pc = {}; + pc.invProj = hlsl::inverse(binding.projectionMatrix); + pc.invViewRot = hlsl::inverse(getMatrix3x4As4x4(binding.viewMatrix)); + pc.invViewRot[0].w = 0.0f; + pc.invViewRot[1].w = 0.0f; + pc.invViewRot[2].w = 0.0f; + pc.invViewRot[3] = float32_t4(0.0f, 0.0f, 0.0f, 1.0f); + pc.orthoMode = binding.isOrthographicProjection ? 1u : 0u; + + willSubmit &= cmdbuf->bindGraphicsPipeline(m_spaceEnvPipeline.get()); + willSubmit &= cmdbuf->bindDescriptorSets(EPBP_GRAPHICS, pipelineLayout, 0u, 1u, descriptorSets); + willSubmit &= cmdbuf->pushConstants(pipelineLayout, IShader::E_SHADER_STAGE::ESS_FRAGMENT, 0u, sizeof(pc), &pc); + willSubmit &= nbl::ext::FullScreenTriangle::recordDrawCall(cmdbuf); + } + const auto viewParams = CSimpleDebugRenderer::SViewParams(binding.viewMatrix, binding.viewProjMatrix); m_renderer->render(cmdbuf, viewParams); diff --git a/61_UI/CMakeLists.txt b/61_UI/CMakeLists.txt index 68dbaedd8..1a4e2b85a 100644 --- a/61_UI/CMakeLists.txt +++ b/61_UI/CMakeLists.txt @@ -18,6 +18,7 @@ if(NBL_BUILD_IMGUI) imtestengine imguizmo "${NBL_EXT_IMGUI_UI_LIB}" + Nabla::ext::FullScreenTriangle ) nbl_create_executable_project("${NBL_EXTRA_SOURCES}" "" "${NBL_INCLUDE_SERACH_DIRECTORIES}" "${NBL_LIBRARIES}") @@ -53,6 +54,11 @@ if(NBL_BUILD_IMGUI) "INPUT": "app_resources/imgui_fragment.hlsl", "KEY": "imgui_fragment", "COMPILE_OPTIONS": ["-T", "ps_6_7", "-E", "PSMain", "-O3"] + }, + { + "INPUT": "app_resources/sky_env_fragment.hlsl", + "KEY": "sky_env_fragment", + "COMPILE_OPTIONS": ["-T", "ps_6_7", "-E", "main", "-O3"] } ] ]=]) 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..3005dafcc --- /dev/null +++ b/61_UI/app_resources/sky_env_fragment.hlsl @@ -0,0 +1,86 @@ +// 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 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 = centerNearVS_H.xyz / max(abs(centerNearVS_H.w), 1e-6f); + const float32_t3 centerFarVS = centerFarVS_H.xyz / max(abs(centerFarVS_H.w), 1e-6f); + 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 = leftNearVS_H.xyz / max(abs(leftNearVS_H.w), 1e-6f); + const float32_t3 rightNearVS = rightNearVS_H.xyz / max(abs(rightNearVS_H.w), 1e-6f); + const float32_t3 downNearVS = downNearVS_H.xyz / max(abs(downNearVS_H.w), 1e-6f); + const float32_t3 upNearVS = upNearVS_H.xyz / max(abs(upNearVS_H.w), 1e-6f); + + 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(viewH.xyz / max(abs(viewH.w), 1e-6f)); + } + 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.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 index 5789ea2a3..dda031c26 100644 --- a/61_UI/include/app/App.hpp +++ b/61_UI/include/app/App.hpp @@ -27,6 +27,7 @@ using json = nlohmann::json; #include "camera/CCubeProjection.hpp" #include "glm/glm/ext/matrix_clip_space.hpp" // TODO: TESTING #include "glm/gtc/quaternion.hpp" +#include "nbl/ext/FullScreenTriangle/FullScreenTriangle.h" #include "nbl/ext/ScreenShot/ScreenShot.h" #include "nbl/this_example/builtin/build/spirv/keys.hpp" #if __has_include("nbl/this_example/builtin/CArchive.h") @@ -270,6 +271,15 @@ class App final : public examples::SimpleWindowedApplication constexpr static inline auto finalSceneRenderFormat = EF_R8G8B8A8_SRGB; constexpr static inline IGPUCommandBuffer::SClearColorValue SceneClearColor = { .float32 = {0.014f,0.018f,0.030f,1.f} }; constexpr static inline IGPUCommandBuffer::SClearDepthStencilValue SceneClearDepth = { .depth = 0.f }; + 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; @@ -316,6 +326,13 @@ class App final : public examples::SimpleWindowedApplication } bool onAppInitialized(smart_refctd_ptr&& system) override; + core::bitflag getLogLevelMask() override + { + return core::bitflag(system::ILogger::ELL_INFO) | + system::ILogger::ELL_WARNING | + system::ILogger::ELL_PERFORMANCE | + system::ILogger::ELL_ERROR; + } bool updateGUIDescriptorSet() { @@ -585,9 +602,7 @@ class App final : public examples::SimpleWindowedApplication inline void syncVisualDebugWindowBindings() { - if (!(m_scriptedInput.enabled && m_scriptedInput.visualDebug)) - return; - if (m_scriptedInput.exclusive) + if (!m_scriptedInput.enabled) return; if (windowBindings.size() < 2u || m_planarProjections.empty()) return; @@ -1682,11 +1697,13 @@ class App final : public examples::SimpleWindowedApplication 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; @@ -1846,6 +1863,13 @@ class App final : public examples::SimpleWindowedApplication nbl::core::smart_refctd_ptr m_sceneRenderpass; nbl::core::smart_refctd_ptr m_renderer; std::optional m_gridGeometryIx = std::nullopt; + core::smart_refctd_ptr m_spaceEnvPipeline; + core::smart_refctd_ptr m_spaceEnvDescriptorSetLayout; + core::smart_refctd_ptr m_spaceEnvDescriptorPool; + core::smart_refctd_ptr m_spaceEnvDescriptorSet; + core::smart_refctd_ptr m_spaceEnvImage; + core::smart_refctd_ptr m_spaceEnvImageView; + core::smart_refctd_ptr m_spaceEnvSampler; CRenderUI m_ui; video::CDumbPresentationOracle oracle; 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; } From bf0e72736913d3feed74b9def76595a22b5c63b3 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Wed, 18 Feb 2026 20:47:59 +0100 Subject: [PATCH 103/205] Integrate frustum visualization into camera smoke --- 61_UI/AppInit.cpp | 18 + 61_UI/AppWorkLoop.cpp | 31 +- 61_UI/CMakeLists.txt | 1 + 61_UI/app_resources/cameraz_smoke_all.json | 1119 ++++++++++++++++---- 61_UI/include/app/App.hpp | 2 + 5 files changed, 946 insertions(+), 225 deletions(-) diff --git a/61_UI/AppInit.cpp b/61_UI/AppInit.cpp index 17eca2b4f..36f24be01 100644 --- a/61_UI/AppInit.cpp +++ b/61_UI/AppInit.cpp @@ -1686,6 +1686,24 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) m_renderer = CSimpleDebugRenderer::create(m_assetManager.get(), m_sceneRenderpass.get(), 0, { &geometries.front().get(), geometries.size() }); if (!m_renderer) return logFail("Failed to create debug renderer!"); + { + const asset::SPushConstantRange singlePcRange = { + .stageFlags = IShader::E_SHADER_STAGE::ESS_VERTEX, + .offset = offsetof(ext::frustum::PushConstants, spc), + .size = sizeof(ext::frustum::SSinglePC) + }; + + ext::frustum::CDrawFrustum::SCreationParameters frustumParams = {}; + frustumParams.transfer = getTransferUpQueue(); + frustumParams.assetManager = m_assetManager; + frustumParams.drawMode = ext::frustum::CDrawFrustum::DrawMode::DM_SINGLE; + frustumParams.singlePipelineLayout = ext::frustum::CDrawFrustum::createPipelineLayoutFromPCRange(m_device.get(), singlePcRange); + frustumParams.renderpass = core::smart_refctd_ptr(m_sceneRenderpass); + frustumParams.utilities = m_utils; + m_drawFrustum = ext::frustum::CDrawFrustum::create(std::move(frustumParams)); + if (!m_drawFrustum) + return logFail("Failed to create frustum drawer."); + } { const auto& pipelines = m_renderer->getInitParams().pipelines; diff --git a/61_UI/AppWorkLoop.cpp b/61_UI/AppWorkLoop.cpp index 5da6e681e..e8d50a420 100644 --- a/61_UI/AppWorkLoop.cpp +++ b/61_UI/AppWorkLoop.cpp @@ -44,7 +44,7 @@ void App::workLoopBody() willSubmit &= cmdbuf->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT); willSubmit &= cmdbuf->beginDebugMarker("UIApp Frame"); - auto renderScene = [&](windowControlBinding& binding) + auto renderScene = [&](windowControlBinding& binding, const uint32_t bindingIx) { if (!binding.sceneFramebuffer) return; @@ -92,6 +92,29 @@ void App::workLoopBody() const auto viewParams = CSimpleDebugRenderer::SViewParams(binding.viewMatrix, binding.viewProjMatrix); m_renderer->render(cmdbuf, viewParams); + if (m_drawFrustum) + { + ext::frustum::CDrawFrustum::DrawParameters drawParams = {}; + drawParams.commandBuffer = cmdbuf; + drawParams.viewProjectionMatrix = binding.viewProjMatrix; + drawParams.lineWidth = 1.2f; + + for (uint32_t frustumIx = 0u; frustumIx < windowBindings.size(); ++frustumIx) + { + const auto& frustumBinding = windowBindings[frustumIx]; + if (!frustumBinding.boundProjectionIx.has_value()) + continue; + if (frustumBinding.activePlanarIx >= m_planarProjections.size()) + continue; + + const float32_t4 color = (frustumIx == bindingIx) ? + float32_t4(1.0f, 0.95f, 0.25f, 1.0f) : + (frustumBinding.isOrthographicProjection ? + float32_t4(0.30f, 0.90f, 1.00f, 1.0f) : + float32_t4(1.00f, 0.45f, 0.90f, 1.0f)); + willSubmit &= m_drawFrustum->renderSingle(drawParams, hlsl::inverse(frustumBinding.viewProjMatrix), color); + } + } } willSubmit &= cmdbuf->endRenderPass(); @@ -128,10 +151,10 @@ void App::workLoopBody() } if (useWindow) - for (auto& binding : windowBindings) - renderScene(binding); + for (uint32_t i = 0u; i < windowBindings.size(); ++i) + renderScene(windowBindings[i], i); else - renderScene(windowBindings[activeRenderWindowIx]); + renderScene(windowBindings[activeRenderWindowIx], activeRenderWindowIx); const IGPUCommandBuffer::SClearColorValue clearValue = { .float32 = {0.f,0.f,0.f,1.f} }; const IGPUCommandBuffer::SRenderpassBeginInfo info = { diff --git a/61_UI/CMakeLists.txt b/61_UI/CMakeLists.txt index 1a4e2b85a..5fe5821fd 100644 --- a/61_UI/CMakeLists.txt +++ b/61_UI/CMakeLists.txt @@ -18,6 +18,7 @@ if(NBL_BUILD_IMGUI) imtestengine imguizmo "${NBL_EXT_IMGUI_UI_LIB}" + Nabla::ext::Frustum Nabla::ext::FullScreenTriangle ) diff --git a/61_UI/app_resources/cameraz_smoke_all.json b/61_UI/app_resources/cameraz_smoke_all.json index cfa34d543..1e0fd4e20 100644 --- a/61_UI/app_resources/cameraz_smoke_all.json +++ b/61_UI/app_resources/cameraz_smoke_all.json @@ -6,9 +6,15 @@ "enableActiveCameraMovement": true, "capture_prefix": "camera_smoke", "capture_frames": [ - 31 + 64 ], "events": [ + { + "frame": 0, + "type": "action", + "action": "set_use_window", + "value": true + }, { "frame": 0, "type": "action", @@ -27,12 +33,48 @@ "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.0, + 2.12, 1.0 ], "rotation_deg": [ @@ -48,34 +90,660 @@ }, { "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": 3, + "frame": 6, "type": "action", "action": "set_active_planar", "value": 1 }, { - "frame": 3, + "frame": 6, "type": "action", "action": "set_projection_type", "value": "perspective" }, { - "frame": 4, + "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.0, - 2.0, + 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, - 0.0 + 141.0, + 82.0 ], "scale": [ 1.0, @@ -84,72 +752,71 @@ ] }, { - "frame": 6, + "frame": 48, "type": "action", "action": "set_active_render_window", "value": 0 }, { - "frame": 6, + "frame": 48, "type": "action", "action": "set_active_planar", - "value": 2 + "value": 8 }, { - "frame": 6, + "frame": 48, "type": "action", "action": "set_projection_type", "value": "perspective" }, { - "frame": 7, - "type": "imguizmo", - "translation": [ - 4.0, - 2.0, - 1.0 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1.0, - 1.0, - 1.0 - ] + "frame": 48, + "type": "action", + "action": "set_left_handed", + "value": true }, { - "frame": 9, + "frame": 48, "type": "action", "action": "set_active_render_window", - "value": 0 + "value": 1 }, { - "frame": 9, + "frame": 48, "type": "action", "action": "set_active_planar", - "value": 3 + "value": 8 }, { - "frame": 9, + "frame": 48, "type": "action", "action": "set_projection_type", - "value": "perspective" + "value": "orthographic" + }, + { + "frame": 48, + "type": "action", + "action": "set_left_handed", + "value": true + }, + { + "frame": 48, + "type": "action", + "action": "set_active_render_window", + "value": 0 }, { - "frame": 10, + "frame": 49, "type": "imguizmo", "translation": [ - 4.0, - 2.0, - 1.0 + 3.949499, + 1.945398, + 1.8 ], "rotation_deg": [ 0.0, - 0.0, - 0.0 + 152.0, + 88.0 ], "scale": [ 1.0, @@ -158,35 +825,17 @@ ] }, { - "frame": 12, - "type": "action", - "action": "set_active_render_window", - "value": 0 - }, - { - "frame": 12, - "type": "action", - "action": "set_active_planar", - "value": 4 - }, - { - "frame": 12, - "type": "action", - "action": "set_projection_type", - "value": "perspective" - }, - { - "frame": 13, + "frame": 51, "type": "imguizmo", "translation": [ - 4.0, - 2.0, - 1.0 + 3.922753, + 1.905666, + 1.83 ], "rotation_deg": [ 0.0, - 0.0, - 0.0 + 160.0, + 93.0 ], "scale": [ 1.0, @@ -195,72 +844,71 @@ ] }, { - "frame": 15, + "frame": 54, "type": "action", "action": "set_active_render_window", "value": 0 }, { - "frame": 15, + "frame": 54, "type": "action", "action": "set_active_planar", - "value": 5 + "value": 9 }, { - "frame": 15, + "frame": 54, "type": "action", "action": "set_projection_type", "value": "perspective" }, { - "frame": 16, - "type": "imguizmo", - "translation": [ - 4.0, - 2.0, - 1.0 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1.0, - 1.0, - 1.0 - ] + "frame": 54, + "type": "action", + "action": "set_left_handed", + "value": false }, { - "frame": 18, + "frame": 54, "type": "action", "action": "set_active_render_window", - "value": 0 + "value": 1 }, { - "frame": 18, + "frame": 54, "type": "action", "action": "set_active_planar", - "value": 6 + "value": 9 }, { - "frame": 18, + "frame": 54, "type": "action", "action": "set_projection_type", - "value": "perspective" + "value": "orthographic" }, { - "frame": 19, + "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.0, - 2.0, - 1.0 + 4.001345, + 2.006909, + 1.9 ], "rotation_deg": [ 0.0, - 0.0, - 0.0 + 171.0, + 99.0 ], "scale": [ 1.0, @@ -269,35 +917,17 @@ ] }, { - "frame": 21, - "type": "action", - "action": "set_active_render_window", - "value": 0 - }, - { - "frame": 21, - "type": "action", - "action": "set_active_planar", - "value": 7 - }, - { - "frame": 21, - "type": "action", - "action": "set_projection_type", - "value": "perspective" - }, - { - "frame": 22, + "frame": 57, "type": "imguizmo", "translation": [ - 4.0, - 2.0, - 1.0 + 3.988672, + 1.95889, + 1.93 ], "rotation_deg": [ 0.0, - 0.0, - 0.0 + 179.0, + 104.0 ], "scale": [ 1.0, @@ -306,72 +936,71 @@ ] }, { - "frame": 24, + "frame": 60, "type": "action", "action": "set_active_render_window", "value": 0 }, { - "frame": 24, + "frame": 60, "type": "action", "action": "set_active_planar", - "value": 8 + "value": 10 }, { - "frame": 24, + "frame": 60, "type": "action", "action": "set_projection_type", "value": "perspective" }, { - "frame": 25, - "type": "imguizmo", - "translation": [ - 4.0, - 2.0, - 1.0 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1.0, - 1.0, - 1.0 - ] + "frame": 60, + "type": "action", + "action": "set_left_handed", + "value": true }, { - "frame": 27, + "frame": 60, "type": "action", "action": "set_active_render_window", - "value": 0 + "value": 1 }, { - "frame": 27, + "frame": 60, "type": "action", "action": "set_active_planar", - "value": 9 + "value": 10 }, { - "frame": 27, + "frame": 60, "type": "action", "action": "set_projection_type", - "value": "perspective" + "value": "orthographic" }, { - "frame": 28, + "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.0, - 2.0, - 1.0 + 4.052559, + 2.066525, + 2.0 ], "rotation_deg": [ 0.0, - 0.0, - 0.0 + 190.0, + 110.0 ], "scale": [ 1.0, @@ -380,35 +1009,17 @@ ] }, { - "frame": 30, - "type": "action", - "action": "set_active_render_window", - "value": 0 - }, - { - "frame": 30, - "type": "action", - "action": "set_active_planar", - "value": 10 - }, - { - "frame": 30, - "type": "action", - "action": "set_projection_type", - "value": "perspective" - }, - { - "frame": 31, + "frame": 63, "type": "imguizmo", "translation": [ - 4.0, - 2.0, - 1.0 + 4.056059, + 2.016717, + 2.03 ], "rotation_deg": [ 0.0, - 0.0, - 0.0 + 198.0, + 115.0 ], "scale": [ 1.0, @@ -425,18 +1036,14 @@ { "frame": 1, "kind": "gimbal_step", - "min_pos_delta": 0.009, - "max_pos_delta": 8.0 + "min_pos_delta": 0.007, + "max_pos_delta": 12.0 }, { "frame": 3, - "kind": "baseline" - }, - { - "frame": 4, "kind": "gimbal_step", - "min_pos_delta": 0.009, - "max_pos_delta": 8.0 + "min_pos_delta": 0.007, + "max_pos_delta": 12.0 }, { "frame": 6, @@ -445,18 +1052,14 @@ { "frame": 7, "kind": "gimbal_step", - "min_pos_delta": 0.009, - "max_pos_delta": 8.0 + "min_pos_delta": 0.007, + "max_pos_delta": 12.0 }, { "frame": 9, - "kind": "baseline" - }, - { - "frame": 10, "kind": "gimbal_step", - "min_pos_delta": 0.009, - "max_pos_delta": 8.0 + "min_pos_delta": 0.007, + "max_pos_delta": 12.0 }, { "frame": 12, @@ -465,18 +1068,14 @@ { "frame": 13, "kind": "gimbal_step", - "min_pos_delta": 0.009, - "max_pos_delta": 8.0 + "min_pos_delta": 0.007, + "max_pos_delta": 12.0 }, { "frame": 15, - "kind": "baseline" - }, - { - "frame": 16, "kind": "gimbal_step", - "min_pos_delta": 0.009, - "max_pos_delta": 8.0 + "min_pos_delta": 0.007, + "max_pos_delta": 12.0 }, { "frame": 18, @@ -485,18 +1084,14 @@ { "frame": 19, "kind": "gimbal_step", - "min_pos_delta": 0.009, - "max_pos_delta": 8.0 + "min_pos_delta": 0.007, + "max_pos_delta": 12.0 }, { "frame": 21, - "kind": "baseline" - }, - { - "frame": 22, "kind": "gimbal_step", - "min_pos_delta": 0.009, - "max_pos_delta": 8.0 + "min_pos_delta": 0.007, + "max_pos_delta": 12.0 }, { "frame": 24, @@ -505,28 +1100,110 @@ { "frame": 25, "kind": "gimbal_step", - "min_pos_delta": 0.009, - "max_pos_delta": 8.0 + "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": 28, + "frame": 31, + "kind": "gimbal_step", + "min_pos_delta": 0.007, + "max_pos_delta": 12.0 + }, + { + "frame": 33, "kind": "gimbal_step", - "min_pos_delta": 0.009, - "max_pos_delta": 8.0 + "min_pos_delta": 0.007, + "max_pos_delta": 12.0 }, { - "frame": 30, + "frame": 36, "kind": "baseline" }, { - "frame": 31, + "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.009, - "max_pos_delta": 8.0 + "min_pos_delta": 0.007, + "max_pos_delta": 12.0 } ] -} +} \ No newline at end of file diff --git a/61_UI/include/app/App.hpp b/61_UI/include/app/App.hpp index dda031c26..3bb9651c4 100644 --- a/61_UI/include/app/App.hpp +++ b/61_UI/include/app/App.hpp @@ -27,6 +27,7 @@ using json = nlohmann::json; #include "camera/CCubeProjection.hpp" #include "glm/glm/ext/matrix_clip_space.hpp" // TODO: TESTING #include "glm/gtc/quaternion.hpp" +#include "nbl/ext/Frustum/CDrawFrustum.h" #include "nbl/ext/FullScreenTriangle/FullScreenTriangle.h" #include "nbl/ext/ScreenShot/ScreenShot.h" #include "nbl/this_example/builtin/build/spirv/keys.hpp" @@ -1862,6 +1863,7 @@ class App final : public examples::SimpleWindowedApplication nbl::core::smart_refctd_ptr m_scene; nbl::core::smart_refctd_ptr m_sceneRenderpass; nbl::core::smart_refctd_ptr m_renderer; + nbl::core::smart_refctd_ptr m_drawFrustum; std::optional m_gridGeometryIx = std::nullopt; core::smart_refctd_ptr m_spaceEnvPipeline; core::smart_refctd_ptr m_spaceEnvDescriptorSetLayout; From 7e432dbd16cc2ced4744d3386d0287c9a6d03c51 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Wed, 18 Feb 2026 20:52:36 +0100 Subject: [PATCH 104/205] Align continuity script with split frustum views --- 61_UI/app_resources/cameraz_continuity.json | 71 +++++++++++++++------ 1 file changed, 50 insertions(+), 21 deletions(-) diff --git a/61_UI/app_resources/cameraz_continuity.json b/61_UI/app_resources/cameraz_continuity.json index b086cabd3..6c4cb77b8 100644 --- a/61_UI/app_resources/cameraz_continuity.json +++ b/61_UI/app_resources/cameraz_continuity.json @@ -12,6 +12,36 @@ 1979 ], "events": [ + { + "frame": 0, + "type": "action", + "action": "set_use_window", + "value": true + }, + { + "frame": 0, + "type": "action", + "action": "set_active_render_window", + "value": 1 + }, + { + "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_left_handed", + "value": true + }, { "frame": 0, "type": "action", @@ -1117,7 +1147,7 @@ "frame": 58, "type": "imguizmo", "translation": [ - -2.424266164716745E-05, + -2.424266164716745e-05, -0.016290791734313338, 0.06 ], @@ -1730,7 +1760,7 @@ 0.06 ], "rotation_deg": [ - 7.494005416219807E-16, + 7.494005416219807e-16, -2.399999999999997, 0.0 ], @@ -3421,7 +3451,7 @@ 0.06 ], "rotation_deg": [ - -4.440892098500626E-16, + -4.440892098500626e-16, 12.0, 0.0 ], @@ -7955,7 +7985,7 @@ "frame": 418, "type": "imguizmo", "translation": [ - -2.2727495294201E-05, + -2.2727495294201e-05, 0.018966024374945666, -0.020363489667891664 ], @@ -8564,13 +8594,13 @@ "type": "imguizmo", "translation": [ -0.015001031908916553, - -5.490252421763199E-05, + -5.490252421763199e-05, 0.00013720596996502914 ], "rotation_deg": [ - 3.8163916471489756E-17, + 3.8163916471489756e-17, -0.14000000000000004, - -2.636779683484747E-16 + -2.636779683484747e-16 ], "scale": [ 1.0, @@ -10255,13 +10285,13 @@ "type": "imguizmo", "translation": [ 0.037501031908916536, - 5.490252421766148E-05, + 5.490252421766148e-05, 0.00013720596996502697 ], "rotation_deg": [ - -1.249000902703301E-16, + -1.249000902703301e-16, 0.6999999999999998, - -5.169475958410885E-16 + -5.169475958410885e-16 ], "scale": [ 1.0, @@ -11987,7 +12017,7 @@ 0.004390591038880794 ], "rotation_deg": [ - 1.9984014443252818E-15, + 1.9984014443252818e-15, 150.0, 0.0 ], @@ -13678,7 +13708,7 @@ 0.004390591038880599 ], "rotation_deg": [ - -3.3306690738754696E-15, + -3.3306690738754696e-15, 150.0, 0.0 ], @@ -15406,7 +15436,7 @@ -0.0027451262108821595 ], "rotation_deg": [ - 1.9984014443252818E-15, + 1.9984014443252818e-15, 150.0, 0.0 ], @@ -17097,7 +17127,7 @@ 0.0027451262108815974 ], "rotation_deg": [ - -3.3306690738754696E-15, + -3.3306690738754696e-15, 150.0, 0.0 ], @@ -25663,7 +25693,7 @@ 1.64 ], "rotation_deg": [ - 3.3306690738754696E-16, + 3.3306690738754696e-16, 120.0, 0.0 ], @@ -27354,7 +27384,7 @@ 1.64 ], "rotation_deg": [ - -5.551115123125783E-15, + -5.551115123125783e-15, 120.0, 0.0 ], @@ -29082,7 +29112,7 @@ 2.0560004744198026 ], "rotation_deg": [ - 3.3306690738754696E-16, + 3.3306690738754696e-16, 140.0, 0.0 ], @@ -30773,7 +30803,7 @@ 2.0560004744198013 ], "rotation_deg": [ - -5.551115123125783E-15, + -5.551115123125783e-15, 140.0, 0.0 ], @@ -35308,7 +35338,7 @@ "type": "imguizmo", "translation": [ 0.05007030434985655, - -4.800047006135017E-05, + -4.800047006135017e-05, 0.5940000000000001 ], "rotation_deg": [ @@ -49482,5 +49512,4 @@ "max_pos_delta": 1.5 } ] -} - +} \ No newline at end of file From d1cb4e937b2ea7e3e9f105b80b88a6512e42a1a5 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Thu, 19 Feb 2026 21:26:49 +0100 Subject: [PATCH 105/205] Finalize camera smoke and continuity tests --- 61_UI/AppImGuiListen.cpp | 16 +- 61_UI/AppInit.cpp | 297 + 61_UI/AppUpdate.cpp | 19 + 61_UI/AppWorkLoop.cpp | 68 +- 61_UI/CMakeLists.txt | 7 +- 61_UI/README.md | 2 +- 61_UI/app_resources/cameras.json | 2 +- 61_UI/app_resources/cameraz_continuity.json | 64801 +++++++++++------- 61_UI/app_resources/sky_env_fragment.hlsl | 24 +- 61_UI/include/app/App.hpp | 107 +- 10 files changed, 41341 insertions(+), 24002 deletions(-) diff --git a/61_UI/AppImGuiListen.cpp b/61_UI/AppImGuiListen.cpp index 8c3dc6bbe..1d640af31 100644 --- a/61_UI/AppImGuiListen.cpp +++ b/61_UI/AppImGuiListen.cpp @@ -115,12 +115,14 @@ void App::imguiListen() const char* projLabel = projection.getParameters().m_type == IPlanarProjection::CProjection::Perspective ? "Persp" : "Ortho"; const std::string overlayText = "Planar " + std::to_string(binding.activePlanarIx) + " | " + projLabel + " | W" + std::to_string(windowIx); const std::string cameraText = std::string(getCameraTypeLabel(planarViewCameraBound)) + ": " + std::string(getCameraTypeDescription(planarViewCameraBound)); + const std::string frustumText = "Frustum: active camera (hidden in owner view)"; const ImVec2 textSize = ImGui::CalcTextSize(overlayText.c_str()); const ImVec2 descSize = ImGui::CalcTextSize(cameraText.c_str()); + const ImVec2 frustumSize = ImGui::CalcTextSize(frustumText.c_str()); const ImVec2 pad = ImVec2(6.0f, 4.0f); const float lineGap = 2.0f; - const float width = std::max(textSize.x, descSize.x); - const float height = textSize.y + descSize.y + lineGap + pad.y * 2.0f; + const float width = std::max(std::max(textSize.x, descSize.x), frustumSize.x); + const float height = textSize.y + descSize.y + frustumSize.y + lineGap * 2.0f + pad.y * 2.0f; ImVec2 overlayPos = ImVec2(cursorPos.x + contentRegionSize.x - width - pad.x * 2.0f - 6.0f, cursorPos.y + 6.0f); overlayPos.x = std::max(overlayPos.x, cursorPos.x + 6.0f); ImVec2 overlayMax = ImVec2(overlayPos.x + width + pad.x * 2.0f, overlayPos.y + height); @@ -129,12 +131,16 @@ void App::imguiListen() drawList->AddRect(overlayPos, overlayMax, ImGui::ColorConvertFloat4ToU32(ImVec4(0.60f, 0.66f, 0.76f, 0.80f)), 6.0f); drawList->AddText(ImVec2(overlayPos.x + pad.x, overlayPos.y + pad.y), ImGui::ColorConvertFloat4ToU32(ImVec4(0.96f, 0.98f, 1.0f, 1.0f)), overlayText.c_str()); drawList->AddText(ImVec2(overlayPos.x + pad.x, overlayPos.y + pad.y + textSize.y + lineGap), ImGui::ColorConvertFloat4ToU32(ImVec4(0.78f, 0.82f, 0.90f, 1.0f)), cameraText.c_str()); + drawList->AddText(ImVec2(overlayPos.x + pad.x, overlayPos.y + pad.y + textSize.y + descSize.y + lineGap * 2.0f), ImGui::ColorConvertFloat4ToU32(ImVec4(0.96f, 0.90f, 0.36f, 1.0f)), frustumText.c_str()); } - // I will assume we need to focus a window to start manipulating objects from it - if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) + const bool windowHovered = ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem); + const bool windowFocused = ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows); + if (!(m_scriptedInput.enabled && m_scriptedInput.exclusive)) { - if (!(m_scriptedInput.enabled && m_scriptedInput.exclusive)) + if (!m_scriptedInput.enabled && windowHovered) + activeRenderWindowIx = windowIx; + else if (windowFocused) activeRenderWindowIx = windowIx; } diff --git a/61_UI/AppInit.cpp b/61_UI/AppInit.cpp index 36f24be01..7f1d14c61 100644 --- a/61_UI/AppInit.cpp +++ b/61_UI/AppInit.cpp @@ -66,6 +66,14 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) .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 { @@ -77,6 +85,275 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) return false; } + m_headlessCameraSmokeMode = program.get("--headless-camera-smoke"); + if (m_headlessCameraSmokeMode) + { + auto fail = [&](const std::string& msg) -> bool + { + std::cerr << "[headless-camera-smoke][fail] " << msg << std::endl; + m_headlessCameraSmokePassed = false; + return false; + }; + + auto configPath = [&]() -> std::filesystem::path + { + if (program.is_used("--file")) + { + std::filesystem::path path = program.get("--file"); + if (path.is_relative()) + path = localInputCWD / path; + return path.lexically_normal(); + } + return (localInputCWD / "app_resources" / "cameras.json").lexically_normal(); + }(); + + json j; + { + std::ifstream file(configPath); + if (!file.is_open()) + return fail("Cannot open config \"" + configPath.string() + "\"."); + + try + { + file >> j; + } + catch (const std::exception& e) + { + return fail("JSON parse error: " + std::string(e.what())); + } + } + + if (!j.contains("cameras") || !j["cameras"].is_array()) + return fail("Missing \"cameras\" array in config."); + + std::vector> cameras; + cameras.reserve(j["cameras"].size()); + for (const auto& jCamera : j["cameras"]) + { + if (!jCamera.contains("type")) + return fail("Camera entry missing \"type\"."); + if (!jCamera.contains("position")) + return fail("Camera entry missing \"position\"."); + + const auto withOrientation = jCamera.contains("orientation"); + auto position = [&]() + { + auto jret = jCamera["position"].get>(); + return float32_t3(jret[0], jret[1], jret[2]); + }(); + + auto getOrientation = [&]() + { + auto jret = jCamera["orientation"].get>(); + return glm::quat(jret[3], jret[0], jret[1], jret[2]); + }; + + auto getTarget = [&]() + { + auto jret = jCamera["target"].get>(); + return float32_t3(jret[0], jret[1], jret[2]); + }; + + constexpr float DefaultMoveScale = 0.01f; + constexpr float DefaultRotateScale = 0.003f; + constexpr float OrbitMoveScale = 0.5f; + const auto type = jCamera["type"].get(); + + if (type == "FPS") + { + if (!withOrientation) + return fail("FPS camera requires \"orientation\"."); + auto camera = make_smart_refctd_ptr(position, getOrientation()); + camera->setMoveSpeedScale(DefaultMoveScale); + camera->setRotationSpeedScale(DefaultRotateScale); + cameras.emplace_back(std::move(camera)); + } + else if (type == "Free") + { + if (!withOrientation) + return fail("Free camera requires \"orientation\"."); + auto camera = make_smart_refctd_ptr(position, getOrientation()); + camera->setMoveSpeedScale(DefaultMoveScale); + camera->setRotationSpeedScale(DefaultRotateScale); + cameras.emplace_back(std::move(camera)); + } + else if (type == "Orbit") + { + auto camera = make_smart_refctd_ptr(position, getTarget()); + camera->setMoveSpeedScale(OrbitMoveScale); + camera->setRotationSpeedScale(DefaultRotateScale); + cameras.emplace_back(std::move(camera)); + } + else if (type == "Arcball") + { + auto camera = make_smart_refctd_ptr(position, getTarget()); + camera->setMoveSpeedScale(OrbitMoveScale); + camera->setRotationSpeedScale(DefaultRotateScale); + cameras.emplace_back(std::move(camera)); + } + else if (type == "Turntable") + { + auto camera = make_smart_refctd_ptr(position, getTarget()); + camera->setMoveSpeedScale(OrbitMoveScale); + camera->setRotationSpeedScale(DefaultRotateScale); + cameras.emplace_back(std::move(camera)); + } + else if (type == "TopDown") + { + auto camera = make_smart_refctd_ptr(position, getTarget()); + camera->setMoveSpeedScale(OrbitMoveScale); + camera->setRotationSpeedScale(DefaultRotateScale); + cameras.emplace_back(std::move(camera)); + } + else if (type == "Isometric") + { + auto camera = make_smart_refctd_ptr(position, getTarget()); + camera->setMoveSpeedScale(OrbitMoveScale); + camera->setRotationSpeedScale(DefaultRotateScale); + cameras.emplace_back(std::move(camera)); + } + else if (type == "Chase") + { + auto camera = make_smart_refctd_ptr(position, getTarget()); + camera->setMoveSpeedScale(OrbitMoveScale); + camera->setRotationSpeedScale(DefaultRotateScale); + cameras.emplace_back(std::move(camera)); + } + else if (type == "Dolly") + { + auto camera = make_smart_refctd_ptr(position, getTarget()); + camera->setMoveSpeedScale(OrbitMoveScale); + camera->setRotationSpeedScale(DefaultRotateScale); + cameras.emplace_back(std::move(camera)); + } + else if (type == "DollyZoom") + { + float baseFov = 40.0f; + if (jCamera.contains("baseFov")) + baseFov = jCamera["baseFov"].get(); + + auto camera = make_smart_refctd_ptr(position, getTarget(), baseFov); + camera->setMoveSpeedScale(OrbitMoveScale); + camera->setRotationSpeedScale(DefaultRotateScale); + cameras.emplace_back(std::move(camera)); + } + else if (type == "Path") + { + auto camera = make_smart_refctd_ptr(position, getTarget()); + camera->setMoveSpeedScale(OrbitMoveScale); + camera->setRotationSpeedScale(DefaultRotateScale); + cameras.emplace_back(std::move(camera)); + } + else + { + return fail("Unsupported camera type \"" + type + "\"."); + } + } + + if (cameras.empty()) + return fail("No cameras defined."); + + auto angleDiffDeg = [](double a, double b) -> double + { + double d = std::fmod(a - b + 180.0, 360.0); + if (d < 0.0) + d += 360.0; + return std::abs(d - 180.0); + }; + + auto isFinite3 = [](const auto& v) -> bool + { + return std::isfinite(v.x) && std::isfinite(v.y) && std::isfinite(v.z); + }; + + for (const auto& cameraRef : cameras) + { + auto* camera = cameraRef.get(); + if (!camera) + return fail("Null camera instance."); + + camera->updateKeyboardMapping([&](auto& map) { map = camera->getKeyboardMappingPreset(); }); + camera->updateMouseMapping([&](auto& map) { map = camera->getMouseMappingPreset(); }); + camera->updateImguizmoMapping([&](auto& map) { map = camera->getImguizmoMappingPreset(); }); + + const auto& beforeGimbal = camera->getGimbal(); + const auto beforePos = beforeGimbal.getPosition(); + const auto beforeEuler = glm::degrees(glm::eulerAngles(beforeGimbal.getOrientation())); + + const uint32_t allowed = camera->getAllowedVirtualEvents(); + std::vector events; + events.reserve(3u); + + auto pushEvent = [&](const CVirtualGimbalEvent::VirtualEventType type, const double magnitude) -> void + { + CVirtualGimbalEvent ev; + ev.type = type; + ev.magnitude = magnitude; + events.emplace_back(ev); + }; + + if (allowed & CVirtualGimbalEvent::MoveForward) + pushEvent(CVirtualGimbalEvent::MoveForward, 1.0); + else if (allowed & CVirtualGimbalEvent::MoveRight) + pushEvent(CVirtualGimbalEvent::MoveRight, 1.0); + else if (allowed & CVirtualGimbalEvent::MoveUp) + pushEvent(CVirtualGimbalEvent::MoveUp, 1.0); + + if (allowed & CVirtualGimbalEvent::PanRight) + pushEvent(CVirtualGimbalEvent::PanRight, 1.0); + else if (allowed & CVirtualGimbalEvent::TiltUp) + pushEvent(CVirtualGimbalEvent::TiltUp, 1.0); + else if (allowed & CVirtualGimbalEvent::RollRight) + pushEvent(CVirtualGimbalEvent::RollRight, 1.0); + + if (events.empty()) + { + for (const auto event : CVirtualGimbalEvent::VirtualEventsTypeTable) + { + if (allowed & event) + { + pushEvent(event, 1.0); + break; + } + } + } + + if (events.empty()) + return fail("No allowed virtual events for camera \"" + std::string(camera->getIdentifier()) + "\"."); + + const bool manipulated = camera->manipulate({ events.data(), events.size() }); + if (!manipulated) + return fail("Manipulation returned false for camera \"" + std::string(camera->getIdentifier()) + "\"."); + + const auto& afterGimbal = camera->getGimbal(); + const auto afterPos = afterGimbal.getPosition(); + const auto afterEuler = glm::degrees(glm::eulerAngles(afterGimbal.getOrientation())); + + if (!isFinite3(afterPos) || !isFinite3(afterEuler)) + return fail("Non-finite state for camera \"" + std::string(camera->getIdentifier()) + "\"."); + + const auto dPos = afterPos - beforePos; + const double posDelta = std::sqrt(dPos.x * dPos.x + dPos.y * dPos.y + dPos.z * dPos.z); + const double rotDeltaDeg = std::max({ + angleDiffDeg(afterEuler.x, beforeEuler.x), + angleDiffDeg(afterEuler.y, beforeEuler.y), + angleDiffDeg(afterEuler.z, beforeEuler.z) + }); + + if (posDelta <= 1e-9 && rotDeltaDeg <= 1e-9) + return fail("No observable change for camera \"" + std::string(camera->getIdentifier()) + "\"."); + + std::cout << "[headless-camera-smoke][pass] " << camera->getIdentifier() + << " pos_delta=" << posDelta + << " rot_delta_deg=" << rotDeltaDeg + << std::endl; + } + + m_headlessCameraSmokePassed = true; + std::cout << "[headless-camera-smoke] PASS cameras=" << cameras.size() << std::endl; + return true; + } + m_ciMode = program.get("--ci"); if (m_ciMode) { @@ -85,6 +362,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) } m_scriptedInput.log = program.get("--script-log"); m_scriptVisualDebugCli = program.get("--script-visual-debug"); + m_disableScreenshotsCli = program.get("--no-screenshots"); // Create imput system m_inputSystem = make_smart_refctd_ptr(logger_opt_smart_ptr(smart_refctd_ptr(m_logger))); @@ -495,6 +773,11 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) entry.action.kind = ScriptedInputEvent::ActionData::Kind::SetLeftHanded; entry.action.value = ev.value("value", false) ? 1 : 0; } + else if (actionStr == "reset_active_camera") + { + entry.action.kind = ScriptedInputEvent::ActionData::Kind::ResetActiveCamera; + entry.action.value = 1; + } else { m_logger->log("Scripted action event has invalid action \"%s\".", ILogger::ELL_WARNING, actionStr.c_str()); @@ -650,6 +933,11 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) std::sort(m_scriptedInput.captureFrames.begin(), m_scriptedInput.captureFrames.end()); m_scriptedInput.captureFrames.erase(std::unique(m_scriptedInput.captureFrames.begin(), m_scriptedInput.captureFrames.end()), m_scriptedInput.captureFrames.end()); } + if (m_disableScreenshotsCli) + { + m_scriptedInput.captureFrames.clear(); + m_scriptedInput.nextCaptureIndex = 0; + } }; if (program.is_used("--script")) @@ -1034,6 +1322,15 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) else binding.boundProjectionIx = binding.lastBoundPerspectivePresetProjectionIx.value(); } + + m_initialPlanarPresets.clear(); + m_initialPlanarPresets.reserve(m_planarProjections.size()); + for (uint32_t planarIx = 0u; planarIx < m_planarProjections.size(); ++planarIx) + { + auto* camera = m_planarProjections[planarIx]->getCamera(); + const std::string presetName = "Planar " + std::to_string(planarIx); + m_initialPlanarPresets.emplace_back(capturePreset(camera, presetName)); + } } // Create asset manager diff --git a/61_UI/AppUpdate.cpp b/61_UI/AppUpdate.cpp index 08ab106e8..c2513260f 100644 --- a/61_UI/AppUpdate.cpp +++ b/61_UI/AppUpdate.cpp @@ -196,6 +196,25 @@ void App::update() auto& binding = windowBindings[activeRenderWindowIx]; binding.leftHandedProjection = action.value != 0; } break; + + case ScriptedInputEvent::ActionData::Kind::ResetActiveCamera: + { + auto& binding = windowBindings[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_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 (!applyPresetToCamera(camera, m_initialPlanarPresets[binding.activePlanarIx])) + m_logger->log("[script][warn] action reset_active_camera failed for planar: %u", ILogger::ELL_WARNING, binding.activePlanarIx); + } break; } }; diff --git a/61_UI/AppWorkLoop.cpp b/61_UI/AppWorkLoop.cpp index e8d50a420..3524b01b9 100644 --- a/61_UI/AppWorkLoop.cpp +++ b/61_UI/AppWorkLoop.cpp @@ -77,7 +77,7 @@ void App::workLoopBody() const IGPUDescriptorSet* descriptorSets[] = { m_spaceEnvDescriptorSet.get() }; SpaceEnvPushConstants pc = {}; pc.invProj = hlsl::inverse(binding.projectionMatrix); - pc.invViewRot = hlsl::inverse(getMatrix3x4As4x4(binding.viewMatrix)); + pc.invViewRot = hlsl::transpose(getMatrix3x4As4x4(binding.viewMatrix)); pc.invViewRot[0].w = 0.0f; pc.invViewRot[1].w = 0.0f; pc.invViewRot[2].w = 0.0f; @@ -94,25 +94,52 @@ void App::workLoopBody() m_renderer->render(cmdbuf, viewParams); if (m_drawFrustum) { - ext::frustum::CDrawFrustum::DrawParameters drawParams = {}; - drawParams.commandBuffer = cmdbuf; - drawParams.viewProjectionMatrix = binding.viewProjMatrix; - drawParams.lineWidth = 1.2f; + const auto findSourceBindingIxForPlanar = [&](const uint32_t planarIx) -> std::optional + { + if (activeRenderWindowIx < windowBindings.size()) + { + const auto& activeBinding = windowBindings[activeRenderWindowIx]; + if (activeBinding.activePlanarIx == planarIx && activeBinding.boundProjectionIx.has_value()) + return activeRenderWindowIx; + } + + for (uint32_t i = 0u; i < windowBindings.size(); ++i) + { + const auto& candidate = windowBindings[i]; + if (candidate.activePlanarIx != planarIx) + continue; + if (!candidate.boundProjectionIx.has_value()) + continue; + return i; + } + return std::nullopt; + }; + + std::optional sourceBindingIx = std::nullopt; + if (boundPlanarCameraIxToManipulate.has_value()) + sourceBindingIx = findSourceBindingIxForPlanar(boundPlanarCameraIxToManipulate.value()); + if (!sourceBindingIx.has_value() && activeRenderWindowIx < windowBindings.size()) + { + const auto& activeBinding = windowBindings[activeRenderWindowIx]; + if (activeBinding.boundProjectionIx.has_value() && activeBinding.activePlanarIx < m_planarProjections.size()) + sourceBindingIx = activeRenderWindowIx; + } - for (uint32_t frustumIx = 0u; frustumIx < windowBindings.size(); ++frustumIx) + if (sourceBindingIx.has_value()) { - const auto& frustumBinding = windowBindings[frustumIx]; - if (!frustumBinding.boundProjectionIx.has_value()) - continue; - if (frustumBinding.activePlanarIx >= m_planarProjections.size()) - continue; - - const float32_t4 color = (frustumIx == bindingIx) ? - float32_t4(1.0f, 0.95f, 0.25f, 1.0f) : - (frustumBinding.isOrthographicProjection ? - float32_t4(0.30f, 0.90f, 1.00f, 1.0f) : - float32_t4(1.00f, 0.45f, 0.90f, 1.0f)); - willSubmit &= m_drawFrustum->renderSingle(drawParams, hlsl::inverse(frustumBinding.viewProjMatrix), color); + const auto& sourceBinding = windowBindings[sourceBindingIx.value()]; + const bool sameCameraAsView = binding.activePlanarIx == sourceBinding.activePlanarIx; + const bool sameWindow = bindingIx == sourceBindingIx.value(); + if (!sameCameraAsView && !sameWindow) + { + ext::frustum::CDrawFrustum::DrawParameters drawParams = {}; + drawParams.commandBuffer = cmdbuf; + drawParams.viewProjectionMatrix = binding.viewProjMatrix; + drawParams.lineWidth = 1.0f; + + const float32_t4 color = float32_t4(1.0f, 0.95f, 0.25f, 1.0f); + willSubmit &= m_drawFrustum->renderSingle(drawParams, hlsl::inverse(sourceBinding.viewProjMatrix), color); + } } } @@ -355,11 +382,12 @@ void App::workLoopBody() if (m_ciFrameCounter >= CiFramesBeforeCapture) { m_ciScreenshotDone = true; - captureScreenshot(m_ciScreenshotPath, "CI"); + if (!m_disableScreenshotsCli) + captureScreenshot(m_ciScreenshotPath, "CI"); } } - if (m_scriptedInput.enabled && !m_scriptedInput.captureFrames.empty()) + if (!m_disableScreenshotsCli && m_scriptedInput.enabled && !m_scriptedInput.captureFrames.empty()) { while (m_scriptedInput.nextCaptureIndex < m_scriptedInput.captureFrames.size() && m_scriptedInput.captureFrames[m_scriptedInput.nextCaptureIndex] == renderedFrameIx) diff --git a/61_UI/CMakeLists.txt b/61_UI/CMakeLists.txt index 6c37aa8d3..3b9f9b8fc 100644 --- a/61_UI/CMakeLists.txt +++ b/61_UI/CMakeLists.txt @@ -1,4 +1,4 @@ -if(NBL_BUILD_IMGUI AND NBL_BUILD_FRUSTUM) +if(NBL_BUILD_IMGUI AND NBL_EXT_FRUSTUM_LIB) set(NBL_EXTRA_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/AppControlPanel.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/AppImGuiListen.cpp" @@ -96,16 +96,15 @@ if(NBL_BUILD_IMGUI AND NBL_BUILD_FRUSTUM) enable_testing() - set(CAMERA_SMOKE_SCRIPT "${CMAKE_CURRENT_SOURCE_DIR}/app_resources/cameraz_smoke_all.json") set(CAMERA_CONTINUITY_SCRIPT "${CMAKE_CURRENT_SOURCE_DIR}/app_resources/cameraz_continuity.json") add_test(NAME NBL_61_UI_CAMERA_SMOKE - COMMAND "$" --ci --script "${CAMERA_SMOKE_SCRIPT}" --script-log + 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 + COMMAND "$" --ci --script "${CAMERA_CONTINUITY_SCRIPT}" --script-visual-debug --no-screenshots WORKING_DIRECTORY "$" COMMAND_EXPAND_LISTS ) diff --git a/61_UI/README.md b/61_UI/README.md index 53dffefac..518312136 100644 --- a/61_UI/README.md +++ b/61_UI/README.md @@ -112,7 +112,7 @@ Notes: - `NBL_61_UI_CAMERA_SMOKE` - `NBL_61_UI_CAMERA_CONTINUITY` -Run from `build_vs2026/examples_tests`: +Run from `build_vs2026/examples_tests/61_UI`: ```powershell ctest -C Debug --output-on-failure -R NBL_61_UI_CAMERA_ diff --git a/61_UI/app_resources/cameras.json b/61_UI/app_resources/cameras.json index 6f078ec22..123adc946 100644 --- a/61_UI/app_resources/cameras.json +++ b/61_UI/app_resources/cameras.json @@ -13,7 +13,7 @@ { "type": "Free", "position": [0.000, 1.500, -9.000], - "orientation": [0.095, -0.835, 0.152, 0.521] + "orientation": [0.082, 0.000, 0.000, 0.997] }, { "type": "Arcball", diff --git a/61_UI/app_resources/cameraz_continuity.json b/61_UI/app_resources/cameraz_continuity.json index 6c4cb77b8..4922d771c 100644 --- a/61_UI/app_resources/cameraz_continuity.json +++ b/61_UI/app_resources/cameraz_continuity.json @@ -5,11 +5,24 @@ "hard_fail": true, "enableActiveCameraMovement": true, "visual_debug": true, - "visual_debug_target_fps": 60.0, - "visual_debug_hold_seconds": 3.0, + "visual_debug_target_fps": 60, + "visual_debug_hold_seconds": 4, "capture_prefix": "camera_continuity", "capture_frames": [ - 1979 + 239, + 479, + 719, + 959, + 1199, + 1439, + 1679, + 1919, + 2159, + 2220, + 2280, + 2340, + 2399, + 2639 ], "events": [ { @@ -22,13 +35,19 @@ "frame": 0, "type": "action", "action": "set_active_render_window", - "value": 1 + "value": 0 + }, + { + "frame": 0, + "type": "action", + "action": "set_active_planar", + "value": 0 }, { "frame": 0, "type": "action", "action": "set_projection_type", - "value": "orthographic" + "value": "perspective" }, { "frame": 0, @@ -39,14 +58,14 @@ { "frame": 0, "type": "action", - "action": "set_left_handed", - "value": true + "action": "reset_active_camera", + "value": 1 }, { "frame": 0, "type": "action", "action": "set_active_render_window", - "value": 0 + "value": 1 }, { "frame": 0, @@ -58,13664 +77,13761 @@ "frame": 0, "type": "action", "action": "set_projection_type", - "value": "perspective" + "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": [ - 0.04, - 0.0, - 0.06 + 0.008, + 0, + 0.018 ], "rotation_deg": [ 0.0, - 12.0, + 4.739231, 0.0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 2, "type": "imguizmo", "translation": [ - 0.039981162362802226, - 0.0015998235910796123, - 0.06 + 0.008079, + 7.9E-05, + 0.017999 ], "rotation_deg": [ - 0.16939902674809185, - 11.995514848286243, - 0.0 + 0.051459, + 4.791816, + 0.237572 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 3, "type": "imguizmo", "translation": [ - 0.039926179931534925, - 0.003063691054215668, - 0.06 + 0.008158, + 0.000158, + 0.017994 ], "rotation_deg": [ - 0.33858700340309256, - 11.9820649810855, - 0.0 + 0.102794, + 4.844197, + 0.474978 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 4, "type": "imguizmo", "translation": [ - 0.039836507657558584, - 0.00452253423361087, - 0.06 + 0.008237, + 0.000237, + 0.017987 ], "rotation_deg": [ - 0.5073531428140463, - 11.95966715525747, - 0.0 + 0.153881, + 4.89634, + 0.712053 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 5, "type": "imguizmo", "translation": [ - 0.03971214634535241, - 0.005958027265265972, - 0.06 + 0.008316, + 0.000314, + 0.017978 ], "rotation_deg": [ - 0.6754871833866745, - 11.928349275704072, - 0.0 + 0.204597, + 4.948204, + 0.948632 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 6, "type": "imguizmo", "translation": [ - 0.03955325980690609, - 0.007363903233557683, - 0.06 + 0.008395, + 0.000391, + 0.017965 ], "rotation_deg": [ - 0.8427796510431396, - 11.888150360603401, - 0.0 + 0.254821, + 4.999758, + 1.18455 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 7, "type": "imguizmo", "translation": [ - 0.039360045285198875, - 0.008733087364053448, - 0.06 + 0.008473, + 0.000467, + 0.01795 ], "rotation_deg": [ - 1.009022120200661, - 11.839120492797944, - 0.0 + 0.304429, + 5.050962, + 1.419643 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 8, "type": "imguizmo", "translation": [ - 0.03913274355825398, - 0.010058764111973288, - 0.06 + 0.008551, + 0.000542, + 0.017932 ], "rotation_deg": [ - 1.1740074734438395, - 11.781320757397596, - 0.0 + 0.353305, + 5.101783, + 1.653746 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 9, "type": "imguizmo", "translation": [ - 0.03887163781117556, - 0.011334328569507748, - 0.06 + 0.008629, + 0.000615, + 0.017911 ], "rotation_deg": [ - 1.3375301595671623, - 11.714823165675211, - 0.0 + 0.401332, + 5.152184, + 1.886696 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 10, "type": "imguizmo", "translation": [ - 0.038577053349573995, - 0.012553425974927018, - 0.06 + 0.008706, + 0.000686, + 0.017888 ], "rotation_deg": [ - 1.4993864496662117, - 11.639710565349535, - 0.0 + 0.448392, + 5.202131, + 2.118332 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 11, "type": "imguizmo", "translation": [ - 0.03824935718897653, - 0.01370998284338098, - 0.06 + 0.008783, + 0.000756, + 0.017861 ], "rotation_deg": [ - 1.6593746909585103, - 11.556076537367254, - 0.0 + 0.494375, + 5.251589, + 2.348491 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 12, "type": "imguizmo", "translation": [ - 0.03788895759799559, - 0.01479823726695948, - 0.06 + 0.008859, + 0.000823, + 0.017833 ], "rotation_deg": [ - 1.8172955580177759, - 11.464025279312793, - 0.0 + 0.53917, + 5.300523, + 2.577014 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 13, "type": "imguizmo", "translation": [ - 0.03749630358964297, - 0.015812767616846555, - 0.06 + 0.008935, + 0.000888, + 0.017801 ], "rotation_deg": [ - 1.9729523011085845, - 11.363671475591103, - 0.0 + 0.582673, + 5.348899, + 2.80374 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 14, "type": "imguizmo", "translation": [ - 0.037071884361918245, - 0.01674851955387298, - 0.06 + 0.00901, + 0.000951, + 0.017767 ], "rotation_deg": [ - 2.126150991312038, - 11.255140154545167, - 0.0 + 0.62478, + 5.396683, + 3.028513 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 15, "type": "imguizmo", "translation": [ - 0.036616228688328804, - 0.017600831208958112, - 0.06 + 0.009084, + 0.001011, + 0.01773 ], "rotation_deg": [ - 2.2767007621370507, - 11.138566532686259, - 0.0 + 0.665392, + 5.443842, + 3.251175 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 16, "type": "imguizmo", "translation": [ - 0.0361299042591039, - 0.018365456408308924, - 0.06 + 0.009157, + 0.001068, + 0.01769 ], "rotation_deg": [ - 2.424414047316227, - 11.014095846231006, - 0.0 + 0.704413, + 5.490344, + 3.471571 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 17, "type": "imguizmo", "translation": [ - 0.0356135169739233, - 0.019038585827644047, - 0.06 + 0.00923, + 0.001122, + 0.017648 ], "rotation_deg": [ - 2.569106814490077, - 10.881883170155152, - 0.0 + 0.741752, + 5.536156, + 3.689548 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 18, "type": "imguizmo", "translation": [ - 0.03506771018704176, - 0.0196168659700558, - 0.06 + 0.009302, + 0.001173, + 0.017604 ], "rotation_deg": [ - 2.7105987944884165, - 10.74209322498944, - 0.0 + 0.777324, + 5.581245, + 3.904954 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 19, "type": "imguizmo", "translation": [ - 0.03449316390574969, - 0.02009741587296306, - 0.06 + 0.009373, + 0.00122, + 0.017557 ], "rotation_deg": [ - 2.84871370592331, - 10.594900171598347, - 0.0 + 0.811047, + 5.62558, + 4.117638 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 20, "type": "imguizmo", "translation": [ - 0.03389059394316883, - 0.02047784146092194, - 0.06 + 0.009442, + 0.001265, + 0.017507 ], "rotation_deg": [ - 2.983279474813725, - 10.44048739419734, - 0.0 + 0.842846, + 5.669131, + 4.327452 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 21, "type": "imguizmo", "translation": [ - 0.03326075102643814, - 0.02075624747278922, - 0.06 + 0.009511, + 0.001306, + 0.017455 ], "rotation_deg": [ - 3.1141284489682852, - 10.279047271878971, - 0.0 + 0.872645, + 5.711868, + 4.534251 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 22, "type": "imguizmo", "translation": [ - 0.03260441986140135, - 0.02093124690381801, - 0.06 + 0.009579, + 0.001343, + 0.017401 ], "rotation_deg": [ - 3.2410976068590265, - 10.110780938932484, - 0.0 + 0.900382, + 5.75376, + 4.737889 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 23, "type": "imguizmo", "translation": [ - 0.03192241815496113, - 0.02100196791564568, - 0.06 + 0.009646, + 0.001376, + 0.017344 ], "rotation_deg": [ - 3.3640287607259185, - 9.93589803425554, - 0.0 + 0.925994, + 5.794778, + 4.938226 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 24, "type": "imguizmo", "translation": [ - 0.031215595596318157, - 0.020968058179748672, - 0.06 + 0.009712, + 0.001406, + 0.017285 ], "rotation_deg": [ - 3.4827687536591174, - 9.754616440170258, - 0.0 + 0.949426, + 5.834894, + 5.135121 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 25, "type": "imguizmo", "translation": [ - 0.030484832798364173, - 0.020829686632725518, - 0.06 + 0.009776, + 0.001431, + 0.017224 ], "rotation_deg": [ - 3.5971696504134014, - 9.567162010968968, - 0.0 + 0.97063, + 5.874079, + 5.328438 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 26, "type": "imguizmo", "translation": [ - 0.02973104020054793, - 0.02058754263466318, - 0.06 + 0.009839, + 0.001453, + 0.01716 ], "rotation_deg": [ - 3.7070889217170673, - 9.373768291527881, - 0.0 + 0.989561, + 5.912307, + 5.518041 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 27, "type": "imguizmo", "translation": [ - 0.02895515693458109, - 0.020242832534779792, - 0.06 + 0.009901, + 0.001471, + 0.017094 ], "rotation_deg": [ - 3.812389621845653, - 9.174676226339276, - 0.0 + 1.006184, + 5.949551, + 5.703798 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 28, "type": "imguizmo", "translation": [ - 0.028158149654396978, - 0.019797273661453535, - 0.06 + 0.009962, + 0.001484, + 0.017026 ], "rotation_deg": [ - 3.9129405592392583, - 8.970133859324644, - 0.0 + 1.020466, + 5.985784, + 5.88558 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 29, "type": "imguizmo", "translation": [ - 0.027341011331820222, - 0.019253085766579026, - 0.06 + 0.010021, + 0.001494, + 0.016956 ], "rotation_deg": [ - 4.008616459950891, - 8.760396024802862, - 0.0 + 1.03238, + 6.020982, + 6.063261 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 30, "type": "imguizmo", "translation": [ - 0.026504760019447435, - 0.018612979966875037, - 0.06 + 0.010079, + 0.001499, + 0.016884 ], "rotation_deg": [ - 4.099298123722207, - 8.545724029998356, - 0.0 + 1.041912, + 6.055121, + 6.236716 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 31, "type": "imguizmo", "translation": [ - 0.025650437582280528, - 0.017880145237237417, - 0.06 + 0.010135, + 0.0015, + 0.01681 ], "rotation_deg": [ - 4.184872572492186, - 8.326385329484848, - 0.0 + 1.049045, + 6.088174, + 6.405824 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 32, "type": "imguizmo", "translation": [ - 0.02477910839969259, - 0.01705823252342649, - 0.06 + 0.01019, + 0.001497, + 0.016734 ], "rotation_deg": [ - 4.26523319115373, - 8.102653191970255, - 0.0 + 1.053776, + 6.120122, + 6.570468 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 33, "type": "imguizmo", "translation": [ - 0.02389185803934378, - 0.016151336553239016, - 0.06 + 0.010244, + 0.001489, + 0.016656 ], "rotation_deg": [ - 4.340279860382808, - 7.874806359837911, - 0.0 + 1.056104, + 6.15094, + 6.730533 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 34, "type": "imguizmo", "translation": [ - 0.022989791904699155, - 0.015163975436780198, - 0.06 + 0.010295, + 0.001478, + 0.016576 ], "rotation_deg": [ - 4.40991908137467, - 7.643128701868261, - 0.0 + 1.056035, + 6.180607, + 6.885908 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 35, "type": "imguizmo", "translation": [ - 0.022074033857833648, - 0.01410106815746648, - 0.06 + 0.010345, + 0.001462, + 0.016494 ], "rotation_deg": [ - 4.4740640923317105, - 7.407908859573719, - 0.0 + 1.053583, + 6.209104, + 7.036483 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 36, "type": "imguizmo", "translation": [ - 0.021145724819239826, - 0.012967910065897524, - 0.06 + 0.010394, + 0.001443, + 0.016411 ], "rotation_deg": [ - 4.532634976557868, - 7.169439887587275, - 0.0 + 1.048767, + 6.23641, + 7.182155 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 37, "type": "imguizmo", "translation": [ - 0.02020602134638308, - 0.011770146498685968, - 0.06 + 0.010441, + 0.001419, + 0.016325 ], "rotation_deg": [ - 4.585558762024875, - 6.928018888552929, - 0.0 + 1.04161, + 6.262504, + 7.322821 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 38, "type": "imguizmo", "translation": [ - 0.019256094192774978, - 0.010513744653674292, - 0.06 + 0.010486, + 0.001391, + 0.016239 ], "rotation_deg": [ - 4.632769512286319, - 6.683946642972776, - 0.0 + 1.032146, + 6.287372, + 7.458384 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 39, "type": "imguizmo", "translation": [ - 0.018297126849360242, - 0.009204963861655441, - 0.06 + 0.01053, + 0.00136, + 0.01615 ], "rotation_deg": [ - 4.674208408626242, - 6.43752723447197, - 0.0 + 1.020412, + 6.310993, + 7.588749 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 40, "type": "imguizmo", "translation": [ - 0.017330314070034346, - 0.007850324402701557, - 0.06 + 0.010571, + 0.001325, + 0.016061 ], "rotation_deg": [ - 4.709823823339943, - 6.189067670948381, - 0.0 + 1.00645, + 6.333352, + 7.713826 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 41, "type": "imguizmo", "translation": [ - 0.01635686038312898, - 0.006456575022456723, - 0.06 + 0.010611, + 0.001286, + 0.015969 ], "rotation_deg": [ - 4.739571384055658, - 5.938877502078992, - 0.0 + 0.99031, + 6.354434, + 7.833526 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 42, "type": "imguizmo", "translation": [ - 0.015377978590719736, - 0.005030659310225598, - 0.06 + 0.010649, + 0.001243, + 0.015877 ], "rotation_deg": [ - 4.76341402901702, - 5.687268433659546, - 0.0 + 0.972047, + 6.374223, + 7.947767 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 43, "type": "imguizmo", "translation": [ - 0.014394888257625716, - 0.003579681106360786, - 0.06 + 0.010685, + 0.001197, + 0.015783 ], "rotation_deg": [ - 4.781322053257388, - 5.4345539392579365, - 0.0 + 0.951721, + 6.392705, + 8.05647 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 44, "type": "imguizmo", "translation": [ - 0.013408814191983651, - 0.0021108691112875022, - 0.06 + 0.01072, + 0.001148, + 0.015688 ], "rotation_deg": [ - 4.793273145608537, - 5.181048869665178, - 0.0 + 0.929397, + 6.409869, + 8.159557 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 45, "type": "imguizmo", "translation": [ - 0.012420984919289496, - 0.0006315408724817354, - 0.06 + 0.010752, + 0.001095, + 0.015591 ], "rotation_deg": [ - 4.799252416497594, - 4.92706906063053, - 0.0 + 0.905146, + 6.425702, + 8.256958 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 46, "type": "imguizmo", "translation": [ - 0.01143263115180861, - -0.0008509336711832751, - 0.06 + 0.010783, + 0.001039, + 0.015494 ], "rotation_deg": [ - 4.799252416497594, - 4.672930939369478, - 0.0 + 0.879043, + 6.440193, + 8.348605 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 47, "type": "imguizmo", "translation": [ - 0.010444984255261554, - -0.002329168906101713, - 0.06 + 0.010811, + 0.000981, + 0.015396 ], "rotation_deg": [ - 4.793273145608537, - 4.418951130334829, - 0.0 + 0.851169, + 6.453331, + 8.434433 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 48, "type": "imguizmo", "translation": [ - 0.009459274714695744, - -0.003795800338690687, - 0.06 + 0.010838, + 0.00092, + 0.015296 ], "rotation_deg": [ - 4.781322053257388, - 4.165446060742073, - 0.0 + 0.821608, + 6.465109, + 8.514383 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 49, "type": "imguizmo", "translation": [ - 0.008476730601454242, - -0.005243521284926882, - 0.06 + 0.010863, + 0.000856, + 0.015196 ], "rotation_deg": [ - 4.763414029017019, - 3.912731566340462, - 0.0 + 0.790448, + 6.475516, + 8.5884 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 50, "type": "imguizmo", "translation": [ - 0.007498576043151886, - -0.006665119271878699, - 0.06 + 0.010885, + 0.00079, + 0.015095 ], "rotation_deg": [ - 4.739571384055657, - 3.6611224979210153, - 0.0 + 0.757785, + 6.484547, + 8.656431 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 51, "type": "imguizmo", "translation": [ - 0.006526029698564641, - -0.008053511969883697, - 0.06 + 0.010906, + 0.000721, + 0.014993 ], "rotation_deg": [ - 4.709823823339942, - 3.4109323290516267, - 0.0 + 0.723712, + 6.492196, + 8.718429 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 52, "type": "imguizmo", "translation": [ - 0.005560303239332553, - -0.009401782476358683, - 0.06 + 0.010925, + 0.000651, + 0.01489 ], "rotation_deg": [ - 4.674208408626242, - 3.162472765528039, - 0.0 + 0.688332, + 6.498455, + 8.774351 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 53, "type": "imguizmo", "translation": [ - 0.004602599840367727, - -0.010703213775460477, - 0.06 + 0.010941, + 0.000579, + 0.014787 ], "rotation_deg": [ - 4.632769512286319, - 2.916053357027231, - 0.0 + 0.651746, + 6.503322, + 8.824158 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 54, "type": "imguizmo", "translation": [ - 0.003654112680848272, - -0.011951322201920933, - 0.06 + 0.010956, + 0.000505, + 0.014683 ], "rotation_deg": [ - 4.585558762024875, - 2.6719811114470784, - 0.0 + 0.614061, + 6.506792, + 8.867816 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 55, "type": "imguizmo", "translation": [ - 0.00271602345766557, - -0.013139889742341182, - 0.06 + 0.010968, + 0.00043, + 0.014579 ], "rotation_deg": [ - 4.532634976557868, - 2.430560112412731, - 0.0 + 0.575385, + 6.508865, + 8.905293 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 56, "type": "imguizmo", "translation": [ - 0.0017895009131771375, - -0.014262995013021596, - 0.06 + 0.010979, + 0.000353, + 0.014474 ], "rotation_deg": [ - 4.474064092331711, - 2.192091140426291, - 0.0 + 0.535829, + 6.509536, + 8.936564 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 57, "type": "imguizmo", "translation": [ - 0.0008756993790991278, - -0.015315042759997595, - 0.06 + 0.010987, + 0.000276, + 0.014369 ], "rotation_deg": [ - 4.409919081374671, - 1.9568712981317473, - 0.0 + 0.495507, + 6.508808, + 8.961608 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 58, "type": "imguizmo", "translation": [ - -2.424266164716745e-05, - -0.016290791734313338, - 0.06 + 0.010993, + 0.000197, + 0.014264 ], "rotation_deg": [ - 4.340279860382808, - 1.725193640162095, - 0.0 + 0.454531, + 6.506679, + 8.980405 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 59, "type": "imguizmo", "translation": [ - -0.0009092039933433156, - -0.017185380803660433, - 0.06 + 0.010998, + 0.000119, + 0.014158 ], "rotation_deg": [ - 4.26523319115373, - 1.49734680802975, - 0.0 + 0.41302, + 6.503153, + 8.992944 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 60, "type": "imguizmo", "translation": [ - -0.0017780820643689015, - -0.01799435317029459, - 0.06 + 0.011, + 4E-05, + 0.014053 ], "rotation_deg": [ - 4.1848725724921865, - 1.2736146705151596, - 0.0 + 0.371087, + 6.49823, + 8.999216 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 61, "type": "imguizmo", "translation": [ - -0.002629794360843588, - -0.0187136785745778, - 0.06 + 0.011, + -4E-05, + 0.013947 ], "rotation_deg": [ - 4.099298123722207, - 1.0542759700016522, - 0.0 + 0.328853, + 6.491914, + 8.999216 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 62, "type": "imguizmo", "translation": [ - -0.003463279755304539, - -0.019339773373529087, - 0.06 + 0.010998, + -0.000119, + 0.013842 ], "rotation_deg": [ - 4.008616459950891, - 0.8396039751971452, - 0.0 + 0.286432, + 6.484211, + 8.992944 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 63, "type": "imguizmo", "translation": [ - -0.004277499828739157, - -0.019869518394353594, - 0.06 + 0.010993, + -0.000197, + 0.013736 ], "rotation_deg": [ - 3.9129405592392574, - 0.6298661406753615, - 0.0 + 0.243945, + 6.475124, + 8.980405 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 64, "type": "imguizmo", "translation": [ - -0.005071440164325831, - -0.020300274474004554, - 0.06 + 0.010987, + -0.000276, + 0.013631 ], "rotation_deg": [ - 3.812389621845653, - 0.42532377366073215, - 0.0 + 0.201507, + 6.464661, + 8.961608 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 65, "type": "imguizmo", "translation": [ - -0.005844111611271043, - -0.02062989560736085, - 0.06 + 0.010979, + -0.000353, + 0.013526 ], "rotation_deg": [ - 3.7070889217170673, - 0.2262317084721259, - 0.0 + 0.159237, + 6.452828, + 8.936564 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 66, "type": "imguizmo", "translation": [ - -0.006594551517167985, - -0.02085673963851654, - 0.06 + 0.010968, + -0.00043, + 0.013421 ], "rotation_deg": [ - 3.5971696504134014, - 0.03283798903104068, - 0.0 + 0.117249, + 6.439636, + 8.905293 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 67, "type": "imguizmo", "translation": [ - -0.007321824927341658, - -0.020979676441919004, - 0.06 + 0.010956, + -0.000505, + 0.013317 ], "rotation_deg": [ - 3.482768753659117, - -0.1546164401702514, - 0.0 + 0.075659, + 6.42509, + 8.867816 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 68, "type": "imguizmo", "translation": [ - -0.008025025749685942, - -0.020998093552597605, - 0.06 + 0.010941, + -0.000579, + 0.013213 ], "rotation_deg": [ - 3.3640287607259194, - -0.33589803425553255, - 0.0 + 0.034578, + 6.409204, + 8.824158 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 69, "type": "imguizmo", "translation": [ - -0.008703277883541563, - -0.020911899217433343, - 0.06 + 0.010925, + -0.000651, + 0.01311 ], "rotation_deg": [ - 3.2410976068590274, - -0.5107809389324762, - 0.0 + -0.005882, + 6.391987, + 8.774351 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 70, "type": "imguizmo", "translation": [ - -0.009355736311208363, - -0.020721522852268307, - 0.06 + 0.010906, + -0.000721, + 0.013007 ], "rotation_deg": [ - 3.1141284489682857, - -0.6790472718789642, - 0.0 + -0.045612, + 6.373452, + 8.718429 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 71, "type": "imguizmo", "translation": [ - -0.009981588150732176, - -0.020427912902577526, - 0.06 + 0.010885, + -0.00079, + 0.012905 ], "rotation_deg": [ - 2.9832794748137252, - -0.8404873941973343, - 0.0 + -0.084507, + 6.35361, + 8.656431 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 72, "type": "imguizmo", "translation": [ - -0.01058005366865449, - -0.020032532118361326, - 0.06 + 0.010863, + -0.000856, + 0.012804 ], "rotation_deg": [ - 2.848713705923312, - -0.9949001715983393, - 0.0 + -0.122462, + 6.332477, + 8.5884 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 73, "type": "imguizmo", "translation": [ - -0.011150387251463296, - -0.019537350266798426, - 0.06 + 0.010838, + -0.00092, + 0.012704 ], "rotation_deg": [ - 2.7105987944884182, - -1.1420932249894329, - 0.0 + -0.159378, + 6.310067, + 8.514383 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 74, "type": "imguizmo", "translation": [ - -0.01169187833453459, - -0.01894483431896489, - 0.06 + 0.010811, + -0.000981, + 0.012604 ], "rotation_deg": [ - 2.5691068144900777, - -1.2818831701551465, - 0.0 + -0.195157, + 6.286395, + 8.434433 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 75, "type": "imguizmo", "translation": [ - -0.012203852287407481, - -0.018257936159508385, - 0.06 + 0.010783, + -0.001039, + 0.012506 ], "rotation_deg": [ - 2.424414047316228, - -1.4140958462310023, - 0.0 + -0.229707, + 6.261478, + 8.348605 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 76, "type": "imguizmo", "translation": [ - -0.012685671254289698, - -0.017480077880507296, - 0.06 + 0.010752, + -0.001095, + 0.012409 ], "rotation_deg": [ - 2.276700762137053, - -1.5385665326862532, - 0.0 + -0.262938, + 6.235334, + 8.256958 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 77, "type": "imguizmo", "translation": [ - -0.013136734948746533, - -0.01661513473278013, - 0.06 + 0.01072, + -0.001148, + 0.012312 ], "rotation_deg": [ - 2.1261509913120404, - -1.6551401545451607, - 0.0 + -0.294765, + 6.20798, + 8.159557 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 78, "type": "imguizmo", "translation": [ - -0.013556481401582922, - -0.015667415819580914, - 0.06 + 0.010685, + -0.001197, + 0.012217 ], "rotation_deg": [ - 1.972952301108586, - -1.7636714755910972, - 0.0 + -0.325107, + 6.179437, + 8.05647 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 79, "type": "imguizmo", "translation": [ - -0.013944387660987113, - -0.014641642628863492, - 0.06 + 0.010649, + -0.001243, + 0.012123 ], "rotation_deg": [ - 1.8172955580177772, - -1.8640252793127874, - 0.0 + -0.353887, + 6.149722, + 7.947767 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 80, "type": "imguizmo", "translation": [ - -0.01429997044406351, - -0.013542925511066067, - 0.06 + 0.010611, + -0.001286, + 0.012031 ], "rotation_deg": [ - 1.659374690958511, - -1.956076537367249, - 0.0 + -0.381036, + 6.118858, + 7.833526 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 81, "type": "imguizmo", "translation": [ - -0.014622786738942985, - -0.012376738219602352, - 0.06 + 0.010571, + -0.001325, + 0.011939 ], "rotation_deg": [ - 1.4993864496662122, - -2.03971056534953, - 0.0 + -0.406486, + 6.086866, + 7.713826 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 82, "type": "imguizmo", "translation": [ - -0.01491243435672047, - -0.011148890640897417, - 0.06 + 0.01053, + -0.00136, + 0.01185 ], "rotation_deg": [ - 1.3375301595671625, - -2.1148231656752072, - 0.0 + -0.430176, + 6.053768, + 7.588749 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 83, "type": "imguizmo", "translation": [ - -0.015168552432532281, - -0.00986549984982576, - 0.06 + 0.010486, + -0.001391, + 0.011761 ], "rotation_deg": [ - 1.174007473443839, - -2.1813207573975917, - 0.0 + -0.45205, + 6.019586, + 7.458384 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 84, "type": "imguizmo", "translation": [ - -0.015390821875148803, - -0.008532959634752008, - 0.06 + 0.010441, + -0.001419, + 0.011675 ], "rotation_deg": [ - 1.0090221202006624, - -2.23912049279794, - 0.0 + -0.472058, + 5.984346, + 7.322821 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 85, "type": "imguizmo", "translation": [ - -0.015578965764522399, - -0.0071579086439988015, - 0.06 + 0.010394, + -0.001443, + 0.011589 ], "rotation_deg": [ - 0.8427796510431402, - -2.288150360603397, - 0.0 + -0.490155, + 5.948072, + 7.182155 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 86, "type": "imguizmo", "translation": [ - -0.01573274969679533, - -0.005747197312434936, - 0.06 + 0.010345, + -0.001462, + 0.011506 ], "rotation_deg": [ - 0.6754871833866748, - -2.328349275704067, - 0.0 + -0.506301, + 5.910788, + 7.036483 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 87, "type": "imguizmo", "translation": [ - -0.01585198207633777, - -0.004307853732953323, - 0.06 + 0.010295, + -0.001478, + 0.011424 ], "rotation_deg": [ - 0.5073531428140463, - -2.3596671552574664, - 0.0 + -0.520465, + 5.872521, + 6.885908 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 88, "type": "imguizmo", "translation": [ - -0.015936514354452102, - -0.002847048642865782, - 0.06 + 0.010244, + -0.001489, + 0.011344 ], "rotation_deg": [ - 0.3385870034030942, - -2.382064981085496, - 0.0 + -0.532618, + 5.833298, + 6.730533 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 89, "type": "imguizmo", "translation": [ - -0.01598624121444608, - -0.0013720596996503569, - 0.06 + 0.01019, + -0.001497, + 0.011266 ], "rotation_deg": [ - 0.169399026748093, - -2.39551484828624, - 0.0 + -0.54274, + 5.793145, + 6.570468 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 90, "type": "imguizmo", "translation": [ - -0.01600110070284434, - 0.00010976477597202561, - 0.06 + 0.010135, + -0.0015, + 0.01119 ], "rotation_deg": [ - 7.494005416219807e-16, - -2.399999999999997, - 0.0 + -0.550815, + 5.752091, + 6.405824 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 91, "type": "imguizmo", "translation": [ - -0.0159810743065747, - 0.0015910424090018512, - 0.06 + 0.010079, + -0.001499, + 0.011116 ], "rotation_deg": [ - -0.16939902674809154, - -2.39551484828624, - 0.0 + -0.556834, + 5.710165, + 6.236716 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 92, "type": "imguizmo", "translation": [ - -0.015926186976033145, - 0.0030643935487818935, - 0.06 + 0.010021, + -0.001494, + 0.011044 ], "rotation_deg": [ - -0.33858700340309267, - -2.382064981085496, - 0.0 + -0.560794, + 5.667394, + 6.063261 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 93, "type": "imguizmo", "translation": [ - -0.015836507093998746, - 0.004522478034045581, - 0.06 + 0.009962, + -0.001484, + 0.010974 ], "rotation_deg": [ - -0.5073531428140469, - -2.3596671552574664, - 0.0 + -0.562698, + 5.623811, + 5.88558 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 94, "type": "imguizmo", "translation": [ - -0.01571214639043722, - 0.005958031761231207, - 0.06 + 0.009901, + -0.001471, + 0.010906 ], "rotation_deg": [ - -0.6754871833866755, - -2.328349275704067, - 0.0 + -0.562556, + 5.579444, + 5.703798 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 95, "type": "imguizmo", "translation": [ - -0.015553259803299331, - 0.007363902873880442, - 0.06 + 0.009839, + -0.001453, + 0.01084 ], "rotation_deg": [ - -0.8427796510431365, - -2.2881503606033977, - 0.0 + -0.560381, + 5.534325, + 5.518041 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 96, "type": "imguizmo", "translation": [ - -0.015360045285487447, - 0.00873308739282761, - 0.06 + 0.009776, + -0.001431, + 0.010776 ], "rotation_deg": [ - -1.0090221202006586, - -2.239120492797941, - 0.0 + -0.556196, + 5.488486, + 5.328438 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 97, "type": "imguizmo", "translation": [ - -0.01513274355823093, - 0.010058764109671341, - 0.06 + 0.009712, + -0.001406, + 0.010715 ], "rotation_deg": [ - -1.1740074734438375, - -2.1813207573975917, - 0.0 + -0.550028, + 5.441958, + 5.135121 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 98, "type": "imguizmo", "translation": [ - -0.014871637811177441, - 0.011334328569691894, - 0.06 + 0.009646, + -0.001376, + 0.010656 ], "rotation_deg": [ - -1.3375301595671605, - -2.114823165675208, - 0.0 + -0.54191, + 5.394772, + 4.938226 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 99, "type": "imguizmo", "translation": [ - -0.014577053349573875, - 0.012553425974912283, - 0.06 + 0.009579, + -0.001343, + 0.010599 ], "rotation_deg": [ - -1.4993864496662104, - -2.039710565349531, - 0.0 + -0.53188, + 5.346963, + 4.737889 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 100, "type": "imguizmo", "translation": [ - -0.014249357188976578, - 0.01370998284338216, - 0.06 + 0.009511, + -0.001306, + 0.010545 ], "rotation_deg": [ - -1.6593746909585092, - -1.9560765373672486, - 0.0 + -0.519981, + 5.298564, + 4.534251 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 101, "type": "imguizmo", "translation": [ - -0.013888957597995627, - 0.01479823726695939, - 0.06 + 0.009442, + -0.001265, + 0.010493 ], "rotation_deg": [ - -1.8172955580177754, - -1.864025279312788, - 0.0 + -0.506266, + 5.249609, + 4.327452 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 102, "type": "imguizmo", "translation": [ - -0.013496303589642999, - 0.01581276761684657, - 0.06 + 0.009373, + -0.00122, + 0.010443 ], "rotation_deg": [ - -1.9729523011085843, - -1.7636714755910978, - 0.0 + -0.490787, + 5.200131, + 4.117638 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 103, "type": "imguizmo", "translation": [ - -0.013071884361918286, - 0.01674851955387297, - 0.06 + 0.009302, + -0.001173, + 0.010396 ], "rotation_deg": [ - -2.126150991312037, - -1.6551401545451625, - 0.0 + -0.473606, + 5.150165, + 3.904954 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 104, "type": "imguizmo", "translation": [ - -0.012616228688328849, - 0.017600831208958116, - 0.06 + 0.00923, + -0.001122, + 0.010352 ], "rotation_deg": [ - -2.2767007621370494, - -1.538566532686255, - 0.0 + -0.454788, + 5.099746, + 3.689548 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 105, "type": "imguizmo", "translation": [ - -0.012129904259103944, - 0.018365456408308927, - 0.06 + 0.009157, + -0.001068, + 0.01031 ], "rotation_deg": [ - -2.424414047316226, - -1.4140958462310032, - 0.0 + -0.434401, + 5.048909, + 3.471571 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 106, "type": "imguizmo", "translation": [ - -0.011613516973923342, - 0.01903858582764405, - 0.06 + 0.009084, + -0.001011, + 0.01027 ], "rotation_deg": [ - -2.5691068144900764, - -1.2818831701551474, - 0.0 + -0.412522, + 4.997689, + 3.251175 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 107, "type": "imguizmo", "translation": [ - -0.011067710187041791, - 0.019616865970055807, - 0.06 + 0.00901, + -0.000951, + 0.010233 ], "rotation_deg": [ - -2.7105987944884165, - -1.1420932249894329, - 0.0 + -0.389228, + 4.946123, + 3.028513 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 108, "type": "imguizmo", "translation": [ - -0.01049316390574973, - 0.020097415872963067, - 0.06 + 0.008935, + -0.000888, + 0.010199 ], "rotation_deg": [ - -2.8487137059233105, - -0.9949001715983414, - 0.0 + -0.364605, + 4.894246, + 2.80374 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 109, "type": "imguizmo", "translation": [ - -0.009890593943168866, - 0.020477841460921947, - 0.06 + 0.008859, + -0.000823, + 0.010167 ], "rotation_deg": [ - -2.9832794748137257, - -0.8404873941973332, - 0.0 + -0.338736, + 4.842094, + 2.577014 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 110, "type": "imguizmo", "translation": [ - -0.009260751026438174, - 0.020756247472789223, - 0.06 + 0.008783, + -0.000756, + 0.010139 ], "rotation_deg": [ - -3.114128448968286, - -0.6790472718789631, - 0.0 + -0.311715, + 4.789704, + 2.348491 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 111, "type": "imguizmo", "translation": [ - -0.008604419861401402, - 0.020931246903818014, - 0.06 + 0.008706, + -0.000686, + 0.010112 ], "rotation_deg": [ - -3.241097606859024, - -0.5107809389324793, - 0.0 + -0.283632, + 4.737111, + 2.118332 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 112, "type": "imguizmo", "translation": [ - -0.007922418154961182, - 0.021001967915645684, - 0.06 + 0.008629, + -0.000615, + 0.010089 ], "rotation_deg": [ - -3.3640287607259163, - -0.33589803425553566, - 0.0 + -0.254588, + 4.684353, + 1.886696 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 113, "type": "imguizmo", "translation": [ - -0.007215595596318212, - 0.020968058179748676, - 0.06 + 0.008551, + -0.000542, + 0.010068 ], "rotation_deg": [ - -3.482768753659115, - -0.15461644017025347, - 0.0 + -0.224681, + 4.631468, + 1.653746 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 114, "type": "imguizmo", "translation": [ - -0.006484832798364218, - 0.02082968663272552, - 0.06 + 0.008473, + -0.000467, + 0.01005 ], "rotation_deg": [ - -3.5971696504133996, - 0.032837989031039655, - 0.0 + -0.194013, + 4.57849, + 1.419643 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 115, "type": "imguizmo", "translation": [ - -0.005731040200547969, - 0.02058754263466318, - 0.06 + 0.008395, + -0.000391, + 0.010035 ], "rotation_deg": [ - -3.7070889217170673, - 0.22623170847212754, - 0.0 + -0.162689, + 4.525458, + 1.18455 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 116, "type": "imguizmo", "translation": [ - -0.004955156934581126, - 0.02024283253477979, - 0.06 + 0.008316, + -0.000314, + 0.010022 ], "rotation_deg": [ - -3.812389621845653, - 0.4253237736607327, - 0.0 + -0.130815, + 4.472407, + 0.948632 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 117, "type": "imguizmo", "translation": [ - -0.0041581496543970165, - 0.019797273661453525, - 0.06 + 0.008237, + -0.000237, + 0.010013 ], "rotation_deg": [ - -3.9129405592392588, - 0.6298661406753658, - 0.0 + -0.098499, + 4.419377, + 0.712053 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 118, "type": "imguizmo", "translation": [ - -0.0033410113318202523, - 0.019253085766579015, - 0.06 + 0.008158, + -0.000158, + 0.010006 ], "rotation_deg": [ - -4.0086164599508916, - 0.8396039751971496, - 0.0 + -0.065852, + 4.366402, + 0.474978 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 119, "type": "imguizmo", "translation": [ - -0.002504760019447488, - 0.01861297996687505, - 0.06 + 0.008079, + -7.9E-05, + 0.010001 ], "rotation_deg": [ - -4.099298123722205, - 1.0542759700016506, - 0.0 + -0.032981, + 4.31352, + 0.237572 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 120, "type": "imguizmo", "translation": [ - -0.0016504375822805786, - 0.017880145237237424, - 0.06 + 0.008, + 0, + 0.01 ], "rotation_deg": [ - -4.184872572492185, - 1.2736146705151585, + 0.0, + 4.260769, 0.0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 121, "type": "imguizmo", "translation": [ - -0.0007791083996926406, - 0.0170582325234265, - 0.06 + 0.007921, + 7.9E-05, + 0.010001 ], "rotation_deg": [ - -4.265233191153729, - 1.497346808029751, - 0.0 + 0.032981, + 4.208184, + -0.237572 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 122, "type": "imguizmo", "translation": [ - 0.00010814196065617621, - 0.016151336553239016, - 0.06 + 0.007842, + 0.000158, + 0.010006 ], "rotation_deg": [ - -4.340279860382807, - 1.725193640162097, - 0.0 + 0.065852, + 4.155803, + -0.474978 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 123, "type": "imguizmo", "translation": [ - 0.0010102080953008018, - 0.015163975436780202, - 0.06 + 0.007763, + 0.000237, + 0.010013 ], "rotation_deg": [ - -4.409919081374669, - 1.9568712981317473, - 0.0 + 0.098499, + 4.10366, + -0.712053 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 124, "type": "imguizmo", "translation": [ - 0.0019259661421663146, - 0.014101068157466475, - 0.06 + 0.007684, + 0.000314, + 0.010022 ], "rotation_deg": [ - -4.47406409233171, - 2.1920911404262906, - 0.0 + 0.130815, + 4.051796, + -0.948632 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 125, "type": "imguizmo", "translation": [ - 0.0028542751807601355, - 0.01296791006589752, - 0.06 + 0.007605, + 0.000391, + 0.010035 ], "rotation_deg": [ - -4.532634976557867, - 2.430560112412734, - 0.0 + 0.162689, + 4.000242, + -1.18455 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 126, "type": "imguizmo", "translation": [ - 0.003793978653616888, - 0.01177014649868595, - 0.06 + 0.007527, + 0.000467, + 0.01005 ], "rotation_deg": [ - -4.585558762024874, - 2.671981111447081, - 0.0 + 0.194013, + 3.949038, + -1.419643 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 127, "type": "imguizmo", "translation": [ - 0.0047439058072249645, - 0.010513744653674311, - 0.06 + 0.007449, + 0.000542, + 0.010068 ], "rotation_deg": [ - -4.632769512286317, - 2.916053357027228, - 0.0 + 0.224681, + 3.898217, + -1.653746 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 128, "type": "imguizmo", "translation": [ - 0.005702873150639708, - 0.009204963861655448, - 0.06 + 0.007371, + 0.000615, + 0.010089 ], "rotation_deg": [ - -4.6742084086262405, - 3.162472765528036, - 0.0 + 0.254588, + 3.847816, + -1.886696 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 129, "type": "imguizmo", "translation": [ - 0.0066696859299656035, - 0.007850324402701564, - 0.06 + 0.007294, + 0.000686, + 0.010112 ], "rotation_deg": [ - -4.70982382333994, - 3.410932329051625, - 0.0 + 0.283632, + 3.797869, + -2.118332 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 130, "type": "imguizmo", "translation": [ - 0.007643139616870965, - 0.00645657502245673, - 0.06 + 0.007217, + 0.000756, + 0.010139 ], "rotation_deg": [ - -4.7395713840556555, - 3.6611224979210135, - 0.0 + 0.311715, + 3.748411, + -2.348491 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 131, "type": "imguizmo", "translation": [ - 0.008622021409280213, - 0.0050306593102256054, - 0.06 + 0.007141, + 0.000823, + 0.010167 ], "rotation_deg": [ - -4.763414029017018, - 3.9127315663404603, - 0.0 + 0.338736, + 3.699477, + -2.577014 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 132, "type": "imguizmo", "translation": [ - 0.00960511174237424, - 0.0035796811063607834, - 0.06 + 0.007065, + 0.000888, + 0.010199 ], "rotation_deg": [ - -4.781322053257387, - 4.165446060742071, - 0.0 + 0.364605, + 3.651101, + -2.80374 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 133, "type": "imguizmo", "translation": [ - 0.010591185808016304, - 0.0021108691112875005, - 0.06 + 0.00699, + 0.000951, + 0.010233 ], "rotation_deg": [ - -4.793273145608536, - 4.41895113033483, - 0.0 + 0.389228, + 3.603317, + -3.028513 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 134, "type": "imguizmo", "translation": [ - 0.011579015080710466, - 0.0006315408724817239, - 0.06 + 0.006916, + 0.001011, + 0.01027 ], "rotation_deg": [ - -4.799252416497593, - 4.672930939369479, - 0.0 + 0.412522, + 3.556158, + -3.251175 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 135, "type": "imguizmo", "translation": [ - 0.012567368848191327, - -0.0008509336711832456, - 0.06 + 0.006843, + 0.001068, + 0.01031 ], "rotation_deg": [ - -4.799252416497594, - 4.927069060630525, - 0.0 + 0.434401, + 3.509656, + -3.471571 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 136, "type": "imguizmo", "translation": [ - 0.013555015744738384, - -0.002329168906101687, - 0.06 + 0.00677, + 0.001122, + 0.010352 ], "rotation_deg": [ - -4.793273145608536, - 5.181048869665174, - 0.0 + 0.454788, + 3.463844, + -3.689548 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 137, "type": "imguizmo", "translation": [ - 0.014540725285304208, - -0.0037958003386906808, - 0.06 + 0.006698, + 0.001173, + 0.010396 ], "rotation_deg": [ - -4.781322053257387, - 5.434553939257933, - 0.0 + 0.473606, + 3.418755, + -3.904954 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 138, "type": "imguizmo", "translation": [ - 0.015523269398545708, - -0.005243521284926873, - 0.06 + 0.006627, + 0.00122, + 0.010443 ], "rotation_deg": [ - -4.763414029017019, - 5.687268433659543, - 0.0 + 0.490787, + 3.37442, + -4.117638 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 139, "type": "imguizmo", "translation": [ - 0.016501423956848067, - -0.00666511927187869, - 0.06 + 0.006558, + 0.001265, + 0.010493 ], "rotation_deg": [ - -4.739571384055656, - 5.938877502078991, - 0.0 + 0.506266, + 3.330869, + -4.327452 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 140, "type": "imguizmo", "translation": [ - 0.017473970301435313, - -0.00805351196988369, - 0.06 + 0.006489, + 0.001306, + 0.010545 ], "rotation_deg": [ - -4.709823823339941, - 6.189067670948379, - 0.0 + 0.519981, + 3.288132, + -4.534251 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 141, "type": "imguizmo", "translation": [ - 0.018439696760667406, - -0.009401782476358683, - 0.06 + 0.006421, + 0.001343, + 0.010599 ], "rotation_deg": [ - -4.674208408626241, - 6.437527234471968, - 0.0 + 0.53188, + 3.24624, + -4.737889 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 142, "type": "imguizmo", "translation": [ - 0.019397400159632226, - -0.010703213775460477, - 0.06 + 0.006354, + 0.001376, + 0.010656 ], "rotation_deg": [ - -4.632769512286318, - 6.683946642972776, - 0.0 + 0.54191, + 3.205222, + -4.938226 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 143, "type": "imguizmo", "translation": [ - 0.020345887319151656, - -0.011951322201920899, - 0.06 + 0.006288, + 0.001406, + 0.010715 ], "rotation_deg": [ - -4.585558762024875, - 6.928018888552923, - 0.0 + 0.550028, + 3.165106, + -5.135121 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 144, "type": "imguizmo", "translation": [ - 0.021283976542334365, - -0.01313988974234115, - 0.06 + 0.006224, + 0.001431, + 0.010776 ], "rotation_deg": [ - -4.532634976557868, - 7.16943988758727, - 0.0 + 0.556196, + 3.125921, + -5.328438 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 145, "type": "imguizmo", "translation": [ - 0.022210499086822807, - -0.014262995013021584, - 0.06 + 0.006161, + 0.001453, + 0.01084 ], "rotation_deg": [ - -4.4740640923317105, - 7.4079088595737135, - 0.0 + 0.560381, + 3.087693, + -5.518041 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 146, "type": "imguizmo", "translation": [ - 0.023124300620900814, - -0.015315042759997581, - 0.06 + 0.006099, + 0.001471, + 0.010906 ], "rotation_deg": [ - -4.40991908137467, - 7.643128701868257, - 0.0 + 0.562556, + 3.050449, + -5.703798 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 147, "type": "imguizmo", "translation": [ - 0.024024242661647103, - -0.016290791734313317, - 0.06 + 0.006038, + 0.001484, + 0.010974 ], "rotation_deg": [ - -4.340279860382808, - 7.8748063598379066, - 0.0 + 0.562698, + 3.014216, + -5.88558 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 148, "type": "imguizmo", "translation": [ - 0.024909203993343253, - -0.017185380803660416, - 0.06 + 0.005979, + 0.001494, + 0.011044 ], "rotation_deg": [ - -4.26523319115373, - 8.102653191970251, - 0.0 + 0.560794, + 2.979018, + -6.063261 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 149, "type": "imguizmo", "translation": [ - 0.02577808206436885, - -0.017994353170294585, - 0.06 + 0.005921, + 0.001499, + 0.011116 ], "rotation_deg": [ - -4.184872572492186, - 8.326385329484845, - 0.0 + 0.556834, + 2.944879, + -6.236716 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 150, "type": "imguizmo", "translation": [ - 0.026629794360843537, - -0.018713678574577796, - 0.06 + 0.005865, + 0.0015, + 0.01119 ], "rotation_deg": [ - -4.099298123722206, - 8.545724029998354, - 0.0 + 0.550815, + 2.911826, + -6.405824 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 151, "type": "imguizmo", "translation": [ - 0.02746327975530446, - -0.01933977337352907, - 0.06 + 0.00581, + 0.001497, + 0.011266 ], "rotation_deg": [ - -4.008616459950892, - 8.760396024802855, - 0.0 + 0.54274, + 2.879878, + -6.570468 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 152, "type": "imguizmo", "translation": [ - 0.028277499828739082, - -0.01986951839435358, - 0.06 + 0.005756, + 0.001489, + 0.011344 ], "rotation_deg": [ - -3.9129405592392597, - 8.970133859324639, - 0.0 + 0.532618, + 2.84906, + -6.730533 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 153, "type": "imguizmo", "translation": [ - 0.02907144016432577, - -0.02030027447400455, - 0.06 + 0.005705, + 0.001478, + 0.011424 ], "rotation_deg": [ - -3.812389621845654, - 9.17467622633927, - 0.0 + 0.520465, + 2.819393, + -6.885908 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 154, "type": "imguizmo", "translation": [ - 0.029844111611270978, - -0.020629895607360848, - 0.06 + 0.005655, + 0.001462, + 0.011506 ], "rotation_deg": [ - -3.707088921717068, - 9.373768291527877, - 0.0 + 0.506301, + 2.790896, + -7.036483 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 155, "type": "imguizmo", "translation": [ - 0.03059455151716792, - -0.020856739638516537, - 0.06 + 0.005606, + 0.001443, + 0.011589 ], "rotation_deg": [ - -3.5971696504134023, - 9.567162010968962, - 0.0 + 0.490155, + 2.76359, + -7.182155 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 156, "type": "imguizmo", "translation": [ - 0.03132182492734159, - -0.020979676441919004, - 0.06 + 0.005559, + 0.001419, + 0.011675 ], "rotation_deg": [ - -3.482768753659118, - 9.754616440170256, - 0.0 + 0.472058, + 2.737496, + -7.322821 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 157, "type": "imguizmo", "translation": [ - 0.032025025749685895, - -0.020998093552597605, - 0.06 + 0.005514, + 0.001391, + 0.011761 ], "rotation_deg": [ - -3.364028760725919, - 9.935898034255539, - 0.0 + 0.45205, + 2.712628, + -7.458384 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 158, "type": "imguizmo", "translation": [ - 0.032703277883541514, - -0.020911899217433343, - 0.06 + 0.00547, + 0.00136, + 0.01185 ], "rotation_deg": [ - -3.241097606859027, - 10.110780938932482, - 0.0 + 0.430176, + 2.689007, + -7.588749 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 159, "type": "imguizmo", "translation": [ - 0.03335573631120831, - -0.020721522852268307, - 0.06 + 0.005429, + 0.001325, + 0.011939 ], "rotation_deg": [ - -3.1141284489682852, - 10.27904727187897, - 0.0 + 0.406486, + 2.666648, + -7.713826 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 160, "type": "imguizmo", "translation": [ - 0.03398158815073212, - -0.020427912902577526, - 0.06 + 0.005389, + 0.001286, + 0.012031 ], "rotation_deg": [ - -2.983279474813725, - 10.44048739419734, - 0.0 + 0.381036, + 2.645566, + -7.833526 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 161, "type": "imguizmo", "translation": [ - 0.03458005366865445, - -0.020032532118361323, - 0.06 + 0.005351, + 0.001243, + 0.012123 ], "rotation_deg": [ - -2.8487137059233096, - 10.594900171598347, - 0.0 + 0.353887, + 2.625777, + -7.947767 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 162, "type": "imguizmo", "translation": [ - 0.03515038725146326, - -0.01953735026679842, - 0.06 + 0.005315, + 0.001197, + 0.012217 ], "rotation_deg": [ - -2.7105987944884156, - 10.74209322498944, - 0.0 + 0.325107, + 2.607295, + -8.05647 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 163, "type": "imguizmo", "translation": [ - 0.03569187833453455, - -0.018944834318964883, - 0.06 + 0.00528, + 0.001148, + 0.012312 ], "rotation_deg": [ - -2.5691068144900755, - 10.881883170155152, - 0.0 + 0.294765, + 2.590131, + -8.159557 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 164, "type": "imguizmo", "translation": [ - 0.03620385228740743, - -0.018257936159508374, - 0.06 + 0.005248, + 0.001095, + 0.012409 ], "rotation_deg": [ - -2.4244140473162257, - 11.014095846231008, - 0.0 + 0.262938, + 2.574298, + -8.256958 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 165, "type": "imguizmo", "translation": [ - 0.03668567125428966, - -0.017480077880507275, - 0.06 + 0.005217, + 0.001039, + 0.012506 ], "rotation_deg": [ - -2.276700762137049, - 11.13856653268626, - 0.0 + 0.229707, + 2.559807, + -8.348605 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 166, "type": "imguizmo", "translation": [ - 0.0371367349487465, - -0.016615134732780104, - 0.06 + 0.005189, + 0.000981, + 0.012604 ], "rotation_deg": [ - -2.126150991312036, - 11.255140154545169, - 0.0 + 0.195157, + 2.546669, + -8.434433 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 167, "type": "imguizmo", "translation": [ - 0.037556481401582874, - -0.015667415819580914, - 0.06 + 0.005162, + 0.00092, + 0.012704 ], "rotation_deg": [ - -1.9729523011085857, - 11.363671475591103, - 0.0 + 0.159378, + 2.534891, + -8.514383 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 168, "type": "imguizmo", "translation": [ - 0.03794438766098707, - -0.014641642628863494, - 0.06 + 0.005137, + 0.000856, + 0.012804 ], "rotation_deg": [ - -1.8172955580177768, - 11.464025279312793, - 0.0 + 0.122462, + 2.524484, + -8.5884 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 169, "type": "imguizmo", "translation": [ - 0.03829997044406347, - -0.013542925511066069, - 0.06 + 0.005115, + 0.00079, + 0.012905 ], "rotation_deg": [ - -1.6593746909585105, - 11.556076537367254, - 0.0 + 0.084507, + 2.515453, + -8.656431 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 170, "type": "imguizmo", "translation": [ - 0.03862278673894294, - -0.012376738219602354, - 0.06 + 0.005094, + 0.000721, + 0.013007 ], "rotation_deg": [ - -1.4993864496662117, - 11.639710565349535, - 0.0 + 0.045612, + 2.507804, + -8.718429 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 171, "type": "imguizmo", "translation": [ - 0.03891243435672043, - -0.011148890640897418, - 0.06 + 0.005075, + 0.000651, + 0.01311 ], "rotation_deg": [ - -1.337530159567162, - 11.714823165675211, - 0.0 + 0.005882, + 2.501545, + -8.774351 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 172, "type": "imguizmo", "translation": [ - 0.03916855243253224, - -0.009865499849825762, - 0.06 + 0.005059, + 0.000579, + 0.013213 ], "rotation_deg": [ - -1.1740074734438386, - 11.781320757397596, - 0.0 + -0.034578, + 2.496678, + -8.824158 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 173, "type": "imguizmo", "translation": [ - 0.039390821875148765, - -0.00853295963475199, - 0.06 + 0.005044, + 0.000505, + 0.013317 ], "rotation_deg": [ - -1.00902212020066, - 11.839120492797944, - 0.0 + -0.075659, + 2.493208, + -8.867816 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 174, "type": "imguizmo", "translation": [ - 0.03957896576452236, - -0.007157908643998786, - 0.06 + 0.005032, + 0.00043, + 0.013421 ], "rotation_deg": [ - -0.8427796510431377, - 11.888150360603401, - 0.0 + -0.117249, + 2.491135, + -8.905293 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 175, "type": "imguizmo", "translation": [ - 0.03973274969679529, - -0.005747197312434959, - 0.06 + 0.005021, + 0.000353, + 0.013526 ], "rotation_deg": [ - -0.6754871833866767, - 11.928349275704072, - 0.0 + -0.159237, + 2.490464, + -8.936564 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 176, "type": "imguizmo", "translation": [ - 0.039851982076337736, - -0.004307853732953343, - 0.06 + 0.005013, + 0.000276, + 0.013631 ], "rotation_deg": [ - -0.5073531428140481, - 11.95966715525747, - 0.0 + -0.201507, + 2.491192, + -8.961608 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 177, "type": "imguizmo", "translation": [ - 0.039936514354452064, - -0.002847048642865782, - 0.06 + 0.005007, + 0.000197, + 0.013736 ], "rotation_deg": [ - -0.33858700340309394, - 11.9820649810855, - 0.0 + -0.243945, + 2.493321, + -8.980405 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 178, "type": "imguizmo", "translation": [ - 0.03998624121444605, - -0.0013720596996503584, - 0.06 + 0.005002, + 0.000119, + 0.013842 ], "rotation_deg": [ - -0.16939902674809273, - 11.995514848286243, - 0.0 + -0.286432, + 2.496847, + -8.992944 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 179, "type": "imguizmo", "translation": [ - 0.0400011007028443, - 0.00010976477597202409, - 0.06 + 0.005, + 4E-05, + 0.013947 ], "rotation_deg": [ - -4.440892098500626e-16, - 12.0, - 0.0 + -0.328853, + 2.50177, + -8.999216 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 180, - "type": "action", - "action": "set_active_render_window", - "value": 0 - }, - { - "frame": 180, - "type": "action", - "action": "set_active_planar", - "value": 1 - }, - { - "frame": 180, - "type": "action", - "action": "set_projection_type", - "value": "perspective" + "type": "imguizmo", + "translation": [ + 0.005, + -4E-05, + 0.014053 + ], + "rotation_deg": [ + -0.371087, + 2.508086, + -8.999216 + ], + "scale": [ + 1, + 1, + 1 + ] }, { "frame": 181, "type": "imguizmo", "translation": [ - 0.0, - 5.739999999999999, - 0.0 + 0.005002, + -0.000119, + 0.014158 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -0.41302, + 2.515789, + -8.992944 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 182, "type": "imguizmo", "translation": [ - 0.05469471076129016, - 5.739999999999999, - 0.07871132068111694 + 0.005007, + -0.000197, + 0.014264 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -0.454531, + 2.524876, + -8.980405 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 183, "type": "imguizmo", "translation": [ - 0.1049457018628703, - 5.739999999999999, - 0.15073359986741086 + 0.005013, + -0.000276, + 0.014369 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -0.495507, + 2.535339, + -8.961608 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 184, "type": "imguizmo", "translation": [ - 0.1554159898370556, - 5.739999999999999, - 0.22250868429365483 + 0.005021, + -0.000353, + 0.014474 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -0.535829, + 2.547172, + -8.936564 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 185, "type": "imguizmo", "translation": [ - 0.2056646451490081, - 5.739999999999999, - 0.2931349414510858 + 0.005032, + -0.00043, + 0.014579 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -0.575385, + 2.560364, + -8.905293 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 186, "type": "imguizmo", "translation": [ - 0.25565930821863303, - 5.739999999999999, - 0.362304039091038 + 0.005044, + -0.000505, + 0.014683 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -0.614061, + 2.57491, + -8.867816 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 187, "type": "imguizmo", "translation": [ - 0.3053352724022978, - 5.739999999999999, - 0.4296678983114296 + 0.005059, + -0.000579, + 0.014787 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -0.651746, + 2.590796, + -8.824158 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 188, "type": "imguizmo", "translation": [ - 0.3546308411959958, - 5.739999999999999, - 0.4948911943090857 + 0.005075, + -0.000651, + 0.01489 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -0.688332, + 2.608013, + -8.774351 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 189, "type": "imguizmo", "translation": [ - 0.40348458297456774, - 5.739999999999999, - 0.5576489656197812 + 0.005094, + -0.000721, + 0.014993 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -0.723712, + 2.626548, + -8.718429 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 190, "type": "imguizmo", "translation": [ - 0.4518356332980126, - 5.739999999999999, - 0.6176285579664093 + 0.005115, + -0.00079, + 0.015095 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -0.757785, + 2.64639, + -8.656431 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 191, "type": "imguizmo", "translation": [ - 0.4996237526793879, - 5.739999999999999, - 0.6745311558943443 + 0.005137, + -0.000856, + 0.015196 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -0.790448, + 2.667523, + -8.5884 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 192, "type": "imguizmo", "translation": [ - 0.5467894030806382, - 5.739999999999999, - 0.7280732735344067 + 0.005162, + -0.00092, + 0.015296 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -0.821608, + 2.689933, + -8.514383 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 193, "type": "imguizmo", "translation": [ - 0.5932738219739832, - 5.739999999999999, - 0.7779881667488505 + 0.005189, + -0.000981, + 0.015396 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -0.851169, + 2.713605, + -8.434433 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 194, "type": "imguizmo", "translation": [ - 0.6390190955619556, - 5.739999999999999, - 0.8240271620505506 + 0.005217, + -0.001039, + 0.015494 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -0.879043, + 2.738522, + -8.348605 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 195, "type": "imguizmo", "translation": [ - 0.6839682309300437, - 5.739999999999999, - 0.8659608954807391 + 0.005248, + -0.001095, + 0.015591 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -0.905146, + 2.764666, + -8.256958 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 196, "type": "imguizmo", "translation": [ - 0.7280652270528233, - 5.739999999999999, - 0.9035804552887989 + 0.00528, + -0.001148, + 0.015688 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -0.929397, + 2.79202, + -8.159557 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 197, "type": "imguizmo", "translation": [ - 0.7712551445642576, - 5.739999999999999, - 0.9366984227200871 + 0.005315, + -0.001197, + 0.015783 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -0.951721, + 2.820563, + -8.05647 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 198, "type": "imguizmo", "translation": [ - 0.8134841742053067, - 5.739999999999999, - 0.9651498057267452 + 0.005351, + -0.001243, + 0.015877 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -0.972047, + 2.850278, + -7.947767 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 199, "type": "imguizmo", "translation": [ - 0.854699703863564, - 5.739999999999999, - 0.9887928609497825 + 0.005389, + -0.001286, + 0.015969 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -0.99031, + 2.881142, + -7.833526 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 200, "type": "imguizmo", "translation": [ - 0.894850384121396, - 5.739999999999999, - 1.0075097998773594 + 0.005429, + -0.001325, + 0.016061 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -1.00645, + 2.913134, + -7.713826 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 201, "type": "imguizmo", "translation": [ - 0.9338861922309233, - 5.739999999999999, - 1.0212073756612294 + 0.00547, + -0.00136, + 0.01615 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -1.020412, + 2.946232, + -7.588749 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 202, "type": "imguizmo", "translation": [ - 0.9717584944361342, - 5.739999999999999, - 1.029817347667846 + 0.005514, + -0.001391, + 0.016239 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -1.032146, + 2.980414, + -7.458384 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 203, "type": "imguizmo", "translation": [ - 1.00842010656449, - 5.739999999999999, - 1.0332968214497675 + 0.005559, + -0.001419, + 0.016325 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -1.04161, + 3.015654, + -7.322821 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 204, "type": "imguizmo", "translation": [ - 1.0438253528125279, - 5.739999999999999, - 1.0316284624436345 + 0.005606, + -0.001443, + 0.016411 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -1.048767, + 3.051928, + -7.182155 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 205, "type": "imguizmo", "translation": [ - 1.0779301226522244, - 5.739999999999999, - 1.0248205823300953 + 0.005655, + -0.001462, + 0.016494 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -1.053583, + 3.089212, + -7.036483 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 206, "type": "imguizmo", "translation": [ - 1.1106919257872196, - 5.739999999999999, - 1.0129070976254282 + 0.005705, + -0.001478, + 0.016576 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -1.056035, + 3.127479, + -6.885908 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 207, "type": "imguizmo", "translation": [ - 1.142069945090437, - 5.739999999999999, - 0.9959473607111657 + 0.005756, + -0.001489, + 0.016656 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -1.056104, + 3.166702, + -6.730533 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 208, "type": "imguizmo", "translation": [ - 1.17202508745714, - 5.739999999999999, - 0.9740258641435138 + 0.00581, + -0.001497, + 0.016734 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -1.053776, + 3.206855, + -6.570468 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 209, "type": "imguizmo", "translation": [ - 1.200520032510072, - 5.739999999999999, - 0.947251819715688 + 0.005865, + -0.0015, + 0.01681 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -1.049045, + 3.247909, + -6.405824 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 210, "type": "imguizmo", "translation": [ - 1.2275192790960014, - 5.739999999999999, - 0.915758614370252 + 0.005921, + -0.001499, + 0.016884 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -1.041912, + 3.289835, + -6.236716 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 211, "type": "imguizmo", "translation": [ - 1.2529891895157341, - 5.739999999999999, - 0.8797031456720809 + 0.005979, + -0.001494, + 0.016956 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -1.03238, + 3.332606, + -6.063261 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 212, "type": "imguizmo", "translation": [ - 1.2768980314325014, - 5.739999999999999, - 0.8392650401525831 + 0.006038, + -0.001484, + 0.017026 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -1.020466, + 3.376189, + -5.88558 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 213, "type": "imguizmo", "translation": [ - 1.2992160174064986, - 5.739999999999999, - 0.7946457584193596 + 0.006099, + -0.001471, + 0.017094 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -1.006184, + 3.420556, + -5.703798 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 214, "type": "imguizmo", "translation": [ - 1.3199153420063263, - 5.739999999999999, - 0.7460675914895857 + 0.006161, + -0.001453, + 0.01716 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -0.989561, + 3.465675, + -5.518041 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 215, "type": "imguizmo", "translation": [ - 1.3389702164510942, - 5.739999999999999, - 0.6937725533473507 + 0.006224, + -0.001431, + 0.017224 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -0.97063, + 3.511514, + -5.328438 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 216, "type": "imguizmo", "translation": [ - 1.3563569007400338, - 5.739999999999999, - 0.6380211752421582 + 0.006288, + -0.001406, + 0.017285 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -0.949426, + 3.558042, + -5.135121 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 217, "type": "imguizmo", "translation": [ - 1.3720537332295781, - 5.739999999999999, - 0.5790912077353495 + 0.006354, + -0.001376, + 0.017344 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -0.925994, + 3.605228, + -4.938226 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 218, "type": "imguizmo", "translation": [ - 1.3860411576210785, - 5.739999999999999, - 0.5172762369607751 + 0.006421, + -0.001343, + 0.017401 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -0.900382, + 3.653037, + -4.737889 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 219, "type": "imguizmo", "translation": [ - 1.3983017473255113, - 5.739999999999999, - 0.4528842219934477 + 0.006489, + -0.001306, + 0.017455 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -0.872645, + 3.701436, + -4.534251 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 220, "type": "imguizmo", "translation": [ - 1.4088202271748425, - 5.739999999999999, - 0.3862359606129166 + 0.006558, + -0.001265, + 0.017507 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -0.842846, + 3.750391, + -4.327452 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 221, "type": "imguizmo", "translation": [ - 1.4175834924529824, - 5.739999999999999, - 0.3176634911048708 + 0.006627, + -0.00122, + 0.017557 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -0.811047, + 3.799869, + -4.117638 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 222, "type": "imguizmo", "translation": [ - 1.4245806252226312, - 5.739999999999999, - 0.24750843806309936 + 0.006698, + -0.001173, + 0.017604 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -0.777324, + 3.849835, + -3.904954 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 223, "type": "imguizmo", "translation": [ - 1.4298029079276682, - 5.739999999999999, - 0.17612031043295068 + 0.00677, + -0.001122, + 0.017648 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -0.741752, + 3.900254, + -3.689548 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 224, "type": "imguizmo", "translation": [ - 1.4332438342541423, - 5.739999999999999, - 0.10385476027534508 + 0.006843, + -0.001068, + 0.01769 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -0.704413, + 3.951091, + -3.471571 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 225, "type": "imguizmo", "translation": [ - 1.4348991172363286, - 5.739999999999999, - 0.03107181092610138 + 0.006916, + -0.001011, + 0.01773 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -0.665392, + 4.002311, + -3.251175 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 226, "type": "imguizmo", "translation": [ - 1.4347666945977537, - 5.739999999999999, - -0.04186593662221716 + 0.00699, + -0.000951, + 0.017767 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -0.62478, + 4.053877, + -3.028513 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 227, "type": "imguizmo", "translation": [ - 1.4328467313205353, - 5.739999999999999, - -0.11459511018020431 + 0.007065, + -0.000888, + 0.017801 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -0.582673, + 4.105754, + -2.80374 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 228, "type": "imguizmo", "translation": [ - 1.4291416194398359, - 5.739999999999999, - -0.1867533766635818 + 0.007141, + -0.000823, + 0.017833 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -0.53917, + 4.157906, + -2.577014 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 229, "type": "imguizmo", "translation": [ - 1.4236559750636828, - 5.739999999999999, - -0.25798124721840254 + 0.007217, + -0.000756, + 0.017861 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -0.494375, + 4.210296, + -2.348491 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 230, "type": "imguizmo", "translation": [ - 1.4163966326218753, - 5.739999999999999, - -0.32792386817643193 + 0.007294, + -0.000686, + 0.017888 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -0.448392, + 4.262889, + -2.118332 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 231, "type": "imguizmo", "translation": [ - 1.4073726363511332, - 5.739999999999999, - -0.39623278891827785 + 0.007371, + -0.000615, + 0.017911 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -0.401332, + 4.315647, + -1.886696 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 232, "type": "imguizmo", "translation": [ - 1.3965952290271069, - 5.739999999999999, - -0.4625676978368471 + 0.007449, + -0.000542, + 0.017932 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -0.353305, + 4.368532, + -1.653746 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 233, "type": "imguizmo", "translation": [ - 1.3840778379572762, - 5.739999999999999, - -0.5265981177526554 + 0.007527, + -0.000467, + 0.01795 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -0.304429, + 4.42151, + -1.419643 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 234, "type": "imguizmo", "translation": [ - 1.3698360582521987, - 5.739999999999999, - -0.5880050523345098 + 0.007605, + -0.000391, + 0.017965 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -0.254821, + 4.474542, + -1.18455 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 235, "type": "imguizmo", "translation": [ - 1.3538876333959453, - 5.739999999999999, - -0.6464825753231862 + 0.007684, + -0.000314, + 0.017978 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -0.204597, + 4.527593, + -0.948632 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 236, "type": "imguizmo", "translation": [ - 1.336252433139925, - 5.739999999999999, - -0.7017393546406625 + 0.007763, + -0.000237, + 0.017987 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -0.153881, + 4.580623, + -0.712053 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 237, "type": "imguizmo", "translation": [ - 1.3169524287476522, - 5.739999999999999, - -0.7535001037918817 + 0.007842, + -0.000158, + 0.017994 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -0.102794, + 4.633598, + -0.474978 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 238, "type": "imguizmo", "translation": [ - 1.2960116656212863, - 5.739999999999999, - -0.8015069533282162 + 0.007921, + -7.9E-05, + 0.017999 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -0.051459, + 4.68648, + -0.237572 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 239, "type": "imguizmo", "translation": [ - 1.2734562333440567, - 5.739999999999999, - -0.8455207355400934 + 0.008, + 0, + 0.018 ], "rotation_deg": [ - 0.0, - 0.0, + -0.0, + 4.739231, 0.0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 240, - "type": "imguizmo", - "translation": [ - 1.2493142331758895, - 5.739999999999999, - -0.8853221759784938 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1.0, - 1.0, - 1.0 - ] + "type": "action", + "action": "set_active_render_window", + "value": 0 + }, + { + "frame": 240, + "type": "action", + "action": "set_active_planar", + "value": 1 + }, + { + "frame": 240, + "type": "action", + "action": "set_projection_type", + "value": "perspective" + }, + { + "frame": 240, + "type": "action", + "action": "set_left_handed", + "value": true + }, + { + "frame": 240, + "type": "action", + "action": "reset_active_camera", + "value": 1 + }, + { + "frame": 240, + "type": "action", + "action": "set_active_render_window", + "value": 1 + }, + { + "frame": 240, + "type": "action", + "action": "set_active_planar", + "value": 1 + }, + { + "frame": 240, + "type": "action", + "action": "set_projection_type", + "value": "orthographic" + }, + { + "frame": 240, + "type": "action", + "action": "set_left_handed", + "value": true + }, + { + "frame": 240, + "type": "action", + "action": "set_active_render_window", + "value": 0 }, { "frame": 241, "type": "imguizmo", "translation": [ - 1.223615743042736, - 5.739999999999999, - -0.9207129858692282 + 0, + 3.6, + 2 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 242, "type": "imguizmo", "translation": [ - 1.1963927800632244, - 5.739999999999999, - -0.9515168499776314 + 0.08447, + 3.598746, + 2.031665 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 243, "type": "imguizmo", "translation": [ - 1.1676792606593167, - 5.739999999999999, - -0.9775803050021973 + 0.168881, + 3.594983, + 2.063242 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 244, "type": "imguizmo", "translation": [ - 1.1375109583006695, - 5.739999999999999, - -0.9987735041210246 + 0.253175, + 3.588715, + 2.094643 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 245, "type": "imguizmo", "translation": [ - 1.105925458935344, - 5.739999999999999, - -1.0149908638821543 + 0.337292, + 3.579946, + 2.12578 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 246, "type": "imguizmo", "translation": [ - 1.072962114162399, - 5.739999999999999, - -1.0261515902150142 + 0.421173, + 3.568682, + 2.156566 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 247, "type": "imguizmo", "translation": [ - 1.038661992204695, - 5.739999999999999, - -1.0322000809424152 + 0.504762, + 3.554932, + 2.186916 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 248, "type": "imguizmo", "translation": [ - 1.003067826743005, - 5.739999999999999, - -1.0331062027878024 + 0.587998, + 3.538703, + 2.216745 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 249, "type": "imguizmo", "translation": [ - 0.9662239636751674, - 5.739999999999999, - -1.028865441497721 + 0.670825, + 3.520009, + 2.24597 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 250, "type": "imguizmo", "translation": [ - 0.9281763058666213, - 5.739999999999999, - -1.019498924331601 + 0.753185, + 3.498861, + 2.274509 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 251, "type": "imguizmo", "translation": [ - 0.8889722559611514, - 5.739999999999999, - -1.0050533148068148 + 0.835019, + 3.475275, + 2.302283 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 252, "type": "imguizmo", "translation": [ - 0.8486606573230967, - 5.739999999999999, - -0.9856005802233779 + 0.916272, + 3.449267, + 2.329215 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 253, "type": "imguizmo", "translation": [ - 0.8072917331845997, - 5.739999999999999, - -0.9612376331264829 + 0.996885, + 3.420855, + 2.355229 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 254, "type": "imguizmo", "translation": [ - 0.7649170240737152, - 5.739999999999999, - -0.9320858484930732 + 1.076805, + 3.390059, + 2.380253 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 255, "type": "imguizmo", "translation": [ - 0.7215893236013293, - 5.739999999999999, - -0.8982904590478129 + 1.155973, + 3.3569, + 2.404217 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 256, "type": "imguizmo", "translation": [ - 0.6773626126868942, - 5.739999999999999, - -0.8600198317209593 + 1.234336, + 3.321402, + 2.427055 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 257, "type": "imguizmo", "translation": [ - 0.6322919923049231, - 5.739999999999999, - -0.8174646288527828 + 1.311839, + 3.283589, + 2.448702 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 258, "type": "imguizmo", "translation": [ - 0.5864336148360403, - 5.739999999999999, - -0.7708368583233813 + 1.388428, + 3.243488, + 2.469099 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 259, "type": "imguizmo", "translation": [ - 0.5398446141081061, - 5.739999999999999, - -0.7203688173400843 + 1.464049, + 3.201126, + 2.488188 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 260, "type": "imguizmo", "translation": [ - 0.4925830342145803, - 5.739999999999999, - -0.6663119351444511 + 1.53865, + 3.156534, + 2.505917 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 261, "type": "imguizmo", "translation": [ - 0.44470775719881134, - 5.739999999999999, - -0.6089355204044361 + 1.612178, + 3.109741, + 2.522235 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 262, "type": "imguizmo", "translation": [ - 0.39627842969434224, - 5.739999999999999, - -0.5485254195321535 + 1.684583, + 3.060782, + 2.537098 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 263, "type": "imguizmo", "translation": [ - 0.34735538861263177, - 5.739999999999999, - -0.485382592611428 + 1.755814, + 3.009689, + 2.550464 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 264, "type": "imguizmo", "translation": [ - 0.2979995859707779, - 5.739999999999999, - -0.4198216140297991 + 1.825821, + 2.956499, + 2.562296 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 265, "type": "imguizmo", "translation": [ - 0.24827251295289124, - 5.739999999999999, - -0.35216910528474155 + 1.894556, + 2.901248, + 2.57256 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 266, "type": "imguizmo", "translation": [ - 0.19823612329974089, - 5.739999999999999, - -0.2827621077717993 + 1.96197, + 2.843975, + 2.581229 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 267, "type": "imguizmo", "translation": [ - 0.1479527561221055, - 5.739999999999999, - -0.21194640366130396 + 2.028017, + 2.784721, + 2.588277 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 268, "type": "imguizmo", "translation": [ - 0.09748505823400519, - 5.739999999999999, - -0.14007479322899688 + 2.092651, + 2.723525, + 2.593686 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 269, "type": "imguizmo", "translation": [ - 0.04689590610256971, - 5.739999999999999, - -0.06750533722279796 + 2.155826, + 2.660432, + 2.597441 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 270, "type": "imguizmo", "translation": [ - -0.0037516724882057442, - 5.739999999999999, - 0.005400426977823267 + 2.217499, + 2.595485, + 2.59953 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 271, "type": "imguizmo", "translation": [ - -0.054394576962234, - 5.739999999999999, - 0.07827928652289069 + 2.277626, + 2.528728, + 2.599948 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 272, "type": "imguizmo", "translation": [ - -0.10496971256679523, - 5.739999999999999, - 0.1507681626000688 + 2.336167, + 2.46021, + 2.598694 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 273, "type": "imguizmo", "translation": [ - -0.15541406898074217, - 5.739999999999999, - 0.2225059192750422 + 2.393079, + 2.389976, + 2.595771 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 274, "type": "imguizmo", "translation": [ - -0.2056647988175139, - 5.739999999999999, - 0.29313516265257505 + 2.448323, + 2.318078, + 2.591188 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 275, "type": "imguizmo", "translation": [ - -0.255659295925152, - 5.739999999999999, - 0.3623040213949174 + 2.501861, + 2.244563, + 2.584957 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 276, "type": "imguizmo", "translation": [ - -0.30533527338577593, - 5.739999999999999, - 0.42966789972711805 + 2.553655, + 2.169485, + 2.577095 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 277, "type": "imguizmo", "translation": [ - -0.35463084111731735, - 5.739999999999999, - 0.4948911941958297 + 2.60367, + 2.092894, + 2.567626 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 278, "type": "imguizmo", "translation": [ - -0.40348458298086204, - 5.739999999999999, - 0.5576489656288408 + 2.65187, + 2.014845, + 2.556574 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 279, "type": "imguizmo", "translation": [ - -0.45183563329750914, - 5.739999999999999, - 0.617628557965684 + 2.698222, + 1.935392, + 2.54397 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 280, "type": "imguizmo", "translation": [ - -0.4996237526794284, - 5.739999999999999, - 0.674531155894402 + 2.742694, + 1.85459, + 2.529851 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 281, "type": "imguizmo", "translation": [ - -0.5467894030806353, - 5.739999999999999, - 0.7280732735344018 + 2.785254, + 1.772495, + 2.514255 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 282, "type": "imguizmo", "translation": [ - -0.5932738219739838, - 5.739999999999999, - 0.7779881667488509 + 2.825873, + 1.689165, + 2.497226 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 283, "type": "imguizmo", "translation": [ - -0.6390190955619557, - 5.739999999999999, - 0.8240271620505499 + 2.864523, + 1.604658, + 2.47881 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 284, "type": "imguizmo", "translation": [ - -0.6839682309300439, - 5.739999999999999, - 0.865960895480739 + 2.901176, + 1.519033, + 2.459061 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 285, "type": "imguizmo", "translation": [ - -0.7280652270528234, - 5.739999999999999, - 0.9035804552887987 + 2.935807, + 1.432349, + 2.438031 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 286, "type": "imguizmo", "translation": [ - -0.7712551445642579, - 5.739999999999999, - 0.9366984227200869 + 2.968393, + 1.344666, + 2.415781 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 287, "type": "imguizmo", "translation": [ - -0.8134841742053073, - 5.739999999999999, - 0.9651498057267454 + 2.99891, + 1.256047, + 2.392372 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 288, "type": "imguizmo", "translation": [ - -0.8546997038635649, - 5.739999999999999, - 0.9887928609497827 + 3.027336, + 1.166552, + 2.367869 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 289, "type": "imguizmo", "translation": [ - -0.8948503841213968, - 5.739999999999999, - 1.0075097998773594 + 3.053653, + 1.076245, + 2.342341 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 290, "type": "imguizmo", "translation": [ - -0.9338861922309241, - 5.739999999999999, - 1.0212073756612292 + 3.077842, + 0.985187, + 2.315859 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 291, "type": "imguizmo", "translation": [ - -0.9717584944361338, - 5.739999999999999, - 1.0298173476678458 + 3.099886, + 0.893442, + 2.288497 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 292, "type": "imguizmo", "translation": [ - -1.0084201065644902, - 5.739999999999999, - 1.0332968214497673 + 3.119769, + 0.801075, + 2.26033 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 293, "type": "imguizmo", "translation": [ - -1.043825352812528, - 5.739999999999999, - 1.0316284624436345 + 3.137479, + 0.70815, + 2.231438 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 294, "type": "imguizmo", "translation": [ - -1.0779301226522249, - 5.739999999999999, - 1.0248205823300953 + 3.153001, + 0.614731, + 2.201901 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 295, "type": "imguizmo", "translation": [ - -1.1106919257872208, - 5.739999999999999, - 1.012907097625428 + 3.166327, + 0.520884, + 2.171801 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 296, "type": "imguizmo", "translation": [ - -1.1420699450904381, - 5.739999999999999, - 0.9959473607111654 + 3.177445, + 0.426674, + 2.141222 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 297, "type": "imguizmo", "translation": [ - -1.1720250874571414, - 5.739999999999999, - 0.9740258641435133 + 3.186349, + 0.332166, + 2.11025 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 298, "type": "imguizmo", "translation": [ - -1.2005200325100736, - 5.739999999999999, - 0.9472518197156874 + 3.193033, + 0.237427, + 2.07897 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 299, "type": "imguizmo", "translation": [ - -1.2275192790960017, - 5.739999999999999, - 0.9157586143702521 + 3.197491, + 0.142522, + 2.04747 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 300, "type": "imguizmo", "translation": [ - -1.2529891895157348, - 5.739999999999999, - 0.879703145672081 + 3.199721, + 0.047519, + 2.015838 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 301, "type": "imguizmo", "translation": [ - -1.276898031432502, - 5.739999999999999, - 0.8392650401525834 + 3.199721, + -0.047519, + 1.984162 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 302, "type": "imguizmo", "translation": [ - -1.2992160174064993, - 5.739999999999999, - 0.7946457584193591 + 3.197491, + -0.142522, + 1.95253 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 303, "type": "imguizmo", "translation": [ - -1.319915342006327, - 5.739999999999999, - 0.7460675914895857 + 3.193033, + -0.237427, + 1.92103 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 304, "type": "imguizmo", "translation": [ - -1.338970216451095, - 5.739999999999999, - 0.6937725533473503 + 3.186349, + -0.332166, + 1.88975 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 305, "type": "imguizmo", "translation": [ - -1.3563569007400345, - 5.739999999999999, - 0.6380211752421577 + 3.177445, + -0.426674, + 1.858778 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 306, "type": "imguizmo", "translation": [ - -1.3720537332295792, - 5.739999999999999, - 0.5790912077353486 + 3.166327, + -0.520884, + 1.828199 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 307, "type": "imguizmo", "translation": [ - -1.386041157621079, - 5.739999999999999, - 0.517276236960776 + 3.153001, + -0.614731, + 1.798099 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 308, "type": "imguizmo", "translation": [ - -1.398301747325512, - 5.739999999999999, - 0.45288422199344786 + 3.137479, + -0.70815, + 1.768562 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 309, "type": "imguizmo", "translation": [ - -1.4088202271748431, - 5.739999999999999, - 0.38623596061291676 + 3.119769, + -0.801075, + 1.73967 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 310, "type": "imguizmo", "translation": [ - -1.417583492452983, - 5.739999999999999, - 0.31766349110487097 + 3.099886, + -0.893442, + 1.711503 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 311, "type": "imguizmo", "translation": [ - -1.4245806252226318, - 5.739999999999999, - 0.24750843806309958 + 3.077842, + -0.985187, + 1.684141 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 312, "type": "imguizmo", "translation": [ - -1.4298029079276688, - 5.739999999999999, - 0.1761203104329504 + 3.053653, + -1.076245, + 1.657659 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 313, "type": "imguizmo", "translation": [ - -1.433243834254143, - 5.739999999999999, - 0.10385476027534485 + 3.027336, + -1.166552, + 1.632131 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 314, "type": "imguizmo", "translation": [ - -1.4348991172363292, - 5.739999999999999, - 0.03107181092610066 + 2.99891, + -1.256047, + 1.607628 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 315, "type": "imguizmo", "translation": [ - -1.4347666945977546, - 5.739999999999999, - -0.041865936622215855 + 2.968393, + -1.344666, + 1.584219 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 316, "type": "imguizmo", "translation": [ - -1.432846731320536, - 5.739999999999999, - -0.11459511018020317 + 2.935807, + -1.432349, + 1.561969 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 317, "type": "imguizmo", "translation": [ - -1.4291416194398365, - 5.739999999999999, - -0.18675337666358166 + 2.901176, + -1.519033, + 1.540939 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 318, "type": "imguizmo", "translation": [ - -1.4236559750636835, - 5.739999999999999, - -0.25798124721840227 + 2.864523, + -1.604658, + 1.52119 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 319, "type": "imguizmo", "translation": [ - -1.416396632621876, - 5.739999999999999, - -0.3279238681764317 + 2.825873, + -1.689165, + 1.502774 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 320, "type": "imguizmo", "translation": [ - -1.4073726363511339, - 5.739999999999999, - -0.3962327889182776 + 2.785254, + -1.772495, + 1.485745 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 321, "type": "imguizmo", "translation": [ - -1.3965952290271075, - 5.739999999999999, - -0.46256769783684737 + 2.742694, + -1.85459, + 1.470149 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 322, "type": "imguizmo", "translation": [ - -1.384077837957277, - 5.739999999999999, - -0.5265981177526557 + 2.698222, + -1.935392, + 1.45603 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 323, "type": "imguizmo", "translation": [ - -1.3698360582521996, - 5.739999999999999, - -0.5880050523345084 + 2.65187, + -2.014845, + 1.443426 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 324, "type": "imguizmo", "translation": [ - -1.353887633395946, - 5.739999999999999, - -0.6464825753231849 + 2.60367, + -2.092894, + 1.432374 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 325, "type": "imguizmo", "translation": [ - -1.3362524331399257, - 5.739999999999999, - -0.7017393546406622 + 2.553655, + -2.169485, + 1.422905 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 326, "type": "imguizmo", "translation": [ - -1.3169524287476526, - 5.739999999999999, - -0.7535001037918813 + 2.501861, + -2.244563, + 1.415043 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 327, "type": "imguizmo", "translation": [ - -1.2960116656212868, - 5.739999999999999, - -0.8015069533282154 + 2.448323, + -2.318078, + 1.408812 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 328, "type": "imguizmo", "translation": [ - -1.2734562333440576, - 5.739999999999999, - -0.8455207355400927 + 2.393079, + -2.389976, + 1.404229 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 329, "type": "imguizmo", "translation": [ - -1.2493142331758897, - 5.739999999999999, - -0.8853221759784939 + 2.336167, + -2.46021, + 1.401306 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 330, "type": "imguizmo", "translation": [ - -1.2236157430427361, - 5.739999999999999, - -0.920712985869228 + 2.277626, + -2.528728, + 1.400052 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 331, "type": "imguizmo", "translation": [ - -1.1963927800632257, - 5.739999999999999, - -0.9515168499776305 + 2.217499, + -2.595485, + 1.40047 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 332, "type": "imguizmo", "translation": [ - -1.167679260659318, - 5.739999999999999, - -0.9775803050021965 + 2.155826, + -2.660432, + 1.402559 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 333, "type": "imguizmo", "translation": [ - -1.1375109583006704, - 5.739999999999999, - -0.9987735041210243 + 2.092651, + -2.723525, + 1.406314 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 334, "type": "imguizmo", "translation": [ - -1.1059254589353449, - 5.739999999999999, - -1.014990863882154 + 2.028017, + -2.784721, + 1.411723 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 335, "type": "imguizmo", "translation": [ - -1.0729621141624, - 5.739999999999999, - -1.0261515902150142 + 1.96197, + -2.843975, + 1.418771 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 336, "type": "imguizmo", "translation": [ - -1.0386619922046958, - 5.739999999999999, - -1.0322000809424152 + 1.894556, + -2.901248, + 1.42744 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 337, "type": "imguizmo", "translation": [ - -1.0030678267430058, - 5.739999999999999, - -1.0331062027878024 + 1.825821, + -2.956499, + 1.437704 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 338, "type": "imguizmo", "translation": [ - -0.9662239636751683, - 5.739999999999999, - -1.028865441497721 + 1.755814, + -3.009689, + 1.449536 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 339, "type": "imguizmo", "translation": [ - -0.928176305866622, - 5.739999999999999, - -1.019498924331601 + 1.684583, + -3.060782, + 1.462902 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 340, "type": "imguizmo", "translation": [ - -0.8889722559611519, - 5.739999999999999, - -1.0050533148068148 + 1.612178, + -3.109741, + 1.477765 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 341, "type": "imguizmo", "translation": [ - -0.8486606573230968, - 5.739999999999999, - -0.9856005802233777 + 1.53865, + -3.156534, + 1.494083 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 342, "type": "imguizmo", "translation": [ - -0.8072917331845998, - 5.739999999999999, - -0.9612376331264826 + 1.464049, + -3.201126, + 1.511812 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 343, "type": "imguizmo", "translation": [ - -0.7649170240737154, - 5.739999999999999, - -0.9320858484930729 + 1.388428, + -3.243488, + 1.530901 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 344, "type": "imguizmo", "translation": [ - -0.7215893236013295, - 5.739999999999999, - -0.8982904590478125 + 1.311839, + -3.283589, + 1.551298 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 345, "type": "imguizmo", "translation": [ - -0.6773626126868937, - 5.739999999999999, - -0.8600198317209585 + 1.234336, + -3.321402, + 1.572945 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 346, "type": "imguizmo", "translation": [ - -0.6322919923049226, - 5.739999999999999, - -0.8174646288527819 + 1.155973, + -3.3569, + 1.595783 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 347, "type": "imguizmo", "translation": [ - -0.5864336148360412, - 5.739999999999999, - -0.7708368583233816 + 1.076805, + -3.390059, + 1.619747 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 348, "type": "imguizmo", "translation": [ - -0.5398446141081069, - 5.739999999999999, - -0.7203688173400845 + 0.996885, + -3.420855, + 1.644771 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 349, "type": "imguizmo", "translation": [ - -0.49258303421458105, - 5.739999999999999, - -0.6663119351444512 + 0.916272, + -3.449267, + 1.670785 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 350, "type": "imguizmo", "translation": [ - -0.4447077571988121, - 5.739999999999999, - -0.6089355204044363 + 0.835019, + -3.475275, + 1.697717 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 351, "type": "imguizmo", "translation": [ - -0.3962784296943429, - 5.739999999999999, - -0.5485254195321534 + 0.753185, + -3.498861, + 1.725491 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 352, "type": "imguizmo", "translation": [ - -0.3473553886126325, - 5.739999999999999, - -0.485382592611428 + 0.670825, + -3.520009, + 1.75403 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 353, "type": "imguizmo", "translation": [ - -0.297999585970778, - 5.739999999999999, - -0.41982161402979834 + 0.587998, + -3.538703, + 1.783255 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 354, "type": "imguizmo", "translation": [ - -0.2482725129528914, - 5.739999999999999, - -0.35216910528474077 + 0.504762, + -3.554932, + 1.813084 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 355, "type": "imguizmo", "translation": [ - -0.19823612329974252, - 5.739999999999999, - -0.28276210777180033 + 0.421173, + -3.568682, + 1.843434 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 356, "type": "imguizmo", "translation": [ - -0.14795275612210698, - 5.739999999999999, - -0.21194640366130502 + 0.337292, + -3.579946, + 1.87422 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 357, "type": "imguizmo", "translation": [ - -0.097485058234006, - 5.739999999999999, - -0.1400747932289969 + 0.253175, + -3.588715, + 1.905357 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 358, "type": "imguizmo", "translation": [ - -0.04689590610257057, - 5.739999999999999, - -0.0675053372227981 + 0.168881, + -3.594983, + 1.936758 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 359, "type": "imguizmo", "translation": [ - 0.003751672488204884, - 5.739999999999999, - 0.005400426977823114 + 0.08447, + -3.598746, + 1.968335 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 360, - "type": "action", - "action": "set_active_render_window", - "value": 0 - }, - { - "frame": 360, - "type": "action", - "action": "set_active_planar", - "value": 2 - }, - { - "frame": 360, - "type": "action", - "action": "set_projection_type", - "value": "perspective" + "type": "imguizmo", + "translation": [ + 0, + -3.6, + 2 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] }, { "frame": 361, "type": "imguizmo", "translation": [ - 0.037500000000000006, - 0.0, - 0.0 + -0.08447, + -3.598746, + 2.031665 ], "rotation_deg": [ - 0.0, - 0.7, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 362, "type": "imguizmo", "translation": [ - 0.037482339715127086, - 0.0008004104013847341, - 0.0019997794888495157 + -0.168881, + -3.594983, + 2.063242 ], "rotation_deg": [ - 0.014822414840458037, - 0.6997383661500308, - 0.02370109023821648 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 363, "type": "imguizmo", "translation": [ - 0.03743079368581399, - 0.001535790758968834, - 0.0038296138177695855 + -0.253175, + -3.588715, + 2.094643 ], "rotation_deg": [ - 0.0296263627977706, - 0.6989537905633207, - 0.047284102837067216 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 364, "type": "imguizmo", "translation": [ - 0.03734672592896117, - 0.0022743803390788625, - 0.005653167792013589 + -0.337292, + -3.579946, + 2.12578 ], "rotation_deg": [ - 0.04439339999622906, - 0.6976472507233524, - 0.07063154841404629 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 365, "type": "imguizmo", "translation": [ - 0.037230137198767894, - 0.003009726514375728, - 0.007447534081582464 + -0.421173, + -3.568682, + 2.156566 ], "rotation_deg": [ - 0.059105128546334026, - 0.6958203744160708, - 0.09362711116970135 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 366, "type": "imguizmo", "translation": [ - 0.03708118106897446, - 0.003741355730028776, - 0.009204879041947101 + -0.504762, + -3.554932, + 2.186916 ], "rotation_deg": [ - 0.0737432194662747, - 0.6934754377018649, - 0.1161562283670957 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 367, "type": "imguizmo", "translation": [ - 0.036900042454873945, - 0.004468321059545822, - 0.010916359205066808 + -0.587998, + -3.538703, + 2.216745 ], "rotation_deg": [ - 0.08828943551755786, - 0.69061536207988, - 0.13810666107760092 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 368, "type": "imguizmo", "translation": [ - 0.03668694708586311, - 0.005189719627258477, - 0.012573455139966608 + -0.670825, + -3.520009, + 2.24597 ], "rotation_deg": [ - 0.10272565392633597, - 0.6872437108481929, - 0.15936905334959353 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 369, "type": "imguizmo", "translation": [ - 0.036442160447977094, - 0.005904652433774164, - 0.014167910711884685 + -0.753185, + -3.498861, + 2.274509 ], "rotation_deg": [ - 0.11703388896212669, - 0.6833646846643872, - 0.17983747701430533 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 370, "type": "imguizmo", "translation": [ - 0.03616598751522562, - 0.006612228779970917, - 0.015691782468658773 + -0.835019, + -3.475275, + 2.302283 ], "rotation_deg": [ - 0.13119631434579354, - 0.678983116312056, - 0.19940995941463166 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 371, "type": "imguizmo", "translation": [ - 0.03585877236466551, - 0.007311567112381287, - 0.017137478554226232 + -0.916272, + -3.449267, + 2.329215 ], "rotation_deg": [ - 0.14519528545886964, - 0.6741044646797563, - 0.21798899142777994 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 372, "type": "imguizmo", "translation": [ - 0.03552089774812088, - 0.008001796142643488, - 0.018497796583699353 + -0.996885, + -3.420855, + 2.355229 ], "rotation_deg": [ - 0.15901336132655539, - 0.6687348079599128, - 0.23548201325081422 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 373, "type": "imguizmo", "translation": [ - 0.03515278461529029, - 0.008682055931326585, - 0.0197659595210582 + -1.076805, + -3.390059, + 2.380253 ], "rotation_deg": [ - 0.17263332634700115, - 0.6628808360761476, - 0.25180187552893807 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 374, "type": "imguizmo", "translation": [ - 0.03475489158929837, - 0.009351498959443256, - 0.020935649442341227 + -1.155973, + -3.3569, + 2.404217 ], "rotation_deg": [ - 0.18603821173980334, - 0.656549842348468, - 0.26686727352919565 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 375, "type": "imguizmo", "translation": [ - 0.034327714395308274, - 0.010009291184342104, - 0.022001039011197647 + -1.234336, + -3.321402, + 2.427055 ], "rotation_deg": [ - 0.1992113166869919, - 0.6497497144066984, - 0.2806031521965623 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 376, "type": "imguizmo", "translation": [ - 0.03387178524290992, - 0.010654613078821805, - 0.022956820510386156 + -1.311839, + -3.283589, + 2.448702 ], "rotation_deg": [ - 0.21213622914016986, - 0.6424889243634754, - 0.29294108007445296 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 377, "type": "imguizmo", "translation": [ - 0.03338767216305311, - 0.011286660652159872, - 0.02379823228455507 + -1.388428, + -3.243488, + 2.469099 ], "rotation_deg": [ - 0.22479684626788168, - 0.6347765182590505, - 0.3038195902267965 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 378, "type": "imguizmo", "translation": [ - 0.03287597830035166, - 0.01190464645178498, - 0.024521082462569756 + -1.464049, + -3.201126, + 2.488188 ], "rotation_deg": [ - 0.2371773945177364, - 0.6266221047910505, - 0.3131844864632196 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 379, "type": "imguizmo", "translation": [ - 0.03233734116164035, - 0.012507800544344843, - 0.02512176984120383 + -1.53865, + -3.156534, + 2.505917 ], "rotation_deg": [ - 0.24926244926828958, - 0.6180358433432368, - 0.32098911334174113 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 380, "type": "imguizmo", "translation": [ - 0.03177243182172079, - 0.013095371474947265, - 0.025597301826152427 + -1.612178, + -3.109741, + 2.522235 ], "rotation_deg": [ - 0.26103695404620086, - 0.6090284313281781, - 0.3271945886038368 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 381, "type": "imguizmo", "translation": [ - 0.031181954087285777, - 0.01366662720337937, - 0.025945309340986527 + -1.684583, + -3.060782, + 2.537098 ], "rotation_deg": [ - 0.272486239284725, - 0.5996110908596065, - 0.3317699968838959 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 382, "type": "imguizmo", "translation": [ - 0.030566643620063783, - 0.014220856016138551, - 0.026164058629772516 + -1.755814, + -3.009689, + 2.550464 ], "rotation_deg": [ - 0.2835960406001648, - 0.5897955547710614, - 0.334692543728017 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 383, "type": "imguizmo", "translation": [ - 0.029927267020276076, - 0.014757367413138881, - 0.0262524598945571 + -1.825821, + -2.956499, + 2.562296 ], "rotation_deg": [ - 0.2943525165635178, - 0.5795940519982398, - 0.33594766915483143 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 384, "type": "imguizmo", "translation": [ - 0.029264620871548297, - 0.01527549296798822, - 0.026210072724685844 + -1.894556, + -2.901248, + 2.57256 ], "rotation_deg": [ - 0.30474226594517273, - 0.569019292343265, - 0.3355291201925975 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 385, "type": "imguizmo", "translation": [ - 0.028579530748466433, - 0.015774587160764266, - 0.0260371082909069 + -1.96197, + -2.843975, + 2.581229 ], "rotation_deg": [ - 0.3147523444111726, - 0.5580844506398562, - 0.33343898203119127 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 386, "type": "imguizmo", "translation": [ - 0.027872850188013703, - 0.016254028182252002, - 0.02573442829332898 + -2.028017, + -2.784721, + 2.588277 ], "rotation_deg": [ - 0.3243702806502433, - 0.5468031503391262, - 0.32968766763379587 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 387, "type": "imguizmo", "translation": [ - 0.02714545962616979, - 0.01671321870864055, - 0.025303540668474744 + -2.092651, + -2.723525, + 2.593686 ], "rotation_deg": [ - 0.33358409191149463, - 0.5351894465364576, - 0.32429386586004216 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 388, "type": "imguizmo", "translation": [ - 0.026398265300997186, - 0.017151586645714253, - 0.024746592076816922 + -2.155826, + -2.660432, + 2.597441 ], "rotation_deg": [ - 0.3423822989334351, - 0.5232578084606041, - 0.3172844483590506 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 389, "type": "imguizmo", "translation": [ - 0.025632198123581477, - 0.01756858584161082, - 0.02406635720822379 + -2.217499, + -2.595485, + 2.59953 ], "rotation_deg": [ - 0.35075394024570283, - 0.5110231014468334, - 0.3086943356962268 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 390, "type": "imguizmo", "translation": [ - 0.02484821251823199, - 0.017963696767258567, - 0.023266224958593804 + -2.277626, + -2.528728, + 2.599948 ], "rotation_deg": [ - 0.3586885858256931, - 0.4985005684165705, - 0.2985663233807609 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 391, "type": "imguizmo", "translation": [ - 0.024047285233388015, - 0.0183364271636449, - 0.022350181546546777 + -2.336167, + -2.46021, + 2.598694 ], "rotation_deg": [ - 0.3661763500930662, - 0.48570581088661596, - 0.28695086866055436 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 392, "type": "imguizmo", "translation": [ - 0.023230414124711826, - 0.018686312655109782, - 0.021322790654283122 + -2.393079, + -2.389976, + 2.595771 ], "rotation_deg": [ - 0.37320790422595135, - 0.47265476953159796, - 0.2739058391467479 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 393, "type": "imguizmo", "translation": [ - 0.022398616911884814, - 0.019012917327899993, - 0.020189170691548783 + -2.448323, + -2.318078, + 2.591188 ], "rotation_deg": [ - 0.3797744877834957, - 0.4593637043238779, - 0.2594962245201946 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 394, "type": "imguizmo", "translation": [ - 0.021552929910655477, - 0.01931583427326332, - 0.018954969295975253 + -2.501861, + -2.244563, + 2.584957 ], "rotation_deg": [ - 0.3858679196202836, - 0.44584917427564835, - 0.24379381275613804 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 395, "type": "imguizmo", "translation": [ - 0.02069440674171906, - 0.01959468609440627, - 0.01762633519683311 + -2.553655, + -2.169485, + 2.577095 ], "rotation_deg": [ - 0.39148060807902463, - 0.43212801680846674, - 0.22687683248013177 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 396, "type": "imguizmo", "translation": [ - 0.01982411701803736, - 0.01984912537668343, - 0.016209887582371914 + -2.60367, + -2.092894, + 2.567626 ], "rotation_deg": [ - 0.39660556044881345, - 0.4182173267759242, - 0.2088295632369606 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 397, "type": "imguizmo", "translation": [ - 0.018943145012234153, - 0.02007883512043287, - 0.014712683123357464 + -2.65187, + -2.014845, + 2.556574 ], "rotation_deg": [ - 0.40123639167717656, - 0.4041344351655874, - 0.18974191561418913 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 398, "type": "imguizmo", "translation": [ - 0.01805258830572656, - 0.020283529135918237, - 0.013142180817092872 + -2.698222, + -1.935392, + 2.54397 ], "rotation_deg": [ - 0.4053673323250529, - 0.3898968875067451, - 0.1697089833121358 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 399, "type": "imguizmo", "translation": [ - 0.017153556421275244, - 0.02046295239988554, - 0.011506204827069306 + -2.742694, + -1.85459, + 2.529851 ], "rotation_deg": [ - 0.40899323575479624, - 0.3755224220108648, - 0.1488305693918427 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 400, "type": "imguizmo", "translation": [ - 0.016247169440657218, - 0.020616881373290392, - 0.00981290550337695 + -2.785254, + -1.772495, + 2.514255 ], "rotation_deg": [ - 0.412109584542245, - 0.3610289474719887, - 0.12721068906124425 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 401, "type": "imguizmo", "translation": [ - 0.015334556609183439, - 0.02074512427979976, - 0.008070718778070907 + -2.825873, + -1.689165, + 2.497226 ], "rotation_deg": [ - 0.4147124961048701, - 0.3464345209546077, - 0.1049570514766347 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 402, "type": "imguizmo", "translation": [ - 0.014416854928799771, - 0.020847521344721443, - 0.006288324137782001 + -2.864523, + -1.604658, + 2.47881 ], "rotation_deg": [ - 0.4167987275389892, - 0.3317573252968066, - 0.08218052314106859 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 403, "type": "imguizmo", "translation": [ - 0.013495207741524129, - 0.02092394499406345, - 0.004474601382950985 + -2.901176, + -1.519033, + 2.459061 ], "rotation_deg": [ - 0.4183656796600214, - 0.31701564645671276, - 0.058994575573019674 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 404, "type": "imguizmo", "translation": [ - 0.012570763304984692, - 0.020974300013475267, - 0.0026385863891093806 + -2.935807, + -1.432349, + 2.438031 ], "rotation_deg": [ - 0.41941140024074697, - 0.3022278507304685, - 0.0355147199969831 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 405, "type": "imguizmo", "translation": [ - 0.011644673361833922, - 0.020998523666873114, - 0.0007894260906021722 + -2.968393, + -1.344666, + 2.415781 ], "rotation_deg": [ - 0.4199345864435394, - 0.28741236187011404, - 0.011857931872366372 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 406, "type": "imguizmo", "translation": [ - 0.01071809170482059, - 0.020996585774601288, - -0.0010636670889790912 + -2.99891, + -1.256047, + 2.392372 ], "rotation_deg": [ - 0.4199345864435394, - 0.27258763812988596, - -0.011857931872366542 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 407, "type": "imguizmo", "translation": [ - 0.009792172739307726, - 0.02096848875103224, - -0.002911461132627139 + -3.027336, + -1.166552, + 2.367869 ], "rotation_deg": [ - 0.41941140024074697, - 0.2577721492695314, - -0.03551471999698341 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 408, "type": "imguizmo", "translation": [ - 0.00886807004502728, - 0.020914267601558588, - -0.0047447504233633555 + -3.053653, + -1.076245, + 2.342341 ], "rotation_deg": [ - 0.4183656796600214, - 0.24298435354328732, - -0.058994575573019695 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 409, "type": "imguizmo", "translation": [ - 0.007946934938863372, - 0.02083398987898074, - -0.006554401606158599 + -3.077842, + -0.985187, + 2.315859 ], "rotation_deg": [ - 0.4167987275389892, - 0.22824267470319334, - -0.08218052314106876 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 410, "type": "imguizmo", "translation": [ - 0.007029915040454912, - 0.020727755599344533, - -0.008331399089848372 + -3.099886, + -0.893442, + 2.288497 ], "rotation_deg": [ - 0.4147124961048701, - 0.21356547904539228, - -0.10495705147663488 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 411, "type": "imguizmo", "translation": [ - 0.00611815284240437, - 0.020595697117333674, - -0.010066889962354619 + -3.119769, + -0.801075, + 2.26033 ], "rotation_deg": [ - 0.412109584542245, - 0.19897105252801126, - -0.12721068906124441 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 412, "type": "imguizmo", "translation": [ - 0.005212784286874287, - 0.02043797896137231, - -0.011752228095448349 + -3.137479, + -0.70815, + 2.231438 ], "rotation_deg": [ - 0.40899323575479624, - 0.1844775779891353, - -0.14883056939184272 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 413, "type": "imguizmo", "translation": [ - 0.004314937350344763, - 0.02025479762864308, - -0.013379017219325592 + -3.153001, + -0.614731, + 2.201901 ], "rotation_deg": [ - 0.4053673323250529, - 0.17010311249325488, - -0.169708983312136 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 414, "type": "imguizmo", "translation": [ - 0.0034257306382952735, - 0.020046381340276094, - -0.014939152752401161 + -3.166327, + -0.520884, + 2.171801 ], "rotation_deg": [ - 0.40123639167717656, - 0.15586556483441263, - -0.18974191561418932 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 415, "type": "imguizmo", "translation": [ - 0.0025462719915614897, - 0.019812989757013844, - -0.016424862177926474 + -3.177445, + -0.426674, + 2.141222 ], "rotation_deg": [ - 0.39660556044881345, - 0.1417826732240757, - -0.20882956323696095 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 416, "type": "imguizmo", "translation": [ - 0.0016776571061035855, - 0.019554913655706233, - -0.01782874376627699 + -3.186349, + -0.332166, + 2.11025 ], "rotation_deg": [ - 0.3914806080790247, - 0.12787198319153337, - -0.22687683248013185 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 417, "type": "imguizmo", "translation": [ - 0.0008209681679054502, - 0.019272474567038823, - -0.01914380344999699 + -3.193033, + -0.237427, + 2.07897 ], "rotation_deg": [ - 0.3858679196202836, - 0.11415082572435162, - -0.24379381275613823 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 418, "type": "imguizmo", "translation": [ - -2.2727495294201e-05, - 0.018966024374945666, - -0.020363489667891664 + -3.197491, + -0.142522, + 2.04747 ], "rotation_deg": [ - 0.37977448778349565, - 0.10063629567612194, - -0.2594962245201949 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 419, "type": "imguizmo", "translation": [ - -0.0008523787437593401, - 0.01863594487820572, - -0.021481726004575534 + -3.199721, + -0.047519, + 2.015838 ], "rotation_deg": [ - 0.37320790422595124, - 0.0873452304684018, - -0.27390583914674826 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 420, "type": "imguizmo", "translation": [ - -0.0016669519353458278, - 0.018282647314769127, - -0.022492941462868236 + -3.199721, + 0.047519, + 1.984162 ], "rotation_deg": [ - 0.3661763500930662, - 0.07429418911338405, - -0.2869508686605546 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 421, "type": "imguizmo", "translation": [ - -0.002465432213290845, - 0.017906571849405903, - -0.02339209821822225 + -3.197491, + 0.142522, + 1.95253 ], "rotation_deg": [ - 0.3586885858256931, - 0.06149943158342944, - -0.29856632338076117 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 422, "type": "imguizmo", "translation": [ - -0.003246824770597988, - 0.01750818702531549, - -0.02417471671691136 + -3.193033, + 0.237427, + 1.92103 ], "rotation_deg": [ - 0.35075394024570283, - 0.048976898553166545, - -0.308694335696227 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 423, "type": "imguizmo", "translation": [ - -0.004010156089442942, - 0.017087989180380257, - -0.024836897992941996 + -3.186349, + 0.332166, + 1.88975 ], "rotation_deg": [ - 0.3423822989334349, - 0.036742191539395806, - -0.31728444835905084 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 424, "type": "imguizmo", "translation": [ - -0.004754475154055448, - 0.0166465018287903, - -0.0253753430925057 + -3.177445, + 0.426674, + 1.858778 ], "rotation_deg": [ - 0.3335840919114946, - 0.024810553463542426, - -0.32429386586004233 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 425, "type": "imguizmo", "translation": [ - -0.005478854635566584, - 0.016184275008809926, - -0.02578736950920107 + -3.166327, + 0.520884, + 1.828199 ], "rotation_deg": [ - 0.3243702806502433, - 0.013196849660873741, - -0.32968766763379603 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 426, "type": "imguizmo", "translation": [ - -0.006182392047344966, - 0.015701884597498536, - -0.026070924548145677 + -3.153001, + 0.614731, + 1.798099 ], "rotation_deg": [ - 0.3147523444111725, - 0.0019155493601436951, - -0.3334389820311915 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 427, "type": "imguizmo", "translation": [ - -0.006864210869382786, - 0.015199931593239452, - -0.026224595552398755 + -3.137479, + 0.70815, + 1.768562 ], "rotation_deg": [ - 0.3047422659451726, - -0.009019292343265031, - -0.33552912019259773 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 428, "type": "imguizmo", "translation": [ - -0.007523461640330552, - 0.014679041366970819, - -0.026247616940747006 + -3.119769, + 0.801075, + 1.73967 ], "rotation_deg": [ - 0.29435251656351785, - -0.019594051998239652, - -0.33594766915483176 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 429, "type": "imguizmo", "translation": [ - -0.008159323015820195, - 0.014139862883051245, - -0.02613987402179168 + -3.099886, + 0.893442, + 1.711503 ], "rotation_deg": [ - 0.28359604060016474, - -0.02979555477106141, - -0.3346925437280173 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 430, "type": "imguizmo", "translation": [ - -0.008771002791757822, - 0.013583067890731057, - -0.025901903565335387 + -3.077842, + 0.985187, + 1.684141 ], "rotation_deg": [ - 0.27248623928472493, - -0.03961109085960652, - -0.3317699968838962 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 431, "type": "imguizmo", "translation": [ - -0.009357738891311397, - 0.013009350087236372, - -0.025534891128221906 + -3.053653, + 1.076245, + 1.657659 ], "rotation_deg": [ - 0.2610369540462009, - -0.0490284313281782, - -0.3271945886038371 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 432, "type": "imguizmo", "translation": [ - -0.009918800314363567, - 0.012419424253508744, - -0.025040665147951665 + -3.027336, + 1.166552, + 1.632131 ], "rotation_deg": [ - 0.2492624492682897, - -0.05803584334323679, - -0.32098911334174146 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 433, "type": "imguizmo", "translation": [ - -0.010453488048246824, - 0.011814025363677084, - -0.024421687833498032 + -2.99891, + 1.256047, + 1.607628 ], "rotation_deg": [ - 0.2371773945177365, - -0.06662210479105055, - -0.31318448646322 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 434, "type": "imguizmo", "translation": [ - -0.010961135938626158, - 0.011193907669371459, - -0.023681042898706112 + -2.968393, + 1.344666, + 1.584219 ], "rotation_deg": [ - 0.22479684626788177, - -0.07477651825905052, - -0.3038195902267967 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 435, "type": "imguizmo", "translation": [ - -0.011441111519444495, - 0.010559843760019469, - -0.022822420199385483 + -2.935807, + 1.432349, + 1.561969 ], "rotation_deg": [ - 0.21213622914016986, - -0.08248892436347544, - -0.2929410800744532 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 436, "type": "imguizmo", "translation": [ - -0.011892816800896579, - 0.009912623600296027, - -0.02185009735063412 + -2.901176, + 1.519033, + 1.540939 ], "rotation_deg": [ - 0.1992113166869921, - -0.08974971440669835, - -0.2806031521965627 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 437, "type": "imguizmo", "translation": [ - -0.012315689014449863, - 0.009253053545925717, - -0.020768918415975165 + -2.864523, + 1.604658, + 1.52119 ], "rotation_deg": [ - 0.1860382117398035, - -0.09654984234846804, - -0.266867273529196 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 438, "type": "imguizmo", "translation": [ - -0.012709201313983974, - 0.008581955339064019, - -0.019584269774476144 + -2.825873, + 1.689165, + 1.502774 ], "rotation_deg": [ - 0.1726333263470012, - -0.10288083607614762, - -0.2518018755289384 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 439, "type": "imguizmo", "translation": [ - -0.0130728634321754, - 0.007900165084508881, - -0.018302053286079367 + -2.785254, + 1.772495, + 1.485745 ], "rotation_deg": [ - 0.15901336132655544, - -0.10873480795991293, - -0.2354820132508146 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 440, "type": "imguizmo", "translation": [ - -0.013406222291309527, - 0.007208532208018262, - -0.016928656888832584 + -2.742694, + 1.85459, + 1.470149 ], "rotation_deg": [ - 0.14519528545886967, - -0.11410446467975643, - -0.2179889914277802 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 441, "type": "imguizmo", "translation": [ - -0.013708862567759031, - 0.006507918398031398, - -0.015470922774502942 + -2.698222, + 1.935392, + 1.45603 ], "rotation_deg": [ - 0.1311963143457935, - -0.1189831163120562, - -0.1994099594146319 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 442, "type": "imguizmo", "translation": [ - -0.013980407209425422, - 0.005799196532112338, - -0.013936113301121775 + -2.65187, + 2.014845, + 1.443426 ], "rotation_deg": [ - 0.11703388896212667, - -0.1233646846643874, - -0.17983747701430552 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 443, "type": "imguizmo", "translation": [ - -0.014220517905498997, - 0.0050832495894531605, - -0.012331874812282206 + -2.60367, + 2.092894, + 1.432374 ], "rotation_deg": [ - 0.10272565392633588, - -0.12724371084819314, - -0.15936905334959364 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 444, "type": "imguizmo", "translation": [ - -0.014428895507951985, - 0.0043609695507918845, - -0.010666199543440011 + -2.553655, + 2.169485, + 1.422905 ], "rotation_deg": [ - 0.08828943551755793, - -0.13061536207988014, - -0.13810666107760122 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 445, "type": "imguizmo", "translation": [ - -0.014605280404239732, - 0.003633256287115495, - -0.008947385804998506 + -2.501861, + 2.244563, + 1.415043 ], "rotation_deg": [ - 0.07374321946627474, - -0.1334754377018651, - -0.11615622836709598 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 446, "type": "imguizmo", "translation": [ - -0.014749452840745607, - 0.0029010164385328066, - -0.007183996640543674 + -2.448323, + 2.318078, + 1.408812 ], "rotation_deg": [ - 0.05910512854633401, - -0.13582037441607092, - -0.09362711116970159 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 447, "type": "imguizmo", "translation": [ - -0.014861233196566641, - 0.002165162284713752, - -0.005384817166191659 + -2.393079, + 2.389976, + 1.404229 ], "rotation_deg": [ - 0.04439339999622902, - -0.1376472507233525, - -0.07063154841404642 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 448, "type": "imguizmo", "translation": [ - -0.014940482207298825, - 0.0014266106083025282, - -0.003558810803582231 + -2.336167, + 2.46021, + 1.401306 ], "rotation_deg": [ - 0.029626362797770714, - -0.13895379056332086, - -0.0472841028370676 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 449, "type": "imguizmo", "translation": [ - -0.014987101138543183, - 0.0006862815527205454, - -0.0017150746245629493 + -2.277626, + 2.528728, + 1.400052 ], "rotation_deg": [ - 0.014822414840458112, - -0.13973836615003088, - -0.023701090238216805 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 450, "type": "imguizmo", "translation": [ - -0.015001031908916553, - -5.490252421763199e-05, - 0.00013720596996502914 + -2.217499, + 2.595485, + 1.40047 ], "rotation_deg": [ - 3.8163916471489756e-17, - -0.14000000000000004, - -2.636779683484747e-16 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 451, "type": "imguizmo", "translation": [ - -0.014982257162413764, - -0.0007960181994473137, - 0.0019888030112523115 + -2.155826, + 2.660432, + 1.402559 ], "rotation_deg": [ - -0.014822414840458036, - -0.13973836615003088, - 0.023701090238216278 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 452, "type": "imguizmo", "translation": [ - -0.014930800290031056, - -0.0015361421351238196, - 0.0038304919359773647 + -2.092651, + 2.723525, + 1.406314 ], "rotation_deg": [ - -0.029626362797770638, - -0.13895379056332086, - 0.04728410283706707 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 453, "type": "imguizmo", "translation": [ - -0.014846725400623805, - -0.0022743522289864577, - 0.005653097542556974 + -2.028017, + 2.784721, + 1.411723 ], "rotation_deg": [ - -0.04439339999622913, - -0.1376472507233525, - 0.0706315484140462 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 454, "type": "imguizmo", "translation": [ - -0.014730137241034882, - -0.003009728763183117, - 0.007447539701539008 + -1.96197, + 2.843975, + 1.418771 ], "rotation_deg": [ - -0.05910512854633413, - -0.13582037441607092, - 0.09362711116970135 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 455, "type": "imguizmo", "translation": [ - -0.014581181065593106, - -0.003741355550124163, - 0.009204878592350548 + -1.894556, + 2.901248, + 1.42744 ], "rotation_deg": [ - -0.07374321946627448, - -0.13347543770186515, - 0.11615622836709519 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 456, "type": "imguizmo", "translation": [ - -0.014400042455144462, - -0.004468321073938172, - 0.010916359241034509 + -1.825821, + 2.956499, + 1.437704 ], "rotation_deg": [ - -0.08828943551755766, - -0.13061536207988012, - 0.13810666107760047 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 457, "type": "imguizmo", "translation": [ - -0.01418694708584148, - -0.005189719626107071, - 0.012573455137089174 + -1.755814, + 3.009689, + 1.449536 ], "rotation_deg": [ - -0.10272565392633581, - -0.12724371084819316, - 0.15936905334959314 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 458, "type": "imguizmo", "translation": [ - -0.013942160447978831, - -0.0059046524338662604, - 0.014167910712114862 + -1.684583, + 3.060782, + 1.462902 ], "rotation_deg": [ - -0.11703388896212659, - -0.12336468466438734, - 0.17983747701430502 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 459, "type": "imguizmo", "translation": [ - -0.013665987515225492, - -0.006612228779963537, - 0.015691782468640354 + -1.612178, + 3.109741, + 1.477765 ], "rotation_deg": [ - -0.13119631434579346, - -0.11898311631205628, - 0.19940995941463147 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 460, "type": "imguizmo", "translation": [ - -0.013358772364665527, - -0.007311567112381866, - 0.0171374785542277 + -1.53865, + 3.156534, + 1.494083 ], "rotation_deg": [ - -0.14519528545886964, - -0.11410446467975652, - 0.21798899142777975 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 461, "type": "imguizmo", "translation": [ - -0.013020897748120881, - -0.008001796142643432, - 0.018497796583699235 + -1.464049, + 3.201126, + 1.511812 ], "rotation_deg": [ - -0.15901336132655539, - -0.10873480795991287, - 0.23548201325081408 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 462, "type": "imguizmo", "translation": [ - -0.012652784615290293, - -0.00868205593132658, - 0.019765959521058214 + -1.388428, + 3.243488, + 1.530901 ], "rotation_deg": [ - -0.1726333263470012, - -0.10288083607614763, - 0.25180187552893796 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 463, "type": "imguizmo", "translation": [ - -0.012254891589298376, - -0.00935149895944324, - 0.02093564944234122 + -1.311839, + 3.283589, + 1.551298 ], "rotation_deg": [ - -0.1860382117398033, - -0.09654984234846813, - 0.2668672735291954 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 464, "type": "imguizmo", "translation": [ - -0.011827714395308275, - -0.01000929118434209, - 0.022001039011197644 + -1.234336, + 3.321402, + 1.572945 ], "rotation_deg": [ - -0.1992113166869919, - -0.08974971440669843, - 0.2806031521965621 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 465, "type": "imguizmo", "translation": [ - -0.01137178524290993, - -0.010654613078821791, - 0.022956820510386156 + -1.155973, + 3.3569, + 1.595783 ], "rotation_deg": [ - -0.21213622914016989, - -0.0824889243634754, - 0.2929410800744528 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 466, "type": "imguizmo", "translation": [ - -0.010887672163053115, - -0.01128666065215986, - 0.02379823228455506 + -1.076805, + 3.390059, + 1.619747 ], "rotation_deg": [ - -0.2247968462678817, - -0.07477651825905048, - 0.3038195902267964 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 467, "type": "imguizmo", "translation": [ - -0.010375978300351661, - -0.01190464645178497, - 0.024521082462569756 + -0.996885, + 3.420855, + 1.644771 ], "rotation_deg": [ - -0.23717739451773645, - -0.06662210479105057, - 0.3131844864632196 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 468, "type": "imguizmo", "translation": [ - -0.009837341161640353, - -0.012507800544344838, - 0.025121769841203834 + -0.916272, + 3.449267, + 1.670785 ], "rotation_deg": [ - -0.24926244926828975, - -0.05803584334323682, - 0.32098911334174113 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 469, "type": "imguizmo", "translation": [ - -0.009272431821720788, - -0.013095371474947257, - 0.02559730182615243 + -0.835019, + 3.475275, + 1.697717 ], "rotation_deg": [ - -0.26103695404620103, - -0.049028431328178024, - 0.3271945886038368 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 470, "type": "imguizmo", "translation": [ - -0.008681954087285768, - -0.01366662720337936, - 0.025945309340986527 + -0.753185, + 3.498861, + 1.725491 ], "rotation_deg": [ - -0.27248623928472504, - -0.039611090859606415, - 0.33176999688389586 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 471, "type": "imguizmo", "translation": [ - -0.00806664362006379, - -0.01422085601613853, - 0.026164058629772516 + -0.670825, + 3.520009, + 1.75403 ], "rotation_deg": [ - -0.28359604060016463, - -0.029795554771061578, - 0.33469254372801693 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 472, "type": "imguizmo", "translation": [ - -0.007427267020276088, - -0.014757367413138864, - 0.0262524598945571 + -0.587998, + 3.538703, + 1.783255 ], "rotation_deg": [ - -0.2943525165635178, - -0.019594051998239922, - 0.3359476691548314 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 473, "type": "imguizmo", "translation": [ - -0.0067646208715482995, - -0.0152754929679882, - 0.026210072724685847 + -0.504762, + 3.554932, + 1.813084 ], "rotation_deg": [ - -0.3047422659451727, - -0.009019292343265038, - 0.33552912019259745 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 474, "type": "imguizmo", "translation": [ - -0.006079530748466435, - -0.015774587160764245, - 0.026037108290906903 + -0.421173, + 3.568682, + 1.843434 ], "rotation_deg": [ - -0.31475234441117256, - 0.0019155493601437212, - 0.3334389820311912 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 475, "type": "imguizmo", "translation": [ - -0.0053728501880136986, - -0.01625402818225199, - 0.025734428293328972 + -0.337292, + 3.579946, + 1.87422 ], "rotation_deg": [ - -0.3243702806502435, - 0.0131968496608738, - 0.32968766763379576 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 476, "type": "imguizmo", "translation": [ - -0.004645459626169784, - -0.01671321870864054, - 0.025303540668474733 + -0.253175, + 3.588715, + 1.905357 ], "rotation_deg": [ - -0.33358409191149474, - 0.024810553463542485, - 0.324293865860042 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 477, "type": "imguizmo", "translation": [ - -0.0038982653009971835, - -0.017151586645714246, - 0.024746592076816912 + -0.168881, + 3.594983, + 1.936758 ], "rotation_deg": [ - -0.3423822989334353, - 0.03674219153939606, - 0.31728444835905045 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 478, "type": "imguizmo", "translation": [ - -0.003132198123581465, - -0.017568585841610813, - 0.024066357208223768 + -0.08447, + 3.598746, + 1.968335 ], "rotation_deg": [ - -0.3507539402457032, - 0.0489768985531668, - 0.3086943356962265 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 479, "type": "imguizmo", "translation": [ - -0.002348212518232, - -0.017963696767258543, - 0.02326622495859381 + 0, + 3.6, + 2 ], "rotation_deg": [ - -0.35868858582569313, - 0.06149943158342936, - 0.29856632338076095 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 480, - "type": "imguizmo", - "translation": [ - -0.0015472852333880207, - -0.018336427163644878, - 0.022350181546546777 - ], - "rotation_deg": [ - -0.3661763500930663, - 0.07429418911338394, - 0.28695086866055436 - ], - "scale": [ - 1.0, - 1.0, - 1.0 - ] + "type": "action", + "action": "set_active_render_window", + "value": 0 + }, + { + "frame": 480, + "type": "action", + "action": "set_active_planar", + "value": 2 + }, + { + "frame": 480, + "type": "action", + "action": "set_projection_type", + "value": "perspective" + }, + { + "frame": 480, + "type": "action", + "action": "set_left_handed", + "value": true + }, + { + "frame": 480, + "type": "action", + "action": "reset_active_camera", + "value": 1 + }, + { + "frame": 480, + "type": "action", + "action": "set_active_render_window", + "value": 1 + }, + { + "frame": 480, + "type": "action", + "action": "set_active_planar", + "value": 2 + }, + { + "frame": 480, + "type": "action", + "action": "set_projection_type", + "value": "orthographic" + }, + { + "frame": 480, + "type": "action", + "action": "set_left_handed", + "value": true + }, + { + "frame": 480, + "type": "action", + "action": "set_active_render_window", + "value": 0 }, { "frame": 481, "type": "imguizmo", "translation": [ - -0.0007304141247118297, - -0.01868631265510976, - 0.021322790654283122 + 0.0, + 0.001139, + 0.001081 ], "rotation_deg": [ - -0.37320790422595146, - 0.08734523046840188, - 0.2739058391467479 + 0.0, + 0.078884, + 0.040918 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 482, "type": "imguizmo", "translation": [ - 0.00010138308811518618, - -0.019012917327899972, - 0.020189170691548766 + 4.8E-05, + 0.001148, + 0.001049 ], "rotation_deg": [ - -0.3797744877834958, - 0.10063629567612202, - 0.25949622452019444 + 0.001584, + 0.080686, + 0.04041 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 483, "type": "imguizmo", "translation": [ - 0.0009470700893445218, - -0.0193158342732633, - 0.01895496929597525 + 9.5E-05, + 0.001157, + 0.001017 ], "rotation_deg": [ - -0.3858679196202837, - 0.11415082572435163, - 0.24379381275613798 + 0.003167, + 0.082432, + 0.039873 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 484, "type": "imguizmo", "translation": [ - 0.0018055932582809425, - -0.019594686094406254, - 0.01762633519683309 + 0.000142, + 0.001165, + 0.000984 ], "rotation_deg": [ - -0.3914806080790248, - 0.12787198319153337, - 0.22687683248013157 + 0.004747, + 0.08412, + 0.039309 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 485, "type": "imguizmo", "translation": [ - 0.0026758829819626493, - -0.01984912537668341, - 0.0162098875823719 + 0.00019, + 0.001172, + 0.00095 ], "rotation_deg": [ - -0.39660556044881357, - 0.14178267322407587, - 0.2088295632369604 + 0.006324, + 0.085749, + 0.038717 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 486, "type": "imguizmo", "translation": [ - 0.0035568549877658525, - -0.020078835120432847, - 0.014712683123357438 + 0.000237, + 0.001179, + 0.000916 ], "rotation_deg": [ - -0.40123639167717673, - 0.1558655648344128, - 0.18974191561418877 + 0.007897, + 0.087319, + 0.038098 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 487, "type": "imguizmo", "translation": [ - 0.004447411694273425, - -0.020283529135918205, - 0.013142180817092886 + 0.000284, + 0.001184, + 0.000881 ], "rotation_deg": [ - -0.40536733232505295, - 0.17010311249325472, - 0.1697089833121359 + 0.009464, + 0.088828, + 0.037452 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 488, "type": "imguizmo", "translation": [ - 0.005346443578724748, - -0.020462952399885517, - 0.01150620482706931 + 0.000331, + 0.001189, + 0.000846 ], "rotation_deg": [ - -0.40899323575479635, - 0.18447757798913514, - 0.1488305693918427 + 0.011025, + 0.090275, + 0.036781 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 489, "type": "imguizmo", "translation": [ - 0.006252830559342776, - -0.020616881373290364, - 0.009812905503376955 + 0.000377, + 0.001193, + 0.000809 ], "rotation_deg": [ - -0.41210958454224506, - 0.19897105252801117, - 0.12721068906124428 + 0.012578, + 0.091659, + 0.036083 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 490, "type": "imguizmo", "translation": [ - 0.007165443390816552, - -0.02074512427979973, - 0.00807071877807091 + 0.000424, + 0.001196, + 0.000773 ], "rotation_deg": [ - -0.41471249610487015, - 0.2135654790453922, - 0.1049570514766347 + 0.014122, + 0.092979, + 0.035361 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 491, "type": "imguizmo", "translation": [ - 0.008083145071200221, - -0.020847521344721422, - 0.006288324137782004 + 0.00047, + 0.001198, + 0.000735 ], "rotation_deg": [ - -0.4167987275389894, - 0.22824267470319326, - 0.0821805231410686 + 0.015657, + 0.094234, + 0.034614 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 492, "type": "imguizmo", "translation": [ - 0.00900479225847587, - -0.020923944994063425, - 0.004474601382950978 + 0.000515, + 0.001199, + 0.000698 ], "rotation_deg": [ - -0.4183656796600216, - 0.24298435354328723, - 0.05899457557301954 + 0.01718, + 0.095424, + 0.033843 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 493, "type": "imguizmo", "translation": [ - 0.009929236695015307, - -0.020974300013475243, - 0.0026385863891093737 + 0.000561, + 0.0012, + 0.000659 ], "rotation_deg": [ - -0.41941140024074713, - 0.2577721492695315, - 0.035514719996982955 + 0.018692, + 0.096547, + 0.033048 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 494, "type": "imguizmo", "translation": [ - 0.010855326638166083, - -0.02099852366687309, - 0.0007894260906021531 + 0.000606, + 0.0012, + 0.000621 ], "rotation_deg": [ - -0.4199345864435396, - 0.272587638129886, - 0.01185793187236608 + 0.02019, + 0.097603, + 0.032231 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 495, "type": "imguizmo", "translation": [ - 0.01178190829517939, - -0.020996585774601267, - -0.001063667088979059 + 0.00065, + 0.001199, + 0.000582 ], "rotation_deg": [ - -0.41993458644353965, - 0.2874123618701137, - -0.011857931872366244 + 0.021674, + 0.098591, + 0.03139 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 496, "type": "imguizmo", "translation": [ - 0.012707827260692256, - -0.02096848875103222, - -0.0029114611326271113 + 0.000694, + 0.001197, + 0.000542 ], "rotation_deg": [ - -0.4194114002407471, - 0.3022278507304682, - -0.03551471999698313 + 0.023144, + 0.09951, + 0.030528 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 497, "type": "imguizmo", "translation": [ - 0.013631929954972716, - -0.020914267601558567, - -0.004744750423363354 + 0.000738, + 0.001194, + 0.000502 ], "rotation_deg": [ - -0.4183656796600215, - 0.3170156464567125, - -0.0589945755730197 + 0.024597, + 0.10036, + 0.029645 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 498, "type": "imguizmo", "translation": [ - 0.01455306506113662, - -0.020833989878980722, - -0.0065544016061585934 + 0.000781, + 0.00119, + 0.000462 ], "rotation_deg": [ - -0.4167987275389894, - 0.33175732529680646, - -0.08218052314106876 + 0.026033, + 0.10114, + 0.028741 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 499, "type": "imguizmo", "translation": [ - 0.01547008495954508, - -0.02072775559934451, - -0.008331399089848368 + 0.000824, + 0.001186, + 0.000421 ], "rotation_deg": [ - -0.41471249610487015, - 0.3464345209546075, - -0.10495705147663488 + 0.027451, + 0.101849, + 0.027817 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 500, "type": "imguizmo", "translation": [ - 0.016381847157595622, - -0.02059569711733365, - -0.010066889962354615 + 0.000865, + 0.00118, + 0.00038 ], "rotation_deg": [ - -0.41210958454224506, - 0.36102894747198855, - -0.12721068906124444 + 0.02885, + 0.102488, + 0.026874 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 501, "type": "imguizmo", "translation": [ - 0.017287215713125713, - -0.02043797896137229, - -0.011752228095448358 + 0.000907, + 0.001174, + 0.000339 ], "rotation_deg": [ - -0.4089932357547963, - 0.3755224220108646, - -0.14883056939184286 + 0.030228, + 0.103055, + 0.025911 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 502, "type": "imguizmo", "translation": [ - 0.018185062649655238, - -0.02025479762864306, - -0.013379017219325605 + 0.000948, + 0.001167, + 0.000298 ], "rotation_deg": [ - -0.40536733232505295, - 0.389896887506745, - -0.16970898331213613 + 0.031586, + 0.10355, + 0.024931 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 503, "type": "imguizmo", "translation": [ - 0.019074269361704702, - -0.020046381340276077, - -0.014939152752401128 + 0.000988, + 0.00116, + 0.000256 ], "rotation_deg": [ - -0.4012363916771767, - 0.40413443516558695, - -0.18974191561418893 + 0.032922, + 0.103973, + 0.023934 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 504, "type": "imguizmo", "translation": [ - 0.019953728008438486, - -0.01981298975701383, - -0.016424862177926446 + 0.001027, + 0.001151, + 0.000214 ], "rotation_deg": [ - -0.3966055604488136, - 0.4182173267759239, - -0.20882956323696059 + 0.034234, + 0.104323, + 0.022919 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 505, "type": "imguizmo", "translation": [ - 0.020822342893896405, - -0.019554913655706215, - -0.01782874376627699 + 0.001066, + 0.001142, + 0.000172 ], "rotation_deg": [ - -0.3914806080790248, - 0.43212801680846646, - -0.22687683248013177 + 0.035523, + 0.104601, + 0.021889 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 506, "type": "imguizmo", "translation": [ - 0.021679031832094537, - -0.019272474567038806, - -0.019143803449996985 + 0.001104, + 0.001132, + 0.00013 ], "rotation_deg": [ - -0.3858679196202838, - 0.44584917427564813, - -0.24379381275613818 + 0.036787, + 0.104806, + 0.020844 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 507, "type": "imguizmo", "translation": [ - 0.022522727495294187, - -0.018966024374945652, - -0.020363489667891654 + 0.001141, + 0.001121, + 8.8E-05 ], "rotation_deg": [ - -0.37977448778349576, - 0.45936370432387774, - -0.2594962245201947 + 0.038025, + 0.104938, + 0.019784 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 508, "type": "imguizmo", "translation": [ - 0.023352378743759325, - -0.018635944878205708, - -0.021481726004575527 + 0.001177, + 0.001109, + 4.6E-05 ], "rotation_deg": [ - -0.3732079042259514, - 0.4726547695315979, - -0.27390583914674815 + 0.039237, + 0.104997, + 0.01871 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 509, "type": "imguizmo", "translation": [ - 0.024166951935345824, - -0.018282647314769102, - -0.022492941462868236 + 0.001213, + 0.001096, + 4E-06 ], "rotation_deg": [ - -0.36617635009306626, - 0.4857058108866158, - -0.28695086866055464 + 0.040422, + 0.104982, + 0.017623 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 510, "type": "imguizmo", "translation": [ - 0.024965432213290846, - -0.017906571849405882, - -0.02339209821822225 + 0.001247, + 0.001083, + -3.8E-05 ], "rotation_deg": [ - -0.35868858582569313, - 0.4985005684165704, - -0.2985663233807612 + 0.041578, + 0.104895, + 0.016524 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 511, "type": "imguizmo", "translation": [ - 0.025746824770597965, - -0.01750818702531548, - -0.02417471671691134 + 0.001281, + 0.001069, + -8.1E-05 ], "rotation_deg": [ - -0.35075394024570317, - 0.5110231014468329, - -0.30869433569622684 + 0.042705, + 0.104734, + 0.015413 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 512, "type": "imguizmo", "translation": [ - 0.02651015608944292, - -0.017087989180380247, - -0.024836897992941982 + 0.001314, + 0.001054, + -0.000123 ], "rotation_deg": [ - -0.3423822989334353, - 0.5232578084606037, - -0.3172844483590508 + 0.043803, + 0.1045, + 0.014292 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 513, "type": "imguizmo", "translation": [ - 0.02725447515405544, - -0.01664650182879028, - -0.025375343092505692 + 0.001346, + 0.001039, + -0.000165 ], "rotation_deg": [ - -0.33358409191149485, - 0.5351894465364573, - -0.3242938658600424 + 0.04487, + 0.104194, + 0.013161 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 514, "type": "imguizmo", "translation": [ - 0.027978854635566573, - -0.016184275008809906, - -0.025787369509201064 + 0.001377, + 0.001023, + -0.000207 ], "rotation_deg": [ - -0.32437028065024354, - 0.546803150339126, - -0.3296876676337961 + 0.045906, + 0.103815, + 0.01202 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 515, "type": "imguizmo", "translation": [ - 0.028682392047344955, - -0.015701884597498515, - -0.02607092454814568 + 0.001407, + 0.001006, + -0.000249 ], "rotation_deg": [ - -0.3147523444111727, - 0.558084450639856, - -0.3334389820311916 + 0.04691, + 0.103363, + 0.010871 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 516, "type": "imguizmo", "translation": [ - 0.029364210869382772, - -0.015199931593239431, - -0.02622459555239876 + 0.001436, + 0.000988, + -0.00029 ], "rotation_deg": [ - -0.30474226594517284, - 0.5690192923432646, - -0.33552912019259784 + 0.047881, + 0.10284, + 0.009715 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 517, "type": "imguizmo", "translation": [ - 0.030023461640330554, - -0.014679041366970793, - -0.02624761694074701 + 0.001465, + 0.00097, + -0.000332 ], "rotation_deg": [ - -0.29435251656351796, - 0.5795940519982395, - -0.3359476691548319 + 0.048819, + 0.102245, + 0.008551 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 518, "type": "imguizmo", "translation": [ - 0.030659323015820188, - -0.014139862883051219, - -0.026139874021791683 + 0.001492, + 0.000951, + -0.000373 ], "rotation_deg": [ - -0.28359604060016497, - 0.5897955547710613, - -0.33469254372801743 + 0.049723, + 0.101578, + 0.007382 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 519, "type": "imguizmo", "translation": [ - 0.03127100279175782, - -0.013583067890731031, - -0.02590190356533539 + 0.001518, + 0.000931, + -0.000414 ], "rotation_deg": [ - -0.27248623928472504, - 0.5996110908596064, - -0.3317699968838963 + 0.050592, + 0.100841, + 0.006208 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 520, "type": "imguizmo", "translation": [ - 0.03185773889131139, - -0.013009350087236346, - -0.02553489112822191 + 0.001543, + 0.000911, + -0.000454 ], "rotation_deg": [ - -0.26103695404620103, - 0.609028431328178, - -0.3271945886038372 + 0.051426, + 0.100033, + 0.005029 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 521, "type": "imguizmo", "translation": [ - 0.03241880031436357, - -0.012419424253508709, - -0.025040665147951665 + 0.001567, + 0.00089, + -0.000495 ], "rotation_deg": [ - -0.24926244926828967, - 0.6180358433432367, - -0.32098911334174157 + 0.052224, + 0.099156, + 0.003847 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 522, "type": "imguizmo", "translation": [ - 0.03295348804824682, - -0.011814025363677046, - -0.024421687833498032 + 0.00159, + 0.000868, + -0.000535 ], "rotation_deg": [ - -0.23717739451773645, - 0.6266221047910504, - -0.31318448646322 + 0.052985, + 0.09821, + 0.002662 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 523, "type": "imguizmo", "translation": [ - 0.03346113593862615, - -0.01119390766937142, - -0.023681042898706112 + 0.001611, + 0.000846, + -0.000574 ], "rotation_deg": [ - -0.22479684626788166, - 0.6347765182590503, - -0.3038195902267969 + 0.05371, + 0.097195, + 0.001476 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 524, "type": "imguizmo", "translation": [ - 0.03394111151944449, - -0.010559843760019432, - -0.022822420199385476 + 0.001632, + 0.000824, + -0.000614 ], "rotation_deg": [ - -0.21213622914016989, - 0.6424889243634753, - -0.29294108007445324 + 0.054397, + 0.096113, + 0.000288 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 525, "type": "imguizmo", "translation": [ - 0.03439281680089658, - -0.00991262360029598, - -0.021850097350634104 + 0.001651, + 0.0008, + -0.000652 ], "rotation_deg": [ - -0.19921131668699182, - 0.6497497144066983, - -0.2806031521965626 + 0.055046, + 0.094963, + -0.0009 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 526, "type": "imguizmo", "translation": [ - 0.03481568901444986, - -0.00925305354592567, - -0.020768918415975144 + 0.00167, + 0.000776, + -0.000691 ], "rotation_deg": [ - -0.18603821173980326, - 0.6565498423484679, - -0.26686727352919587 + 0.055657, + 0.093747, + -0.002087 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 527, "type": "imguizmo", "translation": [ - 0.03520920131398396, - -0.008581955339063991, - -0.01958426977447615 + 0.001687, + 0.000752, + -0.000729 ], "rotation_deg": [ - -0.17263332634700132, - 0.6628808360761473, - -0.25180187552893857 + 0.05623, + 0.092466, + -0.003273 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 528, "type": "imguizmo", "translation": [ - 0.03557286343217538, - -0.007900165084508852, - -0.018302053286079374 + 0.001703, + 0.000727, + -0.000766 ], "rotation_deg": [ - -0.15901336132655555, - 0.6687348079599127, - -0.23548201325081478 + 0.056763, + 0.091121, + -0.004457 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 529, "type": "imguizmo", "translation": [ - 0.03590622229130951, - -0.007208532208018232, - -0.016928656888832595 + 0.001718, + 0.000702, + -0.000803 ], "rotation_deg": [ - -0.14519528545886976, - 0.6741044646797562, - -0.21798899142778044 + 0.057256, + 0.089712, + -0.005637 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 530, "type": "imguizmo", "translation": [ - 0.036208862567759015, - -0.006507918398031371, - -0.01547092277450295 + 0.001731, + 0.000676, + -0.000839 ], "rotation_deg": [ - -0.1311963143457936, - 0.6789831163120559, - -0.19940995941463213 + 0.05771, + 0.088241, + -0.006814 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 531, "type": "imguizmo", "translation": [ - 0.03648040720942541, - -0.005799196532112308, - -0.01393611330112178 + 0.001744, + 0.000649, + -0.000875 ], "rotation_deg": [ - -0.11703388896212674, - 0.683364684664387, - -0.17983747701430575 + 0.058123, + 0.086708, + -0.007986 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 532, "type": "imguizmo", "translation": [ - 0.03672051790549898, - -0.00508324958945313, - -0.01233187481228221 + 0.001755, + 0.000622, + -0.00091 ], "rotation_deg": [ - -0.10272565392633595, - 0.6872437108481928, - -0.15936905334959386 + 0.058496, + 0.085114, + -0.009152 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 533, "type": "imguizmo", "translation": [ - 0.03692889550795197, - -0.004360969550791845, - -0.010666199543439992 + 0.001765, + 0.000595, + -0.000944 ], "rotation_deg": [ - -0.08828943551755784, - 0.6906153620798798, - -0.1381066610776012 + 0.058828, + 0.083462, + -0.010312 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 534, "type": "imguizmo", "translation": [ - 0.03710528040423972, - -0.0036332562871154556, - -0.008947385804998487 + 0.001774, + 0.000567, + -0.000978 ], "rotation_deg": [ - -0.07374321946627463, - 0.6934754377018648, - -0.11615622836709592 + 0.059119, + 0.081751, + -0.011464 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 535, "type": "imguizmo", "translation": [ - 0.037249452840745594, - -0.0029010164385327884, - -0.007183996640543702 + 0.001781, + 0.000539, + -0.001011 ], "rotation_deg": [ - -0.05910512854633429, - 0.6958203744160707, - -0.0936271111697021 + 0.059369, + 0.079983, + -0.012609 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 536, "type": "imguizmo", "translation": [ - 0.037361233196566625, - -0.0021651622847137317, - -0.005384817166191684 + 0.001787, + 0.000511, + -0.001043 ], "rotation_deg": [ - -0.044393399996229294, - 0.6976472507233523, - -0.07063154841404697 + 0.059577, + 0.078159, + -0.013745 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 537, "type": "imguizmo", "translation": [ - 0.037440482207298814, - -0.0014266106083024978, - -0.0035588108035822306 + 0.001792, + 0.000482, + -0.001075 ], "rotation_deg": [ - -0.029626362797770804, - 0.6989537905633206, - -0.04728410283706784 + 0.059744, + 0.076281, + -0.014871 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 538, "type": "imguizmo", "translation": [ - 0.03748710113854317, - -0.000686281552720516, - -0.0017150746245629517 + 0.001796, + 0.000453, + -0.001106 ], "rotation_deg": [ - -0.014822414840458199, - 0.6997383661500307, - -0.023701090238217055 + 0.059869, + 0.07435, + -0.015987 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 539, "type": "imguizmo", "translation": [ - 0.037501031908916536, - 5.490252421766148e-05, - 0.00013720596996502697 + 0.001799, + 0.000423, + -0.001136 ], "rotation_deg": [ - -1.249000902703301e-16, - 0.6999999999999998, - -5.169475958410885e-16 + 0.059953, + 0.072367, + -0.017092 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 540, - "type": "action", - "action": "set_active_render_window", - "value": 0 - }, - { - "frame": 540, - "type": "action", - "action": "set_active_planar", - "value": 3 - }, - { - "frame": 540, - "type": "action", - "action": "set_projection_type", - "value": "perspective" + "type": "imguizmo", + "translation": [ + 0.0018, + 0.000393, + -0.001165 + ], + "rotation_deg": [ + 0.059995, + 0.070333, + -0.018185 + ], + "scale": [ + 1, + 1, + 1 + ] }, { "frame": 541, "type": "imguizmo", "translation": [ - 0.0, - 2.0, - 0.0 + 0.0018, + 0.000363, + -0.001194 ], "rotation_deg": [ - 0.0, - 150.0, - 0.0 + 0.059995, + 0.068251, + -0.019265 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 542, "type": "imguizmo", "translation": [ - 0.06670086678206116, - 1.999058118140111, - 0.0639929436431845 + 0.001799, + 0.000333, + -0.001222 ], "rotation_deg": [ - 1.270492700610689, - 150.0, - 0.0 + 0.059953, + 0.066121, + -0.020332 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 543, "type": "imguizmo", "translation": [ - 0.12798256324740281, - 1.996308996576746, - 0.12254764216862671 + 0.001796, + 0.000302, + -0.001249 ], "rotation_deg": [ - 2.5394025255231947, - 150.0, - 0.0 + 0.059869, + 0.063945, + -0.021384 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 544, "type": "imguizmo", "translation": [ - 0.18953169492323854, - 1.991825382877929, - 0.1809013693444348 + 0.001792, + 0.000272, + -0.001275 ], "rotation_deg": [ - 3.8051485711053474, - 150.0, - 0.0 + 0.059744, + 0.061724, + -0.022422 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 545, "type": "imguizmo", "translation": [ - 0.250810542864644, - 1.9856073172676207, - 0.23832109061063889 + 0.001787, + 0.000241, + -0.0013 ], "rotation_deg": [ - 5.066153875400059, - 150.0, - 0.0 + 0.059577, + 0.05946, + -0.023444 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 546, "type": "imguizmo", "translation": [ - 0.3117796441690647, - 1.9776629903453047, - 0.29455612934230735 + 0.001781, + 0.00021, + -0.001324 ], "rotation_deg": [ - 6.320847382823546, - 150.0, - 0.0 + 0.059369, + 0.057155, + -0.02445 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 547, "type": "imguizmo", "translation": [ - 0.37236008829548517, - 1.9680022642599442, - 0.34932349456213785 + 0.001774, + 0.000178, + -0.001347 ], "rotation_deg": [ - 7.5676659015049585, - 150.0, - 0.0 + 0.059119, + 0.05481, + -0.025439 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 548, "type": "imguizmo", "translation": [ - 0.43247663560487304, - 1.9566371779126996, - 0.40235056447893147 + 0.001765, + 0.000147, + -0.001369 ], "rotation_deg": [ - 8.805056050828796, - 150.0, - 0.0 + 0.058828, + 0.052427, + -0.02641 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 549, "type": "imguizmo", "translation": [ - 0.4920543694811803, - 1.9435818905587785, - 0.4533731427803099 + 0.001755, + 0.000115, + -0.001391 ], "rotation_deg": [ - 10.031476196753717, - 150.0, - 0.0 + 0.058496, + 0.050007, + -0.027362 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 550, "type": "imguizmo", "translation": [ - 0.5510190649975765, - 1.9288526674787003, - 0.5021370389970806 + 0.001744, + 8.4E-05, + -0.001411 ], "rotation_deg": [ - 11.245398372496588, - 150.0, - 0.0 + 0.058123, + 0.047553, + -0.028296 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 551, "type": "imguizmo", "translation": [ - 0.6092972593651071, - 1.9124678594488274, - 0.5483993137352393 + 0.001731, + 5.2E-05, + -0.001431 ], "rotation_deg": [ - 12.445310182188825, - 150.0, - 0.0 + 0.05771, + 0.045065, + -0.02921 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 552, "type": "imguizmo", "translation": [ - 0.6668163452202907, - 1.8944478798997806, - 0.5919294906783793 + 0.001718, + 2.1E-05, + -0.001449 ], "rotation_deg": [ - 13.62971668513332, - 150.0, - 0.0 + 0.057256, + 0.042546, + -0.030103 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 553, "type": "imguizmo", "translation": [ - 0.7235046609438818, - 1.8748151794821493, - 0.6325107046738623 + 0.001703, + -1.1E-05, + -0.001466 ], "rotation_deg": [ - 14.797142258314384, - 150.0, - 0.0 + 0.056763, + 0.039997, + -0.030975 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 554, "type": "imguizmo", "translation": [ - 0.7792915799536043, - 1.8535942180959133, - 0.6699407821549191 + 0.001687, + -4.3E-05, + -0.001483 ], "rotation_deg": [ - 15.946132434840287, - 150.0, - 0.0 + 0.05623, + 0.037421, + -0.031826 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 555, "type": "imguizmo", "translation": [ - 0.834107598695175, - 1.8308114344164415, - 0.7040332483583245 + 0.00167, + -7.4E-05, + -0.001498 ], "rotation_deg": [ - 17.07525571602788, - 150.0, - 0.0 + 0.055657, + 0.034818, + -0.032655 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 556, "type": "imguizmo", "translation": [ - 0.8878844232351503, - 1.806495212955196, - 0.7346182563323569 + 0.001651, + -0.000106, + -0.001512 ], "rotation_deg": [ - 18.1831053548717, - 150.0, - 0.0 + 0.055046, + 0.032191, + -0.033461 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 557, "type": "imguizmo", "translation": [ - 0.9405550543466554, - 1.7806758486961665, - 0.7615434331057618 + 0.001632, + -0.000137, + -0.001526 ], "rotation_deg": [ - 19.268301108675573, - 150.0, - 0.0 + 0.054397, + 0.029541, + -0.034244 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 558, "type": "imguizmo", "translation": [ - 0.9920538709820813, - 1.7533855093520891, - 0.7846746388022319 + 0.001611, + -0.000169, + -0.001538 ], "rotation_deg": [ - 20.329490958663122, - 150.0, - 0.0 + 0.05371, + 0.026871, + -0.035002 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 559, "type": "imguizmo", "translation": [ - 1.0423167120287367, - 1.724658195287486, - 0.8038966349185224 + 0.00159, + -0.0002, + -0.001549 ], "rotation_deg": [ - 21.365352794424826, - 150.0, - 0.0 + 0.052985, + 0.024183, + -0.035737 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 560, "type": "imguizmo", "translation": [ - 1.091280956245605, - 1.694529697158443, - 0.8191136584368774 + 0.001567, + -0.000231, + -0.001559 ], "rotation_deg": [ - 22.374596061102935, - 150.0, - 0.0 + 0.052224, + 0.021477, + -0.036446 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 561, "type": "imguizmo", "translation": [ - 1.1388856002816137, - 1.6630375513219087, - 0.8302498989115686 + 0.001543, + -0.000262, + -0.001568 ], "rotation_deg": [ - 23.355963367262138, - 150.0, - 0.0 + 0.051426, + 0.018757, + -0.03713 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 562, "type": "imguizmo", "translation": [ - 1.1850713346782122, - 1.630220993070069, - 0.8372498761527203 + 0.001518, + -0.000293, + -0.001576 ], "rotation_deg": [ - 24.308232051442694, - 150.0, - 0.0 + 0.050592, + 0.016023, + -0.037788 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 563, "type": "imguizmo", "translation": [ - 1.2297806177615729, - 1.5961209077480583, - 0.840078716625827 + 0.001492, + -0.000324, + -0.001583 ], "rotation_deg": [ - 25.230215705444387, - 150.0, - 0.0 + 0.049723, + 0.013278, + -0.03842 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 564, "type": "imguizmo", "translation": [ - 1.272957747332351, - 1.56077977981591, - 0.8387223271899468 + 0.001465, + -0.000354, + -0.001588 ], "rotation_deg": [ - 26.120765652443374, - 150.0, - 0.0 + 0.048819, + 0.010524, + -0.039025 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 565, "type": "imguizmo", "translation": [ - 1.3145489300636883, - 1.5242416399182106, - 0.8331874653090205 + 0.001436, + -0.000384, + -0.001593 ], "rotation_deg": [ - 26.978772378100505, - 150.0, - 0.0 + 0.047881, + 0.007763, + -0.039603 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 566, "type": "imguizmo", "translation": [ - 1.3545023485209997, - 1.4865520100273983, - 0.8235017053865271 + 0.001407, + -0.000414, + -0.001596 ], "rotation_deg": [ - 27.803166912878, - 150.0, - 0.0 + 0.04691, + 0.004996, + -0.040153 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 567, "type": "imguizmo", "translation": [ - 1.3927682257200455, - 1.4477578467290564, - 0.8097133013911916 + 0.001377, + -0.000444, + -0.001599 ], "rotation_deg": [ - 28.592922163842395, - 150.0, - 0.0 + 0.045906, + 0.002226, + -0.040676 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 568, "type": "imguizmo", "translation": [ - 1.429298887142854, - 1.4079074827198508, - 0.7918909464581413 + 0.001346, + -0.000473, + -0.0016 ], "rotation_deg": [ - 29.347054194294437, - 150.0, - 0.0 + 0.04487, + -0.000546, + -0.041169 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 569, "type": "imguizmo", "translation": [ - 1.4640488201342348, - 1.367050566591013, - 0.770123430663161 + 0.001314, + -0.000502, + -0.0016 ], "rotation_deg": [ - 30.06462344963168, - 150.0, - 0.0 + 0.043803, + -0.003318, + -0.041635 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 570, "type": "imguizmo", "translation": [ - 1.49697473060488, - 1.3252380009723737, - 0.7445191986750015 + 0.001281, + -0.000531, + -0.001599 ], "rotation_deg": [ - 30.744735927916555, - 150.0, - 0.0 + 0.042705, + -0.006087, + -0.042071 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 571, "type": "imguizmo", "translation": [ - 1.5280355969704076, - 1.2825218791140283, - 0.7152058094894966 + 0.001247, + -0.000559, + -0.001597 ], "rotation_deg": [ - 31.386544293691394, - 150.0, - 0.0 + 0.041578, + -0.008852, + -0.042478 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 572, "type": "imguizmo", "translation": [ - 1.5571927212591483, - 1.2389554199846315, - 0.6823293009370596 + 0.001213, + -0.000587, + -0.001594 ], "rotation_deg": [ - 31.989248933652974, - 150.0, - 0.0 + 0.040422, + -0.01161, + -0.042855 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 573, "type": "imguizmo", "translation": [ - 1.5844097773249985, - 1.1945929019671908, - 0.6460534621295606 + 0.001177, + -0.000614, + -0.001589 ], "rotation_deg": [ - 32.55209895287106, - 150.0, - 0.0 + 0.039237, + -0.014361, + -0.043203 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 574, "type": "imguizmo", "translation": [ - 1.609652856105276, - 1.1494895952349595, - 0.606559017471208 + 0.001141, + -0.000641, + -0.001584 ], "rotation_deg": [ - 33.07439311031002, - 150.0, - 0.0 + 0.038025, + -0.017102, + -0.04352 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 575, "type": "imguizmo", "translation": [ - 1.6328905078671885, - 1.103701692891684, - 0.5640427262986593 + 0.001104, + -0.000668, + -0.001577 ], "rotation_deg": [ - 33.555480692487826, - 150.0, - 0.0 + 0.036787, + -0.01983, + -0.043807 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 576, "type": "imguizmo", "translation": [ - 1.6540937813902852, - 1.0572862409619932, - 0.518716402635901 + 0.001066, + -0.000694, + -0.001569 ], "rotation_deg": [ - 33.99476232418401, - 150.0, - 0.0 + 0.035523, + -0.022545, + -0.044063 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 577, "type": "imguizmo", "translation": [ - 1.6732362600360713, - 1.010301067319156, - 0.4708058599474386 + 0.001027, + -0.000719, + -0.001561 ], "rotation_deg": [ - 34.391690715186556, - 150.0, - 0.0 + 0.034234, + -0.025244, + -0.044289 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 578, "type": "imguizmo", "translation": [ - 1.6902940946598521, - 0.9628047096387508, - 0.4205497861469717 + 0.000988, + -0.000744, + -0.001551 ], "rotation_deg": [ - 34.745771342147385, - 150.0, - 0.0 + 0.032922, + -0.027926, + -0.044484 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 579, "type": "imguizmo", "translation": [ - 1.7052460333237944, - 0.914856342468014, - 0.3681985544662176 + 0.000948, + -0.000769, + -0.00154 ], "rotation_deg": [ - 35.05656306469681, - 150.0, - 0.0 + 0.031586, + -0.030588, + -0.044648 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 580, "type": "imguizmo", "translation": [ - 1.7180734477741986, - 0.8665157035017191, - 0.3140129761080622 + 0.000907, + -0.000793, + -0.001528 ], "rotation_deg": [ - 35.323678675049564, - 150.0, - 0.0 + 0.030228, + -0.033229, + -0.044781 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 581, "type": "imguizmo", "translation": [ - 1.728760356649979, - 0.817843019156451, - 0.25826300089826887 + 0.000865, + -0.000817, + -0.001515 ], "rotation_deg": [ - 35.54678538041743, - 150.0, - 0.0 + 0.02885, + -0.035847, + -0.044882 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 582, "type": "imguizmo", "translation": [ - 1.7372934453934528, - 0.7688989295359886, - 0.20122637240902386 + 0.000824, + -0.000839, + -0.001501 ], "rotation_deg": [ - 35.725605217627646, - 150.0, - 0.0 + 0.027451, + -0.038439, + -0.044952 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 583, "type": "imguizmo", "translation": [ - 1.7436620828386202, - 0.7197444128812878, - 0.1431872442544314 + 0.000781, + -0.000862, + -0.001486 ], "rotation_deg": [ - 35.859915399430406, - 150.0, - 0.0 + 0.026033, + -0.041005, + -0.044991 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 584, "type": "imguizmo", "translation": [ - 1.7478583344562715, - 0.6704407095991844, - 0.08443476445150004 + 0.000738, + -0.000884, + -0.001469 ], "rotation_deg": [ - 35.949548592064026, - 150.0, - 0.0 + 0.024597, + -0.043542, + -0.044999 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 585, "type": "imguizmo", "translation": [ - 1.7498769722394254, - 0.6210492459644767, - 0.025261634899269386 + 0.000694, + -0.000905, + -0.001452 ], "rotation_deg": [ - 35.99439312373195, - 150.0, - 0.0 + 0.023144, + -0.046049, + -0.044975 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 586, "type": "imguizmo", "translation": [ - 1.7497154812167732, - 0.5716315575904323, - -0.034037346847331035 + 0.00065, + -0.000925, + -0.001434 ], "rotation_deg": [ - 35.99439312373195, - 150.0, - 0.0 + 0.021674, + -0.048524, + -0.04492 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 587, "type": "imguizmo", "translation": [ - 1.7473740625860192, - 0.5222492127630796, - -0.09316675624406856 + 0.000606, + -0.000945, + -0.001415 ], "rotation_deg": [ - 35.949548592064026, - 150.0, - 0.0 + 0.02019, + -0.050965, + -0.044834 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 588, "type": "imguizmo", "translation": [ - 1.7428556334632148, - 0.472963735734789, - -0.15183201354762751 + 0.000561, + -0.000964, + -0.001395 ], "rotation_deg": [ - 35.85991539943041, - 150.0, - 0.0 + 0.018692, + -0.053371, + -0.044716 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 589, "type": "imguizmo", "translation": [ - 1.7361658232483939, - 0.42383653007271394, - -0.20974085139707527 + 0.000515, + -0.000983, + -0.001373 ], "rotation_deg": [ - 35.725605217627646, - 150.0, - 0.0 + 0.01718, + -0.055739, + -0.044567 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 590, "type": "imguizmo", "translation": [ - 1.7273129666120435, - 0.37492880215759616, - -0.2666047708751479 + 0.00047, + -0.001001, + -0.001351 ], "rotation_deg": [ - 35.54678538041743, - 150.0, - 0.0 + 0.015657, + -0.058068, + -0.044387 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 591, "type": "imguizmo", "translation": [ - 1.7163080931111387, - 0.3263014849282339, - -0.32214047879534785 + 0.000424, + -0.001018, + -0.001328 ], "rotation_deg": [ - 35.323678675049564, - 150.0, - 0.0 + 0.014122, + -0.060358, + -0.044176 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 592, "type": "imguizmo", "translation": [ - 1.7031649134476914, - 0.27801516196662945, - -0.3760712990543473 + 0.000377, + -0.001034, + -0.001304 ], "rotation_deg": [ - 35.05656306469682, - 150.0, - 0.0 + 0.012578, + -0.062604, + -0.043935 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 593, "type": "imguizmo", "translation": [ - 1.6878998023869225, - 0.23012999201838824, - -0.42812855101841907 + 0.000331, + -0.00105, + -0.001279 ], "rotation_deg": [ - 34.745771342147385, - 150.0, - 0.0 + 0.011025, + -0.064808, + -0.043663 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 594, "type": "imguizmo", "translation": [ - 1.6705317783563403, - 0.18270563404241544, - -0.47805288807683727 + 0.000284, + -0.001065, + -0.001253 ], "rotation_deg": [ - 34.391690715186556, - 150.0, - 0.0 + 0.009464, + -0.066966, + -0.04336 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 595, "type": "imguizmo", "translation": [ - 1.6510824797511532, - 0.13580117288328034, - -0.5255955896936473 + 0.000237, + -0.001079, + -0.001227 ], "rotation_deg": [ - 33.99476232418401, - 150.0, - 0.0 + 0.007897, + -0.069077, + -0.043027 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 596, "type": "imguizmo", "translation": [ - 1.6295761379755185, - 0.08947504565885875, - -0.5705198005208638 + 0.00019, + -0.001093, + -0.001199 ], "rotation_deg": [ - 33.555480692487826, - 150.0, - 0.0 + 0.006324, + -0.071141, + -0.042664 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 597, "type": "imguizmo", "translation": [ - 1.6060395472532345, - 0.04378496895495825, - -0.6126017103999037 + 0.000142, + -0.001105, + -0.001171 ], "rotation_deg": [ - 33.07439311031002, - 150.0, - 0.0 + 0.004747, + -0.073154, + -0.042272 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 598, "type": "imguizmo", "translation": [ - 1.5805020312454716, - -0.0012121330823565701, - -0.6516316693725335 + 9.5E-05, + -0.001117, + -0.001142 ], "rotation_deg": [ - 32.552098952871056, - 150.0, - 0.0 + 0.003167, + -0.075117, + -0.04185 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 599, "type": "imguizmo", "translation": [ - 1.5529954065171427, - -0.04546019966716389, - -0.6874152321464173 + 4.8E-05, + -0.001128, + -0.001112 ], "rotation_deg": [ - 31.989248933652966, - 150.0, - 0.0 + 0.001584, + -0.077028, + -0.041398 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 600, "type": "imguizmo", "translation": [ - 1.5235539428974265, - -0.08890410321844325, - -0.7197741268117835 + 0.0, + -0.001139, + -0.001081 ], "rotation_deg": [ - 31.386544293691394, - 150.0, - 0.0 + 0.0, + -0.078884, + -0.040918 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 601, "type": "imguizmo", "translation": [ - 1.4922143207838245, - -0.13148971804217752, - -0.748547142983112 + -4.8E-05, + -0.001148, + -0.001049 ], "rotation_deg": [ - 30.74473592791655, - 150.0, - 0.0 + -0.001584, + -0.080686, + -0.04041 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 602, "type": "imguizmo", "translation": [ - 1.459015585442957, - -0.17316398776522515, - -0.7735909349411636 + -9.5E-05, + -0.001157, + -0.001017 ], "rotation_deg": [ - 30.064623449631675, - 150.0, - 0.0 + -0.003167, + -0.082432, + -0.039873 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 603, "type": "imguizmo", "translation": [ - 1.4239990983650208, - -0.21387499143695599, - -0.794780735774144 + -0.000142, + -0.001165, + -0.000984 ], "rotation_deg": [ - 29.34705419429443, - 150.0, - 0.0 + -0.004747, + -0.08412, + -0.039309 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 604, "type": "imguizmo", "translation": [ - 1.387208485732524, - -0.25357200821628967, - -0.8120109789601824 + -0.00019, + -0.001172, + -0.00095 ], "rotation_deg": [ - 28.5929221638424, - 150.0, - 0.0 + -0.006324, + -0.085749, + -0.038717 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 605, "type": "imguizmo", "translation": [ - 1.3486895840674933, - -0.2922055805635503, - -0.8251958242944343 + -0.000237, + -0.001179, + -0.000916 ], "rotation_deg": [ - 27.803166912878005, - 150.0, - 0.0 + -0.007897, + -0.087319, + -0.038098 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 606, "type": "imguizmo", "translation": [ - 1.308490383124877, - -0.3297275758583974, - -0.8342695855406619 + -0.000284, + -0.001184, + -0.000881 ], "rotation_deg": [ - 26.97877237810051, - 150.0, - 0.0 + -0.009464, + -0.088828, + -0.037452 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 607, "type": "imguizmo", "translation": [ - 1.266660966103287, - -0.366091246367081, - -0.8391870576767604 + -0.000331, + -0.001189, + -0.000846 ], "rotation_deg": [ - 26.120765652443374, - 150.0, - 0.0 + -0.011025, + -0.090275, + -0.036781 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 608, "type": "imguizmo", "translation": [ - 1.2232534472475676, - -0.4012512874842952, - -0.8399237421039044 + -0.000377, + -0.001193, + -0.000809 ], "rotation_deg": [ - 25.230215705444394, - 150.0, - 0.0 + -0.012578, + -0.091659, + -0.036083 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 609, "type": "imguizmo", "translation": [ - 1.1783219069209365, - -0.4351638941770763, - -0.8364759686973339 + -0.000424, + -0.001196, + -0.000773 ], "rotation_deg": [ - 24.3082320514427, - 150.0, - 0.0 + -0.014122, + -0.092979, + -0.035361 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 610, "type": "imguizmo", "translation": [ - 1.1319223242275875, - -0.4677868155604163, - -0.8288609140907326 + -0.00047, + -0.001198, + -0.000735 ], "rotation_deg": [ - 23.35596336726214, - 150.0, - 0.0 + -0.015657, + -0.094234, + -0.034614 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 611, "type": "imguizmo", "translation": [ - 1.084112507269697, - -0.49907940753660696, - -0.8171165161031011 + -0.000515, + -0.001199, + -0.000698 ], "rotation_deg": [ - 22.374596061102938, - 150.0, - 0.0 + -0.01718, + -0.095424, + -0.033843 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 612, "type": "imguizmo", "translation": [ - 1.0349520211257281, - -0.5290026834327227, - -0.8013012847344534 + -0.000561, + -0.0012, + -0.000659 ], "rotation_deg": [ - 21.365352794424837, - 150.0, - 0.0 + -0.018692, + -0.096547, + -0.033048 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 613, "type": "imguizmo", "translation": [ - 0.9845021136397563, - -0.5575193625731629, - -0.7814940106719371 + -0.000606, + -0.0012, + -0.000621 ], "rotation_deg": [ - 20.329490958663133, - 150.0, - 0.0 + -0.02019, + -0.097603, + -0.032231 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 614, "type": "imguizmo", "translation": [ - 0.9328256391142874, - -0.5845939167267277, - -0.757793372758596 + -0.00065, + -0.001199, + -0.000582 ], "rotation_deg": [ - 19.268301108675576, - 150.0, - 0.0 + -0.021674, + -0.098591, + -0.03139 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 615, "type": "imguizmo", "translation": [ - 0.8799869800016216, - -0.6101926143703722, - -0.7303174463803356 + -0.000694, + -0.001197, + -0.000542 ], "rotation_deg": [ - 18.183105354871703, - 150.0, - 0.0 + -0.023144, + -0.09951, + -0.030528 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 616, "type": "imguizmo", "translation": [ - 0.8260519666913348, - -0.6342835627144832, - -0.699203115220292 + -0.000738, + -0.001194, + -0.000502 ], "rotation_deg": [ - 17.075255716027893, - 150.0, - 0.0 + -0.024597, + -0.10036, + -0.029645 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 617, "type": "imguizmo", "translation": [ - 0.771087795493809, - -0.6568367474373251, - -0.6646053893112055 + -0.000781, + -0.00119, + -0.000462 ], "rotation_deg": [ - 15.946132434840298, - 150.0, - 0.0 + -0.026033, + -0.10114, + -0.028741 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 618, "type": "imguizmo", "translation": [ - 0.7151629449220009, - -0.6778240700791444, - -0.6266966327832368 + -0.000824, + -0.001186, + -0.000421 ], "rotation_deg": [ - 14.797142258314391, - 150.0, - 0.0 + -0.027451, + -0.101849, + -0.027817 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 619, "type": "imguizmo", "translation": [ - 0.6583470903757396, - -0.6972193830493539, - -0.58566570515454 + -0.000865, + -0.00118, + -0.00038 ], "rotation_deg": [ - 13.629716685133324, - 150.0, - 0.0 + -0.02885, + -0.102488, + -0.026874 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 620, "type": "imguizmo", "translation": [ - 0.6007110173348545, - -0.7149985222031738, - -0.541717020442643 + -0.000907, + -0.001174, + -0.000339 ], "rotation_deg": [ - 12.445310182188827, - 150.0, - 0.0 + -0.030228, + -0.103055, + -0.025911 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 621, "type": "imguizmo", "translation": [ - 0.5423265331692826, - -0.7311393369471475, - -0.4950695287840944 + -0.000948, + -0.001167, + -0.000298 ], "rotation_deg": [ - 11.245398372496588, - 150.0, - 0.0 + -0.031586, + -0.10355, + -0.024931 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 622, "type": "imguizmo", "translation": [ - 0.48326637767602754, - -0.7456217178360217, - -0.44595562563589697 + -0.000988, + -0.00116, + -0.000256 ], "rotation_deg": [ - 10.031476196753715, - 150.0, - 0.0 + -0.032922, + -0.103973, + -0.023934 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 623, "type": "imguizmo", "translation": [ - 0.4236041324544294, - -0.7584276216266125, - -0.3946199939930307 + -0.001027, + -0.001151, + -0.000214 ], "rotation_deg": [ - 8.805056050828789, - 150.0, - 0.0 + -0.034234, + -0.104323, + -0.022919 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 624, "type": "imguizmo", "translation": [ - 0.3634141292326564, - -0.7695410937574385, - -0.34131838539008047 + -0.001066, + -0.001142, + -0.000172 ], "rotation_deg": [ - 7.567665901504963, - 150.0, - 0.0 + -0.035523, + -0.104601, + -0.021889 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 625, "type": "imguizmo", "translation": [ - 0.3027713572596239, - -0.7789482882261183, - -0.2863163457599523 + -0.001104, + -0.001132, + -0.00013 ], "rotation_deg": [ - 6.320847382823548, - 150.0, - 0.0 + -0.036787, + -0.104806, + -0.020844 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 626, "type": "imguizmo", "translation": [ - 0.2417513698777332, - -0.7866374848397648, - -0.2298878924973977 + -0.001141, + -0.001121, + -8.8E-05 ], "rotation_deg": [ - 5.066153875400057, - 150.0, - 0.0 + -0.038025, + -0.104938, + -0.019784 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 627, "type": "imguizmo", "translation": [ - 0.18043019039281202, - -0.7925991038168868, - -0.1723141493181332 + -0.001177, + -0.001109, + -4.6E-05 ], "rotation_deg": [ - 3.805148571105343, - 150.0, - 0.0 + -0.039237, + -0.104997, + -0.01871 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 628, "type": "imguizmo", "translation": [ - 0.11888421735854332, - -0.7968257177226031, - -0.11388194571463153 + -0.001213, + -0.001096, + -4E-06 ], "rotation_deg": [ - 2.5394025255232027, - 150.0, - 0.0 + -0.040422, + -0.104982, + -0.017623 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 629, "type": "imguizmo", "translation": [ - 0.0571901293933781, - -0.7993120607223021, - -0.05488238798601452 + -0.001247, + -0.001083, + 3.8E-05 ], "rotation_deg": [ - 1.2704927006106939, - 150.0, - 0.0 + -0.041578, + -0.104895, + -0.016524 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 630, "type": "imguizmo", "translation": [ - -0.004575210351470023, - -0.8000550351422152, - 0.004390591038880794 + -0.001281, + -0.001069, + 8.1E-05 ], "rotation_deg": [ - 1.9984014443252818e-15, - 150.0, - 0.0 + -0.042705, + -0.104734, + -0.015413 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 631, "type": "imguizmo", "translation": [ - -0.06633484995394351, - -0.7990537153287331, - 0.06364169636007383 + -0.001314, + -0.001054, + 0.000123 ], "rotation_deg": [ - -1.27049270061069, - 150.0, - 0.0 + -0.043803, + -0.1045, + -0.014292 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 632, "type": "imguizmo", "translation": [ - -0.12801184459365234, - -0.7963093488016555, - 0.1225757419512755 + -0.001346, + -0.001039, + 0.000165 ], "rotation_deg": [ - -2.5394025255231987, - 150.0, - 0.0 + -0.04487, + -0.104194, + -0.013161 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 633, "type": "imguizmo", "translation": [ - -0.1895293524155388, - -0.7918253546999356, - 0.180899121361823 + -0.001377, + -0.001023, + 0.000207 ], "rotation_deg": [ - -3.805148571105356, - 150.0, - 0.0 + -0.045906, + -0.103815, + -0.01202 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 634, "type": "imguizmo", "translation": [ - -0.25081073026526046, - -0.7856073195218594, - 0.2383212704492481 + -0.001407, + -0.001006, + 0.000249 ], "rotation_deg": [ - -5.066153875400071, - 150.0, - 0.0 + -0.04691, + -0.103363, + -0.010871 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 635, "type": "imguizmo", "translation": [ - -0.31177962917701424, - -0.7776629901649649, - 0.29455611495521744 + -0.001436, + -0.000988, + 0.00029 ], "rotation_deg": [ - -6.320847382823529, - 150.0, - 0.0 + -0.047881, + -0.10284, + -0.009715 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 636, "type": "imguizmo", "translation": [ - -0.3723600894948483, - -0.7680022642743706, - 0.34932349571310417 + -0.001465, + -0.00097, + 0.000332 ], "rotation_deg": [ - -7.567665901504945, - 150.0, - 0.0 + -0.048819, + -0.102245, + -0.008551 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 637, "type": "imguizmo", "translation": [ - -0.4324766355089231, - -0.7566371779115448, - 0.4023505643868534 + -0.001492, + -0.000951, + 0.000373 ], "rotation_deg": [ - -8.805056050828787, - 150.0, - 0.0 + -0.049723, + -0.101578, + -0.007382 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 638, "type": "imguizmo", "translation": [ - -0.49205436948885567, - -0.7435818905588701, - 0.4533731427876755 + -0.001518, + -0.000931, + 0.000414 ], "rotation_deg": [ - -10.03147619675371, - 150.0, - 0.0 + -0.050592, + -0.100841, + -0.006208 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 639, "type": "imguizmo", "translation": [ - -0.5510190649969621, - -0.728852667478692, - 0.5021370389964912 + -0.001543, + -0.000911, + 0.000454 ], "rotation_deg": [ - -11.245398372496584, - 150.0, - 0.0 + -0.051426, + -0.100033, + -0.005029 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 640, "type": "imguizmo", "translation": [ - -0.6092972593651562, - -0.7124678594488271, - 0.5483993137352863 + -0.001567, + -0.00089, + 0.000495 ], "rotation_deg": [ - -12.445310182188827, - 150.0, - 0.0 + -0.052224, + -0.099156, + -0.003847 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 641, "type": "imguizmo", "translation": [ - -0.6668163452202867, - -0.6944478798997795, - 0.5919294906783755 + -0.00159, + -0.000868, + 0.000535 ], "rotation_deg": [ - -13.629716685133323, - 150.0, - 0.0 + -0.052985, + -0.09821, + -0.002662 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 642, "type": "imguizmo", "translation": [ - -0.7235046609438823, - -0.6748151794821482, - 0.6325107046738627 + -0.001611, + -0.000846, + 0.000574 ], "rotation_deg": [ - -14.79714225831439, - 150.0, - 0.0 + -0.05371, + -0.097195, + -0.001476 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 643, "type": "imguizmo", "translation": [ - -0.7792915799536041, - -0.6535942180959124, - 0.6699407821549189 + -0.001632, + -0.000824, + 0.000614 ], "rotation_deg": [ - -15.946132434840282, - 150.0, - 0.0 + -0.054397, + -0.096113, + -0.000288 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 644, "type": "imguizmo", "translation": [ - -0.834107598695175, - -0.6308114344164406, - 0.7040332483583246 + -0.001651, + -0.0008, + 0.000652 ], "rotation_deg": [ - -17.075255716027876, - 150.0, - 0.0 + -0.055046, + -0.094963, + 0.0009 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 645, "type": "imguizmo", "translation": [ - -0.8878844232351503, - -0.6064952129551954, - 0.7346182563323569 + -0.00167, + -0.000776, + 0.000691 ], "rotation_deg": [ - -18.1831053548717, - 150.0, - 0.0 + -0.055657, + -0.093747, + 0.002087 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 646, "type": "imguizmo", "translation": [ - -0.9405550543466555, - -0.5806758486961653, - 0.761543433105762 + -0.001687, + -0.000752, + 0.000729 ], "rotation_deg": [ - -19.268301108675576, - 150.0, - 0.0 + -0.05623, + -0.092466, + 0.003273 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 647, "type": "imguizmo", "translation": [ - -0.9920538709820816, - -0.5533855093520879, - 0.7846746388022323 + -0.001703, + -0.000727, + 0.000766 ], "rotation_deg": [ - -20.32949095866313, - 150.0, - 0.0 + -0.056763, + -0.091121, + 0.004457 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 648, "type": "imguizmo", "translation": [ - -1.042316712028737, - -0.5246581952874846, - 0.8038966349185228 + -0.001718, + -0.000702, + 0.000803 ], "rotation_deg": [ - -21.365352794424837, - 150.0, - 0.0 + -0.057256, + -0.089712, + 0.005637 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 649, "type": "imguizmo", "translation": [ - -1.0912809562456054, - -0.4945296971584412, - 0.8191136584368779 + -0.001731, + -0.000676, + 0.000839 ], "rotation_deg": [ - -22.374596061102945, - 150.0, - 0.0 + -0.05771, + -0.088241, + 0.006814 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 650, "type": "imguizmo", "translation": [ - -1.1388856002816141, - -0.4630375513219068, - 0.830249898911569 + -0.001744, + -0.000649, + 0.000875 ], "rotation_deg": [ - -23.35596336726215, - 150.0, - 0.0 + -0.058123, + -0.086708, + 0.007986 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 651, "type": "imguizmo", "translation": [ - -1.1850713346782116, - -0.43022099307006806, - 0.8372498761527206 + -0.001755, + -0.000622, + 0.00091 ], "rotation_deg": [ - -24.308232051442683, - 150.0, - 0.0 + -0.058496, + -0.085114, + 0.009152 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 652, "type": "imguizmo", "translation": [ - -1.2297806177615727, - -0.3961209077480571, - 0.8400787166258273 + -0.001765, + -0.000595, + 0.000944 ], "rotation_deg": [ - -25.23021570544438, - 150.0, - 0.0 + -0.058828, + -0.083462, + 0.010312 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 653, "type": "imguizmo", "translation": [ - -1.272957747332351, - -0.3607797798159086, - 0.8387223271899472 + -0.001774, + -0.000567, + 0.000978 ], "rotation_deg": [ - -26.120765652443367, - 150.0, - 0.0 + -0.059119, + -0.081751, + 0.011464 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 654, "type": "imguizmo", "translation": [ - -1.3145489300636881, - -0.324241639918209, - 0.8331874653090208 + -0.001781, + -0.000539, + 0.001011 ], "rotation_deg": [ - -26.9787723781005, - 150.0, - 0.0 + -0.059369, + -0.079983, + 0.012609 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 655, "type": "imguizmo", "translation": [ - -1.3545023485210004, - -0.28655201002739644, - 0.8235017053865272 + -0.001787, + -0.000511, + 0.001043 ], "rotation_deg": [ - -27.80316691287801, - 150.0, - 0.0 + -0.059577, + -0.078159, + 0.013745 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 656, "type": "imguizmo", "translation": [ - -1.392768225720046, - -0.24775784672905432, - 0.8097133013911917 + -0.001792, + -0.000482, + 0.001075 ], "rotation_deg": [ - -28.592922163842402, - 150.0, - 0.0 + -0.059744, + -0.076281, + 0.014871 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 657, "type": "imguizmo", "translation": [ - -1.4292988871428547, - -0.20790748271984888, - 0.7918909464581413 + -0.001796, + -0.000453, + 0.001106 ], "rotation_deg": [ - -29.347054194294444, - 150.0, - 0.0 + -0.059869, + -0.07435, + 0.015987 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 658, "type": "imguizmo", "translation": [ - -1.4640488201342357, - -0.16705056659101067, - 0.7701234306631608 + -0.001799, + -0.000423, + 0.001136 ], "rotation_deg": [ - -30.064623449631696, - 150.0, - 0.0 + -0.059953, + -0.072367, + 0.017092 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 659, "type": "imguizmo", "translation": [ - -1.4969747306048795, - -0.1252380009723725, - 0.7445191986750022 + -0.0018, + -0.000393, + 0.001165 ], "rotation_deg": [ - -30.74473592791655, - 150.0, - 0.0 + -0.059995, + -0.070333, + 0.018185 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 660, "type": "imguizmo", "translation": [ - -1.5280355969704076, - -0.08252187911402695, - 0.7152058094894969 + -0.0018, + -0.000363, + 0.001194 ], "rotation_deg": [ - -31.386544293691394, - 150.0, - 0.0 + -0.059995, + -0.068251, + 0.019265 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 661, "type": "imguizmo", "translation": [ - -1.5571927212591483, - -0.03895541998463006, - 0.6823293009370599 + -0.001799, + -0.000333, + 0.001222 ], "rotation_deg": [ - -31.989248933652974, - 150.0, - 0.0 + -0.059953, + -0.066121, + 0.020332 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 662, "type": "imguizmo", "translation": [ - -1.5844097773249985, - 0.005407098032810788, - 0.6460534621295605 + -0.001796, + -0.000302, + 0.001249 ], "rotation_deg": [ - -32.55209895287106, - 150.0, - 0.0 + -0.059869, + -0.063945, + 0.021384 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 663, "type": "imguizmo", "translation": [ - -1.609652856105276, - 0.050510404765042045, - 0.606559017471208 + -0.001792, + -0.000272, + 0.001275 ], "rotation_deg": [ - -33.07439311031003, - 150.0, - 0.0 + -0.059744, + -0.061724, + 0.022422 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 664, "type": "imguizmo", "translation": [ - -1.6328905078671887, - 0.09629830710831769, - 0.5640427262986589 + -0.001787, + -0.000241, + 0.0013 ], "rotation_deg": [ - -33.55548069248783, - 150.0, - 0.0 + -0.059577, + -0.05946, + 0.023444 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 665, "type": "imguizmo", "translation": [ - -1.6540937813902852, - 0.1427137590380087, - 0.5187164026359008 + -0.001781, + -0.00021, + 0.001324 ], "rotation_deg": [ - -33.994762324184016, - 150.0, - 0.0 + -0.059369, + -0.057155, + 0.02445 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 666, "type": "imguizmo", "translation": [ - -1.6732362600360717, - 0.18969893268084637, - 0.470805859947438 + -0.001774, + -0.000178, + 0.001347 ], "rotation_deg": [ - -34.39169071518656, - 150.0, - 0.0 + -0.059119, + -0.05481, + 0.025439 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 667, "type": "imguizmo", "translation": [ - -1.690294094659852, - 0.2371952903612502, - 0.42054978614697236 + -0.001765, + -0.000147, + 0.001369 ], "rotation_deg": [ - -34.745771342147385, - 150.0, - 0.0 + -0.058828, + -0.052427, + 0.02641 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 668, "type": "imguizmo", "translation": [ - -1.7052460333237944, - 0.28514365753198745, - 0.36819855446621796 + -0.001755, + -0.000115, + 0.001391 ], "rotation_deg": [ - -35.05656306469681, - 150.0, - 0.0 + -0.058496, + -0.050007, + 0.027362 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 669, "type": "imguizmo", "translation": [ - -1.7180734477741983, - 0.3334842964982822, - 0.3140129761080625 + -0.001744, + -8.4E-05, + 0.001411 ], "rotation_deg": [ - -35.32367867504956, - 150.0, - 0.0 + -0.058123, + -0.047553, + 0.028296 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 670, "type": "imguizmo", "translation": [ - -1.7287603566499787, - 0.38215698084355026, - 0.25826300089826915 + -0.001731, + -5.2E-05, + 0.001431 ], "rotation_deg": [ - -35.546785380417425, - 150.0, - 0.0 + -0.05771, + -0.045065, + 0.02921 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 671, "type": "imguizmo", "translation": [ - -1.7372934453934528, - 0.43110107046401264, - 0.20122637240902413 + -0.001718, + -2.1E-05, + 0.001449 ], "rotation_deg": [ - -35.72560521762764, - 150.0, - 0.0 + -0.057256, + -0.042546, + 0.030103 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 672, "type": "imguizmo", "translation": [ - -1.7436620828386202, - 0.4802555871187139, - 0.14318724425443127 + -0.001703, + 1.1E-05, + 0.001466 ], "rotation_deg": [ - -35.859915399430406, - 150.0, - 0.0 + -0.056763, + -0.039997, + 0.030975 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 673, "type": "imguizmo", "translation": [ - -1.7478583344562715, - 0.5295592904008172, - 0.08443476445149993 + -0.001687, + 4.3E-05, + 0.001483 ], "rotation_deg": [ - -35.94954859206402, - 150.0, - 0.0 + -0.05623, + -0.037421, + 0.031826 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 674, "type": "imguizmo", "translation": [ - -1.7498769722394254, - 0.5789507540355253, - 0.025261634899268894 + -0.00167, + 7.4E-05, + 0.001498 ], "rotation_deg": [ - -35.994393123731946, - 150.0, - 0.0 + -0.055657, + -0.034818, + 0.032655 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 675, "type": "imguizmo", "translation": [ - -1.7497154812167734, - 0.6283684424095682, - -0.0340373468473299 + -0.001651, + 0.000106, + 0.001512 ], "rotation_deg": [ - -35.99439312373195, - 150.0, - 0.0 + -0.055046, + -0.032191, + 0.033461 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 676, "type": "imguizmo", "translation": [ - -1.7473740625860192, - 0.6777507872369212, - -0.09316675624406756 + -0.001632, + 0.000137, + 0.001526 ], "rotation_deg": [ - -35.94954859206402, - 150.0, - 0.0 + -0.054397, + -0.029541, + 0.034244 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 677, "type": "imguizmo", "translation": [ - -1.7428556334632148, - 0.7270362642652123, - -0.15183201354762732 + -0.001611, + 0.000169, + 0.001538 ], "rotation_deg": [ - -35.859915399430406, - 150.0, - 0.0 + -0.05371, + -0.026871, + 0.035002 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 678, "type": "imguizmo", "translation": [ - -1.7361658232483943, - 0.7761634699272872, - -0.209740851397075 + -0.00159, + 0.0002, + 0.001549 ], "rotation_deg": [ - -35.725605217627646, - 150.0, - 0.0 + -0.052985, + -0.024183, + 0.035737 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 679, "type": "imguizmo", "translation": [ - -1.7273129666120437, - 0.825071197842405, - -0.26660477087514767 + -0.001567, + 0.000231, + 0.001559 ], "rotation_deg": [ - -35.54678538041743, - 150.0, - 0.0 + -0.052224, + -0.021477, + 0.036446 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 680, "type": "imguizmo", "translation": [ - -1.716308093111139, - 0.8736985150717673, - -0.32214047879534763 + -0.001543, + 0.000262, + 0.001568 ], "rotation_deg": [ - -35.323678675049564, - 150.0, - 0.0 + -0.051426, + -0.018757, + 0.03713 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 681, "type": "imguizmo", "translation": [ - -1.7031649134476916, - 0.9219848380333722, - -0.37607129905434733 + -0.001518, + 0.000293, + 0.001576 ], "rotation_deg": [ - -35.05656306469682, - 150.0, - 0.0 + -0.050592, + -0.016023, + 0.037788 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 682, "type": "imguizmo", "translation": [ - -1.6878998023869227, - 0.9698700079816133, - -0.42812855101841923 + -0.001492, + 0.000324, + 0.001583 ], "rotation_deg": [ - -34.745771342147385, - 150.0, - 0.0 + -0.049723, + -0.013278, + 0.03842 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 683, "type": "imguizmo", "translation": [ - -1.670531778356341, - 1.0172943659575848, - -0.478052888076836 + -0.001465, + 0.000354, + 0.001588 ], "rotation_deg": [ - -34.39169071518656, - 150.0, - 0.0 + -0.048819, + -0.010524, + 0.039025 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 684, "type": "imguizmo", "translation": [ - -1.6510824797511539, - 1.0641988271167202, - -0.5255955896936462 + -0.001436, + 0.000384, + 0.001593 ], "rotation_deg": [ - -33.994762324184016, - 150.0, - 0.0 + -0.047881, + -0.007763, + 0.039603 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 685, "type": "imguizmo", "translation": [ - -1.6295761379755191, - 1.1105249543411424, - -0.5705198005208636 + -0.001407, + 0.000414, + 0.001596 ], "rotation_deg": [ - -33.55548069248783, - 150.0, - 0.0 + -0.04691, + -0.004996, + 0.040153 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 686, "type": "imguizmo", "translation": [ - -1.6060395472532352, - 1.1562150310450428, - -0.6126017103999034 + -0.001377, + 0.000444, + 0.001599 ], "rotation_deg": [ - -33.07439311031003, - 150.0, - 0.0 + -0.045906, + -0.002226, + 0.040676 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 687, "type": "imguizmo", "translation": [ - -1.5805020312454723, - 1.2012121330823573, - -0.6516316693725328 + -0.001346, + 0.000473, + 0.0016 ], "rotation_deg": [ - -32.55209895287106, - 150.0, - 0.0 + -0.04487, + 0.000546, + 0.041169 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 688, "type": "imguizmo", "translation": [ - -1.5529954065171434, - 1.2454601996671646, - -0.6874152321464166 + -0.001314, + 0.000502, + 0.0016 ], "rotation_deg": [ - -31.989248933652974, - 150.0, - 0.0 + -0.043803, + 0.003318, + 0.041635 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 689, "type": "imguizmo", "translation": [ - -1.523553942897427, - 1.2889041032184445, - -0.7197741268117833 + -0.001281, + 0.000531, + 0.001599 ], "rotation_deg": [ - -31.386544293691394, - 150.0, - 0.0 + -0.042705, + 0.006087, + 0.042071 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 690, "type": "imguizmo", "translation": [ - -1.492214320783825, - 1.331489718042179, - -0.748547142983112 + -0.001247, + 0.000559, + 0.001597 ], "rotation_deg": [ - -30.74473592791655, - 150.0, - 0.0 + -0.041578, + 0.008852, + 0.042478 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 691, "type": "imguizmo", "translation": [ - -1.4590155854429585, - 1.373163987765225, - -0.7735909349411628 + -0.001213, + 0.000587, + 0.001594 ], "rotation_deg": [ - -30.064623449631696, - 150.0, - 0.0 + -0.040422, + 0.01161, + 0.042855 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 692, "type": "imguizmo", "translation": [ - -1.4239990983650224, - 1.4138749914369562, - -0.7947807357741433 + -0.001177, + 0.000614, + 0.001589 ], "rotation_deg": [ - -29.34705419429445, - 150.0, - 0.0 + -0.039237, + 0.014361, + 0.043203 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 693, "type": "imguizmo", "translation": [ - -1.387208485732525, - 1.4535720082162906, - -0.8120109789601822 + -0.001141, + 0.000641, + 0.001584 ], "rotation_deg": [ - -28.59292216384241, - 150.0, - 0.0 + -0.038025, + 0.017102, + 0.04352 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 694, "type": "imguizmo", "translation": [ - -1.3486895840674944, - 1.492205580563551, - -0.825195824294434 + -0.001104, + 0.000668, + 0.001577 ], "rotation_deg": [ - -27.803166912878016, - 150.0, - 0.0 + -0.036787, + 0.01983, + 0.043807 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 695, "type": "imguizmo", "translation": [ - -1.3084903831248782, - 1.5297275758583981, - -0.8342695855406617 + -0.001066, + 0.000694, + 0.001569 ], "rotation_deg": [ - -26.97877237810052, - 150.0, - 0.0 + -0.035523, + 0.022545, + 0.044063 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 696, "type": "imguizmo", "translation": [ - -1.2666609661032882, - 1.5660912463670817, - -0.8391870576767603 + -0.001027, + 0.000719, + 0.001561 ], "rotation_deg": [ - -26.120765652443385, - 150.0, - 0.0 + -0.034234, + 0.025244, + 0.044289 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 697, "type": "imguizmo", "translation": [ - -1.2232534472475682, - 1.6012512874842966, - -0.8399237421039043 + -0.000988, + 0.000744, + 0.001551 ], "rotation_deg": [ - -25.230215705444397, - 150.0, - 0.0 + -0.032922, + 0.027926, + 0.044484 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 698, "type": "imguizmo", "translation": [ - -1.1783219069209374, - 1.6351638941770774, - -0.8364759686973338 + -0.000948, + 0.000769, + 0.00154 ], "rotation_deg": [ - -24.308232051442705, - 150.0, - 0.0 + -0.031586, + 0.030588, + 0.044648 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 699, "type": "imguizmo", "translation": [ - -1.1319223242275882, - 1.6677868155604172, - -0.8288609140907325 + -0.000907, + 0.000793, + 0.001528 ], "rotation_deg": [ - -23.35596336726214, - 150.0, - 0.0 + -0.030228, + 0.033229, + 0.044781 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 700, "type": "imguizmo", "translation": [ - -1.0841125072696978, - 1.6990794075366078, - -0.817116516103101 + -0.000865, + 0.000817, + 0.001515 ], "rotation_deg": [ - -22.37459606110294, - 150.0, - 0.0 + -0.02885, + 0.035847, + 0.044882 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 701, "type": "imguizmo", "translation": [ - -1.0349520211257281, - 1.7290026834327241, - -0.8013012847344533 + -0.000824, + 0.000839, + 0.001501 ], "rotation_deg": [ - -21.36535279442483, - 150.0, - 0.0 + -0.027451, + 0.038439, + 0.044952 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 702, "type": "imguizmo", "translation": [ - -0.9845021136397561, - 1.7575193625731642, - -0.7814940106719368 + -0.000781, + 0.000862, + 0.001486 ], "rotation_deg": [ - -20.329490958663122, - 150.0, - 0.0 + -0.026033, + 0.041005, + 0.044991 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 703, "type": "imguizmo", "translation": [ - -0.9328256391142875, - 1.7845939167267286, - -0.7577933727585957 + -0.000738, + 0.000884, + 0.001469 ], "rotation_deg": [ - -19.26830110867557, - 150.0, - 0.0 + -0.024597, + 0.043542, + 0.044999 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 704, "type": "imguizmo", "translation": [ - -0.8799869800016217, - 1.8101926143703733, - -0.7303174463803351 + -0.000694, + 0.000905, + 0.001452 ], "rotation_deg": [ - -18.183105354871696, - 150.0, - 0.0 + -0.023144, + 0.046049, + 0.044975 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 705, "type": "imguizmo", "translation": [ - -0.826051966691334, - 1.8342835627144847, - -0.6992031152202912 + -0.00065, + 0.000925, + 0.001434 ], "rotation_deg": [ - -17.07525571602787, - 150.0, - 0.0 + -0.021674, + 0.048524, + 0.04492 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 706, "type": "imguizmo", "translation": [ - -0.7710877954938082, - 1.8568367474373264, - -0.6646053893112046 + -0.000606, + 0.000945, + 0.001415 ], "rotation_deg": [ - -15.94613243484027, - 150.0, - 0.0 + -0.02019, + 0.050965, + 0.044834 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 707, "type": "imguizmo", "translation": [ - -0.7151629449220016, - 1.877824070079145, - -0.6266966327832368 + -0.000561, + 0.000964, + 0.001395 ], "rotation_deg": [ - -14.797142258314395, - 150.0, - 0.0 + -0.018692, + 0.053371, + 0.044716 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 708, "type": "imguizmo", "translation": [ - -0.6583470903757402, - 1.8972193830493547, - -0.5856657051545401 + -0.000515, + 0.000983, + 0.001373 ], "rotation_deg": [ - -13.629716685133328, - 150.0, - 0.0 + -0.01718, + 0.055739, + 0.044567 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 709, "type": "imguizmo", "translation": [ - -0.6007110173348551, - 1.9149985222031747, - -0.5417170204426431 + -0.00047, + 0.001001, + 0.001351 ], "rotation_deg": [ - -12.44531018218883, - 150.0, - 0.0 + -0.015657, + 0.058068, + 0.044387 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 710, "type": "imguizmo", "translation": [ - -0.5423265331692831, - 1.9311393369471483, - -0.4950695287840945 + -0.000424, + 0.001018, + 0.001328 ], "rotation_deg": [ - -11.245398372496588, - 150.0, - 0.0 + -0.014122, + 0.060358, + 0.044176 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 711, "type": "imguizmo", "translation": [ - -0.4832663776760281, - 1.9456217178360224, - -0.44595562563589713 + -0.000377, + 0.001034, + 0.001304 ], "rotation_deg": [ - -10.031476196753715, - 150.0, - 0.0 + -0.012578, + 0.062604, + 0.043935 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 712, "type": "imguizmo", "translation": [ - -0.42360413245442996, - 1.9584276216266132, - -0.39461999399303094 + -0.000331, + 0.00105, + 0.001279 ], "rotation_deg": [ - -8.80505605082879, - 150.0, - 0.0 + -0.011025, + 0.064808, + 0.043663 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 713, "type": "imguizmo", "translation": [ - -0.36341412923265626, - 1.9695410937574391, - -0.34131838539007997 + -0.000284, + 0.001065, + 0.001253 ], "rotation_deg": [ - -7.5676659015049506, - 150.0, - 0.0 + -0.009464, + 0.066966, + 0.04336 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 714, "type": "imguizmo", "translation": [ - -0.3027713572596238, - 1.978948288226119, - -0.2863163457599518 + -0.000237, + 0.001079, + 0.001227 ], "rotation_deg": [ - -6.320847382823533, - 150.0, - 0.0 + -0.007897, + 0.069077, + 0.043027 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 715, "type": "imguizmo", "translation": [ - -0.24175136987773482, - 1.9866374848397654, - -0.2298878924973987 + -0.00019, + 0.001093, + 0.001199 ], "rotation_deg": [ - -5.066153875400075, - 150.0, - 0.0 + -0.006324, + 0.071141, + 0.042664 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 716, "type": "imguizmo", "translation": [ - -0.18043019039281344, - 1.9925991038168873, - -0.1723141493181341 + -0.000142, + 0.001105, + 0.001171 ], "rotation_deg": [ - -3.8051485711053608, - 150.0, - 0.0 + -0.004747, + 0.073154, + 0.042272 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 717, "type": "imguizmo", "translation": [ - -0.11888421735854393, - 1.9968257177226036, - -0.11388194571463164 + -9.5E-05, + 0.001117, + 0.001142 ], "rotation_deg": [ - -2.5394025255232044, - 150.0, - 0.0 + -0.003167, + 0.075117, + 0.04185 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 718, "type": "imguizmo", "translation": [ - -0.05719012939337879, - 1.9993120607223025, - -0.05488238798601471 + -4.8E-05, + 0.001128, + 0.001112 ], "rotation_deg": [ - -1.2704927006106954, - 150.0, - 0.0 + -0.001584, + 0.077028, + 0.041398 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 719, "type": "imguizmo", "translation": [ - 0.004575210351469343, - 2.0000550351422155, - 0.004390591038880599 + -0.0, + 0.001139, + 0.001081 ], "rotation_deg": [ - -3.3306690738754696e-15, - 150.0, - 0.0 + -0.0, + 0.078884, + 0.040918 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { @@ -13728,7 +13844,7 @@ "frame": 720, "type": "action", "action": "set_active_planar", - "value": 4 + "value": 3 }, { "frame": 720, @@ -13736,13662 +13852,13789 @@ "action": "set_projection_type", "value": "perspective" }, + { + "frame": 720, + "type": "action", + "action": "set_left_handed", + "value": true + }, + { + "frame": 720, + "type": "action", + "action": "reset_active_camera", + "value": 1 + }, + { + "frame": 720, + "type": "action", + "action": "set_active_render_window", + "value": 1 + }, + { + "frame": 720, + "type": "action", + "action": "set_active_planar", + "value": 3 + }, + { + "frame": 720, + "type": "action", + "action": "set_projection_type", + "value": "orthographic" + }, + { + "frame": 720, + "type": "action", + "action": "set_left_handed", + "value": true + }, + { + "frame": 720, + "type": "action", + "action": "set_active_render_window", + "value": 0 + }, { "frame": 721, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.0 + 0, + 2.5, + 1.8 ], "rotation_deg": [ - 0.0, - 150.0, - 0.0 + 0, + 100, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 722, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.040020520069236704 + 0.100308, + 2.499129, + 1.829026 ], "rotation_deg": [ - 1.270492700610689, - 150.0, - 0.0 + 1.58326, + 99.991289, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 723, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.07678953794844168 + 0.200546, + 2.496516, + 1.857972 ], "rotation_deg": [ - 2.5394025255231947, - 150.0, - 0.0 + 3.162108, + 99.96516, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 724, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.1137190169539431 + 0.300645, + 2.492163, + 1.886756 ], "rotation_deg": [ - 3.8051485711053474, - 150.0, - 0.0 + 4.732142, + 99.921633, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 725, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.15048632571878637 + 0.400534, + 2.486074, + 1.915298 ], "rotation_deg": [ - 5.066153875400059, - 150.0, - 0.0 + 6.288987, + 99.860738, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 726, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.1870677865014388 + 0.500143, + 2.478252, + 1.943519 ], "rotation_deg": [ - 6.320847382823546, - 150.0, - 0.0 + 7.828303, + 99.782517, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 727, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.22341605297729109 + 0.599405, + 2.468702, + 1.97134 ], "rotation_deg": [ - 7.5676659015049585, - 150.0, - 0.0 + 9.345801, + 99.687025, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 728, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.25948598136292383 + 0.698248, + 2.457433, + 1.998683 ], "rotation_deg": [ - 8.805056050828796, - 150.0, - 0.0 + 10.83725, + 99.574327, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 729, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.2952326216887082 + 0.796605, + 2.44445, + 2.025472 ], "rotation_deg": [ - 10.031476196753717, - 150.0, - 0.0 + 12.298494, + 99.444504, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 730, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.33061143899854584 + 0.894407, + 2.429764, + 2.051633 ], "rotation_deg": [ - 11.245398372496588, - 150.0, - 0.0 + 13.725459, + 99.297645, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 731, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.3655783556190644 + 0.991585, + 2.413385, + 2.077093 ], "rotation_deg": [ - 12.445310182188825, - 150.0, - 0.0 + 15.11417, + 99.133852, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 732, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.40008980713217435 + 1.088072, + 2.395324, + 2.10178 ], "rotation_deg": [ - 13.62971668513332, - 150.0, - 0.0 + 16.460754, + 98.95324, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 733, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.43410279656632905 + 1.183801, + 2.375594, + 2.125627 ], "rotation_deg": [ - 14.797142258314384, - 150.0, - 0.0 + 17.761459, + 98.755935, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 734, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.46757494797216265 + 1.278705, + 2.354207, + 2.148565 ], "rotation_deg": [ - 15.946132434840287, - 150.0, - 0.0 + 19.01266, + 98.542074, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 735, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.5004645592171051 + 1.372718, + 2.331181, + 2.170533 ], "rotation_deg": [ - 17.07525571602788, - 150.0, - 0.0 + 20.210869, + 98.311806, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 736, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.5327306539410901 + 1.465775, + 2.306529, + 2.191467 ], "rotation_deg": [ - 18.1831053548717, - 150.0, - 0.0 + 21.352747, + 98.065291, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 737, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.5643330326079934 + 1.557809, + 2.28027, + 2.21131 ], "rotation_deg": [ - 19.268301108675573, - 150.0, - 0.0 + 22.435111, + 97.802702, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 738, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.5952323225892489 + 1.648758, + 2.252422, + 2.230007 ], "rotation_deg": [ - 20.329490958663122, - 150.0, - 0.0 + 23.454944, + 97.524222, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 739, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.625390027217242 + 1.738558, + 2.223004, + 2.247506 ], "rotation_deg": [ - 21.365352794424826, - 150.0, - 0.0 + 24.409405, + 97.230044, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 740, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.6547685737473631 + 1.827147, + 2.192037, + 2.263757 ], "rotation_deg": [ - 22.374596061102935, - 150.0, - 0.0 + 25.295831, + 96.920373, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 741, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.6833313601689682 + 1.914461, + 2.159543, + 2.278715 ], "rotation_deg": [ - 23.355963367262138, - 150.0, - 0.0 + 26.111754, + 96.595426, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 742, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.7110428008069274 + 2.000442, + 2.125543, + 2.29234 ], "rotation_deg": [ - 24.308232051442694, - 150.0, - 0.0 + 26.854899, + 96.255428, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 743, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.7378683706569439 + 2.085029, + 2.090062, + 2.304592 ], "rotation_deg": [ - 25.230215705444387, - 150.0, - 0.0 + 27.523194, + 95.900618, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 744, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.7637746483994108 + 2.168162, + 2.053124, + 2.315438 ], "rotation_deg": [ - 26.120765652443374, - 150.0, - 0.0 + 28.114777, + 95.531241, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 745, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.788729358038213 + 2.249785, + 2.014756, + 2.324847 ], "rotation_deg": [ - 26.978772378100505, - 150.0, - 0.0 + 28.627999, + 95.147556, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 746, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.8127014091125998 + 2.329839, + 1.974983, + 2.332793 ], "rotation_deg": [ - 27.803166912878, - 150.0, - 0.0 + 29.06143, + 94.749829, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 747, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.8356609354320272 + 2.40827, + 1.933834, + 2.339254 ], "rotation_deg": [ - 28.592922163842395, - 150.0, - 0.0 + 29.413861, + 94.338339, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 748, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.8575793322857121 + 2.485023, + 1.891337, + 2.344212 ], "rotation_deg": [ - 29.347054194294437, - 150.0, - 0.0 + 29.684311, + 93.913372, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 749, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.8784292920805408 + 2.560043, + 1.847522, + 2.347654 ], "rotation_deg": [ - 30.06462344963168, - 150.0, - 0.0 + 29.872025, + 93.475223, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 750, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.898184838362928 + 2.63328, + 1.80242, + 2.349569 ], "rotation_deg": [ - 30.744735927916555, - 150.0, - 0.0 + 29.976481, + 93.024199, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 751, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.9168213581822445 + 2.704681, + 1.756061, + 2.349952 ], "rotation_deg": [ - 31.386544293691394, - 150.0, - 0.0 + 29.997386, + 92.560613, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 752, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.9343156327554888 + 2.774198, + 1.708479, + 2.348803 ], "rotation_deg": [ - 31.989248933652974, - 150.0, - 0.0 + 29.934684, + 92.084789, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 753, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.950645866394999 + 2.841781, + 1.659706, + 2.346123 ], "rotation_deg": [ - 32.55209895287106, - 150.0, - 0.0 + 29.788548, + 91.597059, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 754, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.9657917136631656 + 2.907383, + 1.609776, + 2.341922 ], "rotation_deg": [ - 33.07439311031002, - 150.0, - 0.0 + 29.559386, + 91.097761, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 755, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.9797343047203132 + 2.97096, + 1.558725, + 2.33621 ], "rotation_deg": [ - 33.555480692487826, - 150.0, - 0.0 + 29.247837, + 90.587245, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 756, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.992456268834171 + 3.032465, + 1.506587, + 2.329004 ], "rotation_deg": [ - 33.99476232418401, - 150.0, - 0.0 + 28.854769, + 90.065866, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 757, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 1.0039417560216426 + 3.091858, + 1.453399, + 2.320323 ], "rotation_deg": [ - 34.391690715186556, - 150.0, - 0.0 + 28.381278, + 89.533987, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 758, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 1.014176456795911 + 3.149096, + 1.399198, + 2.310193 ], "rotation_deg": [ - 34.745771342147385, - 150.0, - 0.0 + 27.828683, + 88.991979, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 759, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 1.0231476199942764 + 3.204139, + 1.344022, + 2.29864 ], "rotation_deg": [ - 35.05656306469681, - 150.0, - 0.0 + 27.198524, + 88.44022, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 760, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 1.0308440686645188 + 3.256949, + 1.287909, + 2.285697 ], "rotation_deg": [ - 35.323678675049564, - 150.0, - 0.0 + 26.492558, + 87.879095, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 761, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 1.037256213989987 + 3.307489, + 1.230899, + 2.2714 ], "rotation_deg": [ - 35.54678538041743, - 150.0, - 0.0 + 25.712753, + 87.308993, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 762, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 1.0423760672360716 + 3.355724, + 1.173031, + 2.25579 ], "rotation_deg": [ - 35.725605217627646, - 150.0, - 0.0 + 24.861281, + 86.730314, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 763, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 1.0461972497031717 + 3.401621, + 1.114346, + 2.238909 ], "rotation_deg": [ - 35.859915399430406, - 150.0, - 0.0 + 23.940517, + 86.143459, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 764, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 1.0487150006737627 + 3.445146, + 1.054884, + 2.220805 ], "rotation_deg": [ - 35.949548592064026, - 150.0, - 0.0 + 22.953026, + 85.548838, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 765, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 1.049926183343655 + 3.486271, + 0.994687, + 2.201529 ], "rotation_deg": [ - 35.99439312373195, - 150.0, - 0.0 + 21.901561, + 84.946866, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 766, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 1.0498292887300635 + 3.524966, + 0.933796, + 2.181133 ], "rotation_deg": [ - 35.99439312373195, - 150.0, - 0.0 + 20.789052, + 84.337961, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 767, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 1.0484244375516112 + 3.561205, + 0.872255, + 2.159674 ], "rotation_deg": [ - 35.949548592064026, - 150.0, - 0.0 + 19.618601, + 83.722549, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 768, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 1.0457133800779288 + 3.594962, + 0.810106, + 2.137214 ], "rotation_deg": [ - 35.85991539943041, - 150.0, - 0.0 + 18.393469, + 83.101057, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 769, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 1.041699493949036 + 3.626213, + 0.747392, + 2.113813 ], "rotation_deg": [ - 35.725605217627646, - 150.0, - 0.0 + 17.117071, + 82.47392, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 770, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 1.0363877799672256 + 3.654937, + 0.684157, + 2.089538 ], "rotation_deg": [ - 35.54678538041743, - 150.0, - 0.0 + 15.792965, + 81.841575, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 771, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 1.029784855866683 + 3.681114, + 0.620446, + 2.064455 ], "rotation_deg": [ - 35.323678675049564, - 150.0, - 0.0 + 14.424841, + 81.204461, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 772, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 1.021898948068615 + 3.704726, + 0.556302, + 2.038636 ], "rotation_deg": [ - 35.05656306469682, - 150.0, - 0.0 + 13.016512, + 80.563023, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 773, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 1.0127398814321533 + 3.725756, + 0.491771, + 2.012152 ], "rotation_deg": [ - 34.745771342147385, - 150.0, - 0.0 + 11.571904, + 79.917709, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 774, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 1.002319067013804 + 3.744189, + 0.426897, + 1.985076 ], "rotation_deg": [ - 34.391690715186556, - 150.0, - 0.0 + 10.095043, + 79.268967, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 775, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.9906494878506916 + 3.760013, + 0.361725, + 1.957484 ], "rotation_deg": [ - 33.99476232418401, - 150.0, - 0.0 + 8.590045, + 78.61725, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 776, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.9777456827853109 + 3.773216, + 0.296301, + 1.929454 ], "rotation_deg": [ - 33.555480692487826, - 150.0, - 0.0 + 7.061106, + 77.963012, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 777, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.9636237283519408 + 3.78379, + 0.230671, + 1.901062 ], "rotation_deg": [ - 33.07439311031002, - 150.0, - 0.0 + 5.512486, + 77.306709, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 778, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.9483012187472828 + 3.791727, + 0.16488, + 1.872389 ], "rotation_deg": [ - 32.552098952871056, - 150.0, - 0.0 + 3.948501, + 76.648799, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 779, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.9317972439102856 + 3.797021, + 0.098974, + 1.843514 ], "rotation_deg": [ - 31.989248933652966, - 150.0, - 0.0 + 2.373511, + 75.989739, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 780, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.914132365738456 + 3.799669, + 0.032999, + 1.814518 ], "rotation_deg": [ - 31.386544293691394, - 150.0, - 0.0 + 0.791906, + 75.32999, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 781, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.8953285924702947 + 3.799669, + -0.032999, + 1.785482 ], "rotation_deg": [ - 30.74473592791655, - 150.0, - 0.0 + -0.791906, + 74.67001, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 782, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.8754093512657741 + 3.797021, + -0.098974, + 1.756486 ], "rotation_deg": [ - 30.064623449631675, - 150.0, - 0.0 + -2.373511, + 74.010261, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 783, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.8543994590190124 + 3.791727, + -0.16488, + 1.727611 ], "rotation_deg": [ - 29.34705419429443, - 150.0, - 0.0 + -3.948501, + 73.351201, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 784, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.8323250914395143 + 3.78379, + -0.230671, + 1.698938 ], "rotation_deg": [ - 28.5929221638424, - 150.0, - 0.0 + -5.512486, + 72.693291, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 785, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.8092137504404958 + 3.773216, + -0.296301, + 1.670546 ], "rotation_deg": [ - 27.803166912878005, - 150.0, - 0.0 + -7.061106, + 72.036988, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 786, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.7850942298749263 + 3.760013, + -0.361725, + 1.642516 ], "rotation_deg": [ - 26.97877237810051, - 150.0, - 0.0 + -8.590045, + 71.38275, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 787, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.7599965796619721 + 3.744189, + -0.426897, + 1.614924 ], "rotation_deg": [ - 26.120765652443374, - 150.0, - 0.0 + -10.095043, + 70.731033, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 788, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.7339520683485403 + 3.725756, + -0.491771, + 1.587848 ], "rotation_deg": [ - 25.230215705444394, - 150.0, - 0.0 + -11.571904, + 70.082291, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 789, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.7069931441525616 + 3.704726, + -0.556302, + 1.561364 ], "rotation_deg": [ - 24.3082320514427, - 150.0, - 0.0 + -13.016512, + 69.436977, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 790, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.6791533945365522 + 3.681114, + -0.620446, + 1.535545 ], "rotation_deg": [ - 23.35596336726214, - 150.0, - 0.0 + -14.424841, + 68.795539, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 791, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.6504675043618181 + 3.654937, + -0.684157, + 1.510462 ], "rotation_deg": [ - 22.374596061102938, - 150.0, - 0.0 + -15.792965, + 68.158425, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 792, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.6209712126754365 + 3.626213, + -0.747392, + 1.486187 ], "rotation_deg": [ - 21.365352794424837, - 150.0, - 0.0 + -17.117071, + 67.52608, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 793, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.5907012681838535 + 3.594962, + -0.810106, + 1.462786 ], "rotation_deg": [ - 20.329490958663133, - 150.0, - 0.0 + -18.393469, + 66.898943, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 794, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.5596953834685723 + 3.561205, + -0.872255, + 1.440326 ], "rotation_deg": [ - 19.268301108675576, - 150.0, - 0.0 + -19.618601, + 66.277451, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 795, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.5279921880009728 + 3.524966, + -0.933796, + 1.418867 ], "rotation_deg": [ - 18.183105354871703, - 150.0, - 0.0 + -20.789052, + 65.662039, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 796, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.49563118001480067 + 3.486271, + -0.994687, + 1.398471 ], "rotation_deg": [ - 17.075255716027893, - 150.0, - 0.0 + -21.901561, + 65.053134, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 797, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.46265267729628523 + 3.445146, + -1.054884, + 1.379195 ], "rotation_deg": [ - 15.946132434840298, - 150.0, - 0.0 + -22.953026, + 64.451162, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 798, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.4290977669532004 + 3.401621, + -1.114346, + 1.361091 ], "rotation_deg": [ - 14.797142258314391, - 150.0, - 0.0 + -23.940517, + 63.856541, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 799, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.3950082542254436 + 3.355724, + -1.173031, + 1.34421 ], "rotation_deg": [ - 13.629716685133324, - 150.0, - 0.0 + -24.861281, + 63.269686, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 800, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.36042661040091245 + 3.307489, + -1.230899, + 1.3286 ], "rotation_deg": [ - 12.445310182188827, - 150.0, - 0.0 + -25.712753, + 62.691007, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 801, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.3253959199015694 + 3.256949, + -1.287909, + 1.314303 ], "rotation_deg": [ - 11.245398372496588, - 150.0, - 0.0 + -26.492558, + 62.120905, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 802, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.28995982660561637 + 3.204139, + -1.344022, + 1.30136 ], "rotation_deg": [ - 10.031476196753715, - 150.0, - 0.0 + -27.198524, + 61.55978, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 803, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.25416247947265747 + 3.149096, + -1.399198, + 1.289807 ], "rotation_deg": [ - 8.805056050828789, - 150.0, - 0.0 + -27.828683, + 61.008021, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 804, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.21804847753959367 + 3.091858, + -1.453399, + 1.279677 ], "rotation_deg": [ - 7.567665901504963, - 150.0, - 0.0 + -28.381278, + 60.466013, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 805, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.18166281435577417 + 3.032465, + -1.506587, + 1.270996 ], "rotation_deg": [ - 6.320847382823548, - 150.0, - 0.0 + -28.854769, + 59.934134, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 806, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.14505082192663976 + 2.97096, + -1.558725, + 1.26379 ], "rotation_deg": [ - 5.066153875400057, - 150.0, - 0.0 + -29.247837, + 59.412755, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 807, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.10825811423568706 + 2.907383, + -1.609776, + 1.258078 ], "rotation_deg": [ - 3.805148571105343, - 150.0, - 0.0 + -29.559386, + 58.902239, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 808, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.07133053041512583 + 2.841781, + -1.659706, + 1.253877 ], "rotation_deg": [ - 2.5394025255232027, - 150.0, - 0.0 + -29.788548, + 58.402941, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 809, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.03431407763602671 + 2.774198, + -1.708479, + 1.251197 ], "rotation_deg": [ - 1.2704927006106939, - 150.0, - 0.0 + -29.934684, + 57.915211, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 810, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.0027451262108821595 + 2.704681, + -1.756061, + 1.250048 ], "rotation_deg": [ - 1.9984014443252818e-15, - 150.0, - 0.0 + -29.997386, + 57.439387, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 811, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.03980090997236625 + 2.63328, + -1.80242, + 1.250431 ], "rotation_deg": [ - -1.27049270061069, - 150.0, - 0.0 + -29.976481, + 56.975801, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 812, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.07680710675619157 + 2.560043, + -1.847522, + 1.252346 ], "rotation_deg": [ - -2.5394025255231987, - 150.0, - 0.0 + -29.872025, + 56.524777, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 813, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.11371761144932346 + 2.485023, + -1.891337, + 1.255788 ], "rotation_deg": [ - -3.805148571105356, - 150.0, - 0.0 + -29.684311, + 56.086628, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 814, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.1504864381591564 + 2.40827, + -1.933834, + 1.260746 ], "rotation_deg": [ - -5.066153875400071, - 150.0, - 0.0 + -29.413861, + 55.661661, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 815, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.1870677775062087 + 2.329839, + -1.974983, + 1.267207 ], "rotation_deg": [ - -6.320847382823529, - 150.0, - 0.0 + -29.06143, + 55.250171, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 816, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.22341605369690914 + 2.249785, + -2.014756, + 1.275153 ], "rotation_deg": [ - -7.567665901504945, - 150.0, - 0.0 + -28.627999, + 54.852444, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 817, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.2594859813053541 + 2.168162, + -2.053124, + 1.284562 ], "rotation_deg": [ - -8.805056050828787, - 150.0, - 0.0 + -28.114777, + 54.468759, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 818, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.29523262169331355 + 2.085029, + -2.090062, + 1.295408 ], "rotation_deg": [ - -10.03147619675371, - 150.0, - 0.0 + -27.523194, + 54.099382, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 819, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.33061143899817735 + 2.000442, + -2.125543, + 1.30766 ], "rotation_deg": [ - -11.245398372496584, - 150.0, - 0.0 + -26.854899, + 53.744572, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 820, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.3655783556190938 + 1.914461, + -2.159543, + 1.321285 ], "rotation_deg": [ - -12.445310182188827, - 150.0, - 0.0 + -26.111754, + 53.404574, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 821, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.4000898071321721 + 1.827147, + -2.192037, + 1.336243 ], "rotation_deg": [ - -13.629716685133323, - 150.0, - 0.0 + -25.295831, + 53.079627, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 822, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.43410279656632944 + 1.738558, + -2.223004, + 1.352494 ], "rotation_deg": [ - -14.79714225831439, - 150.0, - 0.0 + -24.409405, + 52.769956, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 823, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.4675749479721625 + 1.648758, + -2.252422, + 1.369993 ], "rotation_deg": [ - -15.946132434840282, - 150.0, - 0.0 + -23.454944, + 52.475778, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 824, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.5004645592171051 + 1.557809, + -2.28027, + 1.38869 ], "rotation_deg": [ - -17.075255716027876, - 150.0, - 0.0 + -22.435111, + 52.197298, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 825, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.5327306539410901 + 1.465775, + -2.306529, + 1.408533 ], "rotation_deg": [ - -18.1831053548717, - 150.0, - 0.0 + -21.352747, + 51.934709, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 826, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.5643330326079934 + 1.372718, + -2.331181, + 1.429467 ], "rotation_deg": [ - -19.268301108675576, - 150.0, - 0.0 + -20.210869, + 51.688194, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 827, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.5952323225892491 + 1.278705, + -2.354207, + 1.451435 ], "rotation_deg": [ - -20.32949095866313, - 150.0, - 0.0 + -19.01266, + 51.457926, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 828, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.6253900272172424 + 1.183801, + -2.375594, + 1.474373 ], "rotation_deg": [ - -21.365352794424837, - 150.0, - 0.0 + -17.761459, + 51.244065, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 829, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.6547685737473635 + 1.088072, + -2.395324, + 1.49822 ], "rotation_deg": [ - -22.374596061102945, - 150.0, - 0.0 + -16.460754, + 51.04676, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 830, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.6833313601689687 + 0.991585, + -2.413385, + 1.522907 ], "rotation_deg": [ - -23.35596336726215, - 150.0, - 0.0 + -15.11417, + 50.866148, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 831, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.7110428008069271 + 0.894407, + -2.429764, + 1.548367 ], "rotation_deg": [ - -24.308232051442683, - 150.0, - 0.0 + -13.725459, + 50.702355, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 832, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.7378683706569437 + 0.796605, + -2.44445, + 1.574528 ], "rotation_deg": [ - -25.23021570544438, - 150.0, - 0.0 + -12.298494, + 50.555496, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 833, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.7637746483994106 + 0.698248, + -2.457433, + 1.601317 ], "rotation_deg": [ - -26.120765652443367, - 150.0, - 0.0 + -10.83725, + 50.425673, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 834, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.788729358038213 + 0.599405, + -2.468702, + 1.62866 ], "rotation_deg": [ - -26.9787723781005, - 150.0, - 0.0 + -9.345801, + 50.312975, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 835, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.8127014091126001 + 0.500143, + -2.478252, + 1.656481 ], "rotation_deg": [ - -27.80316691287801, - 150.0, - 0.0 + -7.828303, + 50.217483, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 836, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.8356609354320277 + 0.400534, + -2.486074, + 1.684702 ], "rotation_deg": [ - -28.592922163842402, - 150.0, - 0.0 + -6.288987, + 50.139262, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 837, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.8575793322857128 + 0.300645, + -2.492163, + 1.713244 ], "rotation_deg": [ - -29.347054194294444, - 150.0, - 0.0 + -4.732142, + 50.078367, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 838, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.8784292920805414 + 0.200546, + -2.496516, + 1.742028 ], "rotation_deg": [ - -30.064623449631696, - 150.0, - 0.0 + -3.162108, + 50.03484, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 839, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.898184838362928 + 0.100308, + -2.499129, + 1.770974 ], "rotation_deg": [ - -30.74473592791655, - 150.0, - 0.0 + -1.58326, + 50.008711, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 840, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.9168213581822446 + 0, + -2.5, + 1.8 ], "rotation_deg": [ - -31.386544293691394, - 150.0, - 0.0 + 0, + 50, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 841, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.9343156327554889 + -0.100308, + -2.499129, + 1.829026 ], "rotation_deg": [ - -31.989248933652974, - 150.0, - 0.0 + 1.58326, + 50.008711, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 842, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.9506458663949992 + -0.200546, + -2.496516, + 1.857972 ], "rotation_deg": [ - -32.55209895287106, - 150.0, - 0.0 + 3.162108, + 50.03484, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 843, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.965791713663166 + -0.300645, + -2.492163, + 1.886756 ], "rotation_deg": [ - -33.07439311031003, - 150.0, - 0.0 + 4.732142, + 50.078367, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 844, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.9797343047203133 + -0.400534, + -2.486074, + 1.915298 ], "rotation_deg": [ - -33.55548069248783, - 150.0, - 0.0 + 6.288987, + 50.139262, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 845, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.9924562688341714 + -0.500143, + -2.478252, + 1.943519 ], "rotation_deg": [ - -33.994762324184016, - 150.0, - 0.0 + 7.828303, + 50.217483, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 846, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -1.003941756021643 + -0.599405, + -2.468702, + 1.97134 ], "rotation_deg": [ - -34.39169071518656, - 150.0, - 0.0 + 9.345801, + 50.312975, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 847, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -1.0141764567959113 + -0.698248, + -2.457433, + 1.998683 ], "rotation_deg": [ - -34.745771342147385, - 150.0, - 0.0 + 10.83725, + 50.425673, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 848, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -1.0231476199942768 + -0.796605, + -2.44445, + 2.025472 ], "rotation_deg": [ - -35.05656306469681, - 150.0, - 0.0 + 12.298494, + 50.555496, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 849, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -1.030844068664519 + -0.894407, + -2.429764, + 2.051633 ], "rotation_deg": [ - -35.32367867504956, - 150.0, - 0.0 + 13.725459, + 50.702355, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 850, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -1.0372562139899875 + -0.991585, + -2.413385, + 2.077093 ], "rotation_deg": [ - -35.546785380417425, - 150.0, - 0.0 + 15.11417, + 50.866148, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 851, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -1.042376067236072 + -1.088072, + -2.395324, + 2.10178 ], "rotation_deg": [ - -35.72560521762764, - 150.0, - 0.0 + 16.460754, + 51.04676, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 852, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -1.0461972497031724 + -1.183801, + -2.375594, + 2.125627 ], "rotation_deg": [ - -35.859915399430406, - 150.0, - 0.0 + 17.761459, + 51.244065, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 853, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -1.048715000673763 + -1.278705, + -2.354207, + 2.148565 ], "rotation_deg": [ - -35.94954859206402, - 150.0, - 0.0 + 19.01266, + 51.457926, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 854, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -1.0499261833436555 + -1.372718, + -2.331181, + 2.170533 ], "rotation_deg": [ - -35.994393123731946, - 150.0, - 0.0 + 20.210869, + 51.688194, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 855, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -1.049829288730064 + -1.465775, + -2.306529, + 2.191467 ], "rotation_deg": [ - -35.99439312373195, - 150.0, - 0.0 + 21.352747, + 51.934709, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 856, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -1.0484244375516116 + -1.557809, + -2.28027, + 2.21131 ], "rotation_deg": [ - -35.94954859206402, - 150.0, - 0.0 + 22.435111, + 52.197298, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 857, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -1.0457133800779292 + -1.648758, + -2.252422, + 2.230007 ], "rotation_deg": [ - -35.859915399430406, - 150.0, - 0.0 + 23.454944, + 52.475778, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 858, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -1.0416994939490365 + -1.738558, + -2.223004, + 2.247506 ], "rotation_deg": [ - -35.725605217627646, - 150.0, - 0.0 + 24.409405, + 52.769956, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 859, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -1.036387779967226 + -1.827147, + -2.192037, + 2.263757 ], "rotation_deg": [ - -35.54678538041743, - 150.0, - 0.0 + 25.295831, + 53.079627, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 860, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -1.0297848558666833 + -1.914461, + -2.159543, + 2.278715 ], "rotation_deg": [ - -35.323678675049564, - 150.0, - 0.0 + 26.111754, + 53.404574, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 861, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -1.0218989480686154 + -2.000442, + -2.125543, + 2.29234 ], "rotation_deg": [ - -35.05656306469682, - 150.0, - 0.0 + 26.854899, + 53.744572, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 862, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -1.0127398814321538 + -2.085029, + -2.090062, + 2.304592 ], "rotation_deg": [ - -34.745771342147385, - 150.0, - 0.0 + 27.523194, + 54.099382, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 863, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -1.0023190670138047 + -2.168162, + -2.053124, + 2.315438 ], "rotation_deg": [ - -34.39169071518656, - 150.0, - 0.0 + 28.114777, + 54.468759, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 864, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.9906494878506922 + -2.249785, + -2.014756, + 2.324847 ], "rotation_deg": [ - -33.994762324184016, - 150.0, - 0.0 + 28.627999, + 54.852444, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 865, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.9777456827853115 + -2.329839, + -1.974983, + 2.332793 ], "rotation_deg": [ - -33.55548069248783, - 150.0, - 0.0 + 29.06143, + 55.250171, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 866, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.9636237283519411 + -2.40827, + -1.933834, + 2.339254 ], "rotation_deg": [ - -33.07439311031003, - 150.0, - 0.0 + 29.413861, + 55.661661, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 867, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.9483012187472833 + -2.485023, + -1.891337, + 2.344212 ], "rotation_deg": [ - -32.55209895287106, - 150.0, - 0.0 + 29.684311, + 56.086628, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 868, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.9317972439102862 + -2.560043, + -1.847522, + 2.347654 ], "rotation_deg": [ - -31.989248933652974, - 150.0, - 0.0 + 29.872025, + 56.524777, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 869, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.9141323657384564 + -2.63328, + -1.80242, + 2.349569 ], "rotation_deg": [ - -31.386544293691394, - 150.0, - 0.0 + 29.976481, + 56.975801, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 870, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.8953285924702951 + -2.704681, + -1.756061, + 2.349952 ], "rotation_deg": [ - -30.74473592791655, - 150.0, - 0.0 + 29.997386, + 57.439387, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 871, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.8754093512657752 + -2.774198, + -1.708479, + 2.348803 ], "rotation_deg": [ - -30.064623449631696, - 150.0, - 0.0 + 29.934684, + 57.915211, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 872, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.8543994590190134 + -2.841781, + -1.659706, + 2.346123 ], "rotation_deg": [ - -29.34705419429445, - 150.0, - 0.0 + 29.788548, + 58.402941, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 873, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.8323250914395149 + -2.907383, + -1.609776, + 2.341922 ], "rotation_deg": [ - -28.59292216384241, - 150.0, - 0.0 + 29.559386, + 58.902239, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 874, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.8092137504404965 + -2.97096, + -1.558725, + 2.33621 ], "rotation_deg": [ - -27.803166912878016, - 150.0, - 0.0 + 29.247837, + 59.412755, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 875, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.7850942298749269 + -3.032465, + -1.506587, + 2.329004 ], "rotation_deg": [ - -26.97877237810052, - 150.0, - 0.0 + 28.854769, + 59.934134, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 876, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.7599965796619728 + -3.091858, + -1.453399, + 2.320323 ], "rotation_deg": [ - -26.120765652443385, - 150.0, - 0.0 + 28.381278, + 60.466013, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 877, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.7339520683485409 + -3.149096, + -1.399198, + 2.310193 ], "rotation_deg": [ - -25.230215705444397, - 150.0, - 0.0 + 27.828683, + 61.008021, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 878, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.7069931441525621 + -3.204139, + -1.344022, + 2.29864 ], "rotation_deg": [ - -24.308232051442705, - 150.0, - 0.0 + 27.198524, + 61.55978, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 879, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.6791533945365528 + -3.256949, + -1.287909, + 2.285697 ], "rotation_deg": [ - -23.35596336726214, - 150.0, - 0.0 + 26.492558, + 62.120905, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 880, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.6504675043618187 + -3.307489, + -1.230899, + 2.2714 ], "rotation_deg": [ - -22.37459606110294, - 150.0, - 0.0 + 25.712753, + 62.691007, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 881, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.6209712126754369 + -3.355724, + -1.173031, + 2.25579 ], "rotation_deg": [ - -21.36535279442483, - 150.0, - 0.0 + 24.861281, + 63.269686, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 882, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.5907012681838537 + -3.401621, + -1.114346, + 2.238909 ], "rotation_deg": [ - -20.329490958663122, - 150.0, - 0.0 + 23.940517, + 63.856541, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 883, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.5596953834685725 + -3.445146, + -1.054884, + 2.220805 ], "rotation_deg": [ - -19.26830110867557, - 150.0, - 0.0 + 22.953026, + 64.451162, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 884, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.527992188000973 + -3.486271, + -0.994687, + 2.201529 ], "rotation_deg": [ - -18.183105354871696, - 150.0, - 0.0 + 21.901561, + 65.053134, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 885, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.4956311800148004 + -3.524966, + -0.933796, + 2.181133 ], "rotation_deg": [ - -17.07525571602787, - 150.0, - 0.0 + 20.789052, + 65.662039, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 886, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.46265267729628495 + -3.561205, + -0.872255, + 2.159674 ], "rotation_deg": [ - -15.94613243484027, - 150.0, - 0.0 + 19.618601, + 66.277451, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 887, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.429097766953201 + -3.594962, + -0.810106, + 2.137214 ], "rotation_deg": [ - -14.797142258314395, - 150.0, - 0.0 + 18.393469, + 66.898943, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 888, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.3950082542254441 + -3.626213, + -0.747392, + 2.113813 ], "rotation_deg": [ - -13.629716685133328, - 150.0, - 0.0 + 17.117071, + 67.52608, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 889, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.36042661040091306 + -3.654937, + -0.684157, + 2.089538 ], "rotation_deg": [ - -12.44531018218883, - 150.0, - 0.0 + 15.792965, + 68.158425, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 890, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.32539591990156996 + -3.681114, + -0.620446, + 2.064455 ], "rotation_deg": [ - -11.245398372496588, - 150.0, - 0.0 + 14.424841, + 68.795539, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 891, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.2899598266056169 + -3.704726, + -0.556302, + 2.038636 ], "rotation_deg": [ - -10.031476196753715, - 150.0, - 0.0 + 13.016512, + 69.436977, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 892, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.254162479472658 + -3.725756, + -0.491771, + 2.012152 ], "rotation_deg": [ - -8.80505605082879, - 150.0, - 0.0 + 11.571904, + 70.082291, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 893, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.21804847753959372 + -3.744189, + -0.426897, + 1.985076 ], "rotation_deg": [ - -7.5676659015049506, - 150.0, - 0.0 + 10.095043, + 70.731033, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 894, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.18166281435577425 + -3.760013, + -0.361725, + 1.957484 ], "rotation_deg": [ - -6.320847382823533, - 150.0, - 0.0 + 8.590045, + 71.38275, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 895, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.1450508219266409 + -3.773216, + -0.296301, + 1.929454 ], "rotation_deg": [ - -5.066153875400075, - 150.0, - 0.0 + 7.061106, + 72.036988, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 896, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.10825811423568805 + -3.78379, + -0.230671, + 1.901062 ], "rotation_deg": [ - -3.8051485711053608, - 150.0, - 0.0 + 5.512486, + 72.693291, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 897, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.07133053041512637 + -3.791727, + -0.16488, + 1.872389 ], "rotation_deg": [ - -2.5394025255232044, - 150.0, - 0.0 + 3.948501, + 73.351201, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 898, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - -0.03431407763602727 + -3.797021, + -0.098974, + 1.843514 ], "rotation_deg": [ - -1.2704927006106954, - 150.0, - 0.0 + 2.373511, + 74.010261, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 899, "type": "imguizmo", "translation": [ - 0.0, - 0.0, - 0.0027451262108815974 + -3.799669, + -0.032999, + 1.814518 ], "rotation_deg": [ - -3.3306690738754696e-15, - 150.0, - 0.0 + 0.791906, + 74.67001, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 900, - "type": "action", - "action": "set_active_render_window", - "value": 0 - }, - { - "frame": 900, - "type": "action", - "action": "set_active_planar", - "value": 5 - }, - { - "frame": 900, - "type": "action", - "action": "set_projection_type", - "value": "perspective" + "type": "imguizmo", + "translation": [ + -3.799669, + 0.032999, + 1.785482 + ], + "rotation_deg": [ + -0.791906, + 75.32999, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] }, { "frame": 901, "type": "imguizmo", "translation": [ - 3.5, - 0.0, - 0.0 + -3.797021, + 0.098974, + 1.756486 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + -2.373511, + 75.989739, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 902, "type": "imguizmo", "translation": [ - 3.498351706745195, - 0.09338121349488564, - 0.09598941546477674 + -3.791727, + 0.16488, + 1.727611 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + -3.948501, + 76.648799, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 903, "type": "imguizmo", "translation": [ - 3.493540744009305, - 0.17917558854636395, - 0.1838214632529401 + -3.78379, + 0.230671, + 1.698938 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + -5.512486, + 77.306709, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 904, "type": "imguizmo", "translation": [ - 3.485694420036376, - 0.2653443728925339, - 0.27135205401665224 + -3.773216, + 0.296301, + 1.670546 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + -7.061106, + 77.963012, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 905, "type": "imguizmo", "translation": [ - 3.474812805218336, - 0.3511347600105016, - 0.3574816359159584 + -3.760013, + 0.361725, + 1.642516 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + -8.590045, + 78.61725, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 906, "type": "imguizmo", "translation": [ - 3.460910233104283, - 0.4364915018366905, - 0.441834194013461 + -3.744189, + 0.426897, + 1.614924 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + -10.095043, + 79.268967, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 907, "type": "imguizmo", "translation": [ - 3.444003962454902, - 0.5213041236136793, - 0.5239852418432069 + -3.725756, + 0.491771, + 1.587848 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + -11.571904, + 79.917709, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 908, "type": "imguizmo", "translation": [ - 3.424115061347224, - 0.6054672898468222, - 0.6035258467183974 + -3.704726, + 0.556302, + 1.561364 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + -13.016512, + 80.563023, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 909, "type": "imguizmo", "translation": [ - 3.4012683084778628, - 0.6888761172736524, - 0.6800597141704652 + -3.681114, + 0.620446, + 1.535545 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + -14.424841, + 81.204461, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 910, "type": "imguizmo", "translation": [ - 3.375492168087725, - 0.771426690996607, - 0.7532055584956212 + -3.654937, + 0.684157, + 1.510462 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + -15.792965, + 81.841575, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 911, "type": "imguizmo", "translation": [ - 3.3468187540354477, - 0.8530161631111502, - 0.8225989706028591 + -3.626213, + 0.747392, + 1.486187 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + -17.117071, + 82.47392, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 912, "type": "imguizmo", "translation": [ - 3.3152837898246155, - 0.933542883308407, - 0.8878942360175692 + -3.594962, + 0.810106, + 1.462786 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + -18.393469, + 83.101057, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 913, "type": "imguizmo", "translation": [ - 3.2809265640937606, - 1.0129065253214347, - 0.9487660570107936 + -3.561205, + 0.872255, + 1.440326 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + -19.618601, + 83.722549, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 914, "type": "imguizmo", "translation": [ - 3.243789881667847, - 1.0910082119350464, - 1.004911173232379 + -3.524966, + 0.933796, + 1.418867 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + -20.789052, + 84.337961, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 915, "type": "imguizmo", "translation": [ - 3.203920010228772, - 1.1677506381732456, - 1.056049872537487 + -3.486271, + 0.994687, + 1.398471 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + -21.901561, + 84.946866, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 916, "type": "imguizmo", "translation": [ - 3.1613666226715935, - 1.2430381925292104, - 1.1019273844985356 + -3.445146, + 1.054884, + 1.379195 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + -22.953026, + 85.548838, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 917, "type": "imguizmo", "translation": [ - 3.116182735218291, - 1.316777076085318, - 1.142315149658643 + -3.401621, + 1.114346, + 1.361091 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + -23.940517, + 86.143459, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 918, "type": "imguizmo", "translation": [ - 3.068424641366156, - 1.388875419374914, - 1.1770119582033483 + -3.355724, + 1.173031, + 1.34421 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + -24.861281, + 86.730314, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 919, "type": "imguizmo", "translation": [ - 3.0181518417531, - 1.4592433968402316, - 1.2058449523777839 + -3.307489, + 1.230899, + 1.3286 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + -25.712753, + 87.308993, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 920, "type": "imguizmo", "translation": [ - 2.9654269700272744, - 1.527793338743847, - 1.2286704876553167 + -3.256949, + 1.287909, + 1.314303 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + -26.492558, + 87.879095, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 921, "type": "imguizmo", "translation": [ - 2.9103157148133394, - 1.594439840394259, - 1.2453748483673535 + -3.204139, + 1.344022, + 1.30136 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + -27.198524, + 88.44022, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 922, "type": "imguizmo", "translation": [ - 2.85288673787262, - 1.6590998685494973, - 1.255874814229081 + -3.149096, + 1.399198, + 1.289807 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + -27.828683, + 88.991979, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 923, "type": "imguizmo", "translation": [ - 2.793211588559101, - 1.7216928648662022, - 1.260118074938741 + -3.091858, + 1.453399, + 1.279677 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + -28.381278, + 89.533987, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 924, "type": "imguizmo", "translation": [ - 2.731364614677841, - 1.7821408462652917, - 1.2580834907849205 + -3.032465, + 1.506587, + 1.270996 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + -28.854769, + 90.065866, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 925, "type": "imguizmo", "translation": [ - 2.6674228698568667, - 1.8403685020891636, - 1.2497811979635312 + -2.97096, + 1.558725, + 1.26379 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + -29.247837, + 90.587245, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 926, "type": "imguizmo", "translation": [ - 2.601466017547946, - 1.8963032879294, - 1.2352525580797908 + -2.907383, + 1.609776, + 1.258078 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + -29.559386, + 91.097761, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 927, "type": "imguizmo", "translation": [ - 2.5335762317758475, - 1.9498755160080639, - 1.2145699520867876 + -2.841781, + 1.659706, + 1.253877 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + -29.788548, + 91.597059, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 928, "type": "imguizmo", "translation": [ - 2.4638380947597374, - 2.0010184419999955, - 1.1878364196872122 + -2.774198, + 1.708479, + 1.251197 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + -29.934684, + 92.084789, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 929, "type": "imguizmo", "translation": [ - 2.3923384915342716, - 2.0496683481879283, - 1.1551851459947415 + -2.704681, + 1.756061, + 1.250048 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + -29.997386, + 92.560613, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 930, "type": "imguizmo", "translation": [ - 2.3191665017016527, - 2.095764622846832, - 1.1167787980125026 + -2.63328, + 1.80242, + 1.250431 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + -29.976481, + 93.024199, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 931, "type": "imguizmo", "translation": [ - 2.244413288449548, - 2.1392498357585708, - 1.0728087142342453 + -2.560043, + 1.847522, + 1.252346 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + -29.872025, + 93.475223, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 932, "type": "imguizmo", "translation": [ - 2.168171984973103, - 2.180069809762807, - 1.0234939514055896 + -2.485023, + 1.891337, + 1.255788 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + -29.684311, + 93.913372, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 933, "type": "imguizmo", "translation": [ - 2.0905375784425826, - 2.2181736882549976, - 0.9690801931943411 + -2.40827, + 1.933834, + 1.260746 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + -29.413861, + 94.338339, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 934, "type": "imguizmo", "translation": [ - 2.011606791661178, - 2.2535139985473864, - 0.9098385262068122 + -2.329839, + 1.974983, + 1.267207 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + -29.06143, + 94.749829, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 935, "type": "imguizmo", "translation": [ - 1.9314779625604461, - 2.286046711014064, - 0.8460640894479889 + -2.249785, + 2.014756, + 1.275153 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + -28.627999, + 95.147556, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 936, "type": "imguizmo", "translation": [ - 1.8502509216834866, - 2.315731293946399, - 0.7780746039538516 + -2.168162, + 2.053124, + 1.284562 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + -28.114777, + 95.531241, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 937, "type": "imguizmo", "translation": [ - 1.7680268678085211, - 2.3425307640504998, - 0.7062087899211581 + -2.085029, + 2.090062, + 1.295408 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + -27.523194, + 95.900618, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 938, "type": "imguizmo", "translation": [ - 1.6849082418678123, - 2.3664117325237926, - 0.6308246792204576 + -2.000442, + 2.125543, + 1.30766 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + -26.854899, + 96.255428, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 939, "type": "imguizmo", "translation": [ - 1.6009985993190228, - 2.3873444466533114, - 0.5522978316993266 + -1.914461, + 2.159543, + 1.321285 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + -26.111754, + 96.595426, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 940, "type": "imguizmo", "translation": [ - 1.516402481128007, - 2.405302826883877, - 0.4710194641620934 + -1.827147, + 2.192037, + 1.336243 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + -25.295831, + 96.920373, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 941, "type": "imguizmo", "translation": [ - 1.4312252835237878, - 2.42026449930997, - 0.3873945013474034 + -1.738558, + 2.223004, + 1.352494 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + -24.409405, + 97.230044, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 942, "type": "imguizmo", "translation": [ - 1.3455731266879787, - 2.4322108235508333, - 0.30183955861353584 + -1.648758, + 2.252422, + 1.369993 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + -23.454944, + 97.524222, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 943, "type": "imguizmo", "translation": [ - 1.259552722542252, - 2.441126915974067, - 0.21478086638164712 + -1.557809, + 2.28027, + 1.38869 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + -22.435111, + 97.802702, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 944, "type": "imguizmo", "translation": [ - 1.1732712417985711, - 2.447001668238779, - 0.12665214667725008 + -1.465775, + 2.306529, + 1.408533 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + -21.352747, + 98.065291, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 945, "type": "imguizmo", "translation": [ - 1.0868361804378328, - 2.449827761135195, - 0.03789245234890409 + -1.372718, + 2.331181, + 1.429467 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + -20.210869, + 98.311806, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 946, "type": "imguizmo", "translation": [ - 1.000355225783255, - 2.4496016737034814, - -0.05105602027099654 + -1.278705, + 2.354207, + 1.451435 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + -19.01266, + 98.542074, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 947, "type": "imguizmo", "translation": [ - 0.9139361223353878, - 2.446323687620426, - -0.13975013436610284 + -1.183801, + 2.375594, + 1.474373 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + -17.761459, + 98.755935, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 948, "type": "imguizmo", "translation": [ - 0.8276865375358794, - 2.4399978868485, - -0.22774802032144126 + -1.088072, + 2.395324, + 1.49822 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + -16.460754, + 98.95324, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 949, "type": "imguizmo", "translation": [ - 0.7417139276272481, - 2.4306321525477506, - -0.314611277095613 + -0.991585, + 2.413385, + 1.522907 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + -15.11417, + 99.133852, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 950, "type": "imguizmo", "translation": [ - 0.6561254037757919, - 2.4182381532568598, - -0.399907156312722 + -0.894407, + 2.429764, + 1.548367 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + -13.725459, + 99.297645, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 951, "type": "imguizmo", "translation": [ - 0.5710275986244079, - 2.4028313303555935, - -0.4832107181930219 + -0.796605, + 2.44445, + 1.574528 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + -12.298494, + 99.444504, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 952, "type": "imguizmo", "translation": [ - 0.48652653344160013, - 2.3844308788267674, - -0.564106948581521 + -0.698248, + 2.457433, + 1.601317 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + -10.83725, + 99.574327, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 953, "type": "imguizmo", "translation": [ - 0.402727486032178, - 2.3630597233416903, - -0.6421928265276287 + -0.599405, + 2.468702, + 1.62866 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + -9.345801, + 99.687025, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 954, "type": "imguizmo", "translation": [ - 0.3197348595742256, - 2.338744489698876, - -0.7170793321152561 + -0.500143, + 2.478252, + 1.656481 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + -7.828303, + 99.782517, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 955, "type": "imguizmo", "translation": [ - 0.2376520525457393, - 2.3115154716516138, - -0.7883933845404711 + -0.400534, + 2.486074, + 1.684702 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + -6.288987, + 99.860738, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 956, "type": "imguizmo", "translation": [ - 0.1565813299030015, - 2.2814065931657255, - -0.855779700781296 + -0.300645, + 2.492163, + 1.713244 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + -4.732142, + 99.921633, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 957, "type": "imguizmo", "translation": [ - 0.0766236956711756, - 2.248455366154528, - -0.9189025655998558 + -0.200546, + 2.496516, + 1.742028 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + -3.162108, + 99.96516, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 958, "type": "imguizmo", "translation": [ - -0.0021212328941254133, - 2.2127028437436596, - -0.9774475040588004 + -0.100308, + 2.499129, + 1.770974 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + -1.58326, + 99.991289, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 959, "type": "imguizmo", "translation": [ - -0.07955534941753811, - 2.1741935691239997, - -1.0311228482196264 + 0, + 2.5, + 1.8 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 0, + 100, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 960, - "type": "imguizmo", - "translation": [ - -0.1555821806322771, - 2.132975520056397, - -1.0796611902176756 - ], - "rotation_deg": [ - 0.0, - 100.0, - 0.0 - ], - "scale": [ - 1.0, - 1.0, - 1.0 - ] + "type": "action", + "action": "set_active_render_window", + "value": 0 }, { - "frame": 961, - "type": "imguizmo", - "translation": [ - -0.23010700657381208, - 2.089100049097354, - -1.1228207144746687 - ], - "rotation_deg": [ - 0.0, - 100.0, - 0.0 - ], - "scale": [ - 1.0, - 1.0, - 1.0 - ] + "frame": 960, + "type": "action", + "action": "set_active_planar", + "value": 4 }, { - "frame": 962, - "type": "imguizmo", - "translation": [ - -0.30303697858914547, - 2.0426218196201393, - -1.1603864024117458 + "frame": 960, + "type": "action", + "action": "set_projection_type", + "value": "perspective" + }, + { + "frame": 960, + "type": "action", + "action": "set_left_handed", + "value": true + }, + { + "frame": 960, + "type": "action", + "action": "reset_active_camera", + "value": 1 + }, + { + "frame": 960, + "type": "action", + "action": "set_active_render_window", + "value": 1 + }, + { + "frame": 960, + "type": "action", + "action": "set_active_planar", + "value": 4 + }, + { + "frame": 960, + "type": "action", + "action": "set_projection_type", + "value": "orthographic" + }, + { + "frame": 960, + "type": "action", + "action": "set_left_handed", + "value": true + }, + { + "frame": 960, + "type": "action", + "action": "set_active_render_window", + "value": 0 + }, + { + "frame": 961, + "type": "imguizmo", + "translation": [ + 0, + 0, + 3.1 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 0, + 125, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 962, + "type": "imguizmo", + "translation": [ + 0, + 0, + 3.099756 + ], + "rotation_deg": [ + 1.108669, + 124.989546, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 963, "type": "imguizmo", "translation": [ - -0.3742812350146743, - 1.9935987377110287, - -1.1921711036612164 + 0, + 0, + 3.099024 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 2.216565, + 124.958192, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 964, "type": "imguizmo", "translation": [ - -0.44375101437850833, - 1.9420918800255336, - -1.2180164684402743 + 0, + 0, + 3.097806 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 3.322916, + 124.90596, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 965, "type": "imguizmo", "translation": [ - -0.5113597659862144, - 1.88816541769449, - -1.237793736441652 + 0, + 0, + 3.096101 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 4.426951, + 124.832886, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 966, "type": "imguizmo", "translation": [ - -0.5770232577521969, - 1.8318865363748278, - -1.2514043783109934 + 0, + 0, + 3.09391 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 5.527901, + 124.73902, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 967, "type": "imguizmo", "translation": [ - -0.6406596811423929, - 1.7733253525446013, - -1.2587805865151411 + 0, + 0, + 3.091237 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 6.624999, + 124.62443, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 968, "type": "imguizmo", "translation": [ - -0.7021897530975179, - 1.7125548261465942, - -1.2598856131558573 + 0, + 0, + 3.088081 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 7.71748, + 124.489193, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 969, "type": "imguizmo", "translation": [ - -0.7615368148098847, - 1.6496506696893103, - -1.2547139530460016 + 0, + 0, + 3.084446 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 8.804582, + 124.333405, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 970, "type": "imguizmo", "translation": [ - -0.8186269272307299, - 1.5846912539186218, - -1.2432913711360996 + 0, + 0, + 3.080334 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 9.885548, + 124.157174, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 971, "type": "imguizmo", "translation": [ - -0.8733889631890633, - 1.5177575101775755, - -1.2256747741546528 + 0, + 0, + 3.075748 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 10.959625, + 123.960623, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 972, "type": "imguizmo", "translation": [ - -0.9257546960072662, - 1.4489328295760187, - -1.201951927101681 + 0, + 0, + 3.070691 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 12.026064, + 123.743888, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 973, "type": "imguizmo", "translation": [ - -0.9756588845030363, - 1.3783029590956584, - -1.1722410160079066 + 0, + 0, + 3.065166 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 13.084121, + 123.507122, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 974, "type": "imguizmo", "translation": [ - -1.0230393542717746, - 1.3059558947600016, - -1.1366900591378948 + 0, + 0, + 3.059178 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 14.13306, + 123.250489, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 975, "type": "imguizmo", "translation": [ - -1.0678370751481527, - 1.2319817720022697, - -1.0954761695705044 + 0, + 0, + 3.052731 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 15.17215, + 122.974167, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 976, "type": "imguizmo", "translation": [ - -1.109996234750347, - 1.1564727533678683, - -1.048804672830439 + 0, + 0, + 3.045828 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 16.200666, + 122.67835, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 977, "type": "imguizmo", "translation": [ - -1.1494643080153202, - 1.079522913691332, - -0.9969080839668093 + 0, + 0, + 3.038476 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 17.217891, + 122.363243, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 978, "type": "imguizmo", "translation": [ - -1.186192122638504, - 1.0012281228908007, - -0.9400449491748561 + 0, + 0, + 3.030678 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 18.223117, + 122.029066, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 979, "type": "imguizmo", "translation": [ - -1.2201339203363704, - 0.9216859265260348, - -0.878498557731811 + 0, + 0, + 3.022441 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 19.215643, + 121.676052, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 980, "type": "imguizmo", "translation": [ - -1.2512474138555556, - 0.8409954242687956, - -0.8125755306639656 + 0, + 0, + 3.01377 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 20.194777, + 121.304448, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 981, "type": "imguizmo", "translation": [ - -1.2794938396575095, - 0.759257146436995, - -0.7426042931761426 + 0, + 0, + 3.004672 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 21.159837, + 120.914511, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 982, "type": "imguizmo", "translation": [ - -1.3048380062130394, - 0.6765729287464379, - -0.6689334384538466 + 0, + 0, + 2.995152 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 22.110151, + 120.506514, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 983, "type": "imguizmo", "translation": [ - -1.3272483378465731, - 0.5930457854362006, - -0.5919299909895472 + 0, + 0, + 2.985217 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 23.045055, + 120.080741, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 984, "type": "imguizmo", "translation": [ - -1.3466969140755187, - 0.5087797809257184, - -0.5119775780851219 + 0, + 0, + 2.974875 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 23.9639, + 119.637489, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 985, "type": "imguizmo", "translation": [ - -1.3631595043957085, - 0.4238799001634729, - -0.4294745186399297 + 0, + 0, + 2.964132 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 24.866043, + 119.177067, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 986, "type": "imguizmo", "translation": [ - -1.3766155984695898, - 0.33845191782882594, - -0.3448318387460978 + 0, + 0, + 2.952995 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 25.750857, + 118.699795, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 987, "type": "imguizmo", "translation": [ - -1.3870484316795537, - 0.2526022665499362, - -0.258471223977201 + 0, + 0, + 2.941473 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 26.617724, + 118.206007, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 988, "type": "imguizmo", "translation": [ - -1.3944450060145572, - 0.1664379043019601, - -0.17082291857194848 + 0, + 0, + 2.929574 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 27.466041, + 117.696046, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 989, "type": "imguizmo", "translation": [ - -1.3987961062640304, - 0.08006618115072876, - -0.08232358197902295 + 0, + 0, + 2.917306 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 28.295217, + 117.170268, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 990, "type": "imguizmo", "translation": [ - -1.4000963114988783, - -0.00640529449205858, - 0.0065858865583200005 + 0, + 0, + 2.904678 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 29.104673, + 116.629038, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 991, "type": "imguizmo", "translation": [ - -1.3983440018252848, - -0.09286878993552146, - 0.09546254454010955 + 0, + 0, + 2.891697 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 29.893846, + 116.072736, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 992, "type": "imguizmo", "translation": [ - -1.3935413604028986, - -0.17921658243111382, - 0.1838636129269121 + 0, + 0, + 2.878374 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 30.662185, + 115.501747, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 993, "type": "imguizmo", "translation": [ - -1.385694370724889, - -0.26534109338175493, - 0.2713486820427333 + 0, + 0, + 2.864718 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 31.409156, + 114.91647, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 994, "type": "imguizmo", "translation": [ - -1.3748128091632554, - -0.3511350223713651, - 0.35748190567387095 + 0, + 0, + 2.850737 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 32.134236, + 114.317313, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 995, "type": "imguizmo", "translation": [ - -1.36091023278869, - -0.4364914808478205, - 0.441834172432825 + 0, + 0, + 2.836443 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 32.836922, + 113.704694, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 996, "type": "imguizmo", "translation": [ - -1.3440039624801499, - -0.5213041252927882, - 0.5239852435696551 + 0, + 0, + 2.821844 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 33.516724, + 113.079039, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 997, "type": "imguizmo", "translation": [ - -1.3241150613452046, - -0.6054672897124932, - 0.6035258465802791 + 0, + 0, + 2.806952 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 34.173166, + 112.440785, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 998, "type": "imguizmo", "translation": [ - -1.301268308478024, - -0.6888761172843985, - 0.6800597141815122 + 0, + 0, + 2.791775 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 34.805794, + 111.790375, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 999, "type": "imguizmo", "translation": [ - -1.2754921680877125, - -0.7714266909957476, - 0.7532055584947357 + 0, + 0, + 2.776326 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 35.414164, + 111.128264, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1000, "type": "imguizmo", "translation": [ - -1.2468187540354487, - -0.8530161631112192, - 0.8225989706029284 + 0, + 0, + 2.760615 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 35.997854, + 110.454914, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1001, "type": "imguizmo", "translation": [ - -1.2152837898246154, - -0.9335428833084021, - 0.8878942360175622 + 0, + 0, + 2.744652 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 36.556456, + 109.770792, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1002, "type": "imguizmo", "translation": [ - -1.1809265640937605, - -1.0129065253214358, - 0.9487660570107929 + 0, + 0, + 2.728449 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 37.089581, + 109.076376, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1003, "type": "imguizmo", "translation": [ - -1.143789881667848, - -1.0910082119350462, - 1.0049111732323772 + 0, + 0, + 2.712017 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 37.596858, + 108.372151, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1004, "type": "imguizmo", "translation": [ - -1.1039200102287723, - -1.1677506381732456, - 1.0560498725374856 + 0, + 0, + 2.695367 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 38.077933, + 107.658606, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1005, "type": "imguizmo", "translation": [ - -1.0613666226715934, - -1.2430381925292107, - 1.1019273844985342 + 0, + 0, + 2.678512 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 38.532471, + 106.936239, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1006, "type": "imguizmo", "translation": [ - -1.0161827352182906, - -1.3167770760853184, - 1.1423151496586417 + 0, + 0, + 2.661463 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 38.960156, + 106.205553, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1007, "type": "imguizmo", "translation": [ - -0.9684246413661551, - -1.3888754193749149, - 1.1770119582033471 + 0, + 0, + 2.644231 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 39.360688, + 105.467058, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1008, "type": "imguizmo", "translation": [ - -0.9181518417530996, - -1.4592433968402323, - 1.2058449523777828 + 0, + 0, + 2.62683 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 39.733789, + 104.721269, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1009, "type": "imguizmo", "translation": [ - -0.8654269700272739, - -1.5277933387438483, - 1.2286704876553156 + 0, + 0, + 2.60927 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 40.079199, + 103.968705, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1010, "type": "imguizmo", "translation": [ - -0.8103157148133387, - -1.5944398403942603, - 1.245374848367352 + 0, + 0, + 2.591564 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 40.396677, + 103.20989, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1011, "type": "imguizmo", "translation": [ - -0.752886737872621, - -1.6590998685494966, - 1.2558748142290797 + 0, + 0, + 2.573725 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 40.686002, + 102.445353, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1012, "type": "imguizmo", "translation": [ - -0.6932115885591016, - -1.7216928648662024, - 1.2601180749387397 + 0, + 0, + 2.555765 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 40.946972, + 101.675628, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1013, "type": "imguizmo", "translation": [ - -0.6313646146778419, - -1.7821408462652917, - 1.2580834907849197 + 0, + 0, + 2.537696 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 41.179406, + 100.90125, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1014, "type": "imguizmo", "translation": [ - -0.5674228698568673, - -1.8403685020891636, - 1.24978119796353 + 0, + 0, + 2.519531 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 41.383141, + 100.12276, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1015, "type": "imguizmo", "translation": [ - -0.5014660175479455, - -1.8963032879294006, - 1.2352525580797895 + 0, + 0, + 2.501283 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 41.558036, + 99.3407, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1016, "type": "imguizmo", "translation": [ - -0.43357623177584687, - -1.9498755160080645, - 1.2145699520867863 + 0, + 0, + 2.482964 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 41.703968, + 98.555614, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1017, "type": "imguizmo", "translation": [ - -0.3638380947597374, - -2.001018441999997, - 1.1878364196872109 + 0, + 0, + 2.464588 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 41.820835, + 97.768051, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1018, "type": "imguizmo", "translation": [ - -0.29233849153427044, - -2.0496683481879296, - 1.15518514599474 + 0, + 0, + 2.446166 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 41.908558, + 96.978558, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1019, "type": "imguizmo", "translation": [ - -0.21916650170165367, - -2.095764622846832, - 1.1167787980125021 + 0, + 0, + 2.427713 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 41.967073, + 96.187687, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1020, "type": "imguizmo", "translation": [ - -0.1444132884495489, - -2.1392498357585708, - 1.0728087142342444 + 0, + 0, + 2.40924 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 41.996341, + 95.395988, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1021, "type": "imguizmo", "translation": [ - -0.06817198497310444, - -2.180069809762807, - 1.023493951405589 + 0, + 0, + 2.39076 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 41.996341, + 94.604012, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1022, "type": "imguizmo", "translation": [ - 0.009462421557417125, - -2.2181736882549976, - 0.9690801931943398 + 0, + 0, + 2.372287 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 41.967073, + 93.812313, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1023, "type": "imguizmo", "translation": [ - 0.08839320833882182, - -2.253513998547387, - 0.909838526206811 + 0, + 0, + 2.353834 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 41.908558, + 93.021442, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1024, "type": "imguizmo", "translation": [ - 0.16852203743955418, - -2.2860467110140643, - 0.8460640894479874 + 0, + 0, + 2.335412 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 41.820835, + 92.231949, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1025, "type": "imguizmo", "translation": [ - 0.24974907831651344, - -2.3157312939463996, - 0.7780746039538501 + 0, + 0, + 2.317036 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 41.703968, + 91.444386, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1026, "type": "imguizmo", "translation": [ - 0.33197313219147934, - -2.3425307640505, - 0.706208789921156 + 0, + 0, + 2.298717 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 41.558036, + 90.6593, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1027, "type": "imguizmo", "translation": [ - 0.41509175813218596, - -2.366411732523793, - 0.6308246792204575 + 0, + 0, + 2.280469 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 41.383141, + 89.87724, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1028, "type": "imguizmo", "translation": [ - 0.4990014006809761, - -2.3873444466533127, - 0.5522978316993259 + 0, + 0, + 2.262304 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 41.179406, + 89.09875, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1029, "type": "imguizmo", "translation": [ - 0.583597518871992, - -2.405302826883878, - 0.47101946416209284 + 0, + 0, + 2.244235 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 40.946972, + 88.324372, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1030, "type": "imguizmo", "translation": [ - 0.6687747164762112, - -2.4202644993099707, - 0.38739450134740283 + 0, + 0, + 2.226275 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 40.686002, + 87.554647, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1031, "type": "imguizmo", "translation": [ - 0.7544268733120203, - -2.4322108235508346, - 0.30183955861353534 + 0, + 0, + 2.208436 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 40.396677, + 86.79011, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1032, "type": "imguizmo", "translation": [ - 0.8404472774577475, - -2.4411269159740687, - 0.21478086638164598 + 0, + 0, + 2.19073 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 40.079199, + 86.031295, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1033, "type": "imguizmo", "translation": [ - 0.9267287582014283, - -2.4470016682387805, - 0.126652146677249 + 0, + 0, + 2.17317 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 39.733789, + 85.278731, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1034, "type": "imguizmo", "translation": [ - 1.0131638195621673, - -2.449827761135196, - 0.03789245234890243 + 0, + 0, + 2.155769 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 39.360688, + 84.532942, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1035, "type": "imguizmo", "translation": [ - 1.0996447742167426, - -2.4496016737034836, - -0.05105602027099576 + 0, + 0, + 2.138537 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 38.960156, + 83.794447, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1036, "type": "imguizmo", "translation": [ - 1.1860638776646102, - -2.4463236876204273, - -0.13975013436610229 + 0, + 0, + 2.121488 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 38.532471, + 83.063761, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1037, "type": "imguizmo", "translation": [ - 1.2723134624641195, - -2.4399978868485013, - -0.2277480203214419 + 0, + 0, + 2.104633 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 38.077933, + 82.341394, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1038, "type": "imguizmo", "translation": [ - 1.3582860723727506, - -2.4306321525477523, - -0.31461127709561343 + 0, + 0, + 2.087983 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 37.596858, + 81.627849, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1039, "type": "imguizmo", "translation": [ - 1.443874596224207, - -2.418238153256861, - -0.3999071563127225 + 0, + 0, + 2.071551 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 37.089581, + 80.923624, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1040, "type": "imguizmo", "translation": [ - 1.5289724013755908, - -2.402831330355595, - -0.4832107181930224 + 0, + 0, + 2.055348 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 36.556456, + 80.229208, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1041, "type": "imguizmo", "translation": [ - 1.6134734665583992, - -2.3844308788267687, - -0.564106948581522 + 0, + 0, + 2.039385 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 35.997854, + 79.545086, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1042, "type": "imguizmo", "translation": [ - 1.697272513967821, - -2.3630597233416917, - -0.6421928265276298 + 0, + 0, + 2.023674 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 35.414164, + 78.871736, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1043, "type": "imguizmo", "translation": [ - 1.7802651404257712, - -2.3387444896988776, - -0.717079332115255 + 0, + 0, + 2.008225 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 34.805794, + 78.209625, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1044, "type": "imguizmo", "translation": [ - 1.862347947454258, - -2.3115154716516155, - -0.7883933845404703 + 0, + 0, + 1.993048 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 34.173166, + 77.559215, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1045, "type": "imguizmo", "translation": [ - 1.943418670096997, - -2.281406593165727, - -0.8557797007812963 + 0, + 0, + 1.978156 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 33.516724, + 76.920961, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1046, "type": "imguizmo", "translation": [ - 2.023376304328823, - -2.2484553661545297, - -0.9189025655998562 + 0, + 0, + 1.963557 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 32.836922, + 76.295306, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1047, "type": "imguizmo", "translation": [ - 2.102121232894123, - -2.2127028437436613, - -0.9774475040588003 + 0, + 0, + 1.949263 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 32.134236, + 75.682687, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1048, "type": "imguizmo", "translation": [ - 2.179555349417536, - -2.1741935691240015, - -1.0311228482196262 + 0, + 0, + 1.935282 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 31.409156, + 75.08353, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1049, "type": "imguizmo", "translation": [ - 2.255582180632276, - -2.1329755200563985, - -1.0796611902176763 + 0, + 0, + 1.921626 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 30.662185, + 74.498253, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1050, "type": "imguizmo", "translation": [ - 2.330107006573811, - -2.0891000490973552, - -1.1228207144746691 + 0, + 0, + 1.908303 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 29.893846, + 73.927264, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1051, "type": "imguizmo", "translation": [ - 2.4030369785891423, - -2.042621819620142, - -1.1603864024117454 + 0, + 0, + 1.895322 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 29.104673, + 73.370962, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1052, "type": "imguizmo", "translation": [ - 2.4742812350146717, - -1.9935987377110316, - -1.192171103661216 + 0, + 0, + 1.882694 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 28.295217, + 72.829732, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1053, "type": "imguizmo", "translation": [ - 2.5437510143785067, - -1.9420918800255351, - -1.2180164684402741 + 0, + 0, + 1.870426 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 27.466041, + 72.303954, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1054, "type": "imguizmo", "translation": [ - 2.611359765986213, - -1.888165417694492, - -1.237793736441652 + 0, + 0, + 1.858527 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 26.617724, + 71.793993, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1055, "type": "imguizmo", "translation": [ - 2.677023257752195, - -1.8318865363748296, - -1.2514043783109934 + 0, + 0, + 1.847005 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 25.750857, + 71.300205, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1056, "type": "imguizmo", "translation": [ - 2.7406596811423913, - -1.7733253525446033, - -1.2587805865151411 + 0, + 0, + 1.835868 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 24.866043, + 70.822933, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1057, "type": "imguizmo", "translation": [ - 2.802189753097517, - -1.7125548261465957, - -1.2598856131558573 + 0, + 0, + 1.825125 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 23.9639, + 70.362511, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1058, "type": "imguizmo", "translation": [ - 2.861536814809883, - -1.6496506696893118, - -1.2547139530460016 + 0, + 0, + 1.814783 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 23.045055, + 69.919259, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1059, "type": "imguizmo", "translation": [ - 2.9186269272307284, - -1.5846912539186233, - -1.2432913711360996 + 0, + 0, + 1.804848 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 22.110151, + 69.493486, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1060, "type": "imguizmo", "translation": [ - 2.9733889631890618, - -1.517757510177577, - -1.2256747741546528 + 0, + 0, + 1.795328 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 21.159837, + 69.085489, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1061, "type": "imguizmo", "translation": [ - 3.025754696007265, - -1.4489328295760193, - -1.2019519271016808 + 0, + 0, + 1.78623 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 20.194777, + 68.695552, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1062, "type": "imguizmo", "translation": [ - 3.0756588845030355, - -1.3783029590956586, - -1.1722410160079064 + 0, + 0, + 1.777559 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 19.215643, + 68.323948, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1063, "type": "imguizmo", "translation": [ - 3.123039354271773, - -1.3059558947600025, - -1.1366900591378946 + 0, + 0, + 1.769322 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 18.223117, + 67.970934, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1064, "type": "imguizmo", "translation": [ - 3.1678370751481513, - -1.2319817720022703, - -1.095476169570504 + 0, + 0, + 1.761524 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 17.217891, + 67.636757, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1065, "type": "imguizmo", "translation": [ - 3.2099962347503466, - -1.1564727533678676, - -1.0488046728304379 + 0, + 0, + 1.754172 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 16.200666, + 67.32165, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1066, "type": "imguizmo", "translation": [ - 3.2494643080153196, - -1.0795229136913314, - -0.996908083966808 + 0, + 0, + 1.747269 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 15.17215, + 67.025833, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1067, "type": "imguizmo", "translation": [ - 3.2861921226385027, - -1.0012281228908024, - -0.9400449491748564 + 0, + 0, + 1.740822 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 14.13306, + 66.749511, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1068, "type": "imguizmo", "translation": [ - 3.320133920336369, - -0.9216859265260363, - -0.8784985577318112 + 0, + 0, + 1.734834 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 13.084121, + 66.492878, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1069, "type": "imguizmo", "translation": [ - 3.351247413855554, - -0.840995424268797, - -0.8125755306639658 + 0, + 0, + 1.729309 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 12.026064, + 66.256112, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1070, "type": "imguizmo", "translation": [ - 3.3794938396575076, - -0.7592571464369966, - -0.7426042931761428 + 0, + 0, + 1.724252 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 10.959625, + 66.039377, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1071, "type": "imguizmo", "translation": [ - 3.404838006213038, - -0.6765729287464395, - -0.6689334384538467 + 0, + 0, + 1.719666 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 9.885548, + 65.842826, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1072, "type": "imguizmo", "translation": [ - 3.4272483378465712, - -0.5930457854362021, - -0.5919299909895473 + 0, + 0, + 1.715554 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 8.804582, + 65.666595, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1073, "type": "imguizmo", "translation": [ - 3.4466969140755173, - -0.5087797809257188, - -0.5119775780851209 + 0, + 0, + 1.711919 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 7.71748, + 65.510807, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1074, "type": "imguizmo", "translation": [ - 3.4631595043957066, - -0.42387990016347343, - -0.4294745186399287 + 0, + 0, + 1.708763 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 6.624999, + 65.37557, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1075, "type": "imguizmo", "translation": [ - 3.476615598469588, - -0.3384519178288289, - -0.344831838746099 + 0, + 0, + 1.70609 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 5.527901, + 65.26098, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1076, "type": "imguizmo", "translation": [ - 3.4870484316795514, - -0.25260226654993895, - -0.2584712239772022 + 0, + 0, + 1.703899 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 4.426951, + 65.167114, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1077, "type": "imguizmo", "translation": [ - 3.494445006014555, - -0.1664379043019617, - -0.17082291857194842 + 0, + 0, + 1.702194 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 3.322916, + 65.09404, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1078, "type": "imguizmo", "translation": [ - 3.4987961062640283, - -0.08006618115073047, - -0.08232358197902302 + 0, + 0, + 1.700976 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 2.216565, + 65.041808, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1079, "type": "imguizmo", "translation": [ - 3.5000963114988757, - 0.006405294492056901, - 0.006585886558319931 + 0, + 0, + 1.700244 ], "rotation_deg": [ - 0.0, - 100.0, - 0.0 + 1.108669, + 65.010454, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1080, - "type": "action", - "action": "set_active_render_window", - "value": 0 - }, - { - "frame": 1080, - "type": "action", - "action": "set_active_planar", - "value": 6 - }, - { - "frame": 1080, - "type": "action", - "action": "set_projection_type", - "value": "perspective" + "type": "imguizmo", + "translation": [ + 0, + 0, + 1.7 + ], + "rotation_deg": [ + 0, + 65, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] }, { "frame": 1081, "type": "imguizmo", "translation": [ - 3.5, - 0.0, - 0.0 + 0, + 0, + 1.700244 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -1.108669, + 65.010454, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1082, "type": "imguizmo", "translation": [ - 3.498351706745195, - 0.09338121349488564, - 0.09598941546477674 + 0, + 0, + 1.700976 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -2.216565, + 65.041808, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1083, "type": "imguizmo", "translation": [ - 3.493540744009305, - 0.17917558854636395, - 0.1838214632529401 + 0, + 0, + 1.702194 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -3.322916, + 65.09404, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1084, "type": "imguizmo", "translation": [ - 3.485694420036376, - 0.2653443728925339, - 0.27135205401665224 + 0, + 0, + 1.703899 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -4.426951, + 65.167114, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1085, "type": "imguizmo", "translation": [ - 3.474812805218336, - 0.3511347600105016, - 0.3574816359159584 + 0, + 0, + 1.70609 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -5.527901, + 65.26098, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1086, "type": "imguizmo", "translation": [ - 3.460910233104283, - 0.4364915018366905, - 0.441834194013461 + 0, + 0, + 1.708763 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -6.624999, + 65.37557, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1087, "type": "imguizmo", "translation": [ - 3.444003962454902, - 0.5213041236136793, - 0.5239852418432069 + 0, + 0, + 1.711919 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -7.71748, + 65.510807, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1088, "type": "imguizmo", "translation": [ - 3.424115061347224, - 0.6054672898468222, - 0.6035258467183974 + 0, + 0, + 1.715554 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -8.804582, + 65.666595, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1089, "type": "imguizmo", "translation": [ - 3.4012683084778628, - 0.6888761172736524, - 0.6800597141704652 + 0, + 0, + 1.719666 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -9.885548, + 65.842826, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1090, "type": "imguizmo", "translation": [ - 3.375492168087725, - 0.771426690996607, - 0.7532055584956212 + 0, + 0, + 1.724252 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -10.959625, + 66.039377, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1091, "type": "imguizmo", "translation": [ - 3.3468187540354477, - 0.8530161631111502, - 0.8225989706028591 + 0, + 0, + 1.729309 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -12.026064, + 66.256112, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1092, "type": "imguizmo", "translation": [ - 3.3152837898246155, - 0.933542883308407, - 0.8878942360175692 + 0, + 0, + 1.734834 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -13.084121, + 66.492878, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1093, "type": "imguizmo", "translation": [ - 3.2809265640937606, - 1.0129065253214347, - 0.9487660570107936 + 0, + 0, + 1.740822 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -14.13306, + 66.749511, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1094, "type": "imguizmo", "translation": [ - 3.243789881667847, - 1.0910082119350464, - 1.004911173232379 + 0, + 0, + 1.747269 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -15.17215, + 67.025833, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1095, "type": "imguizmo", "translation": [ - 3.203920010228772, - 1.1677506381732456, - 1.056049872537487 + 0, + 0, + 1.754172 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -16.200666, + 67.32165, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1096, "type": "imguizmo", "translation": [ - 3.1613666226715935, - 1.2430381925292104, - 1.1019273844985356 + 0, + 0, + 1.761524 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -17.217891, + 67.636757, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1097, "type": "imguizmo", "translation": [ - 3.116182735218291, - 1.316777076085318, - 1.142315149658643 + 0, + 0, + 1.769322 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -18.223117, + 67.970934, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1098, "type": "imguizmo", "translation": [ - 3.068424641366156, - 1.388875419374914, - 1.1770119582033483 + 0, + 0, + 1.777559 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -19.215643, + 68.323948, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1099, "type": "imguizmo", "translation": [ - 3.0181518417531, - 1.4592433968402316, - 1.2058449523777839 + 0, + 0, + 1.78623 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -20.194777, + 68.695552, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1100, "type": "imguizmo", "translation": [ - 2.9654269700272744, - 1.527793338743847, - 1.2286704876553167 + 0, + 0, + 1.795328 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -21.159837, + 69.085489, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1101, "type": "imguizmo", "translation": [ - 2.9103157148133394, - 1.594439840394259, - 1.2453748483673535 + 0, + 0, + 1.804848 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -22.110151, + 69.493486, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1102, "type": "imguizmo", "translation": [ - 2.85288673787262, - 1.6590998685494973, - 1.255874814229081 + 0, + 0, + 1.814783 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -23.045055, + 69.919259, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1103, "type": "imguizmo", "translation": [ - 2.793211588559101, - 1.7216928648662022, - 1.260118074938741 + 0, + 0, + 1.825125 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -23.9639, + 70.362511, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1104, "type": "imguizmo", "translation": [ - 2.731364614677841, - 1.7821408462652917, - 1.2580834907849205 + 0, + 0, + 1.835868 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -24.866043, + 70.822933, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1105, "type": "imguizmo", "translation": [ - 2.6674228698568667, - 1.8403685020891636, - 1.2497811979635312 + 0, + 0, + 1.847005 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -25.750857, + 71.300205, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1106, "type": "imguizmo", "translation": [ - 2.601466017547946, - 1.8963032879294, - 1.2352525580797908 + 0, + 0, + 1.858527 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -26.617724, + 71.793993, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1107, "type": "imguizmo", "translation": [ - 2.5335762317758475, - 1.9498755160080639, - 1.2145699520867876 + 0, + 0, + 1.870426 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -27.466041, + 72.303954, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1108, "type": "imguizmo", "translation": [ - 2.4638380947597374, - 2.0010184419999955, - 1.1878364196872122 + 0, + 0, + 1.882694 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -28.295217, + 72.829732, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1109, "type": "imguizmo", "translation": [ - 2.3923384915342716, - 2.0496683481879283, - 1.1551851459947415 + 0, + 0, + 1.895322 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -29.104673, + 73.370962, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1110, "type": "imguizmo", "translation": [ - 2.3191665017016527, - 2.095764622846832, - 1.1167787980125026 + 0, + 0, + 1.908303 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -29.893846, + 73.927264, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1111, "type": "imguizmo", "translation": [ - 2.244413288449548, - 2.1392498357585708, - 1.0728087142342453 + 0, + 0, + 1.921626 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -30.662185, + 74.498253, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1112, "type": "imguizmo", "translation": [ - 2.168171984973103, - 2.180069809762807, - 1.0234939514055896 + 0, + 0, + 1.935282 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -31.409156, + 75.08353, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1113, "type": "imguizmo", "translation": [ - 2.0905375784425826, - 2.2181736882549976, - 0.9690801931943411 + 0, + 0, + 1.949263 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -32.134236, + 75.682687, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1114, "type": "imguizmo", "translation": [ - 2.011606791661178, - 2.2535139985473864, - 0.9098385262068122 + 0, + 0, + 1.963557 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -32.836922, + 76.295306, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1115, "type": "imguizmo", "translation": [ - 1.9314779625604461, - 2.286046711014064, - 0.8460640894479889 + 0, + 0, + 1.978156 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -33.516724, + 76.920961, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1116, "type": "imguizmo", "translation": [ - 1.8502509216834866, - 2.315731293946399, - 0.7780746039538516 + 0, + 0, + 1.993048 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -34.173166, + 77.559215, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1117, "type": "imguizmo", "translation": [ - 1.7680268678085211, - 2.3425307640504998, - 0.7062087899211581 + 0, + 0, + 2.008225 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -34.805794, + 78.209625, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1118, "type": "imguizmo", "translation": [ - 1.6849082418678123, - 2.3664117325237926, - 0.6308246792204576 + 0, + 0, + 2.023674 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -35.414164, + 78.871736, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1119, "type": "imguizmo", "translation": [ - 1.6009985993190228, - 2.3873444466533114, - 0.5522978316993266 + 0, + 0, + 2.039385 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -35.997854, + 79.545086, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1120, "type": "imguizmo", "translation": [ - 1.516402481128007, - 2.405302826883877, - 0.4710194641620934 + 0, + 0, + 2.055348 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -36.556456, + 80.229208, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1121, "type": "imguizmo", "translation": [ - 1.4312252835237878, - 2.42026449930997, - 0.3873945013474034 + 0, + 0, + 2.071551 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -37.089581, + 80.923624, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1122, "type": "imguizmo", "translation": [ - 1.3455731266879787, - 2.4322108235508333, - 0.30183955861353584 + 0, + 0, + 2.087983 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -37.596858, + 81.627849, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1123, "type": "imguizmo", "translation": [ - 1.259552722542252, - 2.441126915974067, - 0.21478086638164712 + 0, + 0, + 2.104633 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -38.077933, + 82.341394, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1124, "type": "imguizmo", "translation": [ - 1.1732712417985711, - 2.447001668238779, - 0.12665214667725008 + 0, + 0, + 2.121488 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -38.532471, + 83.063761, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1125, "type": "imguizmo", "translation": [ - 1.0868361804378328, - 2.449827761135195, - 0.03789245234890409 + 0, + 0, + 2.138537 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -38.960156, + 83.794447, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1126, "type": "imguizmo", "translation": [ - 1.000355225783255, - 2.4496016737034814, - -0.05105602027099654 + 0, + 0, + 2.155769 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -39.360688, + 84.532942, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1127, "type": "imguizmo", "translation": [ - 0.9139361223353878, - 2.446323687620426, - -0.13975013436610284 + 0, + 0, + 2.17317 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -39.733789, + 85.278731, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1128, "type": "imguizmo", "translation": [ - 0.8276865375358794, - 2.4399978868485, - -0.22774802032144126 + 0, + 0, + 2.19073 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -40.079199, + 86.031295, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1129, "type": "imguizmo", "translation": [ - 0.7417139276272481, - 2.4306321525477506, - -0.314611277095613 + 0, + 0, + 2.208436 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -40.396677, + 86.79011, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1130, "type": "imguizmo", "translation": [ - 0.6561254037757919, - 2.4182381532568598, - -0.399907156312722 + 0, + 0, + 2.226275 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -40.686002, + 87.554647, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1131, "type": "imguizmo", "translation": [ - 0.5710275986244079, - 2.4028313303555935, - -0.4832107181930219 + 0, + 0, + 2.244235 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -40.946972, + 88.324372, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1132, "type": "imguizmo", "translation": [ - 0.48652653344160013, - 2.3844308788267674, - -0.564106948581521 + 0, + 0, + 2.262304 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -41.179406, + 89.09875, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1133, "type": "imguizmo", "translation": [ - 0.402727486032178, - 2.3630597233416903, - -0.6421928265276287 + 0, + 0, + 2.280469 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -41.383141, + 89.87724, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1134, "type": "imguizmo", "translation": [ - 0.3197348595742256, - 2.338744489698876, - -0.7170793321152561 + 0, + 0, + 2.298717 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -41.558036, + 90.6593, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1135, "type": "imguizmo", "translation": [ - 0.2376520525457393, - 2.3115154716516138, - -0.7883933845404711 + 0, + 0, + 2.317036 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -41.703968, + 91.444386, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1136, "type": "imguizmo", "translation": [ - 0.1565813299030015, - 2.2814065931657255, - -0.855779700781296 + 0, + 0, + 2.335412 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -41.820835, + 92.231949, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1137, "type": "imguizmo", "translation": [ - 0.0766236956711756, - 2.248455366154528, - -0.9189025655998558 + 0, + 0, + 2.353834 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -41.908558, + 93.021442, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1138, "type": "imguizmo", "translation": [ - -0.0021212328941254133, - 2.2127028437436596, - -0.9774475040588004 + 0, + 0, + 2.372287 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -41.967073, + 93.812313, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1139, "type": "imguizmo", "translation": [ - -0.07955534941753811, - 2.1741935691239997, - -1.0311228482196264 + 0, + 0, + 2.39076 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -41.996341, + 94.604012, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1140, "type": "imguizmo", "translation": [ - -0.1555821806322771, - 2.132975520056397, - -1.0796611902176756 + 0, + 0, + 2.40924 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -41.996341, + 95.395988, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1141, "type": "imguizmo", "translation": [ - -0.23010700657381208, - 2.089100049097354, - -1.1228207144746687 + 0, + 0, + 2.427713 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -41.967073, + 96.187687, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1142, "type": "imguizmo", "translation": [ - -0.30303697858914547, - 2.0426218196201393, - -1.1603864024117458 + 0, + 0, + 2.446166 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -41.908558, + 96.978558, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1143, "type": "imguizmo", "translation": [ - -0.3742812350146743, - 1.9935987377110287, - -1.1921711036612164 + 0, + 0, + 2.464588 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -41.820835, + 97.768051, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1144, "type": "imguizmo", "translation": [ - -0.44375101437850833, - 1.9420918800255336, - -1.2180164684402743 + 0, + 0, + 2.482964 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -41.703968, + 98.555614, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1145, "type": "imguizmo", "translation": [ - -0.5113597659862144, - 1.88816541769449, - -1.237793736441652 + 0, + 0, + 2.501283 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -41.558036, + 99.3407, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1146, "type": "imguizmo", "translation": [ - -0.5770232577521969, - 1.8318865363748278, - -1.2514043783109934 + 0, + 0, + 2.519531 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -41.383141, + 100.12276, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1147, "type": "imguizmo", "translation": [ - -0.6406596811423929, - 1.7733253525446013, - -1.2587805865151411 + 0, + 0, + 2.537696 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -41.179406, + 100.90125, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1148, "type": "imguizmo", "translation": [ - -0.7021897530975179, - 1.7125548261465942, - -1.2598856131558573 + 0, + 0, + 2.555765 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -40.946972, + 101.675628, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1149, "type": "imguizmo", "translation": [ - -0.7615368148098847, - 1.6496506696893103, - -1.2547139530460016 + 0, + 0, + 2.573725 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -40.686002, + 102.445353, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1150, "type": "imguizmo", "translation": [ - -0.8186269272307299, - 1.5846912539186218, - -1.2432913711360996 + 0, + 0, + 2.591564 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -40.396677, + 103.20989, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1151, "type": "imguizmo", "translation": [ - -0.8733889631890633, - 1.5177575101775755, - -1.2256747741546528 + 0, + 0, + 2.60927 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -40.079199, + 103.968705, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1152, "type": "imguizmo", "translation": [ - -0.9257546960072662, - 1.4489328295760187, - -1.201951927101681 + 0, + 0, + 2.62683 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -39.733789, + 104.721269, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1153, "type": "imguizmo", "translation": [ - -0.9756588845030363, - 1.3783029590956584, - -1.1722410160079066 + 0, + 0, + 2.644231 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -39.360688, + 105.467058, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1154, "type": "imguizmo", "translation": [ - -1.0230393542717746, - 1.3059558947600016, - -1.1366900591378948 + 0, + 0, + 2.661463 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -38.960156, + 106.205553, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1155, "type": "imguizmo", "translation": [ - -1.0678370751481527, - 1.2319817720022697, - -1.0954761695705044 + 0, + 0, + 2.678512 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -38.532471, + 106.936239, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1156, "type": "imguizmo", "translation": [ - -1.109996234750347, - 1.1564727533678683, - -1.048804672830439 + 0, + 0, + 2.695367 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -38.077933, + 107.658606, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1157, "type": "imguizmo", "translation": [ - -1.1494643080153202, - 1.079522913691332, - -0.9969080839668093 + 0, + 0, + 2.712017 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -37.596858, + 108.372151, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1158, "type": "imguizmo", "translation": [ - -1.186192122638504, - 1.0012281228908007, - -0.9400449491748561 + 0, + 0, + 2.728449 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -37.089581, + 109.076376, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1159, "type": "imguizmo", "translation": [ - -1.2201339203363704, - 0.9216859265260348, - -0.878498557731811 + 0, + 0, + 2.744652 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -36.556456, + 109.770792, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1160, "type": "imguizmo", "translation": [ - -1.2512474138555556, - 0.8409954242687956, - -0.8125755306639656 + 0, + 0, + 2.760615 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -35.997854, + 110.454914, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1161, "type": "imguizmo", "translation": [ - -1.2794938396575095, - 0.759257146436995, - -0.7426042931761426 + 0, + 0, + 2.776326 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -35.414164, + 111.128264, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1162, "type": "imguizmo", "translation": [ - -1.3048380062130394, - 0.6765729287464379, - -0.6689334384538466 + 0, + 0, + 2.791775 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -34.805794, + 111.790375, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1163, "type": "imguizmo", "translation": [ - -1.3272483378465731, - 0.5930457854362006, - -0.5919299909895472 + 0, + 0, + 2.806952 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -34.173166, + 112.440785, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1164, "type": "imguizmo", "translation": [ - -1.3466969140755187, - 0.5087797809257184, - -0.5119775780851219 + 0, + 0, + 2.821844 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -33.516724, + 113.079039, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1165, "type": "imguizmo", "translation": [ - -1.3631595043957085, - 0.4238799001634729, - -0.4294745186399297 + 0, + 0, + 2.836443 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -32.836922, + 113.704694, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1166, "type": "imguizmo", "translation": [ - -1.3766155984695898, - 0.33845191782882594, - -0.3448318387460978 + 0, + 0, + 2.850737 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -32.134236, + 114.317313, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1167, "type": "imguizmo", "translation": [ - -1.3870484316795537, - 0.2526022665499362, - -0.258471223977201 + 0, + 0, + 2.864718 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -31.409156, + 114.91647, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1168, "type": "imguizmo", "translation": [ - -1.3944450060145572, - 0.1664379043019601, - -0.17082291857194848 + 0, + 0, + 2.878374 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -30.662185, + 115.501747, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1169, "type": "imguizmo", "translation": [ - -1.3987961062640304, - 0.08006618115072876, - -0.08232358197902295 + 0, + 0, + 2.891697 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -29.893846, + 116.072736, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1170, "type": "imguizmo", "translation": [ - -1.4000963114988783, - -0.00640529449205858, - 0.0065858865583200005 + 0, + 0, + 2.904678 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -29.104673, + 116.629038, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1171, "type": "imguizmo", "translation": [ - -1.3983440018252848, - -0.09286878993552146, - 0.09546254454010955 + 0, + 0, + 2.917306 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -28.295217, + 117.170268, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1172, "type": "imguizmo", "translation": [ - -1.3935413604028986, - -0.17921658243111382, - 0.1838636129269121 + 0, + 0, + 2.929574 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -27.466041, + 117.696046, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1173, "type": "imguizmo", "translation": [ - -1.385694370724889, - -0.26534109338175493, - 0.2713486820427333 + 0, + 0, + 2.941473 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -26.617724, + 118.206007, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1174, "type": "imguizmo", "translation": [ - -1.3748128091632554, - -0.3511350223713651, - 0.35748190567387095 + 0, + 0, + 2.952995 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -25.750857, + 118.699795, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1175, "type": "imguizmo", "translation": [ - -1.36091023278869, - -0.4364914808478205, - 0.441834172432825 + 0, + 0, + 2.964132 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -24.866043, + 119.177067, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1176, "type": "imguizmo", "translation": [ - -1.3440039624801499, - -0.5213041252927882, - 0.5239852435696551 + 0, + 0, + 2.974875 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -23.9639, + 119.637489, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1177, "type": "imguizmo", "translation": [ - -1.3241150613452046, - -0.6054672897124932, - 0.6035258465802791 + 0, + 0, + 2.985217 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -23.045055, + 120.080741, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1178, "type": "imguizmo", "translation": [ - -1.301268308478024, - -0.6888761172843985, - 0.6800597141815122 + 0, + 0, + 2.995152 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -22.110151, + 120.506514, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1179, "type": "imguizmo", "translation": [ - -1.2754921680877125, - -0.7714266909957476, - 0.7532055584947357 + 0, + 0, + 3.004672 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -21.159837, + 120.914511, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1180, "type": "imguizmo", "translation": [ - -1.2468187540354487, - -0.8530161631112192, - 0.8225989706029284 + 0, + 0, + 3.01377 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -20.194777, + 121.304448, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1181, "type": "imguizmo", "translation": [ - -1.2152837898246154, - -0.9335428833084021, - 0.8878942360175622 + 0, + 0, + 3.022441 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -19.215643, + 121.676052, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1182, "type": "imguizmo", "translation": [ - -1.1809265640937605, - -1.0129065253214358, - 0.9487660570107929 + 0, + 0, + 3.030678 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -18.223117, + 122.029066, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1183, "type": "imguizmo", "translation": [ - -1.143789881667848, - -1.0910082119350462, - 1.0049111732323772 + 0, + 0, + 3.038476 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -17.217891, + 122.363243, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1184, "type": "imguizmo", "translation": [ - -1.1039200102287723, - -1.1677506381732456, - 1.0560498725374856 + 0, + 0, + 3.045828 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -16.200666, + 122.67835, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1185, "type": "imguizmo", "translation": [ - -1.0613666226715934, - -1.2430381925292107, - 1.1019273844985342 + 0, + 0, + 3.052731 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -15.17215, + 122.974167, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1186, "type": "imguizmo", "translation": [ - -1.0161827352182906, - -1.3167770760853184, - 1.1423151496586417 + 0, + 0, + 3.059178 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -14.13306, + 123.250489, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1187, "type": "imguizmo", "translation": [ - -0.9684246413661551, - -1.3888754193749149, - 1.1770119582033471 + 0, + 0, + 3.065166 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -13.084121, + 123.507122, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1188, "type": "imguizmo", "translation": [ - -0.9181518417530996, - -1.4592433968402323, - 1.2058449523777828 + 0, + 0, + 3.070691 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -12.026064, + 123.743888, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1189, "type": "imguizmo", "translation": [ - -0.8654269700272739, - -1.5277933387438483, - 1.2286704876553156 + 0, + 0, + 3.075748 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -10.959625, + 123.960623, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1190, "type": "imguizmo", "translation": [ - -0.8103157148133387, - -1.5944398403942603, - 1.245374848367352 + 0, + 0, + 3.080334 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -9.885548, + 124.157174, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1191, "type": "imguizmo", "translation": [ - -0.752886737872621, - -1.6590998685494966, - 1.2558748142290797 + 0, + 0, + 3.084446 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -8.804582, + 124.333405, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1192, "type": "imguizmo", "translation": [ - -0.6932115885591016, - -1.7216928648662024, - 1.2601180749387397 + 0, + 0, + 3.088081 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -7.71748, + 124.489193, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1193, "type": "imguizmo", "translation": [ - -0.6313646146778419, - -1.7821408462652917, - 1.2580834907849197 + 0, + 0, + 3.091237 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -6.624999, + 124.62443, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1194, "type": "imguizmo", "translation": [ - -0.5674228698568673, - -1.8403685020891636, - 1.24978119796353 + 0, + 0, + 3.09391 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -5.527901, + 124.73902, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1195, "type": "imguizmo", "translation": [ - -0.5014660175479455, - -1.8963032879294006, - 1.2352525580797895 + 0, + 0, + 3.096101 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -4.426951, + 124.832886, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1196, "type": "imguizmo", "translation": [ - -0.43357623177584687, - -1.9498755160080645, - 1.2145699520867863 + 0, + 0, + 3.097806 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -3.322916, + 124.90596, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1197, "type": "imguizmo", "translation": [ - -0.3638380947597374, - -2.001018441999997, - 1.1878364196872109 + 0, + 0, + 3.099024 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -2.216565, + 124.958192, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1198, "type": "imguizmo", "translation": [ - -0.29233849153427044, - -2.0496683481879296, - 1.15518514599474 + 0, + 0, + 3.099756 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -1.108669, + 124.989546, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1199, "type": "imguizmo", "translation": [ - -0.21916650170165367, - -2.095764622846832, - 1.1167787980125021 + 0, + 0, + 3.1 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 125, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1200, - "type": "imguizmo", - "translation": [ - -0.1444132884495489, - -2.1392498357585708, - 1.0728087142342444 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1.0, - 1.0, - 1.0 - ] + "type": "action", + "action": "set_active_render_window", + "value": 0 + }, + { + "frame": 1200, + "type": "action", + "action": "set_active_planar", + "value": 5 + }, + { + "frame": 1200, + "type": "action", + "action": "set_projection_type", + "value": "perspective" + }, + { + "frame": 1200, + "type": "action", + "action": "set_left_handed", + "value": true + }, + { + "frame": 1200, + "type": "action", + "action": "reset_active_camera", + "value": 1 + }, + { + "frame": 1200, + "type": "action", + "action": "set_active_render_window", + "value": 1 + }, + { + "frame": 1200, + "type": "action", + "action": "set_active_planar", + "value": 5 + }, + { + "frame": 1200, + "type": "action", + "action": "set_projection_type", + "value": "orthographic" + }, + { + "frame": 1200, + "type": "action", + "action": "set_left_handed", + "value": true + }, + { + "frame": 1200, + "type": "action", + "action": "set_active_render_window", + "value": 0 }, { "frame": 1201, "type": "imguizmo", "translation": [ - -0.06817198497310444, - -2.180069809762807, - 1.023493951405589 + 0, + 4, + 2.6 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1202, "type": "imguizmo", "translation": [ - 0.009462421557417125, - -2.2181736882549976, - 0.9690801931943398 + 0.105587, + 3.998606, + 2.62111 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 2.11175, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1203, "type": "imguizmo", "translation": [ - 0.08839320833882182, - -2.253513998547387, - 0.909838526206811 + 0.211101, + 3.994426, + 2.642161 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 4.222028, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1204, "type": "imguizmo", "translation": [ - 0.16852203743955418, - -2.2860467110140643, - 0.8460640894479874 + 0.316468, + 3.987461, + 2.663095 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 6.329363, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1205, "type": "imguizmo", "translation": [ - 0.24974907831651344, - -2.3157312939463996, - 0.7780746039538501 + 0.421614, + 3.977718, + 2.683853 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 8.432288, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1206, "type": "imguizmo", "translation": [ - 0.33197313219147934, - -2.3425307640505, - 0.706208789921156 + 0.526467, + 3.965203, + 2.704377 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 10.529336, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1207, "type": "imguizmo", "translation": [ - 0.41509175813218596, - -2.366411732523793, - 0.6308246792204575 + 0.630952, + 3.949924, + 2.724611 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 12.619046, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1208, "type": "imguizmo", "translation": [ - 0.4990014006809761, - -2.3873444466533127, - 0.5522978316993259 + 0.734998, + 3.931892, + 2.744497 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 14.699961, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1209, "type": "imguizmo", "translation": [ - 0.583597518871992, - -2.405302826883878, - 0.47101946416209284 + 0.838532, + 3.911121, + 2.76398 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 16.770632, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1210, "type": "imguizmo", "translation": [ - 0.6687747164762112, - -2.4202644993099707, - 0.38739450134740283 + 0.941481, + 3.887623, + 2.783006 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 18.829615, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1211, "type": "imguizmo", "translation": [ - 0.7544268733120203, - -2.4322108235508346, - 0.30183955861353534 + 1.043774, + 3.861416, + 2.801522 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 20.875476, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1212, "type": "imguizmo", "translation": [ - 0.8404472774577475, - -2.4411269159740687, - 0.21478086638164598 + 1.145339, + 3.832518, + 2.819477 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 22.906788, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1213, "type": "imguizmo", "translation": [ - 0.9267287582014283, - -2.4470016682387805, - 0.126652146677249 + 1.246107, + 3.80095, + 2.836819 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 24.922136, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1214, "type": "imguizmo", "translation": [ - 1.0131638195621673, - -2.449827761135196, - 0.03789245234890243 + 1.346006, + 3.766732, + 2.853502 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 26.920115, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1215, "type": "imguizmo", "translation": [ - 1.0996447742167426, - -2.4496016737034836, - -0.05105602027099576 + 1.444967, + 3.729889, + 2.869478 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 28.899333, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1216, "type": "imguizmo", "translation": [ - 1.1860638776646102, - -2.4463236876204273, - -0.13975013436610229 + 1.542921, + 3.690447, + 2.884703 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 30.858411, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1217, "type": "imguizmo", "translation": [ - 1.2723134624641195, - -2.4399978868485013, - -0.2277480203214419 + 1.639799, + 3.648432, + 2.899135 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 32.795983, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1218, "type": "imguizmo", "translation": [ - 1.3582860723727506, - -2.4306321525477523, - -0.31461127709561343 + 1.735535, + 3.603875, + 2.912733 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 34.710699, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1219, "type": "imguizmo", "translation": [ - 1.443874596224207, - -2.418238153256861, - -0.3999071563127225 + 1.830061, + 3.556807, + 2.925459 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 36.601225, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1220, "type": "imguizmo", "translation": [ - 1.5289724013755908, - -2.402831330355595, - -0.4832107181930224 + 1.923312, + 3.50726, + 2.937278 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 38.466242, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1221, "type": "imguizmo", "translation": [ - 1.6134734665583992, - -2.3844308788267687, - -0.564106948581522 + 2.015223, + 3.455268, + 2.948157 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 40.304452, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1222, "type": "imguizmo", "translation": [ - 1.697272513967821, - -2.3630597233416917, - -0.6421928265276298 + 2.105729, + 3.400869, + 2.958065 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 42.114573, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1223, "type": "imguizmo", "translation": [ - 1.7802651404257712, - -2.3387444896988776, - -0.717079332115255 + 2.194767, + 3.344099, + 2.966976 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 43.895344, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1224, "type": "imguizmo", "translation": [ - 1.862347947454258, - -2.3115154716516155, - -0.7883933845404703 + 2.282276, + 3.284999, + 2.974864 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 45.645523, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1225, "type": "imguizmo", "translation": [ - 1.943418670096997, - -2.281406593165727, - -0.8557797007812963 + 2.368195, + 3.223609, + 2.981707 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 47.363891, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1226, "type": "imguizmo", "translation": [ - 2.023376304328823, - -2.2484553661545297, - -0.9189025655998562 + 2.452463, + 3.159973, + 2.987486 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 49.049251, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1227, "type": "imguizmo", "translation": [ - 2.102121232894123, - -2.2127028437436613, - -0.9774475040588003 + 2.535021, + 3.094134, + 2.992185 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 50.700427, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1228, "type": "imguizmo", "translation": [ - 2.179555349417536, - -2.1741935691240015, - -1.0311228482196262 + 2.615813, + 3.026139, + 2.995791 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 52.316269, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1229, "type": "imguizmo", "translation": [ - 2.255582180632276, - -2.1329755200563985, - -1.0796611902176763 + 2.694783, + 2.956036, + 2.998294 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 53.895651, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1230, "type": "imguizmo", "translation": [ - 2.330107006573811, - -2.0891000490973552, - -1.1228207144746691 + 2.771874, + 2.883872, + 2.999686 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 55.437473, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1231, "type": "imguizmo", "translation": [ - 2.4030369785891423, - -2.042621819620142, - -1.1603864024117454 + 2.847033, + 2.809698, + 2.999965 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 56.940659, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1232, "type": "imguizmo", "translation": [ - 2.4742812350146717, - -1.9935987377110316, - -1.192171103661216 + 2.920208, + 2.733566, + 2.999129 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 58.404163, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1233, "type": "imguizmo", "translation": [ - 2.5437510143785067, - -1.9420918800255351, - -1.2180164684402741 + 2.991348, + 2.655529, + 2.997181 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 59.826963, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1234, "type": "imguizmo", "translation": [ - 2.611359765986213, - -1.888165417694492, - -1.237793736441652 + 3.060403, + 2.575642, + 2.994125 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 61.208069, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1235, "type": "imguizmo", "translation": [ - 2.677023257752195, - -1.8318865363748296, - -1.2514043783109934 + 3.127326, + 2.493959, + 2.989971 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 62.546519, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1236, "type": "imguizmo", "translation": [ - 2.7406596811423913, - -1.7733253525446033, - -1.2587805865151411 + 3.192069, + 2.410539, + 2.98473 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 63.841378, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1237, "type": "imguizmo", "translation": [ - 2.802189753097517, - -1.7125548261465957, - -1.2598856131558573 + 3.254587, + 2.325438, + 2.978417 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 65.091746, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1238, "type": "imguizmo", "translation": [ - 2.861536814809883, - -1.6496506696893118, - -1.2547139530460016 + 3.314837, + 2.238717, + 2.971049 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 66.29675, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1239, "type": "imguizmo", "translation": [ - 2.9186269272307284, - -1.5846912539186233, - -1.2432913711360996 + 3.372778, + 2.150435, + 2.962647 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 67.45555, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1240, "type": "imguizmo", "translation": [ - 2.9733889631890618, - -1.517757510177577, - -1.2256747741546528 + 3.428367, + 2.060655, + 2.953234 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 68.56734, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1241, "type": "imguizmo", "translation": [ - 3.025754696007265, - -1.4489328295760193, - -1.2019519271016808 + 3.481567, + 1.969439, + 2.942837 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 69.631344, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1242, "type": "imguizmo", "translation": [ - 3.0756588845030355, - -1.3783029590956586, - -1.1722410160079064 + 3.532341, + 1.87685, + 2.931484 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 70.646821, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1243, "type": "imguizmo", "translation": [ - 3.123039354271773, - -1.3059558947600025, - -1.1366900591378946 + 3.580653, + 1.782953, + 2.919207 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 71.613063, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1244, "type": "imguizmo", "translation": [ - 3.1678370751481513, - -1.2319817720022703, - -1.095476169570504 + 3.62647, + 1.687814, + 2.90604 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 72.529397, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1245, "type": "imguizmo", "translation": [ - 3.2099962347503466, - -1.1564727533678676, - -1.0488046728304379 + 3.669759, + 1.591499, + 2.892021 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 73.395184, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1246, "type": "imguizmo", "translation": [ - 3.2494643080153196, - -1.0795229136913314, - -0.996908083966808 + 3.710491, + 1.494074, + 2.877187 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 74.20982, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1247, "type": "imguizmo", "translation": [ - 3.2861921226385027, - -1.0012281228908024, - -0.9400449491748564 + 3.748637, + 1.395608, + 2.861581 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 74.972739, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1248, "type": "imguizmo", "translation": [ - 3.320133920336369, - -0.9216859265260363, - -0.8784985577318112 + 3.78417, + 1.296169, + 2.845246 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 75.683407, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1249, "type": "imguizmo", "translation": [ - 3.351247413855554, - -0.840995424268797, - -0.8125755306639658 + 3.817067, + 1.195827, + 2.828228 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 76.341331, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1250, "type": "imguizmo", "translation": [ - 3.3794938396575076, - -0.7592571464369966, - -0.7426042931761428 + 3.847303, + 1.094652, + 2.810573 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 76.946051, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1251, "type": "imguizmo", "translation": [ - 3.404838006213038, - -0.6765729287464395, - -0.6689334384538467 + 3.874857, + 0.992714, + 2.792331 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 77.497147, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1252, "type": "imguizmo", "translation": [ - 3.4272483378465712, - -0.5930457854362021, - -0.5919299909895473 + 3.899712, + 0.890084, + 2.773553 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 77.994233, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1253, "type": "imguizmo", "translation": [ - 3.4466969140755173, - -0.5087797809257188, - -0.5119775780851209 + 3.921848, + 0.786833, + 2.754292 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 78.436964, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1254, "type": "imguizmo", "translation": [ - 3.4631595043957066, - -0.42387990016347343, - -0.4294745186399287 + 3.941252, + 0.683035, + 2.734601 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 78.825031, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1255, "type": "imguizmo", "translation": [ - 3.476615598469588, - -0.3384519178288289, - -0.344831838746099 + 3.957908, + 0.57876, + 2.714534 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 79.158163, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1256, "type": "imguizmo", "translation": [ - 3.4870484316795514, - -0.25260226654993895, - -0.2584712239772022 + 3.971806, + 0.474082, + 2.694148 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 79.436129, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1257, "type": "imguizmo", "translation": [ - 3.494445006014555, - -0.1664379043019617, - -0.17082291857194842 + 3.982937, + 0.369073, + 2.6735 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 79.658734, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1258, "type": "imguizmo", "translation": [ - 3.4987961062640283, - -0.08006618115073047, - -0.08232358197902302 + 3.991291, + 0.263808, + 2.652647 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 79.825824, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1259, "type": "imguizmo", "translation": [ - 3.5000963114988757, - 0.006405294492056901, - 0.006585886558319931 + 3.996864, + 0.158358, + 2.631647 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 79.937282, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1260, - "type": "action", - "action": "set_active_render_window", - "value": 0 - }, - { - "frame": 1260, - "type": "action", - "action": "set_active_planar", - "value": 7 - }, - { - "frame": 1260, - "type": "action", - "action": "set_projection_type", - "value": "perspective" + "type": "imguizmo", + "translation": [ + 3.999652, + 0.052798, + 2.610559 + ], + "rotation_deg": [ + 0, + 79.993031, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] }, { "frame": 1261, "type": "imguizmo", "translation": [ - 1.64, - 0.0, - 1.64 + 3.999652, + -0.052798, + 2.589441 ], "rotation_deg": [ - 0.0, - 120.0, - 0.0 + 0, + 79.993031, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1262, "type": "imguizmo", "translation": [ - 1.639227656874891, - 0.03281682645677409, - 1.64 + 3.996864, + -0.158358, + 2.568353 ], "rotation_deg": [ - 0.9528695254580167, - 120.0, - 0.0 + 0, + 79.937282, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1263, "type": "imguizmo", "translation": [ - 1.6369733771929313, - 0.06296742111772219, - 1.64 + 3.991291, + -0.263808, + 2.547353 ], "rotation_deg": [ - 1.9045518941423958, - 120.0, - 0.0 + 0, + 79.825824, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1264, "type": "imguizmo", "translation": [ - 1.6332968139599016, - 0.09324959390223336, - 1.64 + 3.982937, + -0.369073, + 2.5265 ], "rotation_deg": [ - 2.8538614283290102, - 120.0, - 0.0 + 0, + 79.658734, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1265, "type": "imguizmo", "translation": [ - 1.6281980001594487, - 0.12339878708940485, - 1.64 + 3.971806, + -0.474082, + 2.505852 ], "rotation_deg": [ - 3.7996154065500436, - 120.0, - 0.0 + 0, + 79.436129, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1266, "type": "imguizmo", "translation": [ - 1.6216836520831495, - 0.15339558493117983, - 1.64 + 3.957908, + -0.57876, + 2.485466 ], "rotation_deg": [ - 4.74063553711766, - 120.0, - 0.0 + 0, + 79.158163, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1267, "type": "imguizmo", "translation": [ - 1.6137618566931538, - 0.18320116344137866, - 1.64 + 3.941252, + -0.683035, + 2.465399 ], "rotation_deg": [ - 5.675749426128718, - 120.0, - 0.0 + 0, + 78.825031, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1268, "type": "imguizmo", "translation": [ - 1.6044424858884132, - 0.21277850471759752, - 1.64 + 3.921848, + -0.786833, + 2.445708 ], "rotation_deg": [ - 6.603792038121597, - 120.0, - 0.0 + 0, + 78.436964, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1269, "type": "imguizmo", "translation": [ - 1.5937371502581978, - 0.24209074978474063, - 1.64 + 3.899712, + -0.890084, + 2.426447 ], "rotation_deg": [ - 7.523607147565286, - 120.0, - 0.0 + 0, + 77.994233, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1270, "type": "imguizmo", "translation": [ - 1.5816591873325336, - 0.27110137997880757, - 1.64 + 3.874857, + -0.992714, + 2.407669 ], "rotation_deg": [ - 8.43404877937244, - 120.0, - 0.0 + 0, + 77.497147, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1271, "type": "imguizmo", "translation": [ - 1.5682236447480375, - 0.29977425160763277, - 1.64 + 3.847303, + -1.094652, + 2.389427 ], "rotation_deg": [ - 9.333982636641618, - 120.0, - 0.0 + 0, + 76.946051, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1272, "type": "imguizmo", "translation": [ - 1.5534472615178194, - 0.328073641848383, - 1.64 + 3.817067, + -1.195827, + 2.371772 ], "rotation_deg": [ - 10.222287513849988, - 120.0, - 0.0 + 0, + 76.341331, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1273, "type": "imguizmo", "translation": [ - 1.5373484471753618, - 0.35596429318438993, - 1.64 + 3.78417, + -1.296169, + 2.354754 ], "rotation_deg": [ - 11.097856693735785, - 120.0, - 0.0 + 0, + 75.683407, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1274, "type": "imguizmo", "translation": [ - 1.519947258838648, - 0.3834114573371734, - 1.64 + 3.748637, + -1.395608, + 2.338419 ], "rotation_deg": [ - 11.959599326130213, - 120.0, - 0.0 + 0, + 74.972739, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1275, "type": "imguizmo", "translation": [ - 1.5012653762214814, - 0.41038093855802626, - 1.64 + 3.710491, + -1.494074, + 2.322813 ], "rotation_deg": [ - 12.806441787020908, - 120.0, - 0.0 + 0, + 74.20982, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1276, "type": "imguizmo", "translation": [ - 1.4813260746232602, - 0.43683913623169396, - 1.64 + 3.669759, + -1.591499, + 2.307979 ], "rotation_deg": [ - 13.637329016153773, - 120.0, - 0.0 + 0, + 73.395184, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1277, "type": "imguizmo", "translation": [ - 1.4601541959308557, - 0.4627530867385546, - 1.64 + 3.62647, + -1.687814, + 2.29396 ], "rotation_deg": [ - 14.451225831506678, - 120.0, - 0.0 + 0, + 72.529397, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1278, "type": "imguizmo", "translation": [ - 1.4377761176687125, - 0.48809050452318403, - 1.64 + 3.580653, + -1.782953, + 2.280793 ], "rotation_deg": [ - 15.247118218997338, - 120.0, - 0.0 + 0, + 71.613063, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1279, "type": "imguizmo", "translation": [ - 1.4142197201357378, - 0.5128198223181384, - 1.64 + 3.532341, + -1.87685, + 2.268516 ], "rotation_deg": [ - 16.024014595818613, - 120.0, - 0.0 + 0, + 70.646821, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1280, "type": "imguizmo", "translation": [ - 1.3895143516699222, - 0.5369102304728377, - 1.64 + 3.481567, + -1.969439, + 2.257163 ], "rotation_deg": [ - 16.780947045827197, - 120.0, - 0.0 + 0, + 69.631344, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1281, "type": "imguizmo", "translation": [ - 1.3636907920839643, - 0.560331715338554, - 1.64 + 3.428367, + -2.060655, + 2.246766 ], "rotation_deg": [ - 17.516972525446597, - 120.0, - 0.0 + 0, + 68.56734, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1282, "type": "imguizmo", "translation": [ - 1.3367812143174558, - 0.5830550966616804, - 1.64 + 3.372778, + -2.150435, + 2.237353 ], "rotation_deg": [ - 18.23117403858202, - 120.0, - 0.0 + 0, + 67.45555, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1283, "type": "imguizmo", "translation": [ - 1.308819144353407, - 0.605052063938694, - 1.64 + 3.314837, + -2.238717, + 2.228951 ], "rotation_deg": [ - 18.922661779083285, - 120.0, - 0.0 + 0, + 66.29675, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1284, "type": "imguizmo", "translation": [ - 1.2798394194490452, - 0.6262952116875169, - 1.64 + 3.254587, + -2.325438, + 2.221583 ], "rotation_deg": [ - 19.59057423933253, - 120.0, - 0.0 + 0, + 65.091746, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1285, "type": "imguizmo", "translation": [ - 1.2498781447329317, - 0.6467580735913346, - 1.64 + 3.192069, + -2.410539, + 2.21527 ], "rotation_deg": [ - 20.234079283575376, - 120.0, - 0.0 + 0, + 63.841378, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1286, "type": "imguizmo", "translation": [ - 1.2189726482224659, - 0.666415155472332, - 1.64 + 3.127326, + -2.493959, + 2.210029 ], "rotation_deg": [ - 20.852375184658495, - 120.0, - 0.0 + 0, + 62.546519, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1287, "type": "imguizmo", "translation": [ - 1.1871614343178254, - 0.6852419670542624, - 1.64 + 3.060403, + -2.575642, + 2.205875 ], "rotation_deg": [ - 21.44469162288179, - 120.0, - 0.0 + 0, + 61.208069, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1288, "type": "imguizmo", "translation": [ - 1.154484135830277, - 0.7032150524742842, - 1.64 + 2.991348, + -2.655529, + 2.202819 ], "rotation_deg": [ - 22.01029064572082, - 120.0, - 0.0 + 0, + 59.826963, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1289, "type": "imguizmo", "translation": [ - 1.12098146460463, - 0.7203120195060435, - 1.64 + 2.920208, + -2.733566, + 2.200871 ], "rotation_deg": [ - 22.548467587223755, - 120.0, - 0.0 + 0, + 58.404163, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1290, "type": "imguizmo", "translation": [ - 1.0866951607973458, - 0.7365115674576009, - 1.64 + 2.847033, + -2.809698, + 2.200035 ], "rotation_deg": [ - 23.05855194593741, - 120.0, - 0.0 + 0, + 56.940659, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1291, "type": "imguizmo", "translation": [ - 1.0516679408735028, - 0.7517935137094406, - 1.64 + 2.771874, + -2.883872, + 2.200314 ], "rotation_deg": [ - 23.539908220268543, - 120.0, - 0.0 + 0, + 55.437473, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1292, "type": "imguizmo", "translation": [ - 1.0159434443873971, - 0.7661388188595009, - 1.64 + 2.694783, + -2.956036, + 2.201706 ], "rotation_deg": [ - 23.991936700239727, - 120.0, - 0.0 + 0, + 53.895651, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1293, "type": "imguizmo", "translation": [ - 0.979566179613096, - 0.7795296104438992, - 1.64 + 2.615813, + -3.026139, + 2.204209 ], "rotation_deg": [ - 24.414074214653297, - 120.0, - 0.0 + 0, + 52.316269, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1294, "type": "imguizmo", "translation": [ - 0.9425814680926663, - 0.7919492052037957, - 1.64 + 2.535021, + -3.094134, + 2.207815 ], "rotation_deg": [ - 24.805794832732513, - 120.0, - 0.0 + 0, + 50.700427, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1295, "type": "imguizmo", "translation": [ - 0.9050353881711805, - 0.8033821298706567, - 1.64 + 2.452463, + -3.159973, + 2.212514 ], "rotation_deg": [ - 25.166610519365864, - 120.0, - 0.0 + 0, + 49.049251, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1296, "type": "imguizmo", "translation": [ - 0.8669747175888339, - 0.8138141404440203, - 1.64 + 2.368195, + -3.223609, + 2.218293 ], "rotation_deg": [ - 25.496071743138003, - 120.0, - 0.0 + 0, + 47.363891, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1297, "type": "imguizmo", "translation": [ - 0.8284468752017072, - 0.823232239937747, - 1.64 + 2.282276, + -3.284999, + 2.225136 ], "rotation_deg": [ - 25.793768036389917, - 120.0, - 0.0 + 0, + 45.645523, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1298, "type": "imguizmo", "translation": [ - 0.7894998619037751, - 0.8316246945726472, - 1.64 + 2.194767, + -3.344099, + 2.233024 ], "rotation_deg": [ - 26.059328506610537, - 120.0, - 0.0 + 0, + 43.895344, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1299, "type": "imguizmo", "translation": [ - 0.750182200823771, - 0.8389810483953068, - 1.64 + 2.105729, + -3.400869, + 2.241935 ], "rotation_deg": [ - 26.292422298522606, - 120.0, - 0.0 + 0, + 42.114573, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1300, "type": "imguizmo", "translation": [ - 0.7105428768714092, - 0.8452921363049057, - 1.64 + 2.015223, + -3.455268, + 2.251843 ], "rotation_deg": [ - 26.492759006287177, - 120.0, - 0.0 + 0, + 40.304452, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1301, "type": "imguizmo", "translation": [ - 0.6706312757082894, - 0.8505500954717897, - 1.64 + 1.923312, + -3.50726, + 2.262722 ], "rotation_deg": [ - 26.66008903531307, - 120.0, - 0.0 + 0, + 38.466242, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1302, "type": "imguizmo", "translation": [ - 0.6304971222195103, - 0.8547483751335789, - 1.64 + 1.830061, + -3.556807, + 2.274541 ], "rotation_deg": [ - 26.794203913220734, - 120.0, - 0.0 + 0, + 36.601225, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1303, "type": "imguizmo", "translation": [ - 0.5901904185626555, - 0.8578817447566012, - 1.64 + 1.735535, + -3.603875, + 2.287267 ], "rotation_deg": [ - 26.894936549572805, - 120.0, - 0.0 + 0, + 34.710699, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1304, "type": "imguizmo", "translation": [ - 0.5497613818713308, - 0.8599463005524858, - 1.64 + 1.639799, + -3.648432, + 2.300865 ], "rotation_deg": [ - 26.962161444048018, - 120.0, - 0.0 + 0, + 32.795983, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1305, "type": "imguizmo", "translation": [ - 0.5092603816908705, - 0.8609394703417974, - 1.64 + 1.542921, + -3.690447, + 2.315297 ], "rotation_deg": [ - 26.99579484279896, - 120.0, - 0.0 + 0, + 30.858411, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1306, "type": "imguizmo", "translation": [ - 0.4687378772241541, - 0.8608600167586525, - 1.64 + 1.444967, + -3.729889, + 2.330522 ], "rotation_deg": [ - 26.99579484279896, - 120.0, - 0.0 + 0, + 28.899333, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1307, "type": "imguizmo", "translation": [ - 0.4282443544657249, - 0.8597080387923216, - 1.64 + 1.346006, + -3.766732, + 2.346498 ], "rotation_deg": [ - 26.962161444048018, - 120.0, - 0.0 + 0, + 26.920115, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1308, "type": "imguizmo", "translation": [ - 0.3878302633025267, - 0.8574849716639017, - 1.64 + 1.246107, + -3.80095, + 2.363181 ], "rotation_deg": [ - 26.894936549572808, - 120.0, - 0.0 + 0, + 24.922136, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1309, "type": "imguizmo", "translation": [ - 0.3475459546596252, - 0.85419358503821, - 1.64 + 1.145339, + -3.832518, + 2.380523 ], "rotation_deg": [ - 26.794203913220734, - 120.0, - 0.0 + 0, + 22.906788, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1310, "type": "imguizmo", "translation": [ - 0.30744161776922857, - 0.8498379795731253, - 1.64 + 1.043774, + -3.861416, + 2.398478 ], "rotation_deg": [ - 26.66008903531307, - 120.0, - 0.0 + 0, + 20.875476, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1311, "type": "imguizmo", "translation": [ - 0.26756721764115154, - 0.8444235818106802, - 1.64 + 0.941481, + -3.887623, + 2.416994 ], "rotation_deg": [ - 26.492759006287177, - 120.0, - 0.0 + 0, + 18.829615, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1312, "type": "imguizmo", "translation": [ - 0.2279724328126359, - 0.8379571374162643, - 1.64 + 0.838532, + -3.911121, + 2.43602 ], "rotation_deg": [ - 26.29242229852261, - 120.0, - 0.0 + 0, + 16.770632, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1313, "type": "imguizmo", "translation": [ - 0.18870659345507812, - 0.8304467027743658, - 1.64 + 0.734998, + -3.931892, + 2.455503 ], "rotation_deg": [ - 26.059328506610537, - 120.0, - 0.0 + 0, + 14.699961, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1314, "type": "imguizmo", "translation": [ - 0.1498186199147805, - 0.8219016349513193, - 1.64 + 0.630952, + -3.949924, + 2.475389 ], "rotation_deg": [ - 25.793768036389917, - 120.0, - 0.0 + 0, + 12.619046, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1315, "type": "imguizmo", "translation": [ - 0.11135696176428965, - 0.8123325800375673, - 1.64 + 0.526467, + -3.965203, + 2.495623 ], "rotation_deg": [ - 25.496071743138003, - 120.0, - 0.0 + 0, + 10.529336, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1316, "type": "imguizmo", "translation": [ - 0.07336953744026398, - 0.801751459883955, - 1.64 + 0.421614, + -3.977718, + 2.516147 ], "rotation_deg": [ - 25.166610519365868, - 120.0, - 0.0 + 0, + 8.432288, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1317, "type": "imguizmo", "translation": [ - 0.03590367454306552, - 0.7901714572485914, - 1.64 + 0.316468, + -3.987461, + 2.536905 ], "rotation_deg": [ - 24.805794832732516, - 120.0, - 0.0 + 0, + 6.329363, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1318, "type": "imguizmo", "translation": [ - -0.0009939491275325688, - 0.7776069993727719, - 1.64 + 0.211101, + -3.994426, + 2.557839 ], "rotation_deg": [ - 24.41407421465329, - 120.0, - 0.0 + 0, + 4.222028, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1319, "type": "imguizmo", "translation": [ - -0.03727736372707465, - 0.7640737400064342, - 1.64 + 0.105587, + -3.998606, + 2.57889 ], "rotation_deg": [ - 23.99193670023972, - 120.0, - 0.0 + 0, + 2.11175, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1320, "type": "imguizmo", "translation": [ - -0.07290136463912357, - 0.749588539905534, - 1.64 + 0, + -4, + 2.6 ], "rotation_deg": [ - 23.539908220268543, - 120.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1321, "type": "imguizmo", "translation": [ - -0.10782156879458578, - 0.7341694458256416, - 1.64 + -0.105587, + -3.998606, + 2.62111 ], "rotation_deg": [ - 23.05855194593741, - 120.0, - 0.0 + 0, + -2.11175, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1322, "type": "imguizmo", "translation": [ - -0.14199446996748483, - 0.7178356680379347, - 1.64 + -0.211101, + -3.994426, + 2.642161 ], "rotation_deg": [ - 22.548467587223755, - 120.0, - 0.0 + 0, + -4.222028, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1323, "type": "imguizmo", "translation": [ - -0.1753774929783042, - 0.7006075563955902, - 1.64 + -0.316468, + -3.987461, + 2.663095 ], "rotation_deg": [ - 22.01029064572082, - 120.0, - 0.0 + 0, + -6.329363, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1324, "type": "imguizmo", "translation": [ - -0.20792904673735776, - 0.6825065749804016, - 1.64 + -0.421614, + -3.977718, + 2.683853 ], "rotation_deg": [ - 21.444691622881795, - 120.0, - 0.0 + 0, + -8.432288, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1325, "type": "imguizmo", "translation": [ - -0.23960857606211147, - 0.6635552753612066, - 1.64 + -0.526467, + -3.965203, + 2.704377 ], "rotation_deg": [ - 20.8523751846585, - 120.0, - 0.0 + 0, + -10.529336, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1326, "type": "imguizmo", "translation": [ - -0.270376612203886, - 0.6437772684974393, - 1.64 + -0.630952, + -3.949924, + 2.724611 ], "rotation_deg": [ - 20.23407928357538, - 120.0, - 0.0 + 0, + -12.619046, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1327, "type": "imguizmo", "translation": [ - -0.3001948220210066, - 0.623197195322817, - 1.64 + -0.734998, + -3.931892, + 2.744497 ], "rotation_deg": [ - 19.59057423933253, - 120.0, - 0.0 + 0, + -14.699961, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1328, "type": "imguizmo", "translation": [ - -0.3290260557371222, - 0.6018406960458029, - 1.64 + -0.838532, + -3.911121, + 2.76398 ], "rotation_deg": [ - 18.92266177908329, - 120.0, - 0.0 + 0, + -16.770632, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1329, "type": "imguizmo", "translation": [ - -0.35683439322520266, - 0.5797343782051005, - 1.64 + -0.941481, + -3.887623, + 2.783006 ], "rotation_deg": [ - 18.231174038582026, - 120.0, - 0.0 + 0, + -18.829615, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1330, "type": "imguizmo", "translation": [ - -0.3835851887595415, - 0.5569057835199728, - 1.64 + -1.043774, + -3.861416, + 2.801522 ], "rotation_deg": [ - 17.516972525446604, - 120.0, - 0.0 + 0, + -20.875476, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1331, "type": "imguizmo", "translation": [ - -0.40924511418001785, - 0.5333833535766906, - 1.64 + -1.145339, + -3.832518, + 2.819477 ], "rotation_deg": [ - 16.780947045827197, - 120.0, - 0.0 + 0, + -22.906788, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1332, "type": "imguizmo", "translation": [ - -0.4337822004148328, - 0.509196394393858, - 1.64 + -1.246107, + -3.80095, + 2.836819 ], "rotation_deg": [ - 16.024014595818624, - 120.0, - 0.0 + 0, + -24.922136, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1333, "type": "imguizmo", "translation": [ - -0.4571658773099938, - 0.48437503991075975, - 1.64 + -1.346006, + -3.766732, + 2.853502 ], "rotation_deg": [ - 15.247118218997347, - 120.0, - 0.0 + 0, + -26.920115, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1334, "type": "imguizmo", "translation": [ - -0.4793670117159167, - 0.4589502144442291, - 1.64 + -1.444967, + -3.729889, + 2.869478 ], "rotation_deg": [ - 14.45122583150668, - 120.0, - 0.0 + 0, + -28.899333, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1335, "type": "imguizmo", "translation": [ - -0.5003579437837052, - 0.43295359416079754, - 1.64 + -1.542921, + -3.690447, + 2.884703 ], "rotation_deg": [ - 13.637329016153776, - 120.0, - 0.0 + 0, + -30.858411, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1336, "type": "imguizmo", "translation": [ - -0.5201125214258765, - 0.40641756761213643, - 1.64 + -1.639799, + -3.648432, + 2.899135 ], "rotation_deg": [ - 12.806441787020917, - 120.0, - 0.0 + 0, + -32.795983, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1337, "type": "imguizmo", "translation": [ - -0.5386061328986069, - 0.3793751953829538, - 1.64 + -1.735535, + -3.603875, + 2.912733 ], "rotation_deg": [ - 11.959599326130222, - 120.0, - 0.0 + 0, + -34.710699, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1338, "type": "imguizmo", "translation": [ - -0.5558157374648987, - 0.35186016890162425, - 1.64 + -1.830061, + -3.556807, + 2.925459 ], "rotation_deg": [ - 11.09785669373579, - 120.0, - 0.0 + 0, + -36.601225, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1339, "type": "imguizmo", "translation": [ - -0.5717198941004705, - 0.32390676846486366, - 1.64 + -1.923312, + -3.50726, + 2.937278 ], "rotation_deg": [ - 10.222287513849992, - 120.0, - 0.0 + 0, + -38.466242, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1340, "type": "imguizmo", "translation": [ - -0.5862987882066028, - 0.2955498205287481, - 1.64 + -2.015223, + -3.455268, + 2.948157 ], "rotation_deg": [ - 9.33398263664162, - 120.0, - 0.0 + 0, + -40.304452, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1341, "type": "imguizmo", "translation": [ - -0.5995342562966611, - 0.26682465431928676, - 1.64 + -2.105729, + -3.400869, + 2.958065 ], "rotation_deg": [ - 8.434048779372437, - 120.0, - 0.0 + 0, + -42.114573, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1342, "type": "imguizmo", "translation": [ - -0.611409808625538, - 0.2377670578166053, - 1.64 + -2.194767, + -3.344099, + 2.966976 ], "rotation_deg": [ - 7.523607147565284, - 120.0, - 0.0 + 0, + -43.895344, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1343, "type": "imguizmo", "translation": [ - -0.6219106497338223, - 0.20841323316757898, - 1.64 + -2.282276, + -3.284999, + 2.974864 ], "rotation_deg": [ - 6.60379203812159, - 120.0, - 0.0 + 0, + -45.645523, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1344, "type": "imguizmo", "translation": [ - -0.6310236968810998, - 0.17879975158246672, - 1.64 + -2.368195, + -3.223609, + 2.981707 ], "rotation_deg": [ - 5.675749426128722, - 120.0, - 0.0 + 0, + -47.363891, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1345, "type": "imguizmo", "translation": [ - -0.6387375963454169, - 0.14896350777173473, - 1.64 + -2.452463, + -3.159973, + 2.987486 ], "rotation_deg": [ - 4.74063553711766, - 120.0, - 0.0 + 0, + -49.049251, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1346, "type": "imguizmo", "translation": [ - -0.6450427375686073, - 0.11894167397984451, - 1.64 + -2.535021, + -3.094134, + 2.992185 ], "rotation_deg": [ - 3.799615406550042, - 120.0, - 0.0 + 0, + -50.700427, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1347, "type": "imguizmo", "translation": [ - -0.6499312651298472, - 0.08877165367326328, - 1.64 + -2.615813, + -3.026139, + 2.995791 ], "rotation_deg": [ - 2.8538614283290062, - 120.0, - 0.0 + 0, + -52.316269, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1348, "type": "imguizmo", "translation": [ - -0.6533970885325346, - 0.05849103494040307, - 1.64 + -2.694783, + -2.956036, + 2.998294 ], "rotation_deg": [ - 1.9045518941424011, - 120.0, - 0.0 + 0, + -53.895651, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1349, "type": "imguizmo", "translation": [ - -0.655435889792288, - 0.02813754366154179, - 1.64 + -2.771874, + -2.883872, + 2.999686 ], "rotation_deg": [ - 0.9528695254580194, - 120.0, - 0.0 + 0, + -55.437473, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1350, "type": "imguizmo", "translation": [ - -0.6560451288166165, - -0.0022510034929234743, - 1.64 + -2.847033, + -2.809698, + 2.999965 ], "rotation_deg": [ - 3.3306690738754696e-16, - 120.0, - 0.0 + 0, + -56.940659, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1351, "type": "imguizmo", "translation": [ - -0.6552240465695613, - -0.03263674617734043, - 1.64 + -2.920208, + -2.733566, + 2.999129 ], "rotation_deg": [ - -0.9528695254580187, - 120.0, - 0.0 + 0, + -58.404163, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1352, "type": "imguizmo", "translation": [ - -0.6529736660173575, - -0.06298182754007718, - 1.64 + -2.991348, + -2.655529, + 2.997181 ], "rotation_deg": [ - -1.9045518941424004, - 120.0, - 0.0 + 0, + -59.826963, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1353, "type": "imguizmo", "translation": [ - -0.6492967908539472, - -0.09324844138844533, - 1.64 + -3.060403, + -2.575642, + 2.994125 ], "rotation_deg": [ - -2.853861428329018, - 120.0, - 0.0 + 0, + -61.208069, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1354, "type": "imguizmo", "translation": [ - -0.6441980020079249, - -0.12339887929050834, - 1.64 + -3.127326, + -2.493959, + 2.989971 ], "rotation_deg": [ - -3.7996154065500534, - 120.0, - 0.0 + 0, + -62.546519, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1355, "type": "imguizmo", "translation": [ - -0.637683651935271, - -0.1533955775550912, - 1.64 + -3.192069, + -2.410539, + 2.98473 ], "rotation_deg": [ - -4.740635537117647, - 120.0, - 0.0 + 0, + -63.841378, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1356, "type": "imguizmo", "translation": [ - -0.6297618567049839, - -0.18320116403146564, - 1.64 + -3.254587, + -2.325438, + 2.978417 ], "rotation_deg": [ - -5.675749426128711, - 120.0, - 0.0 + 0, + -65.091746, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1357, "type": "imguizmo", "translation": [ - -0.620442485887467, - -0.21277850467039044, - 1.64 + -3.314837, + -2.238717, + 2.971049 ], "rotation_deg": [ - -6.6037920381215915, - 120.0, - 0.0 + 0, + -66.29675, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1358, "type": "imguizmo", "translation": [ - -0.6097371502582737, - -0.2420907497885172, - 1.64 + -3.372778, + -2.150435, + 2.962647 ], "rotation_deg": [ - -7.523607147565285, - 120.0, - 0.0 + 0, + -67.45555, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1359, "type": "imguizmo", "translation": [ - -0.5976591873325274, - -0.27110137997850553, - 1.64 + -3.428367, + -2.060655, + 2.953234 ], "rotation_deg": [ - -8.43404877937244, - 120.0, - 0.0 + 0, + -68.56734, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1360, "type": "imguizmo", "translation": [ - -0.5842236447480383, - -0.299774251607657, - 1.64 + -3.481567, + -1.969439, + 2.942837 ], "rotation_deg": [ - -9.333982636641622, - 120.0, - 0.0 + 0, + -69.631344, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1361, "type": "imguizmo", "translation": [ - -0.5694472615178193, - -0.3280736418483813, - 1.64 + -3.532341, + -1.87685, + 2.931484 ], "rotation_deg": [ - -10.222287513849993, - 120.0, - 0.0 + 0, + -70.646821, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1362, "type": "imguizmo", "translation": [ - -0.5533484471753617, - -0.3559642931843903, - 1.64 + -3.580653, + -1.782953, + 2.919207 ], "rotation_deg": [ - -11.097856693735793, - 120.0, - 0.0 + 0, + -71.613063, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1363, "type": "imguizmo", "translation": [ - -0.5359472588386486, - -0.3834114573371734, - 1.64 + -3.62647, + -1.687814, + 2.90604 ], "rotation_deg": [ - -11.959599326130213, - 120.0, - 0.0 + 0, + -72.529397, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1364, "type": "imguizmo", "translation": [ - -0.5172653762214814, - -0.41038093855802643, - 1.64 + -3.669759, + -1.591499, + 2.892021 ], "rotation_deg": [ - -12.806441787020908, - 120.0, - 0.0 + 0, + -73.395184, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1365, "type": "imguizmo", "translation": [ - -0.49732607462326034, - -0.4368391362316941, - 1.64 + -3.710491, + -1.494074, + 2.877187 ], "rotation_deg": [ - -13.637329016153778, - 120.0, - 0.0 + 0, + -74.20982, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1366, "type": "imguizmo", "translation": [ - -0.4761541959308557, - -0.4627530867385548, - 1.64 + -3.748637, + -1.395608, + 2.861581 ], "rotation_deg": [ - -14.451225831506685, - 120.0, - 0.0 + 0, + -74.972739, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1367, "type": "imguizmo", "translation": [ - -0.4537761176687122, - -0.4880905045231844, - 1.64 + -3.78417, + -1.296169, + 2.845246 ], "rotation_deg": [ - -15.247118218997349, - 120.0, - 0.0 + 0, + -75.683407, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1368, "type": "imguizmo", "translation": [ - -0.4302197201357375, - -0.5128198223181389, - 1.64 + -3.817067, + -1.195827, + 2.828228 ], "rotation_deg": [ - -16.024014595818628, - 120.0, - 0.0 + 0, + -76.341331, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1369, "type": "imguizmo", "translation": [ - -0.40551435166992184, - -0.5369102304728384, - 1.64 + -3.847303, + -1.094652, + 2.810573 ], "rotation_deg": [ - -16.78094704582721, - 120.0, - 0.0 + 0, + -76.946051, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1370, "type": "imguizmo", "translation": [ - -0.37969079208396367, - -0.5603317153385546, - 1.64 + -3.874857, + -0.992714, + 2.792331 ], "rotation_deg": [ - -17.516972525446615, - 120.0, - 0.0 + 0, + -77.497147, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1371, "type": "imguizmo", "translation": [ - -0.35278121431745607, - -0.5830550966616804, - 1.64 + -3.899712, + -0.890084, + 2.773553 ], "rotation_deg": [ - -18.231174038582015, - 120.0, - 0.0 + 0, + -77.994233, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1372, "type": "imguizmo", "translation": [ - -0.324819144353407, - -0.6050520639386942, - 1.64 + -3.921848, + -0.786833, + 2.754292 ], "rotation_deg": [ - -18.922661779083285, - 120.0, - 0.0 + 0, + -78.436964, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1373, "type": "imguizmo", "translation": [ - -0.2958394194490453, - -0.6262952116875171, - 1.64 + -3.941252, + -0.683035, + 2.734601 ], "rotation_deg": [ - -19.590574239332533, - 120.0, - 0.0 + 0, + -78.825031, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1374, "type": "imguizmo", "translation": [ - -0.2658781447329314, - -0.646758073591335, - 1.64 + -3.957908, + -0.57876, + 2.714534 ], "rotation_deg": [ - -20.23407928357538, - 120.0, - 0.0 + 0, + -79.158163, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1375, "type": "imguizmo", "translation": [ - -0.23497264822246525, - -0.6664151554723325, - 1.64 + -3.971806, + -0.474082, + 2.694148 ], "rotation_deg": [ - -20.85237518465851, - 120.0, - 0.0 + 0, + -79.436129, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1376, "type": "imguizmo", "translation": [ - -0.20316143431782474, - -0.6852419670542631, - 1.64 + -3.982937, + -0.369073, + 2.6735 ], "rotation_deg": [ - -21.444691622881802, - 120.0, - 0.0 + 0, + -79.658734, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1377, "type": "imguizmo", "translation": [ - -0.17048413583027625, - -0.703215052474285, - 1.64 + -3.991291, + -0.263808, + 2.652647 ], "rotation_deg": [ - -22.010290645720836, - 120.0, - 0.0 + 0, + -79.825824, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1378, "type": "imguizmo", "translation": [ - -0.13698146460462893, - -0.7203120195060444, - 1.64 + -3.996864, + -0.158358, + 2.631647 ], "rotation_deg": [ - -22.548467587223772, - 120.0, - 0.0 + 0, + -79.937282, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1379, "type": "imguizmo", "translation": [ - -0.10269516079734561, - -0.7365115674576013, - 1.64 + -3.999652, + -0.052798, + 2.610559 ], "rotation_deg": [ - -23.058551945937413, - 120.0, - 0.0 + 0, + -79.993031, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1380, "type": "imguizmo", "translation": [ - -0.06766794087350216, - -0.751793513709441, - 1.64 + -3.999652, + 0.052798, + 2.589441 ], "rotation_deg": [ - -23.539908220268547, - 120.0, - 0.0 + 0, + -79.993031, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1381, "type": "imguizmo", "translation": [ - -0.031943444387396845, - -0.7661388188595013, - 1.64 + -3.996864, + 0.158358, + 2.568353 ], "rotation_deg": [ - -23.99193670023973, - 120.0, - 0.0 + 0, + -79.937282, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1382, "type": "imguizmo", "translation": [ - 0.004433820386904652, - -0.7795296104438997, - 1.64 + -3.991291, + 0.263808, + 2.547353 ], "rotation_deg": [ - -24.4140742146533, - 120.0, - 0.0 + 0, + -79.825824, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1383, "type": "imguizmo", "translation": [ - 0.04141853190733425, - -0.7919492052037964, - 1.64 + -3.982937, + 0.369073, + 2.5265 ], "rotation_deg": [ - -24.80579483273252, - 120.0, - 0.0 + 0, + -79.658734, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1384, "type": "imguizmo", "translation": [ - 0.07896461182882031, - -0.8033821298706573, - 1.64 + -3.971806, + 0.474082, + 2.505852 ], "rotation_deg": [ - -25.16661051936587, - 120.0, - 0.0 + 0, + -79.436129, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1385, "type": "imguizmo", "translation": [ - 0.11702528241116694, - -0.813814140444021, - 1.64 + -3.957908, + 0.57876, + 2.485466 ], "rotation_deg": [ - -25.49607174313801, - 120.0, - 0.0 + 0, + -79.158163, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1386, "type": "imguizmo", "translation": [ - 0.1555531247982938, - -0.8232322399377476, - 1.64 + -3.941252, + 0.683035, + 2.465399 ], "rotation_deg": [ - -25.79376803638992, - 120.0, - 0.0 + 0, + -78.825031, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1387, "type": "imguizmo", "translation": [ - 0.19450013809622493, - -0.8316246945726475, - 1.64 + -3.921848, + 0.786833, + 2.445708 ], "rotation_deg": [ - -26.05932850661054, - 120.0, - 0.0 + 0, + -78.436964, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1388, "type": "imguizmo", "translation": [ - 0.23381779917622947, - -0.8389810483953073, - 1.64 + -3.899712, + 0.890084, + 2.426447 ], "rotation_deg": [ - -26.29242229852261, - 120.0, - 0.0 + 0, + -77.994233, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1389, "type": "imguizmo", "translation": [ - 0.2734571231285912, - -0.8452921363049061, - 1.64 + -3.874857, + 0.992714, + 2.407669 ], "rotation_deg": [ - -26.492759006287177, - 120.0, - 0.0 + 0, + -77.497147, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1390, "type": "imguizmo", "translation": [ - 0.313368724291711, - -0.8505500954717902, - 1.64 + -3.847303, + 1.094652, + 2.389427 ], "rotation_deg": [ - -26.66008903531307, - 120.0, - 0.0 + 0, + -76.946051, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1391, "type": "imguizmo", "translation": [ - 0.35350287778049017, - -0.8547483751335794, - 1.64 + -3.817067, + 1.195827, + 2.371772 ], "rotation_deg": [ - -26.794203913220738, - 120.0, - 0.0 + 0, + -76.341331, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1392, "type": "imguizmo", "translation": [ - 0.3938095814373452, - -0.8578817447566016, - 1.64 + -3.78417, + 1.296169, + 2.354754 ], "rotation_deg": [ - -26.89493654957281, - 120.0, - 0.0 + 0, + -75.683407, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1393, "type": "imguizmo", "translation": [ - 0.43423861812866993, - -0.8599463005524862, - 1.64 + -3.748637, + 1.395608, + 2.338419 ], "rotation_deg": [ - -26.96216144404802, - 120.0, - 0.0 + 0, + -74.972739, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1394, "type": "imguizmo", "translation": [ - 0.4747396183091305, - -0.8609394703417979, - 1.64 + -3.710491, + 1.494074, + 2.322813 ], "rotation_deg": [ - -26.995794842798965, - 120.0, - 0.0 + 0, + -74.20982, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1395, "type": "imguizmo", "translation": [ - 0.5152621227758457, - -0.860860016758653, - 1.64 + -3.669759, + 1.591499, + 2.307979 ], "rotation_deg": [ - -26.99579484279897, - 120.0, - 0.0 + 0, + -73.395184, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1396, "type": "imguizmo", "translation": [ - 0.555755645534275, - -0.859708038792322, - 1.64 + -3.62647, + 1.687814, + 2.29396 ], "rotation_deg": [ - -26.96216144404802, - 120.0, - 0.0 + 0, + -72.529397, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1397, "type": "imguizmo", "translation": [ - 0.5961697366974738, - -0.8574849716639021, - 1.64 + -3.580653, + 1.782953, + 2.280793 ], "rotation_deg": [ - -26.89493654957281, - 120.0, - 0.0 + 0, + -71.613063, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1398, "type": "imguizmo", "translation": [ - 0.6364540453403752, - -0.8541935850382104, - 1.64 + -3.532341, + 1.87685, + 2.268516 ], "rotation_deg": [ - -26.79420391322074, - 120.0, - 0.0 + 0, + -70.646821, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1399, "type": "imguizmo", "translation": [ - 0.6765583822307718, - -0.8498379795731258, - 1.64 + -3.481567, + 1.969439, + 2.257163 ], "rotation_deg": [ - -26.660089035313074, - 120.0, - 0.0 + 0, + -69.631344, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1400, "type": "imguizmo", "translation": [ - 0.7164327823588489, - -0.8444235818106807, - 1.64 + -3.428367, + 2.060655, + 2.246766 ], "rotation_deg": [ - -26.49275900628718, - 120.0, - 0.0 + 0, + -68.56734, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1401, "type": "imguizmo", "translation": [ - 0.7560275671873647, - -0.8379571374162648, - 1.64 + -3.372778, + 2.150435, + 2.237353 ], "rotation_deg": [ - -26.292422298522613, - 120.0, - 0.0 + 0, + -67.45555, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1402, "type": "imguizmo", "translation": [ - 0.7952934065449225, - -0.8304467027743663, - 1.64 + -3.314837, + 2.238717, + 2.228951 ], "rotation_deg": [ - -26.05932850661054, - 120.0, - 0.0 + 0, + -66.29675, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1403, "type": "imguizmo", "translation": [ - 0.8341813800852191, - -0.8219016349513202, - 1.64 + -3.254587, + 2.325438, + 2.221583 ], "rotation_deg": [ - -25.793768036389928, - 120.0, - 0.0 + 0, + -65.091746, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1404, "type": "imguizmo", "translation": [ - 0.8726430382357101, - -0.8123325800375678, - 1.64 + -3.192069, + 2.410539, + 2.21527 ], "rotation_deg": [ - -25.496071743138017, - 120.0, - 0.0 + 0, + -63.841378, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1405, "type": "imguizmo", "translation": [ - 0.9106304625597363, - -0.8017514598839556, - 1.64 + -3.127326, + 2.493959, + 2.210029 ], "rotation_deg": [ - -25.16661051936588, - 120.0, - 0.0 + 0, + -62.546519, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1406, "type": "imguizmo", "translation": [ - 0.9480963254569347, - -0.7901714572485918, - 1.64 + -3.060403, + 2.575642, + 2.205875 ], "rotation_deg": [ - -24.805794832732527, - 120.0, - 0.0 + 0, + -61.208069, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1407, "type": "imguizmo", "translation": [ - 0.9849939491275326, - -0.7776069993727726, - 1.64 + -2.991348, + 2.655529, + 2.202819 ], "rotation_deg": [ - -24.4140742146533, - 120.0, - 0.0 + 0, + -59.826963, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1408, "type": "imguizmo", "translation": [ - 1.0212773637270747, - -0.7640737400064349, - 1.64 + -2.920208, + 2.733566, + 2.200871 ], "rotation_deg": [ - -23.99193670023973, - 120.0, - 0.0 + 0, + -58.404163, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1409, "type": "imguizmo", "translation": [ - 1.056901364639124, - -0.7495885399055345, - 1.64 + -2.847033, + 2.809698, + 2.200035 ], "rotation_deg": [ - -23.539908220268547, - 120.0, - 0.0 + 0, + -56.940659, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1410, "type": "imguizmo", "translation": [ - 1.0918215687945865, - -0.7341694458256421, - 1.64 + -2.771874, + 2.883872, + 2.200314 ], "rotation_deg": [ - -23.058551945937413, - 120.0, - 0.0 + 0, + -55.437473, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1411, "type": "imguizmo", "translation": [ - 1.1259944699674846, - -0.7178356680379359, - 1.64 + -2.694783, + 2.956036, + 2.201706 ], "rotation_deg": [ - -22.548467587223776, - 120.0, - 0.0 + 0, + -53.895651, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1412, "type": "imguizmo", "translation": [ - 1.1593774929783038, - -0.7006075563955912, - 1.64 + -2.615813, + 3.026139, + 2.204209 ], "rotation_deg": [ - -22.01029064572084, - 120.0, - 0.0 + 0, + -52.316269, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1413, "type": "imguizmo", "translation": [ - 1.191929046737358, - -0.6825065749804025, - 1.64 + -2.535021, + 3.094134, + 2.207815 ], "rotation_deg": [ - -21.44469162288181, - 120.0, - 0.0 + 0, + -50.700427, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1414, "type": "imguizmo", "translation": [ - 1.2236085760621118, - -0.6635552753612074, - 1.64 + -2.452463, + 3.159973, + 2.212514 ], "rotation_deg": [ - -20.852375184658513, - 120.0, - 0.0 + 0, + -49.049251, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1415, "type": "imguizmo", "translation": [ - 1.2543766122038864, - -0.6437772684974403, - 1.64 + -2.368195, + 3.223609, + 2.218293 ], "rotation_deg": [ - -20.234079283575394, - 120.0, - 0.0 + 0, + -47.363891, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1416, "type": "imguizmo", "translation": [ - 1.284194822021007, - -0.6231971953228179, - 1.64 + -2.282276, + 3.284999, + 2.225136 ], "rotation_deg": [ - -19.590574239332543, - 120.0, - 0.0 + 0, + -45.645523, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1417, "type": "imguizmo", "translation": [ - 1.3130260557371232, - -0.6018406960458037, - 1.64 + -2.194767, + 3.344099, + 2.233024 ], "rotation_deg": [ - -18.922661779083295, - 120.0, - 0.0 + 0, + -43.895344, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1418, "type": "imguizmo", "translation": [ - 1.3408343932252036, - -0.5797343782051012, - 1.64 + -2.105729, + 3.400869, + 2.241935 ], "rotation_deg": [ - -18.23117403858203, - 120.0, - 0.0 + 0, + -42.114573, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1419, "type": "imguizmo", "translation": [ - 1.3675851887595423, - -0.5569057835199734, - 1.64 + -2.015223, + 3.455268, + 2.251843 ], "rotation_deg": [ - -17.516972525446608, - 120.0, - 0.0 + 0, + -40.304452, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1420, "type": "imguizmo", "translation": [ - 1.3932451141800186, - -0.5333833535766914, - 1.64 + -1.923312, + 3.50726, + 2.262722 ], "rotation_deg": [ - -16.780947045827208, - 120.0, - 0.0 + 0, + -38.466242, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1421, "type": "imguizmo", "translation": [ - 1.417782200414834, - -0.5091963943938584, - 1.64 + -1.830061, + 3.556807, + 2.274541 ], "rotation_deg": [ - -16.02401459581862, - 120.0, - 0.0 + 0, + -36.601225, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1422, "type": "imguizmo", "translation": [ - 1.4411658773099951, - -0.4843750399107602, - 1.64 + -1.735535, + 3.603875, + 2.287267 ], "rotation_deg": [ - -15.247118218997342, - 120.0, - 0.0 + 0, + -34.710699, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1423, "type": "imguizmo", "translation": [ - 1.4633670117159179, - -0.45895021444422957, - 1.64 + -1.639799, + 3.648432, + 2.300865 ], "rotation_deg": [ - -14.451225831506678, - 120.0, - 0.0 + 0, + -32.795983, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1424, "type": "imguizmo", "translation": [ - 1.4843579437837064, - -0.43295359416079804, - 1.64 + -1.542921, + 3.690447, + 2.315297 ], "rotation_deg": [ - -13.637329016153773, - 120.0, - 0.0 + 0, + -30.858411, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1425, "type": "imguizmo", "translation": [ - 1.5041125214258777, - -0.4064175676121365, - 1.64 + -1.444967, + 3.729889, + 2.330522 ], "rotation_deg": [ - -12.806441787020901, - 120.0, - 0.0 + 0, + -28.899333, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1426, "type": "imguizmo", "translation": [ - 1.5226061328986082, - -0.3793751953829538, - 1.64 + -1.346006, + 3.766732, + 2.346498 ], "rotation_deg": [ - -11.959599326130203, - 120.0, - 0.0 + 0, + -26.920115, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1427, "type": "imguizmo", "translation": [ - 1.5398157374648995, - -0.35186016890162497, - 1.64 + -1.246107, + 3.80095, + 2.363181 ], "rotation_deg": [ - -11.097856693735796, - 120.0, - 0.0 + 0, + -24.922136, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1428, "type": "imguizmo", "translation": [ - 1.5557198941004713, - -0.3239067684648643, - 1.64 + -1.145339, + 3.832518, + 2.380523 ], "rotation_deg": [ - -10.222287513849997, - 120.0, - 0.0 + 0, + -22.906788, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1429, "type": "imguizmo", "translation": [ - 1.5702987882066035, - -0.29554982052874884, - 1.64 + -1.043774, + 3.861416, + 2.398478 ], "rotation_deg": [ - -9.333982636641625, - 120.0, - 0.0 + 0, + -20.875476, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1430, "type": "imguizmo", "translation": [ - 1.5835342562966621, - -0.2668246543192875, - 1.64 + -0.941481, + 3.887623, + 2.416994 ], "rotation_deg": [ - -8.434048779372445, - 120.0, - 0.0 + 0, + -18.829615, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1431, "type": "imguizmo", "translation": [ - 1.5954098086255388, - -0.237767057816606, - 1.64 + -0.838532, + 3.911121, + 2.43602 ], "rotation_deg": [ - -7.52360714756529, - 120.0, - 0.0 + 0, + -16.770632, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1432, "type": "imguizmo", "translation": [ - 1.6059106497338234, - -0.2084132331675797, - 1.64 + -0.734998, + 3.931892, + 2.455503 ], "rotation_deg": [ - -6.603792038121595, - 120.0, - 0.0 + 0, + -14.699961, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1433, "type": "imguizmo", "translation": [ - 1.6150236968811007, - -0.17879975158246703, - 1.64 + -0.630952, + 3.949924, + 2.475389 ], "rotation_deg": [ - -5.675749426128716, - 120.0, - 0.0 + 0, + -12.619046, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1434, "type": "imguizmo", "translation": [ - 1.622737596345418, - -0.14896350777173506, - 1.64 + -0.526467, + 3.965203, + 2.495623 ], "rotation_deg": [ - -4.740635537117653, - 120.0, - 0.0 + 0, + -10.529336, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1435, "type": "imguizmo", "translation": [ - 1.6290427375686083, - -0.11894167397984569, - 1.64 + -0.421614, + 3.977718, + 2.516147 ], "rotation_deg": [ - -3.7996154065500596, - 120.0, - 0.0 + 0, + -8.432288, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1436, "type": "imguizmo", "translation": [ - 1.6339312651298483, - -0.08877165367326437, - 1.64 + -0.316468, + 3.987461, + 2.536905 ], "rotation_deg": [ - -2.853861428329023, - 120.0, - 0.0 + 0, + -6.329363, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1437, "type": "imguizmo", "translation": [ - 1.6373970885325355, - -0.05849103494040378, - 1.64 + -0.211101, + 3.994426, + 2.557839 ], "rotation_deg": [ - -1.9045518941424064, - 120.0, - 0.0 + 0, + -4.222028, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1438, "type": "imguizmo", "translation": [ - 1.6394358897922887, - -0.028137543661542527, - 1.64 + -0.105587, + 3.998606, + 2.57889 ], "rotation_deg": [ - -0.9528695254580246, - 120.0, - 0.0 + 0, + -2.11175, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1439, "type": "imguizmo", "translation": [ - 1.6400451288166176, - 0.0022510034929227526, - 1.64 + 0, + 4, + 2.6 ], "rotation_deg": [ - -5.551115123125783e-15, - 120.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { @@ -27404,7 +27647,7 @@ "frame": 1440, "type": "action", "action": "set_active_planar", - "value": 8 + "value": 6 }, { "frame": 1440, @@ -27412,22104 +27655,38812 @@ "action": "set_projection_type", "value": "perspective" }, + { + "frame": 1440, + "type": "action", + "action": "set_left_handed", + "value": true + }, + { + "frame": 1440, + "type": "action", + "action": "reset_active_camera", + "value": 1 + }, + { + "frame": 1440, + "type": "action", + "action": "set_active_render_window", + "value": 1 + }, + { + "frame": 1440, + "type": "action", + "action": "set_active_planar", + "value": 6 + }, + { + "frame": 1440, + "type": "action", + "action": "set_projection_type", + "value": "orthographic" + }, + { + "frame": 1440, + "type": "action", + "action": "set_left_handed", + "value": true + }, + { + "frame": 1440, + "type": "action", + "action": "set_active_render_window", + "value": 0 + }, { "frame": 1441, "type": "imguizmo", "translation": [ - 1.64, - 0.0, - 2.05 + 0, + 2.5, + 2.55 ], "rotation_deg": [ - 0.0, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1442, "type": "imguizmo", "translation": [ - 1.639227656874891, - 0.03281682645677409, - 2.1374570229790186 + 0.105587, + 2.499129, + 2.549512 ], "rotation_deg": [ - 0.9528695254580167, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1443, "type": "imguizmo", "translation": [ - 1.6369733771929313, - 0.06296742111772219, - 2.2174817776304567 + 0.211101, + 2.496516, + 2.54805 ], "rotation_deg": [ - 1.9045518941423958, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1444, "type": "imguizmo", "translation": [ - 1.6332968139599016, - 0.09324959390223336, - 2.2972318714373943 + 0.316468, + 2.492163, + 2.545618 ], "rotation_deg": [ - 2.8538614283290102, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1445, "type": "imguizmo", "translation": [ - 1.6281980001594487, - 0.12339878708940485, - 2.3757054905012067 + 0.421614, + 2.486074, + 2.542223 ], "rotation_deg": [ - 3.7996154065500436, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1446, "type": "imguizmo", "translation": [ - 1.6216836520831495, - 0.15339558493117983, - 2.4525600434344867 + 0.526467, + 2.478252, + 2.537874 ], "rotation_deg": [ - 4.74063553711766, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1447, "type": "imguizmo", "translation": [ - 1.6137618566931538, - 0.18320116344137866, - 2.5274087759015886 + 0.630952, + 2.468702, + 2.532583 ], "rotation_deg": [ - 5.675749426128718, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1448, "type": "imguizmo", "translation": [ - 1.6044424858884132, - 0.21277850471759752, - 2.5998791047878735 + 0.734998, + 2.457433, + 2.526365 ], "rotation_deg": [ - 6.603792038121597, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1449, "type": "imguizmo", "translation": [ - 1.5937371502581978, - 0.24209074978474063, - 2.6696099617997566 + 0.838532, + 2.44445, + 2.519238 ], "rotation_deg": [ - 7.523607147565286, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1450, "type": "imguizmo", "translation": [ - 1.5816591873325336, - 0.27110137997880757, - 2.73625395329601 + 0.941481, + 2.429764, + 2.511221 ], "rotation_deg": [ - 8.43404877937244, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1451, "type": "imguizmo", "translation": [ - 1.5682236447480375, - 0.29977425160763277, - 2.799479062104827 + 1.043774, + 2.413385, + 2.502336 ], "rotation_deg": [ - 9.333982636641618, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1452, "type": "imguizmo", "translation": [ - 1.5534472615178194, - 0.328073641848383, - 2.8589703039271184 + 1.145339, + 2.395324, + 2.492609 ], "rotation_deg": [ - 10.222287513849988, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1453, "type": "imguizmo", "translation": [ - 1.5373484471753618, - 0.35596429318438993, - 2.914431296387612 + 1.246107, + 2.375594, + 2.482066 ], "rotation_deg": [ - 11.097856693735785, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1454, "type": "imguizmo", "translation": [ - 1.519947258838648, - 0.3834114573371734, - 2.9655857356117235 + 1.346006, + 2.354207, + 2.470737 ], "rotation_deg": [ - 11.959599326130213, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1455, "type": "imguizmo", "translation": [ - 1.5012653762214814, - 0.41038093855802626, - 3.0121787727563767 + 1.444967, + 2.331181, + 2.458653 ], "rotation_deg": [ - 12.806441787020908, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1456, "type": "imguizmo", "translation": [ - 1.4813260746232602, - 0.43683913623169396, - 3.0539782836542213 + 1.542921, + 2.306529, + 2.445849 ], "rotation_deg": [ - 13.637329016153773, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1457, "type": "imguizmo", "translation": [ - 1.4601541959308557, - 0.4627530867385546, - 3.0907760252445415 + 1.639799, + 2.28027, + 2.432359 ], "rotation_deg": [ - 14.451225831506678, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1458, "type": "imguizmo", "translation": [ - 1.4377761176687125, - 0.48809050452318403, - 3.122388673029717 + 1.735535, + 2.252422, + 2.418221 ], "rotation_deg": [ - 15.247118218997338, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1459, "type": "imguizmo", "translation": [ - 1.4142197201357378, - 0.5128198223181384, - 3.148658734388647 + 1.830061, + 2.223004, + 2.403476 ], "rotation_deg": [ - 16.024014595818613, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1460, "type": "imguizmo", "translation": [ - 1.3895143516699222, - 0.5369102304728377, - 3.1694553331970656 + 1.923312, + 2.192037, + 2.388163 ], "rotation_deg": [ - 16.780947045827197, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1461, "type": "imguizmo", "translation": [ - 1.3636907920839643, - 0.560331715338554, - 3.184674861845811 + 2.015223, + 2.159543, + 2.372326 ], "rotation_deg": [ - 17.516972525446597, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1462, "type": "imguizmo", "translation": [ - 1.3367812143174558, - 0.5830550966616804, - 3.194241497408718 + 2.105729, + 2.125543, + 2.356008 ], "rotation_deg": [ - 18.23117403858202, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1463, "type": "imguizmo", "translation": [ - 1.308819144353407, - 0.605052063938694, - 3.1981075793886293 + 2.194767, + 2.090062, + 2.339256 ], "rotation_deg": [ - 18.922661779083285, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1464, "type": "imguizmo", "translation": [ - 1.2798394194490452, - 0.6262952116875169, - 3.1962538471595936 + 2.282276, + 2.053124, + 2.322116 ], "rotation_deg": [ - 19.59057423933253, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1465, "type": "imguizmo", "translation": [ - 1.2498781447329317, - 0.6467580735913346, - 3.188689535922328 + 2.368195, + 2.014756, + 2.304635 ], "rotation_deg": [ - 20.234079283575376, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1466, "type": "imguizmo", "translation": [ - 1.2189726482224659, - 0.666415155472332, - 3.17545233069492 + 2.452463, + 1.974983, + 2.286862 ], "rotation_deg": [ - 20.852375184658495, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1467, "type": "imguizmo", "translation": [ - 1.1871614343178254, - 0.6852419670542624, - 3.1566081785679616 + 2.535021, + 1.933834, + 2.268848 ], "rotation_deg": [ - 21.44469162288179, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1468, "type": "imguizmo", "translation": [ - 1.154484135830277, - 0.7032150524742842, - 3.1322509601594595 + 2.615813, + 1.891337, + 2.250641 ], "rotation_deg": [ - 22.01029064572082, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1469, "type": "imguizmo", "translation": [ - 1.12098146460463, - 0.7203120195060435, - 3.1025020219063193 + 2.694783, + 1.847522, + 2.232294 ], "rotation_deg": [ - 22.548467587223755, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1470, "type": "imguizmo", "translation": [ - 1.0866951607973458, - 0.7365115674576009, - 3.0675095715225016 + 2.771874, + 1.80242, + 2.213856 ], "rotation_deg": [ - 23.05855194593741, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1471, "type": "imguizmo", "translation": [ - 1.0516679408735028, - 0.7517935137094406, - 3.0274479396356453 + 2.847033, + 1.756061, + 2.19538 ], "rotation_deg": [ - 23.539908220268543, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1472, "type": "imguizmo", "translation": [ - 1.0159434443873971, - 0.7661388188595009, - 2.9825167112806477 + 2.920208, + 1.708479, + 2.176917 ], "rotation_deg": [ - 23.991936700239727, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1473, "type": "imguizmo", "translation": [ - 0.979566179613096, - 0.7795296104438992, - 2.9329397315770662 + 2.991348, + 1.659706, + 2.158518 ], "rotation_deg": [ - 24.414074214653297, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1474, "type": "imguizmo", "translation": [ - 0.9425814680926663, - 0.7919492052037957, - 2.878963990543984 + 3.060403, + 1.609776, + 2.140234 ], "rotation_deg": [ - 24.805794832732513, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1475, "type": "imguizmo", "translation": [ - 0.9050353881711805, - 0.8033821298706567, - 2.8208583926081676 + 3.127326, + 1.558725, + 2.122118 ], "rotation_deg": [ - 25.166610519365864, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1476, "type": "imguizmo", "translation": [ - 0.8669747175888339, - 0.8138141404440203, - 2.7589124169357313 + 3.192069, + 1.506587, + 2.104218 ], "rotation_deg": [ - 25.496071743138003, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1477, "type": "imguizmo", "translation": [ - 0.8284468752017072, - 0.823232239937747, - 2.693434675261499 + 3.254587, + 1.453399, + 2.086585 ], "rotation_deg": [ - 25.793768036389917, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1478, "type": "imguizmo", "translation": [ - 0.7894998619037751, - 0.8316246945726472, - 2.6247513744008613 + 3.314837, + 1.399198, + 2.069269 ], "rotation_deg": [ - 26.059328506610537, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1479, "type": "imguizmo", "translation": [ - 0.750182200823771, - 0.8389810483953068, - 2.5532046911038306 + 3.372778, + 1.344022, + 2.052316 ], "rotation_deg": [ - 26.292422298522606, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1480, "type": "imguizmo", "translation": [ - 0.7105428768714092, - 0.8452921363049057, - 2.479151067347685 + 3.428367, + 1.287909, + 2.035776 ], "rotation_deg": [ - 26.492759006287177, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1481, "type": "imguizmo", "translation": [ - 0.6706312757082894, - 0.8505500954717897, - 2.4029594345609673 + 3.481567, + 1.230899, + 2.019693 ], "rotation_deg": [ - 26.66008903531307, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1482, "type": "imguizmo", "translation": [ - 0.6304971222195103, - 0.8547483751335789, - 2.3250093756256662 + 3.532341, + 1.173031, + 2.004112 ], "rotation_deg": [ - 26.794203913220734, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1483, "type": "imguizmo", "translation": [ - 0.5901904185626555, - 0.8578817447566012, - 2.2456892338143897 + 3.580653, + 1.114346, + 1.989078 ], "rotation_deg": [ - 26.894936549572805, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1484, "type": "imguizmo", "translation": [ - 0.5497613818713308, - 0.8599463005524858, - 2.1653941780837167 + 3.62647, + 1.054884, + 1.974631 ], "rotation_deg": [ - 26.962161444048018, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1485, "type": "imguizmo", "translation": [ - 0.5092603816908705, - 0.8609394703417974, - 2.0845242343623345 + 3.669759, + 0.994687, + 1.960813 ], "rotation_deg": [ - 26.99579484279896, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1486, "type": "imguizmo", "translation": [ - 0.4687378772241541, - 0.8608600167586525, - 2.0034822926419804 + 3.710491, + 0.933796, + 1.947661 ], "rotation_deg": [ - 26.99579484279896, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1487, "type": "imguizmo", "translation": [ - 0.4282443544657249, - 0.8597080387923216, - 1.9226720997997726 + 3.748637, + 0.872255, + 1.935213 ], "rotation_deg": [ - 26.962161444048018, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1488, "type": "imguizmo", "translation": [ - 0.3878302633025267, - 0.8574849716639017, - 1.8424962481515754 + 3.78417, + 0.810106, + 1.923502 ], "rotation_deg": [ - 26.894936549572808, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1489, "type": "imguizmo", "translation": [ - 0.3475459546596252, - 0.85419358503821, - 1.7633541697573303 + 3.817067, + 0.747392, + 1.912563 ], "rotation_deg": [ - 26.794203913220734, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1490, "type": "imguizmo", "translation": [ - 0.30744161776922857, - 0.8498379795731253, - 1.6856401464706305 + 3.847303, + 0.684157, + 1.902424 ], "rotation_deg": [ - 26.66008903531307, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1491, "type": "imguizmo", "translation": [ - 0.26756721764115154, - 0.8444235818106802, - 1.6097413456463574 + 3.874857, + 0.620446, + 1.893115 ], "rotation_deg": [ - 26.492759006287177, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1492, "type": "imguizmo", "translation": [ - 0.2279724328126359, - 0.8379571374162643, - 1.5360358912923917 + 3.899712, + 0.556302, + 1.884661 ], "rotation_deg": [ - 26.29242229852261, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1493, "type": "imguizmo", "translation": [ - 0.18870659345507812, - 0.8304467027743658, - 1.4648909802748267 + 3.921848, + 0.491771, + 1.877086 ], "rotation_deg": [ - 26.059328506610537, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1494, "type": "imguizmo", "translation": [ - 0.1498186199147805, - 0.8219016349513193, - 1.3966610529616552 + 3.941252, + 0.426897, + 1.870411 ], "rotation_deg": [ - 25.793768036389917, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1495, "type": "imguizmo", "translation": [ - 0.11135696176428965, - 0.8123325800375673, - 1.3316860274186815 + 3.957908, + 0.361725, + 1.864655 ], "rotation_deg": [ - 25.496071743138003, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1496, "type": "imguizmo", "translation": [ - 0.07336953744026398, - 0.801751459883955, - 1.2702896059548188 + 3.971806, + 0.296301, + 1.859833 ], "rotation_deg": [ - 25.166610519365868, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1497, "type": "imguizmo", "translation": [ - 0.03590367454306552, - 0.7901714572485914, - 1.212777662453464 + 3.982937, + 0.230671, + 1.855959 ], "rotation_deg": [ - 24.805794832732516, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1498, "type": "imguizmo", "translation": [ - -0.0009939491275325688, - 0.7776069993727719, - 1.1594367185242036 + 3.991291, + 0.16488, + 1.853045 ], "rotation_deg": [ - 24.41407421465329, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1499, "type": "imguizmo", "translation": [ - -0.03727736372707465, - 0.7640737400064342, - 1.1105325160665624 + 3.996864, + 0.098974, + 1.851097 ], "rotation_deg": [ - 23.99193670023972, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1500, "type": "imguizmo", "translation": [ - -0.07290136463912357, - 0.749588539905534, - 1.0663086933572283 + 3.999652, + 0.032999, + 1.850122 ], "rotation_deg": [ - 23.539908220268543, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1501, "type": "imguizmo", "translation": [ - -0.10782156879458578, - 0.7341694458256416, - 1.0269855712564127 + 3.999652, + -0.032999, + 1.850122 ], "rotation_deg": [ - 23.05855194593741, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1502, "type": "imguizmo", "translation": [ - -0.14199446996748483, - 0.7178356680379347, - 0.992759055580409 + 3.996864, + -0.098974, + 1.851097 ], "rotation_deg": [ - 22.548467587223755, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1503, "type": "imguizmo", "translation": [ - -0.1753774929783042, - 0.7006075563955902, - 0.9637996611086691 + 3.991291, + -0.16488, + 1.853045 ], "rotation_deg": [ - 22.01029064572082, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1504, "type": "imguizmo", "translation": [ - -0.20792904673735776, - 0.6825065749804016, - 0.9402516620877498 + 3.982937, + -0.230671, + 1.855959 ], "rotation_deg": [ - 21.444691622881795, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1505, "type": "imguizmo", "translation": [ - -0.23960857606211147, - 0.6635552753612066, - 0.9222323734642723 + 3.971806, + -0.296301, + 1.859833 ], "rotation_deg": [ - 20.8523751846585, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1506, "type": "imguizmo", "translation": [ - -0.270376612203886, - 0.6437772684974393, - 0.9098315664277614 + 3.957908, + -0.361725, + 1.864655 ], "rotation_deg": [ - 20.23407928357538, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1507, "type": "imguizmo", "translation": [ - -0.3001948220210066, - 0.623197195322817, - 0.9031110211750932 + 3.941252, + -0.426897, + 1.870411 ], "rotation_deg": [ - 19.59057423933253, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1508, "type": "imguizmo", "translation": [ - -0.3290260557371222, - 0.6018406960458029, - 0.9021042191246632 + 3.921848, + -0.491771, + 1.877086 ], "rotation_deg": [ - 18.92266177908329, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1509, "type": "imguizmo", "translation": [ - -0.35683439322520266, - 0.5797343782051005, - 0.9068161761136428 + 3.899712, + -0.556302, + 1.884661 ], "rotation_deg": [ - 18.231174038582026, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1510, "type": "imguizmo", "translation": [ - -0.3835851887595415, - 0.5569057835199728, - 0.9172234174093314 + 3.874857, + -0.620446, + 1.893115 ], "rotation_deg": [ - 17.516972525446604, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1511, "type": "imguizmo", "translation": [ - -0.40924511418001785, - 0.5333833535766906, - 0.9332740946590943 + 3.847303, + -0.684157, + 1.902424 ], "rotation_deg": [ - 16.780947045827197, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1512, "type": "imguizmo", "translation": [ - -0.4337822004148328, - 0.509196394393858, - 0.9548882441962462 + 3.817067, + -0.747392, + 1.912563 ], "rotation_deg": [ - 16.024014595818624, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1513, "type": "imguizmo", "translation": [ - -0.4571658773099938, - 0.48437503991075975, - 0.9819581854150184 + 3.78417, + -0.810106, + 1.923502 ], "rotation_deg": [ - 15.247118218997347, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1514, "type": "imguizmo", "translation": [ - -0.4793670117159167, - 0.4589502144442291, - 1.0143490572299183 + 3.748637, + -0.872255, + 1.935213 ], "rotation_deg": [ - 14.45122583150668, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1515, "type": "imguizmo", "translation": [ - -0.5003579437837052, - 0.43295359416079754, - 1.0518994899468739 + 3.710491, + -0.933796, + 1.947661 ], "rotation_deg": [ - 13.637329016153776, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1516, "type": "imguizmo", "translation": [ - -0.5201125214258765, - 0.40641756761213643, - 1.0944224091989336 + 3.669759, + -0.994687, + 1.960813 ], "rotation_deg": [ - 12.806441787020917, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1517, "type": "imguizmo", "translation": [ - -0.5386061328986069, - 0.3793751953829538, - 1.141705967941352 + 3.62647, + -1.054884, + 1.974631 ], "rotation_deg": [ - 11.959599326130222, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1518, "type": "imguizmo", "translation": [ - -0.5558157374648987, - 0.35186016890162425, - 1.193514601862909 + 3.580653, + -1.114346, + 1.989078 ], "rotation_deg": [ - 11.09785669373579, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1519, "type": "imguizmo", "translation": [ - -0.5717198941004705, - 0.32390676846486366, - 1.2495902029554613 + 3.532341, + -1.173031, + 2.004112 ], "rotation_deg": [ - 10.222287513849992, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1520, "type": "imguizmo", "translation": [ - -0.5862987882066028, - 0.2955498205287481, - 1.3096534053950537 + 3.481567, + -1.230899, + 2.019693 ], "rotation_deg": [ - 9.33398263664162, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1521, "type": "imguizmo", "translation": [ - -0.5995342562966611, - 0.26682465431928676, - 1.3734049773284036 + 3.428367, + -1.287909, + 2.035776 ], "rotation_deg": [ - 8.434048779372437, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1522, "type": "imguizmo", "translation": [ - -0.611409808625538, - 0.2377670578166053, - 1.44052731163094 + 3.372778, + -1.344022, + 2.052316 ], "rotation_deg": [ - 7.523607147565284, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1523, "type": "imguizmo", "translation": [ - -0.6219106497338223, - 0.20841323316757898, - 1.5106860082095237 + 3.314837, + -1.399198, + 2.069269 ], "rotation_deg": [ - 6.60379203812159, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1524, "type": "imguizmo", "translation": [ - -0.6310236968810998, - 0.17879975158246672, - 1.583531539966889 + 3.254587, + -1.453399, + 2.086585 ], "rotation_deg": [ - 5.675749426128722, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1525, "type": "imguizmo", "translation": [ - -0.6387375963454169, - 0.14896350777173473, - 1.6587009941280644 + 3.192069, + -1.506587, + 2.104218 ], "rotation_deg": [ - 4.74063553711766, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1526, "type": "imguizmo", "translation": [ - -0.6450427375686073, - 0.11894167397984451, - 1.7358198802535556 + 3.127326, + -1.558725, + 2.122118 ], "rotation_deg": [ - 3.799615406550042, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1527, "type": "imguizmo", "translation": [ - -0.6499312651298472, - 0.08877165367326328, - 1.8145039959318838 + 3.060403, + -1.609776, + 2.140234 ], "rotation_deg": [ - 2.8538614283290062, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1528, "type": "imguizmo", "translation": [ - -0.6533970885325346, - 0.05849103494040307, - 1.894361340856669 + 2.991348, + -1.659706, + 2.158518 ], "rotation_deg": [ - 1.9045518941424011, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1529, "type": "imguizmo", "translation": [ - -0.655435889792288, - 0.02813754366154179, - 1.9749940697524457 + 2.920208, + -1.708479, + 2.176917 ], "rotation_deg": [ - 0.9528695254580194, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1530, "type": "imguizmo", "translation": [ - -0.6560451288166165, - -0.0022510034929234743, - 2.0560004744198026 + 2.847033, + -1.756061, + 2.19538 ], "rotation_deg": [ - 3.3306690738754696e-16, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1531, "type": "imguizmo", "translation": [ - -0.6552240465695613, - -0.03263674617734043, - 2.136976985025433 + 2.771874, + -1.80242, + 2.213856 ], "rotation_deg": [ - -0.9528695254580187, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1532, "type": "imguizmo", "translation": [ - -0.6529736660173575, - -0.06298182754007718, - 2.217520180666742 + 2.694783, + -1.847522, + 2.232294 ], "rotation_deg": [ - -1.9045518941424004, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1533, "type": "imguizmo", "translation": [ - -0.6492967908539472, - -0.09324844138844533, - 2.29722879919449 + 2.615813, + -1.891337, + 2.250641 ], "rotation_deg": [ - -2.853861428329018, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1534, "type": "imguizmo", "translation": [ - -0.6441980020079249, - -0.12339887929050834, - 2.375705736280638 + 2.535021, + -1.933834, + 2.268848 ], "rotation_deg": [ - -3.7996154065500534, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1535, "type": "imguizmo", "translation": [ - -0.637683651935271, - -0.1533955775550912, - 2.45256002377213 + 2.452463, + -1.974983, + 2.286862 ], "rotation_deg": [ - -4.740635537117647, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1536, "type": "imguizmo", "translation": [ - -0.6297618567049839, - -0.18320116403146564, - 2.5274087774745744 + 2.368195, + -2.014756, + 2.304635 ], "rotation_deg": [ - -5.675749426128711, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1537, "type": "imguizmo", "translation": [ - -0.620442485887467, - -0.21277850467039044, - 2.599879104662032 + 2.282276, + -2.053124, + 2.322116 ], "rotation_deg": [ - -6.6037920381215915, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1538, "type": "imguizmo", "translation": [ - -0.6097371502582737, - -0.2420907497885172, - 2.669609961809822 + 2.194767, + -2.090062, + 2.339256 ], "rotation_deg": [ - -7.523607147565285, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1539, "type": "imguizmo", "translation": [ - -0.5976591873325274, - -0.27110137997850553, - 2.736253953295204 + 2.105729, + -2.125543, + 2.356008 ], "rotation_deg": [ - -8.43404877937244, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1540, "type": "imguizmo", "translation": [ - -0.5842236447480383, - -0.299774251607657, - 2.7994790621048904 + 2.015223, + -2.159543, + 2.372326 ], "rotation_deg": [ - -9.333982636641622, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1541, "type": "imguizmo", "translation": [ - -0.5694472615178193, - -0.3280736418483813, - 2.858970303927112 + 1.923312, + -2.192037, + 2.388163 ], "rotation_deg": [ - -10.222287513849993, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1542, "type": "imguizmo", "translation": [ - -0.5533484471753617, - -0.3559642931843903, - 2.9144312963876113 + 1.830061, + -2.223004, + 2.403476 ], "rotation_deg": [ - -11.097856693735793, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1543, "type": "imguizmo", "translation": [ - -0.5359472588386486, - -0.3834114573371734, - 2.965585735611721 + 1.735535, + -2.252422, + 2.418221 ], "rotation_deg": [ - -11.959599326130213, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1544, "type": "imguizmo", "translation": [ - -0.5172653762214814, - -0.41038093855802643, - 3.012178772756376 + 1.639799, + -2.28027, + 2.432359 ], "rotation_deg": [ - -12.806441787020908, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1545, "type": "imguizmo", "translation": [ - -0.49732607462326034, - -0.4368391362316941, - 3.0539782836542195 + 1.542921, + -2.306529, + 2.445849 ], "rotation_deg": [ - -13.637329016153778, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1546, "type": "imguizmo", "translation": [ - -0.4761541959308557, - -0.4627530867385548, - 3.0907760252445398 + 1.444967, + -2.331181, + 2.458653 ], "rotation_deg": [ - -14.451225831506685, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1547, "type": "imguizmo", "translation": [ - -0.4537761176687122, - -0.4880905045231844, - 3.1223886730297163 + 1.346006, + -2.354207, + 2.470737 ], "rotation_deg": [ - -15.247118218997349, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1548, "type": "imguizmo", "translation": [ - -0.4302197201357375, - -0.5128198223181389, - 3.1486587343886456 + 1.246107, + -2.375594, + 2.482066 ], "rotation_deg": [ - -16.024014595818628, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1549, "type": "imguizmo", "translation": [ - -0.40551435166992184, - -0.5369102304728384, - 3.169455333197065 + 1.145339, + -2.395324, + 2.492609 ], "rotation_deg": [ - -16.78094704582721, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1550, "type": "imguizmo", "translation": [ - -0.37969079208396367, - -0.5603317153385546, - 3.184674861845809 + 1.043774, + -2.413385, + 2.502336 ], "rotation_deg": [ - -17.516972525446615, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1551, "type": "imguizmo", "translation": [ - -0.35278121431745607, - -0.5830550966616804, - 3.194241497408717 + 0.941481, + -2.429764, + 2.511221 ], "rotation_deg": [ - -18.231174038582015, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1552, "type": "imguizmo", "translation": [ - -0.324819144353407, - -0.6050520639386942, - 3.1981075793886284 + 0.838532, + -2.44445, + 2.519238 ], "rotation_deg": [ - -18.922661779083285, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1553, "type": "imguizmo", "translation": [ - -0.2958394194490453, - -0.6262952116875171, - 3.1962538471595927 + 0.734998, + -2.457433, + 2.526365 ], "rotation_deg": [ - -19.590574239332533, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1554, "type": "imguizmo", "translation": [ - -0.2658781447329314, - -0.646758073591335, - 3.188689535922327 + 0.630952, + -2.468702, + 2.532583 ], "rotation_deg": [ - -20.23407928357538, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1555, "type": "imguizmo", "translation": [ - -0.23497264822246525, - -0.6664151554723325, - 3.1754523306949185 + 0.526467, + -2.478252, + 2.537874 ], "rotation_deg": [ - -20.85237518465851, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1556, "type": "imguizmo", "translation": [ - -0.20316143431782474, - -0.6852419670542631, - 3.1566081785679603 + 0.421614, + -2.486074, + 2.542223 ], "rotation_deg": [ - -21.444691622881802, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1557, "type": "imguizmo", "translation": [ - -0.17048413583027625, - -0.703215052474285, - 3.132250960159458 + 0.316468, + -2.492163, + 2.545618 ], "rotation_deg": [ - -22.010290645720836, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1558, "type": "imguizmo", "translation": [ - -0.13698146460462893, - -0.7203120195060444, - 3.102502021906318 + 0.211101, + -2.496516, + 2.54805 ], "rotation_deg": [ - -22.548467587223772, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1559, "type": "imguizmo", "translation": [ - -0.10269516079734561, - -0.7365115674576013, - 3.0675095715225003 + 0.105587, + -2.499129, + 2.549512 ], "rotation_deg": [ - -23.058551945937413, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1560, "type": "imguizmo", "translation": [ - -0.06766794087350216, - -0.751793513709441, - 3.027447939635644 + 0, + -2.5, + 2.55 ], "rotation_deg": [ - -23.539908220268547, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1561, "type": "imguizmo", "translation": [ - -0.031943444387396845, - -0.7661388188595013, - 2.9825167112806463 + -0.105587, + -2.499129, + 2.549512 ], "rotation_deg": [ - -23.99193670023973, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1562, "type": "imguizmo", "translation": [ - 0.004433820386904652, - -0.7795296104438997, - 2.9329397315770644 + -0.211101, + -2.496516, + 2.54805 ], "rotation_deg": [ - -24.4140742146533, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1563, "type": "imguizmo", "translation": [ - 0.04141853190733425, - -0.7919492052037964, - 2.878963990543982 + -0.316468, + -2.492163, + 2.545618 ], "rotation_deg": [ - -24.80579483273252, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1564, "type": "imguizmo", "translation": [ - 0.07896461182882031, - -0.8033821298706573, - 2.820858392608165 + -0.421614, + -2.486074, + 2.542223 ], "rotation_deg": [ - -25.16661051936587, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1565, "type": "imguizmo", "translation": [ - 0.11702528241116694, - -0.813814140444021, - 2.7589124169357295 + -0.526467, + -2.478252, + 2.537874 ], "rotation_deg": [ - -25.49607174313801, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1566, "type": "imguizmo", "translation": [ - 0.1555531247982938, - -0.8232322399377476, - 2.693434675261497 + -0.630952, + -2.468702, + 2.532583 ], "rotation_deg": [ - -25.79376803638992, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1567, "type": "imguizmo", "translation": [ - 0.19450013809622493, - -0.8316246945726475, - 2.6247513744008604 + -0.734998, + -2.457433, + 2.526365 ], "rotation_deg": [ - -26.05932850661054, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1568, "type": "imguizmo", "translation": [ - 0.23381779917622947, - -0.8389810483953073, - 2.5532046911038293 + -0.838532, + -2.44445, + 2.519238 ], "rotation_deg": [ - -26.29242229852261, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1569, "type": "imguizmo", "translation": [ - 0.2734571231285912, - -0.8452921363049061, - 2.4791510673476833 + -0.941481, + -2.429764, + 2.511221 ], "rotation_deg": [ - -26.492759006287177, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1570, "type": "imguizmo", "translation": [ - 0.313368724291711, - -0.8505500954717902, - 2.402959434560966 + -1.043774, + -2.413385, + 2.502336 ], "rotation_deg": [ - -26.66008903531307, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1571, "type": "imguizmo", "translation": [ - 0.35350287778049017, - -0.8547483751335794, - 2.3250093756256645 + -1.145339, + -2.395324, + 2.492609 ], "rotation_deg": [ - -26.794203913220738, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1572, "type": "imguizmo", "translation": [ - 0.3938095814373452, - -0.8578817447566016, - 2.245689233814388 + -1.246107, + -2.375594, + 2.482066 ], "rotation_deg": [ - -26.89493654957281, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1573, "type": "imguizmo", "translation": [ - 0.43423861812866993, - -0.8599463005524862, - 2.1653941780837145 + -1.346006, + -2.354207, + 2.470737 ], "rotation_deg": [ - -26.96216144404802, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1574, "type": "imguizmo", "translation": [ - 0.4747396183091305, - -0.8609394703417979, - 2.0845242343623323 + -1.444967, + -2.331181, + 2.458653 ], "rotation_deg": [ - -26.995794842798965, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1575, "type": "imguizmo", "translation": [ - 0.5152621227758457, - -0.860860016758653, - 2.003482292641981 + -1.542921, + -2.306529, + 2.445849 ], "rotation_deg": [ - -26.99579484279897, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1576, "type": "imguizmo", "translation": [ - 0.555755645534275, - -0.859708038792322, - 1.9226720997997726 + -1.639799, + -2.28027, + 2.432359 ], "rotation_deg": [ - -26.96216144404802, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1577, "type": "imguizmo", "translation": [ - 0.5961697366974738, - -0.8574849716639021, - 1.8424962481515743 + -1.735535, + -2.252422, + 2.418221 ], "rotation_deg": [ - -26.89493654957281, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1578, "type": "imguizmo", "translation": [ - 0.6364540453403752, - -0.8541935850382104, - 1.763354169757329 + -1.830061, + -2.223004, + 2.403476 ], "rotation_deg": [ - -26.79420391322074, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1579, "type": "imguizmo", "translation": [ - 0.6765583822307718, - -0.8498379795731258, - 1.6856401464706297 + -1.923312, + -2.192037, + 2.388163 ], "rotation_deg": [ - -26.660089035313074, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1580, "type": "imguizmo", "translation": [ - 0.7164327823588489, - -0.8444235818106807, - 1.6097413456463563 + -2.015223, + -2.159543, + 2.372326 ], "rotation_deg": [ - -26.49275900628718, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1581, "type": "imguizmo", "translation": [ - 0.7560275671873647, - -0.8379571374162648, - 1.53603589129239 + -2.105729, + -2.125543, + 2.356008 ], "rotation_deg": [ - -26.292422298522613, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1582, "type": "imguizmo", "translation": [ - 0.7952934065449225, - -0.8304467027743663, - 1.4648909802748251 + -2.194767, + -2.090062, + 2.339256 ], "rotation_deg": [ - -26.05932850661054, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1583, "type": "imguizmo", "translation": [ - 0.8341813800852191, - -0.8219016349513202, - 1.3966610529616557 + -2.282276, + -2.053124, + 2.322116 ], "rotation_deg": [ - -25.793768036389928, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1584, "type": "imguizmo", "translation": [ - 0.8726430382357101, - -0.8123325800375678, - 1.3316860274186817 + -2.368195, + -2.014756, + 2.304635 ], "rotation_deg": [ - -25.496071743138017, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1585, "type": "imguizmo", "translation": [ - 0.9106304625597363, - -0.8017514598839556, - 1.2702896059548179 + -2.452463, + -1.974983, + 2.286862 ], "rotation_deg": [ - -25.16661051936588, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1586, "type": "imguizmo", "translation": [ - 0.9480963254569347, - -0.7901714572485918, - 1.2127776624534636 + -2.535021, + -1.933834, + 2.268848 ], "rotation_deg": [ - -24.805794832732527, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1587, "type": "imguizmo", "translation": [ - 0.9849939491275326, - -0.7776069993727726, - 1.1594367185242034 + -2.615813, + -1.891337, + 2.250641 ], "rotation_deg": [ - -24.4140742146533, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1588, "type": "imguizmo", "translation": [ - 1.0212773637270747, - -0.7640737400064349, - 1.110532516066562 + -2.694783, + -1.847522, + 2.232294 ], "rotation_deg": [ - -23.99193670023973, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1589, "type": "imguizmo", "translation": [ - 1.056901364639124, - -0.7495885399055345, - 1.0663086933572274 + -2.771874, + -1.80242, + 2.213856 ], "rotation_deg": [ - -23.539908220268547, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1590, "type": "imguizmo", "translation": [ - 1.0918215687945865, - -0.7341694458256421, - 1.0269855712564118 + -2.847033, + -1.756061, + 2.19538 ], "rotation_deg": [ - -23.058551945937413, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1591, "type": "imguizmo", "translation": [ - 1.1259944699674846, - -0.7178356680379359, - 0.9927590555804089 + -2.920208, + -1.708479, + 2.176917 ], "rotation_deg": [ - -22.548467587223776, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1592, "type": "imguizmo", "translation": [ - 1.1593774929783038, - -0.7006075563955912, - 0.9637996611086689 + -2.991348, + -1.659706, + 2.158518 ], "rotation_deg": [ - -22.01029064572084, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1593, "type": "imguizmo", "translation": [ - 1.191929046737358, - -0.6825065749804025, - 0.9402516620877491 + -3.060403, + -1.609776, + 2.140234 ], "rotation_deg": [ - -21.44469162288181, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1594, "type": "imguizmo", "translation": [ - 1.2236085760621118, - -0.6635552753612074, - 0.9222323734642716 + -3.127326, + -1.558725, + 2.122118 ], "rotation_deg": [ - -20.852375184658513, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1595, "type": "imguizmo", "translation": [ - 1.2543766122038864, - -0.6437772684974403, - 0.9098315664277605 + -3.192069, + -1.506587, + 2.104218 ], "rotation_deg": [ - -20.234079283575394, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1596, "type": "imguizmo", "translation": [ - 1.284194822021007, - -0.6231971953228179, - 0.9031110211750922 + -3.254587, + -1.453399, + 2.086585 ], "rotation_deg": [ - -19.590574239332543, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1597, "type": "imguizmo", "translation": [ - 1.3130260557371232, - -0.6018406960458037, - 0.9021042191246622 + -3.314837, + -1.399198, + 2.069269 ], "rotation_deg": [ - -18.922661779083295, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1598, "type": "imguizmo", "translation": [ - 1.3408343932252036, - -0.5797343782051012, - 0.9068161761136418 + -3.372778, + -1.344022, + 2.052316 ], "rotation_deg": [ - -18.23117403858203, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1599, "type": "imguizmo", "translation": [ - 1.3675851887595423, - -0.5569057835199734, - 0.9172234174093304 + -3.428367, + -1.287909, + 2.035776 ], "rotation_deg": [ - -17.516972525446608, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1600, "type": "imguizmo", "translation": [ - 1.3932451141800186, - -0.5333833535766914, - 0.9332740946590933 + -3.481567, + -1.230899, + 2.019693 ], "rotation_deg": [ - -16.780947045827208, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1601, "type": "imguizmo", "translation": [ - 1.417782200414834, - -0.5091963943938584, - 0.9548882441962453 + -3.532341, + -1.173031, + 2.004112 ], "rotation_deg": [ - -16.02401459581862, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1602, "type": "imguizmo", "translation": [ - 1.4411658773099951, - -0.4843750399107602, - 0.9819581854150177 + -3.580653, + -1.114346, + 1.989078 ], "rotation_deg": [ - -15.247118218997342, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1603, "type": "imguizmo", "translation": [ - 1.4633670117159179, - -0.45895021444422957, - 1.0143490572299174 + -3.62647, + -1.054884, + 1.974631 ], "rotation_deg": [ - -14.451225831506678, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1604, "type": "imguizmo", "translation": [ - 1.4843579437837064, - -0.43295359416079804, - 1.0518994899468734 + -3.669759, + -0.994687, + 1.960813 ], "rotation_deg": [ - -13.637329016153773, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1605, "type": "imguizmo", "translation": [ - 1.5041125214258777, - -0.4064175676121365, - 1.0944224091989334 + -3.710491, + -0.933796, + 1.947661 ], "rotation_deg": [ - -12.806441787020901, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1606, "type": "imguizmo", "translation": [ - 1.5226061328986082, - -0.3793751953829538, - 1.1417059679413517 + -3.748637, + -0.872255, + 1.935213 ], "rotation_deg": [ - -11.959599326130203, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1607, "type": "imguizmo", "translation": [ - 1.5398157374648995, - -0.35186016890162497, - 1.1935146018629077 + -3.78417, + -0.810106, + 1.923502 ], "rotation_deg": [ - -11.097856693735796, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1608, "type": "imguizmo", "translation": [ - 1.5557198941004713, - -0.3239067684648643, - 1.2495902029554598 + -3.817067, + -0.747392, + 1.912563 ], "rotation_deg": [ - -10.222287513849997, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1609, "type": "imguizmo", "translation": [ - 1.5702987882066035, - -0.29554982052874884, - 1.3096534053950526 + -3.847303, + -0.684157, + 1.902424 ], "rotation_deg": [ - -9.333982636641625, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1610, "type": "imguizmo", "translation": [ - 1.5835342562966621, - -0.2668246543192875, - 1.3734049773284023 + -3.874857, + -0.620446, + 1.893115 ], "rotation_deg": [ - -8.434048779372445, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1611, "type": "imguizmo", "translation": [ - 1.5954098086255388, - -0.237767057816606, - 1.4405273116309387 + -3.899712, + -0.556302, + 1.884661 ], "rotation_deg": [ - -7.52360714756529, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1612, "type": "imguizmo", "translation": [ - 1.6059106497338234, - -0.2084132331675797, - 1.5106860082095224 + -3.921848, + -0.491771, + 1.877086 ], "rotation_deg": [ - -6.603792038121595, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1613, "type": "imguizmo", "translation": [ - 1.6150236968811007, - -0.17879975158246703, - 1.583531539966889 + -3.941252, + -0.426897, + 1.870411 ], "rotation_deg": [ - -5.675749426128716, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1614, "type": "imguizmo", "translation": [ - 1.622737596345418, - -0.14896350777173506, - 1.658700994128064 + -3.957908, + -0.361725, + 1.864655 ], "rotation_deg": [ - -4.740635537117653, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1615, "type": "imguizmo", "translation": [ - 1.6290427375686083, - -0.11894167397984569, - 1.7358198802535532 + -3.971806, + -0.296301, + 1.859833 ], "rotation_deg": [ - -3.7996154065500596, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1616, "type": "imguizmo", "translation": [ - 1.6339312651298483, - -0.08877165367326437, - 1.8145039959318814 + -3.982937, + -0.230671, + 1.855959 ], "rotation_deg": [ - -2.853861428329023, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1617, "type": "imguizmo", "translation": [ - 1.6373970885325355, - -0.05849103494040378, - 1.8943613408566682 + -3.991291, + -0.16488, + 1.853045 ], "rotation_deg": [ - -1.9045518941424064, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1618, "type": "imguizmo", "translation": [ - 1.6394358897922887, - -0.028137543661542527, - 1.9749940697524448 + -3.996864, + -0.098974, + 1.851097 ], "rotation_deg": [ - -0.9528695254580246, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1619, "type": "imguizmo", "translation": [ - 1.6400451288166176, - 0.0022510034929227526, - 2.0560004744198013 + -3.999652, + -0.032999, + 1.850122 ], "rotation_deg": [ - -5.551115123125783e-15, - 140.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1620, - "type": "action", - "action": "set_active_render_window", - "value": 0 - }, - { - "frame": 1620, - "type": "action", - "action": "set_active_planar", - "value": 9 - }, - { - "frame": 1620, - "type": "action", - "action": "set_projection_type", - "value": "perspective" + "type": "imguizmo", + "translation": [ + -3.999652, + 0.032999, + 1.850122 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] }, { "frame": 1621, "type": "imguizmo", "translation": [ - 0.0, - 1.0395, - 0.0 + -3.996864, + 0.098974, + 1.851097 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1622, "type": "imguizmo", "translation": [ - 0.006339250378967094, - 1.0395, - 0.019797816939610205 + -3.991291, + 0.16488, + 1.853045 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1623, "type": "imguizmo", "translation": [ - 0.012163462811033164, - 1.0395, - 0.03791317679591889 + -3.982937, + 0.230671, + 1.855959 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1624, "type": "imguizmo", "translation": [ - 0.01801309228550459, - 1.0395, - 0.05596636114093453 + -3.971806, + 0.296301, + 1.859833 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1625, "type": "imguizmo", "translation": [ - 0.02383703399385577, - 1.0395, - 0.07373058740766639 + -3.957908, + 0.361725, + 1.864655 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1626, "type": "imguizmo", "translation": [ - 0.029631537381827902, - 1.0395, - 0.09112830251527632 + -3.941252, + 0.426897, + 1.870411 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1627, "type": "imguizmo", "translation": [ - 0.035389102791602914, - 1.0395, - 0.1080719561301614 + -3.921848, + 0.491771, + 1.877086 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1628, "type": "imguizmo", "translation": [ - 0.04110257944788714, - 1.0395, - 0.12447720588566943 + -3.899712, + 0.556302, + 1.884661 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1629, "type": "imguizmo", "translation": [ - 0.04676484727549138, - 1.0395, - 0.1402623160476584 + -3.874857, + 0.620446, + 1.893115 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1630, "type": "imguizmo", "translation": [ - 0.05236885193736966, - 1.0395, - 0.15534864643972185 + -3.847303, + 0.684157, + 1.902424 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1631, "type": "imguizmo", "translation": [ - 0.0579076115300598, - 1.0395, - 0.16966103768683968 + -3.817067, + 0.747392, + 1.912563 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1632, "type": "imguizmo", "translation": [ - 0.06337422544973643, - 1.0395, - 0.18312818617862361 + -3.78417, + 0.810106, + 1.923502 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1633, "type": "imguizmo", "translation": [ - 0.06876188297610654, - 1.0395, - 0.19568299925847615 + -3.748637, + 0.872255, + 1.935213 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1634, "type": "imguizmo", "translation": [ - 0.07406387175879058, - 1.0395, - 0.20726292947917818 + -3.710491, + 0.933796, + 1.947661 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1635, "type": "imguizmo", "translation": [ - 0.07927358617998946, - 1.0395, - 0.21781028621085674 + -3.669759, + 0.994687, + 1.960813 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1636, "type": "imguizmo", "translation": [ - 0.08438453558426869, - 1.0395, - 0.227272523052823 + -3.62647, + 1.054884, + 1.974631 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1637, "type": "imguizmo", "translation": [ - 0.08939035236510615, - 1.0395, - 0.2356024996170952 + -3.580653, + 1.114346, + 1.989078 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1638, "type": "imguizmo", "translation": [ - 0.09428479989813704, - 1.0395, - 0.24275871637944063 + -3.532341, + 1.173031, + 2.004112 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1639, "type": "imguizmo", "translation": [ - 0.09906178031121114, - 1.0395, - 0.24870552142791796 + -3.481567, + 1.230899, + 2.019693 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1640, "type": "imguizmo", "translation": [ - 0.10371534208158233, - 1.0395, - 0.2534132880789091 + -3.428367, + 1.287909, + 2.035776 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1641, "type": "imguizmo", "translation": [ - 0.1082396874507646, - 1.0395, - 0.25685856247576666 + -3.372778, + 1.344022, + 2.052316 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1642, "type": "imguizmo", "translation": [ - 0.11262917964781731, - 1.0395, - 0.2590241804347479 + -3.314837, + 1.399198, + 2.069269 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1643, "type": "imguizmo", "translation": [ - 0.11687834991205992, - 1.0395, - 0.2598993529561153 + -3.254587, + 1.453399, + 2.086585 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1644, "type": "imguizmo", "translation": [ - 0.12098190430646669, - 1.0395, - 0.25947971997438984 + -3.192069, + 1.506587, + 2.104218 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1645, "type": "imguizmo", "translation": [ - 0.12493473031325295, - 1.0395, - 0.25776737207997824 + -3.127326, + 1.558725, + 2.122118 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1646, "type": "imguizmo", "translation": [ - 0.12873190320343583, - 1.0395, - 0.25477084010395684 + -3.060403, + 1.609776, + 2.140234 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1647, "type": "imguizmo", "translation": [ - 0.1323686921724331, - 1.0395, - 0.25050505261789996 + -2.991348, + 1.659706, + 2.158518 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1648, "type": "imguizmo", "translation": [ - 0.13584056623405685, - 1.0395, - 0.24499126156048748 + -2.920208, + 1.708479, + 2.176917 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1649, "type": "imguizmo", "translation": [ - 0.13914319986555768, - 1.0395, - 0.23825693636141546 + -2.847033, + 1.756061, + 2.19538 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1650, "type": "imguizmo", "translation": [ - 0.14227247839668783, - 1.0395, - 0.23033562709007863 + -2.771874, + 1.80242, + 2.213856 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1651, "type": "imguizmo", "translation": [ - 0.14522450313606755, - 1.0395, - 0.22126679731081303 + -2.694783, + 1.847522, + 2.232294 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1652, "type": "imguizmo", "translation": [ - 0.14799559622846944, - 1.0395, - 0.21109562747740282 + -2.615813, + 1.891337, + 2.250641 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1653, "type": "imguizmo", "translation": [ - 0.1505823052369679, - 1.0395, - 0.1998727898463328 + -2.535021, + 1.933834, + 2.268848 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1654, "type": "imguizmo", "translation": [ - 0.1529814074442455, - 1.0395, - 0.18765419603015496 + -2.452463, + 1.974983, + 2.286862 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1655, "type": "imguizmo", "translation": [ - 0.15518991386769762, - 1.0395, - 0.17450071844864767 + -2.368195, + 2.014756, + 2.304635 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1656, "type": "imguizmo", "translation": [ - 0.15720507298333272, - 1.0395, - 0.16047788706548186 + -2.282276, + 2.053124, + 2.322116 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1657, "type": "imguizmo", "translation": [ - 0.15902437415382825, - 1.0395, - 0.14565556292123882 + -2.194767, + 2.090062, + 2.339256 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1658, "type": "imguizmo", "translation": [ - 0.16064555075647236, - 1.0395, - 0.13010759008921935 + -2.105729, + 2.125543, + 2.356008 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1659, "type": "imguizmo", "translation": [ - 0.16206658300709345, - 1.0395, - 0.11391142778798606 + -2.015223, + 2.159543, + 2.372326 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1660, "type": "imguizmo", "translation": [ - 0.16328570047645988, - 1.0395, - 0.09714776448343171 + -1.923312, + 2.192037, + 2.388163 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1661, "type": "imguizmo", "translation": [ - 0.164301384296014, - 1.0395, - 0.0799001159029019 + -1.830061, + 2.223004, + 2.403476 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1662, "type": "imguizmo", "translation": [ - 0.16511236905019375, - 1.0395, - 0.06225440896404172 + -1.735535, + 2.252422, + 2.418221 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1663, "type": "imguizmo", "translation": [ - 0.16571764435298242, - 1.0395, - 0.04429855369121467 + -1.639799, + 2.28027, + 2.432359 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1664, "type": "imguizmo", "translation": [ - 0.16611645610672401, - 1.0395, - 0.026122005252182785 + -1.542921, + 2.306529, + 2.445849 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1665, "type": "imguizmo", "translation": [ - 0.16630830744163497, - 1.0395, - 0.007815318296961419 + -1.444967, + 2.331181, + 2.458653 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1666, "type": "imguizmo", "translation": [ - 0.1662929593348421, - 1.0395, - -0.01053030418089309 + -1.346006, + 2.354207, + 2.470737 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1667, "type": "imguizmo", "translation": [ - 0.16607043090817525, - 1.0395, - -0.028823465213008756 + -1.246107, + 2.375594, + 2.482066 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1668, "type": "imguizmo", "translation": [ - 0.1656409994043439, - 1.0395, - -0.04697302919129732 + -1.145339, + 2.395324, + 2.492609 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1669, "type": "imguizmo", "translation": [ - 0.16500519984152734, - 1.0395, - -0.06488857590097022 + -1.043774, + 2.413385, + 2.502336 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1670, "type": "imguizmo", "translation": [ - 0.1641638243468086, - 1.0395, - -0.08248085098949896 + -0.941481, + 2.429764, + 2.511221 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1671, "type": "imguizmo", "translation": [ - 0.16311792116928261, - 1.0395, - -0.09966221062731084 + -0.838532, + 2.44445, + 2.519238 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1672, "type": "imguizmo", "translation": [ - 0.1618687933740686, - 1.0395, - -0.11634705814493879 + -0.734998, + 2.457433, + 2.526365 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1673, "type": "imguizmo", "translation": [ - 0.1604179972188531, - 1.0395, - -0.1324522704713235 + -0.630952, + 2.468702, + 2.532583 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1674, "type": "imguizmo", "translation": [ - 0.1587673402149866, - 1.0395, - -0.14789761224877163 + -0.526467, + 2.478252, + 2.537874 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1675, "type": "imguizmo", "translation": [ - 0.15691887887554956, - 1.0395, - -0.16260613556147221 + -0.421614, + 2.486074, + 2.542223 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1676, "type": "imguizmo", "translation": [ - 0.1548749161531933, - 1.0395, - -0.17650456328614236 + -0.316468, + 2.492163, + 2.545618 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1677, "type": "imguizmo", "translation": [ - 0.15263799857094745, - 1.0395, - -0.18952365415497036 + -0.211101, + 2.496516, + 2.54805 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1678, "type": "imguizmo", "translation": [ - 0.15021091304956963, - 1.0395, - -0.20159854771212768 + -0.105587, + 2.499129, + 2.549512 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1679, "type": "imguizmo", "translation": [ - 0.14759668343538926, - 1.0395, - -0.21266908744529797 + 0, + 2.5, + 2.55 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1680, - "type": "imguizmo", - "translation": [ - 0.14479856673297145, - 1.0395, - -0.22268012048239563 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1.0, - 1.0, - 1.0 - ] + "type": "action", + "action": "set_active_render_window", + "value": 0 + }, + { + "frame": 1680, + "type": "action", + "action": "set_active_planar", + "value": 7 + }, + { + "frame": 1680, + "type": "action", + "action": "set_projection_type", + "value": "perspective" + }, + { + "frame": 1680, + "type": "action", + "action": "set_left_handed", + "value": true + }, + { + "frame": 1680, + "type": "action", + "action": "reset_active_camera", + "value": 1 + }, + { + "frame": 1680, + "type": "action", + "action": "set_active_render_window", + "value": 1 + }, + { + "frame": 1680, + "type": "action", + "action": "set_active_planar", + "value": 7 + }, + { + "frame": 1680, + "type": "action", + "action": "set_projection_type", + "value": "orthographic" + }, + { + "frame": 1680, + "type": "action", + "action": "set_left_handed", + "value": true + }, + { + "frame": 1680, + "type": "action", + "action": "set_active_render_window", + "value": 0 }, { "frame": 1681, "type": "imguizmo", "translation": [ - 0.14182004904729473, - 1.0395, - -0.23158177236040042 + 0, + 3.6, + 6.1 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 75, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1682, "type": "imguizmo", "translation": [ - 0.13866484124049866, - 1.0395, - -0.23932969549742264 + 0.126705, + 3.631665, + 6.099686 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0.475144, + 74.993031, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1683, "type": "imguizmo", "translation": [ - 0.1353368743086116, - 1.0395, - -0.2458852901301259 + 0.253322, + 3.663242, + 6.098746 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0.949956, + 74.972128, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1684, "type": "imguizmo", "translation": [ - 0.13184029448401913, - 1.0395, - -0.2512158966158066 + 0.379762, + 3.694643, + 6.097179 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 1.424107, + 74.937307, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1685, "type": "imguizmo", "translation": [ - 0.1281794580697746, - 1.0395, - -0.25529495814109077 + 0.505937, + 3.72578, + 6.094987 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 1.897265, + 74.888591, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1686, "type": "imguizmo", "translation": [ - 0.12435892601218837, - 1.0395, - -0.25810215302664236 + 0.63176, + 3.756566, + 6.092171 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 2.369101, + 74.826014, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1687, "type": "imguizmo", "translation": [ - 0.12038345821845643, - 1.0395, - -0.25962349596874784 + 0.757143, + 3.786916, + 6.088733 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 2.839285, + 74.74962, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1688, "type": "imguizmo", "translation": [ - 0.11625800762640887, - 1.0395, - -0.2598514077133955 + 0.881998, + 3.816745, + 6.084676 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 3.307491, + 74.659462, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1689, "type": "imguizmo", "translation": [ - 0.11198771403376584, - 1.0395, - -0.25878475281573776 + 1.006238, + 3.84597, + 6.080002 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 3.773392, + 74.555603, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1690, "type": "imguizmo", "translation": [ - 0.10757789769458996, - 1.0395, - -0.25642884529682053 + 1.129777, + 3.874509, + 6.074715 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 4.236663, + 74.438116, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1691, "type": "imguizmo", "translation": [ - 0.10303405269091205, - 1.0395, - -0.25279542216939704 + 1.252529, + 3.902283, + 6.068819 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 4.696982, + 74.307082, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1692, "type": "imguizmo", "translation": [ - 0.09836184008778923, - 1.0395, - -0.2479025849647216 + 1.374407, + 3.929215, + 6.062317 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 5.154027, + 74.162592, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1693, "type": "imguizmo", "translation": [ - 0.09356708088032245, - 1.0395, - -0.24177470955163066 + 1.495328, + 3.955229, + 6.055214 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 5.607481, + 74.004748, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1694, "type": "imguizmo", "translation": [ - 0.08865574874142189, - 1.0395, - -0.2344423246971907 + 1.615207, + 3.980253, + 6.047515 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 6.057026, + 73.833659, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1695, "type": "imguizmo", "translation": [ - 0.08363396257935415, - 1.0395, - -0.22594195997391642 + 1.73396, + 4.004217, + 6.039225 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 6.50235, + 73.649445, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1696, "type": "imguizmo", "translation": [ - 0.07850797891434448, - 1.0395, - -0.21631596377127793 + 1.851505, + 4.027055, + 6.03035 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 6.943142, + 73.452233, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1697, "type": "imguizmo", "translation": [ - 0.07328418408373165, - 1.0395, - -0.20561229231815425 + 1.967759, + 4.048702, + 6.020897 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 7.379096, + 73.242162, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1698, "type": "imguizmo", "translation": [ - 0.067969086285387, - 1.0395, - -0.193884270767314 + 2.082642, + 4.069099, + 6.010872 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 7.809907, + 73.019377, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1699, "type": "imguizmo", "translation": [ - 0.06256930746931033, - 1.0395, - -0.1811903275321859 + 2.196073, + 4.088188, + 6.000282 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 8.235276, + 72.784035, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1700, "type": "imguizmo", "translation": [ - 0.057091575087504606, - 1.0395, - -0.16759370319944272 + 2.307975, + 4.105917, + 5.989133 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 8.654905, + 72.536298, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1701, "type": "imguizmo", "translation": [ - 0.05154271371240866, - 1.0395, - -0.15316213546757926 + 2.418267, + 4.122235, + 5.977435 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 9.068502, + 72.276341, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1702, "type": "imguizmo", "translation": [ - 0.045929636534329694, - 1.0395, - -0.1379675216811057 + 2.526874, + 4.137098, + 5.965195 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 9.475779, + 72.004343, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1703, "type": "imguizmo", "translation": [ - 0.04025933674846901, - 1.0395, - -0.12208556064159394 + 2.633721, + 4.150464, + 5.952422 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 9.876452, + 71.720494, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1704, "type": "imguizmo", "translation": [ - 0.03453887884227171, - 1.0395, - -0.10559537548005621 + 2.738731, + 4.162296, + 5.939125 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 10.270243, + 71.424993, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1705, "type": "imguizmo", "translation": [ - 0.0287753897939547, - 1.0395, - -0.08857911946948532 + 2.841833, + 4.17256, + 5.925312 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 10.656876, + 71.118045, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1706, "type": "imguizmo", "translation": [ - 0.022976050193179807, - 1.0395, - -0.07112156674138248 + 2.942955, + 4.181229, + 5.910994 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 11.036081, + 70.799864, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1707, "type": "imguizmo", "translation": [ - 0.017148085294932898, - 1.0395, - -0.05330968994529752 + 3.042026, + 4.188277, + 5.89618 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 11.407596, + 70.470671, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1708, "type": "imguizmo", "translation": [ - 0.011298756017756002, - 1.0395, - -0.035232226955464185 + 3.138976, + 4.193686, + 5.880881 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 11.771161, + 70.130697, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1709, "type": "imguizmo", "translation": [ - 0.005435349897546697, - 1.0395, - -0.016979238783173305 + 3.233739, + 4.197441, + 5.865108 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 12.126522, + 69.780178, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1710, "type": "imguizmo", "translation": [ - -0.00043482799180366745, - 1.0395, - 0.0013583391026536842 + 3.326248, + 4.19953, + 5.848871 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 12.473431, + 69.419359, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1711, "type": "imguizmo", "translation": [ - -0.006304464139622748, - 1.0395, - 0.01968914981139778 + 3.41644, + 4.199948, + 5.832182 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 12.811648, + 69.04849, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1712, "type": "imguizmo", "translation": [ - -0.012166245710180674, - 1.0395, - 0.03792187016617581 + 3.50425, + 4.198694, + 5.815052 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 13.140937, + 68.667831, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1713, "type": "imguizmo", "translation": [ - -0.018012869653572766, - 1.0395, - 0.05596566567131397 + 3.589618, + 4.195771, + 5.797494 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 13.461067, + 68.277647, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1714, "type": "imguizmo", "translation": [ - -0.02383705180441031, - 1.0395, - 0.07373064304523608 + 3.672484, + 4.191188, + 5.779519 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 13.771816, + 67.878209, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1715, "type": "imguizmo", "translation": [ - -0.02963153595698339, - 1.0395, - 0.09112829806427036 + 3.752791, + 4.184957, + 5.761141 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 14.072967, + 67.469796, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1716, "type": "imguizmo", "translation": [ - -0.03538910290559035, - 1.0395, - 0.10807195648624157 + 3.830483, + 4.177095, + 5.742371 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 14.36431, + 67.052693, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1717, "type": "imguizmo", "translation": [ - -0.04110257943876802, - 1.0395, - 0.12447720585718276 + 3.905505, + 4.167626, + 5.723224 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 14.645643, + 66.62719, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1718, "type": "imguizmo", "translation": [ - -0.0467648472762208, - 1.0395, - 0.14026231604993705 + 3.977805, + 4.156574, + 5.703711 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 14.916769, + 66.193583, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1719, "type": "imguizmo", "translation": [ - -0.052368851937311243, - 1.0395, - 0.15534864643953938 + 4.047333, + 4.14397, + 5.683848 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 15.177499, + 65.752176, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1720, "type": "imguizmo", "translation": [ - -0.057907611530064414, - 1.0395, - 0.16966103768685414 + 4.11404, + 4.129851, + 5.663647 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 15.427652, + 65.303276, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1721, "type": "imguizmo", "translation": [ - -0.063374225449736, - 1.0395, - 0.18312818617862237 + 4.177881, + 4.114255, + 5.643124 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 15.667052, + 64.847195, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1722, "type": "imguizmo", "translation": [ - -0.06876188297610651, - 1.0395, - 0.1956829992584762 + 4.238809, + 4.097226, + 5.622291 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 15.895535, + 64.384251, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1723, "type": "imguizmo", "translation": [ - -0.07406387175879049, - 1.0395, - 0.207262929479178 + 4.296784, + 4.07881, + 5.601165 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 16.112939, + 63.914767, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1724, "type": "imguizmo", "translation": [ - -0.07927358617998938, - 1.0395, - 0.21781028621085663 + 4.351764, + 4.059061, + 5.579758 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 16.319114, + 63.43907, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1725, "type": "imguizmo", "translation": [ - -0.08438453558426863, - 1.0395, - 0.22727252305282283 + 4.403711, + 4.038031, + 5.558087 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 16.513916, + 62.957493, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1726, "type": "imguizmo", "translation": [ - -0.0893903523651061, - 1.0395, - 0.23560249961709503 + 4.452589, + 4.015781, + 5.536167 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 16.69721, + 62.470369, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1727, "type": "imguizmo", "translation": [ - -0.09428479989813698, - 1.0395, - 0.24275871637944055 + 4.498364, + 3.992372, + 5.514012 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 16.868866, + 61.978039, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1728, "type": "imguizmo", "translation": [ - -0.09906178031121114, - 1.0395, - 0.24870552142791794 + 4.541004, + 3.967869, + 5.491638 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 17.028767, + 61.480846, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1729, "type": "imguizmo", "translation": [ - -0.10371534208158233, - 1.0395, - 0.25341328807890906 + 4.58048, + 3.942341, + 5.469061 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 17.1768, + 60.979136, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1730, "type": "imguizmo", "translation": [ - -0.1082396874507646, - 1.0395, - 0.2568585624757666 + 4.616763, + 3.915859, + 5.446297 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 17.312862, + 60.47326, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1731, "type": "imguizmo", "translation": [ - -0.11262917964781721, - 1.0395, - 0.25902418043474795 + 4.649829, + 3.888497, + 5.423361 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 17.436858, + 59.963569, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1732, "type": "imguizmo", "translation": [ - -0.11687834991205986, - 1.0395, - 0.2598993529561153 + 4.679654, + 3.86033, + 5.400269 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 17.548702, + 59.450419, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1733, "type": "imguizmo", "translation": [ - -0.12098190430646662, - 1.0395, - 0.2594797199743899 + 4.706218, + 3.831438, + 5.377038 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 17.648317, + 58.934167, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1734, "type": "imguizmo", "translation": [ - -0.1249347303132529, - 1.0395, - 0.25776737207997824 + 4.729502, + 3.801901, + 5.353683 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 17.735632, + 58.415173, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1735, "type": "imguizmo", "translation": [ - -0.12873190320343583, - 1.0395, - 0.25477084010395684 + 4.74949, + 3.771801, + 5.330221 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 17.810587, + 57.8938, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1736, "type": "imguizmo", "translation": [ - -0.1323686921724332, - 1.0395, - 0.2505050526178999 + 4.766168, + 3.741222, + 5.306668 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 17.873129, + 57.37041, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1737, "type": "imguizmo", "translation": [ - -0.1358405662340569, - 1.0395, - 0.24499126156048742 + 4.779524, + 3.71025, + 5.283042 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 17.923215, + 56.845367, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1738, "type": "imguizmo", "translation": [ - -0.13914319986555773, - 1.0395, - 0.23825693636141534 + 4.789549, + 3.67897, + 5.259357 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 17.96081, + 56.319039, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1739, "type": "imguizmo", "translation": [ - -0.14227247839668777, - 1.0395, - 0.23033562709007874 + 4.796237, + 3.64747, + 5.235631 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 17.985888, + 55.791791, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1740, "type": "imguizmo", "translation": [ - -0.14522450313606755, - 1.0395, - 0.22126679731081308 + 4.799582, + 3.615838, + 5.21188 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 17.998432, + 55.263992, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1741, "type": "imguizmo", "translation": [ - -0.14799559622846944, - 1.0395, - 0.21109562747740285 + 4.799582, + 3.584162, + 5.18812 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 17.998432, + 54.736008, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1742, "type": "imguizmo", "translation": [ - -0.1505823052369679, - 1.0395, - 0.1998727898463328 + 4.796237, + 3.55253, + 5.164369 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 17.985888, + 54.208209, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1743, "type": "imguizmo", "translation": [ - -0.1529814074442455, - 1.0395, - 0.18765419603015498 + 4.789549, + 3.52103, + 5.140643 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 17.96081, + 53.680961, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1744, "type": "imguizmo", "translation": [ - -0.15518991386769762, - 1.0395, - 0.1745007184486476 + 4.779524, + 3.48975, + 5.116958 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 17.923215, + 53.154633, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1745, "type": "imguizmo", "translation": [ - -0.15720507298333275, - 1.0395, - 0.16047788706548174 + 4.766168, + 3.458778, + 5.093332 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 17.873129, + 52.62959, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1746, "type": "imguizmo", "translation": [ - -0.15902437415382822, - 1.0395, - 0.1456555629212386 + 4.74949, + 3.428199, + 5.069779 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 17.810587, + 52.1062, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1747, "type": "imguizmo", "translation": [ - -0.1606455507564723, - 1.0395, - 0.13010759008921954 + 4.729502, + 3.398099, + 5.046317 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 17.735632, + 51.584827, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1748, "type": "imguizmo", "translation": [ - -0.1620665830070934, - 1.0395, - 0.11391142778798613 + 4.706218, + 3.368562, + 5.022962 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 17.648317, + 51.065833, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1749, "type": "imguizmo", "translation": [ - -0.1632857004764598, - 1.0395, - 0.0971477644834318 + 4.679654, + 3.33967, + 4.999731 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 17.548702, + 50.549581, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1750, "type": "imguizmo", "translation": [ - -0.16430138429601396, - 1.0395, - 0.07990011590290198 + 4.649829, + 3.311503, + 4.976639 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 17.436858, + 50.036431, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1751, "type": "imguizmo", "translation": [ - -0.16511236905019372, - 1.0395, - 0.06225440896404179 + 4.616763, + 3.284141, + 4.953703 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 17.312862, + 49.52674, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1752, "type": "imguizmo", "translation": [ - -0.16571764435298242, - 1.0395, - 0.04429855369121462 + 4.58048, + 3.257659, + 4.930939 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 17.1768, + 49.020864, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1753, "type": "imguizmo", "translation": [ - -0.16611645610672401, - 1.0395, - 0.026122005252182733 + 4.541004, + 3.232131, + 4.908362 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 17.028767, + 48.519154, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1754, "type": "imguizmo", "translation": [ - -0.16630830744163497, - 1.0395, - 0.007815318296961249 + 4.498364, + 3.207628, + 4.885988 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 16.868866, + 48.021961, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1755, "type": "imguizmo", "translation": [ - -0.16629295933484212, - 1.0395, - -0.01053030418089275 + 4.452589, + 3.184219, + 4.863833 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 16.69721, + 47.529631, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1756, "type": "imguizmo", "translation": [ - -0.16607043090817525, - 1.0395, - -0.028823465213008465 + 4.403711, + 3.161969, + 4.841913 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 16.513916, + 47.042507, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1757, "type": "imguizmo", "translation": [ - -0.1656409994043439, - 1.0395, - -0.04697302919129727 + 4.351764, + 3.140939, + 4.820242 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 16.319114, + 46.56093, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1758, "type": "imguizmo", "translation": [ - -0.16500519984152737, - 1.0395, - -0.06488857590097014 + 4.296784, + 3.12119, + 4.798835 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 16.112939, + 46.085233, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1759, "type": "imguizmo", "translation": [ - -0.1641638243468086, - 1.0395, - -0.08248085098949888 + 4.238809, + 3.102774, + 4.777709 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 15.895535, + 45.615749, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1760, "type": "imguizmo", "translation": [ - -0.16311792116928261, - 1.0395, - -0.09966221062731076 + 4.177881, + 3.085745, + 4.756876 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 15.667052, + 45.152805, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1761, "type": "imguizmo", "translation": [ - -0.1618687933740686, - 1.0395, - -0.11634705814493881 + 4.11404, + 3.070149, + 4.736353 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 15.427652, + 44.696724, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1762, "type": "imguizmo", "translation": [ - -0.1604179972188531, - 1.0395, - -0.13245227047132352 + 4.047333, + 3.05603, + 4.716152 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 15.177499, + 44.247824, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1763, "type": "imguizmo", "translation": [ - -0.15876734021498665, - 1.0395, - -0.14789761224877124 + 3.977805, + 3.043426, + 4.696289 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 14.916769, + 43.806417, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1764, "type": "imguizmo", "translation": [ - -0.15691887887554964, - 1.0395, - -0.16260613556147185 + 3.905505, + 3.032374, + 4.676776 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 14.645643, + 43.37281, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1765, "type": "imguizmo", "translation": [ - -0.15487491615319332, - 1.0395, - -0.17650456328614222 + 3.830483, + 3.022905, + 4.657629 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 14.36431, + 42.947307, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1766, "type": "imguizmo", "translation": [ - -0.15263799857094748, - 1.0395, - -0.18952365415497022 + 3.752791, + 3.015043, + 4.638859 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 14.072967, + 42.530204, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1767, "type": "imguizmo", "translation": [ - -0.15021091304956966, - 1.0395, - -0.20159854771212746 + 3.672484, + 3.008812, + 4.620481 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 13.771816, + 42.121791, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1768, "type": "imguizmo", "translation": [ - -0.14759668343538931, - 1.0395, - -0.21266908744529775 + 3.589618, + 3.004229, + 4.602506 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 13.461067, + 41.722353, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1769, "type": "imguizmo", "translation": [ - -0.14479856673297145, - 1.0395, - -0.22268012048239563 + 3.50425, + 3.001306, + 4.584948 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 13.140937, + 41.332169, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1770, "type": "imguizmo", "translation": [ - -0.14182004904729473, - 1.0395, - -0.2315817723604004 + 3.41644, + 3.000052, + 4.567818 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 12.811648, + 40.95151, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1771, "type": "imguizmo", "translation": [ - -0.13866484124049874, - 1.0395, - -0.23932969549742236 + 3.326248, + 3.00047, + 4.551129 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 12.473431, + 40.580641, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1772, "type": "imguizmo", "translation": [ - -0.1353368743086117, - 1.0395, - -0.24588529013012572 + 3.233739, + 3.002559, + 4.534892 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 12.126522, + 40.219822, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1773, "type": "imguizmo", "translation": [ - -0.13184029448401913, - 1.0395, - -0.2512158966158065 + 3.138976, + 3.006314, + 4.519119 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 11.771161, + 39.869303, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1774, "type": "imguizmo", "translation": [ - -0.12817945806977463, - 1.0395, - -0.2552949581410907 + 3.042026, + 3.011723, + 4.50382 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 11.407596, + 39.529329, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1775, "type": "imguizmo", "translation": [ - -0.12435892601218838, - 1.0395, - -0.25810215302664236 + 2.942955, + 3.018771, + 4.489006 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 11.036081, + 39.200136, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1776, "type": "imguizmo", "translation": [ - -0.12038345821845645, - 1.0395, - -0.25962349596874784 + 2.841833, + 3.02744, + 4.474688 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 10.656876, + 38.881955, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1777, "type": "imguizmo", "translation": [ - -0.11625800762640885, - 1.0395, - -0.2598514077133955 + 2.738731, + 3.037704, + 4.460875 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 10.270243, + 38.575007, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1778, "type": "imguizmo", "translation": [ - -0.11198771403376581, - 1.0395, - -0.25878475281573776 + 2.633721, + 3.049536, + 4.447578 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 9.876452, + 38.279506, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1779, "type": "imguizmo", "translation": [ - -0.10757789769458992, - 1.0395, - -0.25642884529682053 + 2.526874, + 3.062902, + 4.434805 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 9.475779, + 37.995657, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1780, "type": "imguizmo", "translation": [ - -0.10303405269091202, - 1.0395, - -0.25279542216939704 + 2.418267, + 3.077765, + 4.422565 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 9.068502, + 37.723659, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1781, "type": "imguizmo", "translation": [ - -0.09836184008778914, - 1.0395, - -0.2479025849647216 + 2.307975, + 3.094083, + 4.410867 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 8.654905, + 37.463702, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1782, "type": "imguizmo", "translation": [ - -0.09356708088032237, - 1.0395, - -0.2417747095516306 + 2.196073, + 3.111812, + 4.399718 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 8.235276, + 37.215965, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1783, "type": "imguizmo", "translation": [ - -0.0886557487414218, - 1.0395, - -0.2344423246971906 + 2.082642, + 3.130901, + 4.389128 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 7.809907, + 36.980623, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1784, "type": "imguizmo", "translation": [ - -0.08363396257935407, - 1.0395, - -0.22594195997391628 + 1.967759, + 3.151298, + 4.379103 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 7.379096, + 36.757838, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1785, "type": "imguizmo", "translation": [ - -0.07850797891434433, - 1.0395, - -0.21631596377127768 + 1.851505, + 3.172945, + 4.36965 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 6.943142, + 36.547767, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1786, "type": "imguizmo", "translation": [ - -0.07328418408373147, - 1.0395, - -0.205612292318154 + 1.73396, + 3.195783, + 4.360775 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 6.50235, + 36.350555, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1787, "type": "imguizmo", "translation": [ - -0.06796908628538698, - 1.0395, - -0.193884270767314 + 1.615207, + 3.219747, + 4.352485 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 6.057026, + 36.166341, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1788, "type": "imguizmo", "translation": [ - -0.06256930746931029, - 1.0395, - -0.18119032753218592 + 1.495328, + 3.244771, + 4.344786 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 5.607481, + 35.995252, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1789, "type": "imguizmo", "translation": [ - -0.05709157508750457, - 1.0395, - -0.16759370319944278 + 1.374407, + 3.270785, + 4.337683 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 5.154027, + 35.837408, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1790, "type": "imguizmo", "translation": [ - -0.051542713712408615, - 1.0395, - -0.1531621354675793 + 1.252529, + 3.297717, + 4.331181 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 4.696982, + 35.692918, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1791, "type": "imguizmo", "translation": [ - -0.04592963653432965, - 1.0395, - -0.13796752168110576 + 1.129777, + 3.325491, + 4.325285 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 4.236663, + 35.561884, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1792, "type": "imguizmo", "translation": [ - -0.04025933674846897, - 1.0395, - -0.12208556064159401 + 1.006238, + 3.35403, + 4.319998 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 3.773392, + 35.444397, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1793, "type": "imguizmo", "translation": [ - -0.03453887884227159, - 1.0395, - -0.1055953754800561 + 0.881998, + 3.383255, + 4.315324 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 3.307491, + 35.340538, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1794, "type": "imguizmo", "translation": [ - -0.028775389793954588, - 1.0395, - -0.08857911946948518 + 0.757143, + 3.413084, + 4.311267 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 2.839285, + 35.25038, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1795, "type": "imguizmo", "translation": [ - -0.022976050193179855, - 1.0395, - -0.07112156674138281 + 0.63176, + 3.443434, + 4.307829 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 2.369101, + 35.173986, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1796, "type": "imguizmo", "translation": [ - -0.017148085294932922, - 1.0395, - -0.05330968994529783 + 0.505937, + 3.47422, + 4.305013 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 1.897265, + 35.111409, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1797, "type": "imguizmo", "translation": [ - -0.011298756017755951, - 1.0395, - -0.035232226955464255 + 0.379762, + 3.505357, + 4.302821 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 1.424107, + 35.062693, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1798, "type": "imguizmo", "translation": [ - -0.005435349897546653, - 1.0395, - -0.01697923878317339 + 0.253322, + 3.536758, + 4.301254 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0.949956, + 35.027872, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1799, "type": "imguizmo", "translation": [ - 0.0004348279918037117, - 1.0395, - 0.001358339102653594 + 0.126705, + 3.568335, + 4.300314 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0.475144, + 35.006969, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1800, - "type": "action", - "action": "set_active_render_window", - "value": 0 - }, - { - "frame": 1800, - "type": "action", - "action": "set_active_planar", - "value": 10 - }, - { - "frame": 1800, - "type": "action", - "action": "set_projection_type", - "value": "perspective" + "type": "imguizmo", + "translation": [ + 0, + 3.6, + 4.3 + ], + "rotation_deg": [ + 0, + 35, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] }, { "frame": 1801, "type": "imguizmo", "translation": [ - 0.0, - 0.0792, - 0.5940000000000001 + -0.126705, + 3.631665, + 4.300314 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -0.475144, + 35.006969, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1802, "type": "imguizmo", "translation": [ - 0.002113083459655698, - 0.07916270147834842, - 0.5940000000000001 + -0.253322, + 3.663242, + 4.301254 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -0.949956, + 35.027872, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1803, "type": "imguizmo", "translation": [ - 0.004054487603677721, - 0.07905383626443915, - 0.5940000000000001 + -0.379762, + 3.694643, + 4.302821 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -1.424107, + 35.062693, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1804, "type": "imguizmo", "translation": [ - 0.006004364095168197, - 0.078876285161966, - 0.5940000000000001 + -0.505937, + 3.72578, + 4.305013 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -1.897265, + 35.111409, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1805, "type": "imguizmo", "translation": [ - 0.007945677997951922, - 0.0786300497637978, - 0.5940000000000001 + -0.63176, + 3.756566, + 4.307829 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -2.369101, + 35.173986, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1806, "type": "imguizmo", "translation": [ - 0.009877179127275969, - 0.07831545441767407, - 0.5940000000000001 + -0.757143, + 3.786916, + 4.311267 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -2.839285, + 35.25038, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1807, "type": "imguizmo", "translation": [ - 0.011796367597200971, - 0.07793288966469379, - 0.5940000000000001 + -0.881998, + 3.816745, + 4.315324 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -3.307491, + 35.340538, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1808, "type": "imguizmo", "translation": [ - 0.013700859815962379, - 0.07748283224534291, - 0.5940000000000001 + -1.006238, + 3.84597, + 4.319998 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -3.773392, + 35.444397, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1809, "type": "imguizmo", "translation": [ - 0.015588282425163793, - 0.07696584286612763, - 0.5940000000000001 + -1.129777, + 3.874509, + 4.325285 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -4.236663, + 35.561884, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1810, "type": "imguizmo", "translation": [ - 0.017456283979123223, - 0.07638256563215652, - 0.5940000000000001 + -1.252529, + 3.902283, + 4.331181 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -4.696982, + 35.692918, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1811, "type": "imguizmo", "translation": [ - 0.019302537176686597, - 0.07573372723417356, - 0.5940000000000001 + -1.374407, + 3.929215, + 4.337683 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -5.154027, + 35.837408, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1812, "type": "imguizmo", "translation": [ - 0.02112474181657881, - 0.07502013604403128, - 0.5940000000000001 + -1.495328, + 3.955229, + 4.344786 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -5.607481, + 35.995252, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1813, "type": "imguizmo", "translation": [ - 0.02292062765870218, - 0.07424268110749309, - 0.5940000000000001 + -1.615207, + 3.980253, + 4.352485 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -6.057026, + 36.166341, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1814, "type": "imguizmo", "translation": [ - 0.02468795725293019, - 0.07340233103659814, - 0.5940000000000001 + -1.73396, + 4.004217, + 4.360775 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -6.50235, + 36.350555, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1815, "type": "imguizmo", "translation": [ - 0.026424528726663156, - 0.07250013280289105, - 0.5940000000000001 + -1.851505, + 4.027055, + 4.36965 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -6.943142, + 36.547767, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1816, "type": "imguizmo", "translation": [ - 0.028128178528089562, - 0.07153721043302574, - 0.5940000000000001 + -1.967759, + 4.048702, + 4.379103 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -7.379096, + 36.757838, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1817, "type": "imguizmo", "translation": [ - 0.029796784121702048, - 0.07051476360836817, - 0.5940000000000001 + -2.082642, + 4.069099, + 4.389128 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -7.809907, + 36.980623, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1818, "type": "imguizmo", "translation": [ - 0.031428266632712344, - 0.06943406617034271, - 0.5940000000000001 + -2.196073, + 4.088188, + 4.399718 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -8.235276, + 37.215965, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1819, "type": "imguizmo", "translation": [ - 0.03302059343707038, - 0.06829646453338442, - 0.5940000000000001 + -2.307975, + 4.105917, + 4.410867 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -8.654905, + 37.463702, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1820, "type": "imguizmo", "translation": [ - 0.03457178069386077, - 0.06710337600747432, - 0.5940000000000001 + -2.418267, + 4.122235, + 4.422565 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -9.068502, + 37.723659, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1821, "type": "imguizmo", "translation": [ - 0.036079895816921524, - 0.06585628703234757, - 0.5940000000000001 + -2.526874, + 4.137098, + 4.434805 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -9.475779, + 37.995657, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1822, "type": "imguizmo", "translation": [ - 0.03754305988260577, - 0.06455675132557472, - 0.5940000000000001 + -2.633721, + 4.150464, + 4.447578 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -9.876452, + 38.279506, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1823, "type": "imguizmo", "translation": [ - 0.03895944997068664, - 0.06320638794682307, - 0.5940000000000001 + -2.738731, + 4.162296, + 4.460875 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -10.270243, + 38.575007, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1824, "type": "imguizmo", "translation": [ - 0.0403273014354889, - 0.06180687928071, - 0.5940000000000001 + -2.841833, + 4.17256, + 4.474688 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -10.656876, + 38.881955, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1825, "type": "imguizmo", "translation": [ - 0.041644910104417655, - 0.0603599689407611, - 0.5940000000000001 + -2.942955, + 4.181229, + 4.489006 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -11.036081, + 39.200136, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1826, "type": "imguizmo", "translation": [ - 0.04291063440114529, - 0.05886745959708494, - 0.5940000000000001 + -3.042026, + 4.188277, + 4.50382 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -11.407596, + 39.529329, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1827, "type": "imguizmo", "translation": [ - 0.044122897390811044, - 0.05733121073047059, - 0.5940000000000001 + -3.138976, + 4.193686, + 4.519119 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -11.771161, + 39.869303, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1828, "type": "imguizmo", "translation": [ - 0.04528018874468562, - 0.05575313631570605, - 0.5940000000000001 + -3.233739, + 4.197441, + 4.534892 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -12.126522, + 40.219822, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1829, "type": "imguizmo", "translation": [ - 0.04638106662185256, - 0.05413520243700407, - 0.5940000000000001 + -3.326248, + 4.19953, + 4.551129 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -12.473431, + 40.580641, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1830, "type": "imguizmo", "translation": [ - 0.04742415946556261, - 0.052479424838505956, - 0.5940000000000001 + -3.41644, + 4.199948, + 4.567818 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -12.811648, + 40.95151, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1831, "type": "imguizmo", "translation": [ - 0.048408167712022526, - 0.05078786641291548, - 0.5940000000000001 + -3.50425, + 4.198694, + 4.584948 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -13.140937, + 41.332169, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1832, "type": "imguizmo", "translation": [ - 0.04933186540948983, - 0.049062634631391364, - 0.5940000000000001 + -3.589618, + 4.195771, + 4.602506 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -13.461067, + 41.722353, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1833, "type": "imguizmo", "translation": [ - 0.050194101745655965, - 0.047305878917900716, - 0.5940000000000001 + -3.672484, + 4.191188, + 4.620481 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -13.771816, + 42.121791, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1834, "type": "imguizmo", "translation": [ - 0.05099380248141516, - 0.045519787971304355, - 0.5940000000000001 + -3.752791, + 4.184957, + 4.638859 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -14.072967, + 42.530204, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1835, "type": "imguizmo", "translation": [ - 0.05172997128923255, - 0.04370658703851066, - 0.5940000000000001 + -3.830483, + 4.177095, + 4.657629 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -14.36431, + 42.947307, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1836, "type": "imguizmo", "translation": [ - 0.05240169099444424, - 0.04186853514209489, - 0.5940000000000001 + -3.905505, + 4.167626, + 4.676776 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -14.645643, + 43.37281, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1837, "type": "imguizmo", "translation": [ - 0.05300812471794276, - 0.04000792226583853, - 0.5940000000000001 + -3.977805, + 4.156574, + 4.696289 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -14.916769, + 43.806417, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1838, "type": "imguizmo", "translation": [ - 0.05354851691882413, - 0.03812706650169449, - 0.5940000000000001 + -4.047333, + 4.14397, + 4.716152 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -15.177499, + 44.247824, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1839, "type": "imguizmo", "translation": [ - 0.05402219433569782, - 0.03622831116173331, - 0.5940000000000001 + -4.11404, + 4.129851, + 4.736353 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -15.427652, + 44.696724, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1840, "type": "imguizmo", "translation": [ - 0.05442856682548663, - 0.03431402185866804, - 0.5940000000000001 + -4.177881, + 4.114255, + 4.756876 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -15.667052, + 45.152805, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1841, "type": "imguizmo", "translation": [ - 0.05476712809867136, - 0.03238658355859542, - 0.5940000000000001 + -4.238809, + 4.097226, + 4.777709 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -15.895535, + 45.615749, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1842, "type": "imguizmo", "translation": [ - 0.055037456350064594, - 0.030448397609625116, - 0.5940000000000001 + -4.296784, + 4.07881, + 4.798835 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -16.112939, + 46.085233, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1843, "type": "imguizmo", "translation": [ - 0.05523921478432749, - 0.02850187875009896, - 0.5940000000000001 + -4.351764, + 4.059061, + 4.820242 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -16.319114, + 46.56093, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1844, "type": "imguizmo", "translation": [ - 0.0553721520355747, - 0.026549452100127668, - 0.5940000000000001 + -4.403711, + 4.038031, + 4.841913 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -16.513916, + 47.042507, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1845, "type": "imguizmo", "translation": [ - 0.05543610248054502, - 0.024593550140193245, - 0.5940000000000001 + -4.452589, + 4.015781, + 4.863833 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -16.69721, + 47.529631, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1846, "type": "imguizmo", "translation": [ - 0.05543098644494739, - 0.022636609680581087, - 0.5940000000000001 + -4.498364, + 3.992372, + 4.885988 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -16.868866, + 48.021961, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1847, "type": "imguizmo", "translation": [ - 0.0553568103027251, - 0.020681068825417915, - 0.5940000000000001 + -4.541004, + 3.967869, + 4.908362 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -17.028767, + 48.519154, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1848, "type": "imguizmo", "translation": [ - 0.05521366646811466, - 0.018729363935097613, - 0.5940000000000001 + -4.58048, + 3.942341, + 4.930939 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -17.1768, + 49.020864, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1849, "type": "imguizmo", "translation": [ - 0.055001733280509134, - 0.01678392659087944, - 0.5940000000000001 + -4.616763, + 3.915859, + 4.953703 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -17.312862, + 49.52674, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1850, "type": "imguizmo", "translation": [ - 0.054721274782269555, - 0.014847180565440777, - 0.5940000000000001 + -4.649829, + 3.888497, + 4.976639 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -17.436858, + 50.036431, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1851, "type": "imguizmo", "translation": [ - 0.05437264038976089, - 0.012921538803158033, - 0.5940000000000001 + -4.679654, + 3.86033, + 4.999731 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -17.548702, + 50.549581, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1852, "type": "imguizmo", "translation": [ - 0.05395626445802288, - 0.011009400413878496, - 0.5940000000000001 + -4.706218, + 3.831438, + 5.022962 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -17.648317, + 51.065833, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1853, "type": "imguizmo", "translation": [ - 0.05347266573961772, - 0.009113147683928144, - 0.5940000000000001 + -4.729502, + 3.801901, + 5.046317 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -17.735632, + 51.584827, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1854, "type": "imguizmo", "translation": [ - 0.052922446738328875, - 0.007235143108079623, - 0.5940000000000001 + -4.74949, + 3.771801, + 5.069779 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -17.810587, + 52.1062, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1855, "type": "imguizmo", "translation": [ - 0.052306292958516534, - 0.0053777264461778735, - 0.5940000000000001 + -4.766168, + 3.741222, + 5.093332 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -17.873129, + 52.62959, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1856, "type": "imguizmo", "translation": [ - 0.05162497205106445, - 0.003543211808090775, - 0.5940000000000001 + -4.779524, + 3.71025, + 5.116958 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -17.923215, + 53.154633, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1857, "type": "imguizmo", "translation": [ - 0.05087933285698249, - 0.0017338847706163177, - 0.5940000000000001 + -4.789549, + 3.67897, + 5.140643 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -17.96081, + 53.680961, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1858, "type": "imguizmo", "translation": [ - 0.05007030434985655, - -4.800047006135017e-05, - 0.5940000000000001 + -4.796237, + 3.64747, + 5.164369 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -17.985888, + 54.208209, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1859, "type": "imguizmo", "translation": [ - 0.049198894478463095, - -0.0018002239068197257, - 0.5940000000000001 + -4.799582, + 3.615838, + 5.18812 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -17.998432, + 54.736008, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1860, "type": "imguizmo", "translation": [ - 0.04826618891099049, - -0.0035206024874503846, - 0.5940000000000001 + -4.799582, + 3.584162, + 5.21188 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -17.998432, + 55.263992, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1861, "type": "imguizmo", "translation": [ - 0.047273349682431576, - -0.0052069928344702626, - 0.5940000000000001 + -4.796237, + 3.55253, + 5.235631 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -17.985888, + 55.791791, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1862, "type": "imguizmo", "translation": [ - 0.04622161374683289, - -0.006857293915502945, - 0.5940000000000001 + -4.789549, + 3.52103, + 5.259357 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -17.96081, + 56.319039, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1863, "type": "imguizmo", "translation": [ - 0.04511229143620387, - -0.00846944966090349, - 0.5940000000000001 + -4.779524, + 3.48975, + 5.283042 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -17.923215, + 56.845367, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1864, "type": "imguizmo", "translation": [ - 0.04394676482800637, - -0.010041451525365102, - 0.5940000000000001 + -4.766168, + 3.458778, + 5.306668 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -17.873129, + 57.37041, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1865, "type": "imguizmo", "translation": [ - 0.042726486023258195, - -0.011571340990316627, - 0.5940000000000001 + -4.74949, + 3.428199, + 5.330221 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -17.810587, + 57.8938, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1866, "type": "imguizmo", "translation": [ - 0.04145297533739612, - -0.013057212003992567, - 0.5940000000000001 + -4.729502, + 3.398099, + 5.353683 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -17.735632, + 58.415173, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1867, "type": "imguizmo", "translation": [ - 0.04012781940615214, - -0.014497213356136438, - 0.5940000000000001 + -4.706218, + 3.368562, + 5.377038 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -17.648317, + 58.934167, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1868, "type": "imguizmo", "translation": [ - 0.03875266920880295, - -0.015889550984378116, - 0.5940000000000001 + -4.679654, + 3.33967, + 5.400269 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -17.548702, + 59.450419, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1869, "type": "imguizmo", "translation": [ - 0.03732923801125527, - -0.01723249020941225, - 0.5940000000000001 + -4.649829, + 3.311503, + 5.423361 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -17.436858, + 59.963569, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1870, "type": "imguizmo", "translation": [ - 0.03585929923152997, - -0.018524357896192518, - 0.5940000000000001 + -4.616763, + 3.284141, + 5.446297 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -17.312862, + 60.47326, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1871, "type": "imguizmo", "translation": [ - 0.034344684230304005, - -0.019763544538449664, - 0.5940000000000001 + -4.58048, + 3.257659, + 5.469061 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -17.1768, + 60.979136, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1872, "type": "imguizmo", "translation": [ - 0.03278728002926307, - -0.020948506263935844, - 0.5940000000000001 + -4.541004, + 3.232131, + 5.491638 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -17.028767, + 61.480846, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1873, "type": "imguizmo", "translation": [ - 0.03118902696010748, - -0.022077766757897282, - 0.5940000000000001 + -4.498364, + 3.207628, + 5.514012 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -16.868866, + 61.978039, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1874, "type": "imguizmo", "translation": [ - 0.029551916247140632, - -0.02314991910237844, - 0.5940000000000001 + -4.452589, + 3.184219, + 5.536167 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -16.69721, + 62.470369, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1875, "type": "imguizmo", "translation": [ - 0.027877987526451382, - -0.024163627529066763, - 0.5940000000000001 + -4.403711, + 3.161969, + 5.558087 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -16.513916, + 62.957493, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1876, "type": "imguizmo", "translation": [ - 0.026169326304781488, - -0.025117629083493558, - 0.5940000000000001 + -4.351764, + 3.140939, + 5.579758 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -16.319114, + 63.43907, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1877, "type": "imguizmo", "translation": [ - 0.024428061361243876, - -0.026010735198518105, - 0.5940000000000001 + -4.296784, + 3.12119, + 5.601165 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -16.112939, + 63.914767, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1878, "type": "imguizmo", "translation": [ - 0.022656362095128994, - -0.026841833175134142, - 0.5940000000000001 + -4.238809, + 3.102774, + 5.622291 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -15.895535, + 64.384251, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1879, "type": "imguizmo", "translation": [ - 0.020856435823103436, - -0.027609887568754436, - 0.5940000000000001 + -4.177881, + 3.085745, + 5.643124 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -15.667052, + 64.847195, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1880, "type": "imguizmo", "translation": [ - 0.01903052502916819, - -0.02831394147924571, - 0.5940000000000001 + -4.11404, + 3.070149, + 5.663647 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -15.427652, + 65.303276, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1881, "type": "imguizmo", "translation": [ - 0.017180904570802874, - -0.028953117743107066, - 0.5940000000000001 + -4.047333, + 3.05603, + 5.683848 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -15.177499, + 65.752176, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1882, "type": "imguizmo", "translation": [ - 0.015309878844776557, - -0.02952662002630648, - 0.5940000000000001 + -3.977805, + 3.043426, + 5.703711 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -14.916769, + 66.193583, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1883, "type": "imguizmo", "translation": [ - 0.013419778916156328, - -0.030033733816413877, - 0.5940000000000001 + -3.905505, + 3.032374, + 5.723224 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -14.645643, + 66.62719, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1884, "type": "imguizmo", "translation": [ - 0.011512959614090561, - -0.030473827312794587, - 0.5940000000000001 + -3.830483, + 3.022905, + 5.742371 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -14.36431, + 67.052693, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1885, "type": "imguizmo", "translation": [ - 0.00959179659798489, - -0.030846352213754303, - 0.5940000000000001 + -3.752791, + 3.015043, + 5.761141 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -14.072967, + 67.469796, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1886, "type": "imguizmo", "translation": [ - 0.007658683397726593, - -0.031150844399654706, - 0.5940000000000001 + -3.672484, + 3.008812, + 5.779519 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -13.771816, + 67.878209, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1887, "type": "imguizmo", "translation": [ - 0.00571602843164429, - -0.031386924511148745, - 0.5940000000000001 + -3.589618, + 3.004229, + 5.797494 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -13.461067, + 68.277647, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1888, "type": "imguizmo", "translation": [ - 0.003766252005918659, - -0.03155429842181511, - 0.5940000000000001 + -3.50425, + 3.001306, + 5.815052 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -13.140937, + 68.667831, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1889, "type": "imguizmo", "translation": [ - 0.001811783299182224, - -0.03165275760460319, - 0.5940000000000001 + -3.41644, + 3.000052, + 5.832182 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -12.811648, + 69.04849, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1890, "type": "imguizmo", "translation": [ - -0.0001449426639345639, - -0.031682179391631746, - 0.5940000000000001 + -3.326248, + 3.00047, + 5.848871 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -12.473431, + 69.419359, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1891, "type": "imguizmo", "translation": [ - -0.0021014880465409238, - -0.031642527127017865, - 0.5940000000000001 + -3.233739, + 3.002559, + 5.865108 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -12.126522, + 69.780178, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1892, "type": "imguizmo", "translation": [ - -0.0040554152367269, - -0.03153385021254559, - 0.5940000000000001 + -3.138976, + 3.006314, + 5.880881 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -11.771161, + 70.130697, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1893, "type": "imguizmo", "translation": [ - -0.006004289884524264, - -0.03135628404611747, - 0.5940000000000001 + -3.042026, + 3.011723, + 5.89618 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -11.407596, + 70.470671, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1894, "type": "imguizmo", "translation": [ - -0.007945683934803445, - -0.031110049853065657, - 0.5940000000000001 + -2.942955, + 3.018771, + 5.910994 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -11.036081, + 70.799864, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1895, "type": "imguizmo", "translation": [ - -0.009877178652327803, - -0.03079545441053262, - 0.5940000000000001 + -2.841833, + 3.02744, + 5.925312 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -10.656876, + 71.118045, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1896, "type": "imguizmo", "translation": [ - -0.01179636763519679, - -0.030412889665265102, - 0.5940000000000001 + -2.738731, + 3.037704, + 5.939125 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -10.270243, + 71.424993, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1897, "type": "imguizmo", "translation": [ - -0.013700859812922682, - -0.029962832245297204, - 0.5940000000000001 + -2.633721, + 3.049536, + 5.952422 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -9.876452, + 71.720494, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1898, "type": "imguizmo", "translation": [ - -0.01558828242540694, - -0.029445842866131274, - 0.5940000000000001 + -2.526874, + 3.062902, + 5.965195 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -9.475779, + 72.004343, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1899, "type": "imguizmo", "translation": [ - -0.017456283979103752, - -0.028862565632156233, - 0.5940000000000001 + -2.418267, + 3.077765, + 5.977435 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -9.068502, + 72.276341, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1900, "type": "imguizmo", "translation": [ - -0.019302537176688145, - -0.028213727234173577, - 0.5940000000000001 + -2.307975, + 3.094083, + 5.989133 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -8.654905, + 72.536298, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1901, "type": "imguizmo", "translation": [ - -0.02112474181657868, - -0.02750013604403128, - 0.5940000000000001 + -2.196073, + 3.111812, + 6.000282 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -8.235276, + 72.784035, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1902, "type": "imguizmo", "translation": [ - -0.022920627658702182, - -0.02672268110749309, - 0.5940000000000001 + -2.082642, + 3.130901, + 6.010872 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -7.809907, + 73.019377, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1903, "type": "imguizmo", "translation": [ - -0.024687957252930173, - -0.02588233103659816, - 0.5940000000000001 + -1.967759, + 3.151298, + 6.020897 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -7.379096, + 73.242162, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1904, "type": "imguizmo", "translation": [ - -0.02642452872666314, - -0.024980132802891065, - 0.5940000000000001 + -1.851505, + 3.172945, + 6.03035 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -6.943142, + 73.452233, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1905, "type": "imguizmo", "translation": [ - -0.028128178528089555, - -0.02401721043302576, - 0.5940000000000001 + -1.73396, + 3.195783, + 6.039225 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -6.50235, + 73.649445, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1906, "type": "imguizmo", "translation": [ - -0.029796784121702048, - -0.02299476360836817, - 0.5940000000000001 + -1.615207, + 3.219747, + 6.047515 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -6.057026, + 73.833659, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1907, "type": "imguizmo", "translation": [ - -0.03142826663271235, - -0.0219140661703427, - 0.5940000000000001 + -1.495328, + 3.244771, + 6.055214 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -5.607481, + 74.004748, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1908, "type": "imguizmo", "translation": [ - -0.033020593437070395, - -0.020776464533384414, - 0.5940000000000001 + -1.374407, + 3.270785, + 6.062317 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -5.154027, + 74.162592, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1909, "type": "imguizmo", "translation": [ - -0.034571780693860785, - -0.019583376007474285, - 0.5940000000000001 + -1.252529, + 3.297717, + 6.068819 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -4.696982, + 74.307082, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1910, "type": "imguizmo", "translation": [ - -0.03607989581692154, - -0.018336287032347527, - 0.5940000000000001 + -1.129777, + 3.325491, + 6.074715 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -4.236663, + 74.438116, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1911, "type": "imguizmo", "translation": [ - -0.03754305988260574, - -0.017036751325574702, - 0.5940000000000001 + -1.006238, + 3.35403, + 6.080002 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -3.773392, + 74.555603, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1912, "type": "imguizmo", "translation": [ - -0.03895944997068662, - -0.015686387946823085, - 0.5940000000000001 + -0.881998, + 3.383255, + 6.084676 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -3.307491, + 74.659462, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1913, "type": "imguizmo", "translation": [ - -0.04032730143548888, - -0.014286879280709996, - 0.5940000000000001 + -0.757143, + 3.413084, + 6.088733 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -2.839285, + 74.74962, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1914, "type": "imguizmo", "translation": [ - -0.041644910104417635, - -0.012839968940761089, - 0.5940000000000001 + -0.63176, + 3.443434, + 6.092171 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -2.369101, + 74.826014, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1915, "type": "imguizmo", "translation": [ - -0.04291063440114529, - -0.011347459597084913, - 0.5940000000000001 + -0.505937, + 3.47422, + 6.094987 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -1.897265, + 74.888591, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1916, "type": "imguizmo", "translation": [ - -0.044122897390811065, - -0.009811210730470568, - 0.5940000000000001 + -0.379762, + 3.505357, + 6.097179 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -1.424107, + 74.937307, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1917, "type": "imguizmo", "translation": [ - -0.04528018874468563, - -0.008233136315706035, - 0.5940000000000001 + -0.253322, + 3.536758, + 6.098746 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -0.949956, + 74.972128, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1918, "type": "imguizmo", "translation": [ - -0.046381066621852575, - -0.006615202437004037, - 0.5940000000000001 + -0.126705, + 3.568335, + 6.099686 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + -0.475144, + 74.993031, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1919, "type": "imguizmo", "translation": [ - -0.0474241594655626, - -0.0049594248385059665, - 0.5940000000000001 + 0, + 3.6, + 6.1 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 75, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1920, - "type": "imguizmo", - "translation": [ - -0.04840816771202252, - -0.0032678664129154864, - 0.5940000000000001 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1.0, - 1.0, - 1.0 - ] + "type": "action", + "action": "set_active_render_window", + "value": 0 + }, + { + "frame": 1920, + "type": "action", + "action": "set_active_planar", + "value": 8 + }, + { + "frame": 1920, + "type": "action", + "action": "set_projection_type", + "value": "perspective" + }, + { + "frame": 1920, + "type": "action", + "action": "set_left_handed", + "value": true + }, + { + "frame": 1920, + "type": "action", + "action": "reset_active_camera", + "value": 1 + }, + { + "frame": 1920, + "type": "action", + "action": "set_active_render_window", + "value": 1 + }, + { + "frame": 1920, + "type": "action", + "action": "set_active_planar", + "value": 8 + }, + { + "frame": 1920, + "type": "action", + "action": "set_projection_type", + "value": "orthographic" + }, + { + "frame": 1920, + "type": "action", + "action": "set_left_handed", + "value": true + }, + { + "frame": 1920, + "type": "action", + "action": "set_active_render_window", + "value": 0 }, { "frame": 1921, "type": "imguizmo", "translation": [ - -0.049331865409489827, - -0.0015426346313913643, - 0.5940000000000001 + 0, + 5.2, + 4.6 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0, + 64, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1922, "type": "imguizmo", "translation": [ - -0.05019410174565596, - 0.00021412108209928945, - 0.5940000000000001 + 0.131984, + 5.198188, + 4.64222 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0.42235, + 63.995122, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1923, "type": "imguizmo", "translation": [ - -0.05099380248141517, - 0.0020002120286956512, - 0.5940000000000001 + 0.263877, + 5.192753, + 4.684323 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 0.844406, + 63.98049, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1924, "type": "imguizmo", "translation": [ - -0.051729971289232554, - 0.003813412961489367, - 0.5940000000000001 + 0.395585, + 5.1837, + 4.72619 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 1.265873, + 63.956115, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1925, "type": "imguizmo", "translation": [ - -0.05240169099444425, - 0.005651464857905129, - 0.5940000000000001 + 0.527018, + 5.171034, + 4.767706 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 1.686458, + 63.922013, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1926, "type": "imguizmo", "translation": [ - -0.05300812471794276, - 0.0075120777341615005, - 0.5940000000000001 + 0.658083, + 5.154764, + 4.808755 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 2.105867, + 63.87821, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1927, "type": "imguizmo", "translation": [ - -0.053548516918824125, - 0.00939293349830549, - 0.5940000000000001 + 0.78869, + 5.134901, + 4.849221 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 2.523809, + 63.824734, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1928, "type": "imguizmo", "translation": [ - -0.05402219433569782, - 0.011291688838266686, - 0.5940000000000001 + 0.918748, + 5.11146, + 4.888993 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 2.939992, + 63.761623, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1929, "type": "imguizmo", "translation": [ - -0.05442856682548662, - 0.01320597814133196, - 0.5940000000000001 + 1.048165, + 5.084457, + 4.92796 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 3.354126, + 63.688922, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1930, "type": "imguizmo", "translation": [ - -0.054767128098671354, - 0.015133416441404573, - 0.5940000000000001 + 1.176851, + 5.05391, + 4.966012 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 3.765923, + 63.606681, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1931, "type": "imguizmo", "translation": [ - -0.055037456350064594, - 0.017071602390374883, - 0.5940000000000001 + 1.304717, + 5.019841, + 5.003045 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 4.175095, + 63.514957, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1932, "type": "imguizmo", "translation": [ - -0.0552392147843275, - 0.01901812124990105, - 0.5940000000000001 + 1.431674, + 4.982274, + 5.038953 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 4.581358, + 63.413815, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1933, "type": "imguizmo", "translation": [ - -0.0553721520355747, - 0.020970547899872342, - 0.5940000000000001 + 1.557633, + 4.941234, + 5.073639 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 4.984427, + 63.303324, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1934, "type": "imguizmo", "translation": [ - -0.05543610248054502, - 0.022926449859806776, - 0.5940000000000001 + 1.682507, + 4.896751, + 5.107004 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 5.384023, + 63.183561, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1935, "type": "imguizmo", "translation": [ - -0.05543098644494739, - 0.024883390319418882, - 0.5940000000000001 + 1.806208, + 4.848856, + 5.138957 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 5.779867, + 63.054611, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1936, "type": "imguizmo", "translation": [ - -0.0553568103027251, - 0.026838931174582057, - 0.5940000000000001 + 1.928651, + 4.797581, + 5.169407 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 6.171682, + 62.916563, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1937, "type": "imguizmo", "translation": [ - -0.05521366646811466, - 0.028790636064902387, - 0.5940000000000001 + 2.049749, + 4.742962, + 5.19827 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 6.559197, + 62.769513, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1938, "type": "imguizmo", "translation": [ - -0.05500173328050915, - 0.030736073409120556, - 0.5940000000000001 + 2.169419, + 4.685038, + 5.225465 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 6.94214, + 62.613564, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1939, "type": "imguizmo", "translation": [ - -0.05472127478226956, - 0.03267281943455922, - 0.5940000000000001 + 2.287577, + 4.623849, + 5.250917 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 7.320245, + 62.448824, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1940, "type": "imguizmo", "translation": [ - -0.0543726403897609, - 0.03459846119684196, - 0.5940000000000001 + 2.40414, + 4.559438, + 5.274556 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 7.693248, + 62.275409, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1941, "type": "imguizmo", "translation": [ - -0.05395626445802289, - 0.03651059958612152, - 0.5940000000000001 + 2.519028, + 4.491849, + 5.296313 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 8.06089, + 62.093438, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1942, "type": "imguizmo", "translation": [ - -0.053472665739617724, - 0.03840685231607186, - 0.5940000000000001 + 2.632161, + 4.421129, + 5.316131 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 8.422915, + 61.90304, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1943, "type": "imguizmo", "translation": [ - -0.05292244673832889, - 0.04028485689192034, - 0.5940000000000001 + 2.743459, + 4.347328, + 5.333952 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 8.779069, + 61.704346, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1944, "type": "imguizmo", "translation": [ - -0.05230629295851656, - 0.04214227355382209, - 0.5940000000000001 + 2.852845, + 4.270498, + 5.349727 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 9.129105, + 61.497495, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1945, "type": "imguizmo", "translation": [ - -0.05162497205106446, - 0.04397678819190922, - 0.5940000000000001 + 2.960243, + 4.190692, + 5.363413 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 9.472778, + 61.282631, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1946, "type": "imguizmo", "translation": [ - -0.050879332856982505, - 0.045786115229383674, - 0.5940000000000001 + 3.065578, + 4.107965, + 5.374971 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 9.80985, + 61.059904, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1947, "type": "imguizmo", "translation": [ - -0.050070304349856556, - 0.04756800047006132, - 0.5940000000000001 + 3.168777, + 4.022375, + 5.38437 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 10.140085, + 60.82947, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1948, "type": "imguizmo", "translation": [ - -0.04919889447846311, - 0.049320223906819706, - 0.5940000000000001 + 3.269767, + 3.933981, + 5.391582 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 10.463254, + 60.591488, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1949, "type": "imguizmo", "translation": [ - -0.0482661889109905, - 0.05104060248745038, - 0.5940000000000001 + 3.368478, + 3.842846, + 5.396587 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 10.77913, + 60.346125, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1950, "type": "imguizmo", "translation": [ - -0.04727334968243158, - 0.05272699283447027, - 0.5940000000000001 + 3.464842, + 3.749033, + 5.399373 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 11.087495, + 60.093551, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1951, "type": "imguizmo", "translation": [ - -0.046221613746832924, - 0.0543772939155029, - 0.5940000000000001 + 3.558791, + 3.652608, + 5.39993 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 11.388132, + 59.833943, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1952, "type": "imguizmo", "translation": [ - -0.045112291436203905, - 0.05598944966090345, - 0.5940000000000001 + 3.65026, + 3.553636, + 5.398258 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 11.680833, + 59.567482, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1953, "type": "imguizmo", "translation": [ - -0.0439467648280064, - 0.05756145152536509, - 0.5940000000000001 + 3.739185, + 3.452188, + 5.394361 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 11.965393, + 59.294353, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1954, "type": "imguizmo", "translation": [ - -0.042726486023258216, - 0.05909134099031661, - 0.5940000000000001 + 3.825504, + 3.348334, + 5.38825 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 12.241614, + 59.014746, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1955, "type": "imguizmo", "translation": [ - -0.04145297533739614, - 0.060577212003992555, - 0.5940000000000001 + 3.909157, + 3.242147, + 5.379942 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 12.509304, + 58.728857, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1956, "type": "imguizmo", "translation": [ - -0.04012781940615216, - 0.06201721335613643, - 0.5940000000000001 + 3.990086, + 3.1337, + 5.369461 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 12.768276, + 58.436885, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1957, "type": "imguizmo", "translation": [ - -0.03875266920880297, - 0.06340955098437813, - 0.5940000000000001 + 4.068234, + 3.023069, + 5.356834 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 13.018349, + 58.139033, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1958, "type": "imguizmo", "translation": [ - -0.03732923801125529, - 0.06475249020941225, - 0.5940000000000001 + 4.143547, + 2.910332, + 5.342098 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 13.25935, + 57.835508, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1959, "type": "imguizmo", "translation": [ - -0.03585929923152999, - 0.06604435789619251, - 0.5940000000000001 + 4.215972, + 2.795566, + 5.325294 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 13.49111, + 57.526523, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1960, "type": "imguizmo", "translation": [ - -0.03434468423030403, - 0.06728354453844967, - 0.5940000000000001 + 4.285459, + 2.678852, + 5.306468 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 13.713468, + 57.212293, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1961, "type": "imguizmo", "translation": [ - -0.03278728002926307, - 0.06846850626393589, - 0.5940000000000001 + 4.351959, + 2.560271, + 5.285673 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 13.926269, + 56.893036, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1962, "type": "imguizmo", "translation": [ - -0.03118902696010748, - 0.06959776675789732, - 0.5940000000000001 + 4.415426, + 2.439905, + 5.262967 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 14.129364, + 56.568976, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1963, "type": "imguizmo", "translation": [ - -0.029551916247140632, - 0.07066991910237846, - 0.5940000000000001 + 4.475816, + 2.317839, + 5.238414 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 14.322613, + 56.240337, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1964, "type": "imguizmo", "translation": [ - -0.027877987526451382, - 0.07168362752906678, - 0.5940000000000001 + 4.533087, + 2.194158, + 5.212081 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 14.505879, + 55.907349, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1965, "type": "imguizmo", "translation": [ - -0.026169326304781467, - 0.07263762908349358, - 0.5940000000000001 + 4.587199, + 2.068948, + 5.184042 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 14.679037, + 55.570245, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1966, "type": "imguizmo", "translation": [ - -0.024428061361243845, - 0.07353073519851812, - 0.5940000000000001 + 4.638114, + 1.942296, + 5.154375 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 14.841964, + 55.229258, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1967, "type": "imguizmo", "translation": [ - -0.02265636209512902, - 0.07436183317513415, - 0.5940000000000001 + 4.685796, + 1.81429, + 5.123163 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 14.994548, + 54.884627, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { "frame": 1968, "type": "imguizmo", "translation": [ - -0.020856435823103456, - 0.07512988756875444, - 0.5940000000000001 + 4.730213, + 1.68502, + 5.090493 ], "rotation_deg": [ - 0.0, - 0.0, - 0.0 + 15.136681, + 54.536592, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 1969, + "type": "imguizmo", + "translation": [ + 4.771333, + 1.554575, + 5.056455 + ], + "rotation_deg": [ + 15.268266, + 54.185395, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 1970, + "type": "imguizmo", + "translation": [ + 4.809128, + 1.423048, + 5.021146 + ], + "rotation_deg": [ + 15.38921, + 53.831282, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 1971, + "type": "imguizmo", + "translation": [ + 4.843572, + 1.290528, + 4.984662 + ], + "rotation_deg": [ + 15.499429, + 53.474498, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 1972, + "type": "imguizmo", + "translation": [ + 4.87464, + 1.157109, + 4.947107 + ], + "rotation_deg": [ + 15.598847, + 53.115293, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 1973, + "type": "imguizmo", + "translation": [ + 4.90231, + 1.022883, + 4.908584 + ], + "rotation_deg": [ + 15.687393, + 52.753917, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 1974, + "type": "imguizmo", + "translation": [ + 4.926564, + 0.887945, + 4.869201 + ], + "rotation_deg": [ + 15.765006, + 52.390621, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 1975, + "type": "imguizmo", + "translation": [ + 4.947385, + 0.752388, + 4.829068 + ], + "rotation_deg": [ + 15.831633, + 52.02566, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 1976, + "type": "imguizmo", + "translation": [ + 4.964758, + 0.616306, + 4.788296 + ], + "rotation_deg": [ + 15.887226, + 51.659287, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 1977, + "type": "imguizmo", + "translation": [ + 4.978671, + 0.479795, + 4.747 + ], + "rotation_deg": [ + 15.931747, + 51.291757, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 1978, + "type": "imguizmo", + "translation": [ + 4.989114, + 0.34295, + 4.705293 + ], + "rotation_deg": [ + 15.965165, + 50.923327, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 1979, + "type": "imguizmo", + "translation": [ + 4.99608, + 0.205866, + 4.663294 + ], + "rotation_deg": [ + 15.987456, + 50.554254, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 1980, + "type": "imguizmo", + "translation": [ + 4.999564, + 0.068638, + 4.621117 + ], + "rotation_deg": [ + 15.998606, + 50.184794, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 1981, + "type": "imguizmo", + "translation": [ + 4.999564, + -0.068638, + 4.578883 + ], + "rotation_deg": [ + 15.998606, + 49.815206, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 1982, + "type": "imguizmo", + "translation": [ + 4.99608, + -0.205866, + 4.536706 + ], + "rotation_deg": [ + 15.987456, + 49.445746, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 1983, + "type": "imguizmo", + "translation": [ + 4.989114, + -0.34295, + 4.494707 + ], + "rotation_deg": [ + 15.965165, + 49.076673, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 1984, + "type": "imguizmo", + "translation": [ + 4.978671, + -0.479795, + 4.453 + ], + "rotation_deg": [ + 15.931747, + 48.708243, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 1985, + "type": "imguizmo", + "translation": [ + 4.964758, + -0.616306, + 4.411704 + ], + "rotation_deg": [ + 15.887226, + 48.340713, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 1986, + "type": "imguizmo", + "translation": [ + 4.947385, + -0.752388, + 4.370932 + ], + "rotation_deg": [ + 15.831633, + 47.97434, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 1987, + "type": "imguizmo", + "translation": [ + 4.926564, + -0.887945, + 4.330799 + ], + "rotation_deg": [ + 15.765006, + 47.609379, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 1988, + "type": "imguizmo", + "translation": [ + 4.90231, + -1.022883, + 4.291416 + ], + "rotation_deg": [ + 15.687393, + 47.246083, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 1989, + "type": "imguizmo", + "translation": [ + 4.87464, + -1.157109, + 4.252893 + ], + "rotation_deg": [ + 15.598847, + 46.884707, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 1990, + "type": "imguizmo", + "translation": [ + 4.843572, + -1.290528, + 4.215338 + ], + "rotation_deg": [ + 15.499429, + 46.525502, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 1991, + "type": "imguizmo", + "translation": [ + 4.809128, + -1.423048, + 4.178854 + ], + "rotation_deg": [ + 15.38921, + 46.168718, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 1992, + "type": "imguizmo", + "translation": [ + 4.771333, + -1.554575, + 4.143545 + ], + "rotation_deg": [ + 15.268266, + 45.814605, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 1993, + "type": "imguizmo", + "translation": [ + 4.730213, + -1.68502, + 4.109507 + ], + "rotation_deg": [ + 15.136681, + 45.463408, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 1994, + "type": "imguizmo", + "translation": [ + 4.685796, + -1.81429, + 4.076837 + ], + "rotation_deg": [ + 14.994548, + 45.115373, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 1995, + "type": "imguizmo", + "translation": [ + 4.638114, + -1.942296, + 4.045625 + ], + "rotation_deg": [ + 14.841964, + 44.770742, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 1996, + "type": "imguizmo", + "translation": [ + 4.587199, + -2.068948, + 4.015958 + ], + "rotation_deg": [ + 14.679037, + 44.429755, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 1997, + "type": "imguizmo", + "translation": [ + 4.533087, + -2.194158, + 3.987919 + ], + "rotation_deg": [ + 14.505879, + 44.092651, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 1998, + "type": "imguizmo", + "translation": [ + 4.475816, + -2.317839, + 3.961586 + ], + "rotation_deg": [ + 14.322613, + 43.759663, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 1999, + "type": "imguizmo", + "translation": [ + 4.415426, + -2.439905, + 3.937033 + ], + "rotation_deg": [ + 14.129364, + 43.431024, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2000, + "type": "imguizmo", + "translation": [ + 4.351959, + -2.560271, + 3.914327 + ], + "rotation_deg": [ + 13.926269, + 43.106964, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2001, + "type": "imguizmo", + "translation": [ + 4.285459, + -2.678852, + 3.893532 + ], + "rotation_deg": [ + 13.713468, + 42.787707, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2002, + "type": "imguizmo", + "translation": [ + 4.215972, + -2.795566, + 3.874706 + ], + "rotation_deg": [ + 13.49111, + 42.473477, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2003, + "type": "imguizmo", + "translation": [ + 4.143547, + -2.910332, + 3.857902 + ], + "rotation_deg": [ + 13.25935, + 42.164492, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2004, + "type": "imguizmo", + "translation": [ + 4.068234, + -3.023069, + 3.843166 + ], + "rotation_deg": [ + 13.018349, + 41.860967, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2005, + "type": "imguizmo", + "translation": [ + 3.990086, + -3.1337, + 3.830539 + ], + "rotation_deg": [ + 12.768276, + 41.563115, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2006, + "type": "imguizmo", + "translation": [ + 3.909157, + -3.242147, + 3.820058 + ], + "rotation_deg": [ + 12.509304, + 41.271143, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2007, + "type": "imguizmo", + "translation": [ + 3.825504, + -3.348334, + 3.81175 + ], + "rotation_deg": [ + 12.241614, + 40.985254, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2008, + "type": "imguizmo", + "translation": [ + 3.739185, + -3.452188, + 3.805639 + ], + "rotation_deg": [ + 11.965393, + 40.705647, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2009, + "type": "imguizmo", + "translation": [ + 3.65026, + -3.553636, + 3.801742 + ], + "rotation_deg": [ + 11.680833, + 40.432518, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2010, + "type": "imguizmo", + "translation": [ + 3.558791, + -3.652608, + 3.80007 + ], + "rotation_deg": [ + 11.388132, + 40.166057, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2011, + "type": "imguizmo", + "translation": [ + 3.464842, + -3.749033, + 3.800627 + ], + "rotation_deg": [ + 11.087495, + 39.906449, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2012, + "type": "imguizmo", + "translation": [ + 3.368478, + -3.842846, + 3.803413 + ], + "rotation_deg": [ + 10.77913, + 39.653875, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2013, + "type": "imguizmo", + "translation": [ + 3.269767, + -3.933981, + 3.808418 + ], + "rotation_deg": [ + 10.463254, + 39.408512, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2014, + "type": "imguizmo", + "translation": [ + 3.168777, + -4.022375, + 3.81563 + ], + "rotation_deg": [ + 10.140085, + 39.17053, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2015, + "type": "imguizmo", + "translation": [ + 3.065578, + -4.107965, + 3.825029 + ], + "rotation_deg": [ + 9.80985, + 38.940096, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2016, + "type": "imguizmo", + "translation": [ + 2.960243, + -4.190692, + 3.836587 + ], + "rotation_deg": [ + 9.472778, + 38.717369, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2017, + "type": "imguizmo", + "translation": [ + 2.852845, + -4.270498, + 3.850273 + ], + "rotation_deg": [ + 9.129105, + 38.502505, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2018, + "type": "imguizmo", + "translation": [ + 2.743459, + -4.347328, + 3.866048 + ], + "rotation_deg": [ + 8.779069, + 38.295654, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2019, + "type": "imguizmo", + "translation": [ + 2.632161, + -4.421129, + 3.883869 + ], + "rotation_deg": [ + 8.422915, + 38.09696, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2020, + "type": "imguizmo", + "translation": [ + 2.519028, + -4.491849, + 3.903687 + ], + "rotation_deg": [ + 8.06089, + 37.906562, + 0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 1, + 1, + 1 ] }, { - "frame": 1969, - "type": "imguizmo", - "translation": [ - -0.019030525029168216, - 0.07583394147924571, - 0.5940000000000001 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1.0, - 1.0, - 1.0 - ] + "frame": 2021, + "type": "imguizmo", + "translation": [ + 2.40414, + -4.559438, + 3.925444 + ], + "rotation_deg": [ + 7.693248, + 37.724591, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2022, + "type": "imguizmo", + "translation": [ + 2.287577, + -4.623849, + 3.949083 + ], + "rotation_deg": [ + 7.320245, + 37.551176, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2023, + "type": "imguizmo", + "translation": [ + 2.169419, + -4.685038, + 3.974535 + ], + "rotation_deg": [ + 6.94214, + 37.386436, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2024, + "type": "imguizmo", + "translation": [ + 2.049749, + -4.742962, + 4.00173 + ], + "rotation_deg": [ + 6.559197, + 37.230487, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2025, + "type": "imguizmo", + "translation": [ + 1.928651, + -4.797581, + 4.030593 + ], + "rotation_deg": [ + 6.171682, + 37.083437, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2026, + "type": "imguizmo", + "translation": [ + 1.806208, + -4.848856, + 4.061043 + ], + "rotation_deg": [ + 5.779867, + 36.945389, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2027, + "type": "imguizmo", + "translation": [ + 1.682507, + -4.896751, + 4.092996 + ], + "rotation_deg": [ + 5.384023, + 36.816439, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2028, + "type": "imguizmo", + "translation": [ + 1.557633, + -4.941234, + 4.126361 + ], + "rotation_deg": [ + 4.984427, + 36.696676, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2029, + "type": "imguizmo", + "translation": [ + 1.431674, + -4.982274, + 4.161047 + ], + "rotation_deg": [ + 4.581358, + 36.586185, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2030, + "type": "imguizmo", + "translation": [ + 1.304717, + -5.019841, + 4.196955 + ], + "rotation_deg": [ + 4.175095, + 36.485043, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2031, + "type": "imguizmo", + "translation": [ + 1.176851, + -5.05391, + 4.233988 + ], + "rotation_deg": [ + 3.765923, + 36.393319, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2032, + "type": "imguizmo", + "translation": [ + 1.048165, + -5.084457, + 4.27204 + ], + "rotation_deg": [ + 3.354126, + 36.311078, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2033, + "type": "imguizmo", + "translation": [ + 0.918748, + -5.11146, + 4.311007 + ], + "rotation_deg": [ + 2.939992, + 36.238377, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2034, + "type": "imguizmo", + "translation": [ + 0.78869, + -5.134901, + 4.350779 + ], + "rotation_deg": [ + 2.523809, + 36.175266, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2035, + "type": "imguizmo", + "translation": [ + 0.658083, + -5.154764, + 4.391245 + ], + "rotation_deg": [ + 2.105867, + 36.12179, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2036, + "type": "imguizmo", + "translation": [ + 0.527018, + -5.171034, + 4.432294 + ], + "rotation_deg": [ + 1.686458, + 36.077987, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2037, + "type": "imguizmo", + "translation": [ + 0.395585, + -5.1837, + 4.47381 + ], + "rotation_deg": [ + 1.265873, + 36.043885, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2038, + "type": "imguizmo", + "translation": [ + 0.263877, + -5.192753, + 4.515677 + ], + "rotation_deg": [ + 0.844406, + 36.01951, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2039, + "type": "imguizmo", + "translation": [ + 0.131984, + -5.198188, + 4.55778 + ], + "rotation_deg": [ + 0.42235, + 36.004878, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2040, + "type": "imguizmo", + "translation": [ + 0, + -5.2, + 4.6 + ], + "rotation_deg": [ + 0, + 36, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2041, + "type": "imguizmo", + "translation": [ + -0.131984, + -5.198188, + 4.64222 + ], + "rotation_deg": [ + -0.42235, + 36.004878, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2042, + "type": "imguizmo", + "translation": [ + -0.263877, + -5.192753, + 4.684323 + ], + "rotation_deg": [ + -0.844406, + 36.01951, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2043, + "type": "imguizmo", + "translation": [ + -0.395585, + -5.1837, + 4.72619 + ], + "rotation_deg": [ + -1.265873, + 36.043885, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2044, + "type": "imguizmo", + "translation": [ + -0.527018, + -5.171034, + 4.767706 + ], + "rotation_deg": [ + -1.686458, + 36.077987, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2045, + "type": "imguizmo", + "translation": [ + -0.658083, + -5.154764, + 4.808755 + ], + "rotation_deg": [ + -2.105867, + 36.12179, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2046, + "type": "imguizmo", + "translation": [ + -0.78869, + -5.134901, + 4.849221 + ], + "rotation_deg": [ + -2.523809, + 36.175266, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2047, + "type": "imguizmo", + "translation": [ + -0.918748, + -5.11146, + 4.888993 + ], + "rotation_deg": [ + -2.939992, + 36.238377, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2048, + "type": "imguizmo", + "translation": [ + -1.048165, + -5.084457, + 4.92796 + ], + "rotation_deg": [ + -3.354126, + 36.311078, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2049, + "type": "imguizmo", + "translation": [ + -1.176851, + -5.05391, + 4.966012 + ], + "rotation_deg": [ + -3.765923, + 36.393319, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2050, + "type": "imguizmo", + "translation": [ + -1.304717, + -5.019841, + 5.003045 + ], + "rotation_deg": [ + -4.175095, + 36.485043, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2051, + "type": "imguizmo", + "translation": [ + -1.431674, + -4.982274, + 5.038953 + ], + "rotation_deg": [ + -4.581358, + 36.586185, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2052, + "type": "imguizmo", + "translation": [ + -1.557633, + -4.941234, + 5.073639 + ], + "rotation_deg": [ + -4.984427, + 36.696676, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2053, + "type": "imguizmo", + "translation": [ + -1.682507, + -4.896751, + 5.107004 + ], + "rotation_deg": [ + -5.384023, + 36.816439, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2054, + "type": "imguizmo", + "translation": [ + -1.806208, + -4.848856, + 5.138957 + ], + "rotation_deg": [ + -5.779867, + 36.945389, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2055, + "type": "imguizmo", + "translation": [ + -1.928651, + -4.797581, + 5.169407 + ], + "rotation_deg": [ + -6.171682, + 37.083437, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2056, + "type": "imguizmo", + "translation": [ + -2.049749, + -4.742962, + 5.19827 + ], + "rotation_deg": [ + -6.559197, + 37.230487, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2057, + "type": "imguizmo", + "translation": [ + -2.169419, + -4.685038, + 5.225465 + ], + "rotation_deg": [ + -6.94214, + 37.386436, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2058, + "type": "imguizmo", + "translation": [ + -2.287577, + -4.623849, + 5.250917 + ], + "rotation_deg": [ + -7.320245, + 37.551176, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2059, + "type": "imguizmo", + "translation": [ + -2.40414, + -4.559438, + 5.274556 + ], + "rotation_deg": [ + -7.693248, + 37.724591, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2060, + "type": "imguizmo", + "translation": [ + -2.519028, + -4.491849, + 5.296313 + ], + "rotation_deg": [ + -8.06089, + 37.906562, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2061, + "type": "imguizmo", + "translation": [ + -2.632161, + -4.421129, + 5.316131 + ], + "rotation_deg": [ + -8.422915, + 38.09696, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2062, + "type": "imguizmo", + "translation": [ + -2.743459, + -4.347328, + 5.333952 + ], + "rotation_deg": [ + -8.779069, + 38.295654, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2063, + "type": "imguizmo", + "translation": [ + -2.852845, + -4.270498, + 5.349727 + ], + "rotation_deg": [ + -9.129105, + 38.502505, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2064, + "type": "imguizmo", + "translation": [ + -2.960243, + -4.190692, + 5.363413 + ], + "rotation_deg": [ + -9.472778, + 38.717369, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2065, + "type": "imguizmo", + "translation": [ + -3.065578, + -4.107965, + 5.374971 + ], + "rotation_deg": [ + -9.80985, + 38.940096, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2066, + "type": "imguizmo", + "translation": [ + -3.168777, + -4.022375, + 5.38437 + ], + "rotation_deg": [ + -10.140085, + 39.17053, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2067, + "type": "imguizmo", + "translation": [ + -3.269767, + -3.933981, + 5.391582 + ], + "rotation_deg": [ + -10.463254, + 39.408512, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2068, + "type": "imguizmo", + "translation": [ + -3.368478, + -3.842846, + 5.396587 + ], + "rotation_deg": [ + -10.77913, + 39.653875, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2069, + "type": "imguizmo", + "translation": [ + -3.464842, + -3.749033, + 5.399373 + ], + "rotation_deg": [ + -11.087495, + 39.906449, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2070, + "type": "imguizmo", + "translation": [ + -3.558791, + -3.652608, + 5.39993 + ], + "rotation_deg": [ + -11.388132, + 40.166057, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2071, + "type": "imguizmo", + "translation": [ + -3.65026, + -3.553636, + 5.398258 + ], + "rotation_deg": [ + -11.680833, + 40.432518, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2072, + "type": "imguizmo", + "translation": [ + -3.739185, + -3.452188, + 5.394361 + ], + "rotation_deg": [ + -11.965393, + 40.705647, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2073, + "type": "imguizmo", + "translation": [ + -3.825504, + -3.348334, + 5.38825 + ], + "rotation_deg": [ + -12.241614, + 40.985254, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2074, + "type": "imguizmo", + "translation": [ + -3.909157, + -3.242147, + 5.379942 + ], + "rotation_deg": [ + -12.509304, + 41.271143, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2075, + "type": "imguizmo", + "translation": [ + -3.990086, + -3.1337, + 5.369461 + ], + "rotation_deg": [ + -12.768276, + 41.563115, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2076, + "type": "imguizmo", + "translation": [ + -4.068234, + -3.023069, + 5.356834 + ], + "rotation_deg": [ + -13.018349, + 41.860967, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2077, + "type": "imguizmo", + "translation": [ + -4.143547, + -2.910332, + 5.342098 + ], + "rotation_deg": [ + -13.25935, + 42.164492, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2078, + "type": "imguizmo", + "translation": [ + -4.215972, + -2.795566, + 5.325294 + ], + "rotation_deg": [ + -13.49111, + 42.473477, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2079, + "type": "imguizmo", + "translation": [ + -4.285459, + -2.678852, + 5.306468 + ], + "rotation_deg": [ + -13.713468, + 42.787707, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2080, + "type": "imguizmo", + "translation": [ + -4.351959, + -2.560271, + 5.285673 + ], + "rotation_deg": [ + -13.926269, + 43.106964, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2081, + "type": "imguizmo", + "translation": [ + -4.415426, + -2.439905, + 5.262967 + ], + "rotation_deg": [ + -14.129364, + 43.431024, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2082, + "type": "imguizmo", + "translation": [ + -4.475816, + -2.317839, + 5.238414 + ], + "rotation_deg": [ + -14.322613, + 43.759663, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2083, + "type": "imguizmo", + "translation": [ + -4.533087, + -2.194158, + 5.212081 + ], + "rotation_deg": [ + -14.505879, + 44.092651, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2084, + "type": "imguizmo", + "translation": [ + -4.587199, + -2.068948, + 5.184042 + ], + "rotation_deg": [ + -14.679037, + 44.429755, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2085, + "type": "imguizmo", + "translation": [ + -4.638114, + -1.942296, + 5.154375 + ], + "rotation_deg": [ + -14.841964, + 44.770742, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2086, + "type": "imguizmo", + "translation": [ + -4.685796, + -1.81429, + 5.123163 + ], + "rotation_deg": [ + -14.994548, + 45.115373, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2087, + "type": "imguizmo", + "translation": [ + -4.730213, + -1.68502, + 5.090493 + ], + "rotation_deg": [ + -15.136681, + 45.463408, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2088, + "type": "imguizmo", + "translation": [ + -4.771333, + -1.554575, + 5.056455 + ], + "rotation_deg": [ + -15.268266, + 45.814605, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2089, + "type": "imguizmo", + "translation": [ + -4.809128, + -1.423048, + 5.021146 + ], + "rotation_deg": [ + -15.38921, + 46.168718, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2090, + "type": "imguizmo", + "translation": [ + -4.843572, + -1.290528, + 4.984662 + ], + "rotation_deg": [ + -15.499429, + 46.525502, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2091, + "type": "imguizmo", + "translation": [ + -4.87464, + -1.157109, + 4.947107 + ], + "rotation_deg": [ + -15.598847, + 46.884707, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2092, + "type": "imguizmo", + "translation": [ + -4.90231, + -1.022883, + 4.908584 + ], + "rotation_deg": [ + -15.687393, + 47.246083, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2093, + "type": "imguizmo", + "translation": [ + -4.926564, + -0.887945, + 4.869201 + ], + "rotation_deg": [ + -15.765006, + 47.609379, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2094, + "type": "imguizmo", + "translation": [ + -4.947385, + -0.752388, + 4.829068 + ], + "rotation_deg": [ + -15.831633, + 47.97434, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2095, + "type": "imguizmo", + "translation": [ + -4.964758, + -0.616306, + 4.788296 + ], + "rotation_deg": [ + -15.887226, + 48.340713, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2096, + "type": "imguizmo", + "translation": [ + -4.978671, + -0.479795, + 4.747 + ], + "rotation_deg": [ + -15.931747, + 48.708243, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2097, + "type": "imguizmo", + "translation": [ + -4.989114, + -0.34295, + 4.705293 + ], + "rotation_deg": [ + -15.965165, + 49.076673, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2098, + "type": "imguizmo", + "translation": [ + -4.99608, + -0.205866, + 4.663294 + ], + "rotation_deg": [ + -15.987456, + 49.445746, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2099, + "type": "imguizmo", + "translation": [ + -4.999564, + -0.068638, + 4.621117 + ], + "rotation_deg": [ + -15.998606, + 49.815206, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2100, + "type": "imguizmo", + "translation": [ + -4.999564, + 0.068638, + 4.578883 + ], + "rotation_deg": [ + -15.998606, + 50.184794, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2101, + "type": "imguizmo", + "translation": [ + -4.99608, + 0.205866, + 4.536706 + ], + "rotation_deg": [ + -15.987456, + 50.554254, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2102, + "type": "imguizmo", + "translation": [ + -4.989114, + 0.34295, + 4.494707 + ], + "rotation_deg": [ + -15.965165, + 50.923327, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2103, + "type": "imguizmo", + "translation": [ + -4.978671, + 0.479795, + 4.453 + ], + "rotation_deg": [ + -15.931747, + 51.291757, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2104, + "type": "imguizmo", + "translation": [ + -4.964758, + 0.616306, + 4.411704 + ], + "rotation_deg": [ + -15.887226, + 51.659287, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2105, + "type": "imguizmo", + "translation": [ + -4.947385, + 0.752388, + 4.370932 + ], + "rotation_deg": [ + -15.831633, + 52.02566, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2106, + "type": "imguizmo", + "translation": [ + -4.926564, + 0.887945, + 4.330799 + ], + "rotation_deg": [ + -15.765006, + 52.390621, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2107, + "type": "imguizmo", + "translation": [ + -4.90231, + 1.022883, + 4.291416 + ], + "rotation_deg": [ + -15.687393, + 52.753917, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2108, + "type": "imguizmo", + "translation": [ + -4.87464, + 1.157109, + 4.252893 + ], + "rotation_deg": [ + -15.598847, + 53.115293, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2109, + "type": "imguizmo", + "translation": [ + -4.843572, + 1.290528, + 4.215338 + ], + "rotation_deg": [ + -15.499429, + 53.474498, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2110, + "type": "imguizmo", + "translation": [ + -4.809128, + 1.423048, + 4.178854 + ], + "rotation_deg": [ + -15.38921, + 53.831282, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2111, + "type": "imguizmo", + "translation": [ + -4.771333, + 1.554575, + 4.143545 + ], + "rotation_deg": [ + -15.268266, + 54.185395, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2112, + "type": "imguizmo", + "translation": [ + -4.730213, + 1.68502, + 4.109507 + ], + "rotation_deg": [ + -15.136681, + 54.536592, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2113, + "type": "imguizmo", + "translation": [ + -4.685796, + 1.81429, + 4.076837 + ], + "rotation_deg": [ + -14.994548, + 54.884627, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2114, + "type": "imguizmo", + "translation": [ + -4.638114, + 1.942296, + 4.045625 + ], + "rotation_deg": [ + -14.841964, + 55.229258, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2115, + "type": "imguizmo", + "translation": [ + -4.587199, + 2.068948, + 4.015958 + ], + "rotation_deg": [ + -14.679037, + 55.570245, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2116, + "type": "imguizmo", + "translation": [ + -4.533087, + 2.194158, + 3.987919 + ], + "rotation_deg": [ + -14.505879, + 55.907349, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2117, + "type": "imguizmo", + "translation": [ + -4.475816, + 2.317839, + 3.961586 + ], + "rotation_deg": [ + -14.322613, + 56.240337, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2118, + "type": "imguizmo", + "translation": [ + -4.415426, + 2.439905, + 3.937033 + ], + "rotation_deg": [ + -14.129364, + 56.568976, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2119, + "type": "imguizmo", + "translation": [ + -4.351959, + 2.560271, + 3.914327 + ], + "rotation_deg": [ + -13.926269, + 56.893036, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2120, + "type": "imguizmo", + "translation": [ + -4.285459, + 2.678852, + 3.893532 + ], + "rotation_deg": [ + -13.713468, + 57.212293, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2121, + "type": "imguizmo", + "translation": [ + -4.215972, + 2.795566, + 3.874706 + ], + "rotation_deg": [ + -13.49111, + 57.526523, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2122, + "type": "imguizmo", + "translation": [ + -4.143547, + 2.910332, + 3.857902 + ], + "rotation_deg": [ + -13.25935, + 57.835508, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2123, + "type": "imguizmo", + "translation": [ + -4.068234, + 3.023069, + 3.843166 + ], + "rotation_deg": [ + -13.018349, + 58.139033, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2124, + "type": "imguizmo", + "translation": [ + -3.990086, + 3.1337, + 3.830539 + ], + "rotation_deg": [ + -12.768276, + 58.436885, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2125, + "type": "imguizmo", + "translation": [ + -3.909157, + 3.242147, + 3.820058 + ], + "rotation_deg": [ + -12.509304, + 58.728857, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2126, + "type": "imguizmo", + "translation": [ + -3.825504, + 3.348334, + 3.81175 + ], + "rotation_deg": [ + -12.241614, + 59.014746, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2127, + "type": "imguizmo", + "translation": [ + -3.739185, + 3.452188, + 3.805639 + ], + "rotation_deg": [ + -11.965393, + 59.294353, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2128, + "type": "imguizmo", + "translation": [ + -3.65026, + 3.553636, + 3.801742 + ], + "rotation_deg": [ + -11.680833, + 59.567482, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2129, + "type": "imguizmo", + "translation": [ + -3.558791, + 3.652608, + 3.80007 + ], + "rotation_deg": [ + -11.388132, + 59.833943, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2130, + "type": "imguizmo", + "translation": [ + -3.464842, + 3.749033, + 3.800627 + ], + "rotation_deg": [ + -11.087495, + 60.093551, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2131, + "type": "imguizmo", + "translation": [ + -3.368478, + 3.842846, + 3.803413 + ], + "rotation_deg": [ + -10.77913, + 60.346125, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2132, + "type": "imguizmo", + "translation": [ + -3.269767, + 3.933981, + 3.808418 + ], + "rotation_deg": [ + -10.463254, + 60.591488, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2133, + "type": "imguizmo", + "translation": [ + -3.168777, + 4.022375, + 3.81563 + ], + "rotation_deg": [ + -10.140085, + 60.82947, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2134, + "type": "imguizmo", + "translation": [ + -3.065578, + 4.107965, + 3.825029 + ], + "rotation_deg": [ + -9.80985, + 61.059904, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2135, + "type": "imguizmo", + "translation": [ + -2.960243, + 4.190692, + 3.836587 + ], + "rotation_deg": [ + -9.472778, + 61.282631, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2136, + "type": "imguizmo", + "translation": [ + -2.852845, + 4.270498, + 3.850273 + ], + "rotation_deg": [ + -9.129105, + 61.497495, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2137, + "type": "imguizmo", + "translation": [ + -2.743459, + 4.347328, + 3.866048 + ], + "rotation_deg": [ + -8.779069, + 61.704346, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2138, + "type": "imguizmo", + "translation": [ + -2.632161, + 4.421129, + 3.883869 + ], + "rotation_deg": [ + -8.422915, + 61.90304, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2139, + "type": "imguizmo", + "translation": [ + -2.519028, + 4.491849, + 3.903687 + ], + "rotation_deg": [ + -8.06089, + 62.093438, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2140, + "type": "imguizmo", + "translation": [ + -2.40414, + 4.559438, + 3.925444 + ], + "rotation_deg": [ + -7.693248, + 62.275409, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2141, + "type": "imguizmo", + "translation": [ + -2.287577, + 4.623849, + 3.949083 + ], + "rotation_deg": [ + -7.320245, + 62.448824, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2142, + "type": "imguizmo", + "translation": [ + -2.169419, + 4.685038, + 3.974535 + ], + "rotation_deg": [ + -6.94214, + 62.613564, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2143, + "type": "imguizmo", + "translation": [ + -2.049749, + 4.742962, + 4.00173 + ], + "rotation_deg": [ + -6.559197, + 62.769513, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2144, + "type": "imguizmo", + "translation": [ + -1.928651, + 4.797581, + 4.030593 + ], + "rotation_deg": [ + -6.171682, + 62.916563, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2145, + "type": "imguizmo", + "translation": [ + -1.806208, + 4.848856, + 4.061043 + ], + "rotation_deg": [ + -5.779867, + 63.054611, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2146, + "type": "imguizmo", + "translation": [ + -1.682507, + 4.896751, + 4.092996 + ], + "rotation_deg": [ + -5.384023, + 63.183561, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2147, + "type": "imguizmo", + "translation": [ + -1.557633, + 4.941234, + 4.126361 + ], + "rotation_deg": [ + -4.984427, + 63.303324, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2148, + "type": "imguizmo", + "translation": [ + -1.431674, + 4.982274, + 4.161047 + ], + "rotation_deg": [ + -4.581358, + 63.413815, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2149, + "type": "imguizmo", + "translation": [ + -1.304717, + 5.019841, + 4.196955 + ], + "rotation_deg": [ + -4.175095, + 63.514957, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2150, + "type": "imguizmo", + "translation": [ + -1.176851, + 5.05391, + 4.233988 + ], + "rotation_deg": [ + -3.765923, + 63.606681, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2151, + "type": "imguizmo", + "translation": [ + -1.048165, + 5.084457, + 4.27204 + ], + "rotation_deg": [ + -3.354126, + 63.688922, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2152, + "type": "imguizmo", + "translation": [ + -0.918748, + 5.11146, + 4.311007 + ], + "rotation_deg": [ + -2.939992, + 63.761623, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2153, + "type": "imguizmo", + "translation": [ + -0.78869, + 5.134901, + 4.350779 + ], + "rotation_deg": [ + -2.523809, + 63.824734, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2154, + "type": "imguizmo", + "translation": [ + -0.658083, + 5.154764, + 4.391245 + ], + "rotation_deg": [ + -2.105867, + 63.87821, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2155, + "type": "imguizmo", + "translation": [ + -0.527018, + 5.171034, + 4.432294 + ], + "rotation_deg": [ + -1.686458, + 63.922013, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2156, + "type": "imguizmo", + "translation": [ + -0.395585, + 5.1837, + 4.47381 + ], + "rotation_deg": [ + -1.265873, + 63.956115, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2157, + "type": "imguizmo", + "translation": [ + -0.263877, + 5.192753, + 4.515677 + ], + "rotation_deg": [ + -0.844406, + 63.98049, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2158, + "type": "imguizmo", + "translation": [ + -0.131984, + 5.198188, + 4.55778 + ], + "rotation_deg": [ + -0.42235, + 63.995122, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2159, + "type": "imguizmo", + "translation": [ + 0, + 5.2, + 4.6 + ], + "rotation_deg": [ + 0, + 64, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2160, + "type": "action", + "action": "set_active_render_window", + "value": 0 + }, + { + "frame": 2160, + "type": "action", + "action": "set_active_planar", + "value": 9 + }, + { + "frame": 2160, + "type": "action", + "action": "set_projection_type", + "value": "perspective" + }, + { + "frame": 2160, + "type": "action", + "action": "set_left_handed", + "value": true + }, + { + "frame": 2160, + "type": "action", + "action": "reset_active_camera", + "value": 1 + }, + { + "frame": 2160, + "type": "action", + "action": "set_active_render_window", + "value": 1 + }, + { + "frame": 2160, + "type": "action", + "action": "set_active_planar", + "value": 9 + }, + { + "frame": 2160, + "type": "action", + "action": "set_projection_type", + "value": "orthographic" + }, + { + "frame": 2160, + "type": "action", + "action": "set_left_handed", + "value": true + }, + { + "frame": 2160, + "type": "action", + "action": "set_active_render_window", + "value": 0 + }, + { + "frame": 2161, + "type": "imguizmo", + "translation": [ + 0.0, + 0.188594, + 12.5 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2162, + "type": "imguizmo", + "translation": [ + 0.050154, + 0.215598, + 12.495644 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2163, + "type": "imguizmo", + "translation": [ + 0.100273, + 0.242001, + 12.48258 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2164, + "type": "imguizmo", + "translation": [ + 0.150322, + 0.267729, + 12.460817 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2165, + "type": "imguizmo", + "translation": [ + 0.200267, + 0.292711, + 12.430369 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2166, + "type": "imguizmo", + "translation": [ + 0.250072, + 0.316878, + 12.391259 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2167, + "type": "imguizmo", + "translation": [ + 0.299702, + 0.340161, + 12.343512 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2168, + "type": "imguizmo", + "translation": [ + 0.349124, + 0.362496, + 12.287164 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2169, + "type": "imguizmo", + "translation": [ + 0.398303, + 0.383821, + 12.222252 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2170, + "type": "imguizmo", + "translation": [ + 0.447203, + 0.404075, + 12.148822 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2171, + "type": "imguizmo", + "translation": [ + 0.495793, + 0.423204, + 12.066926 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2172, + "type": "imguizmo", + "translation": [ + 0.544036, + 0.441153, + 11.97662 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2173, + "type": "imguizmo", + "translation": [ + 0.591901, + 0.457873, + 11.877968 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2174, + "type": "imguizmo", + "translation": [ + 0.639353, + 0.473316, + 11.771037 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2175, + "type": "imguizmo", + "translation": [ + 0.686359, + 0.487441, + 11.655903 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2176, + "type": "imguizmo", + "translation": [ + 0.732887, + 0.500206, + 11.532646 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2177, + "type": "imguizmo", + "translation": [ + 0.778905, + 0.511578, + 11.401351 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2178, + "type": "imguizmo", + "translation": [ + 0.824379, + 0.521523, + 11.262111 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2179, + "type": "imguizmo", + "translation": [ + 0.869279, + 0.530015, + 11.115022 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2180, + "type": "imguizmo", + "translation": [ + 0.913573, + 0.53703, + 10.960187 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2181, + "type": "imguizmo", + "translation": [ + 0.957231, + 0.542548, + 10.797713 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2182, + "type": "imguizmo", + "translation": [ + 1.000221, + 0.546554, + 10.627714 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2183, + "type": "imguizmo", + "translation": [ + 1.042514, + 0.549037, + 10.450309 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2184, + "type": "imguizmo", + "translation": [ + 1.084081, + 0.549989, + 10.265621 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2185, + "type": "imguizmo", + "translation": [ + 1.124892, + 0.549408, + 10.073778 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2186, + "type": "imguizmo", + "translation": [ + 1.16492, + 0.547296, + 9.874915 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2187, + "type": "imguizmo", + "translation": [ + 1.204135, + 0.543659, + 9.66917 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2188, + "type": "imguizmo", + "translation": [ + 1.242511, + 0.538506, + 9.456686 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2189, + "type": "imguizmo", + "translation": [ + 1.280022, + 0.531852, + 9.237611 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2190, + "type": "imguizmo", + "translation": [ + 1.31664, + 0.523716, + 9.012099 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2191, + "type": "imguizmo", + "translation": [ + 1.352341, + 0.514121, + 8.780307 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2192, + "type": "imguizmo", + "translation": [ + 1.387099, + 0.503092, + 8.542395 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2193, + "type": "imguizmo", + "translation": [ + 1.42089, + 0.490661, + 8.298529 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2194, + "type": "imguizmo", + "translation": [ + 1.453692, + 0.476863, + 8.048881 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2195, + "type": "imguizmo", + "translation": [ + 1.48548, + 0.461735, + 7.793623 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2196, + "type": "imguizmo", + "translation": [ + 1.516233, + 0.445321, + 7.532933 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2197, + "type": "imguizmo", + "translation": [ + 1.545929, + 0.427665, + 7.266994 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2198, + "type": "imguizmo", + "translation": [ + 1.574548, + 0.408818, + 6.99599 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2199, + "type": "imguizmo", + "translation": [ + 1.602069, + 0.388831, + 6.72011 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2200, + "type": "imguizmo", + "translation": [ + 1.628474, + 0.36776, + 6.439547 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2201, + "type": "imguizmo", + "translation": [ + 1.653744, + 0.345664, + 6.154497 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2202, + "type": "imguizmo", + "translation": [ + 1.677862, + 0.322605, + 5.865157 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2203, + "type": "imguizmo", + "translation": [ + 1.70081, + 0.298646, + 5.571729 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2204, + "type": "imguizmo", + "translation": [ + 1.722573, + 0.273856, + 5.274419 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2205, + "type": "imguizmo", + "translation": [ + 1.743136, + 0.248302, + 4.973433 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2206, + "type": "imguizmo", + "translation": [ + 1.762483, + 0.222056, + 4.668981 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2207, + "type": "imguizmo", + "translation": [ + 1.780603, + 0.19519, + 4.361274 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2208, + "type": "imguizmo", + "translation": [ + 1.797481, + 0.167781, + 4.050529 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2209, + "type": "imguizmo", + "translation": [ + 1.813107, + 0.139905, + 3.73696 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2210, + "type": "imguizmo", + "translation": [ + 1.827469, + 0.111638, + 3.420787 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2211, + "type": "imguizmo", + "translation": [ + 1.840557, + 0.08306, + 3.102231 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2212, + "type": "imguizmo", + "translation": [ + 1.852363, + 0.054251, + 2.781512 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2213, + "type": "imguizmo", + "translation": [ + 1.862878, + 0.025291, + 2.458854 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2214, + "type": "imguizmo", + "translation": [ + 1.872094, + -0.00374, + 2.134483 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2215, + "type": "imguizmo", + "translation": [ + 1.880006, + -0.032761, + 1.808625 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2216, + "type": "imguizmo", + "translation": [ + 1.886608, + -0.06169, + 1.481506 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2217, + "type": "imguizmo", + "translation": [ + 1.891895, + -0.090448, + 1.153354 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2218, + "type": "imguizmo", + "translation": [ + 1.895863, + -0.118953, + 0.824399 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2219, + "type": "imguizmo", + "translation": [ + 1.89851, + -0.147126, + 0.494869 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2220, + "type": "imguizmo", + "translation": [ + 1.899834, + -0.17489, + 0.164995 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2221, + "type": "imguizmo", + "translation": [ + 1.899834, + -0.202166, + -0.164995 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2222, + "type": "imguizmo", + "translation": [ + 1.89851, + -0.228879, + -0.494869 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2223, + "type": "imguizmo", + "translation": [ + 1.895863, + -0.254954, + -0.824399 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2224, + "type": "imguizmo", + "translation": [ + 1.891895, + -0.280318, + -1.153354 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2225, + "type": "imguizmo", + "translation": [ + 1.886608, + -0.304901, + -1.481506 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2226, + "type": "imguizmo", + "translation": [ + 1.880006, + -0.328634, + -1.808625 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2227, + "type": "imguizmo", + "translation": [ + 1.872094, + -0.351451, + -2.134483 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2228, + "type": "imguizmo", + "translation": [ + 1.862878, + -0.373288, + -2.458854 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2229, + "type": "imguizmo", + "translation": [ + 1.852363, + -0.394085, + -2.781512 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2230, + "type": "imguizmo", + "translation": [ + 1.840557, + -0.413784, + -3.102231 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2231, + "type": "imguizmo", + "translation": [ + 1.827469, + -0.432329, + -3.420787 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2232, + "type": "imguizmo", + "translation": [ + 1.813107, + -0.44967, + -3.73696 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2233, + "type": "imguizmo", + "translation": [ + 1.797481, + -0.465757, + -4.050529 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2234, + "type": "imguizmo", + "translation": [ + 1.780603, + -0.480546, + -4.361274 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2235, + "type": "imguizmo", + "translation": [ + 1.762483, + -0.493996, + -4.668981 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2236, + "type": "imguizmo", + "translation": [ + 1.743136, + -0.506068, + -4.973433 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2237, + "type": "imguizmo", + "translation": [ + 1.722573, + -0.516731, + -5.274419 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2238, + "type": "imguizmo", + "translation": [ + 1.70081, + -0.525953, + -5.571729 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2239, + "type": "imguizmo", + "translation": [ + 1.677862, + -0.533709, + -5.865157 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2240, + "type": "imguizmo", + "translation": [ + 1.653744, + -0.539977, + -6.154497 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2241, + "type": "imguizmo", + "translation": [ + 1.628474, + -0.544741, + -6.439547 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2242, + "type": "imguizmo", + "translation": [ + 1.602069, + -0.547986, + -6.72011 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2243, + "type": "imguizmo", + "translation": [ + 1.574548, + -0.549704, + -6.99599 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2244, + "type": "imguizmo", + "translation": [ + 1.545929, + -0.54989, + -7.266994 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2245, + "type": "imguizmo", + "translation": [ + 1.516233, + -0.548543, + -7.532933 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2246, + "type": "imguizmo", + "translation": [ + 1.48548, + -0.545667, + -7.793623 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2247, + "type": "imguizmo", + "translation": [ + 1.453692, + -0.541271, + -8.048881 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2248, + "type": "imguizmo", + "translation": [ + 1.42089, + -0.535366, + -8.298529 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2249, + "type": "imguizmo", + "translation": [ + 1.387099, + -0.527968, + -8.542395 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2250, + "type": "imguizmo", + "translation": [ + 1.352341, + -0.519099, + -8.780307 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2251, + "type": "imguizmo", + "translation": [ + 1.31664, + -0.508784, + -9.012099 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2252, + "type": "imguizmo", + "translation": [ + 1.280022, + -0.49705, + -9.237611 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2253, + "type": "imguizmo", + "translation": [ + 1.242511, + -0.483931, + -9.456686 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2254, + "type": "imguizmo", + "translation": [ + 1.204135, + -0.469463, + -9.66917 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2255, + "type": "imguizmo", + "translation": [ + 1.16492, + -0.453686, + -9.874915 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2256, + "type": "imguizmo", + "translation": [ + 1.124892, + -0.436645, + -10.073778 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2257, + "type": "imguizmo", + "translation": [ + 1.084081, + -0.418387, + -10.265621 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2258, + "type": "imguizmo", + "translation": [ + 1.042514, + -0.398963, + -10.450309 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2259, + "type": "imguizmo", + "translation": [ + 1.000221, + -0.378427, + -10.627714 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2260, + "type": "imguizmo", + "translation": [ + 0.957231, + -0.356836, + -10.797713 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2261, + "type": "imguizmo", + "translation": [ + 0.913573, + -0.334251, + -10.960187 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2262, + "type": "imguizmo", + "translation": [ + 0.869279, + -0.310734, + -11.115022 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2263, + "type": "imguizmo", + "translation": [ + 0.824379, + -0.286351, + -11.262111 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2264, + "type": "imguizmo", + "translation": [ + 0.778905, + -0.26117, + -11.401351 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2265, + "type": "imguizmo", + "translation": [ + 0.732887, + -0.235261, + -11.532646 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2266, + "type": "imguizmo", + "translation": [ + 0.686359, + -0.208696, + -11.655903 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2267, + "type": "imguizmo", + "translation": [ + 0.639353, + -0.181549, + -11.771037 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2268, + "type": "imguizmo", + "translation": [ + 0.591901, + -0.153897, + -11.877968 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2269, + "type": "imguizmo", + "translation": [ + 0.544036, + -0.125815, + -11.97662 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2270, + "type": "imguizmo", + "translation": [ + 0.495793, + -0.097383, + -12.066926 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2271, + "type": "imguizmo", + "translation": [ + 0.447203, + -0.06868, + -12.148822 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2272, + "type": "imguizmo", + "translation": [ + 0.398303, + -0.039785, + -12.222252 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2273, + "type": "imguizmo", + "translation": [ + 0.349124, + -0.010779, + -12.287164 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2274, + "type": "imguizmo", + "translation": [ + 0.299702, + 0.018257, + -12.343512 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2275, + "type": "imguizmo", + "translation": [ + 0.250072, + 0.047242, + -12.391259 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2276, + "type": "imguizmo", + "translation": [ + 0.200267, + 0.076095, + -12.430369 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2277, + "type": "imguizmo", + "translation": [ + 0.150322, + 0.104737, + -12.460817 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2278, + "type": "imguizmo", + "translation": [ + 0.100273, + 0.133086, + -12.48258 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2279, + "type": "imguizmo", + "translation": [ + 0.050154, + 0.161064, + -12.495644 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2280, + "type": "imguizmo", + "translation": [ + 0.0, + 0.188594, + -12.5 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2281, + "type": "imguizmo", + "translation": [ + -0.050154, + 0.215598, + -12.495644 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2282, + "type": "imguizmo", + "translation": [ + -0.100273, + 0.242001, + -12.48258 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2283, + "type": "imguizmo", + "translation": [ + -0.150322, + 0.267729, + -12.460817 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2284, + "type": "imguizmo", + "translation": [ + -0.200267, + 0.292711, + -12.430369 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2285, + "type": "imguizmo", + "translation": [ + -0.250072, + 0.316878, + -12.391259 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2286, + "type": "imguizmo", + "translation": [ + -0.299702, + 0.340161, + -12.343512 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2287, + "type": "imguizmo", + "translation": [ + -0.349124, + 0.362496, + -12.287164 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2288, + "type": "imguizmo", + "translation": [ + -0.398303, + 0.383821, + -12.222252 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2289, + "type": "imguizmo", + "translation": [ + -0.447203, + 0.404075, + -12.148822 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2290, + "type": "imguizmo", + "translation": [ + -0.495793, + 0.423204, + -12.066926 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2291, + "type": "imguizmo", + "translation": [ + -0.544036, + 0.441153, + -11.97662 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2292, + "type": "imguizmo", + "translation": [ + -0.591901, + 0.457873, + -11.877968 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2293, + "type": "imguizmo", + "translation": [ + -0.639353, + 0.473316, + -11.771037 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2294, + "type": "imguizmo", + "translation": [ + -0.686359, + 0.487441, + -11.655903 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2295, + "type": "imguizmo", + "translation": [ + -0.732887, + 0.500206, + -11.532646 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2296, + "type": "imguizmo", + "translation": [ + -0.778905, + 0.511578, + -11.401351 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2297, + "type": "imguizmo", + "translation": [ + -0.824379, + 0.521523, + -11.262111 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2298, + "type": "imguizmo", + "translation": [ + -0.869279, + 0.530015, + -11.115022 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2299, + "type": "imguizmo", + "translation": [ + -0.913573, + 0.53703, + -10.960187 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2300, + "type": "imguizmo", + "translation": [ + -0.957231, + 0.542548, + -10.797713 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2301, + "type": "imguizmo", + "translation": [ + -1.000221, + 0.546554, + -10.627714 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2302, + "type": "imguizmo", + "translation": [ + -1.042514, + 0.549037, + -10.450309 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2303, + "type": "imguizmo", + "translation": [ + -1.084081, + 0.549989, + -10.265621 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2304, + "type": "imguizmo", + "translation": [ + -1.124892, + 0.549408, + -10.073778 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2305, + "type": "imguizmo", + "translation": [ + -1.16492, + 0.547296, + -9.874915 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2306, + "type": "imguizmo", + "translation": [ + -1.204135, + 0.543659, + -9.66917 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2307, + "type": "imguizmo", + "translation": [ + -1.242511, + 0.538506, + -9.456686 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2308, + "type": "imguizmo", + "translation": [ + -1.280022, + 0.531852, + -9.237611 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2309, + "type": "imguizmo", + "translation": [ + -1.31664, + 0.523716, + -9.012099 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2310, + "type": "imguizmo", + "translation": [ + -1.352341, + 0.514121, + -8.780307 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2311, + "type": "imguizmo", + "translation": [ + -1.387099, + 0.503092, + -8.542395 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2312, + "type": "imguizmo", + "translation": [ + -1.42089, + 0.490661, + -8.298529 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2313, + "type": "imguizmo", + "translation": [ + -1.453692, + 0.476863, + -8.048881 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2314, + "type": "imguizmo", + "translation": [ + -1.48548, + 0.461735, + -7.793623 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2315, + "type": "imguizmo", + "translation": [ + -1.516233, + 0.445321, + -7.532933 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2316, + "type": "imguizmo", + "translation": [ + -1.545929, + 0.427665, + -7.266994 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2317, + "type": "imguizmo", + "translation": [ + -1.574548, + 0.408818, + -6.99599 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2318, + "type": "imguizmo", + "translation": [ + -1.602069, + 0.388831, + -6.72011 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2319, + "type": "imguizmo", + "translation": [ + -1.628474, + 0.36776, + -6.439547 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2320, + "type": "imguizmo", + "translation": [ + -1.653744, + 0.345664, + -6.154497 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2321, + "type": "imguizmo", + "translation": [ + -1.677862, + 0.322605, + -5.865157 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2322, + "type": "imguizmo", + "translation": [ + -1.70081, + 0.298646, + -5.571729 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2323, + "type": "imguizmo", + "translation": [ + -1.722573, + 0.273856, + -5.274419 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2324, + "type": "imguizmo", + "translation": [ + -1.743136, + 0.248302, + -4.973433 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2325, + "type": "imguizmo", + "translation": [ + -1.762483, + 0.222056, + -4.668981 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2326, + "type": "imguizmo", + "translation": [ + -1.780603, + 0.19519, + -4.361274 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2327, + "type": "imguizmo", + "translation": [ + -1.797481, + 0.167781, + -4.050529 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2328, + "type": "imguizmo", + "translation": [ + -1.813107, + 0.139905, + -3.73696 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2329, + "type": "imguizmo", + "translation": [ + -1.827469, + 0.111638, + -3.420787 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2330, + "type": "imguizmo", + "translation": [ + -1.840557, + 0.08306, + -3.102231 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2331, + "type": "imguizmo", + "translation": [ + -1.852363, + 0.054251, + -2.781512 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2332, + "type": "imguizmo", + "translation": [ + -1.862878, + 0.025291, + -2.458854 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2333, + "type": "imguizmo", + "translation": [ + -1.872094, + -0.00374, + -2.134483 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2334, + "type": "imguizmo", + "translation": [ + -1.880006, + -0.032761, + -1.808625 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2335, + "type": "imguizmo", + "translation": [ + -1.886608, + -0.06169, + -1.481506 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2336, + "type": "imguizmo", + "translation": [ + -1.891895, + -0.090448, + -1.153354 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2337, + "type": "imguizmo", + "translation": [ + -1.895863, + -0.118953, + -0.824399 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2338, + "type": "imguizmo", + "translation": [ + -1.89851, + -0.147126, + -0.494869 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2339, + "type": "imguizmo", + "translation": [ + -1.899834, + -0.17489, + -0.164995 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2340, + "type": "imguizmo", + "translation": [ + -1.899834, + -0.202166, + 0.164995 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2341, + "type": "imguizmo", + "translation": [ + -1.89851, + -0.228879, + 0.494869 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2342, + "type": "imguizmo", + "translation": [ + -1.895863, + -0.254954, + 0.824399 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2343, + "type": "imguizmo", + "translation": [ + -1.891895, + -0.280318, + 1.153354 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2344, + "type": "imguizmo", + "translation": [ + -1.886608, + -0.304901, + 1.481506 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2345, + "type": "imguizmo", + "translation": [ + -1.880006, + -0.328634, + 1.808625 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2346, + "type": "imguizmo", + "translation": [ + -1.872094, + -0.351451, + 2.134483 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2347, + "type": "imguizmo", + "translation": [ + -1.862878, + -0.373288, + 2.458854 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2348, + "type": "imguizmo", + "translation": [ + -1.852363, + -0.394085, + 2.781512 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2349, + "type": "imguizmo", + "translation": [ + -1.840557, + -0.413784, + 3.102231 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2350, + "type": "imguizmo", + "translation": [ + -1.827469, + -0.432329, + 3.420787 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2351, + "type": "imguizmo", + "translation": [ + -1.813107, + -0.44967, + 3.73696 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2352, + "type": "imguizmo", + "translation": [ + -1.797481, + -0.465757, + 4.050529 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2353, + "type": "imguizmo", + "translation": [ + -1.780603, + -0.480546, + 4.361274 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2354, + "type": "imguizmo", + "translation": [ + -1.762483, + -0.493996, + 4.668981 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2355, + "type": "imguizmo", + "translation": [ + -1.743136, + -0.506068, + 4.973433 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2356, + "type": "imguizmo", + "translation": [ + -1.722573, + -0.516731, + 5.274419 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2357, + "type": "imguizmo", + "translation": [ + -1.70081, + -0.525953, + 5.571729 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2358, + "type": "imguizmo", + "translation": [ + -1.677862, + -0.533709, + 5.865157 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2359, + "type": "imguizmo", + "translation": [ + -1.653744, + -0.539977, + 6.154497 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2360, + "type": "imguizmo", + "translation": [ + -1.628474, + -0.544741, + 6.439547 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2361, + "type": "imguizmo", + "translation": [ + -1.602069, + -0.547986, + 6.72011 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2362, + "type": "imguizmo", + "translation": [ + -1.574548, + -0.549704, + 6.99599 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2363, + "type": "imguizmo", + "translation": [ + -1.545929, + -0.54989, + 7.266994 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2364, + "type": "imguizmo", + "translation": [ + -1.516233, + -0.548543, + 7.532933 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2365, + "type": "imguizmo", + "translation": [ + -1.48548, + -0.545667, + 7.793623 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2366, + "type": "imguizmo", + "translation": [ + -1.453692, + -0.541271, + 8.048881 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2367, + "type": "imguizmo", + "translation": [ + -1.42089, + -0.535366, + 8.298529 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2368, + "type": "imguizmo", + "translation": [ + -1.387099, + -0.527968, + 8.542395 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2369, + "type": "imguizmo", + "translation": [ + -1.352341, + -0.519099, + 8.780307 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2370, + "type": "imguizmo", + "translation": [ + -1.31664, + -0.508784, + 9.012099 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2371, + "type": "imguizmo", + "translation": [ + -1.280022, + -0.49705, + 9.237611 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2372, + "type": "imguizmo", + "translation": [ + -1.242511, + -0.483931, + 9.456686 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2373, + "type": "imguizmo", + "translation": [ + -1.204135, + -0.469463, + 9.66917 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2374, + "type": "imguizmo", + "translation": [ + -1.16492, + -0.453686, + 9.874915 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2375, + "type": "imguizmo", + "translation": [ + -1.124892, + -0.436645, + 10.073778 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2376, + "type": "imguizmo", + "translation": [ + -1.084081, + -0.418387, + 10.265621 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2377, + "type": "imguizmo", + "translation": [ + -1.042514, + -0.398963, + 10.450309 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2378, + "type": "imguizmo", + "translation": [ + -1.000221, + -0.378427, + 10.627714 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2379, + "type": "imguizmo", + "translation": [ + -0.957231, + -0.356836, + 10.797713 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2380, + "type": "imguizmo", + "translation": [ + -0.913573, + -0.334251, + 10.960187 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2381, + "type": "imguizmo", + "translation": [ + -0.869279, + -0.310734, + 11.115022 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2382, + "type": "imguizmo", + "translation": [ + -0.824379, + -0.286351, + 11.262111 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2383, + "type": "imguizmo", + "translation": [ + -0.778905, + -0.26117, + 11.401351 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2384, + "type": "imguizmo", + "translation": [ + -0.732887, + -0.235261, + 11.532646 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2385, + "type": "imguizmo", + "translation": [ + -0.686359, + -0.208696, + 11.655903 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2386, + "type": "imguizmo", + "translation": [ + -0.639353, + -0.181549, + 11.771037 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2387, + "type": "imguizmo", + "translation": [ + -0.591901, + -0.153897, + 11.877968 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2388, + "type": "imguizmo", + "translation": [ + -0.544036, + -0.125815, + 11.97662 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2389, + "type": "imguizmo", + "translation": [ + -0.495793, + -0.097383, + 12.066926 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2390, + "type": "imguizmo", + "translation": [ + -0.447203, + -0.06868, + 12.148822 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2391, + "type": "imguizmo", + "translation": [ + -0.398303, + -0.039785, + 12.222252 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2392, + "type": "imguizmo", + "translation": [ + -0.349124, + -0.010779, + 12.287164 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2393, + "type": "imguizmo", + "translation": [ + -0.299702, + 0.018257, + 12.343512 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2394, + "type": "imguizmo", + "translation": [ + -0.250072, + 0.047242, + 12.391259 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2395, + "type": "imguizmo", + "translation": [ + -0.200267, + 0.076095, + 12.430369 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2396, + "type": "imguizmo", + "translation": [ + -0.150322, + 0.104737, + 12.460817 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2397, + "type": "imguizmo", + "translation": [ + -0.100273, + 0.133086, + 12.48258 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2398, + "type": "imguizmo", + "translation": [ + -0.050154, + 0.161064, + 12.495644 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2399, + "type": "imguizmo", + "translation": [ + -0.0, + 0.188594, + 12.5 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2400, + "type": "action", + "action": "set_active_render_window", + "value": 0 + }, + { + "frame": 2400, + "type": "action", + "action": "set_active_planar", + "value": 10 + }, + { + "frame": 2400, + "type": "action", + "action": "set_projection_type", + "value": "perspective" + }, + { + "frame": 2400, + "type": "action", + "action": "set_left_handed", + "value": true + }, + { + "frame": 2400, + "type": "action", + "action": "reset_active_camera", + "value": 1 + }, + { + "frame": 2400, + "type": "action", + "action": "set_active_render_window", + "value": 1 + }, + { + "frame": 2400, + "type": "action", + "action": "set_active_planar", + "value": 10 + }, + { + "frame": 2400, + "type": "action", + "action": "set_projection_type", + "value": "orthographic" + }, + { + "frame": 2400, + "type": "action", + "action": "set_left_handed", + "value": true + }, + { + "frame": 2400, + "type": "action", + "action": "set_active_render_window", + "value": 0 + }, + { + "frame": 2401, + "type": "imguizmo", + "translation": [ + 2.2, + 0, + 6.3 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2402, + "type": "imguizmo", + "translation": [ + 2.213198, + 0.06333, + 6.299686 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2403, + "type": "imguizmo", + "translation": [ + 2.226388, + 0.126484, + 6.298746 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2404, + "type": "imguizmo", + "translation": [ + 2.239559, + 0.189286, + 6.297179 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2405, + "type": "imguizmo", + "translation": [ + 2.252702, + 0.251559, + 6.294987 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2406, + "type": "imguizmo", + "translation": [ + 2.265808, + 0.313132, + 6.292171 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2407, + "type": "imguizmo", + "translation": [ + 2.278869, + 0.373832, + 6.288733 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2408, + "type": "imguizmo", + "translation": [ + 2.291875, + 0.43349, + 6.284676 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2409, + "type": "imguizmo", + "translation": [ + 2.304816, + 0.49194, + 6.280002 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2410, + "type": "imguizmo", + "translation": [ + 2.317685, + 0.549018, + 6.274715 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2411, + "type": "imguizmo", + "translation": [ + 2.330472, + 0.604567, + 6.268819 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2412, + "type": "imguizmo", + "translation": [ + 2.343167, + 0.65843, + 6.262317 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2413, + "type": "imguizmo", + "translation": [ + 2.355763, + 0.710458, + 6.255214 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2414, + "type": "imguizmo", + "translation": [ + 2.368251, + 0.760506, + 6.247515 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2415, + "type": "imguizmo", + "translation": [ + 2.380621, + 0.808435, + 6.239225 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2416, + "type": "imguizmo", + "translation": [ + 2.392865, + 0.85411, + 6.23035 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2417, + "type": "imguizmo", + "translation": [ + 2.404975, + 0.897404, + 6.220897 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2418, + "type": "imguizmo", + "translation": [ + 2.416942, + 0.938198, + 6.210872 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2419, + "type": "imguizmo", + "translation": [ + 2.428758, + 0.976376, + 6.200282 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2420, + "type": "imguizmo", + "translation": [ + 2.440414, + 1.011833, + 6.189133 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2421, + "type": "imguizmo", + "translation": [ + 2.451903, + 1.04447, + 6.177435 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2422, + "type": "imguizmo", + "translation": [ + 2.463216, + 1.074196, + 6.165195 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2423, + "type": "imguizmo", + "translation": [ + 2.474346, + 1.100928, + 6.152422 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2424, + "type": "imguizmo", + "translation": [ + 2.485285, + 1.124591, + 6.139125 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2425, + "type": "imguizmo", + "translation": [ + 2.496024, + 1.14512, + 6.125312 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2426, + "type": "imguizmo", + "translation": [ + 2.506558, + 1.162457, + 6.110994 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2427, + "type": "imguizmo", + "translation": [ + 2.516878, + 1.176554, + 6.09618 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2428, + "type": "imguizmo", + "translation": [ + 2.526977, + 1.187372, + 6.080881 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2429, + "type": "imguizmo", + "translation": [ + 2.536848, + 1.194881, + 6.065108 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2430, + "type": "imguizmo", + "translation": [ + 2.546484, + 1.199059, + 6.048871 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2431, + "type": "imguizmo", + "translation": [ + 2.555879, + 1.199895, + 6.032182 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2432, + "type": "imguizmo", + "translation": [ + 2.565026, + 1.197387, + 6.015052 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2433, + "type": "imguizmo", + "translation": [ + 2.573919, + 1.191542, + 5.997494 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2434, + "type": "imguizmo", + "translation": [ + 2.58255, + 1.182375, + 5.979519 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2435, + "type": "imguizmo", + "translation": [ + 2.590916, + 1.169913, + 5.961141 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2436, + "type": "imguizmo", + "translation": [ + 2.599009, + 1.154191, + 5.942371 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2437, + "type": "imguizmo", + "translation": [ + 2.606823, + 1.135251, + 5.923224 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2438, + "type": "imguizmo", + "translation": [ + 2.614355, + 1.113147, + 5.903711 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2439, + "type": "imguizmo", + "translation": [ + 2.621597, + 1.087941, + 5.883848 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2440, + "type": "imguizmo", + "translation": [ + 2.628546, + 1.059702, + 5.863647 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2441, + "type": "imguizmo", + "translation": [ + 2.635196, + 1.02851, + 5.843124 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2442, + "type": "imguizmo", + "translation": [ + 2.641543, + 0.994451, + 5.822291 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2443, + "type": "imguizmo", + "translation": [ + 2.647582, + 0.957621, + 5.801165 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2444, + "type": "imguizmo", + "translation": [ + 2.653309, + 0.918121, + 5.779758 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2445, + "type": "imguizmo", + "translation": [ + 2.65872, + 0.876062, + 5.758087 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2446, + "type": "imguizmo", + "translation": [ + 2.663811, + 0.831562, + 5.736167 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2447, + "type": "imguizmo", + "translation": [ + 2.66858, + 0.784744, + 5.714012 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2448, + "type": "imguizmo", + "translation": [ + 2.673021, + 0.735739, + 5.691638 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2449, + "type": "imguizmo", + "translation": [ + 2.677133, + 0.684683, + 5.669061 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2450, + "type": "imguizmo", + "translation": [ + 2.680913, + 0.631719, + 5.646297 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2451, + "type": "imguizmo", + "translation": [ + 2.684357, + 0.576994, + 5.623361 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2452, + "type": "imguizmo", + "translation": [ + 2.687464, + 0.52066, + 5.600269 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2453, + "type": "imguizmo", + "translation": [ + 2.690231, + 0.462876, + 5.577038 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2454, + "type": "imguizmo", + "translation": [ + 2.692656, + 0.403802, + 5.553683 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2455, + "type": "imguizmo", + "translation": [ + 2.694739, + 0.343602, + 5.530221 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2456, + "type": "imguizmo", + "translation": [ + 2.696476, + 0.282444, + 5.506668 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2457, + "type": "imguizmo", + "translation": [ + 2.697867, + 0.220499, + 5.483042 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2458, + "type": "imguizmo", + "translation": [ + 2.698911, + 0.15794, + 5.459357 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2459, + "type": "imguizmo", + "translation": [ + 2.699608, + 0.09494, + 5.435631 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2460, + "type": "imguizmo", + "translation": [ + 2.699956, + 0.031676, + 5.41188 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2461, + "type": "imguizmo", + "translation": [ + 2.699956, + -0.031676, + 5.38812 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2462, + "type": "imguizmo", + "translation": [ + 2.699608, + -0.09494, + 5.364369 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2463, + "type": "imguizmo", + "translation": [ + 2.698911, + -0.15794, + 5.340643 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2464, + "type": "imguizmo", + "translation": [ + 2.697867, + -0.220499, + 5.316958 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2465, + "type": "imguizmo", + "translation": [ + 2.696476, + -0.282444, + 5.293332 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2466, + "type": "imguizmo", + "translation": [ + 2.694739, + -0.343602, + 5.269779 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2467, + "type": "imguizmo", + "translation": [ + 2.692656, + -0.403802, + 5.246317 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2468, + "type": "imguizmo", + "translation": [ + 2.690231, + -0.462876, + 5.222962 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2469, + "type": "imguizmo", + "translation": [ + 2.687464, + -0.52066, + 5.199731 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2470, + "type": "imguizmo", + "translation": [ + 2.684357, + -0.576994, + 5.176639 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2471, + "type": "imguizmo", + "translation": [ + 2.680913, + -0.631719, + 5.153703 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2472, + "type": "imguizmo", + "translation": [ + 2.677133, + -0.684683, + 5.130939 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2473, + "type": "imguizmo", + "translation": [ + 2.673021, + -0.735739, + 5.108362 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2474, + "type": "imguizmo", + "translation": [ + 2.66858, + -0.784744, + 5.085988 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2475, + "type": "imguizmo", + "translation": [ + 2.663811, + -0.831562, + 5.063833 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2476, + "type": "imguizmo", + "translation": [ + 2.65872, + -0.876062, + 5.041913 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2477, + "type": "imguizmo", + "translation": [ + 2.653309, + -0.918121, + 5.020242 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2478, + "type": "imguizmo", + "translation": [ + 2.647582, + -0.957621, + 4.998835 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2479, + "type": "imguizmo", + "translation": [ + 2.641543, + -0.994451, + 4.977709 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2480, + "type": "imguizmo", + "translation": [ + 2.635196, + -1.02851, + 4.956876 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2481, + "type": "imguizmo", + "translation": [ + 2.628546, + -1.059702, + 4.936353 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2482, + "type": "imguizmo", + "translation": [ + 2.621597, + -1.087941, + 4.916152 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2483, + "type": "imguizmo", + "translation": [ + 2.614355, + -1.113147, + 4.896289 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2484, + "type": "imguizmo", + "translation": [ + 2.606823, + -1.135251, + 4.876776 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2485, + "type": "imguizmo", + "translation": [ + 2.599009, + -1.154191, + 4.857629 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2486, + "type": "imguizmo", + "translation": [ + 2.590916, + -1.169913, + 4.838859 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2487, + "type": "imguizmo", + "translation": [ + 2.58255, + -1.182375, + 4.820481 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2488, + "type": "imguizmo", + "translation": [ + 2.573919, + -1.191542, + 4.802506 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2489, + "type": "imguizmo", + "translation": [ + 2.565026, + -1.197387, + 4.784948 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2490, + "type": "imguizmo", + "translation": [ + 2.555879, + -1.199895, + 4.767818 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2491, + "type": "imguizmo", + "translation": [ + 2.546484, + -1.199059, + 4.751129 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2492, + "type": "imguizmo", + "translation": [ + 2.536848, + -1.194881, + 4.734892 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2493, + "type": "imguizmo", + "translation": [ + 2.526977, + -1.187372, + 4.719119 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2494, + "type": "imguizmo", + "translation": [ + 2.516878, + -1.176554, + 4.70382 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2495, + "type": "imguizmo", + "translation": [ + 2.506558, + -1.162457, + 4.689006 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2496, + "type": "imguizmo", + "translation": [ + 2.496024, + -1.14512, + 4.674688 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2497, + "type": "imguizmo", + "translation": [ + 2.485285, + -1.124591, + 4.660875 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2498, + "type": "imguizmo", + "translation": [ + 2.474346, + -1.100928, + 4.647578 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2499, + "type": "imguizmo", + "translation": [ + 2.463216, + -1.074196, + 4.634805 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2500, + "type": "imguizmo", + "translation": [ + 2.451903, + -1.04447, + 4.622565 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2501, + "type": "imguizmo", + "translation": [ + 2.440414, + -1.011833, + 4.610867 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2502, + "type": "imguizmo", + "translation": [ + 2.428758, + -0.976376, + 4.599718 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2503, + "type": "imguizmo", + "translation": [ + 2.416942, + -0.938198, + 4.589128 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2504, + "type": "imguizmo", + "translation": [ + 2.404975, + -0.897404, + 4.579103 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2505, + "type": "imguizmo", + "translation": [ + 2.392865, + -0.85411, + 4.56965 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2506, + "type": "imguizmo", + "translation": [ + 2.380621, + -0.808435, + 4.560775 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2507, + "type": "imguizmo", + "translation": [ + 2.368251, + -0.760506, + 4.552485 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2508, + "type": "imguizmo", + "translation": [ + 2.355763, + -0.710458, + 4.544786 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2509, + "type": "imguizmo", + "translation": [ + 2.343167, + -0.65843, + 4.537683 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2510, + "type": "imguizmo", + "translation": [ + 2.330472, + -0.604567, + 4.531181 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2511, + "type": "imguizmo", + "translation": [ + 2.317685, + -0.549018, + 4.525285 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2512, + "type": "imguizmo", + "translation": [ + 2.304816, + -0.49194, + 4.519998 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2513, + "type": "imguizmo", + "translation": [ + 2.291875, + -0.43349, + 4.515324 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2514, + "type": "imguizmo", + "translation": [ + 2.278869, + -0.373832, + 4.511267 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2515, + "type": "imguizmo", + "translation": [ + 2.265808, + -0.313132, + 4.507829 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2516, + "type": "imguizmo", + "translation": [ + 2.252702, + -0.251559, + 4.505013 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2517, + "type": "imguizmo", + "translation": [ + 2.239559, + -0.189286, + 4.502821 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2518, + "type": "imguizmo", + "translation": [ + 2.226388, + -0.126484, + 4.501254 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2519, + "type": "imguizmo", + "translation": [ + 2.213198, + -0.06333, + 4.500314 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2520, + "type": "imguizmo", + "translation": [ + 2.2, + 0, + 4.5 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2521, + "type": "imguizmo", + "translation": [ + 2.186802, + 0.06333, + 4.500314 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2522, + "type": "imguizmo", + "translation": [ + 2.173612, + 0.126484, + 4.501254 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2523, + "type": "imguizmo", + "translation": [ + 2.160441, + 0.189286, + 4.502821 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2524, + "type": "imguizmo", + "translation": [ + 2.147298, + 0.251559, + 4.505013 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2525, + "type": "imguizmo", + "translation": [ + 2.134192, + 0.313132, + 4.507829 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2526, + "type": "imguizmo", + "translation": [ + 2.121131, + 0.373832, + 4.511267 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2527, + "type": "imguizmo", + "translation": [ + 2.108125, + 0.43349, + 4.515324 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2528, + "type": "imguizmo", + "translation": [ + 2.095184, + 0.49194, + 4.519998 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2529, + "type": "imguizmo", + "translation": [ + 2.082315, + 0.549018, + 4.525285 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2530, + "type": "imguizmo", + "translation": [ + 2.069528, + 0.604567, + 4.531181 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2531, + "type": "imguizmo", + "translation": [ + 2.056833, + 0.65843, + 4.537683 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2532, + "type": "imguizmo", + "translation": [ + 2.044237, + 0.710458, + 4.544786 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2533, + "type": "imguizmo", + "translation": [ + 2.031749, + 0.760506, + 4.552485 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2534, + "type": "imguizmo", + "translation": [ + 2.019379, + 0.808435, + 4.560775 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2535, + "type": "imguizmo", + "translation": [ + 2.007135, + 0.85411, + 4.56965 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2536, + "type": "imguizmo", + "translation": [ + 1.995025, + 0.897404, + 4.579103 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2537, + "type": "imguizmo", + "translation": [ + 1.983058, + 0.938198, + 4.589128 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2538, + "type": "imguizmo", + "translation": [ + 1.971242, + 0.976376, + 4.599718 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2539, + "type": "imguizmo", + "translation": [ + 1.959586, + 1.011833, + 4.610867 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2540, + "type": "imguizmo", + "translation": [ + 1.948097, + 1.04447, + 4.622565 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2541, + "type": "imguizmo", + "translation": [ + 1.936784, + 1.074196, + 4.634805 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2542, + "type": "imguizmo", + "translation": [ + 1.925654, + 1.100928, + 4.647578 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2543, + "type": "imguizmo", + "translation": [ + 1.914715, + 1.124591, + 4.660875 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2544, + "type": "imguizmo", + "translation": [ + 1.903976, + 1.14512, + 4.674688 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2545, + "type": "imguizmo", + "translation": [ + 1.893442, + 1.162457, + 4.689006 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2546, + "type": "imguizmo", + "translation": [ + 1.883122, + 1.176554, + 4.70382 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2547, + "type": "imguizmo", + "translation": [ + 1.873023, + 1.187372, + 4.719119 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2548, + "type": "imguizmo", + "translation": [ + 1.863152, + 1.194881, + 4.734892 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2549, + "type": "imguizmo", + "translation": [ + 1.853516, + 1.199059, + 4.751129 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2550, + "type": "imguizmo", + "translation": [ + 1.844121, + 1.199895, + 4.767818 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2551, + "type": "imguizmo", + "translation": [ + 1.834974, + 1.197387, + 4.784948 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2552, + "type": "imguizmo", + "translation": [ + 1.826081, + 1.191542, + 4.802506 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2553, + "type": "imguizmo", + "translation": [ + 1.81745, + 1.182375, + 4.820481 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2554, + "type": "imguizmo", + "translation": [ + 1.809084, + 1.169913, + 4.838859 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2555, + "type": "imguizmo", + "translation": [ + 1.800991, + 1.154191, + 4.857629 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2556, + "type": "imguizmo", + "translation": [ + 1.793177, + 1.135251, + 4.876776 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2557, + "type": "imguizmo", + "translation": [ + 1.785645, + 1.113147, + 4.896289 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2558, + "type": "imguizmo", + "translation": [ + 1.778403, + 1.087941, + 4.916152 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2559, + "type": "imguizmo", + "translation": [ + 1.771454, + 1.059702, + 4.936353 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2560, + "type": "imguizmo", + "translation": [ + 1.764804, + 1.02851, + 4.956876 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2561, + "type": "imguizmo", + "translation": [ + 1.758457, + 0.994451, + 4.977709 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2562, + "type": "imguizmo", + "translation": [ + 1.752418, + 0.957621, + 4.998835 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2563, + "type": "imguizmo", + "translation": [ + 1.746691, + 0.918121, + 5.020242 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2564, + "type": "imguizmo", + "translation": [ + 1.74128, + 0.876062, + 5.041913 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2565, + "type": "imguizmo", + "translation": [ + 1.736189, + 0.831562, + 5.063833 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2566, + "type": "imguizmo", + "translation": [ + 1.73142, + 0.784744, + 5.085988 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2567, + "type": "imguizmo", + "translation": [ + 1.726979, + 0.735739, + 5.108362 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2568, + "type": "imguizmo", + "translation": [ + 1.722867, + 0.684683, + 5.130939 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2569, + "type": "imguizmo", + "translation": [ + 1.719087, + 0.631719, + 5.153703 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2570, + "type": "imguizmo", + "translation": [ + 1.715643, + 0.576994, + 5.176639 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2571, + "type": "imguizmo", + "translation": [ + 1.712536, + 0.52066, + 5.199731 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2572, + "type": "imguizmo", + "translation": [ + 1.709769, + 0.462876, + 5.222962 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2573, + "type": "imguizmo", + "translation": [ + 1.707344, + 0.403802, + 5.246317 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2574, + "type": "imguizmo", + "translation": [ + 1.705261, + 0.343602, + 5.269779 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2575, + "type": "imguizmo", + "translation": [ + 1.703524, + 0.282444, + 5.293332 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2576, + "type": "imguizmo", + "translation": [ + 1.702133, + 0.220499, + 5.316958 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2577, + "type": "imguizmo", + "translation": [ + 1.701089, + 0.15794, + 5.340643 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2578, + "type": "imguizmo", + "translation": [ + 1.700392, + 0.09494, + 5.364369 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2579, + "type": "imguizmo", + "translation": [ + 1.700044, + 0.031676, + 5.38812 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2580, + "type": "imguizmo", + "translation": [ + 1.700044, + -0.031676, + 5.41188 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2581, + "type": "imguizmo", + "translation": [ + 1.700392, + -0.09494, + 5.435631 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2582, + "type": "imguizmo", + "translation": [ + 1.701089, + -0.15794, + 5.459357 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2583, + "type": "imguizmo", + "translation": [ + 1.702133, + -0.220499, + 5.483042 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2584, + "type": "imguizmo", + "translation": [ + 1.703524, + -0.282444, + 5.506668 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2585, + "type": "imguizmo", + "translation": [ + 1.705261, + -0.343602, + 5.530221 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2586, + "type": "imguizmo", + "translation": [ + 1.707344, + -0.403802, + 5.553683 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2587, + "type": "imguizmo", + "translation": [ + 1.709769, + -0.462876, + 5.577038 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2588, + "type": "imguizmo", + "translation": [ + 1.712536, + -0.52066, + 5.600269 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2589, + "type": "imguizmo", + "translation": [ + 1.715643, + -0.576994, + 5.623361 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2590, + "type": "imguizmo", + "translation": [ + 1.719087, + -0.631719, + 5.646297 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2591, + "type": "imguizmo", + "translation": [ + 1.722867, + -0.684683, + 5.669061 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2592, + "type": "imguizmo", + "translation": [ + 1.726979, + -0.735739, + 5.691638 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2593, + "type": "imguizmo", + "translation": [ + 1.73142, + -0.784744, + 5.714012 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2594, + "type": "imguizmo", + "translation": [ + 1.736189, + -0.831562, + 5.736167 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2595, + "type": "imguizmo", + "translation": [ + 1.74128, + -0.876062, + 5.758087 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2596, + "type": "imguizmo", + "translation": [ + 1.746691, + -0.918121, + 5.779758 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2597, + "type": "imguizmo", + "translation": [ + 1.752418, + -0.957621, + 5.801165 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2598, + "type": "imguizmo", + "translation": [ + 1.758457, + -0.994451, + 5.822291 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2599, + "type": "imguizmo", + "translation": [ + 1.764804, + -1.02851, + 5.843124 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2600, + "type": "imguizmo", + "translation": [ + 1.771454, + -1.059702, + 5.863647 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2601, + "type": "imguizmo", + "translation": [ + 1.778403, + -1.087941, + 5.883848 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2602, + "type": "imguizmo", + "translation": [ + 1.785645, + -1.113147, + 5.903711 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2603, + "type": "imguizmo", + "translation": [ + 1.793177, + -1.135251, + 5.923224 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2604, + "type": "imguizmo", + "translation": [ + 1.800991, + -1.154191, + 5.942371 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2605, + "type": "imguizmo", + "translation": [ + 1.809084, + -1.169913, + 5.961141 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2606, + "type": "imguizmo", + "translation": [ + 1.81745, + -1.182375, + 5.979519 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2607, + "type": "imguizmo", + "translation": [ + 1.826081, + -1.191542, + 5.997494 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2608, + "type": "imguizmo", + "translation": [ + 1.834974, + -1.197387, + 6.015052 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2609, + "type": "imguizmo", + "translation": [ + 1.844121, + -1.199895, + 6.032182 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2610, + "type": "imguizmo", + "translation": [ + 1.853516, + -1.199059, + 6.048871 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2611, + "type": "imguizmo", + "translation": [ + 1.863152, + -1.194881, + 6.065108 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2612, + "type": "imguizmo", + "translation": [ + 1.873023, + -1.187372, + 6.080881 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2613, + "type": "imguizmo", + "translation": [ + 1.883122, + -1.176554, + 6.09618 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2614, + "type": "imguizmo", + "translation": [ + 1.893442, + -1.162457, + 6.110994 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2615, + "type": "imguizmo", + "translation": [ + 1.903976, + -1.14512, + 6.125312 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2616, + "type": "imguizmo", + "translation": [ + 1.914715, + -1.124591, + 6.139125 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2617, + "type": "imguizmo", + "translation": [ + 1.925654, + -1.100928, + 6.152422 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2618, + "type": "imguizmo", + "translation": [ + 1.936784, + -1.074196, + 6.165195 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2619, + "type": "imguizmo", + "translation": [ + 1.948097, + -1.04447, + 6.177435 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2620, + "type": "imguizmo", + "translation": [ + 1.959586, + -1.011833, + 6.189133 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2621, + "type": "imguizmo", + "translation": [ + 1.971242, + -0.976376, + 6.200282 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2622, + "type": "imguizmo", + "translation": [ + 1.983058, + -0.938198, + 6.210872 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2623, + "type": "imguizmo", + "translation": [ + 1.995025, + -0.897404, + 6.220897 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2624, + "type": "imguizmo", + "translation": [ + 2.007135, + -0.85411, + 6.23035 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2625, + "type": "imguizmo", + "translation": [ + 2.019379, + -0.808435, + 6.239225 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2626, + "type": "imguizmo", + "translation": [ + 2.031749, + -0.760506, + 6.247515 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2627, + "type": "imguizmo", + "translation": [ + 2.044237, + -0.710458, + 6.255214 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2628, + "type": "imguizmo", + "translation": [ + 2.056833, + -0.65843, + 6.262317 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2629, + "type": "imguizmo", + "translation": [ + 2.069528, + -0.604567, + 6.268819 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2630, + "type": "imguizmo", + "translation": [ + 2.082315, + -0.549018, + 6.274715 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2631, + "type": "imguizmo", + "translation": [ + 2.095184, + -0.49194, + 6.280002 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2632, + "type": "imguizmo", + "translation": [ + 2.108125, + -0.43349, + 6.284676 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2633, + "type": "imguizmo", + "translation": [ + 2.121131, + -0.373832, + 6.288733 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2634, + "type": "imguizmo", + "translation": [ + 2.134192, + -0.313132, + 6.292171 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2635, + "type": "imguizmo", + "translation": [ + 2.147298, + -0.251559, + 6.294987 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2636, + "type": "imguizmo", + "translation": [ + 2.160441, + -0.189286, + 6.297179 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2637, + "type": "imguizmo", + "translation": [ + 2.173612, + -0.126484, + 6.298746 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2638, + "type": "imguizmo", + "translation": [ + 2.186802, + -0.06333, + 6.299686 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + { + "frame": 2639, + "type": "imguizmo", + "translation": [ + 2.2, + 0, + 6.3 + ], + "rotation_deg": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + } + ], + "checks": [ + { + "frame": 0, + "kind": "baseline" + }, + { + "frame": 1, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 2, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 3, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 4, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 5, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 6, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 7, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 8, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 9, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 10, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 11, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 12, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 13, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 14, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 15, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 16, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 17, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 18, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 19, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 20, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 21, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 22, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 23, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 24, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 25, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 26, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 27, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 28, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 29, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 30, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 31, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 32, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 33, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 34, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 35, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 36, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 37, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 38, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 39, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 40, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 41, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 42, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 43, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 44, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 45, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 46, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 47, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 48, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 49, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 50, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 51, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 52, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 53, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 54, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 55, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 56, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 57, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 58, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 59, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 60, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 61, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 62, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 63, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 64, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 65, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 66, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 67, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 68, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 69, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 70, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 71, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 72, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 73, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 74, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 75, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 76, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 77, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 78, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 79, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 80, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 81, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 82, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 83, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 84, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 85, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 86, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 87, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 88, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 89, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 90, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 91, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 92, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 93, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 94, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 95, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 96, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 97, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 98, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 99, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 100, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 101, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 102, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 103, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 104, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 105, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 106, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 107, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 108, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 109, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 110, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 111, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 112, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 113, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 114, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 115, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 116, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 117, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 118, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 119, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 120, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 121, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 122, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 123, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 124, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 125, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 126, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 127, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 128, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 129, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 130, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 131, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 132, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 133, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 134, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 135, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 136, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 137, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 138, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 139, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 140, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 141, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 142, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 143, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 144, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 145, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 146, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 147, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 148, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 149, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 150, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 151, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 152, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 153, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 154, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 155, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 156, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 157, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 158, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 159, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 160, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 161, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 162, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 163, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 164, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 165, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 166, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 167, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 168, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 169, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 170, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 171, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 172, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 173, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 174, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 175, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 176, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 177, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 178, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 179, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 180, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 181, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 182, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 183, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 184, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 185, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 186, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 187, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 188, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 189, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 190, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 191, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 192, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 193, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 194, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 195, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 196, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 197, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 198, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 199, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 200, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 201, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 202, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 203, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 204, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 205, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 206, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 207, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 208, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 209, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 210, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 211, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 212, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 213, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 214, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 215, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 216, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 217, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 218, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 219, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 220, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 221, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 222, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 223, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 224, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 225, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 226, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 227, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 228, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 229, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 230, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 231, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 232, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 233, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 234, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 235, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 236, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 237, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 238, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 239, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 240, + "kind": "baseline" + }, + { + "frame": 241, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 242, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 243, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 244, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 245, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 246, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 247, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 248, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 249, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 250, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 251, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 252, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 253, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 254, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 255, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 256, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 257, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 258, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 259, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 260, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 261, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 262, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 263, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 264, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 265, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 266, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 267, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 268, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 269, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 270, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 271, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 272, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 273, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 274, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 275, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 276, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 277, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 278, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 279, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 280, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 281, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 282, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 283, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 284, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 285, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 286, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 287, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 288, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 289, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 290, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 291, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 292, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 293, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 294, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 295, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 296, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 297, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 298, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 299, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 300, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 301, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 302, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 303, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 304, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 305, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 306, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 307, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 308, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 309, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 310, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 311, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 312, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 313, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 314, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 315, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 316, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 317, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 318, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 319, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 320, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 321, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 322, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 323, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 324, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 325, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 326, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 327, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 328, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 329, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 330, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 331, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 332, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 333, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 334, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 335, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 336, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 337, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 338, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 339, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 340, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 341, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 342, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 343, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 344, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 345, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 346, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 347, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 348, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 349, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 350, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 351, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 352, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 353, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 354, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 355, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 356, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 357, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 358, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 359, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 360, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 361, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 362, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 363, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 364, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 365, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 366, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 367, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 368, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 369, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 370, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 371, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 372, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 373, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 374, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 375, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 376, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 377, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 378, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 379, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 380, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 381, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 382, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 383, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 384, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 385, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 386, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 387, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 388, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 389, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 390, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 391, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 392, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 393, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 394, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 395, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 396, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 397, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 398, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 399, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 400, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 401, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 402, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 403, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 404, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 405, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 406, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 407, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 408, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 409, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 410, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 411, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 412, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 413, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 414, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 415, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 416, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 417, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 418, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 419, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 420, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 421, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 422, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 423, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 424, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 425, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 426, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 427, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 428, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 429, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 430, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 431, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 432, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 433, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 434, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 435, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 436, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 437, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 438, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 439, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 440, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 441, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 442, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 443, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 444, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 445, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 446, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 447, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 448, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 449, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 450, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 451, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 452, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 453, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 454, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 455, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 456, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 457, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 458, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 459, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 460, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 461, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 462, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 463, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 464, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 465, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 466, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 467, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 468, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 469, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 470, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 471, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 472, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 473, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 474, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 475, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 476, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 477, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 478, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 479, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 480, + "kind": "baseline" + }, + { + "frame": 481, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 482, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 483, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 484, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 485, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 486, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 487, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 488, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 489, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 490, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 491, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 492, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 493, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 494, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 495, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 496, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 497, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 498, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 499, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 500, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 501, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 502, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 503, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 504, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 505, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 506, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 507, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 508, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 509, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 510, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 511, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 512, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 513, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 514, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 515, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 516, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 517, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 518, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 519, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 520, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 521, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 522, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 523, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 524, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 525, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 526, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 527, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 528, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 529, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 530, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 531, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 532, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 533, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 534, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 535, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 536, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 537, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 538, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 539, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 540, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 541, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 542, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 543, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 544, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 545, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 546, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 547, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 548, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 549, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 550, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 551, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 552, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 553, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 554, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 555, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 556, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 557, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 558, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 559, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 560, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 561, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 562, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 563, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 564, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 565, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 566, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 567, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 568, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 569, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 570, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 571, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 572, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 573, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 574, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 575, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 576, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 577, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 578, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 579, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 580, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 581, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 582, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 583, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 584, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 585, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 586, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 587, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 588, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 589, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 590, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 591, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 592, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 593, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 594, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 595, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 596, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 597, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 598, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 599, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 600, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 601, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 602, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 603, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 604, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 605, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 606, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 607, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 608, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 609, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 610, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 611, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 612, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 613, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 614, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 615, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 616, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 617, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 618, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 619, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 620, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 621, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 622, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1970, - "type": "imguizmo", - "translation": [ - -0.017180904570802895, - 0.07647311774310708, - 0.5940000000000001 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1.0, - 1.0, - 1.0 - ] + "frame": 623, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1971, - "type": "imguizmo", - "translation": [ - -0.015309878844776578, - 0.07704662002630651, - 0.5940000000000001 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1.0, - 1.0, - 1.0 - ] + "frame": 624, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1972, - "type": "imguizmo", - "translation": [ - -0.013419778916156347, - 0.0775537338164139, - 0.5940000000000001 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1.0, - 1.0, - 1.0 - ] + "frame": 625, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1973, - "type": "imguizmo", - "translation": [ - -0.011512959614090556, - 0.07799382731279461, - 0.5940000000000001 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1.0, - 1.0, - 1.0 - ] + "frame": 626, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1974, - "type": "imguizmo", - "translation": [ - -0.009591796597984887, - 0.07836635221375433, - 0.5940000000000001 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1.0, - 1.0, - 1.0 - ] + "frame": 627, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1975, - "type": "imguizmo", - "translation": [ - -0.0076586833977266445, - 0.07867084439965473, - 0.5940000000000001 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1.0, - 1.0, - 1.0 - ] + "frame": 628, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1976, - "type": "imguizmo", - "translation": [ - -0.005716028431644335, - 0.07890692451114875, - 0.5940000000000001 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1.0, - 1.0, - 1.0 - ] + "frame": 629, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1977, - "type": "imguizmo", - "translation": [ - -0.0037662520059186773, - 0.07907429842181513, - 0.5940000000000001 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1.0, - 1.0, - 1.0 - ] + "frame": 630, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1978, - "type": "imguizmo", - "translation": [ - -0.0018117832991822454, - 0.07917275760460321, - 0.5940000000000001 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1.0, - 1.0, - 1.0 - ] + "frame": 631, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1979, - "type": "imguizmo", - "translation": [ - 0.0001449426639345431, - 0.07920217939163175, - 0.5940000000000001 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1.0, - 1.0, - 1.0 - ] - } - ], - "checks": [ + "frame": 632, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, { - "frame": 0, - "kind": "baseline" + "frame": 633, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1, + "frame": 634, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 2, + "frame": 635, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 3, + "frame": 636, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 4, + "frame": 637, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 5, + "frame": 638, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 6, + "frame": 639, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 7, + "frame": 640, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 8, + "frame": 641, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 9, + "frame": 642, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 10, + "frame": 643, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 11, + "frame": 644, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 12, + "frame": 645, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 13, + "frame": 646, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 14, + "frame": 647, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 15, + "frame": 648, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 16, + "frame": 649, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 17, + "frame": 650, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 18, + "frame": 651, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 19, + "frame": 652, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 20, + "frame": 653, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 21, + "frame": 654, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 22, + "frame": 655, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 23, + "frame": 656, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 24, + "frame": 657, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 25, + "frame": 658, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 26, + "frame": 659, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 27, + "frame": 660, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 28, + "frame": 661, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 29, + "frame": 662, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 30, + "frame": 663, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 31, + "frame": 664, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 32, + "frame": 665, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 33, + "frame": 666, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 34, + "frame": 667, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 35, + "frame": 668, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 36, + "frame": 669, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 37, + "frame": 670, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 38, + "frame": 671, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 39, + "frame": 672, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 40, + "frame": 673, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 41, + "frame": 674, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 42, + "frame": 675, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 43, + "frame": 676, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 44, + "frame": 677, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 45, + "frame": 678, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 46, + "frame": 679, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 47, + "frame": 680, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 48, + "frame": 681, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 49, + "frame": 682, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 50, + "frame": 683, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 51, + "frame": 684, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 52, + "frame": 685, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 53, + "frame": 686, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 54, + "frame": 687, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 55, + "frame": 688, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 56, + "frame": 689, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 57, + "frame": 690, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 58, + "frame": 691, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 59, + "frame": 692, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 60, + "frame": 693, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 61, + "frame": 694, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 62, + "frame": 695, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 63, + "frame": 696, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 64, + "frame": 697, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 65, + "frame": 698, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 66, + "frame": 699, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 67, + "frame": 700, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 68, + "frame": 701, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 69, + "frame": 702, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 70, + "frame": 703, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 71, + "frame": 704, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 72, + "frame": 705, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 73, + "frame": 706, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 74, + "frame": 707, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 75, + "frame": 708, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 76, + "frame": 709, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 77, + "frame": 710, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 78, + "frame": 711, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 79, + "frame": 712, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 80, + "frame": 713, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 81, + "frame": 714, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 82, + "frame": 715, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 83, + "frame": 716, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 84, + "frame": 717, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 85, + "frame": 718, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 86, + "frame": 719, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 87, + "frame": 720, + "kind": "baseline" + }, + { + "frame": 721, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 88, + "frame": 722, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 89, + "frame": 723, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 90, + "frame": 724, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 91, + "frame": 725, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 92, + "frame": 726, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 93, + "frame": 727, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 94, + "frame": 728, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 95, + "frame": 729, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 96, + "frame": 730, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 97, + "frame": 731, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 98, + "frame": 732, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 99, + "frame": 733, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 100, + "frame": 734, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 101, + "frame": 735, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 102, + "frame": 736, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 103, + "frame": 737, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 104, + "frame": 738, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 105, + "frame": 739, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 106, + "frame": 740, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 107, + "frame": 741, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 108, + "frame": 742, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 109, + "frame": 743, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 110, + "frame": 744, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 111, + "frame": 745, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 112, + "frame": 746, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 113, + "frame": 747, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 114, + "frame": 748, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 115, + "frame": 749, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 116, + "frame": 750, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 117, + "frame": 751, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 118, + "frame": 752, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 119, + "frame": 753, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 120, + "frame": 754, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 121, + "frame": 755, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 122, + "frame": 756, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 123, + "frame": 757, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 124, + "frame": 758, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 125, + "frame": 759, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 126, + "frame": 760, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 761, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 762, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 763, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 764, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 765, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 766, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 767, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 768, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 769, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 770, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 771, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 772, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 773, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 774, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 775, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 776, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 777, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 778, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 779, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 780, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 781, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 782, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 783, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 784, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 785, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 + }, + { + "frame": 786, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 127, + "frame": 787, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 128, + "frame": 788, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 129, + "frame": 789, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 130, + "frame": 790, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 131, + "frame": 791, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 132, + "frame": 792, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 133, + "frame": 793, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 134, + "frame": 794, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 135, + "frame": 795, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 136, + "frame": 796, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 137, + "frame": 797, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 138, + "frame": 798, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 139, + "frame": 799, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 140, + "frame": 800, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 141, + "frame": 801, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 142, + "frame": 802, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 143, + "frame": 803, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 144, + "frame": 804, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 145, + "frame": 805, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 146, + "frame": 806, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 147, + "frame": 807, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 148, + "frame": 808, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 149, + "frame": 809, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 150, + "frame": 810, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 151, + "frame": 811, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 152, + "frame": 812, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 153, + "frame": 813, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 154, + "frame": 814, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 155, + "frame": 815, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 156, + "frame": 816, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 157, + "frame": 817, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 158, + "frame": 818, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 159, + "frame": 819, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 160, + "frame": 820, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 161, + "frame": 821, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 162, + "frame": 822, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 163, + "frame": 823, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 164, + "frame": 824, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 165, + "frame": 825, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 166, + "frame": 826, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 167, + "frame": 827, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 168, + "frame": 828, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 169, + "frame": 829, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 170, + "frame": 830, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 171, + "frame": 831, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 172, + "frame": 832, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 173, + "frame": 833, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 174, + "frame": 834, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 175, + "frame": 835, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 176, + "frame": 836, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 177, + "frame": 837, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 178, + "frame": 838, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 179, + "frame": 839, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 180, - "kind": "baseline" + "frame": 840, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 181, + "frame": 841, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 182, + "frame": 842, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 183, + "frame": 843, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 184, + "frame": 844, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 185, + "frame": 845, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 186, + "frame": 846, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 187, + "frame": 847, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 188, + "frame": 848, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 189, + "frame": 849, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 190, + "frame": 850, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 191, + "frame": 851, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 192, + "frame": 852, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 193, + "frame": 853, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 194, + "frame": 854, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 195, + "frame": 855, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 196, + "frame": 856, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 197, + "frame": 857, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 198, + "frame": 858, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 199, + "frame": 859, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 200, + "frame": 860, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 201, + "frame": 861, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 202, + "frame": 862, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 203, + "frame": 863, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 204, + "frame": 864, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 205, + "frame": 865, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 206, + "frame": 866, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 207, + "frame": 867, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 208, + "frame": 868, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 209, + "frame": 869, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 210, + "frame": 870, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 211, + "frame": 871, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 212, + "frame": 872, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 213, + "frame": 873, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 214, + "frame": 874, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 215, + "frame": 875, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 216, + "frame": 876, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 217, + "frame": 877, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 218, + "frame": 878, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 219, + "frame": 879, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 220, + "frame": 880, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 221, + "frame": 881, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 222, + "frame": 882, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 223, + "frame": 883, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 224, + "frame": 884, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 225, + "frame": 885, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 226, + "frame": 886, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 227, + "frame": 887, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 228, + "frame": 888, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 229, + "frame": 889, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 230, + "frame": 890, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 231, + "frame": 891, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 232, + "frame": 892, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 233, + "frame": 893, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 234, + "frame": 894, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 235, + "frame": 895, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 236, + "frame": 896, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 237, + "frame": 897, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 238, + "frame": 898, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 239, + "frame": 899, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 240, + "frame": 900, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 241, + "frame": 901, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 242, + "frame": 902, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 243, + "frame": 903, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 244, + "frame": 904, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 245, + "frame": 905, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 246, + "frame": 906, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 247, + "frame": 907, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 248, + "frame": 908, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 249, + "frame": 909, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 250, + "frame": 910, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 251, + "frame": 911, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 252, + "frame": 912, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 253, + "frame": 913, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 254, + "frame": 914, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 255, + "frame": 915, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 256, + "frame": 916, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 257, + "frame": 917, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 258, + "frame": 918, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 259, + "frame": 919, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 260, + "frame": 920, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 261, + "frame": 921, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 262, + "frame": 922, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 263, + "frame": 923, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 264, + "frame": 924, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 265, + "frame": 925, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 266, + "frame": 926, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 267, + "frame": 927, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 268, + "frame": 928, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 269, + "frame": 929, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 270, + "frame": 930, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 271, + "frame": 931, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 272, + "frame": 932, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 273, + "frame": 933, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 274, + "frame": 934, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 275, + "frame": 935, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 276, + "frame": 936, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 277, + "frame": 937, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 278, + "frame": 938, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 279, + "frame": 939, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 280, + "frame": 940, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 281, + "frame": 941, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 282, + "frame": 942, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 283, + "frame": 943, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 284, + "frame": 944, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 285, + "frame": 945, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 286, + "frame": 946, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 287, + "frame": 947, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 288, + "frame": 948, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 289, + "frame": 949, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 290, + "frame": 950, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 291, + "frame": 951, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 292, + "frame": 952, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 293, + "frame": 953, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 294, + "frame": 954, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 295, + "frame": 955, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 296, + "frame": 956, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 297, + "frame": 957, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 298, + "frame": 958, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 299, + "frame": 959, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 300, - "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "frame": 960, + "kind": "baseline" }, { - "frame": 301, + "frame": 961, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 302, + "frame": 962, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 303, + "frame": 963, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 304, + "frame": 964, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 305, + "frame": 965, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 306, + "frame": 966, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 307, + "frame": 967, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 308, + "frame": 968, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 309, + "frame": 969, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 310, + "frame": 970, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 311, + "frame": 971, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 312, + "frame": 972, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 313, + "frame": 973, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 314, + "frame": 974, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 315, + "frame": 975, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 316, + "frame": 976, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 317, + "frame": 977, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 318, + "frame": 978, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 319, + "frame": 979, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 320, + "frame": 980, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 321, + "frame": 981, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 322, + "frame": 982, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 323, + "frame": 983, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 324, + "frame": 984, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 325, + "frame": 985, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 326, + "frame": 986, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 327, + "frame": 987, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 328, + "frame": 988, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 329, + "frame": 989, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 330, + "frame": 990, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 331, + "frame": 991, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 332, + "frame": 992, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 333, + "frame": 993, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 334, + "frame": 994, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 335, + "frame": 995, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 336, + "frame": 996, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 337, + "frame": 997, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 338, + "frame": 998, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 339, + "frame": 999, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 340, + "frame": 1000, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 341, + "frame": 1001, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 342, + "frame": 1002, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 343, + "frame": 1003, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 344, + "frame": 1004, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 345, + "frame": 1005, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 346, + "frame": 1006, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 347, + "frame": 1007, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 348, + "frame": 1008, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 349, + "frame": 1009, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 350, + "frame": 1010, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 351, + "frame": 1011, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 352, + "frame": 1012, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 353, + "frame": 1013, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 354, + "frame": 1014, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 355, + "frame": 1015, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 356, + "frame": 1016, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 357, + "frame": 1017, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 358, + "frame": 1018, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 359, + "frame": 1019, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 360, - "kind": "baseline" + "frame": 1020, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 361, + "frame": 1021, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 362, + "frame": 1022, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 363, + "frame": 1023, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 364, + "frame": 1024, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 365, + "frame": 1025, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 366, + "frame": 1026, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 367, + "frame": 1027, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 368, + "frame": 1028, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 369, + "frame": 1029, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 370, + "frame": 1030, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 371, + "frame": 1031, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 372, + "frame": 1032, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 373, + "frame": 1033, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 374, + "frame": 1034, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 375, + "frame": 1035, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 376, + "frame": 1036, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 377, + "frame": 1037, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 378, + "frame": 1038, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 379, + "frame": 1039, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 380, + "frame": 1040, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 381, + "frame": 1041, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 382, + "frame": 1042, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 383, + "frame": 1043, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 384, + "frame": 1044, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 385, + "frame": 1045, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 386, + "frame": 1046, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 387, + "frame": 1047, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 388, + "frame": 1048, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 389, + "frame": 1049, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 390, + "frame": 1050, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 391, + "frame": 1051, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 392, + "frame": 1052, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 393, + "frame": 1053, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 394, + "frame": 1054, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 395, + "frame": 1055, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 396, + "frame": 1056, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 397, + "frame": 1057, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 398, + "frame": 1058, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 399, + "frame": 1059, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 400, + "frame": 1060, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 401, + "frame": 1061, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 402, + "frame": 1062, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 403, + "frame": 1063, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 404, + "frame": 1064, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 405, + "frame": 1065, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 406, + "frame": 1066, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 407, + "frame": 1067, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 408, + "frame": 1068, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 409, + "frame": 1069, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 410, + "frame": 1070, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 411, + "frame": 1071, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 412, + "frame": 1072, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 413, + "frame": 1073, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 414, + "frame": 1074, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 415, + "frame": 1075, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 416, + "frame": 1076, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 417, + "frame": 1077, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 418, + "frame": 1078, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 419, + "frame": 1079, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 420, + "frame": 1080, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 421, + "frame": 1081, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 422, + "frame": 1082, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 423, + "frame": 1083, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 424, + "frame": 1084, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 425, + "frame": 1085, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 426, + "frame": 1086, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 427, + "frame": 1087, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 428, + "frame": 1088, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 429, + "frame": 1089, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 430, + "frame": 1090, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 431, + "frame": 1091, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 432, + "frame": 1092, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 433, + "frame": 1093, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 434, + "frame": 1094, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 435, + "frame": 1095, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 436, + "frame": 1096, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 437, + "frame": 1097, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 438, + "frame": 1098, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 439, + "frame": 1099, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 440, + "frame": 1100, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 441, + "frame": 1101, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 442, + "frame": 1102, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 443, + "frame": 1103, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 444, + "frame": 1104, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 445, + "frame": 1105, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 446, + "frame": 1106, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 447, + "frame": 1107, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 448, + "frame": 1108, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 449, + "frame": 1109, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 450, + "frame": 1110, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 451, + "frame": 1111, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 452, + "frame": 1112, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 453, + "frame": 1113, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 454, + "frame": 1114, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 455, + "frame": 1115, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 456, + "frame": 1116, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 457, + "frame": 1117, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 458, + "frame": 1118, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 459, + "frame": 1119, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 460, + "frame": 1120, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 461, + "frame": 1121, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 462, + "frame": 1122, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 463, + "frame": 1123, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 464, + "frame": 1124, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 465, + "frame": 1125, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 466, + "frame": 1126, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 467, + "frame": 1127, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 468, + "frame": 1128, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 469, + "frame": 1129, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 470, + "frame": 1130, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 471, + "frame": 1131, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 472, + "frame": 1132, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 473, + "frame": 1133, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 474, + "frame": 1134, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 475, + "frame": 1135, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 476, + "frame": 1136, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 477, + "frame": 1137, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 478, + "frame": 1138, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 479, + "frame": 1139, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 480, + "frame": 1140, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 481, + "frame": 1141, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 482, + "frame": 1142, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 483, + "frame": 1143, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 484, + "frame": 1144, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 485, + "frame": 1145, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 486, + "frame": 1146, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 487, + "frame": 1147, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 488, + "frame": 1148, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 489, + "frame": 1149, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 490, + "frame": 1150, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 491, + "frame": 1151, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 492, + "frame": 1152, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 493, + "frame": 1153, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 494, + "frame": 1154, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 495, + "frame": 1155, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 496, + "frame": 1156, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 497, + "frame": 1157, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 498, + "frame": 1158, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 499, + "frame": 1159, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 500, + "frame": 1160, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 501, + "frame": 1161, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 502, + "frame": 1162, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 503, + "frame": 1163, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 504, + "frame": 1164, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 505, + "frame": 1165, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 506, + "frame": 1166, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 507, + "frame": 1167, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 508, + "frame": 1168, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 509, + "frame": 1169, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 510, + "frame": 1170, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 511, + "frame": 1171, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 512, + "frame": 1172, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 513, + "frame": 1173, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 514, + "frame": 1174, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 515, + "frame": 1175, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 516, + "frame": 1176, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 517, + "frame": 1177, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 518, + "frame": 1178, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 519, + "frame": 1179, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 520, + "frame": 1180, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 521, + "frame": 1181, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 522, + "frame": 1182, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 523, + "frame": 1183, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 524, + "frame": 1184, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 525, + "frame": 1185, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 526, + "frame": 1186, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 527, + "frame": 1187, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 528, + "frame": 1188, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 529, + "frame": 1189, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 530, + "frame": 1190, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 531, + "frame": 1191, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 532, + "frame": 1192, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 533, + "frame": 1193, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 534, + "frame": 1194, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 535, + "frame": 1195, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 536, + "frame": 1196, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 537, + "frame": 1197, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 538, + "frame": 1198, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 539, + "frame": 1199, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 540, + "frame": 1200, "kind": "baseline" }, { - "frame": 541, + "frame": 1201, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 542, + "frame": 1202, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 543, + "frame": 1203, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 544, + "frame": 1204, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 545, + "frame": 1205, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 546, + "frame": 1206, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 547, + "frame": 1207, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 548, + "frame": 1208, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 549, + "frame": 1209, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 550, + "frame": 1210, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 551, + "frame": 1211, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 552, + "frame": 1212, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 553, + "frame": 1213, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 554, + "frame": 1214, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 555, + "frame": 1215, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 556, + "frame": 1216, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 557, + "frame": 1217, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 558, + "frame": 1218, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 559, + "frame": 1219, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 560, + "frame": 1220, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 561, + "frame": 1221, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 562, + "frame": 1222, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 563, + "frame": 1223, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 564, + "frame": 1224, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 565, + "frame": 1225, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 566, + "frame": 1226, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 567, + "frame": 1227, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 568, + "frame": 1228, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 569, + "frame": 1229, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 570, + "frame": 1230, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 571, + "frame": 1231, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 572, + "frame": 1232, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 573, + "frame": 1233, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 574, + "frame": 1234, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 575, + "frame": 1235, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 576, + "frame": 1236, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 577, + "frame": 1237, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 578, + "frame": 1238, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 579, + "frame": 1239, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 580, + "frame": 1240, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 581, + "frame": 1241, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 582, + "frame": 1242, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 583, + "frame": 1243, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 584, + "frame": 1244, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 585, + "frame": 1245, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 586, + "frame": 1246, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 587, + "frame": 1247, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 588, + "frame": 1248, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 589, + "frame": 1249, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 590, + "frame": 1250, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 591, + "frame": 1251, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 592, + "frame": 1252, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 593, + "frame": 1253, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 594, + "frame": 1254, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 595, + "frame": 1255, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 596, + "frame": 1256, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 597, + "frame": 1257, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 598, + "frame": 1258, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 599, + "frame": 1259, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 600, + "frame": 1260, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 601, + "frame": 1261, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 602, + "frame": 1262, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 603, + "frame": 1263, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 604, + "frame": 1264, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 605, + "frame": 1265, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 606, + "frame": 1266, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 607, + "frame": 1267, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 608, + "frame": 1268, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 609, + "frame": 1269, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 610, + "frame": 1270, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 611, + "frame": 1271, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 612, + "frame": 1272, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 613, + "frame": 1273, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 614, + "frame": 1274, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 615, + "frame": 1275, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 616, + "frame": 1276, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 617, + "frame": 1277, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 618, + "frame": 1278, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 619, + "frame": 1279, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 620, + "frame": 1280, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 621, + "frame": 1281, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 622, + "frame": 1282, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 623, + "frame": 1283, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 624, + "frame": 1284, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 625, + "frame": 1285, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 626, + "frame": 1286, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 627, + "frame": 1287, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 628, + "frame": 1288, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 629, + "frame": 1289, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 630, + "frame": 1290, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 631, + "frame": 1291, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 632, + "frame": 1292, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 633, + "frame": 1293, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 634, + "frame": 1294, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 635, + "frame": 1295, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 636, + "frame": 1296, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 637, + "frame": 1297, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 638, + "frame": 1298, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 639, + "frame": 1299, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 640, + "frame": 1300, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 641, + "frame": 1301, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 642, + "frame": 1302, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 643, + "frame": 1303, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 644, + "frame": 1304, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 645, + "frame": 1305, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 646, + "frame": 1306, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 647, + "frame": 1307, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 648, + "frame": 1308, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 649, + "frame": 1309, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 650, + "frame": 1310, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 651, + "frame": 1311, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 652, + "frame": 1312, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 653, + "frame": 1313, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 654, + "frame": 1314, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 655, + "frame": 1315, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 656, + "frame": 1316, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 657, + "frame": 1317, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 658, + "frame": 1318, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 659, + "frame": 1319, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 660, + "frame": 1320, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 661, + "frame": 1321, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 662, + "frame": 1322, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 663, + "frame": 1323, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 664, + "frame": 1324, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 665, + "frame": 1325, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 666, + "frame": 1326, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 667, + "frame": 1327, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 668, + "frame": 1328, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 669, + "frame": 1329, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 670, + "frame": 1330, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 671, + "frame": 1331, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 672, + "frame": 1332, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 673, + "frame": 1333, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 674, + "frame": 1334, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 675, + "frame": 1335, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 676, + "frame": 1336, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 677, + "frame": 1337, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 678, + "frame": 1338, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 679, + "frame": 1339, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 680, + "frame": 1340, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 681, + "frame": 1341, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 682, + "frame": 1342, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 683, + "frame": 1343, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 684, + "frame": 1344, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 685, + "frame": 1345, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 686, + "frame": 1346, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 687, + "frame": 1347, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 688, + "frame": 1348, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 689, + "frame": 1349, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 690, + "frame": 1350, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 691, + "frame": 1351, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 692, + "frame": 1352, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 693, + "frame": 1353, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 694, + "frame": 1354, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 695, + "frame": 1355, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 696, + "frame": 1356, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 697, + "frame": 1357, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 698, + "frame": 1358, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 699, + "frame": 1359, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 700, + "frame": 1360, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 701, + "frame": 1361, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 702, + "frame": 1362, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 703, + "frame": 1363, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 704, + "frame": 1364, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 705, + "frame": 1365, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 706, + "frame": 1366, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 707, + "frame": 1367, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 708, + "frame": 1368, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 709, + "frame": 1369, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 710, + "frame": 1370, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 711, + "frame": 1371, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 712, + "frame": 1372, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 713, + "frame": 1373, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 714, + "frame": 1374, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 715, + "frame": 1375, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 716, + "frame": 1376, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 717, + "frame": 1377, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 718, + "frame": 1378, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 719, + "frame": 1379, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 720, - "kind": "baseline" + "frame": 1380, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 721, + "frame": 1381, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 722, + "frame": 1382, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 723, + "frame": 1383, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 724, + "frame": 1384, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 725, + "frame": 1385, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 726, + "frame": 1386, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 727, + "frame": 1387, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 728, + "frame": 1388, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 729, + "frame": 1389, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 730, + "frame": 1390, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 731, + "frame": 1391, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 732, + "frame": 1392, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 733, + "frame": 1393, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 734, + "frame": 1394, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 735, + "frame": 1395, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 736, + "frame": 1396, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 737, + "frame": 1397, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 738, + "frame": 1398, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 739, + "frame": 1399, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 740, + "frame": 1400, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 741, + "frame": 1401, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 742, + "frame": 1402, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 743, + "frame": 1403, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 744, + "frame": 1404, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 745, + "frame": 1405, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 746, + "frame": 1406, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 747, + "frame": 1407, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 748, + "frame": 1408, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 749, + "frame": 1409, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 750, + "frame": 1410, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 751, + "frame": 1411, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 752, + "frame": 1412, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 753, + "frame": 1413, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 754, + "frame": 1414, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 755, + "frame": 1415, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 756, + "frame": 1416, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 757, + "frame": 1417, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 758, + "frame": 1418, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 759, + "frame": 1419, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 760, + "frame": 1420, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 761, + "frame": 1421, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 762, + "frame": 1422, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 763, + "frame": 1423, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 764, + "frame": 1424, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 765, + "frame": 1425, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 766, + "frame": 1426, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 767, + "frame": 1427, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 768, + "frame": 1428, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 769, + "frame": 1429, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 770, + "frame": 1430, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 771, + "frame": 1431, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 772, + "frame": 1432, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 773, + "frame": 1433, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 774, + "frame": 1434, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 775, + "frame": 1435, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 776, + "frame": 1436, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 777, + "frame": 1437, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 778, + "frame": 1438, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 779, + "frame": 1439, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 780, - "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "frame": 1440, + "kind": "baseline" }, { - "frame": 781, + "frame": 1441, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 782, + "frame": 1442, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 783, + "frame": 1443, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 784, + "frame": 1444, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 785, + "frame": 1445, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 786, + "frame": 1446, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 787, + "frame": 1447, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 788, + "frame": 1448, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 789, + "frame": 1449, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 790, + "frame": 1450, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 791, + "frame": 1451, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 792, + "frame": 1452, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 793, + "frame": 1453, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 794, + "frame": 1454, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 795, + "frame": 1455, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 796, + "frame": 1456, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 797, + "frame": 1457, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 798, + "frame": 1458, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 799, + "frame": 1459, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 800, + "frame": 1460, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 801, + "frame": 1461, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 802, + "frame": 1462, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 803, + "frame": 1463, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 804, + "frame": 1464, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 805, + "frame": 1465, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 806, + "frame": 1466, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 807, + "frame": 1467, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 808, + "frame": 1468, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 809, + "frame": 1469, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 810, + "frame": 1470, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 811, + "frame": 1471, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 812, + "frame": 1472, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 813, + "frame": 1473, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 814, + "frame": 1474, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 815, + "frame": 1475, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 816, + "frame": 1476, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 817, + "frame": 1477, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 818, + "frame": 1478, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 819, + "frame": 1479, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 820, + "frame": 1480, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 821, + "frame": 1481, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 822, + "frame": 1482, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 823, + "frame": 1483, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 824, + "frame": 1484, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 825, + "frame": 1485, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 826, + "frame": 1486, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 827, + "frame": 1487, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 828, + "frame": 1488, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 829, + "frame": 1489, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 830, + "frame": 1490, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 831, + "frame": 1491, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 832, + "frame": 1492, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 833, + "frame": 1493, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 834, + "frame": 1494, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 835, + "frame": 1495, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 836, + "frame": 1496, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 837, + "frame": 1497, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 838, + "frame": 1498, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 839, + "frame": 1499, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 840, + "frame": 1500, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 841, + "frame": 1501, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 842, + "frame": 1502, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 843, + "frame": 1503, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 844, + "frame": 1504, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 845, + "frame": 1505, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 846, + "frame": 1506, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 847, + "frame": 1507, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 848, + "frame": 1508, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 849, + "frame": 1509, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 850, + "frame": 1510, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 851, + "frame": 1511, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 852, + "frame": 1512, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 853, + "frame": 1513, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 854, + "frame": 1514, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 855, + "frame": 1515, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 856, + "frame": 1516, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 857, + "frame": 1517, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 858, + "frame": 1518, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 859, + "frame": 1519, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 860, + "frame": 1520, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 861, + "frame": 1521, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 862, + "frame": 1522, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 863, + "frame": 1523, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 864, + "frame": 1524, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 865, + "frame": 1525, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 866, + "frame": 1526, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 867, + "frame": 1527, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 868, + "frame": 1528, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 869, + "frame": 1529, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 870, + "frame": 1530, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 871, + "frame": 1531, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 872, + "frame": 1532, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 873, + "frame": 1533, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 874, + "frame": 1534, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 875, + "frame": 1535, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 876, + "frame": 1536, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 877, + "frame": 1537, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 878, + "frame": 1538, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 879, + "frame": 1539, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 880, + "frame": 1540, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 881, + "frame": 1541, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 882, + "frame": 1542, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 883, + "frame": 1543, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 884, + "frame": 1544, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 885, + "frame": 1545, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 886, + "frame": 1546, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 887, + "frame": 1547, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 888, + "frame": 1548, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 889, + "frame": 1549, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 890, + "frame": 1550, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 891, + "frame": 1551, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 892, + "frame": 1552, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 893, + "frame": 1553, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 894, + "frame": 1554, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 895, + "frame": 1555, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 896, + "frame": 1556, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 897, + "frame": 1557, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 898, + "frame": 1558, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 899, + "frame": 1559, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 900, - "kind": "baseline" + "frame": 1560, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 901, + "frame": 1561, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 902, + "frame": 1562, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 903, + "frame": 1563, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 904, + "frame": 1564, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 905, + "frame": 1565, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 906, + "frame": 1566, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 907, + "frame": 1567, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 908, + "frame": 1568, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 909, + "frame": 1569, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 910, + "frame": 1570, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 911, + "frame": 1571, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 912, + "frame": 1572, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 913, + "frame": 1573, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 914, + "frame": 1574, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 915, + "frame": 1575, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 916, + "frame": 1576, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 917, + "frame": 1577, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 918, + "frame": 1578, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 919, + "frame": 1579, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 920, + "frame": 1580, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 921, + "frame": 1581, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 922, + "frame": 1582, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 923, + "frame": 1583, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 924, + "frame": 1584, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 925, + "frame": 1585, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 926, + "frame": 1586, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 927, + "frame": 1587, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 928, + "frame": 1588, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 929, + "frame": 1589, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 930, + "frame": 1590, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 931, + "frame": 1591, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 932, + "frame": 1592, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 933, + "frame": 1593, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 934, + "frame": 1594, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 935, + "frame": 1595, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 936, + "frame": 1596, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 937, + "frame": 1597, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 938, + "frame": 1598, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 939, + "frame": 1599, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 940, + "frame": 1600, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 941, + "frame": 1601, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 942, + "frame": 1602, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 943, + "frame": 1603, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 944, + "frame": 1604, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 945, + "frame": 1605, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 946, + "frame": 1606, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 947, + "frame": 1607, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 948, + "frame": 1608, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 949, + "frame": 1609, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 950, + "frame": 1610, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 951, + "frame": 1611, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 952, + "frame": 1612, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 953, + "frame": 1613, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 954, + "frame": 1614, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 955, + "frame": 1615, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 956, + "frame": 1616, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 957, + "frame": 1617, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 958, + "frame": 1618, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 959, + "frame": 1619, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 960, + "frame": 1620, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 961, + "frame": 1621, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 962, + "frame": 1622, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 963, + "frame": 1623, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 964, + "frame": 1624, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 965, + "frame": 1625, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 966, + "frame": 1626, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 967, + "frame": 1627, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 968, + "frame": 1628, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 969, + "frame": 1629, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 970, + "frame": 1630, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 971, + "frame": 1631, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 972, + "frame": 1632, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 973, + "frame": 1633, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 974, + "frame": 1634, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 975, + "frame": 1635, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 976, + "frame": 1636, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 977, + "frame": 1637, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 978, + "frame": 1638, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 979, + "frame": 1639, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 980, + "frame": 1640, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 981, + "frame": 1641, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 982, + "frame": 1642, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 983, + "frame": 1643, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 984, + "frame": 1644, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 985, + "frame": 1645, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 986, + "frame": 1646, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 987, + "frame": 1647, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 988, + "frame": 1648, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 989, + "frame": 1649, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 990, + "frame": 1650, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 991, + "frame": 1651, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 992, + "frame": 1652, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 993, + "frame": 1653, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 994, + "frame": 1654, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 995, + "frame": 1655, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 996, + "frame": 1656, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 997, + "frame": 1657, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 998, + "frame": 1658, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 999, + "frame": 1659, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1000, + "frame": 1660, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1001, + "frame": 1661, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1002, + "frame": 1662, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1003, + "frame": 1663, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1004, + "frame": 1664, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1005, + "frame": 1665, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1006, + "frame": 1666, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1007, + "frame": 1667, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1008, + "frame": 1668, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1009, + "frame": 1669, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1010, + "frame": 1670, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1011, + "frame": 1671, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1012, + "frame": 1672, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1013, + "frame": 1673, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1014, + "frame": 1674, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1015, + "frame": 1675, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1016, + "frame": 1676, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1017, + "frame": 1677, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1018, + "frame": 1678, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1019, + "frame": 1679, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1020, - "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "frame": 1680, + "kind": "baseline" }, { - "frame": 1021, + "frame": 1681, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1022, + "frame": 1682, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1023, + "frame": 1683, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1024, + "frame": 1684, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1025, + "frame": 1685, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1026, + "frame": 1686, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1027, + "frame": 1687, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1028, + "frame": 1688, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1029, + "frame": 1689, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1030, + "frame": 1690, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1031, + "frame": 1691, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1032, + "frame": 1692, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1033, + "frame": 1693, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1034, + "frame": 1694, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1035, + "frame": 1695, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1036, + "frame": 1696, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1037, + "frame": 1697, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1038, + "frame": 1698, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1039, + "frame": 1699, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1040, + "frame": 1700, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1041, + "frame": 1701, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1042, + "frame": 1702, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1043, + "frame": 1703, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1044, + "frame": 1704, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1045, + "frame": 1705, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1046, + "frame": 1706, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1047, + "frame": 1707, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1048, + "frame": 1708, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1049, + "frame": 1709, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1050, + "frame": 1710, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1051, + "frame": 1711, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1052, + "frame": 1712, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1053, + "frame": 1713, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1054, + "frame": 1714, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1055, + "frame": 1715, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1056, + "frame": 1716, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1057, + "frame": 1717, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1058, + "frame": 1718, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1059, + "frame": 1719, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1060, + "frame": 1720, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1061, + "frame": 1721, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1062, + "frame": 1722, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1063, + "frame": 1723, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1064, + "frame": 1724, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1065, + "frame": 1725, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1066, + "frame": 1726, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1067, + "frame": 1727, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1068, + "frame": 1728, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1069, + "frame": 1729, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1070, + "frame": 1730, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1071, + "frame": 1731, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1072, + "frame": 1732, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1073, + "frame": 1733, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1074, + "frame": 1734, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1075, + "frame": 1735, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1076, + "frame": 1736, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1077, + "frame": 1737, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1078, + "frame": 1738, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1079, + "frame": 1739, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1080, - "kind": "baseline" + "frame": 1740, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1081, + "frame": 1741, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1082, + "frame": 1742, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1083, + "frame": 1743, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1084, + "frame": 1744, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1085, + "frame": 1745, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1086, + "frame": 1746, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1087, + "frame": 1747, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1088, + "frame": 1748, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1089, + "frame": 1749, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1090, + "frame": 1750, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1091, + "frame": 1751, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1092, + "frame": 1752, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1093, + "frame": 1753, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1094, + "frame": 1754, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1095, + "frame": 1755, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1096, + "frame": 1756, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1097, + "frame": 1757, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1098, + "frame": 1758, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1099, + "frame": 1759, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1100, + "frame": 1760, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1101, + "frame": 1761, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1102, + "frame": 1762, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1103, + "frame": 1763, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1104, + "frame": 1764, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1105, + "frame": 1765, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1106, + "frame": 1766, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1107, + "frame": 1767, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1108, + "frame": 1768, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1109, + "frame": 1769, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1110, + "frame": 1770, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1111, + "frame": 1771, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1112, + "frame": 1772, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1113, + "frame": 1773, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1114, + "frame": 1774, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1115, + "frame": 1775, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1116, + "frame": 1776, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1117, + "frame": 1777, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1118, + "frame": 1778, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1119, + "frame": 1779, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1120, + "frame": 1780, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1121, + "frame": 1781, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1122, + "frame": 1782, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1123, + "frame": 1783, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1124, + "frame": 1784, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1125, + "frame": 1785, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1126, + "frame": 1786, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1127, + "frame": 1787, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1128, + "frame": 1788, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1129, + "frame": 1789, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1130, + "frame": 1790, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1131, + "frame": 1791, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1132, + "frame": 1792, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1133, + "frame": 1793, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1134, + "frame": 1794, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1135, + "frame": 1795, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1136, + "frame": 1796, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1137, + "frame": 1797, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1138, + "frame": 1798, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1139, + "frame": 1799, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1140, + "frame": 1800, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1141, + "frame": 1801, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1142, + "frame": 1802, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1143, + "frame": 1803, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1144, + "frame": 1804, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1145, + "frame": 1805, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1146, + "frame": 1806, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1147, + "frame": 1807, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1148, + "frame": 1808, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1149, + "frame": 1809, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1150, + "frame": 1810, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1151, + "frame": 1811, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1152, + "frame": 1812, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1153, + "frame": 1813, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1154, + "frame": 1814, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1155, + "frame": 1815, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1156, + "frame": 1816, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1157, + "frame": 1817, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1158, + "frame": 1818, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1159, + "frame": 1819, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1160, + "frame": 1820, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1161, + "frame": 1821, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1162, + "frame": 1822, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1163, + "frame": 1823, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1164, + "frame": 1824, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1165, + "frame": 1825, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1166, + "frame": 1826, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1167, + "frame": 1827, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1168, + "frame": 1828, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1169, + "frame": 1829, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1170, + "frame": 1830, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1171, + "frame": 1831, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1172, + "frame": 1832, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1173, + "frame": 1833, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1174, + "frame": 1834, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1175, + "frame": 1835, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1176, + "frame": 1836, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1177, + "frame": 1837, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1178, + "frame": 1838, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1179, + "frame": 1839, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1180, + "frame": 1840, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1181, + "frame": 1841, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1182, + "frame": 1842, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1183, + "frame": 1843, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1184, + "frame": 1844, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1185, + "frame": 1845, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1186, + "frame": 1846, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1187, + "frame": 1847, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1188, + "frame": 1848, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1189, + "frame": 1849, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1190, + "frame": 1850, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1191, + "frame": 1851, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1192, + "frame": 1852, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1193, + "frame": 1853, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1194, + "frame": 1854, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1195, + "frame": 1855, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1196, + "frame": 1856, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1197, + "frame": 1857, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1198, + "frame": 1858, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1199, + "frame": 1859, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1200, + "frame": 1860, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1201, + "frame": 1861, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1202, + "frame": 1862, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1203, + "frame": 1863, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1204, + "frame": 1864, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1205, + "frame": 1865, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1206, + "frame": 1866, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1207, + "frame": 1867, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1208, + "frame": 1868, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1209, + "frame": 1869, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1210, + "frame": 1870, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1211, + "frame": 1871, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1212, + "frame": 1872, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1213, + "frame": 1873, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1214, + "frame": 1874, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1215, + "frame": 1875, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1216, + "frame": 1876, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1217, + "frame": 1877, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1218, + "frame": 1878, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1219, + "frame": 1879, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1220, + "frame": 1880, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1221, + "frame": 1881, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1222, + "frame": 1882, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1223, + "frame": 1883, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1224, + "frame": 1884, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1225, + "frame": 1885, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1226, + "frame": 1886, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1227, + "frame": 1887, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1228, + "frame": 1888, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1229, + "frame": 1889, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1230, + "frame": 1890, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1231, + "frame": 1891, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1232, + "frame": 1892, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1233, + "frame": 1893, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1234, + "frame": 1894, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1235, + "frame": 1895, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1236, + "frame": 1896, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1237, + "frame": 1897, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1238, + "frame": 1898, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1239, + "frame": 1899, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1240, + "frame": 1900, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1241, + "frame": 1901, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1242, + "frame": 1902, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1243, + "frame": 1903, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1244, + "frame": 1904, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1245, + "frame": 1905, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1246, + "frame": 1906, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1247, + "frame": 1907, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1248, + "frame": 1908, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1249, + "frame": 1909, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1250, + "frame": 1910, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1251, + "frame": 1911, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1252, + "frame": 1912, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1253, + "frame": 1913, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1254, + "frame": 1914, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1255, + "frame": 1915, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1256, + "frame": 1916, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1257, + "frame": 1917, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1258, + "frame": 1918, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1259, + "frame": 1919, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1260, + "frame": 1920, "kind": "baseline" }, { - "frame": 1261, + "frame": 1921, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1262, + "frame": 1922, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1263, + "frame": 1923, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1264, + "frame": 1924, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1265, + "frame": 1925, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1266, + "frame": 1926, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1267, + "frame": 1927, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1268, + "frame": 1928, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1269, + "frame": 1929, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1270, + "frame": 1930, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1271, + "frame": 1931, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1272, + "frame": 1932, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1273, + "frame": 1933, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1274, + "frame": 1934, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1275, + "frame": 1935, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1276, + "frame": 1936, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1277, + "frame": 1937, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1278, + "frame": 1938, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1279, + "frame": 1939, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1280, + "frame": 1940, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1281, + "frame": 1941, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1282, + "frame": 1942, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1283, + "frame": 1943, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1284, + "frame": 1944, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1285, + "frame": 1945, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1286, + "frame": 1946, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1287, + "frame": 1947, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1288, + "frame": 1948, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1289, + "frame": 1949, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1290, + "frame": 1950, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1291, + "frame": 1951, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1292, + "frame": 1952, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1293, + "frame": 1953, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1294, + "frame": 1954, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1295, + "frame": 1955, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1296, + "frame": 1956, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1297, + "frame": 1957, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1298, + "frame": 1958, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1299, + "frame": 1959, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1300, + "frame": 1960, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1301, + "frame": 1961, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1302, + "frame": 1962, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1303, + "frame": 1963, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1304, + "frame": 1964, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1305, + "frame": 1965, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1306, + "frame": 1966, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1307, + "frame": 1967, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1308, + "frame": 1968, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1309, + "frame": 1969, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1310, + "frame": 1970, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1311, + "frame": 1971, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1312, + "frame": 1972, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1313, + "frame": 1973, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1314, + "frame": 1974, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1315, + "frame": 1975, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1316, + "frame": 1976, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1317, + "frame": 1977, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1318, + "frame": 1978, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1319, + "frame": 1979, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1320, + "frame": 1980, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1321, + "frame": 1981, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1322, + "frame": 1982, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1323, + "frame": 1983, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1324, + "frame": 1984, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1325, + "frame": 1985, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1326, + "frame": 1986, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1327, + "frame": 1987, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1328, + "frame": 1988, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1329, + "frame": 1989, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1330, + "frame": 1990, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1331, + "frame": 1991, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1332, + "frame": 1992, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1333, + "frame": 1993, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1334, + "frame": 1994, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1335, + "frame": 1995, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1336, + "frame": 1996, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1337, + "frame": 1997, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1338, + "frame": 1998, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1339, + "frame": 1999, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1340, + "frame": 2000, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1341, + "frame": 2001, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1342, + "frame": 2002, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1343, + "frame": 2003, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1344, + "frame": 2004, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1345, + "frame": 2005, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1346, + "frame": 2006, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1347, + "frame": 2007, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1348, + "frame": 2008, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1349, + "frame": 2009, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1350, + "frame": 2010, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1351, + "frame": 2011, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1352, + "frame": 2012, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1353, + "frame": 2013, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1354, + "frame": 2014, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1355, + "frame": 2015, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1356, + "frame": 2016, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1357, + "frame": 2017, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1358, + "frame": 2018, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1359, + "frame": 2019, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1360, + "frame": 2020, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1361, + "frame": 2021, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1362, + "frame": 2022, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1363, + "frame": 2023, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1364, + "frame": 2024, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1365, + "frame": 2025, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1366, + "frame": 2026, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1367, + "frame": 2027, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1368, + "frame": 2028, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1369, + "frame": 2029, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1370, + "frame": 2030, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1371, + "frame": 2031, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1372, + "frame": 2032, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1373, + "frame": 2033, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1374, + "frame": 2034, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1375, + "frame": 2035, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1376, + "frame": 2036, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1377, + "frame": 2037, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1378, + "frame": 2038, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1379, + "frame": 2039, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1380, + "frame": 2040, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1381, + "frame": 2041, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1382, + "frame": 2042, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1383, + "frame": 2043, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1384, + "frame": 2044, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1385, + "frame": 2045, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1386, + "frame": 2046, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1387, + "frame": 2047, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1388, + "frame": 2048, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1389, + "frame": 2049, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1390, + "frame": 2050, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1391, + "frame": 2051, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1392, + "frame": 2052, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1393, + "frame": 2053, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1394, + "frame": 2054, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1395, + "frame": 2055, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1396, + "frame": 2056, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1397, + "frame": 2057, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1398, + "frame": 2058, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1399, + "frame": 2059, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1400, + "frame": 2060, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1401, + "frame": 2061, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1402, + "frame": 2062, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1403, + "frame": 2063, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1404, + "frame": 2064, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1405, + "frame": 2065, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1406, + "frame": 2066, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1407, + "frame": 2067, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1408, + "frame": 2068, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1409, + "frame": 2069, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1410, + "frame": 2070, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1411, + "frame": 2071, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1412, + "frame": 2072, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1413, + "frame": 2073, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1414, + "frame": 2074, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1415, + "frame": 2075, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1416, + "frame": 2076, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1417, + "frame": 2077, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1418, + "frame": 2078, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1419, + "frame": 2079, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1420, + "frame": 2080, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1421, + "frame": 2081, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1422, + "frame": 2082, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1423, + "frame": 2083, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1424, + "frame": 2084, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1425, + "frame": 2085, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1426, + "frame": 2086, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1427, + "frame": 2087, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1428, + "frame": 2088, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1429, + "frame": 2089, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1430, + "frame": 2090, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1431, + "frame": 2091, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1432, + "frame": 2092, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1433, + "frame": 2093, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1434, + "frame": 2094, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1435, + "frame": 2095, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1436, + "frame": 2096, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1437, + "frame": 2097, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1438, + "frame": 2098, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1439, + "frame": 2099, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1440, - "kind": "baseline" + "frame": 2100, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1441, + "frame": 2101, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1442, + "frame": 2102, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1443, + "frame": 2103, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1444, + "frame": 2104, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1445, + "frame": 2105, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1446, + "frame": 2106, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1447, + "frame": 2107, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1448, + "frame": 2108, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1449, + "frame": 2109, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1450, + "frame": 2110, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1451, + "frame": 2111, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1452, + "frame": 2112, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1453, + "frame": 2113, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1454, + "frame": 2114, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1455, + "frame": 2115, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1456, + "frame": 2116, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1457, + "frame": 2117, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1458, + "frame": 2118, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1459, + "frame": 2119, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1460, + "frame": 2120, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1461, + "frame": 2121, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1462, + "frame": 2122, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1463, + "frame": 2123, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1464, + "frame": 2124, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1465, + "frame": 2125, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1466, + "frame": 2126, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1467, + "frame": 2127, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1468, + "frame": 2128, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1469, + "frame": 2129, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1470, + "frame": 2130, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1471, + "frame": 2131, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1472, + "frame": 2132, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1473, + "frame": 2133, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1474, + "frame": 2134, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1475, + "frame": 2135, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1476, + "frame": 2136, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1477, + "frame": 2137, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1478, + "frame": 2138, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1479, + "frame": 2139, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1480, + "frame": 2140, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1481, + "frame": 2141, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1482, + "frame": 2142, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1483, + "frame": 2143, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1484, + "frame": 2144, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1485, + "frame": 2145, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1486, + "frame": 2146, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1487, + "frame": 2147, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1488, + "frame": 2148, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1489, + "frame": 2149, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1490, + "frame": 2150, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1491, + "frame": 2151, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1492, + "frame": 2152, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1493, + "frame": 2153, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1494, + "frame": 2154, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1495, + "frame": 2155, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1496, + "frame": 2156, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1497, + "frame": 2157, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1498, + "frame": 2158, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1499, + "frame": 2159, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1500, - "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "frame": 2160, + "kind": "baseline" }, { - "frame": 1501, + "frame": 2161, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1502, + "frame": 2162, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1503, + "frame": 2163, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1504, + "frame": 2164, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1505, + "frame": 2165, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1506, + "frame": 2166, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1507, + "frame": 2167, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1508, + "frame": 2168, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1509, + "frame": 2169, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1510, + "frame": 2170, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1511, + "frame": 2171, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1512, + "frame": 2172, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1513, + "frame": 2173, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1514, + "frame": 2174, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1515, + "frame": 2175, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1516, + "frame": 2176, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1517, + "frame": 2177, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1518, + "frame": 2178, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1519, + "frame": 2179, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1520, + "frame": 2180, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1521, + "frame": 2181, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1522, + "frame": 2182, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1523, + "frame": 2183, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1524, + "frame": 2184, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1525, + "frame": 2185, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1526, + "frame": 2186, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1527, + "frame": 2187, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1528, + "frame": 2188, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1529, + "frame": 2189, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1530, + "frame": 2190, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1531, + "frame": 2191, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1532, + "frame": 2192, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1533, + "frame": 2193, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1534, + "frame": 2194, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1535, + "frame": 2195, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1536, + "frame": 2196, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1537, + "frame": 2197, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1538, + "frame": 2198, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1539, + "frame": 2199, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1540, + "frame": 2200, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1541, + "frame": 2201, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1542, + "frame": 2202, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1543, + "frame": 2203, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1544, + "frame": 2204, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1545, + "frame": 2205, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1546, + "frame": 2206, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1547, + "frame": 2207, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1548, + "frame": 2208, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1549, + "frame": 2209, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1550, + "frame": 2210, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1551, + "frame": 2211, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1552, + "frame": 2212, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1553, + "frame": 2213, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1554, + "frame": 2214, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1555, + "frame": 2215, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1556, + "frame": 2216, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1557, + "frame": 2217, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1558, + "frame": 2218, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1559, + "frame": 2219, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1560, + "frame": 2220, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1561, + "frame": 2221, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1562, + "frame": 2222, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1563, + "frame": 2223, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1564, + "frame": 2224, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1565, + "frame": 2225, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1566, + "frame": 2226, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1567, + "frame": 2227, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1568, + "frame": 2228, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1569, + "frame": 2229, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1570, + "frame": 2230, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1571, + "frame": 2231, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1572, + "frame": 2232, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1573, + "frame": 2233, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1574, + "frame": 2234, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1575, + "frame": 2235, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1576, + "frame": 2236, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1577, + "frame": 2237, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1578, + "frame": 2238, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1579, + "frame": 2239, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1580, + "frame": 2240, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1581, + "frame": 2241, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1582, + "frame": 2242, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1583, + "frame": 2243, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1584, + "frame": 2244, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1585, + "frame": 2245, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1586, + "frame": 2246, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1587, + "frame": 2247, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1588, + "frame": 2248, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1589, + "frame": 2249, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1590, + "frame": 2250, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1591, + "frame": 2251, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1592, + "frame": 2252, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1593, + "frame": 2253, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1594, + "frame": 2254, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1595, + "frame": 2255, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1596, + "frame": 2256, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1597, + "frame": 2257, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1598, + "frame": 2258, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1599, + "frame": 2259, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1600, + "frame": 2260, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1601, + "frame": 2261, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1602, + "frame": 2262, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1603, + "frame": 2263, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1604, + "frame": 2264, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1605, + "frame": 2265, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1606, + "frame": 2266, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1607, + "frame": 2267, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1608, + "frame": 2268, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1609, + "frame": 2269, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1610, + "frame": 2270, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1611, + "frame": 2271, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1612, + "frame": 2272, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1613, + "frame": 2273, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1614, + "frame": 2274, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1615, + "frame": 2275, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1616, + "frame": 2276, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1617, + "frame": 2277, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1618, + "frame": 2278, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1619, + "frame": 2279, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1620, - "kind": "baseline" + "frame": 2280, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1621, + "frame": 2281, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1622, + "frame": 2282, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1623, + "frame": 2283, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1624, + "frame": 2284, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1625, + "frame": 2285, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1626, + "frame": 2286, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1627, + "frame": 2287, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1628, + "frame": 2288, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1629, + "frame": 2289, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1630, + "frame": 2290, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1631, + "frame": 2291, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1632, + "frame": 2292, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1633, + "frame": 2293, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1634, + "frame": 2294, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1635, + "frame": 2295, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1636, + "frame": 2296, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1637, + "frame": 2297, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1638, + "frame": 2298, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1639, + "frame": 2299, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1640, + "frame": 2300, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1641, + "frame": 2301, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1642, + "frame": 2302, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1643, + "frame": 2303, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1644, + "frame": 2304, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1645, + "frame": 2305, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1646, + "frame": 2306, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1647, + "frame": 2307, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1648, + "frame": 2308, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1649, + "frame": 2309, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1650, + "frame": 2310, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1651, + "frame": 2311, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1652, + "frame": 2312, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1653, + "frame": 2313, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1654, + "frame": 2314, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1655, + "frame": 2315, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1656, + "frame": 2316, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1657, + "frame": 2317, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1658, + "frame": 2318, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1659, + "frame": 2319, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1660, + "frame": 2320, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1661, + "frame": 2321, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1662, + "frame": 2322, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1663, + "frame": 2323, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1664, + "frame": 2324, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1665, + "frame": 2325, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1666, + "frame": 2326, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1667, + "frame": 2327, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1668, + "frame": 2328, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1669, + "frame": 2329, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1670, + "frame": 2330, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1671, + "frame": 2331, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1672, + "frame": 2332, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1673, + "frame": 2333, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1674, + "frame": 2334, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1675, + "frame": 2335, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1676, + "frame": 2336, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1677, + "frame": 2337, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1678, + "frame": 2338, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1679, + "frame": 2339, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1680, + "frame": 2340, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1681, + "frame": 2341, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1682, + "frame": 2342, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1683, + "frame": 2343, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1684, + "frame": 2344, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1685, + "frame": 2345, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1686, + "frame": 2346, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1687, + "frame": 2347, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1688, + "frame": 2348, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1689, + "frame": 2349, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1690, + "frame": 2350, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1691, + "frame": 2351, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1692, + "frame": 2352, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1693, + "frame": 2353, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1694, + "frame": 2354, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1695, + "frame": 2355, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1696, + "frame": 2356, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1697, + "frame": 2357, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1698, + "frame": 2358, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1699, + "frame": 2359, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1700, + "frame": 2360, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1701, + "frame": 2361, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1702, + "frame": 2362, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1703, + "frame": 2363, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1704, + "frame": 2364, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1705, + "frame": 2365, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1706, + "frame": 2366, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1707, + "frame": 2367, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1708, + "frame": 2368, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1709, + "frame": 2369, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1710, + "frame": 2370, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1711, + "frame": 2371, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1712, + "frame": 2372, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1713, + "frame": 2373, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1714, + "frame": 2374, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1715, + "frame": 2375, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1716, + "frame": 2376, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1717, + "frame": 2377, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1718, + "frame": 2378, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1719, + "frame": 2379, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1720, + "frame": 2380, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1721, + "frame": 2381, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1722, + "frame": 2382, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1723, + "frame": 2383, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1724, + "frame": 2384, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1725, + "frame": 2385, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1726, + "frame": 2386, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1727, + "frame": 2387, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1728, + "frame": 2388, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1729, + "frame": 2389, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1730, + "frame": 2390, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1731, + "frame": 2391, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1732, + "frame": 2392, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1733, + "frame": 2393, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1734, + "frame": 2394, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1735, + "frame": 2395, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1736, + "frame": 2396, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1737, + "frame": 2397, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1738, + "frame": 2398, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1739, + "frame": 2399, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1740, - "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "frame": 2400, + "kind": "baseline" }, { - "frame": 1741, + "frame": 2401, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1742, + "frame": 2402, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1743, + "frame": 2403, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1744, + "frame": 2404, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1745, + "frame": 2405, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1746, + "frame": 2406, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1747, + "frame": 2407, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1748, + "frame": 2408, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1749, + "frame": 2409, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1750, + "frame": 2410, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1751, + "frame": 2411, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1752, + "frame": 2412, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1753, + "frame": 2413, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1754, + "frame": 2414, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1755, + "frame": 2415, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1756, + "frame": 2416, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1757, + "frame": 2417, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1758, + "frame": 2418, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1759, + "frame": 2419, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1760, + "frame": 2420, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1761, + "frame": 2421, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1762, + "frame": 2422, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1763, + "frame": 2423, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1764, + "frame": 2424, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1765, + "frame": 2425, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1766, + "frame": 2426, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1767, + "frame": 2427, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1768, + "frame": 2428, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1769, + "frame": 2429, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1770, + "frame": 2430, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1771, + "frame": 2431, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1772, + "frame": 2432, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1773, + "frame": 2433, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1774, + "frame": 2434, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1775, + "frame": 2435, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1776, + "frame": 2436, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1777, + "frame": 2437, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1778, + "frame": 2438, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1779, + "frame": 2439, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1780, + "frame": 2440, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1781, + "frame": 2441, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1782, + "frame": 2442, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1783, + "frame": 2443, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1784, + "frame": 2444, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1785, + "frame": 2445, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1786, + "frame": 2446, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1787, + "frame": 2447, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1788, + "frame": 2448, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1789, + "frame": 2449, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1790, + "frame": 2450, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1791, + "frame": 2451, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1792, + "frame": 2452, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1793, + "frame": 2453, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1794, + "frame": 2454, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1795, + "frame": 2455, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1796, + "frame": 2456, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1797, + "frame": 2457, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1798, + "frame": 2458, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1799, + "frame": 2459, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1800, - "kind": "baseline" + "frame": 2460, + "kind": "gimbal_step", + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1801, + "frame": 2461, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1802, + "frame": 2462, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1803, + "frame": 2463, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1804, + "frame": 2464, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1805, + "frame": 2465, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1806, + "frame": 2466, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1807, + "frame": 2467, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1808, + "frame": 2468, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1809, + "frame": 2469, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1810, + "frame": 2470, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1811, + "frame": 2471, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1812, + "frame": 2472, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1813, + "frame": 2473, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1814, + "frame": 2474, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1815, + "frame": 2475, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1816, + "frame": 2476, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1817, + "frame": 2477, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1818, + "frame": 2478, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1819, + "frame": 2479, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1820, + "frame": 2480, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1821, + "frame": 2481, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1822, + "frame": 2482, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1823, + "frame": 2483, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1824, + "frame": 2484, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1825, + "frame": 2485, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1826, + "frame": 2486, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1827, + "frame": 2487, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1828, + "frame": 2488, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1829, + "frame": 2489, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1830, + "frame": 2490, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1831, + "frame": 2491, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1832, + "frame": 2492, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1833, + "frame": 2493, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1834, + "frame": 2494, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1835, + "frame": 2495, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1836, + "frame": 2496, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1837, + "frame": 2497, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1838, + "frame": 2498, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1839, + "frame": 2499, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1840, + "frame": 2500, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1841, + "frame": 2501, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1842, + "frame": 2502, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1843, + "frame": 2503, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1844, + "frame": 2504, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1845, + "frame": 2505, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1846, + "frame": 2506, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1847, + "frame": 2507, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1848, + "frame": 2508, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1849, + "frame": 2509, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1850, + "frame": 2510, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1851, + "frame": 2511, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1852, + "frame": 2512, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1853, + "frame": 2513, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1854, + "frame": 2514, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1855, + "frame": 2515, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1856, + "frame": 2516, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1857, + "frame": 2517, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1858, + "frame": 2518, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1859, + "frame": 2519, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1860, + "frame": 2520, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1861, + "frame": 2521, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1862, + "frame": 2522, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1863, + "frame": 2523, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1864, + "frame": 2524, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1865, + "frame": 2525, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1866, + "frame": 2526, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1867, + "frame": 2527, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1868, + "frame": 2528, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1869, + "frame": 2529, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1870, + "frame": 2530, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1871, + "frame": 2531, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1872, + "frame": 2532, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1873, + "frame": 2533, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1874, + "frame": 2534, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1875, + "frame": 2535, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1876, + "frame": 2536, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1877, + "frame": 2537, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1878, + "frame": 2538, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1879, + "frame": 2539, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1880, + "frame": 2540, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1881, + "frame": 2541, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1882, + "frame": 2542, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1883, + "frame": 2543, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1884, + "frame": 2544, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1885, + "frame": 2545, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1886, + "frame": 2546, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1887, + "frame": 2547, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1888, + "frame": 2548, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1889, + "frame": 2549, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1890, + "frame": 2550, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1891, + "frame": 2551, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1892, + "frame": 2552, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1893, + "frame": 2553, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1894, + "frame": 2554, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1895, + "frame": 2555, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1896, + "frame": 2556, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1897, + "frame": 2557, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1898, + "frame": 2558, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1899, + "frame": 2559, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1900, + "frame": 2560, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1901, + "frame": 2561, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1902, + "frame": 2562, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1903, + "frame": 2563, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1904, + "frame": 2564, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1905, + "frame": 2565, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1906, + "frame": 2566, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1907, + "frame": 2567, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1908, + "frame": 2568, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1909, + "frame": 2569, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1910, + "frame": 2570, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1911, + "frame": 2571, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1912, + "frame": 2572, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1913, + "frame": 2573, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1914, + "frame": 2574, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1915, + "frame": 2575, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1916, + "frame": 2576, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1917, + "frame": 2577, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1918, + "frame": 2578, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1919, + "frame": 2579, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1920, + "frame": 2580, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1921, + "frame": 2581, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1922, + "frame": 2582, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1923, + "frame": 2583, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1924, + "frame": 2584, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1925, + "frame": 2585, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1926, + "frame": 2586, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1927, + "frame": 2587, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1928, + "frame": 2588, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1929, + "frame": 2589, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1930, + "frame": 2590, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1931, + "frame": 2591, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1932, + "frame": 2592, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1933, + "frame": 2593, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1934, + "frame": 2594, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1935, + "frame": 2595, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1936, + "frame": 2596, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1937, + "frame": 2597, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1938, + "frame": 2598, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1939, + "frame": 2599, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1940, + "frame": 2600, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1941, + "frame": 2601, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1942, + "frame": 2602, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1943, + "frame": 2603, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1944, + "frame": 2604, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1945, + "frame": 2605, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1946, + "frame": 2606, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1947, + "frame": 2607, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1948, + "frame": 2608, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1949, + "frame": 2609, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1950, + "frame": 2610, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1951, + "frame": 2611, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1952, + "frame": 2612, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1953, + "frame": 2613, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1954, + "frame": 2614, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1955, + "frame": 2615, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1956, + "frame": 2616, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1957, + "frame": 2617, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1958, + "frame": 2618, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1959, + "frame": 2619, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1960, + "frame": 2620, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1961, + "frame": 2621, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1962, + "frame": 2622, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1963, + "frame": 2623, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1964, + "frame": 2624, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1965, + "frame": 2625, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1966, + "frame": 2626, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1967, + "frame": 2627, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1968, + "frame": 2628, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1969, + "frame": 2629, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1970, + "frame": 2630, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1971, + "frame": 2631, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1972, + "frame": 2632, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1973, + "frame": 2633, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1974, + "frame": 2634, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1975, + "frame": 2635, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1976, + "frame": 2636, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1977, + "frame": 2637, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1978, + "frame": 2638, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 }, { - "frame": 1979, + "frame": 2639, "kind": "gimbal_step", - "min_pos_delta": 0.0004, - "max_pos_delta": 1.5 + "min_pos_delta": 0.00025, + "max_pos_delta": 2 } ] -} \ No newline at end of file +} diff --git a/61_UI/app_resources/sky_env_fragment.hlsl b/61_UI/app_resources/sky_env_fragment.hlsl index 3005dafcc..1cd02d677 100644 --- a/61_UI/app_resources/sky_env_fragment.hlsl +++ b/61_UI/app_resources/sky_env_fragment.hlsl @@ -29,6 +29,14 @@ float32_t3 safeNormalize(float32_t3 v) 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; @@ -47,8 +55,8 @@ float32_t3 acesToneMap(float32_t3 x) { 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 = centerNearVS_H.xyz / max(abs(centerNearVS_H.w), 1e-6f); - const float32_t3 centerFarVS = centerFarVS_H.xyz / max(abs(centerFarVS_H.w), 1e-6f); + 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)); @@ -56,10 +64,10 @@ float32_t3 acesToneMap(float32_t3 x) 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 = leftNearVS_H.xyz / max(abs(leftNearVS_H.w), 1e-6f); - const float32_t3 rightNearVS = rightNearVS_H.xyz / max(abs(rightNearVS_H.w), 1e-6f); - const float32_t3 downNearVS = downNearVS_H.xyz / max(abs(downNearVS_H.w), 1e-6f); - const float32_t3 upNearVS = upNearVS_H.xyz / max(abs(upNearVS_H.w), 1e-6f); + 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); @@ -70,7 +78,7 @@ float32_t3 acesToneMap(float32_t3 x) { const float32_t4 clip = float32_t4(ndc, 1.0f, 1.0f); const float32_t4 viewH = mul(pc.invProj, clip); - dirVS = safeNormalize(viewH.xyz / max(abs(viewH.w), 1e-6f)); + dirVS = safeNormalize(safeHomogeneousDivide(viewH)); } const float32_t3 dir = safeNormalize(mul(pc.invViewRot, float32_t4(dirVS, 0.0f)).xyz); @@ -80,7 +88,7 @@ float32_t3 acesToneMap(float32_t3 x) 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.0f); + 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 index 3bb9651c4..4bdb5c061 100644 --- a/61_UI/include/app/App.hpp +++ b/61_UI/include/app/App.hpp @@ -401,6 +401,9 @@ class App final : public examples::SimpleWindowedApplication inline bool keepRunning() override { + if (m_headlessCameraSmokeMode) + return false; + if (m_scriptedInput.enabled && m_scriptedInput.hardFail && m_scriptedInput.failed) { if (!m_ciMode || m_ciScreenshotDone) @@ -418,8 +421,15 @@ class App final : public examples::SimpleWindowedApplication } if (m_ciMode && m_ciScreenshotDone) { - if (m_scriptedInput.enabled && m_scriptedInput.nextCaptureIndex < m_scriptedInput.captureFrames.size()) - return true; + if (m_scriptedInput.enabled) + { + if (m_scriptedInput.nextCaptureIndex < m_scriptedInput.captureFrames.size()) + return true; + if (m_scriptedInput.nextEventIndex < m_scriptedInput.events.size()) + return true; + if (m_scriptedInput.nextCheckIndex < m_scriptedInput.checks.size()) + return true; + } return false; } if (m_surface->irrecoverable()) @@ -430,6 +440,9 @@ class App final : public examples::SimpleWindowedApplication inline bool onAppTerminated() override { + if (m_headlessCameraSmokeMode) + return m_headlessCameraSmokePassed; + return base_t::onAppTerminated(); } @@ -915,8 +928,8 @@ class App final : public examples::SimpleWindowedApplication drawAxisLabel("Z", zPos, IM_COL32(172, 210, 255, 255)); } - inline void drawScriptVisualDebugOverlay(const ImVec2& displaySize) - { + inline void drawScriptVisualDebugOverlay(const ImVec2& displaySize) + { if (!(m_scriptedInput.enabled && m_scriptedInput.visualDebug)) return; if (windowBindings.empty() || m_planarProjections.empty()) @@ -948,12 +961,13 @@ class App final : public examples::SimpleWindowedApplication const uint64_t holdFrames = static_cast(std::round(std::max(0.f, m_scriptedInput.visualCameraHoldSeconds) * fps)); const uint64_t progressFrames = holdFrames ? std::min(elapsedFrames, holdFrames) : elapsedFrames; - const auto cameraLabel = getCameraTypeLabel(camera); - std::string lineTop = "SCRIPT VISUAL DEBUG"; - std::string lineMid = "Camera " + std::to_string(binding.activePlanarIx + 1u) + "/" + std::to_string(m_planarProjections.size()) + " " + std::string(cameraLabel); + const auto cameraLabel = getCameraTypeLabel(camera); + const auto cameraHint = getCameraTypeDescription(camera); + std::string lineTop = "SCRIPT VISUAL DEBUG"; + std::string lineMid = "Camera " + std::to_string(binding.activePlanarIx + 1u) + "/" + std::to_string(m_planarProjections.size()) + " " + std::string(cameraLabel); - char lineBottomBuffer[256] = {}; - if (holdFrames) + char lineBottomBuffer[256] = {}; + if (holdFrames) { const double elapsedSeconds = static_cast(progressFrames) / static_cast(fps); const double holdSeconds = static_cast(holdFrames) / static_cast(fps); @@ -975,29 +989,38 @@ class App final : public examples::SimpleWindowedApplication "Planar %u Frame %llu", binding.activePlanarIx, static_cast(m_realFrameIx)); - } - const std::string lineBottom(lineBottomBuffer); + } + const std::string lineBottom(lineBottomBuffer); + std::string lineHint = std::string(cameraHint); + if (auto* dollyZoom = dynamic_cast(camera)) + { + char fovBuffer[96] = {}; + std::snprintf(fovBuffer, sizeof(fovBuffer), " | Dynamic FOV %.2f deg", dollyZoom->computeDollyFov()); + lineHint += fovBuffer; + } - const float topSize = 50.f; - const float midSize = 38.f; - const float bottomSize = 28.f; - const float marginTop = 18.f; - const float padX = 24.f; - const float padY = 16.f; - const float gap = 6.f; + const float topSize = 50.f; + const float midSize = 38.f; + const float bottomSize = 28.f; + const float hintSize = 24.f; + const float marginTop = 18.f; + const float padX = 24.f; + const float padY = 16.f; + const float gap = 6.f; ImFont* font = ImGui::GetFont(); if (!font) return; - const float textWrap = std::numeric_limits::max(); - const ImVec2 topTextSize = font->CalcTextSizeA(topSize, textWrap, 0.0f, lineTop.c_str()); - const ImVec2 midTextSize = font->CalcTextSizeA(midSize, textWrap, 0.0f, lineMid.c_str()); - const ImVec2 bottomTextSize = font->CalcTextSizeA(bottomSize, textWrap, 0.0f, lineBottom.c_str()); - const float panelWidth = std::max(topTextSize.x, std::max(midTextSize.x, bottomTextSize.x)) + padX * 2.0f; - const float panelHeight = topTextSize.y + midTextSize.y + bottomTextSize.y + gap * 2.0f + padY * 2.0f; - const ImVec2 panelMin((displaySize.x - panelWidth) * 0.5f, marginTop); - const ImVec2 panelMax(panelMin.x + panelWidth, panelMin.y + panelHeight); + const float textWrap = std::numeric_limits::max(); + const ImVec2 topTextSize = font->CalcTextSizeA(topSize, textWrap, 0.0f, lineTop.c_str()); + const ImVec2 midTextSize = font->CalcTextSizeA(midSize, textWrap, 0.0f, lineMid.c_str()); + const ImVec2 bottomTextSize = font->CalcTextSizeA(bottomSize, textWrap, 0.0f, lineBottom.c_str()); + const ImVec2 hintTextSize = font->CalcTextSizeA(hintSize, textWrap, 0.0f, lineHint.c_str()); + const float panelWidth = std::max(std::max(topTextSize.x, midTextSize.x), std::max(bottomTextSize.x, hintTextSize.x)) + padX * 2.0f; + const float panelHeight = topTextSize.y + midTextSize.y + bottomTextSize.y + hintTextSize.y + gap * 3.0f + padY * 2.0f; + const ImVec2 panelMin((displaySize.x - panelWidth) * 0.5f, marginTop); + const ImVec2 panelMax(panelMin.x + panelWidth, panelMin.y + panelHeight); auto* drawList = ImGui::GetForegroundDrawList(); if (!drawList) @@ -1006,17 +1029,20 @@ class App final : public examples::SimpleWindowedApplication drawList->AddRectFilled(panelMin, panelMax, IM_COL32(6, 8, 12, 232), 14.0f); drawList->AddRect(panelMin, panelMax, IM_COL32(255, 166, 64, 255), 14.0f, 0, 2.5f); - const float topX = panelMin.x + (panelWidth - topTextSize.x) * 0.5f; - const float midX = panelMin.x + (panelWidth - midTextSize.x) * 0.5f; - const float bottomX = panelMin.x + (panelWidth - bottomTextSize.x) * 0.5f; - const float topY = panelMin.y + padY; - const float midY = topY + topTextSize.y + gap; - const float bottomY = midY + midTextSize.y + gap; - - drawList->AddText(font, topSize, ImVec2(topX, topY), IM_COL32(255, 206, 120, 255), lineTop.c_str()); - drawList->AddText(font, midSize, ImVec2(midX, midY), IM_COL32(255, 244, 224, 255), lineMid.c_str()); - drawList->AddText(font, bottomSize, ImVec2(bottomX, bottomY), IM_COL32(202, 222, 255, 255), lineBottom.c_str()); - } + const float topX = panelMin.x + (panelWidth - topTextSize.x) * 0.5f; + const float midX = panelMin.x + (panelWidth - midTextSize.x) * 0.5f; + const float bottomX = panelMin.x + (panelWidth - bottomTextSize.x) * 0.5f; + const float hintX = panelMin.x + (panelWidth - hintTextSize.x) * 0.5f; + const float topY = panelMin.y + padY; + const float midY = topY + topTextSize.y + gap; + const float bottomY = midY + midTextSize.y + gap; + const float hintY = bottomY + bottomTextSize.y + gap; + + drawList->AddText(font, topSize, ImVec2(topX, topY), IM_COL32(255, 206, 120, 255), lineTop.c_str()); + drawList->AddText(font, midSize, ImVec2(midX, midY), IM_COL32(255, 244, 224, 255), lineMid.c_str()); + drawList->AddText(font, bottomSize, ImVec2(bottomX, bottomY), IM_COL32(202, 222, 255, 255), lineBottom.c_str()); + drawList->AddText(font, hintSize, ImVec2(hintX, hintY), IM_COL32(170, 204, 255, 255), lineHint.c_str()); + } inline void applyDollyZoomProjection(ICamera* camera, IPlanarProjection::CProjection& projection) { @@ -1771,7 +1797,8 @@ class App final : public examples::SimpleWindowedApplication SetProjectionType, SetProjectionIndex, SetUseWindow, - SetLeftHanded + SetLeftHanded, + ResetActiveCamera }; Kind kind = Kind::SetActiveRenderWindow; @@ -1885,6 +1912,9 @@ class App final : public examples::SimpleWindowedApplication system::path m_ciScreenshotPath; clock_t::time_point m_ciStartedAt = clock_t::time_point::min(); bool m_scriptVisualDebugCli = false; + bool m_disableScreenshotsCli = false; + bool m_headlessCameraSmokeMode = false; + bool m_headlessCameraSmokePassed = false; ScriptedInputState m_scriptedInput; CameraControlSettings m_cameraControls; CameraConstraintSettings m_cameraConstraints; @@ -1896,6 +1926,7 @@ class App final : public examples::SimpleWindowedApplication bool m_logAutoScroll = true; bool m_logWrap = true; std::vector m_presets; + std::vector m_initialPlanarPresets; std::vector m_keyframes; CameraPlaybackState m_playback; CTargetPoseController m_targetPoseController; From 4ef7c1d4610675fd06aa443c3976f96a35424fbf Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Thu, 19 Feb 2026 21:52:03 +0100 Subject: [PATCH 106/205] Load UI sky env from shared media --- 61_UI/AppInit.cpp | 5 +++-- media | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/61_UI/AppInit.cpp b/61_UI/AppInit.cpp index 7f1d14c61..1e4d22214 100644 --- a/61_UI/AppInit.cpp +++ b/61_UI/AppInit.cpp @@ -1733,8 +1733,9 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) SpaceEnvBlobHeader envBlobHeader = {}; std::vector envBlobPayload; - const std::array SpaceEnvSearchRoots = { - localInputCWD / "media", + const std::array SpaceEnvSearchRoots = { + (localInputCWD / ".." / "media" / "envmap").lexically_normal(), + (localInputCWD / ".." / "media").lexically_normal(), localInputCWD / "app_resources" }; for (const auto candidate : SpaceEnvBlobCandidates) diff --git a/media b/media index 0f7ad42b3..d458d8023 160000 --- a/media +++ b/media @@ -1 +1 @@ -Subproject commit 0f7ad42b33abe3143a5d69c4d14b26cf3e538c88 +Subproject commit d458d802347ecf4dcb4c401728ebb192347e0de4 From d5f8582de1756618cbf7a258a680410b7802a42c Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Fri, 3 Apr 2026 15:21:06 +0200 Subject: [PATCH 107/205] Refine cameraz smoke input handling --- 61_UI/AppInit.cpp | 285 ++++++++++++++++++++++++++++++++------ 61_UI/AppUpdate.cpp | 37 +++-- 61_UI/AppWorkLoop.cpp | 3 +- 61_UI/include/app/App.hpp | 1 + 4 files changed, 272 insertions(+), 54 deletions(-) diff --git a/61_UI/AppInit.cpp b/61_UI/AppInit.cpp index 1e4d22214..c928215e7 100644 --- a/61_UI/AppInit.cpp +++ b/61_UI/AppInit.cpp @@ -1,4 +1,5 @@ #include "app/App.hpp" +#include #include #include #include @@ -266,6 +267,148 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) return std::isfinite(v.x) && std::isfinite(v.y) && std::isfinite(v.z); }; + auto computeDelta = [&](ICamera* camera, const float64_t3& beforePos, const float32_t3& beforeEulerDeg, double& outPosDelta, double& outRotDeltaDeg) -> bool + { + if (!camera) + return false; + const auto& gimbal = camera->getGimbal(); + const auto afterPos = gimbal.getPosition(); + const auto afterEuler = glm::degrees(glm::eulerAngles(gimbal.getOrientation())); + if (!isFinite3(afterPos) || !isFinite3(afterEuler)) + return false; + + const double dx = static_cast(afterPos.x - beforePos.x); + const double dy = static_cast(afterPos.y - beforePos.y); + const double dz = static_cast(afterPos.z - beforePos.z); + outPosDelta = std::sqrt(dx * dx + dy * dy + dz * dz); + outRotDeltaDeg = std::max({ + angleDiffDeg(afterEuler.x, beforeEulerDeg.x), + angleDiffDeg(afterEuler.y, beforeEulerDeg.y), + angleDiffDeg(afterEuler.z, beforeEulerDeg.z) + }); + return true; + }; + + auto manipulateAndMeasure = [&](ICamera* camera, const std::vector& events, double& outPosDelta, double& outRotDeltaDeg) -> bool + { + outPosDelta = 0.0; + outRotDeltaDeg = 0.0; + if (!camera || events.empty()) + return false; + + const auto& beforeGimbal = camera->getGimbal(); + const float64_t3 beforePos = beforeGimbal.getPosition(); + const float32_t3 beforeEulerDeg = glm::degrees(glm::eulerAngles(beforeGimbal.getOrientation())); + if (!isFinite3(beforePos) || !isFinite3(beforeEulerDeg)) + return false; + + if (!camera->manipulate({ events.data(), events.size() })) + return false; + + if (!computeDelta(camera, beforePos, beforeEulerDeg, outPosDelta, outRotDeltaDeg)) + return false; + + return outPosDelta > 1e-9 || outRotDeltaDeg > 1e-9; + }; + + auto collectKeyboardVirtualEvents = [&](ICamera* camera, const ui::E_KEY_CODE keyCode) -> std::vector + { + std::vector out; + if (!camera) + return out; + + static std::chrono::microseconds smokeTimestamp = std::chrono::microseconds::zero(); + smokeTimestamp += std::chrono::microseconds(16667); + const auto pressTs = smokeTimestamp; + + SKeyboardEvent pressEvent(pressTs); + pressEvent.keyCode = keyCode; + pressEvent.action = SKeyboardEvent::ECA_PRESSED; + pressEvent.window = nullptr; + + uint32_t potentialCount = 0u; + uint32_t generatedCount = 0u; + camera->beginInputProcessing(pressTs); + camera->processKeyboard(nullptr, potentialCount, {}); + if (potentialCount) + { + std::vector warmup(potentialCount); + generatedCount = potentialCount; + camera->processKeyboard(warmup.data(), generatedCount, { &pressEvent, 1u }); + } + camera->endInputProcessing(); + + smokeTimestamp += std::chrono::microseconds(16667); + const auto sampleTs = smokeTimestamp; + camera->beginInputProcessing(sampleTs); + camera->processKeyboard(nullptr, potentialCount, {}); + out.resize(potentialCount); + if (potentialCount) + { + generatedCount = potentialCount; + camera->processKeyboard(out.data(), generatedCount, {}); + } + camera->endInputProcessing(); + out.resize(generatedCount); + return out; + }; + + auto collectMouseVirtualEvents = [&](ICamera* camera, std::span mouseEvents) -> std::vector + { + std::vector out; + if (!camera) + return out; + + static std::chrono::microseconds smokeTimestamp = std::chrono::microseconds::zero(); + smokeTimestamp += std::chrono::microseconds(16667); + const auto ts = smokeTimestamp; + + uint32_t potentialCount = 0u; + uint32_t generatedCount = 0u; + camera->beginInputProcessing(ts); + camera->processMouse(nullptr, potentialCount, {}); + out.resize(potentialCount); + if (potentialCount) + { + generatedCount = potentialCount; + camera->processMouse(out.data(), generatedCount, mouseEvents); + } + camera->endInputProcessing(); + out.resize(generatedCount); + return out; + }; + + auto filterOrbitMouseEvents = [&](ICamera* camera, std::span input, bool orbitLookDown) -> std::vector + { + if (!isOrbitLikeCamera(camera)) + return std::vector(input.begin(), input.end()); + + std::vector filtered; + filtered.reserve(input.size()); + for (const auto& ev : input) + { + if (ev.type == ui::SMouseEvent::EET_MOVEMENT && !orbitLookDown) + continue; + filtered.emplace_back(ev); + } + return filtered; + }; + + const std::array keyboardCandidates = { + ui::E_KEY_CODE::EKC_W, + ui::E_KEY_CODE::EKC_A, + ui::E_KEY_CODE::EKC_S, + ui::E_KEY_CODE::EKC_D, + ui::E_KEY_CODE::EKC_Q, + ui::E_KEY_CODE::EKC_E, + ui::E_KEY_CODE::EKC_I, + ui::E_KEY_CODE::EKC_J, + ui::E_KEY_CODE::EKC_K, + ui::E_KEY_CODE::EKC_L, + ui::E_KEY_CODE::EKC_U, + ui::E_KEY_CODE::EKC_O + }; + for (const auto& cameraRef : cameras) { auto* camera = cameraRef.get(); @@ -276,76 +419,131 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) camera->updateMouseMapping([&](auto& map) { map = camera->getMouseMappingPreset(); }); camera->updateImguizmoMapping([&](auto& map) { map = camera->getImguizmoMappingPreset(); }); - const auto& beforeGimbal = camera->getGimbal(); - const auto beforePos = beforeGimbal.getPosition(); - const auto beforeEuler = glm::degrees(glm::eulerAngles(beforeGimbal.getOrientation())); - const uint32_t allowed = camera->getAllowedVirtualEvents(); - std::vector events; - events.reserve(3u); - - auto pushEvent = [&](const CVirtualGimbalEvent::VirtualEventType type, const double magnitude) -> void + std::vector directEvents; + directEvents.reserve(3u); + auto pushDirectEvent = [&](const CVirtualGimbalEvent::VirtualEventType type, const double magnitude) -> void { CVirtualGimbalEvent ev; ev.type = type; ev.magnitude = magnitude; - events.emplace_back(ev); + directEvents.emplace_back(ev); }; - if (allowed & CVirtualGimbalEvent::MoveForward) - pushEvent(CVirtualGimbalEvent::MoveForward, 1.0); + pushDirectEvent(CVirtualGimbalEvent::MoveForward, 1.0); else if (allowed & CVirtualGimbalEvent::MoveRight) - pushEvent(CVirtualGimbalEvent::MoveRight, 1.0); + pushDirectEvent(CVirtualGimbalEvent::MoveRight, 1.0); else if (allowed & CVirtualGimbalEvent::MoveUp) - pushEvent(CVirtualGimbalEvent::MoveUp, 1.0); - + pushDirectEvent(CVirtualGimbalEvent::MoveUp, 1.0); if (allowed & CVirtualGimbalEvent::PanRight) - pushEvent(CVirtualGimbalEvent::PanRight, 1.0); + pushDirectEvent(CVirtualGimbalEvent::PanRight, 1.0); else if (allowed & CVirtualGimbalEvent::TiltUp) - pushEvent(CVirtualGimbalEvent::TiltUp, 1.0); + pushDirectEvent(CVirtualGimbalEvent::TiltUp, 1.0); else if (allowed & CVirtualGimbalEvent::RollRight) - pushEvent(CVirtualGimbalEvent::RollRight, 1.0); - - if (events.empty()) + pushDirectEvent(CVirtualGimbalEvent::RollRight, 1.0); + if (directEvents.empty()) { for (const auto event : CVirtualGimbalEvent::VirtualEventsTypeTable) { if (allowed & event) { - pushEvent(event, 1.0); + pushDirectEvent(event, 1.0); break; } } } - - if (events.empty()) + if (directEvents.empty()) return fail("No allowed virtual events for camera \"" + std::string(camera->getIdentifier()) + "\"."); - const bool manipulated = camera->manipulate({ events.data(), events.size() }); - if (!manipulated) - return fail("Manipulation returned false for camera \"" + std::string(camera->getIdentifier()) + "\"."); - - const auto& afterGimbal = camera->getGimbal(); - const auto afterPos = afterGimbal.getPosition(); - const auto afterEuler = glm::degrees(glm::eulerAngles(afterGimbal.getOrientation())); + double directPosDelta = 0.0; + double directRotDelta = 0.0; + if (!manipulateAndMeasure(camera, directEvents, directPosDelta, directRotDelta)) + return fail("Direct manipulate smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\"."); - if (!isFinite3(afterPos) || !isFinite3(afterEuler)) - return fail("Non-finite state for camera \"" + std::string(camera->getIdentifier()) + "\"."); - - const auto dPos = afterPos - beforePos; - const double posDelta = std::sqrt(dPos.x * dPos.x + dPos.y * dPos.y + dPos.z * dPos.z); - const double rotDeltaDeg = std::max({ - angleDiffDeg(afterEuler.x, beforeEuler.x), - angleDiffDeg(afterEuler.y, beforeEuler.y), - angleDiffDeg(afterEuler.z, beforeEuler.z) - }); + bool keyboardOk = false; + double keyboardPosDelta = 0.0; + double keyboardRotDelta = 0.0; + for (const auto key : keyboardCandidates) + { + camera->updateKeyboardMapping([&](auto& map) { map = camera->getKeyboardMappingPreset(); }); + auto keyboardEvents = collectKeyboardVirtualEvents(camera, key); + if (keyboardEvents.empty()) + continue; + if (manipulateAndMeasure(camera, keyboardEvents, keyboardPosDelta, keyboardRotDelta)) + { + keyboardOk = true; + break; + } + } + if (!keyboardOk) + return fail("Keyboard controller smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\"."); + + const auto mousePreset = camera->getMouseMappingPreset(); + const bool hasMoveMapping = + mousePreset.find(ui::EMC_RELATIVE_POSITIVE_MOVEMENT_X) != mousePreset.end() || + mousePreset.find(ui::EMC_RELATIVE_NEGATIVE_MOVEMENT_X) != mousePreset.end() || + mousePreset.find(ui::EMC_RELATIVE_POSITIVE_MOVEMENT_Y) != mousePreset.end() || + mousePreset.find(ui::EMC_RELATIVE_NEGATIVE_MOVEMENT_Y) != mousePreset.end(); + const bool hasScrollMapping = + mousePreset.find(ui::EMC_VERTICAL_POSITIVE_SCROLL) != mousePreset.end() || + mousePreset.find(ui::EMC_VERTICAL_NEGATIVE_SCROLL) != mousePreset.end() || + mousePreset.find(ui::EMC_HORIZONTAL_POSITIVE_SCROLL) != mousePreset.end() || + mousePreset.find(ui::EMC_HORIZONTAL_NEGATIVE_SCROLL) != mousePreset.end(); + + double mouseMovePosDelta = 0.0; + double mouseMoveRotDelta = 0.0; + if (hasMoveMapping) + { + SMouseEvent moveEv(std::chrono::microseconds(16667)); + moveEv.window = nullptr; + moveEv.type = ui::SMouseEvent::EET_MOVEMENT; + moveEv.movementEvent.relativeMovementX = 12; + moveEv.movementEvent.relativeMovementY = -8; + + 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 (isOrbitLikeCamera(camera) && hasBlockedMovement) + return fail("Orbit mouse movement gate failed for camera \"" + std::string(camera->getIdentifier()) + "\"."); + + camera->updateMouseMapping([&](auto& map) { map = camera->getMouseMappingPreset(); }); + auto mouseMoveEvents = collectMouseVirtualEvents(camera, { filteredMoveLookDown.data(), filteredMoveLookDown.size() }); + if (mouseMoveEvents.empty()) + return fail("Mouse move virtual events missing for camera \"" + std::string(camera->getIdentifier()) + "\"."); + if (!manipulateAndMeasure(camera, mouseMoveEvents, mouseMovePosDelta, mouseMoveRotDelta)) + return fail("Mouse move controller smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\"."); + } - if (posDelta <= 1e-9 && rotDeltaDeg <= 1e-9) - return fail("No observable change for camera \"" + std::string(camera->getIdentifier()) + "\"."); + double mouseScrollPosDelta = 0.0; + double mouseScrollRotDelta = 0.0; + if (hasScrollMapping) + { + SMouseEvent scrollEv(std::chrono::microseconds(16667)); + scrollEv.window = nullptr; + scrollEv.type = ui::SMouseEvent::EET_SCROLL; + scrollEv.scrollEvent.verticalScroll = 4; + scrollEv.scrollEvent.horizontalScroll = 2; + const std::array rawScroll = { scrollEv }; + auto filteredScroll = filterOrbitMouseEvents(camera, rawScroll, false); + + camera->updateMouseMapping([&](auto& map) { map = camera->getMouseMappingPreset(); }); + auto mouseScrollEvents = collectMouseVirtualEvents(camera, { filteredScroll.data(), filteredScroll.size() }); + if (mouseScrollEvents.empty()) + return fail("Mouse scroll virtual events missing for camera \"" + std::string(camera->getIdentifier()) + "\"."); + if (!manipulateAndMeasure(camera, mouseScrollEvents, mouseScrollPosDelta, mouseScrollRotDelta)) + return fail("Mouse scroll controller smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\"."); + } std::cout << "[headless-camera-smoke][pass] " << camera->getIdentifier() - << " pos_delta=" << posDelta - << " rot_delta_deg=" << rotDeltaDeg + << " direct_pos_delta=" << directPosDelta + << " direct_rot_delta_deg=" << directRotDelta + << " kb_pos_delta=" << keyboardPosDelta + << " kb_rot_delta_deg=" << keyboardRotDelta + << " mouse_move_pos_delta=" << mouseMovePosDelta + << " mouse_move_rot_delta_deg=" << mouseMoveRotDelta + << " mouse_scroll_pos_delta=" << mouseScrollPosDelta + << " mouse_scroll_rot_delta_deg=" << mouseScrollRotDelta << std::endl; } @@ -481,6 +679,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) m_scriptedInput.visualActivePlanarIx = 0u; m_scriptedInput.visualActivePlanarStartFrame = 0u; m_scriptedInput.scriptedLeftMouseDown = false; + m_scriptedInput.scriptedRightMouseDown = false; m_scriptedInput.framePacerInitialized = false; m_scriptedInput.capturePrefix = "script"; m_scriptedInput.captureOutputDir = localOutputCWD; diff --git a/61_UI/AppUpdate.cpp b/61_UI/AppUpdate.cpp index c2513260f..85e798746 100644 --- a/61_UI/AppUpdate.cpp +++ b/61_UI/AppUpdate.cpp @@ -243,17 +243,28 @@ void App::update() if (!m_scriptedInput.enabled) { m_scriptedInput.scriptedLeftMouseDown = false; + m_scriptedInput.scriptedRightMouseDown = false; } else { for (const auto& ev : scriptedMouse) { - if (ev.type != ui::SMouseEvent::EET_CLICK || ev.clickEvent.mouseButton != ui::EMB_LEFT_BUTTON) + if (ev.type != ui::SMouseEvent::EET_CLICK) continue; - if (ev.clickEvent.action == ui::SMouseEvent::SClickEvent::EA_PRESSED) - m_scriptedInput.scriptedLeftMouseDown = true; - else if (ev.clickEvent.action == ui::SMouseEvent::SClickEvent::EA_RELEASED) - m_scriptedInput.scriptedLeftMouseDown = false; + if (ev.clickEvent.mouseButton == ui::EMB_LEFT_BUTTON) + { + if (ev.clickEvent.action == ui::SMouseEvent::SClickEvent::EA_PRESSED) + m_scriptedInput.scriptedLeftMouseDown = true; + else if (ev.clickEvent.action == ui::SMouseEvent::SClickEvent::EA_RELEASED) + m_scriptedInput.scriptedLeftMouseDown = false; + } + else if (ev.clickEvent.mouseButton == ui::EMB_RIGHT_BUTTON) + { + if (ev.clickEvent.action == ui::SMouseEvent::SClickEvent::EA_PRESSED) + m_scriptedInput.scriptedRightMouseDown = true; + else if (ev.clickEvent.action == ui::SMouseEvent::SClickEvent::EA_RELEASED) + m_scriptedInput.scriptedRightMouseDown = false; + } } } @@ -330,11 +341,17 @@ void App::update() if (isOrbitLikeCamera(camera)) { - const bool orbitMouseDown = ImGui::IsMouseDown(ImGuiMouseButton_Left) || (m_scriptedInput.enabled && m_scriptedInput.scriptedLeftMouseDown); - if (orbitMouseDown) - projection.processMouse(output, vMouseEventsCount, { cameraMouseEvents.data(), cameraMouseEvents.size() }); - else - vMouseEventsCount = 0; + const bool orbitLookDown = ImGui::IsMouseDown(ImGuiMouseButton_Right) || + (m_scriptedInput.enabled && (m_scriptedInput.scriptedLeftMouseDown || m_scriptedInput.scriptedRightMouseDown)); + std::vector filteredOrbitMouseEvents; + filteredOrbitMouseEvents.reserve(cameraMouseEvents.size()); + for (const auto& ev : cameraMouseEvents) + { + if (ev.type == ui::SMouseEvent::EET_MOVEMENT && !orbitLookDown) + continue; + filteredOrbitMouseEvents.emplace_back(ev); + } + projection.processMouse(output, vMouseEventsCount, { filteredOrbitMouseEvents.data(), filteredOrbitMouseEvents.size() }); } else { diff --git a/61_UI/AppWorkLoop.cpp b/61_UI/AppWorkLoop.cpp index 3524b01b9..0ebc54587 100644 --- a/61_UI/AppWorkLoop.cpp +++ b/61_UI/AppWorkLoop.cpp @@ -92,7 +92,8 @@ void App::workLoopBody() const auto viewParams = CSimpleDebugRenderer::SViewParams(binding.viewMatrix, binding.viewProjMatrix); m_renderer->render(cmdbuf, viewParams); - if (m_drawFrustum) + const bool drawScriptFrustum = m_scriptedInput.enabled && m_scriptedInput.visualDebug; + if (m_drawFrustum && drawScriptFrustum) { const auto findSourceBindingIxForPlanar = [&](const uint32_t planarIx) -> std::optional { diff --git a/61_UI/include/app/App.hpp b/61_UI/include/app/App.hpp index 4bdb5c061..c8abe07a5 100644 --- a/61_UI/include/app/App.hpp +++ b/61_UI/include/app/App.hpp @@ -1876,6 +1876,7 @@ class App final : public examples::SimpleWindowedApplication uint32_t visualActivePlanarIx = 0u; uint64_t visualActivePlanarStartFrame = 0u; bool scriptedLeftMouseDown = false; + bool scriptedRightMouseDown = false; bool framePacerInitialized = false; std::chrono::steady_clock::time_point framePacerNext = {}; }; From 32a29b2a40d2c2e2050f699aaff0700b2d14d41b Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Fri, 3 Apr 2026 16:34:45 +0200 Subject: [PATCH 108/205] Fix json alias conflict after master merge --- 61_UI/AppInit.cpp | 12 ++++++------ 61_UI/include/app/App.hpp | 10 +++++----- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/61_UI/AppInit.cpp b/61_UI/AppInit.cpp index c928215e7..1ffa6252f 100644 --- a/61_UI/AppInit.cpp +++ b/61_UI/AppInit.cpp @@ -108,7 +108,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) return (localInputCWD / "app_resources" / "cameras.json").lexically_normal(); }(); - json j; + nbl_json j; { std::ifstream file(configPath); if (!file.is_open()) @@ -605,7 +605,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) { const std::optional cameraJsonFile = program.is_used("--file") ? program.get("--file") : std::optional(std::nullopt); - json j; + nbl_json j; auto loadDefaultConfig = [&]() -> bool { #ifdef _NBL_THIS_EXAMPLE_BUILTIN_C_ARCHIVE_H_ @@ -618,7 +618,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) IFile::success_t result; config.resize(pFile->getSize()); pFile->read(result, config.data(), 0, pFile->getSize()); - j = json::parse(config); + j = nbl_json::parse(config); return true; #else const auto fallbackPath = localInputCWD / "app_resources" / "cameras.json"; @@ -646,7 +646,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) file >> j; } - auto loadScriptJson = [&](const std::string& path, json& out) -> bool + auto loadScriptJson = [&](const std::string& path, nbl_json& out) -> bool { std::ifstream sfile(path); if (!sfile.is_open()) @@ -658,7 +658,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) return true; }; - auto parseScriptedInput = [&](const json& script) -> void + auto parseScriptedInput = [&](const nbl_json& script) -> void { m_scriptedInput.events.clear(); m_scriptedInput.checks.clear(); @@ -1144,7 +1144,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) system::path scriptPath = program.get("--script"); if (scriptPath.is_relative()) scriptPath = localInputCWD / scriptPath; - json scriptJson; + nbl_json scriptJson; if (!loadScriptJson(scriptPath.string(), scriptJson)) return false; parseScriptedInput(scriptJson); diff --git a/61_UI/include/app/App.hpp b/61_UI/include/app/App.hpp index c8abe07a5..43aa47527 100644 --- a/61_UI/include/app/App.hpp +++ b/61_UI/include/app/App.hpp @@ -19,7 +19,7 @@ #include #include "nlohmann/json.hpp" #include "argparse/argparse.hpp" -using json = nlohmann::json; +using nbl_json = nlohmann::json; #include "common.hpp" #include "keysmapping.hpp" @@ -1358,12 +1358,12 @@ class App final : public examples::SimpleWindowedApplication inline bool savePresetsToFile(const system::path& path) { - json root; - root["presets"] = json::array(); + nbl_json root; + root["presets"] = nbl_json::array(); for (const auto& preset : m_presets) { - json j; + nbl_json j; j["name"] = preset.name; j["identifier"] = preset.identifier; j["position"] = { preset.position.x, preset.position.y, preset.position.z }; @@ -1392,7 +1392,7 @@ class App final : public examples::SimpleWindowedApplication if (!in) return false; - json root; + nbl_json root; in >> root; if (!root.contains("presets")) return false; From 0640ebd0829f805bfcb0cd94dbfaeeaf708b2161 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Fri, 3 Apr 2026 19:26:04 +0200 Subject: [PATCH 109/205] Refine cameraz camera solver and presets --- 61_UI/AppControlPanel.cpp | 32 +- 61_UI/AppImGuiListen.cpp | 10 +- 61_UI/AppInit.cpp | 238 ++++++++-- 61_UI/AppUpdate.cpp | 27 +- 61_UI/include/app/App.hpp | 305 +++++++++---- 61_UI/include/common.hpp | 2 + common/include/camera/CArcballCamera.hpp | 1 + common/include/camera/CChaseCamera.hpp | 1 + common/include/camera/CCubeProjection.hpp | 12 +- common/include/camera/CDollyCamera.hpp | 1 + common/include/camera/CDollyZoomCamera.hpp | 22 + common/include/camera/CFPSCamera.hpp | 5 + common/include/camera/CFreeLockCamera.hpp | 5 + common/include/camera/CGimbalInputBinder.hpp | 56 +++ common/include/camera/CIsometricCamera.hpp | 1 + common/include/camera/CLinearProjection.hpp | 12 +- common/include/camera/COrbitCamera.hpp | 5 + common/include/camera/CPathCamera.hpp | 24 + common/include/camera/CPlanarProjection.hpp | 12 +- .../include/camera/CSphericalTargetCamera.hpp | 38 +- .../include/camera/CTargetPoseController.hpp | 418 +++++++++++++++--- common/include/camera/CTopDownCamera.hpp | 1 + common/include/camera/CTurntableCamera.hpp | 1 + common/include/camera/ICamera.hpp | 81 +++- common/include/camera/ILinearProjection.hpp | 5 +- 25 files changed, 1114 insertions(+), 201 deletions(-) create mode 100644 common/include/camera/CGimbalInputBinder.hpp diff --git a/61_UI/AppControlPanel.cpp b/61_UI/AppControlPanel.cpp index 888cb6029..d7d7239db 100644 --- a/61_UI/AppControlPanel.cpp +++ b/61_UI/AppControlPanel.cpp @@ -446,6 +446,8 @@ void App::DrawControlPanel() } ImGui::EndCombo(); } + if (updateBoundVirtualMaps) + syncWindowInputBinding(active); DrawHoverHint("Switch preset projection for this planar"); auto* const boundCamera = planarBound->getCamera(); @@ -532,8 +534,8 @@ void App::DrawControlPanel() ImGui::Text("Object Ix: %s", std::to_string(active.activePlanarIx + 1u).c_str()); ImGui::Separator(); { - auto* orbit = dynamic_cast(boundCamera); - const bool isOrbitLike = orbit != nullptr; + ICamera::SphericalTargetState sphericalState; + const bool isOrbitLike = boundCamera->tryGetSphericalTargetState(sphericalState); float moveSpeed = boundCamera->getMoveSpeedScale(); float rotationSpeed = boundCamera->getRotationSpeedScale(); @@ -550,10 +552,10 @@ void App::DrawControlPanel() if (isOrbitLike) { - float distance = orbit->getDistance(); - ImGui::SliderFloat("Distance", &distance, orbit->MinDistance, orbit->MaxDistance, "%.4f", ImGuiSliderFlags_Logarithmic); + float distance = sphericalState.distance; + ImGui::SliderFloat("Distance", &distance, sphericalState.minDistance, sphericalState.maxDistance, "%.4f", ImGuiSliderFlags_Logarithmic); DrawHoverHint("Current orbit distance"); - orbit->setDistance(distance); + boundCamera->trySetSphericalDistance(distance); } } @@ -572,7 +574,9 @@ void App::DrawControlPanel() if (ImGui::TreeNodeEx("Virtual Event Mappings", flags)) { - displayKeyMappingsAndVirtualStatesInline(&boundProjection); + syncWindowInputBinding(active); + if (displayKeyMappingsAndVirtualStatesInline(&active.inputBinding)) + syncWindowInputBindingToProjection(active); ImGui::TreePop(); } @@ -639,23 +643,25 @@ void App::DrawControlPanel() DrawSectionHeader("OrbitHeader", "Orbit Target", accent); auto* activeCamera = getActiveCamera(); - const bool hasOrbitTarget = withOrbitLikeCamera(activeCamera, [&](auto* orbit) + ICamera::SphericalTargetState orbitState; + const bool hasOrbitTarget = activeCamera && activeCamera->tryGetSphericalTargetState(orbitState); + if (hasOrbitTarget) { - auto target = getCastedVector(orbit->getTarget()); + auto target = getCastedVector(orbitState.target); if (ImGui::InputFloat3("Target", &target[0])) - orbit->target(getCastedVector(target)); + activeCamera->trySetSphericalTarget(getCastedVector(target)); if (ImGui::Button("Target model")) { - auto targetPos = hlsl::transpose(getMatrix3x4As4x4(m_model))[3]; - orbit->target(targetPos); + const auto targetPos = hlsl::transpose(getMatrix3x4As4x4(m_model))[3]; + activeCamera->trySetSphericalTarget(float64_t3(targetPos.x, targetPos.y, targetPos.z)); } DrawHoverHint("Set orbit target to the model position"); ImGui::SameLine(); if (ImGui::Button("Target origin")) - orbit->target(float64_t3(0.0)); + activeCamera->trySetSphericalTarget(float64_t3(0.0)); DrawHoverHint("Set orbit target to world origin"); - }); + } if (!hasOrbitTarget) { ImGui::TextDisabled("Active camera is not orbit."); diff --git a/61_UI/AppImGuiListen.cpp b/61_UI/AppImGuiListen.cpp index 1d640af31..0f321465a 100644 --- a/61_UI/AppImGuiListen.cpp +++ b/61_UI/AppImGuiListen.cpp @@ -23,12 +23,12 @@ void App::imguiListen() for (auto& planar : m_planarProjections) { auto* camera = planar->getCamera(); - withOrbitLikeCamera(camera, [&](auto* orbit) + if (camera) { - auto targetPostion = hlsl::transpose(getMatrix3x4As4x4(m_model))[3]; - orbit->target(targetPostion); - orbit->manipulate({}, {}); - }); + const auto targetPosition = hlsl::transpose(getMatrix3x4As4x4(m_model))[3]; + if (camera->trySetSphericalTarget(float64_t3(targetPosition.x, targetPosition.y, targetPosition.z))) + camera->manipulate({}, {}); + } } } diff --git a/61_UI/AppInit.cpp b/61_UI/AppInit.cpp index 1ffa6252f..29c0df700 100644 --- a/61_UI/AppInit.cpp +++ b/61_UI/AppInit.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -267,6 +268,13 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) return std::isfinite(v.x) && std::isfinite(v.y) && std::isfinite(v.z); }; + auto nearlyEqual3 = [](const auto& a, const auto& b, const double epsilon) -> bool + { + return std::abs(static_cast(a.x - b.x)) <= epsilon && + std::abs(static_cast(a.y - b.y)) <= epsilon && + std::abs(static_cast(a.z - b.z)) <= epsilon; + }; + auto computeDelta = [&](ICamera* camera, const float64_t3& beforePos, const float32_t3& beforeEulerDeg, double& outPosDelta, double& outRotDeltaDeg) -> bool { if (!camera) @@ -311,12 +319,133 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) return outPosDelta > 1e-9 || outRotDeltaDeg > 1e-9; }; - auto collectKeyboardVirtualEvents = [&](ICamera* camera, const ui::E_KEY_CODE keyCode) -> std::vector + auto comparePresetToCamera = [&](ICamera* camera, const CameraPreset& preset, const double posEps, const double rotEpsDeg, const double scalarEps) -> bool + { + if (!camera) + return false; + + auto isFiniteQuat = [](const glm::quat& q) -> bool + { + return std::isfinite(q.x) && std::isfinite(q.y) && std::isfinite(q.z) && std::isfinite(q.w); + }; + + auto angleDiffRad = [](double a, double b) -> double + { + double d = std::fmod(a - b + 3.14159265358979323846, 6.28318530717958647692); + if (d < 0.0) + d += 6.28318530717958647692; + return std::abs(d - 3.14159265358979323846); + }; + + const auto& gimbal = camera->getGimbal(); + const auto currentPos = gimbal.getPosition(); + const auto currentOrientation = glm::normalize(gimbal.getOrientation()); + const auto expectedOrientation = glm::normalize(preset.orientation); + if (!isFinite3(currentPos) || !isFiniteQuat(currentOrientation) || !isFiniteQuat(expectedOrientation)) + return false; + + const double dx = static_cast(currentPos.x - preset.position.x); + const double dy = static_cast(currentPos.y - preset.position.y); + const double dz = static_cast(currentPos.z - preset.position.z); + const double posDelta = std::sqrt(dx * dx + dy * dy + dz * dz); + const double orientationDot = std::clamp(static_cast(std::abs(glm::dot(currentOrientation, expectedOrientation))), 0.0, 1.0); + const double rotDeltaDeg = glm::degrees(2.0 * std::acos(orientationDot)); + if (posDelta > posEps || rotDeltaDeg > rotEpsDeg) + return false; + + if (preset.hasTargetPosition || preset.hasDistance || preset.hasOrbitState) + { + ICamera::SphericalTargetState sphericalState; + if (!camera->tryGetSphericalTargetState(sphericalState)) + return false; + if (preset.hasTargetPosition && !nearlyEqual3(sphericalState.target, preset.targetPosition, scalarEps)) + return false; + if (preset.hasDistance && std::abs(static_cast(sphericalState.distance - preset.distance)) > scalarEps) + return false; + if (preset.hasOrbitState) + { + if (angleDiffRad(preset.orbitU, sphericalState.u) > rotEpsDeg * (3.14159265358979323846 / 180.0)) + return false; + if (angleDiffRad(preset.orbitV, sphericalState.v) > rotEpsDeg * (3.14159265358979323846 / 180.0)) + return false; + if (std::abs(static_cast(sphericalState.distance - preset.orbitDistance)) > scalarEps) + return false; + } + } + + if (preset.hasDynamicPerspectiveState) + { + ICamera::DynamicPerspectiveState dynamicState; + if (!camera->tryGetDynamicPerspectiveState(dynamicState)) + return false; + if (std::abs(static_cast(dynamicState.baseFov - preset.dynamicBaseFov)) > scalarEps) + return false; + if (std::abs(static_cast(dynamicState.referenceDistance - preset.dynamicReferenceDistance)) > scalarEps) + return false; + } + + return true; + }; + + auto describePresetMismatch = [&](ICamera* camera, const CameraPreset& preset) -> std::string { - std::vector out; if (!camera) - return out; + return "camera=null"; + + std::ostringstream oss; + const auto& gimbal = camera->getGimbal(); + const auto currentPos = gimbal.getPosition(); + const auto currentOrientation = glm::normalize(gimbal.getOrientation()); + const auto expectedOrientation = glm::normalize(preset.orientation); + const double dx = static_cast(currentPos.x - preset.position.x); + const double dy = static_cast(currentPos.y - preset.position.y); + const double dz = static_cast(currentPos.z - preset.position.z); + const double posDelta = std::sqrt(dx * dx + dy * dy + dz * dz); + const double orientationDot = std::clamp(static_cast(std::abs(glm::dot(currentOrientation, expectedOrientation))), 0.0, 1.0); + const double rotDeltaDeg = glm::degrees(2.0 * std::acos(orientationDot)); + oss << "pos_delta=" << posDelta + << " rot_delta_deg=" << rotDeltaDeg + << " current_pos=(" << currentPos.x << "," << currentPos.y << "," << currentPos.z << ")" + << " expected_pos=(" << preset.position.x << "," << preset.position.y << "," << preset.position.z << ")" + << " current_quat=(" << currentOrientation.x << "," << currentOrientation.y << "," << currentOrientation.z << "," << currentOrientation.w << ")" + << " expected_quat=(" << expectedOrientation.x << "," << expectedOrientation.y << "," << expectedOrientation.z << "," << expectedOrientation.w << ")"; + + if (preset.hasTargetPosition || preset.hasDistance || preset.hasOrbitState) + { + ICamera::SphericalTargetState sphericalState; + if (camera->tryGetSphericalTargetState(sphericalState)) + { + oss << " target=(" << sphericalState.target.x << "," << sphericalState.target.y << "," << sphericalState.target.z << ")" + << " distance=" << sphericalState.distance + << " orbit_u=" << sphericalState.u + << " orbit_v=" << sphericalState.v; + } + else + { + oss << " spherical_state=unavailable"; + } + } + + if (preset.hasDynamicPerspectiveState) + { + ICamera::DynamicPerspectiveState dynamicState; + if (camera->tryGetDynamicPerspectiveState(dynamicState)) + { + oss << " dynamic_base_fov=" << dynamicState.baseFov + << " dynamic_reference_distance=" << dynamicState.referenceDistance; + } + else + { + oss << " dynamic_perspective_state=unavailable"; + } + } + + return oss.str(); + }; + auto collectKeyboardVirtualEvents = [&](CGimbalInputBinder& inputBinder, const ui::E_KEY_CODE keyCode) -> std::vector + { + std::vector out; static std::chrono::microseconds smokeTimestamp = std::chrono::microseconds::zero(); smokeTimestamp += std::chrono::microseconds(16667); const auto pressTs = smokeTimestamp; @@ -328,52 +457,49 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) uint32_t potentialCount = 0u; uint32_t generatedCount = 0u; - camera->beginInputProcessing(pressTs); - camera->processKeyboard(nullptr, potentialCount, {}); + inputBinder.beginInputProcessing(pressTs); + inputBinder.processKeyboard(nullptr, potentialCount, {}); if (potentialCount) { std::vector warmup(potentialCount); generatedCount = potentialCount; - camera->processKeyboard(warmup.data(), generatedCount, { &pressEvent, 1u }); + inputBinder.processKeyboard(warmup.data(), generatedCount, { &pressEvent, 1u }); } - camera->endInputProcessing(); + inputBinder.endInputProcessing(); smokeTimestamp += std::chrono::microseconds(16667); const auto sampleTs = smokeTimestamp; - camera->beginInputProcessing(sampleTs); - camera->processKeyboard(nullptr, potentialCount, {}); + inputBinder.beginInputProcessing(sampleTs); + inputBinder.processKeyboard(nullptr, potentialCount, {}); out.resize(potentialCount); if (potentialCount) { generatedCount = potentialCount; - camera->processKeyboard(out.data(), generatedCount, {}); + inputBinder.processKeyboard(out.data(), generatedCount, {}); } - camera->endInputProcessing(); + inputBinder.endInputProcessing(); out.resize(generatedCount); return out; }; - auto collectMouseVirtualEvents = [&](ICamera* camera, std::span mouseEvents) -> std::vector + auto collectMouseVirtualEvents = [&](CGimbalInputBinder& inputBinder, std::span mouseEvents) -> std::vector { std::vector out; - if (!camera) - return out; - static std::chrono::microseconds smokeTimestamp = std::chrono::microseconds::zero(); smokeTimestamp += std::chrono::microseconds(16667); const auto ts = smokeTimestamp; uint32_t potentialCount = 0u; uint32_t generatedCount = 0u; - camera->beginInputProcessing(ts); - camera->processMouse(nullptr, potentialCount, {}); + inputBinder.beginInputProcessing(ts); + inputBinder.processMouse(nullptr, potentialCount, {}); out.resize(potentialCount); if (potentialCount) { generatedCount = potentialCount; - camera->processMouse(out.data(), generatedCount, mouseEvents); + inputBinder.processMouse(out.data(), generatedCount, mouseEvents); } - camera->endInputProcessing(); + inputBinder.endInputProcessing(); out.resize(generatedCount); return out; }; @@ -418,6 +544,54 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) camera->updateKeyboardMapping([&](auto& map) { map = camera->getKeyboardMappingPreset(); }); camera->updateMouseMapping([&](auto& map) { map = camera->getMouseMappingPreset(); }); camera->updateImguizmoMapping([&](auto& map) { map = camera->getImguizmoMappingPreset(); }); + CGimbalInputBinder inputBinder; + inputBinder.copyPresetLayoutFrom(*camera); + + const auto initialPreset = capturePreset(camera, "smoke-initial"); + if (!applyPresetToCamera(camera, initialPreset)) + return fail("Preset no-op smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\"."); + + if (initialPreset.hasTargetPosition) + { + CameraPreset shiftedPreset = initialPreset; + shiftedPreset.targetPosition += float64_t3(0.5, -0.25, 0.75); + + const auto shiftedResult = applyPresetToCameraDetailed(camera, shiftedPreset); + if (!shiftedResult.succeeded() || !shiftedResult.changed() || !shiftedResult.exact) + return fail("Preset target apply smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\"."); + + ICamera::SphericalTargetState shiftedState; + if (!camera->tryGetSphericalTargetState(shiftedState) || !nearlyEqual3(shiftedState.target, shiftedPreset.targetPosition, 1e-9)) + return fail("Preset target writeback smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\"."); + + const auto restoredResult = applyPresetToCameraDetailed(camera, initialPreset); + if (!restoredResult.succeeded() || !restoredResult.exact) + return fail("Preset restore smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\"."); + + ICamera::SphericalTargetState restoredState; + if (!camera->tryGetSphericalTargetState(restoredState) || !nearlyEqual3(restoredState.target, initialPreset.targetPosition, 1e-9)) + return fail("Preset target restore smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\"."); + + if (!comparePresetToCamera(camera, initialPreset, 1e-6, 1e-4, 1e-9)) + return fail("Preset restore mismatch smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\"."); + } + + if (initialPreset.hasDynamicPerspectiveState) + { + CameraPreset shiftedPreset = initialPreset; + shiftedPreset.dynamicBaseFov = std::clamp(initialPreset.dynamicBaseFov + 7.5f, 10.0f, 150.0f); + if (std::abs(static_cast(shiftedPreset.dynamicBaseFov - initialPreset.dynamicBaseFov)) < 1e-6) + shiftedPreset.dynamicBaseFov = std::max(10.0f, initialPreset.dynamicBaseFov - 7.5f); + shiftedPreset.dynamicReferenceDistance = std::max(0.1f, initialPreset.dynamicReferenceDistance + 1.25f); + + const auto shiftedResult = applyPresetToCameraDetailed(camera, shiftedPreset); + if (!shiftedResult.succeeded() || !shiftedResult.changed() || !comparePresetToCamera(camera, shiftedPreset, 1e-6, 0.1, 1e-6)) + return fail("Preset dynamic perspective apply smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\". " + describePresetMismatch(camera, shiftedPreset)); + + const auto restoredResult = applyPresetToCameraDetailed(camera, initialPreset); + if (!restoredResult.succeeded() || !comparePresetToCamera(camera, initialPreset, 1e-6, 0.1, 1e-6)) + return fail("Preset dynamic perspective restore smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\". " + describePresetMismatch(camera, initialPreset)); + } const uint32_t allowed = camera->getAllowedVirtualEvents(); std::vector directEvents; @@ -459,14 +633,28 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) double directRotDelta = 0.0; if (!manipulateAndMeasure(camera, directEvents, directPosDelta, directRotDelta)) return fail("Direct manipulate smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\"."); + { + const auto modifiedPreset = capturePreset(camera, "smoke-direct"); + const auto restoreInitial = applyPresetToCameraDetailed(camera, initialPreset); + if (!restoreInitial.succeeded() || !comparePresetToCamera(camera, initialPreset, 1e-3, 0.1, 1e-4)) + return fail("Preset restore from direct smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\". " + describePresetMismatch(camera, initialPreset)); + + const auto applyModified = applyPresetToCameraDetailed(camera, modifiedPreset); + if (!applyModified.succeeded() || !applyModified.changed() || !comparePresetToCamera(camera, modifiedPreset, 1e-3, 0.1, 1e-4)) + return fail("Preset apply from direct smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\". " + describePresetMismatch(camera, modifiedPreset)); + + const auto restoreAgain = applyPresetToCameraDetailed(camera, initialPreset); + if (!restoreAgain.succeeded() || !comparePresetToCamera(camera, initialPreset, 1e-3, 0.1, 1e-4)) + return fail("Preset final restore smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\". " + describePresetMismatch(camera, initialPreset)); + } bool keyboardOk = false; double keyboardPosDelta = 0.0; double keyboardRotDelta = 0.0; for (const auto key : keyboardCandidates) { - camera->updateKeyboardMapping([&](auto& map) { map = camera->getKeyboardMappingPreset(); }); - auto keyboardEvents = collectKeyboardVirtualEvents(camera, key); + inputBinder.copyPresetLayoutFrom(*camera); + auto keyboardEvents = collectKeyboardVirtualEvents(inputBinder, key); if (keyboardEvents.empty()) continue; if (manipulateAndMeasure(camera, keyboardEvents, keyboardPosDelta, keyboardRotDelta)) @@ -507,8 +695,8 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) if (isOrbitLikeCamera(camera) && hasBlockedMovement) return fail("Orbit mouse movement gate failed for camera \"" + std::string(camera->getIdentifier()) + "\"."); - camera->updateMouseMapping([&](auto& map) { map = camera->getMouseMappingPreset(); }); - auto mouseMoveEvents = collectMouseVirtualEvents(camera, { filteredMoveLookDown.data(), filteredMoveLookDown.size() }); + inputBinder.copyPresetLayoutFrom(*camera); + auto mouseMoveEvents = collectMouseVirtualEvents(inputBinder, { filteredMoveLookDown.data(), filteredMoveLookDown.size() }); if (mouseMoveEvents.empty()) return fail("Mouse move virtual events missing for camera \"" + std::string(camera->getIdentifier()) + "\"."); if (!manipulateAndMeasure(camera, mouseMoveEvents, mouseMovePosDelta, mouseMoveRotDelta)) @@ -527,8 +715,8 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) const std::array rawScroll = { scrollEv }; auto filteredScroll = filterOrbitMouseEvents(camera, rawScroll, false); - camera->updateMouseMapping([&](auto& map) { map = camera->getMouseMappingPreset(); }); - auto mouseScrollEvents = collectMouseVirtualEvents(camera, { filteredScroll.data(), filteredScroll.size() }); + inputBinder.copyPresetLayoutFrom(*camera); + auto mouseScrollEvents = collectMouseVirtualEvents(inputBinder, { filteredScroll.data(), filteredScroll.size() }); if (mouseScrollEvents.empty()) return fail("Mouse scroll virtual events missing for camera \"" + std::string(camera->getIdentifier()) + "\"."); if (!manipulateAndMeasure(camera, mouseScrollEvents, mouseScrollPosDelta, mouseScrollRotDelta)) diff --git a/61_UI/AppUpdate.cpp b/61_UI/AppUpdate.cpp index 85e798746..3b3cc8f1b 100644 --- a/61_UI/AppUpdate.cpp +++ b/61_UI/AppUpdate.cpp @@ -324,17 +324,20 @@ void App::update() uint32_t vKeyboardEventsCount = {}; uint32_t vMouseEventsCount = {}; - projection.beginInputProcessing(m_nextPresentationTimestamp); + syncWindowInputBinding(binding); + auto& inputBinding = binding.inputBinding; + + inputBinding.beginInputProcessing(m_nextPresentationTimestamp); { - projection.processKeyboard(nullptr, vKeyboardEventsCount, {}); - projection.processMouse(nullptr, vMouseEventsCount, {}); + inputBinding.processKeyboard(nullptr, vKeyboardEventsCount, {}); + inputBinding.processMouse(nullptr, vMouseEventsCount, {}); const auto totalCount = vKeyboardEventsCount + vMouseEventsCount; if (virtualEvents.size() < totalCount) virtualEvents.resize(totalCount); auto* output = virtualEvents.data(); - projection.processKeyboard(output, vKeyboardEventsCount, { cameraKeyboardEvents.data(), cameraKeyboardEvents.size() }); + inputBinding.processKeyboard(output, vKeyboardEventsCount, { cameraKeyboardEvents.data(), cameraKeyboardEvents.size() }); for (uint32_t i = 0u; i < vKeyboardEventsCount; ++i) output[i].magnitude *= m_cameraControls.keyboardScale; output += vKeyboardEventsCount; @@ -351,16 +354,16 @@ void App::update() continue; filteredOrbitMouseEvents.emplace_back(ev); } - projection.processMouse(output, vMouseEventsCount, { filteredOrbitMouseEvents.data(), filteredOrbitMouseEvents.size() }); + inputBinding.processMouse(output, vMouseEventsCount, { filteredOrbitMouseEvents.data(), filteredOrbitMouseEvents.size() }); } else { - projection.processMouse(output, vMouseEventsCount, { cameraMouseEvents.data(), cameraMouseEvents.size() }); + inputBinding.processMouse(output, vMouseEventsCount, { cameraMouseEvents.data(), cameraMouseEvents.size() }); } vCount = vKeyboardEventsCount + vMouseEventsCount; } - projection.endInputProcessing(); + inputBinding.endInputProcessing(); if (vCount) { @@ -437,16 +440,18 @@ void App::update() static std::vector imguizmoEvents(0x20); uint32_t vCount = 0u; + CGimbalInputBinder imguizmoBinding; + imguizmoBinding.copyPresetLayoutFrom(*camera); - camera->beginInputProcessing(m_nextPresentationTimestamp); + imguizmoBinding.beginInputProcessing(m_nextPresentationTimestamp); { - camera->processImguizmo(nullptr, vCount, {}); + imguizmoBinding.processImguizmo(nullptr, vCount, {}); if (imguizmoEvents.size() < vCount) imguizmoEvents.resize(vCount); - camera->processImguizmo(imguizmoEvents.data(), vCount, { scriptedImguizmo.data(), scriptedImguizmo.size() }); + imguizmoBinding.processImguizmo(imguizmoEvents.data(), vCount, { scriptedImguizmo.data(), scriptedImguizmo.size() }); } - camera->endInputProcessing(); + imguizmoBinding.endInputProcessing(); if (vCount) { diff --git a/61_UI/include/app/App.hpp b/61_UI/include/app/App.hpp index 43aa47527..066e044cd 100644 --- a/61_UI/include/app/App.hpp +++ b/61_UI/include/app/App.hpp @@ -486,12 +486,17 @@ class App final : public examples::SimpleWindowedApplication std::string identifier; float64_t3 position = float64_t3(0.0); glm::quat orientation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f); + float64_t3 targetPosition = float64_t3(0.0); + bool hasTargetPosition = false; float distance = 0.f; bool hasDistance = false; double orbitU = 0.0; double orbitV = 0.0; float orbitDistance = 0.f; bool hasOrbitState = false; + float dynamicBaseFov = 0.f; + float dynamicReferenceDistance = 0.f; + bool hasDynamicPerspectiveState = false; }; struct CameraKeyframe @@ -546,72 +551,51 @@ class App final : public examples::SimpleWindowedApplication inline bool isOrbitLikeCamera(ICamera* camera) { - return dynamic_cast(camera); + return camera && camera->hasCapability(ICamera::SphericalTarget); } - template - inline bool withOrbitLikeCamera(ICamera* camera, Fn&& fn) + inline std::string_view getCameraTypeLabel(const ICamera* camera) const { - if (auto* orbit = dynamic_cast(camera)) + if (!camera) + return "Unknown"; + + switch (camera->getKind()) { - fn(orbit); - return true; + case ICamera::CameraKind::FPS: return "FPS"; + case ICamera::CameraKind::Free: return "Free"; + case ICamera::CameraKind::Orbit: return "Orbit"; + case ICamera::CameraKind::Arcball: return "Arcball"; + case ICamera::CameraKind::Turntable: return "Turntable"; + case ICamera::CameraKind::TopDown: return "TopDown"; + case ICamera::CameraKind::Isometric: return "Isometric"; + case ICamera::CameraKind::Chase: return "Chase"; + case ICamera::CameraKind::Dolly: return "Dolly"; + case ICamera::CameraKind::DollyZoom: return "Dolly Zoom"; + case ICamera::CameraKind::Path: return "Path"; + default: return "Unknown"; } - return false; - } - - inline std::string_view getCameraTypeLabel(const ICamera* camera) const - { - if (dynamic_cast(camera)) - return "FPS"; - if (dynamic_cast(camera)) - return "Free"; - if (dynamic_cast(camera)) - return "Orbit"; - if (dynamic_cast(camera)) - return "Arcball"; - if (dynamic_cast(camera)) - return "Turntable"; - if (dynamic_cast(camera)) - return "TopDown"; - if (dynamic_cast(camera)) - return "Isometric"; - if (dynamic_cast(camera)) - return "Chase"; - if (dynamic_cast(camera)) - return "Dolly"; - if (dynamic_cast(camera)) - return "Dolly Zoom"; - if (dynamic_cast(camera)) - return "Path"; - return "Unknown"; } inline std::string_view getCameraTypeDescription(const ICamera* camera) const { - if (dynamic_cast(camera)) - return "First-person WASD + mouse look"; - if (dynamic_cast(camera)) - return "Free-fly 6DOF with full rotation"; - if (dynamic_cast(camera)) - return "Orbit around target with dolly"; - if (dynamic_cast(camera)) - return "Arcball trackball around target"; - if (dynamic_cast(camera)) - return "Turntable yaw/pitch around target"; - if (dynamic_cast(camera)) - return "Fixed pitch top-down pan"; - if (dynamic_cast(camera)) - return "Fixed isometric view with pan"; - if (dynamic_cast(camera)) - return "Target follow with chase controls"; - if (dynamic_cast(camera)) - return "Rig truck/dolly with look-at"; - if (dynamic_cast(camera)) - return "Orbit with dolly-zoom FOV"; - if (dynamic_cast(camera)) - return "Move along a target path"; - return "Unspecified camera behavior"; + if (!camera) + return "Unspecified camera behavior"; + + switch (camera->getKind()) + { + case ICamera::CameraKind::FPS: return "First-person WASD + mouse look"; + case ICamera::CameraKind::Free: return "Free-fly 6DOF with full rotation"; + case ICamera::CameraKind::Orbit: return "Orbit around target with dolly"; + case ICamera::CameraKind::Arcball: return "Arcball trackball around target"; + case ICamera::CameraKind::Turntable: return "Turntable yaw/pitch around target"; + case ICamera::CameraKind::TopDown: return "Fixed pitch top-down pan"; + case ICamera::CameraKind::Isometric: return "Fixed isometric view with pan"; + case ICamera::CameraKind::Chase: return "Target follow with chase controls"; + case ICamera::CameraKind::Dolly: return "Rig truck/dolly with look-at"; + case ICamera::CameraKind::DollyZoom: return "Orbit with dolly-zoom FOV"; + case ICamera::CameraKind::Path: return "Move along a target path"; + default: return "Unspecified camera behavior"; + } } inline void syncVisualDebugWindowBindings() @@ -992,10 +976,11 @@ class App final : public examples::SimpleWindowedApplication } const std::string lineBottom(lineBottomBuffer); std::string lineHint = std::string(cameraHint); - if (auto* dollyZoom = dynamic_cast(camera)) + float dynamicFov = 0.0f; + if (camera && camera->tryGetDynamicPerspectiveFov(dynamicFov)) { char fovBuffer[96] = {}; - std::snprintf(fovBuffer, sizeof(fovBuffer), " | Dynamic FOV %.2f deg", dollyZoom->computeDollyFov()); + std::snprintf(fovBuffer, sizeof(fovBuffer), " | Dynamic FOV %.2f deg", dynamicFov); lineHint += fovBuffer; } @@ -1046,13 +1031,15 @@ class App final : public examples::SimpleWindowedApplication inline void applyDollyZoomProjection(ICamera* camera, IPlanarProjection::CProjection& projection) { - auto* dolly = dynamic_cast(camera); - if (!dolly) + if (!camera) return; const auto& params = projection.getParameters(); if (params.m_type != IPlanarProjection::CProjection::Perspective) return; - projection.setPerspective(params.m_zNear, params.m_zFar, dolly->computeDollyFov()); + float dynamicFov = 0.0f; + if (!camera->tryGetDynamicPerspectiveFov(dynamicFov)) + return; + projection.setPerspective(params.m_zNear, params.m_zFar, dynamicFov); } inline CameraPreset capturePreset(ICamera* camera, const std::string& name) @@ -1067,29 +1054,40 @@ class App final : public examples::SimpleWindowedApplication preset.position = gimbal.getPosition(); preset.orientation = gimbal.getOrientation(); - auto captureOrbit = [&](auto* orbit) + ICamera::SphericalTargetState sphericalState; + if (camera->tryGetSphericalTargetState(sphericalState)) { - preset.distance = orbit->getDistance(); + preset.targetPosition = sphericalState.target; + preset.hasTargetPosition = true; + preset.distance = sphericalState.distance; preset.hasDistance = true; - preset.orbitDistance = orbit->getDistance(); - preset.orbitU = orbit->getU(); - preset.orbitV = orbit->getV(); + preset.orbitDistance = sphericalState.distance; + preset.orbitU = sphericalState.u; + preset.orbitV = sphericalState.v; preset.hasOrbitState = true; - }; + } - withOrbitLikeCamera(camera, captureOrbit); + ICamera::DynamicPerspectiveState dynamicPerspectiveState; + if (camera->tryGetDynamicPerspectiveState(dynamicPerspectiveState)) + { + preset.dynamicBaseFov = dynamicPerspectiveState.baseFov; + preset.dynamicReferenceDistance = dynamicPerspectiveState.referenceDistance; + preset.hasDynamicPerspectiveState = true; + } return preset; } - inline bool applyPresetToCamera(ICamera* camera, const CameraPreset& preset) + inline CTargetPoseController::SApplyResult applyPresetToCameraDetailed(ICamera* camera, const CameraPreset& preset) { + CTargetPose target; if (!camera) - return false; + return {}; - CTargetPose target; target.position = preset.position; target.orientation = preset.orientation; + target.hasTargetPosition = preset.hasTargetPosition; + target.targetPosition = preset.targetPosition; target.hasDistance = preset.hasDistance; target.distance = preset.distance; target.hasOrbitState = preset.hasOrbitState; @@ -1097,7 +1095,63 @@ class App final : public examples::SimpleWindowedApplication target.orbitV = preset.orbitV; target.orbitDistance = preset.orbitDistance; - return m_targetPoseController.apply(camera, target); + auto result = m_targetPoseController.applyDetailed(camera, target); + if (!preset.hasDynamicPerspectiveState) + return result; + + ICamera::DynamicPerspectiveState beforeState; + if (!camera->tryGetDynamicPerspectiveState(beforeState)) + { + result.exact = false; + return result; + } + + const ICamera::DynamicPerspectiveState desiredState = { + .baseFov = preset.dynamicBaseFov, + .referenceDistance = preset.dynamicReferenceDistance + }; + if (!camera->trySetDynamicPerspectiveState(desiredState)) + { + result.exact = false; + return result; + } + + ICamera::DynamicPerspectiveState afterState; + if (!camera->tryGetDynamicPerspectiveState(afterState)) + { + result.exact = false; + return result; + } + + const auto nearlyEqual = [](const float a, const float b) + { + return std::abs(static_cast(a - b)) <= 1e-6; + }; + + const bool dynamicChanged = !nearlyEqual(beforeState.baseFov, afterState.baseFov) || + !nearlyEqual(beforeState.referenceDistance, afterState.referenceDistance); + const bool dynamicExact = nearlyEqual(afterState.baseFov, desiredState.baseFov) && + nearlyEqual(afterState.referenceDistance, desiredState.referenceDistance); + + if (dynamicChanged) + { + if (!result.succeeded() || !result.changed()) + result.status = CTargetPoseController::SApplyResult::EStatus::AppliedAbsoluteOnly; + else if (result.status == CTargetPoseController::SApplyResult::EStatus::AppliedVirtualEvents) + result.status = CTargetPoseController::SApplyResult::EStatus::AppliedAbsoluteAndVirtualEvents; + } + else if (!result.succeeded() && dynamicExact) + { + result.status = CTargetPoseController::SApplyResult::EStatus::AlreadySatisfied; + } + + result.exact = result.exact && dynamicExact; + return result; + } + + inline bool applyPresetToCamera(ICamera* camera, const CameraPreset& preset) + { + return applyPresetToCameraDetailed(camera, preset).succeeded(); } inline void appendVirtualEventLog(std::string_view source, std::string_view controller, uint32_t planarIx, ICamera* camera, const CVirtualGimbalEvent* events, uint32_t count) @@ -1139,17 +1193,19 @@ class App final : public examples::SimpleWindowedApplication if (!m_cameraConstraints.enabled || !camera) return; - auto clampOrbitDistance = [&](auto* orbit) + if (camera->hasCapability(ICamera::SphericalTarget)) { if (m_cameraConstraints.clampDistance) { - const float clamped = std::clamp(orbit->getDistance(), m_cameraConstraints.minDistance, m_cameraConstraints.maxDistance); - orbit->setDistance(clamped); + ICamera::SphericalTargetState sphericalState; + if (camera->tryGetSphericalTargetState(sphericalState)) + { + const float clamped = std::clamp(sphericalState.distance, m_cameraConstraints.minDistance, m_cameraConstraints.maxDistance); + camera->trySetSphericalDistance(clamped); + } } - }; - - if (withOrbitLikeCamera(camera, clampOrbitDistance)) return; + } if (!(m_cameraConstraints.clampPitch || m_cameraConstraints.clampYaw || m_cameraConstraints.clampRoll)) return; @@ -1331,6 +1387,13 @@ class App final : public examples::SimpleWindowedApplication CameraPreset blended; blended.position = a.preset.position + (b.preset.position - a.preset.position) * alpha; blended.orientation = glm::slerp(a.preset.orientation, b.preset.orientation, static_cast(alpha)); + blended.hasTargetPosition = a.preset.hasTargetPosition || b.preset.hasTargetPosition; + if (blended.hasTargetPosition) + { + const auto ta = a.preset.hasTargetPosition ? a.preset.targetPosition : b.preset.targetPosition; + const auto tb = b.preset.hasTargetPosition ? b.preset.targetPosition : a.preset.targetPosition; + blended.targetPosition = ta + (tb - ta) * alpha; + } blended.hasDistance = a.preset.hasDistance || b.preset.hasDistance; if (blended.hasDistance) { @@ -1352,6 +1415,17 @@ class App final : public examples::SimpleWindowedApplication blended.orbitV = va + (vb - va) * alpha; blended.orbitDistance = da + (db - da) * static_cast(alpha); } + blended.hasDynamicPerspectiveState = a.preset.hasDynamicPerspectiveState || b.preset.hasDynamicPerspectiveState; + if (blended.hasDynamicPerspectiveState) + { + const float fa = a.preset.hasDynamicPerspectiveState ? a.preset.dynamicBaseFov : b.preset.dynamicBaseFov; + const float fb = b.preset.hasDynamicPerspectiveState ? b.preset.dynamicBaseFov : a.preset.dynamicBaseFov; + const float ra = a.preset.hasDynamicPerspectiveState ? a.preset.dynamicReferenceDistance : b.preset.dynamicReferenceDistance; + const float rb = b.preset.hasDynamicPerspectiveState ? b.preset.dynamicReferenceDistance : a.preset.dynamicReferenceDistance; + + blended.dynamicBaseFov = fa + (fb - fa) * static_cast(alpha); + blended.dynamicReferenceDistance = ra + (rb - ra) * static_cast(alpha); + } applyPresetToTargets(blended); } @@ -1368,6 +1442,8 @@ class App final : public examples::SimpleWindowedApplication j["identifier"] = preset.identifier; j["position"] = { preset.position.x, preset.position.y, preset.position.z }; j["orientation"] = { preset.orientation.x, preset.orientation.y, preset.orientation.z, preset.orientation.w }; + if (preset.hasTargetPosition) + j["target_position"] = { preset.targetPosition.x, preset.targetPosition.y, preset.targetPosition.z }; if (preset.hasDistance) j["distance"] = preset.distance; if (preset.hasOrbitState) @@ -1376,6 +1452,11 @@ class App final : public examples::SimpleWindowedApplication j["orbit_v"] = preset.orbitV; j["orbit_distance"] = preset.orbitDistance; } + if (preset.hasDynamicPerspectiveState) + { + j["dynamic_base_fov"] = preset.dynamicBaseFov; + j["dynamic_reference_distance"] = preset.dynamicReferenceDistance; + } root["presets"].push_back(std::move(j)); } @@ -1420,6 +1501,12 @@ class App final : public examples::SimpleWindowedApplication arr[2].get() ); } + if (entry.contains("target_position") && entry["target_position"].is_array()) + { + auto arr = entry["target_position"]; + preset.targetPosition = float64_t3(arr[0].get(), arr[1].get(), arr[2].get()); + preset.hasTargetPosition = true; + } if (entry.contains("distance")) { preset.distance = entry["distance"].get(); @@ -1440,6 +1527,16 @@ class App final : public examples::SimpleWindowedApplication preset.orbitDistance = entry["orbit_distance"].get(); preset.hasOrbitState = true; } + if (entry.contains("dynamic_base_fov")) + { + preset.dynamicBaseFov = entry["dynamic_base_fov"].get(); + preset.hasDynamicPerspectiveState = true; + } + if (entry.contains("dynamic_reference_distance")) + { + preset.dynamicReferenceDistance = entry["dynamic_reference_distance"].get(); + preset.hasDynamicPerspectiveState = true; + } m_presets.emplace_back(std::move(preset)); } @@ -1733,8 +1830,11 @@ class App final : public examples::SimpleWindowedApplication bool isOrthographicProjection = false; float aspectRatio = 16.f / 9.f; bool leftHandedProjection = true; + CGimbalInputBinder inputBinding; std::optional boundProjectionIx = std::nullopt, lastBoundPerspectivePresetProjectionIx = std::nullopt, lastBoundOrthoPresetProjectionIx = std::nullopt; + std::optional inputBindingProjectionIx = std::nullopt; + uint32_t inputBindingPlanarIx = std::numeric_limits::max(); inline void pickDefaultProjections(const planar_projections_range_t& projections) { @@ -1756,9 +1856,56 @@ class App final : public examples::SimpleWindowedApplication init(lastBoundPerspectivePresetProjectionIx = std::nullopt, IPlanarProjection::CProjection::Perspective); init(lastBoundOrthoPresetProjectionIx = std::nullopt, IPlanarProjection::CProjection::Orthographic); boundProjectionIx = lastBoundPerspectivePresetProjectionIx.value(); + inputBindingProjectionIx = std::nullopt; + inputBindingPlanarIx = std::numeric_limits::max(); } }; + inline void syncWindowInputBinding(windowControlBinding& 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]); + binding.inputBindingPlanarIx = binding.activePlanarIx; + binding.inputBindingProjectionIx = projectionIx; + } + + inline void syncWindowInputBindingToProjection(windowControlBinding& 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; + + binding.inputBinding.copyBindingLayoutTo(projections[projectionIx]); + binding.inputBindingPlanarIx = binding.activePlanarIx; + binding.inputBindingProjectionIx = projectionIx; + } + struct ScriptedInputEvent { enum class Type : uint8_t diff --git a/61_UI/include/common.hpp b/61_UI/include/common.hpp index 57fdd5169..cc9b909bc 100644 --- a/61_UI/include/common.hpp +++ b/61_UI/include/common.hpp @@ -19,6 +19,7 @@ #include "camera/CDollyZoomCamera.hpp" #include "camera/CPathCamera.hpp" #include "camera/CTargetPoseController.hpp" +#include "camera/CGimbalInputBinder.hpp" #include "camera/CCubeProjection.hpp" #include "camera/CLinearProjection.hpp" @@ -56,6 +57,7 @@ using nbl::hlsl::CDollyZoomCamera; using nbl::hlsl::CPathCamera; using nbl::hlsl::CTargetPose; using nbl::hlsl::CTargetPoseController; +using nbl::hlsl::CGimbalInputBinder; using nbl::hlsl::IPlanarProjection; using nbl::hlsl::CPlanarProjection; using nbl::hlsl::IGimbalController; diff --git a/common/include/camera/CArcballCamera.hpp b/common/include/camera/CArcballCamera.hpp index b8e21dcf4..6ae68ce8a 100644 --- a/common/include/camera/CArcballCamera.hpp +++ b/common/include/camera/CArcballCamera.hpp @@ -61,6 +61,7 @@ class CArcballCamera final : public CSphericalTargetCamera } virtual const uint32_t getAllowedVirtualEvents() override { return AllowedVirtualEvents; } + virtual CameraKind getKind() const override { return CameraKind::Arcball; } virtual const std::string_view getIdentifier() override { return "Arcball Camera"; } static inline constexpr float MinDistance = base_t::MinDistance; diff --git a/common/include/camera/CChaseCamera.hpp b/common/include/camera/CChaseCamera.hpp index 6b77be94d..afd526670 100644 --- a/common/include/camera/CChaseCamera.hpp +++ b/common/include/camera/CChaseCamera.hpp @@ -68,6 +68,7 @@ class CChaseCamera final : public CSphericalTargetCamera } virtual const uint32_t getAllowedVirtualEvents() override { return AllowedVirtualEvents; } + virtual CameraKind getKind() const override { return CameraKind::Chase; } virtual const std::string_view getIdentifier() override { return "Chase Camera"; } private: diff --git a/common/include/camera/CCubeProjection.hpp b/common/include/camera/CCubeProjection.hpp index d47c5c6b8..d03ff0840 100644 --- a/common/include/camera/CCubeProjection.hpp +++ b/common/include/camera/CCubeProjection.hpp @@ -50,9 +50,15 @@ class CCubeProjection final : public IPerspectiveProjection, public IProjection return core::smart_refctd_ptr(new CCubeProjection(core::smart_refctd_ptr(camera)), core::dont_grab); } - virtual std::span getLinearProjections() const override + virtual uint32_t getLinearProjectionCount() const override { - return { reinterpret_cast(m_quads.data()), m_quads.size() }; + return static_cast(m_quads.size()); + } + + virtual const ILinearProjection::CProjection& getLinearProjection(uint32_t index) const override + { + assert(index < m_quads.size()); + return m_quads[index]; } void transformCube() @@ -91,4 +97,4 @@ class CCubeProjection final : public IPerspectiveProjection, public IProjection } // nbl::hlsl namespace -#endif // _NBL_CCUBE_PROJECTION_HPP_ \ No newline at end of file +#endif // _NBL_CCUBE_PROJECTION_HPP_ diff --git a/common/include/camera/CDollyCamera.hpp b/common/include/camera/CDollyCamera.hpp index e74b56214..96d0082dc 100644 --- a/common/include/camera/CDollyCamera.hpp +++ b/common/include/camera/CDollyCamera.hpp @@ -52,6 +52,7 @@ class CDollyCamera final : public CSphericalTargetCamera } virtual const uint32_t getAllowedVirtualEvents() override { return AllowedVirtualEvents; } + virtual CameraKind getKind() const override { return CameraKind::Dolly; } virtual const std::string_view getIdentifier() override { return "Dolly Camera"; } private: diff --git a/common/include/camera/CDollyZoomCamera.hpp b/common/include/camera/CDollyZoomCamera.hpp index d17ba27b9..2fffa826c 100644 --- a/common/include/camera/CDollyZoomCamera.hpp +++ b/common/include/camera/CDollyZoomCamera.hpp @@ -64,6 +64,28 @@ class CDollyZoomCamera final : public CSphericalTargetCamera } virtual const uint32_t getAllowedVirtualEvents() override { return AllowedVirtualEvents; } + virtual CameraKind getKind() const override { return CameraKind::DollyZoom; } + virtual uint32_t getCapabilities() const override { return base_t::getCapabilities() | base_t::DynamicPerspectiveFov; } + virtual bool tryGetDynamicPerspectiveFov(float& outFov) const override + { + outFov = computeDollyFov(); + return true; + } + virtual bool tryGetDynamicPerspectiveState(DynamicPerspectiveState& out) const override + { + out.baseFov = m_baseFov; + out.referenceDistance = m_referenceDistance; + return true; + } + virtual bool trySetDynamicPerspectiveState(const DynamicPerspectiveState& state) override + { + if (!std::isfinite(state.baseFov) || !std::isfinite(state.referenceDistance) || state.referenceDistance <= 0.f) + return false; + + m_baseFov = state.baseFov; + m_referenceDistance = state.referenceDistance; + return true; + } virtual const std::string_view getIdentifier() override { return "Dolly Zoom Camera"; } private: diff --git a/common/include/camera/CFPSCamera.hpp b/common/include/camera/CFPSCamera.hpp index bc9f0cf21..fdb0ac46f 100644 --- a/common/include/camera/CFPSCamera.hpp +++ b/common/include/camera/CFPSCamera.hpp @@ -116,6 +116,11 @@ class CFPSCamera final : public ICamera return AllowedVirtualEvents; } + virtual CameraKind getKind() const override + { + return CameraKind::FPS; + } + virtual const std::string_view getIdentifier() override { return "FPS Camera"; diff --git a/common/include/camera/CFreeLockCamera.hpp b/common/include/camera/CFreeLockCamera.hpp index 9d4a7a0f0..468b015c2 100644 --- a/common/include/camera/CFreeLockCamera.hpp +++ b/common/include/camera/CFreeLockCamera.hpp @@ -65,6 +65,11 @@ class CFreeCamera final : public ICamera return AllowedVirtualEvents; } + virtual CameraKind getKind() const override + { + return CameraKind::Free; + } + virtual const std::string_view getIdentifier() override { return "Free-Look Camera"; diff --git a/common/include/camera/CGimbalInputBinder.hpp b/common/include/camera/CGimbalInputBinder.hpp new file mode 100644 index 000000000..d7e0384b3 --- /dev/null +++ b/common/include/camera/CGimbalInputBinder.hpp @@ -0,0 +1,56 @@ +#ifndef _NBL_C_GIMBAL_INPUT_BINDER_HPP_ +#define _NBL_C_GIMBAL_INPUT_BINDER_HPP_ + +#include "IGimbalController.hpp" + +namespace nbl::hlsl +{ + +class CGimbalInputBinder final : public IGimbalController +{ +public: + using base_t = IGimbalController; + using base_t::base_t; + + inline void clearBindingLayout() + { + updateKeyboardMapping([](auto& map) { map.clear(); }); + updateMouseMapping([](auto& map) { map.clear(); }); + updateImguizmoMapping([](auto& map) { map.clear(); }); + } + + inline void copyBindingLayoutFrom(const IGimbalManipulateEncoder& encoder) + { + updateKeyboardMapping([&](auto& map) { map = sanitizeMapping(encoder.getKeyboardVirtualEventMap()); }); + updateMouseMapping([&](auto& map) { map = sanitizeMapping(encoder.getMouseVirtualEventMap()); }); + updateImguizmoMapping([&](auto& map) { map = sanitizeMapping(encoder.getImguizmoVirtualEventMap()); }); + } + + inline void copyPresetLayoutFrom(const IGimbalManipulateEncoder& encoder) + { + updateKeyboardMapping([&](auto& map) { map = sanitizeMapping(encoder.getKeyboardMappingPreset()); }); + updateMouseMapping([&](auto& map) { map = sanitizeMapping(encoder.getMouseMappingPreset()); }); + updateImguizmoMapping([&](auto& map) { map = sanitizeMapping(encoder.getImguizmoMappingPreset()); }); + } + + inline void copyBindingLayoutTo(IGimbalManipulateEncoder& encoder) const + { + encoder.updateKeyboardMapping([&](auto& map) { map = sanitizeMapping(getKeyboardVirtualEventMap()); }); + encoder.updateMouseMapping([&](auto& map) { map = sanitizeMapping(getMouseVirtualEventMap()); }); + encoder.updateImguizmoMapping([&](auto& map) { map = sanitizeMapping(getImguizmoVirtualEventMap()); }); + } + +private: + template + inline static Map sanitizeMapping(const Map& source) + { + Map result; + for (const auto& [code, hash] : source) + result.emplace(code, typename Map::mapped_type(hash.event.type)); + return result; + } +}; + +} // namespace nbl::hlsl + +#endif // _NBL_C_GIMBAL_INPUT_BINDER_HPP_ diff --git a/common/include/camera/CIsometricCamera.hpp b/common/include/camera/CIsometricCamera.hpp index 3fb2de1f1..af47ae14c 100644 --- a/common/include/camera/CIsometricCamera.hpp +++ b/common/include/camera/CIsometricCamera.hpp @@ -54,6 +54,7 @@ class CIsometricCamera final : public CSphericalTargetCamera } virtual const uint32_t getAllowedVirtualEvents() override { return AllowedVirtualEvents; } + virtual CameraKind getKind() const override { return CameraKind::Isometric; } virtual const std::string_view getIdentifier() override { return "Isometric Camera"; } private: diff --git a/common/include/camera/CLinearProjection.hpp b/common/include/camera/CLinearProjection.hpp index 791e9cb1b..115f0906c 100644 --- a/common/include/camera/CLinearProjection.hpp +++ b/common/include/camera/CLinearProjection.hpp @@ -22,9 +22,15 @@ namespace nbl::hlsl return core::smart_refctd_ptr(new CLinearProjection(core::smart_refctd_ptr(camera)), core::dont_grab); } - virtual std::span getLinearProjections() const override + virtual uint32_t getLinearProjectionCount() const override { - return std::span(m_projections.data(), m_projections.size()); + return static_cast(m_projections.size()); + } + + virtual const CProjection& getLinearProjection(uint32_t index) const override + { + assert(index < m_projections.size()); + return m_projections[index]; } inline std::span getLinearProjections() @@ -42,4 +48,4 @@ namespace nbl::hlsl } // nbl::hlsl namespace -#endif // _NBL_C_LINEAR_PROJECTION_HPP_ \ No newline at end of file +#endif // _NBL_C_LINEAR_PROJECTION_HPP_ diff --git a/common/include/camera/COrbitCamera.hpp b/common/include/camera/COrbitCamera.hpp index 8df9c32d0..84c2a6dbf 100644 --- a/common/include/camera/COrbitCamera.hpp +++ b/common/include/camera/COrbitCamera.hpp @@ -52,6 +52,11 @@ class COrbitCamera final : public CSphericalTargetCamera return AllowedVirtualEvents; } + virtual CameraKind getKind() const override + { + return CameraKind::Orbit; + } + virtual const std::string_view getIdentifier() override { return "Orbit Camera"; diff --git a/common/include/camera/CPathCamera.hpp b/common/include/camera/CPathCamera.hpp index 4277e4d92..614c13bb1 100644 --- a/common/include/camera/CPathCamera.hpp +++ b/common/include/camera/CPathCamera.hpp @@ -51,6 +51,30 @@ class CPathCamera final : public CSphericalTargetCamera } virtual const uint32_t getAllowedVirtualEvents() override { return AllowedVirtualEvents; } + virtual CameraKind getKind() const override { return CameraKind::Path; } + virtual bool trySetSphericalDistance(float distance) override + { + const auto clamped = std::clamp(distance, MinDistance, MaxDistance); + const bool inRange = clamped == distance; + + const double currentDistance = std::sqrt(m_pathRadius * m_pathRadius + m_pathHeight * m_pathHeight); + if (currentDistance > 1e-9) + { + const double scale = static_cast(clamped) / currentDistance; + m_pathRadius = std::max(MinPathRadius, m_pathRadius * scale); + m_pathHeight *= scale; + } + else + { + m_pathRadius = std::max(MinPathRadius, static_cast(clamped)); + m_pathHeight = 0.0; + } + + updateFromPath(); + + const double appliedDistance = std::sqrt(m_pathRadius * m_pathRadius + m_pathHeight * m_pathHeight); + return inRange && std::abs(appliedDistance - static_cast(clamped)) <= 1e-6; + } virtual const std::string_view getIdentifier() override { return "Path Camera"; } private: diff --git a/common/include/camera/CPlanarProjection.hpp b/common/include/camera/CPlanarProjection.hpp index a85cc39de..4d559a410 100644 --- a/common/include/camera/CPlanarProjection.hpp +++ b/common/include/camera/CPlanarProjection.hpp @@ -20,9 +20,15 @@ namespace nbl::hlsl return core::smart_refctd_ptr(new CPlanarProjection(core::smart_refctd_ptr(camera)), core::dont_grab); } - virtual std::span getLinearProjections() const override + virtual uint32_t getLinearProjectionCount() const override { - return { reinterpret_cast(m_projections.data()), m_projections.size() }; + return static_cast(m_projections.size()); + } + + virtual const ILinearProjection::CProjection& getLinearProjection(uint32_t index) const override + { + assert(index < m_projections.size()); + return m_projections[index]; } inline ProjectionsRange& getPlanarProjections() @@ -39,4 +45,4 @@ namespace nbl::hlsl } // nbl::hlsl namespace -#endif // _NBL_C_PLANAR_PROJECTION_HPP_ \ No newline at end of file +#endif // _NBL_C_PLANAR_PROJECTION_HPP_ diff --git a/common/include/camera/CSphericalTargetCamera.hpp b/common/include/camera/CSphericalTargetCamera.hpp index 0ddcf747e..fae9b4d38 100644 --- a/common/include/camera/CSphericalTargetCamera.hpp +++ b/common/include/camera/CSphericalTargetCamera.hpp @@ -26,11 +26,20 @@ class CSphericalTargetCamera : public ICamera { const auto clamped = std::clamp(d, MinDistance, MaxDistance); const bool ok = clamped == d; + if (m_distance == clamped) + return ok; m_distance = clamped; + applyPose(); return ok; } - inline void target(const float64_t3& p) { m_targetPosition = p; } + inline void target(const float64_t3& p) + { + if (m_targetPosition == p) + return; + m_targetPosition = p; + applyPose(); + } inline float64_t3 getTarget() const { return m_targetPosition; } inline float getDistance() const { return m_distance; } @@ -40,6 +49,33 @@ class CSphericalTargetCamera : public ICamera static inline constexpr float MinDistance = 0.1f; static inline constexpr float MaxDistance = 10000.f; + virtual uint32_t getCapabilities() const override + { + return base_t::SphericalTarget; + } + + virtual bool tryGetSphericalTargetState(typename base_t::SphericalTargetState& out) const override + { + out.target = m_targetPosition; + out.distance = m_distance; + out.u = m_u; + out.v = m_v; + out.minDistance = MinDistance; + out.maxDistance = MaxDistance; + return true; + } + + virtual bool trySetSphericalTarget(const float64_t3& targetPosition) override + { + target(targetPosition); + return true; + } + + virtual bool trySetSphericalDistance(float distance) override + { + return setDistance(distance); + } + protected: struct SphericalBasis { diff --git a/common/include/camera/CTargetPoseController.hpp b/common/include/camera/CTargetPoseController.hpp index fdd566ed8..b56e15998 100644 --- a/common/include/camera/CTargetPoseController.hpp +++ b/common/include/camera/CTargetPoseController.hpp @@ -19,6 +19,8 @@ struct CTargetPose { float64_t3 position = float64_t3(0.0); glm::quat orientation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f); + bool hasTargetPosition = false; + float64_t3 targetPosition = float64_t3(0.0); bool hasDistance = false; float distance = 0.f; bool hasOrbitState = false; @@ -30,30 +32,186 @@ struct CTargetPose class CTargetPoseController { public: + struct SApplyResult + { + enum class EStatus : uint8_t + { + Unsupported, + Failed, + AlreadySatisfied, + AppliedAbsoluteOnly, + AppliedVirtualEvents, + AppliedAbsoluteAndVirtualEvents + }; + + EStatus status = EStatus::Unsupported; + bool exact = false; + uint32_t eventCount = 0u; + + inline bool succeeded() const + { + return status != EStatus::Unsupported && status != EStatus::Failed; + } + + inline bool changed() const + { + return status == EStatus::AppliedAbsoluteOnly || + status == EStatus::AppliedVirtualEvents || + status == EStatus::AppliedAbsoluteAndVirtualEvents; + } + }; + bool buildEvents(ICamera* camera, const CTargetPose& target, std::vector& out) const { out.clear(); if (!camera) return false; - if (auto* orbit = dynamic_cast(camera)) - return buildOrbitEvents(orbit, target, out); + if (camera->hasCapability(ICamera::SphericalTarget)) + return buildSphericalEvents(camera, target, out); return buildFreeEvents(camera, target, out); } - bool apply(ICamera* camera, const CTargetPose& target) const + SApplyResult applyDetailed(ICamera* camera, const CTargetPose& target) const { + SApplyResult result; + if (!camera) + return result; + + bool exact = true; + bool absoluteChanged = false; + + if (!camera->hasCapability(ICamera::SphericalTarget)) + { + bool poseChanged = false; + bool poseExact = false; + if (tryApplyAbsoluteReferencePose(camera, target, poseChanged, poseExact)) + { + absoluteChanged = absoluteChanged || poseChanged; + if (poseExact) + { + result.status = poseChanged ? + SApplyResult::EStatus::AppliedAbsoluteOnly : + SApplyResult::EStatus::AlreadySatisfied; + result.exact = true; + return result; + } + } + } + + if (target.hasTargetPosition) + { + ICamera::SphericalTargetState beforeState; + if (!camera->tryGetSphericalTargetState(beforeState)) + { + exact = false; + } + else + { + const auto beforeTarget = beforeState.target; + if (!camera->trySetSphericalTarget(target.targetPosition)) + { + exact = false; + } + else + { + ICamera::SphericalTargetState afterState; + if (!camera->tryGetSphericalTargetState(afterState)) + { + exact = false; + } + else + { + absoluteChanged = afterState.target != beforeTarget; + exact = exact && afterState.target == target.targetPosition; + } + } + } + } + + if (target.hasDistance || target.hasOrbitState) + { + ICamera::SphericalTargetState beforeState; + if (!camera->tryGetSphericalTargetState(beforeState)) + { + exact = false; + } + else + { + const float desiredDistance = target.hasOrbitState ? target.orbitDistance : target.distance; + const float beforeDistance = beforeState.distance; + if (!camera->trySetSphericalDistance(desiredDistance)) + { + exact = false; + } + else + { + ICamera::SphericalTargetState afterState; + if (!camera->tryGetSphericalTargetState(afterState)) + { + exact = false; + } + else + { + absoluteChanged = absoluteChanged || afterState.distance != beforeDistance; + exact = exact && std::abs(static_cast(afterState.distance - desiredDistance)) <= 1e-6; + } + } + } + } + std::vector events; - if (!buildEvents(camera, target, events)) - return false; - return camera->manipulate({ events.data(), events.size() }); + buildEvents(camera, target, events); + result.eventCount = static_cast(events.size()); + result.exact = exact; + + if (events.empty()) + { + if (absoluteChanged) + result.status = SApplyResult::EStatus::AppliedAbsoluteOnly; + else if (exact) + result.status = SApplyResult::EStatus::AlreadySatisfied; + return result; + } + + if (camera->manipulate({ events.data(), events.size() })) + { + result.status = absoluteChanged ? + SApplyResult::EStatus::AppliedAbsoluteAndVirtualEvents : + SApplyResult::EStatus::AppliedVirtualEvents; + return result; + } + + if (absoluteChanged) + { + result.status = SApplyResult::EStatus::AppliedAbsoluteOnly; + result.exact = false; + return result; + } + + result.status = SApplyResult::EStatus::Failed; + result.exact = false; + return result; + } + + bool apply(ICamera* camera, const CTargetPose& target) const + { + return applyDetailed(camera, target).succeeded(); } private: static constexpr double Pi = 3.14159265358979323846; static constexpr double HalfPi = 0.5 * Pi; + struct SSphericalGoal + { + float64_t3 target = float64_t3(0.0); + double u = 0.0; + double v = 0.0; + float distance = 0.f; + }; + inline double wrapAngleRad(double angle) const { while (angle > Pi) @@ -73,6 +231,18 @@ class CTargetPoseController ev.magnitude = std::abs(value); } + inline double getMoveMagnitudeDenominator(const ICamera* camera) const + { + const double moveScale = camera->getMoveSpeedScale(); + return 0.01 * (moveScale == 0.0 ? 1.0 : moveScale); + } + + inline double getRotationMagnitudeDenominator(const ICamera* camera) const + { + const double rotationScale = camera->getRotationSpeedScale(); + return rotationScale == 0.0 ? 1.0 : rotationScale; + } + inline std::pair computePitchYawFromOrientation(const glm::quat& orientation) const { const auto mat = glm::mat3_cast(orientation); @@ -84,25 +254,80 @@ class CTargetPoseController inline float64_t3 extractYawPitchRollYXZ(const glm::quat& delta) const { - const auto m = glm::mat3_cast(delta); - const double sp = std::clamp(-static_cast(m[1][2]), -1.0, 1.0); - const double pitch = std::asin(sp); - const double cp = std::cos(pitch); + const auto m = getMatrix3x3As4x4(matrix(glm::mat3_cast(delta))); + const double yaw = std::atan2(static_cast(m[2][0]), static_cast(m[2][2])); + const double c2 = std::sqrt(static_cast(m[0][1] * m[0][1] + m[1][1] * m[1][1])); + const double pitch = std::atan2(-static_cast(m[2][1]), c2); + const double s1 = std::sin(yaw); + const double c1 = std::cos(yaw); + const double roll = std::atan2( + s1 * static_cast(m[1][2]) - c1 * static_cast(m[1][0]), + c1 * static_cast(m[0][0]) - s1 * static_cast(m[0][2])); + return float64_t3(pitch, yaw, roll); + } - double yaw = 0.0; - double roll = 0.0; - if (std::abs(cp) > 1e-6) + inline bool computePoseMismatch(ICamera* camera, const CTargetPose& target, double& outPositionDelta, double& outRotationDeltaDeg) const + { + outPositionDelta = 0.0; + outRotationDeltaDeg = 0.0; + if (!camera) + return false; + + const auto& gimbal = camera->getGimbal(); + const auto currentPos = gimbal.getPosition(); + const auto currentOrientation = glm::normalize(gimbal.getOrientation()); + const auto targetOrientation = glm::normalize(target.orientation); + + const double dx = static_cast(currentPos.x - target.position.x); + const double dy = static_cast(currentPos.y - target.position.y); + const double dz = static_cast(currentPos.z - target.position.z); + outPositionDelta = std::sqrt(dx * dx + dy * dy + dz * dz); + + const double orientationDot = std::clamp(static_cast(std::abs(glm::dot(currentOrientation, targetOrientation))), 0.0, 1.0); + outRotationDeltaDeg = glm::degrees(2.0 * std::acos(orientationDot)); + return std::isfinite(outPositionDelta) && std::isfinite(outRotationDeltaDeg); + } + + inline bool tryApplyAbsoluteReferencePose(ICamera* camera, const CTargetPose& target, bool& outChanged, bool& outExact) const + { + outChanged = false; + outExact = false; + if (!camera) + return false; + + switch (camera->getKind()) { - yaw = std::atan2(static_cast(m[0][2]), static_cast(m[2][2])); - roll = std::atan2(static_cast(m[1][0]), static_cast(m[1][1])); + case ICamera::CameraKind::Free: + case ICamera::CameraKind::FPS: + break; + default: + return false; } - else + + double beforePosDelta = 0.0; + double beforeRotDeltaDeg = 0.0; + if (!computePoseMismatch(camera, target, beforePosDelta, beforeRotDeltaDeg)) + return false; + + if (beforePosDelta <= 1e-6 && beforeRotDeltaDeg <= 0.1) { - yaw = std::atan2(-static_cast(m[2][0]), static_cast(m[0][0])); - roll = 0.0; + outExact = true; + return true; } - return float64_t3(pitch, yaw, roll); + auto targetFrame = getMatrix3x3As4x4(matrix(glm::mat3_cast(glm::normalize(target.orientation)))); + targetFrame[3] = float64_t4(target.position, 1.0); + + camera->manipulate({}, &targetFrame); + + double afterPosDelta = 0.0; + double afterRotDeltaDeg = 0.0; + if (!computePoseMismatch(camera, target, afterPosDelta, afterRotDeltaDeg)) + return false; + + outChanged = (std::abs(afterPosDelta - beforePosDelta) > 1e-9) || (std::abs(afterRotDeltaDeg - beforeRotDeltaDeg) > 1e-9); + outExact = afterPosDelta <= 1e-6 && afterRotDeltaDeg <= 0.1; + return true; } inline bool computeOrbitStateFromPositionTarget(const float64_t3& position, const float64_t3& target, @@ -126,42 +351,118 @@ class CTargetPoseController return true; } - template - inline bool buildOrbitEvents(T* orbit, const CTargetPose& target, std::vector& out) const + inline bool resolveSphericalGoal(ICamera* camera, const CTargetPose& target, const ICamera::SphericalTargetState& sphericalState, SSphericalGoal& outGoal) const { - double targetU = orbit->getU(); - double targetV = orbit->getV(); - float targetDistance = orbit->getDistance(); + outGoal.target = target.hasTargetPosition ? target.targetPosition : sphericalState.target; + outGoal.u = sphericalState.u; + outGoal.v = sphericalState.v; + outGoal.distance = sphericalState.distance; if (target.hasOrbitState) { - targetU = target.orbitU; - targetV = target.orbitV; - targetDistance = target.orbitDistance; + outGoal.u = target.orbitU; + outGoal.v = target.orbitV; + outGoal.distance = target.orbitDistance; } else { - const auto orbitTarget = orbit->getTarget(); - if (!computeOrbitStateFromPositionTarget(target.position, orbitTarget, targetU, targetV, targetDistance, T::MinDistance, T::MaxDistance)) + if (!computeOrbitStateFromPositionTarget(target.position, outGoal.target, outGoal.u, outGoal.v, outGoal.distance, sphericalState.minDistance, sphericalState.maxDistance)) return false; } - targetDistance = std::clamp(targetDistance, T::MinDistance, T::MaxDistance); + if (target.hasDistance && !target.hasOrbitState) + outGoal.distance = target.distance; - const double deltaU = targetU - orbit->getU(); - const double deltaV = targetV - orbit->getV(); - const double deltaDistance = static_cast(targetDistance - orbit->getDistance()); + outGoal.distance = std::clamp(outGoal.distance, sphericalState.minDistance, sphericalState.maxDistance); + return true; + } - const double moveScale = orbit->getMoveSpeedScale(); - const double moveDenom = 0.01 * (moveScale == 0.0 ? 1.0 : moveScale); + inline bool buildOrbitTranslateEvents(ICamera* camera, const ICamera::SphericalTargetState& sphericalState, const SSphericalGoal& goal, std::vector& out) const + { + const double moveDenom = getMoveMagnitudeDenominator(camera); + appendSignedEvent(out, wrapAngleRad(goal.v - sphericalState.v) / moveDenom, CVirtualGimbalEvent::MoveRight, CVirtualGimbalEvent::MoveLeft); + appendSignedEvent(out, wrapAngleRad(goal.u - sphericalState.u) / moveDenom, CVirtualGimbalEvent::MoveUp, CVirtualGimbalEvent::MoveDown); + appendSignedEvent(out, static_cast(goal.distance - sphericalState.distance) / 0.01, CVirtualGimbalEvent::MoveForward, CVirtualGimbalEvent::MoveBackward); + return !out.empty(); + } - appendSignedEvent(out, deltaV / moveDenom, CVirtualGimbalEvent::MoveRight, CVirtualGimbalEvent::MoveLeft); - appendSignedEvent(out, deltaU / moveDenom, CVirtualGimbalEvent::MoveUp, CVirtualGimbalEvent::MoveDown); - appendSignedEvent(out, deltaDistance / 0.01, CVirtualGimbalEvent::MoveForward, CVirtualGimbalEvent::MoveBackward); + inline bool buildRotateDistanceEvents(ICamera* camera, const ICamera::SphericalTargetState& sphericalState, const SSphericalGoal& goal, + std::vector& out, bool allowYaw, bool allowPitch, + CVirtualGimbalEvent::VirtualEventType distancePositive, CVirtualGimbalEvent::VirtualEventType distanceNegative) const + { + const double rotationDenom = getRotationMagnitudeDenominator(camera); + if (allowYaw) + appendSignedEvent(out, wrapAngleRad(goal.u - sphericalState.u) / rotationDenom, CVirtualGimbalEvent::PanRight, CVirtualGimbalEvent::PanLeft); + if (allowPitch) + appendSignedEvent(out, wrapAngleRad(goal.v - sphericalState.v) / rotationDenom, CVirtualGimbalEvent::TiltUp, CVirtualGimbalEvent::TiltDown); + if (distancePositive != CVirtualGimbalEvent::None && distanceNegative != CVirtualGimbalEvent::None) + appendSignedEvent(out, static_cast(goal.distance - sphericalState.distance) / 0.01, distancePositive, distanceNegative); + return !out.empty(); + } + inline bool buildPathEvents(ICamera* camera, const CTargetPose& target, const ICamera::SphericalTargetState& sphericalState, std::vector& out) const + { + if (!camera) + return false; + + const auto effectiveTarget = target.hasTargetPosition ? target.targetPosition : sphericalState.target; + const auto currentOffset = camera->getGimbal().getPosition() - effectiveTarget; + const auto desiredOffset = target.position - effectiveTarget; + + const double currentAngle = std::atan2(currentOffset.z, currentOffset.x); + const double desiredAngle = std::atan2(desiredOffset.z, desiredOffset.x); + const double currentRadius = std::sqrt(currentOffset.x * currentOffset.x + currentOffset.z * currentOffset.z); + const double desiredRadius = std::sqrt(desiredOffset.x * desiredOffset.x + desiredOffset.z * desiredOffset.z); + const double currentHeight = currentOffset.y; + const double desiredHeight = desiredOffset.y; + + const double moveDenom = getMoveMagnitudeDenominator(camera); + appendSignedEvent(out, (desiredRadius - currentRadius) / moveDenom, CVirtualGimbalEvent::MoveRight, CVirtualGimbalEvent::MoveLeft); + appendSignedEvent(out, (desiredHeight - currentHeight) / moveDenom, CVirtualGimbalEvent::MoveUp, CVirtualGimbalEvent::MoveDown); + appendSignedEvent(out, wrapAngleRad(desiredAngle - currentAngle) / moveDenom, CVirtualGimbalEvent::MoveForward, CVirtualGimbalEvent::MoveBackward); return !out.empty(); } + inline bool buildSphericalEvents(ICamera* camera, const CTargetPose& target, std::vector& out) const + { + ICamera::SphericalTargetState sphericalState; + if (!camera || !camera->tryGetSphericalTargetState(sphericalState)) + return false; + + if (camera->getKind() == ICamera::CameraKind::Path) + return buildPathEvents(camera, target, sphericalState, out); + + SSphericalGoal goal; + if (!resolveSphericalGoal(camera, target, sphericalState, goal)) + return false; + + switch (camera->getKind()) + { + case ICamera::CameraKind::Orbit: + case ICamera::CameraKind::DollyZoom: + return buildOrbitTranslateEvents(camera, sphericalState, goal, out); + + case ICamera::CameraKind::Turntable: + case ICamera::CameraKind::Arcball: + return buildRotateDistanceEvents(camera, sphericalState, goal, out, true, true, CVirtualGimbalEvent::MoveForward, CVirtualGimbalEvent::MoveBackward); + + case ICamera::CameraKind::TopDown: + return buildRotateDistanceEvents(camera, sphericalState, goal, out, true, false, CVirtualGimbalEvent::MoveForward, CVirtualGimbalEvent::MoveBackward); + + case ICamera::CameraKind::Isometric: + return buildRotateDistanceEvents(camera, sphericalState, goal, out, false, false, CVirtualGimbalEvent::MoveForward, CVirtualGimbalEvent::MoveBackward); + + case ICamera::CameraKind::Dolly: + return buildRotateDistanceEvents(camera, sphericalState, goal, out, true, true, CVirtualGimbalEvent::None, CVirtualGimbalEvent::None); + + case ICamera::CameraKind::Chase: + return buildRotateDistanceEvents(camera, sphericalState, goal, out, true, true, CVirtualGimbalEvent::MoveUp, CVirtualGimbalEvent::MoveDown); + + default: + return buildOrbitTranslateEvents(camera, sphericalState, goal, out); + } + } + inline bool buildFreeEvents(ICamera* camera, const CTargetPose& target, std::vector& out) const { const auto& gimbal = camera->getGimbal(); @@ -180,28 +481,35 @@ class CTargetPoseController appendSignedEvent(out, localDelta.y, CVirtualGimbalEvent::MoveUp, CVirtualGimbalEvent::MoveDown); appendSignedEvent(out, localDelta.z, CVirtualGimbalEvent::MoveForward, CVirtualGimbalEvent::MoveBackward); - if (auto* fps = dynamic_cast(camera)) + switch (camera->getKind()) { - const auto [curPitch, curYaw] = computePitchYawFromOrientation(gimbal.getOrientation()); - const auto [tgtPitch, tgtYaw] = computePitchYawFromOrientation(target.orientation); + case ICamera::CameraKind::FPS: + { + const auto [curPitch, curYaw] = computePitchYawFromOrientation(gimbal.getOrientation()); + const auto [tgtPitch, tgtYaw] = computePitchYawFromOrientation(target.orientation); - const double rotScale = fps->getRotationSpeedScale(); - const double invScale = rotScale == 0.0 ? 1.0 : (1.0 / rotScale); + const double rotScale = camera->getRotationSpeedScale(); + const double invScale = rotScale == 0.0 ? 1.0 : (1.0 / rotScale); - const double deltaPitch = wrapAngleRad(tgtPitch - curPitch) * invScale; - const double deltaYaw = wrapAngleRad(tgtYaw - curYaw) * invScale; + const double deltaPitch = wrapAngleRad(tgtPitch - curPitch) * invScale; + const double deltaYaw = wrapAngleRad(tgtYaw - curYaw) * invScale; - appendSignedEvent(out, deltaPitch, CVirtualGimbalEvent::TiltUp, CVirtualGimbalEvent::TiltDown); - appendSignedEvent(out, deltaYaw, CVirtualGimbalEvent::PanRight, CVirtualGimbalEvent::PanLeft); - } - else if (auto* freeCam = dynamic_cast(camera)) - { - const auto deltaQuat = glm::normalize(target.orientation) * glm::inverse(gimbal.getOrientation()); - const auto angles = extractYawPitchRollYXZ(deltaQuat); + appendSignedEvent(out, deltaPitch, CVirtualGimbalEvent::TiltUp, CVirtualGimbalEvent::TiltDown); + appendSignedEvent(out, deltaYaw, CVirtualGimbalEvent::PanRight, CVirtualGimbalEvent::PanLeft); + } break; + + case ICamera::CameraKind::Free: + { + const auto deltaQuat = glm::inverse(gimbal.getOrientation()) * glm::normalize(target.orientation); + const auto angles = extractYawPitchRollYXZ(deltaQuat); + + appendSignedEvent(out, angles.x, CVirtualGimbalEvent::TiltUp, CVirtualGimbalEvent::TiltDown); + appendSignedEvent(out, angles.y, CVirtualGimbalEvent::PanRight, CVirtualGimbalEvent::PanLeft); + appendSignedEvent(out, angles.z, CVirtualGimbalEvent::RollRight, CVirtualGimbalEvent::RollLeft); + } break; - appendSignedEvent(out, angles.x, CVirtualGimbalEvent::TiltUp, CVirtualGimbalEvent::TiltDown); - appendSignedEvent(out, angles.y, CVirtualGimbalEvent::PanRight, CVirtualGimbalEvent::PanLeft); - appendSignedEvent(out, angles.z, CVirtualGimbalEvent::RollRight, CVirtualGimbalEvent::RollLeft); + default: + break; } return !out.empty(); diff --git a/common/include/camera/CTopDownCamera.hpp b/common/include/camera/CTopDownCamera.hpp index baa178b3c..31a0b7bd7 100644 --- a/common/include/camera/CTopDownCamera.hpp +++ b/common/include/camera/CTopDownCamera.hpp @@ -54,6 +54,7 @@ class CTopDownCamera final : public CSphericalTargetCamera } virtual const uint32_t getAllowedVirtualEvents() override { return AllowedVirtualEvents; } + virtual CameraKind getKind() const override { return CameraKind::TopDown; } virtual const std::string_view getIdentifier() override { return "Top-Down Camera"; } private: diff --git a/common/include/camera/CTurntableCamera.hpp b/common/include/camera/CTurntableCamera.hpp index 97eb6bc2f..e0116a2a2 100644 --- a/common/include/camera/CTurntableCamera.hpp +++ b/common/include/camera/CTurntableCamera.hpp @@ -53,6 +53,7 @@ class CTurntableCamera final : public CSphericalTargetCamera } virtual const uint32_t getAllowedVirtualEvents() override { return AllowedVirtualEvents; } + virtual CameraKind getKind() const override { return CameraKind::Turntable; } virtual const std::string_view getIdentifier() override { return "Turntable Camera"; } static inline constexpr float MinDistance = base_t::MinDistance; diff --git a/common/include/camera/ICamera.hpp b/common/include/camera/ICamera.hpp index 046661b4d..692cc30de 100644 --- a/common/include/camera/ICamera.hpp +++ b/common/include/camera/ICamera.hpp @@ -5,6 +5,8 @@ #ifndef _I_CAMERA_HPP_ #define _I_CAMERA_HPP_ +#include + #include "camera/IGimbalController.hpp" namespace nbl::hlsl // TODO: DIFFERENT NAMESPACE @@ -15,6 +17,45 @@ class ICamera : public IGimbalController, virtual public core::IReferenceCounted public: using IGimbalController::IGimbalController; + enum class CameraKind : uint8_t + { + Unknown, + FPS, + Free, + Orbit, + Arcball, + Turntable, + TopDown, + Isometric, + Chase, + Dolly, + DollyZoom, + Path + }; + + enum CameraCapability : uint32_t + { + None = 0u, + SphericalTarget = core::createBitmask({ 0 }), + DynamicPerspectiveFov = core::createBitmask({ 1 }) + }; + + struct SphericalTargetState + { + float64_t3 target = float64_t3(0.0); + float distance = 0.f; + double u = 0.0; + double v = 0.0; + float minDistance = 0.f; + float maxDistance = 0.f; + }; + + struct DynamicPerspectiveState + { + float baseFov = 0.f; + float referenceDistance = 0.f; + }; + // Gimbal with view parameters representing a camera in world space class CGimbal : public IGimbal { @@ -53,12 +94,50 @@ class ICamera : public IGimbalController, virtual public core::IReferenceCounted // TODO: this really needs to be moved to more abstract interface, eg. IObjectTransform or something and ICamera should inherit it (its also an object!) virtual bool manipulate(std::span virtualEvents, const float64_t4x4* referenceFrame = nullptr) = 0; - // VirtualEventType bitmask for a camera view gimbal manipulation requests filtering + // VirtualEventType bitmask for a camera view gimbal manipulation requests filtering virtual const uint32_t getAllowedVirtualEvents() = 0u; + virtual CameraKind getKind() const = 0; + virtual uint32_t getCapabilities() const { return None; } + // Identifier of a camera type virtual const std::string_view getIdentifier() = 0u; + inline bool hasCapability(CameraCapability capability) const + { + return (getCapabilities() & capability) == capability; + } + + virtual bool tryGetSphericalTargetState(SphericalTargetState& out) const + { + return false; + } + + virtual bool trySetSphericalTarget(const float64_t3& target) + { + return false; + } + + virtual bool trySetSphericalDistance(float distance) + { + return false; + } + + virtual bool tryGetDynamicPerspectiveFov(float& outFov) const + { + return false; + } + + virtual bool tryGetDynamicPerspectiveState(DynamicPerspectiveState& out) const + { + return false; + } + + virtual bool trySetDynamicPerspectiveState(const DynamicPerspectiveState& state) + { + return false; + } + // (***) inline void setMoveSpeedScale(double scalar) { diff --git a/common/include/camera/ILinearProjection.hpp b/common/include/camera/ILinearProjection.hpp index fc0115610..41f9ca34f 100644 --- a/common/include/camera/ILinearProjection.hpp +++ b/common/include/camera/ILinearProjection.hpp @@ -92,7 +92,8 @@ class ILinearProjection : virtual public core::IReferenceCounted bool m_isProjectionSingular; }; - virtual std::span getLinearProjections() const = 0; + virtual uint32_t getLinearProjectionCount() const = 0; + virtual const CProjection& getLinearProjection(uint32_t index) const = 0; inline bool setCamera(core::smart_refctd_ptr&& camera) { @@ -177,4 +178,4 @@ class ILinearProjection : virtual public core::IReferenceCounted } // nbl::hlsl namespace -#endif // _NBL_I_LINEAR_PROJECTION_HPP_ \ No newline at end of file +#endif // _NBL_I_LINEAR_PROJECTION_HPP_ From 5d4f144aa7a1bedb35aec0aeab82770bc362c04c Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Fri, 3 Apr 2026 19:52:30 +0200 Subject: [PATCH 110/205] Rename target pose controller to camera goal solver --- 61_UI/include/app/App.hpp | 14 +- 61_UI/include/common.hpp | 3 +- common/include/camera/CCameraGoalSolver.hpp | 523 ++++++++++++++++++ .../include/camera/CTargetPoseController.hpp | 517 +---------------- 4 files changed, 533 insertions(+), 524 deletions(-) create mode 100644 common/include/camera/CCameraGoalSolver.hpp diff --git a/61_UI/include/app/App.hpp b/61_UI/include/app/App.hpp index 066e044cd..bff113039 100644 --- a/61_UI/include/app/App.hpp +++ b/61_UI/include/app/App.hpp @@ -1078,7 +1078,7 @@ class App final : public examples::SimpleWindowedApplication return preset; } - inline CTargetPoseController::SApplyResult applyPresetToCameraDetailed(ICamera* camera, const CameraPreset& preset) + inline CCameraGoalSolver::SApplyResult applyPresetToCameraDetailed(ICamera* camera, const CameraPreset& preset) { CTargetPose target; if (!camera) @@ -1095,7 +1095,7 @@ class App final : public examples::SimpleWindowedApplication target.orbitV = preset.orbitV; target.orbitDistance = preset.orbitDistance; - auto result = m_targetPoseController.applyDetailed(camera, target); + auto result = m_cameraGoalSolver.applyDetailed(camera, target); if (!preset.hasDynamicPerspectiveState) return result; @@ -1136,13 +1136,13 @@ class App final : public examples::SimpleWindowedApplication if (dynamicChanged) { if (!result.succeeded() || !result.changed()) - result.status = CTargetPoseController::SApplyResult::EStatus::AppliedAbsoluteOnly; - else if (result.status == CTargetPoseController::SApplyResult::EStatus::AppliedVirtualEvents) - result.status = CTargetPoseController::SApplyResult::EStatus::AppliedAbsoluteAndVirtualEvents; + result.status = CCameraGoalSolver::SApplyResult::EStatus::AppliedAbsoluteOnly; + else if (result.status == CCameraGoalSolver::SApplyResult::EStatus::AppliedVirtualEvents) + result.status = CCameraGoalSolver::SApplyResult::EStatus::AppliedAbsoluteAndVirtualEvents; } else if (!result.succeeded() && dynamicExact) { - result.status = CTargetPoseController::SApplyResult::EStatus::AlreadySatisfied; + result.status = CCameraGoalSolver::SApplyResult::EStatus::AlreadySatisfied; } result.exact = result.exact && dynamicExact; @@ -2077,7 +2077,7 @@ class App final : public examples::SimpleWindowedApplication std::vector m_initialPlanarPresets; std::vector m_keyframes; CameraPlaybackState m_playback; - CTargetPoseController m_targetPoseController; + CCameraGoalSolver m_cameraGoalSolver; bool m_playbackAffectsAll = false; float m_newKeyframeTime = 0.f; char m_presetName[64] = "Preset"; diff --git a/61_UI/include/common.hpp b/61_UI/include/common.hpp index cc9b909bc..d2349413a 100644 --- a/61_UI/include/common.hpp +++ b/61_UI/include/common.hpp @@ -18,7 +18,7 @@ #include "camera/CDollyCamera.hpp" #include "camera/CDollyZoomCamera.hpp" #include "camera/CPathCamera.hpp" -#include "camera/CTargetPoseController.hpp" +#include "camera/CCameraGoalSolver.hpp" #include "camera/CGimbalInputBinder.hpp" #include "camera/CCubeProjection.hpp" @@ -56,6 +56,7 @@ using nbl::hlsl::CDollyCamera; using nbl::hlsl::CDollyZoomCamera; using nbl::hlsl::CPathCamera; using nbl::hlsl::CTargetPose; +using nbl::hlsl::CCameraGoalSolver; using nbl::hlsl::CTargetPoseController; using nbl::hlsl::CGimbalInputBinder; using nbl::hlsl::IPlanarProjection; diff --git a/common/include/camera/CCameraGoalSolver.hpp b/common/include/camera/CCameraGoalSolver.hpp new file mode 100644 index 000000000..9d9c7d33b --- /dev/null +++ b/common/include/camera/CCameraGoalSolver.hpp @@ -0,0 +1,523 @@ +#ifndef _C_CAMERA_GOAL_SOLVER_HPP_ +#define _C_CAMERA_GOAL_SOLVER_HPP_ + +#include +#include +#include +#include + +#include "ICamera.hpp" +#include "CFPSCamera.hpp" +#include "CFreeLockCamera.hpp" +#include "CSphericalTargetCamera.hpp" +#include "glm/glm/gtc/quaternion.hpp" + +namespace nbl::hlsl +{ + +struct CTargetPose +{ + float64_t3 position = float64_t3(0.0); + glm::quat orientation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f); + bool hasTargetPosition = false; + float64_t3 targetPosition = float64_t3(0.0); + bool hasDistance = false; + float distance = 0.f; + bool hasOrbitState = false; + double orbitU = 0.0; + double orbitV = 0.0; + float orbitDistance = 0.f; +}; + +class CCameraGoalSolver +{ +public: + struct SApplyResult + { + enum class EStatus : uint8_t + { + Unsupported, + Failed, + AlreadySatisfied, + AppliedAbsoluteOnly, + AppliedVirtualEvents, + AppliedAbsoluteAndVirtualEvents + }; + + EStatus status = EStatus::Unsupported; + bool exact = false; + uint32_t eventCount = 0u; + + inline bool succeeded() const + { + return status != EStatus::Unsupported && status != EStatus::Failed; + } + + inline bool changed() const + { + return status == EStatus::AppliedAbsoluteOnly || + status == EStatus::AppliedVirtualEvents || + status == EStatus::AppliedAbsoluteAndVirtualEvents; + } + }; + + bool buildEvents(ICamera* camera, const CTargetPose& target, std::vector& out) const + { + out.clear(); + if (!camera) + return false; + + if (camera->hasCapability(ICamera::SphericalTarget)) + return buildSphericalEvents(camera, target, out); + + return buildFreeEvents(camera, target, out); + } + + SApplyResult applyDetailed(ICamera* camera, const CTargetPose& target) const + { + SApplyResult result; + if (!camera) + return result; + + bool exact = true; + bool absoluteChanged = false; + + if (!camera->hasCapability(ICamera::SphericalTarget)) + { + bool poseChanged = false; + bool poseExact = false; + if (tryApplyAbsoluteReferencePose(camera, target, poseChanged, poseExact)) + { + absoluteChanged = absoluteChanged || poseChanged; + if (poseExact) + { + result.status = poseChanged ? + SApplyResult::EStatus::AppliedAbsoluteOnly : + SApplyResult::EStatus::AlreadySatisfied; + result.exact = true; + return result; + } + } + } + + if (target.hasTargetPosition) + { + ICamera::SphericalTargetState beforeState; + if (!camera->tryGetSphericalTargetState(beforeState)) + { + exact = false; + } + else + { + const auto beforeTarget = beforeState.target; + if (!camera->trySetSphericalTarget(target.targetPosition)) + { + exact = false; + } + else + { + ICamera::SphericalTargetState afterState; + if (!camera->tryGetSphericalTargetState(afterState)) + { + exact = false; + } + else + { + absoluteChanged = afterState.target != beforeTarget; + exact = exact && afterState.target == target.targetPosition; + } + } + } + } + + if (target.hasDistance || target.hasOrbitState) + { + ICamera::SphericalTargetState beforeState; + if (!camera->tryGetSphericalTargetState(beforeState)) + { + exact = false; + } + else + { + const float desiredDistance = target.hasOrbitState ? target.orbitDistance : target.distance; + const float beforeDistance = beforeState.distance; + if (!camera->trySetSphericalDistance(desiredDistance)) + { + exact = false; + } + else + { + ICamera::SphericalTargetState afterState; + if (!camera->tryGetSphericalTargetState(afterState)) + { + exact = false; + } + else + { + absoluteChanged = absoluteChanged || afterState.distance != beforeDistance; + exact = exact && std::abs(static_cast(afterState.distance - desiredDistance)) <= 1e-6; + } + } + } + } + + std::vector events; + buildEvents(camera, target, events); + result.eventCount = static_cast(events.size()); + result.exact = exact; + + if (events.empty()) + { + if (absoluteChanged) + result.status = SApplyResult::EStatus::AppliedAbsoluteOnly; + else if (exact) + result.status = SApplyResult::EStatus::AlreadySatisfied; + return result; + } + + if (camera->manipulate({ events.data(), events.size() })) + { + result.status = absoluteChanged ? + SApplyResult::EStatus::AppliedAbsoluteAndVirtualEvents : + SApplyResult::EStatus::AppliedVirtualEvents; + return result; + } + + if (absoluteChanged) + { + result.status = SApplyResult::EStatus::AppliedAbsoluteOnly; + result.exact = false; + return result; + } + + result.status = SApplyResult::EStatus::Failed; + result.exact = false; + return result; + } + + bool apply(ICamera* camera, const CTargetPose& target) const + { + return applyDetailed(camera, target).succeeded(); + } + +private: + static constexpr double Pi = 3.14159265358979323846; + static constexpr double HalfPi = 0.5 * Pi; + + struct SSphericalGoal + { + float64_t3 target = float64_t3(0.0); + double u = 0.0; + double v = 0.0; + float distance = 0.f; + }; + + inline double wrapAngleRad(double angle) const + { + while (angle > Pi) + angle -= 2.0 * Pi; + while (angle < -Pi) + angle += 2.0 * Pi; + return angle; + } + + inline void appendSignedEvent(std::vector& events, double value, + CVirtualGimbalEvent::VirtualEventType positive, CVirtualGimbalEvent::VirtualEventType negative) const + { + if (value == 0.0) + return; + auto& ev = events.emplace_back(); + ev.type = (value > 0.0) ? positive : negative; + ev.magnitude = std::abs(value); + } + + inline double getMoveMagnitudeDenominator(const ICamera* camera) const + { + const double moveScale = camera->getMoveSpeedScale(); + return 0.01 * (moveScale == 0.0 ? 1.0 : moveScale); + } + + inline double getRotationMagnitudeDenominator(const ICamera* camera) const + { + const double rotationScale = camera->getRotationSpeedScale(); + return rotationScale == 0.0 ? 1.0 : rotationScale; + } + + inline std::pair computePitchYawFromOrientation(const glm::quat& orientation) const + { + const auto mat = glm::mat3_cast(orientation); + const auto forward = float64_t3(mat[2][0], mat[2][1], mat[2][2]); + const double pitch = std::atan2(std::sqrt(forward.x * forward.x + forward.z * forward.z), forward.y) - HalfPi; + const double yaw = std::atan2(forward.x, forward.z); + return { pitch, yaw }; + } + + inline float64_t3 extractYawPitchRollYXZ(const glm::quat& delta) const + { + const auto m = getMatrix3x3As4x4(matrix(glm::mat3_cast(delta))); + const double yaw = std::atan2(static_cast(m[2][0]), static_cast(m[2][2])); + const double c2 = std::sqrt(static_cast(m[0][1] * m[0][1] + m[1][1] * m[1][1])); + const double pitch = std::atan2(-static_cast(m[2][1]), c2); + const double s1 = std::sin(yaw); + const double c1 = std::cos(yaw); + const double roll = std::atan2( + s1 * static_cast(m[1][2]) - c1 * static_cast(m[1][0]), + c1 * static_cast(m[0][0]) - s1 * static_cast(m[0][2])); + return float64_t3(pitch, yaw, roll); + } + + inline bool computePoseMismatch(ICamera* camera, const CTargetPose& target, double& outPositionDelta, double& outRotationDeltaDeg) const + { + outPositionDelta = 0.0; + outRotationDeltaDeg = 0.0; + if (!camera) + return false; + + const auto& gimbal = camera->getGimbal(); + const auto currentPos = gimbal.getPosition(); + const auto currentOrientation = glm::normalize(gimbal.getOrientation()); + const auto targetOrientation = glm::normalize(target.orientation); + + const double dx = static_cast(currentPos.x - target.position.x); + const double dy = static_cast(currentPos.y - target.position.y); + const double dz = static_cast(currentPos.z - target.position.z); + outPositionDelta = std::sqrt(dx * dx + dy * dy + dz * dz); + + const double orientationDot = std::clamp(static_cast(std::abs(glm::dot(currentOrientation, targetOrientation))), 0.0, 1.0); + outRotationDeltaDeg = glm::degrees(2.0 * std::acos(orientationDot)); + return std::isfinite(outPositionDelta) && std::isfinite(outRotationDeltaDeg); + } + + inline bool tryApplyAbsoluteReferencePose(ICamera* camera, const CTargetPose& target, bool& outChanged, bool& outExact) const + { + outChanged = false; + outExact = false; + if (!camera) + return false; + + switch (camera->getKind()) + { + case ICamera::CameraKind::Free: + case ICamera::CameraKind::FPS: + break; + default: + return false; + } + + double beforePosDelta = 0.0; + double beforeRotDeltaDeg = 0.0; + if (!computePoseMismatch(camera, target, beforePosDelta, beforeRotDeltaDeg)) + return false; + + if (beforePosDelta <= 1e-6 && beforeRotDeltaDeg <= 0.1) + { + outExact = true; + return true; + } + + auto targetFrame = getMatrix3x3As4x4(matrix(glm::mat3_cast(glm::normalize(target.orientation)))); + targetFrame[3] = float64_t4(target.position, 1.0); + + camera->manipulate({}, &targetFrame); + + double afterPosDelta = 0.0; + double afterRotDeltaDeg = 0.0; + if (!computePoseMismatch(camera, target, afterPosDelta, afterRotDeltaDeg)) + return false; + + outChanged = (std::abs(afterPosDelta - beforePosDelta) > 1e-9) || (std::abs(afterRotDeltaDeg - beforeRotDeltaDeg) > 1e-9); + outExact = afterPosDelta <= 1e-6 && afterRotDeltaDeg <= 0.1; + return true; + } + + inline bool computeOrbitStateFromPositionTarget(const float64_t3& position, const float64_t3& target, + double& outU, double& outV, float& outDistance, float minDistance, float maxDistance) const + { + const auto localSpherePosition = position - target; + const double dist = length(localSpherePosition); + if (!std::isfinite(dist)) + return false; + + const double clamped = std::clamp(dist, static_cast(minDistance), static_cast(maxDistance)); + outDistance = static_cast(clamped); + + if (clamped > 0.0) + { + const auto localUnit = localSpherePosition / clamped; + outU = std::atan2(localUnit.y, localUnit.x); + outV = std::asin(localUnit.z); + } + + return true; + } + + inline bool resolveSphericalGoal(ICamera* camera, const CTargetPose& target, const ICamera::SphericalTargetState& sphericalState, SSphericalGoal& outGoal) const + { + outGoal.target = target.hasTargetPosition ? target.targetPosition : sphericalState.target; + outGoal.u = sphericalState.u; + outGoal.v = sphericalState.v; + outGoal.distance = sphericalState.distance; + + if (target.hasOrbitState) + { + outGoal.u = target.orbitU; + outGoal.v = target.orbitV; + outGoal.distance = target.orbitDistance; + } + else + { + if (!computeOrbitStateFromPositionTarget(target.position, outGoal.target, outGoal.u, outGoal.v, outGoal.distance, sphericalState.minDistance, sphericalState.maxDistance)) + return false; + } + + if (target.hasDistance && !target.hasOrbitState) + outGoal.distance = target.distance; + + outGoal.distance = std::clamp(outGoal.distance, sphericalState.minDistance, sphericalState.maxDistance); + return true; + } + + inline bool buildOrbitTranslateEvents(ICamera* camera, const ICamera::SphericalTargetState& sphericalState, const SSphericalGoal& goal, std::vector& out) const + { + const double moveDenom = getMoveMagnitudeDenominator(camera); + appendSignedEvent(out, wrapAngleRad(goal.v - sphericalState.v) / moveDenom, CVirtualGimbalEvent::MoveRight, CVirtualGimbalEvent::MoveLeft); + appendSignedEvent(out, wrapAngleRad(goal.u - sphericalState.u) / moveDenom, CVirtualGimbalEvent::MoveUp, CVirtualGimbalEvent::MoveDown); + appendSignedEvent(out, static_cast(goal.distance - sphericalState.distance) / 0.01, CVirtualGimbalEvent::MoveForward, CVirtualGimbalEvent::MoveBackward); + return !out.empty(); + } + + inline bool buildRotateDistanceEvents(ICamera* camera, const ICamera::SphericalTargetState& sphericalState, const SSphericalGoal& goal, + std::vector& out, bool allowYaw, bool allowPitch, + CVirtualGimbalEvent::VirtualEventType distancePositive, CVirtualGimbalEvent::VirtualEventType distanceNegative) const + { + const double rotationDenom = getRotationMagnitudeDenominator(camera); + if (allowYaw) + appendSignedEvent(out, wrapAngleRad(goal.u - sphericalState.u) / rotationDenom, CVirtualGimbalEvent::PanRight, CVirtualGimbalEvent::PanLeft); + if (allowPitch) + appendSignedEvent(out, wrapAngleRad(goal.v - sphericalState.v) / rotationDenom, CVirtualGimbalEvent::TiltUp, CVirtualGimbalEvent::TiltDown); + if (distancePositive != CVirtualGimbalEvent::None && distanceNegative != CVirtualGimbalEvent::None) + appendSignedEvent(out, static_cast(goal.distance - sphericalState.distance) / 0.01, distancePositive, distanceNegative); + return !out.empty(); + } + + inline bool buildPathEvents(ICamera* camera, const CTargetPose& target, const ICamera::SphericalTargetState& sphericalState, std::vector& out) const + { + if (!camera) + return false; + + const auto effectiveTarget = target.hasTargetPosition ? target.targetPosition : sphericalState.target; + const auto currentOffset = camera->getGimbal().getPosition() - effectiveTarget; + const auto desiredOffset = target.position - effectiveTarget; + + const double currentAngle = std::atan2(currentOffset.z, currentOffset.x); + const double desiredAngle = std::atan2(desiredOffset.z, desiredOffset.x); + const double currentRadius = std::sqrt(currentOffset.x * currentOffset.x + currentOffset.z * currentOffset.z); + const double desiredRadius = std::sqrt(desiredOffset.x * desiredOffset.x + desiredOffset.z * desiredOffset.z); + const double currentHeight = currentOffset.y; + const double desiredHeight = desiredOffset.y; + + const double moveDenom = getMoveMagnitudeDenominator(camera); + appendSignedEvent(out, (desiredRadius - currentRadius) / moveDenom, CVirtualGimbalEvent::MoveRight, CVirtualGimbalEvent::MoveLeft); + appendSignedEvent(out, (desiredHeight - currentHeight) / moveDenom, CVirtualGimbalEvent::MoveUp, CVirtualGimbalEvent::MoveDown); + appendSignedEvent(out, wrapAngleRad(desiredAngle - currentAngle) / moveDenom, CVirtualGimbalEvent::MoveForward, CVirtualGimbalEvent::MoveBackward); + return !out.empty(); + } + + inline bool buildSphericalEvents(ICamera* camera, const CTargetPose& target, std::vector& out) const + { + ICamera::SphericalTargetState sphericalState; + if (!camera || !camera->tryGetSphericalTargetState(sphericalState)) + return false; + + if (camera->getKind() == ICamera::CameraKind::Path) + return buildPathEvents(camera, target, sphericalState, out); + + SSphericalGoal goal; + if (!resolveSphericalGoal(camera, target, sphericalState, goal)) + return false; + + switch (camera->getKind()) + { + case ICamera::CameraKind::Orbit: + case ICamera::CameraKind::DollyZoom: + return buildOrbitTranslateEvents(camera, sphericalState, goal, out); + + case ICamera::CameraKind::Turntable: + case ICamera::CameraKind::Arcball: + return buildRotateDistanceEvents(camera, sphericalState, goal, out, true, true, CVirtualGimbalEvent::MoveForward, CVirtualGimbalEvent::MoveBackward); + + case ICamera::CameraKind::TopDown: + return buildRotateDistanceEvents(camera, sphericalState, goal, out, true, false, CVirtualGimbalEvent::MoveForward, CVirtualGimbalEvent::MoveBackward); + + case ICamera::CameraKind::Isometric: + return buildRotateDistanceEvents(camera, sphericalState, goal, out, false, false, CVirtualGimbalEvent::MoveForward, CVirtualGimbalEvent::MoveBackward); + + case ICamera::CameraKind::Dolly: + return buildRotateDistanceEvents(camera, sphericalState, goal, out, true, true, CVirtualGimbalEvent::None, CVirtualGimbalEvent::None); + + case ICamera::CameraKind::Chase: + return buildRotateDistanceEvents(camera, sphericalState, goal, out, true, true, CVirtualGimbalEvent::MoveUp, CVirtualGimbalEvent::MoveDown); + + default: + return buildOrbitTranslateEvents(camera, sphericalState, goal, out); + } + } + + inline bool buildFreeEvents(ICamera* camera, const CTargetPose& target, std::vector& out) const + { + const auto& gimbal = camera->getGimbal(); + const auto currentPos = gimbal.getPosition(); + const auto right = gimbal.getXAxis(); + const auto up = gimbal.getYAxis(); + const auto forward = gimbal.getZAxis(); + + const auto deltaWorld = target.position - currentPos; + const float64_t3 localDelta( + hlsl::dot(deltaWorld, right), + hlsl::dot(deltaWorld, up), + hlsl::dot(deltaWorld, forward)); + + appendSignedEvent(out, localDelta.x, CVirtualGimbalEvent::MoveRight, CVirtualGimbalEvent::MoveLeft); + appendSignedEvent(out, localDelta.y, CVirtualGimbalEvent::MoveUp, CVirtualGimbalEvent::MoveDown); + appendSignedEvent(out, localDelta.z, CVirtualGimbalEvent::MoveForward, CVirtualGimbalEvent::MoveBackward); + + switch (camera->getKind()) + { + case ICamera::CameraKind::FPS: + { + const auto [curPitch, curYaw] = computePitchYawFromOrientation(gimbal.getOrientation()); + const auto [tgtPitch, tgtYaw] = computePitchYawFromOrientation(target.orientation); + + const double rotScale = camera->getRotationSpeedScale(); + const double invScale = rotScale == 0.0 ? 1.0 : (1.0 / rotScale); + + const double deltaPitch = wrapAngleRad(tgtPitch - curPitch) * invScale; + const double deltaYaw = wrapAngleRad(tgtYaw - curYaw) * invScale; + + appendSignedEvent(out, deltaPitch, CVirtualGimbalEvent::TiltUp, CVirtualGimbalEvent::TiltDown); + appendSignedEvent(out, deltaYaw, CVirtualGimbalEvent::PanRight, CVirtualGimbalEvent::PanLeft); + } break; + + case ICamera::CameraKind::Free: + { + const auto deltaQuat = glm::inverse(gimbal.getOrientation()) * glm::normalize(target.orientation); + const auto angles = extractYawPitchRollYXZ(deltaQuat); + + appendSignedEvent(out, angles.x, CVirtualGimbalEvent::TiltUp, CVirtualGimbalEvent::TiltDown); + appendSignedEvent(out, angles.y, CVirtualGimbalEvent::PanRight, CVirtualGimbalEvent::PanLeft); + appendSignedEvent(out, angles.z, CVirtualGimbalEvent::RollRight, CVirtualGimbalEvent::RollLeft); + } break; + + default: + break; + } + + return !out.empty(); + } +}; + +using CTargetPoseController = CCameraGoalSolver; + +} // namespace nbl::hlsl + +#endif // _C_CAMERA_GOAL_SOLVER_HPP_ diff --git a/common/include/camera/CTargetPoseController.hpp b/common/include/camera/CTargetPoseController.hpp index b56e15998..bf1e45fe6 100644 --- a/common/include/camera/CTargetPoseController.hpp +++ b/common/include/camera/CTargetPoseController.hpp @@ -1,521 +1,6 @@ #ifndef _C_TARGET_POSE_CONTROLLER_HPP_ #define _C_TARGET_POSE_CONTROLLER_HPP_ -#include -#include -#include -#include - -#include "ICamera.hpp" -#include "CFPSCamera.hpp" -#include "CFreeLockCamera.hpp" -#include "CSphericalTargetCamera.hpp" -#include "glm/glm/gtc/quaternion.hpp" - -namespace nbl::hlsl -{ - -struct CTargetPose -{ - float64_t3 position = float64_t3(0.0); - glm::quat orientation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f); - bool hasTargetPosition = false; - float64_t3 targetPosition = float64_t3(0.0); - bool hasDistance = false; - float distance = 0.f; - bool hasOrbitState = false; - double orbitU = 0.0; - double orbitV = 0.0; - float orbitDistance = 0.f; -}; - -class CTargetPoseController -{ -public: - struct SApplyResult - { - enum class EStatus : uint8_t - { - Unsupported, - Failed, - AlreadySatisfied, - AppliedAbsoluteOnly, - AppliedVirtualEvents, - AppliedAbsoluteAndVirtualEvents - }; - - EStatus status = EStatus::Unsupported; - bool exact = false; - uint32_t eventCount = 0u; - - inline bool succeeded() const - { - return status != EStatus::Unsupported && status != EStatus::Failed; - } - - inline bool changed() const - { - return status == EStatus::AppliedAbsoluteOnly || - status == EStatus::AppliedVirtualEvents || - status == EStatus::AppliedAbsoluteAndVirtualEvents; - } - }; - - bool buildEvents(ICamera* camera, const CTargetPose& target, std::vector& out) const - { - out.clear(); - if (!camera) - return false; - - if (camera->hasCapability(ICamera::SphericalTarget)) - return buildSphericalEvents(camera, target, out); - - return buildFreeEvents(camera, target, out); - } - - SApplyResult applyDetailed(ICamera* camera, const CTargetPose& target) const - { - SApplyResult result; - if (!camera) - return result; - - bool exact = true; - bool absoluteChanged = false; - - if (!camera->hasCapability(ICamera::SphericalTarget)) - { - bool poseChanged = false; - bool poseExact = false; - if (tryApplyAbsoluteReferencePose(camera, target, poseChanged, poseExact)) - { - absoluteChanged = absoluteChanged || poseChanged; - if (poseExact) - { - result.status = poseChanged ? - SApplyResult::EStatus::AppliedAbsoluteOnly : - SApplyResult::EStatus::AlreadySatisfied; - result.exact = true; - return result; - } - } - } - - if (target.hasTargetPosition) - { - ICamera::SphericalTargetState beforeState; - if (!camera->tryGetSphericalTargetState(beforeState)) - { - exact = false; - } - else - { - const auto beforeTarget = beforeState.target; - if (!camera->trySetSphericalTarget(target.targetPosition)) - { - exact = false; - } - else - { - ICamera::SphericalTargetState afterState; - if (!camera->tryGetSphericalTargetState(afterState)) - { - exact = false; - } - else - { - absoluteChanged = afterState.target != beforeTarget; - exact = exact && afterState.target == target.targetPosition; - } - } - } - } - - if (target.hasDistance || target.hasOrbitState) - { - ICamera::SphericalTargetState beforeState; - if (!camera->tryGetSphericalTargetState(beforeState)) - { - exact = false; - } - else - { - const float desiredDistance = target.hasOrbitState ? target.orbitDistance : target.distance; - const float beforeDistance = beforeState.distance; - if (!camera->trySetSphericalDistance(desiredDistance)) - { - exact = false; - } - else - { - ICamera::SphericalTargetState afterState; - if (!camera->tryGetSphericalTargetState(afterState)) - { - exact = false; - } - else - { - absoluteChanged = absoluteChanged || afterState.distance != beforeDistance; - exact = exact && std::abs(static_cast(afterState.distance - desiredDistance)) <= 1e-6; - } - } - } - } - - std::vector events; - buildEvents(camera, target, events); - result.eventCount = static_cast(events.size()); - result.exact = exact; - - if (events.empty()) - { - if (absoluteChanged) - result.status = SApplyResult::EStatus::AppliedAbsoluteOnly; - else if (exact) - result.status = SApplyResult::EStatus::AlreadySatisfied; - return result; - } - - if (camera->manipulate({ events.data(), events.size() })) - { - result.status = absoluteChanged ? - SApplyResult::EStatus::AppliedAbsoluteAndVirtualEvents : - SApplyResult::EStatus::AppliedVirtualEvents; - return result; - } - - if (absoluteChanged) - { - result.status = SApplyResult::EStatus::AppliedAbsoluteOnly; - result.exact = false; - return result; - } - - result.status = SApplyResult::EStatus::Failed; - result.exact = false; - return result; - } - - bool apply(ICamera* camera, const CTargetPose& target) const - { - return applyDetailed(camera, target).succeeded(); - } - -private: - static constexpr double Pi = 3.14159265358979323846; - static constexpr double HalfPi = 0.5 * Pi; - - struct SSphericalGoal - { - float64_t3 target = float64_t3(0.0); - double u = 0.0; - double v = 0.0; - float distance = 0.f; - }; - - inline double wrapAngleRad(double angle) const - { - while (angle > Pi) - angle -= 2.0 * Pi; - while (angle < -Pi) - angle += 2.0 * Pi; - return angle; - } - - inline void appendSignedEvent(std::vector& events, double value, - CVirtualGimbalEvent::VirtualEventType positive, CVirtualGimbalEvent::VirtualEventType negative) const - { - if (value == 0.0) - return; - auto& ev = events.emplace_back(); - ev.type = (value > 0.0) ? positive : negative; - ev.magnitude = std::abs(value); - } - - inline double getMoveMagnitudeDenominator(const ICamera* camera) const - { - const double moveScale = camera->getMoveSpeedScale(); - return 0.01 * (moveScale == 0.0 ? 1.0 : moveScale); - } - - inline double getRotationMagnitudeDenominator(const ICamera* camera) const - { - const double rotationScale = camera->getRotationSpeedScale(); - return rotationScale == 0.0 ? 1.0 : rotationScale; - } - - inline std::pair computePitchYawFromOrientation(const glm::quat& orientation) const - { - const auto mat = glm::mat3_cast(orientation); - const auto forward = float64_t3(mat[2][0], mat[2][1], mat[2][2]); - const double pitch = std::atan2(std::sqrt(forward.x * forward.x + forward.z * forward.z), forward.y) - HalfPi; - const double yaw = std::atan2(forward.x, forward.z); - return { pitch, yaw }; - } - - inline float64_t3 extractYawPitchRollYXZ(const glm::quat& delta) const - { - const auto m = getMatrix3x3As4x4(matrix(glm::mat3_cast(delta))); - const double yaw = std::atan2(static_cast(m[2][0]), static_cast(m[2][2])); - const double c2 = std::sqrt(static_cast(m[0][1] * m[0][1] + m[1][1] * m[1][1])); - const double pitch = std::atan2(-static_cast(m[2][1]), c2); - const double s1 = std::sin(yaw); - const double c1 = std::cos(yaw); - const double roll = std::atan2( - s1 * static_cast(m[1][2]) - c1 * static_cast(m[1][0]), - c1 * static_cast(m[0][0]) - s1 * static_cast(m[0][2])); - return float64_t3(pitch, yaw, roll); - } - - inline bool computePoseMismatch(ICamera* camera, const CTargetPose& target, double& outPositionDelta, double& outRotationDeltaDeg) const - { - outPositionDelta = 0.0; - outRotationDeltaDeg = 0.0; - if (!camera) - return false; - - const auto& gimbal = camera->getGimbal(); - const auto currentPos = gimbal.getPosition(); - const auto currentOrientation = glm::normalize(gimbal.getOrientation()); - const auto targetOrientation = glm::normalize(target.orientation); - - const double dx = static_cast(currentPos.x - target.position.x); - const double dy = static_cast(currentPos.y - target.position.y); - const double dz = static_cast(currentPos.z - target.position.z); - outPositionDelta = std::sqrt(dx * dx + dy * dy + dz * dz); - - const double orientationDot = std::clamp(static_cast(std::abs(glm::dot(currentOrientation, targetOrientation))), 0.0, 1.0); - outRotationDeltaDeg = glm::degrees(2.0 * std::acos(orientationDot)); - return std::isfinite(outPositionDelta) && std::isfinite(outRotationDeltaDeg); - } - - inline bool tryApplyAbsoluteReferencePose(ICamera* camera, const CTargetPose& target, bool& outChanged, bool& outExact) const - { - outChanged = false; - outExact = false; - if (!camera) - return false; - - switch (camera->getKind()) - { - case ICamera::CameraKind::Free: - case ICamera::CameraKind::FPS: - break; - default: - return false; - } - - double beforePosDelta = 0.0; - double beforeRotDeltaDeg = 0.0; - if (!computePoseMismatch(camera, target, beforePosDelta, beforeRotDeltaDeg)) - return false; - - if (beforePosDelta <= 1e-6 && beforeRotDeltaDeg <= 0.1) - { - outExact = true; - return true; - } - - auto targetFrame = getMatrix3x3As4x4(matrix(glm::mat3_cast(glm::normalize(target.orientation)))); - targetFrame[3] = float64_t4(target.position, 1.0); - - camera->manipulate({}, &targetFrame); - - double afterPosDelta = 0.0; - double afterRotDeltaDeg = 0.0; - if (!computePoseMismatch(camera, target, afterPosDelta, afterRotDeltaDeg)) - return false; - - outChanged = (std::abs(afterPosDelta - beforePosDelta) > 1e-9) || (std::abs(afterRotDeltaDeg - beforeRotDeltaDeg) > 1e-9); - outExact = afterPosDelta <= 1e-6 && afterRotDeltaDeg <= 0.1; - return true; - } - - inline bool computeOrbitStateFromPositionTarget(const float64_t3& position, const float64_t3& target, - double& outU, double& outV, float& outDistance, float minDistance, float maxDistance) const - { - const auto localSpherePosition = position - target; - const double dist = length(localSpherePosition); - if (!std::isfinite(dist)) - return false; - - const double clamped = std::clamp(dist, static_cast(minDistance), static_cast(maxDistance)); - outDistance = static_cast(clamped); - - if (clamped > 0.0) - { - const auto localUnit = localSpherePosition / clamped; - outU = std::atan2(localUnit.y, localUnit.x); - outV = std::asin(localUnit.z); - } - - return true; - } - - inline bool resolveSphericalGoal(ICamera* camera, const CTargetPose& target, const ICamera::SphericalTargetState& sphericalState, SSphericalGoal& outGoal) const - { - outGoal.target = target.hasTargetPosition ? target.targetPosition : sphericalState.target; - outGoal.u = sphericalState.u; - outGoal.v = sphericalState.v; - outGoal.distance = sphericalState.distance; - - if (target.hasOrbitState) - { - outGoal.u = target.orbitU; - outGoal.v = target.orbitV; - outGoal.distance = target.orbitDistance; - } - else - { - if (!computeOrbitStateFromPositionTarget(target.position, outGoal.target, outGoal.u, outGoal.v, outGoal.distance, sphericalState.minDistance, sphericalState.maxDistance)) - return false; - } - - if (target.hasDistance && !target.hasOrbitState) - outGoal.distance = target.distance; - - outGoal.distance = std::clamp(outGoal.distance, sphericalState.minDistance, sphericalState.maxDistance); - return true; - } - - inline bool buildOrbitTranslateEvents(ICamera* camera, const ICamera::SphericalTargetState& sphericalState, const SSphericalGoal& goal, std::vector& out) const - { - const double moveDenom = getMoveMagnitudeDenominator(camera); - appendSignedEvent(out, wrapAngleRad(goal.v - sphericalState.v) / moveDenom, CVirtualGimbalEvent::MoveRight, CVirtualGimbalEvent::MoveLeft); - appendSignedEvent(out, wrapAngleRad(goal.u - sphericalState.u) / moveDenom, CVirtualGimbalEvent::MoveUp, CVirtualGimbalEvent::MoveDown); - appendSignedEvent(out, static_cast(goal.distance - sphericalState.distance) / 0.01, CVirtualGimbalEvent::MoveForward, CVirtualGimbalEvent::MoveBackward); - return !out.empty(); - } - - inline bool buildRotateDistanceEvents(ICamera* camera, const ICamera::SphericalTargetState& sphericalState, const SSphericalGoal& goal, - std::vector& out, bool allowYaw, bool allowPitch, - CVirtualGimbalEvent::VirtualEventType distancePositive, CVirtualGimbalEvent::VirtualEventType distanceNegative) const - { - const double rotationDenom = getRotationMagnitudeDenominator(camera); - if (allowYaw) - appendSignedEvent(out, wrapAngleRad(goal.u - sphericalState.u) / rotationDenom, CVirtualGimbalEvent::PanRight, CVirtualGimbalEvent::PanLeft); - if (allowPitch) - appendSignedEvent(out, wrapAngleRad(goal.v - sphericalState.v) / rotationDenom, CVirtualGimbalEvent::TiltUp, CVirtualGimbalEvent::TiltDown); - if (distancePositive != CVirtualGimbalEvent::None && distanceNegative != CVirtualGimbalEvent::None) - appendSignedEvent(out, static_cast(goal.distance - sphericalState.distance) / 0.01, distancePositive, distanceNegative); - return !out.empty(); - } - - inline bool buildPathEvents(ICamera* camera, const CTargetPose& target, const ICamera::SphericalTargetState& sphericalState, std::vector& out) const - { - if (!camera) - return false; - - const auto effectiveTarget = target.hasTargetPosition ? target.targetPosition : sphericalState.target; - const auto currentOffset = camera->getGimbal().getPosition() - effectiveTarget; - const auto desiredOffset = target.position - effectiveTarget; - - const double currentAngle = std::atan2(currentOffset.z, currentOffset.x); - const double desiredAngle = std::atan2(desiredOffset.z, desiredOffset.x); - const double currentRadius = std::sqrt(currentOffset.x * currentOffset.x + currentOffset.z * currentOffset.z); - const double desiredRadius = std::sqrt(desiredOffset.x * desiredOffset.x + desiredOffset.z * desiredOffset.z); - const double currentHeight = currentOffset.y; - const double desiredHeight = desiredOffset.y; - - const double moveDenom = getMoveMagnitudeDenominator(camera); - appendSignedEvent(out, (desiredRadius - currentRadius) / moveDenom, CVirtualGimbalEvent::MoveRight, CVirtualGimbalEvent::MoveLeft); - appendSignedEvent(out, (desiredHeight - currentHeight) / moveDenom, CVirtualGimbalEvent::MoveUp, CVirtualGimbalEvent::MoveDown); - appendSignedEvent(out, wrapAngleRad(desiredAngle - currentAngle) / moveDenom, CVirtualGimbalEvent::MoveForward, CVirtualGimbalEvent::MoveBackward); - return !out.empty(); - } - - inline bool buildSphericalEvents(ICamera* camera, const CTargetPose& target, std::vector& out) const - { - ICamera::SphericalTargetState sphericalState; - if (!camera || !camera->tryGetSphericalTargetState(sphericalState)) - return false; - - if (camera->getKind() == ICamera::CameraKind::Path) - return buildPathEvents(camera, target, sphericalState, out); - - SSphericalGoal goal; - if (!resolveSphericalGoal(camera, target, sphericalState, goal)) - return false; - - switch (camera->getKind()) - { - case ICamera::CameraKind::Orbit: - case ICamera::CameraKind::DollyZoom: - return buildOrbitTranslateEvents(camera, sphericalState, goal, out); - - case ICamera::CameraKind::Turntable: - case ICamera::CameraKind::Arcball: - return buildRotateDistanceEvents(camera, sphericalState, goal, out, true, true, CVirtualGimbalEvent::MoveForward, CVirtualGimbalEvent::MoveBackward); - - case ICamera::CameraKind::TopDown: - return buildRotateDistanceEvents(camera, sphericalState, goal, out, true, false, CVirtualGimbalEvent::MoveForward, CVirtualGimbalEvent::MoveBackward); - - case ICamera::CameraKind::Isometric: - return buildRotateDistanceEvents(camera, sphericalState, goal, out, false, false, CVirtualGimbalEvent::MoveForward, CVirtualGimbalEvent::MoveBackward); - - case ICamera::CameraKind::Dolly: - return buildRotateDistanceEvents(camera, sphericalState, goal, out, true, true, CVirtualGimbalEvent::None, CVirtualGimbalEvent::None); - - case ICamera::CameraKind::Chase: - return buildRotateDistanceEvents(camera, sphericalState, goal, out, true, true, CVirtualGimbalEvent::MoveUp, CVirtualGimbalEvent::MoveDown); - - default: - return buildOrbitTranslateEvents(camera, sphericalState, goal, out); - } - } - - inline bool buildFreeEvents(ICamera* camera, const CTargetPose& target, std::vector& out) const - { - const auto& gimbal = camera->getGimbal(); - const auto currentPos = gimbal.getPosition(); - const auto right = gimbal.getXAxis(); - const auto up = gimbal.getYAxis(); - const auto forward = gimbal.getZAxis(); - - const auto deltaWorld = target.position - currentPos; - const float64_t3 localDelta( - hlsl::dot(deltaWorld, right), - hlsl::dot(deltaWorld, up), - hlsl::dot(deltaWorld, forward)); - - appendSignedEvent(out, localDelta.x, CVirtualGimbalEvent::MoveRight, CVirtualGimbalEvent::MoveLeft); - appendSignedEvent(out, localDelta.y, CVirtualGimbalEvent::MoveUp, CVirtualGimbalEvent::MoveDown); - appendSignedEvent(out, localDelta.z, CVirtualGimbalEvent::MoveForward, CVirtualGimbalEvent::MoveBackward); - - switch (camera->getKind()) - { - case ICamera::CameraKind::FPS: - { - const auto [curPitch, curYaw] = computePitchYawFromOrientation(gimbal.getOrientation()); - const auto [tgtPitch, tgtYaw] = computePitchYawFromOrientation(target.orientation); - - const double rotScale = camera->getRotationSpeedScale(); - const double invScale = rotScale == 0.0 ? 1.0 : (1.0 / rotScale); - - const double deltaPitch = wrapAngleRad(tgtPitch - curPitch) * invScale; - const double deltaYaw = wrapAngleRad(tgtYaw - curYaw) * invScale; - - appendSignedEvent(out, deltaPitch, CVirtualGimbalEvent::TiltUp, CVirtualGimbalEvent::TiltDown); - appendSignedEvent(out, deltaYaw, CVirtualGimbalEvent::PanRight, CVirtualGimbalEvent::PanLeft); - } break; - - case ICamera::CameraKind::Free: - { - const auto deltaQuat = glm::inverse(gimbal.getOrientation()) * glm::normalize(target.orientation); - const auto angles = extractYawPitchRollYXZ(deltaQuat); - - appendSignedEvent(out, angles.x, CVirtualGimbalEvent::TiltUp, CVirtualGimbalEvent::TiltDown); - appendSignedEvent(out, angles.y, CVirtualGimbalEvent::PanRight, CVirtualGimbalEvent::PanLeft); - appendSignedEvent(out, angles.z, CVirtualGimbalEvent::RollRight, CVirtualGimbalEvent::RollLeft); - } break; - - default: - break; - } - - return !out.empty(); - } -}; - -} // namespace nbl::hlsl +#include "CCameraGoalSolver.hpp" #endif // _C_TARGET_POSE_CONTROLLER_HPP_ From 6efdce6724488677e70e48e357c6a6876082da4d Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Fri, 3 Apr 2026 20:18:37 +0200 Subject: [PATCH 111/205] Unify cameraz goals across capture apply and playback --- 61_UI/include/app/App.hpp | 199 ++++++++------------ 61_UI/include/common.hpp | 1 + common/include/camera/CCameraGoalSolver.hpp | 94 +++++++-- 3 files changed, 163 insertions(+), 131 deletions(-) diff --git a/61_UI/include/app/App.hpp b/61_UI/include/app/App.hpp index bff113039..3c89357cb 100644 --- a/61_UI/include/app/App.hpp +++ b/61_UI/include/app/App.hpp @@ -1042,48 +1042,26 @@ class App final : public examples::SimpleWindowedApplication projection.setPerspective(params.m_zNear, params.m_zFar, dynamicFov); } - inline CameraPreset capturePreset(ICamera* camera, const std::string& name) + inline void assignGoalToPreset(CameraPreset& preset, const CCameraGoal& goal) const { - CameraPreset preset; - preset.name = name; - if (!camera) - return preset; - - preset.identifier = std::string(camera->getIdentifier()); - const auto& gimbal = camera->getGimbal(); - preset.position = gimbal.getPosition(); - preset.orientation = gimbal.getOrientation(); - - ICamera::SphericalTargetState sphericalState; - if (camera->tryGetSphericalTargetState(sphericalState)) - { - preset.targetPosition = sphericalState.target; - preset.hasTargetPosition = true; - preset.distance = sphericalState.distance; - preset.hasDistance = true; - preset.orbitDistance = sphericalState.distance; - preset.orbitU = sphericalState.u; - preset.orbitV = sphericalState.v; - preset.hasOrbitState = true; - } - - ICamera::DynamicPerspectiveState dynamicPerspectiveState; - if (camera->tryGetDynamicPerspectiveState(dynamicPerspectiveState)) - { - preset.dynamicBaseFov = dynamicPerspectiveState.baseFov; - preset.dynamicReferenceDistance = dynamicPerspectiveState.referenceDistance; - preset.hasDynamicPerspectiveState = true; - } - - return preset; + preset.position = goal.position; + preset.orientation = goal.orientation; + preset.targetPosition = goal.targetPosition; + preset.hasTargetPosition = goal.hasTargetPosition; + preset.distance = goal.distance; + preset.hasDistance = goal.hasDistance; + preset.orbitU = goal.orbitU; + preset.orbitV = goal.orbitV; + preset.orbitDistance = goal.orbitDistance; + preset.hasOrbitState = goal.hasOrbitState; + preset.dynamicBaseFov = goal.dynamicPerspectiveState.baseFov; + preset.dynamicReferenceDistance = goal.dynamicPerspectiveState.referenceDistance; + preset.hasDynamicPerspectiveState = goal.hasDynamicPerspectiveState; } - inline CCameraGoalSolver::SApplyResult applyPresetToCameraDetailed(ICamera* camera, const CameraPreset& preset) + inline CCameraGoal makeGoalFromPreset(const CameraPreset& preset) const { - CTargetPose target; - if (!camera) - return {}; - + CCameraGoal target; target.position = preset.position; target.orientation = preset.orientation; target.hasTargetPosition = preset.hasTargetPosition; @@ -1094,59 +1072,80 @@ class App final : public examples::SimpleWindowedApplication target.orbitU = preset.orbitU; target.orbitV = preset.orbitV; target.orbitDistance = preset.orbitDistance; - - auto result = m_cameraGoalSolver.applyDetailed(camera, target); - if (!preset.hasDynamicPerspectiveState) - return result; - - ICamera::DynamicPerspectiveState beforeState; - if (!camera->tryGetDynamicPerspectiveState(beforeState)) - { - result.exact = false; - return result; - } - - const ICamera::DynamicPerspectiveState desiredState = { + target.hasDynamicPerspectiveState = preset.hasDynamicPerspectiveState; + target.dynamicPerspectiveState = { .baseFov = preset.dynamicBaseFov, .referenceDistance = preset.dynamicReferenceDistance }; - if (!camera->trySetDynamicPerspectiveState(desiredState)) + return target; + } + + inline CCameraGoal blendGoals(const CCameraGoal& a, const CCameraGoal& b, double alpha) const + { + CCameraGoal blended; + blended.position = a.position + (b.position - a.position) * alpha; + blended.orientation = glm::slerp(a.orientation, b.orientation, static_cast(alpha)); + blended.hasTargetPosition = a.hasTargetPosition || b.hasTargetPosition; + if (blended.hasTargetPosition) { - result.exact = false; - return result; + const auto ta = a.hasTargetPosition ? a.targetPosition : b.targetPosition; + const auto tb = b.hasTargetPosition ? b.targetPosition : a.targetPosition; + blended.targetPosition = ta + (tb - ta) * alpha; } - - ICamera::DynamicPerspectiveState afterState; - if (!camera->tryGetDynamicPerspectiveState(afterState)) + blended.hasDistance = a.hasDistance || b.hasDistance; + if (blended.hasDistance) { - result.exact = false; - return result; + const float da = a.hasDistance ? a.distance : b.distance; + const float db = b.hasDistance ? b.distance : a.distance; + blended.distance = da + (db - da) * static_cast(alpha); } - - const auto nearlyEqual = [](const float a, const float b) + blended.hasOrbitState = a.hasOrbitState || b.hasOrbitState; + if (blended.hasOrbitState) { - return std::abs(static_cast(a - b)) <= 1e-6; - }; - - const bool dynamicChanged = !nearlyEqual(beforeState.baseFov, afterState.baseFov) || - !nearlyEqual(beforeState.referenceDistance, afterState.referenceDistance); - const bool dynamicExact = nearlyEqual(afterState.baseFov, desiredState.baseFov) && - nearlyEqual(afterState.referenceDistance, desiredState.referenceDistance); + const double ua = a.hasOrbitState ? a.orbitU : b.orbitU; + const double ub = b.hasOrbitState ? b.orbitU : a.orbitU; + const double va = a.hasOrbitState ? a.orbitV : b.orbitV; + const double vb = b.hasOrbitState ? b.orbitV : a.orbitV; + const float da = a.hasOrbitState ? a.orbitDistance : b.orbitDistance; + const float db = b.hasOrbitState ? b.orbitDistance : a.orbitDistance; - if (dynamicChanged) - { - if (!result.succeeded() || !result.changed()) - result.status = CCameraGoalSolver::SApplyResult::EStatus::AppliedAbsoluteOnly; - else if (result.status == CCameraGoalSolver::SApplyResult::EStatus::AppliedVirtualEvents) - result.status = CCameraGoalSolver::SApplyResult::EStatus::AppliedAbsoluteAndVirtualEvents; + blended.orbitU = ua + (ub - ua) * alpha; + blended.orbitV = va + (vb - va) * alpha; + blended.orbitDistance = da + (db - da) * static_cast(alpha); } - else if (!result.succeeded() && dynamicExact) + blended.hasDynamicPerspectiveState = a.hasDynamicPerspectiveState || b.hasDynamicPerspectiveState; + if (blended.hasDynamicPerspectiveState) { - result.status = CCameraGoalSolver::SApplyResult::EStatus::AlreadySatisfied; + const auto dynamicA = a.hasDynamicPerspectiveState ? a.dynamicPerspectiveState : b.dynamicPerspectiveState; + const auto dynamicB = b.hasDynamicPerspectiveState ? b.dynamicPerspectiveState : a.dynamicPerspectiveState; + blended.dynamicPerspectiveState.baseFov = dynamicA.baseFov + (dynamicB.baseFov - dynamicA.baseFov) * static_cast(alpha); + blended.dynamicPerspectiveState.referenceDistance = + dynamicA.referenceDistance + (dynamicB.referenceDistance - dynamicA.referenceDistance) * static_cast(alpha); } + return blended; + } + + inline CameraPreset capturePreset(ICamera* camera, const std::string& name) + { + CameraPreset preset; + preset.name = name; + if (!camera) + return preset; + + preset.identifier = std::string(camera->getIdentifier()); + CCameraGoal goal; + if (m_cameraGoalSolver.capture(camera, goal)) + assignGoalToPreset(preset, goal); + + return preset; + } + + inline CCameraGoalSolver::SApplyResult applyPresetToCameraDetailed(ICamera* camera, const CameraPreset& preset) + { + if (!camera) + return {}; - result.exact = result.exact && dynamicExact; - return result; + return m_cameraGoalSolver.applyDetailed(camera, makeGoalFromPreset(preset)); } inline bool applyPresetToCamera(ICamera* camera, const CameraPreset& preset) @@ -1384,48 +1383,8 @@ class App final : public examples::SimpleWindowedApplication const double alpha = static_cast(time - a.time) / static_cast(b.time - a.time); - CameraPreset blended; - blended.position = a.preset.position + (b.preset.position - a.preset.position) * alpha; - blended.orientation = glm::slerp(a.preset.orientation, b.preset.orientation, static_cast(alpha)); - blended.hasTargetPosition = a.preset.hasTargetPosition || b.preset.hasTargetPosition; - if (blended.hasTargetPosition) - { - const auto ta = a.preset.hasTargetPosition ? a.preset.targetPosition : b.preset.targetPosition; - const auto tb = b.preset.hasTargetPosition ? b.preset.targetPosition : a.preset.targetPosition; - blended.targetPosition = ta + (tb - ta) * alpha; - } - blended.hasDistance = a.preset.hasDistance || b.preset.hasDistance; - if (blended.hasDistance) - { - const float da = a.preset.hasDistance ? a.preset.distance : b.preset.distance; - const float db = b.preset.hasDistance ? b.preset.distance : a.preset.distance; - blended.distance = da + (db - da) * static_cast(alpha); - } - blended.hasOrbitState = a.preset.hasOrbitState || b.preset.hasOrbitState; - if (blended.hasOrbitState) - { - const double ua = a.preset.hasOrbitState ? a.preset.orbitU : b.preset.orbitU; - const double ub = b.preset.hasOrbitState ? b.preset.orbitU : a.preset.orbitU; - const double va = a.preset.hasOrbitState ? a.preset.orbitV : b.preset.orbitV; - const double vb = b.preset.hasOrbitState ? b.preset.orbitV : a.preset.orbitV; - const float da = a.preset.hasOrbitState ? a.preset.orbitDistance : b.preset.orbitDistance; - const float db = b.preset.hasOrbitState ? b.preset.orbitDistance : a.preset.orbitDistance; - - blended.orbitU = ua + (ub - ua) * alpha; - blended.orbitV = va + (vb - va) * alpha; - blended.orbitDistance = da + (db - da) * static_cast(alpha); - } - blended.hasDynamicPerspectiveState = a.preset.hasDynamicPerspectiveState || b.preset.hasDynamicPerspectiveState; - if (blended.hasDynamicPerspectiveState) - { - const float fa = a.preset.hasDynamicPerspectiveState ? a.preset.dynamicBaseFov : b.preset.dynamicBaseFov; - const float fb = b.preset.hasDynamicPerspectiveState ? b.preset.dynamicBaseFov : a.preset.dynamicBaseFov; - const float ra = a.preset.hasDynamicPerspectiveState ? a.preset.dynamicReferenceDistance : b.preset.dynamicReferenceDistance; - const float rb = b.preset.hasDynamicPerspectiveState ? b.preset.dynamicReferenceDistance : a.preset.dynamicReferenceDistance; - - blended.dynamicBaseFov = fa + (fb - fa) * static_cast(alpha); - blended.dynamicReferenceDistance = ra + (rb - ra) * static_cast(alpha); - } + CameraPreset blended = a.preset; + assignGoalToPreset(blended, blendGoals(makeGoalFromPreset(a.preset), makeGoalFromPreset(b.preset), alpha)); applyPresetToTargets(blended); } diff --git a/61_UI/include/common.hpp b/61_UI/include/common.hpp index d2349413a..ef121fcc8 100644 --- a/61_UI/include/common.hpp +++ b/61_UI/include/common.hpp @@ -55,6 +55,7 @@ using nbl::hlsl::CChaseCamera; using nbl::hlsl::CDollyCamera; using nbl::hlsl::CDollyZoomCamera; using nbl::hlsl::CPathCamera; +using nbl::hlsl::CCameraGoal; using nbl::hlsl::CTargetPose; using nbl::hlsl::CCameraGoalSolver; using nbl::hlsl::CTargetPoseController; diff --git a/common/include/camera/CCameraGoalSolver.hpp b/common/include/camera/CCameraGoalSolver.hpp index 9d9c7d33b..1454afbf9 100644 --- a/common/include/camera/CCameraGoalSolver.hpp +++ b/common/include/camera/CCameraGoalSolver.hpp @@ -15,7 +15,7 @@ namespace nbl::hlsl { -struct CTargetPose +struct CCameraGoal { float64_t3 position = float64_t3(0.0); glm::quat orientation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f); @@ -27,6 +27,8 @@ struct CTargetPose double orbitU = 0.0; double orbitV = 0.0; float orbitDistance = 0.f; + bool hasDynamicPerspectiveState = false; + ICamera::DynamicPerspectiveState dynamicPerspectiveState = {}; }; class CCameraGoalSolver @@ -61,7 +63,7 @@ class CCameraGoalSolver } }; - bool buildEvents(ICamera* camera, const CTargetPose& target, std::vector& out) const + bool buildEvents(ICamera* camera, const CCameraGoal& target, std::vector& out) const { out.clear(); if (!camera) @@ -73,7 +75,40 @@ class CCameraGoalSolver return buildFreeEvents(camera, target, out); } - SApplyResult applyDetailed(ICamera* camera, const CTargetPose& target) const + bool capture(ICamera* camera, CCameraGoal& out) const + { + out = {}; + if (!camera) + return false; + + const auto& gimbal = camera->getGimbal(); + out.position = gimbal.getPosition(); + out.orientation = gimbal.getOrientation(); + + ICamera::SphericalTargetState sphericalState; + if (camera->tryGetSphericalTargetState(sphericalState)) + { + out.targetPosition = sphericalState.target; + out.hasTargetPosition = true; + out.distance = sphericalState.distance; + out.hasDistance = true; + out.orbitDistance = sphericalState.distance; + out.orbitU = sphericalState.u; + out.orbitV = sphericalState.v; + out.hasOrbitState = true; + } + + ICamera::DynamicPerspectiveState dynamicState; + if (camera->tryGetDynamicPerspectiveState(dynamicState)) + { + out.hasDynamicPerspectiveState = true; + out.dynamicPerspectiveState = dynamicState; + } + + return true; + } + + SApplyResult applyDetailed(ICamera* camera, const CCameraGoal& target) const { SApplyResult result; if (!camera) @@ -89,7 +124,7 @@ class CCameraGoalSolver if (tryApplyAbsoluteReferencePose(camera, target, poseChanged, poseExact)) { absoluteChanged = absoluteChanged || poseChanged; - if (poseExact) + if (poseExact && !target.hasDynamicPerspectiveState) { result.status = poseChanged ? SApplyResult::EStatus::AppliedAbsoluteOnly : @@ -161,6 +196,37 @@ class CCameraGoalSolver } } + if (target.hasDynamicPerspectiveState) + { + ICamera::DynamicPerspectiveState beforeState; + if (!camera->tryGetDynamicPerspectiveState(beforeState)) + { + exact = false; + } + else if (!camera->trySetDynamicPerspectiveState(target.dynamicPerspectiveState)) + { + exact = false; + } + else + { + ICamera::DynamicPerspectiveState afterState; + if (!camera->tryGetDynamicPerspectiveState(afterState)) + { + exact = false; + } + else + { + const bool dynamicChanged = !nearlyEqual(beforeState.baseFov, afterState.baseFov) || + !nearlyEqual(beforeState.referenceDistance, afterState.referenceDistance); + const bool dynamicExact = nearlyEqual(afterState.baseFov, target.dynamicPerspectiveState.baseFov) && + nearlyEqual(afterState.referenceDistance, target.dynamicPerspectiveState.referenceDistance); + + absoluteChanged = absoluteChanged || dynamicChanged; + exact = exact && dynamicExact; + } + } + } + std::vector events; buildEvents(camera, target, events); result.eventCount = static_cast(events.size()); @@ -195,7 +261,7 @@ class CCameraGoalSolver return result; } - bool apply(ICamera* camera, const CTargetPose& target) const + bool apply(ICamera* camera, const CCameraGoal& target) const { return applyDetailed(camera, target).succeeded(); } @@ -221,6 +287,11 @@ class CCameraGoalSolver return angle; } + inline bool nearlyEqual(double a, double b, double eps = 1e-6) const + { + return std::abs(a - b) <= eps; + } + inline void appendSignedEvent(std::vector& events, double value, CVirtualGimbalEvent::VirtualEventType positive, CVirtualGimbalEvent::VirtualEventType negative) const { @@ -266,7 +337,7 @@ class CCameraGoalSolver return float64_t3(pitch, yaw, roll); } - inline bool computePoseMismatch(ICamera* camera, const CTargetPose& target, double& outPositionDelta, double& outRotationDeltaDeg) const + inline bool computePoseMismatch(ICamera* camera, const CCameraGoal& target, double& outPositionDelta, double& outRotationDeltaDeg) const { outPositionDelta = 0.0; outRotationDeltaDeg = 0.0; @@ -288,7 +359,7 @@ class CCameraGoalSolver return std::isfinite(outPositionDelta) && std::isfinite(outRotationDeltaDeg); } - inline bool tryApplyAbsoluteReferencePose(ICamera* camera, const CTargetPose& target, bool& outChanged, bool& outExact) const + inline bool tryApplyAbsoluteReferencePose(ICamera* camera, const CCameraGoal& target, bool& outChanged, bool& outExact) const { outChanged = false; outExact = false; @@ -351,7 +422,7 @@ class CCameraGoalSolver return true; } - inline bool resolveSphericalGoal(ICamera* camera, const CTargetPose& target, const ICamera::SphericalTargetState& sphericalState, SSphericalGoal& outGoal) const + inline bool resolveSphericalGoal(ICamera* camera, const CCameraGoal& target, const ICamera::SphericalTargetState& sphericalState, SSphericalGoal& outGoal) const { outGoal.target = target.hasTargetPosition ? target.targetPosition : sphericalState.target; outGoal.u = sphericalState.u; @@ -400,7 +471,7 @@ class CCameraGoalSolver return !out.empty(); } - inline bool buildPathEvents(ICamera* camera, const CTargetPose& target, const ICamera::SphericalTargetState& sphericalState, std::vector& out) const + inline bool buildPathEvents(ICamera* camera, const CCameraGoal& target, const ICamera::SphericalTargetState& sphericalState, std::vector& out) const { if (!camera) return false; @@ -423,7 +494,7 @@ class CCameraGoalSolver return !out.empty(); } - inline bool buildSphericalEvents(ICamera* camera, const CTargetPose& target, std::vector& out) const + inline bool buildSphericalEvents(ICamera* camera, const CCameraGoal& target, std::vector& out) const { ICamera::SphericalTargetState sphericalState; if (!camera || !camera->tryGetSphericalTargetState(sphericalState)) @@ -463,7 +534,7 @@ class CCameraGoalSolver } } - inline bool buildFreeEvents(ICamera* camera, const CTargetPose& target, std::vector& out) const + inline bool buildFreeEvents(ICamera* camera, const CCameraGoal& target, std::vector& out) const { const auto& gimbal = camera->getGimbal(); const auto currentPos = gimbal.getPosition(); @@ -516,6 +587,7 @@ class CCameraGoalSolver } }; +using CTargetPose = CCameraGoal; using CTargetPoseController = CCameraGoalSolver; } // namespace nbl::hlsl From a536336e1362286dcb0edbfdab20265eea5f5f75 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 09:14:01 +0200 Subject: [PATCH 112/205] Refine camera goal solver state handling --- 61_UI/AppInit.cpp | 230 ++++----- 61_UI/include/app/App.hpp | 511 +++++++++++++++----- 61_UI/include/common.hpp | 2 - common/include/camera/CCameraGoalSolver.hpp | 81 ++++ common/include/camera/CPathCamera.hpp | 19 + common/include/camera/ICamera.hpp | 17 + 6 files changed, 613 insertions(+), 247 deletions(-) diff --git a/61_UI/AppInit.cpp b/61_UI/AppInit.cpp index 29c0df700..ae9822a10 100644 --- a/61_UI/AppInit.cpp +++ b/61_UI/AppInit.cpp @@ -321,126 +321,12 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) auto comparePresetToCamera = [&](ICamera* camera, const CameraPreset& preset, const double posEps, const double rotEpsDeg, const double scalarEps) -> bool { - if (!camera) - return false; - - auto isFiniteQuat = [](const glm::quat& q) -> bool - { - return std::isfinite(q.x) && std::isfinite(q.y) && std::isfinite(q.z) && std::isfinite(q.w); - }; - - auto angleDiffRad = [](double a, double b) -> double - { - double d = std::fmod(a - b + 3.14159265358979323846, 6.28318530717958647692); - if (d < 0.0) - d += 6.28318530717958647692; - return std::abs(d - 3.14159265358979323846); - }; - - const auto& gimbal = camera->getGimbal(); - const auto currentPos = gimbal.getPosition(); - const auto currentOrientation = glm::normalize(gimbal.getOrientation()); - const auto expectedOrientation = glm::normalize(preset.orientation); - if (!isFinite3(currentPos) || !isFiniteQuat(currentOrientation) || !isFiniteQuat(expectedOrientation)) - return false; - - const double dx = static_cast(currentPos.x - preset.position.x); - const double dy = static_cast(currentPos.y - preset.position.y); - const double dz = static_cast(currentPos.z - preset.position.z); - const double posDelta = std::sqrt(dx * dx + dy * dy + dz * dz); - const double orientationDot = std::clamp(static_cast(std::abs(glm::dot(currentOrientation, expectedOrientation))), 0.0, 1.0); - const double rotDeltaDeg = glm::degrees(2.0 * std::acos(orientationDot)); - if (posDelta > posEps || rotDeltaDeg > rotEpsDeg) - return false; - - if (preset.hasTargetPosition || preset.hasDistance || preset.hasOrbitState) - { - ICamera::SphericalTargetState sphericalState; - if (!camera->tryGetSphericalTargetState(sphericalState)) - return false; - if (preset.hasTargetPosition && !nearlyEqual3(sphericalState.target, preset.targetPosition, scalarEps)) - return false; - if (preset.hasDistance && std::abs(static_cast(sphericalState.distance - preset.distance)) > scalarEps) - return false; - if (preset.hasOrbitState) - { - if (angleDiffRad(preset.orbitU, sphericalState.u) > rotEpsDeg * (3.14159265358979323846 / 180.0)) - return false; - if (angleDiffRad(preset.orbitV, sphericalState.v) > rotEpsDeg * (3.14159265358979323846 / 180.0)) - return false; - if (std::abs(static_cast(sphericalState.distance - preset.orbitDistance)) > scalarEps) - return false; - } - } - - if (preset.hasDynamicPerspectiveState) - { - ICamera::DynamicPerspectiveState dynamicState; - if (!camera->tryGetDynamicPerspectiveState(dynamicState)) - return false; - if (std::abs(static_cast(dynamicState.baseFov - preset.dynamicBaseFov)) > scalarEps) - return false; - if (std::abs(static_cast(dynamicState.referenceDistance - preset.dynamicReferenceDistance)) > scalarEps) - return false; - } - - return true; + return comparePresetToCameraState(camera, preset, posEps, rotEpsDeg, scalarEps); }; auto describePresetMismatch = [&](ICamera* camera, const CameraPreset& preset) -> std::string { - if (!camera) - return "camera=null"; - - std::ostringstream oss; - const auto& gimbal = camera->getGimbal(); - const auto currentPos = gimbal.getPosition(); - const auto currentOrientation = glm::normalize(gimbal.getOrientation()); - const auto expectedOrientation = glm::normalize(preset.orientation); - const double dx = static_cast(currentPos.x - preset.position.x); - const double dy = static_cast(currentPos.y - preset.position.y); - const double dz = static_cast(currentPos.z - preset.position.z); - const double posDelta = std::sqrt(dx * dx + dy * dy + dz * dz); - const double orientationDot = std::clamp(static_cast(std::abs(glm::dot(currentOrientation, expectedOrientation))), 0.0, 1.0); - const double rotDeltaDeg = glm::degrees(2.0 * std::acos(orientationDot)); - oss << "pos_delta=" << posDelta - << " rot_delta_deg=" << rotDeltaDeg - << " current_pos=(" << currentPos.x << "," << currentPos.y << "," << currentPos.z << ")" - << " expected_pos=(" << preset.position.x << "," << preset.position.y << "," << preset.position.z << ")" - << " current_quat=(" << currentOrientation.x << "," << currentOrientation.y << "," << currentOrientation.z << "," << currentOrientation.w << ")" - << " expected_quat=(" << expectedOrientation.x << "," << expectedOrientation.y << "," << expectedOrientation.z << "," << expectedOrientation.w << ")"; - - if (preset.hasTargetPosition || preset.hasDistance || preset.hasOrbitState) - { - ICamera::SphericalTargetState sphericalState; - if (camera->tryGetSphericalTargetState(sphericalState)) - { - oss << " target=(" << sphericalState.target.x << "," << sphericalState.target.y << "," << sphericalState.target.z << ")" - << " distance=" << sphericalState.distance - << " orbit_u=" << sphericalState.u - << " orbit_v=" << sphericalState.v; - } - else - { - oss << " spherical_state=unavailable"; - } - } - - if (preset.hasDynamicPerspectiveState) - { - ICamera::DynamicPerspectiveState dynamicState; - if (camera->tryGetDynamicPerspectiveState(dynamicState)) - { - oss << " dynamic_base_fov=" << dynamicState.baseFov - << " dynamic_reference_distance=" << dynamicState.referenceDistance; - } - else - { - oss << " dynamic_perspective_state=unavailable"; - } - } - - return oss.str(); + return describePresetCameraMismatch(camera, preset); }; auto collectKeyboardVirtualEvents = [&](CGimbalInputBinder& inputBinder, const ui::E_KEY_CODE keyCode) -> std::vector @@ -535,6 +421,13 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) ui::E_KEY_CODE::EKC_O }; + CameraPreset initialOrbitPreset; + CameraPreset initialPathPreset; + CameraPreset initialDollyZoomPreset; + bool hasOrbitPreset = false; + bool hasPathPreset = false; + bool hasDollyZoomPreset = false; + for (const auto& cameraRef : cameras) { auto* camera = cameraRef.get(); @@ -548,49 +441,72 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) inputBinder.copyPresetLayoutFrom(*camera); const auto initialPreset = capturePreset(camera, "smoke-initial"); + switch (camera->getKind()) + { + case ICamera::CameraKind::Orbit: + initialOrbitPreset = initialPreset; + hasOrbitPreset = true; + break; + case ICamera::CameraKind::Path: + initialPathPreset = initialPreset; + hasPathPreset = true; + break; + case ICamera::CameraKind::DollyZoom: + initialDollyZoomPreset = initialPreset; + hasDollyZoomPreset = true; + break; + default: + break; + } if (!applyPresetToCamera(camera, initialPreset)) return fail("Preset no-op smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\"."); - if (initialPreset.hasTargetPosition) + if (initialPreset.goal.hasTargetPosition) { CameraPreset shiftedPreset = initialPreset; - shiftedPreset.targetPosition += float64_t3(0.5, -0.25, 0.75); + shiftedPreset.goal.targetPosition += float64_t3(0.5, -0.25, 0.75); const auto shiftedResult = applyPresetToCameraDetailed(camera, shiftedPreset); if (!shiftedResult.succeeded() || !shiftedResult.changed() || !shiftedResult.exact) - return fail("Preset target apply smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\"."); + return fail("Preset target apply smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\". " + describeApplyResult(shiftedResult)); ICamera::SphericalTargetState shiftedState; - if (!camera->tryGetSphericalTargetState(shiftedState) || !nearlyEqual3(shiftedState.target, shiftedPreset.targetPosition, 1e-9)) + if (!camera->tryGetSphericalTargetState(shiftedState) || !nearlyEqual3(shiftedState.target, shiftedPreset.goal.targetPosition, 1e-9)) return fail("Preset target writeback smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\"."); const auto restoredResult = applyPresetToCameraDetailed(camera, initialPreset); if (!restoredResult.succeeded() || !restoredResult.exact) - return fail("Preset restore smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\"."); + return fail("Preset restore smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\". " + describeApplyResult(restoredResult)); ICamera::SphericalTargetState restoredState; - if (!camera->tryGetSphericalTargetState(restoredState) || !nearlyEqual3(restoredState.target, initialPreset.targetPosition, 1e-9)) + if (!camera->tryGetSphericalTargetState(restoredState) || !nearlyEqual3(restoredState.target, initialPreset.goal.targetPosition, 1e-9)) return fail("Preset target restore smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\"."); if (!comparePresetToCamera(camera, initialPreset, 1e-6, 1e-4, 1e-9)) - return fail("Preset restore mismatch smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\"."); + return fail("Preset restore mismatch smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\". " + describePresetMismatch(camera, initialPreset)); } - if (initialPreset.hasDynamicPerspectiveState) + if (initialPreset.goal.hasDynamicPerspectiveState) { CameraPreset shiftedPreset = initialPreset; - shiftedPreset.dynamicBaseFov = std::clamp(initialPreset.dynamicBaseFov + 7.5f, 10.0f, 150.0f); - if (std::abs(static_cast(shiftedPreset.dynamicBaseFov - initialPreset.dynamicBaseFov)) < 1e-6) - shiftedPreset.dynamicBaseFov = std::max(10.0f, initialPreset.dynamicBaseFov - 7.5f); - shiftedPreset.dynamicReferenceDistance = std::max(0.1f, initialPreset.dynamicReferenceDistance + 1.25f); + shiftedPreset.goal.dynamicPerspectiveState.baseFov = + std::clamp(initialPreset.goal.dynamicPerspectiveState.baseFov + 7.5f, 10.0f, 150.0f); + if (std::abs(static_cast( + shiftedPreset.goal.dynamicPerspectiveState.baseFov - initialPreset.goal.dynamicPerspectiveState.baseFov)) < 1e-6) + { + shiftedPreset.goal.dynamicPerspectiveState.baseFov = + std::max(10.0f, initialPreset.goal.dynamicPerspectiveState.baseFov - 7.5f); + } + shiftedPreset.goal.dynamicPerspectiveState.referenceDistance = + std::max(0.1f, initialPreset.goal.dynamicPerspectiveState.referenceDistance + 1.25f); const auto shiftedResult = applyPresetToCameraDetailed(camera, shiftedPreset); if (!shiftedResult.succeeded() || !shiftedResult.changed() || !comparePresetToCamera(camera, shiftedPreset, 1e-6, 0.1, 1e-6)) - return fail("Preset dynamic perspective apply smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\". " + describePresetMismatch(camera, shiftedPreset)); + return fail("Preset dynamic perspective apply smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\". " + describeApplyResult(shiftedResult) + " " + describePresetMismatch(camera, shiftedPreset)); const auto restoredResult = applyPresetToCameraDetailed(camera, initialPreset); if (!restoredResult.succeeded() || !comparePresetToCamera(camera, initialPreset, 1e-6, 0.1, 1e-6)) - return fail("Preset dynamic perspective restore smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\". " + describePresetMismatch(camera, initialPreset)); + return fail("Preset dynamic perspective restore smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\". " + describeApplyResult(restoredResult) + " " + describePresetMismatch(camera, initialPreset)); } const uint32_t allowed = camera->getAllowedVirtualEvents(); @@ -735,6 +651,60 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) << std::endl; } + auto findCameraByKind = [&](const ICamera::CameraKind kind) -> ICamera* + { + for (const auto& cameraRef : cameras) + { + auto* candidate = cameraRef.get(); + if (candidate && candidate->getKind() == kind) + return candidate; + } + return nullptr; + }; + + auto verifyApproximateCrossKindApply = [&](ICamera* targetCamera, const CameraPreset& sourcePreset, + const CCameraGoalSolver::SApplyResult::EIssue expectedIssue, const char* label) -> bool + { + if (!targetCamera) + return true; + + const auto baselinePreset = capturePreset(targetCamera, std::string(label) + "-baseline"); + const auto applyResult = applyPresetToCameraDetailed(targetCamera, sourcePreset); + if (!applyResult.succeeded() || !applyResult.approximate() || !applyResult.hasIssue(expectedIssue)) + return fail(std::string("Cross-kind preset smoke failed for ") + label + ". " + describeApplyResult(applyResult)); + + const auto restoreResult = applyPresetToCameraDetailed(targetCamera, baselinePreset); + if (!restoreResult.succeeded() || !comparePresetToCamera(targetCamera, baselinePreset, 1e-6, 0.1, 1e-6)) + return fail(std::string("Cross-kind preset restore smoke failed for ") + label + ". " + describeApplyResult(restoreResult) + " " + describePresetMismatch(targetCamera, baselinePreset)); + + return true; + }; + + ICamera* orbitCamera = findCameraByKind(ICamera::CameraKind::Orbit); + if (hasOrbitPreset && hasPathPreset && orbitCamera) + { + if (!verifyApproximateCrossKindApply( + orbitCamera, + initialPathPreset, + CCameraGoalSolver::SApplyResult::MissingPathState, + "Path->Orbit")) + { + return false; + } + } + + if (hasOrbitPreset && hasDollyZoomPreset && orbitCamera) + { + if (!verifyApproximateCrossKindApply( + orbitCamera, + initialDollyZoomPreset, + CCameraGoalSolver::SApplyResult::MissingDynamicPerspectiveState, + "DollyZoom->Orbit")) + { + return false; + } + } + m_headlessCameraSmokePassed = true; std::cout << "[headless-camera-smoke] PASS cameras=" << cameras.size() << std::endl; return true; diff --git a/61_UI/include/app/App.hpp b/61_UI/include/app/App.hpp index 3c89357cb..0bcadeb5f 100644 --- a/61_UI/include/app/App.hpp +++ b/61_UI/include/app/App.hpp @@ -484,19 +484,7 @@ class App final : public examples::SimpleWindowedApplication { std::string name; std::string identifier; - float64_t3 position = float64_t3(0.0); - glm::quat orientation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f); - float64_t3 targetPosition = float64_t3(0.0); - bool hasTargetPosition = false; - float distance = 0.f; - bool hasDistance = false; - double orbitU = 0.0; - double orbitV = 0.0; - float orbitDistance = 0.f; - bool hasOrbitState = false; - float dynamicBaseFov = 0.f; - float dynamicReferenceDistance = 0.f; - bool hasDynamicPerspectiveState = false; + CCameraGoal goal = {}; }; struct CameraKeyframe @@ -1044,40 +1032,76 @@ class App final : public examples::SimpleWindowedApplication inline void assignGoalToPreset(CameraPreset& preset, const CCameraGoal& goal) const { - preset.position = goal.position; - preset.orientation = goal.orientation; - preset.targetPosition = goal.targetPosition; - preset.hasTargetPosition = goal.hasTargetPosition; - preset.distance = goal.distance; - preset.hasDistance = goal.hasDistance; - preset.orbitU = goal.orbitU; - preset.orbitV = goal.orbitV; - preset.orbitDistance = goal.orbitDistance; - preset.hasOrbitState = goal.hasOrbitState; - preset.dynamicBaseFov = goal.dynamicPerspectiveState.baseFov; - preset.dynamicReferenceDistance = goal.dynamicPerspectiveState.referenceDistance; - preset.hasDynamicPerspectiveState = goal.hasDynamicPerspectiveState; + preset.goal = canonicalizeGoal(goal); } inline CCameraGoal makeGoalFromPreset(const CameraPreset& preset) const { - CCameraGoal target; - target.position = preset.position; - target.orientation = preset.orientation; - target.hasTargetPosition = preset.hasTargetPosition; - target.targetPosition = preset.targetPosition; - target.hasDistance = preset.hasDistance; - target.distance = preset.distance; - target.hasOrbitState = preset.hasOrbitState; - target.orbitU = preset.orbitU; - target.orbitV = preset.orbitV; - target.orbitDistance = preset.orbitDistance; - target.hasDynamicPerspectiveState = preset.hasDynamicPerspectiveState; - target.dynamicPerspectiveState = { - .baseFov = preset.dynamicBaseFov, - .referenceDistance = preset.dynamicReferenceDistance - }; - return target; + return canonicalizeGoal(preset.goal); + } + + inline double wrapAngleRad(double angle) const + { + while (angle > 3.14159265358979323846) + angle -= 6.28318530717958647692; + while (angle < -3.14159265358979323846) + angle += 6.28318530717958647692; + return angle; + } + + inline double lerpWrappedAngleRad(double a, double b, double alpha) const + { + return a + wrapAngleRad(b - a) * alpha; + } + + inline bool applyCanonicalPathGoal(CCameraGoal& goal) const + { + if (!(goal.hasPathState && goal.hasTargetPosition)) + return false; + if (!std::isfinite(goal.pathState.angle) || !std::isfinite(goal.pathState.radius) || !std::isfinite(goal.pathState.height)) + return false; + + const float64_t3 offset( + std::cos(goal.pathState.angle) * goal.pathState.radius, + goal.pathState.height, + std::sin(goal.pathState.angle) * goal.pathState.radius); + const double distance = length(offset); + if (!std::isfinite(distance) || distance <= 1e-9) + return false; + + const float appliedDistance = std::clamp( + static_cast(distance), + CSphericalTargetCamera::MinDistance, + CSphericalTargetCamera::MaxDistance); + const auto local = offset / static_cast(appliedDistance); + goal.orbitU = std::atan2(local.y, local.x); + goal.orbitV = std::asin(std::clamp(local.z, -1.0, 1.0)); + + const float64_t3 spherePosition( + std::cos(goal.orbitV) * std::cos(goal.orbitU) * static_cast(appliedDistance), + std::cos(goal.orbitV) * std::sin(goal.orbitU) * static_cast(appliedDistance), + std::sin(goal.orbitV) * static_cast(appliedDistance)); + + goal.position = goal.targetPosition + spherePosition; + goal.hasDistance = true; + goal.distance = appliedDistance; + goal.hasOrbitState = true; + goal.orbitDistance = appliedDistance; + + const auto forward = normalize(-spherePosition); + const float64_t3 up = normalize(float64_t3( + -std::sin(goal.orbitV) * std::cos(goal.orbitU), + -std::sin(goal.orbitV) * std::sin(goal.orbitU), + std::cos(goal.orbitV))); + const float64_t3 right = normalize(cross(up, forward)); + goal.orientation = glm::quat_cast(glm::dmat3{ right, up, forward }); + return true; + } + + inline CCameraGoal canonicalizeGoal(CCameraGoal goal) const + { + applyCanonicalPathGoal(goal); + return goal; } inline CCameraGoal blendGoals(const CCameraGoal& a, const CCameraGoal& b, double alpha) const @@ -1085,6 +1109,8 @@ class App final : public examples::SimpleWindowedApplication CCameraGoal blended; blended.position = a.position + (b.position - a.position) * alpha; blended.orientation = glm::slerp(a.orientation, b.orientation, static_cast(alpha)); + blended.sourceKind = (a.sourceKind == b.sourceKind) ? a.sourceKind : ICamera::CameraKind::Unknown; + blended.sourceCapabilities = a.sourceCapabilities & b.sourceCapabilities; blended.hasTargetPosition = a.hasTargetPosition || b.hasTargetPosition; if (blended.hasTargetPosition) { @@ -1122,7 +1148,327 @@ class App final : public examples::SimpleWindowedApplication blended.dynamicPerspectiveState.referenceDistance = dynamicA.referenceDistance + (dynamicB.referenceDistance - dynamicA.referenceDistance) * static_cast(alpha); } - return blended; + blended.hasPathState = a.hasPathState || b.hasPathState; + if (blended.hasPathState) + { + const auto pathA = a.hasPathState ? a.pathState : b.pathState; + const auto pathB = b.hasPathState ? b.pathState : a.pathState; + blended.pathState.angle = lerpWrappedAngleRad(pathA.angle, pathB.angle, alpha); + blended.pathState.radius = pathA.radius + (pathB.radius - pathA.radius) * alpha; + blended.pathState.height = pathA.height + (pathB.height - pathA.height) * alpha; + } + return canonicalizeGoal(blended); + } + + inline bool tryCaptureGoal(ICamera* camera, CCameraGoal& out) const + { + return m_cameraGoalSolver.capture(camera, out); + } + + template + inline bool isFiniteVec3(const Vec& v) const + { + return std::isfinite(v.x) && std::isfinite(v.y) && std::isfinite(v.z); + } + + template + inline bool nearlyEqualVec3(const VecA& a, const VecB& b, const double epsilon) const + { + return std::abs(static_cast(a.x - b.x)) <= epsilon && + std::abs(static_cast(a.y - b.y)) <= epsilon && + std::abs(static_cast(a.z - b.z)) <= epsilon; + } + + inline bool compareGoals(const CCameraGoal& actual, const CCameraGoal& expected, + const double posEps, const double rotEpsDeg, const double scalarEps) const + { + auto isFiniteQuat = [](const glm::quat& q) -> bool + { + return std::isfinite(q.x) && std::isfinite(q.y) && std::isfinite(q.z) && std::isfinite(q.w); + }; + + auto angleDiffRad = [](double a, double b) -> double + { + double d = std::fmod(a - b + 3.14159265358979323846, 6.28318530717958647692); + if (d < 0.0) + d += 6.28318530717958647692; + return std::abs(d - 3.14159265358979323846); + }; + + const auto currentOrientation = glm::normalize(actual.orientation); + const auto expectedOrientation = glm::normalize(expected.orientation); + if (!isFiniteVec3(actual.position) || !isFiniteVec3(expected.position) || !isFiniteQuat(currentOrientation) || !isFiniteQuat(expectedOrientation)) + return false; + + const double dx = static_cast(actual.position.x - expected.position.x); + const double dy = static_cast(actual.position.y - expected.position.y); + const double dz = static_cast(actual.position.z - expected.position.z); + const double posDelta = std::sqrt(dx * dx + dy * dy + dz * dz); + const double orientationDot = std::clamp(static_cast(std::abs(glm::dot(currentOrientation, expectedOrientation))), 0.0, 1.0); + const double rotDeltaDeg = glm::degrees(2.0 * std::acos(orientationDot)); + if (posDelta > posEps || rotDeltaDeg > rotEpsDeg) + return false; + + if (expected.hasTargetPosition) + { + if (!actual.hasTargetPosition || !nearlyEqualVec3(actual.targetPosition, expected.targetPosition, scalarEps)) + return false; + } + if (expected.hasDistance) + { + if (!actual.hasDistance || std::abs(static_cast(actual.distance - expected.distance)) > scalarEps) + return false; + } + if (expected.hasOrbitState) + { + if (!actual.hasOrbitState) + return false; + if (angleDiffRad(expected.orbitU, actual.orbitU) > rotEpsDeg * (3.14159265358979323846 / 180.0)) + return false; + if (angleDiffRad(expected.orbitV, actual.orbitV) > rotEpsDeg * (3.14159265358979323846 / 180.0)) + return false; + if (std::abs(static_cast(actual.orbitDistance - expected.orbitDistance)) > scalarEps) + return false; + } + if (expected.hasPathState) + { + if (!actual.hasPathState) + return false; + if (std::abs(wrapAngleRad(expected.pathState.angle - actual.pathState.angle)) > rotEpsDeg * (3.14159265358979323846 / 180.0)) + return false; + if (std::abs(actual.pathState.radius - expected.pathState.radius) > scalarEps) + return false; + if (std::abs(actual.pathState.height - expected.pathState.height) > scalarEps) + return false; + } + if (expected.hasDynamicPerspectiveState) + { + if (!actual.hasDynamicPerspectiveState) + return false; + if (std::abs(static_cast(actual.dynamicPerspectiveState.baseFov - expected.dynamicPerspectiveState.baseFov)) > scalarEps) + return false; + if (std::abs(static_cast(actual.dynamicPerspectiveState.referenceDistance - expected.dynamicPerspectiveState.referenceDistance)) > scalarEps) + return false; + } + + return true; + } + + inline std::string describeGoalMismatch(const CCameraGoal& actual, const CCameraGoal& expected) const + { + std::ostringstream oss; + const auto currentOrientation = glm::normalize(actual.orientation); + const auto expectedOrientation = glm::normalize(expected.orientation); + const double dx = static_cast(actual.position.x - expected.position.x); + const double dy = static_cast(actual.position.y - expected.position.y); + const double dz = static_cast(actual.position.z - expected.position.z); + const double posDelta = std::sqrt(dx * dx + dy * dy + dz * dz); + const double orientationDot = std::clamp(static_cast(std::abs(glm::dot(currentOrientation, expectedOrientation))), 0.0, 1.0); + const double rotDeltaDeg = glm::degrees(2.0 * std::acos(orientationDot)); + oss << "pos_delta=" << posDelta + << " rot_delta_deg=" << rotDeltaDeg + << " current_pos=(" << actual.position.x << "," << actual.position.y << "," << actual.position.z << ")" + << " expected_pos=(" << expected.position.x << "," << expected.position.y << "," << expected.position.z << ")" + << " current_quat=(" << currentOrientation.x << "," << currentOrientation.y << "," << currentOrientation.z << "," << currentOrientation.w << ")" + << " expected_quat=(" << expectedOrientation.x << "," << expectedOrientation.y << "," << expectedOrientation.z << "," << expectedOrientation.w << ")"; + + if (actual.hasTargetPosition) + { + oss << " target=(" << actual.targetPosition.x << "," << actual.targetPosition.y << "," << actual.targetPosition.z << ")"; + if (actual.hasDistance) + oss << " distance=" << actual.distance; + if (actual.hasOrbitState) + oss << " orbit_u=" << actual.orbitU << " orbit_v=" << actual.orbitV; + } + else if (expected.hasTargetPosition || expected.hasDistance || expected.hasOrbitState) + { + oss << " spherical_state=unavailable"; + } + if (actual.hasPathState) + { + oss << " path_angle=" << actual.pathState.angle + << " path_radius=" << actual.pathState.radius + << " path_height=" << actual.pathState.height; + } + else if (expected.hasPathState) + { + oss << " path_state=unavailable"; + } + + if (actual.hasDynamicPerspectiveState) + { + oss << " dynamic_base_fov=" << actual.dynamicPerspectiveState.baseFov + << " dynamic_reference_distance=" << actual.dynamicPerspectiveState.referenceDistance; + } + else if (expected.hasDynamicPerspectiveState) + { + oss << " dynamic_perspective_state=unavailable"; + } + + return oss.str(); + } + + inline std::string describeApplyResult(const CCameraGoalSolver::SApplyResult& result) const + { + std::ostringstream oss; + oss << "status="; + switch (result.status) + { + case CCameraGoalSolver::SApplyResult::EStatus::Unsupported: oss << "Unsupported"; break; + case CCameraGoalSolver::SApplyResult::EStatus::Failed: oss << "Failed"; break; + case CCameraGoalSolver::SApplyResult::EStatus::AlreadySatisfied: oss << "AlreadySatisfied"; break; + case CCameraGoalSolver::SApplyResult::EStatus::AppliedAbsoluteOnly: oss << "AppliedAbsoluteOnly"; break; + case CCameraGoalSolver::SApplyResult::EStatus::AppliedVirtualEvents: oss << "AppliedVirtualEvents"; break; + case CCameraGoalSolver::SApplyResult::EStatus::AppliedAbsoluteAndVirtualEvents: oss << "AppliedAbsoluteAndVirtualEvents"; break; + } + oss << " exact=" << (result.exact ? "true" : "false") + << " events=" << result.eventCount; + + if (result.issues != CCameraGoalSolver::SApplyResult::NoIssue) + { + oss << " issues="; + bool first = true; + auto appendIssue = [&](const char* label, const CCameraGoalSolver::SApplyResult::EIssue issue) -> void + { + if (!result.hasIssue(issue)) + return; + if (!first) + oss << ","; + oss << label; + first = false; + }; + + appendIssue("absolute_pose_fallback", CCameraGoalSolver::SApplyResult::UsedAbsolutePoseFallback); + appendIssue("missing_spherical_state", CCameraGoalSolver::SApplyResult::MissingSphericalTargetState); + appendIssue("missing_path_state", CCameraGoalSolver::SApplyResult::MissingPathState); + appendIssue("missing_dynamic_perspective_state", CCameraGoalSolver::SApplyResult::MissingDynamicPerspectiveState); + appendIssue("virtual_event_replay_failed", CCameraGoalSolver::SApplyResult::VirtualEventReplayFailed); + } + + return oss.str(); + } + + inline bool comparePresetToCameraState(ICamera* camera, const CameraPreset& preset, + const double posEps, const double rotEpsDeg, const double scalarEps) const + { + if (!camera) + return false; + + CCameraGoal actual; + if (!tryCaptureGoal(camera, actual)) + return false; + + return compareGoals(actual, makeGoalFromPreset(preset), posEps, rotEpsDeg, scalarEps); + } + + inline std::string describePresetCameraMismatch(ICamera* camera, const CameraPreset& preset) const + { + if (!camera) + return "camera=null"; + + CCameraGoal actual; + if (!tryCaptureGoal(camera, actual)) + return "goal_state=unavailable"; + + return describeGoalMismatch(actual, makeGoalFromPreset(preset)); + } + + inline nbl_json serializeGoal(const CCameraGoal& goal) const + { + nbl_json j; + j["position"] = { goal.position.x, goal.position.y, goal.position.z }; + j["orientation"] = { goal.orientation.x, goal.orientation.y, goal.orientation.z, goal.orientation.w }; + j["camera_kind"] = static_cast(goal.sourceKind); + j["camera_capabilities"] = goal.sourceCapabilities; + if (goal.hasTargetPosition) + j["target_position"] = { goal.targetPosition.x, goal.targetPosition.y, goal.targetPosition.z }; + if (goal.hasDistance) + j["distance"] = goal.distance; + if (goal.hasOrbitState) + { + j["orbit_u"] = goal.orbitU; + j["orbit_v"] = goal.orbitV; + j["orbit_distance"] = goal.orbitDistance; + } + if (goal.hasPathState) + { + j["path_angle"] = goal.pathState.angle; + j["path_radius"] = goal.pathState.radius; + j["path_height"] = goal.pathState.height; + } + if (goal.hasDynamicPerspectiveState) + { + j["dynamic_base_fov"] = goal.dynamicPerspectiveState.baseFov; + j["dynamic_reference_distance"] = goal.dynamicPerspectiveState.referenceDistance; + } + return j; + } + + inline void deserializeGoal(const nbl_json& entry, CCameraGoal& goal) const + { + goal = {}; + if (entry.contains("camera_kind")) + goal.sourceKind = static_cast(entry["camera_kind"].get()); + if (entry.contains("camera_capabilities")) + goal.sourceCapabilities = entry["camera_capabilities"].get(); + if (entry.contains("position") && entry["position"].is_array()) + { + auto arr = entry["position"]; + goal.position = float64_t3(arr[0].get(), arr[1].get(), arr[2].get()); + } + if (entry.contains("orientation") && entry["orientation"].is_array()) + { + auto arr = entry["orientation"]; + goal.orientation = glm::quat( + arr[3].get(), + arr[0].get(), + arr[1].get(), + arr[2].get() + ); + } + if (entry.contains("target_position") && entry["target_position"].is_array()) + { + auto arr = entry["target_position"]; + goal.targetPosition = float64_t3(arr[0].get(), arr[1].get(), arr[2].get()); + goal.hasTargetPosition = true; + } + if (entry.contains("distance")) + { + goal.distance = entry["distance"].get(); + goal.hasDistance = true; + } + if (entry.contains("orbit_u")) + { + goal.orbitU = entry["orbit_u"].get(); + goal.hasOrbitState = true; + } + if (entry.contains("orbit_v")) + { + goal.orbitV = entry["orbit_v"].get(); + goal.hasOrbitState = true; + } + if (entry.contains("orbit_distance")) + { + goal.orbitDistance = entry["orbit_distance"].get(); + goal.hasOrbitState = true; + } + if (entry.contains("path_angle") && entry.contains("path_radius") && entry.contains("path_height")) + { + goal.pathState.angle = entry["path_angle"].get(); + goal.pathState.radius = entry["path_radius"].get(); + goal.pathState.height = entry["path_height"].get(); + goal.hasPathState = true; + } + if (entry.contains("dynamic_base_fov")) + { + goal.dynamicPerspectiveState.baseFov = entry["dynamic_base_fov"].get(); + goal.hasDynamicPerspectiveState = true; + } + if (entry.contains("dynamic_reference_distance")) + { + goal.dynamicPerspectiveState.referenceDistance = entry["dynamic_reference_distance"].get(); + goal.hasDynamicPerspectiveState = true; + } } inline CameraPreset capturePreset(ICamera* camera, const std::string& name) @@ -1225,8 +1571,8 @@ class App final : public examples::SimpleWindowedApplication return; CameraPreset preset; - preset.position = pos; - preset.orientation = glm::quat(hlsl::radians(clamped)); + preset.goal.position = pos; + preset.goal.orientation = glm::quat(hlsl::radians(clamped)); applyPresetToCamera(camera, preset); } @@ -1396,26 +1742,9 @@ class App final : public examples::SimpleWindowedApplication for (const auto& preset : m_presets) { - nbl_json j; + nbl_json j = serializeGoal(makeGoalFromPreset(preset)); j["name"] = preset.name; j["identifier"] = preset.identifier; - j["position"] = { preset.position.x, preset.position.y, preset.position.z }; - j["orientation"] = { preset.orientation.x, preset.orientation.y, preset.orientation.z, preset.orientation.w }; - if (preset.hasTargetPosition) - j["target_position"] = { preset.targetPosition.x, preset.targetPosition.y, preset.targetPosition.z }; - if (preset.hasDistance) - j["distance"] = preset.distance; - if (preset.hasOrbitState) - { - j["orbit_u"] = preset.orbitU; - j["orbit_v"] = preset.orbitV; - j["orbit_distance"] = preset.orbitDistance; - } - if (preset.hasDynamicPerspectiveState) - { - j["dynamic_base_fov"] = preset.dynamicBaseFov; - j["dynamic_reference_distance"] = preset.dynamicReferenceDistance; - } root["presets"].push_back(std::move(j)); } @@ -1445,57 +1774,9 @@ class App final : public examples::SimpleWindowedApplication preset.name = entry["name"].get(); if (entry.contains("identifier")) preset.identifier = entry["identifier"].get(); - if (entry.contains("position") && entry["position"].is_array()) - { - auto arr = entry["position"]; - preset.position = float64_t3(arr[0].get(), arr[1].get(), arr[2].get()); - } - if (entry.contains("orientation") && entry["orientation"].is_array()) - { - auto arr = entry["orientation"]; - preset.orientation = glm::quat( - arr[3].get(), - arr[0].get(), - arr[1].get(), - arr[2].get() - ); - } - if (entry.contains("target_position") && entry["target_position"].is_array()) - { - auto arr = entry["target_position"]; - preset.targetPosition = float64_t3(arr[0].get(), arr[1].get(), arr[2].get()); - preset.hasTargetPosition = true; - } - if (entry.contains("distance")) - { - preset.distance = entry["distance"].get(); - preset.hasDistance = true; - } - if (entry.contains("orbit_u")) - { - preset.orbitU = entry["orbit_u"].get(); - preset.hasOrbitState = true; - } - if (entry.contains("orbit_v")) - { - preset.orbitV = entry["orbit_v"].get(); - preset.hasOrbitState = true; - } - if (entry.contains("orbit_distance")) - { - preset.orbitDistance = entry["orbit_distance"].get(); - preset.hasOrbitState = true; - } - if (entry.contains("dynamic_base_fov")) - { - preset.dynamicBaseFov = entry["dynamic_base_fov"].get(); - preset.hasDynamicPerspectiveState = true; - } - if (entry.contains("dynamic_reference_distance")) - { - preset.dynamicReferenceDistance = entry["dynamic_reference_distance"].get(); - preset.hasDynamicPerspectiveState = true; - } + CCameraGoal goal; + deserializeGoal(entry, goal); + assignGoalToPreset(preset, goal); m_presets.emplace_back(std::move(preset)); } diff --git a/61_UI/include/common.hpp b/61_UI/include/common.hpp index ef121fcc8..bb08c9688 100644 --- a/61_UI/include/common.hpp +++ b/61_UI/include/common.hpp @@ -56,9 +56,7 @@ using nbl::hlsl::CDollyCamera; using nbl::hlsl::CDollyZoomCamera; using nbl::hlsl::CPathCamera; using nbl::hlsl::CCameraGoal; -using nbl::hlsl::CTargetPose; using nbl::hlsl::CCameraGoalSolver; -using nbl::hlsl::CTargetPoseController; using nbl::hlsl::CGimbalInputBinder; using nbl::hlsl::IPlanarProjection; using nbl::hlsl::CPlanarProjection; diff --git a/common/include/camera/CCameraGoalSolver.hpp b/common/include/camera/CCameraGoalSolver.hpp index 1454afbf9..85482d347 100644 --- a/common/include/camera/CCameraGoalSolver.hpp +++ b/common/include/camera/CCameraGoalSolver.hpp @@ -19,6 +19,8 @@ struct CCameraGoal { float64_t3 position = float64_t3(0.0); glm::quat orientation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f); + ICamera::CameraKind sourceKind = ICamera::CameraKind::Unknown; + uint32_t sourceCapabilities = ICamera::None; bool hasTargetPosition = false; float64_t3 targetPosition = float64_t3(0.0); bool hasDistance = false; @@ -27,6 +29,8 @@ struct CCameraGoal double orbitU = 0.0; double orbitV = 0.0; float orbitDistance = 0.f; + bool hasPathState = false; + ICamera::PathState pathState = {}; bool hasDynamicPerspectiveState = false; ICamera::DynamicPerspectiveState dynamicPerspectiveState = {}; }; @@ -46,9 +50,20 @@ class CCameraGoalSolver AppliedAbsoluteAndVirtualEvents }; + enum EIssue : uint32_t + { + NoIssue = 0u, + UsedAbsolutePoseFallback = 1u << 0, + MissingSphericalTargetState = 1u << 1, + MissingPathState = 1u << 2, + MissingDynamicPerspectiveState = 1u << 3, + VirtualEventReplayFailed = 1u << 4 + }; + EStatus status = EStatus::Unsupported; bool exact = false; uint32_t eventCount = 0u; + uint32_t issues = NoIssue; inline bool succeeded() const { @@ -61,6 +76,16 @@ class CCameraGoalSolver status == EStatus::AppliedVirtualEvents || status == EStatus::AppliedAbsoluteAndVirtualEvents; } + + inline bool approximate() const + { + return succeeded() && !exact; + } + + inline bool hasIssue(EIssue issue) const + { + return (issues & issue) == issue; + } }; bool buildEvents(ICamera* camera, const CCameraGoal& target, std::vector& out) const @@ -84,6 +109,8 @@ class CCameraGoalSolver const auto& gimbal = camera->getGimbal(); out.position = gimbal.getPosition(); out.orientation = gimbal.getOrientation(); + out.sourceKind = camera->getKind(); + out.sourceCapabilities = camera->getCapabilities(); ICamera::SphericalTargetState sphericalState; if (camera->tryGetSphericalTargetState(sphericalState)) @@ -105,6 +132,13 @@ class CCameraGoalSolver out.dynamicPerspectiveState = dynamicState; } + ICamera::PathState pathState; + if (camera->tryGetPathState(pathState)) + { + out.hasPathState = true; + out.pathState = pathState; + } + return true; } @@ -123,6 +157,7 @@ class CCameraGoalSolver bool poseExact = false; if (tryApplyAbsoluteReferencePose(camera, target, poseChanged, poseExact)) { + result.issues |= SApplyResult::UsedAbsolutePoseFallback; absoluteChanged = absoluteChanged || poseChanged; if (poseExact && !target.hasDynamicPerspectiveState) { @@ -140,6 +175,7 @@ class CCameraGoalSolver ICamera::SphericalTargetState beforeState; if (!camera->tryGetSphericalTargetState(beforeState)) { + result.issues |= SApplyResult::MissingSphericalTargetState; exact = false; } else @@ -147,6 +183,7 @@ class CCameraGoalSolver const auto beforeTarget = beforeState.target; if (!camera->trySetSphericalTarget(target.targetPosition)) { + result.issues |= SApplyResult::MissingSphericalTargetState; exact = false; } else @@ -154,6 +191,7 @@ class CCameraGoalSolver ICamera::SphericalTargetState afterState; if (!camera->tryGetSphericalTargetState(afterState)) { + result.issues |= SApplyResult::MissingSphericalTargetState; exact = false; } else @@ -170,6 +208,7 @@ class CCameraGoalSolver ICamera::SphericalTargetState beforeState; if (!camera->tryGetSphericalTargetState(beforeState)) { + result.issues |= SApplyResult::MissingSphericalTargetState; exact = false; } else @@ -178,6 +217,7 @@ class CCameraGoalSolver const float beforeDistance = beforeState.distance; if (!camera->trySetSphericalDistance(desiredDistance)) { + result.issues |= SApplyResult::MissingSphericalTargetState; exact = false; } else @@ -185,6 +225,7 @@ class CCameraGoalSolver ICamera::SphericalTargetState afterState; if (!camera->tryGetSphericalTargetState(afterState)) { + result.issues |= SApplyResult::MissingSphericalTargetState; exact = false; } else @@ -196,15 +237,53 @@ class CCameraGoalSolver } } + if (target.hasPathState) + { + ICamera::PathState beforeState; + if (!camera->tryGetPathState(beforeState)) + { + result.issues |= SApplyResult::MissingPathState; + exact = false; + } + else if (!camera->trySetPathState(target.pathState)) + { + result.issues |= SApplyResult::MissingPathState; + exact = false; + } + else + { + ICamera::PathState afterState; + if (!camera->tryGetPathState(afterState)) + { + result.issues |= SApplyResult::MissingPathState; + exact = false; + } + else + { + const bool pathChanged = !nearlyEqual(beforeState.angle, afterState.angle) || + !nearlyEqual(beforeState.radius, afterState.radius) || + !nearlyEqual(beforeState.height, afterState.height); + const bool pathExact = nearlyEqual(afterState.angle, target.pathState.angle) && + nearlyEqual(afterState.radius, target.pathState.radius) && + nearlyEqual(afterState.height, target.pathState.height); + + absoluteChanged = absoluteChanged || pathChanged; + exact = exact && pathExact; + } + } + } + if (target.hasDynamicPerspectiveState) { ICamera::DynamicPerspectiveState beforeState; if (!camera->tryGetDynamicPerspectiveState(beforeState)) { + result.issues |= SApplyResult::MissingDynamicPerspectiveState; exact = false; } else if (!camera->trySetDynamicPerspectiveState(target.dynamicPerspectiveState)) { + result.issues |= SApplyResult::MissingDynamicPerspectiveState; exact = false; } else @@ -212,6 +291,7 @@ class CCameraGoalSolver ICamera::DynamicPerspectiveState afterState; if (!camera->tryGetDynamicPerspectiveState(afterState)) { + result.issues |= SApplyResult::MissingDynamicPerspectiveState; exact = false; } else @@ -256,6 +336,7 @@ class CCameraGoalSolver return result; } + result.issues |= SApplyResult::VirtualEventReplayFailed; result.status = SApplyResult::EStatus::Failed; result.exact = false; return result; diff --git a/common/include/camera/CPathCamera.hpp b/common/include/camera/CPathCamera.hpp index 614c13bb1..569bc52f5 100644 --- a/common/include/camera/CPathCamera.hpp +++ b/common/include/camera/CPathCamera.hpp @@ -52,6 +52,25 @@ class CPathCamera final : public CSphericalTargetCamera virtual const uint32_t getAllowedVirtualEvents() override { return AllowedVirtualEvents; } virtual CameraKind getKind() const override { return CameraKind::Path; } + virtual bool tryGetPathState(PathState& out) const override + { + out.angle = m_pathAngle; + out.radius = m_pathRadius; + out.height = m_pathHeight; + return true; + } + virtual bool trySetPathState(const PathState& state) override + { + if (!std::isfinite(state.angle) || !std::isfinite(state.radius) || !std::isfinite(state.height)) + return false; + + m_pathAngle = state.angle; + m_pathRadius = std::max(MinPathRadius, state.radius); + m_pathHeight = state.height; + const bool exact = std::abs(m_pathRadius - state.radius) <= 1e-9; + updateFromPath(); + return exact; + } virtual bool trySetSphericalDistance(float distance) override { const auto clamped = std::clamp(distance, MinDistance, MaxDistance); diff --git a/common/include/camera/ICamera.hpp b/common/include/camera/ICamera.hpp index 692cc30de..cb3db720f 100644 --- a/common/include/camera/ICamera.hpp +++ b/common/include/camera/ICamera.hpp @@ -56,6 +56,13 @@ class ICamera : public IGimbalController, virtual public core::IReferenceCounted float referenceDistance = 0.f; }; + struct PathState + { + double angle = 0.0; + double radius = 0.0; + double height = 0.0; + }; + // Gimbal with view parameters representing a camera in world space class CGimbal : public IGimbal { @@ -138,6 +145,16 @@ class ICamera : public IGimbalController, virtual public core::IReferenceCounted return false; } + virtual bool tryGetPathState(PathState& out) const + { + return false; + } + + virtual bool trySetPathState(const PathState& state) + { + return false; + } + // (***) inline void setMoveSpeedScale(double scalar) { From c97af8979fe0b882853231046ff98404fbd75d1c Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 12:13:05 +0200 Subject: [PATCH 113/205] Surface preset compatibility in camera UI --- 61_UI/AppControlPanel.cpp | 25 ++++++- 61_UI/AppInit.cpp | 34 ++++++++- 61_UI/AppUpdate.cpp | 18 ++--- 61_UI/include/app/App.hpp | 77 ++++++++++++++++++-- common/include/camera/CCameraGoalSolver.hpp | 37 ++++++++++ common/include/camera/CGimbalInputBinder.hpp | 29 +++++++- common/include/camera/CPathCamera.hpp | 1 + common/include/camera/ICamera.hpp | 25 ++++++- 8 files changed, 219 insertions(+), 27 deletions(-) diff --git a/61_UI/AppControlPanel.cpp b/61_UI/AppControlPanel.cpp index d7d7239db..7c2973338 100644 --- a/61_UI/AppControlPanel.cpp +++ b/61_UI/AppControlPanel.cpp @@ -704,8 +704,24 @@ void App::DrawControlPanel() if (selectedPreset >= 0 && static_cast(selectedPreset) < m_presets.size()) { + auto* activeCamera = getActiveCamera(); + const auto& preset = m_presets[static_cast(selectedPreset)]; + const auto compatibility = analyzePresetCompatibility(activeCamera, preset); + const ImVec4 warn = ImVec4(0.95f, 0.72f, 0.28f, 1.0f); + const ImVec4 compatibilityColor = !activeCamera ? bad : (compatibility.exact ? good : warn); + + ImGui::TextDisabled("Preset source"); + ImGui::SameLine(); + ImGui::TextColored(muted, "%s", getCameraTypeLabel(preset.goal.sourceKind).data()); + ImGui::TextDisabled("Goal state"); + ImGui::SameLine(); + ImGui::TextColored(muted, "%s", describeGoalStateMask(preset.goal.sourceGoalStateMask).c_str()); + ImGui::TextDisabled("Apply mode"); + ImGui::SameLine(); + ImGui::TextColored(compatibilityColor, "%s", describePresetCompatibility(activeCamera, preset).c_str()); + if (ImGui::Button("Apply preset")) - applyPresetToCamera(getActiveCamera(), m_presets[static_cast(selectedPreset)]); + applyPresetFromUi(activeCamera, preset); DrawHoverHint("Apply selected preset to the active camera"); ImGui::SameLine(); if (ImGui::Button("Remove preset")) @@ -714,6 +730,13 @@ void App::DrawControlPanel() } } + if (!m_lastPresetApplySummary.empty()) + { + const ImVec4 warn = ImVec4(0.95f, 0.72f, 0.28f, 1.0f); + const ImVec4 resultColor = m_lastPresetApplySucceeded ? (m_lastPresetApplyApproximate ? warn : good) : bad; + ImGui::TextColored(resultColor, "%s", m_lastPresetApplySummary.c_str()); + } + DrawSectionHeader("PresetsStorageHeader", "Storage", accent); ImGui::InputText("Preset file", m_presetPath, IM_ARRAYSIZE(m_presetPath)); if (ImGui::Button("Save presets")) diff --git a/61_UI/AppInit.cpp b/61_UI/AppInit.cpp index ae9822a10..49e313029 100644 --- a/61_UI/AppInit.cpp +++ b/61_UI/AppInit.cpp @@ -438,9 +438,12 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) camera->updateMouseMapping([&](auto& map) { map = camera->getMouseMappingPreset(); }); camera->updateImguizmoMapping([&](auto& map) { map = camera->getImguizmoMappingPreset(); }); CGimbalInputBinder inputBinder; - inputBinder.copyPresetLayoutFrom(*camera); + inputBinder.copyDefaultBindingsFromEncoder(*camera); const auto initialPreset = capturePreset(camera, "smoke-initial"); + const auto initialCompatibility = analyzePresetCompatibility(camera, initialPreset); + if (!initialCompatibility.exact || initialCompatibility.missingGoalStateMask != ICamera::GoalStateNone) + return fail("Preset compatibility smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\". missing=" + describeGoalStateMask(initialCompatibility.missingGoalStateMask)); switch (camera->getKind()) { case ICamera::CameraKind::Orbit: @@ -569,7 +572,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) double keyboardRotDelta = 0.0; for (const auto key : keyboardCandidates) { - inputBinder.copyPresetLayoutFrom(*camera); + inputBinder.copyDefaultBindingsFromEncoder(*camera); auto keyboardEvents = collectKeyboardVirtualEvents(inputBinder, key); if (keyboardEvents.empty()) continue; @@ -611,7 +614,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) if (isOrbitLikeCamera(camera) && hasBlockedMovement) return fail("Orbit mouse movement gate failed for camera \"" + std::string(camera->getIdentifier()) + "\"."); - inputBinder.copyPresetLayoutFrom(*camera); + inputBinder.copyDefaultBindingsFromEncoder(*camera); auto mouseMoveEvents = collectMouseVirtualEvents(inputBinder, { filteredMoveLookDown.data(), filteredMoveLookDown.size() }); if (mouseMoveEvents.empty()) return fail("Mouse move virtual events missing for camera \"" + std::string(camera->getIdentifier()) + "\"."); @@ -631,7 +634,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) const std::array rawScroll = { scrollEv }; auto filteredScroll = filterOrbitMouseEvents(camera, rawScroll, false); - inputBinder.copyPresetLayoutFrom(*camera); + inputBinder.copyDefaultBindingsFromEncoder(*camera); auto mouseScrollEvents = collectMouseVirtualEvents(inputBinder, { filteredScroll.data(), filteredScroll.size() }); if (mouseScrollEvents.empty()) return fail("Mouse scroll virtual events missing for camera \"" + std::string(camera->getIdentifier()) + "\"."); @@ -668,6 +671,29 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) if (!targetCamera) return true; + uint32_t expectedMissingGoalStateMask = ICamera::GoalStateNone; + switch (expectedIssue) + { + case CCameraGoalSolver::SApplyResult::MissingPathState: + expectedMissingGoalStateMask = ICamera::GoalStatePath; + break; + case CCameraGoalSolver::SApplyResult::MissingDynamicPerspectiveState: + expectedMissingGoalStateMask = ICamera::GoalStateDynamicPerspective; + break; + case CCameraGoalSolver::SApplyResult::MissingSphericalTargetState: + expectedMissingGoalStateMask = ICamera::GoalStateSphericalTarget; + break; + default: + break; + } + + const auto compatibility = analyzePresetCompatibility(targetCamera, sourcePreset); + if (compatibility.exact || compatibility.missingGoalStateMask != expectedMissingGoalStateMask) + { + return fail(std::string("Cross-kind preset compatibility smoke failed for ") + label + + ". missing=" + describeGoalStateMask(compatibility.missingGoalStateMask)); + } + const auto baselinePreset = capturePreset(targetCamera, std::string(label) + "-baseline"); const auto applyResult = applyPresetToCameraDetailed(targetCamera, sourcePreset); if (!applyResult.succeeded() || !applyResult.approximate() || !applyResult.hasIssue(expectedIssue)) diff --git a/61_UI/AppUpdate.cpp b/61_UI/AppUpdate.cpp index 3b3cc8f1b..51f88cb98 100644 --- a/61_UI/AppUpdate.cpp +++ b/61_UI/AppUpdate.cpp @@ -325,19 +325,19 @@ void App::update() uint32_t vMouseEventsCount = {}; syncWindowInputBinding(binding); - auto& inputBinding = binding.inputBinding; + auto& inputBinder = binding.inputBinding; - inputBinding.beginInputProcessing(m_nextPresentationTimestamp); + inputBinder.beginInputProcessing(m_nextPresentationTimestamp); { - inputBinding.processKeyboard(nullptr, vKeyboardEventsCount, {}); - inputBinding.processMouse(nullptr, vMouseEventsCount, {}); + inputBinder.processKeyboard(nullptr, vKeyboardEventsCount, {}); + inputBinder.processMouse(nullptr, vMouseEventsCount, {}); const auto totalCount = vKeyboardEventsCount + vMouseEventsCount; if (virtualEvents.size() < totalCount) virtualEvents.resize(totalCount); auto* output = virtualEvents.data(); - inputBinding.processKeyboard(output, vKeyboardEventsCount, { cameraKeyboardEvents.data(), cameraKeyboardEvents.size() }); + inputBinder.processKeyboard(output, vKeyboardEventsCount, { cameraKeyboardEvents.data(), cameraKeyboardEvents.size() }); for (uint32_t i = 0u; i < vKeyboardEventsCount; ++i) output[i].magnitude *= m_cameraControls.keyboardScale; output += vKeyboardEventsCount; @@ -354,16 +354,16 @@ void App::update() continue; filteredOrbitMouseEvents.emplace_back(ev); } - inputBinding.processMouse(output, vMouseEventsCount, { filteredOrbitMouseEvents.data(), filteredOrbitMouseEvents.size() }); + inputBinder.processMouse(output, vMouseEventsCount, { filteredOrbitMouseEvents.data(), filteredOrbitMouseEvents.size() }); } else { - inputBinding.processMouse(output, vMouseEventsCount, { cameraMouseEvents.data(), cameraMouseEvents.size() }); + inputBinder.processMouse(output, vMouseEventsCount, { cameraMouseEvents.data(), cameraMouseEvents.size() }); } vCount = vKeyboardEventsCount + vMouseEventsCount; } - inputBinding.endInputProcessing(); + inputBinder.endInputProcessing(); if (vCount) { @@ -441,7 +441,7 @@ void App::update() static std::vector imguizmoEvents(0x20); uint32_t vCount = 0u; CGimbalInputBinder imguizmoBinding; - imguizmoBinding.copyPresetLayoutFrom(*camera); + imguizmoBinding.copyDefaultBindingsFromEncoder(*camera); imguizmoBinding.beginInputProcessing(m_nextPresentationTimestamp); { diff --git a/61_UI/include/app/App.hpp b/61_UI/include/app/App.hpp index 0bcadeb5f..73bb37985 100644 --- a/61_UI/include/app/App.hpp +++ b/61_UI/include/app/App.hpp @@ -542,12 +542,9 @@ class App final : public examples::SimpleWindowedApplication return camera && camera->hasCapability(ICamera::SphericalTarget); } - inline std::string_view getCameraTypeLabel(const ICamera* camera) const + inline std::string_view getCameraTypeLabel(const ICamera::CameraKind kind) const { - if (!camera) - return "Unknown"; - - switch (camera->getKind()) + switch (kind) { case ICamera::CameraKind::FPS: return "FPS"; case ICamera::CameraKind::Free: return "Free"; @@ -564,6 +561,11 @@ class App final : public examples::SimpleWindowedApplication } } + inline std::string_view getCameraTypeLabel(const ICamera* camera) const + { + return camera ? getCameraTypeLabel(camera->getKind()) : "Unknown"; + } + inline std::string_view getCameraTypeDescription(const ICamera* camera) const { if (!camera) @@ -1111,6 +1113,7 @@ class App final : public examples::SimpleWindowedApplication blended.orientation = glm::slerp(a.orientation, b.orientation, static_cast(alpha)); blended.sourceKind = (a.sourceKind == b.sourceKind) ? a.sourceKind : ICamera::CameraKind::Unknown; blended.sourceCapabilities = a.sourceCapabilities & b.sourceCapabilities; + blended.sourceGoalStateMask = a.sourceGoalStateMask | b.sourceGoalStateMask; blended.hasTargetPosition = a.hasTargetPosition || b.hasTargetPosition; if (blended.hasTargetPosition) { @@ -1165,6 +1168,27 @@ class App final : public examples::SimpleWindowedApplication return m_cameraGoalSolver.capture(camera, out); } + inline std::string describeGoalStateMask(uint32_t mask) const + { + if (mask == ICamera::GoalStateNone) + return "Pose only"; + + std::string out; + auto append = [&](const char* label, const uint32_t bit) -> void + { + if ((mask & bit) != bit) + return; + if (!out.empty()) + out += ", "; + out += label; + }; + + append("Spherical target", ICamera::GoalStateSphericalTarget); + append("Dynamic perspective", ICamera::GoalStateDynamicPerspective); + append("Path", ICamera::GoalStatePath); + return out; + } + template inline bool isFiniteVec3(const Vec& v) const { @@ -1348,6 +1372,30 @@ class App final : public examples::SimpleWindowedApplication return oss.str(); } + inline CCameraGoalSolver::SCompatibilityResult analyzePresetCompatibility(ICamera* camera, const CameraPreset& preset) const + { + return m_cameraGoalSolver.analyzeCompatibility(camera, makeGoalFromPreset(preset)); + } + + inline std::string describePresetCompatibility(ICamera* camera, const CameraPreset& preset) const + { + if (!camera) + return "No active camera"; + + const auto compatibility = analyzePresetCompatibility(camera, preset); + std::ostringstream oss; + oss << (compatibility.exact ? "Exact" : "Best-effort") + << " | source=" << getCameraTypeLabel(preset.goal.sourceKind) + << " | target=" << getCameraTypeLabel(camera); + + if (compatibility.missingGoalStateMask != ICamera::GoalStateNone) + oss << " | missing=" << describeGoalStateMask(compatibility.missingGoalStateMask); + else if (!compatibility.sameKind && preset.goal.sourceKind != ICamera::CameraKind::Unknown) + oss << " | shared goal state only"; + + return oss.str(); + } + inline bool comparePresetToCameraState(ICamera* camera, const CameraPreset& preset, const double posEps, const double rotEpsDeg, const double scalarEps) const { @@ -1380,6 +1428,7 @@ class App final : public examples::SimpleWindowedApplication j["orientation"] = { goal.orientation.x, goal.orientation.y, goal.orientation.z, goal.orientation.w }; j["camera_kind"] = static_cast(goal.sourceKind); j["camera_capabilities"] = goal.sourceCapabilities; + j["camera_goal_state_mask"] = goal.sourceGoalStateMask; if (goal.hasTargetPosition) j["target_position"] = { goal.targetPosition.x, goal.targetPosition.y, goal.targetPosition.z }; if (goal.hasDistance) @@ -1411,6 +1460,8 @@ class App final : public examples::SimpleWindowedApplication goal.sourceKind = static_cast(entry["camera_kind"].get()); if (entry.contains("camera_capabilities")) goal.sourceCapabilities = entry["camera_capabilities"].get(); + if (entry.contains("camera_goal_state_mask")) + goal.sourceGoalStateMask = entry["camera_goal_state_mask"].get(); if (entry.contains("position") && entry["position"].is_array()) { auto arr = entry["position"]; @@ -1494,6 +1545,15 @@ class App final : public examples::SimpleWindowedApplication return m_cameraGoalSolver.applyDetailed(camera, makeGoalFromPreset(preset)); } + inline CCameraGoalSolver::SApplyResult applyPresetFromUi(ICamera* camera, const CameraPreset& preset) + { + const auto result = applyPresetToCameraDetailed(camera, preset); + m_lastPresetApplySummary = describeApplyResult(result) + " | " + describePresetCompatibility(camera, preset); + m_lastPresetApplySucceeded = result.succeeded(); + m_lastPresetApplyApproximate = result.approximate(); + return result; + } + inline bool applyPresetToCamera(ICamera* camera, const CameraPreset& preset) { return applyPresetToCameraDetailed(camera, preset).succeeded(); @@ -2120,7 +2180,7 @@ class App final : public examples::SimpleWindowedApplication if (binding.inputBindingPlanarIx == binding.activePlanarIx && binding.inputBindingProjectionIx == projectionIx) return; - binding.inputBinding.copyBindingLayoutFrom(projections[projectionIx]); + binding.inputBinding.copyActiveBindingsFromEncoder(projections[projectionIx]); binding.inputBindingPlanarIx = binding.activePlanarIx; binding.inputBindingProjectionIx = projectionIx; } @@ -2141,7 +2201,7 @@ class App final : public examples::SimpleWindowedApplication if (projectionIx >= projections.size()) return; - binding.inputBinding.copyBindingLayoutTo(projections[projectionIx]); + binding.inputBinding.copyActiveBindingsToEncoder(projections[projectionIx]); binding.inputBindingPlanarIx = binding.activePlanarIx; binding.inputBindingProjectionIx = projectionIx; } @@ -2318,6 +2378,9 @@ class App final : public examples::SimpleWindowedApplication std::vector m_keyframes; CameraPlaybackState m_playback; CCameraGoalSolver m_cameraGoalSolver; + std::string m_lastPresetApplySummary; + bool m_lastPresetApplySucceeded = false; + bool m_lastPresetApplyApproximate = false; bool m_playbackAffectsAll = false; float m_newKeyframeTime = 0.f; char m_presetName[64] = "Preset"; diff --git a/common/include/camera/CCameraGoalSolver.hpp b/common/include/camera/CCameraGoalSolver.hpp index 85482d347..c5f7b66b0 100644 --- a/common/include/camera/CCameraGoalSolver.hpp +++ b/common/include/camera/CCameraGoalSolver.hpp @@ -21,6 +21,7 @@ struct CCameraGoal glm::quat orientation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f); ICamera::CameraKind sourceKind = ICamera::CameraKind::Unknown; uint32_t sourceCapabilities = ICamera::None; + uint32_t sourceGoalStateMask = ICamera::GoalStateNone; bool hasTargetPosition = false; float64_t3 targetPosition = float64_t3(0.0); bool hasDistance = false; @@ -38,6 +39,15 @@ struct CCameraGoal class CCameraGoalSolver { public: + struct SCompatibilityResult + { + bool sameKind = false; + bool exact = false; + uint32_t requiredGoalStateMask = ICamera::GoalStateNone; + uint32_t supportedGoalStateMask = ICamera::GoalStateNone; + uint32_t missingGoalStateMask = ICamera::GoalStateNone; + }; + struct SApplyResult { enum class EStatus : uint8_t @@ -111,6 +121,7 @@ class CCameraGoalSolver out.orientation = gimbal.getOrientation(); out.sourceKind = camera->getKind(); out.sourceCapabilities = camera->getCapabilities(); + out.sourceGoalStateMask = camera->getGoalStateMask(); ICamera::SphericalTargetState sphericalState; if (camera->tryGetSphericalTargetState(sphericalState)) @@ -142,6 +153,20 @@ class CCameraGoalSolver return true; } + SCompatibilityResult analyzeCompatibility(const ICamera* camera, const CCameraGoal& target) const + { + SCompatibilityResult result; + if (!camera) + return result; + + result.sameKind = target.sourceKind == ICamera::CameraKind::Unknown || target.sourceKind == camera->getKind(); + result.supportedGoalStateMask = camera->getGoalStateMask(); + result.requiredGoalStateMask = getRequiredGoalStateMask(target); + result.missingGoalStateMask = result.requiredGoalStateMask & ~result.supportedGoalStateMask; + result.exact = result.missingGoalStateMask == ICamera::GoalStateNone; + return result; + } + SApplyResult applyDetailed(ICamera* camera, const CCameraGoal& target) const { SApplyResult result; @@ -373,6 +398,18 @@ class CCameraGoalSolver return std::abs(a - b) <= eps; } + inline uint32_t getRequiredGoalStateMask(const CCameraGoal& target) const + { + uint32_t mask = ICamera::GoalStateNone; + if (target.hasTargetPosition || target.hasDistance || target.hasOrbitState) + mask |= ICamera::GoalStateSphericalTarget; + if (target.hasDynamicPerspectiveState) + mask |= ICamera::GoalStateDynamicPerspective; + if (target.hasPathState) + mask |= ICamera::GoalStatePath; + return mask; + } + inline void appendSignedEvent(std::vector& events, double value, CVirtualGimbalEvent::VirtualEventType positive, CVirtualGimbalEvent::VirtualEventType negative) const { diff --git a/common/include/camera/CGimbalInputBinder.hpp b/common/include/camera/CGimbalInputBinder.hpp index d7e0384b3..3580afdf5 100644 --- a/common/include/camera/CGimbalInputBinder.hpp +++ b/common/include/camera/CGimbalInputBinder.hpp @@ -12,34 +12,55 @@ class CGimbalInputBinder final : public IGimbalController using base_t = IGimbalController; using base_t::base_t; - inline void clearBindingLayout() + // Runtime input binder. It translates external keyboard/mouse/ImGuizmo input into virtual events. + inline void clearActiveBindings() { updateKeyboardMapping([](auto& map) { map.clear(); }); updateMouseMapping([](auto& map) { map.clear(); }); updateImguizmoMapping([](auto& map) { map.clear(); }); } - inline void copyBindingLayoutFrom(const IGimbalManipulateEncoder& encoder) + inline void clearBindingLayout() + { + clearActiveBindings(); + } + + inline void copyActiveBindingsFromEncoder(const IGimbalManipulateEncoder& encoder) { updateKeyboardMapping([&](auto& map) { map = sanitizeMapping(encoder.getKeyboardVirtualEventMap()); }); updateMouseMapping([&](auto& map) { map = sanitizeMapping(encoder.getMouseVirtualEventMap()); }); updateImguizmoMapping([&](auto& map) { map = sanitizeMapping(encoder.getImguizmoVirtualEventMap()); }); } - inline void copyPresetLayoutFrom(const IGimbalManipulateEncoder& encoder) + inline void copyBindingLayoutFrom(const IGimbalManipulateEncoder& encoder) + { + copyActiveBindingsFromEncoder(encoder); + } + + inline void copyDefaultBindingsFromEncoder(const IGimbalManipulateEncoder& encoder) { updateKeyboardMapping([&](auto& map) { map = sanitizeMapping(encoder.getKeyboardMappingPreset()); }); updateMouseMapping([&](auto& map) { map = sanitizeMapping(encoder.getMouseMappingPreset()); }); updateImguizmoMapping([&](auto& map) { map = sanitizeMapping(encoder.getImguizmoMappingPreset()); }); } - inline void copyBindingLayoutTo(IGimbalManipulateEncoder& encoder) const + inline void copyPresetLayoutFrom(const IGimbalManipulateEncoder& encoder) + { + copyDefaultBindingsFromEncoder(encoder); + } + + inline void copyActiveBindingsToEncoder(IGimbalManipulateEncoder& encoder) const { encoder.updateKeyboardMapping([&](auto& map) { map = sanitizeMapping(getKeyboardVirtualEventMap()); }); encoder.updateMouseMapping([&](auto& map) { map = sanitizeMapping(getMouseVirtualEventMap()); }); encoder.updateImguizmoMapping([&](auto& map) { map = sanitizeMapping(getImguizmoVirtualEventMap()); }); } + inline void copyBindingLayoutTo(IGimbalManipulateEncoder& encoder) const + { + copyActiveBindingsToEncoder(encoder); + } + private: template inline static Map sanitizeMapping(const Map& source) diff --git a/common/include/camera/CPathCamera.hpp b/common/include/camera/CPathCamera.hpp index 569bc52f5..e08f3f1d3 100644 --- a/common/include/camera/CPathCamera.hpp +++ b/common/include/camera/CPathCamera.hpp @@ -52,6 +52,7 @@ class CPathCamera final : public CSphericalTargetCamera virtual const uint32_t getAllowedVirtualEvents() override { return AllowedVirtualEvents; } virtual CameraKind getKind() const override { return CameraKind::Path; } + virtual uint32_t getGoalStateMask() const override { return base_t::getGoalStateMask() | base_t::GoalStatePath; } virtual bool tryGetPathState(PathState& out) const override { out.angle = m_pathAngle; diff --git a/common/include/camera/ICamera.hpp b/common/include/camera/ICamera.hpp index cb3db720f..bfef5f96b 100644 --- a/common/include/camera/ICamera.hpp +++ b/common/include/camera/ICamera.hpp @@ -40,6 +40,14 @@ class ICamera : public IGimbalController, virtual public core::IReferenceCounted DynamicPerspectiveFov = core::createBitmask({ 1 }) }; + enum GoalStateMask : uint32_t + { + GoalStateNone = 0u, + GoalStateSphericalTarget = core::createBitmask({ 0 }), + GoalStateDynamicPerspective = core::createBitmask({ 1 }), + GoalStatePath = core::createBitmask({ 2 }) + }; + struct SphericalTargetState { float64_t3 target = float64_t3(0.0); @@ -97,8 +105,7 @@ class ICamera : public IGimbalController, virtual public core::IReferenceCounted // Returns a gimbal which *models the camera view* virtual const CGimbal& getGimbal() = 0u; - // Manipulates camera with virtual events, returns true if *any* manipulation happens, it may fail partially or fully because each camera type has certain constraints which determine how it actually works - // TODO: this really needs to be moved to more abstract interface, eg. IObjectTransform or something and ICamera should inherit it (its also an object!) + // Camera core contract: consume virtual events only. Raw input binding and absolute goal solving live outside ICamera. virtual bool manipulate(std::span virtualEvents, const float64_t4x4* referenceFrame = nullptr) = 0; // VirtualEventType bitmask for a camera view gimbal manipulation requests filtering @@ -106,6 +113,15 @@ class ICamera : public IGimbalController, virtual public core::IReferenceCounted virtual CameraKind getKind() const = 0; virtual uint32_t getCapabilities() const { return None; } + virtual uint32_t getGoalStateMask() const + { + uint32_t mask = GoalStateNone; + if (hasCapability(SphericalTarget)) + mask |= GoalStateSphericalTarget; + if (hasCapability(DynamicPerspectiveFov)) + mask |= GoalStateDynamicPerspective; + return mask; + } // Identifier of a camera type virtual const std::string_view getIdentifier() = 0u; @@ -115,6 +131,11 @@ class ICamera : public IGimbalController, virtual public core::IReferenceCounted return (getCapabilities() & capability) == capability; } + inline bool supportsGoalState(GoalStateMask goalState) const + { + return (getGoalStateMask() & goalState) == goalState; + } + virtual bool tryGetSphericalTargetState(SphericalTargetState& out) const { return false; From d2c0855de81053fa57e93be69fefd3937cf76916 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 13:10:41 +0200 Subject: [PATCH 114/205] Refine preset policy filtering in 61_UI --- 61_UI/AppControlPanel.cpp | 97 +++++++++++++++++++++++++++++++++------ 61_UI/include/app/App.hpp | 85 ++++++++++++++++++++++++++++++++++ 2 files changed, 168 insertions(+), 14 deletions(-) diff --git a/61_UI/AppControlPanel.cpp b/61_UI/AppControlPanel.cpp index 7c2973338..8634dc782 100644 --- a/61_UI/AppControlPanel.cpp +++ b/61_UI/AppControlPanel.cpp @@ -685,30 +685,70 @@ void App::DrawControlPanel() { auto* activeCamera = getActiveCamera(); m_presets.emplace_back(capturePreset(activeCamera, m_presetName)); + m_selectedPresetIx = static_cast(m_presets.size()) - 1; } DrawHoverHint("Store current camera as a preset"); ImGui::SameLine(); if (ImGui::Button("Clear presets")) + { m_presets.clear(); + m_selectedPresetIx = -1; + } DrawHoverHint("Remove all presets"); if (!m_presets.empty()) { + auto* activeCamera = getActiveCamera(); + const char* presetFilterLabels[] = { "All", "Exact", "Best-effort" }; + int presetFilterIx = static_cast(m_presetFilterMode); + if (ImGui::Combo("Visibility", &presetFilterIx, presetFilterLabels, IM_ARRAYSIZE(presetFilterLabels))) + m_presetFilterMode = static_cast(presetFilterIx); + DrawHoverHint("Filter presets for the active camera using exact or best-effort compatibility"); + + std::vector filteredPresetIndices; + filteredPresetIndices.reserve(m_presets.size()); + for (size_t i = 0; i < m_presets.size(); ++i) + { + if (presetMatchesFilter(activeCamera, m_presets[i])) + filteredPresetIndices.push_back(static_cast(i)); + } + + if (filteredPresetIndices.empty()) + { + ImGui::TextDisabled("No presets match the current filter."); + } + else + { + if (m_selectedPresetIx < 0 || + std::find(filteredPresetIndices.begin(), filteredPresetIndices.end(), m_selectedPresetIx) == filteredPresetIndices.end()) + { + m_selectedPresetIx = filteredPresetIndices.front(); + } + + int selectedFilteredPresetIx = 0; + for (int i = 0; i < static_cast(filteredPresetIndices.size()); ++i) + { + if (filteredPresetIndices[i] == m_selectedPresetIx) + { + selectedFilteredPresetIx = i; + break; + } + } + std::vector names; - names.reserve(m_presets.size()); - for (const auto& preset : m_presets) - names.push_back(preset.name.c_str()); + names.reserve(filteredPresetIndices.size()); + for (const auto presetIx : filteredPresetIndices) + names.push_back(m_presets[static_cast(presetIx)].name.c_str()); - static int selectedPreset = -1; - ImGui::ListBox("Preset list", &selectedPreset, names.data(), static_cast(names.size()), 6); + if (ImGui::ListBox("Preset list", &selectedFilteredPresetIx, names.data(), static_cast(names.size()), 6)) + m_selectedPresetIx = filteredPresetIndices[static_cast(selectedFilteredPresetIx)]; - if (selectedPreset >= 0 && static_cast(selectedPreset) < m_presets.size()) - { - auto* activeCamera = getActiveCamera(); - const auto& preset = m_presets[static_cast(selectedPreset)]; + if (m_selectedPresetIx >= 0 && static_cast(m_selectedPresetIx) < m_presets.size()) + { + const auto& preset = m_presets[static_cast(m_selectedPresetIx)]; const auto compatibility = analyzePresetCompatibility(activeCamera, preset); - const ImVec4 warn = ImVec4(0.95f, 0.72f, 0.28f, 1.0f); const ImVec4 compatibilityColor = !activeCamera ? bad : (compatibility.exact ? good : warn); + const bool canApplyPreset = canMeaningfullyApplyPreset(activeCamera, preset); ImGui::TextDisabled("Preset source"); ImGui::SameLine(); @@ -716,23 +756,52 @@ void App::DrawControlPanel() ImGui::TextDisabled("Goal state"); ImGui::SameLine(); ImGui::TextColored(muted, "%s", describeGoalStateMask(preset.goal.sourceGoalStateMask).c_str()); - ImGui::TextDisabled("Apply mode"); + ImGui::TextDisabled("Policy"); + ImGui::SameLine(); + ImGui::TextColored(canApplyPreset ? compatibilityColor : bad, "%s", describePresetApplyPolicy(activeCamera, preset).c_str()); + ImGui::TextDisabled("Compatibility"); ImGui::SameLine(); ImGui::TextColored(compatibilityColor, "%s", describePresetCompatibility(activeCamera, preset).c_str()); + DrawBadge(compatibility.exact ? "EXACT" : "BEST-EFFORT", compatibility.exact ? good : warn, badgeText); + if (compatibility.missingGoalStateMask != ICamera::GoalStateNone) + { + ImGui::SameLine(); + DrawBadge("DROPS STATE", warn, badgeText); + } + else if (!compatibility.sameKind && preset.goal.sourceKind != ICamera::CameraKind::Unknown) + { + ImGui::SameLine(); + DrawBadge("SHARED STATE", accent, badgeText); + } + if (!canApplyPreset) + { + ImGui::SameLine(); + DrawBadge("BLOCKED", bad, badgeText); + } + + if (!canApplyPreset) + ImGui::BeginDisabled(); if (ImGui::Button("Apply preset")) applyPresetFromUi(activeCamera, preset); - DrawHoverHint("Apply selected preset to the active camera"); + if (!canApplyPreset) + ImGui::EndDisabled(); + DrawHoverHint(canApplyPreset ? + "Apply selected preset to the active camera" : + "Apply is blocked because there is no active camera or the preset goal is invalid"); ImGui::SameLine(); if (ImGui::Button("Remove preset")) - m_presets.erase(m_presets.begin() + selectedPreset); + { + m_presets.erase(m_presets.begin() + m_selectedPresetIx); + m_selectedPresetIx = -1; + } DrawHoverHint("Remove selected preset"); } } + } if (!m_lastPresetApplySummary.empty()) { - const ImVec4 warn = ImVec4(0.95f, 0.72f, 0.28f, 1.0f); const ImVec4 resultColor = m_lastPresetApplySucceeded ? (m_lastPresetApplyApproximate ? warn : good) : bad; ImGui::TextColored(resultColor, "%s", m_lastPresetApplySummary.c_str()); } diff --git a/61_UI/include/app/App.hpp b/61_UI/include/app/App.hpp index 73bb37985..fbeb89991 100644 --- a/61_UI/include/app/App.hpp +++ b/61_UI/include/app/App.hpp @@ -493,6 +493,13 @@ class App final : public examples::SimpleWindowedApplication float time = 0.f; }; + enum class PresetFilterMode : uint8_t + { + All, + Exact, + BestEffort + }; + struct CameraPlaybackState { bool playing = false; @@ -1396,6 +1403,82 @@ class App final : public examples::SimpleWindowedApplication return oss.str(); } + inline const char* getPresetFilterModeLabel(PresetFilterMode mode) const + { + switch (mode) + { + case PresetFilterMode::All: return "All"; + case PresetFilterMode::Exact: return "Exact"; + case PresetFilterMode::BestEffort: return "Best-effort"; + default: return "All"; + } + } + + inline bool presetMatchesFilter(ICamera* camera, const CameraPreset& preset) const + { + switch (m_presetFilterMode) + { + case PresetFilterMode::All: + return true; + case PresetFilterMode::Exact: + return camera && analyzePresetCompatibility(camera, preset).exact; + case PresetFilterMode::BestEffort: + return camera && !analyzePresetCompatibility(camera, preset).exact; + default: + return true; + } + } + + inline bool isGoalFinite(const CCameraGoal& goal) const + { + auto isFiniteQuat = [](const glm::quat& q) -> bool + { + return std::isfinite(q.x) && std::isfinite(q.y) && std::isfinite(q.z) && std::isfinite(q.w); + }; + + if (!isFiniteVec3(goal.position) || !isFiniteQuat(goal.orientation)) + return false; + if (goal.hasTargetPosition && !isFiniteVec3(goal.targetPosition)) + return false; + if (goal.hasDistance && !std::isfinite(goal.distance)) + return false; + if (goal.hasOrbitState && (!std::isfinite(goal.orbitU) || !std::isfinite(goal.orbitV) || !std::isfinite(goal.orbitDistance))) + return false; + if (goal.hasPathState && (!std::isfinite(goal.pathState.angle) || !std::isfinite(goal.pathState.radius) || !std::isfinite(goal.pathState.height))) + return false; + if (goal.hasDynamicPerspectiveState && + (!std::isfinite(goal.dynamicPerspectiveState.baseFov) || !std::isfinite(goal.dynamicPerspectiveState.referenceDistance))) + return false; + return true; + } + + inline bool canMeaningfullyApplyPreset(ICamera* camera, const CameraPreset& preset) const + { + if (!camera) + return false; + return isGoalFinite(makeGoalFromPreset(preset)); + } + + inline std::string describePresetApplyPolicy(ICamera* camera, const CameraPreset& preset) const + { + if (!camera) + return "Blocked | no active camera"; + + if (!canMeaningfullyApplyPreset(camera, preset)) + return "Blocked | invalid goal state"; + + const auto compatibility = analyzePresetCompatibility(camera, preset); + std::ostringstream oss; + oss << (compatibility.exact ? "Exact apply" : "Best-effort apply"); + if (compatibility.missingGoalStateMask != ICamera::GoalStateNone) + oss << " | drops=" << describeGoalStateMask(compatibility.missingGoalStateMask); + else if (!compatibility.sameKind && preset.goal.sourceKind != ICamera::CameraKind::Unknown) + oss << " | shared goal state only"; + else + oss << " | full preview available"; + return oss.str(); + } + inline bool comparePresetToCameraState(ICamera* camera, const CameraPreset& preset, const double posEps, const double rotEpsDeg, const double scalarEps) const { @@ -2381,6 +2464,8 @@ class App final : public examples::SimpleWindowedApplication std::string m_lastPresetApplySummary; bool m_lastPresetApplySucceeded = false; bool m_lastPresetApplyApproximate = false; + PresetFilterMode m_presetFilterMode = PresetFilterMode::All; + int m_selectedPresetIx = -1; bool m_playbackAffectsAll = false; float m_newKeyframeTime = 0.f; char m_presetName[64] = "Preset"; From 60620af8def682fe5da4f8b4b900295d08695fbe Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 13:25:56 +0200 Subject: [PATCH 115/205] Codify shared spherical preset flow for Chase and Dolly --- 61_UI/AppInit.cpp | 60 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/61_UI/AppInit.cpp b/61_UI/AppInit.cpp index 49e313029..0c33e2494 100644 --- a/61_UI/AppInit.cpp +++ b/61_UI/AppInit.cpp @@ -422,9 +422,13 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) }; CameraPreset initialOrbitPreset; + CameraPreset initialChasePreset; + CameraPreset initialDollyPreset; CameraPreset initialPathPreset; CameraPreset initialDollyZoomPreset; bool hasOrbitPreset = false; + bool hasChasePreset = false; + bool hasDollyPreset = false; bool hasPathPreset = false; bool hasDollyZoomPreset = false; @@ -450,6 +454,14 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) initialOrbitPreset = initialPreset; hasOrbitPreset = true; break; + case ICamera::CameraKind::Chase: + initialChasePreset = initialPreset; + hasChasePreset = true; + break; + case ICamera::CameraKind::Dolly: + initialDollyPreset = initialPreset; + hasDollyPreset = true; + break; case ICamera::CameraKind::Path: initialPathPreset = initialPreset; hasPathPreset = true; @@ -706,7 +718,55 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) return true; }; + auto verifyExactCrossKindApply = [&](ICamera* targetCamera, const CameraPreset& sourcePreset, const char* label) -> bool + { + if (!targetCamera) + return true; + + const auto compatibility = analyzePresetCompatibility(targetCamera, sourcePreset); + if (!compatibility.exact || compatibility.missingGoalStateMask != ICamera::GoalStateNone) + { + return fail(std::string("Exact cross-kind preset compatibility smoke failed for ") + label + + ". missing=" + describeGoalStateMask(compatibility.missingGoalStateMask)); + } + + const auto baselinePreset = capturePreset(targetCamera, std::string(label) + "-baseline"); + const auto applyResult = applyPresetToCameraDetailed(targetCamera, sourcePreset); + if (!applyResult.succeeded() || !applyResult.exact || !comparePresetToCamera(targetCamera, sourcePreset, 1e-6, 0.1, 1e-6)) + { + return fail(std::string("Exact cross-kind preset smoke failed for ") + label + ". " + + describeApplyResult(applyResult) + " " + describePresetMismatch(targetCamera, sourcePreset)); + } + + const auto restoreResult = applyPresetToCameraDetailed(targetCamera, baselinePreset); + if (!restoreResult.succeeded() || !restoreResult.exact || !comparePresetToCamera(targetCamera, baselinePreset, 1e-6, 0.1, 1e-6)) + { + return fail(std::string("Exact cross-kind preset restore smoke failed for ") + label + ". " + + describeApplyResult(restoreResult) + " " + describePresetMismatch(targetCamera, baselinePreset)); + } + + return true; + }; + ICamera* orbitCamera = findCameraByKind(ICamera::CameraKind::Orbit); + ICamera* chaseCamera = findCameraByKind(ICamera::CameraKind::Chase); + ICamera* dollyCamera = findCameraByKind(ICamera::CameraKind::Dolly); + if (hasOrbitPreset && hasChasePreset) + { + if (!verifyExactCrossKindApply(orbitCamera, initialChasePreset, "Chase->Orbit")) + return false; + if (!verifyExactCrossKindApply(chaseCamera, initialOrbitPreset, "Orbit->Chase")) + return false; + } + + if (hasOrbitPreset && hasDollyPreset) + { + if (!verifyExactCrossKindApply(orbitCamera, initialDollyPreset, "Dolly->Orbit")) + return false; + if (!verifyExactCrossKindApply(dollyCamera, initialOrbitPreset, "Orbit->Dolly")) + return false; + } + if (hasOrbitPreset && hasPathPreset && orbitCamera) { if (!verifyApproximateCrossKindApply( From 68d2957d47a3ccf0afc696f0f3094934d8811c99 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 13:35:53 +0200 Subject: [PATCH 116/205] Decouple planar projection bindings from controllers --- 61_UI/AppInit.cpp | 9 +++++---- 61_UI/include/app/App.hpp | 4 ++-- common/include/camera/IPlanarProjection.hpp | 6 +++++- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/61_UI/AppInit.cpp b/61_UI/AppInit.cpp index 0c33e2494..874a6ed4d 100644 --- a/61_UI/AppInit.cpp +++ b/61_UI/AppInit.cpp @@ -1708,6 +1708,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) const auto projectionIx = viewport["projection"].get(); auto& projection = planar->getPlanarProjections().emplace_back(projections[projectionIx]); + auto& projectionBinding = projection.getInputBinding(); const bool hasKeyboardBound = viewport["controllers"].contains("keyboard"); const bool hasMouseBound = viewport["controllers"].contains("mouse"); @@ -1715,18 +1716,18 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) if (hasKeyboardBound) { auto keyboardControllerIx = viewport["controllers"]["keyboard"].get(); - projection.updateKeyboardMapping([&](auto& map) { map = controllers.keyboard[keyboardControllerIx]; }); + projectionBinding.updateKeyboardMapping([&](auto& map) { map = controllers.keyboard[keyboardControllerIx]; }); } else - projection.updateKeyboardMapping([&](auto& map) { map = {}; }); // clean the map if not bound + projectionBinding.updateKeyboardMapping([&](auto& map) { map = {}; }); // clean the map if not bound if (hasMouseBound) { auto mouseControllerIx = viewport["controllers"]["mouse"].get(); - projection.updateMouseMapping([&](auto& map) { map = controllers.mouse[mouseControllerIx]; }); + projectionBinding.updateMouseMapping([&](auto& map) { map = controllers.mouse[mouseControllerIx]; }); } else - projection.updateMouseMapping([&](auto& map) { map = {}; }); // clean the map if not bound + projectionBinding.updateMouseMapping([&](auto& map) { map = {}; }); // clean the map if not bound } { diff --git a/61_UI/include/app/App.hpp b/61_UI/include/app/App.hpp index fbeb89991..ea69a9f45 100644 --- a/61_UI/include/app/App.hpp +++ b/61_UI/include/app/App.hpp @@ -2263,7 +2263,7 @@ class App final : public examples::SimpleWindowedApplication if (binding.inputBindingPlanarIx == binding.activePlanarIx && binding.inputBindingProjectionIx == projectionIx) return; - binding.inputBinding.copyActiveBindingsFromEncoder(projections[projectionIx]); + binding.inputBinding.copyBindingLayoutFrom(projections[projectionIx].getInputBinding()); binding.inputBindingPlanarIx = binding.activePlanarIx; binding.inputBindingProjectionIx = projectionIx; } @@ -2284,7 +2284,7 @@ class App final : public examples::SimpleWindowedApplication if (projectionIx >= projections.size()) return; - binding.inputBinding.copyActiveBindingsToEncoder(projections[projectionIx]); + projections[projectionIx].getInputBinding().copyBindingLayoutFrom(binding.inputBinding); binding.inputBindingPlanarIx = binding.activePlanarIx; binding.inputBindingProjectionIx = projectionIx; } diff --git a/common/include/camera/IPlanarProjection.hpp b/common/include/camera/IPlanarProjection.hpp index eac4e6b76..b8b6a89d6 100644 --- a/common/include/camera/IPlanarProjection.hpp +++ b/common/include/camera/IPlanarProjection.hpp @@ -1,6 +1,7 @@ #ifndef _NBL_I_PLANAR_PROJECTION_HPP_ #define _NBL_I_PLANAR_PROJECTION_HPP_ +#include "CGimbalInputBinder.hpp" #include "ILinearProjection.hpp" namespace nbl::hlsl @@ -9,7 +10,7 @@ namespace nbl::hlsl class IPlanarProjection : public ILinearProjection { public: - struct CProjection : public ILinearProjection::CProjection, public IGimbalController + struct CProjection : public ILinearProjection::CProjection { using base_t = ILinearProjection::CProjection; @@ -104,9 +105,12 @@ class IPlanarProjection : public ILinearProjection } inline const ProjectionParameters& getParameters() const { return m_parameters; } + inline const CGimbalInputBinder& getInputBinding() const { return m_inputBinding; } + inline CGimbalInputBinder& getInputBinding() { return m_inputBinding; } private: CProjection() = default; ProjectionParameters m_parameters; + CGimbalInputBinder m_inputBinding; }; protected: From a0db278ae673a252628710f143a2cba47e2cd219 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 13:40:57 +0200 Subject: [PATCH 117/205] Keep camera input presets outside the controller core --- common/include/camera/ICamera.hpp | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/common/include/camera/ICamera.hpp b/common/include/camera/ICamera.hpp index bfef5f96b..1a5073299 100644 --- a/common/include/camera/ICamera.hpp +++ b/common/include/camera/ICamera.hpp @@ -7,16 +7,14 @@ #include -#include "camera/IGimbalController.hpp" +#include "camera/CGimbalInputBinder.hpp" namespace nbl::hlsl // TODO: DIFFERENT NAMESPACE { -class ICamera : public IGimbalController, virtual public core::IReferenceCounted +class ICamera : public IGimbalManipulateEncoder, virtual public core::IReferenceCounted { public: - using IGimbalController::IGimbalController; - enum class CameraKind : uint8_t { Unknown, @@ -102,6 +100,14 @@ class ICamera : public IGimbalController, virtual public core::IReferenceCounted ICamera() {} virtual ~ICamera() = default; + virtual const keyboard_to_virtual_events_t& getKeyboardVirtualEventMap() const override { return m_defaultInputBinding.getKeyboardVirtualEventMap(); } + virtual const mouse_to_virtual_events_t& getMouseVirtualEventMap() const override { return m_defaultInputBinding.getMouseVirtualEventMap(); } + virtual const imguizmo_to_virtual_events_t& getImguizmoVirtualEventMap() const override { return m_defaultInputBinding.getImguizmoVirtualEventMap(); } + + virtual void updateKeyboardMapping(const std::function& mapKeys) override { m_defaultInputBinding.updateKeyboardMapping(mapKeys); } + virtual void updateMouseMapping(const std::function& mapKeys) override { m_defaultInputBinding.updateMouseMapping(mapKeys); } + virtual void updateImguizmoMapping(const std::function& mapKeys) override { m_defaultInputBinding.updateImguizmoMapping(mapKeys); } + // Returns a gimbal which *models the camera view* virtual const CGimbal& getGimbal() = 0u; @@ -196,6 +202,7 @@ class ICamera : public IGimbalController, virtual public core::IReferenceCounted // (***) TODO: I need to think whether a camera should own this or controllers should be able // to set sensitivity to scale magnitudes of generated events we put into manipulate method double m_moveSpeedScale = 0.01, m_rotationSpeedScale = 0.003; + CGimbalInputBinder m_defaultInputBinding; }; } From 254edd15422bfe05d650fb829f08e0ae1c4a92d6 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 13:44:41 +0200 Subject: [PATCH 118/205] Add collected virtual event helpers to input binding --- 61_UI/AppInit.cpp | 41 +---------- 61_UI/AppUpdate.cpp | 77 +++++++------------- common/include/camera/CGimbalInputBinder.hpp | 58 +++++++++++++++ 3 files changed, 88 insertions(+), 88 deletions(-) diff --git a/61_UI/AppInit.cpp b/61_UI/AppInit.cpp index 874a6ed4d..47bd7ac4b 100644 --- a/61_UI/AppInit.cpp +++ b/61_UI/AppInit.cpp @@ -331,7 +331,6 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) auto collectKeyboardVirtualEvents = [&](CGimbalInputBinder& inputBinder, const ui::E_KEY_CODE keyCode) -> std::vector { - std::vector out; static std::chrono::microseconds smokeTimestamp = std::chrono::microseconds::zero(); smokeTimestamp += std::chrono::microseconds(16667); const auto pressTs = smokeTimestamp; @@ -341,53 +340,19 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) pressEvent.action = SKeyboardEvent::ECA_PRESSED; pressEvent.window = nullptr; - uint32_t potentialCount = 0u; - uint32_t generatedCount = 0u; - inputBinder.beginInputProcessing(pressTs); - inputBinder.processKeyboard(nullptr, potentialCount, {}); - if (potentialCount) - { - std::vector warmup(potentialCount); - generatedCount = potentialCount; - inputBinder.processKeyboard(warmup.data(), generatedCount, { &pressEvent, 1u }); - } - inputBinder.endInputProcessing(); + inputBinder.collectVirtualEvents(pressTs, { .keyboardEvents = { &pressEvent, 1u } }); smokeTimestamp += std::chrono::microseconds(16667); const auto sampleTs = smokeTimestamp; - inputBinder.beginInputProcessing(sampleTs); - inputBinder.processKeyboard(nullptr, potentialCount, {}); - out.resize(potentialCount); - if (potentialCount) - { - generatedCount = potentialCount; - inputBinder.processKeyboard(out.data(), generatedCount, {}); - } - inputBinder.endInputProcessing(); - out.resize(generatedCount); - return out; + return inputBinder.collectVirtualEvents(sampleTs).events; }; auto collectMouseVirtualEvents = [&](CGimbalInputBinder& inputBinder, std::span mouseEvents) -> std::vector { - std::vector out; static std::chrono::microseconds smokeTimestamp = std::chrono::microseconds::zero(); smokeTimestamp += std::chrono::microseconds(16667); const auto ts = smokeTimestamp; - - uint32_t potentialCount = 0u; - uint32_t generatedCount = 0u; - inputBinder.beginInputProcessing(ts); - inputBinder.processMouse(nullptr, potentialCount, {}); - out.resize(potentialCount); - if (potentialCount) - { - generatedCount = potentialCount; - inputBinder.processMouse(out.data(), generatedCount, mouseEvents); - } - inputBinder.endInputProcessing(); - out.resize(generatedCount); - return out; + return inputBinder.collectVirtualEvents(ts, { .mouseEvents = mouseEvents }).events; }; auto filterOrbitMouseEvents = [&](ICamera* camera, std::span input, bool orbitLookDown) -> std::vector diff --git a/61_UI/AppUpdate.cpp b/61_UI/AppUpdate.cpp index 51f88cb98..a4a727da8 100644 --- a/61_UI/AppUpdate.cpp +++ b/61_UI/AppUpdate.cpp @@ -319,54 +319,38 @@ void App::update() assert(binding.boundProjectionIx.has_value()); auto& projection = planar->getPlanarProjections()[binding.boundProjectionIx.value()]; - static std::vector virtualEvents(0x45); - uint32_t vCount = {}; - uint32_t vKeyboardEventsCount = {}; - uint32_t vMouseEventsCount = {}; - syncWindowInputBinding(binding); auto& inputBinder = binding.inputBinding; - inputBinder.beginInputProcessing(m_nextPresentationTimestamp); + std::vector filteredOrbitMouseEvents; + std::span mouseInput = { cameraMouseEvents.data(), cameraMouseEvents.size() }; + if (isOrbitLikeCamera(camera)) { - inputBinder.processKeyboard(nullptr, vKeyboardEventsCount, {}); - inputBinder.processMouse(nullptr, vMouseEventsCount, {}); - - const auto totalCount = vKeyboardEventsCount + vMouseEventsCount; - if (virtualEvents.size() < totalCount) - virtualEvents.resize(totalCount); - - auto* output = virtualEvents.data(); - inputBinder.processKeyboard(output, vKeyboardEventsCount, { cameraKeyboardEvents.data(), cameraKeyboardEvents.size() }); - for (uint32_t i = 0u; i < vKeyboardEventsCount; ++i) - output[i].magnitude *= m_cameraControls.keyboardScale; - output += vKeyboardEventsCount; - - if (isOrbitLikeCamera(camera)) + const bool orbitLookDown = ImGui::IsMouseDown(ImGuiMouseButton_Right) || + (m_scriptedInput.enabled && (m_scriptedInput.scriptedLeftMouseDown || m_scriptedInput.scriptedRightMouseDown)); + filteredOrbitMouseEvents.reserve(cameraMouseEvents.size()); + for (const auto& ev : cameraMouseEvents) { - const bool orbitLookDown = ImGui::IsMouseDown(ImGuiMouseButton_Right) || - (m_scriptedInput.enabled && (m_scriptedInput.scriptedLeftMouseDown || m_scriptedInput.scriptedRightMouseDown)); - std::vector filteredOrbitMouseEvents; - filteredOrbitMouseEvents.reserve(cameraMouseEvents.size()); - for (const auto& ev : cameraMouseEvents) - { - if (ev.type == ui::SMouseEvent::EET_MOVEMENT && !orbitLookDown) - continue; - filteredOrbitMouseEvents.emplace_back(ev); - } - inputBinder.processMouse(output, vMouseEventsCount, { filteredOrbitMouseEvents.data(), filteredOrbitMouseEvents.size() }); + if (ev.type == ui::SMouseEvent::EET_MOVEMENT && !orbitLookDown) + continue; + filteredOrbitMouseEvents.emplace_back(ev); } - else - { - inputBinder.processMouse(output, vMouseEventsCount, { cameraMouseEvents.data(), cameraMouseEvents.size() }); - } - - vCount = vKeyboardEventsCount + vMouseEventsCount; + mouseInput = { filteredOrbitMouseEvents.data(), filteredOrbitMouseEvents.size() }; } - inputBinder.endInputProcessing(); + + auto collectedEvents = inputBinder.collectVirtualEvents(m_nextPresentationTimestamp, { + .keyboardEvents = { cameraKeyboardEvents.data(), cameraKeyboardEvents.size() }, + .mouseEvents = mouseInput + }); + auto& virtualEvents = collectedEvents.events; + const uint32_t vCount = collectedEvents.totalCount(); + const uint32_t vKeyboardEventsCount = collectedEvents.keyboardCount; if (vCount) { + for (uint32_t i = 0u; i < vKeyboardEventsCount; ++i) + virtualEvents[i].magnitude *= m_cameraControls.keyboardScale; + applyVirtualEventScaling(virtualEvents, vCount); const char* controllerLabel = "Keyboard/Mouse"; @@ -438,20 +422,13 @@ void App::update() auto& planar = m_planarProjections[binding.activePlanarIx]; auto* camera = planar->getCamera(); - static std::vector imguizmoEvents(0x20); - uint32_t vCount = 0u; CGimbalInputBinder imguizmoBinding; imguizmoBinding.copyDefaultBindingsFromEncoder(*camera); - - imguizmoBinding.beginInputProcessing(m_nextPresentationTimestamp); - { - imguizmoBinding.processImguizmo(nullptr, vCount, {}); - if (imguizmoEvents.size() < vCount) - imguizmoEvents.resize(vCount); - - imguizmoBinding.processImguizmo(imguizmoEvents.data(), vCount, { scriptedImguizmo.data(), scriptedImguizmo.size() }); - } - imguizmoBinding.endInputProcessing(); + auto collectedEvents = imguizmoBinding.collectVirtualEvents(m_nextPresentationTimestamp, { + .imguizmoEvents = { scriptedImguizmo.data(), scriptedImguizmo.size() } + }); + auto& imguizmoEvents = collectedEvents.events; + const uint32_t vCount = collectedEvents.imguizmoCount; if (vCount) { diff --git a/common/include/camera/CGimbalInputBinder.hpp b/common/include/camera/CGimbalInputBinder.hpp index 3580afdf5..3b243d970 100644 --- a/common/include/camera/CGimbalInputBinder.hpp +++ b/common/include/camera/CGimbalInputBinder.hpp @@ -1,6 +1,8 @@ #ifndef _NBL_C_GIMBAL_INPUT_BINDER_HPP_ #define _NBL_C_GIMBAL_INPUT_BINDER_HPP_ +#include + #include "IGimbalController.hpp" namespace nbl::hlsl @@ -12,6 +14,19 @@ class CGimbalInputBinder final : public IGimbalController using base_t = IGimbalController; using base_t::base_t; + struct SCollectedVirtualEvents + { + std::vector events; + uint32_t keyboardCount = 0u; + uint32_t mouseCount = 0u; + uint32_t imguizmoCount = 0u; + + inline uint32_t totalCount() const + { + return keyboardCount + mouseCount + imguizmoCount; + } + }; + // Runtime input binder. It translates external keyboard/mouse/ImGuizmo input into virtual events. inline void clearActiveBindings() { @@ -61,6 +76,49 @@ class CGimbalInputBinder final : public IGimbalController copyActiveBindingsToEncoder(encoder); } + inline SCollectedVirtualEvents collectVirtualEvents( + const std::chrono::microseconds nextPresentationTimeStamp, + const SUpdateParameters parameters = {}) + { + beginInputProcessing(nextPresentationTimeStamp); + + SCollectedVirtualEvents output; + uint32_t keyboardPotentialCount = 0u; + uint32_t mousePotentialCount = 0u; + uint32_t imguizmoPotentialCount = 0u; + + processKeyboard(nullptr, keyboardPotentialCount, {}); + processMouse(nullptr, mousePotentialCount, {}); + processImguizmo(nullptr, imguizmoPotentialCount, {}); + + output.events.resize(keyboardPotentialCount + mousePotentialCount + imguizmoPotentialCount); + auto* dst = output.events.data(); + + if (keyboardPotentialCount) + { + output.keyboardCount = keyboardPotentialCount; + processKeyboard(dst, output.keyboardCount, parameters.keyboardEvents); + dst += output.keyboardCount; + } + + if (mousePotentialCount) + { + output.mouseCount = mousePotentialCount; + processMouse(dst, output.mouseCount, parameters.mouseEvents); + dst += output.mouseCount; + } + + if (imguizmoPotentialCount) + { + output.imguizmoCount = imguizmoPotentialCount; + processImguizmo(dst, output.imguizmoCount, parameters.imguizmoEvents); + } + + endInputProcessing(); + output.events.resize(output.totalCount()); + return output; + } + private: template inline static Map sanitizeMapping(const Map& source) From 62af3909a8f7bd6f0fe4a83e2f2d9f53f3ae57a0 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 13:47:23 +0200 Subject: [PATCH 119/205] Use input binder types in transform editor --- 61_UI/AppTransformEditor.cpp | 6 +++--- 61_UI/include/common.hpp | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/61_UI/AppTransformEditor.cpp b/61_UI/AppTransformEditor.cpp index 09827bbb6..b4370efc7 100644 --- a/61_UI/AppTransformEditor.cpp +++ b/61_UI/AppTransformEditor.cpp @@ -122,8 +122,8 @@ void App::TransformEditorContents() mCurrentGizmoOperation = ImGuizmo::SCALE; float32_t3 matrixTranslation, matrixRotation, matrixScale; - IGimbalController::input_imguizmo_event_t decomposed, recomposed; - imguizmoModel.outDeltaTRS = IGimbalController::input_imguizmo_event_t(1); + CGimbalInputBinder::input_imguizmo_event_t decomposed, recomposed; + imguizmoModel.outDeltaTRS = CGimbalInputBinder::input_imguizmo_event_t(1); ImGuizmo::DecomposeMatrixToComponents(m16TRSmatrix, &matrixTranslation[0], &matrixRotation[0], &matrixScale[0]); decomposed = *reinterpret_cast(m16TRSmatrix); @@ -191,7 +191,7 @@ void App::TransformEditorContents() if (virtualEvents.size() < vCount) virtualEvents.resize(vCount); - IGimbalController::SUpdateParameters params; + CGimbalInputBinder::SUpdateParameters params; params.imguizmoEvents = { { imguizmoModel.outDeltaTRS } }; boundCameraToManipulate->process(virtualEvents.data(), vCount, params); } diff --git a/61_UI/include/common.hpp b/61_UI/include/common.hpp index bb08c9688..97b16fe13 100644 --- a/61_UI/include/common.hpp +++ b/61_UI/include/common.hpp @@ -60,7 +60,6 @@ using nbl::hlsl::CCameraGoalSolver; using nbl::hlsl::CGimbalInputBinder; using nbl::hlsl::IPlanarProjection; using nbl::hlsl::CPlanarProjection; -using nbl::hlsl::IGimbalController; using nbl::hlsl::IGimbalManipulateEncoder; using nbl::hlsl::CVirtualGimbalEvent; using nbl::hlsl::float32_t; From 7e9fa6aa646eeb1bc6e26db76d771999ea7696da Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 13:52:24 +0200 Subject: [PATCH 120/205] Rename camera binding layout contracts --- 61_UI/AppInit.cpp | 12 ++--- 61_UI/AppUpdate.cpp | 2 +- 61_UI/include/common.hpp | 2 +- 61_UI/include/keysmapping.hpp | 4 +- 61_UI/src/keysmapping.cpp | 36 +++++++------- common/include/camera/CGimbalInputBinder.hpp | 51 +++++++++++++------- common/include/camera/ICamera.hpp | 2 +- common/include/camera/IGimbalController.hpp | 12 +++-- 8 files changed, 69 insertions(+), 52 deletions(-) diff --git a/61_UI/AppInit.cpp b/61_UI/AppInit.cpp index 47bd7ac4b..2871ce65b 100644 --- a/61_UI/AppInit.cpp +++ b/61_UI/AppInit.cpp @@ -407,7 +407,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) camera->updateMouseMapping([&](auto& map) { map = camera->getMouseMappingPreset(); }); camera->updateImguizmoMapping([&](auto& map) { map = camera->getImguizmoMappingPreset(); }); CGimbalInputBinder inputBinder; - inputBinder.copyDefaultBindingsFromEncoder(*camera); + inputBinder.copyDefaultBindingsFromLayout(*camera); const auto initialPreset = capturePreset(camera, "smoke-initial"); const auto initialCompatibility = analyzePresetCompatibility(camera, initialPreset); @@ -549,7 +549,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) double keyboardRotDelta = 0.0; for (const auto key : keyboardCandidates) { - inputBinder.copyDefaultBindingsFromEncoder(*camera); + inputBinder.copyDefaultBindingsFromLayout(*camera); auto keyboardEvents = collectKeyboardVirtualEvents(inputBinder, key); if (keyboardEvents.empty()) continue; @@ -591,7 +591,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) if (isOrbitLikeCamera(camera) && hasBlockedMovement) return fail("Orbit mouse movement gate failed for camera \"" + std::string(camera->getIdentifier()) + "\"."); - inputBinder.copyDefaultBindingsFromEncoder(*camera); + inputBinder.copyDefaultBindingsFromLayout(*camera); auto mouseMoveEvents = collectMouseVirtualEvents(inputBinder, { filteredMoveLookDown.data(), filteredMoveLookDown.size() }); if (mouseMoveEvents.empty()) return fail("Mouse move virtual events missing for camera \"" + std::string(camera->getIdentifier()) + "\"."); @@ -611,7 +611,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) const std::array rawScroll = { scrollEv }; auto filteredScroll = filterOrbitMouseEvents(camera, rawScroll, false); - inputBinder.copyDefaultBindingsFromEncoder(*camera); + inputBinder.copyDefaultBindingsFromLayout(*camera); auto mouseScrollEvents = collectMouseVirtualEvents(inputBinder, { filteredScroll.data(), filteredScroll.size() }); if (mouseScrollEvents.empty()) return fail("Mouse scroll virtual events missing for camera \"" + std::string(camera->getIdentifier()) + "\"."); @@ -1562,8 +1562,8 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) struct { - std::vector keyboard; - std::vector mouse; + std::vector keyboard; + std::vector mouse; } controllers; if (j.contains("controllers")) diff --git a/61_UI/AppUpdate.cpp b/61_UI/AppUpdate.cpp index a4a727da8..2cca79360 100644 --- a/61_UI/AppUpdate.cpp +++ b/61_UI/AppUpdate.cpp @@ -423,7 +423,7 @@ void App::update() auto* camera = planar->getCamera(); CGimbalInputBinder imguizmoBinding; - imguizmoBinding.copyDefaultBindingsFromEncoder(*camera); + imguizmoBinding.copyDefaultBindingsFromLayout(*camera); auto collectedEvents = imguizmoBinding.collectVirtualEvents(m_nextPresentationTimestamp, { .imguizmoEvents = { scriptedImguizmo.data(), scriptedImguizmo.size() } }); diff --git a/61_UI/include/common.hpp b/61_UI/include/common.hpp index 97b16fe13..b6fa40602 100644 --- a/61_UI/include/common.hpp +++ b/61_UI/include/common.hpp @@ -58,9 +58,9 @@ using nbl::hlsl::CPathCamera; using nbl::hlsl::CCameraGoal; using nbl::hlsl::CCameraGoalSolver; using nbl::hlsl::CGimbalInputBinder; +using nbl::hlsl::IGimbalBindingLayout; using nbl::hlsl::IPlanarProjection; using nbl::hlsl::CPlanarProjection; -using nbl::hlsl::IGimbalManipulateEncoder; using nbl::hlsl::CVirtualGimbalEvent; using nbl::hlsl::float32_t; using nbl::hlsl::float32_t2; diff --git a/61_UI/include/keysmapping.hpp b/61_UI/include/keysmapping.hpp index ef37bfd89..109784aa4 100644 --- a/61_UI/include/keysmapping.hpp +++ b/61_UI/include/keysmapping.hpp @@ -3,7 +3,7 @@ #include "common.hpp" -bool handleAddMapping(const char* tableID, IGimbalManipulateEncoder* encoder, IGimbalManipulateEncoder::EncoderType activeController, CVirtualGimbalEvent::VirtualEventType& selectedEventType, ui::E_KEY_CODE& newKey, ui::E_MOUSE_CODE& newMouseCode, bool& addMode); -bool displayKeyMappingsAndVirtualStatesInline(IGimbalManipulateEncoder* encoder, bool spawnWindow = false); +bool handleAddMapping(const char* tableID, IGimbalBindingLayout* layout, IGimbalBindingLayout::EncoderType activeController, 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/src/keysmapping.cpp b/61_UI/src/keysmapping.cpp index 1cd112c88..563a3a5ad 100644 --- a/61_UI/src/keysmapping.cpp +++ b/61_UI/src/keysmapping.cpp @@ -2,7 +2,7 @@ #include #include -bool handleAddMapping(const char* tableID, IGimbalManipulateEncoder* encoder, IGimbalManipulateEncoder::EncoderType activeController, CVirtualGimbalEvent::VirtualEventType& selectedEventType, ui::E_KEY_CODE& newKey, ui::E_MOUSE_CODE& newMouseCode, bool& addMode) +bool handleAddMapping(const char* tableID, IGimbalBindingLayout* layout, IGimbalBindingLayout::EncoderType activeController, 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); @@ -28,7 +28,7 @@ bool handleAddMapping(const char* tableID, IGimbalManipulateEncoder* encoder, IG } ImGui::TableSetColumnIndex(1); - if (activeController == IGimbalManipulateEncoder::Keyboard) + if (activeController == IGimbalBindingLayout::Keyboard) { char newKeyDisplay[2] = { ui::keyCodeToChar(newKey, true), '\0' }; if (ImGui::BeginCombo("##selectKey", newKeyDisplay)) @@ -65,10 +65,10 @@ bool handleAddMapping(const char* tableID, IGimbalManipulateEncoder* encoder, IG if (ImGui::Button("Confirm Add", ImVec2(100, 30))) { anyMapUpdated |= true; - if (activeController == IGimbalManipulateEncoder::Keyboard) - encoder->updateKeyboardMapping([&](auto& keys) { keys[newKey] = selectedEventType; }); + if (activeController == IGimbalBindingLayout::Keyboard) + layout->updateKeyboardMapping([&](auto& keys) { keys[newKey] = selectedEventType; }); else - encoder->updateMouseMapping([&](auto& mouse) { mouse[newMouseCode] = selectedEventType; }); + layout->updateMouseMapping([&](auto& mouse) { mouse[newMouseCode] = selectedEventType; }); addMode = false; } @@ -77,11 +77,11 @@ bool handleAddMapping(const char* tableID, IGimbalManipulateEncoder* encoder, IG return anyMapUpdated; } -bool displayKeyMappingsAndVirtualStatesInline(IGimbalManipulateEncoder* encoder, bool spawnWindow) +bool displayKeyMappingsAndVirtualStatesInline(IGimbalBindingLayout* layout, bool spawnWindow) { bool anyMapUpdated = false; - if (!encoder) return anyMapUpdated; + if (!layout) return anyMapUpdated; struct MappingState { @@ -89,14 +89,14 @@ bool displayKeyMappingsAndVirtualStatesInline(IGimbalManipulateEncoder* encoder, 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; - IGimbalManipulateEncoder::EncoderType activeController = IGimbalManipulateEncoder::Keyboard; + IGimbalBindingLayout::EncoderType activeController = IGimbalBindingLayout::Keyboard; }; - static std::unordered_map cameraStates; - auto& state = cameraStates[encoder]; + static std::unordered_map cameraStates; + auto& state = cameraStates[layout]; - const auto& keyboardMappings = encoder->getKeyboardVirtualEventMap(); - const auto& mouseMappings = encoder->getMouseVirtualEventMap(); + const auto& keyboardMappings = layout->getKeyboardVirtualEventMap(); + const auto& mouseMappings = layout->getMouseVirtualEventMap(); if (spawnWindow) { @@ -108,7 +108,7 @@ bool displayKeyMappingsAndVirtualStatesInline(IGimbalManipulateEncoder* encoder, { if (ImGui::BeginTabItem("Keyboard")) { - state.activeController = IGimbalManipulateEncoder::Keyboard; + state.activeController = IGimbalBindingLayout::Keyboard; ImGui::Separator(); if (ImGui::Button("Add Key", ImVec2(100, 30))) @@ -149,7 +149,7 @@ bool displayKeyMappingsAndVirtualStatesInline(IGimbalManipulateEncoder* encoder, if (ImGui::Button(("Delete##deleteKey" + std::to_string(static_cast(keyboardCode))).c_str())) { anyMapUpdated |= true; - encoder->updateKeyboardMapping([keyboardCode](auto& keys) { keys.erase(keyboardCode); }); + layout->updateKeyboardMapping([keyboardCode](auto& keys) { keys.erase(keyboardCode); }); break; } } @@ -158,7 +158,7 @@ bool displayKeyMappingsAndVirtualStatesInline(IGimbalManipulateEncoder* encoder, if (state.addMode) { ImGui::Separator(); - anyMapUpdated |= handleAddMapping("AddKeyboardMappingTable", encoder, state.activeController, state.selectedEventType, state.newKey, state.newMouseCode, state.addMode); + anyMapUpdated |= handleAddMapping("AddKeyboardMappingTable", layout, state.activeController, state.selectedEventType, state.newKey, state.newMouseCode, state.addMode); } ImGui::EndTabItem(); @@ -166,7 +166,7 @@ bool displayKeyMappingsAndVirtualStatesInline(IGimbalManipulateEncoder* encoder, if (ImGui::BeginTabItem("Mouse")) { - state.activeController = IGimbalManipulateEncoder::Mouse; + state.activeController = IGimbalBindingLayout::Mouse; ImGui::Separator(); if (ImGui::Button("Add Key", ImVec2(100, 30))) @@ -207,7 +207,7 @@ bool displayKeyMappingsAndVirtualStatesInline(IGimbalManipulateEncoder* encoder, if (ImGui::Button(("Delete##deleteMouse" + std::to_string(static_cast(mouseCode))).c_str())) { anyMapUpdated |= true; - encoder->updateMouseMapping([mouseCode](auto& mouse) { mouse.erase(mouseCode); }); + layout->updateMouseMapping([mouseCode](auto& mouse) { mouse.erase(mouseCode); }); break; } } @@ -216,7 +216,7 @@ bool displayKeyMappingsAndVirtualStatesInline(IGimbalManipulateEncoder* encoder, if (state.addMode) { ImGui::Separator(); - handleAddMapping("AddMouseMappingTable", encoder, state.activeController, state.selectedEventType, state.newKey, state.newMouseCode, state.addMode); + handleAddMapping("AddMouseMappingTable", layout, state.activeController, state.selectedEventType, state.newKey, state.newMouseCode, state.addMode); } ImGui::EndTabItem(); } diff --git a/common/include/camera/CGimbalInputBinder.hpp b/common/include/camera/CGimbalInputBinder.hpp index 3b243d970..ebabe4bd6 100644 --- a/common/include/camera/CGimbalInputBinder.hpp +++ b/common/include/camera/CGimbalInputBinder.hpp @@ -40,40 +40,55 @@ class CGimbalInputBinder final : public IGimbalController clearActiveBindings(); } - inline void copyActiveBindingsFromEncoder(const IGimbalManipulateEncoder& encoder) + inline void copyActiveBindingsFromLayout(const IGimbalBindingLayout& layout) { - updateKeyboardMapping([&](auto& map) { map = sanitizeMapping(encoder.getKeyboardVirtualEventMap()); }); - updateMouseMapping([&](auto& map) { map = sanitizeMapping(encoder.getMouseVirtualEventMap()); }); - updateImguizmoMapping([&](auto& map) { map = sanitizeMapping(encoder.getImguizmoVirtualEventMap()); }); + updateKeyboardMapping([&](auto& map) { map = sanitizeMapping(layout.getKeyboardVirtualEventMap()); }); + updateMouseMapping([&](auto& map) { map = sanitizeMapping(layout.getMouseVirtualEventMap()); }); + updateImguizmoMapping([&](auto& map) { map = sanitizeMapping(layout.getImguizmoVirtualEventMap()); }); } - inline void copyBindingLayoutFrom(const IGimbalManipulateEncoder& encoder) + inline void copyBindingLayoutFrom(const IGimbalBindingLayout& layout) { - copyActiveBindingsFromEncoder(encoder); + copyActiveBindingsFromLayout(layout); } - inline void copyDefaultBindingsFromEncoder(const IGimbalManipulateEncoder& encoder) + inline void copyDefaultBindingsFromLayout(const IGimbalBindingLayout& layout) { - updateKeyboardMapping([&](auto& map) { map = sanitizeMapping(encoder.getKeyboardMappingPreset()); }); - updateMouseMapping([&](auto& map) { map = sanitizeMapping(encoder.getMouseMappingPreset()); }); - updateImguizmoMapping([&](auto& map) { map = sanitizeMapping(encoder.getImguizmoMappingPreset()); }); + updateKeyboardMapping([&](auto& map) { map = sanitizeMapping(layout.getKeyboardMappingPreset()); }); + updateMouseMapping([&](auto& map) { map = sanitizeMapping(layout.getMouseMappingPreset()); }); + updateImguizmoMapping([&](auto& map) { map = sanitizeMapping(layout.getImguizmoMappingPreset()); }); } - inline void copyPresetLayoutFrom(const IGimbalManipulateEncoder& encoder) + inline void copyPresetLayoutFrom(const IGimbalBindingLayout& layout) { - copyDefaultBindingsFromEncoder(encoder); + copyDefaultBindingsFromLayout(layout); } - inline void copyActiveBindingsToEncoder(IGimbalManipulateEncoder& encoder) const + inline void copyActiveBindingsToLayout(IGimbalBindingLayout& layout) const + { + layout.updateKeyboardMapping([&](auto& map) { map = sanitizeMapping(getKeyboardVirtualEventMap()); }); + layout.updateMouseMapping([&](auto& map) { map = sanitizeMapping(getMouseVirtualEventMap()); }); + layout.updateImguizmoMapping([&](auto& map) { map = sanitizeMapping(getImguizmoVirtualEventMap()); }); + } + + inline void copyBindingLayoutTo(IGimbalBindingLayout& layout) const { - encoder.updateKeyboardMapping([&](auto& map) { map = sanitizeMapping(getKeyboardVirtualEventMap()); }); - encoder.updateMouseMapping([&](auto& map) { map = sanitizeMapping(getMouseVirtualEventMap()); }); - encoder.updateImguizmoMapping([&](auto& map) { map = sanitizeMapping(getImguizmoVirtualEventMap()); }); + copyActiveBindingsToLayout(layout); } - inline void copyBindingLayoutTo(IGimbalManipulateEncoder& encoder) const + inline void copyActiveBindingsFromEncoder(const IGimbalManipulateEncoder& encoder) + { + copyActiveBindingsFromLayout(encoder); + } + + inline void copyDefaultBindingsFromEncoder(const IGimbalManipulateEncoder& encoder) + { + copyDefaultBindingsFromLayout(encoder); + } + + inline void copyActiveBindingsToEncoder(IGimbalManipulateEncoder& encoder) const { - copyActiveBindingsToEncoder(encoder); + copyActiveBindingsToLayout(encoder); } inline SCollectedVirtualEvents collectVirtualEvents( diff --git a/common/include/camera/ICamera.hpp b/common/include/camera/ICamera.hpp index 1a5073299..783d1288c 100644 --- a/common/include/camera/ICamera.hpp +++ b/common/include/camera/ICamera.hpp @@ -12,7 +12,7 @@ namespace nbl::hlsl // TODO: DIFFERENT NAMESPACE { -class ICamera : public IGimbalManipulateEncoder, virtual public core::IReferenceCounted +class ICamera : public IGimbalBindingLayout, virtual public core::IReferenceCounted { public: enum class CameraKind : uint8_t diff --git a/common/include/camera/IGimbalController.hpp b/common/include/camera/IGimbalController.hpp index 3e143a37a..7446505dc 100644 --- a/common/include/camera/IGimbalController.hpp +++ b/common/include/camera/IGimbalController.hpp @@ -17,10 +17,10 @@ namespace ImGuizmo namespace nbl::hlsl { -struct IGimbalManipulateEncoder +struct IGimbalBindingLayout { - IGimbalManipulateEncoder() {} - virtual ~IGimbalManipulateEncoder() {} + IGimbalBindingLayout() {} + virtual ~IGimbalBindingLayout() {} //! output of any controller process method using gimbal_event_t = CVirtualGimbalEvent; @@ -94,10 +94,12 @@ struct IGimbalManipulateEncoder virtual void updateImguizmoMapping(const std::function& mapKeys) = 0; }; -class IGimbalController : public IGimbalManipulateEncoder +using IGimbalManipulateEncoder = IGimbalBindingLayout; + +class IGimbalController : public IGimbalBindingLayout { public: - using IGimbalManipulateEncoder::IGimbalManipulateEncoder; + using IGimbalBindingLayout::IGimbalBindingLayout; IGimbalController() {} virtual ~IGimbalController() {} From 5784c79c5ece31b226c33f76e7e930eadde54356 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 14:00:48 +0200 Subject: [PATCH 121/205] Separate binding storage from input processing --- common/include/camera/ICamera.hpp | 2 +- common/include/camera/IGimbalController.hpp | 66 +++++++++++++++++---- common/include/camera/IPlanarProjection.hpp | 6 +- 3 files changed, 57 insertions(+), 17 deletions(-) diff --git a/common/include/camera/ICamera.hpp b/common/include/camera/ICamera.hpp index 783d1288c..2e50d6822 100644 --- a/common/include/camera/ICamera.hpp +++ b/common/include/camera/ICamera.hpp @@ -202,7 +202,7 @@ class ICamera : public IGimbalBindingLayout, virtual public core::IReferenceCoun // (***) TODO: I need to think whether a camera should own this or controllers should be able // to set sensitivity to scale magnitudes of generated events we put into manipulate method double m_moveSpeedScale = 0.01, m_rotationSpeedScale = 0.003; - CGimbalInputBinder m_defaultInputBinding; + CGimbalBindingLayoutStorage m_defaultInputBinding; }; } diff --git a/common/include/camera/IGimbalController.hpp b/common/include/camera/IGimbalController.hpp index 7446505dc..e1bc42368 100644 --- a/common/include/camera/IGimbalController.hpp +++ b/common/include/camera/IGimbalController.hpp @@ -96,11 +96,63 @@ struct IGimbalBindingLayout using IGimbalManipulateEncoder = IGimbalBindingLayout; -class IGimbalController : public IGimbalBindingLayout +class CGimbalBindingLayoutStorage : public IGimbalBindingLayout { public: using IGimbalBindingLayout::IGimbalBindingLayout; + CGimbalBindingLayoutStorage() {} + virtual ~CGimbalBindingLayoutStorage() {} + + virtual void updateKeyboardMapping(const std::function& mapKeys) override { mapKeys(m_keyboardVirtualEventMap); } + virtual void updateMouseMapping(const std::function& mapKeys) override { mapKeys(m_mouseVirtualEventMap); } + virtual void updateImguizmoMapping(const std::function& mapKeys) override { mapKeys(m_imguizmoVirtualEventMap); } + + virtual const keyboard_to_virtual_events_t& getKeyboardVirtualEventMap() const override { return m_keyboardVirtualEventMap; } + virtual const mouse_to_virtual_events_t& getMouseVirtualEventMap() const override { return m_mouseVirtualEventMap; } + virtual const imguizmo_to_virtual_events_t& getImguizmoVirtualEventMap() const override { return m_imguizmoVirtualEventMap; } + + inline void copyBindingLayoutFrom(const IGimbalBindingLayout& layout) + { + updateKeyboardMapping([&](auto& map) { map = sanitizeMapping(layout.getKeyboardVirtualEventMap()); }); + updateMouseMapping([&](auto& map) { map = sanitizeMapping(layout.getMouseVirtualEventMap()); }); + updateImguizmoMapping([&](auto& map) { map = sanitizeMapping(layout.getImguizmoVirtualEventMap()); }); + } + + inline void copyPresetLayoutFrom(const IGimbalBindingLayout& layout) + { + updateKeyboardMapping([&](auto& map) { map = sanitizeMapping(layout.getKeyboardMappingPreset()); }); + updateMouseMapping([&](auto& map) { map = sanitizeMapping(layout.getMouseMappingPreset()); }); + updateImguizmoMapping([&](auto& map) { map = sanitizeMapping(layout.getImguizmoMappingPreset()); }); + } + + inline void copyBindingLayoutTo(IGimbalBindingLayout& layout) const + { + layout.updateKeyboardMapping([&](auto& map) { map = sanitizeMapping(getKeyboardVirtualEventMap()); }); + layout.updateMouseMapping([&](auto& map) { map = sanitizeMapping(getMouseVirtualEventMap()); }); + layout.updateImguizmoMapping([&](auto& map) { map = sanitizeMapping(getImguizmoVirtualEventMap()); }); + } + +protected: + template + inline static Map sanitizeMapping(const Map& source) + { + Map result; + for (const auto& [code, hash] : source) + result.emplace(code, typename Map::mapped_type(hash.event.type)); + return result; + } + + keyboard_to_virtual_events_t m_keyboardVirtualEventMap; + mouse_to_virtual_events_t m_mouseVirtualEventMap; + imguizmo_to_virtual_events_t m_imguizmoVirtualEventMap; +}; + +class IGimbalController : public CGimbalBindingLayoutStorage +{ +public: + using CGimbalBindingLayoutStorage::CGimbalBindingLayoutStorage; + IGimbalController() {} virtual ~IGimbalController() {} @@ -131,10 +183,6 @@ class IGimbalController : public IGimbalBindingLayout m_lastVirtualUpTimeStamp = m_nextPresentationTimeStamp; } - virtual void updateKeyboardMapping(const std::function& mapKeys) override { mapKeys(m_keyboardVirtualEventMap); } - virtual void updateMouseMapping(const std::function& mapKeys) override { mapKeys(m_mouseVirtualEventMap); } - virtual void updateImguizmoMapping(const std::function& mapKeys) override { mapKeys(m_imguizmoVirtualEventMap); } - struct SUpdateParameters { std::span keyboardEvents = {}; @@ -186,10 +234,6 @@ class IGimbalController : public IGimbalBindingLayout count = vKeyboardEventsCount + vMouseEventsCount + vImguizmoEventsCount; } - virtual const keyboard_to_virtual_events_t& getKeyboardVirtualEventMap() const override { return m_keyboardVirtualEventMap; } - virtual const mouse_to_virtual_events_t& getMouseVirtualEventMap() const override { return m_mouseVirtualEventMap; } - virtual const imguizmo_to_virtual_events_t& getImguizmoVirtualEventMap() const override { return m_imguizmoVirtualEventMap; } - /** * @brief Processes keyboard events to generate virtual manipulation events. * @@ -446,10 +490,6 @@ class IGimbalController : public IGimbalBindingLayout } } - keyboard_to_virtual_events_t m_keyboardVirtualEventMap; - mouse_to_virtual_events_t m_mouseVirtualEventMap; - imguizmo_to_virtual_events_t m_imguizmoVirtualEventMap; - double m_frameDeltaTime = {}; std::chrono::microseconds m_nextPresentationTimeStamp = {}, m_lastVirtualUpTimeStamp = {}; }; diff --git a/common/include/camera/IPlanarProjection.hpp b/common/include/camera/IPlanarProjection.hpp index b8b6a89d6..f79b83559 100644 --- a/common/include/camera/IPlanarProjection.hpp +++ b/common/include/camera/IPlanarProjection.hpp @@ -105,12 +105,12 @@ class IPlanarProjection : public ILinearProjection } inline const ProjectionParameters& getParameters() const { return m_parameters; } - inline const CGimbalInputBinder& getInputBinding() const { return m_inputBinding; } - inline CGimbalInputBinder& getInputBinding() { return m_inputBinding; } + inline const CGimbalBindingLayoutStorage& getInputBinding() const { return m_inputBinding; } + inline CGimbalBindingLayoutStorage& getInputBinding() { return m_inputBinding; } private: CProjection() = default; ProjectionParameters m_parameters; - CGimbalInputBinder m_inputBinding; + CGimbalBindingLayoutStorage m_inputBinding; }; protected: From d36d9aa1998afa3f3bb160ddd63a58a8fc1b7e7f Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 14:08:23 +0200 Subject: [PATCH 122/205] Move camera binding layout into a dedicated header --- common/include/camera/ICamera.hpp | 2 +- .../include/camera/IGimbalBindingLayout.hpp | 127 +++++++++++++++++ common/include/camera/IGimbalController.hpp | 134 +----------------- common/include/camera/IPlanarProjection.hpp | 2 +- 4 files changed, 130 insertions(+), 135 deletions(-) create mode 100644 common/include/camera/IGimbalBindingLayout.hpp diff --git a/common/include/camera/ICamera.hpp b/common/include/camera/ICamera.hpp index 2e50d6822..eeb46daf1 100644 --- a/common/include/camera/ICamera.hpp +++ b/common/include/camera/ICamera.hpp @@ -7,7 +7,7 @@ #include -#include "camera/CGimbalInputBinder.hpp" +#include "camera/IGimbalBindingLayout.hpp" namespace nbl::hlsl // TODO: DIFFERENT NAMESPACE { diff --git a/common/include/camera/IGimbalBindingLayout.hpp b/common/include/camera/IGimbalBindingLayout.hpp new file mode 100644 index 000000000..886be7c64 --- /dev/null +++ b/common/include/camera/IGimbalBindingLayout.hpp @@ -0,0 +1,127 @@ +#ifndef _NBL_I_GIMBAL_BINDING_LAYOUT_HPP_ +#define _NBL_I_GIMBAL_BINDING_LAYOUT_HPP_ + +#include "IGimbal.hpp" + +namespace nbl::hlsl +{ + +struct IGimbalBindingLayout +{ + IGimbalBindingLayout() {} + virtual ~IGimbalBindingLayout() {} + + using gimbal_event_t = CVirtualGimbalEvent; + using encode_keyboard_code_t = ui::E_KEY_CODE; + using encode_mouse_code_t = ui::E_MOUSE_CODE; + using encode_imguizmo_code_t = gimbal_event_t::VirtualEventType; + + enum EncoderType : uint8_t + { + Keyboard, + Mouse, + Imguizmo, + + Count + }; + + struct CKeyInfo + { + union + { + encode_keyboard_code_t keyboardCode; + encode_mouse_code_t mouseCode; + encode_imguizmo_code_t imguizmoCode; + }; + + CKeyInfo(encode_keyboard_code_t code) : keyboardCode(code), type(Keyboard) {} + CKeyInfo(encode_mouse_code_t code) : mouseCode(code), type(Mouse) {} + CKeyInfo(encode_imguizmo_code_t code) : imguizmoCode(code), type(Imguizmo) {} + + EncoderType type; + }; + + struct CHashInfo + { + CHashInfo() {} + CHashInfo(gimbal_event_t::VirtualEventType _type) : event({ .type = _type }) {} + ~CHashInfo() = default; + + gimbal_event_t event = {}; + bool active = false; + }; + + using keyboard_to_virtual_events_t = std::unordered_map; + using mouse_to_virtual_events_t = std::unordered_map; + using imguizmo_to_virtual_events_t = std::unordered_map; + + virtual const keyboard_to_virtual_events_t getKeyboardMappingPreset() const { return {}; } + virtual const mouse_to_virtual_events_t getMouseMappingPreset() const { return {}; } + virtual const imguizmo_to_virtual_events_t getImguizmoMappingPreset() const { return {}; } + + virtual const keyboard_to_virtual_events_t& getKeyboardVirtualEventMap() const = 0; + virtual const mouse_to_virtual_events_t& getMouseVirtualEventMap() const = 0; + virtual const imguizmo_to_virtual_events_t& getImguizmoVirtualEventMap() const = 0; + + virtual void updateKeyboardMapping(const std::function& mapKeys) = 0; + virtual void updateMouseMapping(const std::function& mapKeys) = 0; + virtual void updateImguizmoMapping(const std::function& mapKeys) = 0; +}; + +using IGimbalManipulateEncoder = IGimbalBindingLayout; + +class CGimbalBindingLayoutStorage : public IGimbalBindingLayout +{ +public: + using IGimbalBindingLayout::IGimbalBindingLayout; + + CGimbalBindingLayoutStorage() {} + virtual ~CGimbalBindingLayoutStorage() {} + + virtual void updateKeyboardMapping(const std::function& mapKeys) override { mapKeys(m_keyboardVirtualEventMap); } + virtual void updateMouseMapping(const std::function& mapKeys) override { mapKeys(m_mouseVirtualEventMap); } + virtual void updateImguizmoMapping(const std::function& mapKeys) override { mapKeys(m_imguizmoVirtualEventMap); } + + virtual const keyboard_to_virtual_events_t& getKeyboardVirtualEventMap() const override { return m_keyboardVirtualEventMap; } + virtual const mouse_to_virtual_events_t& getMouseVirtualEventMap() const override { return m_mouseVirtualEventMap; } + virtual const imguizmo_to_virtual_events_t& getImguizmoVirtualEventMap() const override { return m_imguizmoVirtualEventMap; } + + inline void copyBindingLayoutFrom(const IGimbalBindingLayout& layout) + { + updateKeyboardMapping([&](auto& map) { map = sanitizeMapping(layout.getKeyboardVirtualEventMap()); }); + updateMouseMapping([&](auto& map) { map = sanitizeMapping(layout.getMouseVirtualEventMap()); }); + updateImguizmoMapping([&](auto& map) { map = sanitizeMapping(layout.getImguizmoVirtualEventMap()); }); + } + + inline void copyPresetLayoutFrom(const IGimbalBindingLayout& layout) + { + updateKeyboardMapping([&](auto& map) { map = sanitizeMapping(layout.getKeyboardMappingPreset()); }); + updateMouseMapping([&](auto& map) { map = sanitizeMapping(layout.getMouseMappingPreset()); }); + updateImguizmoMapping([&](auto& map) { map = sanitizeMapping(layout.getImguizmoMappingPreset()); }); + } + + inline void copyBindingLayoutTo(IGimbalBindingLayout& layout) const + { + layout.updateKeyboardMapping([&](auto& map) { map = sanitizeMapping(getKeyboardVirtualEventMap()); }); + layout.updateMouseMapping([&](auto& map) { map = sanitizeMapping(getMouseVirtualEventMap()); }); + layout.updateImguizmoMapping([&](auto& map) { map = sanitizeMapping(getImguizmoVirtualEventMap()); }); + } + +protected: + template + inline static Map sanitizeMapping(const Map& source) + { + Map result; + for (const auto& [code, hash] : source) + result.emplace(code, typename Map::mapped_type(hash.event.type)); + return result; + } + + keyboard_to_virtual_events_t m_keyboardVirtualEventMap; + mouse_to_virtual_events_t m_mouseVirtualEventMap; + imguizmo_to_virtual_events_t m_imguizmoVirtualEventMap; +}; + +} // namespace nbl::hlsl + +#endif diff --git a/common/include/camera/IGimbalController.hpp b/common/include/camera/IGimbalController.hpp index e1bc42368..eedab05f2 100644 --- a/common/include/camera/IGimbalController.hpp +++ b/common/include/camera/IGimbalController.hpp @@ -10,144 +10,12 @@ namespace ImGuizmo } ///////////////////////// -#include "IProjection.hpp" -#include "IGimbal.hpp" +#include "IGimbalBindingLayout.hpp" // TODO: DIFFERENT NAMESPACE namespace nbl::hlsl { -struct IGimbalBindingLayout -{ - IGimbalBindingLayout() {} - virtual ~IGimbalBindingLayout() {} - - //! output of any controller process method - using gimbal_event_t = CVirtualGimbalEvent; - - //! encode keyboard code used to translate to gimbal_event_t event - using encode_keyboard_code_t = ui::E_KEY_CODE; - - //! encode mouse code used to translate to gimbal_event_t event - using encode_mouse_code_t = ui::E_MOUSE_CODE; - - //! encode ImGuizmo code used to translate to gimbal_event_t event - using encode_imguizmo_code_t = gimbal_event_t::VirtualEventType; - - //! Encoder types, a controller takes encoder type events and outputs gimbal_event_t events - enum EncoderType : uint8_t - { - Keyboard, - Mouse, - Imguizmo, - - Count - }; - - //! A key in a hash map which is "encode__code_t" as union with information about EncoderType the encode value got produced from - struct CKeyInfo - { - union - { - encode_keyboard_code_t keyboardCode; - encode_mouse_code_t mouseCode; - encode_imguizmo_code_t imguizmoCode; - }; - - CKeyInfo(encode_keyboard_code_t code) : keyboardCode(code), type(Keyboard) {} - CKeyInfo(encode_mouse_code_t code) : mouseCode(code), type(Mouse) {} - CKeyInfo(encode_imguizmo_code_t code) : imguizmoCode(code), type(Imguizmo) {} - - EncoderType type; - }; - - //! Hash value in hash map which is gimbal_event_t & state - struct CHashInfo - { - CHashInfo() {} - CHashInfo(gimbal_event_t::VirtualEventType _type) : event({ .type = _type }) {} - ~CHashInfo() = default; - - gimbal_event_t event = {}; - bool active = false; - }; - - using keyboard_to_virtual_events_t = std::unordered_map; - using mouse_to_virtual_events_t = std::unordered_map; - using imguizmo_to_virtual_events_t = std::unordered_map; - - virtual const keyboard_to_virtual_events_t getKeyboardMappingPreset() const { return {}; } - virtual const mouse_to_virtual_events_t getMouseMappingPreset() const { return {}; } - virtual const imguizmo_to_virtual_events_t getImguizmoMappingPreset() const { return {}; } - - virtual const keyboard_to_virtual_events_t& getKeyboardVirtualEventMap() const = 0; - virtual const mouse_to_virtual_events_t& getMouseVirtualEventMap() const = 0; - virtual const imguizmo_to_virtual_events_t& getImguizmoVirtualEventMap() const = 0; - - // Binds mouse key codes to virtual events, the mapKeys lambda will be executed with controller keyboard_to_virtual_events_t table - virtual void updateKeyboardMapping(const std::function& mapKeys) = 0; - - // Binds mouse key codes to virtual events, the mapKeys lambda will be executed with controller mouse_to_virtual_events_t table - virtual void updateMouseMapping(const std::function& mapKeys) = 0; - - // Binds imguizmo key codes to virtual events, the mapKeys lambda will be executed with controller imguizmo_to_virtual_events_t table - virtual void updateImguizmoMapping(const std::function& mapKeys) = 0; -}; - -using IGimbalManipulateEncoder = IGimbalBindingLayout; - -class CGimbalBindingLayoutStorage : public IGimbalBindingLayout -{ -public: - using IGimbalBindingLayout::IGimbalBindingLayout; - - CGimbalBindingLayoutStorage() {} - virtual ~CGimbalBindingLayoutStorage() {} - - virtual void updateKeyboardMapping(const std::function& mapKeys) override { mapKeys(m_keyboardVirtualEventMap); } - virtual void updateMouseMapping(const std::function& mapKeys) override { mapKeys(m_mouseVirtualEventMap); } - virtual void updateImguizmoMapping(const std::function& mapKeys) override { mapKeys(m_imguizmoVirtualEventMap); } - - virtual const keyboard_to_virtual_events_t& getKeyboardVirtualEventMap() const override { return m_keyboardVirtualEventMap; } - virtual const mouse_to_virtual_events_t& getMouseVirtualEventMap() const override { return m_mouseVirtualEventMap; } - virtual const imguizmo_to_virtual_events_t& getImguizmoVirtualEventMap() const override { return m_imguizmoVirtualEventMap; } - - inline void copyBindingLayoutFrom(const IGimbalBindingLayout& layout) - { - updateKeyboardMapping([&](auto& map) { map = sanitizeMapping(layout.getKeyboardVirtualEventMap()); }); - updateMouseMapping([&](auto& map) { map = sanitizeMapping(layout.getMouseVirtualEventMap()); }); - updateImguizmoMapping([&](auto& map) { map = sanitizeMapping(layout.getImguizmoVirtualEventMap()); }); - } - - inline void copyPresetLayoutFrom(const IGimbalBindingLayout& layout) - { - updateKeyboardMapping([&](auto& map) { map = sanitizeMapping(layout.getKeyboardMappingPreset()); }); - updateMouseMapping([&](auto& map) { map = sanitizeMapping(layout.getMouseMappingPreset()); }); - updateImguizmoMapping([&](auto& map) { map = sanitizeMapping(layout.getImguizmoMappingPreset()); }); - } - - inline void copyBindingLayoutTo(IGimbalBindingLayout& layout) const - { - layout.updateKeyboardMapping([&](auto& map) { map = sanitizeMapping(getKeyboardVirtualEventMap()); }); - layout.updateMouseMapping([&](auto& map) { map = sanitizeMapping(getMouseVirtualEventMap()); }); - layout.updateImguizmoMapping([&](auto& map) { map = sanitizeMapping(getImguizmoVirtualEventMap()); }); - } - -protected: - template - inline static Map sanitizeMapping(const Map& source) - { - Map result; - for (const auto& [code, hash] : source) - result.emplace(code, typename Map::mapped_type(hash.event.type)); - return result; - } - - keyboard_to_virtual_events_t m_keyboardVirtualEventMap; - mouse_to_virtual_events_t m_mouseVirtualEventMap; - imguizmo_to_virtual_events_t m_imguizmoVirtualEventMap; -}; - class IGimbalController : public CGimbalBindingLayoutStorage { public: diff --git a/common/include/camera/IPlanarProjection.hpp b/common/include/camera/IPlanarProjection.hpp index f79b83559..e4721f3a8 100644 --- a/common/include/camera/IPlanarProjection.hpp +++ b/common/include/camera/IPlanarProjection.hpp @@ -1,7 +1,7 @@ #ifndef _NBL_I_PLANAR_PROJECTION_HPP_ #define _NBL_I_PLANAR_PROJECTION_HPP_ -#include "CGimbalInputBinder.hpp" +#include "IGimbalBindingLayout.hpp" #include "ILinearProjection.hpp" namespace nbl::hlsl From 7dc8416f4667835eeb92571989ff379cf4884ec2 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 14:12:33 +0200 Subject: [PATCH 123/205] Rename camera controller JSON to bindings --- 61_UI/AppInit.cpp | 56 +++++++++++++++++--------------- 61_UI/README.md | 2 +- 61_UI/app_resources/cameras.json | 46 +++++++++++++------------- 61_UI/src/keysmapping.cpp | 4 +-- 4 files changed, 56 insertions(+), 52 deletions(-) diff --git a/61_UI/AppInit.cpp b/61_UI/AppInit.cpp index 2871ce65b..706819d08 100644 --- a/61_UI/AppInit.cpp +++ b/61_UI/AppInit.cpp @@ -1564,81 +1564,83 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) { std::vector keyboard; std::vector mouse; - } controllers; + } bindings; - if (j.contains("controllers")) + const char* bindingLayoutsKey = j.contains("bindings") ? "bindings" : (j.contains("controllers") ? "controllers" : nullptr); + + if (bindingLayoutsKey) { - const auto& jControllers = j["controllers"]; + const auto& jBindings = j[bindingLayoutsKey]; - if (jControllers.contains("keyboard")) + if (jBindings.contains("keyboard")) { - for (const auto& jKeyboard : jControllers["keyboard"]) + for (const auto& jKeyboard : jBindings["keyboard"]) { if (jKeyboard.contains("mappings")) { - auto& controller = controllers.keyboard.emplace_back(); + auto& binding = bindings.keyboard.emplace_back(); for (const auto& [key, value] : jKeyboard["mappings"].items()) { const auto nativeCode = stringToKeyCode(key.c_str()); if (nativeCode == EKC_NONE) { - logFail("Invalid native key \"%s\" code mapping for keyboard controller", key.c_str()); + logFail("Invalid native key \"%s\" code mapping for keyboard binding", key.c_str()); return false; } - controller[nativeCode] = CVirtualGimbalEvent::stringToVirtualEvent(value.get()); + binding[nativeCode] = CVirtualGimbalEvent::stringToVirtualEvent(value.get()); } } else { - logFail("Expected \"mappings\" keyword for keyboard controller definition!"); + logFail("Expected \"mappings\" keyword for keyboard binding definition!"); return false; } } } else { - logFail("Expected \"keyboard\" keyword in controllers definition!"); + logFail("Expected \"keyboard\" keyword in bindings definition!"); return false; } - if (jControllers.contains("mouse")) + if (jBindings.contains("mouse")) { - for (const auto& jMouse : jControllers["mouse"]) + for (const auto& jMouse : jBindings["mouse"]) { if (jMouse.contains("mappings")) { - auto& controller = controllers.mouse.emplace_back(); + auto& binding = bindings.mouse.emplace_back(); for (const auto& [key, value] : jMouse["mappings"].items()) { const auto nativeCode = stringToMouseCode(key.c_str()); if (nativeCode == EMC_NONE) { - logFail("Invalid native key \"%s\" code mapping for mouse controller", key.c_str()); + logFail("Invalid native key \"%s\" code mapping for mouse binding", key.c_str()); return false; } - controller[nativeCode] = CVirtualGimbalEvent::stringToVirtualEvent(value.get()); + binding[nativeCode] = CVirtualGimbalEvent::stringToVirtualEvent(value.get()); } } else { - logFail("Expected \"mappings\" keyword for mouse controller definition!"); + logFail("Expected \"mappings\" keyword for mouse binding definition!"); return false; } } } else { - logFail("Expected \"mouse\" keyword in controllers definition"); + logFail("Expected \"mouse\" keyword in bindings definition"); return false; } } else { - logFail("Expected \"controllers\" keyword in controllers JSON"); + logFail("Expected \"bindings\" keyword in camera JSON"); return false; } @@ -1665,31 +1667,33 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) for (const auto viewportIx : boundViewports) { auto& viewport = j["viewports"][viewportIx]; - if (!viewport.contains("projection") || !viewport.contains("controllers")) + const char* viewportBindingsKey = viewport.contains("bindings") ? "bindings" : (viewport.contains("controllers") ? "controllers" : nullptr); + if (!viewport.contains("projection") || !viewportBindingsKey) { - logFail("\"projection\" or \"controllers\" missing in viewport object index %d", viewportIx); + logFail("\"projection\" or \"bindings\" missing in viewport object index %d", viewportIx); return false; } const auto projectionIx = viewport["projection"].get(); auto& projection = planar->getPlanarProjections().emplace_back(projections[projectionIx]); auto& projectionBinding = projection.getInputBinding(); + const auto& jViewportBindings = viewport[viewportBindingsKey]; - const bool hasKeyboardBound = viewport["controllers"].contains("keyboard"); - const bool hasMouseBound = viewport["controllers"].contains("mouse"); + const bool hasKeyboardBound = jViewportBindings.contains("keyboard"); + const bool hasMouseBound = jViewportBindings.contains("mouse"); if (hasKeyboardBound) { - auto keyboardControllerIx = viewport["controllers"]["keyboard"].get(); - projectionBinding.updateKeyboardMapping([&](auto& map) { map = controllers.keyboard[keyboardControllerIx]; }); + auto keyboardBindingIx = jViewportBindings["keyboard"].get(); + projectionBinding.updateKeyboardMapping([&](auto& map) { map = bindings.keyboard[keyboardBindingIx]; }); } else projectionBinding.updateKeyboardMapping([&](auto& map) { map = {}; }); // clean the map if not bound if (hasMouseBound) { - auto mouseControllerIx = viewport["controllers"]["mouse"].get(); - projectionBinding.updateMouseMapping([&](auto& map) { map = controllers.mouse[mouseControllerIx]; }); + auto mouseBindingIx = jViewportBindings["mouse"].get(); + projectionBinding.updateMouseMapping([&](auto& map) { map = bindings.mouse[mouseBindingIx]; }); } else projectionBinding.updateMouseMapping([&](auto& map) { map = {}; }); // clean the map if not bound diff --git a/61_UI/README.md b/61_UI/README.md index 518312136..1b4724676 100644 --- a/61_UI/README.md +++ b/61_UI/README.md @@ -19,7 +19,7 @@ It contains a scripted-input harness that can drive camera actions in CI and val - DollyZoom - Path -Each planar uses one of the configured controller mappings and can be switched at runtime by scripted `action` events. +Each planar uses one of the configured input binding layouts and can be switched at runtime by scripted `action` events. ## Short math context diff --git a/61_UI/app_resources/cameras.json b/61_UI/app_resources/cameras.json index 123adc946..04a61a69b 100644 --- a/61_UI/app_resources/cameras.json +++ b/61_UI/app_resources/cameras.json @@ -74,91 +74,91 @@ "viewports": [ { "projection": 0, - "controllers": { "keyboard": 0, "mouse": 0 } + "bindings": { "keyboard": 0, "mouse": 0 } }, { "projection": 1, - "controllers": { "keyboard": 0, "mouse": 0 } + "bindings": { "keyboard": 0, "mouse": 0 } }, { "projection": 0, - "controllers": { "keyboard": 3, "mouse": 1 } + "bindings": { "keyboard": 3, "mouse": 1 } }, { "projection": 1, - "controllers": { "keyboard": 3, "mouse": 1 } + "bindings": { "keyboard": 3, "mouse": 1 } }, { "projection": 0, - "controllers": { "keyboard": 2, "mouse": 0 } + "bindings": { "keyboard": 2, "mouse": 0 } }, { "projection": 1, - "controllers": { "keyboard": 2, "mouse": 0 } + "bindings": { "keyboard": 2, "mouse": 0 } }, { "projection": 0, - "controllers": { "keyboard": 2, "mouse": 0 } + "bindings": { "keyboard": 2, "mouse": 0 } }, { "projection": 1, - "controllers": { "keyboard": 2, "mouse": 0 } + "bindings": { "keyboard": 2, "mouse": 0 } }, { "projection": 0, - "controllers": { "keyboard": 2, "mouse": 0 } + "bindings": { "keyboard": 2, "mouse": 0 } }, { "projection": 1, - "controllers": { "keyboard": 2, "mouse": 0 } + "bindings": { "keyboard": 2, "mouse": 0 } }, { "projection": 0, - "controllers": { "keyboard": 0, "mouse": 1 } + "bindings": { "keyboard": 0, "mouse": 1 } }, { "projection": 1, - "controllers": { "keyboard": 0, "mouse": 1 } + "bindings": { "keyboard": 0, "mouse": 1 } }, { "projection": 0, - "controllers": { "keyboard": 1, "mouse": 1 } + "bindings": { "keyboard": 1, "mouse": 1 } }, { "projection": 1, - "controllers": { "keyboard": 1, "mouse": 1 } + "bindings": { "keyboard": 1, "mouse": 1 } }, { "projection": 0, - "controllers": { "keyboard": 2, "mouse": 0 } + "bindings": { "keyboard": 2, "mouse": 0 } }, { "projection": 1, - "controllers": { "keyboard": 2, "mouse": 0 } + "bindings": { "keyboard": 2, "mouse": 0 } }, { "projection": 0, - "controllers": { "keyboard": 2, "mouse": 0 } + "bindings": { "keyboard": 2, "mouse": 0 } }, { "projection": 1, - "controllers": { "keyboard": 2, "mouse": 0 } + "bindings": { "keyboard": 2, "mouse": 0 } }, { "projection": 0, - "controllers": { "keyboard": 3, "mouse": 1 } + "bindings": { "keyboard": 3, "mouse": 1 } }, { "projection": 1, - "controllers": { "keyboard": 3, "mouse": 1 } + "bindings": { "keyboard": 3, "mouse": 1 } }, { "projection": 0, - "controllers": { "keyboard": 2, "mouse": 1 } + "bindings": { "keyboard": 2, "mouse": 1 } }, { "projection": 1, - "controllers": { "keyboard": 2, "mouse": 1 } + "bindings": { "keyboard": 2, "mouse": 1 } } ], "planars": [ @@ -174,7 +174,7 @@ { "camera": 9, "viewports": [18, 19] }, { "camera": 10, "viewports": [20, 21] } ], - "controllers": { + "bindings": { "keyboard": [ { "mappings": { diff --git a/61_UI/src/keysmapping.cpp b/61_UI/src/keysmapping.cpp index 563a3a5ad..bd2671ceb 100644 --- a/61_UI/src/keysmapping.cpp +++ b/61_UI/src/keysmapping.cpp @@ -101,10 +101,10 @@ bool displayKeyMappingsAndVirtualStatesInline(IGimbalBindingLayout* layout, bool if (spawnWindow) { ImGui::SetNextWindowSize(ImVec2(600, 400), ImGuiCond_FirstUseEver); - ImGui::Begin("Controller Mappings & Virtual States", nullptr, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_AlwaysVerticalScrollbar); + ImGui::Begin("Binding Layouts & Virtual States", nullptr, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_AlwaysVerticalScrollbar); } - if (ImGui::BeginTabBar("ControllersTabBar")) + if (ImGui::BeginTabBar("BindingsTabBar")) { if (ImGui::BeginTabItem("Keyboard")) { From 5e02c9b1f605b8c6a74b8d52c4af59db3a0797c4 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 14:17:56 +0200 Subject: [PATCH 124/205] Rename gimbal controller to input processor --- 61_UI/AppInit.cpp | 6 +++--- 61_UI/AppUpdate.cpp | 4 ++-- 61_UI/AppWorkLoop.cpp | 2 +- common/include/camera/CGimbalInputBinder.hpp | 4 ++-- common/include/camera/IGimbalController.hpp | 8 +++++--- 5 files changed, 13 insertions(+), 11 deletions(-) diff --git a/61_UI/AppInit.cpp b/61_UI/AppInit.cpp index 706819d08..932ff4229 100644 --- a/61_UI/AppInit.cpp +++ b/61_UI/AppInit.cpp @@ -560,7 +560,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) } } if (!keyboardOk) - return fail("Keyboard controller smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\"."); + return fail("Keyboard binding smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\"."); const auto mousePreset = camera->getMouseMappingPreset(); const bool hasMoveMapping = @@ -596,7 +596,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) if (mouseMoveEvents.empty()) return fail("Mouse move virtual events missing for camera \"" + std::string(camera->getIdentifier()) + "\"."); if (!manipulateAndMeasure(camera, mouseMoveEvents, mouseMovePosDelta, mouseMoveRotDelta)) - return fail("Mouse move controller smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\"."); + return fail("Mouse move binding smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\"."); } double mouseScrollPosDelta = 0.0; @@ -616,7 +616,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) if (mouseScrollEvents.empty()) return fail("Mouse scroll virtual events missing for camera \"" + std::string(camera->getIdentifier()) + "\"."); if (!manipulateAndMeasure(camera, mouseScrollEvents, mouseScrollPosDelta, mouseScrollRotDelta)) - return fail("Mouse scroll controller smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\"."); + return fail("Mouse scroll binding smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\"."); } std::cout << "[headless-camera-smoke][pass] " << camera->getIdentifier() diff --git a/61_UI/AppUpdate.cpp b/61_UI/AppUpdate.cpp index 2cca79360..6dfccd8de 100644 --- a/61_UI/AppUpdate.cpp +++ b/61_UI/AppUpdate.cpp @@ -353,7 +353,7 @@ void App::update() applyVirtualEventScaling(virtualEvents, vCount); - const char* controllerLabel = "Keyboard/Mouse"; + const char* bindingLabel = "Keyboard/Mouse"; auto applyEventsToCamera = [&](ICamera* target, uint32_t planarIx) { if (!target) @@ -373,7 +373,7 @@ void App::update() } applyConstraintsToCamera(target); - appendVirtualEventLog("input", controllerLabel, planarIx, target, virtualEvents.data(), vCount); + appendVirtualEventLog("input", bindingLabel, planarIx, target, virtualEvents.data(), vCount); }; if (m_cameraControls.mirrorInput) diff --git a/61_UI/AppWorkLoop.cpp b/61_UI/AppWorkLoop.cpp index 0ebc54587..62b4a53f3 100644 --- a/61_UI/AppWorkLoop.cpp +++ b/61_UI/AppWorkLoop.cpp @@ -35,7 +35,7 @@ void App::workLoopBody() auto frame = m_tripleBuffers[resourceIx].get(); auto cmdbuf = m_cmdBufs[resourceIx].get(); - // update CPU stuff - controllers, events, UI state + // update CPU stuff - input bindings, events, UI state update(); bool willSubmit = true; diff --git a/common/include/camera/CGimbalInputBinder.hpp b/common/include/camera/CGimbalInputBinder.hpp index ebabe4bd6..54551e212 100644 --- a/common/include/camera/CGimbalInputBinder.hpp +++ b/common/include/camera/CGimbalInputBinder.hpp @@ -8,10 +8,10 @@ namespace nbl::hlsl { -class CGimbalInputBinder final : public IGimbalController +class CGimbalInputBinder final : public IGimbalInputProcessor { public: - using base_t = IGimbalController; + using base_t = IGimbalInputProcessor; using base_t::base_t; struct SCollectedVirtualEvents diff --git a/common/include/camera/IGimbalController.hpp b/common/include/camera/IGimbalController.hpp index eedab05f2..7c9f3b1e9 100644 --- a/common/include/camera/IGimbalController.hpp +++ b/common/include/camera/IGimbalController.hpp @@ -16,13 +16,13 @@ namespace ImGuizmo namespace nbl::hlsl { -class IGimbalController : public CGimbalBindingLayoutStorage +class IGimbalInputProcessor : public CGimbalBindingLayoutStorage { public: using CGimbalBindingLayoutStorage::CGimbalBindingLayoutStorage; - IGimbalController() {} - virtual ~IGimbalController() {} + IGimbalInputProcessor() {} + virtual ~IGimbalInputProcessor() {} //! input of keyboard gimbal controller process utility - Nabla UI event handler produces ui::SKeyboardEvent events using input_keyboard_event_t = ui::SKeyboardEvent; @@ -362,6 +362,8 @@ class IGimbalController : public CGimbalBindingLayoutStorage std::chrono::microseconds m_nextPresentationTimeStamp = {}, m_lastVirtualUpTimeStamp = {}; }; +using IGimbalController = IGimbalInputProcessor; + } // nbl::hlsl namespace #endif // _NBL_I_CAMERA_CONTROLLER_HPP_ From e579f7491f6855329c90458b793d5700fcb90d25 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 14:24:17 +0200 Subject: [PATCH 125/205] Unify camera rig setup across initialization --- 61_UI/AppInit.cpp | 374 ++++++++++-------------------- common/include/camera/ICamera.hpp | 36 +-- 2 files changed, 143 insertions(+), 267 deletions(-) diff --git a/61_UI/AppInit.cpp b/61_UI/AppInit.cpp index 932ff4229..42f22051f 100644 --- a/61_UI/AppInit.cpp +++ b/61_UI/AppInit.cpp @@ -46,6 +46,118 @@ namespace in.read(reinterpret_cast(outPayload.data()), static_cast(outPayload.size())); return in.gcount() == static_cast(outPayload.size()); } + + constexpr float CameraDefaultMoveScale = 0.01f; + constexpr float CameraDefaultRotateScale = 0.003f; + constexpr float CameraOrbitMoveScale = 0.5f; + + void initializeCameraRigConfig(nbl::hlsl::ICamera& camera, const double moveScale, const double rotationScale) + { + camera.setMoveSpeedScale(moveScale); + camera.setRotationSpeedScale(rotationScale); + camera.updateKeyboardMapping([&](auto& map) { map = camera.getKeyboardMappingPreset(); }); + camera.updateMouseMapping([&](auto& map) { map = camera.getMouseMappingPreset(); }); + camera.updateImguizmoMapping([&](auto& map) { map = camera.getImguizmoMappingPreset(); }); + } + + bool createCameraFromJson(const nbl_json& jCamera, std::string& error, smart_refctd_ptr& outCamera) + { + using namespace nbl::hlsl; + + if (!jCamera.contains("type")) + { + error = "Camera entry missing \"type\"."; + return false; + } + + if (!jCamera.contains("position")) + { + error = "Camera entry missing \"position\"."; + return false; + } + + const std::string type = jCamera["type"].get(); + const bool withOrientation = jCamera.contains("orientation"); + const bool withTarget = jCamera.contains("target"); + + auto position = [&]() + { + const auto jret = jCamera["position"].get>(); + return float32_t3(jret[0], jret[1], jret[2]); + }(); + + auto getOrientation = [&]() + { + const auto jret = jCamera["orientation"].get>(); + return glm::quat(jret[3], jret[0], jret[1], jret[2]); + }; + + auto getTarget = [&]() + { + const auto jret = jCamera["target"].get>(); + return float32_t3(jret[0], jret[1], jret[2]); + }; + + auto finalize = [&](auto&& camera, const double moveScale, const double rotationScale) + { + initializeCameraRigConfig(*camera, moveScale, rotationScale); + outCamera = std::move(camera); + return true; + }; + + if (type == "FPS") + { + if (!withOrientation) + { + error = "FPS camera requires \"orientation\"."; + return false; + } + return finalize(make_smart_refctd_ptr(position, getOrientation()), CameraDefaultMoveScale, CameraDefaultRotateScale); + } + + if (type == "Free") + { + if (!withOrientation) + { + error = "Free camera requires \"orientation\"."; + return false; + } + return finalize(make_smart_refctd_ptr(position, getOrientation()), CameraDefaultMoveScale, CameraDefaultRotateScale); + } + + if (!withTarget) + { + error = "Camera type \"" + type + "\" requires \"target\"."; + return false; + } + + if (type == "Orbit") + return finalize(make_smart_refctd_ptr(position, getTarget()), CameraOrbitMoveScale, CameraDefaultRotateScale); + if (type == "Arcball") + return finalize(make_smart_refctd_ptr(position, getTarget()), CameraOrbitMoveScale, CameraDefaultRotateScale); + if (type == "Turntable") + return finalize(make_smart_refctd_ptr(position, getTarget()), CameraOrbitMoveScale, CameraDefaultRotateScale); + if (type == "TopDown") + return finalize(make_smart_refctd_ptr(position, getTarget()), CameraOrbitMoveScale, CameraDefaultRotateScale); + if (type == "Isometric") + return finalize(make_smart_refctd_ptr(position, getTarget()), CameraOrbitMoveScale, CameraDefaultRotateScale); + if (type == "Chase") + return finalize(make_smart_refctd_ptr(position, getTarget()), CameraOrbitMoveScale, CameraDefaultRotateScale); + if (type == "Dolly") + return finalize(make_smart_refctd_ptr(position, getTarget()), CameraOrbitMoveScale, CameraDefaultRotateScale); + if (type == "Path") + return finalize(make_smart_refctd_ptr(position, getTarget()), CameraOrbitMoveScale, CameraDefaultRotateScale); + if (type == "DollyZoom") + { + float baseFov = 40.0f; + if (jCamera.contains("baseFov")) + baseFov = jCamera["baseFov"].get(); + return finalize(make_smart_refctd_ptr(position, getTarget(), baseFov), CameraOrbitMoveScale, CameraDefaultRotateScale); + } + + error = "Unsupported camera type \"" + type + "\"."; + return false; + } } bool App::onAppInitialized(smart_refctd_ptr&& system) @@ -132,124 +244,11 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) cameras.reserve(j["cameras"].size()); for (const auto& jCamera : j["cameras"]) { - if (!jCamera.contains("type")) - return fail("Camera entry missing \"type\"."); - if (!jCamera.contains("position")) - return fail("Camera entry missing \"position\"."); - - const auto withOrientation = jCamera.contains("orientation"); - auto position = [&]() - { - auto jret = jCamera["position"].get>(); - return float32_t3(jret[0], jret[1], jret[2]); - }(); - - auto getOrientation = [&]() - { - auto jret = jCamera["orientation"].get>(); - return glm::quat(jret[3], jret[0], jret[1], jret[2]); - }; - - auto getTarget = [&]() - { - auto jret = jCamera["target"].get>(); - return float32_t3(jret[0], jret[1], jret[2]); - }; - - constexpr float DefaultMoveScale = 0.01f; - constexpr float DefaultRotateScale = 0.003f; - constexpr float OrbitMoveScale = 0.5f; - const auto type = jCamera["type"].get(); - - if (type == "FPS") - { - if (!withOrientation) - return fail("FPS camera requires \"orientation\"."); - auto camera = make_smart_refctd_ptr(position, getOrientation()); - camera->setMoveSpeedScale(DefaultMoveScale); - camera->setRotationSpeedScale(DefaultRotateScale); - cameras.emplace_back(std::move(camera)); - } - else if (type == "Free") - { - if (!withOrientation) - return fail("Free camera requires \"orientation\"."); - auto camera = make_smart_refctd_ptr(position, getOrientation()); - camera->setMoveSpeedScale(DefaultMoveScale); - camera->setRotationSpeedScale(DefaultRotateScale); - cameras.emplace_back(std::move(camera)); - } - else if (type == "Orbit") - { - auto camera = make_smart_refctd_ptr(position, getTarget()); - camera->setMoveSpeedScale(OrbitMoveScale); - camera->setRotationSpeedScale(DefaultRotateScale); - cameras.emplace_back(std::move(camera)); - } - else if (type == "Arcball") - { - auto camera = make_smart_refctd_ptr(position, getTarget()); - camera->setMoveSpeedScale(OrbitMoveScale); - camera->setRotationSpeedScale(DefaultRotateScale); - cameras.emplace_back(std::move(camera)); - } - else if (type == "Turntable") - { - auto camera = make_smart_refctd_ptr(position, getTarget()); - camera->setMoveSpeedScale(OrbitMoveScale); - camera->setRotationSpeedScale(DefaultRotateScale); - cameras.emplace_back(std::move(camera)); - } - else if (type == "TopDown") - { - auto camera = make_smart_refctd_ptr(position, getTarget()); - camera->setMoveSpeedScale(OrbitMoveScale); - camera->setRotationSpeedScale(DefaultRotateScale); - cameras.emplace_back(std::move(camera)); - } - else if (type == "Isometric") - { - auto camera = make_smart_refctd_ptr(position, getTarget()); - camera->setMoveSpeedScale(OrbitMoveScale); - camera->setRotationSpeedScale(DefaultRotateScale); - cameras.emplace_back(std::move(camera)); - } - else if (type == "Chase") - { - auto camera = make_smart_refctd_ptr(position, getTarget()); - camera->setMoveSpeedScale(OrbitMoveScale); - camera->setRotationSpeedScale(DefaultRotateScale); - cameras.emplace_back(std::move(camera)); - } - else if (type == "Dolly") - { - auto camera = make_smart_refctd_ptr(position, getTarget()); - camera->setMoveSpeedScale(OrbitMoveScale); - camera->setRotationSpeedScale(DefaultRotateScale); - cameras.emplace_back(std::move(camera)); - } - else if (type == "DollyZoom") - { - float baseFov = 40.0f; - if (jCamera.contains("baseFov")) - baseFov = jCamera["baseFov"].get(); - - auto camera = make_smart_refctd_ptr(position, getTarget(), baseFov); - camera->setMoveSpeedScale(OrbitMoveScale); - camera->setRotationSpeedScale(DefaultRotateScale); - cameras.emplace_back(std::move(camera)); - } - else if (type == "Path") - { - auto camera = make_smart_refctd_ptr(position, getTarget()); - camera->setMoveSpeedScale(OrbitMoveScale); - camera->setRotationSpeedScale(DefaultRotateScale); - cameras.emplace_back(std::move(camera)); - } - else - { - return fail("Unsupported camera type \"" + type + "\"."); - } + smart_refctd_ptr camera; + std::string error; + if (!createCameraFromJson(jCamera, error, camera)) + return fail(error); + cameras.emplace_back(std::move(camera)); } if (cameras.empty()) @@ -403,9 +402,6 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) if (!camera) return fail("Null camera instance."); - camera->updateKeyboardMapping([&](auto& map) { map = camera->getKeyboardMappingPreset(); }); - camera->updateMouseMapping([&](auto& map) { map = camera->getMouseMappingPreset(); }); - camera->updateImguizmoMapping([&](auto& map) { map = camera->getImguizmoMappingPreset(); }); CGimbalInputBinder inputBinder; inputBinder.copyDefaultBindingsFromLayout(*camera); @@ -1374,132 +1370,14 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) return false; } - const bool withOrientation = jCamera.contains("orientation"); - - auto position = [&]() - { - auto jret = jCamera["position"].get>(); - return float32_t3(jret[0], jret[1], jret[2]); - }(); - - auto getOrientation = [&]() - { - auto jret = jCamera["orientation"].get>(); - - // order important for glm::quat, - // the ctor is GLM_FUNC_QUALIFIER GLM_CONSTEXPR qua::qua(T _w, T _x, T _y, T _z) - // but memory layout (and json) is x,y,z,w - return glm::quat(jret[3], jret[0], jret[1], jret[2]); - }; - - auto getTarget = [&]() - { - auto jret = jCamera["target"].get>(); - return float32_t3(jret[0], jret[1], jret[2]); - }; - - constexpr float DefaultMoveScale = 0.01f; - constexpr float DefaultRotateScale = 0.003f; - constexpr float OrbitMoveScale = 0.5f; - - if (jCamera["type"] == "FPS") - { - if (!withOrientation) - { - logFail("Expected \"orientation\" keyword for FPS camera definition!"); - return false; - } - - auto camera = make_smart_refctd_ptr(position, getOrientation()); - camera->setMoveSpeedScale(DefaultMoveScale); - camera->setRotationSpeedScale(DefaultRotateScale); - cameras.emplace_back(std::move(camera)); - } - else if (jCamera["type"] == "Free") - { - if (!withOrientation) - { - logFail("Expected \"orientation\" keyword for Free camera definition!"); - return false; - } - - auto camera = make_smart_refctd_ptr(position, getOrientation()); - camera->setMoveSpeedScale(DefaultMoveScale); - camera->setRotationSpeedScale(DefaultRotateScale); - cameras.emplace_back(std::move(camera)); - } - else if (jCamera["type"] == "Orbit") - { - auto camera = make_smart_refctd_ptr(position, getTarget()); - camera->setMoveSpeedScale(OrbitMoveScale); - camera->setRotationSpeedScale(DefaultRotateScale); - cameras.emplace_back(std::move(camera)); - } - else if (jCamera["type"] == "Arcball") - { - auto camera = make_smart_refctd_ptr(position, getTarget()); - camera->setMoveSpeedScale(OrbitMoveScale); - camera->setRotationSpeedScale(DefaultRotateScale); - cameras.emplace_back(std::move(camera)); - } - else if (jCamera["type"] == "Turntable") - { - auto camera = make_smart_refctd_ptr(position, getTarget()); - camera->setMoveSpeedScale(OrbitMoveScale); - camera->setRotationSpeedScale(DefaultRotateScale); - cameras.emplace_back(std::move(camera)); - } - else if (jCamera["type"] == "TopDown") - { - auto camera = make_smart_refctd_ptr(position, getTarget()); - camera->setMoveSpeedScale(OrbitMoveScale); - camera->setRotationSpeedScale(DefaultRotateScale); - cameras.emplace_back(std::move(camera)); - } - else if (jCamera["type"] == "Isometric") - { - auto camera = make_smart_refctd_ptr(position, getTarget()); - camera->setMoveSpeedScale(OrbitMoveScale); - camera->setRotationSpeedScale(DefaultRotateScale); - cameras.emplace_back(std::move(camera)); - } - else if (jCamera["type"] == "Chase") + smart_refctd_ptr camera; + std::string error; + if (!createCameraFromJson(jCamera, error, camera)) { - auto camera = make_smart_refctd_ptr(position, getTarget()); - camera->setMoveSpeedScale(OrbitMoveScale); - camera->setRotationSpeedScale(DefaultRotateScale); - cameras.emplace_back(std::move(camera)); - } - else if (jCamera["type"] == "Dolly") - { - auto camera = make_smart_refctd_ptr(position, getTarget()); - camera->setMoveSpeedScale(OrbitMoveScale); - camera->setRotationSpeedScale(DefaultRotateScale); - cameras.emplace_back(std::move(camera)); - } - else if (jCamera["type"] == "DollyZoom") - { - float baseFov = 40.0f; - if (jCamera.contains("baseFov")) - baseFov = jCamera["baseFov"].get(); - - auto camera = make_smart_refctd_ptr(position, getTarget(), baseFov); - camera->setMoveSpeedScale(OrbitMoveScale); - camera->setRotationSpeedScale(DefaultRotateScale); - cameras.emplace_back(std::move(camera)); - } - else if (jCamera["type"] == "Path") - { - auto camera = make_smart_refctd_ptr(position, getTarget()); - camera->setMoveSpeedScale(OrbitMoveScale); - camera->setRotationSpeedScale(DefaultRotateScale); - cameras.emplace_back(std::move(camera)); - } - else - { - logFail("Unsupported camera type!"); + logFail("%s", error.c_str()); return false; } + cameras.emplace_back(std::move(camera)); } else { @@ -1699,14 +1577,6 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) projectionBinding.updateMouseMapping([&](auto& map) { map = {}; }); // clean the map if not bound } - { - auto* camera = planar->getCamera(); - { - camera->updateKeyboardMapping([&](auto& map) { map = camera->getKeyboardMappingPreset(); }); - camera->updateMouseMapping([&](auto& map) { map = camera->getMouseMappingPreset(); }); - camera->updateImguizmoMapping([&](auto& map) { map = camera->getImguizmoMappingPreset(); }); - } - } } } else diff --git a/common/include/camera/ICamera.hpp b/common/include/camera/ICamera.hpp index eeb46daf1..eeff19598 100644 --- a/common/include/camera/ICamera.hpp +++ b/common/include/camera/ICamera.hpp @@ -15,6 +15,13 @@ namespace nbl::hlsl // TODO: DIFFERENT NAMESPACE class ICamera : public IGimbalBindingLayout, virtual public core::IReferenceCounted { public: + struct SRigConfig + { + double moveSpeedScale = 0.01; + double rotationSpeedScale = 0.003; + CGimbalBindingLayoutStorage defaultInputBinding; + }; + enum class CameraKind : uint8_t { Unknown, @@ -100,13 +107,13 @@ class ICamera : public IGimbalBindingLayout, virtual public core::IReferenceCoun ICamera() {} virtual ~ICamera() = default; - virtual const keyboard_to_virtual_events_t& getKeyboardVirtualEventMap() const override { return m_defaultInputBinding.getKeyboardVirtualEventMap(); } - virtual const mouse_to_virtual_events_t& getMouseVirtualEventMap() const override { return m_defaultInputBinding.getMouseVirtualEventMap(); } - virtual const imguizmo_to_virtual_events_t& getImguizmoVirtualEventMap() const override { return m_defaultInputBinding.getImguizmoVirtualEventMap(); } + virtual const keyboard_to_virtual_events_t& getKeyboardVirtualEventMap() const override { return m_rigConfig.defaultInputBinding.getKeyboardVirtualEventMap(); } + virtual const mouse_to_virtual_events_t& getMouseVirtualEventMap() const override { return m_rigConfig.defaultInputBinding.getMouseVirtualEventMap(); } + virtual const imguizmo_to_virtual_events_t& getImguizmoVirtualEventMap() const override { return m_rigConfig.defaultInputBinding.getImguizmoVirtualEventMap(); } - virtual void updateKeyboardMapping(const std::function& mapKeys) override { m_defaultInputBinding.updateKeyboardMapping(mapKeys); } - virtual void updateMouseMapping(const std::function& mapKeys) override { m_defaultInputBinding.updateMouseMapping(mapKeys); } - virtual void updateImguizmoMapping(const std::function& mapKeys) override { m_defaultInputBinding.updateImguizmoMapping(mapKeys); } + virtual void updateKeyboardMapping(const std::function& mapKeys) override { m_rigConfig.defaultInputBinding.updateKeyboardMapping(mapKeys); } + virtual void updateMouseMapping(const std::function& mapKeys) override { m_rigConfig.defaultInputBinding.updateMouseMapping(mapKeys); } + virtual void updateImguizmoMapping(const std::function& mapKeys) override { m_rigConfig.defaultInputBinding.updateImguizmoMapping(mapKeys); } // Returns a gimbal which *models the camera view* virtual const CGimbal& getGimbal() = 0u; @@ -185,24 +192,23 @@ class ICamera : public IGimbalBindingLayout, virtual public core::IReferenceCoun // (***) inline void setMoveSpeedScale(double scalar) { - m_moveSpeedScale = scalar; + m_rigConfig.moveSpeedScale = scalar; } // (***) inline void setRotationSpeedScale(double scalar) { - m_rotationSpeedScale = scalar; + m_rigConfig.rotationSpeedScale = scalar; } - inline double getMoveSpeedScale() const { return m_moveSpeedScale; } - inline double getRotationSpeedScale() const { return m_rotationSpeedScale; } + inline double getMoveSpeedScale() const { return m_rigConfig.moveSpeedScale; } + inline double getRotationSpeedScale() const { return m_rigConfig.rotationSpeedScale; } + inline const SRigConfig& getRigConfig() const { return m_rigConfig; } protected: - - // (***) TODO: I need to think whether a camera should own this or controllers should be able - // to set sensitivity to scale magnitudes of generated events we put into manipulate method - double m_moveSpeedScale = 0.01, m_rotationSpeedScale = 0.003; - CGimbalBindingLayoutStorage m_defaultInputBinding; + SRigConfig m_rigConfig; + double& m_moveSpeedScale = m_rigConfig.moveSpeedScale; + double& m_rotationSpeedScale = m_rigConfig.rotationSpeedScale; }; } From a6bdd9a92d0a09103ae480b4417ca4a194f105b8 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 14:26:52 +0200 Subject: [PATCH 126/205] Route camera motion scales through rig config --- common/include/camera/CArcballCamera.hpp | 6 +++--- common/include/camera/CChaseCamera.hpp | 6 +++--- common/include/camera/CDollyCamera.hpp | 6 +++--- common/include/camera/CDollyZoomCamera.hpp | 4 ++-- common/include/camera/CFPSCamera.hpp | 2 +- common/include/camera/CIsometricCamera.hpp | 2 +- common/include/camera/COrbitCamera.hpp | 4 ++-- common/include/camera/CPathCamera.hpp | 2 +- common/include/camera/CTopDownCamera.hpp | 4 ++-- common/include/camera/CTurntableCamera.hpp | 4 ++-- common/include/camera/ICamera.hpp | 2 -- 11 files changed, 20 insertions(+), 22 deletions(-) diff --git a/common/include/camera/CArcballCamera.hpp b/common/include/camera/CArcballCamera.hpp index 6ae68ce8a..c3f2cffec 100644 --- a/common/include/camera/CArcballCamera.hpp +++ b/common/include/camera/CArcballCamera.hpp @@ -39,11 +39,11 @@ class CArcballCamera final : public CSphericalTargetCamera auto impulse = m_gimbal.accumulate(virtualEvents); - const double deltaYaw = impulse.dVirtualRotation.y * m_rotationSpeedScale; - const double deltaPitch = impulse.dVirtualRotation.x * m_rotationSpeedScale; + const double deltaYaw = impulse.dVirtualRotation.y * getRotationSpeedScale(); + const double deltaPitch = impulse.dVirtualRotation.x * getRotationSpeedScale(); constexpr double translateScalar = 0.01; - const double panScalar = translateScalar * m_moveSpeedScale; + const double panScalar = translateScalar * getMoveSpeedScale(); const double deltaPanX = impulse.dVirtualTranslate.x * panScalar; const double deltaPanY = impulse.dVirtualTranslate.y * panScalar; const double deltaDistance = impulse.dVirtualTranslate.z * translateScalar; diff --git a/common/include/camera/CChaseCamera.hpp b/common/include/camera/CChaseCamera.hpp index afd526670..b9e30035b 100644 --- a/common/include/camera/CChaseCamera.hpp +++ b/common/include/camera/CChaseCamera.hpp @@ -35,11 +35,11 @@ class CChaseCamera final : public CSphericalTargetCamera auto impulse = m_gimbal.accumulate(virtualEvents); - const double deltaYaw = impulse.dVirtualRotation.y * m_rotationSpeedScale; - const double deltaPitch = impulse.dVirtualRotation.x * m_rotationSpeedScale; + const double deltaYaw = impulse.dVirtualRotation.y * getRotationSpeedScale(); + const double deltaPitch = impulse.dVirtualRotation.x * getRotationSpeedScale(); constexpr double translateScalar = 0.01; - const double moveScalar = translateScalar * m_moveSpeedScale; + const double moveScalar = translateScalar * getMoveSpeedScale(); const auto basis = computeBasis(m_u, m_v, m_distance); diff --git a/common/include/camera/CDollyCamera.hpp b/common/include/camera/CDollyCamera.hpp index 96d0082dc..9501962ec 100644 --- a/common/include/camera/CDollyCamera.hpp +++ b/common/include/camera/CDollyCamera.hpp @@ -35,11 +35,11 @@ class CDollyCamera final : public CSphericalTargetCamera auto impulse = m_gimbal.accumulate(virtualEvents); - const double deltaYaw = impulse.dVirtualRotation.y * m_rotationSpeedScale; - const double deltaPitch = impulse.dVirtualRotation.x * m_rotationSpeedScale; + const double deltaYaw = impulse.dVirtualRotation.y * getRotationSpeedScale(); + const double deltaPitch = impulse.dVirtualRotation.x * getRotationSpeedScale(); constexpr double translateScalar = 0.01; - const double moveScalar = translateScalar * m_moveSpeedScale; + const double moveScalar = translateScalar * getMoveSpeedScale(); const auto basis = computeBasis(m_u, m_v, m_distance); const auto delta = (basis.right * impulse.dVirtualTranslate.x + basis.up * impulse.dVirtualTranslate.y + basis.forward * impulse.dVirtualTranslate.z) * moveScalar; diff --git a/common/include/camera/CDollyZoomCamera.hpp b/common/include/camera/CDollyZoomCamera.hpp index 2fffa826c..88bc277a4 100644 --- a/common/include/camera/CDollyZoomCamera.hpp +++ b/common/include/camera/CDollyZoomCamera.hpp @@ -53,8 +53,8 @@ class CDollyZoomCamera final : public CSphericalTargetCamera double deltaDistance = impulse.dVirtualTranslate.z; constexpr auto scalar = 0.01; - deltaU *= scalar * m_moveSpeedScale; - deltaV *= scalar * m_moveSpeedScale; + deltaU *= scalar * getMoveSpeedScale(); + deltaV *= scalar * getMoveSpeedScale(); m_u += deltaU; m_v += deltaV; diff --git a/common/include/camera/CFPSCamera.hpp b/common/include/camera/CFPSCamera.hpp index fdb0ac46f..819c82bc7 100644 --- a/common/include/camera/CFPSCamera.hpp +++ b/common/include/camera/CFPSCamera.hpp @@ -96,7 +96,7 @@ class CFPSCamera final : public ICamera const float rForwardZ = static_cast(reference.frame[2].z); const float rPitch = std::atan2(std::hypot(rForwardX, rForwardZ), rForwardY) - HalfPi; const float gYaw = std::atan2(rForwardX, rForwardZ); - const float newPitch = std::clamp(rPitch + impulse.dVirtualRotation.x * m_rotationSpeedScale, MinVerticalAngle, MaxVerticalAngle), newYaw = gYaw + impulse.dVirtualRotation.y * m_rotationSpeedScale; + const float newPitch = std::clamp(rPitch + impulse.dVirtualRotation.x * getRotationSpeedScale(), MinVerticalAngle, MaxVerticalAngle), newYaw = gYaw + impulse.dVirtualRotation.y * getRotationSpeedScale(); if(validateReference()) m_gimbal.setOrientation(glm::quat(glm::vec3(newPitch, newYaw, 0.0f))); m_gimbal.setPosition(glm::vec3(reference.frame[3]) + reference.orientation * glm::vec3(impulse.dVirtualTranslate)); diff --git a/common/include/camera/CIsometricCamera.hpp b/common/include/camera/CIsometricCamera.hpp index af47ae14c..d92b6c875 100644 --- a/common/include/camera/CIsometricCamera.hpp +++ b/common/include/camera/CIsometricCamera.hpp @@ -37,7 +37,7 @@ class CIsometricCamera final : public CSphericalTargetCamera auto impulse = m_gimbal.accumulate(virtualEvents); constexpr double translateScalar = 0.01; - const double panScalar = translateScalar * m_moveSpeedScale; + const double panScalar = translateScalar * getMoveSpeedScale(); const double deltaPanX = impulse.dVirtualTranslate.x * panScalar; const double deltaPanY = impulse.dVirtualTranslate.y * panScalar; const double deltaDistance = impulse.dVirtualTranslate.z * translateScalar; diff --git a/common/include/camera/COrbitCamera.hpp b/common/include/camera/COrbitCamera.hpp index 84c2a6dbf..1c754b16b 100644 --- a/common/include/camera/COrbitCamera.hpp +++ b/common/include/camera/COrbitCamera.hpp @@ -36,8 +36,8 @@ class COrbitCamera final : public CSphericalTargetCamera // TODO! constexpr auto nastyScalar = 0.01; - deltaU *= nastyScalar * m_moveSpeedScale; - deltaV *= nastyScalar * m_moveSpeedScale; + deltaU *= nastyScalar * getMoveSpeedScale(); + deltaV *= nastyScalar * getMoveSpeedScale(); m_u += deltaU; m_v += deltaV; diff --git a/common/include/camera/CPathCamera.hpp b/common/include/camera/CPathCamera.hpp index e08f3f1d3..2ab5c1cef 100644 --- a/common/include/camera/CPathCamera.hpp +++ b/common/include/camera/CPathCamera.hpp @@ -41,7 +41,7 @@ class CPathCamera final : public CSphericalTargetCamera auto impulse = m_gimbal.accumulate(virtualEvents); constexpr double translateScalar = 0.01; - const double moveScalar = translateScalar * m_moveSpeedScale; + const double moveScalar = translateScalar * getMoveSpeedScale(); m_pathAngle += impulse.dVirtualTranslate.z * moveScalar; m_pathRadius = std::max(MinPathRadius, m_pathRadius + impulse.dVirtualTranslate.x * moveScalar); diff --git a/common/include/camera/CTopDownCamera.hpp b/common/include/camera/CTopDownCamera.hpp index 31a0b7bd7..ff6e68071 100644 --- a/common/include/camera/CTopDownCamera.hpp +++ b/common/include/camera/CTopDownCamera.hpp @@ -35,9 +35,9 @@ class CTopDownCamera final : public CSphericalTargetCamera auto impulse = m_gimbal.accumulate(virtualEvents); - const double deltaYaw = impulse.dVirtualRotation.y * m_rotationSpeedScale; + const double deltaYaw = impulse.dVirtualRotation.y * getRotationSpeedScale(); constexpr double translateScalar = 0.01; - const double panScalar = translateScalar * m_moveSpeedScale; + const double panScalar = translateScalar * getMoveSpeedScale(); const double deltaPanX = impulse.dVirtualTranslate.x * panScalar; const double deltaPanY = impulse.dVirtualTranslate.y * panScalar; const double deltaDistance = impulse.dVirtualTranslate.z * translateScalar; diff --git a/common/include/camera/CTurntableCamera.hpp b/common/include/camera/CTurntableCamera.hpp index e0116a2a2..d6e863dcd 100644 --- a/common/include/camera/CTurntableCamera.hpp +++ b/common/include/camera/CTurntableCamera.hpp @@ -39,8 +39,8 @@ class CTurntableCamera final : public CSphericalTargetCamera auto impulse = m_gimbal.accumulate(virtualEvents); - const double deltaYaw = impulse.dVirtualRotation.y * m_rotationSpeedScale; - const double deltaPitch = impulse.dVirtualRotation.x * m_rotationSpeedScale; + const double deltaYaw = impulse.dVirtualRotation.y * getRotationSpeedScale(); + const double deltaPitch = impulse.dVirtualRotation.x * getRotationSpeedScale(); constexpr double translateScalar = 0.01; const double deltaDistance = impulse.dVirtualTranslate.z * translateScalar; diff --git a/common/include/camera/ICamera.hpp b/common/include/camera/ICamera.hpp index eeff19598..c35709602 100644 --- a/common/include/camera/ICamera.hpp +++ b/common/include/camera/ICamera.hpp @@ -207,8 +207,6 @@ class ICamera : public IGimbalBindingLayout, virtual public core::IReferenceCoun protected: SRigConfig m_rigConfig; - double& m_moveSpeedScale = m_rigConfig.moveSpeedScale; - double& m_rotationSpeedScale = m_rigConfig.rotationSpeedScale; }; } From e459bffb1b74a565b980bb198ae0536763c63834 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 14:29:50 +0200 Subject: [PATCH 127/205] Add camera rig preset reset helpers --- 61_UI/AppInit.cpp | 4 +--- common/include/camera/ICamera.hpp | 6 ++++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/61_UI/AppInit.cpp b/61_UI/AppInit.cpp index 42f22051f..99f0ce274 100644 --- a/61_UI/AppInit.cpp +++ b/61_UI/AppInit.cpp @@ -55,9 +55,7 @@ namespace { camera.setMoveSpeedScale(moveScale); camera.setRotationSpeedScale(rotationScale); - camera.updateKeyboardMapping([&](auto& map) { map = camera.getKeyboardMappingPreset(); }); - camera.updateMouseMapping([&](auto& map) { map = camera.getMouseMappingPreset(); }); - camera.updateImguizmoMapping([&](auto& map) { map = camera.getImguizmoMappingPreset(); }); + camera.resetDefaultInputBindingToPreset(); } bool createCameraFromJson(const nbl_json& jCamera, std::string& error, smart_refctd_ptr& outCamera) diff --git a/common/include/camera/ICamera.hpp b/common/include/camera/ICamera.hpp index c35709602..b872680be 100644 --- a/common/include/camera/ICamera.hpp +++ b/common/include/camera/ICamera.hpp @@ -204,6 +204,12 @@ class ICamera : public IGimbalBindingLayout, virtual public core::IReferenceCoun inline double getMoveSpeedScale() const { return m_rigConfig.moveSpeedScale; } inline double getRotationSpeedScale() const { return m_rigConfig.rotationSpeedScale; } inline const SRigConfig& getRigConfig() const { return m_rigConfig; } + inline void resetDefaultInputBindingToPreset() + { + updateKeyboardMapping([&](auto& map) { map = getKeyboardMappingPreset(); }); + updateMouseMapping([&](auto& map) { map = getMouseMappingPreset(); }); + updateImguizmoMapping([&](auto& map) { map = getImguizmoMappingPreset(); }); + } protected: SRigConfig m_rigConfig; From abb794bc555fa9573c8098cb4a496f779d440883 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 14:46:49 +0200 Subject: [PATCH 128/205] Decouple camera binding layout from ICamera inheritance --- 61_UI/AppInit.cpp | 8 +++--- 61_UI/AppUpdate.cpp | 2 +- common/include/camera/ICamera.hpp | 43 ++++++++++++++++++++++++------- 3 files changed, 38 insertions(+), 15 deletions(-) diff --git a/61_UI/AppInit.cpp b/61_UI/AppInit.cpp index 99f0ce274..bbd0128df 100644 --- a/61_UI/AppInit.cpp +++ b/61_UI/AppInit.cpp @@ -401,7 +401,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) return fail("Null camera instance."); CGimbalInputBinder inputBinder; - inputBinder.copyDefaultBindingsFromLayout(*camera); + camera->copyDefaultInputBindingPresetTo(inputBinder); const auto initialPreset = capturePreset(camera, "smoke-initial"); const auto initialCompatibility = analyzePresetCompatibility(camera, initialPreset); @@ -543,7 +543,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) double keyboardRotDelta = 0.0; for (const auto key : keyboardCandidates) { - inputBinder.copyDefaultBindingsFromLayout(*camera); + camera->copyDefaultInputBindingPresetTo(inputBinder); auto keyboardEvents = collectKeyboardVirtualEvents(inputBinder, key); if (keyboardEvents.empty()) continue; @@ -585,7 +585,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) if (isOrbitLikeCamera(camera) && hasBlockedMovement) return fail("Orbit mouse movement gate failed for camera \"" + std::string(camera->getIdentifier()) + "\"."); - inputBinder.copyDefaultBindingsFromLayout(*camera); + camera->copyDefaultInputBindingPresetTo(inputBinder); auto mouseMoveEvents = collectMouseVirtualEvents(inputBinder, { filteredMoveLookDown.data(), filteredMoveLookDown.size() }); if (mouseMoveEvents.empty()) return fail("Mouse move virtual events missing for camera \"" + std::string(camera->getIdentifier()) + "\"."); @@ -605,7 +605,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) const std::array rawScroll = { scrollEv }; auto filteredScroll = filterOrbitMouseEvents(camera, rawScroll, false); - inputBinder.copyDefaultBindingsFromLayout(*camera); + camera->copyDefaultInputBindingPresetTo(inputBinder); auto mouseScrollEvents = collectMouseVirtualEvents(inputBinder, { filteredScroll.data(), filteredScroll.size() }); if (mouseScrollEvents.empty()) return fail("Mouse scroll virtual events missing for camera \"" + std::string(camera->getIdentifier()) + "\"."); diff --git a/61_UI/AppUpdate.cpp b/61_UI/AppUpdate.cpp index 6dfccd8de..d7ed6fe32 100644 --- a/61_UI/AppUpdate.cpp +++ b/61_UI/AppUpdate.cpp @@ -423,7 +423,7 @@ void App::update() auto* camera = planar->getCamera(); CGimbalInputBinder imguizmoBinding; - imguizmoBinding.copyDefaultBindingsFromLayout(*camera); + camera->copyDefaultInputBindingPresetTo(imguizmoBinding); auto collectedEvents = imguizmoBinding.collectVirtualEvents(m_nextPresentationTimestamp, { .imguizmoEvents = { scriptedImguizmo.data(), scriptedImguizmo.size() } }); diff --git a/common/include/camera/ICamera.hpp b/common/include/camera/ICamera.hpp index b872680be..5b2b6d28d 100644 --- a/common/include/camera/ICamera.hpp +++ b/common/include/camera/ICamera.hpp @@ -12,9 +12,21 @@ namespace nbl::hlsl // TODO: DIFFERENT NAMESPACE { -class ICamera : public IGimbalBindingLayout, virtual public core::IReferenceCounted +class ICamera : virtual public core::IReferenceCounted { public: + using binding_layout_t = IGimbalBindingLayout; + using gimbal_event_t = binding_layout_t::gimbal_event_t; + using encode_keyboard_code_t = binding_layout_t::encode_keyboard_code_t; + using encode_mouse_code_t = binding_layout_t::encode_mouse_code_t; + using encode_imguizmo_code_t = binding_layout_t::encode_imguizmo_code_t; + using EncoderType = binding_layout_t::EncoderType; + using CKeyInfo = binding_layout_t::CKeyInfo; + using CHashInfo = binding_layout_t::CHashInfo; + using keyboard_to_virtual_events_t = binding_layout_t::keyboard_to_virtual_events_t; + using mouse_to_virtual_events_t = binding_layout_t::mouse_to_virtual_events_t; + using imguizmo_to_virtual_events_t = binding_layout_t::imguizmo_to_virtual_events_t; + struct SRigConfig { double moveSpeedScale = 0.01; @@ -107,13 +119,26 @@ class ICamera : public IGimbalBindingLayout, virtual public core::IReferenceCoun ICamera() {} virtual ~ICamera() = default; - virtual const keyboard_to_virtual_events_t& getKeyboardVirtualEventMap() const override { return m_rigConfig.defaultInputBinding.getKeyboardVirtualEventMap(); } - virtual const mouse_to_virtual_events_t& getMouseVirtualEventMap() const override { return m_rigConfig.defaultInputBinding.getMouseVirtualEventMap(); } - virtual const imguizmo_to_virtual_events_t& getImguizmoVirtualEventMap() const override { return m_rigConfig.defaultInputBinding.getImguizmoVirtualEventMap(); } + virtual const keyboard_to_virtual_events_t getKeyboardMappingPreset() const { return {}; } + virtual const mouse_to_virtual_events_t getMouseMappingPreset() const { return {}; } + virtual const imguizmo_to_virtual_events_t getImguizmoMappingPreset() const { return {}; } + + inline const keyboard_to_virtual_events_t& getKeyboardVirtualEventMap() const { return m_rigConfig.defaultInputBinding.getKeyboardVirtualEventMap(); } + inline const mouse_to_virtual_events_t& getMouseVirtualEventMap() const { return m_rigConfig.defaultInputBinding.getMouseVirtualEventMap(); } + inline const imguizmo_to_virtual_events_t& getImguizmoVirtualEventMap() const { return m_rigConfig.defaultInputBinding.getImguizmoVirtualEventMap(); } + + inline void updateKeyboardMapping(const std::function& mapKeys) { m_rigConfig.defaultInputBinding.updateKeyboardMapping(mapKeys); } + inline void updateMouseMapping(const std::function& mapKeys) { m_rigConfig.defaultInputBinding.updateMouseMapping(mapKeys); } + inline void updateImguizmoMapping(const std::function& mapKeys) { m_rigConfig.defaultInputBinding.updateImguizmoMapping(mapKeys); } - virtual void updateKeyboardMapping(const std::function& mapKeys) override { m_rigConfig.defaultInputBinding.updateKeyboardMapping(mapKeys); } - virtual void updateMouseMapping(const std::function& mapKeys) override { m_rigConfig.defaultInputBinding.updateMouseMapping(mapKeys); } - virtual void updateImguizmoMapping(const std::function& mapKeys) override { m_rigConfig.defaultInputBinding.updateImguizmoMapping(mapKeys); } + inline const IGimbalBindingLayout& getDefaultInputBindingLayout() const { return m_rigConfig.defaultInputBinding; } + inline IGimbalBindingLayout& getDefaultInputBindingLayout() { return m_rigConfig.defaultInputBinding; } + inline void copyDefaultInputBindingPresetTo(IGimbalBindingLayout& layout) const + { + layout.updateKeyboardMapping([&](auto& map) { map = getKeyboardMappingPreset(); }); + layout.updateMouseMapping([&](auto& map) { map = getMouseMappingPreset(); }); + layout.updateImguizmoMapping([&](auto& map) { map = getImguizmoMappingPreset(); }); + } // Returns a gimbal which *models the camera view* virtual const CGimbal& getGimbal() = 0u; @@ -206,9 +231,7 @@ class ICamera : public IGimbalBindingLayout, virtual public core::IReferenceCoun inline const SRigConfig& getRigConfig() const { return m_rigConfig; } inline void resetDefaultInputBindingToPreset() { - updateKeyboardMapping([&](auto& map) { map = getKeyboardMappingPreset(); }); - updateMouseMapping([&](auto& map) { map = getMouseMappingPreset(); }); - updateImguizmoMapping([&](auto& map) { map = getImguizmoMappingPreset(); }); + copyDefaultInputBindingPresetTo(m_rigConfig.defaultInputBinding); } protected: From 31d3d6099aa4fa5d883e793978751d149de05602 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 14:54:36 +0200 Subject: [PATCH 129/205] Trim encoder naming from binding layouts --- 61_UI/include/keysmapping.hpp | 2 +- 61_UI/src/keysmapping.cpp | 16 ++++++++-------- common/include/camera/CGimbalInputBinder.hpp | 15 --------------- common/include/camera/ICamera.hpp | 2 +- common/include/camera/IGimbalBindingLayout.hpp | 6 ++---- 5 files changed, 12 insertions(+), 29 deletions(-) diff --git a/61_UI/include/keysmapping.hpp b/61_UI/include/keysmapping.hpp index 109784aa4..ca79bce98 100644 --- a/61_UI/include/keysmapping.hpp +++ b/61_UI/include/keysmapping.hpp @@ -3,7 +3,7 @@ #include "common.hpp" -bool handleAddMapping(const char* tableID, IGimbalBindingLayout* layout, IGimbalBindingLayout::EncoderType activeController, CVirtualGimbalEvent::VirtualEventType& selectedEventType, ui::E_KEY_CODE& newKey, ui::E_MOUSE_CODE& newMouseCode, bool& addMode); +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/src/keysmapping.cpp b/61_UI/src/keysmapping.cpp index bd2671ceb..a4a9d3990 100644 --- a/61_UI/src/keysmapping.cpp +++ b/61_UI/src/keysmapping.cpp @@ -2,7 +2,7 @@ #include #include -bool handleAddMapping(const char* tableID, IGimbalBindingLayout* layout, IGimbalBindingLayout::EncoderType activeController, CVirtualGimbalEvent::VirtualEventType& selectedEventType, ui::E_KEY_CODE& newKey, ui::E_MOUSE_CODE& newMouseCode, bool& addMode) +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); @@ -28,7 +28,7 @@ bool handleAddMapping(const char* tableID, IGimbalBindingLayout* layout, IGimbal } ImGui::TableSetColumnIndex(1); - if (activeController == IGimbalBindingLayout::Keyboard) + if (activeBindingDomain == IGimbalBindingLayout::Keyboard) { char newKeyDisplay[2] = { ui::keyCodeToChar(newKey, true), '\0' }; if (ImGui::BeginCombo("##selectKey", newKeyDisplay)) @@ -65,7 +65,7 @@ bool handleAddMapping(const char* tableID, IGimbalBindingLayout* layout, IGimbal if (ImGui::Button("Confirm Add", ImVec2(100, 30))) { anyMapUpdated |= true; - if (activeController == IGimbalBindingLayout::Keyboard) + if (activeBindingDomain == IGimbalBindingLayout::Keyboard) layout->updateKeyboardMapping([&](auto& keys) { keys[newKey] = selectedEventType; }); else layout->updateMouseMapping([&](auto& mouse) { mouse[newMouseCode] = selectedEventType; }); @@ -89,7 +89,7 @@ bool displayKeyMappingsAndVirtualStatesInline(IGimbalBindingLayout* layout, bool 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::EncoderType activeController = IGimbalBindingLayout::Keyboard; + IGimbalBindingLayout::BindingDomain activeBindingDomain = IGimbalBindingLayout::Keyboard; }; static std::unordered_map cameraStates; @@ -108,7 +108,7 @@ bool displayKeyMappingsAndVirtualStatesInline(IGimbalBindingLayout* layout, bool { if (ImGui::BeginTabItem("Keyboard")) { - state.activeController = IGimbalBindingLayout::Keyboard; + state.activeBindingDomain = IGimbalBindingLayout::Keyboard; ImGui::Separator(); if (ImGui::Button("Add Key", ImVec2(100, 30))) @@ -158,7 +158,7 @@ bool displayKeyMappingsAndVirtualStatesInline(IGimbalBindingLayout* layout, bool if (state.addMode) { ImGui::Separator(); - anyMapUpdated |= handleAddMapping("AddKeyboardMappingTable", layout, state.activeController, state.selectedEventType, state.newKey, state.newMouseCode, state.addMode); + anyMapUpdated |= handleAddMapping("AddKeyboardMappingTable", layout, state.activeBindingDomain, state.selectedEventType, state.newKey, state.newMouseCode, state.addMode); } ImGui::EndTabItem(); @@ -166,7 +166,7 @@ bool displayKeyMappingsAndVirtualStatesInline(IGimbalBindingLayout* layout, bool if (ImGui::BeginTabItem("Mouse")) { - state.activeController = IGimbalBindingLayout::Mouse; + state.activeBindingDomain = IGimbalBindingLayout::Mouse; ImGui::Separator(); if (ImGui::Button("Add Key", ImVec2(100, 30))) @@ -216,7 +216,7 @@ bool displayKeyMappingsAndVirtualStatesInline(IGimbalBindingLayout* layout, bool if (state.addMode) { ImGui::Separator(); - handleAddMapping("AddMouseMappingTable", layout, state.activeController, state.selectedEventType, state.newKey, state.newMouseCode, state.addMode); + handleAddMapping("AddMouseMappingTable", layout, state.activeBindingDomain, state.selectedEventType, state.newKey, state.newMouseCode, state.addMode); } ImGui::EndTabItem(); } diff --git a/common/include/camera/CGimbalInputBinder.hpp b/common/include/camera/CGimbalInputBinder.hpp index 54551e212..158d51783 100644 --- a/common/include/camera/CGimbalInputBinder.hpp +++ b/common/include/camera/CGimbalInputBinder.hpp @@ -76,21 +76,6 @@ class CGimbalInputBinder final : public IGimbalInputProcessor copyActiveBindingsToLayout(layout); } - inline void copyActiveBindingsFromEncoder(const IGimbalManipulateEncoder& encoder) - { - copyActiveBindingsFromLayout(encoder); - } - - inline void copyDefaultBindingsFromEncoder(const IGimbalManipulateEncoder& encoder) - { - copyDefaultBindingsFromLayout(encoder); - } - - inline void copyActiveBindingsToEncoder(IGimbalManipulateEncoder& encoder) const - { - copyActiveBindingsToLayout(encoder); - } - inline SCollectedVirtualEvents collectVirtualEvents( const std::chrono::microseconds nextPresentationTimeStamp, const SUpdateParameters parameters = {}) diff --git a/common/include/camera/ICamera.hpp b/common/include/camera/ICamera.hpp index 5b2b6d28d..6f3ea7078 100644 --- a/common/include/camera/ICamera.hpp +++ b/common/include/camera/ICamera.hpp @@ -20,7 +20,7 @@ class ICamera : virtual public core::IReferenceCounted using encode_keyboard_code_t = binding_layout_t::encode_keyboard_code_t; using encode_mouse_code_t = binding_layout_t::encode_mouse_code_t; using encode_imguizmo_code_t = binding_layout_t::encode_imguizmo_code_t; - using EncoderType = binding_layout_t::EncoderType; + using BindingDomain = binding_layout_t::BindingDomain; using CKeyInfo = binding_layout_t::CKeyInfo; using CHashInfo = binding_layout_t::CHashInfo; using keyboard_to_virtual_events_t = binding_layout_t::keyboard_to_virtual_events_t; diff --git a/common/include/camera/IGimbalBindingLayout.hpp b/common/include/camera/IGimbalBindingLayout.hpp index 886be7c64..26a55e742 100644 --- a/common/include/camera/IGimbalBindingLayout.hpp +++ b/common/include/camera/IGimbalBindingLayout.hpp @@ -16,7 +16,7 @@ struct IGimbalBindingLayout using encode_mouse_code_t = ui::E_MOUSE_CODE; using encode_imguizmo_code_t = gimbal_event_t::VirtualEventType; - enum EncoderType : uint8_t + enum BindingDomain : uint8_t { Keyboard, Mouse, @@ -38,7 +38,7 @@ struct IGimbalBindingLayout CKeyInfo(encode_mouse_code_t code) : mouseCode(code), type(Mouse) {} CKeyInfo(encode_imguizmo_code_t code) : imguizmoCode(code), type(Imguizmo) {} - EncoderType type; + BindingDomain type; }; struct CHashInfo @@ -68,8 +68,6 @@ struct IGimbalBindingLayout virtual void updateImguizmoMapping(const std::function& mapKeys) = 0; }; -using IGimbalManipulateEncoder = IGimbalBindingLayout; - class CGimbalBindingLayoutStorage : public IGimbalBindingLayout { public: From 5f4961a6c532f7795bd54cecf4b2e6b7e524c9b8 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 14:57:22 +0200 Subject: [PATCH 130/205] Drop camera binding layout forwarding helpers --- common/include/camera/ICamera.hpp | 8 -------- 1 file changed, 8 deletions(-) diff --git a/common/include/camera/ICamera.hpp b/common/include/camera/ICamera.hpp index 6f3ea7078..c69bee5f3 100644 --- a/common/include/camera/ICamera.hpp +++ b/common/include/camera/ICamera.hpp @@ -123,14 +123,6 @@ class ICamera : virtual public core::IReferenceCounted virtual const mouse_to_virtual_events_t getMouseMappingPreset() const { return {}; } virtual const imguizmo_to_virtual_events_t getImguizmoMappingPreset() const { return {}; } - inline const keyboard_to_virtual_events_t& getKeyboardVirtualEventMap() const { return m_rigConfig.defaultInputBinding.getKeyboardVirtualEventMap(); } - inline const mouse_to_virtual_events_t& getMouseVirtualEventMap() const { return m_rigConfig.defaultInputBinding.getMouseVirtualEventMap(); } - inline const imguizmo_to_virtual_events_t& getImguizmoVirtualEventMap() const { return m_rigConfig.defaultInputBinding.getImguizmoVirtualEventMap(); } - - inline void updateKeyboardMapping(const std::function& mapKeys) { m_rigConfig.defaultInputBinding.updateKeyboardMapping(mapKeys); } - inline void updateMouseMapping(const std::function& mapKeys) { m_rigConfig.defaultInputBinding.updateMouseMapping(mapKeys); } - inline void updateImguizmoMapping(const std::function& mapKeys) { m_rigConfig.defaultInputBinding.updateImguizmoMapping(mapKeys); } - inline const IGimbalBindingLayout& getDefaultInputBindingLayout() const { return m_rigConfig.defaultInputBinding; } inline IGimbalBindingLayout& getDefaultInputBindingLayout() { return m_rigConfig.defaultInputBinding; } inline void copyDefaultInputBindingPresetTo(IGimbalBindingLayout& layout) const From f8c040709d260d12e44582ecd43ae22921eaa1e2 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 15:03:00 +0200 Subject: [PATCH 131/205] Split camera motion and binding config --- common/include/camera/ICamera.hpp | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/common/include/camera/ICamera.hpp b/common/include/camera/ICamera.hpp index c69bee5f3..6cbd51596 100644 --- a/common/include/camera/ICamera.hpp +++ b/common/include/camera/ICamera.hpp @@ -27,11 +27,15 @@ class ICamera : virtual public core::IReferenceCounted using mouse_to_virtual_events_t = binding_layout_t::mouse_to_virtual_events_t; using imguizmo_to_virtual_events_t = binding_layout_t::imguizmo_to_virtual_events_t; - struct SRigConfig + struct SMotionConfig { double moveSpeedScale = 0.01; double rotationSpeedScale = 0.003; - CGimbalBindingLayoutStorage defaultInputBinding; + }; + + struct SInputBindingConfig + { + CGimbalBindingLayoutStorage defaultBindingLayout; }; enum class CameraKind : uint8_t @@ -123,8 +127,8 @@ class ICamera : virtual public core::IReferenceCounted virtual const mouse_to_virtual_events_t getMouseMappingPreset() const { return {}; } virtual const imguizmo_to_virtual_events_t getImguizmoMappingPreset() const { return {}; } - inline const IGimbalBindingLayout& getDefaultInputBindingLayout() const { return m_rigConfig.defaultInputBinding; } - inline IGimbalBindingLayout& getDefaultInputBindingLayout() { return m_rigConfig.defaultInputBinding; } + inline const IGimbalBindingLayout& getDefaultInputBindingLayout() const { return m_inputBindingConfig.defaultBindingLayout; } + inline IGimbalBindingLayout& getDefaultInputBindingLayout() { return m_inputBindingConfig.defaultBindingLayout; } inline void copyDefaultInputBindingPresetTo(IGimbalBindingLayout& layout) const { layout.updateKeyboardMapping([&](auto& map) { map = getKeyboardMappingPreset(); }); @@ -209,25 +213,27 @@ class ICamera : virtual public core::IReferenceCounted // (***) inline void setMoveSpeedScale(double scalar) { - m_rigConfig.moveSpeedScale = scalar; + m_motionConfig.moveSpeedScale = scalar; } // (***) inline void setRotationSpeedScale(double scalar) { - m_rigConfig.rotationSpeedScale = scalar; + m_motionConfig.rotationSpeedScale = scalar; } - inline double getMoveSpeedScale() const { return m_rigConfig.moveSpeedScale; } - inline double getRotationSpeedScale() const { return m_rigConfig.rotationSpeedScale; } - inline const SRigConfig& getRigConfig() const { return m_rigConfig; } + inline double getMoveSpeedScale() const { return m_motionConfig.moveSpeedScale; } + inline double getRotationSpeedScale() const { return m_motionConfig.rotationSpeedScale; } + inline const SMotionConfig& getMotionConfig() const { return m_motionConfig; } + inline const SInputBindingConfig& getInputBindingConfig() const { return m_inputBindingConfig; } inline void resetDefaultInputBindingToPreset() { - copyDefaultInputBindingPresetTo(m_rigConfig.defaultInputBinding); + copyDefaultInputBindingPresetTo(m_inputBindingConfig.defaultBindingLayout); } protected: - SRigConfig m_rigConfig; + SMotionConfig m_motionConfig; + SInputBindingConfig m_inputBindingConfig; }; } From 94093ca4edd53eed8f36fa4242b1230e00f8ad88 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 15:07:53 +0200 Subject: [PATCH 132/205] Add scoped camera motion scale overrides --- 61_UI/AppControlPanel.cpp | 3 +- 61_UI/AppImGuiListen.cpp | 10 +------ 61_UI/AppInit.cpp | 3 +- 61_UI/AppTransformEditor.cpp | 10 +------ common/include/camera/ICamera.hpp | 49 +++++++++++++++++++++++++++++++ 5 files changed, 53 insertions(+), 22 deletions(-) diff --git a/61_UI/AppControlPanel.cpp b/61_UI/AppControlPanel.cpp index 8634dc782..d7b03ffdc 100644 --- a/61_UI/AppControlPanel.cpp +++ b/61_UI/AppControlPanel.cpp @@ -547,8 +547,7 @@ void App::DrawControlPanel() ImGui::SliderFloat("Rotate speed factor", &rotationSpeed, 0.0001f, 10.f, "%.4f", ImGuiSliderFlags_Logarithmic); DrawHoverHint("Scale rotation speed for this camera"); - boundCamera->setMoveSpeedScale(moveSpeed); - boundCamera->setRotationSpeedScale(rotationSpeed); + boundCamera->setMotionScales(moveSpeed, rotationSpeed); if (isOrbitLike) { diff --git a/61_UI/AppImGuiListen.cpp b/61_UI/AppImGuiListen.cpp index 0f321465a..2c30d9532 100644 --- a/61_UI/AppImGuiListen.cpp +++ b/61_UI/AppImGuiListen.cpp @@ -309,19 +309,11 @@ void App::imguiListen() if (vCount) { - const float pMoveSpeed = targetGimbalManipulationCamera->getMoveSpeedScale(); - const float pRotationSpeed = targetGimbalManipulationCamera->getRotationSpeedScale(); - // I start to think controller should be able to set sensitivity to scale magnitudes of generated events // in order for camera to not keep any magnitude scalars like move or rotation speed scales - targetGimbalManipulationCamera->setMoveSpeedScale(1); - targetGimbalManipulationCamera->setRotationSpeedScale(1); - + auto unitMotionOverride = targetGimbalManipulationCamera->overrideMotionScales(1.0, 1.0); targetGimbalManipulationCamera->manipulate({ virtualEvents.data(), vCount }, &referenceFrame); - - targetGimbalManipulationCamera->setMoveSpeedScale(pMoveSpeed); - targetGimbalManipulationCamera->setRotationSpeedScale(pRotationSpeed); } } diff --git a/61_UI/AppInit.cpp b/61_UI/AppInit.cpp index bbd0128df..6382eb0f3 100644 --- a/61_UI/AppInit.cpp +++ b/61_UI/AppInit.cpp @@ -53,8 +53,7 @@ namespace void initializeCameraRigConfig(nbl::hlsl::ICamera& camera, const double moveScale, const double rotationScale) { - camera.setMoveSpeedScale(moveScale); - camera.setRotationSpeedScale(rotationScale); + camera.setMotionScales(moveScale, rotationScale); camera.resetDefaultInputBindingToPreset(); } diff --git a/61_UI/AppTransformEditor.cpp b/61_UI/AppTransformEditor.cpp index b4370efc7..c4023900b 100644 --- a/61_UI/AppTransformEditor.cpp +++ b/61_UI/AppTransformEditor.cpp @@ -164,18 +164,10 @@ void App::TransformEditorContents() // generate virtual events given delta TRS matrix if (boundCameraToManipulate) { - const float pmSpeed = boundCameraToManipulate->getMoveSpeedScale(); - const float prSpeed = boundCameraToManipulate->getRotationSpeedScale(); - - boundCameraToManipulate->setMoveSpeedScale(1); - boundCameraToManipulate->setRotationSpeedScale(1); - + auto unitMotionOverride = boundCameraToManipulate->overrideMotionScales(1.0, 1.0); auto referenceFrame = getCastedMatrix(imguizmoModel.outTRS); boundCameraToManipulate->manipulate({}, &referenceFrame); - boundCameraToManipulate->setMoveSpeedScale(pmSpeed); - boundCameraToManipulate->setRotationSpeedScale(prSpeed); - /* { static std::vector virtualEvents(0x45); diff --git a/common/include/camera/ICamera.hpp b/common/include/camera/ICamera.hpp index 6cbd51596..81cd2ca65 100644 --- a/common/include/camera/ICamera.hpp +++ b/common/include/camera/ICamera.hpp @@ -6,6 +6,7 @@ #define _I_CAMERA_HPP_ #include +#include #include "camera/IGimbalBindingLayout.hpp" @@ -120,6 +121,44 @@ class ICamera : virtual public core::IReferenceCounted float64_t3x4 m_viewMatrix; }; + class SScopedMotionScaleOverride + { + public: + SScopedMotionScaleOverride(ICamera* camera, const double moveScale, const double rotationScale) + : m_camera(camera) + { + if (!m_camera) + return; + + m_prevMoveScale = m_camera->getMoveSpeedScale(); + m_prevRotationScale = m_camera->getRotationSpeedScale(); + m_camera->setMotionScales(moveScale, rotationScale); + } + + SScopedMotionScaleOverride(const SScopedMotionScaleOverride&) = delete; + SScopedMotionScaleOverride& operator=(const SScopedMotionScaleOverride&) = delete; + + SScopedMotionScaleOverride(SScopedMotionScaleOverride&& other) noexcept + : m_camera(std::exchange(other.m_camera, nullptr)), + m_prevMoveScale(other.m_prevMoveScale), + m_prevRotationScale(other.m_prevRotationScale) + { + } + + SScopedMotionScaleOverride& operator=(SScopedMotionScaleOverride&& other) = delete; + + ~SScopedMotionScaleOverride() + { + if (m_camera) + m_camera->setMotionScales(m_prevMoveScale, m_prevRotationScale); + } + + private: + ICamera* m_camera = nullptr; + double m_prevMoveScale = 0.0; + double m_prevRotationScale = 0.0; + }; + ICamera() {} virtual ~ICamera() = default; @@ -222,10 +261,20 @@ class ICamera : virtual public core::IReferenceCounted m_motionConfig.rotationSpeedScale = scalar; } + inline void setMotionScales(const double moveScale, const double rotationScale) + { + setMoveSpeedScale(moveScale); + setRotationSpeedScale(rotationScale); + } + inline double getMoveSpeedScale() const { return m_motionConfig.moveSpeedScale; } inline double getRotationSpeedScale() const { return m_motionConfig.rotationSpeedScale; } inline const SMotionConfig& getMotionConfig() const { return m_motionConfig; } inline const SInputBindingConfig& getInputBindingConfig() const { return m_inputBindingConfig; } + inline SScopedMotionScaleOverride overrideMotionScales(const double moveScale, const double rotationScale) + { + return SScopedMotionScaleOverride(this, moveScale, rotationScale); + } inline void resetDefaultInputBindingToPreset() { copyDefaultInputBindingPresetTo(m_inputBindingConfig.defaultBindingLayout); From e11d22ea7023ed551bdd9c06662e3370a0806768 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 15:10:31 +0200 Subject: [PATCH 133/205] Wrap unit motion overrides in camera helpers --- 61_UI/AppImGuiListen.cpp | 3 +-- 61_UI/AppTransformEditor.cpp | 3 +-- common/include/camera/ICamera.hpp | 9 +++++++++ 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/61_UI/AppImGuiListen.cpp b/61_UI/AppImGuiListen.cpp index 2c30d9532..96b7c7208 100644 --- a/61_UI/AppImGuiListen.cpp +++ b/61_UI/AppImGuiListen.cpp @@ -312,8 +312,7 @@ void App::imguiListen() // I start to think controller should be able to set sensitivity to scale magnitudes of generated events // in order for camera to not keep any magnitude scalars like move or rotation speed scales - auto unitMotionOverride = targetGimbalManipulationCamera->overrideMotionScales(1.0, 1.0); - targetGimbalManipulationCamera->manipulate({ virtualEvents.data(), vCount }, &referenceFrame); + targetGimbalManipulationCamera->manipulateWithUnitMotionScales({ virtualEvents.data(), vCount }, &referenceFrame); } } diff --git a/61_UI/AppTransformEditor.cpp b/61_UI/AppTransformEditor.cpp index c4023900b..7e2340a21 100644 --- a/61_UI/AppTransformEditor.cpp +++ b/61_UI/AppTransformEditor.cpp @@ -164,9 +164,8 @@ void App::TransformEditorContents() // generate virtual events given delta TRS matrix if (boundCameraToManipulate) { - auto unitMotionOverride = boundCameraToManipulate->overrideMotionScales(1.0, 1.0); auto referenceFrame = getCastedMatrix(imguizmoModel.outTRS); - boundCameraToManipulate->manipulate({}, &referenceFrame); + boundCameraToManipulate->manipulateWithUnitMotionScales({}, &referenceFrame); /* { diff --git a/common/include/camera/ICamera.hpp b/common/include/camera/ICamera.hpp index 81cd2ca65..165cb2b33 100644 --- a/common/include/camera/ICamera.hpp +++ b/common/include/camera/ICamera.hpp @@ -180,6 +180,15 @@ class ICamera : virtual public core::IReferenceCounted // Camera core contract: consume virtual events only. Raw input binding and absolute goal solving live outside ICamera. virtual bool manipulate(std::span virtualEvents, const float64_t4x4* referenceFrame = nullptr) = 0; + inline bool manipulateWithMotionScales(std::span virtualEvents, const float64_t4x4* referenceFrame, const double moveScale, const double rotationScale) + { + auto scopedOverride = overrideMotionScales(moveScale, rotationScale); + return manipulate(virtualEvents, referenceFrame); + } + inline bool manipulateWithUnitMotionScales(std::span virtualEvents, const float64_t4x4* referenceFrame = nullptr) + { + return manipulateWithMotionScales(virtualEvents, referenceFrame, 1.0, 1.0); + } // VirtualEventType bitmask for a camera view gimbal manipulation requests filtering virtual const uint32_t getAllowedVirtualEvents() = 0u; From 2a70bfeb17710c7bea8613780d3d43d686a7a1bf Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 15:14:07 +0200 Subject: [PATCH 134/205] Drop legacy camera goal aliases --- common/include/camera/CCameraGoalSolver.hpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/common/include/camera/CCameraGoalSolver.hpp b/common/include/camera/CCameraGoalSolver.hpp index c5f7b66b0..357f0420d 100644 --- a/common/include/camera/CCameraGoalSolver.hpp +++ b/common/include/camera/CCameraGoalSolver.hpp @@ -705,9 +705,6 @@ class CCameraGoalSolver } }; -using CTargetPose = CCameraGoal; -using CTargetPoseController = CCameraGoalSolver; - } // namespace nbl::hlsl #endif // _C_CAMERA_GOAL_SOLVER_HPP_ From d3f783622307d1d6d910388f5098b641f8fd8ffb Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 15:28:58 +0200 Subject: [PATCH 135/205] Consolidate preset UI analysis in 61_UI --- 61_UI/AppControlPanel.cpp | 91 ++++++++++++----------- 61_UI/include/app/App.hpp | 151 ++++++++++++++++++++++++-------------- 2 files changed, 140 insertions(+), 102 deletions(-) diff --git a/61_UI/AppControlPanel.cpp b/61_UI/AppControlPanel.cpp index d7b03ffdc..1ddf47497 100644 --- a/61_UI/AppControlPanel.cpp +++ b/61_UI/AppControlPanel.cpp @@ -745,59 +745,58 @@ void App::DrawControlPanel() if (m_selectedPresetIx >= 0 && static_cast(m_selectedPresetIx) < m_presets.size()) { const auto& preset = m_presets[static_cast(m_selectedPresetIx)]; - const auto compatibility = analyzePresetCompatibility(activeCamera, preset); - const ImVec4 compatibilityColor = !activeCamera ? bad : (compatibility.exact ? good : warn); - const bool canApplyPreset = canMeaningfullyApplyPreset(activeCamera, preset); - - ImGui::TextDisabled("Preset source"); - ImGui::SameLine(); - ImGui::TextColored(muted, "%s", getCameraTypeLabel(preset.goal.sourceKind).data()); - ImGui::TextDisabled("Goal state"); - ImGui::SameLine(); - ImGui::TextColored(muted, "%s", describeGoalStateMask(preset.goal.sourceGoalStateMask).c_str()); - ImGui::TextDisabled("Policy"); - ImGui::SameLine(); - ImGui::TextColored(canApplyPreset ? compatibilityColor : bad, "%s", describePresetApplyPolicy(activeCamera, preset).c_str()); - ImGui::TextDisabled("Compatibility"); - ImGui::SameLine(); - ImGui::TextColored(compatibilityColor, "%s", describePresetCompatibility(activeCamera, preset).c_str()); - - DrawBadge(compatibility.exact ? "EXACT" : "BEST-EFFORT", compatibility.exact ? good : warn, badgeText); - if (compatibility.missingGoalStateMask != ICamera::GoalStateNone) - { + const auto presetUi = analyzePresetForUi(activeCamera, preset); + const ImVec4 compatibilityColor = !presetUi.hasActiveCamera ? bad : (presetUi.exact() ? good : warn); + + ImGui::TextDisabled("Preset source"); ImGui::SameLine(); - DrawBadge("DROPS STATE", warn, badgeText); - } - else if (!compatibility.sameKind && preset.goal.sourceKind != ICamera::CameraKind::Unknown) - { + ImGui::TextColored(muted, "%s", getCameraTypeLabel(presetUi.goal.sourceKind).data()); + ImGui::TextDisabled("Goal state"); ImGui::SameLine(); - DrawBadge("SHARED STATE", accent, badgeText); - } - if (!canApplyPreset) - { + ImGui::TextColored(muted, "%s", describeGoalStateMask(presetUi.goal.sourceGoalStateMask).c_str()); + ImGui::TextDisabled("Policy"); ImGui::SameLine(); - DrawBadge("BLOCKED", bad, badgeText); - } + ImGui::TextColored(presetUi.canApply ? compatibilityColor : bad, "%s", presetUi.policyLabel.c_str()); + ImGui::TextDisabled("Compatibility"); + ImGui::SameLine(); + ImGui::TextColored(compatibilityColor, "%s", presetUi.compatibilityLabel.c_str()); - if (!canApplyPreset) - ImGui::BeginDisabled(); - if (ImGui::Button("Apply preset")) - applyPresetFromUi(activeCamera, preset); - if (!canApplyPreset) - ImGui::EndDisabled(); - DrawHoverHint(canApplyPreset ? - "Apply selected preset to the active camera" : - "Apply is blocked because there is no active camera or the preset goal is invalid"); - ImGui::SameLine(); - if (ImGui::Button("Remove preset")) - { - m_presets.erase(m_presets.begin() + m_selectedPresetIx); - m_selectedPresetIx = -1; + DrawBadge(presetUi.exact() ? "EXACT" : "BEST-EFFORT", presetUi.exact() ? good : warn, badgeText); + if (presetUi.dropsGoalState()) + { + ImGui::SameLine(); + DrawBadge("DROPS STATE", warn, badgeText); + } + else if (presetUi.usesSharedStateOnly()) + { + ImGui::SameLine(); + DrawBadge("SHARED STATE", accent, badgeText); + } + if (!presetUi.canApply) + { + ImGui::SameLine(); + DrawBadge("BLOCKED", bad, badgeText); + } + + if (!presetUi.canApply) + ImGui::BeginDisabled(); + if (ImGui::Button("Apply preset")) + applyPresetFromUi(activeCamera, preset); + if (!presetUi.canApply) + ImGui::EndDisabled(); + DrawHoverHint(presetUi.canApply ? + "Apply selected preset to the active camera" : + "Apply is blocked because there is no active camera or the preset goal is invalid"); + ImGui::SameLine(); + if (ImGui::Button("Remove preset")) + { + m_presets.erase(m_presets.begin() + m_selectedPresetIx); + m_selectedPresetIx = -1; + } + DrawHoverHint("Remove selected preset"); } - DrawHoverHint("Remove selected preset"); } } - } if (!m_lastPresetApplySummary.empty()) { diff --git a/61_UI/include/app/App.hpp b/61_UI/include/app/App.hpp index ea69a9f45..f0e8924bb 100644 --- a/61_UI/include/app/App.hpp +++ b/61_UI/include/app/App.hpp @@ -500,6 +500,47 @@ class App final : public examples::SimpleWindowedApplication BestEffort }; + struct PresetUiAnalysis + { + CCameraGoal goal = {}; + CCameraGoalSolver::SCompatibilityResult compatibility = {}; + bool hasActiveCamera = false; + bool finiteGoal = false; + bool canApply = false; + std::string compatibilityLabel; + std::string policyLabel; + + inline bool exact() const + { + return compatibility.exact; + } + + inline bool dropsGoalState() const + { + return compatibility.missingGoalStateMask != ICamera::GoalStateNone; + } + + inline bool usesSharedStateOnly() const + { + return !compatibility.sameKind && goal.sourceKind != ICamera::CameraKind::Unknown && !dropsGoalState(); + } + + inline bool matchesFilter(const PresetFilterMode mode) const + { + switch (mode) + { + case PresetFilterMode::All: + return true; + case PresetFilterMode::Exact: + return hasActiveCamera && exact(); + case PresetFilterMode::BestEffort: + return hasActiveCamera && !exact(); + default: + return true; + } + } + }; + struct CameraPlaybackState { bool playing = false; @@ -1379,28 +1420,62 @@ class App final : public examples::SimpleWindowedApplication return oss.str(); } - inline CCameraGoalSolver::SCompatibilityResult analyzePresetCompatibility(ICamera* camera, const CameraPreset& preset) const + inline PresetUiAnalysis analyzePresetForUi(ICamera* camera, const CameraPreset& preset) const { - return m_cameraGoalSolver.analyzeCompatibility(camera, makeGoalFromPreset(preset)); - } + PresetUiAnalysis analysis; + analysis.goal = makeGoalFromPreset(preset); + analysis.hasActiveCamera = camera != nullptr; + analysis.finiteGoal = isGoalFinite(analysis.goal); + analysis.canApply = analysis.hasActiveCamera && analysis.finiteGoal; - inline std::string describePresetCompatibility(ICamera* camera, const CameraPreset& preset) const - { - if (!camera) - return "No active camera"; + if (analysis.hasActiveCamera) + analysis.compatibility = m_cameraGoalSolver.analyzeCompatibility(camera, analysis.goal); - const auto compatibility = analyzePresetCompatibility(camera, preset); - std::ostringstream oss; - oss << (compatibility.exact ? "Exact" : "Best-effort") - << " | source=" << getCameraTypeLabel(preset.goal.sourceKind) - << " | target=" << getCameraTypeLabel(camera); + if (!analysis.hasActiveCamera) + { + analysis.compatibilityLabel = "No active camera"; + analysis.policyLabel = "Blocked | no active camera"; + return analysis; + } - if (compatibility.missingGoalStateMask != ICamera::GoalStateNone) - oss << " | missing=" << describeGoalStateMask(compatibility.missingGoalStateMask); - else if (!compatibility.sameKind && preset.goal.sourceKind != ICamera::CameraKind::Unknown) - oss << " | shared goal state only"; + { + std::ostringstream oss; + oss << (analysis.compatibility.exact ? "Exact" : "Best-effort") + << " | source=" << getCameraTypeLabel(analysis.goal.sourceKind) + << " | target=" << getCameraTypeLabel(camera); - return oss.str(); + if (analysis.compatibility.missingGoalStateMask != ICamera::GoalStateNone) + oss << " | missing=" << describeGoalStateMask(analysis.compatibility.missingGoalStateMask); + else if (!analysis.compatibility.sameKind && analysis.goal.sourceKind != ICamera::CameraKind::Unknown) + oss << " | shared goal state only"; + + analysis.compatibilityLabel = oss.str(); + } + + if (!analysis.finiteGoal) + { + analysis.policyLabel = "Blocked | invalid goal state"; + return analysis; + } + + { + std::ostringstream oss; + oss << (analysis.compatibility.exact ? "Exact apply" : "Best-effort apply"); + if (analysis.compatibility.missingGoalStateMask != ICamera::GoalStateNone) + oss << " | drops=" << describeGoalStateMask(analysis.compatibility.missingGoalStateMask); + else if (!analysis.compatibility.sameKind && analysis.goal.sourceKind != ICamera::CameraKind::Unknown) + oss << " | shared goal state only"; + else + oss << " | full preview available"; + analysis.policyLabel = oss.str(); + } + + return analysis; + } + + inline CCameraGoalSolver::SCompatibilityResult analyzePresetCompatibility(ICamera* camera, const CameraPreset& preset) const + { + return analyzePresetForUi(camera, preset).compatibility; } inline const char* getPresetFilterModeLabel(PresetFilterMode mode) const @@ -1416,17 +1491,7 @@ class App final : public examples::SimpleWindowedApplication inline bool presetMatchesFilter(ICamera* camera, const CameraPreset& preset) const { - switch (m_presetFilterMode) - { - case PresetFilterMode::All: - return true; - case PresetFilterMode::Exact: - return camera && analyzePresetCompatibility(camera, preset).exact; - case PresetFilterMode::BestEffort: - return camera && !analyzePresetCompatibility(camera, preset).exact; - default: - return true; - } + return analyzePresetForUi(camera, preset).matchesFilter(m_presetFilterMode); } inline bool isGoalFinite(const CCameraGoal& goal) const @@ -1452,33 +1517,6 @@ class App final : public examples::SimpleWindowedApplication return true; } - inline bool canMeaningfullyApplyPreset(ICamera* camera, const CameraPreset& preset) const - { - if (!camera) - return false; - return isGoalFinite(makeGoalFromPreset(preset)); - } - - inline std::string describePresetApplyPolicy(ICamera* camera, const CameraPreset& preset) const - { - if (!camera) - return "Blocked | no active camera"; - - if (!canMeaningfullyApplyPreset(camera, preset)) - return "Blocked | invalid goal state"; - - const auto compatibility = analyzePresetCompatibility(camera, preset); - std::ostringstream oss; - oss << (compatibility.exact ? "Exact apply" : "Best-effort apply"); - if (compatibility.missingGoalStateMask != ICamera::GoalStateNone) - oss << " | drops=" << describeGoalStateMask(compatibility.missingGoalStateMask); - else if (!compatibility.sameKind && preset.goal.sourceKind != ICamera::CameraKind::Unknown) - oss << " | shared goal state only"; - else - oss << " | full preview available"; - return oss.str(); - } - inline bool comparePresetToCameraState(ICamera* camera, const CameraPreset& preset, const double posEps, const double rotEpsDeg, const double scalarEps) const { @@ -1631,7 +1669,8 @@ class App final : public examples::SimpleWindowedApplication inline CCameraGoalSolver::SApplyResult applyPresetFromUi(ICamera* camera, const CameraPreset& preset) { const auto result = applyPresetToCameraDetailed(camera, preset); - m_lastPresetApplySummary = describeApplyResult(result) + " | " + describePresetCompatibility(camera, preset); + const auto presetUi = analyzePresetForUi(camera, preset); + m_lastPresetApplySummary = describeApplyResult(result) + " | " + presetUi.compatibilityLabel; m_lastPresetApplySucceeded = result.succeeded(); m_lastPresetApplyApproximate = result.approximate(); return result; From 1fd710d221cf11fc6c2b46a901e963c008008a5c Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 15:37:04 +0200 Subject: [PATCH 136/205] Surface playback preset apply summaries --- 61_UI/AppControlPanel.cpp | 13 ++++++ 61_UI/include/app/App.hpp | 85 +++++++++++++++++++++++++++++++++++---- 2 files changed, 90 insertions(+), 8 deletions(-) diff --git a/61_UI/AppControlPanel.cpp b/61_UI/AppControlPanel.cpp index 1ddf47497..1b8a0027e 100644 --- a/61_UI/AppControlPanel.cpp +++ b/61_UI/AppControlPanel.cpp @@ -850,6 +850,9 @@ void App::DrawControlPanel() { m_playback.playing = false; m_playback.time = 0.f; + m_lastPlaybackApplySummary.clear(); + m_lastPlaybackApplySucceeded = false; + m_lastPlaybackApplyApproximate = false; } DrawHoverHint("Stop playback and reset time"); @@ -858,6 +861,11 @@ void App::DrawControlPanel() const float duration = m_keyframes.back().time; ImGui::SliderFloat("Time", &m_playback.time, 0.f, duration, "%.3f"); } + if (!m_lastPlaybackApplySummary.empty()) + { + const ImVec4 playbackColor = m_lastPlaybackApplySucceeded ? (m_lastPlaybackApplyApproximate ? warn : good) : bad; + ImGui::TextColored(playbackColor, "%s", m_lastPlaybackApplySummary.c_str()); + } DrawSectionHeader("KeyframesHeader", "Keyframes", accent); ImGui::InputFloat("New keyframe time", &m_newKeyframeTime, 0.1f, 1.f, "%.3f"); @@ -874,7 +882,12 @@ void App::DrawControlPanel() DrawHoverHint("Add keyframe from current camera"); ImGui::SameLine(); if (ImGui::Button("Clear keyframes")) + { m_keyframes.clear(); + m_lastPlaybackApplySummary.clear(); + m_lastPlaybackApplySucceeded = false; + m_lastPlaybackApplyApproximate = false; + } DrawHoverHint("Remove all keyframes"); if (!m_keyframes.empty()) diff --git a/61_UI/include/app/App.hpp b/61_UI/include/app/App.hpp index f0e8924bb..b1fdaf322 100644 --- a/61_UI/include/app/App.hpp +++ b/61_UI/include/app/App.hpp @@ -550,6 +550,29 @@ class App final : public examples::SimpleWindowedApplication float time = 0.f; }; + struct PlaybackApplySummary + { + uint32_t targetCount = 0u; + uint32_t successCount = 0u; + uint32_t approximateCount = 0u; + uint32_t failureCount = 0u; + + inline bool hasTargets() const + { + return targetCount > 0u; + } + + inline bool succeeded() const + { + return hasTargets() && failureCount == 0u; + } + + inline bool approximate() const + { + return approximateCount > 0u; + } + }; + struct CameraControlSettings { bool mirrorInput = false; @@ -1681,6 +1704,27 @@ class App final : public examples::SimpleWindowedApplication return applyPresetToCameraDetailed(camera, preset).succeeded(); } + inline std::string describePlaybackApplySummary(const PlaybackApplySummary& summary) const + { + if (!summary.hasTargets()) + return m_playbackAffectsAll ? "Playback apply | no cameras available" : "Playback apply | no active camera"; + + std::ostringstream oss; + oss << "Playback apply | targets=" << summary.targetCount << " | ok=" << summary.successCount; + if (summary.approximateCount > 0u) + oss << " | approximate=" << summary.approximateCount; + if (summary.failureCount > 0u) + oss << " | failed=" << summary.failureCount; + return oss.str(); + } + + inline void storePlaybackApplySummary(const PlaybackApplySummary& summary) + { + m_lastPlaybackApplySummary = describePlaybackApplySummary(summary); + m_lastPlaybackApplySucceeded = summary.succeeded(); + m_lastPlaybackApplyApproximate = summary.approximate(); + } + inline void appendVirtualEventLog(std::string_view source, std::string_view controller, uint32_t planarIx, ICamera* camera, const CVirtualGimbalEvent* events, uint32_t count) { m_uiVirtualEventsThisFrame += count; @@ -1841,12 +1885,32 @@ class App final : public examples::SimpleWindowedApplication count = static_cast(events.size()); } - inline void applyPresetToTargets(const CameraPreset& preset) + inline PlaybackApplySummary applyPresetToTargets(const CameraPreset& preset) { + PlaybackApplySummary summary; + auto applyToCamera = [&](ICamera* camera) -> void + { + if (!camera) + return; + + ++summary.targetCount; + const auto result = applyPresetToCameraDetailed(camera, preset); + if (result.succeeded()) + { + ++summary.successCount; + if (result.approximate()) + ++summary.approximateCount; + } + else + { + ++summary.failureCount; + } + }; + if (!m_playbackAffectsAll) { - applyPresetToCamera(getActiveCamera(), preset); - return; + applyToCamera(getActiveCamera()); + return summary; } std::unordered_set visited; @@ -1860,8 +1924,10 @@ class App final : public examples::SimpleWindowedApplication continue; const auto id = camera->getGimbal().getID(); if (visited.insert(id).second) - applyPresetToCamera(camera, preset); + applyToCamera(camera); } + + return summary; } inline void updatePlayback(double dtSec) @@ -1874,7 +1940,7 @@ class App final : public examples::SimpleWindowedApplication const float duration = m_keyframes.back().time; if (duration <= 0.f) { - applyPresetToTargets(m_keyframes.back().preset); + storePlaybackApplySummary(applyPresetToTargets(m_keyframes.back().preset)); return; } @@ -1892,7 +1958,7 @@ class App final : public examples::SimpleWindowedApplication const auto time = m_playback.time; if (m_keyframes.size() == 1) { - applyPresetToTargets(m_keyframes.front().preset); + storePlaybackApplySummary(applyPresetToTargets(m_keyframes.front().preset)); return; } @@ -1905,7 +1971,7 @@ class App final : public examples::SimpleWindowedApplication if (b.time <= a.time) { - applyPresetToTargets(a.preset); + storePlaybackApplySummary(applyPresetToTargets(a.preset)); return; } @@ -1914,7 +1980,7 @@ class App final : public examples::SimpleWindowedApplication CameraPreset blended = a.preset; assignGoalToPreset(blended, blendGoals(makeGoalFromPreset(a.preset), makeGoalFromPreset(b.preset), alpha)); - applyPresetToTargets(blended); + storePlaybackApplySummary(applyPresetToTargets(blended)); } inline bool savePresetsToFile(const system::path& path) @@ -2503,6 +2569,9 @@ class App final : public examples::SimpleWindowedApplication std::string m_lastPresetApplySummary; bool m_lastPresetApplySucceeded = false; bool m_lastPresetApplyApproximate = false; + std::string m_lastPlaybackApplySummary; + bool m_lastPlaybackApplySucceeded = false; + bool m_lastPlaybackApplyApproximate = false; PresetFilterMode m_presetFilterMode = PresetFilterMode::All; int m_selectedPresetIx = -1; bool m_playbackAffectsAll = false; From 1f57030b59935316f311311ba62fcfc1bdb58a5e Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 15:40:26 +0200 Subject: [PATCH 137/205] Unify preset apply status banners --- 61_UI/AppControlPanel.cpp | 20 +++++++--------- 61_UI/include/app/App.hpp | 48 +++++++++++++++++++++++++++++---------- 2 files changed, 44 insertions(+), 24 deletions(-) diff --git a/61_UI/AppControlPanel.cpp b/61_UI/AppControlPanel.cpp index 1b8a0027e..7b4c01936 100644 --- a/61_UI/AppControlPanel.cpp +++ b/61_UI/AppControlPanel.cpp @@ -798,10 +798,10 @@ void App::DrawControlPanel() } } - if (!m_lastPresetApplySummary.empty()) + if (m_manualPresetApplyBanner.visible()) { - const ImVec4 resultColor = m_lastPresetApplySucceeded ? (m_lastPresetApplyApproximate ? warn : good) : bad; - ImGui::TextColored(resultColor, "%s", m_lastPresetApplySummary.c_str()); + const ImVec4 resultColor = m_manualPresetApplyBanner.succeeded ? (m_manualPresetApplyBanner.approximate ? warn : good) : bad; + ImGui::TextColored(resultColor, "%s", m_manualPresetApplyBanner.summary.c_str()); } DrawSectionHeader("PresetsStorageHeader", "Storage", accent); @@ -850,9 +850,7 @@ void App::DrawControlPanel() { m_playback.playing = false; m_playback.time = 0.f; - m_lastPlaybackApplySummary.clear(); - m_lastPlaybackApplySucceeded = false; - m_lastPlaybackApplyApproximate = false; + clearApplyStatusBanner(m_playbackApplyBanner); } DrawHoverHint("Stop playback and reset time"); @@ -861,10 +859,10 @@ void App::DrawControlPanel() const float duration = m_keyframes.back().time; ImGui::SliderFloat("Time", &m_playback.time, 0.f, duration, "%.3f"); } - if (!m_lastPlaybackApplySummary.empty()) + if (m_playbackApplyBanner.visible()) { - const ImVec4 playbackColor = m_lastPlaybackApplySucceeded ? (m_lastPlaybackApplyApproximate ? warn : good) : bad; - ImGui::TextColored(playbackColor, "%s", m_lastPlaybackApplySummary.c_str()); + const ImVec4 playbackColor = m_playbackApplyBanner.succeeded ? (m_playbackApplyBanner.approximate ? warn : good) : bad; + ImGui::TextColored(playbackColor, "%s", m_playbackApplyBanner.summary.c_str()); } DrawSectionHeader("KeyframesHeader", "Keyframes", accent); @@ -884,9 +882,7 @@ void App::DrawControlPanel() if (ImGui::Button("Clear keyframes")) { m_keyframes.clear(); - m_lastPlaybackApplySummary.clear(); - m_lastPlaybackApplySucceeded = false; - m_lastPlaybackApplyApproximate = false; + clearApplyStatusBanner(m_playbackApplyBanner); } DrawHoverHint("Remove all keyframes"); diff --git a/61_UI/include/app/App.hpp b/61_UI/include/app/App.hpp index b1fdaf322..41cf66a90 100644 --- a/61_UI/include/app/App.hpp +++ b/61_UI/include/app/App.hpp @@ -573,6 +573,18 @@ class App final : public examples::SimpleWindowedApplication } }; + struct ApplyStatusBanner + { + std::string summary; + bool succeeded = false; + bool approximate = false; + + inline bool visible() const + { + return !summary.empty(); + } + }; + struct CameraControlSettings { bool mirrorInput = false; @@ -1693,9 +1705,10 @@ class App final : public examples::SimpleWindowedApplication { const auto result = applyPresetToCameraDetailed(camera, preset); const auto presetUi = analyzePresetForUi(camera, preset); - m_lastPresetApplySummary = describeApplyResult(result) + " | " + presetUi.compatibilityLabel; - m_lastPresetApplySucceeded = result.succeeded(); - m_lastPresetApplyApproximate = result.approximate(); + storeApplyStatusBanner(m_manualPresetApplyBanner, + describeApplyResult(result) + " | " + presetUi.compatibilityLabel, + result.succeeded(), + result.approximate()); return result; } @@ -1704,6 +1717,20 @@ class App final : public examples::SimpleWindowedApplication return applyPresetToCameraDetailed(camera, preset).succeeded(); } + inline void storeApplyStatusBanner(ApplyStatusBanner& banner, std::string summary, const bool succeeded, const bool approximate) + { + banner.summary = std::move(summary); + banner.succeeded = succeeded; + banner.approximate = approximate; + } + + inline void clearApplyStatusBanner(ApplyStatusBanner& banner) + { + banner.summary.clear(); + banner.succeeded = false; + banner.approximate = false; + } + inline std::string describePlaybackApplySummary(const PlaybackApplySummary& summary) const { if (!summary.hasTargets()) @@ -1720,9 +1747,10 @@ class App final : public examples::SimpleWindowedApplication inline void storePlaybackApplySummary(const PlaybackApplySummary& summary) { - m_lastPlaybackApplySummary = describePlaybackApplySummary(summary); - m_lastPlaybackApplySucceeded = summary.succeeded(); - m_lastPlaybackApplyApproximate = summary.approximate(); + storeApplyStatusBanner(m_playbackApplyBanner, + describePlaybackApplySummary(summary), + summary.succeeded(), + summary.approximate()); } inline void appendVirtualEventLog(std::string_view source, std::string_view controller, uint32_t planarIx, ICamera* camera, const CVirtualGimbalEvent* events, uint32_t count) @@ -2566,12 +2594,8 @@ class App final : public examples::SimpleWindowedApplication std::vector m_keyframes; CameraPlaybackState m_playback; CCameraGoalSolver m_cameraGoalSolver; - std::string m_lastPresetApplySummary; - bool m_lastPresetApplySucceeded = false; - bool m_lastPresetApplyApproximate = false; - std::string m_lastPlaybackApplySummary; - bool m_lastPlaybackApplySucceeded = false; - bool m_lastPlaybackApplyApproximate = false; + ApplyStatusBanner m_manualPresetApplyBanner; + ApplyStatusBanner m_playbackApplyBanner; PresetFilterMode m_presetFilterMode = PresetFilterMode::All; int m_selectedPresetIx = -1; bool m_playbackAffectsAll = false; From 3d8a696441805360c4215c4f3ce5782695571a87 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 15:43:48 +0200 Subject: [PATCH 138/205] Preview playback scrubbing through preset flow --- 61_UI/AppControlPanel.cpp | 5 +-- 61_UI/include/app/App.hpp | 72 ++++++++++++++++++++++++--------------- 2 files changed, 48 insertions(+), 29 deletions(-) diff --git a/61_UI/AppControlPanel.cpp b/61_UI/AppControlPanel.cpp index 7b4c01936..ae94a4851 100644 --- a/61_UI/AppControlPanel.cpp +++ b/61_UI/AppControlPanel.cpp @@ -850,14 +850,15 @@ void App::DrawControlPanel() { m_playback.playing = false; m_playback.time = 0.f; - clearApplyStatusBanner(m_playbackApplyBanner); + applyPlaybackAtTime(m_playback.time); } DrawHoverHint("Stop playback and reset time"); if (!m_keyframes.empty()) { const float duration = m_keyframes.back().time; - ImGui::SliderFloat("Time", &m_playback.time, 0.f, duration, "%.3f"); + if (ImGui::SliderFloat("Time", &m_playback.time, 0.f, duration, "%.3f")) + applyPlaybackAtTime(m_playback.time); } if (m_playbackApplyBanner.visible()) { diff --git a/61_UI/include/app/App.hpp b/61_UI/include/app/App.hpp index 41cf66a90..90026f737 100644 --- a/61_UI/include/app/App.hpp +++ b/61_UI/include/app/App.hpp @@ -1958,6 +1958,49 @@ class App final : public examples::SimpleWindowedApplication return summary; } + inline bool tryBuildPlaybackPresetAtTime(const float time, CameraPreset& preset) + { + if (m_keyframes.empty()) + return false; + + if (m_keyframes.size() == 1u) + { + preset = m_keyframes.front().preset; + return true; + } + + const auto clampedTime = std::clamp(time, 0.f, m_keyframes.back().time); + size_t idx = 0u; + while (idx + 1u < m_keyframes.size() && m_keyframes[idx + 1u].time < clampedTime) + ++idx; + + const auto& a = m_keyframes[idx]; + const auto& b = m_keyframes[std::min(idx + 1u, m_keyframes.size() - 1u)]; + if (b.time <= a.time) + { + preset = a.preset; + return true; + } + + const double alpha = static_cast(clampedTime - a.time) / static_cast(b.time - a.time); + preset = a.preset; + assignGoalToPreset(preset, blendGoals(makeGoalFromPreset(a.preset), makeGoalFromPreset(b.preset), alpha)); + return true; + } + + inline bool applyPlaybackAtTime(const float time) + { + CameraPreset preset; + if (!tryBuildPlaybackPresetAtTime(time, preset)) + { + clearApplyStatusBanner(m_playbackApplyBanner); + return false; + } + + storePlaybackApplySummary(applyPresetToTargets(preset)); + return true; + } + inline void updatePlayback(double dtSec) { if (!m_playback.playing || m_keyframes.empty()) @@ -1968,7 +2011,7 @@ class App final : public examples::SimpleWindowedApplication const float duration = m_keyframes.back().time; if (duration <= 0.f) { - storePlaybackApplySummary(applyPresetToTargets(m_keyframes.back().preset)); + applyPlaybackAtTime(m_playback.time); return; } @@ -1983,32 +2026,7 @@ class App final : public examples::SimpleWindowedApplication m_playback.playing = false; } - const auto time = m_playback.time; - if (m_keyframes.size() == 1) - { - storePlaybackApplySummary(applyPresetToTargets(m_keyframes.front().preset)); - return; - } - - size_t idx = 0u; - while (idx + 1u < m_keyframes.size() && m_keyframes[idx + 1u].time < time) - ++idx; - - const auto& a = m_keyframes[idx]; - const auto& b = m_keyframes[std::min(idx + 1u, m_keyframes.size() - 1u)]; - - if (b.time <= a.time) - { - storePlaybackApplySummary(applyPresetToTargets(a.preset)); - return; - } - - const double alpha = static_cast(time - a.time) / static_cast(b.time - a.time); - - CameraPreset blended = a.preset; - assignGoalToPreset(blended, blendGoals(makeGoalFromPreset(a.preset), makeGoalFromPreset(b.preset), alpha)); - - storePlaybackApplySummary(applyPresetToTargets(blended)); + applyPlaybackAtTime(m_playback.time); } inline bool savePresetsToFile(const system::path& path) From cccf6a852a1f882a6db1e78bac332bfb25ca5b53 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 16:06:28 +0200 Subject: [PATCH 139/205] Gate preset capture and keyframe authoring --- 61_UI/AppControlPanel.cpp | 45 +++++++++++++++++++++------- 61_UI/include/app/App.hpp | 62 ++++++++++++++++++++++++++++++++++----- 2 files changed, 90 insertions(+), 17 deletions(-) diff --git a/61_UI/AppControlPanel.cpp b/61_UI/AppControlPanel.cpp index ae94a4851..9d9f1e481 100644 --- a/61_UI/AppControlPanel.cpp +++ b/61_UI/AppControlPanel.cpp @@ -680,13 +680,24 @@ void App::DrawControlPanel() ImGui::PushItemWidth(-1.0f); DrawSectionHeader("PresetsHeader", "Presets", accent); ImGui::InputText("Preset name", m_presetName, IM_ARRAYSIZE(m_presetName)); + auto* activeCamera = getActiveCamera(); + const auto presetCaptureUi = analyzeCameraCaptureForUi(activeCamera); + if (!presetCaptureUi.canCapture) + ImGui::BeginDisabled(); if (ImGui::Button("Add preset")) { - auto* activeCamera = getActiveCamera(); - m_presets.emplace_back(capturePreset(activeCamera, m_presetName)); - m_selectedPresetIx = static_cast(m_presets.size()) - 1; + CameraPreset preset; + if (tryCapturePreset(activeCamera, m_presetName, preset)) + { + m_presets.emplace_back(std::move(preset)); + m_selectedPresetIx = static_cast(m_presets.size()) - 1; + } } - DrawHoverHint("Store current camera as a preset"); + if (!presetCaptureUi.canCapture) + ImGui::EndDisabled(); + DrawHoverHint(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"); ImGui::SameLine(); if (ImGui::Button("Clear presets")) { @@ -694,10 +705,12 @@ void App::DrawControlPanel() m_selectedPresetIx = -1; } DrawHoverHint("Remove all presets"); + ImGui::TextDisabled("Capture"); + ImGui::SameLine(); + ImGui::TextColored(presetCaptureUi.canCapture ? good : bad, "%s", presetCaptureUi.policyLabel.c_str()); if (!m_presets.empty()) { - auto* activeCamera = getActiveCamera(); const char* presetFilterLabels[] = { "All", "Exact", "Best-effort" }; int presetFilterIx = static_cast(m_presetFilterMode); if (ImGui::Combo("Visibility", &presetFilterIx, presetFilterLabels, IM_ARRAYSIZE(presetFilterLabels))) @@ -869,16 +882,28 @@ void App::DrawControlPanel() DrawSectionHeader("KeyframesHeader", "Keyframes", accent); ImGui::InputFloat("New keyframe time", &m_newKeyframeTime, 0.1f, 1.f, "%.3f"); DrawHoverHint("Time value for new keyframe"); + auto* activeCamera = getActiveCamera(); + const auto keyframeCaptureUi = analyzeCameraCaptureForUi(activeCamera); + if (!keyframeCaptureUi.canCapture) + ImGui::BeginDisabled(); if (ImGui::Button("Add keyframe")) { - auto* activeCamera = getActiveCamera(); CameraKeyframe keyframe; keyframe.time = m_newKeyframeTime; - keyframe.preset = capturePreset(activeCamera, "Keyframe"); - m_keyframes.emplace_back(std::move(keyframe)); - std::sort(m_keyframes.begin(), m_keyframes.end(), [](const auto& a, const auto& b) { return a.time < b.time; }); + if (tryCapturePreset(activeCamera, "Keyframe", keyframe.preset)) + { + m_keyframes.emplace_back(std::move(keyframe)); + std::sort(m_keyframes.begin(), m_keyframes.end(), [](const auto& a, const auto& b) { return a.time < b.time; }); + } } - DrawHoverHint("Add keyframe from current camera"); + if (!keyframeCaptureUi.canCapture) + ImGui::EndDisabled(); + DrawHoverHint(keyframeCaptureUi.canCapture ? + "Add keyframe from current camera" : + "Keyframe capture is blocked because there is no active camera or the current goal state is invalid"); + ImGui::TextDisabled("Capture"); + ImGui::SameLine(); + ImGui::TextColored(keyframeCaptureUi.canCapture ? good : bad, "%s", keyframeCaptureUi.policyLabel.c_str()); ImGui::SameLine(); if (ImGui::Button("Clear keyframes")) { diff --git a/61_UI/include/app/App.hpp b/61_UI/include/app/App.hpp index 90026f737..1f09e0199 100644 --- a/61_UI/include/app/App.hpp +++ b/61_UI/include/app/App.hpp @@ -541,6 +541,16 @@ class App final : public examples::SimpleWindowedApplication } }; + struct CaptureUiAnalysis + { + CCameraGoal goal = {}; + bool hasActiveCamera = false; + bool capturedGoal = false; + bool finiteGoal = false; + bool canCapture = false; + std::string policyLabel; + }; + struct CameraPlaybackState { bool playing = false; @@ -1508,6 +1518,39 @@ class App final : public examples::SimpleWindowedApplication return analysis; } + inline CaptureUiAnalysis analyzeCameraCaptureForUi(ICamera* camera) const + { + CaptureUiAnalysis analysis; + analysis.hasActiveCamera = camera != nullptr; + if (!analysis.hasActiveCamera) + { + analysis.policyLabel = "Blocked | no active camera"; + return analysis; + } + + analysis.capturedGoal = m_cameraGoalSolver.capture(camera, analysis.goal); + if (!analysis.capturedGoal) + { + analysis.policyLabel = "Blocked | goal capture failed"; + return analysis; + } + + analysis.goal = canonicalizeGoal(analysis.goal); + analysis.finiteGoal = isGoalFinite(analysis.goal); + if (!analysis.finiteGoal) + { + analysis.policyLabel = "Blocked | invalid goal state"; + return analysis; + } + + analysis.canCapture = true; + std::ostringstream oss; + oss << "Ready | source=" << getCameraTypeLabel(camera) + << " | goal=" << describeGoalStateMask(analysis.goal.sourceGoalStateMask); + analysis.policyLabel = oss.str(); + return analysis; + } + inline CCameraGoalSolver::SCompatibilityResult analyzePresetCompatibility(ICamera* camera, const CameraPreset& preset) const { return analyzePresetForUi(camera, preset).compatibility; @@ -1678,18 +1721,23 @@ class App final : public examples::SimpleWindowedApplication } } - inline CameraPreset capturePreset(ICamera* camera, const std::string& name) + inline bool tryCapturePreset(ICamera* camera, const std::string& name, CameraPreset& preset) { - CameraPreset preset; + const auto captureUi = analyzeCameraCaptureForUi(camera); + preset = {}; preset.name = name; - if (!camera) - return preset; + if (!captureUi.canCapture || !camera) + return false; preset.identifier = std::string(camera->getIdentifier()); - CCameraGoal goal; - if (m_cameraGoalSolver.capture(camera, goal)) - assignGoalToPreset(preset, goal); + assignGoalToPreset(preset, captureUi.goal); + return true; + } + inline CameraPreset capturePreset(ICamera* camera, const std::string& name) + { + CameraPreset preset; + tryCapturePreset(camera, name, preset); return preset; } From b2b20c937bb3d2051397f10f476eab13b8453fc4 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 16:23:27 +0200 Subject: [PATCH 140/205] Promote camera goal utilities into shared camera headers --- 61_UI/include/app/App.hpp | 328 ++---------------- common/include/camera/CCameraGoal.hpp | 350 ++++++++++++++++++++ common/include/camera/CCameraGoalSolver.hpp | 132 ++++---- 3 files changed, 429 insertions(+), 381 deletions(-) create mode 100644 common/include/camera/CCameraGoal.hpp diff --git a/61_UI/include/app/App.hpp b/61_UI/include/app/App.hpp index 1f09e0199..7cf1879c9 100644 --- a/61_UI/include/app/App.hpp +++ b/61_UI/include/app/App.hpp @@ -1127,138 +1127,19 @@ class App final : public examples::SimpleWindowedApplication inline void assignGoalToPreset(CameraPreset& preset, const CCameraGoal& goal) const { - preset.goal = canonicalizeGoal(goal); + preset.goal = nbl::hlsl::canonicalizeGoal(goal); } inline CCameraGoal makeGoalFromPreset(const CameraPreset& preset) const { - return canonicalizeGoal(preset.goal); - } - - inline double wrapAngleRad(double angle) const - { - while (angle > 3.14159265358979323846) - angle -= 6.28318530717958647692; - while (angle < -3.14159265358979323846) - angle += 6.28318530717958647692; - return angle; - } - - inline double lerpWrappedAngleRad(double a, double b, double alpha) const - { - return a + wrapAngleRad(b - a) * alpha; - } - - inline bool applyCanonicalPathGoal(CCameraGoal& goal) const - { - if (!(goal.hasPathState && goal.hasTargetPosition)) - return false; - if (!std::isfinite(goal.pathState.angle) || !std::isfinite(goal.pathState.radius) || !std::isfinite(goal.pathState.height)) - return false; - - const float64_t3 offset( - std::cos(goal.pathState.angle) * goal.pathState.radius, - goal.pathState.height, - std::sin(goal.pathState.angle) * goal.pathState.radius); - const double distance = length(offset); - if (!std::isfinite(distance) || distance <= 1e-9) - return false; - - const float appliedDistance = std::clamp( - static_cast(distance), - CSphericalTargetCamera::MinDistance, - CSphericalTargetCamera::MaxDistance); - const auto local = offset / static_cast(appliedDistance); - goal.orbitU = std::atan2(local.y, local.x); - goal.orbitV = std::asin(std::clamp(local.z, -1.0, 1.0)); - - const float64_t3 spherePosition( - std::cos(goal.orbitV) * std::cos(goal.orbitU) * static_cast(appliedDistance), - std::cos(goal.orbitV) * std::sin(goal.orbitU) * static_cast(appliedDistance), - std::sin(goal.orbitV) * static_cast(appliedDistance)); - - goal.position = goal.targetPosition + spherePosition; - goal.hasDistance = true; - goal.distance = appliedDistance; - goal.hasOrbitState = true; - goal.orbitDistance = appliedDistance; - - const auto forward = normalize(-spherePosition); - const float64_t3 up = normalize(float64_t3( - -std::sin(goal.orbitV) * std::cos(goal.orbitU), - -std::sin(goal.orbitV) * std::sin(goal.orbitU), - std::cos(goal.orbitV))); - const float64_t3 right = normalize(cross(up, forward)); - goal.orientation = glm::quat_cast(glm::dmat3{ right, up, forward }); - return true; - } - - inline CCameraGoal canonicalizeGoal(CCameraGoal goal) const - { - applyCanonicalPathGoal(goal); - return goal; - } - - inline CCameraGoal blendGoals(const CCameraGoal& a, const CCameraGoal& b, double alpha) const - { - CCameraGoal blended; - blended.position = a.position + (b.position - a.position) * alpha; - blended.orientation = glm::slerp(a.orientation, b.orientation, static_cast(alpha)); - blended.sourceKind = (a.sourceKind == b.sourceKind) ? a.sourceKind : ICamera::CameraKind::Unknown; - blended.sourceCapabilities = a.sourceCapabilities & b.sourceCapabilities; - blended.sourceGoalStateMask = a.sourceGoalStateMask | b.sourceGoalStateMask; - blended.hasTargetPosition = a.hasTargetPosition || b.hasTargetPosition; - if (blended.hasTargetPosition) - { - const auto ta = a.hasTargetPosition ? a.targetPosition : b.targetPosition; - const auto tb = b.hasTargetPosition ? b.targetPosition : a.targetPosition; - blended.targetPosition = ta + (tb - ta) * alpha; - } - blended.hasDistance = a.hasDistance || b.hasDistance; - if (blended.hasDistance) - { - const float da = a.hasDistance ? a.distance : b.distance; - const float db = b.hasDistance ? b.distance : a.distance; - blended.distance = da + (db - da) * static_cast(alpha); - } - blended.hasOrbitState = a.hasOrbitState || b.hasOrbitState; - if (blended.hasOrbitState) - { - const double ua = a.hasOrbitState ? a.orbitU : b.orbitU; - const double ub = b.hasOrbitState ? b.orbitU : a.orbitU; - const double va = a.hasOrbitState ? a.orbitV : b.orbitV; - const double vb = b.hasOrbitState ? b.orbitV : a.orbitV; - const float da = a.hasOrbitState ? a.orbitDistance : b.orbitDistance; - const float db = b.hasOrbitState ? b.orbitDistance : a.orbitDistance; - - blended.orbitU = ua + (ub - ua) * alpha; - blended.orbitV = va + (vb - va) * alpha; - blended.orbitDistance = da + (db - da) * static_cast(alpha); - } - blended.hasDynamicPerspectiveState = a.hasDynamicPerspectiveState || b.hasDynamicPerspectiveState; - if (blended.hasDynamicPerspectiveState) - { - const auto dynamicA = a.hasDynamicPerspectiveState ? a.dynamicPerspectiveState : b.dynamicPerspectiveState; - const auto dynamicB = b.hasDynamicPerspectiveState ? b.dynamicPerspectiveState : a.dynamicPerspectiveState; - blended.dynamicPerspectiveState.baseFov = dynamicA.baseFov + (dynamicB.baseFov - dynamicA.baseFov) * static_cast(alpha); - blended.dynamicPerspectiveState.referenceDistance = - dynamicA.referenceDistance + (dynamicB.referenceDistance - dynamicA.referenceDistance) * static_cast(alpha); - } - blended.hasPathState = a.hasPathState || b.hasPathState; - if (blended.hasPathState) - { - const auto pathA = a.hasPathState ? a.pathState : b.pathState; - const auto pathB = b.hasPathState ? b.pathState : a.pathState; - blended.pathState.angle = lerpWrappedAngleRad(pathA.angle, pathB.angle, alpha); - blended.pathState.radius = pathA.radius + (pathB.radius - pathA.radius) * alpha; - blended.pathState.height = pathA.height + (pathB.height - pathA.height) * alpha; - } - return canonicalizeGoal(blended); + return nbl::hlsl::canonicalizeGoal(preset.goal); } inline bool tryCaptureGoal(ICamera* camera, CCameraGoal& out) const { - return m_cameraGoalSolver.capture(camera, out); + const auto capture = m_cameraGoalSolver.captureDetailed(camera); + out = capture.goal; + return capture.captured; } inline std::string describeGoalStateMask(uint32_t mask) const @@ -1282,149 +1163,6 @@ class App final : public examples::SimpleWindowedApplication return out; } - template - inline bool isFiniteVec3(const Vec& v) const - { - return std::isfinite(v.x) && std::isfinite(v.y) && std::isfinite(v.z); - } - - template - inline bool nearlyEqualVec3(const VecA& a, const VecB& b, const double epsilon) const - { - return std::abs(static_cast(a.x - b.x)) <= epsilon && - std::abs(static_cast(a.y - b.y)) <= epsilon && - std::abs(static_cast(a.z - b.z)) <= epsilon; - } - - inline bool compareGoals(const CCameraGoal& actual, const CCameraGoal& expected, - const double posEps, const double rotEpsDeg, const double scalarEps) const - { - auto isFiniteQuat = [](const glm::quat& q) -> bool - { - return std::isfinite(q.x) && std::isfinite(q.y) && std::isfinite(q.z) && std::isfinite(q.w); - }; - - auto angleDiffRad = [](double a, double b) -> double - { - double d = std::fmod(a - b + 3.14159265358979323846, 6.28318530717958647692); - if (d < 0.0) - d += 6.28318530717958647692; - return std::abs(d - 3.14159265358979323846); - }; - - const auto currentOrientation = glm::normalize(actual.orientation); - const auto expectedOrientation = glm::normalize(expected.orientation); - if (!isFiniteVec3(actual.position) || !isFiniteVec3(expected.position) || !isFiniteQuat(currentOrientation) || !isFiniteQuat(expectedOrientation)) - return false; - - const double dx = static_cast(actual.position.x - expected.position.x); - const double dy = static_cast(actual.position.y - expected.position.y); - const double dz = static_cast(actual.position.z - expected.position.z); - const double posDelta = std::sqrt(dx * dx + dy * dy + dz * dz); - const double orientationDot = std::clamp(static_cast(std::abs(glm::dot(currentOrientation, expectedOrientation))), 0.0, 1.0); - const double rotDeltaDeg = glm::degrees(2.0 * std::acos(orientationDot)); - if (posDelta > posEps || rotDeltaDeg > rotEpsDeg) - return false; - - if (expected.hasTargetPosition) - { - if (!actual.hasTargetPosition || !nearlyEqualVec3(actual.targetPosition, expected.targetPosition, scalarEps)) - return false; - } - if (expected.hasDistance) - { - if (!actual.hasDistance || std::abs(static_cast(actual.distance - expected.distance)) > scalarEps) - return false; - } - if (expected.hasOrbitState) - { - if (!actual.hasOrbitState) - return false; - if (angleDiffRad(expected.orbitU, actual.orbitU) > rotEpsDeg * (3.14159265358979323846 / 180.0)) - return false; - if (angleDiffRad(expected.orbitV, actual.orbitV) > rotEpsDeg * (3.14159265358979323846 / 180.0)) - return false; - if (std::abs(static_cast(actual.orbitDistance - expected.orbitDistance)) > scalarEps) - return false; - } - if (expected.hasPathState) - { - if (!actual.hasPathState) - return false; - if (std::abs(wrapAngleRad(expected.pathState.angle - actual.pathState.angle)) > rotEpsDeg * (3.14159265358979323846 / 180.0)) - return false; - if (std::abs(actual.pathState.radius - expected.pathState.radius) > scalarEps) - return false; - if (std::abs(actual.pathState.height - expected.pathState.height) > scalarEps) - return false; - } - if (expected.hasDynamicPerspectiveState) - { - if (!actual.hasDynamicPerspectiveState) - return false; - if (std::abs(static_cast(actual.dynamicPerspectiveState.baseFov - expected.dynamicPerspectiveState.baseFov)) > scalarEps) - return false; - if (std::abs(static_cast(actual.dynamicPerspectiveState.referenceDistance - expected.dynamicPerspectiveState.referenceDistance)) > scalarEps) - return false; - } - - return true; - } - - inline std::string describeGoalMismatch(const CCameraGoal& actual, const CCameraGoal& expected) const - { - std::ostringstream oss; - const auto currentOrientation = glm::normalize(actual.orientation); - const auto expectedOrientation = glm::normalize(expected.orientation); - const double dx = static_cast(actual.position.x - expected.position.x); - const double dy = static_cast(actual.position.y - expected.position.y); - const double dz = static_cast(actual.position.z - expected.position.z); - const double posDelta = std::sqrt(dx * dx + dy * dy + dz * dz); - const double orientationDot = std::clamp(static_cast(std::abs(glm::dot(currentOrientation, expectedOrientation))), 0.0, 1.0); - const double rotDeltaDeg = glm::degrees(2.0 * std::acos(orientationDot)); - oss << "pos_delta=" << posDelta - << " rot_delta_deg=" << rotDeltaDeg - << " current_pos=(" << actual.position.x << "," << actual.position.y << "," << actual.position.z << ")" - << " expected_pos=(" << expected.position.x << "," << expected.position.y << "," << expected.position.z << ")" - << " current_quat=(" << currentOrientation.x << "," << currentOrientation.y << "," << currentOrientation.z << "," << currentOrientation.w << ")" - << " expected_quat=(" << expectedOrientation.x << "," << expectedOrientation.y << "," << expectedOrientation.z << "," << expectedOrientation.w << ")"; - - if (actual.hasTargetPosition) - { - oss << " target=(" << actual.targetPosition.x << "," << actual.targetPosition.y << "," << actual.targetPosition.z << ")"; - if (actual.hasDistance) - oss << " distance=" << actual.distance; - if (actual.hasOrbitState) - oss << " orbit_u=" << actual.orbitU << " orbit_v=" << actual.orbitV; - } - else if (expected.hasTargetPosition || expected.hasDistance || expected.hasOrbitState) - { - oss << " spherical_state=unavailable"; - } - if (actual.hasPathState) - { - oss << " path_angle=" << actual.pathState.angle - << " path_radius=" << actual.pathState.radius - << " path_height=" << actual.pathState.height; - } - else if (expected.hasPathState) - { - oss << " path_state=unavailable"; - } - - if (actual.hasDynamicPerspectiveState) - { - oss << " dynamic_base_fov=" << actual.dynamicPerspectiveState.baseFov - << " dynamic_reference_distance=" << actual.dynamicPerspectiveState.referenceDistance; - } - else if (expected.hasDynamicPerspectiveState) - { - oss << " dynamic_perspective_state=unavailable"; - } - - return oss.str(); - } - inline std::string describeApplyResult(const CCameraGoalSolver::SApplyResult& result) const { std::ostringstream oss; @@ -1470,7 +1208,7 @@ class App final : public examples::SimpleWindowedApplication PresetUiAnalysis analysis; analysis.goal = makeGoalFromPreset(preset); analysis.hasActiveCamera = camera != nullptr; - analysis.finiteGoal = isGoalFinite(analysis.goal); + analysis.finiteGoal = nbl::hlsl::isGoalFinite(analysis.goal); analysis.canApply = analysis.hasActiveCamera && analysis.finiteGoal; if (analysis.hasActiveCamera) @@ -1521,22 +1259,23 @@ class App final : public examples::SimpleWindowedApplication inline CaptureUiAnalysis analyzeCameraCaptureForUi(ICamera* camera) const { CaptureUiAnalysis analysis; - analysis.hasActiveCamera = camera != nullptr; + const auto capture = m_cameraGoalSolver.captureDetailed(camera); + analysis.goal = capture.goal; + analysis.hasActiveCamera = capture.hasCamera; + analysis.capturedGoal = capture.captured; + analysis.finiteGoal = capture.finiteGoal; if (!analysis.hasActiveCamera) { analysis.policyLabel = "Blocked | no active camera"; return analysis; } - analysis.capturedGoal = m_cameraGoalSolver.capture(camera, analysis.goal); if (!analysis.capturedGoal) { analysis.policyLabel = "Blocked | goal capture failed"; return analysis; } - analysis.goal = canonicalizeGoal(analysis.goal); - analysis.finiteGoal = isGoalFinite(analysis.goal); if (!analysis.finiteGoal) { analysis.policyLabel = "Blocked | invalid goal state"; @@ -1572,52 +1311,27 @@ class App final : public examples::SimpleWindowedApplication return analyzePresetForUi(camera, preset).matchesFilter(m_presetFilterMode); } - inline bool isGoalFinite(const CCameraGoal& goal) const - { - auto isFiniteQuat = [](const glm::quat& q) -> bool - { - return std::isfinite(q.x) && std::isfinite(q.y) && std::isfinite(q.z) && std::isfinite(q.w); - }; - - if (!isFiniteVec3(goal.position) || !isFiniteQuat(goal.orientation)) - return false; - if (goal.hasTargetPosition && !isFiniteVec3(goal.targetPosition)) - return false; - if (goal.hasDistance && !std::isfinite(goal.distance)) - return false; - if (goal.hasOrbitState && (!std::isfinite(goal.orbitU) || !std::isfinite(goal.orbitV) || !std::isfinite(goal.orbitDistance))) - return false; - if (goal.hasPathState && (!std::isfinite(goal.pathState.angle) || !std::isfinite(goal.pathState.radius) || !std::isfinite(goal.pathState.height))) - return false; - if (goal.hasDynamicPerspectiveState && - (!std::isfinite(goal.dynamicPerspectiveState.baseFov) || !std::isfinite(goal.dynamicPerspectiveState.referenceDistance))) - return false; - return true; - } - inline bool comparePresetToCameraState(ICamera* camera, const CameraPreset& preset, const double posEps, const double rotEpsDeg, const double scalarEps) const { - if (!camera) + const auto capture = m_cameraGoalSolver.captureDetailed(camera); + if (!capture.canUseGoal()) return false; - CCameraGoal actual; - if (!tryCaptureGoal(camera, actual)) - return false; - - return compareGoals(actual, makeGoalFromPreset(preset), posEps, rotEpsDeg, scalarEps); + return nbl::hlsl::compareGoals(capture.goal, makeGoalFromPreset(preset), posEps, rotEpsDeg, scalarEps); } inline std::string describePresetCameraMismatch(ICamera* camera, const CameraPreset& preset) const { - if (!camera) + const auto capture = m_cameraGoalSolver.captureDetailed(camera); + if (!capture.hasCamera) return "camera=null"; - - CCameraGoal actual; - if (!tryCaptureGoal(camera, actual)) + if (!capture.captured) return "goal_state=unavailable"; + if (!capture.finiteGoal) + return "goal_state=invalid"; - return describeGoalMismatch(actual, makeGoalFromPreset(preset)); + return nbl::hlsl::describeGoalMismatch(capture.goal, makeGoalFromPreset(preset)); } inline nbl_json serializeGoal(const CCameraGoal& goal) const @@ -2032,7 +1746,7 @@ class App final : public examples::SimpleWindowedApplication const double alpha = static_cast(clampedTime - a.time) / static_cast(b.time - a.time); preset = a.preset; - assignGoalToPreset(preset, blendGoals(makeGoalFromPreset(a.preset), makeGoalFromPreset(b.preset), alpha)); + assignGoalToPreset(preset, nbl::hlsl::blendGoals(makeGoalFromPreset(a.preset), makeGoalFromPreset(b.preset), alpha)); return true; } diff --git a/common/include/camera/CCameraGoal.hpp b/common/include/camera/CCameraGoal.hpp new file mode 100644 index 000000000..6110a36f7 --- /dev/null +++ b/common/include/camera/CCameraGoal.hpp @@ -0,0 +1,350 @@ +// 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_GOAL_HPP_ +#define _C_CAMERA_GOAL_HPP_ + +#include +#include +#include +#include + +#include "ICamera.hpp" +#include "CSphericalTargetCamera.hpp" +#include "glm/glm/gtc/quaternion.hpp" + +namespace nbl::hlsl +{ + +struct CCameraGoal +{ + float64_t3 position = float64_t3(0.0); + glm::quat orientation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f); + ICamera::CameraKind sourceKind = ICamera::CameraKind::Unknown; + uint32_t sourceCapabilities = ICamera::None; + uint32_t sourceGoalStateMask = ICamera::GoalStateNone; + bool hasTargetPosition = false; + float64_t3 targetPosition = float64_t3(0.0); + bool hasDistance = false; + float distance = 0.f; + bool hasOrbitState = false; + double orbitU = 0.0; + double orbitV = 0.0; + float orbitDistance = 0.f; + bool hasPathState = false; + ICamera::PathState pathState = {}; + bool hasDynamicPerspectiveState = false; + ICamera::DynamicPerspectiveState dynamicPerspectiveState = {}; +}; + +inline double wrapAngleRad(double angle) +{ + constexpr double Pi = 3.14159265358979323846; + while (angle > Pi) + angle -= 2.0 * Pi; + while (angle < -Pi) + angle += 2.0 * Pi; + return angle; +} + +inline double lerpWrappedAngleRad(double a, double b, double alpha) +{ + return a + wrapAngleRad(b - a) * alpha; +} + +inline bool nearlyEqualGoalScalar(double a, double b, double eps = 1e-6) +{ + return std::abs(a - b) <= eps; +} + +inline uint32_t getRequiredGoalStateMask(const CCameraGoal& target) +{ + uint32_t mask = ICamera::GoalStateNone; + if (target.hasTargetPosition || target.hasDistance || target.hasOrbitState) + mask |= ICamera::GoalStateSphericalTarget; + if (target.hasDynamicPerspectiveState) + mask |= ICamera::GoalStateDynamicPerspective; + if (target.hasPathState) + mask |= ICamera::GoalStatePath; + return mask; +} + +inline bool applyCanonicalPathGoal(CCameraGoal& goal) +{ + if (!(goal.hasPathState && goal.hasTargetPosition)) + return false; + if (!std::isfinite(goal.pathState.angle) || !std::isfinite(goal.pathState.radius) || !std::isfinite(goal.pathState.height)) + return false; + + const float64_t3 offset( + std::cos(goal.pathState.angle) * goal.pathState.radius, + goal.pathState.height, + std::sin(goal.pathState.angle) * goal.pathState.radius); + const double distance = length(offset); + if (!std::isfinite(distance) || distance <= 1e-9) + return false; + + const float appliedDistance = std::clamp( + static_cast(distance), + CSphericalTargetCamera::MinDistance, + CSphericalTargetCamera::MaxDistance); + const auto local = offset / static_cast(appliedDistance); + goal.orbitU = std::atan2(local.y, local.x); + goal.orbitV = std::asin(std::clamp(local.z, -1.0, 1.0)); + + const float64_t3 spherePosition( + std::cos(goal.orbitV) * std::cos(goal.orbitU) * static_cast(appliedDistance), + std::cos(goal.orbitV) * std::sin(goal.orbitU) * static_cast(appliedDistance), + std::sin(goal.orbitV) * static_cast(appliedDistance)); + + goal.position = goal.targetPosition + spherePosition; + goal.hasDistance = true; + goal.distance = appliedDistance; + goal.hasOrbitState = true; + goal.orbitDistance = appliedDistance; + + const auto forward = normalize(-spherePosition); + const float64_t3 up = normalize(float64_t3( + -std::sin(goal.orbitV) * std::cos(goal.orbitU), + -std::sin(goal.orbitV) * std::sin(goal.orbitU), + std::cos(goal.orbitV))); + const float64_t3 right = normalize(cross(up, forward)); + goal.orientation = glm::quat_cast(glm::dmat3{ right, up, forward }); + return true; +} + +inline CCameraGoal canonicalizeGoal(CCameraGoal goal) +{ + applyCanonicalPathGoal(goal); + return goal; +} + +template +inline bool isFiniteVec3(const Vec& v) +{ + return std::isfinite(v.x) && std::isfinite(v.y) && std::isfinite(v.z); +} + +template +inline bool nearlyEqualVec3(const VecA& a, const VecB& b, const double epsilon) +{ + return std::abs(static_cast(a.x - b.x)) <= epsilon && + std::abs(static_cast(a.y - b.y)) <= epsilon && + std::abs(static_cast(a.z - b.z)) <= epsilon; +} + +inline bool isGoalFinite(const CCameraGoal& goal) +{ + auto isFiniteQuat = [](const glm::quat& q) -> bool + { + return std::isfinite(q.x) && std::isfinite(q.y) && std::isfinite(q.z) && std::isfinite(q.w); + }; + + if (!isFiniteVec3(goal.position) || !isFiniteQuat(goal.orientation)) + return false; + if (goal.hasTargetPosition && !isFiniteVec3(goal.targetPosition)) + return false; + if (goal.hasDistance && !std::isfinite(goal.distance)) + return false; + if (goal.hasOrbitState && (!std::isfinite(goal.orbitU) || !std::isfinite(goal.orbitV) || !std::isfinite(goal.orbitDistance))) + return false; + if (goal.hasPathState && (!std::isfinite(goal.pathState.angle) || !std::isfinite(goal.pathState.radius) || !std::isfinite(goal.pathState.height))) + return false; + if (goal.hasDynamicPerspectiveState && + (!std::isfinite(goal.dynamicPerspectiveState.baseFov) || !std::isfinite(goal.dynamicPerspectiveState.referenceDistance))) + return false; + return true; +} + +inline bool compareGoals(const CCameraGoal& actual, const CCameraGoal& expected, + const double posEps, const double rotEpsDeg, const double scalarEps) +{ + auto isFiniteQuat = [](const glm::quat& q) -> bool + { + return std::isfinite(q.x) && std::isfinite(q.y) && std::isfinite(q.z) && std::isfinite(q.w); + }; + + auto angleDiffRad = [](double a, double b) -> double + { + constexpr double Pi = 3.14159265358979323846; + constexpr double TwoPi = 6.28318530717958647692; + double d = std::fmod(a - b + Pi, TwoPi); + if (d < 0.0) + d += TwoPi; + return std::abs(d - Pi); + }; + + const auto currentOrientation = glm::normalize(actual.orientation); + const auto expectedOrientation = glm::normalize(expected.orientation); + if (!isFiniteVec3(actual.position) || !isFiniteVec3(expected.position) || !isFiniteQuat(currentOrientation) || !isFiniteQuat(expectedOrientation)) + return false; + + const double dx = static_cast(actual.position.x - expected.position.x); + const double dy = static_cast(actual.position.y - expected.position.y); + const double dz = static_cast(actual.position.z - expected.position.z); + const double posDelta = std::sqrt(dx * dx + dy * dy + dz * dz); + const double orientationDot = std::clamp(static_cast(std::abs(glm::dot(currentOrientation, expectedOrientation))), 0.0, 1.0); + const double rotDeltaDeg = glm::degrees(2.0 * std::acos(orientationDot)); + if (posDelta > posEps || rotDeltaDeg > rotEpsDeg) + return false; + + if (expected.hasTargetPosition) + { + if (!actual.hasTargetPosition || !nearlyEqualVec3(actual.targetPosition, expected.targetPosition, scalarEps)) + return false; + } + if (expected.hasDistance) + { + if (!actual.hasDistance || std::abs(static_cast(actual.distance - expected.distance)) > scalarEps) + return false; + } + if (expected.hasOrbitState) + { + if (!actual.hasOrbitState) + return false; + if (angleDiffRad(expected.orbitU, actual.orbitU) > rotEpsDeg * (3.14159265358979323846 / 180.0)) + return false; + if (angleDiffRad(expected.orbitV, actual.orbitV) > rotEpsDeg * (3.14159265358979323846 / 180.0)) + return false; + if (std::abs(static_cast(actual.orbitDistance - expected.orbitDistance)) > scalarEps) + return false; + } + if (expected.hasPathState) + { + if (!actual.hasPathState) + return false; + if (std::abs(wrapAngleRad(expected.pathState.angle - actual.pathState.angle)) > rotEpsDeg * (3.14159265358979323846 / 180.0)) + return false; + if (std::abs(actual.pathState.radius - expected.pathState.radius) > scalarEps) + return false; + if (std::abs(actual.pathState.height - expected.pathState.height) > scalarEps) + return false; + } + if (expected.hasDynamicPerspectiveState) + { + if (!actual.hasDynamicPerspectiveState) + return false; + if (std::abs(static_cast(actual.dynamicPerspectiveState.baseFov - expected.dynamicPerspectiveState.baseFov)) > scalarEps) + return false; + if (std::abs(static_cast(actual.dynamicPerspectiveState.referenceDistance - expected.dynamicPerspectiveState.referenceDistance)) > scalarEps) + return false; + } + + return true; +} + +inline std::string describeGoalMismatch(const CCameraGoal& actual, const CCameraGoal& expected) +{ + std::ostringstream oss; + const auto currentOrientation = glm::normalize(actual.orientation); + const auto expectedOrientation = glm::normalize(expected.orientation); + const double dx = static_cast(actual.position.x - expected.position.x); + const double dy = static_cast(actual.position.y - expected.position.y); + const double dz = static_cast(actual.position.z - expected.position.z); + const double posDelta = std::sqrt(dx * dx + dy * dy + dz * dz); + const double orientationDot = std::clamp(static_cast(std::abs(glm::dot(currentOrientation, expectedOrientation))), 0.0, 1.0); + const double rotDeltaDeg = glm::degrees(2.0 * std::acos(orientationDot)); + oss << "pos_delta=" << posDelta + << " rot_delta_deg=" << rotDeltaDeg + << " current_pos=(" << actual.position.x << "," << actual.position.y << "," << actual.position.z << ")" + << " expected_pos=(" << expected.position.x << "," << expected.position.y << "," << expected.position.z << ")" + << " current_quat=(" << currentOrientation.x << "," << currentOrientation.y << "," << currentOrientation.z << "," << currentOrientation.w << ")" + << " expected_quat=(" << expectedOrientation.x << "," << expectedOrientation.y << "," << expectedOrientation.z << "," << expectedOrientation.w << ")"; + + if (actual.hasTargetPosition) + { + oss << " target=(" << actual.targetPosition.x << "," << actual.targetPosition.y << "," << actual.targetPosition.z << ")"; + if (actual.hasDistance) + oss << " distance=" << actual.distance; + if (actual.hasOrbitState) + oss << " orbit_u=" << actual.orbitU << " orbit_v=" << actual.orbitV; + } + else if (expected.hasTargetPosition || expected.hasDistance || expected.hasOrbitState) + { + oss << " spherical_state=unavailable"; + } + if (actual.hasPathState) + { + oss << " path_angle=" << actual.pathState.angle + << " path_radius=" << actual.pathState.radius + << " path_height=" << actual.pathState.height; + } + else if (expected.hasPathState) + { + oss << " path_state=unavailable"; + } + + if (actual.hasDynamicPerspectiveState) + { + oss << " dynamic_base_fov=" << actual.dynamicPerspectiveState.baseFov + << " dynamic_reference_distance=" << actual.dynamicPerspectiveState.referenceDistance; + } + else if (expected.hasDynamicPerspectiveState) + { + oss << " dynamic_perspective_state=unavailable"; + } + + return oss.str(); +} + +inline CCameraGoal blendGoals(const CCameraGoal& a, const CCameraGoal& b, double alpha) +{ + CCameraGoal blended; + blended.position = a.position + (b.position - a.position) * alpha; + blended.orientation = glm::slerp(a.orientation, b.orientation, static_cast(alpha)); + blended.sourceKind = (a.sourceKind == b.sourceKind) ? a.sourceKind : ICamera::CameraKind::Unknown; + blended.sourceCapabilities = a.sourceCapabilities & b.sourceCapabilities; + blended.sourceGoalStateMask = a.sourceGoalStateMask | b.sourceGoalStateMask; + blended.hasTargetPosition = a.hasTargetPosition || b.hasTargetPosition; + if (blended.hasTargetPosition) + { + const auto ta = a.hasTargetPosition ? a.targetPosition : b.targetPosition; + const auto tb = b.hasTargetPosition ? b.targetPosition : a.targetPosition; + blended.targetPosition = ta + (tb - ta) * alpha; + } + blended.hasDistance = a.hasDistance || b.hasDistance; + if (blended.hasDistance) + { + const float da = a.hasDistance ? a.distance : b.distance; + const float db = b.hasDistance ? b.distance : a.distance; + blended.distance = da + (db - da) * static_cast(alpha); + } + blended.hasOrbitState = a.hasOrbitState || b.hasOrbitState; + if (blended.hasOrbitState) + { + const double ua = a.hasOrbitState ? a.orbitU : b.orbitU; + const double ub = b.hasOrbitState ? b.orbitU : a.orbitU; + const double va = a.hasOrbitState ? a.orbitV : b.orbitV; + const double vb = b.hasOrbitState ? b.orbitV : a.orbitV; + const float da = a.hasOrbitState ? a.orbitDistance : b.orbitDistance; + const float db = b.hasOrbitState ? b.orbitDistance : a.orbitDistance; + + blended.orbitU = lerpWrappedAngleRad(ua, ub, alpha); + blended.orbitV = lerpWrappedAngleRad(va, vb, alpha); + blended.orbitDistance = da + (db - da) * static_cast(alpha); + } + blended.hasDynamicPerspectiveState = a.hasDynamicPerspectiveState || b.hasDynamicPerspectiveState; + if (blended.hasDynamicPerspectiveState) + { + const auto dynamicA = a.hasDynamicPerspectiveState ? a.dynamicPerspectiveState : b.dynamicPerspectiveState; + const auto dynamicB = b.hasDynamicPerspectiveState ? b.dynamicPerspectiveState : a.dynamicPerspectiveState; + blended.dynamicPerspectiveState.baseFov = dynamicA.baseFov + (dynamicB.baseFov - dynamicA.baseFov) * static_cast(alpha); + blended.dynamicPerspectiveState.referenceDistance = + dynamicA.referenceDistance + (dynamicB.referenceDistance - dynamicA.referenceDistance) * static_cast(alpha); + } + blended.hasPathState = a.hasPathState || b.hasPathState; + if (blended.hasPathState) + { + const auto pathA = a.hasPathState ? a.pathState : b.pathState; + const auto pathB = b.hasPathState ? b.pathState : a.pathState; + blended.pathState.angle = lerpWrappedAngleRad(pathA.angle, pathB.angle, alpha); + blended.pathState.radius = pathA.radius + (pathB.radius - pathA.radius) * alpha; + blended.pathState.height = pathA.height + (pathB.height - pathA.height) * alpha; + } + return canonicalizeGoal(blended); +} + +} // namespace nbl::hlsl + +#endif // _C_CAMERA_GOAL_HPP_ diff --git a/common/include/camera/CCameraGoalSolver.hpp b/common/include/camera/CCameraGoalSolver.hpp index 357f0420d..885dac876 100644 --- a/common/include/camera/CCameraGoalSolver.hpp +++ b/common/include/camera/CCameraGoalSolver.hpp @@ -6,7 +6,7 @@ #include #include -#include "ICamera.hpp" +#include "CCameraGoal.hpp" #include "CFPSCamera.hpp" #include "CFreeLockCamera.hpp" #include "CSphericalTargetCamera.hpp" @@ -15,30 +15,22 @@ namespace nbl::hlsl { -struct CCameraGoal -{ - float64_t3 position = float64_t3(0.0); - glm::quat orientation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f); - ICamera::CameraKind sourceKind = ICamera::CameraKind::Unknown; - uint32_t sourceCapabilities = ICamera::None; - uint32_t sourceGoalStateMask = ICamera::GoalStateNone; - bool hasTargetPosition = false; - float64_t3 targetPosition = float64_t3(0.0); - bool hasDistance = false; - float distance = 0.f; - bool hasOrbitState = false; - double orbitU = 0.0; - double orbitV = 0.0; - float orbitDistance = 0.f; - bool hasPathState = false; - ICamera::PathState pathState = {}; - bool hasDynamicPerspectiveState = false; - ICamera::DynamicPerspectiveState dynamicPerspectiveState = {}; -}; - class CCameraGoalSolver { public: + struct SCaptureResult + { + bool hasCamera = false; + bool captured = false; + bool finiteGoal = false; + CCameraGoal goal = {}; + + inline bool canUseGoal() const + { + return hasCamera && captured && finiteGoal; + } + }; + struct SCompatibilityResult { bool sameKind = false; @@ -104,10 +96,12 @@ class CCameraGoalSolver if (!camera) return false; + const auto canonicalTarget = canonicalizeGoal(target); + if (camera->hasCapability(ICamera::SphericalTarget)) - return buildSphericalEvents(camera, target, out); + return buildSphericalEvents(camera, canonicalTarget, out); - return buildFreeEvents(camera, target, out); + return buildFreeEvents(camera, canonicalTarget, out); } bool capture(ICamera* camera, CCameraGoal& out) const @@ -150,18 +144,32 @@ class CCameraGoalSolver out.pathState = pathState; } + out = canonicalizeGoal(out); return true; } + SCaptureResult captureDetailed(ICamera* camera) const + { + SCaptureResult result; + result.hasCamera = camera != nullptr; + if (!result.hasCamera) + return result; + + result.captured = capture(camera, result.goal); + result.finiteGoal = result.captured && isGoalFinite(result.goal); + return result; + } + SCompatibilityResult analyzeCompatibility(const ICamera* camera, const CCameraGoal& target) const { SCompatibilityResult result; if (!camera) return result; - result.sameKind = target.sourceKind == ICamera::CameraKind::Unknown || target.sourceKind == camera->getKind(); + const auto canonicalTarget = canonicalizeGoal(target); + result.sameKind = canonicalTarget.sourceKind == ICamera::CameraKind::Unknown || canonicalTarget.sourceKind == camera->getKind(); result.supportedGoalStateMask = camera->getGoalStateMask(); - result.requiredGoalStateMask = getRequiredGoalStateMask(target); + result.requiredGoalStateMask = getRequiredGoalStateMask(canonicalTarget); result.missingGoalStateMask = result.requiredGoalStateMask & ~result.supportedGoalStateMask; result.exact = result.missingGoalStateMask == ICamera::GoalStateNone; return result; @@ -173,6 +181,8 @@ class CCameraGoalSolver if (!camera) return result; + const auto canonicalTarget = canonicalizeGoal(target); + bool exact = true; bool absoluteChanged = false; @@ -180,11 +190,11 @@ class CCameraGoalSolver { bool poseChanged = false; bool poseExact = false; - if (tryApplyAbsoluteReferencePose(camera, target, poseChanged, poseExact)) + if (tryApplyAbsoluteReferencePose(camera, canonicalTarget, poseChanged, poseExact)) { result.issues |= SApplyResult::UsedAbsolutePoseFallback; absoluteChanged = absoluteChanged || poseChanged; - if (poseExact && !target.hasDynamicPerspectiveState) + if (poseExact && !canonicalTarget.hasDynamicPerspectiveState) { result.status = poseChanged ? SApplyResult::EStatus::AppliedAbsoluteOnly : @@ -195,7 +205,7 @@ class CCameraGoalSolver } } - if (target.hasTargetPosition) + if (canonicalTarget.hasTargetPosition) { ICamera::SphericalTargetState beforeState; if (!camera->tryGetSphericalTargetState(beforeState)) @@ -206,7 +216,7 @@ class CCameraGoalSolver else { const auto beforeTarget = beforeState.target; - if (!camera->trySetSphericalTarget(target.targetPosition)) + if (!camera->trySetSphericalTarget(canonicalTarget.targetPosition)) { result.issues |= SApplyResult::MissingSphericalTargetState; exact = false; @@ -222,13 +232,13 @@ class CCameraGoalSolver else { absoluteChanged = afterState.target != beforeTarget; - exact = exact && afterState.target == target.targetPosition; + exact = exact && afterState.target == canonicalTarget.targetPosition; } } } } - if (target.hasDistance || target.hasOrbitState) + if (canonicalTarget.hasDistance || canonicalTarget.hasOrbitState) { ICamera::SphericalTargetState beforeState; if (!camera->tryGetSphericalTargetState(beforeState)) @@ -238,7 +248,7 @@ class CCameraGoalSolver } else { - const float desiredDistance = target.hasOrbitState ? target.orbitDistance : target.distance; + const float desiredDistance = canonicalTarget.hasOrbitState ? canonicalTarget.orbitDistance : canonicalTarget.distance; const float beforeDistance = beforeState.distance; if (!camera->trySetSphericalDistance(desiredDistance)) { @@ -262,7 +272,7 @@ class CCameraGoalSolver } } - if (target.hasPathState) + if (canonicalTarget.hasPathState) { ICamera::PathState beforeState; if (!camera->tryGetPathState(beforeState)) @@ -270,7 +280,7 @@ class CCameraGoalSolver result.issues |= SApplyResult::MissingPathState; exact = false; } - else if (!camera->trySetPathState(target.pathState)) + else if (!camera->trySetPathState(canonicalTarget.pathState)) { result.issues |= SApplyResult::MissingPathState; exact = false; @@ -285,12 +295,12 @@ class CCameraGoalSolver } else { - const bool pathChanged = !nearlyEqual(beforeState.angle, afterState.angle) || - !nearlyEqual(beforeState.radius, afterState.radius) || - !nearlyEqual(beforeState.height, afterState.height); - const bool pathExact = nearlyEqual(afterState.angle, target.pathState.angle) && - nearlyEqual(afterState.radius, target.pathState.radius) && - nearlyEqual(afterState.height, target.pathState.height); + const bool pathChanged = !nearlyEqualGoalScalar(beforeState.angle, afterState.angle) || + !nearlyEqualGoalScalar(beforeState.radius, afterState.radius) || + !nearlyEqualGoalScalar(beforeState.height, afterState.height); + const bool pathExact = nearlyEqualGoalScalar(afterState.angle, canonicalTarget.pathState.angle) && + nearlyEqualGoalScalar(afterState.radius, canonicalTarget.pathState.radius) && + nearlyEqualGoalScalar(afterState.height, canonicalTarget.pathState.height); absoluteChanged = absoluteChanged || pathChanged; exact = exact && pathExact; @@ -298,7 +308,7 @@ class CCameraGoalSolver } } - if (target.hasDynamicPerspectiveState) + if (canonicalTarget.hasDynamicPerspectiveState) { ICamera::DynamicPerspectiveState beforeState; if (!camera->tryGetDynamicPerspectiveState(beforeState)) @@ -306,7 +316,7 @@ class CCameraGoalSolver result.issues |= SApplyResult::MissingDynamicPerspectiveState; exact = false; } - else if (!camera->trySetDynamicPerspectiveState(target.dynamicPerspectiveState)) + else if (!camera->trySetDynamicPerspectiveState(canonicalTarget.dynamicPerspectiveState)) { result.issues |= SApplyResult::MissingDynamicPerspectiveState; exact = false; @@ -321,10 +331,10 @@ class CCameraGoalSolver } else { - const bool dynamicChanged = !nearlyEqual(beforeState.baseFov, afterState.baseFov) || - !nearlyEqual(beforeState.referenceDistance, afterState.referenceDistance); - const bool dynamicExact = nearlyEqual(afterState.baseFov, target.dynamicPerspectiveState.baseFov) && - nearlyEqual(afterState.referenceDistance, target.dynamicPerspectiveState.referenceDistance); + const bool dynamicChanged = !nearlyEqualGoalScalar(beforeState.baseFov, afterState.baseFov) || + !nearlyEqualGoalScalar(beforeState.referenceDistance, afterState.referenceDistance); + const bool dynamicExact = nearlyEqualGoalScalar(afterState.baseFov, canonicalTarget.dynamicPerspectiveState.baseFov) && + nearlyEqualGoalScalar(afterState.referenceDistance, canonicalTarget.dynamicPerspectiveState.referenceDistance); absoluteChanged = absoluteChanged || dynamicChanged; exact = exact && dynamicExact; @@ -333,7 +343,7 @@ class CCameraGoalSolver } std::vector events; - buildEvents(camera, target, events); + buildEvents(camera, canonicalTarget, events); result.eventCount = static_cast(events.size()); result.exact = exact; @@ -384,32 +394,6 @@ class CCameraGoalSolver float distance = 0.f; }; - inline double wrapAngleRad(double angle) const - { - while (angle > Pi) - angle -= 2.0 * Pi; - while (angle < -Pi) - angle += 2.0 * Pi; - return angle; - } - - inline bool nearlyEqual(double a, double b, double eps = 1e-6) const - { - return std::abs(a - b) <= eps; - } - - inline uint32_t getRequiredGoalStateMask(const CCameraGoal& target) const - { - uint32_t mask = ICamera::GoalStateNone; - if (target.hasTargetPosition || target.hasDistance || target.hasOrbitState) - mask |= ICamera::GoalStateSphericalTarget; - if (target.hasDynamicPerspectiveState) - mask |= ICamera::GoalStateDynamicPerspective; - if (target.hasPathState) - mask |= ICamera::GoalStatePath; - return mask; - } - inline void appendSignedEvent(std::vector& events, double value, CVirtualGimbalEvent::VirtualEventType positive, CVirtualGimbalEvent::VirtualEventType negative) const { From 71a5ddf9c187cc0e86082f68972101bd99395d79 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 16:32:38 +0200 Subject: [PATCH 141/205] Promote camera presets into shared camera headers --- 61_UI/include/app/App.hpp | 152 ++-------------------- 61_UI/include/common.hpp | 3 + common/include/camera/CCameraPreset.hpp | 163 ++++++++++++++++++++++++ 3 files changed, 176 insertions(+), 142 deletions(-) create mode 100644 common/include/camera/CCameraPreset.hpp diff --git a/61_UI/include/app/App.hpp b/61_UI/include/app/App.hpp index 7cf1879c9..75a59718e 100644 --- a/61_UI/include/app/App.hpp +++ b/61_UI/include/app/App.hpp @@ -480,18 +480,8 @@ class App final : public examples::SimpleWindowedApplication std::string line; }; - struct CameraPreset - { - std::string name; - std::string identifier; - CCameraGoal goal = {}; - }; - - struct CameraKeyframe - { - CameraPreset preset; - float time = 0.f; - }; + using CameraPreset = CCameraPreset; + using CameraKeyframe = CCameraKeyframe; enum class PresetFilterMode : uint8_t { @@ -1125,16 +1115,6 @@ class App final : public examples::SimpleWindowedApplication projection.setPerspective(params.m_zNear, params.m_zFar, dynamicFov); } - inline void assignGoalToPreset(CameraPreset& preset, const CCameraGoal& goal) const - { - preset.goal = nbl::hlsl::canonicalizeGoal(goal); - } - - inline CCameraGoal makeGoalFromPreset(const CameraPreset& preset) const - { - return nbl::hlsl::canonicalizeGoal(preset.goal); - } - inline bool tryCaptureGoal(ICamera* camera, CCameraGoal& out) const { const auto capture = m_cameraGoalSolver.captureDetailed(camera); @@ -1206,7 +1186,7 @@ class App final : public examples::SimpleWindowedApplication inline PresetUiAnalysis analyzePresetForUi(ICamera* camera, const CameraPreset& preset) const { PresetUiAnalysis analysis; - analysis.goal = makeGoalFromPreset(preset); + analysis.goal = nbl::hlsl::makeGoalFromPreset(preset); analysis.hasActiveCamera = camera != nullptr; analysis.finiteGoal = nbl::hlsl::isGoalFinite(analysis.goal); analysis.canApply = analysis.hasActiveCamera && analysis.finiteGoal; @@ -1318,7 +1298,7 @@ class App final : public examples::SimpleWindowedApplication if (!capture.canUseGoal()) return false; - return nbl::hlsl::compareGoals(capture.goal, makeGoalFromPreset(preset), posEps, rotEpsDeg, scalarEps); + return nbl::hlsl::compareGoals(capture.goal, nbl::hlsl::makeGoalFromPreset(preset), posEps, rotEpsDeg, scalarEps); } inline std::string describePresetCameraMismatch(ICamera* camera, const CameraPreset& preset) const @@ -1331,108 +1311,7 @@ class App final : public examples::SimpleWindowedApplication if (!capture.finiteGoal) return "goal_state=invalid"; - return nbl::hlsl::describeGoalMismatch(capture.goal, makeGoalFromPreset(preset)); - } - - inline nbl_json serializeGoal(const CCameraGoal& goal) const - { - nbl_json j; - j["position"] = { goal.position.x, goal.position.y, goal.position.z }; - j["orientation"] = { goal.orientation.x, goal.orientation.y, goal.orientation.z, goal.orientation.w }; - j["camera_kind"] = static_cast(goal.sourceKind); - j["camera_capabilities"] = goal.sourceCapabilities; - j["camera_goal_state_mask"] = goal.sourceGoalStateMask; - if (goal.hasTargetPosition) - j["target_position"] = { goal.targetPosition.x, goal.targetPosition.y, goal.targetPosition.z }; - if (goal.hasDistance) - j["distance"] = goal.distance; - if (goal.hasOrbitState) - { - j["orbit_u"] = goal.orbitU; - j["orbit_v"] = goal.orbitV; - j["orbit_distance"] = goal.orbitDistance; - } - if (goal.hasPathState) - { - j["path_angle"] = goal.pathState.angle; - j["path_radius"] = goal.pathState.radius; - j["path_height"] = goal.pathState.height; - } - if (goal.hasDynamicPerspectiveState) - { - j["dynamic_base_fov"] = goal.dynamicPerspectiveState.baseFov; - j["dynamic_reference_distance"] = goal.dynamicPerspectiveState.referenceDistance; - } - return j; - } - - inline void deserializeGoal(const nbl_json& entry, CCameraGoal& goal) const - { - goal = {}; - if (entry.contains("camera_kind")) - goal.sourceKind = static_cast(entry["camera_kind"].get()); - if (entry.contains("camera_capabilities")) - goal.sourceCapabilities = entry["camera_capabilities"].get(); - if (entry.contains("camera_goal_state_mask")) - goal.sourceGoalStateMask = entry["camera_goal_state_mask"].get(); - if (entry.contains("position") && entry["position"].is_array()) - { - auto arr = entry["position"]; - goal.position = float64_t3(arr[0].get(), arr[1].get(), arr[2].get()); - } - if (entry.contains("orientation") && entry["orientation"].is_array()) - { - auto arr = entry["orientation"]; - goal.orientation = glm::quat( - arr[3].get(), - arr[0].get(), - arr[1].get(), - arr[2].get() - ); - } - if (entry.contains("target_position") && entry["target_position"].is_array()) - { - auto arr = entry["target_position"]; - goal.targetPosition = float64_t3(arr[0].get(), arr[1].get(), arr[2].get()); - goal.hasTargetPosition = true; - } - if (entry.contains("distance")) - { - goal.distance = entry["distance"].get(); - goal.hasDistance = true; - } - if (entry.contains("orbit_u")) - { - goal.orbitU = entry["orbit_u"].get(); - goal.hasOrbitState = true; - } - if (entry.contains("orbit_v")) - { - goal.orbitV = entry["orbit_v"].get(); - goal.hasOrbitState = true; - } - if (entry.contains("orbit_distance")) - { - goal.orbitDistance = entry["orbit_distance"].get(); - goal.hasOrbitState = true; - } - if (entry.contains("path_angle") && entry.contains("path_radius") && entry.contains("path_height")) - { - goal.pathState.angle = entry["path_angle"].get(); - goal.pathState.radius = entry["path_radius"].get(); - goal.pathState.height = entry["path_height"].get(); - goal.hasPathState = true; - } - if (entry.contains("dynamic_base_fov")) - { - goal.dynamicPerspectiveState.baseFov = entry["dynamic_base_fov"].get(); - goal.hasDynamicPerspectiveState = true; - } - if (entry.contains("dynamic_reference_distance")) - { - goal.dynamicPerspectiveState.referenceDistance = entry["dynamic_reference_distance"].get(); - goal.hasDynamicPerspectiveState = true; - } + return nbl::hlsl::describeGoalMismatch(capture.goal, nbl::hlsl::makeGoalFromPreset(preset)); } inline bool tryCapturePreset(ICamera* camera, const std::string& name, CameraPreset& preset) @@ -1444,7 +1323,7 @@ class App final : public examples::SimpleWindowedApplication return false; preset.identifier = std::string(camera->getIdentifier()); - assignGoalToPreset(preset, captureUi.goal); + nbl::hlsl::assignGoalToPreset(preset, captureUi.goal); return true; } @@ -1460,7 +1339,7 @@ class App final : public examples::SimpleWindowedApplication if (!camera) return {}; - return m_cameraGoalSolver.applyDetailed(camera, makeGoalFromPreset(preset)); + return m_cameraGoalSolver.applyDetailed(camera, nbl::hlsl::makeGoalFromPreset(preset)); } inline CCameraGoalSolver::SApplyResult applyPresetFromUi(ICamera* camera, const CameraPreset& preset) @@ -1746,7 +1625,7 @@ class App final : public examples::SimpleWindowedApplication const double alpha = static_cast(clampedTime - a.time) / static_cast(b.time - a.time); preset = a.preset; - assignGoalToPreset(preset, nbl::hlsl::blendGoals(makeGoalFromPreset(a.preset), makeGoalFromPreset(b.preset), alpha)); + nbl::hlsl::assignGoalToPreset(preset, nbl::hlsl::blendGoals(nbl::hlsl::makeGoalFromPreset(a.preset), nbl::hlsl::makeGoalFromPreset(b.preset), alpha)); return true; } @@ -1797,12 +1676,7 @@ class App final : public examples::SimpleWindowedApplication root["presets"] = nbl_json::array(); for (const auto& preset : m_presets) - { - nbl_json j = serializeGoal(makeGoalFromPreset(preset)); - j["name"] = preset.name; - j["identifier"] = preset.identifier; - root["presets"].push_back(std::move(j)); - } + root["presets"].push_back(nbl::hlsl::serializePreset(preset)); std::ofstream out(path.string(), std::ios::binary); if (!out) @@ -1826,13 +1700,7 @@ class App final : public examples::SimpleWindowedApplication for (const auto& entry : root["presets"]) { CameraPreset preset; - if (entry.contains("name")) - preset.name = entry["name"].get(); - if (entry.contains("identifier")) - preset.identifier = entry["identifier"].get(); - CCameraGoal goal; - deserializeGoal(entry, goal); - assignGoalToPreset(preset, goal); + nbl::hlsl::deserializePreset(entry, preset); m_presets.emplace_back(std::move(preset)); } diff --git a/61_UI/include/common.hpp b/61_UI/include/common.hpp index b6fa40602..7e263d9aa 100644 --- a/61_UI/include/common.hpp +++ b/61_UI/include/common.hpp @@ -18,6 +18,7 @@ #include "camera/CDollyCamera.hpp" #include "camera/CDollyZoomCamera.hpp" #include "camera/CPathCamera.hpp" +#include "camera/CCameraPreset.hpp" #include "camera/CCameraGoalSolver.hpp" #include "camera/CGimbalInputBinder.hpp" @@ -56,6 +57,8 @@ using nbl::hlsl::CDollyCamera; using nbl::hlsl::CDollyZoomCamera; using nbl::hlsl::CPathCamera; using nbl::hlsl::CCameraGoal; +using nbl::hlsl::CCameraPreset; +using nbl::hlsl::CCameraKeyframe; using nbl::hlsl::CCameraGoalSolver; using nbl::hlsl::CGimbalInputBinder; using nbl::hlsl::IGimbalBindingLayout; diff --git a/common/include/camera/CCameraPreset.hpp b/common/include/camera/CCameraPreset.hpp new file mode 100644 index 000000000..d055ced86 --- /dev/null +++ b/common/include/camera/CCameraPreset.hpp @@ -0,0 +1,163 @@ +// 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_PRESET_HPP_ +#define _C_CAMERA_PRESET_HPP_ + +#include + +#include "CCameraGoal.hpp" +#include "nlohmann/json.hpp" + +namespace nbl::hlsl +{ + +struct CCameraPreset +{ + std::string name; + std::string identifier; + CCameraGoal goal = {}; +}; + +struct CCameraKeyframe +{ + CCameraPreset preset; + float time = 0.f; +}; + +inline void assignGoalToPreset(CCameraPreset& preset, const CCameraGoal& goal) +{ + preset.goal = canonicalizeGoal(goal); +} + +inline CCameraGoal makeGoalFromPreset(const CCameraPreset& preset) +{ + return canonicalizeGoal(preset.goal); +} + +inline nlohmann::json serializeGoal(const CCameraGoal& goal) +{ + nlohmann::json j; + j["position"] = { goal.position.x, goal.position.y, goal.position.z }; + j["orientation"] = { goal.orientation.x, goal.orientation.y, goal.orientation.z, goal.orientation.w }; + j["camera_kind"] = static_cast(goal.sourceKind); + j["camera_capabilities"] = goal.sourceCapabilities; + j["camera_goal_state_mask"] = goal.sourceGoalStateMask; + if (goal.hasTargetPosition) + j["target_position"] = { goal.targetPosition.x, goal.targetPosition.y, goal.targetPosition.z }; + if (goal.hasDistance) + j["distance"] = goal.distance; + if (goal.hasOrbitState) + { + j["orbit_u"] = goal.orbitU; + j["orbit_v"] = goal.orbitV; + j["orbit_distance"] = goal.orbitDistance; + } + if (goal.hasPathState) + { + j["path_angle"] = goal.pathState.angle; + j["path_radius"] = goal.pathState.radius; + j["path_height"] = goal.pathState.height; + } + if (goal.hasDynamicPerspectiveState) + { + j["dynamic_base_fov"] = goal.dynamicPerspectiveState.baseFov; + j["dynamic_reference_distance"] = goal.dynamicPerspectiveState.referenceDistance; + } + return j; +} + +inline void deserializeGoal(const nlohmann::json& entry, CCameraGoal& goal) +{ + goal = {}; + if (entry.contains("camera_kind")) + goal.sourceKind = static_cast(entry["camera_kind"].get()); + if (entry.contains("camera_capabilities")) + goal.sourceCapabilities = entry["camera_capabilities"].get(); + if (entry.contains("camera_goal_state_mask")) + goal.sourceGoalStateMask = entry["camera_goal_state_mask"].get(); + if (entry.contains("position") && entry["position"].is_array()) + { + const auto& arr = entry["position"]; + goal.position = float64_t3(arr[0].get(), arr[1].get(), arr[2].get()); + } + if (entry.contains("orientation") && entry["orientation"].is_array()) + { + const auto& arr = entry["orientation"]; + goal.orientation = glm::quat( + arr[3].get(), + arr[0].get(), + arr[1].get(), + arr[2].get() + ); + } + if (entry.contains("target_position") && entry["target_position"].is_array()) + { + const auto& arr = entry["target_position"]; + goal.targetPosition = float64_t3(arr[0].get(), arr[1].get(), arr[2].get()); + goal.hasTargetPosition = true; + } + if (entry.contains("distance")) + { + goal.distance = entry["distance"].get(); + goal.hasDistance = true; + } + if (entry.contains("orbit_u")) + { + goal.orbitU = entry["orbit_u"].get(); + goal.hasOrbitState = true; + } + if (entry.contains("orbit_v")) + { + goal.orbitV = entry["orbit_v"].get(); + goal.hasOrbitState = true; + } + if (entry.contains("orbit_distance")) + { + goal.orbitDistance = entry["orbit_distance"].get(); + goal.hasOrbitState = true; + } + if (entry.contains("path_angle") && entry.contains("path_radius") && entry.contains("path_height")) + { + goal.pathState.angle = entry["path_angle"].get(); + goal.pathState.radius = entry["path_radius"].get(); + goal.pathState.height = entry["path_height"].get(); + goal.hasPathState = true; + } + if (entry.contains("dynamic_base_fov")) + { + goal.dynamicPerspectiveState.baseFov = entry["dynamic_base_fov"].get(); + goal.hasDynamicPerspectiveState = true; + } + if (entry.contains("dynamic_reference_distance")) + { + goal.dynamicPerspectiveState.referenceDistance = entry["dynamic_reference_distance"].get(); + goal.hasDynamicPerspectiveState = true; + } +} + +inline nlohmann::json serializePreset(const CCameraPreset& preset) +{ + auto j = serializeGoal(makeGoalFromPreset(preset)); + j["name"] = preset.name; + j["identifier"] = preset.identifier; + return j; +} + +inline void deserializePreset(const nlohmann::json& entry, CCameraPreset& preset) +{ + preset = {}; + if (entry.contains("name")) + preset.name = entry["name"].get(); + if (entry.contains("identifier")) + preset.identifier = entry["identifier"].get(); + + CCameraGoal goal; + deserializeGoal(entry, goal); + assignGoalToPreset(preset, goal); +} + +} // namespace nbl::hlsl + +#endif // _C_CAMERA_PRESET_HPP_ From a816d3235a6744b21736875b0e003cecb40cedc1 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 17:04:00 +0200 Subject: [PATCH 142/205] Improve playback keyframe authoring in 61_UI --- 61_UI/AppControlPanel.cpp | 116 ++++++++++++++++++++++++++++++++++++-- 61_UI/include/app/App.hpp | 85 ++++++++++++++++++++++++++++ 2 files changed, 197 insertions(+), 4 deletions(-) diff --git a/61_UI/AppControlPanel.cpp b/61_UI/AppControlPanel.cpp index 9d9f1e481..fccecd153 100644 --- a/61_UI/AppControlPanel.cpp +++ b/61_UI/AppControlPanel.cpp @@ -845,6 +845,7 @@ void App::DrawControlPanel() if (ImGui::BeginChild("PlaybackPanel", ImVec2(0, 0), true)) { ImGui::PushItemWidth(-1.0f); + auto* activeCamera = getActiveCamera(); DrawSectionHeader("PlaybackHeader", "Playback", accent); ImGui::Checkbox("Loop", &m_playback.loop); DrawHoverHint("Loop playback when it reaches the end"); @@ -878,22 +879,40 @@ void App::DrawControlPanel() const ImVec4 playbackColor = m_playbackApplyBanner.succeeded ? (m_playbackApplyBanner.approximate ? warn : good) : bad; ImGui::TextColored(playbackColor, "%s", m_playbackApplyBanner.summary.c_str()); } + if (!m_keyframes.empty()) + { + CameraPreset playbackPreviewPreset; + if (tryBuildPlaybackPresetAtTime(m_playback.time, playbackPreviewPreset)) + { + const auto playbackPreviewUi = analyzePresetForUi(activeCamera, playbackPreviewPreset); + const ImVec4 previewColor = !playbackPreviewUi.hasActiveCamera ? bad : (playbackPreviewUi.exact() ? good : warn); + ImGui::TextDisabled("Preview"); + ImGui::SameLine(); + ImGui::TextColored(playbackPreviewUi.canApply ? previewColor : bad, "%s", playbackPreviewUi.policyLabel.c_str()); + } + } DrawSectionHeader("KeyframesHeader", "Keyframes", accent); ImGui::InputFloat("New keyframe time", &m_newKeyframeTime, 0.1f, 1.f, "%.3f"); DrawHoverHint("Time value for new keyframe"); - auto* activeCamera = getActiveCamera(); + ImGui::SameLine(); + if (ImGui::Button("Use playback time")) + m_newKeyframeTime = m_playback.time; + DrawHoverHint("Set new keyframe time from current playback position"); const auto keyframeCaptureUi = analyzeCameraCaptureForUi(activeCamera); if (!keyframeCaptureUi.canCapture) ImGui::BeginDisabled(); if (ImGui::Button("Add keyframe")) { CameraKeyframe keyframe; - keyframe.time = m_newKeyframeTime; + const float authoredTime = std::max(0.f, m_newKeyframeTime); + keyframe.time = authoredTime; + m_newKeyframeTime = authoredTime; if (tryCapturePreset(activeCamera, "Keyframe", keyframe.preset)) { m_keyframes.emplace_back(std::move(keyframe)); - std::sort(m_keyframes.begin(), m_keyframes.end(), [](const auto& a, const auto& b) { return a.time < b.time; }); + sortKeyframesByTime(); + selectKeyframeNearestTime(authoredTime); } } if (!keyframeCaptureUi.canCapture) @@ -908,20 +927,109 @@ void App::DrawControlPanel() if (ImGui::Button("Clear keyframes")) { m_keyframes.clear(); + m_selectedKeyframeIx = -1; + m_playback.time = 0.f; clearApplyStatusBanner(m_playbackApplyBanner); } DrawHoverHint("Remove all keyframes"); if (!m_keyframes.empty()) { + normalizeSelectedKeyframe(); if (ImGui::BeginChild("KeyframeList", ImVec2(0, 120), true)) { for (size_t i = 0; i < m_keyframes.size(); ++i) { - ImGui::Text("[%zu] t=%.3f", i, m_keyframes[i].time); + char label[128]; + snprintf(label, sizeof(label), "[%zu] t=%.3f %s", i, m_keyframes[i].time, m_keyframes[i].preset.name.c_str()); + if (ImGui::Selectable(label, m_selectedKeyframeIx == static_cast(i))) + m_selectedKeyframeIx = static_cast(i); } } ImGui::EndChild(); + + if (auto* selectedKeyframe = getSelectedKeyframe()) + { + const auto keyframeUi = analyzePresetForUi(activeCamera, selectedKeyframe->preset); + const ImVec4 compatibilityColor = !keyframeUi.hasActiveCamera ? bad : (keyframeUi.exact() ? good : warn); + float selectedTime = selectedKeyframe->time; + if (ImGui::InputFloat("Selected time", &selectedTime, 0.1f, 1.f, "%.3f")) + { + selectedTime = std::max(0.f, selectedTime); + selectedKeyframe->time = selectedTime; + sortKeyframesByTime(); + selectKeyframeNearestTime(selectedTime); + clampPlaybackTimeToKeyframes(); + } + DrawHoverHint("Edit selected keyframe time"); + + ImGui::TextDisabled("Keyframe source"); + ImGui::SameLine(); + ImGui::TextColored(muted, "%s", getCameraTypeLabel(keyframeUi.goal.sourceKind).data()); + ImGui::TextDisabled("Goal state"); + ImGui::SameLine(); + ImGui::TextColored(muted, "%s", describeGoalStateMask(keyframeUi.goal.sourceGoalStateMask).c_str()); + ImGui::TextDisabled("Policy"); + ImGui::SameLine(); + ImGui::TextColored(keyframeUi.canApply ? compatibilityColor : bad, "%s", keyframeUi.policyLabel.c_str()); + ImGui::TextDisabled("Compatibility"); + ImGui::SameLine(); + ImGui::TextColored(compatibilityColor, "%s", keyframeUi.compatibilityLabel.c_str()); + + DrawBadge(keyframeUi.exact() ? "EXACT" : "BEST-EFFORT", keyframeUi.exact() ? good : warn, badgeText); + if (keyframeUi.dropsGoalState()) + { + ImGui::SameLine(); + DrawBadge("DROPS STATE", warn, badgeText); + } + else if (keyframeUi.usesSharedStateOnly()) + { + ImGui::SameLine(); + DrawBadge("SHARED STATE", accent, badgeText); + } + if (!keyframeUi.canApply) + { + ImGui::SameLine(); + DrawBadge("BLOCKED", bad, badgeText); + } + + if (!keyframeUi.canApply) + ImGui::BeginDisabled(); + if (ImGui::Button("Apply selected")) + applyPresetFromUi(activeCamera, selectedKeyframe->preset); + if (!keyframeUi.canApply) + ImGui::EndDisabled(); + DrawHoverHint(keyframeUi.canApply ? + "Apply selected keyframe to the active camera" : + "Apply is blocked because there is no active camera or the keyframe goal is invalid"); + ImGui::SameLine(); + if (!keyframeCaptureUi.canCapture) + ImGui::BeginDisabled(); + if (ImGui::Button("Replace from camera")) + replaceSelectedKeyframeFromCamera(activeCamera); + if (!keyframeCaptureUi.canCapture) + ImGui::EndDisabled(); + DrawHoverHint(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"); + ImGui::SameLine(); + if (ImGui::Button("Jump to selected")) + { + m_playback.time = selectedKeyframe->time; + applyPlaybackAtTime(m_playback.time); + } + DrawHoverHint("Set playback time to selected keyframe and preview it"); + ImGui::SameLine(); + if (ImGui::Button("Remove selected")) + { + m_keyframes.erase(m_keyframes.begin() + m_selectedKeyframeIx); + normalizeSelectedKeyframe(); + clampPlaybackTimeToKeyframes(); + if (m_keyframes.empty()) + clearApplyStatusBanner(m_playbackApplyBanner); + } + DrawHoverHint("Remove selected keyframe"); + } } ImGui::PopItemWidth(); } diff --git a/61_UI/include/app/App.hpp b/61_UI/include/app/App.hpp index 75a59718e..948a45699 100644 --- a/61_UI/include/app/App.hpp +++ b/61_UI/include/app/App.hpp @@ -1642,6 +1642,90 @@ class App final : public examples::SimpleWindowedApplication return true; } + inline void sortKeyframesByTime() + { + std::sort(m_keyframes.begin(), m_keyframes.end(), [](const auto& a, const auto& b) { return a.time < b.time; }); + } + + inline void clampPlaybackTimeToKeyframes() + { + if (m_keyframes.empty()) + { + m_playback.time = 0.f; + return; + } + + m_playback.time = std::clamp(m_playback.time, 0.f, m_keyframes.back().time); + } + + inline int selectKeyframeNearestTime(const float time) + { + if (m_keyframes.empty()) + { + m_selectedKeyframeIx = -1; + return m_selectedKeyframeIx; + } + + size_t bestIx = 0u; + float bestDelta = std::abs(m_keyframes.front().time - time); + for (size_t i = 1u; i < m_keyframes.size(); ++i) + { + const float delta = std::abs(m_keyframes[i].time - time); + if (delta < bestDelta) + { + bestDelta = delta; + bestIx = i; + } + } + + m_selectedKeyframeIx = static_cast(bestIx); + return m_selectedKeyframeIx; + } + + inline void normalizeSelectedKeyframe() + { + if (m_keyframes.empty()) + { + m_selectedKeyframeIx = -1; + return; + } + + if (m_selectedKeyframeIx < 0) + m_selectedKeyframeIx = 0; + else if (m_selectedKeyframeIx >= static_cast(m_keyframes.size())) + m_selectedKeyframeIx = static_cast(m_keyframes.size()) - 1; + } + + inline CameraKeyframe* getSelectedKeyframe() + { + normalizeSelectedKeyframe(); + if (m_selectedKeyframeIx < 0) + return nullptr; + return &m_keyframes[static_cast(m_selectedKeyframeIx)]; + } + + inline const CameraKeyframe* getSelectedKeyframe() const + { + if (m_selectedKeyframeIx < 0 || m_selectedKeyframeIx >= static_cast(m_keyframes.size())) + return nullptr; + return &m_keyframes[static_cast(m_selectedKeyframeIx)]; + } + + inline bool 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 (!tryCapturePreset(camera, keyframeName, updatedPreset)) + return false; + + selected->preset = std::move(updatedPreset); + return true; + } + inline void updatePlayback(double dtSec) { if (!m_playback.playing || m_keyframes.empty()) @@ -2246,6 +2330,7 @@ class App final : public examples::SimpleWindowedApplication ApplyStatusBanner m_playbackApplyBanner; PresetFilterMode m_presetFilterMode = PresetFilterMode::All; int m_selectedPresetIx = -1; + int m_selectedKeyframeIx = -1; bool m_playbackAffectsAll = false; float m_newKeyframeTime = 0.f; char m_presetName[64] = "Preset"; From b46384ca65744d1d48ee5c88aab620533a756402 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 17:08:49 +0200 Subject: [PATCH 143/205] Add keyframe storage to 61_UI playback --- 61_UI/AppControlPanel.cpp | 16 +++++++++++++ 61_UI/include/app/App.hpp | 49 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/61_UI/AppControlPanel.cpp b/61_UI/AppControlPanel.cpp index fccecd153..eea4557e2 100644 --- a/61_UI/AppControlPanel.cpp +++ b/61_UI/AppControlPanel.cpp @@ -1030,6 +1030,22 @@ void App::DrawControlPanel() } DrawHoverHint("Remove selected keyframe"); } + + DrawSectionHeader("KeyframesStorageHeader", "Keyframe Storage", accent); + ImGui::InputText("Keyframe file", m_keyframePath, IM_ARRAYSIZE(m_keyframePath)); + if (ImGui::Button("Save keyframes")) + { + if (!saveKeyframesToFile(system::path(m_keyframePath))) + m_logger->log("Failed to save keyframes to \"%s\".", ILogger::ELL_ERROR, m_keyframePath); + } + DrawHoverHint("Save keyframes to JSON file"); + ImGui::SameLine(); + if (ImGui::Button("Load keyframes")) + { + if (!loadKeyframesFromFile(system::path(m_keyframePath))) + m_logger->log("Failed to load keyframes from \"%s\".", ILogger::ELL_ERROR, m_keyframePath); + } + DrawHoverHint("Load keyframes from JSON file"); } ImGui::PopItemWidth(); } diff --git a/61_UI/include/app/App.hpp b/61_UI/include/app/App.hpp index 948a45699..04b2021ac 100644 --- a/61_UI/include/app/App.hpp +++ b/61_UI/include/app/App.hpp @@ -1791,6 +1791,54 @@ class App final : public examples::SimpleWindowedApplication return true; } + inline bool saveKeyframesToFile(const system::path& path) + { + nbl_json root; + root["keyframes"] = nbl_json::array(); + + for (const auto& keyframe : m_keyframes) + { + auto j = nbl::hlsl::serializePreset(keyframe.preset); + j["time"] = keyframe.time; + root["keyframes"].push_back(std::move(j)); + } + + std::ofstream out(path.string(), std::ios::binary); + if (!out) + return false; + out << root.dump(2); + return true; + } + + inline bool loadKeyframesFromFile(const system::path& path) + { + std::ifstream in(path.string(), std::ios::binary); + if (!in) + return false; + + nbl_json root; + in >> root; + if (!root.contains("keyframes")) + return false; + + m_keyframes.clear(); + for (const auto& entry : root["keyframes"]) + { + CameraKeyframe keyframe; + if (entry.contains("time")) + keyframe.time = std::max(0.f, entry["time"].get()); + nbl::hlsl::deserializePreset(entry, keyframe.preset); + m_keyframes.emplace_back(std::move(keyframe)); + } + + sortKeyframesByTime(); + clampPlaybackTimeToKeyframes(); + normalizeSelectedKeyframe(); + if (m_keyframes.empty()) + clearApplyStatusBanner(m_playbackApplyBanner); + return true; + } + void imguiListen(); inline bool shouldCaptureOSCursor() @@ -2335,6 +2383,7 @@ class App final : public examples::SimpleWindowedApplication float m_newKeyframeTime = 0.f; char m_presetName[64] = "Preset"; char m_presetPath[260] = "camera_presets.json"; + char m_keyframePath[260] = "camera_keyframes.json"; std::chrono::microseconds m_lastPresentationTimestamp = {}; bool m_haveLastPresentationTimestamp = false; double m_frameDeltaSec = 0.0; From 4cb6f6fd94869788762a36de5b9f37b5573aeee4 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 17:21:15 +0200 Subject: [PATCH 144/205] Promote camera apply analysis into shared headers --- 61_UI/AppControlPanel.cpp | 6 +- 61_UI/include/app/App.hpp | 53 ++---------- 61_UI/include/common.hpp | 3 + common/include/camera/CCameraGoalAnalysis.hpp | 83 +++++++++++++++++++ 4 files changed, 98 insertions(+), 47 deletions(-) create mode 100644 common/include/camera/CCameraGoalAnalysis.hpp diff --git a/61_UI/AppControlPanel.cpp b/61_UI/AppControlPanel.cpp index eea4557e2..2fbf107b0 100644 --- a/61_UI/AppControlPanel.cpp +++ b/61_UI/AppControlPanel.cpp @@ -759,7 +759,7 @@ void App::DrawControlPanel() { const auto& preset = m_presets[static_cast(m_selectedPresetIx)]; const auto presetUi = analyzePresetForUi(activeCamera, preset); - const ImVec4 compatibilityColor = !presetUi.hasActiveCamera ? bad : (presetUi.exact() ? good : warn); + const ImVec4 compatibilityColor = !presetUi.hasCamera ? bad : (presetUi.exact() ? good : warn); ImGui::TextDisabled("Preset source"); ImGui::SameLine(); @@ -885,7 +885,7 @@ void App::DrawControlPanel() if (tryBuildPlaybackPresetAtTime(m_playback.time, playbackPreviewPreset)) { const auto playbackPreviewUi = analyzePresetForUi(activeCamera, playbackPreviewPreset); - const ImVec4 previewColor = !playbackPreviewUi.hasActiveCamera ? bad : (playbackPreviewUi.exact() ? good : warn); + const ImVec4 previewColor = !playbackPreviewUi.hasCamera ? bad : (playbackPreviewUi.exact() ? good : warn); ImGui::TextDisabled("Preview"); ImGui::SameLine(); ImGui::TextColored(playbackPreviewUi.canApply ? previewColor : bad, "%s", playbackPreviewUi.policyLabel.c_str()); @@ -951,7 +951,7 @@ void App::DrawControlPanel() if (auto* selectedKeyframe = getSelectedKeyframe()) { const auto keyframeUi = analyzePresetForUi(activeCamera, selectedKeyframe->preset); - const ImVec4 compatibilityColor = !keyframeUi.hasActiveCamera ? bad : (keyframeUi.exact() ? good : warn); + const ImVec4 compatibilityColor = !keyframeUi.hasCamera ? bad : (keyframeUi.exact() ? good : warn); float selectedTime = selectedKeyframe->time; if (ImGui::InputFloat("Selected time", &selectedTime, 0.1f, 1.f, "%.3f")) { diff --git a/61_UI/include/app/App.hpp b/61_UI/include/app/App.hpp index 04b2021ac..1c115e0ff 100644 --- a/61_UI/include/app/App.hpp +++ b/61_UI/include/app/App.hpp @@ -490,31 +490,11 @@ class App final : public examples::SimpleWindowedApplication BestEffort }; - struct PresetUiAnalysis + struct PresetUiAnalysis : SCameraGoalApplyAnalysis { - CCameraGoal goal = {}; - CCameraGoalSolver::SCompatibilityResult compatibility = {}; - bool hasActiveCamera = false; - bool finiteGoal = false; - bool canApply = false; std::string compatibilityLabel; std::string policyLabel; - inline bool exact() const - { - return compatibility.exact; - } - - inline bool dropsGoalState() const - { - return compatibility.missingGoalStateMask != ICamera::GoalStateNone; - } - - inline bool usesSharedStateOnly() const - { - return !compatibility.sameKind && goal.sourceKind != ICamera::CameraKind::Unknown && !dropsGoalState(); - } - inline bool matchesFilter(const PresetFilterMode mode) const { switch (mode) @@ -522,22 +502,17 @@ class App final : public examples::SimpleWindowedApplication case PresetFilterMode::All: return true; case PresetFilterMode::Exact: - return hasActiveCamera && exact(); + return hasCamera && exact(); case PresetFilterMode::BestEffort: - return hasActiveCamera && !exact(); + return hasCamera && !exact(); default: return true; } } }; - struct CaptureUiAnalysis + struct CaptureUiAnalysis : SCameraCaptureAnalysis { - CCameraGoal goal = {}; - bool hasActiveCamera = false; - bool capturedGoal = false; - bool finiteGoal = false; - bool canCapture = false; std::string policyLabel; }; @@ -1186,15 +1161,9 @@ class App final : public examples::SimpleWindowedApplication inline PresetUiAnalysis analyzePresetForUi(ICamera* camera, const CameraPreset& preset) const { PresetUiAnalysis analysis; - analysis.goal = nbl::hlsl::makeGoalFromPreset(preset); - analysis.hasActiveCamera = camera != nullptr; - analysis.finiteGoal = nbl::hlsl::isGoalFinite(analysis.goal); - analysis.canApply = analysis.hasActiveCamera && analysis.finiteGoal; + static_cast(analysis) = nbl::hlsl::analyzePresetApply(m_cameraGoalSolver, camera, preset); - if (analysis.hasActiveCamera) - analysis.compatibility = m_cameraGoalSolver.analyzeCompatibility(camera, analysis.goal); - - if (!analysis.hasActiveCamera) + if (!analysis.hasCamera) { analysis.compatibilityLabel = "No active camera"; analysis.policyLabel = "Blocked | no active camera"; @@ -1239,12 +1208,8 @@ class App final : public examples::SimpleWindowedApplication inline CaptureUiAnalysis analyzeCameraCaptureForUi(ICamera* camera) const { CaptureUiAnalysis analysis; - const auto capture = m_cameraGoalSolver.captureDetailed(camera); - analysis.goal = capture.goal; - analysis.hasActiveCamera = capture.hasCamera; - analysis.capturedGoal = capture.captured; - analysis.finiteGoal = capture.finiteGoal; - if (!analysis.hasActiveCamera) + static_cast(analysis) = nbl::hlsl::analyzeCameraCapture(m_cameraGoalSolver, camera); + if (!analysis.hasCamera) { analysis.policyLabel = "Blocked | no active camera"; return analysis; @@ -1272,7 +1237,7 @@ class App final : public examples::SimpleWindowedApplication inline CCameraGoalSolver::SCompatibilityResult analyzePresetCompatibility(ICamera* camera, const CameraPreset& preset) const { - return analyzePresetForUi(camera, preset).compatibility; + return nbl::hlsl::analyzePresetApply(m_cameraGoalSolver, camera, preset).compatibility; } inline const char* getPresetFilterModeLabel(PresetFilterMode mode) const diff --git a/61_UI/include/common.hpp b/61_UI/include/common.hpp index 7e263d9aa..a529194fe 100644 --- a/61_UI/include/common.hpp +++ b/61_UI/include/common.hpp @@ -19,6 +19,7 @@ #include "camera/CDollyZoomCamera.hpp" #include "camera/CPathCamera.hpp" #include "camera/CCameraPreset.hpp" +#include "camera/CCameraGoalAnalysis.hpp" #include "camera/CCameraGoalSolver.hpp" #include "camera/CGimbalInputBinder.hpp" @@ -59,6 +60,8 @@ using nbl::hlsl::CPathCamera; using nbl::hlsl::CCameraGoal; using nbl::hlsl::CCameraPreset; using nbl::hlsl::CCameraKeyframe; +using nbl::hlsl::SCameraGoalApplyAnalysis; +using nbl::hlsl::SCameraCaptureAnalysis; using nbl::hlsl::CCameraGoalSolver; using nbl::hlsl::CGimbalInputBinder; using nbl::hlsl::IGimbalBindingLayout; diff --git a/common/include/camera/CCameraGoalAnalysis.hpp b/common/include/camera/CCameraGoalAnalysis.hpp new file mode 100644 index 000000000..e9c64ce72 --- /dev/null +++ b/common/include/camera/CCameraGoalAnalysis.hpp @@ -0,0 +1,83 @@ +// 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_GOAL_ANALYSIS_HPP_ +#define _C_CAMERA_GOAL_ANALYSIS_HPP_ + +#include "CCameraPreset.hpp" +#include "CCameraGoalSolver.hpp" + +namespace nbl::hlsl +{ + +struct SCameraGoalApplyAnalysis +{ + CCameraGoal goal = {}; + CCameraGoalSolver::SCompatibilityResult compatibility = {}; + bool hasCamera = false; + bool finiteGoal = false; + bool canApply = false; + + inline bool exact() const + { + return compatibility.exact; + } + + inline bool dropsGoalState() const + { + return compatibility.missingGoalStateMask != ICamera::GoalStateNone; + } + + inline bool usesSharedStateOnly() const + { + return !compatibility.sameKind && goal.sourceKind != ICamera::CameraKind::Unknown && !dropsGoalState(); + } + + inline bool isMeaningfulApply() const + { + return canApply; + } +}; + +struct SCameraCaptureAnalysis +{ + CCameraGoal goal = {}; + bool hasCamera = false; + bool capturedGoal = false; + bool finiteGoal = false; + bool canCapture = false; +}; + +inline SCameraGoalApplyAnalysis analyzeGoalApply(const CCameraGoalSolver& solver, const ICamera* camera, const CCameraGoal& goal) +{ + SCameraGoalApplyAnalysis analysis; + analysis.goal = canonicalizeGoal(goal); + analysis.hasCamera = camera != nullptr; + analysis.finiteGoal = isGoalFinite(analysis.goal); + analysis.canApply = analysis.hasCamera && analysis.finiteGoal; + if (analysis.hasCamera) + analysis.compatibility = solver.analyzeCompatibility(camera, analysis.goal); + return analysis; +} + +inline SCameraGoalApplyAnalysis analyzePresetApply(const CCameraGoalSolver& solver, const ICamera* camera, const CCameraPreset& preset) +{ + return analyzeGoalApply(solver, camera, makeGoalFromPreset(preset)); +} + +inline SCameraCaptureAnalysis analyzeCameraCapture(const CCameraGoalSolver& solver, ICamera* camera) +{ + SCameraCaptureAnalysis analysis; + const auto capture = solver.captureDetailed(camera); + analysis.goal = capture.goal; + analysis.hasCamera = capture.hasCamera; + analysis.capturedGoal = capture.captured; + analysis.finiteGoal = capture.finiteGoal; + analysis.canCapture = capture.canUseGoal(); + return analysis; +} + +} // namespace nbl::hlsl + +#endif // _C_CAMERA_GOAL_ANALYSIS_HPP_ From caf6a09e9bc50addc50b7621cd3ca0c5f1fb094a Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 17:32:19 +0200 Subject: [PATCH 145/205] Promote camera keyframe track into shared headers --- 61_UI/AppControlPanel.cpp | 25 ++- 61_UI/include/app/App.hpp | 117 ++---------- 61_UI/include/common.hpp | 2 + .../include/camera/CCameraKeyframeTrack.hpp | 169 ++++++++++++++++++ 4 files changed, 198 insertions(+), 115 deletions(-) create mode 100644 common/include/camera/CCameraKeyframeTrack.hpp diff --git a/61_UI/AppControlPanel.cpp b/61_UI/AppControlPanel.cpp index 2fbf107b0..68cdb5ce2 100644 --- a/61_UI/AppControlPanel.cpp +++ b/61_UI/AppControlPanel.cpp @@ -868,9 +868,9 @@ void App::DrawControlPanel() } DrawHoverHint("Stop playback and reset time"); - if (!m_keyframes.empty()) + if (!m_keyframeTrack.keyframes.empty()) { - const float duration = m_keyframes.back().time; + const float duration = m_keyframeTrack.keyframes.back().time; if (ImGui::SliderFloat("Time", &m_playback.time, 0.f, duration, "%.3f")) applyPlaybackAtTime(m_playback.time); } @@ -879,7 +879,7 @@ void App::DrawControlPanel() const ImVec4 playbackColor = m_playbackApplyBanner.succeeded ? (m_playbackApplyBanner.approximate ? warn : good) : bad; ImGui::TextColored(playbackColor, "%s", m_playbackApplyBanner.summary.c_str()); } - if (!m_keyframes.empty()) + if (!m_keyframeTrack.keyframes.empty()) { CameraPreset playbackPreviewPreset; if (tryBuildPlaybackPresetAtTime(m_playback.time, playbackPreviewPreset)) @@ -910,7 +910,7 @@ void App::DrawControlPanel() m_newKeyframeTime = authoredTime; if (tryCapturePreset(activeCamera, "Keyframe", keyframe.preset)) { - m_keyframes.emplace_back(std::move(keyframe)); + m_keyframeTrack.keyframes.emplace_back(std::move(keyframe)); sortKeyframesByTime(); selectKeyframeNearestTime(authoredTime); } @@ -926,24 +926,23 @@ void App::DrawControlPanel() ImGui::SameLine(); if (ImGui::Button("Clear keyframes")) { - m_keyframes.clear(); - m_selectedKeyframeIx = -1; + m_keyframeTrack = {}; m_playback.time = 0.f; clearApplyStatusBanner(m_playbackApplyBanner); } DrawHoverHint("Remove all keyframes"); - if (!m_keyframes.empty()) + if (!m_keyframeTrack.keyframes.empty()) { normalizeSelectedKeyframe(); if (ImGui::BeginChild("KeyframeList", ImVec2(0, 120), true)) { - for (size_t i = 0; i < m_keyframes.size(); ++i) + for (size_t i = 0; i < m_keyframeTrack.keyframes.size(); ++i) { char label[128]; - snprintf(label, sizeof(label), "[%zu] t=%.3f %s", i, m_keyframes[i].time, m_keyframes[i].preset.name.c_str()); - if (ImGui::Selectable(label, m_selectedKeyframeIx == static_cast(i))) - m_selectedKeyframeIx = static_cast(i); + snprintf(label, sizeof(label), "[%zu] t=%.3f %s", i, m_keyframeTrack.keyframes[i].time, m_keyframeTrack.keyframes[i].preset.name.c_str()); + if (ImGui::Selectable(label, m_keyframeTrack.selectedKeyframeIx == static_cast(i))) + m_keyframeTrack.selectedKeyframeIx = static_cast(i); } } ImGui::EndChild(); @@ -1022,10 +1021,10 @@ void App::DrawControlPanel() ImGui::SameLine(); if (ImGui::Button("Remove selected")) { - m_keyframes.erase(m_keyframes.begin() + m_selectedKeyframeIx); + m_keyframeTrack.keyframes.erase(m_keyframeTrack.keyframes.begin() + m_keyframeTrack.selectedKeyframeIx); normalizeSelectedKeyframe(); clampPlaybackTimeToKeyframes(); - if (m_keyframes.empty()) + if (m_keyframeTrack.keyframes.empty()) clearApplyStatusBanner(m_playbackApplyBanner); } DrawHoverHint("Remove selected keyframe"); diff --git a/61_UI/include/app/App.hpp b/61_UI/include/app/App.hpp index 1c115e0ff..771540658 100644 --- a/61_UI/include/app/App.hpp +++ b/61_UI/include/app/App.hpp @@ -482,6 +482,7 @@ class App final : public examples::SimpleWindowedApplication using CameraPreset = CCameraPreset; using CameraKeyframe = CCameraKeyframe; + using CameraKeyframeTrack = CCameraKeyframeTrack; enum class PresetFilterMode : uint8_t { @@ -1566,32 +1567,7 @@ class App final : public examples::SimpleWindowedApplication inline bool tryBuildPlaybackPresetAtTime(const float time, CameraPreset& preset) { - if (m_keyframes.empty()) - return false; - - if (m_keyframes.size() == 1u) - { - preset = m_keyframes.front().preset; - return true; - } - - const auto clampedTime = std::clamp(time, 0.f, m_keyframes.back().time); - size_t idx = 0u; - while (idx + 1u < m_keyframes.size() && m_keyframes[idx + 1u].time < clampedTime) - ++idx; - - const auto& a = m_keyframes[idx]; - const auto& b = m_keyframes[std::min(idx + 1u, m_keyframes.size() - 1u)]; - if (b.time <= a.time) - { - preset = a.preset; - return true; - } - - const double alpha = static_cast(clampedTime - a.time) / static_cast(b.time - a.time); - preset = a.preset; - nbl::hlsl::assignGoalToPreset(preset, nbl::hlsl::blendGoals(nbl::hlsl::makeGoalFromPreset(a.preset), nbl::hlsl::makeGoalFromPreset(b.preset), alpha)); - return true; + return nbl::hlsl::tryBuildKeyframeTrackPresetAtTime(m_keyframeTrack, time, preset); } inline bool applyPlaybackAtTime(const float time) @@ -1609,71 +1585,32 @@ class App final : public examples::SimpleWindowedApplication inline void sortKeyframesByTime() { - std::sort(m_keyframes.begin(), m_keyframes.end(), [](const auto& a, const auto& b) { return a.time < b.time; }); + nbl::hlsl::sortKeyframeTrackByTime(m_keyframeTrack); } inline void clampPlaybackTimeToKeyframes() { - if (m_keyframes.empty()) - { - m_playback.time = 0.f; - return; - } - - m_playback.time = std::clamp(m_playback.time, 0.f, m_keyframes.back().time); + nbl::hlsl::clampTrackTimeToKeyframes(m_keyframeTrack, m_playback.time); } inline int selectKeyframeNearestTime(const float time) { - if (m_keyframes.empty()) - { - m_selectedKeyframeIx = -1; - return m_selectedKeyframeIx; - } - - size_t bestIx = 0u; - float bestDelta = std::abs(m_keyframes.front().time - time); - for (size_t i = 1u; i < m_keyframes.size(); ++i) - { - const float delta = std::abs(m_keyframes[i].time - time); - if (delta < bestDelta) - { - bestDelta = delta; - bestIx = i; - } - } - - m_selectedKeyframeIx = static_cast(bestIx); - return m_selectedKeyframeIx; + return nbl::hlsl::selectKeyframeTrackNearestTime(m_keyframeTrack, time); } inline void normalizeSelectedKeyframe() { - if (m_keyframes.empty()) - { - m_selectedKeyframeIx = -1; - return; - } - - if (m_selectedKeyframeIx < 0) - m_selectedKeyframeIx = 0; - else if (m_selectedKeyframeIx >= static_cast(m_keyframes.size())) - m_selectedKeyframeIx = static_cast(m_keyframes.size()) - 1; + nbl::hlsl::normalizeSelectedKeyframeTrack(m_keyframeTrack); } inline CameraKeyframe* getSelectedKeyframe() { - normalizeSelectedKeyframe(); - if (m_selectedKeyframeIx < 0) - return nullptr; - return &m_keyframes[static_cast(m_selectedKeyframeIx)]; + return nbl::hlsl::getSelectedKeyframe(m_keyframeTrack); } inline const CameraKeyframe* getSelectedKeyframe() const { - if (m_selectedKeyframeIx < 0 || m_selectedKeyframeIx >= static_cast(m_keyframes.size())) - return nullptr; - return &m_keyframes[static_cast(m_selectedKeyframeIx)]; + return nbl::hlsl::getSelectedKeyframe(m_keyframeTrack); } inline bool replaceSelectedKeyframeFromCamera(ICamera* camera) @@ -1687,18 +1624,17 @@ class App final : public examples::SimpleWindowedApplication if (!tryCapturePreset(camera, keyframeName, updatedPreset)) return false; - selected->preset = std::move(updatedPreset); - return true; + return nbl::hlsl::replaceSelectedKeyframePreset(m_keyframeTrack, std::move(updatedPreset)); } inline void updatePlayback(double dtSec) { - if (!m_playback.playing || m_keyframes.empty()) + if (!m_playback.playing || m_keyframeTrack.keyframes.empty()) return; m_playback.time += static_cast(dtSec * m_playback.speed); - const float duration = m_keyframes.back().time; + const float duration = m_keyframeTrack.keyframes.back().time; if (duration <= 0.f) { applyPlaybackAtTime(m_playback.time); @@ -1758,20 +1694,10 @@ class App final : public examples::SimpleWindowedApplication inline bool saveKeyframesToFile(const system::path& path) { - nbl_json root; - root["keyframes"] = nbl_json::array(); - - for (const auto& keyframe : m_keyframes) - { - auto j = nbl::hlsl::serializePreset(keyframe.preset); - j["time"] = keyframe.time; - root["keyframes"].push_back(std::move(j)); - } - std::ofstream out(path.string(), std::ios::binary); if (!out) return false; - out << root.dump(2); + out << nbl::hlsl::serializeKeyframeTrack(m_keyframeTrack).dump(2); return true; } @@ -1783,23 +1709,11 @@ class App final : public examples::SimpleWindowedApplication nbl_json root; in >> root; - if (!root.contains("keyframes")) + if (!nbl::hlsl::deserializeKeyframeTrack(root, m_keyframeTrack)) return false; - m_keyframes.clear(); - for (const auto& entry : root["keyframes"]) - { - CameraKeyframe keyframe; - if (entry.contains("time")) - keyframe.time = std::max(0.f, entry["time"].get()); - nbl::hlsl::deserializePreset(entry, keyframe.preset); - m_keyframes.emplace_back(std::move(keyframe)); - } - - sortKeyframesByTime(); clampPlaybackTimeToKeyframes(); - normalizeSelectedKeyframe(); - if (m_keyframes.empty()) + if (m_keyframeTrack.keyframes.empty()) clearApplyStatusBanner(m_playbackApplyBanner); return true; } @@ -2336,14 +2250,13 @@ class App final : public examples::SimpleWindowedApplication bool m_logWrap = true; std::vector m_presets; std::vector m_initialPlanarPresets; - std::vector m_keyframes; + CameraKeyframeTrack m_keyframeTrack; CameraPlaybackState m_playback; CCameraGoalSolver m_cameraGoalSolver; ApplyStatusBanner m_manualPresetApplyBanner; ApplyStatusBanner m_playbackApplyBanner; PresetFilterMode m_presetFilterMode = PresetFilterMode::All; int m_selectedPresetIx = -1; - int m_selectedKeyframeIx = -1; bool m_playbackAffectsAll = false; float m_newKeyframeTime = 0.f; char m_presetName[64] = "Preset"; diff --git a/61_UI/include/common.hpp b/61_UI/include/common.hpp index a529194fe..bc25dd14d 100644 --- a/61_UI/include/common.hpp +++ b/61_UI/include/common.hpp @@ -19,6 +19,7 @@ #include "camera/CDollyZoomCamera.hpp" #include "camera/CPathCamera.hpp" #include "camera/CCameraPreset.hpp" +#include "camera/CCameraKeyframeTrack.hpp" #include "camera/CCameraGoalAnalysis.hpp" #include "camera/CCameraGoalSolver.hpp" #include "camera/CGimbalInputBinder.hpp" @@ -60,6 +61,7 @@ using nbl::hlsl::CPathCamera; using nbl::hlsl::CCameraGoal; using nbl::hlsl::CCameraPreset; using nbl::hlsl::CCameraKeyframe; +using nbl::hlsl::CCameraKeyframeTrack; using nbl::hlsl::SCameraGoalApplyAnalysis; using nbl::hlsl::SCameraCaptureAnalysis; using nbl::hlsl::CCameraGoalSolver; diff --git a/common/include/camera/CCameraKeyframeTrack.hpp b/common/include/camera/CCameraKeyframeTrack.hpp new file mode 100644 index 000000000..2cba73c7c --- /dev/null +++ b/common/include/camera/CCameraKeyframeTrack.hpp @@ -0,0 +1,169 @@ +// 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_KEYFRAME_TRACK_HPP_ +#define _C_CAMERA_KEYFRAME_TRACK_HPP_ + +#include +#include +#include + +#include "CCameraPreset.hpp" + +namespace nbl::hlsl +{ + +struct CCameraKeyframeTrack +{ + std::vector keyframes; + int selectedKeyframeIx = -1; +}; + +inline bool tryBuildKeyframeTrackPresetAtTime(const CCameraKeyframeTrack& track, const float time, CCameraPreset& preset) +{ + if (track.keyframes.empty()) + return false; + + if (track.keyframes.size() == 1u) + { + preset = track.keyframes.front().preset; + return true; + } + + const auto clampedTime = std::clamp(time, 0.f, track.keyframes.back().time); + size_t idx = 0u; + while (idx + 1u < track.keyframes.size() && track.keyframes[idx + 1u].time < clampedTime) + ++idx; + + const auto& a = track.keyframes[idx]; + const auto& b = track.keyframes[std::min(idx + 1u, track.keyframes.size() - 1u)]; + if (b.time <= a.time) + { + preset = a.preset; + return true; + } + + const double alpha = static_cast(clampedTime - a.time) / static_cast(b.time - a.time); + preset = a.preset; + assignGoalToPreset(preset, blendGoals(makeGoalFromPreset(a.preset), makeGoalFromPreset(b.preset), alpha)); + return true; +} + +inline void sortKeyframeTrackByTime(CCameraKeyframeTrack& track) +{ + std::sort(track.keyframes.begin(), track.keyframes.end(), [](const auto& a, const auto& b) { return a.time < b.time; }); +} + +inline void clampTrackTimeToKeyframes(const CCameraKeyframeTrack& track, float& time) +{ + if (track.keyframes.empty()) + { + time = 0.f; + return; + } + + time = std::clamp(time, 0.f, track.keyframes.back().time); +} + +inline int selectKeyframeTrackNearestTime(CCameraKeyframeTrack& track, const float time) +{ + if (track.keyframes.empty()) + { + track.selectedKeyframeIx = -1; + return track.selectedKeyframeIx; + } + + size_t bestIx = 0u; + float bestDelta = std::abs(track.keyframes.front().time - time); + for (size_t i = 1u; i < track.keyframes.size(); ++i) + { + const float delta = std::abs(track.keyframes[i].time - time); + if (delta < bestDelta) + { + bestDelta = delta; + bestIx = i; + } + } + + track.selectedKeyframeIx = static_cast(bestIx); + return track.selectedKeyframeIx; +} + +inline void normalizeSelectedKeyframeTrack(CCameraKeyframeTrack& track) +{ + if (track.keyframes.empty()) + { + track.selectedKeyframeIx = -1; + return; + } + + if (track.selectedKeyframeIx < 0) + track.selectedKeyframeIx = 0; + else if (track.selectedKeyframeIx >= static_cast(track.keyframes.size())) + track.selectedKeyframeIx = static_cast(track.keyframes.size()) - 1; +} + +inline CCameraKeyframe* getSelectedKeyframe(CCameraKeyframeTrack& track) +{ + normalizeSelectedKeyframeTrack(track); + if (track.selectedKeyframeIx < 0) + return nullptr; + return &track.keyframes[static_cast(track.selectedKeyframeIx)]; +} + +inline const CCameraKeyframe* getSelectedKeyframe(const CCameraKeyframeTrack& track) +{ + if (track.selectedKeyframeIx < 0 || track.selectedKeyframeIx >= static_cast(track.keyframes.size())) + return nullptr; + return &track.keyframes[static_cast(track.selectedKeyframeIx)]; +} + +inline bool replaceSelectedKeyframePreset(CCameraKeyframeTrack& track, CCameraPreset preset) +{ + auto* selected = getSelectedKeyframe(track); + if (!selected) + return false; + + selected->preset = std::move(preset); + return true; +} + +inline nlohmann::json serializeKeyframeTrack(const CCameraKeyframeTrack& track) +{ + nlohmann::json root; + root["keyframes"] = nlohmann::json::array(); + + for (const auto& keyframe : track.keyframes) + { + auto j = serializePreset(keyframe.preset); + j["time"] = keyframe.time; + root["keyframes"].push_back(std::move(j)); + } + + return root; +} + +inline bool deserializeKeyframeTrack(const nlohmann::json& root, CCameraKeyframeTrack& track) +{ + if (!root.contains("keyframes")) + return false; + + track = {}; + for (const auto& entry : root["keyframes"]) + { + CCameraKeyframe keyframe; + if (entry.contains("time")) + keyframe.time = std::max(0.f, entry["time"].get()); + deserializePreset(entry, keyframe.preset); + track.keyframes.emplace_back(std::move(keyframe)); + } + + sortKeyframeTrackByTime(track); + normalizeSelectedKeyframeTrack(track); + return true; +} + +} // namespace nbl::hlsl + +#endif // _C_CAMERA_KEYFRAME_TRACK_HPP_ From 02c4e33913a2f496455ea9092c73d52fc1d4610b Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 17:36:33 +0200 Subject: [PATCH 146/205] Document shared camera API and utilities --- 61_UI/README.md | 19 ++ common/include/camera/README.md | 316 ++++++++++++++++++++++++++++++++ 2 files changed, 335 insertions(+) create mode 100644 common/include/camera/README.md diff --git a/61_UI/README.md b/61_UI/README.md index 1b4724676..e27bb91ed 100644 --- a/61_UI/README.md +++ b/61_UI/README.md @@ -3,6 +3,25 @@ This example demonstrates interactive camera control in the ImGui-based UI sample. It contains a scripted-input harness that can drive camera actions in CI and validate behavior with frame-based checks. +## Shared camera API + +`61_UI` is the active integration and validation surface for the reusable camera stack in `../common/include/camera`. + +See: + +- [`../common/include/camera/README.md`](../common/include/camera/README.md) + +That shared layer covers: + +- virtual gimbal events +- binding layouts and runtime input binders +- reusable camera kinds and typed state hooks +- best-effort goal capture and apply utilities +- preset and keyframe-track storage helpers + +At the moment other examples are not being migrated yet. +The reusable API is growing in `common/include/camera`, while `61_UI` stays the only active call-site. + ## Cameras in this scene `app_resources/cameras.json` defines 11 camera types: diff --git a/common/include/camera/README.md b/common/include/camera/README.md new file mode 100644 index 000000000..b084059fb --- /dev/null +++ b/common/include/camera/README.md @@ -0,0 +1,316 @@ +# Shared Camera API + +This directory contains the reusable camera stack currently used by `61_UI`. +It lives in `examples_tests/common` on purpose: the API is shared across examples, but it is not engine-core API yet. + +The current design goal is: + +- camera core consumes virtual events only +- raw input binding stays outside the camera +- absolute goals and preset restore are best-effort helpers layered on top +- reusable math, preset, analysis, and keyframe-track utilities live in shared headers +- `61_UI` is the current integration and validation surface + +## Mental model + +The stack is split into 5 layers: + +1. `CVirtualGimbalEvent` + A semantic camera command such as `MoveForward`, `PanLeft`, or `RollRight`. +2. Binding layout + Static mapping from keyboard, mouse, or ImGuizmo inputs to virtual events. +3. Input processor / binder + Runtime translation from external events into virtual events. +4. Camera model + A concrete camera type that consumes virtual events and updates its pose. +5. Goal / preset / track utilities + Best-effort capture, compatibility analysis, restore, blending, and playback helpers. + +The intended flow is: + +`raw input -> binding layout -> input binder -> virtual events -> ICamera::manipulate(...)` + +The best-effort absolute layer sits beside that flow: + +`camera state <-> CCameraGoal <-> CCameraPreset <-> CCameraKeyframeTrack` + +## Core contracts + +### `IGimbal.hpp` + +Defines `CVirtualGimbalEvent` and the low-level gimbal math. + +Important points: + +- `CVirtualGimbalEvent` is the shared semantic command language. +- Translation, rotation, and scale commands are represented explicitly. +- `IGimbal::accumulate(...)` converts a stream of virtual events into a single impulse. + +This is the base abstraction that makes scripted tests, headless CI, and manual input share one command path. + +### `IGimbalBindingLayout.hpp` + +Defines static binding-layout storage and mutation. + +Use it when you need to describe: + +- which keyboard keys trigger which virtual events +- which mouse channels trigger which virtual events +- which ImGuizmo transforms map to which virtual events + +This header does not process input by itself. It only stores mapping layout. + +### `IGimbalController.hpp` + +The file name is legacy. The main type is `IGimbalInputProcessor`. + +Use it when you need runtime input processing: + +- keyboard event streams +- mouse event streams +- ImGuizmo delta transforms + +`IGimbalInputProcessor` owns active runtime mappings and emits virtual events for the current frame. + +### `CGimbalInputBinder.hpp` + +High-level runtime binder built on top of `IGimbalInputProcessor`. + +This is the easiest entry point for examples: + +- copy active bindings from a layout +- copy preset/default bindings from a layout +- collect virtual events for one frame + +Use `CGimbalInputBinder` in viewport or example glue. +Do not embed external input processing inside camera types. + +### `ICamera.hpp` + +Main camera contract. + +Important rules: + +- `ICamera::manipulate(...)` is the core runtime entry point +- camera core consumes virtual events only +- `ICamera` exposes inspection and capability queries +- camera core may expose typed optional state through `tryGet...State` / `trySet...State` + +The typed state hooks are intentionally optional: + +- `SphericalTargetState` +- `DynamicPerspectiveState` +- `PathState` + +This allows best-effort goal solving without turning the core runtime contract into a setter-heavy API. + +`ICamera` also exposes: + +- `CameraKind` +- `CameraCapability` +- `GoalStateMask` +- motion config +- default input binding config + +## Camera families + +### Free cameras + +- `CFPSCamera.hpp` +- `CFreeLockCamera.hpp` + +These are pose-driven cameras without spherical target semantics. +Best-effort absolute apply may fall back to direct reference-pose restoration when event replay alone is insufficient. + +### Spherical-target family + +- `CSphericalTargetCamera.hpp` +- `COrbitCamera.hpp` +- `CArcballCamera.hpp` +- `CTurntableCamera.hpp` +- `CTopDownCamera.hpp` +- `CIsometricCamera.hpp` +- `CChaseCamera.hpp` +- `CDollyCamera.hpp` + +These cameras share: + +- target position +- distance +- orbit angles `u/v` + +`CSphericalTargetCamera` is the common reusable base for that family. + +Current contract: + +- `Orbit`, `Arcball`, `Turntable`, `TopDown`, `Isometric`, `Chase`, and `Dolly` all participate in the shared spherical goal flow +- `Chase` and `Dolly` currently do not carry extra typed state beyond shared spherical state + +### Extended state cameras + +- `CDollyZoomCamera.hpp` +- `CPathCamera.hpp` + +These extend the shared spherical model with extra typed state: + +- `CDollyZoomCamera` uses `DynamicPerspectiveState` +- `CPathCamera` uses `PathState` + +If a camera needs extra state that cannot be faithfully round-tripped through pose plus spherical target data, this is the pattern to follow. + +## Projection layer + +### `IProjection.hpp`, `ILinearProjection.hpp` + +Base projection contracts and linear-projection math. + +### `IPlanarProjection.hpp` + +Planar camera projection wrapper used by `61_UI`. + +`IPlanarProjection::CProjection` carries: + +- perspective or orthographic parameters +- projection matrix update logic +- its own input binding storage for viewport-local bindings + +Important design point: + +- projection owns binding layout storage +- projection does not process raw input by itself +- runtime input processing should happen through `CGimbalInputBinder` + +### `CPlanarProjection.hpp`, `CLinearProjection.hpp`, `CCubeProjection.hpp` + +Concrete projection helpers on top of the above contracts. + +## Goal, preset, and playback utilities + +### `CCameraGoal.hpp` + +Typed camera-state transport object plus reusable math helpers. + +Use it for: + +- canonicalizing captured state +- checking whether a goal is finite +- comparing actual state to expected state +- blending two camera states for playback +- describing mismatches +- determining required typed goal state + +`CCameraGoal` is the shared language between capture, analysis, preset persistence, and playback interpolation. + +### `CCameraGoalSolver.hpp` + +Best-effort absolute layer for cameras. + +Use it for: + +- capture of typed camera state into `CCameraGoal` +- compatibility analysis against a target camera +- best-effort apply of a goal to a camera +- reconstruction of virtual events for replay-driven application + +Important result types: + +- `SCaptureResult` +- `SCompatibilityResult` +- `SApplyResult` + +`SApplyResult` explicitly distinguishes: + +- unsupported +- failed +- already satisfied +- applied by absolute fallback +- applied by virtual events +- mixed absolute + virtual event application + +This is the main contract behind preset restore and cross-camera best-effort behavior. + +### `CCameraGoalAnalysis.hpp` + +Thin reusable analysis layer built on top of `CCameraGoalSolver`. + +Use it when UI or higher-level tools need typed answers for: + +- can this camera state be captured +- can this preset/goal be applied +- is the apply exact or best-effort +- does the target camera drop some goal state +- is the result only using shared state across different camera kinds + +This keeps policy analysis out of example-local UI code. + +### `CCameraPreset.hpp` + +Reusable preset and keyframe state plus JSON IO. + +Provides: + +- `CCameraPreset` +- `CCameraKeyframe` +- goal-to-preset conversion helpers +- preset JSON serialization and deserialization + +This is the storage format used by `61_UI` for preset authoring and playback persistence. + +### `CCameraKeyframeTrack.hpp` + +Reusable keyframe-track helpers on top of presets. + +Provides: + +- `CCameraKeyframeTrack` +- preset-at-time evaluation +- keyframe sorting +- playback-time clamping +- nearest-keyframe selection +- selected-keyframe access +- selected-keyframe preset replacement +- track JSON serialization and deserialization + +This keeps playback-authoring logic reusable without forcing examples to reimplement keyframe math and storage flow. + +## Current integration status + +The shared headers above are designed to be reusable by additional examples. +Right now the active migrated call-site is only: + +- `61_UI` + +That is intentional. +The current work is focused on stabilizing the reusable API surface first, then using `61_UI` as the validation and UX harness. + +## Recommended integration pattern for a new example + +If another example wants to adopt this stack later, the intended path is: + +1. Instantiate a concrete `ICamera`. +2. Store default binding layouts on the camera and/or planar projection. +3. Use `CGimbalInputBinder` at runtime to translate external input into virtual events. +4. Feed the resulting event stream into `ICamera::manipulate(...)`. +5. Use `CCameraGoalSolver`, `CCameraGoalAnalysis`, and `CCameraPreset` only for tooling features such as: + - preset capture + - preset restore + - compatibility preview + - playback interpolation + - scripted validation + +That keeps the hot runtime path event-driven while still allowing higher-level tools to work with absolute camera goals in a controlled way. + +## Legacy compatibility notes + +- `CTargetPoseController.hpp` is currently only a compatibility include for `CCameraGoalSolver.hpp`. +- `IGimbalController.hpp` still has the old file name, but the main runtime-processing type is `IGimbalInputProcessor`. + +## Non-goals + +This layer is not yet: + +- engine-core Nabla camera API +- a promise that every example already uses the stack +- a fully generic animation system beyond camera preset and keyframe utilities + +It is a reusable example-shared camera framework currently validated through `61_UI`. From 48ff7026ae6a38199f365447191e13f935061015 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 17:38:39 +0200 Subject: [PATCH 147/205] Document shared camera headers --- common/include/camera/CCameraGoal.hpp | 3 +++ common/include/camera/CCameraGoalAnalysis.hpp | 2 ++ common/include/camera/CCameraGoalSolver.hpp | 9 +++++++++ common/include/camera/CCameraKeyframeTrack.hpp | 1 + common/include/camera/CCameraPreset.hpp | 2 ++ common/include/camera/CGimbalInputBinder.hpp | 7 +++++++ common/include/camera/CSphericalTargetCamera.hpp | 5 +++++ common/include/camera/ICamera.hpp | 11 ++++++++++- common/include/camera/IGimbal.hpp | 8 +++++++- common/include/camera/IGimbalBindingLayout.hpp | 6 ++++++ common/include/camera/IGimbalController.hpp | 5 +++++ common/include/camera/IPlanarProjection.hpp | 7 +++++++ 12 files changed, 64 insertions(+), 2 deletions(-) diff --git a/common/include/camera/CCameraGoal.hpp b/common/include/camera/CCameraGoal.hpp index 6110a36f7..964a3f9b8 100644 --- a/common/include/camera/CCameraGoal.hpp +++ b/common/include/camera/CCameraGoal.hpp @@ -17,6 +17,9 @@ namespace nbl::hlsl { +/** +* Typed transport object for camera state used by capture, comparison, presets, and playback. +*/ struct CCameraGoal { float64_t3 position = float64_t3(0.0); diff --git a/common/include/camera/CCameraGoalAnalysis.hpp b/common/include/camera/CCameraGoalAnalysis.hpp index e9c64ce72..160480d30 100644 --- a/common/include/camera/CCameraGoalAnalysis.hpp +++ b/common/include/camera/CCameraGoalAnalysis.hpp @@ -11,6 +11,7 @@ namespace nbl::hlsl { +//! Reusable typed answer for `goal/preset -> camera` compatibility checks. struct SCameraGoalApplyAnalysis { CCameraGoal goal = {}; @@ -40,6 +41,7 @@ struct SCameraGoalApplyAnalysis } }; +//! Reusable typed answer for `camera -> goal` capture viability. struct SCameraCaptureAnalysis { CCameraGoal goal = {}; diff --git a/common/include/camera/CCameraGoalSolver.hpp b/common/include/camera/CCameraGoalSolver.hpp index 885dac876..ec618cd6f 100644 --- a/common/include/camera/CCameraGoalSolver.hpp +++ b/common/include/camera/CCameraGoalSolver.hpp @@ -15,9 +15,16 @@ namespace nbl::hlsl { +/** +* Best-effort absolute layer built on top of the event-only camera core. +* +* It captures typed camera state into `CCameraGoal`, analyzes compatibility, +* and tries to apply goals back to cameras using typed hooks and virtual-event replay. +*/ class CCameraGoalSolver { public: + //! Detailed capture result for tooling code. struct SCaptureResult { bool hasCamera = false; @@ -31,6 +38,7 @@ class CCameraGoalSolver } }; + //! Compatibility of a goal with a target camera kind and state mask. struct SCompatibilityResult { bool sameKind = false; @@ -40,6 +48,7 @@ class CCameraGoalSolver uint32_t missingGoalStateMask = ICamera::GoalStateNone; }; + //! Outcome of a best-effort goal apply attempt. struct SApplyResult { enum class EStatus : uint8_t diff --git a/common/include/camera/CCameraKeyframeTrack.hpp b/common/include/camera/CCameraKeyframeTrack.hpp index 2cba73c7c..448993bbc 100644 --- a/common/include/camera/CCameraKeyframeTrack.hpp +++ b/common/include/camera/CCameraKeyframeTrack.hpp @@ -14,6 +14,7 @@ namespace nbl::hlsl { +//! Reusable keyframe container plus selection state for playback tooling. struct CCameraKeyframeTrack { std::vector keyframes; diff --git a/common/include/camera/CCameraPreset.hpp b/common/include/camera/CCameraPreset.hpp index d055ced86..cdb18c953 100644 --- a/common/include/camera/CCameraPreset.hpp +++ b/common/include/camera/CCameraPreset.hpp @@ -13,6 +13,7 @@ namespace nbl::hlsl { +//! Named persisted camera state built on top of `CCameraGoal`. struct CCameraPreset { std::string name; @@ -20,6 +21,7 @@ struct CCameraPreset CCameraGoal goal = {}; }; +//! Time-stamped preset entry used by playback and authoring tools. struct CCameraKeyframe { CCameraPreset preset; diff --git a/common/include/camera/CGimbalInputBinder.hpp b/common/include/camera/CGimbalInputBinder.hpp index 158d51783..806368ca4 100644 --- a/common/include/camera/CGimbalInputBinder.hpp +++ b/common/include/camera/CGimbalInputBinder.hpp @@ -8,6 +8,12 @@ namespace nbl::hlsl { +/** +* High-level runtime binder for examples and viewport glue. +* +* It owns active runtime mappings and collects one frame of virtual events +* from raw keyboard, mouse, and ImGuizmo input. +*/ class CGimbalInputBinder final : public IGimbalInputProcessor { public: @@ -16,6 +22,7 @@ class CGimbalInputBinder final : public IGimbalInputProcessor struct SCollectedVirtualEvents { + //! Concatenated output buffer plus per-domain counts for diagnostics. std::vector events; uint32_t keyboardCount = 0u; uint32_t mouseCount = 0u; diff --git a/common/include/camera/CSphericalTargetCamera.hpp b/common/include/camera/CSphericalTargetCamera.hpp index fae9b4d38..5e85d8c05 100644 --- a/common/include/camera/CSphericalTargetCamera.hpp +++ b/common/include/camera/CSphericalTargetCamera.hpp @@ -9,6 +9,11 @@ namespace nbl::hlsl { +/** +* Common base for cameras orbiting or tracking a target with spherical coordinates. +* +* The shared state is target position, distance, and orbit angles `u/v`. +*/ class CSphericalTargetCamera : public ICamera { public: diff --git a/common/include/camera/ICamera.hpp b/common/include/camera/ICamera.hpp index 165cb2b33..3c4b9b180 100644 --- a/common/include/camera/ICamera.hpp +++ b/common/include/camera/ICamera.hpp @@ -13,6 +13,13 @@ namespace nbl::hlsl // TODO: DIFFERENT NAMESPACE { +/** +* Shared camera contract used by examples. +* +* The hot runtime path is event-only: cameras consume `CVirtualGimbalEvent` +* streams through `manipulate(...)`. Optional typed state hooks exist only for +* tooling features such as capture, compatibility analysis, presets, and playback. +*/ class ICamera : virtual public core::IReferenceCounted { public: @@ -30,12 +37,14 @@ class ICamera : virtual public core::IReferenceCounted struct SMotionConfig { + //! Camera-local scales applied by implementations to virtual motion magnitude. double moveSpeedScale = 0.01; double rotationSpeedScale = 0.003; }; struct SInputBindingConfig { + //! Default binding layout advertised by this camera type. CGimbalBindingLayoutStorage defaultBindingLayout; }; @@ -93,7 +102,7 @@ class ICamera : virtual public core::IReferenceCounted double height = 0.0; }; - // Gimbal with view parameters representing a camera in world space + //! Gimbal that models the camera pose and cached view matrix in world space. class CGimbal : public IGimbal { public: diff --git a/common/include/camera/IGimbal.hpp b/common/include/camera/IGimbal.hpp index 84a7e3750..e65f85a00 100644 --- a/common/include/camera/IGimbal.hpp +++ b/common/include/camera/IGimbal.hpp @@ -8,6 +8,12 @@ // TODO: DIFFERENT NAMESPACE namespace nbl::hlsl { + /** + * Shared semantic camera command. + * + * Input processors and scripted tools emit these events. + * Camera implementations consume them through `ICamera::manipulate(...)`. + */ struct CVirtualGimbalEvent { enum VirtualEventType : uint32_t @@ -137,7 +143,7 @@ namespace nbl::hlsl vector dVirtualTranslate { 0.0f }, dVirtualRotation { 0.0f }, dVirtualScale { 1.0f }; }; - //! Accumulates virtual impulse given allowed virtual event bitmap. Input virtual events are already deltas with respect to some base frame, the utility filters the events & outputs the impulse + //! Accumulates one frame of virtual events into a translation/rotation/scale impulse. template VirtualImpulse accumulate(std::span virtualEvents, const vector& gRightOverride, const vector& gUpOverride, const vector& gForwardOverride) { diff --git a/common/include/camera/IGimbalBindingLayout.hpp b/common/include/camera/IGimbalBindingLayout.hpp index 26a55e742..381a6ba9a 100644 --- a/common/include/camera/IGimbalBindingLayout.hpp +++ b/common/include/camera/IGimbalBindingLayout.hpp @@ -6,6 +6,11 @@ namespace nbl::hlsl { +/** +* Static mapping contract from external input domains to virtual gimbal events. +* +* This type stores binding layout only. It does not process runtime input. +*/ struct IGimbalBindingLayout { IGimbalBindingLayout() {} @@ -71,6 +76,7 @@ struct IGimbalBindingLayout class CGimbalBindingLayoutStorage : public IGimbalBindingLayout { public: + //! Mutable storage for active or preset binding layout. using IGimbalBindingLayout::IGimbalBindingLayout; CGimbalBindingLayoutStorage() {} diff --git a/common/include/camera/IGimbalController.hpp b/common/include/camera/IGimbalController.hpp index 7c9f3b1e9..db726ccbb 100644 --- a/common/include/camera/IGimbalController.hpp +++ b/common/include/camera/IGimbalController.hpp @@ -16,6 +16,11 @@ namespace ImGuizmo namespace nbl::hlsl { +/** +* Runtime processor that turns keyboard, mouse, and ImGuizmo input into virtual events. +* +* The filename is legacy. The intended public type is `IGimbalInputProcessor`. +*/ class IGimbalInputProcessor : public CGimbalBindingLayoutStorage { public: diff --git a/common/include/camera/IPlanarProjection.hpp b/common/include/camera/IPlanarProjection.hpp index e4721f3a8..32c4298a6 100644 --- a/common/include/camera/IPlanarProjection.hpp +++ b/common/include/camera/IPlanarProjection.hpp @@ -7,9 +7,16 @@ namespace nbl::hlsl { +/** +* Linear projection wrapper for one camera-facing planar viewport. +* +* The projection owns viewport-local binding layout storage, while runtime input +* processing is expected to happen through `CGimbalInputBinder`. +*/ class IPlanarProjection : public ILinearProjection { public: + //! One perspective or orthographic projection entry plus its viewport-local bindings. struct CProjection : public ILinearProjection::CProjection { using base_t = ILinearProjection::CProjection; From 4599661f73be16a3b22d376344739efff38a68eb Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 17:59:01 +0200 Subject: [PATCH 148/205] Promote camera playback timeline into shared headers --- 61_UI/AppControlPanel.cpp | 7 +- 61_UI/include/app/App.hpp | 31 +------ 61_UI/include/common.hpp | 3 + .../camera/CCameraPlaybackTimeline.hpp | 93 +++++++++++++++++++ common/include/camera/README.md | 22 ++++- 5 files changed, 124 insertions(+), 32 deletions(-) create mode 100644 common/include/camera/CCameraPlaybackTimeline.hpp diff --git a/61_UI/AppControlPanel.cpp b/61_UI/AppControlPanel.cpp index 68cdb5ce2..5c5ed21f3 100644 --- a/61_UI/AppControlPanel.cpp +++ b/61_UI/AppControlPanel.cpp @@ -862,15 +862,14 @@ void App::DrawControlPanel() ImGui::SameLine(); if (ImGui::Button("Stop")) { - m_playback.playing = false; - m_playback.time = 0.f; + nbl::hlsl::resetPlaybackCursor(m_playback); applyPlaybackAtTime(m_playback.time); } DrawHoverHint("Stop playback and reset time"); if (!m_keyframeTrack.keyframes.empty()) { - const float duration = m_keyframeTrack.keyframes.back().time; + const float duration = nbl::hlsl::getPlaybackTrackDuration(m_keyframeTrack); if (ImGui::SliderFloat("Time", &m_playback.time, 0.f, duration, "%.3f")) applyPlaybackAtTime(m_playback.time); } @@ -927,7 +926,7 @@ void App::DrawControlPanel() if (ImGui::Button("Clear keyframes")) { m_keyframeTrack = {}; - m_playback.time = 0.f; + nbl::hlsl::resetPlaybackCursor(m_playback); clearApplyStatusBanner(m_playbackApplyBanner); } DrawHoverHint("Remove all keyframes"); diff --git a/61_UI/include/app/App.hpp b/61_UI/include/app/App.hpp index 771540658..936418f15 100644 --- a/61_UI/include/app/App.hpp +++ b/61_UI/include/app/App.hpp @@ -517,13 +517,9 @@ class App final : public examples::SimpleWindowedApplication std::string policyLabel; }; - struct CameraPlaybackState + struct CameraPlaybackState : CCameraPlaybackCursor { - bool playing = false; - bool loop = true; bool overrideInput = true; - float speed = 1.f; - float time = 0.f; }; struct PlaybackApplySummary @@ -1590,7 +1586,7 @@ class App final : public examples::SimpleWindowedApplication inline void clampPlaybackTimeToKeyframes() { - nbl::hlsl::clampTrackTimeToKeyframes(m_keyframeTrack, m_playback.time); + nbl::hlsl::clampPlaybackCursorToTrack(m_keyframeTrack, m_playback); } inline int selectKeyframeNearestTime(const float time) @@ -1629,29 +1625,10 @@ class App final : public examples::SimpleWindowedApplication inline void updatePlayback(double dtSec) { - if (!m_playback.playing || m_keyframeTrack.keyframes.empty()) + const auto advance = nbl::hlsl::advancePlaybackCursor(m_playback, m_keyframeTrack, dtSec); + if (!advance.hasTrack || !advance.changedTime) return; - m_playback.time += static_cast(dtSec * m_playback.speed); - - const float duration = m_keyframeTrack.keyframes.back().time; - if (duration <= 0.f) - { - applyPlaybackAtTime(m_playback.time); - return; - } - - if (m_playback.loop) - { - while (m_playback.time > duration) - m_playback.time -= duration; - } - else if (m_playback.time > duration) - { - m_playback.time = duration; - m_playback.playing = false; - } - applyPlaybackAtTime(m_playback.time); } diff --git a/61_UI/include/common.hpp b/61_UI/include/common.hpp index bc25dd14d..573ad87a6 100644 --- a/61_UI/include/common.hpp +++ b/61_UI/include/common.hpp @@ -20,6 +20,7 @@ #include "camera/CPathCamera.hpp" #include "camera/CCameraPreset.hpp" #include "camera/CCameraKeyframeTrack.hpp" +#include "camera/CCameraPlaybackTimeline.hpp" #include "camera/CCameraGoalAnalysis.hpp" #include "camera/CCameraGoalSolver.hpp" #include "camera/CGimbalInputBinder.hpp" @@ -62,6 +63,8 @@ using nbl::hlsl::CCameraGoal; using nbl::hlsl::CCameraPreset; using nbl::hlsl::CCameraKeyframe; using nbl::hlsl::CCameraKeyframeTrack; +using nbl::hlsl::CCameraPlaybackCursor; +using nbl::hlsl::SCameraPlaybackAdvanceResult; using nbl::hlsl::SCameraGoalApplyAnalysis; using nbl::hlsl::SCameraCaptureAnalysis; using nbl::hlsl::CCameraGoalSolver; diff --git a/common/include/camera/CCameraPlaybackTimeline.hpp b/common/include/camera/CCameraPlaybackTimeline.hpp new file mode 100644 index 000000000..7a38b5c47 --- /dev/null +++ b/common/include/camera/CCameraPlaybackTimeline.hpp @@ -0,0 +1,93 @@ +// 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_PLAYBACK_TIMELINE_HPP_ +#define _C_CAMERA_PLAYBACK_TIMELINE_HPP_ + +#include "CCameraKeyframeTrack.hpp" + +namespace nbl::hlsl +{ + +//! Shared playback cursor state for camera keyframe tracks. +struct CCameraPlaybackCursor +{ + bool playing = false; + bool loop = true; + float speed = 1.f; + float time = 0.f; +}; + +//! Outcome of advancing a playback cursor against a keyframe track. +struct SCameraPlaybackAdvanceResult +{ + bool hasTrack = false; + bool changedTime = false; + bool wrapped = false; + bool reachedEnd = false; + bool stopped = false; + float duration = 0.f; + float time = 0.f; +}; + +inline float getPlaybackTrackDuration(const CCameraKeyframeTrack& track) +{ + if (track.keyframes.empty()) + return 0.f; + + return track.keyframes.back().time; +} + +inline void resetPlaybackCursor(CCameraPlaybackCursor& cursor, const float time = 0.f) +{ + cursor.playing = false; + cursor.time = std::max(0.f, time); +} + +inline void clampPlaybackCursorToTrack(const CCameraKeyframeTrack& track, CCameraPlaybackCursor& cursor) +{ + clampTrackTimeToKeyframes(track, cursor.time); +} + +inline SCameraPlaybackAdvanceResult advancePlaybackCursor(CCameraPlaybackCursor& cursor, const CCameraKeyframeTrack& track, const double dtSec) +{ + SCameraPlaybackAdvanceResult result; + result.hasTrack = !track.keyframes.empty(); + result.duration = getPlaybackTrackDuration(track); + result.time = cursor.time; + + if (!result.hasTrack || !cursor.playing) + return result; + + const auto previousTime = cursor.time; + cursor.time += static_cast(dtSec * cursor.speed); + result.changedTime = cursor.time != previousTime; + result.time = cursor.time; + + if (result.duration <= 0.f) + return result; + + if (cursor.loop) + { + while (cursor.time > result.duration) + { + cursor.time -= result.duration; + result.wrapped = true; + } + } + else if (cursor.time > result.duration) + { + cursor.time = result.duration; + cursor.playing = false; + result.reachedEnd = true; + result.stopped = true; + } + + result.time = cursor.time; + return result; +} + +} // namespace nbl::hlsl + +#endif // _C_CAMERA_PLAYBACK_TIMELINE_HPP_ diff --git a/common/include/camera/README.md b/common/include/camera/README.md index b084059fb..09dc70c2f 100644 --- a/common/include/camera/README.md +++ b/common/include/camera/README.md @@ -9,6 +9,7 @@ The current design goal is: - raw input binding stays outside the camera - absolute goals and preset restore are best-effort helpers layered on top - reusable math, preset, analysis, and keyframe-track utilities live in shared headers +- reusable playback cursor and timeline helpers live in shared headers - `61_UI` is the current integration and validation surface ## Mental model @@ -34,6 +35,10 @@ The best-effort absolute layer sits beside that flow: `camera state <-> CCameraGoal <-> CCameraPreset <-> CCameraKeyframeTrack` +and the playback cursor sits beside that state: + +`CCameraPlaybackCursor <-> CCameraKeyframeTrack` + ## Core contracts ### `IGimbal.hpp` @@ -273,6 +278,20 @@ Provides: This keeps playback-authoring logic reusable without forcing examples to reimplement keyframe math and storage flow. +### `CCameraPlaybackTimeline.hpp` + +Reusable playback cursor and timeline helpers on top of keyframe tracks. + +Provides: + +- `CCameraPlaybackCursor` +- `SCameraPlaybackAdvanceResult` +- track duration helpers +- cursor reset and clamping +- per-frame time advance with loop and stop semantics + +This keeps playback-time progression reusable without forcing examples to reimplement cursor stepping rules. + ## Current integration status The shared headers above are designed to be reusable by additional examples. @@ -291,11 +310,12 @@ If another example wants to adopt this stack later, the intended path is: 2. Store default binding layouts on the camera and/or planar projection. 3. Use `CGimbalInputBinder` at runtime to translate external input into virtual events. 4. Feed the resulting event stream into `ICamera::manipulate(...)`. -5. Use `CCameraGoalSolver`, `CCameraGoalAnalysis`, and `CCameraPreset` only for tooling features such as: +5. Use `CCameraGoalSolver`, `CCameraGoalAnalysis`, `CCameraPreset`, `CCameraKeyframeTrack`, and `CCameraPlaybackTimeline` only for tooling features such as: - preset capture - preset restore - compatibility preview - playback interpolation + - playback cursor stepping - scripted validation That keeps the hot runtime path event-driven while still allowing higher-level tools to work with absolute camera goals in a controlled way. From 3ed609b399063851b54764a43c032b662e4c87f2 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 18:06:41 +0200 Subject: [PATCH 149/205] Promote camera persistence into shared headers --- 61_UI/include/app/App.hpp | 45 +------- 61_UI/include/common.hpp | 1 + common/include/camera/CCameraPersistence.hpp | 109 +++++++++++++++++++ common/include/camera/README.md | 16 ++- 4 files changed, 129 insertions(+), 42 deletions(-) create mode 100644 common/include/camera/CCameraPersistence.hpp diff --git a/61_UI/include/app/App.hpp b/61_UI/include/app/App.hpp index 936418f15..6cd19d853 100644 --- a/61_UI/include/app/App.hpp +++ b/61_UI/include/app/App.hpp @@ -1634,59 +1634,22 @@ class App final : public examples::SimpleWindowedApplication inline bool savePresetsToFile(const system::path& path) { - nbl_json root; - root["presets"] = nbl_json::array(); - - for (const auto& preset : m_presets) - root["presets"].push_back(nbl::hlsl::serializePreset(preset)); - - std::ofstream out(path.string(), std::ios::binary); - if (!out) - return false; - out << root.dump(2); - return true; + return nbl::hlsl::savePresetCollectionToFile(path, std::span(m_presets.data(), m_presets.size())); } inline bool loadPresetsFromFile(const system::path& path) { - std::ifstream in(path.string(), std::ios::binary); - if (!in) - return false; - - nbl_json root; - in >> root; - if (!root.contains("presets")) - return false; - - m_presets.clear(); - for (const auto& entry : root["presets"]) - { - CameraPreset preset; - nbl::hlsl::deserializePreset(entry, preset); - m_presets.emplace_back(std::move(preset)); - } - - return true; + return nbl::hlsl::loadPresetCollectionFromFile(path, m_presets); } inline bool saveKeyframesToFile(const system::path& path) { - std::ofstream out(path.string(), std::ios::binary); - if (!out) - return false; - out << nbl::hlsl::serializeKeyframeTrack(m_keyframeTrack).dump(2); - return true; + return nbl::hlsl::saveKeyframeTrackToFile(path, m_keyframeTrack); } inline bool loadKeyframesFromFile(const system::path& path) { - std::ifstream in(path.string(), std::ios::binary); - if (!in) - return false; - - nbl_json root; - in >> root; - if (!nbl::hlsl::deserializeKeyframeTrack(root, m_keyframeTrack)) + if (!nbl::hlsl::loadKeyframeTrackFromFile(path, m_keyframeTrack)) return false; clampPlaybackTimeToKeyframes(); diff --git a/61_UI/include/common.hpp b/61_UI/include/common.hpp index 573ad87a6..9b322a831 100644 --- a/61_UI/include/common.hpp +++ b/61_UI/include/common.hpp @@ -21,6 +21,7 @@ #include "camera/CCameraPreset.hpp" #include "camera/CCameraKeyframeTrack.hpp" #include "camera/CCameraPlaybackTimeline.hpp" +#include "camera/CCameraPersistence.hpp" #include "camera/CCameraGoalAnalysis.hpp" #include "camera/CCameraGoalSolver.hpp" #include "camera/CGimbalInputBinder.hpp" diff --git a/common/include/camera/CCameraPersistence.hpp b/common/include/camera/CCameraPersistence.hpp new file mode 100644 index 000000000..9cf1014c2 --- /dev/null +++ b/common/include/camera/CCameraPersistence.hpp @@ -0,0 +1,109 @@ +// 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_PERSISTENCE_HPP_ +#define _C_CAMERA_PERSISTENCE_HPP_ + +#include +#include + +#include "CCameraKeyframeTrack.hpp" +#include "nbl/system/path.h" + +namespace nbl::hlsl +{ + +//! JSON and file persistence helpers for reusable camera presets and playback tracks. +inline nlohmann::json serializePresetCollection(std::span presets) +{ + nlohmann::json root; + root["presets"] = nlohmann::json::array(); + for (const auto& preset : presets) + root["presets"].push_back(serializePreset(preset)); + return root; +} + +inline bool deserializePresetCollection(const nlohmann::json& root, std::vector& presets) +{ + if (!root.contains("presets") || !root["presets"].is_array()) + return false; + + std::vector loadedPresets; + loadedPresets.reserve(root["presets"].size()); + for (const auto& entry : root["presets"]) + { + CCameraPreset preset; + deserializePreset(entry, preset); + loadedPresets.emplace_back(std::move(preset)); + } + + presets = std::move(loadedPresets); + return true; +} + +inline bool writePresetCollection(std::ostream& out, std::span presets, const int indent = 2) +{ + if (!out) + return false; + + out << serializePresetCollection(presets).dump(indent); + return static_cast(out); +} + +inline bool readPresetCollection(std::istream& in, std::vector& presets) +{ + if (!in) + return false; + + nlohmann::json root; + in >> root; + return deserializePresetCollection(root, presets); +} + +inline bool savePresetCollectionToFile(const system::path& path, std::span presets, const int indent = 2) +{ + std::ofstream out(path.string(), std::ios::binary); + return writePresetCollection(out, presets, indent); +} + +inline bool loadPresetCollectionFromFile(const system::path& path, std::vector& presets) +{ + std::ifstream in(path.string(), std::ios::binary); + return readPresetCollection(in, presets); +} + +inline bool writeKeyframeTrack(std::ostream& out, const CCameraKeyframeTrack& track, const int indent = 2) +{ + if (!out) + return false; + + out << serializeKeyframeTrack(track).dump(indent); + return static_cast(out); +} + +inline bool readKeyframeTrack(std::istream& in, CCameraKeyframeTrack& track) +{ + if (!in) + return false; + + nlohmann::json root; + in >> root; + return deserializeKeyframeTrack(root, track); +} + +inline bool saveKeyframeTrackToFile(const system::path& path, const CCameraKeyframeTrack& track, const int indent = 2) +{ + std::ofstream out(path.string(), std::ios::binary); + return writeKeyframeTrack(out, track, indent); +} + +inline bool loadKeyframeTrackFromFile(const system::path& path, CCameraKeyframeTrack& track) +{ + std::ifstream in(path.string(), std::ios::binary); + return readKeyframeTrack(in, track); +} + +} // namespace nbl::hlsl + +#endif // _C_CAMERA_PERSISTENCE_HPP_ diff --git a/common/include/camera/README.md b/common/include/camera/README.md index 09dc70c2f..663fadaea 100644 --- a/common/include/camera/README.md +++ b/common/include/camera/README.md @@ -10,6 +10,7 @@ The current design goal is: - absolute goals and preset restore are best-effort helpers layered on top - reusable math, preset, analysis, and keyframe-track utilities live in shared headers - reusable playback cursor and timeline helpers live in shared headers +- reusable preset and keyframe persistence helpers live in shared headers - `61_UI` is the current integration and validation surface ## Mental model @@ -292,6 +293,18 @@ Provides: This keeps playback-time progression reusable without forcing examples to reimplement cursor stepping rules. +### `CCameraPersistence.hpp` + +Reusable JSON and file persistence helpers for preset collections and keyframe tracks. + +Provides: + +- preset collection JSON serialization and deserialization +- preset collection file save/load helpers +- keyframe-track file save/load helpers + +This keeps example-level save/load glue out of `61_UI` while reusing the same preset and track formats. + ## Current integration status The shared headers above are designed to be reusable by additional examples. @@ -310,12 +323,13 @@ If another example wants to adopt this stack later, the intended path is: 2. Store default binding layouts on the camera and/or planar projection. 3. Use `CGimbalInputBinder` at runtime to translate external input into virtual events. 4. Feed the resulting event stream into `ICamera::manipulate(...)`. -5. Use `CCameraGoalSolver`, `CCameraGoalAnalysis`, `CCameraPreset`, `CCameraKeyframeTrack`, and `CCameraPlaybackTimeline` only for tooling features such as: +5. Use `CCameraGoalSolver`, `CCameraGoalAnalysis`, `CCameraPreset`, `CCameraKeyframeTrack`, `CCameraPlaybackTimeline`, and `CCameraPersistence` only for tooling features such as: - preset capture - preset restore - compatibility preview - playback interpolation - playback cursor stepping + - preset and keyframe persistence - scripted validation That keeps the hot runtime path event-driven while still allowing higher-level tools to work with absolute camera goals in a controlled way. From 2e85d7a6ed47006b2bc67053d7481a0fe7d7f1fc Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 18:11:31 +0200 Subject: [PATCH 150/205] Promote camera preset flow into shared headers --- 61_UI/AppControlPanel.cpp | 4 +- 61_UI/AppInit.cpp | 38 +++++----- 61_UI/AppUpdate.cpp | 2 +- 61_UI/include/app/App.hpp | 64 ++--------------- 61_UI/include/common.hpp | 1 + common/include/camera/CCameraPresetFlow.hpp | 79 +++++++++++++++++++++ common/include/camera/README.md | 17 ++++- 7 files changed, 122 insertions(+), 83 deletions(-) create mode 100644 common/include/camera/CCameraPresetFlow.hpp diff --git a/61_UI/AppControlPanel.cpp b/61_UI/AppControlPanel.cpp index 5c5ed21f3..a08121c21 100644 --- a/61_UI/AppControlPanel.cpp +++ b/61_UI/AppControlPanel.cpp @@ -687,7 +687,7 @@ void App::DrawControlPanel() if (ImGui::Button("Add preset")) { CameraPreset preset; - if (tryCapturePreset(activeCamera, m_presetName, preset)) + if (nbl::hlsl::tryCapturePreset(m_cameraGoalSolver, activeCamera, m_presetName, preset)) { m_presets.emplace_back(std::move(preset)); m_selectedPresetIx = static_cast(m_presets.size()) - 1; @@ -907,7 +907,7 @@ void App::DrawControlPanel() const float authoredTime = std::max(0.f, m_newKeyframeTime); keyframe.time = authoredTime; m_newKeyframeTime = authoredTime; - if (tryCapturePreset(activeCamera, "Keyframe", keyframe.preset)) + if (nbl::hlsl::tryCapturePreset(m_cameraGoalSolver, activeCamera, "Keyframe", keyframe.preset)) { m_keyframeTrack.keyframes.emplace_back(std::move(keyframe)); sortKeyframesByTime(); diff --git a/61_UI/AppInit.cpp b/61_UI/AppInit.cpp index 6382eb0f3..8c9707f62 100644 --- a/61_UI/AppInit.cpp +++ b/61_UI/AppInit.cpp @@ -317,12 +317,12 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) auto comparePresetToCamera = [&](ICamera* camera, const CameraPreset& preset, const double posEps, const double rotEpsDeg, const double scalarEps) -> bool { - return comparePresetToCameraState(camera, preset, posEps, rotEpsDeg, scalarEps); + return nbl::hlsl::comparePresetToCameraState(m_cameraGoalSolver, camera, preset, posEps, rotEpsDeg, scalarEps); }; auto describePresetMismatch = [&](ICamera* camera, const CameraPreset& preset) -> std::string { - return describePresetCameraMismatch(camera, preset); + return nbl::hlsl::describePresetCameraMismatch(m_cameraGoalSolver, camera, preset); }; auto collectKeyboardVirtualEvents = [&](CGimbalInputBinder& inputBinder, const ui::E_KEY_CODE keyCode) -> std::vector @@ -402,7 +402,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) CGimbalInputBinder inputBinder; camera->copyDefaultInputBindingPresetTo(inputBinder); - const auto initialPreset = capturePreset(camera, "smoke-initial"); + const auto initialPreset = nbl::hlsl::capturePreset(m_cameraGoalSolver, camera, "smoke-initial"); const auto initialCompatibility = analyzePresetCompatibility(camera, initialPreset); if (!initialCompatibility.exact || initialCompatibility.missingGoalStateMask != ICamera::GoalStateNone) return fail("Preset compatibility smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\". missing=" + describeGoalStateMask(initialCompatibility.missingGoalStateMask)); @@ -431,7 +431,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) default: break; } - if (!applyPresetToCamera(camera, initialPreset)) + if (!nbl::hlsl::applyPreset(m_cameraGoalSolver, camera, initialPreset)) return fail("Preset no-op smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\"."); if (initialPreset.goal.hasTargetPosition) @@ -439,7 +439,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) CameraPreset shiftedPreset = initialPreset; shiftedPreset.goal.targetPosition += float64_t3(0.5, -0.25, 0.75); - const auto shiftedResult = applyPresetToCameraDetailed(camera, shiftedPreset); + const auto shiftedResult = nbl::hlsl::applyPresetDetailed(m_cameraGoalSolver, camera, shiftedPreset); if (!shiftedResult.succeeded() || !shiftedResult.changed() || !shiftedResult.exact) return fail("Preset target apply smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\". " + describeApplyResult(shiftedResult)); @@ -447,7 +447,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) if (!camera->tryGetSphericalTargetState(shiftedState) || !nearlyEqual3(shiftedState.target, shiftedPreset.goal.targetPosition, 1e-9)) return fail("Preset target writeback smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\"."); - const auto restoredResult = applyPresetToCameraDetailed(camera, initialPreset); + const auto restoredResult = nbl::hlsl::applyPresetDetailed(m_cameraGoalSolver, camera, initialPreset); if (!restoredResult.succeeded() || !restoredResult.exact) return fail("Preset restore smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\". " + describeApplyResult(restoredResult)); @@ -473,11 +473,11 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) shiftedPreset.goal.dynamicPerspectiveState.referenceDistance = std::max(0.1f, initialPreset.goal.dynamicPerspectiveState.referenceDistance + 1.25f); - const auto shiftedResult = applyPresetToCameraDetailed(camera, shiftedPreset); + const auto shiftedResult = nbl::hlsl::applyPresetDetailed(m_cameraGoalSolver, camera, shiftedPreset); if (!shiftedResult.succeeded() || !shiftedResult.changed() || !comparePresetToCamera(camera, shiftedPreset, 1e-6, 0.1, 1e-6)) return fail("Preset dynamic perspective apply smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\". " + describeApplyResult(shiftedResult) + " " + describePresetMismatch(camera, shiftedPreset)); - const auto restoredResult = applyPresetToCameraDetailed(camera, initialPreset); + const auto restoredResult = nbl::hlsl::applyPresetDetailed(m_cameraGoalSolver, camera, initialPreset); if (!restoredResult.succeeded() || !comparePresetToCamera(camera, initialPreset, 1e-6, 0.1, 1e-6)) return fail("Preset dynamic perspective restore smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\". " + describeApplyResult(restoredResult) + " " + describePresetMismatch(camera, initialPreset)); } @@ -523,16 +523,16 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) if (!manipulateAndMeasure(camera, directEvents, directPosDelta, directRotDelta)) return fail("Direct manipulate smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\"."); { - const auto modifiedPreset = capturePreset(camera, "smoke-direct"); - const auto restoreInitial = applyPresetToCameraDetailed(camera, initialPreset); + const auto modifiedPreset = nbl::hlsl::capturePreset(m_cameraGoalSolver, camera, "smoke-direct"); + const auto restoreInitial = nbl::hlsl::applyPresetDetailed(m_cameraGoalSolver, camera, initialPreset); if (!restoreInitial.succeeded() || !comparePresetToCamera(camera, initialPreset, 1e-3, 0.1, 1e-4)) return fail("Preset restore from direct smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\". " + describePresetMismatch(camera, initialPreset)); - const auto applyModified = applyPresetToCameraDetailed(camera, modifiedPreset); + const auto applyModified = nbl::hlsl::applyPresetDetailed(m_cameraGoalSolver, camera, modifiedPreset); if (!applyModified.succeeded() || !applyModified.changed() || !comparePresetToCamera(camera, modifiedPreset, 1e-3, 0.1, 1e-4)) return fail("Preset apply from direct smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\". " + describePresetMismatch(camera, modifiedPreset)); - const auto restoreAgain = applyPresetToCameraDetailed(camera, initialPreset); + const auto restoreAgain = nbl::hlsl::applyPresetDetailed(m_cameraGoalSolver, camera, initialPreset); if (!restoreAgain.succeeded() || !comparePresetToCamera(camera, initialPreset, 1e-3, 0.1, 1e-4)) return fail("Preset final restore smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\". " + describePresetMismatch(camera, initialPreset)); } @@ -664,12 +664,12 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) ". missing=" + describeGoalStateMask(compatibility.missingGoalStateMask)); } - const auto baselinePreset = capturePreset(targetCamera, std::string(label) + "-baseline"); - const auto applyResult = applyPresetToCameraDetailed(targetCamera, sourcePreset); + const auto baselinePreset = nbl::hlsl::capturePreset(m_cameraGoalSolver, targetCamera, std::string(label) + "-baseline"); + const auto applyResult = nbl::hlsl::applyPresetDetailed(m_cameraGoalSolver, targetCamera, sourcePreset); if (!applyResult.succeeded() || !applyResult.approximate() || !applyResult.hasIssue(expectedIssue)) return fail(std::string("Cross-kind preset smoke failed for ") + label + ". " + describeApplyResult(applyResult)); - const auto restoreResult = applyPresetToCameraDetailed(targetCamera, baselinePreset); + const auto restoreResult = nbl::hlsl::applyPresetDetailed(m_cameraGoalSolver, targetCamera, baselinePreset); if (!restoreResult.succeeded() || !comparePresetToCamera(targetCamera, baselinePreset, 1e-6, 0.1, 1e-6)) return fail(std::string("Cross-kind preset restore smoke failed for ") + label + ". " + describeApplyResult(restoreResult) + " " + describePresetMismatch(targetCamera, baselinePreset)); @@ -688,15 +688,15 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) ". missing=" + describeGoalStateMask(compatibility.missingGoalStateMask)); } - const auto baselinePreset = capturePreset(targetCamera, std::string(label) + "-baseline"); - const auto applyResult = applyPresetToCameraDetailed(targetCamera, sourcePreset); + const auto baselinePreset = nbl::hlsl::capturePreset(m_cameraGoalSolver, targetCamera, std::string(label) + "-baseline"); + const auto applyResult = nbl::hlsl::applyPresetDetailed(m_cameraGoalSolver, targetCamera, sourcePreset); if (!applyResult.succeeded() || !applyResult.exact || !comparePresetToCamera(targetCamera, sourcePreset, 1e-6, 0.1, 1e-6)) { return fail(std::string("Exact cross-kind preset smoke failed for ") + label + ". " + describeApplyResult(applyResult) + " " + describePresetMismatch(targetCamera, sourcePreset)); } - const auto restoreResult = applyPresetToCameraDetailed(targetCamera, baselinePreset); + const auto restoreResult = nbl::hlsl::applyPresetDetailed(m_cameraGoalSolver, targetCamera, baselinePreset); if (!restoreResult.succeeded() || !restoreResult.exact || !comparePresetToCamera(targetCamera, baselinePreset, 1e-6, 0.1, 1e-6)) { return fail(std::string("Exact cross-kind preset restore smoke failed for ") + label + ". " + @@ -1609,7 +1609,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) { auto* camera = m_planarProjections[planarIx]->getCamera(); const std::string presetName = "Planar " + std::to_string(planarIx); - m_initialPlanarPresets.emplace_back(capturePreset(camera, presetName)); + m_initialPlanarPresets.emplace_back(nbl::hlsl::capturePreset(m_cameraGoalSolver, camera, presetName)); } } diff --git a/61_UI/AppUpdate.cpp b/61_UI/AppUpdate.cpp index d7ed6fe32..8f7ad6089 100644 --- a/61_UI/AppUpdate.cpp +++ b/61_UI/AppUpdate.cpp @@ -212,7 +212,7 @@ void App::update() } auto* camera = m_planarProjections[binding.activePlanarIx]->getCamera(); - if (!applyPresetToCamera(camera, m_initialPlanarPresets[binding.activePlanarIx])) + if (!nbl::hlsl::applyPreset(m_cameraGoalSolver, camera, m_initialPlanarPresets[binding.activePlanarIx])) m_logger->log("[script][warn] action reset_active_camera failed for planar: %u", ILogger::ELL_WARNING, binding.activePlanarIx); } break; } diff --git a/61_UI/include/app/App.hpp b/61_UI/include/app/App.hpp index 6cd19d853..b6fa2d388 100644 --- a/61_UI/include/app/App.hpp +++ b/61_UI/include/app/App.hpp @@ -1253,60 +1253,9 @@ class App final : public examples::SimpleWindowedApplication return analyzePresetForUi(camera, preset).matchesFilter(m_presetFilterMode); } - inline bool comparePresetToCameraState(ICamera* camera, const CameraPreset& preset, - const double posEps, const double rotEpsDeg, const double scalarEps) const - { - const auto capture = m_cameraGoalSolver.captureDetailed(camera); - if (!capture.canUseGoal()) - return false; - - return nbl::hlsl::compareGoals(capture.goal, nbl::hlsl::makeGoalFromPreset(preset), posEps, rotEpsDeg, scalarEps); - } - - inline std::string describePresetCameraMismatch(ICamera* camera, const CameraPreset& preset) const - { - const auto capture = m_cameraGoalSolver.captureDetailed(camera); - if (!capture.hasCamera) - return "camera=null"; - if (!capture.captured) - return "goal_state=unavailable"; - if (!capture.finiteGoal) - return "goal_state=invalid"; - - return nbl::hlsl::describeGoalMismatch(capture.goal, nbl::hlsl::makeGoalFromPreset(preset)); - } - - inline bool tryCapturePreset(ICamera* camera, const std::string& name, CameraPreset& preset) - { - const auto captureUi = analyzeCameraCaptureForUi(camera); - preset = {}; - preset.name = name; - if (!captureUi.canCapture || !camera) - return false; - - preset.identifier = std::string(camera->getIdentifier()); - nbl::hlsl::assignGoalToPreset(preset, captureUi.goal); - return true; - } - - inline CameraPreset capturePreset(ICamera* camera, const std::string& name) - { - CameraPreset preset; - tryCapturePreset(camera, name, preset); - return preset; - } - - inline CCameraGoalSolver::SApplyResult applyPresetToCameraDetailed(ICamera* camera, const CameraPreset& preset) - { - if (!camera) - return {}; - - return m_cameraGoalSolver.applyDetailed(camera, nbl::hlsl::makeGoalFromPreset(preset)); - } - inline CCameraGoalSolver::SApplyResult applyPresetFromUi(ICamera* camera, const CameraPreset& preset) { - const auto result = applyPresetToCameraDetailed(camera, preset); + const auto result = nbl::hlsl::applyPresetDetailed(m_cameraGoalSolver, camera, preset); const auto presetUi = analyzePresetForUi(camera, preset); storeApplyStatusBanner(m_manualPresetApplyBanner, describeApplyResult(result) + " | " + presetUi.compatibilityLabel, @@ -1315,11 +1264,6 @@ class App final : public examples::SimpleWindowedApplication return result; } - inline bool applyPresetToCamera(ICamera* camera, const CameraPreset& preset) - { - return applyPresetToCameraDetailed(camera, preset).succeeded(); - } - inline void storeApplyStatusBanner(ApplyStatusBanner& banner, std::string summary, const bool succeeded, const bool approximate) { banner.summary = std::move(summary); @@ -1430,7 +1374,7 @@ class App final : public examples::SimpleWindowedApplication CameraPreset preset; preset.goal.position = pos; preset.goal.orientation = glm::quat(hlsl::radians(clamped)); - applyPresetToCamera(camera, preset); + nbl::hlsl::applyPreset(m_cameraGoalSolver, camera, preset); } inline void applyVirtualEventScaling(std::vector& events, uint32_t count) @@ -1525,7 +1469,7 @@ class App final : public examples::SimpleWindowedApplication return; ++summary.targetCount; - const auto result = applyPresetToCameraDetailed(camera, preset); + const auto result = nbl::hlsl::applyPresetDetailed(m_cameraGoalSolver, camera, preset); if (result.succeeded()) { ++summary.successCount; @@ -1617,7 +1561,7 @@ class App final : public examples::SimpleWindowedApplication CameraPreset updatedPreset; const auto keyframeName = selected->preset.name.empty() ? std::string("Keyframe") : selected->preset.name; - if (!tryCapturePreset(camera, keyframeName, updatedPreset)) + if (!nbl::hlsl::tryCapturePreset(m_cameraGoalSolver, camera, keyframeName, updatedPreset)) return false; return nbl::hlsl::replaceSelectedKeyframePreset(m_keyframeTrack, std::move(updatedPreset)); diff --git a/61_UI/include/common.hpp b/61_UI/include/common.hpp index 9b322a831..55201746a 100644 --- a/61_UI/include/common.hpp +++ b/61_UI/include/common.hpp @@ -19,6 +19,7 @@ #include "camera/CDollyZoomCamera.hpp" #include "camera/CPathCamera.hpp" #include "camera/CCameraPreset.hpp" +#include "camera/CCameraPresetFlow.hpp" #include "camera/CCameraKeyframeTrack.hpp" #include "camera/CCameraPlaybackTimeline.hpp" #include "camera/CCameraPersistence.hpp" diff --git a/common/include/camera/CCameraPresetFlow.hpp b/common/include/camera/CCameraPresetFlow.hpp new file mode 100644 index 000000000..9013fdcbb --- /dev/null +++ b/common/include/camera/CCameraPresetFlow.hpp @@ -0,0 +1,79 @@ +// 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_PRESET_FLOW_HPP_ +#define _C_CAMERA_PRESET_FLOW_HPP_ + +#include +#include + +#include "CCameraGoalAnalysis.hpp" + +namespace nbl::hlsl +{ + +//! Reusable preset capture, comparison, and best-effort apply helpers. +inline bool comparePresetToCameraState(const CCameraGoalSolver& solver, ICamera* camera, const CCameraPreset& preset, + const double posEps, const double rotEpsDeg, const double scalarEps) +{ + const auto capture = solver.captureDetailed(camera); + if (!capture.canUseGoal()) + return false; + + return compareGoals(capture.goal, makeGoalFromPreset(preset), posEps, rotEpsDeg, scalarEps); +} + +inline std::string describePresetCameraMismatch(const CCameraGoalSolver& solver, ICamera* camera, const CCameraPreset& preset) +{ + const auto capture = solver.captureDetailed(camera); + if (!capture.hasCamera) + return "camera=null"; + if (!capture.captured) + return "goal_state=unavailable"; + if (!capture.finiteGoal) + return "goal_state=invalid"; + + return describeGoalMismatch(capture.goal, makeGoalFromPreset(preset)); +} + +inline bool tryCapturePreset(const SCameraCaptureAnalysis& captureAnalysis, ICamera* camera, std::string_view name, CCameraPreset& preset) +{ + preset = {}; + preset.name = std::string(name); + if (!captureAnalysis.canCapture || !camera) + return false; + + preset.identifier = std::string(camera->getIdentifier()); + assignGoalToPreset(preset, captureAnalysis.goal); + return true; +} + +inline bool tryCapturePreset(const CCameraGoalSolver& solver, ICamera* camera, std::string_view name, CCameraPreset& preset) +{ + return tryCapturePreset(analyzeCameraCapture(solver, camera), camera, name, preset); +} + +inline CCameraPreset capturePreset(const CCameraGoalSolver& solver, ICamera* camera, std::string_view name) +{ + CCameraPreset preset; + tryCapturePreset(solver, camera, name, preset); + return preset; +} + +inline CCameraGoalSolver::SApplyResult applyPresetDetailed(const CCameraGoalSolver& solver, ICamera* camera, const CCameraPreset& preset) +{ + if (!camera) + return {}; + + return solver.applyDetailed(camera, makeGoalFromPreset(preset)); +} + +inline bool applyPreset(const CCameraGoalSolver& solver, ICamera* camera, const CCameraPreset& preset) +{ + return applyPresetDetailed(solver, camera, preset).succeeded(); +} + +} // namespace nbl::hlsl + +#endif // _C_CAMERA_PRESET_FLOW_HPP_ diff --git a/common/include/camera/README.md b/common/include/camera/README.md index 663fadaea..50ddf673a 100644 --- a/common/include/camera/README.md +++ b/common/include/camera/README.md @@ -9,6 +9,7 @@ The current design goal is: - raw input binding stays outside the camera - absolute goals and preset restore are best-effort helpers layered on top - reusable math, preset, analysis, and keyframe-track utilities live in shared headers +- reusable preset capture and apply flow helpers live in shared headers - reusable playback cursor and timeline helpers live in shared headers - reusable preset and keyframe persistence helpers live in shared headers - `61_UI` is the current integration and validation surface @@ -262,6 +263,19 @@ Provides: This is the storage format used by `61_UI` for preset authoring and playback persistence. +### `CCameraPresetFlow.hpp` + +Reusable preset capture, comparison, mismatch, and best-effort apply helpers. + +Provides: + +- preset capture from a camera and solver +- preset apply through the shared goal solver +- preset-to-camera comparison helpers +- preset mismatch descriptions for diagnostics + +This keeps solver-backed preset flow reusable without leaving the flow rules in example-local glue. + ### `CCameraKeyframeTrack.hpp` Reusable keyframe-track helpers on top of presets. @@ -323,10 +337,11 @@ If another example wants to adopt this stack later, the intended path is: 2. Store default binding layouts on the camera and/or planar projection. 3. Use `CGimbalInputBinder` at runtime to translate external input into virtual events. 4. Feed the resulting event stream into `ICamera::manipulate(...)`. -5. Use `CCameraGoalSolver`, `CCameraGoalAnalysis`, `CCameraPreset`, `CCameraKeyframeTrack`, `CCameraPlaybackTimeline`, and `CCameraPersistence` only for tooling features such as: +5. Use `CCameraGoalSolver`, `CCameraGoalAnalysis`, `CCameraPreset`, `CCameraPresetFlow`, `CCameraKeyframeTrack`, `CCameraPlaybackTimeline`, and `CCameraPersistence` only for tooling features such as: - preset capture - preset restore - compatibility preview + - preset comparison and mismatch diagnostics - playback interpolation - playback cursor stepping - preset and keyframe persistence From c290cb0da608e7c70f598f95c2092924e42f622e Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 18:16:31 +0200 Subject: [PATCH 151/205] Share camera preset apply summaries --- 61_UI/include/app/App.hpp | 59 ++++----------------- 61_UI/include/common.hpp | 1 + common/include/camera/CCameraPresetFlow.hpp | 54 +++++++++++++++++++ common/include/camera/README.md | 1 + 4 files changed, 65 insertions(+), 50 deletions(-) diff --git a/61_UI/include/app/App.hpp b/61_UI/include/app/App.hpp index b6fa2d388..5b3c28da8 100644 --- a/61_UI/include/app/App.hpp +++ b/61_UI/include/app/App.hpp @@ -522,29 +522,6 @@ class App final : public examples::SimpleWindowedApplication bool overrideInput = true; }; - struct PlaybackApplySummary - { - uint32_t targetCount = 0u; - uint32_t successCount = 0u; - uint32_t approximateCount = 0u; - uint32_t failureCount = 0u; - - inline bool hasTargets() const - { - return targetCount > 0u; - } - - inline bool succeeded() const - { - return hasTargets() && failureCount == 0u; - } - - inline bool approximate() const - { - return approximateCount > 0u; - } - }; - struct ApplyStatusBanner { std::string summary; @@ -1278,7 +1255,7 @@ class App final : public examples::SimpleWindowedApplication banner.approximate = false; } - inline std::string describePlaybackApplySummary(const PlaybackApplySummary& summary) const + inline std::string describePlaybackApplySummary(const SCameraPresetApplySummary& summary) const { if (!summary.hasTargets()) return m_playbackAffectsAll ? "Playback apply | no cameras available" : "Playback apply | no active camera"; @@ -1292,7 +1269,7 @@ class App final : public examples::SimpleWindowedApplication return oss.str(); } - inline void storePlaybackApplySummary(const PlaybackApplySummary& summary) + inline void storePlaybackApplySummary(const SCameraPresetApplySummary& summary) { storeApplyStatusBanner(m_playbackApplyBanner, describePlaybackApplySummary(summary), @@ -1460,34 +1437,16 @@ class App final : public examples::SimpleWindowedApplication count = static_cast(events.size()); } - inline PlaybackApplySummary applyPresetToTargets(const CameraPreset& preset) + inline SCameraPresetApplySummary applyPresetToTargets(const CameraPreset& preset) { - PlaybackApplySummary summary; - auto applyToCamera = [&](ICamera* camera) -> void - { - if (!camera) - return; - - ++summary.targetCount; - const auto result = nbl::hlsl::applyPresetDetailed(m_cameraGoalSolver, camera, preset); - if (result.succeeded()) - { - ++summary.successCount; - if (result.approximate()) - ++summary.approximateCount; - } - else - { - ++summary.failureCount; - } - }; - if (!m_playbackAffectsAll) { - applyToCamera(getActiveCamera()); - return summary; + ICamera* activeCamera = getActiveCamera(); + return nbl::hlsl::applyPresetToCameraRange(m_cameraGoalSolver, std::span(&activeCamera, activeCamera ? 1u : 0u), preset); } + std::vector cameras; + cameras.reserve(windowBindings.size()); std::unordered_set visited; for (auto& binding : windowBindings) { @@ -1499,10 +1458,10 @@ class App final : public examples::SimpleWindowedApplication continue; const auto id = camera->getGimbal().getID(); if (visited.insert(id).second) - applyToCamera(camera); + cameras.push_back(camera); } - return summary; + return nbl::hlsl::applyPresetToCameraRange(m_cameraGoalSolver, std::span(cameras.data(), cameras.size()), preset); } inline bool tryBuildPlaybackPresetAtTime(const float time, CameraPreset& preset) diff --git a/61_UI/include/common.hpp b/61_UI/include/common.hpp index 55201746a..09f245c37 100644 --- a/61_UI/include/common.hpp +++ b/61_UI/include/common.hpp @@ -67,6 +67,7 @@ using nbl::hlsl::CCameraKeyframe; using nbl::hlsl::CCameraKeyframeTrack; using nbl::hlsl::CCameraPlaybackCursor; using nbl::hlsl::SCameraPlaybackAdvanceResult; +using nbl::hlsl::SCameraPresetApplySummary; using nbl::hlsl::SCameraGoalApplyAnalysis; using nbl::hlsl::SCameraCaptureAnalysis; using nbl::hlsl::CCameraGoalSolver; diff --git a/common/include/camera/CCameraPresetFlow.hpp b/common/include/camera/CCameraPresetFlow.hpp index 9013fdcbb..9a1597fda 100644 --- a/common/include/camera/CCameraPresetFlow.hpp +++ b/common/include/camera/CCameraPresetFlow.hpp @@ -5,8 +5,10 @@ #ifndef _C_CAMERA_PRESET_FLOW_HPP_ #define _C_CAMERA_PRESET_FLOW_HPP_ +#include #include #include +#include #include "CCameraGoalAnalysis.hpp" @@ -14,6 +16,29 @@ namespace nbl::hlsl { //! Reusable preset capture, comparison, and best-effort apply helpers. +struct SCameraPresetApplySummary +{ + uint32_t targetCount = 0u; + uint32_t successCount = 0u; + uint32_t approximateCount = 0u; + uint32_t failureCount = 0u; + + inline bool hasTargets() const + { + return targetCount > 0u; + } + + inline bool succeeded() const + { + return hasTargets() && failureCount == 0u; + } + + inline bool approximate() const + { + return approximateCount > 0u; + } +}; + inline bool comparePresetToCameraState(const CCameraGoalSolver& solver, ICamera* camera, const CCameraPreset& preset, const double posEps, const double rotEpsDeg, const double scalarEps) { @@ -74,6 +99,35 @@ inline bool applyPreset(const CCameraGoalSolver& solver, ICamera* camera, const return applyPresetDetailed(solver, camera, preset).succeeded(); } +inline void accumulatePresetApplySummary(SCameraPresetApplySummary& summary, const CCameraGoalSolver::SApplyResult& result) +{ + ++summary.targetCount; + if (result.succeeded()) + { + ++summary.successCount; + if (result.approximate()) + ++summary.approximateCount; + } + else + { + ++summary.failureCount; + } +} + +inline SCameraPresetApplySummary applyPresetToCameraRange(const CCameraGoalSolver& solver, std::span cameras, const CCameraPreset& preset) +{ + SCameraPresetApplySummary summary; + for (auto* camera : cameras) + { + if (!camera) + continue; + + accumulatePresetApplySummary(summary, applyPresetDetailed(solver, camera, preset)); + } + + return summary; +} + } // namespace nbl::hlsl #endif // _C_CAMERA_PRESET_FLOW_HPP_ diff --git a/common/include/camera/README.md b/common/include/camera/README.md index 50ddf673a..1f472e643 100644 --- a/common/include/camera/README.md +++ b/common/include/camera/README.md @@ -271,6 +271,7 @@ Provides: - preset capture from a camera and solver - preset apply through the shared goal solver +- preset apply summaries across camera ranges - preset-to-camera comparison helpers - preset mismatch descriptions for diagnostics From 60d18bbbb4705389031d681a5aafed757f386dd6 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 18:34:01 +0200 Subject: [PATCH 152/205] Harden shared camera helper smoke coverage --- 61_UI/AppInit.cpp | 144 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) diff --git a/61_UI/AppInit.cpp b/61_UI/AppInit.cpp index 8c9707f62..ef35fab9c 100644 --- a/61_UI/AppInit.cpp +++ b/61_UI/AppInit.cpp @@ -706,6 +706,18 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) return true; }; + auto comparePresetValues = [&](const CameraPreset& lhs, const CameraPreset& rhs, const double posEps, const double rotEpsDeg, const double scalarEps) -> bool + { + return lhs.name == rhs.name && + lhs.identifier == rhs.identifier && + nbl::hlsl::compareGoals( + nbl::hlsl::makeGoalFromPreset(lhs), + nbl::hlsl::makeGoalFromPreset(rhs), + posEps, + rotEpsDeg, + scalarEps); + }; + ICamera* orbitCamera = findCameraByKind(ICamera::CameraKind::Orbit); ICamera* chaseCamera = findCameraByKind(ICamera::CameraKind::Chase); ICamera* dollyCamera = findCameraByKind(ICamera::CameraKind::Dolly); @@ -749,6 +761,138 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) } } + { + std::vector sourcePresets; + if (hasOrbitPreset) + sourcePresets.push_back(initialOrbitPreset); + if (hasChasePreset) + sourcePresets.push_back(initialChasePreset); + if (hasDollyPreset) + sourcePresets.push_back(initialDollyPreset); + if (hasPathPreset) + sourcePresets.push_back(initialPathPreset); + if (hasDollyZoomPreset) + sourcePresets.push_back(initialDollyZoomPreset); + + if (sourcePresets.empty()) + return fail("Preset persistence smoke failed to collect source presets."); + + std::stringstream presetBuffer; + if (!nbl::hlsl::writePresetCollection(presetBuffer, std::span(sourcePresets.data(), sourcePresets.size()))) + return fail("Preset persistence smoke failed to serialize preset collection."); + + std::vector loadedPresets; + if (!nbl::hlsl::readPresetCollection(presetBuffer, loadedPresets)) + return fail("Preset persistence smoke failed to deserialize preset collection."); + if (loadedPresets.size() != sourcePresets.size()) + return fail("Preset persistence smoke changed preset count."); + + for (size_t i = 0u; i < sourcePresets.size(); ++i) + { + if (!comparePresetValues(sourcePresets[i], loadedPresets[i], 1e-6, 0.1, 1e-6)) + return fail("Preset persistence smoke changed preset content at index " + std::to_string(i) + "."); + } + + CCameraKeyframeTrack sourceTrack; + sourceTrack.keyframes.reserve(sourcePresets.size()); + for (size_t i = 0u; i < sourcePresets.size(); ++i) + { + CameraKeyframe 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; + + std::stringstream keyframeBuffer; + if (!nbl::hlsl::writeKeyframeTrack(keyframeBuffer, sourceTrack)) + return fail("Keyframe persistence smoke failed to serialize track."); + + CCameraKeyframeTrack loadedTrack; + if (!nbl::hlsl::readKeyframeTrack(keyframeBuffer, loadedTrack)) + return fail("Keyframe persistence smoke failed to deserialize track."); + if (loadedTrack.keyframes.size() != sourceTrack.keyframes.size()) + return fail("Keyframe persistence smoke changed keyframe count."); + + for (size_t i = 0u; i < sourceTrack.keyframes.size(); ++i) + { + if (std::abs(static_cast(loadedTrack.keyframes[i].time - sourceTrack.keyframes[i].time)) > 1e-6) + return fail("Keyframe persistence smoke changed keyframe time at index " + std::to_string(i) + "."); + if (!comparePresetValues(sourceTrack.keyframes[i].preset, loadedTrack.keyframes[i].preset, 1e-6, 0.1, 1e-6)) + return fail("Keyframe persistence smoke changed keyframe preset at index " + std::to_string(i) + "."); + } + } + + if (hasOrbitPreset && hasDollyPreset) + { + CCameraKeyframeTrack playbackTrack; + { + CameraKeyframe a; + a.time = 0.f; + a.preset = initialOrbitPreset; + playbackTrack.keyframes.push_back(a); + } + { + CameraKeyframe b; + b.time = 2.f; + b.preset = initialDollyPreset; + playbackTrack.keyframes.push_back(b); + } + + CCameraPlaybackCursor cursor; + cursor.playing = true; + cursor.loop = false; + cursor.speed = 1.f; + cursor.time = 1.5f; + + const auto advanceToEnd = nbl::hlsl::advancePlaybackCursor(cursor, playbackTrack, 1.0); + if (!advanceToEnd.hasTrack || !advanceToEnd.changedTime || !advanceToEnd.reachedEnd || !advanceToEnd.stopped || advanceToEnd.wrapped) + return fail("Playback timeline smoke failed for non-loop end-of-track advance."); + if (std::abs(static_cast(advanceToEnd.time - 2.f)) > 1e-6) + return fail("Playback timeline smoke produced wrong end-of-track time."); + + nbl::hlsl::resetPlaybackCursor(cursor, 1.25f); + if (cursor.playing || std::abs(static_cast(cursor.time - 1.25f)) > 1e-6) + return fail("Playback timeline smoke failed to reset cursor."); + + cursor.playing = true; + cursor.loop = true; + cursor.speed = 1.f; + cursor.time = 1.5f; + const auto advanceLoop = nbl::hlsl::advancePlaybackCursor(cursor, playbackTrack, 1.0); + if (!advanceLoop.hasTrack || !advanceLoop.changedTime || !advanceLoop.wrapped || advanceLoop.stopped || advanceLoop.reachedEnd) + return fail("Playback timeline smoke failed for looped advance."); + if (std::abs(static_cast(advanceLoop.time - 0.5f)) > 1e-6) + return fail("Playback timeline smoke produced wrong wrapped time."); + + cursor.time = 9.f; + nbl::hlsl::clampPlaybackCursorToTrack(playbackTrack, cursor); + if (std::abs(static_cast(cursor.time - 2.f)) > 1e-6) + return fail("Playback timeline smoke failed to clamp cursor time."); + } + + if (hasOrbitPreset && orbitCamera) + { + std::array exactTargets = { orbitCamera, nullptr }; + const auto exactSummary = nbl::hlsl::applyPresetToCameraRange( + m_cameraGoalSolver, + std::span(exactTargets.data(), exactTargets.size()), + initialOrbitPreset); + if (exactSummary.targetCount != 1u || exactSummary.successCount != 1u || exactSummary.approximateCount != 0u || exactSummary.failureCount != 0u) + return fail("Preset apply summary smoke failed for exact target range."); + } + + if (hasPathPreset && orbitCamera) + { + std::array approximateTargets = { orbitCamera }; + const auto approximateSummary = nbl::hlsl::applyPresetToCameraRange( + m_cameraGoalSolver, + std::span(approximateTargets.data(), approximateTargets.size()), + initialPathPreset); + if (approximateSummary.targetCount != 1u || approximateSummary.successCount != 1u || approximateSummary.approximateCount != 1u || approximateSummary.failureCount != 0u) + return fail("Preset apply summary smoke failed for approximate target range."); + } + m_headlessCameraSmokePassed = true; std::cout << "[headless-camera-smoke] PASS cameras=" << cameras.size() << std::endl; return true; From fe333a26e290f1ca6f162be3c74fc5334b99e417 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 18:37:50 +0200 Subject: [PATCH 153/205] Document and harden shared camera helpers --- 61_UI/AppInit.cpp | 49 +++++++++++++++++++ common/include/camera/CCameraPersistence.hpp | 10 ++++ .../camera/CCameraPlaybackTimeline.hpp | 6 +++ common/include/camera/CCameraPresetFlow.hpp | 11 ++++- 4 files changed, 75 insertions(+), 1 deletion(-) diff --git a/61_UI/AppInit.cpp b/61_UI/AppInit.cpp index ef35fab9c..bd61d4b91 100644 --- a/61_UI/AppInit.cpp +++ b/61_UI/AppInit.cpp @@ -821,6 +821,55 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) if (!comparePresetValues(sourceTrack.keyframes[i].preset, loadedTrack.keyframes[i].preset, 1e-6, 0.1, 1e-6)) return fail("Keyframe persistence smoke changed keyframe preset at index " + std::to_string(i) + "."); } + + 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 (!nbl::hlsl::savePresetCollectionToFile(presetFile, std::span(sourcePresets.data(), sourcePresets.size()))) + return fail("Preset persistence smoke failed to save preset collection file."); + + std::vector fileLoadedPresets; + if (!nbl::hlsl::loadPresetCollectionFromFile(presetFile, fileLoadedPresets)) + return fail("Preset persistence smoke failed to load preset collection file."); + if (fileLoadedPresets.size() != sourcePresets.size()) + return fail("Preset persistence smoke changed preset file count."); + + for (size_t i = 0u; i < sourcePresets.size(); ++i) + { + if (!comparePresetValues(sourcePresets[i], fileLoadedPresets[i], 1e-6, 0.1, 1e-6)) + return fail("Preset persistence smoke changed preset file content at index " + std::to_string(i) + "."); + } + + if (!nbl::hlsl::saveKeyframeTrackToFile(keyframeFile, sourceTrack)) + return fail("Keyframe persistence smoke failed to save track file."); + + CCameraKeyframeTrack fileLoadedTrack; + if (!nbl::hlsl::loadKeyframeTrackFromFile(keyframeFile, fileLoadedTrack)) + return fail("Keyframe persistence smoke failed to load track file."); + if (fileLoadedTrack.keyframes.size() != sourceTrack.keyframes.size()) + return fail("Keyframe persistence smoke changed keyframe file count."); + + for (size_t i = 0u; i < sourceTrack.keyframes.size(); ++i) + { + if (std::abs(static_cast(fileLoadedTrack.keyframes[i].time - sourceTrack.keyframes[i].time)) > 1e-6) + return fail("Keyframe persistence smoke changed keyframe file time at index " + std::to_string(i) + "."); + if (!comparePresetValues(sourceTrack.keyframes[i].preset, fileLoadedTrack.keyframes[i].preset, 1e-6, 0.1, 1e-6)) + return fail("Keyframe persistence smoke changed keyframe file preset at index " + std::to_string(i) + "."); + } } if (hasOrbitPreset && hasDollyPreset) diff --git a/common/include/camera/CCameraPersistence.hpp b/common/include/camera/CCameraPersistence.hpp index 9cf1014c2..a1fb737fb 100644 --- a/common/include/camera/CCameraPersistence.hpp +++ b/common/include/camera/CCameraPersistence.hpp @@ -15,6 +15,7 @@ namespace nbl::hlsl { //! JSON and file persistence helpers for reusable camera presets and playback tracks. +//! Stream-based helpers exist first so examples can choose between file IO and in-memory round-trips. inline nlohmann::json serializePresetCollection(std::span presets) { nlohmann::json root; @@ -24,6 +25,7 @@ inline nlohmann::json serializePresetCollection(std::span p return root; } +//! Parse a preset collection from JSON and replace the destination vector on success. inline bool deserializePresetCollection(const nlohmann::json& root, std::vector& presets) { if (!root.contains("presets") || !root["presets"].is_array()) @@ -42,6 +44,7 @@ inline bool deserializePresetCollection(const nlohmann::json& root, std::vector< return true; } +//! Serialize a preset collection to an arbitrary stream. inline bool writePresetCollection(std::ostream& out, std::span presets, const int indent = 2) { if (!out) @@ -51,6 +54,7 @@ inline bool writePresetCollection(std::ostream& out, std::span(out); } +//! Deserialize a preset collection from an arbitrary stream. inline bool readPresetCollection(std::istream& in, std::vector& presets) { if (!in) @@ -61,18 +65,21 @@ inline bool readPresetCollection(std::istream& in, std::vector& p return deserializePresetCollection(root, presets); } +//! Convenience file wrapper around `writePresetCollection`. inline bool savePresetCollectionToFile(const system::path& path, std::span presets, const int indent = 2) { std::ofstream out(path.string(), std::ios::binary); return writePresetCollection(out, presets, indent); } +//! Convenience file wrapper around `readPresetCollection`. inline bool loadPresetCollectionFromFile(const system::path& path, std::vector& presets) { std::ifstream in(path.string(), std::ios::binary); return readPresetCollection(in, presets); } +//! Serialize a keyframe track to an arbitrary stream. inline bool writeKeyframeTrack(std::ostream& out, const CCameraKeyframeTrack& track, const int indent = 2) { if (!out) @@ -82,6 +89,7 @@ inline bool writeKeyframeTrack(std::ostream& out, const CCameraKeyframeTrack& tr return static_cast(out); } +//! Deserialize a keyframe track from an arbitrary stream. inline bool readKeyframeTrack(std::istream& in, CCameraKeyframeTrack& track) { if (!in) @@ -92,12 +100,14 @@ inline bool readKeyframeTrack(std::istream& in, CCameraKeyframeTrack& track) return deserializeKeyframeTrack(root, track); } +//! Convenience file wrapper around `writeKeyframeTrack`. inline bool saveKeyframeTrackToFile(const system::path& path, const CCameraKeyframeTrack& track, const int indent = 2) { std::ofstream out(path.string(), std::ios::binary); return writeKeyframeTrack(out, track, indent); } +//! Convenience file wrapper around `readKeyframeTrack`. inline bool loadKeyframeTrackFromFile(const system::path& path, CCameraKeyframeTrack& track) { std::ifstream in(path.string(), std::ios::binary); diff --git a/common/include/camera/CCameraPlaybackTimeline.hpp b/common/include/camera/CCameraPlaybackTimeline.hpp index 7a38b5c47..dc23726f3 100644 --- a/common/include/camera/CCameraPlaybackTimeline.hpp +++ b/common/include/camera/CCameraPlaybackTimeline.hpp @@ -11,6 +11,7 @@ namespace nbl::hlsl { //! Shared playback cursor state for camera keyframe tracks. +//! The cursor is intentionally transport-only so examples can own higher-level playback policy. struct CCameraPlaybackCursor { bool playing = false; @@ -20,6 +21,7 @@ struct CCameraPlaybackCursor }; //! Outcome of advancing a playback cursor against a keyframe track. +//! This separates raw time stepping from higher-level example policy and UI feedback. struct SCameraPlaybackAdvanceResult { bool hasTrack = false; @@ -31,6 +33,7 @@ struct SCameraPlaybackAdvanceResult float time = 0.f; }; +//! Duration of the current playback track in seconds. inline float getPlaybackTrackDuration(const CCameraKeyframeTrack& track) { if (track.keyframes.empty()) @@ -39,17 +42,20 @@ inline float getPlaybackTrackDuration(const CCameraKeyframeTrack& track) return track.keyframes.back().time; } +//! Reset cursor time and stop playback without mutating loop or speed settings. inline void resetPlaybackCursor(CCameraPlaybackCursor& cursor, const float time = 0.f) { cursor.playing = false; cursor.time = std::max(0.f, time); } +//! Clamp cursor time into the valid time range of the current track. inline void clampPlaybackCursorToTrack(const CCameraKeyframeTrack& track, CCameraPlaybackCursor& cursor) { clampTrackTimeToKeyframes(track, cursor.time); } +//! Advance cursor time by `dtSec * speed` and report whether playback wrapped or stopped. inline SCameraPlaybackAdvanceResult advancePlaybackCursor(CCameraPlaybackCursor& cursor, const CCameraKeyframeTrack& track, const double dtSec) { SCameraPlaybackAdvanceResult result; diff --git a/common/include/camera/CCameraPresetFlow.hpp b/common/include/camera/CCameraPresetFlow.hpp index 9a1597fda..e73cfcb4d 100644 --- a/common/include/camera/CCameraPresetFlow.hpp +++ b/common/include/camera/CCameraPresetFlow.hpp @@ -15,7 +15,7 @@ namespace nbl::hlsl { -//! Reusable preset capture, comparison, and best-effort apply helpers. +//! Reusable aggregate summary for applying one preset to multiple cameras. struct SCameraPresetApplySummary { uint32_t targetCount = 0u; @@ -39,6 +39,7 @@ struct SCameraPresetApplySummary } }; +//! Compare the current camera state against a preset using the shared goal representation. inline bool comparePresetToCameraState(const CCameraGoalSolver& solver, ICamera* camera, const CCameraPreset& preset, const double posEps, const double rotEpsDeg, const double scalarEps) { @@ -49,6 +50,7 @@ inline bool comparePresetToCameraState(const CCameraGoalSolver& solver, ICamera* return compareGoals(capture.goal, makeGoalFromPreset(preset), posEps, rotEpsDeg, scalarEps); } +//! Explain the first visible mismatch between a camera state and a preset. inline std::string describePresetCameraMismatch(const CCameraGoalSolver& solver, ICamera* camera, const CCameraPreset& preset) { const auto capture = solver.captureDetailed(camera); @@ -62,6 +64,7 @@ inline std::string describePresetCameraMismatch(const CCameraGoalSolver& solver, return describeGoalMismatch(capture.goal, makeGoalFromPreset(preset)); } +//! Build a preset from an already analyzed capture result. inline bool tryCapturePreset(const SCameraCaptureAnalysis& captureAnalysis, ICamera* camera, std::string_view name, CCameraPreset& preset) { preset = {}; @@ -74,11 +77,13 @@ inline bool tryCapturePreset(const SCameraCaptureAnalysis& captureAnalysis, ICam return true; } +//! Capture a preset directly from a camera through the shared goal solver. inline bool tryCapturePreset(const CCameraGoalSolver& solver, ICamera* camera, std::string_view name, CCameraPreset& preset) { return tryCapturePreset(analyzeCameraCapture(solver, camera), camera, name, preset); } +//! Value-returning convenience wrapper around `tryCapturePreset`. inline CCameraPreset capturePreset(const CCameraGoalSolver& solver, ICamera* camera, std::string_view name) { CCameraPreset preset; @@ -86,6 +91,7 @@ inline CCameraPreset capturePreset(const CCameraGoalSolver& solver, ICamera* cam return preset; } +//! Apply a preset through the shared goal solver and preserve detailed apply diagnostics. inline CCameraGoalSolver::SApplyResult applyPresetDetailed(const CCameraGoalSolver& solver, ICamera* camera, const CCameraPreset& preset) { if (!camera) @@ -94,11 +100,13 @@ inline CCameraGoalSolver::SApplyResult applyPresetDetailed(const CCameraGoalSolv return solver.applyDetailed(camera, makeGoalFromPreset(preset)); } +//! Bool-returning convenience wrapper around `applyPresetDetailed`. inline bool applyPreset(const CCameraGoalSolver& solver, ICamera* camera, const CCameraPreset& preset) { return applyPresetDetailed(solver, camera, preset).succeeded(); } +//! Fold one detailed apply result into an aggregate preset-apply summary. inline void accumulatePresetApplySummary(SCameraPresetApplySummary& summary, const CCameraGoalSolver::SApplyResult& result) { ++summary.targetCount; @@ -114,6 +122,7 @@ inline void accumulatePresetApplySummary(SCameraPresetApplySummary& summary, con } } +//! Apply one preset to a camera range and collect a typed aggregate summary. inline SCameraPresetApplySummary applyPresetToCameraRange(const CCameraGoalSolver& solver, std::span cameras, const CCameraPreset& preset) { SCameraPresetApplySummary summary; From 499dde6dbe6cfac719671d0bef7872041d500719 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 19:04:40 +0200 Subject: [PATCH 154/205] Promote camera manipulation utilities into shared headers --- 61_UI/AppUpdate.cpp | 6 +- 61_UI/include/app/App.hpp | 143 +-------------- 61_UI/include/common.hpp | 2 + .../camera/CCameraManipulationUtilities.hpp | 167 ++++++++++++++++++ common/include/camera/README.md | 13 ++ 5 files changed, 186 insertions(+), 145 deletions(-) create mode 100644 common/include/camera/CCameraManipulationUtilities.hpp diff --git a/61_UI/AppUpdate.cpp b/61_UI/AppUpdate.cpp index 8f7ad6089..ba4ba5439 100644 --- a/61_UI/AppUpdate.cpp +++ b/61_UI/AppUpdate.cpp @@ -351,7 +351,7 @@ void App::update() for (uint32_t i = 0u; i < vKeyboardEventsCount; ++i) virtualEvents[i].magnitude *= m_cameraControls.keyboardScale; - applyVirtualEventScaling(virtualEvents, vCount); + nbl::hlsl::scaleVirtualEvents(virtualEvents, vCount, m_cameraControls.translationScale, m_cameraControls.rotationScale); const char* bindingLabel = "Keyboard/Mouse"; auto applyEventsToCamera = [&](ICamera* target, uint32_t planarIx) @@ -363,7 +363,7 @@ void App::update() { std::vector perCameraEvents(virtualEvents.begin(), virtualEvents.begin() + vCount); uint32_t perCount = vCount; - remapTranslationToWorld(target, perCameraEvents, perCount); + nbl::hlsl::remapTranslationEventsFromWorldToCameraLocal(target, perCameraEvents, perCount); if (perCount) target->manipulate({ perCameraEvents.data(), perCount }); } @@ -372,7 +372,7 @@ void App::update() target->manipulate({ virtualEvents.data(), vCount }); } - applyConstraintsToCamera(target); + nbl::hlsl::applyCameraConstraints(m_cameraGoalSolver, target, m_cameraConstraints); appendVirtualEventLog("input", bindingLabel, planarIx, target, virtualEvents.data(), vCount); }; diff --git a/61_UI/include/app/App.hpp b/61_UI/include/app/App.hpp index 5b3c28da8..de84a5b83 100644 --- a/61_UI/include/app/App.hpp +++ b/61_UI/include/app/App.hpp @@ -545,22 +545,7 @@ class App final : public examples::SimpleWindowedApplication float rotationScale = 1.0f; }; - struct CameraConstraintSettings - { - bool enabled = false; - bool clampPitch = false; - bool clampYaw = false; - bool clampRoll = false; - bool clampDistance = false; - float pitchMinDeg = -80.f; - float pitchMaxDeg = 80.f; - float yawMinDeg = -180.f; - float yawMaxDeg = 180.f; - float rollMinDeg = -180.f; - float rollMaxDeg = 180.f; - float minDistance = 0.1f; - float maxDistance = 1000.f; - }; + using CameraConstraintSettings = SCameraConstraintSettings; inline ICamera* getActiveCamera() { @@ -1311,132 +1296,6 @@ class App final : public examples::SimpleWindowedApplication m_virtualEventLog.pop_front(); } - inline void applyConstraintsToCamera(ICamera* camera) - { - if (!m_cameraConstraints.enabled || !camera) - return; - - if (camera->hasCapability(ICamera::SphericalTarget)) - { - if (m_cameraConstraints.clampDistance) - { - ICamera::SphericalTargetState sphericalState; - if (camera->tryGetSphericalTargetState(sphericalState)) - { - const float clamped = std::clamp(sphericalState.distance, m_cameraConstraints.minDistance, m_cameraConstraints.maxDistance); - camera->trySetSphericalDistance(clamped); - } - } - return; - } - - if (!(m_cameraConstraints.clampPitch || m_cameraConstraints.clampYaw || m_cameraConstraints.clampRoll)) - return; - - const auto& gimbal = camera->getGimbal(); - const auto pos = gimbal.getPosition(); - const auto eulerDeg = glm::degrees(glm::eulerAngles(gimbal.getOrientation())); - - auto clamped = eulerDeg; - if (m_cameraConstraints.clampPitch) - clamped.x = std::clamp(clamped.x, m_cameraConstraints.pitchMinDeg, m_cameraConstraints.pitchMaxDeg); - if (m_cameraConstraints.clampYaw) - clamped.y = std::clamp(clamped.y, m_cameraConstraints.yawMinDeg, m_cameraConstraints.yawMaxDeg); - if (m_cameraConstraints.clampRoll) - clamped.z = std::clamp(clamped.z, m_cameraConstraints.rollMinDeg, m_cameraConstraints.rollMaxDeg); - - if (clamped.x == eulerDeg.x && clamped.y == eulerDeg.y && clamped.z == eulerDeg.z) - return; - - CameraPreset preset; - preset.goal.position = pos; - preset.goal.orientation = glm::quat(hlsl::radians(clamped)); - nbl::hlsl::applyPreset(m_cameraGoalSolver, camera, preset); - } - - inline void applyVirtualEventScaling(std::vector& events, uint32_t count) - { - for (uint32_t i = 0u; i < count; ++i) - { - auto& ev = events[i]; - const auto type = ev.type; - - if (type == CVirtualGimbalEvent::MoveForward || type == CVirtualGimbalEvent::MoveBackward || - type == CVirtualGimbalEvent::MoveLeft || type == CVirtualGimbalEvent::MoveRight || - type == CVirtualGimbalEvent::MoveUp || type == CVirtualGimbalEvent::MoveDown) - { - ev.magnitude *= m_cameraControls.translationScale; - } - else if (type == CVirtualGimbalEvent::TiltUp || type == CVirtualGimbalEvent::TiltDown || - type == CVirtualGimbalEvent::PanLeft || type == CVirtualGimbalEvent::PanRight || - type == CVirtualGimbalEvent::RollLeft || type == CVirtualGimbalEvent::RollRight) - { - ev.magnitude *= m_cameraControls.rotationScale; - } - } - } - - inline void remapTranslationToWorld(ICamera* camera, std::vector& events, uint32_t& count) - { - if (!camera) - return; - - float64_t3 worldDelta = float64_t3(0.0); - std::vector filtered; - filtered.reserve(events.size()); - - for (uint32_t i = 0u; i < count; ++i) - { - const auto& ev = events[i]; - switch (ev.type) - { - case CVirtualGimbalEvent::MoveRight: worldDelta.x += ev.magnitude; break; - case CVirtualGimbalEvent::MoveLeft: worldDelta.x -= ev.magnitude; break; - case CVirtualGimbalEvent::MoveUp: worldDelta.y += ev.magnitude; break; - case CVirtualGimbalEvent::MoveDown: worldDelta.y -= ev.magnitude; break; - case CVirtualGimbalEvent::MoveForward: worldDelta.z += ev.magnitude; break; - case CVirtualGimbalEvent::MoveBackward: worldDelta.z -= ev.magnitude; break; - default: - filtered.emplace_back(ev); - break; - } - } - - if (worldDelta.x == 0.0 && worldDelta.y == 0.0 && worldDelta.z == 0.0) - { - events = std::move(filtered); - count = static_cast(events.size()); - return; - } - - const auto& gimbal = camera->getGimbal(); - const auto right = gimbal.getXAxis(); - const auto up = gimbal.getYAxis(); - const auto forward = gimbal.getZAxis(); - - const float64_t3 localDelta = float64_t3( - hlsl::dot(worldDelta, right), - hlsl::dot(worldDelta, up), - hlsl::dot(worldDelta, forward) - ); - - auto emitAxis = [&](double v, CVirtualGimbalEvent::VirtualEventType pos, CVirtualGimbalEvent::VirtualEventType neg) - { - if (v == 0.0) - return; - auto& ev = filtered.emplace_back(); - ev.type = (v > 0.0) ? pos : neg; - ev.magnitude = std::abs(v); - }; - - emitAxis(localDelta.x, CVirtualGimbalEvent::MoveRight, CVirtualGimbalEvent::MoveLeft); - emitAxis(localDelta.y, CVirtualGimbalEvent::MoveUp, CVirtualGimbalEvent::MoveDown); - emitAxis(localDelta.z, CVirtualGimbalEvent::MoveForward, CVirtualGimbalEvent::MoveBackward); - - events = std::move(filtered); - count = static_cast(events.size()); - } - inline SCameraPresetApplySummary applyPresetToTargets(const CameraPreset& preset) { if (!m_playbackAffectsAll) diff --git a/61_UI/include/common.hpp b/61_UI/include/common.hpp index 09f245c37..1c483a490 100644 --- a/61_UI/include/common.hpp +++ b/61_UI/include/common.hpp @@ -25,6 +25,7 @@ #include "camera/CCameraPersistence.hpp" #include "camera/CCameraGoalAnalysis.hpp" #include "camera/CCameraGoalSolver.hpp" +#include "camera/CCameraManipulationUtilities.hpp" #include "camera/CGimbalInputBinder.hpp" #include "camera/CCubeProjection.hpp" @@ -70,6 +71,7 @@ using nbl::hlsl::SCameraPlaybackAdvanceResult; using nbl::hlsl::SCameraPresetApplySummary; using nbl::hlsl::SCameraGoalApplyAnalysis; using nbl::hlsl::SCameraCaptureAnalysis; +using nbl::hlsl::SCameraConstraintSettings; using nbl::hlsl::CCameraGoalSolver; using nbl::hlsl::CGimbalInputBinder; using nbl::hlsl::IGimbalBindingLayout; diff --git a/common/include/camera/CCameraManipulationUtilities.hpp b/common/include/camera/CCameraManipulationUtilities.hpp new file mode 100644 index 000000000..a144579ca --- /dev/null +++ b/common/include/camera/CCameraManipulationUtilities.hpp @@ -0,0 +1,167 @@ +// 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_MANIPULATION_UTILITIES_HPP_ +#define _C_CAMERA_MANIPULATION_UTILITIES_HPP_ + +#include +#include + +#include "CCameraPresetFlow.hpp" + +namespace nbl::hlsl +{ + +//! Reusable constraint settings for post-manipulation camera clamping. +struct SCameraConstraintSettings +{ + bool enabled = false; + bool clampPitch = false; + bool clampYaw = false; + bool clampRoll = false; + bool clampDistance = false; + float pitchMinDeg = -80.f; + float pitchMaxDeg = 80.f; + float yawMinDeg = -180.f; + float yawMaxDeg = 180.f; + float rollMinDeg = -180.f; + float rollMaxDeg = 180.f; + float minDistance = 0.1f; + float maxDistance = 1000.f; +}; + +//! Scale translation and rotation event magnitudes without touching unrelated event types. +inline void scaleVirtualEvents(std::vector& events, const uint32_t count, const float translationScale, const float rotationScale) +{ + for (uint32_t i = 0u; i < count; ++i) + { + auto& ev = events[i]; + const auto type = ev.type; + + if (type == CVirtualGimbalEvent::MoveForward || type == CVirtualGimbalEvent::MoveBackward || + type == CVirtualGimbalEvent::MoveLeft || type == CVirtualGimbalEvent::MoveRight || + type == CVirtualGimbalEvent::MoveUp || type == CVirtualGimbalEvent::MoveDown) + { + ev.magnitude *= translationScale; + } + else if (type == CVirtualGimbalEvent::TiltUp || type == CVirtualGimbalEvent::TiltDown || + type == CVirtualGimbalEvent::PanLeft || type == CVirtualGimbalEvent::PanRight || + type == CVirtualGimbalEvent::RollLeft || type == CVirtualGimbalEvent::RollRight) + { + ev.magnitude *= rotationScale; + } + } +} + +//! Reinterpret world-space translation intents as local camera-space movement events. +inline void remapTranslationEventsFromWorldToCameraLocal(ICamera* camera, std::vector& events, uint32_t& count) +{ + if (!camera) + return; + + float64_t3 worldDelta = float64_t3(0.0); + std::vector filtered; + filtered.reserve(events.size()); + + for (uint32_t i = 0u; i < count; ++i) + { + const auto& ev = events[i]; + switch (ev.type) + { + case CVirtualGimbalEvent::MoveRight: worldDelta.x += ev.magnitude; break; + case CVirtualGimbalEvent::MoveLeft: worldDelta.x -= ev.magnitude; break; + case CVirtualGimbalEvent::MoveUp: worldDelta.y += ev.magnitude; break; + case CVirtualGimbalEvent::MoveDown: worldDelta.y -= ev.magnitude; break; + case CVirtualGimbalEvent::MoveForward: worldDelta.z += ev.magnitude; break; + case CVirtualGimbalEvent::MoveBackward: worldDelta.z -= ev.magnitude; break; + default: + filtered.emplace_back(ev); + break; + } + } + + if (worldDelta.x == 0.0 && worldDelta.y == 0.0 && worldDelta.z == 0.0) + { + events = std::move(filtered); + count = static_cast(events.size()); + return; + } + + const auto& gimbal = camera->getGimbal(); + const auto right = gimbal.getXAxis(); + const auto up = gimbal.getYAxis(); + const auto forward = gimbal.getZAxis(); + + const float64_t3 localDelta = float64_t3( + hlsl::dot(worldDelta, right), + hlsl::dot(worldDelta, up), + hlsl::dot(worldDelta, forward) + ); + + auto emitAxis = [&](double v, CVirtualGimbalEvent::VirtualEventType pos, CVirtualGimbalEvent::VirtualEventType neg) + { + if (v == 0.0) + return; + auto& ev = filtered.emplace_back(); + ev.type = (v > 0.0) ? pos : neg; + ev.magnitude = std::abs(v); + }; + + emitAxis(localDelta.x, CVirtualGimbalEvent::MoveRight, CVirtualGimbalEvent::MoveLeft); + emitAxis(localDelta.y, CVirtualGimbalEvent::MoveUp, CVirtualGimbalEvent::MoveDown); + emitAxis(localDelta.z, CVirtualGimbalEvent::MoveForward, CVirtualGimbalEvent::MoveBackward); + + events = std::move(filtered); + count = static_cast(events.size()); +} + +//! Apply shared distance and Euler-angle constraints after manipulation. +inline bool applyCameraConstraints(const CCameraGoalSolver& solver, ICamera* camera, const SCameraConstraintSettings& constraints) +{ + if (!constraints.enabled || !camera) + return false; + + if (camera->hasCapability(ICamera::SphericalTarget)) + { + if (!constraints.clampDistance) + return false; + + 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 = glm::degrees(glm::eulerAngles(gimbal.getOrientation())); + + auto clamped = eulerDeg; + if (constraints.clampPitch) + clamped.x = std::clamp(clamped.x, constraints.pitchMinDeg, constraints.pitchMaxDeg); + if (constraints.clampYaw) + clamped.y = std::clamp(clamped.y, constraints.yawMinDeg, constraints.yawMaxDeg); + if (constraints.clampRoll) + clamped.z = std::clamp(clamped.z, constraints.rollMinDeg, constraints.rollMaxDeg); + + if (clamped.x == eulerDeg.x && clamped.y == eulerDeg.y && clamped.z == eulerDeg.z) + return false; + + CCameraPreset preset; + preset.goal.position = pos; + preset.goal.orientation = glm::quat(hlsl::radians(clamped)); + return applyPreset(solver, camera, preset); +} + +} // namespace nbl::hlsl + +#endif // _C_CAMERA_MANIPULATION_UTILITIES_HPP_ diff --git a/common/include/camera/README.md b/common/include/camera/README.md index 1f472e643..72fc47d8e 100644 --- a/common/include/camera/README.md +++ b/common/include/camera/README.md @@ -277,6 +277,19 @@ Provides: This keeps solver-backed preset flow reusable without leaving the flow rules in example-local glue. +### `CCameraManipulationUtilities.hpp` + +Reusable manipulation helpers that sit between raw collected virtual events and final camera state. + +Provides: + +- `SCameraConstraintSettings` +- virtual-event translation and rotation scaling +- world-translation remapping into local camera movement +- post-manipulation constraint clamping through the shared goal solver + +This keeps example runtime manipulation policy reusable without leaving event-scaling and constraint logic in example-local glue. + ### `CCameraKeyframeTrack.hpp` Reusable keyframe-track helpers on top of presets. From c621aae18bc7bcac3bb49dafd9168635d2711e92 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 19:10:32 +0200 Subject: [PATCH 155/205] Promote camera text utilities into shared headers --- 61_UI/include/app/App.hpp | 107 -------------- 61_UI/include/common.hpp | 5 + .../include/camera/CCameraTextUtilities.hpp | 134 ++++++++++++++++++ common/include/camera/README.md | 13 ++ 4 files changed, 152 insertions(+), 107 deletions(-) create mode 100644 common/include/camera/CCameraTextUtilities.hpp diff --git a/61_UI/include/app/App.hpp b/61_UI/include/app/App.hpp index de84a5b83..fb9c5b895 100644 --- a/61_UI/include/app/App.hpp +++ b/61_UI/include/app/App.hpp @@ -559,52 +559,6 @@ class App final : public examples::SimpleWindowedApplication return camera && camera->hasCapability(ICamera::SphericalTarget); } - inline std::string_view getCameraTypeLabel(const ICamera::CameraKind kind) const - { - switch (kind) - { - case ICamera::CameraKind::FPS: return "FPS"; - case ICamera::CameraKind::Free: return "Free"; - case ICamera::CameraKind::Orbit: return "Orbit"; - case ICamera::CameraKind::Arcball: return "Arcball"; - case ICamera::CameraKind::Turntable: return "Turntable"; - case ICamera::CameraKind::TopDown: return "TopDown"; - case ICamera::CameraKind::Isometric: return "Isometric"; - case ICamera::CameraKind::Chase: return "Chase"; - case ICamera::CameraKind::Dolly: return "Dolly"; - case ICamera::CameraKind::DollyZoom: return "Dolly Zoom"; - case ICamera::CameraKind::Path: return "Path"; - default: return "Unknown"; - } - } - - inline std::string_view getCameraTypeLabel(const ICamera* camera) const - { - return camera ? getCameraTypeLabel(camera->getKind()) : "Unknown"; - } - - inline std::string_view getCameraTypeDescription(const ICamera* camera) const - { - if (!camera) - return "Unspecified camera behavior"; - - switch (camera->getKind()) - { - case ICamera::CameraKind::FPS: return "First-person WASD + mouse look"; - case ICamera::CameraKind::Free: return "Free-fly 6DOF with full rotation"; - case ICamera::CameraKind::Orbit: return "Orbit around target with dolly"; - case ICamera::CameraKind::Arcball: return "Arcball trackball around target"; - case ICamera::CameraKind::Turntable: return "Turntable yaw/pitch around target"; - case ICamera::CameraKind::TopDown: return "Fixed pitch top-down pan"; - case ICamera::CameraKind::Isometric: return "Fixed isometric view with pan"; - case ICamera::CameraKind::Chase: return "Target follow with chase controls"; - case ICamera::CameraKind::Dolly: return "Rig truck/dolly with look-at"; - case ICamera::CameraKind::DollyZoom: return "Orbit with dolly-zoom FOV"; - case ICamera::CameraKind::Path: return "Move along a target path"; - default: return "Unspecified camera behavior"; - } - } - inline void syncVisualDebugWindowBindings() { if (!m_scriptedInput.enabled) @@ -1056,67 +1010,6 @@ class App final : public examples::SimpleWindowedApplication return capture.captured; } - inline std::string describeGoalStateMask(uint32_t mask) const - { - if (mask == ICamera::GoalStateNone) - return "Pose only"; - - std::string out; - auto append = [&](const char* label, const uint32_t bit) -> void - { - if ((mask & bit) != bit) - return; - if (!out.empty()) - out += ", "; - out += label; - }; - - append("Spherical target", ICamera::GoalStateSphericalTarget); - append("Dynamic perspective", ICamera::GoalStateDynamicPerspective); - append("Path", ICamera::GoalStatePath); - return out; - } - - inline std::string describeApplyResult(const CCameraGoalSolver::SApplyResult& result) const - { - std::ostringstream oss; - oss << "status="; - switch (result.status) - { - case CCameraGoalSolver::SApplyResult::EStatus::Unsupported: oss << "Unsupported"; break; - case CCameraGoalSolver::SApplyResult::EStatus::Failed: oss << "Failed"; break; - case CCameraGoalSolver::SApplyResult::EStatus::AlreadySatisfied: oss << "AlreadySatisfied"; break; - case CCameraGoalSolver::SApplyResult::EStatus::AppliedAbsoluteOnly: oss << "AppliedAbsoluteOnly"; break; - case CCameraGoalSolver::SApplyResult::EStatus::AppliedVirtualEvents: oss << "AppliedVirtualEvents"; break; - case CCameraGoalSolver::SApplyResult::EStatus::AppliedAbsoluteAndVirtualEvents: oss << "AppliedAbsoluteAndVirtualEvents"; break; - } - oss << " exact=" << (result.exact ? "true" : "false") - << " events=" << result.eventCount; - - if (result.issues != CCameraGoalSolver::SApplyResult::NoIssue) - { - oss << " issues="; - bool first = true; - auto appendIssue = [&](const char* label, const CCameraGoalSolver::SApplyResult::EIssue issue) -> void - { - if (!result.hasIssue(issue)) - return; - if (!first) - oss << ","; - oss << label; - first = false; - }; - - appendIssue("absolute_pose_fallback", CCameraGoalSolver::SApplyResult::UsedAbsolutePoseFallback); - appendIssue("missing_spherical_state", CCameraGoalSolver::SApplyResult::MissingSphericalTargetState); - appendIssue("missing_path_state", CCameraGoalSolver::SApplyResult::MissingPathState); - appendIssue("missing_dynamic_perspective_state", CCameraGoalSolver::SApplyResult::MissingDynamicPerspectiveState); - appendIssue("virtual_event_replay_failed", CCameraGoalSolver::SApplyResult::VirtualEventReplayFailed); - } - - return oss.str(); - } - inline PresetUiAnalysis analyzePresetForUi(ICamera* camera, const CameraPreset& preset) const { PresetUiAnalysis analysis; diff --git a/61_UI/include/common.hpp b/61_UI/include/common.hpp index 1c483a490..3bc0287e0 100644 --- a/61_UI/include/common.hpp +++ b/61_UI/include/common.hpp @@ -26,6 +26,7 @@ #include "camera/CCameraGoalAnalysis.hpp" #include "camera/CCameraGoalSolver.hpp" #include "camera/CCameraManipulationUtilities.hpp" +#include "camera/CCameraTextUtilities.hpp" #include "camera/CGimbalInputBinder.hpp" #include "camera/CCubeProjection.hpp" @@ -89,6 +90,10 @@ using nbl::hlsl::float64_t; using nbl::hlsl::float64_t3; using nbl::hlsl::float64_t4x4; using nbl::hlsl::uint16_t2; +using nbl::hlsl::describeApplyResult; +using nbl::hlsl::describeGoalStateMask; +using nbl::hlsl::getCameraTypeDescription; +using nbl::hlsl::getCameraTypeLabel; using nbl::hlsl::getCastedMatrix; using nbl::hlsl::getCastedVector; using nbl::hlsl::getMatrix3x4As4x4; diff --git a/common/include/camera/CCameraTextUtilities.hpp b/common/include/camera/CCameraTextUtilities.hpp new file mode 100644 index 000000000..44027895e --- /dev/null +++ b/common/include/camera/CCameraTextUtilities.hpp @@ -0,0 +1,134 @@ +// 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_TEXT_UTILITIES_HPP_ +#define _C_CAMERA_TEXT_UTILITIES_HPP_ + +#include +#include +#include + +#include "CCameraGoalSolver.hpp" + +namespace nbl::hlsl +{ + +//! Return a short human-readable label for a camera kind. +inline std::string_view getCameraTypeLabel(const ICamera::CameraKind kind) +{ + switch (kind) + { + case ICamera::CameraKind::FPS: return "FPS"; + case ICamera::CameraKind::Free: return "Free"; + case ICamera::CameraKind::Orbit: return "Orbit"; + case ICamera::CameraKind::Arcball: return "Arcball"; + case ICamera::CameraKind::Turntable: return "Turntable"; + case ICamera::CameraKind::TopDown: return "TopDown"; + case ICamera::CameraKind::Isometric: return "Isometric"; + case ICamera::CameraKind::Chase: return "Chase"; + case ICamera::CameraKind::Dolly: return "Dolly"; + case ICamera::CameraKind::DollyZoom: return "Dolly Zoom"; + case ICamera::CameraKind::Path: return "Path"; + default: return "Unknown"; + } +} + +//! Return a short human-readable label for a concrete camera instance. +inline std::string_view getCameraTypeLabel(const ICamera* camera) +{ + return camera ? getCameraTypeLabel(camera->getKind()) : "Unknown"; +} + +//! Return a short human-readable description for a camera kind. +inline std::string_view getCameraTypeDescription(const ICamera::CameraKind kind) +{ + switch (kind) + { + case ICamera::CameraKind::FPS: return "First-person WASD + mouse look"; + case ICamera::CameraKind::Free: return "Free-fly 6DOF with full rotation"; + case ICamera::CameraKind::Orbit: return "Orbit around target with dolly"; + case ICamera::CameraKind::Arcball: return "Arcball trackball around target"; + case ICamera::CameraKind::Turntable: return "Turntable yaw/pitch around target"; + case ICamera::CameraKind::TopDown: return "Fixed pitch top-down pan"; + case ICamera::CameraKind::Isometric: return "Fixed isometric view with pan"; + case ICamera::CameraKind::Chase: return "Target follow with chase controls"; + case ICamera::CameraKind::Dolly: return "Rig truck/dolly with look-at"; + case ICamera::CameraKind::DollyZoom: return "Orbit with dolly-zoom FOV"; + case ICamera::CameraKind::Path: return "Move along a target path"; + default: return "Unspecified camera behavior"; + } +} + +//! Return a short human-readable description for a concrete camera instance. +inline std::string_view getCameraTypeDescription(const ICamera* camera) +{ + return camera ? getCameraTypeDescription(camera->getKind()) : "Unspecified camera behavior"; +} + +//! Describe the typed goal-state mask in a stable human-readable format. +inline std::string describeGoalStateMask(const uint32_t mask) +{ + if (mask == ICamera::GoalStateNone) + return "Pose only"; + + std::string out; + auto append = [&](const char* label, const uint32_t bit) -> void + { + if ((mask & bit) != bit) + return; + if (!out.empty()) + out += ", "; + out += label; + }; + + append("Spherical target", ICamera::GoalStateSphericalTarget); + append("Dynamic perspective", ICamera::GoalStateDynamicPerspective); + append("Path", ICamera::GoalStatePath); + return out; +} + +//! Describe a detailed goal-apply result for logs, smoke tests, and UI summaries. +inline std::string describeApplyResult(const CCameraGoalSolver::SApplyResult& result) +{ + std::ostringstream oss; + oss << "status="; + switch (result.status) + { + case CCameraGoalSolver::SApplyResult::EStatus::Unsupported: oss << "Unsupported"; break; + case CCameraGoalSolver::SApplyResult::EStatus::Failed: oss << "Failed"; break; + case CCameraGoalSolver::SApplyResult::EStatus::AlreadySatisfied: oss << "AlreadySatisfied"; break; + case CCameraGoalSolver::SApplyResult::EStatus::AppliedAbsoluteOnly: oss << "AppliedAbsoluteOnly"; break; + case CCameraGoalSolver::SApplyResult::EStatus::AppliedVirtualEvents: oss << "AppliedVirtualEvents"; break; + case CCameraGoalSolver::SApplyResult::EStatus::AppliedAbsoluteAndVirtualEvents: oss << "AppliedAbsoluteAndVirtualEvents"; break; + } + oss << " exact=" << (result.exact ? "true" : "false") + << " events=" << result.eventCount; + + if (result.issues != CCameraGoalSolver::SApplyResult::NoIssue) + { + oss << " issues="; + bool first = true; + auto appendIssue = [&](const char* label, const CCameraGoalSolver::SApplyResult::EIssue issue) -> void + { + if (!result.hasIssue(issue)) + return; + if (!first) + oss << ","; + oss << label; + first = false; + }; + + appendIssue("absolute_pose_fallback", CCameraGoalSolver::SApplyResult::UsedAbsolutePoseFallback); + appendIssue("missing_spherical_state", CCameraGoalSolver::SApplyResult::MissingSphericalTargetState); + appendIssue("missing_path_state", CCameraGoalSolver::SApplyResult::MissingPathState); + appendIssue("missing_dynamic_perspective_state", CCameraGoalSolver::SApplyResult::MissingDynamicPerspectiveState); + appendIssue("virtual_event_replay_failed", CCameraGoalSolver::SApplyResult::VirtualEventReplayFailed); + } + + return oss.str(); +} + +} // namespace nbl::hlsl + +#endif // _C_CAMERA_TEXT_UTILITIES_HPP_ diff --git a/common/include/camera/README.md b/common/include/camera/README.md index 72fc47d8e..2c6acec1b 100644 --- a/common/include/camera/README.md +++ b/common/include/camera/README.md @@ -250,6 +250,19 @@ Use it when UI or higher-level tools need typed answers for: This keeps policy analysis out of example-local UI code. +### `CCameraTextUtilities.hpp` + +Reusable human-readable metadata and diagnostic text helpers for cameras. + +Provides: + +- camera-kind labels +- camera-kind descriptions +- goal-state mask descriptions +- detailed goal-apply result descriptions + +This keeps camera-specific presentation and diagnostic text reusable without leaving it in example-local glue. + ### `CCameraPreset.hpp` Reusable preset and keyframe state plus JSON IO. From 5b444ab1761f86f684fb41769ed3b6d047c45cd6 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 19:35:56 +0200 Subject: [PATCH 156/205] Promote camera projection utilities into shared headers --- 61_UI/AppImGuiListen.cpp | 6 ++-- 61_UI/include/app/App.hpp | 13 -------- 61_UI/include/common.hpp | 2 ++ .../camera/CCameraProjectionUtilities.hpp | 33 +++++++++++++++++++ common/include/camera/README.md | 10 ++++++ 5 files changed, 48 insertions(+), 16 deletions(-) create mode 100644 common/include/camera/CCameraProjectionUtilities.hpp diff --git a/61_UI/AppImGuiListen.cpp b/61_UI/AppImGuiListen.cpp index 96b7c7208..d7bcbd7ca 100644 --- a/61_UI/AppImGuiListen.cpp +++ b/61_UI/AppImGuiListen.cpp @@ -88,7 +88,7 @@ void App::imguiListen() assert(binding.boundProjectionIx.has_value()); auto& projection = planarBound->getPlanarProjections()[binding.boundProjectionIx.value()]; - applyDollyZoomProjection(planarViewCameraBound, projection); + syncDynamicPerspectiveProjection(planarViewCameraBound, projection); projection.update(binding.leftHandedProjection, binding.aspectRatio); // TODO: @@ -423,7 +423,7 @@ void App::imguiListen() assert(binding.boundProjectionIx.has_value()); auto& projection = planarBound->getPlanarProjections()[binding.boundProjectionIx.value()]; - applyDollyZoomProjection(planarViewCameraBound, projection); + syncDynamicPerspectiveProjection(planarViewCameraBound, projection); projection.update(binding.leftHandedProjection, binding.aspectRatio); } @@ -452,7 +452,7 @@ void App::imguiListen() assert(binding.boundProjectionIx.has_value()); auto& projection = planarBound->getPlanarProjections()[binding.boundProjectionIx.value()]; - applyDollyZoomProjection(boundPlanarCamera, projection); + syncDynamicPerspectiveProjection(boundPlanarCamera, projection); projection.update(binding.leftHandedProjection, binding.aspectRatio); binding.isOrthographicProjection = projection.getParameters().m_type == IPlanarProjection::CProjection::Orthographic; diff --git a/61_UI/include/app/App.hpp b/61_UI/include/app/App.hpp index fb9c5b895..c94a7ad3b 100644 --- a/61_UI/include/app/App.hpp +++ b/61_UI/include/app/App.hpp @@ -990,19 +990,6 @@ class App final : public examples::SimpleWindowedApplication drawList->AddText(font, hintSize, ImVec2(hintX, hintY), IM_COL32(170, 204, 255, 255), lineHint.c_str()); } - inline void applyDollyZoomProjection(ICamera* camera, IPlanarProjection::CProjection& projection) - { - if (!camera) - return; - const auto& params = projection.getParameters(); - if (params.m_type != IPlanarProjection::CProjection::Perspective) - return; - float dynamicFov = 0.0f; - if (!camera->tryGetDynamicPerspectiveFov(dynamicFov)) - return; - projection.setPerspective(params.m_zNear, params.m_zFar, dynamicFov); - } - inline bool tryCaptureGoal(ICamera* camera, CCameraGoal& out) const { const auto capture = m_cameraGoalSolver.captureDetailed(camera); diff --git a/61_UI/include/common.hpp b/61_UI/include/common.hpp index 3bc0287e0..3fd8210b0 100644 --- a/61_UI/include/common.hpp +++ b/61_UI/include/common.hpp @@ -26,6 +26,7 @@ #include "camera/CCameraGoalAnalysis.hpp" #include "camera/CCameraGoalSolver.hpp" #include "camera/CCameraManipulationUtilities.hpp" +#include "camera/CCameraProjectionUtilities.hpp" #include "camera/CCameraTextUtilities.hpp" #include "camera/CGimbalInputBinder.hpp" @@ -99,5 +100,6 @@ using nbl::hlsl::getCastedVector; using nbl::hlsl::getMatrix3x4As4x4; using nbl::hlsl::concatenateBFollowedByA; using nbl::hlsl::mul; +using nbl::hlsl::syncDynamicPerspectiveProjection; #endif // _NBL_THIS_EXAMPLE_COMMON_H_INCLUDED_ diff --git a/common/include/camera/CCameraProjectionUtilities.hpp b/common/include/camera/CCameraProjectionUtilities.hpp new file mode 100644 index 000000000..5e9ceb2a6 --- /dev/null +++ b/common/include/camera/CCameraProjectionUtilities.hpp @@ -0,0 +1,33 @@ +// 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_PROJECTION_UTILITIES_HPP_ +#define _C_CAMERA_PROJECTION_UTILITIES_HPP_ + +#include "IPlanarProjection.hpp" + +namespace nbl::hlsl +{ + +//! Apply a camera-provided dynamic perspective FOV to one planar projection entry. +inline bool syncDynamicPerspectiveProjection(ICamera* camera, IPlanarProjection::CProjection& projection) +{ + if (!camera) + return false; + + const auto& params = projection.getParameters(); + if (params.m_type != IPlanarProjection::CProjection::Perspective) + return false; + + float dynamicFov = 0.0f; + if (!camera->tryGetDynamicPerspectiveFov(dynamicFov)) + return false; + + projection.setPerspective(params.m_zNear, params.m_zFar, dynamicFov); + return true; +} + +} // namespace nbl::hlsl + +#endif // _C_CAMERA_PROJECTION_UTILITIES_HPP_ diff --git a/common/include/camera/README.md b/common/include/camera/README.md index 2c6acec1b..6f2a02f18 100644 --- a/common/include/camera/README.md +++ b/common/include/camera/README.md @@ -303,6 +303,16 @@ Provides: This keeps example runtime manipulation policy reusable without leaving event-scaling and constraint logic in example-local glue. +### `CCameraProjectionUtilities.hpp` + +Reusable helpers that synchronize camera-driven projection state with planar projections. + +Provides: + +- dynamic perspective FOV sync from camera state into `IPlanarProjection::CProjection` + +This keeps camera-specific projection updates reusable without leaving them in example-local glue. + ### `CCameraKeyframeTrack.hpp` Reusable keyframe-track helpers on top of presets. From 610257413d4e569217e8a241e2d4e18e6719e040 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 19:39:57 +0200 Subject: [PATCH 157/205] Share camera analysis text helpers --- 61_UI/include/app/App.hpp | 82 +------------------ .../include/camera/CCameraTextUtilities.hpp | 73 ++++++++++++++++- common/include/camera/README.md | 3 + 3 files changed, 79 insertions(+), 79 deletions(-) diff --git a/61_UI/include/app/App.hpp b/61_UI/include/app/App.hpp index c94a7ad3b..e2ee52155 100644 --- a/61_UI/include/app/App.hpp +++ b/61_UI/include/app/App.hpp @@ -1001,46 +1001,8 @@ class App final : public examples::SimpleWindowedApplication { PresetUiAnalysis analysis; static_cast(analysis) = nbl::hlsl::analyzePresetApply(m_cameraGoalSolver, camera, preset); - - if (!analysis.hasCamera) - { - analysis.compatibilityLabel = "No active camera"; - analysis.policyLabel = "Blocked | no active camera"; - return analysis; - } - - { - std::ostringstream oss; - oss << (analysis.compatibility.exact ? "Exact" : "Best-effort") - << " | source=" << getCameraTypeLabel(analysis.goal.sourceKind) - << " | target=" << getCameraTypeLabel(camera); - - if (analysis.compatibility.missingGoalStateMask != ICamera::GoalStateNone) - oss << " | missing=" << describeGoalStateMask(analysis.compatibility.missingGoalStateMask); - else if (!analysis.compatibility.sameKind && analysis.goal.sourceKind != ICamera::CameraKind::Unknown) - oss << " | shared goal state only"; - - analysis.compatibilityLabel = oss.str(); - } - - if (!analysis.finiteGoal) - { - analysis.policyLabel = "Blocked | invalid goal state"; - return analysis; - } - - { - std::ostringstream oss; - oss << (analysis.compatibility.exact ? "Exact apply" : "Best-effort apply"); - if (analysis.compatibility.missingGoalStateMask != ICamera::GoalStateNone) - oss << " | drops=" << describeGoalStateMask(analysis.compatibility.missingGoalStateMask); - else if (!analysis.compatibility.sameKind && analysis.goal.sourceKind != ICamera::CameraKind::Unknown) - oss << " | shared goal state only"; - else - oss << " | full preview available"; - analysis.policyLabel = oss.str(); - } - + analysis.compatibilityLabel = nbl::hlsl::describeGoalApplyCompatibility(analysis, camera); + analysis.policyLabel = nbl::hlsl::describeGoalApplyPolicy(analysis); return analysis; } @@ -1048,29 +1010,7 @@ class App final : public examples::SimpleWindowedApplication { CaptureUiAnalysis analysis; static_cast(analysis) = nbl::hlsl::analyzeCameraCapture(m_cameraGoalSolver, camera); - if (!analysis.hasCamera) - { - analysis.policyLabel = "Blocked | no active camera"; - return analysis; - } - - if (!analysis.capturedGoal) - { - analysis.policyLabel = "Blocked | goal capture failed"; - return analysis; - } - - if (!analysis.finiteGoal) - { - analysis.policyLabel = "Blocked | invalid goal state"; - return analysis; - } - - analysis.canCapture = true; - std::ostringstream oss; - oss << "Ready | source=" << getCameraTypeLabel(camera) - << " | goal=" << describeGoalStateMask(analysis.goal.sourceGoalStateMask); - analysis.policyLabel = oss.str(); + analysis.policyLabel = nbl::hlsl::describeCameraCapturePolicy(analysis, camera); return analysis; } @@ -1120,24 +1060,10 @@ class App final : public examples::SimpleWindowedApplication banner.approximate = false; } - inline std::string describePlaybackApplySummary(const SCameraPresetApplySummary& summary) const - { - if (!summary.hasTargets()) - return m_playbackAffectsAll ? "Playback apply | no cameras available" : "Playback apply | no active camera"; - - std::ostringstream oss; - oss << "Playback apply | targets=" << summary.targetCount << " | ok=" << summary.successCount; - if (summary.approximateCount > 0u) - oss << " | approximate=" << summary.approximateCount; - if (summary.failureCount > 0u) - oss << " | failed=" << summary.failureCount; - return oss.str(); - } - inline void storePlaybackApplySummary(const SCameraPresetApplySummary& summary) { storeApplyStatusBanner(m_playbackApplyBanner, - describePlaybackApplySummary(summary), + nbl::hlsl::describePresetApplySummary(summary, m_playbackAffectsAll ? "Playback apply | no cameras available" : "Playback apply | no active camera"), summary.succeeded(), summary.approximate()); } diff --git a/common/include/camera/CCameraTextUtilities.hpp b/common/include/camera/CCameraTextUtilities.hpp index 44027895e..c51555e08 100644 --- a/common/include/camera/CCameraTextUtilities.hpp +++ b/common/include/camera/CCameraTextUtilities.hpp @@ -9,7 +9,8 @@ #include #include -#include "CCameraGoalSolver.hpp" +#include "CCameraGoalAnalysis.hpp" +#include "CCameraPresetFlow.hpp" namespace nbl::hlsl { @@ -129,6 +130,76 @@ inline std::string describeApplyResult(const CCameraGoalSolver::SApplyResult& re return oss.str(); } +//! Describe compatibility preview for applying one analyzed goal to a target camera. +inline std::string describeGoalApplyCompatibility(const SCameraGoalApplyAnalysis& analysis, const ICamera* targetCamera) +{ + if (!analysis.hasCamera) + return "No active camera"; + + std::ostringstream oss; + oss << (analysis.compatibility.exact ? "Exact" : "Best-effort") + << " | source=" << getCameraTypeLabel(analysis.goal.sourceKind) + << " | target=" << getCameraTypeLabel(targetCamera); + + if (analysis.compatibility.missingGoalStateMask != ICamera::GoalStateNone) + oss << " | missing=" << describeGoalStateMask(analysis.compatibility.missingGoalStateMask); + else if (!analysis.compatibility.sameKind && analysis.goal.sourceKind != ICamera::CameraKind::Unknown) + oss << " | shared goal state only"; + + return oss.str(); +} + +//! Describe whether an analyzed goal can be meaningfully applied to the target camera. +inline std::string describeGoalApplyPolicy(const SCameraGoalApplyAnalysis& analysis) +{ + if (!analysis.hasCamera) + return "Blocked | no active camera"; + if (!analysis.finiteGoal) + return "Blocked | invalid goal state"; + + std::ostringstream oss; + oss << (analysis.compatibility.exact ? "Exact apply" : "Best-effort apply"); + if (analysis.compatibility.missingGoalStateMask != ICamera::GoalStateNone) + oss << " | drops=" << describeGoalStateMask(analysis.compatibility.missingGoalStateMask); + else if (!analysis.compatibility.sameKind && analysis.goal.sourceKind != ICamera::CameraKind::Unknown) + oss << " | shared goal state only"; + else + oss << " | full preview available"; + + return oss.str(); +} + +//! Describe whether one analyzed camera state can be captured into a reusable goal. +inline std::string describeCameraCapturePolicy(const SCameraCaptureAnalysis& analysis, const ICamera* camera) +{ + if (!analysis.hasCamera) + return "Blocked | no active camera"; + if (!analysis.capturedGoal) + return "Blocked | goal capture failed"; + if (!analysis.finiteGoal) + return "Blocked | invalid goal state"; + + std::ostringstream oss; + oss << "Ready | source=" << getCameraTypeLabel(camera) + << " | goal=" << describeGoalStateMask(analysis.goal.sourceGoalStateMask); + return oss.str(); +} + +//! Describe the aggregate outcome of applying one preset to multiple cameras. +inline std::string describePresetApplySummary(const SCameraPresetApplySummary& summary, std::string_view noTargetsLabel, std::string_view prefix = "Playback apply") +{ + if (!summary.hasTargets()) + return std::string(noTargetsLabel); + + std::ostringstream oss; + oss << prefix << " | targets=" << summary.targetCount << " | ok=" << summary.successCount; + if (summary.approximateCount > 0u) + oss << " | approximate=" << summary.approximateCount; + if (summary.failureCount > 0u) + oss << " | failed=" << summary.failureCount; + return oss.str(); +} + } // namespace nbl::hlsl #endif // _C_CAMERA_TEXT_UTILITIES_HPP_ diff --git a/common/include/camera/README.md b/common/include/camera/README.md index 6f2a02f18..d783aba3c 100644 --- a/common/include/camera/README.md +++ b/common/include/camera/README.md @@ -260,6 +260,9 @@ Provides: - camera-kind descriptions - goal-state mask descriptions - detailed goal-apply result descriptions +- analyzed goal-apply compatibility and policy descriptions +- analyzed camera-capture policy descriptions +- aggregate preset-apply summary descriptions This keeps camera-specific presentation and diagnostic text reusable without leaving it in example-local glue. From 20224d5cdee82214d5252b21b8d0365206d20eaa Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 20:03:31 +0200 Subject: [PATCH 158/205] Harden camera helper smoke coverage --- 61_UI/AppInit.cpp | 146 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) diff --git a/61_UI/AppInit.cpp b/61_UI/AppInit.cpp index bd61d4b91..5549700ce 100644 --- a/61_UI/AppInit.cpp +++ b/61_UI/AppInit.cpp @@ -383,11 +383,13 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) }; CameraPreset initialOrbitPreset; + CameraPreset initialFreePreset; CameraPreset initialChasePreset; CameraPreset initialDollyPreset; CameraPreset initialPathPreset; CameraPreset initialDollyZoomPreset; bool hasOrbitPreset = false; + bool hasFreePreset = false; bool hasChasePreset = false; bool hasDollyPreset = false; bool hasPathPreset = false; @@ -412,6 +414,10 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) initialOrbitPreset = initialPreset; hasOrbitPreset = true; break; + case ICamera::CameraKind::Free: + initialFreePreset = initialPreset; + hasFreePreset = true; + break; case ICamera::CameraKind::Chase: initialChasePreset = initialPreset; hasChasePreset = true; @@ -719,8 +725,10 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) }; ICamera* orbitCamera = findCameraByKind(ICamera::CameraKind::Orbit); + ICamera* freeCamera = findCameraByKind(ICamera::CameraKind::Free); ICamera* chaseCamera = findCameraByKind(ICamera::CameraKind::Chase); ICamera* dollyCamera = findCameraByKind(ICamera::CameraKind::Dolly); + ICamera* dollyZoomCamera = findCameraByKind(ICamera::CameraKind::DollyZoom); if (hasOrbitPreset && hasChasePreset) { if (!verifyExactCrossKindApply(orbitCamera, initialChasePreset, "Chase->Orbit")) @@ -942,6 +950,144 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) return fail("Preset apply summary smoke failed for approximate target range."); } + { + 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::hlsl::scaleVirtualEvents(scaledEvents, static_cast(scaledEvents.size()), 0.5f, 2.0f); + if (std::abs(scaledEvents[0].magnitude - 1.0) > 1e-9 || + std::abs(scaledEvents[1].magnitude - 6.0) > 1e-9 || + std::abs(scaledEvents[2].magnitude - 4.0) > 1e-9) + { + return fail("Camera manipulation utilities smoke failed for virtual-event scaling."); + } + } + + if (hasFreePreset && freeCamera) + { + CameraPreset orientedPreset = initialFreePreset; + orientedPreset.goal.orientation = glm::quat(hlsl::radians(float32_t3(0.f, 90.f, 0.f))); + const auto orientResult = nbl::hlsl::applyPresetDetailed(m_cameraGoalSolver, freeCamera, orientedPreset); + if (!orientResult.succeeded() || !comparePresetToCamera(freeCamera, orientedPreset, 1e-6, 0.1, 1e-6)) + return fail("Camera manipulation utilities smoke failed to orient Free camera before translation remap."); + + std::vector worldTranslationEvents(3u); + worldTranslationEvents[0].type = CVirtualGimbalEvent::MoveRight; + worldTranslationEvents[0].magnitude = 1.25; + worldTranslationEvents[1].type = CVirtualGimbalEvent::MoveUp; + worldTranslationEvents[1].magnitude = 0.5; + worldTranslationEvents[2].type = CVirtualGimbalEvent::MoveForward; + worldTranslationEvents[2].magnitude = 2.0; + uint32_t remappedCount = static_cast(worldTranslationEvents.size()); + nbl::hlsl::remapTranslationEventsFromWorldToCameraLocal(freeCamera, worldTranslationEvents, remappedCount); + if (remappedCount == 0u) + return fail("Camera manipulation utilities smoke produced empty translation remap."); + + if (!freeCamera->manipulate({ worldTranslationEvents.data(), remappedCount })) + return fail("Camera manipulation utilities smoke failed to apply remapped translation."); + + const auto remappedPosition = freeCamera->getGimbal().getPosition(); + const auto positionDelta = remappedPosition - orientedPreset.goal.position; + const float64_t3 expectedWorldDelta(1.25, 0.5, 2.0); + if (!nearlyEqual3(positionDelta, expectedWorldDelta, 1e-6)) + return fail("Camera manipulation utilities smoke changed world-space translation semantics."); + + CameraPreset pitchPreset = initialFreePreset; + pitchPreset.goal.orientation = glm::quat(hlsl::radians(float32_t3(60.f, 0.f, 0.f))); + const auto pitchResult = nbl::hlsl::applyPresetDetailed(m_cameraGoalSolver, freeCamera, pitchPreset); + if (!pitchResult.succeeded()) + return fail("Camera manipulation utilities smoke failed to prepare Free camera pitch clamp."); + + SCameraConstraintSettings freeConstraints; + freeConstraints.enabled = true; + freeConstraints.clampPitch = true; + freeConstraints.pitchMinDeg = -15.f; + freeConstraints.pitchMaxDeg = 15.f; + if (!nbl::hlsl::applyCameraConstraints(m_cameraGoalSolver, freeCamera, freeConstraints)) + return fail("Camera manipulation utilities smoke failed to clamp Free camera orientation."); + + const auto freeEulerDeg = glm::degrees(glm::eulerAngles(freeCamera->getGimbal().getOrientation())); + if (std::abs(static_cast(freeEulerDeg.x - 15.f)) > 0.1) + return fail("Camera manipulation utilities smoke produced wrong clamped Free camera pitch."); + + const auto restoreFree = nbl::hlsl::applyPresetDetailed(m_cameraGoalSolver, freeCamera, initialFreePreset); + if (!restoreFree.succeeded() || !comparePresetToCamera(freeCamera, initialFreePreset, 1e-6, 0.1, 1e-6)) + return fail("Camera manipulation utilities smoke failed to restore Free camera baseline."); + } + + if (hasOrbitPreset && orbitCamera && initialOrbitPreset.goal.hasDistance) + { + CameraPreset farOrbitPreset = initialOrbitPreset; + farOrbitPreset.goal.distance = initialOrbitPreset.goal.distance + 10.f; + const auto farOrbitResult = nbl::hlsl::applyPresetDetailed(m_cameraGoalSolver, orbitCamera, farOrbitPreset); + if (!farOrbitResult.succeeded()) + return fail("Camera manipulation utilities smoke failed to prepare Orbit distance clamp."); + + SCameraConstraintSettings orbitConstraints; + orbitConstraints.enabled = true; + orbitConstraints.clampDistance = true; + orbitConstraints.minDistance = std::max(0.1f, initialOrbitPreset.goal.distance * 0.5f); + orbitConstraints.maxDistance = initialOrbitPreset.goal.distance * 0.75f; + if (!nbl::hlsl::applyCameraConstraints(m_cameraGoalSolver, orbitCamera, orbitConstraints)) + return fail("Camera manipulation utilities smoke failed to clamp Orbit distance."); + + ICamera::SphericalTargetState clampedOrbitState; + if (!orbitCamera->tryGetSphericalTargetState(clampedOrbitState) || + std::abs(static_cast(clampedOrbitState.distance - orbitConstraints.maxDistance)) > 1e-6) + { + return fail("Camera manipulation utilities smoke produced wrong clamped Orbit distance."); + } + + const auto restoreOrbit = nbl::hlsl::applyPresetDetailed(m_cameraGoalSolver, orbitCamera, initialOrbitPreset); + if (!restoreOrbit.succeeded() || !comparePresetToCamera(orbitCamera, initialOrbitPreset, 1e-6, 0.1, 1e-6)) + return fail("Camera manipulation utilities smoke failed to restore Orbit baseline."); + } + + if (hasDollyZoomPreset && dollyZoomCamera) + { + float dynamicFov = 0.0f; + if (!dollyZoomCamera->tryGetDynamicPerspectiveFov(dynamicFov)) + return fail("Camera projection utilities smoke failed to query DollyZoom dynamic FOV."); + + auto perspectiveProjection = IPlanarProjection::CProjection::create(0.1f, 100.f, 60.f); + if (!nbl::hlsl::syncDynamicPerspectiveProjection(dollyZoomCamera, perspectiveProjection)) + return fail("Camera projection utilities smoke failed to sync dynamic perspective projection."); + if (std::abs(static_cast(perspectiveProjection.getParameters().m_planar.perspective.fov - dynamicFov)) > 1e-6) + return fail("Camera projection utilities smoke produced wrong dynamic perspective FOV."); + + auto orthographicProjection = IPlanarProjection::CProjection::create(0.1f, 100.f, 10.f); + if (nbl::hlsl::syncDynamicPerspectiveProjection(dollyZoomCamera, orthographicProjection)) + return fail("Camera projection utilities smoke unexpectedly synced orthographic projection."); + } + + { + if (getCameraTypeLabel(ICamera::CameraKind::DollyZoom) != "Dolly Zoom") + return fail("Camera text utilities smoke failed for Dolly Zoom label."); + if (getCameraTypeDescription(ICamera::CameraKind::Path) != "Move along a target path") + return fail("Camera text utilities smoke failed for Path description."); + if (describeGoalStateMask(ICamera::GoalStateNone) != "Pose only") + return fail("Camera text utilities smoke failed for empty goal-state description."); + if (describeGoalStateMask(ICamera::GoalStateSphericalTarget | ICamera::GoalStateDynamicPerspective) != "Spherical target, Dynamic perspective") + return fail("Camera text utilities smoke failed for combined goal-state description."); + + CCameraGoalSolver::SApplyResult defaultApplyResult; + const auto applyResultText = describeApplyResult(defaultApplyResult); + if (applyResultText.find("status=Unsupported") == std::string::npos || applyResultText.find("events=0") == std::string::npos) + return fail("Camera text utilities smoke failed for apply-result description."); + + SCameraPresetApplySummary summary; + summary.targetCount = 2u; + summary.successCount = 2u; + summary.approximateCount = 1u; + const auto summaryText = nbl::hlsl::describePresetApplySummary(summary, "none"); + if (summaryText.find("targets=2") == std::string::npos || summaryText.find("approximate=1") == std::string::npos) + return fail("Camera text utilities smoke failed for preset-apply summary description."); + } + m_headlessCameraSmokePassed = true; std::cout << "[headless-camera-smoke] PASS cameras=" << cameras.size() << std::endl; return true; From e5adb581fa06ae1d1e70b919f7199951f5eb679b Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 20:10:00 +0200 Subject: [PATCH 159/205] Share camera presentation utilities --- 61_UI/include/app/App.hpp | 46 ++--------- 61_UI/include/common.hpp | 4 + .../camera/CCameraPresentationUtilities.hpp | 78 +++++++++++++++++++ common/include/camera/README.md | 13 ++++ 4 files changed, 100 insertions(+), 41 deletions(-) create mode 100644 common/include/camera/CCameraPresentationUtilities.hpp diff --git a/61_UI/include/app/App.hpp b/61_UI/include/app/App.hpp index e2ee52155..2731429a7 100644 --- a/61_UI/include/app/App.hpp +++ b/61_UI/include/app/App.hpp @@ -484,38 +484,9 @@ class App final : public examples::SimpleWindowedApplication using CameraKeyframe = CCameraKeyframe; using CameraKeyframeTrack = CCameraKeyframeTrack; - enum class PresetFilterMode : uint8_t - { - All, - Exact, - BestEffort - }; - - struct PresetUiAnalysis : SCameraGoalApplyAnalysis - { - std::string compatibilityLabel; - std::string policyLabel; - - inline bool matchesFilter(const PresetFilterMode mode) const - { - switch (mode) - { - case PresetFilterMode::All: - return true; - case PresetFilterMode::Exact: - return hasCamera && exact(); - case PresetFilterMode::BestEffort: - return hasCamera && !exact(); - default: - return true; - } - } - }; - - struct CaptureUiAnalysis : SCameraCaptureAnalysis - { - std::string policyLabel; - }; + using PresetFilterMode = EPresetApplyPresentationFilter; + using PresetUiAnalysis = SCameraGoalApplyPresentation; + using CaptureUiAnalysis = SCameraCapturePresentation; struct CameraPlaybackState : CCameraPlaybackCursor { @@ -999,19 +970,12 @@ class App final : public examples::SimpleWindowedApplication inline PresetUiAnalysis analyzePresetForUi(ICamera* camera, const CameraPreset& preset) const { - PresetUiAnalysis analysis; - static_cast(analysis) = nbl::hlsl::analyzePresetApply(m_cameraGoalSolver, camera, preset); - analysis.compatibilityLabel = nbl::hlsl::describeGoalApplyCompatibility(analysis, camera); - analysis.policyLabel = nbl::hlsl::describeGoalApplyPolicy(analysis); - return analysis; + return nbl::hlsl::analyzePresetPresentation(m_cameraGoalSolver, camera, preset); } inline CaptureUiAnalysis analyzeCameraCaptureForUi(ICamera* camera) const { - CaptureUiAnalysis analysis; - static_cast(analysis) = nbl::hlsl::analyzeCameraCapture(m_cameraGoalSolver, camera); - analysis.policyLabel = nbl::hlsl::describeCameraCapturePolicy(analysis, camera); - return analysis; + return nbl::hlsl::analyzeCapturePresentation(m_cameraGoalSolver, camera); } inline CCameraGoalSolver::SCompatibilityResult analyzePresetCompatibility(ICamera* camera, const CameraPreset& preset) const diff --git a/61_UI/include/common.hpp b/61_UI/include/common.hpp index 3fd8210b0..fc3697b3d 100644 --- a/61_UI/include/common.hpp +++ b/61_UI/include/common.hpp @@ -26,6 +26,7 @@ #include "camera/CCameraGoalAnalysis.hpp" #include "camera/CCameraGoalSolver.hpp" #include "camera/CCameraManipulationUtilities.hpp" +#include "camera/CCameraPresentationUtilities.hpp" #include "camera/CCameraProjectionUtilities.hpp" #include "camera/CCameraTextUtilities.hpp" #include "camera/CGimbalInputBinder.hpp" @@ -73,9 +74,12 @@ using nbl::hlsl::SCameraPlaybackAdvanceResult; using nbl::hlsl::SCameraPresetApplySummary; using nbl::hlsl::SCameraGoalApplyAnalysis; using nbl::hlsl::SCameraCaptureAnalysis; +using nbl::hlsl::SCameraGoalApplyPresentation; +using nbl::hlsl::SCameraCapturePresentation; using nbl::hlsl::SCameraConstraintSettings; using nbl::hlsl::CCameraGoalSolver; using nbl::hlsl::CGimbalInputBinder; +using nbl::hlsl::EPresetApplyPresentationFilter; using nbl::hlsl::IGimbalBindingLayout; using nbl::hlsl::IPlanarProjection; using nbl::hlsl::CPlanarProjection; diff --git a/common/include/camera/CCameraPresentationUtilities.hpp b/common/include/camera/CCameraPresentationUtilities.hpp new file mode 100644 index 000000000..3dd6ad9e4 --- /dev/null +++ b/common/include/camera/CCameraPresentationUtilities.hpp @@ -0,0 +1,78 @@ +// 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_PRESENTATION_UTILITIES_HPP_ +#define _C_CAMERA_PRESENTATION_UTILITIES_HPP_ + +#include + +#include "CCameraTextUtilities.hpp" + +namespace nbl::hlsl +{ + +//! Shared exactness-oriented filter used by preset presentation surfaces. +enum class EPresetApplyPresentationFilter : uint8_t +{ + All, + Exact, + BestEffort +}; + +//! Presentation-ready wrapper around analyzed goal apply compatibility. +struct SCameraGoalApplyPresentation final : SCameraGoalApplyAnalysis +{ + std::string compatibilityLabel; + std::string policyLabel; + + inline bool matchesFilter(const EPresetApplyPresentationFilter mode) const + { + switch (mode) + { + case EPresetApplyPresentationFilter::All: + return true; + case EPresetApplyPresentationFilter::Exact: + return hasCamera && exact(); + case EPresetApplyPresentationFilter::BestEffort: + return hasCamera && !exact(); + default: + return true; + } + } +}; + +//! Presentation-ready wrapper around analyzed camera capture viability. +struct SCameraCapturePresentation final : SCameraCaptureAnalysis +{ + std::string policyLabel; +}; + +//! Build presentation text for one analyzed goal-apply result. +inline SCameraGoalApplyPresentation makeGoalApplyPresentation(const SCameraGoalApplyAnalysis& analysis, const ICamera* targetCamera) +{ + SCameraGoalApplyPresentation presentation; + static_cast(presentation) = analysis; + presentation.compatibilityLabel = describeGoalApplyCompatibility(analysis, targetCamera); + presentation.policyLabel = describeGoalApplyPolicy(analysis); + return presentation; +} + +//! Analyze one preset against one camera and return reusable presentation data. +inline SCameraGoalApplyPresentation analyzePresetPresentation(const CCameraGoalSolver& solver, const ICamera* camera, const CCameraPreset& preset) +{ + return makeGoalApplyPresentation(analyzePresetApply(solver, camera, preset), camera); +} + +//! Analyze one camera capture path and return reusable presentation data. +inline SCameraCapturePresentation analyzeCapturePresentation(const CCameraGoalSolver& solver, ICamera* camera) +{ + SCameraCapturePresentation presentation; + static_cast(presentation) = analyzeCameraCapture(solver, camera); + presentation.policyLabel = describeCameraCapturePolicy(presentation, camera); + return presentation; +} + +} // namespace nbl::hlsl + +#endif // _C_CAMERA_PRESENTATION_UTILITIES_HPP_ diff --git a/common/include/camera/README.md b/common/include/camera/README.md index d783aba3c..bfe19d365 100644 --- a/common/include/camera/README.md +++ b/common/include/camera/README.md @@ -266,6 +266,19 @@ Provides: This keeps camera-specific presentation and diagnostic text reusable without leaving it in example-local glue. +### `CCameraPresentationUtilities.hpp` + +Reusable presentation-oriented wrappers built on top of shared camera analysis and text helpers. + +Provides: + +- exact-vs-best-effort preset presentation filtering +- presentation-ready apply-analysis structs +- presentation-ready capture-analysis structs +- ready-to-render compatibility and policy labels + +This keeps higher-level preset and capture presentation flow reusable without leaving it in example-local glue. + ### `CCameraPreset.hpp` Reusable preset and keyframe state plus JSON IO. From e2f823ca5f5ad917db76ccf33988107d0e5a0449 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 20:16:55 +0200 Subject: [PATCH 160/205] Share camera presentation badge utilities --- 61_UI/AppControlPanel.cpp | 30 +++++++--- 61_UI/AppInit.cpp | 55 +++++++++++++++++++ 61_UI/include/app/App.hpp | 11 ---- 61_UI/include/common.hpp | 1 + .../camera/CCameraPresentationUtilities.hpp | 38 +++++++++++++ common/include/camera/README.md | 2 + 6 files changed, 117 insertions(+), 20 deletions(-) diff --git a/61_UI/AppControlPanel.cpp b/61_UI/AppControlPanel.cpp index a08121c21..cfaf8d1ae 100644 --- a/61_UI/AppControlPanel.cpp +++ b/61_UI/AppControlPanel.cpp @@ -711,7 +711,11 @@ void App::DrawControlPanel() if (!m_presets.empty()) { - const char* presetFilterLabels[] = { "All", "Exact", "Best-effort" }; + const char* presetFilterLabels[] = { + nbl::hlsl::getPresetApplyPresentationFilterLabel(PresetFilterMode::All), + nbl::hlsl::getPresetApplyPresentationFilterLabel(PresetFilterMode::Exact), + nbl::hlsl::getPresetApplyPresentationFilterLabel(PresetFilterMode::BestEffort) + }; int presetFilterIx = static_cast(m_presetFilterMode); if (ImGui::Combo("Visibility", &presetFilterIx, presetFilterLabels, IM_ARRAYSIZE(presetFilterLabels))) m_presetFilterMode = static_cast(presetFilterIx); @@ -759,6 +763,7 @@ void App::DrawControlPanel() { const auto& preset = m_presets[static_cast(m_selectedPresetIx)]; const auto presetUi = analyzePresetForUi(activeCamera, preset); + const auto presetBadges = nbl::hlsl::collectGoalApplyPresentationBadges(presetUi); const ImVec4 compatibilityColor = !presetUi.hasCamera ? bad : (presetUi.exact() ? good : warn); ImGui::TextDisabled("Preset source"); @@ -774,18 +779,21 @@ void App::DrawControlPanel() ImGui::SameLine(); ImGui::TextColored(compatibilityColor, "%s", presetUi.compatibilityLabel.c_str()); - DrawBadge(presetUi.exact() ? "EXACT" : "BEST-EFFORT", presetUi.exact() ? good : warn, badgeText); - if (presetUi.dropsGoalState()) + if (presetBadges.exact) + DrawBadge("EXACT", good, badgeText); + else if (presetBadges.bestEffort) + DrawBadge("BEST-EFFORT", warn, badgeText); + if (presetBadges.dropsState) { ImGui::SameLine(); DrawBadge("DROPS STATE", warn, badgeText); } - else if (presetUi.usesSharedStateOnly()) + else if (presetBadges.sharedStateOnly) { ImGui::SameLine(); DrawBadge("SHARED STATE", accent, badgeText); } - if (!presetUi.canApply) + if (presetBadges.blocked) { ImGui::SameLine(); DrawBadge("BLOCKED", bad, badgeText); @@ -949,6 +957,7 @@ void App::DrawControlPanel() if (auto* selectedKeyframe = getSelectedKeyframe()) { const auto keyframeUi = analyzePresetForUi(activeCamera, selectedKeyframe->preset); + const auto keyframeBadges = nbl::hlsl::collectGoalApplyPresentationBadges(keyframeUi); const ImVec4 compatibilityColor = !keyframeUi.hasCamera ? bad : (keyframeUi.exact() ? good : warn); float selectedTime = selectedKeyframe->time; if (ImGui::InputFloat("Selected time", &selectedTime, 0.1f, 1.f, "%.3f")) @@ -974,18 +983,21 @@ void App::DrawControlPanel() ImGui::SameLine(); ImGui::TextColored(compatibilityColor, "%s", keyframeUi.compatibilityLabel.c_str()); - DrawBadge(keyframeUi.exact() ? "EXACT" : "BEST-EFFORT", keyframeUi.exact() ? good : warn, badgeText); - if (keyframeUi.dropsGoalState()) + if (keyframeBadges.exact) + DrawBadge("EXACT", good, badgeText); + else if (keyframeBadges.bestEffort) + DrawBadge("BEST-EFFORT", warn, badgeText); + if (keyframeBadges.dropsState) { ImGui::SameLine(); DrawBadge("DROPS STATE", warn, badgeText); } - else if (keyframeUi.usesSharedStateOnly()) + else if (keyframeBadges.sharedStateOnly) { ImGui::SameLine(); DrawBadge("SHARED STATE", accent, badgeText); } - if (!keyframeUi.canApply) + if (keyframeBadges.blocked) { ImGui::SameLine(); DrawBadge("BLOCKED", bad, badgeText); diff --git a/61_UI/AppInit.cpp b/61_UI/AppInit.cpp index 5549700ce..67802ea33 100644 --- a/61_UI/AppInit.cpp +++ b/61_UI/AppInit.cpp @@ -769,6 +769,61 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) } } + if (hasOrbitPreset) + { + if (std::string_view(nbl::hlsl::getPresetApplyPresentationFilterLabel(EPresetApplyPresentationFilter::All)) != "All" || + std::string_view(nbl::hlsl::getPresetApplyPresentationFilterLabel(EPresetApplyPresentationFilter::Exact)) != "Exact" || + std::string_view(nbl::hlsl::getPresetApplyPresentationFilterLabel(EPresetApplyPresentationFilter::BestEffort)) != "Best-effort") + { + return fail("Presentation utilities smoke returned an unexpected filter label."); + } + + const auto blockedPresentation = nbl::hlsl::analyzePresetPresentation(m_cameraGoalSolver, nullptr, initialOrbitPreset); + if (blockedPresentation.matchesFilter(EPresetApplyPresentationFilter::Exact) || + blockedPresentation.matchesFilter(EPresetApplyPresentationFilter::BestEffort)) + { + return fail("Presentation utilities smoke allowed a null-camera preset through an exactness filter."); + } + + const auto blockedBadges = nbl::hlsl::collectGoalApplyPresentationBadges(blockedPresentation); + if (!blockedBadges.blocked || blockedBadges.exact || blockedBadges.bestEffort) + return fail("Presentation utilities smoke produced wrong blocked badge flags."); + + if (orbitCamera) + { + const auto exactPresentation = nbl::hlsl::analyzePresetPresentation(m_cameraGoalSolver, orbitCamera, initialOrbitPreset); + if (!exactPresentation.matchesFilter(EPresetApplyPresentationFilter::All) || + !exactPresentation.matchesFilter(EPresetApplyPresentationFilter::Exact) || + exactPresentation.matchesFilter(EPresetApplyPresentationFilter::BestEffort)) + { + return fail("Presentation utilities smoke failed exact filtering."); + } + + const auto exactBadges = nbl::hlsl::collectGoalApplyPresentationBadges(exactPresentation); + if (!exactBadges.exact || exactBadges.bestEffort || exactBadges.dropsState || exactBadges.sharedStateOnly || exactBadges.blocked) + return fail("Presentation utilities smoke produced wrong exact badge flags."); + + const auto capturePresentation = nbl::hlsl::analyzeCapturePresentation(m_cameraGoalSolver, orbitCamera); + if (!capturePresentation.canCapture || capturePresentation.policyLabel.empty()) + return fail("Presentation utilities smoke failed orbit capture presentation."); + } + } + + if (hasOrbitPreset && hasPathPreset && orbitCamera) + { + const auto approximatePresentation = nbl::hlsl::analyzePresetPresentation(m_cameraGoalSolver, orbitCamera, initialPathPreset); + if (!approximatePresentation.matchesFilter(EPresetApplyPresentationFilter::All) || + approximatePresentation.matchesFilter(EPresetApplyPresentationFilter::Exact) || + !approximatePresentation.matchesFilter(EPresetApplyPresentationFilter::BestEffort)) + { + return fail("Presentation utilities smoke failed best-effort filtering."); + } + + const auto approximateBadges = nbl::hlsl::collectGoalApplyPresentationBadges(approximatePresentation); + if (approximateBadges.exact || !approximateBadges.bestEffort || !approximateBadges.dropsState || approximateBadges.sharedStateOnly || approximateBadges.blocked) + return fail("Presentation utilities smoke produced wrong best-effort badge flags."); + } + { std::vector sourcePresets; if (hasOrbitPreset) diff --git a/61_UI/include/app/App.hpp b/61_UI/include/app/App.hpp index 2731429a7..1bbe5bfb9 100644 --- a/61_UI/include/app/App.hpp +++ b/61_UI/include/app/App.hpp @@ -983,17 +983,6 @@ class App final : public examples::SimpleWindowedApplication return nbl::hlsl::analyzePresetApply(m_cameraGoalSolver, camera, preset).compatibility; } - inline const char* getPresetFilterModeLabel(PresetFilterMode mode) const - { - switch (mode) - { - case PresetFilterMode::All: return "All"; - case PresetFilterMode::Exact: return "Exact"; - case PresetFilterMode::BestEffort: return "Best-effort"; - default: return "All"; - } - } - inline bool presetMatchesFilter(ICamera* camera, const CameraPreset& preset) const { return analyzePresetForUi(camera, preset).matchesFilter(m_presetFilterMode); diff --git a/61_UI/include/common.hpp b/61_UI/include/common.hpp index fc3697b3d..7373cbada 100644 --- a/61_UI/include/common.hpp +++ b/61_UI/include/common.hpp @@ -75,6 +75,7 @@ using nbl::hlsl::SCameraPresetApplySummary; using nbl::hlsl::SCameraGoalApplyAnalysis; using nbl::hlsl::SCameraCaptureAnalysis; using nbl::hlsl::SCameraGoalApplyPresentation; +using nbl::hlsl::SCameraGoalApplyPresentationBadges; using nbl::hlsl::SCameraCapturePresentation; using nbl::hlsl::SCameraConstraintSettings; using nbl::hlsl::CCameraGoalSolver; diff --git a/common/include/camera/CCameraPresentationUtilities.hpp b/common/include/camera/CCameraPresentationUtilities.hpp index 3dd6ad9e4..c04effa5f 100644 --- a/common/include/camera/CCameraPresentationUtilities.hpp +++ b/common/include/camera/CCameraPresentationUtilities.hpp @@ -20,6 +20,16 @@ enum class EPresetApplyPresentationFilter : uint8_t BestEffort }; +//! Shared badge/pill policy derived from one analyzed presentation answer. +struct SCameraGoalApplyPresentationBadges final +{ + bool exact = false; + bool bestEffort = false; + bool dropsState = false; + bool sharedStateOnly = false; + bool blocked = false; +}; + //! Presentation-ready wrapper around analyzed goal apply compatibility. struct SCameraGoalApplyPresentation final : SCameraGoalApplyAnalysis { @@ -48,6 +58,22 @@ struct SCameraCapturePresentation final : SCameraCaptureAnalysis std::string policyLabel; }; +//! Shared user-facing label for the exactness filter selector. +inline const char* getPresetApplyPresentationFilterLabel(const EPresetApplyPresentationFilter mode) +{ + switch (mode) + { + case EPresetApplyPresentationFilter::All: + return "All"; + case EPresetApplyPresentationFilter::Exact: + return "Exact"; + case EPresetApplyPresentationFilter::BestEffort: + return "Best-effort"; + default: + return "All"; + } +} + //! Build presentation text for one analyzed goal-apply result. inline SCameraGoalApplyPresentation makeGoalApplyPresentation(const SCameraGoalApplyAnalysis& analysis, const ICamera* targetCamera) { @@ -64,6 +90,18 @@ inline SCameraGoalApplyPresentation analyzePresetPresentation(const CCameraGoalS return makeGoalApplyPresentation(analyzePresetApply(solver, camera, preset), camera); } +//! Build reusable badge flags for one preset/keyframe compatibility answer. +inline SCameraGoalApplyPresentationBadges collectGoalApplyPresentationBadges(const SCameraGoalApplyPresentation& presentation) +{ + SCameraGoalApplyPresentationBadges badges; + badges.exact = presentation.exact(); + badges.bestEffort = presentation.hasCamera && !presentation.exact(); + badges.dropsState = presentation.dropsGoalState(); + badges.sharedStateOnly = presentation.usesSharedStateOnly(); + badges.blocked = !presentation.canApply; + return badges; +} + //! Analyze one camera capture path and return reusable presentation data. inline SCameraCapturePresentation analyzeCapturePresentation(const CCameraGoalSolver& solver, ICamera* camera) { diff --git a/common/include/camera/README.md b/common/include/camera/README.md index bfe19d365..3629e1ebe 100644 --- a/common/include/camera/README.md +++ b/common/include/camera/README.md @@ -273,8 +273,10 @@ Reusable presentation-oriented wrappers built on top of shared camera analysis a Provides: - exact-vs-best-effort preset presentation filtering +- reusable labels for presentation filters - presentation-ready apply-analysis structs - presentation-ready capture-analysis structs +- reusable badge flags for apply/result presentation - ready-to-render compatibility and policy labels This keeps higher-level preset and capture presentation flow reusable without leaving it in example-local glue. From 7ab899cfad21b2e5bbcae5357d5cf1322743b7d9 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 20:23:03 +0200 Subject: [PATCH 161/205] Share camera preset comparison helpers --- 61_UI/AppInit.cpp | 40 +++---------------- .../include/camera/CCameraKeyframeTrack.hpp | 31 ++++++++++++++ common/include/camera/CCameraPreset.hpp | 9 +++++ common/include/camera/README.md | 2 + 4 files changed, 48 insertions(+), 34 deletions(-) diff --git a/61_UI/AppInit.cpp b/61_UI/AppInit.cpp index 67802ea33..286078a3b 100644 --- a/61_UI/AppInit.cpp +++ b/61_UI/AppInit.cpp @@ -712,18 +712,6 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) return true; }; - auto comparePresetValues = [&](const CameraPreset& lhs, const CameraPreset& rhs, const double posEps, const double rotEpsDeg, const double scalarEps) -> bool - { - return lhs.name == rhs.name && - lhs.identifier == rhs.identifier && - nbl::hlsl::compareGoals( - nbl::hlsl::makeGoalFromPreset(lhs), - nbl::hlsl::makeGoalFromPreset(rhs), - posEps, - rotEpsDeg, - scalarEps); - }; - ICamera* orbitCamera = findCameraByKind(ICamera::CameraKind::Orbit); ICamera* freeCamera = findCameraByKind(ICamera::CameraKind::Free); ICamera* chaseCamera = findCameraByKind(ICamera::CameraKind::Chase); @@ -852,7 +840,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) for (size_t i = 0u; i < sourcePresets.size(); ++i) { - if (!comparePresetValues(sourcePresets[i], loadedPresets[i], 1e-6, 0.1, 1e-6)) + if (!nbl::hlsl::comparePresets(sourcePresets[i], loadedPresets[i], 1e-6, 0.1, 1e-6)) return fail("Preset persistence smoke changed preset content at index " + std::to_string(i) + "."); } @@ -874,16 +862,8 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) CCameraKeyframeTrack loadedTrack; if (!nbl::hlsl::readKeyframeTrack(keyframeBuffer, loadedTrack)) return fail("Keyframe persistence smoke failed to deserialize track."); - if (loadedTrack.keyframes.size() != sourceTrack.keyframes.size()) - return fail("Keyframe persistence smoke changed keyframe count."); - - for (size_t i = 0u; i < sourceTrack.keyframes.size(); ++i) - { - if (std::abs(static_cast(loadedTrack.keyframes[i].time - sourceTrack.keyframes[i].time)) > 1e-6) - return fail("Keyframe persistence smoke changed keyframe time at index " + std::to_string(i) + "."); - if (!comparePresetValues(sourceTrack.keyframes[i].preset, loadedTrack.keyframes[i].preset, 1e-6, 0.1, 1e-6)) - return fail("Keyframe persistence smoke changed keyframe preset at index " + std::to_string(i) + "."); - } + if (!nbl::hlsl::compareKeyframeTrackContent(sourceTrack, loadedTrack, 1e-6, 1e-6, 0.1, 1e-6)) + return fail("Keyframe persistence smoke changed stream track content."); struct TempFileCleanup final { @@ -913,7 +893,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) for (size_t i = 0u; i < sourcePresets.size(); ++i) { - if (!comparePresetValues(sourcePresets[i], fileLoadedPresets[i], 1e-6, 0.1, 1e-6)) + if (!nbl::hlsl::comparePresets(sourcePresets[i], fileLoadedPresets[i], 1e-6, 0.1, 1e-6)) return fail("Preset persistence smoke changed preset file content at index " + std::to_string(i) + "."); } @@ -923,16 +903,8 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) CCameraKeyframeTrack fileLoadedTrack; if (!nbl::hlsl::loadKeyframeTrackFromFile(keyframeFile, fileLoadedTrack)) return fail("Keyframe persistence smoke failed to load track file."); - if (fileLoadedTrack.keyframes.size() != sourceTrack.keyframes.size()) - return fail("Keyframe persistence smoke changed keyframe file count."); - - for (size_t i = 0u; i < sourceTrack.keyframes.size(); ++i) - { - if (std::abs(static_cast(fileLoadedTrack.keyframes[i].time - sourceTrack.keyframes[i].time)) > 1e-6) - return fail("Keyframe persistence smoke changed keyframe file time at index " + std::to_string(i) + "."); - if (!comparePresetValues(sourceTrack.keyframes[i].preset, fileLoadedTrack.keyframes[i].preset, 1e-6, 0.1, 1e-6)) - return fail("Keyframe persistence smoke changed keyframe file preset at index " + std::to_string(i) + "."); - } + if (!nbl::hlsl::compareKeyframeTrackContent(sourceTrack, fileLoadedTrack, 1e-6, 1e-6, 0.1, 1e-6)) + return fail("Keyframe persistence smoke changed file track content."); } if (hasOrbitPreset && hasDollyPreset) diff --git a/common/include/camera/CCameraKeyframeTrack.hpp b/common/include/camera/CCameraKeyframeTrack.hpp index 448993bbc..edf991078 100644 --- a/common/include/camera/CCameraKeyframeTrack.hpp +++ b/common/include/camera/CCameraKeyframeTrack.hpp @@ -21,6 +21,37 @@ struct CCameraKeyframeTrack int selectedKeyframeIx = -1; }; +//! Compare two keyframes by authored time and shared preset state. +inline bool compareKeyframes(const CCameraKeyframe& lhs, const CCameraKeyframe& rhs, + const double timeEps, const double posEps, const double rotEpsDeg, const double scalarEps) +{ + return std::abs(static_cast(lhs.time - rhs.time)) <= timeEps && + comparePresets(lhs.preset, rhs.preset, posEps, rotEpsDeg, scalarEps); +} + +//! Compare two authored keyframe tracks with optional selection-state checking. +inline bool compareKeyframeTracks(const CCameraKeyframeTrack& lhs, const CCameraKeyframeTrack& rhs, + const double timeEps, const double posEps, const double rotEpsDeg, const double scalarEps, const bool compareSelection = true) +{ + if ((compareSelection && lhs.selectedKeyframeIx != rhs.selectedKeyframeIx) || lhs.keyframes.size() != rhs.keyframes.size()) + return false; + + for (size_t i = 0u; i < lhs.keyframes.size(); ++i) + { + if (!compareKeyframes(lhs.keyframes[i], rhs.keyframes[i], timeEps, posEps, rotEpsDeg, scalarEps)) + return false; + } + + return true; +} + +//! Compare only the serialized/authored content of two tracks and ignore transient UI selection state. +inline bool compareKeyframeTrackContent(const CCameraKeyframeTrack& lhs, const CCameraKeyframeTrack& rhs, + const double timeEps, const double posEps, const double rotEpsDeg, const double scalarEps) +{ + return compareKeyframeTracks(lhs, rhs, timeEps, posEps, rotEpsDeg, scalarEps, false); +} + inline bool tryBuildKeyframeTrackPresetAtTime(const CCameraKeyframeTrack& track, const float time, CCameraPreset& preset) { if (track.keyframes.empty()) diff --git a/common/include/camera/CCameraPreset.hpp b/common/include/camera/CCameraPreset.hpp index cdb18c953..e07e2189d 100644 --- a/common/include/camera/CCameraPreset.hpp +++ b/common/include/camera/CCameraPreset.hpp @@ -38,6 +38,15 @@ inline CCameraGoal makeGoalFromPreset(const CCameraPreset& preset) return canonicalizeGoal(preset.goal); } +//! Compare two named presets through their shared canonical goal state. +inline bool comparePresets(const CCameraPreset& lhs, const CCameraPreset& rhs, + const double posEps, const double rotEpsDeg, const double scalarEps) +{ + return lhs.name == rhs.name && + lhs.identifier == rhs.identifier && + compareGoals(makeGoalFromPreset(lhs), makeGoalFromPreset(rhs), posEps, rotEpsDeg, scalarEps); +} + inline nlohmann::json serializeGoal(const CCameraGoal& goal) { nlohmann::json j; diff --git a/common/include/camera/README.md b/common/include/camera/README.md index 3629e1ebe..80c7479b5 100644 --- a/common/include/camera/README.md +++ b/common/include/camera/README.md @@ -289,6 +289,7 @@ Provides: - `CCameraPreset` - `CCameraKeyframe` +- preset comparison helpers - goal-to-preset conversion helpers - preset JSON serialization and deserialization @@ -338,6 +339,7 @@ Reusable keyframe-track helpers on top of presets. Provides: - `CCameraKeyframeTrack` +- keyframe and track comparison helpers with optional selection-state checks - preset-at-time evaluation - keyframe sorting - playback-time clamping From 32b4c7efb6ba16d42c1641ef56f4f7cdb72d02de Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 20:27:12 +0200 Subject: [PATCH 162/205] Share camera presentation data labels --- 61_UI/AppControlPanel.cpp | 30 +++++++++---------- 61_UI/AppInit.cpp | 8 ++++- .../camera/CCameraPresentationUtilities.hpp | 8 +++++ common/include/camera/README.md | 1 + 4 files changed, 30 insertions(+), 17 deletions(-) diff --git a/61_UI/AppControlPanel.cpp b/61_UI/AppControlPanel.cpp index cfaf8d1ae..1ed680893 100644 --- a/61_UI/AppControlPanel.cpp +++ b/61_UI/AppControlPanel.cpp @@ -763,15 +763,14 @@ void App::DrawControlPanel() { const auto& preset = m_presets[static_cast(m_selectedPresetIx)]; const auto presetUi = analyzePresetForUi(activeCamera, preset); - const auto presetBadges = nbl::hlsl::collectGoalApplyPresentationBadges(presetUi); const ImVec4 compatibilityColor = !presetUi.hasCamera ? bad : (presetUi.exact() ? good : warn); ImGui::TextDisabled("Preset source"); ImGui::SameLine(); - ImGui::TextColored(muted, "%s", getCameraTypeLabel(presetUi.goal.sourceKind).data()); + ImGui::TextColored(muted, "%s", presetUi.sourceKindLabel.c_str()); ImGui::TextDisabled("Goal state"); ImGui::SameLine(); - ImGui::TextColored(muted, "%s", describeGoalStateMask(presetUi.goal.sourceGoalStateMask).c_str()); + ImGui::TextColored(muted, "%s", presetUi.goalStateLabel.c_str()); ImGui::TextDisabled("Policy"); ImGui::SameLine(); ImGui::TextColored(presetUi.canApply ? compatibilityColor : bad, "%s", presetUi.policyLabel.c_str()); @@ -779,21 +778,21 @@ void App::DrawControlPanel() ImGui::SameLine(); ImGui::TextColored(compatibilityColor, "%s", presetUi.compatibilityLabel.c_str()); - if (presetBadges.exact) + if (presetUi.badges.exact) DrawBadge("EXACT", good, badgeText); - else if (presetBadges.bestEffort) + else if (presetUi.badges.bestEffort) DrawBadge("BEST-EFFORT", warn, badgeText); - if (presetBadges.dropsState) + if (presetUi.badges.dropsState) { ImGui::SameLine(); DrawBadge("DROPS STATE", warn, badgeText); } - else if (presetBadges.sharedStateOnly) + else if (presetUi.badges.sharedStateOnly) { ImGui::SameLine(); DrawBadge("SHARED STATE", accent, badgeText); } - if (presetBadges.blocked) + if (presetUi.badges.blocked) { ImGui::SameLine(); DrawBadge("BLOCKED", bad, badgeText); @@ -957,7 +956,6 @@ void App::DrawControlPanel() if (auto* selectedKeyframe = getSelectedKeyframe()) { const auto keyframeUi = analyzePresetForUi(activeCamera, selectedKeyframe->preset); - const auto keyframeBadges = nbl::hlsl::collectGoalApplyPresentationBadges(keyframeUi); const ImVec4 compatibilityColor = !keyframeUi.hasCamera ? bad : (keyframeUi.exact() ? good : warn); float selectedTime = selectedKeyframe->time; if (ImGui::InputFloat("Selected time", &selectedTime, 0.1f, 1.f, "%.3f")) @@ -972,10 +970,10 @@ void App::DrawControlPanel() ImGui::TextDisabled("Keyframe source"); ImGui::SameLine(); - ImGui::TextColored(muted, "%s", getCameraTypeLabel(keyframeUi.goal.sourceKind).data()); + ImGui::TextColored(muted, "%s", keyframeUi.sourceKindLabel.c_str()); ImGui::TextDisabled("Goal state"); ImGui::SameLine(); - ImGui::TextColored(muted, "%s", describeGoalStateMask(keyframeUi.goal.sourceGoalStateMask).c_str()); + ImGui::TextColored(muted, "%s", keyframeUi.goalStateLabel.c_str()); ImGui::TextDisabled("Policy"); ImGui::SameLine(); ImGui::TextColored(keyframeUi.canApply ? compatibilityColor : bad, "%s", keyframeUi.policyLabel.c_str()); @@ -983,21 +981,21 @@ void App::DrawControlPanel() ImGui::SameLine(); ImGui::TextColored(compatibilityColor, "%s", keyframeUi.compatibilityLabel.c_str()); - if (keyframeBadges.exact) + if (keyframeUi.badges.exact) DrawBadge("EXACT", good, badgeText); - else if (keyframeBadges.bestEffort) + else if (keyframeUi.badges.bestEffort) DrawBadge("BEST-EFFORT", warn, badgeText); - if (keyframeBadges.dropsState) + if (keyframeUi.badges.dropsState) { ImGui::SameLine(); DrawBadge("DROPS STATE", warn, badgeText); } - else if (keyframeBadges.sharedStateOnly) + else if (keyframeUi.badges.sharedStateOnly) { ImGui::SameLine(); DrawBadge("SHARED STATE", accent, badgeText); } - if (keyframeBadges.blocked) + if (keyframeUi.badges.blocked) { ImGui::SameLine(); DrawBadge("BLOCKED", bad, badgeText); diff --git a/61_UI/AppInit.cpp b/61_UI/AppInit.cpp index 286078a3b..8524724c8 100644 --- a/61_UI/AppInit.cpp +++ b/61_UI/AppInit.cpp @@ -772,9 +772,11 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) { return fail("Presentation utilities smoke allowed a null-camera preset through an exactness filter."); } + if (blockedPresentation.sourceKindLabel.empty() || blockedPresentation.goalStateLabel.empty()) + return fail("Presentation utilities smoke produced empty blocked presentation labels."); const auto blockedBadges = nbl::hlsl::collectGoalApplyPresentationBadges(blockedPresentation); - if (!blockedBadges.blocked || blockedBadges.exact || blockedBadges.bestEffort) + if (!blockedBadges.blocked || blockedBadges.exact || blockedBadges.bestEffort || blockedPresentation.badges.blocked != blockedBadges.blocked) return fail("Presentation utilities smoke produced wrong blocked badge flags."); if (orbitCamera) @@ -790,6 +792,8 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) const auto exactBadges = nbl::hlsl::collectGoalApplyPresentationBadges(exactPresentation); if (!exactBadges.exact || exactBadges.bestEffort || exactBadges.dropsState || exactBadges.sharedStateOnly || exactBadges.blocked) return fail("Presentation utilities smoke produced wrong exact badge flags."); + if (exactPresentation.sourceKindLabel.empty() || exactPresentation.goalStateLabel.empty()) + return fail("Presentation utilities smoke produced empty exact presentation labels."); const auto capturePresentation = nbl::hlsl::analyzeCapturePresentation(m_cameraGoalSolver, orbitCamera); if (!capturePresentation.canCapture || capturePresentation.policyLabel.empty()) @@ -810,6 +814,8 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) const auto approximateBadges = nbl::hlsl::collectGoalApplyPresentationBadges(approximatePresentation); if (approximateBadges.exact || !approximateBadges.bestEffort || !approximateBadges.dropsState || approximateBadges.sharedStateOnly || approximateBadges.blocked) return fail("Presentation utilities smoke produced wrong best-effort badge flags."); + if (approximatePresentation.sourceKindLabel.empty() || approximatePresentation.goalStateLabel.empty()) + return fail("Presentation utilities smoke produced empty best-effort presentation labels."); } { diff --git a/common/include/camera/CCameraPresentationUtilities.hpp b/common/include/camera/CCameraPresentationUtilities.hpp index c04effa5f..c7604a41a 100644 --- a/common/include/camera/CCameraPresentationUtilities.hpp +++ b/common/include/camera/CCameraPresentationUtilities.hpp @@ -33,6 +33,9 @@ struct SCameraGoalApplyPresentationBadges final //! Presentation-ready wrapper around analyzed goal apply compatibility. struct SCameraGoalApplyPresentation final : SCameraGoalApplyAnalysis { + SCameraGoalApplyPresentationBadges badges; + std::string sourceKindLabel; + std::string goalStateLabel; std::string compatibilityLabel; std::string policyLabel; @@ -58,6 +61,8 @@ struct SCameraCapturePresentation final : SCameraCaptureAnalysis std::string policyLabel; }; +inline SCameraGoalApplyPresentationBadges collectGoalApplyPresentationBadges(const SCameraGoalApplyPresentation& presentation); + //! Shared user-facing label for the exactness filter selector. inline const char* getPresetApplyPresentationFilterLabel(const EPresetApplyPresentationFilter mode) { @@ -79,6 +84,9 @@ inline SCameraGoalApplyPresentation makeGoalApplyPresentation(const SCameraGoalA { SCameraGoalApplyPresentation presentation; static_cast(presentation) = analysis; + presentation.badges = collectGoalApplyPresentationBadges(presentation); + presentation.sourceKindLabel = std::string(getCameraTypeLabel(presentation.goal.sourceKind)); + presentation.goalStateLabel = describeGoalStateMask(presentation.goal.sourceGoalStateMask); presentation.compatibilityLabel = describeGoalApplyCompatibility(analysis, targetCamera); presentation.policyLabel = describeGoalApplyPolicy(analysis); return presentation; diff --git a/common/include/camera/README.md b/common/include/camera/README.md index 80c7479b5..7167356ac 100644 --- a/common/include/camera/README.md +++ b/common/include/camera/README.md @@ -277,6 +277,7 @@ Provides: - presentation-ready apply-analysis structs - presentation-ready capture-analysis structs - reusable badge flags for apply/result presentation +- presentation-ready source-kind and goal-state labels - ready-to-render compatibility and policy labels This keeps higher-level preset and capture presentation flow reusable without leaving it in example-local glue. From 8a996d47024dbfd983c191284f2e7a0f26f06a4b Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 20:30:35 +0200 Subject: [PATCH 163/205] Share camera preset collection comparison --- 61_UI/AppInit.cpp | 22 ++++++++++------------ common/include/camera/CCameraPreset.hpp | 17 +++++++++++++++++ common/include/camera/README.md | 1 + 3 files changed, 28 insertions(+), 12 deletions(-) diff --git a/61_UI/AppInit.cpp b/61_UI/AppInit.cpp index 8524724c8..15d05c343 100644 --- a/61_UI/AppInit.cpp +++ b/61_UI/AppInit.cpp @@ -841,13 +841,12 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) std::vector loadedPresets; if (!nbl::hlsl::readPresetCollection(presetBuffer, loadedPresets)) return fail("Preset persistence smoke failed to deserialize preset collection."); - if (loadedPresets.size() != sourcePresets.size()) - return fail("Preset persistence smoke changed preset count."); - - for (size_t i = 0u; i < sourcePresets.size(); ++i) + if (!nbl::hlsl::comparePresetCollections( + std::span(sourcePresets.data(), sourcePresets.size()), + std::span(loadedPresets.data(), loadedPresets.size()), + 1e-6, 0.1, 1e-6)) { - if (!nbl::hlsl::comparePresets(sourcePresets[i], loadedPresets[i], 1e-6, 0.1, 1e-6)) - return fail("Preset persistence smoke changed preset content at index " + std::to_string(i) + "."); + return fail("Preset persistence smoke changed stream preset collection content."); } CCameraKeyframeTrack sourceTrack; @@ -894,13 +893,12 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) std::vector fileLoadedPresets; if (!nbl::hlsl::loadPresetCollectionFromFile(presetFile, fileLoadedPresets)) return fail("Preset persistence smoke failed to load preset collection file."); - if (fileLoadedPresets.size() != sourcePresets.size()) - return fail("Preset persistence smoke changed preset file count."); - - for (size_t i = 0u; i < sourcePresets.size(); ++i) + if (!nbl::hlsl::comparePresetCollections( + std::span(sourcePresets.data(), sourcePresets.size()), + std::span(fileLoadedPresets.data(), fileLoadedPresets.size()), + 1e-6, 0.1, 1e-6)) { - if (!nbl::hlsl::comparePresets(sourcePresets[i], fileLoadedPresets[i], 1e-6, 0.1, 1e-6)) - return fail("Preset persistence smoke changed preset file content at index " + std::to_string(i) + "."); + return fail("Preset persistence smoke changed file preset collection content."); } if (!nbl::hlsl::saveKeyframeTrackToFile(keyframeFile, sourceTrack)) diff --git a/common/include/camera/CCameraPreset.hpp b/common/include/camera/CCameraPreset.hpp index e07e2189d..a8cd41acc 100644 --- a/common/include/camera/CCameraPreset.hpp +++ b/common/include/camera/CCameraPreset.hpp @@ -5,6 +5,7 @@ #ifndef _C_CAMERA_PRESET_HPP_ #define _C_CAMERA_PRESET_HPP_ +#include #include #include "CCameraGoal.hpp" @@ -47,6 +48,22 @@ inline bool comparePresets(const CCameraPreset& lhs, const CCameraPreset& rhs, compareGoals(makeGoalFromPreset(lhs), makeGoalFromPreset(rhs), posEps, rotEpsDeg, scalarEps); } +//! Compare two preset collections element-by-element through the shared canonical goal state. +inline bool comparePresetCollections(std::span lhs, std::span rhs, + const double posEps, const double rotEpsDeg, const double scalarEps) +{ + if (lhs.size() != rhs.size()) + return false; + + for (size_t i = 0u; i < lhs.size(); ++i) + { + if (!comparePresets(lhs[i], rhs[i], posEps, rotEpsDeg, scalarEps)) + return false; + } + + return true; +} + inline nlohmann::json serializeGoal(const CCameraGoal& goal) { nlohmann::json j; diff --git a/common/include/camera/README.md b/common/include/camera/README.md index 7167356ac..f03525bce 100644 --- a/common/include/camera/README.md +++ b/common/include/camera/README.md @@ -291,6 +291,7 @@ Provides: - `CCameraPreset` - `CCameraKeyframe` - preset comparison helpers +- preset collection comparison helpers - goal-to-preset conversion helpers - preset JSON serialization and deserialization From dcd52d1227a6f9ba2d30ce206504bfb93ca1774b Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Mon, 6 Apr 2026 15:29:50 +0200 Subject: [PATCH 164/205] Compact camera continuity scripting --- 61_UI/AppInit.cpp | 238 + 61_UI/AppUpdate.cpp | 84 +- 61_UI/README.md | 16 +- 61_UI/app_resources/cameraz_continuity.json | 66732 +--------------- 61_UI/include/app/App.hpp | 10 +- 61_UI/include/common.hpp | 6 + .../include/camera/CCameraSequenceScript.hpp | 873 + common/include/camera/README.md | 35 + 8 files changed, 1527 insertions(+), 66467 deletions(-) create mode 100644 common/include/camera/CCameraSequenceScript.hpp diff --git a/61_UI/AppInit.cpp b/61_UI/AppInit.cpp index 15d05c343..64cf1b671 100644 --- a/61_UI/AppInit.cpp +++ b/61_UI/AppInit.cpp @@ -1,10 +1,12 @@ #include "app/App.hpp" #include #include +#include #include #include #include #include +#include #include #include #include @@ -1230,8 +1232,33 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) return true; }; + std::optional pendingScriptedSequence; + bool scriptedInputParseFailed = false; + std::string scriptedInputParseError; + + auto finalizeScriptedInput = [&]() -> void + { + std::sort(m_scriptedInput.events.begin(), m_scriptedInput.events.end(), + [](const ScriptedInputEvent& a, const ScriptedInputEvent& b) { return a.frame < b.frame; }); + std::sort(m_scriptedInput.checks.begin(), m_scriptedInput.checks.end(), + [](const ScriptedInputCheck& a, const ScriptedInputCheck& b) { return a.frame < b.frame; }); + if (!m_scriptedInput.captureFrames.empty()) + { + std::sort(m_scriptedInput.captureFrames.begin(), m_scriptedInput.captureFrames.end()); + m_scriptedInput.captureFrames.erase(std::unique(m_scriptedInput.captureFrames.begin(), m_scriptedInput.captureFrames.end()), m_scriptedInput.captureFrames.end()); + } + if (m_disableScreenshotsCli) + { + m_scriptedInput.captureFrames.clear(); + m_scriptedInput.nextCaptureIndex = 0; + } + }; + auto parseScriptedInput = [&](const nbl_json& script) -> void { + pendingScriptedSequence.reset(); + scriptedInputParseFailed = false; + scriptedInputParseError.clear(); m_scriptedInput.events.clear(); m_scriptedInput.checks.clear(); m_scriptedInput.captureFrames.clear(); @@ -1316,6 +1343,21 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) m_cameraControls.rotationScale = controls["rotation_scale"].get(); } + if (script.contains("segments")) + { + CCameraSequenceScript sequence; + std::string sequenceError; + if (!deserializeCameraSequenceScript(script, sequence, &sequenceError)) + { + scriptedInputParseFailed = true; + scriptedInputParseError = std::move(sequenceError); + } + else + { + pendingScriptedSequence = std::move(sequence); + } + } + if (script.contains("events")) for (const auto& ev : script["events"]) { @@ -1720,10 +1762,20 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) if (!loadScriptJson(scriptPath.string(), scriptJson)) return false; parseScriptedInput(scriptJson); + if (scriptedInputParseFailed) + { + logFail("Camera sequence script parse failed: %s", scriptedInputParseError.c_str()); + return false; + } } else if (j.contains("scripted_input")) { parseScriptedInput(j["scripted_input"]); + if (scriptedInputParseFailed) + { + logFail("Camera sequence script parse failed: %s", scriptedInputParseError.c_str()); + return false; + } } std::vector> cameras; @@ -1981,6 +2033,192 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) const std::string presetName = "Planar " + std::to_string(planarIx); m_initialPlanarPresets.emplace_back(nbl::hlsl::capturePreset(m_cameraGoalSolver, camera, presetName)); } + + if (pendingScriptedSequence.has_value()) + { + auto expandCameraSequenceScript = [&](const CCameraSequenceScript& sequence) -> bool + { + m_scriptedInput.events.clear(); + m_scriptedInput.checks.clear(); + m_scriptedInput.captureFrames.clear(); + m_scriptedInput.nextEventIndex = 0; + m_scriptedInput.nextCheckIndex = 0; + m_scriptedInput.nextCaptureIndex = 0; + m_scriptedInput.failed = false; + m_scriptedInput.summaryReported = false; + m_scriptedInput.baselineValid = false; + m_scriptedInput.stepValid = false; + + auto pushAction = [&](const uint64_t frame, const ScriptedInputEvent::ActionData::Kind kind, const int32_t value) -> void + { + ScriptedInputEvent entry; + entry.frame = frame; + entry.type = ScriptedInputEvent::Type::Action; + entry.action.kind = kind; + entry.action.value = value; + m_scriptedInput.events.emplace_back(std::move(entry)); + }; + + auto pushGoal = [&](const uint64_t frame, const CCameraGoal& goal) -> void + { + ScriptedInputEvent entry; + entry.frame = frame; + entry.type = ScriptedInputEvent::Type::Goal; + entry.goal.goal = goal; + entry.goal.requireExact = true; + m_scriptedInput.events.emplace_back(std::move(entry)); + }; + + auto pushStepCheck = [&](const uint64_t frame, const CCameraSequenceContinuitySettings& continuity) -> void + { + ScriptedInputCheck entry; + entry.frame = frame; + entry.kind = ScriptedInputCheck::Kind::GimbalStep; + if (continuity.hasPosDeltaConstraint) + { + entry.hasPosDeltaConstraint = true; + entry.minPosDelta = continuity.minPosDelta; + entry.posTolerance = continuity.maxPosDelta; + } + if (continuity.hasEulerDeltaConstraint) + { + entry.hasEulerDeltaConstraint = true; + entry.minEulerDeltaDeg = continuity.minEulerDeltaDeg; + entry.eulerToleranceDeg = continuity.maxEulerDeltaDeg; + } + m_scriptedInput.checks.emplace_back(std::move(entry)); + }; + + auto resolvePlanarIx = [&](const CCameraSequenceSegment& segment) -> std::optional + { + 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 useWindow = sequence.defaults.presentations.size() > 1u; + if (!useWindow) + { + for (const auto& segment : sequence.segments) + { + if (getSequenceSegmentPresentations(sequence, segment).size() > 1u) + { + useWindow = true; + break; + } + } + } + pushAction(0u, ScriptedInputEvent::ActionData::Kind::SetUseWindow, useWindow ? 1 : 0); + + const double fps = std::max(1.0, static_cast(sequence.fps)); + uint64_t frameCursor = 0u; + for (const auto& segment : sequence.segments) + { + auto planarIx = resolvePlanarIx(segment); + if (!planarIx.has_value()) + { + const auto kindLabel = segment.cameraKind != ICamera::CameraKind::Unknown ? std::string(getCameraTypeLabel(segment.cameraKind)) : std::string("Unknown"); + 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()); + return false; + } + + const auto presentations = getSequenceSegmentPresentations(sequence, segment); + if (presentations.size() > 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(), presentations.size(), windowBindings.size()); + } + + pushAction(frameCursor, ScriptedInputEvent::ActionData::Kind::SetActiveRenderWindow, 0); + pushAction(frameCursor, ScriptedInputEvent::ActionData::Kind::SetActivePlanar, static_cast(planarIx.value())); + if (!presentations.empty()) + { + pushAction(frameCursor, ScriptedInputEvent::ActionData::Kind::SetProjectionType, static_cast(presentations[0].projection)); + pushAction(frameCursor, ScriptedInputEvent::ActionData::Kind::SetLeftHanded, presentations[0].leftHanded ? 1 : 0); + } + if (getSequenceSegmentResetCamera(sequence, segment)) + pushAction(frameCursor, ScriptedInputEvent::ActionData::Kind::ResetActiveCamera, 1); + + for (size_t windowIx = 1u; windowIx < std::min(presentations.size(), windowBindings.size()); ++windowIx) + { + pushAction(frameCursor, ScriptedInputEvent::ActionData::Kind::SetActiveRenderWindow, static_cast(windowIx)); + pushAction(frameCursor, ScriptedInputEvent::ActionData::Kind::SetActivePlanar, static_cast(planarIx.value())); + pushAction(frameCursor, ScriptedInputEvent::ActionData::Kind::SetProjectionType, static_cast(presentations[windowIx].projection)); + pushAction(frameCursor, ScriptedInputEvent::ActionData::Kind::SetLeftHanded, presentations[windowIx].leftHanded ? 1 : 0); + } + pushAction(frameCursor, ScriptedInputEvent::ActionData::Kind::SetActiveRenderWindow, 0); + + const auto& referencePreset = m_initialPlanarPresets[planarIx.value()]; + CCameraKeyframeTrack track; + std::string trackError; + if (!buildSequenceTrackFromReference(referencePreset, segment, track, &trackError)) + { + logFail("Sequence segment \"%s\" failed to build track: %s", segment.name.c_str(), trackError.c_str()); + return false; + } + + const float durationSeconds = getSequenceSegmentDurationSeconds(sequence, segment, &track); + const uint64_t durationFrames = std::max(1ull, static_cast(std::llround(std::max(0.f, durationSeconds) * static_cast(fps)))); + for (uint64_t frameOffset = 0u; frameOffset < durationFrames; ++frameOffset) + { + const float alpha = durationFrames > 1u ? static_cast(frameOffset) / static_cast(durationFrames - 1u) : 0.f; + const float sampleTime = durationSeconds * alpha; + CCameraPreset preset; + if (!tryBuildKeyframeTrackPresetAtTime(track, sampleTime, preset)) + { + logFail("Sequence segment \"%s\" failed to sample track at t=%f.", segment.name.c_str(), sampleTime); + return false; + } + pushGoal(frameCursor + frameOffset, makeGoalFromPreset(preset)); + } + + const auto continuity = getSequenceSegmentContinuity(sequence, segment); + if (continuity.baseline) + { + ScriptedInputCheck baseline; + baseline.frame = frameCursor; + baseline.kind = ScriptedInputCheck::Kind::Baseline; + m_scriptedInput.checks.emplace_back(std::move(baseline)); + } + if (continuity.step) + { + for (uint64_t frameOffset = 1u; frameOffset < durationFrames; ++frameOffset) + pushStepCheck(frameCursor + frameOffset, continuity); + } + + for (const auto fraction : getSequenceSegmentCaptureFractions(sequence, segment)) + { + const auto offset = durationFrames > 1u ? + static_cast(std::llround(static_cast(fraction) * static_cast(durationFrames - 1u))) : + 0u; + m_scriptedInput.captureFrames.emplace_back(frameCursor + offset); + } + + frameCursor += durationFrames; + } + + finalizeScriptedInput(); + return true; + }; + + if (!expandCameraSequenceScript(*pendingScriptedSequence)) + return false; + } } // Create asset manager diff --git a/61_UI/AppUpdate.cpp b/61_UI/AppUpdate.cpp index ba4ba5439..3b183c4b4 100644 --- a/61_UI/AppUpdate.cpp +++ b/61_UI/AppUpdate.cpp @@ -60,6 +60,7 @@ void App::update() std::vector scriptedKeyboard; std::vector scriptedImguizmo; std::vector scriptedActions; + std::vector scriptedGoals; const CVirtualGimbalEvent* scriptedImguizmoVirtual = nullptr; uint32_t scriptedImguizmoVirtualCount = 0u; @@ -111,6 +112,10 @@ void App::update() { scriptedActions.emplace_back(ev.action); } + else if (ev.type == ScriptedInputEvent::Type::Goal) + { + scriptedGoals.emplace_back(ev.goal); + } ++m_scriptedInput.nextEventIndex; } @@ -301,13 +306,14 @@ void App::update() .keyboardEvents = { capturedEvents.keyboard.data(), capturedEvents.keyboard.size() } }; - if (m_scriptedInput.log && (scriptedKeyboard.size() || scriptedMouse.size() || scriptedImguizmo.size())) + if (m_scriptedInput.log && (scriptedKeyboard.size() || scriptedMouse.size() || scriptedImguizmo.size() || scriptedGoals.size())) { - m_logger->log("[script] frame %llu input kb=%zu mouse=%zu imguizmo=%zu", ILogger::ELL_INFO, + m_logger->log("[script] frame %llu input kb=%zu mouse=%zu imguizmo=%zu goals=%zu", ILogger::ELL_INFO, static_cast(m_realFrameIx), scriptedKeyboard.size(), scriptedMouse.size(), - scriptedImguizmo.size()); + scriptedImguizmo.size(), + scriptedGoals.size()); } if (enableActiveCameraMovement && !skipCameraInput) @@ -455,6 +461,48 @@ void App::update() scriptedImguizmoVirtualCount = vCount; } + if (m_scriptedInput.enabled && scriptedGoals.size() && !skipCameraInput) + { + auto& binding = windowBindings[activeRenderWindowIx]; + auto& planar = m_planarProjections[binding.activePlanarIx]; + auto* camera = planar->getCamera(); + + 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 : scriptedGoals) + { + 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, + describeApplyResult(result).c_str()); + continue; + } + } + + if (camera) + { + for (auto& projection : planar->getPlanarProjections()) + nbl::hlsl::syncDynamicPerspectiveProjection(camera, projection); + } + + if (m_scriptedInput.log && camera) + { + const auto& gimbal = camera->getGimbal(); + const auto pos = gimbal.getPosition(); + const auto euler = glm::degrees(glm::eulerAngles(gimbal.getOrientation())); + m_logger->log("[script] goal_apply gimbal pos=(%.3f, %.3f, %.3f) euler_deg=(%.3f, %.3f, %.3f)", ILogger::ELL_INFO, + pos.x, pos.y, pos.z, euler.x, euler.y, euler.z); + } + } + if (m_scriptedInput.enabled && m_scriptedInput.nextCheckIndex < m_scriptedInput.checks.size()) { auto* camera = [&]() -> ICamera* @@ -654,24 +702,42 @@ void App::update() const auto dmax = std::max(dx, std::max(dy, dz)); bool ok = true; + bool requiresProgress = false; + bool hasProgress = false; if (check.hasPosDeltaConstraint) { - if (dpos < check.minPosDelta || dpos > check.posTolerance) + if (dpos > check.posTolerance) { ok = false; - logFail("[script][fail] gimbal_step frame=%llu pos_delta=%.6f expected=[%.6f, %.6f]", - static_cast(frame), dpos, check.minPosDelta, check.posTolerance); + logFail("[script][fail] gimbal_step frame=%llu pos_delta=%.6f max=%.6f", + static_cast(frame), dpos, check.posTolerance); + } + if (check.minPosDelta > 0.0f) + { + requiresProgress = true; + hasProgress = hasProgress || dpos >= check.minPosDelta; } } if (check.hasEulerDeltaConstraint) { - if (dmax < check.minEulerDeltaDeg || dmax > check.eulerToleranceDeg) + if (dmax > check.eulerToleranceDeg) { ok = false; - logFail("[script][fail] gimbal_step frame=%llu euler_delta=%.6f expected=[%.6f, %.6f]", - static_cast(frame), dmax, check.minEulerDeltaDeg, check.eulerToleranceDeg); + logFail("[script][fail] gimbal_step frame=%llu euler_delta=%.6f max=%.6f", + static_cast(frame), dmax, check.eulerToleranceDeg); + } + if (check.minEulerDeltaDeg > 0.0f) + { + requiresProgress = true; + hasProgress = hasProgress || dmax >= check.minEulerDeltaDeg; } } + if (requiresProgress && !hasProgress) + { + ok = false; + logFail("[script][fail] gimbal_step frame=%llu missing progress pos_delta=%.6f euler_delta=%.6f", + static_cast(frame), dpos, dmax); + } if (ok) logPass("[script][pass] gimbal_step frame=%llu pos_delta=%.6f euler_delta=%.6f", diff --git a/61_UI/README.md b/61_UI/README.md index e27bb91ed..4acb83282 100644 --- a/61_UI/README.md +++ b/61_UI/README.md @@ -83,13 +83,17 @@ FAIL means missing movement, out-of-range movement, invalid state, or missing re Goal: verify smooth frame-to-frame behavior (no visible teleport-like jumps). -Per camera sequence: +The continuity asset is now a compact authored camera-sequence spec, not a committed frame dump. +`61_UI` expands that shared camera-domain description into its own runtime scripted checks. + +Per authored segment: 1. select planar 2. store `baseline` -3. hold camera segment for `3.0 s` (`180` frames at `60 FPS`) -4. apply 3 small `imguizmo` movement steps spread over the segment -5. run `gimbal_step` after each step +3. build a reusable keyframe track from the active camera reference preset +4. sample that track for `4.0 s` at `60 FPS` +5. run `gimbal_step` on each generated frame step +6. capture selected milestones such as `end` PASS means every step delta stayed inside configured continuity ranges. FAIL means any step exceeded max range or failed minimum expected motion. @@ -98,6 +102,7 @@ Continuity also supports visual debug mode: - large top-center overlay with active camera type and segment progress - fixed frame pacing (`visual_debug_target_fps`) so camera time is human-readable +- compact authored JSON that stays in camera-domain and is reusable outside `61_UI` ## Build and run @@ -121,7 +126,8 @@ For CI-style exit with automatic screenshot/capture behavior: Notes: -- continuity visual run takes about `33 s` (`11` cameras × `3 s`) +- continuity visual run takes about `47 s` +- the authored continuity JSON is compact and segment-based rather than frame-by-frame - if `visual_debug` is present in json, CLI flag is optional ## CTest entries diff --git a/61_UI/app_resources/cameraz_continuity.json b/61_UI/app_resources/cameraz_continuity.json index 4922d771c..bf11cbb5f 100644 --- a/61_UI/app_resources/cameraz_continuity.json +++ b/61_UI/app_resources/cameraz_continuity.json @@ -8,66459 +8,287 @@ "visual_debug_target_fps": 60, "visual_debug_hold_seconds": 4, "capture_prefix": "camera_continuity", - "capture_frames": [ - 239, - 479, - 719, - 959, - 1199, - 1439, - 1679, - 1919, - 2159, - 2220, - 2280, - 2340, - 2399, - 2639 - ], - "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": "reset_active_camera", - "value": 1 - }, - { - "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": [ - 0.008, - 0, - 0.018 - ], - "rotation_deg": [ - 0.0, - 4.739231, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2, - "type": "imguizmo", - "translation": [ - 0.008079, - 7.9E-05, - 0.017999 - ], - "rotation_deg": [ - 0.051459, - 4.791816, - 0.237572 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 3, - "type": "imguizmo", - "translation": [ - 0.008158, - 0.000158, - 0.017994 - ], - "rotation_deg": [ - 0.102794, - 4.844197, - 0.474978 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 4, - "type": "imguizmo", - "translation": [ - 0.008237, - 0.000237, - 0.017987 - ], - "rotation_deg": [ - 0.153881, - 4.89634, - 0.712053 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 5, - "type": "imguizmo", - "translation": [ - 0.008316, - 0.000314, - 0.017978 - ], - "rotation_deg": [ - 0.204597, - 4.948204, - 0.948632 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 6, - "type": "imguizmo", - "translation": [ - 0.008395, - 0.000391, - 0.017965 - ], - "rotation_deg": [ - 0.254821, - 4.999758, - 1.18455 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 7, - "type": "imguizmo", - "translation": [ - 0.008473, - 0.000467, - 0.01795 - ], - "rotation_deg": [ - 0.304429, - 5.050962, - 1.419643 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 8, - "type": "imguizmo", - "translation": [ - 0.008551, - 0.000542, - 0.017932 - ], - "rotation_deg": [ - 0.353305, - 5.101783, - 1.653746 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 9, - "type": "imguizmo", - "translation": [ - 0.008629, - 0.000615, - 0.017911 - ], - "rotation_deg": [ - 0.401332, - 5.152184, - 1.886696 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 10, - "type": "imguizmo", - "translation": [ - 0.008706, - 0.000686, - 0.017888 - ], - "rotation_deg": [ - 0.448392, - 5.202131, - 2.118332 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 11, - "type": "imguizmo", - "translation": [ - 0.008783, - 0.000756, - 0.017861 - ], - "rotation_deg": [ - 0.494375, - 5.251589, - 2.348491 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 12, - "type": "imguizmo", - "translation": [ - 0.008859, - 0.000823, - 0.017833 - ], - "rotation_deg": [ - 0.53917, - 5.300523, - 2.577014 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 13, - "type": "imguizmo", - "translation": [ - 0.008935, - 0.000888, - 0.017801 - ], - "rotation_deg": [ - 0.582673, - 5.348899, - 2.80374 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 14, - "type": "imguizmo", - "translation": [ - 0.00901, - 0.000951, - 0.017767 - ], - "rotation_deg": [ - 0.62478, - 5.396683, - 3.028513 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 15, - "type": "imguizmo", - "translation": [ - 0.009084, - 0.001011, - 0.01773 - ], - "rotation_deg": [ - 0.665392, - 5.443842, - 3.251175 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 16, - "type": "imguizmo", - "translation": [ - 0.009157, - 0.001068, - 0.01769 - ], - "rotation_deg": [ - 0.704413, - 5.490344, - 3.471571 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 17, - "type": "imguizmo", - "translation": [ - 0.00923, - 0.001122, - 0.017648 - ], - "rotation_deg": [ - 0.741752, - 5.536156, - 3.689548 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 18, - "type": "imguizmo", - "translation": [ - 0.009302, - 0.001173, - 0.017604 - ], - "rotation_deg": [ - 0.777324, - 5.581245, - 3.904954 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 19, - "type": "imguizmo", - "translation": [ - 0.009373, - 0.00122, - 0.017557 - ], - "rotation_deg": [ - 0.811047, - 5.62558, - 4.117638 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 20, - "type": "imguizmo", - "translation": [ - 0.009442, - 0.001265, - 0.017507 - ], - "rotation_deg": [ - 0.842846, - 5.669131, - 4.327452 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 21, - "type": "imguizmo", - "translation": [ - 0.009511, - 0.001306, - 0.017455 - ], - "rotation_deg": [ - 0.872645, - 5.711868, - 4.534251 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 22, - "type": "imguizmo", - "translation": [ - 0.009579, - 0.001343, - 0.017401 - ], - "rotation_deg": [ - 0.900382, - 5.75376, - 4.737889 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 23, - "type": "imguizmo", - "translation": [ - 0.009646, - 0.001376, - 0.017344 - ], - "rotation_deg": [ - 0.925994, - 5.794778, - 4.938226 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 24, - "type": "imguizmo", - "translation": [ - 0.009712, - 0.001406, - 0.017285 - ], - "rotation_deg": [ - 0.949426, - 5.834894, - 5.135121 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 25, - "type": "imguizmo", - "translation": [ - 0.009776, - 0.001431, - 0.017224 - ], - "rotation_deg": [ - 0.97063, - 5.874079, - 5.328438 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 26, - "type": "imguizmo", - "translation": [ - 0.009839, - 0.001453, - 0.01716 - ], - "rotation_deg": [ - 0.989561, - 5.912307, - 5.518041 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 27, - "type": "imguizmo", - "translation": [ - 0.009901, - 0.001471, - 0.017094 - ], - "rotation_deg": [ - 1.006184, - 5.949551, - 5.703798 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 28, - "type": "imguizmo", - "translation": [ - 0.009962, - 0.001484, - 0.017026 - ], - "rotation_deg": [ - 1.020466, - 5.985784, - 5.88558 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 29, - "type": "imguizmo", - "translation": [ - 0.010021, - 0.001494, - 0.016956 - ], - "rotation_deg": [ - 1.03238, - 6.020982, - 6.063261 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 30, - "type": "imguizmo", - "translation": [ - 0.010079, - 0.001499, - 0.016884 - ], - "rotation_deg": [ - 1.041912, - 6.055121, - 6.236716 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 31, - "type": "imguizmo", - "translation": [ - 0.010135, - 0.0015, - 0.01681 - ], - "rotation_deg": [ - 1.049045, - 6.088174, - 6.405824 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 32, - "type": "imguizmo", - "translation": [ - 0.01019, - 0.001497, - 0.016734 - ], - "rotation_deg": [ - 1.053776, - 6.120122, - 6.570468 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 33, - "type": "imguizmo", - "translation": [ - 0.010244, - 0.001489, - 0.016656 - ], - "rotation_deg": [ - 1.056104, - 6.15094, - 6.730533 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 34, - "type": "imguizmo", - "translation": [ - 0.010295, - 0.001478, - 0.016576 - ], - "rotation_deg": [ - 1.056035, - 6.180607, - 6.885908 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 35, - "type": "imguizmo", - "translation": [ - 0.010345, - 0.001462, - 0.016494 - ], - "rotation_deg": [ - 1.053583, - 6.209104, - 7.036483 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 36, - "type": "imguizmo", - "translation": [ - 0.010394, - 0.001443, - 0.016411 - ], - "rotation_deg": [ - 1.048767, - 6.23641, - 7.182155 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 37, - "type": "imguizmo", - "translation": [ - 0.010441, - 0.001419, - 0.016325 - ], - "rotation_deg": [ - 1.04161, - 6.262504, - 7.322821 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 38, - "type": "imguizmo", - "translation": [ - 0.010486, - 0.001391, - 0.016239 - ], - "rotation_deg": [ - 1.032146, - 6.287372, - 7.458384 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 39, - "type": "imguizmo", - "translation": [ - 0.01053, - 0.00136, - 0.01615 - ], - "rotation_deg": [ - 1.020412, - 6.310993, - 7.588749 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 40, - "type": "imguizmo", - "translation": [ - 0.010571, - 0.001325, - 0.016061 - ], - "rotation_deg": [ - 1.00645, - 6.333352, - 7.713826 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 41, - "type": "imguizmo", - "translation": [ - 0.010611, - 0.001286, - 0.015969 - ], - "rotation_deg": [ - 0.99031, - 6.354434, - 7.833526 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 42, - "type": "imguizmo", - "translation": [ - 0.010649, - 0.001243, - 0.015877 - ], - "rotation_deg": [ - 0.972047, - 6.374223, - 7.947767 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 43, - "type": "imguizmo", - "translation": [ - 0.010685, - 0.001197, - 0.015783 - ], - "rotation_deg": [ - 0.951721, - 6.392705, - 8.05647 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 44, - "type": "imguizmo", - "translation": [ - 0.01072, - 0.001148, - 0.015688 - ], - "rotation_deg": [ - 0.929397, - 6.409869, - 8.159557 - ], - "scale": [ - 1, - 1, - 1 + "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 + } + } + ] + }, + { + "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 + } + } + ] + }, + { + "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 + } + } + ] + }, + { + "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 + } + } + ] + }, + { + "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 + } + } + ] + }, + { + "name": "chase_arc", + "camera_kind": "Chase", + "keyframes": [ + { + "time": 0.0 + }, + { + "time": 4.0, + "delta": { + "target_offset": [ + 1.4, + -1.8, + 0.6 + ], + "orbit_u_delta_deg": -44.0, + "orbit_distance_delta": -1.4 + } + } + ] + }, + { + "name": "dolly_slide", + "camera_kind": "Dolly", + "keyframes": [ + { + "time": 0.0 + }, + { + "time": 4.0, + "delta": { + "target_offset": [ + 2.6, + 0.8, + -0.4 + ], + "orbit_distance_delta": -2.8 + } + } + ] + }, + { + "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 + } + } + ] + }, + { + "name": "path_arc", + "camera_kind": "Path", + "keyframes": [ + { + "time": 0.0 + }, + { + "time": 1.5, + "delta": { + "path_angle_delta_deg": 42.0, + "path_radius_delta": -3.5, + "path_height_delta": 1.0 + } + }, + { + "time": 4.0, + "delta": { + "path_angle_delta_deg": 115.0, + "path_radius_delta": -8.0, + "path_height_delta": 2.2 + } + } ] - }, - { - "frame": 45, - "type": "imguizmo", - "translation": [ - 0.010752, - 0.001095, - 0.015591 - ], - "rotation_deg": [ - 0.905146, - 6.425702, - 8.256958 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 46, - "type": "imguizmo", - "translation": [ - 0.010783, - 0.001039, - 0.015494 - ], - "rotation_deg": [ - 0.879043, - 6.440193, - 8.348605 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 47, - "type": "imguizmo", - "translation": [ - 0.010811, - 0.000981, - 0.015396 - ], - "rotation_deg": [ - 0.851169, - 6.453331, - 8.434433 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 48, - "type": "imguizmo", - "translation": [ - 0.010838, - 0.00092, - 0.015296 - ], - "rotation_deg": [ - 0.821608, - 6.465109, - 8.514383 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 49, - "type": "imguizmo", - "translation": [ - 0.010863, - 0.000856, - 0.015196 - ], - "rotation_deg": [ - 0.790448, - 6.475516, - 8.5884 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 50, - "type": "imguizmo", - "translation": [ - 0.010885, - 0.00079, - 0.015095 - ], - "rotation_deg": [ - 0.757785, - 6.484547, - 8.656431 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 51, - "type": "imguizmo", - "translation": [ - 0.010906, - 0.000721, - 0.014993 - ], - "rotation_deg": [ - 0.723712, - 6.492196, - 8.718429 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 52, - "type": "imguizmo", - "translation": [ - 0.010925, - 0.000651, - 0.01489 - ], - "rotation_deg": [ - 0.688332, - 6.498455, - 8.774351 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 53, - "type": "imguizmo", - "translation": [ - 0.010941, - 0.000579, - 0.014787 - ], - "rotation_deg": [ - 0.651746, - 6.503322, - 8.824158 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 54, - "type": "imguizmo", - "translation": [ - 0.010956, - 0.000505, - 0.014683 - ], - "rotation_deg": [ - 0.614061, - 6.506792, - 8.867816 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 55, - "type": "imguizmo", - "translation": [ - 0.010968, - 0.00043, - 0.014579 - ], - "rotation_deg": [ - 0.575385, - 6.508865, - 8.905293 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 56, - "type": "imguizmo", - "translation": [ - 0.010979, - 0.000353, - 0.014474 - ], - "rotation_deg": [ - 0.535829, - 6.509536, - 8.936564 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 57, - "type": "imguizmo", - "translation": [ - 0.010987, - 0.000276, - 0.014369 - ], - "rotation_deg": [ - 0.495507, - 6.508808, - 8.961608 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 58, - "type": "imguizmo", - "translation": [ - 0.010993, - 0.000197, - 0.014264 - ], - "rotation_deg": [ - 0.454531, - 6.506679, - 8.980405 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 59, - "type": "imguizmo", - "translation": [ - 0.010998, - 0.000119, - 0.014158 - ], - "rotation_deg": [ - 0.41302, - 6.503153, - 8.992944 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 60, - "type": "imguizmo", - "translation": [ - 0.011, - 4E-05, - 0.014053 - ], - "rotation_deg": [ - 0.371087, - 6.49823, - 8.999216 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 61, - "type": "imguizmo", - "translation": [ - 0.011, - -4E-05, - 0.013947 - ], - "rotation_deg": [ - 0.328853, - 6.491914, - 8.999216 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 62, - "type": "imguizmo", - "translation": [ - 0.010998, - -0.000119, - 0.013842 - ], - "rotation_deg": [ - 0.286432, - 6.484211, - 8.992944 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 63, - "type": "imguizmo", - "translation": [ - 0.010993, - -0.000197, - 0.013736 - ], - "rotation_deg": [ - 0.243945, - 6.475124, - 8.980405 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 64, - "type": "imguizmo", - "translation": [ - 0.010987, - -0.000276, - 0.013631 - ], - "rotation_deg": [ - 0.201507, - 6.464661, - 8.961608 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 65, - "type": "imguizmo", - "translation": [ - 0.010979, - -0.000353, - 0.013526 - ], - "rotation_deg": [ - 0.159237, - 6.452828, - 8.936564 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 66, - "type": "imguizmo", - "translation": [ - 0.010968, - -0.00043, - 0.013421 - ], - "rotation_deg": [ - 0.117249, - 6.439636, - 8.905293 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 67, - "type": "imguizmo", - "translation": [ - 0.010956, - -0.000505, - 0.013317 - ], - "rotation_deg": [ - 0.075659, - 6.42509, - 8.867816 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 68, - "type": "imguizmo", - "translation": [ - 0.010941, - -0.000579, - 0.013213 - ], - "rotation_deg": [ - 0.034578, - 6.409204, - 8.824158 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 69, - "type": "imguizmo", - "translation": [ - 0.010925, - -0.000651, - 0.01311 - ], - "rotation_deg": [ - -0.005882, - 6.391987, - 8.774351 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 70, - "type": "imguizmo", - "translation": [ - 0.010906, - -0.000721, - 0.013007 - ], - "rotation_deg": [ - -0.045612, - 6.373452, - 8.718429 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 71, - "type": "imguizmo", - "translation": [ - 0.010885, - -0.00079, - 0.012905 - ], - "rotation_deg": [ - -0.084507, - 6.35361, - 8.656431 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 72, - "type": "imguizmo", - "translation": [ - 0.010863, - -0.000856, - 0.012804 - ], - "rotation_deg": [ - -0.122462, - 6.332477, - 8.5884 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 73, - "type": "imguizmo", - "translation": [ - 0.010838, - -0.00092, - 0.012704 - ], - "rotation_deg": [ - -0.159378, - 6.310067, - 8.514383 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 74, - "type": "imguizmo", - "translation": [ - 0.010811, - -0.000981, - 0.012604 - ], - "rotation_deg": [ - -0.195157, - 6.286395, - 8.434433 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 75, - "type": "imguizmo", - "translation": [ - 0.010783, - -0.001039, - 0.012506 - ], - "rotation_deg": [ - -0.229707, - 6.261478, - 8.348605 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 76, - "type": "imguizmo", - "translation": [ - 0.010752, - -0.001095, - 0.012409 - ], - "rotation_deg": [ - -0.262938, - 6.235334, - 8.256958 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 77, - "type": "imguizmo", - "translation": [ - 0.01072, - -0.001148, - 0.012312 - ], - "rotation_deg": [ - -0.294765, - 6.20798, - 8.159557 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 78, - "type": "imguizmo", - "translation": [ - 0.010685, - -0.001197, - 0.012217 - ], - "rotation_deg": [ - -0.325107, - 6.179437, - 8.05647 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 79, - "type": "imguizmo", - "translation": [ - 0.010649, - -0.001243, - 0.012123 - ], - "rotation_deg": [ - -0.353887, - 6.149722, - 7.947767 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 80, - "type": "imguizmo", - "translation": [ - 0.010611, - -0.001286, - 0.012031 - ], - "rotation_deg": [ - -0.381036, - 6.118858, - 7.833526 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 81, - "type": "imguizmo", - "translation": [ - 0.010571, - -0.001325, - 0.011939 - ], - "rotation_deg": [ - -0.406486, - 6.086866, - 7.713826 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 82, - "type": "imguizmo", - "translation": [ - 0.01053, - -0.00136, - 0.01185 - ], - "rotation_deg": [ - -0.430176, - 6.053768, - 7.588749 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 83, - "type": "imguizmo", - "translation": [ - 0.010486, - -0.001391, - 0.011761 - ], - "rotation_deg": [ - -0.45205, - 6.019586, - 7.458384 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 84, - "type": "imguizmo", - "translation": [ - 0.010441, - -0.001419, - 0.011675 - ], - "rotation_deg": [ - -0.472058, - 5.984346, - 7.322821 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 85, - "type": "imguizmo", - "translation": [ - 0.010394, - -0.001443, - 0.011589 - ], - "rotation_deg": [ - -0.490155, - 5.948072, - 7.182155 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 86, - "type": "imguizmo", - "translation": [ - 0.010345, - -0.001462, - 0.011506 - ], - "rotation_deg": [ - -0.506301, - 5.910788, - 7.036483 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 87, - "type": "imguizmo", - "translation": [ - 0.010295, - -0.001478, - 0.011424 - ], - "rotation_deg": [ - -0.520465, - 5.872521, - 6.885908 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 88, - "type": "imguizmo", - "translation": [ - 0.010244, - -0.001489, - 0.011344 - ], - "rotation_deg": [ - -0.532618, - 5.833298, - 6.730533 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 89, - "type": "imguizmo", - "translation": [ - 0.01019, - -0.001497, - 0.011266 - ], - "rotation_deg": [ - -0.54274, - 5.793145, - 6.570468 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 90, - "type": "imguizmo", - "translation": [ - 0.010135, - -0.0015, - 0.01119 - ], - "rotation_deg": [ - -0.550815, - 5.752091, - 6.405824 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 91, - "type": "imguizmo", - "translation": [ - 0.010079, - -0.001499, - 0.011116 - ], - "rotation_deg": [ - -0.556834, - 5.710165, - 6.236716 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 92, - "type": "imguizmo", - "translation": [ - 0.010021, - -0.001494, - 0.011044 - ], - "rotation_deg": [ - -0.560794, - 5.667394, - 6.063261 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 93, - "type": "imguizmo", - "translation": [ - 0.009962, - -0.001484, - 0.010974 - ], - "rotation_deg": [ - -0.562698, - 5.623811, - 5.88558 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 94, - "type": "imguizmo", - "translation": [ - 0.009901, - -0.001471, - 0.010906 - ], - "rotation_deg": [ - -0.562556, - 5.579444, - 5.703798 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 95, - "type": "imguizmo", - "translation": [ - 0.009839, - -0.001453, - 0.01084 - ], - "rotation_deg": [ - -0.560381, - 5.534325, - 5.518041 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 96, - "type": "imguizmo", - "translation": [ - 0.009776, - -0.001431, - 0.010776 - ], - "rotation_deg": [ - -0.556196, - 5.488486, - 5.328438 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 97, - "type": "imguizmo", - "translation": [ - 0.009712, - -0.001406, - 0.010715 - ], - "rotation_deg": [ - -0.550028, - 5.441958, - 5.135121 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 98, - "type": "imguizmo", - "translation": [ - 0.009646, - -0.001376, - 0.010656 - ], - "rotation_deg": [ - -0.54191, - 5.394772, - 4.938226 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 99, - "type": "imguizmo", - "translation": [ - 0.009579, - -0.001343, - 0.010599 - ], - "rotation_deg": [ - -0.53188, - 5.346963, - 4.737889 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 100, - "type": "imguizmo", - "translation": [ - 0.009511, - -0.001306, - 0.010545 - ], - "rotation_deg": [ - -0.519981, - 5.298564, - 4.534251 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 101, - "type": "imguizmo", - "translation": [ - 0.009442, - -0.001265, - 0.010493 - ], - "rotation_deg": [ - -0.506266, - 5.249609, - 4.327452 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 102, - "type": "imguizmo", - "translation": [ - 0.009373, - -0.00122, - 0.010443 - ], - "rotation_deg": [ - -0.490787, - 5.200131, - 4.117638 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 103, - "type": "imguizmo", - "translation": [ - 0.009302, - -0.001173, - 0.010396 - ], - "rotation_deg": [ - -0.473606, - 5.150165, - 3.904954 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 104, - "type": "imguizmo", - "translation": [ - 0.00923, - -0.001122, - 0.010352 - ], - "rotation_deg": [ - -0.454788, - 5.099746, - 3.689548 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 105, - "type": "imguizmo", - "translation": [ - 0.009157, - -0.001068, - 0.01031 - ], - "rotation_deg": [ - -0.434401, - 5.048909, - 3.471571 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 106, - "type": "imguizmo", - "translation": [ - 0.009084, - -0.001011, - 0.01027 - ], - "rotation_deg": [ - -0.412522, - 4.997689, - 3.251175 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 107, - "type": "imguizmo", - "translation": [ - 0.00901, - -0.000951, - 0.010233 - ], - "rotation_deg": [ - -0.389228, - 4.946123, - 3.028513 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 108, - "type": "imguizmo", - "translation": [ - 0.008935, - -0.000888, - 0.010199 - ], - "rotation_deg": [ - -0.364605, - 4.894246, - 2.80374 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 109, - "type": "imguizmo", - "translation": [ - 0.008859, - -0.000823, - 0.010167 - ], - "rotation_deg": [ - -0.338736, - 4.842094, - 2.577014 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 110, - "type": "imguizmo", - "translation": [ - 0.008783, - -0.000756, - 0.010139 - ], - "rotation_deg": [ - -0.311715, - 4.789704, - 2.348491 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 111, - "type": "imguizmo", - "translation": [ - 0.008706, - -0.000686, - 0.010112 - ], - "rotation_deg": [ - -0.283632, - 4.737111, - 2.118332 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 112, - "type": "imguizmo", - "translation": [ - 0.008629, - -0.000615, - 0.010089 - ], - "rotation_deg": [ - -0.254588, - 4.684353, - 1.886696 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 113, - "type": "imguizmo", - "translation": [ - 0.008551, - -0.000542, - 0.010068 - ], - "rotation_deg": [ - -0.224681, - 4.631468, - 1.653746 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 114, - "type": "imguizmo", - "translation": [ - 0.008473, - -0.000467, - 0.01005 - ], - "rotation_deg": [ - -0.194013, - 4.57849, - 1.419643 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 115, - "type": "imguizmo", - "translation": [ - 0.008395, - -0.000391, - 0.010035 - ], - "rotation_deg": [ - -0.162689, - 4.525458, - 1.18455 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 116, - "type": "imguizmo", - "translation": [ - 0.008316, - -0.000314, - 0.010022 - ], - "rotation_deg": [ - -0.130815, - 4.472407, - 0.948632 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 117, - "type": "imguizmo", - "translation": [ - 0.008237, - -0.000237, - 0.010013 - ], - "rotation_deg": [ - -0.098499, - 4.419377, - 0.712053 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 118, - "type": "imguizmo", - "translation": [ - 0.008158, - -0.000158, - 0.010006 - ], - "rotation_deg": [ - -0.065852, - 4.366402, - 0.474978 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 119, - "type": "imguizmo", - "translation": [ - 0.008079, - -7.9E-05, - 0.010001 - ], - "rotation_deg": [ - -0.032981, - 4.31352, - 0.237572 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 120, - "type": "imguizmo", - "translation": [ - 0.008, - 0, - 0.01 - ], - "rotation_deg": [ - 0.0, - 4.260769, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 121, - "type": "imguizmo", - "translation": [ - 0.007921, - 7.9E-05, - 0.010001 - ], - "rotation_deg": [ - 0.032981, - 4.208184, - -0.237572 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 122, - "type": "imguizmo", - "translation": [ - 0.007842, - 0.000158, - 0.010006 - ], - "rotation_deg": [ - 0.065852, - 4.155803, - -0.474978 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 123, - "type": "imguizmo", - "translation": [ - 0.007763, - 0.000237, - 0.010013 - ], - "rotation_deg": [ - 0.098499, - 4.10366, - -0.712053 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 124, - "type": "imguizmo", - "translation": [ - 0.007684, - 0.000314, - 0.010022 - ], - "rotation_deg": [ - 0.130815, - 4.051796, - -0.948632 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 125, - "type": "imguizmo", - "translation": [ - 0.007605, - 0.000391, - 0.010035 - ], - "rotation_deg": [ - 0.162689, - 4.000242, - -1.18455 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 126, - "type": "imguizmo", - "translation": [ - 0.007527, - 0.000467, - 0.01005 - ], - "rotation_deg": [ - 0.194013, - 3.949038, - -1.419643 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 127, - "type": "imguizmo", - "translation": [ - 0.007449, - 0.000542, - 0.010068 - ], - "rotation_deg": [ - 0.224681, - 3.898217, - -1.653746 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 128, - "type": "imguizmo", - "translation": [ - 0.007371, - 0.000615, - 0.010089 - ], - "rotation_deg": [ - 0.254588, - 3.847816, - -1.886696 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 129, - "type": "imguizmo", - "translation": [ - 0.007294, - 0.000686, - 0.010112 - ], - "rotation_deg": [ - 0.283632, - 3.797869, - -2.118332 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 130, - "type": "imguizmo", - "translation": [ - 0.007217, - 0.000756, - 0.010139 - ], - "rotation_deg": [ - 0.311715, - 3.748411, - -2.348491 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 131, - "type": "imguizmo", - "translation": [ - 0.007141, - 0.000823, - 0.010167 - ], - "rotation_deg": [ - 0.338736, - 3.699477, - -2.577014 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 132, - "type": "imguizmo", - "translation": [ - 0.007065, - 0.000888, - 0.010199 - ], - "rotation_deg": [ - 0.364605, - 3.651101, - -2.80374 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 133, - "type": "imguizmo", - "translation": [ - 0.00699, - 0.000951, - 0.010233 - ], - "rotation_deg": [ - 0.389228, - 3.603317, - -3.028513 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 134, - "type": "imguizmo", - "translation": [ - 0.006916, - 0.001011, - 0.01027 - ], - "rotation_deg": [ - 0.412522, - 3.556158, - -3.251175 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 135, - "type": "imguizmo", - "translation": [ - 0.006843, - 0.001068, - 0.01031 - ], - "rotation_deg": [ - 0.434401, - 3.509656, - -3.471571 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 136, - "type": "imguizmo", - "translation": [ - 0.00677, - 0.001122, - 0.010352 - ], - "rotation_deg": [ - 0.454788, - 3.463844, - -3.689548 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 137, - "type": "imguizmo", - "translation": [ - 0.006698, - 0.001173, - 0.010396 - ], - "rotation_deg": [ - 0.473606, - 3.418755, - -3.904954 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 138, - "type": "imguizmo", - "translation": [ - 0.006627, - 0.00122, - 0.010443 - ], - "rotation_deg": [ - 0.490787, - 3.37442, - -4.117638 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 139, - "type": "imguizmo", - "translation": [ - 0.006558, - 0.001265, - 0.010493 - ], - "rotation_deg": [ - 0.506266, - 3.330869, - -4.327452 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 140, - "type": "imguizmo", - "translation": [ - 0.006489, - 0.001306, - 0.010545 - ], - "rotation_deg": [ - 0.519981, - 3.288132, - -4.534251 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 141, - "type": "imguizmo", - "translation": [ - 0.006421, - 0.001343, - 0.010599 - ], - "rotation_deg": [ - 0.53188, - 3.24624, - -4.737889 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 142, - "type": "imguizmo", - "translation": [ - 0.006354, - 0.001376, - 0.010656 - ], - "rotation_deg": [ - 0.54191, - 3.205222, - -4.938226 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 143, - "type": "imguizmo", - "translation": [ - 0.006288, - 0.001406, - 0.010715 - ], - "rotation_deg": [ - 0.550028, - 3.165106, - -5.135121 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 144, - "type": "imguizmo", - "translation": [ - 0.006224, - 0.001431, - 0.010776 - ], - "rotation_deg": [ - 0.556196, - 3.125921, - -5.328438 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 145, - "type": "imguizmo", - "translation": [ - 0.006161, - 0.001453, - 0.01084 - ], - "rotation_deg": [ - 0.560381, - 3.087693, - -5.518041 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 146, - "type": "imguizmo", - "translation": [ - 0.006099, - 0.001471, - 0.010906 - ], - "rotation_deg": [ - 0.562556, - 3.050449, - -5.703798 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 147, - "type": "imguizmo", - "translation": [ - 0.006038, - 0.001484, - 0.010974 - ], - "rotation_deg": [ - 0.562698, - 3.014216, - -5.88558 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 148, - "type": "imguizmo", - "translation": [ - 0.005979, - 0.001494, - 0.011044 - ], - "rotation_deg": [ - 0.560794, - 2.979018, - -6.063261 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 149, - "type": "imguizmo", - "translation": [ - 0.005921, - 0.001499, - 0.011116 - ], - "rotation_deg": [ - 0.556834, - 2.944879, - -6.236716 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 150, - "type": "imguizmo", - "translation": [ - 0.005865, - 0.0015, - 0.01119 - ], - "rotation_deg": [ - 0.550815, - 2.911826, - -6.405824 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 151, - "type": "imguizmo", - "translation": [ - 0.00581, - 0.001497, - 0.011266 - ], - "rotation_deg": [ - 0.54274, - 2.879878, - -6.570468 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 152, - "type": "imguizmo", - "translation": [ - 0.005756, - 0.001489, - 0.011344 - ], - "rotation_deg": [ - 0.532618, - 2.84906, - -6.730533 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 153, - "type": "imguizmo", - "translation": [ - 0.005705, - 0.001478, - 0.011424 - ], - "rotation_deg": [ - 0.520465, - 2.819393, - -6.885908 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 154, - "type": "imguizmo", - "translation": [ - 0.005655, - 0.001462, - 0.011506 - ], - "rotation_deg": [ - 0.506301, - 2.790896, - -7.036483 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 155, - "type": "imguizmo", - "translation": [ - 0.005606, - 0.001443, - 0.011589 - ], - "rotation_deg": [ - 0.490155, - 2.76359, - -7.182155 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 156, - "type": "imguizmo", - "translation": [ - 0.005559, - 0.001419, - 0.011675 - ], - "rotation_deg": [ - 0.472058, - 2.737496, - -7.322821 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 157, - "type": "imguizmo", - "translation": [ - 0.005514, - 0.001391, - 0.011761 - ], - "rotation_deg": [ - 0.45205, - 2.712628, - -7.458384 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 158, - "type": "imguizmo", - "translation": [ - 0.00547, - 0.00136, - 0.01185 - ], - "rotation_deg": [ - 0.430176, - 2.689007, - -7.588749 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 159, - "type": "imguizmo", - "translation": [ - 0.005429, - 0.001325, - 0.011939 - ], - "rotation_deg": [ - 0.406486, - 2.666648, - -7.713826 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 160, - "type": "imguizmo", - "translation": [ - 0.005389, - 0.001286, - 0.012031 - ], - "rotation_deg": [ - 0.381036, - 2.645566, - -7.833526 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 161, - "type": "imguizmo", - "translation": [ - 0.005351, - 0.001243, - 0.012123 - ], - "rotation_deg": [ - 0.353887, - 2.625777, - -7.947767 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 162, - "type": "imguizmo", - "translation": [ - 0.005315, - 0.001197, - 0.012217 - ], - "rotation_deg": [ - 0.325107, - 2.607295, - -8.05647 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 163, - "type": "imguizmo", - "translation": [ - 0.00528, - 0.001148, - 0.012312 - ], - "rotation_deg": [ - 0.294765, - 2.590131, - -8.159557 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 164, - "type": "imguizmo", - "translation": [ - 0.005248, - 0.001095, - 0.012409 - ], - "rotation_deg": [ - 0.262938, - 2.574298, - -8.256958 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 165, - "type": "imguizmo", - "translation": [ - 0.005217, - 0.001039, - 0.012506 - ], - "rotation_deg": [ - 0.229707, - 2.559807, - -8.348605 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 166, - "type": "imguizmo", - "translation": [ - 0.005189, - 0.000981, - 0.012604 - ], - "rotation_deg": [ - 0.195157, - 2.546669, - -8.434433 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 167, - "type": "imguizmo", - "translation": [ - 0.005162, - 0.00092, - 0.012704 - ], - "rotation_deg": [ - 0.159378, - 2.534891, - -8.514383 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 168, - "type": "imguizmo", - "translation": [ - 0.005137, - 0.000856, - 0.012804 - ], - "rotation_deg": [ - 0.122462, - 2.524484, - -8.5884 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 169, - "type": "imguizmo", - "translation": [ - 0.005115, - 0.00079, - 0.012905 - ], - "rotation_deg": [ - 0.084507, - 2.515453, - -8.656431 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 170, - "type": "imguizmo", - "translation": [ - 0.005094, - 0.000721, - 0.013007 - ], - "rotation_deg": [ - 0.045612, - 2.507804, - -8.718429 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 171, - "type": "imguizmo", - "translation": [ - 0.005075, - 0.000651, - 0.01311 - ], - "rotation_deg": [ - 0.005882, - 2.501545, - -8.774351 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 172, - "type": "imguizmo", - "translation": [ - 0.005059, - 0.000579, - 0.013213 - ], - "rotation_deg": [ - -0.034578, - 2.496678, - -8.824158 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 173, - "type": "imguizmo", - "translation": [ - 0.005044, - 0.000505, - 0.013317 - ], - "rotation_deg": [ - -0.075659, - 2.493208, - -8.867816 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 174, - "type": "imguizmo", - "translation": [ - 0.005032, - 0.00043, - 0.013421 - ], - "rotation_deg": [ - -0.117249, - 2.491135, - -8.905293 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 175, - "type": "imguizmo", - "translation": [ - 0.005021, - 0.000353, - 0.013526 - ], - "rotation_deg": [ - -0.159237, - 2.490464, - -8.936564 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 176, - "type": "imguizmo", - "translation": [ - 0.005013, - 0.000276, - 0.013631 - ], - "rotation_deg": [ - -0.201507, - 2.491192, - -8.961608 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 177, - "type": "imguizmo", - "translation": [ - 0.005007, - 0.000197, - 0.013736 - ], - "rotation_deg": [ - -0.243945, - 2.493321, - -8.980405 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 178, - "type": "imguizmo", - "translation": [ - 0.005002, - 0.000119, - 0.013842 - ], - "rotation_deg": [ - -0.286432, - 2.496847, - -8.992944 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 179, - "type": "imguizmo", - "translation": [ - 0.005, - 4E-05, - 0.013947 - ], - "rotation_deg": [ - -0.328853, - 2.50177, - -8.999216 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 180, - "type": "imguizmo", - "translation": [ - 0.005, - -4E-05, - 0.014053 - ], - "rotation_deg": [ - -0.371087, - 2.508086, - -8.999216 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 181, - "type": "imguizmo", - "translation": [ - 0.005002, - -0.000119, - 0.014158 - ], - "rotation_deg": [ - -0.41302, - 2.515789, - -8.992944 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 182, - "type": "imguizmo", - "translation": [ - 0.005007, - -0.000197, - 0.014264 - ], - "rotation_deg": [ - -0.454531, - 2.524876, - -8.980405 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 183, - "type": "imguizmo", - "translation": [ - 0.005013, - -0.000276, - 0.014369 - ], - "rotation_deg": [ - -0.495507, - 2.535339, - -8.961608 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 184, - "type": "imguizmo", - "translation": [ - 0.005021, - -0.000353, - 0.014474 - ], - "rotation_deg": [ - -0.535829, - 2.547172, - -8.936564 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 185, - "type": "imguizmo", - "translation": [ - 0.005032, - -0.00043, - 0.014579 - ], - "rotation_deg": [ - -0.575385, - 2.560364, - -8.905293 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 186, - "type": "imguizmo", - "translation": [ - 0.005044, - -0.000505, - 0.014683 - ], - "rotation_deg": [ - -0.614061, - 2.57491, - -8.867816 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 187, - "type": "imguizmo", - "translation": [ - 0.005059, - -0.000579, - 0.014787 - ], - "rotation_deg": [ - -0.651746, - 2.590796, - -8.824158 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 188, - "type": "imguizmo", - "translation": [ - 0.005075, - -0.000651, - 0.01489 - ], - "rotation_deg": [ - -0.688332, - 2.608013, - -8.774351 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 189, - "type": "imguizmo", - "translation": [ - 0.005094, - -0.000721, - 0.014993 - ], - "rotation_deg": [ - -0.723712, - 2.626548, - -8.718429 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 190, - "type": "imguizmo", - "translation": [ - 0.005115, - -0.00079, - 0.015095 - ], - "rotation_deg": [ - -0.757785, - 2.64639, - -8.656431 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 191, - "type": "imguizmo", - "translation": [ - 0.005137, - -0.000856, - 0.015196 - ], - "rotation_deg": [ - -0.790448, - 2.667523, - -8.5884 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 192, - "type": "imguizmo", - "translation": [ - 0.005162, - -0.00092, - 0.015296 - ], - "rotation_deg": [ - -0.821608, - 2.689933, - -8.514383 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 193, - "type": "imguizmo", - "translation": [ - 0.005189, - -0.000981, - 0.015396 - ], - "rotation_deg": [ - -0.851169, - 2.713605, - -8.434433 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 194, - "type": "imguizmo", - "translation": [ - 0.005217, - -0.001039, - 0.015494 - ], - "rotation_deg": [ - -0.879043, - 2.738522, - -8.348605 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 195, - "type": "imguizmo", - "translation": [ - 0.005248, - -0.001095, - 0.015591 - ], - "rotation_deg": [ - -0.905146, - 2.764666, - -8.256958 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 196, - "type": "imguizmo", - "translation": [ - 0.00528, - -0.001148, - 0.015688 - ], - "rotation_deg": [ - -0.929397, - 2.79202, - -8.159557 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 197, - "type": "imguizmo", - "translation": [ - 0.005315, - -0.001197, - 0.015783 - ], - "rotation_deg": [ - -0.951721, - 2.820563, - -8.05647 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 198, - "type": "imguizmo", - "translation": [ - 0.005351, - -0.001243, - 0.015877 - ], - "rotation_deg": [ - -0.972047, - 2.850278, - -7.947767 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 199, - "type": "imguizmo", - "translation": [ - 0.005389, - -0.001286, - 0.015969 - ], - "rotation_deg": [ - -0.99031, - 2.881142, - -7.833526 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 200, - "type": "imguizmo", - "translation": [ - 0.005429, - -0.001325, - 0.016061 - ], - "rotation_deg": [ - -1.00645, - 2.913134, - -7.713826 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 201, - "type": "imguizmo", - "translation": [ - 0.00547, - -0.00136, - 0.01615 - ], - "rotation_deg": [ - -1.020412, - 2.946232, - -7.588749 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 202, - "type": "imguizmo", - "translation": [ - 0.005514, - -0.001391, - 0.016239 - ], - "rotation_deg": [ - -1.032146, - 2.980414, - -7.458384 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 203, - "type": "imguizmo", - "translation": [ - 0.005559, - -0.001419, - 0.016325 - ], - "rotation_deg": [ - -1.04161, - 3.015654, - -7.322821 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 204, - "type": "imguizmo", - "translation": [ - 0.005606, - -0.001443, - 0.016411 - ], - "rotation_deg": [ - -1.048767, - 3.051928, - -7.182155 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 205, - "type": "imguizmo", - "translation": [ - 0.005655, - -0.001462, - 0.016494 - ], - "rotation_deg": [ - -1.053583, - 3.089212, - -7.036483 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 206, - "type": "imguizmo", - "translation": [ - 0.005705, - -0.001478, - 0.016576 - ], - "rotation_deg": [ - -1.056035, - 3.127479, - -6.885908 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 207, - "type": "imguizmo", - "translation": [ - 0.005756, - -0.001489, - 0.016656 - ], - "rotation_deg": [ - -1.056104, - 3.166702, - -6.730533 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 208, - "type": "imguizmo", - "translation": [ - 0.00581, - -0.001497, - 0.016734 - ], - "rotation_deg": [ - -1.053776, - 3.206855, - -6.570468 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 209, - "type": "imguizmo", - "translation": [ - 0.005865, - -0.0015, - 0.01681 - ], - "rotation_deg": [ - -1.049045, - 3.247909, - -6.405824 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 210, - "type": "imguizmo", - "translation": [ - 0.005921, - -0.001499, - 0.016884 - ], - "rotation_deg": [ - -1.041912, - 3.289835, - -6.236716 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 211, - "type": "imguizmo", - "translation": [ - 0.005979, - -0.001494, - 0.016956 - ], - "rotation_deg": [ - -1.03238, - 3.332606, - -6.063261 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 212, - "type": "imguizmo", - "translation": [ - 0.006038, - -0.001484, - 0.017026 - ], - "rotation_deg": [ - -1.020466, - 3.376189, - -5.88558 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 213, - "type": "imguizmo", - "translation": [ - 0.006099, - -0.001471, - 0.017094 - ], - "rotation_deg": [ - -1.006184, - 3.420556, - -5.703798 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 214, - "type": "imguizmo", - "translation": [ - 0.006161, - -0.001453, - 0.01716 - ], - "rotation_deg": [ - -0.989561, - 3.465675, - -5.518041 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 215, - "type": "imguizmo", - "translation": [ - 0.006224, - -0.001431, - 0.017224 - ], - "rotation_deg": [ - -0.97063, - 3.511514, - -5.328438 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 216, - "type": "imguizmo", - "translation": [ - 0.006288, - -0.001406, - 0.017285 - ], - "rotation_deg": [ - -0.949426, - 3.558042, - -5.135121 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 217, - "type": "imguizmo", - "translation": [ - 0.006354, - -0.001376, - 0.017344 - ], - "rotation_deg": [ - -0.925994, - 3.605228, - -4.938226 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 218, - "type": "imguizmo", - "translation": [ - 0.006421, - -0.001343, - 0.017401 - ], - "rotation_deg": [ - -0.900382, - 3.653037, - -4.737889 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 219, - "type": "imguizmo", - "translation": [ - 0.006489, - -0.001306, - 0.017455 - ], - "rotation_deg": [ - -0.872645, - 3.701436, - -4.534251 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 220, - "type": "imguizmo", - "translation": [ - 0.006558, - -0.001265, - 0.017507 - ], - "rotation_deg": [ - -0.842846, - 3.750391, - -4.327452 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 221, - "type": "imguizmo", - "translation": [ - 0.006627, - -0.00122, - 0.017557 - ], - "rotation_deg": [ - -0.811047, - 3.799869, - -4.117638 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 222, - "type": "imguizmo", - "translation": [ - 0.006698, - -0.001173, - 0.017604 - ], - "rotation_deg": [ - -0.777324, - 3.849835, - -3.904954 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 223, - "type": "imguizmo", - "translation": [ - 0.00677, - -0.001122, - 0.017648 - ], - "rotation_deg": [ - -0.741752, - 3.900254, - -3.689548 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 224, - "type": "imguizmo", - "translation": [ - 0.006843, - -0.001068, - 0.01769 - ], - "rotation_deg": [ - -0.704413, - 3.951091, - -3.471571 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 225, - "type": "imguizmo", - "translation": [ - 0.006916, - -0.001011, - 0.01773 - ], - "rotation_deg": [ - -0.665392, - 4.002311, - -3.251175 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 226, - "type": "imguizmo", - "translation": [ - 0.00699, - -0.000951, - 0.017767 - ], - "rotation_deg": [ - -0.62478, - 4.053877, - -3.028513 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 227, - "type": "imguizmo", - "translation": [ - 0.007065, - -0.000888, - 0.017801 - ], - "rotation_deg": [ - -0.582673, - 4.105754, - -2.80374 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 228, - "type": "imguizmo", - "translation": [ - 0.007141, - -0.000823, - 0.017833 - ], - "rotation_deg": [ - -0.53917, - 4.157906, - -2.577014 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 229, - "type": "imguizmo", - "translation": [ - 0.007217, - -0.000756, - 0.017861 - ], - "rotation_deg": [ - -0.494375, - 4.210296, - -2.348491 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 230, - "type": "imguizmo", - "translation": [ - 0.007294, - -0.000686, - 0.017888 - ], - "rotation_deg": [ - -0.448392, - 4.262889, - -2.118332 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 231, - "type": "imguizmo", - "translation": [ - 0.007371, - -0.000615, - 0.017911 - ], - "rotation_deg": [ - -0.401332, - 4.315647, - -1.886696 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 232, - "type": "imguizmo", - "translation": [ - 0.007449, - -0.000542, - 0.017932 - ], - "rotation_deg": [ - -0.353305, - 4.368532, - -1.653746 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 233, - "type": "imguizmo", - "translation": [ - 0.007527, - -0.000467, - 0.01795 - ], - "rotation_deg": [ - -0.304429, - 4.42151, - -1.419643 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 234, - "type": "imguizmo", - "translation": [ - 0.007605, - -0.000391, - 0.017965 - ], - "rotation_deg": [ - -0.254821, - 4.474542, - -1.18455 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 235, - "type": "imguizmo", - "translation": [ - 0.007684, - -0.000314, - 0.017978 - ], - "rotation_deg": [ - -0.204597, - 4.527593, - -0.948632 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 236, - "type": "imguizmo", - "translation": [ - 0.007763, - -0.000237, - 0.017987 - ], - "rotation_deg": [ - -0.153881, - 4.580623, - -0.712053 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 237, - "type": "imguizmo", - "translation": [ - 0.007842, - -0.000158, - 0.017994 - ], - "rotation_deg": [ - -0.102794, - 4.633598, - -0.474978 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 238, - "type": "imguizmo", - "translation": [ - 0.007921, - -7.9E-05, - 0.017999 - ], - "rotation_deg": [ - -0.051459, - 4.68648, - -0.237572 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 239, - "type": "imguizmo", - "translation": [ - 0.008, - 0, - 0.018 - ], - "rotation_deg": [ - -0.0, - 4.739231, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 240, - "type": "action", - "action": "set_active_render_window", - "value": 0 - }, - { - "frame": 240, - "type": "action", - "action": "set_active_planar", - "value": 1 - }, - { - "frame": 240, - "type": "action", - "action": "set_projection_type", - "value": "perspective" - }, - { - "frame": 240, - "type": "action", - "action": "set_left_handed", - "value": true - }, - { - "frame": 240, - "type": "action", - "action": "reset_active_camera", - "value": 1 - }, - { - "frame": 240, - "type": "action", - "action": "set_active_render_window", - "value": 1 - }, - { - "frame": 240, - "type": "action", - "action": "set_active_planar", - "value": 1 - }, - { - "frame": 240, - "type": "action", - "action": "set_projection_type", - "value": "orthographic" - }, - { - "frame": 240, - "type": "action", - "action": "set_left_handed", - "value": true - }, - { - "frame": 240, - "type": "action", - "action": "set_active_render_window", - "value": 0 - }, - { - "frame": 241, - "type": "imguizmo", - "translation": [ - 0, - 3.6, - 2 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 242, - "type": "imguizmo", - "translation": [ - 0.08447, - 3.598746, - 2.031665 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 243, - "type": "imguizmo", - "translation": [ - 0.168881, - 3.594983, - 2.063242 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 244, - "type": "imguizmo", - "translation": [ - 0.253175, - 3.588715, - 2.094643 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 245, - "type": "imguizmo", - "translation": [ - 0.337292, - 3.579946, - 2.12578 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 246, - "type": "imguizmo", - "translation": [ - 0.421173, - 3.568682, - 2.156566 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 247, - "type": "imguizmo", - "translation": [ - 0.504762, - 3.554932, - 2.186916 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 248, - "type": "imguizmo", - "translation": [ - 0.587998, - 3.538703, - 2.216745 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 249, - "type": "imguizmo", - "translation": [ - 0.670825, - 3.520009, - 2.24597 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 250, - "type": "imguizmo", - "translation": [ - 0.753185, - 3.498861, - 2.274509 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 251, - "type": "imguizmo", - "translation": [ - 0.835019, - 3.475275, - 2.302283 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 252, - "type": "imguizmo", - "translation": [ - 0.916272, - 3.449267, - 2.329215 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 253, - "type": "imguizmo", - "translation": [ - 0.996885, - 3.420855, - 2.355229 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 254, - "type": "imguizmo", - "translation": [ - 1.076805, - 3.390059, - 2.380253 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 255, - "type": "imguizmo", - "translation": [ - 1.155973, - 3.3569, - 2.404217 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 256, - "type": "imguizmo", - "translation": [ - 1.234336, - 3.321402, - 2.427055 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 257, - "type": "imguizmo", - "translation": [ - 1.311839, - 3.283589, - 2.448702 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 258, - "type": "imguizmo", - "translation": [ - 1.388428, - 3.243488, - 2.469099 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 259, - "type": "imguizmo", - "translation": [ - 1.464049, - 3.201126, - 2.488188 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 260, - "type": "imguizmo", - "translation": [ - 1.53865, - 3.156534, - 2.505917 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 261, - "type": "imguizmo", - "translation": [ - 1.612178, - 3.109741, - 2.522235 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 262, - "type": "imguizmo", - "translation": [ - 1.684583, - 3.060782, - 2.537098 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 263, - "type": "imguizmo", - "translation": [ - 1.755814, - 3.009689, - 2.550464 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 264, - "type": "imguizmo", - "translation": [ - 1.825821, - 2.956499, - 2.562296 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 265, - "type": "imguizmo", - "translation": [ - 1.894556, - 2.901248, - 2.57256 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 266, - "type": "imguizmo", - "translation": [ - 1.96197, - 2.843975, - 2.581229 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 267, - "type": "imguizmo", - "translation": [ - 2.028017, - 2.784721, - 2.588277 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 268, - "type": "imguizmo", - "translation": [ - 2.092651, - 2.723525, - 2.593686 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 269, - "type": "imguizmo", - "translation": [ - 2.155826, - 2.660432, - 2.597441 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 270, - "type": "imguizmo", - "translation": [ - 2.217499, - 2.595485, - 2.59953 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 271, - "type": "imguizmo", - "translation": [ - 2.277626, - 2.528728, - 2.599948 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 272, - "type": "imguizmo", - "translation": [ - 2.336167, - 2.46021, - 2.598694 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 273, - "type": "imguizmo", - "translation": [ - 2.393079, - 2.389976, - 2.595771 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 274, - "type": "imguizmo", - "translation": [ - 2.448323, - 2.318078, - 2.591188 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 275, - "type": "imguizmo", - "translation": [ - 2.501861, - 2.244563, - 2.584957 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 276, - "type": "imguizmo", - "translation": [ - 2.553655, - 2.169485, - 2.577095 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 277, - "type": "imguizmo", - "translation": [ - 2.60367, - 2.092894, - 2.567626 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 278, - "type": "imguizmo", - "translation": [ - 2.65187, - 2.014845, - 2.556574 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 279, - "type": "imguizmo", - "translation": [ - 2.698222, - 1.935392, - 2.54397 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 280, - "type": "imguizmo", - "translation": [ - 2.742694, - 1.85459, - 2.529851 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 281, - "type": "imguizmo", - "translation": [ - 2.785254, - 1.772495, - 2.514255 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 282, - "type": "imguizmo", - "translation": [ - 2.825873, - 1.689165, - 2.497226 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 283, - "type": "imguizmo", - "translation": [ - 2.864523, - 1.604658, - 2.47881 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 284, - "type": "imguizmo", - "translation": [ - 2.901176, - 1.519033, - 2.459061 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 285, - "type": "imguizmo", - "translation": [ - 2.935807, - 1.432349, - 2.438031 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 286, - "type": "imguizmo", - "translation": [ - 2.968393, - 1.344666, - 2.415781 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 287, - "type": "imguizmo", - "translation": [ - 2.99891, - 1.256047, - 2.392372 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 288, - "type": "imguizmo", - "translation": [ - 3.027336, - 1.166552, - 2.367869 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 289, - "type": "imguizmo", - "translation": [ - 3.053653, - 1.076245, - 2.342341 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 290, - "type": "imguizmo", - "translation": [ - 3.077842, - 0.985187, - 2.315859 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 291, - "type": "imguizmo", - "translation": [ - 3.099886, - 0.893442, - 2.288497 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 292, - "type": "imguizmo", - "translation": [ - 3.119769, - 0.801075, - 2.26033 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 293, - "type": "imguizmo", - "translation": [ - 3.137479, - 0.70815, - 2.231438 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 294, - "type": "imguizmo", - "translation": [ - 3.153001, - 0.614731, - 2.201901 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 295, - "type": "imguizmo", - "translation": [ - 3.166327, - 0.520884, - 2.171801 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 296, - "type": "imguizmo", - "translation": [ - 3.177445, - 0.426674, - 2.141222 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 297, - "type": "imguizmo", - "translation": [ - 3.186349, - 0.332166, - 2.11025 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 298, - "type": "imguizmo", - "translation": [ - 3.193033, - 0.237427, - 2.07897 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 299, - "type": "imguizmo", - "translation": [ - 3.197491, - 0.142522, - 2.04747 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 300, - "type": "imguizmo", - "translation": [ - 3.199721, - 0.047519, - 2.015838 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 301, - "type": "imguizmo", - "translation": [ - 3.199721, - -0.047519, - 1.984162 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 302, - "type": "imguizmo", - "translation": [ - 3.197491, - -0.142522, - 1.95253 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 303, - "type": "imguizmo", - "translation": [ - 3.193033, - -0.237427, - 1.92103 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 304, - "type": "imguizmo", - "translation": [ - 3.186349, - -0.332166, - 1.88975 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 305, - "type": "imguizmo", - "translation": [ - 3.177445, - -0.426674, - 1.858778 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 306, - "type": "imguizmo", - "translation": [ - 3.166327, - -0.520884, - 1.828199 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 307, - "type": "imguizmo", - "translation": [ - 3.153001, - -0.614731, - 1.798099 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 308, - "type": "imguizmo", - "translation": [ - 3.137479, - -0.70815, - 1.768562 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 309, - "type": "imguizmo", - "translation": [ - 3.119769, - -0.801075, - 1.73967 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 310, - "type": "imguizmo", - "translation": [ - 3.099886, - -0.893442, - 1.711503 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 311, - "type": "imguizmo", - "translation": [ - 3.077842, - -0.985187, - 1.684141 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 312, - "type": "imguizmo", - "translation": [ - 3.053653, - -1.076245, - 1.657659 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 313, - "type": "imguizmo", - "translation": [ - 3.027336, - -1.166552, - 1.632131 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 314, - "type": "imguizmo", - "translation": [ - 2.99891, - -1.256047, - 1.607628 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 315, - "type": "imguizmo", - "translation": [ - 2.968393, - -1.344666, - 1.584219 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 316, - "type": "imguizmo", - "translation": [ - 2.935807, - -1.432349, - 1.561969 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 317, - "type": "imguizmo", - "translation": [ - 2.901176, - -1.519033, - 1.540939 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 318, - "type": "imguizmo", - "translation": [ - 2.864523, - -1.604658, - 1.52119 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 319, - "type": "imguizmo", - "translation": [ - 2.825873, - -1.689165, - 1.502774 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 320, - "type": "imguizmo", - "translation": [ - 2.785254, - -1.772495, - 1.485745 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 321, - "type": "imguizmo", - "translation": [ - 2.742694, - -1.85459, - 1.470149 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 322, - "type": "imguizmo", - "translation": [ - 2.698222, - -1.935392, - 1.45603 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 323, - "type": "imguizmo", - "translation": [ - 2.65187, - -2.014845, - 1.443426 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 324, - "type": "imguizmo", - "translation": [ - 2.60367, - -2.092894, - 1.432374 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 325, - "type": "imguizmo", - "translation": [ - 2.553655, - -2.169485, - 1.422905 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 326, - "type": "imguizmo", - "translation": [ - 2.501861, - -2.244563, - 1.415043 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 327, - "type": "imguizmo", - "translation": [ - 2.448323, - -2.318078, - 1.408812 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 328, - "type": "imguizmo", - "translation": [ - 2.393079, - -2.389976, - 1.404229 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 329, - "type": "imguizmo", - "translation": [ - 2.336167, - -2.46021, - 1.401306 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 330, - "type": "imguizmo", - "translation": [ - 2.277626, - -2.528728, - 1.400052 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 331, - "type": "imguizmo", - "translation": [ - 2.217499, - -2.595485, - 1.40047 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 332, - "type": "imguizmo", - "translation": [ - 2.155826, - -2.660432, - 1.402559 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 333, - "type": "imguizmo", - "translation": [ - 2.092651, - -2.723525, - 1.406314 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 334, - "type": "imguizmo", - "translation": [ - 2.028017, - -2.784721, - 1.411723 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 335, - "type": "imguizmo", - "translation": [ - 1.96197, - -2.843975, - 1.418771 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 336, - "type": "imguizmo", - "translation": [ - 1.894556, - -2.901248, - 1.42744 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 337, - "type": "imguizmo", - "translation": [ - 1.825821, - -2.956499, - 1.437704 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 338, - "type": "imguizmo", - "translation": [ - 1.755814, - -3.009689, - 1.449536 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 339, - "type": "imguizmo", - "translation": [ - 1.684583, - -3.060782, - 1.462902 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 340, - "type": "imguizmo", - "translation": [ - 1.612178, - -3.109741, - 1.477765 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 341, - "type": "imguizmo", - "translation": [ - 1.53865, - -3.156534, - 1.494083 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 342, - "type": "imguizmo", - "translation": [ - 1.464049, - -3.201126, - 1.511812 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 343, - "type": "imguizmo", - "translation": [ - 1.388428, - -3.243488, - 1.530901 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 344, - "type": "imguizmo", - "translation": [ - 1.311839, - -3.283589, - 1.551298 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 345, - "type": "imguizmo", - "translation": [ - 1.234336, - -3.321402, - 1.572945 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 346, - "type": "imguizmo", - "translation": [ - 1.155973, - -3.3569, - 1.595783 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 347, - "type": "imguizmo", - "translation": [ - 1.076805, - -3.390059, - 1.619747 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 348, - "type": "imguizmo", - "translation": [ - 0.996885, - -3.420855, - 1.644771 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 349, - "type": "imguizmo", - "translation": [ - 0.916272, - -3.449267, - 1.670785 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 350, - "type": "imguizmo", - "translation": [ - 0.835019, - -3.475275, - 1.697717 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 351, - "type": "imguizmo", - "translation": [ - 0.753185, - -3.498861, - 1.725491 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 352, - "type": "imguizmo", - "translation": [ - 0.670825, - -3.520009, - 1.75403 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 353, - "type": "imguizmo", - "translation": [ - 0.587998, - -3.538703, - 1.783255 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 354, - "type": "imguizmo", - "translation": [ - 0.504762, - -3.554932, - 1.813084 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 355, - "type": "imguizmo", - "translation": [ - 0.421173, - -3.568682, - 1.843434 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 356, - "type": "imguizmo", - "translation": [ - 0.337292, - -3.579946, - 1.87422 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 357, - "type": "imguizmo", - "translation": [ - 0.253175, - -3.588715, - 1.905357 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 358, - "type": "imguizmo", - "translation": [ - 0.168881, - -3.594983, - 1.936758 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 359, - "type": "imguizmo", - "translation": [ - 0.08447, - -3.598746, - 1.968335 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 360, - "type": "imguizmo", - "translation": [ - 0, - -3.6, - 2 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 361, - "type": "imguizmo", - "translation": [ - -0.08447, - -3.598746, - 2.031665 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 362, - "type": "imguizmo", - "translation": [ - -0.168881, - -3.594983, - 2.063242 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 363, - "type": "imguizmo", - "translation": [ - -0.253175, - -3.588715, - 2.094643 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 364, - "type": "imguizmo", - "translation": [ - -0.337292, - -3.579946, - 2.12578 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 365, - "type": "imguizmo", - "translation": [ - -0.421173, - -3.568682, - 2.156566 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 366, - "type": "imguizmo", - "translation": [ - -0.504762, - -3.554932, - 2.186916 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 367, - "type": "imguizmo", - "translation": [ - -0.587998, - -3.538703, - 2.216745 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 368, - "type": "imguizmo", - "translation": [ - -0.670825, - -3.520009, - 2.24597 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 369, - "type": "imguizmo", - "translation": [ - -0.753185, - -3.498861, - 2.274509 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 370, - "type": "imguizmo", - "translation": [ - -0.835019, - -3.475275, - 2.302283 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 371, - "type": "imguizmo", - "translation": [ - -0.916272, - -3.449267, - 2.329215 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 372, - "type": "imguizmo", - "translation": [ - -0.996885, - -3.420855, - 2.355229 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 373, - "type": "imguizmo", - "translation": [ - -1.076805, - -3.390059, - 2.380253 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 374, - "type": "imguizmo", - "translation": [ - -1.155973, - -3.3569, - 2.404217 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 375, - "type": "imguizmo", - "translation": [ - -1.234336, - -3.321402, - 2.427055 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 376, - "type": "imguizmo", - "translation": [ - -1.311839, - -3.283589, - 2.448702 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 377, - "type": "imguizmo", - "translation": [ - -1.388428, - -3.243488, - 2.469099 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 378, - "type": "imguizmo", - "translation": [ - -1.464049, - -3.201126, - 2.488188 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 379, - "type": "imguizmo", - "translation": [ - -1.53865, - -3.156534, - 2.505917 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 380, - "type": "imguizmo", - "translation": [ - -1.612178, - -3.109741, - 2.522235 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 381, - "type": "imguizmo", - "translation": [ - -1.684583, - -3.060782, - 2.537098 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 382, - "type": "imguizmo", - "translation": [ - -1.755814, - -3.009689, - 2.550464 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 383, - "type": "imguizmo", - "translation": [ - -1.825821, - -2.956499, - 2.562296 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 384, - "type": "imguizmo", - "translation": [ - -1.894556, - -2.901248, - 2.57256 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 385, - "type": "imguizmo", - "translation": [ - -1.96197, - -2.843975, - 2.581229 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 386, - "type": "imguizmo", - "translation": [ - -2.028017, - -2.784721, - 2.588277 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 387, - "type": "imguizmo", - "translation": [ - -2.092651, - -2.723525, - 2.593686 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 388, - "type": "imguizmo", - "translation": [ - -2.155826, - -2.660432, - 2.597441 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 389, - "type": "imguizmo", - "translation": [ - -2.217499, - -2.595485, - 2.59953 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 390, - "type": "imguizmo", - "translation": [ - -2.277626, - -2.528728, - 2.599948 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 391, - "type": "imguizmo", - "translation": [ - -2.336167, - -2.46021, - 2.598694 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 392, - "type": "imguizmo", - "translation": [ - -2.393079, - -2.389976, - 2.595771 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 393, - "type": "imguizmo", - "translation": [ - -2.448323, - -2.318078, - 2.591188 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 394, - "type": "imguizmo", - "translation": [ - -2.501861, - -2.244563, - 2.584957 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 395, - "type": "imguizmo", - "translation": [ - -2.553655, - -2.169485, - 2.577095 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 396, - "type": "imguizmo", - "translation": [ - -2.60367, - -2.092894, - 2.567626 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 397, - "type": "imguizmo", - "translation": [ - -2.65187, - -2.014845, - 2.556574 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 398, - "type": "imguizmo", - "translation": [ - -2.698222, - -1.935392, - 2.54397 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 399, - "type": "imguizmo", - "translation": [ - -2.742694, - -1.85459, - 2.529851 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 400, - "type": "imguizmo", - "translation": [ - -2.785254, - -1.772495, - 2.514255 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 401, - "type": "imguizmo", - "translation": [ - -2.825873, - -1.689165, - 2.497226 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 402, - "type": "imguizmo", - "translation": [ - -2.864523, - -1.604658, - 2.47881 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 403, - "type": "imguizmo", - "translation": [ - -2.901176, - -1.519033, - 2.459061 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 404, - "type": "imguizmo", - "translation": [ - -2.935807, - -1.432349, - 2.438031 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 405, - "type": "imguizmo", - "translation": [ - -2.968393, - -1.344666, - 2.415781 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 406, - "type": "imguizmo", - "translation": [ - -2.99891, - -1.256047, - 2.392372 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 407, - "type": "imguizmo", - "translation": [ - -3.027336, - -1.166552, - 2.367869 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 408, - "type": "imguizmo", - "translation": [ - -3.053653, - -1.076245, - 2.342341 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 409, - "type": "imguizmo", - "translation": [ - -3.077842, - -0.985187, - 2.315859 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 410, - "type": "imguizmo", - "translation": [ - -3.099886, - -0.893442, - 2.288497 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 411, - "type": "imguizmo", - "translation": [ - -3.119769, - -0.801075, - 2.26033 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 412, - "type": "imguizmo", - "translation": [ - -3.137479, - -0.70815, - 2.231438 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 413, - "type": "imguizmo", - "translation": [ - -3.153001, - -0.614731, - 2.201901 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 414, - "type": "imguizmo", - "translation": [ - -3.166327, - -0.520884, - 2.171801 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 415, - "type": "imguizmo", - "translation": [ - -3.177445, - -0.426674, - 2.141222 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 416, - "type": "imguizmo", - "translation": [ - -3.186349, - -0.332166, - 2.11025 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 417, - "type": "imguizmo", - "translation": [ - -3.193033, - -0.237427, - 2.07897 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 418, - "type": "imguizmo", - "translation": [ - -3.197491, - -0.142522, - 2.04747 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 419, - "type": "imguizmo", - "translation": [ - -3.199721, - -0.047519, - 2.015838 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 420, - "type": "imguizmo", - "translation": [ - -3.199721, - 0.047519, - 1.984162 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 421, - "type": "imguizmo", - "translation": [ - -3.197491, - 0.142522, - 1.95253 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 422, - "type": "imguizmo", - "translation": [ - -3.193033, - 0.237427, - 1.92103 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 423, - "type": "imguizmo", - "translation": [ - -3.186349, - 0.332166, - 1.88975 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 424, - "type": "imguizmo", - "translation": [ - -3.177445, - 0.426674, - 1.858778 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 425, - "type": "imguizmo", - "translation": [ - -3.166327, - 0.520884, - 1.828199 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 426, - "type": "imguizmo", - "translation": [ - -3.153001, - 0.614731, - 1.798099 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 427, - "type": "imguizmo", - "translation": [ - -3.137479, - 0.70815, - 1.768562 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 428, - "type": "imguizmo", - "translation": [ - -3.119769, - 0.801075, - 1.73967 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 429, - "type": "imguizmo", - "translation": [ - -3.099886, - 0.893442, - 1.711503 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 430, - "type": "imguizmo", - "translation": [ - -3.077842, - 0.985187, - 1.684141 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 431, - "type": "imguizmo", - "translation": [ - -3.053653, - 1.076245, - 1.657659 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 432, - "type": "imguizmo", - "translation": [ - -3.027336, - 1.166552, - 1.632131 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 433, - "type": "imguizmo", - "translation": [ - -2.99891, - 1.256047, - 1.607628 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 434, - "type": "imguizmo", - "translation": [ - -2.968393, - 1.344666, - 1.584219 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 435, - "type": "imguizmo", - "translation": [ - -2.935807, - 1.432349, - 1.561969 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 436, - "type": "imguizmo", - "translation": [ - -2.901176, - 1.519033, - 1.540939 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 437, - "type": "imguizmo", - "translation": [ - -2.864523, - 1.604658, - 1.52119 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 438, - "type": "imguizmo", - "translation": [ - -2.825873, - 1.689165, - 1.502774 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 439, - "type": "imguizmo", - "translation": [ - -2.785254, - 1.772495, - 1.485745 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 440, - "type": "imguizmo", - "translation": [ - -2.742694, - 1.85459, - 1.470149 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 441, - "type": "imguizmo", - "translation": [ - -2.698222, - 1.935392, - 1.45603 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 442, - "type": "imguizmo", - "translation": [ - -2.65187, - 2.014845, - 1.443426 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 443, - "type": "imguizmo", - "translation": [ - -2.60367, - 2.092894, - 1.432374 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 444, - "type": "imguizmo", - "translation": [ - -2.553655, - 2.169485, - 1.422905 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 445, - "type": "imguizmo", - "translation": [ - -2.501861, - 2.244563, - 1.415043 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 446, - "type": "imguizmo", - "translation": [ - -2.448323, - 2.318078, - 1.408812 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 447, - "type": "imguizmo", - "translation": [ - -2.393079, - 2.389976, - 1.404229 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 448, - "type": "imguizmo", - "translation": [ - -2.336167, - 2.46021, - 1.401306 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 449, - "type": "imguizmo", - "translation": [ - -2.277626, - 2.528728, - 1.400052 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 450, - "type": "imguizmo", - "translation": [ - -2.217499, - 2.595485, - 1.40047 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 451, - "type": "imguizmo", - "translation": [ - -2.155826, - 2.660432, - 1.402559 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 452, - "type": "imguizmo", - "translation": [ - -2.092651, - 2.723525, - 1.406314 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 453, - "type": "imguizmo", - "translation": [ - -2.028017, - 2.784721, - 1.411723 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 454, - "type": "imguizmo", - "translation": [ - -1.96197, - 2.843975, - 1.418771 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 455, - "type": "imguizmo", - "translation": [ - -1.894556, - 2.901248, - 1.42744 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 456, - "type": "imguizmo", - "translation": [ - -1.825821, - 2.956499, - 1.437704 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 457, - "type": "imguizmo", - "translation": [ - -1.755814, - 3.009689, - 1.449536 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 458, - "type": "imguizmo", - "translation": [ - -1.684583, - 3.060782, - 1.462902 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 459, - "type": "imguizmo", - "translation": [ - -1.612178, - 3.109741, - 1.477765 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 460, - "type": "imguizmo", - "translation": [ - -1.53865, - 3.156534, - 1.494083 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 461, - "type": "imguizmo", - "translation": [ - -1.464049, - 3.201126, - 1.511812 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 462, - "type": "imguizmo", - "translation": [ - -1.388428, - 3.243488, - 1.530901 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 463, - "type": "imguizmo", - "translation": [ - -1.311839, - 3.283589, - 1.551298 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 464, - "type": "imguizmo", - "translation": [ - -1.234336, - 3.321402, - 1.572945 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 465, - "type": "imguizmo", - "translation": [ - -1.155973, - 3.3569, - 1.595783 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 466, - "type": "imguizmo", - "translation": [ - -1.076805, - 3.390059, - 1.619747 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 467, - "type": "imguizmo", - "translation": [ - -0.996885, - 3.420855, - 1.644771 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 468, - "type": "imguizmo", - "translation": [ - -0.916272, - 3.449267, - 1.670785 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 469, - "type": "imguizmo", - "translation": [ - -0.835019, - 3.475275, - 1.697717 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 470, - "type": "imguizmo", - "translation": [ - -0.753185, - 3.498861, - 1.725491 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 471, - "type": "imguizmo", - "translation": [ - -0.670825, - 3.520009, - 1.75403 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 472, - "type": "imguizmo", - "translation": [ - -0.587998, - 3.538703, - 1.783255 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 473, - "type": "imguizmo", - "translation": [ - -0.504762, - 3.554932, - 1.813084 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 474, - "type": "imguizmo", - "translation": [ - -0.421173, - 3.568682, - 1.843434 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 475, - "type": "imguizmo", - "translation": [ - -0.337292, - 3.579946, - 1.87422 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 476, - "type": "imguizmo", - "translation": [ - -0.253175, - 3.588715, - 1.905357 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 477, - "type": "imguizmo", - "translation": [ - -0.168881, - 3.594983, - 1.936758 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 478, - "type": "imguizmo", - "translation": [ - -0.08447, - 3.598746, - 1.968335 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 479, - "type": "imguizmo", - "translation": [ - 0, - 3.6, - 2 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 480, - "type": "action", - "action": "set_active_render_window", - "value": 0 - }, - { - "frame": 480, - "type": "action", - "action": "set_active_planar", - "value": 2 - }, - { - "frame": 480, - "type": "action", - "action": "set_projection_type", - "value": "perspective" - }, - { - "frame": 480, - "type": "action", - "action": "set_left_handed", - "value": true - }, - { - "frame": 480, - "type": "action", - "action": "reset_active_camera", - "value": 1 - }, - { - "frame": 480, - "type": "action", - "action": "set_active_render_window", - "value": 1 - }, - { - "frame": 480, - "type": "action", - "action": "set_active_planar", - "value": 2 - }, - { - "frame": 480, - "type": "action", - "action": "set_projection_type", - "value": "orthographic" - }, - { - "frame": 480, - "type": "action", - "action": "set_left_handed", - "value": true - }, - { - "frame": 480, - "type": "action", - "action": "set_active_render_window", - "value": 0 - }, - { - "frame": 481, - "type": "imguizmo", - "translation": [ - 0.0, - 0.001139, - 0.001081 - ], - "rotation_deg": [ - 0.0, - 0.078884, - 0.040918 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 482, - "type": "imguizmo", - "translation": [ - 4.8E-05, - 0.001148, - 0.001049 - ], - "rotation_deg": [ - 0.001584, - 0.080686, - 0.04041 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 483, - "type": "imguizmo", - "translation": [ - 9.5E-05, - 0.001157, - 0.001017 - ], - "rotation_deg": [ - 0.003167, - 0.082432, - 0.039873 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 484, - "type": "imguizmo", - "translation": [ - 0.000142, - 0.001165, - 0.000984 - ], - "rotation_deg": [ - 0.004747, - 0.08412, - 0.039309 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 485, - "type": "imguizmo", - "translation": [ - 0.00019, - 0.001172, - 0.00095 - ], - "rotation_deg": [ - 0.006324, - 0.085749, - 0.038717 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 486, - "type": "imguizmo", - "translation": [ - 0.000237, - 0.001179, - 0.000916 - ], - "rotation_deg": [ - 0.007897, - 0.087319, - 0.038098 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 487, - "type": "imguizmo", - "translation": [ - 0.000284, - 0.001184, - 0.000881 - ], - "rotation_deg": [ - 0.009464, - 0.088828, - 0.037452 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 488, - "type": "imguizmo", - "translation": [ - 0.000331, - 0.001189, - 0.000846 - ], - "rotation_deg": [ - 0.011025, - 0.090275, - 0.036781 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 489, - "type": "imguizmo", - "translation": [ - 0.000377, - 0.001193, - 0.000809 - ], - "rotation_deg": [ - 0.012578, - 0.091659, - 0.036083 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 490, - "type": "imguizmo", - "translation": [ - 0.000424, - 0.001196, - 0.000773 - ], - "rotation_deg": [ - 0.014122, - 0.092979, - 0.035361 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 491, - "type": "imguizmo", - "translation": [ - 0.00047, - 0.001198, - 0.000735 - ], - "rotation_deg": [ - 0.015657, - 0.094234, - 0.034614 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 492, - "type": "imguizmo", - "translation": [ - 0.000515, - 0.001199, - 0.000698 - ], - "rotation_deg": [ - 0.01718, - 0.095424, - 0.033843 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 493, - "type": "imguizmo", - "translation": [ - 0.000561, - 0.0012, - 0.000659 - ], - "rotation_deg": [ - 0.018692, - 0.096547, - 0.033048 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 494, - "type": "imguizmo", - "translation": [ - 0.000606, - 0.0012, - 0.000621 - ], - "rotation_deg": [ - 0.02019, - 0.097603, - 0.032231 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 495, - "type": "imguizmo", - "translation": [ - 0.00065, - 0.001199, - 0.000582 - ], - "rotation_deg": [ - 0.021674, - 0.098591, - 0.03139 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 496, - "type": "imguizmo", - "translation": [ - 0.000694, - 0.001197, - 0.000542 - ], - "rotation_deg": [ - 0.023144, - 0.09951, - 0.030528 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 497, - "type": "imguizmo", - "translation": [ - 0.000738, - 0.001194, - 0.000502 - ], - "rotation_deg": [ - 0.024597, - 0.10036, - 0.029645 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 498, - "type": "imguizmo", - "translation": [ - 0.000781, - 0.00119, - 0.000462 - ], - "rotation_deg": [ - 0.026033, - 0.10114, - 0.028741 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 499, - "type": "imguizmo", - "translation": [ - 0.000824, - 0.001186, - 0.000421 - ], - "rotation_deg": [ - 0.027451, - 0.101849, - 0.027817 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 500, - "type": "imguizmo", - "translation": [ - 0.000865, - 0.00118, - 0.00038 - ], - "rotation_deg": [ - 0.02885, - 0.102488, - 0.026874 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 501, - "type": "imguizmo", - "translation": [ - 0.000907, - 0.001174, - 0.000339 - ], - "rotation_deg": [ - 0.030228, - 0.103055, - 0.025911 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 502, - "type": "imguizmo", - "translation": [ - 0.000948, - 0.001167, - 0.000298 - ], - "rotation_deg": [ - 0.031586, - 0.10355, - 0.024931 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 503, - "type": "imguizmo", - "translation": [ - 0.000988, - 0.00116, - 0.000256 - ], - "rotation_deg": [ - 0.032922, - 0.103973, - 0.023934 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 504, - "type": "imguizmo", - "translation": [ - 0.001027, - 0.001151, - 0.000214 - ], - "rotation_deg": [ - 0.034234, - 0.104323, - 0.022919 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 505, - "type": "imguizmo", - "translation": [ - 0.001066, - 0.001142, - 0.000172 - ], - "rotation_deg": [ - 0.035523, - 0.104601, - 0.021889 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 506, - "type": "imguizmo", - "translation": [ - 0.001104, - 0.001132, - 0.00013 - ], - "rotation_deg": [ - 0.036787, - 0.104806, - 0.020844 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 507, - "type": "imguizmo", - "translation": [ - 0.001141, - 0.001121, - 8.8E-05 - ], - "rotation_deg": [ - 0.038025, - 0.104938, - 0.019784 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 508, - "type": "imguizmo", - "translation": [ - 0.001177, - 0.001109, - 4.6E-05 - ], - "rotation_deg": [ - 0.039237, - 0.104997, - 0.01871 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 509, - "type": "imguizmo", - "translation": [ - 0.001213, - 0.001096, - 4E-06 - ], - "rotation_deg": [ - 0.040422, - 0.104982, - 0.017623 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 510, - "type": "imguizmo", - "translation": [ - 0.001247, - 0.001083, - -3.8E-05 - ], - "rotation_deg": [ - 0.041578, - 0.104895, - 0.016524 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 511, - "type": "imguizmo", - "translation": [ - 0.001281, - 0.001069, - -8.1E-05 - ], - "rotation_deg": [ - 0.042705, - 0.104734, - 0.015413 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 512, - "type": "imguizmo", - "translation": [ - 0.001314, - 0.001054, - -0.000123 - ], - "rotation_deg": [ - 0.043803, - 0.1045, - 0.014292 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 513, - "type": "imguizmo", - "translation": [ - 0.001346, - 0.001039, - -0.000165 - ], - "rotation_deg": [ - 0.04487, - 0.104194, - 0.013161 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 514, - "type": "imguizmo", - "translation": [ - 0.001377, - 0.001023, - -0.000207 - ], - "rotation_deg": [ - 0.045906, - 0.103815, - 0.01202 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 515, - "type": "imguizmo", - "translation": [ - 0.001407, - 0.001006, - -0.000249 - ], - "rotation_deg": [ - 0.04691, - 0.103363, - 0.010871 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 516, - "type": "imguizmo", - "translation": [ - 0.001436, - 0.000988, - -0.00029 - ], - "rotation_deg": [ - 0.047881, - 0.10284, - 0.009715 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 517, - "type": "imguizmo", - "translation": [ - 0.001465, - 0.00097, - -0.000332 - ], - "rotation_deg": [ - 0.048819, - 0.102245, - 0.008551 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 518, - "type": "imguizmo", - "translation": [ - 0.001492, - 0.000951, - -0.000373 - ], - "rotation_deg": [ - 0.049723, - 0.101578, - 0.007382 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 519, - "type": "imguizmo", - "translation": [ - 0.001518, - 0.000931, - -0.000414 - ], - "rotation_deg": [ - 0.050592, - 0.100841, - 0.006208 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 520, - "type": "imguizmo", - "translation": [ - 0.001543, - 0.000911, - -0.000454 - ], - "rotation_deg": [ - 0.051426, - 0.100033, - 0.005029 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 521, - "type": "imguizmo", - "translation": [ - 0.001567, - 0.00089, - -0.000495 - ], - "rotation_deg": [ - 0.052224, - 0.099156, - 0.003847 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 522, - "type": "imguizmo", - "translation": [ - 0.00159, - 0.000868, - -0.000535 - ], - "rotation_deg": [ - 0.052985, - 0.09821, - 0.002662 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 523, - "type": "imguizmo", - "translation": [ - 0.001611, - 0.000846, - -0.000574 - ], - "rotation_deg": [ - 0.05371, - 0.097195, - 0.001476 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 524, - "type": "imguizmo", - "translation": [ - 0.001632, - 0.000824, - -0.000614 - ], - "rotation_deg": [ - 0.054397, - 0.096113, - 0.000288 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 525, - "type": "imguizmo", - "translation": [ - 0.001651, - 0.0008, - -0.000652 - ], - "rotation_deg": [ - 0.055046, - 0.094963, - -0.0009 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 526, - "type": "imguizmo", - "translation": [ - 0.00167, - 0.000776, - -0.000691 - ], - "rotation_deg": [ - 0.055657, - 0.093747, - -0.002087 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 527, - "type": "imguizmo", - "translation": [ - 0.001687, - 0.000752, - -0.000729 - ], - "rotation_deg": [ - 0.05623, - 0.092466, - -0.003273 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 528, - "type": "imguizmo", - "translation": [ - 0.001703, - 0.000727, - -0.000766 - ], - "rotation_deg": [ - 0.056763, - 0.091121, - -0.004457 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 529, - "type": "imguizmo", - "translation": [ - 0.001718, - 0.000702, - -0.000803 - ], - "rotation_deg": [ - 0.057256, - 0.089712, - -0.005637 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 530, - "type": "imguizmo", - "translation": [ - 0.001731, - 0.000676, - -0.000839 - ], - "rotation_deg": [ - 0.05771, - 0.088241, - -0.006814 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 531, - "type": "imguizmo", - "translation": [ - 0.001744, - 0.000649, - -0.000875 - ], - "rotation_deg": [ - 0.058123, - 0.086708, - -0.007986 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 532, - "type": "imguizmo", - "translation": [ - 0.001755, - 0.000622, - -0.00091 - ], - "rotation_deg": [ - 0.058496, - 0.085114, - -0.009152 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 533, - "type": "imguizmo", - "translation": [ - 0.001765, - 0.000595, - -0.000944 - ], - "rotation_deg": [ - 0.058828, - 0.083462, - -0.010312 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 534, - "type": "imguizmo", - "translation": [ - 0.001774, - 0.000567, - -0.000978 - ], - "rotation_deg": [ - 0.059119, - 0.081751, - -0.011464 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 535, - "type": "imguizmo", - "translation": [ - 0.001781, - 0.000539, - -0.001011 - ], - "rotation_deg": [ - 0.059369, - 0.079983, - -0.012609 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 536, - "type": "imguizmo", - "translation": [ - 0.001787, - 0.000511, - -0.001043 - ], - "rotation_deg": [ - 0.059577, - 0.078159, - -0.013745 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 537, - "type": "imguizmo", - "translation": [ - 0.001792, - 0.000482, - -0.001075 - ], - "rotation_deg": [ - 0.059744, - 0.076281, - -0.014871 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 538, - "type": "imguizmo", - "translation": [ - 0.001796, - 0.000453, - -0.001106 - ], - "rotation_deg": [ - 0.059869, - 0.07435, - -0.015987 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 539, - "type": "imguizmo", - "translation": [ - 0.001799, - 0.000423, - -0.001136 - ], - "rotation_deg": [ - 0.059953, - 0.072367, - -0.017092 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 540, - "type": "imguizmo", - "translation": [ - 0.0018, - 0.000393, - -0.001165 - ], - "rotation_deg": [ - 0.059995, - 0.070333, - -0.018185 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 541, - "type": "imguizmo", - "translation": [ - 0.0018, - 0.000363, - -0.001194 - ], - "rotation_deg": [ - 0.059995, - 0.068251, - -0.019265 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 542, - "type": "imguizmo", - "translation": [ - 0.001799, - 0.000333, - -0.001222 - ], - "rotation_deg": [ - 0.059953, - 0.066121, - -0.020332 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 543, - "type": "imguizmo", - "translation": [ - 0.001796, - 0.000302, - -0.001249 - ], - "rotation_deg": [ - 0.059869, - 0.063945, - -0.021384 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 544, - "type": "imguizmo", - "translation": [ - 0.001792, - 0.000272, - -0.001275 - ], - "rotation_deg": [ - 0.059744, - 0.061724, - -0.022422 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 545, - "type": "imguizmo", - "translation": [ - 0.001787, - 0.000241, - -0.0013 - ], - "rotation_deg": [ - 0.059577, - 0.05946, - -0.023444 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 546, - "type": "imguizmo", - "translation": [ - 0.001781, - 0.00021, - -0.001324 - ], - "rotation_deg": [ - 0.059369, - 0.057155, - -0.02445 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 547, - "type": "imguizmo", - "translation": [ - 0.001774, - 0.000178, - -0.001347 - ], - "rotation_deg": [ - 0.059119, - 0.05481, - -0.025439 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 548, - "type": "imguizmo", - "translation": [ - 0.001765, - 0.000147, - -0.001369 - ], - "rotation_deg": [ - 0.058828, - 0.052427, - -0.02641 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 549, - "type": "imguizmo", - "translation": [ - 0.001755, - 0.000115, - -0.001391 - ], - "rotation_deg": [ - 0.058496, - 0.050007, - -0.027362 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 550, - "type": "imguizmo", - "translation": [ - 0.001744, - 8.4E-05, - -0.001411 - ], - "rotation_deg": [ - 0.058123, - 0.047553, - -0.028296 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 551, - "type": "imguizmo", - "translation": [ - 0.001731, - 5.2E-05, - -0.001431 - ], - "rotation_deg": [ - 0.05771, - 0.045065, - -0.02921 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 552, - "type": "imguizmo", - "translation": [ - 0.001718, - 2.1E-05, - -0.001449 - ], - "rotation_deg": [ - 0.057256, - 0.042546, - -0.030103 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 553, - "type": "imguizmo", - "translation": [ - 0.001703, - -1.1E-05, - -0.001466 - ], - "rotation_deg": [ - 0.056763, - 0.039997, - -0.030975 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 554, - "type": "imguizmo", - "translation": [ - 0.001687, - -4.3E-05, - -0.001483 - ], - "rotation_deg": [ - 0.05623, - 0.037421, - -0.031826 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 555, - "type": "imguizmo", - "translation": [ - 0.00167, - -7.4E-05, - -0.001498 - ], - "rotation_deg": [ - 0.055657, - 0.034818, - -0.032655 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 556, - "type": "imguizmo", - "translation": [ - 0.001651, - -0.000106, - -0.001512 - ], - "rotation_deg": [ - 0.055046, - 0.032191, - -0.033461 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 557, - "type": "imguizmo", - "translation": [ - 0.001632, - -0.000137, - -0.001526 - ], - "rotation_deg": [ - 0.054397, - 0.029541, - -0.034244 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 558, - "type": "imguizmo", - "translation": [ - 0.001611, - -0.000169, - -0.001538 - ], - "rotation_deg": [ - 0.05371, - 0.026871, - -0.035002 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 559, - "type": "imguizmo", - "translation": [ - 0.00159, - -0.0002, - -0.001549 - ], - "rotation_deg": [ - 0.052985, - 0.024183, - -0.035737 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 560, - "type": "imguizmo", - "translation": [ - 0.001567, - -0.000231, - -0.001559 - ], - "rotation_deg": [ - 0.052224, - 0.021477, - -0.036446 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 561, - "type": "imguizmo", - "translation": [ - 0.001543, - -0.000262, - -0.001568 - ], - "rotation_deg": [ - 0.051426, - 0.018757, - -0.03713 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 562, - "type": "imguizmo", - "translation": [ - 0.001518, - -0.000293, - -0.001576 - ], - "rotation_deg": [ - 0.050592, - 0.016023, - -0.037788 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 563, - "type": "imguizmo", - "translation": [ - 0.001492, - -0.000324, - -0.001583 - ], - "rotation_deg": [ - 0.049723, - 0.013278, - -0.03842 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 564, - "type": "imguizmo", - "translation": [ - 0.001465, - -0.000354, - -0.001588 - ], - "rotation_deg": [ - 0.048819, - 0.010524, - -0.039025 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 565, - "type": "imguizmo", - "translation": [ - 0.001436, - -0.000384, - -0.001593 - ], - "rotation_deg": [ - 0.047881, - 0.007763, - -0.039603 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 566, - "type": "imguizmo", - "translation": [ - 0.001407, - -0.000414, - -0.001596 - ], - "rotation_deg": [ - 0.04691, - 0.004996, - -0.040153 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 567, - "type": "imguizmo", - "translation": [ - 0.001377, - -0.000444, - -0.001599 - ], - "rotation_deg": [ - 0.045906, - 0.002226, - -0.040676 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 568, - "type": "imguizmo", - "translation": [ - 0.001346, - -0.000473, - -0.0016 - ], - "rotation_deg": [ - 0.04487, - -0.000546, - -0.041169 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 569, - "type": "imguizmo", - "translation": [ - 0.001314, - -0.000502, - -0.0016 - ], - "rotation_deg": [ - 0.043803, - -0.003318, - -0.041635 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 570, - "type": "imguizmo", - "translation": [ - 0.001281, - -0.000531, - -0.001599 - ], - "rotation_deg": [ - 0.042705, - -0.006087, - -0.042071 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 571, - "type": "imguizmo", - "translation": [ - 0.001247, - -0.000559, - -0.001597 - ], - "rotation_deg": [ - 0.041578, - -0.008852, - -0.042478 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 572, - "type": "imguizmo", - "translation": [ - 0.001213, - -0.000587, - -0.001594 - ], - "rotation_deg": [ - 0.040422, - -0.01161, - -0.042855 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 573, - "type": "imguizmo", - "translation": [ - 0.001177, - -0.000614, - -0.001589 - ], - "rotation_deg": [ - 0.039237, - -0.014361, - -0.043203 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 574, - "type": "imguizmo", - "translation": [ - 0.001141, - -0.000641, - -0.001584 - ], - "rotation_deg": [ - 0.038025, - -0.017102, - -0.04352 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 575, - "type": "imguizmo", - "translation": [ - 0.001104, - -0.000668, - -0.001577 - ], - "rotation_deg": [ - 0.036787, - -0.01983, - -0.043807 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 576, - "type": "imguizmo", - "translation": [ - 0.001066, - -0.000694, - -0.001569 - ], - "rotation_deg": [ - 0.035523, - -0.022545, - -0.044063 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 577, - "type": "imguizmo", - "translation": [ - 0.001027, - -0.000719, - -0.001561 - ], - "rotation_deg": [ - 0.034234, - -0.025244, - -0.044289 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 578, - "type": "imguizmo", - "translation": [ - 0.000988, - -0.000744, - -0.001551 - ], - "rotation_deg": [ - 0.032922, - -0.027926, - -0.044484 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 579, - "type": "imguizmo", - "translation": [ - 0.000948, - -0.000769, - -0.00154 - ], - "rotation_deg": [ - 0.031586, - -0.030588, - -0.044648 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 580, - "type": "imguizmo", - "translation": [ - 0.000907, - -0.000793, - -0.001528 - ], - "rotation_deg": [ - 0.030228, - -0.033229, - -0.044781 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 581, - "type": "imguizmo", - "translation": [ - 0.000865, - -0.000817, - -0.001515 - ], - "rotation_deg": [ - 0.02885, - -0.035847, - -0.044882 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 582, - "type": "imguizmo", - "translation": [ - 0.000824, - -0.000839, - -0.001501 - ], - "rotation_deg": [ - 0.027451, - -0.038439, - -0.044952 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 583, - "type": "imguizmo", - "translation": [ - 0.000781, - -0.000862, - -0.001486 - ], - "rotation_deg": [ - 0.026033, - -0.041005, - -0.044991 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 584, - "type": "imguizmo", - "translation": [ - 0.000738, - -0.000884, - -0.001469 - ], - "rotation_deg": [ - 0.024597, - -0.043542, - -0.044999 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 585, - "type": "imguizmo", - "translation": [ - 0.000694, - -0.000905, - -0.001452 - ], - "rotation_deg": [ - 0.023144, - -0.046049, - -0.044975 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 586, - "type": "imguizmo", - "translation": [ - 0.00065, - -0.000925, - -0.001434 - ], - "rotation_deg": [ - 0.021674, - -0.048524, - -0.04492 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 587, - "type": "imguizmo", - "translation": [ - 0.000606, - -0.000945, - -0.001415 - ], - "rotation_deg": [ - 0.02019, - -0.050965, - -0.044834 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 588, - "type": "imguizmo", - "translation": [ - 0.000561, - -0.000964, - -0.001395 - ], - "rotation_deg": [ - 0.018692, - -0.053371, - -0.044716 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 589, - "type": "imguizmo", - "translation": [ - 0.000515, - -0.000983, - -0.001373 - ], - "rotation_deg": [ - 0.01718, - -0.055739, - -0.044567 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 590, - "type": "imguizmo", - "translation": [ - 0.00047, - -0.001001, - -0.001351 - ], - "rotation_deg": [ - 0.015657, - -0.058068, - -0.044387 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 591, - "type": "imguizmo", - "translation": [ - 0.000424, - -0.001018, - -0.001328 - ], - "rotation_deg": [ - 0.014122, - -0.060358, - -0.044176 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 592, - "type": "imguizmo", - "translation": [ - 0.000377, - -0.001034, - -0.001304 - ], - "rotation_deg": [ - 0.012578, - -0.062604, - -0.043935 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 593, - "type": "imguizmo", - "translation": [ - 0.000331, - -0.00105, - -0.001279 - ], - "rotation_deg": [ - 0.011025, - -0.064808, - -0.043663 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 594, - "type": "imguizmo", - "translation": [ - 0.000284, - -0.001065, - -0.001253 - ], - "rotation_deg": [ - 0.009464, - -0.066966, - -0.04336 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 595, - "type": "imguizmo", - "translation": [ - 0.000237, - -0.001079, - -0.001227 - ], - "rotation_deg": [ - 0.007897, - -0.069077, - -0.043027 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 596, - "type": "imguizmo", - "translation": [ - 0.00019, - -0.001093, - -0.001199 - ], - "rotation_deg": [ - 0.006324, - -0.071141, - -0.042664 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 597, - "type": "imguizmo", - "translation": [ - 0.000142, - -0.001105, - -0.001171 - ], - "rotation_deg": [ - 0.004747, - -0.073154, - -0.042272 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 598, - "type": "imguizmo", - "translation": [ - 9.5E-05, - -0.001117, - -0.001142 - ], - "rotation_deg": [ - 0.003167, - -0.075117, - -0.04185 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 599, - "type": "imguizmo", - "translation": [ - 4.8E-05, - -0.001128, - -0.001112 - ], - "rotation_deg": [ - 0.001584, - -0.077028, - -0.041398 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 600, - "type": "imguizmo", - "translation": [ - 0.0, - -0.001139, - -0.001081 - ], - "rotation_deg": [ - 0.0, - -0.078884, - -0.040918 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 601, - "type": "imguizmo", - "translation": [ - -4.8E-05, - -0.001148, - -0.001049 - ], - "rotation_deg": [ - -0.001584, - -0.080686, - -0.04041 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 602, - "type": "imguizmo", - "translation": [ - -9.5E-05, - -0.001157, - -0.001017 - ], - "rotation_deg": [ - -0.003167, - -0.082432, - -0.039873 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 603, - "type": "imguizmo", - "translation": [ - -0.000142, - -0.001165, - -0.000984 - ], - "rotation_deg": [ - -0.004747, - -0.08412, - -0.039309 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 604, - "type": "imguizmo", - "translation": [ - -0.00019, - -0.001172, - -0.00095 - ], - "rotation_deg": [ - -0.006324, - -0.085749, - -0.038717 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 605, - "type": "imguizmo", - "translation": [ - -0.000237, - -0.001179, - -0.000916 - ], - "rotation_deg": [ - -0.007897, - -0.087319, - -0.038098 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 606, - "type": "imguizmo", - "translation": [ - -0.000284, - -0.001184, - -0.000881 - ], - "rotation_deg": [ - -0.009464, - -0.088828, - -0.037452 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 607, - "type": "imguizmo", - "translation": [ - -0.000331, - -0.001189, - -0.000846 - ], - "rotation_deg": [ - -0.011025, - -0.090275, - -0.036781 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 608, - "type": "imguizmo", - "translation": [ - -0.000377, - -0.001193, - -0.000809 - ], - "rotation_deg": [ - -0.012578, - -0.091659, - -0.036083 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 609, - "type": "imguizmo", - "translation": [ - -0.000424, - -0.001196, - -0.000773 - ], - "rotation_deg": [ - -0.014122, - -0.092979, - -0.035361 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 610, - "type": "imguizmo", - "translation": [ - -0.00047, - -0.001198, - -0.000735 - ], - "rotation_deg": [ - -0.015657, - -0.094234, - -0.034614 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 611, - "type": "imguizmo", - "translation": [ - -0.000515, - -0.001199, - -0.000698 - ], - "rotation_deg": [ - -0.01718, - -0.095424, - -0.033843 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 612, - "type": "imguizmo", - "translation": [ - -0.000561, - -0.0012, - -0.000659 - ], - "rotation_deg": [ - -0.018692, - -0.096547, - -0.033048 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 613, - "type": "imguizmo", - "translation": [ - -0.000606, - -0.0012, - -0.000621 - ], - "rotation_deg": [ - -0.02019, - -0.097603, - -0.032231 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 614, - "type": "imguizmo", - "translation": [ - -0.00065, - -0.001199, - -0.000582 - ], - "rotation_deg": [ - -0.021674, - -0.098591, - -0.03139 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 615, - "type": "imguizmo", - "translation": [ - -0.000694, - -0.001197, - -0.000542 - ], - "rotation_deg": [ - -0.023144, - -0.09951, - -0.030528 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 616, - "type": "imguizmo", - "translation": [ - -0.000738, - -0.001194, - -0.000502 - ], - "rotation_deg": [ - -0.024597, - -0.10036, - -0.029645 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 617, - "type": "imguizmo", - "translation": [ - -0.000781, - -0.00119, - -0.000462 - ], - "rotation_deg": [ - -0.026033, - -0.10114, - -0.028741 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 618, - "type": "imguizmo", - "translation": [ - -0.000824, - -0.001186, - -0.000421 - ], - "rotation_deg": [ - -0.027451, - -0.101849, - -0.027817 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 619, - "type": "imguizmo", - "translation": [ - -0.000865, - -0.00118, - -0.00038 - ], - "rotation_deg": [ - -0.02885, - -0.102488, - -0.026874 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 620, - "type": "imguizmo", - "translation": [ - -0.000907, - -0.001174, - -0.000339 - ], - "rotation_deg": [ - -0.030228, - -0.103055, - -0.025911 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 621, - "type": "imguizmo", - "translation": [ - -0.000948, - -0.001167, - -0.000298 - ], - "rotation_deg": [ - -0.031586, - -0.10355, - -0.024931 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 622, - "type": "imguizmo", - "translation": [ - -0.000988, - -0.00116, - -0.000256 - ], - "rotation_deg": [ - -0.032922, - -0.103973, - -0.023934 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 623, - "type": "imguizmo", - "translation": [ - -0.001027, - -0.001151, - -0.000214 - ], - "rotation_deg": [ - -0.034234, - -0.104323, - -0.022919 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 624, - "type": "imguizmo", - "translation": [ - -0.001066, - -0.001142, - -0.000172 - ], - "rotation_deg": [ - -0.035523, - -0.104601, - -0.021889 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 625, - "type": "imguizmo", - "translation": [ - -0.001104, - -0.001132, - -0.00013 - ], - "rotation_deg": [ - -0.036787, - -0.104806, - -0.020844 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 626, - "type": "imguizmo", - "translation": [ - -0.001141, - -0.001121, - -8.8E-05 - ], - "rotation_deg": [ - -0.038025, - -0.104938, - -0.019784 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 627, - "type": "imguizmo", - "translation": [ - -0.001177, - -0.001109, - -4.6E-05 - ], - "rotation_deg": [ - -0.039237, - -0.104997, - -0.01871 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 628, - "type": "imguizmo", - "translation": [ - -0.001213, - -0.001096, - -4E-06 - ], - "rotation_deg": [ - -0.040422, - -0.104982, - -0.017623 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 629, - "type": "imguizmo", - "translation": [ - -0.001247, - -0.001083, - 3.8E-05 - ], - "rotation_deg": [ - -0.041578, - -0.104895, - -0.016524 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 630, - "type": "imguizmo", - "translation": [ - -0.001281, - -0.001069, - 8.1E-05 - ], - "rotation_deg": [ - -0.042705, - -0.104734, - -0.015413 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 631, - "type": "imguizmo", - "translation": [ - -0.001314, - -0.001054, - 0.000123 - ], - "rotation_deg": [ - -0.043803, - -0.1045, - -0.014292 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 632, - "type": "imguizmo", - "translation": [ - -0.001346, - -0.001039, - 0.000165 - ], - "rotation_deg": [ - -0.04487, - -0.104194, - -0.013161 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 633, - "type": "imguizmo", - "translation": [ - -0.001377, - -0.001023, - 0.000207 - ], - "rotation_deg": [ - -0.045906, - -0.103815, - -0.01202 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 634, - "type": "imguizmo", - "translation": [ - -0.001407, - -0.001006, - 0.000249 - ], - "rotation_deg": [ - -0.04691, - -0.103363, - -0.010871 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 635, - "type": "imguizmo", - "translation": [ - -0.001436, - -0.000988, - 0.00029 - ], - "rotation_deg": [ - -0.047881, - -0.10284, - -0.009715 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 636, - "type": "imguizmo", - "translation": [ - -0.001465, - -0.00097, - 0.000332 - ], - "rotation_deg": [ - -0.048819, - -0.102245, - -0.008551 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 637, - "type": "imguizmo", - "translation": [ - -0.001492, - -0.000951, - 0.000373 - ], - "rotation_deg": [ - -0.049723, - -0.101578, - -0.007382 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 638, - "type": "imguizmo", - "translation": [ - -0.001518, - -0.000931, - 0.000414 - ], - "rotation_deg": [ - -0.050592, - -0.100841, - -0.006208 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 639, - "type": "imguizmo", - "translation": [ - -0.001543, - -0.000911, - 0.000454 - ], - "rotation_deg": [ - -0.051426, - -0.100033, - -0.005029 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 640, - "type": "imguizmo", - "translation": [ - -0.001567, - -0.00089, - 0.000495 - ], - "rotation_deg": [ - -0.052224, - -0.099156, - -0.003847 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 641, - "type": "imguizmo", - "translation": [ - -0.00159, - -0.000868, - 0.000535 - ], - "rotation_deg": [ - -0.052985, - -0.09821, - -0.002662 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 642, - "type": "imguizmo", - "translation": [ - -0.001611, - -0.000846, - 0.000574 - ], - "rotation_deg": [ - -0.05371, - -0.097195, - -0.001476 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 643, - "type": "imguizmo", - "translation": [ - -0.001632, - -0.000824, - 0.000614 - ], - "rotation_deg": [ - -0.054397, - -0.096113, - -0.000288 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 644, - "type": "imguizmo", - "translation": [ - -0.001651, - -0.0008, - 0.000652 - ], - "rotation_deg": [ - -0.055046, - -0.094963, - 0.0009 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 645, - "type": "imguizmo", - "translation": [ - -0.00167, - -0.000776, - 0.000691 - ], - "rotation_deg": [ - -0.055657, - -0.093747, - 0.002087 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 646, - "type": "imguizmo", - "translation": [ - -0.001687, - -0.000752, - 0.000729 - ], - "rotation_deg": [ - -0.05623, - -0.092466, - 0.003273 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 647, - "type": "imguizmo", - "translation": [ - -0.001703, - -0.000727, - 0.000766 - ], - "rotation_deg": [ - -0.056763, - -0.091121, - 0.004457 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 648, - "type": "imguizmo", - "translation": [ - -0.001718, - -0.000702, - 0.000803 - ], - "rotation_deg": [ - -0.057256, - -0.089712, - 0.005637 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 649, - "type": "imguizmo", - "translation": [ - -0.001731, - -0.000676, - 0.000839 - ], - "rotation_deg": [ - -0.05771, - -0.088241, - 0.006814 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 650, - "type": "imguizmo", - "translation": [ - -0.001744, - -0.000649, - 0.000875 - ], - "rotation_deg": [ - -0.058123, - -0.086708, - 0.007986 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 651, - "type": "imguizmo", - "translation": [ - -0.001755, - -0.000622, - 0.00091 - ], - "rotation_deg": [ - -0.058496, - -0.085114, - 0.009152 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 652, - "type": "imguizmo", - "translation": [ - -0.001765, - -0.000595, - 0.000944 - ], - "rotation_deg": [ - -0.058828, - -0.083462, - 0.010312 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 653, - "type": "imguizmo", - "translation": [ - -0.001774, - -0.000567, - 0.000978 - ], - "rotation_deg": [ - -0.059119, - -0.081751, - 0.011464 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 654, - "type": "imguizmo", - "translation": [ - -0.001781, - -0.000539, - 0.001011 - ], - "rotation_deg": [ - -0.059369, - -0.079983, - 0.012609 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 655, - "type": "imguizmo", - "translation": [ - -0.001787, - -0.000511, - 0.001043 - ], - "rotation_deg": [ - -0.059577, - -0.078159, - 0.013745 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 656, - "type": "imguizmo", - "translation": [ - -0.001792, - -0.000482, - 0.001075 - ], - "rotation_deg": [ - -0.059744, - -0.076281, - 0.014871 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 657, - "type": "imguizmo", - "translation": [ - -0.001796, - -0.000453, - 0.001106 - ], - "rotation_deg": [ - -0.059869, - -0.07435, - 0.015987 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 658, - "type": "imguizmo", - "translation": [ - -0.001799, - -0.000423, - 0.001136 - ], - "rotation_deg": [ - -0.059953, - -0.072367, - 0.017092 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 659, - "type": "imguizmo", - "translation": [ - -0.0018, - -0.000393, - 0.001165 - ], - "rotation_deg": [ - -0.059995, - -0.070333, - 0.018185 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 660, - "type": "imguizmo", - "translation": [ - -0.0018, - -0.000363, - 0.001194 - ], - "rotation_deg": [ - -0.059995, - -0.068251, - 0.019265 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 661, - "type": "imguizmo", - "translation": [ - -0.001799, - -0.000333, - 0.001222 - ], - "rotation_deg": [ - -0.059953, - -0.066121, - 0.020332 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 662, - "type": "imguizmo", - "translation": [ - -0.001796, - -0.000302, - 0.001249 - ], - "rotation_deg": [ - -0.059869, - -0.063945, - 0.021384 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 663, - "type": "imguizmo", - "translation": [ - -0.001792, - -0.000272, - 0.001275 - ], - "rotation_deg": [ - -0.059744, - -0.061724, - 0.022422 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 664, - "type": "imguizmo", - "translation": [ - -0.001787, - -0.000241, - 0.0013 - ], - "rotation_deg": [ - -0.059577, - -0.05946, - 0.023444 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 665, - "type": "imguizmo", - "translation": [ - -0.001781, - -0.00021, - 0.001324 - ], - "rotation_deg": [ - -0.059369, - -0.057155, - 0.02445 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 666, - "type": "imguizmo", - "translation": [ - -0.001774, - -0.000178, - 0.001347 - ], - "rotation_deg": [ - -0.059119, - -0.05481, - 0.025439 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 667, - "type": "imguizmo", - "translation": [ - -0.001765, - -0.000147, - 0.001369 - ], - "rotation_deg": [ - -0.058828, - -0.052427, - 0.02641 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 668, - "type": "imguizmo", - "translation": [ - -0.001755, - -0.000115, - 0.001391 - ], - "rotation_deg": [ - -0.058496, - -0.050007, - 0.027362 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 669, - "type": "imguizmo", - "translation": [ - -0.001744, - -8.4E-05, - 0.001411 - ], - "rotation_deg": [ - -0.058123, - -0.047553, - 0.028296 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 670, - "type": "imguizmo", - "translation": [ - -0.001731, - -5.2E-05, - 0.001431 - ], - "rotation_deg": [ - -0.05771, - -0.045065, - 0.02921 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 671, - "type": "imguizmo", - "translation": [ - -0.001718, - -2.1E-05, - 0.001449 - ], - "rotation_deg": [ - -0.057256, - -0.042546, - 0.030103 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 672, - "type": "imguizmo", - "translation": [ - -0.001703, - 1.1E-05, - 0.001466 - ], - "rotation_deg": [ - -0.056763, - -0.039997, - 0.030975 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 673, - "type": "imguizmo", - "translation": [ - -0.001687, - 4.3E-05, - 0.001483 - ], - "rotation_deg": [ - -0.05623, - -0.037421, - 0.031826 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 674, - "type": "imguizmo", - "translation": [ - -0.00167, - 7.4E-05, - 0.001498 - ], - "rotation_deg": [ - -0.055657, - -0.034818, - 0.032655 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 675, - "type": "imguizmo", - "translation": [ - -0.001651, - 0.000106, - 0.001512 - ], - "rotation_deg": [ - -0.055046, - -0.032191, - 0.033461 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 676, - "type": "imguizmo", - "translation": [ - -0.001632, - 0.000137, - 0.001526 - ], - "rotation_deg": [ - -0.054397, - -0.029541, - 0.034244 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 677, - "type": "imguizmo", - "translation": [ - -0.001611, - 0.000169, - 0.001538 - ], - "rotation_deg": [ - -0.05371, - -0.026871, - 0.035002 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 678, - "type": "imguizmo", - "translation": [ - -0.00159, - 0.0002, - 0.001549 - ], - "rotation_deg": [ - -0.052985, - -0.024183, - 0.035737 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 679, - "type": "imguizmo", - "translation": [ - -0.001567, - 0.000231, - 0.001559 - ], - "rotation_deg": [ - -0.052224, - -0.021477, - 0.036446 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 680, - "type": "imguizmo", - "translation": [ - -0.001543, - 0.000262, - 0.001568 - ], - "rotation_deg": [ - -0.051426, - -0.018757, - 0.03713 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 681, - "type": "imguizmo", - "translation": [ - -0.001518, - 0.000293, - 0.001576 - ], - "rotation_deg": [ - -0.050592, - -0.016023, - 0.037788 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 682, - "type": "imguizmo", - "translation": [ - -0.001492, - 0.000324, - 0.001583 - ], - "rotation_deg": [ - -0.049723, - -0.013278, - 0.03842 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 683, - "type": "imguizmo", - "translation": [ - -0.001465, - 0.000354, - 0.001588 - ], - "rotation_deg": [ - -0.048819, - -0.010524, - 0.039025 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 684, - "type": "imguizmo", - "translation": [ - -0.001436, - 0.000384, - 0.001593 - ], - "rotation_deg": [ - -0.047881, - -0.007763, - 0.039603 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 685, - "type": "imguizmo", - "translation": [ - -0.001407, - 0.000414, - 0.001596 - ], - "rotation_deg": [ - -0.04691, - -0.004996, - 0.040153 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 686, - "type": "imguizmo", - "translation": [ - -0.001377, - 0.000444, - 0.001599 - ], - "rotation_deg": [ - -0.045906, - -0.002226, - 0.040676 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 687, - "type": "imguizmo", - "translation": [ - -0.001346, - 0.000473, - 0.0016 - ], - "rotation_deg": [ - -0.04487, - 0.000546, - 0.041169 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 688, - "type": "imguizmo", - "translation": [ - -0.001314, - 0.000502, - 0.0016 - ], - "rotation_deg": [ - -0.043803, - 0.003318, - 0.041635 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 689, - "type": "imguizmo", - "translation": [ - -0.001281, - 0.000531, - 0.001599 - ], - "rotation_deg": [ - -0.042705, - 0.006087, - 0.042071 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 690, - "type": "imguizmo", - "translation": [ - -0.001247, - 0.000559, - 0.001597 - ], - "rotation_deg": [ - -0.041578, - 0.008852, - 0.042478 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 691, - "type": "imguizmo", - "translation": [ - -0.001213, - 0.000587, - 0.001594 - ], - "rotation_deg": [ - -0.040422, - 0.01161, - 0.042855 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 692, - "type": "imguizmo", - "translation": [ - -0.001177, - 0.000614, - 0.001589 - ], - "rotation_deg": [ - -0.039237, - 0.014361, - 0.043203 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 693, - "type": "imguizmo", - "translation": [ - -0.001141, - 0.000641, - 0.001584 - ], - "rotation_deg": [ - -0.038025, - 0.017102, - 0.04352 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 694, - "type": "imguizmo", - "translation": [ - -0.001104, - 0.000668, - 0.001577 - ], - "rotation_deg": [ - -0.036787, - 0.01983, - 0.043807 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 695, - "type": "imguizmo", - "translation": [ - -0.001066, - 0.000694, - 0.001569 - ], - "rotation_deg": [ - -0.035523, - 0.022545, - 0.044063 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 696, - "type": "imguizmo", - "translation": [ - -0.001027, - 0.000719, - 0.001561 - ], - "rotation_deg": [ - -0.034234, - 0.025244, - 0.044289 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 697, - "type": "imguizmo", - "translation": [ - -0.000988, - 0.000744, - 0.001551 - ], - "rotation_deg": [ - -0.032922, - 0.027926, - 0.044484 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 698, - "type": "imguizmo", - "translation": [ - -0.000948, - 0.000769, - 0.00154 - ], - "rotation_deg": [ - -0.031586, - 0.030588, - 0.044648 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 699, - "type": "imguizmo", - "translation": [ - -0.000907, - 0.000793, - 0.001528 - ], - "rotation_deg": [ - -0.030228, - 0.033229, - 0.044781 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 700, - "type": "imguizmo", - "translation": [ - -0.000865, - 0.000817, - 0.001515 - ], - "rotation_deg": [ - -0.02885, - 0.035847, - 0.044882 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 701, - "type": "imguizmo", - "translation": [ - -0.000824, - 0.000839, - 0.001501 - ], - "rotation_deg": [ - -0.027451, - 0.038439, - 0.044952 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 702, - "type": "imguizmo", - "translation": [ - -0.000781, - 0.000862, - 0.001486 - ], - "rotation_deg": [ - -0.026033, - 0.041005, - 0.044991 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 703, - "type": "imguizmo", - "translation": [ - -0.000738, - 0.000884, - 0.001469 - ], - "rotation_deg": [ - -0.024597, - 0.043542, - 0.044999 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 704, - "type": "imguizmo", - "translation": [ - -0.000694, - 0.000905, - 0.001452 - ], - "rotation_deg": [ - -0.023144, - 0.046049, - 0.044975 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 705, - "type": "imguizmo", - "translation": [ - -0.00065, - 0.000925, - 0.001434 - ], - "rotation_deg": [ - -0.021674, - 0.048524, - 0.04492 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 706, - "type": "imguizmo", - "translation": [ - -0.000606, - 0.000945, - 0.001415 - ], - "rotation_deg": [ - -0.02019, - 0.050965, - 0.044834 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 707, - "type": "imguizmo", - "translation": [ - -0.000561, - 0.000964, - 0.001395 - ], - "rotation_deg": [ - -0.018692, - 0.053371, - 0.044716 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 708, - "type": "imguizmo", - "translation": [ - -0.000515, - 0.000983, - 0.001373 - ], - "rotation_deg": [ - -0.01718, - 0.055739, - 0.044567 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 709, - "type": "imguizmo", - "translation": [ - -0.00047, - 0.001001, - 0.001351 - ], - "rotation_deg": [ - -0.015657, - 0.058068, - 0.044387 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 710, - "type": "imguizmo", - "translation": [ - -0.000424, - 0.001018, - 0.001328 - ], - "rotation_deg": [ - -0.014122, - 0.060358, - 0.044176 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 711, - "type": "imguizmo", - "translation": [ - -0.000377, - 0.001034, - 0.001304 - ], - "rotation_deg": [ - -0.012578, - 0.062604, - 0.043935 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 712, - "type": "imguizmo", - "translation": [ - -0.000331, - 0.00105, - 0.001279 - ], - "rotation_deg": [ - -0.011025, - 0.064808, - 0.043663 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 713, - "type": "imguizmo", - "translation": [ - -0.000284, - 0.001065, - 0.001253 - ], - "rotation_deg": [ - -0.009464, - 0.066966, - 0.04336 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 714, - "type": "imguizmo", - "translation": [ - -0.000237, - 0.001079, - 0.001227 - ], - "rotation_deg": [ - -0.007897, - 0.069077, - 0.043027 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 715, - "type": "imguizmo", - "translation": [ - -0.00019, - 0.001093, - 0.001199 - ], - "rotation_deg": [ - -0.006324, - 0.071141, - 0.042664 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 716, - "type": "imguizmo", - "translation": [ - -0.000142, - 0.001105, - 0.001171 - ], - "rotation_deg": [ - -0.004747, - 0.073154, - 0.042272 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 717, - "type": "imguizmo", - "translation": [ - -9.5E-05, - 0.001117, - 0.001142 - ], - "rotation_deg": [ - -0.003167, - 0.075117, - 0.04185 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 718, - "type": "imguizmo", - "translation": [ - -4.8E-05, - 0.001128, - 0.001112 - ], - "rotation_deg": [ - -0.001584, - 0.077028, - 0.041398 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 719, - "type": "imguizmo", - "translation": [ - -0.0, - 0.001139, - 0.001081 - ], - "rotation_deg": [ - -0.0, - 0.078884, - 0.040918 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 720, - "type": "action", - "action": "set_active_render_window", - "value": 0 - }, - { - "frame": 720, - "type": "action", - "action": "set_active_planar", - "value": 3 - }, - { - "frame": 720, - "type": "action", - "action": "set_projection_type", - "value": "perspective" - }, - { - "frame": 720, - "type": "action", - "action": "set_left_handed", - "value": true - }, - { - "frame": 720, - "type": "action", - "action": "reset_active_camera", - "value": 1 - }, - { - "frame": 720, - "type": "action", - "action": "set_active_render_window", - "value": 1 - }, - { - "frame": 720, - "type": "action", - "action": "set_active_planar", - "value": 3 - }, - { - "frame": 720, - "type": "action", - "action": "set_projection_type", - "value": "orthographic" - }, - { - "frame": 720, - "type": "action", - "action": "set_left_handed", - "value": true - }, - { - "frame": 720, - "type": "action", - "action": "set_active_render_window", - "value": 0 - }, - { - "frame": 721, - "type": "imguizmo", - "translation": [ - 0, - 2.5, - 1.8 - ], - "rotation_deg": [ - 0, - 100, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 722, - "type": "imguizmo", - "translation": [ - 0.100308, - 2.499129, - 1.829026 - ], - "rotation_deg": [ - 1.58326, - 99.991289, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 723, - "type": "imguizmo", - "translation": [ - 0.200546, - 2.496516, - 1.857972 - ], - "rotation_deg": [ - 3.162108, - 99.96516, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 724, - "type": "imguizmo", - "translation": [ - 0.300645, - 2.492163, - 1.886756 - ], - "rotation_deg": [ - 4.732142, - 99.921633, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 725, - "type": "imguizmo", - "translation": [ - 0.400534, - 2.486074, - 1.915298 - ], - "rotation_deg": [ - 6.288987, - 99.860738, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 726, - "type": "imguizmo", - "translation": [ - 0.500143, - 2.478252, - 1.943519 - ], - "rotation_deg": [ - 7.828303, - 99.782517, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 727, - "type": "imguizmo", - "translation": [ - 0.599405, - 2.468702, - 1.97134 - ], - "rotation_deg": [ - 9.345801, - 99.687025, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 728, - "type": "imguizmo", - "translation": [ - 0.698248, - 2.457433, - 1.998683 - ], - "rotation_deg": [ - 10.83725, - 99.574327, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 729, - "type": "imguizmo", - "translation": [ - 0.796605, - 2.44445, - 2.025472 - ], - "rotation_deg": [ - 12.298494, - 99.444504, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 730, - "type": "imguizmo", - "translation": [ - 0.894407, - 2.429764, - 2.051633 - ], - "rotation_deg": [ - 13.725459, - 99.297645, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 731, - "type": "imguizmo", - "translation": [ - 0.991585, - 2.413385, - 2.077093 - ], - "rotation_deg": [ - 15.11417, - 99.133852, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 732, - "type": "imguizmo", - "translation": [ - 1.088072, - 2.395324, - 2.10178 - ], - "rotation_deg": [ - 16.460754, - 98.95324, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 733, - "type": "imguizmo", - "translation": [ - 1.183801, - 2.375594, - 2.125627 - ], - "rotation_deg": [ - 17.761459, - 98.755935, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 734, - "type": "imguizmo", - "translation": [ - 1.278705, - 2.354207, - 2.148565 - ], - "rotation_deg": [ - 19.01266, - 98.542074, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 735, - "type": "imguizmo", - "translation": [ - 1.372718, - 2.331181, - 2.170533 - ], - "rotation_deg": [ - 20.210869, - 98.311806, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 736, - "type": "imguizmo", - "translation": [ - 1.465775, - 2.306529, - 2.191467 - ], - "rotation_deg": [ - 21.352747, - 98.065291, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 737, - "type": "imguizmo", - "translation": [ - 1.557809, - 2.28027, - 2.21131 - ], - "rotation_deg": [ - 22.435111, - 97.802702, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 738, - "type": "imguizmo", - "translation": [ - 1.648758, - 2.252422, - 2.230007 - ], - "rotation_deg": [ - 23.454944, - 97.524222, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 739, - "type": "imguizmo", - "translation": [ - 1.738558, - 2.223004, - 2.247506 - ], - "rotation_deg": [ - 24.409405, - 97.230044, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 740, - "type": "imguizmo", - "translation": [ - 1.827147, - 2.192037, - 2.263757 - ], - "rotation_deg": [ - 25.295831, - 96.920373, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 741, - "type": "imguizmo", - "translation": [ - 1.914461, - 2.159543, - 2.278715 - ], - "rotation_deg": [ - 26.111754, - 96.595426, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 742, - "type": "imguizmo", - "translation": [ - 2.000442, - 2.125543, - 2.29234 - ], - "rotation_deg": [ - 26.854899, - 96.255428, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 743, - "type": "imguizmo", - "translation": [ - 2.085029, - 2.090062, - 2.304592 - ], - "rotation_deg": [ - 27.523194, - 95.900618, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 744, - "type": "imguizmo", - "translation": [ - 2.168162, - 2.053124, - 2.315438 - ], - "rotation_deg": [ - 28.114777, - 95.531241, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 745, - "type": "imguizmo", - "translation": [ - 2.249785, - 2.014756, - 2.324847 - ], - "rotation_deg": [ - 28.627999, - 95.147556, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 746, - "type": "imguizmo", - "translation": [ - 2.329839, - 1.974983, - 2.332793 - ], - "rotation_deg": [ - 29.06143, - 94.749829, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 747, - "type": "imguizmo", - "translation": [ - 2.40827, - 1.933834, - 2.339254 - ], - "rotation_deg": [ - 29.413861, - 94.338339, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 748, - "type": "imguizmo", - "translation": [ - 2.485023, - 1.891337, - 2.344212 - ], - "rotation_deg": [ - 29.684311, - 93.913372, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 749, - "type": "imguizmo", - "translation": [ - 2.560043, - 1.847522, - 2.347654 - ], - "rotation_deg": [ - 29.872025, - 93.475223, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 750, - "type": "imguizmo", - "translation": [ - 2.63328, - 1.80242, - 2.349569 - ], - "rotation_deg": [ - 29.976481, - 93.024199, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 751, - "type": "imguizmo", - "translation": [ - 2.704681, - 1.756061, - 2.349952 - ], - "rotation_deg": [ - 29.997386, - 92.560613, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 752, - "type": "imguizmo", - "translation": [ - 2.774198, - 1.708479, - 2.348803 - ], - "rotation_deg": [ - 29.934684, - 92.084789, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 753, - "type": "imguizmo", - "translation": [ - 2.841781, - 1.659706, - 2.346123 - ], - "rotation_deg": [ - 29.788548, - 91.597059, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 754, - "type": "imguizmo", - "translation": [ - 2.907383, - 1.609776, - 2.341922 - ], - "rotation_deg": [ - 29.559386, - 91.097761, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 755, - "type": "imguizmo", - "translation": [ - 2.97096, - 1.558725, - 2.33621 - ], - "rotation_deg": [ - 29.247837, - 90.587245, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 756, - "type": "imguizmo", - "translation": [ - 3.032465, - 1.506587, - 2.329004 - ], - "rotation_deg": [ - 28.854769, - 90.065866, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 757, - "type": "imguizmo", - "translation": [ - 3.091858, - 1.453399, - 2.320323 - ], - "rotation_deg": [ - 28.381278, - 89.533987, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 758, - "type": "imguizmo", - "translation": [ - 3.149096, - 1.399198, - 2.310193 - ], - "rotation_deg": [ - 27.828683, - 88.991979, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 759, - "type": "imguizmo", - "translation": [ - 3.204139, - 1.344022, - 2.29864 - ], - "rotation_deg": [ - 27.198524, - 88.44022, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 760, - "type": "imguizmo", - "translation": [ - 3.256949, - 1.287909, - 2.285697 - ], - "rotation_deg": [ - 26.492558, - 87.879095, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 761, - "type": "imguizmo", - "translation": [ - 3.307489, - 1.230899, - 2.2714 - ], - "rotation_deg": [ - 25.712753, - 87.308993, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 762, - "type": "imguizmo", - "translation": [ - 3.355724, - 1.173031, - 2.25579 - ], - "rotation_deg": [ - 24.861281, - 86.730314, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 763, - "type": "imguizmo", - "translation": [ - 3.401621, - 1.114346, - 2.238909 - ], - "rotation_deg": [ - 23.940517, - 86.143459, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 764, - "type": "imguizmo", - "translation": [ - 3.445146, - 1.054884, - 2.220805 - ], - "rotation_deg": [ - 22.953026, - 85.548838, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 765, - "type": "imguizmo", - "translation": [ - 3.486271, - 0.994687, - 2.201529 - ], - "rotation_deg": [ - 21.901561, - 84.946866, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 766, - "type": "imguizmo", - "translation": [ - 3.524966, - 0.933796, - 2.181133 - ], - "rotation_deg": [ - 20.789052, - 84.337961, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 767, - "type": "imguizmo", - "translation": [ - 3.561205, - 0.872255, - 2.159674 - ], - "rotation_deg": [ - 19.618601, - 83.722549, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 768, - "type": "imguizmo", - "translation": [ - 3.594962, - 0.810106, - 2.137214 - ], - "rotation_deg": [ - 18.393469, - 83.101057, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 769, - "type": "imguizmo", - "translation": [ - 3.626213, - 0.747392, - 2.113813 - ], - "rotation_deg": [ - 17.117071, - 82.47392, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 770, - "type": "imguizmo", - "translation": [ - 3.654937, - 0.684157, - 2.089538 - ], - "rotation_deg": [ - 15.792965, - 81.841575, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 771, - "type": "imguizmo", - "translation": [ - 3.681114, - 0.620446, - 2.064455 - ], - "rotation_deg": [ - 14.424841, - 81.204461, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 772, - "type": "imguizmo", - "translation": [ - 3.704726, - 0.556302, - 2.038636 - ], - "rotation_deg": [ - 13.016512, - 80.563023, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 773, - "type": "imguizmo", - "translation": [ - 3.725756, - 0.491771, - 2.012152 - ], - "rotation_deg": [ - 11.571904, - 79.917709, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 774, - "type": "imguizmo", - "translation": [ - 3.744189, - 0.426897, - 1.985076 - ], - "rotation_deg": [ - 10.095043, - 79.268967, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 775, - "type": "imguizmo", - "translation": [ - 3.760013, - 0.361725, - 1.957484 - ], - "rotation_deg": [ - 8.590045, - 78.61725, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 776, - "type": "imguizmo", - "translation": [ - 3.773216, - 0.296301, - 1.929454 - ], - "rotation_deg": [ - 7.061106, - 77.963012, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 777, - "type": "imguizmo", - "translation": [ - 3.78379, - 0.230671, - 1.901062 - ], - "rotation_deg": [ - 5.512486, - 77.306709, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 778, - "type": "imguizmo", - "translation": [ - 3.791727, - 0.16488, - 1.872389 - ], - "rotation_deg": [ - 3.948501, - 76.648799, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 779, - "type": "imguizmo", - "translation": [ - 3.797021, - 0.098974, - 1.843514 - ], - "rotation_deg": [ - 2.373511, - 75.989739, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 780, - "type": "imguizmo", - "translation": [ - 3.799669, - 0.032999, - 1.814518 - ], - "rotation_deg": [ - 0.791906, - 75.32999, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 781, - "type": "imguizmo", - "translation": [ - 3.799669, - -0.032999, - 1.785482 - ], - "rotation_deg": [ - -0.791906, - 74.67001, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 782, - "type": "imguizmo", - "translation": [ - 3.797021, - -0.098974, - 1.756486 - ], - "rotation_deg": [ - -2.373511, - 74.010261, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 783, - "type": "imguizmo", - "translation": [ - 3.791727, - -0.16488, - 1.727611 - ], - "rotation_deg": [ - -3.948501, - 73.351201, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 784, - "type": "imguizmo", - "translation": [ - 3.78379, - -0.230671, - 1.698938 - ], - "rotation_deg": [ - -5.512486, - 72.693291, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 785, - "type": "imguizmo", - "translation": [ - 3.773216, - -0.296301, - 1.670546 - ], - "rotation_deg": [ - -7.061106, - 72.036988, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 786, - "type": "imguizmo", - "translation": [ - 3.760013, - -0.361725, - 1.642516 - ], - "rotation_deg": [ - -8.590045, - 71.38275, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 787, - "type": "imguizmo", - "translation": [ - 3.744189, - -0.426897, - 1.614924 - ], - "rotation_deg": [ - -10.095043, - 70.731033, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 788, - "type": "imguizmo", - "translation": [ - 3.725756, - -0.491771, - 1.587848 - ], - "rotation_deg": [ - -11.571904, - 70.082291, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 789, - "type": "imguizmo", - "translation": [ - 3.704726, - -0.556302, - 1.561364 - ], - "rotation_deg": [ - -13.016512, - 69.436977, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 790, - "type": "imguizmo", - "translation": [ - 3.681114, - -0.620446, - 1.535545 - ], - "rotation_deg": [ - -14.424841, - 68.795539, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 791, - "type": "imguizmo", - "translation": [ - 3.654937, - -0.684157, - 1.510462 - ], - "rotation_deg": [ - -15.792965, - 68.158425, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 792, - "type": "imguizmo", - "translation": [ - 3.626213, - -0.747392, - 1.486187 - ], - "rotation_deg": [ - -17.117071, - 67.52608, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 793, - "type": "imguizmo", - "translation": [ - 3.594962, - -0.810106, - 1.462786 - ], - "rotation_deg": [ - -18.393469, - 66.898943, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 794, - "type": "imguizmo", - "translation": [ - 3.561205, - -0.872255, - 1.440326 - ], - "rotation_deg": [ - -19.618601, - 66.277451, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 795, - "type": "imguizmo", - "translation": [ - 3.524966, - -0.933796, - 1.418867 - ], - "rotation_deg": [ - -20.789052, - 65.662039, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 796, - "type": "imguizmo", - "translation": [ - 3.486271, - -0.994687, - 1.398471 - ], - "rotation_deg": [ - -21.901561, - 65.053134, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 797, - "type": "imguizmo", - "translation": [ - 3.445146, - -1.054884, - 1.379195 - ], - "rotation_deg": [ - -22.953026, - 64.451162, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 798, - "type": "imguizmo", - "translation": [ - 3.401621, - -1.114346, - 1.361091 - ], - "rotation_deg": [ - -23.940517, - 63.856541, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 799, - "type": "imguizmo", - "translation": [ - 3.355724, - -1.173031, - 1.34421 - ], - "rotation_deg": [ - -24.861281, - 63.269686, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 800, - "type": "imguizmo", - "translation": [ - 3.307489, - -1.230899, - 1.3286 - ], - "rotation_deg": [ - -25.712753, - 62.691007, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 801, - "type": "imguizmo", - "translation": [ - 3.256949, - -1.287909, - 1.314303 - ], - "rotation_deg": [ - -26.492558, - 62.120905, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 802, - "type": "imguizmo", - "translation": [ - 3.204139, - -1.344022, - 1.30136 - ], - "rotation_deg": [ - -27.198524, - 61.55978, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 803, - "type": "imguizmo", - "translation": [ - 3.149096, - -1.399198, - 1.289807 - ], - "rotation_deg": [ - -27.828683, - 61.008021, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 804, - "type": "imguizmo", - "translation": [ - 3.091858, - -1.453399, - 1.279677 - ], - "rotation_deg": [ - -28.381278, - 60.466013, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 805, - "type": "imguizmo", - "translation": [ - 3.032465, - -1.506587, - 1.270996 - ], - "rotation_deg": [ - -28.854769, - 59.934134, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 806, - "type": "imguizmo", - "translation": [ - 2.97096, - -1.558725, - 1.26379 - ], - "rotation_deg": [ - -29.247837, - 59.412755, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 807, - "type": "imguizmo", - "translation": [ - 2.907383, - -1.609776, - 1.258078 - ], - "rotation_deg": [ - -29.559386, - 58.902239, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 808, - "type": "imguizmo", - "translation": [ - 2.841781, - -1.659706, - 1.253877 - ], - "rotation_deg": [ - -29.788548, - 58.402941, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 809, - "type": "imguizmo", - "translation": [ - 2.774198, - -1.708479, - 1.251197 - ], - "rotation_deg": [ - -29.934684, - 57.915211, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 810, - "type": "imguizmo", - "translation": [ - 2.704681, - -1.756061, - 1.250048 - ], - "rotation_deg": [ - -29.997386, - 57.439387, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 811, - "type": "imguizmo", - "translation": [ - 2.63328, - -1.80242, - 1.250431 - ], - "rotation_deg": [ - -29.976481, - 56.975801, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 812, - "type": "imguizmo", - "translation": [ - 2.560043, - -1.847522, - 1.252346 - ], - "rotation_deg": [ - -29.872025, - 56.524777, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 813, - "type": "imguizmo", - "translation": [ - 2.485023, - -1.891337, - 1.255788 - ], - "rotation_deg": [ - -29.684311, - 56.086628, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 814, - "type": "imguizmo", - "translation": [ - 2.40827, - -1.933834, - 1.260746 - ], - "rotation_deg": [ - -29.413861, - 55.661661, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 815, - "type": "imguizmo", - "translation": [ - 2.329839, - -1.974983, - 1.267207 - ], - "rotation_deg": [ - -29.06143, - 55.250171, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 816, - "type": "imguizmo", - "translation": [ - 2.249785, - -2.014756, - 1.275153 - ], - "rotation_deg": [ - -28.627999, - 54.852444, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 817, - "type": "imguizmo", - "translation": [ - 2.168162, - -2.053124, - 1.284562 - ], - "rotation_deg": [ - -28.114777, - 54.468759, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 818, - "type": "imguizmo", - "translation": [ - 2.085029, - -2.090062, - 1.295408 - ], - "rotation_deg": [ - -27.523194, - 54.099382, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 819, - "type": "imguizmo", - "translation": [ - 2.000442, - -2.125543, - 1.30766 - ], - "rotation_deg": [ - -26.854899, - 53.744572, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 820, - "type": "imguizmo", - "translation": [ - 1.914461, - -2.159543, - 1.321285 - ], - "rotation_deg": [ - -26.111754, - 53.404574, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 821, - "type": "imguizmo", - "translation": [ - 1.827147, - -2.192037, - 1.336243 - ], - "rotation_deg": [ - -25.295831, - 53.079627, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 822, - "type": "imguizmo", - "translation": [ - 1.738558, - -2.223004, - 1.352494 - ], - "rotation_deg": [ - -24.409405, - 52.769956, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 823, - "type": "imguizmo", - "translation": [ - 1.648758, - -2.252422, - 1.369993 - ], - "rotation_deg": [ - -23.454944, - 52.475778, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 824, - "type": "imguizmo", - "translation": [ - 1.557809, - -2.28027, - 1.38869 - ], - "rotation_deg": [ - -22.435111, - 52.197298, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 825, - "type": "imguizmo", - "translation": [ - 1.465775, - -2.306529, - 1.408533 - ], - "rotation_deg": [ - -21.352747, - 51.934709, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 826, - "type": "imguizmo", - "translation": [ - 1.372718, - -2.331181, - 1.429467 - ], - "rotation_deg": [ - -20.210869, - 51.688194, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 827, - "type": "imguizmo", - "translation": [ - 1.278705, - -2.354207, - 1.451435 - ], - "rotation_deg": [ - -19.01266, - 51.457926, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 828, - "type": "imguizmo", - "translation": [ - 1.183801, - -2.375594, - 1.474373 - ], - "rotation_deg": [ - -17.761459, - 51.244065, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 829, - "type": "imguizmo", - "translation": [ - 1.088072, - -2.395324, - 1.49822 - ], - "rotation_deg": [ - -16.460754, - 51.04676, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 830, - "type": "imguizmo", - "translation": [ - 0.991585, - -2.413385, - 1.522907 - ], - "rotation_deg": [ - -15.11417, - 50.866148, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 831, - "type": "imguizmo", - "translation": [ - 0.894407, - -2.429764, - 1.548367 - ], - "rotation_deg": [ - -13.725459, - 50.702355, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 832, - "type": "imguizmo", - "translation": [ - 0.796605, - -2.44445, - 1.574528 - ], - "rotation_deg": [ - -12.298494, - 50.555496, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 833, - "type": "imguizmo", - "translation": [ - 0.698248, - -2.457433, - 1.601317 - ], - "rotation_deg": [ - -10.83725, - 50.425673, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 834, - "type": "imguizmo", - "translation": [ - 0.599405, - -2.468702, - 1.62866 - ], - "rotation_deg": [ - -9.345801, - 50.312975, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 835, - "type": "imguizmo", - "translation": [ - 0.500143, - -2.478252, - 1.656481 - ], - "rotation_deg": [ - -7.828303, - 50.217483, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 836, - "type": "imguizmo", - "translation": [ - 0.400534, - -2.486074, - 1.684702 - ], - "rotation_deg": [ - -6.288987, - 50.139262, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 837, - "type": "imguizmo", - "translation": [ - 0.300645, - -2.492163, - 1.713244 - ], - "rotation_deg": [ - -4.732142, - 50.078367, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 838, - "type": "imguizmo", - "translation": [ - 0.200546, - -2.496516, - 1.742028 - ], - "rotation_deg": [ - -3.162108, - 50.03484, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 839, - "type": "imguizmo", - "translation": [ - 0.100308, - -2.499129, - 1.770974 - ], - "rotation_deg": [ - -1.58326, - 50.008711, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 840, - "type": "imguizmo", - "translation": [ - 0, - -2.5, - 1.8 - ], - "rotation_deg": [ - 0, - 50, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 841, - "type": "imguizmo", - "translation": [ - -0.100308, - -2.499129, - 1.829026 - ], - "rotation_deg": [ - 1.58326, - 50.008711, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 842, - "type": "imguizmo", - "translation": [ - -0.200546, - -2.496516, - 1.857972 - ], - "rotation_deg": [ - 3.162108, - 50.03484, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 843, - "type": "imguizmo", - "translation": [ - -0.300645, - -2.492163, - 1.886756 - ], - "rotation_deg": [ - 4.732142, - 50.078367, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 844, - "type": "imguizmo", - "translation": [ - -0.400534, - -2.486074, - 1.915298 - ], - "rotation_deg": [ - 6.288987, - 50.139262, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 845, - "type": "imguizmo", - "translation": [ - -0.500143, - -2.478252, - 1.943519 - ], - "rotation_deg": [ - 7.828303, - 50.217483, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 846, - "type": "imguizmo", - "translation": [ - -0.599405, - -2.468702, - 1.97134 - ], - "rotation_deg": [ - 9.345801, - 50.312975, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 847, - "type": "imguizmo", - "translation": [ - -0.698248, - -2.457433, - 1.998683 - ], - "rotation_deg": [ - 10.83725, - 50.425673, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 848, - "type": "imguizmo", - "translation": [ - -0.796605, - -2.44445, - 2.025472 - ], - "rotation_deg": [ - 12.298494, - 50.555496, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 849, - "type": "imguizmo", - "translation": [ - -0.894407, - -2.429764, - 2.051633 - ], - "rotation_deg": [ - 13.725459, - 50.702355, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 850, - "type": "imguizmo", - "translation": [ - -0.991585, - -2.413385, - 2.077093 - ], - "rotation_deg": [ - 15.11417, - 50.866148, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 851, - "type": "imguizmo", - "translation": [ - -1.088072, - -2.395324, - 2.10178 - ], - "rotation_deg": [ - 16.460754, - 51.04676, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 852, - "type": "imguizmo", - "translation": [ - -1.183801, - -2.375594, - 2.125627 - ], - "rotation_deg": [ - 17.761459, - 51.244065, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 853, - "type": "imguizmo", - "translation": [ - -1.278705, - -2.354207, - 2.148565 - ], - "rotation_deg": [ - 19.01266, - 51.457926, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 854, - "type": "imguizmo", - "translation": [ - -1.372718, - -2.331181, - 2.170533 - ], - "rotation_deg": [ - 20.210869, - 51.688194, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 855, - "type": "imguizmo", - "translation": [ - -1.465775, - -2.306529, - 2.191467 - ], - "rotation_deg": [ - 21.352747, - 51.934709, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 856, - "type": "imguizmo", - "translation": [ - -1.557809, - -2.28027, - 2.21131 - ], - "rotation_deg": [ - 22.435111, - 52.197298, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 857, - "type": "imguizmo", - "translation": [ - -1.648758, - -2.252422, - 2.230007 - ], - "rotation_deg": [ - 23.454944, - 52.475778, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 858, - "type": "imguizmo", - "translation": [ - -1.738558, - -2.223004, - 2.247506 - ], - "rotation_deg": [ - 24.409405, - 52.769956, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 859, - "type": "imguizmo", - "translation": [ - -1.827147, - -2.192037, - 2.263757 - ], - "rotation_deg": [ - 25.295831, - 53.079627, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 860, - "type": "imguizmo", - "translation": [ - -1.914461, - -2.159543, - 2.278715 - ], - "rotation_deg": [ - 26.111754, - 53.404574, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 861, - "type": "imguizmo", - "translation": [ - -2.000442, - -2.125543, - 2.29234 - ], - "rotation_deg": [ - 26.854899, - 53.744572, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 862, - "type": "imguizmo", - "translation": [ - -2.085029, - -2.090062, - 2.304592 - ], - "rotation_deg": [ - 27.523194, - 54.099382, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 863, - "type": "imguizmo", - "translation": [ - -2.168162, - -2.053124, - 2.315438 - ], - "rotation_deg": [ - 28.114777, - 54.468759, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 864, - "type": "imguizmo", - "translation": [ - -2.249785, - -2.014756, - 2.324847 - ], - "rotation_deg": [ - 28.627999, - 54.852444, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 865, - "type": "imguizmo", - "translation": [ - -2.329839, - -1.974983, - 2.332793 - ], - "rotation_deg": [ - 29.06143, - 55.250171, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 866, - "type": "imguizmo", - "translation": [ - -2.40827, - -1.933834, - 2.339254 - ], - "rotation_deg": [ - 29.413861, - 55.661661, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 867, - "type": "imguizmo", - "translation": [ - -2.485023, - -1.891337, - 2.344212 - ], - "rotation_deg": [ - 29.684311, - 56.086628, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 868, - "type": "imguizmo", - "translation": [ - -2.560043, - -1.847522, - 2.347654 - ], - "rotation_deg": [ - 29.872025, - 56.524777, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 869, - "type": "imguizmo", - "translation": [ - -2.63328, - -1.80242, - 2.349569 - ], - "rotation_deg": [ - 29.976481, - 56.975801, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 870, - "type": "imguizmo", - "translation": [ - -2.704681, - -1.756061, - 2.349952 - ], - "rotation_deg": [ - 29.997386, - 57.439387, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 871, - "type": "imguizmo", - "translation": [ - -2.774198, - -1.708479, - 2.348803 - ], - "rotation_deg": [ - 29.934684, - 57.915211, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 872, - "type": "imguizmo", - "translation": [ - -2.841781, - -1.659706, - 2.346123 - ], - "rotation_deg": [ - 29.788548, - 58.402941, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 873, - "type": "imguizmo", - "translation": [ - -2.907383, - -1.609776, - 2.341922 - ], - "rotation_deg": [ - 29.559386, - 58.902239, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 874, - "type": "imguizmo", - "translation": [ - -2.97096, - -1.558725, - 2.33621 - ], - "rotation_deg": [ - 29.247837, - 59.412755, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 875, - "type": "imguizmo", - "translation": [ - -3.032465, - -1.506587, - 2.329004 - ], - "rotation_deg": [ - 28.854769, - 59.934134, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 876, - "type": "imguizmo", - "translation": [ - -3.091858, - -1.453399, - 2.320323 - ], - "rotation_deg": [ - 28.381278, - 60.466013, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 877, - "type": "imguizmo", - "translation": [ - -3.149096, - -1.399198, - 2.310193 - ], - "rotation_deg": [ - 27.828683, - 61.008021, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 878, - "type": "imguizmo", - "translation": [ - -3.204139, - -1.344022, - 2.29864 - ], - "rotation_deg": [ - 27.198524, - 61.55978, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 879, - "type": "imguizmo", - "translation": [ - -3.256949, - -1.287909, - 2.285697 - ], - "rotation_deg": [ - 26.492558, - 62.120905, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 880, - "type": "imguizmo", - "translation": [ - -3.307489, - -1.230899, - 2.2714 - ], - "rotation_deg": [ - 25.712753, - 62.691007, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 881, - "type": "imguizmo", - "translation": [ - -3.355724, - -1.173031, - 2.25579 - ], - "rotation_deg": [ - 24.861281, - 63.269686, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 882, - "type": "imguizmo", - "translation": [ - -3.401621, - -1.114346, - 2.238909 - ], - "rotation_deg": [ - 23.940517, - 63.856541, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 883, - "type": "imguizmo", - "translation": [ - -3.445146, - -1.054884, - 2.220805 - ], - "rotation_deg": [ - 22.953026, - 64.451162, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 884, - "type": "imguizmo", - "translation": [ - -3.486271, - -0.994687, - 2.201529 - ], - "rotation_deg": [ - 21.901561, - 65.053134, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 885, - "type": "imguizmo", - "translation": [ - -3.524966, - -0.933796, - 2.181133 - ], - "rotation_deg": [ - 20.789052, - 65.662039, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 886, - "type": "imguizmo", - "translation": [ - -3.561205, - -0.872255, - 2.159674 - ], - "rotation_deg": [ - 19.618601, - 66.277451, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 887, - "type": "imguizmo", - "translation": [ - -3.594962, - -0.810106, - 2.137214 - ], - "rotation_deg": [ - 18.393469, - 66.898943, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 888, - "type": "imguizmo", - "translation": [ - -3.626213, - -0.747392, - 2.113813 - ], - "rotation_deg": [ - 17.117071, - 67.52608, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 889, - "type": "imguizmo", - "translation": [ - -3.654937, - -0.684157, - 2.089538 - ], - "rotation_deg": [ - 15.792965, - 68.158425, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 890, - "type": "imguizmo", - "translation": [ - -3.681114, - -0.620446, - 2.064455 - ], - "rotation_deg": [ - 14.424841, - 68.795539, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 891, - "type": "imguizmo", - "translation": [ - -3.704726, - -0.556302, - 2.038636 - ], - "rotation_deg": [ - 13.016512, - 69.436977, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 892, - "type": "imguizmo", - "translation": [ - -3.725756, - -0.491771, - 2.012152 - ], - "rotation_deg": [ - 11.571904, - 70.082291, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 893, - "type": "imguizmo", - "translation": [ - -3.744189, - -0.426897, - 1.985076 - ], - "rotation_deg": [ - 10.095043, - 70.731033, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 894, - "type": "imguizmo", - "translation": [ - -3.760013, - -0.361725, - 1.957484 - ], - "rotation_deg": [ - 8.590045, - 71.38275, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 895, - "type": "imguizmo", - "translation": [ - -3.773216, - -0.296301, - 1.929454 - ], - "rotation_deg": [ - 7.061106, - 72.036988, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 896, - "type": "imguizmo", - "translation": [ - -3.78379, - -0.230671, - 1.901062 - ], - "rotation_deg": [ - 5.512486, - 72.693291, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 897, - "type": "imguizmo", - "translation": [ - -3.791727, - -0.16488, - 1.872389 - ], - "rotation_deg": [ - 3.948501, - 73.351201, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 898, - "type": "imguizmo", - "translation": [ - -3.797021, - -0.098974, - 1.843514 - ], - "rotation_deg": [ - 2.373511, - 74.010261, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 899, - "type": "imguizmo", - "translation": [ - -3.799669, - -0.032999, - 1.814518 - ], - "rotation_deg": [ - 0.791906, - 74.67001, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 900, - "type": "imguizmo", - "translation": [ - -3.799669, - 0.032999, - 1.785482 - ], - "rotation_deg": [ - -0.791906, - 75.32999, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 901, - "type": "imguizmo", - "translation": [ - -3.797021, - 0.098974, - 1.756486 - ], - "rotation_deg": [ - -2.373511, - 75.989739, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 902, - "type": "imguizmo", - "translation": [ - -3.791727, - 0.16488, - 1.727611 - ], - "rotation_deg": [ - -3.948501, - 76.648799, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 903, - "type": "imguizmo", - "translation": [ - -3.78379, - 0.230671, - 1.698938 - ], - "rotation_deg": [ - -5.512486, - 77.306709, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 904, - "type": "imguizmo", - "translation": [ - -3.773216, - 0.296301, - 1.670546 - ], - "rotation_deg": [ - -7.061106, - 77.963012, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 905, - "type": "imguizmo", - "translation": [ - -3.760013, - 0.361725, - 1.642516 - ], - "rotation_deg": [ - -8.590045, - 78.61725, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 906, - "type": "imguizmo", - "translation": [ - -3.744189, - 0.426897, - 1.614924 - ], - "rotation_deg": [ - -10.095043, - 79.268967, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 907, - "type": "imguizmo", - "translation": [ - -3.725756, - 0.491771, - 1.587848 - ], - "rotation_deg": [ - -11.571904, - 79.917709, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 908, - "type": "imguizmo", - "translation": [ - -3.704726, - 0.556302, - 1.561364 - ], - "rotation_deg": [ - -13.016512, - 80.563023, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 909, - "type": "imguizmo", - "translation": [ - -3.681114, - 0.620446, - 1.535545 - ], - "rotation_deg": [ - -14.424841, - 81.204461, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 910, - "type": "imguizmo", - "translation": [ - -3.654937, - 0.684157, - 1.510462 - ], - "rotation_deg": [ - -15.792965, - 81.841575, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 911, - "type": "imguizmo", - "translation": [ - -3.626213, - 0.747392, - 1.486187 - ], - "rotation_deg": [ - -17.117071, - 82.47392, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 912, - "type": "imguizmo", - "translation": [ - -3.594962, - 0.810106, - 1.462786 - ], - "rotation_deg": [ - -18.393469, - 83.101057, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 913, - "type": "imguizmo", - "translation": [ - -3.561205, - 0.872255, - 1.440326 - ], - "rotation_deg": [ - -19.618601, - 83.722549, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 914, - "type": "imguizmo", - "translation": [ - -3.524966, - 0.933796, - 1.418867 - ], - "rotation_deg": [ - -20.789052, - 84.337961, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 915, - "type": "imguizmo", - "translation": [ - -3.486271, - 0.994687, - 1.398471 - ], - "rotation_deg": [ - -21.901561, - 84.946866, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 916, - "type": "imguizmo", - "translation": [ - -3.445146, - 1.054884, - 1.379195 - ], - "rotation_deg": [ - -22.953026, - 85.548838, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 917, - "type": "imguizmo", - "translation": [ - -3.401621, - 1.114346, - 1.361091 - ], - "rotation_deg": [ - -23.940517, - 86.143459, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 918, - "type": "imguizmo", - "translation": [ - -3.355724, - 1.173031, - 1.34421 - ], - "rotation_deg": [ - -24.861281, - 86.730314, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 919, - "type": "imguizmo", - "translation": [ - -3.307489, - 1.230899, - 1.3286 - ], - "rotation_deg": [ - -25.712753, - 87.308993, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 920, - "type": "imguizmo", - "translation": [ - -3.256949, - 1.287909, - 1.314303 - ], - "rotation_deg": [ - -26.492558, - 87.879095, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 921, - "type": "imguizmo", - "translation": [ - -3.204139, - 1.344022, - 1.30136 - ], - "rotation_deg": [ - -27.198524, - 88.44022, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 922, - "type": "imguizmo", - "translation": [ - -3.149096, - 1.399198, - 1.289807 - ], - "rotation_deg": [ - -27.828683, - 88.991979, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 923, - "type": "imguizmo", - "translation": [ - -3.091858, - 1.453399, - 1.279677 - ], - "rotation_deg": [ - -28.381278, - 89.533987, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 924, - "type": "imguizmo", - "translation": [ - -3.032465, - 1.506587, - 1.270996 - ], - "rotation_deg": [ - -28.854769, - 90.065866, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 925, - "type": "imguizmo", - "translation": [ - -2.97096, - 1.558725, - 1.26379 - ], - "rotation_deg": [ - -29.247837, - 90.587245, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 926, - "type": "imguizmo", - "translation": [ - -2.907383, - 1.609776, - 1.258078 - ], - "rotation_deg": [ - -29.559386, - 91.097761, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 927, - "type": "imguizmo", - "translation": [ - -2.841781, - 1.659706, - 1.253877 - ], - "rotation_deg": [ - -29.788548, - 91.597059, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 928, - "type": "imguizmo", - "translation": [ - -2.774198, - 1.708479, - 1.251197 - ], - "rotation_deg": [ - -29.934684, - 92.084789, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 929, - "type": "imguizmo", - "translation": [ - -2.704681, - 1.756061, - 1.250048 - ], - "rotation_deg": [ - -29.997386, - 92.560613, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 930, - "type": "imguizmo", - "translation": [ - -2.63328, - 1.80242, - 1.250431 - ], - "rotation_deg": [ - -29.976481, - 93.024199, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 931, - "type": "imguizmo", - "translation": [ - -2.560043, - 1.847522, - 1.252346 - ], - "rotation_deg": [ - -29.872025, - 93.475223, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 932, - "type": "imguizmo", - "translation": [ - -2.485023, - 1.891337, - 1.255788 - ], - "rotation_deg": [ - -29.684311, - 93.913372, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 933, - "type": "imguizmo", - "translation": [ - -2.40827, - 1.933834, - 1.260746 - ], - "rotation_deg": [ - -29.413861, - 94.338339, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 934, - "type": "imguizmo", - "translation": [ - -2.329839, - 1.974983, - 1.267207 - ], - "rotation_deg": [ - -29.06143, - 94.749829, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 935, - "type": "imguizmo", - "translation": [ - -2.249785, - 2.014756, - 1.275153 - ], - "rotation_deg": [ - -28.627999, - 95.147556, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 936, - "type": "imguizmo", - "translation": [ - -2.168162, - 2.053124, - 1.284562 - ], - "rotation_deg": [ - -28.114777, - 95.531241, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 937, - "type": "imguizmo", - "translation": [ - -2.085029, - 2.090062, - 1.295408 - ], - "rotation_deg": [ - -27.523194, - 95.900618, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 938, - "type": "imguizmo", - "translation": [ - -2.000442, - 2.125543, - 1.30766 - ], - "rotation_deg": [ - -26.854899, - 96.255428, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 939, - "type": "imguizmo", - "translation": [ - -1.914461, - 2.159543, - 1.321285 - ], - "rotation_deg": [ - -26.111754, - 96.595426, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 940, - "type": "imguizmo", - "translation": [ - -1.827147, - 2.192037, - 1.336243 - ], - "rotation_deg": [ - -25.295831, - 96.920373, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 941, - "type": "imguizmo", - "translation": [ - -1.738558, - 2.223004, - 1.352494 - ], - "rotation_deg": [ - -24.409405, - 97.230044, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 942, - "type": "imguizmo", - "translation": [ - -1.648758, - 2.252422, - 1.369993 - ], - "rotation_deg": [ - -23.454944, - 97.524222, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 943, - "type": "imguizmo", - "translation": [ - -1.557809, - 2.28027, - 1.38869 - ], - "rotation_deg": [ - -22.435111, - 97.802702, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 944, - "type": "imguizmo", - "translation": [ - -1.465775, - 2.306529, - 1.408533 - ], - "rotation_deg": [ - -21.352747, - 98.065291, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 945, - "type": "imguizmo", - "translation": [ - -1.372718, - 2.331181, - 1.429467 - ], - "rotation_deg": [ - -20.210869, - 98.311806, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 946, - "type": "imguizmo", - "translation": [ - -1.278705, - 2.354207, - 1.451435 - ], - "rotation_deg": [ - -19.01266, - 98.542074, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 947, - "type": "imguizmo", - "translation": [ - -1.183801, - 2.375594, - 1.474373 - ], - "rotation_deg": [ - -17.761459, - 98.755935, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 948, - "type": "imguizmo", - "translation": [ - -1.088072, - 2.395324, - 1.49822 - ], - "rotation_deg": [ - -16.460754, - 98.95324, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 949, - "type": "imguizmo", - "translation": [ - -0.991585, - 2.413385, - 1.522907 - ], - "rotation_deg": [ - -15.11417, - 99.133852, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 950, - "type": "imguizmo", - "translation": [ - -0.894407, - 2.429764, - 1.548367 - ], - "rotation_deg": [ - -13.725459, - 99.297645, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 951, - "type": "imguizmo", - "translation": [ - -0.796605, - 2.44445, - 1.574528 - ], - "rotation_deg": [ - -12.298494, - 99.444504, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 952, - "type": "imguizmo", - "translation": [ - -0.698248, - 2.457433, - 1.601317 - ], - "rotation_deg": [ - -10.83725, - 99.574327, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 953, - "type": "imguizmo", - "translation": [ - -0.599405, - 2.468702, - 1.62866 - ], - "rotation_deg": [ - -9.345801, - 99.687025, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 954, - "type": "imguizmo", - "translation": [ - -0.500143, - 2.478252, - 1.656481 - ], - "rotation_deg": [ - -7.828303, - 99.782517, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 955, - "type": "imguizmo", - "translation": [ - -0.400534, - 2.486074, - 1.684702 - ], - "rotation_deg": [ - -6.288987, - 99.860738, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 956, - "type": "imguizmo", - "translation": [ - -0.300645, - 2.492163, - 1.713244 - ], - "rotation_deg": [ - -4.732142, - 99.921633, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 957, - "type": "imguizmo", - "translation": [ - -0.200546, - 2.496516, - 1.742028 - ], - "rotation_deg": [ - -3.162108, - 99.96516, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 958, - "type": "imguizmo", - "translation": [ - -0.100308, - 2.499129, - 1.770974 - ], - "rotation_deg": [ - -1.58326, - 99.991289, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 959, - "type": "imguizmo", - "translation": [ - 0, - 2.5, - 1.8 - ], - "rotation_deg": [ - 0, - 100, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 960, - "type": "action", - "action": "set_active_render_window", - "value": 0 - }, - { - "frame": 960, - "type": "action", - "action": "set_active_planar", - "value": 4 - }, - { - "frame": 960, - "type": "action", - "action": "set_projection_type", - "value": "perspective" - }, - { - "frame": 960, - "type": "action", - "action": "set_left_handed", - "value": true - }, - { - "frame": 960, - "type": "action", - "action": "reset_active_camera", - "value": 1 - }, - { - "frame": 960, - "type": "action", - "action": "set_active_render_window", - "value": 1 - }, - { - "frame": 960, - "type": "action", - "action": "set_active_planar", - "value": 4 - }, - { - "frame": 960, - "type": "action", - "action": "set_projection_type", - "value": "orthographic" - }, - { - "frame": 960, - "type": "action", - "action": "set_left_handed", - "value": true - }, - { - "frame": 960, - "type": "action", - "action": "set_active_render_window", - "value": 0 - }, - { - "frame": 961, - "type": "imguizmo", - "translation": [ - 0, - 0, - 3.1 - ], - "rotation_deg": [ - 0, - 125, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 962, - "type": "imguizmo", - "translation": [ - 0, - 0, - 3.099756 - ], - "rotation_deg": [ - 1.108669, - 124.989546, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 963, - "type": "imguizmo", - "translation": [ - 0, - 0, - 3.099024 - ], - "rotation_deg": [ - 2.216565, - 124.958192, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 964, - "type": "imguizmo", - "translation": [ - 0, - 0, - 3.097806 - ], - "rotation_deg": [ - 3.322916, - 124.90596, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 965, - "type": "imguizmo", - "translation": [ - 0, - 0, - 3.096101 - ], - "rotation_deg": [ - 4.426951, - 124.832886, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 966, - "type": "imguizmo", - "translation": [ - 0, - 0, - 3.09391 - ], - "rotation_deg": [ - 5.527901, - 124.73902, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 967, - "type": "imguizmo", - "translation": [ - 0, - 0, - 3.091237 - ], - "rotation_deg": [ - 6.624999, - 124.62443, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 968, - "type": "imguizmo", - "translation": [ - 0, - 0, - 3.088081 - ], - "rotation_deg": [ - 7.71748, - 124.489193, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 969, - "type": "imguizmo", - "translation": [ - 0, - 0, - 3.084446 - ], - "rotation_deg": [ - 8.804582, - 124.333405, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 970, - "type": "imguizmo", - "translation": [ - 0, - 0, - 3.080334 - ], - "rotation_deg": [ - 9.885548, - 124.157174, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 971, - "type": "imguizmo", - "translation": [ - 0, - 0, - 3.075748 - ], - "rotation_deg": [ - 10.959625, - 123.960623, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 972, - "type": "imguizmo", - "translation": [ - 0, - 0, - 3.070691 - ], - "rotation_deg": [ - 12.026064, - 123.743888, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 973, - "type": "imguizmo", - "translation": [ - 0, - 0, - 3.065166 - ], - "rotation_deg": [ - 13.084121, - 123.507122, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 974, - "type": "imguizmo", - "translation": [ - 0, - 0, - 3.059178 - ], - "rotation_deg": [ - 14.13306, - 123.250489, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 975, - "type": "imguizmo", - "translation": [ - 0, - 0, - 3.052731 - ], - "rotation_deg": [ - 15.17215, - 122.974167, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 976, - "type": "imguizmo", - "translation": [ - 0, - 0, - 3.045828 - ], - "rotation_deg": [ - 16.200666, - 122.67835, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 977, - "type": "imguizmo", - "translation": [ - 0, - 0, - 3.038476 - ], - "rotation_deg": [ - 17.217891, - 122.363243, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 978, - "type": "imguizmo", - "translation": [ - 0, - 0, - 3.030678 - ], - "rotation_deg": [ - 18.223117, - 122.029066, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 979, - "type": "imguizmo", - "translation": [ - 0, - 0, - 3.022441 - ], - "rotation_deg": [ - 19.215643, - 121.676052, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 980, - "type": "imguizmo", - "translation": [ - 0, - 0, - 3.01377 - ], - "rotation_deg": [ - 20.194777, - 121.304448, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 981, - "type": "imguizmo", - "translation": [ - 0, - 0, - 3.004672 - ], - "rotation_deg": [ - 21.159837, - 120.914511, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 982, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.995152 - ], - "rotation_deg": [ - 22.110151, - 120.506514, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 983, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.985217 - ], - "rotation_deg": [ - 23.045055, - 120.080741, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 984, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.974875 - ], - "rotation_deg": [ - 23.9639, - 119.637489, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 985, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.964132 - ], - "rotation_deg": [ - 24.866043, - 119.177067, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 986, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.952995 - ], - "rotation_deg": [ - 25.750857, - 118.699795, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 987, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.941473 - ], - "rotation_deg": [ - 26.617724, - 118.206007, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 988, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.929574 - ], - "rotation_deg": [ - 27.466041, - 117.696046, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 989, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.917306 - ], - "rotation_deg": [ - 28.295217, - 117.170268, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 990, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.904678 - ], - "rotation_deg": [ - 29.104673, - 116.629038, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 991, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.891697 - ], - "rotation_deg": [ - 29.893846, - 116.072736, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 992, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.878374 - ], - "rotation_deg": [ - 30.662185, - 115.501747, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 993, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.864718 - ], - "rotation_deg": [ - 31.409156, - 114.91647, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 994, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.850737 - ], - "rotation_deg": [ - 32.134236, - 114.317313, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 995, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.836443 - ], - "rotation_deg": [ - 32.836922, - 113.704694, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 996, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.821844 - ], - "rotation_deg": [ - 33.516724, - 113.079039, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 997, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.806952 - ], - "rotation_deg": [ - 34.173166, - 112.440785, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 998, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.791775 - ], - "rotation_deg": [ - 34.805794, - 111.790375, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 999, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.776326 - ], - "rotation_deg": [ - 35.414164, - 111.128264, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1000, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.760615 - ], - "rotation_deg": [ - 35.997854, - 110.454914, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1001, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.744652 - ], - "rotation_deg": [ - 36.556456, - 109.770792, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1002, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.728449 - ], - "rotation_deg": [ - 37.089581, - 109.076376, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1003, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.712017 - ], - "rotation_deg": [ - 37.596858, - 108.372151, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1004, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.695367 - ], - "rotation_deg": [ - 38.077933, - 107.658606, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1005, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.678512 - ], - "rotation_deg": [ - 38.532471, - 106.936239, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1006, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.661463 - ], - "rotation_deg": [ - 38.960156, - 106.205553, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1007, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.644231 - ], - "rotation_deg": [ - 39.360688, - 105.467058, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1008, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.62683 - ], - "rotation_deg": [ - 39.733789, - 104.721269, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1009, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.60927 - ], - "rotation_deg": [ - 40.079199, - 103.968705, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1010, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.591564 - ], - "rotation_deg": [ - 40.396677, - 103.20989, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1011, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.573725 - ], - "rotation_deg": [ - 40.686002, - 102.445353, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1012, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.555765 - ], - "rotation_deg": [ - 40.946972, - 101.675628, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1013, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.537696 - ], - "rotation_deg": [ - 41.179406, - 100.90125, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1014, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.519531 - ], - "rotation_deg": [ - 41.383141, - 100.12276, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1015, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.501283 - ], - "rotation_deg": [ - 41.558036, - 99.3407, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1016, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.482964 - ], - "rotation_deg": [ - 41.703968, - 98.555614, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1017, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.464588 - ], - "rotation_deg": [ - 41.820835, - 97.768051, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1018, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.446166 - ], - "rotation_deg": [ - 41.908558, - 96.978558, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1019, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.427713 - ], - "rotation_deg": [ - 41.967073, - 96.187687, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1020, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.40924 - ], - "rotation_deg": [ - 41.996341, - 95.395988, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1021, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.39076 - ], - "rotation_deg": [ - 41.996341, - 94.604012, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1022, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.372287 - ], - "rotation_deg": [ - 41.967073, - 93.812313, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1023, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.353834 - ], - "rotation_deg": [ - 41.908558, - 93.021442, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1024, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.335412 - ], - "rotation_deg": [ - 41.820835, - 92.231949, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1025, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.317036 - ], - "rotation_deg": [ - 41.703968, - 91.444386, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1026, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.298717 - ], - "rotation_deg": [ - 41.558036, - 90.6593, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1027, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.280469 - ], - "rotation_deg": [ - 41.383141, - 89.87724, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1028, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.262304 - ], - "rotation_deg": [ - 41.179406, - 89.09875, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1029, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.244235 - ], - "rotation_deg": [ - 40.946972, - 88.324372, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1030, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.226275 - ], - "rotation_deg": [ - 40.686002, - 87.554647, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1031, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.208436 - ], - "rotation_deg": [ - 40.396677, - 86.79011, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1032, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.19073 - ], - "rotation_deg": [ - 40.079199, - 86.031295, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1033, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.17317 - ], - "rotation_deg": [ - 39.733789, - 85.278731, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1034, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.155769 - ], - "rotation_deg": [ - 39.360688, - 84.532942, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1035, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.138537 - ], - "rotation_deg": [ - 38.960156, - 83.794447, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1036, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.121488 - ], - "rotation_deg": [ - 38.532471, - 83.063761, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1037, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.104633 - ], - "rotation_deg": [ - 38.077933, - 82.341394, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1038, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.087983 - ], - "rotation_deg": [ - 37.596858, - 81.627849, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1039, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.071551 - ], - "rotation_deg": [ - 37.089581, - 80.923624, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1040, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.055348 - ], - "rotation_deg": [ - 36.556456, - 80.229208, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1041, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.039385 - ], - "rotation_deg": [ - 35.997854, - 79.545086, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1042, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.023674 - ], - "rotation_deg": [ - 35.414164, - 78.871736, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1043, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.008225 - ], - "rotation_deg": [ - 34.805794, - 78.209625, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1044, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.993048 - ], - "rotation_deg": [ - 34.173166, - 77.559215, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1045, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.978156 - ], - "rotation_deg": [ - 33.516724, - 76.920961, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1046, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.963557 - ], - "rotation_deg": [ - 32.836922, - 76.295306, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1047, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.949263 - ], - "rotation_deg": [ - 32.134236, - 75.682687, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1048, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.935282 - ], - "rotation_deg": [ - 31.409156, - 75.08353, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1049, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.921626 - ], - "rotation_deg": [ - 30.662185, - 74.498253, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1050, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.908303 - ], - "rotation_deg": [ - 29.893846, - 73.927264, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1051, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.895322 - ], - "rotation_deg": [ - 29.104673, - 73.370962, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1052, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.882694 - ], - "rotation_deg": [ - 28.295217, - 72.829732, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1053, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.870426 - ], - "rotation_deg": [ - 27.466041, - 72.303954, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1054, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.858527 - ], - "rotation_deg": [ - 26.617724, - 71.793993, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1055, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.847005 - ], - "rotation_deg": [ - 25.750857, - 71.300205, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1056, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.835868 - ], - "rotation_deg": [ - 24.866043, - 70.822933, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1057, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.825125 - ], - "rotation_deg": [ - 23.9639, - 70.362511, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1058, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.814783 - ], - "rotation_deg": [ - 23.045055, - 69.919259, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1059, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.804848 - ], - "rotation_deg": [ - 22.110151, - 69.493486, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1060, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.795328 - ], - "rotation_deg": [ - 21.159837, - 69.085489, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1061, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.78623 - ], - "rotation_deg": [ - 20.194777, - 68.695552, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1062, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.777559 - ], - "rotation_deg": [ - 19.215643, - 68.323948, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1063, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.769322 - ], - "rotation_deg": [ - 18.223117, - 67.970934, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1064, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.761524 - ], - "rotation_deg": [ - 17.217891, - 67.636757, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1065, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.754172 - ], - "rotation_deg": [ - 16.200666, - 67.32165, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1066, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.747269 - ], - "rotation_deg": [ - 15.17215, - 67.025833, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1067, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.740822 - ], - "rotation_deg": [ - 14.13306, - 66.749511, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1068, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.734834 - ], - "rotation_deg": [ - 13.084121, - 66.492878, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1069, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.729309 - ], - "rotation_deg": [ - 12.026064, - 66.256112, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1070, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.724252 - ], - "rotation_deg": [ - 10.959625, - 66.039377, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1071, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.719666 - ], - "rotation_deg": [ - 9.885548, - 65.842826, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1072, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.715554 - ], - "rotation_deg": [ - 8.804582, - 65.666595, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1073, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.711919 - ], - "rotation_deg": [ - 7.71748, - 65.510807, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1074, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.708763 - ], - "rotation_deg": [ - 6.624999, - 65.37557, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1075, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.70609 - ], - "rotation_deg": [ - 5.527901, - 65.26098, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1076, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.703899 - ], - "rotation_deg": [ - 4.426951, - 65.167114, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1077, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.702194 - ], - "rotation_deg": [ - 3.322916, - 65.09404, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1078, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.700976 - ], - "rotation_deg": [ - 2.216565, - 65.041808, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1079, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.700244 - ], - "rotation_deg": [ - 1.108669, - 65.010454, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1080, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.7 - ], - "rotation_deg": [ - 0, - 65, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1081, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.700244 - ], - "rotation_deg": [ - -1.108669, - 65.010454, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1082, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.700976 - ], - "rotation_deg": [ - -2.216565, - 65.041808, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1083, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.702194 - ], - "rotation_deg": [ - -3.322916, - 65.09404, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1084, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.703899 - ], - "rotation_deg": [ - -4.426951, - 65.167114, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1085, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.70609 - ], - "rotation_deg": [ - -5.527901, - 65.26098, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1086, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.708763 - ], - "rotation_deg": [ - -6.624999, - 65.37557, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1087, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.711919 - ], - "rotation_deg": [ - -7.71748, - 65.510807, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1088, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.715554 - ], - "rotation_deg": [ - -8.804582, - 65.666595, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1089, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.719666 - ], - "rotation_deg": [ - -9.885548, - 65.842826, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1090, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.724252 - ], - "rotation_deg": [ - -10.959625, - 66.039377, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1091, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.729309 - ], - "rotation_deg": [ - -12.026064, - 66.256112, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1092, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.734834 - ], - "rotation_deg": [ - -13.084121, - 66.492878, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1093, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.740822 - ], - "rotation_deg": [ - -14.13306, - 66.749511, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1094, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.747269 - ], - "rotation_deg": [ - -15.17215, - 67.025833, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1095, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.754172 - ], - "rotation_deg": [ - -16.200666, - 67.32165, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1096, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.761524 - ], - "rotation_deg": [ - -17.217891, - 67.636757, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1097, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.769322 - ], - "rotation_deg": [ - -18.223117, - 67.970934, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1098, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.777559 - ], - "rotation_deg": [ - -19.215643, - 68.323948, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1099, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.78623 - ], - "rotation_deg": [ - -20.194777, - 68.695552, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1100, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.795328 - ], - "rotation_deg": [ - -21.159837, - 69.085489, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1101, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.804848 - ], - "rotation_deg": [ - -22.110151, - 69.493486, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1102, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.814783 - ], - "rotation_deg": [ - -23.045055, - 69.919259, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1103, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.825125 - ], - "rotation_deg": [ - -23.9639, - 70.362511, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1104, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.835868 - ], - "rotation_deg": [ - -24.866043, - 70.822933, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1105, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.847005 - ], - "rotation_deg": [ - -25.750857, - 71.300205, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1106, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.858527 - ], - "rotation_deg": [ - -26.617724, - 71.793993, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1107, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.870426 - ], - "rotation_deg": [ - -27.466041, - 72.303954, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1108, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.882694 - ], - "rotation_deg": [ - -28.295217, - 72.829732, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1109, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.895322 - ], - "rotation_deg": [ - -29.104673, - 73.370962, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1110, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.908303 - ], - "rotation_deg": [ - -29.893846, - 73.927264, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1111, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.921626 - ], - "rotation_deg": [ - -30.662185, - 74.498253, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1112, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.935282 - ], - "rotation_deg": [ - -31.409156, - 75.08353, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1113, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.949263 - ], - "rotation_deg": [ - -32.134236, - 75.682687, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1114, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.963557 - ], - "rotation_deg": [ - -32.836922, - 76.295306, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1115, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.978156 - ], - "rotation_deg": [ - -33.516724, - 76.920961, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1116, - "type": "imguizmo", - "translation": [ - 0, - 0, - 1.993048 - ], - "rotation_deg": [ - -34.173166, - 77.559215, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1117, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.008225 - ], - "rotation_deg": [ - -34.805794, - 78.209625, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1118, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.023674 - ], - "rotation_deg": [ - -35.414164, - 78.871736, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1119, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.039385 - ], - "rotation_deg": [ - -35.997854, - 79.545086, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1120, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.055348 - ], - "rotation_deg": [ - -36.556456, - 80.229208, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1121, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.071551 - ], - "rotation_deg": [ - -37.089581, - 80.923624, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1122, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.087983 - ], - "rotation_deg": [ - -37.596858, - 81.627849, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1123, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.104633 - ], - "rotation_deg": [ - -38.077933, - 82.341394, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1124, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.121488 - ], - "rotation_deg": [ - -38.532471, - 83.063761, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1125, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.138537 - ], - "rotation_deg": [ - -38.960156, - 83.794447, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1126, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.155769 - ], - "rotation_deg": [ - -39.360688, - 84.532942, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1127, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.17317 - ], - "rotation_deg": [ - -39.733789, - 85.278731, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1128, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.19073 - ], - "rotation_deg": [ - -40.079199, - 86.031295, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1129, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.208436 - ], - "rotation_deg": [ - -40.396677, - 86.79011, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1130, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.226275 - ], - "rotation_deg": [ - -40.686002, - 87.554647, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1131, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.244235 - ], - "rotation_deg": [ - -40.946972, - 88.324372, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1132, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.262304 - ], - "rotation_deg": [ - -41.179406, - 89.09875, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1133, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.280469 - ], - "rotation_deg": [ - -41.383141, - 89.87724, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1134, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.298717 - ], - "rotation_deg": [ - -41.558036, - 90.6593, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1135, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.317036 - ], - "rotation_deg": [ - -41.703968, - 91.444386, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1136, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.335412 - ], - "rotation_deg": [ - -41.820835, - 92.231949, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1137, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.353834 - ], - "rotation_deg": [ - -41.908558, - 93.021442, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1138, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.372287 - ], - "rotation_deg": [ - -41.967073, - 93.812313, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1139, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.39076 - ], - "rotation_deg": [ - -41.996341, - 94.604012, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1140, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.40924 - ], - "rotation_deg": [ - -41.996341, - 95.395988, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1141, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.427713 - ], - "rotation_deg": [ - -41.967073, - 96.187687, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1142, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.446166 - ], - "rotation_deg": [ - -41.908558, - 96.978558, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1143, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.464588 - ], - "rotation_deg": [ - -41.820835, - 97.768051, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1144, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.482964 - ], - "rotation_deg": [ - -41.703968, - 98.555614, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1145, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.501283 - ], - "rotation_deg": [ - -41.558036, - 99.3407, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1146, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.519531 - ], - "rotation_deg": [ - -41.383141, - 100.12276, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1147, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.537696 - ], - "rotation_deg": [ - -41.179406, - 100.90125, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1148, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.555765 - ], - "rotation_deg": [ - -40.946972, - 101.675628, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1149, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.573725 - ], - "rotation_deg": [ - -40.686002, - 102.445353, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1150, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.591564 - ], - "rotation_deg": [ - -40.396677, - 103.20989, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1151, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.60927 - ], - "rotation_deg": [ - -40.079199, - 103.968705, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1152, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.62683 - ], - "rotation_deg": [ - -39.733789, - 104.721269, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1153, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.644231 - ], - "rotation_deg": [ - -39.360688, - 105.467058, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1154, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.661463 - ], - "rotation_deg": [ - -38.960156, - 106.205553, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1155, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.678512 - ], - "rotation_deg": [ - -38.532471, - 106.936239, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1156, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.695367 - ], - "rotation_deg": [ - -38.077933, - 107.658606, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1157, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.712017 - ], - "rotation_deg": [ - -37.596858, - 108.372151, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1158, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.728449 - ], - "rotation_deg": [ - -37.089581, - 109.076376, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1159, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.744652 - ], - "rotation_deg": [ - -36.556456, - 109.770792, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1160, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.760615 - ], - "rotation_deg": [ - -35.997854, - 110.454914, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1161, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.776326 - ], - "rotation_deg": [ - -35.414164, - 111.128264, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1162, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.791775 - ], - "rotation_deg": [ - -34.805794, - 111.790375, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1163, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.806952 - ], - "rotation_deg": [ - -34.173166, - 112.440785, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1164, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.821844 - ], - "rotation_deg": [ - -33.516724, - 113.079039, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1165, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.836443 - ], - "rotation_deg": [ - -32.836922, - 113.704694, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1166, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.850737 - ], - "rotation_deg": [ - -32.134236, - 114.317313, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1167, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.864718 - ], - "rotation_deg": [ - -31.409156, - 114.91647, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1168, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.878374 - ], - "rotation_deg": [ - -30.662185, - 115.501747, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1169, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.891697 - ], - "rotation_deg": [ - -29.893846, - 116.072736, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1170, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.904678 - ], - "rotation_deg": [ - -29.104673, - 116.629038, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1171, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.917306 - ], - "rotation_deg": [ - -28.295217, - 117.170268, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1172, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.929574 - ], - "rotation_deg": [ - -27.466041, - 117.696046, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1173, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.941473 - ], - "rotation_deg": [ - -26.617724, - 118.206007, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1174, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.952995 - ], - "rotation_deg": [ - -25.750857, - 118.699795, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1175, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.964132 - ], - "rotation_deg": [ - -24.866043, - 119.177067, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1176, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.974875 - ], - "rotation_deg": [ - -23.9639, - 119.637489, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1177, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.985217 - ], - "rotation_deg": [ - -23.045055, - 120.080741, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1178, - "type": "imguizmo", - "translation": [ - 0, - 0, - 2.995152 - ], - "rotation_deg": [ - -22.110151, - 120.506514, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1179, - "type": "imguizmo", - "translation": [ - 0, - 0, - 3.004672 - ], - "rotation_deg": [ - -21.159837, - 120.914511, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1180, - "type": "imguizmo", - "translation": [ - 0, - 0, - 3.01377 - ], - "rotation_deg": [ - -20.194777, - 121.304448, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1181, - "type": "imguizmo", - "translation": [ - 0, - 0, - 3.022441 - ], - "rotation_deg": [ - -19.215643, - 121.676052, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1182, - "type": "imguizmo", - "translation": [ - 0, - 0, - 3.030678 - ], - "rotation_deg": [ - -18.223117, - 122.029066, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1183, - "type": "imguizmo", - "translation": [ - 0, - 0, - 3.038476 - ], - "rotation_deg": [ - -17.217891, - 122.363243, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1184, - "type": "imguizmo", - "translation": [ - 0, - 0, - 3.045828 - ], - "rotation_deg": [ - -16.200666, - 122.67835, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1185, - "type": "imguizmo", - "translation": [ - 0, - 0, - 3.052731 - ], - "rotation_deg": [ - -15.17215, - 122.974167, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1186, - "type": "imguizmo", - "translation": [ - 0, - 0, - 3.059178 - ], - "rotation_deg": [ - -14.13306, - 123.250489, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1187, - "type": "imguizmo", - "translation": [ - 0, - 0, - 3.065166 - ], - "rotation_deg": [ - -13.084121, - 123.507122, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1188, - "type": "imguizmo", - "translation": [ - 0, - 0, - 3.070691 - ], - "rotation_deg": [ - -12.026064, - 123.743888, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1189, - "type": "imguizmo", - "translation": [ - 0, - 0, - 3.075748 - ], - "rotation_deg": [ - -10.959625, - 123.960623, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1190, - "type": "imguizmo", - "translation": [ - 0, - 0, - 3.080334 - ], - "rotation_deg": [ - -9.885548, - 124.157174, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1191, - "type": "imguizmo", - "translation": [ - 0, - 0, - 3.084446 - ], - "rotation_deg": [ - -8.804582, - 124.333405, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1192, - "type": "imguizmo", - "translation": [ - 0, - 0, - 3.088081 - ], - "rotation_deg": [ - -7.71748, - 124.489193, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1193, - "type": "imguizmo", - "translation": [ - 0, - 0, - 3.091237 - ], - "rotation_deg": [ - -6.624999, - 124.62443, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1194, - "type": "imguizmo", - "translation": [ - 0, - 0, - 3.09391 - ], - "rotation_deg": [ - -5.527901, - 124.73902, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1195, - "type": "imguizmo", - "translation": [ - 0, - 0, - 3.096101 - ], - "rotation_deg": [ - -4.426951, - 124.832886, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1196, - "type": "imguizmo", - "translation": [ - 0, - 0, - 3.097806 - ], - "rotation_deg": [ - -3.322916, - 124.90596, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1197, - "type": "imguizmo", - "translation": [ - 0, - 0, - 3.099024 - ], - "rotation_deg": [ - -2.216565, - 124.958192, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1198, - "type": "imguizmo", - "translation": [ - 0, - 0, - 3.099756 - ], - "rotation_deg": [ - -1.108669, - 124.989546, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1199, - "type": "imguizmo", - "translation": [ - 0, - 0, - 3.1 - ], - "rotation_deg": [ - 0, - 125, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1200, - "type": "action", - "action": "set_active_render_window", - "value": 0 - }, - { - "frame": 1200, - "type": "action", - "action": "set_active_planar", - "value": 5 - }, - { - "frame": 1200, - "type": "action", - "action": "set_projection_type", - "value": "perspective" - }, - { - "frame": 1200, - "type": "action", - "action": "set_left_handed", - "value": true - }, - { - "frame": 1200, - "type": "action", - "action": "reset_active_camera", - "value": 1 - }, - { - "frame": 1200, - "type": "action", - "action": "set_active_render_window", - "value": 1 - }, - { - "frame": 1200, - "type": "action", - "action": "set_active_planar", - "value": 5 - }, - { - "frame": 1200, - "type": "action", - "action": "set_projection_type", - "value": "orthographic" - }, - { - "frame": 1200, - "type": "action", - "action": "set_left_handed", - "value": true - }, - { - "frame": 1200, - "type": "action", - "action": "set_active_render_window", - "value": 0 - }, - { - "frame": 1201, - "type": "imguizmo", - "translation": [ - 0, - 4, - 2.6 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1202, - "type": "imguizmo", - "translation": [ - 0.105587, - 3.998606, - 2.62111 - ], - "rotation_deg": [ - 0, - 2.11175, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1203, - "type": "imguizmo", - "translation": [ - 0.211101, - 3.994426, - 2.642161 - ], - "rotation_deg": [ - 0, - 4.222028, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1204, - "type": "imguizmo", - "translation": [ - 0.316468, - 3.987461, - 2.663095 - ], - "rotation_deg": [ - 0, - 6.329363, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1205, - "type": "imguizmo", - "translation": [ - 0.421614, - 3.977718, - 2.683853 - ], - "rotation_deg": [ - 0, - 8.432288, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1206, - "type": "imguizmo", - "translation": [ - 0.526467, - 3.965203, - 2.704377 - ], - "rotation_deg": [ - 0, - 10.529336, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1207, - "type": "imguizmo", - "translation": [ - 0.630952, - 3.949924, - 2.724611 - ], - "rotation_deg": [ - 0, - 12.619046, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1208, - "type": "imguizmo", - "translation": [ - 0.734998, - 3.931892, - 2.744497 - ], - "rotation_deg": [ - 0, - 14.699961, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1209, - "type": "imguizmo", - "translation": [ - 0.838532, - 3.911121, - 2.76398 - ], - "rotation_deg": [ - 0, - 16.770632, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1210, - "type": "imguizmo", - "translation": [ - 0.941481, - 3.887623, - 2.783006 - ], - "rotation_deg": [ - 0, - 18.829615, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1211, - "type": "imguizmo", - "translation": [ - 1.043774, - 3.861416, - 2.801522 - ], - "rotation_deg": [ - 0, - 20.875476, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1212, - "type": "imguizmo", - "translation": [ - 1.145339, - 3.832518, - 2.819477 - ], - "rotation_deg": [ - 0, - 22.906788, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1213, - "type": "imguizmo", - "translation": [ - 1.246107, - 3.80095, - 2.836819 - ], - "rotation_deg": [ - 0, - 24.922136, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1214, - "type": "imguizmo", - "translation": [ - 1.346006, - 3.766732, - 2.853502 - ], - "rotation_deg": [ - 0, - 26.920115, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1215, - "type": "imguizmo", - "translation": [ - 1.444967, - 3.729889, - 2.869478 - ], - "rotation_deg": [ - 0, - 28.899333, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1216, - "type": "imguizmo", - "translation": [ - 1.542921, - 3.690447, - 2.884703 - ], - "rotation_deg": [ - 0, - 30.858411, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1217, - "type": "imguizmo", - "translation": [ - 1.639799, - 3.648432, - 2.899135 - ], - "rotation_deg": [ - 0, - 32.795983, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1218, - "type": "imguizmo", - "translation": [ - 1.735535, - 3.603875, - 2.912733 - ], - "rotation_deg": [ - 0, - 34.710699, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1219, - "type": "imguizmo", - "translation": [ - 1.830061, - 3.556807, - 2.925459 - ], - "rotation_deg": [ - 0, - 36.601225, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1220, - "type": "imguizmo", - "translation": [ - 1.923312, - 3.50726, - 2.937278 - ], - "rotation_deg": [ - 0, - 38.466242, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1221, - "type": "imguizmo", - "translation": [ - 2.015223, - 3.455268, - 2.948157 - ], - "rotation_deg": [ - 0, - 40.304452, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1222, - "type": "imguizmo", - "translation": [ - 2.105729, - 3.400869, - 2.958065 - ], - "rotation_deg": [ - 0, - 42.114573, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1223, - "type": "imguizmo", - "translation": [ - 2.194767, - 3.344099, - 2.966976 - ], - "rotation_deg": [ - 0, - 43.895344, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1224, - "type": "imguizmo", - "translation": [ - 2.282276, - 3.284999, - 2.974864 - ], - "rotation_deg": [ - 0, - 45.645523, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1225, - "type": "imguizmo", - "translation": [ - 2.368195, - 3.223609, - 2.981707 - ], - "rotation_deg": [ - 0, - 47.363891, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1226, - "type": "imguizmo", - "translation": [ - 2.452463, - 3.159973, - 2.987486 - ], - "rotation_deg": [ - 0, - 49.049251, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1227, - "type": "imguizmo", - "translation": [ - 2.535021, - 3.094134, - 2.992185 - ], - "rotation_deg": [ - 0, - 50.700427, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1228, - "type": "imguizmo", - "translation": [ - 2.615813, - 3.026139, - 2.995791 - ], - "rotation_deg": [ - 0, - 52.316269, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1229, - "type": "imguizmo", - "translation": [ - 2.694783, - 2.956036, - 2.998294 - ], - "rotation_deg": [ - 0, - 53.895651, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1230, - "type": "imguizmo", - "translation": [ - 2.771874, - 2.883872, - 2.999686 - ], - "rotation_deg": [ - 0, - 55.437473, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1231, - "type": "imguizmo", - "translation": [ - 2.847033, - 2.809698, - 2.999965 - ], - "rotation_deg": [ - 0, - 56.940659, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1232, - "type": "imguizmo", - "translation": [ - 2.920208, - 2.733566, - 2.999129 - ], - "rotation_deg": [ - 0, - 58.404163, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1233, - "type": "imguizmo", - "translation": [ - 2.991348, - 2.655529, - 2.997181 - ], - "rotation_deg": [ - 0, - 59.826963, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1234, - "type": "imguizmo", - "translation": [ - 3.060403, - 2.575642, - 2.994125 - ], - "rotation_deg": [ - 0, - 61.208069, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1235, - "type": "imguizmo", - "translation": [ - 3.127326, - 2.493959, - 2.989971 - ], - "rotation_deg": [ - 0, - 62.546519, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1236, - "type": "imguizmo", - "translation": [ - 3.192069, - 2.410539, - 2.98473 - ], - "rotation_deg": [ - 0, - 63.841378, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1237, - "type": "imguizmo", - "translation": [ - 3.254587, - 2.325438, - 2.978417 - ], - "rotation_deg": [ - 0, - 65.091746, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1238, - "type": "imguizmo", - "translation": [ - 3.314837, - 2.238717, - 2.971049 - ], - "rotation_deg": [ - 0, - 66.29675, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1239, - "type": "imguizmo", - "translation": [ - 3.372778, - 2.150435, - 2.962647 - ], - "rotation_deg": [ - 0, - 67.45555, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1240, - "type": "imguizmo", - "translation": [ - 3.428367, - 2.060655, - 2.953234 - ], - "rotation_deg": [ - 0, - 68.56734, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1241, - "type": "imguizmo", - "translation": [ - 3.481567, - 1.969439, - 2.942837 - ], - "rotation_deg": [ - 0, - 69.631344, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1242, - "type": "imguizmo", - "translation": [ - 3.532341, - 1.87685, - 2.931484 - ], - "rotation_deg": [ - 0, - 70.646821, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1243, - "type": "imguizmo", - "translation": [ - 3.580653, - 1.782953, - 2.919207 - ], - "rotation_deg": [ - 0, - 71.613063, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1244, - "type": "imguizmo", - "translation": [ - 3.62647, - 1.687814, - 2.90604 - ], - "rotation_deg": [ - 0, - 72.529397, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1245, - "type": "imguizmo", - "translation": [ - 3.669759, - 1.591499, - 2.892021 - ], - "rotation_deg": [ - 0, - 73.395184, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1246, - "type": "imguizmo", - "translation": [ - 3.710491, - 1.494074, - 2.877187 - ], - "rotation_deg": [ - 0, - 74.20982, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1247, - "type": "imguizmo", - "translation": [ - 3.748637, - 1.395608, - 2.861581 - ], - "rotation_deg": [ - 0, - 74.972739, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1248, - "type": "imguizmo", - "translation": [ - 3.78417, - 1.296169, - 2.845246 - ], - "rotation_deg": [ - 0, - 75.683407, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1249, - "type": "imguizmo", - "translation": [ - 3.817067, - 1.195827, - 2.828228 - ], - "rotation_deg": [ - 0, - 76.341331, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1250, - "type": "imguizmo", - "translation": [ - 3.847303, - 1.094652, - 2.810573 - ], - "rotation_deg": [ - 0, - 76.946051, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1251, - "type": "imguizmo", - "translation": [ - 3.874857, - 0.992714, - 2.792331 - ], - "rotation_deg": [ - 0, - 77.497147, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1252, - "type": "imguizmo", - "translation": [ - 3.899712, - 0.890084, - 2.773553 - ], - "rotation_deg": [ - 0, - 77.994233, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1253, - "type": "imguizmo", - "translation": [ - 3.921848, - 0.786833, - 2.754292 - ], - "rotation_deg": [ - 0, - 78.436964, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1254, - "type": "imguizmo", - "translation": [ - 3.941252, - 0.683035, - 2.734601 - ], - "rotation_deg": [ - 0, - 78.825031, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1255, - "type": "imguizmo", - "translation": [ - 3.957908, - 0.57876, - 2.714534 - ], - "rotation_deg": [ - 0, - 79.158163, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1256, - "type": "imguizmo", - "translation": [ - 3.971806, - 0.474082, - 2.694148 - ], - "rotation_deg": [ - 0, - 79.436129, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1257, - "type": "imguizmo", - "translation": [ - 3.982937, - 0.369073, - 2.6735 - ], - "rotation_deg": [ - 0, - 79.658734, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1258, - "type": "imguizmo", - "translation": [ - 3.991291, - 0.263808, - 2.652647 - ], - "rotation_deg": [ - 0, - 79.825824, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1259, - "type": "imguizmo", - "translation": [ - 3.996864, - 0.158358, - 2.631647 - ], - "rotation_deg": [ - 0, - 79.937282, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1260, - "type": "imguizmo", - "translation": [ - 3.999652, - 0.052798, - 2.610559 - ], - "rotation_deg": [ - 0, - 79.993031, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1261, - "type": "imguizmo", - "translation": [ - 3.999652, - -0.052798, - 2.589441 - ], - "rotation_deg": [ - 0, - 79.993031, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1262, - "type": "imguizmo", - "translation": [ - 3.996864, - -0.158358, - 2.568353 - ], - "rotation_deg": [ - 0, - 79.937282, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1263, - "type": "imguizmo", - "translation": [ - 3.991291, - -0.263808, - 2.547353 - ], - "rotation_deg": [ - 0, - 79.825824, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1264, - "type": "imguizmo", - "translation": [ - 3.982937, - -0.369073, - 2.5265 - ], - "rotation_deg": [ - 0, - 79.658734, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1265, - "type": "imguizmo", - "translation": [ - 3.971806, - -0.474082, - 2.505852 - ], - "rotation_deg": [ - 0, - 79.436129, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1266, - "type": "imguizmo", - "translation": [ - 3.957908, - -0.57876, - 2.485466 - ], - "rotation_deg": [ - 0, - 79.158163, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1267, - "type": "imguizmo", - "translation": [ - 3.941252, - -0.683035, - 2.465399 - ], - "rotation_deg": [ - 0, - 78.825031, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1268, - "type": "imguizmo", - "translation": [ - 3.921848, - -0.786833, - 2.445708 - ], - "rotation_deg": [ - 0, - 78.436964, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1269, - "type": "imguizmo", - "translation": [ - 3.899712, - -0.890084, - 2.426447 - ], - "rotation_deg": [ - 0, - 77.994233, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1270, - "type": "imguizmo", - "translation": [ - 3.874857, - -0.992714, - 2.407669 - ], - "rotation_deg": [ - 0, - 77.497147, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1271, - "type": "imguizmo", - "translation": [ - 3.847303, - -1.094652, - 2.389427 - ], - "rotation_deg": [ - 0, - 76.946051, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1272, - "type": "imguizmo", - "translation": [ - 3.817067, - -1.195827, - 2.371772 - ], - "rotation_deg": [ - 0, - 76.341331, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1273, - "type": "imguizmo", - "translation": [ - 3.78417, - -1.296169, - 2.354754 - ], - "rotation_deg": [ - 0, - 75.683407, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1274, - "type": "imguizmo", - "translation": [ - 3.748637, - -1.395608, - 2.338419 - ], - "rotation_deg": [ - 0, - 74.972739, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1275, - "type": "imguizmo", - "translation": [ - 3.710491, - -1.494074, - 2.322813 - ], - "rotation_deg": [ - 0, - 74.20982, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1276, - "type": "imguizmo", - "translation": [ - 3.669759, - -1.591499, - 2.307979 - ], - "rotation_deg": [ - 0, - 73.395184, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1277, - "type": "imguizmo", - "translation": [ - 3.62647, - -1.687814, - 2.29396 - ], - "rotation_deg": [ - 0, - 72.529397, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1278, - "type": "imguizmo", - "translation": [ - 3.580653, - -1.782953, - 2.280793 - ], - "rotation_deg": [ - 0, - 71.613063, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1279, - "type": "imguizmo", - "translation": [ - 3.532341, - -1.87685, - 2.268516 - ], - "rotation_deg": [ - 0, - 70.646821, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1280, - "type": "imguizmo", - "translation": [ - 3.481567, - -1.969439, - 2.257163 - ], - "rotation_deg": [ - 0, - 69.631344, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1281, - "type": "imguizmo", - "translation": [ - 3.428367, - -2.060655, - 2.246766 - ], - "rotation_deg": [ - 0, - 68.56734, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1282, - "type": "imguizmo", - "translation": [ - 3.372778, - -2.150435, - 2.237353 - ], - "rotation_deg": [ - 0, - 67.45555, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1283, - "type": "imguizmo", - "translation": [ - 3.314837, - -2.238717, - 2.228951 - ], - "rotation_deg": [ - 0, - 66.29675, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1284, - "type": "imguizmo", - "translation": [ - 3.254587, - -2.325438, - 2.221583 - ], - "rotation_deg": [ - 0, - 65.091746, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1285, - "type": "imguizmo", - "translation": [ - 3.192069, - -2.410539, - 2.21527 - ], - "rotation_deg": [ - 0, - 63.841378, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1286, - "type": "imguizmo", - "translation": [ - 3.127326, - -2.493959, - 2.210029 - ], - "rotation_deg": [ - 0, - 62.546519, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1287, - "type": "imguizmo", - "translation": [ - 3.060403, - -2.575642, - 2.205875 - ], - "rotation_deg": [ - 0, - 61.208069, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1288, - "type": "imguizmo", - "translation": [ - 2.991348, - -2.655529, - 2.202819 - ], - "rotation_deg": [ - 0, - 59.826963, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1289, - "type": "imguizmo", - "translation": [ - 2.920208, - -2.733566, - 2.200871 - ], - "rotation_deg": [ - 0, - 58.404163, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1290, - "type": "imguizmo", - "translation": [ - 2.847033, - -2.809698, - 2.200035 - ], - "rotation_deg": [ - 0, - 56.940659, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1291, - "type": "imguizmo", - "translation": [ - 2.771874, - -2.883872, - 2.200314 - ], - "rotation_deg": [ - 0, - 55.437473, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1292, - "type": "imguizmo", - "translation": [ - 2.694783, - -2.956036, - 2.201706 - ], - "rotation_deg": [ - 0, - 53.895651, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1293, - "type": "imguizmo", - "translation": [ - 2.615813, - -3.026139, - 2.204209 - ], - "rotation_deg": [ - 0, - 52.316269, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1294, - "type": "imguizmo", - "translation": [ - 2.535021, - -3.094134, - 2.207815 - ], - "rotation_deg": [ - 0, - 50.700427, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1295, - "type": "imguizmo", - "translation": [ - 2.452463, - -3.159973, - 2.212514 - ], - "rotation_deg": [ - 0, - 49.049251, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1296, - "type": "imguizmo", - "translation": [ - 2.368195, - -3.223609, - 2.218293 - ], - "rotation_deg": [ - 0, - 47.363891, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1297, - "type": "imguizmo", - "translation": [ - 2.282276, - -3.284999, - 2.225136 - ], - "rotation_deg": [ - 0, - 45.645523, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1298, - "type": "imguizmo", - "translation": [ - 2.194767, - -3.344099, - 2.233024 - ], - "rotation_deg": [ - 0, - 43.895344, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1299, - "type": "imguizmo", - "translation": [ - 2.105729, - -3.400869, - 2.241935 - ], - "rotation_deg": [ - 0, - 42.114573, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1300, - "type": "imguizmo", - "translation": [ - 2.015223, - -3.455268, - 2.251843 - ], - "rotation_deg": [ - 0, - 40.304452, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1301, - "type": "imguizmo", - "translation": [ - 1.923312, - -3.50726, - 2.262722 - ], - "rotation_deg": [ - 0, - 38.466242, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1302, - "type": "imguizmo", - "translation": [ - 1.830061, - -3.556807, - 2.274541 - ], - "rotation_deg": [ - 0, - 36.601225, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1303, - "type": "imguizmo", - "translation": [ - 1.735535, - -3.603875, - 2.287267 - ], - "rotation_deg": [ - 0, - 34.710699, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1304, - "type": "imguizmo", - "translation": [ - 1.639799, - -3.648432, - 2.300865 - ], - "rotation_deg": [ - 0, - 32.795983, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1305, - "type": "imguizmo", - "translation": [ - 1.542921, - -3.690447, - 2.315297 - ], - "rotation_deg": [ - 0, - 30.858411, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1306, - "type": "imguizmo", - "translation": [ - 1.444967, - -3.729889, - 2.330522 - ], - "rotation_deg": [ - 0, - 28.899333, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1307, - "type": "imguizmo", - "translation": [ - 1.346006, - -3.766732, - 2.346498 - ], - "rotation_deg": [ - 0, - 26.920115, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1308, - "type": "imguizmo", - "translation": [ - 1.246107, - -3.80095, - 2.363181 - ], - "rotation_deg": [ - 0, - 24.922136, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1309, - "type": "imguizmo", - "translation": [ - 1.145339, - -3.832518, - 2.380523 - ], - "rotation_deg": [ - 0, - 22.906788, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1310, - "type": "imguizmo", - "translation": [ - 1.043774, - -3.861416, - 2.398478 - ], - "rotation_deg": [ - 0, - 20.875476, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1311, - "type": "imguizmo", - "translation": [ - 0.941481, - -3.887623, - 2.416994 - ], - "rotation_deg": [ - 0, - 18.829615, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1312, - "type": "imguizmo", - "translation": [ - 0.838532, - -3.911121, - 2.43602 - ], - "rotation_deg": [ - 0, - 16.770632, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1313, - "type": "imguizmo", - "translation": [ - 0.734998, - -3.931892, - 2.455503 - ], - "rotation_deg": [ - 0, - 14.699961, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1314, - "type": "imguizmo", - "translation": [ - 0.630952, - -3.949924, - 2.475389 - ], - "rotation_deg": [ - 0, - 12.619046, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1315, - "type": "imguizmo", - "translation": [ - 0.526467, - -3.965203, - 2.495623 - ], - "rotation_deg": [ - 0, - 10.529336, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1316, - "type": "imguizmo", - "translation": [ - 0.421614, - -3.977718, - 2.516147 - ], - "rotation_deg": [ - 0, - 8.432288, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1317, - "type": "imguizmo", - "translation": [ - 0.316468, - -3.987461, - 2.536905 - ], - "rotation_deg": [ - 0, - 6.329363, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1318, - "type": "imguizmo", - "translation": [ - 0.211101, - -3.994426, - 2.557839 - ], - "rotation_deg": [ - 0, - 4.222028, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1319, - "type": "imguizmo", - "translation": [ - 0.105587, - -3.998606, - 2.57889 - ], - "rotation_deg": [ - 0, - 2.11175, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1320, - "type": "imguizmo", - "translation": [ - 0, - -4, - 2.6 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1321, - "type": "imguizmo", - "translation": [ - -0.105587, - -3.998606, - 2.62111 - ], - "rotation_deg": [ - 0, - -2.11175, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1322, - "type": "imguizmo", - "translation": [ - -0.211101, - -3.994426, - 2.642161 - ], - "rotation_deg": [ - 0, - -4.222028, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1323, - "type": "imguizmo", - "translation": [ - -0.316468, - -3.987461, - 2.663095 - ], - "rotation_deg": [ - 0, - -6.329363, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1324, - "type": "imguizmo", - "translation": [ - -0.421614, - -3.977718, - 2.683853 - ], - "rotation_deg": [ - 0, - -8.432288, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1325, - "type": "imguizmo", - "translation": [ - -0.526467, - -3.965203, - 2.704377 - ], - "rotation_deg": [ - 0, - -10.529336, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1326, - "type": "imguizmo", - "translation": [ - -0.630952, - -3.949924, - 2.724611 - ], - "rotation_deg": [ - 0, - -12.619046, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1327, - "type": "imguizmo", - "translation": [ - -0.734998, - -3.931892, - 2.744497 - ], - "rotation_deg": [ - 0, - -14.699961, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1328, - "type": "imguizmo", - "translation": [ - -0.838532, - -3.911121, - 2.76398 - ], - "rotation_deg": [ - 0, - -16.770632, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1329, - "type": "imguizmo", - "translation": [ - -0.941481, - -3.887623, - 2.783006 - ], - "rotation_deg": [ - 0, - -18.829615, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1330, - "type": "imguizmo", - "translation": [ - -1.043774, - -3.861416, - 2.801522 - ], - "rotation_deg": [ - 0, - -20.875476, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1331, - "type": "imguizmo", - "translation": [ - -1.145339, - -3.832518, - 2.819477 - ], - "rotation_deg": [ - 0, - -22.906788, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1332, - "type": "imguizmo", - "translation": [ - -1.246107, - -3.80095, - 2.836819 - ], - "rotation_deg": [ - 0, - -24.922136, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1333, - "type": "imguizmo", - "translation": [ - -1.346006, - -3.766732, - 2.853502 - ], - "rotation_deg": [ - 0, - -26.920115, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1334, - "type": "imguizmo", - "translation": [ - -1.444967, - -3.729889, - 2.869478 - ], - "rotation_deg": [ - 0, - -28.899333, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1335, - "type": "imguizmo", - "translation": [ - -1.542921, - -3.690447, - 2.884703 - ], - "rotation_deg": [ - 0, - -30.858411, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1336, - "type": "imguizmo", - "translation": [ - -1.639799, - -3.648432, - 2.899135 - ], - "rotation_deg": [ - 0, - -32.795983, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1337, - "type": "imguizmo", - "translation": [ - -1.735535, - -3.603875, - 2.912733 - ], - "rotation_deg": [ - 0, - -34.710699, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1338, - "type": "imguizmo", - "translation": [ - -1.830061, - -3.556807, - 2.925459 - ], - "rotation_deg": [ - 0, - -36.601225, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1339, - "type": "imguizmo", - "translation": [ - -1.923312, - -3.50726, - 2.937278 - ], - "rotation_deg": [ - 0, - -38.466242, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1340, - "type": "imguizmo", - "translation": [ - -2.015223, - -3.455268, - 2.948157 - ], - "rotation_deg": [ - 0, - -40.304452, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1341, - "type": "imguizmo", - "translation": [ - -2.105729, - -3.400869, - 2.958065 - ], - "rotation_deg": [ - 0, - -42.114573, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1342, - "type": "imguizmo", - "translation": [ - -2.194767, - -3.344099, - 2.966976 - ], - "rotation_deg": [ - 0, - -43.895344, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1343, - "type": "imguizmo", - "translation": [ - -2.282276, - -3.284999, - 2.974864 - ], - "rotation_deg": [ - 0, - -45.645523, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1344, - "type": "imguizmo", - "translation": [ - -2.368195, - -3.223609, - 2.981707 - ], - "rotation_deg": [ - 0, - -47.363891, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1345, - "type": "imguizmo", - "translation": [ - -2.452463, - -3.159973, - 2.987486 - ], - "rotation_deg": [ - 0, - -49.049251, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1346, - "type": "imguizmo", - "translation": [ - -2.535021, - -3.094134, - 2.992185 - ], - "rotation_deg": [ - 0, - -50.700427, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1347, - "type": "imguizmo", - "translation": [ - -2.615813, - -3.026139, - 2.995791 - ], - "rotation_deg": [ - 0, - -52.316269, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1348, - "type": "imguizmo", - "translation": [ - -2.694783, - -2.956036, - 2.998294 - ], - "rotation_deg": [ - 0, - -53.895651, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1349, - "type": "imguizmo", - "translation": [ - -2.771874, - -2.883872, - 2.999686 - ], - "rotation_deg": [ - 0, - -55.437473, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1350, - "type": "imguizmo", - "translation": [ - -2.847033, - -2.809698, - 2.999965 - ], - "rotation_deg": [ - 0, - -56.940659, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1351, - "type": "imguizmo", - "translation": [ - -2.920208, - -2.733566, - 2.999129 - ], - "rotation_deg": [ - 0, - -58.404163, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1352, - "type": "imguizmo", - "translation": [ - -2.991348, - -2.655529, - 2.997181 - ], - "rotation_deg": [ - 0, - -59.826963, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1353, - "type": "imguizmo", - "translation": [ - -3.060403, - -2.575642, - 2.994125 - ], - "rotation_deg": [ - 0, - -61.208069, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1354, - "type": "imguizmo", - "translation": [ - -3.127326, - -2.493959, - 2.989971 - ], - "rotation_deg": [ - 0, - -62.546519, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1355, - "type": "imguizmo", - "translation": [ - -3.192069, - -2.410539, - 2.98473 - ], - "rotation_deg": [ - 0, - -63.841378, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1356, - "type": "imguizmo", - "translation": [ - -3.254587, - -2.325438, - 2.978417 - ], - "rotation_deg": [ - 0, - -65.091746, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1357, - "type": "imguizmo", - "translation": [ - -3.314837, - -2.238717, - 2.971049 - ], - "rotation_deg": [ - 0, - -66.29675, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1358, - "type": "imguizmo", - "translation": [ - -3.372778, - -2.150435, - 2.962647 - ], - "rotation_deg": [ - 0, - -67.45555, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1359, - "type": "imguizmo", - "translation": [ - -3.428367, - -2.060655, - 2.953234 - ], - "rotation_deg": [ - 0, - -68.56734, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1360, - "type": "imguizmo", - "translation": [ - -3.481567, - -1.969439, - 2.942837 - ], - "rotation_deg": [ - 0, - -69.631344, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1361, - "type": "imguizmo", - "translation": [ - -3.532341, - -1.87685, - 2.931484 - ], - "rotation_deg": [ - 0, - -70.646821, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1362, - "type": "imguizmo", - "translation": [ - -3.580653, - -1.782953, - 2.919207 - ], - "rotation_deg": [ - 0, - -71.613063, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1363, - "type": "imguizmo", - "translation": [ - -3.62647, - -1.687814, - 2.90604 - ], - "rotation_deg": [ - 0, - -72.529397, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1364, - "type": "imguizmo", - "translation": [ - -3.669759, - -1.591499, - 2.892021 - ], - "rotation_deg": [ - 0, - -73.395184, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1365, - "type": "imguizmo", - "translation": [ - -3.710491, - -1.494074, - 2.877187 - ], - "rotation_deg": [ - 0, - -74.20982, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1366, - "type": "imguizmo", - "translation": [ - -3.748637, - -1.395608, - 2.861581 - ], - "rotation_deg": [ - 0, - -74.972739, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1367, - "type": "imguizmo", - "translation": [ - -3.78417, - -1.296169, - 2.845246 - ], - "rotation_deg": [ - 0, - -75.683407, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1368, - "type": "imguizmo", - "translation": [ - -3.817067, - -1.195827, - 2.828228 - ], - "rotation_deg": [ - 0, - -76.341331, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1369, - "type": "imguizmo", - "translation": [ - -3.847303, - -1.094652, - 2.810573 - ], - "rotation_deg": [ - 0, - -76.946051, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1370, - "type": "imguizmo", - "translation": [ - -3.874857, - -0.992714, - 2.792331 - ], - "rotation_deg": [ - 0, - -77.497147, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1371, - "type": "imguizmo", - "translation": [ - -3.899712, - -0.890084, - 2.773553 - ], - "rotation_deg": [ - 0, - -77.994233, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1372, - "type": "imguizmo", - "translation": [ - -3.921848, - -0.786833, - 2.754292 - ], - "rotation_deg": [ - 0, - -78.436964, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1373, - "type": "imguizmo", - "translation": [ - -3.941252, - -0.683035, - 2.734601 - ], - "rotation_deg": [ - 0, - -78.825031, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1374, - "type": "imguizmo", - "translation": [ - -3.957908, - -0.57876, - 2.714534 - ], - "rotation_deg": [ - 0, - -79.158163, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1375, - "type": "imguizmo", - "translation": [ - -3.971806, - -0.474082, - 2.694148 - ], - "rotation_deg": [ - 0, - -79.436129, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1376, - "type": "imguizmo", - "translation": [ - -3.982937, - -0.369073, - 2.6735 - ], - "rotation_deg": [ - 0, - -79.658734, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1377, - "type": "imguizmo", - "translation": [ - -3.991291, - -0.263808, - 2.652647 - ], - "rotation_deg": [ - 0, - -79.825824, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1378, - "type": "imguizmo", - "translation": [ - -3.996864, - -0.158358, - 2.631647 - ], - "rotation_deg": [ - 0, - -79.937282, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1379, - "type": "imguizmo", - "translation": [ - -3.999652, - -0.052798, - 2.610559 - ], - "rotation_deg": [ - 0, - -79.993031, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1380, - "type": "imguizmo", - "translation": [ - -3.999652, - 0.052798, - 2.589441 - ], - "rotation_deg": [ - 0, - -79.993031, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1381, - "type": "imguizmo", - "translation": [ - -3.996864, - 0.158358, - 2.568353 - ], - "rotation_deg": [ - 0, - -79.937282, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1382, - "type": "imguizmo", - "translation": [ - -3.991291, - 0.263808, - 2.547353 - ], - "rotation_deg": [ - 0, - -79.825824, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1383, - "type": "imguizmo", - "translation": [ - -3.982937, - 0.369073, - 2.5265 - ], - "rotation_deg": [ - 0, - -79.658734, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1384, - "type": "imguizmo", - "translation": [ - -3.971806, - 0.474082, - 2.505852 - ], - "rotation_deg": [ - 0, - -79.436129, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1385, - "type": "imguizmo", - "translation": [ - -3.957908, - 0.57876, - 2.485466 - ], - "rotation_deg": [ - 0, - -79.158163, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1386, - "type": "imguizmo", - "translation": [ - -3.941252, - 0.683035, - 2.465399 - ], - "rotation_deg": [ - 0, - -78.825031, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1387, - "type": "imguizmo", - "translation": [ - -3.921848, - 0.786833, - 2.445708 - ], - "rotation_deg": [ - 0, - -78.436964, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1388, - "type": "imguizmo", - "translation": [ - -3.899712, - 0.890084, - 2.426447 - ], - "rotation_deg": [ - 0, - -77.994233, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1389, - "type": "imguizmo", - "translation": [ - -3.874857, - 0.992714, - 2.407669 - ], - "rotation_deg": [ - 0, - -77.497147, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1390, - "type": "imguizmo", - "translation": [ - -3.847303, - 1.094652, - 2.389427 - ], - "rotation_deg": [ - 0, - -76.946051, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1391, - "type": "imguizmo", - "translation": [ - -3.817067, - 1.195827, - 2.371772 - ], - "rotation_deg": [ - 0, - -76.341331, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1392, - "type": "imguizmo", - "translation": [ - -3.78417, - 1.296169, - 2.354754 - ], - "rotation_deg": [ - 0, - -75.683407, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1393, - "type": "imguizmo", - "translation": [ - -3.748637, - 1.395608, - 2.338419 - ], - "rotation_deg": [ - 0, - -74.972739, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1394, - "type": "imguizmo", - "translation": [ - -3.710491, - 1.494074, - 2.322813 - ], - "rotation_deg": [ - 0, - -74.20982, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1395, - "type": "imguizmo", - "translation": [ - -3.669759, - 1.591499, - 2.307979 - ], - "rotation_deg": [ - 0, - -73.395184, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1396, - "type": "imguizmo", - "translation": [ - -3.62647, - 1.687814, - 2.29396 - ], - "rotation_deg": [ - 0, - -72.529397, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1397, - "type": "imguizmo", - "translation": [ - -3.580653, - 1.782953, - 2.280793 - ], - "rotation_deg": [ - 0, - -71.613063, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1398, - "type": "imguizmo", - "translation": [ - -3.532341, - 1.87685, - 2.268516 - ], - "rotation_deg": [ - 0, - -70.646821, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1399, - "type": "imguizmo", - "translation": [ - -3.481567, - 1.969439, - 2.257163 - ], - "rotation_deg": [ - 0, - -69.631344, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1400, - "type": "imguizmo", - "translation": [ - -3.428367, - 2.060655, - 2.246766 - ], - "rotation_deg": [ - 0, - -68.56734, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1401, - "type": "imguizmo", - "translation": [ - -3.372778, - 2.150435, - 2.237353 - ], - "rotation_deg": [ - 0, - -67.45555, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1402, - "type": "imguizmo", - "translation": [ - -3.314837, - 2.238717, - 2.228951 - ], - "rotation_deg": [ - 0, - -66.29675, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1403, - "type": "imguizmo", - "translation": [ - -3.254587, - 2.325438, - 2.221583 - ], - "rotation_deg": [ - 0, - -65.091746, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1404, - "type": "imguizmo", - "translation": [ - -3.192069, - 2.410539, - 2.21527 - ], - "rotation_deg": [ - 0, - -63.841378, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1405, - "type": "imguizmo", - "translation": [ - -3.127326, - 2.493959, - 2.210029 - ], - "rotation_deg": [ - 0, - -62.546519, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1406, - "type": "imguizmo", - "translation": [ - -3.060403, - 2.575642, - 2.205875 - ], - "rotation_deg": [ - 0, - -61.208069, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1407, - "type": "imguizmo", - "translation": [ - -2.991348, - 2.655529, - 2.202819 - ], - "rotation_deg": [ - 0, - -59.826963, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1408, - "type": "imguizmo", - "translation": [ - -2.920208, - 2.733566, - 2.200871 - ], - "rotation_deg": [ - 0, - -58.404163, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1409, - "type": "imguizmo", - "translation": [ - -2.847033, - 2.809698, - 2.200035 - ], - "rotation_deg": [ - 0, - -56.940659, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1410, - "type": "imguizmo", - "translation": [ - -2.771874, - 2.883872, - 2.200314 - ], - "rotation_deg": [ - 0, - -55.437473, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1411, - "type": "imguizmo", - "translation": [ - -2.694783, - 2.956036, - 2.201706 - ], - "rotation_deg": [ - 0, - -53.895651, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1412, - "type": "imguizmo", - "translation": [ - -2.615813, - 3.026139, - 2.204209 - ], - "rotation_deg": [ - 0, - -52.316269, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1413, - "type": "imguizmo", - "translation": [ - -2.535021, - 3.094134, - 2.207815 - ], - "rotation_deg": [ - 0, - -50.700427, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1414, - "type": "imguizmo", - "translation": [ - -2.452463, - 3.159973, - 2.212514 - ], - "rotation_deg": [ - 0, - -49.049251, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1415, - "type": "imguizmo", - "translation": [ - -2.368195, - 3.223609, - 2.218293 - ], - "rotation_deg": [ - 0, - -47.363891, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1416, - "type": "imguizmo", - "translation": [ - -2.282276, - 3.284999, - 2.225136 - ], - "rotation_deg": [ - 0, - -45.645523, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1417, - "type": "imguizmo", - "translation": [ - -2.194767, - 3.344099, - 2.233024 - ], - "rotation_deg": [ - 0, - -43.895344, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1418, - "type": "imguizmo", - "translation": [ - -2.105729, - 3.400869, - 2.241935 - ], - "rotation_deg": [ - 0, - -42.114573, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1419, - "type": "imguizmo", - "translation": [ - -2.015223, - 3.455268, - 2.251843 - ], - "rotation_deg": [ - 0, - -40.304452, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1420, - "type": "imguizmo", - "translation": [ - -1.923312, - 3.50726, - 2.262722 - ], - "rotation_deg": [ - 0, - -38.466242, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1421, - "type": "imguizmo", - "translation": [ - -1.830061, - 3.556807, - 2.274541 - ], - "rotation_deg": [ - 0, - -36.601225, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1422, - "type": "imguizmo", - "translation": [ - -1.735535, - 3.603875, - 2.287267 - ], - "rotation_deg": [ - 0, - -34.710699, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1423, - "type": "imguizmo", - "translation": [ - -1.639799, - 3.648432, - 2.300865 - ], - "rotation_deg": [ - 0, - -32.795983, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1424, - "type": "imguizmo", - "translation": [ - -1.542921, - 3.690447, - 2.315297 - ], - "rotation_deg": [ - 0, - -30.858411, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1425, - "type": "imguizmo", - "translation": [ - -1.444967, - 3.729889, - 2.330522 - ], - "rotation_deg": [ - 0, - -28.899333, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1426, - "type": "imguizmo", - "translation": [ - -1.346006, - 3.766732, - 2.346498 - ], - "rotation_deg": [ - 0, - -26.920115, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1427, - "type": "imguizmo", - "translation": [ - -1.246107, - 3.80095, - 2.363181 - ], - "rotation_deg": [ - 0, - -24.922136, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1428, - "type": "imguizmo", - "translation": [ - -1.145339, - 3.832518, - 2.380523 - ], - "rotation_deg": [ - 0, - -22.906788, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1429, - "type": "imguizmo", - "translation": [ - -1.043774, - 3.861416, - 2.398478 - ], - "rotation_deg": [ - 0, - -20.875476, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1430, - "type": "imguizmo", - "translation": [ - -0.941481, - 3.887623, - 2.416994 - ], - "rotation_deg": [ - 0, - -18.829615, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1431, - "type": "imguizmo", - "translation": [ - -0.838532, - 3.911121, - 2.43602 - ], - "rotation_deg": [ - 0, - -16.770632, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1432, - "type": "imguizmo", - "translation": [ - -0.734998, - 3.931892, - 2.455503 - ], - "rotation_deg": [ - 0, - -14.699961, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1433, - "type": "imguizmo", - "translation": [ - -0.630952, - 3.949924, - 2.475389 - ], - "rotation_deg": [ - 0, - -12.619046, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1434, - "type": "imguizmo", - "translation": [ - -0.526467, - 3.965203, - 2.495623 - ], - "rotation_deg": [ - 0, - -10.529336, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1435, - "type": "imguizmo", - "translation": [ - -0.421614, - 3.977718, - 2.516147 - ], - "rotation_deg": [ - 0, - -8.432288, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1436, - "type": "imguizmo", - "translation": [ - -0.316468, - 3.987461, - 2.536905 - ], - "rotation_deg": [ - 0, - -6.329363, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1437, - "type": "imguizmo", - "translation": [ - -0.211101, - 3.994426, - 2.557839 - ], - "rotation_deg": [ - 0, - -4.222028, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1438, - "type": "imguizmo", - "translation": [ - -0.105587, - 3.998606, - 2.57889 - ], - "rotation_deg": [ - 0, - -2.11175, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1439, - "type": "imguizmo", - "translation": [ - 0, - 4, - 2.6 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1440, - "type": "action", - "action": "set_active_render_window", - "value": 0 - }, - { - "frame": 1440, - "type": "action", - "action": "set_active_planar", - "value": 6 - }, - { - "frame": 1440, - "type": "action", - "action": "set_projection_type", - "value": "perspective" - }, - { - "frame": 1440, - "type": "action", - "action": "set_left_handed", - "value": true - }, - { - "frame": 1440, - "type": "action", - "action": "reset_active_camera", - "value": 1 - }, - { - "frame": 1440, - "type": "action", - "action": "set_active_render_window", - "value": 1 - }, - { - "frame": 1440, - "type": "action", - "action": "set_active_planar", - "value": 6 - }, - { - "frame": 1440, - "type": "action", - "action": "set_projection_type", - "value": "orthographic" - }, - { - "frame": 1440, - "type": "action", - "action": "set_left_handed", - "value": true - }, - { - "frame": 1440, - "type": "action", - "action": "set_active_render_window", - "value": 0 - }, - { - "frame": 1441, - "type": "imguizmo", - "translation": [ - 0, - 2.5, - 2.55 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1442, - "type": "imguizmo", - "translation": [ - 0.105587, - 2.499129, - 2.549512 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1443, - "type": "imguizmo", - "translation": [ - 0.211101, - 2.496516, - 2.54805 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1444, - "type": "imguizmo", - "translation": [ - 0.316468, - 2.492163, - 2.545618 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1445, - "type": "imguizmo", - "translation": [ - 0.421614, - 2.486074, - 2.542223 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1446, - "type": "imguizmo", - "translation": [ - 0.526467, - 2.478252, - 2.537874 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1447, - "type": "imguizmo", - "translation": [ - 0.630952, - 2.468702, - 2.532583 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1448, - "type": "imguizmo", - "translation": [ - 0.734998, - 2.457433, - 2.526365 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1449, - "type": "imguizmo", - "translation": [ - 0.838532, - 2.44445, - 2.519238 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1450, - "type": "imguizmo", - "translation": [ - 0.941481, - 2.429764, - 2.511221 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1451, - "type": "imguizmo", - "translation": [ - 1.043774, - 2.413385, - 2.502336 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1452, - "type": "imguizmo", - "translation": [ - 1.145339, - 2.395324, - 2.492609 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1453, - "type": "imguizmo", - "translation": [ - 1.246107, - 2.375594, - 2.482066 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1454, - "type": "imguizmo", - "translation": [ - 1.346006, - 2.354207, - 2.470737 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1455, - "type": "imguizmo", - "translation": [ - 1.444967, - 2.331181, - 2.458653 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1456, - "type": "imguizmo", - "translation": [ - 1.542921, - 2.306529, - 2.445849 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1457, - "type": "imguizmo", - "translation": [ - 1.639799, - 2.28027, - 2.432359 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1458, - "type": "imguizmo", - "translation": [ - 1.735535, - 2.252422, - 2.418221 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1459, - "type": "imguizmo", - "translation": [ - 1.830061, - 2.223004, - 2.403476 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1460, - "type": "imguizmo", - "translation": [ - 1.923312, - 2.192037, - 2.388163 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1461, - "type": "imguizmo", - "translation": [ - 2.015223, - 2.159543, - 2.372326 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1462, - "type": "imguizmo", - "translation": [ - 2.105729, - 2.125543, - 2.356008 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1463, - "type": "imguizmo", - "translation": [ - 2.194767, - 2.090062, - 2.339256 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1464, - "type": "imguizmo", - "translation": [ - 2.282276, - 2.053124, - 2.322116 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1465, - "type": "imguizmo", - "translation": [ - 2.368195, - 2.014756, - 2.304635 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1466, - "type": "imguizmo", - "translation": [ - 2.452463, - 1.974983, - 2.286862 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1467, - "type": "imguizmo", - "translation": [ - 2.535021, - 1.933834, - 2.268848 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1468, - "type": "imguizmo", - "translation": [ - 2.615813, - 1.891337, - 2.250641 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1469, - "type": "imguizmo", - "translation": [ - 2.694783, - 1.847522, - 2.232294 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1470, - "type": "imguizmo", - "translation": [ - 2.771874, - 1.80242, - 2.213856 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1471, - "type": "imguizmo", - "translation": [ - 2.847033, - 1.756061, - 2.19538 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1472, - "type": "imguizmo", - "translation": [ - 2.920208, - 1.708479, - 2.176917 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1473, - "type": "imguizmo", - "translation": [ - 2.991348, - 1.659706, - 2.158518 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1474, - "type": "imguizmo", - "translation": [ - 3.060403, - 1.609776, - 2.140234 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1475, - "type": "imguizmo", - "translation": [ - 3.127326, - 1.558725, - 2.122118 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1476, - "type": "imguizmo", - "translation": [ - 3.192069, - 1.506587, - 2.104218 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1477, - "type": "imguizmo", - "translation": [ - 3.254587, - 1.453399, - 2.086585 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1478, - "type": "imguizmo", - "translation": [ - 3.314837, - 1.399198, - 2.069269 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1479, - "type": "imguizmo", - "translation": [ - 3.372778, - 1.344022, - 2.052316 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1480, - "type": "imguizmo", - "translation": [ - 3.428367, - 1.287909, - 2.035776 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1481, - "type": "imguizmo", - "translation": [ - 3.481567, - 1.230899, - 2.019693 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1482, - "type": "imguizmo", - "translation": [ - 3.532341, - 1.173031, - 2.004112 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1483, - "type": "imguizmo", - "translation": [ - 3.580653, - 1.114346, - 1.989078 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1484, - "type": "imguizmo", - "translation": [ - 3.62647, - 1.054884, - 1.974631 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1485, - "type": "imguizmo", - "translation": [ - 3.669759, - 0.994687, - 1.960813 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1486, - "type": "imguizmo", - "translation": [ - 3.710491, - 0.933796, - 1.947661 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1487, - "type": "imguizmo", - "translation": [ - 3.748637, - 0.872255, - 1.935213 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1488, - "type": "imguizmo", - "translation": [ - 3.78417, - 0.810106, - 1.923502 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1489, - "type": "imguizmo", - "translation": [ - 3.817067, - 0.747392, - 1.912563 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1490, - "type": "imguizmo", - "translation": [ - 3.847303, - 0.684157, - 1.902424 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1491, - "type": "imguizmo", - "translation": [ - 3.874857, - 0.620446, - 1.893115 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1492, - "type": "imguizmo", - "translation": [ - 3.899712, - 0.556302, - 1.884661 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1493, - "type": "imguizmo", - "translation": [ - 3.921848, - 0.491771, - 1.877086 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1494, - "type": "imguizmo", - "translation": [ - 3.941252, - 0.426897, - 1.870411 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1495, - "type": "imguizmo", - "translation": [ - 3.957908, - 0.361725, - 1.864655 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1496, - "type": "imguizmo", - "translation": [ - 3.971806, - 0.296301, - 1.859833 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1497, - "type": "imguizmo", - "translation": [ - 3.982937, - 0.230671, - 1.855959 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1498, - "type": "imguizmo", - "translation": [ - 3.991291, - 0.16488, - 1.853045 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1499, - "type": "imguizmo", - "translation": [ - 3.996864, - 0.098974, - 1.851097 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1500, - "type": "imguizmo", - "translation": [ - 3.999652, - 0.032999, - 1.850122 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1501, - "type": "imguizmo", - "translation": [ - 3.999652, - -0.032999, - 1.850122 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1502, - "type": "imguizmo", - "translation": [ - 3.996864, - -0.098974, - 1.851097 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1503, - "type": "imguizmo", - "translation": [ - 3.991291, - -0.16488, - 1.853045 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1504, - "type": "imguizmo", - "translation": [ - 3.982937, - -0.230671, - 1.855959 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1505, - "type": "imguizmo", - "translation": [ - 3.971806, - -0.296301, - 1.859833 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1506, - "type": "imguizmo", - "translation": [ - 3.957908, - -0.361725, - 1.864655 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1507, - "type": "imguizmo", - "translation": [ - 3.941252, - -0.426897, - 1.870411 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1508, - "type": "imguizmo", - "translation": [ - 3.921848, - -0.491771, - 1.877086 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1509, - "type": "imguizmo", - "translation": [ - 3.899712, - -0.556302, - 1.884661 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1510, - "type": "imguizmo", - "translation": [ - 3.874857, - -0.620446, - 1.893115 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1511, - "type": "imguizmo", - "translation": [ - 3.847303, - -0.684157, - 1.902424 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1512, - "type": "imguizmo", - "translation": [ - 3.817067, - -0.747392, - 1.912563 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1513, - "type": "imguizmo", - "translation": [ - 3.78417, - -0.810106, - 1.923502 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1514, - "type": "imguizmo", - "translation": [ - 3.748637, - -0.872255, - 1.935213 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1515, - "type": "imguizmo", - "translation": [ - 3.710491, - -0.933796, - 1.947661 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1516, - "type": "imguizmo", - "translation": [ - 3.669759, - -0.994687, - 1.960813 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1517, - "type": "imguizmo", - "translation": [ - 3.62647, - -1.054884, - 1.974631 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1518, - "type": "imguizmo", - "translation": [ - 3.580653, - -1.114346, - 1.989078 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1519, - "type": "imguizmo", - "translation": [ - 3.532341, - -1.173031, - 2.004112 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1520, - "type": "imguizmo", - "translation": [ - 3.481567, - -1.230899, - 2.019693 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1521, - "type": "imguizmo", - "translation": [ - 3.428367, - -1.287909, - 2.035776 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1522, - "type": "imguizmo", - "translation": [ - 3.372778, - -1.344022, - 2.052316 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1523, - "type": "imguizmo", - "translation": [ - 3.314837, - -1.399198, - 2.069269 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1524, - "type": "imguizmo", - "translation": [ - 3.254587, - -1.453399, - 2.086585 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1525, - "type": "imguizmo", - "translation": [ - 3.192069, - -1.506587, - 2.104218 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1526, - "type": "imguizmo", - "translation": [ - 3.127326, - -1.558725, - 2.122118 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1527, - "type": "imguizmo", - "translation": [ - 3.060403, - -1.609776, - 2.140234 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1528, - "type": "imguizmo", - "translation": [ - 2.991348, - -1.659706, - 2.158518 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1529, - "type": "imguizmo", - "translation": [ - 2.920208, - -1.708479, - 2.176917 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1530, - "type": "imguizmo", - "translation": [ - 2.847033, - -1.756061, - 2.19538 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1531, - "type": "imguizmo", - "translation": [ - 2.771874, - -1.80242, - 2.213856 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1532, - "type": "imguizmo", - "translation": [ - 2.694783, - -1.847522, - 2.232294 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1533, - "type": "imguizmo", - "translation": [ - 2.615813, - -1.891337, - 2.250641 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1534, - "type": "imguizmo", - "translation": [ - 2.535021, - -1.933834, - 2.268848 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1535, - "type": "imguizmo", - "translation": [ - 2.452463, - -1.974983, - 2.286862 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1536, - "type": "imguizmo", - "translation": [ - 2.368195, - -2.014756, - 2.304635 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1537, - "type": "imguizmo", - "translation": [ - 2.282276, - -2.053124, - 2.322116 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1538, - "type": "imguizmo", - "translation": [ - 2.194767, - -2.090062, - 2.339256 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1539, - "type": "imguizmo", - "translation": [ - 2.105729, - -2.125543, - 2.356008 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1540, - "type": "imguizmo", - "translation": [ - 2.015223, - -2.159543, - 2.372326 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1541, - "type": "imguizmo", - "translation": [ - 1.923312, - -2.192037, - 2.388163 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1542, - "type": "imguizmo", - "translation": [ - 1.830061, - -2.223004, - 2.403476 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1543, - "type": "imguizmo", - "translation": [ - 1.735535, - -2.252422, - 2.418221 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1544, - "type": "imguizmo", - "translation": [ - 1.639799, - -2.28027, - 2.432359 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1545, - "type": "imguizmo", - "translation": [ - 1.542921, - -2.306529, - 2.445849 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1546, - "type": "imguizmo", - "translation": [ - 1.444967, - -2.331181, - 2.458653 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1547, - "type": "imguizmo", - "translation": [ - 1.346006, - -2.354207, - 2.470737 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1548, - "type": "imguizmo", - "translation": [ - 1.246107, - -2.375594, - 2.482066 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1549, - "type": "imguizmo", - "translation": [ - 1.145339, - -2.395324, - 2.492609 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1550, - "type": "imguizmo", - "translation": [ - 1.043774, - -2.413385, - 2.502336 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1551, - "type": "imguizmo", - "translation": [ - 0.941481, - -2.429764, - 2.511221 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1552, - "type": "imguizmo", - "translation": [ - 0.838532, - -2.44445, - 2.519238 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1553, - "type": "imguizmo", - "translation": [ - 0.734998, - -2.457433, - 2.526365 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1554, - "type": "imguizmo", - "translation": [ - 0.630952, - -2.468702, - 2.532583 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1555, - "type": "imguizmo", - "translation": [ - 0.526467, - -2.478252, - 2.537874 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1556, - "type": "imguizmo", - "translation": [ - 0.421614, - -2.486074, - 2.542223 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1557, - "type": "imguizmo", - "translation": [ - 0.316468, - -2.492163, - 2.545618 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1558, - "type": "imguizmo", - "translation": [ - 0.211101, - -2.496516, - 2.54805 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1559, - "type": "imguizmo", - "translation": [ - 0.105587, - -2.499129, - 2.549512 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1560, - "type": "imguizmo", - "translation": [ - 0, - -2.5, - 2.55 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1561, - "type": "imguizmo", - "translation": [ - -0.105587, - -2.499129, - 2.549512 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1562, - "type": "imguizmo", - "translation": [ - -0.211101, - -2.496516, - 2.54805 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1563, - "type": "imguizmo", - "translation": [ - -0.316468, - -2.492163, - 2.545618 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1564, - "type": "imguizmo", - "translation": [ - -0.421614, - -2.486074, - 2.542223 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1565, - "type": "imguizmo", - "translation": [ - -0.526467, - -2.478252, - 2.537874 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1566, - "type": "imguizmo", - "translation": [ - -0.630952, - -2.468702, - 2.532583 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1567, - "type": "imguizmo", - "translation": [ - -0.734998, - -2.457433, - 2.526365 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1568, - "type": "imguizmo", - "translation": [ - -0.838532, - -2.44445, - 2.519238 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1569, - "type": "imguizmo", - "translation": [ - -0.941481, - -2.429764, - 2.511221 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1570, - "type": "imguizmo", - "translation": [ - -1.043774, - -2.413385, - 2.502336 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1571, - "type": "imguizmo", - "translation": [ - -1.145339, - -2.395324, - 2.492609 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1572, - "type": "imguizmo", - "translation": [ - -1.246107, - -2.375594, - 2.482066 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1573, - "type": "imguizmo", - "translation": [ - -1.346006, - -2.354207, - 2.470737 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1574, - "type": "imguizmo", - "translation": [ - -1.444967, - -2.331181, - 2.458653 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1575, - "type": "imguizmo", - "translation": [ - -1.542921, - -2.306529, - 2.445849 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1576, - "type": "imguizmo", - "translation": [ - -1.639799, - -2.28027, - 2.432359 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1577, - "type": "imguizmo", - "translation": [ - -1.735535, - -2.252422, - 2.418221 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1578, - "type": "imguizmo", - "translation": [ - -1.830061, - -2.223004, - 2.403476 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1579, - "type": "imguizmo", - "translation": [ - -1.923312, - -2.192037, - 2.388163 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1580, - "type": "imguizmo", - "translation": [ - -2.015223, - -2.159543, - 2.372326 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1581, - "type": "imguizmo", - "translation": [ - -2.105729, - -2.125543, - 2.356008 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1582, - "type": "imguizmo", - "translation": [ - -2.194767, - -2.090062, - 2.339256 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1583, - "type": "imguizmo", - "translation": [ - -2.282276, - -2.053124, - 2.322116 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1584, - "type": "imguizmo", - "translation": [ - -2.368195, - -2.014756, - 2.304635 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1585, - "type": "imguizmo", - "translation": [ - -2.452463, - -1.974983, - 2.286862 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1586, - "type": "imguizmo", - "translation": [ - -2.535021, - -1.933834, - 2.268848 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1587, - "type": "imguizmo", - "translation": [ - -2.615813, - -1.891337, - 2.250641 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1588, - "type": "imguizmo", - "translation": [ - -2.694783, - -1.847522, - 2.232294 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1589, - "type": "imguizmo", - "translation": [ - -2.771874, - -1.80242, - 2.213856 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1590, - "type": "imguizmo", - "translation": [ - -2.847033, - -1.756061, - 2.19538 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1591, - "type": "imguizmo", - "translation": [ - -2.920208, - -1.708479, - 2.176917 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1592, - "type": "imguizmo", - "translation": [ - -2.991348, - -1.659706, - 2.158518 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1593, - "type": "imguizmo", - "translation": [ - -3.060403, - -1.609776, - 2.140234 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1594, - "type": "imguizmo", - "translation": [ - -3.127326, - -1.558725, - 2.122118 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1595, - "type": "imguizmo", - "translation": [ - -3.192069, - -1.506587, - 2.104218 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1596, - "type": "imguizmo", - "translation": [ - -3.254587, - -1.453399, - 2.086585 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1597, - "type": "imguizmo", - "translation": [ - -3.314837, - -1.399198, - 2.069269 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1598, - "type": "imguizmo", - "translation": [ - -3.372778, - -1.344022, - 2.052316 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1599, - "type": "imguizmo", - "translation": [ - -3.428367, - -1.287909, - 2.035776 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1600, - "type": "imguizmo", - "translation": [ - -3.481567, - -1.230899, - 2.019693 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1601, - "type": "imguizmo", - "translation": [ - -3.532341, - -1.173031, - 2.004112 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1602, - "type": "imguizmo", - "translation": [ - -3.580653, - -1.114346, - 1.989078 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1603, - "type": "imguizmo", - "translation": [ - -3.62647, - -1.054884, - 1.974631 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1604, - "type": "imguizmo", - "translation": [ - -3.669759, - -0.994687, - 1.960813 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1605, - "type": "imguizmo", - "translation": [ - -3.710491, - -0.933796, - 1.947661 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1606, - "type": "imguizmo", - "translation": [ - -3.748637, - -0.872255, - 1.935213 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1607, - "type": "imguizmo", - "translation": [ - -3.78417, - -0.810106, - 1.923502 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1608, - "type": "imguizmo", - "translation": [ - -3.817067, - -0.747392, - 1.912563 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1609, - "type": "imguizmo", - "translation": [ - -3.847303, - -0.684157, - 1.902424 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1610, - "type": "imguizmo", - "translation": [ - -3.874857, - -0.620446, - 1.893115 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1611, - "type": "imguizmo", - "translation": [ - -3.899712, - -0.556302, - 1.884661 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1612, - "type": "imguizmo", - "translation": [ - -3.921848, - -0.491771, - 1.877086 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1613, - "type": "imguizmo", - "translation": [ - -3.941252, - -0.426897, - 1.870411 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1614, - "type": "imguizmo", - "translation": [ - -3.957908, - -0.361725, - 1.864655 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1615, - "type": "imguizmo", - "translation": [ - -3.971806, - -0.296301, - 1.859833 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1616, - "type": "imguizmo", - "translation": [ - -3.982937, - -0.230671, - 1.855959 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1617, - "type": "imguizmo", - "translation": [ - -3.991291, - -0.16488, - 1.853045 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1618, - "type": "imguizmo", - "translation": [ - -3.996864, - -0.098974, - 1.851097 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1619, - "type": "imguizmo", - "translation": [ - -3.999652, - -0.032999, - 1.850122 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1620, - "type": "imguizmo", - "translation": [ - -3.999652, - 0.032999, - 1.850122 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1621, - "type": "imguizmo", - "translation": [ - -3.996864, - 0.098974, - 1.851097 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1622, - "type": "imguizmo", - "translation": [ - -3.991291, - 0.16488, - 1.853045 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1623, - "type": "imguizmo", - "translation": [ - -3.982937, - 0.230671, - 1.855959 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1624, - "type": "imguizmo", - "translation": [ - -3.971806, - 0.296301, - 1.859833 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1625, - "type": "imguizmo", - "translation": [ - -3.957908, - 0.361725, - 1.864655 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1626, - "type": "imguizmo", - "translation": [ - -3.941252, - 0.426897, - 1.870411 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1627, - "type": "imguizmo", - "translation": [ - -3.921848, - 0.491771, - 1.877086 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1628, - "type": "imguizmo", - "translation": [ - -3.899712, - 0.556302, - 1.884661 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1629, - "type": "imguizmo", - "translation": [ - -3.874857, - 0.620446, - 1.893115 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1630, - "type": "imguizmo", - "translation": [ - -3.847303, - 0.684157, - 1.902424 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1631, - "type": "imguizmo", - "translation": [ - -3.817067, - 0.747392, - 1.912563 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1632, - "type": "imguizmo", - "translation": [ - -3.78417, - 0.810106, - 1.923502 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1633, - "type": "imguizmo", - "translation": [ - -3.748637, - 0.872255, - 1.935213 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1634, - "type": "imguizmo", - "translation": [ - -3.710491, - 0.933796, - 1.947661 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1635, - "type": "imguizmo", - "translation": [ - -3.669759, - 0.994687, - 1.960813 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1636, - "type": "imguizmo", - "translation": [ - -3.62647, - 1.054884, - 1.974631 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1637, - "type": "imguizmo", - "translation": [ - -3.580653, - 1.114346, - 1.989078 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1638, - "type": "imguizmo", - "translation": [ - -3.532341, - 1.173031, - 2.004112 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1639, - "type": "imguizmo", - "translation": [ - -3.481567, - 1.230899, - 2.019693 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1640, - "type": "imguizmo", - "translation": [ - -3.428367, - 1.287909, - 2.035776 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1641, - "type": "imguizmo", - "translation": [ - -3.372778, - 1.344022, - 2.052316 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1642, - "type": "imguizmo", - "translation": [ - -3.314837, - 1.399198, - 2.069269 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1643, - "type": "imguizmo", - "translation": [ - -3.254587, - 1.453399, - 2.086585 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1644, - "type": "imguizmo", - "translation": [ - -3.192069, - 1.506587, - 2.104218 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1645, - "type": "imguizmo", - "translation": [ - -3.127326, - 1.558725, - 2.122118 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1646, - "type": "imguizmo", - "translation": [ - -3.060403, - 1.609776, - 2.140234 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1647, - "type": "imguizmo", - "translation": [ - -2.991348, - 1.659706, - 2.158518 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1648, - "type": "imguizmo", - "translation": [ - -2.920208, - 1.708479, - 2.176917 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1649, - "type": "imguizmo", - "translation": [ - -2.847033, - 1.756061, - 2.19538 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1650, - "type": "imguizmo", - "translation": [ - -2.771874, - 1.80242, - 2.213856 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1651, - "type": "imguizmo", - "translation": [ - -2.694783, - 1.847522, - 2.232294 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1652, - "type": "imguizmo", - "translation": [ - -2.615813, - 1.891337, - 2.250641 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1653, - "type": "imguizmo", - "translation": [ - -2.535021, - 1.933834, - 2.268848 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1654, - "type": "imguizmo", - "translation": [ - -2.452463, - 1.974983, - 2.286862 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1655, - "type": "imguizmo", - "translation": [ - -2.368195, - 2.014756, - 2.304635 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1656, - "type": "imguizmo", - "translation": [ - -2.282276, - 2.053124, - 2.322116 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1657, - "type": "imguizmo", - "translation": [ - -2.194767, - 2.090062, - 2.339256 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1658, - "type": "imguizmo", - "translation": [ - -2.105729, - 2.125543, - 2.356008 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1659, - "type": "imguizmo", - "translation": [ - -2.015223, - 2.159543, - 2.372326 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1660, - "type": "imguizmo", - "translation": [ - -1.923312, - 2.192037, - 2.388163 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1661, - "type": "imguizmo", - "translation": [ - -1.830061, - 2.223004, - 2.403476 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1662, - "type": "imguizmo", - "translation": [ - -1.735535, - 2.252422, - 2.418221 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1663, - "type": "imguizmo", - "translation": [ - -1.639799, - 2.28027, - 2.432359 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1664, - "type": "imguizmo", - "translation": [ - -1.542921, - 2.306529, - 2.445849 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1665, - "type": "imguizmo", - "translation": [ - -1.444967, - 2.331181, - 2.458653 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1666, - "type": "imguizmo", - "translation": [ - -1.346006, - 2.354207, - 2.470737 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1667, - "type": "imguizmo", - "translation": [ - -1.246107, - 2.375594, - 2.482066 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1668, - "type": "imguizmo", - "translation": [ - -1.145339, - 2.395324, - 2.492609 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1669, - "type": "imguizmo", - "translation": [ - -1.043774, - 2.413385, - 2.502336 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1670, - "type": "imguizmo", - "translation": [ - -0.941481, - 2.429764, - 2.511221 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1671, - "type": "imguizmo", - "translation": [ - -0.838532, - 2.44445, - 2.519238 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1672, - "type": "imguizmo", - "translation": [ - -0.734998, - 2.457433, - 2.526365 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1673, - "type": "imguizmo", - "translation": [ - -0.630952, - 2.468702, - 2.532583 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1674, - "type": "imguizmo", - "translation": [ - -0.526467, - 2.478252, - 2.537874 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1675, - "type": "imguizmo", - "translation": [ - -0.421614, - 2.486074, - 2.542223 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1676, - "type": "imguizmo", - "translation": [ - -0.316468, - 2.492163, - 2.545618 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1677, - "type": "imguizmo", - "translation": [ - -0.211101, - 2.496516, - 2.54805 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1678, - "type": "imguizmo", - "translation": [ - -0.105587, - 2.499129, - 2.549512 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1679, - "type": "imguizmo", - "translation": [ - 0, - 2.5, - 2.55 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1680, - "type": "action", - "action": "set_active_render_window", - "value": 0 - }, - { - "frame": 1680, - "type": "action", - "action": "set_active_planar", - "value": 7 - }, - { - "frame": 1680, - "type": "action", - "action": "set_projection_type", - "value": "perspective" - }, - { - "frame": 1680, - "type": "action", - "action": "set_left_handed", - "value": true - }, - { - "frame": 1680, - "type": "action", - "action": "reset_active_camera", - "value": 1 - }, - { - "frame": 1680, - "type": "action", - "action": "set_active_render_window", - "value": 1 - }, - { - "frame": 1680, - "type": "action", - "action": "set_active_planar", - "value": 7 - }, - { - "frame": 1680, - "type": "action", - "action": "set_projection_type", - "value": "orthographic" - }, - { - "frame": 1680, - "type": "action", - "action": "set_left_handed", - "value": true - }, - { - "frame": 1680, - "type": "action", - "action": "set_active_render_window", - "value": 0 - }, - { - "frame": 1681, - "type": "imguizmo", - "translation": [ - 0, - 3.6, - 6.1 - ], - "rotation_deg": [ - 0, - 75, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1682, - "type": "imguizmo", - "translation": [ - 0.126705, - 3.631665, - 6.099686 - ], - "rotation_deg": [ - 0.475144, - 74.993031, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1683, - "type": "imguizmo", - "translation": [ - 0.253322, - 3.663242, - 6.098746 - ], - "rotation_deg": [ - 0.949956, - 74.972128, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1684, - "type": "imguizmo", - "translation": [ - 0.379762, - 3.694643, - 6.097179 - ], - "rotation_deg": [ - 1.424107, - 74.937307, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1685, - "type": "imguizmo", - "translation": [ - 0.505937, - 3.72578, - 6.094987 - ], - "rotation_deg": [ - 1.897265, - 74.888591, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1686, - "type": "imguizmo", - "translation": [ - 0.63176, - 3.756566, - 6.092171 - ], - "rotation_deg": [ - 2.369101, - 74.826014, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1687, - "type": "imguizmo", - "translation": [ - 0.757143, - 3.786916, - 6.088733 - ], - "rotation_deg": [ - 2.839285, - 74.74962, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1688, - "type": "imguizmo", - "translation": [ - 0.881998, - 3.816745, - 6.084676 - ], - "rotation_deg": [ - 3.307491, - 74.659462, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1689, - "type": "imguizmo", - "translation": [ - 1.006238, - 3.84597, - 6.080002 - ], - "rotation_deg": [ - 3.773392, - 74.555603, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1690, - "type": "imguizmo", - "translation": [ - 1.129777, - 3.874509, - 6.074715 - ], - "rotation_deg": [ - 4.236663, - 74.438116, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1691, - "type": "imguizmo", - "translation": [ - 1.252529, - 3.902283, - 6.068819 - ], - "rotation_deg": [ - 4.696982, - 74.307082, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1692, - "type": "imguizmo", - "translation": [ - 1.374407, - 3.929215, - 6.062317 - ], - "rotation_deg": [ - 5.154027, - 74.162592, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1693, - "type": "imguizmo", - "translation": [ - 1.495328, - 3.955229, - 6.055214 - ], - "rotation_deg": [ - 5.607481, - 74.004748, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1694, - "type": "imguizmo", - "translation": [ - 1.615207, - 3.980253, - 6.047515 - ], - "rotation_deg": [ - 6.057026, - 73.833659, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1695, - "type": "imguizmo", - "translation": [ - 1.73396, - 4.004217, - 6.039225 - ], - "rotation_deg": [ - 6.50235, - 73.649445, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1696, - "type": "imguizmo", - "translation": [ - 1.851505, - 4.027055, - 6.03035 - ], - "rotation_deg": [ - 6.943142, - 73.452233, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1697, - "type": "imguizmo", - "translation": [ - 1.967759, - 4.048702, - 6.020897 - ], - "rotation_deg": [ - 7.379096, - 73.242162, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1698, - "type": "imguizmo", - "translation": [ - 2.082642, - 4.069099, - 6.010872 - ], - "rotation_deg": [ - 7.809907, - 73.019377, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1699, - "type": "imguizmo", - "translation": [ - 2.196073, - 4.088188, - 6.000282 - ], - "rotation_deg": [ - 8.235276, - 72.784035, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1700, - "type": "imguizmo", - "translation": [ - 2.307975, - 4.105917, - 5.989133 - ], - "rotation_deg": [ - 8.654905, - 72.536298, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1701, - "type": "imguizmo", - "translation": [ - 2.418267, - 4.122235, - 5.977435 - ], - "rotation_deg": [ - 9.068502, - 72.276341, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1702, - "type": "imguizmo", - "translation": [ - 2.526874, - 4.137098, - 5.965195 - ], - "rotation_deg": [ - 9.475779, - 72.004343, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1703, - "type": "imguizmo", - "translation": [ - 2.633721, - 4.150464, - 5.952422 - ], - "rotation_deg": [ - 9.876452, - 71.720494, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1704, - "type": "imguizmo", - "translation": [ - 2.738731, - 4.162296, - 5.939125 - ], - "rotation_deg": [ - 10.270243, - 71.424993, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1705, - "type": "imguizmo", - "translation": [ - 2.841833, - 4.17256, - 5.925312 - ], - "rotation_deg": [ - 10.656876, - 71.118045, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1706, - "type": "imguizmo", - "translation": [ - 2.942955, - 4.181229, - 5.910994 - ], - "rotation_deg": [ - 11.036081, - 70.799864, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1707, - "type": "imguizmo", - "translation": [ - 3.042026, - 4.188277, - 5.89618 - ], - "rotation_deg": [ - 11.407596, - 70.470671, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1708, - "type": "imguizmo", - "translation": [ - 3.138976, - 4.193686, - 5.880881 - ], - "rotation_deg": [ - 11.771161, - 70.130697, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1709, - "type": "imguizmo", - "translation": [ - 3.233739, - 4.197441, - 5.865108 - ], - "rotation_deg": [ - 12.126522, - 69.780178, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1710, - "type": "imguizmo", - "translation": [ - 3.326248, - 4.19953, - 5.848871 - ], - "rotation_deg": [ - 12.473431, - 69.419359, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1711, - "type": "imguizmo", - "translation": [ - 3.41644, - 4.199948, - 5.832182 - ], - "rotation_deg": [ - 12.811648, - 69.04849, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1712, - "type": "imguizmo", - "translation": [ - 3.50425, - 4.198694, - 5.815052 - ], - "rotation_deg": [ - 13.140937, - 68.667831, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1713, - "type": "imguizmo", - "translation": [ - 3.589618, - 4.195771, - 5.797494 - ], - "rotation_deg": [ - 13.461067, - 68.277647, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1714, - "type": "imguizmo", - "translation": [ - 3.672484, - 4.191188, - 5.779519 - ], - "rotation_deg": [ - 13.771816, - 67.878209, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1715, - "type": "imguizmo", - "translation": [ - 3.752791, - 4.184957, - 5.761141 - ], - "rotation_deg": [ - 14.072967, - 67.469796, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1716, - "type": "imguizmo", - "translation": [ - 3.830483, - 4.177095, - 5.742371 - ], - "rotation_deg": [ - 14.36431, - 67.052693, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1717, - "type": "imguizmo", - "translation": [ - 3.905505, - 4.167626, - 5.723224 - ], - "rotation_deg": [ - 14.645643, - 66.62719, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1718, - "type": "imguizmo", - "translation": [ - 3.977805, - 4.156574, - 5.703711 - ], - "rotation_deg": [ - 14.916769, - 66.193583, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1719, - "type": "imguizmo", - "translation": [ - 4.047333, - 4.14397, - 5.683848 - ], - "rotation_deg": [ - 15.177499, - 65.752176, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1720, - "type": "imguizmo", - "translation": [ - 4.11404, - 4.129851, - 5.663647 - ], - "rotation_deg": [ - 15.427652, - 65.303276, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1721, - "type": "imguizmo", - "translation": [ - 4.177881, - 4.114255, - 5.643124 - ], - "rotation_deg": [ - 15.667052, - 64.847195, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1722, - "type": "imguizmo", - "translation": [ - 4.238809, - 4.097226, - 5.622291 - ], - "rotation_deg": [ - 15.895535, - 64.384251, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1723, - "type": "imguizmo", - "translation": [ - 4.296784, - 4.07881, - 5.601165 - ], - "rotation_deg": [ - 16.112939, - 63.914767, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1724, - "type": "imguizmo", - "translation": [ - 4.351764, - 4.059061, - 5.579758 - ], - "rotation_deg": [ - 16.319114, - 63.43907, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1725, - "type": "imguizmo", - "translation": [ - 4.403711, - 4.038031, - 5.558087 - ], - "rotation_deg": [ - 16.513916, - 62.957493, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1726, - "type": "imguizmo", - "translation": [ - 4.452589, - 4.015781, - 5.536167 - ], - "rotation_deg": [ - 16.69721, - 62.470369, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1727, - "type": "imguizmo", - "translation": [ - 4.498364, - 3.992372, - 5.514012 - ], - "rotation_deg": [ - 16.868866, - 61.978039, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1728, - "type": "imguizmo", - "translation": [ - 4.541004, - 3.967869, - 5.491638 - ], - "rotation_deg": [ - 17.028767, - 61.480846, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1729, - "type": "imguizmo", - "translation": [ - 4.58048, - 3.942341, - 5.469061 - ], - "rotation_deg": [ - 17.1768, - 60.979136, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1730, - "type": "imguizmo", - "translation": [ - 4.616763, - 3.915859, - 5.446297 - ], - "rotation_deg": [ - 17.312862, - 60.47326, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1731, - "type": "imguizmo", - "translation": [ - 4.649829, - 3.888497, - 5.423361 - ], - "rotation_deg": [ - 17.436858, - 59.963569, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1732, - "type": "imguizmo", - "translation": [ - 4.679654, - 3.86033, - 5.400269 - ], - "rotation_deg": [ - 17.548702, - 59.450419, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1733, - "type": "imguizmo", - "translation": [ - 4.706218, - 3.831438, - 5.377038 - ], - "rotation_deg": [ - 17.648317, - 58.934167, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1734, - "type": "imguizmo", - "translation": [ - 4.729502, - 3.801901, - 5.353683 - ], - "rotation_deg": [ - 17.735632, - 58.415173, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1735, - "type": "imguizmo", - "translation": [ - 4.74949, - 3.771801, - 5.330221 - ], - "rotation_deg": [ - 17.810587, - 57.8938, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1736, - "type": "imguizmo", - "translation": [ - 4.766168, - 3.741222, - 5.306668 - ], - "rotation_deg": [ - 17.873129, - 57.37041, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1737, - "type": "imguizmo", - "translation": [ - 4.779524, - 3.71025, - 5.283042 - ], - "rotation_deg": [ - 17.923215, - 56.845367, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1738, - "type": "imguizmo", - "translation": [ - 4.789549, - 3.67897, - 5.259357 - ], - "rotation_deg": [ - 17.96081, - 56.319039, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1739, - "type": "imguizmo", - "translation": [ - 4.796237, - 3.64747, - 5.235631 - ], - "rotation_deg": [ - 17.985888, - 55.791791, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1740, - "type": "imguizmo", - "translation": [ - 4.799582, - 3.615838, - 5.21188 - ], - "rotation_deg": [ - 17.998432, - 55.263992, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1741, - "type": "imguizmo", - "translation": [ - 4.799582, - 3.584162, - 5.18812 - ], - "rotation_deg": [ - 17.998432, - 54.736008, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1742, - "type": "imguizmo", - "translation": [ - 4.796237, - 3.55253, - 5.164369 - ], - "rotation_deg": [ - 17.985888, - 54.208209, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1743, - "type": "imguizmo", - "translation": [ - 4.789549, - 3.52103, - 5.140643 - ], - "rotation_deg": [ - 17.96081, - 53.680961, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1744, - "type": "imguizmo", - "translation": [ - 4.779524, - 3.48975, - 5.116958 - ], - "rotation_deg": [ - 17.923215, - 53.154633, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1745, - "type": "imguizmo", - "translation": [ - 4.766168, - 3.458778, - 5.093332 - ], - "rotation_deg": [ - 17.873129, - 52.62959, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1746, - "type": "imguizmo", - "translation": [ - 4.74949, - 3.428199, - 5.069779 - ], - "rotation_deg": [ - 17.810587, - 52.1062, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1747, - "type": "imguizmo", - "translation": [ - 4.729502, - 3.398099, - 5.046317 - ], - "rotation_deg": [ - 17.735632, - 51.584827, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1748, - "type": "imguizmo", - "translation": [ - 4.706218, - 3.368562, - 5.022962 - ], - "rotation_deg": [ - 17.648317, - 51.065833, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1749, - "type": "imguizmo", - "translation": [ - 4.679654, - 3.33967, - 4.999731 - ], - "rotation_deg": [ - 17.548702, - 50.549581, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1750, - "type": "imguizmo", - "translation": [ - 4.649829, - 3.311503, - 4.976639 - ], - "rotation_deg": [ - 17.436858, - 50.036431, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1751, - "type": "imguizmo", - "translation": [ - 4.616763, - 3.284141, - 4.953703 - ], - "rotation_deg": [ - 17.312862, - 49.52674, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1752, - "type": "imguizmo", - "translation": [ - 4.58048, - 3.257659, - 4.930939 - ], - "rotation_deg": [ - 17.1768, - 49.020864, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1753, - "type": "imguizmo", - "translation": [ - 4.541004, - 3.232131, - 4.908362 - ], - "rotation_deg": [ - 17.028767, - 48.519154, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1754, - "type": "imguizmo", - "translation": [ - 4.498364, - 3.207628, - 4.885988 - ], - "rotation_deg": [ - 16.868866, - 48.021961, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1755, - "type": "imguizmo", - "translation": [ - 4.452589, - 3.184219, - 4.863833 - ], - "rotation_deg": [ - 16.69721, - 47.529631, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1756, - "type": "imguizmo", - "translation": [ - 4.403711, - 3.161969, - 4.841913 - ], - "rotation_deg": [ - 16.513916, - 47.042507, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1757, - "type": "imguizmo", - "translation": [ - 4.351764, - 3.140939, - 4.820242 - ], - "rotation_deg": [ - 16.319114, - 46.56093, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1758, - "type": "imguizmo", - "translation": [ - 4.296784, - 3.12119, - 4.798835 - ], - "rotation_deg": [ - 16.112939, - 46.085233, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1759, - "type": "imguizmo", - "translation": [ - 4.238809, - 3.102774, - 4.777709 - ], - "rotation_deg": [ - 15.895535, - 45.615749, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1760, - "type": "imguizmo", - "translation": [ - 4.177881, - 3.085745, - 4.756876 - ], - "rotation_deg": [ - 15.667052, - 45.152805, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1761, - "type": "imguizmo", - "translation": [ - 4.11404, - 3.070149, - 4.736353 - ], - "rotation_deg": [ - 15.427652, - 44.696724, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1762, - "type": "imguizmo", - "translation": [ - 4.047333, - 3.05603, - 4.716152 - ], - "rotation_deg": [ - 15.177499, - 44.247824, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1763, - "type": "imguizmo", - "translation": [ - 3.977805, - 3.043426, - 4.696289 - ], - "rotation_deg": [ - 14.916769, - 43.806417, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1764, - "type": "imguizmo", - "translation": [ - 3.905505, - 3.032374, - 4.676776 - ], - "rotation_deg": [ - 14.645643, - 43.37281, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1765, - "type": "imguizmo", - "translation": [ - 3.830483, - 3.022905, - 4.657629 - ], - "rotation_deg": [ - 14.36431, - 42.947307, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1766, - "type": "imguizmo", - "translation": [ - 3.752791, - 3.015043, - 4.638859 - ], - "rotation_deg": [ - 14.072967, - 42.530204, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1767, - "type": "imguizmo", - "translation": [ - 3.672484, - 3.008812, - 4.620481 - ], - "rotation_deg": [ - 13.771816, - 42.121791, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1768, - "type": "imguizmo", - "translation": [ - 3.589618, - 3.004229, - 4.602506 - ], - "rotation_deg": [ - 13.461067, - 41.722353, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1769, - "type": "imguizmo", - "translation": [ - 3.50425, - 3.001306, - 4.584948 - ], - "rotation_deg": [ - 13.140937, - 41.332169, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1770, - "type": "imguizmo", - "translation": [ - 3.41644, - 3.000052, - 4.567818 - ], - "rotation_deg": [ - 12.811648, - 40.95151, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1771, - "type": "imguizmo", - "translation": [ - 3.326248, - 3.00047, - 4.551129 - ], - "rotation_deg": [ - 12.473431, - 40.580641, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1772, - "type": "imguizmo", - "translation": [ - 3.233739, - 3.002559, - 4.534892 - ], - "rotation_deg": [ - 12.126522, - 40.219822, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1773, - "type": "imguizmo", - "translation": [ - 3.138976, - 3.006314, - 4.519119 - ], - "rotation_deg": [ - 11.771161, - 39.869303, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1774, - "type": "imguizmo", - "translation": [ - 3.042026, - 3.011723, - 4.50382 - ], - "rotation_deg": [ - 11.407596, - 39.529329, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1775, - "type": "imguizmo", - "translation": [ - 2.942955, - 3.018771, - 4.489006 - ], - "rotation_deg": [ - 11.036081, - 39.200136, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1776, - "type": "imguizmo", - "translation": [ - 2.841833, - 3.02744, - 4.474688 - ], - "rotation_deg": [ - 10.656876, - 38.881955, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1777, - "type": "imguizmo", - "translation": [ - 2.738731, - 3.037704, - 4.460875 - ], - "rotation_deg": [ - 10.270243, - 38.575007, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1778, - "type": "imguizmo", - "translation": [ - 2.633721, - 3.049536, - 4.447578 - ], - "rotation_deg": [ - 9.876452, - 38.279506, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1779, - "type": "imguizmo", - "translation": [ - 2.526874, - 3.062902, - 4.434805 - ], - "rotation_deg": [ - 9.475779, - 37.995657, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1780, - "type": "imguizmo", - "translation": [ - 2.418267, - 3.077765, - 4.422565 - ], - "rotation_deg": [ - 9.068502, - 37.723659, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1781, - "type": "imguizmo", - "translation": [ - 2.307975, - 3.094083, - 4.410867 - ], - "rotation_deg": [ - 8.654905, - 37.463702, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1782, - "type": "imguizmo", - "translation": [ - 2.196073, - 3.111812, - 4.399718 - ], - "rotation_deg": [ - 8.235276, - 37.215965, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1783, - "type": "imguizmo", - "translation": [ - 2.082642, - 3.130901, - 4.389128 - ], - "rotation_deg": [ - 7.809907, - 36.980623, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1784, - "type": "imguizmo", - "translation": [ - 1.967759, - 3.151298, - 4.379103 - ], - "rotation_deg": [ - 7.379096, - 36.757838, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1785, - "type": "imguizmo", - "translation": [ - 1.851505, - 3.172945, - 4.36965 - ], - "rotation_deg": [ - 6.943142, - 36.547767, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1786, - "type": "imguizmo", - "translation": [ - 1.73396, - 3.195783, - 4.360775 - ], - "rotation_deg": [ - 6.50235, - 36.350555, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1787, - "type": "imguizmo", - "translation": [ - 1.615207, - 3.219747, - 4.352485 - ], - "rotation_deg": [ - 6.057026, - 36.166341, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1788, - "type": "imguizmo", - "translation": [ - 1.495328, - 3.244771, - 4.344786 - ], - "rotation_deg": [ - 5.607481, - 35.995252, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1789, - "type": "imguizmo", - "translation": [ - 1.374407, - 3.270785, - 4.337683 - ], - "rotation_deg": [ - 5.154027, - 35.837408, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1790, - "type": "imguizmo", - "translation": [ - 1.252529, - 3.297717, - 4.331181 - ], - "rotation_deg": [ - 4.696982, - 35.692918, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1791, - "type": "imguizmo", - "translation": [ - 1.129777, - 3.325491, - 4.325285 - ], - "rotation_deg": [ - 4.236663, - 35.561884, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1792, - "type": "imguizmo", - "translation": [ - 1.006238, - 3.35403, - 4.319998 - ], - "rotation_deg": [ - 3.773392, - 35.444397, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1793, - "type": "imguizmo", - "translation": [ - 0.881998, - 3.383255, - 4.315324 - ], - "rotation_deg": [ - 3.307491, - 35.340538, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1794, - "type": "imguizmo", - "translation": [ - 0.757143, - 3.413084, - 4.311267 - ], - "rotation_deg": [ - 2.839285, - 35.25038, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1795, - "type": "imguizmo", - "translation": [ - 0.63176, - 3.443434, - 4.307829 - ], - "rotation_deg": [ - 2.369101, - 35.173986, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1796, - "type": "imguizmo", - "translation": [ - 0.505937, - 3.47422, - 4.305013 - ], - "rotation_deg": [ - 1.897265, - 35.111409, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1797, - "type": "imguizmo", - "translation": [ - 0.379762, - 3.505357, - 4.302821 - ], - "rotation_deg": [ - 1.424107, - 35.062693, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1798, - "type": "imguizmo", - "translation": [ - 0.253322, - 3.536758, - 4.301254 - ], - "rotation_deg": [ - 0.949956, - 35.027872, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1799, - "type": "imguizmo", - "translation": [ - 0.126705, - 3.568335, - 4.300314 - ], - "rotation_deg": [ - 0.475144, - 35.006969, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1800, - "type": "imguizmo", - "translation": [ - 0, - 3.6, - 4.3 - ], - "rotation_deg": [ - 0, - 35, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1801, - "type": "imguizmo", - "translation": [ - -0.126705, - 3.631665, - 4.300314 - ], - "rotation_deg": [ - -0.475144, - 35.006969, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1802, - "type": "imguizmo", - "translation": [ - -0.253322, - 3.663242, - 4.301254 - ], - "rotation_deg": [ - -0.949956, - 35.027872, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1803, - "type": "imguizmo", - "translation": [ - -0.379762, - 3.694643, - 4.302821 - ], - "rotation_deg": [ - -1.424107, - 35.062693, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1804, - "type": "imguizmo", - "translation": [ - -0.505937, - 3.72578, - 4.305013 - ], - "rotation_deg": [ - -1.897265, - 35.111409, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1805, - "type": "imguizmo", - "translation": [ - -0.63176, - 3.756566, - 4.307829 - ], - "rotation_deg": [ - -2.369101, - 35.173986, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1806, - "type": "imguizmo", - "translation": [ - -0.757143, - 3.786916, - 4.311267 - ], - "rotation_deg": [ - -2.839285, - 35.25038, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1807, - "type": "imguizmo", - "translation": [ - -0.881998, - 3.816745, - 4.315324 - ], - "rotation_deg": [ - -3.307491, - 35.340538, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1808, - "type": "imguizmo", - "translation": [ - -1.006238, - 3.84597, - 4.319998 - ], - "rotation_deg": [ - -3.773392, - 35.444397, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1809, - "type": "imguizmo", - "translation": [ - -1.129777, - 3.874509, - 4.325285 - ], - "rotation_deg": [ - -4.236663, - 35.561884, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1810, - "type": "imguizmo", - "translation": [ - -1.252529, - 3.902283, - 4.331181 - ], - "rotation_deg": [ - -4.696982, - 35.692918, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1811, - "type": "imguizmo", - "translation": [ - -1.374407, - 3.929215, - 4.337683 - ], - "rotation_deg": [ - -5.154027, - 35.837408, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1812, - "type": "imguizmo", - "translation": [ - -1.495328, - 3.955229, - 4.344786 - ], - "rotation_deg": [ - -5.607481, - 35.995252, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1813, - "type": "imguizmo", - "translation": [ - -1.615207, - 3.980253, - 4.352485 - ], - "rotation_deg": [ - -6.057026, - 36.166341, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1814, - "type": "imguizmo", - "translation": [ - -1.73396, - 4.004217, - 4.360775 - ], - "rotation_deg": [ - -6.50235, - 36.350555, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1815, - "type": "imguizmo", - "translation": [ - -1.851505, - 4.027055, - 4.36965 - ], - "rotation_deg": [ - -6.943142, - 36.547767, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1816, - "type": "imguizmo", - "translation": [ - -1.967759, - 4.048702, - 4.379103 - ], - "rotation_deg": [ - -7.379096, - 36.757838, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1817, - "type": "imguizmo", - "translation": [ - -2.082642, - 4.069099, - 4.389128 - ], - "rotation_deg": [ - -7.809907, - 36.980623, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1818, - "type": "imguizmo", - "translation": [ - -2.196073, - 4.088188, - 4.399718 - ], - "rotation_deg": [ - -8.235276, - 37.215965, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1819, - "type": "imguizmo", - "translation": [ - -2.307975, - 4.105917, - 4.410867 - ], - "rotation_deg": [ - -8.654905, - 37.463702, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1820, - "type": "imguizmo", - "translation": [ - -2.418267, - 4.122235, - 4.422565 - ], - "rotation_deg": [ - -9.068502, - 37.723659, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1821, - "type": "imguizmo", - "translation": [ - -2.526874, - 4.137098, - 4.434805 - ], - "rotation_deg": [ - -9.475779, - 37.995657, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1822, - "type": "imguizmo", - "translation": [ - -2.633721, - 4.150464, - 4.447578 - ], - "rotation_deg": [ - -9.876452, - 38.279506, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1823, - "type": "imguizmo", - "translation": [ - -2.738731, - 4.162296, - 4.460875 - ], - "rotation_deg": [ - -10.270243, - 38.575007, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1824, - "type": "imguizmo", - "translation": [ - -2.841833, - 4.17256, - 4.474688 - ], - "rotation_deg": [ - -10.656876, - 38.881955, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1825, - "type": "imguizmo", - "translation": [ - -2.942955, - 4.181229, - 4.489006 - ], - "rotation_deg": [ - -11.036081, - 39.200136, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1826, - "type": "imguizmo", - "translation": [ - -3.042026, - 4.188277, - 4.50382 - ], - "rotation_deg": [ - -11.407596, - 39.529329, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1827, - "type": "imguizmo", - "translation": [ - -3.138976, - 4.193686, - 4.519119 - ], - "rotation_deg": [ - -11.771161, - 39.869303, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1828, - "type": "imguizmo", - "translation": [ - -3.233739, - 4.197441, - 4.534892 - ], - "rotation_deg": [ - -12.126522, - 40.219822, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1829, - "type": "imguizmo", - "translation": [ - -3.326248, - 4.19953, - 4.551129 - ], - "rotation_deg": [ - -12.473431, - 40.580641, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1830, - "type": "imguizmo", - "translation": [ - -3.41644, - 4.199948, - 4.567818 - ], - "rotation_deg": [ - -12.811648, - 40.95151, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1831, - "type": "imguizmo", - "translation": [ - -3.50425, - 4.198694, - 4.584948 - ], - "rotation_deg": [ - -13.140937, - 41.332169, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1832, - "type": "imguizmo", - "translation": [ - -3.589618, - 4.195771, - 4.602506 - ], - "rotation_deg": [ - -13.461067, - 41.722353, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1833, - "type": "imguizmo", - "translation": [ - -3.672484, - 4.191188, - 4.620481 - ], - "rotation_deg": [ - -13.771816, - 42.121791, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1834, - "type": "imguizmo", - "translation": [ - -3.752791, - 4.184957, - 4.638859 - ], - "rotation_deg": [ - -14.072967, - 42.530204, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1835, - "type": "imguizmo", - "translation": [ - -3.830483, - 4.177095, - 4.657629 - ], - "rotation_deg": [ - -14.36431, - 42.947307, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1836, - "type": "imguizmo", - "translation": [ - -3.905505, - 4.167626, - 4.676776 - ], - "rotation_deg": [ - -14.645643, - 43.37281, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1837, - "type": "imguizmo", - "translation": [ - -3.977805, - 4.156574, - 4.696289 - ], - "rotation_deg": [ - -14.916769, - 43.806417, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1838, - "type": "imguizmo", - "translation": [ - -4.047333, - 4.14397, - 4.716152 - ], - "rotation_deg": [ - -15.177499, - 44.247824, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1839, - "type": "imguizmo", - "translation": [ - -4.11404, - 4.129851, - 4.736353 - ], - "rotation_deg": [ - -15.427652, - 44.696724, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1840, - "type": "imguizmo", - "translation": [ - -4.177881, - 4.114255, - 4.756876 - ], - "rotation_deg": [ - -15.667052, - 45.152805, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1841, - "type": "imguizmo", - "translation": [ - -4.238809, - 4.097226, - 4.777709 - ], - "rotation_deg": [ - -15.895535, - 45.615749, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1842, - "type": "imguizmo", - "translation": [ - -4.296784, - 4.07881, - 4.798835 - ], - "rotation_deg": [ - -16.112939, - 46.085233, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1843, - "type": "imguizmo", - "translation": [ - -4.351764, - 4.059061, - 4.820242 - ], - "rotation_deg": [ - -16.319114, - 46.56093, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1844, - "type": "imguizmo", - "translation": [ - -4.403711, - 4.038031, - 4.841913 - ], - "rotation_deg": [ - -16.513916, - 47.042507, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1845, - "type": "imguizmo", - "translation": [ - -4.452589, - 4.015781, - 4.863833 - ], - "rotation_deg": [ - -16.69721, - 47.529631, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1846, - "type": "imguizmo", - "translation": [ - -4.498364, - 3.992372, - 4.885988 - ], - "rotation_deg": [ - -16.868866, - 48.021961, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1847, - "type": "imguizmo", - "translation": [ - -4.541004, - 3.967869, - 4.908362 - ], - "rotation_deg": [ - -17.028767, - 48.519154, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1848, - "type": "imguizmo", - "translation": [ - -4.58048, - 3.942341, - 4.930939 - ], - "rotation_deg": [ - -17.1768, - 49.020864, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1849, - "type": "imguizmo", - "translation": [ - -4.616763, - 3.915859, - 4.953703 - ], - "rotation_deg": [ - -17.312862, - 49.52674, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1850, - "type": "imguizmo", - "translation": [ - -4.649829, - 3.888497, - 4.976639 - ], - "rotation_deg": [ - -17.436858, - 50.036431, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1851, - "type": "imguizmo", - "translation": [ - -4.679654, - 3.86033, - 4.999731 - ], - "rotation_deg": [ - -17.548702, - 50.549581, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1852, - "type": "imguizmo", - "translation": [ - -4.706218, - 3.831438, - 5.022962 - ], - "rotation_deg": [ - -17.648317, - 51.065833, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1853, - "type": "imguizmo", - "translation": [ - -4.729502, - 3.801901, - 5.046317 - ], - "rotation_deg": [ - -17.735632, - 51.584827, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1854, - "type": "imguizmo", - "translation": [ - -4.74949, - 3.771801, - 5.069779 - ], - "rotation_deg": [ - -17.810587, - 52.1062, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1855, - "type": "imguizmo", - "translation": [ - -4.766168, - 3.741222, - 5.093332 - ], - "rotation_deg": [ - -17.873129, - 52.62959, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1856, - "type": "imguizmo", - "translation": [ - -4.779524, - 3.71025, - 5.116958 - ], - "rotation_deg": [ - -17.923215, - 53.154633, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1857, - "type": "imguizmo", - "translation": [ - -4.789549, - 3.67897, - 5.140643 - ], - "rotation_deg": [ - -17.96081, - 53.680961, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1858, - "type": "imguizmo", - "translation": [ - -4.796237, - 3.64747, - 5.164369 - ], - "rotation_deg": [ - -17.985888, - 54.208209, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1859, - "type": "imguizmo", - "translation": [ - -4.799582, - 3.615838, - 5.18812 - ], - "rotation_deg": [ - -17.998432, - 54.736008, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1860, - "type": "imguizmo", - "translation": [ - -4.799582, - 3.584162, - 5.21188 - ], - "rotation_deg": [ - -17.998432, - 55.263992, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1861, - "type": "imguizmo", - "translation": [ - -4.796237, - 3.55253, - 5.235631 - ], - "rotation_deg": [ - -17.985888, - 55.791791, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1862, - "type": "imguizmo", - "translation": [ - -4.789549, - 3.52103, - 5.259357 - ], - "rotation_deg": [ - -17.96081, - 56.319039, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1863, - "type": "imguizmo", - "translation": [ - -4.779524, - 3.48975, - 5.283042 - ], - "rotation_deg": [ - -17.923215, - 56.845367, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1864, - "type": "imguizmo", - "translation": [ - -4.766168, - 3.458778, - 5.306668 - ], - "rotation_deg": [ - -17.873129, - 57.37041, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1865, - "type": "imguizmo", - "translation": [ - -4.74949, - 3.428199, - 5.330221 - ], - "rotation_deg": [ - -17.810587, - 57.8938, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1866, - "type": "imguizmo", - "translation": [ - -4.729502, - 3.398099, - 5.353683 - ], - "rotation_deg": [ - -17.735632, - 58.415173, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1867, - "type": "imguizmo", - "translation": [ - -4.706218, - 3.368562, - 5.377038 - ], - "rotation_deg": [ - -17.648317, - 58.934167, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1868, - "type": "imguizmo", - "translation": [ - -4.679654, - 3.33967, - 5.400269 - ], - "rotation_deg": [ - -17.548702, - 59.450419, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1869, - "type": "imguizmo", - "translation": [ - -4.649829, - 3.311503, - 5.423361 - ], - "rotation_deg": [ - -17.436858, - 59.963569, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1870, - "type": "imguizmo", - "translation": [ - -4.616763, - 3.284141, - 5.446297 - ], - "rotation_deg": [ - -17.312862, - 60.47326, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1871, - "type": "imguizmo", - "translation": [ - -4.58048, - 3.257659, - 5.469061 - ], - "rotation_deg": [ - -17.1768, - 60.979136, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1872, - "type": "imguizmo", - "translation": [ - -4.541004, - 3.232131, - 5.491638 - ], - "rotation_deg": [ - -17.028767, - 61.480846, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1873, - "type": "imguizmo", - "translation": [ - -4.498364, - 3.207628, - 5.514012 - ], - "rotation_deg": [ - -16.868866, - 61.978039, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1874, - "type": "imguizmo", - "translation": [ - -4.452589, - 3.184219, - 5.536167 - ], - "rotation_deg": [ - -16.69721, - 62.470369, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1875, - "type": "imguizmo", - "translation": [ - -4.403711, - 3.161969, - 5.558087 - ], - "rotation_deg": [ - -16.513916, - 62.957493, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1876, - "type": "imguizmo", - "translation": [ - -4.351764, - 3.140939, - 5.579758 - ], - "rotation_deg": [ - -16.319114, - 63.43907, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1877, - "type": "imguizmo", - "translation": [ - -4.296784, - 3.12119, - 5.601165 - ], - "rotation_deg": [ - -16.112939, - 63.914767, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1878, - "type": "imguizmo", - "translation": [ - -4.238809, - 3.102774, - 5.622291 - ], - "rotation_deg": [ - -15.895535, - 64.384251, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1879, - "type": "imguizmo", - "translation": [ - -4.177881, - 3.085745, - 5.643124 - ], - "rotation_deg": [ - -15.667052, - 64.847195, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1880, - "type": "imguizmo", - "translation": [ - -4.11404, - 3.070149, - 5.663647 - ], - "rotation_deg": [ - -15.427652, - 65.303276, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1881, - "type": "imguizmo", - "translation": [ - -4.047333, - 3.05603, - 5.683848 - ], - "rotation_deg": [ - -15.177499, - 65.752176, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1882, - "type": "imguizmo", - "translation": [ - -3.977805, - 3.043426, - 5.703711 - ], - "rotation_deg": [ - -14.916769, - 66.193583, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1883, - "type": "imguizmo", - "translation": [ - -3.905505, - 3.032374, - 5.723224 - ], - "rotation_deg": [ - -14.645643, - 66.62719, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1884, - "type": "imguizmo", - "translation": [ - -3.830483, - 3.022905, - 5.742371 - ], - "rotation_deg": [ - -14.36431, - 67.052693, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1885, - "type": "imguizmo", - "translation": [ - -3.752791, - 3.015043, - 5.761141 - ], - "rotation_deg": [ - -14.072967, - 67.469796, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1886, - "type": "imguizmo", - "translation": [ - -3.672484, - 3.008812, - 5.779519 - ], - "rotation_deg": [ - -13.771816, - 67.878209, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1887, - "type": "imguizmo", - "translation": [ - -3.589618, - 3.004229, - 5.797494 - ], - "rotation_deg": [ - -13.461067, - 68.277647, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1888, - "type": "imguizmo", - "translation": [ - -3.50425, - 3.001306, - 5.815052 - ], - "rotation_deg": [ - -13.140937, - 68.667831, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1889, - "type": "imguizmo", - "translation": [ - -3.41644, - 3.000052, - 5.832182 - ], - "rotation_deg": [ - -12.811648, - 69.04849, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1890, - "type": "imguizmo", - "translation": [ - -3.326248, - 3.00047, - 5.848871 - ], - "rotation_deg": [ - -12.473431, - 69.419359, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1891, - "type": "imguizmo", - "translation": [ - -3.233739, - 3.002559, - 5.865108 - ], - "rotation_deg": [ - -12.126522, - 69.780178, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1892, - "type": "imguizmo", - "translation": [ - -3.138976, - 3.006314, - 5.880881 - ], - "rotation_deg": [ - -11.771161, - 70.130697, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1893, - "type": "imguizmo", - "translation": [ - -3.042026, - 3.011723, - 5.89618 - ], - "rotation_deg": [ - -11.407596, - 70.470671, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1894, - "type": "imguizmo", - "translation": [ - -2.942955, - 3.018771, - 5.910994 - ], - "rotation_deg": [ - -11.036081, - 70.799864, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1895, - "type": "imguizmo", - "translation": [ - -2.841833, - 3.02744, - 5.925312 - ], - "rotation_deg": [ - -10.656876, - 71.118045, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1896, - "type": "imguizmo", - "translation": [ - -2.738731, - 3.037704, - 5.939125 - ], - "rotation_deg": [ - -10.270243, - 71.424993, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1897, - "type": "imguizmo", - "translation": [ - -2.633721, - 3.049536, - 5.952422 - ], - "rotation_deg": [ - -9.876452, - 71.720494, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1898, - "type": "imguizmo", - "translation": [ - -2.526874, - 3.062902, - 5.965195 - ], - "rotation_deg": [ - -9.475779, - 72.004343, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1899, - "type": "imguizmo", - "translation": [ - -2.418267, - 3.077765, - 5.977435 - ], - "rotation_deg": [ - -9.068502, - 72.276341, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1900, - "type": "imguizmo", - "translation": [ - -2.307975, - 3.094083, - 5.989133 - ], - "rotation_deg": [ - -8.654905, - 72.536298, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1901, - "type": "imguizmo", - "translation": [ - -2.196073, - 3.111812, - 6.000282 - ], - "rotation_deg": [ - -8.235276, - 72.784035, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1902, - "type": "imguizmo", - "translation": [ - -2.082642, - 3.130901, - 6.010872 - ], - "rotation_deg": [ - -7.809907, - 73.019377, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1903, - "type": "imguizmo", - "translation": [ - -1.967759, - 3.151298, - 6.020897 - ], - "rotation_deg": [ - -7.379096, - 73.242162, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1904, - "type": "imguizmo", - "translation": [ - -1.851505, - 3.172945, - 6.03035 - ], - "rotation_deg": [ - -6.943142, - 73.452233, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1905, - "type": "imguizmo", - "translation": [ - -1.73396, - 3.195783, - 6.039225 - ], - "rotation_deg": [ - -6.50235, - 73.649445, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1906, - "type": "imguizmo", - "translation": [ - -1.615207, - 3.219747, - 6.047515 - ], - "rotation_deg": [ - -6.057026, - 73.833659, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1907, - "type": "imguizmo", - "translation": [ - -1.495328, - 3.244771, - 6.055214 - ], - "rotation_deg": [ - -5.607481, - 74.004748, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1908, - "type": "imguizmo", - "translation": [ - -1.374407, - 3.270785, - 6.062317 - ], - "rotation_deg": [ - -5.154027, - 74.162592, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1909, - "type": "imguizmo", - "translation": [ - -1.252529, - 3.297717, - 6.068819 - ], - "rotation_deg": [ - -4.696982, - 74.307082, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1910, - "type": "imguizmo", - "translation": [ - -1.129777, - 3.325491, - 6.074715 - ], - "rotation_deg": [ - -4.236663, - 74.438116, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1911, - "type": "imguizmo", - "translation": [ - -1.006238, - 3.35403, - 6.080002 - ], - "rotation_deg": [ - -3.773392, - 74.555603, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1912, - "type": "imguizmo", - "translation": [ - -0.881998, - 3.383255, - 6.084676 - ], - "rotation_deg": [ - -3.307491, - 74.659462, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1913, - "type": "imguizmo", - "translation": [ - -0.757143, - 3.413084, - 6.088733 - ], - "rotation_deg": [ - -2.839285, - 74.74962, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1914, - "type": "imguizmo", - "translation": [ - -0.63176, - 3.443434, - 6.092171 - ], - "rotation_deg": [ - -2.369101, - 74.826014, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1915, - "type": "imguizmo", - "translation": [ - -0.505937, - 3.47422, - 6.094987 - ], - "rotation_deg": [ - -1.897265, - 74.888591, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1916, - "type": "imguizmo", - "translation": [ - -0.379762, - 3.505357, - 6.097179 - ], - "rotation_deg": [ - -1.424107, - 74.937307, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1917, - "type": "imguizmo", - "translation": [ - -0.253322, - 3.536758, - 6.098746 - ], - "rotation_deg": [ - -0.949956, - 74.972128, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1918, - "type": "imguizmo", - "translation": [ - -0.126705, - 3.568335, - 6.099686 - ], - "rotation_deg": [ - -0.475144, - 74.993031, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1919, - "type": "imguizmo", - "translation": [ - 0, - 3.6, - 6.1 - ], - "rotation_deg": [ - 0, - 75, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1920, - "type": "action", - "action": "set_active_render_window", - "value": 0 - }, - { - "frame": 1920, - "type": "action", - "action": "set_active_planar", - "value": 8 - }, - { - "frame": 1920, - "type": "action", - "action": "set_projection_type", - "value": "perspective" - }, - { - "frame": 1920, - "type": "action", - "action": "set_left_handed", - "value": true - }, - { - "frame": 1920, - "type": "action", - "action": "reset_active_camera", - "value": 1 - }, - { - "frame": 1920, - "type": "action", - "action": "set_active_render_window", - "value": 1 - }, - { - "frame": 1920, - "type": "action", - "action": "set_active_planar", - "value": 8 - }, - { - "frame": 1920, - "type": "action", - "action": "set_projection_type", - "value": "orthographic" - }, - { - "frame": 1920, - "type": "action", - "action": "set_left_handed", - "value": true - }, - { - "frame": 1920, - "type": "action", - "action": "set_active_render_window", - "value": 0 - }, - { - "frame": 1921, - "type": "imguizmo", - "translation": [ - 0, - 5.2, - 4.6 - ], - "rotation_deg": [ - 0, - 64, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1922, - "type": "imguizmo", - "translation": [ - 0.131984, - 5.198188, - 4.64222 - ], - "rotation_deg": [ - 0.42235, - 63.995122, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1923, - "type": "imguizmo", - "translation": [ - 0.263877, - 5.192753, - 4.684323 - ], - "rotation_deg": [ - 0.844406, - 63.98049, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1924, - "type": "imguizmo", - "translation": [ - 0.395585, - 5.1837, - 4.72619 - ], - "rotation_deg": [ - 1.265873, - 63.956115, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1925, - "type": "imguizmo", - "translation": [ - 0.527018, - 5.171034, - 4.767706 - ], - "rotation_deg": [ - 1.686458, - 63.922013, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1926, - "type": "imguizmo", - "translation": [ - 0.658083, - 5.154764, - 4.808755 - ], - "rotation_deg": [ - 2.105867, - 63.87821, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1927, - "type": "imguizmo", - "translation": [ - 0.78869, - 5.134901, - 4.849221 - ], - "rotation_deg": [ - 2.523809, - 63.824734, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1928, - "type": "imguizmo", - "translation": [ - 0.918748, - 5.11146, - 4.888993 - ], - "rotation_deg": [ - 2.939992, - 63.761623, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1929, - "type": "imguizmo", - "translation": [ - 1.048165, - 5.084457, - 4.92796 - ], - "rotation_deg": [ - 3.354126, - 63.688922, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1930, - "type": "imguizmo", - "translation": [ - 1.176851, - 5.05391, - 4.966012 - ], - "rotation_deg": [ - 3.765923, - 63.606681, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1931, - "type": "imguizmo", - "translation": [ - 1.304717, - 5.019841, - 5.003045 - ], - "rotation_deg": [ - 4.175095, - 63.514957, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1932, - "type": "imguizmo", - "translation": [ - 1.431674, - 4.982274, - 5.038953 - ], - "rotation_deg": [ - 4.581358, - 63.413815, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1933, - "type": "imguizmo", - "translation": [ - 1.557633, - 4.941234, - 5.073639 - ], - "rotation_deg": [ - 4.984427, - 63.303324, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1934, - "type": "imguizmo", - "translation": [ - 1.682507, - 4.896751, - 5.107004 - ], - "rotation_deg": [ - 5.384023, - 63.183561, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1935, - "type": "imguizmo", - "translation": [ - 1.806208, - 4.848856, - 5.138957 - ], - "rotation_deg": [ - 5.779867, - 63.054611, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1936, - "type": "imguizmo", - "translation": [ - 1.928651, - 4.797581, - 5.169407 - ], - "rotation_deg": [ - 6.171682, - 62.916563, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1937, - "type": "imguizmo", - "translation": [ - 2.049749, - 4.742962, - 5.19827 - ], - "rotation_deg": [ - 6.559197, - 62.769513, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1938, - "type": "imguizmo", - "translation": [ - 2.169419, - 4.685038, - 5.225465 - ], - "rotation_deg": [ - 6.94214, - 62.613564, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1939, - "type": "imguizmo", - "translation": [ - 2.287577, - 4.623849, - 5.250917 - ], - "rotation_deg": [ - 7.320245, - 62.448824, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1940, - "type": "imguizmo", - "translation": [ - 2.40414, - 4.559438, - 5.274556 - ], - "rotation_deg": [ - 7.693248, - 62.275409, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1941, - "type": "imguizmo", - "translation": [ - 2.519028, - 4.491849, - 5.296313 - ], - "rotation_deg": [ - 8.06089, - 62.093438, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1942, - "type": "imguizmo", - "translation": [ - 2.632161, - 4.421129, - 5.316131 - ], - "rotation_deg": [ - 8.422915, - 61.90304, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1943, - "type": "imguizmo", - "translation": [ - 2.743459, - 4.347328, - 5.333952 - ], - "rotation_deg": [ - 8.779069, - 61.704346, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1944, - "type": "imguizmo", - "translation": [ - 2.852845, - 4.270498, - 5.349727 - ], - "rotation_deg": [ - 9.129105, - 61.497495, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1945, - "type": "imguizmo", - "translation": [ - 2.960243, - 4.190692, - 5.363413 - ], - "rotation_deg": [ - 9.472778, - 61.282631, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1946, - "type": "imguizmo", - "translation": [ - 3.065578, - 4.107965, - 5.374971 - ], - "rotation_deg": [ - 9.80985, - 61.059904, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1947, - "type": "imguizmo", - "translation": [ - 3.168777, - 4.022375, - 5.38437 - ], - "rotation_deg": [ - 10.140085, - 60.82947, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1948, - "type": "imguizmo", - "translation": [ - 3.269767, - 3.933981, - 5.391582 - ], - "rotation_deg": [ - 10.463254, - 60.591488, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1949, - "type": "imguizmo", - "translation": [ - 3.368478, - 3.842846, - 5.396587 - ], - "rotation_deg": [ - 10.77913, - 60.346125, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1950, - "type": "imguizmo", - "translation": [ - 3.464842, - 3.749033, - 5.399373 - ], - "rotation_deg": [ - 11.087495, - 60.093551, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1951, - "type": "imguizmo", - "translation": [ - 3.558791, - 3.652608, - 5.39993 - ], - "rotation_deg": [ - 11.388132, - 59.833943, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1952, - "type": "imguizmo", - "translation": [ - 3.65026, - 3.553636, - 5.398258 - ], - "rotation_deg": [ - 11.680833, - 59.567482, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1953, - "type": "imguizmo", - "translation": [ - 3.739185, - 3.452188, - 5.394361 - ], - "rotation_deg": [ - 11.965393, - 59.294353, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1954, - "type": "imguizmo", - "translation": [ - 3.825504, - 3.348334, - 5.38825 - ], - "rotation_deg": [ - 12.241614, - 59.014746, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1955, - "type": "imguizmo", - "translation": [ - 3.909157, - 3.242147, - 5.379942 - ], - "rotation_deg": [ - 12.509304, - 58.728857, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1956, - "type": "imguizmo", - "translation": [ - 3.990086, - 3.1337, - 5.369461 - ], - "rotation_deg": [ - 12.768276, - 58.436885, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1957, - "type": "imguizmo", - "translation": [ - 4.068234, - 3.023069, - 5.356834 - ], - "rotation_deg": [ - 13.018349, - 58.139033, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1958, - "type": "imguizmo", - "translation": [ - 4.143547, - 2.910332, - 5.342098 - ], - "rotation_deg": [ - 13.25935, - 57.835508, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1959, - "type": "imguizmo", - "translation": [ - 4.215972, - 2.795566, - 5.325294 - ], - "rotation_deg": [ - 13.49111, - 57.526523, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1960, - "type": "imguizmo", - "translation": [ - 4.285459, - 2.678852, - 5.306468 - ], - "rotation_deg": [ - 13.713468, - 57.212293, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1961, - "type": "imguizmo", - "translation": [ - 4.351959, - 2.560271, - 5.285673 - ], - "rotation_deg": [ - 13.926269, - 56.893036, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1962, - "type": "imguizmo", - "translation": [ - 4.415426, - 2.439905, - 5.262967 - ], - "rotation_deg": [ - 14.129364, - 56.568976, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1963, - "type": "imguizmo", - "translation": [ - 4.475816, - 2.317839, - 5.238414 - ], - "rotation_deg": [ - 14.322613, - 56.240337, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1964, - "type": "imguizmo", - "translation": [ - 4.533087, - 2.194158, - 5.212081 - ], - "rotation_deg": [ - 14.505879, - 55.907349, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1965, - "type": "imguizmo", - "translation": [ - 4.587199, - 2.068948, - 5.184042 - ], - "rotation_deg": [ - 14.679037, - 55.570245, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1966, - "type": "imguizmo", - "translation": [ - 4.638114, - 1.942296, - 5.154375 - ], - "rotation_deg": [ - 14.841964, - 55.229258, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1967, - "type": "imguizmo", - "translation": [ - 4.685796, - 1.81429, - 5.123163 - ], - "rotation_deg": [ - 14.994548, - 54.884627, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1968, - "type": "imguizmo", - "translation": [ - 4.730213, - 1.68502, - 5.090493 - ], - "rotation_deg": [ - 15.136681, - 54.536592, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1969, - "type": "imguizmo", - "translation": [ - 4.771333, - 1.554575, - 5.056455 - ], - "rotation_deg": [ - 15.268266, - 54.185395, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1970, - "type": "imguizmo", - "translation": [ - 4.809128, - 1.423048, - 5.021146 - ], - "rotation_deg": [ - 15.38921, - 53.831282, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1971, - "type": "imguizmo", - "translation": [ - 4.843572, - 1.290528, - 4.984662 - ], - "rotation_deg": [ - 15.499429, - 53.474498, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1972, - "type": "imguizmo", - "translation": [ - 4.87464, - 1.157109, - 4.947107 - ], - "rotation_deg": [ - 15.598847, - 53.115293, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1973, - "type": "imguizmo", - "translation": [ - 4.90231, - 1.022883, - 4.908584 - ], - "rotation_deg": [ - 15.687393, - 52.753917, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1974, - "type": "imguizmo", - "translation": [ - 4.926564, - 0.887945, - 4.869201 - ], - "rotation_deg": [ - 15.765006, - 52.390621, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1975, - "type": "imguizmo", - "translation": [ - 4.947385, - 0.752388, - 4.829068 - ], - "rotation_deg": [ - 15.831633, - 52.02566, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1976, - "type": "imguizmo", - "translation": [ - 4.964758, - 0.616306, - 4.788296 - ], - "rotation_deg": [ - 15.887226, - 51.659287, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1977, - "type": "imguizmo", - "translation": [ - 4.978671, - 0.479795, - 4.747 - ], - "rotation_deg": [ - 15.931747, - 51.291757, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1978, - "type": "imguizmo", - "translation": [ - 4.989114, - 0.34295, - 4.705293 - ], - "rotation_deg": [ - 15.965165, - 50.923327, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1979, - "type": "imguizmo", - "translation": [ - 4.99608, - 0.205866, - 4.663294 - ], - "rotation_deg": [ - 15.987456, - 50.554254, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1980, - "type": "imguizmo", - "translation": [ - 4.999564, - 0.068638, - 4.621117 - ], - "rotation_deg": [ - 15.998606, - 50.184794, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1981, - "type": "imguizmo", - "translation": [ - 4.999564, - -0.068638, - 4.578883 - ], - "rotation_deg": [ - 15.998606, - 49.815206, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1982, - "type": "imguizmo", - "translation": [ - 4.99608, - -0.205866, - 4.536706 - ], - "rotation_deg": [ - 15.987456, - 49.445746, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1983, - "type": "imguizmo", - "translation": [ - 4.989114, - -0.34295, - 4.494707 - ], - "rotation_deg": [ - 15.965165, - 49.076673, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1984, - "type": "imguizmo", - "translation": [ - 4.978671, - -0.479795, - 4.453 - ], - "rotation_deg": [ - 15.931747, - 48.708243, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1985, - "type": "imguizmo", - "translation": [ - 4.964758, - -0.616306, - 4.411704 - ], - "rotation_deg": [ - 15.887226, - 48.340713, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1986, - "type": "imguizmo", - "translation": [ - 4.947385, - -0.752388, - 4.370932 - ], - "rotation_deg": [ - 15.831633, - 47.97434, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1987, - "type": "imguizmo", - "translation": [ - 4.926564, - -0.887945, - 4.330799 - ], - "rotation_deg": [ - 15.765006, - 47.609379, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1988, - "type": "imguizmo", - "translation": [ - 4.90231, - -1.022883, - 4.291416 - ], - "rotation_deg": [ - 15.687393, - 47.246083, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1989, - "type": "imguizmo", - "translation": [ - 4.87464, - -1.157109, - 4.252893 - ], - "rotation_deg": [ - 15.598847, - 46.884707, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1990, - "type": "imguizmo", - "translation": [ - 4.843572, - -1.290528, - 4.215338 - ], - "rotation_deg": [ - 15.499429, - 46.525502, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1991, - "type": "imguizmo", - "translation": [ - 4.809128, - -1.423048, - 4.178854 - ], - "rotation_deg": [ - 15.38921, - 46.168718, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1992, - "type": "imguizmo", - "translation": [ - 4.771333, - -1.554575, - 4.143545 - ], - "rotation_deg": [ - 15.268266, - 45.814605, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1993, - "type": "imguizmo", - "translation": [ - 4.730213, - -1.68502, - 4.109507 - ], - "rotation_deg": [ - 15.136681, - 45.463408, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1994, - "type": "imguizmo", - "translation": [ - 4.685796, - -1.81429, - 4.076837 - ], - "rotation_deg": [ - 14.994548, - 45.115373, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1995, - "type": "imguizmo", - "translation": [ - 4.638114, - -1.942296, - 4.045625 - ], - "rotation_deg": [ - 14.841964, - 44.770742, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1996, - "type": "imguizmo", - "translation": [ - 4.587199, - -2.068948, - 4.015958 - ], - "rotation_deg": [ - 14.679037, - 44.429755, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1997, - "type": "imguizmo", - "translation": [ - 4.533087, - -2.194158, - 3.987919 - ], - "rotation_deg": [ - 14.505879, - 44.092651, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1998, - "type": "imguizmo", - "translation": [ - 4.475816, - -2.317839, - 3.961586 - ], - "rotation_deg": [ - 14.322613, - 43.759663, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 1999, - "type": "imguizmo", - "translation": [ - 4.415426, - -2.439905, - 3.937033 - ], - "rotation_deg": [ - 14.129364, - 43.431024, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2000, - "type": "imguizmo", - "translation": [ - 4.351959, - -2.560271, - 3.914327 - ], - "rotation_deg": [ - 13.926269, - 43.106964, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2001, - "type": "imguizmo", - "translation": [ - 4.285459, - -2.678852, - 3.893532 - ], - "rotation_deg": [ - 13.713468, - 42.787707, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2002, - "type": "imguizmo", - "translation": [ - 4.215972, - -2.795566, - 3.874706 - ], - "rotation_deg": [ - 13.49111, - 42.473477, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2003, - "type": "imguizmo", - "translation": [ - 4.143547, - -2.910332, - 3.857902 - ], - "rotation_deg": [ - 13.25935, - 42.164492, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2004, - "type": "imguizmo", - "translation": [ - 4.068234, - -3.023069, - 3.843166 - ], - "rotation_deg": [ - 13.018349, - 41.860967, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2005, - "type": "imguizmo", - "translation": [ - 3.990086, - -3.1337, - 3.830539 - ], - "rotation_deg": [ - 12.768276, - 41.563115, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2006, - "type": "imguizmo", - "translation": [ - 3.909157, - -3.242147, - 3.820058 - ], - "rotation_deg": [ - 12.509304, - 41.271143, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2007, - "type": "imguizmo", - "translation": [ - 3.825504, - -3.348334, - 3.81175 - ], - "rotation_deg": [ - 12.241614, - 40.985254, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2008, - "type": "imguizmo", - "translation": [ - 3.739185, - -3.452188, - 3.805639 - ], - "rotation_deg": [ - 11.965393, - 40.705647, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2009, - "type": "imguizmo", - "translation": [ - 3.65026, - -3.553636, - 3.801742 - ], - "rotation_deg": [ - 11.680833, - 40.432518, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2010, - "type": "imguizmo", - "translation": [ - 3.558791, - -3.652608, - 3.80007 - ], - "rotation_deg": [ - 11.388132, - 40.166057, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2011, - "type": "imguizmo", - "translation": [ - 3.464842, - -3.749033, - 3.800627 - ], - "rotation_deg": [ - 11.087495, - 39.906449, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2012, - "type": "imguizmo", - "translation": [ - 3.368478, - -3.842846, - 3.803413 - ], - "rotation_deg": [ - 10.77913, - 39.653875, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2013, - "type": "imguizmo", - "translation": [ - 3.269767, - -3.933981, - 3.808418 - ], - "rotation_deg": [ - 10.463254, - 39.408512, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2014, - "type": "imguizmo", - "translation": [ - 3.168777, - -4.022375, - 3.81563 - ], - "rotation_deg": [ - 10.140085, - 39.17053, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2015, - "type": "imguizmo", - "translation": [ - 3.065578, - -4.107965, - 3.825029 - ], - "rotation_deg": [ - 9.80985, - 38.940096, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2016, - "type": "imguizmo", - "translation": [ - 2.960243, - -4.190692, - 3.836587 - ], - "rotation_deg": [ - 9.472778, - 38.717369, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2017, - "type": "imguizmo", - "translation": [ - 2.852845, - -4.270498, - 3.850273 - ], - "rotation_deg": [ - 9.129105, - 38.502505, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2018, - "type": "imguizmo", - "translation": [ - 2.743459, - -4.347328, - 3.866048 - ], - "rotation_deg": [ - 8.779069, - 38.295654, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2019, - "type": "imguizmo", - "translation": [ - 2.632161, - -4.421129, - 3.883869 - ], - "rotation_deg": [ - 8.422915, - 38.09696, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2020, - "type": "imguizmo", - "translation": [ - 2.519028, - -4.491849, - 3.903687 - ], - "rotation_deg": [ - 8.06089, - 37.906562, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2021, - "type": "imguizmo", - "translation": [ - 2.40414, - -4.559438, - 3.925444 - ], - "rotation_deg": [ - 7.693248, - 37.724591, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2022, - "type": "imguizmo", - "translation": [ - 2.287577, - -4.623849, - 3.949083 - ], - "rotation_deg": [ - 7.320245, - 37.551176, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2023, - "type": "imguizmo", - "translation": [ - 2.169419, - -4.685038, - 3.974535 - ], - "rotation_deg": [ - 6.94214, - 37.386436, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2024, - "type": "imguizmo", - "translation": [ - 2.049749, - -4.742962, - 4.00173 - ], - "rotation_deg": [ - 6.559197, - 37.230487, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2025, - "type": "imguizmo", - "translation": [ - 1.928651, - -4.797581, - 4.030593 - ], - "rotation_deg": [ - 6.171682, - 37.083437, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2026, - "type": "imguizmo", - "translation": [ - 1.806208, - -4.848856, - 4.061043 - ], - "rotation_deg": [ - 5.779867, - 36.945389, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2027, - "type": "imguizmo", - "translation": [ - 1.682507, - -4.896751, - 4.092996 - ], - "rotation_deg": [ - 5.384023, - 36.816439, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2028, - "type": "imguizmo", - "translation": [ - 1.557633, - -4.941234, - 4.126361 - ], - "rotation_deg": [ - 4.984427, - 36.696676, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2029, - "type": "imguizmo", - "translation": [ - 1.431674, - -4.982274, - 4.161047 - ], - "rotation_deg": [ - 4.581358, - 36.586185, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2030, - "type": "imguizmo", - "translation": [ - 1.304717, - -5.019841, - 4.196955 - ], - "rotation_deg": [ - 4.175095, - 36.485043, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2031, - "type": "imguizmo", - "translation": [ - 1.176851, - -5.05391, - 4.233988 - ], - "rotation_deg": [ - 3.765923, - 36.393319, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2032, - "type": "imguizmo", - "translation": [ - 1.048165, - -5.084457, - 4.27204 - ], - "rotation_deg": [ - 3.354126, - 36.311078, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2033, - "type": "imguizmo", - "translation": [ - 0.918748, - -5.11146, - 4.311007 - ], - "rotation_deg": [ - 2.939992, - 36.238377, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2034, - "type": "imguizmo", - "translation": [ - 0.78869, - -5.134901, - 4.350779 - ], - "rotation_deg": [ - 2.523809, - 36.175266, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2035, - "type": "imguizmo", - "translation": [ - 0.658083, - -5.154764, - 4.391245 - ], - "rotation_deg": [ - 2.105867, - 36.12179, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2036, - "type": "imguizmo", - "translation": [ - 0.527018, - -5.171034, - 4.432294 - ], - "rotation_deg": [ - 1.686458, - 36.077987, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2037, - "type": "imguizmo", - "translation": [ - 0.395585, - -5.1837, - 4.47381 - ], - "rotation_deg": [ - 1.265873, - 36.043885, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2038, - "type": "imguizmo", - "translation": [ - 0.263877, - -5.192753, - 4.515677 - ], - "rotation_deg": [ - 0.844406, - 36.01951, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2039, - "type": "imguizmo", - "translation": [ - 0.131984, - -5.198188, - 4.55778 - ], - "rotation_deg": [ - 0.42235, - 36.004878, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2040, - "type": "imguizmo", - "translation": [ - 0, - -5.2, - 4.6 - ], - "rotation_deg": [ - 0, - 36, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2041, - "type": "imguizmo", - "translation": [ - -0.131984, - -5.198188, - 4.64222 - ], - "rotation_deg": [ - -0.42235, - 36.004878, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2042, - "type": "imguizmo", - "translation": [ - -0.263877, - -5.192753, - 4.684323 - ], - "rotation_deg": [ - -0.844406, - 36.01951, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2043, - "type": "imguizmo", - "translation": [ - -0.395585, - -5.1837, - 4.72619 - ], - "rotation_deg": [ - -1.265873, - 36.043885, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2044, - "type": "imguizmo", - "translation": [ - -0.527018, - -5.171034, - 4.767706 - ], - "rotation_deg": [ - -1.686458, - 36.077987, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2045, - "type": "imguizmo", - "translation": [ - -0.658083, - -5.154764, - 4.808755 - ], - "rotation_deg": [ - -2.105867, - 36.12179, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2046, - "type": "imguizmo", - "translation": [ - -0.78869, - -5.134901, - 4.849221 - ], - "rotation_deg": [ - -2.523809, - 36.175266, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2047, - "type": "imguizmo", - "translation": [ - -0.918748, - -5.11146, - 4.888993 - ], - "rotation_deg": [ - -2.939992, - 36.238377, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2048, - "type": "imguizmo", - "translation": [ - -1.048165, - -5.084457, - 4.92796 - ], - "rotation_deg": [ - -3.354126, - 36.311078, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2049, - "type": "imguizmo", - "translation": [ - -1.176851, - -5.05391, - 4.966012 - ], - "rotation_deg": [ - -3.765923, - 36.393319, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2050, - "type": "imguizmo", - "translation": [ - -1.304717, - -5.019841, - 5.003045 - ], - "rotation_deg": [ - -4.175095, - 36.485043, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2051, - "type": "imguizmo", - "translation": [ - -1.431674, - -4.982274, - 5.038953 - ], - "rotation_deg": [ - -4.581358, - 36.586185, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2052, - "type": "imguizmo", - "translation": [ - -1.557633, - -4.941234, - 5.073639 - ], - "rotation_deg": [ - -4.984427, - 36.696676, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2053, - "type": "imguizmo", - "translation": [ - -1.682507, - -4.896751, - 5.107004 - ], - "rotation_deg": [ - -5.384023, - 36.816439, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2054, - "type": "imguizmo", - "translation": [ - -1.806208, - -4.848856, - 5.138957 - ], - "rotation_deg": [ - -5.779867, - 36.945389, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2055, - "type": "imguizmo", - "translation": [ - -1.928651, - -4.797581, - 5.169407 - ], - "rotation_deg": [ - -6.171682, - 37.083437, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2056, - "type": "imguizmo", - "translation": [ - -2.049749, - -4.742962, - 5.19827 - ], - "rotation_deg": [ - -6.559197, - 37.230487, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2057, - "type": "imguizmo", - "translation": [ - -2.169419, - -4.685038, - 5.225465 - ], - "rotation_deg": [ - -6.94214, - 37.386436, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2058, - "type": "imguizmo", - "translation": [ - -2.287577, - -4.623849, - 5.250917 - ], - "rotation_deg": [ - -7.320245, - 37.551176, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2059, - "type": "imguizmo", - "translation": [ - -2.40414, - -4.559438, - 5.274556 - ], - "rotation_deg": [ - -7.693248, - 37.724591, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2060, - "type": "imguizmo", - "translation": [ - -2.519028, - -4.491849, - 5.296313 - ], - "rotation_deg": [ - -8.06089, - 37.906562, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2061, - "type": "imguizmo", - "translation": [ - -2.632161, - -4.421129, - 5.316131 - ], - "rotation_deg": [ - -8.422915, - 38.09696, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2062, - "type": "imguizmo", - "translation": [ - -2.743459, - -4.347328, - 5.333952 - ], - "rotation_deg": [ - -8.779069, - 38.295654, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2063, - "type": "imguizmo", - "translation": [ - -2.852845, - -4.270498, - 5.349727 - ], - "rotation_deg": [ - -9.129105, - 38.502505, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2064, - "type": "imguizmo", - "translation": [ - -2.960243, - -4.190692, - 5.363413 - ], - "rotation_deg": [ - -9.472778, - 38.717369, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2065, - "type": "imguizmo", - "translation": [ - -3.065578, - -4.107965, - 5.374971 - ], - "rotation_deg": [ - -9.80985, - 38.940096, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2066, - "type": "imguizmo", - "translation": [ - -3.168777, - -4.022375, - 5.38437 - ], - "rotation_deg": [ - -10.140085, - 39.17053, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2067, - "type": "imguizmo", - "translation": [ - -3.269767, - -3.933981, - 5.391582 - ], - "rotation_deg": [ - -10.463254, - 39.408512, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2068, - "type": "imguizmo", - "translation": [ - -3.368478, - -3.842846, - 5.396587 - ], - "rotation_deg": [ - -10.77913, - 39.653875, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2069, - "type": "imguizmo", - "translation": [ - -3.464842, - -3.749033, - 5.399373 - ], - "rotation_deg": [ - -11.087495, - 39.906449, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2070, - "type": "imguizmo", - "translation": [ - -3.558791, - -3.652608, - 5.39993 - ], - "rotation_deg": [ - -11.388132, - 40.166057, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2071, - "type": "imguizmo", - "translation": [ - -3.65026, - -3.553636, - 5.398258 - ], - "rotation_deg": [ - -11.680833, - 40.432518, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2072, - "type": "imguizmo", - "translation": [ - -3.739185, - -3.452188, - 5.394361 - ], - "rotation_deg": [ - -11.965393, - 40.705647, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2073, - "type": "imguizmo", - "translation": [ - -3.825504, - -3.348334, - 5.38825 - ], - "rotation_deg": [ - -12.241614, - 40.985254, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2074, - "type": "imguizmo", - "translation": [ - -3.909157, - -3.242147, - 5.379942 - ], - "rotation_deg": [ - -12.509304, - 41.271143, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2075, - "type": "imguizmo", - "translation": [ - -3.990086, - -3.1337, - 5.369461 - ], - "rotation_deg": [ - -12.768276, - 41.563115, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2076, - "type": "imguizmo", - "translation": [ - -4.068234, - -3.023069, - 5.356834 - ], - "rotation_deg": [ - -13.018349, - 41.860967, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2077, - "type": "imguizmo", - "translation": [ - -4.143547, - -2.910332, - 5.342098 - ], - "rotation_deg": [ - -13.25935, - 42.164492, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2078, - "type": "imguizmo", - "translation": [ - -4.215972, - -2.795566, - 5.325294 - ], - "rotation_deg": [ - -13.49111, - 42.473477, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2079, - "type": "imguizmo", - "translation": [ - -4.285459, - -2.678852, - 5.306468 - ], - "rotation_deg": [ - -13.713468, - 42.787707, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2080, - "type": "imguizmo", - "translation": [ - -4.351959, - -2.560271, - 5.285673 - ], - "rotation_deg": [ - -13.926269, - 43.106964, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2081, - "type": "imguizmo", - "translation": [ - -4.415426, - -2.439905, - 5.262967 - ], - "rotation_deg": [ - -14.129364, - 43.431024, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2082, - "type": "imguizmo", - "translation": [ - -4.475816, - -2.317839, - 5.238414 - ], - "rotation_deg": [ - -14.322613, - 43.759663, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2083, - "type": "imguizmo", - "translation": [ - -4.533087, - -2.194158, - 5.212081 - ], - "rotation_deg": [ - -14.505879, - 44.092651, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2084, - "type": "imguizmo", - "translation": [ - -4.587199, - -2.068948, - 5.184042 - ], - "rotation_deg": [ - -14.679037, - 44.429755, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2085, - "type": "imguizmo", - "translation": [ - -4.638114, - -1.942296, - 5.154375 - ], - "rotation_deg": [ - -14.841964, - 44.770742, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2086, - "type": "imguizmo", - "translation": [ - -4.685796, - -1.81429, - 5.123163 - ], - "rotation_deg": [ - -14.994548, - 45.115373, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2087, - "type": "imguizmo", - "translation": [ - -4.730213, - -1.68502, - 5.090493 - ], - "rotation_deg": [ - -15.136681, - 45.463408, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2088, - "type": "imguizmo", - "translation": [ - -4.771333, - -1.554575, - 5.056455 - ], - "rotation_deg": [ - -15.268266, - 45.814605, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2089, - "type": "imguizmo", - "translation": [ - -4.809128, - -1.423048, - 5.021146 - ], - "rotation_deg": [ - -15.38921, - 46.168718, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2090, - "type": "imguizmo", - "translation": [ - -4.843572, - -1.290528, - 4.984662 - ], - "rotation_deg": [ - -15.499429, - 46.525502, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2091, - "type": "imguizmo", - "translation": [ - -4.87464, - -1.157109, - 4.947107 - ], - "rotation_deg": [ - -15.598847, - 46.884707, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2092, - "type": "imguizmo", - "translation": [ - -4.90231, - -1.022883, - 4.908584 - ], - "rotation_deg": [ - -15.687393, - 47.246083, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2093, - "type": "imguizmo", - "translation": [ - -4.926564, - -0.887945, - 4.869201 - ], - "rotation_deg": [ - -15.765006, - 47.609379, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2094, - "type": "imguizmo", - "translation": [ - -4.947385, - -0.752388, - 4.829068 - ], - "rotation_deg": [ - -15.831633, - 47.97434, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2095, - "type": "imguizmo", - "translation": [ - -4.964758, - -0.616306, - 4.788296 - ], - "rotation_deg": [ - -15.887226, - 48.340713, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2096, - "type": "imguizmo", - "translation": [ - -4.978671, - -0.479795, - 4.747 - ], - "rotation_deg": [ - -15.931747, - 48.708243, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2097, - "type": "imguizmo", - "translation": [ - -4.989114, - -0.34295, - 4.705293 - ], - "rotation_deg": [ - -15.965165, - 49.076673, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2098, - "type": "imguizmo", - "translation": [ - -4.99608, - -0.205866, - 4.663294 - ], - "rotation_deg": [ - -15.987456, - 49.445746, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2099, - "type": "imguizmo", - "translation": [ - -4.999564, - -0.068638, - 4.621117 - ], - "rotation_deg": [ - -15.998606, - 49.815206, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2100, - "type": "imguizmo", - "translation": [ - -4.999564, - 0.068638, - 4.578883 - ], - "rotation_deg": [ - -15.998606, - 50.184794, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2101, - "type": "imguizmo", - "translation": [ - -4.99608, - 0.205866, - 4.536706 - ], - "rotation_deg": [ - -15.987456, - 50.554254, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2102, - "type": "imguizmo", - "translation": [ - -4.989114, - 0.34295, - 4.494707 - ], - "rotation_deg": [ - -15.965165, - 50.923327, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2103, - "type": "imguizmo", - "translation": [ - -4.978671, - 0.479795, - 4.453 - ], - "rotation_deg": [ - -15.931747, - 51.291757, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2104, - "type": "imguizmo", - "translation": [ - -4.964758, - 0.616306, - 4.411704 - ], - "rotation_deg": [ - -15.887226, - 51.659287, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2105, - "type": "imguizmo", - "translation": [ - -4.947385, - 0.752388, - 4.370932 - ], - "rotation_deg": [ - -15.831633, - 52.02566, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2106, - "type": "imguizmo", - "translation": [ - -4.926564, - 0.887945, - 4.330799 - ], - "rotation_deg": [ - -15.765006, - 52.390621, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2107, - "type": "imguizmo", - "translation": [ - -4.90231, - 1.022883, - 4.291416 - ], - "rotation_deg": [ - -15.687393, - 52.753917, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2108, - "type": "imguizmo", - "translation": [ - -4.87464, - 1.157109, - 4.252893 - ], - "rotation_deg": [ - -15.598847, - 53.115293, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2109, - "type": "imguizmo", - "translation": [ - -4.843572, - 1.290528, - 4.215338 - ], - "rotation_deg": [ - -15.499429, - 53.474498, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2110, - "type": "imguizmo", - "translation": [ - -4.809128, - 1.423048, - 4.178854 - ], - "rotation_deg": [ - -15.38921, - 53.831282, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2111, - "type": "imguizmo", - "translation": [ - -4.771333, - 1.554575, - 4.143545 - ], - "rotation_deg": [ - -15.268266, - 54.185395, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2112, - "type": "imguizmo", - "translation": [ - -4.730213, - 1.68502, - 4.109507 - ], - "rotation_deg": [ - -15.136681, - 54.536592, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2113, - "type": "imguizmo", - "translation": [ - -4.685796, - 1.81429, - 4.076837 - ], - "rotation_deg": [ - -14.994548, - 54.884627, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2114, - "type": "imguizmo", - "translation": [ - -4.638114, - 1.942296, - 4.045625 - ], - "rotation_deg": [ - -14.841964, - 55.229258, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2115, - "type": "imguizmo", - "translation": [ - -4.587199, - 2.068948, - 4.015958 - ], - "rotation_deg": [ - -14.679037, - 55.570245, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2116, - "type": "imguizmo", - "translation": [ - -4.533087, - 2.194158, - 3.987919 - ], - "rotation_deg": [ - -14.505879, - 55.907349, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2117, - "type": "imguizmo", - "translation": [ - -4.475816, - 2.317839, - 3.961586 - ], - "rotation_deg": [ - -14.322613, - 56.240337, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2118, - "type": "imguizmo", - "translation": [ - -4.415426, - 2.439905, - 3.937033 - ], - "rotation_deg": [ - -14.129364, - 56.568976, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2119, - "type": "imguizmo", - "translation": [ - -4.351959, - 2.560271, - 3.914327 - ], - "rotation_deg": [ - -13.926269, - 56.893036, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2120, - "type": "imguizmo", - "translation": [ - -4.285459, - 2.678852, - 3.893532 - ], - "rotation_deg": [ - -13.713468, - 57.212293, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2121, - "type": "imguizmo", - "translation": [ - -4.215972, - 2.795566, - 3.874706 - ], - "rotation_deg": [ - -13.49111, - 57.526523, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2122, - "type": "imguizmo", - "translation": [ - -4.143547, - 2.910332, - 3.857902 - ], - "rotation_deg": [ - -13.25935, - 57.835508, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2123, - "type": "imguizmo", - "translation": [ - -4.068234, - 3.023069, - 3.843166 - ], - "rotation_deg": [ - -13.018349, - 58.139033, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2124, - "type": "imguizmo", - "translation": [ - -3.990086, - 3.1337, - 3.830539 - ], - "rotation_deg": [ - -12.768276, - 58.436885, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2125, - "type": "imguizmo", - "translation": [ - -3.909157, - 3.242147, - 3.820058 - ], - "rotation_deg": [ - -12.509304, - 58.728857, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2126, - "type": "imguizmo", - "translation": [ - -3.825504, - 3.348334, - 3.81175 - ], - "rotation_deg": [ - -12.241614, - 59.014746, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2127, - "type": "imguizmo", - "translation": [ - -3.739185, - 3.452188, - 3.805639 - ], - "rotation_deg": [ - -11.965393, - 59.294353, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2128, - "type": "imguizmo", - "translation": [ - -3.65026, - 3.553636, - 3.801742 - ], - "rotation_deg": [ - -11.680833, - 59.567482, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2129, - "type": "imguizmo", - "translation": [ - -3.558791, - 3.652608, - 3.80007 - ], - "rotation_deg": [ - -11.388132, - 59.833943, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2130, - "type": "imguizmo", - "translation": [ - -3.464842, - 3.749033, - 3.800627 - ], - "rotation_deg": [ - -11.087495, - 60.093551, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2131, - "type": "imguizmo", - "translation": [ - -3.368478, - 3.842846, - 3.803413 - ], - "rotation_deg": [ - -10.77913, - 60.346125, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2132, - "type": "imguizmo", - "translation": [ - -3.269767, - 3.933981, - 3.808418 - ], - "rotation_deg": [ - -10.463254, - 60.591488, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2133, - "type": "imguizmo", - "translation": [ - -3.168777, - 4.022375, - 3.81563 - ], - "rotation_deg": [ - -10.140085, - 60.82947, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2134, - "type": "imguizmo", - "translation": [ - -3.065578, - 4.107965, - 3.825029 - ], - "rotation_deg": [ - -9.80985, - 61.059904, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2135, - "type": "imguizmo", - "translation": [ - -2.960243, - 4.190692, - 3.836587 - ], - "rotation_deg": [ - -9.472778, - 61.282631, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2136, - "type": "imguizmo", - "translation": [ - -2.852845, - 4.270498, - 3.850273 - ], - "rotation_deg": [ - -9.129105, - 61.497495, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2137, - "type": "imguizmo", - "translation": [ - -2.743459, - 4.347328, - 3.866048 - ], - "rotation_deg": [ - -8.779069, - 61.704346, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2138, - "type": "imguizmo", - "translation": [ - -2.632161, - 4.421129, - 3.883869 - ], - "rotation_deg": [ - -8.422915, - 61.90304, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2139, - "type": "imguizmo", - "translation": [ - -2.519028, - 4.491849, - 3.903687 - ], - "rotation_deg": [ - -8.06089, - 62.093438, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2140, - "type": "imguizmo", - "translation": [ - -2.40414, - 4.559438, - 3.925444 - ], - "rotation_deg": [ - -7.693248, - 62.275409, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2141, - "type": "imguizmo", - "translation": [ - -2.287577, - 4.623849, - 3.949083 - ], - "rotation_deg": [ - -7.320245, - 62.448824, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2142, - "type": "imguizmo", - "translation": [ - -2.169419, - 4.685038, - 3.974535 - ], - "rotation_deg": [ - -6.94214, - 62.613564, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2143, - "type": "imguizmo", - "translation": [ - -2.049749, - 4.742962, - 4.00173 - ], - "rotation_deg": [ - -6.559197, - 62.769513, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2144, - "type": "imguizmo", - "translation": [ - -1.928651, - 4.797581, - 4.030593 - ], - "rotation_deg": [ - -6.171682, - 62.916563, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2145, - "type": "imguizmo", - "translation": [ - -1.806208, - 4.848856, - 4.061043 - ], - "rotation_deg": [ - -5.779867, - 63.054611, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2146, - "type": "imguizmo", - "translation": [ - -1.682507, - 4.896751, - 4.092996 - ], - "rotation_deg": [ - -5.384023, - 63.183561, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2147, - "type": "imguizmo", - "translation": [ - -1.557633, - 4.941234, - 4.126361 - ], - "rotation_deg": [ - -4.984427, - 63.303324, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2148, - "type": "imguizmo", - "translation": [ - -1.431674, - 4.982274, - 4.161047 - ], - "rotation_deg": [ - -4.581358, - 63.413815, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2149, - "type": "imguizmo", - "translation": [ - -1.304717, - 5.019841, - 4.196955 - ], - "rotation_deg": [ - -4.175095, - 63.514957, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2150, - "type": "imguizmo", - "translation": [ - -1.176851, - 5.05391, - 4.233988 - ], - "rotation_deg": [ - -3.765923, - 63.606681, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2151, - "type": "imguizmo", - "translation": [ - -1.048165, - 5.084457, - 4.27204 - ], - "rotation_deg": [ - -3.354126, - 63.688922, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2152, - "type": "imguizmo", - "translation": [ - -0.918748, - 5.11146, - 4.311007 - ], - "rotation_deg": [ - -2.939992, - 63.761623, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2153, - "type": "imguizmo", - "translation": [ - -0.78869, - 5.134901, - 4.350779 - ], - "rotation_deg": [ - -2.523809, - 63.824734, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2154, - "type": "imguizmo", - "translation": [ - -0.658083, - 5.154764, - 4.391245 - ], - "rotation_deg": [ - -2.105867, - 63.87821, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2155, - "type": "imguizmo", - "translation": [ - -0.527018, - 5.171034, - 4.432294 - ], - "rotation_deg": [ - -1.686458, - 63.922013, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2156, - "type": "imguizmo", - "translation": [ - -0.395585, - 5.1837, - 4.47381 - ], - "rotation_deg": [ - -1.265873, - 63.956115, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2157, - "type": "imguizmo", - "translation": [ - -0.263877, - 5.192753, - 4.515677 - ], - "rotation_deg": [ - -0.844406, - 63.98049, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2158, - "type": "imguizmo", - "translation": [ - -0.131984, - 5.198188, - 4.55778 - ], - "rotation_deg": [ - -0.42235, - 63.995122, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2159, - "type": "imguizmo", - "translation": [ - 0, - 5.2, - 4.6 - ], - "rotation_deg": [ - 0, - 64, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2160, - "type": "action", - "action": "set_active_render_window", - "value": 0 - }, - { - "frame": 2160, - "type": "action", - "action": "set_active_planar", - "value": 9 - }, - { - "frame": 2160, - "type": "action", - "action": "set_projection_type", - "value": "perspective" - }, - { - "frame": 2160, - "type": "action", - "action": "set_left_handed", - "value": true - }, - { - "frame": 2160, - "type": "action", - "action": "reset_active_camera", - "value": 1 - }, - { - "frame": 2160, - "type": "action", - "action": "set_active_render_window", - "value": 1 - }, - { - "frame": 2160, - "type": "action", - "action": "set_active_planar", - "value": 9 - }, - { - "frame": 2160, - "type": "action", - "action": "set_projection_type", - "value": "orthographic" - }, - { - "frame": 2160, - "type": "action", - "action": "set_left_handed", - "value": true - }, - { - "frame": 2160, - "type": "action", - "action": "set_active_render_window", - "value": 0 - }, - { - "frame": 2161, - "type": "imguizmo", - "translation": [ - 0.0, - 0.188594, - 12.5 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2162, - "type": "imguizmo", - "translation": [ - 0.050154, - 0.215598, - 12.495644 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2163, - "type": "imguizmo", - "translation": [ - 0.100273, - 0.242001, - 12.48258 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2164, - "type": "imguizmo", - "translation": [ - 0.150322, - 0.267729, - 12.460817 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2165, - "type": "imguizmo", - "translation": [ - 0.200267, - 0.292711, - 12.430369 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2166, - "type": "imguizmo", - "translation": [ - 0.250072, - 0.316878, - 12.391259 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2167, - "type": "imguizmo", - "translation": [ - 0.299702, - 0.340161, - 12.343512 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2168, - "type": "imguizmo", - "translation": [ - 0.349124, - 0.362496, - 12.287164 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2169, - "type": "imguizmo", - "translation": [ - 0.398303, - 0.383821, - 12.222252 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2170, - "type": "imguizmo", - "translation": [ - 0.447203, - 0.404075, - 12.148822 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2171, - "type": "imguizmo", - "translation": [ - 0.495793, - 0.423204, - 12.066926 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2172, - "type": "imguizmo", - "translation": [ - 0.544036, - 0.441153, - 11.97662 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2173, - "type": "imguizmo", - "translation": [ - 0.591901, - 0.457873, - 11.877968 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2174, - "type": "imguizmo", - "translation": [ - 0.639353, - 0.473316, - 11.771037 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2175, - "type": "imguizmo", - "translation": [ - 0.686359, - 0.487441, - 11.655903 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2176, - "type": "imguizmo", - "translation": [ - 0.732887, - 0.500206, - 11.532646 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2177, - "type": "imguizmo", - "translation": [ - 0.778905, - 0.511578, - 11.401351 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2178, - "type": "imguizmo", - "translation": [ - 0.824379, - 0.521523, - 11.262111 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2179, - "type": "imguizmo", - "translation": [ - 0.869279, - 0.530015, - 11.115022 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2180, - "type": "imguizmo", - "translation": [ - 0.913573, - 0.53703, - 10.960187 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2181, - "type": "imguizmo", - "translation": [ - 0.957231, - 0.542548, - 10.797713 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2182, - "type": "imguizmo", - "translation": [ - 1.000221, - 0.546554, - 10.627714 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2183, - "type": "imguizmo", - "translation": [ - 1.042514, - 0.549037, - 10.450309 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2184, - "type": "imguizmo", - "translation": [ - 1.084081, - 0.549989, - 10.265621 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2185, - "type": "imguizmo", - "translation": [ - 1.124892, - 0.549408, - 10.073778 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2186, - "type": "imguizmo", - "translation": [ - 1.16492, - 0.547296, - 9.874915 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2187, - "type": "imguizmo", - "translation": [ - 1.204135, - 0.543659, - 9.66917 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2188, - "type": "imguizmo", - "translation": [ - 1.242511, - 0.538506, - 9.456686 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2189, - "type": "imguizmo", - "translation": [ - 1.280022, - 0.531852, - 9.237611 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2190, - "type": "imguizmo", - "translation": [ - 1.31664, - 0.523716, - 9.012099 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2191, - "type": "imguizmo", - "translation": [ - 1.352341, - 0.514121, - 8.780307 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2192, - "type": "imguizmo", - "translation": [ - 1.387099, - 0.503092, - 8.542395 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2193, - "type": "imguizmo", - "translation": [ - 1.42089, - 0.490661, - 8.298529 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2194, - "type": "imguizmo", - "translation": [ - 1.453692, - 0.476863, - 8.048881 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2195, - "type": "imguizmo", - "translation": [ - 1.48548, - 0.461735, - 7.793623 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2196, - "type": "imguizmo", - "translation": [ - 1.516233, - 0.445321, - 7.532933 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2197, - "type": "imguizmo", - "translation": [ - 1.545929, - 0.427665, - 7.266994 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2198, - "type": "imguizmo", - "translation": [ - 1.574548, - 0.408818, - 6.99599 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2199, - "type": "imguizmo", - "translation": [ - 1.602069, - 0.388831, - 6.72011 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2200, - "type": "imguizmo", - "translation": [ - 1.628474, - 0.36776, - 6.439547 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2201, - "type": "imguizmo", - "translation": [ - 1.653744, - 0.345664, - 6.154497 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2202, - "type": "imguizmo", - "translation": [ - 1.677862, - 0.322605, - 5.865157 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2203, - "type": "imguizmo", - "translation": [ - 1.70081, - 0.298646, - 5.571729 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2204, - "type": "imguizmo", - "translation": [ - 1.722573, - 0.273856, - 5.274419 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2205, - "type": "imguizmo", - "translation": [ - 1.743136, - 0.248302, - 4.973433 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2206, - "type": "imguizmo", - "translation": [ - 1.762483, - 0.222056, - 4.668981 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2207, - "type": "imguizmo", - "translation": [ - 1.780603, - 0.19519, - 4.361274 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2208, - "type": "imguizmo", - "translation": [ - 1.797481, - 0.167781, - 4.050529 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2209, - "type": "imguizmo", - "translation": [ - 1.813107, - 0.139905, - 3.73696 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2210, - "type": "imguizmo", - "translation": [ - 1.827469, - 0.111638, - 3.420787 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2211, - "type": "imguizmo", - "translation": [ - 1.840557, - 0.08306, - 3.102231 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2212, - "type": "imguizmo", - "translation": [ - 1.852363, - 0.054251, - 2.781512 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2213, - "type": "imguizmo", - "translation": [ - 1.862878, - 0.025291, - 2.458854 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2214, - "type": "imguizmo", - "translation": [ - 1.872094, - -0.00374, - 2.134483 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2215, - "type": "imguizmo", - "translation": [ - 1.880006, - -0.032761, - 1.808625 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2216, - "type": "imguizmo", - "translation": [ - 1.886608, - -0.06169, - 1.481506 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2217, - "type": "imguizmo", - "translation": [ - 1.891895, - -0.090448, - 1.153354 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2218, - "type": "imguizmo", - "translation": [ - 1.895863, - -0.118953, - 0.824399 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2219, - "type": "imguizmo", - "translation": [ - 1.89851, - -0.147126, - 0.494869 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2220, - "type": "imguizmo", - "translation": [ - 1.899834, - -0.17489, - 0.164995 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2221, - "type": "imguizmo", - "translation": [ - 1.899834, - -0.202166, - -0.164995 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2222, - "type": "imguizmo", - "translation": [ - 1.89851, - -0.228879, - -0.494869 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2223, - "type": "imguizmo", - "translation": [ - 1.895863, - -0.254954, - -0.824399 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2224, - "type": "imguizmo", - "translation": [ - 1.891895, - -0.280318, - -1.153354 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2225, - "type": "imguizmo", - "translation": [ - 1.886608, - -0.304901, - -1.481506 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2226, - "type": "imguizmo", - "translation": [ - 1.880006, - -0.328634, - -1.808625 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2227, - "type": "imguizmo", - "translation": [ - 1.872094, - -0.351451, - -2.134483 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2228, - "type": "imguizmo", - "translation": [ - 1.862878, - -0.373288, - -2.458854 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2229, - "type": "imguizmo", - "translation": [ - 1.852363, - -0.394085, - -2.781512 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2230, - "type": "imguizmo", - "translation": [ - 1.840557, - -0.413784, - -3.102231 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2231, - "type": "imguizmo", - "translation": [ - 1.827469, - -0.432329, - -3.420787 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2232, - "type": "imguizmo", - "translation": [ - 1.813107, - -0.44967, - -3.73696 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2233, - "type": "imguizmo", - "translation": [ - 1.797481, - -0.465757, - -4.050529 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2234, - "type": "imguizmo", - "translation": [ - 1.780603, - -0.480546, - -4.361274 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2235, - "type": "imguizmo", - "translation": [ - 1.762483, - -0.493996, - -4.668981 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2236, - "type": "imguizmo", - "translation": [ - 1.743136, - -0.506068, - -4.973433 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2237, - "type": "imguizmo", - "translation": [ - 1.722573, - -0.516731, - -5.274419 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2238, - "type": "imguizmo", - "translation": [ - 1.70081, - -0.525953, - -5.571729 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2239, - "type": "imguizmo", - "translation": [ - 1.677862, - -0.533709, - -5.865157 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2240, - "type": "imguizmo", - "translation": [ - 1.653744, - -0.539977, - -6.154497 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2241, - "type": "imguizmo", - "translation": [ - 1.628474, - -0.544741, - -6.439547 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2242, - "type": "imguizmo", - "translation": [ - 1.602069, - -0.547986, - -6.72011 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2243, - "type": "imguizmo", - "translation": [ - 1.574548, - -0.549704, - -6.99599 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2244, - "type": "imguizmo", - "translation": [ - 1.545929, - -0.54989, - -7.266994 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2245, - "type": "imguizmo", - "translation": [ - 1.516233, - -0.548543, - -7.532933 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2246, - "type": "imguizmo", - "translation": [ - 1.48548, - -0.545667, - -7.793623 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2247, - "type": "imguizmo", - "translation": [ - 1.453692, - -0.541271, - -8.048881 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2248, - "type": "imguizmo", - "translation": [ - 1.42089, - -0.535366, - -8.298529 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2249, - "type": "imguizmo", - "translation": [ - 1.387099, - -0.527968, - -8.542395 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2250, - "type": "imguizmo", - "translation": [ - 1.352341, - -0.519099, - -8.780307 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2251, - "type": "imguizmo", - "translation": [ - 1.31664, - -0.508784, - -9.012099 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2252, - "type": "imguizmo", - "translation": [ - 1.280022, - -0.49705, - -9.237611 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2253, - "type": "imguizmo", - "translation": [ - 1.242511, - -0.483931, - -9.456686 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2254, - "type": "imguizmo", - "translation": [ - 1.204135, - -0.469463, - -9.66917 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2255, - "type": "imguizmo", - "translation": [ - 1.16492, - -0.453686, - -9.874915 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2256, - "type": "imguizmo", - "translation": [ - 1.124892, - -0.436645, - -10.073778 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2257, - "type": "imguizmo", - "translation": [ - 1.084081, - -0.418387, - -10.265621 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2258, - "type": "imguizmo", - "translation": [ - 1.042514, - -0.398963, - -10.450309 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2259, - "type": "imguizmo", - "translation": [ - 1.000221, - -0.378427, - -10.627714 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2260, - "type": "imguizmo", - "translation": [ - 0.957231, - -0.356836, - -10.797713 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2261, - "type": "imguizmo", - "translation": [ - 0.913573, - -0.334251, - -10.960187 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2262, - "type": "imguizmo", - "translation": [ - 0.869279, - -0.310734, - -11.115022 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2263, - "type": "imguizmo", - "translation": [ - 0.824379, - -0.286351, - -11.262111 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2264, - "type": "imguizmo", - "translation": [ - 0.778905, - -0.26117, - -11.401351 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2265, - "type": "imguizmo", - "translation": [ - 0.732887, - -0.235261, - -11.532646 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2266, - "type": "imguizmo", - "translation": [ - 0.686359, - -0.208696, - -11.655903 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2267, - "type": "imguizmo", - "translation": [ - 0.639353, - -0.181549, - -11.771037 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2268, - "type": "imguizmo", - "translation": [ - 0.591901, - -0.153897, - -11.877968 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2269, - "type": "imguizmo", - "translation": [ - 0.544036, - -0.125815, - -11.97662 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2270, - "type": "imguizmo", - "translation": [ - 0.495793, - -0.097383, - -12.066926 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2271, - "type": "imguizmo", - "translation": [ - 0.447203, - -0.06868, - -12.148822 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2272, - "type": "imguizmo", - "translation": [ - 0.398303, - -0.039785, - -12.222252 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2273, - "type": "imguizmo", - "translation": [ - 0.349124, - -0.010779, - -12.287164 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2274, - "type": "imguizmo", - "translation": [ - 0.299702, - 0.018257, - -12.343512 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2275, - "type": "imguizmo", - "translation": [ - 0.250072, - 0.047242, - -12.391259 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2276, - "type": "imguizmo", - "translation": [ - 0.200267, - 0.076095, - -12.430369 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2277, - "type": "imguizmo", - "translation": [ - 0.150322, - 0.104737, - -12.460817 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2278, - "type": "imguizmo", - "translation": [ - 0.100273, - 0.133086, - -12.48258 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2279, - "type": "imguizmo", - "translation": [ - 0.050154, - 0.161064, - -12.495644 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2280, - "type": "imguizmo", - "translation": [ - 0.0, - 0.188594, - -12.5 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2281, - "type": "imguizmo", - "translation": [ - -0.050154, - 0.215598, - -12.495644 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2282, - "type": "imguizmo", - "translation": [ - -0.100273, - 0.242001, - -12.48258 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2283, - "type": "imguizmo", - "translation": [ - -0.150322, - 0.267729, - -12.460817 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2284, - "type": "imguizmo", - "translation": [ - -0.200267, - 0.292711, - -12.430369 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2285, - "type": "imguizmo", - "translation": [ - -0.250072, - 0.316878, - -12.391259 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2286, - "type": "imguizmo", - "translation": [ - -0.299702, - 0.340161, - -12.343512 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2287, - "type": "imguizmo", - "translation": [ - -0.349124, - 0.362496, - -12.287164 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2288, - "type": "imguizmo", - "translation": [ - -0.398303, - 0.383821, - -12.222252 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2289, - "type": "imguizmo", - "translation": [ - -0.447203, - 0.404075, - -12.148822 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2290, - "type": "imguizmo", - "translation": [ - -0.495793, - 0.423204, - -12.066926 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2291, - "type": "imguizmo", - "translation": [ - -0.544036, - 0.441153, - -11.97662 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2292, - "type": "imguizmo", - "translation": [ - -0.591901, - 0.457873, - -11.877968 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2293, - "type": "imguizmo", - "translation": [ - -0.639353, - 0.473316, - -11.771037 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2294, - "type": "imguizmo", - "translation": [ - -0.686359, - 0.487441, - -11.655903 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2295, - "type": "imguizmo", - "translation": [ - -0.732887, - 0.500206, - -11.532646 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2296, - "type": "imguizmo", - "translation": [ - -0.778905, - 0.511578, - -11.401351 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2297, - "type": "imguizmo", - "translation": [ - -0.824379, - 0.521523, - -11.262111 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2298, - "type": "imguizmo", - "translation": [ - -0.869279, - 0.530015, - -11.115022 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2299, - "type": "imguizmo", - "translation": [ - -0.913573, - 0.53703, - -10.960187 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2300, - "type": "imguizmo", - "translation": [ - -0.957231, - 0.542548, - -10.797713 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2301, - "type": "imguizmo", - "translation": [ - -1.000221, - 0.546554, - -10.627714 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2302, - "type": "imguizmo", - "translation": [ - -1.042514, - 0.549037, - -10.450309 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2303, - "type": "imguizmo", - "translation": [ - -1.084081, - 0.549989, - -10.265621 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2304, - "type": "imguizmo", - "translation": [ - -1.124892, - 0.549408, - -10.073778 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2305, - "type": "imguizmo", - "translation": [ - -1.16492, - 0.547296, - -9.874915 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2306, - "type": "imguizmo", - "translation": [ - -1.204135, - 0.543659, - -9.66917 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2307, - "type": "imguizmo", - "translation": [ - -1.242511, - 0.538506, - -9.456686 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2308, - "type": "imguizmo", - "translation": [ - -1.280022, - 0.531852, - -9.237611 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2309, - "type": "imguizmo", - "translation": [ - -1.31664, - 0.523716, - -9.012099 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2310, - "type": "imguizmo", - "translation": [ - -1.352341, - 0.514121, - -8.780307 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2311, - "type": "imguizmo", - "translation": [ - -1.387099, - 0.503092, - -8.542395 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2312, - "type": "imguizmo", - "translation": [ - -1.42089, - 0.490661, - -8.298529 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2313, - "type": "imguizmo", - "translation": [ - -1.453692, - 0.476863, - -8.048881 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2314, - "type": "imguizmo", - "translation": [ - -1.48548, - 0.461735, - -7.793623 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2315, - "type": "imguizmo", - "translation": [ - -1.516233, - 0.445321, - -7.532933 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2316, - "type": "imguizmo", - "translation": [ - -1.545929, - 0.427665, - -7.266994 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2317, - "type": "imguizmo", - "translation": [ - -1.574548, - 0.408818, - -6.99599 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2318, - "type": "imguizmo", - "translation": [ - -1.602069, - 0.388831, - -6.72011 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2319, - "type": "imguizmo", - "translation": [ - -1.628474, - 0.36776, - -6.439547 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2320, - "type": "imguizmo", - "translation": [ - -1.653744, - 0.345664, - -6.154497 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2321, - "type": "imguizmo", - "translation": [ - -1.677862, - 0.322605, - -5.865157 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2322, - "type": "imguizmo", - "translation": [ - -1.70081, - 0.298646, - -5.571729 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2323, - "type": "imguizmo", - "translation": [ - -1.722573, - 0.273856, - -5.274419 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2324, - "type": "imguizmo", - "translation": [ - -1.743136, - 0.248302, - -4.973433 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2325, - "type": "imguizmo", - "translation": [ - -1.762483, - 0.222056, - -4.668981 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2326, - "type": "imguizmo", - "translation": [ - -1.780603, - 0.19519, - -4.361274 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2327, - "type": "imguizmo", - "translation": [ - -1.797481, - 0.167781, - -4.050529 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2328, - "type": "imguizmo", - "translation": [ - -1.813107, - 0.139905, - -3.73696 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2329, - "type": "imguizmo", - "translation": [ - -1.827469, - 0.111638, - -3.420787 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2330, - "type": "imguizmo", - "translation": [ - -1.840557, - 0.08306, - -3.102231 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2331, - "type": "imguizmo", - "translation": [ - -1.852363, - 0.054251, - -2.781512 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2332, - "type": "imguizmo", - "translation": [ - -1.862878, - 0.025291, - -2.458854 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2333, - "type": "imguizmo", - "translation": [ - -1.872094, - -0.00374, - -2.134483 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2334, - "type": "imguizmo", - "translation": [ - -1.880006, - -0.032761, - -1.808625 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2335, - "type": "imguizmo", - "translation": [ - -1.886608, - -0.06169, - -1.481506 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2336, - "type": "imguizmo", - "translation": [ - -1.891895, - -0.090448, - -1.153354 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2337, - "type": "imguizmo", - "translation": [ - -1.895863, - -0.118953, - -0.824399 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2338, - "type": "imguizmo", - "translation": [ - -1.89851, - -0.147126, - -0.494869 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2339, - "type": "imguizmo", - "translation": [ - -1.899834, - -0.17489, - -0.164995 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2340, - "type": "imguizmo", - "translation": [ - -1.899834, - -0.202166, - 0.164995 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2341, - "type": "imguizmo", - "translation": [ - -1.89851, - -0.228879, - 0.494869 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2342, - "type": "imguizmo", - "translation": [ - -1.895863, - -0.254954, - 0.824399 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2343, - "type": "imguizmo", - "translation": [ - -1.891895, - -0.280318, - 1.153354 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2344, - "type": "imguizmo", - "translation": [ - -1.886608, - -0.304901, - 1.481506 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2345, - "type": "imguizmo", - "translation": [ - -1.880006, - -0.328634, - 1.808625 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2346, - "type": "imguizmo", - "translation": [ - -1.872094, - -0.351451, - 2.134483 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2347, - "type": "imguizmo", - "translation": [ - -1.862878, - -0.373288, - 2.458854 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2348, - "type": "imguizmo", - "translation": [ - -1.852363, - -0.394085, - 2.781512 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2349, - "type": "imguizmo", - "translation": [ - -1.840557, - -0.413784, - 3.102231 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2350, - "type": "imguizmo", - "translation": [ - -1.827469, - -0.432329, - 3.420787 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2351, - "type": "imguizmo", - "translation": [ - -1.813107, - -0.44967, - 3.73696 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2352, - "type": "imguizmo", - "translation": [ - -1.797481, - -0.465757, - 4.050529 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2353, - "type": "imguizmo", - "translation": [ - -1.780603, - -0.480546, - 4.361274 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2354, - "type": "imguizmo", - "translation": [ - -1.762483, - -0.493996, - 4.668981 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2355, - "type": "imguizmo", - "translation": [ - -1.743136, - -0.506068, - 4.973433 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2356, - "type": "imguizmo", - "translation": [ - -1.722573, - -0.516731, - 5.274419 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2357, - "type": "imguizmo", - "translation": [ - -1.70081, - -0.525953, - 5.571729 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2358, - "type": "imguizmo", - "translation": [ - -1.677862, - -0.533709, - 5.865157 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2359, - "type": "imguizmo", - "translation": [ - -1.653744, - -0.539977, - 6.154497 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2360, - "type": "imguizmo", - "translation": [ - -1.628474, - -0.544741, - 6.439547 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2361, - "type": "imguizmo", - "translation": [ - -1.602069, - -0.547986, - 6.72011 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2362, - "type": "imguizmo", - "translation": [ - -1.574548, - -0.549704, - 6.99599 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2363, - "type": "imguizmo", - "translation": [ - -1.545929, - -0.54989, - 7.266994 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2364, - "type": "imguizmo", - "translation": [ - -1.516233, - -0.548543, - 7.532933 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2365, - "type": "imguizmo", - "translation": [ - -1.48548, - -0.545667, - 7.793623 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2366, - "type": "imguizmo", - "translation": [ - -1.453692, - -0.541271, - 8.048881 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2367, - "type": "imguizmo", - "translation": [ - -1.42089, - -0.535366, - 8.298529 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2368, - "type": "imguizmo", - "translation": [ - -1.387099, - -0.527968, - 8.542395 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2369, - "type": "imguizmo", - "translation": [ - -1.352341, - -0.519099, - 8.780307 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2370, - "type": "imguizmo", - "translation": [ - -1.31664, - -0.508784, - 9.012099 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2371, - "type": "imguizmo", - "translation": [ - -1.280022, - -0.49705, - 9.237611 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2372, - "type": "imguizmo", - "translation": [ - -1.242511, - -0.483931, - 9.456686 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2373, - "type": "imguizmo", - "translation": [ - -1.204135, - -0.469463, - 9.66917 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2374, - "type": "imguizmo", - "translation": [ - -1.16492, - -0.453686, - 9.874915 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2375, - "type": "imguizmo", - "translation": [ - -1.124892, - -0.436645, - 10.073778 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2376, - "type": "imguizmo", - "translation": [ - -1.084081, - -0.418387, - 10.265621 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2377, - "type": "imguizmo", - "translation": [ - -1.042514, - -0.398963, - 10.450309 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2378, - "type": "imguizmo", - "translation": [ - -1.000221, - -0.378427, - 10.627714 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2379, - "type": "imguizmo", - "translation": [ - -0.957231, - -0.356836, - 10.797713 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2380, - "type": "imguizmo", - "translation": [ - -0.913573, - -0.334251, - 10.960187 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2381, - "type": "imguizmo", - "translation": [ - -0.869279, - -0.310734, - 11.115022 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2382, - "type": "imguizmo", - "translation": [ - -0.824379, - -0.286351, - 11.262111 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2383, - "type": "imguizmo", - "translation": [ - -0.778905, - -0.26117, - 11.401351 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2384, - "type": "imguizmo", - "translation": [ - -0.732887, - -0.235261, - 11.532646 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2385, - "type": "imguizmo", - "translation": [ - -0.686359, - -0.208696, - 11.655903 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2386, - "type": "imguizmo", - "translation": [ - -0.639353, - -0.181549, - 11.771037 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2387, - "type": "imguizmo", - "translation": [ - -0.591901, - -0.153897, - 11.877968 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2388, - "type": "imguizmo", - "translation": [ - -0.544036, - -0.125815, - 11.97662 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2389, - "type": "imguizmo", - "translation": [ - -0.495793, - -0.097383, - 12.066926 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2390, - "type": "imguizmo", - "translation": [ - -0.447203, - -0.06868, - 12.148822 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2391, - "type": "imguizmo", - "translation": [ - -0.398303, - -0.039785, - 12.222252 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2392, - "type": "imguizmo", - "translation": [ - -0.349124, - -0.010779, - 12.287164 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2393, - "type": "imguizmo", - "translation": [ - -0.299702, - 0.018257, - 12.343512 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2394, - "type": "imguizmo", - "translation": [ - -0.250072, - 0.047242, - 12.391259 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2395, - "type": "imguizmo", - "translation": [ - -0.200267, - 0.076095, - 12.430369 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2396, - "type": "imguizmo", - "translation": [ - -0.150322, - 0.104737, - 12.460817 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2397, - "type": "imguizmo", - "translation": [ - -0.100273, - 0.133086, - 12.48258 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2398, - "type": "imguizmo", - "translation": [ - -0.050154, - 0.161064, - 12.495644 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2399, - "type": "imguizmo", - "translation": [ - -0.0, - 0.188594, - 12.5 - ], - "rotation_deg": [ - 0.0, - 0.0, - 0.0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2400, - "type": "action", - "action": "set_active_render_window", - "value": 0 - }, - { - "frame": 2400, - "type": "action", - "action": "set_active_planar", - "value": 10 - }, - { - "frame": 2400, - "type": "action", - "action": "set_projection_type", - "value": "perspective" - }, - { - "frame": 2400, - "type": "action", - "action": "set_left_handed", - "value": true - }, - { - "frame": 2400, - "type": "action", - "action": "reset_active_camera", - "value": 1 - }, - { - "frame": 2400, - "type": "action", - "action": "set_active_render_window", - "value": 1 - }, - { - "frame": 2400, - "type": "action", - "action": "set_active_planar", - "value": 10 - }, - { - "frame": 2400, - "type": "action", - "action": "set_projection_type", - "value": "orthographic" - }, - { - "frame": 2400, - "type": "action", - "action": "set_left_handed", - "value": true - }, - { - "frame": 2400, - "type": "action", - "action": "set_active_render_window", - "value": 0 - }, - { - "frame": 2401, - "type": "imguizmo", - "translation": [ - 2.2, - 0, - 6.3 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2402, - "type": "imguizmo", - "translation": [ - 2.213198, - 0.06333, - 6.299686 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2403, - "type": "imguizmo", - "translation": [ - 2.226388, - 0.126484, - 6.298746 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2404, - "type": "imguizmo", - "translation": [ - 2.239559, - 0.189286, - 6.297179 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2405, - "type": "imguizmo", - "translation": [ - 2.252702, - 0.251559, - 6.294987 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2406, - "type": "imguizmo", - "translation": [ - 2.265808, - 0.313132, - 6.292171 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2407, - "type": "imguizmo", - "translation": [ - 2.278869, - 0.373832, - 6.288733 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2408, - "type": "imguizmo", - "translation": [ - 2.291875, - 0.43349, - 6.284676 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2409, - "type": "imguizmo", - "translation": [ - 2.304816, - 0.49194, - 6.280002 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2410, - "type": "imguizmo", - "translation": [ - 2.317685, - 0.549018, - 6.274715 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2411, - "type": "imguizmo", - "translation": [ - 2.330472, - 0.604567, - 6.268819 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2412, - "type": "imguizmo", - "translation": [ - 2.343167, - 0.65843, - 6.262317 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2413, - "type": "imguizmo", - "translation": [ - 2.355763, - 0.710458, - 6.255214 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2414, - "type": "imguizmo", - "translation": [ - 2.368251, - 0.760506, - 6.247515 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2415, - "type": "imguizmo", - "translation": [ - 2.380621, - 0.808435, - 6.239225 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2416, - "type": "imguizmo", - "translation": [ - 2.392865, - 0.85411, - 6.23035 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2417, - "type": "imguizmo", - "translation": [ - 2.404975, - 0.897404, - 6.220897 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2418, - "type": "imguizmo", - "translation": [ - 2.416942, - 0.938198, - 6.210872 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2419, - "type": "imguizmo", - "translation": [ - 2.428758, - 0.976376, - 6.200282 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2420, - "type": "imguizmo", - "translation": [ - 2.440414, - 1.011833, - 6.189133 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2421, - "type": "imguizmo", - "translation": [ - 2.451903, - 1.04447, - 6.177435 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2422, - "type": "imguizmo", - "translation": [ - 2.463216, - 1.074196, - 6.165195 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2423, - "type": "imguizmo", - "translation": [ - 2.474346, - 1.100928, - 6.152422 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2424, - "type": "imguizmo", - "translation": [ - 2.485285, - 1.124591, - 6.139125 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2425, - "type": "imguizmo", - "translation": [ - 2.496024, - 1.14512, - 6.125312 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2426, - "type": "imguizmo", - "translation": [ - 2.506558, - 1.162457, - 6.110994 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2427, - "type": "imguizmo", - "translation": [ - 2.516878, - 1.176554, - 6.09618 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2428, - "type": "imguizmo", - "translation": [ - 2.526977, - 1.187372, - 6.080881 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2429, - "type": "imguizmo", - "translation": [ - 2.536848, - 1.194881, - 6.065108 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2430, - "type": "imguizmo", - "translation": [ - 2.546484, - 1.199059, - 6.048871 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2431, - "type": "imguizmo", - "translation": [ - 2.555879, - 1.199895, - 6.032182 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2432, - "type": "imguizmo", - "translation": [ - 2.565026, - 1.197387, - 6.015052 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2433, - "type": "imguizmo", - "translation": [ - 2.573919, - 1.191542, - 5.997494 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2434, - "type": "imguizmo", - "translation": [ - 2.58255, - 1.182375, - 5.979519 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2435, - "type": "imguizmo", - "translation": [ - 2.590916, - 1.169913, - 5.961141 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2436, - "type": "imguizmo", - "translation": [ - 2.599009, - 1.154191, - 5.942371 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2437, - "type": "imguizmo", - "translation": [ - 2.606823, - 1.135251, - 5.923224 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2438, - "type": "imguizmo", - "translation": [ - 2.614355, - 1.113147, - 5.903711 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2439, - "type": "imguizmo", - "translation": [ - 2.621597, - 1.087941, - 5.883848 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2440, - "type": "imguizmo", - "translation": [ - 2.628546, - 1.059702, - 5.863647 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2441, - "type": "imguizmo", - "translation": [ - 2.635196, - 1.02851, - 5.843124 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2442, - "type": "imguizmo", - "translation": [ - 2.641543, - 0.994451, - 5.822291 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2443, - "type": "imguizmo", - "translation": [ - 2.647582, - 0.957621, - 5.801165 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2444, - "type": "imguizmo", - "translation": [ - 2.653309, - 0.918121, - 5.779758 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2445, - "type": "imguizmo", - "translation": [ - 2.65872, - 0.876062, - 5.758087 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2446, - "type": "imguizmo", - "translation": [ - 2.663811, - 0.831562, - 5.736167 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2447, - "type": "imguizmo", - "translation": [ - 2.66858, - 0.784744, - 5.714012 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2448, - "type": "imguizmo", - "translation": [ - 2.673021, - 0.735739, - 5.691638 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2449, - "type": "imguizmo", - "translation": [ - 2.677133, - 0.684683, - 5.669061 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2450, - "type": "imguizmo", - "translation": [ - 2.680913, - 0.631719, - 5.646297 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2451, - "type": "imguizmo", - "translation": [ - 2.684357, - 0.576994, - 5.623361 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2452, - "type": "imguizmo", - "translation": [ - 2.687464, - 0.52066, - 5.600269 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2453, - "type": "imguizmo", - "translation": [ - 2.690231, - 0.462876, - 5.577038 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2454, - "type": "imguizmo", - "translation": [ - 2.692656, - 0.403802, - 5.553683 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2455, - "type": "imguizmo", - "translation": [ - 2.694739, - 0.343602, - 5.530221 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2456, - "type": "imguizmo", - "translation": [ - 2.696476, - 0.282444, - 5.506668 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2457, - "type": "imguizmo", - "translation": [ - 2.697867, - 0.220499, - 5.483042 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2458, - "type": "imguizmo", - "translation": [ - 2.698911, - 0.15794, - 5.459357 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2459, - "type": "imguizmo", - "translation": [ - 2.699608, - 0.09494, - 5.435631 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2460, - "type": "imguizmo", - "translation": [ - 2.699956, - 0.031676, - 5.41188 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2461, - "type": "imguizmo", - "translation": [ - 2.699956, - -0.031676, - 5.38812 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2462, - "type": "imguizmo", - "translation": [ - 2.699608, - -0.09494, - 5.364369 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2463, - "type": "imguizmo", - "translation": [ - 2.698911, - -0.15794, - 5.340643 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2464, - "type": "imguizmo", - "translation": [ - 2.697867, - -0.220499, - 5.316958 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2465, - "type": "imguizmo", - "translation": [ - 2.696476, - -0.282444, - 5.293332 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2466, - "type": "imguizmo", - "translation": [ - 2.694739, - -0.343602, - 5.269779 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2467, - "type": "imguizmo", - "translation": [ - 2.692656, - -0.403802, - 5.246317 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2468, - "type": "imguizmo", - "translation": [ - 2.690231, - -0.462876, - 5.222962 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2469, - "type": "imguizmo", - "translation": [ - 2.687464, - -0.52066, - 5.199731 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2470, - "type": "imguizmo", - "translation": [ - 2.684357, - -0.576994, - 5.176639 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2471, - "type": "imguizmo", - "translation": [ - 2.680913, - -0.631719, - 5.153703 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2472, - "type": "imguizmo", - "translation": [ - 2.677133, - -0.684683, - 5.130939 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2473, - "type": "imguizmo", - "translation": [ - 2.673021, - -0.735739, - 5.108362 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2474, - "type": "imguizmo", - "translation": [ - 2.66858, - -0.784744, - 5.085988 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2475, - "type": "imguizmo", - "translation": [ - 2.663811, - -0.831562, - 5.063833 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2476, - "type": "imguizmo", - "translation": [ - 2.65872, - -0.876062, - 5.041913 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2477, - "type": "imguizmo", - "translation": [ - 2.653309, - -0.918121, - 5.020242 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2478, - "type": "imguizmo", - "translation": [ - 2.647582, - -0.957621, - 4.998835 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2479, - "type": "imguizmo", - "translation": [ - 2.641543, - -0.994451, - 4.977709 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2480, - "type": "imguizmo", - "translation": [ - 2.635196, - -1.02851, - 4.956876 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2481, - "type": "imguizmo", - "translation": [ - 2.628546, - -1.059702, - 4.936353 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2482, - "type": "imguizmo", - "translation": [ - 2.621597, - -1.087941, - 4.916152 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2483, - "type": "imguizmo", - "translation": [ - 2.614355, - -1.113147, - 4.896289 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2484, - "type": "imguizmo", - "translation": [ - 2.606823, - -1.135251, - 4.876776 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2485, - "type": "imguizmo", - "translation": [ - 2.599009, - -1.154191, - 4.857629 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2486, - "type": "imguizmo", - "translation": [ - 2.590916, - -1.169913, - 4.838859 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2487, - "type": "imguizmo", - "translation": [ - 2.58255, - -1.182375, - 4.820481 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2488, - "type": "imguizmo", - "translation": [ - 2.573919, - -1.191542, - 4.802506 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2489, - "type": "imguizmo", - "translation": [ - 2.565026, - -1.197387, - 4.784948 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2490, - "type": "imguizmo", - "translation": [ - 2.555879, - -1.199895, - 4.767818 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2491, - "type": "imguizmo", - "translation": [ - 2.546484, - -1.199059, - 4.751129 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2492, - "type": "imguizmo", - "translation": [ - 2.536848, - -1.194881, - 4.734892 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2493, - "type": "imguizmo", - "translation": [ - 2.526977, - -1.187372, - 4.719119 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2494, - "type": "imguizmo", - "translation": [ - 2.516878, - -1.176554, - 4.70382 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2495, - "type": "imguizmo", - "translation": [ - 2.506558, - -1.162457, - 4.689006 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2496, - "type": "imguizmo", - "translation": [ - 2.496024, - -1.14512, - 4.674688 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2497, - "type": "imguizmo", - "translation": [ - 2.485285, - -1.124591, - 4.660875 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2498, - "type": "imguizmo", - "translation": [ - 2.474346, - -1.100928, - 4.647578 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2499, - "type": "imguizmo", - "translation": [ - 2.463216, - -1.074196, - 4.634805 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2500, - "type": "imguizmo", - "translation": [ - 2.451903, - -1.04447, - 4.622565 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2501, - "type": "imguizmo", - "translation": [ - 2.440414, - -1.011833, - 4.610867 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2502, - "type": "imguizmo", - "translation": [ - 2.428758, - -0.976376, - 4.599718 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2503, - "type": "imguizmo", - "translation": [ - 2.416942, - -0.938198, - 4.589128 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2504, - "type": "imguizmo", - "translation": [ - 2.404975, - -0.897404, - 4.579103 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2505, - "type": "imguizmo", - "translation": [ - 2.392865, - -0.85411, - 4.56965 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2506, - "type": "imguizmo", - "translation": [ - 2.380621, - -0.808435, - 4.560775 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2507, - "type": "imguizmo", - "translation": [ - 2.368251, - -0.760506, - 4.552485 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2508, - "type": "imguizmo", - "translation": [ - 2.355763, - -0.710458, - 4.544786 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2509, - "type": "imguizmo", - "translation": [ - 2.343167, - -0.65843, - 4.537683 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2510, - "type": "imguizmo", - "translation": [ - 2.330472, - -0.604567, - 4.531181 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2511, - "type": "imguizmo", - "translation": [ - 2.317685, - -0.549018, - 4.525285 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2512, - "type": "imguizmo", - "translation": [ - 2.304816, - -0.49194, - 4.519998 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2513, - "type": "imguizmo", - "translation": [ - 2.291875, - -0.43349, - 4.515324 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2514, - "type": "imguizmo", - "translation": [ - 2.278869, - -0.373832, - 4.511267 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2515, - "type": "imguizmo", - "translation": [ - 2.265808, - -0.313132, - 4.507829 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2516, - "type": "imguizmo", - "translation": [ - 2.252702, - -0.251559, - 4.505013 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2517, - "type": "imguizmo", - "translation": [ - 2.239559, - -0.189286, - 4.502821 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2518, - "type": "imguizmo", - "translation": [ - 2.226388, - -0.126484, - 4.501254 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2519, - "type": "imguizmo", - "translation": [ - 2.213198, - -0.06333, - 4.500314 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2520, - "type": "imguizmo", - "translation": [ - 2.2, - 0, - 4.5 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2521, - "type": "imguizmo", - "translation": [ - 2.186802, - 0.06333, - 4.500314 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2522, - "type": "imguizmo", - "translation": [ - 2.173612, - 0.126484, - 4.501254 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2523, - "type": "imguizmo", - "translation": [ - 2.160441, - 0.189286, - 4.502821 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2524, - "type": "imguizmo", - "translation": [ - 2.147298, - 0.251559, - 4.505013 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2525, - "type": "imguizmo", - "translation": [ - 2.134192, - 0.313132, - 4.507829 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2526, - "type": "imguizmo", - "translation": [ - 2.121131, - 0.373832, - 4.511267 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2527, - "type": "imguizmo", - "translation": [ - 2.108125, - 0.43349, - 4.515324 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2528, - "type": "imguizmo", - "translation": [ - 2.095184, - 0.49194, - 4.519998 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2529, - "type": "imguizmo", - "translation": [ - 2.082315, - 0.549018, - 4.525285 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2530, - "type": "imguizmo", - "translation": [ - 2.069528, - 0.604567, - 4.531181 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2531, - "type": "imguizmo", - "translation": [ - 2.056833, - 0.65843, - 4.537683 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2532, - "type": "imguizmo", - "translation": [ - 2.044237, - 0.710458, - 4.544786 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2533, - "type": "imguizmo", - "translation": [ - 2.031749, - 0.760506, - 4.552485 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2534, - "type": "imguizmo", - "translation": [ - 2.019379, - 0.808435, - 4.560775 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2535, - "type": "imguizmo", - "translation": [ - 2.007135, - 0.85411, - 4.56965 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2536, - "type": "imguizmo", - "translation": [ - 1.995025, - 0.897404, - 4.579103 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2537, - "type": "imguizmo", - "translation": [ - 1.983058, - 0.938198, - 4.589128 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2538, - "type": "imguizmo", - "translation": [ - 1.971242, - 0.976376, - 4.599718 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2539, - "type": "imguizmo", - "translation": [ - 1.959586, - 1.011833, - 4.610867 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2540, - "type": "imguizmo", - "translation": [ - 1.948097, - 1.04447, - 4.622565 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2541, - "type": "imguizmo", - "translation": [ - 1.936784, - 1.074196, - 4.634805 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2542, - "type": "imguizmo", - "translation": [ - 1.925654, - 1.100928, - 4.647578 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2543, - "type": "imguizmo", - "translation": [ - 1.914715, - 1.124591, - 4.660875 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2544, - "type": "imguizmo", - "translation": [ - 1.903976, - 1.14512, - 4.674688 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2545, - "type": "imguizmo", - "translation": [ - 1.893442, - 1.162457, - 4.689006 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2546, - "type": "imguizmo", - "translation": [ - 1.883122, - 1.176554, - 4.70382 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2547, - "type": "imguizmo", - "translation": [ - 1.873023, - 1.187372, - 4.719119 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2548, - "type": "imguizmo", - "translation": [ - 1.863152, - 1.194881, - 4.734892 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2549, - "type": "imguizmo", - "translation": [ - 1.853516, - 1.199059, - 4.751129 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2550, - "type": "imguizmo", - "translation": [ - 1.844121, - 1.199895, - 4.767818 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2551, - "type": "imguizmo", - "translation": [ - 1.834974, - 1.197387, - 4.784948 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2552, - "type": "imguizmo", - "translation": [ - 1.826081, - 1.191542, - 4.802506 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2553, - "type": "imguizmo", - "translation": [ - 1.81745, - 1.182375, - 4.820481 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2554, - "type": "imguizmo", - "translation": [ - 1.809084, - 1.169913, - 4.838859 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2555, - "type": "imguizmo", - "translation": [ - 1.800991, - 1.154191, - 4.857629 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2556, - "type": "imguizmo", - "translation": [ - 1.793177, - 1.135251, - 4.876776 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2557, - "type": "imguizmo", - "translation": [ - 1.785645, - 1.113147, - 4.896289 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2558, - "type": "imguizmo", - "translation": [ - 1.778403, - 1.087941, - 4.916152 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2559, - "type": "imguizmo", - "translation": [ - 1.771454, - 1.059702, - 4.936353 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2560, - "type": "imguizmo", - "translation": [ - 1.764804, - 1.02851, - 4.956876 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2561, - "type": "imguizmo", - "translation": [ - 1.758457, - 0.994451, - 4.977709 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2562, - "type": "imguizmo", - "translation": [ - 1.752418, - 0.957621, - 4.998835 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2563, - "type": "imguizmo", - "translation": [ - 1.746691, - 0.918121, - 5.020242 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2564, - "type": "imguizmo", - "translation": [ - 1.74128, - 0.876062, - 5.041913 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2565, - "type": "imguizmo", - "translation": [ - 1.736189, - 0.831562, - 5.063833 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2566, - "type": "imguizmo", - "translation": [ - 1.73142, - 0.784744, - 5.085988 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2567, - "type": "imguizmo", - "translation": [ - 1.726979, - 0.735739, - 5.108362 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2568, - "type": "imguizmo", - "translation": [ - 1.722867, - 0.684683, - 5.130939 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2569, - "type": "imguizmo", - "translation": [ - 1.719087, - 0.631719, - 5.153703 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2570, - "type": "imguizmo", - "translation": [ - 1.715643, - 0.576994, - 5.176639 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2571, - "type": "imguizmo", - "translation": [ - 1.712536, - 0.52066, - 5.199731 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2572, - "type": "imguizmo", - "translation": [ - 1.709769, - 0.462876, - 5.222962 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2573, - "type": "imguizmo", - "translation": [ - 1.707344, - 0.403802, - 5.246317 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2574, - "type": "imguizmo", - "translation": [ - 1.705261, - 0.343602, - 5.269779 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2575, - "type": "imguizmo", - "translation": [ - 1.703524, - 0.282444, - 5.293332 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2576, - "type": "imguizmo", - "translation": [ - 1.702133, - 0.220499, - 5.316958 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2577, - "type": "imguizmo", - "translation": [ - 1.701089, - 0.15794, - 5.340643 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2578, - "type": "imguizmo", - "translation": [ - 1.700392, - 0.09494, - 5.364369 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2579, - "type": "imguizmo", - "translation": [ - 1.700044, - 0.031676, - 5.38812 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2580, - "type": "imguizmo", - "translation": [ - 1.700044, - -0.031676, - 5.41188 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2581, - "type": "imguizmo", - "translation": [ - 1.700392, - -0.09494, - 5.435631 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2582, - "type": "imguizmo", - "translation": [ - 1.701089, - -0.15794, - 5.459357 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2583, - "type": "imguizmo", - "translation": [ - 1.702133, - -0.220499, - 5.483042 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2584, - "type": "imguizmo", - "translation": [ - 1.703524, - -0.282444, - 5.506668 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2585, - "type": "imguizmo", - "translation": [ - 1.705261, - -0.343602, - 5.530221 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2586, - "type": "imguizmo", - "translation": [ - 1.707344, - -0.403802, - 5.553683 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2587, - "type": "imguizmo", - "translation": [ - 1.709769, - -0.462876, - 5.577038 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2588, - "type": "imguizmo", - "translation": [ - 1.712536, - -0.52066, - 5.600269 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2589, - "type": "imguizmo", - "translation": [ - 1.715643, - -0.576994, - 5.623361 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2590, - "type": "imguizmo", - "translation": [ - 1.719087, - -0.631719, - 5.646297 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2591, - "type": "imguizmo", - "translation": [ - 1.722867, - -0.684683, - 5.669061 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2592, - "type": "imguizmo", - "translation": [ - 1.726979, - -0.735739, - 5.691638 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2593, - "type": "imguizmo", - "translation": [ - 1.73142, - -0.784744, - 5.714012 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2594, - "type": "imguizmo", - "translation": [ - 1.736189, - -0.831562, - 5.736167 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2595, - "type": "imguizmo", - "translation": [ - 1.74128, - -0.876062, - 5.758087 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2596, - "type": "imguizmo", - "translation": [ - 1.746691, - -0.918121, - 5.779758 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2597, - "type": "imguizmo", - "translation": [ - 1.752418, - -0.957621, - 5.801165 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2598, - "type": "imguizmo", - "translation": [ - 1.758457, - -0.994451, - 5.822291 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2599, - "type": "imguizmo", - "translation": [ - 1.764804, - -1.02851, - 5.843124 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2600, - "type": "imguizmo", - "translation": [ - 1.771454, - -1.059702, - 5.863647 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2601, - "type": "imguizmo", - "translation": [ - 1.778403, - -1.087941, - 5.883848 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2602, - "type": "imguizmo", - "translation": [ - 1.785645, - -1.113147, - 5.903711 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2603, - "type": "imguizmo", - "translation": [ - 1.793177, - -1.135251, - 5.923224 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2604, - "type": "imguizmo", - "translation": [ - 1.800991, - -1.154191, - 5.942371 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2605, - "type": "imguizmo", - "translation": [ - 1.809084, - -1.169913, - 5.961141 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2606, - "type": "imguizmo", - "translation": [ - 1.81745, - -1.182375, - 5.979519 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2607, - "type": "imguizmo", - "translation": [ - 1.826081, - -1.191542, - 5.997494 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2608, - "type": "imguizmo", - "translation": [ - 1.834974, - -1.197387, - 6.015052 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2609, - "type": "imguizmo", - "translation": [ - 1.844121, - -1.199895, - 6.032182 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2610, - "type": "imguizmo", - "translation": [ - 1.853516, - -1.199059, - 6.048871 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2611, - "type": "imguizmo", - "translation": [ - 1.863152, - -1.194881, - 6.065108 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2612, - "type": "imguizmo", - "translation": [ - 1.873023, - -1.187372, - 6.080881 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2613, - "type": "imguizmo", - "translation": [ - 1.883122, - -1.176554, - 6.09618 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2614, - "type": "imguizmo", - "translation": [ - 1.893442, - -1.162457, - 6.110994 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2615, - "type": "imguizmo", - "translation": [ - 1.903976, - -1.14512, - 6.125312 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2616, - "type": "imguizmo", - "translation": [ - 1.914715, - -1.124591, - 6.139125 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2617, - "type": "imguizmo", - "translation": [ - 1.925654, - -1.100928, - 6.152422 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2618, - "type": "imguizmo", - "translation": [ - 1.936784, - -1.074196, - 6.165195 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2619, - "type": "imguizmo", - "translation": [ - 1.948097, - -1.04447, - 6.177435 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2620, - "type": "imguizmo", - "translation": [ - 1.959586, - -1.011833, - 6.189133 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2621, - "type": "imguizmo", - "translation": [ - 1.971242, - -0.976376, - 6.200282 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2622, - "type": "imguizmo", - "translation": [ - 1.983058, - -0.938198, - 6.210872 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2623, - "type": "imguizmo", - "translation": [ - 1.995025, - -0.897404, - 6.220897 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2624, - "type": "imguizmo", - "translation": [ - 2.007135, - -0.85411, - 6.23035 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2625, - "type": "imguizmo", - "translation": [ - 2.019379, - -0.808435, - 6.239225 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2626, - "type": "imguizmo", - "translation": [ - 2.031749, - -0.760506, - 6.247515 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2627, - "type": "imguizmo", - "translation": [ - 2.044237, - -0.710458, - 6.255214 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2628, - "type": "imguizmo", - "translation": [ - 2.056833, - -0.65843, - 6.262317 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2629, - "type": "imguizmo", - "translation": [ - 2.069528, - -0.604567, - 6.268819 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2630, - "type": "imguizmo", - "translation": [ - 2.082315, - -0.549018, - 6.274715 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2631, - "type": "imguizmo", - "translation": [ - 2.095184, - -0.49194, - 6.280002 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2632, - "type": "imguizmo", - "translation": [ - 2.108125, - -0.43349, - 6.284676 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2633, - "type": "imguizmo", - "translation": [ - 2.121131, - -0.373832, - 6.288733 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2634, - "type": "imguizmo", - "translation": [ - 2.134192, - -0.313132, - 6.292171 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2635, - "type": "imguizmo", - "translation": [ - 2.147298, - -0.251559, - 6.294987 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2636, - "type": "imguizmo", - "translation": [ - 2.160441, - -0.189286, - 6.297179 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2637, - "type": "imguizmo", - "translation": [ - 2.173612, - -0.126484, - 6.298746 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2638, - "type": "imguizmo", - "translation": [ - 2.186802, - -0.06333, - 6.299686 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - }, - { - "frame": 2639, - "type": "imguizmo", - "translation": [ - 2.2, - 0, - 6.3 - ], - "rotation_deg": [ - 0, - 0, - 0 - ], - "scale": [ - 1, - 1, - 1 - ] - } - ], - "checks": [ - { - "frame": 0, - "kind": "baseline" - }, - { - "frame": 1, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 3, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 4, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 5, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 6, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 7, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 8, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 9, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 10, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 11, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 12, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 13, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 14, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 15, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 16, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 17, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 18, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 19, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 20, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 21, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 22, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 23, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 24, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 25, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 26, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 27, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 28, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 29, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 30, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 31, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 32, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 33, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 34, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 35, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 36, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 37, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 38, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 39, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 40, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 41, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 42, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 43, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 44, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 45, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 46, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 47, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 48, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 49, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 50, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 51, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 52, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 53, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 54, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 55, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 56, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 57, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 58, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 59, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 60, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 61, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 62, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 63, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 64, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 65, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 66, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 67, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 68, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 69, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 70, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 71, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 72, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 73, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 74, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 75, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 76, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 77, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 78, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 79, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 80, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 81, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 82, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 83, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 84, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 85, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 86, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 87, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 88, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 89, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 90, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 91, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 92, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 93, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 94, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 95, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 96, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 97, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 98, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 99, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 100, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 101, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 102, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 103, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 104, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 105, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 106, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 107, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 108, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 109, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 110, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 111, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 112, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 113, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 114, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 115, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 116, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 117, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 118, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 119, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 120, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 121, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 122, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 123, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 124, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 125, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 126, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 127, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 128, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 129, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 130, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 131, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 132, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 133, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 134, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 135, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 136, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 137, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 138, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 139, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 140, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 141, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 142, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 143, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 144, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 145, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 146, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 147, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 148, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 149, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 150, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 151, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 152, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 153, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 154, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 155, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 156, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 157, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 158, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 159, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 160, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 161, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 162, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 163, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 164, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 165, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 166, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 167, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 168, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 169, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 170, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 171, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 172, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 173, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 174, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 175, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 176, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 177, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 178, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 179, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 180, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 181, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 182, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 183, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 184, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 185, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 186, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 187, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 188, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 189, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 190, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 191, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 192, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 193, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 194, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 195, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 196, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 197, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 198, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 199, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 200, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 201, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 202, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 203, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 204, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 205, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 206, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 207, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 208, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 209, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 210, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 211, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 212, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 213, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 214, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 215, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 216, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 217, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 218, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 219, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 220, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 221, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 222, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 223, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 224, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 225, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 226, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 227, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 228, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 229, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 230, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 231, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 232, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 233, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 234, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 235, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 236, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 237, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 238, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 239, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 240, - "kind": "baseline" - }, - { - "frame": 241, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 242, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 243, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 244, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 245, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 246, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 247, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 248, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 249, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 250, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 251, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 252, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 253, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 254, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 255, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 256, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 257, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 258, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 259, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 260, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 261, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 262, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 263, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 264, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 265, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 266, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 267, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 268, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 269, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 270, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 271, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 272, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 273, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 274, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 275, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 276, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 277, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 278, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 279, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 280, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 281, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 282, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 283, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 284, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 285, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 286, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 287, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 288, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 289, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 290, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 291, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 292, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 293, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 294, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 295, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 296, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 297, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 298, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 299, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 300, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 301, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 302, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 303, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 304, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 305, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 306, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 307, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 308, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 309, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 310, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 311, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 312, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 313, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 314, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 315, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 316, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 317, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 318, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 319, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 320, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 321, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 322, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 323, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 324, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 325, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 326, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 327, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 328, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 329, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 330, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 331, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 332, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 333, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 334, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 335, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 336, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 337, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 338, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 339, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 340, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 341, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 342, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 343, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 344, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 345, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 346, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 347, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 348, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 349, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 350, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 351, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 352, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 353, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 354, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 355, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 356, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 357, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 358, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 359, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 360, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 361, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 362, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 363, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 364, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 365, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 366, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 367, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 368, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 369, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 370, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 371, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 372, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 373, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 374, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 375, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 376, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 377, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 378, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 379, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 380, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 381, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 382, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 383, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 384, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 385, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 386, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 387, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 388, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 389, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 390, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 391, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 392, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 393, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 394, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 395, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 396, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 397, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 398, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 399, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 400, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 401, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 402, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 403, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 404, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 405, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 406, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 407, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 408, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 409, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 410, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 411, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 412, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 413, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 414, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 415, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 416, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 417, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 418, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 419, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 420, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 421, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 422, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 423, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 424, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 425, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 426, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 427, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 428, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 429, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 430, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 431, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 432, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 433, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 434, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 435, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 436, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 437, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 438, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 439, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 440, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 441, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 442, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 443, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 444, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 445, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 446, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 447, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 448, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 449, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 450, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 451, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 452, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 453, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 454, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 455, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 456, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 457, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 458, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 459, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 460, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 461, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 462, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 463, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 464, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 465, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 466, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 467, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 468, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 469, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 470, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 471, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 472, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 473, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 474, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 475, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 476, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 477, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 478, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 479, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 480, - "kind": "baseline" - }, - { - "frame": 481, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 482, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 483, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 484, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 485, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 486, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 487, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 488, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 489, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 490, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 491, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 492, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 493, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 494, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 495, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 496, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 497, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 498, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 499, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 500, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 501, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 502, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 503, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 504, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 505, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 506, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 507, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 508, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 509, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 510, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 511, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 512, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 513, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 514, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 515, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 516, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 517, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 518, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 519, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 520, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 521, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 522, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 523, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 524, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 525, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 526, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 527, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 528, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 529, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 530, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 531, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 532, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 533, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 534, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 535, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 536, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 537, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 538, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 539, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 540, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 541, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 542, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 543, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 544, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 545, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 546, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 547, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 548, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 549, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 550, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 551, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 552, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 553, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 554, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 555, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 556, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 557, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 558, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 559, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 560, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 561, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 562, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 563, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 564, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 565, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 566, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 567, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 568, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 569, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 570, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 571, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 572, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 573, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 574, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 575, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 576, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 577, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 578, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 579, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 580, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 581, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 582, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 583, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 584, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 585, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 586, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 587, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 588, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 589, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 590, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 591, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 592, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 593, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 594, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 595, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 596, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 597, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 598, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 599, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 600, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 601, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 602, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 603, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 604, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 605, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 606, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 607, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 608, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 609, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 610, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 611, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 612, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 613, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 614, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 615, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 616, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 617, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 618, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 619, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 620, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 621, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 622, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 623, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 624, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 625, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 626, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 627, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 628, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 629, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 630, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 631, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 632, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 633, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 634, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 635, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 636, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 637, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 638, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 639, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 640, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 641, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 642, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 643, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 644, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 645, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 646, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 647, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 648, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 649, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 650, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 651, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 652, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 653, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 654, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 655, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 656, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 657, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 658, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 659, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 660, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 661, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 662, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 663, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 664, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 665, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 666, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 667, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 668, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 669, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 670, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 671, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 672, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 673, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 674, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 675, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 676, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 677, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 678, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 679, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 680, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 681, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 682, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 683, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 684, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 685, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 686, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 687, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 688, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 689, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 690, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 691, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 692, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 693, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 694, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 695, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 696, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 697, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 698, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 699, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 700, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 701, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 702, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 703, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 704, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 705, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 706, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 707, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 708, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 709, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 710, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 711, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 712, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 713, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 714, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 715, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 716, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 717, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 718, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 719, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 720, - "kind": "baseline" - }, - { - "frame": 721, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 722, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 723, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 724, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 725, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 726, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 727, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 728, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 729, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 730, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 731, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 732, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 733, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 734, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 735, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 736, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 737, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 738, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 739, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 740, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 741, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 742, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 743, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 744, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 745, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 746, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 747, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 748, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 749, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 750, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 751, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 752, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 753, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 754, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 755, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 756, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 757, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 758, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 759, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 760, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 761, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 762, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 763, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 764, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 765, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 766, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 767, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 768, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 769, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 770, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 771, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 772, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 773, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 774, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 775, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 776, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 777, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 778, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 779, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 780, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 781, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 782, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 783, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 784, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 785, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 786, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 787, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 788, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 789, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 790, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 791, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 792, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 793, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 794, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 795, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 796, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 797, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 798, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 799, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 800, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 801, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 802, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 803, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 804, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 805, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 806, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 807, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 808, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 809, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 810, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 811, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 812, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 813, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 814, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 815, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 816, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 817, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 818, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 819, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 820, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 821, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 822, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 823, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 824, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 825, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 826, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 827, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 828, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 829, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 830, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 831, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 832, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 833, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 834, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 835, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 836, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 837, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 838, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 839, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 840, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 841, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 842, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 843, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 844, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 845, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 846, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 847, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 848, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 849, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 850, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 851, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 852, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 853, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 854, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 855, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 856, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 857, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 858, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 859, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 860, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 861, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 862, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 863, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 864, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 865, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 866, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 867, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 868, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 869, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 870, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 871, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 872, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 873, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 874, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 875, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 876, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 877, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 878, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 879, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 880, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 881, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 882, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 883, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 884, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 885, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 886, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 887, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 888, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 889, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 890, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 891, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 892, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 893, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 894, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 895, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 896, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 897, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 898, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 899, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 900, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 901, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 902, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 903, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 904, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 905, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 906, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 907, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 908, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 909, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 910, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 911, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 912, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 913, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 914, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 915, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 916, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 917, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 918, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 919, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 920, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 921, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 922, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 923, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 924, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 925, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 926, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 927, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 928, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 929, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 930, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 931, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 932, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 933, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 934, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 935, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 936, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 937, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 938, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 939, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 940, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 941, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 942, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 943, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 944, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 945, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 946, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 947, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 948, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 949, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 950, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 951, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 952, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 953, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 954, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 955, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 956, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 957, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 958, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 959, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 960, - "kind": "baseline" - }, - { - "frame": 961, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 962, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 963, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 964, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 965, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 966, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 967, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 968, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 969, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 970, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 971, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 972, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 973, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 974, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 975, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 976, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 977, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 978, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 979, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 980, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 981, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 982, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 983, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 984, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 985, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 986, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 987, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 988, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 989, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 990, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 991, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 992, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 993, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 994, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 995, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 996, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 997, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 998, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 999, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1000, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1001, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1002, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1003, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1004, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1005, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1006, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1007, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1008, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1009, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1010, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1011, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1012, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1013, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1014, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1015, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1016, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1017, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1018, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1019, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1020, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1021, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1022, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1023, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1024, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1025, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1026, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1027, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1028, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1029, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1030, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1031, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1032, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1033, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1034, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1035, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1036, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1037, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1038, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1039, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1040, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1041, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1042, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1043, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1044, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1045, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1046, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1047, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1048, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1049, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1050, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1051, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1052, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1053, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1054, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1055, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1056, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1057, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1058, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1059, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1060, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1061, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1062, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1063, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1064, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1065, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1066, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1067, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1068, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1069, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1070, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1071, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1072, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1073, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1074, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1075, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1076, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1077, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1078, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1079, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1080, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1081, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1082, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1083, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1084, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1085, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1086, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1087, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1088, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1089, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1090, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1091, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1092, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1093, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1094, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1095, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1096, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1097, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1098, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1099, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1100, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1101, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1102, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1103, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1104, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1105, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1106, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1107, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1108, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1109, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1110, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1111, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1112, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1113, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1114, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1115, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1116, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1117, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1118, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1119, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1120, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1121, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1122, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1123, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1124, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1125, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1126, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1127, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1128, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1129, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1130, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1131, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1132, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1133, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1134, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1135, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1136, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1137, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1138, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1139, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1140, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1141, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1142, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1143, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1144, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1145, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1146, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1147, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1148, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1149, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1150, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1151, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1152, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1153, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1154, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1155, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1156, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1157, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1158, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1159, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1160, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1161, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1162, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1163, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1164, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1165, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1166, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1167, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1168, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1169, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1170, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1171, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1172, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1173, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1174, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1175, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1176, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1177, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1178, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1179, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1180, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1181, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1182, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1183, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1184, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1185, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1186, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1187, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1188, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1189, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1190, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1191, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1192, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1193, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1194, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1195, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1196, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1197, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1198, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1199, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1200, - "kind": "baseline" - }, - { - "frame": 1201, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1202, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1203, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1204, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1205, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1206, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1207, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1208, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1209, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1210, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1211, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1212, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1213, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1214, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1215, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1216, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1217, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1218, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1219, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1220, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1221, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1222, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1223, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1224, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1225, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1226, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1227, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1228, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1229, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1230, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1231, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1232, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1233, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1234, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1235, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1236, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1237, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1238, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1239, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1240, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1241, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1242, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1243, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1244, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1245, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1246, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1247, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1248, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1249, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1250, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1251, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1252, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1253, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1254, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1255, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1256, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1257, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1258, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1259, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1260, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1261, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1262, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1263, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1264, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1265, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1266, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1267, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1268, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1269, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1270, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1271, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1272, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1273, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1274, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1275, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1276, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1277, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1278, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1279, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1280, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1281, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1282, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1283, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1284, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1285, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1286, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1287, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1288, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1289, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1290, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1291, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1292, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1293, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1294, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1295, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1296, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1297, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1298, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1299, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1300, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1301, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1302, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1303, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1304, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1305, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1306, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1307, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1308, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1309, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1310, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1311, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1312, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1313, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1314, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1315, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1316, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1317, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1318, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1319, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1320, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1321, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1322, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1323, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1324, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1325, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1326, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1327, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1328, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1329, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1330, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1331, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1332, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1333, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1334, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1335, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1336, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1337, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1338, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1339, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1340, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1341, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1342, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1343, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1344, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1345, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1346, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1347, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1348, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1349, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1350, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1351, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1352, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1353, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1354, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1355, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1356, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1357, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1358, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1359, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1360, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1361, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1362, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1363, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1364, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1365, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1366, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1367, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1368, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1369, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1370, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1371, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1372, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1373, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1374, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1375, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1376, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1377, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1378, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1379, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1380, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1381, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1382, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1383, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1384, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1385, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1386, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1387, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1388, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1389, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1390, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1391, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1392, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1393, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1394, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1395, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1396, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1397, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1398, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1399, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1400, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1401, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1402, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1403, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1404, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1405, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1406, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1407, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1408, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1409, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1410, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1411, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1412, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1413, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1414, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1415, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1416, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1417, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1418, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1419, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1420, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1421, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1422, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1423, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1424, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1425, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1426, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1427, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1428, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1429, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1430, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1431, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1432, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1433, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1434, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1435, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1436, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1437, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1438, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1439, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1440, - "kind": "baseline" - }, - { - "frame": 1441, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1442, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1443, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1444, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1445, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1446, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1447, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1448, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1449, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1450, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1451, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1452, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1453, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1454, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1455, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1456, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1457, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1458, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1459, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1460, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1461, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1462, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1463, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1464, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1465, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1466, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1467, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1468, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1469, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1470, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1471, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1472, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1473, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1474, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1475, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1476, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1477, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1478, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1479, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1480, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1481, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1482, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1483, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1484, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1485, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1486, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1487, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1488, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1489, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1490, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1491, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1492, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1493, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1494, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1495, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1496, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1497, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1498, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1499, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1500, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1501, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1502, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1503, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1504, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1505, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1506, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1507, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1508, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1509, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1510, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1511, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1512, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1513, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1514, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1515, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1516, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1517, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1518, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1519, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1520, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1521, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1522, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1523, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1524, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1525, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1526, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1527, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1528, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1529, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1530, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1531, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1532, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1533, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1534, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1535, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1536, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1537, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1538, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1539, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1540, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1541, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1542, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1543, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1544, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1545, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1546, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1547, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1548, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1549, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1550, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1551, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1552, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1553, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1554, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1555, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1556, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1557, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1558, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1559, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1560, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1561, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1562, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1563, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1564, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1565, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1566, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1567, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1568, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1569, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1570, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1571, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1572, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1573, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1574, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1575, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1576, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1577, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1578, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1579, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1580, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1581, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1582, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1583, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1584, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1585, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1586, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1587, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1588, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1589, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1590, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1591, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1592, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1593, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1594, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1595, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1596, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1597, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1598, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1599, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1600, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1601, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1602, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1603, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1604, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1605, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1606, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1607, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1608, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1609, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1610, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1611, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1612, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1613, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1614, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1615, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1616, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1617, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1618, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1619, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1620, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1621, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1622, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1623, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1624, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1625, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1626, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1627, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1628, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1629, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1630, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1631, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1632, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1633, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1634, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1635, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1636, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1637, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1638, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1639, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1640, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1641, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1642, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1643, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1644, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1645, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1646, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1647, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1648, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1649, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1650, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1651, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1652, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1653, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1654, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1655, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1656, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1657, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1658, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1659, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1660, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1661, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1662, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1663, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1664, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1665, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1666, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1667, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1668, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1669, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1670, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1671, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1672, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1673, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1674, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1675, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1676, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1677, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1678, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1679, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1680, - "kind": "baseline" - }, - { - "frame": 1681, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1682, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1683, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1684, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1685, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1686, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1687, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1688, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1689, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1690, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1691, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1692, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1693, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1694, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1695, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1696, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1697, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1698, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1699, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1700, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1701, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1702, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1703, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1704, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1705, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1706, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1707, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1708, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1709, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1710, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1711, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1712, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1713, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1714, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1715, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1716, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1717, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1718, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1719, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1720, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1721, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1722, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1723, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1724, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1725, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1726, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1727, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1728, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1729, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1730, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1731, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1732, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1733, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1734, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1735, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1736, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1737, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1738, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1739, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1740, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1741, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1742, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1743, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1744, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1745, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1746, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1747, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1748, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1749, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1750, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1751, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1752, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1753, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1754, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1755, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1756, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1757, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1758, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1759, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1760, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1761, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1762, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1763, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1764, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1765, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1766, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1767, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1768, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1769, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1770, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1771, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1772, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1773, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1774, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1775, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1776, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1777, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1778, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1779, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1780, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1781, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1782, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1783, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1784, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1785, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1786, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1787, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1788, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1789, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1790, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1791, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1792, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1793, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1794, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1795, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1796, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1797, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1798, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1799, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1800, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1801, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1802, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1803, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1804, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1805, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1806, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1807, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1808, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1809, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1810, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1811, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1812, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1813, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1814, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1815, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1816, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1817, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1818, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1819, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1820, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1821, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1822, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1823, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1824, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1825, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1826, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1827, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1828, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1829, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1830, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1831, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1832, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1833, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1834, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1835, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1836, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1837, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1838, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1839, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1840, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1841, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1842, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1843, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1844, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1845, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1846, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1847, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1848, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1849, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1850, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1851, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1852, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1853, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1854, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1855, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1856, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1857, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1858, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1859, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1860, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1861, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1862, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1863, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1864, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1865, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1866, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1867, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1868, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1869, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1870, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1871, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1872, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1873, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1874, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1875, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1876, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1877, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1878, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1879, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1880, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1881, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1882, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1883, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1884, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1885, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1886, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1887, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1888, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1889, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1890, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1891, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1892, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1893, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1894, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1895, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1896, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1897, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1898, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1899, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1900, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1901, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1902, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1903, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1904, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1905, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1906, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1907, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1908, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1909, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1910, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1911, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1912, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1913, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1914, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1915, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1916, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1917, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1918, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1919, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1920, - "kind": "baseline" - }, - { - "frame": 1921, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1922, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1923, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1924, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1925, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1926, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1927, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1928, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1929, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1930, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1931, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1932, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1933, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1934, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1935, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1936, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1937, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1938, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1939, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1940, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1941, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1942, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1943, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1944, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1945, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1946, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1947, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1948, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1949, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1950, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1951, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1952, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1953, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1954, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1955, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1956, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1957, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1958, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1959, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1960, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1961, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1962, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1963, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1964, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1965, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1966, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1967, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1968, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1969, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1970, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1971, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1972, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1973, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1974, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1975, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1976, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1977, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1978, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1979, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1980, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1981, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1982, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1983, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1984, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1985, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1986, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1987, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1988, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1989, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1990, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1991, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1992, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1993, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1994, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1995, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1996, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1997, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1998, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 1999, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2000, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2001, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2002, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2003, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2004, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2005, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2006, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2007, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2008, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2009, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2010, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2011, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2012, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2013, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2014, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2015, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2016, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2017, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2018, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2019, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2020, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2021, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2022, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2023, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2024, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2025, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2026, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2027, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2028, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2029, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2030, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2031, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2032, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2033, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2034, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2035, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2036, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2037, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2038, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2039, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2040, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2041, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2042, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2043, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2044, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2045, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2046, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2047, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2048, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2049, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2050, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2051, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2052, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2053, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2054, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2055, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2056, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2057, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2058, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2059, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2060, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2061, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2062, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2063, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2064, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2065, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2066, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2067, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2068, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2069, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2070, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2071, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2072, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2073, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2074, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2075, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2076, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2077, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2078, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2079, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2080, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2081, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2082, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2083, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2084, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2085, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2086, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2087, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2088, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2089, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2090, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2091, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2092, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2093, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2094, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2095, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2096, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2097, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2098, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2099, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2100, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2101, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2102, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2103, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2104, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2105, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2106, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2107, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2108, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2109, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2110, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2111, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2112, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2113, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2114, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2115, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2116, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2117, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2118, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2119, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2120, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2121, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2122, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2123, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2124, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2125, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2126, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2127, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2128, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2129, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2130, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2131, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2132, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2133, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2134, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2135, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2136, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2137, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2138, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2139, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2140, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2141, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2142, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2143, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2144, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2145, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2146, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2147, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2148, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2149, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2150, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2151, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2152, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2153, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2154, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2155, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2156, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2157, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2158, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2159, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2160, - "kind": "baseline" - }, - { - "frame": 2161, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2162, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2163, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2164, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2165, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2166, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2167, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2168, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2169, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2170, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2171, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2172, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2173, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2174, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2175, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2176, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2177, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2178, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2179, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2180, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2181, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2182, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2183, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2184, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2185, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2186, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2187, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2188, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2189, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2190, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2191, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2192, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2193, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2194, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2195, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2196, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2197, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2198, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2199, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2200, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2201, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2202, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2203, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2204, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2205, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2206, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2207, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2208, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2209, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2210, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2211, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2212, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2213, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2214, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2215, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2216, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2217, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2218, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2219, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2220, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2221, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2222, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2223, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2224, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2225, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2226, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2227, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2228, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2229, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2230, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2231, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2232, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2233, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2234, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2235, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2236, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2237, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2238, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2239, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2240, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2241, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2242, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2243, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2244, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2245, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2246, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2247, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2248, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2249, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2250, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2251, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2252, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2253, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2254, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2255, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2256, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2257, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2258, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2259, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2260, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2261, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2262, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2263, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2264, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2265, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2266, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2267, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2268, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2269, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2270, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2271, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2272, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2273, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2274, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2275, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2276, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2277, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2278, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2279, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2280, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2281, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2282, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2283, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2284, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2285, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2286, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2287, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2288, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2289, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2290, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2291, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2292, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2293, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2294, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2295, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2296, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2297, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2298, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2299, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2300, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2301, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2302, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2303, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2304, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2305, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2306, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2307, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2308, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2309, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2310, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2311, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2312, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2313, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2314, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2315, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2316, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2317, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2318, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2319, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2320, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2321, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2322, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2323, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2324, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2325, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2326, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2327, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2328, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2329, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2330, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2331, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2332, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2333, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2334, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2335, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2336, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2337, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2338, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2339, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2340, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2341, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2342, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2343, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2344, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2345, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2346, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2347, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2348, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2349, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2350, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2351, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2352, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2353, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2354, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2355, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2356, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2357, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2358, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2359, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2360, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2361, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2362, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2363, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2364, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2365, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2366, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2367, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2368, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2369, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2370, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2371, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2372, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2373, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2374, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2375, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2376, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2377, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2378, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2379, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2380, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2381, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2382, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2383, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2384, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2385, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2386, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2387, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2388, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2389, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2390, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2391, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2392, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2393, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2394, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2395, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2396, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2397, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2398, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2399, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2400, - "kind": "baseline" - }, - { - "frame": 2401, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2402, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2403, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2404, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2405, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2406, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2407, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2408, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2409, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2410, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2411, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2412, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2413, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2414, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2415, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2416, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2417, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2418, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2419, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2420, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2421, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2422, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2423, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2424, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2425, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2426, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2427, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2428, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2429, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2430, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2431, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2432, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2433, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2434, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2435, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2436, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2437, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2438, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2439, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2440, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2441, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2442, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2443, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2444, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2445, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2446, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2447, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2448, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2449, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2450, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2451, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2452, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2453, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2454, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2455, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2456, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2457, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2458, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2459, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2460, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2461, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2462, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2463, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2464, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2465, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2466, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2467, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2468, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2469, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2470, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2471, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2472, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2473, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2474, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2475, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2476, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2477, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2478, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2479, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2480, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2481, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2482, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2483, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2484, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2485, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2486, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2487, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2488, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2489, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2490, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2491, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2492, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2493, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2494, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2495, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2496, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2497, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2498, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2499, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2500, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2501, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2502, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2503, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2504, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2505, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2506, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2507, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2508, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2509, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2510, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2511, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2512, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2513, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2514, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2515, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2516, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2517, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2518, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2519, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2520, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2521, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2522, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2523, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2524, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2525, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2526, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2527, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2528, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2529, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2530, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2531, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2532, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2533, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2534, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2535, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2536, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2537, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2538, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2539, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2540, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2541, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2542, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2543, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2544, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2545, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2546, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2547, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2548, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2549, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2550, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2551, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2552, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2553, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2554, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2555, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2556, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2557, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2558, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2559, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2560, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2561, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2562, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2563, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2564, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2565, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2566, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2567, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2568, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2569, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2570, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2571, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2572, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2573, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2574, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2575, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2576, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2577, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2578, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2579, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2580, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2581, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2582, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2583, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2584, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2585, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2586, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2587, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2588, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2589, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2590, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2591, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2592, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2593, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2594, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2595, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2596, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2597, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2598, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2599, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2600, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2601, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2602, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2603, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2604, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2605, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2606, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2607, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2608, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2609, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2610, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2611, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2612, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2613, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2614, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2615, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2616, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2617, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2618, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2619, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2620, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2621, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2622, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2623, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2624, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2625, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2626, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2627, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2628, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2629, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2630, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2631, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2632, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2633, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2634, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2635, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2636, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2637, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2638, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 - }, - { - "frame": 2639, - "kind": "gimbal_step", - "min_pos_delta": 0.00025, - "max_pos_delta": 2 } ] } diff --git a/61_UI/include/app/App.hpp b/61_UI/include/app/App.hpp index 1bbe5bfb9..5899968e5 100644 --- a/61_UI/include/app/App.hpp +++ b/61_UI/include/app/App.hpp @@ -1549,7 +1549,8 @@ class App final : public examples::SimpleWindowedApplication Keyboard, Mouse, Imguizmo, - Action + Action, + Goal }; struct KeyboardData @@ -1588,12 +1589,19 @@ class App final : public examples::SimpleWindowedApplication int32_t value = 0; }; + struct GoalData + { + CCameraGoal goal = {}; + bool requireExact = true; + }; + uint64_t frame = 0; Type type = Type::Keyboard; KeyboardData keyboard; MouseData mouse; float32_t4x4 imguizmo = float32_t4x4(1.f); ActionData action; + GoalData goal; }; struct ScriptedInputCheck diff --git a/61_UI/include/common.hpp b/61_UI/include/common.hpp index 7373cbada..24b56cca1 100644 --- a/61_UI/include/common.hpp +++ b/61_UI/include/common.hpp @@ -23,6 +23,7 @@ #include "camera/CCameraKeyframeTrack.hpp" #include "camera/CCameraPlaybackTimeline.hpp" #include "camera/CCameraPersistence.hpp" +#include "camera/CCameraSequenceScript.hpp" #include "camera/CCameraGoalAnalysis.hpp" #include "camera/CCameraGoalSolver.hpp" #include "camera/CCameraManipulationUtilities.hpp" @@ -70,6 +71,11 @@ using nbl::hlsl::CCameraPreset; using nbl::hlsl::CCameraKeyframe; using nbl::hlsl::CCameraKeyframeTrack; using nbl::hlsl::CCameraPlaybackCursor; +using nbl::hlsl::CCameraSequenceScript; +using nbl::hlsl::CCameraSequenceSegment; +using nbl::hlsl::CCameraSequenceKeyframe; +using nbl::hlsl::CCameraSequencePresentation; +using nbl::hlsl::CCameraSequenceContinuitySettings; using nbl::hlsl::SCameraPlaybackAdvanceResult; using nbl::hlsl::SCameraPresetApplySummary; using nbl::hlsl::SCameraGoalApplyAnalysis; diff --git a/common/include/camera/CCameraSequenceScript.hpp b/common/include/camera/CCameraSequenceScript.hpp new file mode 100644 index 000000000..8043f1a9b --- /dev/null +++ b/common/include/camera/CCameraSequenceScript.hpp @@ -0,0 +1,873 @@ +// 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_SEQUENCE_SCRIPT_HPP_ +#define _C_CAMERA_SEQUENCE_SCRIPT_HPP_ + +#include +#include +#include +#include +#include +#include + +#include "CCameraKeyframeTrack.hpp" +#include "IPlanarProjection.hpp" + +namespace nbl::hlsl +{ + +/** +* Compact authored camera-sequence format shared by playback, scripting, and validation tooling. +* +* The authored file describes: +* +* - which camera kind a segment targets +* - which reusable projection presentations should be shown +* - which keyframed camera goals should be sampled over time +* - which continuity thresholds and capture points should be generated +* +* The format intentionally does not store: +* +* - per-frame low-level event dumps +* - `61_UI`-specific window actions as authored source data +* - ImGuizmo transforms as the primary authored primitive +* +* A consumer such as `61_UI` may expand the compact sequence into its own runtime event/check +* representation, but the authored source of truth stays camera-domain and reusable. +*/ + +//! Authored projection view request for camera-sequence playback. +struct CCameraSequencePresentation +{ + IPlanarProjection::CProjection::ProjectionType projection = IPlanarProjection::CProjection::Perspective; + bool leftHanded = true; +}; + +//! Shared continuity thresholds authored once and reused per sequence segment. +//! Max bounds are enforced per-step, while minimum progress can be satisfied by either position or rotation change. +struct CCameraSequenceContinuitySettings +{ + bool baseline = true; + bool step = true; + bool hasPosDeltaConstraint = true; + float minPosDelta = 0.00025f; + float maxPosDelta = 2.f; + bool hasEulerDeltaConstraint = false; + float minEulerDeltaDeg = 0.f; + float maxEulerDeltaDeg = 1.f; +}; + +//! Relative goal adjustment authored against an initial preset captured from the target camera. +//! Deltas stay camera-domain and avoid binding the authored file to any specific input device or example. +struct CCameraSequenceGoalDelta +{ + bool hasPositionOffset = false; + float64_t3 positionOffset = float64_t3(0.0); + + bool hasRotationEulerDegOffset = false; + float32_t3 rotationEulerDegOffset = float32_t3(0.f); + + bool hasTargetOffset = false; + float64_t3 targetOffset = float64_t3(0.0); + + bool hasOrbitUDeltaDeg = false; + double orbitUDeltaDeg = 0.0; + + bool hasOrbitVDeltaDeg = false; + double orbitVDeltaDeg = 0.0; + + bool hasOrbitDistanceDelta = false; + float orbitDistanceDelta = 0.f; + + bool hasPathAngleDeltaDeg = false; + double pathAngleDeltaDeg = 0.0; + + bool hasPathRadiusDelta = false; + double pathRadiusDelta = 0.0; + + bool hasPathHeightDelta = false; + double pathHeightDelta = 0.0; + + bool hasDynamicBaseFovDelta = false; + float dynamicBaseFovDelta = 0.f; + + bool hasDynamicReferenceDistanceDelta = false; + float dynamicReferenceDistanceDelta = 0.f; +}; + +//! One authored keyframe inside a reusable camera-sequence segment. +//! A keyframe can be described either as an absolute preset or as a delta relative to the captured reference preset. +struct CCameraSequenceKeyframe +{ + float time = 0.f; + bool hasAbsolutePreset = false; + CCameraPreset absolutePreset = {}; + bool hasDelta = false; + CCameraSequenceGoalDelta delta = {}; +}; + +//! Defaults shared by all camera-sequence segments unless overridden locally. +struct CCameraSequenceSegmentDefaults +{ + float durationSeconds = 4.f; + std::vector presentations; + CCameraSequenceContinuitySettings continuity = {}; + std::vector captureFractions = { 1.f }; + bool resetCamera = true; +}; + +//! Authored reusable camera-sequence segment. +//! A segment is the main unit of authored playback and validation and usually maps to one camera showcase chunk. +struct CCameraSequenceSegment +{ + std::string name; + ICamera::CameraKind cameraKind = ICamera::CameraKind::Unknown; + std::string cameraIdentifier; + + bool hasDurationSeconds = false; + float durationSeconds = 0.f; + + bool hasResetCamera = false; + bool resetCamera = true; + + std::vector presentations; + + bool hasContinuity = false; + CCameraSequenceContinuitySettings continuity = {}; + + bool hasCaptureFractions = false; + std::vector captureFractions; + + std::vector keyframes; +}; + +//! Top-level reusable camera-sequence script. +//! Consumers are expected to expand this compact description into their own runtime playback/check pipeline. +struct CCameraSequenceScript +{ + bool enabled = true; + bool log = false; + bool exclusive = false; + bool hardFail = false; + bool visualDebug = false; + float visualDebugTargetFps = 0.f; + float visualDebugHoldSeconds = 0.f; + bool hasEnableActiveCameraMovement = false; + bool enableActiveCameraMovement = true; + std::string capturePrefix = "script"; + float fps = 60.f; + CCameraSequenceSegmentDefaults defaults = {}; + std::vector segments; +}; + +inline bool tryParseCameraKind(std::string_view value, ICamera::CameraKind& outKind) +{ + if (value == "FPS") + outKind = ICamera::CameraKind::FPS; + else if (value == "Free") + outKind = ICamera::CameraKind::Free; + else if (value == "Orbit") + outKind = ICamera::CameraKind::Orbit; + else if (value == "Arcball") + outKind = ICamera::CameraKind::Arcball; + else if (value == "Turntable") + outKind = ICamera::CameraKind::Turntable; + else if (value == "TopDown") + outKind = ICamera::CameraKind::TopDown; + else if (value == "Isometric") + outKind = ICamera::CameraKind::Isometric; + else if (value == "Chase") + outKind = ICamera::CameraKind::Chase; + else if (value == "Dolly") + outKind = ICamera::CameraKind::Dolly; + else if (value == "DollyZoom" || value == "Dolly Zoom") + outKind = ICamera::CameraKind::DollyZoom; + else if (value == "Path") + outKind = ICamera::CameraKind::Path; + else + return false; + + return true; +} + +inline bool tryParseProjectionType(std::string_view value, IPlanarProjection::CProjection::ProjectionType& outType) +{ + if (value == "perspective" || value == "Perspective") + outType = IPlanarProjection::CProjection::Perspective; + else if (value == "orthographic" || value == "Orthographic") + outType = IPlanarProjection::CProjection::Orthographic; + else + return false; + + return true; +} + +inline void normalizeCaptureFractions(std::vector& fractions) +{ + for (auto& fraction : fractions) + fraction = std::clamp(fraction, 0.f, 1.f); + + std::sort(fractions.begin(), fractions.end()); + fractions.erase(std::unique(fractions.begin(), fractions.end(), + [](const float lhs, const float rhs) { return std::abs(lhs - rhs) <= 1e-6f; }), + fractions.end()); +} + +inline bool tryParseCaptureFraction(const nlohmann::json& entry, float& outFraction) +{ + if (entry.is_number()) + { + outFraction = std::clamp(entry.get(), 0.f, 1.f); + return true; + } + + if (!entry.is_string()) + return false; + + const auto tag = entry.get(); + if (tag == "start") + outFraction = 0.f; + else if (tag == "mid" || tag == "middle") + outFraction = 0.5f; + else if (tag == "end") + outFraction = 1.f; + else + return false; + + return true; +} + +inline bool deserializeSequencePresentations(const nlohmann::json& root, std::vector& out, std::string* error = nullptr) +{ + out.clear(); + if (!root.is_array()) + { + if (error) + *error = "Sequence presentations must be an array."; + return false; + } + + for (const auto& entry : root) + { + if (!entry.is_object() || !entry.contains("projection")) + { + if (error) + *error = "Sequence presentation entry missing \"projection\"."; + return false; + } + + CCameraSequencePresentation presentation; + if (!tryParseProjectionType(entry["projection"].get(), presentation.projection)) + { + if (error) + *error = "Sequence presentation has invalid projection type."; + return false; + } + if (entry.contains("left_handed")) + presentation.leftHanded = entry["left_handed"].get(); + out.emplace_back(presentation); + } + + return true; +} + +inline bool deserializeSequenceContinuity(const nlohmann::json& root, CCameraSequenceContinuitySettings& out, std::string* error = nullptr) +{ + if (!root.is_object()) + { + if (error) + *error = "Sequence continuity settings must be an object."; + return false; + } + + out = {}; + if (root.contains("baseline")) + out.baseline = root["baseline"].get(); + if (root.contains("step")) + out.step = root["step"].get(); + + if (root.contains("min_pos_delta")) + { + out.minPosDelta = root["min_pos_delta"].get(); + out.hasPosDeltaConstraint = true; + } + if (root.contains("max_pos_delta")) + { + out.maxPosDelta = root["max_pos_delta"].get(); + out.hasPosDeltaConstraint = true; + } + else if (root.contains("pos_tolerance")) + { + out.maxPosDelta = root["pos_tolerance"].get(); + out.hasPosDeltaConstraint = true; + } + + if (root.contains("min_euler_delta_deg")) + { + out.minEulerDeltaDeg = root["min_euler_delta_deg"].get(); + out.hasEulerDeltaConstraint = true; + } + if (root.contains("max_euler_delta_deg")) + { + out.maxEulerDeltaDeg = root["max_euler_delta_deg"].get(); + out.hasEulerDeltaConstraint = true; + } + else if (root.contains("euler_tolerance_deg")) + { + out.maxEulerDeltaDeg = root["euler_tolerance_deg"].get(); + out.hasEulerDeltaConstraint = true; + } + + if (root.contains("disable_pos_delta")) + out.hasPosDeltaConstraint = !root["disable_pos_delta"].get(); + if (root.contains("disable_euler_delta")) + out.hasEulerDeltaConstraint = !root["disable_euler_delta"].get(); + + if (out.step && !(out.hasPosDeltaConstraint || out.hasEulerDeltaConstraint)) + { + if (error) + *error = "Sequence continuity step checks require at least one delta constraint."; + return false; + } + + return true; +} + +inline bool deserializeSequenceGoalDelta(const nlohmann::json& root, CCameraSequenceGoalDelta& out, std::string* error = nullptr) +{ + if (!root.is_object()) + { + if (error) + *error = "Sequence keyframe delta must be an object."; + return false; + } + + out = {}; + auto readFloat3 = [](const nlohmann::json& entry, auto& outValue) -> void + { + const auto arr = entry.get>(); + outValue = std::decay_t(arr[0], arr[1], arr[2]); + }; + auto readDouble3 = [](const nlohmann::json& entry, auto& outValue) -> void + { + const auto arr = entry.get>(); + outValue = std::decay_t(arr[0], arr[1], arr[2]); + }; + + if (root.contains("position_offset")) + { + readDouble3(root["position_offset"], out.positionOffset); + out.hasPositionOffset = true; + } + if (root.contains("rotation_euler_deg_offset")) + { + readFloat3(root["rotation_euler_deg_offset"], out.rotationEulerDegOffset); + out.hasRotationEulerDegOffset = true; + } + if (root.contains("target_offset")) + { + readDouble3(root["target_offset"], out.targetOffset); + out.hasTargetOffset = true; + } + if (root.contains("orbit_u_delta_deg")) + { + out.orbitUDeltaDeg = root["orbit_u_delta_deg"].get(); + out.hasOrbitUDeltaDeg = true; + } + if (root.contains("orbit_v_delta_deg")) + { + out.orbitVDeltaDeg = root["orbit_v_delta_deg"].get(); + out.hasOrbitVDeltaDeg = true; + } + if (root.contains("orbit_distance_delta")) + { + out.orbitDistanceDelta = root["orbit_distance_delta"].get(); + out.hasOrbitDistanceDelta = true; + } + if (root.contains("path_angle_delta_deg")) + { + out.pathAngleDeltaDeg = root["path_angle_delta_deg"].get(); + out.hasPathAngleDeltaDeg = true; + } + if (root.contains("path_radius_delta")) + { + out.pathRadiusDelta = root["path_radius_delta"].get(); + out.hasPathRadiusDelta = true; + } + if (root.contains("path_height_delta")) + { + out.pathHeightDelta = root["path_height_delta"].get(); + out.hasPathHeightDelta = true; + } + if (root.contains("dynamic_base_fov_delta")) + { + out.dynamicBaseFovDelta = root["dynamic_base_fov_delta"].get(); + out.hasDynamicBaseFovDelta = true; + } + if (root.contains("dynamic_reference_distance_delta")) + { + out.dynamicReferenceDistanceDelta = root["dynamic_reference_distance_delta"].get(); + out.hasDynamicReferenceDistanceDelta = true; + } + + return true; +} + +inline bool deserializeSequenceKeyframe(const nlohmann::json& root, CCameraSequenceKeyframe& out, std::string* error = nullptr) +{ + if (!root.is_object()) + { + if (error) + *error = "Sequence keyframe must be an object."; + return false; + } + + out = {}; + if (root.contains("time")) + out.time = std::max(0.f, root["time"].get()); + + if (root.contains("delta")) + { + if (!deserializeSequenceGoalDelta(root["delta"], out.delta, error)) + return false; + out.hasDelta = true; + } + + if (root.contains("preset")) + { + deserializePreset(root["preset"], out.absolutePreset); + out.hasAbsolutePreset = true; + } + else if (root.contains("position") || root.contains("orientation") || root.contains("target_position") || + root.contains("distance") || root.contains("orbit_u") || root.contains("orbit_v") || + root.contains("orbit_distance") || root.contains("path_angle") || root.contains("path_radius") || + root.contains("path_height") || root.contains("dynamic_base_fov") || root.contains("dynamic_reference_distance")) + { + deserializePreset(root, out.absolutePreset); + out.hasAbsolutePreset = true; + } + + return true; +} + +inline bool deserializeSequenceSegment(const nlohmann::json& root, CCameraSequenceSegment& out, std::string* error = nullptr) +{ + if (!root.is_object()) + { + if (error) + *error = "Sequence segment must be an object."; + return false; + } + + out = {}; + if (root.contains("name")) + out.name = root["name"].get(); + if (root.contains("camera_identifier")) + out.cameraIdentifier = root["camera_identifier"].get(); + if (root.contains("camera_kind")) + { + if (!tryParseCameraKind(root["camera_kind"].get(), out.cameraKind)) + { + if (error) + *error = "Sequence segment has invalid camera_kind."; + return false; + } + } + if (root.contains("duration_seconds")) + { + out.durationSeconds = std::max(0.f, root["duration_seconds"].get()); + out.hasDurationSeconds = true; + } + if (root.contains("reset_camera")) + { + out.resetCamera = root["reset_camera"].get(); + out.hasResetCamera = true; + } + if (root.contains("presentations")) + { + if (!deserializeSequencePresentations(root["presentations"], out.presentations, error)) + return false; + } + if (root.contains("continuity")) + { + if (!deserializeSequenceContinuity(root["continuity"], out.continuity, error)) + return false; + out.hasContinuity = true; + } + if (root.contains("captures")) + { + if (!root["captures"].is_array()) + { + if (error) + *error = "Sequence segment captures must be an array."; + return false; + } + + out.captureFractions.clear(); + for (const auto& entry : root["captures"]) + { + float fraction = 0.f; + if (!tryParseCaptureFraction(entry, fraction)) + { + if (error) + *error = "Sequence segment capture entry is invalid."; + return false; + } + out.captureFractions.emplace_back(fraction); + } + normalizeCaptureFractions(out.captureFractions); + out.hasCaptureFractions = true; + } + if (root.contains("keyframes")) + { + if (!root["keyframes"].is_array()) + { + if (error) + *error = "Sequence segment keyframes must be an array."; + return false; + } + for (const auto& entry : root["keyframes"]) + { + CCameraSequenceKeyframe keyframe; + if (!deserializeSequenceKeyframe(entry, keyframe, error)) + return false; + out.keyframes.emplace_back(std::move(keyframe)); + } + } + + if (out.keyframes.empty()) + { + if (error) + *error = "Sequence segment requires at least one keyframe."; + return false; + } + if (out.cameraKind == ICamera::CameraKind::Unknown && out.cameraIdentifier.empty()) + { + if (error) + *error = "Sequence segment requires camera_kind or camera_identifier."; + return false; + } + + return true; +} + +inline bool deserializeCameraSequenceScript(const nlohmann::json& root, CCameraSequenceScript& out, std::string* error = nullptr) +{ + if (!root.is_object()) + { + if (error) + *error = "Camera sequence script must be an object."; + return false; + } + + out = {}; + if (root.contains("enabled")) + out.enabled = root["enabled"].get(); + if (root.contains("log")) + out.log = root["log"].get(); + if (root.contains("exclusive")) + out.exclusive = root["exclusive"].get(); + if (root.contains("exclusive_input")) + out.exclusive = root["exclusive_input"].get() || out.exclusive; + if (root.contains("hard_fail")) + out.hardFail = root["hard_fail"].get(); + if (root.contains("visual_debug")) + out.visualDebug = root["visual_debug"].get(); + if (root.contains("visual_debug_target_fps")) + out.visualDebugTargetFps = root["visual_debug_target_fps"].get(); + if (root.contains("visual_debug_hold_seconds")) + out.visualDebugHoldSeconds = root["visual_debug_hold_seconds"].get(); + if (root.contains("enableActiveCameraMovement")) + { + out.enableActiveCameraMovement = root["enableActiveCameraMovement"].get(); + out.hasEnableActiveCameraMovement = true; + } + if (root.contains("capture_prefix")) + out.capturePrefix = root["capture_prefix"].get(); + if (root.contains("fps")) + out.fps = std::max(1.f, root["fps"].get()); + + if (root.contains("defaults")) + { + const auto& defaults = root["defaults"]; + if (!defaults.is_object()) + { + if (error) + *error = "Camera sequence defaults must be an object."; + return false; + } + + if (defaults.contains("duration_seconds")) + out.defaults.durationSeconds = std::max(0.f, defaults["duration_seconds"].get()); + if (defaults.contains("reset_camera")) + out.defaults.resetCamera = defaults["reset_camera"].get(); + if (defaults.contains("presentations")) + { + if (!deserializeSequencePresentations(defaults["presentations"], out.defaults.presentations, error)) + return false; + } + if (defaults.contains("continuity")) + { + if (!deserializeSequenceContinuity(defaults["continuity"], out.defaults.continuity, error)) + return false; + } + if (defaults.contains("captures")) + { + if (!defaults["captures"].is_array()) + { + if (error) + *error = "Camera sequence default captures must be an array."; + return false; + } + + out.defaults.captureFractions.clear(); + for (const auto& entry : defaults["captures"]) + { + float fraction = 0.f; + if (!tryParseCaptureFraction(entry, fraction)) + { + if (error) + *error = "Camera sequence default capture entry is invalid."; + return false; + } + out.defaults.captureFractions.emplace_back(fraction); + } + normalizeCaptureFractions(out.defaults.captureFractions); + } + } + + if (!root.contains("segments") || !root["segments"].is_array()) + { + if (error) + *error = "Camera sequence script requires a \"segments\" array."; + return false; + } + + for (const auto& entry : root["segments"]) + { + CCameraSequenceSegment segment; + if (!deserializeSequenceSegment(entry, segment, error)) + return false; + out.segments.emplace_back(std::move(segment)); + } + + if (out.segments.empty()) + { + if (error) + *error = "Camera sequence script must contain at least one segment."; + return false; + } + + return true; +} + +inline bool applyCanonicalSphericalGoal(CCameraGoal& goal) +{ + if (!(goal.hasTargetPosition && goal.hasOrbitState)) + return false; + if (!std::isfinite(goal.orbitU) || !std::isfinite(goal.orbitV) || !std::isfinite(goal.orbitDistance)) + return false; + + const float appliedDistance = std::clamp(goal.orbitDistance, CSphericalTargetCamera::MinDistance, CSphericalTargetCamera::MaxDistance); + const float64_t3 spherePosition( + std::cos(goal.orbitV) * std::cos(goal.orbitU) * static_cast(appliedDistance), + std::cos(goal.orbitV) * std::sin(goal.orbitU) * static_cast(appliedDistance), + std::sin(goal.orbitV) * static_cast(appliedDistance)); + goal.position = goal.targetPosition + spherePosition; + goal.hasDistance = true; + goal.distance = appliedDistance; + goal.orbitDistance = appliedDistance; + + const auto forward = normalize(-spherePosition); + const float64_t3 up = normalize(float64_t3( + -std::sin(goal.orbitV) * std::cos(goal.orbitU), + -std::sin(goal.orbitV) * std::sin(goal.orbitU), + std::cos(goal.orbitV))); + const float64_t3 right = normalize(cross(up, forward)); + goal.orientation = glm::quat_cast(glm::dmat3{ right, up, forward }); + return true; +} + +inline bool buildSequenceKeyframePreset(const CCameraPreset& reference, const CCameraSequenceKeyframe& authored, CCameraPreset& outPreset, std::string* error = nullptr) +{ + if (authored.hasAbsolutePreset) + { + outPreset = authored.absolutePreset; + if (outPreset.identifier.empty()) + outPreset.identifier = reference.identifier; + if (outPreset.name.empty()) + outPreset.name = reference.name; + return isGoalFinite(makeGoalFromPreset(outPreset)); + } + + outPreset = reference; + if (!authored.hasDelta) + return true; + + auto goal = makeGoalFromPreset(reference); + const auto& delta = authored.delta; + + const bool hasPoseDelta = delta.hasPositionOffset || delta.hasRotationEulerDegOffset; + const bool hasSphericalDelta = delta.hasTargetOffset || delta.hasOrbitUDeltaDeg || delta.hasOrbitVDeltaDeg || delta.hasOrbitDistanceDelta; + const bool hasPathDelta = delta.hasPathAngleDeltaDeg || delta.hasPathRadiusDelta || delta.hasPathHeightDelta; + + if (hasPoseDelta && (hasSphericalDelta || hasPathDelta)) + { + if (error) + *error = "Sequence keyframe delta cannot mix pose offsets with spherical/path deltas."; + return false; + } + + if (delta.hasPositionOffset) + goal.position += delta.positionOffset; + + if (delta.hasRotationEulerDegOffset) + { + const auto deltaRadians = glm::radians(delta.rotationEulerDegOffset); + goal.orientation = glm::normalize(goal.orientation * glm::quat(deltaRadians)); + } + + if (delta.hasTargetOffset) + { + if (!goal.hasTargetPosition) + { + if (error) + *error = "Sequence keyframe target_offset requires target state."; + return false; + } + goal.targetPosition += delta.targetOffset; + } + + if (delta.hasOrbitUDeltaDeg || delta.hasOrbitVDeltaDeg || delta.hasOrbitDistanceDelta) + { + if (!goal.hasOrbitState) + { + if (error) + *error = "Sequence keyframe orbit deltas require spherical orbit state."; + return false; + } + if (delta.hasOrbitUDeltaDeg) + goal.orbitU = wrapAngleRad(goal.orbitU + glm::radians(delta.orbitUDeltaDeg)); + if (delta.hasOrbitVDeltaDeg) + goal.orbitV = std::clamp(goal.orbitV + glm::radians(delta.orbitVDeltaDeg), -1.55334303427, 1.55334303427); + if (delta.hasOrbitDistanceDelta) + goal.orbitDistance += delta.orbitDistanceDelta; + } + + if (delta.hasPathAngleDeltaDeg || delta.hasPathRadiusDelta || delta.hasPathHeightDelta) + { + if (!goal.hasPathState) + { + if (error) + *error = "Sequence keyframe path deltas require path state."; + return false; + } + if (delta.hasPathAngleDeltaDeg) + goal.pathState.angle = wrapAngleRad(goal.pathState.angle + glm::radians(delta.pathAngleDeltaDeg)); + if (delta.hasPathRadiusDelta) + goal.pathState.radius += delta.pathRadiusDelta; + if (delta.hasPathHeightDelta) + goal.pathState.height += delta.pathHeightDelta; + } + + if (delta.hasDynamicBaseFovDelta || delta.hasDynamicReferenceDistanceDelta) + { + if (!goal.hasDynamicPerspectiveState) + { + if (error) + *error = "Sequence keyframe dynamic perspective deltas require dynamic perspective state."; + return false; + } + if (delta.hasDynamicBaseFovDelta) + goal.dynamicPerspectiveState.baseFov = std::clamp(goal.dynamicPerspectiveState.baseFov + delta.dynamicBaseFovDelta, 1.f, 179.f); + if (delta.hasDynamicReferenceDistanceDelta) + goal.dynamicPerspectiveState.referenceDistance = std::max(0.001f, goal.dynamicPerspectiveState.referenceDistance + delta.dynamicReferenceDistanceDelta); + } + + if (hasPathDelta) + { + if (!applyCanonicalPathGoal(goal)) + { + if (error) + *error = "Sequence keyframe failed to canonicalize path state."; + return false; + } + } + else if (hasSphericalDelta) + { + if (!applyCanonicalSphericalGoal(goal)) + { + if (error) + *error = "Sequence keyframe failed to canonicalize spherical state."; + return false; + } + } + + if (!isGoalFinite(goal)) + { + if (error) + *error = "Sequence keyframe produced a non-finite goal."; + return false; + } + + assignGoalToPreset(outPreset, goal); + return true; +} + +inline bool buildSequenceTrackFromReference(const CCameraPreset& reference, const CCameraSequenceSegment& segment, CCameraKeyframeTrack& outTrack, std::string* error = nullptr) +{ + outTrack = {}; + outTrack.keyframes.reserve(segment.keyframes.size()); + + for (const auto& entry : segment.keyframes) + { + CCameraKeyframe keyframe; + keyframe.time = std::max(0.f, entry.time); + if (!buildSequenceKeyframePreset(reference, entry, keyframe.preset, error)) + return false; + outTrack.keyframes.emplace_back(std::move(keyframe)); + } + + sortKeyframeTrackByTime(outTrack); + normalizeSelectedKeyframeTrack(outTrack); + return !outTrack.keyframes.empty(); +} + +inline float getSequenceSegmentDurationSeconds(const CCameraSequenceScript& script, const CCameraSequenceSegment& segment, const CCameraKeyframeTrack* track = nullptr) +{ + if (segment.hasDurationSeconds) + return std::max(0.f, segment.durationSeconds); + if (script.defaults.durationSeconds > 0.f) + return script.defaults.durationSeconds; + if (track) + return getPlaybackTrackDuration(*track); + return 0.f; +} + +inline const std::vector& getSequenceSegmentPresentations(const CCameraSequenceScript& script, const CCameraSequenceSegment& segment) +{ + return segment.presentations.empty() ? script.defaults.presentations : segment.presentations; +} + +inline CCameraSequenceContinuitySettings getSequenceSegmentContinuity(const CCameraSequenceScript& script, const CCameraSequenceSegment& segment) +{ + return segment.hasContinuity ? segment.continuity : script.defaults.continuity; +} + +inline std::vector getSequenceSegmentCaptureFractions(const CCameraSequenceScript& script, const CCameraSequenceSegment& segment) +{ + auto captures = segment.hasCaptureFractions ? segment.captureFractions : script.defaults.captureFractions; + normalizeCaptureFractions(captures); + return captures; +} + +inline bool getSequenceSegmentResetCamera(const CCameraSequenceScript& script, const CCameraSequenceSegment& segment) +{ + return segment.hasResetCamera ? segment.resetCamera : script.defaults.resetCamera; +} + +} // namespace nbl::hlsl + +#endif // _C_CAMERA_SEQUENCE_SCRIPT_HPP_ diff --git a/common/include/camera/README.md b/common/include/camera/README.md index f03525bce..9d85f378b 100644 --- a/common/include/camera/README.md +++ b/common/include/camera/README.md @@ -12,6 +12,7 @@ The current design goal is: - reusable preset capture and apply flow helpers live in shared headers - reusable playback cursor and timeline helpers live in shared headers - reusable preset and keyframe persistence helpers live in shared headers +- reusable compact camera-sequence scripting helpers live in shared headers - `61_UI` is the current integration and validation surface ## Mental model @@ -41,6 +42,10 @@ and the playback cursor sits beside that state: `CCameraPlaybackCursor <-> CCameraKeyframeTrack` +and compact authored sequence scripts sit above the same shared camera-domain state: + +`CCameraSequenceScript -> CCameraKeyframeTrack -> CCameraPreset -> CCameraGoal` + ## Core contracts ### `IGimbal.hpp` @@ -366,6 +371,36 @@ Provides: This keeps playback-time progression reusable without forcing examples to reimplement cursor stepping rules. +### `CCameraSequenceScript.hpp` + +Reusable compact authored camera-sequence format for playback and validation. + +Provides: + +- `CCameraSequencePresentation` +- `CCameraSequenceContinuitySettings` +- `CCameraSequenceGoalDelta` +- `CCameraSequenceKeyframe` +- `CCameraSequenceSegment` +- `CCameraSequenceScript` +- parsing and normalization helpers +- reusable sequence-to-track construction from captured reference presets + +The important design rule is that the authored format stays camera-domain: + +- segment and keyframe based +- keyed by camera kind or identifier +- keyed by projection presentation requests +- keyed by compact continuity thresholds and capture fractions + +and deliberately does not store: + +- expanded per-frame event dumps +- `61_UI`-specific window-routing commands as authored source data +- ImGuizmo matrices as the authored motion primitive + +That makes the same authored sequence usable by any future consumer that understands the shared camera API, even if its runtime expansion path differs from `61_UI`. + ### `CCameraPersistence.hpp` Reusable JSON and file persistence helpers for preset collections and keyframe tracks. From 6b5b4636c24fccb027ae6a1ef9a4c1f88f23effd Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Mon, 6 Apr 2026 15:49:48 +0200 Subject: [PATCH 165/205] Add tracked-target camera follow flow --- 61_UI/AppControlPanel.cpp | 58 ++- 61_UI/AppImGuiListen.cpp | 79 ++--- 61_UI/AppInit.cpp | 108 +++++- 61_UI/AppTransformEditor.cpp | 41 +-- 61_UI/AppUpdate.cpp | 2 + 61_UI/AppWorkLoop.cpp | 21 +- 61_UI/README.md | 23 ++ 61_UI/include/app/App.hpp | 254 +++++++++++++- 61_UI/include/common.hpp | 7 + .../include/camera/CCameraFollowUtilities.hpp | 331 ++++++++++++++++++ common/include/camera/README.md | 26 ++ 11 files changed, 872 insertions(+), 78 deletions(-) create mode 100644 common/include/camera/CCameraFollowUtilities.hpp diff --git a/61_UI/AppControlPanel.cpp b/61_UI/AppControlPanel.cpp index 1ed680893..a138722ea 100644 --- a/61_UI/AppControlPanel.cpp +++ b/61_UI/AppControlPanel.cpp @@ -531,7 +531,7 @@ void App::DrawControlPanel() if (ImGui::TreeNodeEx("Bound Camera", flags)) { ImGui::Text("Type: %s", boundCamera->getIdentifier().data()); - ImGui::Text("Object Ix: %s", std::to_string(active.activePlanarIx + 1u).c_str()); + ImGui::Text("Object Ix: %s", std::to_string(active.activePlanarIx + 2u).c_str()); ImGui::Separator(); { ICamera::SphericalTargetState sphericalState; @@ -665,6 +665,62 @@ void App::DrawControlPanel() { ImGui::TextDisabled("Active camera is not orbit."); } + + DrawSectionHeader("FollowHeader", "Follow Target", accent); + auto* activeFollowConfig = getActiveFollowConfig(); + if (activeFollowConfig) + { + auto& followConfig = *activeFollowConfig; + ImGui::Checkbox("Enable follow", &followConfig.enabled); + DrawHoverHint("Apply tracked-target follow to the active planar camera"); + + const char* followModeLabels[] = { + getCameraFollowModeLabel(ECameraFollowMode::Disabled), + getCameraFollowModeLabel(ECameraFollowMode::OrbitTarget), + getCameraFollowModeLabel(ECameraFollowMode::LookAtTarget), + getCameraFollowModeLabel(ECameraFollowMode::KeepWorldOffset), + getCameraFollowModeLabel(ECameraFollowMode::KeepLocalOffset) + }; + int followModeIx = static_cast(followConfig.mode); + if (ImGui::Combo("Mode", &followModeIx, followModeLabels, IM_ARRAYSIZE(followModeLabels))) + followConfig.mode = static_cast(followModeIx); + + auto trackedTarget = getCastedVector(m_followTarget.getGimbal().getPosition()); + if (ImGui::InputFloat3("Tracked target", &trackedTarget[0])) + m_followTarget.setPosition(getCastedVector(trackedTarget)); + + ImGui::Checkbox("Show target marker", &m_followTargetVisible); + DrawHoverHint("Render the tracked target marker in the scene"); + + if (ImGui::Button("Target = model")) + resetFollowTargetToModel(); + DrawHoverHint("Snap tracked target pose to the model transform"); + ImGui::SameLine(); + if (ImGui::Button("Target origin")) + m_followTarget.setPose(float64_t3(0.0), glm::quat(1.0f, 0.0f, 0.0f, 0.0f)); + DrawHoverHint("Reset tracked target to identity at world origin"); + ImGui::SameLine(); + if (ImGui::Button("Capture current offset")) + captureFollowOffsetsForPlanar(getActivePlanarIx()); + DrawHoverHint("Store current camera-to-target relation into the active follow config"); + + if (cameraFollowModeUsesWorldOffset(followConfig.mode)) + { + auto worldOffset = getCastedVector(followConfig.worldOffset); + if (ImGui::InputFloat3("World offset", &worldOffset[0])) + followConfig.worldOffset = getCastedVector(worldOffset); + } + if (cameraFollowModeUsesLocalOffset(followConfig.mode)) + { + auto localOffset = getCastedVector(followConfig.localOffset); + if (ImGui::InputFloat3("Local offset", &localOffset[0])) + followConfig.localOffset = getCastedVector(localOffset); + } + } + else + { + ImGui::TextDisabled("No active follow config."); + } ImGui::PopItemWidth(); } ImGui::EndChild(); diff --git a/61_UI/AppImGuiListen.cpp b/61_UI/AppImGuiListen.cpp index d7bcbd7ca..65e240c8e 100644 --- a/61_UI/AppImGuiListen.cpp +++ b/61_UI/AppImGuiListen.cpp @@ -18,20 +18,6 @@ void App::imguiListen() SImResourceInfo info; info.samplerIx = (uint16_t)nbl::ext::imgui::UI::DefaultSamplerIx::USER; - // ORBIT CAMERA TEST - { - for (auto& planar : m_planarProjections) - { - auto* camera = planar->getCamera(); - if (camera) - { - const auto targetPosition = hlsl::transpose(getMatrix3x4As4x4(m_model))[3]; - if (camera->trySetSphericalTarget(float64_t3(targetPosition.x, targetPosition.y, targetPosition.z))) - camera->manipulate({}, {}); - } - } - } - // render bound planar camera views onto GUI windows if (useWindow) { @@ -44,7 +30,8 @@ void App::imguiListen() size_t gizmoIx = {}; size_t manipulationCounter = {}; - const std::optional modelInUseIx = ImGuizmo::IsUsingAny() ? std::optional(boundPlanarCameraIxToManipulate.has_value() ? 1u + boundPlanarCameraIxToManipulate.value() : 0u) : std::optional(std::nullopt); + const std::optional modelInUseIx = ImGuizmo::IsUsingAny() ? std::optional(getManipulatedObjectIx()) : std::optional(std::nullopt); + (void)modelInUseIx; for (uint32_t windowIx = 0; windowIx < windowBindings.size(); ++windowIx) { @@ -156,16 +143,14 @@ void App::imguiListen() if (!hideSceneGizmos) { - for (uint32_t modelIx = 0; modelIx < 1u + m_planarProjections.size(); modelIx++) + for (uint32_t objectIx = 0u; objectIx < getManipulableObjectCount(); ++objectIx) { ImGuizmo::PushID(gizmoIx); ++gizmoIx; - const bool isCameraGimbalTarget = modelIx; // I assume scene demo model is 0th ix, left are planar cameras - ICamera* const targetGimbalManipulationCamera = isCameraGimbalTarget ? m_planarProjections[modelIx - 1u]->getCamera() : nullptr; + const auto planarIx = getManipulableObjectPlanarIx(objectIx); + const bool isFollowTarget = isManipulableObjectFollowTarget(objectIx); + ICamera* const targetGimbalManipulationCamera = planarIx.has_value() ? m_planarProjections[planarIx.value()]->getCamera() : nullptr; - // if we try to manipulate a camera which appears to be the same camera we see scene from then obvsly it doesn't make sense to manipulate its gizmo so we skip it - // EDIT: it actually makes some sense if you assume render planar view is rendered with ortho projection, but we would need to add imguizmo controller virtual map - // to ban forward/backward in this mode if this condition is true if (targetGimbalManipulationCamera == planarViewCameraBound) { ImGuizmo::PopID(); @@ -173,24 +158,10 @@ void App::imguiListen() } ImGuizmoModelM16InOut imguizmoModel; + imguizmoModel.inTRS = getManipulableObjectTransform(objectIx); - if (isCameraGimbalTarget) - { - assert(targetGimbalManipulationCamera); - imguizmoModel.inTRS = getCastedMatrix(targetGimbalManipulationCamera->getGimbal().template operator() < float64_t4x4 > ()); - } - else - imguizmoModel.inTRS = hlsl::transpose(getMatrix3x4As4x4(m_model)); - - const float gizmoWorldRadius = 0.22f; - float32_t3 gizmoWorldPos = {}; - if (isCameraGimbalTarget) - gizmoWorldPos = getCastedVector(targetGimbalManipulationCamera->getGimbal().getPosition()); - else - { - const auto modelPos = hlsl::transpose(getMatrix3x4As4x4(m_model))[3]; - gizmoWorldPos = float32_t3(modelPos.x, modelPos.y, modelPos.z); - } + const float gizmoWorldRadius = isFollowTarget ? 0.35f : 0.22f; + const auto gizmoWorldPos = getManipulableObjectWorldPosition(objectIx); const auto viewPos = mul(viewMatrix, float32_t4(gizmoWorldPos, 1.0f)); const float depth = std::max(0.001f, std::abs(viewPos.z)); @@ -210,9 +181,7 @@ void App::imguiListen() if (targetGimbalManipulationCamera) { const auto referenceFrame = getCastedMatrix(*reinterpret_cast(ImGuizmo::GetReferenceFrame())); - - boundCameraToManipulate = smart_refctd_ptr(targetGimbalManipulationCamera); - boundPlanarCameraIxToManipulate = modelIx - 1u; + bindManipulatedCamera(planarIx.value()); // TODO: TO BE REMOVED, ONLY FOR TESTING ITS INCOMPLETE TYPE! const auto& imguizmoCtx = ImGuizmo::GetContext(); @@ -317,12 +286,16 @@ void App::imguiListen() } } + else if (isFollowTarget) + { + setFollowTargetTransform(getCastedMatrix(imguizmoModel.outTRS)); + bindManipulatedFollowTarget(); + applyFollowToConfiguredCameras(); + } else { - // again, for scene demo model full affine transformation without limits is assumed m_model = float32_t3x4(hlsl::transpose(imguizmoModel.outTRS)); - boundCameraToManipulate = nullptr; - boundPlanarCameraIxToManipulate = std::nullopt; + bindManipulatedModel(); } } @@ -330,7 +303,7 @@ void App::imguiListen() { if (targetGimbalManipulationCamera && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) { - const uint32_t newPlanarIx = modelIx - 1u; + const uint32_t newPlanarIx = planarIx.value(); if (newPlanarIx < m_planarProjections.size()) { binding.activePlanarIx = newPlanarIx; @@ -339,6 +312,10 @@ void App::imguiListen() activeRenderWindowIx = windowIx; } } + else if (isFollowTarget && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) + bindManipulatedFollowTarget(); + else if (!targetGimbalManipulationCamera && !isFollowTarget && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) + bindManipulatedModel(); ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.2f, 0.2f, 0.2f, 0.8f)); ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); @@ -357,11 +334,13 @@ void App::imguiListen() if (targetGimbalManipulationCamera) ident = targetGimbalManipulationCamera->getIdentifier(); + else if (isFollowTarget) + ident = m_followTarget.getIdentifier(); else ident = "Geometry Creator Object"; ImGui::Text("Identifier: %s", ident.c_str()); - ImGui::Text("Object Ix: %u", modelIx); + ImGui::Text("Object Ix: %u", objectIx); if (targetGimbalManipulationCamera) { ImGui::Separator(); @@ -369,6 +348,13 @@ void App::imguiListen() ImGui::TextDisabled("LMB drag: manipulate gizmo"); ImGui::TextDisabled("SPACE: toggle move mode"); } + else if (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"); + } ImGui::End(); @@ -439,6 +425,7 @@ void App::imguiListen() DrawControlPanel(); UpdateBoundCameraMovement(); UpdateCursorVisibility(); + applyFollowToConfiguredCameras(); // update camera matrices for scene rendering { diff --git a/61_UI/AppInit.cpp b/61_UI/AppInit.cpp index 64cf1b671..d894d4254 100644 --- a/61_UI/AppInit.cpp +++ b/61_UI/AppInit.cpp @@ -327,6 +327,16 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) return nbl::hlsl::describePresetCameraMismatch(m_cameraGoalSolver, camera, preset); }; + auto compareGoalToCamera = [&](ICamera* camera, const CCameraGoal& goal, const char* label) -> bool + { + auto capture = m_cameraGoalSolver.captureDetailed(camera); + if (!capture.canUseGoal()) + return fail(std::string("Follow smoke failed to capture camera state for ") + label + "."); + if (!nbl::hlsl::compareGoals(capture.goal, goal, 1e-6, 0.1, 1e-6)) + return fail(std::string("Follow smoke mismatch for ") + label + ". " + nbl::hlsl::describeGoalMismatch(capture.goal, goal)); + return true; + }; + auto collectKeyboardVirtualEvents = [&](CGimbalInputBinder& inputBinder, const ui::E_KEY_CODE keyCode) -> std::vector { static std::chrono::microseconds smokeTimestamp = std::chrono::microseconds::zero(); @@ -719,6 +729,84 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) ICamera* chaseCamera = findCameraByKind(ICamera::CameraKind::Chase); ICamera* dollyCamera = findCameraByKind(ICamera::CameraKind::Dolly); ICamera* dollyZoomCamera = findCameraByKind(ICamera::CameraKind::DollyZoom); + + { + CTrackedTarget trackedTarget( + float64_t3(2.25, -0.75, 1.25), + glm::quat(glm::dvec3(0.18, -0.22, 0.41)), + "Smoke Target"); + + if (orbitCamera) + { + const auto baselinePreset = nbl::hlsl::capturePreset(m_cameraGoalSolver, orbitCamera, "orbit-follow-baseline"); + SCameraFollowConfig followConfig = {}; + followConfig.enabled = true; + followConfig.mode = ECameraFollowMode::OrbitTarget; + + CCameraGoal followGoal = {}; + if (!nbl::hlsl::tryBuildFollowGoal(m_cameraGoalSolver, orbitCamera, trackedTarget, followConfig, followGoal)) + return fail("Orbit follow smoke failed to build follow goal."); + + const auto applyResult = nbl::hlsl::applyFollowToCamera(m_cameraGoalSolver, orbitCamera, trackedTarget, followConfig); + if (!applyResult.succeeded()) + return fail("Orbit follow smoke failed to apply follow goal."); + if (!compareGoalToCamera(orbitCamera, followGoal, "orbit follow")) + return false; + + const auto restoreResult = nbl::hlsl::applyPresetDetailed(m_cameraGoalSolver, orbitCamera, baselinePreset); + if (!restoreResult.succeeded() || !comparePresetToCamera(orbitCamera, baselinePreset, 1e-6, 0.1, 1e-6)) + return fail("Orbit follow smoke failed to restore the baseline preset."); + } + + if (freeCamera) + { + const auto baselinePreset = nbl::hlsl::capturePreset(m_cameraGoalSolver, freeCamera, "free-follow-baseline"); + SCameraFollowConfig followConfig = {}; + followConfig.enabled = true; + followConfig.mode = ECameraFollowMode::LookAtTarget; + + CCameraGoal followGoal = {}; + if (!nbl::hlsl::tryBuildFollowGoal(m_cameraGoalSolver, freeCamera, trackedTarget, followConfig, followGoal)) + return fail("Free follow smoke failed to build follow goal."); + + const auto applyResult = nbl::hlsl::applyFollowToCamera(m_cameraGoalSolver, freeCamera, trackedTarget, followConfig); + if (!applyResult.succeeded()) + return fail("Free follow smoke failed to apply follow goal."); + if (!compareGoalToCamera(freeCamera, followGoal, "free look-at follow")) + return false; + + const auto restoreResult = nbl::hlsl::applyPresetDetailed(m_cameraGoalSolver, freeCamera, baselinePreset); + if (!restoreResult.succeeded() || !comparePresetToCamera(freeCamera, baselinePreset, 1e-6, 0.1, 1e-6)) + return fail("Free follow smoke failed to restore the baseline preset."); + } + + if (chaseCamera) + { + const auto baselinePreset = nbl::hlsl::capturePreset(m_cameraGoalSolver, chaseCamera, "chase-follow-baseline"); + SCameraFollowConfig followConfig = {}; + followConfig.enabled = true; + followConfig.mode = ECameraFollowMode::KeepLocalOffset; + if (!nbl::hlsl::captureFollowOffsetsFromCamera(m_cameraGoalSolver, chaseCamera, trackedTarget, followConfig)) + return fail("Chase follow smoke failed to capture local offset."); + + trackedTarget.setPose(float64_t3(-1.5, 0.5, 2.25), glm::quat(glm::dvec3(-0.12, 0.35, 0.27))); + + CCameraGoal followGoal = {}; + if (!nbl::hlsl::tryBuildFollowGoal(m_cameraGoalSolver, chaseCamera, trackedTarget, followConfig, followGoal)) + return fail("Chase follow smoke failed to build follow goal."); + + const auto applyResult = nbl::hlsl::applyFollowToCamera(m_cameraGoalSolver, chaseCamera, trackedTarget, followConfig); + if (!applyResult.succeeded()) + return fail("Chase follow smoke failed to apply follow goal."); + if (!compareGoalToCamera(chaseCamera, followGoal, "chase local-offset follow")) + return false; + + const auto restoreResult = nbl::hlsl::applyPresetDetailed(m_cameraGoalSolver, chaseCamera, baselinePreset); + if (!restoreResult.succeeded() || !comparePresetToCamera(chaseCamera, baselinePreset, 1e-6, 0.1, 1e-6)) + return fail("Chase follow smoke failed to restore the baseline preset."); + } + } + if (hasOrbitPreset && hasChasePreset) { if (!verifyExactCrossKindApply(orbitCamera, initialChasePreset, "Chase->Orbit")) @@ -2034,6 +2122,19 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) m_initialPlanarPresets.emplace_back(nbl::hlsl::capturePreset(m_cameraGoalSolver, camera, presetName)); } + resetFollowTargetToModel(); + m_planarFollowConfigs.clear(); + m_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 = makeDefaultFollowConfig(camera); + m_planarFollowConfigs.emplace_back(config); + if (config.enabled) + captureFollowOffsetsForPlanar(planarIx); + } + bindManipulatedModel(); + if (pendingScriptedSequence.has_value()) { auto expandCameraSequenceScript = [&](const CCameraSequenceScript& sequence) -> bool @@ -2894,17 +2995,22 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) { const auto& pipelines = m_renderer->getInitParams().pipelines; m_gridGeometryIx = std::nullopt; + m_followTargetGeometryIx = std::nullopt; auto ix = 0u; for (const auto& name : m_scene->getInitParams().geometryNames) { if (name == "Cone") + { m_renderer->getGeometry(ix).pipeline = pipelines[CSimpleDebugRenderer::SInitParams::PipelineType::Cone]; + if (!m_followTargetGeometryIx.has_value()) + m_followTargetGeometryIx = ix; + } else if (name == "Grid") m_gridGeometryIx = ix; ix++; } } - m_renderer->m_instances.resize(m_gridGeometryIx.has_value() ? 2u : 1u); + m_renderer->m_instances.resize(1u + (m_gridGeometryIx.has_value() ? 1u : 0u) + (m_followTargetGeometryIx.has_value() ? 1u : 0u)); const auto dpyInfo = m_winMgr->getPrimaryDisplayInfo(); for (uint32_t i = 0u; i < windowBindings.size(); ++i) diff --git a/61_UI/AppTransformEditor.cpp b/61_UI/AppTransformEditor.cpp index 7e2340a21..02618fe4d 100644 --- a/61_UI/AppTransformEditor.cpp +++ b/61_UI/AppTransformEditor.cpp @@ -7,48 +7,33 @@ void App::TransformEditorContents() static bool boundSizing = false; static bool boundSizingSnap = false; - const size_t objectsCount = m_planarProjections.size() + 1u; + const size_t objectsCount = getManipulableObjectCount(); assert(objectsCount); std::vector sbels(objectsCount); for (size_t i = 0; i < objectsCount; ++i) - sbels[i] = "Object " + std::to_string(i); + sbels[i] = getManipulableObjectLabel(static_cast(i)); std::vector labels(objectsCount); for (size_t i = 0; i < objectsCount; ++i) labels[i] = sbels[i].c_str(); - int activeObject = boundCameraToManipulate ? static_cast(boundPlanarCameraIxToManipulate.value() + 1u) : 0; + int activeObject = static_cast(getManipulatedObjectIx()); if (ImGui::Combo("Active Object", &activeObject, labels.data(), static_cast(labels.size()))) - { - const auto newActiveObject = static_cast(activeObject); - - if (newActiveObject) // camera - { - boundPlanarCameraIxToManipulate = newActiveObject - 1u; - ICamera* const targetGimbalManipulationCamera = m_planarProjections[boundPlanarCameraIxToManipulate.value()]->getCamera(); - boundCameraToManipulate = smart_refctd_ptr(targetGimbalManipulationCamera); - } - else // gc model - { - boundPlanarCameraIxToManipulate = std::nullopt; - boundCameraToManipulate = nullptr; - } - } + bindManipulatedObjectByIx(static_cast(activeObject)); ImGuizmoModelM16InOut imguizmoModel; - if (boundCameraToManipulate) - imguizmoModel.inTRS = getCastedMatrix(boundCameraToManipulate->getGimbal().template operator() < float64_t4x4 > ()); - else - imguizmoModel.inTRS = hlsl::transpose(getMatrix3x4As4x4(m_model)); + imguizmoModel.inTRS = getManipulableObjectTransform(static_cast(activeObject)); imguizmoModel.outTRS = imguizmoModel.inTRS; float* m16TRSmatrix = &imguizmoModel.outTRS[0][0]; std::string indent; - if (boundCameraToManipulate) + if (m_manipulatedObjectKind == SceneManipulatedObjectKind::Camera && boundCameraToManipulate) indent = boundCameraToManipulate->getIdentifier(); + else if (m_manipulatedObjectKind == SceneManipulatedObjectKind::FollowTarget) + indent = m_followTarget.getIdentifier(); else indent = "Geometry Creator Object"; @@ -84,7 +69,7 @@ void App::TransformEditorContents() ImGui::Separator(); - if (!boundCameraToManipulate) + if (m_manipulatedObjectKind == SceneManipulatedObjectKind::Model) { const auto& names = m_scene->getInitParams().geometryNames; if (!names.empty()) @@ -162,7 +147,7 @@ void App::TransformEditorContents() } // generate virtual events given delta TRS matrix - if (boundCameraToManipulate) + if (m_manipulatedObjectKind == SceneManipulatedObjectKind::Camera && boundCameraToManipulate) { auto referenceFrame = getCastedMatrix(imguizmoModel.outTRS); boundCameraToManipulate->manipulateWithUnitMotionScales({}, &referenceFrame); @@ -209,9 +194,13 @@ void App::TransformEditorContents() } */ } + else if (m_manipulatedObjectKind == SceneManipulatedObjectKind::FollowTarget) + { + setFollowTargetTransform(getCastedMatrix(imguizmoModel.outTRS)); + applyFollowToConfiguredCameras(); + } else { - // for scene demo model full affine transformation without limits is assumed m_model = float32_t3x4(hlsl::transpose(imguizmoModel.outTRS)); } diff --git a/61_UI/AppUpdate.cpp b/61_UI/AppUpdate.cpp index 3b183c4b4..497a1530b 100644 --- a/61_UI/AppUpdate.cpp +++ b/61_UI/AppUpdate.cpp @@ -503,6 +503,8 @@ void App::update() } } + applyFollowToConfiguredCameras(); + if (m_scriptedInput.enabled && m_scriptedInput.nextCheckIndex < m_scriptedInput.checks.size()) { auto* camera = [&]() -> ICamera* diff --git a/61_UI/AppWorkLoop.cpp b/61_UI/AppWorkLoop.cpp index 62b4a53f3..7d89a68d8 100644 --- a/61_UI/AppWorkLoop.cpp +++ b/61_UI/AppWorkLoop.cpp @@ -160,12 +160,13 @@ void App::workLoopBody() instance.packedGeo = m_renderer->getGeometries().data() + gcIndex; } - if (m_gridGeometryIx.has_value() && m_renderer->m_instances.size() > 1u) + const uint32_t gridInstanceIx = 1u; + if (m_gridGeometryIx.has_value() && m_renderer->m_instances.size() > gridInstanceIx) { const auto gridIx = m_gridGeometryIx.value(); if (gridIx < geomCount) { - auto& gridInstance = m_renderer->m_instances[1]; + auto& gridInstance = m_renderer->m_instances[gridInstanceIx]; gridInstance.packedGeo = m_renderer->getGeometries().data() + gridIx; constexpr float gridExtent = 32.0f; @@ -176,6 +177,22 @@ void App::workLoopBody() gridInstance.world = gridWorld; } } + + const uint32_t followInstanceIx = 1u + (m_gridGeometryIx.has_value() ? 1u : 0u); + if (m_renderer->m_instances.size() > followInstanceIx) + { + auto& followInstance = m_renderer->m_instances[followInstanceIx]; + if (m_followTargetVisible && m_followTargetGeometryIx.has_value() && m_followTargetGeometryIx.value() < geomCount) + { + followInstance.packedGeo = m_renderer->getGeometries().data() + m_followTargetGeometryIx.value(); + followInstance.world = computeFollowTargetMarkerWorld(); + } + else + { + followInstance.packedGeo = nullptr; + followInstance.world = float32_t3x4(1.0f); + } + } } if (useWindow) diff --git a/61_UI/README.md b/61_UI/README.md index 4acb83282..f41b612e0 100644 --- a/61_UI/README.md +++ b/61_UI/README.md @@ -18,6 +18,7 @@ That shared layer covers: - reusable camera kinds and typed state hooks - best-effort goal capture and apply utilities - preset and keyframe-track storage helpers +- tracked-target and follow helpers built on top of shared goals At the moment other examples are not being migrated yet. The reusable API is growing in `common/include/camera`, while `61_UI` stays the only active call-site. @@ -40,6 +41,28 @@ The reusable API is growing in `common/include/camera`, while `61_UI` stays the Each planar uses one of the configured input binding layouts and can be switched at runtime by scripted `action` events. +## Follow target integration + +`61_UI` also exposes one tracked target in the default scene. + +That target owns its own gimbal and is integrated through the shared follow layer rather than +through direct camera hacks. The example can: + +- manipulate the tracked target with the scene gizmo +- show a marker for the tracked target +- let selected cameras follow it through reusable follow modes + +The current default follow setup is: + +- `Orbit`, `Arcball`, `Turntable`, `TopDown`, `Isometric`, `DollyZoom`, `Path` + use `OrbitTarget` +- `Chase`, `Dolly` + use `KeepLocalOffset` + +The follow layer is live-scene behavior only for now. +Scripted continuity/CI runs intentionally do not re-apply follow every frame yet, because the +current sequence asset does not author target animation separately from camera motion. + ## Short math context Each camera is represented by a gimbal pose `(R, p)` and produces a view matrix from camera basis vectors and position. diff --git a/61_UI/include/app/App.hpp b/61_UI/include/app/App.hpp index 5899968e5..ca69cadca 100644 --- a/61_UI/include/app/App.hpp +++ b/61_UI/include/app/App.hpp @@ -505,6 +505,13 @@ class App final : public examples::SimpleWindowedApplication } }; + enum class SceneManipulatedObjectKind : uint8_t + { + Model, + FollowTarget, + Camera + }; + struct CameraControlSettings { bool mirrorInput = false; @@ -525,6 +532,246 @@ class App final : public examples::SimpleWindowedApplication return planar ? planar->getCamera() : nullptr; } + inline uint32_t getActivePlanarIx() const + { + return windowBindings[activeRenderWindowIx].activePlanarIx; + } + + inline SCameraFollowConfig* getActiveFollowConfig() + { + const auto planarIx = getActivePlanarIx(); + if (planarIx >= m_planarFollowConfigs.size()) + return nullptr; + return &m_planarFollowConfigs[planarIx]; + } + + inline const SCameraFollowConfig* getActiveFollowConfig() const + { + const auto planarIx = getActivePlanarIx(); + if (planarIx >= m_planarFollowConfigs.size()) + return nullptr; + return &m_planarFollowConfigs[planarIx]; + } + + inline uint32_t getManipulableObjectCount() const + { + return 2u + static_cast(m_planarProjections.size()); + } + + inline bool isManipulableObjectFollowTarget(const uint32_t objectIx) const + { + return objectIx == 1u; + } + + inline std::optional getManipulableObjectPlanarIx(const uint32_t objectIx) const + { + if (objectIx < 2u) + return std::nullopt; + const auto planarIx = objectIx - 2u; + if (planarIx >= m_planarProjections.size()) + return std::nullopt; + return planarIx; + } + + inline uint32_t getManipulatedObjectIx() const + { + switch (m_manipulatedObjectKind) + { + case SceneManipulatedObjectKind::Model: + return 0u; + case SceneManipulatedObjectKind::FollowTarget: + return 1u; + case SceneManipulatedObjectKind::Camera: + default: + return boundPlanarCameraIxToManipulate.has_value() ? (boundPlanarCameraIxToManipulate.value() + 2u) : 0u; + } + } + + inline void bindManipulatedModel() + { + m_manipulatedObjectKind = SceneManipulatedObjectKind::Model; + boundCameraToManipulate = nullptr; + boundPlanarCameraIxToManipulate = std::nullopt; + } + + inline void bindManipulatedFollowTarget() + { + m_manipulatedObjectKind = SceneManipulatedObjectKind::FollowTarget; + boundCameraToManipulate = nullptr; + boundPlanarCameraIxToManipulate = std::nullopt; + } + + inline void 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_manipulatedObjectKind = SceneManipulatedObjectKind::Camera; + boundPlanarCameraIxToManipulate = planarIx; + boundCameraToManipulate = smart_refctd_ptr(camera); + } + + inline void bindManipulatedObjectByIx(const uint32_t objectIx) + { + if (objectIx == 0u) + return bindManipulatedModel(); + if (isManipulableObjectFollowTarget(objectIx)) + return bindManipulatedFollowTarget(); + if (const auto planarIx = getManipulableObjectPlanarIx(objectIx); planarIx.has_value()) + return bindManipulatedCamera(planarIx.value()); + bindManipulatedModel(); + } + + inline std::string getManipulableObjectLabel(const uint32_t objectIx) const + { + if (objectIx == 0u) + return "Model"; + if (isManipulableObjectFollowTarget(objectIx)) + return m_followTarget.getIdentifier(); + if (const auto planarIx = getManipulableObjectPlanarIx(objectIx); planarIx.has_value()) + { + auto* camera = m_planarProjections[planarIx.value()] ? m_planarProjections[planarIx.value()]->getCamera() : nullptr; + if (!camera) + return "Camera " + std::to_string(planarIx.value()); + return std::string(getCameraTypeLabel(camera)) + " Camera"; + } + return "Unknown"; + } + + inline float32_t4x4 getManipulableObjectTransform(const uint32_t objectIx) const + { + if (objectIx == 0u) + return hlsl::transpose(getMatrix3x4As4x4(m_model)); + if (isManipulableObjectFollowTarget(objectIx)) + return getCastedMatrix(m_followTarget.getGimbal().template operator()()); + + if (const auto planarIx = getManipulableObjectPlanarIx(objectIx); planarIx.has_value()) + { + auto* camera = m_planarProjections[planarIx.value()] ? m_planarProjections[planarIx.value()]->getCamera() : nullptr; + if (camera) + return getCastedMatrix(camera->getGimbal().template operator()()); + } + + return float32_t4x4(1.0f); + } + + inline float32_t3 getManipulableObjectWorldPosition(const uint32_t objectIx) const + { + if (objectIx == 0u) + { + const auto modelPos = hlsl::transpose(getMatrix3x4As4x4(m_model))[3]; + return float32_t3(modelPos.x, modelPos.y, modelPos.z); + } + if (isManipulableObjectFollowTarget(objectIx)) + return getCastedVector(m_followTarget.getGimbal().getPosition()); + + if (const auto planarIx = getManipulableObjectPlanarIx(objectIx); planarIx.has_value()) + { + auto* camera = m_planarProjections[planarIx.value()] ? m_planarProjections[planarIx.value()]->getCamera() : nullptr; + if (camera) + return getCastedVector(camera->getGimbal().getPosition()); + } + + return float32_t3(0.0f); + } + + inline float32_t3x4 computeFollowTargetMarkerWorld() const + { + auto markerWorld = getCastedMatrix(m_followTarget.getGimbal().operator()()); + const float32_t3x4 markerLocal = { + float32_t4(0.22f, 0.0f, 0.0f, 0.0f), + float32_t4(0.0f, 0.35f, 0.0f, 0.45f), + float32_t4(0.0f, 0.0f, 0.22f, 0.0f) + }; + return concatenateBFollowedByA(markerLocal, markerWorld); + } + + inline void setFollowTargetTransform(const float64_t4x4& transform) + { + m_followTarget.trySetFromTransform(transform); + } + + inline bool captureFollowOffsetsForPlanar(const uint32_t planarIx) + { + if (planarIx >= m_planarProjections.size() || planarIx >= m_planarFollowConfigs.size()) + return false; + auto* camera = m_planarProjections[planarIx] ? m_planarProjections[planarIx]->getCamera() : nullptr; + return nbl::hlsl::captureFollowOffsetsFromCamera(m_cameraGoalSolver, camera, m_followTarget, m_planarFollowConfigs[planarIx]); + } + + inline void resetFollowTargetToModel() + { + const auto modelTransform = hlsl::transpose(getMatrix3x4As4x4(m_model)); + setFollowTargetTransform(getCastedMatrix(modelTransform)); + } + + inline SCameraFollowConfig makeDefaultFollowConfig(ICamera* camera) + { + SCameraFollowConfig config = {}; + if (!camera) + return config; + + switch (camera->getKind()) + { + case ICamera::CameraKind::Orbit: + case ICamera::CameraKind::Arcball: + case ICamera::CameraKind::Turntable: + case ICamera::CameraKind::TopDown: + case ICamera::CameraKind::Isometric: + case ICamera::CameraKind::DollyZoom: + case ICamera::CameraKind::Path: + config.enabled = true; + config.mode = ECameraFollowMode::OrbitTarget; + break; + case ICamera::CameraKind::Chase: + case ICamera::CameraKind::Dolly: + config.enabled = true; + config.mode = ECameraFollowMode::KeepLocalOffset; + break; + default: + break; + } + + return config; + } + + inline void applyFollowToConfiguredCameras() + { + if (m_scriptedInput.enabled) + return; + if (m_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_planarFollowConfigs[planarIx]; + if (!config.enabled || config.mode == ECameraFollowMode::Disabled) + continue; + + const auto result = nbl::hlsl::applyFollowToCamera(m_cameraGoalSolver, camera, m_followTarget, config); + if (!result.succeeded()) + continue; + + for (auto& projection : planar->getPlanarProjections()) + nbl::hlsl::syncDynamicPerspectiveProjection(camera, projection); + } + } + inline bool isOrbitLikeCamera(ICamera* camera) { return camera && camera->hasCapability(ICamera::SphericalTarget); @@ -1438,9 +1685,12 @@ class App final : public examples::SimpleWindowedApplication // one model object in the world, testing multiuple cameraz for which view is rendered to separate frame buffers (so what they see) with new controller API including imguizmo nbl::hlsl::float32_t3x4 m_model = nbl::hlsl::float32_t3x4(1.f); + CTrackedTarget m_followTarget; + std::vector m_planarFollowConfigs; + bool m_followTargetVisible = true; + std::optional m_followTargetGeometryIx = std::nullopt; + SceneManipulatedObjectKind m_manipulatedObjectKind = SceneManipulatedObjectKind::Model; - // if we had working IObjectTransform or something similar then it would be it instead, it is "last manipulated object" I need for TRS editor - // in reality we should store range of those IObjectTransforem interface range & index to object representing last manipulated one nbl::core::smart_refctd_ptr boundCameraToManipulate = nullptr; std::optional boundPlanarCameraIxToManipulate = std::nullopt; diff --git a/61_UI/include/common.hpp b/61_UI/include/common.hpp index 24b56cca1..b5cb44aec 100644 --- a/61_UI/include/common.hpp +++ b/61_UI/include/common.hpp @@ -29,6 +29,7 @@ #include "camera/CCameraManipulationUtilities.hpp" #include "camera/CCameraPresentationUtilities.hpp" #include "camera/CCameraProjectionUtilities.hpp" +#include "camera/CCameraFollowUtilities.hpp" #include "camera/CCameraTextUtilities.hpp" #include "camera/CGimbalInputBinder.hpp" @@ -67,6 +68,7 @@ using nbl::hlsl::CDollyCamera; using nbl::hlsl::CDollyZoomCamera; using nbl::hlsl::CPathCamera; using nbl::hlsl::CCameraGoal; +using nbl::hlsl::CTrackedTarget; using nbl::hlsl::CCameraPreset; using nbl::hlsl::CCameraKeyframe; using nbl::hlsl::CCameraKeyframeTrack; @@ -80,12 +82,14 @@ using nbl::hlsl::SCameraPlaybackAdvanceResult; using nbl::hlsl::SCameraPresetApplySummary; using nbl::hlsl::SCameraGoalApplyAnalysis; using nbl::hlsl::SCameraCaptureAnalysis; +using nbl::hlsl::SCameraFollowConfig; using nbl::hlsl::SCameraGoalApplyPresentation; using nbl::hlsl::SCameraGoalApplyPresentationBadges; using nbl::hlsl::SCameraCapturePresentation; using nbl::hlsl::SCameraConstraintSettings; using nbl::hlsl::CCameraGoalSolver; using nbl::hlsl::CGimbalInputBinder; +using nbl::hlsl::ECameraFollowMode; using nbl::hlsl::EPresetApplyPresentationFilter; using nbl::hlsl::IGimbalBindingLayout; using nbl::hlsl::IPlanarProjection; @@ -104,11 +108,14 @@ using nbl::hlsl::float64_t4x4; using nbl::hlsl::uint16_t2; using nbl::hlsl::describeApplyResult; using nbl::hlsl::describeGoalStateMask; +using nbl::hlsl::getCameraFollowModeLabel; using nbl::hlsl::getCameraTypeDescription; using nbl::hlsl::getCameraTypeLabel; using nbl::hlsl::getCastedMatrix; using nbl::hlsl::getCastedVector; using nbl::hlsl::getMatrix3x4As4x4; +using nbl::hlsl::cameraFollowModeUsesLocalOffset; +using nbl::hlsl::cameraFollowModeUsesWorldOffset; using nbl::hlsl::concatenateBFollowedByA; using nbl::hlsl::mul; using nbl::hlsl::syncDynamicPerspectiveProjection; diff --git a/common/include/camera/CCameraFollowUtilities.hpp b/common/include/camera/CCameraFollowUtilities.hpp new file mode 100644 index 000000000..33082a30e --- /dev/null +++ b/common/include/camera/CCameraFollowUtilities.hpp @@ -0,0 +1,331 @@ +// 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_FOLLOW_UTILITIES_HPP_ +#define _C_CAMERA_FOLLOW_UTILITIES_HPP_ + +#include +#include + +#include "CCameraGoalSolver.hpp" + +namespace nbl::hlsl +{ + +/** +* Reusable tracked-target and follow helpers layered on top of the shared camera API. +* +* The tracked subject owns its own gimbal. Follow stays outside `ICamera` and maps +* a camera plus tracked target into a `CCameraGoal`. +*/ +class CTrackedTarget +{ +public: + using gimbal_t = ICamera::CGimbal; + + CTrackedTarget( + const float64_t3& position = float64_t3(0.0), + const glm::quat& orientation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f), + std::string identifier = "Follow Target") + : m_identifier(std::move(identifier)), + m_gimbal({ .position = position, .orientation = orientation }) + { + m_gimbal.updateView(); + } + + inline const std::string& getIdentifier() const { return m_identifier; } + inline const gimbal_t& getGimbal() const { return m_gimbal; } + inline gimbal_t& getGimbal() { return m_gimbal; } + + inline void setPose(const float64_t3& position, const glm::quat& orientation) + { + m_gimbal.begin(); + m_gimbal.setPosition(position); + m_gimbal.setOrientation(orientation); + m_gimbal.end(); + m_gimbal.updateView(); + } + + inline void setPosition(const float64_t3& position) + { + setPose(position, m_gimbal.getOrientation()); + } + + inline void setOrientation(const glm::quat& orientation) + { + setPose(m_gimbal.getPosition(), orientation); + } + + inline bool trySetFromTransform(const float64_t4x4& transform) + { + const auto right = normalize(float64_t3(transform[0])); + const auto up = normalize(float64_t3(transform[1])); + const auto forward = normalize(float64_t3(transform[2])); + if (!isOrthoBase(right, up, forward)) + return false; + + setPose(float64_t3(transform[3]), glm::quat_cast(glm::dmat3{ right, up, forward })); + return true; + } + +private: + std::string m_identifier; + gimbal_t m_gimbal; +}; + +enum class ECameraFollowMode : uint8_t +{ + Disabled, + OrbitTarget, + LookAtTarget, + KeepWorldOffset, + KeepLocalOffset +}; + +struct SCameraFollowConfig +{ + bool enabled = false; + ECameraFollowMode mode = ECameraFollowMode::OrbitTarget; + float64_t3 worldOffset = float64_t3(0.0); + float64_t3 localOffset = float64_t3(0.0); +}; + +inline constexpr const char* getCameraFollowModeLabel(const ECameraFollowMode mode) +{ + switch (mode) + { + case ECameraFollowMode::Disabled: return "Disabled"; + case ECameraFollowMode::OrbitTarget: return "Orbit target"; + case ECameraFollowMode::LookAtTarget: return "Look at target"; + case ECameraFollowMode::KeepWorldOffset: return "Keep world offset"; + case ECameraFollowMode::KeepLocalOffset: return "Keep local offset"; + default: return "Unknown"; + } +} + +inline constexpr bool cameraFollowModeUsesWorldOffset(const ECameraFollowMode mode) +{ + return mode == ECameraFollowMode::KeepWorldOffset; +} + +inline constexpr bool cameraFollowModeUsesLocalOffset(const ECameraFollowMode mode) +{ + return mode == ECameraFollowMode::KeepLocalOffset; +} + +inline float64_t3 transformFollowLocalOffset(const ICamera::CGimbal& gimbal, const float64_t3& localOffset) +{ + return gimbal.getXAxis() * localOffset.x + + gimbal.getYAxis() * localOffset.y + + gimbal.getZAxis() * localOffset.z; +} + +inline float64_t3 projectFollowWorldOffsetToLocal(const ICamera::CGimbal& gimbal, const float64_t3& worldOffset) +{ + return float64_t3( + hlsl::dot(worldOffset, gimbal.getXAxis()), + hlsl::dot(worldOffset, gimbal.getYAxis()), + hlsl::dot(worldOffset, gimbal.getZAxis())); +} + +inline bool buildFollowLookAtOrientation( + const float64_t3& position, + const float64_t3& targetPosition, + const float64_t3& preferredUp, + glm::quat& outOrientation) +{ + const auto toTarget = targetPosition - position; + const double toTargetLength = length(toTarget); + if (!std::isfinite(toTargetLength) || toTargetLength <= 1e-9) + return false; + + const auto forward = toTarget / toTargetLength; + auto up = preferredUp; + if (!isFiniteVec3(up) || length(up) <= 1e-9) + up = float64_t3(0.0, 0.0, 1.0); + else + up = normalize(up); + + auto right = cross(up, forward); + if (!isFiniteVec3(right) || length(right) <= 1e-9) + { + const auto fallbackUp = std::abs(forward.z) < 0.99 ? float64_t3(0.0, 0.0, 1.0) : float64_t3(0.0, 1.0, 0.0); + right = cross(fallbackUp, forward); + if (!isFiniteVec3(right) || length(right) <= 1e-9) + return false; + } + right = normalize(right); + up = normalize(cross(forward, right)); + if (!isOrthoBase(right, up, forward)) + return false; + + outOrientation = glm::quat_cast(glm::dmat3{ right, up, forward }); + return true; +} + +inline bool applyFollowSphericalPose( + CCameraGoal& goal, + const float64_t3& targetPosition, + const double orbitU, + const double orbitV, + const float distance) +{ + if (!std::isfinite(orbitU) || !std::isfinite(orbitV) || !std::isfinite(distance)) + return false; + + const float clampedDistance = std::clamp(distance, CSphericalTargetCamera::MinDistance, CSphericalTargetCamera::MaxDistance); + const float64_t3 spherePosition( + std::cos(orbitV) * std::cos(orbitU) * static_cast(clampedDistance), + std::cos(orbitV) * std::sin(orbitU) * static_cast(clampedDistance), + std::sin(orbitV) * static_cast(clampedDistance)); + + const auto forward = normalize(-spherePosition); + const auto up = normalize(float64_t3( + -std::sin(orbitV) * std::cos(orbitU), + -std::sin(orbitV) * std::sin(orbitU), + std::cos(orbitV))); + const auto right = normalize(cross(up, forward)); + if (!isOrthoBase(right, up, forward)) + return false; + + goal.hasTargetPosition = true; + goal.targetPosition = targetPosition; + goal.hasDistance = true; + goal.distance = clampedDistance; + goal.hasOrbitState = true; + goal.orbitU = orbitU; + goal.orbitV = orbitV; + goal.orbitDistance = clampedDistance; + goal.position = targetPosition + spherePosition; + goal.orientation = glm::quat_cast(glm::dmat3{ right, up, forward }); + return true; +} + +inline bool buildFollowSphericalGoalFromPose(CCameraGoal& goal, const float64_t3& targetPosition, const float64_t3& position) +{ + const auto offset = position - targetPosition; + const double distance = length(offset); + if (!std::isfinite(distance) || distance <= 1e-9) + return false; + + const float clampedDistance = std::clamp(static_cast(distance), CSphericalTargetCamera::MinDistance, CSphericalTargetCamera::MaxDistance); + const auto local = offset / static_cast(clampedDistance); + const double orbitU = std::atan2(local.y, local.x); + const double orbitV = std::asin(std::clamp(local.z, -1.0, 1.0)); + + return applyFollowSphericalPose(goal, targetPosition, orbitU, orbitV, clampedDistance); +} + +inline bool captureFollowOffsetsFromCamera( + const CCameraGoalSolver& solver, + ICamera* camera, + const CTrackedTarget& trackedTarget, + SCameraFollowConfig& ioConfig) +{ + const auto capture = solver.captureDetailed(camera); + if (!capture.canUseGoal()) + return false; + + const auto& targetGimbal = trackedTarget.getGimbal(); + ioConfig.worldOffset = capture.goal.position - targetGimbal.getPosition(); + ioConfig.localOffset = projectFollowWorldOffsetToLocal(targetGimbal, ioConfig.worldOffset); + return true; +} + +inline bool tryBuildFollowGoal( + const CCameraGoalSolver& solver, + ICamera* camera, + const CTrackedTarget& trackedTarget, + const SCameraFollowConfig& config, + CCameraGoal& outGoal) +{ + if (!camera || !config.enabled || config.mode == ECameraFollowMode::Disabled) + return false; + + const auto capture = solver.captureDetailed(camera); + if (!capture.canUseGoal()) + return false; + + outGoal = capture.goal; + + const auto& targetGimbal = trackedTarget.getGimbal(); + const auto targetPosition = targetGimbal.getPosition(); + + switch (config.mode) + { + case ECameraFollowMode::OrbitTarget: + { + if (!camera->supportsGoalState(ICamera::GoalStateSphericalTarget)) + return false; + + if (outGoal.hasPathState) + { + outGoal.hasTargetPosition = true; + outGoal.targetPosition = targetPosition; + outGoal = canonicalizeGoal(outGoal); + return isGoalFinite(outGoal); + } + + const bool hasSphericalState = outGoal.hasOrbitState || outGoal.hasDistance; + if (!hasSphericalState) + return false; + + const auto orbitDistance = outGoal.hasOrbitState ? outGoal.orbitDistance : outGoal.distance; + return applyFollowSphericalPose(outGoal, targetPosition, outGoal.orbitU, outGoal.orbitV, orbitDistance); + } + + case ECameraFollowMode::LookAtTarget: + { + if (camera->supportsGoalState(ICamera::GoalStateSphericalTarget)) + return buildFollowSphericalGoalFromPose(outGoal, targetPosition, capture.goal.position); + + outGoal.position = capture.goal.position; + return buildFollowLookAtOrientation(outGoal.position, targetPosition, targetGimbal.getYAxis(), outGoal.orientation) && isGoalFinite(outGoal); + } + + case ECameraFollowMode::KeepWorldOffset: + { + const auto position = targetPosition + config.worldOffset; + if (camera->supportsGoalState(ICamera::GoalStateSphericalTarget)) + return buildFollowSphericalGoalFromPose(outGoal, targetPosition, position); + + outGoal.position = position; + return buildFollowLookAtOrientation(outGoal.position, targetPosition, targetGimbal.getYAxis(), outGoal.orientation) && isGoalFinite(outGoal); + } + + case ECameraFollowMode::KeepLocalOffset: + { + const auto position = targetPosition + transformFollowLocalOffset(targetGimbal, config.localOffset); + if (camera->supportsGoalState(ICamera::GoalStateSphericalTarget)) + return buildFollowSphericalGoalFromPose(outGoal, targetPosition, position); + + outGoal.position = position; + return buildFollowLookAtOrientation(outGoal.position, targetPosition, targetGimbal.getYAxis(), outGoal.orientation) && isGoalFinite(outGoal); + } + + default: + return false; + } +} + +inline CCameraGoalSolver::SApplyResult applyFollowToCamera( + const CCameraGoalSolver& solver, + ICamera* camera, + const CTrackedTarget& trackedTarget, + const SCameraFollowConfig& config, + CCameraGoal* outGoal = nullptr) +{ + CCameraGoal goal = {}; + if (!tryBuildFollowGoal(solver, camera, trackedTarget, config, goal)) + return {}; + + if (outGoal) + *outGoal = goal; + + return solver.applyDetailed(camera, goal); +} + +} // namespace nbl::hlsl + +#endif // _C_CAMERA_FOLLOW_UTILITIES_HPP_ diff --git a/common/include/camera/README.md b/common/include/camera/README.md index 9d85f378b..7c2d0b4ef 100644 --- a/common/include/camera/README.md +++ b/common/include/camera/README.md @@ -13,6 +13,7 @@ The current design goal is: - reusable playback cursor and timeline helpers live in shared headers - reusable preset and keyframe persistence helpers live in shared headers - reusable compact camera-sequence scripting helpers live in shared headers +- reusable tracked-target and follow helpers live in shared headers - `61_UI` is the current integration and validation surface ## Mental model @@ -46,6 +47,10 @@ and compact authored sequence scripts sit above the same shared camera-domain st `CCameraSequenceScript -> CCameraKeyframeTrack -> CCameraPreset -> CCameraGoal` +and tracked-target follow sits beside the same goal layer: + +`CTrackedTarget + SCameraFollowConfig -> CCameraGoal -> CCameraGoalSolver` + ## Core contracts ### `IGimbal.hpp` @@ -259,6 +264,27 @@ This keeps policy analysis out of example-local UI code. Reusable human-readable metadata and diagnostic text helpers for cameras. +### `CCameraFollowUtilities.hpp` + +Reusable tracked-target and follow helpers layered on top of the shared camera API. + +Important design points: + +- follow stays outside `ICamera` +- the tracked subject owns its own gimbal through `CTrackedTarget` +- follow modes map target motion into `CCameraGoal` +- goal application still goes through `CCameraGoalSolver` + +Current follow modes: + +- `LookAtTarget` +- `OrbitTarget` +- `KeepWorldOffset` +- `KeepLocalOffset` + +This keeps the camera runtime contract event-driven while still allowing higher-level +tracking behavior to be reused by tools and examples. + Provides: - camera-kind labels From 44d258db78da9c01ee4f6853a85da2cd3d76db2a Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Mon, 6 Apr 2026 18:04:07 +0200 Subject: [PATCH 166/205] Refine tracked target follow integration --- 61_UI/AppControlPanel.cpp | 17 +- 61_UI/AppImGuiListen.cpp | 29 +- 61_UI/AppInit.cpp | 312 ++++++++++++++++- 61_UI/AppTransformEditor.cpp | 2 + 61_UI/AppUpdate.cpp | 201 ++++++++++- 61_UI/README.md | 10 +- 61_UI/app_resources/cameraz_continuity.json | 314 +++++++++++++++++- 61_UI/include/app/App.hpp | 196 ++++++++++- 61_UI/include/common.hpp | 3 + .../CCameraFollowRegressionUtilities.hpp | 220 ++++++++++++ .../include/camera/CCameraFollowUtilities.hpp | 58 ++++ .../include/camera/CCameraSequenceScript.hpp | 242 ++++++++++++++ common/include/camera/README.md | 4 + 13 files changed, 1563 insertions(+), 45 deletions(-) create mode 100644 common/include/camera/CCameraFollowRegressionUtilities.hpp diff --git a/61_UI/AppControlPanel.cpp b/61_UI/AppControlPanel.cpp index a138722ea..5deb54e0f 100644 --- a/61_UI/AppControlPanel.cpp +++ b/61_UI/AppControlPanel.cpp @@ -671,6 +671,8 @@ void App::DrawControlPanel() if (activeFollowConfig) { auto& followConfig = *activeFollowConfig; + const bool prevFollowEnabled = followConfig.enabled; + const auto prevFollowMode = followConfig.mode; ImGui::Checkbox("Enable follow", &followConfig.enabled); DrawHoverHint("Apply tracked-target follow to the active planar camera"); @@ -684,6 +686,11 @@ void App::DrawControlPanel() 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::hlsl::cameraFollowModeUsesCapturedOffset(followConfig.mode)) + captureFollowOffsetsForPlanar(getActivePlanarIx()); + if (followStateChanged && followConfig.enabled) + applyFollowToConfiguredCameras(); auto trackedTarget = getCastedVector(m_followTarget.getGimbal().getPosition()); if (ImGui::InputFloat3("Tracked target", &trackedTarget[0])) @@ -692,9 +699,13 @@ void App::DrawControlPanel() ImGui::Checkbox("Show target marker", &m_followTargetVisible); DrawHoverHint("Render the tracked target marker in the scene"); - if (ImGui::Button("Target = model")) - resetFollowTargetToModel(); - DrawHoverHint("Snap tracked target pose to the model transform"); + if (ImGui::Button("Reset target")) + resetFollowTargetToDefault(); + DrawHoverHint("Reset tracked target gimbal to the default world-space follow pose"); + ImGui::SameLine(); + if (ImGui::Button("Snap to model")) + snapFollowTargetToModel(); + DrawHoverHint("Optionally snap tracked target gimbal to the model transform"); ImGui::SameLine(); if (ImGui::Button("Target origin")) m_followTarget.setPose(float64_t3(0.0), glm::quat(1.0f, 0.0f, 0.0f, 0.0f)); diff --git a/61_UI/AppImGuiListen.cpp b/61_UI/AppImGuiListen.cpp index 65e240c8e..496a0f40d 100644 --- a/61_UI/AppImGuiListen.cpp +++ b/61_UI/AppImGuiListen.cpp @@ -137,10 +137,13 @@ void App::imguiListen() imguizmoPlanar.projection = getCastedMatrix(hlsl::transpose(projection.getProjectionMatrix())); const auto viewMatrix = getMatrix3x4As4x4(getCastedMatrix(planarViewCameraBound->getGimbal().getViewMatrix())); const auto projectionMatrix = getCastedMatrix(projection.getProjectionMatrix()); + const auto viewProjMatrix = mul(projectionMatrix, viewMatrix); if (flipGizmoY) // note we allow to flip gizmo just to match our coordinates imguizmoPlanar.projection[1][1] *= -1.f; // https://johannesugb.github.io/gpu-programming/why-do-opengl-proj-matrices-fail-in-vulkan/ + drawFollowTargetViewportOverlay(viewProjMatrix, cursorPos, contentRegionSize); + if (!hideSceneGizmos) { for (uint32_t objectIx = 0u; objectIx < getManipulableObjectCount(); ++objectIx) @@ -282,6 +285,7 @@ void App::imguiListen() // in order for camera to not keep any magnitude scalars like move or rotation speed scales targetGimbalManipulationCamera->manipulateWithUnitMotionScales({ virtualEvents.data(), vCount }, &referenceFrame); + refreshFollowOffsetConfigForPlanar(planarIx.value()); } } @@ -415,6 +419,19 @@ void App::imguiListen() ImGui::Image(info, contentRegionSize); ImGuizmo::SetRect(cursorPos.x, cursorPos.y, contentRegionSize.x, contentRegionSize.y); + { + auto& binding = windowBindings[activeRenderWindowIx]; + auto& planarBound = m_planarProjections[binding.activePlanarIx]; + auto* planarViewCameraBound = planarBound ? planarBound->getCamera() : nullptr; + if (planarViewCameraBound && binding.boundProjectionIx.has_value()) + { + auto& projection = planarBound->getPlanarProjections()[binding.boundProjectionIx.value()]; + const auto viewMatrix = getMatrix3x4As4x4(getCastedMatrix(planarViewCameraBound->getGimbal().getViewMatrix())); + const auto projectionMatrix = getCastedMatrix(projection.getProjectionMatrix()); + const auto viewProjMatrix = mul(projectionMatrix, viewMatrix); + drawFollowTargetViewportOverlay(viewProjMatrix, cursorPos, contentRegionSize); + } + } ImGui::End(); ImGui::PopStyleColor(1); @@ -443,13 +460,13 @@ void App::imguiListen() projection.update(binding.leftHandedProjection, binding.aspectRatio); binding.isOrthographicProjection = projection.getParameters().m_type == IPlanarProjection::CProjection::Orthographic; - auto viewMatrix = getCastedMatrix(boundPlanarCamera->getGimbal().getViewMatrix()); - auto projectionMatrix = getCastedMatrix(projection.getProjectionMatrix()); - auto viewProjMatrix = mul(projectionMatrix, getMatrix3x4As4x4(viewMatrix)); + auto viewMatrix = getCastedMatrix(boundPlanarCamera->getGimbal().getViewMatrix()); + auto projectionMatrix = getCastedMatrix(projection.getProjectionMatrix()); + auto viewProjMatrix = mul(projectionMatrix, getMatrix3x4As4x4(viewMatrix)); - binding.viewMatrix = viewMatrix; - binding.projectionMatrix = projectionMatrix; - binding.viewProjMatrix = viewProjMatrix; + binding.viewMatrix = viewMatrix; + binding.projectionMatrix = projectionMatrix; + binding.viewProjMatrix = viewProjMatrix; } } diff --git a/61_UI/AppInit.cpp b/61_UI/AppInit.cpp index d894d4254..b68121c0f 100644 --- a/61_UI/AppInit.cpp +++ b/61_UI/AppInit.cpp @@ -337,6 +337,145 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) return true; }; + auto tryBuildFollowViewProjForCamera = [&](ICamera* camera, float32_t4x4& outViewProjMatrix) -> bool + { + if (!camera) + return false; + + for (uint32_t planarIx = 0u; planarIx < m_planarProjections.size(); ++planarIx) + { + auto& planar = m_planarProjections[planarIx]; + if (!planar || planar->getCamera() != camera) + continue; + + const auto& projections = planar->getPlanarProjections(); + if (projections.empty()) + return false; + + uint32_t projectionIx = 0u; + for (uint32_t ix = 0u; ix < projections.size(); ++ix) + { + if (projections[ix].getParameters().m_type == IPlanarProjection::CProjection::Perspective) + { + projectionIx = ix; + break; + } + } + + const auto viewMatrix = getMatrix3x4As4x4(getCastedMatrix(camera->getGimbal().getViewMatrix())); + const auto projectionMatrix = getCastedMatrix(projections[projectionIx].getProjectionMatrix()); + outViewProjMatrix = mul(projectionMatrix, viewMatrix); + return true; + } + + return false; + }; + + auto verifyFollowTargetContract = [&](ICamera* camera, const CTrackedTarget& trackedTarget, + const SCameraFollowConfig& followConfig, const CCameraGoal& followGoal, const char* label) -> bool + { + nbl::hlsl::SCameraFollowRegressionResult regression = {}; + std::string regressionError; + float32_t4x4 viewProjMatrix = float32_t4x4(1.0f); + const bool hasViewProjMatrix = tryBuildFollowViewProjForCamera(camera, viewProjMatrix); + if (!nbl::hlsl::validateFollowTargetContract( + camera, + trackedTarget, + followConfig, + followGoal, + regression, + ®ressionError, + 0.1f, + 1e-6, + 1e-9, + hasViewProjMatrix ? &viewProjMatrix : nullptr, + 0.03f)) + { + return fail(std::string("Follow smoke contract failed for ") + label + ". " + regressionError); + } + return true; + }; + + auto verifyFollowTargetMarkerAlignment = [&](const CTrackedTarget& trackedTarget, const char* label) -> bool + { + m_followTarget.setPose(trackedTarget.getGimbal().getPosition(), trackedTarget.getGimbal().getOrientation()); + const auto markerWorld = computeFollowTargetMarkerWorld(); + const auto markerTransform = hlsl::transpose(getMatrix3x4As4x4(markerWorld)); + const auto markerPosition = getCastedVector(float32_t3(markerTransform[3])); + const auto positionDelta = markerPosition - trackedTarget.getGimbal().getPosition(); + const auto errorLength = length(positionDelta); + if (!std::isfinite(errorLength) || errorLength > 1e-9) + { + return fail(std::string("Follow target marker alignment smoke failed for ") + label + "."); + } + return true; + }; + + auto verifyOffsetFollowRecapture = [&](ICamera* camera, const CTrackedTarget& trackedTarget, const char* label) -> bool + { + if (!camera) + return true; + + const auto baselinePreset = nbl::hlsl::capturePreset(m_cameraGoalSolver, camera, std::string(label) + " baseline"); + SCameraFollowConfig followConfig = {}; + followConfig.enabled = true; + followConfig.mode = ECameraFollowMode::KeepLocalOffset; + + if (!nbl::hlsl::captureFollowOffsetsFromCamera(m_cameraGoalSolver, camera, trackedTarget, followConfig)) + return fail(std::string("Follow recapture smoke failed to capture initial offset for ") + label + "."); + + const auto initialApply = nbl::hlsl::applyFollowToCamera(m_cameraGoalSolver, camera, trackedTarget, followConfig); + if (!initialApply.succeeded()) + return fail(std::string("Follow recapture smoke failed to apply initial follow for ") + label + "."); + + auto editedPreset = nbl::hlsl::capturePreset(m_cameraGoalSolver, camera, std::string(label) + " edited"); + if (!editedPreset.goal.hasOrbitState) + return fail(std::string("Follow recapture smoke missing orbit state for ") + label + "."); + + editedPreset.goal.orbitU = nbl::hlsl::wrapAngleRad(editedPreset.goal.orbitU + glm::radians(18.0)); + editedPreset.goal.orbitDistance = std::clamp(editedPreset.goal.orbitDistance + 0.75f, CSphericalTargetCamera::MinDistance, CSphericalTargetCamera::MaxDistance); + editedPreset.goal = nbl::hlsl::canonicalizeGoal(editedPreset.goal); + if (!nbl::hlsl::isGoalFinite(editedPreset.goal)) + return fail(std::string("Follow recapture smoke produced a non-finite edited goal for ") + label + "."); + + const auto editedApply = nbl::hlsl::applyPresetDetailed(m_cameraGoalSolver, camera, editedPreset); + if (!editedApply.succeeded() || !editedApply.changed()) + { + return fail(std::string("Follow recapture smoke failed to apply edited preset for ") + label + + ". " + describeApplyResult(editedApply)); + } + + const auto reachedEditedPreset = nbl::hlsl::capturePreset(m_cameraGoalSolver, camera, std::string(label) + " reached"); + + if (!nbl::hlsl::captureFollowOffsetsFromCamera(m_cameraGoalSolver, camera, trackedTarget, followConfig)) + return fail(std::string("Follow recapture smoke failed to recapture offset for ") + label + "."); + + CCameraGoal recapturedGoal = {}; + if (!nbl::hlsl::tryBuildFollowGoal(m_cameraGoalSolver, camera, trackedTarget, followConfig, recapturedGoal)) + return fail(std::string("Follow recapture smoke failed to rebuild follow goal for ") + label + "."); + + const auto recapturedApply = nbl::hlsl::applyFollowToCamera(m_cameraGoalSolver, camera, trackedTarget, followConfig); + if (!recapturedApply.succeeded()) + { + return fail(std::string("Follow recapture smoke failed to apply recaptured follow for ") + label + + ". " + describeApplyResult(recapturedApply)); + } + + if (!comparePresetToCamera(camera, reachedEditedPreset, 5e-6, 0.1, 5e-6)) + return fail(std::string("Follow recapture smoke mismatch for ") + label + ". " + describePresetMismatch(camera, reachedEditedPreset)); + if (!verifyFollowTargetContract(camera, trackedTarget, followConfig, recapturedGoal, label)) + return false; + + const auto restoreResult = nbl::hlsl::applyPresetDetailed(m_cameraGoalSolver, camera, baselinePreset); + if (!restoreResult.succeeded() || !comparePresetToCamera(camera, baselinePreset, 1e-6, 0.1, 1e-6)) + { + return fail(std::string("Follow recapture smoke failed to restore baseline for ") + label + + ". " + describeApplyResult(restoreResult) + " " + describePresetMismatch(camera, baselinePreset)); + } + + return true; + }; + auto collectKeyboardVirtualEvents = [&](CGimbalInputBinder& inputBinder, const ui::E_KEY_CODE keyCode) -> std::vector { static std::chrono::microseconds smokeTimestamp = std::chrono::microseconds::zero(); @@ -736,6 +875,9 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) glm::quat(glm::dvec3(0.18, -0.22, 0.41)), "Smoke Target"); + const auto movedTrackedTargetPosition = float64_t3(-1.5, 0.5, 2.25); + const auto movedTrackedTargetOrientation = glm::quat(glm::dvec3(-0.12, 0.35, 0.27)); + if (orbitCamera) { const auto baselinePreset = nbl::hlsl::capturePreset(m_cameraGoalSolver, orbitCamera, "orbit-follow-baseline"); @@ -752,10 +894,75 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) return fail("Orbit follow smoke failed to apply follow goal."); if (!compareGoalToCamera(orbitCamera, followGoal, "orbit follow")) return false; + if (!verifyFollowTargetContract(orbitCamera, trackedTarget, followConfig, followGoal, "orbit follow")) + return false; + if (!verifyFollowTargetMarkerAlignment(trackedTarget, "orbit follow")) + return false; const auto restoreResult = nbl::hlsl::applyPresetDetailed(m_cameraGoalSolver, orbitCamera, baselinePreset); if (!restoreResult.succeeded() || !comparePresetToCamera(orbitCamera, baselinePreset, 1e-6, 0.1, 1e-6)) return fail("Orbit follow smoke failed to restore the baseline preset."); + + followConfig.mode = ECameraFollowMode::KeepWorldOffset; + followConfig.worldOffset = float64_t3(4.0, -1.5, 2.0); + trackedTarget.setPose(movedTrackedTargetPosition, movedTrackedTargetOrientation); + + CCameraGoal worldOffsetGoal = {}; + if (!nbl::hlsl::tryBuildFollowGoal(m_cameraGoalSolver, orbitCamera, trackedTarget, followConfig, worldOffsetGoal)) + return fail("Orbit keep-world-offset smoke failed to build follow goal."); + + const auto worldOffsetApplyResult = nbl::hlsl::applyFollowToCamera(m_cameraGoalSolver, orbitCamera, trackedTarget, followConfig); + if (!worldOffsetApplyResult.succeeded()) + return fail("Orbit keep-world-offset smoke failed to apply follow goal."); + if (!compareGoalToCamera(orbitCamera, worldOffsetGoal, "orbit keep-world-offset follow")) + return false; + if (!verifyFollowTargetContract(orbitCamera, trackedTarget, followConfig, worldOffsetGoal, "orbit keep-world-offset follow")) + return false; + + const auto restoreWorldOffsetResult = nbl::hlsl::applyPresetDetailed(m_cameraGoalSolver, orbitCamera, baselinePreset); + if (!restoreWorldOffsetResult.succeeded() || !comparePresetToCamera(orbitCamera, baselinePreset, 1e-6, 0.1, 1e-6)) + return fail("Orbit keep-world-offset smoke failed to restore the baseline preset."); + } + + 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::hlsl::capturePreset(m_cameraGoalSolver, defaultFollowCamera, label + " baseline"); + + trackedTarget.setPose(float64_t3(2.25, -0.75, 1.25), glm::quat(glm::dvec3(0.18, -0.22, 0.41))); + if ((nbl::hlsl::cameraFollowModeUsesLocalOffset(followConfig.mode) || nbl::hlsl::cameraFollowModeUsesWorldOffset(followConfig.mode)) && + !nbl::hlsl::captureFollowOffsetsFromCamera(m_cameraGoalSolver, defaultFollowCamera, trackedTarget, followConfig)) + { + return fail("Default follow smoke failed to capture offsets for camera \"" + std::string(defaultFollowCamera->getIdentifier()) + "\"."); + } + + trackedTarget.setPose(movedTrackedTargetPosition, movedTrackedTargetOrientation); + + CCameraGoal followGoal = {}; + if (!nbl::hlsl::tryBuildFollowGoal(m_cameraGoalSolver, defaultFollowCamera, trackedTarget, followConfig, followGoal)) + return fail("Default follow smoke failed to build follow goal for camera \"" + std::string(defaultFollowCamera->getIdentifier()) + "\"."); + + const auto applyResult = nbl::hlsl::applyFollowToCamera(m_cameraGoalSolver, defaultFollowCamera, trackedTarget, followConfig); + if (!applyResult.succeeded()) + return fail("Default follow smoke failed to apply follow goal for camera \"" + std::string(defaultFollowCamera->getIdentifier()) + "\"."); + if (!compareGoalToCamera(defaultFollowCamera, followGoal, label.c_str())) + return false; + if (!verifyFollowTargetContract(defaultFollowCamera, trackedTarget, followConfig, followGoal, label.c_str())) + return false; + if (!verifyFollowTargetMarkerAlignment(trackedTarget, label.c_str())) + return false; + + const auto restoreResult = nbl::hlsl::applyPresetDetailed(m_cameraGoalSolver, defaultFollowCamera, baselinePreset); + if (!restoreResult.succeeded() || !comparePresetToCamera(defaultFollowCamera, baselinePreset, 1e-6, 0.1, 1e-6)) + return fail("Default follow smoke failed to restore the baseline preset for camera \"" + std::string(defaultFollowCamera->getIdentifier()) + "\"."); } if (freeCamera) @@ -774,10 +981,34 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) return fail("Free follow smoke failed to apply follow goal."); if (!compareGoalToCamera(freeCamera, followGoal, "free look-at follow")) return false; + if (!verifyFollowTargetContract(freeCamera, trackedTarget, followConfig, followGoal, "free look-at follow")) + return false; + if (!verifyFollowTargetMarkerAlignment(trackedTarget, "free look-at follow")) + return false; const auto restoreResult = nbl::hlsl::applyPresetDetailed(m_cameraGoalSolver, freeCamera, baselinePreset); if (!restoreResult.succeeded() || !comparePresetToCamera(freeCamera, baselinePreset, 1e-6, 0.1, 1e-6)) return fail("Free follow smoke failed to restore the baseline preset."); + + followConfig.mode = ECameraFollowMode::KeepWorldOffset; + followConfig.worldOffset = float64_t3(5.0, -2.0, 1.5); + trackedTarget.setPose(movedTrackedTargetPosition, movedTrackedTargetOrientation); + + CCameraGoal keepWorldGoal = {}; + if (!nbl::hlsl::tryBuildFollowGoal(m_cameraGoalSolver, freeCamera, trackedTarget, followConfig, keepWorldGoal)) + return fail("Free keep-world-offset smoke failed to build follow goal."); + + const auto keepWorldApplyResult = nbl::hlsl::applyFollowToCamera(m_cameraGoalSolver, freeCamera, trackedTarget, followConfig); + if (!keepWorldApplyResult.succeeded()) + return fail("Free keep-world-offset smoke failed to apply follow goal."); + if (!compareGoalToCamera(freeCamera, keepWorldGoal, "free keep-world-offset follow")) + return false; + if (!verifyFollowTargetContract(freeCamera, trackedTarget, followConfig, keepWorldGoal, "free keep-world-offset follow")) + return false; + + const auto restoreWorldOffsetResult = nbl::hlsl::applyPresetDetailed(m_cameraGoalSolver, freeCamera, baselinePreset); + if (!restoreWorldOffsetResult.succeeded() || !comparePresetToCamera(freeCamera, baselinePreset, 1e-6, 0.1, 1e-6)) + return fail("Free keep-world-offset smoke failed to restore the baseline preset."); } if (chaseCamera) @@ -789,7 +1020,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) if (!nbl::hlsl::captureFollowOffsetsFromCamera(m_cameraGoalSolver, chaseCamera, trackedTarget, followConfig)) return fail("Chase follow smoke failed to capture local offset."); - trackedTarget.setPose(float64_t3(-1.5, 0.5, 2.25), glm::quat(glm::dvec3(-0.12, 0.35, 0.27))); + trackedTarget.setPose(movedTrackedTargetPosition, movedTrackedTargetOrientation); CCameraGoal followGoal = {}; if (!nbl::hlsl::tryBuildFollowGoal(m_cameraGoalSolver, chaseCamera, trackedTarget, followConfig, followGoal)) @@ -800,11 +1031,20 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) return fail("Chase follow smoke failed to apply follow goal."); if (!compareGoalToCamera(chaseCamera, followGoal, "chase local-offset follow")) return false; + if (!verifyFollowTargetContract(chaseCamera, trackedTarget, followConfig, followGoal, "chase local-offset follow")) + return false; + if (!verifyFollowTargetMarkerAlignment(trackedTarget, "chase local-offset follow")) + return false; const auto restoreResult = nbl::hlsl::applyPresetDetailed(m_cameraGoalSolver, chaseCamera, baselinePreset); if (!restoreResult.succeeded() || !comparePresetToCamera(chaseCamera, baselinePreset, 1e-6, 0.1, 1e-6)) return fail("Chase follow smoke failed to restore the baseline preset."); } + + if (!verifyOffsetFollowRecapture(chaseCamera, trackedTarget, "chase follow recapture")) + return false; + if (!verifyOffsetFollowRecapture(dollyCamera, trackedTarget, "dolly follow recapture")) + return false; } if (hasOrbitPreset && hasChasePreset) @@ -2122,7 +2362,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) m_initialPlanarPresets.emplace_back(nbl::hlsl::capturePreset(m_cameraGoalSolver, camera, presetName)); } - resetFollowTargetToModel(); + resetFollowTargetToDefault(); m_planarFollowConfigs.clear(); m_planarFollowConfigs.reserve(m_planarProjections.size()); for (uint32_t planarIx = 0u; planarIx < m_planarProjections.size(); ++planarIx) @@ -2170,6 +2410,25 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) m_scriptedInput.events.emplace_back(std::move(entry)); }; + auto pushTrackedTargetTransform = [&](const uint64_t frame, const CCameraSequenceTrackedTargetPose& pose) -> void + { + ScriptedInputEvent entry; + entry.frame = frame; + entry.type = ScriptedInputEvent::Type::TrackedTargetTransform; + ICamera::CGimbal gimbal({ .position = pose.position, .orientation = pose.orientation }); + entry.trackedTargetTransform.transform = gimbal.operator()(); + m_scriptedInput.events.emplace_back(std::move(entry)); + }; + + auto pushSegmentLabel = [&](const uint64_t frame, std::string label) -> void + { + ScriptedInputEvent entry; + entry.frame = frame; + entry.type = ScriptedInputEvent::Type::SegmentLabel; + entry.segmentLabel.label = std::move(label); + m_scriptedInput.events.emplace_back(std::move(entry)); + }; + auto pushStepCheck = [&](const uint64_t frame, const CCameraSequenceContinuitySettings& continuity) -> void { ScriptedInputCheck entry; @@ -2190,6 +2449,16 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) m_scriptedInput.checks.emplace_back(std::move(entry)); }; + auto pushFollowTargetLockCheck = [&](const uint64_t frame, const float toleranceDeg = 1.0f, const float screenToleranceNdc = 0.03f) -> void + { + ScriptedInputCheck entry; + entry.frame = frame; + entry.kind = ScriptedInputCheck::Kind::FollowTargetLock; + entry.eulerToleranceDeg = toleranceDeg; + entry.posTolerance = screenToleranceNdc; + m_scriptedInput.checks.emplace_back(std::move(entry)); + }; + auto resolvePlanarIx = [&](const CCameraSequenceSegment& segment) -> std::optional { std::optional match; @@ -2226,6 +2495,10 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) pushAction(0u, ScriptedInputEvent::ActionData::Kind::SetUseWindow, useWindow ? 1 : 0); const double fps = std::max(1.0, static_cast(sequence.fps)); + const CCameraSequenceTrackedTargetPose referenceTrackedTargetPose = { + .position = getDefaultFollowTargetPosition(), + .orientation = getDefaultFollowTargetOrientation() + }; uint64_t frameCursor = 0u; for (const auto& segment : sequence.segments) { @@ -2238,6 +2511,12 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) return false; } + pushSegmentLabel(frameCursor, segment.name); + const bool useTrackedTargetFollow = nbl::hlsl::sequenceSegmentUsesTrackedTargetTrack(segment) && + planarIx.value() < m_planarFollowConfigs.size() && + m_planarFollowConfigs[planarIx.value()].enabled && + m_planarFollowConfigs[planarIx.value()].mode != ECameraFollowMode::Disabled; + const auto presentations = getSequenceSegmentPresentations(sequence, segment); if (presentations.size() > windowBindings.size()) { @@ -2273,6 +2552,16 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) return false; } + CCameraSequenceTrackedTargetTrack trackedTargetTrack; + if (nbl::hlsl::sequenceSegmentUsesTrackedTargetTrack(segment)) + { + if (!nbl::hlsl::buildSequenceTrackedTargetTrackFromReference(referenceTrackedTargetPose, segment, trackedTargetTrack, &trackError)) + { + logFail("Sequence segment \"%s\" failed to build tracked-target track: %s", segment.name.c_str(), trackError.c_str()); + return false; + } + } + const float durationSeconds = getSequenceSegmentDurationSeconds(sequence, segment, &track); const uint64_t durationFrames = std::max(1ull, static_cast(std::llround(std::max(0.f, durationSeconds) * static_cast(fps)))); for (uint64_t frameOffset = 0u; frameOffset < durationFrames; ++frameOffset) @@ -2286,6 +2575,16 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) return false; } pushGoal(frameCursor + frameOffset, makeGoalFromPreset(preset)); + if (!trackedTargetTrack.keyframes.empty()) + { + CCameraSequenceTrackedTargetPose trackedTargetPose; + if (!nbl::hlsl::tryBuildSequenceTrackedTargetPoseAtTime(trackedTargetTrack, sampleTime, trackedTargetPose)) + { + logFail("Sequence segment \"%s\" failed to sample tracked-target track at t=%f.", segment.name.c_str(), sampleTime); + return false; + } + pushTrackedTargetTransform(frameCursor + frameOffset, trackedTargetPose); + } } const auto continuity = getSequenceSegmentContinuity(sequence, segment); @@ -2299,7 +2598,11 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) if (continuity.step) { for (uint64_t frameOffset = 1u; frameOffset < durationFrames; ++frameOffset) + { pushStepCheck(frameCursor + frameOffset, continuity); + if (useTrackedTargetFollow) + pushFollowTargetLockCheck(frameCursor + frameOffset); + } } for (const auto fraction : getSequenceSegmentCaptureFractions(sequence, segment)) @@ -2999,12 +3302,13 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) auto ix = 0u; for (const auto& name : m_scene->getInitParams().geometryNames) { - if (name == "Cone") + if (name == "Cube") { - m_renderer->getGeometry(ix).pipeline = pipelines[CSimpleDebugRenderer::SInitParams::PipelineType::Cone]; if (!m_followTargetGeometryIx.has_value()) m_followTargetGeometryIx = ix; } + else if (name == "Cone") + m_renderer->getGeometry(ix).pipeline = pipelines[CSimpleDebugRenderer::SInitParams::PipelineType::Cone]; else if (name == "Grid") m_gridGeometryIx = ix; ix++; diff --git a/61_UI/AppTransformEditor.cpp b/61_UI/AppTransformEditor.cpp index 02618fe4d..7e29e957f 100644 --- a/61_UI/AppTransformEditor.cpp +++ b/61_UI/AppTransformEditor.cpp @@ -151,6 +151,8 @@ void App::TransformEditorContents() { auto referenceFrame = getCastedMatrix(imguizmoModel.outTRS); boundCameraToManipulate->manipulateWithUnitMotionScales({}, &referenceFrame); + if (boundPlanarCameraIxToManipulate.has_value()) + refreshFollowOffsetConfigForPlanar(boundPlanarCameraIxToManipulate.value()); /* { diff --git a/61_UI/AppUpdate.cpp b/61_UI/AppUpdate.cpp index 497a1530b..7f102d0d2 100644 --- a/61_UI/AppUpdate.cpp +++ b/61_UI/AppUpdate.cpp @@ -61,6 +61,8 @@ void App::update() std::vector scriptedImguizmo; std::vector scriptedActions; std::vector scriptedGoals; + std::vector scriptedTrackedTargetTransforms; + std::vector scriptedSegmentLabels; const CVirtualGimbalEvent* scriptedImguizmoVirtual = nullptr; uint32_t scriptedImguizmoVirtualCount = 0u; @@ -116,11 +118,22 @@ void App::update() { scriptedGoals.emplace_back(ev.goal); } + else if (ev.type == ScriptedInputEvent::Type::TrackedTargetTransform) + { + scriptedTrackedTargetTransforms.emplace_back(ev.trackedTargetTransform.transform); + } + else if (ev.type == ScriptedInputEvent::Type::SegmentLabel) + { + scriptedSegmentLabels.emplace_back(ev.segmentLabel.label); + } ++m_scriptedInput.nextEventIndex; } } + if (!scriptedSegmentLabels.empty()) + m_scriptedInput.visualSegmentLabel = scriptedSegmentLabels.back(); + if (m_scriptedInput.enabled && scriptedActions.size()) { auto applyAction = [&](const ScriptedInputEvent::ActionData& action) -> void @@ -306,14 +319,15 @@ void App::update() .keyboardEvents = { capturedEvents.keyboard.data(), capturedEvents.keyboard.size() } }; - if (m_scriptedInput.log && (scriptedKeyboard.size() || scriptedMouse.size() || scriptedImguizmo.size() || scriptedGoals.size())) + if (m_scriptedInput.log && (scriptedKeyboard.size() || scriptedMouse.size() || scriptedImguizmo.size() || scriptedGoals.size() || scriptedTrackedTargetTransforms.size())) { - m_logger->log("[script] frame %llu input kb=%zu mouse=%zu imguizmo=%zu goals=%zu", ILogger::ELL_INFO, + m_logger->log("[script] frame %llu input kb=%zu mouse=%zu imguizmo=%zu goals=%zu target=%zu", ILogger::ELL_INFO, static_cast(m_realFrameIx), scriptedKeyboard.size(), scriptedMouse.size(), scriptedImguizmo.size(), - scriptedGoals.size()); + scriptedGoals.size(), + scriptedTrackedTargetTransforms.size()); } if (enableActiveCameraMovement && !skipCameraInput) @@ -379,6 +393,8 @@ void App::update() } nbl::hlsl::applyCameraConstraints(m_cameraGoalSolver, target, m_cameraConstraints); + if (!m_scriptedInput.enabled) + refreshFollowOffsetConfigForPlanar(planarIx); appendVirtualEventLog("input", bindingLabel, planarIx, target, virtualEvents.data(), vCount); }; @@ -503,7 +519,116 @@ void App::update() } } - applyFollowToConfiguredCameras(); + auto tryComputeProjectedFollowMetricsForPlanar = [&](const uint32_t planarIx, float& ndcX, float& ndcY, float& ndcRadius) -> bool + { + if (activeRenderWindowIx >= windowBindings.size()) + return false; + + const auto& binding = windowBindings[activeRenderWindowIx]; + if (binding.activePlanarIx != planarIx || !binding.boundProjectionIx.has_value()) + return false; + if (planarIx >= m_planarProjections.size() || !m_planarProjections[planarIx]) + return false; + + auto& planar = m_planarProjections[planarIx]; + auto* camera = planar->getCamera(); + if (!camera) + return false; + + const auto projectionIx = binding.boundProjectionIx.value(); + auto& projections = planar->getPlanarProjections(); + if (projectionIx >= projections.size()) + return false; + + const auto viewMatrix = getMatrix3x4As4x4(getCastedMatrix(camera->getGimbal().getViewMatrix())); + const auto projectionMatrix = getCastedMatrix(projections[projectionIx].getProjectionMatrix()); + const auto viewProjMatrix = mul(projectionMatrix, viewMatrix); + return nbl::hlsl::tryComputeProjectedFollowTargetMetrics(viewProjMatrix, m_followTarget, ndcX, ndcY, &ndcRadius); + }; + + auto resetScriptedVisualFollowState = [&]() -> void + { + m_scriptedInput.visualFollowActive = false; + m_scriptedInput.visualFollowMode = ECameraFollowMode::Disabled; + m_scriptedInput.visualFollowLockValid = false; + m_scriptedInput.visualFollowLockAngleDeg = 0.0f; + m_scriptedInput.visualFollowTargetDistance = 0.0f; + m_scriptedInput.visualFollowProjectedValid = false; + m_scriptedInput.visualFollowTargetCenterNdcX = 0.0f; + m_scriptedInput.visualFollowTargetCenterNdcY = 0.0f; + m_scriptedInput.visualFollowTargetCenterNdcRadius = 0.0f; + }; + + auto updateScriptedVisualFollowState = [&](const uint32_t planarIx, ICamera* activeCamera, const SCameraFollowConfig* config) -> void + { + if (!activeCamera || !config || !config->enabled || config->mode == ECameraFollowMode::Disabled) + { + resetScriptedVisualFollowState(); + return; + } + + m_scriptedInput.visualFollowActive = true; + m_scriptedInput.visualFollowMode = config->mode; + + float lockAngleDeg = 0.0f; + double targetDistance = 0.0; + m_scriptedInput.visualFollowLockValid = nbl::hlsl::cameraFollowModeLocksViewToTarget(config->mode) && + nbl::hlsl::tryComputeFollowTargetLockMetrics(activeCamera->getGimbal(), m_followTarget, lockAngleDeg, &targetDistance); + if (m_scriptedInput.visualFollowLockValid) + { + m_scriptedInput.visualFollowLockAngleDeg = lockAngleDeg; + m_scriptedInput.visualFollowTargetDistance = static_cast(targetDistance); + } + else + { + m_scriptedInput.visualFollowLockAngleDeg = 0.0f; + m_scriptedInput.visualFollowTargetDistance = 0.0f; + } + + float ndcX = 0.0f; + float ndcY = 0.0f; + float ndcRadius = 0.0f; + m_scriptedInput.visualFollowProjectedValid = + m_scriptedInput.visualFollowLockValid && + tryComputeProjectedFollowMetricsForPlanar(planarIx, ndcX, ndcY, ndcRadius); + if (m_scriptedInput.visualFollowProjectedValid) + { + m_scriptedInput.visualFollowTargetCenterNdcX = ndcX; + m_scriptedInput.visualFollowTargetCenterNdcY = ndcY; + m_scriptedInput.visualFollowTargetCenterNdcRadius = ndcRadius; + } + else + { + m_scriptedInput.visualFollowTargetCenterNdcX = 0.0f; + m_scriptedInput.visualFollowTargetCenterNdcY = 0.0f; + m_scriptedInput.visualFollowTargetCenterNdcRadius = 0.0f; + } + }; + + if (!scriptedTrackedTargetTransforms.empty()) + { + setFollowTargetTransform(scriptedTrackedTargetTransforms.back()); + applyFollowToConfiguredCameras(true); + if (activeRenderWindowIx < windowBindings.size()) + { + const auto planarIx = windowBindings[activeRenderWindowIx].activePlanarIx; + if (planarIx < m_planarFollowConfigs.size()) + { + auto* activeCamera = planarIx < m_planarProjections.size() && m_planarProjections[planarIx] ? + m_planarProjections[planarIx]->getCamera() : nullptr; + updateScriptedVisualFollowState(planarIx, activeCamera, &m_planarFollowConfigs[planarIx]); + } + else + resetScriptedVisualFollowState(); + } + else + resetScriptedVisualFollowState(); + } + else + { + applyFollowToConfiguredCameras(); + resetScriptedVisualFollowState(); + } if (m_scriptedInput.enabled && m_scriptedInput.nextCheckIndex < m_scriptedInput.checks.size()) { @@ -746,6 +871,74 @@ void App::update() static_cast(frame), dpos, dmax); setStepReference(pos, euler); } + else if (check.kind == ScriptedInputCheck::Kind::FollowTargetLock) + { + SCameraFollowConfig activeFollowConfig = {}; + bool hasActiveFollowConfig = false; + bool hasFollowViewProjMatrix = false; + float32_t4x4 followViewProjMatrix = float32_t4x4(1.0f); + if (activeRenderWindowIx < windowBindings.size()) + { + const auto& binding = windowBindings[activeRenderWindowIx]; + const auto planarIx = binding.activePlanarIx; + if (planarIx < m_planarFollowConfigs.size()) + { + activeFollowConfig = m_planarFollowConfigs[planarIx]; + hasActiveFollowConfig = true; + } + if (planarIx < m_planarProjections.size() && m_planarProjections[planarIx] && binding.boundProjectionIx.has_value()) + { + auto& planar = m_planarProjections[planarIx]; + const auto projectionIx = binding.boundProjectionIx.value(); + auto& projections = planar->getPlanarProjections(); + if (projectionIx < projections.size()) + { + const auto viewMatrix = getMatrix3x4As4x4(getCastedMatrix(camera->getGimbal().getViewMatrix())); + const auto projectionMatrix = getCastedMatrix(projections[projectionIx].getProjectionMatrix()); + followViewProjMatrix = mul(projectionMatrix, viewMatrix); + hasFollowViewProjMatrix = true; + } + } + } + + nbl::hlsl::SCameraFollowRegressionResult regression = {}; + std::string regressionError; + const auto expectedFollowGoal = hasActiveFollowConfig + ? [&]() -> CCameraGoal + { + CCameraGoal goal = {}; + nbl::hlsl::tryBuildFollowGoal(m_cameraGoalSolver, camera, m_followTarget, activeFollowConfig, goal); + return goal; + }() + : CCameraGoal{}; + const bool ok = hasActiveFollowConfig && + nbl::hlsl::validateFollowTargetContract( + camera, + m_followTarget, + activeFollowConfig, + expectedFollowGoal, + regression, + ®ressionError, + check.eulerToleranceDeg, + 1e-6, + 1e-9, + hasFollowViewProjMatrix ? &followViewProjMatrix : nullptr, + check.posTolerance); + if (!ok) + { + logFail("[script][fail] follow_lock frame=%llu %s", + static_cast(frame), + regressionError.empty() ? "follow contract mismatch" : regressionError.c_str()); + } + else + { + logPass("[script][pass] follow_lock frame=%llu angle_deg=%.6f target_distance=%.6f screen_ndc=%.6f", + static_cast(frame), + regression.lockAngleDeg, + regression.targetDistance, + regression.projectedNdcRadius); + } + } ++m_scriptedInput.nextCheckIndex; } diff --git a/61_UI/README.md b/61_UI/README.md index f41b612e0..0e3e3ae68 100644 --- a/61_UI/README.md +++ b/61_UI/README.md @@ -114,9 +114,10 @@ Per authored segment: 1. select planar 2. store `baseline` 3. build a reusable keyframe track from the active camera reference preset -4. sample that track for `4.0 s` at `60 FPS` -5. run `gimbal_step` on each generated frame step -6. capture selected milestones such as `end` +4. optionally build a tracked-target track from the default tracked-target pose +5. sample the authored track(s) for `4.0 s` at `60 FPS` +6. run `gimbal_step` on each generated frame step +7. capture selected milestones such as `end` PASS means every step delta stayed inside configured continuity ranges. FAIL means any step exceeded max range or failed minimum expected motion. @@ -127,6 +128,9 @@ Continuity also supports visual debug mode: - fixed frame pacing (`visual_debug_target_fps`) so camera time is human-readable - compact authored JSON that stays in camera-domain and is reusable outside `61_UI` +For follow-enabled cameras, continuity can now also author `target_keyframes`. +That drives the shared tracked target through the reusable follow layer instead of hardcoding camera hacks in `61_UI`. + ## Build and run Build this example first: diff --git a/61_UI/app_resources/cameraz_continuity.json b/61_UI/app_resources/cameraz_continuity.json index bf11cbb5f..9ca9f8224 100644 --- a/61_UI/app_resources/cameraz_continuity.json +++ b/61_UI/app_resources/cameraz_continuity.json @@ -79,6 +79,41 @@ "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 + ] + } + } ] }, { @@ -125,6 +160,41 @@ "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 + ] + } + } ] }, { @@ -146,6 +216,41 @@ "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 + ] + } + } ] }, { @@ -179,6 +284,41 @@ "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 + ] + } + } ] }, { @@ -199,25 +339,83 @@ "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": 4.0, + "time": 1.5, "delta": { - "target_offset": [ - 1.4, - -1.8, + "position_offset": [ + 1.5, + -1.2, 0.6 ], - "orbit_u_delta_deg": -44.0, - "orbit_distance_delta": -1.4 + "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 + ] } } ] @@ -226,18 +424,42 @@ "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": { - "target_offset": [ - 2.6, - 0.8, - -0.4 + "position_offset": [ + 5.3, + 3.1, + -0.1 ], - "orbit_distance_delta": -2.8 + "rotation_euler_deg_offset": [ + 0.0, + -28.0, + 0.0 + ] } } ] @@ -263,6 +485,41 @@ "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 + ] + } + } ] }, { @@ -288,6 +545,41 @@ "path_height_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/include/app/App.hpp b/61_UI/include/app/App.hpp index ca69cadca..d30adfee7 100644 --- a/61_UI/include/app/App.hpp +++ b/61_UI/include/app/App.hpp @@ -687,13 +687,17 @@ class App final : public examples::SimpleWindowedApplication inline float32_t3x4 computeFollowTargetMarkerWorld() const { - auto markerWorld = getCastedMatrix(m_followTarget.getGimbal().operator()()); - const float32_t3x4 markerLocal = { - float32_t4(0.22f, 0.0f, 0.0f, 0.0f), - float32_t4(0.0f, 0.35f, 0.0f, 0.45f), - float32_t4(0.0f, 0.0f, 0.22f, 0.0f) + const auto& targetGimbal = m_followTarget.getGimbal(); + const auto position = getCastedVector(targetGimbal.getPosition()); + const auto axisX = getCastedVector(targetGimbal.getXAxis()); + const auto axisY = getCastedVector(targetGimbal.getYAxis()); + const auto axisZ = getCastedVector(targetGimbal.getZAxis()); + const float markerScale = (m_scriptedInput.enabled && m_scriptedInput.visualDebug) ? 0.6f : 0.28f; + return { + float32_t4(axisX * markerScale, position.x), + float32_t4(axisY * markerScale, position.y), + float32_t4(axisZ * markerScale, position.z) }; - return concatenateBFollowedByA(markerLocal, markerWorld); } inline void setFollowTargetTransform(const float64_t4x4& transform) @@ -709,7 +713,63 @@ class App final : public examples::SimpleWindowedApplication return nbl::hlsl::captureFollowOffsetsFromCamera(m_cameraGoalSolver, camera, m_followTarget, m_planarFollowConfigs[planarIx]); } - inline void resetFollowTargetToModel() + inline bool followConfigUsesCapturedOffset(const SCameraFollowConfig& config) const + { + return config.enabled && nbl::hlsl::cameraFollowModeUsesCapturedOffset(config.mode); + } + + inline void refreshFollowOffsetConfigForPlanar(const uint32_t planarIx) + { + if (planarIx >= m_planarProjections.size() || planarIx >= m_planarFollowConfigs.size()) + return; + + auto& config = m_planarFollowConfigs[planarIx]; + if (!followConfigUsesCapturedOffset(config)) + return; + + auto* camera = m_planarProjections[planarIx] ? m_planarProjections[planarIx]->getCamera() : nullptr; + if (!camera) + return; + + nbl::hlsl::captureFollowOffsetsFromCamera(m_cameraGoalSolver, camera, m_followTarget, config); + } + + inline void refreshFollowOffsetConfigsForCamera(ICamera* camera) + { + if (!camera) + return; + + for (uint32_t planarIx = 0u; planarIx < m_planarProjections.size() && planarIx < m_planarFollowConfigs.size(); ++planarIx) + { + auto* planarCamera = m_planarProjections[planarIx] ? m_planarProjections[planarIx]->getCamera() : nullptr; + if (planarCamera != camera) + continue; + refreshFollowOffsetConfigForPlanar(planarIx); + } + } + + inline void refreshAllFollowOffsetConfigs() + { + for (uint32_t planarIx = 0u; planarIx < m_planarProjections.size() && planarIx < m_planarFollowConfigs.size(); ++planarIx) + refreshFollowOffsetConfigForPlanar(planarIx); + } + + inline float64_t3 getDefaultFollowTargetPosition() const + { + return float64_t3(6.0, -4.5, 2.25); + } + + inline glm::quat getDefaultFollowTargetOrientation() const + { + return glm::quat(1.0, 0.0, 0.0, 0.0); + } + + inline void resetFollowTargetToDefault() + { + m_followTarget.setPose(getDefaultFollowTargetPosition(), getDefaultFollowTargetOrientation()); + } + + inline void snapFollowTargetToModel() { const auto modelTransform = hlsl::transpose(getMatrix3x4As4x4(m_model)); setFollowTargetTransform(getCastedMatrix(modelTransform)); @@ -745,9 +805,9 @@ class App final : public examples::SimpleWindowedApplication return config; } - inline void applyFollowToConfiguredCameras() + inline void applyFollowToConfiguredCameras(const bool allowDuringScriptedInput = false) { - if (m_scriptedInput.enabled) + if (m_scriptedInput.enabled && !allowDuringScriptedInput) return; if (m_planarFollowConfigs.size() != m_planarProjections.size()) return; @@ -848,6 +908,54 @@ class App final : public examples::SimpleWindowedApplication return std::isfinite(outScreen.x) && std::isfinite(outScreen.y); } + inline void drawFollowTargetViewportOverlay( + const float32_t4x4& viewProjMatrix, + const ImVec2& viewportPos, + const ImVec2& viewportSize) const + { + if (!(m_scriptedInput.enabled && m_scriptedInput.visualDebug && m_scriptedInput.visualFollowActive)) + return; + if (viewportSize.x <= 1.0f || viewportSize.y <= 1.0f) + return; + + float ndcX = 0.0f; + float ndcY = 0.0f; + float ndcRadius = 0.0f; + if (!nbl::hlsl::tryComputeProjectedFollowTargetMetrics(viewProjMatrix, m_followTarget, ndcX, ndcY, &ndcRadius)) + return; + + auto* drawList = ImGui::GetWindowDrawList(); + if (!drawList) + return; + + const ImVec2 center( + viewportPos.x + viewportSize.x * 0.5f, + viewportPos.y + viewportSize.y * 0.5f); + const ImVec2 target( + viewportPos.x + (ndcX * 0.5f + 0.5f) * viewportSize.x, + viewportPos.y + (-ndcY * 0.5f + 0.5f) * viewportSize.y); + + const bool centered = ndcRadius <= 0.03f; + const ImU32 centerColor = IM_COL32(255, 170, 72, 235); + const ImU32 targetColor = centered ? IM_COL32(64, 255, 164, 245) : IM_COL32(90, 220, 255, 245); + const ImU32 targetFillColor = centered ? IM_COL32(24, 120, 76, 120) : IM_COL32(20, 92, 124, 120); + const ImU32 lineColor = centered ? IM_COL32(96, 255, 186, 200) : IM_COL32(120, 220, 255, 200); + const float centerRadius = 16.0f; + const float targetRadius = centered ? 18.0f : 14.0f; + + drawList->AddCircle(center, centerRadius, centerColor, 32, 2.5f); + drawList->AddLine(ImVec2(center.x - 22.0f, center.y), ImVec2(center.x + 22.0f, center.y), centerColor, 2.0f); + drawList->AddLine(ImVec2(center.x, center.y - 22.0f), ImVec2(center.x, center.y + 22.0f), centerColor, 2.0f); + + drawList->AddLine(center, target, lineColor, 2.0f); + drawList->AddCircleFilled(target, targetRadius, targetFillColor, 24); + drawList->AddCircle(target, targetRadius, targetColor, 32, 2.5f); + drawList->AddLine(ImVec2(target.x - 14.0f, target.y), ImVec2(target.x + 14.0f, target.y), targetColor, 2.0f); + drawList->AddLine(ImVec2(target.x, target.y - 14.0f), ImVec2(target.x, target.y + 14.0f), targetColor, 2.0f); + + drawList->AddText(ImVec2(target.x + 16.0f, target.y - 28.0f), targetColor, "FOLLOW TARGET"); + } + inline void drawWorldReferenceOverlay( const ImVec2& viewportPos, const ImVec2& viewportSize, @@ -1153,7 +1261,9 @@ class App final : public examples::SimpleWindowedApplication binding.activePlanarIx, static_cast(m_realFrameIx)); } - const std::string lineBottom(lineBottomBuffer); + std::string lineBottom(lineBottomBuffer); + if (!m_scriptedInput.visualSegmentLabel.empty()) + lineBottom += " | " + m_scriptedInput.visualSegmentLabel; std::string lineHint = std::string(cameraHint); float dynamicFov = 0.0f; if (camera && camera->tryGetDynamicPerspectiveFov(dynamicFov)) @@ -1162,6 +1272,30 @@ class App final : public examples::SimpleWindowedApplication std::snprintf(fovBuffer, sizeof(fovBuffer), " | Dynamic FOV %.2f deg", dynamicFov); lineHint += fovBuffer; } + if (m_scriptedInput.visualFollowActive) + { + lineHint += " | " + std::string(nbl::hlsl::getCameraFollowModeDescription(m_scriptedInput.visualFollowMode)); + if (m_scriptedInput.visualFollowLockValid) + { + char followBuffer[192] = {}; + std::snprintf( + followBuffer, + sizeof(followBuffer), + " | lock %.2f deg | target %.2f | center err %.3f", + m_scriptedInput.visualFollowLockAngleDeg, + m_scriptedInput.visualFollowTargetDistance, + m_scriptedInput.visualFollowTargetCenterNdcRadius); + lineHint += followBuffer; + } + else + { + lineHint += " | lock n/a | target n/a | center err n/a"; + } + } + else + { + lineHint += " | Follow off"; + } const float topSize = 50.f; const float midSize = 38.f; @@ -1238,6 +1372,8 @@ class App final : public examples::SimpleWindowedApplication inline CCameraGoalSolver::SApplyResult applyPresetFromUi(ICamera* camera, const CameraPreset& preset) { const auto result = nbl::hlsl::applyPresetDetailed(m_cameraGoalSolver, camera, preset); + if (result.succeeded()) + refreshFollowOffsetConfigsForCamera(camera); const auto presetUi = analyzePresetForUi(camera, preset); storeApplyStatusBanner(m_manualPresetApplyBanner, describeApplyResult(result) + " | " + presetUi.compatibilityLabel, @@ -1304,10 +1440,14 @@ class App final : public examples::SimpleWindowedApplication inline SCameraPresetApplySummary applyPresetToTargets(const CameraPreset& preset) { + SCameraPresetApplySummary summary = {}; if (!m_playbackAffectsAll) { ICamera* activeCamera = getActiveCamera(); - return nbl::hlsl::applyPresetToCameraRange(m_cameraGoalSolver, std::span(&activeCamera, activeCamera ? 1u : 0u), preset); + summary = nbl::hlsl::applyPresetToCameraRange(m_cameraGoalSolver, std::span(&activeCamera, activeCamera ? 1u : 0u), preset); + if (summary.succeeded()) + refreshFollowOffsetConfigsForCamera(activeCamera); + return summary; } std::vector cameras; @@ -1326,7 +1466,10 @@ class App final : public examples::SimpleWindowedApplication cameras.push_back(camera); } - return nbl::hlsl::applyPresetToCameraRange(m_cameraGoalSolver, std::span(cameras.data(), cameras.size()), preset); + summary = nbl::hlsl::applyPresetToCameraRange(m_cameraGoalSolver, std::span(cameras.data(), cameras.size()), preset); + if (summary.succeeded()) + refreshAllFollowOffsetConfigs(); + return summary; } inline bool tryBuildPlaybackPresetAtTime(const float time, CameraPreset& preset) @@ -1800,7 +1943,9 @@ class App final : public examples::SimpleWindowedApplication Mouse, Imguizmo, Action, - Goal + Goal, + TrackedTargetTransform, + SegmentLabel }; struct KeyboardData @@ -1845,6 +1990,16 @@ class App final : public examples::SimpleWindowedApplication bool requireExact = true; }; + struct TrackedTargetTransformData + { + float64_t4x4 transform = float64_t4x4(1.0); + }; + + struct SegmentLabelData + { + std::string label; + }; + uint64_t frame = 0; Type type = Type::Keyboard; KeyboardData keyboard; @@ -1852,6 +2007,8 @@ class App final : public examples::SimpleWindowedApplication float32_t4x4 imguizmo = float32_t4x4(1.f); ActionData action; GoalData goal; + TrackedTargetTransformData trackedTargetTransform; + SegmentLabelData segmentLabel; }; struct ScriptedInputCheck @@ -1862,7 +2019,8 @@ class App final : public examples::SimpleWindowedApplication ImguizmoVirtual, GimbalNear, GimbalDelta, - GimbalStep + GimbalStep, + FollowTargetLock }; struct ExpectedVirtualEvent @@ -1916,6 +2074,16 @@ class App final : public examples::SimpleWindowedApplication bool visualActivePlanarValid = false; uint32_t visualActivePlanarIx = 0u; uint64_t visualActivePlanarStartFrame = 0u; + std::string visualSegmentLabel; + bool visualFollowActive = false; + ECameraFollowMode visualFollowMode = ECameraFollowMode::Disabled; + bool visualFollowLockValid = false; + float visualFollowLockAngleDeg = 0.0f; + float visualFollowTargetDistance = 0.0f; + bool visualFollowProjectedValid = false; + float visualFollowTargetCenterNdcX = 0.0f; + float visualFollowTargetCenterNdcY = 0.0f; + float visualFollowTargetCenterNdcRadius = 0.0f; bool scriptedLeftMouseDown = false; bool scriptedRightMouseDown = false; bool framePacerInitialized = false; diff --git a/61_UI/include/common.hpp b/61_UI/include/common.hpp index b5cb44aec..272d418fb 100644 --- a/61_UI/include/common.hpp +++ b/61_UI/include/common.hpp @@ -30,6 +30,7 @@ #include "camera/CCameraPresentationUtilities.hpp" #include "camera/CCameraProjectionUtilities.hpp" #include "camera/CCameraFollowUtilities.hpp" +#include "camera/CCameraFollowRegressionUtilities.hpp" #include "camera/CCameraTextUtilities.hpp" #include "camera/CGimbalInputBinder.hpp" @@ -76,6 +77,8 @@ using nbl::hlsl::CCameraPlaybackCursor; using nbl::hlsl::CCameraSequenceScript; using nbl::hlsl::CCameraSequenceSegment; using nbl::hlsl::CCameraSequenceKeyframe; +using nbl::hlsl::CCameraSequenceTrackedTargetPose; +using nbl::hlsl::CCameraSequenceTrackedTargetTrack; using nbl::hlsl::CCameraSequencePresentation; using nbl::hlsl::CCameraSequenceContinuitySettings; using nbl::hlsl::SCameraPlaybackAdvanceResult; diff --git a/common/include/camera/CCameraFollowRegressionUtilities.hpp b/common/include/camera/CCameraFollowRegressionUtilities.hpp new file mode 100644 index 000000000..f54a69e68 --- /dev/null +++ b/common/include/camera/CCameraFollowRegressionUtilities.hpp @@ -0,0 +1,220 @@ +// 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_FOLLOW_REGRESSION_UTILITIES_HPP_ +#define _C_CAMERA_FOLLOW_REGRESSION_UTILITIES_HPP_ + +#include + +#include "CCameraFollowUtilities.hpp" + +namespace nbl::hlsl +{ + +/** +* Reusable follow-contract validation helpers. +* +* The checks stay camera-domain: +* +* - camera-to-target direction must match the camera forward axis for locking modes +* - target distance must be finite and internally consistent +* - spherical cameras must write the tracked target back into spherical target state +* - spherical distance must match the goal-derived distance when present +*/ +struct SCameraFollowRegressionResult +{ + bool passed = false; + bool hasLockMetrics = false; + float lockAngleDeg = 0.0f; + double targetDistance = 0.0; + bool hasProjectedMetrics = false; + float projectedNdcX = 0.0f; + float projectedNdcY = 0.0f; + float projectedNdcRadius = 0.0f; + bool hasSphericalState = false; + float64_t3 sphericalTarget = float64_t3(0.0); + float sphericalDistance = 0.0f; +}; + +inline bool tryComputeProjectedFollowTargetMetrics( + const float32_t4x4& viewProjMatrix, + const CTrackedTarget& trackedTarget, + float& outNdcX, + float& outNdcY, + float* outNdcRadius = nullptr) +{ + const auto target = getCastedVector(trackedTarget.getGimbal().getPosition()); + const auto clip = mul(viewProjMatrix, float32_t4(target.x, target.y, target.z, 1.0f)); + if (!std::isfinite(clip.x) || !std::isfinite(clip.y) || !std::isfinite(clip.z) || !std::isfinite(clip.w)) + return false; + + const auto absW = std::abs(clip.w); + if (absW < 1e-5f) + return false; + + const float invW = 1.0f / clip.w; + outNdcX = clip.x * invW; + outNdcY = clip.y * invW; + if (!std::isfinite(outNdcX) || !std::isfinite(outNdcY)) + return false; + + if (outNdcRadius) + *outNdcRadius = std::sqrt(outNdcX * outNdcX + outNdcY * outNdcY); + + return true; +} + +inline bool validateProjectedFollowTargetContract( + const float32_t4x4& viewProjMatrix, + const CTrackedTarget& trackedTarget, + float& outNdcRadius, + std::string* error = nullptr, + const float ndcRadiusTolerance = 0.03f) +{ + float ndcX = 0.0f; + float ndcY = 0.0f; + if (!tryComputeProjectedFollowTargetMetrics(viewProjMatrix, trackedTarget, ndcX, ndcY, &outNdcRadius)) + { + if (error) + *error = "failed to project follow target"; + return false; + } + + if (outNdcRadius > ndcRadiusTolerance) + { + if (error) + { + *error = "projected target mismatch ndc=(" + std::to_string(ndcX) + + "," + std::to_string(ndcY) + ") radius=" + std::to_string(outNdcRadius); + } + return false; + } + + return true; +} + +inline bool validateFollowTargetContract( + ICamera* camera, + const CTrackedTarget& trackedTarget, + const SCameraFollowConfig& followConfig, + const CCameraGoal& followGoal, + SCameraFollowRegressionResult& out, + std::string* error = nullptr, + const float lockAngleToleranceDeg = 0.1f, + const double distanceTolerance = 1e-6, + const double targetTolerance = 1e-9, + const float32_t4x4* viewProjMatrix = nullptr, + const float projectedNdcTolerance = 0.03f) +{ + out = {}; + if (!camera) + { + if (error) + *error = "missing camera"; + return false; + } + + if (cameraFollowModeLocksViewToTarget(followConfig.mode)) + { + out.hasLockMetrics = tryComputeFollowTargetLockMetrics(camera->getGimbal(), trackedTarget, out.lockAngleDeg, &out.targetDistance); + if (!out.hasLockMetrics) + { + if (error) + *error = "failed to compute follow lock metrics"; + return false; + } + + const auto expectedTargetDistance = length(trackedTarget.getGimbal().getPosition() - camera->getGimbal().getPosition()); + if (!std::isfinite(expectedTargetDistance) || std::abs(expectedTargetDistance - out.targetDistance) > distanceTolerance) + { + if (error) + { + *error = "target distance mismatch actual=" + std::to_string(out.targetDistance) + + " expected=" + std::to_string(expectedTargetDistance); + } + return false; + } + + if (out.lockAngleDeg > lockAngleToleranceDeg) + { + if (error) + *error = "lock angle mismatch angle_deg=" + std::to_string(out.lockAngleDeg); + return false; + } + + if (viewProjMatrix) + { + out.hasProjectedMetrics = tryComputeProjectedFollowTargetMetrics( + *viewProjMatrix, + trackedTarget, + out.projectedNdcX, + out.projectedNdcY, + &out.projectedNdcRadius); + if (!out.hasProjectedMetrics) + { + if (error) + *error = "failed to compute projected follow target metrics"; + return false; + } + + if (out.projectedNdcRadius > projectedNdcTolerance) + { + if (error) + { + *error = "projected target mismatch ndc=(" + std::to_string(out.projectedNdcX) + + "," + std::to_string(out.projectedNdcY) + ") radius=" + std::to_string(out.projectedNdcRadius); + } + return false; + } + } + } + + if (camera->supportsGoalState(ICamera::GoalStateSphericalTarget)) + { + ICamera::SphericalTargetState state; + if (!camera->tryGetSphericalTargetState(state)) + { + if (error) + *error = "missing spherical target state"; + return false; + } + + out.hasSphericalState = true; + out.sphericalTarget = state.target; + out.sphericalDistance = state.distance; + + const auto trackedTargetPosition = trackedTarget.getGimbal().getPosition(); + const auto targetDelta = state.target - trackedTargetPosition; + const auto targetDeltaLen = length(targetDelta); + if (!std::isfinite(targetDeltaLen) || targetDeltaLen > targetTolerance) + { + if (error) + *error = "spherical target writeback mismatch"; + return false; + } + + const auto actualDistance = length(camera->getGimbal().getPosition() - trackedTargetPosition); + const auto expectedDistance = followGoal.hasOrbitState ? static_cast(followGoal.orbitDistance) : + (followGoal.hasDistance ? static_cast(followGoal.distance) : actualDistance); + if (!std::isfinite(actualDistance) || !std::isfinite(expectedDistance) || + std::abs(actualDistance - expectedDistance) > distanceTolerance || + std::abs(static_cast(state.distance) - expectedDistance) > distanceTolerance) + { + if (error) + { + *error = "spherical distance mismatch actual=" + std::to_string(actualDistance) + + " state=" + std::to_string(state.distance) + + " expected=" + std::to_string(expectedDistance); + } + return false; + } + } + + out.passed = true; + return true; +} + +} // namespace nbl::hlsl + +#endif // _C_CAMERA_FOLLOW_REGRESSION_UTILITIES_HPP_ diff --git a/common/include/camera/CCameraFollowUtilities.hpp b/common/include/camera/CCameraFollowUtilities.hpp index 33082a30e..dceb649fd 100644 --- a/common/include/camera/CCameraFollowUtilities.hpp +++ b/common/include/camera/CCameraFollowUtilities.hpp @@ -104,6 +104,33 @@ inline constexpr const char* getCameraFollowModeLabel(const ECameraFollowMode mo } } +inline constexpr const char* getCameraFollowModeDescription(const ECameraFollowMode mode) +{ + switch (mode) + { + case ECameraFollowMode::Disabled: return "Follow disabled"; + case ECameraFollowMode::OrbitTarget: return "Keep orbit around moving target and keep it centered"; + case ECameraFollowMode::LookAtTarget: return "Keep camera position and lock the view onto the target"; + case ECameraFollowMode::KeepWorldOffset: return "Move with the target in world offset and keep it centered"; + case ECameraFollowMode::KeepLocalOffset: return "Move with the target in target-local offset and keep it centered"; + default: return "Unknown follow mode"; + } +} + +inline constexpr bool cameraFollowModeLocksViewToTarget(const ECameraFollowMode mode) +{ + switch (mode) + { + case ECameraFollowMode::OrbitTarget: + case ECameraFollowMode::LookAtTarget: + case ECameraFollowMode::KeepWorldOffset: + case ECameraFollowMode::KeepLocalOffset: + return true; + default: + return false; + } +} + inline constexpr bool cameraFollowModeUsesWorldOffset(const ECameraFollowMode mode) { return mode == ECameraFollowMode::KeepWorldOffset; @@ -114,6 +141,11 @@ inline constexpr bool cameraFollowModeUsesLocalOffset(const ECameraFollowMode mo return mode == ECameraFollowMode::KeepLocalOffset; } +inline constexpr bool cameraFollowModeUsesCapturedOffset(const ECameraFollowMode mode) +{ + return cameraFollowModeUsesWorldOffset(mode) || cameraFollowModeUsesLocalOffset(mode); +} + inline float64_t3 transformFollowLocalOffset(const ICamera::CGimbal& gimbal, const float64_t3& localOffset) { return gimbal.getXAxis() * localOffset.x + @@ -233,6 +265,32 @@ inline bool captureFollowOffsetsFromCamera( return true; } +inline bool tryComputeFollowTargetLockMetrics( + const ICamera::CGimbal& cameraGimbal, + const CTrackedTarget& trackedTarget, + float& outAngleDeg, + double* outDistance = nullptr) +{ + const auto toTarget = trackedTarget.getGimbal().getPosition() - cameraGimbal.getPosition(); + const auto targetDistance = length(toTarget); + if (!std::isfinite(targetDistance) || targetDistance <= 1e-9) + return false; + + const auto forward = normalize(cameraGimbal.getZAxis()); + if (!isFiniteVec3(forward) || length(forward) <= 1e-9) + return false; + + const auto targetDir = toTarget / targetDistance; + const auto dotForward = std::clamp(dot(forward, targetDir), -1.0, 1.0); + outAngleDeg = static_cast(glm::degrees(std::acos(dotForward))); + if (!std::isfinite(outAngleDeg)) + return false; + + if (outDistance) + *outDistance = targetDistance; + return true; +} + inline bool tryBuildFollowGoal( const CCameraGoalSolver& solver, ICamera* camera, diff --git a/common/include/camera/CCameraSequenceScript.hpp b/common/include/camera/CCameraSequenceScript.hpp index 8043f1a9b..6b9e2c5ac 100644 --- a/common/include/camera/CCameraSequenceScript.hpp +++ b/common/include/camera/CCameraSequenceScript.hpp @@ -14,6 +14,7 @@ #include "CCameraKeyframeTrack.hpp" #include "IPlanarProjection.hpp" +#include "glm/glm/gtc/quaternion.hpp" namespace nbl::hlsl { @@ -26,6 +27,7 @@ namespace nbl::hlsl * - which camera kind a segment targets * - which reusable projection presentations should be shown * - which keyframed camera goals should be sampled over time +* - which tracked-target poses should be sampled over time * - which continuity thresholds and capture points should be generated * * The format intentionally does not store: @@ -108,6 +110,48 @@ struct CCameraSequenceKeyframe CCameraSequenceGoalDelta delta = {}; }; +//! Concrete tracked-target pose sampled from a shared authored sequence. +struct CCameraSequenceTrackedTargetPose +{ + float64_t3 position = float64_t3(0.0); + glm::quat orientation = glm::quat(1.0, 0.0, 0.0, 0.0); +}; + +//! Relative tracked-target adjustment authored against an initial tracked-target pose. +struct CCameraSequenceTrackedTargetDelta +{ + bool hasPositionOffset = false; + float64_t3 positionOffset = float64_t3(0.0); + + bool hasRotationEulerDegOffset = false; + float32_t3 rotationEulerDegOffset = float32_t3(0.f); +}; + +//! One authored tracked-target keyframe inside a reusable camera-sequence segment. +//! Target keyframes stay camera-domain and can drive follow behavior without example-specific object references. +struct CCameraSequenceTrackedTargetKeyframe +{ + float time = 0.f; + bool hasAbsolutePosition = false; + float64_t3 absolutePosition = float64_t3(0.0); + bool hasAbsoluteRotationEulerDeg = false; + float32_t3 absoluteRotationEulerDeg = float32_t3(0.f); + bool hasDelta = false; + CCameraSequenceTrackedTargetDelta delta = {}; +}; + +//! Runtime sampled tracked-target track built from an authored segment plus a reference pose. +struct CCameraSequenceTrackedTargetTrack +{ + struct SKeyframe + { + float time = 0.f; + CCameraSequenceTrackedTargetPose pose = {}; + }; + + std::vector keyframes; +}; + //! Defaults shared by all camera-sequence segments unless overridden locally. struct CCameraSequenceSegmentDefaults { @@ -141,6 +185,7 @@ struct CCameraSequenceSegment std::vector captureFractions; std::vector keyframes; + std::vector targetKeyframes; }; //! Top-level reusable camera-sequence script. @@ -452,6 +497,77 @@ inline bool deserializeSequenceKeyframe(const nlohmann::json& root, CCameraSeque return true; } +inline bool deserializeSequenceTrackedTargetDelta(const nlohmann::json& root, CCameraSequenceTrackedTargetDelta& out, std::string* error = nullptr) +{ + if (!root.is_object()) + { + if (error) + *error = "Sequence target delta must be an object."; + return false; + } + + out = {}; + auto readFloat3 = [](const nlohmann::json& entry, auto& outValue) -> void + { + const auto arr = entry.get>(); + outValue = std::decay_t(arr[0], arr[1], arr[2]); + }; + auto readDouble3 = [](const nlohmann::json& entry, auto& outValue) -> void + { + const auto arr = entry.get>(); + outValue = std::decay_t(arr[0], arr[1], arr[2]); + }; + + if (root.contains("position_offset")) + { + readDouble3(root["position_offset"], out.positionOffset); + out.hasPositionOffset = true; + } + if (root.contains("rotation_euler_deg_offset")) + { + readFloat3(root["rotation_euler_deg_offset"], out.rotationEulerDegOffset); + out.hasRotationEulerDegOffset = true; + } + + return true; +} + +inline bool deserializeSequenceTrackedTargetKeyframe(const nlohmann::json& root, CCameraSequenceTrackedTargetKeyframe& out, std::string* error = nullptr) +{ + if (!root.is_object()) + { + if (error) + *error = "Sequence target keyframe must be an object."; + return false; + } + + out = {}; + if (root.contains("time")) + out.time = std::max(0.f, root["time"].get()); + + if (root.contains("delta")) + { + if (!deserializeSequenceTrackedTargetDelta(root["delta"], out.delta, error)) + return false; + out.hasDelta = true; + } + + if (root.contains("position")) + { + const auto arr = root["position"].get>(); + out.absolutePosition = float64_t3(arr[0], arr[1], arr[2]); + out.hasAbsolutePosition = true; + } + if (root.contains("rotation_euler_deg")) + { + const auto arr = root["rotation_euler_deg"].get>(); + out.absoluteRotationEulerDeg = float32_t3(arr[0], arr[1], arr[2]); + out.hasAbsoluteRotationEulerDeg = true; + } + + return true; +} + inline bool deserializeSequenceSegment(const nlohmann::json& root, CCameraSequenceSegment& out, std::string* error = nullptr) { if (!root.is_object()) @@ -536,6 +652,22 @@ inline bool deserializeSequenceSegment(const nlohmann::json& root, CCameraSequen out.keyframes.emplace_back(std::move(keyframe)); } } + if (root.contains("target_keyframes")) + { + if (!root["target_keyframes"].is_array()) + { + if (error) + *error = "Sequence segment target_keyframes must be an array."; + return false; + } + for (const auto& entry : root["target_keyframes"]) + { + CCameraSequenceTrackedTargetKeyframe keyframe; + if (!deserializeSequenceTrackedTargetKeyframe(entry, keyframe, error)) + return false; + out.targetKeyframes.emplace_back(std::move(keyframe)); + } + } if (out.keyframes.empty()) { @@ -835,6 +967,116 @@ inline bool buildSequenceTrackFromReference(const CCameraPreset& reference, cons return !outTrack.keyframes.empty(); } +inline bool isSequenceTrackedTargetPoseFinite(const CCameraSequenceTrackedTargetPose& pose) +{ + return isFiniteVec3(pose.position) && + std::isfinite(pose.orientation.x) && + std::isfinite(pose.orientation.y) && + std::isfinite(pose.orientation.z) && + std::isfinite(pose.orientation.w); +} + +inline bool buildSequenceTrackedTargetPoseFromReference( + const CCameraSequenceTrackedTargetPose& reference, + const CCameraSequenceTrackedTargetKeyframe& authored, + CCameraSequenceTrackedTargetPose& outPose, + std::string* error = nullptr) +{ + outPose = reference; + + if (authored.hasAbsolutePosition) + outPose.position = authored.absolutePosition; + if (authored.hasAbsoluteRotationEulerDeg) + outPose.orientation = glm::quat(glm::radians(authored.absoluteRotationEulerDeg)); + + if (authored.hasDelta) + { + if (authored.delta.hasPositionOffset) + outPose.position += authored.delta.positionOffset; + if (authored.delta.hasRotationEulerDegOffset) + outPose.orientation = glm::normalize(outPose.orientation * glm::quat(glm::radians(authored.delta.rotationEulerDegOffset))); + } + + if (!isSequenceTrackedTargetPoseFinite(outPose)) + { + if (error) + *error = "Sequence target keyframe produced a non-finite pose."; + return false; + } + + return true; +} + +inline bool buildSequenceTrackedTargetTrackFromReference( + const CCameraSequenceTrackedTargetPose& reference, + const CCameraSequenceSegment& segment, + CCameraSequenceTrackedTargetTrack& outTrack, + std::string* error = nullptr) +{ + outTrack = {}; + outTrack.keyframes.reserve(segment.targetKeyframes.size()); + + for (const auto& entry : segment.targetKeyframes) + { + CCameraSequenceTrackedTargetTrack::SKeyframe keyframe; + keyframe.time = std::max(0.f, entry.time); + if (!buildSequenceTrackedTargetPoseFromReference(reference, entry, keyframe.pose, error)) + return false; + outTrack.keyframes.emplace_back(std::move(keyframe)); + } + + std::sort(outTrack.keyframes.begin(), outTrack.keyframes.end(), + [](const auto& lhs, const auto& rhs) + { + if (lhs.time == rhs.time) + return false; + return lhs.time < rhs.time; + }); + + return !outTrack.keyframes.empty(); +} + +inline bool tryBuildSequenceTrackedTargetPoseAtTime( + const CCameraSequenceTrackedTargetTrack& track, + const float time, + CCameraSequenceTrackedTargetPose& outPose) +{ + if (track.keyframes.empty()) + return false; + if (track.keyframes.size() == 1u || time <= track.keyframes.front().time) + { + outPose = track.keyframes.front().pose; + return true; + } + if (time >= track.keyframes.back().time) + { + outPose = track.keyframes.back().pose; + return true; + } + + for (size_t ix = 1u; ix < track.keyframes.size(); ++ix) + { + const auto& lhs = track.keyframes[ix - 1u]; + const auto& rhs = track.keyframes[ix]; + if (time > rhs.time) + continue; + + const auto span = std::max(1e-6f, rhs.time - lhs.time); + const auto alpha = std::clamp((time - lhs.time) / span, 0.f, 1.f); + outPose.position = lhs.pose.position + (rhs.pose.position - lhs.pose.position) * static_cast(alpha); + outPose.orientation = glm::normalize(glm::slerp(lhs.pose.orientation, rhs.pose.orientation, alpha)); + return true; + } + + outPose = track.keyframes.back().pose; + return true; +} + +inline bool sequenceSegmentUsesTrackedTargetTrack(const CCameraSequenceSegment& segment) +{ + return !segment.targetKeyframes.empty(); +} + inline float getSequenceSegmentDurationSeconds(const CCameraSequenceScript& script, const CCameraSequenceSegment& segment, const CCameraKeyframeTrack* track = nullptr) { if (segment.hasDurationSeconds) diff --git a/common/include/camera/README.md b/common/include/camera/README.md index 7c2d0b4ef..4401fed4b 100644 --- a/common/include/camera/README.md +++ b/common/include/camera/README.md @@ -51,6 +51,10 @@ and tracked-target follow sits beside the same goal layer: `CTrackedTarget + SCameraFollowConfig -> CCameraGoal -> CCameraGoalSolver` +and sequence-authored tracked-target motion can feed that same follow layer: + +`CCameraSequenceScript.target_keyframes -> CCameraSequenceTrackedTargetTrack -> CTrackedTarget` + ## Core contracts ### `IGimbal.hpp` From 3fb7be092db086994bc04c5c4c8fa7822b22606c Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Mon, 6 Apr 2026 19:00:37 +0200 Subject: [PATCH 167/205] Compile camera sequence policies and follow regressions --- 61_UI/AppInit.cpp | 321 ++++++++++-------- 61_UI/README.md | 11 +- .../CCameraFollowRegressionUtilities.hpp | 83 +++++ .../include/camera/CCameraFollowUtilities.hpp | 37 ++ .../include/camera/CCameraSequenceScript.hpp | 166 ++++++++- common/include/camera/README.md | 33 ++ 6 files changed, 502 insertions(+), 149 deletions(-) diff --git a/61_UI/AppInit.cpp b/61_UI/AppInit.cpp index b68121c0f..ef8a47b16 100644 --- a/61_UI/AppInit.cpp +++ b/61_UI/AppInit.cpp @@ -327,16 +327,6 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) return nbl::hlsl::describePresetCameraMismatch(m_cameraGoalSolver, camera, preset); }; - auto compareGoalToCamera = [&](ICamera* camera, const CCameraGoal& goal, const char* label) -> bool - { - auto capture = m_cameraGoalSolver.captureDetailed(camera); - if (!capture.canUseGoal()) - return fail(std::string("Follow smoke failed to capture camera state for ") + label + "."); - if (!nbl::hlsl::compareGoals(capture.goal, goal, 1e-6, 0.1, 1e-6)) - return fail(std::string("Follow smoke mismatch for ") + label + ". " + nbl::hlsl::describeGoalMismatch(capture.goal, goal)); - return true; - }; - auto tryBuildFollowViewProjForCamera = [&](ICamera* camera, float32_t4x4& outViewProjMatrix) -> bool { if (!camera) @@ -371,6 +361,26 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) return false; }; + auto buildAndValidateFollowTargetContract = [&](ICamera* camera, const CTrackedTarget& trackedTarget, + const SCameraFollowConfig& followConfig, const char* label, nbl::hlsl::SCameraFollowApplyValidationResult& outResult) -> bool + { + std::string regressionError; + float32_t4x4 viewProjMatrix = float32_t4x4(1.0f); + const bool hasViewProjMatrix = tryBuildFollowViewProjForCamera(camera, viewProjMatrix); + if (!nbl::hlsl::buildApplyAndValidateFollowTargetContract( + m_cameraGoalSolver, + camera, + trackedTarget, + followConfig, + outResult, + ®ressionError, + hasViewProjMatrix ? &viewProjMatrix : nullptr)) + { + return fail(std::string("Follow smoke contract failed for ") + label + ". " + regressionError); + } + return true; + }; + auto verifyFollowTargetContract = [&](ICamera* camera, const CTrackedTarget& trackedTarget, const SCameraFollowConfig& followConfig, const CCameraGoal& followGoal, const char* label) -> bool { @@ -885,16 +895,8 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) followConfig.enabled = true; followConfig.mode = ECameraFollowMode::OrbitTarget; - CCameraGoal followGoal = {}; - if (!nbl::hlsl::tryBuildFollowGoal(m_cameraGoalSolver, orbitCamera, trackedTarget, followConfig, followGoal)) - return fail("Orbit follow smoke failed to build follow goal."); - - const auto applyResult = nbl::hlsl::applyFollowToCamera(m_cameraGoalSolver, orbitCamera, trackedTarget, followConfig); - if (!applyResult.succeeded()) - return fail("Orbit follow smoke failed to apply follow goal."); - if (!compareGoalToCamera(orbitCamera, followGoal, "orbit follow")) - return false; - if (!verifyFollowTargetContract(orbitCamera, trackedTarget, followConfig, followGoal, "orbit follow")) + nbl::hlsl::SCameraFollowApplyValidationResult followResult = {}; + if (!buildAndValidateFollowTargetContract(orbitCamera, trackedTarget, followConfig, "orbit follow", followResult)) return false; if (!verifyFollowTargetMarkerAlignment(trackedTarget, "orbit follow")) return false; @@ -907,16 +909,8 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) followConfig.worldOffset = float64_t3(4.0, -1.5, 2.0); trackedTarget.setPose(movedTrackedTargetPosition, movedTrackedTargetOrientation); - CCameraGoal worldOffsetGoal = {}; - if (!nbl::hlsl::tryBuildFollowGoal(m_cameraGoalSolver, orbitCamera, trackedTarget, followConfig, worldOffsetGoal)) - return fail("Orbit keep-world-offset smoke failed to build follow goal."); - - const auto worldOffsetApplyResult = nbl::hlsl::applyFollowToCamera(m_cameraGoalSolver, orbitCamera, trackedTarget, followConfig); - if (!worldOffsetApplyResult.succeeded()) - return fail("Orbit keep-world-offset smoke failed to apply follow goal."); - if (!compareGoalToCamera(orbitCamera, worldOffsetGoal, "orbit keep-world-offset follow")) - return false; - if (!verifyFollowTargetContract(orbitCamera, trackedTarget, followConfig, worldOffsetGoal, "orbit keep-world-offset follow")) + nbl::hlsl::SCameraFollowApplyValidationResult worldOffsetResult = {}; + if (!buildAndValidateFollowTargetContract(orbitCamera, trackedTarget, followConfig, "orbit keep-world-offset follow", worldOffsetResult)) return false; const auto restoreWorldOffsetResult = nbl::hlsl::applyPresetDetailed(m_cameraGoalSolver, orbitCamera, baselinePreset); @@ -946,16 +940,8 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) trackedTarget.setPose(movedTrackedTargetPosition, movedTrackedTargetOrientation); - CCameraGoal followGoal = {}; - if (!nbl::hlsl::tryBuildFollowGoal(m_cameraGoalSolver, defaultFollowCamera, trackedTarget, followConfig, followGoal)) - return fail("Default follow smoke failed to build follow goal for camera \"" + std::string(defaultFollowCamera->getIdentifier()) + "\"."); - - const auto applyResult = nbl::hlsl::applyFollowToCamera(m_cameraGoalSolver, defaultFollowCamera, trackedTarget, followConfig); - if (!applyResult.succeeded()) - return fail("Default follow smoke failed to apply follow goal for camera \"" + std::string(defaultFollowCamera->getIdentifier()) + "\"."); - if (!compareGoalToCamera(defaultFollowCamera, followGoal, label.c_str())) - return false; - if (!verifyFollowTargetContract(defaultFollowCamera, trackedTarget, followConfig, followGoal, label.c_str())) + nbl::hlsl::SCameraFollowApplyValidationResult defaultFollowResult = {}; + if (!buildAndValidateFollowTargetContract(defaultFollowCamera, trackedTarget, followConfig, label.c_str(), defaultFollowResult)) return false; if (!verifyFollowTargetMarkerAlignment(trackedTarget, label.c_str())) return false; @@ -972,16 +958,8 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) followConfig.enabled = true; followConfig.mode = ECameraFollowMode::LookAtTarget; - CCameraGoal followGoal = {}; - if (!nbl::hlsl::tryBuildFollowGoal(m_cameraGoalSolver, freeCamera, trackedTarget, followConfig, followGoal)) - return fail("Free follow smoke failed to build follow goal."); - - const auto applyResult = nbl::hlsl::applyFollowToCamera(m_cameraGoalSolver, freeCamera, trackedTarget, followConfig); - if (!applyResult.succeeded()) - return fail("Free follow smoke failed to apply follow goal."); - if (!compareGoalToCamera(freeCamera, followGoal, "free look-at follow")) - return false; - if (!verifyFollowTargetContract(freeCamera, trackedTarget, followConfig, followGoal, "free look-at follow")) + nbl::hlsl::SCameraFollowApplyValidationResult lookAtResult = {}; + if (!buildAndValidateFollowTargetContract(freeCamera, trackedTarget, followConfig, "free look-at follow", lookAtResult)) return false; if (!verifyFollowTargetMarkerAlignment(trackedTarget, "free look-at follow")) return false; @@ -994,16 +972,8 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) followConfig.worldOffset = float64_t3(5.0, -2.0, 1.5); trackedTarget.setPose(movedTrackedTargetPosition, movedTrackedTargetOrientation); - CCameraGoal keepWorldGoal = {}; - if (!nbl::hlsl::tryBuildFollowGoal(m_cameraGoalSolver, freeCamera, trackedTarget, followConfig, keepWorldGoal)) - return fail("Free keep-world-offset smoke failed to build follow goal."); - - const auto keepWorldApplyResult = nbl::hlsl::applyFollowToCamera(m_cameraGoalSolver, freeCamera, trackedTarget, followConfig); - if (!keepWorldApplyResult.succeeded()) - return fail("Free keep-world-offset smoke failed to apply follow goal."); - if (!compareGoalToCamera(freeCamera, keepWorldGoal, "free keep-world-offset follow")) - return false; - if (!verifyFollowTargetContract(freeCamera, trackedTarget, followConfig, keepWorldGoal, "free keep-world-offset follow")) + nbl::hlsl::SCameraFollowApplyValidationResult keepWorldResult = {}; + if (!buildAndValidateFollowTargetContract(freeCamera, trackedTarget, followConfig, "free keep-world-offset follow", keepWorldResult)) return false; const auto restoreWorldOffsetResult = nbl::hlsl::applyPresetDetailed(m_cameraGoalSolver, freeCamera, baselinePreset); @@ -1022,16 +992,8 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) trackedTarget.setPose(movedTrackedTargetPosition, movedTrackedTargetOrientation); - CCameraGoal followGoal = {}; - if (!nbl::hlsl::tryBuildFollowGoal(m_cameraGoalSolver, chaseCamera, trackedTarget, followConfig, followGoal)) - return fail("Chase follow smoke failed to build follow goal."); - - const auto applyResult = nbl::hlsl::applyFollowToCamera(m_cameraGoalSolver, chaseCamera, trackedTarget, followConfig); - if (!applyResult.succeeded()) - return fail("Chase follow smoke failed to apply follow goal."); - if (!compareGoalToCamera(chaseCamera, followGoal, "chase local-offset follow")) - return false; - if (!verifyFollowTargetContract(chaseCamera, trackedTarget, followConfig, followGoal, "chase local-offset follow")) + nbl::hlsl::SCameraFollowApplyValidationResult localOffsetResult = {}; + if (!buildAndValidateFollowTargetContract(chaseCamera, trackedTarget, followConfig, "chase local-offset follow", localOffsetResult)) return false; if (!verifyFollowTargetMarkerAlignment(trackedTarget, "chase local-offset follow")) return false; @@ -1289,6 +1251,104 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) return fail("Playback timeline smoke failed to clamp cursor time."); } + if (hasOrbitPreset) + { + CCameraSequenceScript sequence; + sequence.fps = 4.f; + sequence.defaults.durationSeconds = 2.f; + sequence.defaults.presentations = { + { .projection = IPlanarProjection::CProjection::Perspective, .leftHanded = true }, + { .projection = IPlanarProjection::CProjection::Orthographic, .leftHanded = false } + }; + sequence.defaults.captureFractions = { 0.f, 0.5f, 1.f }; + + CCameraSequenceSegment segment; + segment.name = "sequence_compile_smoke"; + segment.cameraKind = ICamera::CameraKind::Orbit; + { + CCameraSequenceKeyframe keyframe; + keyframe.time = 0.f; + keyframe.hasAbsolutePreset = true; + keyframe.absolutePreset = initialOrbitPreset; + segment.keyframes.push_back(keyframe); + } + { + nbl::hlsl::CCameraSequenceTrackedTargetKeyframe keyframe; + keyframe.time = 0.f; + keyframe.hasAbsolutePosition = true; + keyframe.absolutePosition = float64_t3(1.0, 2.0, 3.0); + segment.targetKeyframes.push_back(keyframe); + } + { + nbl::hlsl::CCameraSequenceTrackedTargetKeyframe keyframe; + keyframe.time = 1.f; + keyframe.hasAbsolutePosition = true; + keyframe.absolutePosition = float64_t3(4.0, 5.0, 6.0); + segment.targetKeyframes.push_back(keyframe); + } + { + nbl::hlsl::CCameraSequenceTrackedTargetKeyframe keyframe; + keyframe.time = 1.f; + keyframe.hasAbsolutePosition = true; + keyframe.absolutePosition = float64_t3(7.0, 8.0, 9.0); + segment.targetKeyframes.push_back(keyframe); + } + sequence.segments.push_back(segment); + + if (!nbl::hlsl::sequenceScriptUsesMultiplePresentations(sequence)) + return fail("Sequence compile smoke failed to detect multi-presentation authored defaults."); + + const CCameraSequenceTrackedTargetPose referenceTrackedTargetPose = { + .position = getDefaultFollowTargetPosition(), + .orientation = getDefaultFollowTargetOrientation() + }; + + nbl::hlsl::CCameraSequenceCompiledSegment compiledSegment; + std::string compileError; + if (!nbl::hlsl::compileSequenceSegmentFromReference( + sequence, + sequence.segments.front(), + initialOrbitPreset, + referenceTrackedTargetPose, + compiledSegment, + &compileError)) + { + return fail("Sequence compile smoke failed to compile a shared segment. " + compileError); + } + + if (compiledSegment.durationFrames != 8ull || compiledSegment.sampleTimes.size() != 8u) + return fail("Sequence compile smoke produced wrong sampled frame count."); + if (compiledSegment.captureFrameOffsets.size() != 3u || + compiledSegment.captureFrameOffsets[0] != 0ull || + compiledSegment.captureFrameOffsets[1] != 4ull || + compiledSegment.captureFrameOffsets[2] != 7ull) + { + return fail("Sequence compile smoke produced wrong capture frame offsets."); + } + if (compiledSegment.presentations.size() != 2u) + return fail("Sequence compile smoke lost authored presentations."); + if (!compiledSegment.usesTrackedTargetTrack() || compiledSegment.trackedTargetTrack.keyframes.size() != 2u) + return fail("Sequence compile smoke failed to normalize tracked-target keyframes."); + + std::vector framePolicies; + if (!nbl::hlsl::buildCompiledSegmentFramePolicies(compiledSegment, framePolicies, true)) + return fail("Sequence compile smoke failed to build shared frame policies."); + if (framePolicies.size() != 8u) + return fail("Sequence compile smoke produced wrong frame-policy count."); + if (!framePolicies[0].baseline || framePolicies[0].continuityStep || !framePolicies[0].capture) + return fail("Sequence compile smoke produced wrong first-frame policy."); + if (!framePolicies[1].continuityStep || !framePolicies[1].followTargetLock || framePolicies[1].baseline) + return fail("Sequence compile smoke produced wrong continuity follow policy."); + if (!framePolicies[4].capture || !framePolicies[7].capture) + return fail("Sequence compile smoke produced wrong capture milestone policy."); + + CCameraSequenceTrackedTargetPose poseAtOne; + if (!nbl::hlsl::tryBuildSequenceTrackedTargetPoseAtTime(compiledSegment.trackedTargetTrack, 1.f, poseAtOne)) + return fail("Sequence compile smoke failed to sample normalized tracked-target track."); + if (length(poseAtOne.position - float64_t3(7.0, 8.0, 9.0)) > 1e-9) + return fail("Sequence compile smoke did not keep the last authored target pose for duplicate keyframe time."); + } + if (hasOrbitPreset && orbitCamera) { std::array exactTargets = { orbitCamera, nullptr }; @@ -2480,21 +2540,9 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) return match; }; - bool useWindow = sequence.defaults.presentations.size() > 1u; - if (!useWindow) - { - for (const auto& segment : sequence.segments) - { - if (getSequenceSegmentPresentations(sequence, segment).size() > 1u) - { - useWindow = true; - break; - } - } - } + const bool useWindow = nbl::hlsl::sequenceScriptUsesMultiplePresentations(sequence); pushAction(0u, ScriptedInputEvent::ActionData::Kind::SetUseWindow, useWindow ? 1 : 0); - const double fps = std::max(1.0, static_cast(sequence.fps)); const CCameraSequenceTrackedTargetPose referenceTrackedTargetPose = { .position = getDefaultFollowTargetPosition(), .orientation = getDefaultFollowTargetOrientation() @@ -2517,103 +2565,88 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) m_planarFollowConfigs[planarIx.value()].enabled && m_planarFollowConfigs[planarIx.value()].mode != ECameraFollowMode::Disabled; - const auto presentations = getSequenceSegmentPresentations(sequence, segment); - if (presentations.size() > windowBindings.size()) + nbl::hlsl::CCameraSequenceCompiledSegment compiledSegment; + std::string trackError; + if (!nbl::hlsl::compileSequenceSegmentFromReference( + sequence, + segment, + m_initialPlanarPresets[planarIx.value()], + referenceTrackedTargetPose, + compiledSegment, + &trackError)) + { + logFail("Sequence segment \"%s\" failed to compile: %s", segment.name.c_str(), trackError.c_str()); + return false; + } + + if (compiledSegment.presentations.size() > 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(), presentations.size(), windowBindings.size()); + ILogger::ELL_WARNING, segment.name.c_str(), compiledSegment.presentations.size(), windowBindings.size()); } pushAction(frameCursor, ScriptedInputEvent::ActionData::Kind::SetActiveRenderWindow, 0); pushAction(frameCursor, ScriptedInputEvent::ActionData::Kind::SetActivePlanar, static_cast(planarIx.value())); - if (!presentations.empty()) + if (!compiledSegment.presentations.empty()) { - pushAction(frameCursor, ScriptedInputEvent::ActionData::Kind::SetProjectionType, static_cast(presentations[0].projection)); - pushAction(frameCursor, ScriptedInputEvent::ActionData::Kind::SetLeftHanded, presentations[0].leftHanded ? 1 : 0); + pushAction(frameCursor, ScriptedInputEvent::ActionData::Kind::SetProjectionType, static_cast(compiledSegment.presentations[0].projection)); + pushAction(frameCursor, ScriptedInputEvent::ActionData::Kind::SetLeftHanded, compiledSegment.presentations[0].leftHanded ? 1 : 0); } - if (getSequenceSegmentResetCamera(sequence, segment)) + if (compiledSegment.resetCamera) pushAction(frameCursor, ScriptedInputEvent::ActionData::Kind::ResetActiveCamera, 1); - for (size_t windowIx = 1u; windowIx < std::min(presentations.size(), windowBindings.size()); ++windowIx) + for (size_t windowIx = 1u; windowIx < std::min(compiledSegment.presentations.size(), windowBindings.size()); ++windowIx) { pushAction(frameCursor, ScriptedInputEvent::ActionData::Kind::SetActiveRenderWindow, static_cast(windowIx)); pushAction(frameCursor, ScriptedInputEvent::ActionData::Kind::SetActivePlanar, static_cast(planarIx.value())); - pushAction(frameCursor, ScriptedInputEvent::ActionData::Kind::SetProjectionType, static_cast(presentations[windowIx].projection)); - pushAction(frameCursor, ScriptedInputEvent::ActionData::Kind::SetLeftHanded, presentations[windowIx].leftHanded ? 1 : 0); + pushAction(frameCursor, ScriptedInputEvent::ActionData::Kind::SetProjectionType, static_cast(compiledSegment.presentations[windowIx].projection)); + pushAction(frameCursor, ScriptedInputEvent::ActionData::Kind::SetLeftHanded, compiledSegment.presentations[windowIx].leftHanded ? 1 : 0); } pushAction(frameCursor, ScriptedInputEvent::ActionData::Kind::SetActiveRenderWindow, 0); - const auto& referencePreset = m_initialPlanarPresets[planarIx.value()]; - CCameraKeyframeTrack track; - std::string trackError; - if (!buildSequenceTrackFromReference(referencePreset, segment, track, &trackError)) + std::vector framePolicies; + if (!nbl::hlsl::buildCompiledSegmentFramePolicies(compiledSegment, framePolicies, useTrackedTargetFollow)) { - logFail("Sequence segment \"%s\" failed to build track: %s", segment.name.c_str(), trackError.c_str()); + logFail("Sequence segment \"%s\" failed to build compiled frame policies.", segment.name.c_str()); return false; } - CCameraSequenceTrackedTargetTrack trackedTargetTrack; - if (nbl::hlsl::sequenceSegmentUsesTrackedTargetTrack(segment)) - { - if (!nbl::hlsl::buildSequenceTrackedTargetTrackFromReference(referenceTrackedTargetPose, segment, trackedTargetTrack, &trackError)) - { - logFail("Sequence segment \"%s\" failed to build tracked-target track: %s", segment.name.c_str(), trackError.c_str()); - return false; - } - } - - const float durationSeconds = getSequenceSegmentDurationSeconds(sequence, segment, &track); - const uint64_t durationFrames = std::max(1ull, static_cast(std::llround(std::max(0.f, durationSeconds) * static_cast(fps)))); - for (uint64_t frameOffset = 0u; frameOffset < durationFrames; ++frameOffset) + for (const auto& policy : framePolicies) { - const float alpha = durationFrames > 1u ? static_cast(frameOffset) / static_cast(durationFrames - 1u) : 0.f; - const float sampleTime = durationSeconds * alpha; CCameraPreset preset; - if (!tryBuildKeyframeTrackPresetAtTime(track, sampleTime, preset)) + if (!tryBuildKeyframeTrackPresetAtTime(compiledSegment.track, policy.sampleTime, preset)) { - logFail("Sequence segment \"%s\" failed to sample track at t=%f.", segment.name.c_str(), sampleTime); + logFail("Sequence segment \"%s\" failed to sample track at t=%f.", segment.name.c_str(), policy.sampleTime); return false; } - pushGoal(frameCursor + frameOffset, makeGoalFromPreset(preset)); - if (!trackedTargetTrack.keyframes.empty()) + pushGoal(frameCursor + policy.frameOffset, makeGoalFromPreset(preset)); + if (compiledSegment.usesTrackedTargetTrack()) { CCameraSequenceTrackedTargetPose trackedTargetPose; - if (!nbl::hlsl::tryBuildSequenceTrackedTargetPoseAtTime(trackedTargetTrack, sampleTime, trackedTargetPose)) + if (!nbl::hlsl::tryBuildSequenceTrackedTargetPoseAtTime(compiledSegment.trackedTargetTrack, policy.sampleTime, trackedTargetPose)) { - logFail("Sequence segment \"%s\" failed to sample tracked-target track at t=%f.", segment.name.c_str(), sampleTime); + logFail("Sequence segment \"%s\" failed to sample tracked-target track at t=%f.", segment.name.c_str(), policy.sampleTime); return false; } - pushTrackedTargetTransform(frameCursor + frameOffset, trackedTargetPose); + pushTrackedTargetTransform(frameCursor + policy.frameOffset, trackedTargetPose); } - } - const auto continuity = getSequenceSegmentContinuity(sequence, segment); - if (continuity.baseline) - { - ScriptedInputCheck baseline; - baseline.frame = frameCursor; - baseline.kind = ScriptedInputCheck::Kind::Baseline; - m_scriptedInput.checks.emplace_back(std::move(baseline)); - } - if (continuity.step) - { - for (uint64_t frameOffset = 1u; frameOffset < durationFrames; ++frameOffset) + if (policy.baseline) { - pushStepCheck(frameCursor + frameOffset, continuity); - if (useTrackedTargetFollow) - pushFollowTargetLockCheck(frameCursor + frameOffset); + ScriptedInputCheck baseline; + baseline.frame = frameCursor + policy.frameOffset; + baseline.kind = ScriptedInputCheck::Kind::Baseline; + m_scriptedInput.checks.emplace_back(std::move(baseline)); } + if (policy.continuityStep) + pushStepCheck(frameCursor + policy.frameOffset, compiledSegment.continuity); + if (policy.followTargetLock) + pushFollowTargetLockCheck(frameCursor + policy.frameOffset); + if (policy.capture) + m_scriptedInput.captureFrames.emplace_back(frameCursor + policy.frameOffset); } - for (const auto fraction : getSequenceSegmentCaptureFractions(sequence, segment)) - { - const auto offset = durationFrames > 1u ? - static_cast(std::llround(static_cast(fraction) * static_cast(durationFrames - 1u))) : - 0u; - m_scriptedInput.captureFrames.emplace_back(frameCursor + offset); - } - - frameCursor += durationFrames; + frameCursor += compiledSegment.durationFrames; } finalizeScriptedInput(); diff --git a/61_UI/README.md b/61_UI/README.md index 0e3e3ae68..18b58f603 100644 --- a/61_UI/README.md +++ b/61_UI/README.md @@ -59,9 +59,12 @@ The current default follow setup is: - `Chase`, `Dolly` use `KeepLocalOffset` -The follow layer is live-scene behavior only for now. -Scripted continuity/CI runs intentionally do not re-apply follow every frame yet, because the -current sequence asset does not author target animation separately from camera motion. +The tracked target is not the large cone or any particular scene model. +The tracked target is the reusable `CTrackedTarget` gimbal. +`61_UI` only renders a marker and optional reference geometry for that gimbal. + +Scripted continuity/CI runs now drive the same follow layer through authored `target_keyframes`. +That means live runtime and scripted validation both consume the same tracked-target semantics. ## Short math context @@ -107,7 +110,7 @@ FAIL means missing movement, out-of-range movement, invalid state, or missing re Goal: verify smooth frame-to-frame behavior (no visible teleport-like jumps). The continuity asset is now a compact authored camera-sequence spec, not a committed frame dump. -`61_UI` expands that shared camera-domain description into its own runtime scripted checks. +`61_UI` first compiles that shared camera-domain description into normalized sampled segments and shared frame-policy schedules, and only then expands it into its own runtime scripted checks. Per authored segment: diff --git a/common/include/camera/CCameraFollowRegressionUtilities.hpp b/common/include/camera/CCameraFollowRegressionUtilities.hpp index f54a69e68..9c8ba2909 100644 --- a/common/include/camera/CCameraFollowRegressionUtilities.hpp +++ b/common/include/camera/CCameraFollowRegressionUtilities.hpp @@ -37,6 +37,19 @@ struct SCameraFollowRegressionResult float sphericalDistance = 0.0f; }; +//! Bundled reusable follow regression flow. +//! The helper builds a follow goal, applies it, verifies the resulting camera state, +//! and then checks the lock/writeback follow contract. +struct SCameraFollowApplyValidationResult +{ + bool hasGoal = false; + CCameraGoal goal = {}; + CCameraGoalSolver::SApplyResult applyResult = {}; + bool hasCapturedGoal = false; + CCameraGoal capturedGoal = {}; + SCameraFollowRegressionResult regression = {}; +}; + inline bool tryComputeProjectedFollowTargetMetrics( const float32_t4x4& viewProjMatrix, const CTrackedTarget& trackedTarget, @@ -215,6 +228,76 @@ inline bool validateFollowTargetContract( return true; } +inline bool buildApplyAndValidateFollowTargetContract( + const CCameraGoalSolver& solver, + ICamera* camera, + const CTrackedTarget& trackedTarget, + const SCameraFollowConfig& followConfig, + SCameraFollowApplyValidationResult& out, + std::string* error = nullptr, + const float32_t4x4* viewProjMatrix = nullptr, + const float lockAngleToleranceDeg = 0.1f, + const double distanceTolerance = 1e-6, + const double targetTolerance = 1e-9, + const float projectedNdcTolerance = 0.03f, + const double posTolerance = 1e-6, + const double rotToleranceDeg = 0.1, + const double scalarTolerance = 1e-6) +{ + out = {}; + + if (!tryBuildFollowGoal(solver, camera, trackedTarget, followConfig, out.goal)) + { + if (error) + *error = "failed to build follow goal"; + return false; + } + out.hasGoal = true; + + out.applyResult = applyFollowToCamera(solver, camera, trackedTarget, followConfig); + if (!out.applyResult.succeeded()) + { + if (error) + *error = "failed to apply follow goal"; + return false; + } + + const auto capture = solver.captureDetailed(camera); + if (!capture.canUseGoal()) + { + if (error) + *error = "failed to capture camera state after follow apply"; + return false; + } + + out.hasCapturedGoal = true; + out.capturedGoal = capture.goal; + if (!compareGoals(out.capturedGoal, out.goal, posTolerance, rotToleranceDeg, scalarTolerance)) + { + if (error) + *error = std::string("follow goal mismatch. ") + describeGoalMismatch(out.capturedGoal, out.goal); + return false; + } + + if (!validateFollowTargetContract( + camera, + trackedTarget, + followConfig, + out.goal, + out.regression, + error, + lockAngleToleranceDeg, + distanceTolerance, + targetTolerance, + viewProjMatrix, + projectedNdcTolerance)) + { + return false; + } + + return true; +} + } // namespace nbl::hlsl #endif // _C_CAMERA_FOLLOW_REGRESSION_UTILITIES_HPP_ diff --git a/common/include/camera/CCameraFollowUtilities.hpp b/common/include/camera/CCameraFollowUtilities.hpp index dceb649fd..cf9bc5977 100644 --- a/common/include/camera/CCameraFollowUtilities.hpp +++ b/common/include/camera/CCameraFollowUtilities.hpp @@ -74,6 +74,18 @@ class CTrackedTarget gimbal_t m_gimbal; }; +/** +* Follow policy layered on top of a tracked target gimbal. +* +* The modes are intentionally explicit because `follow` is not one behavior: +* +* - `OrbitTarget` keeps a target-relative orbit/path rig and re-centers the tracked target +* - `LookAtTarget` keeps the camera world position and only rotates the view toward the target +* - `KeepWorldOffset` keeps a world-space camera offset from the target and locks the view onto it +* - `KeepLocalOffset` keeps a target-local camera offset and locks the view onto it +* +* The tracked target remains the source of truth. The camera does not own the tracked subject. +*/ enum class ECameraFollowMode : uint8_t { Disabled, @@ -83,6 +95,8 @@ enum class ECameraFollowMode : uint8_t KeepLocalOffset }; +//! Reusable follow configuration interpreted against a tracked target gimbal. +//! `worldOffset` and `localOffset` are only meaningful for their matching offset-based modes. struct SCameraFollowConfig { bool enabled = false; @@ -131,6 +145,24 @@ inline constexpr bool cameraFollowModeLocksViewToTarget(const ECameraFollowMode } } +inline constexpr bool cameraFollowModeMovesCameraPosition(const ECameraFollowMode mode) +{ + switch (mode) + { + case ECameraFollowMode::OrbitTarget: + case ECameraFollowMode::KeepWorldOffset: + case ECameraFollowMode::KeepLocalOffset: + return true; + default: + return false; + } +} + +inline constexpr bool cameraFollowModeKeepsCameraWorldPosition(const ECameraFollowMode mode) +{ + return mode == ECameraFollowMode::LookAtTarget; +} + inline constexpr bool cameraFollowModeUsesWorldOffset(const ECameraFollowMode mode) { return mode == ECameraFollowMode::KeepWorldOffset; @@ -141,6 +173,11 @@ inline constexpr bool cameraFollowModeUsesLocalOffset(const ECameraFollowMode mo return mode == ECameraFollowMode::KeepLocalOffset; } +inline constexpr bool cameraFollowModeUsesTrackedTargetLocalFrame(const ECameraFollowMode mode) +{ + return mode == ECameraFollowMode::KeepLocalOffset; +} + inline constexpr bool cameraFollowModeUsesCapturedOffset(const ECameraFollowMode mode) { return cameraFollowModeUsesWorldOffset(mode) || cameraFollowModeUsesLocalOffset(mode); diff --git a/common/include/camera/CCameraSequenceScript.hpp b/common/include/camera/CCameraSequenceScript.hpp index 6b9e2c5ac..98795a215 100644 --- a/common/include/camera/CCameraSequenceScript.hpp +++ b/common/include/camera/CCameraSequenceScript.hpp @@ -141,6 +141,7 @@ struct CCameraSequenceTrackedTargetKeyframe }; //! Runtime sampled tracked-target track built from an authored segment plus a reference pose. +//! Keyframes are normalized by time before sampling. Duplicate times collapse to the last authored pose. struct CCameraSequenceTrackedTargetTrack { struct SKeyframe @@ -207,6 +208,39 @@ struct CCameraSequenceScript std::vector segments; }; +//! Reusable compiled sequence segment derived from authored data plus captured references. +//! Consumers can build their own runtime actions/checks from this normalized representation. +struct CCameraSequenceCompiledSegment +{ + std::string name; + std::vector presentations; + CCameraSequenceContinuitySettings continuity = {}; + bool resetCamera = true; + float durationSeconds = 0.f; + uint64_t durationFrames = 0ull; + std::vector sampleTimes; + std::vector captureFrameOffsets; + CCameraKeyframeTrack track = {}; + CCameraSequenceTrackedTargetTrack trackedTargetTrack = {}; + + inline bool usesTrackedTargetTrack() const + { + return !trackedTargetTrack.keyframes.empty(); + } +}; + +//! One compiled frame policy entry derived from a reusable compiled segment. +//! Consumers can map these booleans to their own runtime checks and capture requests. +struct CCameraSequenceCompiledFramePolicy +{ + uint64_t frameOffset = 0ull; + float sampleTime = 0.f; + bool capture = false; + bool baseline = false; + bool continuityStep = false; + bool followTargetLock = false; +}; + inline bool tryParseCameraKind(std::string_view value, ICamera::CameraKind& outKind) { if (value == "FPS") @@ -1025,7 +1059,7 @@ inline bool buildSequenceTrackedTargetTrackFromReference( outTrack.keyframes.emplace_back(std::move(keyframe)); } - std::sort(outTrack.keyframes.begin(), outTrack.keyframes.end(), + std::stable_sort(outTrack.keyframes.begin(), outTrack.keyframes.end(), [](const auto& lhs, const auto& rhs) { if (lhs.time == rhs.time) @@ -1033,6 +1067,17 @@ inline bool buildSequenceTrackedTargetTrackFromReference( return lhs.time < rhs.time; }); + std::vector normalized; + normalized.reserve(outTrack.keyframes.size()); + for (const auto& keyframe : outTrack.keyframes) + { + if (!normalized.empty() && std::abs(normalized.back().time - keyframe.time) <= 1e-6f) + normalized.back() = keyframe; + else + normalized.emplace_back(keyframe); + } + outTrack.keyframes = std::move(normalized); + return !outTrack.keyframes.empty(); } @@ -1110,6 +1155,125 @@ inline bool getSequenceSegmentResetCamera(const CCameraSequenceScript& script, c return segment.hasResetCamera ? segment.resetCamera : script.defaults.resetCamera; } +inline bool sequenceScriptUsesMultiplePresentations(const CCameraSequenceScript& script) +{ + if (script.defaults.presentations.size() > 1u) + return true; + + for (const auto& segment : script.segments) + { + if (getSequenceSegmentPresentations(script, segment).size() > 1u) + return true; + } + + return false; +} + +inline uint64_t buildSequenceDurationFrames(const float durationSeconds, const float fps) +{ + const auto safeDuration = std::max(0.f, durationSeconds); + const auto safeFps = std::max(1.f, fps); + return std::max(1ull, static_cast(std::llround(static_cast(safeDuration) * static_cast(safeFps)))); +} + +//! Build one sampled time per authored frame in the compiled segment. +inline void buildSequenceSampleTimes(const float durationSeconds, const uint64_t durationFrames, std::vector& outTimes) +{ + outTimes.clear(); + outTimes.reserve(durationFrames); + + for (uint64_t frameOffset = 0u; frameOffset < durationFrames; ++frameOffset) + { + const float alpha = durationFrames > 1u ? static_cast(frameOffset) / static_cast(durationFrames - 1u) : 0.f; + outTimes.emplace_back(durationSeconds * alpha); + } +} + +//! Expand normalized capture fractions into concrete frame offsets inside the compiled segment. +inline void buildSequenceCaptureFrameOffsets( + const uint64_t durationFrames, + const std::vector& captureFractions, + std::vector& outOffsets) +{ + outOffsets.clear(); + outOffsets.reserve(captureFractions.size()); + + for (const auto fraction : captureFractions) + { + const auto offset = durationFrames > 1u ? + static_cast(std::llround(static_cast(fraction) * static_cast(durationFrames - 1u))) : + 0ull; + outOffsets.emplace_back(offset); + } + + std::sort(outOffsets.begin(), outOffsets.end()); + outOffsets.erase(std::unique(outOffsets.begin(), outOffsets.end()), outOffsets.end()); +} + +//! Compile one authored sequence segment into normalized reusable data for runtime consumers. +inline bool compileSequenceSegmentFromReference( + const CCameraSequenceScript& script, + const CCameraSequenceSegment& segment, + const CCameraPreset& referencePreset, + const CCameraSequenceTrackedTargetPose& referenceTrackedTargetPose, + CCameraSequenceCompiledSegment& outSegment, + std::string* error = nullptr) +{ + outSegment = {}; + outSegment.name = segment.name; + outSegment.presentations = getSequenceSegmentPresentations(script, segment); + outSegment.continuity = getSequenceSegmentContinuity(script, segment); + outSegment.resetCamera = getSequenceSegmentResetCamera(script, segment); + + if (!buildSequenceTrackFromReference(referencePreset, segment, outSegment.track, error)) + return false; + + if (sequenceSegmentUsesTrackedTargetTrack(segment) && + !buildSequenceTrackedTargetTrackFromReference(referenceTrackedTargetPose, segment, outSegment.trackedTargetTrack, error)) + { + return false; + } + + outSegment.durationSeconds = getSequenceSegmentDurationSeconds(script, segment, &outSegment.track); + outSegment.durationFrames = buildSequenceDurationFrames(outSegment.durationSeconds, script.fps); + buildSequenceSampleTimes(outSegment.durationSeconds, outSegment.durationFrames, outSegment.sampleTimes); + buildSequenceCaptureFrameOffsets(outSegment.durationFrames, getSequenceSegmentCaptureFractions(script, segment), outSegment.captureFrameOffsets); + return true; +} + +inline bool buildCompiledSegmentFramePolicies( + const CCameraSequenceCompiledSegment& segment, + std::vector& outPolicies, + const bool includeFollowTargetLock = false) +{ + if (segment.sampleTimes.size() != segment.durationFrames) + return false; + + outPolicies.clear(); + outPolicies.reserve(segment.durationFrames); + + size_t captureIx = 0u; + for (uint64_t frameOffset = 0u; frameOffset < segment.durationFrames; ++frameOffset) + { + CCameraSequenceCompiledFramePolicy policy; + policy.frameOffset = frameOffset; + policy.sampleTime = segment.sampleTimes[frameOffset]; + policy.baseline = segment.continuity.baseline && frameOffset == 0u; + policy.continuityStep = segment.continuity.step && frameOffset > 0u; + policy.followTargetLock = includeFollowTargetLock && segment.usesTrackedTargetTrack() && policy.continuityStep; + + while (captureIx < segment.captureFrameOffsets.size() && segment.captureFrameOffsets[captureIx] < frameOffset) + ++captureIx; + policy.capture = captureIx < segment.captureFrameOffsets.size() && segment.captureFrameOffsets[captureIx] == frameOffset; + if (policy.capture) + ++captureIx; + + outPolicies.emplace_back(std::move(policy)); + } + + return true; +} + } // namespace nbl::hlsl #endif // _C_CAMERA_SEQUENCE_SCRIPT_HPP_ diff --git a/common/include/camera/README.md b/common/include/camera/README.md index 4401fed4b..bc5c3be63 100644 --- a/common/include/camera/README.md +++ b/common/include/camera/README.md @@ -278,6 +278,7 @@ Important design points: - the tracked subject owns its own gimbal through `CTrackedTarget` - follow modes map target motion into `CCameraGoal` - goal application still goes through `CCameraGoalSolver` +- follow semantics are defined against the tracked-target gimbal, not against any scene model or mesh Current follow modes: @@ -286,6 +287,22 @@ Current follow modes: - `KeepWorldOffset` - `KeepLocalOffset` +Current follow invariants: + +- all enabled follow modes lock the final camera view onto the tracked-target position +- `LookAtTarget` keeps the camera world position and only rotates it toward the target +- `OrbitTarget` keeps the camera on a target-relative spherical/path rig and recenters the tracked target +- `KeepWorldOffset` keeps a world-space offset from the tracked target and recenters it +- `KeepLocalOffset` keeps an offset in the tracked-target local frame and recenters it +- the tracked target remains the source of truth; the camera does not own the subject + +This is why the regression layer validates: + +- camera forward axis vs. camera-to-target direction +- projected target center error in NDC +- spherical target writeback for spherical cameras +- target distance consistency after apply + This keeps the camera runtime contract event-driven while still allowing higher-level tracking behavior to be reused by tools and examples. @@ -413,8 +430,12 @@ Provides: - `CCameraSequenceKeyframe` - `CCameraSequenceSegment` - `CCameraSequenceScript` +- `CCameraSequenceCompiledSegment` +- `CCameraSequenceCompiledFramePolicy` - parsing and normalization helpers - reusable sequence-to-track construction from captured reference presets +- reusable segment compilation from authored data into sampled times, capture offsets, and normalized tracks +- reusable frame-policy scheduling for baseline, continuity-step, follow-lock, and capture milestones The important design rule is that the authored format stays camera-domain: @@ -429,6 +450,18 @@ and deliberately does not store: - `61_UI`-specific window-routing commands as authored source data - ImGuizmo matrices as the authored motion primitive +Tracked-target motion follows the same rule: + +- authored `target_keyframes` describe only tracked-target pose over time +- they do not refer to a scene object id, mesh, or `61_UI` model +- consumers may map those poses to their own runtime objects, but the authored source of truth stays a tracked-target gimbal track + +Track normalization rules: + +- negative keyframe times clamp to `0` +- tracked-target keyframes are sorted by time before sampling +- duplicate tracked-target keyframe times collapse to the last authored pose + That makes the same authored sequence usable by any future consumer that understands the shared camera API, even if its runtime expansion path differs from `61_UI`. ### `CCameraPersistence.hpp` From dea6ca0564593991960c31c0ef7d9de868aa1312 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Tue, 7 Apr 2026 07:02:01 +0200 Subject: [PATCH 168/205] Share scripted runtime camera utilities --- 61_UI/AppInit.cpp | 974 +++++++----------- 61_UI/AppUpdate.cpp | 559 +++------- 61_UI/AppWorkLoop.cpp | 6 +- 61_UI/README.md | 11 +- 61_UI/include/app/App.hpp | 131 +-- 61_UI/include/common.hpp | 16 + .../CCameraFollowRegressionUtilities.hpp | 46 + .../camera/CCameraScriptedCheckRunner.hpp | 539 ++++++++++ .../include/camera/CCameraScriptedRuntime.hpp | 360 +++++++ .../CCameraScriptedRuntimePersistence.hpp | 697 +++++++++++++ .../camera/CCameraSequenceScriptedBuilder.hpp | 125 +++ common/include/camera/README.md | 78 ++ 12 files changed, 2363 insertions(+), 1179 deletions(-) create mode 100644 common/include/camera/CCameraScriptedCheckRunner.hpp create mode 100644 common/include/camera/CCameraScriptedRuntime.hpp create mode 100644 common/include/camera/CCameraScriptedRuntimePersistence.hpp create mode 100644 common/include/camera/CCameraSequenceScriptedBuilder.hpp diff --git a/61_UI/AppInit.cpp b/61_UI/AppInit.cpp index ef8a47b16..7e9df23e5 100644 --- a/61_UI/AppInit.cpp +++ b/61_UI/AppInit.cpp @@ -361,6 +361,18 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) return false; }; + auto buildFollowVisualMetricsForCamera = [&](ICamera* camera, const CTrackedTarget& trackedTarget, + const SCameraFollowConfig& followConfig) -> SCameraFollowVisualMetrics + { + float32_t4x4 viewProjMatrix = float32_t4x4(1.0f); + const bool hasViewProjMatrix = tryBuildFollowViewProjForCamera(camera, viewProjMatrix); + return nbl::hlsl::buildFollowVisualMetrics( + camera, + trackedTarget, + &followConfig, + hasViewProjMatrix ? &viewProjMatrix : nullptr); + }; + auto buildAndValidateFollowTargetContract = [&](ICamera* camera, const CTrackedTarget& trackedTarget, const SCameraFollowConfig& followConfig, const char* label, nbl::hlsl::SCameraFollowApplyValidationResult& outResult) -> bool { @@ -381,6 +393,202 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) return true; }; + auto verifyFollowVisualMetrics = [&](ICamera* camera, const CTrackedTarget& trackedTarget, + const SCameraFollowConfig& followConfig, const char* label) -> bool + { + const auto metrics = buildFollowVisualMetricsForCamera(camera, trackedTarget, followConfig); + float32_t4x4 viewProjMatrix = float32_t4x4(1.0f); + const bool expectsProjectedMetrics = tryBuildFollowViewProjForCamera(camera, viewProjMatrix); + if (!metrics.active) + return fail(std::string("Follow visual metrics smoke was inactive for ") + label + "."); + if (nbl::hlsl::cameraFollowModeLocksViewToTarget(followConfig.mode) && !metrics.lockValid) + return fail(std::string("Follow visual metrics smoke was missing lock metrics for ") + label + "."); + if (expectsProjectedMetrics && !metrics.projectedValid) + return fail(std::string("Follow visual metrics smoke was missing projected metrics for ") + label + "."); + if (metrics.projectedValid && metrics.projectedNdcRadius > 0.03f) + return fail(std::string("Follow visual metrics smoke had projected center error for ") + label + "."); + return true; + }; + + auto verifyScriptedRuntimeFrameBatch = [&]() -> bool + { + CCameraScriptedTimeline timeline = {}; + nbl::hlsl::appendScriptedActionEvent(timeline, 3u, CCameraScriptedInputEvent::ActionData::Kind::SetActivePlanar, 4); + { + CCameraGoal goal = {}; + goal.position = float64_t3(1.0, 2.0, 3.0); + nbl::hlsl::appendScriptedGoalEvent(timeline, 3u, goal, true); + } + nbl::hlsl::appendScriptedSegmentLabelEvent(timeline, 3u, "segment-three"); + { + float64_t4x4 transform = float64_t4x4(1.0); + transform[3] = glm::dvec4(7.0, 8.0, 9.0, 1.0); + nbl::hlsl::appendScriptedTrackedTargetTransformEvent(timeline, 4u, transform); + } + + size_t nextEventIndex = 0u; + CCameraScriptedFrameEvents batch; + nbl::hlsl::dequeueScriptedFrameEvents(timeline.events, nextEventIndex, 3u, batch); + if (nextEventIndex != 3u || batch.actions.size() != 1u || batch.goals.size() != 1u || + batch.segmentLabels.size() != 1u || !batch.mouse.empty() || !batch.keyboard.empty()) + { + return fail("Scripted runtime frame batch smoke failed for frame 3."); + } + if (batch.actions.front().kind != CCameraScriptedInputEvent::ActionData::Kind::SetActivePlanar || + batch.actions.front().value != 4 || batch.segmentLabels.front() != "segment-three") + { + return fail("Scripted runtime frame batch payload smoke failed for frame 3."); + } + + nbl::hlsl::dequeueScriptedFrameEvents(timeline.events, nextEventIndex, 4u, batch); + if (nextEventIndex != timeline.events.size() || batch.trackedTargetTransforms.size() != 1u || + !batch.actions.empty() || !batch.goals.empty()) + { + return fail("Scripted runtime frame batch smoke failed for frame 4."); + } + const auto trackedTargetPosition = float64_t3(batch.trackedTargetTransforms.front().transform[3]); + if (!nearlyEqual3(trackedTargetPosition, float64_t3(7.0, 8.0, 9.0), 1e-9)) + return fail("Scripted runtime tracked-target payload smoke failed."); + + return true; + }; + + auto verifyScriptedRuntimeParser = [&]() -> bool + { + nbl_json script = { + { "enabled", true }, + { "capture_prefix", "parser_smoke" }, + { "camera_controls", { + { "keyboard_scale", 2.0f }, + { "rotation_scale", 0.5f } + } }, + { "events", nbl_json::array({ + { + { "frame", 2u }, + { "type", "action" }, + { "action", "set_active_planar" }, + { "value", 3 } + }, + { + { "frame", 2u }, + { "type", "keyboard" }, + { "key", "KEY_KEY_W" }, + { "action", "pressed" }, + { "capture", true } + } + }) }, + { "checks", nbl_json::array({ + { + { "frame", 2u }, + { "kind", "baseline" } + }, + { + { "frame", 3u }, + { "kind", "gimbal_step" }, + { "min_pos_delta", 0.01f }, + { "max_pos_delta", 1.0f } + } + }) } + }; + + CCameraScriptedInputParseResult parsed; + std::string parseError; + if (!nbl::hlsl::deserializeCameraScriptedInput(script, parsed, &parseError)) + return fail("Scripted runtime parser smoke failed to parse low-level runtime payload. " + parseError); + if (!parsed.enabled || parsed.capturePrefix != "parser_smoke" || !parsed.cameraControls.hasKeyboardScale || !parsed.cameraControls.hasRotationScale) + return fail("Scripted runtime parser smoke lost top-level metadata."); + if (parsed.timeline.events.size() != 2u || parsed.timeline.checks.size() != 2u || parsed.timeline.captureFrames.size() != 1u) + return fail("Scripted runtime parser smoke produced wrong payload counts."); + if (parsed.timeline.captureFrames.front() != 2u) + return fail("Scripted runtime parser smoke produced wrong capture frame."); + + size_t nextEventIndex = 0u; + CCameraScriptedFrameEvents batch; + nbl::hlsl::dequeueScriptedFrameEvents(parsed.timeline.events, nextEventIndex, 2u, batch); + if (batch.actions.size() != 1u || batch.keyboard.size() != 1u || batch.actions.front().value != 3) + return fail("Scripted runtime parser smoke produced wrong frame-two batch."); + if (parsed.timeline.checks.front().kind != CCameraScriptedInputCheck::Kind::Baseline || + parsed.timeline.checks.back().kind != CCameraScriptedInputCheck::Kind::GimbalStep) + { + return fail("Scripted runtime parser smoke produced wrong check kinds."); + } + + return true; + }; + + auto verifyScriptedCheckRunner = [&]() -> bool + { + auto orbitCamera = core::make_smart_refctd_ptr(float64_t3(0.0, 1.5, -6.0), float64_t3(0.0, 0.0, 0.0)); + CTrackedTarget trackedTarget( + float64_t3(2.0, 0.5, -1.5), + glm::angleAxis(glm::radians(35.0), float64_t3(0.0, 1.0, 0.0))); + + CCameraScriptedTimeline timeline = {}; + nbl::hlsl::appendScriptedBaselineCheck(timeline, 1u); + nbl::hlsl::appendScriptedGimbalStepCheck(timeline, 2u, true, 2.0f, 0.005f, true, 45.0f, 0.05f); + nbl::hlsl::appendScriptedFollowTargetLockCheck(timeline, 3u, 0.1f, 0.03f); + + CCameraScriptedCheckRuntimeState state = {}; + { + const auto frameResult = evaluateScriptedChecksForFrame( + timeline.checks, + state, + { + .frame = 1u, + .camera = orbitCamera.get() + }); + if (frameResult.hadFailures || state.nextCheckIndex != 1u || !state.baselineValid || !state.stepValid) + return fail("Scripted check runner baseline smoke failed."); + } + + { + CVirtualGimbalEvent stepEvent = {}; + stepEvent.type = CVirtualGimbalEvent::MoveRight; + stepEvent.magnitude = 12.0; + if (!orbitCamera->manipulate({ &stepEvent, 1u })) + return fail("Scripted check runner smoke failed to manipulate the camera for step validation."); + + const auto frameResult = evaluateScriptedChecksForFrame( + timeline.checks, + state, + { + .frame = 2u, + .camera = orbitCamera.get() + }); + if (frameResult.hadFailures || state.nextCheckIndex != 2u) + { + const auto details = !frameResult.logs.empty() ? frameResult.logs.front().text : std::string("missing log details"); + return fail(std::string("Scripted check runner step smoke failed. ") + details); + } + } + + SCameraFollowConfig followConfig = {}; + followConfig.enabled = true; + followConfig.mode = ECameraFollowMode::OrbitTarget; + if (!nbl::hlsl::applyFollowToCamera(m_cameraGoalSolver, orbitCamera.get(), trackedTarget, followConfig).succeeded()) + return fail("Scripted check runner smoke failed to apply follow before follow-lock validation."); + + { + const auto frameResult = evaluateScriptedChecksForFrame( + timeline.checks, + state, + { + .frame = 3u, + .camera = orbitCamera.get(), + .trackedTarget = &trackedTarget, + .followConfig = &followConfig, + .goalSolver = &m_cameraGoalSolver + }); + if (frameResult.hadFailures || state.nextCheckIndex != timeline.checks.size()) + { + const auto details = !frameResult.logs.empty() ? frameResult.logs.front().text : std::string("missing log details"); + return fail(std::string("Scripted check runner follow-lock smoke failed. ") + details); + } + } + + return true; + }; + auto verifyFollowTargetContract = [&](ICamera* camera, const CTrackedTarget& trackedTarget, const SCameraFollowConfig& followConfig, const CCameraGoal& followGoal, const char* label) -> bool { @@ -486,6 +694,13 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) return true; }; + if (!verifyScriptedRuntimeFrameBatch()) + return false; + if (!verifyScriptedRuntimeParser()) + return false; + if (!verifyScriptedCheckRunner()) + return false; + auto collectKeyboardVirtualEvents = [&](CGimbalInputBinder& inputBinder, const ui::E_KEY_CODE keyCode) -> std::vector { static std::chrono::microseconds smokeTimestamp = std::chrono::microseconds::zero(); @@ -898,6 +1113,8 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) nbl::hlsl::SCameraFollowApplyValidationResult followResult = {}; if (!buildAndValidateFollowTargetContract(orbitCamera, trackedTarget, followConfig, "orbit follow", followResult)) return false; + if (!verifyFollowVisualMetrics(orbitCamera, trackedTarget, followConfig, "orbit follow")) + return false; if (!verifyFollowTargetMarkerAlignment(trackedTarget, "orbit follow")) return false; @@ -912,6 +1129,8 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) nbl::hlsl::SCameraFollowApplyValidationResult worldOffsetResult = {}; if (!buildAndValidateFollowTargetContract(orbitCamera, trackedTarget, followConfig, "orbit keep-world-offset follow", worldOffsetResult)) return false; + if (!verifyFollowVisualMetrics(orbitCamera, trackedTarget, followConfig, "orbit keep-world-offset follow")) + return false; const auto restoreWorldOffsetResult = nbl::hlsl::applyPresetDetailed(m_cameraGoalSolver, orbitCamera, baselinePreset); if (!restoreWorldOffsetResult.succeeded() || !comparePresetToCamera(orbitCamera, baselinePreset, 1e-6, 0.1, 1e-6)) @@ -943,6 +1162,8 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) nbl::hlsl::SCameraFollowApplyValidationResult defaultFollowResult = {}; if (!buildAndValidateFollowTargetContract(defaultFollowCamera, trackedTarget, followConfig, label.c_str(), defaultFollowResult)) return false; + if (!verifyFollowVisualMetrics(defaultFollowCamera, trackedTarget, followConfig, label.c_str())) + return false; if (!verifyFollowTargetMarkerAlignment(trackedTarget, label.c_str())) return false; @@ -961,6 +1182,8 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) nbl::hlsl::SCameraFollowApplyValidationResult lookAtResult = {}; if (!buildAndValidateFollowTargetContract(freeCamera, trackedTarget, followConfig, "free look-at follow", lookAtResult)) return false; + if (!verifyFollowVisualMetrics(freeCamera, trackedTarget, followConfig, "free look-at follow")) + return false; if (!verifyFollowTargetMarkerAlignment(trackedTarget, "free look-at follow")) return false; @@ -975,6 +1198,8 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) nbl::hlsl::SCameraFollowApplyValidationResult keepWorldResult = {}; if (!buildAndValidateFollowTargetContract(freeCamera, trackedTarget, followConfig, "free keep-world-offset follow", keepWorldResult)) return false; + if (!verifyFollowVisualMetrics(freeCamera, trackedTarget, followConfig, "free keep-world-offset follow")) + return false; const auto restoreWorldOffsetResult = nbl::hlsl::applyPresetDetailed(m_cameraGoalSolver, freeCamera, baselinePreset); if (!restoreWorldOffsetResult.succeeded() || !comparePresetToCamera(freeCamera, baselinePreset, 1e-6, 0.1, 1e-6)) @@ -995,6 +1220,8 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) nbl::hlsl::SCameraFollowApplyValidationResult localOffsetResult = {}; if (!buildAndValidateFollowTargetContract(chaseCamera, trackedTarget, followConfig, "chase local-offset follow", localOffsetResult)) return false; + if (!verifyFollowVisualMetrics(chaseCamera, trackedTarget, followConfig, "chase local-offset follow")) + return false; if (!verifyFollowTargetMarkerAlignment(trackedTarget, "chase local-offset follow")) return false; @@ -1347,6 +1574,69 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) return fail("Sequence compile smoke failed to sample normalized tracked-target track."); if (length(poseAtOne.position - float64_t3(7.0, 8.0, 9.0)) > 1e-9) return fail("Sequence compile smoke did not keep the last authored target pose for duplicate keyframe time."); + + CCameraScriptedTimeline scriptedTimeline; + std::string runtimeBuildError; + if (!nbl::hlsl::appendCompiledSequenceSegmentToScriptedTimeline( + scriptedTimeline, + 11u, + compiledSegment, + { + .planarIx = 5u, + .availableWindowCount = 2u, + .useWindow = true, + .includeFollowTargetLock = true + }, + &runtimeBuildError)) + { + return fail("Sequence runtime builder smoke failed to append a compiled segment. " + runtimeBuildError); + } + nbl::hlsl::finalizeScriptedTimeline(scriptedTimeline); + + if (scriptedTimeline.captureFrames.size() != 3u || + scriptedTimeline.captureFrames[0] != 11ull || + scriptedTimeline.captureFrames[1] != 15ull || + scriptedTimeline.captureFrames[2] != 18ull) + { + return fail("Sequence runtime builder smoke produced wrong capture frames."); + } + + 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 != 1u || stepChecks != 7u || followChecks != 7u) + return fail("Sequence runtime builder smoke produced wrong scripted check counts."); + + size_t runtimeNextEventIndex = 0u; + CCameraScriptedFrameEvents runtimeBatch; + nbl::hlsl::dequeueScriptedFrameEvents(scriptedTimeline.events, runtimeNextEventIndex, 11u, runtimeBatch); + if (runtimeBatch.actions.size() != 10u || runtimeBatch.goals.size() != 1u || + runtimeBatch.trackedTargetTransforms.size() != 1u || runtimeBatch.segmentLabels.size() != 1u) + { + return fail("Sequence runtime builder smoke produced wrong first-frame batch."); + } + if (runtimeBatch.actions.front().kind != CCameraScriptedInputEvent::ActionData::Kind::SetActiveRenderWindow || + runtimeBatch.segmentLabels.front() != "sequence_compile_smoke") + { + return fail("Sequence runtime builder smoke lost first-frame scripted payload."); + } } if (hasOrbitPreset && orbitCamera) @@ -1624,22 +1914,18 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) bool scriptedInputParseFailed = false; std::string scriptedInputParseError; + auto resetScriptedInputRuntimeState = [&]() -> void + { + m_scriptedInput.nextEventIndex = 0; + m_scriptedInput.checkRuntime = {}; + m_scriptedInput.nextCaptureIndex = 0; + m_scriptedInput.failed = false; + m_scriptedInput.summaryReported = false; + }; + auto finalizeScriptedInput = [&]() -> void { - std::sort(m_scriptedInput.events.begin(), m_scriptedInput.events.end(), - [](const ScriptedInputEvent& a, const ScriptedInputEvent& b) { return a.frame < b.frame; }); - std::sort(m_scriptedInput.checks.begin(), m_scriptedInput.checks.end(), - [](const ScriptedInputCheck& a, const ScriptedInputCheck& b) { return a.frame < b.frame; }); - if (!m_scriptedInput.captureFrames.empty()) - { - std::sort(m_scriptedInput.captureFrames.begin(), m_scriptedInput.captureFrames.end()); - m_scriptedInput.captureFrames.erase(std::unique(m_scriptedInput.captureFrames.begin(), m_scriptedInput.captureFrames.end()), m_scriptedInput.captureFrames.end()); - } - if (m_disableScreenshotsCli) - { - m_scriptedInput.captureFrames.clear(); - m_scriptedInput.nextCaptureIndex = 0; - } + nbl::hlsl::finalizeScriptedTimeline(m_scriptedInput.timeline, m_disableScreenshotsCli); }; auto parseScriptedInput = [&](const nbl_json& script) -> void @@ -1647,16 +1933,8 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) pendingScriptedSequence.reset(); scriptedInputParseFailed = false; scriptedInputParseError.clear(); - m_scriptedInput.events.clear(); - m_scriptedInput.checks.clear(); - m_scriptedInput.captureFrames.clear(); - m_scriptedInput.nextEventIndex = 0; - m_scriptedInput.nextCheckIndex = 0; - m_scriptedInput.nextCaptureIndex = 0; - m_scriptedInput.failed = false; - m_scriptedInput.summaryReported = false; - m_scriptedInput.baselineValid = false; - m_scriptedInput.stepValid = false; + m_scriptedInput.timeline.clear(); + resetScriptedInputRuntimeState(); m_scriptedInput.exclusive = false; m_scriptedInput.hardFail = false; m_scriptedInput.visualDebug = false; @@ -1671,23 +1949,20 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) m_scriptedInput.capturePrefix = "script"; m_scriptedInput.captureOutputDir = localOutputCWD; - if (script.contains("enabled")) - m_scriptedInput.enabled = script["enabled"].get(); - else - m_scriptedInput.enabled = true; - - if (script.contains("log")) - m_scriptedInput.log = script["log"].get() || m_scriptedInput.log; - - if (script.contains("hard_fail")) - m_scriptedInput.hardFail = script["hard_fail"].get(); + CCameraScriptedInputParseResult parsed; + if (!nbl::hlsl::deserializeCameraScriptedInput(script, parsed, &scriptedInputParseError)) + { + scriptedInputParseFailed = true; + return; + } - if (script.contains("visual_debug")) - m_scriptedInput.visualDebug = script["visual_debug"].get(); - if (script.contains("visual_debug_target_fps")) - m_scriptedInput.visualTargetFps = script["visual_debug_target_fps"].get(); - if (script.contains("visual_debug_hold_seconds")) - m_scriptedInput.visualCameraHoldSeconds = script["visual_debug_hold_seconds"].get(); + 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_scriptVisualDebugCli) m_scriptedInput.visualDebug = true; if (m_scriptedInput.visualDebug) @@ -1698,447 +1973,31 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) m_scriptedInput.visualCameraHoldSeconds = 3.f; } - if (script.contains("enableActiveCameraMovement")) - enableActiveCameraMovement = script["enableActiveCameraMovement"].get(); + if (parsed.hasEnableActiveCameraMovement) + enableActiveCameraMovement = parsed.enableActiveCameraMovement; else if (m_scriptedInput.enabled) enableActiveCameraMovement = true; - if (script.contains("exclusive_input")) - m_scriptedInput.exclusive = script["exclusive_input"].get() || m_scriptedInput.exclusive; - if (script.contains("exclusive")) - m_scriptedInput.exclusive = script["exclusive"].get() || m_scriptedInput.exclusive; - - if (script.contains("capture_prefix")) - m_scriptedInput.capturePrefix = script["capture_prefix"].get(); - if (m_scriptedInput.capturePrefix.empty()) - m_scriptedInput.capturePrefix = "script"; - if (script.contains("capture_frames")) - for (const auto& frame : script["capture_frames"]) - m_scriptedInput.captureFrames.emplace_back(frame.get()); - - if (script.contains("camera_controls")) - { - const auto& controls = script["camera_controls"]; - if (controls.contains("keyboard_scale")) - m_cameraControls.keyboardScale = controls["keyboard_scale"].get(); - if (controls.contains("mouse_move_scale")) - m_cameraControls.mouseMoveScale = controls["mouse_move_scale"].get(); - if (controls.contains("mouse_scroll_scale")) - m_cameraControls.mouseScrollScale = controls["mouse_scroll_scale"].get(); - if (controls.contains("translation_scale")) - m_cameraControls.translationScale = controls["translation_scale"].get(); - if (controls.contains("rotation_scale")) - m_cameraControls.rotationScale = controls["rotation_scale"].get(); - } - - if (script.contains("segments")) - { - CCameraSequenceScript sequence; - std::string sequenceError; - if (!deserializeCameraSequenceScript(script, sequence, &sequenceError)) - { - scriptedInputParseFailed = true; - scriptedInputParseError = std::move(sequenceError); - } - else - { - pendingScriptedSequence = std::move(sequence); - } - } - - if (script.contains("events")) - for (const auto& ev : script["events"]) - { - if (!ev.contains("frame") || !ev.contains("type")) - { - m_logger->log("Scripted input event missing \"frame\" or \"type\".", ILogger::ELL_WARNING); - continue; - } - - const auto frame = ev["frame"].get(); - const auto type = ev["type"].get(); - const bool captureFrame = ev.value("capture", false); - - if (type == "keyboard") - { - if (!ev.contains("key") || !ev.contains("action")) - { - m_logger->log("Scripted keyboard event missing \"key\" or \"action\".", ILogger::ELL_WARNING); - continue; - } - - const auto keyStr = ev["key"].get(); - const auto actionStr = ev["action"].get(); - const auto key = ui::stringToKeyCode(keyStr); - if (key == ui::EKC_NONE) - { - m_logger->log("Scripted keyboard event has invalid key \"%s\".", ILogger::ELL_WARNING, keyStr.c_str()); - continue; - } - - ui::SKeyboardEvent::E_KEY_ACTION action = ui::SKeyboardEvent::ECA_UNITIALIZED; - if (actionStr == "pressed" || actionStr == "press") - action = ui::SKeyboardEvent::ECA_PRESSED; - else if (actionStr == "released" || actionStr == "release") - action = ui::SKeyboardEvent::ECA_RELEASED; - - if (action == ui::SKeyboardEvent::ECA_UNITIALIZED) - { - m_logger->log("Scripted keyboard event has invalid action \"%s\".", ILogger::ELL_WARNING, actionStr.c_str()); - continue; - } - - ScriptedInputEvent entry; - entry.frame = frame; - entry.type = ScriptedInputEvent::Type::Keyboard; - entry.keyboard.key = key; - entry.keyboard.action = action; - m_scriptedInput.events.emplace_back(entry); - if (captureFrame) - m_scriptedInput.captureFrames.emplace_back(frame); - } - else if (type == "mouse") - { - if (!ev.contains("kind")) - { - m_logger->log("Scripted mouse event missing \"kind\".", ILogger::ELL_WARNING); - continue; - } - - const auto kind = ev["kind"].get(); - ScriptedInputEvent entry; - entry.frame = frame; - entry.type = ScriptedInputEvent::Type::Mouse; - - if (kind == "move") - { - entry.mouse.type = ui::SMouseEvent::EET_MOVEMENT; - entry.mouse.dx = ev.value("dx", 0); - entry.mouse.dy = ev.value("dy", 0); - } - else if (kind == "scroll") - { - entry.mouse.type = ui::SMouseEvent::EET_SCROLL; - entry.mouse.v = ev.value("v", 0); - entry.mouse.h = ev.value("h", 0); - } - else if (kind == "click") - { - if (!ev.contains("button") || !ev.contains("action")) - { - m_logger->log("Scripted click event missing \"button\" or \"action\".", ILogger::ELL_WARNING); - continue; - } - - const auto buttonStr = ev["button"].get(); - const auto actionStr = ev["action"].get(); - - ui::E_MOUSE_BUTTON button = ui::EMB_LEFT_BUTTON; - if (buttonStr == "LEFT_BUTTON") button = ui::EMB_LEFT_BUTTON; - else if (buttonStr == "RIGHT_BUTTON") button = ui::EMB_RIGHT_BUTTON; - else if (buttonStr == "MIDDLE_BUTTON") button = ui::EMB_MIDDLE_BUTTON; - else if (buttonStr == "BUTTON_4") button = ui::EMB_BUTTON_4; - else if (buttonStr == "BUTTON_5") button = ui::EMB_BUTTON_5; - else - { - m_logger->log("Scripted click event has invalid button \"%s\".", ILogger::ELL_WARNING, buttonStr.c_str()); - continue; - } - - ui::SMouseEvent::SClickEvent::E_ACTION action = ui::SMouseEvent::SClickEvent::EA_UNITIALIZED; - if (actionStr == "pressed" || actionStr == "press") - action = ui::SMouseEvent::SClickEvent::EA_PRESSED; - else if (actionStr == "released" || actionStr == "release") - action = ui::SMouseEvent::SClickEvent::EA_RELEASED; - - if (action == ui::SMouseEvent::SClickEvent::EA_UNITIALIZED) - { - m_logger->log("Scripted click event has invalid action \"%s\".", ILogger::ELL_WARNING, actionStr.c_str()); - continue; - } - - entry.mouse.type = ui::SMouseEvent::EET_CLICK; - entry.mouse.button = button; - entry.mouse.action = action; - entry.mouse.x = ev.value("x", 0); - entry.mouse.y = ev.value("y", 0); - } - else - { - m_logger->log("Scripted mouse event has invalid kind \"%s\".", ILogger::ELL_WARNING, kind.c_str()); - continue; - } - - m_scriptedInput.events.emplace_back(entry); - if (captureFrame) - m_scriptedInput.captureFrames.emplace_back(frame); - } - else if (type == "imguizmo") - { - ScriptedInputEvent entry; - entry.frame = frame; - entry.type = ScriptedInputEvent::Type::Imguizmo; - - if (ev.contains("delta_trs")) - { - const auto arr = ev["delta_trs"].get>(); - float m16[16]; - for (size_t i = 0u; i < 16u; ++i) - m16[i] = arr[i]; - entry.imguizmo = *reinterpret_cast(m16); - } - else - { - const auto t = ev.contains("translation") ? ev["translation"].get>() : std::array{0.f, 0.f, 0.f}; - const auto r = ev.contains("rotation_deg") ? ev["rotation_deg"].get>() : std::array{0.f, 0.f, 0.f}; - const auto s = ev.contains("scale") ? ev["scale"].get>() : std::array{1.f, 1.f, 1.f}; - - float m16[16]; - float tr[3] = { t[0], t[1], t[2] }; - float rot[3] = { r[0], r[1], r[2] }; - float sc[3] = { s[0], s[1], s[2] }; - - ImGuizmo::RecomposeMatrixFromComponents(tr, rot, sc, m16); - entry.imguizmo = *reinterpret_cast(m16); - } - - m_scriptedInput.events.emplace_back(entry); - if (captureFrame) - m_scriptedInput.captureFrames.emplace_back(frame); - } - else if (type == "action") - { - if (!ev.contains("action")) - { - m_logger->log("Scripted action event missing \"action\".", ILogger::ELL_WARNING); - continue; - } - - const auto actionStr = ev["action"].get(); - ScriptedInputEvent entry; - entry.frame = frame; - entry.type = ScriptedInputEvent::Type::Action; - - auto getValueInt = [&]() -> int32_t - { - if (ev.contains("value")) - return ev["value"].get(); - if (ev.contains("index")) - return ev["index"].get(); - return 0; - }; - - if (actionStr == "set_active_render_window") - { - entry.action.kind = ScriptedInputEvent::ActionData::Kind::SetActiveRenderWindow; - entry.action.value = getValueInt(); - } - else if (actionStr == "set_active_planar") - { - entry.action.kind = ScriptedInputEvent::ActionData::Kind::SetActivePlanar; - entry.action.value = getValueInt(); - } - else if (actionStr == "set_projection_type") - { - entry.action.kind = ScriptedInputEvent::ActionData::Kind::SetProjectionType; - if (ev.contains("value") && ev["value"].is_string()) - { - const auto valueStr = ev["value"].get(); - if (valueStr == "perspective") - entry.action.value = static_cast(IPlanarProjection::CProjection::Perspective); - else if (valueStr == "orthographic") - entry.action.value = static_cast(IPlanarProjection::CProjection::Orthographic); - else - { - m_logger->log("Scripted action projection type has invalid value \"%s\".", ILogger::ELL_WARNING, valueStr.c_str()); - continue; - } - } - else - { - entry.action.value = getValueInt(); - } - } - else if (actionStr == "set_projection_index") - { - entry.action.kind = ScriptedInputEvent::ActionData::Kind::SetProjectionIndex; - entry.action.value = getValueInt(); - } - else if (actionStr == "set_use_window") - { - entry.action.kind = ScriptedInputEvent::ActionData::Kind::SetUseWindow; - entry.action.value = ev.value("value", false) ? 1 : 0; - } - else if (actionStr == "set_left_handed") - { - entry.action.kind = ScriptedInputEvent::ActionData::Kind::SetLeftHanded; - entry.action.value = ev.value("value", false) ? 1 : 0; - } - else if (actionStr == "reset_active_camera") - { - entry.action.kind = ScriptedInputEvent::ActionData::Kind::ResetActiveCamera; - entry.action.value = 1; - } - else - { - m_logger->log("Scripted action event has invalid action \"%s\".", ILogger::ELL_WARNING, actionStr.c_str()); - continue; - } - - m_scriptedInput.events.emplace_back(entry); - if (captureFrame) - m_scriptedInput.captureFrames.emplace_back(frame); - } - else - { - m_logger->log("Scripted input event has invalid type \"%s\".", ILogger::ELL_WARNING, type.c_str()); - } - } - - if (script.contains("checks")) - { - for (const auto& chk : script["checks"]) - { - if (!chk.contains("frame") || !chk.contains("kind")) - { - m_logger->log("Scripted check missing \"frame\" or \"kind\".", ILogger::ELL_WARNING); - continue; - } - - const auto frame = chk["frame"].get(); - const auto kind = chk["kind"].get(); - - ScriptedInputCheck entry; - entry.frame = frame; - - if (kind == "baseline") - { - entry.kind = ScriptedInputCheck::Kind::Baseline; - } - else if (kind == "imguizmo_virtual") - { - entry.kind = ScriptedInputCheck::Kind::ImguizmoVirtual; - entry.tolerance = chk.value("tolerance", entry.tolerance); - - if (!chk.contains("events")) - { - m_logger->log("Imguizmo virtual check missing \"events\".", ILogger::ELL_WARNING); - continue; - } - - for (const auto& ev : chk["events"]) - { - if (!ev.contains("type") || !ev.contains("magnitude")) - { - m_logger->log("Imguizmo virtual check event missing \"type\" or \"magnitude\".", ILogger::ELL_WARNING); - continue; - } - - const auto typeStr = ev["type"].get(); - const auto type = CVirtualGimbalEvent::stringToVirtualEvent(typeStr); - if (type == CVirtualGimbalEvent::None) - { - m_logger->log("Imguizmo virtual check event has invalid type \"%s\".", ILogger::ELL_WARNING, typeStr.c_str()); - continue; - } - - ScriptedInputCheck::ExpectedVirtualEvent expected; - expected.type = type; - expected.magnitude = ev["magnitude"].get(); - entry.expectedVirtualEvents.emplace_back(expected); - } - } - else if (kind == "gimbal_near") - { - entry.kind = ScriptedInputCheck::Kind::GimbalNear; - entry.posTolerance = chk.value("pos_tolerance", entry.posTolerance); - entry.eulerToleranceDeg = chk.value("euler_tolerance_deg", entry.eulerToleranceDeg); - - if (chk.contains("position")) - { - const auto pos = chk["position"].get>(); - entry.expectedPos = float32_t3(pos[0], pos[1], pos[2]); - entry.hasExpectedPos = true; - } - if (chk.contains("euler_deg")) - { - const auto euler = chk["euler_deg"].get>(); - entry.expectedEulerDeg = float32_t3(euler[0], euler[1], euler[2]); - entry.hasExpectedEuler = true; - } - } - else if (kind == "gimbal_delta") - { - entry.kind = ScriptedInputCheck::Kind::GimbalDelta; - entry.posTolerance = chk.value("pos_tolerance", entry.posTolerance); - entry.eulerToleranceDeg = chk.value("euler_tolerance_deg", entry.eulerToleranceDeg); - } - else if (kind == "gimbal_step") - { - entry.kind = ScriptedInputCheck::Kind::GimbalStep; - - if (chk.contains("min_pos_delta")) - { - entry.minPosDelta = chk["min_pos_delta"].get(); - entry.hasPosDeltaConstraint = true; - } - if (chk.contains("max_pos_delta")) - { - entry.posTolerance = chk["max_pos_delta"].get(); - entry.hasPosDeltaConstraint = true; - } - else if (chk.contains("pos_tolerance")) - { - entry.posTolerance = chk["pos_tolerance"].get(); - entry.hasPosDeltaConstraint = true; - } - - if (chk.contains("min_euler_delta_deg")) - { - entry.minEulerDeltaDeg = chk["min_euler_delta_deg"].get(); - entry.hasEulerDeltaConstraint = true; - } - if (chk.contains("max_euler_delta_deg")) - { - entry.eulerToleranceDeg = chk["max_euler_delta_deg"].get(); - entry.hasEulerDeltaConstraint = true; - } - else if (chk.contains("euler_tolerance_deg")) - { - entry.eulerToleranceDeg = chk["euler_tolerance_deg"].get(); - entry.hasEulerDeltaConstraint = true; - } - - if (!entry.hasPosDeltaConstraint && !entry.hasEulerDeltaConstraint) - { - m_logger->log("gimbal_step check requires at least one delta constraint.", ILogger::ELL_WARNING); - continue; - } - } - else - { - m_logger->log("Scripted check has invalid kind \"%s\".", ILogger::ELL_WARNING, kind.c_str()); - continue; - } - - m_scriptedInput.checks.emplace_back(entry); - } - } - - std::sort(m_scriptedInput.events.begin(), m_scriptedInput.events.end(), - [](const ScriptedInputEvent& a, const ScriptedInputEvent& b) { return a.frame < b.frame; }); - std::sort(m_scriptedInput.checks.begin(), m_scriptedInput.checks.end(), - [](const ScriptedInputCheck& a, const ScriptedInputCheck& b) { return a.frame < b.frame; }); - if (!m_scriptedInput.captureFrames.empty()) - { - std::sort(m_scriptedInput.captureFrames.begin(), m_scriptedInput.captureFrames.end()); - m_scriptedInput.captureFrames.erase(std::unique(m_scriptedInput.captureFrames.begin(), m_scriptedInput.captureFrames.end()), m_scriptedInput.captureFrames.end()); - } - if (m_disableScreenshotsCli) - { - m_scriptedInput.captureFrames.clear(); - m_scriptedInput.nextCaptureIndex = 0; - } + m_scriptedInput.exclusive = parsed.exclusive; + m_scriptedInput.capturePrefix = parsed.capturePrefix.empty() ? "script" : 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); + finalizeScriptedInput(); }; if (program.is_used("--script")) @@ -2439,85 +2298,8 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) { auto expandCameraSequenceScript = [&](const CCameraSequenceScript& sequence) -> bool { - m_scriptedInput.events.clear(); - m_scriptedInput.checks.clear(); - m_scriptedInput.captureFrames.clear(); - m_scriptedInput.nextEventIndex = 0; - m_scriptedInput.nextCheckIndex = 0; - m_scriptedInput.nextCaptureIndex = 0; - m_scriptedInput.failed = false; - m_scriptedInput.summaryReported = false; - m_scriptedInput.baselineValid = false; - m_scriptedInput.stepValid = false; - - auto pushAction = [&](const uint64_t frame, const ScriptedInputEvent::ActionData::Kind kind, const int32_t value) -> void - { - ScriptedInputEvent entry; - entry.frame = frame; - entry.type = ScriptedInputEvent::Type::Action; - entry.action.kind = kind; - entry.action.value = value; - m_scriptedInput.events.emplace_back(std::move(entry)); - }; - - auto pushGoal = [&](const uint64_t frame, const CCameraGoal& goal) -> void - { - ScriptedInputEvent entry; - entry.frame = frame; - entry.type = ScriptedInputEvent::Type::Goal; - entry.goal.goal = goal; - entry.goal.requireExact = true; - m_scriptedInput.events.emplace_back(std::move(entry)); - }; - - auto pushTrackedTargetTransform = [&](const uint64_t frame, const CCameraSequenceTrackedTargetPose& pose) -> void - { - ScriptedInputEvent entry; - entry.frame = frame; - entry.type = ScriptedInputEvent::Type::TrackedTargetTransform; - ICamera::CGimbal gimbal({ .position = pose.position, .orientation = pose.orientation }); - entry.trackedTargetTransform.transform = gimbal.operator()(); - m_scriptedInput.events.emplace_back(std::move(entry)); - }; - - auto pushSegmentLabel = [&](const uint64_t frame, std::string label) -> void - { - ScriptedInputEvent entry; - entry.frame = frame; - entry.type = ScriptedInputEvent::Type::SegmentLabel; - entry.segmentLabel.label = std::move(label); - m_scriptedInput.events.emplace_back(std::move(entry)); - }; - - auto pushStepCheck = [&](const uint64_t frame, const CCameraSequenceContinuitySettings& continuity) -> void - { - ScriptedInputCheck entry; - entry.frame = frame; - entry.kind = ScriptedInputCheck::Kind::GimbalStep; - if (continuity.hasPosDeltaConstraint) - { - entry.hasPosDeltaConstraint = true; - entry.minPosDelta = continuity.minPosDelta; - entry.posTolerance = continuity.maxPosDelta; - } - if (continuity.hasEulerDeltaConstraint) - { - entry.hasEulerDeltaConstraint = true; - entry.minEulerDeltaDeg = continuity.minEulerDeltaDeg; - entry.eulerToleranceDeg = continuity.maxEulerDeltaDeg; - } - m_scriptedInput.checks.emplace_back(std::move(entry)); - }; - - auto pushFollowTargetLockCheck = [&](const uint64_t frame, const float toleranceDeg = 1.0f, const float screenToleranceNdc = 0.03f) -> void - { - ScriptedInputCheck entry; - entry.frame = frame; - entry.kind = ScriptedInputCheck::Kind::FollowTargetLock; - entry.eulerToleranceDeg = toleranceDeg; - entry.posTolerance = screenToleranceNdc; - m_scriptedInput.checks.emplace_back(std::move(entry)); - }; + CCameraScriptedTimeline timeline; + resetScriptedInputRuntimeState(); auto resolvePlanarIx = [&](const CCameraSequenceSegment& segment) -> std::optional { @@ -2541,7 +2323,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) }; const bool useWindow = nbl::hlsl::sequenceScriptUsesMultiplePresentations(sequence); - pushAction(0u, ScriptedInputEvent::ActionData::Kind::SetUseWindow, useWindow ? 1 : 0); + nbl::hlsl::appendScriptedActionEvent(timeline, 0u, CCameraScriptedInputEvent::ActionData::Kind::SetUseWindow, useWindow ? 1 : 0); const CCameraSequenceTrackedTargetPose referenceTrackedTargetPose = { .position = getDefaultFollowTargetPosition(), @@ -2558,8 +2340,6 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) segment.name.c_str(), kindLabel.c_str(), segment.cameraIdentifier.c_str()); return false; } - - pushSegmentLabel(frameCursor, segment.name); const bool useTrackedTargetFollow = nbl::hlsl::sequenceSegmentUsesTrackedTargetTrack(segment) && planarIx.value() < m_planarFollowConfigs.size() && m_planarFollowConfigs[planarIx.value()].enabled && @@ -2585,71 +2365,29 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) ILogger::ELL_WARNING, segment.name.c_str(), compiledSegment.presentations.size(), windowBindings.size()); } - pushAction(frameCursor, ScriptedInputEvent::ActionData::Kind::SetActiveRenderWindow, 0); - pushAction(frameCursor, ScriptedInputEvent::ActionData::Kind::SetActivePlanar, static_cast(planarIx.value())); - if (!compiledSegment.presentations.empty()) - { - pushAction(frameCursor, ScriptedInputEvent::ActionData::Kind::SetProjectionType, static_cast(compiledSegment.presentations[0].projection)); - pushAction(frameCursor, ScriptedInputEvent::ActionData::Kind::SetLeftHanded, compiledSegment.presentations[0].leftHanded ? 1 : 0); - } - if (compiledSegment.resetCamera) - pushAction(frameCursor, ScriptedInputEvent::ActionData::Kind::ResetActiveCamera, 1); - - for (size_t windowIx = 1u; windowIx < std::min(compiledSegment.presentations.size(), windowBindings.size()); ++windowIx) - { - pushAction(frameCursor, ScriptedInputEvent::ActionData::Kind::SetActiveRenderWindow, static_cast(windowIx)); - pushAction(frameCursor, ScriptedInputEvent::ActionData::Kind::SetActivePlanar, static_cast(planarIx.value())); - pushAction(frameCursor, ScriptedInputEvent::ActionData::Kind::SetProjectionType, static_cast(compiledSegment.presentations[windowIx].projection)); - pushAction(frameCursor, ScriptedInputEvent::ActionData::Kind::SetLeftHanded, compiledSegment.presentations[windowIx].leftHanded ? 1 : 0); - } - pushAction(frameCursor, ScriptedInputEvent::ActionData::Kind::SetActiveRenderWindow, 0); - - std::vector framePolicies; - if (!nbl::hlsl::buildCompiledSegmentFramePolicies(compiledSegment, framePolicies, useTrackedTargetFollow)) + std::string buildError; + if (!nbl::hlsl::appendCompiledSequenceSegmentToScriptedTimeline( + timeline, + frameCursor, + compiledSegment, + { + .planarIx = planarIx.value(), + .availableWindowCount = windowBindings.size(), + .useWindow = useWindow, + .includeFollowTargetLock = useTrackedTargetFollow + }, + &buildError)) { - logFail("Sequence segment \"%s\" failed to build compiled frame policies.", segment.name.c_str()); + logFail("Sequence segment \"%s\" failed to build scripted runtime data: %s", + segment.name.c_str(), buildError.c_str()); return false; } - for (const auto& policy : framePolicies) - { - CCameraPreset preset; - if (!tryBuildKeyframeTrackPresetAtTime(compiledSegment.track, policy.sampleTime, preset)) - { - logFail("Sequence segment \"%s\" failed to sample track at t=%f.", segment.name.c_str(), policy.sampleTime); - return false; - } - pushGoal(frameCursor + policy.frameOffset, makeGoalFromPreset(preset)); - if (compiledSegment.usesTrackedTargetTrack()) - { - CCameraSequenceTrackedTargetPose trackedTargetPose; - if (!nbl::hlsl::tryBuildSequenceTrackedTargetPoseAtTime(compiledSegment.trackedTargetTrack, policy.sampleTime, trackedTargetPose)) - { - logFail("Sequence segment \"%s\" failed to sample tracked-target track at t=%f.", segment.name.c_str(), policy.sampleTime); - return false; - } - pushTrackedTargetTransform(frameCursor + policy.frameOffset, trackedTargetPose); - } - - if (policy.baseline) - { - ScriptedInputCheck baseline; - baseline.frame = frameCursor + policy.frameOffset; - baseline.kind = ScriptedInputCheck::Kind::Baseline; - m_scriptedInput.checks.emplace_back(std::move(baseline)); - } - if (policy.continuityStep) - pushStepCheck(frameCursor + policy.frameOffset, compiledSegment.continuity); - if (policy.followTargetLock) - pushFollowTargetLockCheck(frameCursor + policy.frameOffset); - if (policy.capture) - m_scriptedInput.captureFrames.emplace_back(frameCursor + policy.frameOffset); - } - frameCursor += compiledSegment.durationFrames; } - finalizeScriptedInput(); + nbl::hlsl::finalizeScriptedTimeline(timeline, m_disableScreenshotsCli); + m_scriptedInput.timeline = std::move(timeline); return true; }; diff --git a/61_UI/AppUpdate.cpp b/61_UI/AppUpdate.cpp index 7f102d0d2..37bf79cfc 100644 --- a/61_UI/AppUpdate.cpp +++ b/61_UI/AppUpdate.cpp @@ -58,89 +58,60 @@ void App::update() std::vector scriptedMouse; std::vector scriptedKeyboard; - std::vector scriptedImguizmo; - std::vector scriptedActions; - std::vector scriptedGoals; - std::vector scriptedTrackedTargetTransforms; - std::vector scriptedSegmentLabels; + CCameraScriptedFrameEvents scriptedFrameEvents; const CVirtualGimbalEvent* scriptedImguizmoVirtual = nullptr; uint32_t scriptedImguizmoVirtualCount = 0u; - if (m_scriptedInput.enabled && m_scriptedInput.nextEventIndex < m_scriptedInput.events.size()) + if (m_scriptedInput.enabled && m_scriptedInput.nextEventIndex < m_scriptedInput.timeline.events.size()) + nbl::hlsl::dequeueScriptedFrameEvents( + m_scriptedInput.timeline.events, + m_scriptedInput.nextEventIndex, + m_realFrameIx, + scriptedFrameEvents); + + for (const auto& authoredKeyboard : scriptedFrameEvents.keyboard) + { + SKeyboardEvent e(m_nextPresentationTimestamp); + e.keyCode = authoredKeyboard.key; + e.action = authoredKeyboard.action; + e.window = m_window.get(); + scriptedKeyboard.emplace_back(e); + } + for (const auto& authoredMouse : scriptedFrameEvents.mouse) { - const auto frame = m_realFrameIx; - while (m_scriptedInput.nextEventIndex < m_scriptedInput.events.size() && - m_scriptedInput.events[m_scriptedInput.nextEventIndex].frame == frame) + SMouseEvent e(m_nextPresentationTimestamp); + e.window = m_window.get(); + e.type = authoredMouse.type; + if (authoredMouse.type == ui::SMouseEvent::EET_CLICK) { - const auto& ev = m_scriptedInput.events[m_scriptedInput.nextEventIndex]; - - if (ev.type == ScriptedInputEvent::Type::Keyboard) - { - SKeyboardEvent e(m_nextPresentationTimestamp); - e.keyCode = ev.keyboard.key; - e.action = ev.keyboard.action; - e.window = m_window.get(); - scriptedKeyboard.emplace_back(e); - } - else if (ev.type == ScriptedInputEvent::Type::Mouse) - { - SMouseEvent e(m_nextPresentationTimestamp); - e.window = m_window.get(); - e.type = ev.mouse.type; - if (ev.mouse.type == ui::SMouseEvent::EET_CLICK) - { - e.clickEvent.mouseButton = ev.mouse.button; - e.clickEvent.action = ev.mouse.action; - e.clickEvent.clickPosX = ev.mouse.x; - e.clickEvent.clickPosY = ev.mouse.y; - } - else if (ev.mouse.type == ui::SMouseEvent::EET_SCROLL) - { - e.scrollEvent.verticalScroll = ev.mouse.v; - e.scrollEvent.horizontalScroll = ev.mouse.h; - } - else if (ev.mouse.type == ui::SMouseEvent::EET_MOVEMENT) - { - e.movementEvent.relativeMovementX = ev.mouse.dx; - e.movementEvent.relativeMovementY = ev.mouse.dy; - } - scriptedMouse.emplace_back(e); - } - else if (ev.type == ScriptedInputEvent::Type::Imguizmo) - { - scriptedImguizmo.emplace_back(ev.imguizmo); - } - else if (ev.type == ScriptedInputEvent::Type::Action) - { - scriptedActions.emplace_back(ev.action); - } - else if (ev.type == ScriptedInputEvent::Type::Goal) - { - scriptedGoals.emplace_back(ev.goal); - } - else if (ev.type == ScriptedInputEvent::Type::TrackedTargetTransform) - { - scriptedTrackedTargetTransforms.emplace_back(ev.trackedTargetTransform.transform); - } - else if (ev.type == ScriptedInputEvent::Type::SegmentLabel) - { - scriptedSegmentLabels.emplace_back(ev.segmentLabel.label); - } - - ++m_scriptedInput.nextEventIndex; + e.clickEvent.mouseButton = authoredMouse.button; + e.clickEvent.action = authoredMouse.action; + e.clickEvent.clickPosX = authoredMouse.x; + e.clickEvent.clickPosY = authoredMouse.y; + } + else if (authoredMouse.type == ui::SMouseEvent::EET_SCROLL) + { + e.scrollEvent.verticalScroll = authoredMouse.v; + e.scrollEvent.horizontalScroll = authoredMouse.h; } + else if (authoredMouse.type == ui::SMouseEvent::EET_MOVEMENT) + { + e.movementEvent.relativeMovementX = authoredMouse.dx; + e.movementEvent.relativeMovementY = authoredMouse.dy; + } + scriptedMouse.emplace_back(e); } - if (!scriptedSegmentLabels.empty()) - m_scriptedInput.visualSegmentLabel = scriptedSegmentLabels.back(); + if (!scriptedFrameEvents.segmentLabels.empty()) + m_scriptedInput.visualSegmentLabel = scriptedFrameEvents.segmentLabels.back(); - if (m_scriptedInput.enabled && scriptedActions.size()) + if (m_scriptedInput.enabled && scriptedFrameEvents.actions.size()) { - auto applyAction = [&](const ScriptedInputEvent::ActionData& action) -> void + auto applyAction = [&](const CCameraScriptedInputEvent::ActionData& action) -> void { switch (action.kind) { - case ScriptedInputEvent::ActionData::Kind::SetActiveRenderWindow: + case CCameraScriptedInputEvent::ActionData::Kind::SetActiveRenderWindow: { if (action.value < 0 || static_cast(action.value) >= windowBindings.size()) { @@ -150,7 +121,7 @@ void App::update() activeRenderWindowIx = static_cast(action.value); } break; - case ScriptedInputEvent::ActionData::Kind::SetActivePlanar: + case CCameraScriptedInputEvent::ActionData::Kind::SetActivePlanar: { if (action.value < 0 || static_cast(action.value) >= m_planarProjections.size()) { @@ -165,7 +136,7 @@ void App::update() m_scriptedInput.visualActivePlanarStartFrame = m_realFrameIx; } break; - case ScriptedInputEvent::ActionData::Kind::SetProjectionType: + case CCameraScriptedInputEvent::ActionData::Kind::SetProjectionType: { auto& binding = windowBindings[activeRenderWindowIx]; if (!binding.lastBoundPerspectivePresetProjectionIx.has_value() || !binding.lastBoundOrthoPresetProjectionIx.has_value()) @@ -186,7 +157,7 @@ void App::update() } } break; - case ScriptedInputEvent::ActionData::Kind::SetProjectionIndex: + case CCameraScriptedInputEvent::ActionData::Kind::SetProjectionIndex: { auto& binding = windowBindings[activeRenderWindowIx]; auto& projections = m_planarProjections[binding.activePlanarIx]->getPlanarProjections(); @@ -204,18 +175,18 @@ void App::update() binding.lastBoundOrthoPresetProjectionIx = ix; } break; - case ScriptedInputEvent::ActionData::Kind::SetUseWindow: + case CCameraScriptedInputEvent::ActionData::Kind::SetUseWindow: { useWindow = action.value != 0; } break; - case ScriptedInputEvent::ActionData::Kind::SetLeftHanded: + case CCameraScriptedInputEvent::ActionData::Kind::SetLeftHanded: { auto& binding = windowBindings[activeRenderWindowIx]; binding.leftHandedProjection = action.value != 0; } break; - case ScriptedInputEvent::ActionData::Kind::ResetActiveCamera: + case CCameraScriptedInputEvent::ActionData::Kind::ResetActiveCamera: { auto& binding = windowBindings[activeRenderWindowIx]; if (binding.activePlanarIx >= m_planarProjections.size()) @@ -236,16 +207,16 @@ void App::update() } }; - for (const auto& action : scriptedActions) - if (action.kind == ScriptedInputEvent::ActionData::Kind::SetActiveRenderWindow) + for (const auto& action : scriptedFrameEvents.actions) + if (action.kind == CCameraScriptedInputEvent::ActionData::Kind::SetActiveRenderWindow) applyAction(action); - for (const auto& action : scriptedActions) - if (action.kind != ScriptedInputEvent::ActionData::Kind::SetActiveRenderWindow) + for (const auto& action : scriptedFrameEvents.actions) + if (action.kind != CCameraScriptedInputEvent::ActionData::Kind::SetActiveRenderWindow) applyAction(action); if (m_scriptedInput.log) - m_logger->log("[script] frame %llu actions=%zu", ILogger::ELL_INFO, static_cast(m_realFrameIx), scriptedActions.size()); + m_logger->log("[script] frame %llu actions=%zu", ILogger::ELL_INFO, static_cast(m_realFrameIx), scriptedFrameEvents.actions.size()); } if (m_scriptedInput.enabled && m_scriptedInput.visualDebug && !m_scriptedInput.visualActivePlanarValid) @@ -319,15 +290,15 @@ void App::update() .keyboardEvents = { capturedEvents.keyboard.data(), capturedEvents.keyboard.size() } }; - if (m_scriptedInput.log && (scriptedKeyboard.size() || scriptedMouse.size() || scriptedImguizmo.size() || scriptedGoals.size() || scriptedTrackedTargetTransforms.size())) + if (m_scriptedInput.log && (scriptedKeyboard.size() || scriptedMouse.size() || scriptedFrameEvents.imguizmo.size() || scriptedFrameEvents.goals.size() || scriptedFrameEvents.trackedTargetTransforms.size())) { m_logger->log("[script] frame %llu input kb=%zu mouse=%zu imguizmo=%zu goals=%zu target=%zu", ILogger::ELL_INFO, static_cast(m_realFrameIx), scriptedKeyboard.size(), scriptedMouse.size(), - scriptedImguizmo.size(), - scriptedGoals.size(), - scriptedTrackedTargetTransforms.size()); + scriptedFrameEvents.imguizmo.size(), + scriptedFrameEvents.goals.size(), + scriptedFrameEvents.trackedTargetTransforms.size()); } if (enableActiveCameraMovement && !skipCameraInput) @@ -438,7 +409,7 @@ void App::update() } } - if (m_scriptedInput.enabled && scriptedImguizmo.size() && !skipCameraInput) + if (m_scriptedInput.enabled && scriptedFrameEvents.imguizmo.size() && !skipCameraInput) { auto& binding = windowBindings[activeRenderWindowIx]; auto& planar = m_planarProjections[binding.activePlanarIx]; @@ -447,7 +418,7 @@ void App::update() CGimbalInputBinder imguizmoBinding; camera->copyDefaultInputBindingPresetTo(imguizmoBinding); auto collectedEvents = imguizmoBinding.collectVirtualEvents(m_nextPresentationTimestamp, { - .imguizmoEvents = { scriptedImguizmo.data(), scriptedImguizmo.size() } + .imguizmoEvents = { scriptedFrameEvents.imguizmo.data(), scriptedFrameEvents.imguizmo.size() } }); auto& imguizmoEvents = collectedEvents.events; const uint32_t vCount = collectedEvents.imguizmoCount; @@ -477,7 +448,7 @@ void App::update() scriptedImguizmoVirtualCount = vCount; } - if (m_scriptedInput.enabled && scriptedGoals.size() && !skipCameraInput) + if (m_scriptedInput.enabled && scriptedFrameEvents.goals.size() && !skipCameraInput) { auto& binding = windowBindings[activeRenderWindowIx]; auto& planar = m_planarProjections[binding.activePlanarIx]; @@ -489,7 +460,7 @@ void App::update() m_logger->log(fmt, ILogger::ELL_ERROR, std::forward(args)...); }; - for (const auto& goalEvent : scriptedGoals) + for (const auto& goalEvent : scriptedFrameEvents.goals) { const auto result = m_cameraGoalSolver.applyDetailed(camera, goalEvent.goal); if (!result.succeeded() || (goalEvent.requireExact && !result.exact)) @@ -519,7 +490,7 @@ void App::update() } } - auto tryComputeProjectedFollowMetricsForPlanar = [&](const uint32_t planarIx, float& ndcX, float& ndcY, float& ndcRadius) -> bool + auto tryBuildFollowViewProjForPlanar = [&](const uint32_t planarIx, float32_t4x4& outViewProjMatrix) -> bool { if (activeRenderWindowIx >= windowBindings.size()) return false; @@ -542,73 +513,28 @@ void App::update() const auto viewMatrix = getMatrix3x4As4x4(getCastedMatrix(camera->getGimbal().getViewMatrix())); const auto projectionMatrix = getCastedMatrix(projections[projectionIx].getProjectionMatrix()); - const auto viewProjMatrix = mul(projectionMatrix, viewMatrix); - return nbl::hlsl::tryComputeProjectedFollowTargetMetrics(viewProjMatrix, m_followTarget, ndcX, ndcY, &ndcRadius); + outViewProjMatrix = mul(projectionMatrix, viewMatrix); + return true; }; - auto resetScriptedVisualFollowState = [&]() -> void + auto setScriptedVisualFollowState = [&](const SCameraFollowVisualMetrics& metrics) -> void { - m_scriptedInput.visualFollowActive = false; - m_scriptedInput.visualFollowMode = ECameraFollowMode::Disabled; - m_scriptedInput.visualFollowLockValid = false; - m_scriptedInput.visualFollowLockAngleDeg = 0.0f; - m_scriptedInput.visualFollowTargetDistance = 0.0f; - m_scriptedInput.visualFollowProjectedValid = false; - m_scriptedInput.visualFollowTargetCenterNdcX = 0.0f; - m_scriptedInput.visualFollowTargetCenterNdcY = 0.0f; - m_scriptedInput.visualFollowTargetCenterNdcRadius = 0.0f; + m_scriptedInput.visualFollowActive = metrics.active; + m_scriptedInput.visualFollowMode = metrics.mode; + m_scriptedInput.visualFollowLockValid = metrics.lockValid; + m_scriptedInput.visualFollowLockAngleDeg = metrics.lockAngleDeg; + m_scriptedInput.visualFollowTargetDistance = metrics.targetDistance; + m_scriptedInput.visualFollowProjectedValid = metrics.projectedValid; + m_scriptedInput.visualFollowTargetCenterNdcX = metrics.projectedNdcX; + m_scriptedInput.visualFollowTargetCenterNdcY = metrics.projectedNdcY; + m_scriptedInput.visualFollowTargetCenterNdcRadius = metrics.projectedNdcRadius; }; - auto updateScriptedVisualFollowState = [&](const uint32_t planarIx, ICamera* activeCamera, const SCameraFollowConfig* config) -> void + if (!scriptedFrameEvents.trackedTargetTransforms.empty()) { - if (!activeCamera || !config || !config->enabled || config->mode == ECameraFollowMode::Disabled) - { - resetScriptedVisualFollowState(); - return; - } - - m_scriptedInput.visualFollowActive = true; - m_scriptedInput.visualFollowMode = config->mode; - - float lockAngleDeg = 0.0f; - double targetDistance = 0.0; - m_scriptedInput.visualFollowLockValid = nbl::hlsl::cameraFollowModeLocksViewToTarget(config->mode) && - nbl::hlsl::tryComputeFollowTargetLockMetrics(activeCamera->getGimbal(), m_followTarget, lockAngleDeg, &targetDistance); - if (m_scriptedInput.visualFollowLockValid) - { - m_scriptedInput.visualFollowLockAngleDeg = lockAngleDeg; - m_scriptedInput.visualFollowTargetDistance = static_cast(targetDistance); - } - else - { - m_scriptedInput.visualFollowLockAngleDeg = 0.0f; - m_scriptedInput.visualFollowTargetDistance = 0.0f; - } - - float ndcX = 0.0f; - float ndcY = 0.0f; - float ndcRadius = 0.0f; - m_scriptedInput.visualFollowProjectedValid = - m_scriptedInput.visualFollowLockValid && - tryComputeProjectedFollowMetricsForPlanar(planarIx, ndcX, ndcY, ndcRadius); - if (m_scriptedInput.visualFollowProjectedValid) - { - m_scriptedInput.visualFollowTargetCenterNdcX = ndcX; - m_scriptedInput.visualFollowTargetCenterNdcY = ndcY; - m_scriptedInput.visualFollowTargetCenterNdcRadius = ndcRadius; - } - else - { - m_scriptedInput.visualFollowTargetCenterNdcX = 0.0f; - m_scriptedInput.visualFollowTargetCenterNdcY = 0.0f; - m_scriptedInput.visualFollowTargetCenterNdcRadius = 0.0f; - } - }; - - if (!scriptedTrackedTargetTransforms.empty()) - { - setFollowTargetTransform(scriptedTrackedTargetTransforms.back()); + setFollowTargetTransform(scriptedFrameEvents.trackedTargetTransforms.back().transform); applyFollowToConfiguredCameras(true); + SCameraFollowVisualMetrics followMetrics = {}; if (activeRenderWindowIx < windowBindings.size()) { const auto planarIx = windowBindings[activeRenderWindowIx].activePlanarIx; @@ -616,21 +542,24 @@ void App::update() { auto* activeCamera = planarIx < m_planarProjections.size() && m_planarProjections[planarIx] ? m_planarProjections[planarIx]->getCamera() : nullptr; - updateScriptedVisualFollowState(planarIx, activeCamera, &m_planarFollowConfigs[planarIx]); + float32_t4x4 viewProjMatrix = float32_t4x4(1.0f); + const float32_t4x4* viewProjMatrixPtr = tryBuildFollowViewProjForPlanar(planarIx, viewProjMatrix) ? &viewProjMatrix : nullptr; + followMetrics = nbl::hlsl::buildFollowVisualMetrics( + activeCamera, + m_followTarget, + &m_planarFollowConfigs[planarIx], + viewProjMatrixPtr); } - else - resetScriptedVisualFollowState(); } - else - resetScriptedVisualFollowState(); + setScriptedVisualFollowState(followMetrics); } else { applyFollowToConfiguredCameras(); - resetScriptedVisualFollowState(); + setScriptedVisualFollowState({}); } - if (m_scriptedInput.enabled && m_scriptedInput.nextCheckIndex < m_scriptedInput.checks.size()) + if (m_scriptedInput.enabled && m_scriptedInput.checkRuntime.nextCheckIndex < m_scriptedInput.timeline.checks.size()) { auto* camera = [&]() -> ICamera* { @@ -654,296 +583,62 @@ void App::update() return; m_logger->log(fmt, ILogger::ELL_INFO, std::forward(args)...); }; - - auto angleDiffDeg = [](float a, float b) -> float - { - float d = std::fmod(a - b + 180.0f, 360.0f); - if (d < 0.0f) - d += 360.0f; - return std::abs(d - 180.0f); - }; - - auto isFinite3 = [](const float32_t3& v) -> bool - { - return std::isfinite(v.x) && std::isfinite(v.y) && std::isfinite(v.z); - }; - auto setStepReference = [&](const float32_t3& newPos, const float32_t3& newEuler) -> void - { - m_scriptedInput.stepValid = true; - m_scriptedInput.stepPos = newPos; - m_scriptedInput.stepEulerDeg = newEuler; - }; - - const auto frame = m_realFrameIx; - while (m_scriptedInput.nextCheckIndex < m_scriptedInput.checks.size() && - m_scriptedInput.checks[m_scriptedInput.nextCheckIndex].frame == frame) + SCameraFollowConfig activeFollowConfig = {}; + bool hasActiveFollowConfig = false; + bool hasFollowViewProjMatrix = false; + float32_t4x4 followViewProjMatrix = float32_t4x4(1.0f); + if (activeRenderWindowIx < windowBindings.size()) { - const auto& check = m_scriptedInput.checks[m_scriptedInput.nextCheckIndex]; - - if (!camera) - { - logFail("[script][fail] check frame=%llu no active camera", static_cast(frame)); - ++m_scriptedInput.nextCheckIndex; - continue; - } - - const auto& gimbal = camera->getGimbal(); - const auto pos = gimbal.getPosition(); - const auto euler = glm::degrees(glm::eulerAngles(gimbal.getOrientation())); - - if (!isFinite3(pos) || !isFinite3(euler)) - { - logFail("[script][fail] check frame=%llu non-finite gimbal state", static_cast(frame)); - ++m_scriptedInput.nextCheckIndex; - continue; - } - - if (check.kind == ScriptedInputCheck::Kind::Baseline) + const auto& binding = windowBindings[activeRenderWindowIx]; + const auto planarIx = binding.activePlanarIx; + if (planarIx < m_planarFollowConfigs.size()) { - m_scriptedInput.baselineValid = true; - m_scriptedInput.baselinePos = pos; - m_scriptedInput.baselineEulerDeg = euler; - setStepReference(pos, euler); - logPass("[script][pass] baseline frame=%llu pos=(%.3f, %.3f, %.3f) euler_deg=(%.3f, %.3f, %.3f)", - static_cast(frame), - pos.x, pos.y, pos.z, euler.x, euler.y, euler.z); + activeFollowConfig = m_planarFollowConfigs[planarIx]; + hasActiveFollowConfig = true; } - else if (check.kind == ScriptedInputCheck::Kind::ImguizmoVirtual) + if (camera && planarIx < m_planarProjections.size() && m_planarProjections[planarIx] && binding.boundProjectionIx.has_value()) { - bool ok = true; - if (!scriptedImguizmoVirtual || scriptedImguizmoVirtualCount == 0u) + auto& planar = m_planarProjections[planarIx]; + const auto projectionIx = binding.boundProjectionIx.value(); + auto& projections = planar->getPlanarProjections(); + if (projectionIx < projections.size()) { - ok = false; - } - else - { - for (const auto& expected : check.expectedVirtualEvents) - { - bool found = false; - double actual = 0.0; - for (uint32_t i = 0u; i < scriptedImguizmoVirtualCount; ++i) - { - if (scriptedImguizmoVirtual[i].type == expected.type) - { - found = true; - actual = scriptedImguizmoVirtual[i].magnitude; - break; - } - } - if (!found || std::abs(actual - expected.magnitude) > check.tolerance) - { - ok = false; - logFail("[script][fail] imguizmo_virtual frame=%llu type=%s expected=%.6f actual=%.6f tol=%.6f", - static_cast(frame), - CVirtualGimbalEvent::virtualEventToString(expected.type).data(), - expected.magnitude, - actual, - check.tolerance); - } - } + const auto viewMatrix = getMatrix3x4As4x4(getCastedMatrix(camera->getGimbal().getViewMatrix())); + const auto projectionMatrix = getCastedMatrix(projections[projectionIx].getProjectionMatrix()); + followViewProjMatrix = mul(projectionMatrix, viewMatrix); + hasFollowViewProjMatrix = true; } - - if (ok) - logPass("[script][pass] imguizmo_virtual frame=%llu events=%zu", static_cast(frame), check.expectedVirtualEvents.size()); } - else if (check.kind == ScriptedInputCheck::Kind::GimbalNear) - { - bool ok = true; - if (check.hasExpectedPos) - { - const auto diff = float32_t3(pos.x - check.expectedPos.x, pos.y - check.expectedPos.y, pos.z - check.expectedPos.z); - const auto d = std::sqrt(diff.x * diff.x + diff.y * diff.y + diff.z * diff.z); - if (d > check.posTolerance) - { - ok = false; - logFail("[script][fail] gimbal_near frame=%llu pos_diff=%.6f tol=%.6f", - static_cast(frame), d, check.posTolerance); - } - } - if (check.hasExpectedEuler) - { - const auto dx = angleDiffDeg(euler.x, check.expectedEulerDeg.x); - const auto dy = angleDiffDeg(euler.y, check.expectedEulerDeg.y); - const auto dz = angleDiffDeg(euler.z, check.expectedEulerDeg.z); - const auto dmax = std::max(dx, std::max(dy, dz)); - if (dmax > check.eulerToleranceDeg) - { - ok = false; - logFail("[script][fail] gimbal_near frame=%llu euler_diff=%.6f tol=%.6f", - static_cast(frame), dmax, check.eulerToleranceDeg); - } - } + } - if (ok) - logPass("[script][pass] gimbal_near frame=%llu", static_cast(frame)); - } - else if (check.kind == ScriptedInputCheck::Kind::GimbalDelta) + const auto checkResult = nbl::hlsl::evaluateScriptedChecksForFrame( + m_scriptedInput.timeline.checks, + m_scriptedInput.checkRuntime, { - if (!m_scriptedInput.baselineValid) - { - logFail("[script][fail] gimbal_delta frame=%llu missing baseline", static_cast(frame)); - } - else - { - const auto diff = float32_t3(pos.x - m_scriptedInput.baselinePos.x, pos.y - m_scriptedInput.baselinePos.y, pos.z - m_scriptedInput.baselinePos.z); - const auto dpos = std::sqrt(diff.x * diff.x + diff.y * diff.y + diff.z * diff.z); - const auto dx = angleDiffDeg(euler.x, m_scriptedInput.baselineEulerDeg.x); - const auto dy = angleDiffDeg(euler.y, m_scriptedInput.baselineEulerDeg.y); - const auto dz = angleDiffDeg(euler.z, m_scriptedInput.baselineEulerDeg.z); - const auto dmax = std::max(dx, std::max(dy, dz)); - - if (dpos > check.posTolerance || dmax > check.eulerToleranceDeg) - { - logFail("[script][fail] gimbal_delta frame=%llu pos_diff=%.6f tol=%.6f euler_diff=%.6f tol=%.6f", - static_cast(frame), - dpos, check.posTolerance, - dmax, check.eulerToleranceDeg); - } - else - { - logPass("[script][pass] gimbal_delta frame=%llu pos_diff=%.6f euler_diff=%.6f", - static_cast(frame), dpos, dmax); - } - } - } - else if (check.kind == ScriptedInputCheck::Kind::GimbalStep) + .frame = m_realFrameIx, + .camera = camera, + .imguizmoVirtual = scriptedImguizmoVirtual, + .imguizmoVirtualCount = scriptedImguizmoVirtualCount, + .trackedTarget = &m_followTarget, + .followConfig = hasActiveFollowConfig ? &activeFollowConfig : nullptr, + .followViewProjMatrix = hasFollowViewProjMatrix ? &followViewProjMatrix : nullptr, + .goalSolver = &m_cameraGoalSolver + }); + + for (const auto& entry : checkResult.logs) + { + if (entry.failure) { - if (!m_scriptedInput.stepValid) - { - if (m_scriptedInput.baselineValid) - setStepReference(m_scriptedInput.baselinePos, m_scriptedInput.baselineEulerDeg); - else - { - logFail("[script][fail] gimbal_step frame=%llu missing step reference", static_cast(frame)); - setStepReference(pos, euler); - ++m_scriptedInput.nextCheckIndex; - continue; - } - } - - const auto diff = float32_t3(pos.x - m_scriptedInput.stepPos.x, pos.y - m_scriptedInput.stepPos.y, pos.z - m_scriptedInput.stepPos.z); - const auto dpos = std::sqrt(diff.x * diff.x + diff.y * diff.y + diff.z * diff.z); - const auto dx = angleDiffDeg(euler.x, m_scriptedInput.stepEulerDeg.x); - const auto dy = angleDiffDeg(euler.y, m_scriptedInput.stepEulerDeg.y); - const auto dz = angleDiffDeg(euler.z, m_scriptedInput.stepEulerDeg.z); - const auto dmax = std::max(dx, std::max(dy, dz)); - - bool ok = true; - bool requiresProgress = false; - bool hasProgress = false; - if (check.hasPosDeltaConstraint) - { - if (dpos > check.posTolerance) - { - ok = false; - logFail("[script][fail] gimbal_step frame=%llu pos_delta=%.6f max=%.6f", - static_cast(frame), dpos, check.posTolerance); - } - if (check.minPosDelta > 0.0f) - { - requiresProgress = true; - hasProgress = hasProgress || dpos >= check.minPosDelta; - } - } - if (check.hasEulerDeltaConstraint) - { - if (dmax > check.eulerToleranceDeg) - { - ok = false; - logFail("[script][fail] gimbal_step frame=%llu euler_delta=%.6f max=%.6f", - static_cast(frame), dmax, check.eulerToleranceDeg); - } - if (check.minEulerDeltaDeg > 0.0f) - { - requiresProgress = true; - hasProgress = hasProgress || dmax >= check.minEulerDeltaDeg; - } - } - if (requiresProgress && !hasProgress) - { - ok = false; - logFail("[script][fail] gimbal_step frame=%llu missing progress pos_delta=%.6f euler_delta=%.6f", - static_cast(frame), dpos, dmax); - } - - if (ok) - logPass("[script][pass] gimbal_step frame=%llu pos_delta=%.6f euler_delta=%.6f", - static_cast(frame), dpos, dmax); - setStepReference(pos, euler); + m_scriptedInput.failed = true; + logFail("%s", entry.text.c_str()); } - else if (check.kind == ScriptedInputCheck::Kind::FollowTargetLock) + else { - SCameraFollowConfig activeFollowConfig = {}; - bool hasActiveFollowConfig = false; - bool hasFollowViewProjMatrix = false; - float32_t4x4 followViewProjMatrix = float32_t4x4(1.0f); - if (activeRenderWindowIx < windowBindings.size()) - { - const auto& binding = windowBindings[activeRenderWindowIx]; - const auto planarIx = binding.activePlanarIx; - if (planarIx < m_planarFollowConfigs.size()) - { - activeFollowConfig = m_planarFollowConfigs[planarIx]; - hasActiveFollowConfig = true; - } - if (planarIx < m_planarProjections.size() && m_planarProjections[planarIx] && binding.boundProjectionIx.has_value()) - { - auto& planar = m_planarProjections[planarIx]; - const auto projectionIx = binding.boundProjectionIx.value(); - auto& projections = planar->getPlanarProjections(); - if (projectionIx < projections.size()) - { - const auto viewMatrix = getMatrix3x4As4x4(getCastedMatrix(camera->getGimbal().getViewMatrix())); - const auto projectionMatrix = getCastedMatrix(projections[projectionIx].getProjectionMatrix()); - followViewProjMatrix = mul(projectionMatrix, viewMatrix); - hasFollowViewProjMatrix = true; - } - } - } - - nbl::hlsl::SCameraFollowRegressionResult regression = {}; - std::string regressionError; - const auto expectedFollowGoal = hasActiveFollowConfig - ? [&]() -> CCameraGoal - { - CCameraGoal goal = {}; - nbl::hlsl::tryBuildFollowGoal(m_cameraGoalSolver, camera, m_followTarget, activeFollowConfig, goal); - return goal; - }() - : CCameraGoal{}; - const bool ok = hasActiveFollowConfig && - nbl::hlsl::validateFollowTargetContract( - camera, - m_followTarget, - activeFollowConfig, - expectedFollowGoal, - regression, - ®ressionError, - check.eulerToleranceDeg, - 1e-6, - 1e-9, - hasFollowViewProjMatrix ? &followViewProjMatrix : nullptr, - check.posTolerance); - if (!ok) - { - logFail("[script][fail] follow_lock frame=%llu %s", - static_cast(frame), - regressionError.empty() ? "follow contract mismatch" : regressionError.c_str()); - } - else - { - logPass("[script][pass] follow_lock frame=%llu angle_deg=%.6f target_distance=%.6f screen_ndc=%.6f", - static_cast(frame), - regression.lockAngleDeg, - regression.targetDistance, - regression.projectedNdcRadius); - } + logPass("%s", entry.text.c_str()); } - - ++m_scriptedInput.nextCheckIndex; } - if (!m_scriptedInput.summaryReported && m_scriptedInput.nextCheckIndex >= m_scriptedInput.checks.size()) + if (!m_scriptedInput.summaryReported && m_scriptedInput.checkRuntime.nextCheckIndex >= m_scriptedInput.timeline.checks.size()) { m_scriptedInput.summaryReported = true; if (m_scriptedInput.failed) diff --git a/61_UI/AppWorkLoop.cpp b/61_UI/AppWorkLoop.cpp index 7d89a68d8..f3f6f3f0c 100644 --- a/61_UI/AppWorkLoop.cpp +++ b/61_UI/AppWorkLoop.cpp @@ -405,10 +405,10 @@ void App::workLoopBody() } } - if (!m_disableScreenshotsCli && m_scriptedInput.enabled && !m_scriptedInput.captureFrames.empty()) + if (!m_disableScreenshotsCli && m_scriptedInput.enabled && !m_scriptedInput.timeline.captureFrames.empty()) { - while (m_scriptedInput.nextCaptureIndex < m_scriptedInput.captureFrames.size() && - m_scriptedInput.captureFrames[m_scriptedInput.nextCaptureIndex] == renderedFrameIx) + 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"); diff --git a/61_UI/README.md b/61_UI/README.md index 18b58f603..17c1da4d0 100644 --- a/61_UI/README.md +++ b/61_UI/README.md @@ -110,7 +110,16 @@ FAIL means missing movement, out-of-range movement, invalid state, or missing re Goal: verify smooth frame-to-frame behavior (no visible teleport-like jumps). The continuity asset is now a compact authored camera-sequence spec, not a committed frame dump. -`61_UI` first compiles that shared camera-domain description into normalized sampled segments and shared frame-policy schedules, and only then expands it into its own runtime scripted checks. + +`61_UI` also consumes shared scripted runtime helpers: + +- `CCameraScriptedRuntime.hpp` for expanded per-frame payload types +- `CCameraScriptedRuntimePersistence.hpp` for low-level scripted JSON parsing and backward compatibility +- `CCameraSequenceScriptedBuilder.hpp` for compact-sequence to scripted-runtime expansion +- `CCameraScriptedCheckRunner.hpp` for baseline/step/follow-lock check evaluation + +The example keeps only the runtime-object lookup and logging glue locally. +`61_UI` first compiles that shared camera-domain description into normalized sampled segments and shared frame-policy schedules, then feeds the shared scripted-runtime contract before adapting it to the local example loop. Per authored segment: diff --git a/61_UI/include/app/App.hpp b/61_UI/include/app/App.hpp index d30adfee7..6214d8693 100644 --- a/61_UI/include/app/App.hpp +++ b/61_UI/include/app/App.hpp @@ -25,7 +25,7 @@ using nbl_json = nlohmann::json; #include "keysmapping.hpp" #include "app/AppTypes.hpp" #include "camera/CCubeProjection.hpp" -#include "glm/glm/ext/matrix_clip_space.hpp" // TODO: TESTING +#include "glm/glm/ext/matrix_clip_space.hpp" #include "glm/gtc/quaternion.hpp" #include "nbl/ext/Frustum/CDrawFrustum.h" #include "nbl/ext/FullScreenTriangle/FullScreenTriangle.h" @@ -423,11 +423,11 @@ class App final : public examples::SimpleWindowedApplication { if (m_scriptedInput.enabled) { - if (m_scriptedInput.nextCaptureIndex < m_scriptedInput.captureFrames.size()) + if (m_scriptedInput.nextCaptureIndex < m_scriptedInput.timeline.captureFrames.size()) return true; - if (m_scriptedInput.nextEventIndex < m_scriptedInput.events.size()) + if (m_scriptedInput.nextEventIndex < m_scriptedInput.timeline.events.size()) return true; - if (m_scriptedInput.nextCheckIndex < m_scriptedInput.checks.size()) + if (m_scriptedInput.checkRuntime.nextCheckIndex < m_scriptedInput.timeline.checks.size()) return true; } return false; @@ -1935,117 +1935,6 @@ class App final : public examples::SimpleWindowedApplication binding.inputBindingProjectionIx = projectionIx; } - struct ScriptedInputEvent - { - enum class Type : uint8_t - { - Keyboard, - Mouse, - Imguizmo, - Action, - Goal, - TrackedTargetTransform, - SegmentLabel - }; - - struct KeyboardData - { - ui::E_KEY_CODE key = ui::EKC_NONE; - ui::SKeyboardEvent::E_KEY_ACTION action = ui::SKeyboardEvent::ECA_UNITIALIZED; - }; - - struct MouseData - { - ui::SMouseEvent::E_EVENT_TYPE type = ui::SMouseEvent::EET_UNITIALIZED; - ui::E_MOUSE_BUTTON button = ui::EMB_LEFT_BUTTON; - ui::SMouseEvent::SClickEvent::E_ACTION action = ui::SMouseEvent::SClickEvent::EA_UNITIALIZED; - int16_t x = 0; - int16_t y = 0; - int16_t dx = 0; - int16_t dy = 0; - int16_t v = 0; - int16_t h = 0; - }; - - struct ActionData - { - enum class Kind : uint8_t - { - SetActiveRenderWindow, - SetActivePlanar, - SetProjectionType, - SetProjectionIndex, - SetUseWindow, - SetLeftHanded, - ResetActiveCamera - }; - - Kind kind = Kind::SetActiveRenderWindow; - int32_t value = 0; - }; - - struct GoalData - { - CCameraGoal goal = {}; - bool requireExact = true; - }; - - struct TrackedTargetTransformData - { - float64_t4x4 transform = float64_t4x4(1.0); - }; - - struct SegmentLabelData - { - std::string label; - }; - - uint64_t frame = 0; - Type type = Type::Keyboard; - KeyboardData keyboard; - MouseData mouse; - float32_t4x4 imguizmo = float32_t4x4(1.f); - ActionData action; - GoalData goal; - TrackedTargetTransformData trackedTargetTransform; - SegmentLabelData segmentLabel; - }; - - struct ScriptedInputCheck - { - enum class Kind : uint8_t - { - Baseline, - ImguizmoVirtual, - GimbalNear, - GimbalDelta, - GimbalStep, - FollowTargetLock - }; - - struct ExpectedVirtualEvent - { - CVirtualGimbalEvent::VirtualEventType type = CVirtualGimbalEvent::None; - float64_t magnitude = 0.0; - }; - - uint64_t frame = 0; - Kind kind = Kind::Baseline; - float tolerance = 1e-3f; - std::vector expectedVirtualEvents; - - float32_t3 expectedPos = float32_t3(0.f); - float32_t3 expectedEulerDeg = float32_t3(0.f); - bool hasExpectedPos = false; - bool hasExpectedEuler = false; - float posTolerance = 0.05f; - float eulerToleranceDeg = 1.0f; - float minPosDelta = 0.0f; - float minEulerDeltaDeg = 0.0f; - bool hasPosDeltaConstraint = false; - bool hasEulerDeltaConstraint = false; - }; - struct ScriptedInputState { bool enabled = false; @@ -2055,22 +1944,14 @@ class App final : public examples::SimpleWindowedApplication bool visualDebug = false; float visualTargetFps = 0.f; float visualCameraHoldSeconds = 0.f; - std::vector events; + CCameraScriptedTimeline timeline = {}; size_t nextEventIndex = 0; - std::vector checks; - size_t nextCheckIndex = 0; - std::vector captureFrames; + CCameraScriptedCheckRuntimeState checkRuntime = {}; size_t nextCaptureIndex = 0; std::string capturePrefix = "script"; system::path captureOutputDir; bool failed = false; bool summaryReported = false; - bool baselineValid = false; - float32_t3 baselinePos = float32_t3(0.f); - float32_t3 baselineEulerDeg = float32_t3(0.f); - bool stepValid = false; - float32_t3 stepPos = float32_t3(0.f); - float32_t3 stepEulerDeg = float32_t3(0.f); bool visualActivePlanarValid = false; uint32_t visualActivePlanarIx = 0u; uint64_t visualActivePlanarStartFrame = 0u; diff --git a/61_UI/include/common.hpp b/61_UI/include/common.hpp index 272d418fb..61a3db576 100644 --- a/61_UI/include/common.hpp +++ b/61_UI/include/common.hpp @@ -24,6 +24,10 @@ #include "camera/CCameraPlaybackTimeline.hpp" #include "camera/CCameraPersistence.hpp" #include "camera/CCameraSequenceScript.hpp" +#include "camera/CCameraSequenceScriptedBuilder.hpp" +#include "camera/CCameraScriptedRuntime.hpp" +#include "camera/CCameraScriptedRuntimePersistence.hpp" +#include "camera/CCameraScriptedCheckRunner.hpp" #include "camera/CCameraGoalAnalysis.hpp" #include "camera/CCameraGoalSolver.hpp" #include "camera/CCameraManipulationUtilities.hpp" @@ -81,11 +85,23 @@ using nbl::hlsl::CCameraSequenceTrackedTargetPose; using nbl::hlsl::CCameraSequenceTrackedTargetTrack; using nbl::hlsl::CCameraSequencePresentation; using nbl::hlsl::CCameraSequenceContinuitySettings; +using nbl::hlsl::CCameraSequenceScriptedSegmentBuildInfo; +using nbl::hlsl::CCameraScriptedInputEvent; +using nbl::hlsl::CCameraScriptedInputCheck; +using nbl::hlsl::CCameraScriptedFrameEvents; +using nbl::hlsl::CCameraScriptedTimeline; +using nbl::hlsl::CCameraScriptedControlOverrides; +using nbl::hlsl::CCameraScriptedInputParseResult; +using nbl::hlsl::CCameraScriptedCheckContext; +using nbl::hlsl::CCameraScriptedCheckFrameResult; +using nbl::hlsl::CCameraScriptedCheckLogEntry; +using nbl::hlsl::CCameraScriptedCheckRuntimeState; using nbl::hlsl::SCameraPlaybackAdvanceResult; using nbl::hlsl::SCameraPresetApplySummary; using nbl::hlsl::SCameraGoalApplyAnalysis; using nbl::hlsl::SCameraCaptureAnalysis; using nbl::hlsl::SCameraFollowConfig; +using nbl::hlsl::SCameraFollowVisualMetrics; using nbl::hlsl::SCameraGoalApplyPresentation; using nbl::hlsl::SCameraGoalApplyPresentationBadges; using nbl::hlsl::SCameraCapturePresentation; diff --git a/common/include/camera/CCameraFollowRegressionUtilities.hpp b/common/include/camera/CCameraFollowRegressionUtilities.hpp index 9c8ba2909..7da8f2c08 100644 --- a/common/include/camera/CCameraFollowRegressionUtilities.hpp +++ b/common/include/camera/CCameraFollowRegressionUtilities.hpp @@ -37,6 +37,20 @@ struct SCameraFollowRegressionResult float sphericalDistance = 0.0f; }; +//! Reusable visual/debug metrics for one active follow configuration. +struct SCameraFollowVisualMetrics +{ + bool active = false; + ECameraFollowMode mode = ECameraFollowMode::Disabled; + bool lockValid = false; + float lockAngleDeg = 0.0f; + float targetDistance = 0.0f; + bool projectedValid = false; + float projectedNdcX = 0.0f; + float projectedNdcY = 0.0f; + float projectedNdcRadius = 0.0f; +}; + //! Bundled reusable follow regression flow. //! The helper builds a follow goal, applies it, verifies the resulting camera state, //! and then checks the lock/writeback follow contract. @@ -107,6 +121,38 @@ inline bool validateProjectedFollowTargetContract( return true; } +inline SCameraFollowVisualMetrics buildFollowVisualMetrics( + ICamera* camera, + const CTrackedTarget& trackedTarget, + const SCameraFollowConfig* followConfig, + const float32_t4x4* viewProjMatrix = nullptr) +{ + SCameraFollowVisualMetrics out = {}; + if (!camera || !followConfig || !followConfig->enabled || followConfig->mode == ECameraFollowMode::Disabled) + return out; + + out.active = true; + out.mode = followConfig->mode; + + double targetDistance = 0.0; + out.lockValid = cameraFollowModeLocksViewToTarget(followConfig->mode) && + tryComputeFollowTargetLockMetrics(camera->getGimbal(), trackedTarget, out.lockAngleDeg, &targetDistance); + if (out.lockValid) + out.targetDistance = static_cast(targetDistance); + + if (out.lockValid && viewProjMatrix) + { + out.projectedValid = tryComputeProjectedFollowTargetMetrics( + *viewProjMatrix, + trackedTarget, + out.projectedNdcX, + out.projectedNdcY, + &out.projectedNdcRadius); + } + + return out; +} + inline bool validateFollowTargetContract( ICamera* camera, const CTrackedTarget& trackedTarget, diff --git a/common/include/camera/CCameraScriptedCheckRunner.hpp b/common/include/camera/CCameraScriptedCheckRunner.hpp new file mode 100644 index 000000000..51f438fe0 --- /dev/null +++ b/common/include/camera/CCameraScriptedCheckRunner.hpp @@ -0,0 +1,539 @@ +// 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_SCRIPTED_CHECK_RUNNER_HPP_ +#define _C_CAMERA_SCRIPTED_CHECK_RUNNER_HPP_ + +#include +#include +#include +#include +#include +#include + +#include "CCameraFollowRegressionUtilities.hpp" +#include "CCameraScriptedRuntime.hpp" + +namespace nbl::hlsl +{ + +/** +* Runtime state for authored scripted checks. +* +* The state is intentionally small: +* +* - authored check data stays in `CCameraScriptedInputCheck` +* - the runner only remembers where the next check starts +* - baseline and step references are maintained here so consumers do not have to +* keep duplicating the same bookkeeping +*/ +struct CCameraScriptedCheckRuntimeState +{ + size_t nextCheckIndex = 0u; + bool baselineValid = false; + float32_t3 baselinePos = float32_t3(0.f); + float32_t3 baselineEulerDeg = float32_t3(0.f); + bool stepValid = false; + float32_t3 stepPos = float32_t3(0.f); + float32_t3 stepEulerDeg = float32_t3(0.f); +}; + +//! Shared per-frame evaluation context for authored scripted checks. +struct CCameraScriptedCheckContext +{ + uint64_t frame = 0ull; + ICamera* camera = nullptr; + const CVirtualGimbalEvent* imguizmoVirtual = nullptr; + uint32_t imguizmoVirtualCount = 0u; + const CTrackedTarget* trackedTarget = nullptr; + const SCameraFollowConfig* followConfig = nullptr; + const float32_t4x4* followViewProjMatrix = nullptr; + const CCameraGoalSolver* goalSolver = nullptr; +}; + +//! Reusable log entry produced by scripted check evaluation. +struct CCameraScriptedCheckLogEntry +{ + bool failure = false; + std::string text; +}; + +//! Result for one frame worth of scripted checks. +struct CCameraScriptedCheckFrameResult +{ + std::vector logs; + bool hadFailures = false; +}; + +inline float scriptedCheckAngleDiffDeg(const float a, const float b) +{ + float d = std::fmod(a - b + 180.0f, 360.0f); + if (d < 0.0f) + d += 360.0f; + return std::abs(d - 180.0f); +} + +inline bool scriptedCheckFinite3(const float32_t3& v) +{ + return std::isfinite(v.x) && std::isfinite(v.y) && std::isfinite(v.z); +} + +inline void scriptedCheckSetStepReference( + CCameraScriptedCheckRuntimeState& state, + const float32_t3& pos, + const float32_t3& eulerDeg) +{ + state.stepValid = true; + state.stepPos = pos; + state.stepEulerDeg = eulerDeg; +} + +template +inline std::string buildScriptedCheckMessage(Fn&& formatter) +{ + std::ostringstream oss; + formatter(oss); + return oss.str(); +} + +inline void appendScriptedCheckLog( + CCameraScriptedCheckFrameResult& result, + const bool failure, + std::string&& text) +{ + result.logs.push_back({ + .failure = failure, + .text = std::move(text) + }); + result.hadFailures = result.hadFailures || failure; +} + +//! Evaluate all authored scripted checks scheduled for the current frame. +inline CCameraScriptedCheckFrameResult evaluateScriptedChecksForFrame( + const std::vector& checks, + CCameraScriptedCheckRuntimeState& state, + const CCameraScriptedCheckContext& context) +{ + CCameraScriptedCheckFrameResult result = {}; + + while (state.nextCheckIndex < checks.size() && checks[state.nextCheckIndex].frame == context.frame) + { + const auto& check = checks[state.nextCheckIndex]; + + if (!context.camera) + { + appendScriptedCheckLog( + result, + true, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << "[script][fail] check frame=" << context.frame << " no active camera"; + })); + ++state.nextCheckIndex; + continue; + } + + const auto& gimbal = context.camera->getGimbal(); + const auto pos = gimbal.getPosition(); + const auto eulerDeg = glm::degrees(glm::eulerAngles(gimbal.getOrientation())); + + if (!scriptedCheckFinite3(pos) || !scriptedCheckFinite3(eulerDeg)) + { + appendScriptedCheckLog( + result, + true, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << "[script][fail] check frame=" << context.frame << " non-finite gimbal state"; + })); + ++state.nextCheckIndex; + continue; + } + + switch (check.kind) + { + case CCameraScriptedInputCheck::Kind::Baseline: + { + state.baselineValid = true; + state.baselinePos = pos; + state.baselineEulerDeg = eulerDeg; + scriptedCheckSetStepReference(state, pos, eulerDeg); + appendScriptedCheckLog( + result, + false, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << std::fixed << std::setprecision(3); + oss << "[script][pass] baseline frame=" << context.frame + << " pos=(" << pos.x << ", " << pos.y << ", " << pos.z << ")" + << " euler_deg=(" << eulerDeg.x << ", " << eulerDeg.y << ", " << eulerDeg.z << ")"; + })); + break; + } + case CCameraScriptedInputCheck::Kind::ImguizmoVirtual: + { + bool ok = true; + if (!context.imguizmoVirtual || context.imguizmoVirtualCount == 0u) + { + ok = false; + } + else + { + for (const auto& expected : check.expectedVirtualEvents) + { + bool found = false; + double actual = 0.0; + for (uint32_t i = 0u; i < context.imguizmoVirtualCount; ++i) + { + if (context.imguizmoVirtual[i].type == expected.type) + { + found = true; + actual = context.imguizmoVirtual[i].magnitude; + break; + } + } + + if (!found || std::abs(actual - expected.magnitude) > check.tolerance) + { + ok = false; + appendScriptedCheckLog( + result, + true, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << std::fixed << std::setprecision(6); + oss << "[script][fail] imguizmo_virtual frame=" << context.frame + << " type=" << CVirtualGimbalEvent::virtualEventToString(expected.type).data() + << " expected=" << expected.magnitude + << " actual=" << actual + << " tol=" << check.tolerance; + })); + } + } + } + + if (ok) + { + appendScriptedCheckLog( + result, + false, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << "[script][pass] imguizmo_virtual frame=" << context.frame + << " events=" << check.expectedVirtualEvents.size(); + })); + } + break; + } + case CCameraScriptedInputCheck::Kind::GimbalNear: + { + bool ok = true; + if (check.hasExpectedPos) + { + const auto diff = float32_t3(pos.x - check.expectedPos.x, pos.y - check.expectedPos.y, pos.z - check.expectedPos.z); + const auto distance = std::sqrt(diff.x * diff.x + diff.y * diff.y + diff.z * diff.z); + if (distance > check.posTolerance) + { + ok = false; + appendScriptedCheckLog( + result, + true, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << std::fixed << std::setprecision(6); + oss << "[script][fail] gimbal_near frame=" << context.frame + << " pos_diff=" << distance + << " tol=" << check.posTolerance; + })); + } + } + if (check.hasExpectedEuler) + { + const auto dx = scriptedCheckAngleDiffDeg(eulerDeg.x, check.expectedEulerDeg.x); + const auto dy = scriptedCheckAngleDiffDeg(eulerDeg.y, check.expectedEulerDeg.y); + const auto dz = scriptedCheckAngleDiffDeg(eulerDeg.z, check.expectedEulerDeg.z); + const auto maxAngle = std::max(dx, std::max(dy, dz)); + if (maxAngle > check.eulerToleranceDeg) + { + ok = false; + appendScriptedCheckLog( + result, + true, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << std::fixed << std::setprecision(6); + oss << "[script][fail] gimbal_near frame=" << context.frame + << " euler_diff=" << maxAngle + << " tol=" << check.eulerToleranceDeg; + })); + } + } + + if (ok) + { + appendScriptedCheckLog( + result, + false, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << "[script][pass] gimbal_near frame=" << context.frame; + })); + } + break; + } + case CCameraScriptedInputCheck::Kind::GimbalDelta: + { + if (!state.baselineValid) + { + appendScriptedCheckLog( + result, + true, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << "[script][fail] gimbal_delta frame=" << context.frame << " missing baseline"; + })); + break; + } + + const auto diff = float32_t3(pos.x - state.baselinePos.x, pos.y - state.baselinePos.y, pos.z - state.baselinePos.z); + const auto dpos = std::sqrt(diff.x * diff.x + diff.y * diff.y + diff.z * diff.z); + const auto dx = scriptedCheckAngleDiffDeg(eulerDeg.x, state.baselineEulerDeg.x); + const auto dy = scriptedCheckAngleDiffDeg(eulerDeg.y, state.baselineEulerDeg.y); + const auto dz = scriptedCheckAngleDiffDeg(eulerDeg.z, state.baselineEulerDeg.z); + const auto dmax = std::max(dx, std::max(dy, dz)); + + if (dpos > check.posTolerance || dmax > check.eulerToleranceDeg) + { + appendScriptedCheckLog( + result, + true, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << std::fixed << std::setprecision(6); + oss << "[script][fail] gimbal_delta frame=" << context.frame + << " pos_diff=" << dpos + << " tol=" << check.posTolerance + << " euler_diff=" << dmax + << " tol=" << check.eulerToleranceDeg; + })); + } + else + { + appendScriptedCheckLog( + result, + false, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << std::fixed << std::setprecision(6); + oss << "[script][pass] gimbal_delta frame=" << context.frame + << " pos_diff=" << dpos + << " euler_diff=" << dmax; + })); + } + break; + } + case CCameraScriptedInputCheck::Kind::GimbalStep: + { + if (!state.stepValid) + { + if (state.baselineValid) + { + scriptedCheckSetStepReference(state, state.baselinePos, state.baselineEulerDeg); + } + else + { + appendScriptedCheckLog( + result, + true, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << "[script][fail] gimbal_step frame=" << context.frame << " missing step reference"; + })); + scriptedCheckSetStepReference(state, pos, eulerDeg); + ++state.nextCheckIndex; + continue; + } + } + + const auto diff = float32_t3(pos.x - state.stepPos.x, pos.y - state.stepPos.y, pos.z - state.stepPos.z); + const auto dpos = std::sqrt(diff.x * diff.x + diff.y * diff.y + diff.z * diff.z); + const auto dx = scriptedCheckAngleDiffDeg(eulerDeg.x, state.stepEulerDeg.x); + const auto dy = scriptedCheckAngleDiffDeg(eulerDeg.y, state.stepEulerDeg.y); + const auto dz = scriptedCheckAngleDiffDeg(eulerDeg.z, state.stepEulerDeg.z); + const auto dmax = std::max(dx, std::max(dy, dz)); + + bool ok = true; + bool requiresProgress = false; + bool hasProgress = false; + if (check.hasPosDeltaConstraint) + { + if (dpos > check.posTolerance) + { + ok = false; + appendScriptedCheckLog( + result, + true, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << std::fixed << std::setprecision(6); + oss << "[script][fail] gimbal_step frame=" << context.frame + << " pos_delta=" << dpos + << " max=" << check.posTolerance; + })); + } + if (check.minPosDelta > 0.0f) + { + requiresProgress = true; + hasProgress = hasProgress || dpos >= check.minPosDelta; + } + } + if (check.hasEulerDeltaConstraint) + { + if (dmax > check.eulerToleranceDeg) + { + ok = false; + appendScriptedCheckLog( + result, + true, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << std::fixed << std::setprecision(6); + oss << "[script][fail] gimbal_step frame=" << context.frame + << " euler_delta=" << dmax + << " max=" << check.eulerToleranceDeg; + })); + } + if (check.minEulerDeltaDeg > 0.0f) + { + requiresProgress = true; + hasProgress = hasProgress || dmax >= check.minEulerDeltaDeg; + } + } + if (requiresProgress && !hasProgress) + { + ok = false; + appendScriptedCheckLog( + result, + true, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << std::fixed << std::setprecision(6); + oss << "[script][fail] gimbal_step frame=" << context.frame + << " missing progress pos_delta=" << dpos + << " euler_delta=" << dmax; + })); + } + + if (ok) + { + appendScriptedCheckLog( + result, + false, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << std::fixed << std::setprecision(6); + oss << "[script][pass] gimbal_step frame=" << context.frame + << " pos_delta=" << dpos + << " euler_delta=" << dmax; + })); + } + scriptedCheckSetStepReference(state, pos, eulerDeg); + break; + } + case CCameraScriptedInputCheck::Kind::FollowTargetLock: + { + if (!context.followConfig) + { + appendScriptedCheckLog( + result, + true, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << "[script][fail] follow_lock frame=" << context.frame << " missing follow config"; + })); + break; + } + if (!context.trackedTarget) + { + appendScriptedCheckLog( + result, + true, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << "[script][fail] follow_lock frame=" << context.frame << " missing tracked target"; + })); + break; + } + if (!context.goalSolver) + { + appendScriptedCheckLog( + result, + true, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << "[script][fail] follow_lock frame=" << context.frame << " missing goal solver"; + })); + break; + } + + SCameraFollowRegressionResult regression = {}; + std::string regressionError; + CCameraGoal expectedFollowGoal = {}; + const bool ok = tryBuildFollowGoal( + *context.goalSolver, + context.camera, + *context.trackedTarget, + *context.followConfig, + expectedFollowGoal) && + validateFollowTargetContract( + context.camera, + *context.trackedTarget, + *context.followConfig, + expectedFollowGoal, + regression, + ®ressionError, + check.eulerToleranceDeg, + 1e-6, + 1e-9, + context.followViewProjMatrix, + check.posTolerance); + + if (!ok) + { + appendScriptedCheckLog( + result, + true, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << "[script][fail] follow_lock frame=" << context.frame << ' ' + << (regressionError.empty() ? "follow contract mismatch" : regressionError); + })); + } + else + { + appendScriptedCheckLog( + result, + false, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << std::fixed << std::setprecision(6); + oss << "[script][pass] follow_lock frame=" << context.frame + << " angle_deg=" << regression.lockAngleDeg + << " target_distance=" << regression.targetDistance + << " screen_ndc=" << regression.projectedNdcRadius; + })); + } + break; + } + } + + ++state.nextCheckIndex; + } + + return result; +} + +} // namespace nbl::hlsl + +#endif // _C_CAMERA_SCRIPTED_CHECK_RUNNER_HPP_ diff --git a/common/include/camera/CCameraScriptedRuntime.hpp b/common/include/camera/CCameraScriptedRuntime.hpp new file mode 100644 index 000000000..2f5f61b1d --- /dev/null +++ b/common/include/camera/CCameraScriptedRuntime.hpp @@ -0,0 +1,360 @@ +// 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_SCRIPTED_RUNTIME_HPP_ +#define _C_CAMERA_SCRIPTED_RUNTIME_HPP_ + +#include +#include +#include +#include + +#include "CCameraGoal.hpp" +#include "IGimbal.hpp" +#include "nbl/ui/SInputEvent.h" + +namespace nbl::hlsl +{ + +/** +* Shared scripted runtime contract used by camera-sequence consumers. +* +* The compact authored sequence remains camera-domain. A concrete runtime may still expand it +* into low-level per-frame events and checks, but those expanded payloads live in this shared +* header rather than inside one example. +*/ +struct CCameraScriptedInputEvent +{ + enum class Type : uint8_t + { + Keyboard, + Mouse, + Imguizmo, + Action, + Goal, + TrackedTargetTransform, + SegmentLabel + }; + + struct KeyboardData + { + ui::E_KEY_CODE key = ui::EKC_NONE; + ui::SKeyboardEvent::E_KEY_ACTION action = ui::SKeyboardEvent::ECA_UNITIALIZED; + }; + + struct MouseData + { + ui::SMouseEvent::E_EVENT_TYPE type = ui::SMouseEvent::EET_UNITIALIZED; + ui::E_MOUSE_BUTTON button = ui::EMB_LEFT_BUTTON; + ui::SMouseEvent::SClickEvent::E_ACTION action = ui::SMouseEvent::SClickEvent::EA_UNITIALIZED; + int16_t x = 0; + int16_t y = 0; + int16_t dx = 0; + int16_t dy = 0; + int16_t v = 0; + int16_t h = 0; + }; + + struct ActionData + { + enum class Kind : uint8_t + { + SetActiveRenderWindow, + SetActivePlanar, + SetProjectionType, + SetProjectionIndex, + SetUseWindow, + SetLeftHanded, + ResetActiveCamera + }; + + Kind kind = Kind::SetActiveRenderWindow; + int32_t value = 0; + }; + + struct GoalData + { + CCameraGoal goal = {}; + bool requireExact = true; + }; + + struct TrackedTargetTransformData + { + float64_t4x4 transform = float64_t4x4(1.0); + }; + + struct SegmentLabelData + { + std::string label; + }; + + uint64_t frame = 0; + Type type = Type::Keyboard; + KeyboardData keyboard; + MouseData mouse; + float32_t4x4 imguizmo = float32_t4x4(1.f); + ActionData action; + GoalData goal; + TrackedTargetTransformData trackedTargetTransform; + SegmentLabelData segmentLabel; +}; + +struct CCameraScriptedInputCheck +{ + enum class Kind : uint8_t + { + Baseline, + ImguizmoVirtual, + GimbalNear, + GimbalDelta, + GimbalStep, + FollowTargetLock + }; + + struct ExpectedVirtualEvent + { + CVirtualGimbalEvent::VirtualEventType type = CVirtualGimbalEvent::None; + float64_t magnitude = 0.0; + }; + + uint64_t frame = 0; + Kind kind = Kind::Baseline; + float tolerance = 1e-3f; + std::vector expectedVirtualEvents; + + float32_t3 expectedPos = float32_t3(0.f); + float32_t3 expectedEulerDeg = float32_t3(0.f); + bool hasExpectedPos = false; + bool hasExpectedEuler = false; + float posTolerance = 0.05f; + float eulerToleranceDeg = 1.0f; + float minPosDelta = 0.0f; + float minEulerDeltaDeg = 0.0f; + bool hasPosDeltaConstraint = false; + bool hasEulerDeltaConstraint = false; +}; + +//! Fully expanded scripted timeline shared between authored parsers and runtime consumers. +struct CCameraScriptedTimeline +{ + std::vector events; + std::vector checks; + std::vector captureFrames; + + inline void clear() + { + events.clear(); + checks.clear(); + captureFrames.clear(); + } + + inline bool empty() const + { + return events.empty() && checks.empty() && captureFrames.empty(); + } +}; + +inline void finalizeScriptedTimeline( + std::vector& events, + std::vector& checks, + std::vector& captureFrames, + const bool disableCaptureFrames = false) +{ + std::stable_sort(events.begin(), events.end(), + [](const CCameraScriptedInputEvent& a, const CCameraScriptedInputEvent& b) { return a.frame < b.frame; }); + std::stable_sort(checks.begin(), checks.end(), + [](const CCameraScriptedInputCheck& a, const CCameraScriptedInputCheck& b) { return a.frame < b.frame; }); + if (!captureFrames.empty()) + { + std::sort(captureFrames.begin(), captureFrames.end()); + captureFrames.erase(std::unique(captureFrames.begin(), captureFrames.end()), captureFrames.end()); + } + if (disableCaptureFrames) + captureFrames.clear(); +} + +inline void finalizeScriptedTimeline(CCameraScriptedTimeline& timeline, const bool disableCaptureFrames = false) +{ + finalizeScriptedTimeline(timeline.events, timeline.checks, timeline.captureFrames, disableCaptureFrames); +} + +inline void appendScriptedActionEvent( + CCameraScriptedTimeline& timeline, + const uint64_t frame, + const CCameraScriptedInputEvent::ActionData::Kind kind, + const int32_t value) +{ + CCameraScriptedInputEvent entry; + entry.frame = frame; + entry.type = CCameraScriptedInputEvent::Type::Action; + entry.action.kind = kind; + entry.action.value = value; + timeline.events.emplace_back(std::move(entry)); +} + +inline void appendScriptedGoalEvent( + CCameraScriptedTimeline& timeline, + const uint64_t frame, + const CCameraGoal& goal, + const bool requireExact = true) +{ + CCameraScriptedInputEvent entry; + entry.frame = frame; + entry.type = CCameraScriptedInputEvent::Type::Goal; + entry.goal.goal = goal; + entry.goal.requireExact = requireExact; + timeline.events.emplace_back(std::move(entry)); +} + +inline void appendScriptedTrackedTargetTransformEvent( + CCameraScriptedTimeline& timeline, + const uint64_t frame, + const float64_t4x4& transform) +{ + CCameraScriptedInputEvent entry; + entry.frame = frame; + entry.type = CCameraScriptedInputEvent::Type::TrackedTargetTransform; + entry.trackedTargetTransform.transform = transform; + timeline.events.emplace_back(std::move(entry)); +} + +inline void appendScriptedSegmentLabelEvent( + CCameraScriptedTimeline& timeline, + const uint64_t frame, + std::string label) +{ + CCameraScriptedInputEvent entry; + entry.frame = frame; + entry.type = CCameraScriptedInputEvent::Type::SegmentLabel; + entry.segmentLabel.label = std::move(label); + timeline.events.emplace_back(std::move(entry)); +} + +inline void appendScriptedBaselineCheck(CCameraScriptedTimeline& timeline, const uint64_t frame) +{ + CCameraScriptedInputCheck entry; + entry.frame = frame; + entry.kind = CCameraScriptedInputCheck::Kind::Baseline; + timeline.checks.emplace_back(std::move(entry)); +} + +inline void appendScriptedGimbalStepCheck( + CCameraScriptedTimeline& timeline, + const uint64_t frame, + const bool hasPosDeltaConstraint, + const float posTolerance, + const float minPosDelta, + const bool hasEulerDeltaConstraint, + const float eulerToleranceDeg, + const float minEulerDeltaDeg) +{ + CCameraScriptedInputCheck entry; + entry.frame = frame; + entry.kind = CCameraScriptedInputCheck::Kind::GimbalStep; + if (hasPosDeltaConstraint) + { + entry.hasPosDeltaConstraint = true; + entry.posTolerance = posTolerance; + entry.minPosDelta = minPosDelta; + } + if (hasEulerDeltaConstraint) + { + entry.hasEulerDeltaConstraint = true; + entry.eulerToleranceDeg = eulerToleranceDeg; + entry.minEulerDeltaDeg = minEulerDeltaDeg; + } + timeline.checks.emplace_back(std::move(entry)); +} + +inline void appendScriptedFollowTargetLockCheck( + CCameraScriptedTimeline& timeline, + const uint64_t frame, + const float toleranceDeg = 1.0f, + const float screenToleranceNdc = 0.03f) +{ + CCameraScriptedInputCheck entry; + entry.frame = frame; + entry.kind = CCameraScriptedInputCheck::Kind::FollowTargetLock; + entry.eulerToleranceDeg = toleranceDeg; + entry.posTolerance = screenToleranceNdc; + timeline.checks.emplace_back(std::move(entry)); +} + +/** +* Per-frame scripted runtime batch already partitioned by payload kind. +* +* Consumers can dequeue authored events for one frame and then adapt only the buckets they care +* about, without repeatedly switching on `CCameraScriptedInputEvent::Type` in local glue. +*/ +struct CCameraScriptedFrameEvents +{ + std::vector keyboard; + std::vector mouse; + std::vector imguizmo; + std::vector actions; + std::vector goals; + std::vector trackedTargetTransforms; + std::vector segmentLabels; + + inline void clear() + { + keyboard.clear(); + mouse.clear(); + imguizmo.clear(); + actions.clear(); + goals.clear(); + trackedTargetTransforms.clear(); + segmentLabels.clear(); + } + + inline bool empty() const + { + return keyboard.empty() && mouse.empty() && imguizmo.empty() && actions.empty() && + goals.empty() && trackedTargetTransforms.empty() && segmentLabels.empty(); + } +}; + +//! Dequeue all authored scripted events scheduled for one frame. +inline void dequeueScriptedFrameEvents( + const std::vector& events, + size_t& nextEventIndex, + const uint64_t frame, + CCameraScriptedFrameEvents& out) +{ + out.clear(); + while (nextEventIndex < events.size() && events[nextEventIndex].frame == frame) + { + const auto& ev = events[nextEventIndex]; + switch (ev.type) + { + case CCameraScriptedInputEvent::Type::Keyboard: + out.keyboard.emplace_back(ev.keyboard); + break; + case CCameraScriptedInputEvent::Type::Mouse: + out.mouse.emplace_back(ev.mouse); + break; + case CCameraScriptedInputEvent::Type::Imguizmo: + out.imguizmo.emplace_back(ev.imguizmo); + break; + case CCameraScriptedInputEvent::Type::Action: + out.actions.emplace_back(ev.action); + break; + case CCameraScriptedInputEvent::Type::Goal: + out.goals.emplace_back(ev.goal); + break; + case CCameraScriptedInputEvent::Type::TrackedTargetTransform: + out.trackedTargetTransforms.emplace_back(ev.trackedTargetTransform); + break; + case CCameraScriptedInputEvent::Type::SegmentLabel: + out.segmentLabels.emplace_back(ev.segmentLabel.label); + break; + } + + ++nextEventIndex; + } +} + +} // namespace nbl::hlsl + +#endif diff --git a/common/include/camera/CCameraScriptedRuntimePersistence.hpp b/common/include/camera/CCameraScriptedRuntimePersistence.hpp new file mode 100644 index 000000000..07aa8af2f --- /dev/null +++ b/common/include/camera/CCameraScriptedRuntimePersistence.hpp @@ -0,0 +1,697 @@ +// 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_SCRIPTED_RUNTIME_PERSISTENCE_HPP_ +#define _C_CAMERA_SCRIPTED_RUNTIME_PERSISTENCE_HPP_ + +#include +#include +#include +#include +#include + +#include "CCameraScriptedRuntime.hpp" +#include "CCameraSequenceScript.hpp" +#include "nbl/ui/SInputEvent.h" +#include "glm/glm/gtc/matrix_transform.hpp" +#include "glm/glm/gtc/quaternion.hpp" +#include "nlohmann/json.hpp" + +namespace nbl::hlsl +{ + +/** +* Shared JSON parser for low-level scripted runtime payloads. +* +* This keeps legacy `events/checks/capture_frames` authored data reusable and backward-compatible +* without leaving parser glue inside one example. +* +* The parser can also detect compact `segments` and forward them into `CCameraSequenceScript`. +*/ +inline float32_t4x4 composeScriptedImguizmoTransform( + const std::array& translation, + const std::array& rotationDeg, + const std::array& scale) +{ + const auto translationMatrix = glm::translate(glm::mat4(1.f), glm::vec3(translation[0], translation[1], translation[2])); + const auto rotationMatrix = glm::mat4_cast(glm::quat(glm::radians(glm::vec3(rotationDeg[0], rotationDeg[1], rotationDeg[2])))); + const auto scaleMatrix = glm::scale(glm::mat4(1.f), glm::vec3(scale[0], scale[1], scale[2])); + return float32_t4x4(translationMatrix * rotationMatrix * scaleMatrix); +} + +inline float32_t4x4 makeScriptedMatrixFromArray(const std::array& values) +{ + 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; +} + +//! Optional runtime control-scale overrides parsed from low-level scripted JSON. +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; +}; + +/** +* Parsed top-level scripted-runtime input including: +* +* - low-level expanded runtime payloads (`events`, `checks`, `capture_frames`) +* - compact camera-sequence data (`segments`) when present +* - optional runtime/debug policy flags used by scripted consumers +*/ +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 = {}; + CCameraScriptedTimeline timeline = {}; + std::optional sequence; + std::vector warnings; +}; + +inline void appendScriptedInputParseWarning(CCameraScriptedInputParseResult& out, std::string warning) +{ + out.warnings.emplace_back(std::move(warning)); +} + +inline ui::E_KEY_CODE parseScriptedKeyCode(std::string_view key) +{ + if (const auto parsed = ui::stringToKeyCode(key); parsed != ui::EKC_NONE) + return parsed; + + constexpr std::string_view keyKeyPrefix = "KEY_KEY_"; + if (key.starts_with(keyKeyPrefix)) + return ui::stringToKeyCode(key.substr(keyKeyPrefix.size())); + + constexpr std::string_view ekcPrefix = "EKC_"; + if (key.starts_with(ekcPrefix)) + return ui::stringToKeyCode(key.substr(ekcPrefix.size())); + + return ui::EKC_NONE; +} + +inline void appendScriptedInputCaptureFrame(CCameraScriptedInputParseResult& out, const uint64_t frame, const bool captureFrame) +{ + if (captureFrame) + out.timeline.captureFrames.emplace_back(frame); +} + +inline std::optional parseScriptedKeyboardAction(std::string_view action) +{ + if (action == "pressed" || action == "press") + return ui::SKeyboardEvent::ECA_PRESSED; + if (action == "released" || action == "release") + return ui::SKeyboardEvent::ECA_RELEASED; + return std::nullopt; +} + +inline std::optional parseScriptedMouseButton(std::string_view button) +{ + if (button == "LEFT_BUTTON") + return ui::EMB_LEFT_BUTTON; + if (button == "RIGHT_BUTTON") + return ui::EMB_RIGHT_BUTTON; + if (button == "MIDDLE_BUTTON") + return ui::EMB_MIDDLE_BUTTON; + if (button == "BUTTON_4") + return ui::EMB_BUTTON_4; + if (button == "BUTTON_5") + return ui::EMB_BUTTON_5; + return std::nullopt; +} + +inline std::optional parseScriptedMouseClickAction(std::string_view action) +{ + if (action == "pressed" || action == "press") + return ui::SMouseEvent::SClickEvent::EA_PRESSED; + if (action == "released" || action == "release") + return ui::SMouseEvent::SClickEvent::EA_RELEASED; + return std::nullopt; +} + +inline void parseScriptedCaptureFrames( + const nlohmann::json& script, + CCameraScriptedInputParseResult& out) +{ + if (!script.contains("capture_frames")) + return; + + for (const auto& frame : script["capture_frames"]) + out.timeline.captureFrames.emplace_back(frame.get()); +} + +inline void parseScriptedControlOverrides( + const nlohmann::json& controls, + CCameraScriptedControlOverrides& out) +{ + if (controls.contains("keyboard_scale")) + { + out.hasKeyboardScale = true; + out.keyboardScale = controls["keyboard_scale"].get(); + } + if (controls.contains("mouse_move_scale")) + { + out.hasMouseMoveScale = true; + out.mouseMoveScale = controls["mouse_move_scale"].get(); + } + if (controls.contains("mouse_scroll_scale")) + { + out.hasMouseScrollScale = true; + out.mouseScrollScale = controls["mouse_scroll_scale"].get(); + } + if (controls.contains("translation_scale")) + { + out.hasTranslationScale = true; + out.translationScale = controls["translation_scale"].get(); + } + if (controls.contains("rotation_scale")) + { + out.hasRotationScale = true; + out.rotationScale = controls["rotation_scale"].get(); + } +} + +inline bool parseScriptedSequenceIfPresent( + const nlohmann::json& script, + CCameraScriptedInputParseResult& out, + std::string* error) +{ + if (!script.contains("segments")) + return true; + + CCameraSequenceScript sequence; + std::string sequenceError; + if (!deserializeCameraSequenceScript(script, sequence, &sequenceError)) + { + if (error) + *error = std::move(sequenceError); + return false; + } + + out.sequence = std::move(sequence); + return true; +} + +inline void parseScriptedKeyboardEvent( + const nlohmann::json& event, + const uint64_t frame, + const bool captureFrame, + CCameraScriptedInputParseResult& out) +{ + if (!event.contains("key") || !event.contains("action")) + { + appendScriptedInputParseWarning(out, "Scripted keyboard event missing \"key\" or \"action\"."); + return; + } + + const auto keyStr = event["key"].get(); + const auto actionStr = event["action"].get(); + const auto key = parseScriptedKeyCode(keyStr); + if (key == ui::EKC_NONE) + { + appendScriptedInputParseWarning(out, "Scripted keyboard event has invalid key \"" + keyStr + "\"."); + return; + } + + const auto action = parseScriptedKeyboardAction(actionStr); + if (!action.has_value()) + { + appendScriptedInputParseWarning(out, "Scripted keyboard event has invalid action \"" + actionStr + "\"."); + return; + } + + CCameraScriptedInputEvent entry; + entry.frame = frame; + entry.type = CCameraScriptedInputEvent::Type::Keyboard; + entry.keyboard.key = key; + entry.keyboard.action = action.value(); + out.timeline.events.emplace_back(std::move(entry)); + appendScriptedInputCaptureFrame(out, frame, captureFrame); +} + +inline void parseScriptedMouseEvent( + const nlohmann::json& event, + const uint64_t frame, + const bool captureFrame, + CCameraScriptedInputParseResult& out) +{ + if (!event.contains("kind")) + { + appendScriptedInputParseWarning(out, "Scripted mouse event missing \"kind\"."); + return; + } + + const auto kind = event["kind"].get(); + CCameraScriptedInputEvent entry; + entry.frame = frame; + entry.type = CCameraScriptedInputEvent::Type::Mouse; + + if (kind == "move") + { + entry.mouse.type = ui::SMouseEvent::EET_MOVEMENT; + entry.mouse.dx = event.value("dx", 0); + entry.mouse.dy = event.value("dy", 0); + } + else if (kind == "scroll") + { + entry.mouse.type = ui::SMouseEvent::EET_SCROLL; + entry.mouse.v = event.value("v", 0); + entry.mouse.h = event.value("h", 0); + } + else if (kind == "click") + { + if (!event.contains("button") || !event.contains("action")) + { + appendScriptedInputParseWarning(out, "Scripted click event missing \"button\" or \"action\"."); + return; + } + + const auto buttonStr = event["button"].get(); + const auto actionStr = event["action"].get(); + const auto button = parseScriptedMouseButton(buttonStr); + if (!button.has_value()) + { + appendScriptedInputParseWarning(out, "Scripted click event has invalid button \"" + buttonStr + "\"."); + return; + } + + const auto action = parseScriptedMouseClickAction(actionStr); + if (!action.has_value()) + { + appendScriptedInputParseWarning(out, "Scripted click event has invalid action \"" + actionStr + "\"."); + return; + } + + entry.mouse.type = ui::SMouseEvent::EET_CLICK; + entry.mouse.button = button.value(); + entry.mouse.action = action.value(); + entry.mouse.x = event.value("x", 0); + entry.mouse.y = event.value("y", 0); + } + else + { + appendScriptedInputParseWarning(out, "Scripted mouse event has invalid kind \"" + kind + "\"."); + return; + } + + out.timeline.events.emplace_back(std::move(entry)); + appendScriptedInputCaptureFrame(out, frame, captureFrame); +} + +inline void parseScriptedImguizmoEvent( + const nlohmann::json& event, + const uint64_t frame, + const bool captureFrame, + CCameraScriptedInputParseResult& out) +{ + CCameraScriptedInputEvent entry; + entry.frame = frame; + entry.type = 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 rotationDeg = 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, rotationDeg, scale); + } + + out.timeline.events.emplace_back(std::move(entry)); + appendScriptedInputCaptureFrame(out, frame, captureFrame); +} + +inline int32_t parseScriptedActionIntValue(const nlohmann::json& event) +{ + if (event.contains("value")) + return event["value"].get(); + if (event.contains("index")) + return event["index"].get(); + return 0; +} + +inline bool parseScriptedProjectionActionValue( + const nlohmann::json& event, + CCameraScriptedInputEvent::ActionData& action, + CCameraScriptedInputParseResult& out) +{ + if (event.contains("value") && event["value"].is_string()) + { + const auto valueStr = event["value"].get(); + if (valueStr == "perspective") + action.value = static_cast(IPlanarProjection::CProjection::Perspective); + else if (valueStr == "orthographic") + action.value = static_cast(IPlanarProjection::CProjection::Orthographic); + else + { + appendScriptedInputParseWarning(out, "Scripted action projection type has invalid value \"" + valueStr + "\"."); + return false; + } + } + else + { + action.value = parseScriptedActionIntValue(event); + } + + return true; +} + +inline void parseScriptedActionEvent( + const nlohmann::json& event, + const uint64_t frame, + const bool captureFrame, + CCameraScriptedInputParseResult& out) +{ + if (!event.contains("action")) + { + appendScriptedInputParseWarning(out, "Scripted action event missing \"action\"."); + return; + } + + const auto actionStr = event["action"].get(); + CCameraScriptedInputEvent entry; + entry.frame = frame; + entry.type = CCameraScriptedInputEvent::Type::Action; + + if (actionStr == "set_active_render_window") + { + entry.action.kind = CCameraScriptedInputEvent::ActionData::Kind::SetActiveRenderWindow; + entry.action.value = parseScriptedActionIntValue(event); + } + else if (actionStr == "set_active_planar") + { + entry.action.kind = CCameraScriptedInputEvent::ActionData::Kind::SetActivePlanar; + entry.action.value = parseScriptedActionIntValue(event); + } + else if (actionStr == "set_projection_type") + { + entry.action.kind = CCameraScriptedInputEvent::ActionData::Kind::SetProjectionType; + if (!parseScriptedProjectionActionValue(event, entry.action, out)) + return; + } + else if (actionStr == "set_projection_index") + { + entry.action.kind = CCameraScriptedInputEvent::ActionData::Kind::SetProjectionIndex; + entry.action.value = parseScriptedActionIntValue(event); + } + else if (actionStr == "set_use_window") + { + entry.action.kind = CCameraScriptedInputEvent::ActionData::Kind::SetUseWindow; + entry.action.value = event.value("value", false) ? 1 : 0; + } + else if (actionStr == "set_left_handed") + { + entry.action.kind = CCameraScriptedInputEvent::ActionData::Kind::SetLeftHanded; + entry.action.value = event.value("value", false) ? 1 : 0; + } + else if (actionStr == "reset_active_camera") + { + entry.action.kind = CCameraScriptedInputEvent::ActionData::Kind::ResetActiveCamera; + entry.action.value = 1; + } + else + { + appendScriptedInputParseWarning(out, "Scripted action event has invalid action \"" + actionStr + "\"."); + return; + } + + out.timeline.events.emplace_back(std::move(entry)); + appendScriptedInputCaptureFrame(out, frame, captureFrame); +} + +inline void parseScriptedInputEvent( + const nlohmann::json& event, + CCameraScriptedInputParseResult& out) +{ + if (!event.contains("frame") || !event.contains("type")) + { + 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") + parseScriptedKeyboardEvent(event, frame, captureFrame, out); + else if (type == "mouse") + parseScriptedMouseEvent(event, frame, captureFrame, out); + else if (type == "imguizmo") + parseScriptedImguizmoEvent(event, frame, captureFrame, out); + else if (type == "action") + parseScriptedActionEvent(event, frame, captureFrame, out); + else + appendScriptedInputParseWarning(out, "Scripted input event has invalid type \"" + type + "\"."); +} + +inline void parseScriptedInputEvents( + const nlohmann::json& script, + CCameraScriptedInputParseResult& out) +{ + if (!script.contains("events")) + return; + + for (const auto& event : script["events"]) + parseScriptedInputEvent(event, out); +} + +inline bool parseScriptedImguizmoVirtualCheck( + const nlohmann::json& check, + CCameraScriptedInputCheck& outCheck, + CCameraScriptedInputParseResult& out) +{ + outCheck.kind = CCameraScriptedInputCheck::Kind::ImguizmoVirtual; + outCheck.tolerance = check.value("tolerance", outCheck.tolerance); + + if (!check.contains("events")) + { + appendScriptedInputParseWarning(out, "Imguizmo virtual check missing \"events\"."); + return false; + } + + for (const auto& expectedEvent : check["events"]) + { + if (!expectedEvent.contains("type") || !expectedEvent.contains("magnitude")) + { + appendScriptedInputParseWarning(out, "Imguizmo virtual check event missing \"type\" or \"magnitude\"."); + continue; + } + + const auto typeStr = expectedEvent["type"].get(); + const auto type = CVirtualGimbalEvent::stringToVirtualEvent(typeStr); + if (type == CVirtualGimbalEvent::None) + { + appendScriptedInputParseWarning(out, "Imguizmo virtual check event has invalid type \"" + typeStr + "\"."); + continue; + } + + CCameraScriptedInputCheck::ExpectedVirtualEvent expected; + expected.type = type; + expected.magnitude = expectedEvent["magnitude"].get(); + outCheck.expectedVirtualEvents.emplace_back(expected); + } + + return true; +} + +inline bool parseScriptedCheck( + const nlohmann::json& check, + CCameraScriptedInputParseResult& out) +{ + if (!check.contains("frame") || !check.contains("kind")) + { + appendScriptedInputParseWarning(out, "Scripted check missing \"frame\" or \"kind\"."); + return false; + } + + const auto frame = check["frame"].get(); + const auto kind = check["kind"].get(); + + CCameraScriptedInputCheck entry; + entry.frame = frame; + + if (kind == "baseline") + { + entry.kind = CCameraScriptedInputCheck::Kind::Baseline; + } + else if (kind == "imguizmo_virtual") + { + if (!parseScriptedImguizmoVirtualCheck(check, entry, out)) + return false; + } + else if (kind == "gimbal_near") + { + entry.kind = CCameraScriptedInputCheck::Kind::GimbalNear; + entry.posTolerance = check.value("pos_tolerance", entry.posTolerance); + entry.eulerToleranceDeg = check.value("euler_tolerance_deg", entry.eulerToleranceDeg); + + if (check.contains("position")) + { + const auto pos = check["position"].get>(); + entry.expectedPos = float32_t3(pos[0], pos[1], pos[2]); + entry.hasExpectedPos = true; + } + if (check.contains("euler_deg")) + { + const auto euler = check["euler_deg"].get>(); + entry.expectedEulerDeg = float32_t3(euler[0], euler[1], euler[2]); + entry.hasExpectedEuler = true; + } + } + else if (kind == "gimbal_delta") + { + entry.kind = 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 = 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) + { + appendScriptedInputParseWarning(out, "gimbal_step check requires at least one delta constraint."); + return false; + } + } + else + { + appendScriptedInputParseWarning(out, "Scripted check has invalid kind \"" + kind + "\"."); + return false; + } + + out.timeline.checks.emplace_back(std::move(entry)); + return true; +} + +inline void parseScriptedChecks( + const nlohmann::json& script, + CCameraScriptedInputParseResult& out) +{ + if (!script.contains("checks")) + return; + + for (const auto& check : script["checks"]) + parseScriptedCheck(check, out); +} + +inline bool deserializeCameraScriptedInput( + const nlohmann::json& script, + CCameraScriptedInputParseResult& out, + std::string* error = nullptr) +{ + 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"; + + parseScriptedCaptureFrames(script, out); + + if (script.contains("camera_controls")) + parseScriptedControlOverrides(script["camera_controls"], out.cameraControls); + + if (!parseScriptedSequenceIfPresent(script, out, error)) + return false; + + parseScriptedInputEvents(script, out); + parseScriptedChecks(script, out); + + finalizeScriptedTimeline(out.timeline); + return true; +} + +} // namespace nbl::hlsl + +#endif diff --git a/common/include/camera/CCameraSequenceScriptedBuilder.hpp b/common/include/camera/CCameraSequenceScriptedBuilder.hpp new file mode 100644 index 000000000..7bb3191df --- /dev/null +++ b/common/include/camera/CCameraSequenceScriptedBuilder.hpp @@ -0,0 +1,125 @@ +// 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_SEQUENCE_SCRIPTED_BUILDER_HPP_ +#define _C_CAMERA_SEQUENCE_SCRIPTED_BUILDER_HPP_ + +#include + +#include "CCameraScriptedRuntime.hpp" +#include "CCameraSequenceScript.hpp" +#include "ICamera.hpp" + +namespace nbl::hlsl +{ + +/** +* Build expanded scripted runtime data from a compiled camera-sequence segment. +* +* This keeps authored sequence semantics in shared camera helpers instead of re-encoding +* `Goal`, `TrackedTargetTransform`, `Baseline`, `GimbalStep`, and capture scheduling inside +* one example. +*/ +struct CCameraSequenceScriptedSegmentBuildInfo +{ + //! Planar that should receive the compiled segment. + uint32_t planarIx = 0u; + //! Number of windows the consumer can actually route presentation actions to. + size_t availableWindowCount = 1u; + //! Whether secondary window presentation actions should be emitted. + bool useWindow = false; + //! Whether per-frame follow-lock checks should be generated for this segment. + bool includeFollowTargetLock = false; +}; + +//! Append one compiled segment as expanded scripted runtime payloads. +inline bool appendCompiledSequenceSegmentToScriptedTimeline( + CCameraScriptedTimeline& timeline, + const uint64_t baseFrame, + const CCameraSequenceCompiledSegment& compiledSegment, + const CCameraSequenceScriptedSegmentBuildInfo& buildInfo, + std::string* error = nullptr) +{ + std::vector framePolicies; + if (!buildCompiledSegmentFramePolicies(compiledSegment, framePolicies, buildInfo.includeFollowTargetLock)) + { + if (error) + *error = "Failed to build compiled frame policies."; + return false; + } + + appendScriptedSegmentLabelEvent(timeline, baseFrame, compiledSegment.name); + appendScriptedActionEvent(timeline, baseFrame, CCameraScriptedInputEvent::ActionData::Kind::SetActiveRenderWindow, 0); + appendScriptedActionEvent(timeline, baseFrame, CCameraScriptedInputEvent::ActionData::Kind::SetActivePlanar, static_cast(buildInfo.planarIx)); + if (!compiledSegment.presentations.empty()) + { + appendScriptedActionEvent(timeline, baseFrame, CCameraScriptedInputEvent::ActionData::Kind::SetProjectionType, static_cast(compiledSegment.presentations[0].projection)); + appendScriptedActionEvent(timeline, baseFrame, CCameraScriptedInputEvent::ActionData::Kind::SetLeftHanded, compiledSegment.presentations[0].leftHanded ? 1 : 0); + } + if (compiledSegment.resetCamera) + appendScriptedActionEvent(timeline, baseFrame, CCameraScriptedInputEvent::ActionData::Kind::ResetActiveCamera, 1); + + if (buildInfo.useWindow) + { + for (size_t windowIx = 1u; windowIx < std::min(compiledSegment.presentations.size(), buildInfo.availableWindowCount); ++windowIx) + { + appendScriptedActionEvent(timeline, baseFrame, CCameraScriptedInputEvent::ActionData::Kind::SetActiveRenderWindow, static_cast(windowIx)); + appendScriptedActionEvent(timeline, baseFrame, CCameraScriptedInputEvent::ActionData::Kind::SetActivePlanar, static_cast(buildInfo.planarIx)); + appendScriptedActionEvent(timeline, baseFrame, CCameraScriptedInputEvent::ActionData::Kind::SetProjectionType, static_cast(compiledSegment.presentations[windowIx].projection)); + appendScriptedActionEvent(timeline, baseFrame, CCameraScriptedInputEvent::ActionData::Kind::SetLeftHanded, compiledSegment.presentations[windowIx].leftHanded ? 1 : 0); + } + appendScriptedActionEvent(timeline, baseFrame, CCameraScriptedInputEvent::ActionData::Kind::SetActiveRenderWindow, 0); + } + + for (const auto& policy : framePolicies) + { + CCameraPreset preset; + if (!tryBuildKeyframeTrackPresetAtTime(compiledSegment.track, policy.sampleTime, preset)) + { + if (error) + *error = "Failed to sample compiled segment track."; + return false; + } + appendScriptedGoalEvent(timeline, baseFrame + policy.frameOffset, makeGoalFromPreset(preset)); + + if (compiledSegment.usesTrackedTargetTrack()) + { + CCameraSequenceTrackedTargetPose trackedTargetPose; + if (!tryBuildSequenceTrackedTargetPoseAtTime(compiledSegment.trackedTargetTrack, policy.sampleTime, trackedTargetPose)) + { + if (error) + *error = "Failed to sample compiled tracked-target track."; + return false; + } + + ICamera::CGimbal gimbal({ .position = trackedTargetPose.position, .orientation = trackedTargetPose.orientation }); + appendScriptedTrackedTargetTransformEvent(timeline, baseFrame + policy.frameOffset, gimbal.operator()()); + } + + if (policy.baseline) + appendScriptedBaselineCheck(timeline, baseFrame + policy.frameOffset); + if (policy.continuityStep) + { + 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) + appendScriptedFollowTargetLockCheck(timeline, baseFrame + policy.frameOffset); + if (policy.capture) + timeline.captureFrames.emplace_back(baseFrame + policy.frameOffset); + } + + return true; +} + +} // namespace nbl::hlsl + +#endif diff --git a/common/include/camera/README.md b/common/include/camera/README.md index bc5c3be63..7b2b66462 100644 --- a/common/include/camera/README.md +++ b/common/include/camera/README.md @@ -464,6 +464,84 @@ Track normalization rules: That makes the same authored sequence usable by any future consumer that understands the shared camera API, even if its runtime expansion path differs from `61_UI`. +### `CCameraScriptedRuntime.hpp` + +Shared expanded scripted-runtime payloads for consumers that want a frame-by-frame execution path. + +Provides: + +- `CCameraScriptedInputEvent` +- `CCameraScriptedInputCheck` +- `CCameraScriptedTimeline` +- `CCameraScriptedFrameEvents` +- reusable scripted-timeline finalization helpers +- reusable event/check append helpers +- reusable per-frame event dequeue/bucketing + +This sits one layer below the compact authored sequence: + +- `CCameraSequenceScript` remains the authored source of truth +- `CCameraScriptedRuntime` is only the normalized expanded runtime contract +- consumers such as `61_UI` can adapt that contract to their own concrete event loop without redefining event/check structs locally + +### `CCameraScriptedRuntimePersistence.hpp` + +Reusable JSON parser and compatibility layer for low-level scripted runtime payloads. + +Provides: + +- `CCameraScriptedControlOverrides` +- `CCameraScriptedInputParseResult` +- shared parsing of legacy low-level runtime JSON: + - `events` + - `checks` + - `capture_frames` + - top-level scripted debug and control overrides +- compatibility parsing for legacy key names such as `KEY_KEY_W` +- optional handoff into `CCameraSequenceScript` when the same file contains compact `segments` + +This keeps old scripted-runtime assets reusable without leaving parser logic inside `61_UI`. +It also means other future consumers can accept the same low-level payloads or progressively migrate them +to compact camera-sequence scripts without changing the shared parsing contract. + +### `CCameraSequenceScriptedBuilder.hpp` + +Reusable authored-sequence to scripted-runtime builder helpers. + +Provides: + +- `CCameraSequenceScriptedSegmentBuildInfo` +- reusable conversion from one compiled sequence segment into: + - scripted `Action` events + - scripted `Goal` events + - scripted tracked-target transforms + - baseline/continuity/follow-lock checks + - capture frame milestones + +This sits between `CCameraSequenceScript.hpp` and `CCameraScriptedRuntime.hpp`: + +- `CCameraSequenceScript` owns authored compact segment semantics +- `CCameraSequenceScriptedBuilder` expands compiled segments into shared runtime payloads +- `61_UI` only resolves camera/planar targets and feeds those shared payloads into its local loop + +### `CCameraScriptedCheckRunner.hpp` + +Reusable scripted-check runtime state and per-frame evaluation helpers. + +Provides: + +- `CCameraScriptedCheckRuntimeState` +- `CCameraScriptedCheckContext` +- `CCameraScriptedCheckLogEntry` +- `CCameraScriptedCheckFrameResult` +- reusable baseline, near, delta, step, and follow-lock evaluation for one frame + +This sits above `CCameraScriptedRuntime.hpp`: + +- `CCameraScriptedRuntime` only normalizes authored expanded payloads +- `CCameraScriptedCheckRunner` evaluates those payloads against live camera state +- consumers can keep only their logging and runtime-object lookup glue locally + ### `CCameraPersistence.hpp` Reusable JSON and file persistence helpers for preset collections and keyframe tracks. From 1dd16f5b1ff4628b6309fb961aa4acd6c5c64162 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Tue, 7 Apr 2026 07:04:36 +0200 Subject: [PATCH 169/205] Rewrite camera API documentation --- 61_UI/README.md | 210 +++++---- common/include/camera/README.md | 795 ++++++++++++++++---------------- 2 files changed, 494 insertions(+), 511 deletions(-) diff --git a/61_UI/README.md b/61_UI/README.md index 17c1da4d0..32b49c8d7 100644 --- a/61_UI/README.md +++ b/61_UI/README.md @@ -1,31 +1,52 @@ # 61_UI Cameraz -This example demonstrates interactive camera control in the ImGui-based UI sample. -It contains a scripted-input harness that can drive camera actions in CI and validate behavior with frame-based checks. +`61_UI` is the current integration, UX, and validation harness for the shared camera stack in +[`../common/include/camera`](../common/include/camera/README.md). -## Shared camera API +If you want the architecture, design rationale, and reusable API breakdown, start there first. +This README focuses on what `61_UI` adds on top of the shared layer. -`61_UI` is the active integration and validation surface for the reusable camera stack in `../common/include/camera`. +## Role of this example -See: +`61_UI` is the only actively migrated consumer right now. -- [`../common/include/camera/README.md`](../common/include/camera/README.md) +It is used to: -That shared layer covers: +- exercise all current camera models in one scene +- validate the shared input, goal, preset, playback, follow, and scripting layers +- provide an interactive manual playground +- provide CI-oriented smoke and continuity coverage -- virtual gimbal events -- binding layouts and runtime input binders -- reusable camera kinds and typed state hooks -- best-effort goal capture and apply utilities -- preset and keyframe-track storage helpers -- tracked-target and follow helpers built on top of shared goals +The example is intentionally not the source of truth for camera semantics. +Its job is to consume the shared camera APIs and expose them through a visible, testable UI. -At the moment other examples are not being migrated yet. -The reusable API is growing in `common/include/camera`, while `61_UI` stays the only active call-site. +## What `61_UI` contributes locally -## Cameras in this scene +The reusable camera layer stops at shared camera-domain contracts. +`61_UI` adds the local glue needed to turn that into an example application: -`app_resources/cameras.json` defines 11 camera types: +- scene setup and demo geometry +- planar/window routing +- ImGui control panel and transform editor +- screenshot capture +- scripted visual-debug HUD +- local logging and failure reporting + +That means the shared camera layer owns: + +- camera semantics +- follow semantics +- compact sequence semantics +- scripted runtime payloads +- scripted check semantics + +and `61_UI` owns: + +- how those things are presented and driven in one concrete sample + +## Cameras in the scene + +`app_resources/cameras.json` configures the currently showcased camera set: - FPS - Orbit @@ -39,125 +60,115 @@ The reusable API is growing in `common/include/camera`, while `61_UI` stays the - DollyZoom - Path -Each planar uses one of the configured input binding layouts and can be switched at runtime by scripted `action` events. - -## Follow target integration - -`61_UI` also exposes one tracked target in the default scene. - -That target owns its own gimbal and is integrated through the shared follow layer rather than -through direct camera hacks. The example can: - -- manipulate the tracked target with the scene gizmo -- show a marker for the tracked target -- let selected cameras follow it through reusable follow modes +These are exposed through the active planar/view configuration in the example UI. -The current default follow setup is: +## Follow target in `61_UI` -- `Orbit`, `Arcball`, `Turntable`, `TopDown`, `Isometric`, `DollyZoom`, `Path` - use `OrbitTarget` -- `Chase`, `Dolly` - use `KeepLocalOffset` - -The tracked target is not the large cone or any particular scene model. -The tracked target is the reusable `CTrackedTarget` gimbal. -`61_UI` only renders a marker and optional reference geometry for that gimbal. +`61_UI` exposes one tracked target in the default scene. -Scripted continuity/CI runs now drive the same follow layer through authored `target_keyframes`. -That means live runtime and scripted validation both consume the same tracked-target semantics. +Important rule: -## Short math context +- the tracked target is the reusable `CTrackedTarget` gimbal +- it is not the large cone +- it is not any scene object id +- the rendered marker is only a visualization of that gimbal -Each camera is represented by a gimbal pose `(R, p)` and produces a view matrix from camera basis vectors and position. +This is important because the shared follow layer is intentionally modeled around: -For a world-space point `x_w`, clip-space projection is: +- tracked target pose +- follow mode +- follow config -`x_c = P * V * x_w` +and not around a mesh reference. -where: +### Default follow usage in the scene -- `V` is camera view transform -- `P` is selected planar projection (perspective or orthographic) +Current default setup: -The scripted smoothness checks use per-step deltas: +- `Orbit`, `Arcball`, `Turntable`, `TopDown`, `Isometric`, `DollyZoom`, `Path` + use `OrbitTarget` +- `Chase`, `Dolly` + use `KeepLocalOffset` -- position delta: `d_pos = ||p_t - p_{t-1}||` -- rotation delta: `d_rot = max(angleDiff(euler_t, euler_{t-1}))` +Manual runtime and scripted continuity both drive the same follow layer. -and validate them against configured `[min, max]` ranges. +## Scripted assets -## Scripted test assets +`61_UI` currently uses two camera-focused scripted assets: - `app_resources/cameraz_smoke_all.json` - `app_resources/cameraz_continuity.json` -### Smoke script +### Smoke + +Purpose: -Goal: verify that every camera can be selected and responds to scripted input. +- validate basic camera selection and movement +- validate helper contracts in a small, cheap run -Per camera sequence: +### Continuity -1. select planar -2. store `baseline` -3. apply one `imguizmo` movement step -4. run `gimbal_step` check +Purpose: -PASS means each camera produced a finite and expected movement delta. -FAIL means missing movement, out-of-range movement, invalid state, or missing reference. +- validate smooth frame-to-frame motion +- validate tracked-target follow lock during scripted target motion +- provide a readable visual-debug showcase -### Continuity script +The continuity asset is now a compact authored camera-sequence script. +It is no longer a giant committed frame dump. -Goal: verify smooth frame-to-frame behavior (no visible teleport-like jumps). +## Shared pieces consumed by `61_UI` -The continuity asset is now a compact authored camera-sequence spec, not a committed frame dump. +The example now consumes these shared scripting and follow pieces directly: -`61_UI` also consumes shared scripted runtime helpers: +- [`CCameraSequenceScript.hpp`](../common/include/camera/CCameraSequenceScript.hpp) +- [`CCameraScriptedRuntime.hpp`](../common/include/camera/CCameraScriptedRuntime.hpp) +- [`CCameraScriptedRuntimePersistence.hpp`](../common/include/camera/CCameraScriptedRuntimePersistence.hpp) +- [`CCameraSequenceScriptedBuilder.hpp`](../common/include/camera/CCameraSequenceScriptedBuilder.hpp) +- [`CCameraScriptedCheckRunner.hpp`](../common/include/camera/CCameraScriptedCheckRunner.hpp) +- [`CCameraFollowUtilities.hpp`](../common/include/camera/CCameraFollowUtilities.hpp) +- [`CCameraFollowRegressionUtilities.hpp`](../common/include/camera/CCameraFollowRegressionUtilities.hpp) -- `CCameraScriptedRuntime.hpp` for expanded per-frame payload types -- `CCameraScriptedRuntimePersistence.hpp` for low-level scripted JSON parsing and backward compatibility -- `CCameraSequenceScriptedBuilder.hpp` for compact-sequence to scripted-runtime expansion -- `CCameraScriptedCheckRunner.hpp` for baseline/step/follow-lock check evaluation +That means `61_UI` no longer owns a private scripting model or private follow math. -The example keeps only the runtime-object lookup and logging glue locally. -`61_UI` first compiles that shared camera-domain description into normalized sampled segments and shared frame-policy schedules, then feeds the shared scripted-runtime contract before adapting it to the local example loop. +## Manual usage -Per authored segment: +Typical manual workflow: -1. select planar -2. store `baseline` -3. build a reusable keyframe track from the active camera reference preset -4. optionally build a tracked-target track from the default tracked-target pose -5. sample the authored track(s) for `4.0 s` at `60 FPS` -6. run `gimbal_step` on each generated frame step -7. capture selected milestones such as `end` +1. Pick a camera/planar in the UI. +2. Manipulate the camera through mouse, keyboard, or ImGuizmo-backed controls. +3. Use presets and playback tools if needed. +4. Move the tracked target marker. +5. Observe how follow-enabled cameras react. -PASS means every step delta stayed inside configured continuity ranges. -FAIL means any step exceeded max range or failed minimum expected motion. +## CI and validation -Continuity also supports visual debug mode: +`CMakeLists.txt` registers two dedicated tests: -- large top-center overlay with active camera type and segment progress -- fixed frame pacing (`visual_debug_target_fps`) so camera time is human-readable -- compact authored JSON that stays in camera-domain and is reusable outside `61_UI` +- `NBL_61_UI_CAMERA_SMOKE` +- `NBL_61_UI_CAMERA_CONTINUITY` + +Run from `build_vs2026/examples_tests/61_UI`: -For follow-enabled cameras, continuity can now also author `target_keyframes`. -That drives the shared tracked target through the reusable follow layer instead of hardcoding camera hacks in `61_UI`. +```powershell +ctest -C Debug --output-on-failure -R NBL_61_UI_CAMERA_ +``` ## Build and run -Build this example first: +Build: ```powershell cmake --build build_vs2026/examples_tests/61_UI --config Debug --target 61_ui -- /m:1 ``` -Run manually from executable directory: +Run manual smoke-style playback: ```powershell ./61_ui_d.exe --script app_resources/cameraz_smoke_all.json --script-log ``` -For CI-style exit with automatic screenshot/capture behavior: +Run continuity in CI-style mode: ```powershell ./61_ui_d.exe --ci --script app_resources/cameraz_continuity.json --script-log --script-visual-debug @@ -165,19 +176,6 @@ For CI-style exit with automatic screenshot/capture behavior: Notes: -- continuity visual run takes about `47 s` -- the authored continuity JSON is compact and segment-based rather than frame-by-frame -- if `visual_debug` is present in json, CLI flag is optional - -## CTest entries - -`CMakeLists.txt` registers two dedicated tests: - -- `NBL_61_UI_CAMERA_SMOKE` -- `NBL_61_UI_CAMERA_CONTINUITY` - -Run from `build_vs2026/examples_tests/61_UI`: - -```powershell -ctest -C Debug --output-on-failure -R NBL_61_UI_CAMERA_ -``` +- continuity visual run is about `47 s` +- `visual_debug` can also be authored in JSON +- the compact continuity asset stays camera-domain and reusable instead of storing example-specific frame dumps diff --git a/common/include/camera/README.md b/common/include/camera/README.md index 7b2b66462..947999b65 100644 --- a/common/include/camera/README.md +++ b/common/include/camera/README.md @@ -1,600 +1,585 @@ # Shared Camera API -This directory contains the reusable camera stack currently used by `61_UI`. -It lives in `examples_tests/common` on purpose: the API is shared across examples, but it is not engine-core API yet. +This directory contains the reusable camera stack currently validated by [`61_UI`](../../../61_UI/README.md). -The current design goal is: +It intentionally lives in `examples_tests/common` instead of engine core: -- camera core consumes virtual events only -- raw input binding stays outside the camera -- absolute goals and preset restore are best-effort helpers layered on top -- reusable math, preset, analysis, and keyframe-track utilities live in shared headers -- reusable preset capture and apply flow helpers live in shared headers -- reusable playback cursor and timeline helpers live in shared headers -- reusable preset and keyframe persistence helpers live in shared headers -- reusable compact camera-sequence scripting helpers live in shared headers -- reusable tracked-target and follow helpers live in shared headers -- `61_UI` is the current integration and validation surface +- the API is meant to be reusable across examples and tools +- the API is still being shaped through one active integration surface +- the current goal is to stabilize the design before promoting any part of it deeper into Nabla -## Mental model +At the moment the only migrated consumer is `61_UI`. +That is intentional. +The stack is designed to be reusable by other examples, offline tools, and future renderers, but they are not being migrated yet. -The stack is split into 5 layers: +## What this stack is -1. `CVirtualGimbalEvent` - A semantic camera command such as `MoveForward`, `PanLeft`, or `RollRight`. -2. Binding layout - Static mapping from keyboard, mouse, or ImGuizmo inputs to virtual events. -3. Input processor / binder - Runtime translation from external events into virtual events. -4. Camera model - A concrete camera type that consumes virtual events and updates its pose. -5. Goal / preset / track utilities - Best-effort capture, compatibility analysis, restore, blending, and playback helpers. +This stack is a reusable camera framework with two clearly separated halves: -The intended flow is: +1. Runtime camera control + The hot path that reacts to input and updates camera pose. +2. Tooling and validation + The sidecar layer used for capture, restore, presets, tracks, playback, scripted validation, and follow. -`raw input -> binding layout -> input binder -> virtual events -> ICamera::manipulate(...)` +The runtime half is intentionally event-driven. +The tooling half is intentionally state-driven. -The best-effort absolute layer sits beside that flow: +That split is the core design decision. -`camera state <-> CCameraGoal <-> CCameraPreset <-> CCameraKeyframeTrack` +## What this stack is not -and the playback cursor sits beside that state: +This stack is not: -`CCameraPlaybackCursor <-> CCameraKeyframeTrack` +- engine-core Nabla camera API +- a promise that all examples already use it +- a generic scene animation system +- a direct scene-object-follow system +- a setter-heavy camera API with arbitrary absolute pose mutation in the hot runtime path -and compact authored sequence scripts sit above the same shared camera-domain state: +## Design goals -`CCameraSequenceScript -> CCameraKeyframeTrack -> CCameraPreset -> CCameraGoal` +The design goals are: -and tracked-target follow sits beside the same goal layer: +- one semantic command language for keyboard, mouse, gizmo, scripts, and CI +- no input-device assumptions inside camera models +- no viewport glue inside camera models +- no direct dependence on `61_UI` concepts in the reusable camera layer +- best-effort absolute restore for tooling without turning cameras into mutable state bags +- reusable persistence, analysis, playback, follow, and validation helpers -`CTrackedTarget + SCameraFollowConfig -> CCameraGoal -> CCameraGoalSolver` +## Why virtual events and not absolute setters -and sequence-authored tracked-target motion can feed that same follow layer: +The runtime contract is intentionally built around virtual events such as: -`CCameraSequenceScript.target_keyframes -> CCameraSequenceTrackedTargetTrack -> CTrackedTarget` +- `MoveForward` +- `PanLeft` +- `TiltUp` +- `RollRight` -## Core contracts +instead of runtime methods like: -### `IGimbal.hpp` +- `setPosition(...)` +- `setTarget(...)` +- `setYawPitchRoll(...)` -Defines `CVirtualGimbalEvent` and the low-level gimbal math. +The reason is architectural, not cosmetic. -Important points: +### What the event-driven contract buys us -- `CVirtualGimbalEvent` is the shared semantic command language. -- Translation, rotation, and scale commands are represented explicitly. -- `IGimbal::accumulate(...)` converts a stream of virtual events into a single impulse. +It gives us one shared runtime path for: -This is the base abstraction that makes scripted tests, headless CI, and manual input share one command path. +- live keyboard and mouse input +- ImGuizmo manipulation +- scripted playback +- CI validation +- future headless or tool-driven input sources -### `IGimbalBindingLayout.hpp` +It also keeps camera semantics inside the camera type: -Defines static binding-layout storage and mutation. +- `Orbit` means orbit +- `FPS` means FPS +- `Path` means path-driven camera -Use it when you need to describe: +instead of allowing every caller to overwrite camera internals arbitrarily. -- which keyboard keys trigger which virtual events -- which mouse channels trigger which virtual events -- which ImGuizmo transforms map to which virtual events +### Why the absolute layer still exists -This header does not process input by itself. It only stores mapping layout. +Tooling still needs absolute-ish operations: -### `IGimbalController.hpp` +- capture current state +- restore preset +- scrub playback +- compare two camera states +- run validation against expected state -The file name is legacy. The main type is `IGimbalInputProcessor`. +That is why the absolute layer exists, but it is kept outside `ICamera`. -Use it when you need runtime input processing: +The intended pattern is: -- keyboard event streams -- mouse event streams -- ImGuizmo delta transforms +`camera <-> goal/preset/track/solver` -`IGimbalInputProcessor` owns active runtime mappings and emits virtual events for the current frame. +not: -### `CGimbalInputBinder.hpp` +`camera exposes public setters for everything` -High-level runtime binder built on top of `IGimbalInputProcessor`. +This is why the design uses: -This is the easiest entry point for examples: +- [`CCameraGoal.hpp`](CCameraGoal.hpp) +- [`CCameraGoalSolver.hpp`](CCameraGoalSolver.hpp) +- [`CCameraPreset.hpp`](CCameraPreset.hpp) +- [`CCameraKeyframeTrack.hpp`](CCameraKeyframeTrack.hpp) -- copy active bindings from a layout -- copy preset/default bindings from a layout -- collect virtual events for one frame +instead of making `ICamera` setter-heavy. -Use `CGimbalInputBinder` in viewport or example glue. -Do not embed external input processing inside camera types. +## High-level architecture -### `ICamera.hpp` +There are two main paths. -Main camera contract. +### Runtime path -Important rules: +```text +raw input + -> binding layout + -> input processor / binder + -> virtual gimbal events + -> ICamera::manipulate(...) + -> updated camera gimbal / view +``` -- `ICamera::manipulate(...)` is the core runtime entry point -- camera core consumes virtual events only -- `ICamera` exposes inspection and capability queries -- camera core may expose typed optional state through `tryGet...State` / `trySet...State` +### Tooling path -The typed state hooks are intentionally optional: +```text +ICamera + <-> CCameraGoal + <-> CCameraPreset + <-> CCameraKeyframeTrack + <-> CCameraPlaybackCursor + <-> CCameraSequenceScript +``` -- `SphericalTargetState` -- `DynamicPerspectiveState` -- `PathState` +### Follow path -This allows best-effort goal solving without turning the core runtime contract into a setter-heavy API. +```text +CTrackedTarget + SCameraFollowConfig + -> CCameraGoal + -> CCameraGoalSolver + -> camera state +``` -`ICamera` also exposes: +### Scripted sequence path -- `CameraKind` -- `CameraCapability` -- `GoalStateMask` -- motion config -- default input binding config +```text +CCameraSequenceScript + -> compiled sequence segment + -> scripted runtime timeline + -> scripted check runner + -> runtime logging / CI / screenshots +``` -## Camera families +## Stack breakdown -### Free cameras +### 1. Gimbal and semantic commands -- `CFPSCamera.hpp` -- `CFreeLockCamera.hpp` +- [`IGimbal.hpp`](IGimbal.hpp) -These are pose-driven cameras without spherical target semantics. -Best-effort absolute apply may fall back to direct reference-pose restoration when event replay alone is insufficient. +This is the mathematical foundation. -### Spherical-target family +It defines: -- `CSphericalTargetCamera.hpp` -- `COrbitCamera.hpp` -- `CArcballCamera.hpp` -- `CTurntableCamera.hpp` -- `CTopDownCamera.hpp` -- `CIsometricCamera.hpp` -- `CChaseCamera.hpp` -- `CDollyCamera.hpp` +- `CVirtualGimbalEvent` +- low-level gimbal math +- accumulation of multiple semantic commands into one camera impulse -These cameras share: - -- target position -- distance -- orbit angles `u/v` - -`CSphericalTargetCamera` is the common reusable base for that family. - -Current contract: - -- `Orbit`, `Arcball`, `Turntable`, `TopDown`, `Isometric`, `Chase`, and `Dolly` all participate in the shared spherical goal flow -- `Chase` and `Dolly` currently do not carry extra typed state beyond shared spherical state +This is the shared command language used by all camera types and all runtime input sources. -### Extended state cameras +### 2. Binding layout -- `CDollyZoomCamera.hpp` -- `CPathCamera.hpp` +- [`IGimbalBindingLayout.hpp`](IGimbalBindingLayout.hpp) -These extend the shared spherical model with extra typed state: +This layer stores static mappings such as: -- `CDollyZoomCamera` uses `DynamicPerspectiveState` -- `CPathCamera` uses `PathState` +- keyboard key -> virtual event +- mouse input -> virtual event +- ImGuizmo delta -> virtual event -If a camera needs extra state that cannot be faithfully round-tripped through pose plus spherical target data, this is the pattern to follow. +This layer does not process runtime input. +It only stores how input should map to the semantic command language. -## Projection layer +### 3. Runtime input processing -### `IProjection.hpp`, `ILinearProjection.hpp` +- [`IGimbalController.hpp`](IGimbalController.hpp) +- [`CGimbalInputBinder.hpp`](CGimbalInputBinder.hpp) -Base projection contracts and linear-projection math. +The filename `IGimbalController.hpp` is legacy. +The main runtime type is `IGimbalInputProcessor`. -### `IPlanarProjection.hpp` +This layer: -Planar camera projection wrapper used by `61_UI`. +- receives actual keyboard and mouse streams +- receives ImGuizmo transforms +- emits virtual events for the current frame -`IPlanarProjection::CProjection` carries: +`CGimbalInputBinder` is the convenience runtime binder that examples should usually use. -- perspective or orthographic parameters -- projection matrix update logic -- its own input binding storage for viewport-local bindings +### 4. Camera core -Important design point: +- [`ICamera.hpp`](ICamera.hpp) -- projection owns binding layout storage -- projection does not process raw input by itself -- runtime input processing should happen through `CGimbalInputBinder` +This is the core contract that camera models implement. -### `CPlanarProjection.hpp`, `CLinearProjection.hpp`, `CCubeProjection.hpp` +Important properties: -Concrete projection helpers on top of the above contracts. +- runtime entry point is `manipulate(...)` +- the runtime contract consumes virtual events only +- cameras own their own gimbal and view state +- cameras expose typed optional state hooks only for tooling -## Goal, preset, and playback utilities - -### `CCameraGoal.hpp` - -Typed camera-state transport object plus reusable math helpers. - -Use it for: +`ICamera` also exposes: -- canonicalizing captured state -- checking whether a goal is finite -- comparing actual state to expected state -- blending two camera states for playback -- describing mismatches -- determining required typed goal state +- `CameraKind` +- `CameraCapability` +- `GoalStateMask` +- motion config +- default input binding config -`CCameraGoal` is the shared language between capture, analysis, preset persistence, and playback interpolation. +### 5. Projection layer -### `CCameraGoalSolver.hpp` +- [`ILinearProjection.hpp`](ILinearProjection.hpp) +- [`IPlanarProjection.hpp`](IPlanarProjection.hpp) +- [`CPlanarProjection.hpp`](CPlanarProjection.hpp) +- [`CLinearProjection.hpp`](CLinearProjection.hpp) +- [`CCubeProjection.hpp`](CCubeProjection.hpp) -Best-effort absolute layer for cameras. +This layer handles projection state. -Use it for: +Important rule: -- capture of typed camera state into `CCameraGoal` -- compatibility analysis against a target camera -- best-effort apply of a goal to a camera -- reconstruction of virtual events for replay-driven application +- projection may own binding layout storage +- projection does not own raw input processing -Important result types: +That separation was one of the major cleanup goals of the refactor. -- `SCaptureResult` -- `SCompatibilityResult` -- `SApplyResult` +### 6. Goal / preset / track tooling -`SApplyResult` explicitly distinguishes: +- [`CCameraGoal.hpp`](CCameraGoal.hpp) +- [`CCameraGoalSolver.hpp`](CCameraGoalSolver.hpp) +- [`CCameraGoalAnalysis.hpp`](CCameraGoalAnalysis.hpp) +- [`CCameraPreset.hpp`](CCameraPreset.hpp) +- [`CCameraPresetFlow.hpp`](CCameraPresetFlow.hpp) +- [`CCameraKeyframeTrack.hpp`](CCameraKeyframeTrack.hpp) +- [`CCameraPlaybackTimeline.hpp`](CCameraPlaybackTimeline.hpp) +- [`CCameraPersistence.hpp`](CCameraPersistence.hpp) -- unsupported -- failed -- already satisfied -- applied by absolute fallback -- applied by virtual events -- mixed absolute + virtual event application +This is the tooling half of the stack. -This is the main contract behind preset restore and cross-camera best-effort behavior. +It covers: -### `CCameraGoalAnalysis.hpp` +- state capture +- compatibility analysis +- best-effort restore +- preset storage +- keyframe playback +- persistence +- diagnostics and presentation helpers -Thin reusable analysis layer built on top of `CCameraGoalSolver`. +### 7. Follow -Use it when UI or higher-level tools need typed answers for: +- [`CCameraFollowUtilities.hpp`](CCameraFollowUtilities.hpp) +- [`CCameraFollowRegressionUtilities.hpp`](CCameraFollowRegressionUtilities.hpp) -- can this camera state be captured -- can this preset/goal be applied -- is the apply exact or best-effort -- does the target camera drop some goal state -- is the result only using shared state across different camera kinds +Follow is deliberately not part of `ICamera`. -This keeps policy analysis out of example-local UI code. +The tracked subject owns its own gimbal through `CTrackedTarget`. +Follow stays as a policy layer above the camera. -### `CCameraTextUtilities.hpp` +That means the camera API does not know about meshes, scene nodes, or `61_UI`. +It only knows about: -Reusable human-readable metadata and diagnostic text helpers for cameras. +- camera +- tracked target gimbal +- follow config +- best-effort goal application -### `CCameraFollowUtilities.hpp` +### 8. Scripted sequence and validation -Reusable tracked-target and follow helpers layered on top of the shared camera API. +- [`CCameraSequenceScript.hpp`](CCameraSequenceScript.hpp) +- [`CCameraScriptedRuntime.hpp`](CCameraScriptedRuntime.hpp) +- [`CCameraScriptedRuntimePersistence.hpp`](CCameraScriptedRuntimePersistence.hpp) +- [`CCameraSequenceScriptedBuilder.hpp`](CCameraSequenceScriptedBuilder.hpp) +- [`CCameraScriptedCheckRunner.hpp`](CCameraScriptedCheckRunner.hpp) -Important design points: +This is the reusable scripting and CI half. -- follow stays outside `ICamera` -- the tracked subject owns its own gimbal through `CTrackedTarget` -- follow modes map target motion into `CCameraGoal` -- goal application still goes through `CCameraGoalSolver` -- follow semantics are defined against the tracked-target gimbal, not against any scene model or mesh +It supports two levels of representation: -Current follow modes: +1. Compact authored camera-domain script +2. Expanded frame-by-frame scripted runtime payload -- `LookAtTarget` -- `OrbitTarget` -- `KeepWorldOffset` -- `KeepLocalOffset` +That separation is important. +Authored assets stay short and meaningful. +Expanded runtime payloads stay normalized and reusable. -Current follow invariants: +## Camera families -- all enabled follow modes lock the final camera view onto the tracked-target position -- `LookAtTarget` keeps the camera world position and only rotates it toward the target -- `OrbitTarget` keeps the camera on a target-relative spherical/path rig and recenters the tracked target -- `KeepWorldOffset` keeps a world-space offset from the tracked target and recenters it -- `KeepLocalOffset` keeps an offset in the tracked-target local frame and recenters it -- the tracked target remains the source of truth; the camera does not own the subject +### Free cameras -This is why the regression layer validates: +- [`CFPSCamera.hpp`](CFPSCamera.hpp) +- [`CFreeLockCamera.hpp`](CFreeLockCamera.hpp) -- camera forward axis vs. camera-to-target direction -- projected target center error in NDC -- spherical target writeback for spherical cameras -- target distance consistency after apply +These are pose-driven cameras without spherical target semantics. -This keeps the camera runtime contract event-driven while still allowing higher-level -tracking behavior to be reused by tools and examples. +### Spherical-target family -Provides: +- [`CSphericalTargetCamera.hpp`](CSphericalTargetCamera.hpp) +- [`COrbitCamera.hpp`](COrbitCamera.hpp) +- [`CArcballCamera.hpp`](CArcballCamera.hpp) +- [`CTurntableCamera.hpp`](CTurntableCamera.hpp) +- [`CTopDownCamera.hpp`](CTopDownCamera.hpp) +- [`CIsometricCamera.hpp`](CIsometricCamera.hpp) +- [`CChaseCamera.hpp`](CChaseCamera.hpp) +- [`CDollyCamera.hpp`](CDollyCamera.hpp) -- camera-kind labels -- camera-kind descriptions -- goal-state mask descriptions -- detailed goal-apply result descriptions -- analyzed goal-apply compatibility and policy descriptions -- analyzed camera-capture policy descriptions -- aggregate preset-apply summary descriptions +These cameras share: -This keeps camera-specific presentation and diagnostic text reusable without leaving it in example-local glue. +- target position +- distance +- orbit angles -### `CCameraPresentationUtilities.hpp` +They participate in the shared spherical goal flow. -Reusable presentation-oriented wrappers built on top of shared camera analysis and text helpers. +### Extended-state cameras -Provides: +- [`CDollyZoomCamera.hpp`](CDollyZoomCamera.hpp) +- [`CPathCamera.hpp`](CPathCamera.hpp) -- exact-vs-best-effort preset presentation filtering -- reusable labels for presentation filters -- presentation-ready apply-analysis structs -- presentation-ready capture-analysis structs -- reusable badge flags for apply/result presentation -- presentation-ready source-kind and goal-state labels -- ready-to-render compatibility and policy labels +These extend the shared base with typed extra state: -This keeps higher-level preset and capture presentation flow reusable without leaving it in example-local glue. +- `DynamicPerspectiveState` +- `PathState` -### `CCameraPreset.hpp` +## Capabilities and typed state -Reusable preset and keyframe state plus JSON IO. +The core camera contract exposes: -Provides: +- `CameraCapability` +- `GoalStateMask` -- `CCameraPreset` -- `CCameraKeyframe` -- preset comparison helpers -- preset collection comparison helpers -- goal-to-preset conversion helpers -- preset JSON serialization and deserialization +The currently relevant typed state is: -This is the storage format used by `61_UI` for preset authoring and playback persistence. +- `SphericalTargetState` +- `DynamicPerspectiveState` +- `PathState` -### `CCameraPresetFlow.hpp` +The rule is: -Reusable preset capture, comparison, mismatch, and best-effort apply helpers. +- if a camera can round-trip through shared spherical state, do not add fake extra state +- if a camera has real additional semantics that would be lost, add typed state explicitly -Provides: +That is why: -- preset capture from a camera and solver -- preset apply through the shared goal solver -- preset apply summaries across camera ranges -- preset-to-camera comparison helpers -- preset mismatch descriptions for diagnostics +- `Chase` and `Dolly` currently stay on shared spherical state +- `DollyZoom` has dynamic perspective state +- `Path` has path state -This keeps solver-backed preset flow reusable without leaving the flow rules in example-local glue. +## Follow model -### `CCameraManipulationUtilities.hpp` +Follow is modeled around a tracked target gimbal, not around a scene object id. -Reusable manipulation helpers that sit between raw collected virtual events and final camera state. +### Source of truth -Provides: +The source of truth is: -- `SCameraConstraintSettings` -- virtual-event translation and rotation scaling -- world-translation remapping into local camera movement -- post-manipulation constraint clamping through the shared goal solver +- `CTrackedTarget` -This keeps example runtime manipulation policy reusable without leaving event-scaling and constraint logic in example-local glue. +which literally owns a gimbal. -### `CCameraProjectionUtilities.hpp` +### Follow modes -Reusable helpers that synchronize camera-driven projection state with planar projections. +Current modes: -Provides: +- `LookAtTarget` +- `OrbitTarget` +- `KeepWorldOffset` +- `KeepLocalOffset` -- dynamic perspective FOV sync from camera state into `IPlanarProjection::CProjection` +### Follow invariants -This keeps camera-specific projection updates reusable without leaving them in example-local glue. +For enabled modes, the camera must stay logically locked to the tracked target: -### `CCameraKeyframeTrack.hpp` +- camera-to-target direction must match the expected view direction +- projected target center error must stay small when projection is available +- spherical cameras must write target state back consistently +- camera-target distance must remain internally consistent -Reusable keyframe-track helpers on top of presets. +Those invariants are reusable and validated through: -Provides: +- [`CCameraFollowRegressionUtilities.hpp`](CCameraFollowRegressionUtilities.hpp) -- `CCameraKeyframeTrack` -- keyframe and track comparison helpers with optional selection-state checks -- preset-at-time evaluation -- keyframe sorting -- playback-time clamping -- nearest-keyframe selection -- selected-keyframe access -- selected-keyframe preset replacement -- track JSON serialization and deserialization +## Compact sequence design -This keeps playback-authoring logic reusable without forcing examples to reimplement keyframe math and storage flow. +The compact sequence format is deliberately camera-domain. -### `CCameraPlaybackTimeline.hpp` +It describes: -Reusable playback cursor and timeline helpers on top of keyframe tracks. +- camera kind or identifier +- projection presentation requests +- goal keyframes +- tracked-target keyframes +- continuity thresholds +- capture fractions -Provides: +It deliberately does not describe: -- `CCameraPlaybackCursor` -- `SCameraPlaybackAdvanceResult` -- track duration helpers -- cursor reset and clamping -- per-frame time advance with loop and stop semantics +- `61_UI` window actions as authored source data +- frame-by-frame event dumps +- ImGuizmo matrices as authored motion primitives -This keeps playback-time progression reusable without forcing examples to reimplement cursor stepping rules. +This is why the new continuity asset became small and maintainable instead of being a giant generated dump. -### `CCameraSequenceScript.hpp` +## Scripted runtime design -Reusable compact authored camera-sequence format for playback and validation. +The expanded scripted runtime exists so that a consumer can execute frame-by-frame logic without redefining runtime types locally. -Provides: +It is split into: -- `CCameraSequencePresentation` -- `CCameraSequenceContinuitySettings` -- `CCameraSequenceGoalDelta` -- `CCameraSequenceKeyframe` -- `CCameraSequenceSegment` -- `CCameraSequenceScript` -- `CCameraSequenceCompiledSegment` -- `CCameraSequenceCompiledFramePolicy` -- parsing and normalization helpers -- reusable sequence-to-track construction from captured reference presets -- reusable segment compilation from authored data into sampled times, capture offsets, and normalized tracks -- reusable frame-policy scheduling for baseline, continuity-step, follow-lock, and capture milestones +- authored parsing and normalization +- timeline finalization +- segment-to-runtime expansion +- per-frame dequeue +- per-frame check evaluation -The important design rule is that the authored format stays camera-domain: +This keeps `61_UI` from owning a private scripting subsystem. -- segment and keyframe based -- keyed by camera kind or identifier -- keyed by projection presentation requests -- keyed by compact continuity thresholds and capture fractions +## Current validation story -and deliberately does not store: +The main active validation surface is: -- expanded per-frame event dumps -- `61_UI`-specific window-routing commands as authored source data -- ImGuizmo matrices as the authored motion primitive +- [`61_UI`](../../../61_UI/README.md) -Tracked-target motion follows the same rule: +The current camera-focused tests are: -- authored `target_keyframes` describe only tracked-target pose over time -- they do not refer to a scene object id, mesh, or `61_UI` model -- consumers may map those poses to their own runtime objects, but the authored source of truth stays a tracked-target gimbal track +- `NBL_61_UI_CAMERA_SMOKE` +- `NBL_61_UI_CAMERA_CONTINUITY` -Track normalization rules: +### Smoke -- negative keyframe times clamp to `0` -- tracked-target keyframes are sorted by time before sampling -- duplicate tracked-target keyframe times collapse to the last authored pose +Purpose: -That makes the same authored sequence usable by any future consumer that understands the shared camera API, even if its runtime expansion path differs from `61_UI`. +- prove that camera selection and basic scripted manipulation still work +- validate preset, sequence, runtime, and follow helper contracts with small regression checks -### `CCameraScriptedRuntime.hpp` +### Continuity -Shared expanded scripted-runtime payloads for consumers that want a frame-by-frame execution path. +Purpose: -Provides: +- prove that camera motion remains smooth frame-to-frame +- prove that follow target lock remains valid during scripted target motion -- `CCameraScriptedInputEvent` -- `CCameraScriptedInputCheck` -- `CCameraScriptedTimeline` -- `CCameraScriptedFrameEvents` -- reusable scripted-timeline finalization helpers -- reusable event/check append helpers -- reusable per-frame event dequeue/bucketing +This test now runs on the compact authored sequence format rather than a large expanded frame dump. -This sits one layer below the compact authored sequence: +## Current consumer -- `CCameraSequenceScript` remains the authored source of truth -- `CCameraScriptedRuntime` is only the normalized expanded runtime contract -- consumers such as `61_UI` can adapt that contract to their own concrete event loop without redefining event/check structs locally +Current active consumer: -### `CCameraScriptedRuntimePersistence.hpp` +- [`61_UI`](../../../61_UI/README.md) -Reusable JSON parser and compatibility layer for low-level scripted runtime payloads. +Other examples are intentionally not migrated yet. +The idea is to stabilize one shared stack first, then reuse it elsewhere. -Provides: +## Recommended integration patterns -- `CCameraScriptedControlOverrides` -- `CCameraScriptedInputParseResult` -- shared parsing of legacy low-level runtime JSON: - - `events` - - `checks` - - `capture_frames` - - top-level scripted debug and control overrides -- compatibility parsing for legacy key names such as `KEY_KEY_W` -- optional handoff into `CCameraSequenceScript` when the same file contains compact `segments` +### Minimal runtime integration -This keeps old scripted-runtime assets reusable without leaving parser logic inside `61_UI`. -It also means other future consumers can accept the same low-level payloads or progressively migrate them -to compact camera-sequence scripts without changing the shared parsing contract. +```cpp +auto camera = core::make_smart_refctd_ptr(eye, target); -### `CCameraSequenceScriptedBuilder.hpp` +CGimbalInputBinder binder; +camera->copyDefaultInputBindingPresetTo(binder); -Reusable authored-sequence to scripted-runtime builder helpers. +auto collected = binder.collectVirtualEvents(timestamp, { + .mouseEvents = { mouseEvents.data(), mouseEvents.size() }, + .keyboardEvents = { keyEvents.data(), keyEvents.size() } +}); -Provides: +camera->manipulate(collected.events); +``` -- `CCameraSequenceScriptedSegmentBuildInfo` -- reusable conversion from one compiled sequence segment into: - - scripted `Action` events - - scripted `Goal` events - - scripted tracked-target transforms - - baseline/continuity/follow-lock checks - - capture frame milestones +### Preset / tooling integration -This sits between `CCameraSequenceScript.hpp` and `CCameraScriptedRuntime.hpp`: +```cpp +CCameraGoalSolver solver; -- `CCameraSequenceScript` owns authored compact segment semantics -- `CCameraSequenceScriptedBuilder` expands compiled segments into shared runtime payloads -- `61_UI` only resolves camera/planar targets and feeds those shared payloads into its local loop +auto capture = solver.captureDetailed(camera.get()); +if (capture.canUseGoal()) +{ + CCameraPreset preset; + assignGoalToPreset(preset, capture.goal); -### `CCameraScriptedCheckRunner.hpp` + auto apply = applyPresetDetailed(solver, camera.get(), preset); + if (!apply.succeeded()) + { + // report exact vs best-effort or unsupported state + } +} +``` -Reusable scripted-check runtime state and per-frame evaluation helpers. +### Follow integration -Provides: +```cpp +CTrackedTarget trackedTarget(position, orientation); -- `CCameraScriptedCheckRuntimeState` -- `CCameraScriptedCheckContext` -- `CCameraScriptedCheckLogEntry` -- `CCameraScriptedCheckFrameResult` -- reusable baseline, near, delta, step, and follow-lock evaluation for one frame +SCameraFollowConfig follow = {}; +follow.enabled = true; +follow.mode = ECameraFollowMode::KeepLocalOffset; +follow.localOffset = float64_t3(-4.0, 0.0, 1.0); -This sits above `CCameraScriptedRuntime.hpp`: +CCameraGoalSolver solver; +auto result = applyFollowToCamera(solver, camera.get(), trackedTarget, follow); +``` -- `CCameraScriptedRuntime` only normalizes authored expanded payloads -- `CCameraScriptedCheckRunner` evaluates those payloads against live camera state -- consumers can keep only their logging and runtime-object lookup glue locally +### Sequence / CI integration -### `CCameraPersistence.hpp` +```cpp +CCameraSequenceScript script = ...; +CCameraSequenceCompiledSegment segment = ...; -Reusable JSON and file persistence helpers for preset collections and keyframe tracks. +CCameraScriptedTimeline timeline; +appendCompiledSequenceSegmentToScriptedTimeline( + timeline, + baseFrame, + segment, + buildInfo); -Provides: +finalizeScriptedTimeline(timeline); -- preset collection JSON serialization and deserialization -- preset collection file save/load helpers -- keyframe-track file save/load helpers +CCameraScriptedCheckRuntimeState state = {}; +auto frameResult = evaluateScriptedChecksForFrame( + timeline.checks, + state, + context); +``` -This keeps example-level save/load glue out of `61_UI` while reusing the same preset and track formats. +## Why this split matters -## Current integration status +The design deliberately keeps these concerns separate: -The shared headers above are designed to be reusable by additional examples. -Right now the active migrated call-site is only: +- input binding +- camera semantics +- absolute/tooling state +- follow policy +- scripted playback and validation -- `61_UI` +That separation is what keeps the stack reusable. -That is intentional. -The current work is focused on stabilizing the reusable API surface first, then using `61_UI` as the validation and UX harness. +If any one of those concerns leaks into the others: -## Recommended integration pattern for a new example +- cameras become setter-heavy +- projections become input processors +- examples own private copies of state math +- scripts become example-specific +- follow becomes scene-object-specific -If another example wants to adopt this stack later, the intended path is: +The current refactor was mostly about removing exactly those leaks. -1. Instantiate a concrete `ICamera`. -2. Store default binding layouts on the camera and/or planar projection. -3. Use `CGimbalInputBinder` at runtime to translate external input into virtual events. -4. Feed the resulting event stream into `ICamera::manipulate(...)`. -5. Use `CCameraGoalSolver`, `CCameraGoalAnalysis`, `CCameraPreset`, `CCameraPresetFlow`, `CCameraKeyframeTrack`, `CCameraPlaybackTimeline`, and `CCameraPersistence` only for tooling features such as: - - preset capture - - preset restore - - compatibility preview - - preset comparison and mismatch diagnostics - - playback interpolation - - playback cursor stepping - - preset and keyframe persistence - - scripted validation +## Legacy and compatibility notes -That keeps the hot runtime path event-driven while still allowing higher-level tools to work with absolute camera goals in a controlled way. +- [`CTargetPoseController.hpp`](CTargetPoseController.hpp) is a compatibility include for [`CCameraGoalSolver.hpp`](CCameraGoalSolver.hpp) +- [`IGimbalController.hpp`](IGimbalController.hpp) keeps the historical filename, but the active runtime-processing type is `IGimbalInputProcessor` +- low-level scripted JSON still accepts legacy key names such as `KEY_KEY_W` -## Legacy compatibility notes +## Practical status -- `CTargetPoseController.hpp` is currently only a compatibility include for `CCameraGoalSolver.hpp`. -- `IGimbalController.hpp` still has the old file name, but the main runtime-processing type is `IGimbalInputProcessor`. +Today the stack is in a good place for: -## Non-goals +- reuse by additional examples +- reuse by camera-heavy tools +- reuse by offline render or validation flows -This layer is not yet: +without assuming: -- engine-core Nabla camera API -- a promise that every example already uses the stack -- a fully generic animation system beyond camera preset and keyframe utilities +- `61_UI` +- a specific scene object type +- a specific input device +- a specific renderer -It is a reusable example-shared camera framework currently validated through `61_UI`. +That is the main architectural win of the current design. From d81c459df898e3f25c9dace79bf93b1d20f0e909 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Tue, 7 Apr 2026 07:26:06 +0200 Subject: [PATCH 170/205] Clean camera API naming and docs --- 61_UI/AppInit.cpp | 2 +- .../CCameraScriptedRuntimePersistence.hpp | 6 +- .../include/camera/CCameraSequenceScript.hpp | 6 +- common/include/camera/CGimbalInputBinder.hpp | 2 +- .../include/camera/CTargetPoseController.hpp | 6 -- ...ntroller.hpp => IGimbalInputProcessor.hpp} | 10 +--- common/include/camera/README.md | 55 +++++-------------- 7 files changed, 25 insertions(+), 62 deletions(-) delete mode 100644 common/include/camera/CTargetPoseController.hpp rename common/include/camera/{IGimbalController.hpp => IGimbalInputProcessor.hpp} (98%) diff --git a/61_UI/AppInit.cpp b/61_UI/AppInit.cpp index 7e9df23e5..1a886b23e 100644 --- a/61_UI/AppInit.cpp +++ b/61_UI/AppInit.cpp @@ -472,7 +472,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) { { "frame", 2u }, { "type", "keyboard" }, - { "key", "KEY_KEY_W" }, + { "key", "W" }, { "action", "pressed" }, { "capture", true } } diff --git a/common/include/camera/CCameraScriptedRuntimePersistence.hpp b/common/include/camera/CCameraScriptedRuntimePersistence.hpp index 07aa8af2f..b88563487 100644 --- a/common/include/camera/CCameraScriptedRuntimePersistence.hpp +++ b/common/include/camera/CCameraScriptedRuntimePersistence.hpp @@ -24,10 +24,10 @@ namespace nbl::hlsl /** * Shared JSON parser for low-level scripted runtime payloads. * -* This keeps legacy `events/checks/capture_frames` authored data reusable and backward-compatible -* without leaving parser glue inside one example. +* The parser handles low-level `events/checks/capture_frames` payloads without leaving parser glue +* inside one consumer. * -* The parser can also detect compact `segments` and forward them into `CCameraSequenceScript`. +* It can also detect compact `segments` and forward them into `CCameraSequenceScript`. */ inline float32_t4x4 composeScriptedImguizmoTransform( const std::array& translation, diff --git a/common/include/camera/CCameraSequenceScript.hpp b/common/include/camera/CCameraSequenceScript.hpp index 98795a215..6b64a2e28 100644 --- a/common/include/camera/CCameraSequenceScript.hpp +++ b/common/include/camera/CCameraSequenceScript.hpp @@ -33,11 +33,11 @@ namespace nbl::hlsl * The format intentionally does not store: * * - per-frame low-level event dumps -* - `61_UI`-specific window actions as authored source data +* - consumer-specific window actions as authored source data * - ImGuizmo transforms as the primary authored primitive * -* A consumer such as `61_UI` may expand the compact sequence into its own runtime event/check -* representation, but the authored source of truth stays camera-domain and reusable. +* A consumer may expand the compact sequence into its own runtime event/check representation, but +* the authored source of truth stays camera-domain and reusable. */ //! Authored projection view request for camera-sequence playback. diff --git a/common/include/camera/CGimbalInputBinder.hpp b/common/include/camera/CGimbalInputBinder.hpp index 806368ca4..1ae245b00 100644 --- a/common/include/camera/CGimbalInputBinder.hpp +++ b/common/include/camera/CGimbalInputBinder.hpp @@ -3,7 +3,7 @@ #include -#include "IGimbalController.hpp" +#include "IGimbalInputProcessor.hpp" namespace nbl::hlsl { diff --git a/common/include/camera/CTargetPoseController.hpp b/common/include/camera/CTargetPoseController.hpp deleted file mode 100644 index bf1e45fe6..000000000 --- a/common/include/camera/CTargetPoseController.hpp +++ /dev/null @@ -1,6 +0,0 @@ -#ifndef _C_TARGET_POSE_CONTROLLER_HPP_ -#define _C_TARGET_POSE_CONTROLLER_HPP_ - -#include "CCameraGoalSolver.hpp" - -#endif // _C_TARGET_POSE_CONTROLLER_HPP_ diff --git a/common/include/camera/IGimbalController.hpp b/common/include/camera/IGimbalInputProcessor.hpp similarity index 98% rename from common/include/camera/IGimbalController.hpp rename to common/include/camera/IGimbalInputProcessor.hpp index db726ccbb..473008a24 100644 --- a/common/include/camera/IGimbalController.hpp +++ b/common/include/camera/IGimbalInputProcessor.hpp @@ -1,5 +1,5 @@ -#ifndef _NBL_I_CAMERA_CONTROLLER_HPP_ -#define _NBL_I_CAMERA_CONTROLLER_HPP_ +#ifndef _NBL_I_GIMBAL_INPUT_PROCESSOR_HPP_ +#define _NBL_I_GIMBAL_INPUT_PROCESSOR_HPP_ ///////////////////////// // TODO: TEMPORARY!!! @@ -18,8 +18,6 @@ namespace nbl::hlsl /** * Runtime processor that turns keyboard, mouse, and ImGuizmo input into virtual events. -* -* The filename is legacy. The intended public type is `IGimbalInputProcessor`. */ class IGimbalInputProcessor : public CGimbalBindingLayoutStorage { @@ -367,8 +365,6 @@ class IGimbalInputProcessor : public CGimbalBindingLayoutStorage std::chrono::microseconds m_nextPresentationTimeStamp = {}, m_lastVirtualUpTimeStamp = {}; }; -using IGimbalController = IGimbalInputProcessor; - } // nbl::hlsl namespace -#endif // _NBL_I_CAMERA_CONTROLLER_HPP_ +#endif // _NBL_I_GIMBAL_INPUT_PROCESSOR_HPP_ diff --git a/common/include/camera/README.md b/common/include/camera/README.md index 947999b65..3f423b23b 100644 --- a/common/include/camera/README.md +++ b/common/include/camera/README.md @@ -1,17 +1,14 @@ # Shared Camera API -This directory contains the reusable camera stack currently validated by [`61_UI`](../../../61_UI/README.md). +This directory contains the reusable camera stack. +The current full consumer and validation harness is [`61_UI`](../../../61_UI/README.md). It intentionally lives in `examples_tests/common` instead of engine core: -- the API is meant to be reusable across examples and tools +- the API is meant to be reusable across consumers and tools - the API is still being shaped through one active integration surface - the current goal is to stabilize the design before promoting any part of it deeper into Nabla -At the moment the only migrated consumer is `61_UI`. -That is intentional. -The stack is designed to be reusable by other examples, offline tools, and future renderers, but they are not being migrated yet. - ## What this stack is This stack is a reusable camera framework with two clearly separated halves: @@ -31,7 +28,7 @@ That split is the core design decision. This stack is not: - engine-core Nabla camera API -- a promise that all examples already use it +- a promise that every potential consumer already uses it - a generic scene animation system - a direct scene-object-follow system - a setter-heavy camera API with arbitrary absolute pose mutation in the hot runtime path @@ -43,7 +40,7 @@ The design goals are: - one semantic command language for keyboard, mouse, gizmo, scripts, and CI - no input-device assumptions inside camera models - no viewport glue inside camera models -- no direct dependence on `61_UI` concepts in the reusable camera layer +- no direct dependence on consumer-specific UI concepts in the reusable camera layer - best-effort absolute restore for tooling without turning cameras into mutable state bags - reusable persistence, analysis, playback, follow, and validation helpers @@ -187,10 +184,9 @@ It only stores how input should map to the semantic command language. ### 3. Runtime input processing -- [`IGimbalController.hpp`](IGimbalController.hpp) +- [`IGimbalInputProcessor.hpp`](IGimbalInputProcessor.hpp) - [`CGimbalInputBinder.hpp`](CGimbalInputBinder.hpp) -The filename `IGimbalController.hpp` is legacy. The main runtime type is `IGimbalInputProcessor`. This layer: @@ -199,7 +195,7 @@ This layer: - receives ImGuizmo transforms - emits virtual events for the current frame -`CGimbalInputBinder` is the convenience runtime binder that examples should usually use. +`CGimbalInputBinder` is the convenience runtime binder that a consumer should usually use. ### 4. Camera core @@ -272,7 +268,7 @@ Follow is deliberately not part of `ICamera`. The tracked subject owns its own gimbal through `CTrackedTarget`. Follow stays as a policy layer above the camera. -That means the camera API does not know about meshes, scene nodes, or `61_UI`. +That means the camera API does not know about meshes, scene nodes, or any particular UI harness. It only knows about: - camera @@ -410,7 +406,7 @@ It describes: It deliberately does not describe: -- `61_UI` window actions as authored source data +- consumer-specific window actions as authored source data - frame-by-frame event dumps - ImGuizmo matrices as authored motion primitives @@ -428,18 +424,11 @@ It is split into: - per-frame dequeue - per-frame check evaluation -This keeps `61_UI` from owning a private scripting subsystem. +This keeps one consumer from owning a private scripting subsystem. ## Current validation story -The main active validation surface is: - -- [`61_UI`](../../../61_UI/README.md) - -The current camera-focused tests are: - -- `NBL_61_UI_CAMERA_SMOKE` -- `NBL_61_UI_CAMERA_CONTINUITY` +The current camera-focused validation is exercised through the active consumer harness. ### Smoke @@ -457,15 +446,6 @@ Purpose: This test now runs on the compact authored sequence format rather than a large expanded frame dump. -## Current consumer - -Current active consumer: - -- [`61_UI`](../../../61_UI/README.md) - -Other examples are intentionally not migrated yet. -The idea is to stabilize one shared stack first, then reuse it elsewhere. - ## Recommended integration patterns ### Minimal runtime integration @@ -555,29 +535,22 @@ If any one of those concerns leaks into the others: - cameras become setter-heavy - projections become input processors -- examples own private copies of state math -- scripts become example-specific +- consumers own private copies of state math +- scripts become consumer-specific - follow becomes scene-object-specific The current refactor was mostly about removing exactly those leaks. -## Legacy and compatibility notes - -- [`CTargetPoseController.hpp`](CTargetPoseController.hpp) is a compatibility include for [`CCameraGoalSolver.hpp`](CCameraGoalSolver.hpp) -- [`IGimbalController.hpp`](IGimbalController.hpp) keeps the historical filename, but the active runtime-processing type is `IGimbalInputProcessor` -- low-level scripted JSON still accepts legacy key names such as `KEY_KEY_W` - ## Practical status Today the stack is in a good place for: -- reuse by additional examples +- reuse by additional consumers - reuse by camera-heavy tools - reuse by offline render or validation flows without assuming: -- `61_UI` - a specific scene object type - a specific input device - a specific renderer From 705f62a846d4ed720bb37b43a425d4bae366ddc0 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Tue, 7 Apr 2026 12:38:17 +0200 Subject: [PATCH 171/205] Polish camera API boundaries and runtime cleanup --- 61_UI/AppControlPanel.cpp | 59 +- 61_UI/AppImGuiListen.cpp | 111 +- 61_UI/AppInit.cpp | 528 +++---- 61_UI/AppTransformEditor.cpp | 62 +- 61_UI/AppUpdate.cpp | 73 +- 61_UI/AppWorkLoop.cpp | 2 +- 61_UI/CMakeLists.txt | 1 - 61_UI/README.md | 4 +- 61_UI/include/app/App.hpp | 123 +- 61_UI/include/common.hpp | 215 ++- 61_UI/include/transform.hpp | 19 - 61_UI/src/transform.cpp | 144 -- common/CMakeLists.txt | 7 +- common/include/camera/CArcballCamera.hpp | 62 +- .../CCameraFollowRegressionUtilities.hpp | 4 +- .../include/camera/CCameraFollowUtilities.hpp | 24 +- common/include/camera/CCameraGoal.hpp | 62 +- common/include/camera/CCameraGoalAnalysis.hpp | 4 +- common/include/camera/CCameraGoalSolver.hpp | 27 +- .../camera/CCameraInputBindingUtilities.hpp | 390 ++++++ .../include/camera/CCameraKeyframeTrack.hpp | 39 +- .../CCameraKeyframeTrackPersistence.hpp | 24 + .../camera/CCameraManipulationUtilities.hpp | 23 +- .../include/camera/CCameraMathUtilities.hpp | 359 +++++ common/include/camera/CCameraPersistence.hpp | 112 +- .../camera/CCameraPlaybackTimeline.hpp | 8 +- .../camera/CCameraPresentationUtilities.hpp | 4 +- common/include/camera/CCameraPreset.hpp | 127 +- common/include/camera/CCameraPresetFlow.hpp | 4 +- .../camera/CCameraPresetPersistence.hpp | 30 + .../camera/CCameraProjectionUtilities.hpp | 4 +- .../camera/CCameraScriptedCheckRunner.hpp | 48 +- .../include/camera/CCameraScriptedRuntime.hpp | 56 +- .../CCameraScriptedRuntimePersistence.hpp | 651 +-------- .../include/camera/CCameraSequenceScript.hpp | 578 +------- .../CCameraSequenceScriptPersistence.hpp | 22 + .../camera/CCameraSequenceScriptedBuilder.hpp | 18 +- .../include/camera/CCameraTextUtilities.hpp | 4 +- common/include/camera/CChaseCamera.hpp | 62 +- common/include/camera/CCubeProjection.hpp | 10 +- common/include/camera/CDollyCamera.hpp | 62 +- common/include/camera/CDollyZoomCamera.hpp | 54 +- common/include/camera/CFPSCamera.hpp | 82 +- common/include/camera/CFreeLockCamera.hpp | 72 +- .../include/camera/CGeneralPurposeGimbal.hpp | 5 +- common/include/camera/CGimbalInputBinder.hpp | 21 +- common/include/camera/CIsometricCamera.hpp | 54 +- common/include/camera/CLinearProjection.hpp | 2 +- common/include/camera/COrbitCamera.hpp | 65 +- common/include/camera/CPathCamera.hpp | 54 +- common/include/camera/CPlanarProjection.hpp | 2 +- .../include/camera/CSphericalTargetCamera.hpp | 10 +- common/include/camera/CTopDownCamera.hpp | 58 +- common/include/camera/CTurntableCamera.hpp | 60 +- common/include/camera/ICamera.hpp | 53 +- common/include/camera/IGimbal.hpp | 133 +- .../include/camera/IGimbalBindingLayout.hpp | 17 +- .../include/camera/IGimbalInputProcessor.hpp | 44 +- common/include/camera/ILinearProjection.hpp | 4 +- .../include/camera/IPerspectiveProjection.hpp | 2 +- common/include/camera/IPlanarProjection.hpp | 18 +- common/include/camera/IProjection.hpp | 4 +- common/include/camera/IRange.hpp | 4 +- common/include/camera/README.md | 52 +- .../examples/camera/CCameraPersistence.cpp | 338 +++++ .../CCameraScriptedRuntimePersistence.cpp | 1238 +++++++++++++++++ 66 files changed, 3404 insertions(+), 3208 deletions(-) delete mode 100644 61_UI/include/transform.hpp delete mode 100644 61_UI/src/transform.cpp create mode 100644 common/include/camera/CCameraInputBindingUtilities.hpp create mode 100644 common/include/camera/CCameraKeyframeTrackPersistence.hpp create mode 100644 common/include/camera/CCameraMathUtilities.hpp create mode 100644 common/include/camera/CCameraPresetPersistence.hpp create mode 100644 common/include/camera/CCameraSequenceScriptPersistence.hpp create mode 100644 common/src/nbl/examples/camera/CCameraPersistence.cpp create mode 100644 common/src/nbl/examples/camera/CCameraScriptedRuntimePersistence.cpp diff --git a/61_UI/AppControlPanel.cpp b/61_UI/AppControlPanel.cpp index 5deb54e0f..f31b3b443 100644 --- a/61_UI/AppControlPanel.cpp +++ b/61_UI/AppControlPanel.cpp @@ -1,4 +1,31 @@ #include "app/App.hpp" +#include "camera/CCameraPersistence.hpp" + +bool App::savePresetsToFile(const nbl::system::path& path) +{ + return nbl::system::savePresetCollectionToFile(path, std::span(m_presets.data(), m_presets.size())); +} + +bool App::loadPresetsFromFile(const nbl::system::path& path) +{ + return nbl::system::loadPresetCollectionFromFile(path, m_presets); +} + +bool App::saveKeyframesToFile(const nbl::system::path& path) +{ + return nbl::system::saveKeyframeTrackToFile(path, m_keyframeTrack); +} + +bool App::loadKeyframesFromFile(const nbl::system::path& path) +{ + if (!nbl::system::loadKeyframeTrackFromFile(path, m_keyframeTrack)) + return false; + + clampPlaybackTimeToKeyframes(); + if (m_keyframeTrack.keyframes.empty()) + clearApplyStatusBanner(m_playbackApplyBanner); + return true; +} void App::DrawControlPanel() { @@ -277,7 +304,7 @@ void App::DrawControlPanel() { const auto& gimbal = activeCamera->getGimbal(); const auto pos = gimbal.getPosition(); - const auto euler = glm::degrees(glm::eulerAngles(gimbal.getOrientation())); + const auto euler = getQuaternionEulerDegrees(gimbal.getOrientation()); if (BeginCard("CameraCard", CalcCardHeight(5), cardTop, cardBottom, cardBorder)) { @@ -562,7 +589,7 @@ void App::DrawControlPanel() { auto& gimbal = boundCamera->getGimbal(); const auto position = getCastedVector(gimbal.getPosition()); - const auto& orientation = gimbal.getOrientation(); + const auto orientation = getCastedVector(gimbal.getOrientation().data); const auto viewMatrix = getCastedMatrix(gimbal.getViewMatrix()); addMatrixTable("Position", ("PositionTable_" + activePlanarIxString).c_str(), 1, 3, &position[0], false); @@ -687,7 +714,7 @@ void App::DrawControlPanel() 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::hlsl::cameraFollowModeUsesCapturedOffset(followConfig.mode)) + if (followStateChanged && followConfig.enabled && nbl::core::cameraFollowModeUsesCapturedOffset(followConfig.mode)) captureFollowOffsetsForPlanar(getActivePlanarIx()); if (followStateChanged && followConfig.enabled) applyFollowToConfiguredCameras(); @@ -708,7 +735,7 @@ void App::DrawControlPanel() DrawHoverHint("Optionally snap tracked target gimbal to the model transform"); ImGui::SameLine(); if (ImGui::Button("Target origin")) - m_followTarget.setPose(float64_t3(0.0), glm::quat(1.0f, 0.0f, 0.0f, 0.0f)); + m_followTarget.setPose(float64_t3(0.0), makeIdentityQuaternion()); DrawHoverHint("Reset tracked target to identity at world origin"); ImGui::SameLine(); if (ImGui::Button("Capture current offset")) @@ -754,7 +781,7 @@ void App::DrawControlPanel() if (ImGui::Button("Add preset")) { CameraPreset preset; - if (nbl::hlsl::tryCapturePreset(m_cameraGoalSolver, activeCamera, m_presetName, preset)) + if (nbl::core::tryCapturePreset(m_cameraGoalSolver, activeCamera, m_presetName, preset)) { m_presets.emplace_back(std::move(preset)); m_selectedPresetIx = static_cast(m_presets.size()) - 1; @@ -779,9 +806,9 @@ void App::DrawControlPanel() if (!m_presets.empty()) { const char* presetFilterLabels[] = { - nbl::hlsl::getPresetApplyPresentationFilterLabel(PresetFilterMode::All), - nbl::hlsl::getPresetApplyPresentationFilterLabel(PresetFilterMode::Exact), - nbl::hlsl::getPresetApplyPresentationFilterLabel(PresetFilterMode::BestEffort) + nbl::core::getPresetApplyPresentationFilterLabel(PresetFilterMode::All), + nbl::core::getPresetApplyPresentationFilterLabel(PresetFilterMode::Exact), + nbl::core::getPresetApplyPresentationFilterLabel(PresetFilterMode::BestEffort) }; int presetFilterIx = static_cast(m_presetFilterMode); if (ImGui::Combo("Visibility", &presetFilterIx, presetFilterLabels, IM_ARRAYSIZE(presetFilterLabels))) @@ -895,14 +922,14 @@ void App::DrawControlPanel() ImGui::InputText("Preset file", m_presetPath, IM_ARRAYSIZE(m_presetPath)); if (ImGui::Button("Save presets")) { - if (!savePresetsToFile(system::path(m_presetPath))) + if (!savePresetsToFile(nbl::system::path(m_presetPath))) m_logger->log("Failed to save presets to \"%s\".", ILogger::ELL_ERROR, m_presetPath); } DrawHoverHint("Save presets to JSON file"); ImGui::SameLine(); if (ImGui::Button("Load presets")) { - if (!loadPresetsFromFile(system::path(m_presetPath))) + if (!loadPresetsFromFile(nbl::system::path(m_presetPath))) m_logger->log("Failed to load presets from \"%s\".", ILogger::ELL_ERROR, m_presetPath); } DrawHoverHint("Load presets from JSON file"); @@ -936,14 +963,14 @@ void App::DrawControlPanel() ImGui::SameLine(); if (ImGui::Button("Stop")) { - nbl::hlsl::resetPlaybackCursor(m_playback); + nbl::core::resetPlaybackCursor(m_playback); applyPlaybackAtTime(m_playback.time); } DrawHoverHint("Stop playback and reset time"); if (!m_keyframeTrack.keyframes.empty()) { - const float duration = nbl::hlsl::getPlaybackTrackDuration(m_keyframeTrack); + const float duration = nbl::core::getPlaybackTrackDuration(m_keyframeTrack); if (ImGui::SliderFloat("Time", &m_playback.time, 0.f, duration, "%.3f")) applyPlaybackAtTime(m_playback.time); } @@ -981,7 +1008,7 @@ void App::DrawControlPanel() const float authoredTime = std::max(0.f, m_newKeyframeTime); keyframe.time = authoredTime; m_newKeyframeTime = authoredTime; - if (nbl::hlsl::tryCapturePreset(m_cameraGoalSolver, activeCamera, "Keyframe", keyframe.preset)) + if (nbl::core::tryCapturePreset(m_cameraGoalSolver, activeCamera, "Keyframe", keyframe.preset)) { m_keyframeTrack.keyframes.emplace_back(std::move(keyframe)); sortKeyframesByTime(); @@ -1000,7 +1027,7 @@ void App::DrawControlPanel() if (ImGui::Button("Clear keyframes")) { m_keyframeTrack = {}; - nbl::hlsl::resetPlaybackCursor(m_playback); + nbl::core::resetPlaybackCursor(m_playback); clearApplyStatusBanner(m_playbackApplyBanner); } DrawHoverHint("Remove all keyframes"); @@ -1110,14 +1137,14 @@ void App::DrawControlPanel() ImGui::InputText("Keyframe file", m_keyframePath, IM_ARRAYSIZE(m_keyframePath)); if (ImGui::Button("Save keyframes")) { - if (!saveKeyframesToFile(system::path(m_keyframePath))) + if (!saveKeyframesToFile(nbl::system::path(m_keyframePath))) m_logger->log("Failed to save keyframes to \"%s\".", ILogger::ELL_ERROR, m_keyframePath); } DrawHoverHint("Save keyframes to JSON file"); ImGui::SameLine(); if (ImGui::Button("Load keyframes")) { - if (!loadKeyframesFromFile(system::path(m_keyframePath))) + if (!loadKeyframesFromFile(nbl::system::path(m_keyframePath))) m_logger->log("Failed to load keyframes from \"%s\".", ILogger::ELL_ERROR, m_keyframePath); } DrawHoverHint("Load keyframes from JSON file"); diff --git a/61_UI/AppImGuiListen.cpp b/61_UI/AppImGuiListen.cpp index 496a0f40d..d406c1572 100644 --- a/61_UI/AppImGuiListen.cpp +++ b/61_UI/AppImGuiListen.cpp @@ -78,9 +78,6 @@ void App::imguiListen() syncDynamicPerspectiveProjection(planarViewCameraBound, projection); projection.update(binding.leftHandedProjection, binding.aspectRatio); - // TODO: - // would be nice to normalize imguizmo visual vectors (possible with styles) - // first 0th texture is for UI texture atlas, then there are our window textures auto fboImguiTextureID = windowIx + 1u; info.textureID = fboImguiTextureID; @@ -183,112 +180,10 @@ void App::imguiListen() { if (targetGimbalManipulationCamera) { - const auto referenceFrame = getCastedMatrix(*reinterpret_cast(ImGuizmo::GetReferenceFrame())); + const auto referenceFrame = getCastedMatrix(imguizmoModel.outTRS); bindManipulatedCamera(planarIx.value()); - - // TODO: TO BE REMOVED, ONLY FOR TESTING ITS INCOMPLETE TYPE! - const auto& imguizmoCtx = ImGuizmo::GetContext(); - - struct - { - float32_t3 t, r, s; - } out, delta; - - ImGuizmo::DecomposeMatrixToComponents(&imguizmoModel.outTRS[0][0], &out.t[0], &out.r[0], &out.s[0]); - ImGuizmo::DecomposeMatrixToComponents(&imguizmoModel.outDeltaTRS[0][0], &delta.t[0], &delta.r[0], &delta.s[0]); - { - std::vector virtualEvents; - - auto requestMagnitudeUpdateWithScalar = [&](float signPivot, float dScalar, float dMagnitude, auto positive, auto negative) - { - if (dScalar != signPivot) - { - auto& ev = virtualEvents.emplace_back(); - auto code = (dScalar > signPivot) ? positive : negative; - - ev.type = code; - ev.magnitude += dMagnitude; - } - }; - - // TODO TESTING STUFF WITH MY IMGUIZMO UPDATES - // IT WILL BE REMOVED ONCE ALL TESTS ARE DONE - // AND CONTROLLER API WILL BE USED INSTEAD - - // translations - { - ImGuizmo::OPERATION ioType; - const auto dScalar = ImGuizmo::GetTranslationDeltaScalar(&ioType); - - if (dScalar) - { - switch (ioType) - { - case ImGuizmo::OPERATION::TRANSLATE_X: - { - requestMagnitudeUpdateWithScalar(0.f, dScalar, std::abs(dScalar), CVirtualGimbalEvent::VirtualEventType::MoveRight, CVirtualGimbalEvent::VirtualEventType::MoveLeft); - } break; - - case ImGuizmo::OPERATION::TRANSLATE_Y: - { - requestMagnitudeUpdateWithScalar(0.f, dScalar, std::abs(dScalar), CVirtualGimbalEvent::VirtualEventType::MoveUp, CVirtualGimbalEvent::VirtualEventType::MoveDown); - } break; - - case ImGuizmo::OPERATION::TRANSLATE_Z: - { - requestMagnitudeUpdateWithScalar(0.f, dScalar, std::abs(dScalar), CVirtualGimbalEvent::VirtualEventType::MoveForward, CVirtualGimbalEvent::VirtualEventType::MoveBackward); - } break; - - default: break; - } - } - } - - // TODO: ok becuase I have only one reference from imguizmo I must do it differently when - // I have local base && want to do rotation with respect to world instead; we almost there - - // rotations - { - ImGuizmo::OPERATION ioType; - float dRadians = ImGuizmo::GetRotationDeltaRadians(&ioType); - - if (dRadians) - { - switch (ioType) - { - case ImGuizmo::OPERATION::ROTATE_X: - { - requestMagnitudeUpdateWithScalar(0.f, dRadians, std::abs(dRadians), CVirtualGimbalEvent::VirtualEventType::TiltUp, CVirtualGimbalEvent::VirtualEventType::TiltDown); - } break; - - case ImGuizmo::OPERATION::ROTATE_Y: - { - requestMagnitudeUpdateWithScalar(0.f, dRadians, std::abs(dRadians), CVirtualGimbalEvent::VirtualEventType::PanRight, CVirtualGimbalEvent::VirtualEventType::PanLeft); - } break; - - case ImGuizmo::OPERATION::ROTATE_Z: - { - requestMagnitudeUpdateWithScalar(0.f, dRadians, std::abs(dRadians), CVirtualGimbalEvent::VirtualEventType::RollRight, CVirtualGimbalEvent::VirtualEventType::RollLeft); - } break; - - default: - assert(false); break; // should never be hit - } - } - } - - const auto vCount = virtualEvents.size(); - - if (vCount) - { - // I start to think controller should be able to set sensitivity to scale magnitudes of generated events - // in order for camera to not keep any magnitude scalars like move or rotation speed scales - - targetGimbalManipulationCamera->manipulateWithUnitMotionScales({ virtualEvents.data(), vCount }, &referenceFrame); - refreshFollowOffsetConfigForPlanar(planarIx.value()); - } - - } + nbl::core::applyReferenceFrameToCamera(targetGimbalManipulationCamera, referenceFrame); + refreshFollowOffsetConfigForPlanar(planarIx.value()); } else if (isFollowTarget) { diff --git a/61_UI/AppInit.cpp b/61_UI/AppInit.cpp index 1a886b23e..ac1213ecf 100644 --- a/61_UI/AppInit.cpp +++ b/61_UI/AppInit.cpp @@ -11,8 +11,14 @@ #include #include +#include "camera/CCameraPersistence.hpp" +#include "camera/CCameraScriptedRuntimePersistence.hpp" +#include "nlohmann/json.hpp" + namespace { + using camera_json_t = nlohmann::json; + struct SpaceEnvBlobHeader final { uint32_t magic = 0u; @@ -53,16 +59,13 @@ namespace constexpr float CameraDefaultRotateScale = 0.003f; constexpr float CameraOrbitMoveScale = 0.5f; - void initializeCameraRigConfig(nbl::hlsl::ICamera& camera, const double moveScale, const double rotationScale) + void initializeCameraRigConfig(nbl::core::ICamera& camera, const double moveScale, const double rotationScale) { camera.setMotionScales(moveScale, rotationScale); - camera.resetDefaultInputBindingToPreset(); } - bool createCameraFromJson(const nbl_json& jCamera, std::string& error, smart_refctd_ptr& outCamera) + bool createCameraFromJson(const camera_json_t& jCamera, std::string& error, smart_refctd_ptr& outCamera) { - using namespace nbl::hlsl; - if (!jCamera.contains("type")) { error = "Camera entry missing \"type\"."; @@ -82,19 +85,19 @@ namespace auto position = [&]() { const auto jret = jCamera["position"].get>(); - return float32_t3(jret[0], jret[1], jret[2]); + return float64_t3(jret[0], jret[1], jret[2]); }(); auto getOrientation = [&]() { const auto jret = jCamera["orientation"].get>(); - return glm::quat(jret[3], jret[0], jret[1], jret[2]); + return makeQuaternionFromComponents(jret[0], jret[1], jret[2], jret[3]); }; auto getTarget = [&]() { const auto jret = jCamera["target"].get>(); - return float32_t3(jret[0], jret[1], jret[2]); + return float64_t3(jret[0], jret[1], jret[2]); }; auto finalize = [&](auto&& camera, const double moveScale, const double rotationScale) @@ -111,7 +114,7 @@ namespace error = "FPS camera requires \"orientation\"."; return false; } - return finalize(make_smart_refctd_ptr(position, getOrientation()), CameraDefaultMoveScale, CameraDefaultRotateScale); + return finalize(make_smart_refctd_ptr(position, getOrientation()), CameraDefaultMoveScale, CameraDefaultRotateScale); } if (type == "Free") @@ -121,7 +124,7 @@ namespace error = "Free camera requires \"orientation\"."; return false; } - return finalize(make_smart_refctd_ptr(position, getOrientation()), CameraDefaultMoveScale, CameraDefaultRotateScale); + return finalize(make_smart_refctd_ptr(position, getOrientation()), CameraDefaultMoveScale, CameraDefaultRotateScale); } if (!withTarget) @@ -220,7 +223,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) return (localInputCWD / "app_resources" / "cameras.json").lexically_normal(); }(); - nbl_json j; + camera_json_t j; { std::ifstream file(configPath); if (!file.is_open()) @@ -279,7 +282,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) return false; const auto& gimbal = camera->getGimbal(); const auto afterPos = gimbal.getPosition(); - const auto afterEuler = glm::degrees(glm::eulerAngles(gimbal.getOrientation())); + const auto afterEuler = getCastedVector(getQuaternionEulerDegrees(gimbal.getOrientation())); if (!isFinite3(afterPos) || !isFinite3(afterEuler)) return false; @@ -304,7 +307,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) const auto& beforeGimbal = camera->getGimbal(); const float64_t3 beforePos = beforeGimbal.getPosition(); - const float32_t3 beforeEulerDeg = glm::degrees(glm::eulerAngles(beforeGimbal.getOrientation())); + const float32_t3 beforeEulerDeg = getCastedVector(getQuaternionEulerDegrees(beforeGimbal.getOrientation())); if (!isFinite3(beforePos) || !isFinite3(beforeEulerDeg)) return false; @@ -319,12 +322,12 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) auto comparePresetToCamera = [&](ICamera* camera, const CameraPreset& preset, const double posEps, const double rotEpsDeg, const double scalarEps) -> bool { - return nbl::hlsl::comparePresetToCameraState(m_cameraGoalSolver, camera, preset, posEps, rotEpsDeg, scalarEps); + return nbl::core::comparePresetToCameraState(m_cameraGoalSolver, camera, preset, posEps, rotEpsDeg, scalarEps); }; auto describePresetMismatch = [&](ICamera* camera, const CameraPreset& preset) -> std::string { - return nbl::hlsl::describePresetCameraMismatch(m_cameraGoalSolver, camera, preset); + return nbl::core::describePresetCameraMismatch(m_cameraGoalSolver, camera, preset); }; auto tryBuildFollowViewProjForCamera = [&](ICamera* camera, float32_t4x4& outViewProjMatrix) -> bool @@ -366,7 +369,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) { float32_t4x4 viewProjMatrix = float32_t4x4(1.0f); const bool hasViewProjMatrix = tryBuildFollowViewProjForCamera(camera, viewProjMatrix); - return nbl::hlsl::buildFollowVisualMetrics( + return nbl::core::buildFollowVisualMetrics( camera, trackedTarget, &followConfig, @@ -374,12 +377,12 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) }; auto buildAndValidateFollowTargetContract = [&](ICamera* camera, const CTrackedTarget& trackedTarget, - const SCameraFollowConfig& followConfig, const char* label, nbl::hlsl::SCameraFollowApplyValidationResult& outResult) -> bool + const SCameraFollowConfig& followConfig, const char* label, nbl::core::SCameraFollowApplyValidationResult& outResult) -> bool { std::string regressionError; float32_t4x4 viewProjMatrix = float32_t4x4(1.0f); const bool hasViewProjMatrix = tryBuildFollowViewProjForCamera(camera, viewProjMatrix); - if (!nbl::hlsl::buildApplyAndValidateFollowTargetContract( + if (!nbl::core::buildApplyAndValidateFollowTargetContract( m_cameraGoalSolver, camera, trackedTarget, @@ -401,7 +404,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) const bool expectsProjectedMetrics = tryBuildFollowViewProjForCamera(camera, viewProjMatrix); if (!metrics.active) return fail(std::string("Follow visual metrics smoke was inactive for ") + label + "."); - if (nbl::hlsl::cameraFollowModeLocksViewToTarget(followConfig.mode) && !metrics.lockValid) + if (nbl::core::cameraFollowModeLocksViewToTarget(followConfig.mode) && !metrics.lockValid) return fail(std::string("Follow visual metrics smoke was missing lock metrics for ") + label + "."); if (expectsProjectedMetrics && !metrics.projectedValid) return fail(std::string("Follow visual metrics smoke was missing projected metrics for ") + label + "."); @@ -413,22 +416,22 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) auto verifyScriptedRuntimeFrameBatch = [&]() -> bool { CCameraScriptedTimeline timeline = {}; - nbl::hlsl::appendScriptedActionEvent(timeline, 3u, CCameraScriptedInputEvent::ActionData::Kind::SetActivePlanar, 4); + nbl::system::appendScriptedActionEvent(timeline, 3u, CCameraScriptedInputEvent::ActionData::Kind::SetActivePlanar, 4); { CCameraGoal goal = {}; goal.position = float64_t3(1.0, 2.0, 3.0); - nbl::hlsl::appendScriptedGoalEvent(timeline, 3u, goal, true); + nbl::system::appendScriptedGoalEvent(timeline, 3u, goal, true); } - nbl::hlsl::appendScriptedSegmentLabelEvent(timeline, 3u, "segment-three"); + nbl::system::appendScriptedSegmentLabelEvent(timeline, 3u, "segment-three"); { float64_t4x4 transform = float64_t4x4(1.0); - transform[3] = glm::dvec4(7.0, 8.0, 9.0, 1.0); - nbl::hlsl::appendScriptedTrackedTargetTransformEvent(timeline, 4u, transform); + transform[3] = float64_t4(7.0, 8.0, 9.0, 1.0); + nbl::system::appendScriptedTrackedTargetTransformEvent(timeline, 4u, transform); } size_t nextEventIndex = 0u; CCameraScriptedFrameEvents batch; - nbl::hlsl::dequeueScriptedFrameEvents(timeline.events, nextEventIndex, 3u, batch); + nbl::system::dequeueScriptedFrameEvents(timeline.events, nextEventIndex, 3u, batch); if (nextEventIndex != 3u || batch.actions.size() != 1u || batch.goals.size() != 1u || batch.segmentLabels.size() != 1u || !batch.mouse.empty() || !batch.keyboard.empty()) { @@ -440,7 +443,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) return fail("Scripted runtime frame batch payload smoke failed for frame 3."); } - nbl::hlsl::dequeueScriptedFrameEvents(timeline.events, nextEventIndex, 4u, batch); + nbl::system::dequeueScriptedFrameEvents(timeline.events, nextEventIndex, 4u, batch); if (nextEventIndex != timeline.events.size() || batch.trackedTargetTransforms.size() != 1u || !batch.actions.empty() || !batch.goals.empty()) { @@ -455,45 +458,46 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) auto verifyScriptedRuntimeParser = [&]() -> bool { - nbl_json script = { - { "enabled", true }, - { "capture_prefix", "parser_smoke" }, - { "camera_controls", { - { "keyboard_scale", 2.0f }, - { "rotation_scale", 0.5f } - } }, - { "events", nbl_json::array({ - { - { "frame", 2u }, - { "type", "action" }, - { "action", "set_active_planar" }, - { "value", 3 } - }, - { - { "frame", 2u }, - { "type", "keyboard" }, - { "key", "W" }, - { "action", "pressed" }, - { "capture", true } - } - }) }, - { "checks", nbl_json::array({ - { - { "frame", 2u }, - { "kind", "baseline" } - }, - { - { "frame", 3u }, - { "kind", "gimbal_step" }, - { "min_pos_delta", 0.01f }, - { "max_pos_delta", 1.0f } - } - }) } - }; - - CCameraScriptedInputParseResult parsed; + std::stringstream script; + script << R"json({ + "enabled": true, + "capture_prefix": "parser_smoke", + "camera_controls": { + "keyboard_scale": 2.0, + "rotation_scale": 0.5 + }, + "events": [ + { + "frame": 2, + "type": "action", + "action": "set_active_planar", + "value": 3 + }, + { + "frame": 2, + "type": "keyboard", + "key": "W", + "action": "pressed", + "capture": true + } + ], + "checks": [ + { + "frame": 2, + "kind": "baseline" + }, + { + "frame": 3, + "kind": "gimbal_step", + "min_pos_delta": 0.01, + "max_pos_delta": 1.0 + } + ] +})json"; + + nbl::system::CCameraScriptedInputParseResult parsed; std::string parseError; - if (!nbl::hlsl::deserializeCameraScriptedInput(script, parsed, &parseError)) + if (!nbl::system::readCameraScriptedInput(script, parsed, &parseError)) return fail("Scripted runtime parser smoke failed to parse low-level runtime payload. " + parseError); if (!parsed.enabled || parsed.capturePrefix != "parser_smoke" || !parsed.cameraControls.hasKeyboardScale || !parsed.cameraControls.hasRotationScale) return fail("Scripted runtime parser smoke lost top-level metadata."); @@ -504,7 +508,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) size_t nextEventIndex = 0u; CCameraScriptedFrameEvents batch; - nbl::hlsl::dequeueScriptedFrameEvents(parsed.timeline.events, nextEventIndex, 2u, batch); + nbl::system::dequeueScriptedFrameEvents(parsed.timeline.events, nextEventIndex, 2u, batch); if (batch.actions.size() != 1u || batch.keyboard.size() != 1u || batch.actions.front().value != 3) return fail("Scripted runtime parser smoke produced wrong frame-two batch."); if (parsed.timeline.checks.front().kind != CCameraScriptedInputCheck::Kind::Baseline || @@ -521,12 +525,12 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) auto orbitCamera = core::make_smart_refctd_ptr(float64_t3(0.0, 1.5, -6.0), float64_t3(0.0, 0.0, 0.0)); CTrackedTarget trackedTarget( float64_t3(2.0, 0.5, -1.5), - glm::angleAxis(glm::radians(35.0), float64_t3(0.0, 1.0, 0.0))); + makeQuaternionFromAxisAngle(float64_t3(0.0, 1.0, 0.0), hlsl::radians(35.0))); CCameraScriptedTimeline timeline = {}; - nbl::hlsl::appendScriptedBaselineCheck(timeline, 1u); - nbl::hlsl::appendScriptedGimbalStepCheck(timeline, 2u, true, 2.0f, 0.005f, true, 45.0f, 0.05f); - nbl::hlsl::appendScriptedFollowTargetLockCheck(timeline, 3u, 0.1f, 0.03f); + nbl::system::appendScriptedBaselineCheck(timeline, 1u); + nbl::system::appendScriptedGimbalStepCheck(timeline, 2u, true, 2.0f, 0.005f, true, 45.0f, 0.05f); + nbl::system::appendScriptedFollowTargetLockCheck(timeline, 3u, 0.1f, 0.03f); CCameraScriptedCheckRuntimeState state = {}; { @@ -538,7 +542,28 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) .camera = orbitCamera.get() }); if (frameResult.hadFailures || state.nextCheckIndex != 1u || !state.baselineValid || !state.stepValid) - return fail("Scripted check runner baseline smoke failed."); + { + const auto& gimbal = orbitCamera->getGimbal(); + const auto pos = gimbal.getPosition(); + const auto orientation = gimbal.getOrientation(); + const auto basis = gimbal.getOrthonornalMatrix(); + const auto eulerDeg = getQuaternionEulerDegrees(gimbal.getOrientation()); + std::ostringstream oss; + oss << std::fixed << std::setprecision(6) + << "Scripted check runner baseline smoke failed." + << " nextCheckIndex=" << state.nextCheckIndex + << " baselineValid=" << state.baselineValid + << " stepValid=" << state.stepValid + << " 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; + return fail(oss.str()); + } } { @@ -565,7 +590,8 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) SCameraFollowConfig followConfig = {}; followConfig.enabled = true; followConfig.mode = ECameraFollowMode::OrbitTarget; - if (!nbl::hlsl::applyFollowToCamera(m_cameraGoalSolver, orbitCamera.get(), trackedTarget, followConfig).succeeded()) + CCameraGoal followGoal = {}; + if (!nbl::core::applyFollowToCamera(m_cameraGoalSolver, orbitCamera.get(), trackedTarget, followConfig, &followGoal).succeeded()) return fail("Scripted check runner smoke failed to apply follow before follow-lock validation."); { @@ -582,7 +608,41 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) if (frameResult.hadFailures || state.nextCheckIndex != timeline.checks.size()) { const auto details = !frameResult.logs.empty() ? frameResult.logs.front().text : std::string("missing log details"); - return fail(std::string("Scripted check runner follow-lock smoke failed. ") + 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); + const auto desiredRight = normalize(cross(float64_t3(0.0, 1.0, 0.0), desiredForward)); + const auto desiredUp = normalize(cross(desiredForward, desiredRight)); + const auto goalRightVec = normalizeQuaternion(followGoal.orientation).transformVector(float64_t3(1.0, 0.0, 0.0), true); + const auto goalUpVec = normalizeQuaternion(followGoal.orientation).transformVector(float64_t3(0.0, 1.0, 0.0), true); + const auto goalForwardVec = normalizeQuaternion(followGoal.orientation).transformVector(float64_t3(0.0, 0.0, 1.0), true); + const auto goalBasis = getQuaternionBasisMatrix(followGoal.orientation); + float lockAngle = 0.0f; + double targetDistance = 0.0; + const bool hasLockMetrics = nbl::core::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; + return fail(oss.str()); } } @@ -592,11 +652,11 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) auto verifyFollowTargetContract = [&](ICamera* camera, const CTrackedTarget& trackedTarget, const SCameraFollowConfig& followConfig, const CCameraGoal& followGoal, const char* label) -> bool { - nbl::hlsl::SCameraFollowRegressionResult regression = {}; + nbl::core::SCameraFollowRegressionResult regression = {}; std::string regressionError; float32_t4x4 viewProjMatrix = float32_t4x4(1.0f); const bool hasViewProjMatrix = tryBuildFollowViewProjForCamera(camera, viewProjMatrix); - if (!nbl::hlsl::validateFollowTargetContract( + if (!nbl::core::validateFollowTargetContract( camera, trackedTarget, followConfig, @@ -634,45 +694,45 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) if (!camera) return true; - const auto baselinePreset = nbl::hlsl::capturePreset(m_cameraGoalSolver, camera, std::string(label) + " baseline"); + const auto baselinePreset = nbl::core::capturePreset(m_cameraGoalSolver, camera, std::string(label) + " baseline"); SCameraFollowConfig followConfig = {}; followConfig.enabled = true; followConfig.mode = ECameraFollowMode::KeepLocalOffset; - if (!nbl::hlsl::captureFollowOffsetsFromCamera(m_cameraGoalSolver, camera, trackedTarget, followConfig)) + if (!nbl::core::captureFollowOffsetsFromCamera(m_cameraGoalSolver, camera, trackedTarget, followConfig)) return fail(std::string("Follow recapture smoke failed to capture initial offset for ") + label + "."); - const auto initialApply = nbl::hlsl::applyFollowToCamera(m_cameraGoalSolver, camera, trackedTarget, followConfig); + const auto initialApply = nbl::core::applyFollowToCamera(m_cameraGoalSolver, camera, trackedTarget, followConfig); if (!initialApply.succeeded()) return fail(std::string("Follow recapture smoke failed to apply initial follow for ") + label + "."); - auto editedPreset = nbl::hlsl::capturePreset(m_cameraGoalSolver, camera, std::string(label) + " edited"); + auto editedPreset = nbl::core::capturePreset(m_cameraGoalSolver, camera, std::string(label) + " edited"); if (!editedPreset.goal.hasOrbitState) return fail(std::string("Follow recapture smoke missing orbit state for ") + label + "."); - editedPreset.goal.orbitU = nbl::hlsl::wrapAngleRad(editedPreset.goal.orbitU + glm::radians(18.0)); + editedPreset.goal.orbitU = hlsl::wrapAngleRad(editedPreset.goal.orbitU + hlsl::radians(18.0)); editedPreset.goal.orbitDistance = std::clamp(editedPreset.goal.orbitDistance + 0.75f, CSphericalTargetCamera::MinDistance, CSphericalTargetCamera::MaxDistance); - editedPreset.goal = nbl::hlsl::canonicalizeGoal(editedPreset.goal); - if (!nbl::hlsl::isGoalFinite(editedPreset.goal)) + editedPreset.goal = nbl::core::canonicalizeGoal(editedPreset.goal); + if (!nbl::core::isGoalFinite(editedPreset.goal)) return fail(std::string("Follow recapture smoke produced a non-finite edited goal for ") + label + "."); - const auto editedApply = nbl::hlsl::applyPresetDetailed(m_cameraGoalSolver, camera, editedPreset); + const auto editedApply = nbl::core::applyPresetDetailed(m_cameraGoalSolver, camera, editedPreset); if (!editedApply.succeeded() || !editedApply.changed()) { return fail(std::string("Follow recapture smoke failed to apply edited preset for ") + label + ". " + describeApplyResult(editedApply)); } - const auto reachedEditedPreset = nbl::hlsl::capturePreset(m_cameraGoalSolver, camera, std::string(label) + " reached"); + const auto reachedEditedPreset = nbl::core::capturePreset(m_cameraGoalSolver, camera, std::string(label) + " reached"); - if (!nbl::hlsl::captureFollowOffsetsFromCamera(m_cameraGoalSolver, camera, trackedTarget, followConfig)) + if (!nbl::core::captureFollowOffsetsFromCamera(m_cameraGoalSolver, camera, trackedTarget, followConfig)) return fail(std::string("Follow recapture smoke failed to recapture offset for ") + label + "."); CCameraGoal recapturedGoal = {}; - if (!nbl::hlsl::tryBuildFollowGoal(m_cameraGoalSolver, camera, trackedTarget, followConfig, recapturedGoal)) + if (!nbl::core::tryBuildFollowGoal(m_cameraGoalSolver, camera, trackedTarget, followConfig, recapturedGoal)) return fail(std::string("Follow recapture smoke failed to rebuild follow goal for ") + label + "."); - const auto recapturedApply = nbl::hlsl::applyFollowToCamera(m_cameraGoalSolver, camera, trackedTarget, followConfig); + const auto recapturedApply = nbl::core::applyFollowToCamera(m_cameraGoalSolver, camera, trackedTarget, followConfig); if (!recapturedApply.succeeded()) { return fail(std::string("Follow recapture smoke failed to apply recaptured follow for ") + label + @@ -684,7 +744,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) if (!verifyFollowTargetContract(camera, trackedTarget, followConfig, recapturedGoal, label)) return false; - const auto restoreResult = nbl::hlsl::applyPresetDetailed(m_cameraGoalSolver, camera, baselinePreset); + const auto restoreResult = nbl::core::applyPresetDetailed(m_cameraGoalSolver, camera, baselinePreset); if (!restoreResult.succeeded() || !comparePresetToCamera(camera, baselinePreset, 1e-6, 0.1, 1e-6)) { return fail(std::string("Follow recapture smoke failed to restore baseline for ") + label + @@ -778,9 +838,9 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) return fail("Null camera instance."); CGimbalInputBinder inputBinder; - camera->copyDefaultInputBindingPresetTo(inputBinder); + applyDefaultCameraInputBindingPreset(inputBinder, *camera); - const auto initialPreset = nbl::hlsl::capturePreset(m_cameraGoalSolver, camera, "smoke-initial"); + const auto initialPreset = nbl::core::capturePreset(m_cameraGoalSolver, camera, "smoke-initial"); const auto initialCompatibility = analyzePresetCompatibility(camera, initialPreset); if (!initialCompatibility.exact || initialCompatibility.missingGoalStateMask != ICamera::GoalStateNone) return fail("Preset compatibility smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\". missing=" + describeGoalStateMask(initialCompatibility.missingGoalStateMask)); @@ -813,7 +873,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) default: break; } - if (!nbl::hlsl::applyPreset(m_cameraGoalSolver, camera, initialPreset)) + if (!nbl::core::applyPreset(m_cameraGoalSolver, camera, initialPreset)) return fail("Preset no-op smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\"."); if (initialPreset.goal.hasTargetPosition) @@ -821,7 +881,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) CameraPreset shiftedPreset = initialPreset; shiftedPreset.goal.targetPosition += float64_t3(0.5, -0.25, 0.75); - const auto shiftedResult = nbl::hlsl::applyPresetDetailed(m_cameraGoalSolver, camera, shiftedPreset); + const auto shiftedResult = nbl::core::applyPresetDetailed(m_cameraGoalSolver, camera, shiftedPreset); if (!shiftedResult.succeeded() || !shiftedResult.changed() || !shiftedResult.exact) return fail("Preset target apply smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\". " + describeApplyResult(shiftedResult)); @@ -829,7 +889,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) if (!camera->tryGetSphericalTargetState(shiftedState) || !nearlyEqual3(shiftedState.target, shiftedPreset.goal.targetPosition, 1e-9)) return fail("Preset target writeback smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\"."); - const auto restoredResult = nbl::hlsl::applyPresetDetailed(m_cameraGoalSolver, camera, initialPreset); + const auto restoredResult = nbl::core::applyPresetDetailed(m_cameraGoalSolver, camera, initialPreset); if (!restoredResult.succeeded() || !restoredResult.exact) return fail("Preset restore smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\". " + describeApplyResult(restoredResult)); @@ -855,11 +915,11 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) shiftedPreset.goal.dynamicPerspectiveState.referenceDistance = std::max(0.1f, initialPreset.goal.dynamicPerspectiveState.referenceDistance + 1.25f); - const auto shiftedResult = nbl::hlsl::applyPresetDetailed(m_cameraGoalSolver, camera, shiftedPreset); + const auto shiftedResult = nbl::core::applyPresetDetailed(m_cameraGoalSolver, camera, shiftedPreset); if (!shiftedResult.succeeded() || !shiftedResult.changed() || !comparePresetToCamera(camera, shiftedPreset, 1e-6, 0.1, 1e-6)) return fail("Preset dynamic perspective apply smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\". " + describeApplyResult(shiftedResult) + " " + describePresetMismatch(camera, shiftedPreset)); - const auto restoredResult = nbl::hlsl::applyPresetDetailed(m_cameraGoalSolver, camera, initialPreset); + const auto restoredResult = nbl::core::applyPresetDetailed(m_cameraGoalSolver, camera, initialPreset); if (!restoredResult.succeeded() || !comparePresetToCamera(camera, initialPreset, 1e-6, 0.1, 1e-6)) return fail("Preset dynamic perspective restore smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\". " + describeApplyResult(restoredResult) + " " + describePresetMismatch(camera, initialPreset)); } @@ -905,16 +965,16 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) if (!manipulateAndMeasure(camera, directEvents, directPosDelta, directRotDelta)) return fail("Direct manipulate smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\"."); { - const auto modifiedPreset = nbl::hlsl::capturePreset(m_cameraGoalSolver, camera, "smoke-direct"); - const auto restoreInitial = nbl::hlsl::applyPresetDetailed(m_cameraGoalSolver, camera, initialPreset); + const auto modifiedPreset = nbl::core::capturePreset(m_cameraGoalSolver, camera, "smoke-direct"); + const auto restoreInitial = nbl::core::applyPresetDetailed(m_cameraGoalSolver, camera, initialPreset); if (!restoreInitial.succeeded() || !comparePresetToCamera(camera, initialPreset, 1e-3, 0.1, 1e-4)) return fail("Preset restore from direct smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\". " + describePresetMismatch(camera, initialPreset)); - const auto applyModified = nbl::hlsl::applyPresetDetailed(m_cameraGoalSolver, camera, modifiedPreset); + const auto applyModified = nbl::core::applyPresetDetailed(m_cameraGoalSolver, camera, modifiedPreset); if (!applyModified.succeeded() || !applyModified.changed() || !comparePresetToCamera(camera, modifiedPreset, 1e-3, 0.1, 1e-4)) return fail("Preset apply from direct smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\". " + describePresetMismatch(camera, modifiedPreset)); - const auto restoreAgain = nbl::hlsl::applyPresetDetailed(m_cameraGoalSolver, camera, initialPreset); + const auto restoreAgain = nbl::core::applyPresetDetailed(m_cameraGoalSolver, camera, initialPreset); if (!restoreAgain.succeeded() || !comparePresetToCamera(camera, initialPreset, 1e-3, 0.1, 1e-4)) return fail("Preset final restore smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\". " + describePresetMismatch(camera, initialPreset)); } @@ -924,7 +984,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) double keyboardRotDelta = 0.0; for (const auto key : keyboardCandidates) { - camera->copyDefaultInputBindingPresetTo(inputBinder); + applyDefaultCameraInputBindingPreset(inputBinder, *camera); auto keyboardEvents = collectKeyboardVirtualEvents(inputBinder, key); if (keyboardEvents.empty()) continue; @@ -937,7 +997,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) if (!keyboardOk) return fail("Keyboard binding smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\"."); - const auto mousePreset = camera->getMouseMappingPreset(); + const auto& mousePreset = getDefaultCameraMouseMappingPreset(*camera); const bool hasMoveMapping = mousePreset.find(ui::EMC_RELATIVE_POSITIVE_MOVEMENT_X) != mousePreset.end() || mousePreset.find(ui::EMC_RELATIVE_NEGATIVE_MOVEMENT_X) != mousePreset.end() || @@ -966,7 +1026,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) if (isOrbitLikeCamera(camera) && hasBlockedMovement) return fail("Orbit mouse movement gate failed for camera \"" + std::string(camera->getIdentifier()) + "\"."); - camera->copyDefaultInputBindingPresetTo(inputBinder); + applyDefaultCameraInputBindingPreset(inputBinder, *camera); auto mouseMoveEvents = collectMouseVirtualEvents(inputBinder, { filteredMoveLookDown.data(), filteredMoveLookDown.size() }); if (mouseMoveEvents.empty()) return fail("Mouse move virtual events missing for camera \"" + std::string(camera->getIdentifier()) + "\"."); @@ -986,7 +1046,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) const std::array rawScroll = { scrollEv }; auto filteredScroll = filterOrbitMouseEvents(camera, rawScroll, false); - camera->copyDefaultInputBindingPresetTo(inputBinder); + applyDefaultCameraInputBindingPreset(inputBinder, *camera); auto mouseScrollEvents = collectMouseVirtualEvents(inputBinder, { filteredScroll.data(), filteredScroll.size() }); if (mouseScrollEvents.empty()) return fail("Mouse scroll virtual events missing for camera \"" + std::string(camera->getIdentifier()) + "\"."); @@ -1046,12 +1106,12 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) ". missing=" + describeGoalStateMask(compatibility.missingGoalStateMask)); } - const auto baselinePreset = nbl::hlsl::capturePreset(m_cameraGoalSolver, targetCamera, std::string(label) + "-baseline"); - const auto applyResult = nbl::hlsl::applyPresetDetailed(m_cameraGoalSolver, targetCamera, sourcePreset); + const auto baselinePreset = nbl::core::capturePreset(m_cameraGoalSolver, targetCamera, std::string(label) + "-baseline"); + const auto applyResult = nbl::core::applyPresetDetailed(m_cameraGoalSolver, targetCamera, sourcePreset); if (!applyResult.succeeded() || !applyResult.approximate() || !applyResult.hasIssue(expectedIssue)) return fail(std::string("Cross-kind preset smoke failed for ") + label + ". " + describeApplyResult(applyResult)); - const auto restoreResult = nbl::hlsl::applyPresetDetailed(m_cameraGoalSolver, targetCamera, baselinePreset); + const auto restoreResult = nbl::core::applyPresetDetailed(m_cameraGoalSolver, targetCamera, baselinePreset); if (!restoreResult.succeeded() || !comparePresetToCamera(targetCamera, baselinePreset, 1e-6, 0.1, 1e-6)) return fail(std::string("Cross-kind preset restore smoke failed for ") + label + ". " + describeApplyResult(restoreResult) + " " + describePresetMismatch(targetCamera, baselinePreset)); @@ -1070,15 +1130,15 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) ". missing=" + describeGoalStateMask(compatibility.missingGoalStateMask)); } - const auto baselinePreset = nbl::hlsl::capturePreset(m_cameraGoalSolver, targetCamera, std::string(label) + "-baseline"); - const auto applyResult = nbl::hlsl::applyPresetDetailed(m_cameraGoalSolver, targetCamera, sourcePreset); + const auto baselinePreset = nbl::core::capturePreset(m_cameraGoalSolver, targetCamera, std::string(label) + "-baseline"); + const auto applyResult = nbl::core::applyPresetDetailed(m_cameraGoalSolver, targetCamera, sourcePreset); if (!applyResult.succeeded() || !applyResult.exact || !comparePresetToCamera(targetCamera, sourcePreset, 1e-6, 0.1, 1e-6)) { return fail(std::string("Exact cross-kind preset smoke failed for ") + label + ". " + describeApplyResult(applyResult) + " " + describePresetMismatch(targetCamera, sourcePreset)); } - const auto restoreResult = nbl::hlsl::applyPresetDetailed(m_cameraGoalSolver, targetCamera, baselinePreset); + const auto restoreResult = nbl::core::applyPresetDetailed(m_cameraGoalSolver, targetCamera, baselinePreset); if (!restoreResult.succeeded() || !restoreResult.exact || !comparePresetToCamera(targetCamera, baselinePreset, 1e-6, 0.1, 1e-6)) { return fail(std::string("Exact cross-kind preset restore smoke failed for ") + label + ". " + @@ -1097,20 +1157,20 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) { CTrackedTarget trackedTarget( float64_t3(2.25, -0.75, 1.25), - glm::quat(glm::dvec3(0.18, -0.22, 0.41)), + makeQuaternionFromEulerRadians(float64_t3(0.18, -0.22, 0.41)), "Smoke Target"); const auto movedTrackedTargetPosition = float64_t3(-1.5, 0.5, 2.25); - const auto movedTrackedTargetOrientation = glm::quat(glm::dvec3(-0.12, 0.35, 0.27)); + const auto movedTrackedTargetOrientation = makeQuaternionFromEulerRadians(float64_t3(-0.12, 0.35, 0.27)); if (orbitCamera) { - const auto baselinePreset = nbl::hlsl::capturePreset(m_cameraGoalSolver, orbitCamera, "orbit-follow-baseline"); + const auto baselinePreset = nbl::core::capturePreset(m_cameraGoalSolver, orbitCamera, "orbit-follow-baseline"); SCameraFollowConfig followConfig = {}; followConfig.enabled = true; followConfig.mode = ECameraFollowMode::OrbitTarget; - nbl::hlsl::SCameraFollowApplyValidationResult followResult = {}; + nbl::core::SCameraFollowApplyValidationResult followResult = {}; if (!buildAndValidateFollowTargetContract(orbitCamera, trackedTarget, followConfig, "orbit follow", followResult)) return false; if (!verifyFollowVisualMetrics(orbitCamera, trackedTarget, followConfig, "orbit follow")) @@ -1118,7 +1178,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) if (!verifyFollowTargetMarkerAlignment(trackedTarget, "orbit follow")) return false; - const auto restoreResult = nbl::hlsl::applyPresetDetailed(m_cameraGoalSolver, orbitCamera, baselinePreset); + const auto restoreResult = nbl::core::applyPresetDetailed(m_cameraGoalSolver, orbitCamera, baselinePreset); if (!restoreResult.succeeded() || !comparePresetToCamera(orbitCamera, baselinePreset, 1e-6, 0.1, 1e-6)) return fail("Orbit follow smoke failed to restore the baseline preset."); @@ -1126,13 +1186,13 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) followConfig.worldOffset = float64_t3(4.0, -1.5, 2.0); trackedTarget.setPose(movedTrackedTargetPosition, movedTrackedTargetOrientation); - nbl::hlsl::SCameraFollowApplyValidationResult worldOffsetResult = {}; + nbl::core::SCameraFollowApplyValidationResult worldOffsetResult = {}; if (!buildAndValidateFollowTargetContract(orbitCamera, trackedTarget, followConfig, "orbit keep-world-offset follow", worldOffsetResult)) return false; if (!verifyFollowVisualMetrics(orbitCamera, trackedTarget, followConfig, "orbit keep-world-offset follow")) return false; - const auto restoreWorldOffsetResult = nbl::hlsl::applyPresetDetailed(m_cameraGoalSolver, orbitCamera, baselinePreset); + const auto restoreWorldOffsetResult = nbl::core::applyPresetDetailed(m_cameraGoalSolver, orbitCamera, baselinePreset); if (!restoreWorldOffsetResult.succeeded() || !comparePresetToCamera(orbitCamera, baselinePreset, 1e-6, 0.1, 1e-6)) return fail("Orbit keep-world-offset smoke failed to restore the baseline preset."); } @@ -1148,18 +1208,18 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) continue; const auto label = std::string(defaultFollowCamera->getIdentifier()) + " default follow"; - const auto baselinePreset = nbl::hlsl::capturePreset(m_cameraGoalSolver, defaultFollowCamera, label + " baseline"); + const auto baselinePreset = nbl::core::capturePreset(m_cameraGoalSolver, defaultFollowCamera, label + " baseline"); - trackedTarget.setPose(float64_t3(2.25, -0.75, 1.25), glm::quat(glm::dvec3(0.18, -0.22, 0.41))); - if ((nbl::hlsl::cameraFollowModeUsesLocalOffset(followConfig.mode) || nbl::hlsl::cameraFollowModeUsesWorldOffset(followConfig.mode)) && - !nbl::hlsl::captureFollowOffsetsFromCamera(m_cameraGoalSolver, defaultFollowCamera, trackedTarget, followConfig)) + trackedTarget.setPose(float64_t3(2.25, -0.75, 1.25), makeQuaternionFromEulerRadians(float64_t3(0.18, -0.22, 0.41))); + if ((nbl::core::cameraFollowModeUsesLocalOffset(followConfig.mode) || nbl::core::cameraFollowModeUsesWorldOffset(followConfig.mode)) && + !nbl::core::captureFollowOffsetsFromCamera(m_cameraGoalSolver, defaultFollowCamera, trackedTarget, followConfig)) { return fail("Default follow smoke failed to capture offsets for camera \"" + std::string(defaultFollowCamera->getIdentifier()) + "\"."); } trackedTarget.setPose(movedTrackedTargetPosition, movedTrackedTargetOrientation); - nbl::hlsl::SCameraFollowApplyValidationResult defaultFollowResult = {}; + nbl::core::SCameraFollowApplyValidationResult defaultFollowResult = {}; if (!buildAndValidateFollowTargetContract(defaultFollowCamera, trackedTarget, followConfig, label.c_str(), defaultFollowResult)) return false; if (!verifyFollowVisualMetrics(defaultFollowCamera, trackedTarget, followConfig, label.c_str())) @@ -1167,19 +1227,19 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) if (!verifyFollowTargetMarkerAlignment(trackedTarget, label.c_str())) return false; - const auto restoreResult = nbl::hlsl::applyPresetDetailed(m_cameraGoalSolver, defaultFollowCamera, baselinePreset); + const auto restoreResult = nbl::core::applyPresetDetailed(m_cameraGoalSolver, defaultFollowCamera, baselinePreset); if (!restoreResult.succeeded() || !comparePresetToCamera(defaultFollowCamera, baselinePreset, 1e-6, 0.1, 1e-6)) return fail("Default follow smoke failed to restore the baseline preset for camera \"" + std::string(defaultFollowCamera->getIdentifier()) + "\"."); } if (freeCamera) { - const auto baselinePreset = nbl::hlsl::capturePreset(m_cameraGoalSolver, freeCamera, "free-follow-baseline"); + const auto baselinePreset = nbl::core::capturePreset(m_cameraGoalSolver, freeCamera, "free-follow-baseline"); SCameraFollowConfig followConfig = {}; followConfig.enabled = true; followConfig.mode = ECameraFollowMode::LookAtTarget; - nbl::hlsl::SCameraFollowApplyValidationResult lookAtResult = {}; + nbl::core::SCameraFollowApplyValidationResult lookAtResult = {}; if (!buildAndValidateFollowTargetContract(freeCamera, trackedTarget, followConfig, "free look-at follow", lookAtResult)) return false; if (!verifyFollowVisualMetrics(freeCamera, trackedTarget, followConfig, "free look-at follow")) @@ -1187,7 +1247,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) if (!verifyFollowTargetMarkerAlignment(trackedTarget, "free look-at follow")) return false; - const auto restoreResult = nbl::hlsl::applyPresetDetailed(m_cameraGoalSolver, freeCamera, baselinePreset); + const auto restoreResult = nbl::core::applyPresetDetailed(m_cameraGoalSolver, freeCamera, baselinePreset); if (!restoreResult.succeeded() || !comparePresetToCamera(freeCamera, baselinePreset, 1e-6, 0.1, 1e-6)) return fail("Free follow smoke failed to restore the baseline preset."); @@ -1195,29 +1255,29 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) followConfig.worldOffset = float64_t3(5.0, -2.0, 1.5); trackedTarget.setPose(movedTrackedTargetPosition, movedTrackedTargetOrientation); - nbl::hlsl::SCameraFollowApplyValidationResult keepWorldResult = {}; + nbl::core::SCameraFollowApplyValidationResult keepWorldResult = {}; if (!buildAndValidateFollowTargetContract(freeCamera, trackedTarget, followConfig, "free keep-world-offset follow", keepWorldResult)) return false; if (!verifyFollowVisualMetrics(freeCamera, trackedTarget, followConfig, "free keep-world-offset follow")) return false; - const auto restoreWorldOffsetResult = nbl::hlsl::applyPresetDetailed(m_cameraGoalSolver, freeCamera, baselinePreset); + const auto restoreWorldOffsetResult = nbl::core::applyPresetDetailed(m_cameraGoalSolver, freeCamera, baselinePreset); if (!restoreWorldOffsetResult.succeeded() || !comparePresetToCamera(freeCamera, baselinePreset, 1e-6, 0.1, 1e-6)) return fail("Free keep-world-offset smoke failed to restore the baseline preset."); } if (chaseCamera) { - const auto baselinePreset = nbl::hlsl::capturePreset(m_cameraGoalSolver, chaseCamera, "chase-follow-baseline"); + const auto baselinePreset = nbl::core::capturePreset(m_cameraGoalSolver, chaseCamera, "chase-follow-baseline"); SCameraFollowConfig followConfig = {}; followConfig.enabled = true; followConfig.mode = ECameraFollowMode::KeepLocalOffset; - if (!nbl::hlsl::captureFollowOffsetsFromCamera(m_cameraGoalSolver, chaseCamera, trackedTarget, followConfig)) + if (!nbl::core::captureFollowOffsetsFromCamera(m_cameraGoalSolver, chaseCamera, trackedTarget, followConfig)) return fail("Chase follow smoke failed to capture local offset."); trackedTarget.setPose(movedTrackedTargetPosition, movedTrackedTargetOrientation); - nbl::hlsl::SCameraFollowApplyValidationResult localOffsetResult = {}; + nbl::core::SCameraFollowApplyValidationResult localOffsetResult = {}; if (!buildAndValidateFollowTargetContract(chaseCamera, trackedTarget, followConfig, "chase local-offset follow", localOffsetResult)) return false; if (!verifyFollowVisualMetrics(chaseCamera, trackedTarget, followConfig, "chase local-offset follow")) @@ -1225,7 +1285,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) if (!verifyFollowTargetMarkerAlignment(trackedTarget, "chase local-offset follow")) return false; - const auto restoreResult = nbl::hlsl::applyPresetDetailed(m_cameraGoalSolver, chaseCamera, baselinePreset); + const auto restoreResult = nbl::core::applyPresetDetailed(m_cameraGoalSolver, chaseCamera, baselinePreset); if (!restoreResult.succeeded() || !comparePresetToCamera(chaseCamera, baselinePreset, 1e-6, 0.1, 1e-6)) return fail("Chase follow smoke failed to restore the baseline preset."); } @@ -1278,14 +1338,14 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) if (hasOrbitPreset) { - if (std::string_view(nbl::hlsl::getPresetApplyPresentationFilterLabel(EPresetApplyPresentationFilter::All)) != "All" || - std::string_view(nbl::hlsl::getPresetApplyPresentationFilterLabel(EPresetApplyPresentationFilter::Exact)) != "Exact" || - std::string_view(nbl::hlsl::getPresetApplyPresentationFilterLabel(EPresetApplyPresentationFilter::BestEffort)) != "Best-effort") + if (std::string_view(nbl::core::getPresetApplyPresentationFilterLabel(EPresetApplyPresentationFilter::All)) != "All" || + std::string_view(nbl::core::getPresetApplyPresentationFilterLabel(EPresetApplyPresentationFilter::Exact)) != "Exact" || + std::string_view(nbl::core::getPresetApplyPresentationFilterLabel(EPresetApplyPresentationFilter::BestEffort)) != "Best-effort") { return fail("Presentation utilities smoke returned an unexpected filter label."); } - const auto blockedPresentation = nbl::hlsl::analyzePresetPresentation(m_cameraGoalSolver, nullptr, initialOrbitPreset); + const auto blockedPresentation = nbl::core::analyzePresetPresentation(m_cameraGoalSolver, nullptr, initialOrbitPreset); if (blockedPresentation.matchesFilter(EPresetApplyPresentationFilter::Exact) || blockedPresentation.matchesFilter(EPresetApplyPresentationFilter::BestEffort)) { @@ -1294,13 +1354,13 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) if (blockedPresentation.sourceKindLabel.empty() || blockedPresentation.goalStateLabel.empty()) return fail("Presentation utilities smoke produced empty blocked presentation labels."); - const auto blockedBadges = nbl::hlsl::collectGoalApplyPresentationBadges(blockedPresentation); + const auto blockedBadges = nbl::core::collectGoalApplyPresentationBadges(blockedPresentation); if (!blockedBadges.blocked || blockedBadges.exact || blockedBadges.bestEffort || blockedPresentation.badges.blocked != blockedBadges.blocked) return fail("Presentation utilities smoke produced wrong blocked badge flags."); if (orbitCamera) { - const auto exactPresentation = nbl::hlsl::analyzePresetPresentation(m_cameraGoalSolver, orbitCamera, initialOrbitPreset); + const auto exactPresentation = nbl::core::analyzePresetPresentation(m_cameraGoalSolver, orbitCamera, initialOrbitPreset); if (!exactPresentation.matchesFilter(EPresetApplyPresentationFilter::All) || !exactPresentation.matchesFilter(EPresetApplyPresentationFilter::Exact) || exactPresentation.matchesFilter(EPresetApplyPresentationFilter::BestEffort)) @@ -1308,13 +1368,13 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) return fail("Presentation utilities smoke failed exact filtering."); } - const auto exactBadges = nbl::hlsl::collectGoalApplyPresentationBadges(exactPresentation); + const auto exactBadges = nbl::core::collectGoalApplyPresentationBadges(exactPresentation); if (!exactBadges.exact || exactBadges.bestEffort || exactBadges.dropsState || exactBadges.sharedStateOnly || exactBadges.blocked) return fail("Presentation utilities smoke produced wrong exact badge flags."); if (exactPresentation.sourceKindLabel.empty() || exactPresentation.goalStateLabel.empty()) return fail("Presentation utilities smoke produced empty exact presentation labels."); - const auto capturePresentation = nbl::hlsl::analyzeCapturePresentation(m_cameraGoalSolver, orbitCamera); + const auto capturePresentation = nbl::core::analyzeCapturePresentation(m_cameraGoalSolver, orbitCamera); if (!capturePresentation.canCapture || capturePresentation.policyLabel.empty()) return fail("Presentation utilities smoke failed orbit capture presentation."); } @@ -1322,7 +1382,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) if (hasOrbitPreset && hasPathPreset && orbitCamera) { - const auto approximatePresentation = nbl::hlsl::analyzePresetPresentation(m_cameraGoalSolver, orbitCamera, initialPathPreset); + const auto approximatePresentation = nbl::core::analyzePresetPresentation(m_cameraGoalSolver, orbitCamera, initialPathPreset); if (!approximatePresentation.matchesFilter(EPresetApplyPresentationFilter::All) || approximatePresentation.matchesFilter(EPresetApplyPresentationFilter::Exact) || !approximatePresentation.matchesFilter(EPresetApplyPresentationFilter::BestEffort)) @@ -1330,7 +1390,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) return fail("Presentation utilities smoke failed best-effort filtering."); } - const auto approximateBadges = nbl::hlsl::collectGoalApplyPresentationBadges(approximatePresentation); + const auto approximateBadges = nbl::core::collectGoalApplyPresentationBadges(approximatePresentation); if (approximateBadges.exact || !approximateBadges.bestEffort || !approximateBadges.dropsState || approximateBadges.sharedStateOnly || approximateBadges.blocked) return fail("Presentation utilities smoke produced wrong best-effort badge flags."); if (approximatePresentation.sourceKindLabel.empty() || approximatePresentation.goalStateLabel.empty()) @@ -1354,13 +1414,13 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) return fail("Preset persistence smoke failed to collect source presets."); std::stringstream presetBuffer; - if (!nbl::hlsl::writePresetCollection(presetBuffer, std::span(sourcePresets.data(), sourcePresets.size()))) + if (!nbl::system::writePresetCollection(presetBuffer, std::span(sourcePresets.data(), sourcePresets.size()))) return fail("Preset persistence smoke failed to serialize preset collection."); std::vector loadedPresets; - if (!nbl::hlsl::readPresetCollection(presetBuffer, loadedPresets)) + if (!nbl::system::readPresetCollection(presetBuffer, loadedPresets)) return fail("Preset persistence smoke failed to deserialize preset collection."); - if (!nbl::hlsl::comparePresetCollections( + if (!nbl::core::comparePresetCollections( std::span(sourcePresets.data(), sourcePresets.size()), std::span(loadedPresets.data(), loadedPresets.size()), 1e-6, 0.1, 1e-6)) @@ -1380,13 +1440,13 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) sourceTrack.selectedKeyframeIx = static_cast(sourceTrack.keyframes.size()) - 1; std::stringstream keyframeBuffer; - if (!nbl::hlsl::writeKeyframeTrack(keyframeBuffer, sourceTrack)) + if (!nbl::system::writeKeyframeTrack(keyframeBuffer, sourceTrack)) return fail("Keyframe persistence smoke failed to serialize track."); CCameraKeyframeTrack loadedTrack; - if (!nbl::hlsl::readKeyframeTrack(keyframeBuffer, loadedTrack)) + if (!nbl::system::readKeyframeTrack(keyframeBuffer, loadedTrack)) return fail("Keyframe persistence smoke failed to deserialize track."); - if (!nbl::hlsl::compareKeyframeTrackContent(sourceTrack, loadedTrack, 1e-6, 1e-6, 0.1, 1e-6)) + if (!nbl::core::compareKeyframeTrackContent(sourceTrack, loadedTrack, 1e-6, 1e-6, 0.1, 1e-6)) return fail("Keyframe persistence smoke changed stream track content."); struct TempFileCleanup final @@ -1406,13 +1466,13 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) const auto keyframeFile = tempDir / ("nabla_cameraz_keyframes_" + uniqueSuffix + ".json"); tempFiles.paths = { presetFile, keyframeFile }; - if (!nbl::hlsl::savePresetCollectionToFile(presetFile, std::span(sourcePresets.data(), sourcePresets.size()))) + if (!nbl::system::savePresetCollectionToFile(presetFile, std::span(sourcePresets.data(), sourcePresets.size()))) return fail("Preset persistence smoke failed to save preset collection file."); std::vector fileLoadedPresets; - if (!nbl::hlsl::loadPresetCollectionFromFile(presetFile, fileLoadedPresets)) + if (!nbl::system::loadPresetCollectionFromFile(presetFile, fileLoadedPresets)) return fail("Preset persistence smoke failed to load preset collection file."); - if (!nbl::hlsl::comparePresetCollections( + if (!nbl::core::comparePresetCollections( std::span(sourcePresets.data(), sourcePresets.size()), std::span(fileLoadedPresets.data(), fileLoadedPresets.size()), 1e-6, 0.1, 1e-6)) @@ -1420,13 +1480,13 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) return fail("Preset persistence smoke changed file preset collection content."); } - if (!nbl::hlsl::saveKeyframeTrackToFile(keyframeFile, sourceTrack)) + if (!nbl::system::saveKeyframeTrackToFile(keyframeFile, sourceTrack)) return fail("Keyframe persistence smoke failed to save track file."); CCameraKeyframeTrack fileLoadedTrack; - if (!nbl::hlsl::loadKeyframeTrackFromFile(keyframeFile, fileLoadedTrack)) + if (!nbl::system::loadKeyframeTrackFromFile(keyframeFile, fileLoadedTrack)) return fail("Keyframe persistence smoke failed to load track file."); - if (!nbl::hlsl::compareKeyframeTrackContent(sourceTrack, fileLoadedTrack, 1e-6, 1e-6, 0.1, 1e-6)) + if (!nbl::core::compareKeyframeTrackContent(sourceTrack, fileLoadedTrack, 1e-6, 1e-6, 0.1, 1e-6)) return fail("Keyframe persistence smoke changed file track content."); } @@ -1452,13 +1512,13 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) cursor.speed = 1.f; cursor.time = 1.5f; - const auto advanceToEnd = nbl::hlsl::advancePlaybackCursor(cursor, playbackTrack, 1.0); + const auto advanceToEnd = nbl::core::advancePlaybackCursor(cursor, playbackTrack, 1.0); if (!advanceToEnd.hasTrack || !advanceToEnd.changedTime || !advanceToEnd.reachedEnd || !advanceToEnd.stopped || advanceToEnd.wrapped) return fail("Playback timeline smoke failed for non-loop end-of-track advance."); if (std::abs(static_cast(advanceToEnd.time - 2.f)) > 1e-6) return fail("Playback timeline smoke produced wrong end-of-track time."); - nbl::hlsl::resetPlaybackCursor(cursor, 1.25f); + nbl::core::resetPlaybackCursor(cursor, 1.25f); if (cursor.playing || std::abs(static_cast(cursor.time - 1.25f)) > 1e-6) return fail("Playback timeline smoke failed to reset cursor."); @@ -1466,14 +1526,14 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) cursor.loop = true; cursor.speed = 1.f; cursor.time = 1.5f; - const auto advanceLoop = nbl::hlsl::advancePlaybackCursor(cursor, playbackTrack, 1.0); + const auto advanceLoop = nbl::core::advancePlaybackCursor(cursor, playbackTrack, 1.0); if (!advanceLoop.hasTrack || !advanceLoop.changedTime || !advanceLoop.wrapped || advanceLoop.stopped || advanceLoop.reachedEnd) return fail("Playback timeline smoke failed for looped advance."); if (std::abs(static_cast(advanceLoop.time - 0.5f)) > 1e-6) return fail("Playback timeline smoke produced wrong wrapped time."); cursor.time = 9.f; - nbl::hlsl::clampPlaybackCursorToTrack(playbackTrack, cursor); + nbl::core::clampPlaybackCursorToTrack(playbackTrack, cursor); if (std::abs(static_cast(cursor.time - 2.f)) > 1e-6) return fail("Playback timeline smoke failed to clamp cursor time."); } @@ -1500,21 +1560,21 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) segment.keyframes.push_back(keyframe); } { - nbl::hlsl::CCameraSequenceTrackedTargetKeyframe keyframe; + nbl::core::CCameraSequenceTrackedTargetKeyframe keyframe; keyframe.time = 0.f; keyframe.hasAbsolutePosition = true; keyframe.absolutePosition = float64_t3(1.0, 2.0, 3.0); segment.targetKeyframes.push_back(keyframe); } { - nbl::hlsl::CCameraSequenceTrackedTargetKeyframe keyframe; + nbl::core::CCameraSequenceTrackedTargetKeyframe keyframe; keyframe.time = 1.f; keyframe.hasAbsolutePosition = true; keyframe.absolutePosition = float64_t3(4.0, 5.0, 6.0); segment.targetKeyframes.push_back(keyframe); } { - nbl::hlsl::CCameraSequenceTrackedTargetKeyframe keyframe; + nbl::core::CCameraSequenceTrackedTargetKeyframe keyframe; keyframe.time = 1.f; keyframe.hasAbsolutePosition = true; keyframe.absolutePosition = float64_t3(7.0, 8.0, 9.0); @@ -1522,7 +1582,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) } sequence.segments.push_back(segment); - if (!nbl::hlsl::sequenceScriptUsesMultiplePresentations(sequence)) + if (!nbl::core::sequenceScriptUsesMultiplePresentations(sequence)) return fail("Sequence compile smoke failed to detect multi-presentation authored defaults."); const CCameraSequenceTrackedTargetPose referenceTrackedTargetPose = { @@ -1530,9 +1590,9 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) .orientation = getDefaultFollowTargetOrientation() }; - nbl::hlsl::CCameraSequenceCompiledSegment compiledSegment; + nbl::core::CCameraSequenceCompiledSegment compiledSegment; std::string compileError; - if (!nbl::hlsl::compileSequenceSegmentFromReference( + if (!nbl::core::compileSequenceSegmentFromReference( sequence, sequence.segments.front(), initialOrbitPreset, @@ -1557,8 +1617,8 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) if (!compiledSegment.usesTrackedTargetTrack() || compiledSegment.trackedTargetTrack.keyframes.size() != 2u) return fail("Sequence compile smoke failed to normalize tracked-target keyframes."); - std::vector framePolicies; - if (!nbl::hlsl::buildCompiledSegmentFramePolicies(compiledSegment, framePolicies, true)) + std::vector framePolicies; + if (!nbl::core::buildCompiledSegmentFramePolicies(compiledSegment, framePolicies, true)) return fail("Sequence compile smoke failed to build shared frame policies."); if (framePolicies.size() != 8u) return fail("Sequence compile smoke produced wrong frame-policy count."); @@ -1570,14 +1630,14 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) return fail("Sequence compile smoke produced wrong capture milestone policy."); CCameraSequenceTrackedTargetPose poseAtOne; - if (!nbl::hlsl::tryBuildSequenceTrackedTargetPoseAtTime(compiledSegment.trackedTargetTrack, 1.f, poseAtOne)) + if (!nbl::core::tryBuildSequenceTrackedTargetPoseAtTime(compiledSegment.trackedTargetTrack, 1.f, poseAtOne)) return fail("Sequence compile smoke failed to sample normalized tracked-target track."); if (length(poseAtOne.position - float64_t3(7.0, 8.0, 9.0)) > 1e-9) return fail("Sequence compile smoke did not keep the last authored target pose for duplicate keyframe time."); CCameraScriptedTimeline scriptedTimeline; std::string runtimeBuildError; - if (!nbl::hlsl::appendCompiledSequenceSegmentToScriptedTimeline( + if (!nbl::system::appendCompiledSequenceSegmentToScriptedTimeline( scriptedTimeline, 11u, compiledSegment, @@ -1591,7 +1651,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) { return fail("Sequence runtime builder smoke failed to append a compiled segment. " + runtimeBuildError); } - nbl::hlsl::finalizeScriptedTimeline(scriptedTimeline); + nbl::system::finalizeScriptedTimeline(scriptedTimeline); if (scriptedTimeline.captureFrames.size() != 3u || scriptedTimeline.captureFrames[0] != 11ull || @@ -1626,7 +1686,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) size_t runtimeNextEventIndex = 0u; CCameraScriptedFrameEvents runtimeBatch; - nbl::hlsl::dequeueScriptedFrameEvents(scriptedTimeline.events, runtimeNextEventIndex, 11u, runtimeBatch); + nbl::system::dequeueScriptedFrameEvents(scriptedTimeline.events, runtimeNextEventIndex, 11u, runtimeBatch); if (runtimeBatch.actions.size() != 10u || runtimeBatch.goals.size() != 1u || runtimeBatch.trackedTargetTransforms.size() != 1u || runtimeBatch.segmentLabels.size() != 1u) { @@ -1642,7 +1702,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) if (hasOrbitPreset && orbitCamera) { std::array exactTargets = { orbitCamera, nullptr }; - const auto exactSummary = nbl::hlsl::applyPresetToCameraRange( + const auto exactSummary = nbl::core::applyPresetToCameraRange( m_cameraGoalSolver, std::span(exactTargets.data(), exactTargets.size()), initialOrbitPreset); @@ -1653,7 +1713,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) if (hasPathPreset && orbitCamera) { std::array approximateTargets = { orbitCamera }; - const auto approximateSummary = nbl::hlsl::applyPresetToCameraRange( + const auto approximateSummary = nbl::core::applyPresetToCameraRange( m_cameraGoalSolver, std::span(approximateTargets.data(), approximateTargets.size()), initialPathPreset); @@ -1669,7 +1729,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) scaledEvents[1].magnitude = 3.0; scaledEvents[2].type = CVirtualGimbalEvent::ScaleXInc; scaledEvents[2].magnitude = 4.0; - nbl::hlsl::scaleVirtualEvents(scaledEvents, static_cast(scaledEvents.size()), 0.5f, 2.0f); + nbl::core::scaleVirtualEvents(scaledEvents, static_cast(scaledEvents.size()), 0.5f, 2.0f); if (std::abs(scaledEvents[0].magnitude - 1.0) > 1e-9 || std::abs(scaledEvents[1].magnitude - 6.0) > 1e-9 || std::abs(scaledEvents[2].magnitude - 4.0) > 1e-9) @@ -1681,8 +1741,8 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) if (hasFreePreset && freeCamera) { CameraPreset orientedPreset = initialFreePreset; - orientedPreset.goal.orientation = glm::quat(hlsl::radians(float32_t3(0.f, 90.f, 0.f))); - const auto orientResult = nbl::hlsl::applyPresetDetailed(m_cameraGoalSolver, freeCamera, orientedPreset); + orientedPreset.goal.orientation = makeQuaternionFromEulerDegrees(float64_t3(0.0, 90.0, 0.0)); + const auto orientResult = nbl::core::applyPresetDetailed(m_cameraGoalSolver, freeCamera, orientedPreset); if (!orientResult.succeeded() || !comparePresetToCamera(freeCamera, orientedPreset, 1e-6, 0.1, 1e-6)) return fail("Camera manipulation utilities smoke failed to orient Free camera before translation remap."); @@ -1694,7 +1754,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) worldTranslationEvents[2].type = CVirtualGimbalEvent::MoveForward; worldTranslationEvents[2].magnitude = 2.0; uint32_t remappedCount = static_cast(worldTranslationEvents.size()); - nbl::hlsl::remapTranslationEventsFromWorldToCameraLocal(freeCamera, worldTranslationEvents, remappedCount); + nbl::core::remapTranslationEventsFromWorldToCameraLocal(freeCamera, worldTranslationEvents, remappedCount); if (remappedCount == 0u) return fail("Camera manipulation utilities smoke produced empty translation remap."); @@ -1708,8 +1768,8 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) return fail("Camera manipulation utilities smoke changed world-space translation semantics."); CameraPreset pitchPreset = initialFreePreset; - pitchPreset.goal.orientation = glm::quat(hlsl::radians(float32_t3(60.f, 0.f, 0.f))); - const auto pitchResult = nbl::hlsl::applyPresetDetailed(m_cameraGoalSolver, freeCamera, pitchPreset); + pitchPreset.goal.orientation = makeQuaternionFromEulerDegrees(float64_t3(60.0, 0.0, 0.0)); + const auto pitchResult = nbl::core::applyPresetDetailed(m_cameraGoalSolver, freeCamera, pitchPreset); if (!pitchResult.succeeded()) return fail("Camera manipulation utilities smoke failed to prepare Free camera pitch clamp."); @@ -1718,14 +1778,14 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) freeConstraints.clampPitch = true; freeConstraints.pitchMinDeg = -15.f; freeConstraints.pitchMaxDeg = 15.f; - if (!nbl::hlsl::applyCameraConstraints(m_cameraGoalSolver, freeCamera, freeConstraints)) + if (!nbl::core::applyCameraConstraints(m_cameraGoalSolver, freeCamera, freeConstraints)) return fail("Camera manipulation utilities smoke failed to clamp Free camera orientation."); - const auto freeEulerDeg = glm::degrees(glm::eulerAngles(freeCamera->getGimbal().getOrientation())); + const auto freeEulerDeg = getQuaternionEulerDegrees(freeCamera->getGimbal().getOrientation()); if (std::abs(static_cast(freeEulerDeg.x - 15.f)) > 0.1) return fail("Camera manipulation utilities smoke produced wrong clamped Free camera pitch."); - const auto restoreFree = nbl::hlsl::applyPresetDetailed(m_cameraGoalSolver, freeCamera, initialFreePreset); + const auto restoreFree = nbl::core::applyPresetDetailed(m_cameraGoalSolver, freeCamera, initialFreePreset); if (!restoreFree.succeeded() || !comparePresetToCamera(freeCamera, initialFreePreset, 1e-6, 0.1, 1e-6)) return fail("Camera manipulation utilities smoke failed to restore Free camera baseline."); } @@ -1734,7 +1794,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) { CameraPreset farOrbitPreset = initialOrbitPreset; farOrbitPreset.goal.distance = initialOrbitPreset.goal.distance + 10.f; - const auto farOrbitResult = nbl::hlsl::applyPresetDetailed(m_cameraGoalSolver, orbitCamera, farOrbitPreset); + const auto farOrbitResult = nbl::core::applyPresetDetailed(m_cameraGoalSolver, orbitCamera, farOrbitPreset); if (!farOrbitResult.succeeded()) return fail("Camera manipulation utilities smoke failed to prepare Orbit distance clamp."); @@ -1743,7 +1803,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) orbitConstraints.clampDistance = true; orbitConstraints.minDistance = std::max(0.1f, initialOrbitPreset.goal.distance * 0.5f); orbitConstraints.maxDistance = initialOrbitPreset.goal.distance * 0.75f; - if (!nbl::hlsl::applyCameraConstraints(m_cameraGoalSolver, orbitCamera, orbitConstraints)) + if (!nbl::core::applyCameraConstraints(m_cameraGoalSolver, orbitCamera, orbitConstraints)) return fail("Camera manipulation utilities smoke failed to clamp Orbit distance."); ICamera::SphericalTargetState clampedOrbitState; @@ -1753,7 +1813,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) return fail("Camera manipulation utilities smoke produced wrong clamped Orbit distance."); } - const auto restoreOrbit = nbl::hlsl::applyPresetDetailed(m_cameraGoalSolver, orbitCamera, initialOrbitPreset); + const auto restoreOrbit = nbl::core::applyPresetDetailed(m_cameraGoalSolver, orbitCamera, initialOrbitPreset); if (!restoreOrbit.succeeded() || !comparePresetToCamera(orbitCamera, initialOrbitPreset, 1e-6, 0.1, 1e-6)) return fail("Camera manipulation utilities smoke failed to restore Orbit baseline."); } @@ -1765,13 +1825,13 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) return fail("Camera projection utilities smoke failed to query DollyZoom dynamic FOV."); auto perspectiveProjection = IPlanarProjection::CProjection::create(0.1f, 100.f, 60.f); - if (!nbl::hlsl::syncDynamicPerspectiveProjection(dollyZoomCamera, perspectiveProjection)) + if (!nbl::core::syncDynamicPerspectiveProjection(dollyZoomCamera, perspectiveProjection)) return fail("Camera projection utilities smoke failed to sync dynamic perspective projection."); if (std::abs(static_cast(perspectiveProjection.getParameters().m_planar.perspective.fov - dynamicFov)) > 1e-6) return fail("Camera projection utilities smoke produced wrong dynamic perspective FOV."); auto orthographicProjection = IPlanarProjection::CProjection::create(0.1f, 100.f, 10.f); - if (nbl::hlsl::syncDynamicPerspectiveProjection(dollyZoomCamera, orthographicProjection)) + if (nbl::core::syncDynamicPerspectiveProjection(dollyZoomCamera, orthographicProjection)) return fail("Camera projection utilities smoke unexpectedly synced orthographic projection."); } @@ -1794,7 +1854,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) summary.targetCount = 2u; summary.successCount = 2u; summary.approximateCount = 1u; - const auto summaryText = nbl::hlsl::describePresetApplySummary(summary, "none"); + const auto summaryText = nbl::core::describePresetApplySummary(summary, "none"); if (summaryText.find("targets=2") == std::string::npos || summaryText.find("approximate=1") == std::string::npos) return fail("Camera text utilities smoke failed for preset-apply summary description."); } @@ -1823,7 +1883,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) return false; { - smart_refctd_ptr examplesHeaderArch, examplesSourceArch, examplesBuildArch, thisExampleArch, thisExampleBuildArch; + smart_refctd_ptr examplesHeaderArch, examplesSourceArch, examplesBuildArch, thisExampleArch, thisExampleBuildArch; #ifdef NBL_EMBED_BUILTIN_RESOURCES examplesHeaderArch = core::make_smart_refctd_ptr(smart_refctd_ptr(m_logger)); examplesSourceArch = core::make_smart_refctd_ptr(smart_refctd_ptr(m_logger)); @@ -1837,12 +1897,12 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) thisExampleBuildArch = make_smart_refctd_ptr(smart_refctd_ptr(m_logger)); #endif #else - examplesHeaderArch = make_smart_refctd_ptr(localInputCWD/"../common/include/nbl/examples", smart_refctd_ptr(m_logger), m_system.get()); - examplesSourceArch = make_smart_refctd_ptr(localInputCWD/"../common/src/nbl/examples", smart_refctd_ptr(m_logger), m_system.get()); - examplesBuildArch = make_smart_refctd_ptr(NBL_EXAMPLES_BUILD_MOUNT_POINT, smart_refctd_ptr(m_logger), m_system.get()); - thisExampleArch = make_smart_refctd_ptr(localInputCWD/"app_resources", smart_refctd_ptr(m_logger), m_system.get()); + examplesHeaderArch = make_smart_refctd_ptr(localInputCWD/"../common/include/nbl/examples", smart_refctd_ptr(m_logger), m_system.get()); + examplesSourceArch = make_smart_refctd_ptr(localInputCWD/"../common/src/nbl/examples", smart_refctd_ptr(m_logger), m_system.get()); + examplesBuildArch = make_smart_refctd_ptr(NBL_EXAMPLES_BUILD_MOUNT_POINT, smart_refctd_ptr(m_logger), m_system.get()); + thisExampleArch = make_smart_refctd_ptr(localInputCWD/"app_resources", smart_refctd_ptr(m_logger), m_system.get()); #ifdef NBL_THIS_EXAMPLE_BUILD_MOUNT_POINT - thisExampleBuildArch = make_smart_refctd_ptr(NBL_THIS_EXAMPLE_BUILD_MOUNT_POINT, smart_refctd_ptr(m_logger), m_system.get()); + thisExampleBuildArch = make_smart_refctd_ptr(NBL_THIS_EXAMPLE_BUILD_MOUNT_POINT, smart_refctd_ptr(m_logger), m_system.get()); #endif #endif m_system->mount(std::move(examplesHeaderArch),"nbl/examples"); @@ -1857,7 +1917,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) { const std::optional cameraJsonFile = program.is_used("--file") ? program.get("--file") : std::optional(std::nullopt); - nbl_json j; + camera_json_t j; auto loadDefaultConfig = [&]() -> bool { #ifdef _NBL_THIS_EXAMPLE_BUILTIN_C_ARCHIVE_H_ @@ -1870,7 +1930,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) IFile::success_t result; config.resize(pFile->getSize()); pFile->read(result, config.data(), 0, pFile->getSize()); - j = nbl_json::parse(config); + j = camera_json_t::parse(config); return true; #else const auto fallbackPath = localInputCWD / "app_resources" / "cameras.json"; @@ -1898,18 +1958,6 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) file >> j; } - auto loadScriptJson = [&](const std::string& path, nbl_json& out) -> bool - { - std::ifstream sfile(path); - if (!sfile.is_open()) - { - m_logger->log("Cannot open scripted input file \"%s\".", ILogger::ELL_ERROR, path.c_str()); - return false; - } - sfile >> out; - return true; - }; - std::optional pendingScriptedSequence; bool scriptedInputParseFailed = false; std::string scriptedInputParseError; @@ -1925,10 +1973,10 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) auto finalizeScriptedInput = [&]() -> void { - nbl::hlsl::finalizeScriptedTimeline(m_scriptedInput.timeline, m_disableScreenshotsCli); + nbl::system::finalizeScriptedTimeline(m_scriptedInput.timeline, m_disableScreenshotsCli); }; - auto parseScriptedInput = [&](const nbl_json& script) -> void + auto applyParsedScriptedInput = [&](nbl::system::CCameraScriptedInputParseResult parsed) -> void { pendingScriptedSequence.reset(); scriptedInputParseFailed = false; @@ -1949,13 +1997,6 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) m_scriptedInput.capturePrefix = "script"; m_scriptedInput.captureOutputDir = localOutputCWD; - CCameraScriptedInputParseResult parsed; - if (!nbl::hlsl::deserializeCameraScriptedInput(script, parsed, &scriptedInputParseError)) - { - scriptedInputParseFailed = true; - return; - } - m_scriptedInput.enabled = parsed.enabled; if (parsed.hasLog) m_scriptedInput.log = parsed.log || m_scriptedInput.log; @@ -2002,22 +2043,26 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) if (program.is_used("--script")) { - system::path scriptPath = program.get("--script"); + nbl::system::path scriptPath = program.get("--script"); if (scriptPath.is_relative()) scriptPath = localInputCWD / scriptPath; - nbl_json scriptJson; - if (!loadScriptJson(scriptPath.string(), scriptJson)) - return false; - parseScriptedInput(scriptJson); - if (scriptedInputParseFailed) + nbl::system::CCameraScriptedInputParseResult parsed; + if (!nbl::system::loadCameraScriptedInputFromFile(scriptPath, parsed, &scriptedInputParseError)) { logFail("Camera sequence script parse failed: %s", scriptedInputParseError.c_str()); return false; } + applyParsedScriptedInput(std::move(parsed)); } else if (j.contains("scripted_input")) { - parseScriptedInput(j["scripted_input"]); + std::stringstream scriptedInputStream; + scriptedInputStream << j["scripted_input"].dump(); + nbl::system::CCameraScriptedInputParseResult parsed; + if (!nbl::system::readCameraScriptedInput(scriptedInputStream, parsed, &scriptedInputParseError)) + scriptedInputParseFailed = true; + else + applyParsedScriptedInput(std::move(parsed)); if (scriptedInputParseFailed) { logFail("Camera sequence script parse failed: %s", scriptedInputParseError.c_str()); @@ -2110,7 +2155,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) std::vector mouse; } bindings; - const char* bindingLayoutsKey = j.contains("bindings") ? "bindings" : (j.contains("controllers") ? "controllers" : nullptr); + const char* bindingLayoutsKey = j.contains("bindings") ? "bindings" : nullptr; if (bindingLayoutsKey) { @@ -2211,7 +2256,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) for (const auto viewportIx : boundViewports) { auto& viewport = j["viewports"][viewportIx]; - const char* viewportBindingsKey = viewport.contains("bindings") ? "bindings" : (viewport.contains("controllers") ? "controllers" : nullptr); + const char* viewportBindingsKey = viewport.contains("bindings") ? "bindings" : nullptr; if (!viewport.contains("projection") || !viewportBindingsKey) { logFail("\"projection\" or \"bindings\" missing in viewport object index %d", viewportIx); @@ -2278,7 +2323,23 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) { auto* camera = m_planarProjections[planarIx]->getCamera(); const std::string presetName = "Planar " + std::to_string(planarIx); - m_initialPlanarPresets.emplace_back(nbl::hlsl::capturePreset(m_cameraGoalSolver, camera, presetName)); + const auto captureAnalysis = nbl::core::analyzeCameraCapture(m_cameraGoalSolver, camera); + if (!captureAnalysis.canCapture) + { + const auto kindLabel = camera ? std::string(getCameraTypeLabel(camera->getKind())) : std::string("Unknown"); + const auto reason = !captureAnalysis.hasCamera ? "missing camera" : + (!captureAnalysis.capturedGoal ? "capture failed" : + (!captureAnalysis.finiteGoal ? "non-finite goal" : "unknown")); + return logFail("Failed to capture initial planar preset %u for camera kind \"%s\": %s", + planarIx, kindLabel.c_str(), reason); + } + + CameraPreset preset = {}; + if (!nbl::core::tryCapturePreset(captureAnalysis, camera, presetName, preset)) + return logFail("Failed to build initial planar preset %u for camera kind \"%s\".", + planarIx, + camera ? std::string(getCameraTypeLabel(camera->getKind())).c_str() : "Unknown"); + m_initialPlanarPresets.emplace_back(std::move(preset)); } resetFollowTargetToDefault(); @@ -2322,8 +2383,8 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) return match; }; - const bool useWindow = nbl::hlsl::sequenceScriptUsesMultiplePresentations(sequence); - nbl::hlsl::appendScriptedActionEvent(timeline, 0u, CCameraScriptedInputEvent::ActionData::Kind::SetUseWindow, useWindow ? 1 : 0); + const bool useWindow = nbl::core::sequenceScriptUsesMultiplePresentations(sequence); + nbl::system::appendScriptedActionEvent(timeline, 0u, CCameraScriptedInputEvent::ActionData::Kind::SetUseWindow, useWindow ? 1 : 0); const CCameraSequenceTrackedTargetPose referenceTrackedTargetPose = { .position = getDefaultFollowTargetPosition(), @@ -2340,14 +2401,14 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) segment.name.c_str(), kindLabel.c_str(), segment.cameraIdentifier.c_str()); return false; } - const bool useTrackedTargetFollow = nbl::hlsl::sequenceSegmentUsesTrackedTargetTrack(segment) && + const bool useTrackedTargetFollow = nbl::core::sequenceSegmentUsesTrackedTargetTrack(segment) && planarIx.value() < m_planarFollowConfigs.size() && m_planarFollowConfigs[planarIx.value()].enabled && m_planarFollowConfigs[planarIx.value()].mode != ECameraFollowMode::Disabled; - nbl::hlsl::CCameraSequenceCompiledSegment compiledSegment; + nbl::core::CCameraSequenceCompiledSegment compiledSegment; std::string trackError; - if (!nbl::hlsl::compileSequenceSegmentFromReference( + if (!nbl::core::compileSequenceSegmentFromReference( sequence, segment, m_initialPlanarPresets[planarIx.value()], @@ -2366,7 +2427,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) } std::string buildError; - if (!nbl::hlsl::appendCompiledSequenceSegmentToScriptedTimeline( + if (!nbl::system::appendCompiledSequenceSegmentToScriptedTimeline( timeline, frameCursor, compiledSegment, @@ -2386,7 +2447,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) frameCursor += compiledSegment.durationFrames; } - nbl::hlsl::finalizeScriptedTimeline(timeline, m_disableScreenshotsCli); + nbl::system::finalizeScriptedTimeline(timeline, m_disableScreenshotsCli); m_scriptedInput.timeline = std::move(timeline); return true; }; @@ -2471,7 +2532,6 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) // We just live life in easy mode and have the Swapchain Creation Parameters get deduced from the surface. // We don't need any control over the format of the swapchain because we'll be only using Renderpasses this time! - // TODO: improve the queue allocation/choice and allocate a dedicated presentation queue to improve responsiveness and race to present. ISwapchain::SSharedCreationParams sharedParams = {}; sharedParams.imageUsage |= IGPUImage::EUF_TRANSFER_SRC_BIT; auto swapchainResources = std::make_unique(); @@ -2972,7 +3032,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) .immutableSamplers = &m_spaceEnvSampler } }; - m_spaceEnvDescriptorSetLayout = m_device->createDescriptorSetLayout(bindings); + m_spaceEnvDescriptorSetLayout = m_device->createDescriptorSetLayout(std::span{ bindings }); if (!m_spaceEnvDescriptorSetLayout) return logFail("Failed to create space environment descriptor set layout."); @@ -3109,3 +3169,5 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) } + + diff --git a/61_UI/AppTransformEditor.cpp b/61_UI/AppTransformEditor.cpp index 7e29e957f..235368446 100644 --- a/61_UI/AppTransformEditor.cpp +++ b/61_UI/AppTransformEditor.cpp @@ -107,11 +107,14 @@ void App::TransformEditorContents() mCurrentGizmoOperation = ImGuizmo::SCALE; float32_t3 matrixTranslation, matrixRotation, matrixScale; - CGimbalInputBinder::input_imguizmo_event_t decomposed, recomposed; - imguizmoModel.outDeltaTRS = CGimbalInputBinder::input_imguizmo_event_t(1); + imguizmoModel.outDeltaTRS = hlsl::float32_t4x4(1.0f); - ImGuizmo::DecomposeMatrixToComponents(m16TRSmatrix, &matrixTranslation[0], &matrixRotation[0], &matrixScale[0]); - decomposed = *reinterpret_cast(m16TRSmatrix); + if (!hlsl::decomposeTransformMatrix(imguizmoModel.outTRS, matrixTranslation, matrixRotation, matrixScale)) + { + matrixTranslation = float32_t3(imguizmoModel.outTRS[3].x, imguizmoModel.outTRS[3].y, imguizmoModel.outTRS[3].z); + matrixRotation = float32_t3(0.0f); + matrixScale = float32_t3(1.0f); + } { ImGuiInputTextFlags flags = 0; @@ -119,8 +122,11 @@ void App::TransformEditorContents() ImGui::InputFloat3("Rt", &matrixRotation[0], "%.3f", flags); ImGui::InputFloat3("Sc", &matrixScale[0], "%.3f", flags); } - ImGuizmo::RecomposeMatrixFromComponents(&matrixTranslation[0], &matrixRotation[0], &matrixScale[0], m16TRSmatrix); - recomposed = *reinterpret_cast(m16TRSmatrix); + imguizmoModel.outTRS = hlsl::composeTransformMatrix( + matrixTranslation, + hlsl::makeQuaternionFromEulerDegrees(matrixRotation), + matrixScale); + m16TRSmatrix = &imguizmoModel.outTRS[0][0]; if (mCurrentGizmoOperation != ImGuizmo::SCALE) { @@ -150,51 +156,9 @@ void App::TransformEditorContents() if (m_manipulatedObjectKind == SceneManipulatedObjectKind::Camera && boundCameraToManipulate) { auto referenceFrame = getCastedMatrix(imguizmoModel.outTRS); - boundCameraToManipulate->manipulateWithUnitMotionScales({}, &referenceFrame); + nbl::core::applyReferenceFrameToCamera(boundCameraToManipulate.get(), referenceFrame); if (boundPlanarCameraIxToManipulate.has_value()) refreshFollowOffsetConfigForPlanar(boundPlanarCameraIxToManipulate.value()); - - /* - { - static std::vector virtualEvents(0x45); - - if (not enableActiveCameraMovement) - { - uint32_t vCount = {}; - - boundCameraToManipulate->beginInputProcessing(m_nextPresentationTimestamp); - { - boundCameraToManipulate->process(nullptr, vCount); - - if (virtualEvents.size() < vCount) - virtualEvents.resize(vCount); - - CGimbalInputBinder::SUpdateParameters params; - params.imguizmoEvents = { { imguizmoModel.outDeltaTRS } }; - boundCameraToManipulate->process(virtualEvents.data(), vCount, params); - } - boundCameraToManipulate->endInputProcessing(); - - // I start to think controller should be able to set sensitivity to scale magnitudes of generated events - // in order for camera to not keep any magnitude scalars like move or rotation speed scales - - if (vCount) - { - const float pmSpeed = boundCameraToManipulate->getMoveSpeedScale(); - const float prSpeed = boundCameraToManipulate->getRotationSpeedScale(); - - boundCameraToManipulate->setMoveSpeedScale(1); - boundCameraToManipulate->setRotationSpeedScale(1); - - auto referenceFrame = getCastedMatrix(imguizmoModel.outTRS); - boundCameraToManipulate->manipulate({ virtualEvents.data(), vCount }, &referenceFrame); - - boundCameraToManipulate->setMoveSpeedScale(pmSpeed); - boundCameraToManipulate->setRotationSpeedScale(prSpeed); - } - } - } - */ } else if (m_manipulatedObjectKind == SceneManipulatedObjectKind::FollowTarget) { diff --git a/61_UI/AppUpdate.cpp b/61_UI/AppUpdate.cpp index 37bf79cfc..3d89571da 100644 --- a/61_UI/AppUpdate.cpp +++ b/61_UI/AppUpdate.cpp @@ -63,7 +63,7 @@ void App::update() uint32_t scriptedImguizmoVirtualCount = 0u; if (m_scriptedInput.enabled && m_scriptedInput.nextEventIndex < m_scriptedInput.timeline.events.size()) - nbl::hlsl::dequeueScriptedFrameEvents( + nbl::system::dequeueScriptedFrameEvents( m_scriptedInput.timeline.events, m_scriptedInput.nextEventIndex, m_realFrameIx, @@ -73,7 +73,10 @@ void App::update() { SKeyboardEvent e(m_nextPresentationTimestamp); e.keyCode = authoredKeyboard.key; - e.action = authoredKeyboard.action; + e.action = + authoredKeyboard.action == CCameraScriptedInputEvent::KeyboardData::Action::Pressed ? + ui::SKeyboardEvent::ECA_PRESSED : + ui::SKeyboardEvent::ECA_RELEASED; e.window = m_window.get(); scriptedKeyboard.emplace_back(e); } @@ -81,23 +84,30 @@ void App::update() { SMouseEvent e(m_nextPresentationTimestamp); e.window = m_window.get(); - e.type = authoredMouse.type; - if (authoredMouse.type == ui::SMouseEvent::EET_CLICK) + switch (authoredMouse.type) { - e.clickEvent.mouseButton = authoredMouse.button; - e.clickEvent.action = authoredMouse.action; - e.clickEvent.clickPosX = authoredMouse.x; - e.clickEvent.clickPosY = authoredMouse.y; - } - else if (authoredMouse.type == ui::SMouseEvent::EET_SCROLL) - { - e.scrollEvent.verticalScroll = authoredMouse.v; - e.scrollEvent.horizontalScroll = authoredMouse.h; - } - else if (authoredMouse.type == ui::SMouseEvent::EET_MOVEMENT) - { - e.movementEvent.relativeMovementX = authoredMouse.dx; - e.movementEvent.relativeMovementY = authoredMouse.dy; + case CCameraScriptedInputEvent::MouseData::Type::Click: + e.type = ui::SMouseEvent::EET_CLICK; + e.clickEvent.mouseButton = authoredMouse.button; + e.clickEvent.action = + authoredMouse.action == CCameraScriptedInputEvent::MouseData::ClickAction::Pressed ? + ui::SMouseEvent::SClickEvent::EA_PRESSED : + ui::SMouseEvent::SClickEvent::EA_RELEASED; + e.clickEvent.clickPosX = authoredMouse.x; + e.clickEvent.clickPosY = authoredMouse.y; + break; + case CCameraScriptedInputEvent::MouseData::Type::Scroll: + e.type = ui::SMouseEvent::EET_SCROLL; + e.scrollEvent.verticalScroll = authoredMouse.v; + e.scrollEvent.horizontalScroll = authoredMouse.h; + break; + case CCameraScriptedInputEvent::MouseData::Type::Movement: + e.type = ui::SMouseEvent::EET_MOVEMENT; + e.movementEvent.relativeMovementX = authoredMouse.dx; + e.movementEvent.relativeMovementY = authoredMouse.dy; + break; + default: + continue; } scriptedMouse.emplace_back(e); } @@ -201,7 +211,7 @@ void App::update() } auto* camera = m_planarProjections[binding.activePlanarIx]->getCamera(); - if (!nbl::hlsl::applyPreset(m_cameraGoalSolver, camera, m_initialPlanarPresets[binding.activePlanarIx])) + if (!nbl::core::applyPreset(m_cameraGoalSolver, camera, m_initialPlanarPresets[binding.activePlanarIx])) m_logger->log("[script][warn] action reset_active_camera failed for planar: %u", ILogger::ELL_WARNING, binding.activePlanarIx); } break; } @@ -342,7 +352,7 @@ void App::update() for (uint32_t i = 0u; i < vKeyboardEventsCount; ++i) virtualEvents[i].magnitude *= m_cameraControls.keyboardScale; - nbl::hlsl::scaleVirtualEvents(virtualEvents, vCount, m_cameraControls.translationScale, m_cameraControls.rotationScale); + nbl::core::scaleVirtualEvents(virtualEvents, vCount, m_cameraControls.translationScale, m_cameraControls.rotationScale); const char* bindingLabel = "Keyboard/Mouse"; auto applyEventsToCamera = [&](ICamera* target, uint32_t planarIx) @@ -354,7 +364,7 @@ void App::update() { std::vector perCameraEvents(virtualEvents.begin(), virtualEvents.begin() + vCount); uint32_t perCount = vCount; - nbl::hlsl::remapTranslationEventsFromWorldToCameraLocal(target, perCameraEvents, perCount); + nbl::core::remapTranslationEventsFromWorldToCameraLocal(target, perCameraEvents, perCount); if (perCount) target->manipulate({ perCameraEvents.data(), perCount }); } @@ -363,7 +373,7 @@ void App::update() target->manipulate({ virtualEvents.data(), vCount }); } - nbl::hlsl::applyCameraConstraints(m_cameraGoalSolver, target, m_cameraConstraints); + nbl::core::applyCameraConstraints(m_cameraGoalSolver, target, m_cameraConstraints); if (!m_scriptedInput.enabled) refreshFollowOffsetConfigForPlanar(planarIx); appendVirtualEventLog("input", bindingLabel, planarIx, target, virtualEvents.data(), vCount); @@ -371,7 +381,7 @@ void App::update() if (m_cameraControls.mirrorInput) { - std::unordered_set visited; + std::unordered_set visited; for (size_t bindingIx = 0u; bindingIx < windowBindings.size(); ++bindingIx) { auto& bindingIt = windowBindings[bindingIx]; @@ -381,8 +391,7 @@ void App::update() auto* target = planarIt->getCamera(); if (!target) continue; - const auto id = target->getGimbal().getID(); - if (!visited.insert(id).second) + if (!visited.insert(target).second) continue; applyEventsToCamera(target, bindingIt.activePlanarIx); } @@ -402,7 +411,7 @@ void App::update() const auto& gimbal = camera->getGimbal(); const auto pos = gimbal.getPosition(); - const auto euler = glm::degrees(glm::eulerAngles(gimbal.getOrientation())); + const auto euler = getQuaternionEulerDegrees(gimbal.getOrientation()); m_logger->log("[script] gimbal pos=(%.3f, %.3f, %.3f) euler_deg=(%.3f, %.3f, %.3f)", ILogger::ELL_INFO, pos.x, pos.y, pos.z, euler.x, euler.y, euler.z); } @@ -416,7 +425,7 @@ void App::update() auto* camera = planar->getCamera(); CGimbalInputBinder imguizmoBinding; - camera->copyDefaultInputBindingPresetTo(imguizmoBinding); + applyDefaultCameraInputBindingPreset(imguizmoBinding, *camera); auto collectedEvents = imguizmoBinding.collectVirtualEvents(m_nextPresentationTimestamp, { .imguizmoEvents = { scriptedFrameEvents.imguizmo.data(), scriptedFrameEvents.imguizmo.size() } }); @@ -438,7 +447,7 @@ void App::update() const auto& gimbal = camera->getGimbal(); const auto pos = gimbal.getPosition(); - const auto euler = glm::degrees(glm::eulerAngles(gimbal.getOrientation())); + const auto euler = getQuaternionEulerDegrees(gimbal.getOrientation()); m_logger->log("[script] imguizmo gimbal pos=(%.3f, %.3f, %.3f) euler_deg=(%.3f, %.3f, %.3f)", ILogger::ELL_INFO, pos.x, pos.y, pos.z, euler.x, euler.y, euler.z); } @@ -477,14 +486,14 @@ void App::update() if (camera) { for (auto& projection : planar->getPlanarProjections()) - nbl::hlsl::syncDynamicPerspectiveProjection(camera, projection); + nbl::core::syncDynamicPerspectiveProjection(camera, projection); } if (m_scriptedInput.log && camera) { const auto& gimbal = camera->getGimbal(); const auto pos = gimbal.getPosition(); - const auto euler = glm::degrees(glm::eulerAngles(gimbal.getOrientation())); + const auto euler = getQuaternionEulerDegrees(gimbal.getOrientation()); m_logger->log("[script] goal_apply gimbal pos=(%.3f, %.3f, %.3f) euler_deg=(%.3f, %.3f, %.3f)", ILogger::ELL_INFO, pos.x, pos.y, pos.z, euler.x, euler.y, euler.z); } @@ -544,7 +553,7 @@ void App::update() m_planarProjections[planarIx]->getCamera() : nullptr; float32_t4x4 viewProjMatrix = float32_t4x4(1.0f); const float32_t4x4* viewProjMatrixPtr = tryBuildFollowViewProjForPlanar(planarIx, viewProjMatrix) ? &viewProjMatrix : nullptr; - followMetrics = nbl::hlsl::buildFollowVisualMetrics( + followMetrics = nbl::core::buildFollowVisualMetrics( activeCamera, m_followTarget, &m_planarFollowConfigs[planarIx], @@ -611,7 +620,7 @@ void App::update() } } - const auto checkResult = nbl::hlsl::evaluateScriptedChecksForFrame( + const auto checkResult = nbl::system::evaluateScriptedChecksForFrame( m_scriptedInput.timeline.checks, m_scriptedInput.checkRuntime, { diff --git a/61_UI/AppWorkLoop.cpp b/61_UI/AppWorkLoop.cpp index f3f6f3f0c..bbcae0934 100644 --- a/61_UI/AppWorkLoop.cpp +++ b/61_UI/AppWorkLoop.cpp @@ -329,7 +329,7 @@ void App::workLoopBody() m_realFrameIx++; const uint64_t renderedFrameIx = m_realFrameIx - 1u; - auto captureScreenshot = [&](const system::path& outPath, const char* tag) -> void + auto captureScreenshot = [&](const nbl::system::path& outPath, const char* tag) -> void { if (!m_device || !m_assetManager || !m_surface) return; diff --git a/61_UI/CMakeLists.txt b/61_UI/CMakeLists.txt index 3b9f9b8fc..60b2ed74c 100644 --- a/61_UI/CMakeLists.txt +++ b/61_UI/CMakeLists.txt @@ -7,7 +7,6 @@ if(NBL_BUILD_IMGUI AND NBL_EXT_FRUSTUM_LIB) "${CMAKE_CURRENT_SOURCE_DIR}/AppUpdate.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/AppWorkLoop.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/keysmapping.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/src/transform.cpp" ) set(NBL_INCLUDE_SERACH_DIRECTORIES diff --git a/61_UI/README.md b/61_UI/README.md index 32b49c8d7..86890e6f2 100644 --- a/61_UI/README.md +++ b/61_UI/README.md @@ -1,6 +1,6 @@ # 61_UI Cameraz -`61_UI` is the current integration, UX, and validation harness for the shared camera stack in +`61_UI` is a full integration example and validation target for the shared camera stack in [`../common/include/camera`](../common/include/camera/README.md). If you want the architecture, design rationale, and reusable API breakdown, start there first. @@ -8,8 +8,6 @@ This README focuses on what `61_UI` adds on top of the shared layer. ## Role of this example -`61_UI` is the only actively migrated consumer right now. - It is used to: - exercise all current camera models in one scene diff --git a/61_UI/include/app/App.hpp b/61_UI/include/app/App.hpp index 6214d8693..a9119e27d 100644 --- a/61_UI/include/app/App.hpp +++ b/61_UI/include/app/App.hpp @@ -17,16 +17,12 @@ #include #include #include -#include "nlohmann/json.hpp" #include "argparse/argparse.hpp" -using nbl_json = nlohmann::json; #include "common.hpp" #include "keysmapping.hpp" #include "app/AppTypes.hpp" #include "camera/CCubeProjection.hpp" -#include "glm/glm/ext/matrix_clip_space.hpp" -#include "glm/gtc/quaternion.hpp" #include "nbl/ext/Frustum/CDrawFrustum.h" #include "nbl/ext/FullScreenTriangle/FullScreenTriangle.h" #include "nbl/ext/ScreenShot/ScreenShot.h" @@ -38,7 +34,7 @@ using nbl_json = nlohmann::json; #include "nbl/this_example/builtin/build/CArchive.h" #endif -class CUIEventCallback : public nbl::video::ISmoothResizeSurface::ICallback // I cannot use common CEventCallback because I MUST inherit this callback in order to use smooth resize surface with window callback (for my input events) +class CUIEventCallback : public nbl::video::ISmoothResizeSurface::ICallback { public: CUIEventCallback(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)) {} @@ -127,7 +123,7 @@ class CSwapchainResources final : public ISmoothResizeSurface::ISwapchainResourc .baseArrayLayer = 0, .layerCount = 1 }, - .oldLayout = IGPUImage::LAYOUT::UNDEFINED, // I do not care about previous contents of the swapchain + .oldLayout = IGPUImage::LAYOUT::UNDEFINED, .newLayout = blitDstLayout }, { @@ -154,7 +150,6 @@ class CSwapchainResources final : public ISmoothResizeSurface::ISwapchainResourc depInfo.imgBarriers = { preBarriers,needToAcquireSrcOwnership ? 2ull : 1ull }; success &= cmdbuf->pipelineBarrier(asset::EDF_NONE, depInfo); - // TODO: Implement scaling modes other than a plain STRETCH, and allow for using subrectangles of the initial contents { const auto srcOffset = source.rect.offset; const auto srcExtent = source.rect.extent; @@ -327,12 +322,12 @@ class App final : public examples::SimpleWindowedApplication } bool onAppInitialized(smart_refctd_ptr&& system) override; - core::bitflag getLogLevelMask() override + core::bitflag getLogLevelMask() override { - return core::bitflag(system::ILogger::ELL_INFO) | - system::ILogger::ELL_WARNING | - system::ILogger::ELL_PERFORMANCE | - system::ILogger::ELL_ERROR; + 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() @@ -474,7 +469,7 @@ class App final : public examples::SimpleWindowedApplication CVirtualGimbalEvent::VirtualEventType type = CVirtualGimbalEvent::None; float64_t magnitude = 0.0; std::string source; - std::string controller; + std::string inputSource; std::string camera; uint32_t planarIx = 0u; std::string line; @@ -710,12 +705,12 @@ class App final : public examples::SimpleWindowedApplication if (planarIx >= m_planarProjections.size() || planarIx >= m_planarFollowConfigs.size()) return false; auto* camera = m_planarProjections[planarIx] ? m_planarProjections[planarIx]->getCamera() : nullptr; - return nbl::hlsl::captureFollowOffsetsFromCamera(m_cameraGoalSolver, camera, m_followTarget, m_planarFollowConfigs[planarIx]); + return nbl::core::captureFollowOffsetsFromCamera(m_cameraGoalSolver, camera, m_followTarget, m_planarFollowConfigs[planarIx]); } inline bool followConfigUsesCapturedOffset(const SCameraFollowConfig& config) const { - return config.enabled && nbl::hlsl::cameraFollowModeUsesCapturedOffset(config.mode); + return config.enabled && nbl::core::cameraFollowModeUsesCapturedOffset(config.mode); } inline void refreshFollowOffsetConfigForPlanar(const uint32_t planarIx) @@ -731,7 +726,7 @@ class App final : public examples::SimpleWindowedApplication if (!camera) return; - nbl::hlsl::captureFollowOffsetsFromCamera(m_cameraGoalSolver, camera, m_followTarget, config); + nbl::core::captureFollowOffsetsFromCamera(m_cameraGoalSolver, camera, m_followTarget, config); } inline void refreshFollowOffsetConfigsForCamera(ICamera* camera) @@ -759,9 +754,9 @@ class App final : public examples::SimpleWindowedApplication return float64_t3(6.0, -4.5, 2.25); } - inline glm::quat getDefaultFollowTargetOrientation() const + inline camera_quaternion_t getDefaultFollowTargetOrientation() const { - return glm::quat(1.0, 0.0, 0.0, 0.0); + return makeIdentityQuaternion(); } inline void resetFollowTargetToDefault() @@ -823,12 +818,12 @@ class App final : public examples::SimpleWindowedApplication if (!config.enabled || config.mode == ECameraFollowMode::Disabled) continue; - const auto result = nbl::hlsl::applyFollowToCamera(m_cameraGoalSolver, camera, m_followTarget, config); + const auto result = nbl::core::applyFollowToCamera(m_cameraGoalSolver, camera, m_followTarget, config); if (!result.succeeded()) continue; for (auto& projection : planar->getPlanarProjections()) - nbl::hlsl::syncDynamicPerspectiveProjection(camera, projection); + nbl::core::syncDynamicPerspectiveProjection(camera, projection); } } @@ -921,7 +916,7 @@ class App final : public examples::SimpleWindowedApplication float ndcX = 0.0f; float ndcY = 0.0f; float ndcRadius = 0.0f; - if (!nbl::hlsl::tryComputeProjectedFollowTargetMetrics(viewProjMatrix, m_followTarget, ndcX, ndcY, &ndcRadius)) + if (!nbl::core::tryComputeProjectedFollowTargetMetrics(viewProjMatrix, m_followTarget, ndcX, ndcY, &ndcRadius)) return; auto* drawList = ImGui::GetWindowDrawList(); @@ -1274,7 +1269,7 @@ class App final : public examples::SimpleWindowedApplication } if (m_scriptedInput.visualFollowActive) { - lineHint += " | " + std::string(nbl::hlsl::getCameraFollowModeDescription(m_scriptedInput.visualFollowMode)); + lineHint += " | " + std::string(nbl::core::getCameraFollowModeDescription(m_scriptedInput.visualFollowMode)); if (m_scriptedInput.visualFollowLockValid) { char followBuffer[192] = {}; @@ -1351,17 +1346,17 @@ class App final : public examples::SimpleWindowedApplication inline PresetUiAnalysis analyzePresetForUi(ICamera* camera, const CameraPreset& preset) const { - return nbl::hlsl::analyzePresetPresentation(m_cameraGoalSolver, camera, preset); + return nbl::core::analyzePresetPresentation(m_cameraGoalSolver, camera, preset); } inline CaptureUiAnalysis analyzeCameraCaptureForUi(ICamera* camera) const { - return nbl::hlsl::analyzeCapturePresentation(m_cameraGoalSolver, camera); + return nbl::core::analyzeCapturePresentation(m_cameraGoalSolver, camera); } inline CCameraGoalSolver::SCompatibilityResult analyzePresetCompatibility(ICamera* camera, const CameraPreset& preset) const { - return nbl::hlsl::analyzePresetApply(m_cameraGoalSolver, camera, preset).compatibility; + return nbl::core::analyzePresetApply(m_cameraGoalSolver, camera, preset).compatibility; } inline bool presetMatchesFilter(ICamera* camera, const CameraPreset& preset) const @@ -1371,7 +1366,7 @@ class App final : public examples::SimpleWindowedApplication inline CCameraGoalSolver::SApplyResult applyPresetFromUi(ICamera* camera, const CameraPreset& preset) { - const auto result = nbl::hlsl::applyPresetDetailed(m_cameraGoalSolver, camera, preset); + const auto result = nbl::core::applyPresetDetailed(m_cameraGoalSolver, camera, preset); if (result.succeeded()) refreshFollowOffsetConfigsForCamera(camera); const auto presetUi = analyzePresetForUi(camera, preset); @@ -1399,25 +1394,25 @@ class App final : public examples::SimpleWindowedApplication inline void storePlaybackApplySummary(const SCameraPresetApplySummary& summary) { storeApplyStatusBanner(m_playbackApplyBanner, - nbl::hlsl::describePresetApplySummary(summary, m_playbackAffectsAll ? "Playback apply | no cameras available" : "Playback apply | no active camera"), + nbl::core::describePresetApplySummary(summary, m_playbackAffectsAll ? "Playback apply | no cameras available" : "Playback apply | no active camera"), summary.succeeded(), summary.approximate()); } - inline void appendVirtualEventLog(std::string_view source, std::string_view controller, uint32_t planarIx, ICamera* camera, const CVirtualGimbalEvent* events, uint32_t count) + inline void appendVirtualEventLog(std::string_view source, std::string_view inputSource, uint32_t planarIx, ICamera* camera, const CVirtualGimbalEvent* events, uint32_t count) { m_uiVirtualEventsThisFrame += count; const std::string sourceStr(source); - const std::string controllerStr(controller); + 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 ctrl=%s cam=%s planar=%u event=%s mag=%.6f", + "virtual frame=%llu src=%s input=%s cam=%s planar=%u event=%s mag=%.6f", static_cast(m_realFrameIx), sourceStr.c_str(), - controllerStr.c_str(), + inputSourceStr.c_str(), cameraName.c_str(), planarIx, eventName, @@ -1427,7 +1422,7 @@ class App final : public examples::SimpleWindowedApplication events[i].type, events[i].magnitude, sourceStr, - controllerStr, + inputSourceStr, cameraName, planarIx, std::move(line) @@ -1444,7 +1439,7 @@ class App final : public examples::SimpleWindowedApplication if (!m_playbackAffectsAll) { ICamera* activeCamera = getActiveCamera(); - summary = nbl::hlsl::applyPresetToCameraRange(m_cameraGoalSolver, std::span(&activeCamera, activeCamera ? 1u : 0u), preset); + summary = nbl::core::applyPresetToCameraRange(m_cameraGoalSolver, std::span(&activeCamera, activeCamera ? 1u : 0u), preset); if (summary.succeeded()) refreshFollowOffsetConfigsForCamera(activeCamera); return summary; @@ -1452,7 +1447,7 @@ class App final : public examples::SimpleWindowedApplication std::vector cameras; cameras.reserve(windowBindings.size()); - std::unordered_set visited; + std::unordered_set visited; for (auto& binding : windowBindings) { auto& planar = m_planarProjections[binding.activePlanarIx]; @@ -1461,12 +1456,11 @@ class App final : public examples::SimpleWindowedApplication auto* camera = planar->getCamera(); if (!camera) continue; - const auto id = camera->getGimbal().getID(); - if (visited.insert(id).second) + if (visited.insert(camera).second) cameras.push_back(camera); } - summary = nbl::hlsl::applyPresetToCameraRange(m_cameraGoalSolver, std::span(cameras.data(), cameras.size()), preset); + summary = nbl::core::applyPresetToCameraRange(m_cameraGoalSolver, std::span(cameras.data(), cameras.size()), preset); if (summary.succeeded()) refreshAllFollowOffsetConfigs(); return summary; @@ -1474,7 +1468,7 @@ class App final : public examples::SimpleWindowedApplication inline bool tryBuildPlaybackPresetAtTime(const float time, CameraPreset& preset) { - return nbl::hlsl::tryBuildKeyframeTrackPresetAtTime(m_keyframeTrack, time, preset); + return nbl::core::tryBuildKeyframeTrackPresetAtTime(m_keyframeTrack, time, preset); } inline bool applyPlaybackAtTime(const float time) @@ -1492,32 +1486,32 @@ class App final : public examples::SimpleWindowedApplication inline void sortKeyframesByTime() { - nbl::hlsl::sortKeyframeTrackByTime(m_keyframeTrack); + nbl::core::sortKeyframeTrackByTime(m_keyframeTrack); } inline void clampPlaybackTimeToKeyframes() { - nbl::hlsl::clampPlaybackCursorToTrack(m_keyframeTrack, m_playback); + nbl::core::clampPlaybackCursorToTrack(m_keyframeTrack, m_playback); } inline int selectKeyframeNearestTime(const float time) { - return nbl::hlsl::selectKeyframeTrackNearestTime(m_keyframeTrack, time); + return nbl::core::selectKeyframeTrackNearestTime(m_keyframeTrack, time); } inline void normalizeSelectedKeyframe() { - nbl::hlsl::normalizeSelectedKeyframeTrack(m_keyframeTrack); + nbl::core::normalizeSelectedKeyframeTrack(m_keyframeTrack); } inline CameraKeyframe* getSelectedKeyframe() { - return nbl::hlsl::getSelectedKeyframe(m_keyframeTrack); + return nbl::core::getSelectedKeyframe(m_keyframeTrack); } inline const CameraKeyframe* getSelectedKeyframe() const { - return nbl::hlsl::getSelectedKeyframe(m_keyframeTrack); + return nbl::core::getSelectedKeyframe(m_keyframeTrack); } inline bool replaceSelectedKeyframeFromCamera(ICamera* camera) @@ -1528,46 +1522,25 @@ class App final : public examples::SimpleWindowedApplication CameraPreset updatedPreset; const auto keyframeName = selected->preset.name.empty() ? std::string("Keyframe") : selected->preset.name; - if (!nbl::hlsl::tryCapturePreset(m_cameraGoalSolver, camera, keyframeName, updatedPreset)) + if (!nbl::core::tryCapturePreset(m_cameraGoalSolver, camera, keyframeName, updatedPreset)) return false; - return nbl::hlsl::replaceSelectedKeyframePreset(m_keyframeTrack, std::move(updatedPreset)); + return nbl::core::replaceSelectedKeyframePreset(m_keyframeTrack, std::move(updatedPreset)); } inline void updatePlayback(double dtSec) { - const auto advance = nbl::hlsl::advancePlaybackCursor(m_playback, m_keyframeTrack, dtSec); + const auto advance = nbl::core::advancePlaybackCursor(m_playback, m_keyframeTrack, dtSec); if (!advance.hasTrack || !advance.changedTime) return; applyPlaybackAtTime(m_playback.time); } - inline bool savePresetsToFile(const system::path& path) - { - return nbl::hlsl::savePresetCollectionToFile(path, std::span(m_presets.data(), m_presets.size())); - } - - inline bool loadPresetsFromFile(const system::path& path) - { - return nbl::hlsl::loadPresetCollectionFromFile(path, m_presets); - } - - inline bool saveKeyframesToFile(const system::path& path) - { - return nbl::hlsl::saveKeyframeTrackToFile(path, m_keyframeTrack); - } - - inline bool loadKeyframesFromFile(const system::path& path) - { - if (!nbl::hlsl::loadKeyframeTrackFromFile(path, m_keyframeTrack)) - return false; - - clampPlaybackTimeToKeyframes(); - if (m_keyframeTrack.keyframes.empty()) - clearApplyStatusBanner(m_playbackApplyBanner); - return true; - } + 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(); @@ -1826,7 +1799,7 @@ class App final : public examples::SimpleWindowedApplication core::smart_refctd_ptr descriptorSet; }; - // one model object in the world, testing multiuple cameraz for which view is rendered to separate frame buffers (so what they see) with new controller API including imguizmo + // Demo scene object rendered into the viewports alongside the tracked target and cameras. nbl::hlsl::float32_t3x4 m_model = nbl::hlsl::float32_t3x4(1.f); CTrackedTarget m_followTarget; std::vector m_planarFollowConfigs; @@ -1949,7 +1922,7 @@ class App final : public examples::SimpleWindowedApplication CCameraScriptedCheckRuntimeState checkRuntime = {}; size_t nextCaptureIndex = 0; std::string capturePrefix = "script"; - system::path captureOutputDir; + nbl::system::path captureOutputDir; bool failed = false; bool summaryReported = false; bool visualActivePlanarValid = false; @@ -2000,7 +1973,7 @@ class App final : public examples::SimpleWindowedApplication bool m_ciMode = false; bool m_ciScreenshotDone = false; uint32_t m_ciFrameCounter = 0u; - system::path m_ciScreenshotPath; + nbl::system::path m_ciScreenshotPath; clock_t::time_point m_ciStartedAt = clock_t::time_point::min(); bool m_scriptVisualDebugCli = false; bool m_disableScreenshotsCli = false; diff --git a/61_UI/include/common.hpp b/61_UI/include/common.hpp index 61a3db576..e7240f731 100644 --- a/61_UI/include/common.hpp +++ b/61_UI/include/common.hpp @@ -22,11 +22,9 @@ #include "camera/CCameraPresetFlow.hpp" #include "camera/CCameraKeyframeTrack.hpp" #include "camera/CCameraPlaybackTimeline.hpp" -#include "camera/CCameraPersistence.hpp" #include "camera/CCameraSequenceScript.hpp" #include "camera/CCameraSequenceScriptedBuilder.hpp" #include "camera/CCameraScriptedRuntime.hpp" -#include "camera/CCameraScriptedRuntimePersistence.hpp" #include "camera/CCameraScriptedCheckRunner.hpp" #include "camera/CCameraGoalAnalysis.hpp" #include "camera/CCameraGoalSolver.hpp" @@ -36,6 +34,7 @@ #include "camera/CCameraFollowUtilities.hpp" #include "camera/CCameraFollowRegressionUtilities.hpp" #include "camera/CCameraTextUtilities.hpp" +#include "camera/CCameraInputBindingUtilities.hpp" #include "camera/CGimbalInputBinder.hpp" #include "camera/CCubeProjection.hpp" @@ -50,70 +49,128 @@ #include "imgui/imgui_internal.h" #include "imguizmo/ImGuizmo.h" -using namespace nbl; -using namespace nbl::core; -using namespace nbl::system; -using namespace nbl::asset; -using namespace nbl::ui; -using namespace nbl::video; -using namespace nbl::examples; - +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::hlsl::ICamera; -using nbl::hlsl::CFPSCamera; -using nbl::hlsl::CFreeCamera; -using nbl::hlsl::CSphericalTargetCamera; -using nbl::hlsl::COrbitCamera; -using nbl::hlsl::CArcballCamera; -using nbl::hlsl::CTurntableCamera; -using nbl::hlsl::CTopDownCamera; -using nbl::hlsl::CIsometricCamera; -using nbl::hlsl::CChaseCamera; -using nbl::hlsl::CDollyCamera; -using nbl::hlsl::CDollyZoomCamera; -using nbl::hlsl::CPathCamera; -using nbl::hlsl::CCameraGoal; -using nbl::hlsl::CTrackedTarget; -using nbl::hlsl::CCameraPreset; -using nbl::hlsl::CCameraKeyframe; -using nbl::hlsl::CCameraKeyframeTrack; -using nbl::hlsl::CCameraPlaybackCursor; -using nbl::hlsl::CCameraSequenceScript; -using nbl::hlsl::CCameraSequenceSegment; -using nbl::hlsl::CCameraSequenceKeyframe; -using nbl::hlsl::CCameraSequenceTrackedTargetPose; -using nbl::hlsl::CCameraSequenceTrackedTargetTrack; -using nbl::hlsl::CCameraSequencePresentation; -using nbl::hlsl::CCameraSequenceContinuitySettings; -using nbl::hlsl::CCameraSequenceScriptedSegmentBuildInfo; -using nbl::hlsl::CCameraScriptedInputEvent; -using nbl::hlsl::CCameraScriptedInputCheck; -using nbl::hlsl::CCameraScriptedFrameEvents; -using nbl::hlsl::CCameraScriptedTimeline; -using nbl::hlsl::CCameraScriptedControlOverrides; -using nbl::hlsl::CCameraScriptedInputParseResult; -using nbl::hlsl::CCameraScriptedCheckContext; -using nbl::hlsl::CCameraScriptedCheckFrameResult; -using nbl::hlsl::CCameraScriptedCheckLogEntry; -using nbl::hlsl::CCameraScriptedCheckRuntimeState; -using nbl::hlsl::SCameraPlaybackAdvanceResult; -using nbl::hlsl::SCameraPresetApplySummary; -using nbl::hlsl::SCameraGoalApplyAnalysis; -using nbl::hlsl::SCameraCaptureAnalysis; -using nbl::hlsl::SCameraFollowConfig; -using nbl::hlsl::SCameraFollowVisualMetrics; -using nbl::hlsl::SCameraGoalApplyPresentation; -using nbl::hlsl::SCameraGoalApplyPresentationBadges; -using nbl::hlsl::SCameraCapturePresentation; -using nbl::hlsl::SCameraConstraintSettings; -using nbl::hlsl::CCameraGoalSolver; -using nbl::hlsl::CGimbalInputBinder; -using nbl::hlsl::ECameraFollowMode; -using nbl::hlsl::EPresetApplyPresentationFilter; -using nbl::hlsl::IGimbalBindingLayout; -using nbl::hlsl::IPlanarProjection; -using nbl::hlsl::CPlanarProjection; -using nbl::hlsl::CVirtualGimbalEvent; +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::CCameraSequenceScriptedSegmentBuildInfo; +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::core::SCameraFollowVisualMetrics; +using nbl::core::SCameraGoalApplyPresentation; +using nbl::core::SCameraGoalApplyPresentationBadges; +using nbl::core::SCameraCapturePresentation; +using nbl::core::SCameraConstraintSettings; +using nbl::core::CCameraGoalSolver; +using nbl::core::ECameraFollowMode; +using nbl::core::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; @@ -123,20 +180,32 @@ 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::describeApplyResult; -using nbl::hlsl::describeGoalStateMask; -using nbl::hlsl::getCameraFollowModeLabel; -using nbl::hlsl::getCameraTypeDescription; -using nbl::hlsl::getCameraTypeLabel; +using nbl::hlsl::getQuaternionEulerDegrees; +using nbl::core::describeApplyResult; +using nbl::core::describeGoalStateMask; +using nbl::core::getCameraFollowModeLabel; +using nbl::core::getCameraTypeDescription; +using nbl::core::getCameraTypeLabel; using nbl::hlsl::getCastedMatrix; using nbl::hlsl::getCastedVector; using nbl::hlsl::getMatrix3x4As4x4; -using nbl::hlsl::cameraFollowModeUsesLocalOffset; -using nbl::hlsl::cameraFollowModeUsesWorldOffset; +using nbl::hlsl::getQuaternionBasisMatrix; +using nbl::hlsl::makeQuaternionFromAxisAngle; +using nbl::hlsl::makeQuaternionFromComponents; using nbl::hlsl::concatenateBFollowedByA; +using nbl::hlsl::makeQuaternionFromEulerDegrees; +using nbl::hlsl::makeQuaternionFromEulerRadians; using nbl::hlsl::mul; -using nbl::hlsl::syncDynamicPerspectiveProjection; +using nbl::hlsl::camera_quaternion_t; +using nbl::hlsl::makeIdentityQuaternion; +using nbl::hlsl::normalizeQuaternion; +using nbl::core::cameraFollowModeUsesLocalOffset; +using nbl::core::cameraFollowModeUsesWorldOffset; +using nbl::core::syncDynamicPerspectiveProjection; +using nbl::ui::applyDefaultCameraInputBindingPreset; +using nbl::ui::getDefaultCameraMouseMappingPreset; #endif // _NBL_THIS_EXAMPLE_COMMON_H_INCLUDED_ diff --git a/61_UI/include/transform.hpp b/61_UI/include/transform.hpp deleted file mode 100644 index 936a5af40..000000000 --- a/61_UI/include/transform.hpp +++ /dev/null @@ -1,19 +0,0 @@ -#ifndef _NBL_THIS_EXAMPLE_TRANSFORM_H_INCLUDED_ -#define _NBL_THIS_EXAMPLE_TRANSFORM_H_INCLUDED_ - -#include -#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); - -#endif // _NBL_THIS_EXAMPLE_TRANSFORM_H_INCLUDED_ diff --git a/61_UI/src/transform.cpp b/61_UI/src/transform.cpp deleted file mode 100644 index d1778ec90..000000000 --- a/61_UI/src/transform.cpp +++ /dev/null @@ -1,144 +0,0 @@ -#include "transform.hpp" - -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, nullptr, useSnap ? &snap[0] : nullptr, boundSizing ? bounds : nullptr, boundSizingSnap ? boundsSnap : nullptr); - - if(params.enableViewManipulate) - ImGuizmo::ViewManipulate(cameraView, params.camDistance, ImVec2(viewManipulateRight - 128, viewManipulateTop), ImVec2(128, 128), 0x10101010); - - ImGui::End(); - ImGui::PopStyleColor(); - - return retval; -} - diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index b91dfc91e..21bbbb814 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -10,6 +10,11 @@ nbl_create_ext_library_project(ExamplesAPI "" "${CMAKE_CURRENT_SOURCE_DIR}/src/nbl/examples/pch.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/include" "" "") +target_sources(${LIB_NAME} PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/src/nbl/examples/camera/CCameraPersistence.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/nbl/examples/camera/CCameraScriptedRuntimePersistence.cpp" +) + set_target_properties(${LIB_NAME} PROPERTIES DISABLE_PRECOMPILE_HEADERS OFF) target_precompile_headers(${LIB_NAME} PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include/nbl/examples/PCH.hpp") @@ -106,4 +111,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/CArcballCamera.hpp b/common/include/camera/CArcballCamera.hpp index c3f2cffec..a1492bf57 100644 --- a/common/include/camera/CArcballCamera.hpp +++ b/common/include/camera/CArcballCamera.hpp @@ -10,7 +10,7 @@ #include "CSphericalTargetCamera.hpp" -namespace nbl::hlsl +namespace nbl::core { class CArcballCamera final : public CSphericalTargetCamera @@ -26,10 +26,6 @@ class CArcballCamera final : public CSphericalTargetCamera } ~CArcballCamera() = default; - const base_t::keyboard_to_virtual_events_t getKeyboardMappingPreset() const override { return m_keyboard_to_virtual_events_preset; } - const base_t::mouse_to_virtual_events_t getMouseMappingPreset() const override { return m_mouse_to_virtual_events_preset; } - const base_t::imguizmo_to_virtual_events_t getImguizmoMappingPreset() const override { return m_imguizmo_to_virtual_events_preset; } - const typename base_t::CGimbal& getGimbal() override { return m_gimbal; } virtual bool manipulate(std::span virtualEvents, const float64_t4x4* referenceFrame = nullptr) override @@ -60,9 +56,9 @@ class CArcballCamera final : public CSphericalTargetCamera return applyPose(); } - virtual const uint32_t getAllowedVirtualEvents() override { return AllowedVirtualEvents; } + virtual uint32_t getAllowedVirtualEvents() const override { return AllowedVirtualEvents; } virtual CameraKind getKind() const override { return CameraKind::Arcball; } - virtual const std::string_view getIdentifier() override { return "Arcball Camera"; } + virtual std::string_view getIdentifier() const override { return "Arcball Camera"; } static inline constexpr float MinDistance = base_t::MinDistance; static inline constexpr float MaxDistance = base_t::MaxDistance; @@ -72,58 +68,6 @@ class CArcballCamera final : public CSphericalTargetCamera static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate | CVirtualGimbalEvent::Rotate; static inline constexpr double MaxPitch = 1.5533430342749532; static inline constexpr double MinPitch = -MaxPitch; - - static inline const auto m_keyboard_to_virtual_events_preset = []() - { - typename base_t::keyboard_to_virtual_events_t preset; - - preset[ui::E_KEY_CODE::EKC_W] = CVirtualGimbalEvent::MoveForward; - preset[ui::E_KEY_CODE::EKC_S] = CVirtualGimbalEvent::MoveBackward; - preset[ui::E_KEY_CODE::EKC_A] = CVirtualGimbalEvent::MoveLeft; - preset[ui::E_KEY_CODE::EKC_D] = CVirtualGimbalEvent::MoveRight; - preset[ui::E_KEY_CODE::EKC_Q] = CVirtualGimbalEvent::MoveDown; - preset[ui::E_KEY_CODE::EKC_E] = CVirtualGimbalEvent::MoveUp; - preset[ui::E_KEY_CODE::EKC_I] = CVirtualGimbalEvent::TiltDown; - preset[ui::E_KEY_CODE::EKC_K] = CVirtualGimbalEvent::TiltUp; - preset[ui::E_KEY_CODE::EKC_J] = CVirtualGimbalEvent::PanLeft; - preset[ui::E_KEY_CODE::EKC_L] = CVirtualGimbalEvent::PanRight; - - return preset; - }(); - - static inline const auto m_mouse_to_virtual_events_preset = []() - { - typename base_t::mouse_to_virtual_events_t preset; - - preset[ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_X] = CVirtualGimbalEvent::PanRight; - preset[ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_X] = CVirtualGimbalEvent::PanLeft; - preset[ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_Y] = CVirtualGimbalEvent::TiltUp; - preset[ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_Y] = CVirtualGimbalEvent::TiltDown; - preset[ui::E_MOUSE_CODE::EMC_VERTICAL_POSITIVE_SCROLL] = CVirtualGimbalEvent::MoveForward; - preset[ui::E_MOUSE_CODE::EMC_HORIZONTAL_POSITIVE_SCROLL] = CVirtualGimbalEvent::MoveForward; - preset[ui::E_MOUSE_CODE::EMC_VERTICAL_NEGATIVE_SCROLL] = CVirtualGimbalEvent::MoveBackward; - preset[ui::E_MOUSE_CODE::EMC_HORIZONTAL_NEGATIVE_SCROLL] = CVirtualGimbalEvent::MoveBackward; - - return preset; - }(); - - static inline const auto m_imguizmo_to_virtual_events_preset = []() - { - typename base_t::imguizmo_to_virtual_events_t preset; - - preset[CVirtualGimbalEvent::MoveForward] = CVirtualGimbalEvent::MoveForward; - preset[CVirtualGimbalEvent::MoveBackward] = CVirtualGimbalEvent::MoveBackward; - preset[CVirtualGimbalEvent::MoveLeft] = CVirtualGimbalEvent::MoveLeft; - preset[CVirtualGimbalEvent::MoveRight] = CVirtualGimbalEvent::MoveRight; - preset[CVirtualGimbalEvent::MoveUp] = CVirtualGimbalEvent::MoveUp; - preset[CVirtualGimbalEvent::MoveDown] = CVirtualGimbalEvent::MoveDown; - preset[CVirtualGimbalEvent::TiltDown] = CVirtualGimbalEvent::TiltDown; - preset[CVirtualGimbalEvent::TiltUp] = CVirtualGimbalEvent::TiltUp; - preset[CVirtualGimbalEvent::PanLeft] = CVirtualGimbalEvent::PanLeft; - preset[CVirtualGimbalEvent::PanRight] = CVirtualGimbalEvent::PanRight; - - return preset; - }(); }; } diff --git a/common/include/camera/CCameraFollowRegressionUtilities.hpp b/common/include/camera/CCameraFollowRegressionUtilities.hpp index 7da8f2c08..3eae10333 100644 --- a/common/include/camera/CCameraFollowRegressionUtilities.hpp +++ b/common/include/camera/CCameraFollowRegressionUtilities.hpp @@ -9,7 +9,7 @@ #include "CCameraFollowUtilities.hpp" -namespace nbl::hlsl +namespace nbl::core { /** @@ -344,6 +344,6 @@ inline bool buildApplyAndValidateFollowTargetContract( return true; } -} // namespace nbl::hlsl +} // namespace nbl::core #endif // _C_CAMERA_FOLLOW_REGRESSION_UTILITIES_HPP_ diff --git a/common/include/camera/CCameraFollowUtilities.hpp b/common/include/camera/CCameraFollowUtilities.hpp index cf9bc5977..a044bdac7 100644 --- a/common/include/camera/CCameraFollowUtilities.hpp +++ b/common/include/camera/CCameraFollowUtilities.hpp @@ -10,7 +10,7 @@ #include "CCameraGoalSolver.hpp" -namespace nbl::hlsl +namespace nbl::core { /** @@ -26,7 +26,7 @@ class CTrackedTarget CTrackedTarget( const float64_t3& position = float64_t3(0.0), - const glm::quat& orientation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f), + const camera_quaternion_t& orientation = makeIdentityQuaternion(), std::string identifier = "Follow Target") : m_identifier(std::move(identifier)), m_gimbal({ .position = position, .orientation = orientation }) @@ -38,7 +38,7 @@ class CTrackedTarget inline const gimbal_t& getGimbal() const { return m_gimbal; } inline gimbal_t& getGimbal() { return m_gimbal; } - inline void setPose(const float64_t3& position, const glm::quat& orientation) + inline void setPose(const float64_t3& position, const camera_quaternion_t& orientation) { m_gimbal.begin(); m_gimbal.setPosition(position); @@ -52,7 +52,7 @@ class CTrackedTarget setPose(position, m_gimbal.getOrientation()); } - inline void setOrientation(const glm::quat& orientation) + inline void setOrientation(const camera_quaternion_t& orientation) { setPose(m_gimbal.getPosition(), orientation); } @@ -65,7 +65,7 @@ class CTrackedTarget if (!isOrthoBase(right, up, forward)) return false; - setPose(float64_t3(transform[3]), glm::quat_cast(glm::dmat3{ right, up, forward })); + setPose(float64_t3(transform[3]), makeQuaternionFromBasis(right, up, forward)); return true; } @@ -202,7 +202,7 @@ inline bool buildFollowLookAtOrientation( const float64_t3& position, const float64_t3& targetPosition, const float64_t3& preferredUp, - glm::quat& outOrientation) + camera_quaternion_t& outOrientation) { const auto toTarget = targetPosition - position; const double toTargetLength = length(toTarget); @@ -229,7 +229,7 @@ inline bool buildFollowLookAtOrientation( if (!isOrthoBase(right, up, forward)) return false; - outOrientation = glm::quat_cast(glm::dmat3{ right, up, forward }); + outOrientation = makeQuaternionFromBasis(right, up, forward); return true; } @@ -243,7 +243,7 @@ inline bool applyFollowSphericalPose( if (!std::isfinite(orbitU) || !std::isfinite(orbitV) || !std::isfinite(distance)) return false; - const float clampedDistance = std::clamp(distance, CSphericalTargetCamera::MinDistance, CSphericalTargetCamera::MaxDistance); + const float clampedDistance = std::clamp(distance, ICamera::SphericalMinDistance, ICamera::SphericalMaxDistance); const float64_t3 spherePosition( std::cos(orbitV) * std::cos(orbitU) * static_cast(clampedDistance), std::cos(orbitV) * std::sin(orbitU) * static_cast(clampedDistance), @@ -267,7 +267,7 @@ inline bool applyFollowSphericalPose( goal.orbitV = orbitV; goal.orbitDistance = clampedDistance; goal.position = targetPosition + spherePosition; - goal.orientation = glm::quat_cast(glm::dmat3{ right, up, forward }); + goal.orientation = makeQuaternionFromBasis(right, up, forward); return true; } @@ -278,7 +278,7 @@ inline bool buildFollowSphericalGoalFromPose(CCameraGoal& goal, const float64_t3 if (!std::isfinite(distance) || distance <= 1e-9) return false; - const float clampedDistance = std::clamp(static_cast(distance), CSphericalTargetCamera::MinDistance, CSphericalTargetCamera::MaxDistance); + const float clampedDistance = std::clamp(static_cast(distance), ICamera::SphericalMinDistance, ICamera::SphericalMaxDistance); const auto local = offset / static_cast(clampedDistance); const double orbitU = std::atan2(local.y, local.x); const double orbitV = std::asin(std::clamp(local.z, -1.0, 1.0)); @@ -319,7 +319,7 @@ inline bool tryComputeFollowTargetLockMetrics( const auto targetDir = toTarget / targetDistance; const auto dotForward = std::clamp(dot(forward, targetDir), -1.0, 1.0); - outAngleDeg = static_cast(glm::degrees(std::acos(dotForward))); + outAngleDeg = static_cast(hlsl::degrees(std::acos(dotForward))); if (!std::isfinite(outAngleDeg)) return false; @@ -421,6 +421,6 @@ inline CCameraGoalSolver::SApplyResult applyFollowToCamera( return solver.applyDetailed(camera, goal); } -} // namespace nbl::hlsl +} // namespace nbl::core #endif // _C_CAMERA_FOLLOW_UTILITIES_HPP_ diff --git a/common/include/camera/CCameraGoal.hpp b/common/include/camera/CCameraGoal.hpp index 964a3f9b8..866d46097 100644 --- a/common/include/camera/CCameraGoal.hpp +++ b/common/include/camera/CCameraGoal.hpp @@ -11,10 +11,8 @@ #include #include "ICamera.hpp" -#include "CSphericalTargetCamera.hpp" -#include "glm/glm/gtc/quaternion.hpp" -namespace nbl::hlsl +namespace nbl::core { /** @@ -23,7 +21,7 @@ namespace nbl::hlsl struct CCameraGoal { float64_t3 position = float64_t3(0.0); - glm::quat orientation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f); + camera_quaternion_t orientation = makeIdentityQuaternion(); ICamera::CameraKind sourceKind = ICamera::CameraKind::Unknown; uint32_t sourceCapabilities = ICamera::None; uint32_t sourceGoalStateMask = ICamera::GoalStateNone; @@ -41,19 +39,9 @@ struct CCameraGoal ICamera::DynamicPerspectiveState dynamicPerspectiveState = {}; }; -inline double wrapAngleRad(double angle) -{ - constexpr double Pi = 3.14159265358979323846; - while (angle > Pi) - angle -= 2.0 * Pi; - while (angle < -Pi) - angle += 2.0 * Pi; - return angle; -} - inline double lerpWrappedAngleRad(double a, double b, double alpha) { - return a + wrapAngleRad(b - a) * alpha; + return a + hlsl::wrapAngleRad(b - a) * alpha; } inline bool nearlyEqualGoalScalar(double a, double b, double eps = 1e-6) @@ -90,8 +78,8 @@ inline bool applyCanonicalPathGoal(CCameraGoal& goal) const float appliedDistance = std::clamp( static_cast(distance), - CSphericalTargetCamera::MinDistance, - CSphericalTargetCamera::MaxDistance); + ICamera::SphericalMinDistance, + ICamera::SphericalMaxDistance); const auto local = offset / static_cast(appliedDistance); goal.orbitU = std::atan2(local.y, local.x); goal.orbitV = std::asin(std::clamp(local.z, -1.0, 1.0)); @@ -113,7 +101,7 @@ inline bool applyCanonicalPathGoal(CCameraGoal& goal) -std::sin(goal.orbitV) * std::sin(goal.orbitU), std::cos(goal.orbitV))); const float64_t3 right = normalize(cross(up, forward)); - goal.orientation = glm::quat_cast(glm::dmat3{ right, up, forward }); + goal.orientation = makeQuaternionFromBasis(right, up, forward); return true; } @@ -139,12 +127,7 @@ inline bool nearlyEqualVec3(const VecA& a, const VecB& b, const double epsilon) inline bool isGoalFinite(const CCameraGoal& goal) { - auto isFiniteQuat = [](const glm::quat& q) -> bool - { - return std::isfinite(q.x) && std::isfinite(q.y) && std::isfinite(q.z) && std::isfinite(q.w); - }; - - if (!isFiniteVec3(goal.position) || !isFiniteQuat(goal.orientation)) + if (!isFiniteVec3(goal.position) || !isFiniteQuaternion(goal.orientation)) return false; if (goal.hasTargetPosition && !isFiniteVec3(goal.targetPosition)) return false; @@ -163,11 +146,6 @@ inline bool isGoalFinite(const CCameraGoal& goal) inline bool compareGoals(const CCameraGoal& actual, const CCameraGoal& expected, const double posEps, const double rotEpsDeg, const double scalarEps) { - auto isFiniteQuat = [](const glm::quat& q) -> bool - { - return std::isfinite(q.x) && std::isfinite(q.y) && std::isfinite(q.z) && std::isfinite(q.w); - }; - auto angleDiffRad = [](double a, double b) -> double { constexpr double Pi = 3.14159265358979323846; @@ -178,17 +156,16 @@ inline bool compareGoals(const CCameraGoal& actual, const CCameraGoal& expected, return std::abs(d - Pi); }; - const auto currentOrientation = glm::normalize(actual.orientation); - const auto expectedOrientation = glm::normalize(expected.orientation); - if (!isFiniteVec3(actual.position) || !isFiniteVec3(expected.position) || !isFiniteQuat(currentOrientation) || !isFiniteQuat(expectedOrientation)) + const auto currentOrientation = normalizeQuaternion(actual.orientation); + const auto expectedOrientation = normalizeQuaternion(expected.orientation); + if (!isFiniteVec3(actual.position) || !isFiniteVec3(expected.position) || !isFiniteQuaternion(currentOrientation) || !isFiniteQuaternion(expectedOrientation)) return false; const double dx = static_cast(actual.position.x - expected.position.x); const double dy = static_cast(actual.position.y - expected.position.y); const double dz = static_cast(actual.position.z - expected.position.z); const double posDelta = std::sqrt(dx * dx + dy * dy + dz * dz); - const double orientationDot = std::clamp(static_cast(std::abs(glm::dot(currentOrientation, expectedOrientation))), 0.0, 1.0); - const double rotDeltaDeg = glm::degrees(2.0 * std::acos(orientationDot)); + const double rotDeltaDeg = getQuaternionAngularDistanceDegrees(currentOrientation, expectedOrientation); if (posDelta > posEps || rotDeltaDeg > rotEpsDeg) return false; @@ -217,7 +194,7 @@ inline bool compareGoals(const CCameraGoal& actual, const CCameraGoal& expected, { if (!actual.hasPathState) return false; - if (std::abs(wrapAngleRad(expected.pathState.angle - actual.pathState.angle)) > rotEpsDeg * (3.14159265358979323846 / 180.0)) + if (std::abs(hlsl::wrapAngleRad(expected.pathState.angle - actual.pathState.angle)) > rotEpsDeg * (3.14159265358979323846 / 180.0)) return false; if (std::abs(actual.pathState.radius - expected.pathState.radius) > scalarEps) return false; @@ -240,20 +217,19 @@ inline bool compareGoals(const CCameraGoal& actual, const CCameraGoal& expected, inline std::string describeGoalMismatch(const CCameraGoal& actual, const CCameraGoal& expected) { std::ostringstream oss; - const auto currentOrientation = glm::normalize(actual.orientation); - const auto expectedOrientation = glm::normalize(expected.orientation); + const auto currentOrientation = normalizeQuaternion(actual.orientation); + const auto expectedOrientation = normalizeQuaternion(expected.orientation); const double dx = static_cast(actual.position.x - expected.position.x); const double dy = static_cast(actual.position.y - expected.position.y); const double dz = static_cast(actual.position.z - expected.position.z); const double posDelta = std::sqrt(dx * dx + dy * dy + dz * dz); - const double orientationDot = std::clamp(static_cast(std::abs(glm::dot(currentOrientation, expectedOrientation))), 0.0, 1.0); - const double rotDeltaDeg = glm::degrees(2.0 * std::acos(orientationDot)); + const double rotDeltaDeg = getQuaternionAngularDistanceDegrees(currentOrientation, expectedOrientation); oss << "pos_delta=" << posDelta << " rot_delta_deg=" << rotDeltaDeg << " current_pos=(" << actual.position.x << "," << actual.position.y << "," << actual.position.z << ")" << " expected_pos=(" << expected.position.x << "," << expected.position.y << "," << expected.position.z << ")" - << " current_quat=(" << currentOrientation.x << "," << currentOrientation.y << "," << currentOrientation.z << "," << currentOrientation.w << ")" - << " expected_quat=(" << expectedOrientation.x << "," << expectedOrientation.y << "," << expectedOrientation.z << "," << expectedOrientation.w << ")"; + << " current_quat=(" << currentOrientation.data.x << "," << currentOrientation.data.y << "," << currentOrientation.data.z << "," << currentOrientation.data.w << ")" + << " expected_quat=(" << expectedOrientation.data.x << "," << expectedOrientation.data.y << "," << expectedOrientation.data.z << "," << expectedOrientation.data.w << ")"; if (actual.hasTargetPosition) { @@ -295,7 +271,7 @@ inline CCameraGoal blendGoals(const CCameraGoal& a, const CCameraGoal& b, double { CCameraGoal blended; blended.position = a.position + (b.position - a.position) * alpha; - blended.orientation = glm::slerp(a.orientation, b.orientation, static_cast(alpha)); + blended.orientation = slerpQuaternion(a.orientation, b.orientation, static_cast(alpha)); blended.sourceKind = (a.sourceKind == b.sourceKind) ? a.sourceKind : ICamera::CameraKind::Unknown; blended.sourceCapabilities = a.sourceCapabilities & b.sourceCapabilities; blended.sourceGoalStateMask = a.sourceGoalStateMask | b.sourceGoalStateMask; @@ -348,6 +324,6 @@ inline CCameraGoal blendGoals(const CCameraGoal& a, const CCameraGoal& b, double return canonicalizeGoal(blended); } -} // namespace nbl::hlsl +} // namespace nbl::core #endif // _C_CAMERA_GOAL_HPP_ diff --git a/common/include/camera/CCameraGoalAnalysis.hpp b/common/include/camera/CCameraGoalAnalysis.hpp index 160480d30..7146df065 100644 --- a/common/include/camera/CCameraGoalAnalysis.hpp +++ b/common/include/camera/CCameraGoalAnalysis.hpp @@ -8,7 +8,7 @@ #include "CCameraPreset.hpp" #include "CCameraGoalSolver.hpp" -namespace nbl::hlsl +namespace nbl::core { //! Reusable typed answer for `goal/preset -> camera` compatibility checks. @@ -80,6 +80,6 @@ inline SCameraCaptureAnalysis analyzeCameraCapture(const CCameraGoalSolver& solv return analysis; } -} // namespace nbl::hlsl +} // namespace nbl::core #endif // _C_CAMERA_GOAL_ANALYSIS_HPP_ diff --git a/common/include/camera/CCameraGoalSolver.hpp b/common/include/camera/CCameraGoalSolver.hpp index ec618cd6f..bbd42d7b4 100644 --- a/common/include/camera/CCameraGoalSolver.hpp +++ b/common/include/camera/CCameraGoalSolver.hpp @@ -7,12 +7,8 @@ #include #include "CCameraGoal.hpp" -#include "CFPSCamera.hpp" -#include "CFreeLockCamera.hpp" -#include "CSphericalTargetCamera.hpp" -#include "glm/glm/gtc/quaternion.hpp" -namespace nbl::hlsl +namespace nbl::core { /** @@ -425,18 +421,18 @@ class CCameraGoalSolver return rotationScale == 0.0 ? 1.0 : rotationScale; } - inline std::pair computePitchYawFromOrientation(const glm::quat& orientation) const + inline std::pair computePitchYawFromOrientation(const camera_quaternion_t& orientation) const { - const auto mat = glm::mat3_cast(orientation); + const auto mat = getQuaternionBasisMatrix(orientation); const auto forward = float64_t3(mat[2][0], mat[2][1], mat[2][2]); const double pitch = std::atan2(std::sqrt(forward.x * forward.x + forward.z * forward.z), forward.y) - HalfPi; const double yaw = std::atan2(forward.x, forward.z); return { pitch, yaw }; } - inline float64_t3 extractYawPitchRollYXZ(const glm::quat& delta) const + inline float64_t3 extractYawPitchRollYXZ(const camera_quaternion_t& delta) const { - const auto m = getMatrix3x3As4x4(matrix(glm::mat3_cast(delta))); + const auto m = getMatrix3x3As4x4(getQuaternionBasisMatrix(delta)); const double yaw = std::atan2(static_cast(m[2][0]), static_cast(m[2][2])); const double c2 = std::sqrt(static_cast(m[0][1] * m[0][1] + m[1][1] * m[1][1])); const double pitch = std::atan2(-static_cast(m[2][1]), c2); @@ -457,16 +453,15 @@ class CCameraGoalSolver const auto& gimbal = camera->getGimbal(); const auto currentPos = gimbal.getPosition(); - const auto currentOrientation = glm::normalize(gimbal.getOrientation()); - const auto targetOrientation = glm::normalize(target.orientation); + const auto currentOrientation = normalizeQuaternion(gimbal.getOrientation()); + const auto targetOrientation = normalizeQuaternion(target.orientation); const double dx = static_cast(currentPos.x - target.position.x); const double dy = static_cast(currentPos.y - target.position.y); const double dz = static_cast(currentPos.z - target.position.z); outPositionDelta = std::sqrt(dx * dx + dy * dy + dz * dz); - const double orientationDot = std::clamp(static_cast(std::abs(glm::dot(currentOrientation, targetOrientation))), 0.0, 1.0); - outRotationDeltaDeg = glm::degrees(2.0 * std::acos(orientationDot)); + outRotationDeltaDeg = getQuaternionAngularDistanceDegrees(currentOrientation, targetOrientation); return std::isfinite(outPositionDelta) && std::isfinite(outRotationDeltaDeg); } @@ -497,7 +492,7 @@ class CCameraGoalSolver return true; } - auto targetFrame = getMatrix3x3As4x4(matrix(glm::mat3_cast(glm::normalize(target.orientation)))); + auto targetFrame = getMatrix3x3As4x4(getQuaternionBasisMatrix(target.orientation)); targetFrame[3] = float64_t4(target.position, 1.0); camera->manipulate({}, &targetFrame); @@ -682,7 +677,7 @@ class CCameraGoalSolver case ICamera::CameraKind::Free: { - const auto deltaQuat = glm::inverse(gimbal.getOrientation()) * glm::normalize(target.orientation); + const auto deltaQuat = inverseQuaternion(gimbal.getOrientation()) * normalizeQuaternion(target.orientation); const auto angles = extractYawPitchRollYXZ(deltaQuat); appendSignedEvent(out, angles.x, CVirtualGimbalEvent::TiltUp, CVirtualGimbalEvent::TiltDown); @@ -698,6 +693,6 @@ class CCameraGoalSolver } }; -} // namespace nbl::hlsl +} // namespace nbl::core #endif // _C_CAMERA_GOAL_SOLVER_HPP_ diff --git a/common/include/camera/CCameraInputBindingUtilities.hpp b/common/include/camera/CCameraInputBindingUtilities.hpp new file mode 100644 index 000000000..42f6ea0a6 --- /dev/null +++ b/common/include/camera/CCameraInputBindingUtilities.hpp @@ -0,0 +1,390 @@ +#ifndef _NBL_C_CAMERA_INPUT_BINDING_UTILITIES_HPP_ +#define _NBL_C_CAMERA_INPUT_BINDING_UTILITIES_HPP_ + +#include "ICamera.hpp" +#include "IGimbalBindingLayout.hpp" + +namespace nbl::ui +{ + +struct SCameraInputBindingPreset +{ + IGimbalBindingLayout::keyboard_to_virtual_events_t keyboard; + IGimbalBindingLayout::mouse_to_virtual_events_t mouse; + IGimbalBindingLayout::imguizmo_to_virtual_events_t imguizmo; +}; + +namespace impl +{ + +inline IGimbalBindingLayout::keyboard_to_virtual_events_t makeKeyboardPreset( + std::initializer_list> bindings) +{ + IGimbalBindingLayout::keyboard_to_virtual_events_t preset; + for (const auto& [code, event] : bindings) + preset.emplace(code, IGimbalBindingLayout::CHashInfo(event)); + return preset; +} + +inline IGimbalBindingLayout::mouse_to_virtual_events_t makeMousePreset( + std::initializer_list> bindings) +{ + IGimbalBindingLayout::mouse_to_virtual_events_t preset; + for (const auto& [code, event] : bindings) + preset.emplace(code, IGimbalBindingLayout::CHashInfo(event)); + return preset; +} + +inline IGimbalBindingLayout::imguizmo_to_virtual_events_t makeImguizmoPreset(const uint32_t allowedVirtualEvents) +{ + IGimbalBindingLayout::imguizmo_to_virtual_events_t preset; + for (const auto event : core::CVirtualGimbalEvent::VirtualEventsTypeTable) + { + if (event == core::CVirtualGimbalEvent::None) + continue; + if ((allowedVirtualEvents & event) != event) + continue; + preset.emplace(event, IGimbalBindingLayout::CHashInfo(event)); + } + return preset; +} + +inline const IGimbalBindingLayout::keyboard_to_virtual_events_t& emptyKeyboardPreset() +{ + static const auto preset = IGimbalBindingLayout::keyboard_to_virtual_events_t{}; + return preset; +} + +inline const IGimbalBindingLayout::mouse_to_virtual_events_t& emptyMousePreset() +{ + static const auto preset = IGimbalBindingLayout::mouse_to_virtual_events_t{}; + return preset; +} + +inline const IGimbalBindingLayout::keyboard_to_virtual_events_t& fpsKeyboardPreset() +{ + static const auto preset = makeKeyboardPreset({ + { ui::E_KEY_CODE::EKC_W, core::CVirtualGimbalEvent::MoveForward }, + { ui::E_KEY_CODE::EKC_S, core::CVirtualGimbalEvent::MoveBackward }, + { ui::E_KEY_CODE::EKC_A, core::CVirtualGimbalEvent::MoveLeft }, + { ui::E_KEY_CODE::EKC_D, core::CVirtualGimbalEvent::MoveRight }, + { ui::E_KEY_CODE::EKC_I, core::CVirtualGimbalEvent::TiltDown }, + { ui::E_KEY_CODE::EKC_K, core::CVirtualGimbalEvent::TiltUp }, + { ui::E_KEY_CODE::EKC_J, core::CVirtualGimbalEvent::PanLeft }, + { ui::E_KEY_CODE::EKC_L, core::CVirtualGimbalEvent::PanRight } + }); + return preset; +} + +inline const IGimbalBindingLayout::keyboard_to_virtual_events_t& freeKeyboardPreset() +{ + static const auto preset = makeKeyboardPreset({ + { ui::E_KEY_CODE::EKC_W, core::CVirtualGimbalEvent::MoveForward }, + { ui::E_KEY_CODE::EKC_S, core::CVirtualGimbalEvent::MoveBackward }, + { ui::E_KEY_CODE::EKC_A, core::CVirtualGimbalEvent::MoveLeft }, + { ui::E_KEY_CODE::EKC_D, core::CVirtualGimbalEvent::MoveRight }, + { ui::E_KEY_CODE::EKC_I, core::CVirtualGimbalEvent::TiltDown }, + { ui::E_KEY_CODE::EKC_K, core::CVirtualGimbalEvent::TiltUp }, + { ui::E_KEY_CODE::EKC_J, core::CVirtualGimbalEvent::PanLeft }, + { ui::E_KEY_CODE::EKC_L, core::CVirtualGimbalEvent::PanRight }, + { ui::E_KEY_CODE::EKC_Q, core::CVirtualGimbalEvent::RollLeft }, + { ui::E_KEY_CODE::EKC_E, core::CVirtualGimbalEvent::RollRight } + }); + return preset; +} + +inline const IGimbalBindingLayout::keyboard_to_virtual_events_t& orbitKeyboardPreset() +{ + static const auto preset = makeKeyboardPreset({ + { ui::E_KEY_CODE::EKC_W, core::CVirtualGimbalEvent::MoveUp }, + { ui::E_KEY_CODE::EKC_S, core::CVirtualGimbalEvent::MoveDown }, + { ui::E_KEY_CODE::EKC_A, core::CVirtualGimbalEvent::MoveLeft }, + { ui::E_KEY_CODE::EKC_D, core::CVirtualGimbalEvent::MoveRight }, + { ui::E_KEY_CODE::EKC_E, core::CVirtualGimbalEvent::MoveForward }, + { ui::E_KEY_CODE::EKC_Q, core::CVirtualGimbalEvent::MoveBackward } + }); + return preset; +} + +inline const IGimbalBindingLayout::keyboard_to_virtual_events_t& arcballKeyboardPreset() +{ + static const auto preset = makeKeyboardPreset({ + { ui::E_KEY_CODE::EKC_W, core::CVirtualGimbalEvent::MoveForward }, + { ui::E_KEY_CODE::EKC_S, core::CVirtualGimbalEvent::MoveBackward }, + { ui::E_KEY_CODE::EKC_A, core::CVirtualGimbalEvent::MoveLeft }, + { ui::E_KEY_CODE::EKC_D, core::CVirtualGimbalEvent::MoveRight }, + { ui::E_KEY_CODE::EKC_Q, core::CVirtualGimbalEvent::MoveDown }, + { ui::E_KEY_CODE::EKC_E, core::CVirtualGimbalEvent::MoveUp }, + { ui::E_KEY_CODE::EKC_I, core::CVirtualGimbalEvent::TiltDown }, + { ui::E_KEY_CODE::EKC_K, core::CVirtualGimbalEvent::TiltUp }, + { ui::E_KEY_CODE::EKC_J, core::CVirtualGimbalEvent::PanLeft }, + { ui::E_KEY_CODE::EKC_L, core::CVirtualGimbalEvent::PanRight } + }); + return preset; +} + +inline const IGimbalBindingLayout::keyboard_to_virtual_events_t& turntableKeyboardPreset() +{ + static const auto preset = makeKeyboardPreset({ + { ui::E_KEY_CODE::EKC_W, core::CVirtualGimbalEvent::MoveForward }, + { ui::E_KEY_CODE::EKC_S, core::CVirtualGimbalEvent::MoveBackward }, + { ui::E_KEY_CODE::EKC_A, core::CVirtualGimbalEvent::PanLeft }, + { ui::E_KEY_CODE::EKC_D, core::CVirtualGimbalEvent::PanRight }, + { ui::E_KEY_CODE::EKC_I, core::CVirtualGimbalEvent::TiltDown }, + { ui::E_KEY_CODE::EKC_K, core::CVirtualGimbalEvent::TiltUp }, + { ui::E_KEY_CODE::EKC_J, core::CVirtualGimbalEvent::PanLeft }, + { ui::E_KEY_CODE::EKC_L, core::CVirtualGimbalEvent::PanRight } + }); + return preset; +} + +inline const IGimbalBindingLayout::keyboard_to_virtual_events_t& topDownKeyboardPreset() +{ + static const auto preset = makeKeyboardPreset({ + { ui::E_KEY_CODE::EKC_W, core::CVirtualGimbalEvent::MoveUp }, + { ui::E_KEY_CODE::EKC_S, core::CVirtualGimbalEvent::MoveDown }, + { ui::E_KEY_CODE::EKC_A, core::CVirtualGimbalEvent::MoveLeft }, + { ui::E_KEY_CODE::EKC_D, core::CVirtualGimbalEvent::MoveRight }, + { ui::E_KEY_CODE::EKC_Q, core::CVirtualGimbalEvent::MoveBackward }, + { ui::E_KEY_CODE::EKC_E, core::CVirtualGimbalEvent::MoveForward }, + { ui::E_KEY_CODE::EKC_J, core::CVirtualGimbalEvent::PanLeft }, + { ui::E_KEY_CODE::EKC_L, core::CVirtualGimbalEvent::PanRight } + }); + return preset; +} + +inline const IGimbalBindingLayout::keyboard_to_virtual_events_t& isometricKeyboardPreset() +{ + static const auto preset = makeKeyboardPreset({ + { ui::E_KEY_CODE::EKC_W, core::CVirtualGimbalEvent::MoveUp }, + { ui::E_KEY_CODE::EKC_S, core::CVirtualGimbalEvent::MoveDown }, + { ui::E_KEY_CODE::EKC_A, core::CVirtualGimbalEvent::MoveLeft }, + { ui::E_KEY_CODE::EKC_D, core::CVirtualGimbalEvent::MoveRight }, + { ui::E_KEY_CODE::EKC_Q, core::CVirtualGimbalEvent::MoveBackward }, + { ui::E_KEY_CODE::EKC_E, core::CVirtualGimbalEvent::MoveForward } + }); + return preset; +} + +inline const IGimbalBindingLayout::keyboard_to_virtual_events_t& chaseKeyboardPreset() +{ + return arcballKeyboardPreset(); +} + +inline const IGimbalBindingLayout::keyboard_to_virtual_events_t& dollyKeyboardPreset() +{ + return arcballKeyboardPreset(); +} + +inline const IGimbalBindingLayout::keyboard_to_virtual_events_t& dollyZoomKeyboardPreset() +{ + static const auto preset = makeKeyboardPreset({ + { ui::E_KEY_CODE::EKC_W, core::CVirtualGimbalEvent::MoveUp }, + { ui::E_KEY_CODE::EKC_S, core::CVirtualGimbalEvent::MoveDown }, + { ui::E_KEY_CODE::EKC_A, core::CVirtualGimbalEvent::MoveLeft }, + { ui::E_KEY_CODE::EKC_D, core::CVirtualGimbalEvent::MoveRight }, + { ui::E_KEY_CODE::EKC_E, core::CVirtualGimbalEvent::MoveForward }, + { ui::E_KEY_CODE::EKC_Q, core::CVirtualGimbalEvent::MoveBackward } + }); + return preset; +} + +inline const IGimbalBindingLayout::keyboard_to_virtual_events_t& pathKeyboardPreset() +{ + static const auto preset = makeKeyboardPreset({ + { ui::E_KEY_CODE::EKC_W, core::CVirtualGimbalEvent::MoveForward }, + { ui::E_KEY_CODE::EKC_S, core::CVirtualGimbalEvent::MoveBackward }, + { ui::E_KEY_CODE::EKC_A, core::CVirtualGimbalEvent::MoveLeft }, + { ui::E_KEY_CODE::EKC_D, core::CVirtualGimbalEvent::MoveRight }, + { ui::E_KEY_CODE::EKC_Q, core::CVirtualGimbalEvent::MoveDown }, + { ui::E_KEY_CODE::EKC_E, core::CVirtualGimbalEvent::MoveUp } + }); + return preset; +} + +inline const IGimbalBindingLayout::mouse_to_virtual_events_t& fpsMousePreset() +{ + static const auto preset = makeMousePreset({ + { ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_X, core::CVirtualGimbalEvent::PanRight }, + { ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_X, core::CVirtualGimbalEvent::PanLeft }, + { ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_Y, core::CVirtualGimbalEvent::TiltUp }, + { ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_Y, core::CVirtualGimbalEvent::TiltDown } + }); + return preset; +} + +inline const IGimbalBindingLayout::mouse_to_virtual_events_t& freeMousePreset() +{ + return fpsMousePreset(); +} + +inline const IGimbalBindingLayout::mouse_to_virtual_events_t& orbitMousePreset() +{ + static const auto preset = makeMousePreset({ + { ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_X, core::CVirtualGimbalEvent::MoveRight }, + { ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_X, core::CVirtualGimbalEvent::MoveLeft }, + { ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_Y, core::CVirtualGimbalEvent::MoveUp }, + { ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_Y, core::CVirtualGimbalEvent::MoveDown }, + { ui::E_MOUSE_CODE::EMC_VERTICAL_POSITIVE_SCROLL, core::CVirtualGimbalEvent::MoveForward }, + { ui::E_MOUSE_CODE::EMC_HORIZONTAL_POSITIVE_SCROLL, core::CVirtualGimbalEvent::MoveForward }, + { ui::E_MOUSE_CODE::EMC_VERTICAL_NEGATIVE_SCROLL, core::CVirtualGimbalEvent::MoveBackward }, + { ui::E_MOUSE_CODE::EMC_HORIZONTAL_NEGATIVE_SCROLL, core::CVirtualGimbalEvent::MoveBackward } + }); + return preset; +} + +inline const IGimbalBindingLayout::mouse_to_virtual_events_t& arcballMousePreset() +{ + static const auto preset = makeMousePreset({ + { ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_X, core::CVirtualGimbalEvent::PanRight }, + { ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_X, core::CVirtualGimbalEvent::PanLeft }, + { ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_Y, core::CVirtualGimbalEvent::TiltUp }, + { ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_Y, core::CVirtualGimbalEvent::TiltDown }, + { ui::E_MOUSE_CODE::EMC_VERTICAL_POSITIVE_SCROLL, core::CVirtualGimbalEvent::MoveForward }, + { ui::E_MOUSE_CODE::EMC_HORIZONTAL_POSITIVE_SCROLL, core::CVirtualGimbalEvent::MoveForward }, + { ui::E_MOUSE_CODE::EMC_VERTICAL_NEGATIVE_SCROLL, core::CVirtualGimbalEvent::MoveBackward }, + { ui::E_MOUSE_CODE::EMC_HORIZONTAL_NEGATIVE_SCROLL, core::CVirtualGimbalEvent::MoveBackward } + }); + return preset; +} + +inline const IGimbalBindingLayout::mouse_to_virtual_events_t& turntableMousePreset() +{ + return arcballMousePreset(); +} + +inline const IGimbalBindingLayout::mouse_to_virtual_events_t& topDownMousePreset() +{ + static const auto preset = makeMousePreset({ + { ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_X, core::CVirtualGimbalEvent::PanRight }, + { ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_X, core::CVirtualGimbalEvent::PanLeft }, + { ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_Y, core::CVirtualGimbalEvent::MoveUp }, + { ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_Y, core::CVirtualGimbalEvent::MoveDown }, + { ui::E_MOUSE_CODE::EMC_VERTICAL_POSITIVE_SCROLL, core::CVirtualGimbalEvent::MoveForward }, + { ui::E_MOUSE_CODE::EMC_HORIZONTAL_POSITIVE_SCROLL, core::CVirtualGimbalEvent::MoveForward }, + { ui::E_MOUSE_CODE::EMC_VERTICAL_NEGATIVE_SCROLL, core::CVirtualGimbalEvent::MoveBackward }, + { ui::E_MOUSE_CODE::EMC_HORIZONTAL_NEGATIVE_SCROLL, core::CVirtualGimbalEvent::MoveBackward } + }); + return preset; +} + +inline const IGimbalBindingLayout::mouse_to_virtual_events_t& isometricMousePreset() +{ + static const auto preset = makeMousePreset({ + { ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_X, core::CVirtualGimbalEvent::MoveRight }, + { ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_X, core::CVirtualGimbalEvent::MoveLeft }, + { ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_Y, core::CVirtualGimbalEvent::MoveUp }, + { ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_Y, core::CVirtualGimbalEvent::MoveDown }, + { ui::E_MOUSE_CODE::EMC_VERTICAL_POSITIVE_SCROLL, core::CVirtualGimbalEvent::MoveForward }, + { ui::E_MOUSE_CODE::EMC_HORIZONTAL_POSITIVE_SCROLL, core::CVirtualGimbalEvent::MoveForward }, + { ui::E_MOUSE_CODE::EMC_VERTICAL_NEGATIVE_SCROLL, core::CVirtualGimbalEvent::MoveBackward }, + { ui::E_MOUSE_CODE::EMC_HORIZONTAL_NEGATIVE_SCROLL, core::CVirtualGimbalEvent::MoveBackward } + }); + return preset; +} + +inline const IGimbalBindingLayout::mouse_to_virtual_events_t& chaseMousePreset() +{ + static const auto preset = makeMousePreset({ + { ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_X, core::CVirtualGimbalEvent::PanRight }, + { ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_X, core::CVirtualGimbalEvent::PanLeft }, + { ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_Y, core::CVirtualGimbalEvent::TiltUp }, + { ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_Y, core::CVirtualGimbalEvent::TiltDown }, + { ui::E_MOUSE_CODE::EMC_VERTICAL_POSITIVE_SCROLL, core::CVirtualGimbalEvent::MoveUp }, + { ui::E_MOUSE_CODE::EMC_HORIZONTAL_POSITIVE_SCROLL, core::CVirtualGimbalEvent::MoveUp }, + { ui::E_MOUSE_CODE::EMC_VERTICAL_NEGATIVE_SCROLL, core::CVirtualGimbalEvent::MoveDown }, + { ui::E_MOUSE_CODE::EMC_HORIZONTAL_NEGATIVE_SCROLL, core::CVirtualGimbalEvent::MoveDown } + }); + return preset; +} + +inline const IGimbalBindingLayout::mouse_to_virtual_events_t& dollyMousePreset() +{ + static const auto preset = makeMousePreset({ + { ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_X, core::CVirtualGimbalEvent::PanRight }, + { ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_X, core::CVirtualGimbalEvent::PanLeft }, + { ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_Y, core::CVirtualGimbalEvent::TiltUp }, + { ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_Y, core::CVirtualGimbalEvent::TiltDown }, + { ui::E_MOUSE_CODE::EMC_VERTICAL_POSITIVE_SCROLL, core::CVirtualGimbalEvent::MoveForward }, + { ui::E_MOUSE_CODE::EMC_HORIZONTAL_POSITIVE_SCROLL, core::CVirtualGimbalEvent::MoveForward }, + { ui::E_MOUSE_CODE::EMC_VERTICAL_NEGATIVE_SCROLL, core::CVirtualGimbalEvent::MoveBackward }, + { ui::E_MOUSE_CODE::EMC_HORIZONTAL_NEGATIVE_SCROLL, core::CVirtualGimbalEvent::MoveBackward } + }); + return preset; +} + +inline const IGimbalBindingLayout::mouse_to_virtual_events_t& dollyZoomMousePreset() +{ + return isometricMousePreset(); +} + +inline const IGimbalBindingLayout::mouse_to_virtual_events_t& pathMousePreset() +{ + return isometricMousePreset(); +} + +} // namespace impl + +inline const IGimbalBindingLayout::keyboard_to_virtual_events_t& getDefaultCameraKeyboardMappingPreset(const core::ICamera& camera) +{ + switch (camera.getKind()) + { + case core::ICamera::CameraKind::FPS: return impl::fpsKeyboardPreset(); + case core::ICamera::CameraKind::Free: return impl::freeKeyboardPreset(); + case core::ICamera::CameraKind::Orbit: return impl::orbitKeyboardPreset(); + case core::ICamera::CameraKind::Arcball: return impl::arcballKeyboardPreset(); + case core::ICamera::CameraKind::Turntable: return impl::turntableKeyboardPreset(); + case core::ICamera::CameraKind::TopDown: return impl::topDownKeyboardPreset(); + case core::ICamera::CameraKind::Isometric: return impl::isometricKeyboardPreset(); + case core::ICamera::CameraKind::Chase: return impl::chaseKeyboardPreset(); + case core::ICamera::CameraKind::Dolly: return impl::dollyKeyboardPreset(); + case core::ICamera::CameraKind::DollyZoom: return impl::dollyZoomKeyboardPreset(); + case core::ICamera::CameraKind::Path: return impl::pathKeyboardPreset(); + default: return impl::emptyKeyboardPreset(); + } +} + +inline const IGimbalBindingLayout::mouse_to_virtual_events_t& getDefaultCameraMouseMappingPreset(const core::ICamera& camera) +{ + switch (camera.getKind()) + { + case core::ICamera::CameraKind::FPS: return impl::fpsMousePreset(); + case core::ICamera::CameraKind::Free: return impl::freeMousePreset(); + case core::ICamera::CameraKind::Orbit: return impl::orbitMousePreset(); + case core::ICamera::CameraKind::Arcball: return impl::arcballMousePreset(); + case core::ICamera::CameraKind::Turntable: return impl::turntableMousePreset(); + case core::ICamera::CameraKind::TopDown: return impl::topDownMousePreset(); + case core::ICamera::CameraKind::Isometric: return impl::isometricMousePreset(); + case core::ICamera::CameraKind::Chase: return impl::chaseMousePreset(); + case core::ICamera::CameraKind::Dolly: return impl::dollyMousePreset(); + case core::ICamera::CameraKind::DollyZoom: return impl::dollyZoomMousePreset(); + case core::ICamera::CameraKind::Path: return impl::pathMousePreset(); + default: return impl::emptyMousePreset(); + } +} + +inline IGimbalBindingLayout::imguizmo_to_virtual_events_t buildDefaultCameraImguizmoMappingPreset(const core::ICamera& camera) +{ + return impl::makeImguizmoPreset(camera.getAllowedVirtualEvents()); +} + +inline SCameraInputBindingPreset buildDefaultCameraInputBindingPreset(const core::ICamera& camera) +{ + SCameraInputBindingPreset preset; + preset.keyboard = getDefaultCameraKeyboardMappingPreset(camera); + preset.mouse = getDefaultCameraMouseMappingPreset(camera); + preset.imguizmo = buildDefaultCameraImguizmoMappingPreset(camera); + return preset; +} + +inline void applyDefaultCameraInputBindingPreset(IGimbalBindingLayout& layout, const core::ICamera& camera) +{ + const auto preset = buildDefaultCameraInputBindingPreset(camera); + layout.updateKeyboardMapping([&](auto& map) { map = preset.keyboard; }); + layout.updateMouseMapping([&](auto& map) { map = preset.mouse; }); + layout.updateImguizmoMapping([&](auto& map) { map = preset.imguizmo; }); +} + +} // namespace nbl::ui + +#endif // _NBL_C_CAMERA_INPUT_BINDING_UTILITIES_HPP_ diff --git a/common/include/camera/CCameraKeyframeTrack.hpp b/common/include/camera/CCameraKeyframeTrack.hpp index edf991078..822467e68 100644 --- a/common/include/camera/CCameraKeyframeTrack.hpp +++ b/common/include/camera/CCameraKeyframeTrack.hpp @@ -11,7 +11,7 @@ #include "CCameraPreset.hpp" -namespace nbl::hlsl +namespace nbl::core { //! Reusable keyframe container plus selection state for playback tooling. @@ -161,41 +161,6 @@ inline bool replaceSelectedKeyframePreset(CCameraKeyframeTrack& track, CCameraPr return true; } -inline nlohmann::json serializeKeyframeTrack(const CCameraKeyframeTrack& track) -{ - nlohmann::json root; - root["keyframes"] = nlohmann::json::array(); - - for (const auto& keyframe : track.keyframes) - { - auto j = serializePreset(keyframe.preset); - j["time"] = keyframe.time; - root["keyframes"].push_back(std::move(j)); - } - - return root; -} - -inline bool deserializeKeyframeTrack(const nlohmann::json& root, CCameraKeyframeTrack& track) -{ - if (!root.contains("keyframes")) - return false; - - track = {}; - for (const auto& entry : root["keyframes"]) - { - CCameraKeyframe keyframe; - if (entry.contains("time")) - keyframe.time = std::max(0.f, entry["time"].get()); - deserializePreset(entry, keyframe.preset); - track.keyframes.emplace_back(std::move(keyframe)); - } - - sortKeyframeTrackByTime(track); - normalizeSelectedKeyframeTrack(track); - return true; -} - -} // namespace nbl::hlsl +} // namespace nbl::core #endif // _C_CAMERA_KEYFRAME_TRACK_HPP_ diff --git a/common/include/camera/CCameraKeyframeTrackPersistence.hpp b/common/include/camera/CCameraKeyframeTrackPersistence.hpp new file mode 100644 index 000000000..2deedc859 --- /dev/null +++ b/common/include/camera/CCameraKeyframeTrackPersistence.hpp @@ -0,0 +1,24 @@ +// 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_KEYFRAME_TRACK_PERSISTENCE_HPP_ +#define _C_CAMERA_KEYFRAME_TRACK_PERSISTENCE_HPP_ + +#include + +#include "CCameraKeyframeTrack.hpp" +#include "nbl/system/path.h" + +namespace nbl::system +{ + +bool writeKeyframeTrack(std::ostream& out, const core::CCameraKeyframeTrack& track, int indent = 2); +bool readKeyframeTrack(std::istream& in, core::CCameraKeyframeTrack& track); + +bool saveKeyframeTrackToFile(const path& path, const core::CCameraKeyframeTrack& track, int indent = 2); +bool loadKeyframeTrackFromFile(const path& path, core::CCameraKeyframeTrack& track); + +} // namespace nbl::system + +#endif // _C_CAMERA_KEYFRAME_TRACK_PERSISTENCE_HPP_ diff --git a/common/include/camera/CCameraManipulationUtilities.hpp b/common/include/camera/CCameraManipulationUtilities.hpp index a144579ca..d35d0de81 100644 --- a/common/include/camera/CCameraManipulationUtilities.hpp +++ b/common/include/camera/CCameraManipulationUtilities.hpp @@ -10,7 +10,7 @@ #include "CCameraPresetFlow.hpp" -namespace nbl::hlsl +namespace nbl::core { //! Reusable constraint settings for post-manipulation camera clamping. @@ -31,6 +31,15 @@ struct SCameraConstraintSettings float maxDistance = 1000.f; }; +//! Apply an authored world-space reference frame through the shared camera runtime entry point. +inline bool applyReferenceFrameToCamera(ICamera* camera, const float64_t4x4& referenceFrame) +{ + if (!camera) + return false; + + return camera->manipulateWithUnitMotionScales({}, &referenceFrame); +} + //! Scale translation and rotation event magnitudes without touching unrelated event types. inline void scaleVirtualEvents(std::vector& events, const uint32_t count, const float translationScale, const float rotationScale) { @@ -143,25 +152,25 @@ inline bool applyCameraConstraints(const CCameraGoalSolver& solver, ICamera* cam const auto& gimbal = camera->getGimbal(); const auto pos = gimbal.getPosition(); - const auto eulerDeg = glm::degrees(glm::eulerAngles(gimbal.getOrientation())); + const auto eulerDeg = getQuaternionEulerDegrees(gimbal.getOrientation()); auto clamped = eulerDeg; if (constraints.clampPitch) - clamped.x = std::clamp(clamped.x, constraints.pitchMinDeg, constraints.pitchMaxDeg); + clamped.x = std::clamp(clamped.x, static_cast(constraints.pitchMinDeg), static_cast(constraints.pitchMaxDeg)); if (constraints.clampYaw) - clamped.y = std::clamp(clamped.y, constraints.yawMinDeg, constraints.yawMaxDeg); + clamped.y = std::clamp(clamped.y, static_cast(constraints.yawMinDeg), static_cast(constraints.yawMaxDeg)); if (constraints.clampRoll) - clamped.z = std::clamp(clamped.z, constraints.rollMinDeg, constraints.rollMaxDeg); + 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; CCameraPreset preset; preset.goal.position = pos; - preset.goal.orientation = glm::quat(hlsl::radians(clamped)); + preset.goal.orientation = makeQuaternionFromEulerDegrees(clamped); return applyPreset(solver, camera, preset); } -} // namespace nbl::hlsl +} // namespace nbl::core #endif // _C_CAMERA_MANIPULATION_UTILITIES_HPP_ diff --git a/common/include/camera/CCameraMathUtilities.hpp b/common/include/camera/CCameraMathUtilities.hpp new file mode 100644 index 000000000..aa3721385 --- /dev/null +++ b/common/include/camera/CCameraMathUtilities.hpp @@ -0,0 +1,359 @@ +#ifndef _C_CAMERA_MATH_UTILITIES_HPP_ +#define _C_CAMERA_MATH_UTILITIES_HPP_ + +#include +#include +#include +#include + +#include "nbl/builtin/hlsl/matrix_utils/transformation_matrix_utils.hlsl" + +namespace nbl::hlsl +{ + +template +inline T wrapAngleRad(T angle) +{ + constexpr T Pi = static_cast(3.141592653589793238462643383279502884L); + constexpr T TwoPi = Pi * static_cast(2); + + angle = std::fmod(angle + Pi, TwoPi); + if (angle < static_cast(0)) + angle += TwoPi; + return angle - Pi; +} + +template +using camera_vector_t = vector; + +template +using camera_matrix_t = matrix; + +template +using camera_quaternion_t = math::quaternion; + +template +inline camera_quaternion_t makeIdentityQuaternion() +{ + return camera_quaternion_t::create(); +} + +template +inline camera_quaternion_t makeQuaternionFromComponents(const T x, const T y, const T z, const T w) +{ + camera_quaternion_t output; + output.data = camera_vector_t(x, y, z, w); + return output; +} + +template +inline camera_quaternion_t normalizeQuaternion(const camera_quaternion_t& q) +{ + return normalize(q); +} + +template +inline bool isFiniteQuaternion(const camera_quaternion_t& q) +{ + return std::isfinite(q.data.x) && + std::isfinite(q.data.y) && + std::isfinite(q.data.z) && + std::isfinite(q.data.w); +} + +template +inline camera_quaternion_t makeQuaternionFromAxisAngle(const camera_vector_t& axis, const T radians) +{ + return camera_quaternion_t::create(axis, radians); +} + +template +inline camera_quaternion_t makeQuaternionFromEulerRadians(const camera_vector_t& eulerRadians) +{ + return camera_quaternion_t::create(eulerRadians.x, eulerRadians.y, eulerRadians.z); +} + +template +inline camera_quaternion_t makeQuaternionFromEulerDegrees(const camera_vector_t& eulerDegrees) +{ + return makeQuaternionFromEulerRadians(camera_vector_t( + radians(eulerDegrees.x), + radians(eulerDegrees.y), + radians(eulerDegrees.z))); +} + +template +inline camera_quaternion_t makeQuaternionFromBasis( + const camera_vector_t& right, + const camera_vector_t& up, + const camera_vector_t& forward) +{ + const auto safeNormalize = [](const camera_vector_t& v, const camera_vector_t& fallback) + { + const auto len = length(v); + if (!std::isfinite(len) || len <= std::numeric_limits::epsilon()) + return fallback; + return v / len; + }; + + const auto canonicalForward = safeNormalize(forward, camera_vector_t(T(0), T(0), T(1))); + + auto canonicalRight = right - canonicalForward * dot(right, canonicalForward); + canonicalRight = safeNormalize( + canonicalRight, + safeNormalize(cross(up, canonicalForward), camera_vector_t(T(1), T(0), T(0)))); + + auto canonicalUp = cross(canonicalForward, canonicalRight); + canonicalUp = safeNormalize( + canonicalUp, + safeNormalize(up - canonicalForward * dot(up, canonicalForward), camera_vector_t(T(0), T(1), T(0)))); + + canonicalRight = safeNormalize(cross(canonicalUp, canonicalForward), canonicalRight); + canonicalUp = safeNormalize(cross(canonicalForward, canonicalRight), canonicalUp); + + const camera_matrix_t basis { canonicalRight, canonicalUp, canonicalForward }; + const auto desiredRight = canonicalRight; + const auto desiredUp = canonicalUp; + const auto desiredForward = canonicalForward; + + const auto scoreCandidate = [&](const camera_quaternion_t& candidate) + { + if (!isFiniteQuaternion(candidate)) + return std::numeric_limits::infinity(); + + const auto normalizedCandidate = normalizeQuaternion(candidate); + const auto rebuiltRight = normalizedCandidate.transformVector(camera_vector_t(T(1), T(0), T(0)), true); + const auto rebuiltUp = normalizedCandidate.transformVector(camera_vector_t(T(0), T(1), T(0)), true); + const auto rebuiltForward = normalizedCandidate.transformVector(camera_vector_t(T(0), T(0), T(1)), true); + + const T rightError = length(rebuiltRight - desiredRight); + const T upError = length(rebuiltUp - desiredUp); + const T forwardError = length(rebuiltForward - desiredForward); + return rightError + upError + forwardError; + }; + + const auto quaternionFromMatrixFallback = [&](const camera_matrix_t& m) + { + const T m00 = m[0][0]; + const T m11 = m[1][1]; + const T m22 = m[2][2]; + const T trace = m00 + m11 + m22; + + camera_quaternion_t output = makeIdentityQuaternion(); + if (trace > T(0)) + { + const T scale = std::sqrt(trace + T(1)); + const T invScale = T(0.5) / scale; + output.data.x = (m[2][1] - m[1][2]) * invScale; + output.data.y = (m[0][2] - m[2][0]) * invScale; + output.data.z = (m[1][0] - m[0][1]) * invScale; + output.data.w = scale * T(0.5); + } + else if (m00 >= m11 && m00 >= m22) + { + const T scale = std::sqrt(T(1) + m00 - m11 - m22); + const T invScale = T(0.5) / scale; + output.data.x = scale * T(0.5); + output.data.y = (m[0][1] + m[1][0]) * invScale; + output.data.z = (m[2][0] + m[0][2]) * invScale; + output.data.w = (m[2][1] - m[1][2]) * invScale; + } + else if (m11 >= m22) + { + const T scale = std::sqrt(T(1) + m11 - m00 - m22); + const T invScale = T(0.5) / scale; + output.data.x = (m[0][1] + m[1][0]) * invScale; + output.data.y = scale * T(0.5); + output.data.z = (m[1][2] + m[2][1]) * invScale; + output.data.w = (m[0][2] - m[2][0]) * invScale; + } + else + { + const T scale = std::sqrt(T(1) + m22 - m00 - m11); + const T invScale = T(0.5) / scale; + output.data.x = (m[2][0] + m[0][2]) * invScale; + output.data.y = (m[1][2] + m[2][1]) * invScale; + output.data.z = scale * T(0.5); + output.data.w = (m[1][0] - m[0][1]) * invScale; + } + return normalizeQuaternion(output); + }; + + const camera_matrix_t transposedBasis = hlsl::transpose(basis); + const camera_quaternion_t castCandidates[] = { + normalizeQuaternion(hlsl::_static_cast>(basis)), + normalizeQuaternion(hlsl::_static_cast>(transposedBasis)) + }; + const camera_quaternion_t fallbackCandidates[] = { + quaternionFromMatrixFallback(basis), + quaternionFromMatrixFallback(transposedBasis) + }; + + camera_quaternion_t bestCandidate = makeIdentityQuaternion(); + T bestScore = std::numeric_limits::infinity(); + bool foundFiniteCandidate = false; + + for (const auto& candidate : castCandidates) + { + const T score = scoreCandidate(candidate); + if (score < bestScore) + { + bestScore = score; + bestCandidate = candidate; + foundFiniteCandidate = true; + } + } + + if (!foundFiniteCandidate) + { + for (const auto& candidate : fallbackCandidates) + { + const T score = scoreCandidate(candidate); + if (score < bestScore) + { + bestScore = score; + bestCandidate = candidate; + foundFiniteCandidate = true; + } + } + } + + if (!foundFiniteCandidate) + return makeIdentityQuaternion(); + + return normalizeQuaternion(bestCandidate); +} + +template +inline camera_vector_t rotateVectorByQuaternion(const camera_quaternion_t& orientation, const camera_vector_t& vectorToRotate) +{ + return normalizeQuaternion(orientation).transformVector(vectorToRotate, true); +} + +template +inline camera_vector_t getQuaternionEulerRadians(const camera_quaternion_t& orientation) +{ + const auto q = normalizeQuaternion(orientation); + const T x = q.data.x; + const T y = q.data.y; + const T z = q.data.z; + const T w = q.data.w; + + const T pitch = std::atan2( + T(2) * (y * z + w * x), + w * w - x * x - y * y + z * z); + const T yaw = std::asin(std::clamp( + T(-2) * (x * z - w * y), + T(-1), + T(1))); + const T roll = std::atan2( + T(2) * (x * y + w * z), + w * w + x * x - y * y - z * z); + + return camera_vector_t(pitch, yaw, roll); +} + +template +inline camera_vector_t getQuaternionEulerDegrees(const camera_quaternion_t& orientation) +{ + const auto eulerRadians = getQuaternionEulerRadians(orientation); + return camera_vector_t( + degrees(eulerRadians.x), + degrees(eulerRadians.y), + degrees(eulerRadians.z)); +} + +template +inline T getQuaternionAngularDistanceRadians(const camera_quaternion_t& lhs, const camera_quaternion_t& rhs) +{ + const auto lhsNormalized = normalizeQuaternion(lhs); + const auto rhsNormalized = normalizeQuaternion(rhs); + const T orientationDot = std::clamp( + static_cast(std::abs(dot(lhsNormalized.data, rhsNormalized.data))), + T(0), + T(1)); + return T(2) * std::acos(orientationDot); +} + +template +inline T getQuaternionAngularDistanceDegrees(const camera_quaternion_t& lhs, const camera_quaternion_t& rhs) +{ + return degrees(getQuaternionAngularDistanceRadians(lhs, rhs)); +} + +template +inline camera_quaternion_t slerpQuaternion(const camera_quaternion_t& lhs, const camera_quaternion_t& rhs, const T alpha) +{ + return camera_quaternion_t::slerp(normalizeQuaternion(lhs), normalizeQuaternion(rhs), alpha); +} + +template +inline camera_quaternion_t inverseQuaternion(const camera_quaternion_t& q) +{ + return inverse(q); +} + +template +inline camera_matrix_t getQuaternionBasisMatrix(const camera_quaternion_t& orientation) +{ + const auto q = normalizeQuaternion(orientation); + return camera_matrix_t( + q.transformVector(camera_vector_t(T(1), T(0), T(0)), true), + q.transformVector(camera_vector_t(T(0), T(1), T(0)), true), + q.transformVector(camera_vector_t(T(0), T(0), T(1)), true)); +} + +template +inline camera_matrix_t composeTransformMatrix( + const camera_vector_t& translation, + const camera_quaternion_t& orientation, + const camera_vector_t& scale = camera_vector_t(T(1))) +{ + camera_matrix_t output = camera_matrix_t(1); + const auto basis = getQuaternionBasisMatrix(orientation); + output[0] = camera_vector_t(basis[0] * scale.x, T(0)); + output[1] = camera_vector_t(basis[1] * scale.y, T(0)); + output[2] = camera_vector_t(basis[2] * scale.z, T(0)); + output[3] = camera_vector_t(translation, T(1)); + return output; +} + +template +inline bool decomposeTransformMatrix( + const camera_matrix_t& transform, + camera_vector_t& outTranslation, + camera_vector_t& outRotationEulerDegrees, + camera_vector_t& outScale) +{ + outTranslation = camera_vector_t(transform[3].x, transform[3].y, transform[3].z); + + auto right = camera_vector_t(transform[0].x, transform[0].y, transform[0].z); + auto up = camera_vector_t(transform[1].x, transform[1].y, transform[1].z); + auto forward = camera_vector_t(transform[2].x, transform[2].y, transform[2].z); + + outScale = camera_vector_t(length(right), length(up), length(forward)); + + if (!std::isfinite(outScale.x) || !std::isfinite(outScale.y) || !std::isfinite(outScale.z)) + return false; + + constexpr T Epsilon = std::numeric_limits::epsilon(); + if (outScale.x <= Epsilon || outScale.y <= Epsilon || outScale.z <= Epsilon) + return false; + + right /= outScale.x; + up /= outScale.y; + forward /= outScale.z; + + const auto orientation = makeQuaternionFromBasis(right, up, forward); + if (!isFiniteQuaternion(orientation)) + return false; + + outRotationEulerDegrees = getQuaternionEulerDegrees(orientation); + return std::isfinite(outRotationEulerDegrees.x) && + std::isfinite(outRotationEulerDegrees.y) && + std::isfinite(outRotationEulerDegrees.z); +} + +} // namespace nbl::hlsl + +#endif // _C_CAMERA_MATH_UTILITIES_HPP_ diff --git a/common/include/camera/CCameraPersistence.hpp b/common/include/camera/CCameraPersistence.hpp index a1fb737fb..046476504 100644 --- a/common/include/camera/CCameraPersistence.hpp +++ b/common/include/camera/CCameraPersistence.hpp @@ -5,115 +5,23 @@ #ifndef _C_CAMERA_PERSISTENCE_HPP_ #define _C_CAMERA_PERSISTENCE_HPP_ -#include +#include #include +#include -#include "CCameraKeyframeTrack.hpp" +#include "CCameraKeyframeTrackPersistence.hpp" +#include "CCameraPresetPersistence.hpp" #include "nbl/system/path.h" -namespace nbl::hlsl +namespace nbl::system { -//! JSON and file persistence helpers for reusable camera presets and playback tracks. -//! Stream-based helpers exist first so examples can choose between file IO and in-memory round-trips. -inline nlohmann::json serializePresetCollection(std::span presets) -{ - nlohmann::json root; - root["presets"] = nlohmann::json::array(); - for (const auto& preset : presets) - root["presets"].push_back(serializePreset(preset)); - return root; -} - -//! Parse a preset collection from JSON and replace the destination vector on success. -inline bool deserializePresetCollection(const nlohmann::json& root, std::vector& presets) -{ - if (!root.contains("presets") || !root["presets"].is_array()) - return false; - - std::vector loadedPresets; - loadedPresets.reserve(root["presets"].size()); - for (const auto& entry : root["presets"]) - { - CCameraPreset preset; - deserializePreset(entry, preset); - loadedPresets.emplace_back(std::move(preset)); - } - - presets = std::move(loadedPresets); - return true; -} - -//! Serialize a preset collection to an arbitrary stream. -inline bool writePresetCollection(std::ostream& out, std::span presets, const int indent = 2) -{ - if (!out) - return false; - - out << serializePresetCollection(presets).dump(indent); - return static_cast(out); -} - -//! Deserialize a preset collection from an arbitrary stream. -inline bool readPresetCollection(std::istream& in, std::vector& presets) -{ - if (!in) - return false; - - nlohmann::json root; - in >> root; - return deserializePresetCollection(root, presets); -} - -//! Convenience file wrapper around `writePresetCollection`. -inline bool savePresetCollectionToFile(const system::path& path, std::span presets, const int indent = 2) -{ - std::ofstream out(path.string(), std::ios::binary); - return writePresetCollection(out, presets, indent); -} - -//! Convenience file wrapper around `readPresetCollection`. -inline bool loadPresetCollectionFromFile(const system::path& path, std::vector& presets) -{ - std::ifstream in(path.string(), std::ios::binary); - return readPresetCollection(in, presets); -} - -//! Serialize a keyframe track to an arbitrary stream. -inline bool writeKeyframeTrack(std::ostream& out, const CCameraKeyframeTrack& track, const int indent = 2) -{ - if (!out) - return false; - - out << serializeKeyframeTrack(track).dump(indent); - return static_cast(out); -} +bool writePresetCollection(std::ostream& out, std::span presets, int indent = 2); +bool readPresetCollection(std::istream& in, std::vector& presets); -//! Deserialize a keyframe track from an arbitrary stream. -inline bool readKeyframeTrack(std::istream& in, CCameraKeyframeTrack& track) -{ - if (!in) - return false; - - nlohmann::json root; - in >> root; - return deserializeKeyframeTrack(root, track); -} - -//! Convenience file wrapper around `writeKeyframeTrack`. -inline bool saveKeyframeTrackToFile(const system::path& path, const CCameraKeyframeTrack& track, const int indent = 2) -{ - std::ofstream out(path.string(), std::ios::binary); - return writeKeyframeTrack(out, track, indent); -} - -//! Convenience file wrapper around `readKeyframeTrack`. -inline bool loadKeyframeTrackFromFile(const system::path& path, CCameraKeyframeTrack& track) -{ - std::ifstream in(path.string(), std::ios::binary); - return readKeyframeTrack(in, track); -} +bool savePresetCollectionToFile(const path& path, std::span presets, int indent = 2); +bool loadPresetCollectionFromFile(const path& path, std::vector& presets); -} // namespace nbl::hlsl +} // namespace nbl::system #endif // _C_CAMERA_PERSISTENCE_HPP_ diff --git a/common/include/camera/CCameraPlaybackTimeline.hpp b/common/include/camera/CCameraPlaybackTimeline.hpp index dc23726f3..1dc39019c 100644 --- a/common/include/camera/CCameraPlaybackTimeline.hpp +++ b/common/include/camera/CCameraPlaybackTimeline.hpp @@ -7,11 +7,11 @@ #include "CCameraKeyframeTrack.hpp" -namespace nbl::hlsl +namespace nbl::core { //! Shared playback cursor state for camera keyframe tracks. -//! The cursor is intentionally transport-only so examples can own higher-level playback policy. +//! The cursor is intentionally transport-only so consumers can own higher-level playback policy. struct CCameraPlaybackCursor { bool playing = false; @@ -21,7 +21,7 @@ struct CCameraPlaybackCursor }; //! Outcome of advancing a playback cursor against a keyframe track. -//! This separates raw time stepping from higher-level example policy and UI feedback. +//! This separates raw time stepping from higher-level consumer policy and UI feedback. struct SCameraPlaybackAdvanceResult { bool hasTrack = false; @@ -94,6 +94,6 @@ inline SCameraPlaybackAdvanceResult advancePlaybackCursor(CCameraPlaybackCursor& return result; } -} // namespace nbl::hlsl +} // namespace nbl::core #endif // _C_CAMERA_PLAYBACK_TIMELINE_HPP_ diff --git a/common/include/camera/CCameraPresentationUtilities.hpp b/common/include/camera/CCameraPresentationUtilities.hpp index c7604a41a..f6f818a10 100644 --- a/common/include/camera/CCameraPresentationUtilities.hpp +++ b/common/include/camera/CCameraPresentationUtilities.hpp @@ -9,7 +9,7 @@ #include "CCameraTextUtilities.hpp" -namespace nbl::hlsl +namespace nbl::core { //! Shared exactness-oriented filter used by preset presentation surfaces. @@ -119,6 +119,6 @@ inline SCameraCapturePresentation analyzeCapturePresentation(const CCameraGoalSo return presentation; } -} // namespace nbl::hlsl +} // namespace nbl::core #endif // _C_CAMERA_PRESENTATION_UTILITIES_HPP_ diff --git a/common/include/camera/CCameraPreset.hpp b/common/include/camera/CCameraPreset.hpp index a8cd41acc..e9a4e218c 100644 --- a/common/include/camera/CCameraPreset.hpp +++ b/common/include/camera/CCameraPreset.hpp @@ -9,9 +9,8 @@ #include #include "CCameraGoal.hpp" -#include "nlohmann/json.hpp" -namespace nbl::hlsl +namespace nbl::core { //! Named persisted camera state built on top of `CCameraGoal`. @@ -64,128 +63,6 @@ inline bool comparePresetCollections(std::span lhs, std::sp return true; } -inline nlohmann::json serializeGoal(const CCameraGoal& goal) -{ - nlohmann::json j; - j["position"] = { goal.position.x, goal.position.y, goal.position.z }; - j["orientation"] = { goal.orientation.x, goal.orientation.y, goal.orientation.z, goal.orientation.w }; - j["camera_kind"] = static_cast(goal.sourceKind); - j["camera_capabilities"] = goal.sourceCapabilities; - j["camera_goal_state_mask"] = goal.sourceGoalStateMask; - if (goal.hasTargetPosition) - j["target_position"] = { goal.targetPosition.x, goal.targetPosition.y, goal.targetPosition.z }; - if (goal.hasDistance) - j["distance"] = goal.distance; - if (goal.hasOrbitState) - { - j["orbit_u"] = goal.orbitU; - j["orbit_v"] = goal.orbitV; - j["orbit_distance"] = goal.orbitDistance; - } - if (goal.hasPathState) - { - j["path_angle"] = goal.pathState.angle; - j["path_radius"] = goal.pathState.radius; - j["path_height"] = goal.pathState.height; - } - if (goal.hasDynamicPerspectiveState) - { - j["dynamic_base_fov"] = goal.dynamicPerspectiveState.baseFov; - j["dynamic_reference_distance"] = goal.dynamicPerspectiveState.referenceDistance; - } - return j; -} - -inline void deserializeGoal(const nlohmann::json& entry, CCameraGoal& goal) -{ - goal = {}; - if (entry.contains("camera_kind")) - goal.sourceKind = static_cast(entry["camera_kind"].get()); - if (entry.contains("camera_capabilities")) - goal.sourceCapabilities = entry["camera_capabilities"].get(); - if (entry.contains("camera_goal_state_mask")) - goal.sourceGoalStateMask = entry["camera_goal_state_mask"].get(); - if (entry.contains("position") && entry["position"].is_array()) - { - const auto& arr = entry["position"]; - goal.position = float64_t3(arr[0].get(), arr[1].get(), arr[2].get()); - } - if (entry.contains("orientation") && entry["orientation"].is_array()) - { - const auto& arr = entry["orientation"]; - goal.orientation = glm::quat( - arr[3].get(), - arr[0].get(), - arr[1].get(), - arr[2].get() - ); - } - if (entry.contains("target_position") && entry["target_position"].is_array()) - { - const auto& arr = entry["target_position"]; - goal.targetPosition = float64_t3(arr[0].get(), arr[1].get(), arr[2].get()); - goal.hasTargetPosition = true; - } - if (entry.contains("distance")) - { - goal.distance = entry["distance"].get(); - goal.hasDistance = true; - } - if (entry.contains("orbit_u")) - { - goal.orbitU = entry["orbit_u"].get(); - goal.hasOrbitState = true; - } - if (entry.contains("orbit_v")) - { - goal.orbitV = entry["orbit_v"].get(); - goal.hasOrbitState = true; - } - if (entry.contains("orbit_distance")) - { - goal.orbitDistance = entry["orbit_distance"].get(); - goal.hasOrbitState = true; - } - if (entry.contains("path_angle") && entry.contains("path_radius") && entry.contains("path_height")) - { - goal.pathState.angle = entry["path_angle"].get(); - goal.pathState.radius = entry["path_radius"].get(); - goal.pathState.height = entry["path_height"].get(); - goal.hasPathState = true; - } - if (entry.contains("dynamic_base_fov")) - { - goal.dynamicPerspectiveState.baseFov = entry["dynamic_base_fov"].get(); - goal.hasDynamicPerspectiveState = true; - } - if (entry.contains("dynamic_reference_distance")) - { - goal.dynamicPerspectiveState.referenceDistance = entry["dynamic_reference_distance"].get(); - goal.hasDynamicPerspectiveState = true; - } -} - -inline nlohmann::json serializePreset(const CCameraPreset& preset) -{ - auto j = serializeGoal(makeGoalFromPreset(preset)); - j["name"] = preset.name; - j["identifier"] = preset.identifier; - return j; -} - -inline void deserializePreset(const nlohmann::json& entry, CCameraPreset& preset) -{ - preset = {}; - if (entry.contains("name")) - preset.name = entry["name"].get(); - if (entry.contains("identifier")) - preset.identifier = entry["identifier"].get(); - - CCameraGoal goal; - deserializeGoal(entry, goal); - assignGoalToPreset(preset, goal); -} - -} // namespace nbl::hlsl +} // namespace nbl::core #endif // _C_CAMERA_PRESET_HPP_ diff --git a/common/include/camera/CCameraPresetFlow.hpp b/common/include/camera/CCameraPresetFlow.hpp index e73cfcb4d..95aa029df 100644 --- a/common/include/camera/CCameraPresetFlow.hpp +++ b/common/include/camera/CCameraPresetFlow.hpp @@ -12,7 +12,7 @@ #include "CCameraGoalAnalysis.hpp" -namespace nbl::hlsl +namespace nbl::core { //! Reusable aggregate summary for applying one preset to multiple cameras. @@ -137,6 +137,6 @@ inline SCameraPresetApplySummary applyPresetToCameraRange(const CCameraGoalSolve return summary; } -} // namespace nbl::hlsl +} // namespace nbl::core #endif // _C_CAMERA_PRESET_FLOW_HPP_ diff --git a/common/include/camera/CCameraPresetPersistence.hpp b/common/include/camera/CCameraPresetPersistence.hpp new file mode 100644 index 000000000..6c587f7af --- /dev/null +++ b/common/include/camera/CCameraPresetPersistence.hpp @@ -0,0 +1,30 @@ +// 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_PRESET_PERSISTENCE_HPP_ +#define _C_CAMERA_PRESET_PERSISTENCE_HPP_ + +#include + +#include "CCameraPreset.hpp" +#include "nbl/system/path.h" + +namespace nbl::system +{ + +bool writeGoal(std::ostream& out, const core::CCameraGoal& goal, int indent = 2); +bool readGoal(std::istream& in, core::CCameraGoal& goal); + +bool saveGoalToFile(const path& path, const core::CCameraGoal& goal, int indent = 2); +bool loadGoalFromFile(const path& path, core::CCameraGoal& goal); + +bool writePreset(std::ostream& out, const core::CCameraPreset& preset, int indent = 2); +bool readPreset(std::istream& in, core::CCameraPreset& preset); + +bool savePresetToFile(const path& path, const core::CCameraPreset& preset, int indent = 2); +bool loadPresetFromFile(const path& path, core::CCameraPreset& preset); + +} // namespace nbl::system + +#endif // _C_CAMERA_PRESET_PERSISTENCE_HPP_ diff --git a/common/include/camera/CCameraProjectionUtilities.hpp b/common/include/camera/CCameraProjectionUtilities.hpp index 5e9ceb2a6..d35815ac4 100644 --- a/common/include/camera/CCameraProjectionUtilities.hpp +++ b/common/include/camera/CCameraProjectionUtilities.hpp @@ -7,7 +7,7 @@ #include "IPlanarProjection.hpp" -namespace nbl::hlsl +namespace nbl::core { //! Apply a camera-provided dynamic perspective FOV to one planar projection entry. @@ -28,6 +28,6 @@ inline bool syncDynamicPerspectiveProjection(ICamera* camera, IPlanarProjection: return true; } -} // namespace nbl::hlsl +} // namespace nbl::core #endif // _C_CAMERA_PROJECTION_UTILITIES_HPP_ diff --git a/common/include/camera/CCameraScriptedCheckRunner.hpp b/common/include/camera/CCameraScriptedCheckRunner.hpp index 51f438fe0..c469b32d0 100644 --- a/common/include/camera/CCameraScriptedCheckRunner.hpp +++ b/common/include/camera/CCameraScriptedCheckRunner.hpp @@ -15,7 +15,7 @@ #include "CCameraFollowRegressionUtilities.hpp" #include "CCameraScriptedRuntime.hpp" -namespace nbl::hlsl +namespace nbl::system { /** @@ -32,24 +32,24 @@ struct CCameraScriptedCheckRuntimeState { size_t nextCheckIndex = 0u; bool baselineValid = false; - float32_t3 baselinePos = float32_t3(0.f); - float32_t3 baselineEulerDeg = float32_t3(0.f); + hlsl::float32_t3 baselinePos = hlsl::float32_t3(0.f); + hlsl::float32_t3 baselineEulerDeg = hlsl::float32_t3(0.f); bool stepValid = false; - float32_t3 stepPos = float32_t3(0.f); - float32_t3 stepEulerDeg = float32_t3(0.f); + hlsl::float32_t3 stepPos = hlsl::float32_t3(0.f); + hlsl::float32_t3 stepEulerDeg = hlsl::float32_t3(0.f); }; //! Shared per-frame evaluation context for authored scripted checks. struct CCameraScriptedCheckContext { uint64_t frame = 0ull; - ICamera* camera = nullptr; - const CVirtualGimbalEvent* imguizmoVirtual = nullptr; + core::ICamera* camera = nullptr; + const core::CVirtualGimbalEvent* imguizmoVirtual = nullptr; uint32_t imguizmoVirtualCount = 0u; - const CTrackedTarget* trackedTarget = nullptr; - const SCameraFollowConfig* followConfig = nullptr; - const float32_t4x4* followViewProjMatrix = nullptr; - const CCameraGoalSolver* goalSolver = nullptr; + const core::CTrackedTarget* trackedTarget = nullptr; + const core::SCameraFollowConfig* followConfig = nullptr; + const hlsl::float32_t4x4* followViewProjMatrix = nullptr; + const core::CCameraGoalSolver* goalSolver = nullptr; }; //! Reusable log entry produced by scripted check evaluation. @@ -74,15 +74,15 @@ inline float scriptedCheckAngleDiffDeg(const float a, const float b) return std::abs(d - 180.0f); } -inline bool scriptedCheckFinite3(const float32_t3& v) +inline bool scriptedCheckFinite3(const hlsl::float32_t3& v) { return std::isfinite(v.x) && std::isfinite(v.y) && std::isfinite(v.z); } inline void scriptedCheckSetStepReference( CCameraScriptedCheckRuntimeState& state, - const float32_t3& pos, - const float32_t3& eulerDeg) + const hlsl::float32_t3& pos, + const hlsl::float32_t3& eulerDeg) { state.stepValid = true; state.stepPos = pos; @@ -136,7 +136,7 @@ inline CCameraScriptedCheckFrameResult evaluateScriptedChecksForFrame( const auto& gimbal = context.camera->getGimbal(); const auto pos = gimbal.getPosition(); - const auto eulerDeg = glm::degrees(glm::eulerAngles(gimbal.getOrientation())); + const auto eulerDeg = hlsl::getCastedVector(hlsl::getQuaternionEulerDegrees(gimbal.getOrientation())); if (!scriptedCheckFinite3(pos) || !scriptedCheckFinite3(eulerDeg)) { @@ -204,7 +204,7 @@ inline CCameraScriptedCheckFrameResult evaluateScriptedChecksForFrame( { oss << std::fixed << std::setprecision(6); oss << "[script][fail] imguizmo_virtual frame=" << context.frame - << " type=" << CVirtualGimbalEvent::virtualEventToString(expected.type).data() + << " type=" << core::CVirtualGimbalEvent::virtualEventToString(expected.type).data() << " expected=" << expected.magnitude << " actual=" << actual << " tol=" << check.tolerance; @@ -231,7 +231,7 @@ inline CCameraScriptedCheckFrameResult evaluateScriptedChecksForFrame( bool ok = true; if (check.hasExpectedPos) { - const auto diff = float32_t3(pos.x - check.expectedPos.x, pos.y - check.expectedPos.y, pos.z - check.expectedPos.z); + const auto diff = hlsl::float32_t3(pos.x - check.expectedPos.x, pos.y - check.expectedPos.y, pos.z - check.expectedPos.z); const auto distance = std::sqrt(diff.x * diff.x + diff.y * diff.y + diff.z * diff.z); if (distance > check.posTolerance) { @@ -296,7 +296,7 @@ inline CCameraScriptedCheckFrameResult evaluateScriptedChecksForFrame( break; } - const auto diff = float32_t3(pos.x - state.baselinePos.x, pos.y - state.baselinePos.y, pos.z - state.baselinePos.z); + const auto diff = hlsl::float32_t3(pos.x - state.baselinePos.x, pos.y - state.baselinePos.y, pos.z - state.baselinePos.z); const auto dpos = std::sqrt(diff.x * diff.x + diff.y * diff.y + diff.z * diff.z); const auto dx = scriptedCheckAngleDiffDeg(eulerDeg.x, state.baselineEulerDeg.x); const auto dy = scriptedCheckAngleDiffDeg(eulerDeg.y, state.baselineEulerDeg.y); @@ -356,7 +356,7 @@ inline CCameraScriptedCheckFrameResult evaluateScriptedChecksForFrame( } } - const auto diff = float32_t3(pos.x - state.stepPos.x, pos.y - state.stepPos.y, pos.z - state.stepPos.z); + const auto diff = hlsl::float32_t3(pos.x - state.stepPos.x, pos.y - state.stepPos.y, pos.z - state.stepPos.z); const auto dpos = std::sqrt(diff.x * diff.x + diff.y * diff.y + diff.z * diff.z); const auto dx = scriptedCheckAngleDiffDeg(eulerDeg.x, state.stepEulerDeg.x); const auto dy = scriptedCheckAngleDiffDeg(eulerDeg.y, state.stepEulerDeg.y); @@ -477,16 +477,16 @@ inline CCameraScriptedCheckFrameResult evaluateScriptedChecksForFrame( break; } - SCameraFollowRegressionResult regression = {}; + core::SCameraFollowRegressionResult regression = {}; std::string regressionError; - CCameraGoal expectedFollowGoal = {}; - const bool ok = tryBuildFollowGoal( + core::CCameraGoal expectedFollowGoal = {}; + const bool ok = core::tryBuildFollowGoal( *context.goalSolver, context.camera, *context.trackedTarget, *context.followConfig, expectedFollowGoal) && - validateFollowTargetContract( + core::validateFollowTargetContract( context.camera, *context.trackedTarget, *context.followConfig, @@ -534,6 +534,6 @@ inline CCameraScriptedCheckFrameResult evaluateScriptedChecksForFrame( return result; } -} // namespace nbl::hlsl +} // namespace nbl::system #endif // _C_CAMERA_SCRIPTED_CHECK_RUNNER_HPP_ diff --git a/common/include/camera/CCameraScriptedRuntime.hpp b/common/include/camera/CCameraScriptedRuntime.hpp index 2f5f61b1d..e3d8b85d4 100644 --- a/common/include/camera/CCameraScriptedRuntime.hpp +++ b/common/include/camera/CCameraScriptedRuntime.hpp @@ -12,9 +12,9 @@ #include "CCameraGoal.hpp" #include "IGimbal.hpp" -#include "nbl/ui/SInputEvent.h" +#include "nbl/ui/KeyCodes.h" -namespace nbl::hlsl +namespace nbl::system { /** @@ -22,7 +22,7 @@ namespace nbl::hlsl * * The compact authored sequence remains camera-domain. A concrete runtime may still expand it * into low-level per-frame events and checks, but those expanded payloads live in this shared -* header rather than inside one example. +* header rather than inside one consumer. */ struct CCameraScriptedInputEvent { @@ -39,15 +39,37 @@ struct CCameraScriptedInputEvent struct KeyboardData { + enum class Action : uint8_t + { + Uninitialized = 0, + Pressed = 1, + Released = 2 + }; + ui::E_KEY_CODE key = ui::EKC_NONE; - ui::SKeyboardEvent::E_KEY_ACTION action = ui::SKeyboardEvent::ECA_UNITIALIZED; + Action action = Action::Uninitialized; }; struct MouseData { - ui::SMouseEvent::E_EVENT_TYPE type = ui::SMouseEvent::EET_UNITIALIZED; + enum class Type : uint8_t + { + Uninitialized = 0, + Click = 1, + Scroll = 2, + Movement = 4 + }; + + enum class ClickAction : uint8_t + { + Uninitialized = 0, + Pressed = 1, + Released = 2 + }; + + Type type = Type::Uninitialized; ui::E_MOUSE_BUTTON button = ui::EMB_LEFT_BUTTON; - ui::SMouseEvent::SClickEvent::E_ACTION action = ui::SMouseEvent::SClickEvent::EA_UNITIALIZED; + ClickAction action = ClickAction::Uninitialized; int16_t x = 0; int16_t y = 0; int16_t dx = 0; @@ -75,13 +97,13 @@ struct CCameraScriptedInputEvent struct GoalData { - CCameraGoal goal = {}; + core::CCameraGoal goal = {}; bool requireExact = true; }; struct TrackedTargetTransformData { - float64_t4x4 transform = float64_t4x4(1.0); + hlsl::float64_t4x4 transform = hlsl::float64_t4x4(1.0); }; struct SegmentLabelData @@ -93,7 +115,7 @@ struct CCameraScriptedInputEvent Type type = Type::Keyboard; KeyboardData keyboard; MouseData mouse; - float32_t4x4 imguizmo = float32_t4x4(1.f); + hlsl::float32_t4x4 imguizmo = hlsl::float32_t4x4(1.f); ActionData action; GoalData goal; TrackedTargetTransformData trackedTargetTransform; @@ -114,8 +136,8 @@ struct CCameraScriptedInputCheck struct ExpectedVirtualEvent { - CVirtualGimbalEvent::VirtualEventType type = CVirtualGimbalEvent::None; - float64_t magnitude = 0.0; + core::CVirtualGimbalEvent::VirtualEventType type = core::CVirtualGimbalEvent::None; + hlsl::float64_t magnitude = 0.0; }; uint64_t frame = 0; @@ -123,8 +145,8 @@ struct CCameraScriptedInputCheck float tolerance = 1e-3f; std::vector expectedVirtualEvents; - float32_t3 expectedPos = float32_t3(0.f); - float32_t3 expectedEulerDeg = float32_t3(0.f); + hlsl::float32_t3 expectedPos = hlsl::float32_t3(0.f); + hlsl::float32_t3 expectedEulerDeg = hlsl::float32_t3(0.f); bool hasExpectedPos = false; bool hasExpectedEuler = false; float posTolerance = 0.05f; @@ -196,7 +218,7 @@ inline void appendScriptedActionEvent( inline void appendScriptedGoalEvent( CCameraScriptedTimeline& timeline, const uint64_t frame, - const CCameraGoal& goal, + const core::CCameraGoal& goal, const bool requireExact = true) { CCameraScriptedInputEvent entry; @@ -210,7 +232,7 @@ inline void appendScriptedGoalEvent( inline void appendScriptedTrackedTargetTransformEvent( CCameraScriptedTimeline& timeline, const uint64_t frame, - const float64_t4x4& transform) + const hlsl::float64_t4x4& transform) { CCameraScriptedInputEvent entry; entry.frame = frame; @@ -291,7 +313,7 @@ struct CCameraScriptedFrameEvents { std::vector keyboard; std::vector mouse; - std::vector imguizmo; + std::vector imguizmo; std::vector actions; std::vector goals; std::vector trackedTargetTransforms; @@ -355,6 +377,6 @@ inline void dequeueScriptedFrameEvents( } } -} // namespace nbl::hlsl +} // namespace nbl::system #endif diff --git a/common/include/camera/CCameraScriptedRuntimePersistence.hpp b/common/include/camera/CCameraScriptedRuntimePersistence.hpp index b88563487..7280762b0 100644 --- a/common/include/camera/CCameraScriptedRuntimePersistence.hpp +++ b/common/include/camera/CCameraScriptedRuntimePersistence.hpp @@ -5,53 +5,17 @@ #ifndef _C_CAMERA_SCRIPTED_RUNTIME_PERSISTENCE_HPP_ #define _C_CAMERA_SCRIPTED_RUNTIME_PERSISTENCE_HPP_ -#include +#include #include #include -#include #include #include "CCameraScriptedRuntime.hpp" -#include "CCameraSequenceScript.hpp" -#include "nbl/ui/SInputEvent.h" -#include "glm/glm/gtc/matrix_transform.hpp" -#include "glm/glm/gtc/quaternion.hpp" -#include "nlohmann/json.hpp" +#include "CCameraSequenceScriptPersistence.hpp" -namespace nbl::hlsl +namespace nbl::system { -/** -* Shared JSON parser for low-level scripted runtime payloads. -* -* The parser handles low-level `events/checks/capture_frames` payloads without leaving parser glue -* inside one consumer. -* -* It can also detect compact `segments` and forward them into `CCameraSequenceScript`. -*/ -inline float32_t4x4 composeScriptedImguizmoTransform( - const std::array& translation, - const std::array& rotationDeg, - const std::array& scale) -{ - const auto translationMatrix = glm::translate(glm::mat4(1.f), glm::vec3(translation[0], translation[1], translation[2])); - const auto rotationMatrix = glm::mat4_cast(glm::quat(glm::radians(glm::vec3(rotationDeg[0], rotationDeg[1], rotationDeg[2])))); - const auto scaleMatrix = glm::scale(glm::mat4(1.f), glm::vec3(scale[0], scale[1], scale[2])); - return float32_t4x4(translationMatrix * rotationMatrix * scaleMatrix); -} - -inline float32_t4x4 makeScriptedMatrixFromArray(const std::array& values) -{ - 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; -} - -//! Optional runtime control-scale overrides parsed from low-level scripted JSON. struct CCameraScriptedControlOverrides { bool hasKeyboardScale = false; @@ -66,13 +30,6 @@ struct CCameraScriptedControlOverrides float rotationScale = 1.f; }; -/** -* Parsed top-level scripted-runtime input including: -* -* - low-level expanded runtime payloads (`events`, `checks`, `capture_frames`) -* - compact camera-sequence data (`segments`) when present -* - optional runtime/debug policy flags used by scripted consumers -*/ struct CCameraScriptedInputParseResult { bool enabled = true; @@ -88,7 +45,7 @@ struct CCameraScriptedInputParseResult std::string capturePrefix = "script"; CCameraScriptedControlOverrides cameraControls = {}; CCameraScriptedTimeline timeline = {}; - std::optional sequence; + std::optional sequence; std::vector warnings; }; @@ -97,601 +54,9 @@ inline void appendScriptedInputParseWarning(CCameraScriptedInputParseResult& out out.warnings.emplace_back(std::move(warning)); } -inline ui::E_KEY_CODE parseScriptedKeyCode(std::string_view key) -{ - if (const auto parsed = ui::stringToKeyCode(key); parsed != ui::EKC_NONE) - return parsed; - - constexpr std::string_view keyKeyPrefix = "KEY_KEY_"; - if (key.starts_with(keyKeyPrefix)) - return ui::stringToKeyCode(key.substr(keyKeyPrefix.size())); - - constexpr std::string_view ekcPrefix = "EKC_"; - if (key.starts_with(ekcPrefix)) - return ui::stringToKeyCode(key.substr(ekcPrefix.size())); - - return ui::EKC_NONE; -} - -inline void appendScriptedInputCaptureFrame(CCameraScriptedInputParseResult& out, const uint64_t frame, const bool captureFrame) -{ - if (captureFrame) - out.timeline.captureFrames.emplace_back(frame); -} - -inline std::optional parseScriptedKeyboardAction(std::string_view action) -{ - if (action == "pressed" || action == "press") - return ui::SKeyboardEvent::ECA_PRESSED; - if (action == "released" || action == "release") - return ui::SKeyboardEvent::ECA_RELEASED; - return std::nullopt; -} - -inline std::optional parseScriptedMouseButton(std::string_view button) -{ - if (button == "LEFT_BUTTON") - return ui::EMB_LEFT_BUTTON; - if (button == "RIGHT_BUTTON") - return ui::EMB_RIGHT_BUTTON; - if (button == "MIDDLE_BUTTON") - return ui::EMB_MIDDLE_BUTTON; - if (button == "BUTTON_4") - return ui::EMB_BUTTON_4; - if (button == "BUTTON_5") - return ui::EMB_BUTTON_5; - return std::nullopt; -} - -inline std::optional parseScriptedMouseClickAction(std::string_view action) -{ - if (action == "pressed" || action == "press") - return ui::SMouseEvent::SClickEvent::EA_PRESSED; - if (action == "released" || action == "release") - return ui::SMouseEvent::SClickEvent::EA_RELEASED; - return std::nullopt; -} - -inline void parseScriptedCaptureFrames( - const nlohmann::json& script, - CCameraScriptedInputParseResult& out) -{ - if (!script.contains("capture_frames")) - return; - - for (const auto& frame : script["capture_frames"]) - out.timeline.captureFrames.emplace_back(frame.get()); -} - -inline void parseScriptedControlOverrides( - const nlohmann::json& controls, - CCameraScriptedControlOverrides& out) -{ - if (controls.contains("keyboard_scale")) - { - out.hasKeyboardScale = true; - out.keyboardScale = controls["keyboard_scale"].get(); - } - if (controls.contains("mouse_move_scale")) - { - out.hasMouseMoveScale = true; - out.mouseMoveScale = controls["mouse_move_scale"].get(); - } - if (controls.contains("mouse_scroll_scale")) - { - out.hasMouseScrollScale = true; - out.mouseScrollScale = controls["mouse_scroll_scale"].get(); - } - if (controls.contains("translation_scale")) - { - out.hasTranslationScale = true; - out.translationScale = controls["translation_scale"].get(); - } - if (controls.contains("rotation_scale")) - { - out.hasRotationScale = true; - out.rotationScale = controls["rotation_scale"].get(); - } -} - -inline bool parseScriptedSequenceIfPresent( - const nlohmann::json& script, - CCameraScriptedInputParseResult& out, - std::string* error) -{ - if (!script.contains("segments")) - return true; - - CCameraSequenceScript sequence; - std::string sequenceError; - if (!deserializeCameraSequenceScript(script, sequence, &sequenceError)) - { - if (error) - *error = std::move(sequenceError); - return false; - } - - out.sequence = std::move(sequence); - return true; -} - -inline void parseScriptedKeyboardEvent( - const nlohmann::json& event, - const uint64_t frame, - const bool captureFrame, - CCameraScriptedInputParseResult& out) -{ - if (!event.contains("key") || !event.contains("action")) - { - appendScriptedInputParseWarning(out, "Scripted keyboard event missing \"key\" or \"action\"."); - return; - } - - const auto keyStr = event["key"].get(); - const auto actionStr = event["action"].get(); - const auto key = parseScriptedKeyCode(keyStr); - if (key == ui::EKC_NONE) - { - appendScriptedInputParseWarning(out, "Scripted keyboard event has invalid key \"" + keyStr + "\"."); - return; - } - - const auto action = parseScriptedKeyboardAction(actionStr); - if (!action.has_value()) - { - appendScriptedInputParseWarning(out, "Scripted keyboard event has invalid action \"" + actionStr + "\"."); - return; - } - - CCameraScriptedInputEvent entry; - entry.frame = frame; - entry.type = CCameraScriptedInputEvent::Type::Keyboard; - entry.keyboard.key = key; - entry.keyboard.action = action.value(); - out.timeline.events.emplace_back(std::move(entry)); - appendScriptedInputCaptureFrame(out, frame, captureFrame); -} - -inline void parseScriptedMouseEvent( - const nlohmann::json& event, - const uint64_t frame, - const bool captureFrame, - CCameraScriptedInputParseResult& out) -{ - if (!event.contains("kind")) - { - appendScriptedInputParseWarning(out, "Scripted mouse event missing \"kind\"."); - return; - } - - const auto kind = event["kind"].get(); - CCameraScriptedInputEvent entry; - entry.frame = frame; - entry.type = CCameraScriptedInputEvent::Type::Mouse; - - if (kind == "move") - { - entry.mouse.type = ui::SMouseEvent::EET_MOVEMENT; - entry.mouse.dx = event.value("dx", 0); - entry.mouse.dy = event.value("dy", 0); - } - else if (kind == "scroll") - { - entry.mouse.type = ui::SMouseEvent::EET_SCROLL; - entry.mouse.v = event.value("v", 0); - entry.mouse.h = event.value("h", 0); - } - else if (kind == "click") - { - if (!event.contains("button") || !event.contains("action")) - { - appendScriptedInputParseWarning(out, "Scripted click event missing \"button\" or \"action\"."); - return; - } - - const auto buttonStr = event["button"].get(); - const auto actionStr = event["action"].get(); - const auto button = parseScriptedMouseButton(buttonStr); - if (!button.has_value()) - { - appendScriptedInputParseWarning(out, "Scripted click event has invalid button \"" + buttonStr + "\"."); - return; - } - - const auto action = parseScriptedMouseClickAction(actionStr); - if (!action.has_value()) - { - appendScriptedInputParseWarning(out, "Scripted click event has invalid action \"" + actionStr + "\"."); - return; - } - - entry.mouse.type = ui::SMouseEvent::EET_CLICK; - entry.mouse.button = button.value(); - entry.mouse.action = action.value(); - entry.mouse.x = event.value("x", 0); - entry.mouse.y = event.value("y", 0); - } - else - { - appendScriptedInputParseWarning(out, "Scripted mouse event has invalid kind \"" + kind + "\"."); - return; - } - - out.timeline.events.emplace_back(std::move(entry)); - appendScriptedInputCaptureFrame(out, frame, captureFrame); -} - -inline void parseScriptedImguizmoEvent( - const nlohmann::json& event, - const uint64_t frame, - const bool captureFrame, - CCameraScriptedInputParseResult& out) -{ - CCameraScriptedInputEvent entry; - entry.frame = frame; - entry.type = 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 rotationDeg = 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, rotationDeg, scale); - } - - out.timeline.events.emplace_back(std::move(entry)); - appendScriptedInputCaptureFrame(out, frame, captureFrame); -} - -inline int32_t parseScriptedActionIntValue(const nlohmann::json& event) -{ - if (event.contains("value")) - return event["value"].get(); - if (event.contains("index")) - return event["index"].get(); - return 0; -} - -inline bool parseScriptedProjectionActionValue( - const nlohmann::json& event, - CCameraScriptedInputEvent::ActionData& action, - CCameraScriptedInputParseResult& out) -{ - if (event.contains("value") && event["value"].is_string()) - { - const auto valueStr = event["value"].get(); - if (valueStr == "perspective") - action.value = static_cast(IPlanarProjection::CProjection::Perspective); - else if (valueStr == "orthographic") - action.value = static_cast(IPlanarProjection::CProjection::Orthographic); - else - { - appendScriptedInputParseWarning(out, "Scripted action projection type has invalid value \"" + valueStr + "\"."); - return false; - } - } - else - { - action.value = parseScriptedActionIntValue(event); - } - - return true; -} - -inline void parseScriptedActionEvent( - const nlohmann::json& event, - const uint64_t frame, - const bool captureFrame, - CCameraScriptedInputParseResult& out) -{ - if (!event.contains("action")) - { - appendScriptedInputParseWarning(out, "Scripted action event missing \"action\"."); - return; - } - - const auto actionStr = event["action"].get(); - CCameraScriptedInputEvent entry; - entry.frame = frame; - entry.type = CCameraScriptedInputEvent::Type::Action; - - if (actionStr == "set_active_render_window") - { - entry.action.kind = CCameraScriptedInputEvent::ActionData::Kind::SetActiveRenderWindow; - entry.action.value = parseScriptedActionIntValue(event); - } - else if (actionStr == "set_active_planar") - { - entry.action.kind = CCameraScriptedInputEvent::ActionData::Kind::SetActivePlanar; - entry.action.value = parseScriptedActionIntValue(event); - } - else if (actionStr == "set_projection_type") - { - entry.action.kind = CCameraScriptedInputEvent::ActionData::Kind::SetProjectionType; - if (!parseScriptedProjectionActionValue(event, entry.action, out)) - return; - } - else if (actionStr == "set_projection_index") - { - entry.action.kind = CCameraScriptedInputEvent::ActionData::Kind::SetProjectionIndex; - entry.action.value = parseScriptedActionIntValue(event); - } - else if (actionStr == "set_use_window") - { - entry.action.kind = CCameraScriptedInputEvent::ActionData::Kind::SetUseWindow; - entry.action.value = event.value("value", false) ? 1 : 0; - } - else if (actionStr == "set_left_handed") - { - entry.action.kind = CCameraScriptedInputEvent::ActionData::Kind::SetLeftHanded; - entry.action.value = event.value("value", false) ? 1 : 0; - } - else if (actionStr == "reset_active_camera") - { - entry.action.kind = CCameraScriptedInputEvent::ActionData::Kind::ResetActiveCamera; - entry.action.value = 1; - } - else - { - appendScriptedInputParseWarning(out, "Scripted action event has invalid action \"" + actionStr + "\"."); - return; - } - - out.timeline.events.emplace_back(std::move(entry)); - appendScriptedInputCaptureFrame(out, frame, captureFrame); -} - -inline void parseScriptedInputEvent( - const nlohmann::json& event, - CCameraScriptedInputParseResult& out) -{ - if (!event.contains("frame") || !event.contains("type")) - { - 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") - parseScriptedKeyboardEvent(event, frame, captureFrame, out); - else if (type == "mouse") - parseScriptedMouseEvent(event, frame, captureFrame, out); - else if (type == "imguizmo") - parseScriptedImguizmoEvent(event, frame, captureFrame, out); - else if (type == "action") - parseScriptedActionEvent(event, frame, captureFrame, out); - else - appendScriptedInputParseWarning(out, "Scripted input event has invalid type \"" + type + "\"."); -} - -inline void parseScriptedInputEvents( - const nlohmann::json& script, - CCameraScriptedInputParseResult& out) -{ - if (!script.contains("events")) - return; - - for (const auto& event : script["events"]) - parseScriptedInputEvent(event, out); -} - -inline bool parseScriptedImguizmoVirtualCheck( - const nlohmann::json& check, - CCameraScriptedInputCheck& outCheck, - CCameraScriptedInputParseResult& out) -{ - outCheck.kind = CCameraScriptedInputCheck::Kind::ImguizmoVirtual; - outCheck.tolerance = check.value("tolerance", outCheck.tolerance); - - if (!check.contains("events")) - { - appendScriptedInputParseWarning(out, "Imguizmo virtual check missing \"events\"."); - return false; - } - - for (const auto& expectedEvent : check["events"]) - { - if (!expectedEvent.contains("type") || !expectedEvent.contains("magnitude")) - { - appendScriptedInputParseWarning(out, "Imguizmo virtual check event missing \"type\" or \"magnitude\"."); - continue; - } - - const auto typeStr = expectedEvent["type"].get(); - const auto type = CVirtualGimbalEvent::stringToVirtualEvent(typeStr); - if (type == CVirtualGimbalEvent::None) - { - appendScriptedInputParseWarning(out, "Imguizmo virtual check event has invalid type \"" + typeStr + "\"."); - continue; - } - - CCameraScriptedInputCheck::ExpectedVirtualEvent expected; - expected.type = type; - expected.magnitude = expectedEvent["magnitude"].get(); - outCheck.expectedVirtualEvents.emplace_back(expected); - } - - return true; -} - -inline bool parseScriptedCheck( - const nlohmann::json& check, - CCameraScriptedInputParseResult& out) -{ - if (!check.contains("frame") || !check.contains("kind")) - { - appendScriptedInputParseWarning(out, "Scripted check missing \"frame\" or \"kind\"."); - return false; - } - - const auto frame = check["frame"].get(); - const auto kind = check["kind"].get(); - - CCameraScriptedInputCheck entry; - entry.frame = frame; - - if (kind == "baseline") - { - entry.kind = CCameraScriptedInputCheck::Kind::Baseline; - } - else if (kind == "imguizmo_virtual") - { - if (!parseScriptedImguizmoVirtualCheck(check, entry, out)) - return false; - } - else if (kind == "gimbal_near") - { - entry.kind = CCameraScriptedInputCheck::Kind::GimbalNear; - entry.posTolerance = check.value("pos_tolerance", entry.posTolerance); - entry.eulerToleranceDeg = check.value("euler_tolerance_deg", entry.eulerToleranceDeg); - - if (check.contains("position")) - { - const auto pos = check["position"].get>(); - entry.expectedPos = float32_t3(pos[0], pos[1], pos[2]); - entry.hasExpectedPos = true; - } - if (check.contains("euler_deg")) - { - const auto euler = check["euler_deg"].get>(); - entry.expectedEulerDeg = float32_t3(euler[0], euler[1], euler[2]); - entry.hasExpectedEuler = true; - } - } - else if (kind == "gimbal_delta") - { - entry.kind = 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 = 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) - { - appendScriptedInputParseWarning(out, "gimbal_step check requires at least one delta constraint."); - return false; - } - } - else - { - appendScriptedInputParseWarning(out, "Scripted check has invalid kind \"" + kind + "\"."); - return false; - } - - out.timeline.checks.emplace_back(std::move(entry)); - return true; -} - -inline void parseScriptedChecks( - const nlohmann::json& script, - CCameraScriptedInputParseResult& out) -{ - if (!script.contains("checks")) - return; - - for (const auto& check : script["checks"]) - parseScriptedCheck(check, out); -} - -inline bool deserializeCameraScriptedInput( - const nlohmann::json& script, - CCameraScriptedInputParseResult& out, - std::string* error = nullptr) -{ - 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"; - - parseScriptedCaptureFrames(script, out); - - if (script.contains("camera_controls")) - parseScriptedControlOverrides(script["camera_controls"], out.cameraControls); - - if (!parseScriptedSequenceIfPresent(script, out, error)) - return false; - - parseScriptedInputEvents(script, out); - parseScriptedChecks(script, out); - - finalizeScriptedTimeline(out.timeline); - return true; -} +bool readCameraScriptedInput(std::istream& in, CCameraScriptedInputParseResult& out, std::string* error = nullptr); +bool loadCameraScriptedInputFromFile(const path& path, CCameraScriptedInputParseResult& out, std::string* error = nullptr); -} // namespace nbl::hlsl +} // namespace nbl::system -#endif +#endif // _C_CAMERA_SCRIPTED_RUNTIME_PERSISTENCE_HPP_ diff --git a/common/include/camera/CCameraSequenceScript.hpp b/common/include/camera/CCameraSequenceScript.hpp index 6b64a2e28..37f15aeec 100644 --- a/common/include/camera/CCameraSequenceScript.hpp +++ b/common/include/camera/CCameraSequenceScript.hpp @@ -12,11 +12,11 @@ #include #include +#include "CCameraMathUtilities.hpp" #include "CCameraKeyframeTrack.hpp" #include "IPlanarProjection.hpp" -#include "glm/glm/gtc/quaternion.hpp" -namespace nbl::hlsl +namespace nbl::core { /** @@ -33,7 +33,7 @@ namespace nbl::hlsl * The format intentionally does not store: * * - per-frame low-level event dumps -* - consumer-specific window actions as authored source data +* - runtime-specific window actions as authored source data * - ImGuizmo transforms as the primary authored primitive * * A consumer may expand the compact sequence into its own runtime event/check representation, but @@ -62,7 +62,7 @@ struct CCameraSequenceContinuitySettings }; //! Relative goal adjustment authored against an initial preset captured from the target camera. -//! Deltas stay camera-domain and avoid binding the authored file to any specific input device or example. +//! Deltas stay camera-domain and avoid binding the authored file to any specific input device or consumer. struct CCameraSequenceGoalDelta { bool hasPositionOffset = false; @@ -114,7 +114,7 @@ struct CCameraSequenceKeyframe struct CCameraSequenceTrackedTargetPose { float64_t3 position = float64_t3(0.0); - glm::quat orientation = glm::quat(1.0, 0.0, 0.0, 0.0); + camera_quaternion_t orientation = makeIdentityQuaternion(); }; //! Relative tracked-target adjustment authored against an initial tracked-target pose. @@ -128,7 +128,7 @@ struct CCameraSequenceTrackedTargetDelta }; //! One authored tracked-target keyframe inside a reusable camera-sequence segment. -//! Target keyframes stay camera-domain and can drive follow behavior without example-specific object references. +//! Target keyframes stay camera-domain and can drive follow behavior without runtime-object references. struct CCameraSequenceTrackedTargetKeyframe { float time = 0.f; @@ -294,541 +294,6 @@ inline void normalizeCaptureFractions(std::vector& fractions) fractions.end()); } -inline bool tryParseCaptureFraction(const nlohmann::json& entry, float& outFraction) -{ - if (entry.is_number()) - { - outFraction = std::clamp(entry.get(), 0.f, 1.f); - return true; - } - - if (!entry.is_string()) - return false; - - const auto tag = entry.get(); - if (tag == "start") - outFraction = 0.f; - else if (tag == "mid" || tag == "middle") - outFraction = 0.5f; - else if (tag == "end") - outFraction = 1.f; - else - return false; - - return true; -} - -inline bool deserializeSequencePresentations(const nlohmann::json& root, std::vector& out, std::string* error = nullptr) -{ - out.clear(); - if (!root.is_array()) - { - if (error) - *error = "Sequence presentations must be an array."; - return false; - } - - for (const auto& entry : root) - { - if (!entry.is_object() || !entry.contains("projection")) - { - if (error) - *error = "Sequence presentation entry missing \"projection\"."; - return false; - } - - CCameraSequencePresentation presentation; - if (!tryParseProjectionType(entry["projection"].get(), presentation.projection)) - { - if (error) - *error = "Sequence presentation has invalid projection type."; - return false; - } - if (entry.contains("left_handed")) - presentation.leftHanded = entry["left_handed"].get(); - out.emplace_back(presentation); - } - - return true; -} - -inline bool deserializeSequenceContinuity(const nlohmann::json& root, CCameraSequenceContinuitySettings& out, std::string* error = nullptr) -{ - if (!root.is_object()) - { - if (error) - *error = "Sequence continuity settings must be an object."; - return false; - } - - out = {}; - if (root.contains("baseline")) - out.baseline = root["baseline"].get(); - if (root.contains("step")) - out.step = root["step"].get(); - - if (root.contains("min_pos_delta")) - { - out.minPosDelta = root["min_pos_delta"].get(); - out.hasPosDeltaConstraint = true; - } - if (root.contains("max_pos_delta")) - { - out.maxPosDelta = root["max_pos_delta"].get(); - out.hasPosDeltaConstraint = true; - } - else if (root.contains("pos_tolerance")) - { - out.maxPosDelta = root["pos_tolerance"].get(); - out.hasPosDeltaConstraint = true; - } - - if (root.contains("min_euler_delta_deg")) - { - out.minEulerDeltaDeg = root["min_euler_delta_deg"].get(); - out.hasEulerDeltaConstraint = true; - } - if (root.contains("max_euler_delta_deg")) - { - out.maxEulerDeltaDeg = root["max_euler_delta_deg"].get(); - out.hasEulerDeltaConstraint = true; - } - else if (root.contains("euler_tolerance_deg")) - { - out.maxEulerDeltaDeg = root["euler_tolerance_deg"].get(); - out.hasEulerDeltaConstraint = true; - } - - if (root.contains("disable_pos_delta")) - out.hasPosDeltaConstraint = !root["disable_pos_delta"].get(); - if (root.contains("disable_euler_delta")) - out.hasEulerDeltaConstraint = !root["disable_euler_delta"].get(); - - if (out.step && !(out.hasPosDeltaConstraint || out.hasEulerDeltaConstraint)) - { - if (error) - *error = "Sequence continuity step checks require at least one delta constraint."; - return false; - } - - return true; -} - -inline bool deserializeSequenceGoalDelta(const nlohmann::json& root, CCameraSequenceGoalDelta& out, std::string* error = nullptr) -{ - if (!root.is_object()) - { - if (error) - *error = "Sequence keyframe delta must be an object."; - return false; - } - - out = {}; - auto readFloat3 = [](const nlohmann::json& entry, auto& outValue) -> void - { - const auto arr = entry.get>(); - outValue = std::decay_t(arr[0], arr[1], arr[2]); - }; - auto readDouble3 = [](const nlohmann::json& entry, auto& outValue) -> void - { - const auto arr = entry.get>(); - outValue = std::decay_t(arr[0], arr[1], arr[2]); - }; - - if (root.contains("position_offset")) - { - readDouble3(root["position_offset"], out.positionOffset); - out.hasPositionOffset = true; - } - if (root.contains("rotation_euler_deg_offset")) - { - readFloat3(root["rotation_euler_deg_offset"], out.rotationEulerDegOffset); - out.hasRotationEulerDegOffset = true; - } - if (root.contains("target_offset")) - { - readDouble3(root["target_offset"], out.targetOffset); - out.hasTargetOffset = true; - } - if (root.contains("orbit_u_delta_deg")) - { - out.orbitUDeltaDeg = root["orbit_u_delta_deg"].get(); - out.hasOrbitUDeltaDeg = true; - } - if (root.contains("orbit_v_delta_deg")) - { - out.orbitVDeltaDeg = root["orbit_v_delta_deg"].get(); - out.hasOrbitVDeltaDeg = true; - } - if (root.contains("orbit_distance_delta")) - { - out.orbitDistanceDelta = root["orbit_distance_delta"].get(); - out.hasOrbitDistanceDelta = true; - } - if (root.contains("path_angle_delta_deg")) - { - out.pathAngleDeltaDeg = root["path_angle_delta_deg"].get(); - out.hasPathAngleDeltaDeg = true; - } - if (root.contains("path_radius_delta")) - { - out.pathRadiusDelta = root["path_radius_delta"].get(); - out.hasPathRadiusDelta = true; - } - if (root.contains("path_height_delta")) - { - out.pathHeightDelta = root["path_height_delta"].get(); - out.hasPathHeightDelta = true; - } - if (root.contains("dynamic_base_fov_delta")) - { - out.dynamicBaseFovDelta = root["dynamic_base_fov_delta"].get(); - out.hasDynamicBaseFovDelta = true; - } - if (root.contains("dynamic_reference_distance_delta")) - { - out.dynamicReferenceDistanceDelta = root["dynamic_reference_distance_delta"].get(); - out.hasDynamicReferenceDistanceDelta = true; - } - - return true; -} - -inline bool deserializeSequenceKeyframe(const nlohmann::json& root, CCameraSequenceKeyframe& out, std::string* error = nullptr) -{ - if (!root.is_object()) - { - if (error) - *error = "Sequence keyframe must be an object."; - return false; - } - - out = {}; - if (root.contains("time")) - out.time = std::max(0.f, root["time"].get()); - - if (root.contains("delta")) - { - if (!deserializeSequenceGoalDelta(root["delta"], out.delta, error)) - return false; - out.hasDelta = true; - } - - if (root.contains("preset")) - { - deserializePreset(root["preset"], out.absolutePreset); - out.hasAbsolutePreset = true; - } - else if (root.contains("position") || root.contains("orientation") || root.contains("target_position") || - root.contains("distance") || root.contains("orbit_u") || root.contains("orbit_v") || - root.contains("orbit_distance") || root.contains("path_angle") || root.contains("path_radius") || - root.contains("path_height") || root.contains("dynamic_base_fov") || root.contains("dynamic_reference_distance")) - { - deserializePreset(root, out.absolutePreset); - out.hasAbsolutePreset = true; - } - - return true; -} - -inline bool deserializeSequenceTrackedTargetDelta(const nlohmann::json& root, CCameraSequenceTrackedTargetDelta& out, std::string* error = nullptr) -{ - if (!root.is_object()) - { - if (error) - *error = "Sequence target delta must be an object."; - return false; - } - - out = {}; - auto readFloat3 = [](const nlohmann::json& entry, auto& outValue) -> void - { - const auto arr = entry.get>(); - outValue = std::decay_t(arr[0], arr[1], arr[2]); - }; - auto readDouble3 = [](const nlohmann::json& entry, auto& outValue) -> void - { - const auto arr = entry.get>(); - outValue = std::decay_t(arr[0], arr[1], arr[2]); - }; - - if (root.contains("position_offset")) - { - readDouble3(root["position_offset"], out.positionOffset); - out.hasPositionOffset = true; - } - if (root.contains("rotation_euler_deg_offset")) - { - readFloat3(root["rotation_euler_deg_offset"], out.rotationEulerDegOffset); - out.hasRotationEulerDegOffset = true; - } - - return true; -} - -inline bool deserializeSequenceTrackedTargetKeyframe(const nlohmann::json& root, CCameraSequenceTrackedTargetKeyframe& out, std::string* error = nullptr) -{ - if (!root.is_object()) - { - if (error) - *error = "Sequence target keyframe must be an object."; - return false; - } - - out = {}; - if (root.contains("time")) - out.time = std::max(0.f, root["time"].get()); - - if (root.contains("delta")) - { - if (!deserializeSequenceTrackedTargetDelta(root["delta"], out.delta, error)) - return false; - out.hasDelta = true; - } - - if (root.contains("position")) - { - const auto arr = root["position"].get>(); - out.absolutePosition = float64_t3(arr[0], arr[1], arr[2]); - out.hasAbsolutePosition = true; - } - if (root.contains("rotation_euler_deg")) - { - const auto arr = root["rotation_euler_deg"].get>(); - out.absoluteRotationEulerDeg = float32_t3(arr[0], arr[1], arr[2]); - out.hasAbsoluteRotationEulerDeg = true; - } - - return true; -} - -inline bool deserializeSequenceSegment(const nlohmann::json& root, CCameraSequenceSegment& out, std::string* error = nullptr) -{ - if (!root.is_object()) - { - if (error) - *error = "Sequence segment must be an object."; - return false; - } - - out = {}; - if (root.contains("name")) - out.name = root["name"].get(); - if (root.contains("camera_identifier")) - out.cameraIdentifier = root["camera_identifier"].get(); - if (root.contains("camera_kind")) - { - if (!tryParseCameraKind(root["camera_kind"].get(), out.cameraKind)) - { - if (error) - *error = "Sequence segment has invalid camera_kind."; - return false; - } - } - if (root.contains("duration_seconds")) - { - out.durationSeconds = std::max(0.f, root["duration_seconds"].get()); - out.hasDurationSeconds = true; - } - if (root.contains("reset_camera")) - { - out.resetCamera = root["reset_camera"].get(); - out.hasResetCamera = true; - } - if (root.contains("presentations")) - { - if (!deserializeSequencePresentations(root["presentations"], out.presentations, error)) - return false; - } - if (root.contains("continuity")) - { - if (!deserializeSequenceContinuity(root["continuity"], out.continuity, error)) - return false; - out.hasContinuity = true; - } - if (root.contains("captures")) - { - if (!root["captures"].is_array()) - { - if (error) - *error = "Sequence segment captures must be an array."; - return false; - } - - out.captureFractions.clear(); - for (const auto& entry : root["captures"]) - { - float fraction = 0.f; - if (!tryParseCaptureFraction(entry, fraction)) - { - if (error) - *error = "Sequence segment capture entry is invalid."; - return false; - } - out.captureFractions.emplace_back(fraction); - } - normalizeCaptureFractions(out.captureFractions); - out.hasCaptureFractions = true; - } - if (root.contains("keyframes")) - { - if (!root["keyframes"].is_array()) - { - if (error) - *error = "Sequence segment keyframes must be an array."; - return false; - } - for (const auto& entry : root["keyframes"]) - { - CCameraSequenceKeyframe keyframe; - if (!deserializeSequenceKeyframe(entry, keyframe, error)) - return false; - out.keyframes.emplace_back(std::move(keyframe)); - } - } - if (root.contains("target_keyframes")) - { - if (!root["target_keyframes"].is_array()) - { - if (error) - *error = "Sequence segment target_keyframes must be an array."; - return false; - } - for (const auto& entry : root["target_keyframes"]) - { - CCameraSequenceTrackedTargetKeyframe keyframe; - if (!deserializeSequenceTrackedTargetKeyframe(entry, keyframe, error)) - return false; - out.targetKeyframes.emplace_back(std::move(keyframe)); - } - } - - if (out.keyframes.empty()) - { - if (error) - *error = "Sequence segment requires at least one keyframe."; - return false; - } - if (out.cameraKind == ICamera::CameraKind::Unknown && out.cameraIdentifier.empty()) - { - if (error) - *error = "Sequence segment requires camera_kind or camera_identifier."; - return false; - } - - return true; -} - -inline bool deserializeCameraSequenceScript(const nlohmann::json& root, CCameraSequenceScript& out, std::string* error = nullptr) -{ - if (!root.is_object()) - { - if (error) - *error = "Camera sequence script must be an object."; - return false; - } - - out = {}; - if (root.contains("enabled")) - out.enabled = root["enabled"].get(); - if (root.contains("log")) - out.log = root["log"].get(); - if (root.contains("exclusive")) - out.exclusive = root["exclusive"].get(); - if (root.contains("exclusive_input")) - out.exclusive = root["exclusive_input"].get() || out.exclusive; - if (root.contains("hard_fail")) - out.hardFail = root["hard_fail"].get(); - if (root.contains("visual_debug")) - out.visualDebug = root["visual_debug"].get(); - if (root.contains("visual_debug_target_fps")) - out.visualDebugTargetFps = root["visual_debug_target_fps"].get(); - if (root.contains("visual_debug_hold_seconds")) - out.visualDebugHoldSeconds = root["visual_debug_hold_seconds"].get(); - if (root.contains("enableActiveCameraMovement")) - { - out.enableActiveCameraMovement = root["enableActiveCameraMovement"].get(); - out.hasEnableActiveCameraMovement = true; - } - if (root.contains("capture_prefix")) - out.capturePrefix = root["capture_prefix"].get(); - if (root.contains("fps")) - out.fps = std::max(1.f, root["fps"].get()); - - if (root.contains("defaults")) - { - const auto& defaults = root["defaults"]; - if (!defaults.is_object()) - { - if (error) - *error = "Camera sequence defaults must be an object."; - return false; - } - - if (defaults.contains("duration_seconds")) - out.defaults.durationSeconds = std::max(0.f, defaults["duration_seconds"].get()); - if (defaults.contains("reset_camera")) - out.defaults.resetCamera = defaults["reset_camera"].get(); - if (defaults.contains("presentations")) - { - if (!deserializeSequencePresentations(defaults["presentations"], out.defaults.presentations, error)) - return false; - } - if (defaults.contains("continuity")) - { - if (!deserializeSequenceContinuity(defaults["continuity"], out.defaults.continuity, error)) - return false; - } - if (defaults.contains("captures")) - { - if (!defaults["captures"].is_array()) - { - if (error) - *error = "Camera sequence default captures must be an array."; - return false; - } - - out.defaults.captureFractions.clear(); - for (const auto& entry : defaults["captures"]) - { - float fraction = 0.f; - if (!tryParseCaptureFraction(entry, fraction)) - { - if (error) - *error = "Camera sequence default capture entry is invalid."; - return false; - } - out.defaults.captureFractions.emplace_back(fraction); - } - normalizeCaptureFractions(out.defaults.captureFractions); - } - } - - if (!root.contains("segments") || !root["segments"].is_array()) - { - if (error) - *error = "Camera sequence script requires a \"segments\" array."; - return false; - } - - for (const auto& entry : root["segments"]) - { - CCameraSequenceSegment segment; - if (!deserializeSequenceSegment(entry, segment, error)) - return false; - out.segments.emplace_back(std::move(segment)); - } - - if (out.segments.empty()) - { - if (error) - *error = "Camera sequence script must contain at least one segment."; - return false; - } - - return true; -} - inline bool applyCanonicalSphericalGoal(CCameraGoal& goal) { if (!(goal.hasTargetPosition && goal.hasOrbitState)) @@ -836,7 +301,7 @@ inline bool applyCanonicalSphericalGoal(CCameraGoal& goal) if (!std::isfinite(goal.orbitU) || !std::isfinite(goal.orbitV) || !std::isfinite(goal.orbitDistance)) return false; - const float appliedDistance = std::clamp(goal.orbitDistance, CSphericalTargetCamera::MinDistance, CSphericalTargetCamera::MaxDistance); + const float appliedDistance = std::clamp(goal.orbitDistance, ICamera::SphericalMinDistance, ICamera::SphericalMaxDistance); const float64_t3 spherePosition( std::cos(goal.orbitV) * std::cos(goal.orbitU) * static_cast(appliedDistance), std::cos(goal.orbitV) * std::sin(goal.orbitU) * static_cast(appliedDistance), @@ -852,7 +317,7 @@ inline bool applyCanonicalSphericalGoal(CCameraGoal& goal) -std::sin(goal.orbitV) * std::sin(goal.orbitU), std::cos(goal.orbitV))); const float64_t3 right = normalize(cross(up, forward)); - goal.orientation = glm::quat_cast(glm::dmat3{ right, up, forward }); + goal.orientation = makeQuaternionFromBasis(right, up, forward); return true; } @@ -891,8 +356,7 @@ inline bool buildSequenceKeyframePreset(const CCameraPreset& reference, const CC if (delta.hasRotationEulerDegOffset) { - const auto deltaRadians = glm::radians(delta.rotationEulerDegOffset); - goal.orientation = glm::normalize(goal.orientation * glm::quat(deltaRadians)); + goal.orientation = normalizeQuaternion(goal.orientation * makeQuaternionFromEulerDegrees(getCastedVector(delta.rotationEulerDegOffset))); } if (delta.hasTargetOffset) @@ -915,9 +379,9 @@ inline bool buildSequenceKeyframePreset(const CCameraPreset& reference, const CC return false; } if (delta.hasOrbitUDeltaDeg) - goal.orbitU = wrapAngleRad(goal.orbitU + glm::radians(delta.orbitUDeltaDeg)); + goal.orbitU = wrapAngleRad(goal.orbitU + hlsl::radians(delta.orbitUDeltaDeg)); if (delta.hasOrbitVDeltaDeg) - goal.orbitV = std::clamp(goal.orbitV + glm::radians(delta.orbitVDeltaDeg), -1.55334303427, 1.55334303427); + goal.orbitV = std::clamp(goal.orbitV + hlsl::radians(delta.orbitVDeltaDeg), -1.55334303427, 1.55334303427); if (delta.hasOrbitDistanceDelta) goal.orbitDistance += delta.orbitDistanceDelta; } @@ -931,7 +395,7 @@ inline bool buildSequenceKeyframePreset(const CCameraPreset& reference, const CC return false; } if (delta.hasPathAngleDeltaDeg) - goal.pathState.angle = wrapAngleRad(goal.pathState.angle + glm::radians(delta.pathAngleDeltaDeg)); + goal.pathState.angle = wrapAngleRad(goal.pathState.angle + hlsl::radians(delta.pathAngleDeltaDeg)); if (delta.hasPathRadiusDelta) goal.pathState.radius += delta.pathRadiusDelta; if (delta.hasPathHeightDelta) @@ -1004,10 +468,10 @@ inline bool buildSequenceTrackFromReference(const CCameraPreset& reference, cons inline bool isSequenceTrackedTargetPoseFinite(const CCameraSequenceTrackedTargetPose& pose) { return isFiniteVec3(pose.position) && - std::isfinite(pose.orientation.x) && - std::isfinite(pose.orientation.y) && - std::isfinite(pose.orientation.z) && - std::isfinite(pose.orientation.w); + std::isfinite(pose.orientation.data.x) && + std::isfinite(pose.orientation.data.y) && + std::isfinite(pose.orientation.data.z) && + std::isfinite(pose.orientation.data.w); } inline bool buildSequenceTrackedTargetPoseFromReference( @@ -1021,14 +485,14 @@ inline bool buildSequenceTrackedTargetPoseFromReference( if (authored.hasAbsolutePosition) outPose.position = authored.absolutePosition; if (authored.hasAbsoluteRotationEulerDeg) - outPose.orientation = glm::quat(glm::radians(authored.absoluteRotationEulerDeg)); + outPose.orientation = makeQuaternionFromEulerDegrees(getCastedVector(authored.absoluteRotationEulerDeg)); if (authored.hasDelta) { if (authored.delta.hasPositionOffset) outPose.position += authored.delta.positionOffset; if (authored.delta.hasRotationEulerDegOffset) - outPose.orientation = glm::normalize(outPose.orientation * glm::quat(glm::radians(authored.delta.rotationEulerDegOffset))); + outPose.orientation = normalizeQuaternion(outPose.orientation * makeQuaternionFromEulerDegrees(getCastedVector(authored.delta.rotationEulerDegOffset))); } if (!isSequenceTrackedTargetPoseFinite(outPose)) @@ -1109,7 +573,7 @@ inline bool tryBuildSequenceTrackedTargetPoseAtTime( const auto span = std::max(1e-6f, rhs.time - lhs.time); const auto alpha = std::clamp((time - lhs.time) / span, 0.f, 1.f); outPose.position = lhs.pose.position + (rhs.pose.position - lhs.pose.position) * static_cast(alpha); - outPose.orientation = glm::normalize(glm::slerp(lhs.pose.orientation, rhs.pose.orientation, alpha)); + outPose.orientation = slerpQuaternion(lhs.pose.orientation, rhs.pose.orientation, static_cast(alpha)); return true; } @@ -1129,7 +593,7 @@ inline float getSequenceSegmentDurationSeconds(const CCameraSequenceScript& scri if (script.defaults.durationSeconds > 0.f) return script.defaults.durationSeconds; if (track) - return getPlaybackTrackDuration(*track); + return track->keyframes.empty() ? 0.f : track->keyframes.back().time; return 0.f; } @@ -1274,6 +738,6 @@ inline bool buildCompiledSegmentFramePolicies( return true; } -} // namespace nbl::hlsl +} // namespace nbl::core #endif // _C_CAMERA_SEQUENCE_SCRIPT_HPP_ diff --git a/common/include/camera/CCameraSequenceScriptPersistence.hpp b/common/include/camera/CCameraSequenceScriptPersistence.hpp new file mode 100644 index 000000000..1d00433dc --- /dev/null +++ b/common/include/camera/CCameraSequenceScriptPersistence.hpp @@ -0,0 +1,22 @@ +// 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_SEQUENCE_SCRIPT_PERSISTENCE_HPP_ +#define _C_CAMERA_SEQUENCE_SCRIPT_PERSISTENCE_HPP_ + +#include +#include + +#include "CCameraSequenceScript.hpp" +#include "nbl/system/path.h" + +namespace nbl::system +{ + +bool readCameraSequenceScript(std::istream& in, core::CCameraSequenceScript& out, std::string* error = nullptr); +bool loadCameraSequenceScriptFromFile(const path& path, core::CCameraSequenceScript& out, std::string* error = nullptr); + +} // namespace nbl::system + +#endif // _C_CAMERA_SEQUENCE_SCRIPT_PERSISTENCE_HPP_ diff --git a/common/include/camera/CCameraSequenceScriptedBuilder.hpp b/common/include/camera/CCameraSequenceScriptedBuilder.hpp index 7bb3191df..db09943aa 100644 --- a/common/include/camera/CCameraSequenceScriptedBuilder.hpp +++ b/common/include/camera/CCameraSequenceScriptedBuilder.hpp @@ -11,7 +11,7 @@ #include "CCameraSequenceScript.hpp" #include "ICamera.hpp" -namespace nbl::hlsl +namespace nbl::system { /** @@ -19,7 +19,7 @@ namespace nbl::hlsl * * This keeps authored sequence semantics in shared camera helpers instead of re-encoding * `Goal`, `TrackedTargetTransform`, `Baseline`, `GimbalStep`, and capture scheduling inside -* one example. +* one consumer. */ struct CCameraSequenceScriptedSegmentBuildInfo { @@ -37,11 +37,11 @@ struct CCameraSequenceScriptedSegmentBuildInfo inline bool appendCompiledSequenceSegmentToScriptedTimeline( CCameraScriptedTimeline& timeline, const uint64_t baseFrame, - const CCameraSequenceCompiledSegment& compiledSegment, + const core::CCameraSequenceCompiledSegment& compiledSegment, const CCameraSequenceScriptedSegmentBuildInfo& buildInfo, std::string* error = nullptr) { - std::vector framePolicies; + std::vector framePolicies; if (!buildCompiledSegmentFramePolicies(compiledSegment, framePolicies, buildInfo.includeFollowTargetLock)) { if (error) @@ -74,7 +74,7 @@ inline bool appendCompiledSequenceSegmentToScriptedTimeline( for (const auto& policy : framePolicies) { - CCameraPreset preset; + core::CCameraPreset preset; if (!tryBuildKeyframeTrackPresetAtTime(compiledSegment.track, policy.sampleTime, preset)) { if (error) @@ -85,7 +85,7 @@ inline bool appendCompiledSequenceSegmentToScriptedTimeline( if (compiledSegment.usesTrackedTargetTrack()) { - CCameraSequenceTrackedTargetPose trackedTargetPose; + core::CCameraSequenceTrackedTargetPose trackedTargetPose; if (!tryBuildSequenceTrackedTargetPoseAtTime(compiledSegment.trackedTargetTrack, policy.sampleTime, trackedTargetPose)) { if (error) @@ -93,8 +93,8 @@ inline bool appendCompiledSequenceSegmentToScriptedTimeline( return false; } - ICamera::CGimbal gimbal({ .position = trackedTargetPose.position, .orientation = trackedTargetPose.orientation }); - appendScriptedTrackedTargetTransformEvent(timeline, baseFrame + policy.frameOffset, gimbal.operator()()); + core::ICamera::CGimbal gimbal({ .position = trackedTargetPose.position, .orientation = trackedTargetPose.orientation }); + appendScriptedTrackedTargetTransformEvent(timeline, baseFrame + policy.frameOffset, gimbal.operator()()); } if (policy.baseline) @@ -120,6 +120,6 @@ inline bool appendCompiledSequenceSegmentToScriptedTimeline( return true; } -} // namespace nbl::hlsl +} // namespace nbl::system #endif diff --git a/common/include/camera/CCameraTextUtilities.hpp b/common/include/camera/CCameraTextUtilities.hpp index c51555e08..70b7f2a04 100644 --- a/common/include/camera/CCameraTextUtilities.hpp +++ b/common/include/camera/CCameraTextUtilities.hpp @@ -12,7 +12,7 @@ #include "CCameraGoalAnalysis.hpp" #include "CCameraPresetFlow.hpp" -namespace nbl::hlsl +namespace nbl::core { //! Return a short human-readable label for a camera kind. @@ -200,6 +200,6 @@ inline std::string describePresetApplySummary(const SCameraPresetApplySummary& s return oss.str(); } -} // namespace nbl::hlsl +} // namespace nbl::core #endif // _C_CAMERA_TEXT_UTILITIES_HPP_ diff --git a/common/include/camera/CChaseCamera.hpp b/common/include/camera/CChaseCamera.hpp index b9e30035b..c7be77460 100644 --- a/common/include/camera/CChaseCamera.hpp +++ b/common/include/camera/CChaseCamera.hpp @@ -6,7 +6,7 @@ #include "CSphericalTargetCamera.hpp" -namespace nbl::hlsl +namespace nbl::core { class CChaseCamera final : public CSphericalTargetCamera @@ -22,10 +22,6 @@ class CChaseCamera final : public CSphericalTargetCamera } ~CChaseCamera() = default; - const base_t::keyboard_to_virtual_events_t getKeyboardMappingPreset() const override { return m_keyboard_to_virtual_events_preset; } - const base_t::mouse_to_virtual_events_t getMouseMappingPreset() const override { return m_mouse_to_virtual_events_preset; } - const base_t::imguizmo_to_virtual_events_t getImguizmoMappingPreset() const override { return m_imguizmo_to_virtual_events_preset; } - const typename base_t::CGimbal& getGimbal() override { return m_gimbal; } virtual bool manipulate(std::span virtualEvents, const float64_t4x4* referenceFrame = nullptr) override @@ -67,66 +63,14 @@ class CChaseCamera final : public CSphericalTargetCamera return applyPose(); } - virtual const uint32_t getAllowedVirtualEvents() override { return AllowedVirtualEvents; } + virtual uint32_t getAllowedVirtualEvents() const override { return AllowedVirtualEvents; } virtual CameraKind getKind() const override { return CameraKind::Chase; } - virtual const std::string_view getIdentifier() override { return "Chase Camera"; } + virtual std::string_view getIdentifier() const override { return "Chase Camera"; } private: static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate | CVirtualGimbalEvent::Rotate; static inline constexpr double MaxPitch = 1.2217304763960306; static inline constexpr double MinPitch = -1.0471975511965976; - - static inline const auto m_keyboard_to_virtual_events_preset = []() - { - typename base_t::keyboard_to_virtual_events_t preset; - - preset[ui::E_KEY_CODE::EKC_W] = CVirtualGimbalEvent::MoveForward; - preset[ui::E_KEY_CODE::EKC_S] = CVirtualGimbalEvent::MoveBackward; - preset[ui::E_KEY_CODE::EKC_A] = CVirtualGimbalEvent::MoveLeft; - preset[ui::E_KEY_CODE::EKC_D] = CVirtualGimbalEvent::MoveRight; - preset[ui::E_KEY_CODE::EKC_Q] = CVirtualGimbalEvent::MoveDown; - preset[ui::E_KEY_CODE::EKC_E] = CVirtualGimbalEvent::MoveUp; - preset[ui::E_KEY_CODE::EKC_I] = CVirtualGimbalEvent::TiltDown; - preset[ui::E_KEY_CODE::EKC_K] = CVirtualGimbalEvent::TiltUp; - preset[ui::E_KEY_CODE::EKC_J] = CVirtualGimbalEvent::PanLeft; - preset[ui::E_KEY_CODE::EKC_L] = CVirtualGimbalEvent::PanRight; - - return preset; - }(); - - static inline const auto m_mouse_to_virtual_events_preset = []() - { - typename base_t::mouse_to_virtual_events_t preset; - - preset[ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_X] = CVirtualGimbalEvent::PanRight; - preset[ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_X] = CVirtualGimbalEvent::PanLeft; - preset[ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_Y] = CVirtualGimbalEvent::TiltUp; - preset[ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_Y] = CVirtualGimbalEvent::TiltDown; - preset[ui::E_MOUSE_CODE::EMC_VERTICAL_POSITIVE_SCROLL] = CVirtualGimbalEvent::MoveUp; - preset[ui::E_MOUSE_CODE::EMC_HORIZONTAL_POSITIVE_SCROLL] = CVirtualGimbalEvent::MoveUp; - preset[ui::E_MOUSE_CODE::EMC_VERTICAL_NEGATIVE_SCROLL] = CVirtualGimbalEvent::MoveDown; - preset[ui::E_MOUSE_CODE::EMC_HORIZONTAL_NEGATIVE_SCROLL] = CVirtualGimbalEvent::MoveDown; - - return preset; - }(); - - static inline const auto m_imguizmo_to_virtual_events_preset = []() - { - typename base_t::imguizmo_to_virtual_events_t preset; - - preset[CVirtualGimbalEvent::MoveForward] = CVirtualGimbalEvent::MoveForward; - preset[CVirtualGimbalEvent::MoveBackward] = CVirtualGimbalEvent::MoveBackward; - preset[CVirtualGimbalEvent::MoveLeft] = CVirtualGimbalEvent::MoveLeft; - preset[CVirtualGimbalEvent::MoveRight] = CVirtualGimbalEvent::MoveRight; - preset[CVirtualGimbalEvent::MoveUp] = CVirtualGimbalEvent::MoveUp; - preset[CVirtualGimbalEvent::MoveDown] = CVirtualGimbalEvent::MoveDown; - preset[CVirtualGimbalEvent::TiltDown] = CVirtualGimbalEvent::TiltDown; - preset[CVirtualGimbalEvent::TiltUp] = CVirtualGimbalEvent::TiltUp; - preset[CVirtualGimbalEvent::PanLeft] = CVirtualGimbalEvent::PanLeft; - preset[CVirtualGimbalEvent::PanRight] = CVirtualGimbalEvent::PanRight; - - return preset; - }(); }; } diff --git a/common/include/camera/CCubeProjection.hpp b/common/include/camera/CCubeProjection.hpp index d03ff0840..4afa38f2c 100644 --- a/common/include/camera/CCubeProjection.hpp +++ b/common/include/camera/CCubeProjection.hpp @@ -4,7 +4,7 @@ #include "IRange.hpp" #include "IPerspectiveProjection.hpp" -namespace nbl::hlsl +namespace nbl::core { /** @@ -63,7 +63,7 @@ class CCubeProjection final : public IPerspectiveProjection, public IProjection void transformCube() { - // TODO: update m_quads + // Cube-face quad generation is not implemented yet. } virtual ProjectionType getProjectionType() const override { return ProjectionType::Cube; } @@ -72,12 +72,12 @@ class CCubeProjection final : public IPerspectiveProjection, public IProjection { auto direction = normalize(vecToProjectionSpace); - // TODO: project onto cube using quads representing faces + // Cube-face projection is not implemented yet. } virtual bool unproject(const projection_vector_t& vecFromProjectionSpace, projection_vector_t& output) const override { - // TODO: return back direction vector? + // Reverse projection is not implemented yet. } template @@ -95,6 +95,6 @@ class CCubeProjection final : public IPerspectiveProjection, public IProjection std::array m_quads; }; -} // nbl::hlsl namespace +} // namespace nbl::core #endif // _NBL_CCUBE_PROJECTION_HPP_ diff --git a/common/include/camera/CDollyCamera.hpp b/common/include/camera/CDollyCamera.hpp index 9501962ec..990b9a3d6 100644 --- a/common/include/camera/CDollyCamera.hpp +++ b/common/include/camera/CDollyCamera.hpp @@ -6,7 +6,7 @@ #include "CSphericalTargetCamera.hpp" -namespace nbl::hlsl +namespace nbl::core { class CDollyCamera final : public CSphericalTargetCamera @@ -22,10 +22,6 @@ class CDollyCamera final : public CSphericalTargetCamera } ~CDollyCamera() = default; - const base_t::keyboard_to_virtual_events_t getKeyboardMappingPreset() const override { return m_keyboard_to_virtual_events_preset; } - const base_t::mouse_to_virtual_events_t getMouseMappingPreset() const override { return m_mouse_to_virtual_events_preset; } - const base_t::imguizmo_to_virtual_events_t getImguizmoMappingPreset() const override { return m_imguizmo_to_virtual_events_preset; } - const typename base_t::CGimbal& getGimbal() override { return m_gimbal; } virtual bool manipulate(std::span virtualEvents, const float64_t4x4* referenceFrame = nullptr) override @@ -51,66 +47,14 @@ class CDollyCamera final : public CSphericalTargetCamera return applyPose(); } - virtual const uint32_t getAllowedVirtualEvents() override { return AllowedVirtualEvents; } + virtual uint32_t getAllowedVirtualEvents() const override { return AllowedVirtualEvents; } virtual CameraKind getKind() const override { return CameraKind::Dolly; } - virtual const std::string_view getIdentifier() override { return "Dolly Camera"; } + virtual std::string_view getIdentifier() const override { return "Dolly Camera"; } private: static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate | CVirtualGimbalEvent::Rotate; static inline constexpr double MaxPitch = 1.4835298641951802; static inline constexpr double MinPitch = -1.4835298641951802; - - static inline const auto m_keyboard_to_virtual_events_preset = []() - { - typename base_t::keyboard_to_virtual_events_t preset; - - preset[ui::E_KEY_CODE::EKC_W] = CVirtualGimbalEvent::MoveForward; - preset[ui::E_KEY_CODE::EKC_S] = CVirtualGimbalEvent::MoveBackward; - preset[ui::E_KEY_CODE::EKC_A] = CVirtualGimbalEvent::MoveLeft; - preset[ui::E_KEY_CODE::EKC_D] = CVirtualGimbalEvent::MoveRight; - preset[ui::E_KEY_CODE::EKC_Q] = CVirtualGimbalEvent::MoveDown; - preset[ui::E_KEY_CODE::EKC_E] = CVirtualGimbalEvent::MoveUp; - preset[ui::E_KEY_CODE::EKC_I] = CVirtualGimbalEvent::TiltDown; - preset[ui::E_KEY_CODE::EKC_K] = CVirtualGimbalEvent::TiltUp; - preset[ui::E_KEY_CODE::EKC_J] = CVirtualGimbalEvent::PanLeft; - preset[ui::E_KEY_CODE::EKC_L] = CVirtualGimbalEvent::PanRight; - - return preset; - }(); - - static inline const auto m_mouse_to_virtual_events_preset = []() - { - typename base_t::mouse_to_virtual_events_t preset; - - preset[ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_X] = CVirtualGimbalEvent::PanRight; - preset[ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_X] = CVirtualGimbalEvent::PanLeft; - preset[ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_Y] = CVirtualGimbalEvent::TiltUp; - preset[ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_Y] = CVirtualGimbalEvent::TiltDown; - preset[ui::E_MOUSE_CODE::EMC_VERTICAL_POSITIVE_SCROLL] = CVirtualGimbalEvent::MoveForward; - preset[ui::E_MOUSE_CODE::EMC_HORIZONTAL_POSITIVE_SCROLL] = CVirtualGimbalEvent::MoveForward; - preset[ui::E_MOUSE_CODE::EMC_VERTICAL_NEGATIVE_SCROLL] = CVirtualGimbalEvent::MoveBackward; - preset[ui::E_MOUSE_CODE::EMC_HORIZONTAL_NEGATIVE_SCROLL] = CVirtualGimbalEvent::MoveBackward; - - return preset; - }(); - - static inline const auto m_imguizmo_to_virtual_events_preset = []() - { - typename base_t::imguizmo_to_virtual_events_t preset; - - preset[CVirtualGimbalEvent::MoveForward] = CVirtualGimbalEvent::MoveForward; - preset[CVirtualGimbalEvent::MoveBackward] = CVirtualGimbalEvent::MoveBackward; - preset[CVirtualGimbalEvent::MoveLeft] = CVirtualGimbalEvent::MoveLeft; - preset[CVirtualGimbalEvent::MoveRight] = CVirtualGimbalEvent::MoveRight; - preset[CVirtualGimbalEvent::MoveUp] = CVirtualGimbalEvent::MoveUp; - preset[CVirtualGimbalEvent::MoveDown] = CVirtualGimbalEvent::MoveDown; - preset[CVirtualGimbalEvent::TiltDown] = CVirtualGimbalEvent::TiltDown; - preset[CVirtualGimbalEvent::TiltUp] = CVirtualGimbalEvent::TiltUp; - preset[CVirtualGimbalEvent::PanLeft] = CVirtualGimbalEvent::PanLeft; - preset[CVirtualGimbalEvent::PanRight] = CVirtualGimbalEvent::PanRight; - - return preset; - }(); }; } diff --git a/common/include/camera/CDollyZoomCamera.hpp b/common/include/camera/CDollyZoomCamera.hpp index 88bc277a4..cabb24974 100644 --- a/common/include/camera/CDollyZoomCamera.hpp +++ b/common/include/camera/CDollyZoomCamera.hpp @@ -6,7 +6,7 @@ #include "CSphericalTargetCamera.hpp" -namespace nbl::hlsl +namespace nbl::core { class CDollyZoomCamera final : public CSphericalTargetCamera @@ -21,10 +21,6 @@ class CDollyZoomCamera final : public CSphericalTargetCamera } ~CDollyZoomCamera() = default; - const base_t::keyboard_to_virtual_events_t getKeyboardMappingPreset() const override { return m_keyboard_to_virtual_events_preset; } - const base_t::mouse_to_virtual_events_t getMouseMappingPreset() const override { return m_mouse_to_virtual_events_preset; } - const base_t::imguizmo_to_virtual_events_t getImguizmoMappingPreset() const override { return m_imguizmo_to_virtual_events_preset; } - const typename base_t::CGimbal& getGimbal() override { return m_gimbal; } float getBaseFov() const { return m_baseFov; } @@ -63,7 +59,7 @@ class CDollyZoomCamera final : public CSphericalTargetCamera return applyPose(); } - virtual const uint32_t getAllowedVirtualEvents() override { return AllowedVirtualEvents; } + virtual uint32_t getAllowedVirtualEvents() const override { return AllowedVirtualEvents; } virtual CameraKind getKind() const override { return CameraKind::DollyZoom; } virtual uint32_t getCapabilities() const override { return base_t::getCapabilities() | base_t::DynamicPerspectiveFov; } virtual bool tryGetDynamicPerspectiveFov(float& outFov) const override @@ -86,57 +82,13 @@ class CDollyZoomCamera final : public CSphericalTargetCamera m_referenceDistance = state.referenceDistance; return true; } - virtual const std::string_view getIdentifier() override { return "Dolly Zoom Camera"; } + virtual std::string_view getIdentifier() const override { return "Dolly Zoom Camera"; } private: static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate; float m_baseFov = 40.0f; float m_referenceDistance = 1.0f; - - static inline const auto m_keyboard_to_virtual_events_preset = []() - { - typename base_t::keyboard_to_virtual_events_t preset; - - preset[ui::E_KEY_CODE::EKC_W] = CVirtualGimbalEvent::MoveUp; - preset[ui::E_KEY_CODE::EKC_S] = CVirtualGimbalEvent::MoveDown; - preset[ui::E_KEY_CODE::EKC_A] = CVirtualGimbalEvent::MoveLeft; - preset[ui::E_KEY_CODE::EKC_D] = CVirtualGimbalEvent::MoveRight; - preset[ui::E_KEY_CODE::EKC_E] = CVirtualGimbalEvent::MoveForward; - preset[ui::E_KEY_CODE::EKC_Q] = CVirtualGimbalEvent::MoveBackward; - - return preset; - }(); - - static inline const auto m_mouse_to_virtual_events_preset = []() - { - typename base_t::mouse_to_virtual_events_t preset; - - preset[ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_X] = CVirtualGimbalEvent::MoveRight; - preset[ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_X] = CVirtualGimbalEvent::MoveLeft; - preset[ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_Y] = CVirtualGimbalEvent::MoveUp; - preset[ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_Y] = CVirtualGimbalEvent::MoveDown; - preset[ui::E_MOUSE_CODE::EMC_VERTICAL_POSITIVE_SCROLL] = CVirtualGimbalEvent::MoveForward; - preset[ui::E_MOUSE_CODE::EMC_HORIZONTAL_POSITIVE_SCROLL] = CVirtualGimbalEvent::MoveForward; - preset[ui::E_MOUSE_CODE::EMC_VERTICAL_NEGATIVE_SCROLL] = CVirtualGimbalEvent::MoveBackward; - preset[ui::E_MOUSE_CODE::EMC_HORIZONTAL_NEGATIVE_SCROLL] = CVirtualGimbalEvent::MoveBackward; - - return preset; - }(); - - static inline const auto m_imguizmo_to_virtual_events_preset = []() - { - typename base_t::imguizmo_to_virtual_events_t preset; - - preset[CVirtualGimbalEvent::MoveForward] = CVirtualGimbalEvent::MoveForward; - preset[CVirtualGimbalEvent::MoveBackward] = CVirtualGimbalEvent::MoveBackward; - preset[CVirtualGimbalEvent::MoveLeft] = CVirtualGimbalEvent::MoveLeft; - preset[CVirtualGimbalEvent::MoveRight] = CVirtualGimbalEvent::MoveRight; - preset[CVirtualGimbalEvent::MoveUp] = CVirtualGimbalEvent::MoveUp; - preset[CVirtualGimbalEvent::MoveDown] = CVirtualGimbalEvent::MoveDown; - - return preset; - }(); }; } diff --git a/common/include/camera/CFPSCamera.hpp b/common/include/camera/CFPSCamera.hpp index 819c82bc7..754e5a1bf 100644 --- a/common/include/camera/CFPSCamera.hpp +++ b/common/include/camera/CFPSCamera.hpp @@ -9,10 +9,9 @@ #include "ICamera.hpp" -namespace nbl::hlsl // TODO: DIFFERENT NAMESPACE +namespace nbl::core { -// FPS Camera class CFPSCamera final : public ICamera { public: @@ -20,7 +19,7 @@ class CFPSCamera final : public ICamera static inline constexpr float HalfPi = 1.57079632679489661923f; static inline constexpr float RadToDeg = 57.2957795130823208768f; - CFPSCamera(const float64_t3& position, const glm::quat& orientation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f)) + CFPSCamera(const float64_t3& position, const camera_quaternion_t& orientation = makeIdentityQuaternion()) : base_t(), m_gimbal({ .position = position, .orientation = orientation }) { m_gimbal.begin(); @@ -31,31 +30,19 @@ class CFPSCamera final : public ICamera const float gForwardZ = static_cast(gForward.z); const float gPitch = std::atan2(std::hypot(gForwardX, gForwardZ), gForwardY) - HalfPi; const float gYaw = std::atan2(gForwardX, gForwardZ); - auto test = glm::quat(glm::vec3(gPitch, gYaw, 0.0f)); - - - m_gimbal.setOrientation(test); + m_gimbal.setOrientation(makeQuaternionFromEulerRadians(float64_t3(gPitch, gYaw, 0.0f))); } m_gimbal.end(); } ~CFPSCamera() = default; - const base_t::keyboard_to_virtual_events_t getKeyboardMappingPreset() const override { return m_keyboard_to_virtual_events_preset; } - const base_t::mouse_to_virtual_events_t getMouseMappingPreset() const override { return m_mouse_to_virtual_events_preset; } - const base_t::imguizmo_to_virtual_events_t getImguizmoMappingPreset() const override { return m_imguizmo_to_virtual_events_preset; } - const typename base_t::CGimbal& getGimbal() override { return m_gimbal; } - // rotation events IN RADIANS - virtual bool manipulate(std::span virtualEvents, const float64_t4x4* referenceFrame = nullptr) override { - // TODO: note, for FPS camera its assumed tilt is performed with respect to "world" up vector which is (0,1,0) - // but in reality its all about where -(gravity force) vector is, we can just add it and construct yaw quat with respect to this new custom vector instead - if (not virtualEvents.size() and not referenceFrame) return false; @@ -68,10 +55,10 @@ class CFPSCamera final : public ICamera if (referenceFrame) { const auto& q = reference.orientation; - const float w = static_cast(q.w); - const float x = static_cast(q.x); - const float y = static_cast(q.y); - const float z = static_cast(q.z); + const float w = static_cast(q.data.w); + const float x = static_cast(q.data.x); + const float y = static_cast(q.data.y); + const float z = static_cast(q.data.z); const float sinr_cosp = 2.f * (w * z + x * y); const float cosr_cosp = 1.f - 2.f * (y * y + z * z); const float roll = RadToDeg * std::atan2(sinr_cosp, cosr_cosp); @@ -98,8 +85,9 @@ class CFPSCamera final : public ICamera const float gYaw = std::atan2(rForwardX, rForwardZ); const float newPitch = std::clamp(rPitch + impulse.dVirtualRotation.x * getRotationSpeedScale(), MinVerticalAngle, MaxVerticalAngle), newYaw = gYaw + impulse.dVirtualRotation.y * getRotationSpeedScale(); - if(validateReference()) m_gimbal.setOrientation(glm::quat(glm::vec3(newPitch, newYaw, 0.0f))); - m_gimbal.setPosition(glm::vec3(reference.frame[3]) + reference.orientation * glm::vec3(impulse.dVirtualTranslate)); + if (validateReference()) + m_gimbal.setOrientation(makeQuaternionFromEulerRadians(float64_t3(newPitch, newYaw, 0.0f))); + m_gimbal.setPosition(float64_t3(reference.frame[3]) + rotateVectorByQuaternion(reference.orientation, float64_t3(impulse.dVirtualTranslate))); } m_gimbal.end(); @@ -111,7 +99,7 @@ class CFPSCamera final : public ICamera return manipulated; } - virtual const uint32_t getAllowedVirtualEvents() override + virtual uint32_t getAllowedVirtualEvents() const override { return AllowedVirtualEvents; } @@ -121,7 +109,7 @@ class CFPSCamera final : public ICamera return CameraKind::FPS; } - virtual const std::string_view getIdentifier() override + virtual std::string_view getIdentifier() const override { return "FPS Camera"; } @@ -132,52 +120,6 @@ class CFPSCamera final : public ICamera static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate | CVirtualGimbalEvent::Rotate; static inline constexpr float MaxVerticalAngle = 1.53588974175501f, MinVerticalAngle = -MaxVerticalAngle; - - static inline const auto m_keyboard_to_virtual_events_preset = []() - { - typename base_t::keyboard_to_virtual_events_t preset; - - preset[ui::E_KEY_CODE::EKC_W] = CVirtualGimbalEvent::MoveForward; - preset[ui::E_KEY_CODE::EKC_S] = CVirtualGimbalEvent::MoveBackward; - preset[ui::E_KEY_CODE::EKC_A] = CVirtualGimbalEvent::MoveLeft; - preset[ui::E_KEY_CODE::EKC_D] = CVirtualGimbalEvent::MoveRight; - preset[ui::E_KEY_CODE::EKC_I] = CVirtualGimbalEvent::TiltDown; - preset[ui::E_KEY_CODE::EKC_K] = CVirtualGimbalEvent::TiltUp; - preset[ui::E_KEY_CODE::EKC_J] = CVirtualGimbalEvent::PanLeft; - preset[ui::E_KEY_CODE::EKC_L] = CVirtualGimbalEvent::PanRight; - - return preset; - }(); - - static inline const auto m_mouse_to_virtual_events_preset = []() - { - typename base_t::mouse_to_virtual_events_t preset; - - preset[ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_X] = CVirtualGimbalEvent::PanRight; - preset[ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_X] = CVirtualGimbalEvent::PanLeft; - preset[ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_Y] = CVirtualGimbalEvent::TiltUp; - preset[ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_Y] = CVirtualGimbalEvent::TiltDown; - - return preset; - }(); - - static inline const auto m_imguizmo_to_virtual_events_preset = []() - { - typename base_t::imguizmo_to_virtual_events_t preset; - - preset[CVirtualGimbalEvent::MoveForward] = CVirtualGimbalEvent::MoveForward; - preset[CVirtualGimbalEvent::MoveBackward] = CVirtualGimbalEvent::MoveBackward; - preset[CVirtualGimbalEvent::MoveLeft] = CVirtualGimbalEvent::MoveLeft; - preset[CVirtualGimbalEvent::MoveRight] = CVirtualGimbalEvent::MoveRight; - preset[CVirtualGimbalEvent::MoveUp] = CVirtualGimbalEvent::MoveUp; - preset[CVirtualGimbalEvent::MoveDown] = CVirtualGimbalEvent::MoveDown; - preset[CVirtualGimbalEvent::TiltDown] = CVirtualGimbalEvent::TiltDown; - preset[CVirtualGimbalEvent::TiltUp] = CVirtualGimbalEvent::TiltUp; - preset[CVirtualGimbalEvent::PanLeft] = CVirtualGimbalEvent::PanLeft; - preset[CVirtualGimbalEvent::PanRight] = CVirtualGimbalEvent::PanRight; - - return preset; - }(); }; } diff --git a/common/include/camera/CFreeLockCamera.hpp b/common/include/camera/CFreeLockCamera.hpp index 468b015c2..ab2bc8bf9 100644 --- a/common/include/camera/CFreeLockCamera.hpp +++ b/common/include/camera/CFreeLockCamera.hpp @@ -7,7 +7,7 @@ #include "ICamera.hpp" -namespace nbl::hlsl // TODO: DIFFERENT NAMESPACE +namespace nbl::core { // Free Lock Camera class CFreeCamera final : public ICamera @@ -15,14 +15,10 @@ class CFreeCamera final : public ICamera public: using base_t = ICamera; - CFreeCamera(const float64_t3& position, const glm::quat& orientation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f)) + CFreeCamera(const float64_t3& position, const camera_quaternion_t& orientation = makeIdentityQuaternion()) : base_t(), m_gimbal({ .position = position, .orientation = orientation }) {} ~CFreeCamera() = default; - const base_t::keyboard_to_virtual_events_t getKeyboardMappingPreset() const override { return m_keyboard_to_virtual_events_preset; } - const base_t::mouse_to_virtual_events_t getMouseMappingPreset() const override { return m_mouse_to_virtual_events_preset; } - const base_t::imguizmo_to_virtual_events_t getImguizmoMappingPreset() const override { return m_imguizmo_to_virtual_events_preset; } - const typename base_t::CGimbal& getGimbal() override { return m_gimbal; @@ -43,12 +39,12 @@ class CFreeCamera final : public ICamera m_gimbal.begin(); { - glm::quat pitch = glm::angleAxis(impulse.dVirtualRotation.x, glm::vec3(reference.frame[0])); - glm::quat yaw = glm::angleAxis(impulse.dVirtualRotation.y, glm::vec3(reference.frame[1])); - glm::quat roll = glm::angleAxis(impulse.dVirtualRotation.z, glm::vec3(reference.frame[2])); + const auto pitch = makeQuaternionFromAxisAngle(normalize(float64_t3(reference.frame[0])), impulse.dVirtualRotation.x); + const auto yaw = makeQuaternionFromAxisAngle(normalize(float64_t3(reference.frame[1])), impulse.dVirtualRotation.y); + const auto roll = makeQuaternionFromAxisAngle(normalize(float64_t3(reference.frame[2])), impulse.dVirtualRotation.z); - m_gimbal.setOrientation(yaw * pitch * roll * reference.orientation); - m_gimbal.setPosition(glm::vec3(reference.frame[3]) + reference.orientation * glm::vec3(impulse.dVirtualTranslate)); + m_gimbal.setOrientation(normalizeQuaternion(yaw * pitch * roll * reference.orientation)); + m_gimbal.setPosition(float64_t3(reference.frame[3]) + rotateVectorByQuaternion(reference.orientation, float64_t3(impulse.dVirtualTranslate))); } m_gimbal.end(); @@ -60,7 +56,7 @@ class CFreeCamera final : public ICamera return manipulated; } - virtual const uint32_t getAllowedVirtualEvents() override + virtual uint32_t getAllowedVirtualEvents() const override { return AllowedVirtualEvents; } @@ -70,7 +66,7 @@ class CFreeCamera final : public ICamera return CameraKind::Free; } - virtual const std::string_view getIdentifier() override + virtual std::string_view getIdentifier() const override { return "Free-Look Camera"; } @@ -79,56 +75,6 @@ class CFreeCamera final : public ICamera typename base_t::CGimbal m_gimbal; static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate | CVirtualGimbalEvent::Rotate; - - static inline const auto m_keyboard_to_virtual_events_preset = []() - { - typename base_t::keyboard_to_virtual_events_t preset; - - preset[ui::E_KEY_CODE::EKC_W] = CVirtualGimbalEvent::MoveForward; - preset[ui::E_KEY_CODE::EKC_S] = CVirtualGimbalEvent::MoveBackward; - preset[ui::E_KEY_CODE::EKC_A] = CVirtualGimbalEvent::MoveLeft; - preset[ui::E_KEY_CODE::EKC_D] = CVirtualGimbalEvent::MoveRight; - preset[ui::E_KEY_CODE::EKC_I] = CVirtualGimbalEvent::TiltDown; - preset[ui::E_KEY_CODE::EKC_K] = CVirtualGimbalEvent::TiltUp; - preset[ui::E_KEY_CODE::EKC_J] = CVirtualGimbalEvent::PanLeft; - preset[ui::E_KEY_CODE::EKC_L] = CVirtualGimbalEvent::PanRight; - preset[ui::E_KEY_CODE::EKC_Q] = CVirtualGimbalEvent::RollLeft; - preset[ui::E_KEY_CODE::EKC_E] = CVirtualGimbalEvent::RollRight; - - return preset; - }(); - - static inline const auto m_mouse_to_virtual_events_preset = []() - { - typename base_t::mouse_to_virtual_events_t preset; - - preset[ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_X] = CVirtualGimbalEvent::PanRight; - preset[ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_X] = CVirtualGimbalEvent::PanLeft; - preset[ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_Y] = CVirtualGimbalEvent::TiltUp; - preset[ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_Y] = CVirtualGimbalEvent::TiltDown; - - return preset; - }(); - - static inline const auto m_imguizmo_to_virtual_events_preset = []() - { - typename base_t::imguizmo_to_virtual_events_t preset; - - preset[CVirtualGimbalEvent::MoveForward] = CVirtualGimbalEvent::MoveForward; - preset[CVirtualGimbalEvent::MoveBackward] = CVirtualGimbalEvent::MoveBackward; - preset[CVirtualGimbalEvent::MoveLeft] = CVirtualGimbalEvent::MoveLeft; - preset[CVirtualGimbalEvent::MoveRight] = CVirtualGimbalEvent::MoveRight; - preset[CVirtualGimbalEvent::MoveUp] = CVirtualGimbalEvent::MoveUp; - preset[CVirtualGimbalEvent::MoveDown] = CVirtualGimbalEvent::MoveDown; - preset[CVirtualGimbalEvent::TiltDown] = CVirtualGimbalEvent::TiltDown; - preset[CVirtualGimbalEvent::TiltUp] = CVirtualGimbalEvent::TiltUp; - preset[CVirtualGimbalEvent::PanLeft] = CVirtualGimbalEvent::PanLeft; - preset[CVirtualGimbalEvent::PanRight] = CVirtualGimbalEvent::PanRight; - preset[CVirtualGimbalEvent::RollLeft] = CVirtualGimbalEvent::RollLeft; - preset[CVirtualGimbalEvent::RollRight] = CVirtualGimbalEvent::RollRight; - - return preset; - }(); }; } diff --git a/common/include/camera/CGeneralPurposeGimbal.hpp b/common/include/camera/CGeneralPurposeGimbal.hpp index 7e1c07096..422d4a326 100644 --- a/common/include/camera/CGeneralPurposeGimbal.hpp +++ b/common/include/camera/CGeneralPurposeGimbal.hpp @@ -3,8 +3,7 @@ #include "IGimbal.hpp" -// TODO: DIFFERENT NAMESPACE -namespace nbl::hlsl +namespace nbl::core { template class CGeneralPurposeGimbal : public IGimbal @@ -17,4 +16,4 @@ namespace nbl::hlsl }; } -#endif // _NBL_IGIMBAL_HPP_ \ No newline at end of file +#endif // _NBL_CGENERAL_PURPOSE_GIMBAL_HPP_ diff --git a/common/include/camera/CGimbalInputBinder.hpp b/common/include/camera/CGimbalInputBinder.hpp index 1ae245b00..3473d3669 100644 --- a/common/include/camera/CGimbalInputBinder.hpp +++ b/common/include/camera/CGimbalInputBinder.hpp @@ -5,11 +5,11 @@ #include "IGimbalInputProcessor.hpp" -namespace nbl::hlsl +namespace nbl::ui { /** -* High-level runtime binder for examples and viewport glue. +* High-level runtime binder for consumers and viewport glue. * * It owns active runtime mappings and collects one frame of virtual events * from raw keyboard, mouse, and ImGuizmo input. @@ -19,6 +19,9 @@ class CGimbalInputBinder final : public IGimbalInputProcessor public: using base_t = IGimbalInputProcessor; using base_t::base_t; + using input_keyboard_event_t = base_t::input_keyboard_event_t; + using input_mouse_event_t = base_t::input_mouse_event_t; + using input_imguizmo_event_t = base_t::input_imguizmo_event_t; struct SCollectedVirtualEvents { @@ -59,18 +62,6 @@ class CGimbalInputBinder final : public IGimbalInputProcessor copyActiveBindingsFromLayout(layout); } - inline void copyDefaultBindingsFromLayout(const IGimbalBindingLayout& layout) - { - updateKeyboardMapping([&](auto& map) { map = sanitizeMapping(layout.getKeyboardMappingPreset()); }); - updateMouseMapping([&](auto& map) { map = sanitizeMapping(layout.getMouseMappingPreset()); }); - updateImguizmoMapping([&](auto& map) { map = sanitizeMapping(layout.getImguizmoMappingPreset()); }); - } - - inline void copyPresetLayoutFrom(const IGimbalBindingLayout& layout) - { - copyDefaultBindingsFromLayout(layout); - } - inline void copyActiveBindingsToLayout(IGimbalBindingLayout& layout) const { layout.updateKeyboardMapping([&](auto& map) { map = sanitizeMapping(getKeyboardVirtualEventMap()); }); @@ -137,6 +128,6 @@ class CGimbalInputBinder final : public IGimbalInputProcessor } }; -} // namespace nbl::hlsl +} // namespace nbl::ui #endif // _NBL_C_GIMBAL_INPUT_BINDER_HPP_ diff --git a/common/include/camera/CIsometricCamera.hpp b/common/include/camera/CIsometricCamera.hpp index d92b6c875..0a19121fa 100644 --- a/common/include/camera/CIsometricCamera.hpp +++ b/common/include/camera/CIsometricCamera.hpp @@ -6,7 +6,7 @@ #include "CSphericalTargetCamera.hpp" -namespace nbl::hlsl +namespace nbl::core { class CIsometricCamera final : public CSphericalTargetCamera @@ -23,10 +23,6 @@ class CIsometricCamera final : public CSphericalTargetCamera } ~CIsometricCamera() = default; - const base_t::keyboard_to_virtual_events_t getKeyboardMappingPreset() const override { return m_keyboard_to_virtual_events_preset; } - const base_t::mouse_to_virtual_events_t getMouseMappingPreset() const override { return m_mouse_to_virtual_events_preset; } - const base_t::imguizmo_to_virtual_events_t getImguizmoMappingPreset() const override { return m_imguizmo_to_virtual_events_preset; } - const typename base_t::CGimbal& getGimbal() override { return m_gimbal; } virtual bool manipulate(std::span virtualEvents, const float64_t4x4* referenceFrame = nullptr) override @@ -53,58 +49,14 @@ class CIsometricCamera final : public CSphericalTargetCamera return applyPose(); } - virtual const uint32_t getAllowedVirtualEvents() override { return AllowedVirtualEvents; } + virtual uint32_t getAllowedVirtualEvents() const override { return AllowedVirtualEvents; } virtual CameraKind getKind() const override { return CameraKind::Isometric; } - virtual const std::string_view getIdentifier() override { return "Isometric Camera"; } + virtual std::string_view getIdentifier() const override { return "Isometric Camera"; } private: static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate; static inline constexpr double IsoYaw = 0.7853981633974483; static inline constexpr double IsoPitch = 0.6154797086703873; - - static inline const auto m_keyboard_to_virtual_events_preset = []() - { - typename base_t::keyboard_to_virtual_events_t preset; - - preset[ui::E_KEY_CODE::EKC_W] = CVirtualGimbalEvent::MoveUp; - preset[ui::E_KEY_CODE::EKC_S] = CVirtualGimbalEvent::MoveDown; - preset[ui::E_KEY_CODE::EKC_A] = CVirtualGimbalEvent::MoveLeft; - preset[ui::E_KEY_CODE::EKC_D] = CVirtualGimbalEvent::MoveRight; - preset[ui::E_KEY_CODE::EKC_Q] = CVirtualGimbalEvent::MoveBackward; - preset[ui::E_KEY_CODE::EKC_E] = CVirtualGimbalEvent::MoveForward; - - return preset; - }(); - - static inline const auto m_mouse_to_virtual_events_preset = []() - { - typename base_t::mouse_to_virtual_events_t preset; - - preset[ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_X] = CVirtualGimbalEvent::MoveRight; - preset[ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_X] = CVirtualGimbalEvent::MoveLeft; - preset[ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_Y] = CVirtualGimbalEvent::MoveUp; - preset[ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_Y] = CVirtualGimbalEvent::MoveDown; - preset[ui::E_MOUSE_CODE::EMC_VERTICAL_POSITIVE_SCROLL] = CVirtualGimbalEvent::MoveForward; - preset[ui::E_MOUSE_CODE::EMC_HORIZONTAL_POSITIVE_SCROLL] = CVirtualGimbalEvent::MoveForward; - preset[ui::E_MOUSE_CODE::EMC_VERTICAL_NEGATIVE_SCROLL] = CVirtualGimbalEvent::MoveBackward; - preset[ui::E_MOUSE_CODE::EMC_HORIZONTAL_NEGATIVE_SCROLL] = CVirtualGimbalEvent::MoveBackward; - - return preset; - }(); - - static inline const auto m_imguizmo_to_virtual_events_preset = []() - { - typename base_t::imguizmo_to_virtual_events_t preset; - - preset[CVirtualGimbalEvent::MoveForward] = CVirtualGimbalEvent::MoveForward; - preset[CVirtualGimbalEvent::MoveBackward] = CVirtualGimbalEvent::MoveBackward; - preset[CVirtualGimbalEvent::MoveLeft] = CVirtualGimbalEvent::MoveLeft; - preset[CVirtualGimbalEvent::MoveRight] = CVirtualGimbalEvent::MoveRight; - preset[CVirtualGimbalEvent::MoveUp] = CVirtualGimbalEvent::MoveUp; - preset[CVirtualGimbalEvent::MoveDown] = CVirtualGimbalEvent::MoveDown; - - return preset; - }(); }; } diff --git a/common/include/camera/CLinearProjection.hpp b/common/include/camera/CLinearProjection.hpp index 115f0906c..40e9813dd 100644 --- a/common/include/camera/CLinearProjection.hpp +++ b/common/include/camera/CLinearProjection.hpp @@ -4,7 +4,7 @@ #include "ILinearProjection.hpp" #include "IRange.hpp" -namespace nbl::hlsl +namespace nbl::core { template ProjectionsRange> class CLinearProjection : public ILinearProjection diff --git a/common/include/camera/COrbitCamera.hpp b/common/include/camera/COrbitCamera.hpp index 1c754b16b..2ab8b1d12 100644 --- a/common/include/camera/COrbitCamera.hpp +++ b/common/include/camera/COrbitCamera.hpp @@ -5,7 +5,7 @@ #include #include "CSphericalTargetCamera.hpp" -namespace nbl::hlsl +namespace nbl::core { class COrbitCamera final : public CSphericalTargetCamera @@ -21,33 +21,26 @@ class COrbitCamera final : public CSphericalTargetCamera } ~COrbitCamera() = default; - const base_t::keyboard_to_virtual_events_t getKeyboardMappingPreset() const override { return m_keyboard_to_virtual_events_preset; } - const base_t::mouse_to_virtual_events_t getMouseMappingPreset() const override { return m_mouse_to_virtual_events_preset; } - const base_t::imguizmo_to_virtual_events_t getImguizmoMappingPreset() const override { return m_imguizmo_to_virtual_events_preset; } - const typename base_t::CGimbal& getGimbal() override { return m_gimbal; } virtual bool manipulate(std::span virtualEvents, const float64_t4x4* referenceFrame = nullptr) override { - // TODO: it must work differently, we should take another gimbal to control target - auto impulse = m_gimbal.accumulate(virtualEvents); double deltaU = impulse.dVirtualTranslate.y, deltaV = impulse.dVirtualTranslate.x, deltaDistance = impulse.dVirtualTranslate.z; - // TODO! - constexpr auto nastyScalar = 0.01; - deltaU *= nastyScalar * getMoveSpeedScale(); - deltaV *= nastyScalar * getMoveSpeedScale(); + constexpr auto orbitMotionScalar = 0.01; + deltaU *= orbitMotionScalar * getMoveSpeedScale(); + deltaV *= orbitMotionScalar * getMoveSpeedScale(); m_u += deltaU; m_v += deltaV; - m_distance = std::clamp(m_distance += deltaDistance * nastyScalar, MinDistance, MaxDistance); + m_distance = std::clamp(m_distance += deltaDistance * orbitMotionScalar, MinDistance, MaxDistance); return applyPose(); } - virtual const uint32_t getAllowedVirtualEvents() override + virtual uint32_t getAllowedVirtualEvents() const override { return AllowedVirtualEvents; } @@ -57,7 +50,7 @@ class COrbitCamera final : public CSphericalTargetCamera return CameraKind::Orbit; } - virtual const std::string_view getIdentifier() override + virtual std::string_view getIdentifier() const override { return "Orbit Camera"; } @@ -66,50 +59,6 @@ class COrbitCamera final : public CSphericalTargetCamera static inline constexpr float MaxDistance = base_t::MaxDistance; static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate; - - static inline const auto m_keyboard_to_virtual_events_preset = []() - { - typename base_t::keyboard_to_virtual_events_t preset; - - preset[ui::E_KEY_CODE::EKC_W] = CVirtualGimbalEvent::MoveUp; - preset[ui::E_KEY_CODE::EKC_S] = CVirtualGimbalEvent::MoveDown; - preset[ui::E_KEY_CODE::EKC_A] = CVirtualGimbalEvent::MoveLeft; - preset[ui::E_KEY_CODE::EKC_D] = CVirtualGimbalEvent::MoveRight; - preset[ui::E_KEY_CODE::EKC_E] = CVirtualGimbalEvent::MoveForward; - preset[ui::E_KEY_CODE::EKC_Q] = CVirtualGimbalEvent::MoveBackward; - - return preset; - }(); - - static inline const auto m_mouse_to_virtual_events_preset = []() - { - typename base_t::mouse_to_virtual_events_t preset; - - preset[ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_X] = CVirtualGimbalEvent::MoveRight; - preset[ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_X] = CVirtualGimbalEvent::MoveLeft; - preset[ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_Y] = CVirtualGimbalEvent::MoveUp; - preset[ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_Y] = CVirtualGimbalEvent::MoveDown; - preset[ui::E_MOUSE_CODE::EMC_VERTICAL_POSITIVE_SCROLL] = CVirtualGimbalEvent::MoveForward; - preset[ui::E_MOUSE_CODE::EMC_HORIZONTAL_POSITIVE_SCROLL] = CVirtualGimbalEvent::MoveForward; - preset[ui::E_MOUSE_CODE::EMC_VERTICAL_NEGATIVE_SCROLL] = CVirtualGimbalEvent::MoveBackward; - preset[ui::E_MOUSE_CODE::EMC_HORIZONTAL_NEGATIVE_SCROLL] = CVirtualGimbalEvent::MoveBackward; - - return preset; - }(); - - static inline const auto m_imguizmo_to_virtual_events_preset = []() - { - typename base_t::imguizmo_to_virtual_events_t preset; - - preset[CVirtualGimbalEvent::MoveForward] = CVirtualGimbalEvent::MoveForward; - preset[CVirtualGimbalEvent::MoveBackward] = CVirtualGimbalEvent::MoveBackward; - preset[CVirtualGimbalEvent::MoveLeft] = CVirtualGimbalEvent::MoveLeft; - preset[CVirtualGimbalEvent::MoveRight] = CVirtualGimbalEvent::MoveRight; - preset[CVirtualGimbalEvent::MoveUp] = CVirtualGimbalEvent::MoveUp; - preset[CVirtualGimbalEvent::MoveDown] = CVirtualGimbalEvent::MoveDown; - - return preset; - }(); }; } diff --git a/common/include/camera/CPathCamera.hpp b/common/include/camera/CPathCamera.hpp index 2ab5c1cef..3f12ca923 100644 --- a/common/include/camera/CPathCamera.hpp +++ b/common/include/camera/CPathCamera.hpp @@ -6,7 +6,7 @@ #include "CSphericalTargetCamera.hpp" -namespace nbl::hlsl +namespace nbl::core { class CPathCamera final : public CSphericalTargetCamera @@ -27,10 +27,6 @@ class CPathCamera final : public CSphericalTargetCamera } ~CPathCamera() = default; - const base_t::keyboard_to_virtual_events_t getKeyboardMappingPreset() const override { return m_keyboard_to_virtual_events_preset; } - const base_t::mouse_to_virtual_events_t getMouseMappingPreset() const override { return m_mouse_to_virtual_events_preset; } - const base_t::imguizmo_to_virtual_events_t getImguizmoMappingPreset() const override { return m_imguizmo_to_virtual_events_preset; } - const typename base_t::CGimbal& getGimbal() override { return m_gimbal; } virtual bool manipulate(std::span virtualEvents, const float64_t4x4* referenceFrame = nullptr) override @@ -50,7 +46,7 @@ class CPathCamera final : public CSphericalTargetCamera return updateFromPath(); } - virtual const uint32_t getAllowedVirtualEvents() override { return AllowedVirtualEvents; } + virtual uint32_t getAllowedVirtualEvents() const override { return AllowedVirtualEvents; } virtual CameraKind getKind() const override { return CameraKind::Path; } virtual uint32_t getGoalStateMask() const override { return base_t::getGoalStateMask() | base_t::GoalStatePath; } virtual bool tryGetPathState(PathState& out) const override @@ -95,7 +91,7 @@ class CPathCamera final : public CSphericalTargetCamera const double appliedDistance = std::sqrt(m_pathRadius * m_pathRadius + m_pathHeight * m_pathHeight); return inRange && std::abs(appliedDistance - static_cast(clamped)) <= 1e-6; } - virtual const std::string_view getIdentifier() override { return "Path Camera"; } + virtual std::string_view getIdentifier() const override { return "Path Camera"; } private: static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate; @@ -113,50 +109,6 @@ class CPathCamera final : public CSphericalTargetCamera initFromPosition(position); return applyPose(); } - - static inline const auto m_keyboard_to_virtual_events_preset = []() - { - typename base_t::keyboard_to_virtual_events_t preset; - - preset[ui::E_KEY_CODE::EKC_W] = CVirtualGimbalEvent::MoveForward; - preset[ui::E_KEY_CODE::EKC_S] = CVirtualGimbalEvent::MoveBackward; - preset[ui::E_KEY_CODE::EKC_A] = CVirtualGimbalEvent::MoveLeft; - preset[ui::E_KEY_CODE::EKC_D] = CVirtualGimbalEvent::MoveRight; - preset[ui::E_KEY_CODE::EKC_Q] = CVirtualGimbalEvent::MoveDown; - preset[ui::E_KEY_CODE::EKC_E] = CVirtualGimbalEvent::MoveUp; - - return preset; - }(); - - static inline const auto m_mouse_to_virtual_events_preset = []() - { - typename base_t::mouse_to_virtual_events_t preset; - - preset[ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_X] = CVirtualGimbalEvent::MoveRight; - preset[ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_X] = CVirtualGimbalEvent::MoveLeft; - preset[ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_Y] = CVirtualGimbalEvent::MoveUp; - preset[ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_Y] = CVirtualGimbalEvent::MoveDown; - preset[ui::E_MOUSE_CODE::EMC_VERTICAL_POSITIVE_SCROLL] = CVirtualGimbalEvent::MoveForward; - preset[ui::E_MOUSE_CODE::EMC_HORIZONTAL_POSITIVE_SCROLL] = CVirtualGimbalEvent::MoveForward; - preset[ui::E_MOUSE_CODE::EMC_VERTICAL_NEGATIVE_SCROLL] = CVirtualGimbalEvent::MoveBackward; - preset[ui::E_MOUSE_CODE::EMC_HORIZONTAL_NEGATIVE_SCROLL] = CVirtualGimbalEvent::MoveBackward; - - return preset; - }(); - - static inline const auto m_imguizmo_to_virtual_events_preset = []() - { - typename base_t::imguizmo_to_virtual_events_t preset; - - preset[CVirtualGimbalEvent::MoveForward] = CVirtualGimbalEvent::MoveForward; - preset[CVirtualGimbalEvent::MoveBackward] = CVirtualGimbalEvent::MoveBackward; - preset[CVirtualGimbalEvent::MoveLeft] = CVirtualGimbalEvent::MoveLeft; - preset[CVirtualGimbalEvent::MoveRight] = CVirtualGimbalEvent::MoveRight; - preset[CVirtualGimbalEvent::MoveUp] = CVirtualGimbalEvent::MoveUp; - preset[CVirtualGimbalEvent::MoveDown] = CVirtualGimbalEvent::MoveDown; - - return preset; - }(); }; } diff --git a/common/include/camera/CPlanarProjection.hpp b/common/include/camera/CPlanarProjection.hpp index 4d559a410..7a5255363 100644 --- a/common/include/camera/CPlanarProjection.hpp +++ b/common/include/camera/CPlanarProjection.hpp @@ -4,7 +4,7 @@ #include "IPlanarProjection.hpp" #include "IRange.hpp" -namespace nbl::hlsl +namespace nbl::core { template ProjectionsRange> class CPlanarProjection : public IPlanarProjection diff --git a/common/include/camera/CSphericalTargetCamera.hpp b/common/include/camera/CSphericalTargetCamera.hpp index 5e85d8c05..c74f8e5dd 100644 --- a/common/include/camera/CSphericalTargetCamera.hpp +++ b/common/include/camera/CSphericalTargetCamera.hpp @@ -6,7 +6,7 @@ #include "ICamera.hpp" -namespace nbl::hlsl +namespace nbl::core { /** @@ -21,7 +21,7 @@ class CSphericalTargetCamera : public ICamera CSphericalTargetCamera(const float64_t3& position, const float64_t3& target) : base_t(), m_targetPosition(target), m_distance(1.0f), - m_gimbal({ .position = position, .orientation = glm::quat(glm::vec3(0.0f)) }) + m_gimbal({ .position = position, .orientation = makeIdentityQuaternion() }) { initFromPosition(position); } @@ -51,8 +51,8 @@ class CSphericalTargetCamera : public ICamera inline double getU() const { return m_u; } inline double getV() const { return m_v; } - static inline constexpr float MinDistance = 0.1f; - static inline constexpr float MaxDistance = 10000.f; + static inline constexpr float MinDistance = base_t::SphericalMinDistance; + static inline constexpr float MaxDistance = base_t::SphericalMaxDistance; virtual uint32_t getCapabilities() const override { @@ -135,7 +135,7 @@ class CSphericalTargetCamera : public ICamera { const auto basis = computeBasis(m_u, m_v, m_distance); const auto newPosition = basis.localSpherePosition + m_targetPosition; - const auto newOrientation = glm::quat_cast(glm::dmat3{ basis.right, basis.up, basis.forward }); + const auto newOrientation = makeQuaternionFromBasis(basis.right, basis.up, basis.forward); m_gimbal.begin(); { diff --git a/common/include/camera/CTopDownCamera.hpp b/common/include/camera/CTopDownCamera.hpp index ff6e68071..011fab01b 100644 --- a/common/include/camera/CTopDownCamera.hpp +++ b/common/include/camera/CTopDownCamera.hpp @@ -6,7 +6,7 @@ #include "CSphericalTargetCamera.hpp" -namespace nbl::hlsl +namespace nbl::core { class CTopDownCamera final : public CSphericalTargetCamera @@ -22,10 +22,6 @@ class CTopDownCamera final : public CSphericalTargetCamera } ~CTopDownCamera() = default; - const base_t::keyboard_to_virtual_events_t getKeyboardMappingPreset() const override { return m_keyboard_to_virtual_events_preset; } - const base_t::mouse_to_virtual_events_t getMouseMappingPreset() const override { return m_mouse_to_virtual_events_preset; } - const base_t::imguizmo_to_virtual_events_t getImguizmoMappingPreset() const override { return m_imguizmo_to_virtual_events_preset; } - const typename base_t::CGimbal& getGimbal() override { return m_gimbal; } virtual bool manipulate(std::span virtualEvents, const float64_t4x4* referenceFrame = nullptr) override @@ -53,61 +49,13 @@ class CTopDownCamera final : public CSphericalTargetCamera return applyPose(); } - virtual const uint32_t getAllowedVirtualEvents() override { return AllowedVirtualEvents; } + virtual uint32_t getAllowedVirtualEvents() const override { return AllowedVirtualEvents; } virtual CameraKind getKind() const override { return CameraKind::TopDown; } - virtual const std::string_view getIdentifier() override { return "Top-Down Camera"; } + virtual std::string_view getIdentifier() const override { return "Top-Down Camera"; } private: static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate | CVirtualGimbalEvent::Rotate; static inline constexpr double TopDownPitch = -1.5707963267948966; - - static inline const auto m_keyboard_to_virtual_events_preset = []() - { - typename base_t::keyboard_to_virtual_events_t preset; - - preset[ui::E_KEY_CODE::EKC_W] = CVirtualGimbalEvent::MoveUp; - preset[ui::E_KEY_CODE::EKC_S] = CVirtualGimbalEvent::MoveDown; - preset[ui::E_KEY_CODE::EKC_A] = CVirtualGimbalEvent::MoveLeft; - preset[ui::E_KEY_CODE::EKC_D] = CVirtualGimbalEvent::MoveRight; - preset[ui::E_KEY_CODE::EKC_Q] = CVirtualGimbalEvent::MoveBackward; - preset[ui::E_KEY_CODE::EKC_E] = CVirtualGimbalEvent::MoveForward; - preset[ui::E_KEY_CODE::EKC_J] = CVirtualGimbalEvent::PanLeft; - preset[ui::E_KEY_CODE::EKC_L] = CVirtualGimbalEvent::PanRight; - - return preset; - }(); - - static inline const auto m_mouse_to_virtual_events_preset = []() - { - typename base_t::mouse_to_virtual_events_t preset; - - preset[ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_X] = CVirtualGimbalEvent::PanRight; - preset[ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_X] = CVirtualGimbalEvent::PanLeft; - preset[ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_Y] = CVirtualGimbalEvent::MoveUp; - preset[ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_Y] = CVirtualGimbalEvent::MoveDown; - preset[ui::E_MOUSE_CODE::EMC_VERTICAL_POSITIVE_SCROLL] = CVirtualGimbalEvent::MoveForward; - preset[ui::E_MOUSE_CODE::EMC_HORIZONTAL_POSITIVE_SCROLL] = CVirtualGimbalEvent::MoveForward; - preset[ui::E_MOUSE_CODE::EMC_VERTICAL_NEGATIVE_SCROLL] = CVirtualGimbalEvent::MoveBackward; - preset[ui::E_MOUSE_CODE::EMC_HORIZONTAL_NEGATIVE_SCROLL] = CVirtualGimbalEvent::MoveBackward; - - return preset; - }(); - - static inline const auto m_imguizmo_to_virtual_events_preset = []() - { - typename base_t::imguizmo_to_virtual_events_t preset; - - preset[CVirtualGimbalEvent::MoveForward] = CVirtualGimbalEvent::MoveForward; - preset[CVirtualGimbalEvent::MoveBackward] = CVirtualGimbalEvent::MoveBackward; - preset[CVirtualGimbalEvent::MoveLeft] = CVirtualGimbalEvent::MoveLeft; - preset[CVirtualGimbalEvent::MoveRight] = CVirtualGimbalEvent::MoveRight; - preset[CVirtualGimbalEvent::MoveUp] = CVirtualGimbalEvent::MoveUp; - preset[CVirtualGimbalEvent::MoveDown] = CVirtualGimbalEvent::MoveDown; - preset[CVirtualGimbalEvent::PanLeft] = CVirtualGimbalEvent::PanLeft; - preset[CVirtualGimbalEvent::PanRight] = CVirtualGimbalEvent::PanRight; - - return preset; - }(); }; } diff --git a/common/include/camera/CTurntableCamera.hpp b/common/include/camera/CTurntableCamera.hpp index d6e863dcd..b340bf85a 100644 --- a/common/include/camera/CTurntableCamera.hpp +++ b/common/include/camera/CTurntableCamera.hpp @@ -10,7 +10,7 @@ #include "CSphericalTargetCamera.hpp" -namespace nbl::hlsl +namespace nbl::core { class CTurntableCamera final : public CSphericalTargetCamera @@ -26,10 +26,6 @@ class CTurntableCamera final : public CSphericalTargetCamera } ~CTurntableCamera() = default; - const base_t::keyboard_to_virtual_events_t getKeyboardMappingPreset() const override { return m_keyboard_to_virtual_events_preset; } - const base_t::mouse_to_virtual_events_t getMouseMappingPreset() const override { return m_mouse_to_virtual_events_preset; } - const base_t::imguizmo_to_virtual_events_t getImguizmoMappingPreset() const override { return m_imguizmo_to_virtual_events_preset; } - const typename base_t::CGimbal& getGimbal() override { return m_gimbal; } virtual bool manipulate(std::span virtualEvents, const float64_t4x4* referenceFrame = nullptr) override @@ -52,9 +48,9 @@ class CTurntableCamera final : public CSphericalTargetCamera return applyPose(); } - virtual const uint32_t getAllowedVirtualEvents() override { return AllowedVirtualEvents; } + virtual uint32_t getAllowedVirtualEvents() const override { return AllowedVirtualEvents; } virtual CameraKind getKind() const override { return CameraKind::Turntable; } - virtual const std::string_view getIdentifier() override { return "Turntable Camera"; } + virtual std::string_view getIdentifier() const override { return "Turntable Camera"; } static inline constexpr float MinDistance = base_t::MinDistance; static inline constexpr float MaxDistance = base_t::MaxDistance; @@ -64,56 +60,6 @@ class CTurntableCamera final : public CSphericalTargetCamera static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate | CVirtualGimbalEvent::Rotate; static inline constexpr double MaxPitch = 1.5533430342749532; static inline constexpr double MinPitch = -MaxPitch; - - static inline const auto m_keyboard_to_virtual_events_preset = []() - { - typename base_t::keyboard_to_virtual_events_t preset; - - preset[ui::E_KEY_CODE::EKC_W] = CVirtualGimbalEvent::MoveForward; - preset[ui::E_KEY_CODE::EKC_S] = CVirtualGimbalEvent::MoveBackward; - preset[ui::E_KEY_CODE::EKC_A] = CVirtualGimbalEvent::PanLeft; - preset[ui::E_KEY_CODE::EKC_D] = CVirtualGimbalEvent::PanRight; - preset[ui::E_KEY_CODE::EKC_I] = CVirtualGimbalEvent::TiltDown; - preset[ui::E_KEY_CODE::EKC_K] = CVirtualGimbalEvent::TiltUp; - preset[ui::E_KEY_CODE::EKC_J] = CVirtualGimbalEvent::PanLeft; - preset[ui::E_KEY_CODE::EKC_L] = CVirtualGimbalEvent::PanRight; - - return preset; - }(); - - static inline const auto m_mouse_to_virtual_events_preset = []() - { - typename base_t::mouse_to_virtual_events_t preset; - - preset[ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_X] = CVirtualGimbalEvent::PanRight; - preset[ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_X] = CVirtualGimbalEvent::PanLeft; - preset[ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_Y] = CVirtualGimbalEvent::TiltUp; - preset[ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_Y] = CVirtualGimbalEvent::TiltDown; - preset[ui::E_MOUSE_CODE::EMC_VERTICAL_POSITIVE_SCROLL] = CVirtualGimbalEvent::MoveForward; - preset[ui::E_MOUSE_CODE::EMC_HORIZONTAL_POSITIVE_SCROLL] = CVirtualGimbalEvent::MoveForward; - preset[ui::E_MOUSE_CODE::EMC_VERTICAL_NEGATIVE_SCROLL] = CVirtualGimbalEvent::MoveBackward; - preset[ui::E_MOUSE_CODE::EMC_HORIZONTAL_NEGATIVE_SCROLL] = CVirtualGimbalEvent::MoveBackward; - - return preset; - }(); - - static inline const auto m_imguizmo_to_virtual_events_preset = []() - { - typename base_t::imguizmo_to_virtual_events_t preset; - - preset[CVirtualGimbalEvent::MoveForward] = CVirtualGimbalEvent::MoveForward; - preset[CVirtualGimbalEvent::MoveBackward] = CVirtualGimbalEvent::MoveBackward; - preset[CVirtualGimbalEvent::MoveLeft] = CVirtualGimbalEvent::MoveLeft; - preset[CVirtualGimbalEvent::MoveRight] = CVirtualGimbalEvent::MoveRight; - preset[CVirtualGimbalEvent::MoveUp] = CVirtualGimbalEvent::MoveUp; - preset[CVirtualGimbalEvent::MoveDown] = CVirtualGimbalEvent::MoveDown; - preset[CVirtualGimbalEvent::TiltDown] = CVirtualGimbalEvent::TiltDown; - preset[CVirtualGimbalEvent::TiltUp] = CVirtualGimbalEvent::TiltUp; - preset[CVirtualGimbalEvent::PanLeft] = CVirtualGimbalEvent::PanLeft; - preset[CVirtualGimbalEvent::PanRight] = CVirtualGimbalEvent::PanRight; - - return preset; - }(); }; } diff --git a/common/include/camera/ICamera.hpp b/common/include/camera/ICamera.hpp index 3c4b9b180..1a613e411 100644 --- a/common/include/camera/ICamera.hpp +++ b/common/include/camera/ICamera.hpp @@ -8,13 +8,13 @@ #include #include -#include "camera/IGimbalBindingLayout.hpp" +#include "IGimbal.hpp" -namespace nbl::hlsl // TODO: DIFFERENT NAMESPACE +namespace nbl::core { /** -* Shared camera contract used by examples. +* Shared camera contract. * * The hot runtime path is event-only: cameras consume `CVirtualGimbalEvent` * streams through `manipulate(...)`. Optional typed state hooks exist only for @@ -23,17 +23,8 @@ namespace nbl::hlsl // TODO: DIFFERENT NAMESPACE class ICamera : virtual public core::IReferenceCounted { public: - using binding_layout_t = IGimbalBindingLayout; - using gimbal_event_t = binding_layout_t::gimbal_event_t; - using encode_keyboard_code_t = binding_layout_t::encode_keyboard_code_t; - using encode_mouse_code_t = binding_layout_t::encode_mouse_code_t; - using encode_imguizmo_code_t = binding_layout_t::encode_imguizmo_code_t; - using BindingDomain = binding_layout_t::BindingDomain; - using CKeyInfo = binding_layout_t::CKeyInfo; - using CHashInfo = binding_layout_t::CHashInfo; - using keyboard_to_virtual_events_t = binding_layout_t::keyboard_to_virtual_events_t; - using mouse_to_virtual_events_t = binding_layout_t::mouse_to_virtual_events_t; - using imguizmo_to_virtual_events_t = binding_layout_t::imguizmo_to_virtual_events_t; + static inline constexpr float SphericalMinDistance = 0.1f; + static inline constexpr float SphericalMaxDistance = 10000.f; struct SMotionConfig { @@ -42,12 +33,6 @@ class ICamera : virtual public core::IReferenceCounted double rotationSpeedScale = 0.003; }; - struct SInputBindingConfig - { - //! Default binding layout advertised by this camera type. - CGimbalBindingLayoutStorage defaultBindingLayout; - }; - enum class CameraKind : uint8_t { Unknown, @@ -171,20 +156,6 @@ class ICamera : virtual public core::IReferenceCounted ICamera() {} virtual ~ICamera() = default; - virtual const keyboard_to_virtual_events_t getKeyboardMappingPreset() const { return {}; } - virtual const mouse_to_virtual_events_t getMouseMappingPreset() const { return {}; } - virtual const imguizmo_to_virtual_events_t getImguizmoMappingPreset() const { return {}; } - - inline const IGimbalBindingLayout& getDefaultInputBindingLayout() const { return m_inputBindingConfig.defaultBindingLayout; } - inline IGimbalBindingLayout& getDefaultInputBindingLayout() { return m_inputBindingConfig.defaultBindingLayout; } - inline void copyDefaultInputBindingPresetTo(IGimbalBindingLayout& layout) const - { - layout.updateKeyboardMapping([&](auto& map) { map = getKeyboardMappingPreset(); }); - layout.updateMouseMapping([&](auto& map) { map = getMouseMappingPreset(); }); - layout.updateImguizmoMapping([&](auto& map) { map = getImguizmoMappingPreset(); }); - } - - // Returns a gimbal which *models the camera view* virtual const CGimbal& getGimbal() = 0u; // Camera core contract: consume virtual events only. Raw input binding and absolute goal solving live outside ICamera. @@ -199,8 +170,7 @@ class ICamera : virtual public core::IReferenceCounted return manipulateWithMotionScales(virtualEvents, referenceFrame, 1.0, 1.0); } - // VirtualEventType bitmask for a camera view gimbal manipulation requests filtering - virtual const uint32_t getAllowedVirtualEvents() = 0u; + virtual uint32_t getAllowedVirtualEvents() const = 0u; virtual CameraKind getKind() const = 0; virtual uint32_t getCapabilities() const { return None; } @@ -214,8 +184,7 @@ class ICamera : virtual public core::IReferenceCounted return mask; } - // Identifier of a camera type - virtual const std::string_view getIdentifier() = 0u; + virtual std::string_view getIdentifier() const = 0u; inline bool hasCapability(CameraCapability capability) const { @@ -267,13 +236,11 @@ class ICamera : virtual public core::IReferenceCounted return false; } - // (***) inline void setMoveSpeedScale(double scalar) { m_motionConfig.moveSpeedScale = scalar; } - // (***) inline void setRotationSpeedScale(double scalar) { m_motionConfig.rotationSpeedScale = scalar; @@ -288,19 +255,13 @@ class ICamera : virtual public core::IReferenceCounted inline double getMoveSpeedScale() const { return m_motionConfig.moveSpeedScale; } inline double getRotationSpeedScale() const { return m_motionConfig.rotationSpeedScale; } inline const SMotionConfig& getMotionConfig() const { return m_motionConfig; } - inline const SInputBindingConfig& getInputBindingConfig() const { return m_inputBindingConfig; } inline SScopedMotionScaleOverride overrideMotionScales(const double moveScale, const double rotationScale) { return SScopedMotionScaleOverride(this, moveScale, rotationScale); } - inline void resetDefaultInputBindingToPreset() - { - copyDefaultInputBindingPresetTo(m_inputBindingConfig.defaultBindingLayout); - } protected: SMotionConfig m_motionConfig; - SInputBindingConfig m_inputBindingConfig; }; } diff --git a/common/include/camera/IGimbal.hpp b/common/include/camera/IGimbal.hpp index e65f85a00..f68790a7b 100644 --- a/common/include/camera/IGimbal.hpp +++ b/common/include/camera/IGimbal.hpp @@ -1,13 +1,52 @@ #ifndef _NBL_IGIMBAL_HPP_ #define _NBL_IGIMBAL_HPP_ -#include "glm/glm/ext/matrix_transform.hpp" // TODO: TEMPORARY!!! whatever used will be moved to cpp -#include "glm/glm/gtc/quaternion.hpp" -#include "nbl/builtin/hlsl/matrix_utils/transformation_matrix_utils.hlsl" +#include "CCameraMathUtilities.hpp" -// TODO: DIFFERENT NAMESPACE -namespace nbl::hlsl +namespace nbl::core { + using hlsl::camera_matrix_t; + using hlsl::camera_quaternion_t; + using hlsl::camera_vector_t; + using hlsl::composeTransformMatrix; + using hlsl::cross; + using hlsl::degrees; + using hlsl::dot; + using hlsl::float32_t; + using hlsl::float32_t3; + using hlsl::float32_t4; + using hlsl::float32_t4x4; + using hlsl::float64_t; + using hlsl::float64_t3; + using hlsl::float64_t4; + using hlsl::float64_t3x4; + using hlsl::float64_t4x4; + using hlsl::getCastedMatrix; + using hlsl::getCastedVector; + using hlsl::getMatrix3x3As4x4; + using hlsl::getMatrix3x4As4x4; + using hlsl::getQuaternionAngularDistanceDegrees; + using hlsl::getQuaternionBasisMatrix; + using hlsl::getQuaternionEulerDegrees; + using hlsl::getQuaternionEulerRadians; + using hlsl::inverseQuaternion; + using hlsl::isFiniteQuaternion; + using hlsl::isOrthoBase; + using hlsl::length; + using hlsl::makeIdentityQuaternion; + using hlsl::makeQuaternionFromAxisAngle; + using hlsl::makeQuaternionFromBasis; + using hlsl::makeQuaternionFromComponents; + using hlsl::makeQuaternionFromEulerDegrees; + using hlsl::makeQuaternionFromEulerRadians; + using hlsl::mul; + using hlsl::normalize; + using hlsl::normalizeQuaternion; + using hlsl::radians; + using hlsl::rotateVectorByQuaternion; + using hlsl::slerpQuaternion; + using hlsl::wrapAngleRad; + /** * Shared semantic camera command. * @@ -126,7 +165,7 @@ namespace nbl::hlsl struct CReferenceTransform { float64_t4x4 frame; - glm::quat orientation; + camera_quaternion_t orientation = makeIdentityQuaternion(); }; template @@ -135,17 +174,20 @@ namespace nbl::hlsl { public: using precision_t = T; + using quaternion_t = camera_quaternion_t; + template + using vector_t = camera_vector_t; //! underlying type for world matrix (TRS) - using model_matrix_t = matrix; + using model_matrix_t = hlsl::matrix; struct VirtualImpulse { - vector dVirtualTranslate { 0.0f }, dVirtualRotation { 0.0f }, dVirtualScale { 1.0f }; + vector_t<3u> dVirtualTranslate { 0.0f }, dVirtualRotation { 0.0f }, dVirtualScale { 1.0f }; }; //! Accumulates one frame of virtual events into a translation/rotation/scale impulse. template - VirtualImpulse accumulate(std::span virtualEvents, const vector& gRightOverride, const vector& gUpOverride, const vector& gForwardOverride) + VirtualImpulse accumulate(std::span virtualEvents, const vector_t<3u>& gRightOverride, const vector_t<3u>& gUpOverride, const vector_t<3u>& gForwardOverride) { VirtualImpulse impulse; @@ -240,8 +282,8 @@ namespace nbl::hlsl struct SCreationParameters { - vector position; - glm::quat orientation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f); + vector_t<3u> position; + quaternion_t orientation = makeIdentityQuaternion(); }; IGimbal(const IGimbal&) = default; @@ -250,22 +292,20 @@ namespace nbl::hlsl IGimbal& operator=(IGimbal&&) noexcept = default; IGimbal(SCreationParameters&& parameters) - : m_position(parameters.position), m_orientation(parameters.orientation), m_id(reinterpret_cast(this)) + : m_position(parameters.position), m_orientation(parameters.orientation) { updateOrthonormalOrientationBase(); } - inline const uintptr_t getID() const { return m_id; } - void begin() { m_isManipulating = true; m_counter = 0u; } - inline void setPosition(const vector& position) + inline void setPosition(const vector_t<3u>& position) { - assert(m_isManipulating); // TODO: log error and return without doing nothing + assert(m_isManipulating); if (m_position != position) m_counter++; @@ -273,43 +313,43 @@ namespace nbl::hlsl m_position = position; } - inline void setScale(const vector& scale) + inline void setScale(const vector_t<3u>& scale) { m_scale = scale; } - inline void setOrientation(const glm::quat& orientation) + inline void setOrientation(const quaternion_t& orientation) { - assert(m_isManipulating); // TODO: log error and return without doing nothing + assert(m_isManipulating); - if(m_orientation != orientation) + if (m_orientation.data != orientation.data) m_counter++; - m_orientation = glm::normalize(orientation); + m_orientation = normalizeQuaternion(orientation); updateOrthonormalOrientationBase(); } inline void transform(const CReferenceTransform& reference, const VirtualImpulse& impulse) { - setOrientation(reference.orientation * glm::quat(impulse.dVirtualRotation)); + setOrientation(reference.orientation * makeQuaternionFromEulerRadians(impulse.dVirtualRotation)); setPosition(mul(float64_t4(impulse.dVirtualTranslate, 1), reference.frame).xyz); } - inline void rotate(const vector& axis, float dRadians) + inline void rotate(const vector_t<3u>& axis, float dRadians) { - assert(m_isManipulating); // TODO: log error and return without doing nothing + assert(m_isManipulating); if(dRadians) m_counter++; - glm::quat dRotation = glm::angleAxis(dRadians, axis); - m_orientation = glm::normalize(dRotation * m_orientation); + const auto dRotation = makeQuaternionFromAxisAngle(axis, static_cast(dRadians)); + m_orientation = normalizeQuaternion(dRotation * m_orientation); updateOrthonormalOrientationBase(); } - inline void move(vector delta) + inline void move(vector_t<3u> delta) { - assert(m_isManipulating); // TODO: log error and return without doing nothing + assert(m_isManipulating); auto newPosition = m_position + delta; @@ -350,30 +390,30 @@ namespace nbl::hlsl //! World matrix (TRS) template - requires is_any_of_v> + requires is_any_of_v> const TRS operator()() const { const auto& position = getPosition(); const auto& rotation = getOrthonornalMatrix(); const auto& scale = getScale(); - if constexpr (is_same_v) + if constexpr (std::is_same_v) { return { - vector(rotation[0] * scale.x, position.x), - vector(rotation[1] * scale.y, position.y), - vector(rotation[2] * scale.z, position.z) + camera_vector_t(rotation[0] * scale.x, position.x), + camera_vector_t(rotation[1] * scale.y, position.y), + camera_vector_t(rotation[2] * scale.z, position.z) }; } else { return { - vector(rotation[0] * scale.x, T(0)), - vector(rotation[1] * scale.y, T(0)), - vector(rotation[2] * scale.z, T(0)), - vector(position, T(1)) + camera_vector_t(rotation[0] * scale.x, T(0)), + camera_vector_t(rotation[1] * scale.y, T(0)), + camera_vector_t(rotation[2] * scale.z, T(0)), + camera_vector_t(position, T(1)) }; } } @@ -419,7 +459,7 @@ namespace nbl::hlsl out->frame[3] = float64_t4(getPosition(), 1); } - out->orientation = glm::quat_cast(glm::dmat3{ out->frame[0], out->frame[1], out->frame[2] }); + out->orientation = makeQuaternionFromBasis(float64_t3(out->frame[0]), float64_t3(out->frame[1]), float64_t3(out->frame[2])); return true; } @@ -427,21 +467,20 @@ namespace nbl::hlsl private: inline void updateOrthonormalOrientationBase() { - m_orthonormal = matrix(glm::mat3_cast(glm::normalize(m_orientation))); + m_orthonormal = getQuaternionBasisMatrix(m_orientation); } //! Position of a gimbal in world space - vector m_position; + vector_t<3u> m_position; //! Normalized orientation of gimbal - //! TODO: precision + replace with our "quat at home" - glm::quat m_orientation; + quaternion_t m_orientation; //! Scale transform component - vector m_scale = { 1.f, 1.f , 1.f }; + vector_t<3u> m_scale = { 1.f, 1.f , 1.f }; - //! Orthonormal base composed from "m_orientation" representing gimbal's "forward", "up" & "right" vectors in local space - basically it spans orientation space - matrix m_orthonormal; + //! Orthonormal basis reconstructed from the current orientation. + hlsl::matrix m_orthonormal; //! Counter that increments for each performed manipulation, resets with each begin() call size_t m_counter = {}; @@ -449,9 +488,7 @@ namespace nbl::hlsl //! Tracks whether gimbal is currently in manipulation mode bool m_isManipulating = false; - //! The fact ImGUIZMO has global context I don't like, however for IDs we can do a life-tracking trick and cast addresses which are unique & we don't need any global associative container to track them! - const uintptr_t m_id; }; -} // namespace nbl::hlsl +} // namespace nbl::core #endif // _NBL_IGIMBAL_HPP_ diff --git a/common/include/camera/IGimbalBindingLayout.hpp b/common/include/camera/IGimbalBindingLayout.hpp index 381a6ba9a..5268720a8 100644 --- a/common/include/camera/IGimbalBindingLayout.hpp +++ b/common/include/camera/IGimbalBindingLayout.hpp @@ -3,7 +3,7 @@ #include "IGimbal.hpp" -namespace nbl::hlsl +namespace nbl::ui { /** @@ -16,7 +16,7 @@ struct IGimbalBindingLayout IGimbalBindingLayout() {} virtual ~IGimbalBindingLayout() {} - using gimbal_event_t = CVirtualGimbalEvent; + using gimbal_event_t = core::CVirtualGimbalEvent; using encode_keyboard_code_t = ui::E_KEY_CODE; using encode_mouse_code_t = ui::E_MOUSE_CODE; using encode_imguizmo_code_t = gimbal_event_t::VirtualEventType; @@ -60,10 +60,6 @@ struct IGimbalBindingLayout using mouse_to_virtual_events_t = std::unordered_map; using imguizmo_to_virtual_events_t = std::unordered_map; - virtual const keyboard_to_virtual_events_t getKeyboardMappingPreset() const { return {}; } - virtual const mouse_to_virtual_events_t getMouseMappingPreset() const { return {}; } - virtual const imguizmo_to_virtual_events_t getImguizmoMappingPreset() const { return {}; } - virtual const keyboard_to_virtual_events_t& getKeyboardVirtualEventMap() const = 0; virtual const mouse_to_virtual_events_t& getMouseVirtualEventMap() const = 0; virtual const imguizmo_to_virtual_events_t& getImguizmoVirtualEventMap() const = 0; @@ -97,13 +93,6 @@ class CGimbalBindingLayoutStorage : public IGimbalBindingLayout updateImguizmoMapping([&](auto& map) { map = sanitizeMapping(layout.getImguizmoVirtualEventMap()); }); } - inline void copyPresetLayoutFrom(const IGimbalBindingLayout& layout) - { - updateKeyboardMapping([&](auto& map) { map = sanitizeMapping(layout.getKeyboardMappingPreset()); }); - updateMouseMapping([&](auto& map) { map = sanitizeMapping(layout.getMouseMappingPreset()); }); - updateImguizmoMapping([&](auto& map) { map = sanitizeMapping(layout.getImguizmoMappingPreset()); }); - } - inline void copyBindingLayoutTo(IGimbalBindingLayout& layout) const { layout.updateKeyboardMapping([&](auto& map) { map = sanitizeMapping(getKeyboardVirtualEventMap()); }); @@ -126,6 +115,6 @@ class CGimbalBindingLayoutStorage : public IGimbalBindingLayout imguizmo_to_virtual_events_t m_imguizmoVirtualEventMap; }; -} // namespace nbl::hlsl +} // namespace nbl::ui #endif diff --git a/common/include/camera/IGimbalInputProcessor.hpp b/common/include/camera/IGimbalInputProcessor.hpp index 473008a24..1dbb3b23b 100644 --- a/common/include/camera/IGimbalInputProcessor.hpp +++ b/common/include/camera/IGimbalInputProcessor.hpp @@ -1,19 +1,12 @@ #ifndef _NBL_I_GIMBAL_INPUT_PROCESSOR_HPP_ #define _NBL_I_GIMBAL_INPUT_PROCESSOR_HPP_ -///////////////////////// -// TODO: TEMPORARY!!! -#include "common.hpp" -namespace ImGuizmo -{ - void DecomposeMatrixToComponents(const float*, float*, float*, float*); -} -///////////////////////// +#include "nbl/ui/KeyCodes.h" +#include "nbl/ui/SInputEvent.h" #include "IGimbalBindingLayout.hpp" -// TODO: DIFFERENT NAMESPACE -namespace nbl::hlsl +namespace nbl::ui { /** @@ -24,17 +17,17 @@ class IGimbalInputProcessor : public CGimbalBindingLayoutStorage public: using CGimbalBindingLayoutStorage::CGimbalBindingLayoutStorage; - IGimbalInputProcessor() {} - virtual ~IGimbalInputProcessor() {} + IGimbalInputProcessor() = default; + virtual ~IGimbalInputProcessor() = default; - //! input of keyboard gimbal controller process utility - Nabla UI event handler produces ui::SKeyboardEvent events + //! Keyboard events consumed by the processor. using input_keyboard_event_t = ui::SKeyboardEvent; - //! input of mouse gimbal controller process utility - Nabla UI event handler produces ui::SMouseEvent events + //! Mouse events consumed by the processor. using input_mouse_event_t = ui::SMouseEvent; - //! input of ImGuizmo gimbal controller process utility - ImGuizmo manipulate utility produces "delta (TRS) matrix" events - using input_imguizmo_event_t = float32_t4x4; + //! ImGuizmo world-space delta transforms consumed by the processor. + using input_imguizmo_event_t = hlsl::float32_t4x4; void beginInputProcessing(const std::chrono::microseconds nextPresentationTimeStamp) { @@ -251,9 +244,9 @@ class IGimbalInputProcessor : public CGimbalBindingLayoutStorage /** * @brief Processes input events from ImGuizmo and generates virtual gimbal events. * - * @note This function is intended to process transformations provided by ImGuizmo and convert - * them into virtual gimbal events for the ICamera::World mode (ICamera::Local is invalid!). - * The function computes translation, rotation, and scale deltas from ImGuizmo's delta matrix, + * @note This function processes world-space delta transforms authored by ImGuizmo and converts + * them into virtual gimbal events for world-space camera manipulation. + * The function computes translation, rotation, and scale deltas from each transform matrix, * which are then mapped to corresponding virtual events using a predefined mapping. * * @param "output" is pointer to the array where generated gimbal events will be stored. @@ -289,11 +282,11 @@ class IGimbalInputProcessor : public CGimbalBindingLayoutStorage struct { - float32_t3 dTranslation, dRotation, dScale; + hlsl::float32_t3 dTranslation, dRotation, dScale; } world; - // TODO: write it in Nabla, this is temp - ImGuizmo::DecomposeMatrixToComponents(&deltaWorldTRS[0][0], &world.dTranslation[0], &world.dRotation[0], &world.dScale[0]); + if (!hlsl::decomposeTransformMatrix(deltaWorldTRS, world.dTranslation, world.dRotation, world.dScale)) + continue; // Delta translation impulse requestMagnitudeUpdateWithScalar(0.f, world.dTranslation[0], std::abs(world.dTranslation[0]), gimbal_event_t::MoveRight, gimbal_event_t::MoveLeft, m_imguizmoVirtualEventMap); @@ -301,7 +294,7 @@ class IGimbalInputProcessor : public CGimbalBindingLayoutStorage requestMagnitudeUpdateWithScalar(0.f, world.dTranslation[2], std::abs(world.dTranslation[2]), gimbal_event_t::MoveForward, gimbal_event_t::MoveBackward, m_imguizmoVirtualEventMap); // Delta rotation impulse - const float32_t3 dRotationRad = + const hlsl::float32_t3 dRotationRad = { hlsl::radians(world.dRotation[0]), hlsl::radians(world.dRotation[1]), @@ -323,7 +316,6 @@ class IGimbalInputProcessor : public CGimbalBindingLayoutStorage private: - //! helper utility, for any controller this should be called before any update of hash map void preprocess(auto& map) { for (auto& [key, hash] : map) @@ -335,7 +327,6 @@ class IGimbalInputProcessor : public CGimbalBindingLayoutStorage } } - //! helper utility, for any controller this should be called after updating a hash map void postprocess(const auto& map, gimbal_event_t* output, uint32_t& count) { for (const auto& [key, hash] : map) @@ -348,7 +339,6 @@ class IGimbalInputProcessor : public CGimbalBindingLayoutStorage } } - //! helper utility, it *doesn't* assume we keep requested events alive but only increase their magnitude template void requestMagnitudeUpdateWithScalar(float signPivot, float dScalar, float dMagnitude, EncodeType positive, EncodeType negative, Map& map) { @@ -365,6 +355,6 @@ class IGimbalInputProcessor : public CGimbalBindingLayoutStorage std::chrono::microseconds m_nextPresentationTimeStamp = {}, m_lastVirtualUpTimeStamp = {}; }; -} // nbl::hlsl namespace +} // namespace nbl::ui #endif // _NBL_I_GIMBAL_INPUT_PROCESSOR_HPP_ diff --git a/common/include/camera/ILinearProjection.hpp b/common/include/camera/ILinearProjection.hpp index 41f9ca34f..641a38e53 100644 --- a/common/include/camera/ILinearProjection.hpp +++ b/common/include/camera/ILinearProjection.hpp @@ -1,10 +1,10 @@ -#ifndef _NBL_I_LINEAR_PROJECTION_HPP_ +#ifndef _NBL_I_LINEAR_PROJECTION_HPP_ #define _NBL_I_LINEAR_PROJECTION_HPP_ #include "IProjection.hpp" #include "ICamera.hpp" -namespace nbl::hlsl +namespace nbl::core { /** diff --git a/common/include/camera/IPerspectiveProjection.hpp b/common/include/camera/IPerspectiveProjection.hpp index 479db170e..d934c245c 100644 --- a/common/include/camera/IPerspectiveProjection.hpp +++ b/common/include/camera/IPerspectiveProjection.hpp @@ -3,7 +3,7 @@ #include "ILinearProjection.hpp" -namespace nbl::hlsl +namespace nbl::core { /** diff --git a/common/include/camera/IPlanarProjection.hpp b/common/include/camera/IPlanarProjection.hpp index 32c4298a6..d34c7a48c 100644 --- a/common/include/camera/IPlanarProjection.hpp +++ b/common/include/camera/IPlanarProjection.hpp @@ -4,7 +4,7 @@ #include "IGimbalBindingLayout.hpp" #include "ILinearProjection.hpp" -namespace nbl::hlsl +namespace nbl::core { /** @@ -77,9 +77,9 @@ class IPlanarProjection : public ILinearProjection const auto& fov = m_parameters.m_planar.perspective.fov; if (leftHanded) - base_t::setProjectionMatrix(buildProjectionMatrixPerspectiveFovLH(hlsl::radians(fov), aspectRatio, m_parameters.m_zNear, m_parameters.m_zFar)); + base_t::setProjectionMatrix(hlsl::buildProjectionMatrixPerspectiveFovLH(hlsl::radians(fov), aspectRatio, m_parameters.m_zNear, m_parameters.m_zFar)); else - base_t::setProjectionMatrix(buildProjectionMatrixPerspectiveFovRH(hlsl::radians(fov), aspectRatio, m_parameters.m_zNear, m_parameters.m_zFar)); + base_t::setProjectionMatrix(hlsl::buildProjectionMatrixPerspectiveFovRH(hlsl::radians(fov), aspectRatio, m_parameters.m_zNear, m_parameters.m_zFar)); } break; case Orthographic: @@ -88,9 +88,9 @@ class IPlanarProjection : public ILinearProjection const auto viewHeight = orthoW * core::reciprocal(aspectRatio); if (leftHanded) - base_t::setProjectionMatrix(buildProjectionMatrixOrthoLH(orthoW, viewHeight, m_parameters.m_zNear, m_parameters.m_zFar)); + base_t::setProjectionMatrix(hlsl::buildProjectionMatrixOrthoLH(orthoW, viewHeight, m_parameters.m_zNear, m_parameters.m_zFar)); else - base_t::setProjectionMatrix(buildProjectionMatrixOrthoRH(orthoW, viewHeight, m_parameters.m_zNear, m_parameters.m_zFar)); + base_t::setProjectionMatrix(hlsl::buildProjectionMatrixOrthoRH(orthoW, viewHeight, m_parameters.m_zNear, m_parameters.m_zFar)); } break; } } @@ -112,12 +112,12 @@ class IPlanarProjection : public ILinearProjection } inline const ProjectionParameters& getParameters() const { return m_parameters; } - inline const CGimbalBindingLayoutStorage& getInputBinding() const { return m_inputBinding; } - inline CGimbalBindingLayoutStorage& getInputBinding() { return m_inputBinding; } + inline const ui::CGimbalBindingLayoutStorage& getInputBinding() const { return m_inputBinding; } + inline ui::CGimbalBindingLayoutStorage& getInputBinding() { return m_inputBinding; } private: CProjection() = default; ProjectionParameters m_parameters; - CGimbalBindingLayoutStorage m_inputBinding; + ui::CGimbalBindingLayoutStorage m_inputBinding; }; protected: @@ -126,6 +126,6 @@ class IPlanarProjection : public ILinearProjection virtual ~IPlanarProjection() = default; }; -} // nbl::hlsl namespace +} // namespace nbl::core #endif // _NBL_I_PLANAR_PROJECTION_HPP_ diff --git a/common/include/camera/IProjection.hpp b/common/include/camera/IProjection.hpp index cb6facdcc..cd3a9909a 100644 --- a/common/include/camera/IProjection.hpp +++ b/common/include/camera/IProjection.hpp @@ -3,7 +3,7 @@ #include -namespace nbl::hlsl +namespace nbl::core { //! Interface class for any type of projection @@ -65,6 +65,6 @@ class IProjection virtual ProjectionType getProjectionType() const = 0; }; -} // namespace nbl::hlsl +} // namespace nbl::core #endif // _NBL_IPROJECTION_HPP_ \ No newline at end of file diff --git a/common/include/camera/IRange.hpp b/common/include/camera/IRange.hpp index a6ed29270..91a3d9ed7 100644 --- a/common/include/camera/IRange.hpp +++ b/common/include/camera/IRange.hpp @@ -1,7 +1,7 @@ #ifndef _NBL_IRANGE_HPP_ #define _NBL_IRANGE_HPP_ -namespace nbl::hlsl +namespace nbl::core { template @@ -15,6 +15,6 @@ concept ContiguousGeneralPurposeRangeOf = GeneralPurposeRange && std::ranges::contiguous_range && std::same_as, T>; -} // namespace nbl::hlsl +} // namespace nbl::core #endif // _NBL_IRANGE_HPP_ \ No newline at end of file diff --git a/common/include/camera/README.md b/common/include/camera/README.md index 3f423b23b..68615b93f 100644 --- a/common/include/camera/README.md +++ b/common/include/camera/README.md @@ -1,13 +1,7 @@ # Shared Camera API -This directory contains the reusable camera stack. -The current full consumer and validation harness is [`61_UI`](../../../61_UI/README.md). - -It intentionally lives in `examples_tests/common` instead of engine core: - -- the API is meant to be reusable across consumers and tools -- the API is still being shaped through one active integration surface -- the current goal is to stabilize the design before promoting any part of it deeper into Nabla +This directory contains the shared camera stack. +A complete runnable integration is [`61_UI`](../../../61_UI/README.md). ## What this stack is @@ -28,7 +22,6 @@ That split is the core design decision. This stack is not: - engine-core Nabla camera API -- a promise that every potential consumer already uses it - a generic scene animation system - a direct scene-object-follow system - a setter-heavy camera API with arbitrary absolute pose mutation in the hot runtime path @@ -40,10 +33,23 @@ The design goals are: - one semantic command language for keyboard, mouse, gizmo, scripts, and CI - no input-device assumptions inside camera models - no viewport glue inside camera models -- no direct dependence on consumer-specific UI concepts in the reusable camera layer +- no direct dependence on application-specific UI concepts in the reusable camera layer - best-effort absolute restore for tooling without turning cameras into mutable state bags - reusable persistence, analysis, playback, follow, and validation helpers +## Namespace split + +The stack is split across existing Nabla namespaces: + +- `nbl::hlsl` + math types and camera math helpers +- `nbl::core` + camera runtime model, goals, presets, tracks, playback, follow, and authored sequence data +- `nbl::ui` + binding layouts, input processors, binders, and default input mappings +- `nbl::system` + persistence, scripted runtime payloads, scripted parsing, and scripted check execution + ## Why virtual events and not absolute setters The runtime contract is intentionally built around virtual events such as: @@ -406,7 +412,7 @@ It describes: It deliberately does not describe: -- consumer-specific window actions as authored source data +- runtime-specific window actions as authored source data - frame-by-frame event dumps - ImGuizmo matrices as authored motion primitives @@ -424,11 +430,11 @@ It is split into: - per-frame dequeue - per-frame check evaluation -This keeps one consumer from owning a private scripting subsystem. +This keeps one runtime from owning a private scripting subsystem. ## Current validation story -The current camera-focused validation is exercised through the active consumer harness. +The current camera-focused validation is exercised through scripted smoke and continuity tests. ### Smoke @@ -454,7 +460,7 @@ This test now runs on the compact authored sequence format rather than a large e auto camera = core::make_smart_refctd_ptr(eye, target); CGimbalInputBinder binder; -camera->copyDefaultInputBindingPresetTo(binder); +applyDefaultCameraInputBindingPreset(binder, *camera); auto collected = binder.collectVirtualEvents(timestamp, { .mouseEvents = { mouseEvents.data(), mouseEvents.size() }, @@ -536,23 +542,7 @@ If any one of those concerns leaks into the others: - cameras become setter-heavy - projections become input processors - consumers own private copies of state math -- scripts become consumer-specific +- scripts become runtime-specific - follow becomes scene-object-specific The current refactor was mostly about removing exactly those leaks. - -## Practical status - -Today the stack is in a good place for: - -- reuse by additional consumers -- reuse by camera-heavy tools -- reuse by offline render or validation flows - -without assuming: - -- a specific scene object type -- a specific input device -- a specific renderer - -That is the main architectural win of the current design. diff --git a/common/src/nbl/examples/camera/CCameraPersistence.cpp b/common/src/nbl/examples/camera/CCameraPersistence.cpp new file mode 100644 index 000000000..fa959088a --- /dev/null +++ b/common/src/nbl/examples/camera/CCameraPersistence.cpp @@ -0,0 +1,338 @@ +// 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 "camera/CCameraPersistence.hpp" + +#include +#include + +#include "nlohmann/json.hpp" + +namespace +{ +using json_t = nlohmann::json; + +json_t serializeGoalJson(const nbl::core::CCameraGoal& goal) +{ + json_t json; + json["position"] = { goal.position.x, goal.position.y, goal.position.z }; + json["orientation"] = { + goal.orientation.data.x, + goal.orientation.data.y, + goal.orientation.data.z, + goal.orientation.data.w + }; + json["camera_kind"] = static_cast(goal.sourceKind); + json["camera_capabilities"] = goal.sourceCapabilities; + json["camera_goal_state_mask"] = goal.sourceGoalStateMask; + + if (goal.hasTargetPosition) + json["target_position"] = { goal.targetPosition.x, goal.targetPosition.y, goal.targetPosition.z }; + if (goal.hasDistance) + json["distance"] = goal.distance; + if (goal.hasOrbitState) + { + json["orbit_u"] = goal.orbitU; + json["orbit_v"] = goal.orbitV; + json["orbit_distance"] = goal.orbitDistance; + } + if (goal.hasPathState) + { + json["path_angle"] = goal.pathState.angle; + json["path_radius"] = goal.pathState.radius; + json["path_height"] = goal.pathState.height; + } + if (goal.hasDynamicPerspectiveState) + { + json["dynamic_base_fov"] = goal.dynamicPerspectiveState.baseFov; + json["dynamic_reference_distance"] = goal.dynamicPerspectiveState.referenceDistance; + } + + return json; +} + +void deserializeGoalJson(const json_t& entry, nbl::core::CCameraGoal& goal) +{ + goal = {}; + + if (entry.contains("camera_kind")) + goal.sourceKind = static_cast(entry["camera_kind"].get()); + if (entry.contains("camera_capabilities")) + goal.sourceCapabilities = entry["camera_capabilities"].get(); + if (entry.contains("camera_goal_state_mask")) + goal.sourceGoalStateMask = entry["camera_goal_state_mask"].get(); + + if (entry.contains("position") && entry["position"].is_array()) + { + const auto values = entry["position"].get>(); + goal.position = nbl::hlsl::float64_t3(values[0], values[1], values[2]); + } + if (entry.contains("orientation") && entry["orientation"].is_array()) + { + const auto values = entry["orientation"].get>(); + goal.orientation = nbl::hlsl::makeQuaternionFromComponents( + values[0], + values[1], + values[2], + values[3]); + } + if (entry.contains("target_position") && entry["target_position"].is_array()) + { + const auto values = entry["target_position"].get>(); + goal.targetPosition = nbl::hlsl::float64_t3(values[0], values[1], values[2]); + goal.hasTargetPosition = true; + } + if (entry.contains("distance")) + { + goal.distance = entry["distance"].get(); + goal.hasDistance = true; + } + if (entry.contains("orbit_u")) + { + goal.orbitU = entry["orbit_u"].get(); + goal.hasOrbitState = true; + } + if (entry.contains("orbit_v")) + { + goal.orbitV = entry["orbit_v"].get(); + goal.hasOrbitState = true; + } + if (entry.contains("orbit_distance")) + { + goal.orbitDistance = entry["orbit_distance"].get(); + goal.hasOrbitState = true; + } + if (entry.contains("path_angle") && entry.contains("path_radius") && entry.contains("path_height")) + { + goal.pathState.angle = entry["path_angle"].get(); + goal.pathState.radius = entry["path_radius"].get(); + goal.pathState.height = entry["path_height"].get(); + goal.hasPathState = true; + } + if (entry.contains("dynamic_base_fov")) + { + goal.dynamicPerspectiveState.baseFov = entry["dynamic_base_fov"].get(); + goal.hasDynamicPerspectiveState = true; + } + if (entry.contains("dynamic_reference_distance")) + { + goal.dynamicPerspectiveState.referenceDistance = entry["dynamic_reference_distance"].get(); + goal.hasDynamicPerspectiveState = true; + } +} + +json_t serializePresetJson(const nbl::core::CCameraPreset& preset) +{ + auto json = serializeGoalJson(nbl::core::makeGoalFromPreset(preset)); + json["name"] = preset.name; + json["identifier"] = preset.identifier; + return json; +} + +void deserializePresetJson(const json_t& entry, nbl::core::CCameraPreset& preset) +{ + preset = {}; + if (entry.contains("name")) + preset.name = entry["name"].get(); + if (entry.contains("identifier")) + preset.identifier = entry["identifier"].get(); + + nbl::core::CCameraGoal goal; + deserializeGoalJson(entry, goal); + nbl::core::assignGoalToPreset(preset, goal); +} + +json_t serializeKeyframeTrackJson(const nbl::core::CCameraKeyframeTrack& track) +{ + json_t root; + root["keyframes"] = json_t::array(); + + for (const auto& keyframe : track.keyframes) + { + auto json = serializePresetJson(keyframe.preset); + json["time"] = keyframe.time; + root["keyframes"].push_back(std::move(json)); + } + + return root; +} + +bool deserializeKeyframeTrackJson(const json_t& root, nbl::core::CCameraKeyframeTrack& track) +{ + if (!root.contains("keyframes") || !root["keyframes"].is_array()) + return false; + + track = {}; + for (const auto& entry : root["keyframes"]) + { + nbl::core::CCameraKeyframe keyframe; + if (entry.contains("time")) + keyframe.time = std::max(0.f, entry["time"].get()); + deserializePresetJson(entry, keyframe.preset); + track.keyframes.emplace_back(std::move(keyframe)); + } + + nbl::core::sortKeyframeTrackByTime(track); + nbl::core::normalizeSelectedKeyframeTrack(track); + return true; +} + +json_t serializePresetCollectionJson(std::span presets) +{ + json_t root; + root["presets"] = json_t::array(); + for (const auto& preset : presets) + root["presets"].push_back(serializePresetJson(preset)); + return root; +} + +bool deserializePresetCollectionJson(const json_t& root, std::vector& presets) +{ + if (!root.contains("presets") || !root["presets"].is_array()) + return false; + + std::vector loadedPresets; + loadedPresets.reserve(root["presets"].size()); + for (const auto& entry : root["presets"]) + { + nbl::core::CCameraPreset preset; + deserializePresetJson(entry, preset); + loadedPresets.emplace_back(std::move(preset)); + } + + presets = std::move(loadedPresets); + return true; +} +} // anonymous namespace + +namespace nbl::system +{ + +bool writeGoal(std::ostream& out, const core::CCameraGoal& goal, const int indent) +{ + if (!out) + return false; + + out << serializeGoalJson(goal).dump(indent); + return static_cast(out); +} + +bool readGoal(std::istream& in, core::CCameraGoal& goal) +{ + if (!in) + return false; + + json_t root; + in >> root; + deserializeGoalJson(root, goal); + return true; +} + +bool saveGoalToFile(const path& filePath, const core::CCameraGoal& goal, const int indent) +{ + std::ofstream out(filePath.string(), std::ios::binary); + return writeGoal(out, goal, indent); +} + +bool loadGoalFromFile(const path& filePath, core::CCameraGoal& goal) +{ + std::ifstream in(filePath.string(), std::ios::binary); + return readGoal(in, goal); +} + +bool writePreset(std::ostream& out, const core::CCameraPreset& preset, const int indent) +{ + if (!out) + return false; + + out << serializePresetJson(preset).dump(indent); + return static_cast(out); +} + +bool readPreset(std::istream& in, core::CCameraPreset& preset) +{ + if (!in) + return false; + + json_t root; + in >> root; + deserializePresetJson(root, preset); + return true; +} + +bool savePresetToFile(const path& filePath, const core::CCameraPreset& preset, const int indent) +{ + std::ofstream out(filePath.string(), std::ios::binary); + return writePreset(out, preset, indent); +} + +bool loadPresetFromFile(const path& filePath, core::CCameraPreset& preset) +{ + std::ifstream in(filePath.string(), std::ios::binary); + return readPreset(in, preset); +} + +bool writeKeyframeTrack(std::ostream& out, const core::CCameraKeyframeTrack& track, const int indent) +{ + if (!out) + return false; + + out << serializeKeyframeTrackJson(track).dump(indent); + return static_cast(out); +} + +bool readKeyframeTrack(std::istream& in, core::CCameraKeyframeTrack& track) +{ + if (!in) + return false; + + json_t root; + in >> root; + return deserializeKeyframeTrackJson(root, track); +} + +bool saveKeyframeTrackToFile(const path& filePath, const core::CCameraKeyframeTrack& track, const int indent) +{ + std::ofstream out(filePath.string(), std::ios::binary); + return writeKeyframeTrack(out, track, indent); +} + +bool loadKeyframeTrackFromFile(const path& filePath, core::CCameraKeyframeTrack& track) +{ + std::ifstream in(filePath.string(), std::ios::binary); + return readKeyframeTrack(in, track); +} + +bool writePresetCollection(std::ostream& out, std::span presets, const int indent) +{ + if (!out) + return false; + + out << serializePresetCollectionJson(presets).dump(indent); + return static_cast(out); +} + +bool readPresetCollection(std::istream& in, std::vector& presets) +{ + if (!in) + return false; + + json_t root; + in >> root; + return deserializePresetCollectionJson(root, presets); +} + +bool savePresetCollectionToFile(const path& filePath, std::span presets, const int indent) +{ + std::ofstream out(filePath.string(), std::ios::binary); + return writePresetCollection(out, presets, indent); +} + +bool loadPresetCollectionFromFile(const path& filePath, std::vector& presets) +{ + std::ifstream in(filePath.string(), std::ios::binary); + return readPresetCollection(in, presets); +} + +} // namespace nbl::system diff --git a/common/src/nbl/examples/camera/CCameraScriptedRuntimePersistence.cpp b/common/src/nbl/examples/camera/CCameraScriptedRuntimePersistence.cpp new file mode 100644 index 000000000..f3e7a4cab --- /dev/null +++ b/common/src/nbl/examples/camera/CCameraScriptedRuntimePersistence.cpp @@ -0,0 +1,1238 @@ +// 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 "camera/CCameraScriptedRuntimePersistence.hpp" + +#include +#include +#include +#include +#include + +#include "nlohmann/json.hpp" + +namespace +{ +using json_t = nlohmann::json; + +void deserializeGoalJson(const json_t& entry, nbl::core::CCameraGoal& goal) +{ + goal = {}; + + if (entry.contains("camera_kind")) + goal.sourceKind = static_cast(entry["camera_kind"].get()); + if (entry.contains("camera_capabilities")) + goal.sourceCapabilities = entry["camera_capabilities"].get(); + if (entry.contains("camera_goal_state_mask")) + goal.sourceGoalStateMask = entry["camera_goal_state_mask"].get(); + + if (entry.contains("position") && entry["position"].is_array()) + { + const auto values = entry["position"].get>(); + goal.position = nbl::hlsl::float64_t3(values[0], values[1], values[2]); + } + if (entry.contains("orientation") && entry["orientation"].is_array()) + { + const auto values = entry["orientation"].get>(); + goal.orientation = nbl::hlsl::makeQuaternionFromComponents( + values[0], + values[1], + values[2], + values[3]); + } + if (entry.contains("target_position") && entry["target_position"].is_array()) + { + const auto values = entry["target_position"].get>(); + goal.targetPosition = nbl::hlsl::float64_t3(values[0], values[1], values[2]); + goal.hasTargetPosition = true; + } + if (entry.contains("distance")) + { + goal.distance = entry["distance"].get(); + goal.hasDistance = true; + } + if (entry.contains("orbit_u")) + { + goal.orbitU = entry["orbit_u"].get(); + goal.hasOrbitState = true; + } + if (entry.contains("orbit_v")) + { + goal.orbitV = entry["orbit_v"].get(); + goal.hasOrbitState = true; + } + if (entry.contains("orbit_distance")) + { + goal.orbitDistance = entry["orbit_distance"].get(); + goal.hasOrbitState = true; + } + if (entry.contains("path_angle") && entry.contains("path_radius") && entry.contains("path_height")) + { + goal.pathState.angle = entry["path_angle"].get(); + goal.pathState.radius = entry["path_radius"].get(); + goal.pathState.height = entry["path_height"].get(); + goal.hasPathState = true; + } + if (entry.contains("dynamic_base_fov")) + { + goal.dynamicPerspectiveState.baseFov = entry["dynamic_base_fov"].get(); + goal.hasDynamicPerspectiveState = true; + } + if (entry.contains("dynamic_reference_distance")) + { + goal.dynamicPerspectiveState.referenceDistance = entry["dynamic_reference_distance"].get(); + goal.hasDynamicPerspectiveState = true; + } +} + +void deserializePresetJson(const json_t& entry, nbl::core::CCameraPreset& preset) +{ + preset = {}; + if (entry.contains("name")) + preset.name = entry["name"].get(); + if (entry.contains("identifier")) + preset.identifier = entry["identifier"].get(); + + nbl::core::CCameraGoal goal; + deserializeGoalJson(entry, goal); + nbl::core::assignGoalToPreset(preset, goal); +} + +bool tryParseCaptureFractionJson(const json_t& entry, float& outFraction) +{ + if (entry.is_number()) + { + outFraction = std::clamp(entry.get(), 0.f, 1.f); + return true; + } + + if (!entry.is_string()) + return false; + + const auto tag = entry.get(); + if (tag == "start") + outFraction = 0.f; + else if (tag == "mid" || tag == "middle") + outFraction = 0.5f; + else if (tag == "end") + outFraction = 1.f; + else + return false; + + return true; +} + +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]); +} + +bool deserializeSequencePresentationsJson(const json_t& root, std::vector& out, std::string* error) +{ + out.clear(); + if (!root.is_array()) + { + if (error) + *error = "Sequence presentations must be an array."; + return false; + } + + for (const auto& entry : root) + { + if (!entry.is_object() || !entry.contains("projection")) + { + if (error) + *error = "Sequence presentation entry missing \"projection\"."; + return false; + } + + nbl::core::CCameraSequencePresentation presentation; + if (!nbl::core::tryParseProjectionType(entry["projection"].get(), presentation.projection)) + { + if (error) + *error = "Sequence presentation has invalid projection type."; + return false; + } + if (entry.contains("left_handed")) + presentation.leftHanded = entry["left_handed"].get(); + out.emplace_back(presentation); + } + + return true; +} + +bool deserializeSequenceContinuityJson(const json_t& root, nbl::core::CCameraSequenceContinuitySettings& out, std::string* error) +{ + if (!root.is_object()) + { + if (error) + *error = "Sequence continuity settings must be an object."; + return false; + } + + out = {}; + if (root.contains("baseline")) + out.baseline = root["baseline"].get(); + if (root.contains("step")) + out.step = root["step"].get(); + + if (root.contains("min_pos_delta")) + { + out.minPosDelta = root["min_pos_delta"].get(); + out.hasPosDeltaConstraint = true; + } + if (root.contains("max_pos_delta")) + { + out.maxPosDelta = root["max_pos_delta"].get(); + out.hasPosDeltaConstraint = true; + } + else if (root.contains("pos_tolerance")) + { + out.maxPosDelta = root["pos_tolerance"].get(); + out.hasPosDeltaConstraint = true; + } + + if (root.contains("min_euler_delta_deg")) + { + out.minEulerDeltaDeg = root["min_euler_delta_deg"].get(); + out.hasEulerDeltaConstraint = true; + } + if (root.contains("max_euler_delta_deg")) + { + out.maxEulerDeltaDeg = root["max_euler_delta_deg"].get(); + out.hasEulerDeltaConstraint = true; + } + else if (root.contains("euler_tolerance_deg")) + { + out.maxEulerDeltaDeg = root["euler_tolerance_deg"].get(); + out.hasEulerDeltaConstraint = true; + } + + if (root.contains("disable_pos_delta")) + out.hasPosDeltaConstraint = !root["disable_pos_delta"].get(); + if (root.contains("disable_euler_delta")) + out.hasEulerDeltaConstraint = !root["disable_euler_delta"].get(); + + if (out.step && !(out.hasPosDeltaConstraint || out.hasEulerDeltaConstraint)) + { + if (error) + *error = "Sequence continuity step checks require at least one delta constraint."; + return false; + } + + return true; +} + +bool deserializeSequenceGoalDeltaJson(const json_t& root, nbl::core::CCameraSequenceGoalDelta& out, std::string* error) +{ + if (!root.is_object()) + { + if (error) + *error = "Sequence keyframe delta must be an object."; + return false; + } + + out = {}; + if (root.contains("position_offset")) + { + readVector3(root["position_offset"], out.positionOffset); + out.hasPositionOffset = true; + } + if (root.contains("rotation_euler_deg_offset")) + { + readVector3(root["rotation_euler_deg_offset"], out.rotationEulerDegOffset); + out.hasRotationEulerDegOffset = true; + } + if (root.contains("target_offset")) + { + readVector3(root["target_offset"], out.targetOffset); + out.hasTargetOffset = true; + } + if (root.contains("orbit_u_delta_deg")) + { + out.orbitUDeltaDeg = root["orbit_u_delta_deg"].get(); + out.hasOrbitUDeltaDeg = true; + } + if (root.contains("orbit_v_delta_deg")) + { + out.orbitVDeltaDeg = root["orbit_v_delta_deg"].get(); + out.hasOrbitVDeltaDeg = true; + } + if (root.contains("orbit_distance_delta")) + { + out.orbitDistanceDelta = root["orbit_distance_delta"].get(); + out.hasOrbitDistanceDelta = true; + } + if (root.contains("path_angle_delta_deg")) + { + out.pathAngleDeltaDeg = root["path_angle_delta_deg"].get(); + out.hasPathAngleDeltaDeg = true; + } + if (root.contains("path_radius_delta")) + { + out.pathRadiusDelta = root["path_radius_delta"].get(); + out.hasPathRadiusDelta = true; + } + if (root.contains("path_height_delta")) + { + out.pathHeightDelta = root["path_height_delta"].get(); + out.hasPathHeightDelta = true; + } + if (root.contains("dynamic_base_fov_delta")) + { + out.dynamicBaseFovDelta = root["dynamic_base_fov_delta"].get(); + out.hasDynamicBaseFovDelta = true; + } + if (root.contains("dynamic_reference_distance_delta")) + { + out.dynamicReferenceDistanceDelta = root["dynamic_reference_distance_delta"].get(); + out.hasDynamicReferenceDistanceDelta = true; + } + + return true; +} + +bool deserializeSequenceKeyframeJson(const json_t& root, nbl::core::CCameraSequenceKeyframe& out, std::string* error) +{ + if (!root.is_object()) + { + if (error) + *error = "Sequence keyframe must be an object."; + return false; + } + + out = {}; + if (root.contains("time")) + out.time = std::max(0.f, root["time"].get()); + + if (root.contains("delta")) + { + if (!deserializeSequenceGoalDeltaJson(root["delta"], out.delta, error)) + return false; + out.hasDelta = true; + } + + if (root.contains("preset")) + { + deserializePresetJson(root["preset"], out.absolutePreset); + out.hasAbsolutePreset = true; + } + else if (root.contains("position") || root.contains("orientation") || root.contains("target_position") || + root.contains("distance") || root.contains("orbit_u") || root.contains("orbit_v") || + root.contains("orbit_distance") || root.contains("path_angle") || root.contains("path_radius") || + root.contains("path_height") || root.contains("dynamic_base_fov") || root.contains("dynamic_reference_distance")) + { + deserializePresetJson(root, out.absolutePreset); + out.hasAbsolutePreset = true; + } + + return true; +} + +bool deserializeSequenceTrackedTargetDeltaJson(const json_t& root, nbl::core::CCameraSequenceTrackedTargetDelta& out, std::string* error) +{ + if (!root.is_object()) + { + if (error) + *error = "Sequence target delta must be an object."; + return false; + } + + out = {}; + if (root.contains("position_offset")) + { + readVector3(root["position_offset"], out.positionOffset); + out.hasPositionOffset = true; + } + if (root.contains("rotation_euler_deg_offset")) + { + readVector3(root["rotation_euler_deg_offset"], out.rotationEulerDegOffset); + out.hasRotationEulerDegOffset = true; + } + + return true; +} + +bool deserializeSequenceTrackedTargetKeyframeJson(const json_t& root, nbl::core::CCameraSequenceTrackedTargetKeyframe& out, std::string* error) +{ + if (!root.is_object()) + { + if (error) + *error = "Sequence target keyframe must be an object."; + return false; + } + + out = {}; + if (root.contains("time")) + out.time = std::max(0.f, root["time"].get()); + + if (root.contains("delta")) + { + if (!deserializeSequenceTrackedTargetDeltaJson(root["delta"], out.delta, error)) + return false; + out.hasDelta = true; + } + + if (root.contains("position")) + { + readVector3(root["position"], out.absolutePosition); + out.hasAbsolutePosition = true; + } + if (root.contains("rotation_euler_deg")) + { + readVector3(root["rotation_euler_deg"], out.absoluteRotationEulerDeg); + out.hasAbsoluteRotationEulerDeg = true; + } + + return true; +} + +bool deserializeSequenceSegmentJson(const json_t& root, nbl::core::CCameraSequenceSegment& out, std::string* error) +{ + if (!root.is_object()) + { + if (error) + *error = "Sequence segment must be an object."; + return false; + } + + out = {}; + if (root.contains("name")) + out.name = root["name"].get(); + if (root.contains("camera_identifier")) + out.cameraIdentifier = root["camera_identifier"].get(); + if (root.contains("camera_kind")) + { + if (!nbl::core::tryParseCameraKind(root["camera_kind"].get(), out.cameraKind)) + { + if (error) + *error = "Sequence segment has invalid camera_kind."; + return false; + } + } + if (root.contains("duration_seconds")) + { + out.durationSeconds = std::max(0.f, root["duration_seconds"].get()); + out.hasDurationSeconds = true; + } + if (root.contains("reset_camera")) + { + out.resetCamera = root["reset_camera"].get(); + out.hasResetCamera = true; + } + if (root.contains("presentations")) + { + if (!deserializeSequencePresentationsJson(root["presentations"], out.presentations, error)) + return false; + } + if (root.contains("continuity")) + { + if (!deserializeSequenceContinuityJson(root["continuity"], out.continuity, error)) + return false; + out.hasContinuity = true; + } + if (root.contains("captures")) + { + if (!root["captures"].is_array()) + { + if (error) + *error = "Sequence segment captures must be an array."; + return false; + } + + out.captureFractions.clear(); + for (const auto& entry : root["captures"]) + { + float fraction = 0.f; + if (!tryParseCaptureFractionJson(entry, fraction)) + { + if (error) + *error = "Sequence segment capture entry is invalid."; + return false; + } + out.captureFractions.emplace_back(fraction); + } + nbl::core::normalizeCaptureFractions(out.captureFractions); + out.hasCaptureFractions = true; + } + if (root.contains("keyframes")) + { + if (!root["keyframes"].is_array()) + { + if (error) + *error = "Sequence segment keyframes must be an array."; + return false; + } + for (const auto& entry : root["keyframes"]) + { + nbl::core::CCameraSequenceKeyframe keyframe; + if (!deserializeSequenceKeyframeJson(entry, keyframe, error)) + return false; + out.keyframes.emplace_back(std::move(keyframe)); + } + } + if (root.contains("target_keyframes")) + { + if (!root["target_keyframes"].is_array()) + { + if (error) + *error = "Sequence segment target_keyframes must be an array."; + return false; + } + for (const auto& entry : root["target_keyframes"]) + { + nbl::core::CCameraSequenceTrackedTargetKeyframe keyframe; + if (!deserializeSequenceTrackedTargetKeyframeJson(entry, keyframe, error)) + return false; + out.targetKeyframes.emplace_back(std::move(keyframe)); + } + } + + if (out.keyframes.empty()) + { + if (error) + *error = "Sequence segment requires at least one keyframe."; + return false; + } + if (out.cameraKind == nbl::core::ICamera::CameraKind::Unknown && out.cameraIdentifier.empty()) + { + if (error) + *error = "Sequence segment requires camera_kind or camera_identifier."; + return false; + } + + return true; +} + +bool deserializeCameraSequenceScriptJson(const json_t& root, nbl::core::CCameraSequenceScript& out, std::string* error) +{ + if (!root.is_object()) + { + if (error) + *error = "Camera sequence script must be an object."; + return false; + } + + out = {}; + if (root.contains("enabled")) + out.enabled = root["enabled"].get(); + if (root.contains("log")) + out.log = root["log"].get(); + if (root.contains("exclusive")) + out.exclusive = root["exclusive"].get(); + if (root.contains("exclusive_input")) + out.exclusive = root["exclusive_input"].get() || out.exclusive; + if (root.contains("hard_fail")) + out.hardFail = root["hard_fail"].get(); + if (root.contains("visual_debug")) + out.visualDebug = root["visual_debug"].get(); + if (root.contains("visual_debug_target_fps")) + out.visualDebugTargetFps = root["visual_debug_target_fps"].get(); + if (root.contains("visual_debug_hold_seconds")) + out.visualDebugHoldSeconds = root["visual_debug_hold_seconds"].get(); + if (root.contains("enableActiveCameraMovement")) + { + out.enableActiveCameraMovement = root["enableActiveCameraMovement"].get(); + out.hasEnableActiveCameraMovement = true; + } + if (root.contains("capture_prefix")) + out.capturePrefix = root["capture_prefix"].get(); + if (root.contains("fps")) + out.fps = std::max(1.f, root["fps"].get()); + + if (root.contains("defaults")) + { + const auto& defaults = root["defaults"]; + if (!defaults.is_object()) + { + if (error) + *error = "Camera sequence defaults must be an object."; + return false; + } + + if (defaults.contains("duration_seconds")) + out.defaults.durationSeconds = std::max(0.f, defaults["duration_seconds"].get()); + if (defaults.contains("reset_camera")) + out.defaults.resetCamera = defaults["reset_camera"].get(); + if (defaults.contains("presentations")) + { + if (!deserializeSequencePresentationsJson(defaults["presentations"], out.defaults.presentations, error)) + return false; + } + if (defaults.contains("continuity")) + { + if (!deserializeSequenceContinuityJson(defaults["continuity"], out.defaults.continuity, error)) + return false; + } + if (defaults.contains("captures")) + { + if (!defaults["captures"].is_array()) + { + if (error) + *error = "Camera sequence default captures must be an array."; + return false; + } + + out.defaults.captureFractions.clear(); + for (const auto& entry : defaults["captures"]) + { + float fraction = 0.f; + if (!tryParseCaptureFractionJson(entry, fraction)) + { + if (error) + *error = "Camera sequence default capture entry is invalid."; + return false; + } + out.defaults.captureFractions.emplace_back(fraction); + } + nbl::core::normalizeCaptureFractions(out.defaults.captureFractions); + } + } + + if (!root.contains("segments") || !root["segments"].is_array()) + { + if (error) + *error = "Camera sequence script requires a \"segments\" array."; + return false; + } + + for (const auto& entry : root["segments"]) + { + nbl::core::CCameraSequenceSegment segment; + if (!deserializeSequenceSegmentJson(entry, segment, error)) + return false; + out.segments.emplace_back(std::move(segment)); + } + + if (out.segments.empty()) + { + if (error) + *error = "Camera sequence script must contain at least one segment."; + return false; + } + + return true; +} + +nbl::hlsl::float32_t4x4 composeScriptedImguizmoTransform( + const std::array& translation, + const std::array& rotationDeg, + const std::array& scale) +{ + return nbl::hlsl::composeTransformMatrix( + nbl::hlsl::float32_t3(translation[0], translation[1], translation[2]), + nbl::hlsl::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) +{ + if (button == "LEFT_BUTTON") + return nbl::ui::EMB_LEFT_BUTTON; + if (button == "RIGHT_BUTTON") + return nbl::ui::EMB_RIGHT_BUTTON; + if (button == "MIDDLE_BUTTON") + return nbl::ui::EMB_MIDDLE_BUTTON; + if (button == "BUTTON_4") + return nbl::ui::EMB_BUTTON_4; + if (button == "BUTTON_5") + return nbl::ui::EMB_BUTTON_5; + return std::nullopt; +} + +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 parseScriptedCaptureFramesJson(const json_t& script, nbl::system::CCameraScriptedInputParseResult& out) +{ + if (!script.contains("capture_frames")) + return; + + for (const auto& frame : script["capture_frames"]) + out.timeline.captureFrames.emplace_back(frame.get()); +} + +void parseScriptedControlOverridesJson(const json_t& controls, nbl::system::CCameraScriptedControlOverrides& out) +{ + if (controls.contains("keyboard_scale")) + { + out.hasKeyboardScale = true; + out.keyboardScale = controls["keyboard_scale"].get(); + } + if (controls.contains("mouse_move_scale")) + { + out.hasMouseMoveScale = true; + out.mouseMoveScale = controls["mouse_move_scale"].get(); + } + if (controls.contains("mouse_scroll_scale")) + { + out.hasMouseScrollScale = true; + out.mouseScrollScale = controls["mouse_scroll_scale"].get(); + } + if (controls.contains("translation_scale")) + { + out.hasTranslationScale = true; + out.translationScale = controls["translation_scale"].get(); + } + if (controls.contains("rotation_scale")) + { + out.hasRotationScale = true; + out.rotationScale = controls["rotation_scale"].get(); + } +} + +bool parseScriptedSequenceIfPresentJson(const json_t& script, nbl::system::CCameraScriptedInputParseResult& out, std::string* error) +{ + if (!script.contains("segments")) + return true; + + nbl::core::CCameraSequenceScript sequence; + if (!deserializeCameraSequenceScriptJson(script, sequence, error)) + return false; + + out.sequence = std::move(sequence); + return true; +} + +void appendScriptedCaptureFrame(nbl::system::CCameraScriptedInputParseResult& out, const uint64_t frame, const bool captureFrame) +{ + if (captureFrame) + out.timeline.captureFrames.emplace_back(frame); +} + +void parseScriptedKeyboardEventJson(const json_t& event, const uint64_t frame, const bool captureFrame, nbl::system::CCameraScriptedInputParseResult& out) +{ + if (!event.contains("key") || !event.contains("action")) + { + nbl::system::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::system::appendScriptedInputParseWarning(out, "Scripted keyboard event has invalid key \"" + keyText + "\"."); + return; + } + + const auto action = parseScriptedKeyboardAction(actionText); + if (!action.has_value()) + { + nbl::system::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::system::CCameraScriptedInputParseResult& out) +{ + if (!event.contains("kind")) + { + nbl::system::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.dx = event.value("dx", 0); + entry.mouse.dy = event.value("dy", 0); + } + else if (kind == "scroll") + { + entry.mouse.type = nbl::system::CCameraScriptedInputEvent::MouseData::Type::Scroll; + entry.mouse.v = event.value("v", 0); + entry.mouse.h = event.value("h", 0); + } + else if (kind == "click") + { + if (!event.contains("button") || !event.contains("action")) + { + nbl::system::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::system::appendScriptedInputParseWarning(out, "Scripted click event has invalid button \"" + buttonText + "\"."); + return; + } + + const auto action = parseScriptedMouseClickAction(actionText); + if (!action.has_value()) + { + nbl::system::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.x = event.value("x", 0); + entry.mouse.y = event.value("y", 0); + } + else + { + nbl::system::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::system::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::system::CCameraScriptedInputEvent::ActionData& action, nbl::system::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::system::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::system::CCameraScriptedInputParseResult& out) +{ + if (!event.contains("action")) + { + nbl::system::appendScriptedInputParseWarning(out, "Scripted action event missing \"action\"."); + return; + } + + const auto actionText = event["action"].get(); + nbl::system::CCameraScriptedInputEvent entry; + entry.frame = frame; + entry.type = nbl::system::CCameraScriptedInputEvent::Type::Action; + + if (actionText == "set_active_render_window") + { + entry.action.kind = nbl::system::CCameraScriptedInputEvent::ActionData::Kind::SetActiveRenderWindow; + entry.action.value = parseScriptedActionIntValue(event); + } + else if (actionText == "set_active_planar") + { + entry.action.kind = nbl::system::CCameraScriptedInputEvent::ActionData::Kind::SetActivePlanar; + entry.action.value = parseScriptedActionIntValue(event); + } + else if (actionText == "set_projection_type") + { + entry.action.kind = nbl::system::CCameraScriptedInputEvent::ActionData::Kind::SetProjectionType; + if (!parseScriptedProjectionActionValue(event, entry.action, out)) + return; + } + else if (actionText == "set_projection_index") + { + entry.action.kind = nbl::system::CCameraScriptedInputEvent::ActionData::Kind::SetProjectionIndex; + entry.action.value = parseScriptedActionIntValue(event); + } + else if (actionText == "set_use_window") + { + entry.action.kind = nbl::system::CCameraScriptedInputEvent::ActionData::Kind::SetUseWindow; + entry.action.value = event.value("value", false) ? 1 : 0; + } + else if (actionText == "set_left_handed") + { + entry.action.kind = nbl::system::CCameraScriptedInputEvent::ActionData::Kind::SetLeftHanded; + entry.action.value = event.value("value", false) ? 1 : 0; + } + else if (actionText == "reset_active_camera") + { + entry.action.kind = nbl::system::CCameraScriptedInputEvent::ActionData::Kind::ResetActiveCamera; + entry.action.value = 1; + } + else + { + nbl::system::appendScriptedInputParseWarning(out, "Scripted action event has invalid action \"" + actionText + "\"."); + return; + } + + out.timeline.events.emplace_back(std::move(entry)); + appendScriptedCaptureFrame(out, frame, captureFrame); +} + +void parseScriptedInputEventJson(const json_t& event, nbl::system::CCameraScriptedInputParseResult& out) +{ + if (!event.contains("frame") || !event.contains("type")) + { + nbl::system::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::system::appendScriptedInputParseWarning(out, "Scripted input event has invalid type \"" + type + "\"."); +} + +void parseScriptedInputEventsJson(const json_t& script, nbl::system::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::system::CCameraScriptedInputParseResult& out) +{ + outCheck.kind = nbl::system::CCameraScriptedInputCheck::Kind::ImguizmoVirtual; + outCheck.tolerance = check.value("tolerance", outCheck.tolerance); + + if (!check.contains("events")) + { + nbl::system::appendScriptedInputParseWarning(out, "Imguizmo virtual check missing \"events\"."); + return false; + } + + for (const auto& expectedEvent : check["events"]) + { + if (!expectedEvent.contains("type") || !expectedEvent.contains("magnitude")) + { + nbl::system::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::system::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::system::CCameraScriptedInputParseResult& out) +{ + if (!check.contains("frame") || !check.contains("kind")) + { + nbl::system::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::system::appendScriptedInputParseWarning(out, "gimbal_step check requires at least one delta constraint."); + return false; + } + } + else + { + nbl::system::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::system::CCameraScriptedInputParseResult& out) +{ + if (!script.contains("checks")) + return; + + for (const auto& check : script["checks"]) + parseScriptedCheckJson(check, out); +} +} // anonymous namespace + +namespace nbl::system +{ + +bool readCameraSequenceScript(std::istream& in, core::CCameraSequenceScript& out, std::string* error) +{ + if (!in) + { + if (error) + *error = "Input stream is not readable."; + return false; + } + + json_t root; + in >> root; + return deserializeCameraSequenceScriptJson(root, out, error); +} + +bool loadCameraSequenceScriptFromFile(const path& filePath, core::CCameraSequenceScript& out, std::string* error) +{ + std::ifstream in(filePath.string(), std::ios::binary); + if (!in.is_open()) + { + if (error) + *error = "Cannot open camera sequence script file."; + return false; + } + + return readCameraSequenceScript(in, out, error); +} + +bool readCameraScriptedInput(std::istream& in, CCameraScriptedInputParseResult& out, std::string* error) +{ + if (!in) + { + if (error) + *error = "Input stream is not readable."; + return false; + } + + json_t script; + in >> script; + + 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"; + + parseScriptedCaptureFramesJson(script, out); + + if (script.contains("camera_controls")) + parseScriptedControlOverridesJson(script["camera_controls"], out.cameraControls); + + if (!parseScriptedSequenceIfPresentJson(script, out, error)) + return false; + + parseScriptedInputEventsJson(script, out); + parseScriptedChecksJson(script, out); + + finalizeScriptedTimeline(out.timeline); + return true; +} + +bool loadCameraScriptedInputFromFile(const path& filePath, CCameraScriptedInputParseResult& out, std::string* error) +{ + std::ifstream in(filePath.string(), std::ios::binary); + if (!in.is_open()) + { + if (error) + *error = "Cannot open scripted input file."; + return false; + } + + return readCameraScriptedInput(in, out, error); +} + +} // namespace nbl::system From 1a95be48f6491f72c37576c2001c3029de7762a8 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Tue, 7 Apr 2026 12:58:30 +0200 Subject: [PATCH 172/205] Extract camera event contract and hide binding storage --- .../include/camera/CCameraScriptedRuntime.hpp | 2 +- common/include/camera/CVirtualGimbalEvent.hpp | 129 ++++++++++++++++++ common/include/camera/IGimbal.hpp | 116 +--------------- .../include/camera/IGimbalBindingLayout.hpp | 42 +++--- common/include/camera/IPlanarProjection.hpp | 4 +- common/include/camera/README.md | 9 +- 6 files changed, 161 insertions(+), 141 deletions(-) create mode 100644 common/include/camera/CVirtualGimbalEvent.hpp diff --git a/common/include/camera/CCameraScriptedRuntime.hpp b/common/include/camera/CCameraScriptedRuntime.hpp index e3d8b85d4..e3fe511ae 100644 --- a/common/include/camera/CCameraScriptedRuntime.hpp +++ b/common/include/camera/CCameraScriptedRuntime.hpp @@ -11,7 +11,7 @@ #include #include "CCameraGoal.hpp" -#include "IGimbal.hpp" +#include "CVirtualGimbalEvent.hpp" #include "nbl/ui/KeyCodes.h" namespace nbl::system diff --git a/common/include/camera/CVirtualGimbalEvent.hpp b/common/include/camera/CVirtualGimbalEvent.hpp new file mode 100644 index 000000000..0d43ac7cd --- /dev/null +++ b/common/include/camera/CVirtualGimbalEvent.hpp @@ -0,0 +1,129 @@ +#ifndef _NBL_C_VIRTUAL_GIMBAL_EVENT_HPP_ +#define _NBL_C_VIRTUAL_GIMBAL_EVENT_HPP_ + +#include +#include +#include + +#include "nbl/builtin/hlsl/cpp_compat/vector.hlsl" +#include "nbl/core/math/intutil.h" + +namespace nbl::core +{ + +/** +* Shared semantic camera command. +* +* Input processors and scripted tools emit these events. +* Camera implementations consume them through `ICamera::manipulate(...)`. +*/ +struct CVirtualGimbalEvent +{ + enum VirtualEventType : uint32_t + { + None = 0, + + MoveForward = core::createBitmask({ 0 }), + MoveBackward = core::createBitmask({ 1 }), + MoveLeft = core::createBitmask({ 2 }), + MoveRight = core::createBitmask({ 3 }), + MoveUp = core::createBitmask({ 4 }), + MoveDown = core::createBitmask({ 5 }), + TiltUp = core::createBitmask({ 6 }), + TiltDown = core::createBitmask({ 7 }), + PanLeft = core::createBitmask({ 8 }), + PanRight = core::createBitmask({ 9 }), + RollLeft = core::createBitmask({ 10 }), + RollRight = core::createBitmask({ 11 }), + ScaleXInc = core::createBitmask({ 12 }), + ScaleXDec = core::createBitmask({ 13 }), + ScaleYInc = core::createBitmask({ 14 }), + ScaleYDec = core::createBitmask({ 15 }), + ScaleZInc = core::createBitmask({ 16 }), + ScaleZDec = core::createBitmask({ 17 }), + + EventsCount = 18, + + Translate = MoveForward | MoveBackward | MoveLeft | MoveRight | MoveUp | MoveDown, + Rotate = TiltUp | TiltDown | PanLeft | PanRight | RollLeft | RollRight, + Scale = ScaleXInc | ScaleXDec | ScaleYInc | ScaleYDec | ScaleZInc | ScaleZDec, + + All = Translate | Rotate | Scale + }; + + using manipulation_encode_t = hlsl::float64_t; + + VirtualEventType type = None; + manipulation_encode_t magnitude = {}; + + static constexpr std::string_view virtualEventToString(VirtualEventType event) + { + switch (event) + { + case MoveForward: return "MoveForward"; + case MoveBackward: return "MoveBackward"; + case MoveLeft: return "MoveLeft"; + case MoveRight: return "MoveRight"; + case MoveUp: return "MoveUp"; + case MoveDown: return "MoveDown"; + case TiltUp: return "TiltUp"; + case TiltDown: return "TiltDown"; + case PanLeft: return "PanLeft"; + case PanRight: return "PanRight"; + case RollLeft: return "RollLeft"; + case RollRight: return "RollRight"; + case ScaleXInc: return "ScaleXInc"; + case ScaleXDec: return "ScaleXDec"; + case ScaleYInc: return "ScaleYInc"; + case ScaleYDec: return "ScaleYDec"; + case ScaleZInc: return "ScaleZInc"; + case ScaleZDec: return "ScaleZDec"; + case Translate: return "Translate"; + case Rotate: return "Rotate"; + case Scale: return "Scale"; + case None: return "None"; + default: return "Unknown"; + } + } + + static constexpr VirtualEventType stringToVirtualEvent(std::string_view event) + { + if (event == "MoveForward") return MoveForward; + if (event == "MoveBackward") return MoveBackward; + if (event == "MoveLeft") return MoveLeft; + if (event == "MoveRight") return MoveRight; + if (event == "MoveUp") return MoveUp; + if (event == "MoveDown") return MoveDown; + if (event == "TiltUp") return TiltUp; + if (event == "TiltDown") return TiltDown; + if (event == "PanLeft") return PanLeft; + if (event == "PanRight") return PanRight; + if (event == "RollLeft") return RollLeft; + if (event == "RollRight") return RollRight; + if (event == "ScaleXInc") return ScaleXInc; + if (event == "ScaleXDec") return ScaleXDec; + if (event == "ScaleYInc") return ScaleYInc; + if (event == "ScaleYDec") return ScaleYDec; + if (event == "ScaleZInc") return ScaleZInc; + if (event == "ScaleZDec") return ScaleZDec; + if (event == "Translate") return Translate; + if (event == "Rotate") return Rotate; + if (event == "Scale") return Scale; + if (event == "None") return None; + return None; + } + + static inline constexpr auto VirtualEventsTypeTable = []() + { + std::array output; + + for (uint16_t i = 0u; i < EventsCount; ++i) + output[i] = static_cast(core::createBitmask({ i })); + + return output; + }(); +}; + +} // namespace nbl::core + +#endif // _NBL_C_VIRTUAL_GIMBAL_EVENT_HPP_ diff --git a/common/include/camera/IGimbal.hpp b/common/include/camera/IGimbal.hpp index f68790a7b..0e3cb1db4 100644 --- a/common/include/camera/IGimbal.hpp +++ b/common/include/camera/IGimbal.hpp @@ -2,6 +2,7 @@ #define _NBL_IGIMBAL_HPP_ #include "CCameraMathUtilities.hpp" +#include "CVirtualGimbalEvent.hpp" namespace nbl::core { @@ -47,121 +48,6 @@ namespace nbl::core using hlsl::slerpQuaternion; using hlsl::wrapAngleRad; - /** - * Shared semantic camera command. - * - * Input processors and scripted tools emit these events. - * Camera implementations consume them through `ICamera::manipulate(...)`. - */ - struct CVirtualGimbalEvent - { - enum VirtualEventType : uint32_t - { - None = 0, - - // Individual events - MoveForward = core::createBitmask({ 0 }), - MoveBackward = core::createBitmask({ 1 }), - MoveLeft = core::createBitmask({ 2 }), - MoveRight = core::createBitmask({ 3 }), - MoveUp = core::createBitmask({ 4 }), - MoveDown = core::createBitmask({ 5 }), - TiltUp = core::createBitmask({ 6 }), - TiltDown = core::createBitmask({ 7 }), - PanLeft = core::createBitmask({ 8 }), - PanRight = core::createBitmask({ 9 }), - RollLeft = core::createBitmask({ 10 }), - RollRight = core::createBitmask({ 11 }), - ScaleXInc = core::createBitmask({ 12 }), - ScaleXDec = core::createBitmask({ 13 }), - ScaleYInc = core::createBitmask({ 14 }), - ScaleYDec = core::createBitmask({ 15 }), - ScaleZInc = core::createBitmask({ 16 }), - ScaleZDec = core::createBitmask({ 17 }), - - EventsCount = 18, - - // Grouped bitmasks - Translate = MoveForward | MoveBackward | MoveLeft | MoveRight | MoveUp | MoveDown, - Rotate = TiltUp | TiltDown | PanLeft | PanRight | RollLeft | RollRight, - Scale = ScaleXInc | ScaleXDec | ScaleYInc | ScaleYDec | ScaleZInc | ScaleZDec, - - All = Translate | Rotate | Scale - }; - - using manipulation_encode_t = float64_t; - - VirtualEventType type = None; - manipulation_encode_t magnitude = {}; - - static constexpr std::string_view virtualEventToString(VirtualEventType event) - { - switch (event) - { - case MoveForward: return "MoveForward"; - case MoveBackward: return "MoveBackward"; - case MoveLeft: return "MoveLeft"; - case MoveRight: return "MoveRight"; - case MoveUp: return "MoveUp"; - case MoveDown: return "MoveDown"; - case TiltUp: return "TiltUp"; - case TiltDown: return "TiltDown"; - case PanLeft: return "PanLeft"; - case PanRight: return "PanRight"; - case RollLeft: return "RollLeft"; - case RollRight: return "RollRight"; - case ScaleXInc: return "ScaleXInc"; - case ScaleXDec: return "ScaleXDec"; - case ScaleYInc: return "ScaleYInc"; - case ScaleYDec: return "ScaleYDec"; - case ScaleZInc: return "ScaleZInc"; - case ScaleZDec: return "ScaleZDec"; - case Translate: return "Translate"; - case Rotate: return "Rotate"; - case Scale: return "Scale"; - case None: return "None"; - default: return "Unknown"; - } - } - - static constexpr VirtualEventType stringToVirtualEvent(std::string_view event) - { - if (event == "MoveForward") return MoveForward; - if (event == "MoveBackward") return MoveBackward; - if (event == "MoveLeft") return MoveLeft; - if (event == "MoveRight") return MoveRight; - if (event == "MoveUp") return MoveUp; - if (event == "MoveDown") return MoveDown; - if (event == "TiltUp") return TiltUp; - if (event == "TiltDown") return TiltDown; - if (event == "PanLeft") return PanLeft; - if (event == "PanRight") return PanRight; - if (event == "RollLeft") return RollLeft; - if (event == "RollRight") return RollRight; - if (event == "ScaleXInc") return ScaleXInc; - if (event == "ScaleXDec") return ScaleXDec; - if (event == "ScaleYInc") return ScaleYInc; - if (event == "ScaleYDec") return ScaleYDec; - if (event == "ScaleZInc") return ScaleZInc; - if (event == "ScaleZDec") return ScaleZDec; - if (event == "Translate") return Translate; - if (event == "Rotate") return Rotate; - if (event == "Scale") return Scale; - if (event == "None") return None; - return None; - } - - static inline constexpr auto VirtualEventsTypeTable = []() - { - std::array output; - - for (uint16_t i = 0u; i < EventsCount; ++i) - output[i] = static_cast(core::createBitmask({ i })); - - return output; - }(); - }; - struct CReferenceTransform { float64_t4x4 frame; diff --git a/common/include/camera/IGimbalBindingLayout.hpp b/common/include/camera/IGimbalBindingLayout.hpp index 5268720a8..2bc8e86da 100644 --- a/common/include/camera/IGimbalBindingLayout.hpp +++ b/common/include/camera/IGimbalBindingLayout.hpp @@ -1,7 +1,11 @@ #ifndef _NBL_I_GIMBAL_BINDING_LAYOUT_HPP_ #define _NBL_I_GIMBAL_BINDING_LAYOUT_HPP_ -#include "IGimbal.hpp" +#include +#include + +#include "CVirtualGimbalEvent.hpp" +#include "nbl/ui/KeyCodes.h" namespace nbl::ui { @@ -67,24 +71,6 @@ struct IGimbalBindingLayout virtual void updateKeyboardMapping(const std::function& mapKeys) = 0; virtual void updateMouseMapping(const std::function& mapKeys) = 0; virtual void updateImguizmoMapping(const std::function& mapKeys) = 0; -}; - -class CGimbalBindingLayoutStorage : public IGimbalBindingLayout -{ -public: - //! Mutable storage for active or preset binding layout. - using IGimbalBindingLayout::IGimbalBindingLayout; - - CGimbalBindingLayoutStorage() {} - virtual ~CGimbalBindingLayoutStorage() {} - - virtual void updateKeyboardMapping(const std::function& mapKeys) override { mapKeys(m_keyboardVirtualEventMap); } - virtual void updateMouseMapping(const std::function& mapKeys) override { mapKeys(m_mouseVirtualEventMap); } - virtual void updateImguizmoMapping(const std::function& mapKeys) override { mapKeys(m_imguizmoVirtualEventMap); } - - virtual const keyboard_to_virtual_events_t& getKeyboardVirtualEventMap() const override { return m_keyboardVirtualEventMap; } - virtual const mouse_to_virtual_events_t& getMouseVirtualEventMap() const override { return m_mouseVirtualEventMap; } - virtual const imguizmo_to_virtual_events_t& getImguizmoVirtualEventMap() const override { return m_imguizmoVirtualEventMap; } inline void copyBindingLayoutFrom(const IGimbalBindingLayout& layout) { @@ -109,6 +95,24 @@ class CGimbalBindingLayoutStorage : public IGimbalBindingLayout result.emplace(code, typename Map::mapped_type(hash.event.type)); return result; } +}; + +class CGimbalBindingLayoutStorage : public IGimbalBindingLayout +{ +public: + //! Mutable storage for active or preset binding layout. + using IGimbalBindingLayout::IGimbalBindingLayout; + + CGimbalBindingLayoutStorage() {} + virtual ~CGimbalBindingLayoutStorage() {} + + virtual void updateKeyboardMapping(const std::function& mapKeys) override { mapKeys(m_keyboardVirtualEventMap); } + virtual void updateMouseMapping(const std::function& mapKeys) override { mapKeys(m_mouseVirtualEventMap); } + virtual void updateImguizmoMapping(const std::function& mapKeys) override { mapKeys(m_imguizmoVirtualEventMap); } + + virtual const keyboard_to_virtual_events_t& getKeyboardVirtualEventMap() const override { return m_keyboardVirtualEventMap; } + virtual const mouse_to_virtual_events_t& getMouseVirtualEventMap() const override { return m_mouseVirtualEventMap; } + virtual const imguizmo_to_virtual_events_t& getImguizmoVirtualEventMap() const override { return m_imguizmoVirtualEventMap; } keyboard_to_virtual_events_t m_keyboardVirtualEventMap; mouse_to_virtual_events_t m_mouseVirtualEventMap; diff --git a/common/include/camera/IPlanarProjection.hpp b/common/include/camera/IPlanarProjection.hpp index d34c7a48c..ba19fea6d 100644 --- a/common/include/camera/IPlanarProjection.hpp +++ b/common/include/camera/IPlanarProjection.hpp @@ -112,8 +112,8 @@ class IPlanarProjection : public ILinearProjection } inline const ProjectionParameters& getParameters() const { return m_parameters; } - inline const ui::CGimbalBindingLayoutStorage& getInputBinding() const { return m_inputBinding; } - inline ui::CGimbalBindingLayoutStorage& getInputBinding() { return m_inputBinding; } + inline const ui::IGimbalBindingLayout& getInputBinding() const { return m_inputBinding; } + inline ui::IGimbalBindingLayout& getInputBinding() { return m_inputBinding; } private: CProjection() = default; ProjectionParameters m_parameters; diff --git a/common/include/camera/README.md b/common/include/camera/README.md index 68615b93f..a7daa2ae4 100644 --- a/common/include/camera/README.md +++ b/common/include/camera/README.md @@ -136,7 +136,7 @@ ICamera <-> CCameraGoal <-> CCameraPreset <-> CCameraKeyframeTrack - <-> CCameraPlaybackCursor + <-> CCameraPlaybackTimeline <-> CCameraSequenceScript ``` @@ -163,13 +163,14 @@ CCameraSequenceScript ### 1. Gimbal and semantic commands +- [`CVirtualGimbalEvent.hpp`](CVirtualGimbalEvent.hpp) - [`IGimbal.hpp`](IGimbal.hpp) This is the mathematical foundation. It defines: -- `CVirtualGimbalEvent` +- the shared semantic event language in `CVirtualGimbalEvent` - low-level gimbal math - accumulation of multiple semantic commands into one camera impulse @@ -222,7 +223,7 @@ Important properties: - `CameraCapability` - `GoalStateMask` - motion config -- default input binding config +- typed state hooks used by tooling ### 5. Projection layer @@ -236,7 +237,7 @@ This layer handles projection state. Important rule: -- projection may own binding layout storage +- projection may own viewport-local binding layout state - projection does not own raw input processing That separation was one of the major cleanup goals of the refactor. From 4ae1e9c5abb43589c912c48331ab13ac7c702047 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Tue, 7 Apr 2026 13:10:53 +0200 Subject: [PATCH 173/205] Move camera presentation helpers to ui --- 61_UI/AppControlPanel.cpp | 6 +- 61_UI/AppInit.cpp | 22 ++-- 61_UI/include/app/App.hpp | 6 +- 61_UI/include/common.hpp | 16 +-- .../camera/CCameraPresentationUtilities.hpp | 20 ++-- .../include/camera/CCameraTextUtilities.hpp | 108 +++++++++--------- common/include/camera/README.md | 4 +- 7 files changed, 91 insertions(+), 91 deletions(-) diff --git a/61_UI/AppControlPanel.cpp b/61_UI/AppControlPanel.cpp index f31b3b443..a7dfaa51d 100644 --- a/61_UI/AppControlPanel.cpp +++ b/61_UI/AppControlPanel.cpp @@ -806,9 +806,9 @@ void App::DrawControlPanel() if (!m_presets.empty()) { const char* presetFilterLabels[] = { - nbl::core::getPresetApplyPresentationFilterLabel(PresetFilterMode::All), - nbl::core::getPresetApplyPresentationFilterLabel(PresetFilterMode::Exact), - nbl::core::getPresetApplyPresentationFilterLabel(PresetFilterMode::BestEffort) + nbl::ui::getPresetApplyPresentationFilterLabel(PresetFilterMode::All), + nbl::ui::getPresetApplyPresentationFilterLabel(PresetFilterMode::Exact), + nbl::ui::getPresetApplyPresentationFilterLabel(PresetFilterMode::BestEffort) }; int presetFilterIx = static_cast(m_presetFilterMode); if (ImGui::Combo("Visibility", &presetFilterIx, presetFilterLabels, IM_ARRAYSIZE(presetFilterLabels))) diff --git a/61_UI/AppInit.cpp b/61_UI/AppInit.cpp index ac1213ecf..d345dcc90 100644 --- a/61_UI/AppInit.cpp +++ b/61_UI/AppInit.cpp @@ -1338,14 +1338,14 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) if (hasOrbitPreset) { - if (std::string_view(nbl::core::getPresetApplyPresentationFilterLabel(EPresetApplyPresentationFilter::All)) != "All" || - std::string_view(nbl::core::getPresetApplyPresentationFilterLabel(EPresetApplyPresentationFilter::Exact)) != "Exact" || - std::string_view(nbl::core::getPresetApplyPresentationFilterLabel(EPresetApplyPresentationFilter::BestEffort)) != "Best-effort") + if (std::string_view(nbl::ui::getPresetApplyPresentationFilterLabel(EPresetApplyPresentationFilter::All)) != "All" || + std::string_view(nbl::ui::getPresetApplyPresentationFilterLabel(EPresetApplyPresentationFilter::Exact)) != "Exact" || + std::string_view(nbl::ui::getPresetApplyPresentationFilterLabel(EPresetApplyPresentationFilter::BestEffort)) != "Best-effort") { return fail("Presentation utilities smoke returned an unexpected filter label."); } - const auto blockedPresentation = nbl::core::analyzePresetPresentation(m_cameraGoalSolver, nullptr, initialOrbitPreset); + const auto blockedPresentation = nbl::ui::analyzePresetPresentation(m_cameraGoalSolver, nullptr, initialOrbitPreset); if (blockedPresentation.matchesFilter(EPresetApplyPresentationFilter::Exact) || blockedPresentation.matchesFilter(EPresetApplyPresentationFilter::BestEffort)) { @@ -1354,13 +1354,13 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) if (blockedPresentation.sourceKindLabel.empty() || blockedPresentation.goalStateLabel.empty()) return fail("Presentation utilities smoke produced empty blocked presentation labels."); - const auto blockedBadges = nbl::core::collectGoalApplyPresentationBadges(blockedPresentation); + const auto blockedBadges = nbl::ui::collectGoalApplyPresentationBadges(blockedPresentation); if (!blockedBadges.blocked || blockedBadges.exact || blockedBadges.bestEffort || blockedPresentation.badges.blocked != blockedBadges.blocked) return fail("Presentation utilities smoke produced wrong blocked badge flags."); if (orbitCamera) { - const auto exactPresentation = nbl::core::analyzePresetPresentation(m_cameraGoalSolver, orbitCamera, initialOrbitPreset); + const auto exactPresentation = nbl::ui::analyzePresetPresentation(m_cameraGoalSolver, orbitCamera, initialOrbitPreset); if (!exactPresentation.matchesFilter(EPresetApplyPresentationFilter::All) || !exactPresentation.matchesFilter(EPresetApplyPresentationFilter::Exact) || exactPresentation.matchesFilter(EPresetApplyPresentationFilter::BestEffort)) @@ -1368,13 +1368,13 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) return fail("Presentation utilities smoke failed exact filtering."); } - const auto exactBadges = nbl::core::collectGoalApplyPresentationBadges(exactPresentation); + const auto exactBadges = nbl::ui::collectGoalApplyPresentationBadges(exactPresentation); if (!exactBadges.exact || exactBadges.bestEffort || exactBadges.dropsState || exactBadges.sharedStateOnly || exactBadges.blocked) return fail("Presentation utilities smoke produced wrong exact badge flags."); if (exactPresentation.sourceKindLabel.empty() || exactPresentation.goalStateLabel.empty()) return fail("Presentation utilities smoke produced empty exact presentation labels."); - const auto capturePresentation = nbl::core::analyzeCapturePresentation(m_cameraGoalSolver, orbitCamera); + const auto capturePresentation = nbl::ui::analyzeCapturePresentation(m_cameraGoalSolver, orbitCamera); if (!capturePresentation.canCapture || capturePresentation.policyLabel.empty()) return fail("Presentation utilities smoke failed orbit capture presentation."); } @@ -1382,7 +1382,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) if (hasOrbitPreset && hasPathPreset && orbitCamera) { - const auto approximatePresentation = nbl::core::analyzePresetPresentation(m_cameraGoalSolver, orbitCamera, initialPathPreset); + const auto approximatePresentation = nbl::ui::analyzePresetPresentation(m_cameraGoalSolver, orbitCamera, initialPathPreset); if (!approximatePresentation.matchesFilter(EPresetApplyPresentationFilter::All) || approximatePresentation.matchesFilter(EPresetApplyPresentationFilter::Exact) || !approximatePresentation.matchesFilter(EPresetApplyPresentationFilter::BestEffort)) @@ -1390,7 +1390,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) return fail("Presentation utilities smoke failed best-effort filtering."); } - const auto approximateBadges = nbl::core::collectGoalApplyPresentationBadges(approximatePresentation); + const auto approximateBadges = nbl::ui::collectGoalApplyPresentationBadges(approximatePresentation); if (approximateBadges.exact || !approximateBadges.bestEffort || !approximateBadges.dropsState || approximateBadges.sharedStateOnly || approximateBadges.blocked) return fail("Presentation utilities smoke produced wrong best-effort badge flags."); if (approximatePresentation.sourceKindLabel.empty() || approximatePresentation.goalStateLabel.empty()) @@ -1854,7 +1854,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) summary.targetCount = 2u; summary.successCount = 2u; summary.approximateCount = 1u; - const auto summaryText = nbl::core::describePresetApplySummary(summary, "none"); + const auto summaryText = nbl::ui::describePresetApplySummary(summary, "none"); if (summaryText.find("targets=2") == std::string::npos || summaryText.find("approximate=1") == std::string::npos) return fail("Camera text utilities smoke failed for preset-apply summary description."); } diff --git a/61_UI/include/app/App.hpp b/61_UI/include/app/App.hpp index a9119e27d..63827be92 100644 --- a/61_UI/include/app/App.hpp +++ b/61_UI/include/app/App.hpp @@ -1346,12 +1346,12 @@ class App final : public examples::SimpleWindowedApplication inline PresetUiAnalysis analyzePresetForUi(ICamera* camera, const CameraPreset& preset) const { - return nbl::core::analyzePresetPresentation(m_cameraGoalSolver, camera, preset); + return nbl::ui::analyzePresetPresentation(m_cameraGoalSolver, camera, preset); } inline CaptureUiAnalysis analyzeCameraCaptureForUi(ICamera* camera) const { - return nbl::core::analyzeCapturePresentation(m_cameraGoalSolver, camera); + return nbl::ui::analyzeCapturePresentation(m_cameraGoalSolver, camera); } inline CCameraGoalSolver::SCompatibilityResult analyzePresetCompatibility(ICamera* camera, const CameraPreset& preset) const @@ -1394,7 +1394,7 @@ class App final : public examples::SimpleWindowedApplication inline void storePlaybackApplySummary(const SCameraPresetApplySummary& summary) { storeApplyStatusBanner(m_playbackApplyBanner, - nbl::core::describePresetApplySummary(summary, m_playbackAffectsAll ? "Playback apply | no cameras available" : "Playback apply | no active camera"), + nbl::ui::describePresetApplySummary(summary, m_playbackAffectsAll ? "Playback apply | no cameras available" : "Playback apply | no active camera"), summary.succeeded(), summary.approximate()); } diff --git a/61_UI/include/common.hpp b/61_UI/include/common.hpp index e7240f731..3aa57a92a 100644 --- a/61_UI/include/common.hpp +++ b/61_UI/include/common.hpp @@ -159,13 +159,13 @@ using nbl::core::SCameraGoalApplyAnalysis; using nbl::core::SCameraCaptureAnalysis; using nbl::core::SCameraFollowConfig; using nbl::core::SCameraFollowVisualMetrics; -using nbl::core::SCameraGoalApplyPresentation; -using nbl::core::SCameraGoalApplyPresentationBadges; -using nbl::core::SCameraCapturePresentation; +using nbl::ui::SCameraGoalApplyPresentation; +using nbl::ui::SCameraGoalApplyPresentationBadges; +using nbl::ui::SCameraCapturePresentation; using nbl::core::SCameraConstraintSettings; using nbl::core::CCameraGoalSolver; using nbl::core::ECameraFollowMode; -using nbl::core::EPresetApplyPresentationFilter; +using nbl::ui::EPresetApplyPresentationFilter; using nbl::core::IPlanarProjection; using nbl::core::CPlanarProjection; using nbl::core::CVirtualGimbalEvent; @@ -184,11 +184,11 @@ using nbl::hlsl::float64_t4; using nbl::hlsl::float64_t4x4; using nbl::hlsl::uint16_t2; using nbl::hlsl::getQuaternionEulerDegrees; -using nbl::core::describeApplyResult; -using nbl::core::describeGoalStateMask; +using nbl::ui::describeApplyResult; +using nbl::ui::describeGoalStateMask; using nbl::core::getCameraFollowModeLabel; -using nbl::core::getCameraTypeDescription; -using nbl::core::getCameraTypeLabel; +using nbl::ui::getCameraTypeDescription; +using nbl::ui::getCameraTypeLabel; using nbl::hlsl::getCastedMatrix; using nbl::hlsl::getCastedVector; using nbl::hlsl::getMatrix3x4As4x4; diff --git a/common/include/camera/CCameraPresentationUtilities.hpp b/common/include/camera/CCameraPresentationUtilities.hpp index f6f818a10..a1b1c83d7 100644 --- a/common/include/camera/CCameraPresentationUtilities.hpp +++ b/common/include/camera/CCameraPresentationUtilities.hpp @@ -9,7 +9,7 @@ #include "CCameraTextUtilities.hpp" -namespace nbl::core +namespace nbl::ui { //! Shared exactness-oriented filter used by preset presentation surfaces. @@ -31,7 +31,7 @@ struct SCameraGoalApplyPresentationBadges final }; //! Presentation-ready wrapper around analyzed goal apply compatibility. -struct SCameraGoalApplyPresentation final : SCameraGoalApplyAnalysis +struct SCameraGoalApplyPresentation final : core::SCameraGoalApplyAnalysis { SCameraGoalApplyPresentationBadges badges; std::string sourceKindLabel; @@ -56,7 +56,7 @@ struct SCameraGoalApplyPresentation final : SCameraGoalApplyAnalysis }; //! Presentation-ready wrapper around analyzed camera capture viability. -struct SCameraCapturePresentation final : SCameraCaptureAnalysis +struct SCameraCapturePresentation final : core::SCameraCaptureAnalysis { std::string policyLabel; }; @@ -80,10 +80,10 @@ inline const char* getPresetApplyPresentationFilterLabel(const EPresetApplyPrese } //! Build presentation text for one analyzed goal-apply result. -inline SCameraGoalApplyPresentation makeGoalApplyPresentation(const SCameraGoalApplyAnalysis& analysis, const ICamera* targetCamera) +inline SCameraGoalApplyPresentation makeGoalApplyPresentation(const core::SCameraGoalApplyAnalysis& analysis, const core::ICamera* targetCamera) { SCameraGoalApplyPresentation presentation; - static_cast(presentation) = analysis; + static_cast(presentation) = analysis; presentation.badges = collectGoalApplyPresentationBadges(presentation); presentation.sourceKindLabel = std::string(getCameraTypeLabel(presentation.goal.sourceKind)); presentation.goalStateLabel = describeGoalStateMask(presentation.goal.sourceGoalStateMask); @@ -93,9 +93,9 @@ inline SCameraGoalApplyPresentation makeGoalApplyPresentation(const SCameraGoalA } //! Analyze one preset against one camera and return reusable presentation data. -inline SCameraGoalApplyPresentation analyzePresetPresentation(const CCameraGoalSolver& solver, const ICamera* camera, const CCameraPreset& preset) +inline SCameraGoalApplyPresentation analyzePresetPresentation(const core::CCameraGoalSolver& solver, const core::ICamera* camera, const core::CCameraPreset& preset) { - return makeGoalApplyPresentation(analyzePresetApply(solver, camera, preset), camera); + return makeGoalApplyPresentation(core::analyzePresetApply(solver, camera, preset), camera); } //! Build reusable badge flags for one preset/keyframe compatibility answer. @@ -111,14 +111,14 @@ inline SCameraGoalApplyPresentationBadges collectGoalApplyPresentationBadges(con } //! Analyze one camera capture path and return reusable presentation data. -inline SCameraCapturePresentation analyzeCapturePresentation(const CCameraGoalSolver& solver, ICamera* camera) +inline SCameraCapturePresentation analyzeCapturePresentation(const core::CCameraGoalSolver& solver, core::ICamera* camera) { SCameraCapturePresentation presentation; - static_cast(presentation) = analyzeCameraCapture(solver, camera); + static_cast(presentation) = core::analyzeCameraCapture(solver, camera); presentation.policyLabel = describeCameraCapturePolicy(presentation, camera); return presentation; } -} // namespace nbl::core +} // namespace nbl::ui #endif // _C_CAMERA_PRESENTATION_UTILITIES_HPP_ diff --git a/common/include/camera/CCameraTextUtilities.hpp b/common/include/camera/CCameraTextUtilities.hpp index 70b7f2a04..323cc9cd4 100644 --- a/common/include/camera/CCameraTextUtilities.hpp +++ b/common/include/camera/CCameraTextUtilities.hpp @@ -12,57 +12,57 @@ #include "CCameraGoalAnalysis.hpp" #include "CCameraPresetFlow.hpp" -namespace nbl::core +namespace nbl::ui { //! Return a short human-readable label for a camera kind. -inline std::string_view getCameraTypeLabel(const ICamera::CameraKind kind) +inline std::string_view getCameraTypeLabel(const core::ICamera::CameraKind kind) { switch (kind) { - case ICamera::CameraKind::FPS: return "FPS"; - case ICamera::CameraKind::Free: return "Free"; - case ICamera::CameraKind::Orbit: return "Orbit"; - case ICamera::CameraKind::Arcball: return "Arcball"; - case ICamera::CameraKind::Turntable: return "Turntable"; - case ICamera::CameraKind::TopDown: return "TopDown"; - case ICamera::CameraKind::Isometric: return "Isometric"; - case ICamera::CameraKind::Chase: return "Chase"; - case ICamera::CameraKind::Dolly: return "Dolly"; - case ICamera::CameraKind::DollyZoom: return "Dolly Zoom"; - case ICamera::CameraKind::Path: return "Path"; + case core::ICamera::CameraKind::FPS: return "FPS"; + case core::ICamera::CameraKind::Free: return "Free"; + case core::ICamera::CameraKind::Orbit: return "Orbit"; + case core::ICamera::CameraKind::Arcball: return "Arcball"; + case core::ICamera::CameraKind::Turntable: return "Turntable"; + case core::ICamera::CameraKind::TopDown: return "TopDown"; + case core::ICamera::CameraKind::Isometric: return "Isometric"; + case core::ICamera::CameraKind::Chase: return "Chase"; + case core::ICamera::CameraKind::Dolly: return "Dolly"; + case core::ICamera::CameraKind::DollyZoom: return "Dolly Zoom"; + case core::ICamera::CameraKind::Path: return "Path"; default: return "Unknown"; } } //! Return a short human-readable label for a concrete camera instance. -inline std::string_view getCameraTypeLabel(const ICamera* camera) +inline std::string_view getCameraTypeLabel(const core::ICamera* camera) { return camera ? getCameraTypeLabel(camera->getKind()) : "Unknown"; } //! Return a short human-readable description for a camera kind. -inline std::string_view getCameraTypeDescription(const ICamera::CameraKind kind) +inline std::string_view getCameraTypeDescription(const core::ICamera::CameraKind kind) { switch (kind) { - case ICamera::CameraKind::FPS: return "First-person WASD + mouse look"; - case ICamera::CameraKind::Free: return "Free-fly 6DOF with full rotation"; - case ICamera::CameraKind::Orbit: return "Orbit around target with dolly"; - case ICamera::CameraKind::Arcball: return "Arcball trackball around target"; - case ICamera::CameraKind::Turntable: return "Turntable yaw/pitch around target"; - case ICamera::CameraKind::TopDown: return "Fixed pitch top-down pan"; - case ICamera::CameraKind::Isometric: return "Fixed isometric view with pan"; - case ICamera::CameraKind::Chase: return "Target follow with chase controls"; - case ICamera::CameraKind::Dolly: return "Rig truck/dolly with look-at"; - case ICamera::CameraKind::DollyZoom: return "Orbit with dolly-zoom FOV"; - case ICamera::CameraKind::Path: return "Move along a target path"; + case core::ICamera::CameraKind::FPS: return "First-person WASD + mouse look"; + case core::ICamera::CameraKind::Free: return "Free-fly 6DOF with full rotation"; + case core::ICamera::CameraKind::Orbit: return "Orbit around target with dolly"; + case core::ICamera::CameraKind::Arcball: return "Arcball trackball around target"; + case core::ICamera::CameraKind::Turntable: return "Turntable yaw/pitch around target"; + case core::ICamera::CameraKind::TopDown: return "Fixed pitch top-down pan"; + case core::ICamera::CameraKind::Isometric: return "Fixed isometric view with pan"; + case core::ICamera::CameraKind::Chase: return "Target follow with chase controls"; + case core::ICamera::CameraKind::Dolly: return "Rig truck/dolly with look-at"; + case core::ICamera::CameraKind::DollyZoom: return "Orbit with dolly-zoom FOV"; + case core::ICamera::CameraKind::Path: return "Move along a target path"; default: return "Unspecified camera behavior"; } } //! Return a short human-readable description for a concrete camera instance. -inline std::string_view getCameraTypeDescription(const ICamera* camera) +inline std::string_view getCameraTypeDescription(const core::ICamera* camera) { return camera ? getCameraTypeDescription(camera->getKind()) : "Unspecified camera behavior"; } @@ -70,7 +70,7 @@ inline std::string_view getCameraTypeDescription(const ICamera* camera) //! Describe the typed goal-state mask in a stable human-readable format. inline std::string describeGoalStateMask(const uint32_t mask) { - if (mask == ICamera::GoalStateNone) + if (mask == core::ICamera::GoalStateNone) return "Pose only"; std::string out; @@ -83,34 +83,34 @@ inline std::string describeGoalStateMask(const uint32_t mask) out += label; }; - append("Spherical target", ICamera::GoalStateSphericalTarget); - append("Dynamic perspective", ICamera::GoalStateDynamicPerspective); - append("Path", ICamera::GoalStatePath); + append("Spherical target", core::ICamera::GoalStateSphericalTarget); + append("Dynamic perspective", core::ICamera::GoalStateDynamicPerspective); + append("Path", core::ICamera::GoalStatePath); return out; } //! Describe a detailed goal-apply result for logs, smoke tests, and UI summaries. -inline std::string describeApplyResult(const CCameraGoalSolver::SApplyResult& result) +inline std::string describeApplyResult(const core::CCameraGoalSolver::SApplyResult& result) { std::ostringstream oss; oss << "status="; switch (result.status) { - case CCameraGoalSolver::SApplyResult::EStatus::Unsupported: oss << "Unsupported"; break; - case CCameraGoalSolver::SApplyResult::EStatus::Failed: oss << "Failed"; break; - case CCameraGoalSolver::SApplyResult::EStatus::AlreadySatisfied: oss << "AlreadySatisfied"; break; - case CCameraGoalSolver::SApplyResult::EStatus::AppliedAbsoluteOnly: oss << "AppliedAbsoluteOnly"; break; - case CCameraGoalSolver::SApplyResult::EStatus::AppliedVirtualEvents: oss << "AppliedVirtualEvents"; break; - case CCameraGoalSolver::SApplyResult::EStatus::AppliedAbsoluteAndVirtualEvents: oss << "AppliedAbsoluteAndVirtualEvents"; break; + case core::CCameraGoalSolver::SApplyResult::EStatus::Unsupported: oss << "Unsupported"; break; + case core::CCameraGoalSolver::SApplyResult::EStatus::Failed: oss << "Failed"; break; + case core::CCameraGoalSolver::SApplyResult::EStatus::AlreadySatisfied: oss << "AlreadySatisfied"; break; + case core::CCameraGoalSolver::SApplyResult::EStatus::AppliedAbsoluteOnly: oss << "AppliedAbsoluteOnly"; break; + case core::CCameraGoalSolver::SApplyResult::EStatus::AppliedVirtualEvents: oss << "AppliedVirtualEvents"; break; + case core::CCameraGoalSolver::SApplyResult::EStatus::AppliedAbsoluteAndVirtualEvents: oss << "AppliedAbsoluteAndVirtualEvents"; break; } oss << " exact=" << (result.exact ? "true" : "false") << " events=" << result.eventCount; - if (result.issues != CCameraGoalSolver::SApplyResult::NoIssue) + if (result.issues != core::CCameraGoalSolver::SApplyResult::NoIssue) { oss << " issues="; bool first = true; - auto appendIssue = [&](const char* label, const CCameraGoalSolver::SApplyResult::EIssue issue) -> void + auto appendIssue = [&](const char* label, const core::CCameraGoalSolver::SApplyResult::EIssue issue) -> void { if (!result.hasIssue(issue)) return; @@ -120,18 +120,18 @@ inline std::string describeApplyResult(const CCameraGoalSolver::SApplyResult& re first = false; }; - appendIssue("absolute_pose_fallback", CCameraGoalSolver::SApplyResult::UsedAbsolutePoseFallback); - appendIssue("missing_spherical_state", CCameraGoalSolver::SApplyResult::MissingSphericalTargetState); - appendIssue("missing_path_state", CCameraGoalSolver::SApplyResult::MissingPathState); - appendIssue("missing_dynamic_perspective_state", CCameraGoalSolver::SApplyResult::MissingDynamicPerspectiveState); - appendIssue("virtual_event_replay_failed", CCameraGoalSolver::SApplyResult::VirtualEventReplayFailed); + appendIssue("absolute_pose_fallback", core::CCameraGoalSolver::SApplyResult::UsedAbsolutePoseFallback); + appendIssue("missing_spherical_state", core::CCameraGoalSolver::SApplyResult::MissingSphericalTargetState); + appendIssue("missing_path_state", core::CCameraGoalSolver::SApplyResult::MissingPathState); + appendIssue("missing_dynamic_perspective_state", core::CCameraGoalSolver::SApplyResult::MissingDynamicPerspectiveState); + appendIssue("virtual_event_replay_failed", core::CCameraGoalSolver::SApplyResult::VirtualEventReplayFailed); } return oss.str(); } //! Describe compatibility preview for applying one analyzed goal to a target camera. -inline std::string describeGoalApplyCompatibility(const SCameraGoalApplyAnalysis& analysis, const ICamera* targetCamera) +inline std::string describeGoalApplyCompatibility(const core::SCameraGoalApplyAnalysis& analysis, const core::ICamera* targetCamera) { if (!analysis.hasCamera) return "No active camera"; @@ -141,16 +141,16 @@ inline std::string describeGoalApplyCompatibility(const SCameraGoalApplyAnalysis << " | source=" << getCameraTypeLabel(analysis.goal.sourceKind) << " | target=" << getCameraTypeLabel(targetCamera); - if (analysis.compatibility.missingGoalStateMask != ICamera::GoalStateNone) + if (analysis.compatibility.missingGoalStateMask != core::ICamera::GoalStateNone) oss << " | missing=" << describeGoalStateMask(analysis.compatibility.missingGoalStateMask); - else if (!analysis.compatibility.sameKind && analysis.goal.sourceKind != ICamera::CameraKind::Unknown) + else if (!analysis.compatibility.sameKind && analysis.goal.sourceKind != core::ICamera::CameraKind::Unknown) oss << " | shared goal state only"; return oss.str(); } //! Describe whether an analyzed goal can be meaningfully applied to the target camera. -inline std::string describeGoalApplyPolicy(const SCameraGoalApplyAnalysis& analysis) +inline std::string describeGoalApplyPolicy(const core::SCameraGoalApplyAnalysis& analysis) { if (!analysis.hasCamera) return "Blocked | no active camera"; @@ -159,9 +159,9 @@ inline std::string describeGoalApplyPolicy(const SCameraGoalApplyAnalysis& analy std::ostringstream oss; oss << (analysis.compatibility.exact ? "Exact apply" : "Best-effort apply"); - if (analysis.compatibility.missingGoalStateMask != ICamera::GoalStateNone) + if (analysis.compatibility.missingGoalStateMask != core::ICamera::GoalStateNone) oss << " | drops=" << describeGoalStateMask(analysis.compatibility.missingGoalStateMask); - else if (!analysis.compatibility.sameKind && analysis.goal.sourceKind != ICamera::CameraKind::Unknown) + else if (!analysis.compatibility.sameKind && analysis.goal.sourceKind != core::ICamera::CameraKind::Unknown) oss << " | shared goal state only"; else oss << " | full preview available"; @@ -170,7 +170,7 @@ inline std::string describeGoalApplyPolicy(const SCameraGoalApplyAnalysis& analy } //! Describe whether one analyzed camera state can be captured into a reusable goal. -inline std::string describeCameraCapturePolicy(const SCameraCaptureAnalysis& analysis, const ICamera* camera) +inline std::string describeCameraCapturePolicy(const core::SCameraCaptureAnalysis& analysis, const core::ICamera* camera) { if (!analysis.hasCamera) return "Blocked | no active camera"; @@ -186,7 +186,7 @@ inline std::string describeCameraCapturePolicy(const SCameraCaptureAnalysis& ana } //! Describe the aggregate outcome of applying one preset to multiple cameras. -inline std::string describePresetApplySummary(const SCameraPresetApplySummary& summary, std::string_view noTargetsLabel, std::string_view prefix = "Playback apply") +inline std::string describePresetApplySummary(const core::SCameraPresetApplySummary& summary, std::string_view noTargetsLabel, std::string_view prefix = "Playback apply") { if (!summary.hasTargets()) return std::string(noTargetsLabel); @@ -200,6 +200,6 @@ inline std::string describePresetApplySummary(const SCameraPresetApplySummary& s return oss.str(); } -} // namespace nbl::core +} // namespace nbl::ui #endif // _C_CAMERA_TEXT_UTILITIES_HPP_ diff --git a/common/include/camera/README.md b/common/include/camera/README.md index a7daa2ae4..7410c73a5 100644 --- a/common/include/camera/README.md +++ b/common/include/camera/README.md @@ -46,7 +46,7 @@ The stack is split across existing Nabla namespaces: - `nbl::core` camera runtime model, goals, presets, tracks, playback, follow, and authored sequence data - `nbl::ui` - binding layouts, input processors, binders, and default input mappings + binding layouts, input processors, binders, default input mappings, and user-facing presentation/text helpers - `nbl::system` persistence, scripted runtime payloads, scripted parsing, and scripted check execution @@ -263,7 +263,7 @@ It covers: - preset storage - keyframe playback - persistence -- diagnostics and presentation helpers +- UI-facing diagnostics and presentation helpers ### 7. Follow From 8fca07ecd4bb393ca69e73a130e2aa7480a77795 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Tue, 7 Apr 2026 13:16:28 +0200 Subject: [PATCH 174/205] Move follow labels into ui text helpers --- 61_UI/include/app/App.hpp | 2 +- 61_UI/include/common.hpp | 3 +- .../include/camera/CCameraFollowUtilities.hpp | 26 ----------------- .../include/camera/CCameraTextUtilities.hpp | 29 +++++++++++++++++++ 4 files changed, 32 insertions(+), 28 deletions(-) diff --git a/61_UI/include/app/App.hpp b/61_UI/include/app/App.hpp index 63827be92..de8929645 100644 --- a/61_UI/include/app/App.hpp +++ b/61_UI/include/app/App.hpp @@ -1269,7 +1269,7 @@ class App final : public examples::SimpleWindowedApplication } if (m_scriptedInput.visualFollowActive) { - lineHint += " | " + std::string(nbl::core::getCameraFollowModeDescription(m_scriptedInput.visualFollowMode)); + lineHint += " | " + std::string(nbl::ui::getCameraFollowModeDescription(m_scriptedInput.visualFollowMode)); if (m_scriptedInput.visualFollowLockValid) { char followBuffer[192] = {}; diff --git a/61_UI/include/common.hpp b/61_UI/include/common.hpp index 3aa57a92a..f4399fc88 100644 --- a/61_UI/include/common.hpp +++ b/61_UI/include/common.hpp @@ -186,7 +186,8 @@ using nbl::hlsl::uint16_t2; using nbl::hlsl::getQuaternionEulerDegrees; using nbl::ui::describeApplyResult; using nbl::ui::describeGoalStateMask; -using nbl::core::getCameraFollowModeLabel; +using nbl::ui::getCameraFollowModeLabel; +using nbl::ui::getCameraFollowModeDescription; using nbl::ui::getCameraTypeDescription; using nbl::ui::getCameraTypeLabel; using nbl::hlsl::getCastedMatrix; diff --git a/common/include/camera/CCameraFollowUtilities.hpp b/common/include/camera/CCameraFollowUtilities.hpp index a044bdac7..679516a5e 100644 --- a/common/include/camera/CCameraFollowUtilities.hpp +++ b/common/include/camera/CCameraFollowUtilities.hpp @@ -105,32 +105,6 @@ struct SCameraFollowConfig float64_t3 localOffset = float64_t3(0.0); }; -inline constexpr const char* getCameraFollowModeLabel(const ECameraFollowMode mode) -{ - switch (mode) - { - case ECameraFollowMode::Disabled: return "Disabled"; - case ECameraFollowMode::OrbitTarget: return "Orbit target"; - case ECameraFollowMode::LookAtTarget: return "Look at target"; - case ECameraFollowMode::KeepWorldOffset: return "Keep world offset"; - case ECameraFollowMode::KeepLocalOffset: return "Keep local offset"; - default: return "Unknown"; - } -} - -inline constexpr const char* getCameraFollowModeDescription(const ECameraFollowMode mode) -{ - switch (mode) - { - case ECameraFollowMode::Disabled: return "Follow disabled"; - case ECameraFollowMode::OrbitTarget: return "Keep orbit around moving target and keep it centered"; - case ECameraFollowMode::LookAtTarget: return "Keep camera position and lock the view onto the target"; - case ECameraFollowMode::KeepWorldOffset: return "Move with the target in world offset and keep it centered"; - case ECameraFollowMode::KeepLocalOffset: return "Move with the target in target-local offset and keep it centered"; - default: return "Unknown follow mode"; - } -} - inline constexpr bool cameraFollowModeLocksViewToTarget(const ECameraFollowMode mode) { switch (mode) diff --git a/common/include/camera/CCameraTextUtilities.hpp b/common/include/camera/CCameraTextUtilities.hpp index 323cc9cd4..7d23a9571 100644 --- a/common/include/camera/CCameraTextUtilities.hpp +++ b/common/include/camera/CCameraTextUtilities.hpp @@ -9,6 +9,7 @@ #include #include +#include "CCameraFollowUtilities.hpp" #include "CCameraGoalAnalysis.hpp" #include "CCameraPresetFlow.hpp" @@ -67,6 +68,34 @@ inline std::string_view getCameraTypeDescription(const core::ICamera* camera) return camera ? getCameraTypeDescription(camera->getKind()) : "Unspecified camera behavior"; } +//! Return a short human-readable label for a follow mode. +inline constexpr const char* getCameraFollowModeLabel(const core::ECameraFollowMode mode) +{ + switch (mode) + { + case core::ECameraFollowMode::Disabled: return "Disabled"; + case core::ECameraFollowMode::OrbitTarget: return "Orbit target"; + case core::ECameraFollowMode::LookAtTarget: return "Look at target"; + case core::ECameraFollowMode::KeepWorldOffset: return "Keep world offset"; + case core::ECameraFollowMode::KeepLocalOffset: return "Keep local offset"; + default: return "Unknown"; + } +} + +//! Return a short human-readable description for a follow mode. +inline constexpr const char* getCameraFollowModeDescription(const core::ECameraFollowMode mode) +{ + switch (mode) + { + case core::ECameraFollowMode::Disabled: return "Follow disabled"; + case core::ECameraFollowMode::OrbitTarget: return "Keep orbit around moving target and keep it centered"; + case core::ECameraFollowMode::LookAtTarget: return "Keep camera position and lock the view onto the target"; + case core::ECameraFollowMode::KeepWorldOffset: return "Move with the target in world offset and keep it centered"; + case core::ECameraFollowMode::KeepLocalOffset: return "Move with the target in target-local offset and keep it centered"; + default: return "Unknown follow mode"; + } +} + //! Describe the typed goal-state mask in a stable human-readable format. inline std::string describeGoalStateMask(const uint32_t mask) { From 5990235d6ab208fcbb2b4e3fe3f8a458a29cc801 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Tue, 7 Apr 2026 13:24:42 +0200 Subject: [PATCH 175/205] Move follow regression utilities into system --- 61_UI/AppInit.cpp | 22 ++--- 61_UI/AppUpdate.cpp | 2 +- 61_UI/include/app/App.hpp | 2 +- 61_UI/include/common.hpp | 2 +- .../CCameraFollowRegressionUtilities.hpp | 82 +++++++++---------- .../camera/CCameraScriptedCheckRunner.hpp | 4 +- common/include/camera/README.md | 4 +- 7 files changed, 59 insertions(+), 59 deletions(-) diff --git a/61_UI/AppInit.cpp b/61_UI/AppInit.cpp index d345dcc90..2973ebbbb 100644 --- a/61_UI/AppInit.cpp +++ b/61_UI/AppInit.cpp @@ -369,7 +369,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) { float32_t4x4 viewProjMatrix = float32_t4x4(1.0f); const bool hasViewProjMatrix = tryBuildFollowViewProjForCamera(camera, viewProjMatrix); - return nbl::core::buildFollowVisualMetrics( + return nbl::system::buildFollowVisualMetrics( camera, trackedTarget, &followConfig, @@ -377,12 +377,12 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) }; auto buildAndValidateFollowTargetContract = [&](ICamera* camera, const CTrackedTarget& trackedTarget, - const SCameraFollowConfig& followConfig, const char* label, nbl::core::SCameraFollowApplyValidationResult& outResult) -> bool + const SCameraFollowConfig& followConfig, const char* label, nbl::system::SCameraFollowApplyValidationResult& outResult) -> bool { std::string regressionError; float32_t4x4 viewProjMatrix = float32_t4x4(1.0f); const bool hasViewProjMatrix = tryBuildFollowViewProjForCamera(camera, viewProjMatrix); - if (!nbl::core::buildApplyAndValidateFollowTargetContract( + if (!nbl::system::buildApplyAndValidateFollowTargetContract( m_cameraGoalSolver, camera, trackedTarget, @@ -652,11 +652,11 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) auto verifyFollowTargetContract = [&](ICamera* camera, const CTrackedTarget& trackedTarget, const SCameraFollowConfig& followConfig, const CCameraGoal& followGoal, const char* label) -> bool { - nbl::core::SCameraFollowRegressionResult regression = {}; + nbl::system::SCameraFollowRegressionResult regression = {}; std::string regressionError; float32_t4x4 viewProjMatrix = float32_t4x4(1.0f); const bool hasViewProjMatrix = tryBuildFollowViewProjForCamera(camera, viewProjMatrix); - if (!nbl::core::validateFollowTargetContract( + if (!nbl::system::validateFollowTargetContract( camera, trackedTarget, followConfig, @@ -1170,7 +1170,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) followConfig.enabled = true; followConfig.mode = ECameraFollowMode::OrbitTarget; - nbl::core::SCameraFollowApplyValidationResult followResult = {}; + nbl::system::SCameraFollowApplyValidationResult followResult = {}; if (!buildAndValidateFollowTargetContract(orbitCamera, trackedTarget, followConfig, "orbit follow", followResult)) return false; if (!verifyFollowVisualMetrics(orbitCamera, trackedTarget, followConfig, "orbit follow")) @@ -1186,7 +1186,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) followConfig.worldOffset = float64_t3(4.0, -1.5, 2.0); trackedTarget.setPose(movedTrackedTargetPosition, movedTrackedTargetOrientation); - nbl::core::SCameraFollowApplyValidationResult worldOffsetResult = {}; + nbl::system::SCameraFollowApplyValidationResult worldOffsetResult = {}; if (!buildAndValidateFollowTargetContract(orbitCamera, trackedTarget, followConfig, "orbit keep-world-offset follow", worldOffsetResult)) return false; if (!verifyFollowVisualMetrics(orbitCamera, trackedTarget, followConfig, "orbit keep-world-offset follow")) @@ -1219,7 +1219,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) trackedTarget.setPose(movedTrackedTargetPosition, movedTrackedTargetOrientation); - nbl::core::SCameraFollowApplyValidationResult defaultFollowResult = {}; + nbl::system::SCameraFollowApplyValidationResult defaultFollowResult = {}; if (!buildAndValidateFollowTargetContract(defaultFollowCamera, trackedTarget, followConfig, label.c_str(), defaultFollowResult)) return false; if (!verifyFollowVisualMetrics(defaultFollowCamera, trackedTarget, followConfig, label.c_str())) @@ -1239,7 +1239,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) followConfig.enabled = true; followConfig.mode = ECameraFollowMode::LookAtTarget; - nbl::core::SCameraFollowApplyValidationResult lookAtResult = {}; + nbl::system::SCameraFollowApplyValidationResult lookAtResult = {}; if (!buildAndValidateFollowTargetContract(freeCamera, trackedTarget, followConfig, "free look-at follow", lookAtResult)) return false; if (!verifyFollowVisualMetrics(freeCamera, trackedTarget, followConfig, "free look-at follow")) @@ -1255,7 +1255,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) followConfig.worldOffset = float64_t3(5.0, -2.0, 1.5); trackedTarget.setPose(movedTrackedTargetPosition, movedTrackedTargetOrientation); - nbl::core::SCameraFollowApplyValidationResult keepWorldResult = {}; + nbl::system::SCameraFollowApplyValidationResult keepWorldResult = {}; if (!buildAndValidateFollowTargetContract(freeCamera, trackedTarget, followConfig, "free keep-world-offset follow", keepWorldResult)) return false; if (!verifyFollowVisualMetrics(freeCamera, trackedTarget, followConfig, "free keep-world-offset follow")) @@ -1277,7 +1277,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) trackedTarget.setPose(movedTrackedTargetPosition, movedTrackedTargetOrientation); - nbl::core::SCameraFollowApplyValidationResult localOffsetResult = {}; + nbl::system::SCameraFollowApplyValidationResult localOffsetResult = {}; if (!buildAndValidateFollowTargetContract(chaseCamera, trackedTarget, followConfig, "chase local-offset follow", localOffsetResult)) return false; if (!verifyFollowVisualMetrics(chaseCamera, trackedTarget, followConfig, "chase local-offset follow")) diff --git a/61_UI/AppUpdate.cpp b/61_UI/AppUpdate.cpp index 3d89571da..00817a012 100644 --- a/61_UI/AppUpdate.cpp +++ b/61_UI/AppUpdate.cpp @@ -553,7 +553,7 @@ void App::update() m_planarProjections[planarIx]->getCamera() : nullptr; float32_t4x4 viewProjMatrix = float32_t4x4(1.0f); const float32_t4x4* viewProjMatrixPtr = tryBuildFollowViewProjForPlanar(planarIx, viewProjMatrix) ? &viewProjMatrix : nullptr; - followMetrics = nbl::core::buildFollowVisualMetrics( + followMetrics = nbl::system::buildFollowVisualMetrics( activeCamera, m_followTarget, &m_planarFollowConfigs[planarIx], diff --git a/61_UI/include/app/App.hpp b/61_UI/include/app/App.hpp index de8929645..a938b3c94 100644 --- a/61_UI/include/app/App.hpp +++ b/61_UI/include/app/App.hpp @@ -916,7 +916,7 @@ class App final : public examples::SimpleWindowedApplication float ndcX = 0.0f; float ndcY = 0.0f; float ndcRadius = 0.0f; - if (!nbl::core::tryComputeProjectedFollowTargetMetrics(viewProjMatrix, m_followTarget, ndcX, ndcY, &ndcRadius)) + if (!nbl::system::tryComputeProjectedFollowTargetMetrics(viewProjMatrix, m_followTarget, ndcX, ndcY, &ndcRadius)) return; auto* drawList = ImGui::GetWindowDrawList(); diff --git a/61_UI/include/common.hpp b/61_UI/include/common.hpp index f4399fc88..0d3d5b6e5 100644 --- a/61_UI/include/common.hpp +++ b/61_UI/include/common.hpp @@ -158,7 +158,7 @@ using nbl::core::SCameraPresetApplySummary; using nbl::core::SCameraGoalApplyAnalysis; using nbl::core::SCameraCaptureAnalysis; using nbl::core::SCameraFollowConfig; -using nbl::core::SCameraFollowVisualMetrics; +using nbl::system::SCameraFollowVisualMetrics; using nbl::ui::SCameraGoalApplyPresentation; using nbl::ui::SCameraGoalApplyPresentationBadges; using nbl::ui::SCameraCapturePresentation; diff --git a/common/include/camera/CCameraFollowRegressionUtilities.hpp b/common/include/camera/CCameraFollowRegressionUtilities.hpp index 3eae10333..14aec821c 100644 --- a/common/include/camera/CCameraFollowRegressionUtilities.hpp +++ b/common/include/camera/CCameraFollowRegressionUtilities.hpp @@ -9,7 +9,7 @@ #include "CCameraFollowUtilities.hpp" -namespace nbl::core +namespace nbl::system { /** @@ -33,7 +33,7 @@ struct SCameraFollowRegressionResult float projectedNdcY = 0.0f; float projectedNdcRadius = 0.0f; bool hasSphericalState = false; - float64_t3 sphericalTarget = float64_t3(0.0); + hlsl::float64_t3 sphericalTarget = hlsl::float64_t3(0.0); float sphericalDistance = 0.0f; }; @@ -41,7 +41,7 @@ struct SCameraFollowRegressionResult struct SCameraFollowVisualMetrics { bool active = false; - ECameraFollowMode mode = ECameraFollowMode::Disabled; + core::ECameraFollowMode mode = core::ECameraFollowMode::Disabled; bool lockValid = false; float lockAngleDeg = 0.0f; float targetDistance = 0.0f; @@ -57,22 +57,22 @@ struct SCameraFollowVisualMetrics struct SCameraFollowApplyValidationResult { bool hasGoal = false; - CCameraGoal goal = {}; - CCameraGoalSolver::SApplyResult applyResult = {}; + core::CCameraGoal goal = {}; + core::CCameraGoalSolver::SApplyResult applyResult = {}; bool hasCapturedGoal = false; - CCameraGoal capturedGoal = {}; + core::CCameraGoal capturedGoal = {}; SCameraFollowRegressionResult regression = {}; }; inline bool tryComputeProjectedFollowTargetMetrics( - const float32_t4x4& viewProjMatrix, - const CTrackedTarget& trackedTarget, + const hlsl::float32_t4x4& viewProjMatrix, + const core::CTrackedTarget& trackedTarget, float& outNdcX, float& outNdcY, float* outNdcRadius = nullptr) { - const auto target = getCastedVector(trackedTarget.getGimbal().getPosition()); - const auto clip = mul(viewProjMatrix, float32_t4(target.x, target.y, target.z, 1.0f)); + const auto target = hlsl::getCastedVector(trackedTarget.getGimbal().getPosition()); + const auto clip = hlsl::mul(viewProjMatrix, hlsl::float32_t4(target.x, target.y, target.z, 1.0f)); if (!std::isfinite(clip.x) || !std::isfinite(clip.y) || !std::isfinite(clip.z) || !std::isfinite(clip.w)) return false; @@ -93,8 +93,8 @@ inline bool tryComputeProjectedFollowTargetMetrics( } inline bool validateProjectedFollowTargetContract( - const float32_t4x4& viewProjMatrix, - const CTrackedTarget& trackedTarget, + const hlsl::float32_t4x4& viewProjMatrix, + const core::CTrackedTarget& trackedTarget, float& outNdcRadius, std::string* error = nullptr, const float ndcRadiusTolerance = 0.03f) @@ -122,21 +122,21 @@ inline bool validateProjectedFollowTargetContract( } inline SCameraFollowVisualMetrics buildFollowVisualMetrics( - ICamera* camera, - const CTrackedTarget& trackedTarget, - const SCameraFollowConfig* followConfig, - const float32_t4x4* viewProjMatrix = nullptr) + core::ICamera* camera, + const core::CTrackedTarget& trackedTarget, + const core::SCameraFollowConfig* followConfig, + const hlsl::float32_t4x4* viewProjMatrix = nullptr) { SCameraFollowVisualMetrics out = {}; - if (!camera || !followConfig || !followConfig->enabled || followConfig->mode == ECameraFollowMode::Disabled) + if (!camera || !followConfig || !followConfig->enabled || followConfig->mode == core::ECameraFollowMode::Disabled) return out; out.active = true; out.mode = followConfig->mode; double targetDistance = 0.0; - out.lockValid = cameraFollowModeLocksViewToTarget(followConfig->mode) && - tryComputeFollowTargetLockMetrics(camera->getGimbal(), trackedTarget, out.lockAngleDeg, &targetDistance); + out.lockValid = core::cameraFollowModeLocksViewToTarget(followConfig->mode) && + core::tryComputeFollowTargetLockMetrics(camera->getGimbal(), trackedTarget, out.lockAngleDeg, &targetDistance); if (out.lockValid) out.targetDistance = static_cast(targetDistance); @@ -154,16 +154,16 @@ inline SCameraFollowVisualMetrics buildFollowVisualMetrics( } inline bool validateFollowTargetContract( - ICamera* camera, - const CTrackedTarget& trackedTarget, - const SCameraFollowConfig& followConfig, - const CCameraGoal& followGoal, + core::ICamera* camera, + const core::CTrackedTarget& trackedTarget, + const core::SCameraFollowConfig& followConfig, + const core::CCameraGoal& followGoal, SCameraFollowRegressionResult& out, std::string* error = nullptr, const float lockAngleToleranceDeg = 0.1f, const double distanceTolerance = 1e-6, const double targetTolerance = 1e-9, - const float32_t4x4* viewProjMatrix = nullptr, + const hlsl::float32_t4x4* viewProjMatrix = nullptr, const float projectedNdcTolerance = 0.03f) { out = {}; @@ -174,9 +174,9 @@ inline bool validateFollowTargetContract( return false; } - if (cameraFollowModeLocksViewToTarget(followConfig.mode)) + if (core::cameraFollowModeLocksViewToTarget(followConfig.mode)) { - out.hasLockMetrics = tryComputeFollowTargetLockMetrics(camera->getGimbal(), trackedTarget, out.lockAngleDeg, &out.targetDistance); + out.hasLockMetrics = core::tryComputeFollowTargetLockMetrics(camera->getGimbal(), trackedTarget, out.lockAngleDeg, &out.targetDistance); if (!out.hasLockMetrics) { if (error) @@ -184,7 +184,7 @@ inline bool validateFollowTargetContract( return false; } - const auto expectedTargetDistance = length(trackedTarget.getGimbal().getPosition() - camera->getGimbal().getPosition()); + const auto expectedTargetDistance = hlsl::length(trackedTarget.getGimbal().getPosition() - camera->getGimbal().getPosition()); if (!std::isfinite(expectedTargetDistance) || std::abs(expectedTargetDistance - out.targetDistance) > distanceTolerance) { if (error) @@ -229,9 +229,9 @@ inline bool validateFollowTargetContract( } } - if (camera->supportsGoalState(ICamera::GoalStateSphericalTarget)) + if (camera->supportsGoalState(core::ICamera::GoalStateSphericalTarget)) { - ICamera::SphericalTargetState state; + core::ICamera::SphericalTargetState state; if (!camera->tryGetSphericalTargetState(state)) { if (error) @@ -245,7 +245,7 @@ inline bool validateFollowTargetContract( const auto trackedTargetPosition = trackedTarget.getGimbal().getPosition(); const auto targetDelta = state.target - trackedTargetPosition; - const auto targetDeltaLen = length(targetDelta); + const auto targetDeltaLen = hlsl::length(targetDelta); if (!std::isfinite(targetDeltaLen) || targetDeltaLen > targetTolerance) { if (error) @@ -253,7 +253,7 @@ inline bool validateFollowTargetContract( return false; } - const auto actualDistance = length(camera->getGimbal().getPosition() - trackedTargetPosition); + const auto actualDistance = hlsl::length(camera->getGimbal().getPosition() - trackedTargetPosition); const auto expectedDistance = followGoal.hasOrbitState ? static_cast(followGoal.orbitDistance) : (followGoal.hasDistance ? static_cast(followGoal.distance) : actualDistance); if (!std::isfinite(actualDistance) || !std::isfinite(expectedDistance) || @@ -275,13 +275,13 @@ inline bool validateFollowTargetContract( } inline bool buildApplyAndValidateFollowTargetContract( - const CCameraGoalSolver& solver, - ICamera* camera, - const CTrackedTarget& trackedTarget, - const SCameraFollowConfig& followConfig, + const core::CCameraGoalSolver& solver, + core::ICamera* camera, + const core::CTrackedTarget& trackedTarget, + const core::SCameraFollowConfig& followConfig, SCameraFollowApplyValidationResult& out, std::string* error = nullptr, - const float32_t4x4* viewProjMatrix = nullptr, + const hlsl::float32_t4x4* viewProjMatrix = nullptr, const float lockAngleToleranceDeg = 0.1f, const double distanceTolerance = 1e-6, const double targetTolerance = 1e-9, @@ -292,7 +292,7 @@ inline bool buildApplyAndValidateFollowTargetContract( { out = {}; - if (!tryBuildFollowGoal(solver, camera, trackedTarget, followConfig, out.goal)) + if (!core::tryBuildFollowGoal(solver, camera, trackedTarget, followConfig, out.goal)) { if (error) *error = "failed to build follow goal"; @@ -300,7 +300,7 @@ inline bool buildApplyAndValidateFollowTargetContract( } out.hasGoal = true; - out.applyResult = applyFollowToCamera(solver, camera, trackedTarget, followConfig); + out.applyResult = core::applyFollowToCamera(solver, camera, trackedTarget, followConfig); if (!out.applyResult.succeeded()) { if (error) @@ -318,10 +318,10 @@ inline bool buildApplyAndValidateFollowTargetContract( out.hasCapturedGoal = true; out.capturedGoal = capture.goal; - if (!compareGoals(out.capturedGoal, out.goal, posTolerance, rotToleranceDeg, scalarTolerance)) + if (!core::compareGoals(out.capturedGoal, out.goal, posTolerance, rotToleranceDeg, scalarTolerance)) { if (error) - *error = std::string("follow goal mismatch. ") + describeGoalMismatch(out.capturedGoal, out.goal); + *error = std::string("follow goal mismatch. ") + core::describeGoalMismatch(out.capturedGoal, out.goal); return false; } @@ -344,6 +344,6 @@ inline bool buildApplyAndValidateFollowTargetContract( return true; } -} // namespace nbl::core +} // namespace nbl::system #endif // _C_CAMERA_FOLLOW_REGRESSION_UTILITIES_HPP_ diff --git a/common/include/camera/CCameraScriptedCheckRunner.hpp b/common/include/camera/CCameraScriptedCheckRunner.hpp index c469b32d0..722f580ee 100644 --- a/common/include/camera/CCameraScriptedCheckRunner.hpp +++ b/common/include/camera/CCameraScriptedCheckRunner.hpp @@ -477,7 +477,7 @@ inline CCameraScriptedCheckFrameResult evaluateScriptedChecksForFrame( break; } - core::SCameraFollowRegressionResult regression = {}; + SCameraFollowRegressionResult regression = {}; std::string regressionError; core::CCameraGoal expectedFollowGoal = {}; const bool ok = core::tryBuildFollowGoal( @@ -486,7 +486,7 @@ inline CCameraScriptedCheckFrameResult evaluateScriptedChecksForFrame( *context.trackedTarget, *context.followConfig, expectedFollowGoal) && - core::validateFollowTargetContract( + validateFollowTargetContract( context.camera, *context.trackedTarget, *context.followConfig, diff --git a/common/include/camera/README.md b/common/include/camera/README.md index 7410c73a5..d77357b12 100644 --- a/common/include/camera/README.md +++ b/common/include/camera/README.md @@ -48,7 +48,7 @@ The stack is split across existing Nabla namespaces: - `nbl::ui` binding layouts, input processors, binders, default input mappings, and user-facing presentation/text helpers - `nbl::system` - persistence, scripted runtime payloads, scripted parsing, and scripted check execution + persistence, scripted runtime payloads, scripted parsing, scripted check execution, and follow-contract validation helpers ## Why virtual events and not absolute setters @@ -268,7 +268,6 @@ It covers: ### 7. Follow - [`CCameraFollowUtilities.hpp`](CCameraFollowUtilities.hpp) -- [`CCameraFollowRegressionUtilities.hpp`](CCameraFollowRegressionUtilities.hpp) Follow is deliberately not part of `ICamera`. @@ -290,6 +289,7 @@ It only knows about: - [`CCameraScriptedRuntimePersistence.hpp`](CCameraScriptedRuntimePersistence.hpp) - [`CCameraSequenceScriptedBuilder.hpp`](CCameraSequenceScriptedBuilder.hpp) - [`CCameraScriptedCheckRunner.hpp`](CCameraScriptedCheckRunner.hpp) +- [`CCameraFollowRegressionUtilities.hpp`](CCameraFollowRegressionUtilities.hpp) This is the reusable scripting and CI half. From d8ae9c2fc38949a51ae56a895c9e7d6d0cc7e0e3 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Tue, 7 Apr 2026 13:43:06 +0200 Subject: [PATCH 176/205] Qualify camera math through hlsl --- 61_UI/include/app/App.hpp | 10 -- common/include/camera/CArcballCamera.hpp | 4 +- .../include/camera/CCameraFollowUtilities.hpp | 92 +++++++++---------- common/include/camera/CCameraGoal.hpp | 38 ++++---- common/include/camera/CCameraGoalSolver.hpp | 46 +++++----- .../camera/CCameraManipulationUtilities.hpp | 10 +- .../include/camera/CCameraSequenceScript.hpp | 40 ++++---- common/include/camera/CChaseCamera.hpp | 16 ++-- common/include/camera/CCubeProjection.hpp | 2 +- common/include/camera/CDollyCamera.hpp | 4 +- common/include/camera/CDollyZoomCamera.hpp | 4 +- common/include/camera/CFPSCamera.hpp | 10 +- common/include/camera/CFreeLockCamera.hpp | 14 +-- .../include/camera/CGeneralPurposeGimbal.hpp | 2 +- common/include/camera/CIsometricCamera.hpp | 4 +- common/include/camera/COrbitCamera.hpp | 6 +- common/include/camera/CPathCamera.hpp | 6 +- .../include/camera/CSphericalTargetCamera.hpp | 40 ++++---- common/include/camera/CTopDownCamera.hpp | 4 +- common/include/camera/CTurntableCamera.hpp | 4 +- common/include/camera/ICamera.hpp | 26 +++--- common/include/camera/IGimbal.hpp | 90 +++++------------- common/include/camera/ILinearProjection.hpp | 26 +++--- .../include/camera/IPerspectiveProjection.hpp | 2 +- common/include/camera/IPlanarProjection.hpp | 8 +- common/include/camera/IProjection.hpp | 4 +- 26 files changed, 230 insertions(+), 282 deletions(-) diff --git a/61_UI/include/app/App.hpp b/61_UI/include/app/App.hpp index a938b3c94..77998a254 100644 --- a/61_UI/include/app/App.hpp +++ b/61_UI/include/app/App.hpp @@ -247,16 +247,6 @@ static smart_refctd_ptr createSceneFramebuffer(ILogicalDevice* return device->createFramebuffer(std::move(params)); } -/* - Renders scene texture to an offline - framebuffer which color attachment - is then sampled into a imgui window. - - Written with Nabla, it's UI extension - and got integrated with ImGuizmo to - handle scene's object translations. -*/ - class App final : public examples::SimpleWindowedApplication { using base_t = examples::SimpleWindowedApplication; diff --git a/common/include/camera/CArcballCamera.hpp b/common/include/camera/CArcballCamera.hpp index a1492bf57..624e99109 100644 --- a/common/include/camera/CArcballCamera.hpp +++ b/common/include/camera/CArcballCamera.hpp @@ -18,7 +18,7 @@ class CArcballCamera final : public CSphericalTargetCamera public: using base_t = CSphericalTargetCamera; - CArcballCamera(const float64_t3& position, const float64_t3& target) + CArcballCamera(const hlsl::float64_t3& position, const hlsl::float64_t3& target) : base_t(position, target) { m_v = std::clamp(m_v, MinPitch, MaxPitch); @@ -28,7 +28,7 @@ class CArcballCamera final : public CSphericalTargetCamera const typename base_t::CGimbal& getGimbal() override { return m_gimbal; } - virtual bool manipulate(std::span virtualEvents, const float64_t4x4* referenceFrame = nullptr) override + virtual bool manipulate(std::span virtualEvents, const hlsl::float64_t4x4* referenceFrame = nullptr) override { if (not virtualEvents.size() and not referenceFrame) return false; diff --git a/common/include/camera/CCameraFollowUtilities.hpp b/common/include/camera/CCameraFollowUtilities.hpp index 679516a5e..fd84ba38f 100644 --- a/common/include/camera/CCameraFollowUtilities.hpp +++ b/common/include/camera/CCameraFollowUtilities.hpp @@ -25,8 +25,8 @@ class CTrackedTarget using gimbal_t = ICamera::CGimbal; CTrackedTarget( - const float64_t3& position = float64_t3(0.0), - const camera_quaternion_t& orientation = makeIdentityQuaternion(), + const hlsl::float64_t3& position = hlsl::float64_t3(0.0), + const hlsl::camera_quaternion_t& orientation = hlsl::makeIdentityQuaternion(), std::string identifier = "Follow Target") : m_identifier(std::move(identifier)), m_gimbal({ .position = position, .orientation = orientation }) @@ -38,7 +38,7 @@ class CTrackedTarget inline const gimbal_t& getGimbal() const { return m_gimbal; } inline gimbal_t& getGimbal() { return m_gimbal; } - inline void setPose(const float64_t3& position, const camera_quaternion_t& orientation) + inline void setPose(const hlsl::float64_t3& position, const hlsl::camera_quaternion_t& orientation) { m_gimbal.begin(); m_gimbal.setPosition(position); @@ -47,25 +47,25 @@ class CTrackedTarget m_gimbal.updateView(); } - inline void setPosition(const float64_t3& position) + inline void setPosition(const hlsl::float64_t3& position) { setPose(position, m_gimbal.getOrientation()); } - inline void setOrientation(const camera_quaternion_t& orientation) + inline void setOrientation(const hlsl::camera_quaternion_t& orientation) { setPose(m_gimbal.getPosition(), orientation); } - inline bool trySetFromTransform(const float64_t4x4& transform) + inline bool trySetFromTransform(const hlsl::float64_t4x4& transform) { - const auto right = normalize(float64_t3(transform[0])); - const auto up = normalize(float64_t3(transform[1])); - const auto forward = normalize(float64_t3(transform[2])); - if (!isOrthoBase(right, up, forward)) + const auto right = hlsl::normalize(hlsl::float64_t3(transform[0])); + const auto up = hlsl::normalize(hlsl::float64_t3(transform[1])); + const auto forward = hlsl::normalize(hlsl::float64_t3(transform[2])); + if (!hlsl::isOrthoBase(right, up, forward)) return false; - setPose(float64_t3(transform[3]), makeQuaternionFromBasis(right, up, forward)); + setPose(hlsl::float64_t3(transform[3]), hlsl::makeQuaternionFromBasis(right, up, forward)); return true; } @@ -101,8 +101,8 @@ struct SCameraFollowConfig { bool enabled = false; ECameraFollowMode mode = ECameraFollowMode::OrbitTarget; - float64_t3 worldOffset = float64_t3(0.0); - float64_t3 localOffset = float64_t3(0.0); + hlsl::float64_t3 worldOffset = hlsl::float64_t3(0.0); + hlsl::float64_t3 localOffset = hlsl::float64_t3(0.0); }; inline constexpr bool cameraFollowModeLocksViewToTarget(const ECameraFollowMode mode) @@ -157,59 +157,59 @@ inline constexpr bool cameraFollowModeUsesCapturedOffset(const ECameraFollowMode return cameraFollowModeUsesWorldOffset(mode) || cameraFollowModeUsesLocalOffset(mode); } -inline float64_t3 transformFollowLocalOffset(const ICamera::CGimbal& gimbal, const float64_t3& localOffset) +inline hlsl::float64_t3 transformFollowLocalOffset(const ICamera::CGimbal& gimbal, const hlsl::float64_t3& localOffset) { return gimbal.getXAxis() * localOffset.x + gimbal.getYAxis() * localOffset.y + gimbal.getZAxis() * localOffset.z; } -inline float64_t3 projectFollowWorldOffsetToLocal(const ICamera::CGimbal& gimbal, const float64_t3& worldOffset) +inline hlsl::float64_t3 projectFollowWorldOffsetToLocal(const ICamera::CGimbal& gimbal, const hlsl::float64_t3& worldOffset) { - return float64_t3( + return hlsl::float64_t3( hlsl::dot(worldOffset, gimbal.getXAxis()), hlsl::dot(worldOffset, gimbal.getYAxis()), hlsl::dot(worldOffset, gimbal.getZAxis())); } inline bool buildFollowLookAtOrientation( - const float64_t3& position, - const float64_t3& targetPosition, - const float64_t3& preferredUp, - camera_quaternion_t& outOrientation) + const hlsl::float64_t3& position, + const hlsl::float64_t3& targetPosition, + const hlsl::float64_t3& preferredUp, + hlsl::camera_quaternion_t& outOrientation) { const auto toTarget = targetPosition - position; - const double toTargetLength = length(toTarget); + const double toTargetLength = hlsl::length(toTarget); if (!std::isfinite(toTargetLength) || toTargetLength <= 1e-9) return false; const auto forward = toTarget / toTargetLength; auto up = preferredUp; - if (!isFiniteVec3(up) || length(up) <= 1e-9) - up = float64_t3(0.0, 0.0, 1.0); + if (!isFiniteVec3(up) || hlsl::length(up) <= 1e-9) + up = hlsl::float64_t3(0.0, 0.0, 1.0); else - up = normalize(up); + up = hlsl::normalize(up); - auto right = cross(up, forward); - if (!isFiniteVec3(right) || length(right) <= 1e-9) + auto right = hlsl::cross(up, forward); + if (!isFiniteVec3(right) || hlsl::length(right) <= 1e-9) { - const auto fallbackUp = std::abs(forward.z) < 0.99 ? float64_t3(0.0, 0.0, 1.0) : float64_t3(0.0, 1.0, 0.0); - right = cross(fallbackUp, forward); - if (!isFiniteVec3(right) || length(right) <= 1e-9) + const auto fallbackUp = std::abs(forward.z) < 0.99 ? hlsl::float64_t3(0.0, 0.0, 1.0) : hlsl::float64_t3(0.0, 1.0, 0.0); + right = hlsl::cross(fallbackUp, forward); + if (!isFiniteVec3(right) || hlsl::length(right) <= 1e-9) return false; } - right = normalize(right); - up = normalize(cross(forward, right)); - if (!isOrthoBase(right, up, forward)) + right = hlsl::normalize(right); + up = hlsl::normalize(hlsl::cross(forward, right)); + if (!hlsl::isOrthoBase(right, up, forward)) return false; - outOrientation = makeQuaternionFromBasis(right, up, forward); + outOrientation = hlsl::makeQuaternionFromBasis(right, up, forward); return true; } inline bool applyFollowSphericalPose( CCameraGoal& goal, - const float64_t3& targetPosition, + const hlsl::float64_t3& targetPosition, const double orbitU, const double orbitV, const float distance) @@ -218,18 +218,18 @@ inline bool applyFollowSphericalPose( return false; const float clampedDistance = std::clamp(distance, ICamera::SphericalMinDistance, ICamera::SphericalMaxDistance); - const float64_t3 spherePosition( + const hlsl::float64_t3 spherePosition( std::cos(orbitV) * std::cos(orbitU) * static_cast(clampedDistance), std::cos(orbitV) * std::sin(orbitU) * static_cast(clampedDistance), std::sin(orbitV) * static_cast(clampedDistance)); - const auto forward = normalize(-spherePosition); - const auto up = normalize(float64_t3( + const auto forward = hlsl::normalize(-spherePosition); + const auto up = hlsl::normalize(hlsl::float64_t3( -std::sin(orbitV) * std::cos(orbitU), -std::sin(orbitV) * std::sin(orbitU), std::cos(orbitV))); - const auto right = normalize(cross(up, forward)); - if (!isOrthoBase(right, up, forward)) + const auto right = hlsl::normalize(hlsl::cross(up, forward)); + if (!hlsl::isOrthoBase(right, up, forward)) return false; goal.hasTargetPosition = true; @@ -241,14 +241,14 @@ inline bool applyFollowSphericalPose( goal.orbitV = orbitV; goal.orbitDistance = clampedDistance; goal.position = targetPosition + spherePosition; - goal.orientation = makeQuaternionFromBasis(right, up, forward); + goal.orientation = hlsl::makeQuaternionFromBasis(right, up, forward); return true; } -inline bool buildFollowSphericalGoalFromPose(CCameraGoal& goal, const float64_t3& targetPosition, const float64_t3& position) +inline bool buildFollowSphericalGoalFromPose(CCameraGoal& goal, const hlsl::float64_t3& targetPosition, const hlsl::float64_t3& position) { const auto offset = position - targetPosition; - const double distance = length(offset); + const double distance = hlsl::length(offset); if (!std::isfinite(distance) || distance <= 1e-9) return false; @@ -283,16 +283,16 @@ inline bool tryComputeFollowTargetLockMetrics( double* outDistance = nullptr) { const auto toTarget = trackedTarget.getGimbal().getPosition() - cameraGimbal.getPosition(); - const auto targetDistance = length(toTarget); + const auto targetDistance = hlsl::length(toTarget); if (!std::isfinite(targetDistance) || targetDistance <= 1e-9) return false; - const auto forward = normalize(cameraGimbal.getZAxis()); - if (!isFiniteVec3(forward) || length(forward) <= 1e-9) + const auto forward = hlsl::normalize(cameraGimbal.getZAxis()); + if (!isFiniteVec3(forward) || hlsl::length(forward) <= 1e-9) return false; const auto targetDir = toTarget / targetDistance; - const auto dotForward = std::clamp(dot(forward, targetDir), -1.0, 1.0); + const auto dotForward = std::clamp(hlsl::dot(forward, targetDir), -1.0, 1.0); outAngleDeg = static_cast(hlsl::degrees(std::acos(dotForward))); if (!std::isfinite(outAngleDeg)) return false; diff --git a/common/include/camera/CCameraGoal.hpp b/common/include/camera/CCameraGoal.hpp index 866d46097..565a1f2f2 100644 --- a/common/include/camera/CCameraGoal.hpp +++ b/common/include/camera/CCameraGoal.hpp @@ -20,13 +20,13 @@ namespace nbl::core */ struct CCameraGoal { - float64_t3 position = float64_t3(0.0); - camera_quaternion_t orientation = makeIdentityQuaternion(); + hlsl::float64_t3 position = hlsl::float64_t3(0.0); + hlsl::camera_quaternion_t orientation = hlsl::makeIdentityQuaternion(); ICamera::CameraKind sourceKind = ICamera::CameraKind::Unknown; uint32_t sourceCapabilities = ICamera::None; uint32_t sourceGoalStateMask = ICamera::GoalStateNone; bool hasTargetPosition = false; - float64_t3 targetPosition = float64_t3(0.0); + hlsl::float64_t3 targetPosition = hlsl::float64_t3(0.0); bool hasDistance = false; float distance = 0.f; bool hasOrbitState = false; @@ -68,11 +68,11 @@ inline bool applyCanonicalPathGoal(CCameraGoal& goal) if (!std::isfinite(goal.pathState.angle) || !std::isfinite(goal.pathState.radius) || !std::isfinite(goal.pathState.height)) return false; - const float64_t3 offset( + const hlsl::float64_t3 offset( std::cos(goal.pathState.angle) * goal.pathState.radius, goal.pathState.height, std::sin(goal.pathState.angle) * goal.pathState.radius); - const double distance = length(offset); + const double distance = hlsl::length(offset); if (!std::isfinite(distance) || distance <= 1e-9) return false; @@ -84,7 +84,7 @@ inline bool applyCanonicalPathGoal(CCameraGoal& goal) goal.orbitU = std::atan2(local.y, local.x); goal.orbitV = std::asin(std::clamp(local.z, -1.0, 1.0)); - const float64_t3 spherePosition( + const hlsl::float64_t3 spherePosition( std::cos(goal.orbitV) * std::cos(goal.orbitU) * static_cast(appliedDistance), std::cos(goal.orbitV) * std::sin(goal.orbitU) * static_cast(appliedDistance), std::sin(goal.orbitV) * static_cast(appliedDistance)); @@ -95,13 +95,13 @@ inline bool applyCanonicalPathGoal(CCameraGoal& goal) goal.hasOrbitState = true; goal.orbitDistance = appliedDistance; - const auto forward = normalize(-spherePosition); - const float64_t3 up = normalize(float64_t3( + const auto forward = hlsl::normalize(-spherePosition); + const hlsl::float64_t3 up = hlsl::normalize(hlsl::float64_t3( -std::sin(goal.orbitV) * std::cos(goal.orbitU), -std::sin(goal.orbitV) * std::sin(goal.orbitU), std::cos(goal.orbitV))); - const float64_t3 right = normalize(cross(up, forward)); - goal.orientation = makeQuaternionFromBasis(right, up, forward); + const hlsl::float64_t3 right = hlsl::normalize(hlsl::cross(up, forward)); + goal.orientation = hlsl::makeQuaternionFromBasis(right, up, forward); return true; } @@ -127,7 +127,7 @@ inline bool nearlyEqualVec3(const VecA& a, const VecB& b, const double epsilon) inline bool isGoalFinite(const CCameraGoal& goal) { - if (!isFiniteVec3(goal.position) || !isFiniteQuaternion(goal.orientation)) + if (!isFiniteVec3(goal.position) || !hlsl::isFiniteQuaternion(goal.orientation)) return false; if (goal.hasTargetPosition && !isFiniteVec3(goal.targetPosition)) return false; @@ -156,16 +156,16 @@ inline bool compareGoals(const CCameraGoal& actual, const CCameraGoal& expected, return std::abs(d - Pi); }; - const auto currentOrientation = normalizeQuaternion(actual.orientation); - const auto expectedOrientation = normalizeQuaternion(expected.orientation); - if (!isFiniteVec3(actual.position) || !isFiniteVec3(expected.position) || !isFiniteQuaternion(currentOrientation) || !isFiniteQuaternion(expectedOrientation)) + const auto currentOrientation = hlsl::normalizeQuaternion(actual.orientation); + const auto expectedOrientation = hlsl::normalizeQuaternion(expected.orientation); + if (!isFiniteVec3(actual.position) || !isFiniteVec3(expected.position) || !hlsl::isFiniteQuaternion(currentOrientation) || !hlsl::isFiniteQuaternion(expectedOrientation)) return false; const double dx = static_cast(actual.position.x - expected.position.x); const double dy = static_cast(actual.position.y - expected.position.y); const double dz = static_cast(actual.position.z - expected.position.z); const double posDelta = std::sqrt(dx * dx + dy * dy + dz * dz); - const double rotDeltaDeg = getQuaternionAngularDistanceDegrees(currentOrientation, expectedOrientation); + const double rotDeltaDeg = hlsl::getQuaternionAngularDistanceDegrees(currentOrientation, expectedOrientation); if (posDelta > posEps || rotDeltaDeg > rotEpsDeg) return false; @@ -217,13 +217,13 @@ inline bool compareGoals(const CCameraGoal& actual, const CCameraGoal& expected, inline std::string describeGoalMismatch(const CCameraGoal& actual, const CCameraGoal& expected) { std::ostringstream oss; - const auto currentOrientation = normalizeQuaternion(actual.orientation); - const auto expectedOrientation = normalizeQuaternion(expected.orientation); + const auto currentOrientation = hlsl::normalizeQuaternion(actual.orientation); + const auto expectedOrientation = hlsl::normalizeQuaternion(expected.orientation); const double dx = static_cast(actual.position.x - expected.position.x); const double dy = static_cast(actual.position.y - expected.position.y); const double dz = static_cast(actual.position.z - expected.position.z); const double posDelta = std::sqrt(dx * dx + dy * dy + dz * dz); - const double rotDeltaDeg = getQuaternionAngularDistanceDegrees(currentOrientation, expectedOrientation); + const double rotDeltaDeg = hlsl::getQuaternionAngularDistanceDegrees(currentOrientation, expectedOrientation); oss << "pos_delta=" << posDelta << " rot_delta_deg=" << rotDeltaDeg << " current_pos=(" << actual.position.x << "," << actual.position.y << "," << actual.position.z << ")" @@ -271,7 +271,7 @@ inline CCameraGoal blendGoals(const CCameraGoal& a, const CCameraGoal& b, double { CCameraGoal blended; blended.position = a.position + (b.position - a.position) * alpha; - blended.orientation = slerpQuaternion(a.orientation, b.orientation, static_cast(alpha)); + blended.orientation = hlsl::slerpQuaternion(a.orientation, b.orientation, static_cast(alpha)); blended.sourceKind = (a.sourceKind == b.sourceKind) ? a.sourceKind : ICamera::CameraKind::Unknown; blended.sourceCapabilities = a.sourceCapabilities & b.sourceCapabilities; blended.sourceGoalStateMask = a.sourceGoalStateMask | b.sourceGoalStateMask; diff --git a/common/include/camera/CCameraGoalSolver.hpp b/common/include/camera/CCameraGoalSolver.hpp index bbd42d7b4..7d0296b82 100644 --- a/common/include/camera/CCameraGoalSolver.hpp +++ b/common/include/camera/CCameraGoalSolver.hpp @@ -393,7 +393,7 @@ class CCameraGoalSolver struct SSphericalGoal { - float64_t3 target = float64_t3(0.0); + hlsl::float64_t3 target = hlsl::float64_t3(0.0); double u = 0.0; double v = 0.0; float distance = 0.f; @@ -421,18 +421,18 @@ class CCameraGoalSolver return rotationScale == 0.0 ? 1.0 : rotationScale; } - inline std::pair computePitchYawFromOrientation(const camera_quaternion_t& orientation) const + inline std::pair computePitchYawFromOrientation(const hlsl::camera_quaternion_t& orientation) const { - const auto mat = getQuaternionBasisMatrix(orientation); - const auto forward = float64_t3(mat[2][0], mat[2][1], mat[2][2]); + const auto mat = hlsl::getQuaternionBasisMatrix(orientation); + const auto forward = hlsl::float64_t3(mat[2][0], mat[2][1], mat[2][2]); const double pitch = std::atan2(std::sqrt(forward.x * forward.x + forward.z * forward.z), forward.y) - HalfPi; const double yaw = std::atan2(forward.x, forward.z); return { pitch, yaw }; } - inline float64_t3 extractYawPitchRollYXZ(const camera_quaternion_t& delta) const + inline hlsl::float64_t3 extractYawPitchRollYXZ(const hlsl::camera_quaternion_t& delta) const { - const auto m = getMatrix3x3As4x4(getQuaternionBasisMatrix(delta)); + const auto m = hlsl::getMatrix3x3As4x4(hlsl::getQuaternionBasisMatrix(delta)); const double yaw = std::atan2(static_cast(m[2][0]), static_cast(m[2][2])); const double c2 = std::sqrt(static_cast(m[0][1] * m[0][1] + m[1][1] * m[1][1])); const double pitch = std::atan2(-static_cast(m[2][1]), c2); @@ -441,7 +441,7 @@ class CCameraGoalSolver const double roll = std::atan2( s1 * static_cast(m[1][2]) - c1 * static_cast(m[1][0]), c1 * static_cast(m[0][0]) - s1 * static_cast(m[0][2])); - return float64_t3(pitch, yaw, roll); + return hlsl::float64_t3(pitch, yaw, roll); } inline bool computePoseMismatch(ICamera* camera, const CCameraGoal& target, double& outPositionDelta, double& outRotationDeltaDeg) const @@ -453,15 +453,15 @@ class CCameraGoalSolver const auto& gimbal = camera->getGimbal(); const auto currentPos = gimbal.getPosition(); - const auto currentOrientation = normalizeQuaternion(gimbal.getOrientation()); - const auto targetOrientation = normalizeQuaternion(target.orientation); + const auto currentOrientation = hlsl::normalizeQuaternion(gimbal.getOrientation()); + const auto targetOrientation = hlsl::normalizeQuaternion(target.orientation); const double dx = static_cast(currentPos.x - target.position.x); const double dy = static_cast(currentPos.y - target.position.y); const double dz = static_cast(currentPos.z - target.position.z); outPositionDelta = std::sqrt(dx * dx + dy * dy + dz * dz); - outRotationDeltaDeg = getQuaternionAngularDistanceDegrees(currentOrientation, targetOrientation); + outRotationDeltaDeg = hlsl::getQuaternionAngularDistanceDegrees(currentOrientation, targetOrientation); return std::isfinite(outPositionDelta) && std::isfinite(outRotationDeltaDeg); } @@ -492,8 +492,8 @@ class CCameraGoalSolver return true; } - auto targetFrame = getMatrix3x3As4x4(getQuaternionBasisMatrix(target.orientation)); - targetFrame[3] = float64_t4(target.position, 1.0); + auto targetFrame = hlsl::getMatrix3x3As4x4(hlsl::getQuaternionBasisMatrix(target.orientation)); + targetFrame[3] = hlsl::float64_t4(target.position, 1.0); camera->manipulate({}, &targetFrame); @@ -507,11 +507,11 @@ class CCameraGoalSolver return true; } - inline bool computeOrbitStateFromPositionTarget(const float64_t3& position, const float64_t3& target, + inline bool computeOrbitStateFromPositionTarget(const hlsl::float64_t3& position, const hlsl::float64_t3& target, double& outU, double& outV, float& outDistance, float minDistance, float maxDistance) const { const auto localSpherePosition = position - target; - const double dist = length(localSpherePosition); + const double dist = hlsl::length(localSpherePosition); if (!std::isfinite(dist)) return false; @@ -557,8 +557,8 @@ class CCameraGoalSolver inline bool buildOrbitTranslateEvents(ICamera* camera, const ICamera::SphericalTargetState& sphericalState, const SSphericalGoal& goal, std::vector& out) const { const double moveDenom = getMoveMagnitudeDenominator(camera); - appendSignedEvent(out, wrapAngleRad(goal.v - sphericalState.v) / moveDenom, CVirtualGimbalEvent::MoveRight, CVirtualGimbalEvent::MoveLeft); - appendSignedEvent(out, wrapAngleRad(goal.u - sphericalState.u) / moveDenom, CVirtualGimbalEvent::MoveUp, CVirtualGimbalEvent::MoveDown); + appendSignedEvent(out, hlsl::wrapAngleRad(goal.v - sphericalState.v) / moveDenom, CVirtualGimbalEvent::MoveRight, CVirtualGimbalEvent::MoveLeft); + appendSignedEvent(out, hlsl::wrapAngleRad(goal.u - sphericalState.u) / moveDenom, CVirtualGimbalEvent::MoveUp, CVirtualGimbalEvent::MoveDown); appendSignedEvent(out, static_cast(goal.distance - sphericalState.distance) / 0.01, CVirtualGimbalEvent::MoveForward, CVirtualGimbalEvent::MoveBackward); return !out.empty(); } @@ -569,9 +569,9 @@ class CCameraGoalSolver { const double rotationDenom = getRotationMagnitudeDenominator(camera); if (allowYaw) - appendSignedEvent(out, wrapAngleRad(goal.u - sphericalState.u) / rotationDenom, CVirtualGimbalEvent::PanRight, CVirtualGimbalEvent::PanLeft); + appendSignedEvent(out, hlsl::wrapAngleRad(goal.u - sphericalState.u) / rotationDenom, CVirtualGimbalEvent::PanRight, CVirtualGimbalEvent::PanLeft); if (allowPitch) - appendSignedEvent(out, wrapAngleRad(goal.v - sphericalState.v) / rotationDenom, CVirtualGimbalEvent::TiltUp, CVirtualGimbalEvent::TiltDown); + appendSignedEvent(out, hlsl::wrapAngleRad(goal.v - sphericalState.v) / rotationDenom, CVirtualGimbalEvent::TiltUp, CVirtualGimbalEvent::TiltDown); if (distancePositive != CVirtualGimbalEvent::None && distanceNegative != CVirtualGimbalEvent::None) appendSignedEvent(out, static_cast(goal.distance - sphericalState.distance) / 0.01, distancePositive, distanceNegative); return !out.empty(); @@ -596,7 +596,7 @@ class CCameraGoalSolver const double moveDenom = getMoveMagnitudeDenominator(camera); appendSignedEvent(out, (desiredRadius - currentRadius) / moveDenom, CVirtualGimbalEvent::MoveRight, CVirtualGimbalEvent::MoveLeft); appendSignedEvent(out, (desiredHeight - currentHeight) / moveDenom, CVirtualGimbalEvent::MoveUp, CVirtualGimbalEvent::MoveDown); - appendSignedEvent(out, wrapAngleRad(desiredAngle - currentAngle) / moveDenom, CVirtualGimbalEvent::MoveForward, CVirtualGimbalEvent::MoveBackward); + appendSignedEvent(out, hlsl::wrapAngleRad(desiredAngle - currentAngle) / moveDenom, CVirtualGimbalEvent::MoveForward, CVirtualGimbalEvent::MoveBackward); return !out.empty(); } @@ -649,7 +649,7 @@ class CCameraGoalSolver const auto forward = gimbal.getZAxis(); const auto deltaWorld = target.position - currentPos; - const float64_t3 localDelta( + const hlsl::float64_t3 localDelta( hlsl::dot(deltaWorld, right), hlsl::dot(deltaWorld, up), hlsl::dot(deltaWorld, forward)); @@ -668,8 +668,8 @@ class CCameraGoalSolver const double rotScale = camera->getRotationSpeedScale(); const double invScale = rotScale == 0.0 ? 1.0 : (1.0 / rotScale); - const double deltaPitch = wrapAngleRad(tgtPitch - curPitch) * invScale; - const double deltaYaw = wrapAngleRad(tgtYaw - curYaw) * invScale; + const double deltaPitch = hlsl::wrapAngleRad(tgtPitch - curPitch) * invScale; + const double deltaYaw = hlsl::wrapAngleRad(tgtYaw - curYaw) * invScale; appendSignedEvent(out, deltaPitch, CVirtualGimbalEvent::TiltUp, CVirtualGimbalEvent::TiltDown); appendSignedEvent(out, deltaYaw, CVirtualGimbalEvent::PanRight, CVirtualGimbalEvent::PanLeft); @@ -677,7 +677,7 @@ class CCameraGoalSolver case ICamera::CameraKind::Free: { - const auto deltaQuat = inverseQuaternion(gimbal.getOrientation()) * normalizeQuaternion(target.orientation); + const auto deltaQuat = hlsl::inverseQuaternion(gimbal.getOrientation()) * hlsl::normalizeQuaternion(target.orientation); const auto angles = extractYawPitchRollYXZ(deltaQuat); appendSignedEvent(out, angles.x, CVirtualGimbalEvent::TiltUp, CVirtualGimbalEvent::TiltDown); diff --git a/common/include/camera/CCameraManipulationUtilities.hpp b/common/include/camera/CCameraManipulationUtilities.hpp index d35d0de81..07cdbf7b8 100644 --- a/common/include/camera/CCameraManipulationUtilities.hpp +++ b/common/include/camera/CCameraManipulationUtilities.hpp @@ -32,7 +32,7 @@ struct SCameraConstraintSettings }; //! Apply an authored world-space reference frame through the shared camera runtime entry point. -inline bool applyReferenceFrameToCamera(ICamera* camera, const float64_t4x4& referenceFrame) +inline bool applyReferenceFrameToCamera(ICamera* camera, const hlsl::float64_t4x4& referenceFrame) { if (!camera) return false; @@ -69,7 +69,7 @@ inline void remapTranslationEventsFromWorldToCameraLocal(ICamera* camera, std::v if (!camera) return; - float64_t3 worldDelta = float64_t3(0.0); + hlsl::float64_t3 worldDelta = hlsl::float64_t3(0.0); std::vector filtered; filtered.reserve(events.size()); @@ -102,7 +102,7 @@ inline void remapTranslationEventsFromWorldToCameraLocal(ICamera* camera, std::v const auto up = gimbal.getYAxis(); const auto forward = gimbal.getZAxis(); - const float64_t3 localDelta = float64_t3( + const hlsl::float64_t3 localDelta = hlsl::float64_t3( hlsl::dot(worldDelta, right), hlsl::dot(worldDelta, up), hlsl::dot(worldDelta, forward) @@ -152,7 +152,7 @@ inline bool applyCameraConstraints(const CCameraGoalSolver& solver, ICamera* cam const auto& gimbal = camera->getGimbal(); const auto pos = gimbal.getPosition(); - const auto eulerDeg = getQuaternionEulerDegrees(gimbal.getOrientation()); + const auto eulerDeg = hlsl::getQuaternionEulerDegrees(gimbal.getOrientation()); auto clamped = eulerDeg; if (constraints.clampPitch) @@ -167,7 +167,7 @@ inline bool applyCameraConstraints(const CCameraGoalSolver& solver, ICamera* cam CCameraPreset preset; preset.goal.position = pos; - preset.goal.orientation = makeQuaternionFromEulerDegrees(clamped); + preset.goal.orientation = hlsl::makeQuaternionFromEulerDegrees(clamped); return applyPreset(solver, camera, preset); } diff --git a/common/include/camera/CCameraSequenceScript.hpp b/common/include/camera/CCameraSequenceScript.hpp index 37f15aeec..945ba71f4 100644 --- a/common/include/camera/CCameraSequenceScript.hpp +++ b/common/include/camera/CCameraSequenceScript.hpp @@ -66,13 +66,13 @@ struct CCameraSequenceContinuitySettings struct CCameraSequenceGoalDelta { bool hasPositionOffset = false; - float64_t3 positionOffset = float64_t3(0.0); + hlsl::float64_t3 positionOffset = hlsl::float64_t3(0.0); bool hasRotationEulerDegOffset = false; - float32_t3 rotationEulerDegOffset = float32_t3(0.f); + hlsl::float32_t3 rotationEulerDegOffset = hlsl::float32_t3(0.f); bool hasTargetOffset = false; - float64_t3 targetOffset = float64_t3(0.0); + hlsl::float64_t3 targetOffset = hlsl::float64_t3(0.0); bool hasOrbitUDeltaDeg = false; double orbitUDeltaDeg = 0.0; @@ -113,18 +113,18 @@ struct CCameraSequenceKeyframe //! Concrete tracked-target pose sampled from a shared authored sequence. struct CCameraSequenceTrackedTargetPose { - float64_t3 position = float64_t3(0.0); - camera_quaternion_t orientation = makeIdentityQuaternion(); + hlsl::float64_t3 position = hlsl::float64_t3(0.0); + hlsl::camera_quaternion_t orientation = hlsl::makeIdentityQuaternion(); }; //! Relative tracked-target adjustment authored against an initial tracked-target pose. struct CCameraSequenceTrackedTargetDelta { bool hasPositionOffset = false; - float64_t3 positionOffset = float64_t3(0.0); + hlsl::float64_t3 positionOffset = hlsl::float64_t3(0.0); bool hasRotationEulerDegOffset = false; - float32_t3 rotationEulerDegOffset = float32_t3(0.f); + hlsl::float32_t3 rotationEulerDegOffset = hlsl::float32_t3(0.f); }; //! One authored tracked-target keyframe inside a reusable camera-sequence segment. @@ -133,9 +133,9 @@ struct CCameraSequenceTrackedTargetKeyframe { float time = 0.f; bool hasAbsolutePosition = false; - float64_t3 absolutePosition = float64_t3(0.0); + hlsl::float64_t3 absolutePosition = hlsl::float64_t3(0.0); bool hasAbsoluteRotationEulerDeg = false; - float32_t3 absoluteRotationEulerDeg = float32_t3(0.f); + hlsl::float32_t3 absoluteRotationEulerDeg = hlsl::float32_t3(0.f); bool hasDelta = false; CCameraSequenceTrackedTargetDelta delta = {}; }; @@ -302,7 +302,7 @@ inline bool applyCanonicalSphericalGoal(CCameraGoal& goal) return false; const float appliedDistance = std::clamp(goal.orbitDistance, ICamera::SphericalMinDistance, ICamera::SphericalMaxDistance); - const float64_t3 spherePosition( + const hlsl::float64_t3 spherePosition( std::cos(goal.orbitV) * std::cos(goal.orbitU) * static_cast(appliedDistance), std::cos(goal.orbitV) * std::sin(goal.orbitU) * static_cast(appliedDistance), std::sin(goal.orbitV) * static_cast(appliedDistance)); @@ -311,13 +311,13 @@ inline bool applyCanonicalSphericalGoal(CCameraGoal& goal) goal.distance = appliedDistance; goal.orbitDistance = appliedDistance; - const auto forward = normalize(-spherePosition); - const float64_t3 up = normalize(float64_t3( + const auto forward = hlsl::normalize(-spherePosition); + const hlsl::float64_t3 up = hlsl::normalize(hlsl::float64_t3( -std::sin(goal.orbitV) * std::cos(goal.orbitU), -std::sin(goal.orbitV) * std::sin(goal.orbitU), std::cos(goal.orbitV))); - const float64_t3 right = normalize(cross(up, forward)); - goal.orientation = makeQuaternionFromBasis(right, up, forward); + const hlsl::float64_t3 right = hlsl::normalize(hlsl::cross(up, forward)); + goal.orientation = hlsl::makeQuaternionFromBasis(right, up, forward); return true; } @@ -356,7 +356,7 @@ inline bool buildSequenceKeyframePreset(const CCameraPreset& reference, const CC if (delta.hasRotationEulerDegOffset) { - goal.orientation = normalizeQuaternion(goal.orientation * makeQuaternionFromEulerDegrees(getCastedVector(delta.rotationEulerDegOffset))); + goal.orientation = hlsl::normalizeQuaternion(goal.orientation * hlsl::makeQuaternionFromEulerDegrees(hlsl::getCastedVector(delta.rotationEulerDegOffset))); } if (delta.hasTargetOffset) @@ -379,7 +379,7 @@ inline bool buildSequenceKeyframePreset(const CCameraPreset& reference, const CC return false; } if (delta.hasOrbitUDeltaDeg) - goal.orbitU = wrapAngleRad(goal.orbitU + hlsl::radians(delta.orbitUDeltaDeg)); + goal.orbitU = hlsl::wrapAngleRad(goal.orbitU + hlsl::radians(delta.orbitUDeltaDeg)); if (delta.hasOrbitVDeltaDeg) goal.orbitV = std::clamp(goal.orbitV + hlsl::radians(delta.orbitVDeltaDeg), -1.55334303427, 1.55334303427); if (delta.hasOrbitDistanceDelta) @@ -395,7 +395,7 @@ inline bool buildSequenceKeyframePreset(const CCameraPreset& reference, const CC return false; } if (delta.hasPathAngleDeltaDeg) - goal.pathState.angle = wrapAngleRad(goal.pathState.angle + hlsl::radians(delta.pathAngleDeltaDeg)); + goal.pathState.angle = hlsl::wrapAngleRad(goal.pathState.angle + hlsl::radians(delta.pathAngleDeltaDeg)); if (delta.hasPathRadiusDelta) goal.pathState.radius += delta.pathRadiusDelta; if (delta.hasPathHeightDelta) @@ -485,14 +485,14 @@ inline bool buildSequenceTrackedTargetPoseFromReference( if (authored.hasAbsolutePosition) outPose.position = authored.absolutePosition; if (authored.hasAbsoluteRotationEulerDeg) - outPose.orientation = makeQuaternionFromEulerDegrees(getCastedVector(authored.absoluteRotationEulerDeg)); + outPose.orientation = hlsl::makeQuaternionFromEulerDegrees(hlsl::getCastedVector(authored.absoluteRotationEulerDeg)); if (authored.hasDelta) { if (authored.delta.hasPositionOffset) outPose.position += authored.delta.positionOffset; if (authored.delta.hasRotationEulerDegOffset) - outPose.orientation = normalizeQuaternion(outPose.orientation * makeQuaternionFromEulerDegrees(getCastedVector(authored.delta.rotationEulerDegOffset))); + outPose.orientation = hlsl::normalizeQuaternion(outPose.orientation * hlsl::makeQuaternionFromEulerDegrees(hlsl::getCastedVector(authored.delta.rotationEulerDegOffset))); } if (!isSequenceTrackedTargetPoseFinite(outPose)) @@ -573,7 +573,7 @@ inline bool tryBuildSequenceTrackedTargetPoseAtTime( const auto span = std::max(1e-6f, rhs.time - lhs.time); const auto alpha = std::clamp((time - lhs.time) / span, 0.f, 1.f); outPose.position = lhs.pose.position + (rhs.pose.position - lhs.pose.position) * static_cast(alpha); - outPose.orientation = slerpQuaternion(lhs.pose.orientation, rhs.pose.orientation, static_cast(alpha)); + outPose.orientation = hlsl::slerpQuaternion(lhs.pose.orientation, rhs.pose.orientation, static_cast(alpha)); return true; } diff --git a/common/include/camera/CChaseCamera.hpp b/common/include/camera/CChaseCamera.hpp index c7be77460..9018107b1 100644 --- a/common/include/camera/CChaseCamera.hpp +++ b/common/include/camera/CChaseCamera.hpp @@ -14,7 +14,7 @@ class CChaseCamera final : public CSphericalTargetCamera public: using base_t = CSphericalTargetCamera; - CChaseCamera(const float64_t3& position, const float64_t3& target) + CChaseCamera(const hlsl::float64_t3& position, const hlsl::float64_t3& target) : base_t(position, target) { m_v = std::clamp(m_v, MinPitch, MaxPitch); @@ -24,7 +24,7 @@ class CChaseCamera final : public CSphericalTargetCamera const typename base_t::CGimbal& getGimbal() override { return m_gimbal; } - virtual bool manipulate(std::span virtualEvents, const float64_t4x4* referenceFrame = nullptr) override + virtual bool manipulate(std::span virtualEvents, const hlsl::float64_t4x4* referenceFrame = nullptr) override { if (not virtualEvents.size() and not referenceFrame) return false; @@ -39,20 +39,20 @@ class CChaseCamera final : public CSphericalTargetCamera const auto basis = computeBasis(m_u, m_v, m_distance); - float64_t3 planarForward = float64_t3(basis.forward.x, 0.0, basis.forward.z); - float64_t3 planarRight = float64_t3(basis.right.x, 0.0, basis.right.z); + hlsl::float64_t3 planarForward = hlsl::float64_t3(basis.forward.x, 0.0, basis.forward.z); + hlsl::float64_t3 planarRight = hlsl::float64_t3(basis.right.x, 0.0, basis.right.z); - const double forwardLen = length(planarForward); + const double forwardLen = hlsl::length(planarForward); if (forwardLen > 0.0) planarForward /= forwardLen; else - planarForward = float64_t3(0.0, 0.0, 1.0); + planarForward = hlsl::float64_t3(0.0, 0.0, 1.0); - const double rightLen = length(planarRight); + const double rightLen = hlsl::length(planarRight); if (rightLen > 0.0) planarRight /= rightLen; else - planarRight = float64_t3(1.0, 0.0, 0.0); + planarRight = hlsl::float64_t3(1.0, 0.0, 0.0); m_targetPosition += (planarRight * impulse.dVirtualTranslate.x + planarForward * impulse.dVirtualTranslate.z) * moveScalar; m_distance = std::clamp(m_distance + static_cast(impulse.dVirtualTranslate.y * translateScalar), MinDistance, MaxDistance); diff --git a/common/include/camera/CCubeProjection.hpp b/common/include/camera/CCubeProjection.hpp index 4afa38f2c..3e0bb0126 100644 --- a/common/include/camera/CCubeProjection.hpp +++ b/common/include/camera/CCubeProjection.hpp @@ -70,7 +70,7 @@ class CCubeProjection final : public IPerspectiveProjection, public IProjection virtual void project(const projection_vector_t& vecToProjectionSpace, projection_vector_t& output) const override { - auto direction = normalize(vecToProjectionSpace); + auto direction = hlsl::normalize(vecToProjectionSpace); // Cube-face projection is not implemented yet. } diff --git a/common/include/camera/CDollyCamera.hpp b/common/include/camera/CDollyCamera.hpp index 990b9a3d6..7b8455ccf 100644 --- a/common/include/camera/CDollyCamera.hpp +++ b/common/include/camera/CDollyCamera.hpp @@ -14,7 +14,7 @@ class CDollyCamera final : public CSphericalTargetCamera public: using base_t = CSphericalTargetCamera; - CDollyCamera(const float64_t3& position, const float64_t3& target) + CDollyCamera(const hlsl::float64_t3& position, const hlsl::float64_t3& target) : base_t(position, target) { m_v = std::clamp(m_v, MinPitch, MaxPitch); @@ -24,7 +24,7 @@ class CDollyCamera final : public CSphericalTargetCamera const typename base_t::CGimbal& getGimbal() override { return m_gimbal; } - virtual bool manipulate(std::span virtualEvents, const float64_t4x4* referenceFrame = nullptr) override + virtual bool manipulate(std::span virtualEvents, const hlsl::float64_t4x4* referenceFrame = nullptr) override { if (not virtualEvents.size() and not referenceFrame) return false; diff --git a/common/include/camera/CDollyZoomCamera.hpp b/common/include/camera/CDollyZoomCamera.hpp index cabb24974..699c5f454 100644 --- a/common/include/camera/CDollyZoomCamera.hpp +++ b/common/include/camera/CDollyZoomCamera.hpp @@ -14,7 +14,7 @@ class CDollyZoomCamera final : public CSphericalTargetCamera public: using base_t = CSphericalTargetCamera; - CDollyZoomCamera(const float64_t3& position, const float64_t3& target, float baseFov = 40.0f) + CDollyZoomCamera(const hlsl::float64_t3& position, const hlsl::float64_t3& target, float baseFov = 40.0f) : base_t(position, target), m_baseFov(baseFov), m_referenceDistance(m_distance) { applyPose(); @@ -38,7 +38,7 @@ class CDollyZoomCamera final : public CSphericalTargetCamera return static_cast(std::clamp(fovDeg, 10.0, 150.0)); } - virtual bool manipulate(std::span virtualEvents, const float64_t4x4* referenceFrame = nullptr) override + virtual bool manipulate(std::span virtualEvents, const hlsl::float64_t4x4* referenceFrame = nullptr) override { if (not virtualEvents.size() and not referenceFrame) return false; diff --git a/common/include/camera/CFPSCamera.hpp b/common/include/camera/CFPSCamera.hpp index 754e5a1bf..4a4145a7e 100644 --- a/common/include/camera/CFPSCamera.hpp +++ b/common/include/camera/CFPSCamera.hpp @@ -19,7 +19,7 @@ class CFPSCamera final : public ICamera static inline constexpr float HalfPi = 1.57079632679489661923f; static inline constexpr float RadToDeg = 57.2957795130823208768f; - CFPSCamera(const float64_t3& position, const camera_quaternion_t& orientation = makeIdentityQuaternion()) + CFPSCamera(const hlsl::float64_t3& position, const hlsl::camera_quaternion_t& orientation = hlsl::makeIdentityQuaternion()) : base_t(), m_gimbal({ .position = position, .orientation = orientation }) { m_gimbal.begin(); @@ -30,7 +30,7 @@ class CFPSCamera final : public ICamera const float gForwardZ = static_cast(gForward.z); const float gPitch = std::atan2(std::hypot(gForwardX, gForwardZ), gForwardY) - HalfPi; const float gYaw = std::atan2(gForwardX, gForwardZ); - m_gimbal.setOrientation(makeQuaternionFromEulerRadians(float64_t3(gPitch, gYaw, 0.0f))); + m_gimbal.setOrientation(hlsl::makeQuaternionFromEulerRadians(hlsl::float64_t3(gPitch, gYaw, 0.0f))); } m_gimbal.end(); } @@ -41,7 +41,7 @@ class CFPSCamera final : public ICamera return m_gimbal; } - virtual bool manipulate(std::span virtualEvents, const float64_t4x4* referenceFrame = nullptr) override + virtual bool manipulate(std::span virtualEvents, const hlsl::float64_t4x4* referenceFrame = nullptr) override { if (not virtualEvents.size() and not referenceFrame) return false; @@ -86,8 +86,8 @@ class CFPSCamera final : public ICamera const float newPitch = std::clamp(rPitch + impulse.dVirtualRotation.x * getRotationSpeedScale(), MinVerticalAngle, MaxVerticalAngle), newYaw = gYaw + impulse.dVirtualRotation.y * getRotationSpeedScale(); if (validateReference()) - m_gimbal.setOrientation(makeQuaternionFromEulerRadians(float64_t3(newPitch, newYaw, 0.0f))); - m_gimbal.setPosition(float64_t3(reference.frame[3]) + rotateVectorByQuaternion(reference.orientation, float64_t3(impulse.dVirtualTranslate))); + m_gimbal.setOrientation(hlsl::makeQuaternionFromEulerRadians(hlsl::float64_t3(newPitch, newYaw, 0.0f))); + m_gimbal.setPosition(hlsl::float64_t3(reference.frame[3]) + hlsl::rotateVectorByQuaternion(reference.orientation, hlsl::float64_t3(impulse.dVirtualTranslate))); } m_gimbal.end(); diff --git a/common/include/camera/CFreeLockCamera.hpp b/common/include/camera/CFreeLockCamera.hpp index ab2bc8bf9..3c4c57a8c 100644 --- a/common/include/camera/CFreeLockCamera.hpp +++ b/common/include/camera/CFreeLockCamera.hpp @@ -15,7 +15,7 @@ class CFreeCamera final : public ICamera public: using base_t = ICamera; - CFreeCamera(const float64_t3& position, const camera_quaternion_t& orientation = makeIdentityQuaternion()) + CFreeCamera(const hlsl::float64_t3& position, const hlsl::camera_quaternion_t& orientation = hlsl::makeIdentityQuaternion()) : base_t(), m_gimbal({ .position = position, .orientation = orientation }) {} ~CFreeCamera() = default; @@ -24,7 +24,7 @@ class CFreeCamera final : public ICamera return m_gimbal; } - virtual bool manipulate(std::span virtualEvents, const float64_t4x4* referenceFrame = nullptr) override + virtual bool manipulate(std::span virtualEvents, const hlsl::float64_t4x4* referenceFrame = nullptr) override { if (not virtualEvents.size() and not referenceFrame) return false; @@ -39,12 +39,12 @@ class CFreeCamera final : public ICamera m_gimbal.begin(); { - const auto pitch = makeQuaternionFromAxisAngle(normalize(float64_t3(reference.frame[0])), impulse.dVirtualRotation.x); - const auto yaw = makeQuaternionFromAxisAngle(normalize(float64_t3(reference.frame[1])), impulse.dVirtualRotation.y); - const auto roll = makeQuaternionFromAxisAngle(normalize(float64_t3(reference.frame[2])), impulse.dVirtualRotation.z); + const auto pitch = hlsl::makeQuaternionFromAxisAngle(hlsl::normalize(hlsl::float64_t3(reference.frame[0])), impulse.dVirtualRotation.x); + const auto yaw = hlsl::makeQuaternionFromAxisAngle(hlsl::normalize(hlsl::float64_t3(reference.frame[1])), impulse.dVirtualRotation.y); + const auto roll = hlsl::makeQuaternionFromAxisAngle(hlsl::normalize(hlsl::float64_t3(reference.frame[2])), impulse.dVirtualRotation.z); - m_gimbal.setOrientation(normalizeQuaternion(yaw * pitch * roll * reference.orientation)); - m_gimbal.setPosition(float64_t3(reference.frame[3]) + rotateVectorByQuaternion(reference.orientation, float64_t3(impulse.dVirtualTranslate))); + m_gimbal.setOrientation(hlsl::normalizeQuaternion(yaw * pitch * roll * reference.orientation)); + m_gimbal.setPosition(hlsl::float64_t3(reference.frame[3]) + hlsl::rotateVectorByQuaternion(reference.orientation, hlsl::float64_t3(impulse.dVirtualTranslate))); } m_gimbal.end(); diff --git a/common/include/camera/CGeneralPurposeGimbal.hpp b/common/include/camera/CGeneralPurposeGimbal.hpp index 422d4a326..d48add267 100644 --- a/common/include/camera/CGeneralPurposeGimbal.hpp +++ b/common/include/camera/CGeneralPurposeGimbal.hpp @@ -5,7 +5,7 @@ namespace nbl::core { - template + template class CGeneralPurposeGimbal : public IGimbal { public: diff --git a/common/include/camera/CIsometricCamera.hpp b/common/include/camera/CIsometricCamera.hpp index 0a19121fa..68c8ff96d 100644 --- a/common/include/camera/CIsometricCamera.hpp +++ b/common/include/camera/CIsometricCamera.hpp @@ -14,7 +14,7 @@ class CIsometricCamera final : public CSphericalTargetCamera public: using base_t = CSphericalTargetCamera; - CIsometricCamera(const float64_t3& position, const float64_t3& target) + CIsometricCamera(const hlsl::float64_t3& position, const hlsl::float64_t3& target) : base_t(position, target) { m_u = IsoYaw; @@ -25,7 +25,7 @@ class CIsometricCamera final : public CSphericalTargetCamera const typename base_t::CGimbal& getGimbal() override { return m_gimbal; } - virtual bool manipulate(std::span virtualEvents, const float64_t4x4* referenceFrame = nullptr) override + virtual bool manipulate(std::span virtualEvents, const hlsl::float64_t4x4* referenceFrame = nullptr) override { if (not virtualEvents.size() and not referenceFrame) return false; diff --git a/common/include/camera/COrbitCamera.hpp b/common/include/camera/COrbitCamera.hpp index 2ab8b1d12..421d5435a 100644 --- a/common/include/camera/COrbitCamera.hpp +++ b/common/include/camera/COrbitCamera.hpp @@ -13,17 +13,17 @@ class COrbitCamera final : public CSphericalTargetCamera public: using base_t = CSphericalTargetCamera; - COrbitCamera(const float64_t3& position, const float64_t3& target) + COrbitCamera(const hlsl::float64_t3& position, const hlsl::float64_t3& target) : base_t(position, target) { - m_distance = std::clamp(length(m_targetPosition - position), MinDistance, MaxDistance); + m_distance = std::clamp(hlsl::length(m_targetPosition - position), MinDistance, MaxDistance); applyPose(); } ~COrbitCamera() = default; const typename base_t::CGimbal& getGimbal() override { return m_gimbal; } - virtual bool manipulate(std::span virtualEvents, const float64_t4x4* referenceFrame = nullptr) override + virtual bool manipulate(std::span virtualEvents, const hlsl::float64_t4x4* referenceFrame = nullptr) override { auto impulse = m_gimbal.accumulate(virtualEvents); double deltaU = impulse.dVirtualTranslate.y, deltaV = impulse.dVirtualTranslate.x, deltaDistance = impulse.dVirtualTranslate.z; diff --git a/common/include/camera/CPathCamera.hpp b/common/include/camera/CPathCamera.hpp index 3f12ca923..25cc8cd35 100644 --- a/common/include/camera/CPathCamera.hpp +++ b/common/include/camera/CPathCamera.hpp @@ -14,7 +14,7 @@ class CPathCamera final : public CSphericalTargetCamera public: using base_t = CSphericalTargetCamera; - CPathCamera(const float64_t3& position, const float64_t3& target) + CPathCamera(const hlsl::float64_t3& position, const hlsl::float64_t3& target) : base_t(position, target) { const auto offset = position - target; @@ -29,7 +29,7 @@ class CPathCamera final : public CSphericalTargetCamera const typename base_t::CGimbal& getGimbal() override { return m_gimbal; } - virtual bool manipulate(std::span virtualEvents, const float64_t4x4* referenceFrame = nullptr) override + virtual bool manipulate(std::span virtualEvents, const hlsl::float64_t4x4* referenceFrame = nullptr) override { if (not virtualEvents.size() and not referenceFrame) return false; @@ -105,7 +105,7 @@ class CPathCamera final : public CSphericalTargetCamera { const double x = std::cos(m_pathAngle) * m_pathRadius; const double z = std::sin(m_pathAngle) * m_pathRadius; - const float64_t3 position = m_targetPosition + float64_t3(x, m_pathHeight, z); + const hlsl::float64_t3 position = m_targetPosition + hlsl::float64_t3(x, m_pathHeight, z); initFromPosition(position); return applyPose(); } diff --git a/common/include/camera/CSphericalTargetCamera.hpp b/common/include/camera/CSphericalTargetCamera.hpp index c74f8e5dd..667b427f4 100644 --- a/common/include/camera/CSphericalTargetCamera.hpp +++ b/common/include/camera/CSphericalTargetCamera.hpp @@ -19,9 +19,9 @@ class CSphericalTargetCamera : public ICamera public: using base_t = ICamera; - CSphericalTargetCamera(const float64_t3& position, const float64_t3& target) + CSphericalTargetCamera(const hlsl::float64_t3& position, const hlsl::float64_t3& target) : base_t(), m_targetPosition(target), m_distance(1.0f), - m_gimbal({ .position = position, .orientation = makeIdentityQuaternion() }) + m_gimbal({ .position = position, .orientation = hlsl::makeIdentityQuaternion() }) { initFromPosition(position); } @@ -38,14 +38,14 @@ class CSphericalTargetCamera : public ICamera return ok; } - inline void target(const float64_t3& p) + inline void target(const hlsl::float64_t3& p) { if (m_targetPosition == p) return; m_targetPosition = p; applyPose(); } - inline float64_t3 getTarget() const { return m_targetPosition; } + inline hlsl::float64_t3 getTarget() const { return m_targetPosition; } inline float getDistance() const { return m_distance; } inline double getU() const { return m_u; } @@ -70,7 +70,7 @@ class CSphericalTargetCamera : public ICamera return true; } - virtual bool trySetSphericalTarget(const float64_t3& targetPosition) override + virtual bool trySetSphericalTarget(const hlsl::float64_t3& targetPosition) override { target(targetPosition); return true; @@ -84,15 +84,15 @@ class CSphericalTargetCamera : public ICamera protected: struct SphericalBasis { - float64_t3 localSpherePosition; - float64_t3 right; - float64_t3 up; - float64_t3 forward; + hlsl::float64_t3 localSpherePosition; + hlsl::float64_t3 right; + hlsl::float64_t3 up; + hlsl::float64_t3 forward; }; - inline float64_t3 S(double su, double sv) const + inline hlsl::float64_t3 S(double su, double sv) const { - return float64_t3 + return hlsl::float64_t3 { std::cos(sv) * std::cos(su), std::cos(sv) * std::sin(su), @@ -100,9 +100,9 @@ class CSphericalTargetCamera : public ICamera }; } - inline float64_t3 Sdv(double su, double sv) const + inline hlsl::float64_t3 Sdv(double su, double sv) const { - return float64_t3 + return hlsl::float64_t3 { -std::sin(sv) * std::cos(su), -std::sin(sv) * std::sin(su), @@ -113,17 +113,17 @@ class CSphericalTargetCamera : public ICamera inline SphericalBasis computeBasis(double su, double sv, float distance) const { const auto localSpherePosition = S(su, sv) * static_cast(distance); - const auto forward = normalize(-localSpherePosition); - const auto up = normalize(Sdv(su, sv)); - const auto right = normalize(cross(up, forward)); + const auto forward = hlsl::normalize(-localSpherePosition); + const auto up = hlsl::normalize(Sdv(su, sv)); + const auto right = hlsl::normalize(hlsl::cross(up, forward)); return { localSpherePosition, right, up, forward }; } - inline void initFromPosition(const float64_t3& position) + inline void initFromPosition(const hlsl::float64_t3& position) { const auto offset = position - m_targetPosition; - const double dist = length(offset); + const double dist = hlsl::length(offset); const double safeDist = std::isfinite(dist) && dist > 0.0 ? dist : static_cast(MinDistance); m_distance = std::clamp(static_cast(safeDist), MinDistance, MaxDistance); const auto local = offset / static_cast(m_distance); @@ -135,7 +135,7 @@ class CSphericalTargetCamera : public ICamera { const auto basis = computeBasis(m_u, m_v, m_distance); const auto newPosition = basis.localSpherePosition + m_targetPosition; - const auto newOrientation = makeQuaternionFromBasis(basis.right, basis.up, basis.forward); + const auto newOrientation = hlsl::makeQuaternionFromBasis(basis.right, basis.up, basis.forward); m_gimbal.begin(); { @@ -151,7 +151,7 @@ class CSphericalTargetCamera : public ICamera return manipulated; } - float64_t3 m_targetPosition; + hlsl::float64_t3 m_targetPosition; float m_distance; typename base_t::CGimbal m_gimbal; double m_u = {}; diff --git a/common/include/camera/CTopDownCamera.hpp b/common/include/camera/CTopDownCamera.hpp index 011fab01b..a558eb25b 100644 --- a/common/include/camera/CTopDownCamera.hpp +++ b/common/include/camera/CTopDownCamera.hpp @@ -14,7 +14,7 @@ class CTopDownCamera final : public CSphericalTargetCamera public: using base_t = CSphericalTargetCamera; - CTopDownCamera(const float64_t3& position, const float64_t3& target) + CTopDownCamera(const hlsl::float64_t3& position, const hlsl::float64_t3& target) : base_t(position, target) { m_v = TopDownPitch; @@ -24,7 +24,7 @@ class CTopDownCamera final : public CSphericalTargetCamera const typename base_t::CGimbal& getGimbal() override { return m_gimbal; } - virtual bool manipulate(std::span virtualEvents, const float64_t4x4* referenceFrame = nullptr) override + virtual bool manipulate(std::span virtualEvents, const hlsl::float64_t4x4* referenceFrame = nullptr) override { if (not virtualEvents.size() and not referenceFrame) return false; diff --git a/common/include/camera/CTurntableCamera.hpp b/common/include/camera/CTurntableCamera.hpp index b340bf85a..abbcd086a 100644 --- a/common/include/camera/CTurntableCamera.hpp +++ b/common/include/camera/CTurntableCamera.hpp @@ -18,7 +18,7 @@ class CTurntableCamera final : public CSphericalTargetCamera public: using base_t = CSphericalTargetCamera; - CTurntableCamera(const float64_t3& position, const float64_t3& target) + CTurntableCamera(const hlsl::float64_t3& position, const hlsl::float64_t3& target) : base_t(position, target) { m_v = std::clamp(m_v, MinPitch, MaxPitch); @@ -28,7 +28,7 @@ class CTurntableCamera final : public CSphericalTargetCamera const typename base_t::CGimbal& getGimbal() override { return m_gimbal; } - virtual bool manipulate(std::span virtualEvents, const float64_t4x4* referenceFrame = nullptr) override + virtual bool manipulate(std::span virtualEvents, const hlsl::float64_t4x4* referenceFrame = nullptr) override { if (not virtualEvents.size() and not referenceFrame) return false; diff --git a/common/include/camera/ICamera.hpp b/common/include/camera/ICamera.hpp index 1a613e411..285996978 100644 --- a/common/include/camera/ICamera.hpp +++ b/common/include/camera/ICamera.hpp @@ -66,7 +66,7 @@ class ICamera : virtual public core::IReferenceCounted struct SphericalTargetState { - float64_t3 target = float64_t3(0.0); + hlsl::float64_t3 target = hlsl::float64_t3(0.0); float distance = 0.f; double u = 0.0; double v = 0.0; @@ -88,10 +88,10 @@ class ICamera : virtual public core::IReferenceCounted }; //! Gimbal that models the camera pose and cached view matrix in world space. - class CGimbal : public IGimbal + class CGimbal : public IGimbal { public: - using base_t = IGimbal; + using base_t = IGimbal; CGimbal(typename base_t::SCreationParameters&& parameters) : base_t(std::move(parameters)) { updateView(); } ~CGimbal() = default; @@ -100,19 +100,19 @@ class ICamera : virtual public core::IReferenceCounted { const auto& gRight = base_t::getXAxis(), gUp = base_t::getYAxis(), gForward = base_t::getZAxis(); - assert(isOrthoBase(gRight, gUp, gForward)); + assert(hlsl::isOrthoBase(gRight, gUp, gForward)); const auto& position = base_t::getPosition(); - m_viewMatrix[0u] = float64_t4(gRight, -hlsl::dot(gRight, position)); - m_viewMatrix[1u] = float64_t4(gUp, -hlsl::dot(gUp, position)); - m_viewMatrix[2u] = float64_t4(gForward, -hlsl::dot(gForward, position)); + m_viewMatrix[0u] = hlsl::float64_t4(gRight, -hlsl::dot(gRight, position)); + m_viewMatrix[1u] = hlsl::float64_t4(gUp, -hlsl::dot(gUp, position)); + m_viewMatrix[2u] = hlsl::float64_t4(gForward, -hlsl::dot(gForward, position)); } - inline const float64_t3x4& getViewMatrix() const { return m_viewMatrix; } + inline const hlsl::float64_t3x4& getViewMatrix() const { return m_viewMatrix; } private: - float64_t3x4 m_viewMatrix; + hlsl::float64_t3x4 m_viewMatrix; }; class SScopedMotionScaleOverride @@ -159,13 +159,13 @@ class ICamera : virtual public core::IReferenceCounted virtual const CGimbal& getGimbal() = 0u; // Camera core contract: consume virtual events only. Raw input binding and absolute goal solving live outside ICamera. - virtual bool manipulate(std::span virtualEvents, const float64_t4x4* referenceFrame = nullptr) = 0; - inline bool manipulateWithMotionScales(std::span virtualEvents, const float64_t4x4* referenceFrame, const double moveScale, const double rotationScale) + virtual bool manipulate(std::span virtualEvents, const hlsl::float64_t4x4* referenceFrame = nullptr) = 0; + inline bool manipulateWithMotionScales(std::span virtualEvents, const hlsl::float64_t4x4* referenceFrame, const double moveScale, const double rotationScale) { auto scopedOverride = overrideMotionScales(moveScale, rotationScale); return manipulate(virtualEvents, referenceFrame); } - inline bool manipulateWithUnitMotionScales(std::span virtualEvents, const float64_t4x4* referenceFrame = nullptr) + inline bool manipulateWithUnitMotionScales(std::span virtualEvents, const hlsl::float64_t4x4* referenceFrame = nullptr) { return manipulateWithMotionScales(virtualEvents, referenceFrame, 1.0, 1.0); } @@ -201,7 +201,7 @@ class ICamera : virtual public core::IReferenceCounted return false; } - virtual bool trySetSphericalTarget(const float64_t3& target) + virtual bool trySetSphericalTarget(const hlsl::float64_t3& target) { return false; } diff --git a/common/include/camera/IGimbal.hpp b/common/include/camera/IGimbal.hpp index 0e3cb1db4..0a007982e 100644 --- a/common/include/camera/IGimbal.hpp +++ b/common/include/camera/IGimbal.hpp @@ -6,63 +6,21 @@ namespace nbl::core { - using hlsl::camera_matrix_t; - using hlsl::camera_quaternion_t; - using hlsl::camera_vector_t; - using hlsl::composeTransformMatrix; - using hlsl::cross; - using hlsl::degrees; - using hlsl::dot; - using hlsl::float32_t; - using hlsl::float32_t3; - using hlsl::float32_t4; - using hlsl::float32_t4x4; - using hlsl::float64_t; - using hlsl::float64_t3; - using hlsl::float64_t4; - using hlsl::float64_t3x4; - using hlsl::float64_t4x4; - using hlsl::getCastedMatrix; - using hlsl::getCastedVector; - using hlsl::getMatrix3x3As4x4; - using hlsl::getMatrix3x4As4x4; - using hlsl::getQuaternionAngularDistanceDegrees; - using hlsl::getQuaternionBasisMatrix; - using hlsl::getQuaternionEulerDegrees; - using hlsl::getQuaternionEulerRadians; - using hlsl::inverseQuaternion; - using hlsl::isFiniteQuaternion; - using hlsl::isOrthoBase; - using hlsl::length; - using hlsl::makeIdentityQuaternion; - using hlsl::makeQuaternionFromAxisAngle; - using hlsl::makeQuaternionFromBasis; - using hlsl::makeQuaternionFromComponents; - using hlsl::makeQuaternionFromEulerDegrees; - using hlsl::makeQuaternionFromEulerRadians; - using hlsl::mul; - using hlsl::normalize; - using hlsl::normalizeQuaternion; - using hlsl::radians; - using hlsl::rotateVectorByQuaternion; - using hlsl::slerpQuaternion; - using hlsl::wrapAngleRad; - struct CReferenceTransform { - float64_t4x4 frame; - camera_quaternion_t orientation = makeIdentityQuaternion(); + hlsl::float64_t4x4 frame; + hlsl::camera_quaternion_t orientation = hlsl::makeIdentityQuaternion(); }; template - requires is_any_of_v + requires is_any_of_v class IGimbal { public: using precision_t = T; - using quaternion_t = camera_quaternion_t; + using quaternion_t = hlsl::camera_quaternion_t; template - using vector_t = camera_vector_t; + using vector_t = hlsl::camera_vector_t; //! underlying type for world matrix (TRS) using model_matrix_t = hlsl::matrix; @@ -169,7 +127,7 @@ namespace nbl::core struct SCreationParameters { vector_t<3u> position; - quaternion_t orientation = makeIdentityQuaternion(); + quaternion_t orientation = hlsl::makeIdentityQuaternion(); }; IGimbal(const IGimbal&) = default; @@ -211,14 +169,14 @@ namespace nbl::core if (m_orientation.data != orientation.data) m_counter++; - m_orientation = normalizeQuaternion(orientation); + m_orientation = hlsl::normalizeQuaternion(orientation); updateOrthonormalOrientationBase(); } inline void transform(const CReferenceTransform& reference, const VirtualImpulse& impulse) { - setOrientation(reference.orientation * makeQuaternionFromEulerRadians(impulse.dVirtualRotation)); - setPosition(mul(float64_t4(impulse.dVirtualTranslate, 1), reference.frame).xyz); + setOrientation(reference.orientation * hlsl::makeQuaternionFromEulerRadians(impulse.dVirtualRotation)); + setPosition(hlsl::mul(hlsl::float64_t4(impulse.dVirtualTranslate, 1), reference.frame).xyz); } inline void rotate(const vector_t<3u>& axis, float dRadians) @@ -228,8 +186,8 @@ namespace nbl::core if(dRadians) m_counter++; - const auto dRotation = makeQuaternionFromAxisAngle(axis, static_cast(dRadians)); - m_orientation = normalizeQuaternion(dRotation * m_orientation); + const auto dRotation = hlsl::makeQuaternionFromAxisAngle(axis, static_cast(dRadians)); + m_orientation = hlsl::normalizeQuaternion(dRotation * m_orientation); updateOrthonormalOrientationBase(); } @@ -287,19 +245,19 @@ namespace nbl::core { return { - camera_vector_t(rotation[0] * scale.x, position.x), - camera_vector_t(rotation[1] * scale.y, position.y), - camera_vector_t(rotation[2] * scale.z, position.z) + hlsl::camera_vector_t(rotation[0] * scale.x, position.x), + hlsl::camera_vector_t(rotation[1] * scale.y, position.y), + hlsl::camera_vector_t(rotation[2] * scale.z, position.z) }; } else { return { - camera_vector_t(rotation[0] * scale.x, T(0)), - camera_vector_t(rotation[1] * scale.y, T(0)), - camera_vector_t(rotation[2] * scale.z, T(0)), - camera_vector_t(position, T(1)) + hlsl::camera_vector_t(rotation[0] * scale.x, T(0)), + hlsl::camera_vector_t(rotation[1] * scale.y, T(0)), + hlsl::camera_vector_t(rotation[2] * scale.z, T(0)), + hlsl::camera_vector_t(position, T(1)) }; } } @@ -328,7 +286,7 @@ namespace nbl::core //! Returns true if gimbal records a manipulation inline bool isManipulating() const { return m_isManipulating; } - bool extractReferenceTransform(CReferenceTransform* out, const float64_t4x4* referenceFrame = nullptr) + bool extractReferenceTransform(CReferenceTransform* out, const hlsl::float64_t4x4* referenceFrame = nullptr) { if (not out) return false; @@ -336,16 +294,16 @@ namespace nbl::core if (referenceFrame) { out->frame = *referenceFrame; - if (not isOrthoBase(float64_t3(out->frame[0]), float64_t3(out->frame[1]), float64_t3(out->frame[2]))) + if (not hlsl::isOrthoBase(hlsl::float64_t3(out->frame[0]), hlsl::float64_t3(out->frame[1]), hlsl::float64_t3(out->frame[2]))) return false; } else { - out->frame = getMatrix3x3As4x4(getOrthonornalMatrix()); - out->frame[3] = float64_t4(getPosition(), 1); + out->frame = hlsl::getMatrix3x3As4x4(getOrthonornalMatrix()); + out->frame[3] = hlsl::float64_t4(getPosition(), 1); } - out->orientation = makeQuaternionFromBasis(float64_t3(out->frame[0]), float64_t3(out->frame[1]), float64_t3(out->frame[2])); + out->orientation = hlsl::makeQuaternionFromBasis(hlsl::float64_t3(out->frame[0]), hlsl::float64_t3(out->frame[1]), hlsl::float64_t3(out->frame[2])); return true; } @@ -353,7 +311,7 @@ namespace nbl::core private: inline void updateOrthonormalOrientationBase() { - m_orthonormal = getQuaternionBasisMatrix(m_orientation); + m_orthonormal = hlsl::getQuaternionBasisMatrix(m_orientation); } //! Position of a gimbal in world space diff --git a/common/include/camera/ILinearProjection.hpp b/common/include/camera/ILinearProjection.hpp index 641a38e53..6e58964d3 100644 --- a/common/include/camera/ILinearProjection.hpp +++ b/common/include/camera/ILinearProjection.hpp @@ -24,10 +24,10 @@ class ILinearProjection : virtual public core::IReferenceCounted using model_matrix_t = typename decltype(m_camera)::pointee::CGimbal::model_matrix_t; //! underlying type for linear concatenated matrix - using concatenated_matrix_t = float64_t4x4; + using concatenated_matrix_t = hlsl::float64_t4x4; //! underlying type for linear inverse of concatenated matrix - using inv_concatenated_matrix_t = std::optional; + using inv_concatenated_matrix_t = std::optional; struct CProjection : public IProjection { @@ -50,7 +50,7 @@ class ILinearProjection : virtual public core::IReferenceCounted virtual void project(const projection_vector_t& vecToProjectionSpace, projection_vector_t& output) const override { - output = mul(m_projectionMatrix, vecToProjectionSpace); + output = hlsl::mul(m_projectionMatrix, vecToProjectionSpace); } virtual bool unproject(const projection_vector_t& vecFromProjectionSpace, projection_vector_t& output) const override @@ -58,7 +58,7 @@ class ILinearProjection : virtual public core::IReferenceCounted if (m_isProjectionSingular) return false; - output = mul(m_invProjectionMatrix.value(), vecFromProjectionSpace); + output = hlsl::mul(m_invProjectionMatrix.value(), vecFromProjectionSpace); return true; } @@ -81,7 +81,7 @@ class ILinearProjection : virtual public core::IReferenceCounted else { m_isProjectionLeftHanded = det < 0.0; - m_invProjectionMatrix = inverse(m_projectionMatrix); + m_invProjectionMatrix = hlsl::inverse(m_projectionMatrix); } } @@ -119,7 +119,7 @@ class ILinearProjection : virtual public core::IReferenceCounted inline concatenated_matrix_t getMV(const model_matrix_t& model) const { const auto& v = m_camera->getGimbal().getViewMatrix(); - return mul(getMatrix3x4As4x4(v), getMatrix3x4As4x4(model)); + return hlsl::mul(hlsl::getMatrix3x4As4x4(v), hlsl::getMatrix3x4As4x4(model)); } /** @@ -132,8 +132,8 @@ class ILinearProjection : virtual public core::IReferenceCounted { const auto& v = m_camera->getGimbal().getViewMatrix(); const auto& p = projection.getProjectionMatrix(); - auto mv = mul(getMatrix3x4As4x4(v), getMatrix3x4As4x4(model)); - return mul(p, mv); + auto mv = hlsl::mul(hlsl::getMatrix3x4As4x4(v), hlsl::getMatrix3x4As4x4(model)); + return hlsl::mul(p, mv); } /** @@ -145,7 +145,7 @@ class ILinearProjection : virtual public core::IReferenceCounted inline concatenated_matrix_t getMVP(const CProjection& projection, const concatenated_matrix_t& mv) const { const auto& p = projection.getProjectionMatrix(); - return mul(p, mv); + return hlsl::mul(p, mv); } /** @@ -156,8 +156,8 @@ class ILinearProjection : virtual public core::IReferenceCounted inline inv_concatenated_matrix_t getMVInverse(const model_matrix_t& model) const { const auto mv = getMV(model); - if (auto det = determinant(mv); det) - return inverse(mv); + if (auto det = hlsl::determinant(mv); det) + return hlsl::inverse(mv); return std::nullopt; } @@ -170,8 +170,8 @@ class ILinearProjection : virtual public core::IReferenceCounted inline inv_concatenated_matrix_t getMVPInverse(const CProjection& projection, const model_matrix_t& model) const { const auto mvp = getMVP(projection, model); - if (auto det = determinant(mvp); det) - return inverse(mvp); + if (auto det = hlsl::determinant(mvp); det) + return hlsl::inverse(mvp); return std::nullopt; } }; diff --git a/common/include/camera/IPerspectiveProjection.hpp b/common/include/camera/IPerspectiveProjection.hpp index d934c245c..2c319d0c0 100644 --- a/common/include/camera/IPerspectiveProjection.hpp +++ b/common/include/camera/IPerspectiveProjection.hpp @@ -36,7 +36,7 @@ class IPerspectiveProjection : public ILinearProjection inline void setQuadTransform(const ILinearProjection::model_matrix_t& pretransform, ILinearProjection::concatenated_matrix_t viewport) { - auto concatenated = mul(getMatrix3x4As4x4(pretransform), viewport); + auto concatenated = hlsl::mul(hlsl::getMatrix3x4As4x4(pretransform), viewport); base_t::setProjectionMatrix(concatenated); m_pretransform = pretransform; diff --git a/common/include/camera/IPlanarProjection.hpp b/common/include/camera/IPlanarProjection.hpp index ba19fea6d..8f58ce2b0 100644 --- a/common/include/camera/IPlanarProjection.hpp +++ b/common/include/camera/IPlanarProjection.hpp @@ -77,9 +77,9 @@ class IPlanarProjection : public ILinearProjection const auto& fov = m_parameters.m_planar.perspective.fov; if (leftHanded) - base_t::setProjectionMatrix(hlsl::buildProjectionMatrixPerspectiveFovLH(hlsl::radians(fov), aspectRatio, m_parameters.m_zNear, m_parameters.m_zFar)); + base_t::setProjectionMatrix(hlsl::buildProjectionMatrixPerspectiveFovLH(hlsl::radians(fov), aspectRatio, m_parameters.m_zNear, m_parameters.m_zFar)); else - base_t::setProjectionMatrix(hlsl::buildProjectionMatrixPerspectiveFovRH(hlsl::radians(fov), aspectRatio, m_parameters.m_zNear, m_parameters.m_zFar)); + base_t::setProjectionMatrix(hlsl::buildProjectionMatrixPerspectiveFovRH(hlsl::radians(fov), aspectRatio, m_parameters.m_zNear, m_parameters.m_zFar)); } break; case Orthographic: @@ -88,9 +88,9 @@ class IPlanarProjection : public ILinearProjection const auto viewHeight = orthoW * core::reciprocal(aspectRatio); if (leftHanded) - base_t::setProjectionMatrix(hlsl::buildProjectionMatrixOrthoLH(orthoW, viewHeight, m_parameters.m_zNear, m_parameters.m_zFar)); + base_t::setProjectionMatrix(hlsl::buildProjectionMatrixOrthoLH(orthoW, viewHeight, m_parameters.m_zNear, m_parameters.m_zFar)); else - base_t::setProjectionMatrix(hlsl::buildProjectionMatrixOrthoRH(orthoW, viewHeight, m_parameters.m_zNear, m_parameters.m_zFar)); + base_t::setProjectionMatrix(hlsl::buildProjectionMatrixOrthoRH(orthoW, viewHeight, m_parameters.m_zNear, m_parameters.m_zFar)); } break; } } diff --git a/common/include/camera/IProjection.hpp b/common/include/camera/IProjection.hpp index cd3a9909a..3621784d0 100644 --- a/common/include/camera/IProjection.hpp +++ b/common/include/camera/IProjection.hpp @@ -11,7 +11,7 @@ class IProjection { public: //! underlying type for all vectors we project or un-project (inverse), projections *may* transform vectors in less dimensions - using projection_vector_t = float64_t4; + using projection_vector_t = hlsl::float64_t4; enum class ProjectionType { @@ -67,4 +67,4 @@ class IProjection } // namespace nbl::core -#endif // _NBL_IPROJECTION_HPP_ \ No newline at end of file +#endif // _NBL_IPROJECTION_HPP_ From d660266f4c824167f16318554e39789fe1cdbdaf Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Tue, 7 Apr 2026 13:46:56 +0200 Subject: [PATCH 177/205] Clarify camera namespace split in docs --- common/include/camera/README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/common/include/camera/README.md b/common/include/camera/README.md index d77357b12..3ccc82139 100644 --- a/common/include/camera/README.md +++ b/common/include/camera/README.md @@ -50,6 +50,9 @@ The stack is split across existing Nabla namespaces: - `nbl::system` persistence, scripted runtime payloads, scripted parsing, scripted check execution, and follow-contract validation helpers +The shared camera math is written against `nbl::hlsl`. +Consumers of this stack are expected to talk to camera math through `nbl::hlsl` types and helpers rather than through direct `glm::...` calls. + ## Why virtual events and not absolute setters The runtime contract is intentionally built around virtual events such as: @@ -165,6 +168,7 @@ CCameraSequenceScript - [`CVirtualGimbalEvent.hpp`](CVirtualGimbalEvent.hpp) - [`IGimbal.hpp`](IGimbal.hpp) +- [`CCameraMathUtilities.hpp`](CCameraMathUtilities.hpp) This is the mathematical foundation. @@ -172,6 +176,7 @@ It defines: - the shared semantic event language in `CVirtualGimbalEvent` - low-level gimbal math +- reusable camera-oriented math helpers in `nbl::hlsl` - accumulation of multiple semantic commands into one camera impulse This is the shared command language used by all camera types and all runtime input sources. @@ -193,6 +198,7 @@ It only stores how input should map to the semantic command language. - [`IGimbalInputProcessor.hpp`](IGimbalInputProcessor.hpp) - [`CGimbalInputBinder.hpp`](CGimbalInputBinder.hpp) +- [`CCameraInputBindingUtilities.hpp`](CCameraInputBindingUtilities.hpp) The main runtime type is `IGimbalInputProcessor`. @@ -201,6 +207,7 @@ This layer: - receives actual keyboard and mouse streams - receives ImGuizmo transforms - emits virtual events for the current frame +- stores reusable default keyboard, mouse, and ImGuizmo binding presets for camera kinds `CGimbalInputBinder` is the convenience runtime binder that a consumer should usually use. @@ -536,6 +543,13 @@ The design deliberately keeps these concerns separate: - follow policy - scripted playback and validation +It also keeps the math side explicit: + +- camera-space vectors, matrices, and quaternions come from `nbl::hlsl` +- runtime camera semantics stay in `nbl::core` +- input-device mappings stay in `nbl::ui` +- scripting, persistence, and validation helpers stay in `nbl::system` + That separation is what keeps the stack reusable. If any one of those concerns leaks into the others: From 896901dd68a5236906a7812a99bcce443838281f Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Tue, 7 Apr 2026 13:49:51 +0200 Subject: [PATCH 178/205] Document camera support headers --- common/include/camera/CCameraInputBindingUtilities.hpp | 1 + common/include/camera/CCameraKeyframeTrackPersistence.hpp | 4 ++++ common/include/camera/CCameraMathUtilities.hpp | 1 + common/include/camera/CCameraPresetPersistence.hpp | 8 ++++++++ .../include/camera/CCameraScriptedRuntimePersistence.hpp | 4 ++++ .../include/camera/CCameraSequenceScriptPersistence.hpp | 2 ++ 6 files changed, 20 insertions(+) diff --git a/common/include/camera/CCameraInputBindingUtilities.hpp b/common/include/camera/CCameraInputBindingUtilities.hpp index 42f6ea0a6..3b91d6c89 100644 --- a/common/include/camera/CCameraInputBindingUtilities.hpp +++ b/common/include/camera/CCameraInputBindingUtilities.hpp @@ -7,6 +7,7 @@ namespace nbl::ui { +//! Reusable keyboard, mouse, and ImGuizmo binding preset grouped for one camera kind. struct SCameraInputBindingPreset { IGimbalBindingLayout::keyboard_to_virtual_events_t keyboard; diff --git a/common/include/camera/CCameraKeyframeTrackPersistence.hpp b/common/include/camera/CCameraKeyframeTrackPersistence.hpp index 2deedc859..7b8c718f9 100644 --- a/common/include/camera/CCameraKeyframeTrackPersistence.hpp +++ b/common/include/camera/CCameraKeyframeTrackPersistence.hpp @@ -13,10 +13,14 @@ namespace nbl::system { +//! Serialize one camera keyframe track into an existing stream. bool writeKeyframeTrack(std::ostream& out, const core::CCameraKeyframeTrack& track, int indent = 2); +//! Deserialize one camera keyframe track from an existing stream. bool readKeyframeTrack(std::istream& in, core::CCameraKeyframeTrack& track); +//! Save one camera keyframe track to a file. bool saveKeyframeTrackToFile(const path& path, const core::CCameraKeyframeTrack& track, int indent = 2); +//! Load one camera keyframe track from a file. bool loadKeyframeTrackFromFile(const path& path, core::CCameraKeyframeTrack& track); } // namespace nbl::system diff --git a/common/include/camera/CCameraMathUtilities.hpp b/common/include/camera/CCameraMathUtilities.hpp index aa3721385..a09a7fab9 100644 --- a/common/include/camera/CCameraMathUtilities.hpp +++ b/common/include/camera/CCameraMathUtilities.hpp @@ -11,6 +11,7 @@ namespace nbl::hlsl { +//! Camera-oriented math aliases and helpers built on top of Nabla `nbl::hlsl` types. template inline T wrapAngleRad(T angle) { diff --git a/common/include/camera/CCameraPresetPersistence.hpp b/common/include/camera/CCameraPresetPersistence.hpp index 6c587f7af..58ad19103 100644 --- a/common/include/camera/CCameraPresetPersistence.hpp +++ b/common/include/camera/CCameraPresetPersistence.hpp @@ -13,16 +13,24 @@ namespace nbl::system { +//! Serialize one camera goal into an existing stream. bool writeGoal(std::ostream& out, const core::CCameraGoal& goal, int indent = 2); +//! Deserialize one camera goal from an existing stream. bool readGoal(std::istream& in, core::CCameraGoal& goal); +//! Save one camera goal to a file. bool saveGoalToFile(const path& path, const core::CCameraGoal& goal, int indent = 2); +//! Load one camera goal from a file. bool loadGoalFromFile(const path& path, core::CCameraGoal& goal); +//! Serialize one camera preset into an existing stream. bool writePreset(std::ostream& out, const core::CCameraPreset& preset, int indent = 2); +//! Deserialize one camera preset from an existing stream. bool readPreset(std::istream& in, core::CCameraPreset& preset); +//! Save one camera preset to a file. bool savePresetToFile(const path& path, const core::CCameraPreset& preset, int indent = 2); +//! Load one camera preset from a file. bool loadPresetFromFile(const path& path, core::CCameraPreset& preset); } // namespace nbl::system diff --git a/common/include/camera/CCameraScriptedRuntimePersistence.hpp b/common/include/camera/CCameraScriptedRuntimePersistence.hpp index 7280762b0..094d35966 100644 --- a/common/include/camera/CCameraScriptedRuntimePersistence.hpp +++ b/common/include/camera/CCameraScriptedRuntimePersistence.hpp @@ -16,6 +16,7 @@ namespace nbl::system { +//! Optional scripted control overrides parsed alongside one runtime payload. struct CCameraScriptedControlOverrides { bool hasKeyboardScale = false; @@ -30,6 +31,7 @@ struct CCameraScriptedControlOverrides float rotationScale = 1.f; }; +//! Parsed low-level scripted runtime payload plus optional compact authored sequence. struct CCameraScriptedInputParseResult { bool enabled = true; @@ -54,7 +56,9 @@ inline void appendScriptedInputParseWarning(CCameraScriptedInputParseResult& out out.warnings.emplace_back(std::move(warning)); } +//! Parse one low-level scripted runtime payload from an existing stream. bool readCameraScriptedInput(std::istream& in, CCameraScriptedInputParseResult& out, std::string* error = nullptr); +//! Load one low-level scripted runtime payload from a file. bool loadCameraScriptedInputFromFile(const path& path, CCameraScriptedInputParseResult& out, std::string* error = nullptr); } // namespace nbl::system diff --git a/common/include/camera/CCameraSequenceScriptPersistence.hpp b/common/include/camera/CCameraSequenceScriptPersistence.hpp index 1d00433dc..7b76807cc 100644 --- a/common/include/camera/CCameraSequenceScriptPersistence.hpp +++ b/common/include/camera/CCameraSequenceScriptPersistence.hpp @@ -14,7 +14,9 @@ namespace nbl::system { +//! Parse one compact camera-sequence script from an existing stream. bool readCameraSequenceScript(std::istream& in, core::CCameraSequenceScript& out, std::string* error = nullptr); +//! Load one compact camera-sequence script from a file. bool loadCameraSequenceScriptFromFile(const path& path, core::CCameraSequenceScript& out, std::string* error = nullptr); } // namespace nbl::system From ea451cd82950e9ae0dc8693023f23949c56fc817 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Wed, 8 Apr 2026 07:18:52 +0200 Subject: [PATCH 179/205] Tighten camera API cleanup --- 61_UI/AppControlPanel.cpp | 484 ++++++-------- 61_UI/AppImGuiListen.cpp | 599 ++++++++--------- 61_UI/AppInit.cpp | 622 +++--------------- 61_UI/AppTransformEditor.cpp | 19 +- 61_UI/AppWorkLoop.cpp | 6 +- 61_UI/include/app/App.hpp | 619 +---------------- .../include/app/AppCameraConfigUtilities.hpp | 167 +++++ 61_UI/include/app/AppResourceUtilities.hpp | 194 ++++++ 61_UI/include/app/AppTypes.hpp | 48 ++ .../app/AppViewportBindingUtilities.hpp | 86 +++ 61_UI/include/common.hpp | 3 + common/include/camera/CArcballCamera.hpp | 16 +- .../camera/CCameraControlPanelUiUtilities.hpp | 442 +++++++++++++ .../CCameraFollowRegressionUtilities.hpp | 70 +- .../include/camera/CCameraFollowUtilities.hpp | 99 +-- common/include/camera/CCameraGoal.hpp | 111 +--- common/include/camera/CCameraGoalSolver.hpp | 152 ++--- .../camera/CCameraInputBindingUtilities.hpp | 336 ++++++---- .../include/camera/CCameraMathUtilities.hpp | 368 ++++++++++- ...ameraScriptVisualDebugOverlayUtilities.hpp | 212 ++++++ .../camera/CCameraScriptedCheckRunner.hpp | 56 +- .../include/camera/CCameraScriptedRuntime.hpp | 5 +- .../include/camera/CCameraSequenceScript.hpp | 46 +- .../CCameraSmokeRegressionUtilities.hpp | 128 ++++ .../CCameraViewportOverlayUtilities.hpp | 311 +++++++++ common/include/camera/CChaseCamera.hpp | 38 +- common/include/camera/CDollyCamera.hpp | 18 +- common/include/camera/CDollyZoomCamera.hpp | 23 +- common/include/camera/CFPSCamera.hpp | 32 +- common/include/camera/CIsometricCamera.hpp | 14 +- common/include/camera/COrbitCamera.hpp | 12 +- common/include/camera/CPathCamera.hpp | 122 ++-- .../include/camera/CSphericalTargetCamera.hpp | 103 +-- common/include/camera/CTopDownCamera.hpp | 14 +- common/include/camera/CTurntableCamera.hpp | 12 +- common/include/camera/ICamera.hpp | 31 +- common/include/camera/IGimbal.hpp | 10 +- .../include/camera/IGimbalInputProcessor.hpp | 3 +- 38 files changed, 3232 insertions(+), 2399 deletions(-) create mode 100644 61_UI/include/app/AppCameraConfigUtilities.hpp create mode 100644 61_UI/include/app/AppResourceUtilities.hpp create mode 100644 61_UI/include/app/AppViewportBindingUtilities.hpp create mode 100644 common/include/camera/CCameraControlPanelUiUtilities.hpp create mode 100644 common/include/camera/CCameraScriptVisualDebugOverlayUtilities.hpp create mode 100644 common/include/camera/CCameraSmokeRegressionUtilities.hpp create mode 100644 common/include/camera/CCameraViewportOverlayUtilities.hpp diff --git a/61_UI/AppControlPanel.cpp b/61_UI/AppControlPanel.cpp index a7dfaa51d..78d756b56 100644 --- a/61_UI/AppControlPanel.cpp +++ b/61_UI/AppControlPanel.cpp @@ -29,43 +29,14 @@ bool App::loadKeyframesFromFile(const nbl::system::path& path) void App::DrawControlPanel() { + const nbl::ui::SCameraControlPanelStyle panelStyle = {}; const ImVec2 displaySize = ImGui::GetIO().DisplaySize; - const float panelWidth = std::clamp(displaySize.x * 0.19f, 200.0f, displaySize.x * 0.25f); - const float panelHeight = std::clamp(displaySize.y * 0.34f, 200.0f, displaySize.y * 0.50f); - const ImVec2 panelSize = { panelWidth, panelHeight }; + const ImVec2 panelSize = nbl::ui::calcControlPanelWindowSize(displaySize, panelStyle); const ImVec2 panelPos = { 0.0f, 0.0f }; ImGui::SetNextWindowPos(panelPos, ImGuiCond_Always); ImGui::SetNextWindowSize(panelSize, ImGuiCond_Always); - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(5.0f, 4.0f)); - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(4.0f, 1.0f)); - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(3.0f, 2.0f)); - ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f); - ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f); - ImGui::PushStyleVar(ImGuiStyleVar_TabRounding, 3.0f); - ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarRounding, 4.0f); - ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f); - ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2(3.0f, 2.0f)); - - ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.05f, 0.06f, 0.08f, 0.0f)); - ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.10f, 0.12f, 0.16f, 0.44f)); - ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.64f, 0.72f, 0.84f, 0.55f)); - ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.16f, 0.19f, 0.24f, 0.54f)); - ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, ImVec4(0.26f, 0.32f, 0.40f, 0.64f)); - ImGui::PushStyleColor(ImGuiCol_FrameBgActive, ImVec4(0.30f, 0.36f, 0.45f, 0.70f)); - ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.14f, 0.18f, 0.24f, 0.60f)); - ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(0.24f, 0.30f, 0.40f, 0.70f)); - ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(0.28f, 0.36f, 0.46f, 0.78f)); - ImGui::PushStyleColor(ImGuiCol_Tab, ImVec4(0.14f, 0.18f, 0.24f, 0.60f)); - ImGui::PushStyleColor(ImGuiCol_TabHovered, ImVec4(0.24f, 0.30f, 0.40f, 0.70f)); - ImGui::PushStyleColor(ImGuiCol_TabActive, ImVec4(0.20f, 0.26f, 0.36f, 0.78f)); - ImGui::PushStyleColor(ImGuiCol_TableRowBg, ImVec4(0.12f, 0.14f, 0.18f, 0.50f)); - ImGui::PushStyleColor(ImGuiCol_TableRowBgAlt, ImVec4(0.16f, 0.18f, 0.22f, 0.50f)); - ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.98f, 0.99f, 1.0f, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_TextDisabled, ImVec4(0.82f, 0.86f, 0.90f, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_Separator, ImVec4(0.54f, 0.60f, 0.70f, 0.80f)); - ImGui::PushStyleColor(ImGuiCol_SeparatorHovered, ImVec4(0.68f, 0.76f, 0.88f, 0.90f)); - ImGui::PushStyleColor(ImGuiCol_SeparatorActive, ImVec4(0.82f, 0.90f, 1.0f, 0.96f)); + nbl::ui::pushControlPanelWindowStyle(panelStyle); ImGui::SetNextWindowCollapsed(false, ImGuiCond_Always); ImGui::SetNextWindowBgAlpha(0.0f); @@ -73,28 +44,25 @@ void App::DrawControlPanel() ImGui::SetNextWindowFocus(); ImGui::Begin("Control Panel", nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar); - const ImVec4 accent = ImVec4(0.60f, 0.82f, 1.0f, 1.0f); - const ImVec4 good = ImVec4(0.45f, 0.90f, 0.60f, 1.0f); - const ImVec4 bad = ImVec4(1.0f, 0.50f, 0.45f, 1.0f); - const ImVec4 warn = ImVec4(0.95f, 0.80f, 0.45f, 1.0f); - const ImVec4 muted = ImVec4(0.92f, 0.93f, 0.95f, 1.0f); - const ImVec4 badgeText = ImVec4(0.10f, 0.11f, 0.13f, 1.0f); - const ImVec4 keyBg = ImVec4(0.20f, 0.22f, 0.25f, 1.0f); - const ImVec4 keyFg = ImVec4(0.92f, 0.94f, 0.96f, 1.0f); - const ImGuiTableFlags tableFlags = ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_RowBg | ImGuiTableFlags_PadOuterX; - const ImVec4 panelBg = ImVec4(0.03f, 0.04f, 0.05f, 0.50f); - const ImVec4 panelEdge = ImVec4(0.62f, 0.70f, 0.84f, 0.60f); - const ImVec4 panelStripe = ImVec4(0.28f, 0.56f, 0.90f, 0.70f); - const ImVec4 panelShadow = ImVec4(0.0f, 0.0f, 0.0f, 0.12f); + const ImVec4 accent = panelStyle.AccentColor; + const ImVec4 good = panelStyle.GoodColor; + const ImVec4 bad = panelStyle.BadColor; + const ImVec4 warn = panelStyle.WarnColor; + const ImVec4 muted = panelStyle.MutedColor; + const ImVec4 badgeText = panelStyle.BadgeTextColor; + const ImVec4 keyBg = panelStyle.KeyBackgroundColor; + const ImVec4 keyFg = panelStyle.KeyTextColor; + const ImGuiTableFlags tableFlags = panelStyle.SummaryTableFlags; + const ImVec4 inactiveBadge = panelStyle.InactiveBadgeColor; + const ImVec4 cardTop = panelStyle.CardTopColor; + const ImVec4 cardBottom = panelStyle.CardBottomColor; + const ImVec4 cardBorder = panelStyle.CardBorderColor; { const ImVec2 panelPos = ImGui::GetWindowPos(); const ImVec2 panelSize = ImGui::GetWindowSize(); - auto* drawList = ImGui::GetWindowDrawList(); - drawList->AddRectFilled(ImVec2(panelPos.x + 2.0f, panelPos.y + 3.0f), ImVec2(panelPos.x + panelSize.x + 4.0f, panelPos.y + panelSize.y + 5.0f), ImGui::ColorConvertFloat4ToU32(panelShadow), 8.0f); - drawList->AddRectFilled(panelPos, ImVec2(panelPos.x + panelSize.x, panelPos.y + panelSize.y), ImGui::ColorConvertFloat4ToU32(panelBg), 6.0f); - drawList->AddRect(panelPos, ImVec2(panelPos.x + panelSize.x, panelPos.y + panelSize.y), ImGui::ColorConvertFloat4ToU32(panelEdge), 6.0f); - drawList->AddRectFilled(panelPos, ImVec2(panelPos.x + 4.0f, panelPos.y + panelSize.y), ImGui::ColorConvertFloat4ToU32(panelStripe), 6.0f); + if (auto* drawList = ImGui::GetWindowDrawList(); drawList) + nbl::ui::drawControlPanelWindowBackdrop(*drawList, panelPos, panelSize, panelStyle); } auto row = [&](const char* label, auto&& drawValue) @@ -106,143 +74,69 @@ void App::DrawControlPanel() drawValue(); }; - auto metricMax = [&](const std::array& values, float minValue) -> float + ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, panelStyle.CardChildRounding); + if (ImGui::BeginChild("PanelHeader", ImVec2(0.0f, panelStyle.HeaderWindowHeight), true, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse)) { - float maxValue = minValue; - for (const float v : values) - maxValue = std::max(maxValue, v); - return maxValue; - }; - - auto miniStat = [&](const char* id, const char* label, const ImVec4& color, const std::array& series, float minValue, auto&& drawValue) - { - ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.14f, 0.16f, 0.19f, 0.75f)); - ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 6.0f); - if (ImGui::BeginChild(id, ImVec2(0, 56), true, ImGuiWindowFlags_NoScrollbar)) - { - ImGui::TextDisabled("%s", label); - ImGui::SetWindowFontScale(1.05f); - drawValue(); - ImGui::SetWindowFontScale(1.0f); - ImGui::PushStyleColor(ImGuiCol_PlotLines, color); - const float maxValue = metricMax(series, minValue); - ImGui::PlotLines("##plot", series.data(), static_cast(UiMetricSamples), static_cast(m_uiMetricIndex), nullptr, 0.0f, maxValue, ImVec2(0, 24)); - ImGui::PopStyleColor(); - } - ImGui::EndChild(); - ImGui::PopStyleVar(); - ImGui::PopStyleColor(); - }; - - auto calcPillWidth = [&](const char* label, const ImVec2& pad) - { - return ImGui::CalcTextSize(label).x + pad.x * 2.0f; - }; - - auto drawTogglePill = [&](const char* label, bool& value, const ImVec4& onCol, const ImVec4& offCol, const ImVec2& pad) - { - ImGui::PushStyleColor(ImGuiCol_Button, value ? onCol : offCol); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, value ? onCol : offCol); - ImGui::PushStyleColor(ImGuiCol_ButtonActive, value ? onCol : offCol); - ImGui::PushStyleColor(ImGuiCol_Text, badgeText); - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, pad); - if (ImGui::Button(label)) - value = !value; - ImGui::PopStyleVar(); - ImGui::PopStyleColor(4); - }; - - ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 6.0f); - if (ImGui::BeginChild("PanelHeader", ImVec2(0, 64), true, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse)) - { - ImGui::Dummy(ImVec2(0.0f, 1.0f)); - ImGui::SetWindowFontScale(1.08f); + ImGui::Dummy(ImVec2(0.0f, panelStyle.HeaderDummyY)); + ImGui::SetWindowFontScale(panelStyle.HeaderTitleFontScale); ImGui::TextColored(accent, "Control Panel"); ImGui::SetWindowFontScale(1.0f); { - const ImVec2 badgePad = ImVec2(6.0f, 2.0f); const float gap = ImGui::GetStyle().ItemSpacing.x; - const char* badgeWindow = useWindow ? "WINDOW" : "FULL"; - const char* badgeMove = enableActiveCameraMovement ? "MOVE ON" : "MOVE OFF"; - const char* badgeScript = m_scriptedInput.enabled ? (m_scriptedInput.exclusive ? "SCRIPT EXCL" : "SCRIPT") : "SCRIPT OFF"; - const float badgeRowWidth = calcPillWidth(badgeWindow, badgePad) - + gap + calcPillWidth(badgeMove, badgePad) - + gap + calcPillWidth(badgeScript, badgePad) - + (m_ciMode ? (gap + calcPillWidth("CI", badgePad)) : 0.0f); - ImGui::SetCursorPosX(ImGui::GetCursorPosX() + std::max(0.0f, (ImGui::GetContentRegionAvail().x - badgeRowWidth) * 0.5f)); - - DrawBadge(badgeWindow, accent, badgeText); - ImGui::SameLine(0.0f, gap); - DrawBadge(badgeMove, enableActiveCameraMovement ? good : bad, badgeText); - ImGui::SameLine(0.0f, gap); - DrawBadge(badgeScript, m_scriptedInput.enabled ? accent : ImVec4(0.35f, 0.36f, 0.38f, 1.0f), badgeText); - if (m_ciMode) - { - ImGui::SameLine(0.0f, gap); - DrawBadge("CI", warn, badgeText); - } + std::array headerBadges = {{ + { useWindow ? "WINDOW" : "FULL", accent }, + { enableActiveCameraMovement ? "MOVE ON" : "MOVE OFF", enableActiveCameraMovement ? good : bad }, + { m_scriptedInput.enabled ? (m_scriptedInput.exclusive ? "SCRIPT EXCL" : "SCRIPT") : "SCRIPT OFF", m_scriptedInput.enabled ? accent : inactiveBadge }, + { "CI", warn } + }}; + const size_t headerBadgeCount = m_ciMode ? headerBadges.size() : headerBadges.size() - 1u; + nbl::ui::drawBadgeRow(std::span(headerBadges.data(), headerBadgeCount), badgeText, gap, panelStyle); } - ImGui::Dummy(ImVec2(0.0f, 2.0f)); + ImGui::Dummy(ImVec2(0.0f, panelStyle.HeaderGapSmall)); { - const ImVec2 keyPad = ImVec2(4.0f, 1.0f); const float gap = ImGui::GetStyle().ItemSpacing.x; const float groupGap = gap * 2.0f; - const float moveWidth = ImGui::CalcTextSize("Move").x + gap - + calcPillWidth("W", keyPad) + gap - + calcPillWidth("A", keyPad) + gap - + calcPillWidth("S", keyPad) + gap - + calcPillWidth("D", keyPad); - const float lookWidth = ImGui::CalcTextSize("Look").x + gap + calcPillWidth("RMB", keyPad); - const float zoomWidth = ImGui::CalcTextSize("Zoom").x + gap + calcPillWidth("MW", keyPad); - const float rowWidth = moveWidth + groupGap + lookWidth + groupGap + zoomWidth; - ImGui::SetCursorPosX(ImGui::GetCursorPosX() + std::max(0.0f, (ImGui::GetContentRegionAvail().x - rowWidth) * 0.5f)); - - ImGui::TextDisabled("Move"); - ImGui::SameLine(); - DrawKeyHint("W", keyBg, keyFg); - ImGui::SameLine(); - DrawKeyHint("A", keyBg, keyFg); - ImGui::SameLine(); - DrawKeyHint("S", keyBg, keyFg); - ImGui::SameLine(); - DrawKeyHint("D", keyBg, keyFg); - - ImGui::SameLine(0.0f, groupGap); - ImGui::TextDisabled("Look"); - ImGui::SameLine(); - DrawKeyHint("RMB", keyBg, keyFg); - - ImGui::SameLine(0.0f, groupGap); - ImGui::TextDisabled("Zoom"); - ImGui::SameLine(); - DrawKeyHint("MW", keyBg, keyFg); + static constexpr std::array MoveKeys = { "W", "A", "S", "D" }; + static constexpr std::array LookKeys = { "RMB" }; + static constexpr std::array ZoomKeys = { "MW" }; + const std::array keyHintGroups = {{ + { "Move", MoveKeys }, + { "Look", LookKeys }, + { "Zoom", ZoomKeys } + }}; + nbl::ui::drawKeyHintGroupRow(keyHintGroups, gap, groupGap, keyBg, keyFg, panelStyle); } - ImGui::Dummy(ImVec2(0.0f, 2.0f)); + ImGui::Dummy(ImVec2(0.0f, panelStyle.HeaderGapSmall)); if (ImGui::BeginTable("HeaderMetrics", 3, ImGuiTableFlags_SizingStretchProp)) { const float frameMs = std::max(0.0f, m_uiLastFrameMs); const float fps = frameMs > 0.0f ? (1000.0f / frameMs) : 0.0f; + const std::array miniStats = {{ + { "FrameStat", "Frame", accent, panelStyle.DefaultFrameMetricMin }, + { "InputStat", "Input", accent, panelStyle.DefaultEventMetricMin }, + { "VirtualStat", "Virtual", accent, panelStyle.DefaultEventMetricMin } + }}; ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); - miniStat("FrameStat", "Frame", accent, m_uiFrameMs, 16.0f, [&] + nbl::ui::drawMiniStat(miniStats[0], m_uiFrameMs, m_uiMetricIndex, [&] { ImGui::TextColored(accent, "%.1f ms %.0f fps", frameMs, fps); - }); + }, panelStyle); ImGui::TableSetColumnIndex(1); - miniStat("InputStat", "Input", accent, m_uiInputCounts, 4.0f, [&] + nbl::ui::drawMiniStat(miniStats[1], m_uiInputCounts, m_uiMetricIndex, [&] { ImGui::TextColored(accent, "%u ev", m_uiLastInputEvents); - }); + }, panelStyle); ImGui::TableSetColumnIndex(2); - miniStat("VirtualStat", "Virtual", accent, m_uiVirtualCounts, 4.0f, [&] + nbl::ui::drawMiniStat(miniStats[2], m_uiVirtualCounts, m_uiMetricIndex, [&] { ImGui::TextColored(accent, "%u ev", m_uiLastVirtualEvents); - }); + }, panelStyle); ImGui::EndTable(); } } @@ -254,18 +148,23 @@ void App::DrawControlPanel() { const ImVec2 togglePad = ImVec2(6.0f, 2.0f); const float gap = ImGui::GetStyle().ItemSpacing.x; - const float rowWidth = calcPillWidth("WINDOW", togglePad) - + gap + calcPillWidth("STATUS", togglePad) - + gap + calcPillWidth("EVENT LOG", togglePad); - ImGui::SetCursorPosX(ImGui::GetCursorPosX() + std::max(0.0f, (ImGui::GetContentRegionAvail().x - rowWidth) * 0.5f)); - drawTogglePill("WINDOW", useWindow, accent, ImVec4(0.35f, 0.36f, 0.38f, 1.0f), togglePad); - DrawHoverHint("Toggle split render windows"); + const std::array toggleLabels = { "WINDOW", "STATUS", "EVENT LOG" }; + float rowWidth = 0.0f; + for (size_t i = 0; i < toggleLabels.size(); ++i) + { + if (i > 0u) + rowWidth += gap; + rowWidth += nbl::ui::calcPillWidth(toggleLabels[i], togglePad); + } + nbl::ui::centerControlPanelRow(rowWidth); + nbl::ui::drawTogglePill("WINDOW", useWindow, accent, inactiveBadge, badgeText, togglePad); + nbl::ui::drawHoverHint("Toggle split render windows"); ImGui::SameLine(0.0f, gap); - drawTogglePill("STATUS", m_showHud, accent, ImVec4(0.35f, 0.36f, 0.38f, 1.0f), togglePad); - DrawHoverHint("Show system and camera status panel"); + nbl::ui::drawTogglePill("STATUS", m_showHud, accent, inactiveBadge, badgeText, togglePad); + nbl::ui::drawHoverHint("Show system and camera status panel"); ImGui::SameLine(0.0f, gap); - drawTogglePill("EVENT LOG", m_showEventLog, accent, ImVec4(0.35f, 0.36f, 0.38f, 1.0f), togglePad); - DrawHoverHint("Show virtual event log"); + nbl::ui::drawTogglePill("EVENT LOG", m_showEventLog, accent, inactiveBadge, badgeText, togglePad); + nbl::ui::drawHoverHint("Show virtual event log"); } ImGui::Separator(); @@ -278,26 +177,22 @@ void App::DrawControlPanel() if (ImGui::BeginChild("StatusPanel", ImVec2(0, 0), true)) { ImGui::PushItemWidth(-1.0f); - const ImVec4 cardTop = ImVec4(0.20f, 0.22f, 0.26f, 0.98f); - const ImVec4 cardBottom = ImVec4(0.12f, 0.13f, 0.15f, 0.98f); - const ImVec4 cardBorder = ImVec4(0.45f, 0.48f, 0.54f, 1.0f); - - DrawSectionHeader("SessionHeader", "Session", accent); - if (BeginCard("SessionCard", CalcCardHeight(3), cardTop, cardBottom, cardBorder)) + nbl::ui::drawSectionHeader("SessionHeader", "Session", accent, panelStyle); + if (nbl::ui::beginCard("SessionCard", nbl::ui::calcCameraControlPanelCardHeight(3, panelStyle), cardTop, cardBottom, cardBorder, panelStyle)) { if (ImGui::BeginTable("SessionTable", 2, tableFlags)) { - ImGui::TableSetupColumn("Label", ImGuiTableColumnFlags_WidthFixed, 120.0f); + ImGui::TableSetupColumn("Label", ImGuiTableColumnFlags_WidthFixed, panelStyle.SummaryLabelColumnWidth); ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch); - row("Mode", [&] { DrawDot(accent); ImGui::TextColored(accent, "%s", useWindow ? "Window" : "Fullscreen"); }); - row("Active window", [&] { DrawDot(accent); ImGui::TextColored(accent, "%u", activeRenderWindowIx); }); - row("Movement", [&] { const ImVec4 c = enableActiveCameraMovement ? good : bad; DrawDot(c); ImGui::TextColored(c, "%s", enableActiveCameraMovement ? "Enabled" : "Disabled"); }); + row("Mode", [&] { nbl::ui::drawDot(accent, panelStyle); ImGui::TextColored(accent, "%s", useWindow ? "Window" : "Fullscreen"); }); + row("Active window", [&] { nbl::ui::drawDot(accent, panelStyle); ImGui::TextColored(accent, "%u", activeRenderWindowIx); }); + row("Movement", [&] { const ImVec4 c = enableActiveCameraMovement ? good : bad; nbl::ui::drawDot(c, panelStyle); ImGui::TextColored(c, "%s", enableActiveCameraMovement ? "Enabled" : "Disabled"); }); ImGui::EndTable(); } } - EndCard(); + nbl::ui::endCard(); - DrawSectionHeader("CameraHeader", "Camera", accent); + nbl::ui::drawSectionHeader("CameraHeader", "Camera", accent, panelStyle); auto* activeCamera = getActiveCamera(); if (activeCamera) @@ -306,30 +201,30 @@ void App::DrawControlPanel() const auto pos = gimbal.getPosition(); const auto euler = getQuaternionEulerDegrees(gimbal.getOrientation()); - if (BeginCard("CameraCard", CalcCardHeight(5), cardTop, cardBottom, cardBorder)) + if (nbl::ui::beginCard("CameraCard", nbl::ui::calcCameraControlPanelCardHeight(5, panelStyle), cardTop, cardBottom, cardBorder, panelStyle)) { if (ImGui::BeginTable("CameraTable", 2, tableFlags)) { - ImGui::TableSetupColumn("Label", ImGuiTableColumnFlags_WidthFixed, 120.0f); + ImGui::TableSetupColumn("Label", ImGuiTableColumnFlags_WidthFixed, panelStyle.SummaryLabelColumnWidth); ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch); - row("Name", [&] { DrawDot(accent); ImGui::TextColored(muted, "%s", activeCamera->getIdentifier().data()); }); - row("Position", [&] { DrawDot(muted); ImGui::TextColored(muted, "%.2f %.2f %.2f", pos.x, pos.y, pos.z); }); - row("Euler", [&] { DrawDot(muted); ImGui::TextColored(muted, "%.1f %.1f %.1f", euler.x, euler.y, euler.z); }); - row("Move scale", [&] { DrawDot(muted); ImGui::TextColored(muted, "%.4f", activeCamera->getMoveSpeedScale()); }); - row("Rotate scale", [&] { DrawDot(muted); ImGui::TextColored(muted, "%.4f", activeCamera->getRotationSpeedScale()); }); + row("Name", [&] { nbl::ui::drawDot(accent, panelStyle); ImGui::TextColored(muted, "%s", activeCamera->getIdentifier().data()); }); + row("Position", [&] { nbl::ui::drawDot(muted, panelStyle); ImGui::TextColored(muted, "%.2f %.2f %.2f", pos.x, pos.y, pos.z); }); + row("Euler", [&] { nbl::ui::drawDot(muted, panelStyle); ImGui::TextColored(muted, "%.1f %.1f %.1f", euler.x, euler.y, euler.z); }); + row("Move scale", [&] { nbl::ui::drawDot(muted, panelStyle); ImGui::TextColored(muted, "%.4f", activeCamera->getMoveSpeedScale()); }); + row("Rotate scale", [&] { nbl::ui::drawDot(muted, panelStyle); ImGui::TextColored(muted, "%.4f", activeCamera->getRotationSpeedScale()); }); ImGui::EndTable(); } } - EndCard(); + nbl::ui::endCard(); } else { - if (BeginCard("CameraCard", CalcCardHeight(2), cardTop, cardBottom, cardBorder)) + if (nbl::ui::beginCard("CameraCard", nbl::ui::calcCameraControlPanelCardHeight(2, panelStyle), cardTop, cardBottom, cardBorder, panelStyle)) ImGui::TextDisabled("No active camera"); - EndCard(); + nbl::ui::endCard(); } - DrawSectionHeader("ProjectionHeader", "Projection", accent); + nbl::ui::drawSectionHeader("ProjectionHeader", "Projection", accent, panelStyle); auto& binding = windowBindings[activeRenderWindowIx]; auto& planar = m_planarProjections[binding.activePlanarIx]; @@ -337,29 +232,29 @@ void App::DrawControlPanel() { auto& projection = planar->getPlanarProjections()[binding.boundProjectionIx.value()]; const auto& params = projection.getParameters(); - if (BeginCard("ProjectionCard", CalcCardHeight(4), cardTop, cardBottom, cardBorder)) + if (nbl::ui::beginCard("ProjectionCard", nbl::ui::calcCameraControlPanelCardHeight(4, panelStyle), cardTop, cardBottom, cardBorder, panelStyle)) { if (ImGui::BeginTable("ProjectionTable", 2, tableFlags)) { - ImGui::TableSetupColumn("Label", ImGuiTableColumnFlags_WidthFixed, 120.0f); + ImGui::TableSetupColumn("Label", ImGuiTableColumnFlags_WidthFixed, panelStyle.SummaryLabelColumnWidth); ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch); - row("Type", [&] { DrawDot(accent); ImGui::TextColored(muted, "%s", params.m_type == IPlanarProjection::CProjection::Perspective ? "Perspective" : "Orthographic"); }); - row("zNear", [&] { DrawDot(muted); ImGui::TextColored(muted, "%.2f", params.m_zNear); }); - row("zFar", [&] { DrawDot(muted); ImGui::TextColored(muted, "%.2f", params.m_zFar); }); + row("Type", [&] { nbl::ui::drawDot(accent, panelStyle); ImGui::TextColored(muted, "%s", params.m_type == IPlanarProjection::CProjection::Perspective ? "Perspective" : "Orthographic"); }); + row("zNear", [&] { nbl::ui::drawDot(muted, panelStyle); ImGui::TextColored(muted, "%.2f", params.m_zNear); }); + row("zFar", [&] { nbl::ui::drawDot(muted, panelStyle); ImGui::TextColored(muted, "%.2f", params.m_zFar); }); if (params.m_type == IPlanarProjection::CProjection::Perspective) - row("Fov", [&] { DrawDot(muted); ImGui::TextColored(muted, "%.1f", params.m_planar.perspective.fov); }); + row("Fov", [&] { nbl::ui::drawDot(muted, panelStyle); ImGui::TextColored(muted, "%.1f", params.m_planar.perspective.fov); }); else - row("Ortho width", [&] { DrawDot(muted); ImGui::TextColored(muted, "%.1f", params.m_planar.orthographic.orthoWidth); }); + row("Ortho width", [&] { nbl::ui::drawDot(muted, panelStyle); ImGui::TextColored(muted, "%.1f", params.m_planar.orthographic.orthoWidth); }); ImGui::EndTable(); } } - EndCard(); + nbl::ui::endCard(); } else { - if (BeginCard("ProjectionCard", CalcCardHeight(2), cardTop, cardBottom, cardBorder)) + if (nbl::ui::beginCard("ProjectionCard", nbl::ui::calcCameraControlPanelCardHeight(2, panelStyle), cardTop, cardBottom, cardBorder, panelStyle)) ImGui::TextDisabled("No projection bound"); - EndCard(); + nbl::ui::endCard(); } ImGui::PopItemWidth(); } @@ -377,9 +272,9 @@ void App::DrawControlPanel() auto& active = windowBindings[activeRenderWindowIx]; const auto activeRenderWindowIxString = std::to_string(activeRenderWindowIx); - DrawSectionHeader("PlanarSelectHeader", "Planar Selection", accent); + nbl::ui::drawSectionHeader("PlanarSelectHeader", "Planar Selection", accent, panelStyle); ImGui::Text("Active Render Window: %s", activeRenderWindowIxString.c_str()); - DrawHoverHint("Window that receives input and camera switching"); + nbl::ui::drawHoverHint("Window that receives input and camera switching"); { const size_t planarsCount = m_planarProjections.size(); assert(planarsCount); @@ -398,7 +293,7 @@ void App::DrawControlPanel() active.activePlanarIx = static_cast(currentPlanarIx); active.pickDefaultProjections(m_planarProjections[active.activePlanarIx]->getPlanarProjections()); } - DrawHoverHint("Select which camera the window renders"); + nbl::ui::drawHoverHint("Select which camera the window renders"); } assert(active.boundProjectionIx.has_value()); @@ -409,7 +304,7 @@ void App::DrawControlPanel() auto& planarBound = m_planarProjections[active.activePlanarIx]; assert(planarBound); - DrawSectionHeader("ProjectionParamsHeader", "Projection Parameters", accent); + nbl::ui::drawSectionHeader("ProjectionParamsHeader", "Projection Parameters", accent, panelStyle); auto selectedProjectionType = planarBound->getPlanarProjections()[active.boundProjectionIx.value()].getParameters().m_type; { @@ -427,7 +322,7 @@ void App::DrawControlPanel() default: active.boundProjectionIx = std::nullopt; assert(false); break; } } - DrawHoverHint("Switch projection type for this planar"); + nbl::ui::drawHoverHint("Switch projection type for this planar"); } auto getPresetName = [&](auto ix) -> std::string @@ -475,7 +370,7 @@ void App::DrawControlPanel() } if (updateBoundVirtualMaps) syncWindowInputBinding(active); - DrawHoverHint("Switch preset projection for this planar"); + nbl::ui::drawHoverHint("Switch preset projection for this planar"); auto* const boundCamera = planarBound->getCamera(); auto& boundProjection = planarBound->getPlanarProjections()[active.boundProjectionIx.value()]; @@ -485,11 +380,11 @@ void App::DrawControlPanel() if (useWindow) ImGui::Checkbox("Allow axes to flip##allowAxesToFlip", &active.allowGizmoAxesToFlip); - DrawHoverHint("Allow ImGuizmo axes to flip based on view"); + nbl::ui::drawHoverHint("Allow ImGuizmo axes to flip based on view"); if(useWindow) ImGui::Checkbox("Draw debug grid##drawDebugGrid", &active.enableDebugGridDraw); - DrawHoverHint("Toggle debug grid in the render window"); + nbl::ui::drawHoverHint("Toggle debug grid in the render window"); if (ImGui::RadioButton("LH", active.leftHandedProjection)) active.leftHandedProjection = true; @@ -498,40 +393,40 @@ void App::DrawControlPanel() if (ImGui::RadioButton("RH", not active.leftHandedProjection)) active.leftHandedProjection = false; - DrawHoverHint("Toggle left or right handed projection"); + nbl::ui::drawHoverHint("Toggle left or right handed projection"); updateParameters.m_zNear = std::clamp(updateParameters.m_zNear, 0.1f, 100.f); updateParameters.m_zFar = std::clamp(updateParameters.m_zFar, 110.f, 10000.f); ImGui::SliderFloat("zNear", &updateParameters.m_zNear, 0.1f, 100.f, "%.2f", ImGuiSliderFlags_Logarithmic); - DrawHoverHint("Near clip plane"); + nbl::ui::drawHoverHint("Near clip plane"); ImGui::SliderFloat("zFar", &updateParameters.m_zFar, 110.f, 10000.f, "%.1f", ImGuiSliderFlags_Logarithmic); - DrawHoverHint("Far clip plane"); + nbl::ui::drawHoverHint("Far clip plane"); switch (selectedProjectionType) { case IPlanarProjection::CProjection::Perspective: { ImGui::SliderFloat("Fov", &updateParameters.m_planar.perspective.fov, 20.f, 150.f, "%.1f", ImGuiSliderFlags_Logarithmic); - DrawHoverHint("Perspective field of view"); + nbl::ui::drawHoverHint("Perspective field of view"); boundProjection.setPerspective(updateParameters.m_zNear, updateParameters.m_zFar, updateParameters.m_planar.perspective.fov); } break; case IPlanarProjection::CProjection::Orthographic: { ImGui::SliderFloat("Ortho width", &updateParameters.m_planar.orthographic.orthoWidth, 1.f, 30.f, "%.1f", ImGuiSliderFlags_Logarithmic); - DrawHoverHint("Orthographic width"); + nbl::ui::drawHoverHint("Orthographic width"); boundProjection.setOrthographic(updateParameters.m_zNear, updateParameters.m_zFar, updateParameters.m_planar.orthographic.orthoWidth); } break; default: break; } - DrawSectionHeader("CursorHeader", "Cursor Behaviour", accent); + nbl::ui::drawSectionHeader("CursorHeader", "Cursor Behaviour", accent, panelStyle); if (ImGui::TreeNodeEx("Cursor Behaviour")) { ImGui::Checkbox("Capture OS cursor in move mode", &captureCursorInMoveMode); - DrawHoverHint("When disabled the app never warps or clamps system cursor"); + nbl::ui::drawHoverHint("When disabled the app never warps or clamps system cursor"); if (captureCursorInMoveMode) { if (ImGui::RadioButton("Clamp to the window", !resetCursorToCenter)) @@ -553,7 +448,7 @@ void App::DrawControlPanel() ImGui::Separator(); - DrawSectionHeader("BoundCameraHeader", "Bound Camera", accent); + nbl::ui::drawSectionHeader("BoundCameraHeader", "Bound Camera", accent, panelStyle); const auto flags = ImGuiTreeNodeFlags_DefaultOpen; if (ImGui::TreeNodeEx("Bound Camera", flags)) { @@ -568,11 +463,11 @@ void App::DrawControlPanel() float rotationSpeed = boundCamera->getRotationSpeedScale(); ImGui::SliderFloat("Move speed factor", &moveSpeed, 0.0001f, 10.f, "%.4f", ImGuiSliderFlags_Logarithmic); - DrawHoverHint("Scale translation speed for this camera"); + nbl::ui::drawHoverHint("Scale translation speed for this camera"); if (boundCamera->getAllowedVirtualEvents() & CVirtualGimbalEvent::Rotate) ImGui::SliderFloat("Rotate speed factor", &rotationSpeed, 0.0001f, 10.f, "%.4f", ImGuiSliderFlags_Logarithmic); - DrawHoverHint("Scale rotation speed for this camera"); + nbl::ui::drawHoverHint("Scale rotation speed for this camera"); boundCamera->setMotionScales(moveSpeed, rotationSpeed); @@ -580,7 +475,7 @@ void App::DrawControlPanel() { float distance = sphericalState.distance; ImGui::SliderFloat("Distance", &distance, sphericalState.minDistance, sphericalState.maxDistance, "%.4f", ImGuiSliderFlags_Logarithmic); - DrawHoverHint("Current orbit distance"); + nbl::ui::drawHoverHint("Current orbit distance"); boundCamera->trySetSphericalDistance(distance); } } @@ -621,52 +516,52 @@ void App::DrawControlPanel() if (ImGui::BeginChild("CameraPanel", ImVec2(0, 0), true)) { ImGui::PushItemWidth(-1.0f); - DrawSectionHeader("CameraInputHeader", "Input", accent); + nbl::ui::drawSectionHeader("CameraInputHeader", "Input", accent, panelStyle); ImGui::Checkbox("Mirror input to all cameras", &m_cameraControls.mirrorInput); - DrawHoverHint("Apply keyboard and mouse input to every camera"); + nbl::ui::drawHoverHint("Apply keyboard and mouse input to every camera"); ImGui::Checkbox("World translate", &m_cameraControls.worldTranslate); - DrawHoverHint("Translate in world space instead of camera space"); + nbl::ui::drawHoverHint("Translate in world space instead of camera space"); ImGui::SliderFloat("Keyboard scale", &m_cameraControls.keyboardScale, 0.01f, 10.f, "%.2f"); - DrawHoverHint("Scale keyboard movement magnitudes"); + nbl::ui::drawHoverHint("Scale keyboard movement magnitudes"); ImGui::SliderFloat("Mouse move scale", &m_cameraControls.mouseMoveScale, 0.01f, 10.f, "%.2f"); - DrawHoverHint("Scale mouse move magnitudes"); + nbl::ui::drawHoverHint("Scale mouse move magnitudes"); ImGui::SliderFloat("Mouse scroll scale", &m_cameraControls.mouseScrollScale, 0.01f, 10.f, "%.2f"); - DrawHoverHint("Scale mouse wheel magnitudes"); + nbl::ui::drawHoverHint("Scale mouse wheel magnitudes"); ImGui::SliderFloat("Translate scale", &m_cameraControls.translationScale, 0.01f, 10.f, "%.2f"); - DrawHoverHint("Overall translation scale for virtual events"); + nbl::ui::drawHoverHint("Overall translation scale for virtual events"); ImGui::SliderFloat("Rotate scale", &m_cameraControls.rotationScale, 0.01f, 10.f, "%.2f"); - DrawHoverHint("Overall rotation scale for virtual events"); + nbl::ui::drawHoverHint("Overall rotation scale for virtual events"); - DrawSectionHeader("CameraConstraintsHeader", "Constraints", accent); + nbl::ui::drawSectionHeader("CameraConstraintsHeader", "Constraints", accent, panelStyle); ImGui::Checkbox("Enable constraints", &m_cameraConstraints.enabled); - DrawHoverHint("Enable or disable all camera constraints"); + nbl::ui::drawHoverHint("Enable or disable all camera constraints"); ImGui::Checkbox("Clamp distance", &m_cameraConstraints.clampDistance); - DrawHoverHint("Clamp orbit distance to min/max"); + nbl::ui::drawHoverHint("Clamp orbit distance to min/max"); ImGui::SliderFloat("Min distance", &m_cameraConstraints.minDistance, 0.01f, 1000.f, "%.3f", ImGuiSliderFlags_Logarithmic); - DrawHoverHint("Minimum orbit distance"); + nbl::ui::drawHoverHint("Minimum orbit distance"); ImGui::SliderFloat("Max distance", &m_cameraConstraints.maxDistance, 0.01f, 10000.f, "%.3f", ImGuiSliderFlags_Logarithmic); - DrawHoverHint("Maximum orbit distance"); + nbl::ui::drawHoverHint("Maximum orbit distance"); ImGui::Separator(); ImGui::Checkbox("Clamp pitch", &m_cameraConstraints.clampPitch); - DrawHoverHint("Clamp pitch angle"); + nbl::ui::drawHoverHint("Clamp pitch angle"); ImGui::SliderFloat("Pitch min", &m_cameraConstraints.pitchMinDeg, -180.f, 180.f, "%.1f"); - DrawHoverHint("Minimum pitch in degrees"); + nbl::ui::drawHoverHint("Minimum pitch in degrees"); ImGui::SliderFloat("Pitch max", &m_cameraConstraints.pitchMaxDeg, -180.f, 180.f, "%.1f"); - DrawHoverHint("Maximum pitch in degrees"); + nbl::ui::drawHoverHint("Maximum pitch in degrees"); ImGui::Checkbox("Clamp yaw", &m_cameraConstraints.clampYaw); - DrawHoverHint("Clamp yaw angle"); + nbl::ui::drawHoverHint("Clamp yaw angle"); ImGui::SliderFloat("Yaw min", &m_cameraConstraints.yawMinDeg, -180.f, 180.f, "%.1f"); - DrawHoverHint("Minimum yaw in degrees"); + nbl::ui::drawHoverHint("Minimum yaw in degrees"); ImGui::SliderFloat("Yaw max", &m_cameraConstraints.yawMaxDeg, -180.f, 180.f, "%.1f"); - DrawHoverHint("Maximum yaw in degrees"); + nbl::ui::drawHoverHint("Maximum yaw in degrees"); ImGui::Checkbox("Clamp roll", &m_cameraConstraints.clampRoll); - DrawHoverHint("Clamp roll angle"); + nbl::ui::drawHoverHint("Clamp roll angle"); ImGui::SliderFloat("Roll min", &m_cameraConstraints.rollMinDeg, -180.f, 180.f, "%.1f"); - DrawHoverHint("Minimum roll in degrees"); + nbl::ui::drawHoverHint("Minimum roll in degrees"); ImGui::SliderFloat("Roll max", &m_cameraConstraints.rollMaxDeg, -180.f, 180.f, "%.1f"); - DrawHoverHint("Maximum roll in degrees"); + nbl::ui::drawHoverHint("Maximum roll in degrees"); - DrawSectionHeader("OrbitHeader", "Orbit Target", accent); + nbl::ui::drawSectionHeader("OrbitHeader", "Orbit Target", accent, panelStyle); auto* activeCamera = getActiveCamera(); ICamera::SphericalTargetState orbitState; @@ -682,18 +577,18 @@ void App::DrawControlPanel() const auto targetPos = hlsl::transpose(getMatrix3x4As4x4(m_model))[3]; activeCamera->trySetSphericalTarget(float64_t3(targetPos.x, targetPos.y, targetPos.z)); } - DrawHoverHint("Set orbit target to the model position"); + nbl::ui::drawHoverHint("Set orbit target to the model position"); ImGui::SameLine(); if (ImGui::Button("Target origin")) activeCamera->trySetSphericalTarget(float64_t3(0.0)); - DrawHoverHint("Set orbit target to world origin"); + nbl::ui::drawHoverHint("Set orbit target to world origin"); } if (!hasOrbitTarget) { ImGui::TextDisabled("Active camera is not orbit."); } - DrawSectionHeader("FollowHeader", "Follow Target", accent); + nbl::ui::drawSectionHeader("FollowHeader", "Follow Target", accent, panelStyle); auto* activeFollowConfig = getActiveFollowConfig(); if (activeFollowConfig) { @@ -701,7 +596,7 @@ void App::DrawControlPanel() const bool prevFollowEnabled = followConfig.enabled; const auto prevFollowMode = followConfig.mode; ImGui::Checkbox("Enable follow", &followConfig.enabled); - DrawHoverHint("Apply tracked-target follow to the active planar camera"); + nbl::ui::drawHoverHint("Apply tracked-target follow to the active planar camera"); const char* followModeLabels[] = { getCameraFollowModeLabel(ECameraFollowMode::Disabled), @@ -724,23 +619,23 @@ void App::DrawControlPanel() m_followTarget.setPosition(getCastedVector(trackedTarget)); ImGui::Checkbox("Show target marker", &m_followTargetVisible); - DrawHoverHint("Render the tracked target marker in the scene"); + nbl::ui::drawHoverHint("Render the tracked target marker in the scene"); if (ImGui::Button("Reset target")) resetFollowTargetToDefault(); - DrawHoverHint("Reset tracked target gimbal to the default world-space follow pose"); + nbl::ui::drawHoverHint("Reset tracked target gimbal to the default world-space follow pose"); ImGui::SameLine(); if (ImGui::Button("Snap to model")) snapFollowTargetToModel(); - DrawHoverHint("Optionally snap tracked target gimbal to the model transform"); + nbl::ui::drawHoverHint("Optionally snap tracked target gimbal to the model transform"); ImGui::SameLine(); if (ImGui::Button("Target origin")) m_followTarget.setPose(float64_t3(0.0), makeIdentityQuaternion()); - DrawHoverHint("Reset tracked target to identity at world origin"); + nbl::ui::drawHoverHint("Reset tracked target to identity at world origin"); ImGui::SameLine(); if (ImGui::Button("Capture current offset")) captureFollowOffsetsForPlanar(getActivePlanarIx()); - DrawHoverHint("Store current camera-to-target relation into the active follow config"); + nbl::ui::drawHoverHint("Store current camera-to-target relation into the active follow config"); if (cameraFollowModeUsesWorldOffset(followConfig.mode)) { @@ -772,7 +667,7 @@ void App::DrawControlPanel() if (ImGui::BeginChild("PresetsPanel", ImVec2(0, 0), true)) { ImGui::PushItemWidth(-1.0f); - DrawSectionHeader("PresetsHeader", "Presets", accent); + nbl::ui::drawSectionHeader("PresetsHeader", "Presets", accent, panelStyle); ImGui::InputText("Preset name", m_presetName, IM_ARRAYSIZE(m_presetName)); auto* activeCamera = getActiveCamera(); const auto presetCaptureUi = analyzeCameraCaptureForUi(activeCamera); @@ -789,7 +684,7 @@ void App::DrawControlPanel() } if (!presetCaptureUi.canCapture) ImGui::EndDisabled(); - DrawHoverHint(presetCaptureUi.canCapture ? + nbl::ui::drawHoverHint(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"); ImGui::SameLine(); @@ -798,7 +693,7 @@ void App::DrawControlPanel() m_presets.clear(); m_selectedPresetIx = -1; } - DrawHoverHint("Remove all presets"); + nbl::ui::drawHoverHint("Remove all presets"); ImGui::TextDisabled("Capture"); ImGui::SameLine(); ImGui::TextColored(presetCaptureUi.canCapture ? good : bad, "%s", presetCaptureUi.policyLabel.c_str()); @@ -813,7 +708,7 @@ void App::DrawControlPanel() int presetFilterIx = static_cast(m_presetFilterMode); if (ImGui::Combo("Visibility", &presetFilterIx, presetFilterLabels, IM_ARRAYSIZE(presetFilterLabels))) m_presetFilterMode = static_cast(presetFilterIx); - DrawHoverHint("Filter presets for the active camera using exact or best-effort compatibility"); + nbl::ui::drawHoverHint("Filter presets for the active camera using exact or best-effort compatibility"); std::vector filteredPresetIndices; filteredPresetIndices.reserve(m_presets.size()); @@ -873,23 +768,23 @@ void App::DrawControlPanel() ImGui::TextColored(compatibilityColor, "%s", presetUi.compatibilityLabel.c_str()); if (presetUi.badges.exact) - DrawBadge("EXACT", good, badgeText); + nbl::ui::drawBadge("EXACT", good, badgeText, panelStyle); else if (presetUi.badges.bestEffort) - DrawBadge("BEST-EFFORT", warn, badgeText); + nbl::ui::drawBadge("BEST-EFFORT", warn, badgeText, panelStyle); if (presetUi.badges.dropsState) { ImGui::SameLine(); - DrawBadge("DROPS STATE", warn, badgeText); + nbl::ui::drawBadge("DROPS STATE", warn, badgeText, panelStyle); } else if (presetUi.badges.sharedStateOnly) { ImGui::SameLine(); - DrawBadge("SHARED STATE", accent, badgeText); + nbl::ui::drawBadge("SHARED STATE", accent, badgeText, panelStyle); } if (presetUi.badges.blocked) { ImGui::SameLine(); - DrawBadge("BLOCKED", bad, badgeText); + nbl::ui::drawBadge("BLOCKED", bad, badgeText, panelStyle); } if (!presetUi.canApply) @@ -898,7 +793,7 @@ void App::DrawControlPanel() applyPresetFromUi(activeCamera, preset); if (!presetUi.canApply) ImGui::EndDisabled(); - DrawHoverHint(presetUi.canApply ? + nbl::ui::drawHoverHint(presetUi.canApply ? "Apply selected preset to the active camera" : "Apply is blocked because there is no active camera or the preset goal is invalid"); ImGui::SameLine(); @@ -907,7 +802,7 @@ void App::DrawControlPanel() m_presets.erase(m_presets.begin() + m_selectedPresetIx); m_selectedPresetIx = -1; } - DrawHoverHint("Remove selected preset"); + nbl::ui::drawHoverHint("Remove selected preset"); } } } @@ -918,21 +813,21 @@ void App::DrawControlPanel() ImGui::TextColored(resultColor, "%s", m_manualPresetApplyBanner.summary.c_str()); } - DrawSectionHeader("PresetsStorageHeader", "Storage", accent); + nbl::ui::drawSectionHeader("PresetsStorageHeader", "Storage", accent, panelStyle); ImGui::InputText("Preset file", m_presetPath, IM_ARRAYSIZE(m_presetPath)); if (ImGui::Button("Save presets")) { if (!savePresetsToFile(nbl::system::path(m_presetPath))) m_logger->log("Failed to save presets to \"%s\".", ILogger::ELL_ERROR, m_presetPath); } - DrawHoverHint("Save presets to JSON file"); + nbl::ui::drawHoverHint("Save presets to JSON file"); ImGui::SameLine(); if (ImGui::Button("Load presets")) { if (!loadPresetsFromFile(nbl::system::path(m_presetPath))) m_logger->log("Failed to load presets from \"%s\".", ILogger::ELL_ERROR, m_presetPath); } - DrawHoverHint("Load presets from JSON file"); + nbl::ui::drawHoverHint("Load presets from JSON file"); ImGui::PopItemWidth(); } ImGui::EndChild(); @@ -947,26 +842,26 @@ void App::DrawControlPanel() { ImGui::PushItemWidth(-1.0f); auto* activeCamera = getActiveCamera(); - DrawSectionHeader("PlaybackHeader", "Playback", accent); + nbl::ui::drawSectionHeader("PlaybackHeader", "Playback", accent, panelStyle); ImGui::Checkbox("Loop", &m_playback.loop); - DrawHoverHint("Loop playback when it reaches the end"); + nbl::ui::drawHoverHint("Loop playback when it reaches the end"); ImGui::Checkbox("Override input", &m_playback.overrideInput); - DrawHoverHint("Ignore manual input during playback"); + nbl::ui::drawHoverHint("Ignore manual input during playback"); ImGui::Checkbox("Affect all cameras", &m_playbackAffectsAll); - DrawHoverHint("Apply playback to all cameras"); + nbl::ui::drawHoverHint("Apply playback to all cameras"); ImGui::SliderFloat("Speed", &m_playback.speed, 0.1f, 4.f, "%.2f"); - DrawHoverHint("Playback speed multiplier"); + nbl::ui::drawHoverHint("Playback speed multiplier"); if (ImGui::Button(m_playback.playing ? "Pause" : "Play")) m_playback.playing = !m_playback.playing; - DrawHoverHint("Start or pause playback"); + nbl::ui::drawHoverHint("Start or pause playback"); ImGui::SameLine(); if (ImGui::Button("Stop")) { nbl::core::resetPlaybackCursor(m_playback); applyPlaybackAtTime(m_playback.time); } - DrawHoverHint("Stop playback and reset time"); + nbl::ui::drawHoverHint("Stop playback and reset time"); if (!m_keyframeTrack.keyframes.empty()) { @@ -992,13 +887,13 @@ void App::DrawControlPanel() } } - DrawSectionHeader("KeyframesHeader", "Keyframes", accent); + nbl::ui::drawSectionHeader("KeyframesHeader", "Keyframes", accent, panelStyle); ImGui::InputFloat("New keyframe time", &m_newKeyframeTime, 0.1f, 1.f, "%.3f"); - DrawHoverHint("Time value for new keyframe"); + nbl::ui::drawHoverHint("Time value for new keyframe"); ImGui::SameLine(); if (ImGui::Button("Use playback time")) m_newKeyframeTime = m_playback.time; - DrawHoverHint("Set new keyframe time from current playback position"); + nbl::ui::drawHoverHint("Set new keyframe time from current playback position"); const auto keyframeCaptureUi = analyzeCameraCaptureForUi(activeCamera); if (!keyframeCaptureUi.canCapture) ImGui::BeginDisabled(); @@ -1017,7 +912,7 @@ void App::DrawControlPanel() } if (!keyframeCaptureUi.canCapture) ImGui::EndDisabled(); - DrawHoverHint(keyframeCaptureUi.canCapture ? + nbl::ui::drawHoverHint(keyframeCaptureUi.canCapture ? "Add keyframe from current camera" : "Keyframe capture is blocked because there is no active camera or the current goal state is invalid"); ImGui::TextDisabled("Capture"); @@ -1030,7 +925,7 @@ void App::DrawControlPanel() nbl::core::resetPlaybackCursor(m_playback); clearApplyStatusBanner(m_playbackApplyBanner); } - DrawHoverHint("Remove all keyframes"); + nbl::ui::drawHoverHint("Remove all keyframes"); if (!m_keyframeTrack.keyframes.empty()) { @@ -1060,7 +955,7 @@ void App::DrawControlPanel() selectKeyframeNearestTime(selectedTime); clampPlaybackTimeToKeyframes(); } - DrawHoverHint("Edit selected keyframe time"); + nbl::ui::drawHoverHint("Edit selected keyframe time"); ImGui::TextDisabled("Keyframe source"); ImGui::SameLine(); @@ -1076,23 +971,23 @@ void App::DrawControlPanel() ImGui::TextColored(compatibilityColor, "%s", keyframeUi.compatibilityLabel.c_str()); if (keyframeUi.badges.exact) - DrawBadge("EXACT", good, badgeText); + nbl::ui::drawBadge("EXACT", good, badgeText, panelStyle); else if (keyframeUi.badges.bestEffort) - DrawBadge("BEST-EFFORT", warn, badgeText); + nbl::ui::drawBadge("BEST-EFFORT", warn, badgeText, panelStyle); if (keyframeUi.badges.dropsState) { ImGui::SameLine(); - DrawBadge("DROPS STATE", warn, badgeText); + nbl::ui::drawBadge("DROPS STATE", warn, badgeText, panelStyle); } else if (keyframeUi.badges.sharedStateOnly) { ImGui::SameLine(); - DrawBadge("SHARED STATE", accent, badgeText); + nbl::ui::drawBadge("SHARED STATE", accent, badgeText, panelStyle); } if (keyframeUi.badges.blocked) { ImGui::SameLine(); - DrawBadge("BLOCKED", bad, badgeText); + nbl::ui::drawBadge("BLOCKED", bad, badgeText, panelStyle); } if (!keyframeUi.canApply) @@ -1101,7 +996,7 @@ void App::DrawControlPanel() applyPresetFromUi(activeCamera, selectedKeyframe->preset); if (!keyframeUi.canApply) ImGui::EndDisabled(); - DrawHoverHint(keyframeUi.canApply ? + nbl::ui::drawHoverHint(keyframeUi.canApply ? "Apply selected keyframe to the active camera" : "Apply is blocked because there is no active camera or the keyframe goal is invalid"); ImGui::SameLine(); @@ -1111,7 +1006,7 @@ void App::DrawControlPanel() replaceSelectedKeyframeFromCamera(activeCamera); if (!keyframeCaptureUi.canCapture) ImGui::EndDisabled(); - DrawHoverHint(keyframeCaptureUi.canCapture ? + nbl::ui::drawHoverHint(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"); ImGui::SameLine(); @@ -1120,7 +1015,7 @@ void App::DrawControlPanel() m_playback.time = selectedKeyframe->time; applyPlaybackAtTime(m_playback.time); } - DrawHoverHint("Set playback time to selected keyframe and preview it"); + nbl::ui::drawHoverHint("Set playback time to selected keyframe and preview it"); ImGui::SameLine(); if (ImGui::Button("Remove selected")) { @@ -1130,24 +1025,24 @@ void App::DrawControlPanel() if (m_keyframeTrack.keyframes.empty()) clearApplyStatusBanner(m_playbackApplyBanner); } - DrawHoverHint("Remove selected keyframe"); + nbl::ui::drawHoverHint("Remove selected keyframe"); } - DrawSectionHeader("KeyframesStorageHeader", "Keyframe Storage", accent); + nbl::ui::drawSectionHeader("KeyframesStorageHeader", "Keyframe Storage", accent, panelStyle); ImGui::InputText("Keyframe file", m_keyframePath, IM_ARRAYSIZE(m_keyframePath)); if (ImGui::Button("Save keyframes")) { if (!saveKeyframesToFile(nbl::system::path(m_keyframePath))) m_logger->log("Failed to save keyframes to \"%s\".", ILogger::ELL_ERROR, m_keyframePath); } - DrawHoverHint("Save keyframes to JSON file"); + nbl::ui::drawHoverHint("Save keyframes to JSON file"); ImGui::SameLine(); if (ImGui::Button("Load keyframes")) { if (!loadKeyframesFromFile(nbl::system::path(m_keyframePath))) m_logger->log("Failed to load keyframes from \"%s\".", ILogger::ELL_ERROR, m_keyframePath); } - DrawHoverHint("Load keyframes from JSON file"); + nbl::ui::drawHoverHint("Load keyframes from JSON file"); } ImGui::PopItemWidth(); } @@ -1161,7 +1056,7 @@ void App::DrawControlPanel() ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 4.0f); if (ImGui::BeginChild("GizmoPanel", ImVec2(0, 0), true)) { - DrawSectionHeader("GizmoHeader", "Gizmo", accent); + nbl::ui::drawSectionHeader("GizmoHeader", "Gizmo", accent, panelStyle); TransformEditorContents(); } ImGui::EndChild(); @@ -1174,7 +1069,7 @@ void App::DrawControlPanel() ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 4.0f); if (ImGui::BeginChild("LogPanel", ImVec2(0, 0), true)) { - DrawSectionHeader("LogHeader", "Virtual Events", accent); + nbl::ui::drawSectionHeader("LogHeader", "Virtual Events", accent, panelStyle); ImGui::Checkbox("Auto-scroll", &m_logAutoScroll); ImGui::SameLine(); ImGui::Checkbox("Wrap", &m_logWrap); @@ -1210,8 +1105,7 @@ void App::DrawControlPanel() } ImGui::End(); - ImGui::PopStyleColor(19); - ImGui::PopStyleVar(9); + nbl::ui::popControlPanelWindowStyle(); } diff --git a/61_UI/AppImGuiListen.cpp b/61_UI/AppImGuiListen.cpp index d406c1572..d9573dedd 100644 --- a/61_UI/AppImGuiListen.cpp +++ b/61_UI/AppImGuiListen.cpp @@ -1,372 +1,295 @@ #include "app/App.hpp" +#include "app/AppViewportBindingUtilities.hpp" void App::imguiListen() { - ImGuiIO& io = ImGui::GetIO(); - if (m_ciMode) - { - io.IniFilename = nullptr; - useWindow = true; - } - - ImGuizmo::BeginFrame(); + ImGuiIO& io = ImGui::GetIO(); + if (m_ciMode) + { + io.IniFilename = nullptr; + useWindow = true; + } + + ImGuizmo::BeginFrame(); + + SImResourceInfo info; + info.samplerIx = static_cast(nbl::ext::imgui::UI::DefaultSamplerIx::USER); + + if (useWindow) + drawWindowedViewportWindows(io, info); + else + drawFullscreenViewportWindow(io, info); + + drawScriptVisualDebugOverlay(io.DisplaySize); + DrawControlPanel(); + UpdateBoundCameraMovement(); + UpdateCursorVisibility(); + applyFollowToConfiguredCameras(); + refreshViewportBindingMatrices(); +} + +void App::drawWindowedViewportWindows(ImGuiIO& io, SImResourceInfo& info) +{ + syncVisualDebugWindowBindings(); + const bool hideSceneGizmos = enableActiveCameraMovement || (m_scriptedInput.enabled && m_scriptedInput.visualDebug); + ImGuizmo::Enable(!hideSceneGizmos); + + size_t gizmoIx = 0u; + size_t manipulationCounter = 0u; + const std::optional manipulatedObjectIx = ImGuizmo::IsUsingAny() ? std::optional(getManipulatedObjectIx()) : std::nullopt; + (void)manipulatedObjectIx; + + for (uint32_t windowIx = 0u; windowIx < windowBindings.size(); ++windowIx) + { + { + const auto& rw = wInit.renderWindows[windowIx]; + const ImGuiCond windowCond = m_ciMode ? ImGuiCond_Always : ImGuiCond_Appearing; + ImGui::SetNextWindowPos({ rw.iPos.x, rw.iPos.y }, windowCond); + ImGui::SetNextWindowSize({ rw.iSize.x, rw.iSize.y }, windowCond); + } + ImGui::SetNextWindowSizeConstraints(ImVec2(0x45, 0x45), ImVec2(7680, 4320)); + + nbl::ui::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); + const ImVec2 contentRegionSize = ImGui::GetContentRegionAvail(); + const ImVec2 cursorPos = ImGui::GetCursorScreenPos(); + const nbl::ui::SViewportOverlayRect viewportRect = { cursorPos, contentRegionSize }; + + if (ImGuiWindow* const window = ImGui::GetCurrentWindow()) + { + const auto mousePos = ImGui::GetMousePos(); + const bool mouseInsideViewport = + mousePos.x >= cursorPos.x && + mousePos.y >= cursorPos.y && + mousePos.x <= cursorPos.x + contentRegionSize.x && + mousePos.y <= cursorPos.y + contentRegionSize.y; + if (mouseInsideViewport) + window->Flags |= ImGuiWindowFlags_NoMove; + else + window->Flags &= ~ImGuiWindowFlags_NoMove; + } + + auto& binding = windowBindings[windowIx]; + nbl::ui::SBoundViewportCameraState viewportState = {}; + const auto planarSpan = std::span>(m_planarProjections.data(), m_planarProjections.size()); + if (!nbl::ui::tryBuildViewportBoundCameraState(planarSpan, binding, contentRegionSize, flipGizmoY, viewportState)) + { + ImGui::End(); + nbl::ui::popViewportWindowStyle(); + continue; + } + + auto* const planarViewCameraBound = viewportState.camera; + auto& projection = *viewportState.projection; + info.textureID = windowIx + 1u; + + ImGuizmo::AllowAxisFlip(binding.allowGizmoAxesToFlip); + ImGuizmo::SetOrthographic(projection.getParameters().m_type == IPlanarProjection::CProjection::Orthographic); + ImGuizmo::SetDrawlist(); + ImGui::Image(info, contentRegionSize); + ImGuizmo::SetRect(cursorPos.x, cursorPos.y, contentRegionSize.x, contentRegionSize.y); + + if (auto* drawList = ImGui::GetWindowDrawList(); drawList) + { + const char* projLabel = 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(getCameraTypeLabel(planarViewCameraBound)) + ": " + std::string(getCameraTypeDescription(planarViewCameraBound)); + overlayData.detail = "Frustum: active camera (hidden in owner view)"; + nbl::ui::drawViewportInfoOverlay(*drawList, viewportRect, overlayData); + + if (m_scriptedInput.enabled && m_scriptedInput.visualDebug && m_scriptedInput.visualFollowActive) + nbl::ui::drawFollowTargetViewportOverlay(*drawList, viewportState.viewProjMatrix, m_followTarget, viewportRect); + } + + const bool windowHovered = ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem); + const bool windowFocused = ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows); + if (!(m_scriptedInput.enabled && m_scriptedInput.exclusive)) + { + if (!m_scriptedInput.enabled && windowHovered) + activeRenderWindowIx = windowIx; + else if (windowFocused) + activeRenderWindowIx = windowIx; + } + + if (!hideSceneGizmos) + { + for (uint32_t objectIx = 0u; objectIx < getManipulableObjectCount(); ++objectIx) { - if (!m_ciMode) + ImGuizmo::PushID(gizmoIx); + ++gizmoIx; + + const auto planarIx = getManipulableObjectPlanarIx(objectIx); + const bool isFollowTarget = isManipulableObjectFollowTarget(objectIx); + ICamera* const targetGimbalManipulationCamera = planarIx.has_value() ? m_planarProjections[planarIx.value()]->getCamera() : nullptr; + if (targetGimbalManipulationCamera == planarViewCameraBound) { + ImGuizmo::PopID(); + continue; } - SImResourceInfo info; - info.samplerIx = (uint16_t)nbl::ext::imgui::UI::DefaultSamplerIx::USER; + ImGuizmoModelM16InOut imguizmoModel; + imguizmoModel.inTRS = getManipulableObjectTransform(objectIx); - // render bound planar camera views onto GUI windows - if (useWindow) + const float gizmoWorldRadius = isFollowTarget ? 0.35f : 0.22f; + const auto gizmoWorldPos = getManipulableObjectWorldPosition(objectIx); + const auto viewPos = mul(viewportState.viewMatrix, float32_t4(gizmoWorldPos, 1.0f)); + const float depth = std::max(0.001f, std::abs(viewPos.z)); + float gizmoSizeClip = 0.1f; + if (projection.getParameters().m_type == IPlanarProjection::CProjection::Perspective) + gizmoSizeClip = (gizmoWorldRadius * viewportState.projectionMatrix[1][1]) / depth; + else + gizmoSizeClip = gizmoWorldRadius * viewportState.projectionMatrix[1][1]; + ImGuizmo::SetGizmoSizeClipSpace(gizmoSizeClip); + + imguizmoModel.outTRS = imguizmoModel.inTRS; + const bool success = ImGuizmo::Manipulate( + &viewportState.imguizmoPlanar.view[0][0], + &viewportState.imguizmoPlanar.projection[0][0], + ImGuizmo::OPERATION::UNIVERSAL, + mCurrentGizmoMode, + &imguizmoModel.outTRS[0][0], + &imguizmoModel.outDeltaTRS[0][0], + useSnap ? &snap[0] : nullptr); + + if (success) { - syncVisualDebugWindowBindings(); - const bool hideSceneGizmos = enableActiveCameraMovement || (m_scriptedInput.enabled && m_scriptedInput.visualDebug); - if(hideSceneGizmos) - ImGuizmo::Enable(false); + if (targetGimbalManipulationCamera) + { + const auto referenceFrame = getCastedMatrix(imguizmoModel.outTRS); + bindManipulatedCamera(planarIx.value()); + nbl::core::applyReferenceFrameToCamera(targetGimbalManipulationCamera, referenceFrame); + refreshFollowOffsetConfigForPlanar(planarIx.value()); + } + else if (isFollowTarget) + { + setFollowTargetTransform(getCastedMatrix(imguizmoModel.outTRS)); + bindManipulatedFollowTarget(); + applyFollowToConfiguredCameras(); + } else - ImGuizmo::Enable(true); - - size_t gizmoIx = {}; - size_t manipulationCounter = {}; - const std::optional modelInUseIx = ImGuizmo::IsUsingAny() ? std::optional(getManipulatedObjectIx()) : std::optional(std::nullopt); - (void)modelInUseIx; - - for (uint32_t windowIx = 0; windowIx < windowBindings.size(); ++windowIx) { - // setup - { - const auto& rw = wInit.renderWindows[windowIx]; - const ImGuiCond windowCond = m_ciMode ? ImGuiCond_Always : ImGuiCond_Appearing; - ImGui::SetNextWindowPos({ rw.iPos.x, rw.iPos.y }, windowCond); - ImGui::SetNextWindowSize({ rw.iSize.x, rw.iSize.y }, windowCond); - } - ImGui::SetNextWindowSizeConstraints(ImVec2(0x45, 0x45), ImVec2(7680, 4320)); - - ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); - ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f)); - ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.0f, 0.0f, 0.0f, 0.0f)); - const std::string ident = "Render Window \"" + std::to_string(windowIx) + "\""; - - ImGui::Begin(ident.data(), 0, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoBringToFrontOnFocus); - const ImVec2 contentRegionSize = ImGui::GetContentRegionAvail(), windowPos = ImGui::GetWindowPos(), cursorPos = ImGui::GetCursorScreenPos(); - - ImGuiWindow* window = ImGui::GetCurrentWindow(); - { - const auto mPos = ImGui::GetMousePos(); - - if (mPos.x < cursorPos.x || mPos.y < cursorPos.y || mPos.x > cursorPos.x + contentRegionSize.x || mPos.y > cursorPos.y + contentRegionSize.y) - window->Flags &= ~ImGuiWindowFlags_NoMove; - else - window->Flags |= ImGuiWindowFlags_NoMove; - } - - // setup bound entities for the window like camera & projections - auto& binding = windowBindings[windowIx]; - auto& planarBound = m_planarProjections[binding.activePlanarIx]; - assert(planarBound); - - binding.aspectRatio = contentRegionSize.x / contentRegionSize.y; - auto* planarViewCameraBound = planarBound->getCamera(); - - assert(planarViewCameraBound); - assert(binding.boundProjectionIx.has_value()); - - auto& projection = planarBound->getPlanarProjections()[binding.boundProjectionIx.value()]; - syncDynamicPerspectiveProjection(planarViewCameraBound, projection); - projection.update(binding.leftHandedProjection, binding.aspectRatio); - - // first 0th texture is for UI texture atlas, then there are our window textures - auto fboImguiTextureID = windowIx + 1u; - info.textureID = fboImguiTextureID; - - if(binding.allowGizmoAxesToFlip) - ImGuizmo::AllowAxisFlip(true); - else - ImGuizmo::AllowAxisFlip(false); - - if(projection.getParameters().m_type == IPlanarProjection::CProjection::Orthographic) - ImGuizmo::SetOrthographic(true); - else - ImGuizmo::SetOrthographic(false); - - ImGuizmo::SetDrawlist(); - ImGui::Image(info, contentRegionSize); - ImGuizmo::SetRect(cursorPos.x, cursorPos.y, contentRegionSize.x, contentRegionSize.y); - { - const char* projLabel = projection.getParameters().m_type == IPlanarProjection::CProjection::Perspective ? "Persp" : "Ortho"; - const std::string overlayText = "Planar " + std::to_string(binding.activePlanarIx) + " | " + projLabel + " | W" + std::to_string(windowIx); - const std::string cameraText = std::string(getCameraTypeLabel(planarViewCameraBound)) + ": " + std::string(getCameraTypeDescription(planarViewCameraBound)); - const std::string frustumText = "Frustum: active camera (hidden in owner view)"; - const ImVec2 textSize = ImGui::CalcTextSize(overlayText.c_str()); - const ImVec2 descSize = ImGui::CalcTextSize(cameraText.c_str()); - const ImVec2 frustumSize = ImGui::CalcTextSize(frustumText.c_str()); - const ImVec2 pad = ImVec2(6.0f, 4.0f); - const float lineGap = 2.0f; - const float width = std::max(std::max(textSize.x, descSize.x), frustumSize.x); - const float height = textSize.y + descSize.y + frustumSize.y + lineGap * 2.0f + pad.y * 2.0f; - ImVec2 overlayPos = ImVec2(cursorPos.x + contentRegionSize.x - width - pad.x * 2.0f - 6.0f, cursorPos.y + 6.0f); - overlayPos.x = std::max(overlayPos.x, cursorPos.x + 6.0f); - ImVec2 overlayMax = ImVec2(overlayPos.x + width + pad.x * 2.0f, overlayPos.y + height); - auto* drawList = ImGui::GetWindowDrawList(); - drawList->AddRectFilled(overlayPos, overlayMax, ImGui::ColorConvertFloat4ToU32(ImVec4(0.05f, 0.06f, 0.08f, 0.80f)), 6.0f); - drawList->AddRect(overlayPos, overlayMax, ImGui::ColorConvertFloat4ToU32(ImVec4(0.60f, 0.66f, 0.76f, 0.80f)), 6.0f); - drawList->AddText(ImVec2(overlayPos.x + pad.x, overlayPos.y + pad.y), ImGui::ColorConvertFloat4ToU32(ImVec4(0.96f, 0.98f, 1.0f, 1.0f)), overlayText.c_str()); - drawList->AddText(ImVec2(overlayPos.x + pad.x, overlayPos.y + pad.y + textSize.y + lineGap), ImGui::ColorConvertFloat4ToU32(ImVec4(0.78f, 0.82f, 0.90f, 1.0f)), cameraText.c_str()); - drawList->AddText(ImVec2(overlayPos.x + pad.x, overlayPos.y + pad.y + textSize.y + descSize.y + lineGap * 2.0f), ImGui::ColorConvertFloat4ToU32(ImVec4(0.96f, 0.90f, 0.36f, 1.0f)), frustumText.c_str()); - } + m_model = float32_t3x4(hlsl::transpose(imguizmoModel.outTRS)); + bindManipulatedModel(); + } + } - const bool windowHovered = ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem); - const bool windowFocused = ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows); - if (!(m_scriptedInput.enabled && m_scriptedInput.exclusive)) + if (ImGuizmo::IsOver() && !ImGuizmo::IsUsingAny() && !enableActiveCameraMovement) + { + if (targetGimbalManipulationCamera && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) + { + const uint32_t newPlanarIx = planarIx.value(); + if (newPlanarIx < m_planarProjections.size()) { - if (!m_scriptedInput.enabled && windowHovered) - activeRenderWindowIx = windowIx; - else if (windowFocused) + binding.activePlanarIx = newPlanarIx; + binding.pickDefaultProjections(m_planarProjections[binding.activePlanarIx]->getPlanarProjections()); + if (!(m_scriptedInput.enabled && m_scriptedInput.exclusive)) activeRenderWindowIx = windowIx; } - - // we render a scene from view of a camera bound to planar window - ImGuizmoPlanarM16InOut imguizmoPlanar; - imguizmoPlanar.view = getCastedMatrix(hlsl::transpose(getMatrix3x4As4x4(planarViewCameraBound->getGimbal().getViewMatrix()))); - imguizmoPlanar.projection = getCastedMatrix(hlsl::transpose(projection.getProjectionMatrix())); - const auto viewMatrix = getMatrix3x4As4x4(getCastedMatrix(planarViewCameraBound->getGimbal().getViewMatrix())); - const auto projectionMatrix = getCastedMatrix(projection.getProjectionMatrix()); - const auto viewProjMatrix = mul(projectionMatrix, viewMatrix); - - if (flipGizmoY) // note we allow to flip gizmo just to match our coordinates - imguizmoPlanar.projection[1][1] *= -1.f; // https://johannesugb.github.io/gpu-programming/why-do-opengl-proj-matrices-fail-in-vulkan/ - - drawFollowTargetViewportOverlay(viewProjMatrix, cursorPos, contentRegionSize); - - if (!hideSceneGizmos) - { - for (uint32_t objectIx = 0u; objectIx < getManipulableObjectCount(); ++objectIx) - { - ImGuizmo::PushID(gizmoIx); ++gizmoIx; - - const auto planarIx = getManipulableObjectPlanarIx(objectIx); - const bool isFollowTarget = isManipulableObjectFollowTarget(objectIx); - ICamera* const targetGimbalManipulationCamera = planarIx.has_value() ? m_planarProjections[planarIx.value()]->getCamera() : nullptr; - - if (targetGimbalManipulationCamera == planarViewCameraBound) - { - ImGuizmo::PopID(); - continue; - } - - ImGuizmoModelM16InOut imguizmoModel; - imguizmoModel.inTRS = getManipulableObjectTransform(objectIx); - - const float gizmoWorldRadius = isFollowTarget ? 0.35f : 0.22f; - const auto gizmoWorldPos = getManipulableObjectWorldPosition(objectIx); - - const auto viewPos = mul(viewMatrix, float32_t4(gizmoWorldPos, 1.0f)); - const float depth = std::max(0.001f, std::abs(viewPos.z)); - float gizmoSizeClip = 0.1f; - if (projection.getParameters().m_type == IPlanarProjection::CProjection::Perspective) - gizmoSizeClip = (gizmoWorldRadius * projectionMatrix[1][1]) / depth; - else - gizmoSizeClip = gizmoWorldRadius * projectionMatrix[1][1]; - ImGuizmo::SetGizmoSizeClipSpace(gizmoSizeClip); - - imguizmoModel.outTRS = imguizmoModel.inTRS; - { - const bool success = ImGuizmo::Manipulate(&imguizmoPlanar.view[0][0], &imguizmoPlanar.projection[0][0], ImGuizmo::OPERATION::UNIVERSAL, mCurrentGizmoMode, &imguizmoModel.outTRS[0][0], &imguizmoModel.outDeltaTRS[0][0], useSnap ? &snap[0] : nullptr); - - if (success) - { - if (targetGimbalManipulationCamera) - { - const auto referenceFrame = getCastedMatrix(imguizmoModel.outTRS); - bindManipulatedCamera(planarIx.value()); - nbl::core::applyReferenceFrameToCamera(targetGimbalManipulationCamera, referenceFrame); - refreshFollowOffsetConfigForPlanar(planarIx.value()); - } - else if (isFollowTarget) - { - setFollowTargetTransform(getCastedMatrix(imguizmoModel.outTRS)); - bindManipulatedFollowTarget(); - applyFollowToConfiguredCameras(); - } - else - { - m_model = float32_t3x4(hlsl::transpose(imguizmoModel.outTRS)); - bindManipulatedModel(); - } - } - - if (ImGuizmo::IsOver() and not ImGuizmo::IsUsingAny() && not enableActiveCameraMovement) - { - if (targetGimbalManipulationCamera && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) - { - const uint32_t newPlanarIx = planarIx.value(); - if (newPlanarIx < m_planarProjections.size()) - { - binding.activePlanarIx = newPlanarIx; - binding.pickDefaultProjections(m_planarProjections[binding.activePlanarIx]->getPlanarProjections()); - if (!(m_scriptedInput.enabled && m_scriptedInput.exclusive)) - activeRenderWindowIx = windowIx; - } - } - else if (isFollowTarget && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) - bindManipulatedFollowTarget(); - else if (!targetGimbalManipulationCamera && !isFollowTarget && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) - bindManipulatedModel(); - - ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.2f, 0.2f, 0.2f, 0.8f)); - ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); - ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.5f); - - ImGuiIO& io = ImGui::GetIO(); - ImVec2 mousePos = io.MousePos; - ImGui::SetNextWindowPos(ImVec2(mousePos.x + 10, mousePos.y + 10), ImGuiCond_Always); - - ImGui::Begin("InfoOverlay", nullptr, - ImGuiWindowFlags_NoDecoration | - ImGuiWindowFlags_AlwaysAutoResize | - ImGuiWindowFlags_NoSavedSettings); - - std::string ident; - - if (targetGimbalManipulationCamera) - ident = targetGimbalManipulationCamera->getIdentifier(); - else if (isFollowTarget) - ident = m_followTarget.getIdentifier(); - else - ident = "Geometry Creator Object"; - - ImGui::Text("Identifier: %s", ident.c_str()); - ImGui::Text("Object Ix: %u", objectIx); - if (targetGimbalManipulationCamera) - { - ImGui::Separator(); - ImGui::TextDisabled("RMB: switch view to this camera"); - ImGui::TextDisabled("LMB drag: manipulate gizmo"); - ImGui::TextDisabled("SPACE: toggle move mode"); - } - else if (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"); - } - - ImGui::End(); - - ImGui::PopStyleVar(); - ImGui::PopStyleColor(2); - } - } - ImGuizmo::PopID(); - } - } - - ImGui::End(); - ImGui::PopStyleColor(1); - ImGui::PopStyleVar(3); } - if (windowBindings.size() > 1u) + else if (isFollowTarget && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) { - const auto& topRw = wInit.renderWindows[0]; - const float splitY = topRw.iPos.y + topRw.iSize.y; - const float gap = std::max(0.0f, wInit.renderWindows[1].iPos.y - splitY); - ImGui::SetNextWindowPos(ImVec2(0.0f, 0.0f), ImGuiCond_Always); - ImGui::SetNextWindowSize(io.DisplaySize, ImGuiCond_Always); - ImGui::Begin("SplitOverlay", nullptr, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoBringToFrontOnFocus); - auto* drawList = ImGui::GetWindowDrawList(); - if (gap >= 2.0f) - drawList->AddRectFilled(ImVec2(0.0f, splitY), ImVec2(io.DisplaySize.x, splitY + gap), ImGui::ColorConvertFloat4ToU32(ImVec4(0.05f, 0.06f, 0.08f, 0.85f))); - else - drawList->AddLine(ImVec2(0.0f, splitY), ImVec2(io.DisplaySize.x, splitY), ImGui::ColorConvertFloat4ToU32(ImVec4(0.80f, 0.84f, 0.92f, 0.75f)), 2.0f); - ImGui::End(); + bindManipulatedFollowTarget(); } - assert(manipulationCounter <= 1u); - } - // render selected camera view onto full screen - else - { - info.textureID = 1u + activeRenderWindowIx; - - 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); - const ImVec2 contentRegionSize = ImGui::GetContentRegionAvail(), windowPos = ImGui::GetWindowPos(), cursorPos = ImGui::GetCursorScreenPos(); + else if (!targetGimbalManipulationCamera && !isFollowTarget && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) { - auto& binding = windowBindings[activeRenderWindowIx]; - auto& planarBound = m_planarProjections[binding.activePlanarIx]; - assert(planarBound); + bindManipulatedModel(); + } - binding.aspectRatio = contentRegionSize.x / contentRegionSize.y; - auto* planarViewCameraBound = planarBound->getCamera(); + const ImVec2 mousePos = ImGui::GetIO().MousePos; + nbl::ui::beginHoverInfoOverlay("InfoOverlay", mousePos); - assert(planarViewCameraBound); - assert(binding.boundProjectionIx.has_value()); + std::string objectLabel; + if (targetGimbalManipulationCamera) + objectLabel = targetGimbalManipulationCamera->getIdentifier(); + else if (isFollowTarget) + objectLabel = m_followTarget.getIdentifier(); + else + objectLabel = "Geometry Creator Object"; - auto& projection = planarBound->getPlanarProjections()[binding.boundProjectionIx.value()]; - syncDynamicPerspectiveProjection(planarViewCameraBound, projection); - projection.update(binding.leftHandedProjection, binding.aspectRatio); + ImGui::Text("Identifier: %s", objectLabel.c_str()); + ImGui::Text("Object Ix: %u", objectIx); + if (targetGimbalManipulationCamera) + { + ImGui::Separator(); + ImGui::TextDisabled("RMB: switch view to this camera"); + ImGui::TextDisabled("LMB drag: manipulate gizmo"); + ImGui::TextDisabled("SPACE: toggle move mode"); } - - ImGui::Image(info, contentRegionSize); - ImGuizmo::SetRect(cursorPos.x, cursorPos.y, contentRegionSize.x, contentRegionSize.y); + else if (isFollowTarget) { - auto& binding = windowBindings[activeRenderWindowIx]; - auto& planarBound = m_planarProjections[binding.activePlanarIx]; - auto* planarViewCameraBound = planarBound ? planarBound->getCamera() : nullptr; - if (planarViewCameraBound && binding.boundProjectionIx.has_value()) - { - auto& projection = planarBound->getPlanarProjections()[binding.boundProjectionIx.value()]; - const auto viewMatrix = getMatrix3x4As4x4(getCastedMatrix(planarViewCameraBound->getGimbal().getViewMatrix())); - const auto projectionMatrix = getCastedMatrix(projection.getProjectionMatrix()); - const auto viewProjMatrix = mul(projectionMatrix, viewMatrix); - drawFollowTargetViewportOverlay(viewProjMatrix, cursorPos, contentRegionSize); - } + 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"); } - ImGui::End(); - ImGui::PopStyleColor(1); + nbl::ui::endHoverInfoOverlay(); } - } - - drawScriptVisualDebugOverlay(io.DisplaySize); - DrawControlPanel(); - UpdateBoundCameraMovement(); - UpdateCursorVisibility(); - applyFollowToConfiguredCameras(); - - // update camera matrices for scene rendering - { - for (uint32_t i = 0u; i < windowBindings.size(); ++i) - { - auto& binding = windowBindings[i]; - - auto& planarBound = m_planarProjections[binding.activePlanarIx]; - assert(planarBound); - auto* boundPlanarCamera = planarBound->getCamera(); - - assert(binding.boundProjectionIx.has_value()); - auto& projection = planarBound->getPlanarProjections()[binding.boundProjectionIx.value()]; - syncDynamicPerspectiveProjection(boundPlanarCamera, projection); - projection.update(binding.leftHandedProjection, binding.aspectRatio); - binding.isOrthographicProjection = projection.getParameters().m_type == IPlanarProjection::CProjection::Orthographic; - - auto viewMatrix = getCastedMatrix(boundPlanarCamera->getGimbal().getViewMatrix()); - auto projectionMatrix = getCastedMatrix(projection.getProjectionMatrix()); - auto viewProjMatrix = mul(projectionMatrix, getMatrix3x4As4x4(viewMatrix)); - binding.viewMatrix = viewMatrix; - binding.projectionMatrix = projectionMatrix; - binding.viewProjMatrix = viewProjMatrix; - } + ImGuizmo::PopID(); } + } + + ImGui::End(); + nbl::ui::popViewportWindowStyle(); + } + + if (windowBindings.size() > 1u) + { + const auto& topRw = wInit.renderWindows[0]; + const float splitY = topRw.iPos.y + topRw.iSize.y; + const float gap = std::max(0.0f, wInit.renderWindows[1].iPos.y - splitY); + ImGui::SetNextWindowPos(ImVec2(0.0f, 0.0f), ImGuiCond_Always); + ImGui::SetNextWindowSize(io.DisplaySize, ImGuiCond_Always); + ImGui::Begin("SplitOverlay", nullptr, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoBringToFrontOnFocus); + if (auto* drawList = ImGui::GetWindowDrawList(); drawList) + nbl::ui::drawViewportSplitOverlay(*drawList, io.DisplaySize, splitY, gap); + ImGui::End(); + } + + assert(manipulationCounter <= 1u); +} - - +void App::drawFullscreenViewportWindow(ImGuiIO& io, SImResourceInfo& info) +{ + info.textureID = 1u + 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); + const ImVec2 contentRegionSize = ImGui::GetContentRegionAvail(); + const ImVec2 cursorPos = ImGui::GetCursorScreenPos(); + + nbl::ui::SBoundViewportCameraState viewportState = {}; + const auto planarSpan = std::span>(m_planarProjections.data(), m_planarProjections.size()); + const bool viewportValid = nbl::ui::tryBuildViewportBoundCameraState(planarSpan, windowBindings[activeRenderWindowIx], contentRegionSize, false, viewportState); + + ImGui::Image(info, contentRegionSize); + ImGuizmo::SetRect(cursorPos.x, cursorPos.y, contentRegionSize.x, contentRegionSize.y); + if (viewportValid && m_scriptedInput.enabled && m_scriptedInput.visualDebug && m_scriptedInput.visualFollowActive) + { + if (auto* drawList = ImGui::GetWindowDrawList(); drawList) + { + const nbl::ui::SViewportOverlayRect followRect = { cursorPos, contentRegionSize }; + nbl::ui::drawFollowTargetViewportOverlay(*drawList, viewportState.viewProjMatrix, m_followTarget, followRect); + } + } + + ImGui::End(); + ImGui::PopStyleColor(1); } +void App::refreshViewportBindingMatrices() +{ + const auto planarSpan = std::span>(m_planarProjections.data(), m_planarProjections.size()); + for (auto& binding : windowBindings) + { + nbl::ui::SBoundViewportCameraState viewportState = {}; + nbl::ui::tryBuildWindowBindingMatrices(planarSpan, binding, viewportState); + } +} diff --git a/61_UI/AppInit.cpp b/61_UI/AppInit.cpp index 2973ebbbb..a63c0ad84 100644 --- a/61_UI/AppInit.cpp +++ b/61_UI/AppInit.cpp @@ -4,162 +4,24 @@ #include #include #include -#include -#include #include #include #include +#include #include +#include "app/AppCameraConfigUtilities.hpp" +#include "app/AppResourceUtilities.hpp" #include "camera/CCameraPersistence.hpp" #include "camera/CCameraScriptedRuntimePersistence.hpp" -#include "nlohmann/json.hpp" +#include "camera/CCameraSmokeRegressionUtilities.hpp" namespace { - using camera_json_t = nlohmann::json; - - struct SpaceEnvBlobHeader final - { - uint32_t magic = 0u; - uint32_t width = 0u; - uint32_t height = 0u; - uint32_t format = 0u; - uint64_t payloadSize = 0ull; - }; - - constexpr uint32_t SpaceEnvBlobMagic = 0x31425645u; // "EVB1" - constexpr uint32_t SpaceEnvBlobFormatRgba16Sfloat = 2u; - - bool loadSpaceEnvBlob(const std::filesystem::path& blobPath, SpaceEnvBlobHeader& outHeader, std::vector& outPayload) - { - std::ifstream in(blobPath, std::ios::binary); - if (!in.is_open()) - return false; - - in.read(reinterpret_cast(&outHeader), sizeof(outHeader)); - if (in.gcount() != sizeof(outHeader)) - return false; - - if (outHeader.magic != SpaceEnvBlobMagic || outHeader.format != 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; - - outPayload.resize(static_cast(outHeader.payloadSize)); - in.read(reinterpret_cast(outPayload.data()), static_cast(outPayload.size())); - return in.gcount() == static_cast(outPayload.size()); - } - - constexpr float CameraDefaultMoveScale = 0.01f; - constexpr float CameraDefaultRotateScale = 0.003f; - constexpr float CameraOrbitMoveScale = 0.5f; - - void initializeCameraRigConfig(nbl::core::ICamera& camera, const double moveScale, const double rotationScale) - { - camera.setMotionScales(moveScale, rotationScale); - } - - bool createCameraFromJson(const camera_json_t& jCamera, std::string& error, smart_refctd_ptr& outCamera) - { - if (!jCamera.contains("type")) - { - error = "Camera entry missing \"type\"."; - return false; - } - - if (!jCamera.contains("position")) - { - error = "Camera entry missing \"position\"."; - return false; - } - - const std::string type = jCamera["type"].get(); - const bool withOrientation = jCamera.contains("orientation"); - const bool withTarget = jCamera.contains("target"); - - auto position = [&]() - { - const auto jret = jCamera["position"].get>(); - return float64_t3(jret[0], jret[1], jret[2]); - }(); - - auto getOrientation = [&]() - { - const auto jret = jCamera["orientation"].get>(); - return makeQuaternionFromComponents(jret[0], jret[1], jret[2], jret[3]); - }; - - auto getTarget = [&]() - { - const auto jret = jCamera["target"].get>(); - return float64_t3(jret[0], jret[1], jret[2]); - }; - - auto finalize = [&](auto&& camera, const double moveScale, const double rotationScale) - { - initializeCameraRigConfig(*camera, moveScale, rotationScale); - outCamera = std::move(camera); - return true; - }; - - if (type == "FPS") - { - if (!withOrientation) - { - error = "FPS camera requires \"orientation\"."; - return false; - } - return finalize(make_smart_refctd_ptr(position, getOrientation()), CameraDefaultMoveScale, CameraDefaultRotateScale); - } + using camera_json_t = nbl::system::camera_json_t; - if (type == "Free") - { - if (!withOrientation) - { - error = "Free camera requires \"orientation\"."; - return false; - } - return finalize(make_smart_refctd_ptr(position, getOrientation()), CameraDefaultMoveScale, CameraDefaultRotateScale); - } - - if (!withTarget) - { - error = "Camera type \"" + type + "\" requires \"target\"."; - return false; - } - - if (type == "Orbit") - return finalize(make_smart_refctd_ptr(position, getTarget()), CameraOrbitMoveScale, CameraDefaultRotateScale); - if (type == "Arcball") - return finalize(make_smart_refctd_ptr(position, getTarget()), CameraOrbitMoveScale, CameraDefaultRotateScale); - if (type == "Turntable") - return finalize(make_smart_refctd_ptr(position, getTarget()), CameraOrbitMoveScale, CameraDefaultRotateScale); - if (type == "TopDown") - return finalize(make_smart_refctd_ptr(position, getTarget()), CameraOrbitMoveScale, CameraDefaultRotateScale); - if (type == "Isometric") - return finalize(make_smart_refctd_ptr(position, getTarget()), CameraOrbitMoveScale, CameraDefaultRotateScale); - if (type == "Chase") - return finalize(make_smart_refctd_ptr(position, getTarget()), CameraOrbitMoveScale, CameraDefaultRotateScale); - if (type == "Dolly") - return finalize(make_smart_refctd_ptr(position, getTarget()), CameraOrbitMoveScale, CameraDefaultRotateScale); - if (type == "Path") - return finalize(make_smart_refctd_ptr(position, getTarget()), CameraOrbitMoveScale, CameraDefaultRotateScale); - if (type == "DollyZoom") - { - float baseFov = 40.0f; - if (jCamera.contains("baseFov")) - baseFov = jCamera["baseFov"].get(); - return finalize(make_smart_refctd_ptr(position, getTarget(), baseFov), CameraOrbitMoveScale, CameraDefaultRotateScale); - } - - error = "Unsupported camera type \"" + type + "\"."; - return false; - } + constexpr double CameraTinyScalarEpsilon = nbl::system::SCameraSmokeComparisonThresholds::TinyScalarEpsilon; + constexpr nbl::system::SCameraFollowRegressionThresholds CameraFollowRegressionThresholds = {}; } bool App::onAppInitialized(smart_refctd_ptr&& system) @@ -211,6 +73,9 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) return false; }; + if (!asset_base_t::onAppInitialized(std::move(system))) + return fail("Failed to initialize mounted resources for headless camera smoke."); + auto configPath = [&]() -> std::filesystem::path { if (program.is_used("--file")) @@ -220,109 +85,26 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) path = localInputCWD / path; return path.lexically_normal(); } - return (localInputCWD / "app_resources" / "cameras.json").lexically_normal(); + return std::filesystem::path(nbl::system::SCameraAppResourcePaths::DefaultCameraConfigRelativePath); }(); camera_json_t j; - { - std::ifstream file(configPath); - if (!file.is_open()) - return fail("Cannot open config \"" + configPath.string() + "\"."); - - try - { - file >> j; - } - catch (const std::exception& e) - { - return fail("JSON parse error: " + std::string(e.what())); - } - } - - if (!j.contains("cameras") || !j["cameras"].is_array()) - return fail("Missing \"cameras\" array in config."); + std::string jsonError; + if (!nbl::system::loadJsonFromPath(*m_system, configPath, j, &jsonError)) + return fail(jsonError); std::vector> cameras; - cameras.reserve(j["cameras"].size()); - for (const auto& jCamera : j["cameras"]) - { - smart_refctd_ptr camera; - std::string error; - if (!createCameraFromJson(jCamera, error, camera)) - return fail(error); - cameras.emplace_back(std::move(camera)); - } - - if (cameras.empty()) - return fail("No cameras defined."); + if (!nbl::system::tryLoadCameraCollectionFromJson(j, jsonError, cameras)) + return fail(jsonError); - auto angleDiffDeg = [](double a, double b) -> double + auto comparePresetToCameraDefault = [&](ICamera* camera, const CameraPreset& preset) -> bool { - double d = std::fmod(a - b + 180.0, 360.0); - if (d < 0.0) - d += 360.0; - return std::abs(d - 180.0); + return nbl::system::comparePresetToCameraStateWithDefaultThresholds(m_cameraGoalSolver, camera, preset); }; - auto isFinite3 = [](const auto& v) -> bool + auto comparePresetToCameraStrict = [&](ICamera* camera, const CameraPreset& preset) -> bool { - return std::isfinite(v.x) && std::isfinite(v.y) && std::isfinite(v.z); - }; - - auto nearlyEqual3 = [](const auto& a, const auto& b, const double epsilon) -> bool - { - return std::abs(static_cast(a.x - b.x)) <= epsilon && - std::abs(static_cast(a.y - b.y)) <= epsilon && - std::abs(static_cast(a.z - b.z)) <= epsilon; - }; - - auto computeDelta = [&](ICamera* camera, const float64_t3& beforePos, const float32_t3& beforeEulerDeg, double& outPosDelta, double& outRotDeltaDeg) -> bool - { - if (!camera) - return false; - const auto& gimbal = camera->getGimbal(); - const auto afterPos = gimbal.getPosition(); - const auto afterEuler = getCastedVector(getQuaternionEulerDegrees(gimbal.getOrientation())); - if (!isFinite3(afterPos) || !isFinite3(afterEuler)) - return false; - - const double dx = static_cast(afterPos.x - beforePos.x); - const double dy = static_cast(afterPos.y - beforePos.y); - const double dz = static_cast(afterPos.z - beforePos.z); - outPosDelta = std::sqrt(dx * dx + dy * dy + dz * dz); - outRotDeltaDeg = std::max({ - angleDiffDeg(afterEuler.x, beforeEulerDeg.x), - angleDiffDeg(afterEuler.y, beforeEulerDeg.y), - angleDiffDeg(afterEuler.z, beforeEulerDeg.z) - }); - return true; - }; - - auto manipulateAndMeasure = [&](ICamera* camera, const std::vector& events, double& outPosDelta, double& outRotDeltaDeg) -> bool - { - outPosDelta = 0.0; - outRotDeltaDeg = 0.0; - if (!camera || events.empty()) - return false; - - const auto& beforeGimbal = camera->getGimbal(); - const float64_t3 beforePos = beforeGimbal.getPosition(); - const float32_t3 beforeEulerDeg = getCastedVector(getQuaternionEulerDegrees(beforeGimbal.getOrientation())); - if (!isFinite3(beforePos) || !isFinite3(beforeEulerDeg)) - return false; - - if (!camera->manipulate({ events.data(), events.size() })) - return false; - - if (!computeDelta(camera, beforePos, beforeEulerDeg, outPosDelta, outRotDeltaDeg)) - return false; - - return outPosDelta > 1e-9 || outRotDeltaDeg > 1e-9; - }; - - auto comparePresetToCamera = [&](ICamera* camera, const CameraPreset& preset, const double posEps, const double rotEpsDeg, const double scalarEps) -> bool - { - return nbl::core::comparePresetToCameraState(m_cameraGoalSolver, camera, preset, posEps, rotEpsDeg, scalarEps); + return nbl::system::comparePresetToCameraStateWithStrictThresholds(m_cameraGoalSolver, camera, preset); }; auto describePresetMismatch = [&](ICamera* camera, const CameraPreset& preset) -> std::string @@ -408,7 +190,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) return fail(std::string("Follow visual metrics smoke was missing lock metrics for ") + label + "."); if (expectsProjectedMetrics && !metrics.projectedValid) return fail(std::string("Follow visual metrics smoke was missing projected metrics for ") + label + "."); - if (metrics.projectedValid && metrics.projectedNdcRadius > 0.03f) + if (metrics.projectedValid && metrics.projectedNdcRadius > CameraFollowRegressionThresholds.projectedNdcTolerance) return fail(std::string("Follow visual metrics smoke had projected center error for ") + label + "."); return true; }; @@ -450,7 +232,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) return fail("Scripted runtime frame batch smoke failed for frame 4."); } const auto trackedTargetPosition = float64_t3(batch.trackedTargetTransforms.front().transform[3]); - if (!nearlyEqual3(trackedTargetPosition, float64_t3(7.0, 8.0, 9.0), 1e-9)) + if (!hlsl::nearlyEqualVec3(trackedTargetPosition, float64_t3(7.0, 8.0, 9.0), CameraTinyScalarEpsilon)) return fail("Scripted runtime tracked-target payload smoke failed."); return true; @@ -530,7 +312,11 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) CCameraScriptedTimeline timeline = {}; nbl::system::appendScriptedBaselineCheck(timeline, 1u); nbl::system::appendScriptedGimbalStepCheck(timeline, 2u, true, 2.0f, 0.005f, true, 45.0f, 0.05f); - nbl::system::appendScriptedFollowTargetLockCheck(timeline, 3u, 0.1f, 0.03f); + nbl::system::appendScriptedFollowTargetLockCheck( + timeline, + 3u, + CameraFollowRegressionThresholds.lockAngleToleranceDeg, + CameraFollowRegressionThresholds.projectedNdcTolerance); CCameraScriptedCheckRuntimeState state = {}; { @@ -663,11 +449,8 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) followGoal, regression, ®ressionError, - 0.1f, - 1e-6, - 1e-9, hasViewProjMatrix ? &viewProjMatrix : nullptr, - 0.03f)) + CameraFollowRegressionThresholds)) { return fail(std::string("Follow smoke contract failed for ") + label + ". " + regressionError); } @@ -682,7 +465,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) const auto markerPosition = getCastedVector(float32_t3(markerTransform[3])); const auto positionDelta = markerPosition - trackedTarget.getGimbal().getPosition(); const auto errorLength = length(positionDelta); - if (!std::isfinite(errorLength) || errorLength > 1e-9) + if (!std::isfinite(errorLength) || errorLength > CameraTinyScalarEpsilon) { return fail(std::string("Follow target marker alignment smoke failed for ") + label + "."); } @@ -739,13 +522,13 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) ". " + describeApplyResult(recapturedApply)); } - if (!comparePresetToCamera(camera, reachedEditedPreset, 5e-6, 0.1, 5e-6)) + if (!nbl::core::comparePresetToCameraState(m_cameraGoalSolver, camera, reachedEditedPreset, 5e-6, nbl::system::SCameraSmokeComparisonThresholds::StrictAngularToleranceDeg, 5e-6)) return fail(std::string("Follow recapture smoke mismatch for ") + label + ". " + describePresetMismatch(camera, reachedEditedPreset)); if (!verifyFollowTargetContract(camera, trackedTarget, followConfig, recapturedGoal, label)) return false; const auto restoreResult = nbl::core::applyPresetDetailed(m_cameraGoalSolver, camera, baselinePreset); - if (!restoreResult.succeeded() || !comparePresetToCamera(camera, baselinePreset, 1e-6, 0.1, 1e-6)) + if (!restoreResult.succeeded() || !comparePresetToCameraStrict(camera, baselinePreset)) { return fail(std::string("Follow recapture smoke failed to restore baseline for ") + label + ". " + describeApplyResult(restoreResult) + " " + describePresetMismatch(camera, baselinePreset)); @@ -886,7 +669,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) return fail("Preset target apply smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\". " + describeApplyResult(shiftedResult)); ICamera::SphericalTargetState shiftedState; - if (!camera->tryGetSphericalTargetState(shiftedState) || !nearlyEqual3(shiftedState.target, shiftedPreset.goal.targetPosition, 1e-9)) + if (!camera->tryGetSphericalTargetState(shiftedState) || !hlsl::nearlyEqualVec3(shiftedState.target, shiftedPreset.goal.targetPosition, 1e-9)) return fail("Preset target writeback smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\"."); const auto restoredResult = nbl::core::applyPresetDetailed(m_cameraGoalSolver, camera, initialPreset); @@ -894,10 +677,10 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) return fail("Preset restore smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\". " + describeApplyResult(restoredResult)); ICamera::SphericalTargetState restoredState; - if (!camera->tryGetSphericalTargetState(restoredState) || !nearlyEqual3(restoredState.target, initialPreset.goal.targetPosition, 1e-9)) + if (!camera->tryGetSphericalTargetState(restoredState) || !hlsl::nearlyEqualVec3(restoredState.target, initialPreset.goal.targetPosition, 1e-9)) return fail("Preset target restore smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\"."); - if (!comparePresetToCamera(camera, initialPreset, 1e-6, 1e-4, 1e-9)) + if (!comparePresetToCameraDefault(camera, initialPreset)) return fail("Preset restore mismatch smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\". " + describePresetMismatch(camera, initialPreset)); } @@ -916,11 +699,11 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) std::max(0.1f, initialPreset.goal.dynamicPerspectiveState.referenceDistance + 1.25f); const auto shiftedResult = nbl::core::applyPresetDetailed(m_cameraGoalSolver, camera, shiftedPreset); - if (!shiftedResult.succeeded() || !shiftedResult.changed() || !comparePresetToCamera(camera, shiftedPreset, 1e-6, 0.1, 1e-6)) + if (!shiftedResult.succeeded() || !shiftedResult.changed() || !comparePresetToCameraStrict(camera, shiftedPreset)) return fail("Preset dynamic perspective apply smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\". " + describeApplyResult(shiftedResult) + " " + describePresetMismatch(camera, shiftedPreset)); const auto restoredResult = nbl::core::applyPresetDetailed(m_cameraGoalSolver, camera, initialPreset); - if (!restoredResult.succeeded() || !comparePresetToCamera(camera, initialPreset, 1e-6, 0.1, 1e-6)) + if (!restoredResult.succeeded() || !comparePresetToCameraStrict(camera, initialPreset)) return fail("Preset dynamic perspective restore smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\". " + describeApplyResult(restoredResult) + " " + describePresetMismatch(camera, initialPreset)); } @@ -960,35 +743,33 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) if (directEvents.empty()) return fail("No allowed virtual events for camera \"" + std::string(camera->getIdentifier()) + "\"."); - double directPosDelta = 0.0; - double directRotDelta = 0.0; - if (!manipulateAndMeasure(camera, directEvents, directPosDelta, directRotDelta)) + nbl::system::SCameraManipulationDelta directDelta = {}; + if (!nbl::system::tryManipulateCameraAndMeasureDelta(camera, { directEvents.data(), directEvents.size() }, directDelta, CameraTinyScalarEpsilon)) return fail("Direct manipulate smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\"."); { const auto modifiedPreset = nbl::core::capturePreset(m_cameraGoalSolver, camera, "smoke-direct"); const auto restoreInitial = nbl::core::applyPresetDetailed(m_cameraGoalSolver, camera, initialPreset); - if (!restoreInitial.succeeded() || !comparePresetToCamera(camera, initialPreset, 1e-3, 0.1, 1e-4)) + if (!restoreInitial.succeeded() || !nbl::core::comparePresetToCameraState(m_cameraGoalSolver, camera, initialPreset, 1e-3, nbl::system::SCameraSmokeComparisonThresholds::StrictAngularToleranceDeg, 1e-4)) return fail("Preset restore from direct smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\". " + describePresetMismatch(camera, initialPreset)); const auto applyModified = nbl::core::applyPresetDetailed(m_cameraGoalSolver, camera, modifiedPreset); - if (!applyModified.succeeded() || !applyModified.changed() || !comparePresetToCamera(camera, modifiedPreset, 1e-3, 0.1, 1e-4)) + if (!applyModified.succeeded() || !applyModified.changed() || !nbl::core::comparePresetToCameraState(m_cameraGoalSolver, camera, modifiedPreset, 1e-3, nbl::system::SCameraSmokeComparisonThresholds::StrictAngularToleranceDeg, 1e-4)) return fail("Preset apply from direct smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\". " + describePresetMismatch(camera, modifiedPreset)); const auto restoreAgain = nbl::core::applyPresetDetailed(m_cameraGoalSolver, camera, initialPreset); - if (!restoreAgain.succeeded() || !comparePresetToCamera(camera, initialPreset, 1e-3, 0.1, 1e-4)) + if (!restoreAgain.succeeded() || !nbl::core::comparePresetToCameraState(m_cameraGoalSolver, camera, initialPreset, 1e-3, nbl::system::SCameraSmokeComparisonThresholds::StrictAngularToleranceDeg, 1e-4)) return fail("Preset final restore smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\". " + describePresetMismatch(camera, initialPreset)); } bool keyboardOk = false; - double keyboardPosDelta = 0.0; - double keyboardRotDelta = 0.0; + nbl::system::SCameraManipulationDelta keyboardDelta = {}; for (const auto key : keyboardCandidates) { applyDefaultCameraInputBindingPreset(inputBinder, *camera); auto keyboardEvents = collectKeyboardVirtualEvents(inputBinder, key); if (keyboardEvents.empty()) continue; - if (manipulateAndMeasure(camera, keyboardEvents, keyboardPosDelta, keyboardRotDelta)) + if (nbl::system::tryManipulateCameraAndMeasureDelta(camera, { keyboardEvents.data(), keyboardEvents.size() }, keyboardDelta, CameraTinyScalarEpsilon)) { keyboardOk = true; break; @@ -1009,8 +790,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) mousePreset.find(ui::EMC_HORIZONTAL_POSITIVE_SCROLL) != mousePreset.end() || mousePreset.find(ui::EMC_HORIZONTAL_NEGATIVE_SCROLL) != mousePreset.end(); - double mouseMovePosDelta = 0.0; - double mouseMoveRotDelta = 0.0; + nbl::system::SCameraManipulationDelta mouseMoveDelta = {}; if (hasMoveMapping) { SMouseEvent moveEv(std::chrono::microseconds(16667)); @@ -1030,12 +810,11 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) auto mouseMoveEvents = collectMouseVirtualEvents(inputBinder, { filteredMoveLookDown.data(), filteredMoveLookDown.size() }); if (mouseMoveEvents.empty()) return fail("Mouse move virtual events missing for camera \"" + std::string(camera->getIdentifier()) + "\"."); - if (!manipulateAndMeasure(camera, mouseMoveEvents, mouseMovePosDelta, mouseMoveRotDelta)) + if (!nbl::system::tryManipulateCameraAndMeasureDelta(camera, { mouseMoveEvents.data(), mouseMoveEvents.size() }, mouseMoveDelta, CameraTinyScalarEpsilon)) return fail("Mouse move binding smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\"."); } - double mouseScrollPosDelta = 0.0; - double mouseScrollRotDelta = 0.0; + nbl::system::SCameraManipulationDelta mouseScrollDelta = {}; if (hasScrollMapping) { SMouseEvent scrollEv(std::chrono::microseconds(16667)); @@ -1050,19 +829,19 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) auto mouseScrollEvents = collectMouseVirtualEvents(inputBinder, { filteredScroll.data(), filteredScroll.size() }); if (mouseScrollEvents.empty()) return fail("Mouse scroll virtual events missing for camera \"" + std::string(camera->getIdentifier()) + "\"."); - if (!manipulateAndMeasure(camera, mouseScrollEvents, mouseScrollPosDelta, mouseScrollRotDelta)) + if (!nbl::system::tryManipulateCameraAndMeasureDelta(camera, { mouseScrollEvents.data(), mouseScrollEvents.size() }, mouseScrollDelta, CameraTinyScalarEpsilon)) return fail("Mouse scroll binding smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\"."); } std::cout << "[headless-camera-smoke][pass] " << camera->getIdentifier() - << " direct_pos_delta=" << directPosDelta - << " direct_rot_delta_deg=" << directRotDelta - << " kb_pos_delta=" << keyboardPosDelta - << " kb_rot_delta_deg=" << keyboardRotDelta - << " mouse_move_pos_delta=" << mouseMovePosDelta - << " mouse_move_rot_delta_deg=" << mouseMoveRotDelta - << " mouse_scroll_pos_delta=" << mouseScrollPosDelta - << " mouse_scroll_rot_delta_deg=" << mouseScrollRotDelta + << " 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; } @@ -1112,7 +891,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) return fail(std::string("Cross-kind preset smoke failed for ") + label + ". " + describeApplyResult(applyResult)); const auto restoreResult = nbl::core::applyPresetDetailed(m_cameraGoalSolver, targetCamera, baselinePreset); - if (!restoreResult.succeeded() || !comparePresetToCamera(targetCamera, baselinePreset, 1e-6, 0.1, 1e-6)) + if (!restoreResult.succeeded() || !comparePresetToCameraStrict(targetCamera, baselinePreset)) return fail(std::string("Cross-kind preset restore smoke failed for ") + label + ". " + describeApplyResult(restoreResult) + " " + describePresetMismatch(targetCamera, baselinePreset)); return true; @@ -1132,14 +911,14 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) const auto baselinePreset = nbl::core::capturePreset(m_cameraGoalSolver, targetCamera, std::string(label) + "-baseline"); const auto applyResult = nbl::core::applyPresetDetailed(m_cameraGoalSolver, targetCamera, sourcePreset); - if (!applyResult.succeeded() || !applyResult.exact || !comparePresetToCamera(targetCamera, sourcePreset, 1e-6, 0.1, 1e-6)) + if (!applyResult.succeeded() || !applyResult.exact || !comparePresetToCameraStrict(targetCamera, sourcePreset)) { return fail(std::string("Exact cross-kind preset smoke failed for ") + label + ". " + describeApplyResult(applyResult) + " " + describePresetMismatch(targetCamera, sourcePreset)); } const auto restoreResult = nbl::core::applyPresetDetailed(m_cameraGoalSolver, targetCamera, baselinePreset); - if (!restoreResult.succeeded() || !restoreResult.exact || !comparePresetToCamera(targetCamera, baselinePreset, 1e-6, 0.1, 1e-6)) + if (!restoreResult.succeeded() || !restoreResult.exact || !comparePresetToCameraStrict(targetCamera, baselinePreset)) { return fail(std::string("Exact cross-kind preset restore smoke failed for ") + label + ". " + describeApplyResult(restoreResult) + " " + describePresetMismatch(targetCamera, baselinePreset)); @@ -1179,7 +958,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) return false; const auto restoreResult = nbl::core::applyPresetDetailed(m_cameraGoalSolver, orbitCamera, baselinePreset); - if (!restoreResult.succeeded() || !comparePresetToCamera(orbitCamera, baselinePreset, 1e-6, 0.1, 1e-6)) + if (!restoreResult.succeeded() || !comparePresetToCameraStrict(orbitCamera, baselinePreset)) return fail("Orbit follow smoke failed to restore the baseline preset."); followConfig.mode = ECameraFollowMode::KeepWorldOffset; @@ -1193,7 +972,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) return false; const auto restoreWorldOffsetResult = nbl::core::applyPresetDetailed(m_cameraGoalSolver, orbitCamera, baselinePreset); - if (!restoreWorldOffsetResult.succeeded() || !comparePresetToCamera(orbitCamera, baselinePreset, 1e-6, 0.1, 1e-6)) + if (!restoreWorldOffsetResult.succeeded() || !comparePresetToCameraStrict(orbitCamera, baselinePreset)) return fail("Orbit keep-world-offset smoke failed to restore the baseline preset."); } @@ -1228,7 +1007,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) return false; const auto restoreResult = nbl::core::applyPresetDetailed(m_cameraGoalSolver, defaultFollowCamera, baselinePreset); - if (!restoreResult.succeeded() || !comparePresetToCamera(defaultFollowCamera, baselinePreset, 1e-6, 0.1, 1e-6)) + if (!restoreResult.succeeded() || !comparePresetToCameraStrict(defaultFollowCamera, baselinePreset)) return fail("Default follow smoke failed to restore the baseline preset for camera \"" + std::string(defaultFollowCamera->getIdentifier()) + "\"."); } @@ -1248,7 +1027,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) return false; const auto restoreResult = nbl::core::applyPresetDetailed(m_cameraGoalSolver, freeCamera, baselinePreset); - if (!restoreResult.succeeded() || !comparePresetToCamera(freeCamera, baselinePreset, 1e-6, 0.1, 1e-6)) + if (!restoreResult.succeeded() || !comparePresetToCameraStrict(freeCamera, baselinePreset)) return fail("Free follow smoke failed to restore the baseline preset."); followConfig.mode = ECameraFollowMode::KeepWorldOffset; @@ -1262,7 +1041,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) return false; const auto restoreWorldOffsetResult = nbl::core::applyPresetDetailed(m_cameraGoalSolver, freeCamera, baselinePreset); - if (!restoreWorldOffsetResult.succeeded() || !comparePresetToCamera(freeCamera, baselinePreset, 1e-6, 0.1, 1e-6)) + if (!restoreWorldOffsetResult.succeeded() || !comparePresetToCameraStrict(freeCamera, baselinePreset)) return fail("Free keep-world-offset smoke failed to restore the baseline preset."); } @@ -1286,7 +1065,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) return false; const auto restoreResult = nbl::core::applyPresetDetailed(m_cameraGoalSolver, chaseCamera, baselinePreset); - if (!restoreResult.succeeded() || !comparePresetToCamera(chaseCamera, baselinePreset, 1e-6, 0.1, 1e-6)) + if (!restoreResult.succeeded() || !comparePresetToCameraStrict(chaseCamera, baselinePreset)) return fail("Chase follow smoke failed to restore the baseline preset."); } @@ -1446,7 +1225,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) CCameraKeyframeTrack loadedTrack; if (!nbl::system::readKeyframeTrack(keyframeBuffer, loadedTrack)) return fail("Keyframe persistence smoke failed to deserialize track."); - if (!nbl::core::compareKeyframeTrackContent(sourceTrack, loadedTrack, 1e-6, 1e-6, 0.1, 1e-6)) + if (!nbl::system::compareKeyframeTrackContentWithStrictThresholds(sourceTrack, loadedTrack)) return fail("Keyframe persistence smoke changed stream track content."); struct TempFileCleanup final @@ -1486,7 +1265,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) CCameraKeyframeTrack fileLoadedTrack; if (!nbl::system::loadKeyframeTrackFromFile(keyframeFile, fileLoadedTrack)) return fail("Keyframe persistence smoke failed to load track file."); - if (!nbl::core::compareKeyframeTrackContent(sourceTrack, fileLoadedTrack, 1e-6, 1e-6, 0.1, 1e-6)) + if (!nbl::system::compareKeyframeTrackContentWithStrictThresholds(sourceTrack, fileLoadedTrack)) return fail("Keyframe persistence smoke changed file track content."); } @@ -1743,7 +1522,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) CameraPreset orientedPreset = initialFreePreset; orientedPreset.goal.orientation = makeQuaternionFromEulerDegrees(float64_t3(0.0, 90.0, 0.0)); const auto orientResult = nbl::core::applyPresetDetailed(m_cameraGoalSolver, freeCamera, orientedPreset); - if (!orientResult.succeeded() || !comparePresetToCamera(freeCamera, orientedPreset, 1e-6, 0.1, 1e-6)) + if (!orientResult.succeeded() || !comparePresetToCameraStrict(freeCamera, orientedPreset)) return fail("Camera manipulation utilities smoke failed to orient Free camera before translation remap."); std::vector worldTranslationEvents(3u); @@ -1764,7 +1543,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) const auto remappedPosition = freeCamera->getGimbal().getPosition(); const auto positionDelta = remappedPosition - orientedPreset.goal.position; const float64_t3 expectedWorldDelta(1.25, 0.5, 2.0); - if (!nearlyEqual3(positionDelta, expectedWorldDelta, 1e-6)) + if (!hlsl::nearlyEqualVec3(positionDelta, expectedWorldDelta, 1e-6)) return fail("Camera manipulation utilities smoke changed world-space translation semantics."); CameraPreset pitchPreset = initialFreePreset; @@ -1786,7 +1565,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) return fail("Camera manipulation utilities smoke produced wrong clamped Free camera pitch."); const auto restoreFree = nbl::core::applyPresetDetailed(m_cameraGoalSolver, freeCamera, initialFreePreset); - if (!restoreFree.succeeded() || !comparePresetToCamera(freeCamera, initialFreePreset, 1e-6, 0.1, 1e-6)) + if (!restoreFree.succeeded() || !comparePresetToCameraStrict(freeCamera, initialFreePreset)) return fail("Camera manipulation utilities smoke failed to restore Free camera baseline."); } @@ -1814,7 +1593,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) } const auto restoreOrbit = nbl::core::applyPresetDetailed(m_cameraGoalSolver, orbitCamera, initialOrbitPreset); - if (!restoreOrbit.succeeded() || !comparePresetToCamera(orbitCamera, initialOrbitPreset, 1e-6, 0.1, 1e-6)) + if (!restoreOrbit.succeeded() || !comparePresetToCameraStrict(orbitCamera, initialOrbitPreset)) return fail("Camera manipulation utilities smoke failed to restore Orbit baseline."); } @@ -1878,85 +1657,46 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) m_inputSystem = make_smart_refctd_ptr(logger_opt_smart_ptr(smart_refctd_ptr(m_logger))); m_logFormatter = core::make_smart_refctd_ptr(); - // Remember to call the base class initialization! - if (!base_t::onAppInitialized(std::move(system))) + if (!base_t::onAppInitialized(smart_refctd_ptr(system))) + return false; + if (!asset_base_t::onAppInitialized(std::move(system))) return false; { - smart_refctd_ptr examplesHeaderArch, examplesSourceArch, examplesBuildArch, thisExampleArch, thisExampleBuildArch; -#ifdef NBL_EMBED_BUILTIN_RESOURCES - examplesHeaderArch = core::make_smart_refctd_ptr(smart_refctd_ptr(m_logger)); - examplesSourceArch = core::make_smart_refctd_ptr(smart_refctd_ptr(m_logger)); - examplesBuildArch = core::make_smart_refctd_ptr(smart_refctd_ptr(m_logger)); - - #ifdef _NBL_THIS_EXAMPLE_BUILTIN_C_ARCHIVE_H_ - thisExampleArch = make_smart_refctd_ptr(smart_refctd_ptr(m_logger)); - #endif - - #ifdef _NBL_THIS_EXAMPLE_BUILTIN_BUILD_C_ARCHIVE_H_ - thisExampleBuildArch = make_smart_refctd_ptr(smart_refctd_ptr(m_logger)); - #endif -#else - examplesHeaderArch = make_smart_refctd_ptr(localInputCWD/"../common/include/nbl/examples", smart_refctd_ptr(m_logger), m_system.get()); - examplesSourceArch = make_smart_refctd_ptr(localInputCWD/"../common/src/nbl/examples", smart_refctd_ptr(m_logger), m_system.get()); - examplesBuildArch = make_smart_refctd_ptr(NBL_EXAMPLES_BUILD_MOUNT_POINT, smart_refctd_ptr(m_logger), m_system.get()); - thisExampleArch = make_smart_refctd_ptr(localInputCWD/"app_resources", smart_refctd_ptr(m_logger), m_system.get()); - #ifdef NBL_THIS_EXAMPLE_BUILD_MOUNT_POINT - thisExampleBuildArch = make_smart_refctd_ptr(NBL_THIS_EXAMPLE_BUILD_MOUNT_POINT, smart_refctd_ptr(m_logger), m_system.get()); - #endif -#endif - m_system->mount(std::move(examplesHeaderArch),"nbl/examples"); - m_system->mount(std::move(examplesSourceArch),"nbl/examples"); - m_system->mount(std::move(examplesBuildArch),"nbl/examples"); - if (thisExampleArch) - m_system->mount(std::move(thisExampleArch),"app_resources"); - if (thisExampleBuildArch) - m_system->mount(std::move(thisExampleBuildArch),"app_resources"); - } - - { - const std::optional cameraJsonFile = program.is_used("--file") ? program.get("--file") : std::optional(std::nullopt); + const std::optional cameraJsonFile = program.is_used("--file") ? std::optional(program.get("--file")) : std::optional(std::nullopt); camera_json_t j; auto loadDefaultConfig = [&]() -> bool { -#ifdef _NBL_THIS_EXAMPLE_BUILTIN_C_ARCHIVE_H_ - auto assets = make_smart_refctd_ptr(smart_refctd_ptr(m_logger)); - auto pFile = assets->getFile("cameras.json", IFile::ECF_READ, ""); - if (!pFile) - return logFail("Could not open builtin cameras.json!"); - - string config; - IFile::success_t result; - config.resize(pFile->getSize()); - pFile->read(result, config.data(), 0, pFile->getSize()); - j = camera_json_t::parse(config); - return true; -#else - const auto fallbackPath = localInputCWD / "app_resources" / "cameras.json"; - std::ifstream fallbackFile(fallbackPath); - if (!fallbackFile.is_open()) - return logFail("Cannot open default config \"%s\".", fallbackPath.string().c_str()); - fallbackFile >> j; + const auto configPath = std::filesystem::path(nbl::system::SCameraAppResourcePaths::DefaultCameraConfigRelativePath); + std::string jsonError; + if (!nbl::system::loadJsonFromPath(*m_system, configPath, j, &jsonError)) + return logFail("%s", jsonError.c_str()); return true; -#endif }; - auto file = cameraJsonFile.has_value() ? std::ifstream(cameraJsonFile.value()) : std::ifstream(); - if (!file.is_open()) + std::string jsonError; + std::filesystem::path resolvedCameraJsonFile; + const bool hasUserConfig = cameraJsonFile.has_value(); + if (hasUserConfig) + { + resolvedCameraJsonFile = nbl::system::resolveInputPath(localInputCWD, cameraJsonFile.value()); + } + + if (hasUserConfig && nbl::system::loadJsonFromPath(*m_system, resolvedCameraJsonFile, j, &jsonError)) { - if (cameraJsonFile.has_value()) - m_logger->log("Cannot open input \"%s\" json file. Switching to default config.", ILogger::ELL_WARNING, cameraJsonFile.value().c_str()); + // Loaded from explicit user path. + } + else + { + if (hasUserConfig) + m_logger->log("Cannot open input \"%s\" json file (%s). Switching to default config.", ILogger::ELL_WARNING, resolvedCameraJsonFile.string().c_str(), jsonError.c_str()); else m_logger->log("No input json file provided. Switching to default config.", ILogger::ELL_INFO); if (!loadDefaultConfig()) return false; } - else - { - file >> j; - } std::optional pendingScriptedSequence; bool scriptedInputParseFailed = false; @@ -2043,9 +1783,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) if (program.is_used("--script")) { - nbl::system::path scriptPath = program.get("--script"); - if (scriptPath.is_relative()) - scriptPath = localInputCWD / scriptPath; + nbl::system::path scriptPath = nbl::system::resolveInputPath(localInputCWD, program.get("--script")); nbl::system::CCameraScriptedInputParseResult parsed; if (!nbl::system::loadCameraScriptedInputFromFile(scriptPath, parsed, &scriptedInputParseError)) { @@ -2071,30 +1809,11 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) } std::vector> cameras; - for (const auto& jCamera : j["cameras"]) + std::string cameraConfigError; + if (!nbl::system::tryLoadCameraCollectionFromJson(j, cameraConfigError, cameras)) { - if (jCamera.contains("type")) - { - if (!jCamera.contains("position")) - { - logFail("Expected \"position\" keyword for camera definition!"); - return false; - } - - smart_refctd_ptr camera; - std::string error; - if (!createCameraFromJson(jCamera, error, camera)) - { - logFail("%s", error.c_str()); - return false; - } - cameras.emplace_back(std::move(camera)); - } - else - { - logFail("Expected \"type\" keyword for camera definition!"); - return false; - } + logFail("%s", cameraConfigError.c_str()); + return false; } std::vector projections; @@ -2457,9 +2176,6 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) } } - // Create asset manager - m_assetManager = make_smart_refctd_ptr(smart_refctd_ptr(m_system)); - // First create the resources that don't depend on a swapchain m_semaphore = m_device->createSemaphore(m_realFrameIx); if (!m_semaphore) @@ -2601,112 +2317,21 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) // UI { { - constexpr std::array ImGuiStreamingBufferSizes = { - 32ull * 1024ull * 1024ull, - 16ull * 1024ull * 1024ull, - 8ull * 1024ull * 1024ull, - 4ull * 1024ull * 1024ull, - 2ull * 1024ull * 1024ull - }; - auto createImGuiStreamingBuffer = [&](size_t size) -> smart_refctd_ptr - { - constexpr uint32_t minStreamingBufferAllocationSize = 128u; - constexpr uint32_t maxStreamingBufferAllocationAlignment = 4096u; - - auto getRequiredAccessFlags = [&](const bitflag& properties) - { - bitflag flags(IDeviceMemoryAllocation::EMCAF_NO_MAPPING_ACCESS); - - if (properties.hasFlags(IDeviceMemoryAllocation::EMPF_HOST_READABLE_BIT)) - flags |= IDeviceMemoryAllocation::EMCAF_READ; - if (properties.hasFlags(IDeviceMemoryAllocation::EMPF_HOST_WRITABLE_BIT)) - flags |= IDeviceMemoryAllocation::EMCAF_WRITE; - - return flags; - }; - - IGPUBuffer::SCreationParams mdiCreationParams = {}; - mdiCreationParams.usage = nbl::ext::imgui::UI::SCachedCreationParams::RequiredUsageFlags; - mdiCreationParams.size = size; - - auto buffer = m_utils->getLogicalDevice()->createBuffer(std::move(mdiCreationParams)); - if (!buffer) - { - m_logger->log("Failed to create ImGui streaming buffer object for size=%zu.", ILogger::ELL_WARNING, size); - return nullptr; - } - - buffer->setObjectDebugName("ImGui MDI Upstream Buffer"); - - auto memoryReqs = buffer->getMemoryReqs(); - const auto upStreamingBits = m_utils->getLogicalDevice()->getPhysicalDevice()->getUpStreamingMemoryTypeBits(); - const auto reqMemoryTypeBits = memoryReqs.memoryTypeBits; - memoryReqs.memoryTypeBits &= upStreamingBits; - if (!memoryReqs.memoryTypeBits) - { - m_logger->log("No compatible up-streaming memory type for ImGui buffer size=%zu reqBits=0x%08x upBits=0x%08x.", ILogger::ELL_WARNING, size, reqMemoryTypeBits, upStreamingBits); - return nullptr; - } - - auto allocation = m_utils->getLogicalDevice()->allocate(memoryReqs, buffer.get(), nbl::ext::imgui::UI::SCachedCreationParams::RequiredAllocateFlags); - if (!allocation.isValid()) - { - m_logger->log("Failed to allocate ImGui streaming buffer memory for size=%zu reqBits=0x%08x upBits=0x%08x filteredBits=0x%08x sizeReq=%llu.", ILogger::ELL_WARNING, size, reqMemoryTypeBits, upStreamingBits, memoryReqs.memoryTypeBits, memoryReqs.size); - return nullptr; - } - - auto memory = allocation.memory; - - if (!memory->map({ 0ull, memoryReqs.size }, getRequiredAccessFlags(memory->getMemoryPropertyFlags()))) - { - m_logger->log("Could not map ImGui streaming buffer memory for size=%zu.", ILogger::ELL_WARNING, size); - return nullptr; - } - - return make_smart_refctd_ptr( - SBufferRange{0ull, mdiCreationParams.size, std::move(buffer)}, - maxStreamingBufferAllocationAlignment, - minStreamingBufferAllocationSize); - }; - - smart_refctd_ptr imguiStreamingBuffer = nullptr; - for (const auto candidateSize : ImGuiStreamingBufferSizes) - { - imguiStreamingBuffer = createImGuiStreamingBuffer(candidateSize); - if (imguiStreamingBuffer) - break; - } - if (!imguiStreamingBuffer) - return logFail("Failed to create ImGui streaming buffer."); - nbl::ext::imgui::UI::SCreationParameters params; params.resources.texturesInfo = { .setIx = 0u, .bindingIx = 0u }; params.resources.samplersInfo = { .setIx = 0u, .bindingIx = 1u }; - params.assetManager = m_assetManager; + 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.streamingBuffer = std::move(imguiStreamingBuffer); params.subpassIx = 0u; params.transfer = getTransferUpQueue(); params.utilities = m_utils; - auto loadPrecompiledShader = [&](const std::string_view key) -> smart_refctd_ptr - { - IAssetLoader::SAssetLoadParams loadParams = {}; - loadParams.logger = m_logger.get(); - loadParams.workingDirectory = "app_resources"; - auto bundle = m_assetManager->getAsset(key.data(), loadParams); - const auto& contents = bundle.getContents(); - if (contents.empty()) - return nullptr; - return IAsset::castDown(contents[0]); - }; - const auto vertexKey = nbl::this_example::builtin::build::get_spirv_key<"imgui_vertex">(m_device.get()); const auto fragmentKey = nbl::this_example::builtin::build::get_spirv_key<"imgui_fragment">(m_device.get()); - auto vertexShader = loadPrecompiledShader(vertexKey.data()); - auto fragmentShader = loadPrecompiledShader(fragmentKey.data()); + auto vertexShader = nbl::system::loadPrecompiledShaderFromAppResources(*m_assetMgr, m_logger.get(), vertexKey); + auto fragmentShader = nbl::system::loadPrecompiledShaderFromAppResources(*m_assetMgr, m_logger.get(), fragmentKey); if (!vertexShader || !fragmentShader) return logFail("Failed to load precompiled ImGui shaders."); @@ -2850,30 +2475,10 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) } { - constexpr std::string_view SpaceEnvBlobCandidates[] = { - "rich_blue_nebulae_1_8k.rgba16f.envblob" - }; - - SpaceEnvBlobHeader envBlobHeader = {}; + nbl::system::SSpaceEnvBlobHeader envBlobHeader = {}; std::vector envBlobPayload; - const std::array SpaceEnvSearchRoots = { - (localInputCWD / ".." / "media" / "envmap").lexically_normal(), - (localInputCWD / ".." / "media").lexically_normal(), - localInputCWD / "app_resources" - }; - for (const auto candidate : SpaceEnvBlobCandidates) - { - for (const auto& root : SpaceEnvSearchRoots) - { - const auto candidatePath = root / candidate; - if (loadSpaceEnvBlob(candidatePath, envBlobHeader, envBlobPayload)) - { - break; - } - } - if (!envBlobPayload.empty()) - break; - } + const auto spaceEnvSearchRoots = nbl::system::makeSpaceEnvSearchRoots(localInputCWD); + nbl::system::loadFirstSpaceEnvBlobFromRoots(*m_system, spaceEnvSearchRoots, envBlobHeader, envBlobPayload); if (envBlobPayload.empty()) return logFail("Failed to load space environment blob from available assets."); @@ -3050,23 +2655,12 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) if (!pipelineLayout) return logFail("Failed to create space environment pipeline layout."); - auto loadPrecompiledShader = [&](const std::string_view key) -> smart_refctd_ptr - { - IAssetLoader::SAssetLoadParams loadParams = {}; - loadParams.logger = m_logger.get(); - loadParams.workingDirectory = "app_resources"; - auto bundle = m_assetManager->getAsset(key.data(), loadParams); - const auto& contents = bundle.getContents(); - if (contents.empty()) - return nullptr; - return IAsset::castDown(contents[0]); - }; const auto spaceFragKey = nbl::this_example::builtin::build::get_spirv_key<"sky_env_fragment">(m_device.get()); - auto fragmentShader = loadPrecompiledShader(spaceFragKey.data()); + auto fragmentShader = nbl::system::loadPrecompiledShaderFromAppResources(*m_assetMgr, m_logger.get(), spaceFragKey); if (!fragmentShader) return logFail("Failed to load space environment fragment shader."); - nbl::ext::FullScreenTriangle::ProtoPipeline fsTriProto(m_assetManager.get(), m_device.get(), m_logger.get()); + nbl::ext::FullScreenTriangle::ProtoPipeline fsTriProto(m_assetMgr.get(), m_device.get(), m_logger.get()); if (!fsTriProto) return logFail("Failed to create FullScreenTriangle prototype pipeline."); @@ -3104,7 +2698,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) const auto& geometries = m_scene->getInitParams().geometries; if (geometries.empty()) return logFail("No geometries found for scene!"); - m_renderer = CSimpleDebugRenderer::create(m_assetManager.get(), m_sceneRenderpass.get(), 0, { &geometries.front().get(), geometries.size() }); + m_renderer = CSimpleDebugRenderer::create(m_assetMgr.get(), m_sceneRenderpass.get(), 0, { &geometries.front().get(), geometries.size() }); if (!m_renderer) return logFail("Failed to create debug renderer!"); { @@ -3116,7 +2710,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) ext::frustum::CDrawFrustum::SCreationParameters frustumParams = {}; frustumParams.transfer = getTransferUpQueue(); - frustumParams.assetManager = m_assetManager; + frustumParams.assetManager = m_assetMgr; frustumParams.drawMode = ext::frustum::CDrawFrustum::DrawMode::DM_SINGLE; frustumParams.singlePipelineLayout = ext::frustum::CDrawFrustum::createPipelineLayoutFromPCRange(m_device.get(), singlePcRange); frustumParams.renderpass = core::smart_refctd_ptr(m_sceneRenderpass); diff --git a/61_UI/AppTransformEditor.cpp b/61_UI/AppTransformEditor.cpp index 235368446..2d4a40830 100644 --- a/61_UI/AppTransformEditor.cpp +++ b/61_UI/AppTransformEditor.cpp @@ -46,24 +46,9 @@ void App::TransformEditorContents() if (ImGui::IsItemHovered()) { - ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.2f, 0.2f, 0.2f, 0.8f)); - ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); - ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.5f); - - ImVec2 mousePos = ImGui::GetMousePos(); - ImGui::SetNextWindowPos(ImVec2(mousePos.x + 10, mousePos.y + 10), ImGuiCond_Always); - - ImGui::Begin("HoverOverlay", nullptr, - ImGuiWindowFlags_NoDecoration | - ImGuiWindowFlags_AlwaysAutoResize | - ImGuiWindowFlags_NoSavedSettings); - + nbl::ui::beginHoverInfoOverlay("HoverOverlay", ImGui::GetMousePos()); ImGui::Text("Right-click and drag on the gizmo to manipulate the object."); - - ImGui::End(); - - ImGui::PopStyleVar(); - ImGui::PopStyleColor(2); + nbl::ui::endHoverInfoOverlay(); } } diff --git a/61_UI/AppWorkLoop.cpp b/61_UI/AppWorkLoop.cpp index bbcae0934..dc1d7fce7 100644 --- a/61_UI/AppWorkLoop.cpp +++ b/61_UI/AppWorkLoop.cpp @@ -44,7 +44,7 @@ void App::workLoopBody() willSubmit &= cmdbuf->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT); willSubmit &= cmdbuf->beginDebugMarker("UIApp Frame"); - auto renderScene = [&](windowControlBinding& binding, const uint32_t bindingIx) + auto renderScene = [&](SWindowControlBinding& binding, const uint32_t bindingIx) { if (!binding.sceneFramebuffer) return; @@ -331,7 +331,7 @@ void App::workLoopBody() const uint64_t renderedFrameIx = m_realFrameIx - 1u; auto captureScreenshot = [&](const nbl::system::path& outPath, const char* tag) -> void { - if (!m_device || !m_assetManager || !m_surface) + if (!m_device || !m_assetMgr || !m_surface) return; m_logger->log("%s screenshot capture start (frame %llu).", ILogger::ELL_INFO, tag, static_cast(renderedFrameIx)); @@ -372,7 +372,7 @@ void App::workLoopBody() getGraphicsQueue(), nullptr, frameView.get(), - m_assetManager.get(), + m_assetMgr.get(), outPath, asset::IImage::LAYOUT::TRANSFER_SRC_OPTIMAL, asset::ACCESS_FLAGS::COLOR_ATTACHMENT_WRITE_BIT); diff --git a/61_UI/include/app/App.hpp b/61_UI/include/app/App.hpp index 77998a254..f19ddf1ba 100644 --- a/61_UI/include/app/App.hpp +++ b/61_UI/include/app/App.hpp @@ -11,7 +11,6 @@ #include #include #include -#include #include #include #include @@ -27,12 +26,6 @@ #include "nbl/ext/FullScreenTriangle/FullScreenTriangle.h" #include "nbl/ext/ScreenShot/ScreenShot.h" #include "nbl/this_example/builtin/build/spirv/keys.hpp" -#if __has_include("nbl/this_example/builtin/CArchive.h") -#include "nbl/this_example/builtin/CArchive.h" -#endif -#if __has_include("nbl/this_example/builtin/build/CArchive.h") -#include "nbl/this_example/builtin/build/CArchive.h" -#endif class CUIEventCallback : public nbl::video::ISmoothResizeSurface::ICallback { @@ -247,9 +240,10 @@ static smart_refctd_ptr createSceneFramebuffer(ILogicalDevice* return device->createFramebuffer(std::move(params)); } -class App final : public examples::SimpleWindowedApplication +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; constexpr static inline clock_t::duration DisplayImageDuration = std::chrono::milliseconds(900); @@ -860,331 +854,7 @@ class App final : public examples::SimpleWindowedApplication orthoBinding.boundProjectionIx = orthoBinding.lastBoundOrthoPresetProjectionIx.value(); } - inline bool projectWorldPointToViewport( - const float32_t4x4& viewProjMatrix, - const float32_t3& worldPoint, - const ImVec2& viewportPos, - const ImVec2& viewportSize, - ImVec2& outScreen) const - { - if (viewportSize.x <= 1.0f || viewportSize.y <= 1.0f) - return false; - - const auto clip = mul(viewProjMatrix, float32_t4(worldPoint.x, worldPoint.y, worldPoint.z, 1.0f)); - if (!std::isfinite(clip.x) || !std::isfinite(clip.y) || !std::isfinite(clip.z) || !std::isfinite(clip.w)) - return false; - - const float absW = std::abs(clip.w); - if (absW < 1e-5f) - return false; - - const float invW = 1.0f / clip.w; - const float ndcX = clip.x * invW; - const float ndcY = clip.y * invW; - const float ndcZ = clip.z * invW; - - if (!std::isfinite(ndcX) || !std::isfinite(ndcY) || !std::isfinite(ndcZ)) - return false; - if (std::abs(ndcX) > 100.0f || std::abs(ndcY) > 100.0f || std::abs(ndcZ) > 100.0f) - return false; - - outScreen.x = viewportPos.x + (ndcX * 0.5f + 0.5f) * viewportSize.x; - outScreen.y = viewportPos.y + (-ndcY * 0.5f + 0.5f) * viewportSize.y; - return std::isfinite(outScreen.x) && std::isfinite(outScreen.y); - } - - inline void drawFollowTargetViewportOverlay( - const float32_t4x4& viewProjMatrix, - const ImVec2& viewportPos, - const ImVec2& viewportSize) const - { - if (!(m_scriptedInput.enabled && m_scriptedInput.visualDebug && m_scriptedInput.visualFollowActive)) - return; - if (viewportSize.x <= 1.0f || viewportSize.y <= 1.0f) - return; - - float ndcX = 0.0f; - float ndcY = 0.0f; - float ndcRadius = 0.0f; - if (!nbl::system::tryComputeProjectedFollowTargetMetrics(viewProjMatrix, m_followTarget, ndcX, ndcY, &ndcRadius)) - return; - - auto* drawList = ImGui::GetWindowDrawList(); - if (!drawList) - return; - - const ImVec2 center( - viewportPos.x + viewportSize.x * 0.5f, - viewportPos.y + viewportSize.y * 0.5f); - const ImVec2 target( - viewportPos.x + (ndcX * 0.5f + 0.5f) * viewportSize.x, - viewportPos.y + (-ndcY * 0.5f + 0.5f) * viewportSize.y); - - const bool centered = ndcRadius <= 0.03f; - const ImU32 centerColor = IM_COL32(255, 170, 72, 235); - const ImU32 targetColor = centered ? IM_COL32(64, 255, 164, 245) : IM_COL32(90, 220, 255, 245); - const ImU32 targetFillColor = centered ? IM_COL32(24, 120, 76, 120) : IM_COL32(20, 92, 124, 120); - const ImU32 lineColor = centered ? IM_COL32(96, 255, 186, 200) : IM_COL32(120, 220, 255, 200); - const float centerRadius = 16.0f; - const float targetRadius = centered ? 18.0f : 14.0f; - - drawList->AddCircle(center, centerRadius, centerColor, 32, 2.5f); - drawList->AddLine(ImVec2(center.x - 22.0f, center.y), ImVec2(center.x + 22.0f, center.y), centerColor, 2.0f); - drawList->AddLine(ImVec2(center.x, center.y - 22.0f), ImVec2(center.x, center.y + 22.0f), centerColor, 2.0f); - - drawList->AddLine(center, target, lineColor, 2.0f); - drawList->AddCircleFilled(target, targetRadius, targetFillColor, 24); - drawList->AddCircle(target, targetRadius, targetColor, 32, 2.5f); - drawList->AddLine(ImVec2(target.x - 14.0f, target.y), ImVec2(target.x + 14.0f, target.y), targetColor, 2.0f); - drawList->AddLine(ImVec2(target.x, target.y - 14.0f), ImVec2(target.x, target.y + 14.0f), targetColor, 2.0f); - - drawList->AddText(ImVec2(target.x + 16.0f, target.y - 28.0f), targetColor, "FOLLOW TARGET"); - } - - inline void drawWorldReferenceOverlay( - const ImVec2& viewportPos, - const ImVec2& viewportSize, - const float32_t4x4& viewMatrix, - const float32_t4x4& projectionMatrix, - bool leftHandedProjection, - float nearPlane, - float farPlane) - { - if (!(m_scriptedInput.enabled && m_scriptedInput.visualDebug)) - return; - if (viewportSize.x <= 1.0f || viewportSize.y <= 1.0f) - return; - - auto* drawList = ImGui::GetWindowDrawList(); - if (!drawList) - return; - - const float safeNear = std::max(nearPlane, 0.001f); - const float safeFar = std::max(farPlane, safeNear + 0.001f); - const auto depthOfViewPoint = [&](const float32_t4& viewPoint) -> float - { - return leftHandedProjection ? viewPoint.z : -viewPoint.z; - }; - const auto ndcToViewport = [&](const ImVec2& ndc) -> ImVec2 - { - return ImVec2( - viewportPos.x + (ndc.x * 0.5f + 0.5f) * viewportSize.x, - viewportPos.y + (-ndc.y * 0.5f + 0.5f) * viewportSize.y); - }; - const auto clipSegmentByDepthRange = [&](float32_t4& viewA, float32_t4& viewB) -> bool - { - const float32_t4 a0 = viewA; - const float32_t4 b0 = viewB; - const float32_t4 delta = b0 - a0; - const float depthA = depthOfViewPoint(a0); - const float depthB = depthOfViewPoint(b0); - - float tEnter = 0.0f; - float tExit = 1.0f; - const auto clipByConstraint = [&](float fa, float fb) -> bool - { - if (fa < 0.0f && fb < 0.0f) - return false; - if (fa >= 0.0f && fb >= 0.0f) - return true; - - const float denom = fa - fb; - if (std::abs(denom) < 1e-6f) - return false; - const float t = std::clamp(fa / denom, 0.0f, 1.0f); - - if (fa < 0.0f) - tEnter = std::max(tEnter, t); - else - tExit = std::min(tExit, t); - - return tEnter <= tExit; - }; - - if (!clipByConstraint(depthA - safeNear, depthB - safeNear)) - return false; - if (!clipByConstraint(safeFar - depthA, safeFar - depthB)) - return false; - - viewA = a0 + delta * tEnter; - viewB = a0 + delta * tExit; - return true; - }; - const auto projectViewPointToNdc = [&](const float32_t4& viewPoint, ImVec2& outNdc) -> bool - { - const auto clip = mul(projectionMatrix, viewPoint); - if (!std::isfinite(clip.x) || !std::isfinite(clip.y) || !std::isfinite(clip.z) || !std::isfinite(clip.w)) - return false; - - const float absW = std::abs(clip.w); - if (absW < 1e-6f) - return false; - - const float invW = 1.0f / clip.w; - const float ndcX = clip.x * invW; - const float ndcY = clip.y * invW; - const float ndcZ = clip.z * invW; - if (!std::isfinite(ndcX) || !std::isfinite(ndcY) || !std::isfinite(ndcZ)) - return false; - if (std::abs(ndcX) > 1e4f || std::abs(ndcY) > 1e4f || std::abs(ndcZ) > 1e4f) - return false; - - outNdc = ImVec2(ndcX, ndcY); - return true; - }; - const auto clipNdcSegmentToViewport = [&](ImVec2& ndcA, ImVec2& ndcB) -> bool - { - float tEnter = 0.0f; - float tExit = 1.0f; - const float dx = ndcB.x - ndcA.x; - const float dy = ndcB.y - ndcA.y; - const auto clipTest = [&](float p, float q) -> bool - { - if (std::abs(p) < 1e-6f) - return q >= 0.0f; - - const float r = q / p; - if (p < 0.0f) - { - if (r > tExit) - return false; - tEnter = std::max(tEnter, r); - } - else - { - if (r < tEnter) - return false; - tExit = std::min(tExit, r); - } - return tEnter <= tExit; - }; - - if (!clipTest(-dx, ndcA.x + 1.0f)) - return false; - if (!clipTest(dx, 1.0f - ndcA.x)) - return false; - if (!clipTest(-dy, ndcA.y + 1.0f)) - return false; - if (!clipTest(dy, 1.0f - ndcA.y)) - return false; - - const ImVec2 a0 = ndcA; - ndcA = ImVec2(a0.x + dx * tEnter, a0.y + dy * tEnter); - ndcB = ImVec2(a0.x + dx * tExit, a0.y + dy * tExit); - return true; - }; - const auto projectWorldPointToViewportClipped = [&](const float32_t3& worldPoint, ImVec2& outScreen) -> bool - { - const auto viewPoint = mul(viewMatrix, float32_t4(worldPoint.x, worldPoint.y, worldPoint.z, 1.0f)); - if (!std::isfinite(viewPoint.x) || !std::isfinite(viewPoint.y) || !std::isfinite(viewPoint.z) || !std::isfinite(viewPoint.w)) - return false; - - const float depth = depthOfViewPoint(viewPoint); - if (depth < safeNear || depth > safeFar) - return false; - - ImVec2 ndcPoint = {}; - if (!projectViewPointToNdc(viewPoint, ndcPoint)) - return false; - if (ndcPoint.x < -1.0f || ndcPoint.x > 1.0f || ndcPoint.y < -1.0f || ndcPoint.y > 1.0f) - return false; - - outScreen = ndcToViewport(ndcPoint); - return std::isfinite(outScreen.x) && std::isfinite(outScreen.y); - }; - - const auto drawProjectedSegment = [&](const float32_t3& aWorld, const float32_t3& bWorld, ImU32 color, float thickness) -> void - { - float32_t4 viewA = mul(viewMatrix, float32_t4(aWorld.x, aWorld.y, aWorld.z, 1.0f)); - float32_t4 viewB = mul(viewMatrix, float32_t4(bWorld.x, bWorld.y, bWorld.z, 1.0f)); - if (!std::isfinite(viewA.x) || !std::isfinite(viewA.y) || !std::isfinite(viewA.z) || !std::isfinite(viewA.w) || - !std::isfinite(viewB.x) || !std::isfinite(viewB.y) || !std::isfinite(viewB.z) || !std::isfinite(viewB.w)) - return; - if (!clipSegmentByDepthRange(viewA, viewB)) - return; - - ImVec2 ndcA = {}; - ImVec2 ndcB = {}; - if (!projectViewPointToNdc(viewA, ndcA)) - return; - if (!projectViewPointToNdc(viewB, ndcB)) - return; - if (!clipNdcSegmentToViewport(ndcA, ndcB)) - return; - - const ImVec2 screenA = ndcToViewport(ndcA); - const ImVec2 screenB = ndcToViewport(ndcB); - drawList->AddLine(screenA, screenB, color, thickness); - }; - - auto drawWorldLine = [&](const float32_t3& aWorld, const float32_t3& bWorld, ImU32 color, float thickness) -> void - { - drawProjectedSegment(aWorld, bWorld, color, thickness); - }; - - const float32_t3 origin = float32_t3(0.0f); - ImVec2 originScreen = {}; - if (!projectWorldPointToViewportClipped(origin, originScreen)) - return; - - constexpr float axisLength = 5.0f; - const float32_t3 xPos = float32_t3(axisLength, 0.0f, 0.0f); - const float32_t3 yPos = float32_t3(0.0f, axisLength, 0.0f); - const float32_t3 zPos = float32_t3(0.0f, 0.0f, axisLength); - const float32_t3 xNeg = float32_t3(-axisLength * 0.4f, 0.0f, 0.0f); - const float32_t3 yNeg = float32_t3(0.0f, -axisLength * 0.3f, 0.0f); - const float32_t3 zNeg = float32_t3(0.0f, 0.0f, -axisLength * 0.4f); - - drawWorldLine(origin, xPos, IM_COL32(244, 92, 92, 245), 2.8f); - drawWorldLine(origin, yPos, IM_COL32(124, 236, 132, 245), 2.8f); - drawWorldLine(origin, zPos, IM_COL32(106, 166, 255, 245), 2.8f); - drawWorldLine(origin, xNeg, IM_COL32(128, 74, 74, 170), 1.4f); - drawWorldLine(origin, yNeg, IM_COL32(74, 128, 78, 170), 1.4f); - drawWorldLine(origin, zNeg, IM_COL32(70, 88, 124, 170), 1.4f); - - const auto drawAxisArrowHead = [&](const float32_t3& tipWorld, const float32_t3& tailWorld, ImU32 color) -> void - { - ImVec2 tipScreen = {}; - ImVec2 tailScreen = {}; - if (!projectWorldPointToViewportClipped(tipWorld, tipScreen) || !projectWorldPointToViewportClipped(tailWorld, tailScreen)) - return; - - const ImVec2 dir = ImVec2(tipScreen.x - tailScreen.x, tipScreen.y - tailScreen.y); - const float len = std::sqrt(dir.x * dir.x + dir.y * dir.y); - if (len < 1e-3f) - return; - - const ImVec2 n = ImVec2(dir.x / len, dir.y / len); - const ImVec2 ortho = ImVec2(-n.y, n.x); - const float headLength = 9.0f; - const float headHalfWidth = 4.5f; - - const ImVec2 base = ImVec2(tipScreen.x - n.x * headLength, tipScreen.y - n.y * headLength); - const ImVec2 left = ImVec2(base.x + ortho.x * headHalfWidth, base.y + ortho.y * headHalfWidth); - const ImVec2 right = ImVec2(base.x - ortho.x * headHalfWidth, base.y - ortho.y * headHalfWidth); - drawList->AddTriangleFilled(tipScreen, left, right, color); - }; - - drawAxisArrowHead(xPos, float32_t3(axisLength - 0.55f, 0.0f, 0.0f), IM_COL32(255, 162, 162, 255)); - drawAxisArrowHead(yPos, float32_t3(0.0f, axisLength - 0.55f, 0.0f), IM_COL32(186, 255, 192, 255)); - drawAxisArrowHead(zPos, float32_t3(0.0f, 0.0f, axisLength - 0.55f), IM_COL32(178, 216, 255, 255)); - - auto drawAxisLabel = [&](const char* label, const float32_t3& worldPoint, ImU32 color) -> void - { - ImVec2 screenPos = {}; - if (!projectWorldPointToViewportClipped(worldPoint, screenPos)) - return; - drawList->AddText(ImVec2(screenPos.x + 4.0f, screenPos.y + 3.0f), color, label); - }; - - drawList->AddCircleFilled(originScreen, 4.0f, IM_COL32(240, 248, 255, 220), 16); - - drawAxisLabel("X", xPos, IM_COL32(255, 152, 152, 255)); - drawAxisLabel("Y", yPos, IM_COL32(172, 255, 178, 255)); - drawAxisLabel("Z", zPos, IM_COL32(172, 210, 255, 255)); - } - - inline void drawScriptVisualDebugOverlay(const ImVec2& displaySize) + inline void drawScriptVisualDebugOverlay(const ImVec2& displaySize) { if (!(m_scriptedInput.enabled && m_scriptedInput.visualDebug)) return; @@ -1217,114 +887,33 @@ class App final : public examples::SimpleWindowedApplication const uint64_t holdFrames = static_cast(std::round(std::max(0.f, m_scriptedInput.visualCameraHoldSeconds) * fps)); const uint64_t progressFrames = holdFrames ? std::min(elapsedFrames, holdFrames) : elapsedFrames; - const auto cameraLabel = getCameraTypeLabel(camera); - const auto cameraHint = getCameraTypeDescription(camera); - std::string lineTop = "SCRIPT VISUAL DEBUG"; - std::string lineMid = "Camera " + std::to_string(binding.activePlanarIx + 1u) + "/" + std::to_string(m_planarProjections.size()) + " " + std::string(cameraLabel); - - char lineBottomBuffer[256] = {}; - if (holdFrames) + nbl::ui::SCameraScriptVisualDebugStatus debugStatus = {}; + debugStatus.cameraLabel = getCameraTypeLabel(camera); + debugStatus.cameraHint = getCameraTypeDescription(camera); + debugStatus.cameraIndex = binding.activePlanarIx; + debugStatus.cameraCount = static_cast(m_planarProjections.size()); + debugStatus.planarIndex = binding.activePlanarIx; + debugStatus.hasHoldFrames = holdFrames > 0u; + debugStatus.progressFrames = progressFrames; + debugStatus.holdFrames = holdFrames; + debugStatus.targetFps = fps; + debugStatus.absoluteFrame = m_realFrameIx; + debugStatus.segmentLabel = m_scriptedInput.visualSegmentLabel; + debugStatus.followActive = m_scriptedInput.visualFollowActive; + debugStatus.followModeDescription = nbl::ui::getCameraFollowModeDescription(m_scriptedInput.visualFollowMode); + debugStatus.followLockValid = m_scriptedInput.visualFollowLockValid; + debugStatus.followLockAngleDeg = m_scriptedInput.visualFollowLockAngleDeg; + debugStatus.followTargetDistance = m_scriptedInput.visualFollowTargetDistance; + debugStatus.followTargetCenterNdcRadius = m_scriptedInput.visualFollowTargetCenterNdcRadius; + + float dynamicFov = 0.0f; + if (camera && camera->tryGetDynamicPerspectiveFov(dynamicFov)) { - const double elapsedSeconds = static_cast(progressFrames) / static_cast(fps); - const double holdSeconds = static_cast(holdFrames) / static_cast(fps); - std::snprintf( - lineBottomBuffer, - sizeof(lineBottomBuffer), - "Planar %u Segment %.1f/%.1f s Frame %llu/%llu", - binding.activePlanarIx, - elapsedSeconds, - holdSeconds, - static_cast(progressFrames), - static_cast(holdFrames)); + debugStatus.hasDynamicFov = true; + debugStatus.dynamicFovDeg = dynamicFov; } - else - { - std::snprintf( - lineBottomBuffer, - sizeof(lineBottomBuffer), - "Planar %u Frame %llu", - binding.activePlanarIx, - static_cast(m_realFrameIx)); - } - std::string lineBottom(lineBottomBuffer); - if (!m_scriptedInput.visualSegmentLabel.empty()) - lineBottom += " | " + m_scriptedInput.visualSegmentLabel; - std::string lineHint = std::string(cameraHint); - float dynamicFov = 0.0f; - if (camera && camera->tryGetDynamicPerspectiveFov(dynamicFov)) - { - char fovBuffer[96] = {}; - std::snprintf(fovBuffer, sizeof(fovBuffer), " | Dynamic FOV %.2f deg", dynamicFov); - lineHint += fovBuffer; - } - if (m_scriptedInput.visualFollowActive) - { - lineHint += " | " + std::string(nbl::ui::getCameraFollowModeDescription(m_scriptedInput.visualFollowMode)); - if (m_scriptedInput.visualFollowLockValid) - { - char followBuffer[192] = {}; - std::snprintf( - followBuffer, - sizeof(followBuffer), - " | lock %.2f deg | target %.2f | center err %.3f", - m_scriptedInput.visualFollowLockAngleDeg, - m_scriptedInput.visualFollowTargetDistance, - m_scriptedInput.visualFollowTargetCenterNdcRadius); - lineHint += followBuffer; - } - else - { - lineHint += " | lock n/a | target n/a | center err n/a"; - } - } - else - { - lineHint += " | Follow off"; - } - - const float topSize = 50.f; - const float midSize = 38.f; - const float bottomSize = 28.f; - const float hintSize = 24.f; - const float marginTop = 18.f; - const float padX = 24.f; - const float padY = 16.f; - const float gap = 6.f; - - ImFont* font = ImGui::GetFont(); - if (!font) - return; - - const float textWrap = std::numeric_limits::max(); - const ImVec2 topTextSize = font->CalcTextSizeA(topSize, textWrap, 0.0f, lineTop.c_str()); - const ImVec2 midTextSize = font->CalcTextSizeA(midSize, textWrap, 0.0f, lineMid.c_str()); - const ImVec2 bottomTextSize = font->CalcTextSizeA(bottomSize, textWrap, 0.0f, lineBottom.c_str()); - const ImVec2 hintTextSize = font->CalcTextSizeA(hintSize, textWrap, 0.0f, lineHint.c_str()); - const float panelWidth = std::max(std::max(topTextSize.x, midTextSize.x), std::max(bottomTextSize.x, hintTextSize.x)) + padX * 2.0f; - const float panelHeight = topTextSize.y + midTextSize.y + bottomTextSize.y + hintTextSize.y + gap * 3.0f + padY * 2.0f; - const ImVec2 panelMin((displaySize.x - panelWidth) * 0.5f, marginTop); - const ImVec2 panelMax(panelMin.x + panelWidth, panelMin.y + panelHeight); - - auto* drawList = ImGui::GetForegroundDrawList(); - if (!drawList) - return; - drawList->AddRectFilled(panelMin, panelMax, IM_COL32(6, 8, 12, 232), 14.0f); - drawList->AddRect(panelMin, panelMax, IM_COL32(255, 166, 64, 255), 14.0f, 0, 2.5f); - - const float topX = panelMin.x + (panelWidth - topTextSize.x) * 0.5f; - const float midX = panelMin.x + (panelWidth - midTextSize.x) * 0.5f; - const float bottomX = panelMin.x + (panelWidth - bottomTextSize.x) * 0.5f; - const float hintX = panelMin.x + (panelWidth - hintTextSize.x) * 0.5f; - const float topY = panelMin.y + padY; - const float midY = topY + topTextSize.y + gap; - const float bottomY = midY + midTextSize.y + gap; - const float hintY = bottomY + bottomTextSize.y + gap; - - drawList->AddText(font, topSize, ImVec2(topX, topY), IM_COL32(255, 206, 120, 255), lineTop.c_str()); - drawList->AddText(font, midSize, ImVec2(midX, midY), IM_COL32(255, 244, 224, 255), lineMid.c_str()); - drawList->AddText(font, bottomSize, ImVec2(bottomX, bottomY), IM_COL32(202, 222, 255, 255), lineBottom.c_str()); - drawList->AddText(font, hintSize, ImVec2(hintX, hintY), IM_COL32(170, 204, 255, 255), lineHint.c_str()); + nbl::ui::drawScriptVisualDebugOverlay(displaySize, nbl::ui::buildScriptVisualDebugOverlayData(debugStatus)); } inline bool tryCaptureGoal(ICamera* camera, CCameraGoal& out) const @@ -1533,6 +1122,9 @@ class App final : public examples::SimpleWindowedApplication bool loadKeyframesFromFile(const nbl::system::path& path); void imguiListen(); + void drawWindowedViewportWindows(ImGuiIO& io, SImResourceInfo& info); + void drawFullscreenViewportWindow(ImGuiIO& io, SImResourceInfo& info); + void refreshViewportBindingMatrices(); inline bool shouldCaptureOSCursor() { @@ -1614,103 +1206,6 @@ class App final : public examples::SimpleWindowedApplication m_uiVirtualEventsThisFrame = 0u; } - inline void DrawBadge(const char* label, const ImVec4& bg, const ImVec4& fg) - { - 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(6.0f, 2.0f)); - ImGui::Button(label); - ImGui::PopStyleVar(); - ImGui::PopStyleColor(4); - } - - inline void DrawKeyHint(const char* label, const ImVec4& bg, const ImVec4& fg) - { - 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(4.0f, 1.0f)); - ImGui::SmallButton(label); - ImGui::PopStyleVar(); - ImGui::PopStyleColor(4); - } - - inline void DrawHoverHint(const char* text) - { - if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayShort)) - { - ImGui::BeginTooltip(); - ImGui::TextUnformatted(text); - ImGui::EndTooltip(); - } - } - - inline void DrawDot(const ImVec4& color) - { - ImVec2 p = ImGui::GetCursorScreenPos(); - const float radius = 3.5f; - ImGui::GetWindowDrawList()->AddCircleFilled(ImVec2(p.x + radius, p.y + radius + 1.0f), radius, ImGui::ColorConvertFloat4ToU32(color)); - ImGui::Dummy(ImVec2(radius * 2.0f + 2.0f, radius * 2.0f)); - ImGui::SameLine(0, 6.0f); - } - - inline void DrawSectionHeader(const char* id, const char* label, const ImVec4& accent) - { - ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 4.0f); - ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.14f, 0.18f, 0.22f, 0.52f)); - if (ImGui::BeginChild(id, ImVec2(0, 20), true, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse)) - { - ImVec2 p = ImGui::GetWindowPos(); - ImVec2 s = ImGui::GetWindowSize(); - ImGui::GetWindowDrawList()->AddRectFilled(p, ImVec2(p.x + 2.0f, p.y + s.y), ImGui::ColorConvertFloat4ToU32(accent), 4.0f); - ImGui::SetCursorPosX(8.0f); - ImGui::AlignTextToFramePadding(); - ImGui::TextColored(accent, "%s", label); - } - ImGui::EndChild(); - ImGui::PopStyleColor(); - ImGui::PopStyleVar(); - ImGui::Spacing(); - } - - inline float CalcCardHeight(int rows) const - { - return ImGui::GetFrameHeightWithSpacing() * (static_cast(rows) + 1.0f) + 10.0f; - } - - inline bool BeginCard(const char* id, float height, const ImVec4& top, const ImVec4& bottom, const ImVec4& border) - { - ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 6.0f); - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(10.0f, 8.0f)); - ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0, 0, 0, 0)); - const bool open = ImGui::BeginChild(id, ImVec2(0, height), true, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse); - ImVec2 p = ImGui::GetWindowPos(); - ImVec2 s = ImGui::GetWindowSize(); - const ImU32 colTop = ImGui::ColorConvertFloat4ToU32(top); - const ImU32 colBottom = ImGui::ColorConvertFloat4ToU32(bottom); - ImGui::GetWindowDrawList()->AddRectFilledMultiColor( - p, - ImVec2(p.x + s.x, p.y + s.y), - colTop, - colTop, - colBottom, - colBottom - ); - ImGui::GetWindowDrawList()->AddRect(p, ImVec2(p.x + s.x, p.y + s.y), ImGui::ColorConvertFloat4ToU32(border), 6.0f); - return open; - } - - inline void EndCard() - { - ImGui::EndChild(); - ImGui::PopStyleColor(); - ImGui::PopStyleVar(2); - } - - void DrawControlPanel(); void TransformEditorContents(); @@ -1764,8 +1259,6 @@ class App final : public examples::SimpleWindowedApplication std::array, MaxFramesInFlight> m_tripleBuffers; // Resources derived from the images std::array, MaxFramesInFlight> m_framebuffers = {}; - // We will use it to get some asset stuff like geometry creator - smart_refctd_ptr m_assetManager; // Input system for capturing system events core::smart_refctd_ptr m_inputSystem; // Handles mouse events @@ -1807,53 +1300,7 @@ class App final : public examples::SimpleWindowedApplication bool resetCursorToCenter = true; - struct windowControlBinding - { - 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, lastBoundPerspectivePresetProjectionIx = std::nullopt, lastBoundOrthoPresetProjectionIx = std::nullopt; - std::optional inputBindingProjectionIx = std::nullopt; - uint32_t inputBindingPlanarIx = std::numeric_limits::max(); - - 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 = std::numeric_limits::max(); - } - }; - - inline void syncWindowInputBinding(windowControlBinding& binding) + inline void syncWindowInputBinding(SWindowControlBinding& binding) { if (!binding.boundProjectionIx.has_value()) return; @@ -1877,7 +1324,7 @@ class App final : public examples::SimpleWindowedApplication binding.inputBindingProjectionIx = projectionIx; } - inline void syncWindowInputBindingToProjection(windowControlBinding& binding) + inline void syncWindowInputBindingToProjection(SWindowControlBinding& binding) { if (!binding.boundProjectionIx.has_value()) return; @@ -1935,7 +1382,7 @@ class App final : public examples::SimpleWindowedApplication }; static constexpr inline auto MaxSceneFBOs = 2u; - std::array windowBindings; + std::array windowBindings; uint32_t activeRenderWindowIx = 0u; // UI font atlas + viewport FBO color attachment textures diff --git a/61_UI/include/app/AppCameraConfigUtilities.hpp b/61_UI/include/app/AppCameraConfigUtilities.hpp new file mode 100644 index 000000000..34a7ef95e --- /dev/null +++ b/61_UI/include/app/AppCameraConfigUtilities.hpp @@ -0,0 +1,167 @@ +#ifndef _APP_CAMERA_CONFIG_UTILITIES_HPP_ +#define _APP_CAMERA_CONFIG_UTILITIES_HPP_ + +#include +#include +#include + +#include "app/AppResourceUtilities.hpp" +#include "camera/CArcballCamera.hpp" +#include "camera/CChaseCamera.hpp" +#include "camera/CDollyCamera.hpp" +#include "camera/CDollyZoomCamera.hpp" +#include "camera/CFPSCamera.hpp" +#include "camera/CFreeLockCamera.hpp" +#include "camera/CIsometricCamera.hpp" +#include "camera/COrbitCamera.hpp" +#include "camera/CPathCamera.hpp" +#include "camera/CTopDownCamera.hpp" +#include "camera/CTurntableCamera.hpp" + +namespace nbl::system +{ + +struct SCameraConfigFactoryMotionScales final +{ + static constexpr double DefaultMove = core::ICamera::DefaultMoveSpeedScale; + static constexpr double DefaultRotate = core::ICamera::DefaultRotationSpeedScale; + static constexpr double TargetRigMove = 0.5; +}; + +inline void initializeCameraMotionConfig(core::ICamera& camera, const double moveScale, const double rotationScale) +{ + camera.setMotionScales(moveScale, rotationScale); +} + +inline bool tryCreateCameraFromJson( + const camera_json_t& jCamera, + std::string& error, + core::smart_refctd_ptr& outCamera) +{ + if (!jCamera.contains("type")) + { + error = "Camera entry missing \"type\"."; + return false; + } + + if (!jCamera.contains("position")) + { + error = "Camera entry missing \"position\"."; + return false; + } + + const std::string type = jCamera["type"].get(); + const bool withOrientation = jCamera.contains("orientation"); + const bool withTarget = jCamera.contains("target"); + + const auto position = [&]() + { + const auto value = jCamera["position"].get>(); + return hlsl::float64_t3(value[0], value[1], value[2]); + }(); + + const auto getOrientation = [&]() + { + const auto value = jCamera["orientation"].get>(); + return hlsl::makeQuaternionFromComponents(value[0], value[1], value[2], value[3]); + }; + + const auto getTarget = [&]() + { + const auto value = jCamera["target"].get>(); + return hlsl::float64_t3(value[0], value[1], value[2]); + }; + + const auto finalize = [&](auto&& camera, const double moveScale, const double rotationScale) + { + initializeCameraMotionConfig(*camera, moveScale, rotationScale); + outCamera = std::move(camera); + return true; + }; + + if (type == "FPS") + { + if (!withOrientation) + { + error = "FPS camera requires \"orientation\"."; + return false; + } + return finalize(core::make_smart_refctd_ptr(position, getOrientation()), SCameraConfigFactoryMotionScales::DefaultMove, SCameraConfigFactoryMotionScales::DefaultRotate); + } + + if (type == "Free") + { + if (!withOrientation) + { + error = "Free camera requires \"orientation\"."; + return false; + } + return finalize(core::make_smart_refctd_ptr(position, getOrientation()), SCameraConfigFactoryMotionScales::DefaultMove, SCameraConfigFactoryMotionScales::DefaultRotate); + } + + if (!withTarget) + { + error = "Camera type \"" + type + "\" requires \"target\"."; + return false; + } + + if (type == "Orbit") + return finalize(core::make_smart_refctd_ptr(position, getTarget()), SCameraConfigFactoryMotionScales::TargetRigMove, SCameraConfigFactoryMotionScales::DefaultRotate); + if (type == "Arcball") + return finalize(core::make_smart_refctd_ptr(position, getTarget()), SCameraConfigFactoryMotionScales::TargetRigMove, SCameraConfigFactoryMotionScales::DefaultRotate); + if (type == "Turntable") + return finalize(core::make_smart_refctd_ptr(position, getTarget()), SCameraConfigFactoryMotionScales::TargetRigMove, SCameraConfigFactoryMotionScales::DefaultRotate); + if (type == "TopDown") + return finalize(core::make_smart_refctd_ptr(position, getTarget()), SCameraConfigFactoryMotionScales::TargetRigMove, SCameraConfigFactoryMotionScales::DefaultRotate); + if (type == "Isometric") + return finalize(core::make_smart_refctd_ptr(position, getTarget()), SCameraConfigFactoryMotionScales::TargetRigMove, SCameraConfigFactoryMotionScales::DefaultRotate); + if (type == "Chase") + return finalize(core::make_smart_refctd_ptr(position, getTarget()), SCameraConfigFactoryMotionScales::TargetRigMove, SCameraConfigFactoryMotionScales::DefaultRotate); + if (type == "Dolly") + return finalize(core::make_smart_refctd_ptr(position, getTarget()), SCameraConfigFactoryMotionScales::TargetRigMove, SCameraConfigFactoryMotionScales::DefaultRotate); + if (type == "Path") + return finalize(core::make_smart_refctd_ptr(position, getTarget()), SCameraConfigFactoryMotionScales::TargetRigMove, SCameraConfigFactoryMotionScales::DefaultRotate); + if (type == "DollyZoom") + { + if (jCamera.contains("baseFov")) + return finalize(core::make_smart_refctd_ptr(position, getTarget(), jCamera["baseFov"].get()), SCameraConfigFactoryMotionScales::TargetRigMove, SCameraConfigFactoryMotionScales::DefaultRotate); + return finalize(core::make_smart_refctd_ptr(position, getTarget()), SCameraConfigFactoryMotionScales::TargetRigMove, SCameraConfigFactoryMotionScales::DefaultRotate); + } + + error = "Unsupported camera type \"" + type + "\"."; + return false; +} + +inline bool tryLoadCameraCollectionFromJson( + const camera_json_t& json, + std::string& error, + std::vector>& outCameras) +{ + outCameras.clear(); + if (!json.contains("cameras") || !json["cameras"].is_array()) + { + error = "Missing \"cameras\" array in config."; + return false; + } + + outCameras.reserve(json["cameras"].size()); + for (const auto& jCamera : json["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; +} + +} // namespace nbl::system + +#endif // _APP_CAMERA_CONFIG_UTILITIES_HPP_ diff --git a/61_UI/include/app/AppResourceUtilities.hpp b/61_UI/include/app/AppResourceUtilities.hpp new file mode 100644 index 000000000..82cf82d51 --- /dev/null +++ b/61_UI/include/app/AppResourceUtilities.hpp @@ -0,0 +1,194 @@ +// 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_THIS_EXAMPLE_APP_RESOURCE_UTILITIES_HPP_ +#define _NBL_THIS_EXAMPLE_APP_RESOURCE_UTILITIES_HPP_ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common.hpp" +#include "nlohmann/json.hpp" + +namespace nbl::system +{ + +using camera_json_t = nlohmann::json; + +struct SSpaceEnvBlobHeader final +{ + uint32_t magic = 0u; + uint32_t width = 0u; + uint32_t height = 0u; + uint32_t format = 0u; + uint64_t payloadSize = 0ull; +}; + +struct SCameraAppResourcePaths final +{ + static constexpr uint32_t SpaceEnvBlobMagic = 0x31425645u; // "EVB1" + static constexpr uint32_t SpaceEnvBlobFormatRgba16Sfloat = 2u; + static constexpr std::string_view AppResourcesWorkingDirectory = "app_resources"; + static constexpr std::string_view DefaultCameraConfigRelativePath = "app_resources/cameras.json"; + static constexpr std::string_view SpaceEnvBlobCandidate = "rich_blue_nebulae_1_8k.rgba16f.envblob"; +}; + +inline bool loadFileBytes(ISystem& system, const std::filesystem::path& path, std::vector& outPayload) +{ + ISystem::future_t> future; + system.createFile(future, path, IFile::ECF_READ | IFile::ECF_MAPPABLE); + auto file = future.acquire(); + if (!file || !file->get()) + return false; + + auto& input = *file->get(); + const auto fileSize = input.getSize(); + outPayload.resize(fileSize); + if (outPayload.empty()) + return true; + + IFile::success_t readResult; + input.read(readResult, outPayload.data(), 0, fileSize); + return static_cast(readResult); +} + +inline bool loadFileText(ISystem& system, const std::filesystem::path& path, std::string& outText) +{ + std::vector payload; + if (!loadFileBytes(system, path, payload)) + return false; + + outText.assign(reinterpret_cast(payload.data()), payload.size()); + return true; +} + +inline bool parseJsonText(std::string_view text, camera_json_t& outJson, std::string* error = nullptr) +{ + 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; + } +} + +inline bool loadJsonFromPath( + ISystem& system, + const std::filesystem::path& path, + camera_json_t& outJson, + std::string* error = nullptr) +{ + std::string jsonText; + if (!loadFileText(system, path, jsonText)) + { + if (error) + *error = "Cannot open config \"" + path.string() + "\"."; + return false; + } + + return parseJsonText(jsonText, outJson, error); +} + +inline path resolveInputPath(const path& localInputCWD, path pathValue) +{ + if (pathValue.is_relative()) + pathValue = (localInputCWD / pathValue).lexically_normal(); + return pathValue; +} + +inline bool parseSpaceEnvBlobBytes( + std::span blobBytes, + SSpaceEnvBlobHeader& outHeader, + std::vector& outPayload) +{ + if (blobBytes.size() < sizeof(SSpaceEnvBlobHeader)) + return false; + + std::memcpy(&outHeader, blobBytes.data(), sizeof(outHeader)); + + if (outHeader.magic != SCameraAppResourcePaths::SpaceEnvBlobMagic || + outHeader.format != SCameraAppResourcePaths::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( + ISystem& system, + const std::filesystem::path& blobPath, + SSpaceEnvBlobHeader& outHeader, + std::vector& outPayload) +{ + std::vector blobBytes; + if (!loadFileBytes(system, blobPath, blobBytes)) + return false; + return parseSpaceEnvBlobBytes(blobBytes, outHeader, outPayload); +} + +inline std::array makeSpaceEnvSearchRoots(const path& localInputCWD) +{ + return { + (localInputCWD / ".." / "media" / "envmap").lexically_normal(), + (localInputCWD / ".." / "media").lexically_normal(), + (localInputCWD / SCameraAppResourcePaths::AppResourcesWorkingDirectory).lexically_normal() + }; +} + +inline bool loadFirstSpaceEnvBlobFromRoots( + ISystem& system, + const std::array& searchRoots, + SSpaceEnvBlobHeader& outHeader, + std::vector& outPayload) +{ + for (const auto& root : searchRoots) + { + if (loadSpaceEnvBlob(system, root / SCameraAppResourcePaths::SpaceEnvBlobCandidate, outHeader, outPayload)) + return true; + } + return false; +} + +inline core::smart_refctd_ptr loadPrecompiledShaderFromAppResources( + asset::IAssetManager& assetManager, + ILogger* logger, + const std::string_view key) +{ + asset::IAssetLoader::SAssetLoadParams loadParams = {}; + loadParams.logger = logger; + loadParams.workingDirectory = SCameraAppResourcePaths::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 + +#endif // _NBL_THIS_EXAMPLE_APP_RESOURCE_UTILITIES_HPP_ diff --git a/61_UI/include/app/AppTypes.hpp b/61_UI/include/app/AppTypes.hpp index 436898cf6..6b6e5cb08 100644 --- a/61_UI/include/app/AppTypes.hpp +++ b/61_UI/include/app/AppTypes.hpp @@ -16,6 +16,54 @@ struct ImGuizmoModelM16InOut float32_t4x4 inTRS, outTRS, outDeltaTRS; }; +struct SWindowControlBinding final +{ + 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 = std::numeric_limits::max(); + + 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 = std::numeric_limits::max(); + } +}; + constexpr IGPUImage::SSubresourceRange TripleBufferUsedSubresourceRange = { .aspectMask = IGPUImage::EAF_COLOR_BIT, diff --git a/61_UI/include/app/AppViewportBindingUtilities.hpp b/61_UI/include/app/AppViewportBindingUtilities.hpp new file mode 100644 index 000000000..59c21bd34 --- /dev/null +++ b/61_UI/include/app/AppViewportBindingUtilities.hpp @@ -0,0 +1,86 @@ +#ifndef _NBL_THIS_EXAMPLE_APP_VIEWPORT_BINDING_UTILITIES_HPP_ +#define _NBL_THIS_EXAMPLE_APP_VIEWPORT_BINDING_UTILITIES_HPP_ + +#include +#include + +#include "app/AppTypes.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 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::syncDynamicPerspectiveProjection(camera, projection); + projection.update(binding.leftHandedProjection, binding.aspectRatio); + + outState.camera = camera; + outState.projection = &projection; + outState.viewMatrix = getMatrix3x4As4x4(getCastedMatrix(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 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; +} + +} // namespace nbl::ui + +#endif // _NBL_THIS_EXAMPLE_APP_VIEWPORT_BINDING_UTILITIES_HPP_ diff --git a/61_UI/include/common.hpp b/61_UI/include/common.hpp index 0d3d5b6e5..a2ae9c595 100644 --- a/61_UI/include/common.hpp +++ b/61_UI/include/common.hpp @@ -33,6 +33,9 @@ #include "camera/CCameraProjectionUtilities.hpp" #include "camera/CCameraFollowUtilities.hpp" #include "camera/CCameraFollowRegressionUtilities.hpp" +#include "camera/CCameraControlPanelUiUtilities.hpp" +#include "camera/CCameraScriptVisualDebugOverlayUtilities.hpp" +#include "camera/CCameraViewportOverlayUtilities.hpp" #include "camera/CCameraTextUtilities.hpp" #include "camera/CCameraInputBindingUtilities.hpp" #include "camera/CGimbalInputBinder.hpp" diff --git a/common/include/camera/CArcballCamera.hpp b/common/include/camera/CArcballCamera.hpp index 624e99109..a4b367b8e 100644 --- a/common/include/camera/CArcballCamera.hpp +++ b/common/include/camera/CArcballCamera.hpp @@ -33,16 +33,14 @@ class CArcballCamera final : public CSphericalTargetCamera if (not virtualEvents.size() and not referenceFrame) return false; - auto impulse = m_gimbal.accumulate(virtualEvents); + const auto impulse = m_gimbal.accumulate(virtualEvents); - const double deltaYaw = impulse.dVirtualRotation.y * getRotationSpeedScale(); - const double deltaPitch = impulse.dVirtualRotation.x * getRotationSpeedScale(); + const double deltaYaw = scaleVirtualRotation(impulse.dVirtualRotation.y); + const double deltaPitch = scaleVirtualRotation(impulse.dVirtualRotation.x); - constexpr double translateScalar = 0.01; - const double panScalar = translateScalar * getMoveSpeedScale(); - const double deltaPanX = impulse.dVirtualTranslate.x * panScalar; - const double deltaPanY = impulse.dVirtualTranslate.y * panScalar; - const double deltaDistance = impulse.dVirtualTranslate.z * translateScalar; + const double deltaPanX = scaleVirtualTranslation(impulse.dVirtualTranslate.x); + const double deltaPanY = scaleVirtualTranslation(impulse.dVirtualTranslate.y); + const double deltaDistance = scaleUnscaledVirtualTranslation(impulse.dVirtualTranslate.z); m_u += deltaYaw; m_v = std::clamp(m_v + deltaPitch, MinPitch, MaxPitch); @@ -66,7 +64,7 @@ class CArcballCamera final : public CSphericalTargetCamera private: static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate | CVirtualGimbalEvent::Rotate; - static inline constexpr double MaxPitch = 1.5533430342749532; + static inline constexpr double MaxPitch = hlsl::numbers::pi * (89.0 / 180.0); static inline constexpr double MinPitch = -MaxPitch; }; diff --git a/common/include/camera/CCameraControlPanelUiUtilities.hpp b/common/include/camera/CCameraControlPanelUiUtilities.hpp new file mode 100644 index 000000000..2b81aada4 --- /dev/null +++ b/common/include/camera/CCameraControlPanelUiUtilities.hpp @@ -0,0 +1,442 @@ +// 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 "imgui/imgui.h" + +namespace nbl::ui +{ + +//! Shared visual theme and layout constants for the control panel consumer UI. +struct SCameraControlPanelStyle final +{ + static constexpr float WindowWidthRatio = 0.19f; + static constexpr float WindowMinWidth = 200.0f; + static constexpr float WindowMaxWidthRatio = 0.25f; + static constexpr float WindowHeightRatio = 0.34f; + static constexpr float WindowMinHeight = 200.0f; + static constexpr float WindowMaxHeightRatio = 0.50f; + + static constexpr ImVec2 WindowPadding = ImVec2(5.0f, 4.0f); + static constexpr ImVec2 FramePadding = ImVec2(4.0f, 1.0f); + static constexpr ImVec2 ItemSpacing = ImVec2(3.0f, 2.0f); + static constexpr ImVec2 CellPadding = ImVec2(3.0f, 2.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(6.0f, 2.0f); + static constexpr ImVec2 KeyHintPadding = ImVec2(4.0f, 1.0f); + static constexpr float BadgeFramePaddingX = 6.0f; + static constexpr float BadgeFramePaddingY = 2.0f; + static constexpr float KeyHintFramePaddingX = 4.0f; + static constexpr float KeyHintFramePaddingY = 1.0f; + static constexpr float DotRadius = 3.5f; + static constexpr float DotYOffset = 1.0f; + static constexpr float DotSpacing = 6.0f; + static constexpr float SectionChildRounding = 4.0f; + static constexpr float CardChildRounding = 6.0f; + static constexpr ImVec2 CardWindowPadding = ImVec2(10.0f, 8.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 = 8.0f; + static constexpr float SectionHeaderHeight = 20.0f; + static constexpr float SectionSpacingY = 0.0f; + static constexpr float CardExtraRows = 1.0f; + static constexpr float CardHeightPadding = 10.0f; + static constexpr float MiniStatHeight = 56.0f; + static constexpr float MiniStatPlotHeight = 24.0f; + static constexpr float MiniStatChildRounding = 6.0f; + static constexpr float HeaderWindowHeight = 64.0f; + static constexpr float HeaderTitleFontScale = 1.08f; + static constexpr float HeaderMetricFontScale = 1.05f; + static constexpr float HeaderDummyY = 1.0f; + static constexpr float HeaderGapSmall = 2.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 = 120.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; +}; + +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)); +} + +inline float calcPillWidth(const char* label, const ImVec2& padding) +{ + return ImGui::CalcTextSize(label).x + padding.x * 2.0f; +} + +inline void centerControlPanelRow(const float contentWidth) +{ + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + std::max(0.0f, (ImGui::GetContentRegionAvail().x - contentWidth) * 0.5f)); +} + +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); +} + +inline void popControlPanelWindowStyle() +{ + ImGui::PopStyleColor(19); + ImGui::PopStyleVar(9); +} + +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); +} + +inline float calcCameraControlPanelCardHeight(const int rows, const SCameraControlPanelStyle& style = {}) +{ + return ImGui::GetFrameHeightWithSpacing() * (static_cast(rows) + style.CardExtraRows) + style.CardHeightPadding; +} + +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); +} + +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; +} + +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); + } +} + +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); +} + +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; +} + +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); + } +} + +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); + } +} + +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 +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(); +} + +inline void drawHoverHint(const char* text) +{ + if (!ImGui::IsItemHovered(ImGuiHoveredFlags_DelayShort)) + return; + ImGui::BeginTooltip(); + ImGui::TextUnformatted(text); + ImGui::EndTooltip(); +} + +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); +} + +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(); +} + +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; +} + +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/CCameraFollowRegressionUtilities.hpp b/common/include/camera/CCameraFollowRegressionUtilities.hpp index 14aec821c..a8e1b1d63 100644 --- a/common/include/camera/CCameraFollowRegressionUtilities.hpp +++ b/common/include/camera/CCameraFollowRegressionUtilities.hpp @@ -51,6 +51,28 @@ struct SCameraFollowVisualMetrics float projectedNdcRadius = 0.0f; }; +//! Shared tolerances for follow target lock, writeback, and projected-center checks. +struct SCameraFollowRegressionThresholds +{ + static inline constexpr float DefaultClipWEpsilon = 1e-5f; + static inline constexpr float DefaultProjectedNdcTolerance = 0.03f; + static inline constexpr float DefaultLockAngleToleranceDeg = static_cast(core::ICamera::DefaultAngularToleranceDeg); + static inline constexpr double DefaultDistanceTolerance = core::ICamera::ScalarTolerance; + static inline constexpr double DefaultTargetTolerance = core::ICamera::TinyScalarEpsilon; + static inline constexpr double DefaultPositionTolerance = core::ICamera::DefaultPositionTolerance; + static inline constexpr double DefaultRotationToleranceDeg = core::ICamera::DefaultAngularToleranceDeg; + static inline constexpr double DefaultScalarTolerance = core::ICamera::ScalarTolerance; + + float clipWEpsilon = DefaultClipWEpsilon; + float projectedNdcTolerance = DefaultProjectedNdcTolerance; + float lockAngleToleranceDeg = DefaultLockAngleToleranceDeg; + double distanceTolerance = DefaultDistanceTolerance; + double targetTolerance = DefaultTargetTolerance; + double positionTolerance = DefaultPositionTolerance; + double rotationToleranceDeg = DefaultRotationToleranceDeg; + double scalarTolerance = DefaultScalarTolerance; +}; + //! Bundled reusable follow regression flow. //! The helper builds a follow goal, applies it, verifies the resulting camera state, //! and then checks the lock/writeback follow contract. @@ -69,7 +91,8 @@ inline bool tryComputeProjectedFollowTargetMetrics( const core::CTrackedTarget& trackedTarget, float& outNdcX, float& outNdcY, - float* outNdcRadius = nullptr) + float* outNdcRadius = nullptr, + const float clipWEpsilon = SCameraFollowRegressionThresholds::DefaultClipWEpsilon) { const auto target = hlsl::getCastedVector(trackedTarget.getGimbal().getPosition()); const auto clip = hlsl::mul(viewProjMatrix, hlsl::float32_t4(target.x, target.y, target.z, 1.0f)); @@ -77,7 +100,7 @@ inline bool tryComputeProjectedFollowTargetMetrics( return false; const auto absW = std::abs(clip.w); - if (absW < 1e-5f) + if (absW < clipWEpsilon) return false; const float invW = 1.0f / clip.w; @@ -87,7 +110,7 @@ inline bool tryComputeProjectedFollowTargetMetrics( return false; if (outNdcRadius) - *outNdcRadius = std::sqrt(outNdcX * outNdcX + outNdcY * outNdcY); + *outNdcRadius = hlsl::length(hlsl::float32_t2(outNdcX, outNdcY)); return true; } @@ -97,18 +120,18 @@ inline bool validateProjectedFollowTargetContract( const core::CTrackedTarget& trackedTarget, float& outNdcRadius, std::string* error = nullptr, - const float ndcRadiusTolerance = 0.03f) + const SCameraFollowRegressionThresholds& thresholds = {}) { float ndcX = 0.0f; float ndcY = 0.0f; - if (!tryComputeProjectedFollowTargetMetrics(viewProjMatrix, trackedTarget, ndcX, ndcY, &outNdcRadius)) + if (!tryComputeProjectedFollowTargetMetrics(viewProjMatrix, trackedTarget, ndcX, ndcY, &outNdcRadius, thresholds.clipWEpsilon)) { if (error) *error = "failed to project follow target"; return false; } - if (outNdcRadius > ndcRadiusTolerance) + if (outNdcRadius > thresholds.projectedNdcTolerance) { if (error) { @@ -160,11 +183,8 @@ inline bool validateFollowTargetContract( const core::CCameraGoal& followGoal, SCameraFollowRegressionResult& out, std::string* error = nullptr, - const float lockAngleToleranceDeg = 0.1f, - const double distanceTolerance = 1e-6, - const double targetTolerance = 1e-9, const hlsl::float32_t4x4* viewProjMatrix = nullptr, - const float projectedNdcTolerance = 0.03f) + const SCameraFollowRegressionThresholds& thresholds = {}) { out = {}; if (!camera) @@ -185,7 +205,7 @@ inline bool validateFollowTargetContract( } const auto expectedTargetDistance = hlsl::length(trackedTarget.getGimbal().getPosition() - camera->getGimbal().getPosition()); - if (!std::isfinite(expectedTargetDistance) || std::abs(expectedTargetDistance - out.targetDistance) > distanceTolerance) + if (!std::isfinite(expectedTargetDistance) || std::abs(expectedTargetDistance - out.targetDistance) > thresholds.distanceTolerance) { if (error) { @@ -195,7 +215,7 @@ inline bool validateFollowTargetContract( return false; } - if (out.lockAngleDeg > lockAngleToleranceDeg) + if (out.lockAngleDeg > thresholds.lockAngleToleranceDeg) { if (error) *error = "lock angle mismatch angle_deg=" + std::to_string(out.lockAngleDeg); @@ -209,7 +229,8 @@ inline bool validateFollowTargetContract( trackedTarget, out.projectedNdcX, out.projectedNdcY, - &out.projectedNdcRadius); + &out.projectedNdcRadius, + thresholds.clipWEpsilon); if (!out.hasProjectedMetrics) { if (error) @@ -217,7 +238,7 @@ inline bool validateFollowTargetContract( return false; } - if (out.projectedNdcRadius > projectedNdcTolerance) + if (out.projectedNdcRadius > thresholds.projectedNdcTolerance) { if (error) { @@ -246,7 +267,7 @@ inline bool validateFollowTargetContract( const auto trackedTargetPosition = trackedTarget.getGimbal().getPosition(); const auto targetDelta = state.target - trackedTargetPosition; const auto targetDeltaLen = hlsl::length(targetDelta); - if (!std::isfinite(targetDeltaLen) || targetDeltaLen > targetTolerance) + if (!std::isfinite(targetDeltaLen) || targetDeltaLen > thresholds.targetTolerance) { if (error) *error = "spherical target writeback mismatch"; @@ -257,8 +278,8 @@ inline bool validateFollowTargetContract( const auto expectedDistance = followGoal.hasOrbitState ? static_cast(followGoal.orbitDistance) : (followGoal.hasDistance ? static_cast(followGoal.distance) : actualDistance); if (!std::isfinite(actualDistance) || !std::isfinite(expectedDistance) || - std::abs(actualDistance - expectedDistance) > distanceTolerance || - std::abs(static_cast(state.distance) - expectedDistance) > distanceTolerance) + std::abs(actualDistance - expectedDistance) > thresholds.distanceTolerance || + std::abs(static_cast(state.distance) - expectedDistance) > thresholds.distanceTolerance) { if (error) { @@ -282,13 +303,7 @@ inline bool buildApplyAndValidateFollowTargetContract( SCameraFollowApplyValidationResult& out, std::string* error = nullptr, const hlsl::float32_t4x4* viewProjMatrix = nullptr, - const float lockAngleToleranceDeg = 0.1f, - const double distanceTolerance = 1e-6, - const double targetTolerance = 1e-9, - const float projectedNdcTolerance = 0.03f, - const double posTolerance = 1e-6, - const double rotToleranceDeg = 0.1, - const double scalarTolerance = 1e-6) + const SCameraFollowRegressionThresholds& thresholds = {}) { out = {}; @@ -318,7 +333,7 @@ inline bool buildApplyAndValidateFollowTargetContract( out.hasCapturedGoal = true; out.capturedGoal = capture.goal; - if (!core::compareGoals(out.capturedGoal, out.goal, posTolerance, rotToleranceDeg, scalarTolerance)) + if (!core::compareGoals(out.capturedGoal, out.goal, thresholds.positionTolerance, thresholds.rotationToleranceDeg, thresholds.scalarTolerance)) { if (error) *error = std::string("follow goal mismatch. ") + core::describeGoalMismatch(out.capturedGoal, out.goal); @@ -332,11 +347,8 @@ inline bool buildApplyAndValidateFollowTargetContract( out.goal, out.regression, error, - lockAngleToleranceDeg, - distanceTolerance, - targetTolerance, viewProjMatrix, - projectedNdcTolerance)) + thresholds)) { return false; } diff --git a/common/include/camera/CCameraFollowUtilities.hpp b/common/include/camera/CCameraFollowUtilities.hpp index fd84ba38f..6e19e59d2 100644 --- a/common/include/camera/CCameraFollowUtilities.hpp +++ b/common/include/camera/CCameraFollowUtilities.hpp @@ -59,13 +59,12 @@ class CTrackedTarget inline bool trySetFromTransform(const hlsl::float64_t4x4& transform) { - const auto right = hlsl::normalize(hlsl::float64_t3(transform[0])); - const auto up = hlsl::normalize(hlsl::float64_t3(transform[1])); - const auto forward = hlsl::normalize(hlsl::float64_t3(transform[2])); - if (!hlsl::isOrthoBase(right, up, forward)) + hlsl::float64_t3 position = hlsl::float64_t3(0.0); + hlsl::camera_quaternion_t orientation = hlsl::makeIdentityQuaternion(); + if (!hlsl::tryExtractRigidPoseFromTransform(transform, position, orientation)) return false; - setPose(hlsl::float64_t3(transform[3]), hlsl::makeQuaternionFromBasis(right, up, forward)); + setPose(position, orientation); return true; } @@ -178,33 +177,7 @@ inline bool buildFollowLookAtOrientation( const hlsl::float64_t3& preferredUp, hlsl::camera_quaternion_t& outOrientation) { - const auto toTarget = targetPosition - position; - const double toTargetLength = hlsl::length(toTarget); - if (!std::isfinite(toTargetLength) || toTargetLength <= 1e-9) - return false; - - const auto forward = toTarget / toTargetLength; - auto up = preferredUp; - if (!isFiniteVec3(up) || hlsl::length(up) <= 1e-9) - up = hlsl::float64_t3(0.0, 0.0, 1.0); - else - up = hlsl::normalize(up); - - auto right = hlsl::cross(up, forward); - if (!isFiniteVec3(right) || hlsl::length(right) <= 1e-9) - { - const auto fallbackUp = std::abs(forward.z) < 0.99 ? hlsl::float64_t3(0.0, 0.0, 1.0) : hlsl::float64_t3(0.0, 1.0, 0.0); - right = hlsl::cross(fallbackUp, forward); - if (!isFiniteVec3(right) || hlsl::length(right) <= 1e-9) - return false; - } - right = hlsl::normalize(right); - up = hlsl::normalize(hlsl::cross(forward, right)); - if (!hlsl::isOrthoBase(right, up, forward)) - return false; - - outOrientation = hlsl::makeQuaternionFromBasis(right, up, forward); - return true; + return hlsl::tryBuildLookAtOrientation(position, targetPosition, preferredUp, outOrientation); } inline bool applyFollowSphericalPose( @@ -214,50 +187,50 @@ inline bool applyFollowSphericalPose( const double orbitV, const float distance) { - if (!std::isfinite(orbitU) || !std::isfinite(orbitV) || !std::isfinite(distance)) - return false; - - const float clampedDistance = std::clamp(distance, ICamera::SphericalMinDistance, ICamera::SphericalMaxDistance); - const hlsl::float64_t3 spherePosition( - std::cos(orbitV) * std::cos(orbitU) * static_cast(clampedDistance), - std::cos(orbitV) * std::sin(orbitU) * static_cast(clampedDistance), - std::sin(orbitV) * static_cast(clampedDistance)); - - const auto forward = hlsl::normalize(-spherePosition); - const auto up = hlsl::normalize(hlsl::float64_t3( - -std::sin(orbitV) * std::cos(orbitU), - -std::sin(orbitV) * std::sin(orbitU), - std::cos(orbitV))); - const auto right = hlsl::normalize(hlsl::cross(up, forward)); - if (!hlsl::isOrthoBase(right, up, forward)) + hlsl::float64_t appliedDistance = 0.0; + if (!hlsl::tryBuildSphericalPoseFromOrbit( + targetPosition, + orbitU, + orbitV, + static_cast(distance), + static_cast(ICamera::SphericalMinDistance), + static_cast(ICamera::SphericalMaxDistance), + goal.position, + goal.orientation, + &appliedDistance)) + { return false; + } goal.hasTargetPosition = true; goal.targetPosition = targetPosition; goal.hasDistance = true; - goal.distance = clampedDistance; + goal.distance = static_cast(appliedDistance); goal.hasOrbitState = true; goal.orbitU = orbitU; goal.orbitV = orbitV; - goal.orbitDistance = clampedDistance; - goal.position = targetPosition + spherePosition; - goal.orientation = hlsl::makeQuaternionFromBasis(right, up, forward); + goal.orbitDistance = static_cast(appliedDistance); return true; } inline bool buildFollowSphericalGoalFromPose(CCameraGoal& goal, const hlsl::float64_t3& targetPosition, const hlsl::float64_t3& position) { - const auto offset = position - targetPosition; - const double distance = hlsl::length(offset); - if (!std::isfinite(distance) || distance <= 1e-9) + hlsl::float64_t orbitU = 0.0; + hlsl::float64_t orbitV = 0.0; + hlsl::float64_t distance = 0.0; + if (!hlsl::tryBuildOrbitFromPosition( + targetPosition, + position, + static_cast(ICamera::SphericalMinDistance), + static_cast(ICamera::SphericalMaxDistance), + orbitU, + orbitV, + distance)) + { return false; + } - const float clampedDistance = std::clamp(static_cast(distance), ICamera::SphericalMinDistance, ICamera::SphericalMaxDistance); - const auto local = offset / static_cast(clampedDistance); - const double orbitU = std::atan2(local.y, local.x); - const double orbitV = std::asin(std::clamp(local.z, -1.0, 1.0)); - - return applyFollowSphericalPose(goal, targetPosition, orbitU, orbitV, clampedDistance); + return applyFollowSphericalPose(goal, targetPosition, orbitU, orbitV, static_cast(distance)); } inline bool captureFollowOffsetsFromCamera( @@ -284,11 +257,11 @@ inline bool tryComputeFollowTargetLockMetrics( { const auto toTarget = trackedTarget.getGimbal().getPosition() - cameraGimbal.getPosition(); const auto targetDistance = hlsl::length(toTarget); - if (!std::isfinite(targetDistance) || targetDistance <= 1e-9) + if (!std::isfinite(targetDistance) || targetDistance <= ICamera::TinyScalarEpsilon) return false; const auto forward = hlsl::normalize(cameraGimbal.getZAxis()); - if (!isFiniteVec3(forward) || hlsl::length(forward) <= 1e-9) + if (!hlsl::isFiniteVec3(forward) || hlsl::length(forward) <= ICamera::TinyScalarEpsilon) return false; const auto targetDir = toTarget / targetDistance; diff --git a/common/include/camera/CCameraGoal.hpp b/common/include/camera/CCameraGoal.hpp index 565a1f2f2..0e76e4288 100644 --- a/common/include/camera/CCameraGoal.hpp +++ b/common/include/camera/CCameraGoal.hpp @@ -44,11 +44,6 @@ inline double lerpWrappedAngleRad(double a, double b, double alpha) return a + hlsl::wrapAngleRad(b - a) * alpha; } -inline bool nearlyEqualGoalScalar(double a, double b, double eps = 1e-6) -{ - return std::abs(a - b) <= eps; -} - inline uint32_t getRequiredGoalStateMask(const CCameraGoal& target) { uint32_t mask = ICamera::GoalStateNone; @@ -68,40 +63,28 @@ inline bool applyCanonicalPathGoal(CCameraGoal& goal) if (!std::isfinite(goal.pathState.angle) || !std::isfinite(goal.pathState.radius) || !std::isfinite(goal.pathState.height)) return false; - const hlsl::float64_t3 offset( - std::cos(goal.pathState.angle) * goal.pathState.radius, + hlsl::float64_t appliedOrbitDistance = 0.0; + if (!hlsl::tryBuildPathPoseFromState( + goal.targetPosition, + goal.pathState.angle, + goal.pathState.radius, goal.pathState.height, - std::sin(goal.pathState.angle) * goal.pathState.radius); - const double distance = hlsl::length(offset); - if (!std::isfinite(distance) || distance <= 1e-9) + static_cast(ICamera::SphericalMinDistance), + static_cast(ICamera::SphericalMinDistance), + static_cast(ICamera::SphericalMaxDistance), + goal.position, + goal.orientation, + &appliedOrbitDistance, + &goal.orbitU, + &goal.orbitV)) + { return false; + } - const float appliedDistance = std::clamp( - static_cast(distance), - ICamera::SphericalMinDistance, - ICamera::SphericalMaxDistance); - const auto local = offset / static_cast(appliedDistance); - goal.orbitU = std::atan2(local.y, local.x); - goal.orbitV = std::asin(std::clamp(local.z, -1.0, 1.0)); - - const hlsl::float64_t3 spherePosition( - std::cos(goal.orbitV) * std::cos(goal.orbitU) * static_cast(appliedDistance), - std::cos(goal.orbitV) * std::sin(goal.orbitU) * static_cast(appliedDistance), - std::sin(goal.orbitV) * static_cast(appliedDistance)); - - goal.position = goal.targetPosition + spherePosition; goal.hasDistance = true; - goal.distance = appliedDistance; + goal.distance = static_cast(appliedOrbitDistance); goal.hasOrbitState = true; - goal.orbitDistance = appliedDistance; - - const auto forward = hlsl::normalize(-spherePosition); - const hlsl::float64_t3 up = hlsl::normalize(hlsl::float64_t3( - -std::sin(goal.orbitV) * std::cos(goal.orbitU), - -std::sin(goal.orbitV) * std::sin(goal.orbitU), - std::cos(goal.orbitV))); - const hlsl::float64_t3 right = hlsl::normalize(hlsl::cross(up, forward)); - goal.orientation = hlsl::makeQuaternionFromBasis(right, up, forward); + goal.orbitDistance = static_cast(appliedOrbitDistance); return true; } @@ -111,25 +94,11 @@ inline CCameraGoal canonicalizeGoal(CCameraGoal goal) return goal; } -template -inline bool isFiniteVec3(const Vec& v) -{ - return std::isfinite(v.x) && std::isfinite(v.y) && std::isfinite(v.z); -} - -template -inline bool nearlyEqualVec3(const VecA& a, const VecB& b, const double epsilon) -{ - return std::abs(static_cast(a.x - b.x)) <= epsilon && - std::abs(static_cast(a.y - b.y)) <= epsilon && - std::abs(static_cast(a.z - b.z)) <= epsilon; -} - inline bool isGoalFinite(const CCameraGoal& goal) { - if (!isFiniteVec3(goal.position) || !hlsl::isFiniteQuaternion(goal.orientation)) + if (!hlsl::isFiniteVec3(goal.position) || !hlsl::isFiniteQuaternion(goal.orientation)) return false; - if (goal.hasTargetPosition && !isFiniteVec3(goal.targetPosition)) + if (goal.hasTargetPosition && !hlsl::isFiniteVec3(goal.targetPosition)) return false; if (goal.hasDistance && !std::isfinite(goal.distance)) return false; @@ -146,68 +115,55 @@ inline bool isGoalFinite(const CCameraGoal& goal) inline bool compareGoals(const CCameraGoal& actual, const CCameraGoal& expected, const double posEps, const double rotEpsDeg, const double scalarEps) { - auto angleDiffRad = [](double a, double b) -> double - { - constexpr double Pi = 3.14159265358979323846; - constexpr double TwoPi = 6.28318530717958647692; - double d = std::fmod(a - b + Pi, TwoPi); - if (d < 0.0) - d += TwoPi; - return std::abs(d - Pi); - }; - const auto currentOrientation = hlsl::normalizeQuaternion(actual.orientation); const auto expectedOrientation = hlsl::normalizeQuaternion(expected.orientation); - if (!isFiniteVec3(actual.position) || !isFiniteVec3(expected.position) || !hlsl::isFiniteQuaternion(currentOrientation) || !hlsl::isFiniteQuaternion(expectedOrientation)) + if (!hlsl::isFiniteVec3(actual.position) || !hlsl::isFiniteVec3(expected.position) || !hlsl::isFiniteQuaternion(currentOrientation) || !hlsl::isFiniteQuaternion(expectedOrientation)) return false; - const double dx = static_cast(actual.position.x - expected.position.x); - const double dy = static_cast(actual.position.y - expected.position.y); - const double dz = static_cast(actual.position.z - expected.position.z); - const double posDelta = std::sqrt(dx * dx + dy * dy + dz * dz); + const double posDelta = hlsl::length(actual.position - expected.position); const double rotDeltaDeg = hlsl::getQuaternionAngularDistanceDegrees(currentOrientation, expectedOrientation); if (posDelta > posEps || rotDeltaDeg > rotEpsDeg) return false; if (expected.hasTargetPosition) { - if (!actual.hasTargetPosition || !nearlyEqualVec3(actual.targetPosition, expected.targetPosition, scalarEps)) + if (!actual.hasTargetPosition || !hlsl::nearlyEqualVec3(actual.targetPosition, expected.targetPosition, scalarEps)) return false; } if (expected.hasDistance) { - if (!actual.hasDistance || std::abs(static_cast(actual.distance - expected.distance)) > scalarEps) + if (!actual.hasDistance || !hlsl::nearlyEqualScalar(static_cast(actual.distance), static_cast(expected.distance), scalarEps)) return false; } if (expected.hasOrbitState) { if (!actual.hasOrbitState) return false; - if (angleDiffRad(expected.orbitU, actual.orbitU) > rotEpsDeg * (3.14159265358979323846 / 180.0)) + if (hlsl::degrees(hlsl::getWrappedAngleDistanceRadians(expected.orbitU, actual.orbitU)) > rotEpsDeg) return false; - if (angleDiffRad(expected.orbitV, actual.orbitV) > rotEpsDeg * (3.14159265358979323846 / 180.0)) + if (hlsl::degrees(hlsl::getWrappedAngleDistanceRadians(expected.orbitV, actual.orbitV)) > rotEpsDeg) return false; - if (std::abs(static_cast(actual.orbitDistance - expected.orbitDistance)) > scalarEps) + if (!hlsl::nearlyEqualScalar(static_cast(actual.orbitDistance), static_cast(expected.orbitDistance), scalarEps)) return false; } if (expected.hasPathState) { if (!actual.hasPathState) return false; - if (std::abs(hlsl::wrapAngleRad(expected.pathState.angle - actual.pathState.angle)) > rotEpsDeg * (3.14159265358979323846 / 180.0)) + if (hlsl::degrees(hlsl::getWrappedAngleDistanceRadians(expected.pathState.angle, actual.pathState.angle)) > rotEpsDeg) return false; - if (std::abs(actual.pathState.radius - expected.pathState.radius) > scalarEps) + if (!hlsl::nearlyEqualScalar(actual.pathState.radius, expected.pathState.radius, scalarEps)) return false; - if (std::abs(actual.pathState.height - expected.pathState.height) > scalarEps) + if (!hlsl::nearlyEqualScalar(actual.pathState.height, expected.pathState.height, scalarEps)) return false; } if (expected.hasDynamicPerspectiveState) { if (!actual.hasDynamicPerspectiveState) return false; - if (std::abs(static_cast(actual.dynamicPerspectiveState.baseFov - expected.dynamicPerspectiveState.baseFov)) > scalarEps) + if (!hlsl::nearlyEqualScalar(static_cast(actual.dynamicPerspectiveState.baseFov), static_cast(expected.dynamicPerspectiveState.baseFov), scalarEps)) return false; - if (std::abs(static_cast(actual.dynamicPerspectiveState.referenceDistance - expected.dynamicPerspectiveState.referenceDistance)) > scalarEps) + if (!hlsl::nearlyEqualScalar(static_cast(actual.dynamicPerspectiveState.referenceDistance), static_cast(expected.dynamicPerspectiveState.referenceDistance), scalarEps)) return false; } @@ -219,10 +175,7 @@ inline std::string describeGoalMismatch(const CCameraGoal& actual, const CCamera std::ostringstream oss; const auto currentOrientation = hlsl::normalizeQuaternion(actual.orientation); const auto expectedOrientation = hlsl::normalizeQuaternion(expected.orientation); - const double dx = static_cast(actual.position.x - expected.position.x); - const double dy = static_cast(actual.position.y - expected.position.y); - const double dz = static_cast(actual.position.z - expected.position.z); - const double posDelta = std::sqrt(dx * dx + dy * dy + dz * dz); + const double posDelta = hlsl::length(actual.position - expected.position); const double rotDeltaDeg = hlsl::getQuaternionAngularDistanceDegrees(currentOrientation, expectedOrientation); oss << "pos_delta=" << posDelta << " rot_delta_deg=" << rotDeltaDeg diff --git a/common/include/camera/CCameraGoalSolver.hpp b/common/include/camera/CCameraGoalSolver.hpp index 7d0296b82..de4103dac 100644 --- a/common/include/camera/CCameraGoalSolver.hpp +++ b/common/include/camera/CCameraGoalSolver.hpp @@ -271,7 +271,7 @@ class CCameraGoalSolver else { absoluteChanged = absoluteChanged || afterState.distance != beforeDistance; - exact = exact && std::abs(static_cast(afterState.distance - desiredDistance)) <= 1e-6; + exact = exact && std::abs(static_cast(afterState.distance - desiredDistance)) <= ICamera::ScalarTolerance; } } } @@ -300,12 +300,12 @@ class CCameraGoalSolver } else { - const bool pathChanged = !nearlyEqualGoalScalar(beforeState.angle, afterState.angle) || - !nearlyEqualGoalScalar(beforeState.radius, afterState.radius) || - !nearlyEqualGoalScalar(beforeState.height, afterState.height); - const bool pathExact = nearlyEqualGoalScalar(afterState.angle, canonicalTarget.pathState.angle) && - nearlyEqualGoalScalar(afterState.radius, canonicalTarget.pathState.radius) && - nearlyEqualGoalScalar(afterState.height, canonicalTarget.pathState.height); + const bool pathChanged = !hlsl::nearlyEqualScalar(beforeState.angle, afterState.angle, static_cast(ICamera::ScalarTolerance)) || + !hlsl::nearlyEqualScalar(beforeState.radius, afterState.radius, static_cast(ICamera::ScalarTolerance)) || + !hlsl::nearlyEqualScalar(beforeState.height, afterState.height, static_cast(ICamera::ScalarTolerance)); + const bool pathExact = hlsl::nearlyEqualScalar(afterState.angle, canonicalTarget.pathState.angle, static_cast(ICamera::ScalarTolerance)) && + hlsl::nearlyEqualScalar(afterState.radius, canonicalTarget.pathState.radius, static_cast(ICamera::ScalarTolerance)) && + hlsl::nearlyEqualScalar(afterState.height, canonicalTarget.pathState.height, static_cast(ICamera::ScalarTolerance)); absoluteChanged = absoluteChanged || pathChanged; exact = exact && pathExact; @@ -336,10 +336,10 @@ class CCameraGoalSolver } else { - const bool dynamicChanged = !nearlyEqualGoalScalar(beforeState.baseFov, afterState.baseFov) || - !nearlyEqualGoalScalar(beforeState.referenceDistance, afterState.referenceDistance); - const bool dynamicExact = nearlyEqualGoalScalar(afterState.baseFov, canonicalTarget.dynamicPerspectiveState.baseFov) && - nearlyEqualGoalScalar(afterState.referenceDistance, canonicalTarget.dynamicPerspectiveState.referenceDistance); + const bool dynamicChanged = !hlsl::nearlyEqualScalar(beforeState.baseFov, afterState.baseFov, static_cast(ICamera::ScalarTolerance)) || + !hlsl::nearlyEqualScalar(beforeState.referenceDistance, afterState.referenceDistance, static_cast(ICamera::ScalarTolerance)); + const bool dynamicExact = hlsl::nearlyEqualScalar(afterState.baseFov, canonicalTarget.dynamicPerspectiveState.baseFov, static_cast(ICamera::ScalarTolerance)) && + hlsl::nearlyEqualScalar(afterState.referenceDistance, canonicalTarget.dynamicPerspectiveState.referenceDistance, static_cast(ICamera::ScalarTolerance)); absoluteChanged = absoluteChanged || dynamicChanged; exact = exact && dynamicExact; @@ -388,9 +388,6 @@ class CCameraGoalSolver } private: - static constexpr double Pi = 3.14159265358979323846; - static constexpr double HalfPi = 0.5 * Pi; - struct SSphericalGoal { hlsl::float64_t3 target = hlsl::float64_t3(0.0); @@ -402,17 +399,33 @@ class CCameraGoalSolver inline void appendSignedEvent(std::vector& events, double value, CVirtualGimbalEvent::VirtualEventType positive, CVirtualGimbalEvent::VirtualEventType negative) const { - if (value == 0.0) + if (!std::isfinite(value) || std::abs(value) <= ICamera::TinyScalarEpsilon) return; auto& ev = events.emplace_back(); ev.type = (value > 0.0) ? positive : negative; ev.magnitude = std::abs(value); } + inline void appendScalarDeltaEvent(std::vector& events, const double delta, const double denominator, + const double tolerance, CVirtualGimbalEvent::VirtualEventType positive, CVirtualGimbalEvent::VirtualEventType negative) const + { + if (!std::isfinite(delta) || std::abs(delta) <= tolerance) + return; + appendSignedEvent(events, delta / denominator, positive, negative); + } + + inline void appendAngularDeltaEvent(std::vector& events, const double deltaRadians, const double denominator, + const double toleranceDeg, CVirtualGimbalEvent::VirtualEventType positive, CVirtualGimbalEvent::VirtualEventType negative) const + { + if (!std::isfinite(deltaRadians) || std::abs(hlsl::degrees(deltaRadians)) <= toleranceDeg) + return; + appendSignedEvent(events, deltaRadians / denominator, positive, negative); + } + inline double getMoveMagnitudeDenominator(const ICamera* camera) const { const double moveScale = camera->getMoveSpeedScale(); - return 0.01 * (moveScale == 0.0 ? 1.0 : moveScale); + return ICamera::VirtualTranslationStep * (moveScale == 0.0 ? 1.0 : moveScale); } inline double getRotationMagnitudeDenominator(const ICamera* camera) const @@ -424,24 +437,13 @@ class CCameraGoalSolver inline std::pair computePitchYawFromOrientation(const hlsl::camera_quaternion_t& orientation) const { const auto mat = hlsl::getQuaternionBasisMatrix(orientation); - const auto forward = hlsl::float64_t3(mat[2][0], mat[2][1], mat[2][2]); - const double pitch = std::atan2(std::sqrt(forward.x * forward.x + forward.z * forward.z), forward.y) - HalfPi; - const double yaw = std::atan2(forward.x, forward.z); - return { pitch, yaw }; + const auto pitchYaw = hlsl::getPitchYawFromForwardVector(hlsl::float64_t3(mat[2][0], mat[2][1], mat[2][2])); + return { pitchYaw.x, pitchYaw.y }; } inline hlsl::float64_t3 extractYawPitchRollYXZ(const hlsl::camera_quaternion_t& delta) const { - const auto m = hlsl::getMatrix3x3As4x4(hlsl::getQuaternionBasisMatrix(delta)); - const double yaw = std::atan2(static_cast(m[2][0]), static_cast(m[2][2])); - const double c2 = std::sqrt(static_cast(m[0][1] * m[0][1] + m[1][1] * m[1][1])); - const double pitch = std::atan2(-static_cast(m[2][1]), c2); - const double s1 = std::sin(yaw); - const double c1 = std::cos(yaw); - const double roll = std::atan2( - s1 * static_cast(m[1][2]) - c1 * static_cast(m[1][0]), - c1 * static_cast(m[0][0]) - s1 * static_cast(m[0][2])); - return hlsl::float64_t3(pitch, yaw, roll); + return hlsl::getQuaternionEulerRadiansYXZ(delta); } inline bool computePoseMismatch(ICamera* camera, const CCameraGoal& target, double& outPositionDelta, double& outRotationDeltaDeg) const @@ -456,10 +458,7 @@ class CCameraGoalSolver const auto currentOrientation = hlsl::normalizeQuaternion(gimbal.getOrientation()); const auto targetOrientation = hlsl::normalizeQuaternion(target.orientation); - const double dx = static_cast(currentPos.x - target.position.x); - const double dy = static_cast(currentPos.y - target.position.y); - const double dz = static_cast(currentPos.z - target.position.z); - outPositionDelta = std::sqrt(dx * dx + dy * dy + dz * dz); + outPositionDelta = hlsl::length(currentPos - target.position); outRotationDeltaDeg = hlsl::getQuaternionAngularDistanceDegrees(currentOrientation, targetOrientation); return std::isfinite(outPositionDelta) && std::isfinite(outRotationDeltaDeg); @@ -486,7 +485,7 @@ class CCameraGoalSolver if (!computePoseMismatch(camera, target, beforePosDelta, beforeRotDeltaDeg)) return false; - if (beforePosDelta <= 1e-6 && beforeRotDeltaDeg <= 0.1) + if (beforePosDelta <= ICamera::DefaultPositionTolerance && beforeRotDeltaDeg <= ICamera::DefaultAngularToleranceDeg) { outExact = true; return true; @@ -502,29 +501,29 @@ class CCameraGoalSolver if (!computePoseMismatch(camera, target, afterPosDelta, afterRotDeltaDeg)) return false; - outChanged = (std::abs(afterPosDelta - beforePosDelta) > 1e-9) || (std::abs(afterRotDeltaDeg - beforeRotDeltaDeg) > 1e-9); - outExact = afterPosDelta <= 1e-6 && afterRotDeltaDeg <= 0.1; + outChanged = (std::abs(afterPosDelta - beforePosDelta) > ICamera::TinyScalarEpsilon) || + (std::abs(afterRotDeltaDeg - beforeRotDeltaDeg) > ICamera::TinyScalarEpsilon); + outExact = afterPosDelta <= ICamera::DefaultPositionTolerance && afterRotDeltaDeg <= ICamera::DefaultAngularToleranceDeg; return true; } inline bool computeOrbitStateFromPositionTarget(const hlsl::float64_t3& position, const hlsl::float64_t3& target, double& outU, double& outV, float& outDistance, float minDistance, float maxDistance) const { - const auto localSpherePosition = position - target; - const double dist = hlsl::length(localSpherePosition); - if (!std::isfinite(dist)) - return false; - - const double clamped = std::clamp(dist, static_cast(minDistance), static_cast(maxDistance)); - outDistance = static_cast(clamped); - - if (clamped > 0.0) + hlsl::float64_t clampedDistance = static_cast(outDistance); + if (!hlsl::tryBuildOrbitFromPosition( + target, + position, + static_cast(minDistance), + static_cast(maxDistance), + outU, + outV, + clampedDistance)) { - const auto localUnit = localSpherePosition / clamped; - outU = std::atan2(localUnit.y, localUnit.x); - outV = std::asin(localUnit.z); + return false; } + outDistance = static_cast(clampedDistance); return true; } @@ -557,9 +556,9 @@ class CCameraGoalSolver inline bool buildOrbitTranslateEvents(ICamera* camera, const ICamera::SphericalTargetState& sphericalState, const SSphericalGoal& goal, std::vector& out) const { const double moveDenom = getMoveMagnitudeDenominator(camera); - appendSignedEvent(out, hlsl::wrapAngleRad(goal.v - sphericalState.v) / moveDenom, CVirtualGimbalEvent::MoveRight, CVirtualGimbalEvent::MoveLeft); - appendSignedEvent(out, hlsl::wrapAngleRad(goal.u - sphericalState.u) / moveDenom, CVirtualGimbalEvent::MoveUp, CVirtualGimbalEvent::MoveDown); - appendSignedEvent(out, static_cast(goal.distance - sphericalState.distance) / 0.01, CVirtualGimbalEvent::MoveForward, CVirtualGimbalEvent::MoveBackward); + appendAngularDeltaEvent(out, hlsl::wrapAngleRad(goal.v - sphericalState.v), moveDenom, ICamera::DefaultAngularToleranceDeg, CVirtualGimbalEvent::MoveRight, CVirtualGimbalEvent::MoveLeft); + appendAngularDeltaEvent(out, hlsl::wrapAngleRad(goal.u - sphericalState.u), moveDenom, ICamera::DefaultAngularToleranceDeg, CVirtualGimbalEvent::MoveUp, CVirtualGimbalEvent::MoveDown); + appendScalarDeltaEvent(out, static_cast(goal.distance - sphericalState.distance), ICamera::VirtualTranslationStep, ICamera::ScalarTolerance, CVirtualGimbalEvent::MoveForward, CVirtualGimbalEvent::MoveBackward); return !out.empty(); } @@ -569,11 +568,11 @@ class CCameraGoalSolver { const double rotationDenom = getRotationMagnitudeDenominator(camera); if (allowYaw) - appendSignedEvent(out, hlsl::wrapAngleRad(goal.u - sphericalState.u) / rotationDenom, CVirtualGimbalEvent::PanRight, CVirtualGimbalEvent::PanLeft); + appendAngularDeltaEvent(out, hlsl::wrapAngleRad(goal.u - sphericalState.u), rotationDenom, ICamera::DefaultAngularToleranceDeg, CVirtualGimbalEvent::PanRight, CVirtualGimbalEvent::PanLeft); if (allowPitch) - appendSignedEvent(out, hlsl::wrapAngleRad(goal.v - sphericalState.v) / rotationDenom, CVirtualGimbalEvent::TiltUp, CVirtualGimbalEvent::TiltDown); + appendAngularDeltaEvent(out, hlsl::wrapAngleRad(goal.v - sphericalState.v), rotationDenom, ICamera::DefaultAngularToleranceDeg, CVirtualGimbalEvent::TiltUp, CVirtualGimbalEvent::TiltDown); if (distancePositive != CVirtualGimbalEvent::None && distanceNegative != CVirtualGimbalEvent::None) - appendSignedEvent(out, static_cast(goal.distance - sphericalState.distance) / 0.01, distancePositive, distanceNegative); + appendScalarDeltaEvent(out, static_cast(goal.distance - sphericalState.distance), ICamera::VirtualTranslationStep, ICamera::ScalarTolerance, distancePositive, distanceNegative); return !out.empty(); } @@ -583,20 +582,23 @@ class CCameraGoalSolver return false; const auto effectiveTarget = target.hasTargetPosition ? target.targetPosition : sphericalState.target; - const auto currentOffset = camera->getGimbal().getPosition() - effectiveTarget; - const auto desiredOffset = target.position - effectiveTarget; - - const double currentAngle = std::atan2(currentOffset.z, currentOffset.x); - const double desiredAngle = std::atan2(desiredOffset.z, desiredOffset.x); - const double currentRadius = std::sqrt(currentOffset.x * currentOffset.x + currentOffset.z * currentOffset.z); - const double desiredRadius = std::sqrt(desiredOffset.x * desiredOffset.x + desiredOffset.z * desiredOffset.z); - const double currentHeight = currentOffset.y; - const double desiredHeight = desiredOffset.y; + double currentAngle = 0.0; + double desiredAngle = 0.0; + double currentRadius = 0.0; + double desiredRadius = 0.0; + double currentHeight = 0.0; + double desiredHeight = 0.0; + constexpr double MinPathRadius = static_cast(ICamera::SphericalMinDistance); + + if (!hlsl::tryBuildPathStateFromPosition(effectiveTarget, camera->getGimbal().getPosition(), MinPathRadius, currentAngle, currentRadius, currentHeight)) + return false; + if (!hlsl::tryBuildPathStateFromPosition(effectiveTarget, target.position, MinPathRadius, desiredAngle, desiredRadius, desiredHeight)) + return false; const double moveDenom = getMoveMagnitudeDenominator(camera); - appendSignedEvent(out, (desiredRadius - currentRadius) / moveDenom, CVirtualGimbalEvent::MoveRight, CVirtualGimbalEvent::MoveLeft); - appendSignedEvent(out, (desiredHeight - currentHeight) / moveDenom, CVirtualGimbalEvent::MoveUp, CVirtualGimbalEvent::MoveDown); - appendSignedEvent(out, hlsl::wrapAngleRad(desiredAngle - currentAngle) / moveDenom, CVirtualGimbalEvent::MoveForward, CVirtualGimbalEvent::MoveBackward); + appendScalarDeltaEvent(out, desiredRadius - currentRadius, moveDenom, ICamera::ScalarTolerance, CVirtualGimbalEvent::MoveRight, CVirtualGimbalEvent::MoveLeft); + appendScalarDeltaEvent(out, desiredHeight - currentHeight, moveDenom, ICamera::ScalarTolerance, CVirtualGimbalEvent::MoveUp, CVirtualGimbalEvent::MoveDown); + appendAngularDeltaEvent(out, hlsl::wrapAngleRad(desiredAngle - currentAngle), moveDenom, ICamera::DefaultAngularToleranceDeg, CVirtualGimbalEvent::MoveForward, CVirtualGimbalEvent::MoveBackward); return !out.empty(); } @@ -654,9 +656,9 @@ class CCameraGoalSolver hlsl::dot(deltaWorld, up), hlsl::dot(deltaWorld, forward)); - appendSignedEvent(out, localDelta.x, CVirtualGimbalEvent::MoveRight, CVirtualGimbalEvent::MoveLeft); - appendSignedEvent(out, localDelta.y, CVirtualGimbalEvent::MoveUp, CVirtualGimbalEvent::MoveDown); - appendSignedEvent(out, localDelta.z, CVirtualGimbalEvent::MoveForward, CVirtualGimbalEvent::MoveBackward); + appendScalarDeltaEvent(out, localDelta.x, 1.0, ICamera::ScalarTolerance, CVirtualGimbalEvent::MoveRight, CVirtualGimbalEvent::MoveLeft); + appendScalarDeltaEvent(out, localDelta.y, 1.0, ICamera::ScalarTolerance, CVirtualGimbalEvent::MoveUp, CVirtualGimbalEvent::MoveDown); + appendScalarDeltaEvent(out, localDelta.z, 1.0, ICamera::ScalarTolerance, CVirtualGimbalEvent::MoveForward, CVirtualGimbalEvent::MoveBackward); switch (camera->getKind()) { @@ -671,8 +673,8 @@ class CCameraGoalSolver const double deltaPitch = hlsl::wrapAngleRad(tgtPitch - curPitch) * invScale; const double deltaYaw = hlsl::wrapAngleRad(tgtYaw - curYaw) * invScale; - appendSignedEvent(out, deltaPitch, CVirtualGimbalEvent::TiltUp, CVirtualGimbalEvent::TiltDown); - appendSignedEvent(out, deltaYaw, CVirtualGimbalEvent::PanRight, CVirtualGimbalEvent::PanLeft); + appendAngularDeltaEvent(out, deltaPitch, 1.0, ICamera::DefaultAngularToleranceDeg, CVirtualGimbalEvent::TiltUp, CVirtualGimbalEvent::TiltDown); + appendAngularDeltaEvent(out, deltaYaw, 1.0, ICamera::DefaultAngularToleranceDeg, CVirtualGimbalEvent::PanRight, CVirtualGimbalEvent::PanLeft); } break; case ICamera::CameraKind::Free: @@ -680,9 +682,9 @@ class CCameraGoalSolver const auto deltaQuat = hlsl::inverseQuaternion(gimbal.getOrientation()) * hlsl::normalizeQuaternion(target.orientation); const auto angles = extractYawPitchRollYXZ(deltaQuat); - appendSignedEvent(out, angles.x, CVirtualGimbalEvent::TiltUp, CVirtualGimbalEvent::TiltDown); - appendSignedEvent(out, angles.y, CVirtualGimbalEvent::PanRight, CVirtualGimbalEvent::PanLeft); - appendSignedEvent(out, angles.z, CVirtualGimbalEvent::RollRight, CVirtualGimbalEvent::RollLeft); + appendAngularDeltaEvent(out, angles.x, 1.0, ICamera::DefaultAngularToleranceDeg, CVirtualGimbalEvent::TiltUp, CVirtualGimbalEvent::TiltDown); + appendAngularDeltaEvent(out, angles.y, 1.0, ICamera::DefaultAngularToleranceDeg, CVirtualGimbalEvent::PanRight, CVirtualGimbalEvent::PanLeft); + appendAngularDeltaEvent(out, angles.z, 1.0, ICamera::DefaultAngularToleranceDeg, CVirtualGimbalEvent::RollRight, CVirtualGimbalEvent::RollLeft); } break; default: diff --git a/common/include/camera/CCameraInputBindingUtilities.hpp b/common/include/camera/CCameraInputBindingUtilities.hpp index 3b91d6c89..01d341c6a 100644 --- a/common/include/camera/CCameraInputBindingUtilities.hpp +++ b/common/include/camera/CCameraInputBindingUtilities.hpp @@ -36,6 +36,80 @@ inline IGimbalBindingLayout::mouse_to_virtual_events_t makeMousePreset( return preset; } +inline IGimbalBindingLayout::keyboard_to_virtual_events_t extendKeyboardPreset( + IGimbalBindingLayout::keyboard_to_virtual_events_t preset, + std::initializer_list> bindings) +{ + for (const auto& [code, event] : bindings) + preset.emplace(code, IGimbalBindingLayout::CHashInfo(event)); + return preset; +} + +inline IGimbalBindingLayout::mouse_to_virtual_events_t extendMousePreset( + IGimbalBindingLayout::mouse_to_virtual_events_t preset, + std::initializer_list> bindings) +{ + for (const auto& [code, event] : bindings) + preset.emplace(code, IGimbalBindingLayout::CHashInfo(event)); + return preset; +} + +inline IGimbalBindingLayout::keyboard_to_virtual_events_t makeWasdKeyboardPreset( + const core::CVirtualGimbalEvent::VirtualEventType w, + const core::CVirtualGimbalEvent::VirtualEventType s, + const core::CVirtualGimbalEvent::VirtualEventType a, + const core::CVirtualGimbalEvent::VirtualEventType d) +{ + return makeKeyboardPreset({ + { ui::E_KEY_CODE::EKC_W, w }, + { ui::E_KEY_CODE::EKC_S, s }, + { ui::E_KEY_CODE::EKC_A, a }, + { ui::E_KEY_CODE::EKC_D, d } + }); +} + +inline IGimbalBindingLayout::keyboard_to_virtual_events_t appendIjklLookKeyboardPreset( + IGimbalBindingLayout::keyboard_to_virtual_events_t preset, + const core::CVirtualGimbalEvent::VirtualEventType i, + const core::CVirtualGimbalEvent::VirtualEventType k, + const core::CVirtualGimbalEvent::VirtualEventType j, + const core::CVirtualGimbalEvent::VirtualEventType l) +{ + return extendKeyboardPreset(std::move(preset), { + { ui::E_KEY_CODE::EKC_I, i }, + { ui::E_KEY_CODE::EKC_K, k }, + { ui::E_KEY_CODE::EKC_J, j }, + { ui::E_KEY_CODE::EKC_L, l } + }); +} + +inline IGimbalBindingLayout::mouse_to_virtual_events_t makeRelativeMousePreset( + const core::CVirtualGimbalEvent::VirtualEventType positiveX, + const core::CVirtualGimbalEvent::VirtualEventType negativeX, + const core::CVirtualGimbalEvent::VirtualEventType positiveY, + const core::CVirtualGimbalEvent::VirtualEventType negativeY) +{ + return makeMousePreset({ + { ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_X, positiveX }, + { ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_X, negativeX }, + { ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_Y, positiveY }, + { ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_Y, negativeY } + }); +} + +inline IGimbalBindingLayout::mouse_to_virtual_events_t appendSymmetricScrollPreset( + IGimbalBindingLayout::mouse_to_virtual_events_t preset, + const core::CVirtualGimbalEvent::VirtualEventType positive, + const core::CVirtualGimbalEvent::VirtualEventType negative) +{ + return extendMousePreset(std::move(preset), { + { ui::E_MOUSE_CODE::EMC_VERTICAL_POSITIVE_SCROLL, positive }, + { ui::E_MOUSE_CODE::EMC_HORIZONTAL_POSITIVE_SCROLL, positive }, + { ui::E_MOUSE_CODE::EMC_VERTICAL_NEGATIVE_SCROLL, negative }, + { ui::E_MOUSE_CODE::EMC_HORIZONTAL_NEGATIVE_SCROLL, negative } + }); +} + inline IGimbalBindingLayout::imguizmo_to_virtual_events_t makeImguizmoPreset(const uint32_t allowedVirtualEvents) { IGimbalBindingLayout::imguizmo_to_virtual_events_t preset; @@ -64,30 +138,22 @@ inline const IGimbalBindingLayout::mouse_to_virtual_events_t& emptyMousePreset() inline const IGimbalBindingLayout::keyboard_to_virtual_events_t& fpsKeyboardPreset() { - static const auto preset = makeKeyboardPreset({ - { ui::E_KEY_CODE::EKC_W, core::CVirtualGimbalEvent::MoveForward }, - { ui::E_KEY_CODE::EKC_S, core::CVirtualGimbalEvent::MoveBackward }, - { ui::E_KEY_CODE::EKC_A, core::CVirtualGimbalEvent::MoveLeft }, - { ui::E_KEY_CODE::EKC_D, core::CVirtualGimbalEvent::MoveRight }, - { ui::E_KEY_CODE::EKC_I, core::CVirtualGimbalEvent::TiltDown }, - { ui::E_KEY_CODE::EKC_K, core::CVirtualGimbalEvent::TiltUp }, - { ui::E_KEY_CODE::EKC_J, core::CVirtualGimbalEvent::PanLeft }, - { ui::E_KEY_CODE::EKC_L, core::CVirtualGimbalEvent::PanRight } - }); + static const auto preset = appendIjklLookKeyboardPreset( + makeWasdKeyboardPreset( + core::CVirtualGimbalEvent::MoveForward, + core::CVirtualGimbalEvent::MoveBackward, + core::CVirtualGimbalEvent::MoveLeft, + core::CVirtualGimbalEvent::MoveRight), + core::CVirtualGimbalEvent::TiltDown, + core::CVirtualGimbalEvent::TiltUp, + core::CVirtualGimbalEvent::PanLeft, + core::CVirtualGimbalEvent::PanRight); return preset; } inline const IGimbalBindingLayout::keyboard_to_virtual_events_t& freeKeyboardPreset() { - static const auto preset = makeKeyboardPreset({ - { ui::E_KEY_CODE::EKC_W, core::CVirtualGimbalEvent::MoveForward }, - { ui::E_KEY_CODE::EKC_S, core::CVirtualGimbalEvent::MoveBackward }, - { ui::E_KEY_CODE::EKC_A, core::CVirtualGimbalEvent::MoveLeft }, - { ui::E_KEY_CODE::EKC_D, core::CVirtualGimbalEvent::MoveRight }, - { ui::E_KEY_CODE::EKC_I, core::CVirtualGimbalEvent::TiltDown }, - { ui::E_KEY_CODE::EKC_K, core::CVirtualGimbalEvent::TiltUp }, - { ui::E_KEY_CODE::EKC_J, core::CVirtualGimbalEvent::PanLeft }, - { ui::E_KEY_CODE::EKC_L, core::CVirtualGimbalEvent::PanRight }, + static const auto preset = extendKeyboardPreset(fpsKeyboardPreset(), { { ui::E_KEY_CODE::EKC_Q, core::CVirtualGimbalEvent::RollLeft }, { ui::E_KEY_CODE::EKC_E, core::CVirtualGimbalEvent::RollRight } }); @@ -96,74 +162,83 @@ inline const IGimbalBindingLayout::keyboard_to_virtual_events_t& freeKeyboardPre inline const IGimbalBindingLayout::keyboard_to_virtual_events_t& orbitKeyboardPreset() { - static const auto preset = makeKeyboardPreset({ - { ui::E_KEY_CODE::EKC_W, core::CVirtualGimbalEvent::MoveUp }, - { ui::E_KEY_CODE::EKC_S, core::CVirtualGimbalEvent::MoveDown }, - { ui::E_KEY_CODE::EKC_A, core::CVirtualGimbalEvent::MoveLeft }, - { ui::E_KEY_CODE::EKC_D, core::CVirtualGimbalEvent::MoveRight }, + static const auto preset = extendKeyboardPreset( + makeWasdKeyboardPreset( + core::CVirtualGimbalEvent::MoveUp, + core::CVirtualGimbalEvent::MoveDown, + core::CVirtualGimbalEvent::MoveLeft, + core::CVirtualGimbalEvent::MoveRight), + { { ui::E_KEY_CODE::EKC_E, core::CVirtualGimbalEvent::MoveForward }, { ui::E_KEY_CODE::EKC_Q, core::CVirtualGimbalEvent::MoveBackward } - }); + }); return preset; } inline const IGimbalBindingLayout::keyboard_to_virtual_events_t& arcballKeyboardPreset() { - static const auto preset = makeKeyboardPreset({ - { ui::E_KEY_CODE::EKC_W, core::CVirtualGimbalEvent::MoveForward }, - { ui::E_KEY_CODE::EKC_S, core::CVirtualGimbalEvent::MoveBackward }, - { ui::E_KEY_CODE::EKC_A, core::CVirtualGimbalEvent::MoveLeft }, - { ui::E_KEY_CODE::EKC_D, core::CVirtualGimbalEvent::MoveRight }, + static const auto preset = appendIjklLookKeyboardPreset( + extendKeyboardPreset( + makeWasdKeyboardPreset( + core::CVirtualGimbalEvent::MoveForward, + core::CVirtualGimbalEvent::MoveBackward, + core::CVirtualGimbalEvent::MoveLeft, + core::CVirtualGimbalEvent::MoveRight), + { { ui::E_KEY_CODE::EKC_Q, core::CVirtualGimbalEvent::MoveDown }, { ui::E_KEY_CODE::EKC_E, core::CVirtualGimbalEvent::MoveUp }, - { ui::E_KEY_CODE::EKC_I, core::CVirtualGimbalEvent::TiltDown }, - { ui::E_KEY_CODE::EKC_K, core::CVirtualGimbalEvent::TiltUp }, - { ui::E_KEY_CODE::EKC_J, core::CVirtualGimbalEvent::PanLeft }, - { ui::E_KEY_CODE::EKC_L, core::CVirtualGimbalEvent::PanRight } - }); + }), + core::CVirtualGimbalEvent::TiltDown, + core::CVirtualGimbalEvent::TiltUp, + core::CVirtualGimbalEvent::PanLeft, + core::CVirtualGimbalEvent::PanRight); return preset; } inline const IGimbalBindingLayout::keyboard_to_virtual_events_t& turntableKeyboardPreset() { - static const auto preset = makeKeyboardPreset({ - { ui::E_KEY_CODE::EKC_W, core::CVirtualGimbalEvent::MoveForward }, - { ui::E_KEY_CODE::EKC_S, core::CVirtualGimbalEvent::MoveBackward }, - { ui::E_KEY_CODE::EKC_A, core::CVirtualGimbalEvent::PanLeft }, - { ui::E_KEY_CODE::EKC_D, core::CVirtualGimbalEvent::PanRight }, - { ui::E_KEY_CODE::EKC_I, core::CVirtualGimbalEvent::TiltDown }, - { ui::E_KEY_CODE::EKC_K, core::CVirtualGimbalEvent::TiltUp }, - { ui::E_KEY_CODE::EKC_J, core::CVirtualGimbalEvent::PanLeft }, - { ui::E_KEY_CODE::EKC_L, core::CVirtualGimbalEvent::PanRight } - }); + static const auto preset = appendIjklLookKeyboardPreset( + makeWasdKeyboardPreset( + core::CVirtualGimbalEvent::MoveForward, + core::CVirtualGimbalEvent::MoveBackward, + core::CVirtualGimbalEvent::PanLeft, + core::CVirtualGimbalEvent::PanRight), + core::CVirtualGimbalEvent::TiltDown, + core::CVirtualGimbalEvent::TiltUp, + core::CVirtualGimbalEvent::PanLeft, + core::CVirtualGimbalEvent::PanRight); return preset; } inline const IGimbalBindingLayout::keyboard_to_virtual_events_t& topDownKeyboardPreset() { - static const auto preset = makeKeyboardPreset({ - { ui::E_KEY_CODE::EKC_W, core::CVirtualGimbalEvent::MoveUp }, - { ui::E_KEY_CODE::EKC_S, core::CVirtualGimbalEvent::MoveDown }, - { ui::E_KEY_CODE::EKC_A, core::CVirtualGimbalEvent::MoveLeft }, - { ui::E_KEY_CODE::EKC_D, core::CVirtualGimbalEvent::MoveRight }, + static const auto preset = extendKeyboardPreset( + makeWasdKeyboardPreset( + core::CVirtualGimbalEvent::MoveUp, + core::CVirtualGimbalEvent::MoveDown, + core::CVirtualGimbalEvent::MoveLeft, + core::CVirtualGimbalEvent::MoveRight), + { { ui::E_KEY_CODE::EKC_Q, core::CVirtualGimbalEvent::MoveBackward }, { ui::E_KEY_CODE::EKC_E, core::CVirtualGimbalEvent::MoveForward }, { ui::E_KEY_CODE::EKC_J, core::CVirtualGimbalEvent::PanLeft }, { ui::E_KEY_CODE::EKC_L, core::CVirtualGimbalEvent::PanRight } - }); + }); return preset; } inline const IGimbalBindingLayout::keyboard_to_virtual_events_t& isometricKeyboardPreset() { - static const auto preset = makeKeyboardPreset({ - { ui::E_KEY_CODE::EKC_W, core::CVirtualGimbalEvent::MoveUp }, - { ui::E_KEY_CODE::EKC_S, core::CVirtualGimbalEvent::MoveDown }, - { ui::E_KEY_CODE::EKC_A, core::CVirtualGimbalEvent::MoveLeft }, - { ui::E_KEY_CODE::EKC_D, core::CVirtualGimbalEvent::MoveRight }, + static const auto preset = extendKeyboardPreset( + makeWasdKeyboardPreset( + core::CVirtualGimbalEvent::MoveUp, + core::CVirtualGimbalEvent::MoveDown, + core::CVirtualGimbalEvent::MoveLeft, + core::CVirtualGimbalEvent::MoveRight), + { { ui::E_KEY_CODE::EKC_Q, core::CVirtualGimbalEvent::MoveBackward }, { ui::E_KEY_CODE::EKC_E, core::CVirtualGimbalEvent::MoveForward } - }); + }); return preset; } @@ -179,38 +254,41 @@ inline const IGimbalBindingLayout::keyboard_to_virtual_events_t& dollyKeyboardPr inline const IGimbalBindingLayout::keyboard_to_virtual_events_t& dollyZoomKeyboardPreset() { - static const auto preset = makeKeyboardPreset({ - { ui::E_KEY_CODE::EKC_W, core::CVirtualGimbalEvent::MoveUp }, - { ui::E_KEY_CODE::EKC_S, core::CVirtualGimbalEvent::MoveDown }, - { ui::E_KEY_CODE::EKC_A, core::CVirtualGimbalEvent::MoveLeft }, - { ui::E_KEY_CODE::EKC_D, core::CVirtualGimbalEvent::MoveRight }, + static const auto preset = extendKeyboardPreset( + makeWasdKeyboardPreset( + core::CVirtualGimbalEvent::MoveUp, + core::CVirtualGimbalEvent::MoveDown, + core::CVirtualGimbalEvent::MoveLeft, + core::CVirtualGimbalEvent::MoveRight), + { { ui::E_KEY_CODE::EKC_E, core::CVirtualGimbalEvent::MoveForward }, { ui::E_KEY_CODE::EKC_Q, core::CVirtualGimbalEvent::MoveBackward } - }); + }); return preset; } inline const IGimbalBindingLayout::keyboard_to_virtual_events_t& pathKeyboardPreset() { - static const auto preset = makeKeyboardPreset({ - { ui::E_KEY_CODE::EKC_W, core::CVirtualGimbalEvent::MoveForward }, - { ui::E_KEY_CODE::EKC_S, core::CVirtualGimbalEvent::MoveBackward }, - { ui::E_KEY_CODE::EKC_A, core::CVirtualGimbalEvent::MoveLeft }, - { ui::E_KEY_CODE::EKC_D, core::CVirtualGimbalEvent::MoveRight }, + static const auto preset = extendKeyboardPreset( + makeWasdKeyboardPreset( + core::CVirtualGimbalEvent::MoveForward, + core::CVirtualGimbalEvent::MoveBackward, + core::CVirtualGimbalEvent::MoveLeft, + core::CVirtualGimbalEvent::MoveRight), + { { ui::E_KEY_CODE::EKC_Q, core::CVirtualGimbalEvent::MoveDown }, { ui::E_KEY_CODE::EKC_E, core::CVirtualGimbalEvent::MoveUp } - }); + }); return preset; } inline const IGimbalBindingLayout::mouse_to_virtual_events_t& fpsMousePreset() { - static const auto preset = makeMousePreset({ - { ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_X, core::CVirtualGimbalEvent::PanRight }, - { ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_X, core::CVirtualGimbalEvent::PanLeft }, - { ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_Y, core::CVirtualGimbalEvent::TiltUp }, - { ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_Y, core::CVirtualGimbalEvent::TiltDown } - }); + static const auto preset = makeRelativeMousePreset( + core::CVirtualGimbalEvent::PanRight, + core::CVirtualGimbalEvent::PanLeft, + core::CVirtualGimbalEvent::TiltUp, + core::CVirtualGimbalEvent::TiltDown); return preset; } @@ -221,31 +299,27 @@ inline const IGimbalBindingLayout::mouse_to_virtual_events_t& freeMousePreset() inline const IGimbalBindingLayout::mouse_to_virtual_events_t& orbitMousePreset() { - static const auto preset = makeMousePreset({ - { ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_X, core::CVirtualGimbalEvent::MoveRight }, - { ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_X, core::CVirtualGimbalEvent::MoveLeft }, - { ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_Y, core::CVirtualGimbalEvent::MoveUp }, - { ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_Y, core::CVirtualGimbalEvent::MoveDown }, - { ui::E_MOUSE_CODE::EMC_VERTICAL_POSITIVE_SCROLL, core::CVirtualGimbalEvent::MoveForward }, - { ui::E_MOUSE_CODE::EMC_HORIZONTAL_POSITIVE_SCROLL, core::CVirtualGimbalEvent::MoveForward }, - { ui::E_MOUSE_CODE::EMC_VERTICAL_NEGATIVE_SCROLL, core::CVirtualGimbalEvent::MoveBackward }, - { ui::E_MOUSE_CODE::EMC_HORIZONTAL_NEGATIVE_SCROLL, core::CVirtualGimbalEvent::MoveBackward } - }); + static const auto preset = appendSymmetricScrollPreset( + makeRelativeMousePreset( + core::CVirtualGimbalEvent::MoveRight, + core::CVirtualGimbalEvent::MoveLeft, + core::CVirtualGimbalEvent::MoveUp, + core::CVirtualGimbalEvent::MoveDown), + core::CVirtualGimbalEvent::MoveForward, + core::CVirtualGimbalEvent::MoveBackward); return preset; } inline const IGimbalBindingLayout::mouse_to_virtual_events_t& arcballMousePreset() { - static const auto preset = makeMousePreset({ - { ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_X, core::CVirtualGimbalEvent::PanRight }, - { ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_X, core::CVirtualGimbalEvent::PanLeft }, - { ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_Y, core::CVirtualGimbalEvent::TiltUp }, - { ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_Y, core::CVirtualGimbalEvent::TiltDown }, - { ui::E_MOUSE_CODE::EMC_VERTICAL_POSITIVE_SCROLL, core::CVirtualGimbalEvent::MoveForward }, - { ui::E_MOUSE_CODE::EMC_HORIZONTAL_POSITIVE_SCROLL, core::CVirtualGimbalEvent::MoveForward }, - { ui::E_MOUSE_CODE::EMC_VERTICAL_NEGATIVE_SCROLL, core::CVirtualGimbalEvent::MoveBackward }, - { ui::E_MOUSE_CODE::EMC_HORIZONTAL_NEGATIVE_SCROLL, core::CVirtualGimbalEvent::MoveBackward } - }); + static const auto preset = appendSymmetricScrollPreset( + makeRelativeMousePreset( + core::CVirtualGimbalEvent::PanRight, + core::CVirtualGimbalEvent::PanLeft, + core::CVirtualGimbalEvent::TiltUp, + core::CVirtualGimbalEvent::TiltDown), + core::CVirtualGimbalEvent::MoveForward, + core::CVirtualGimbalEvent::MoveBackward); return preset; } @@ -256,61 +330,53 @@ inline const IGimbalBindingLayout::mouse_to_virtual_events_t& turntableMousePres inline const IGimbalBindingLayout::mouse_to_virtual_events_t& topDownMousePreset() { - static const auto preset = makeMousePreset({ - { ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_X, core::CVirtualGimbalEvent::PanRight }, - { ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_X, core::CVirtualGimbalEvent::PanLeft }, - { ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_Y, core::CVirtualGimbalEvent::MoveUp }, - { ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_Y, core::CVirtualGimbalEvent::MoveDown }, - { ui::E_MOUSE_CODE::EMC_VERTICAL_POSITIVE_SCROLL, core::CVirtualGimbalEvent::MoveForward }, - { ui::E_MOUSE_CODE::EMC_HORIZONTAL_POSITIVE_SCROLL, core::CVirtualGimbalEvent::MoveForward }, - { ui::E_MOUSE_CODE::EMC_VERTICAL_NEGATIVE_SCROLL, core::CVirtualGimbalEvent::MoveBackward }, - { ui::E_MOUSE_CODE::EMC_HORIZONTAL_NEGATIVE_SCROLL, core::CVirtualGimbalEvent::MoveBackward } - }); + static const auto preset = appendSymmetricScrollPreset( + makeRelativeMousePreset( + core::CVirtualGimbalEvent::PanRight, + core::CVirtualGimbalEvent::PanLeft, + core::CVirtualGimbalEvent::MoveUp, + core::CVirtualGimbalEvent::MoveDown), + core::CVirtualGimbalEvent::MoveForward, + core::CVirtualGimbalEvent::MoveBackward); return preset; } inline const IGimbalBindingLayout::mouse_to_virtual_events_t& isometricMousePreset() { - static const auto preset = makeMousePreset({ - { ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_X, core::CVirtualGimbalEvent::MoveRight }, - { ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_X, core::CVirtualGimbalEvent::MoveLeft }, - { ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_Y, core::CVirtualGimbalEvent::MoveUp }, - { ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_Y, core::CVirtualGimbalEvent::MoveDown }, - { ui::E_MOUSE_CODE::EMC_VERTICAL_POSITIVE_SCROLL, core::CVirtualGimbalEvent::MoveForward }, - { ui::E_MOUSE_CODE::EMC_HORIZONTAL_POSITIVE_SCROLL, core::CVirtualGimbalEvent::MoveForward }, - { ui::E_MOUSE_CODE::EMC_VERTICAL_NEGATIVE_SCROLL, core::CVirtualGimbalEvent::MoveBackward }, - { ui::E_MOUSE_CODE::EMC_HORIZONTAL_NEGATIVE_SCROLL, core::CVirtualGimbalEvent::MoveBackward } - }); + static const auto preset = appendSymmetricScrollPreset( + makeRelativeMousePreset( + core::CVirtualGimbalEvent::MoveRight, + core::CVirtualGimbalEvent::MoveLeft, + core::CVirtualGimbalEvent::MoveUp, + core::CVirtualGimbalEvent::MoveDown), + core::CVirtualGimbalEvent::MoveForward, + core::CVirtualGimbalEvent::MoveBackward); return preset; } inline const IGimbalBindingLayout::mouse_to_virtual_events_t& chaseMousePreset() { - static const auto preset = makeMousePreset({ - { ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_X, core::CVirtualGimbalEvent::PanRight }, - { ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_X, core::CVirtualGimbalEvent::PanLeft }, - { ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_Y, core::CVirtualGimbalEvent::TiltUp }, - { ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_Y, core::CVirtualGimbalEvent::TiltDown }, - { ui::E_MOUSE_CODE::EMC_VERTICAL_POSITIVE_SCROLL, core::CVirtualGimbalEvent::MoveUp }, - { ui::E_MOUSE_CODE::EMC_HORIZONTAL_POSITIVE_SCROLL, core::CVirtualGimbalEvent::MoveUp }, - { ui::E_MOUSE_CODE::EMC_VERTICAL_NEGATIVE_SCROLL, core::CVirtualGimbalEvent::MoveDown }, - { ui::E_MOUSE_CODE::EMC_HORIZONTAL_NEGATIVE_SCROLL, core::CVirtualGimbalEvent::MoveDown } - }); + static const auto preset = appendSymmetricScrollPreset( + makeRelativeMousePreset( + core::CVirtualGimbalEvent::PanRight, + core::CVirtualGimbalEvent::PanLeft, + core::CVirtualGimbalEvent::TiltUp, + core::CVirtualGimbalEvent::TiltDown), + core::CVirtualGimbalEvent::MoveUp, + core::CVirtualGimbalEvent::MoveDown); return preset; } inline const IGimbalBindingLayout::mouse_to_virtual_events_t& dollyMousePreset() { - static const auto preset = makeMousePreset({ - { ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_X, core::CVirtualGimbalEvent::PanRight }, - { ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_X, core::CVirtualGimbalEvent::PanLeft }, - { ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_Y, core::CVirtualGimbalEvent::TiltUp }, - { ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_Y, core::CVirtualGimbalEvent::TiltDown }, - { ui::E_MOUSE_CODE::EMC_VERTICAL_POSITIVE_SCROLL, core::CVirtualGimbalEvent::MoveForward }, - { ui::E_MOUSE_CODE::EMC_HORIZONTAL_POSITIVE_SCROLL, core::CVirtualGimbalEvent::MoveForward }, - { ui::E_MOUSE_CODE::EMC_VERTICAL_NEGATIVE_SCROLL, core::CVirtualGimbalEvent::MoveBackward }, - { ui::E_MOUSE_CODE::EMC_HORIZONTAL_NEGATIVE_SCROLL, core::CVirtualGimbalEvent::MoveBackward } - }); + static const auto preset = appendSymmetricScrollPreset( + makeRelativeMousePreset( + core::CVirtualGimbalEvent::PanRight, + core::CVirtualGimbalEvent::PanLeft, + core::CVirtualGimbalEvent::TiltUp, + core::CVirtualGimbalEvent::TiltDown), + core::CVirtualGimbalEvent::MoveForward, + core::CVirtualGimbalEvent::MoveBackward); return preset; } diff --git a/common/include/camera/CCameraMathUtilities.hpp b/common/include/camera/CCameraMathUtilities.hpp index a09a7fab9..c2a2aa86e 100644 --- a/common/include/camera/CCameraMathUtilities.hpp +++ b/common/include/camera/CCameraMathUtilities.hpp @@ -6,6 +6,7 @@ #include #include +#include "nbl/builtin/hlsl/numbers.hlsl" #include "nbl/builtin/hlsl/matrix_utils/transformation_matrix_utils.hlsl" namespace nbl::hlsl @@ -15,7 +16,7 @@ namespace nbl::hlsl template inline T wrapAngleRad(T angle) { - constexpr T Pi = static_cast(3.141592653589793238462643383279502884L); + constexpr T Pi = numbers::pi; constexpr T TwoPi = Pi * static_cast(2); angle = std::fmod(angle + Pi, TwoPi); @@ -24,6 +25,30 @@ inline T wrapAngleRad(T angle) return angle - Pi; } +template +inline T getWrappedAngleDistanceRadians(const T a, const T b) +{ + return std::abs(wrapAngleRad(a - b)); +} + +template +inline T getWrappedAngleDistanceDegrees(const T a, const T b) +{ + constexpr T HalfTurn = static_cast(180); + constexpr T FullTurn = static_cast(360); + + T angle = std::fmod(a - b + HalfTurn, FullTurn); + if (angle < static_cast(0)) + angle += FullTurn; + return std::abs(angle - HalfTurn); +} + +template +inline bool isFiniteScalar(const T value) +{ + return std::isfinite(value); +} + template using camera_vector_t = vector; @@ -143,7 +168,7 @@ inline camera_quaternion_t makeQuaternionFromBasis( camera_quaternion_t output = makeIdentityQuaternion(); if (trace > T(0)) { - const T scale = std::sqrt(trace + T(1)); + const T scale = hlsl::sqrt(trace + T(1)); const T invScale = T(0.5) / scale; output.data.x = (m[2][1] - m[1][2]) * invScale; output.data.y = (m[0][2] - m[2][0]) * invScale; @@ -152,7 +177,7 @@ inline camera_quaternion_t makeQuaternionFromBasis( } else if (m00 >= m11 && m00 >= m22) { - const T scale = std::sqrt(T(1) + m00 - m11 - m22); + const T scale = hlsl::sqrt(T(1) + m00 - m11 - m22); const T invScale = T(0.5) / scale; output.data.x = scale * T(0.5); output.data.y = (m[0][1] + m[1][0]) * invScale; @@ -161,7 +186,7 @@ inline camera_quaternion_t makeQuaternionFromBasis( } else if (m11 >= m22) { - const T scale = std::sqrt(T(1) + m11 - m00 - m22); + const T scale = hlsl::sqrt(T(1) + m11 - m00 - m22); const T invScale = T(0.5) / scale; output.data.x = (m[0][1] + m[1][0]) * invScale; output.data.y = scale * T(0.5); @@ -170,7 +195,7 @@ inline camera_quaternion_t makeQuaternionFromBasis( } else { - const T scale = std::sqrt(T(1) + m22 - m00 - m11); + const T scale = hlsl::sqrt(T(1) + m22 - m00 - m11); const T invScale = T(0.5) / scale; output.data.x = (m[2][0] + m[0][2]) * invScale; output.data.y = (m[1][2] + m[2][1]) * invScale; @@ -225,6 +250,295 @@ inline camera_quaternion_t makeQuaternionFromBasis( return normalizeQuaternion(bestCandidate); } +template +inline bool isFiniteVec3(const camera_vector_t& value) +{ + return isFiniteScalar(value.x) && isFiniteScalar(value.y) && isFiniteScalar(value.z); +} + +template +inline bool nearlyEqualScalar(const T a, const T b, const T epsilon) +{ + return std::abs(a - b) <= epsilon; +} + +template +inline bool nearlyEqualVec3(const VecA& a, const VecB& b, const T epsilon) +{ + const camera_vector_t delta( + static_cast(a.x - b.x), + static_cast(a.y - b.y), + static_cast(a.z - b.z)); + return length(delta) <= epsilon; +} + +template +inline camera_vector_t safeNormalizeVec3(const camera_vector_t& value, const camera_vector_t& fallback) +{ + const auto len = length(value); + if (!isFiniteScalar(len) || len <= std::numeric_limits::epsilon()) + return fallback; + return value / len; +} + +template +inline camera_vector_t makeSphericalOffsetFromOrbit(const T orbitU, const T orbitV, const T distance) +{ + return camera_vector_t( + hlsl::cos(orbitV) * hlsl::cos(orbitU) * distance, + hlsl::cos(orbitV) * hlsl::sin(orbitU) * distance, + hlsl::sin(orbitV) * distance); +} + +template +inline T getPlanarRadiusXZ(const camera_vector_t& offset) +{ + return length(camera_vector_t(offset.x, offset.z)); +} + +template +inline T getPathDistance(const T radius, const T height) +{ + return length(camera_vector_t(radius, height)); +} + +template +inline camera_vector_t makePathOffsetFromState(const T angle, const T radius, const T height) +{ + return camera_vector_t(hlsl::cos(angle) * radius, height, hlsl::sin(angle) * radius); +} + +template +inline bool sanitizePathState(T& angle, T& radius, T& height, const T minRadius) +{ + if (!isFiniteScalar(angle) || !isFiniteScalar(radius) || !isFiniteScalar(height)) + return false; + + radius = std::max(minRadius, radius); + return isFiniteScalar(radius); +} + +template +inline bool tryScalePathStateDistance( + const T desiredDistance, + const T minRadius, + T& radius, + T& height, + T* outAppliedDistance = nullptr) +{ + if (!isFiniteScalar(desiredDistance) || !isFiniteScalar(radius) || !isFiniteScalar(height)) + return false; + + const T currentDistance = getPathDistance(radius, height); + constexpr T Epsilon = std::numeric_limits::epsilon(); + if (currentDistance > Epsilon) + { + const T scale = desiredDistance / currentDistance; + radius = std::max(minRadius, radius * scale); + height *= scale; + } + else + { + radius = std::max(minRadius, desiredDistance); + height = T(0); + } + + if (outAppliedDistance) + *outAppliedDistance = getPathDistance(radius, height); + return isFiniteScalar(radius) && isFiniteScalar(height); +} + +template +inline bool tryBuildPathStateFromPosition( + const camera_vector_t& targetPosition, + const camera_vector_t& position, + const T minRadius, + T& outAngle, + T& outRadius, + T& outHeight) +{ + const auto offset = position - targetPosition; + const auto radius = getPlanarRadiusXZ(offset); + if (!isFiniteScalar(radius) || !isFiniteScalar(offset.y)) + return false; + + outAngle = hlsl::atan2(offset.z, offset.x); + outRadius = std::max(minRadius, radius); + outHeight = offset.y; + return isFiniteScalar(outAngle) && isFiniteScalar(outRadius) && isFiniteScalar(outHeight); +} + +template +inline bool tryBuildLookAtOrientation( + const camera_vector_t& position, + const camera_vector_t& targetPosition, + const camera_vector_t& preferredUp, + camera_quaternion_t& outOrientation) +{ + const auto toTarget = targetPosition - position; + const auto toTargetLength = length(toTarget); + if (!isFiniteScalar(toTargetLength) || toTargetLength <= std::numeric_limits::epsilon()) + return false; + + const auto forward = toTarget / toTargetLength; + auto up = safeNormalizeVec3(preferredUp, camera_vector_t(T(0), T(0), T(1))); + auto right = cross(up, forward); + if (!isFiniteVec3(right) || length(right) <= std::numeric_limits::epsilon()) + { + const auto fallbackUp = std::abs(forward.z) < T(0.99) ? + camera_vector_t(T(0), T(0), T(1)) : + camera_vector_t(T(0), T(1), T(0)); + right = cross(fallbackUp, forward); + if (!isFiniteVec3(right) || length(right) <= std::numeric_limits::epsilon()) + return false; + } + + right = normalize(right); + up = normalize(cross(forward, right)); + if (!isOrthoBase(right, up, forward)) + return false; + + outOrientation = makeQuaternionFromBasis(right, up, forward); + return true; +} + +template +inline bool tryExtractRigidPoseFromTransform( + const camera_matrix_t& transform, + camera_vector_t& outTranslation, + camera_quaternion_t& outOrientation) +{ + outTranslation = camera_vector_t(transform[3].x, transform[3].y, transform[3].z); + + auto right = camera_vector_t(transform[0].x, transform[0].y, transform[0].z); + auto up = camera_vector_t(transform[1].x, transform[1].y, transform[1].z); + auto forward = camera_vector_t(transform[2].x, transform[2].y, transform[2].z); + + const T rightLen = length(right); + const T upLen = length(up); + const T forwardLen = length(forward); + constexpr T Epsilon = std::numeric_limits::epsilon(); + if (!isFiniteScalar(rightLen) || !isFiniteScalar(upLen) || !isFiniteScalar(forwardLen)) + return false; + if (rightLen <= Epsilon || upLen <= Epsilon || forwardLen <= Epsilon) + return false; + + right /= rightLen; + up /= upLen; + forward /= forwardLen; + if (!isOrthoBase(right, up, forward)) + return false; + + outOrientation = makeQuaternionFromBasis(right, up, forward); + return isFiniteQuaternion(outOrientation); +} + +template +inline bool tryBuildSphericalPoseFromOrbit( + const camera_vector_t& targetPosition, + const T orbitU, + const T orbitV, + const T distance, + const T minDistance, + const T maxDistance, + camera_vector_t& outPosition, + camera_quaternion_t& outOrientation, + T* outAppliedDistance = nullptr) +{ + if (!isFiniteScalar(orbitU) || !isFiniteScalar(orbitV) || !isFiniteScalar(distance)) + return false; + + const T appliedDistance = std::clamp(distance, minDistance, maxDistance); + const auto spherePosition = makeSphericalOffsetFromOrbit(orbitU, orbitV, appliedDistance); + const auto forward = safeNormalizeVec3(-spherePosition, camera_vector_t(T(0), T(0), T(1))); + auto up = safeNormalizeVec3( + camera_vector_t( + -hlsl::sin(orbitV) * hlsl::cos(orbitU), + -hlsl::sin(orbitV) * hlsl::sin(orbitU), + hlsl::cos(orbitV)), + camera_vector_t(T(0), T(0), T(1))); + auto right = safeNormalizeVec3(cross(up, forward), camera_vector_t(T(1), T(0), T(0))); + up = safeNormalizeVec3(cross(forward, right), up); + right = safeNormalizeVec3(cross(up, forward), right); + if (!isOrthoBase(right, up, forward)) + return false; + + outPosition = targetPosition + spherePosition; + outOrientation = makeQuaternionFromBasis(right, up, forward); + if (outAppliedDistance) + *outAppliedDistance = appliedDistance; + return true; +} + +template +inline bool tryBuildOrbitFromPosition( + const camera_vector_t& targetPosition, + const camera_vector_t& position, + const T minDistance, + const T maxDistance, + T& outOrbitU, + T& outOrbitV, + T& outDistance) +{ + const auto offset = position - targetPosition; + const auto distance = length(offset); + if (!isFiniteScalar(distance) || distance <= std::numeric_limits::epsilon()) + return false; + + outDistance = std::clamp(distance, minDistance, maxDistance); + const auto local = offset / outDistance; + outOrbitU = hlsl::atan2(local.y, local.x); + outOrbitV = hlsl::asin(std::clamp(local.z, T(-1), T(1))); + return isFiniteScalar(outOrbitU) && isFiniteScalar(outOrbitV) && isFiniteScalar(outDistance); +} + +template +inline camera_vector_t getPitchYawFromForwardVector(const camera_vector_t& forward) +{ + const T planarLength = length(camera_vector_t(forward.x, forward.z)); + return camera_vector_t( + hlsl::atan2(planarLength, forward.y) - numbers::pi * T(0.5), + hlsl::atan2(forward.x, forward.z)); +} + +template +inline bool tryBuildPathPoseFromState( + const camera_vector_t& targetPosition, + const T pathAngle, + const T pathRadius, + const T pathHeight, + const T minRadius, + const T minDistance, + const T maxDistance, + camera_vector_t& outPosition, + camera_quaternion_t& outOrientation, + T* outAppliedDistance = nullptr, + T* outOrbitU = nullptr, + T* outOrbitV = nullptr) +{ + if (!isFiniteScalar(pathAngle) || !isFiniteScalar(pathRadius) || !isFiniteScalar(pathHeight)) + return false; + + const T appliedRadius = std::max(minRadius, pathRadius); + const auto offset = makePathOffsetFromState(pathAngle, appliedRadius, pathHeight); + + T orbitU = T(0); + T orbitV = T(0); + T distance = T(0); + if (!tryBuildOrbitFromPosition(targetPosition, targetPosition + offset, minDistance, maxDistance, orbitU, orbitV, distance)) + return false; + if (!tryBuildSphericalPoseFromOrbit(targetPosition, orbitU, orbitV, distance, minDistance, maxDistance, outPosition, outOrientation, &distance)) + return false; + + if (outAppliedDistance) + *outAppliedDistance = distance; + if (outOrbitU) + *outOrbitU = orbitU; + if (outOrbitV) + *outOrbitV = orbitV; + return true; +} + template inline camera_vector_t rotateVectorByQuaternion(const camera_quaternion_t& orientation, const camera_vector_t& vectorToRotate) { @@ -240,14 +554,14 @@ inline camera_vector_t getQuaternionEulerRadians(const camera_quaternion_t const T z = q.data.z; const T w = q.data.w; - const T pitch = std::atan2( + const T pitch = hlsl::atan2( T(2) * (y * z + w * x), w * w - x * x - y * y + z * z); - const T yaw = std::asin(std::clamp( + const T yaw = hlsl::asin(std::clamp( T(-2) * (x * z - w * y), T(-1), T(1))); - const T roll = std::atan2( + const T roll = hlsl::atan2( T(2) * (x * y + w * z), w * w + x * x - y * y - z * z); @@ -273,7 +587,7 @@ inline T getQuaternionAngularDistanceRadians(const camera_quaternion_t& lhs, static_cast(std::abs(dot(lhsNormalized.data, rhsNormalized.data))), T(0), T(1)); - return T(2) * std::acos(orientationDot); + return T(2) * hlsl::acos(orientationDot); } template @@ -304,6 +618,42 @@ inline camera_matrix_t getQuaternionBasisMatrix(const camera_quaternion q.transformVector(camera_vector_t(T(0), T(0), T(1)), true)); } +template +inline camera_vector_t getQuaternionEulerRadiansYXZ(const camera_quaternion_t& orientation) +{ + const auto basis = getQuaternionBasisMatrix(orientation); + const T yaw = hlsl::atan2(basis[2][0], basis[2][2]); + const T c2 = hlsl::length(camera_vector_t(basis[0][1], basis[1][1])); + const T pitch = hlsl::atan2(-basis[2][1], c2); + const T s1 = hlsl::sin(yaw); + const T c1 = hlsl::cos(yaw); + const T roll = hlsl::atan2( + s1 * basis[1][2] - c1 * basis[1][0], + c1 * basis[0][0] - s1 * basis[0][2]); + return camera_vector_t(pitch, yaw, roll); +} + +template +inline camera_vector_t getQuaternionEulerDegreesYXZ(const camera_quaternion_t& orientation) +{ + const auto eulerRadians = getQuaternionEulerRadiansYXZ(orientation); + return camera_vector_t( + degrees(eulerRadians.x), + degrees(eulerRadians.y), + degrees(eulerRadians.z)); +} + +template +inline camera_vector_t getWrappedEulerDistanceDegrees( + const camera_vector_t& a, + const camera_vector_t& b) +{ + return camera_vector_t( + getWrappedAngleDistanceDegrees(a.x, b.x), + getWrappedAngleDistanceDegrees(a.y, b.y), + getWrappedAngleDistanceDegrees(a.z, b.z)); +} + template inline camera_matrix_t composeTransformMatrix( const camera_vector_t& translation, diff --git a/common/include/camera/CCameraScriptVisualDebugOverlayUtilities.hpp b/common/include/camera/CCameraScriptVisualDebugOverlayUtilities.hpp new file mode 100644 index 000000000..950d8c386 --- /dev/null +++ b/common/include/camera/CCameraScriptVisualDebugOverlayUtilities.hpp @@ -0,0 +1,212 @@ +// 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 "imgui/imgui.h" + +namespace nbl::ui +{ + +//! 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(); + } +}; + +//! Shared camera/debug state used to format one scripted visual debug HUD payload. +struct SCameraScriptVisualDebugStatus final +{ + 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 = 60.0f; + 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; +}; + +//! 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; +}; + +//! Draw the scripted visual debug HUD on the foreground draw list. +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()); +} + +//! Build the display strings for one scripted visual debug HUD snapshot. +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); + + char progressBuffer[256] = {}; + 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::snprintf( + progressBuffer, + sizeof(progressBuffer), + "Planar %u Segment %.1f/%.1f s Frame %llu/%llu", + status.planarIndex, + elapsedSeconds, + holdSeconds, + static_cast(status.progressFrames), + static_cast(status.holdFrames)); + } + else + { + std::snprintf( + progressBuffer, + sizeof(progressBuffer), + "Planar %u Frame %llu", + status.planarIndex, + static_cast(status.absoluteFrame)); + } + out.progressLine = progressBuffer; + if (!status.segmentLabel.empty()) + out.progressLine += " | " + std::string(status.segmentLabel); + + out.hintLine = std::string(status.cameraHint); + if (status.hasDynamicFov) + { + char fovBuffer[96] = {}; + std::snprintf(fovBuffer, sizeof(fovBuffer), " | Dynamic FOV %.2f deg", status.dynamicFovDeg); + out.hintLine += fovBuffer; + } + + if (status.followActive) + { + out.hintLine += " | " + std::string(status.followModeDescription); + if (status.followLockValid) + { + char followBuffer[192] = {}; + std::snprintf( + followBuffer, + sizeof(followBuffer), + " | lock %.2f deg | target %.2f | center err %.3f", + status.followLockAngleDeg, + status.followTargetDistance, + status.followTargetCenterNdcRadius); + out.hintLine += followBuffer; + } + 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/CCameraScriptedCheckRunner.hpp b/common/include/camera/CCameraScriptedCheckRunner.hpp index 722f580ee..848a9116f 100644 --- a/common/include/camera/CCameraScriptedCheckRunner.hpp +++ b/common/include/camera/CCameraScriptedCheckRunner.hpp @@ -66,19 +66,6 @@ struct CCameraScriptedCheckFrameResult bool hadFailures = false; }; -inline float scriptedCheckAngleDiffDeg(const float a, const float b) -{ - float d = std::fmod(a - b + 180.0f, 360.0f); - if (d < 0.0f) - d += 360.0f; - return std::abs(d - 180.0f); -} - -inline bool scriptedCheckFinite3(const hlsl::float32_t3& v) -{ - return std::isfinite(v.x) && std::isfinite(v.y) && std::isfinite(v.z); -} - inline void scriptedCheckSetStepReference( CCameraScriptedCheckRuntimeState& state, const hlsl::float32_t3& pos, @@ -138,7 +125,7 @@ inline CCameraScriptedCheckFrameResult evaluateScriptedChecksForFrame( const auto pos = gimbal.getPosition(); const auto eulerDeg = hlsl::getCastedVector(hlsl::getQuaternionEulerDegrees(gimbal.getOrientation())); - if (!scriptedCheckFinite3(pos) || !scriptedCheckFinite3(eulerDeg)) + if (!hlsl::isFiniteVec3(pos) || !hlsl::isFiniteVec3(eulerDeg)) { appendScriptedCheckLog( result, @@ -231,8 +218,8 @@ inline CCameraScriptedCheckFrameResult evaluateScriptedChecksForFrame( bool ok = true; if (check.hasExpectedPos) { - const auto diff = hlsl::float32_t3(pos.x - check.expectedPos.x, pos.y - check.expectedPos.y, pos.z - check.expectedPos.z); - const auto distance = std::sqrt(diff.x * diff.x + diff.y * diff.y + diff.z * diff.z); + const hlsl::float64_t3 diff = pos - hlsl::getCastedVector(check.expectedPos); + const double distance = hlsl::length(diff); if (distance > check.posTolerance) { ok = false; @@ -250,9 +237,10 @@ inline CCameraScriptedCheckFrameResult evaluateScriptedChecksForFrame( } if (check.hasExpectedEuler) { - const auto dx = scriptedCheckAngleDiffDeg(eulerDeg.x, check.expectedEulerDeg.x); - const auto dy = scriptedCheckAngleDiffDeg(eulerDeg.y, check.expectedEulerDeg.y); - const auto dz = scriptedCheckAngleDiffDeg(eulerDeg.z, check.expectedEulerDeg.z); + const auto deltaEulerDeg = hlsl::getWrappedEulerDistanceDegrees(eulerDeg, check.expectedEulerDeg); + const auto dx = deltaEulerDeg.x; + const auto dy = deltaEulerDeg.y; + const auto dz = deltaEulerDeg.z; const auto maxAngle = std::max(dx, std::max(dy, dz)); if (maxAngle > check.eulerToleranceDeg) { @@ -296,11 +284,12 @@ inline CCameraScriptedCheckFrameResult evaluateScriptedChecksForFrame( break; } - const auto diff = hlsl::float32_t3(pos.x - state.baselinePos.x, pos.y - state.baselinePos.y, pos.z - state.baselinePos.z); - const auto dpos = std::sqrt(diff.x * diff.x + diff.y * diff.y + diff.z * diff.z); - const auto dx = scriptedCheckAngleDiffDeg(eulerDeg.x, state.baselineEulerDeg.x); - const auto dy = scriptedCheckAngleDiffDeg(eulerDeg.y, state.baselineEulerDeg.y); - const auto dz = scriptedCheckAngleDiffDeg(eulerDeg.z, state.baselineEulerDeg.z); + const hlsl::float64_t3 diff = pos - hlsl::getCastedVector(state.baselinePos); + const double dpos = hlsl::length(diff); + const auto deltaEulerDeg = hlsl::getWrappedEulerDistanceDegrees(eulerDeg, state.baselineEulerDeg); + const auto dx = deltaEulerDeg.x; + const auto dy = deltaEulerDeg.y; + const auto dz = deltaEulerDeg.z; const auto dmax = std::max(dx, std::max(dy, dz)); if (dpos > check.posTolerance || dmax > check.eulerToleranceDeg) @@ -356,11 +345,12 @@ inline CCameraScriptedCheckFrameResult evaluateScriptedChecksForFrame( } } - const auto diff = hlsl::float32_t3(pos.x - state.stepPos.x, pos.y - state.stepPos.y, pos.z - state.stepPos.z); - const auto dpos = std::sqrt(diff.x * diff.x + diff.y * diff.y + diff.z * diff.z); - const auto dx = scriptedCheckAngleDiffDeg(eulerDeg.x, state.stepEulerDeg.x); - const auto dy = scriptedCheckAngleDiffDeg(eulerDeg.y, state.stepEulerDeg.y); - const auto dz = scriptedCheckAngleDiffDeg(eulerDeg.z, state.stepEulerDeg.z); + const hlsl::float64_t3 diff = pos - hlsl::getCastedVector(state.stepPos); + const double dpos = hlsl::length(diff); + const auto deltaEulerDeg = hlsl::getWrappedEulerDistanceDegrees(eulerDeg, state.stepEulerDeg); + const auto dx = deltaEulerDeg.x; + const auto dy = deltaEulerDeg.y; + const auto dz = deltaEulerDeg.z; const auto dmax = std::max(dx, std::max(dy, dz)); bool ok = true; @@ -480,6 +470,9 @@ inline CCameraScriptedCheckFrameResult evaluateScriptedChecksForFrame( SCameraFollowRegressionResult regression = {}; std::string regressionError; core::CCameraGoal expectedFollowGoal = {}; + SCameraFollowRegressionThresholds thresholds = {}; + thresholds.lockAngleToleranceDeg = check.eulerToleranceDeg; + thresholds.projectedNdcTolerance = check.posTolerance; const bool ok = core::tryBuildFollowGoal( *context.goalSolver, context.camera, @@ -493,11 +486,8 @@ inline CCameraScriptedCheckFrameResult evaluateScriptedChecksForFrame( expectedFollowGoal, regression, ®ressionError, - check.eulerToleranceDeg, - 1e-6, - 1e-9, context.followViewProjMatrix, - check.posTolerance); + thresholds); if (!ok) { diff --git a/common/include/camera/CCameraScriptedRuntime.hpp b/common/include/camera/CCameraScriptedRuntime.hpp index e3fe511ae..db6c0c5bc 100644 --- a/common/include/camera/CCameraScriptedRuntime.hpp +++ b/common/include/camera/CCameraScriptedRuntime.hpp @@ -11,6 +11,7 @@ #include #include "CCameraGoal.hpp" +#include "CCameraFollowRegressionUtilities.hpp" #include "CVirtualGimbalEvent.hpp" #include "nbl/ui/KeyCodes.h" @@ -292,8 +293,8 @@ inline void appendScriptedGimbalStepCheck( inline void appendScriptedFollowTargetLockCheck( CCameraScriptedTimeline& timeline, const uint64_t frame, - const float toleranceDeg = 1.0f, - const float screenToleranceNdc = 0.03f) + const float toleranceDeg = static_cast(core::ICamera::DefaultAngularToleranceDeg), + const float screenToleranceNdc = SCameraFollowRegressionThresholds::DefaultProjectedNdcTolerance) { CCameraScriptedInputCheck entry; entry.frame = frame; diff --git a/common/include/camera/CCameraSequenceScript.hpp b/common/include/camera/CCameraSequenceScript.hpp index 945ba71f4..145272db6 100644 --- a/common/include/camera/CCameraSequenceScript.hpp +++ b/common/include/camera/CCameraSequenceScript.hpp @@ -290,7 +290,7 @@ inline void normalizeCaptureFractions(std::vector& fractions) std::sort(fractions.begin(), fractions.end()); fractions.erase(std::unique(fractions.begin(), fractions.end(), - [](const float lhs, const float rhs) { return std::abs(lhs - rhs) <= 1e-6f; }), + [](const float lhs, const float rhs) { return std::abs(lhs - rhs) <= static_cast(ICamera::ScalarTolerance); }), fractions.end()); } @@ -301,23 +301,24 @@ inline bool applyCanonicalSphericalGoal(CCameraGoal& goal) if (!std::isfinite(goal.orbitU) || !std::isfinite(goal.orbitV) || !std::isfinite(goal.orbitDistance)) return false; - const float appliedDistance = std::clamp(goal.orbitDistance, ICamera::SphericalMinDistance, ICamera::SphericalMaxDistance); - const hlsl::float64_t3 spherePosition( - std::cos(goal.orbitV) * std::cos(goal.orbitU) * static_cast(appliedDistance), - std::cos(goal.orbitV) * std::sin(goal.orbitU) * static_cast(appliedDistance), - std::sin(goal.orbitV) * static_cast(appliedDistance)); - goal.position = goal.targetPosition + spherePosition; + hlsl::float64_t appliedDistance = 0.0; + if (!hlsl::tryBuildSphericalPoseFromOrbit( + goal.targetPosition, + goal.orbitU, + goal.orbitV, + static_cast(goal.orbitDistance), + static_cast(ICamera::SphericalMinDistance), + static_cast(ICamera::SphericalMaxDistance), + goal.position, + goal.orientation, + &appliedDistance)) + { + return false; + } + goal.hasDistance = true; - goal.distance = appliedDistance; - goal.orbitDistance = appliedDistance; - - const auto forward = hlsl::normalize(-spherePosition); - const hlsl::float64_t3 up = hlsl::normalize(hlsl::float64_t3( - -std::sin(goal.orbitV) * std::cos(goal.orbitU), - -std::sin(goal.orbitV) * std::sin(goal.orbitU), - std::cos(goal.orbitV))); - const hlsl::float64_t3 right = hlsl::normalize(hlsl::cross(up, forward)); - goal.orientation = hlsl::makeQuaternionFromBasis(right, up, forward); + goal.distance = static_cast(appliedDistance); + goal.orbitDistance = static_cast(appliedDistance); return true; } @@ -381,7 +382,10 @@ inline bool buildSequenceKeyframePreset(const CCameraPreset& reference, const CC if (delta.hasOrbitUDeltaDeg) goal.orbitU = hlsl::wrapAngleRad(goal.orbitU + hlsl::radians(delta.orbitUDeltaDeg)); if (delta.hasOrbitVDeltaDeg) - goal.orbitV = std::clamp(goal.orbitV + hlsl::radians(delta.orbitVDeltaDeg), -1.55334303427, 1.55334303427); + { + constexpr double OrbitPitchLimit = hlsl::numbers::pi * (89.0 / 180.0); + goal.orbitV = std::clamp(goal.orbitV + hlsl::radians(delta.orbitVDeltaDeg), -OrbitPitchLimit, OrbitPitchLimit); + } if (delta.hasOrbitDistanceDelta) goal.orbitDistance += delta.orbitDistanceDelta; } @@ -467,7 +471,7 @@ inline bool buildSequenceTrackFromReference(const CCameraPreset& reference, cons inline bool isSequenceTrackedTargetPoseFinite(const CCameraSequenceTrackedTargetPose& pose) { - return isFiniteVec3(pose.position) && + return hlsl::isFiniteVec3(pose.position) && std::isfinite(pose.orientation.data.x) && std::isfinite(pose.orientation.data.y) && std::isfinite(pose.orientation.data.z) && @@ -535,7 +539,7 @@ inline bool buildSequenceTrackedTargetTrackFromReference( normalized.reserve(outTrack.keyframes.size()); for (const auto& keyframe : outTrack.keyframes) { - if (!normalized.empty() && std::abs(normalized.back().time - keyframe.time) <= 1e-6f) + if (!normalized.empty() && std::abs(normalized.back().time - keyframe.time) <= static_cast(ICamera::ScalarTolerance)) normalized.back() = keyframe; else normalized.emplace_back(keyframe); @@ -570,7 +574,7 @@ inline bool tryBuildSequenceTrackedTargetPoseAtTime( if (time > rhs.time) continue; - const auto span = std::max(1e-6f, rhs.time - lhs.time); + const auto span = std::max(static_cast(ICamera::ScalarTolerance), rhs.time - lhs.time); const auto alpha = std::clamp((time - lhs.time) / span, 0.f, 1.f); outPose.position = lhs.pose.position + (rhs.pose.position - lhs.pose.position) * static_cast(alpha); outPose.orientation = hlsl::slerpQuaternion(lhs.pose.orientation, rhs.pose.orientation, static_cast(alpha)); diff --git a/common/include/camera/CCameraSmokeRegressionUtilities.hpp b/common/include/camera/CCameraSmokeRegressionUtilities.hpp new file mode 100644 index 000000000..5ec9a7a14 --- /dev/null +++ b/common/include/camera/CCameraSmokeRegressionUtilities.hpp @@ -0,0 +1,128 @@ +// 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_SMOKE_REGRESSION_UTILITIES_HPP_ +#define _C_CAMERA_SMOKE_REGRESSION_UTILITIES_HPP_ + +#include + +#include "CCameraKeyframeTrack.hpp" +#include "CCameraMathUtilities.hpp" +#include "CCameraPresetFlow.hpp" +#include "ICamera.hpp" + +namespace nbl::system +{ + +//! Small shared bundle for smoke/regression pose deltas measured around one camera manipulation. +struct SCameraManipulationDelta +{ + double position = 0.0; + double rotationDeg = 0.0; +}; + +struct SCameraSmokeComparisonThresholds final +{ + static constexpr double TinyScalarEpsilon = core::ICamera::TinyScalarEpsilon; + static constexpr double DefaultPositionTolerance = core::ICamera::DefaultPositionTolerance; + static constexpr double DefaultAngularToleranceDeg = core::ICamera::DefaultAngularToleranceDeg; + static constexpr double DefaultScalarTolerance = core::ICamera::ScalarTolerance; + static constexpr double StrictPositionTolerance = core::ICamera::ScalarTolerance; + static constexpr double StrictAngularToleranceDeg = core::ICamera::DefaultAngularToleranceDeg; + static constexpr double StrictScalarTolerance = core::ICamera::ScalarTolerance; + static constexpr double TrackTimeTolerance = core::ICamera::ScalarTolerance; +}; + +//! Measure one camera pose delta against an authored reference pose. +inline bool tryComputeCameraManipulationDelta( + core::ICamera* camera, + const hlsl::float64_t3& beforePosition, + const hlsl::camera_quaternion_t& beforeOrientation, + SCameraManipulationDelta& outDelta) +{ + outDelta = {}; + if (!camera) + return false; + + const auto& gimbal = camera->getGimbal(); + const auto afterPosition = gimbal.getPosition(); + const auto afterOrientation = hlsl::normalizeQuaternion(gimbal.getOrientation()); + if (!hlsl::isFiniteVec3(afterPosition) || !hlsl::isFiniteQuaternion(beforeOrientation) || !hlsl::isFiniteQuaternion(afterOrientation)) + return false; + + outDelta.position = hlsl::length(afterPosition - beforePosition); + outDelta.rotationDeg = hlsl::getQuaternionAngularDistanceDegrees(beforeOrientation, afterOrientation); + return true; +} + +//! Manipulate a camera and report how far its pose moved in position and Euler-angle terms. +inline bool tryManipulateCameraAndMeasureDelta( + core::ICamera* camera, + std::span events, + SCameraManipulationDelta& outDelta, + const double tinyEpsilon = SCameraSmokeComparisonThresholds::TinyScalarEpsilon) +{ + outDelta = {}; + if (!camera || events.empty()) + return false; + + const auto& beforeGimbal = camera->getGimbal(); + const auto beforePosition = beforeGimbal.getPosition(); + const auto beforeOrientation = hlsl::normalizeQuaternion(beforeGimbal.getOrientation()); + if (!hlsl::isFiniteVec3(beforePosition) || !hlsl::isFiniteQuaternion(beforeOrientation)) + return false; + + if (!camera->manipulate(events)) + return false; + + if (!tryComputeCameraManipulationDelta(camera, beforePosition, beforeOrientation, outDelta)) + return false; + + return outDelta.position > tinyEpsilon || outDelta.rotationDeg > tinyEpsilon; +} + +inline bool comparePresetToCameraStateWithDefaultThresholds( + const core::CCameraGoalSolver& solver, + core::ICamera* camera, + const core::CCameraPreset& preset) +{ + return core::comparePresetToCameraState( + solver, + camera, + preset, + SCameraSmokeComparisonThresholds::DefaultPositionTolerance, + SCameraSmokeComparisonThresholds::DefaultAngularToleranceDeg, + SCameraSmokeComparisonThresholds::DefaultScalarTolerance); +} + +inline bool comparePresetToCameraStateWithStrictThresholds( + const core::CCameraGoalSolver& solver, + core::ICamera* camera, + const core::CCameraPreset& preset) +{ + return core::comparePresetToCameraState( + solver, + camera, + preset, + SCameraSmokeComparisonThresholds::StrictPositionTolerance, + SCameraSmokeComparisonThresholds::StrictAngularToleranceDeg, + SCameraSmokeComparisonThresholds::StrictScalarTolerance); +} + +inline bool compareKeyframeTrackContentWithStrictThresholds( + const core::CCameraKeyframeTrack& lhs, + const core::CCameraKeyframeTrack& rhs) +{ + return core::compareKeyframeTrackContent( + lhs, + rhs, + SCameraSmokeComparisonThresholds::TrackTimeTolerance, + SCameraSmokeComparisonThresholds::StrictPositionTolerance, + SCameraSmokeComparisonThresholds::StrictAngularToleranceDeg, + SCameraSmokeComparisonThresholds::StrictScalarTolerance); +} + +} // namespace nbl::system + +#endif // _C_CAMERA_SMOKE_REGRESSION_UTILITIES_HPP_ diff --git a/common/include/camera/CCameraViewportOverlayUtilities.hpp b/common/include/camera/CCameraViewportOverlayUtilities.hpp new file mode 100644 index 000000000..c46bf43ad --- /dev/null +++ b/common/include/camera/CCameraViewportOverlayUtilities.hpp @@ -0,0 +1,311 @@ +// 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 "CCameraFollowRegressionUtilities.hpp" +#include "imgui/imgui.h" + +namespace nbl::ui +{ + +//! 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); + } +}; + +//! 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; +}; + +//! 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; +}; + +//! 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() && !detail.empty(); + } +}; + +//! 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; +}; + +//! 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; +}; + +//! 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; +}; + +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); +} + +inline void popViewportWindowStyle() +{ + ImGui::PopStyleColor(); + ImGui::PopStyleVar(3); +} + +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 ImVec2 detailSize = ImGui::CalcTextSize(data.detail.c_str()); + const float width = std::max(std::max(headlineSize.x, descriptionSize.x), detailSize.x); + const float height = headlineSize.y + descriptionSize.y + detailSize.y + style.lineGap * 2.0f + style.padding.y * 2.0f; + 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()); + 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()); +} + +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); +} + +inline void endHoverInfoOverlay() +{ + ImGui::End(); + ImGui::PopStyleVar(); + ImGui::PopStyleColor(2); +} + +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); +} + +//! Draw one follow-target overlay in the active viewport using projected tracked-target metrics. +inline void drawFollowTargetViewportOverlay( + ImDrawList& drawList, + const hlsl::float32_t4x4& viewProjMatrix, + const core::CTrackedTarget& trackedTarget, + const SViewportOverlayRect& viewportRect, + const SCameraFollowTargetViewportOverlayStyle& style = {}) +{ + if (!viewportRect.valid()) + return; + + float ndcX = 0.0f; + float ndcY = 0.0f; + float ndcRadius = 0.0f; + if (!system::tryComputeProjectedFollowTargetMetrics(viewProjMatrix, trackedTarget, ndcX, ndcY, &ndcRadius)) + return; + + const bool centered = ndcRadius <= style.centeredNdcRadius; + const ImVec2 center = viewportRect.getCenter(); + const ImVec2 target = viewportRect.ndcToScreen(ImVec2(ndcX, ndcY)); + 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/camera/CChaseCamera.hpp b/common/include/camera/CChaseCamera.hpp index 9018107b1..b6f098eda 100644 --- a/common/include/camera/CChaseCamera.hpp +++ b/common/include/camera/CChaseCamera.hpp @@ -29,33 +29,23 @@ class CChaseCamera final : public CSphericalTargetCamera if (not virtualEvents.size() and not referenceFrame) return false; - auto impulse = m_gimbal.accumulate(virtualEvents); + const auto impulse = m_gimbal.accumulate(virtualEvents); - const double deltaYaw = impulse.dVirtualRotation.y * getRotationSpeedScale(); - const double deltaPitch = impulse.dVirtualRotation.x * getRotationSpeedScale(); - - constexpr double translateScalar = 0.01; - const double moveScalar = translateScalar * getMoveSpeedScale(); + const double deltaYaw = scaleVirtualRotation(impulse.dVirtualRotation.y); + const double deltaPitch = scaleVirtualRotation(impulse.dVirtualRotation.x); const auto basis = computeBasis(m_u, m_v, m_distance); - hlsl::float64_t3 planarForward = hlsl::float64_t3(basis.forward.x, 0.0, basis.forward.z); - hlsl::float64_t3 planarRight = hlsl::float64_t3(basis.right.x, 0.0, basis.right.z); - - const double forwardLen = hlsl::length(planarForward); - if (forwardLen > 0.0) - planarForward /= forwardLen; - else - planarForward = hlsl::float64_t3(0.0, 0.0, 1.0); - - const double rightLen = hlsl::length(planarRight); - if (rightLen > 0.0) - planarRight /= rightLen; - else - planarRight = hlsl::float64_t3(1.0, 0.0, 0.0); + const auto planarForward = hlsl::safeNormalizeVec3( + hlsl::float64_t3(basis.forward.x, 0.0, basis.forward.z), + hlsl::float64_t3(0.0, 0.0, 1.0)); + const auto planarRight = hlsl::safeNormalizeVec3( + hlsl::float64_t3(basis.right.x, 0.0, basis.right.z), + hlsl::float64_t3(1.0, 0.0, 0.0)); - m_targetPosition += (planarRight * impulse.dVirtualTranslate.x + planarForward * impulse.dVirtualTranslate.z) * moveScalar; - m_distance = std::clamp(m_distance + static_cast(impulse.dVirtualTranslate.y * translateScalar), MinDistance, MaxDistance); + m_targetPosition += planarRight * scaleVirtualTranslation(impulse.dVirtualTranslate.x) + + planarForward * scaleVirtualTranslation(impulse.dVirtualTranslate.z); + m_distance = std::clamp(m_distance + static_cast(scaleUnscaledVirtualTranslation(impulse.dVirtualTranslate.y)), MinDistance, MaxDistance); m_u += deltaYaw; m_v = std::clamp(m_v + deltaPitch, MinPitch, MaxPitch); @@ -69,8 +59,8 @@ class CChaseCamera final : public CSphericalTargetCamera private: static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate | CVirtualGimbalEvent::Rotate; - static inline constexpr double MaxPitch = 1.2217304763960306; - static inline constexpr double MinPitch = -1.0471975511965976; + static inline constexpr double MaxPitch = hlsl::numbers::pi * (70.0 / 180.0); + static inline constexpr double MinPitch = -hlsl::numbers::pi / 3.0; }; } diff --git a/common/include/camera/CDollyCamera.hpp b/common/include/camera/CDollyCamera.hpp index 7b8455ccf..af28c3c47 100644 --- a/common/include/camera/CDollyCamera.hpp +++ b/common/include/camera/CDollyCamera.hpp @@ -29,16 +29,16 @@ class CDollyCamera final : public CSphericalTargetCamera if (not virtualEvents.size() and not referenceFrame) return false; - auto impulse = m_gimbal.accumulate(virtualEvents); + const auto impulse = m_gimbal.accumulate(virtualEvents); - const double deltaYaw = impulse.dVirtualRotation.y * getRotationSpeedScale(); - const double deltaPitch = impulse.dVirtualRotation.x * getRotationSpeedScale(); - - constexpr double translateScalar = 0.01; - const double moveScalar = translateScalar * getMoveSpeedScale(); + const double deltaYaw = scaleVirtualRotation(impulse.dVirtualRotation.y); + const double deltaPitch = scaleVirtualRotation(impulse.dVirtualRotation.x); const auto basis = computeBasis(m_u, m_v, m_distance); - const auto delta = (basis.right * impulse.dVirtualTranslate.x + basis.up * impulse.dVirtualTranslate.y + basis.forward * impulse.dVirtualTranslate.z) * moveScalar; + const auto delta = + basis.right * scaleVirtualTranslation(impulse.dVirtualTranslate.x) + + basis.up * scaleVirtualTranslation(impulse.dVirtualTranslate.y) + + basis.forward * scaleVirtualTranslation(impulse.dVirtualTranslate.z); m_targetPosition += delta; m_u += deltaYaw; @@ -53,8 +53,8 @@ class CDollyCamera final : public CSphericalTargetCamera private: static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate | CVirtualGimbalEvent::Rotate; - static inline constexpr double MaxPitch = 1.4835298641951802; - static inline constexpr double MinPitch = -1.4835298641951802; + static inline constexpr double MaxPitch = hlsl::numbers::pi * (85.0 / 180.0); + static inline constexpr double MinPitch = -MaxPitch; }; } diff --git a/common/include/camera/CDollyZoomCamera.hpp b/common/include/camera/CDollyZoomCamera.hpp index 699c5f454..66b1ebef0 100644 --- a/common/include/camera/CDollyZoomCamera.hpp +++ b/common/include/camera/CDollyZoomCamera.hpp @@ -14,7 +14,7 @@ class CDollyZoomCamera final : public CSphericalTargetCamera public: using base_t = CSphericalTargetCamera; - CDollyZoomCamera(const hlsl::float64_t3& position, const hlsl::float64_t3& target, float baseFov = 40.0f) + CDollyZoomCamera(const hlsl::float64_t3& position, const hlsl::float64_t3& target, float baseFov = DefaultBaseFovDeg) : base_t(position, target), m_baseFov(baseFov), m_referenceDistance(m_distance) { applyPose(); @@ -35,7 +35,7 @@ class CDollyZoomCamera final : public CSphericalTargetCamera const double ratio = static_cast(m_referenceDistance) / std::max(static_cast(m_distance), static_cast(MinDistance)); const double fov = 2.0 * std::atan(base * ratio); const double fovDeg = hlsl::degrees(fov); - return static_cast(std::clamp(fovDeg, 10.0, 150.0)); + return static_cast(std::clamp(fovDeg, static_cast(MinDynamicFovDeg), static_cast(MaxDynamicFovDeg))); } virtual bool manipulate(std::span virtualEvents, const hlsl::float64_t4x4* referenceFrame = nullptr) override @@ -43,18 +43,14 @@ class CDollyZoomCamera final : public CSphericalTargetCamera if (not virtualEvents.size() and not referenceFrame) return false; - auto impulse = m_gimbal.accumulate(virtualEvents); - double deltaU = impulse.dVirtualTranslate.y; - double deltaV = impulse.dVirtualTranslate.x; - double deltaDistance = impulse.dVirtualTranslate.z; - - constexpr auto scalar = 0.01; - deltaU *= scalar * getMoveSpeedScale(); - deltaV *= scalar * getMoveSpeedScale(); + const auto impulse = m_gimbal.accumulate(virtualEvents); + const double deltaU = scaleVirtualTranslation(impulse.dVirtualTranslate.y); + const double deltaV = scaleVirtualTranslation(impulse.dVirtualTranslate.x); + const double deltaDistance = scaleUnscaledVirtualTranslation(impulse.dVirtualTranslate.z); m_u += deltaU; m_v += deltaV; - m_distance = std::clamp(m_distance + static_cast(deltaDistance * scalar), MinDistance, MaxDistance); + m_distance = std::clamp(m_distance + static_cast(deltaDistance), MinDistance, MaxDistance); return applyPose(); } @@ -86,8 +82,11 @@ class CDollyZoomCamera final : public CSphericalTargetCamera private: static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate; + static inline constexpr float DefaultBaseFovDeg = 40.0f; + static inline constexpr float MinDynamicFovDeg = 10.0f; + static inline constexpr float MaxDynamicFovDeg = 150.0f; - float m_baseFov = 40.0f; + float m_baseFov = DefaultBaseFovDeg; float m_referenceDistance = 1.0f; }; diff --git a/common/include/camera/CFPSCamera.hpp b/common/include/camera/CFPSCamera.hpp index 4a4145a7e..8b26eeabc 100644 --- a/common/include/camera/CFPSCamera.hpp +++ b/common/include/camera/CFPSCamera.hpp @@ -16,21 +16,14 @@ class CFPSCamera final : public ICamera { public: using base_t = ICamera; - static inline constexpr float HalfPi = 1.57079632679489661923f; - static inline constexpr float RadToDeg = 57.2957795130823208768f; CFPSCamera(const hlsl::float64_t3& position, const hlsl::camera_quaternion_t& orientation = hlsl::makeIdentityQuaternion()) : base_t(), m_gimbal({ .position = position, .orientation = orientation }) { m_gimbal.begin(); { - const auto& gForward = m_gimbal.getZAxis(); - const float gForwardX = static_cast(gForward.x); - const float gForwardY = static_cast(gForward.y); - const float gForwardZ = static_cast(gForward.z); - const float gPitch = std::atan2(std::hypot(gForwardX, gForwardZ), gForwardY) - HalfPi; - const float gYaw = std::atan2(gForwardX, gForwardZ); - m_gimbal.setOrientation(hlsl::makeQuaternionFromEulerRadians(hlsl::float64_t3(gPitch, gYaw, 0.0f))); + const auto pitchYaw = hlsl::getPitchYawFromForwardVector(m_gimbal.getZAxis()); + m_gimbal.setOrientation(hlsl::makeQuaternionFromEulerRadians(hlsl::float64_t3(pitchYaw.x, pitchYaw.y, 0.0))); } m_gimbal.end(); } @@ -54,14 +47,7 @@ class CFPSCamera final : public ICamera { if (referenceFrame) { - const auto& q = reference.orientation; - const float w = static_cast(q.data.w); - const float x = static_cast(q.data.x); - const float y = static_cast(q.data.y); - const float z = static_cast(q.data.z); - const float sinr_cosp = 2.f * (w * z + x * y); - const float cosr_cosp = 1.f - 2.f * (y * y + z * z); - const float roll = RadToDeg * std::atan2(sinr_cosp, cosr_cosp); + const float roll = static_cast(hlsl::degrees(hlsl::getQuaternionEulerRadiansYXZ(reference.orientation).z)); const float absRoll = std::abs(roll); constexpr float epsilon = 1.e-4f; @@ -78,12 +64,9 @@ class CFPSCamera final : public ICamera m_gimbal.begin(); { - const float rForwardX = static_cast(reference.frame[2].x); - const float rForwardY = static_cast(reference.frame[2].y); - const float rForwardZ = static_cast(reference.frame[2].z); - const float rPitch = std::atan2(std::hypot(rForwardX, rForwardZ), rForwardY) - HalfPi; - const float gYaw = std::atan2(rForwardX, rForwardZ); - const float newPitch = std::clamp(rPitch + impulse.dVirtualRotation.x * getRotationSpeedScale(), MinVerticalAngle, MaxVerticalAngle), newYaw = gYaw + impulse.dVirtualRotation.y * getRotationSpeedScale(); + const auto pitchYaw = hlsl::getPitchYawFromForwardVector(hlsl::float64_t3(reference.frame[2])); + const float newPitch = std::clamp(static_cast(pitchYaw.x + scaleVirtualRotation(impulse.dVirtualRotation.x)), MinVerticalAngle, MaxVerticalAngle); + const float newYaw = static_cast(pitchYaw.y + scaleVirtualRotation(impulse.dVirtualRotation.y)); if (validateReference()) m_gimbal.setOrientation(hlsl::makeQuaternionFromEulerRadians(hlsl::float64_t3(newPitch, newYaw, 0.0f))); @@ -119,7 +102,8 @@ class CFPSCamera final : public ICamera typename base_t::CGimbal m_gimbal; static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate | CVirtualGimbalEvent::Rotate; - static inline constexpr float MaxVerticalAngle = 1.53588974175501f, MinVerticalAngle = -MaxVerticalAngle; + static inline constexpr float MaxVerticalAngle = static_cast(hlsl::numbers::pi * (88.0 / 180.0)); + static inline constexpr float MinVerticalAngle = -MaxVerticalAngle; }; } diff --git a/common/include/camera/CIsometricCamera.hpp b/common/include/camera/CIsometricCamera.hpp index 68c8ff96d..647198aa5 100644 --- a/common/include/camera/CIsometricCamera.hpp +++ b/common/include/camera/CIsometricCamera.hpp @@ -30,13 +30,11 @@ class CIsometricCamera final : public CSphericalTargetCamera if (not virtualEvents.size() and not referenceFrame) return false; - auto impulse = m_gimbal.accumulate(virtualEvents); + const auto impulse = m_gimbal.accumulate(virtualEvents); - constexpr double translateScalar = 0.01; - const double panScalar = translateScalar * getMoveSpeedScale(); - const double deltaPanX = impulse.dVirtualTranslate.x * panScalar; - const double deltaPanY = impulse.dVirtualTranslate.y * panScalar; - const double deltaDistance = impulse.dVirtualTranslate.z * translateScalar; + const double deltaPanX = scaleVirtualTranslation(impulse.dVirtualTranslate.x); + const double deltaPanY = scaleVirtualTranslation(impulse.dVirtualTranslate.y); + const double deltaDistance = scaleUnscaledVirtualTranslation(impulse.dVirtualTranslate.z); m_u = IsoYaw; m_v = IsoPitch; @@ -55,8 +53,8 @@ class CIsometricCamera final : public CSphericalTargetCamera private: static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate; - static inline constexpr double IsoYaw = 0.7853981633974483; - static inline constexpr double IsoPitch = 0.6154797086703873; + static inline constexpr double IsoYaw = hlsl::numbers::pi * 0.25; + static inline constexpr double IsoPitch = hlsl::numbers::pi * (35.264389682754654 / 180.0); }; } diff --git a/common/include/camera/COrbitCamera.hpp b/common/include/camera/COrbitCamera.hpp index 421d5435a..0e4b0f895 100644 --- a/common/include/camera/COrbitCamera.hpp +++ b/common/include/camera/COrbitCamera.hpp @@ -25,17 +25,15 @@ class COrbitCamera final : public CSphericalTargetCamera virtual bool manipulate(std::span virtualEvents, const hlsl::float64_t4x4* referenceFrame = nullptr) override { - auto impulse = m_gimbal.accumulate(virtualEvents); - double deltaU = impulse.dVirtualTranslate.y, deltaV = impulse.dVirtualTranslate.x, deltaDistance = impulse.dVirtualTranslate.z; - - constexpr auto orbitMotionScalar = 0.01; - deltaU *= orbitMotionScalar * getMoveSpeedScale(); - deltaV *= orbitMotionScalar * getMoveSpeedScale(); + const auto impulse = m_gimbal.accumulate(virtualEvents); + const double deltaU = scaleVirtualTranslation(impulse.dVirtualTranslate.y); + const double deltaV = scaleVirtualTranslation(impulse.dVirtualTranslate.x); + const double deltaDistance = scaleUnscaledVirtualTranslation(impulse.dVirtualTranslate.z); m_u += deltaU; m_v += deltaV; - m_distance = std::clamp(m_distance += deltaDistance * orbitMotionScalar, MinDistance, MaxDistance); + m_distance = std::clamp(m_distance + static_cast(deltaDistance), MinDistance, MaxDistance); return applyPose(); } diff --git a/common/include/camera/CPathCamera.hpp b/common/include/camera/CPathCamera.hpp index 25cc8cd35..ccbf15737 100644 --- a/common/include/camera/CPathCamera.hpp +++ b/common/include/camera/CPathCamera.hpp @@ -2,8 +2,6 @@ #define _C_PATH_CAMERA_HPP_ #include -#include - #include "CSphericalTargetCamera.hpp" namespace nbl::core @@ -17,12 +15,20 @@ class CPathCamera final : public CSphericalTargetCamera CPathCamera(const hlsl::float64_t3& position, const hlsl::float64_t3& target) : base_t(position, target) { - const auto offset = position - target; - m_pathRadius = std::sqrt(offset.x * offset.x + offset.z * offset.z); - if (m_pathRadius < MinPathRadius) - m_pathRadius = MinPathRadius; - m_pathHeight = offset.y; - m_pathAngle = std::atan2(offset.z, offset.x); + if (!hlsl::tryBuildPathStateFromPosition( + target, + position, + MinPathRadius, + m_pathState.angle, + m_pathState.radius, + m_pathState.height)) + { + m_pathState = { + .angle = 0.0, + .radius = MinPathRadius, + .height = 0.0 + }; + } updateFromPath(); } ~CPathCamera() = default; @@ -34,14 +40,13 @@ class CPathCamera final : public CSphericalTargetCamera if (not virtualEvents.size() and not referenceFrame) return false; - auto impulse = m_gimbal.accumulate(virtualEvents); + const auto impulse = m_gimbal.accumulate(virtualEvents); - constexpr double translateScalar = 0.01; - const double moveScalar = translateScalar * getMoveSpeedScale(); - - m_pathAngle += impulse.dVirtualTranslate.z * moveScalar; - m_pathRadius = std::max(MinPathRadius, m_pathRadius + impulse.dVirtualTranslate.x * moveScalar); - m_pathHeight += impulse.dVirtualTranslate.y * moveScalar; + m_pathState.angle += scaleVirtualTranslation(impulse.dVirtualTranslate.z); + m_pathState.radius += scaleVirtualTranslation(impulse.dVirtualTranslate.x); + m_pathState.height += scaleVirtualTranslation(impulse.dVirtualTranslate.y); + if (!hlsl::sanitizePathState(m_pathState.angle, m_pathState.radius, m_pathState.height, MinPathRadius)) + return false; return updateFromPath(); } @@ -51,20 +56,20 @@ class CPathCamera final : public CSphericalTargetCamera virtual uint32_t getGoalStateMask() const override { return base_t::getGoalStateMask() | base_t::GoalStatePath; } virtual bool tryGetPathState(PathState& out) const override { - out.angle = m_pathAngle; - out.radius = m_pathRadius; - out.height = m_pathHeight; + out = m_pathState; return true; } virtual bool trySetPathState(const PathState& state) override { - if (!std::isfinite(state.angle) || !std::isfinite(state.radius) || !std::isfinite(state.height)) + auto sanitized = state; + if (!hlsl::sanitizePathState(sanitized.angle, sanitized.radius, sanitized.height, MinPathRadius)) return false; - m_pathAngle = state.angle; - m_pathRadius = std::max(MinPathRadius, state.radius); - m_pathHeight = state.height; - const bool exact = std::abs(m_pathRadius - state.radius) <= 1e-9; + const bool exact = hlsl::nearlyEqualScalar( + static_cast(sanitized.radius), + static_cast(state.radius), + static_cast(ICamera::TinyScalarEpsilon)); + m_pathState = sanitized; updateFromPath(); return exact; } @@ -73,41 +78,66 @@ class CPathCamera final : public CSphericalTargetCamera const auto clamped = std::clamp(distance, MinDistance, MaxDistance); const bool inRange = clamped == distance; - const double currentDistance = std::sqrt(m_pathRadius * m_pathRadius + m_pathHeight * m_pathHeight); - if (currentDistance > 1e-9) - { - const double scale = static_cast(clamped) / currentDistance; - m_pathRadius = std::max(MinPathRadius, m_pathRadius * scale); - m_pathHeight *= scale; - } - else - { - m_pathRadius = std::max(MinPathRadius, static_cast(clamped)); - m_pathHeight = 0.0; - } + if (!hlsl::tryScalePathStateDistance( + static_cast(clamped), + MinPathRadius, + m_pathState.radius, + m_pathState.height)) + return false; updateFromPath(); - const double appliedDistance = std::sqrt(m_pathRadius * m_pathRadius + m_pathHeight * m_pathHeight); - return inRange && std::abs(appliedDistance - static_cast(clamped)) <= 1e-6; + const double appliedDistance = hlsl::getPathDistance(m_pathState.radius, m_pathState.height); + return inRange && std::abs(appliedDistance - static_cast(clamped)) <= ICamera::ScalarTolerance; } virtual std::string_view getIdentifier() const override { return "Path Camera"; } private: static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate; - static inline constexpr double MinPathRadius = 0.1; + static inline constexpr double MinPathRadius = ICamera::SphericalMinDistance; - double m_pathAngle = 0.0; - double m_pathRadius = 1.0; - double m_pathHeight = 0.0; + PathState m_pathState = { .angle = 0.0, .radius = 1.0, .height = 0.0 }; bool updateFromPath() { - const double x = std::cos(m_pathAngle) * m_pathRadius; - const double z = std::sin(m_pathAngle) * m_pathRadius; - const hlsl::float64_t3 position = m_targetPosition + hlsl::float64_t3(x, m_pathHeight, z); - initFromPosition(position); - return applyPose(); + hlsl::float64_t3 position = hlsl::float64_t3(0.0); + hlsl::camera_quaternion_t orientation = hlsl::makeIdentityQuaternion(); + hlsl::float64_t appliedDistance = static_cast(m_distance); + double orbitU = m_u; + double orbitV = m_v; + if (!hlsl::tryBuildPathPoseFromState( + m_targetPosition, + m_pathState.angle, + m_pathState.radius, + m_pathState.height, + MinPathRadius, + static_cast(MinDistance), + static_cast(MaxDistance), + position, + orientation, + &appliedDistance, + &orbitU, + &orbitV)) + { + return false; + } + + m_distance = static_cast(appliedDistance); + m_u = orbitU; + m_v = orbitV; + + m_gimbal.begin(); + { + m_gimbal.setPosition(position); + m_gimbal.setOrientation(orientation); + } + m_gimbal.end(); + + const bool manipulated = bool(m_gimbal.getManipulationCounter()); + if (manipulated) + m_gimbal.updateView(); + + return manipulated; } }; diff --git a/common/include/camera/CSphericalTargetCamera.hpp b/common/include/camera/CSphericalTargetCamera.hpp index 667b427f4..1ee1c795a 100644 --- a/common/include/camera/CSphericalTargetCamera.hpp +++ b/common/include/camera/CSphericalTargetCamera.hpp @@ -2,8 +2,6 @@ #define _C_SPHERICAL_TARGET_CAMERA_HPP_ #include -#include - #include "ICamera.hpp" namespace nbl::core @@ -84,58 +82,81 @@ class CSphericalTargetCamera : public ICamera protected: struct SphericalBasis { - hlsl::float64_t3 localSpherePosition; - hlsl::float64_t3 right; - hlsl::float64_t3 up; - hlsl::float64_t3 forward; + hlsl::float64_t3 localSpherePosition = hlsl::float64_t3(0.0); + hlsl::float64_t3 right = hlsl::float64_t3(1.0, 0.0, 0.0); + hlsl::float64_t3 up = hlsl::float64_t3(0.0, 0.0, 1.0); + hlsl::float64_t3 forward = hlsl::float64_t3(0.0, 1.0, 0.0); }; - inline hlsl::float64_t3 S(double su, double sv) const - { - return hlsl::float64_t3 - { - std::cos(sv) * std::cos(su), - std::cos(sv) * std::sin(su), - std::sin(sv) - }; - } - - inline hlsl::float64_t3 Sdv(double su, double sv) const + inline SphericalBasis computeBasis(double orbitU, double orbitV, float distance) const { - return hlsl::float64_t3 + SphericalBasis basis; + hlsl::float64_t3 position = hlsl::float64_t3(0.0); + hlsl::camera_quaternion_t orientation = hlsl::makeIdentityQuaternion(); + if (!hlsl::tryBuildSphericalPoseFromOrbit( + m_targetPosition, + orbitU, + orbitV, + static_cast(distance), + static_cast(MinDistance), + static_cast(MaxDistance), + position, + orientation)) { - -std::sin(sv) * std::cos(su), - -std::sin(sv) * std::sin(su), - std::cos(sv) - }; - } - - inline SphericalBasis computeBasis(double su, double sv, float distance) const - { - const auto localSpherePosition = S(su, sv) * static_cast(distance); - const auto forward = hlsl::normalize(-localSpherePosition); - const auto up = hlsl::normalize(Sdv(su, sv)); - const auto right = hlsl::normalize(hlsl::cross(up, forward)); + return basis; + } - return { localSpherePosition, right, up, forward }; + basis.localSpherePosition = position - m_targetPosition; + basis.right = orientation.transformVector(hlsl::float64_t3(1.0, 0.0, 0.0), true); + basis.up = orientation.transformVector(hlsl::float64_t3(0.0, 1.0, 0.0), true); + basis.forward = orientation.transformVector(hlsl::float64_t3(0.0, 0.0, 1.0), true); + return basis; } inline void initFromPosition(const hlsl::float64_t3& position) { - const auto offset = position - m_targetPosition; - const double dist = hlsl::length(offset); - const double safeDist = std::isfinite(dist) && dist > 0.0 ? dist : static_cast(MinDistance); - m_distance = std::clamp(static_cast(safeDist), MinDistance, MaxDistance); - const auto local = offset / static_cast(m_distance); - m_u = std::atan2(local.y, local.x); - m_v = std::asin(std::clamp(local.z, -1.0, 1.0)); + double orbitU = 0.0; + double orbitV = 0.0; + hlsl::float64_t appliedDistance = static_cast(MinDistance); + if (!hlsl::tryBuildOrbitFromPosition( + m_targetPosition, + position, + static_cast(MinDistance), + static_cast(MaxDistance), + orbitU, + orbitV, + appliedDistance)) + { + m_distance = MinDistance; + m_u = 0.0; + m_v = 0.0; + return; + } + + m_distance = static_cast(appliedDistance); + m_u = orbitU; + m_v = orbitV; } inline bool applyPose() { - const auto basis = computeBasis(m_u, m_v, m_distance); - const auto newPosition = basis.localSpherePosition + m_targetPosition; - const auto newOrientation = hlsl::makeQuaternionFromBasis(basis.right, basis.up, basis.forward); + hlsl::float64_t3 newPosition = hlsl::float64_t3(0.0); + hlsl::camera_quaternion_t newOrientation = hlsl::makeIdentityQuaternion(); + hlsl::float64_t appliedDistance = static_cast(m_distance); + if (!hlsl::tryBuildSphericalPoseFromOrbit( + m_targetPosition, + m_u, + m_v, + static_cast(m_distance), + static_cast(MinDistance), + static_cast(MaxDistance), + newPosition, + newOrientation, + &appliedDistance)) + { + return false; + } + m_distance = static_cast(appliedDistance); m_gimbal.begin(); { diff --git a/common/include/camera/CTopDownCamera.hpp b/common/include/camera/CTopDownCamera.hpp index a558eb25b..07b8d0ee9 100644 --- a/common/include/camera/CTopDownCamera.hpp +++ b/common/include/camera/CTopDownCamera.hpp @@ -29,14 +29,12 @@ class CTopDownCamera final : public CSphericalTargetCamera if (not virtualEvents.size() and not referenceFrame) return false; - auto impulse = m_gimbal.accumulate(virtualEvents); + const auto impulse = m_gimbal.accumulate(virtualEvents); - const double deltaYaw = impulse.dVirtualRotation.y * getRotationSpeedScale(); - constexpr double translateScalar = 0.01; - const double panScalar = translateScalar * getMoveSpeedScale(); - const double deltaPanX = impulse.dVirtualTranslate.x * panScalar; - const double deltaPanY = impulse.dVirtualTranslate.y * panScalar; - const double deltaDistance = impulse.dVirtualTranslate.z * translateScalar; + const double deltaYaw = scaleVirtualRotation(impulse.dVirtualRotation.y); + const double deltaPanX = scaleVirtualTranslation(impulse.dVirtualTranslate.x); + const double deltaPanY = scaleVirtualTranslation(impulse.dVirtualTranslate.y); + const double deltaDistance = scaleUnscaledVirtualTranslation(impulse.dVirtualTranslate.z); m_u += deltaYaw; m_v = TopDownPitch; @@ -55,7 +53,7 @@ class CTopDownCamera final : public CSphericalTargetCamera private: static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate | CVirtualGimbalEvent::Rotate; - static inline constexpr double TopDownPitch = -1.5707963267948966; + static inline constexpr double TopDownPitch = -hlsl::numbers::pi * 0.5; }; } diff --git a/common/include/camera/CTurntableCamera.hpp b/common/include/camera/CTurntableCamera.hpp index abbcd086a..1b4bb8b45 100644 --- a/common/include/camera/CTurntableCamera.hpp +++ b/common/include/camera/CTurntableCamera.hpp @@ -33,13 +33,11 @@ class CTurntableCamera final : public CSphericalTargetCamera if (not virtualEvents.size() and not referenceFrame) return false; - auto impulse = m_gimbal.accumulate(virtualEvents); + const auto impulse = m_gimbal.accumulate(virtualEvents); - const double deltaYaw = impulse.dVirtualRotation.y * getRotationSpeedScale(); - const double deltaPitch = impulse.dVirtualRotation.x * getRotationSpeedScale(); - - constexpr double translateScalar = 0.01; - const double deltaDistance = impulse.dVirtualTranslate.z * translateScalar; + const double deltaYaw = scaleVirtualRotation(impulse.dVirtualRotation.y); + const double deltaPitch = scaleVirtualRotation(impulse.dVirtualRotation.x); + const double deltaDistance = scaleUnscaledVirtualTranslation(impulse.dVirtualTranslate.z); m_u += deltaYaw; m_v = std::clamp(m_v + deltaPitch, MinPitch, MaxPitch); @@ -58,7 +56,7 @@ class CTurntableCamera final : public CSphericalTargetCamera private: static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate | CVirtualGimbalEvent::Rotate; - static inline constexpr double MaxPitch = 1.5533430342749532; + static inline constexpr double MaxPitch = hlsl::numbers::pi * (89.0 / 180.0); static inline constexpr double MinPitch = -MaxPitch; }; diff --git a/common/include/camera/ICamera.hpp b/common/include/camera/ICamera.hpp index 285996978..409fee652 100644 --- a/common/include/camera/ICamera.hpp +++ b/common/include/camera/ICamera.hpp @@ -25,12 +25,19 @@ class ICamera : virtual public core::IReferenceCounted public: static inline constexpr float SphericalMinDistance = 0.1f; static inline constexpr float SphericalMaxDistance = 10000.f; + static inline constexpr double VirtualTranslationStep = 0.01; + static inline constexpr double DefaultMoveSpeedScale = VirtualTranslationStep; + static inline constexpr double DefaultRotationSpeedScale = 0.003; + static inline constexpr double ScalarTolerance = 1e-6; + static inline constexpr double TinyScalarEpsilon = 1e-9; + static inline constexpr double DefaultPositionTolerance = 2.0 * ScalarTolerance; + static inline constexpr double DefaultAngularToleranceDeg = 0.1; struct SMotionConfig { //! Camera-local scales applied by implementations to virtual motion magnitude. - double moveSpeedScale = 0.01; - double rotationSpeedScale = 0.003; + double moveSpeedScale = DefaultMoveSpeedScale; + double rotationSpeedScale = DefaultRotationSpeedScale; }; enum class CameraKind : uint8_t @@ -255,6 +262,26 @@ class ICamera : virtual public core::IReferenceCounted inline double getMoveSpeedScale() const { return m_motionConfig.moveSpeedScale; } inline double getRotationSpeedScale() const { return m_motionConfig.rotationSpeedScale; } inline const SMotionConfig& getMotionConfig() const { return m_motionConfig; } + inline double getScaledVirtualTranslationMagnitude() const + { + return VirtualTranslationStep * getMoveSpeedScale(); + } + inline double getUnscaledVirtualTranslationMagnitude() const + { + return VirtualTranslationStep; + } + inline double scaleVirtualTranslation(const double magnitude) const + { + return magnitude * getScaledVirtualTranslationMagnitude(); + } + inline double scaleUnscaledVirtualTranslation(const double magnitude) const + { + return magnitude * getUnscaledVirtualTranslationMagnitude(); + } + inline double scaleVirtualRotation(const double magnitude) const + { + return magnitude * getRotationSpeedScale(); + } inline SScopedMotionScaleOverride overrideMotionScales(const double moveScale, const double rotationScale) { return SScopedMotionScaleOverride(this, moveScale, rotationScale); diff --git a/common/include/camera/IGimbal.hpp b/common/include/camera/IGimbal.hpp index 0a007982e..632e0fa06 100644 --- a/common/include/camera/IGimbal.hpp +++ b/common/include/camera/IGimbal.hpp @@ -296,15 +296,21 @@ namespace nbl::core out->frame = *referenceFrame; if (not hlsl::isOrthoBase(hlsl::float64_t3(out->frame[0]), hlsl::float64_t3(out->frame[1]), hlsl::float64_t3(out->frame[2]))) return false; + out->orientation = hlsl::makeQuaternionFromBasis( + hlsl::float64_t3(out->frame[0]), + hlsl::float64_t3(out->frame[1]), + hlsl::float64_t3(out->frame[2])); } else { out->frame = hlsl::getMatrix3x3As4x4(getOrthonornalMatrix()); out->frame[3] = hlsl::float64_t4(getPosition(), 1); + out->orientation = hlsl::makeQuaternionFromBasis( + hlsl::float64_t3(out->frame[0]), + hlsl::float64_t3(out->frame[1]), + hlsl::float64_t3(out->frame[2])); } - out->orientation = hlsl::makeQuaternionFromBasis(hlsl::float64_t3(out->frame[0]), hlsl::float64_t3(out->frame[1]), hlsl::float64_t3(out->frame[2])); - return true; } diff --git a/common/include/camera/IGimbalInputProcessor.hpp b/common/include/camera/IGimbalInputProcessor.hpp index 1dbb3b23b..a1ce8912e 100644 --- a/common/include/camera/IGimbalInputProcessor.hpp +++ b/common/include/camera/IGimbalInputProcessor.hpp @@ -15,6 +15,8 @@ namespace nbl::ui class IGimbalInputProcessor : public CGimbalBindingLayoutStorage { public: + static inline constexpr double MaxFrameDeltaMs = 200.0; + using CGimbalBindingLayoutStorage::CGimbalBindingLayoutStorage; IGimbalInputProcessor() = default; @@ -33,7 +35,6 @@ class IGimbalInputProcessor : public CGimbalBindingLayoutStorage { m_nextPresentationTimeStamp = nextPresentationTimeStamp; const auto deltaMs = std::chrono::duration_cast(m_nextPresentationTimeStamp - m_lastVirtualUpTimeStamp).count(); - constexpr double MaxFrameDeltaMs = 200.0; if (deltaMs < 0) m_frameDeltaTime = 0.0; else if (static_cast(deltaMs) > MaxFrameDeltaMs) From ac3a6a5953d8f081d0004d5b3dcf250b8db5e563 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Wed, 8 Apr 2026 07:47:45 +0200 Subject: [PATCH 180/205] Unify camera rigid math and resource loading --- 61_UI/AppInit.cpp | 64 +- 61_UI/AppTransformEditor.cpp | 20 +- 61_UI/include/app/AppResourceUtilities.hpp | 83 +++ .../camera/CCameraInputBindingUtilities.hpp | 695 +++++++++--------- .../include/camera/CCameraMathUtilities.hpp | 104 ++- .../camera/CCameraScriptedCheckRunner.hpp | 111 +-- common/include/camera/IGimbal.hpp | 15 +- .../include/camera/IGimbalInputProcessor.hpp | 27 +- 8 files changed, 611 insertions(+), 508 deletions(-) diff --git a/61_UI/AppInit.cpp b/61_UI/AppInit.cpp index a63c0ad84..7d0675475 100644 --- a/61_UI/AppInit.cpp +++ b/61_UI/AppInit.cpp @@ -76,22 +76,22 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) if (!asset_base_t::onAppInitialized(std::move(system))) return fail("Failed to initialize mounted resources for headless camera smoke."); - auto configPath = [&]() -> std::filesystem::path - { - if (program.is_used("--file")) - { - std::filesystem::path path = program.get("--file"); - if (path.is_relative()) - path = localInputCWD / path; - return path.lexically_normal(); - } - return std::filesystem::path(nbl::system::SCameraAppResourcePaths::DefaultCameraConfigRelativePath); - }(); - camera_json_t j; std::string jsonError; - if (!nbl::system::loadJsonFromPath(*m_system, configPath, j, &jsonError)) + if (program.is_used("--file")) + { + const auto configPath = std::filesystem::path(program.get("--file")); + if (!nbl::system::loadJsonFromMountedResourceOrResolvedPath(*m_system, localInputCWD, configPath, j, nullptr, &jsonError)) + return fail(jsonError); + } + else if (!nbl::system::loadJsonFromPath( + *m_system, + std::filesystem::path(nbl::system::SCameraAppResourcePaths::DefaultCameraConfigRelativePath), + j, + &jsonError)) + { return fail(jsonError); + } std::vector> cameras; if (!nbl::system::tryLoadCameraCollectionFromJson(j, jsonError, cameras)) @@ -327,7 +327,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) .frame = 1u, .camera = orbitCamera.get() }); - if (frameResult.hadFailures || state.nextCheckIndex != 1u || !state.baselineValid || !state.stepValid) + if (frameResult.hadFailures || state.nextCheckIndex != 1u || !state.baseline.valid || !state.step.valid) { const auto& gimbal = orbitCamera->getGimbal(); const auto pos = gimbal.getPosition(); @@ -335,11 +335,11 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) const auto basis = gimbal.getOrthonornalMatrix(); const auto eulerDeg = getQuaternionEulerDegrees(gimbal.getOrientation()); std::ostringstream oss; - oss << std::fixed << std::setprecision(6) - << "Scripted check runner baseline smoke failed." - << " nextCheckIndex=" << state.nextCheckIndex - << " baselineValid=" << state.baselineValid - << " stepValid=" << state.stepValid + 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 << ")" @@ -1678,19 +1678,14 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) std::string jsonError; std::filesystem::path resolvedCameraJsonFile; const bool hasUserConfig = cameraJsonFile.has_value(); - if (hasUserConfig) + if (hasUserConfig && nbl::system::loadJsonFromMountedResourceOrResolvedPath(*m_system, localInputCWD, cameraJsonFile.value(), j, &resolvedCameraJsonFile, &jsonError)) { - resolvedCameraJsonFile = nbl::system::resolveInputPath(localInputCWD, cameraJsonFile.value()); - } - - if (hasUserConfig && nbl::system::loadJsonFromPath(*m_system, resolvedCameraJsonFile, j, &jsonError)) - { - // Loaded from explicit user path. + // Loaded from mounted resources alias or explicit filesystem override. } else { if (hasUserConfig) - m_logger->log("Cannot open input \"%s\" json file (%s). Switching to default config.", ILogger::ELL_WARNING, resolvedCameraJsonFile.string().c_str(), jsonError.c_str()); + m_logger->log("Cannot open input \"%s\" json file (%s). Switching to default config.", ILogger::ELL_WARNING, cameraJsonFile.value().string().c_str(), jsonError.c_str()); else m_logger->log("No input json file provided. Switching to default config.", ILogger::ELL_INFO); @@ -1783,9 +1778,17 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) if (program.is_used("--script")) { - nbl::system::path scriptPath = nbl::system::resolveInputPath(localInputCWD, program.get("--script")); + nbl::system::path scriptPath = program.get("--script"); + std::string scriptedInputText; + if (!nbl::system::loadTextFromMountedResourceOrResolvedPath(*m_system, localInputCWD, scriptPath, scriptedInputText, &scriptPath)) + { + logFail("Camera sequence script parse failed: Cannot open scripted input file."); + return false; + } + + std::stringstream scriptedInputStream(scriptedInputText); nbl::system::CCameraScriptedInputParseResult parsed; - if (!nbl::system::loadCameraScriptedInputFromFile(scriptPath, parsed, &scriptedInputParseError)) + if (!nbl::system::readCameraScriptedInput(scriptedInputStream, parsed, &scriptedInputParseError)) { logFail("Camera sequence script parse failed: %s", scriptedInputParseError.c_str()); return false; @@ -2477,8 +2480,7 @@ bool App::onAppInitialized(smart_refctd_ptr&& system) { nbl::system::SSpaceEnvBlobHeader envBlobHeader = {}; std::vector envBlobPayload; - const auto spaceEnvSearchRoots = nbl::system::makeSpaceEnvSearchRoots(localInputCWD); - nbl::system::loadFirstSpaceEnvBlobFromRoots(*m_system, spaceEnvSearchRoots, envBlobHeader, envBlobPayload); + nbl::system::loadPreferredSpaceEnvBlob(*m_system, localInputCWD, envBlobHeader, envBlobPayload); if (envBlobPayload.empty()) return logFail("Failed to load space environment blob from available assets."); diff --git a/61_UI/AppTransformEditor.cpp b/61_UI/AppTransformEditor.cpp index 2d4a40830..5d2fde78a 100644 --- a/61_UI/AppTransformEditor.cpp +++ b/61_UI/AppTransformEditor.cpp @@ -91,26 +91,28 @@ void App::TransformEditorContents() if (ImGui::RadioButton("Scale", mCurrentGizmoOperation == ImGuizmo::SCALE)) mCurrentGizmoOperation = ImGuizmo::SCALE; - float32_t3 matrixTranslation, matrixRotation, matrixScale; + hlsl::SRigidTransformComponents transformState = {}; + float32_t3 matrixRotation = float32_t3(0.0f); imguizmoModel.outDeltaTRS = hlsl::float32_t4x4(1.0f); - if (!hlsl::decomposeTransformMatrix(imguizmoModel.outTRS, matrixTranslation, matrixRotation, matrixScale)) + if (!hlsl::tryExtractRigidTransformComponents(imguizmoModel.outTRS, transformState)) { - matrixTranslation = float32_t3(imguizmoModel.outTRS[3].x, imguizmoModel.outTRS[3].y, imguizmoModel.outTRS[3].z); - matrixRotation = float32_t3(0.0f); - matrixScale = float32_t3(1.0f); + transformState.translation = float32_t3(imguizmoModel.outTRS[3].x, imguizmoModel.outTRS[3].y, imguizmoModel.outTRS[3].z); + transformState.orientation = hlsl::makeIdentityQuaternion(); + transformState.scale = float32_t3(1.0f); } + matrixRotation = hlsl::getQuaternionEulerDegrees(transformState.orientation); { ImGuiInputTextFlags flags = 0; - ImGui::InputFloat3("Tr", &matrixTranslation[0], "%.3f", flags); + ImGui::InputFloat3("Tr", &transformState.translation[0], "%.3f", flags); ImGui::InputFloat3("Rt", &matrixRotation[0], "%.3f", flags); - ImGui::InputFloat3("Sc", &matrixScale[0], "%.3f", flags); + ImGui::InputFloat3("Sc", &transformState.scale[0], "%.3f", flags); } imguizmoModel.outTRS = hlsl::composeTransformMatrix( - matrixTranslation, + transformState.translation, hlsl::makeQuaternionFromEulerDegrees(matrixRotation), - matrixScale); + transformState.scale); m16TRSmatrix = &imguizmoModel.outTRS[0][0]; if (mCurrentGizmoOperation != ImGuizmo::SCALE) diff --git a/61_UI/include/app/AppResourceUtilities.hpp b/61_UI/include/app/AppResourceUtilities.hpp index 82cf82d51..18458802d 100644 --- a/61_UI/include/app/AppResourceUtilities.hpp +++ b/61_UI/include/app/AppResourceUtilities.hpp @@ -108,6 +108,59 @@ inline path resolveInputPath(const path& localInputCWD, path pathValue) return pathValue; } +inline bool loadTextFromMountedResourceOrResolvedPath( + ISystem& system, + const path& localInputCWD, + const path& pathValue, + std::string& outText, + path* outLoadedPath = nullptr) +{ + if (loadFileText(system, pathValue, outText)) + { + if (outLoadedPath) + *outLoadedPath = pathValue; + return true; + } + + const auto resolvedPath = resolveInputPath(localInputCWD, pathValue); + if (resolvedPath == pathValue) + return false; + + if (!loadFileText(system, resolvedPath, outText)) + return false; + + if (outLoadedPath) + *outLoadedPath = resolvedPath; + return true; +} + +inline bool loadJsonFromMountedResourceOrResolvedPath( + ISystem& system, + const path& localInputCWD, + const path& pathValue, + camera_json_t& outJson, + path* outLoadedPath = nullptr, + std::string* error = nullptr) +{ + if (loadJsonFromPath(system, pathValue, outJson, error)) + { + if (outLoadedPath) + *outLoadedPath = pathValue; + return true; + } + + const auto resolvedPath = resolveInputPath(localInputCWD, pathValue); + if (resolvedPath == pathValue) + return false; + + if (!loadJsonFromPath(system, resolvedPath, outJson, error)) + return false; + + if (outLoadedPath) + *outLoadedPath = resolvedPath; + return true; +} + inline bool parseSpaceEnvBlobBytes( std::span blobBytes, SSpaceEnvBlobHeader& outHeader, @@ -174,6 +227,36 @@ inline bool loadFirstSpaceEnvBlobFromRoots( return false; } +inline bool loadPreferredSpaceEnvBlob( + ISystem& system, + const path& localInputCWD, + SSpaceEnvBlobHeader& outHeader, + std::vector& outPayload, + path* outLoadedPath = nullptr) +{ + const path mountedPath = path(SCameraAppResourcePaths::AppResourcesWorkingDirectory) / path(SCameraAppResourcePaths::SpaceEnvBlobCandidate); + if (loadSpaceEnvBlob(system, mountedPath, outHeader, outPayload)) + { + if (outLoadedPath) + *outLoadedPath = mountedPath; + return true; + } + + const auto searchRoots = makeSpaceEnvSearchRoots(localInputCWD); + for (const auto& root : searchRoots) + { + const auto candidate = root / path(SCameraAppResourcePaths::SpaceEnvBlobCandidate); + if (!loadSpaceEnvBlob(system, candidate, outHeader, outPayload)) + continue; + + if (outLoadedPath) + *outLoadedPath = candidate; + return true; + } + + return false; +} + inline core::smart_refctd_ptr loadPrecompiledShaderFromAppResources( asset::IAssetManager& assetManager, ILogger* logger, diff --git a/common/include/camera/CCameraInputBindingUtilities.hpp b/common/include/camera/CCameraInputBindingUtilities.hpp index 01d341c6a..f7bb8435c 100644 --- a/common/include/camera/CCameraInputBindingUtilities.hpp +++ b/common/include/camera/CCameraInputBindingUtilities.hpp @@ -1,6 +1,8 @@ #ifndef _NBL_C_CAMERA_INPUT_BINDING_UTILITIES_HPP_ #define _NBL_C_CAMERA_INPUT_BINDING_UTILITIES_HPP_ +#include + #include "ICamera.hpp" #include "IGimbalBindingLayout.hpp" @@ -18,96 +20,112 @@ struct SCameraInputBindingPreset namespace impl { -inline IGimbalBindingLayout::keyboard_to_virtual_events_t makeKeyboardPreset( - std::initializer_list> bindings) -{ - IGimbalBindingLayout::keyboard_to_virtual_events_t preset; - for (const auto& [code, event] : bindings) - preset.emplace(code, IGimbalBindingLayout::CHashInfo(event)); - return preset; -} - -inline IGimbalBindingLayout::mouse_to_virtual_events_t makeMousePreset( - std::initializer_list> bindings) -{ - IGimbalBindingLayout::mouse_to_virtual_events_t preset; - for (const auto& [code, event] : bindings) - preset.emplace(code, IGimbalBindingLayout::CHashInfo(event)); - return preset; -} +struct SKeyboardPresetSpec +{ + std::array wasd = { + core::CVirtualGimbalEvent::None, + core::CVirtualGimbalEvent::None, + core::CVirtualGimbalEvent::None, + core::CVirtualGimbalEvent::None + }; + std::array qe = { + core::CVirtualGimbalEvent::None, + core::CVirtualGimbalEvent::None + }; + std::array ijkl = { + core::CVirtualGimbalEvent::None, + core::CVirtualGimbalEvent::None, + core::CVirtualGimbalEvent::None, + core::CVirtualGimbalEvent::None + }; +}; -inline IGimbalBindingLayout::keyboard_to_virtual_events_t extendKeyboardPreset( - IGimbalBindingLayout::keyboard_to_virtual_events_t preset, - std::initializer_list> bindings) -{ - for (const auto& [code, event] : bindings) - preset.emplace(code, IGimbalBindingLayout::CHashInfo(event)); - return preset; -} +struct SMousePresetSpec +{ + std::array relative = { + core::CVirtualGimbalEvent::None, + core::CVirtualGimbalEvent::None, + core::CVirtualGimbalEvent::None, + core::CVirtualGimbalEvent::None + }; + std::array scroll = { + core::CVirtualGimbalEvent::None, + core::CVirtualGimbalEvent::None + }; +}; -inline IGimbalBindingLayout::mouse_to_virtual_events_t extendMousePreset( - IGimbalBindingLayout::mouse_to_virtual_events_t preset, - std::initializer_list> bindings) +template +inline void appendBindingSpec(Map& preset, const Codes& codes, const Events& events) { - for (const auto& [code, event] : bindings) - preset.emplace(code, IGimbalBindingLayout::CHashInfo(event)); - return preset; + for (size_t i = 0u; i < codes.size() && i < events.size(); ++i) + { + const auto event = events[i]; + if (event == core::CVirtualGimbalEvent::None) + continue; + preset.emplace(codes[i], IGimbalBindingLayout::CHashInfo(event)); + } } -inline IGimbalBindingLayout::keyboard_to_virtual_events_t makeWasdKeyboardPreset( - const core::CVirtualGimbalEvent::VirtualEventType w, - const core::CVirtualGimbalEvent::VirtualEventType s, - const core::CVirtualGimbalEvent::VirtualEventType a, - const core::CVirtualGimbalEvent::VirtualEventType d) -{ - return makeKeyboardPreset({ - { ui::E_KEY_CODE::EKC_W, w }, - { ui::E_KEY_CODE::EKC_S, s }, - { ui::E_KEY_CODE::EKC_A, a }, - { ui::E_KEY_CODE::EKC_D, d } - }); -} +inline IGimbalBindingLayout::keyboard_to_virtual_events_t buildKeyboardPreset(const SKeyboardPresetSpec& spec) +{ + static constexpr std::array KeyboardWasdCodes = { + ui::E_KEY_CODE::EKC_W, + ui::E_KEY_CODE::EKC_S, + ui::E_KEY_CODE::EKC_A, + ui::E_KEY_CODE::EKC_D + }; + static constexpr std::array KeyboardQeCodes = { + ui::E_KEY_CODE::EKC_Q, + ui::E_KEY_CODE::EKC_E + }; + static constexpr std::array KeyboardIjklCodes = { + ui::E_KEY_CODE::EKC_I, + ui::E_KEY_CODE::EKC_K, + ui::E_KEY_CODE::EKC_J, + ui::E_KEY_CODE::EKC_L + }; -inline IGimbalBindingLayout::keyboard_to_virtual_events_t appendIjklLookKeyboardPreset( - IGimbalBindingLayout::keyboard_to_virtual_events_t preset, - const core::CVirtualGimbalEvent::VirtualEventType i, - const core::CVirtualGimbalEvent::VirtualEventType k, - const core::CVirtualGimbalEvent::VirtualEventType j, - const core::CVirtualGimbalEvent::VirtualEventType l) -{ - return extendKeyboardPreset(std::move(preset), { - { ui::E_KEY_CODE::EKC_I, i }, - { ui::E_KEY_CODE::EKC_K, k }, - { ui::E_KEY_CODE::EKC_J, j }, - { ui::E_KEY_CODE::EKC_L, l } - }); + IGimbalBindingLayout::keyboard_to_virtual_events_t preset; + appendBindingSpec(preset, KeyboardWasdCodes, spec.wasd); + appendBindingSpec(preset, KeyboardQeCodes, spec.qe); + appendBindingSpec(preset, KeyboardIjklCodes, spec.ijkl); + return preset; } -inline IGimbalBindingLayout::mouse_to_virtual_events_t makeRelativeMousePreset( - const core::CVirtualGimbalEvent::VirtualEventType positiveX, - const core::CVirtualGimbalEvent::VirtualEventType negativeX, - const core::CVirtualGimbalEvent::VirtualEventType positiveY, - const core::CVirtualGimbalEvent::VirtualEventType negativeY) +inline IGimbalBindingLayout::mouse_to_virtual_events_t buildMousePreset(const SMousePresetSpec& spec) { - return makeMousePreset({ - { ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_X, positiveX }, - { ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_X, negativeX }, - { ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_Y, positiveY }, - { ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_Y, negativeY } - }); -} + static constexpr std::array RelativeMouseCodes = { + ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_X, + ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_X, + ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_Y, + ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_Y + }; + static constexpr std::array PositiveScrollCodes = { + ui::E_MOUSE_CODE::EMC_VERTICAL_POSITIVE_SCROLL, + ui::E_MOUSE_CODE::EMC_HORIZONTAL_POSITIVE_SCROLL + }; + static constexpr std::array NegativeScrollCodes = { + ui::E_MOUSE_CODE::EMC_VERTICAL_NEGATIVE_SCROLL, + ui::E_MOUSE_CODE::EMC_HORIZONTAL_NEGATIVE_SCROLL + }; -inline IGimbalBindingLayout::mouse_to_virtual_events_t appendSymmetricScrollPreset( - IGimbalBindingLayout::mouse_to_virtual_events_t preset, - const core::CVirtualGimbalEvent::VirtualEventType positive, - const core::CVirtualGimbalEvent::VirtualEventType negative) -{ - return extendMousePreset(std::move(preset), { - { ui::E_MOUSE_CODE::EMC_VERTICAL_POSITIVE_SCROLL, positive }, - { ui::E_MOUSE_CODE::EMC_HORIZONTAL_POSITIVE_SCROLL, positive }, - { ui::E_MOUSE_CODE::EMC_VERTICAL_NEGATIVE_SCROLL, negative }, - { ui::E_MOUSE_CODE::EMC_HORIZONTAL_NEGATIVE_SCROLL, negative } - }); + IGimbalBindingLayout::mouse_to_virtual_events_t preset; + appendBindingSpec(preset, RelativeMouseCodes, spec.relative); + if (spec.scroll[0] != core::CVirtualGimbalEvent::None) + { + appendBindingSpec( + preset, + PositiveScrollCodes, + std::array{ spec.scroll[0], spec.scroll[0] }); + } + if (spec.scroll[1] != core::CVirtualGimbalEvent::None) + { + appendBindingSpec( + preset, + NegativeScrollCodes, + std::array{ spec.scroll[1], spec.scroll[1] }); + } + return preset; } inline IGimbalBindingLayout::imguizmo_to_virtual_events_t makeImguizmoPreset(const uint32_t allowedVirtualEvents) @@ -136,298 +154,269 @@ inline const IGimbalBindingLayout::mouse_to_virtual_events_t& emptyMousePreset() return preset; } -inline const IGimbalBindingLayout::keyboard_to_virtual_events_t& fpsKeyboardPreset() -{ - static const auto preset = appendIjklLookKeyboardPreset( - makeWasdKeyboardPreset( - core::CVirtualGimbalEvent::MoveForward, - core::CVirtualGimbalEvent::MoveBackward, - core::CVirtualGimbalEvent::MoveLeft, - core::CVirtualGimbalEvent::MoveRight), - core::CVirtualGimbalEvent::TiltDown, - core::CVirtualGimbalEvent::TiltUp, - core::CVirtualGimbalEvent::PanLeft, - core::CVirtualGimbalEvent::PanRight); - return preset; -} - -inline const IGimbalBindingLayout::keyboard_to_virtual_events_t& freeKeyboardPreset() -{ - static const auto preset = extendKeyboardPreset(fpsKeyboardPreset(), { - { ui::E_KEY_CODE::EKC_Q, core::CVirtualGimbalEvent::RollLeft }, - { ui::E_KEY_CODE::EKC_E, core::CVirtualGimbalEvent::RollRight } - }); - return preset; -} - -inline const IGimbalBindingLayout::keyboard_to_virtual_events_t& orbitKeyboardPreset() +inline const IGimbalBindingLayout::keyboard_to_virtual_events_t& keyboardPresetForKind(const core::ICamera::CameraKind kind) { - static const auto preset = extendKeyboardPreset( - makeWasdKeyboardPreset( - core::CVirtualGimbalEvent::MoveUp, - core::CVirtualGimbalEvent::MoveDown, - core::CVirtualGimbalEvent::MoveLeft, - core::CVirtualGimbalEvent::MoveRight), + switch (kind) + { + case core::ICamera::CameraKind::FPS: { - { ui::E_KEY_CODE::EKC_E, core::CVirtualGimbalEvent::MoveForward }, - { ui::E_KEY_CODE::EKC_Q, core::CVirtualGimbalEvent::MoveBackward } - }); - return preset; -} - -inline const IGimbalBindingLayout::keyboard_to_virtual_events_t& arcballKeyboardPreset() -{ - static const auto preset = appendIjklLookKeyboardPreset( - extendKeyboardPreset( - makeWasdKeyboardPreset( - core::CVirtualGimbalEvent::MoveForward, - core::CVirtualGimbalEvent::MoveBackward, - core::CVirtualGimbalEvent::MoveLeft, - core::CVirtualGimbalEvent::MoveRight), - { - { ui::E_KEY_CODE::EKC_Q, core::CVirtualGimbalEvent::MoveDown }, - { ui::E_KEY_CODE::EKC_E, core::CVirtualGimbalEvent::MoveUp }, - }), - core::CVirtualGimbalEvent::TiltDown, - core::CVirtualGimbalEvent::TiltUp, - core::CVirtualGimbalEvent::PanLeft, - core::CVirtualGimbalEvent::PanRight); - return preset; -} - -inline const IGimbalBindingLayout::keyboard_to_virtual_events_t& turntableKeyboardPreset() -{ - static const auto preset = appendIjklLookKeyboardPreset( - makeWasdKeyboardPreset( - core::CVirtualGimbalEvent::MoveForward, - core::CVirtualGimbalEvent::MoveBackward, - core::CVirtualGimbalEvent::PanLeft, - core::CVirtualGimbalEvent::PanRight), - core::CVirtualGimbalEvent::TiltDown, - core::CVirtualGimbalEvent::TiltUp, - core::CVirtualGimbalEvent::PanLeft, - core::CVirtualGimbalEvent::PanRight); - return preset; -} - -inline const IGimbalBindingLayout::keyboard_to_virtual_events_t& topDownKeyboardPreset() -{ - static const auto preset = extendKeyboardPreset( - makeWasdKeyboardPreset( - core::CVirtualGimbalEvent::MoveUp, - core::CVirtualGimbalEvent::MoveDown, - core::CVirtualGimbalEvent::MoveLeft, - core::CVirtualGimbalEvent::MoveRight), + static const auto preset = buildKeyboardPreset({ + .wasd = { + core::CVirtualGimbalEvent::MoveForward, + core::CVirtualGimbalEvent::MoveBackward, + core::CVirtualGimbalEvent::MoveLeft, + core::CVirtualGimbalEvent::MoveRight + }, + .ijkl = { + core::CVirtualGimbalEvent::TiltDown, + core::CVirtualGimbalEvent::TiltUp, + core::CVirtualGimbalEvent::PanLeft, + core::CVirtualGimbalEvent::PanRight + } + }); + return preset; + } + case core::ICamera::CameraKind::Free: { - { ui::E_KEY_CODE::EKC_Q, core::CVirtualGimbalEvent::MoveBackward }, - { ui::E_KEY_CODE::EKC_E, core::CVirtualGimbalEvent::MoveForward }, - { ui::E_KEY_CODE::EKC_J, core::CVirtualGimbalEvent::PanLeft }, - { ui::E_KEY_CODE::EKC_L, core::CVirtualGimbalEvent::PanRight } - }); - return preset; -} - -inline const IGimbalBindingLayout::keyboard_to_virtual_events_t& isometricKeyboardPreset() -{ - static const auto preset = extendKeyboardPreset( - makeWasdKeyboardPreset( - core::CVirtualGimbalEvent::MoveUp, - core::CVirtualGimbalEvent::MoveDown, - core::CVirtualGimbalEvent::MoveLeft, - core::CVirtualGimbalEvent::MoveRight), + static const auto preset = buildKeyboardPreset({ + .wasd = { + core::CVirtualGimbalEvent::MoveForward, + core::CVirtualGimbalEvent::MoveBackward, + core::CVirtualGimbalEvent::MoveLeft, + core::CVirtualGimbalEvent::MoveRight + }, + .qe = { + core::CVirtualGimbalEvent::RollLeft, + core::CVirtualGimbalEvent::RollRight + }, + .ijkl = { + core::CVirtualGimbalEvent::TiltDown, + core::CVirtualGimbalEvent::TiltUp, + core::CVirtualGimbalEvent::PanLeft, + core::CVirtualGimbalEvent::PanRight + } + }); + return preset; + } + case core::ICamera::CameraKind::Orbit: { - { ui::E_KEY_CODE::EKC_Q, core::CVirtualGimbalEvent::MoveBackward }, - { ui::E_KEY_CODE::EKC_E, core::CVirtualGimbalEvent::MoveForward } - }); - return preset; -} - -inline const IGimbalBindingLayout::keyboard_to_virtual_events_t& chaseKeyboardPreset() -{ - return arcballKeyboardPreset(); -} - -inline const IGimbalBindingLayout::keyboard_to_virtual_events_t& dollyKeyboardPreset() -{ - return arcballKeyboardPreset(); -} - -inline const IGimbalBindingLayout::keyboard_to_virtual_events_t& dollyZoomKeyboardPreset() -{ - static const auto preset = extendKeyboardPreset( - makeWasdKeyboardPreset( - core::CVirtualGimbalEvent::MoveUp, - core::CVirtualGimbalEvent::MoveDown, - core::CVirtualGimbalEvent::MoveLeft, - core::CVirtualGimbalEvent::MoveRight), + static const auto preset = buildKeyboardPreset({ + .wasd = { + core::CVirtualGimbalEvent::MoveUp, + core::CVirtualGimbalEvent::MoveDown, + core::CVirtualGimbalEvent::MoveLeft, + core::CVirtualGimbalEvent::MoveRight + }, + .qe = { + core::CVirtualGimbalEvent::MoveBackward, + core::CVirtualGimbalEvent::MoveForward + } + }); + return preset; + } + case core::ICamera::CameraKind::Arcball: + case core::ICamera::CameraKind::Chase: + case core::ICamera::CameraKind::Dolly: { - { ui::E_KEY_CODE::EKC_E, core::CVirtualGimbalEvent::MoveForward }, - { ui::E_KEY_CODE::EKC_Q, core::CVirtualGimbalEvent::MoveBackward } - }); - return preset; -} - -inline const IGimbalBindingLayout::keyboard_to_virtual_events_t& pathKeyboardPreset() -{ - static const auto preset = extendKeyboardPreset( - makeWasdKeyboardPreset( - core::CVirtualGimbalEvent::MoveForward, - core::CVirtualGimbalEvent::MoveBackward, - core::CVirtualGimbalEvent::MoveLeft, - core::CVirtualGimbalEvent::MoveRight), + static const auto preset = buildKeyboardPreset({ + .wasd = { + core::CVirtualGimbalEvent::MoveForward, + core::CVirtualGimbalEvent::MoveBackward, + core::CVirtualGimbalEvent::MoveLeft, + core::CVirtualGimbalEvent::MoveRight + }, + .qe = { + core::CVirtualGimbalEvent::MoveDown, + core::CVirtualGimbalEvent::MoveUp + }, + .ijkl = { + core::CVirtualGimbalEvent::TiltDown, + core::CVirtualGimbalEvent::TiltUp, + core::CVirtualGimbalEvent::PanLeft, + core::CVirtualGimbalEvent::PanRight + } + }); + return preset; + } + case core::ICamera::CameraKind::Turntable: { - { ui::E_KEY_CODE::EKC_Q, core::CVirtualGimbalEvent::MoveDown }, - { ui::E_KEY_CODE::EKC_E, core::CVirtualGimbalEvent::MoveUp } - }); - return preset; -} - -inline const IGimbalBindingLayout::mouse_to_virtual_events_t& fpsMousePreset() -{ - static const auto preset = makeRelativeMousePreset( - core::CVirtualGimbalEvent::PanRight, - core::CVirtualGimbalEvent::PanLeft, - core::CVirtualGimbalEvent::TiltUp, - core::CVirtualGimbalEvent::TiltDown); - return preset; -} - -inline const IGimbalBindingLayout::mouse_to_virtual_events_t& freeMousePreset() -{ - return fpsMousePreset(); -} - -inline const IGimbalBindingLayout::mouse_to_virtual_events_t& orbitMousePreset() -{ - static const auto preset = appendSymmetricScrollPreset( - makeRelativeMousePreset( - core::CVirtualGimbalEvent::MoveRight, - core::CVirtualGimbalEvent::MoveLeft, - core::CVirtualGimbalEvent::MoveUp, - core::CVirtualGimbalEvent::MoveDown), - core::CVirtualGimbalEvent::MoveForward, - core::CVirtualGimbalEvent::MoveBackward); - return preset; -} - -inline const IGimbalBindingLayout::mouse_to_virtual_events_t& arcballMousePreset() -{ - static const auto preset = appendSymmetricScrollPreset( - makeRelativeMousePreset( - core::CVirtualGimbalEvent::PanRight, - core::CVirtualGimbalEvent::PanLeft, - core::CVirtualGimbalEvent::TiltUp, - core::CVirtualGimbalEvent::TiltDown), - core::CVirtualGimbalEvent::MoveForward, - core::CVirtualGimbalEvent::MoveBackward); - return preset; -} - -inline const IGimbalBindingLayout::mouse_to_virtual_events_t& turntableMousePreset() -{ - return arcballMousePreset(); -} - -inline const IGimbalBindingLayout::mouse_to_virtual_events_t& topDownMousePreset() -{ - static const auto preset = appendSymmetricScrollPreset( - makeRelativeMousePreset( - core::CVirtualGimbalEvent::PanRight, - core::CVirtualGimbalEvent::PanLeft, - core::CVirtualGimbalEvent::MoveUp, - core::CVirtualGimbalEvent::MoveDown), - core::CVirtualGimbalEvent::MoveForward, - core::CVirtualGimbalEvent::MoveBackward); - return preset; -} - -inline const IGimbalBindingLayout::mouse_to_virtual_events_t& isometricMousePreset() -{ - static const auto preset = appendSymmetricScrollPreset( - makeRelativeMousePreset( - core::CVirtualGimbalEvent::MoveRight, - core::CVirtualGimbalEvent::MoveLeft, - core::CVirtualGimbalEvent::MoveUp, - core::CVirtualGimbalEvent::MoveDown), - core::CVirtualGimbalEvent::MoveForward, - core::CVirtualGimbalEvent::MoveBackward); - return preset; -} - -inline const IGimbalBindingLayout::mouse_to_virtual_events_t& chaseMousePreset() -{ - static const auto preset = appendSymmetricScrollPreset( - makeRelativeMousePreset( - core::CVirtualGimbalEvent::PanRight, - core::CVirtualGimbalEvent::PanLeft, - core::CVirtualGimbalEvent::TiltUp, - core::CVirtualGimbalEvent::TiltDown), - core::CVirtualGimbalEvent::MoveUp, - core::CVirtualGimbalEvent::MoveDown); - return preset; -} - -inline const IGimbalBindingLayout::mouse_to_virtual_events_t& dollyMousePreset() -{ - static const auto preset = appendSymmetricScrollPreset( - makeRelativeMousePreset( - core::CVirtualGimbalEvent::PanRight, - core::CVirtualGimbalEvent::PanLeft, - core::CVirtualGimbalEvent::TiltUp, - core::CVirtualGimbalEvent::TiltDown), - core::CVirtualGimbalEvent::MoveForward, - core::CVirtualGimbalEvent::MoveBackward); - return preset; -} - -inline const IGimbalBindingLayout::mouse_to_virtual_events_t& dollyZoomMousePreset() -{ - return isometricMousePreset(); + static const auto preset = buildKeyboardPreset({ + .wasd = { + core::CVirtualGimbalEvent::MoveForward, + core::CVirtualGimbalEvent::MoveBackward, + core::CVirtualGimbalEvent::PanLeft, + core::CVirtualGimbalEvent::PanRight + }, + .ijkl = { + core::CVirtualGimbalEvent::TiltDown, + core::CVirtualGimbalEvent::TiltUp, + core::CVirtualGimbalEvent::PanLeft, + core::CVirtualGimbalEvent::PanRight + } + }); + return preset; + } + case core::ICamera::CameraKind::TopDown: + { + static const auto preset = buildKeyboardPreset({ + .wasd = { + core::CVirtualGimbalEvent::MoveUp, + core::CVirtualGimbalEvent::MoveDown, + core::CVirtualGimbalEvent::MoveLeft, + core::CVirtualGimbalEvent::MoveRight + }, + .qe = { + core::CVirtualGimbalEvent::MoveBackward, + core::CVirtualGimbalEvent::MoveForward + }, + .ijkl = { + core::CVirtualGimbalEvent::None, + core::CVirtualGimbalEvent::None, + core::CVirtualGimbalEvent::PanLeft, + core::CVirtualGimbalEvent::PanRight + } + }); + return preset; + } + case core::ICamera::CameraKind::Isometric: + case core::ICamera::CameraKind::DollyZoom: + { + static const auto preset = buildKeyboardPreset({ + .wasd = { + core::CVirtualGimbalEvent::MoveUp, + core::CVirtualGimbalEvent::MoveDown, + core::CVirtualGimbalEvent::MoveLeft, + core::CVirtualGimbalEvent::MoveRight + }, + .qe = { + core::CVirtualGimbalEvent::MoveBackward, + core::CVirtualGimbalEvent::MoveForward + } + }); + return preset; + } + case core::ICamera::CameraKind::Path: + { + static const auto preset = buildKeyboardPreset({ + .wasd = { + core::CVirtualGimbalEvent::MoveForward, + core::CVirtualGimbalEvent::MoveBackward, + core::CVirtualGimbalEvent::MoveLeft, + core::CVirtualGimbalEvent::MoveRight + }, + .qe = { + core::CVirtualGimbalEvent::MoveDown, + core::CVirtualGimbalEvent::MoveUp + } + }); + return preset; + } + default: + return emptyKeyboardPreset(); + } } -inline const IGimbalBindingLayout::mouse_to_virtual_events_t& pathMousePreset() +inline const IGimbalBindingLayout::mouse_to_virtual_events_t& mousePresetForKind(const core::ICamera::CameraKind kind) { - return isometricMousePreset(); + switch (kind) + { + case core::ICamera::CameraKind::FPS: + case core::ICamera::CameraKind::Free: + { + static const auto preset = buildMousePreset({ + .relative = { + core::CVirtualGimbalEvent::PanRight, + core::CVirtualGimbalEvent::PanLeft, + core::CVirtualGimbalEvent::TiltUp, + core::CVirtualGimbalEvent::TiltDown + } + }); + return preset; + } + case core::ICamera::CameraKind::Orbit: + case core::ICamera::CameraKind::Isometric: + case core::ICamera::CameraKind::DollyZoom: + case core::ICamera::CameraKind::Path: + { + static const auto preset = buildMousePreset({ + .relative = { + core::CVirtualGimbalEvent::MoveRight, + core::CVirtualGimbalEvent::MoveLeft, + core::CVirtualGimbalEvent::MoveUp, + core::CVirtualGimbalEvent::MoveDown + }, + .scroll = { + core::CVirtualGimbalEvent::MoveForward, + core::CVirtualGimbalEvent::MoveBackward + } + }); + return preset; + } + case core::ICamera::CameraKind::Arcball: + case core::ICamera::CameraKind::Turntable: + case core::ICamera::CameraKind::Dolly: + { + static const auto preset = buildMousePreset({ + .relative = { + core::CVirtualGimbalEvent::PanRight, + core::CVirtualGimbalEvent::PanLeft, + core::CVirtualGimbalEvent::TiltUp, + core::CVirtualGimbalEvent::TiltDown + }, + .scroll = { + core::CVirtualGimbalEvent::MoveForward, + core::CVirtualGimbalEvent::MoveBackward + } + }); + return preset; + } + case core::ICamera::CameraKind::TopDown: + { + static const auto preset = buildMousePreset({ + .relative = { + core::CVirtualGimbalEvent::PanRight, + core::CVirtualGimbalEvent::PanLeft, + core::CVirtualGimbalEvent::MoveUp, + core::CVirtualGimbalEvent::MoveDown + }, + .scroll = { + core::CVirtualGimbalEvent::MoveForward, + core::CVirtualGimbalEvent::MoveBackward + } + }); + return preset; + } + case core::ICamera::CameraKind::Chase: + { + static const auto preset = buildMousePreset({ + .relative = { + core::CVirtualGimbalEvent::PanRight, + core::CVirtualGimbalEvent::PanLeft, + core::CVirtualGimbalEvent::TiltUp, + core::CVirtualGimbalEvent::TiltDown + }, + .scroll = { + core::CVirtualGimbalEvent::MoveUp, + core::CVirtualGimbalEvent::MoveDown + } + }); + return preset; + } + default: + return emptyMousePreset(); + } } } // namespace impl inline const IGimbalBindingLayout::keyboard_to_virtual_events_t& getDefaultCameraKeyboardMappingPreset(const core::ICamera& camera) { - switch (camera.getKind()) - { - case core::ICamera::CameraKind::FPS: return impl::fpsKeyboardPreset(); - case core::ICamera::CameraKind::Free: return impl::freeKeyboardPreset(); - case core::ICamera::CameraKind::Orbit: return impl::orbitKeyboardPreset(); - case core::ICamera::CameraKind::Arcball: return impl::arcballKeyboardPreset(); - case core::ICamera::CameraKind::Turntable: return impl::turntableKeyboardPreset(); - case core::ICamera::CameraKind::TopDown: return impl::topDownKeyboardPreset(); - case core::ICamera::CameraKind::Isometric: return impl::isometricKeyboardPreset(); - case core::ICamera::CameraKind::Chase: return impl::chaseKeyboardPreset(); - case core::ICamera::CameraKind::Dolly: return impl::dollyKeyboardPreset(); - case core::ICamera::CameraKind::DollyZoom: return impl::dollyZoomKeyboardPreset(); - case core::ICamera::CameraKind::Path: return impl::pathKeyboardPreset(); - default: return impl::emptyKeyboardPreset(); - } + return impl::keyboardPresetForKind(camera.getKind()); } inline const IGimbalBindingLayout::mouse_to_virtual_events_t& getDefaultCameraMouseMappingPreset(const core::ICamera& camera) { - switch (camera.getKind()) - { - case core::ICamera::CameraKind::FPS: return impl::fpsMousePreset(); - case core::ICamera::CameraKind::Free: return impl::freeMousePreset(); - case core::ICamera::CameraKind::Orbit: return impl::orbitMousePreset(); - case core::ICamera::CameraKind::Arcball: return impl::arcballMousePreset(); - case core::ICamera::CameraKind::Turntable: return impl::turntableMousePreset(); - case core::ICamera::CameraKind::TopDown: return impl::topDownMousePreset(); - case core::ICamera::CameraKind::Isometric: return impl::isometricMousePreset(); - case core::ICamera::CameraKind::Chase: return impl::chaseMousePreset(); - case core::ICamera::CameraKind::Dolly: return impl::dollyMousePreset(); - case core::ICamera::CameraKind::DollyZoom: return impl::dollyZoomMousePreset(); - case core::ICamera::CameraKind::Path: return impl::pathMousePreset(); - default: return impl::emptyMousePreset(); - } + return impl::mousePresetForKind(camera.getKind()); } inline IGimbalBindingLayout::imguizmo_to_virtual_events_t buildDefaultCameraImguizmoMappingPreset(const core::ICamera& camera) diff --git a/common/include/camera/CCameraMathUtilities.hpp b/common/include/camera/CCameraMathUtilities.hpp index c2a2aa86e..3d4051718 100644 --- a/common/include/camera/CCameraMathUtilities.hpp +++ b/common/include/camera/CCameraMathUtilities.hpp @@ -58,6 +58,22 @@ using camera_matrix_t = matrix; template using camera_quaternion_t = math::quaternion; +template +inline camera_quaternion_t makeIdentityQuaternion(); + +template +struct SRigidTransformComponents +{ + camera_vector_t translation = camera_vector_t(T(0)); + camera_quaternion_t orientation = camera_quaternion_t::create(); + camera_vector_t scale = camera_vector_t(T(1)); +}; + +template +inline bool tryExtractRigidTransformComponents( + const camera_matrix_t& transform, + SRigidTransformComponents& outComponents); + template inline camera_quaternion_t makeIdentityQuaternion() { @@ -408,29 +424,13 @@ inline bool tryExtractRigidPoseFromTransform( camera_vector_t& outTranslation, camera_quaternion_t& outOrientation) { - outTranslation = camera_vector_t(transform[3].x, transform[3].y, transform[3].z); - - auto right = camera_vector_t(transform[0].x, transform[0].y, transform[0].z); - auto up = camera_vector_t(transform[1].x, transform[1].y, transform[1].z); - auto forward = camera_vector_t(transform[2].x, transform[2].y, transform[2].z); - - const T rightLen = length(right); - const T upLen = length(up); - const T forwardLen = length(forward); - constexpr T Epsilon = std::numeric_limits::epsilon(); - if (!isFiniteScalar(rightLen) || !isFiniteScalar(upLen) || !isFiniteScalar(forwardLen)) - return false; - if (rightLen <= Epsilon || upLen <= Epsilon || forwardLen <= Epsilon) - return false; - - right /= rightLen; - up /= upLen; - forward /= forwardLen; - if (!isOrthoBase(right, up, forward)) + SRigidTransformComponents components; + if (!tryExtractRigidTransformComponents(transform, components)) return false; - outOrientation = makeQuaternionFromBasis(right, up, forward); - return isFiniteQuaternion(outOrientation); + outTranslation = components.translation; + outOrientation = components.orientation; + return true; } template @@ -654,6 +654,12 @@ inline camera_vector_t getWrappedEulerDistanceDegrees( getWrappedAngleDistanceDegrees(a.z, b.z)); } +template +inline T getMaxVectorComponent(const camera_vector_t& value) +{ + return std::max(value.x, std::max(value.y, value.z)); +} + template inline camera_matrix_t composeTransformMatrix( const camera_vector_t& translation, @@ -670,36 +676,64 @@ inline camera_matrix_t composeTransformMatrix( } template -inline bool decomposeTransformMatrix( +inline bool tryExtractRigidTransformComponents( const camera_matrix_t& transform, - camera_vector_t& outTranslation, - camera_vector_t& outRotationEulerDegrees, - camera_vector_t& outScale) + SRigidTransformComponents& outComponents) { - outTranslation = camera_vector_t(transform[3].x, transform[3].y, transform[3].z); + outComponents.translation = camera_vector_t(transform[3].x, transform[3].y, transform[3].z); auto right = camera_vector_t(transform[0].x, transform[0].y, transform[0].z); auto up = camera_vector_t(transform[1].x, transform[1].y, transform[1].z); auto forward = camera_vector_t(transform[2].x, transform[2].y, transform[2].z); - outScale = camera_vector_t(length(right), length(up), length(forward)); + outComponents.scale = camera_vector_t(length(right), length(up), length(forward)); - if (!std::isfinite(outScale.x) || !std::isfinite(outScale.y) || !std::isfinite(outScale.z)) + if (!isFiniteVec3(outComponents.translation) || !isFiniteVec3(outComponents.scale)) return false; constexpr T Epsilon = std::numeric_limits::epsilon(); - if (outScale.x <= Epsilon || outScale.y <= Epsilon || outScale.z <= Epsilon) + if (outComponents.scale.x <= Epsilon || outComponents.scale.y <= Epsilon || outComponents.scale.z <= Epsilon) + return false; + + right /= outComponents.scale.x; + up /= outComponents.scale.y; + forward /= outComponents.scale.z; + if (!isOrthoBase(right, up, forward)) + return false; + + outComponents.orientation = makeQuaternionFromBasis(right, up, forward); + return isFiniteQuaternion(outComponents.orientation); +} + +template +inline bool tryBuildRigidFrameFromTransform( + const camera_matrix_t& transform, + camera_matrix_t& outFrame, + camera_quaternion_t& outOrientation) +{ + SRigidTransformComponents components; + if (!tryExtractRigidTransformComponents(transform, components)) return false; - right /= outScale.x; - up /= outScale.y; - forward /= outScale.z; + outOrientation = components.orientation; + outFrame = composeTransformMatrix(components.translation, components.orientation); + return true; +} - const auto orientation = makeQuaternionFromBasis(right, up, forward); - if (!isFiniteQuaternion(orientation)) +template +inline bool decomposeTransformMatrix( + const camera_matrix_t& transform, + camera_vector_t& outTranslation, + camera_vector_t& outRotationEulerDegrees, + camera_vector_t& outScale) +{ + SRigidTransformComponents components; + if (!tryExtractRigidTransformComponents(transform, components)) return false; - outRotationEulerDegrees = getQuaternionEulerDegrees(orientation); + outTranslation = components.translation; + outScale = components.scale; + outRotationEulerDegrees = getQuaternionEulerDegrees(components.orientation); return std::isfinite(outRotationEulerDegrees.x) && std::isfinite(outRotationEulerDegrees.y) && std::isfinite(outRotationEulerDegrees.z); diff --git a/common/include/camera/CCameraScriptedCheckRunner.hpp b/common/include/camera/CCameraScriptedCheckRunner.hpp index 848a9116f..89248b008 100644 --- a/common/include/camera/CCameraScriptedCheckRunner.hpp +++ b/common/include/camera/CCameraScriptedCheckRunner.hpp @@ -30,13 +30,16 @@ namespace nbl::system */ struct CCameraScriptedCheckRuntimeState { + struct SPoseReference + { + bool valid = false; + hlsl::float64_t3 position = hlsl::float64_t3(0.0); + hlsl::camera_quaternion_t orientation = hlsl::makeIdentityQuaternion(); + }; + size_t nextCheckIndex = 0u; - bool baselineValid = false; - hlsl::float32_t3 baselinePos = hlsl::float32_t3(0.f); - hlsl::float32_t3 baselineEulerDeg = hlsl::float32_t3(0.f); - bool stepValid = false; - hlsl::float32_t3 stepPos = hlsl::float32_t3(0.f); - hlsl::float32_t3 stepEulerDeg = hlsl::float32_t3(0.f); + SPoseReference baseline = {}; + SPoseReference step = {}; }; //! Shared per-frame evaluation context for authored scripted checks. @@ -68,12 +71,32 @@ struct CCameraScriptedCheckFrameResult inline void scriptedCheckSetStepReference( CCameraScriptedCheckRuntimeState& state, - const hlsl::float32_t3& pos, - const hlsl::float32_t3& eulerDeg) + const hlsl::float64_t3& position, + const hlsl::camera_quaternion_t& orientation) +{ + state.step.valid = true; + state.step.position = position; + state.step.orientation = hlsl::normalizeQuaternion(orientation); +} + +inline void scriptedCheckSetBaselineReference( + CCameraScriptedCheckRuntimeState& state, + const hlsl::float64_t3& position, + const hlsl::camera_quaternion_t& orientation) +{ + state.baseline.valid = true; + state.baseline.position = position; + state.baseline.orientation = hlsl::normalizeQuaternion(orientation); + scriptedCheckSetStepReference(state, position, orientation); +} + +inline float scriptedCheckComputeRotationDeltaDegrees( + const hlsl::camera_quaternion_t& currentOrientation, + const hlsl::camera_quaternion_t& referenceOrientation) { - state.stepValid = true; - state.stepPos = pos; - state.stepEulerDeg = eulerDeg; + return static_cast(hlsl::getQuaternionAngularDistanceDegrees( + hlsl::normalizeQuaternion(currentOrientation), + hlsl::normalizeQuaternion(referenceOrientation))); } template @@ -123,9 +146,10 @@ inline CCameraScriptedCheckFrameResult evaluateScriptedChecksForFrame( const auto& gimbal = context.camera->getGimbal(); const auto pos = gimbal.getPosition(); - const auto eulerDeg = hlsl::getCastedVector(hlsl::getQuaternionEulerDegrees(gimbal.getOrientation())); + const auto orientation = hlsl::normalizeQuaternion(gimbal.getOrientation()); + const auto eulerDeg = hlsl::getCastedVector(hlsl::getQuaternionEulerDegrees(orientation)); - if (!hlsl::isFiniteVec3(pos) || !hlsl::isFiniteVec3(eulerDeg)) + if (!hlsl::isFiniteVec3(pos) || !hlsl::isFiniteQuaternion(orientation) || !hlsl::isFiniteVec3(eulerDeg)) { appendScriptedCheckLog( result, @@ -142,10 +166,7 @@ inline CCameraScriptedCheckFrameResult evaluateScriptedChecksForFrame( { case CCameraScriptedInputCheck::Kind::Baseline: { - state.baselineValid = true; - state.baselinePos = pos; - state.baselineEulerDeg = eulerDeg; - scriptedCheckSetStepReference(state, pos, eulerDeg); + scriptedCheckSetBaselineReference(state, pos, orientation); appendScriptedCheckLog( result, false, @@ -237,12 +258,10 @@ inline CCameraScriptedCheckFrameResult evaluateScriptedChecksForFrame( } if (check.hasExpectedEuler) { - const auto deltaEulerDeg = hlsl::getWrappedEulerDistanceDegrees(eulerDeg, check.expectedEulerDeg); - const auto dx = deltaEulerDeg.x; - const auto dy = deltaEulerDeg.y; - const auto dz = deltaEulerDeg.z; - const auto maxAngle = std::max(dx, std::max(dy, dz)); - if (maxAngle > check.eulerToleranceDeg) + const auto expectedOrientation = hlsl::makeQuaternionFromEulerDegrees( + hlsl::getCastedVector(check.expectedEulerDeg)); + const auto rotationDeltaDeg = scriptedCheckComputeRotationDeltaDegrees(orientation, expectedOrientation); + if (rotationDeltaDeg > check.eulerToleranceDeg) { ok = false; appendScriptedCheckLog( @@ -252,7 +271,7 @@ inline CCameraScriptedCheckFrameResult evaluateScriptedChecksForFrame( { oss << std::fixed << std::setprecision(6); oss << "[script][fail] gimbal_near frame=" << context.frame - << " euler_diff=" << maxAngle + << " rot_delta_deg=" << rotationDeltaDeg << " tol=" << check.eulerToleranceDeg; })); } @@ -272,7 +291,7 @@ inline CCameraScriptedCheckFrameResult evaluateScriptedChecksForFrame( } case CCameraScriptedInputCheck::Kind::GimbalDelta: { - if (!state.baselineValid) + if (!state.baseline.valid) { appendScriptedCheckLog( result, @@ -284,15 +303,11 @@ inline CCameraScriptedCheckFrameResult evaluateScriptedChecksForFrame( break; } - const hlsl::float64_t3 diff = pos - hlsl::getCastedVector(state.baselinePos); + const hlsl::float64_t3 diff = pos - state.baseline.position; const double dpos = hlsl::length(diff); - const auto deltaEulerDeg = hlsl::getWrappedEulerDistanceDegrees(eulerDeg, state.baselineEulerDeg); - const auto dx = deltaEulerDeg.x; - const auto dy = deltaEulerDeg.y; - const auto dz = deltaEulerDeg.z; - const auto dmax = std::max(dx, std::max(dy, dz)); + const auto rotationDeltaDeg = scriptedCheckComputeRotationDeltaDegrees(orientation, state.baseline.orientation); - if (dpos > check.posTolerance || dmax > check.eulerToleranceDeg) + if (dpos > check.posTolerance || rotationDeltaDeg > check.eulerToleranceDeg) { appendScriptedCheckLog( result, @@ -303,7 +318,7 @@ inline CCameraScriptedCheckFrameResult evaluateScriptedChecksForFrame( oss << "[script][fail] gimbal_delta frame=" << context.frame << " pos_diff=" << dpos << " tol=" << check.posTolerance - << " euler_diff=" << dmax + << " rot_delta_deg=" << rotationDeltaDeg << " tol=" << check.eulerToleranceDeg; })); } @@ -317,18 +332,18 @@ inline CCameraScriptedCheckFrameResult evaluateScriptedChecksForFrame( oss << std::fixed << std::setprecision(6); oss << "[script][pass] gimbal_delta frame=" << context.frame << " pos_diff=" << dpos - << " euler_diff=" << dmax; + << " rot_delta_deg=" << rotationDeltaDeg; })); } break; } case CCameraScriptedInputCheck::Kind::GimbalStep: { - if (!state.stepValid) + if (!state.step.valid) { - if (state.baselineValid) + if (state.baseline.valid) { - scriptedCheckSetStepReference(state, state.baselinePos, state.baselineEulerDeg); + scriptedCheckSetStepReference(state, state.baseline.position, state.baseline.orientation); } else { @@ -339,19 +354,15 @@ inline CCameraScriptedCheckFrameResult evaluateScriptedChecksForFrame( { oss << "[script][fail] gimbal_step frame=" << context.frame << " missing step reference"; })); - scriptedCheckSetStepReference(state, pos, eulerDeg); + scriptedCheckSetStepReference(state, pos, orientation); ++state.nextCheckIndex; continue; } } - const hlsl::float64_t3 diff = pos - hlsl::getCastedVector(state.stepPos); + const hlsl::float64_t3 diff = pos - state.step.position; const double dpos = hlsl::length(diff); - const auto deltaEulerDeg = hlsl::getWrappedEulerDistanceDegrees(eulerDeg, state.stepEulerDeg); - const auto dx = deltaEulerDeg.x; - const auto dy = deltaEulerDeg.y; - const auto dz = deltaEulerDeg.z; - const auto dmax = std::max(dx, std::max(dy, dz)); + const auto rotationDeltaDeg = scriptedCheckComputeRotationDeltaDegrees(orientation, state.step.orientation); bool ok = true; bool requiresProgress = false; @@ -380,7 +391,7 @@ inline CCameraScriptedCheckFrameResult evaluateScriptedChecksForFrame( } if (check.hasEulerDeltaConstraint) { - if (dmax > check.eulerToleranceDeg) + if (rotationDeltaDeg > check.eulerToleranceDeg) { ok = false; appendScriptedCheckLog( @@ -390,14 +401,14 @@ inline CCameraScriptedCheckFrameResult evaluateScriptedChecksForFrame( { oss << std::fixed << std::setprecision(6); oss << "[script][fail] gimbal_step frame=" << context.frame - << " euler_delta=" << dmax + << " rot_delta_deg=" << rotationDeltaDeg << " max=" << check.eulerToleranceDeg; })); } if (check.minEulerDeltaDeg > 0.0f) { requiresProgress = true; - hasProgress = hasProgress || dmax >= check.minEulerDeltaDeg; + hasProgress = hasProgress || rotationDeltaDeg >= check.minEulerDeltaDeg; } } if (requiresProgress && !hasProgress) @@ -411,7 +422,7 @@ inline CCameraScriptedCheckFrameResult evaluateScriptedChecksForFrame( oss << std::fixed << std::setprecision(6); oss << "[script][fail] gimbal_step frame=" << context.frame << " missing progress pos_delta=" << dpos - << " euler_delta=" << dmax; + << " rot_delta_deg=" << rotationDeltaDeg; })); } @@ -425,10 +436,10 @@ inline CCameraScriptedCheckFrameResult evaluateScriptedChecksForFrame( oss << std::fixed << std::setprecision(6); oss << "[script][pass] gimbal_step frame=" << context.frame << " pos_delta=" << dpos - << " euler_delta=" << dmax; + << " rot_delta_deg=" << rotationDeltaDeg; })); } - scriptedCheckSetStepReference(state, pos, eulerDeg); + scriptedCheckSetStepReference(state, pos, orientation); break; } case CCameraScriptedInputCheck::Kind::FollowTargetLock: diff --git a/common/include/camera/IGimbal.hpp b/common/include/camera/IGimbal.hpp index 632e0fa06..bc764c313 100644 --- a/common/include/camera/IGimbal.hpp +++ b/common/include/camera/IGimbal.hpp @@ -293,22 +293,13 @@ namespace nbl::core if (referenceFrame) { - out->frame = *referenceFrame; - if (not hlsl::isOrthoBase(hlsl::float64_t3(out->frame[0]), hlsl::float64_t3(out->frame[1]), hlsl::float64_t3(out->frame[2]))) + if (!hlsl::tryBuildRigidFrameFromTransform(*referenceFrame, out->frame, out->orientation)) return false; - out->orientation = hlsl::makeQuaternionFromBasis( - hlsl::float64_t3(out->frame[0]), - hlsl::float64_t3(out->frame[1]), - hlsl::float64_t3(out->frame[2])); } else { - out->frame = hlsl::getMatrix3x3As4x4(getOrthonornalMatrix()); - out->frame[3] = hlsl::float64_t4(getPosition(), 1); - out->orientation = hlsl::makeQuaternionFromBasis( - hlsl::float64_t3(out->frame[0]), - hlsl::float64_t3(out->frame[1]), - hlsl::float64_t3(out->frame[2])); + out->orientation = getOrientation(); + out->frame = hlsl::composeTransformMatrix(getPosition(), out->orientation); } return true; diff --git a/common/include/camera/IGimbalInputProcessor.hpp b/common/include/camera/IGimbalInputProcessor.hpp index a1ce8912e..76e61b7c3 100644 --- a/common/include/camera/IGimbalInputProcessor.hpp +++ b/common/include/camera/IGimbalInputProcessor.hpp @@ -281,34 +281,25 @@ class IGimbalInputProcessor : public CGimbalBindingLayoutStorage { const auto& deltaWorldTRS = ev; - struct - { - hlsl::float32_t3 dTranslation, dRotation, dScale; - } world; - - if (!hlsl::decomposeTransformMatrix(deltaWorldTRS, world.dTranslation, world.dRotation, world.dScale)) + hlsl::SRigidTransformComponents world = {}; + if (!hlsl::tryExtractRigidTransformComponents(deltaWorldTRS, world)) continue; // Delta translation impulse - requestMagnitudeUpdateWithScalar(0.f, world.dTranslation[0], std::abs(world.dTranslation[0]), gimbal_event_t::MoveRight, gimbal_event_t::MoveLeft, m_imguizmoVirtualEventMap); - requestMagnitudeUpdateWithScalar(0.f, world.dTranslation[1], std::abs(world.dTranslation[1]), gimbal_event_t::MoveUp, gimbal_event_t::MoveDown, m_imguizmoVirtualEventMap); - requestMagnitudeUpdateWithScalar(0.f, world.dTranslation[2], std::abs(world.dTranslation[2]), gimbal_event_t::MoveForward, gimbal_event_t::MoveBackward, m_imguizmoVirtualEventMap); + requestMagnitudeUpdateWithScalar(0.f, world.translation[0], std::abs(world.translation[0]), gimbal_event_t::MoveRight, gimbal_event_t::MoveLeft, m_imguizmoVirtualEventMap); + requestMagnitudeUpdateWithScalar(0.f, world.translation[1], std::abs(world.translation[1]), gimbal_event_t::MoveUp, gimbal_event_t::MoveDown, m_imguizmoVirtualEventMap); + requestMagnitudeUpdateWithScalar(0.f, world.translation[2], std::abs(world.translation[2]), gimbal_event_t::MoveForward, gimbal_event_t::MoveBackward, m_imguizmoVirtualEventMap); // Delta rotation impulse - const hlsl::float32_t3 dRotationRad = - { - hlsl::radians(world.dRotation[0]), - hlsl::radians(world.dRotation[1]), - hlsl::radians(world.dRotation[2]) - }; + const auto dRotationRad = hlsl::getQuaternionEulerRadians(world.orientation); requestMagnitudeUpdateWithScalar(0.f, dRotationRad[0], std::abs(dRotationRad[0]), gimbal_event_t::TiltUp , gimbal_event_t::TiltDown, m_imguizmoVirtualEventMap); requestMagnitudeUpdateWithScalar(0.f, dRotationRad[1], std::abs(dRotationRad[1]), gimbal_event_t::PanRight, gimbal_event_t::PanLeft, m_imguizmoVirtualEventMap); requestMagnitudeUpdateWithScalar(0.f, dRotationRad[2], std::abs(dRotationRad[2]), gimbal_event_t::RollRight, gimbal_event_t::RollLeft, m_imguizmoVirtualEventMap); // Delta scale impulse - requestMagnitudeUpdateWithScalar(1.f, world.dScale[0], std::abs(world.dScale[0]), gimbal_event_t::ScaleXInc, gimbal_event_t::ScaleXDec, m_imguizmoVirtualEventMap); - requestMagnitudeUpdateWithScalar(1.f, world.dScale[1], std::abs(world.dScale[1]), gimbal_event_t::ScaleYInc, gimbal_event_t::ScaleYDec, m_imguizmoVirtualEventMap); - requestMagnitudeUpdateWithScalar(1.f, world.dScale[2], std::abs(world.dScale[2]), gimbal_event_t::ScaleZInc, gimbal_event_t::ScaleZDec, m_imguizmoVirtualEventMap); + requestMagnitudeUpdateWithScalar(1.f, world.scale[0], std::abs(world.scale[0]), gimbal_event_t::ScaleXInc, gimbal_event_t::ScaleXDec, m_imguizmoVirtualEventMap); + requestMagnitudeUpdateWithScalar(1.f, world.scale[1], std::abs(world.scale[1]), gimbal_event_t::ScaleYInc, gimbal_event_t::ScaleYDec, m_imguizmoVirtualEventMap); + requestMagnitudeUpdateWithScalar(1.f, world.scale[2], std::abs(world.scale[2]), gimbal_event_t::ScaleZInc, gimbal_event_t::ScaleZDec, m_imguizmoVirtualEventMap); } postprocess(m_imguizmoVirtualEventMap, output, count); From c1014d05c634451b65a489940b54a15f75db0eb3 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Thu, 9 Apr 2026 08:43:14 +0200 Subject: [PATCH 181/205] Save camera cleanup work in progress --- 61_UI/AppCameraConfigJsonParsing.cpp | 683 ++++ 61_UI/AppCameraConfigUtilities.cpp | 65 + 61_UI/AppCameraConfiguration.cpp | 111 + 61_UI/AppCameraPlanarRuntime.cpp | 175 + 61_UI/AppCameraUiState.cpp | 84 + 61_UI/AppControlPanel.cpp | 1119 +------ 61_UI/AppControlPanelCameraTab.cpp | 157 + 61_UI/AppControlPanelPlaybackTab.cpp | 188 ++ 61_UI/AppControlPanelPresetsTab.cpp | 151 + 61_UI/AppControlPanelProjectionTab.cpp | 85 + 61_UI/AppControlPanelStatusTab.cpp | 113 + 61_UI/AppControlPanelTabs.cpp | 203 ++ 61_UI/AppControlPanelUtilityTabs.cpp | 52 + 61_UI/AppDebugSceneRendererResources.cpp | 69 + 61_UI/AppFollowRuntime.cpp | 290 ++ 61_UI/AppFrameCapture.cpp | 75 + 61_UI/AppFrameRuntime.cpp | 126 + 61_UI/AppHeadlessCameraSmoke.cpp | 118 + 61_UI/AppHeadlessCameraSmokeChecks.inl | 956 ++++++ 61_UI/AppHeadlessCameraSmokeHelpers.inl | 1443 +++++++++ 61_UI/AppImGuiListen.cpp | 281 +- 61_UI/AppInit.cpp | 2838 +---------------- 61_UI/AppInputRuntime.cpp | 236 ++ 61_UI/AppManipulableObjects.cpp | 230 ++ 61_UI/AppPresentationResources.cpp | 162 + 61_UI/AppPresetPlayback.cpp | 226 ++ 61_UI/AppResourceBootstrap.cpp | 11 + 61_UI/AppResourceUtilities.cpp | 117 + 61_UI/AppSceneDebugInstances.cpp | 102 + 61_UI/AppSceneFramebufferResources.cpp | 147 + 61_UI/AppSceneRenderPasses.cpp | 102 + 61_UI/AppSceneResources.cpp | 17 + 61_UI/AppScriptedInitialization.cpp | 236 ++ 61_UI/AppScriptedInputRuntime.cpp | 312 ++ 61_UI/AppScriptedValidation.cpp | 77 + 61_UI/AppSpaceEnvironmentResources.cpp | 254 ++ 61_UI/AppTextResourceUtilities.cpp | 81 + 61_UI/AppTransformEditor.cpp | 262 +- 61_UI/AppUiInputCapture.cpp | 93 + 61_UI/AppUiResources.cpp | 160 + 61_UI/AppUiRuntime.cpp | 243 ++ 61_UI/AppUpdate.cpp | 745 +---- 61_UI/AppViewportGizmos.cpp | 104 + 61_UI/AppViewportWindows.cpp | 152 + 61_UI/AppWorkLoop.cpp | 431 +-- 61_UI/CMakeLists.txt | 37 + 61_UI/README.md | 4 +- 61_UI/app_resources/cameras.json | 2 +- 61_UI/app_resources/cameraz_continuity.json | 2 +- 61_UI/include/app/App.hpp | 1540 ++------- .../include/app/AppCameraConfigUtilities.hpp | 209 +- .../app/AppControlPanelAuthoringUtilities.hpp | 86 + 61_UI/include/app/AppGizmoUtilities.hpp | 57 + .../AppProjectionControlPanelUiUtilities.hpp | 303 ++ 61_UI/include/app/AppRenderPassUtilities.hpp | 38 + .../include/app/AppResourcePathUtilities.hpp | 218 ++ 61_UI/include/app/AppResourceUtilities.hpp | 285 +- 61_UI/include/app/AppSwapchainResources.hpp | 123 + 61_UI/include/app/AppTypes.hpp | 647 +++- .../app/AppViewportBindingUtilities.hpp | 219 +- .../app/AppViewportWindowUtilities.hpp | 102 + 61_UI/include/common.hpp | 2 + 61_UI/src/keysmapping.cpp | 45 +- common/include/camera/CArcballCamera.hpp | 17 +- .../camera/CCameraControlPanelUiUtilities.hpp | 136 + .../include/camera/CCameraFileUtilities.hpp | 82 + .../CCameraFollowRegressionUtilities.hpp | 111 +- .../include/camera/CCameraFollowUtilities.hpp | 158 +- common/include/camera/CCameraGoal.hpp | 213 +- common/include/camera/CCameraGoalSolver.hpp | 288 +- .../camera/CCameraInputBindingUtilities.hpp | 747 +++-- .../include/camera/CCameraKeyframeTrack.hpp | 6 +- .../CCameraKeyframeTrackPersistence.hpp | 6 +- .../include/camera/CCameraKindUtilities.hpp | 139 + .../camera/CCameraManipulationUtilities.hpp | 85 +- .../include/camera/CCameraMathUtilities.hpp | 352 +- .../include/camera/CCameraPathUtilities.hpp | 381 +++ common/include/camera/CCameraPersistence.hpp | 10 +- .../camera/CCameraPresetPersistence.hpp | 10 +- ...ameraScriptVisualDebugOverlayUtilities.hpp | 77 +- .../camera/CCameraScriptedCheckRunner.hpp | 110 +- .../include/camera/CCameraScriptedRuntime.hpp | 18 +- .../CCameraScriptedRuntimePersistence.hpp | 7 +- .../CCameraScriptedUiInputUtilities.hpp | 87 + .../include/camera/CCameraSequenceScript.hpp | 86 +- .../CCameraSequenceScriptPersistence.hpp | 7 +- .../CCameraSmokeRegressionUtilities.hpp | 14 +- .../camera/CCameraTargetRelativeUtilities.hpp | 268 ++ .../include/camera/CCameraTextUtilities.hpp | 34 +- .../CCameraViewportOverlayUtilities.hpp | 12 +- .../camera/CCameraVirtualEventUtilities.hpp | 168 + common/include/camera/CChaseCamera.hpp | 18 +- common/include/camera/CDollyCamera.hpp | 15 +- common/include/camera/CDollyZoomCamera.hpp | 9 +- common/include/camera/CFPSCamera.hpp | 21 +- common/include/camera/CFreeLockCamera.hpp | 3 +- common/include/camera/CIsometricCamera.hpp | 10 +- common/include/camera/COrbitCamera.hpp | 7 +- common/include/camera/CPathCamera.hpp | 105 +- .../include/camera/CSphericalTargetCamera.hpp | 97 +- common/include/camera/CTopDownCamera.hpp | 12 +- common/include/camera/CTurntableCamera.hpp | 2 +- common/include/camera/CVirtualGimbalEvent.hpp | 15 + common/include/camera/ICamera.hpp | 30 + common/include/camera/IGimbal.hpp | 2 +- .../include/camera/IGimbalInputProcessor.hpp | 377 ++- common/include/camera/IRange.hpp | 3 +- common/include/camera/README.md | 4 +- .../nbl/examples/common/CEventCallback.hpp | 6 +- .../CCameraJsonPersistenceUtilities.hpp | 97 + .../examples/camera/CCameraPersistence.cpp | 158 +- .../CCameraScriptedRuntimePersistence.cpp | 126 +- 112 files changed, 13948 insertions(+), 8599 deletions(-) create mode 100644 61_UI/AppCameraConfigJsonParsing.cpp create mode 100644 61_UI/AppCameraConfigUtilities.cpp create mode 100644 61_UI/AppCameraConfiguration.cpp create mode 100644 61_UI/AppCameraPlanarRuntime.cpp create mode 100644 61_UI/AppCameraUiState.cpp create mode 100644 61_UI/AppControlPanelCameraTab.cpp create mode 100644 61_UI/AppControlPanelPlaybackTab.cpp create mode 100644 61_UI/AppControlPanelPresetsTab.cpp create mode 100644 61_UI/AppControlPanelProjectionTab.cpp create mode 100644 61_UI/AppControlPanelStatusTab.cpp create mode 100644 61_UI/AppControlPanelTabs.cpp create mode 100644 61_UI/AppControlPanelUtilityTabs.cpp create mode 100644 61_UI/AppDebugSceneRendererResources.cpp create mode 100644 61_UI/AppFollowRuntime.cpp create mode 100644 61_UI/AppFrameCapture.cpp create mode 100644 61_UI/AppFrameRuntime.cpp create mode 100644 61_UI/AppHeadlessCameraSmoke.cpp create mode 100644 61_UI/AppHeadlessCameraSmokeChecks.inl create mode 100644 61_UI/AppHeadlessCameraSmokeHelpers.inl create mode 100644 61_UI/AppInputRuntime.cpp create mode 100644 61_UI/AppManipulableObjects.cpp create mode 100644 61_UI/AppPresentationResources.cpp create mode 100644 61_UI/AppPresetPlayback.cpp create mode 100644 61_UI/AppResourceBootstrap.cpp create mode 100644 61_UI/AppResourceUtilities.cpp create mode 100644 61_UI/AppSceneDebugInstances.cpp create mode 100644 61_UI/AppSceneFramebufferResources.cpp create mode 100644 61_UI/AppSceneRenderPasses.cpp create mode 100644 61_UI/AppSceneResources.cpp create mode 100644 61_UI/AppScriptedInitialization.cpp create mode 100644 61_UI/AppScriptedInputRuntime.cpp create mode 100644 61_UI/AppScriptedValidation.cpp create mode 100644 61_UI/AppSpaceEnvironmentResources.cpp create mode 100644 61_UI/AppTextResourceUtilities.cpp create mode 100644 61_UI/AppUiInputCapture.cpp create mode 100644 61_UI/AppUiResources.cpp create mode 100644 61_UI/AppUiRuntime.cpp create mode 100644 61_UI/AppViewportGizmos.cpp create mode 100644 61_UI/AppViewportWindows.cpp create mode 100644 61_UI/include/app/AppControlPanelAuthoringUtilities.hpp create mode 100644 61_UI/include/app/AppGizmoUtilities.hpp create mode 100644 61_UI/include/app/AppProjectionControlPanelUiUtilities.hpp create mode 100644 61_UI/include/app/AppRenderPassUtilities.hpp create mode 100644 61_UI/include/app/AppResourcePathUtilities.hpp create mode 100644 61_UI/include/app/AppSwapchainResources.hpp create mode 100644 61_UI/include/app/AppViewportWindowUtilities.hpp create mode 100644 common/include/camera/CCameraFileUtilities.hpp create mode 100644 common/include/camera/CCameraKindUtilities.hpp create mode 100644 common/include/camera/CCameraPathUtilities.hpp create mode 100644 common/include/camera/CCameraScriptedUiInputUtilities.hpp create mode 100644 common/include/camera/CCameraTargetRelativeUtilities.hpp create mode 100644 common/include/camera/CCameraVirtualEventUtilities.hpp create mode 100644 common/src/nbl/examples/camera/CCameraJsonPersistenceUtilities.hpp diff --git a/61_UI/AppCameraConfigJsonParsing.cpp b/61_UI/AppCameraConfigJsonParsing.cpp new file mode 100644 index 000000000..bd7722173 --- /dev/null +++ b/61_UI/AppCameraConfigJsonParsing.cpp @@ -0,0 +1,683 @@ +#include "app/AppCameraConfigUtilities.hpp" + +#include +#include +#include +#include + +#include "keysmapping.hpp" +#include "nlohmann/json.hpp" +#include "camera/CArcballCamera.hpp" +#include "camera/CChaseCamera.hpp" +#include "camera/CDollyCamera.hpp" +#include "camera/CDollyZoomCamera.hpp" +#include "camera/CFPSCamera.hpp" +#include "camera/CFreeLockCamera.hpp" +#include "camera/CIsometricCamera.hpp" +#include "camera/COrbitCamera.hpp" +#include "camera/CPathCamera.hpp" +#include "camera/CTopDownCamera.hpp" +#include "camera/CTurntableCamera.hpp" + +namespace nbl::system +{ + +using camera_json_t = nlohmann::json; + +namespace +{ + +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::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; +} + +} // namespace + +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..ae5ff6ff6 --- /dev/null +++ b/61_UI/AppCameraConfiguration.cpp @@ -0,0 +1,111 @@ +#include "app/App.hpp" + +#include +#include +#include +#include + +#include "app/AppCameraConfigUtilities.hpp" +#include "app/AppResourceUtilities.hpp" +#include "app/AppViewportBindingUtilities.hpp" +#include "camera/CCameraPersistence.hpp" +#include "camera/CCameraScriptedRuntimePersistence.hpp" + +namespace +{ +} // namespace + +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; +} + +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 = nbl::core::makeDefaultFollowConfig(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..5d3ca174b --- /dev/null +++ b/61_UI/AppCameraPlanarRuntime.cpp @@ -0,0 +1,175 @@ +#include "app/AppCameraConfigUtilities.hpp" + +#include +#include + +namespace +{ + +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 + +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::analyzeCameraCapture(goalSolver, camera); + if (!captureAnalysis.canCapture) + { + const auto kindLabel = camera ? std::string(core::getCameraKindLabel(camera->getKind())) : std::string("Unknown"); + const auto reason = + !captureAnalysis.hasCamera ? "missing camera" : + (!captureAnalysis.capturedGoal ? "capture failed" : + (!captureAnalysis.finiteGoal ? "non-finite goal" : "unknown")); + outError = + "Failed to capture initial planar preset " + std::to_string(planarIx) + + " for camera kind \"" + kindLabel + "\": " + reason; + return false; + } + + core::CCameraPreset preset = {}; + if (!core::tryCapturePreset(captureAnalysis, camera, presetName, preset)) + { + outError = + "Failed to build initial planar preset " + std::to_string(planarIx) + + " for camera kind \"" + (camera ? std::string(core::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..777542e03 --- /dev/null +++ b/61_UI/AppCameraUiState.cpp @@ -0,0 +1,84 @@ +#include "app/App.hpp" +#include "app/AppResourceUtilities.hpp" + +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() +{ + const auto planarIx = getActivePlanarIx(); + if (planarIx >= m_sceneInteraction.planarFollowConfigs.size()) + return nullptr; + return &m_sceneInteraction.planarFollowConfigs[planarIx]; +} + +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) +{ + outContext = {}; + outContext.viewport = tryGetActiveViewportRuntimeState(); + return outContext.valid(); +} + +bool App::tryBuildActiveProjectionTabContext(SActiveProjectionTabContext& outContext) +{ + outContext = {}; + outContext.viewport = tryGetActiveViewportRuntimeState(); + if (!outContext.valid()) + return false; + + outContext.activeRenderWindowIxString = std::to_string(m_viewports.activeRenderWindowIx); + outContext.activePlanarIxString = std::to_string(outContext.viewport.binding->activePlanarIx); + return true; +} + +bool App::tryBuildActiveScriptedCameraContext(SActiveScriptedCameraContext& outContext) +{ + outContext = {}; + outContext.viewport = tryGetActiveViewportRuntimeState(); + if (!outContext.valid()) + return false; + + outContext.followConfig = getActiveFollowConfig(); + 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 index 78d756b56..1387c6b18 100644 --- a/61_UI/AppControlPanel.cpp +++ b/61_UI/AppControlPanel.cpp @@ -3,1110 +3,59 @@ bool App::savePresetsToFile(const nbl::system::path& path) { - return nbl::system::savePresetCollectionToFile(path, std::span(m_presets.data(), m_presets.size())); + return nbl::system::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::loadPresetCollectionFromFile(path, m_presets); + return nbl::system::loadPresetCollectionFromFile(*m_system, path, m_presetAuthoring.presets); } bool App::saveKeyframesToFile(const nbl::system::path& path) { - return nbl::system::saveKeyframeTrackToFile(path, m_keyframeTrack); + return nbl::system::saveKeyframeTrackToFile(*m_system, path, m_playbackAuthoring.keyframeTrack); } bool App::loadKeyframesFromFile(const nbl::system::path& path) { - if (!nbl::system::loadKeyframeTrackFromFile(path, m_keyframeTrack)) + if (!nbl::system::loadKeyframeTrackFromFile(*m_system, path, m_playbackAuthoring.keyframeTrack)) return false; clampPlaybackTimeToKeyframes(); - if (m_keyframeTrack.keyframes.empty()) - clearApplyStatusBanner(m_playbackApplyBanner); + 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::calcControlPanelWindowSize(displaySize, panelStyle); - const ImVec2 panelPos = { 0.0f, 0.0f }; - ImGui::SetNextWindowPos(panelPos, ImGuiCond_Always); - ImGui::SetNextWindowSize(panelSize, ImGuiCond_Always); - - nbl::ui::pushControlPanelWindowStyle(panelStyle); - - ImGui::SetNextWindowCollapsed(false, ImGuiCond_Always); - ImGui::SetNextWindowBgAlpha(0.0f); - if (m_ciMode) - ImGui::SetNextWindowFocus(); - ImGui::Begin("Control Panel", nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar); - - const ImVec4 accent = panelStyle.AccentColor; - const ImVec4 good = panelStyle.GoodColor; - const ImVec4 bad = panelStyle.BadColor; - const ImVec4 warn = panelStyle.WarnColor; - const ImVec4 muted = panelStyle.MutedColor; - const ImVec4 badgeText = panelStyle.BadgeTextColor; - const ImVec4 keyBg = panelStyle.KeyBackgroundColor; - const ImVec4 keyFg = panelStyle.KeyTextColor; - const ImGuiTableFlags tableFlags = panelStyle.SummaryTableFlags; - const ImVec4 inactiveBadge = panelStyle.InactiveBadgeColor; - const ImVec4 cardTop = panelStyle.CardTopColor; - const ImVec4 cardBottom = panelStyle.CardBottomColor; - const ImVec4 cardBorder = panelStyle.CardBorderColor; - - { - const ImVec2 panelPos = ImGui::GetWindowPos(); - const ImVec2 panelSize = ImGui::GetWindowSize(); - if (auto* drawList = ImGui::GetWindowDrawList(); drawList) - nbl::ui::drawControlPanelWindowBackdrop(*drawList, panelPos, panelSize, panelStyle); - } - - auto row = [&](const char* label, auto&& drawValue) - { - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::TextUnformatted(label); - ImGui::TableSetColumnIndex(1); - drawValue(); - }; - - 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(accent, "Control Panel"); - ImGui::SetWindowFontScale(1.0f); - { - const float gap = ImGui::GetStyle().ItemSpacing.x; - std::array headerBadges = {{ - { useWindow ? "WINDOW" : "FULL", accent }, - { enableActiveCameraMovement ? "MOVE ON" : "MOVE OFF", enableActiveCameraMovement ? good : bad }, - { m_scriptedInput.enabled ? (m_scriptedInput.exclusive ? "SCRIPT EXCL" : "SCRIPT") : "SCRIPT OFF", m_scriptedInput.enabled ? accent : inactiveBadge }, - { "CI", warn } - }}; - const size_t headerBadgeCount = m_ciMode ? headerBadges.size() : headerBadges.size() - 1u; - nbl::ui::drawBadgeRow(std::span(headerBadges.data(), headerBadgeCount), badgeText, gap, panelStyle); - } - - ImGui::Dummy(ImVec2(0.0f, panelStyle.HeaderGapSmall)); - { - const float gap = ImGui::GetStyle().ItemSpacing.x; - const float groupGap = gap * 2.0f; - static constexpr std::array MoveKeys = { "W", "A", "S", "D" }; - static constexpr std::array LookKeys = { "RMB" }; - static constexpr std::array ZoomKeys = { "MW" }; - const std::array keyHintGroups = {{ - { "Move", MoveKeys }, - { "Look", LookKeys }, - { "Zoom", ZoomKeys } - }}; - nbl::ui::drawKeyHintGroupRow(keyHintGroups, gap, groupGap, keyBg, keyFg, panelStyle); - } - - ImGui::Dummy(ImVec2(0.0f, panelStyle.HeaderGapSmall)); - if (ImGui::BeginTable("HeaderMetrics", 3, ImGuiTableFlags_SizingStretchProp)) - { - const float frameMs = std::max(0.0f, m_uiLastFrameMs); - const float fps = frameMs > 0.0f ? (1000.0f / frameMs) : 0.0f; - const std::array miniStats = {{ - { "FrameStat", "Frame", accent, panelStyle.DefaultFrameMetricMin }, - { "InputStat", "Input", accent, panelStyle.DefaultEventMetricMin }, - { "VirtualStat", "Virtual", accent, panelStyle.DefaultEventMetricMin } - }}; - - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - nbl::ui::drawMiniStat(miniStats[0], m_uiFrameMs, m_uiMetricIndex, [&] - { - ImGui::TextColored(accent, "%.1f ms %.0f fps", frameMs, fps); - }, panelStyle); - - ImGui::TableSetColumnIndex(1); - nbl::ui::drawMiniStat(miniStats[1], m_uiInputCounts, m_uiMetricIndex, [&] - { - ImGui::TextColored(accent, "%u ev", m_uiLastInputEvents); - }, panelStyle); - - ImGui::TableSetColumnIndex(2); - nbl::ui::drawMiniStat(miniStats[2], m_uiVirtualCounts, m_uiMetricIndex, [&] - { - ImGui::TextColored(accent, "%u ev", m_uiLastVirtualEvents); - }, panelStyle); - ImGui::EndTable(); - } - } - ImGui::EndChild(); - ImGui::PopStyleVar(); - - ImGui::Spacing(); - - { - const ImVec2 togglePad = ImVec2(6.0f, 2.0f); - const float gap = ImGui::GetStyle().ItemSpacing.x; - const std::array toggleLabels = { "WINDOW", "STATUS", "EVENT LOG" }; - float rowWidth = 0.0f; - for (size_t i = 0; i < toggleLabels.size(); ++i) - { - if (i > 0u) - rowWidth += gap; - rowWidth += nbl::ui::calcPillWidth(toggleLabels[i], togglePad); - } - nbl::ui::centerControlPanelRow(rowWidth); - nbl::ui::drawTogglePill("WINDOW", useWindow, accent, inactiveBadge, badgeText, togglePad); - nbl::ui::drawHoverHint("Toggle split render windows"); - ImGui::SameLine(0.0f, gap); - nbl::ui::drawTogglePill("STATUS", m_showHud, accent, inactiveBadge, badgeText, togglePad); - nbl::ui::drawHoverHint("Show system and camera status panel"); - ImGui::SameLine(0.0f, gap); - nbl::ui::drawTogglePill("EVENT LOG", m_showEventLog, accent, inactiveBadge, badgeText, togglePad); - nbl::ui::drawHoverHint("Show virtual event log"); - } - - ImGui::Separator(); - - if (ImGui::BeginTabBar("ControlTabs")) - { - if (m_showHud && ImGui::BeginTabItem("Status")) - { - ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 4.0f); - if (ImGui::BeginChild("StatusPanel", ImVec2(0, 0), true)) - { - ImGui::PushItemWidth(-1.0f); - nbl::ui::drawSectionHeader("SessionHeader", "Session", accent, panelStyle); - if (nbl::ui::beginCard("SessionCard", nbl::ui::calcCameraControlPanelCardHeight(3, panelStyle), cardTop, cardBottom, cardBorder, panelStyle)) - { - if (ImGui::BeginTable("SessionTable", 2, tableFlags)) - { - ImGui::TableSetupColumn("Label", ImGuiTableColumnFlags_WidthFixed, panelStyle.SummaryLabelColumnWidth); - ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch); - row("Mode", [&] { nbl::ui::drawDot(accent, panelStyle); ImGui::TextColored(accent, "%s", useWindow ? "Window" : "Fullscreen"); }); - row("Active window", [&] { nbl::ui::drawDot(accent, panelStyle); ImGui::TextColored(accent, "%u", activeRenderWindowIx); }); - row("Movement", [&] { const ImVec4 c = enableActiveCameraMovement ? good : bad; nbl::ui::drawDot(c, panelStyle); ImGui::TextColored(c, "%s", enableActiveCameraMovement ? "Enabled" : "Disabled"); }); - ImGui::EndTable(); - } - } - nbl::ui::endCard(); - - nbl::ui::drawSectionHeader("CameraHeader", "Camera", accent, panelStyle); - - auto* activeCamera = getActiveCamera(); - if (activeCamera) - { - const auto& gimbal = activeCamera->getGimbal(); - const auto pos = gimbal.getPosition(); - const auto euler = getQuaternionEulerDegrees(gimbal.getOrientation()); - - if (nbl::ui::beginCard("CameraCard", nbl::ui::calcCameraControlPanelCardHeight(5, panelStyle), cardTop, cardBottom, cardBorder, panelStyle)) - { - if (ImGui::BeginTable("CameraTable", 2, tableFlags)) - { - ImGui::TableSetupColumn("Label", ImGuiTableColumnFlags_WidthFixed, panelStyle.SummaryLabelColumnWidth); - ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch); - row("Name", [&] { nbl::ui::drawDot(accent, panelStyle); ImGui::TextColored(muted, "%s", activeCamera->getIdentifier().data()); }); - row("Position", [&] { nbl::ui::drawDot(muted, panelStyle); ImGui::TextColored(muted, "%.2f %.2f %.2f", pos.x, pos.y, pos.z); }); - row("Euler", [&] { nbl::ui::drawDot(muted, panelStyle); ImGui::TextColored(muted, "%.1f %.1f %.1f", euler.x, euler.y, euler.z); }); - row("Move scale", [&] { nbl::ui::drawDot(muted, panelStyle); ImGui::TextColored(muted, "%.4f", activeCamera->getMoveSpeedScale()); }); - row("Rotate scale", [&] { nbl::ui::drawDot(muted, panelStyle); ImGui::TextColored(muted, "%.4f", activeCamera->getRotationSpeedScale()); }); - ImGui::EndTable(); - } - } - nbl::ui::endCard(); - } - else - { - if (nbl::ui::beginCard("CameraCard", nbl::ui::calcCameraControlPanelCardHeight(2, panelStyle), cardTop, cardBottom, cardBorder, panelStyle)) - ImGui::TextDisabled("No active camera"); - nbl::ui::endCard(); - } - - nbl::ui::drawSectionHeader("ProjectionHeader", "Projection", accent, panelStyle); - - auto& binding = windowBindings[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::beginCard("ProjectionCard", nbl::ui::calcCameraControlPanelCardHeight(4, panelStyle), cardTop, cardBottom, cardBorder, panelStyle)) - { - if (ImGui::BeginTable("ProjectionTable", 2, tableFlags)) - { - ImGui::TableSetupColumn("Label", ImGuiTableColumnFlags_WidthFixed, panelStyle.SummaryLabelColumnWidth); - ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch); - row("Type", [&] { nbl::ui::drawDot(accent, panelStyle); ImGui::TextColored(muted, "%s", params.m_type == IPlanarProjection::CProjection::Perspective ? "Perspective" : "Orthographic"); }); - row("zNear", [&] { nbl::ui::drawDot(muted, panelStyle); ImGui::TextColored(muted, "%.2f", params.m_zNear); }); - row("zFar", [&] { nbl::ui::drawDot(muted, panelStyle); ImGui::TextColored(muted, "%.2f", params.m_zFar); }); - if (params.m_type == IPlanarProjection::CProjection::Perspective) - row("Fov", [&] { nbl::ui::drawDot(muted, panelStyle); ImGui::TextColored(muted, "%.1f", params.m_planar.perspective.fov); }); - else - row("Ortho width", [&] { nbl::ui::drawDot(muted, panelStyle); ImGui::TextColored(muted, "%.1f", params.m_planar.orthographic.orthoWidth); }); - ImGui::EndTable(); - } - } - nbl::ui::endCard(); - } - else - { - if (nbl::ui::beginCard("ProjectionCard", nbl::ui::calcCameraControlPanelCardHeight(2, panelStyle), cardTop, cardBottom, cardBorder, panelStyle)) - ImGui::TextDisabled("No projection bound"); - nbl::ui::endCard(); - } - ImGui::PopItemWidth(); - } - ImGui::EndChild(); - ImGui::PopStyleVar(); - ImGui::EndTabItem(); - } - - if (ImGui::BeginTabItem("Projection")) - { - ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 4.0f); - if (ImGui::BeginChild("ProjectionPanel", ImVec2(0, 0), true)) - { - ImGui::PushItemWidth(-1.0f); - auto& active = windowBindings[activeRenderWindowIx]; - const auto activeRenderWindowIxString = std::to_string(activeRenderWindowIx); - - nbl::ui::drawSectionHeader("PlanarSelectHeader", "Planar Selection", accent, panelStyle); - ImGui::Text("Active Render Window: %s", activeRenderWindowIxString.c_str()); - nbl::ui::drawHoverHint("Window that receives input and camera switching"); - { - const size_t planarsCount = m_planarProjections.size(); - assert(planarsCount); - - std::vector sbels(planarsCount); - for (size_t i = 0; i < planarsCount; ++i) - sbels[i] = "Planar " + std::to_string(i); - - std::vector labels(planarsCount); - for (size_t i = 0; i < planarsCount; ++i) - labels[i] = sbels[i].c_str(); - - int currentPlanarIx = static_cast(active.activePlanarIx); - if (ImGui::Combo("Active Planar", ¤tPlanarIx, labels.data(), static_cast(labels.size()))) - { - active.activePlanarIx = static_cast(currentPlanarIx); - active.pickDefaultProjections(m_planarProjections[active.activePlanarIx]->getPlanarProjections()); - } - nbl::ui::drawHoverHint("Select which camera the window renders"); - } - - assert(active.boundProjectionIx.has_value()); - assert(active.lastBoundPerspectivePresetProjectionIx.has_value()); - assert(active.lastBoundOrthoPresetProjectionIx.has_value()); - - const auto activePlanarIxString = std::to_string(active.activePlanarIx); - auto& planarBound = m_planarProjections[active.activePlanarIx]; - assert(planarBound); - - nbl::ui::drawSectionHeader("ProjectionParamsHeader", "Projection Parameters", accent, panelStyle); - - auto selectedProjectionType = planarBound->getPlanarProjections()[active.boundProjectionIx.value()].getParameters().m_type; - { - const char* labels[] = { "Perspective", "Orthographic" }; - int type = static_cast(selectedProjectionType); - - if (ImGui::Combo("Projection Type", &type, labels, IM_ARRAYSIZE(labels))) - { - selectedProjectionType = static_cast(type); - - switch (selectedProjectionType) - { - case IPlanarProjection::CProjection::Perspective: active.boundProjectionIx = active.lastBoundPerspectivePresetProjectionIx.value(); break; - case IPlanarProjection::CProjection::Orthographic: active.boundProjectionIx = active.lastBoundOrthoPresetProjectionIx.value(); break; - default: active.boundProjectionIx = std::nullopt; assert(false); break; - } - } - nbl::ui::drawHoverHint("Switch projection type for this planar"); - } - - auto getPresetName = [&](auto ix) -> std::string - { - switch (selectedProjectionType) - { - case IPlanarProjection::CProjection::Perspective: return "Perspective Projection Preset " + std::to_string(ix); - case IPlanarProjection::CProjection::Orthographic: return "Orthographic Projection Preset " + std::to_string(ix); - default: return "Unknown Projection Preset " + std::to_string(ix); - } - }; - - bool updateBoundVirtualMaps = false; - if (ImGui::BeginCombo("Projection Preset", getPresetName(active.boundProjectionIx.value()).c_str())) - { - auto& projections = planarBound->getPlanarProjections(); - - for (uint32_t i = 0; i < projections.size(); ++i) - { - const auto& projection = projections[i]; - const auto& params = projection.getParameters(); - - if (params.m_type != selectedProjectionType) - continue; - - bool isSelected = (i == active.boundProjectionIx.value()); - - if (ImGui::Selectable(getPresetName(i).c_str(), isSelected)) - { - active.boundProjectionIx = i; - updateBoundVirtualMaps |= true; - - switch (selectedProjectionType) - { - case IPlanarProjection::CProjection::Perspective: active.lastBoundPerspectivePresetProjectionIx = active.boundProjectionIx.value(); break; - case IPlanarProjection::CProjection::Orthographic: active.lastBoundOrthoPresetProjectionIx = active.boundProjectionIx.value(); break; - default: assert(false); break; - } - } - - if (isSelected) - ImGui::SetItemDefaultFocus(); - } - ImGui::EndCombo(); - } - if (updateBoundVirtualMaps) - syncWindowInputBinding(active); - nbl::ui::drawHoverHint("Switch preset projection for this planar"); - - auto* const boundCamera = planarBound->getCamera(); - auto& boundProjection = planarBound->getPlanarProjections()[active.boundProjectionIx.value()]; - assert(not boundProjection.isProjectionSingular()); - - auto updateParameters = boundProjection.getParameters(); - - if (useWindow) - ImGui::Checkbox("Allow axes to flip##allowAxesToFlip", &active.allowGizmoAxesToFlip); - nbl::ui::drawHoverHint("Allow ImGuizmo axes to flip based on view"); - - if(useWindow) - ImGui::Checkbox("Draw debug grid##drawDebugGrid", &active.enableDebugGridDraw); - nbl::ui::drawHoverHint("Toggle debug grid in the render window"); - - if (ImGui::RadioButton("LH", active.leftHandedProjection)) - active.leftHandedProjection = true; - - ImGui::SameLine(); - - if (ImGui::RadioButton("RH", not active.leftHandedProjection)) - active.leftHandedProjection = false; - nbl::ui::drawHoverHint("Toggle left or right handed projection"); - - updateParameters.m_zNear = std::clamp(updateParameters.m_zNear, 0.1f, 100.f); - updateParameters.m_zFar = std::clamp(updateParameters.m_zFar, 110.f, 10000.f); - - ImGui::SliderFloat("zNear", &updateParameters.m_zNear, 0.1f, 100.f, "%.2f", ImGuiSliderFlags_Logarithmic); - nbl::ui::drawHoverHint("Near clip plane"); - ImGui::SliderFloat("zFar", &updateParameters.m_zFar, 110.f, 10000.f, "%.1f", ImGuiSliderFlags_Logarithmic); - nbl::ui::drawHoverHint("Far clip plane"); - - switch (selectedProjectionType) - { - case IPlanarProjection::CProjection::Perspective: - { - ImGui::SliderFloat("Fov", &updateParameters.m_planar.perspective.fov, 20.f, 150.f, "%.1f", ImGuiSliderFlags_Logarithmic); - nbl::ui::drawHoverHint("Perspective field of view"); - boundProjection.setPerspective(updateParameters.m_zNear, updateParameters.m_zFar, updateParameters.m_planar.perspective.fov); - } break; - - case IPlanarProjection::CProjection::Orthographic: - { - ImGui::SliderFloat("Ortho width", &updateParameters.m_planar.orthographic.orthoWidth, 1.f, 30.f, "%.1f", ImGuiSliderFlags_Logarithmic); - nbl::ui::drawHoverHint("Orthographic width"); - boundProjection.setOrthographic(updateParameters.m_zNear, updateParameters.m_zFar, updateParameters.m_planar.orthographic.orthoWidth); - } break; - - default: break; - } - - nbl::ui::drawSectionHeader("CursorHeader", "Cursor Behaviour", accent, panelStyle); - if (ImGui::TreeNodeEx("Cursor Behaviour")) - { - ImGui::Checkbox("Capture OS cursor in move mode", &captureCursorInMoveMode); - nbl::ui::drawHoverHint("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(); - } - - if (enableActiveCameraMovement) - ImGui::TextColored(good, "Bound Camera Movement: Enabled"); - else - ImGui::TextColored(bad, "Bound Camera Movement: Disabled"); - - ImGui::Separator(); - - nbl::ui::drawSectionHeader("BoundCameraHeader", "Bound Camera", accent, panelStyle); - const auto flags = ImGuiTreeNodeFlags_DefaultOpen; - if (ImGui::TreeNodeEx("Bound Camera", flags)) - { - ImGui::Text("Type: %s", boundCamera->getIdentifier().data()); - ImGui::Text("Object Ix: %s", std::to_string(active.activePlanarIx + 2u).c_str()); - ImGui::Separator(); - { - ICamera::SphericalTargetState sphericalState; - const bool isOrbitLike = boundCamera->tryGetSphericalTargetState(sphericalState); - - float moveSpeed = boundCamera->getMoveSpeedScale(); - float rotationSpeed = boundCamera->getRotationSpeedScale(); - - ImGui::SliderFloat("Move speed factor", &moveSpeed, 0.0001f, 10.f, "%.4f", ImGuiSliderFlags_Logarithmic); - nbl::ui::drawHoverHint("Scale translation speed for this camera"); - - if (boundCamera->getAllowedVirtualEvents() & CVirtualGimbalEvent::Rotate) - ImGui::SliderFloat("Rotate speed factor", &rotationSpeed, 0.0001f, 10.f, "%.4f", ImGuiSliderFlags_Logarithmic); - nbl::ui::drawHoverHint("Scale rotation speed for this camera"); - - boundCamera->setMotionScales(moveSpeed, rotationSpeed); - - if (isOrbitLike) - { - float distance = sphericalState.distance; - ImGui::SliderFloat("Distance", &distance, sphericalState.minDistance, sphericalState.maxDistance, "%.4f", ImGuiSliderFlags_Logarithmic); - nbl::ui::drawHoverHint("Current orbit distance"); - boundCamera->trySetSphericalDistance(distance); - } - } - - if (ImGui::TreeNodeEx("World Data", flags)) - { - auto& gimbal = boundCamera->getGimbal(); - const auto position = getCastedVector(gimbal.getPosition()); - const auto orientation = getCastedVector(gimbal.getOrientation().data); - const auto viewMatrix = getCastedMatrix(gimbal.getViewMatrix()); - - addMatrixTable("Position", ("PositionTable_" + activePlanarIxString).c_str(), 1, 3, &position[0], false); - addMatrixTable("Orientation (Quaternion)", ("OrientationTable_" + activePlanarIxString).c_str(), 1, 4, &orientation[0], false); - addMatrixTable("View Matrix", ("ViewMatrixTable_" + activePlanarIxString).c_str(), 3, 4, &viewMatrix[0][0], false); - ImGui::TreePop(); - } - - if (ImGui::TreeNodeEx("Virtual Event Mappings", flags)) - { - syncWindowInputBinding(active); - if (displayKeyMappingsAndVirtualStatesInline(&active.inputBinding)) - syncWindowInputBindingToProjection(active); - ImGui::TreePop(); - } - - ImGui::TreePop(); - } - ImGui::PopItemWidth(); - } - ImGui::EndChild(); - ImGui::PopStyleVar(); - ImGui::EndTabItem(); - } - - if (ImGui::BeginTabItem("Camera")) - { - ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 4.0f); - if (ImGui::BeginChild("CameraPanel", ImVec2(0, 0), true)) - { - ImGui::PushItemWidth(-1.0f); - nbl::ui::drawSectionHeader("CameraInputHeader", "Input", accent, panelStyle); - ImGui::Checkbox("Mirror input to all cameras", &m_cameraControls.mirrorInput); - nbl::ui::drawHoverHint("Apply keyboard and mouse input to every camera"); - ImGui::Checkbox("World translate", &m_cameraControls.worldTranslate); - nbl::ui::drawHoverHint("Translate in world space instead of camera space"); - ImGui::SliderFloat("Keyboard scale", &m_cameraControls.keyboardScale, 0.01f, 10.f, "%.2f"); - nbl::ui::drawHoverHint("Scale keyboard movement magnitudes"); - ImGui::SliderFloat("Mouse move scale", &m_cameraControls.mouseMoveScale, 0.01f, 10.f, "%.2f"); - nbl::ui::drawHoverHint("Scale mouse move magnitudes"); - ImGui::SliderFloat("Mouse scroll scale", &m_cameraControls.mouseScrollScale, 0.01f, 10.f, "%.2f"); - nbl::ui::drawHoverHint("Scale mouse wheel magnitudes"); - ImGui::SliderFloat("Translate scale", &m_cameraControls.translationScale, 0.01f, 10.f, "%.2f"); - nbl::ui::drawHoverHint("Overall translation scale for virtual events"); - ImGui::SliderFloat("Rotate scale", &m_cameraControls.rotationScale, 0.01f, 10.f, "%.2f"); - nbl::ui::drawHoverHint("Overall rotation scale for virtual events"); - - nbl::ui::drawSectionHeader("CameraConstraintsHeader", "Constraints", accent, panelStyle); - ImGui::Checkbox("Enable constraints", &m_cameraConstraints.enabled); - nbl::ui::drawHoverHint("Enable or disable all camera constraints"); - ImGui::Checkbox("Clamp distance", &m_cameraConstraints.clampDistance); - nbl::ui::drawHoverHint("Clamp orbit distance to min/max"); - ImGui::SliderFloat("Min distance", &m_cameraConstraints.minDistance, 0.01f, 1000.f, "%.3f", ImGuiSliderFlags_Logarithmic); - nbl::ui::drawHoverHint("Minimum orbit distance"); - ImGui::SliderFloat("Max distance", &m_cameraConstraints.maxDistance, 0.01f, 10000.f, "%.3f", ImGuiSliderFlags_Logarithmic); - nbl::ui::drawHoverHint("Maximum orbit distance"); - ImGui::Separator(); - ImGui::Checkbox("Clamp pitch", &m_cameraConstraints.clampPitch); - nbl::ui::drawHoverHint("Clamp pitch angle"); - ImGui::SliderFloat("Pitch min", &m_cameraConstraints.pitchMinDeg, -180.f, 180.f, "%.1f"); - nbl::ui::drawHoverHint("Minimum pitch in degrees"); - ImGui::SliderFloat("Pitch max", &m_cameraConstraints.pitchMaxDeg, -180.f, 180.f, "%.1f"); - nbl::ui::drawHoverHint("Maximum pitch in degrees"); - ImGui::Checkbox("Clamp yaw", &m_cameraConstraints.clampYaw); - nbl::ui::drawHoverHint("Clamp yaw angle"); - ImGui::SliderFloat("Yaw min", &m_cameraConstraints.yawMinDeg, -180.f, 180.f, "%.1f"); - nbl::ui::drawHoverHint("Minimum yaw in degrees"); - ImGui::SliderFloat("Yaw max", &m_cameraConstraints.yawMaxDeg, -180.f, 180.f, "%.1f"); - nbl::ui::drawHoverHint("Maximum yaw in degrees"); - ImGui::Checkbox("Clamp roll", &m_cameraConstraints.clampRoll); - nbl::ui::drawHoverHint("Clamp roll angle"); - ImGui::SliderFloat("Roll min", &m_cameraConstraints.rollMinDeg, -180.f, 180.f, "%.1f"); - nbl::ui::drawHoverHint("Minimum roll in degrees"); - ImGui::SliderFloat("Roll max", &m_cameraConstraints.rollMaxDeg, -180.f, 180.f, "%.1f"); - nbl::ui::drawHoverHint("Maximum roll in degrees"); - - nbl::ui::drawSectionHeader("OrbitHeader", "Orbit Target", accent, panelStyle); - - auto* activeCamera = getActiveCamera(); - ICamera::SphericalTargetState orbitState; - const bool hasOrbitTarget = activeCamera && activeCamera->tryGetSphericalTargetState(orbitState); - if (hasOrbitTarget) - { - auto target = getCastedVector(orbitState.target); - if (ImGui::InputFloat3("Target", &target[0])) - activeCamera->trySetSphericalTarget(getCastedVector(target)); - - if (ImGui::Button("Target model")) - { - const auto targetPos = hlsl::transpose(getMatrix3x4As4x4(m_model))[3]; - activeCamera->trySetSphericalTarget(float64_t3(targetPos.x, targetPos.y, targetPos.z)); - } - nbl::ui::drawHoverHint("Set orbit target to the model position"); - ImGui::SameLine(); - if (ImGui::Button("Target origin")) - activeCamera->trySetSphericalTarget(float64_t3(0.0)); - nbl::ui::drawHoverHint("Set orbit target to world origin"); - } - if (!hasOrbitTarget) - { - ImGui::TextDisabled("Active camera is not orbit."); - } - - nbl::ui::drawSectionHeader("FollowHeader", "Follow Target", accent, panelStyle); - auto* activeFollowConfig = getActiveFollowConfig(); - if (activeFollowConfig) - { - auto& followConfig = *activeFollowConfig; - const bool prevFollowEnabled = followConfig.enabled; - const auto prevFollowMode = followConfig.mode; - ImGui::Checkbox("Enable follow", &followConfig.enabled); - nbl::ui::drawHoverHint("Apply tracked-target follow to the active planar camera"); - - const char* followModeLabels[] = { - getCameraFollowModeLabel(ECameraFollowMode::Disabled), - getCameraFollowModeLabel(ECameraFollowMode::OrbitTarget), - getCameraFollowModeLabel(ECameraFollowMode::LookAtTarget), - getCameraFollowModeLabel(ECameraFollowMode::KeepWorldOffset), - 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::cameraFollowModeUsesCapturedOffset(followConfig.mode)) - captureFollowOffsetsForPlanar(getActivePlanarIx()); - if (followStateChanged && followConfig.enabled) - applyFollowToConfiguredCameras(); - - auto trackedTarget = getCastedVector(m_followTarget.getGimbal().getPosition()); - if (ImGui::InputFloat3("Tracked target", &trackedTarget[0])) - m_followTarget.setPosition(getCastedVector(trackedTarget)); - - ImGui::Checkbox("Show target marker", &m_followTargetVisible); - nbl::ui::drawHoverHint("Render the tracked target marker in the scene"); - - if (ImGui::Button("Reset target")) - resetFollowTargetToDefault(); - nbl::ui::drawHoverHint("Reset tracked target gimbal to the default world-space follow pose"); - ImGui::SameLine(); - if (ImGui::Button("Snap to model")) - snapFollowTargetToModel(); - nbl::ui::drawHoverHint("Optionally snap tracked target gimbal to the model transform"); - ImGui::SameLine(); - if (ImGui::Button("Target origin")) - m_followTarget.setPose(float64_t3(0.0), makeIdentityQuaternion()); - nbl::ui::drawHoverHint("Reset tracked target to identity at world origin"); - ImGui::SameLine(); - if (ImGui::Button("Capture current offset")) - captureFollowOffsetsForPlanar(getActivePlanarIx()); - nbl::ui::drawHoverHint("Store current camera-to-target relation into the active follow config"); - - if (cameraFollowModeUsesWorldOffset(followConfig.mode)) - { - auto worldOffset = getCastedVector(followConfig.worldOffset); - if (ImGui::InputFloat3("World offset", &worldOffset[0])) - followConfig.worldOffset = getCastedVector(worldOffset); - } - if (cameraFollowModeUsesLocalOffset(followConfig.mode)) - { - auto localOffset = getCastedVector(followConfig.localOffset); - if (ImGui::InputFloat3("Local offset", &localOffset[0])) - followConfig.localOffset = getCastedVector(localOffset); - } - } - else - { - ImGui::TextDisabled("No active follow config."); - } - ImGui::PopItemWidth(); - } - ImGui::EndChild(); - ImGui::PopStyleVar(); - ImGui::EndTabItem(); - } - - if (ImGui::BeginTabItem("Presets")) - { - ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 4.0f); - if (ImGui::BeginChild("PresetsPanel", ImVec2(0, 0), true)) - { - ImGui::PushItemWidth(-1.0f); - nbl::ui::drawSectionHeader("PresetsHeader", "Presets", accent, panelStyle); - ImGui::InputText("Preset name", m_presetName, IM_ARRAYSIZE(m_presetName)); - auto* activeCamera = getActiveCamera(); - const auto presetCaptureUi = analyzeCameraCaptureForUi(activeCamera); - if (!presetCaptureUi.canCapture) - ImGui::BeginDisabled(); - if (ImGui::Button("Add preset")) - { - CameraPreset preset; - if (nbl::core::tryCapturePreset(m_cameraGoalSolver, activeCamera, m_presetName, preset)) - { - m_presets.emplace_back(std::move(preset)); - m_selectedPresetIx = static_cast(m_presets.size()) - 1; - } - } - if (!presetCaptureUi.canCapture) - ImGui::EndDisabled(); - nbl::ui::drawHoverHint(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"); - ImGui::SameLine(); - if (ImGui::Button("Clear presets")) - { - m_presets.clear(); - m_selectedPresetIx = -1; - } - nbl::ui::drawHoverHint("Remove all presets"); - ImGui::TextDisabled("Capture"); - ImGui::SameLine(); - ImGui::TextColored(presetCaptureUi.canCapture ? good : bad, "%s", presetCaptureUi.policyLabel.c_str()); - - if (!m_presets.empty()) - { - const char* presetFilterLabels[] = { - nbl::ui::getPresetApplyPresentationFilterLabel(PresetFilterMode::All), - nbl::ui::getPresetApplyPresentationFilterLabel(PresetFilterMode::Exact), - nbl::ui::getPresetApplyPresentationFilterLabel(PresetFilterMode::BestEffort) - }; - int presetFilterIx = static_cast(m_presetFilterMode); - if (ImGui::Combo("Visibility", &presetFilterIx, presetFilterLabels, IM_ARRAYSIZE(presetFilterLabels))) - m_presetFilterMode = static_cast(presetFilterIx); - nbl::ui::drawHoverHint("Filter presets for the active camera using exact or best-effort compatibility"); - - std::vector filteredPresetIndices; - filteredPresetIndices.reserve(m_presets.size()); - for (size_t i = 0; i < m_presets.size(); ++i) - { - if (presetMatchesFilter(activeCamera, m_presets[i])) - filteredPresetIndices.push_back(static_cast(i)); - } - - if (filteredPresetIndices.empty()) - { - ImGui::TextDisabled("No presets match the current filter."); - } - else - { - if (m_selectedPresetIx < 0 || - std::find(filteredPresetIndices.begin(), filteredPresetIndices.end(), m_selectedPresetIx) == filteredPresetIndices.end()) - { - m_selectedPresetIx = filteredPresetIndices.front(); - } - - int selectedFilteredPresetIx = 0; - for (int i = 0; i < static_cast(filteredPresetIndices.size()); ++i) - { - if (filteredPresetIndices[i] == m_selectedPresetIx) - { - selectedFilteredPresetIx = i; - break; - } - } - - std::vector names; - names.reserve(filteredPresetIndices.size()); - for (const auto presetIx : filteredPresetIndices) - names.push_back(m_presets[static_cast(presetIx)].name.c_str()); - - if (ImGui::ListBox("Preset list", &selectedFilteredPresetIx, names.data(), static_cast(names.size()), 6)) - m_selectedPresetIx = filteredPresetIndices[static_cast(selectedFilteredPresetIx)]; - - if (m_selectedPresetIx >= 0 && static_cast(m_selectedPresetIx) < m_presets.size()) - { - const auto& preset = m_presets[static_cast(m_selectedPresetIx)]; - const auto presetUi = analyzePresetForUi(activeCamera, preset); - const ImVec4 compatibilityColor = !presetUi.hasCamera ? bad : (presetUi.exact() ? good : warn); - - ImGui::TextDisabled("Preset source"); - ImGui::SameLine(); - ImGui::TextColored(muted, "%s", presetUi.sourceKindLabel.c_str()); - ImGui::TextDisabled("Goal state"); - ImGui::SameLine(); - ImGui::TextColored(muted, "%s", presetUi.goalStateLabel.c_str()); - ImGui::TextDisabled("Policy"); - ImGui::SameLine(); - ImGui::TextColored(presetUi.canApply ? compatibilityColor : bad, "%s", presetUi.policyLabel.c_str()); - ImGui::TextDisabled("Compatibility"); - ImGui::SameLine(); - ImGui::TextColored(compatibilityColor, "%s", presetUi.compatibilityLabel.c_str()); - - if (presetUi.badges.exact) - nbl::ui::drawBadge("EXACT", good, badgeText, panelStyle); - else if (presetUi.badges.bestEffort) - nbl::ui::drawBadge("BEST-EFFORT", warn, badgeText, panelStyle); - if (presetUi.badges.dropsState) - { - ImGui::SameLine(); - nbl::ui::drawBadge("DROPS STATE", warn, badgeText, panelStyle); - } - else if (presetUi.badges.sharedStateOnly) - { - ImGui::SameLine(); - nbl::ui::drawBadge("SHARED STATE", accent, badgeText, panelStyle); - } - if (presetUi.badges.blocked) - { - ImGui::SameLine(); - nbl::ui::drawBadge("BLOCKED", bad, badgeText, panelStyle); - } - - if (!presetUi.canApply) - ImGui::BeginDisabled(); - if (ImGui::Button("Apply preset")) - applyPresetFromUi(activeCamera, preset); - if (!presetUi.canApply) - ImGui::EndDisabled(); - nbl::ui::drawHoverHint(presetUi.canApply ? - "Apply selected preset to the active camera" : - "Apply is blocked because there is no active camera or the preset goal is invalid"); - ImGui::SameLine(); - if (ImGui::Button("Remove preset")) - { - m_presets.erase(m_presets.begin() + m_selectedPresetIx); - m_selectedPresetIx = -1; - } - nbl::ui::drawHoverHint("Remove selected preset"); - } - } - } - - if (m_manualPresetApplyBanner.visible()) - { - const ImVec4 resultColor = m_manualPresetApplyBanner.succeeded ? (m_manualPresetApplyBanner.approximate ? warn : good) : bad; - ImGui::TextColored(resultColor, "%s", m_manualPresetApplyBanner.summary.c_str()); - } - - nbl::ui::drawSectionHeader("PresetsStorageHeader", "Storage", accent, panelStyle); - ImGui::InputText("Preset file", m_presetPath, IM_ARRAYSIZE(m_presetPath)); - if (ImGui::Button("Save presets")) - { - if (!savePresetsToFile(nbl::system::path(m_presetPath))) - m_logger->log("Failed to save presets to \"%s\".", ILogger::ELL_ERROR, m_presetPath); - } - nbl::ui::drawHoverHint("Save presets to JSON file"); - ImGui::SameLine(); - if (ImGui::Button("Load presets")) - { - if (!loadPresetsFromFile(nbl::system::path(m_presetPath))) - m_logger->log("Failed to load presets from \"%s\".", ILogger::ELL_ERROR, m_presetPath); - } - nbl::ui::drawHoverHint("Load presets from JSON file"); - ImGui::PopItemWidth(); - } - ImGui::EndChild(); - ImGui::PopStyleVar(); - ImGui::EndTabItem(); - } - - if (ImGui::BeginTabItem("Playback")) - { - ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 4.0f); - if (ImGui::BeginChild("PlaybackPanel", ImVec2(0, 0), true)) - { - ImGui::PushItemWidth(-1.0f); - auto* activeCamera = getActiveCamera(); - nbl::ui::drawSectionHeader("PlaybackHeader", "Playback", accent, panelStyle); - ImGui::Checkbox("Loop", &m_playback.loop); - nbl::ui::drawHoverHint("Loop playback when it reaches the end"); - ImGui::Checkbox("Override input", &m_playback.overrideInput); - nbl::ui::drawHoverHint("Ignore manual input during playback"); - ImGui::Checkbox("Affect all cameras", &m_playbackAffectsAll); - nbl::ui::drawHoverHint("Apply playback to all cameras"); - ImGui::SliderFloat("Speed", &m_playback.speed, 0.1f, 4.f, "%.2f"); - nbl::ui::drawHoverHint("Playback speed multiplier"); - - if (ImGui::Button(m_playback.playing ? "Pause" : "Play")) - m_playback.playing = !m_playback.playing; - nbl::ui::drawHoverHint("Start or pause playback"); - ImGui::SameLine(); - if (ImGui::Button("Stop")) - { - nbl::core::resetPlaybackCursor(m_playback); - applyPlaybackAtTime(m_playback.time); - } - nbl::ui::drawHoverHint("Stop playback and reset time"); - - if (!m_keyframeTrack.keyframes.empty()) - { - const float duration = nbl::core::getPlaybackTrackDuration(m_keyframeTrack); - if (ImGui::SliderFloat("Time", &m_playback.time, 0.f, duration, "%.3f")) - applyPlaybackAtTime(m_playback.time); - } - if (m_playbackApplyBanner.visible()) - { - const ImVec4 playbackColor = m_playbackApplyBanner.succeeded ? (m_playbackApplyBanner.approximate ? warn : good) : bad; - ImGui::TextColored(playbackColor, "%s", m_playbackApplyBanner.summary.c_str()); - } - if (!m_keyframeTrack.keyframes.empty()) - { - CameraPreset playbackPreviewPreset; - if (tryBuildPlaybackPresetAtTime(m_playback.time, playbackPreviewPreset)) - { - const auto playbackPreviewUi = analyzePresetForUi(activeCamera, playbackPreviewPreset); - const ImVec4 previewColor = !playbackPreviewUi.hasCamera ? bad : (playbackPreviewUi.exact() ? good : warn); - ImGui::TextDisabled("Preview"); - ImGui::SameLine(); - ImGui::TextColored(playbackPreviewUi.canApply ? previewColor : bad, "%s", playbackPreviewUi.policyLabel.c_str()); - } - } - - nbl::ui::drawSectionHeader("KeyframesHeader", "Keyframes", accent, panelStyle); - ImGui::InputFloat("New keyframe time", &m_newKeyframeTime, 0.1f, 1.f, "%.3f"); - nbl::ui::drawHoverHint("Time value for new keyframe"); - ImGui::SameLine(); - if (ImGui::Button("Use playback time")) - m_newKeyframeTime = m_playback.time; - nbl::ui::drawHoverHint("Set new keyframe time from current playback position"); - const auto keyframeCaptureUi = analyzeCameraCaptureForUi(activeCamera); - if (!keyframeCaptureUi.canCapture) - ImGui::BeginDisabled(); - if (ImGui::Button("Add keyframe")) - { - CameraKeyframe keyframe; - const float authoredTime = std::max(0.f, m_newKeyframeTime); - keyframe.time = authoredTime; - m_newKeyframeTime = authoredTime; - if (nbl::core::tryCapturePreset(m_cameraGoalSolver, activeCamera, "Keyframe", keyframe.preset)) - { - m_keyframeTrack.keyframes.emplace_back(std::move(keyframe)); - sortKeyframesByTime(); - selectKeyframeNearestTime(authoredTime); - } - } - if (!keyframeCaptureUi.canCapture) - ImGui::EndDisabled(); - nbl::ui::drawHoverHint(keyframeCaptureUi.canCapture ? - "Add keyframe from current camera" : - "Keyframe capture is blocked because there is no active camera or the current goal state is invalid"); - ImGui::TextDisabled("Capture"); - ImGui::SameLine(); - ImGui::TextColored(keyframeCaptureUi.canCapture ? good : bad, "%s", keyframeCaptureUi.policyLabel.c_str()); - ImGui::SameLine(); - if (ImGui::Button("Clear keyframes")) - { - m_keyframeTrack = {}; - nbl::core::resetPlaybackCursor(m_playback); - clearApplyStatusBanner(m_playbackApplyBanner); - } - nbl::ui::drawHoverHint("Remove all keyframes"); - - if (!m_keyframeTrack.keyframes.empty()) - { - normalizeSelectedKeyframe(); - if (ImGui::BeginChild("KeyframeList", ImVec2(0, 120), true)) - { - for (size_t i = 0; i < m_keyframeTrack.keyframes.size(); ++i) - { - char label[128]; - snprintf(label, sizeof(label), "[%zu] t=%.3f %s", i, m_keyframeTrack.keyframes[i].time, m_keyframeTrack.keyframes[i].preset.name.c_str()); - if (ImGui::Selectable(label, m_keyframeTrack.selectedKeyframeIx == static_cast(i))) - m_keyframeTrack.selectedKeyframeIx = static_cast(i); - } - } - ImGui::EndChild(); - - if (auto* selectedKeyframe = getSelectedKeyframe()) - { - const auto keyframeUi = analyzePresetForUi(activeCamera, selectedKeyframe->preset); - const ImVec4 compatibilityColor = !keyframeUi.hasCamera ? bad : (keyframeUi.exact() ? good : warn); - float selectedTime = selectedKeyframe->time; - if (ImGui::InputFloat("Selected time", &selectedTime, 0.1f, 1.f, "%.3f")) - { - selectedTime = std::max(0.f, selectedTime); - selectedKeyframe->time = selectedTime; - sortKeyframesByTime(); - selectKeyframeNearestTime(selectedTime); - clampPlaybackTimeToKeyframes(); - } - nbl::ui::drawHoverHint("Edit selected keyframe time"); - - ImGui::TextDisabled("Keyframe source"); - ImGui::SameLine(); - ImGui::TextColored(muted, "%s", keyframeUi.sourceKindLabel.c_str()); - ImGui::TextDisabled("Goal state"); - ImGui::SameLine(); - ImGui::TextColored(muted, "%s", keyframeUi.goalStateLabel.c_str()); - ImGui::TextDisabled("Policy"); - ImGui::SameLine(); - ImGui::TextColored(keyframeUi.canApply ? compatibilityColor : bad, "%s", keyframeUi.policyLabel.c_str()); - ImGui::TextDisabled("Compatibility"); - ImGui::SameLine(); - ImGui::TextColored(compatibilityColor, "%s", keyframeUi.compatibilityLabel.c_str()); - - if (keyframeUi.badges.exact) - nbl::ui::drawBadge("EXACT", good, badgeText, panelStyle); - else if (keyframeUi.badges.bestEffort) - nbl::ui::drawBadge("BEST-EFFORT", warn, badgeText, panelStyle); - if (keyframeUi.badges.dropsState) - { - ImGui::SameLine(); - nbl::ui::drawBadge("DROPS STATE", warn, badgeText, panelStyle); - } - else if (keyframeUi.badges.sharedStateOnly) - { - ImGui::SameLine(); - nbl::ui::drawBadge("SHARED STATE", accent, badgeText, panelStyle); - } - if (keyframeUi.badges.blocked) - { - ImGui::SameLine(); - nbl::ui::drawBadge("BLOCKED", bad, badgeText, panelStyle); - } - - if (!keyframeUi.canApply) - ImGui::BeginDisabled(); - if (ImGui::Button("Apply selected")) - applyPresetFromUi(activeCamera, selectedKeyframe->preset); - if (!keyframeUi.canApply) - ImGui::EndDisabled(); - nbl::ui::drawHoverHint(keyframeUi.canApply ? - "Apply selected keyframe to the active camera" : - "Apply is blocked because there is no active camera or the keyframe goal is invalid"); - ImGui::SameLine(); - if (!keyframeCaptureUi.canCapture) - ImGui::BeginDisabled(); - if (ImGui::Button("Replace from camera")) - replaceSelectedKeyframeFromCamera(activeCamera); - if (!keyframeCaptureUi.canCapture) - ImGui::EndDisabled(); - nbl::ui::drawHoverHint(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"); - ImGui::SameLine(); - if (ImGui::Button("Jump to selected")) - { - m_playback.time = selectedKeyframe->time; - applyPlaybackAtTime(m_playback.time); - } - nbl::ui::drawHoverHint("Set playback time to selected keyframe and preview it"); - ImGui::SameLine(); - if (ImGui::Button("Remove selected")) - { - m_keyframeTrack.keyframes.erase(m_keyframeTrack.keyframes.begin() + m_keyframeTrack.selectedKeyframeIx); - normalizeSelectedKeyframe(); - clampPlaybackTimeToKeyframes(); - if (m_keyframeTrack.keyframes.empty()) - clearApplyStatusBanner(m_playbackApplyBanner); - } - nbl::ui::drawHoverHint("Remove selected keyframe"); - } - - nbl::ui::drawSectionHeader("KeyframesStorageHeader", "Keyframe Storage", accent, panelStyle); - ImGui::InputText("Keyframe file", m_keyframePath, IM_ARRAYSIZE(m_keyframePath)); - if (ImGui::Button("Save keyframes")) - { - if (!saveKeyframesToFile(nbl::system::path(m_keyframePath))) - m_logger->log("Failed to save keyframes to \"%s\".", ILogger::ELL_ERROR, m_keyframePath); - } - nbl::ui::drawHoverHint("Save keyframes to JSON file"); - ImGui::SameLine(); - if (ImGui::Button("Load keyframes")) - { - if (!loadKeyframesFromFile(nbl::system::path(m_keyframePath))) - m_logger->log("Failed to load keyframes from \"%s\".", ILogger::ELL_ERROR, m_keyframePath); - } - nbl::ui::drawHoverHint("Load keyframes from JSON file"); - } - ImGui::PopItemWidth(); - } - ImGui::EndChild(); - ImGui::PopStyleVar(); - ImGui::EndTabItem(); - } - - if (ImGui::BeginTabItem("Gizmo")) - { - ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 4.0f); - if (ImGui::BeginChild("GizmoPanel", ImVec2(0, 0), true)) - { - nbl::ui::drawSectionHeader("GizmoHeader", "Gizmo", accent, panelStyle); - TransformEditorContents(); - } - ImGui::EndChild(); - ImGui::PopStyleVar(); - ImGui::EndTabItem(); - } - - if (m_showEventLog && ImGui::BeginTabItem("Log")) - { - ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 4.0f); - if (ImGui::BeginChild("LogPanel", ImVec2(0, 0), true)) - { - nbl::ui::drawSectionHeader("LogHeader", "Virtual Events", accent, panelStyle); - ImGui::Checkbox("Auto-scroll", &m_logAutoScroll); - ImGui::SameLine(); - ImGui::Checkbox("Wrap", &m_logWrap); - ImGui::Separator(); - - ImGuiWindowFlags logFlags = m_logWrap ? ImGuiWindowFlags_None : ImGuiWindowFlags_HorizontalScrollbar; - if (ImGui::BeginChild("LogList", ImVec2(0, 0), false, logFlags)) - { - const float scrollY = ImGui::GetScrollY(); - const float scrollMax = ImGui::GetScrollMaxY(); - const bool wasAtBottom = scrollY >= scrollMax - 5.0f; - const size_t start = m_virtualEventLog.size() > 200 ? m_virtualEventLog.size() - 200 : 0; - if (m_logWrap) - ImGui::PushTextWrapPos(0.0f); - for (size_t i = start; i < m_virtualEventLog.size(); ++i) - { - const auto& entry = m_virtualEventLog[i]; - ImGui::TextUnformatted(entry.line.c_str()); - } - if (m_logWrap) - ImGui::PopTextWrapPos(); - if (m_logAutoScroll && wasAtBottom && !m_virtualEventLog.empty()) - ImGui::SetScrollHereY(1.0f); - } - ImGui::EndChild(); - } - ImGui::EndChild(); - ImGui::PopStyleVar(); - ImGui::EndTabItem(); - } - - ImGui::EndTabBar(); - } - - ImGui::End(); - nbl::ui::popControlPanelWindowStyle(); - + const nbl::ui::SCameraControlPanelStyle panelStyle = {}; + const ImVec2 displaySize = ImGui::GetIO().DisplaySize; + const ImVec2 panelSize = nbl::ui::calcControlPanelWindowSize(displaySize, panelStyle); + const ImVec2 panelPos = { 0.0f, 0.0f }; + ImGui::SetNextWindowPos(panelPos, ImGuiCond_Always); + ImGui::SetNextWindowSize(panelSize, ImGuiCond_Always); + + nbl::ui::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::drawControlPanelWindowBackdrop(*drawList, ImGui::GetWindowPos(), ImGui::GetWindowSize(), panelStyle); + + drawControlPanelHeader(panelStyle); + ImGui::Spacing(); + drawControlPanelToggles(panelStyle); + ImGui::Separator(); + drawControlPanelTabs(panelStyle); + + ImGui::End(); + nbl::ui::popControlPanelWindowStyle(); } - diff --git a/61_UI/AppControlPanelCameraTab.cpp b/61_UI/AppControlPanelCameraTab.cpp new file mode 100644 index 000000000..3ad0fe1a3 --- /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::beginControlPanelTabChild("CameraPanel", panelStyle)) + { + nbl::ui::endControlPanelTabChild(); + return; + } + + ImGui::PushItemWidth(-1.0f); + nbl::ui::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::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::drawSliderFloatWithHint(spec); + } + + nbl::ui::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::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::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::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::drawSliderFloatWithHint(spec); + } + + nbl::ui::drawSectionHeader("OrbitHeader", "Orbit Target", panelStyle.AccentColor, panelStyle); + auto* activeCamera = getActiveCamera(); + ICamera::SphericalTargetState orbitState; + const bool hasOrbitTarget = activeCamera && activeCamera->tryGetSphericalTargetState(orbitState); + if (hasOrbitTarget) + { + auto target = getCastedVector(orbitState.target); + if (ImGui::InputFloat3("Target", &target[0])) + activeCamera->trySetSphericalTarget(getCastedVector(target)); + + if (nbl::ui::drawActionButtonWithHint("Target model", "Set orbit target to the model position")) + { + const auto targetPos = hlsl::transpose(getMatrix3x4As4x4(m_sceneInteraction.model))[3]; + activeCamera->trySetSphericalTarget(float64_t3(targetPos.x, targetPos.y, targetPos.z)); + } + ImGui::SameLine(); + if (nbl::ui::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::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::drawCheckboxWithHint({ .label = "Enable follow", .value = &followConfig.enabled, .hint = "Apply tracked-target follow to the active planar camera" }); + + const char* followModeLabels[] = { + getCameraFollowModeLabel(ECameraFollowMode::Disabled), + getCameraFollowModeLabel(ECameraFollowMode::OrbitTarget), + getCameraFollowModeLabel(ECameraFollowMode::LookAtTarget), + getCameraFollowModeLabel(ECameraFollowMode::KeepWorldOffset), + 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::cameraFollowModeUsesCapturedOffset(followConfig.mode)) + captureFollowOffsetsForPlanar(getActivePlanarIx()); + if (followStateChanged && followConfig.enabled) + applyFollowToConfiguredCameras(); + + auto trackedTarget = getCastedVector(m_sceneInteraction.followTarget.getGimbal().getPosition()); + if (ImGui::InputFloat3("Tracked target", &trackedTarget[0])) + m_sceneInteraction.followTarget.setPosition(getCastedVector(trackedTarget)); + + nbl::ui::drawCheckboxWithHint({ .label = "Show target marker", .value = &m_sceneInteraction.followTargetVisible, .hint = "Render the tracked target marker in the scene" }); + + if (nbl::ui::drawActionButtonWithHint("Reset target", "Reset tracked target gimbal to the default world-space follow pose")) + resetFollowTargetToDefault(); + ImGui::SameLine(); + if (nbl::ui::drawActionButtonWithHint("Snap to model", "Optionally snap tracked target gimbal to the model transform")) + snapFollowTargetToModel(); + ImGui::SameLine(); + if (nbl::ui::drawActionButtonWithHint("Target origin", "Reset tracked target to identity at world origin")) + m_sceneInteraction.followTarget.setPose(float64_t3(0.0), makeIdentityQuaternion()); + ImGui::SameLine(); + if (nbl::ui::drawActionButtonWithHint("Capture current offset", "Store current camera-to-target relation into the active follow config")) + captureFollowOffsetsForPlanar(getActivePlanarIx()); + + if (cameraFollowModeUsesWorldOffset(followConfig.mode)) + { + auto worldOffset = getCastedVector(followConfig.worldOffset); + if (ImGui::InputFloat3("World offset", &worldOffset[0])) + followConfig.worldOffset = getCastedVector(worldOffset); + } + if (cameraFollowModeUsesLocalOffset(followConfig.mode)) + { + auto localOffset = getCastedVector(followConfig.localOffset); + if (ImGui::InputFloat3("Local offset", &localOffset[0])) + followConfig.localOffset = getCastedVector(localOffset); + } + } + else + { + ImGui::TextDisabled("No active follow config."); + } + + ImGui::PopItemWidth(); + nbl::ui::endControlPanelTabChild(); +} diff --git a/61_UI/AppControlPanelPlaybackTab.cpp b/61_UI/AppControlPanelPlaybackTab.cpp new file mode 100644 index 000000000..058a4d438 --- /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::beginControlPanelTabChild("PlaybackPanel", panelStyle)) + { + nbl::ui::endControlPanelTabChild(); + return; + } + + ImGui::PushItemWidth(-1.0f); + auto* activeCamera = getActiveCamera(); + nbl::ui::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::drawCheckboxWithHint(spec); + } + nbl::ui::drawSliderFloatWithHint({ + .label = "Speed", + .value = &playbackAuthoring.playback.speed, + .minValue = SCameraAppAuthoringDefaults::PlaybackSpeedMin, + .maxValue = SCameraAppAuthoringDefaults::PlaybackSpeedMax, + .format = "%.2f", + .hint = "Playback speed multiplier" + }); + + if (nbl::ui::drawActionButtonWithHint(playbackAuthoring.playback.playing ? "Pause" : "Play", "Start or pause playback")) + playbackAuthoring.playback.playing = !playbackAuthoring.playback.playing; + ImGui::SameLine(); + if (nbl::ui::drawActionButtonWithHint("Stop", "Stop playback and reset time")) + { + nbl::core::resetPlaybackCursor(playbackAuthoring.playback); + applyPlaybackAtTime(playbackAuthoring.playback.time); + } + + if (!playbackAuthoring.keyframeTrack.keyframes.empty()) + { + const float duration = nbl::core::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::drawPolicyStatus({ + .label = "Preview", + .value = playbackPreviewUi.policyLabel, + .active = playbackPreviewUi.canApply + }, panelStyle); + } + } + + nbl::ui::drawSectionHeader("KeyframesHeader", "Keyframes", panelStyle.AccentColor, panelStyle); + ImGui::InputFloat("New keyframe time", &playbackAuthoring.newKeyframeTime, SCameraAppAuthoringDefaults::KeyframeTimeStep, SCameraAppAuthoringDefaults::KeyframeTimeFastStep, "%.3f"); + nbl::ui::drawHoverHint("Time value for new keyframe"); + ImGui::SameLine(); + if (nbl::ui::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::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::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::drawPolicyStatus({ + .label = "Capture", + .value = keyframeCaptureUi.policyLabel, + .active = keyframeCaptureUi.canCapture + }, panelStyle); + ImGui::SameLine(); + if (nbl::ui::drawActionButtonWithHint("Clear keyframes", "Remove all keyframes")) + { + playbackAuthoring.keyframeTrack = {}; + nbl::core::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::drawHoverHint("Edit selected keyframe time"); + + nbl::ui::drawGoalApplyPresentationSummary(keyframeUi, panelStyle); + + if (!keyframeUi.canApply) + ImGui::BeginDisabled(); + if (nbl::ui::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::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::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::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::drawSectionHeader("KeyframesStorageHeader", "Keyframe Storage", panelStyle.AccentColor, panelStyle); + nbl::ui::inputTextString("Keyframe file", playbackAuthoring.keyframePath); + if (nbl::ui::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::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::endControlPanelTabChild(); +} diff --git a/61_UI/AppControlPanelPresetsTab.cpp b/61_UI/AppControlPanelPresetsTab.cpp new file mode 100644 index 000000000..c1c3a56e2 --- /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::beginControlPanelTabChild("PresetsPanel", panelStyle)) + { + nbl::ui::endControlPanelTabChild(); + return; + } + + ImGui::PushItemWidth(-1.0f); + nbl::ui::drawSectionHeader("PresetsHeader", "Presets", panelStyle.AccentColor, panelStyle); + nbl::ui::inputTextString("Preset name", presetAuthoring.presetName); + auto* activeCamera = getActiveCamera(); + const auto presetCaptureUi = analyzeCameraCaptureForUi(activeCamera); + if (!presetCaptureUi.canCapture) + ImGui::BeginDisabled(); + if (nbl::ui::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::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::drawActionButtonWithHint("Clear presets", "Remove all presets")) + { + presetAuthoring.presets.clear(); + presetAuthoring.selectedPresetIx = -1; + } + nbl::ui::drawPolicyStatus({ + .label = "Capture", + .value = presetCaptureUi.policyLabel, + .active = presetCaptureUi.canCapture + }, panelStyle); + + if (!presetAuthoring.presets.empty()) + { + const char* presetFilterLabels[] = { + nbl::ui::getPresetApplyPresentationFilterLabel(PresetFilterMode::All), + nbl::ui::getPresetApplyPresentationFilterLabel(PresetFilterMode::Exact), + nbl::ui::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::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::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::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::drawSectionHeader("PresetsStorageHeader", "Storage", panelStyle.AccentColor, panelStyle); + nbl::ui::inputTextString("Preset file", presetAuthoring.presetPath); + if (nbl::ui::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::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::endControlPanelTabChild(); +} diff --git a/61_UI/AppControlPanelProjectionTab.cpp b/61_UI/AppControlPanelProjectionTab.cpp new file mode 100644 index 000000000..5f7df8fe3 --- /dev/null +++ b/61_UI/AppControlPanelProjectionTab.cpp @@ -0,0 +1,85 @@ +#include "app/App.hpp" +#include "app/AppProjectionControlPanelUiUtilities.hpp" + +void App::drawControlPanelProjectionTab(const nbl::ui::SCameraControlPanelStyle& panelStyle) +{ + if (!nbl::ui::beginControlPanelTabChild("ProjectionPanel", panelStyle)) + { + nbl::ui::endControlPanelTabChild(); + return; + } + + ImGui::PushItemWidth(-1.0f); + SActiveProjectionTabContext runtime = {}; + if (!tryBuildActiveProjectionTabContext(runtime)) + { + ImGui::TextDisabled("No active viewport."); + ImGui::PopItemWidth(); + nbl::ui::endControlPanelTabChild(); + return; + } + + nbl::ui::drawSectionHeader("PlanarSelectHeader", "Planar Selection", panelStyle.AccentColor, panelStyle); + ImGui::Text("Active Render Window: %s", runtime.activeRenderWindowIxString.c_str()); + nbl::ui::drawHoverHint("Window that receives input and camera switching"); + + auto refreshRuntime = [&]() -> bool + { + return tryBuildActiveProjectionTabContext(runtime); + }; + + assert(!m_planarProjections.empty()); + auto& binding = runtime.requireBinding(); + if (!nbl::ui::drawProjectionPlanarSelector(getPlanarProjectionSpan(), runtime, refreshRuntime)) + { + ImGui::PopItemWidth(); + nbl::ui::endControlPanelTabChild(); + return; + } + nbl::ui::drawHoverHint("Select which camera the window renders"); + + assert(binding.boundProjectionIx.has_value()); + assert(binding.lastBoundPerspectivePresetProjectionIx.has_value()); + assert(binding.lastBoundOrthoPresetProjectionIx.has_value()); + + nbl::ui::drawSectionHeader("ProjectionParamsHeader", "Projection Parameters", panelStyle.AccentColor, panelStyle); + if (!nbl::ui::drawProjectionTypeSelector(getPlanarProjectionSpan(), runtime, refreshRuntime)) + { + ImGui::PopItemWidth(); + nbl::ui::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::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::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::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::endControlPanelTabChild(); +} diff --git a/61_UI/AppControlPanelStatusTab.cpp b/61_UI/AppControlPanelStatusTab.cpp new file mode 100644 index 000000000..bf4b6338d --- /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::beginControlPanelTabChild("StatusPanel", panelStyle)) + { + nbl::ui::endControlPanelTabChild(); + return; + } + + ImGui::PushItemWidth(-1.0f); + nbl::ui::drawSectionHeader("SessionHeader", "Session", panelStyle.AccentColor, panelStyle); + if (nbl::ui::beginCard("SessionCard", nbl::ui::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::drawStatusLine(row, panelStyle); + ImGui::EndTable(); + } + } + nbl::ui::endCard(); + + nbl::ui::drawSectionHeader("CameraHeader", "Camera", panelStyle.AccentColor, panelStyle); + if (auto* activeCamera = getActiveCamera()) + { + const auto& gimbal = activeCamera->getGimbal(); + const auto pos = gimbal.getPosition(); + const auto euler = hlsl::getCameraOrientationEulerDegrees(gimbal.getOrientation()); + + if (nbl::ui::beginCard("CameraCard", nbl::ui::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::drawStatusLine(row, panelStyle); + ImGui::EndTable(); + } + } + nbl::ui::endCard(); + } + else if (nbl::ui::beginCard("CameraCard", nbl::ui::calcCameraControlPanelCardHeight(2, panelStyle), panelStyle.CardTopColor, panelStyle.CardBottomColor, panelStyle.CardBorderColor, panelStyle)) + { + ImGui::TextDisabled("No active camera"); + nbl::ui::endCard(); + } + + nbl::ui::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::beginCard("ProjectionCard", nbl::ui::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::drawStatusLine({ .label = "Type", .value = typeText, .dotColor = panelStyle.AccentColor, .valueColor = panelStyle.MutedColor }, panelStyle); + nbl::ui::drawStatusLine({ .label = "zNear", .value = zNearText, .dotColor = panelStyle.MutedColor, .valueColor = panelStyle.MutedColor }, panelStyle); + nbl::ui::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::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::drawStatusLine({ .label = "Ortho width", .value = orthoWidthText, .dotColor = panelStyle.MutedColor, .valueColor = panelStyle.MutedColor }, panelStyle); + } + ImGui::EndTable(); + } + } + nbl::ui::endCard(); + } + else if (nbl::ui::beginCard("ProjectionCard", nbl::ui::calcCameraControlPanelCardHeight(2, panelStyle), panelStyle.CardTopColor, panelStyle.CardBottomColor, panelStyle.CardBorderColor, panelStyle)) + { + ImGui::TextDisabled("No projection bound"); + nbl::ui::endCard(); + } + + ImGui::PopItemWidth(); + nbl::ui::endControlPanelTabChild(); +} diff --git a/61_UI/AppControlPanelTabs.cpp b/61_UI/AppControlPanelTabs.cpp new file mode 100644 index 000000000..f81483895 --- /dev/null +++ b/61_UI/AppControlPanelTabs.cpp @@ -0,0 +1,203 @@ +#include "app/App.hpp" + +#include +#include +#include + +namespace +{ + +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::calcPillWidth(toggles[toggleIx].label, panelStyle.TogglePadding); + } + return rowWidth; +} + +} // namespace + +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::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::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::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::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::drawMiniStat(miniStats[1], m_uiMetrics.inputCounts, m_uiMetrics.sampleIndex, [&] + { + ImGui::TextColored(panelStyle.AccentColor, "%u ev", m_uiMetrics.lastInputEvents); + }, panelStyle); + + ImGui::TableSetColumnIndex(2); + nbl::ui::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::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::drawTogglePill( + toggle.label, + getToggleValue(toggle.binding), + panelStyle.AccentColor, + panelStyle.InactiveBadgeColor, + panelStyle.BadgeTextColor, + panelStyle.TogglePadding); + nbl::ui::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..89993fc07 --- /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::beginControlPanelTabChild("GizmoPanel", panelStyle)) + { + nbl::ui::endControlPanelTabChild(); + return; + } + + nbl::ui::drawSectionHeader("GizmoHeader", "Gizmo", panelStyle.AccentColor, panelStyle); + TransformEditorContents(); + nbl::ui::endControlPanelTabChild(); +} + +void App::drawControlPanelLogTab(const nbl::ui::SCameraControlPanelStyle& panelStyle) +{ + auto& eventLog = m_eventLog; + + if (!nbl::ui::beginControlPanelTabChild("LogPanel", panelStyle)) + { + nbl::ui::endControlPanelTabChild(); + return; + } + + nbl::ui::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::endControlPanelTabChild(); +} diff --git a/61_UI/AppDebugSceneRendererResources.cpp b/61_UI/AppDebugSceneRendererResources.cpp new file mode 100644 index 000000000..3a4be7cd6 --- /dev/null +++ b/61_UI/AppDebugSceneRendererResources.cpp @@ -0,0 +1,69 @@ +#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 asset::SPushConstantRange singlePcRange = { + .stageFlags = IShader::E_SHADER_STAGE::ESS_VERTEX, + .offset = offsetof(ext::frustum::PushConstants, spc), + .size = sizeof(ext::frustum::SSinglePC) + }; + + ext::frustum::CDrawFrustum::SCreationParameters frustumParams = {}; + frustumParams.transfer = getTransferUpQueue(); + frustumParams.assetManager = m_assetMgr; + frustumParams.drawMode = ext::frustum::CDrawFrustum::DrawMode::DM_SINGLE; + frustumParams.singlePipelineLayout = ext::frustum::CDrawFrustum::createPipelineLayoutFromPCRange(m_device.get(), singlePcRange); + frustumParams.renderpass = core::smart_refctd_ptr(m_debugScene.renderpass); + frustumParams.utilities = m_utils; + m_debugScene.frustumDrawer = ext::frustum::CDrawFrustum::create(std::move(frustumParams)); + if (!m_debugScene.frustumDrawer) + return logFail("Failed to create frustum drawer."); + + 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..80e5f1e57 --- /dev/null +++ b/61_UI/AppFollowRuntime.cpp @@ -0,0 +1,290 @@ +#include "app/App.hpp" + +namespace +{ + +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 bool ensureScriptVisualDebugCamera( + const std::vector>& planarProjections, + std::span windowBindings, + const uint32_t activeRenderWindowIx, + SWindowControlBinding*& outBinding, + ICamera*& outCamera) +{ + outBinding = nullptr; + outCamera = nullptr; + + SActiveViewportRuntimeState viewportState = {}; + if (!nbl::ui::tryBuildActiveViewportRuntimeState(planarProjections, windowBindings, activeRenderWindowIx, viewportState)) + return false; + + outBinding = viewportState.binding; + outCamera = viewportState.camera; + return true; +} + +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 = getCameraTypeLabel(&camera); + status.cameraHint = 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::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; +} + +} // namespace + +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::captureFollowOffsetsFromCamera( + m_cameraGoalSolver, + camera, + m_sceneInteraction.followTarget, + m_sceneInteraction.planarFollowConfigs[planarIx]); +} + +bool App::followConfigUsesCapturedOffset(const SCameraFollowConfig& config) const +{ + return config.enabled && nbl::core::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::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(getMatrix3x4As4x4(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::applyFollowToCamera(m_cameraGoalSolver, camera, m_sceneInteraction.followTarget, config); + if (!result.succeeded()) + continue; + + for (auto& projection : planar->getPlanarProjections()) + nbl::core::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; + + SWindowControlBinding* activeBinding = nullptr; + ICamera* camera = nullptr; + if (!ensureScriptVisualDebugCamera( + m_planarProjections, + std::span(m_viewports.windowBindings.data(), m_viewports.windowBindings.size()), + m_viewports.activeRenderWindowIx, + activeBinding, + camera)) + return; + + if (!m_scriptedInput.visualPlanar.valid) + { + m_scriptedInput.visualPlanar.valid = true; + m_scriptedInput.visualPlanar.planarIx = activeBinding->activePlanarIx; + m_scriptedInput.visualPlanar.startFrame = m_realFrameIx; + } + + const auto debugStatus = buildScriptVisualDebugStatus( + *camera, + activeBinding->activePlanarIx, + m_planarProjections.size(), + m_realFrameIx, + m_scriptedInput); + + nbl::ui::drawScriptVisualDebugOverlay(displaySize, nbl::ui::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..f95c8d0aa --- /dev/null +++ b/61_UI/AppHeadlessCameraSmoke.cpp @@ -0,0 +1,118 @@ +#include "app/App.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "app/AppCameraConfigUtilities.hpp" +#include "app/AppResourceUtilities.hpp" +#include "camera/CCameraPathUtilities.hpp" +#include "camera/CCameraPersistence.hpp" +#include "camera/CCameraScriptedRuntimePersistence.hpp" +#include "camera/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, + .orbitCamera = cameraInventory.orbit, + .freeCamera = cameraInventory.free, + .chaseCamera = cameraInventory.chase, + .dollyCamera = cameraInventory.dolly, + .dollyZoomCamera = cameraInventory.dollyZoom + }; + + if (!runSmokeStep([&](std::string& smokeError) + { + return verifyFollowSmoke( + resolvedSmokeState, + { cameras.data(), cameras.size() }, + { smokePlanars.data(), smokePlanars.size() }, + [](ICamera* camera) { return nbl::core::makeDefaultFollowConfig(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..ca4023a27 --- /dev/null +++ b/61_UI/AppHeadlessCameraSmokeChecks.inl @@ -0,0 +1,956 @@ + struct SCameraSmokeResolvedState final + { + const CCameraGoalSolver& goalSolver; + nbl::system::ISystem* system = nullptr; + const SCameraSmokePresetInventory& initialPresets; + ICamera* orbitCamera = nullptr; + ICamera* freeCamera = nullptr; + ICamera* chaseCamera = nullptr; + ICamera* dollyCamera = 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::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::MissingDynamicPerspectiveState, + "DollyZoom->Orbit", + outError)) + { + return false; + } + } + + if (!state.initialPresets.orbit.has_value()) + return true; + + if (std::string_view(nbl::ui::getPresetApplyPresentationFilterLabel(EPresetApplyPresentationFilter::All)) != "All" || + std::string_view(nbl::ui::getPresetApplyPresentationFilterLabel(EPresetApplyPresentationFilter::Exact)) != "Exact" || + std::string_view(nbl::ui::getPresetApplyPresentationFilterLabel(EPresetApplyPresentationFilter::BestEffort)) != "Best-effort") + { + outError = "Presentation utilities smoke returned an unexpected filter label."; + return false; + } + + const auto blockedPresentation = nbl::ui::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::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::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::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::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::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::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 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()); + + std::stringstream presetBuffer; + if (!nbl::system::writePresetCollection(presetBuffer, sourcePresetSpan)) + { + outError = "Preset persistence smoke failed to serialize preset collection."; + return false; + } + + std::vector loadedPresets; + if (!nbl::system::readPresetCollection(presetBuffer, loadedPresets)) + { + outError = "Preset persistence smoke failed to deserialize preset collection."; + return false; + } + if (!nbl::core::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; + + std::stringstream keyframeBuffer; + if (!nbl::system::writeKeyframeTrack(keyframeBuffer, sourceTrack)) + { + outError = "Keyframe persistence smoke failed to serialize track."; + return false; + } + + CCameraKeyframeTrack loadedTrack; + if (!nbl::system::readKeyframeTrack(keyframeBuffer, loadedTrack)) + { + outError = "Keyframe persistence smoke failed to deserialize track."; + return false; + } + if (!nbl::system::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::savePresetCollectionToFile(system, presetFile, sourcePresetSpan)) + { + outError = "Preset persistence smoke failed to save preset collection file."; + return false; + } + + std::vector fileLoadedPresets; + if (!nbl::system::loadPresetCollectionFromFile(system, presetFile, fileLoadedPresets)) + { + outError = "Preset persistence smoke failed to load preset collection file."; + return false; + } + if (!nbl::core::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::saveKeyframeTrackToFile(system, keyframeFile, sourceTrack)) + { + outError = "Keyframe persistence smoke failed to save track file."; + return false; + } + + CCameraKeyframeTrack fileLoadedTrack; + if (!nbl::system::loadKeyframeTrackFromFile(system, keyframeFile, fileLoadedTrack)) + { + outError = "Keyframe persistence smoke failed to load track file."; + return false; + } + if (!nbl::system::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::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::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::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::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::sequenceScriptUsesMultiplePresentations(sequence)) + { + outError = "Sequence compile smoke failed to detect multi-presentation authored defaults."; + return false; + } + + const CCameraSequenceTrackedTargetPose referenceTrackedTargetPose = { + .position = SCameraAppSceneDefaults::DefaultFollowTargetPosition, + .orientation = SCameraAppSceneDefaults::DefaultFollowTargetOrientation + }; + + nbl::core::CCameraSequenceCompiledSegment compiledSegment; + std::string compileError; + if (!nbl::core::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::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::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::string runtimeBuildError; + if (!nbl::system::appendCompiledSequenceSegmentToScriptedTimeline( + scriptedTimeline, + 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::finalizeScriptedTimeline(scriptedTimeline); + + 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; + CCameraScriptedFrameEvents runtimeBatch; + nbl::system::dequeueScriptedFrameEvents(scriptedTimeline.events, runtimeNextEventIndex, SCameraSmokeSequenceDefaults::StartFrame, runtimeBatch); + if (runtimeBatch.actions.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 (runtimeBatch.actions.front().kind != CCameraScriptedInputEvent::ActionData::Kind::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::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::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; + } + } + + { + 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::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; + } + } + + if (state.initialPresets.free.has_value() && state.freeCamera) + { + CameraPreset orientedPreset = state.initialPresets.free.value(); + orientedPreset.goal.orientation = hlsl::makeQuaternionFromEulerDegreesYXZ(SCameraSmokeManipulationDefaults::FreeOrientationYawDeg); + const auto orientResult = nbl::core::applyPresetDetailed(state.goalSolver, state.freeCamera, orientedPreset); + if (!orientResult.succeeded() || !nbl::system::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::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::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::makeQuaternionFromEulerDegreesYXZ(SCameraSmokeManipulationDefaults::FreePitchClampSourceDeg); + const auto pitchResult = nbl::core::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::core::applyCameraConstraints(state.goalSolver, state.freeCamera, freeConstraints)) + { + outError = "Camera manipulation utilities smoke failed to clamp Free camera orientation."; + return false; + } + + const auto freeEulerDeg = hlsl::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::applyPresetDetailed(state.goalSolver, state.freeCamera, state.initialPresets.free.value()); + if (!restoreFree.succeeded() || !nbl::system::comparePresetToCameraStateWithStrictThresholds(state.goalSolver, state.freeCamera, state.initialPresets.free.value())) + { + outError = "Camera manipulation utilities smoke failed to restore Free camera baseline."; + 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::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::core::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::applyPresetDetailed(state.goalSolver, state.orbitCamera, state.initialPresets.orbit.value()); + if (!restoreOrbit.succeeded() || !nbl::system::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::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::syncDynamicPerspectiveProjection(state.dollyZoomCamera, orthographicProjection)) + { + outError = "Camera projection utilities smoke unexpectedly synced orthographic projection."; + return false; + } + } + + if (getCameraTypeLabel(ICamera::CameraKind::DollyZoom) != "Dolly Zoom") + { + outError = "Camera text utilities smoke failed for Dolly Zoom label."; + return false; + } + if (getCameraTypeDescription(ICamera::CameraKind::Path) != std::string(nbl::core::SCameraPathDefaults::Description)) + { + outError = "Camera text utilities smoke failed for Path description."; + return false; + } + if (describeGoalStateMask(ICamera::GoalStateNone) != "Pose only") + { + outError = "Camera text utilities smoke failed for empty goal-state description."; + return false; + } + if (describeGoalStateMask(ICamera::GoalStateSphericalTarget | ICamera::GoalStateDynamicPerspective) != "Spherical target, Dynamic perspective") + { + outError = "Camera text utilities smoke failed for combined goal-state description."; + return false; + } + + CCameraGoalSolver::SApplyResult defaultApplyResult; + const auto applyResultText = 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::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::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 = nbl::core::makeDefaultFollowConfig(defaultFollowCamera); + if (!followConfig.enabled || followConfig.mode == ECameraFollowMode::Disabled) + continue; + + const auto label = std::string(defaultFollowCamera->getIdentifier()) + " default follow"; + const auto baselinePreset = nbl::core::capturePreset(state.goalSolver, defaultFollowCamera, label + " baseline"); + + trackedTarget.setPose( + SCameraSmokeFollowScenario::InitialTargetPosition, + SCameraSmokeFollowScenario::InitialTargetOrientation); + if ((nbl::core::cameraFollowModeUsesLocalOffset(followConfig.mode) || nbl::core::cameraFollowModeUsesWorldOffset(followConfig.mode)) && + !nbl::core::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::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::capturePreset(state.goalSolver, state.chaseCamera, "chase-follow-baseline"); + SCameraFollowConfig followConfig = {}; + followConfig.enabled = true; + followConfig.mode = ECameraFollowMode::KeepLocalOffset; + if (!nbl::core::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..d96fb0abd --- /dev/null +++ b/61_UI/AppHeadlessCameraSmokeHelpers.inl @@ -0,0 +1,1443 @@ +namespace +{ + 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 = + 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 = + 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 = + 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* orbit = nullptr; + ICamera* free = nullptr; + ICamera* chase = nullptr; + ICamera* dolly = 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::MissingPathState: + return ICamera::GoalStatePath; + case CCameraGoalSolver::SApplyResult::MissingDynamicPerspectiveState: + return ICamera::GoalStateDynamicPerspective; + case CCameraGoalSolver::SApplyResult::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 { + .orbit = findCameraByKind(cameras, ICamera::CameraKind::Orbit), + .free = findCameraByKind(cameras, ICamera::CameraKind::Free), + .chase = findCameraByKind(cameras, ICamera::CameraKind::Chase), + .dolly = findCameraByKind(cameras, ICamera::CameraKind::Dolly), + .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::comparePresetToCameraStateWithDefaultThresholds(goalSolver, camera, preset); + case EPresetComparePolicy::StrictThresholds: + return nbl::system::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::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::applyPresetDetailed(goalSolver, camera, preset); + if (!applyResult.succeeded() || + (requireChanged && !applyResult.changed()) || + (requireExact && !applyResult.exact)) + { + outError = std::string(failurePrefix) + ". " + 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::applyPresetDetailed(goalSolver, camera, preset); + if (restoreResult.succeeded() && nbl::system::comparePresetToCameraStateWithStrictThresholds(goalSolver, camera, preset)) + return true; + + outError = std::string(failurePrefix) + ". " + describeApplyResult(restoreResult); + if (camera) + outError += " " + nbl::core::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::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::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::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::getMatrix3x4As4x4(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 contract 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; + applyDefaultCameraInputBindingPreset(inputBinder, *camera); + + const std::string cameraIdentifier(camera->getIdentifier()); + const auto initialPreset = nbl::core::capturePreset(goalSolver, camera, "smoke-initial"); + const auto initialCompatibility = nbl::core::analyzePresetApply(goalSolver, camera, initialPreset).compatibility; + if (!initialCompatibility.exact || initialCompatibility.missingGoalStateMask != ICamera::GoalStateNone) + { + outError = "Preset compatibility smoke failed for camera \"" + cameraIdentifier + + "\". missing=" + describeGoalStateMask(initialCompatibility.missingGoalStateMask); + return false; + } + + storeInitialPresetForKind(camera->getKind(), initialPreset, initialPresets); + + if (!nbl::core::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::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::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::tryManipulateCameraAndMeasureDelta(camera, { directEvents.data(), directEvents.size() }, directDelta, CameraTinyScalarEpsilon)) + { + outError = "Direct manipulate smoke failed for camera \"" + cameraIdentifier + "\"."; + return false; + } + + { + const auto modifiedPreset = nbl::core::capturePreset(goalSolver, camera, "smoke-direct"); + 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, + true, + 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) + { + applyDefaultCameraInputBindingPreset(inputBinder, *camera); + auto keyboardEvents = collectKeyboardVirtualEvents(inputBinder, key); + if (keyboardEvents.empty()) + continue; + if (nbl::system::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 = getDefaultCameraMouseMappingPreset(*camera); + const bool hasMoveMapping = nbl::ui::hasMouseRelativeMovementBinding(mousePreset); + const bool hasScrollMapping = nbl::ui::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; + } + + 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::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); + + 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::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 uint32_t expectedMissingGoalStateMask = expectedMissingGoalStateMaskForIssue(expectedIssue); + const auto compatibility = nbl::core::analyzePresetApply(goalSolver, targetCamera, sourcePreset).compatibility; + if (compatibility.exact || compatibility.missingGoalStateMask != expectedMissingGoalStateMask) + { + outError = std::string("Cross-kind preset compatibility smoke failed for ") + label + + ". missing=" + describeGoalStateMask(compatibility.missingGoalStateMask); + return false; + } + + const auto baselinePreset = nbl::core::capturePreset(goalSolver, targetCamera, std::string(label) + "-baseline"); + const auto applyResult = nbl::core::applyPresetDetailed(goalSolver, targetCamera, sourcePreset); + if (!applyResult.succeeded() || !applyResult.approximate() || !applyResult.hasIssue(expectedIssue)) + { + outError = std::string("Cross-kind preset smoke failed for ") + label + ". " + 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::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=" + describeGoalStateMask(compatibility.missingGoalStateMask); + return false; + } + + const auto baselinePreset = nbl::core::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::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::validateFollowTargetContract( + camera, + trackedTarget, + followConfig, + followGoal, + regression, + ®ressionError, + hasProjectionContext ? &projectionContext : nullptr, + CameraFollowRegressionThresholds)) + { + return true; + } + + outError = std::string("Follow smoke contract 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(getMatrix3x4As4x4(markerWorld)); + const auto markerPosition = getCastedVector(float32_t3(markerTransform[3])); + const auto positionDelta = markerPosition - trackedTarget.getGimbal().getPosition(); + const auto errorLength = length(positionDelta); + if (hlsl::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::capturePreset(goalSolver, camera, std::string(label) + " baseline"); + SCameraFollowConfig followConfig = {}; + followConfig.enabled = true; + followConfig.mode = ECameraFollowMode::KeepLocalOffset; + + if (!nbl::core::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::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::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.orbitU = hlsl::wrapAngleRad( + editedPreset.goal.orbitU + hlsl::radians(SCameraSmokeFollowScenario::OrbitRecaptureDeltaDeg)); + editedPreset.goal.orbitDistance = std::clamp( + editedPreset.goal.orbitDistance + SCameraSmokeFollowScenario::OrbitRecaptureDistanceDelta, + CSphericalTargetCamera::MinDistance, + CSphericalTargetCamera::MaxDistance); + editedPreset.goal = nbl::core::canonicalizeGoal(editedPreset.goal); + if (!nbl::core::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::applyPresetDetailed(goalSolver, camera, editedPreset); + if (!editedApply.succeeded() || !editedApply.changed()) + { + outError = std::string("Follow recapture smoke failed to apply edited preset for ") + std::string(label) + + ". " + describeApplyResult(editedApply); + return false; + } + + const auto reachedEditedPreset = nbl::core::capturePreset(goalSolver, camera, std::string(label) + " reached"); + + if (!nbl::core::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::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::applyFollowToCamera(goalSolver, camera, trackedTarget, followConfig); + if (!recapturedApply.succeeded()) + { + outError = std::string("Follow recapture smoke failed to apply recaptured follow for ") + std::string(label) + + ". " + describeApplyResult(recapturedApply); + return false; + } + + if (!nbl::system::comparePresetToCameraStateWithStrictThresholds(goalSolver, camera, reachedEditedPreset)) + { + outError = std::string("Follow recapture smoke mismatch for ") + std::string(label) + ". " + + nbl::core::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 = {}; + nbl::system::appendScriptedActionEvent( + timeline, + SCameraSmokeRuntimeDefaults::ActionFrame, + CCameraScriptedInputEvent::ActionData::Kind::SetActivePlanar, + SCameraSmokeRuntimeDefaults::ActivePlanarValue); + { + CCameraGoal goal = {}; + goal.position = SCameraSmokeRuntimeDefaults::GoalPosition; + nbl::system::appendScriptedGoalEvent(timeline, SCameraSmokeRuntimeDefaults::ActionFrame, goal, true); + } + nbl::system::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::appendScriptedTrackedTargetTransformEvent(timeline, SCameraSmokeRuntimeDefaults::FollowFrame, transform); + } + + size_t nextEventIndex = 0u; + CCameraScriptedFrameEvents batch; + nbl::system::dequeueScriptedFrameEvents(timeline.events, nextEventIndex, SCameraSmokeRuntimeDefaults::ActionFrame, batch); + if (nextEventIndex != 3u || batch.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 (batch.actions.front().kind != CCameraScriptedInputEvent::ActionData::Kind::SetActivePlanar || + batch.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::dequeueScriptedFrameEvents(timeline.events, nextEventIndex, SCameraSmokeRuntimeDefaults::FollowFrame, batch); + if (nextEventIndex != timeline.events.size() || batch.trackedTargetTransforms.size() != 1u || + !batch.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::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::system::CCameraScriptedInputParseResult parsed; + std::string parseError; + const std::string scriptText = makeScriptedRuntimeParserSmokeJson().dump(); + if (!nbl::system::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() != 2u || 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; + CCameraScriptedFrameEvents batch; + nbl::system::dequeueScriptedFrameEvents(parsed.timeline.events, nextEventIndex, SCameraSmokeRuntimeParserDefaults::EventFrame, batch); + if (batch.actions.size() != 1u || + batch.keyboard.size() != 1u || + batch.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::appendScriptedBaselineCheck(timeline, SCameraSmokeScriptedCheckDefaults::BaselineFrame); + nbl::system::appendScriptedGimbalStepCheck( + timeline, + SCameraSmokeScriptedCheckDefaults::StepFrame, + true, + SCameraSmokeScriptedCheckDefaults::PositionTolerance, + SCameraSmokeScriptedCheckDefaults::MinPositionDelta, + true, + SCameraSmokeScriptedCheckDefaults::AngularToleranceDeg, + SCameraSmokeScriptedCheckDefaults::MinAngularDeltaDeg); + nbl::system::appendScriptedFollowTargetLockCheck( + timeline, + SCameraSmokeScriptedCheckDefaults::FollowLockFrame, + CameraFollowRegressionThresholds.lockAngleToleranceDeg, + CameraFollowRegressionThresholds.projectedNdcTolerance); + + CCameraScriptedCheckRuntimeState state = {}; + { + const auto frameResult = 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::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 = 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::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 = 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 = makeIdentityQuaternion(); + if (!nbl::hlsl::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 = getQuaternionBasisMatrix(desiredOrientation); + const auto desiredRight = desiredBasis[0]; + const auto desiredUp = desiredBasis[1]; + const auto goalRightVec = normalizeQuaternion(followGoal.orientation).transformVector(float64_t3(1.0, 0.0, 0.0), true); + const auto goalUpVec = normalizeQuaternion(followGoal.orientation).transformVector(float64_t3(0.0, 1.0, 0.0), true); + const auto goalForwardVec = normalizeQuaternion(followGoal.orientation).transformVector(float64_t3(0.0, 0.0, 1.0), true); + const auto goalBasis = getQuaternionBasisMatrix(followGoal.orientation); + float lockAngle = 0.0f; + double targetDistance = 0.0; + const bool hasLockMetrics = nbl::core::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 index d9573dedd..2d701435f 100644 --- a/61_UI/AppImGuiListen.cpp +++ b/61_UI/AppImGuiListen.cpp @@ -1,295 +1,22 @@ #include "app/App.hpp" -#include "app/AppViewportBindingUtilities.hpp" void App::imguiListen() { ImGuiIO& io = ImGui::GetIO(); - if (m_ciMode) - { + if (m_cliRuntime.ciMode) io.IniFilename = nullptr; - useWindow = true; - } ImGuizmo::BeginFrame(); - SImResourceInfo info; - info.samplerIx = static_cast(nbl::ext::imgui::UI::DefaultSamplerIx::USER); + auto info = SCameraAppUiTextureSlots::makeDefaultViewportResourceInfo(); - if (useWindow) + if (m_viewports.useWindow) drawWindowedViewportWindows(io, info); else drawFullscreenViewportWindow(io, info); drawScriptVisualDebugOverlay(io.DisplaySize); DrawControlPanel(); - UpdateBoundCameraMovement(); - UpdateCursorVisibility(); - applyFollowToConfiguredCameras(); - refreshViewportBindingMatrices(); -} - -void App::drawWindowedViewportWindows(ImGuiIO& io, SImResourceInfo& info) -{ - syncVisualDebugWindowBindings(); - const bool hideSceneGizmos = enableActiveCameraMovement || (m_scriptedInput.enabled && m_scriptedInput.visualDebug); - ImGuizmo::Enable(!hideSceneGizmos); - - size_t gizmoIx = 0u; - size_t manipulationCounter = 0u; - const std::optional manipulatedObjectIx = ImGuizmo::IsUsingAny() ? std::optional(getManipulatedObjectIx()) : std::nullopt; - (void)manipulatedObjectIx; - - for (uint32_t windowIx = 0u; windowIx < windowBindings.size(); ++windowIx) - { - { - const auto& rw = wInit.renderWindows[windowIx]; - const ImGuiCond windowCond = m_ciMode ? ImGuiCond_Always : ImGuiCond_Appearing; - ImGui::SetNextWindowPos({ rw.iPos.x, rw.iPos.y }, windowCond); - ImGui::SetNextWindowSize({ rw.iSize.x, rw.iSize.y }, windowCond); - } - ImGui::SetNextWindowSizeConstraints(ImVec2(0x45, 0x45), ImVec2(7680, 4320)); - - nbl::ui::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); - const ImVec2 contentRegionSize = ImGui::GetContentRegionAvail(); - const ImVec2 cursorPos = ImGui::GetCursorScreenPos(); - const nbl::ui::SViewportOverlayRect viewportRect = { cursorPos, contentRegionSize }; - - if (ImGuiWindow* const window = ImGui::GetCurrentWindow()) - { - const auto mousePos = ImGui::GetMousePos(); - const bool mouseInsideViewport = - mousePos.x >= cursorPos.x && - mousePos.y >= cursorPos.y && - mousePos.x <= cursorPos.x + contentRegionSize.x && - mousePos.y <= cursorPos.y + contentRegionSize.y; - if (mouseInsideViewport) - window->Flags |= ImGuiWindowFlags_NoMove; - else - window->Flags &= ~ImGuiWindowFlags_NoMove; - } - - auto& binding = windowBindings[windowIx]; - nbl::ui::SBoundViewportCameraState viewportState = {}; - const auto planarSpan = std::span>(m_planarProjections.data(), m_planarProjections.size()); - if (!nbl::ui::tryBuildViewportBoundCameraState(planarSpan, binding, contentRegionSize, flipGizmoY, viewportState)) - { - ImGui::End(); - nbl::ui::popViewportWindowStyle(); - continue; - } - - auto* const planarViewCameraBound = viewportState.camera; - auto& projection = *viewportState.projection; - info.textureID = windowIx + 1u; - - ImGuizmo::AllowAxisFlip(binding.allowGizmoAxesToFlip); - ImGuizmo::SetOrthographic(projection.getParameters().m_type == IPlanarProjection::CProjection::Orthographic); - ImGuizmo::SetDrawlist(); - ImGui::Image(info, contentRegionSize); - ImGuizmo::SetRect(cursorPos.x, cursorPos.y, contentRegionSize.x, contentRegionSize.y); - - if (auto* drawList = ImGui::GetWindowDrawList(); drawList) - { - const char* projLabel = 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(getCameraTypeLabel(planarViewCameraBound)) + ": " + std::string(getCameraTypeDescription(planarViewCameraBound)); - overlayData.detail = "Frustum: active camera (hidden in owner view)"; - nbl::ui::drawViewportInfoOverlay(*drawList, viewportRect, overlayData); - - if (m_scriptedInput.enabled && m_scriptedInput.visualDebug && m_scriptedInput.visualFollowActive) - nbl::ui::drawFollowTargetViewportOverlay(*drawList, viewportState.viewProjMatrix, m_followTarget, viewportRect); - } - - const bool windowHovered = ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem); - const bool windowFocused = ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows); - if (!(m_scriptedInput.enabled && m_scriptedInput.exclusive)) - { - if (!m_scriptedInput.enabled && windowHovered) - activeRenderWindowIx = windowIx; - else if (windowFocused) - activeRenderWindowIx = windowIx; - } - - if (!hideSceneGizmos) - { - for (uint32_t objectIx = 0u; objectIx < getManipulableObjectCount(); ++objectIx) - { - ImGuizmo::PushID(gizmoIx); - ++gizmoIx; - - const auto planarIx = getManipulableObjectPlanarIx(objectIx); - const bool isFollowTarget = isManipulableObjectFollowTarget(objectIx); - ICamera* const targetGimbalManipulationCamera = planarIx.has_value() ? m_planarProjections[planarIx.value()]->getCamera() : nullptr; - if (targetGimbalManipulationCamera == planarViewCameraBound) - { - ImGuizmo::PopID(); - continue; - } - - ImGuizmoModelM16InOut imguizmoModel; - imguizmoModel.inTRS = getManipulableObjectTransform(objectIx); - - const float gizmoWorldRadius = isFollowTarget ? 0.35f : 0.22f; - const auto gizmoWorldPos = getManipulableObjectWorldPosition(objectIx); - const auto viewPos = mul(viewportState.viewMatrix, float32_t4(gizmoWorldPos, 1.0f)); - const float depth = std::max(0.001f, std::abs(viewPos.z)); - float gizmoSizeClip = 0.1f; - if (projection.getParameters().m_type == IPlanarProjection::CProjection::Perspective) - gizmoSizeClip = (gizmoWorldRadius * viewportState.projectionMatrix[1][1]) / depth; - else - gizmoSizeClip = gizmoWorldRadius * viewportState.projectionMatrix[1][1]; - ImGuizmo::SetGizmoSizeClipSpace(gizmoSizeClip); - - imguizmoModel.outTRS = imguizmoModel.inTRS; - const bool success = ImGuizmo::Manipulate( - &viewportState.imguizmoPlanar.view[0][0], - &viewportState.imguizmoPlanar.projection[0][0], - ImGuizmo::OPERATION::UNIVERSAL, - mCurrentGizmoMode, - &imguizmoModel.outTRS[0][0], - &imguizmoModel.outDeltaTRS[0][0], - useSnap ? &snap[0] : nullptr); - - if (success) - { - if (targetGimbalManipulationCamera) - { - const auto referenceFrame = getCastedMatrix(imguizmoModel.outTRS); - bindManipulatedCamera(planarIx.value()); - nbl::core::applyReferenceFrameToCamera(targetGimbalManipulationCamera, referenceFrame); - refreshFollowOffsetConfigForPlanar(planarIx.value()); - } - else if (isFollowTarget) - { - setFollowTargetTransform(getCastedMatrix(imguizmoModel.outTRS)); - bindManipulatedFollowTarget(); - applyFollowToConfiguredCameras(); - } - else - { - m_model = float32_t3x4(hlsl::transpose(imguizmoModel.outTRS)); - bindManipulatedModel(); - } - } - - if (ImGuizmo::IsOver() && !ImGuizmo::IsUsingAny() && !enableActiveCameraMovement) - { - if (targetGimbalManipulationCamera && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) - { - const uint32_t newPlanarIx = planarIx.value(); - if (newPlanarIx < m_planarProjections.size()) - { - binding.activePlanarIx = newPlanarIx; - binding.pickDefaultProjections(m_planarProjections[binding.activePlanarIx]->getPlanarProjections()); - if (!(m_scriptedInput.enabled && m_scriptedInput.exclusive)) - activeRenderWindowIx = windowIx; - } - } - else if (isFollowTarget && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) - { - bindManipulatedFollowTarget(); - } - else if (!targetGimbalManipulationCamera && !isFollowTarget && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) - { - bindManipulatedModel(); - } - - const ImVec2 mousePos = ImGui::GetIO().MousePos; - nbl::ui::beginHoverInfoOverlay("InfoOverlay", mousePos); - - std::string objectLabel; - if (targetGimbalManipulationCamera) - objectLabel = targetGimbalManipulationCamera->getIdentifier(); - else if (isFollowTarget) - objectLabel = m_followTarget.getIdentifier(); - else - objectLabel = "Geometry Creator Object"; - - ImGui::Text("Identifier: %s", objectLabel.c_str()); - ImGui::Text("Object Ix: %u", objectIx); - if (targetGimbalManipulationCamera) - { - ImGui::Separator(); - ImGui::TextDisabled("RMB: switch view to this camera"); - ImGui::TextDisabled("LMB drag: manipulate gizmo"); - ImGui::TextDisabled("SPACE: toggle move mode"); - } - else if (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::endHoverInfoOverlay(); - } - - ImGuizmo::PopID(); - } - } - - ImGui::End(); - nbl::ui::popViewportWindowStyle(); - } - - if (windowBindings.size() > 1u) - { - const auto& topRw = wInit.renderWindows[0]; - const float splitY = topRw.iPos.y + topRw.iSize.y; - const float gap = std::max(0.0f, wInit.renderWindows[1].iPos.y - splitY); - ImGui::SetNextWindowPos(ImVec2(0.0f, 0.0f), ImGuiCond_Always); - ImGui::SetNextWindowSize(io.DisplaySize, ImGuiCond_Always); - ImGui::Begin("SplitOverlay", nullptr, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoBringToFrontOnFocus); - if (auto* drawList = ImGui::GetWindowDrawList(); drawList) - nbl::ui::drawViewportSplitOverlay(*drawList, io.DisplaySize, splitY, gap); - ImGui::End(); - } - - assert(manipulationCounter <= 1u); -} - -void App::drawFullscreenViewportWindow(ImGuiIO& io, SImResourceInfo& info) -{ - info.textureID = 1u + 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); - const ImVec2 contentRegionSize = ImGui::GetContentRegionAvail(); - const ImVec2 cursorPos = ImGui::GetCursorScreenPos(); - - nbl::ui::SBoundViewportCameraState viewportState = {}; - const auto planarSpan = std::span>(m_planarProjections.data(), m_planarProjections.size()); - const bool viewportValid = nbl::ui::tryBuildViewportBoundCameraState(planarSpan, windowBindings[activeRenderWindowIx], contentRegionSize, false, viewportState); - - ImGui::Image(info, contentRegionSize); - ImGuizmo::SetRect(cursorPos.x, cursorPos.y, contentRegionSize.x, contentRegionSize.y); - if (viewportValid && m_scriptedInput.enabled && m_scriptedInput.visualDebug && m_scriptedInput.visualFollowActive) - { - if (auto* drawList = ImGui::GetWindowDrawList(); drawList) - { - const nbl::ui::SViewportOverlayRect followRect = { cursorPos, contentRegionSize }; - nbl::ui::drawFollowTargetViewportOverlay(*drawList, viewportState.viewProjMatrix, m_followTarget, followRect); - } - } - - ImGui::End(); - ImGui::PopStyleColor(1); -} - -void App::refreshViewportBindingMatrices() -{ - const auto planarSpan = std::span>(m_planarProjections.data(), m_planarProjections.size()); - for (auto& binding : windowBindings) - { - nbl::ui::SBoundViewportCameraState viewportState = {}; - nbl::ui::tryBuildWindowBindingMatrices(planarSpan, binding, viewportState); - } + finalizeUiFrameState(); } diff --git a/61_UI/AppInit.cpp b/61_UI/AppInit.cpp index 7d0675475..74ee23e4a 100644 --- a/61_UI/AppInit.cpp +++ b/61_UI/AppInit.cpp @@ -1,2769 +1,81 @@ #include "app/App.hpp" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "app/AppCameraConfigUtilities.hpp" #include "app/AppResourceUtilities.hpp" -#include "camera/CCameraPersistence.hpp" -#include "camera/CCameraScriptedRuntimePersistence.hpp" -#include "camera/CCameraSmokeRegressionUtilities.hpp" - -namespace -{ - using camera_json_t = nbl::system::camera_json_t; - - constexpr double CameraTinyScalarEpsilon = nbl::system::SCameraSmokeComparisonThresholds::TinyScalarEpsilon; - constexpr nbl::system::SCameraFollowRegressionThresholds CameraFollowRegressionThresholds = {}; -} 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_headlessCameraSmokeMode = program.get("--headless-camera-smoke"); - if (m_headlessCameraSmokeMode) - { - auto fail = [&](const std::string& msg) -> bool - { - std::cerr << "[headless-camera-smoke][fail] " << msg << std::endl; - m_headlessCameraSmokePassed = false; - return false; - }; - - if (!asset_base_t::onAppInitialized(std::move(system))) - return fail("Failed to initialize mounted resources for headless camera smoke."); - - camera_json_t j; - std::string jsonError; - if (program.is_used("--file")) - { - const auto configPath = std::filesystem::path(program.get("--file")); - if (!nbl::system::loadJsonFromMountedResourceOrResolvedPath(*m_system, localInputCWD, configPath, j, nullptr, &jsonError)) - return fail(jsonError); - } - else if (!nbl::system::loadJsonFromPath( - *m_system, - std::filesystem::path(nbl::system::SCameraAppResourcePaths::DefaultCameraConfigRelativePath), - j, - &jsonError)) - { - return fail(jsonError); - } - - std::vector> cameras; - if (!nbl::system::tryLoadCameraCollectionFromJson(j, jsonError, cameras)) - return fail(jsonError); - - auto comparePresetToCameraDefault = [&](ICamera* camera, const CameraPreset& preset) -> bool - { - return nbl::system::comparePresetToCameraStateWithDefaultThresholds(m_cameraGoalSolver, camera, preset); - }; - - auto comparePresetToCameraStrict = [&](ICamera* camera, const CameraPreset& preset) -> bool - { - return nbl::system::comparePresetToCameraStateWithStrictThresholds(m_cameraGoalSolver, camera, preset); - }; - - auto describePresetMismatch = [&](ICamera* camera, const CameraPreset& preset) -> std::string - { - return nbl::core::describePresetCameraMismatch(m_cameraGoalSolver, camera, preset); - }; - - auto tryBuildFollowViewProjForCamera = [&](ICamera* camera, float32_t4x4& outViewProjMatrix) -> bool - { - if (!camera) - return false; - - for (uint32_t planarIx = 0u; planarIx < m_planarProjections.size(); ++planarIx) - { - auto& planar = m_planarProjections[planarIx]; - if (!planar || planar->getCamera() != camera) - continue; - - const auto& projections = planar->getPlanarProjections(); - if (projections.empty()) - return false; - - uint32_t projectionIx = 0u; - for (uint32_t ix = 0u; ix < projections.size(); ++ix) - { - if (projections[ix].getParameters().m_type == IPlanarProjection::CProjection::Perspective) - { - projectionIx = ix; - break; - } - } - - const auto viewMatrix = getMatrix3x4As4x4(getCastedMatrix(camera->getGimbal().getViewMatrix())); - const auto projectionMatrix = getCastedMatrix(projections[projectionIx].getProjectionMatrix()); - outViewProjMatrix = mul(projectionMatrix, viewMatrix); - return true; - } - - return false; - }; - - auto buildFollowVisualMetricsForCamera = [&](ICamera* camera, const CTrackedTarget& trackedTarget, - const SCameraFollowConfig& followConfig) -> SCameraFollowVisualMetrics - { - float32_t4x4 viewProjMatrix = float32_t4x4(1.0f); - const bool hasViewProjMatrix = tryBuildFollowViewProjForCamera(camera, viewProjMatrix); - return nbl::system::buildFollowVisualMetrics( - camera, - trackedTarget, - &followConfig, - hasViewProjMatrix ? &viewProjMatrix : nullptr); - }; - - auto buildAndValidateFollowTargetContract = [&](ICamera* camera, const CTrackedTarget& trackedTarget, - const SCameraFollowConfig& followConfig, const char* label, nbl::system::SCameraFollowApplyValidationResult& outResult) -> bool - { - std::string regressionError; - float32_t4x4 viewProjMatrix = float32_t4x4(1.0f); - const bool hasViewProjMatrix = tryBuildFollowViewProjForCamera(camera, viewProjMatrix); - if (!nbl::system::buildApplyAndValidateFollowTargetContract( - m_cameraGoalSolver, - camera, - trackedTarget, - followConfig, - outResult, - ®ressionError, - hasViewProjMatrix ? &viewProjMatrix : nullptr)) - { - return fail(std::string("Follow smoke contract failed for ") + label + ". " + regressionError); - } - return true; - }; - - auto verifyFollowVisualMetrics = [&](ICamera* camera, const CTrackedTarget& trackedTarget, - const SCameraFollowConfig& followConfig, const char* label) -> bool - { - const auto metrics = buildFollowVisualMetricsForCamera(camera, trackedTarget, followConfig); - float32_t4x4 viewProjMatrix = float32_t4x4(1.0f); - const bool expectsProjectedMetrics = tryBuildFollowViewProjForCamera(camera, viewProjMatrix); - if (!metrics.active) - return fail(std::string("Follow visual metrics smoke was inactive for ") + label + "."); - if (nbl::core::cameraFollowModeLocksViewToTarget(followConfig.mode) && !metrics.lockValid) - return fail(std::string("Follow visual metrics smoke was missing lock metrics for ") + label + "."); - if (expectsProjectedMetrics && !metrics.projectedValid) - return fail(std::string("Follow visual metrics smoke was missing projected metrics for ") + label + "."); - if (metrics.projectedValid && metrics.projectedNdcRadius > CameraFollowRegressionThresholds.projectedNdcTolerance) - return fail(std::string("Follow visual metrics smoke had projected center error for ") + label + "."); - return true; - }; - - auto verifyScriptedRuntimeFrameBatch = [&]() -> bool - { - CCameraScriptedTimeline timeline = {}; - nbl::system::appendScriptedActionEvent(timeline, 3u, CCameraScriptedInputEvent::ActionData::Kind::SetActivePlanar, 4); - { - CCameraGoal goal = {}; - goal.position = float64_t3(1.0, 2.0, 3.0); - nbl::system::appendScriptedGoalEvent(timeline, 3u, goal, true); - } - nbl::system::appendScriptedSegmentLabelEvent(timeline, 3u, "segment-three"); - { - float64_t4x4 transform = float64_t4x4(1.0); - transform[3] = float64_t4(7.0, 8.0, 9.0, 1.0); - nbl::system::appendScriptedTrackedTargetTransformEvent(timeline, 4u, transform); - } - - size_t nextEventIndex = 0u; - CCameraScriptedFrameEvents batch; - nbl::system::dequeueScriptedFrameEvents(timeline.events, nextEventIndex, 3u, batch); - if (nextEventIndex != 3u || batch.actions.size() != 1u || batch.goals.size() != 1u || - batch.segmentLabels.size() != 1u || !batch.mouse.empty() || !batch.keyboard.empty()) - { - return fail("Scripted runtime frame batch smoke failed for frame 3."); - } - if (batch.actions.front().kind != CCameraScriptedInputEvent::ActionData::Kind::SetActivePlanar || - batch.actions.front().value != 4 || batch.segmentLabels.front() != "segment-three") - { - return fail("Scripted runtime frame batch payload smoke failed for frame 3."); - } - - nbl::system::dequeueScriptedFrameEvents(timeline.events, nextEventIndex, 4u, batch); - if (nextEventIndex != timeline.events.size() || batch.trackedTargetTransforms.size() != 1u || - !batch.actions.empty() || !batch.goals.empty()) - { - return fail("Scripted runtime frame batch smoke failed for frame 4."); - } - const auto trackedTargetPosition = float64_t3(batch.trackedTargetTransforms.front().transform[3]); - if (!hlsl::nearlyEqualVec3(trackedTargetPosition, float64_t3(7.0, 8.0, 9.0), CameraTinyScalarEpsilon)) - return fail("Scripted runtime tracked-target payload smoke failed."); - - return true; - }; - - auto verifyScriptedRuntimeParser = [&]() -> bool - { - std::stringstream script; - script << R"json({ - "enabled": true, - "capture_prefix": "parser_smoke", - "camera_controls": { - "keyboard_scale": 2.0, - "rotation_scale": 0.5 - }, - "events": [ - { - "frame": 2, - "type": "action", - "action": "set_active_planar", - "value": 3 - }, - { - "frame": 2, - "type": "keyboard", - "key": "W", - "action": "pressed", - "capture": true - } - ], - "checks": [ - { - "frame": 2, - "kind": "baseline" - }, - { - "frame": 3, - "kind": "gimbal_step", - "min_pos_delta": 0.01, - "max_pos_delta": 1.0 - } - ] -})json"; - - nbl::system::CCameraScriptedInputParseResult parsed; - std::string parseError; - if (!nbl::system::readCameraScriptedInput(script, parsed, &parseError)) - return fail("Scripted runtime parser smoke failed to parse low-level runtime payload. " + parseError); - if (!parsed.enabled || parsed.capturePrefix != "parser_smoke" || !parsed.cameraControls.hasKeyboardScale || !parsed.cameraControls.hasRotationScale) - return fail("Scripted runtime parser smoke lost top-level metadata."); - if (parsed.timeline.events.size() != 2u || parsed.timeline.checks.size() != 2u || parsed.timeline.captureFrames.size() != 1u) - return fail("Scripted runtime parser smoke produced wrong payload counts."); - if (parsed.timeline.captureFrames.front() != 2u) - return fail("Scripted runtime parser smoke produced wrong capture frame."); - - size_t nextEventIndex = 0u; - CCameraScriptedFrameEvents batch; - nbl::system::dequeueScriptedFrameEvents(parsed.timeline.events, nextEventIndex, 2u, batch); - if (batch.actions.size() != 1u || batch.keyboard.size() != 1u || batch.actions.front().value != 3) - return fail("Scripted runtime parser smoke produced wrong frame-two batch."); - if (parsed.timeline.checks.front().kind != CCameraScriptedInputCheck::Kind::Baseline || - parsed.timeline.checks.back().kind != CCameraScriptedInputCheck::Kind::GimbalStep) - { - return fail("Scripted runtime parser smoke produced wrong check kinds."); - } - - return true; - }; - - auto verifyScriptedCheckRunner = [&]() -> bool - { - auto orbitCamera = core::make_smart_refctd_ptr(float64_t3(0.0, 1.5, -6.0), float64_t3(0.0, 0.0, 0.0)); - CTrackedTarget trackedTarget( - float64_t3(2.0, 0.5, -1.5), - makeQuaternionFromAxisAngle(float64_t3(0.0, 1.0, 0.0), hlsl::radians(35.0))); - - CCameraScriptedTimeline timeline = {}; - nbl::system::appendScriptedBaselineCheck(timeline, 1u); - nbl::system::appendScriptedGimbalStepCheck(timeline, 2u, true, 2.0f, 0.005f, true, 45.0f, 0.05f); - nbl::system::appendScriptedFollowTargetLockCheck( - timeline, - 3u, - CameraFollowRegressionThresholds.lockAngleToleranceDeg, - CameraFollowRegressionThresholds.projectedNdcTolerance); - - CCameraScriptedCheckRuntimeState state = {}; - { - const auto frameResult = evaluateScriptedChecksForFrame( - timeline.checks, - state, - { - .frame = 1u, - .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 = getQuaternionEulerDegrees(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; - return fail(oss.str()); - } - } - - { - CVirtualGimbalEvent stepEvent = {}; - stepEvent.type = CVirtualGimbalEvent::MoveRight; - stepEvent.magnitude = 12.0; - if (!orbitCamera->manipulate({ &stepEvent, 1u })) - return fail("Scripted check runner smoke failed to manipulate the camera for step validation."); - - const auto frameResult = evaluateScriptedChecksForFrame( - timeline.checks, - state, - { - .frame = 2u, - .camera = orbitCamera.get() - }); - if (frameResult.hadFailures || state.nextCheckIndex != 2u) - { - const auto details = !frameResult.logs.empty() ? frameResult.logs.front().text : std::string("missing log details"); - return fail(std::string("Scripted check runner step smoke failed. ") + details); - } - } - - SCameraFollowConfig followConfig = {}; - followConfig.enabled = true; - followConfig.mode = ECameraFollowMode::OrbitTarget; - CCameraGoal followGoal = {}; - if (!nbl::core::applyFollowToCamera(m_cameraGoalSolver, orbitCamera.get(), trackedTarget, followConfig, &followGoal).succeeded()) - return fail("Scripted check runner smoke failed to apply follow before follow-lock validation."); - - { - const auto frameResult = evaluateScriptedChecksForFrame( - timeline.checks, - state, - { - .frame = 3u, - .camera = orbitCamera.get(), - .trackedTarget = &trackedTarget, - .followConfig = &followConfig, - .goalSolver = &m_cameraGoalSolver - }); - 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); - const auto desiredRight = normalize(cross(float64_t3(0.0, 1.0, 0.0), desiredForward)); - const auto desiredUp = normalize(cross(desiredForward, desiredRight)); - const auto goalRightVec = normalizeQuaternion(followGoal.orientation).transformVector(float64_t3(1.0, 0.0, 0.0), true); - const auto goalUpVec = normalizeQuaternion(followGoal.orientation).transformVector(float64_t3(0.0, 1.0, 0.0), true); - const auto goalForwardVec = normalizeQuaternion(followGoal.orientation).transformVector(float64_t3(0.0, 0.0, 1.0), true); - const auto goalBasis = getQuaternionBasisMatrix(followGoal.orientation); - float lockAngle = 0.0f; - double targetDistance = 0.0; - const bool hasLockMetrics = nbl::core::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; - return fail(oss.str()); - } - } - - return true; - }; - - auto verifyFollowTargetContract = [&](ICamera* camera, const CTrackedTarget& trackedTarget, - const SCameraFollowConfig& followConfig, const CCameraGoal& followGoal, const char* label) -> bool - { - nbl::system::SCameraFollowRegressionResult regression = {}; - std::string regressionError; - float32_t4x4 viewProjMatrix = float32_t4x4(1.0f); - const bool hasViewProjMatrix = tryBuildFollowViewProjForCamera(camera, viewProjMatrix); - if (!nbl::system::validateFollowTargetContract( - camera, - trackedTarget, - followConfig, - followGoal, - regression, - ®ressionError, - hasViewProjMatrix ? &viewProjMatrix : nullptr, - CameraFollowRegressionThresholds)) - { - return fail(std::string("Follow smoke contract failed for ") + label + ". " + regressionError); - } - return true; - }; - - auto verifyFollowTargetMarkerAlignment = [&](const CTrackedTarget& trackedTarget, const char* label) -> bool - { - m_followTarget.setPose(trackedTarget.getGimbal().getPosition(), trackedTarget.getGimbal().getOrientation()); - const auto markerWorld = computeFollowTargetMarkerWorld(); - const auto markerTransform = hlsl::transpose(getMatrix3x4As4x4(markerWorld)); - const auto markerPosition = getCastedVector(float32_t3(markerTransform[3])); - const auto positionDelta = markerPosition - trackedTarget.getGimbal().getPosition(); - const auto errorLength = length(positionDelta); - if (!std::isfinite(errorLength) || errorLength > CameraTinyScalarEpsilon) - { - return fail(std::string("Follow target marker alignment smoke failed for ") + label + "."); - } - return true; - }; - - auto verifyOffsetFollowRecapture = [&](ICamera* camera, const CTrackedTarget& trackedTarget, const char* label) -> bool - { - if (!camera) - return true; - - const auto baselinePreset = nbl::core::capturePreset(m_cameraGoalSolver, camera, std::string(label) + " baseline"); - SCameraFollowConfig followConfig = {}; - followConfig.enabled = true; - followConfig.mode = ECameraFollowMode::KeepLocalOffset; - - if (!nbl::core::captureFollowOffsetsFromCamera(m_cameraGoalSolver, camera, trackedTarget, followConfig)) - return fail(std::string("Follow recapture smoke failed to capture initial offset for ") + label + "."); - - const auto initialApply = nbl::core::applyFollowToCamera(m_cameraGoalSolver, camera, trackedTarget, followConfig); - if (!initialApply.succeeded()) - return fail(std::string("Follow recapture smoke failed to apply initial follow for ") + label + "."); - - auto editedPreset = nbl::core::capturePreset(m_cameraGoalSolver, camera, std::string(label) + " edited"); - if (!editedPreset.goal.hasOrbitState) - return fail(std::string("Follow recapture smoke missing orbit state for ") + label + "."); - - editedPreset.goal.orbitU = hlsl::wrapAngleRad(editedPreset.goal.orbitU + hlsl::radians(18.0)); - editedPreset.goal.orbitDistance = std::clamp(editedPreset.goal.orbitDistance + 0.75f, CSphericalTargetCamera::MinDistance, CSphericalTargetCamera::MaxDistance); - editedPreset.goal = nbl::core::canonicalizeGoal(editedPreset.goal); - if (!nbl::core::isGoalFinite(editedPreset.goal)) - return fail(std::string("Follow recapture smoke produced a non-finite edited goal for ") + label + "."); - - const auto editedApply = nbl::core::applyPresetDetailed(m_cameraGoalSolver, camera, editedPreset); - if (!editedApply.succeeded() || !editedApply.changed()) - { - return fail(std::string("Follow recapture smoke failed to apply edited preset for ") + label + - ". " + describeApplyResult(editedApply)); - } - - const auto reachedEditedPreset = nbl::core::capturePreset(m_cameraGoalSolver, camera, std::string(label) + " reached"); - - if (!nbl::core::captureFollowOffsetsFromCamera(m_cameraGoalSolver, camera, trackedTarget, followConfig)) - return fail(std::string("Follow recapture smoke failed to recapture offset for ") + label + "."); - - CCameraGoal recapturedGoal = {}; - if (!nbl::core::tryBuildFollowGoal(m_cameraGoalSolver, camera, trackedTarget, followConfig, recapturedGoal)) - return fail(std::string("Follow recapture smoke failed to rebuild follow goal for ") + label + "."); - - const auto recapturedApply = nbl::core::applyFollowToCamera(m_cameraGoalSolver, camera, trackedTarget, followConfig); - if (!recapturedApply.succeeded()) - { - return fail(std::string("Follow recapture smoke failed to apply recaptured follow for ") + label + - ". " + describeApplyResult(recapturedApply)); - } - - if (!nbl::core::comparePresetToCameraState(m_cameraGoalSolver, camera, reachedEditedPreset, 5e-6, nbl::system::SCameraSmokeComparisonThresholds::StrictAngularToleranceDeg, 5e-6)) - return fail(std::string("Follow recapture smoke mismatch for ") + label + ". " + describePresetMismatch(camera, reachedEditedPreset)); - if (!verifyFollowTargetContract(camera, trackedTarget, followConfig, recapturedGoal, label)) - return false; - - const auto restoreResult = nbl::core::applyPresetDetailed(m_cameraGoalSolver, camera, baselinePreset); - if (!restoreResult.succeeded() || !comparePresetToCameraStrict(camera, baselinePreset)) - { - return fail(std::string("Follow recapture smoke failed to restore baseline for ") + label + - ". " + describeApplyResult(restoreResult) + " " + describePresetMismatch(camera, baselinePreset)); - } - - return true; - }; - - if (!verifyScriptedRuntimeFrameBatch()) - return false; - if (!verifyScriptedRuntimeParser()) - return false; - if (!verifyScriptedCheckRunner()) - return false; - - auto collectKeyboardVirtualEvents = [&](CGimbalInputBinder& inputBinder, const ui::E_KEY_CODE keyCode) -> std::vector - { - static std::chrono::microseconds smokeTimestamp = std::chrono::microseconds::zero(); - smokeTimestamp += std::chrono::microseconds(16667); - 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 += std::chrono::microseconds(16667); - const auto sampleTs = smokeTimestamp; - return inputBinder.collectVirtualEvents(sampleTs).events; - }; - - auto collectMouseVirtualEvents = [&](CGimbalInputBinder& inputBinder, std::span mouseEvents) -> std::vector - { - static std::chrono::microseconds smokeTimestamp = std::chrono::microseconds::zero(); - smokeTimestamp += std::chrono::microseconds(16667); - const auto ts = smokeTimestamp; - return inputBinder.collectVirtualEvents(ts, { .mouseEvents = mouseEvents }).events; - }; - - auto filterOrbitMouseEvents = [&](ICamera* camera, std::span input, bool orbitLookDown) -> std::vector - { - if (!isOrbitLikeCamera(camera)) - return std::vector(input.begin(), input.end()); - - std::vector filtered; - filtered.reserve(input.size()); - for (const auto& ev : input) - { - if (ev.type == ui::SMouseEvent::EET_MOVEMENT && !orbitLookDown) - continue; - filtered.emplace_back(ev); - } - return filtered; - }; - - const std::array keyboardCandidates = { - ui::E_KEY_CODE::EKC_W, - ui::E_KEY_CODE::EKC_A, - ui::E_KEY_CODE::EKC_S, - ui::E_KEY_CODE::EKC_D, - ui::E_KEY_CODE::EKC_Q, - ui::E_KEY_CODE::EKC_E, - ui::E_KEY_CODE::EKC_I, - ui::E_KEY_CODE::EKC_J, - ui::E_KEY_CODE::EKC_K, - ui::E_KEY_CODE::EKC_L, - ui::E_KEY_CODE::EKC_U, - ui::E_KEY_CODE::EKC_O - }; - - CameraPreset initialOrbitPreset; - CameraPreset initialFreePreset; - CameraPreset initialChasePreset; - CameraPreset initialDollyPreset; - CameraPreset initialPathPreset; - CameraPreset initialDollyZoomPreset; - bool hasOrbitPreset = false; - bool hasFreePreset = false; - bool hasChasePreset = false; - bool hasDollyPreset = false; - bool hasPathPreset = false; - bool hasDollyZoomPreset = false; - - for (const auto& cameraRef : cameras) - { - auto* camera = cameraRef.get(); - if (!camera) - return fail("Null camera instance."); - - CGimbalInputBinder inputBinder; - applyDefaultCameraInputBindingPreset(inputBinder, *camera); - - const auto initialPreset = nbl::core::capturePreset(m_cameraGoalSolver, camera, "smoke-initial"); - const auto initialCompatibility = analyzePresetCompatibility(camera, initialPreset); - if (!initialCompatibility.exact || initialCompatibility.missingGoalStateMask != ICamera::GoalStateNone) - return fail("Preset compatibility smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\". missing=" + describeGoalStateMask(initialCompatibility.missingGoalStateMask)); - switch (camera->getKind()) - { - case ICamera::CameraKind::Orbit: - initialOrbitPreset = initialPreset; - hasOrbitPreset = true; - break; - case ICamera::CameraKind::Free: - initialFreePreset = initialPreset; - hasFreePreset = true; - break; - case ICamera::CameraKind::Chase: - initialChasePreset = initialPreset; - hasChasePreset = true; - break; - case ICamera::CameraKind::Dolly: - initialDollyPreset = initialPreset; - hasDollyPreset = true; - break; - case ICamera::CameraKind::Path: - initialPathPreset = initialPreset; - hasPathPreset = true; - break; - case ICamera::CameraKind::DollyZoom: - initialDollyZoomPreset = initialPreset; - hasDollyZoomPreset = true; - break; - default: - break; - } - if (!nbl::core::applyPreset(m_cameraGoalSolver, camera, initialPreset)) - return fail("Preset no-op smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\"."); - - if (initialPreset.goal.hasTargetPosition) - { - CameraPreset shiftedPreset = initialPreset; - shiftedPreset.goal.targetPosition += float64_t3(0.5, -0.25, 0.75); - - const auto shiftedResult = nbl::core::applyPresetDetailed(m_cameraGoalSolver, camera, shiftedPreset); - if (!shiftedResult.succeeded() || !shiftedResult.changed() || !shiftedResult.exact) - return fail("Preset target apply smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\". " + describeApplyResult(shiftedResult)); - - ICamera::SphericalTargetState shiftedState; - if (!camera->tryGetSphericalTargetState(shiftedState) || !hlsl::nearlyEqualVec3(shiftedState.target, shiftedPreset.goal.targetPosition, 1e-9)) - return fail("Preset target writeback smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\"."); - - const auto restoredResult = nbl::core::applyPresetDetailed(m_cameraGoalSolver, camera, initialPreset); - if (!restoredResult.succeeded() || !restoredResult.exact) - return fail("Preset restore smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\". " + describeApplyResult(restoredResult)); - - ICamera::SphericalTargetState restoredState; - if (!camera->tryGetSphericalTargetState(restoredState) || !hlsl::nearlyEqualVec3(restoredState.target, initialPreset.goal.targetPosition, 1e-9)) - return fail("Preset target restore smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\"."); - - if (!comparePresetToCameraDefault(camera, initialPreset)) - return fail("Preset restore mismatch smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\". " + describePresetMismatch(camera, initialPreset)); - } - - if (initialPreset.goal.hasDynamicPerspectiveState) - { - CameraPreset shiftedPreset = initialPreset; - shiftedPreset.goal.dynamicPerspectiveState.baseFov = - std::clamp(initialPreset.goal.dynamicPerspectiveState.baseFov + 7.5f, 10.0f, 150.0f); - if (std::abs(static_cast( - shiftedPreset.goal.dynamicPerspectiveState.baseFov - initialPreset.goal.dynamicPerspectiveState.baseFov)) < 1e-6) - { - shiftedPreset.goal.dynamicPerspectiveState.baseFov = - std::max(10.0f, initialPreset.goal.dynamicPerspectiveState.baseFov - 7.5f); - } - shiftedPreset.goal.dynamicPerspectiveState.referenceDistance = - std::max(0.1f, initialPreset.goal.dynamicPerspectiveState.referenceDistance + 1.25f); - - const auto shiftedResult = nbl::core::applyPresetDetailed(m_cameraGoalSolver, camera, shiftedPreset); - if (!shiftedResult.succeeded() || !shiftedResult.changed() || !comparePresetToCameraStrict(camera, shiftedPreset)) - return fail("Preset dynamic perspective apply smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\". " + describeApplyResult(shiftedResult) + " " + describePresetMismatch(camera, shiftedPreset)); - - const auto restoredResult = nbl::core::applyPresetDetailed(m_cameraGoalSolver, camera, initialPreset); - if (!restoredResult.succeeded() || !comparePresetToCameraStrict(camera, initialPreset)) - return fail("Preset dynamic perspective restore smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\". " + describeApplyResult(restoredResult) + " " + describePresetMismatch(camera, initialPreset)); - } - - const uint32_t allowed = camera->getAllowedVirtualEvents(); - std::vector directEvents; - directEvents.reserve(3u); - auto pushDirectEvent = [&](const CVirtualGimbalEvent::VirtualEventType type, const double magnitude) -> void - { - CVirtualGimbalEvent ev; - ev.type = type; - ev.magnitude = magnitude; - directEvents.emplace_back(ev); - }; - if (allowed & CVirtualGimbalEvent::MoveForward) - pushDirectEvent(CVirtualGimbalEvent::MoveForward, 1.0); - else if (allowed & CVirtualGimbalEvent::MoveRight) - pushDirectEvent(CVirtualGimbalEvent::MoveRight, 1.0); - else if (allowed & CVirtualGimbalEvent::MoveUp) - pushDirectEvent(CVirtualGimbalEvent::MoveUp, 1.0); - if (allowed & CVirtualGimbalEvent::PanRight) - pushDirectEvent(CVirtualGimbalEvent::PanRight, 1.0); - else if (allowed & CVirtualGimbalEvent::TiltUp) - pushDirectEvent(CVirtualGimbalEvent::TiltUp, 1.0); - else if (allowed & CVirtualGimbalEvent::RollRight) - pushDirectEvent(CVirtualGimbalEvent::RollRight, 1.0); - if (directEvents.empty()) - { - for (const auto event : CVirtualGimbalEvent::VirtualEventsTypeTable) - { - if (allowed & event) - { - pushDirectEvent(event, 1.0); - break; - } - } - } - if (directEvents.empty()) - return fail("No allowed virtual events for camera \"" + std::string(camera->getIdentifier()) + "\"."); - - nbl::system::SCameraManipulationDelta directDelta = {}; - if (!nbl::system::tryManipulateCameraAndMeasureDelta(camera, { directEvents.data(), directEvents.size() }, directDelta, CameraTinyScalarEpsilon)) - return fail("Direct manipulate smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\"."); - { - const auto modifiedPreset = nbl::core::capturePreset(m_cameraGoalSolver, camera, "smoke-direct"); - const auto restoreInitial = nbl::core::applyPresetDetailed(m_cameraGoalSolver, camera, initialPreset); - if (!restoreInitial.succeeded() || !nbl::core::comparePresetToCameraState(m_cameraGoalSolver, camera, initialPreset, 1e-3, nbl::system::SCameraSmokeComparisonThresholds::StrictAngularToleranceDeg, 1e-4)) - return fail("Preset restore from direct smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\". " + describePresetMismatch(camera, initialPreset)); - - const auto applyModified = nbl::core::applyPresetDetailed(m_cameraGoalSolver, camera, modifiedPreset); - if (!applyModified.succeeded() || !applyModified.changed() || !nbl::core::comparePresetToCameraState(m_cameraGoalSolver, camera, modifiedPreset, 1e-3, nbl::system::SCameraSmokeComparisonThresholds::StrictAngularToleranceDeg, 1e-4)) - return fail("Preset apply from direct smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\". " + describePresetMismatch(camera, modifiedPreset)); - - const auto restoreAgain = nbl::core::applyPresetDetailed(m_cameraGoalSolver, camera, initialPreset); - if (!restoreAgain.succeeded() || !nbl::core::comparePresetToCameraState(m_cameraGoalSolver, camera, initialPreset, 1e-3, nbl::system::SCameraSmokeComparisonThresholds::StrictAngularToleranceDeg, 1e-4)) - return fail("Preset final restore smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\". " + describePresetMismatch(camera, initialPreset)); - } - - bool keyboardOk = false; - nbl::system::SCameraManipulationDelta keyboardDelta = {}; - for (const auto key : keyboardCandidates) - { - applyDefaultCameraInputBindingPreset(inputBinder, *camera); - auto keyboardEvents = collectKeyboardVirtualEvents(inputBinder, key); - if (keyboardEvents.empty()) - continue; - if (nbl::system::tryManipulateCameraAndMeasureDelta(camera, { keyboardEvents.data(), keyboardEvents.size() }, keyboardDelta, CameraTinyScalarEpsilon)) - { - keyboardOk = true; - break; - } - } - if (!keyboardOk) - return fail("Keyboard binding smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\"."); - - const auto& mousePreset = getDefaultCameraMouseMappingPreset(*camera); - const bool hasMoveMapping = - mousePreset.find(ui::EMC_RELATIVE_POSITIVE_MOVEMENT_X) != mousePreset.end() || - mousePreset.find(ui::EMC_RELATIVE_NEGATIVE_MOVEMENT_X) != mousePreset.end() || - mousePreset.find(ui::EMC_RELATIVE_POSITIVE_MOVEMENT_Y) != mousePreset.end() || - mousePreset.find(ui::EMC_RELATIVE_NEGATIVE_MOVEMENT_Y) != mousePreset.end(); - const bool hasScrollMapping = - mousePreset.find(ui::EMC_VERTICAL_POSITIVE_SCROLL) != mousePreset.end() || - mousePreset.find(ui::EMC_VERTICAL_NEGATIVE_SCROLL) != mousePreset.end() || - mousePreset.find(ui::EMC_HORIZONTAL_POSITIVE_SCROLL) != mousePreset.end() || - mousePreset.find(ui::EMC_HORIZONTAL_NEGATIVE_SCROLL) != mousePreset.end(); - - nbl::system::SCameraManipulationDelta mouseMoveDelta = {}; - if (hasMoveMapping) - { - SMouseEvent moveEv(std::chrono::microseconds(16667)); - moveEv.window = nullptr; - moveEv.type = ui::SMouseEvent::EET_MOVEMENT; - moveEv.movementEvent.relativeMovementX = 12; - moveEv.movementEvent.relativeMovementY = -8; - - 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 (isOrbitLikeCamera(camera) && hasBlockedMovement) - return fail("Orbit mouse movement gate failed for camera \"" + std::string(camera->getIdentifier()) + "\"."); - - applyDefaultCameraInputBindingPreset(inputBinder, *camera); - auto mouseMoveEvents = collectMouseVirtualEvents(inputBinder, { filteredMoveLookDown.data(), filteredMoveLookDown.size() }); - if (mouseMoveEvents.empty()) - return fail("Mouse move virtual events missing for camera \"" + std::string(camera->getIdentifier()) + "\"."); - if (!nbl::system::tryManipulateCameraAndMeasureDelta(camera, { mouseMoveEvents.data(), mouseMoveEvents.size() }, mouseMoveDelta, CameraTinyScalarEpsilon)) - return fail("Mouse move binding smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\"."); - } - - nbl::system::SCameraManipulationDelta mouseScrollDelta = {}; - if (hasScrollMapping) - { - SMouseEvent scrollEv(std::chrono::microseconds(16667)); - scrollEv.window = nullptr; - scrollEv.type = ui::SMouseEvent::EET_SCROLL; - scrollEv.scrollEvent.verticalScroll = 4; - scrollEv.scrollEvent.horizontalScroll = 2; - const std::array rawScroll = { scrollEv }; - auto filteredScroll = filterOrbitMouseEvents(camera, rawScroll, false); - - applyDefaultCameraInputBindingPreset(inputBinder, *camera); - auto mouseScrollEvents = collectMouseVirtualEvents(inputBinder, { filteredScroll.data(), filteredScroll.size() }); - if (mouseScrollEvents.empty()) - return fail("Mouse scroll virtual events missing for camera \"" + std::string(camera->getIdentifier()) + "\"."); - if (!nbl::system::tryManipulateCameraAndMeasureDelta(camera, { mouseScrollEvents.data(), mouseScrollEvents.size() }, mouseScrollDelta, CameraTinyScalarEpsilon)) - return fail("Mouse scroll binding smoke failed for camera \"" + std::string(camera->getIdentifier()) + "\"."); - } - - std::cout << "[headless-camera-smoke][pass] " << camera->getIdentifier() - << " 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; - } - - auto findCameraByKind = [&](const ICamera::CameraKind kind) -> ICamera* - { - for (const auto& cameraRef : cameras) - { - auto* candidate = cameraRef.get(); - if (candidate && candidate->getKind() == kind) - return candidate; - } - return nullptr; - }; - - auto verifyApproximateCrossKindApply = [&](ICamera* targetCamera, const CameraPreset& sourcePreset, - const CCameraGoalSolver::SApplyResult::EIssue expectedIssue, const char* label) -> bool - { - if (!targetCamera) - return true; - - uint32_t expectedMissingGoalStateMask = ICamera::GoalStateNone; - switch (expectedIssue) - { - case CCameraGoalSolver::SApplyResult::MissingPathState: - expectedMissingGoalStateMask = ICamera::GoalStatePath; - break; - case CCameraGoalSolver::SApplyResult::MissingDynamicPerspectiveState: - expectedMissingGoalStateMask = ICamera::GoalStateDynamicPerspective; - break; - case CCameraGoalSolver::SApplyResult::MissingSphericalTargetState: - expectedMissingGoalStateMask = ICamera::GoalStateSphericalTarget; - break; - default: - break; - } - - const auto compatibility = analyzePresetCompatibility(targetCamera, sourcePreset); - if (compatibility.exact || compatibility.missingGoalStateMask != expectedMissingGoalStateMask) - { - return fail(std::string("Cross-kind preset compatibility smoke failed for ") + label + - ". missing=" + describeGoalStateMask(compatibility.missingGoalStateMask)); - } - - const auto baselinePreset = nbl::core::capturePreset(m_cameraGoalSolver, targetCamera, std::string(label) + "-baseline"); - const auto applyResult = nbl::core::applyPresetDetailed(m_cameraGoalSolver, targetCamera, sourcePreset); - if (!applyResult.succeeded() || !applyResult.approximate() || !applyResult.hasIssue(expectedIssue)) - return fail(std::string("Cross-kind preset smoke failed for ") + label + ". " + describeApplyResult(applyResult)); - - const auto restoreResult = nbl::core::applyPresetDetailed(m_cameraGoalSolver, targetCamera, baselinePreset); - if (!restoreResult.succeeded() || !comparePresetToCameraStrict(targetCamera, baselinePreset)) - return fail(std::string("Cross-kind preset restore smoke failed for ") + label + ". " + describeApplyResult(restoreResult) + " " + describePresetMismatch(targetCamera, baselinePreset)); - - return true; - }; - - auto verifyExactCrossKindApply = [&](ICamera* targetCamera, const CameraPreset& sourcePreset, const char* label) -> bool - { - if (!targetCamera) - return true; - - const auto compatibility = analyzePresetCompatibility(targetCamera, sourcePreset); - if (!compatibility.exact || compatibility.missingGoalStateMask != ICamera::GoalStateNone) - { - return fail(std::string("Exact cross-kind preset compatibility smoke failed for ") + label + - ". missing=" + describeGoalStateMask(compatibility.missingGoalStateMask)); - } - - const auto baselinePreset = nbl::core::capturePreset(m_cameraGoalSolver, targetCamera, std::string(label) + "-baseline"); - const auto applyResult = nbl::core::applyPresetDetailed(m_cameraGoalSolver, targetCamera, sourcePreset); - if (!applyResult.succeeded() || !applyResult.exact || !comparePresetToCameraStrict(targetCamera, sourcePreset)) - { - return fail(std::string("Exact cross-kind preset smoke failed for ") + label + ". " + - describeApplyResult(applyResult) + " " + describePresetMismatch(targetCamera, sourcePreset)); - } - - const auto restoreResult = nbl::core::applyPresetDetailed(m_cameraGoalSolver, targetCamera, baselinePreset); - if (!restoreResult.succeeded() || !restoreResult.exact || !comparePresetToCameraStrict(targetCamera, baselinePreset)) - { - return fail(std::string("Exact cross-kind preset restore smoke failed for ") + label + ". " + - describeApplyResult(restoreResult) + " " + describePresetMismatch(targetCamera, baselinePreset)); - } - - return true; - }; - - ICamera* orbitCamera = findCameraByKind(ICamera::CameraKind::Orbit); - ICamera* freeCamera = findCameraByKind(ICamera::CameraKind::Free); - ICamera* chaseCamera = findCameraByKind(ICamera::CameraKind::Chase); - ICamera* dollyCamera = findCameraByKind(ICamera::CameraKind::Dolly); - ICamera* dollyZoomCamera = findCameraByKind(ICamera::CameraKind::DollyZoom); - - { - CTrackedTarget trackedTarget( - float64_t3(2.25, -0.75, 1.25), - makeQuaternionFromEulerRadians(float64_t3(0.18, -0.22, 0.41)), - "Smoke Target"); - - const auto movedTrackedTargetPosition = float64_t3(-1.5, 0.5, 2.25); - const auto movedTrackedTargetOrientation = makeQuaternionFromEulerRadians(float64_t3(-0.12, 0.35, 0.27)); - - if (orbitCamera) - { - const auto baselinePreset = nbl::core::capturePreset(m_cameraGoalSolver, orbitCamera, "orbit-follow-baseline"); - SCameraFollowConfig followConfig = {}; - followConfig.enabled = true; - followConfig.mode = ECameraFollowMode::OrbitTarget; - - nbl::system::SCameraFollowApplyValidationResult followResult = {}; - if (!buildAndValidateFollowTargetContract(orbitCamera, trackedTarget, followConfig, "orbit follow", followResult)) - return false; - if (!verifyFollowVisualMetrics(orbitCamera, trackedTarget, followConfig, "orbit follow")) - return false; - if (!verifyFollowTargetMarkerAlignment(trackedTarget, "orbit follow")) - return false; - - const auto restoreResult = nbl::core::applyPresetDetailed(m_cameraGoalSolver, orbitCamera, baselinePreset); - if (!restoreResult.succeeded() || !comparePresetToCameraStrict(orbitCamera, baselinePreset)) - return fail("Orbit follow smoke failed to restore the baseline preset."); - - followConfig.mode = ECameraFollowMode::KeepWorldOffset; - followConfig.worldOffset = float64_t3(4.0, -1.5, 2.0); - trackedTarget.setPose(movedTrackedTargetPosition, movedTrackedTargetOrientation); - - nbl::system::SCameraFollowApplyValidationResult worldOffsetResult = {}; - if (!buildAndValidateFollowTargetContract(orbitCamera, trackedTarget, followConfig, "orbit keep-world-offset follow", worldOffsetResult)) - return false; - if (!verifyFollowVisualMetrics(orbitCamera, trackedTarget, followConfig, "orbit keep-world-offset follow")) - return false; - - const auto restoreWorldOffsetResult = nbl::core::applyPresetDetailed(m_cameraGoalSolver, orbitCamera, baselinePreset); - if (!restoreWorldOffsetResult.succeeded() || !comparePresetToCameraStrict(orbitCamera, baselinePreset)) - return fail("Orbit keep-world-offset smoke failed to restore the baseline preset."); - } - - 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::capturePreset(m_cameraGoalSolver, defaultFollowCamera, label + " baseline"); - - trackedTarget.setPose(float64_t3(2.25, -0.75, 1.25), makeQuaternionFromEulerRadians(float64_t3(0.18, -0.22, 0.41))); - if ((nbl::core::cameraFollowModeUsesLocalOffset(followConfig.mode) || nbl::core::cameraFollowModeUsesWorldOffset(followConfig.mode)) && - !nbl::core::captureFollowOffsetsFromCamera(m_cameraGoalSolver, defaultFollowCamera, trackedTarget, followConfig)) - { - return fail("Default follow smoke failed to capture offsets for camera \"" + std::string(defaultFollowCamera->getIdentifier()) + "\"."); - } - - trackedTarget.setPose(movedTrackedTargetPosition, movedTrackedTargetOrientation); - - nbl::system::SCameraFollowApplyValidationResult defaultFollowResult = {}; - if (!buildAndValidateFollowTargetContract(defaultFollowCamera, trackedTarget, followConfig, label.c_str(), defaultFollowResult)) - return false; - if (!verifyFollowVisualMetrics(defaultFollowCamera, trackedTarget, followConfig, label.c_str())) - return false; - if (!verifyFollowTargetMarkerAlignment(trackedTarget, label.c_str())) - return false; - - const auto restoreResult = nbl::core::applyPresetDetailed(m_cameraGoalSolver, defaultFollowCamera, baselinePreset); - if (!restoreResult.succeeded() || !comparePresetToCameraStrict(defaultFollowCamera, baselinePreset)) - return fail("Default follow smoke failed to restore the baseline preset for camera \"" + std::string(defaultFollowCamera->getIdentifier()) + "\"."); - } - - if (freeCamera) - { - const auto baselinePreset = nbl::core::capturePreset(m_cameraGoalSolver, freeCamera, "free-follow-baseline"); - SCameraFollowConfig followConfig = {}; - followConfig.enabled = true; - followConfig.mode = ECameraFollowMode::LookAtTarget; - - nbl::system::SCameraFollowApplyValidationResult lookAtResult = {}; - if (!buildAndValidateFollowTargetContract(freeCamera, trackedTarget, followConfig, "free look-at follow", lookAtResult)) - return false; - if (!verifyFollowVisualMetrics(freeCamera, trackedTarget, followConfig, "free look-at follow")) - return false; - if (!verifyFollowTargetMarkerAlignment(trackedTarget, "free look-at follow")) - return false; - - const auto restoreResult = nbl::core::applyPresetDetailed(m_cameraGoalSolver, freeCamera, baselinePreset); - if (!restoreResult.succeeded() || !comparePresetToCameraStrict(freeCamera, baselinePreset)) - return fail("Free follow smoke failed to restore the baseline preset."); - - followConfig.mode = ECameraFollowMode::KeepWorldOffset; - followConfig.worldOffset = float64_t3(5.0, -2.0, 1.5); - trackedTarget.setPose(movedTrackedTargetPosition, movedTrackedTargetOrientation); - - nbl::system::SCameraFollowApplyValidationResult keepWorldResult = {}; - if (!buildAndValidateFollowTargetContract(freeCamera, trackedTarget, followConfig, "free keep-world-offset follow", keepWorldResult)) - return false; - if (!verifyFollowVisualMetrics(freeCamera, trackedTarget, followConfig, "free keep-world-offset follow")) - return false; - - const auto restoreWorldOffsetResult = nbl::core::applyPresetDetailed(m_cameraGoalSolver, freeCamera, baselinePreset); - if (!restoreWorldOffsetResult.succeeded() || !comparePresetToCameraStrict(freeCamera, baselinePreset)) - return fail("Free keep-world-offset smoke failed to restore the baseline preset."); - } - - if (chaseCamera) - { - const auto baselinePreset = nbl::core::capturePreset(m_cameraGoalSolver, chaseCamera, "chase-follow-baseline"); - SCameraFollowConfig followConfig = {}; - followConfig.enabled = true; - followConfig.mode = ECameraFollowMode::KeepLocalOffset; - if (!nbl::core::captureFollowOffsetsFromCamera(m_cameraGoalSolver, chaseCamera, trackedTarget, followConfig)) - return fail("Chase follow smoke failed to capture local offset."); - - trackedTarget.setPose(movedTrackedTargetPosition, movedTrackedTargetOrientation); - - nbl::system::SCameraFollowApplyValidationResult localOffsetResult = {}; - if (!buildAndValidateFollowTargetContract(chaseCamera, trackedTarget, followConfig, "chase local-offset follow", localOffsetResult)) - return false; - if (!verifyFollowVisualMetrics(chaseCamera, trackedTarget, followConfig, "chase local-offset follow")) - return false; - if (!verifyFollowTargetMarkerAlignment(trackedTarget, "chase local-offset follow")) - return false; - - const auto restoreResult = nbl::core::applyPresetDetailed(m_cameraGoalSolver, chaseCamera, baselinePreset); - if (!restoreResult.succeeded() || !comparePresetToCameraStrict(chaseCamera, baselinePreset)) - return fail("Chase follow smoke failed to restore the baseline preset."); - } - - if (!verifyOffsetFollowRecapture(chaseCamera, trackedTarget, "chase follow recapture")) - return false; - if (!verifyOffsetFollowRecapture(dollyCamera, trackedTarget, "dolly follow recapture")) - return false; - } - - if (hasOrbitPreset && hasChasePreset) - { - if (!verifyExactCrossKindApply(orbitCamera, initialChasePreset, "Chase->Orbit")) - return false; - if (!verifyExactCrossKindApply(chaseCamera, initialOrbitPreset, "Orbit->Chase")) - return false; - } - - if (hasOrbitPreset && hasDollyPreset) - { - if (!verifyExactCrossKindApply(orbitCamera, initialDollyPreset, "Dolly->Orbit")) - return false; - if (!verifyExactCrossKindApply(dollyCamera, initialOrbitPreset, "Orbit->Dolly")) - return false; - } - - if (hasOrbitPreset && hasPathPreset && orbitCamera) - { - if (!verifyApproximateCrossKindApply( - orbitCamera, - initialPathPreset, - CCameraGoalSolver::SApplyResult::MissingPathState, - "Path->Orbit")) - { - return false; - } - } - - if (hasOrbitPreset && hasDollyZoomPreset && orbitCamera) - { - if (!verifyApproximateCrossKindApply( - orbitCamera, - initialDollyZoomPreset, - CCameraGoalSolver::SApplyResult::MissingDynamicPerspectiveState, - "DollyZoom->Orbit")) - { - return false; - } - } - - if (hasOrbitPreset) - { - if (std::string_view(nbl::ui::getPresetApplyPresentationFilterLabel(EPresetApplyPresentationFilter::All)) != "All" || - std::string_view(nbl::ui::getPresetApplyPresentationFilterLabel(EPresetApplyPresentationFilter::Exact)) != "Exact" || - std::string_view(nbl::ui::getPresetApplyPresentationFilterLabel(EPresetApplyPresentationFilter::BestEffort)) != "Best-effort") - { - return fail("Presentation utilities smoke returned an unexpected filter label."); - } - - const auto blockedPresentation = nbl::ui::analyzePresetPresentation(m_cameraGoalSolver, nullptr, initialOrbitPreset); - if (blockedPresentation.matchesFilter(EPresetApplyPresentationFilter::Exact) || - blockedPresentation.matchesFilter(EPresetApplyPresentationFilter::BestEffort)) - { - return fail("Presentation utilities smoke allowed a null-camera preset through an exactness filter."); - } - if (blockedPresentation.sourceKindLabel.empty() || blockedPresentation.goalStateLabel.empty()) - return fail("Presentation utilities smoke produced empty blocked presentation labels."); - - const auto blockedBadges = nbl::ui::collectGoalApplyPresentationBadges(blockedPresentation); - if (!blockedBadges.blocked || blockedBadges.exact || blockedBadges.bestEffort || blockedPresentation.badges.blocked != blockedBadges.blocked) - return fail("Presentation utilities smoke produced wrong blocked badge flags."); - - if (orbitCamera) - { - const auto exactPresentation = nbl::ui::analyzePresetPresentation(m_cameraGoalSolver, orbitCamera, initialOrbitPreset); - if (!exactPresentation.matchesFilter(EPresetApplyPresentationFilter::All) || - !exactPresentation.matchesFilter(EPresetApplyPresentationFilter::Exact) || - exactPresentation.matchesFilter(EPresetApplyPresentationFilter::BestEffort)) - { - return fail("Presentation utilities smoke failed exact filtering."); - } - - const auto exactBadges = nbl::ui::collectGoalApplyPresentationBadges(exactPresentation); - if (!exactBadges.exact || exactBadges.bestEffort || exactBadges.dropsState || exactBadges.sharedStateOnly || exactBadges.blocked) - return fail("Presentation utilities smoke produced wrong exact badge flags."); - if (exactPresentation.sourceKindLabel.empty() || exactPresentation.goalStateLabel.empty()) - return fail("Presentation utilities smoke produced empty exact presentation labels."); - - const auto capturePresentation = nbl::ui::analyzeCapturePresentation(m_cameraGoalSolver, orbitCamera); - if (!capturePresentation.canCapture || capturePresentation.policyLabel.empty()) - return fail("Presentation utilities smoke failed orbit capture presentation."); - } - } - - if (hasOrbitPreset && hasPathPreset && orbitCamera) - { - const auto approximatePresentation = nbl::ui::analyzePresetPresentation(m_cameraGoalSolver, orbitCamera, initialPathPreset); - if (!approximatePresentation.matchesFilter(EPresetApplyPresentationFilter::All) || - approximatePresentation.matchesFilter(EPresetApplyPresentationFilter::Exact) || - !approximatePresentation.matchesFilter(EPresetApplyPresentationFilter::BestEffort)) - { - return fail("Presentation utilities smoke failed best-effort filtering."); - } - - const auto approximateBadges = nbl::ui::collectGoalApplyPresentationBadges(approximatePresentation); - if (approximateBadges.exact || !approximateBadges.bestEffort || !approximateBadges.dropsState || approximateBadges.sharedStateOnly || approximateBadges.blocked) - return fail("Presentation utilities smoke produced wrong best-effort badge flags."); - if (approximatePresentation.sourceKindLabel.empty() || approximatePresentation.goalStateLabel.empty()) - return fail("Presentation utilities smoke produced empty best-effort presentation labels."); - } - - { - std::vector sourcePresets; - if (hasOrbitPreset) - sourcePresets.push_back(initialOrbitPreset); - if (hasChasePreset) - sourcePresets.push_back(initialChasePreset); - if (hasDollyPreset) - sourcePresets.push_back(initialDollyPreset); - if (hasPathPreset) - sourcePresets.push_back(initialPathPreset); - if (hasDollyZoomPreset) - sourcePresets.push_back(initialDollyZoomPreset); - - if (sourcePresets.empty()) - return fail("Preset persistence smoke failed to collect source presets."); - - std::stringstream presetBuffer; - if (!nbl::system::writePresetCollection(presetBuffer, std::span(sourcePresets.data(), sourcePresets.size()))) - return fail("Preset persistence smoke failed to serialize preset collection."); - - std::vector loadedPresets; - if (!nbl::system::readPresetCollection(presetBuffer, loadedPresets)) - return fail("Preset persistence smoke failed to deserialize preset collection."); - if (!nbl::core::comparePresetCollections( - std::span(sourcePresets.data(), sourcePresets.size()), - std::span(loadedPresets.data(), loadedPresets.size()), - 1e-6, 0.1, 1e-6)) - { - return fail("Preset persistence smoke changed stream preset collection content."); - } - - CCameraKeyframeTrack sourceTrack; - sourceTrack.keyframes.reserve(sourcePresets.size()); - for (size_t i = 0u; i < sourcePresets.size(); ++i) - { - CameraKeyframe 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; - - std::stringstream keyframeBuffer; - if (!nbl::system::writeKeyframeTrack(keyframeBuffer, sourceTrack)) - return fail("Keyframe persistence smoke failed to serialize track."); - - CCameraKeyframeTrack loadedTrack; - if (!nbl::system::readKeyframeTrack(keyframeBuffer, loadedTrack)) - return fail("Keyframe persistence smoke failed to deserialize track."); - if (!nbl::system::compareKeyframeTrackContentWithStrictThresholds(sourceTrack, loadedTrack)) - return fail("Keyframe persistence smoke changed stream track content."); - - 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 (!nbl::system::savePresetCollectionToFile(presetFile, std::span(sourcePresets.data(), sourcePresets.size()))) - return fail("Preset persistence smoke failed to save preset collection file."); - - std::vector fileLoadedPresets; - if (!nbl::system::loadPresetCollectionFromFile(presetFile, fileLoadedPresets)) - return fail("Preset persistence smoke failed to load preset collection file."); - if (!nbl::core::comparePresetCollections( - std::span(sourcePresets.data(), sourcePresets.size()), - std::span(fileLoadedPresets.data(), fileLoadedPresets.size()), - 1e-6, 0.1, 1e-6)) - { - return fail("Preset persistence smoke changed file preset collection content."); - } - - if (!nbl::system::saveKeyframeTrackToFile(keyframeFile, sourceTrack)) - return fail("Keyframe persistence smoke failed to save track file."); - - CCameraKeyframeTrack fileLoadedTrack; - if (!nbl::system::loadKeyframeTrackFromFile(keyframeFile, fileLoadedTrack)) - return fail("Keyframe persistence smoke failed to load track file."); - if (!nbl::system::compareKeyframeTrackContentWithStrictThresholds(sourceTrack, fileLoadedTrack)) - return fail("Keyframe persistence smoke changed file track content."); - } - - if (hasOrbitPreset && hasDollyPreset) - { - CCameraKeyframeTrack playbackTrack; - { - CameraKeyframe a; - a.time = 0.f; - a.preset = initialOrbitPreset; - playbackTrack.keyframes.push_back(a); - } - { - CameraKeyframe b; - b.time = 2.f; - b.preset = initialDollyPreset; - playbackTrack.keyframes.push_back(b); - } - - CCameraPlaybackCursor cursor; - cursor.playing = true; - cursor.loop = false; - cursor.speed = 1.f; - cursor.time = 1.5f; - - const auto advanceToEnd = nbl::core::advancePlaybackCursor(cursor, playbackTrack, 1.0); - if (!advanceToEnd.hasTrack || !advanceToEnd.changedTime || !advanceToEnd.reachedEnd || !advanceToEnd.stopped || advanceToEnd.wrapped) - return fail("Playback timeline smoke failed for non-loop end-of-track advance."); - if (std::abs(static_cast(advanceToEnd.time - 2.f)) > 1e-6) - return fail("Playback timeline smoke produced wrong end-of-track time."); - - nbl::core::resetPlaybackCursor(cursor, 1.25f); - if (cursor.playing || std::abs(static_cast(cursor.time - 1.25f)) > 1e-6) - return fail("Playback timeline smoke failed to reset cursor."); - - cursor.playing = true; - cursor.loop = true; - cursor.speed = 1.f; - cursor.time = 1.5f; - const auto advanceLoop = nbl::core::advancePlaybackCursor(cursor, playbackTrack, 1.0); - if (!advanceLoop.hasTrack || !advanceLoop.changedTime || !advanceLoop.wrapped || advanceLoop.stopped || advanceLoop.reachedEnd) - return fail("Playback timeline smoke failed for looped advance."); - if (std::abs(static_cast(advanceLoop.time - 0.5f)) > 1e-6) - return fail("Playback timeline smoke produced wrong wrapped time."); - - cursor.time = 9.f; - nbl::core::clampPlaybackCursorToTrack(playbackTrack, cursor); - if (std::abs(static_cast(cursor.time - 2.f)) > 1e-6) - return fail("Playback timeline smoke failed to clamp cursor time."); - } - - if (hasOrbitPreset) - { - CCameraSequenceScript sequence; - sequence.fps = 4.f; - sequence.defaults.durationSeconds = 2.f; - sequence.defaults.presentations = { - { .projection = IPlanarProjection::CProjection::Perspective, .leftHanded = true }, - { .projection = IPlanarProjection::CProjection::Orthographic, .leftHanded = false } - }; - sequence.defaults.captureFractions = { 0.f, 0.5f, 1.f }; - - CCameraSequenceSegment segment; - segment.name = "sequence_compile_smoke"; - segment.cameraKind = ICamera::CameraKind::Orbit; - { - CCameraSequenceKeyframe keyframe; - keyframe.time = 0.f; - keyframe.hasAbsolutePreset = true; - keyframe.absolutePreset = initialOrbitPreset; - segment.keyframes.push_back(keyframe); - } - { - nbl::core::CCameraSequenceTrackedTargetKeyframe keyframe; - keyframe.time = 0.f; - keyframe.hasAbsolutePosition = true; - keyframe.absolutePosition = float64_t3(1.0, 2.0, 3.0); - segment.targetKeyframes.push_back(keyframe); - } - { - nbl::core::CCameraSequenceTrackedTargetKeyframe keyframe; - keyframe.time = 1.f; - keyframe.hasAbsolutePosition = true; - keyframe.absolutePosition = float64_t3(4.0, 5.0, 6.0); - segment.targetKeyframes.push_back(keyframe); - } - { - nbl::core::CCameraSequenceTrackedTargetKeyframe keyframe; - keyframe.time = 1.f; - keyframe.hasAbsolutePosition = true; - keyframe.absolutePosition = float64_t3(7.0, 8.0, 9.0); - segment.targetKeyframes.push_back(keyframe); - } - sequence.segments.push_back(segment); - - if (!nbl::core::sequenceScriptUsesMultiplePresentations(sequence)) - return fail("Sequence compile smoke failed to detect multi-presentation authored defaults."); - - const CCameraSequenceTrackedTargetPose referenceTrackedTargetPose = { - .position = getDefaultFollowTargetPosition(), - .orientation = getDefaultFollowTargetOrientation() - }; - - nbl::core::CCameraSequenceCompiledSegment compiledSegment; - std::string compileError; - if (!nbl::core::compileSequenceSegmentFromReference( - sequence, - sequence.segments.front(), - initialOrbitPreset, - referenceTrackedTargetPose, - compiledSegment, - &compileError)) - { - return fail("Sequence compile smoke failed to compile a shared segment. " + compileError); - } - - if (compiledSegment.durationFrames != 8ull || compiledSegment.sampleTimes.size() != 8u) - return fail("Sequence compile smoke produced wrong sampled frame count."); - if (compiledSegment.captureFrameOffsets.size() != 3u || - compiledSegment.captureFrameOffsets[0] != 0ull || - compiledSegment.captureFrameOffsets[1] != 4ull || - compiledSegment.captureFrameOffsets[2] != 7ull) - { - return fail("Sequence compile smoke produced wrong capture frame offsets."); - } - if (compiledSegment.presentations.size() != 2u) - return fail("Sequence compile smoke lost authored presentations."); - if (!compiledSegment.usesTrackedTargetTrack() || compiledSegment.trackedTargetTrack.keyframes.size() != 2u) - return fail("Sequence compile smoke failed to normalize tracked-target keyframes."); - - std::vector framePolicies; - if (!nbl::core::buildCompiledSegmentFramePolicies(compiledSegment, framePolicies, true)) - return fail("Sequence compile smoke failed to build shared frame policies."); - if (framePolicies.size() != 8u) - return fail("Sequence compile smoke produced wrong frame-policy count."); - if (!framePolicies[0].baseline || framePolicies[0].continuityStep || !framePolicies[0].capture) - return fail("Sequence compile smoke produced wrong first-frame policy."); - if (!framePolicies[1].continuityStep || !framePolicies[1].followTargetLock || framePolicies[1].baseline) - return fail("Sequence compile smoke produced wrong continuity follow policy."); - if (!framePolicies[4].capture || !framePolicies[7].capture) - return fail("Sequence compile smoke produced wrong capture milestone policy."); - - CCameraSequenceTrackedTargetPose poseAtOne; - if (!nbl::core::tryBuildSequenceTrackedTargetPoseAtTime(compiledSegment.trackedTargetTrack, 1.f, poseAtOne)) - return fail("Sequence compile smoke failed to sample normalized tracked-target track."); - if (length(poseAtOne.position - float64_t3(7.0, 8.0, 9.0)) > 1e-9) - return fail("Sequence compile smoke did not keep the last authored target pose for duplicate keyframe time."); - - CCameraScriptedTimeline scriptedTimeline; - std::string runtimeBuildError; - if (!nbl::system::appendCompiledSequenceSegmentToScriptedTimeline( - scriptedTimeline, - 11u, - compiledSegment, - { - .planarIx = 5u, - .availableWindowCount = 2u, - .useWindow = true, - .includeFollowTargetLock = true - }, - &runtimeBuildError)) - { - return fail("Sequence runtime builder smoke failed to append a compiled segment. " + runtimeBuildError); - } - nbl::system::finalizeScriptedTimeline(scriptedTimeline); - - if (scriptedTimeline.captureFrames.size() != 3u || - scriptedTimeline.captureFrames[0] != 11ull || - scriptedTimeline.captureFrames[1] != 15ull || - scriptedTimeline.captureFrames[2] != 18ull) - { - return fail("Sequence runtime builder smoke produced wrong capture frames."); - } - - 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 != 1u || stepChecks != 7u || followChecks != 7u) - return fail("Sequence runtime builder smoke produced wrong scripted check counts."); - - size_t runtimeNextEventIndex = 0u; - CCameraScriptedFrameEvents runtimeBatch; - nbl::system::dequeueScriptedFrameEvents(scriptedTimeline.events, runtimeNextEventIndex, 11u, runtimeBatch); - if (runtimeBatch.actions.size() != 10u || runtimeBatch.goals.size() != 1u || - runtimeBatch.trackedTargetTransforms.size() != 1u || runtimeBatch.segmentLabels.size() != 1u) - { - return fail("Sequence runtime builder smoke produced wrong first-frame batch."); - } - if (runtimeBatch.actions.front().kind != CCameraScriptedInputEvent::ActionData::Kind::SetActiveRenderWindow || - runtimeBatch.segmentLabels.front() != "sequence_compile_smoke") - { - return fail("Sequence runtime builder smoke lost first-frame scripted payload."); - } - } - - if (hasOrbitPreset && orbitCamera) - { - std::array exactTargets = { orbitCamera, nullptr }; - const auto exactSummary = nbl::core::applyPresetToCameraRange( - m_cameraGoalSolver, - std::span(exactTargets.data(), exactTargets.size()), - initialOrbitPreset); - if (exactSummary.targetCount != 1u || exactSummary.successCount != 1u || exactSummary.approximateCount != 0u || exactSummary.failureCount != 0u) - return fail("Preset apply summary smoke failed for exact target range."); - } - - if (hasPathPreset && orbitCamera) - { - std::array approximateTargets = { orbitCamera }; - const auto approximateSummary = nbl::core::applyPresetToCameraRange( - m_cameraGoalSolver, - std::span(approximateTargets.data(), approximateTargets.size()), - initialPathPreset); - if (approximateSummary.targetCount != 1u || approximateSummary.successCount != 1u || approximateSummary.approximateCount != 1u || approximateSummary.failureCount != 0u) - return fail("Preset apply summary smoke failed for approximate target range."); - } - - { - 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::scaleVirtualEvents(scaledEvents, static_cast(scaledEvents.size()), 0.5f, 2.0f); - if (std::abs(scaledEvents[0].magnitude - 1.0) > 1e-9 || - std::abs(scaledEvents[1].magnitude - 6.0) > 1e-9 || - std::abs(scaledEvents[2].magnitude - 4.0) > 1e-9) - { - return fail("Camera manipulation utilities smoke failed for virtual-event scaling."); - } - } - - if (hasFreePreset && freeCamera) - { - CameraPreset orientedPreset = initialFreePreset; - orientedPreset.goal.orientation = makeQuaternionFromEulerDegrees(float64_t3(0.0, 90.0, 0.0)); - const auto orientResult = nbl::core::applyPresetDetailed(m_cameraGoalSolver, freeCamera, orientedPreset); - if (!orientResult.succeeded() || !comparePresetToCameraStrict(freeCamera, orientedPreset)) - return fail("Camera manipulation utilities smoke failed to orient Free camera before translation remap."); - - std::vector worldTranslationEvents(3u); - worldTranslationEvents[0].type = CVirtualGimbalEvent::MoveRight; - worldTranslationEvents[0].magnitude = 1.25; - worldTranslationEvents[1].type = CVirtualGimbalEvent::MoveUp; - worldTranslationEvents[1].magnitude = 0.5; - worldTranslationEvents[2].type = CVirtualGimbalEvent::MoveForward; - worldTranslationEvents[2].magnitude = 2.0; - uint32_t remappedCount = static_cast(worldTranslationEvents.size()); - nbl::core::remapTranslationEventsFromWorldToCameraLocal(freeCamera, worldTranslationEvents, remappedCount); - if (remappedCount == 0u) - return fail("Camera manipulation utilities smoke produced empty translation remap."); - - if (!freeCamera->manipulate({ worldTranslationEvents.data(), remappedCount })) - return fail("Camera manipulation utilities smoke failed to apply remapped translation."); - - const auto remappedPosition = freeCamera->getGimbal().getPosition(); - const auto positionDelta = remappedPosition - orientedPreset.goal.position; - const float64_t3 expectedWorldDelta(1.25, 0.5, 2.0); - if (!hlsl::nearlyEqualVec3(positionDelta, expectedWorldDelta, 1e-6)) - return fail("Camera manipulation utilities smoke changed world-space translation semantics."); - - CameraPreset pitchPreset = initialFreePreset; - pitchPreset.goal.orientation = makeQuaternionFromEulerDegrees(float64_t3(60.0, 0.0, 0.0)); - const auto pitchResult = nbl::core::applyPresetDetailed(m_cameraGoalSolver, freeCamera, pitchPreset); - if (!pitchResult.succeeded()) - return fail("Camera manipulation utilities smoke failed to prepare Free camera pitch clamp."); - - SCameraConstraintSettings freeConstraints; - freeConstraints.enabled = true; - freeConstraints.clampPitch = true; - freeConstraints.pitchMinDeg = -15.f; - freeConstraints.pitchMaxDeg = 15.f; - if (!nbl::core::applyCameraConstraints(m_cameraGoalSolver, freeCamera, freeConstraints)) - return fail("Camera manipulation utilities smoke failed to clamp Free camera orientation."); - - const auto freeEulerDeg = getQuaternionEulerDegrees(freeCamera->getGimbal().getOrientation()); - if (std::abs(static_cast(freeEulerDeg.x - 15.f)) > 0.1) - return fail("Camera manipulation utilities smoke produced wrong clamped Free camera pitch."); - - const auto restoreFree = nbl::core::applyPresetDetailed(m_cameraGoalSolver, freeCamera, initialFreePreset); - if (!restoreFree.succeeded() || !comparePresetToCameraStrict(freeCamera, initialFreePreset)) - return fail("Camera manipulation utilities smoke failed to restore Free camera baseline."); - } - - if (hasOrbitPreset && orbitCamera && initialOrbitPreset.goal.hasDistance) - { - CameraPreset farOrbitPreset = initialOrbitPreset; - farOrbitPreset.goal.distance = initialOrbitPreset.goal.distance + 10.f; - const auto farOrbitResult = nbl::core::applyPresetDetailed(m_cameraGoalSolver, orbitCamera, farOrbitPreset); - if (!farOrbitResult.succeeded()) - return fail("Camera manipulation utilities smoke failed to prepare Orbit distance clamp."); - - SCameraConstraintSettings orbitConstraints; - orbitConstraints.enabled = true; - orbitConstraints.clampDistance = true; - orbitConstraints.minDistance = std::max(0.1f, initialOrbitPreset.goal.distance * 0.5f); - orbitConstraints.maxDistance = initialOrbitPreset.goal.distance * 0.75f; - if (!nbl::core::applyCameraConstraints(m_cameraGoalSolver, orbitCamera, orbitConstraints)) - return fail("Camera manipulation utilities smoke failed to clamp Orbit distance."); - - ICamera::SphericalTargetState clampedOrbitState; - if (!orbitCamera->tryGetSphericalTargetState(clampedOrbitState) || - std::abs(static_cast(clampedOrbitState.distance - orbitConstraints.maxDistance)) > 1e-6) - { - return fail("Camera manipulation utilities smoke produced wrong clamped Orbit distance."); - } - - const auto restoreOrbit = nbl::core::applyPresetDetailed(m_cameraGoalSolver, orbitCamera, initialOrbitPreset); - if (!restoreOrbit.succeeded() || !comparePresetToCameraStrict(orbitCamera, initialOrbitPreset)) - return fail("Camera manipulation utilities smoke failed to restore Orbit baseline."); - } - - if (hasDollyZoomPreset && dollyZoomCamera) - { - float dynamicFov = 0.0f; - if (!dollyZoomCamera->tryGetDynamicPerspectiveFov(dynamicFov)) - return fail("Camera projection utilities smoke failed to query DollyZoom dynamic FOV."); - - auto perspectiveProjection = IPlanarProjection::CProjection::create(0.1f, 100.f, 60.f); - if (!nbl::core::syncDynamicPerspectiveProjection(dollyZoomCamera, perspectiveProjection)) - return fail("Camera projection utilities smoke failed to sync dynamic perspective projection."); - if (std::abs(static_cast(perspectiveProjection.getParameters().m_planar.perspective.fov - dynamicFov)) > 1e-6) - return fail("Camera projection utilities smoke produced wrong dynamic perspective FOV."); - - auto orthographicProjection = IPlanarProjection::CProjection::create(0.1f, 100.f, 10.f); - if (nbl::core::syncDynamicPerspectiveProjection(dollyZoomCamera, orthographicProjection)) - return fail("Camera projection utilities smoke unexpectedly synced orthographic projection."); - } - - { - if (getCameraTypeLabel(ICamera::CameraKind::DollyZoom) != "Dolly Zoom") - return fail("Camera text utilities smoke failed for Dolly Zoom label."); - if (getCameraTypeDescription(ICamera::CameraKind::Path) != "Move along a target path") - return fail("Camera text utilities smoke failed for Path description."); - if (describeGoalStateMask(ICamera::GoalStateNone) != "Pose only") - return fail("Camera text utilities smoke failed for empty goal-state description."); - if (describeGoalStateMask(ICamera::GoalStateSphericalTarget | ICamera::GoalStateDynamicPerspective) != "Spherical target, Dynamic perspective") - return fail("Camera text utilities smoke failed for combined goal-state description."); - - CCameraGoalSolver::SApplyResult defaultApplyResult; - const auto applyResultText = describeApplyResult(defaultApplyResult); - if (applyResultText.find("status=Unsupported") == std::string::npos || applyResultText.find("events=0") == std::string::npos) - return fail("Camera text utilities smoke failed for apply-result description."); - - SCameraPresetApplySummary summary; - summary.targetCount = 2u; - summary.successCount = 2u; - summary.approximateCount = 1u; - const auto summaryText = nbl::ui::describePresetApplySummary(summary, "none"); - if (summaryText.find("targets=2") == std::string::npos || summaryText.find("approximate=1") == std::string::npos) - return fail("Camera text utilities smoke failed for preset-apply summary description."); - } - - m_headlessCameraSmokePassed = true; - std::cout << "[headless-camera-smoke] PASS cameras=" << cameras.size() << std::endl; - return true; - } - - m_ciMode = program.get("--ci"); - if (m_ciMode) - { - m_ciScreenshotPath = localOutputCWD / "cameraz_ci.png"; - m_ciStartedAt = clock_t::now(); - } - m_scriptedInput.log = program.get("--script-log"); - m_scriptVisualDebugCli = program.get("--script-visual-debug"); - m_disableScreenshotsCli = program.get("--no-screenshots"); - - // Create imput system - 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 (!asset_base_t::onAppInitialized(std::move(system))) - return false; - - { - const std::optional cameraJsonFile = program.is_used("--file") ? std::optional(program.get("--file")) : std::optional(std::nullopt); - - camera_json_t j; - auto loadDefaultConfig = [&]() -> bool - { - const auto configPath = std::filesystem::path(nbl::system::SCameraAppResourcePaths::DefaultCameraConfigRelativePath); - std::string jsonError; - if (!nbl::system::loadJsonFromPath(*m_system, configPath, j, &jsonError)) - return logFail("%s", jsonError.c_str()); - return true; - }; - - std::string jsonError; - std::filesystem::path resolvedCameraJsonFile; - const bool hasUserConfig = cameraJsonFile.has_value(); - if (hasUserConfig && nbl::system::loadJsonFromMountedResourceOrResolvedPath(*m_system, localInputCWD, cameraJsonFile.value(), j, &resolvedCameraJsonFile, &jsonError)) - { - // Loaded from mounted resources alias or explicit filesystem override. - } - else - { - if (hasUserConfig) - m_logger->log("Cannot open input \"%s\" json file (%s). Switching to default config.", ILogger::ELL_WARNING, cameraJsonFile.value().string().c_str(), jsonError.c_str()); - else - m_logger->log("No input json file provided. Switching to default config.", ILogger::ELL_INFO); - - if (!loadDefaultConfig()) - return false; - } - - std::optional pendingScriptedSequence; - bool scriptedInputParseFailed = false; - std::string scriptedInputParseError; - - auto resetScriptedInputRuntimeState = [&]() -> void - { - m_scriptedInput.nextEventIndex = 0; - m_scriptedInput.checkRuntime = {}; - m_scriptedInput.nextCaptureIndex = 0; - m_scriptedInput.failed = false; - m_scriptedInput.summaryReported = false; - }; - - auto finalizeScriptedInput = [&]() -> void - { - nbl::system::finalizeScriptedTimeline(m_scriptedInput.timeline, m_disableScreenshotsCli); - }; - - auto applyParsedScriptedInput = [&](nbl::system::CCameraScriptedInputParseResult parsed) -> void - { - pendingScriptedSequence.reset(); - scriptedInputParseFailed = false; - scriptedInputParseError.clear(); - m_scriptedInput.timeline.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.visualActivePlanarValid = false; - m_scriptedInput.visualActivePlanarIx = 0u; - m_scriptedInput.visualActivePlanarStartFrame = 0u; - m_scriptedInput.scriptedLeftMouseDown = false; - m_scriptedInput.scriptedRightMouseDown = false; - m_scriptedInput.framePacerInitialized = false; - m_scriptedInput.capturePrefix = "script"; - 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_scriptVisualDebugCli) - m_scriptedInput.visualDebug = true; - if (m_scriptedInput.visualDebug) - { - if (m_scriptedInput.visualTargetFps <= 0.f) - m_scriptedInput.visualTargetFps = 60.f; - if (m_scriptedInput.visualCameraHoldSeconds <= 0.f) - m_scriptedInput.visualCameraHoldSeconds = 3.f; - } - - if (parsed.hasEnableActiveCameraMovement) - enableActiveCameraMovement = parsed.enableActiveCameraMovement; - else if (m_scriptedInput.enabled) - enableActiveCameraMovement = true; - - m_scriptedInput.exclusive = parsed.exclusive; - m_scriptedInput.capturePrefix = parsed.capturePrefix.empty() ? "script" : 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); - finalizeScriptedInput(); - }; - - if (program.is_used("--script")) - { - nbl::system::path scriptPath = program.get("--script"); - std::string scriptedInputText; - if (!nbl::system::loadTextFromMountedResourceOrResolvedPath(*m_system, localInputCWD, scriptPath, scriptedInputText, &scriptPath)) - { - logFail("Camera sequence script parse failed: Cannot open scripted input file."); - return false; - } - - std::stringstream scriptedInputStream(scriptedInputText); - nbl::system::CCameraScriptedInputParseResult parsed; - if (!nbl::system::readCameraScriptedInput(scriptedInputStream, parsed, &scriptedInputParseError)) - { - logFail("Camera sequence script parse failed: %s", scriptedInputParseError.c_str()); - return false; - } - applyParsedScriptedInput(std::move(parsed)); - } - else if (j.contains("scripted_input")) - { - std::stringstream scriptedInputStream; - scriptedInputStream << j["scripted_input"].dump(); - nbl::system::CCameraScriptedInputParseResult parsed; - if (!nbl::system::readCameraScriptedInput(scriptedInputStream, parsed, &scriptedInputParseError)) - scriptedInputParseFailed = true; - else - applyParsedScriptedInput(std::move(parsed)); - if (scriptedInputParseFailed) - { - logFail("Camera sequence script parse failed: %s", scriptedInputParseError.c_str()); - return false; - } - } - - std::vector> cameras; - std::string cameraConfigError; - if (!nbl::system::tryLoadCameraCollectionFromJson(j, cameraConfigError, cameras)) - { - logFail("%s", cameraConfigError.c_str()); - return false; - } - - std::vector projections; - for (const auto& jProjection : j["projections"]) - { - if (jProjection.contains("type")) - { - float zNear, zFar; - - if (!jProjection.contains("zNear")) - { - logFail("Expected \"zNear\" keyword for planar projection definition!"); - return false; - } - - if (!jProjection.contains("zFar")) - { - logFail("Expected \"zFar\" keyword for planar projection definition!"); - return false; - } - - zNear = jProjection["zNear"].get(); - zFar = jProjection["zFar"].get(); - - if (jProjection["type"] == "perspective") - { - if (!jProjection.contains("fov")) - { - logFail("Expected \"fov\" keyword for planar perspective projection definition!"); - return false; - } - - float fov = jProjection["fov"].get(); - projections.emplace_back(IPlanarProjection::CProjection::create(zNear, zFar, fov)); - } - else if (jProjection["type"] == "orthographic") - { - if (!jProjection.contains("orthoWidth")) - { - logFail("Expected \"orthoWidth\" keyword for planar orthographic projection definition!"); - return false; - } - - float orthoWidth = jProjection["orthoWidth"].get(); - projections.emplace_back(IPlanarProjection::CProjection::create(zNear, zFar, orthoWidth)); - } - else - { - logFail("Unsupported projection!"); - return false; - } - } - } - - struct - { - std::vector keyboard; - std::vector mouse; - } bindings; - - const char* bindingLayoutsKey = j.contains("bindings") ? "bindings" : nullptr; - - if (bindingLayoutsKey) - { - const auto& jBindings = j[bindingLayoutsKey]; - - if (jBindings.contains("keyboard")) - { - for (const auto& jKeyboard : jBindings["keyboard"]) - { - if (jKeyboard.contains("mappings")) - { - auto& binding = bindings.keyboard.emplace_back(); - for (const auto& [key, value] : jKeyboard["mappings"].items()) - { - const auto nativeCode = stringToKeyCode(key.c_str()); - - if (nativeCode == EKC_NONE) - { - logFail("Invalid native key \"%s\" code mapping for keyboard binding", key.c_str()); - return false; - } - - binding[nativeCode] = CVirtualGimbalEvent::stringToVirtualEvent(value.get()); - } - } - else - { - logFail("Expected \"mappings\" keyword for keyboard binding definition!"); - return false; - } - } - } - else - { - logFail("Expected \"keyboard\" keyword in bindings definition!"); - return false; - } - - if (jBindings.contains("mouse")) - { - for (const auto& jMouse : jBindings["mouse"]) - { - if (jMouse.contains("mappings")) - { - auto& binding = bindings.mouse.emplace_back(); - for (const auto& [key, value] : jMouse["mappings"].items()) - { - const auto nativeCode = stringToMouseCode(key.c_str()); - - if (nativeCode == EMC_NONE) - { - logFail("Invalid native key \"%s\" code mapping for mouse binding", key.c_str()); - return false; - } - - binding[nativeCode] = CVirtualGimbalEvent::stringToVirtualEvent(value.get()); - } - } - else - { - logFail("Expected \"mappings\" keyword for mouse binding definition!"); - return false; - } - } - } - else - { - logFail("Expected \"mouse\" keyword in bindings definition"); - return false; - } - } - else - { - logFail("Expected \"bindings\" keyword in camera JSON"); - return false; - } - - if (j.contains("viewports") && j.contains("planars")) - { - for (const auto& jPlanar : j["planars"]) - { - if (!jPlanar.contains("camera")) - { - logFail("Expected \"camera\" value in planar object"); - return false; - } - - if (!jPlanar.contains("viewports")) - { - logFail("Expected \"viewports\" list in planar object"); - return false; - } - - const auto cameraIx = jPlanar["camera"].get(); - auto boundViewports = jPlanar["viewports"].get>(); - - auto& planar = m_planarProjections.emplace_back() = planar_projection_t::create(smart_refctd_ptr(cameras[cameraIx])); - for (const auto viewportIx : boundViewports) - { - auto& viewport = j["viewports"][viewportIx]; - const char* viewportBindingsKey = viewport.contains("bindings") ? "bindings" : nullptr; - if (!viewport.contains("projection") || !viewportBindingsKey) - { - logFail("\"projection\" or \"bindings\" missing in viewport object index %d", viewportIx); - return false; - } - - const auto projectionIx = viewport["projection"].get(); - auto& projection = planar->getPlanarProjections().emplace_back(projections[projectionIx]); - auto& projectionBinding = projection.getInputBinding(); - const auto& jViewportBindings = viewport[viewportBindingsKey]; - - const bool hasKeyboardBound = jViewportBindings.contains("keyboard"); - const bool hasMouseBound = jViewportBindings.contains("mouse"); - - if (hasKeyboardBound) - { - auto keyboardBindingIx = jViewportBindings["keyboard"].get(); - projectionBinding.updateKeyboardMapping([&](auto& map) { map = bindings.keyboard[keyboardBindingIx]; }); - } - else - projectionBinding.updateKeyboardMapping([&](auto& map) { map = {}; }); // clean the map if not bound - - if (hasMouseBound) - { - auto mouseBindingIx = jViewportBindings["mouse"].get(); - projectionBinding.updateMouseMapping([&](auto& map) { map = bindings.mouse[mouseBindingIx]; }); - } - else - projectionBinding.updateMouseMapping([&](auto& map) { map = {}; }); // clean the map if not bound - } - - } - } - else - { - logFail("Expected \"viewports\" and \"planars\" lists in JSON"); - return false; - } - - if (m_planarProjections.empty()) - { - logFail("Expected at least 1 planar"); - return false; - } - - // init render window planar references - we make all render windows start with focus on first - // planar but in a way that first window has the planar's perspective preset bound & second orthographic - for (uint32_t i = 0u; i < windowBindings.size(); ++i) - { - auto& binding = windowBindings[i]; - - auto& planar = m_planarProjections[binding.activePlanarIx = 0]; - binding.pickDefaultProjections(planar->getPlanarProjections()); - - if (i) - binding.boundProjectionIx = binding.lastBoundOrthoPresetProjectionIx.value(); - else - binding.boundProjectionIx = binding.lastBoundPerspectivePresetProjectionIx.value(); - } - - m_initialPlanarPresets.clear(); - m_initialPlanarPresets.reserve(m_planarProjections.size()); - for (uint32_t planarIx = 0u; planarIx < m_planarProjections.size(); ++planarIx) - { - auto* camera = m_planarProjections[planarIx]->getCamera(); - const std::string presetName = "Planar " + std::to_string(planarIx); - const auto captureAnalysis = nbl::core::analyzeCameraCapture(m_cameraGoalSolver, camera); - if (!captureAnalysis.canCapture) - { - const auto kindLabel = camera ? std::string(getCameraTypeLabel(camera->getKind())) : std::string("Unknown"); - const auto reason = !captureAnalysis.hasCamera ? "missing camera" : - (!captureAnalysis.capturedGoal ? "capture failed" : - (!captureAnalysis.finiteGoal ? "non-finite goal" : "unknown")); - return logFail("Failed to capture initial planar preset %u for camera kind \"%s\": %s", - planarIx, kindLabel.c_str(), reason); - } - - CameraPreset preset = {}; - if (!nbl::core::tryCapturePreset(captureAnalysis, camera, presetName, preset)) - return logFail("Failed to build initial planar preset %u for camera kind \"%s\".", - planarIx, - camera ? std::string(getCameraTypeLabel(camera->getKind())).c_str() : "Unknown"); - m_initialPlanarPresets.emplace_back(std::move(preset)); - } - - resetFollowTargetToDefault(); - m_planarFollowConfigs.clear(); - m_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 = makeDefaultFollowConfig(camera); - m_planarFollowConfigs.emplace_back(config); - if (config.enabled) - captureFollowOffsetsForPlanar(planarIx); - } - bindManipulatedModel(); - - if (pendingScriptedSequence.has_value()) - { - auto expandCameraSequenceScript = [&](const CCameraSequenceScript& sequence) -> bool - { - CCameraScriptedTimeline timeline; - resetScriptedInputRuntimeState(); - - auto resolvePlanarIx = [&](const CCameraSequenceSegment& segment) -> std::optional - { - 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; - }; - - const bool useWindow = nbl::core::sequenceScriptUsesMultiplePresentations(sequence); - nbl::system::appendScriptedActionEvent(timeline, 0u, CCameraScriptedInputEvent::ActionData::Kind::SetUseWindow, useWindow ? 1 : 0); - - const CCameraSequenceTrackedTargetPose referenceTrackedTargetPose = { - .position = getDefaultFollowTargetPosition(), - .orientation = getDefaultFollowTargetOrientation() - }; - uint64_t frameCursor = 0u; - for (const auto& segment : sequence.segments) - { - auto planarIx = resolvePlanarIx(segment); - if (!planarIx.has_value()) - { - const auto kindLabel = segment.cameraKind != ICamera::CameraKind::Unknown ? std::string(getCameraTypeLabel(segment.cameraKind)) : std::string("Unknown"); - 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()); - return false; - } - const bool useTrackedTargetFollow = nbl::core::sequenceSegmentUsesTrackedTargetTrack(segment) && - planarIx.value() < m_planarFollowConfigs.size() && - m_planarFollowConfigs[planarIx.value()].enabled && - m_planarFollowConfigs[planarIx.value()].mode != ECameraFollowMode::Disabled; - - nbl::core::CCameraSequenceCompiledSegment compiledSegment; - std::string trackError; - if (!nbl::core::compileSequenceSegmentFromReference( - sequence, - segment, - m_initialPlanarPresets[planarIx.value()], - referenceTrackedTargetPose, - compiledSegment, - &trackError)) - { - logFail("Sequence segment \"%s\" failed to compile: %s", segment.name.c_str(), trackError.c_str()); - return false; - } - - if (compiledSegment.presentations.size() > 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(), windowBindings.size()); - } - - std::string buildError; - if (!nbl::system::appendCompiledSequenceSegmentToScriptedTimeline( - timeline, - frameCursor, - compiledSegment, - { - .planarIx = planarIx.value(), - .availableWindowCount = windowBindings.size(), - .useWindow = useWindow, - .includeFollowTargetLock = useTrackedTargetFollow - }, - &buildError)) - { - logFail("Sequence segment \"%s\" failed to build scripted runtime data: %s", - segment.name.c_str(), buildError.c_str()); - return false; - } - - frameCursor += compiledSegment.durationFrames; - } - - nbl::system::finalizeScriptedTimeline(timeline, m_disableScreenshotsCli); - m_scriptedInput.timeline = std::move(timeline); - return true; - }; - - if (!expandCameraSequenceScript(*pendingScriptedSequence)) - return false; - } - } - - // First create the resources that don't depend on a swapchain - m_semaphore = m_device->createSemaphore(m_realFrameIx); - if (!m_semaphore) - return logFail("Failed to Create a Semaphore!"); - - // The nice thing about having a triple buffer is that you don't need to do acrobatics to account for the formats available to the surface. - // You can transcode to the swapchain's format while copying, and I actually recommend to do surface rotation, tonemapping and OETF application there. - const auto format = asset::EF_R8G8B8A8_SRGB; - // Could be more clever and use the copy Triple Buffer to Swapchain as an opportunity to do a MSAA resolve or something - const auto samples = IGPUImage::ESCF_1_BIT; - - // Create the renderpass - { - 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, // because we clear we don't care about contents when we grab the triple buffer img again - /*.finalLayout = */IGPUImage::LAYOUT::TRANSFER_SRC_OPTIMAL // put it already in the correct layout for the blit operation - }}, - IGPURenderpass::SCreationParams::ColorAttachmentsEnd - }; - IGPURenderpass::SCreationParams::SSubpassDescription subpasses[] = { - {}, - IGPURenderpass::SCreationParams::SubpassesEnd - }; - subpasses[0].colorAttachments[0] = { .render = {.attachmentIndex = 0,.layout = IGPUImage::LAYOUT::ATTACHMENT_OPTIMAL} }; - // We actually need external dependencies to ensure ordering of the Implicit Layout Transitions relative to the semaphore signals - IGPURenderpass::SCreationParams::SSubpassDependency dependencies[] = { - // wipe-transition to ATTACHMENT_OPTIMAL - { - .srcSubpass = IGPURenderpass::SCreationParams::SSubpassDependency::External, - .dstSubpass = 0, - .memoryBarrier = { - // we can have NONE as Sources because the semaphore wait is ALL_COMMANDS - // https://github.com/KhronosGroup/Vulkan-Docs/issues/2319 - .dstStageMask = asset::PIPELINE_STAGE_FLAGS::COLOR_ATTACHMENT_OUTPUT_BIT, - .dstAccessMask = asset::ACCESS_FLAGS::COLOR_ATTACHMENT_WRITE_BIT - } - // leave view offsets and flags default - }, - // ATTACHMENT_OPTIMAL to PRESENT_SRC - { - .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 - // we can have NONE as the Destinations because the semaphore signal is ALL_COMMANDS - // https://github.com/KhronosGroup/Vulkan-Docs/issues/2319 - } - // leave view offsets and flags default - }, - IGPURenderpass::SCreationParams::DependenciesEnd - }; - - IGPURenderpass::SCreationParams params = {}; - params.colorAttachments = colorAttachments; - params.subpasses = subpasses; - params.dependencies = dependencies; - m_renderpass = m_device->createRenderpass(params); - if (!m_renderpass) - return logFail("Failed to Create a Renderpass!"); - } - - // We just live life in easy mode and have the Swapchain Creation Parameters get deduced from the surface. - // We don't need any control over the format of the swapchain because we'll be only using Renderpasses this time! - ISwapchain::SSharedCreationParams sharedParams = {}; - sharedParams.imageUsage |= IGPUImage::EUF_TRANSFER_SRC_BIT; - auto swapchainResources = std::make_unique(); - if (!m_surface || !m_surface->init(m_surface->pickQueue(m_device.get()), std::move(swapchainResources), sharedParams)) - return logFail("Failed to Create a Swapchain!"); - - // Normally you'd want to recreate these images whenever the swapchain is resized in some increment, like 64 pixels or something. - // But I'm super lazy here and will just create "worst case sized images" and waste all the VRAM I can get. - const auto dpyInfo = m_winMgr->getPrimaryDisplayInfo(); - for (auto i = 0; i < MaxFramesInFlight; i++) - { - auto& image = m_tripleBuffers[i]; - { - IGPUImage::SCreationParams params = {}; - params = asset::IImage::SCreationParams{ - .type = IGPUImage::ET_2D, - .samples = samples, - .format = format, - .extent = {dpyInfo.resX,dpyInfo.resY,1}, - .mipLevels = 1, - .arrayLayers = 1, - .flags = IGPUImage::ECF_NONE, - // in this example I'll be using a renderpass to clear the image, and then a blit to copy it to the swapchain - .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!"); - - // use dedicated allocations, we have plenty of allocations left, even on Win32 - 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()); - - // create framebuffers for the images - { - auto imageView = m_device->createImageView({ - .flags = IGPUImageView::ECF_NONE, - // give it a Transfer SRC usage flag so we can transition to the Tranfer SRC layout with End Renderpass - .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); - } - } - - // This time we'll create all CommandBuffers from one CommandPool, to keep life simple. However the Pool must support individually resettable CommandBuffers - // because they cannot be pre-recorded because the fraembuffers/swapchain images they use will change when a swapchain recreates. - 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!"); - - // UI - { - { - 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 vertexKey = nbl::this_example::builtin::build::get_spirv_key<"imgui_vertex">(m_device.get()); - const auto fragmentKey = nbl::this_example::builtin::build::get_spirv_key<"imgui_fragment">(m_device.get()); - auto vertexShader = nbl::system::loadPrecompiledShaderFromAppResources(*m_assetMgr, m_logger.get(), vertexKey); - auto fragmentShader = nbl::system::loadPrecompiledShaderFromAppResources(*m_assetMgr, m_logger.get(), fragmentKey); - if (!vertexShader || !fragmentShader) - return logFail("Failed to load precompiled ImGui shaders."); - - params.spirv = nbl::ext::imgui::UI::SCreationParameters::PrecompiledShaders{ - .vertex = std::move(vertexShader), - .fragment = std::move(fragmentShader) - }; - - m_ui.manager = nbl::ext::imgui::UI::create(std::move(params)); - } - - if (!m_ui.manager) - return false; - - // note that we use default layout provided by our extension, but you are free to create your own by filling nbl::ext::imgui::UI::S_CREATION_PARAMETERS::resources - const auto* descriptorSetLayout = m_ui.manager->getPipeline()->getLayout()->getDescriptorSetLayout(0u); - - IDescriptorPool::SCreateInfo descriptorPoolInfo = {}; - descriptorPoolInfo.maxDescriptorCount[static_cast(asset::IDescriptor::E_TYPE::ET_SAMPLER)] = (uint32_t)nbl::ext::imgui::UI::DefaultSamplerIx::COUNT; - descriptorPoolInfo.maxDescriptorCount[static_cast(asset::IDescriptor::E_TYPE::ET_SAMPLED_IMAGE)] = TotalUISampleTexturesAmount; - descriptorPoolInfo.maxSets = 1u; - descriptorPoolInfo.flags = IDescriptorPool::E_CREATE_FLAGS::ECF_UPDATE_AFTER_BIND_BIT; - - m_descriptorSetPool = m_device->createDescriptorPool(std::move(descriptorPoolInfo)); - assert(m_descriptorSetPool); - - m_descriptorSetPool->createDescriptorSets(1u, &descriptorSetLayout, &m_ui.descriptorSet); - assert(m_ui.descriptorSet); - - m_ui.manager->registerListener([this]() -> void { imguiListen(); }); - { - const auto ds = float32_t2{ m_window->getWidth(), m_window->getHeight() }; - - wInit.trsEditor.iPos = iPaddingOffset; - wInit.trsEditor.iSize = { 0.0f, ds.y - wInit.trsEditor.iPos.y * 2 }; - - const float panelWidth = std::clamp(ds.x * 0.33f, 380.0f, ds.x * 0.48f); - wInit.planars.iSize = { panelWidth, ds.y - iPaddingOffset.y * 2 }; - wInit.planars.iPos = { ds.x - wInit.planars.iSize.x - iPaddingOffset.x, 0 + iPaddingOffset.y }; - - { - const float renderPaddingX = 0.0f; - const float renderPaddingY = 0.0f; - const float splitGap = 4.0f; - float leftX = renderPaddingX; - float eachXSize = std::max(0.0f, ds.x - 2.0f * renderPaddingX); - float eachYSize = (ds.y - 2.0f * renderPaddingY - (wInit.renderWindows.size() - 1) * splitGap) / wInit.renderWindows.size(); - - for (size_t i = 0; i < wInit.renderWindows.size(); ++i) - { - auto& rw = wInit.renderWindows[i]; - rw.iPos = { leftX, renderPaddingY + i * (eachYSize + splitGap) }; - rw.iSize = { eachXSize, eachYSize }; - } - } - } - } - - // Geometry Creator Render Scene FBOs - { - const uint32_t addtionalBufferOwnershipFamilies[] = { getGraphicsQueue()->getFamilyIndex() }; - m_scene = CGeometryCreatorScene::create( - { - .transferQueue = getTransferUpQueue(), - .utilities = m_utils.get(), - .logger = m_logger.get(), - .addtionalBufferOwnershipFamilies = addtionalBufferOwnershipFamilies - }, - CSimpleDebugRenderer::DefaultPolygonGeometryPatch - ); - - if (!m_scene) - return logFail("Could not create geometry creator scene!"); - - { - 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 - }}, - 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[] = { - { - .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 = {}; - m_sceneRenderpass = m_device->createRenderpass(std::move(params)); - if (!m_sceneRenderpass) - return logFail("Failed to create Scene Renderpass!"); - } - - { - nbl::system::SSpaceEnvBlobHeader envBlobHeader = {}; - std::vector envBlobPayload; - nbl::system::loadPreferredSpaceEnvBlob(*m_system, localInputCWD, envBlobHeader, envBlobPayload); - if (envBlobPayload.empty()) - return logFail("Failed to load space environment blob from available assets."); - - const E_FORMAT envFormat = EF_R16G16B16A16_SFLOAT; - const asset::VkExtent3D envExtent = { envBlobHeader.width, envBlobHeader.height, 1u }; - constexpr uint32_t envMipLevels = 1u; - constexpr uint32_t envArrayLayers = 1u; - const E_FORMAT envGpuFormat = envFormat; - const std::array envRegions = {{ - { - .bufferOffset = 0ull, - .bufferRowLength = 0u, - .bufferImageHeight = 0u, - .imageSubresource = { - .aspectMask = IImage::E_ASPECT_FLAGS::EAF_COLOR_BIT, - .mipLevel = 0u, - .baseArrayLayer = 0u, - .layerCount = envArrayLayers - }, - .imageOffset = { 0, 0, 0 }, - .imageExtent = envExtent - } - }}; - - IGPUImage::SCreationParams imageParams = {}; - imageParams = asset::IImage::SCreationParams{ - .type = IGPUImage::ET_2D, - .samples = IGPUImage::ESCF_1_BIT, - .format = envGpuFormat, - .extent = envExtent, - .mipLevels = envMipLevels, - .arrayLayers = envArrayLayers, - .flags = IGPUImage::ECF_NONE, - .usage = IGPUImage::EUF_SAMPLED_BIT | IGPUImage::EUF_TRANSFER_DST_BIT - }; - m_spaceEnvImage = m_device->createImage(std::move(imageParams)); - if (!m_spaceEnvImage) - return logFail("Failed to create space environment image."); - m_spaceEnvImage->setObjectDebugName("61_UI Space Environment"); - - auto memReqs = m_spaceEnvImage->getMemoryReqs(); - memReqs.memoryTypeBits &= m_physicalDevice->getDeviceLocalMemoryTypeBits(); - if (!m_device->allocate(memReqs, m_spaceEnvImage.get()).isValid()) - return logFail("Failed to allocate memory for space environment image."); - - 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_spaceEnvImage.get(), - .subresourceRange = { - .aspectMask = IGPUImage::EAF_COLOR_BIT, - .baseMipLevel = 0u, - .levelCount = envMipLevels, - .baseArrayLayer = 0u, - .layerCount = envArrayLayers - }, - .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(), - envFormat, - m_spaceEnvImage.get(), - IGPUImage::LAYOUT::TRANSFER_DST_OPTIMAL, - std::span(envRegions)); - 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_spaceEnvImage.get(), - .subresourceRange = { - .aspectMask = IGPUImage::EAF_COLOR_BIT, - .baseMipLevel = 0u, - .levelCount = envMipLevels, - .baseArrayLayer = 0u, - .layerCount = envArrayLayers - }, - .oldLayout = IGPUImage::LAYOUT::TRANSFER_DST_OPTIMAL, - .newLayout = IGPUImage::LAYOUT::READ_ONLY_OPTIMAL - } - }; - const IGPUCommandBuffer::SPipelineBarrierDependencyInfo postDep = { .imgBarriers = postBarrier }; - success = success && cmdbuf->pipelineBarrier(asset::EDF_NONE, postDep); - return success; - }); - if (uploadResult.copy() != IQueue::RESULT::SUCCESS) - return logFail("Failed to upload space environment map."); - - IGPUImageView::SCreationParams viewParams = {}; - viewParams.subUsages = IGPUImage::EUF_SAMPLED_BIT; - viewParams.image = core::smart_refctd_ptr(m_spaceEnvImage); - viewParams.viewType = IGPUImageView::ET_2D; - viewParams.format = envGpuFormat; - viewParams.subresourceRange.aspectMask = IGPUImage::EAF_COLOR_BIT; - viewParams.subresourceRange.baseMipLevel = 0u; - viewParams.subresourceRange.levelCount = envMipLevels; - viewParams.subresourceRange.baseArrayLayer = 0u; - viewParams.subresourceRange.layerCount = envArrayLayers; - m_spaceEnvImageView = m_device->createImageView(std::move(viewParams)); - if (!m_spaceEnvImageView) - return logFail("Failed to create space environment image view."); - - 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_spaceEnvSampler = m_device->createSampler(samplerParams); - if (!m_spaceEnvSampler) - return logFail("Failed to create space environment sampler."); - - 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_spaceEnvSampler - } - }; - m_spaceEnvDescriptorSetLayout = m_device->createDescriptorSetLayout(std::span{ bindings }); - if (!m_spaceEnvDescriptorSetLayout) - return logFail("Failed to create space environment descriptor set layout."); - - 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_spaceEnvDescriptorSetLayout), - nullptr, - nullptr, - nullptr); - if (!pipelineLayout) - return logFail("Failed to create space environment pipeline layout."); - - 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 logFail("Failed to load space environment fragment shader."); - - nbl::ext::FullScreenTriangle::ProtoPipeline fsTriProto(m_assetMgr.get(), m_device.get(), m_logger.get()); - if (!fsTriProto) - return logFail("Failed to create FullScreenTriangle prototype pipeline."); - - const IGPUPipelineBase::SShaderSpecInfo fragmentSpec = { - .shader = fragmentShader.get(), - .entryPoint = "main" - }; - m_spaceEnvPipeline = fsTriProto.createPipeline(fragmentSpec, pipelineLayout.get(), m_sceneRenderpass.get()); - if (!m_spaceEnvPipeline) - return logFail("Failed to create space environment pipeline."); - - uint32_t setCount = 1u; - const IGPUDescriptorSetLayout* setLayouts[] = { m_spaceEnvDescriptorSetLayout.get() }; - m_spaceEnvDescriptorPool = m_device->createDescriptorPoolForDSLayouts(IDescriptorPool::E_CREATE_FLAGS::ECF_NONE, setLayouts, &setCount); - if (!m_spaceEnvDescriptorPool) - return logFail("Failed to create space environment descriptor pool."); - m_spaceEnvDescriptorSet = m_spaceEnvDescriptorPool->createDescriptorSet(core::smart_refctd_ptr(m_spaceEnvDescriptorSetLayout)); - if (!m_spaceEnvDescriptorSet) - return logFail("Failed to create space environment descriptor set."); - - IGPUDescriptorSet::SDescriptorInfo info = {}; - info.desc = m_spaceEnvImageView; - info.info.image.imageLayout = IImage::LAYOUT::READ_ONLY_OPTIMAL; - - IGPUDescriptorSet::SWriteDescriptorSet write = {}; - write.dstSet = m_spaceEnvDescriptorSet.get(); - write.binding = 0u; - write.arrayElement = 0u; - write.count = 1u; - write.info = &info; - if (!m_device->updateDescriptorSets({ &write, 1u }, {})) - return logFail("Failed to update space environment descriptor set."); - } - - const auto& geometries = m_scene->getInitParams().geometries; - if (geometries.empty()) - return logFail("No geometries found for scene!"); - m_renderer = CSimpleDebugRenderer::create(m_assetMgr.get(), m_sceneRenderpass.get(), 0, { &geometries.front().get(), geometries.size() }); - if (!m_renderer) - return logFail("Failed to create debug renderer!"); - { - const asset::SPushConstantRange singlePcRange = { - .stageFlags = IShader::E_SHADER_STAGE::ESS_VERTEX, - .offset = offsetof(ext::frustum::PushConstants, spc), - .size = sizeof(ext::frustum::SSinglePC) - }; - - ext::frustum::CDrawFrustum::SCreationParameters frustumParams = {}; - frustumParams.transfer = getTransferUpQueue(); - frustumParams.assetManager = m_assetMgr; - frustumParams.drawMode = ext::frustum::CDrawFrustum::DrawMode::DM_SINGLE; - frustumParams.singlePipelineLayout = ext::frustum::CDrawFrustum::createPipelineLayoutFromPCRange(m_device.get(), singlePcRange); - frustumParams.renderpass = core::smart_refctd_ptr(m_sceneRenderpass); - frustumParams.utilities = m_utils; - m_drawFrustum = ext::frustum::CDrawFrustum::create(std::move(frustumParams)); - if (!m_drawFrustum) - return logFail("Failed to create frustum drawer."); - } - - { - const auto& pipelines = m_renderer->getInitParams().pipelines; - m_gridGeometryIx = std::nullopt; - m_followTargetGeometryIx = std::nullopt; - auto ix = 0u; - for (const auto& name : m_scene->getInitParams().geometryNames) - { - if (name == "Cube") - { - if (!m_followTargetGeometryIx.has_value()) - m_followTargetGeometryIx = ix; - } - else if (name == "Cone") - m_renderer->getGeometry(ix).pipeline = pipelines[CSimpleDebugRenderer::SInitParams::PipelineType::Cone]; - else if (name == "Grid") - m_gridGeometryIx = ix; - ix++; - } - } - m_renderer->m_instances.resize(1u + (m_gridGeometryIx.has_value() ? 1u : 0u) + (m_followTargetGeometryIx.has_value() ? 1u : 0u)); - - const auto dpyInfo = m_winMgr->getPrimaryDisplayInfo(); - for (uint32_t i = 0u; i < windowBindings.size(); ++i) - { - auto& binding = windowBindings[i]; - binding.sceneColorView = createAttachmentView(m_device.get(), finalSceneRenderFormat, dpyInfo.resX, dpyInfo.resY, "UI Scene Color Attachment"); - binding.sceneDepthView = createAttachmentView(m_device.get(), sceneRenderDepthFormat, dpyInfo.resX, dpyInfo.resY, "UI Scene Depth Attachment"); - binding.sceneFramebuffer = createSceneFramebuffer(m_device.get(), m_sceneRenderpass.get(), binding.sceneColorView.get(), binding.sceneDepthView.get()); - if (!binding.sceneFramebuffer) - return logFail("Could not create geometry creator scene[%d]!", i); - } - } - - 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; - + 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..90d6b81e6 --- /dev/null +++ b/61_UI/AppInputRuntime.cpp @@ -0,0 +1,236 @@ +#include "app/App.hpp" + +#include + +namespace +{ + +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::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::remapTranslationEventsFromWorldToCameraLocal(target, perCameraEvents, perCount); + if (perCount) + target->manipulate({ perCameraEvents.data(), perCount }); + } + else + { + target->manipulate({ collectedVirtualEvents.events.data(), collectedVirtualEvents.totalCount() }); + } + + nbl::core::applyCameraConstraints(goalSolver, target, cameraConstraints); + if (!scriptedInputEnabled) + refreshFollowOffsets(planarIx); + appendVirtualEventLog(target, planarIx, collectedVirtualEvents); +} + +} // namespace + +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..55b7be33e --- /dev/null +++ b/61_UI/AppManipulableObjects.cpp @@ -0,0 +1,230 @@ +#include "app/App.hpp" + +namespace +{ + +inline float32_t4x4 buildModelManipulationTransform(const float32_t3x4& model) +{ + return hlsl::transpose(getMatrix3x4As4x4(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 getCastedVector(camera.getGimbal().getPosition()); +} + +inline float32_t4x4 buildFollowTargetTransform(const CTrackedTarget& trackedTarget) +{ + return getCastedMatrix(trackedTarget.getGimbal().template operator()()); +} + +inline float32_t3 buildFollowTargetWorldPosition(const CTrackedTarget& trackedTarget) +{ + return getCastedVector(trackedTarget.getGimbal().getPosition()); +} + +} // namespace + +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(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::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..694b5b78a --- /dev/null +++ b/61_UI/AppPresentationResources.cpp @@ -0,0 +1,162 @@ +#include "app/App.hpp" + +nbl::hlsl::uint32_t2 App::getPresentationRenderExtent() const +{ + if (m_cliRuntime.ciMode) + 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; +} + +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!"); + } + + if (!m_surface || !m_surface->init(m_surface->pickQueue(m_device.get()), std::make_unique(), { .imageUsage = IGPUImage::EUF_TRANSFER_SRC_BIT })) + 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..54d37c0ae --- /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::analyzePresetPresentation(m_cameraGoalSolver, camera, preset); +} + +App::CaptureUiAnalysis App::analyzeCameraCaptureForUi(ICamera* camera) const +{ + return nbl::ui::analyzeCapturePresentation(m_cameraGoalSolver, camera); +} + +CCameraGoalSolver::SCompatibilityResult App::analyzePresetCompatibility(ICamera* camera, const CameraPreset& preset) const +{ + return nbl::core::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::applyPresetDetailed(m_cameraGoalSolver, camera, preset); + if (result.succeeded()) + refreshFollowOffsetConfigsForCamera(camera); + + const auto presetUi = analyzePresetForUi(camera, preset); + storeApplyStatusBanner( + m_presetAuthoring.applyBanner, + 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::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::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::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::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::sortKeyframeTrackByTime(m_playbackAuthoring.keyframeTrack); +} + +void App::clampPlaybackTimeToKeyframes() +{ + nbl::core::clampPlaybackCursorToTrack(m_playbackAuthoring.keyframeTrack, m_playbackAuthoring.playback); +} + +int App::selectKeyframeNearestTime(const float time) +{ + return nbl::core::selectKeyframeTrackNearestTime(m_playbackAuthoring.keyframeTrack, time); +} + +void App::normalizeSelectedKeyframe() +{ + nbl::core::normalizeSelectedKeyframeTrack(m_playbackAuthoring.keyframeTrack); +} + +App::CameraKeyframe* App::getSelectedKeyframe() +{ + return nbl::core::getSelectedKeyframe(m_playbackAuthoring.keyframeTrack); +} + +const App::CameraKeyframe* App::getSelectedKeyframe() const +{ + return nbl::core::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::tryCapturePreset(m_cameraGoalSolver, camera, keyframeName, updatedPreset)) + return false; + + return nbl::core::replaceSelectedKeyframePreset(m_playbackAuthoring.keyframeTrack, std::move(updatedPreset)); +} + +void App::updatePlayback(const double dtSec) +{ + const auto advance = nbl::core::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..a21bb4f19 --- /dev/null +++ b/61_UI/AppResourceUtilities.cpp @@ -0,0 +1,117 @@ +#include "app/AppResourceUtilities.hpp" + +#include +#include +#include + +#include "app/AppResourcePathUtilities.hpp" +#include "camera/CCameraFileUtilities.hpp" + +namespace +{ + +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::readBinaryFile(system, blobPath, blobBytes)) + return false; + return parseSpaceEnvBlobBytes(blobBytes, outHeader, outPayload); +} + +} // namespace + +namespace nbl::system +{ + +bool mountOptionalSharedEnvmapResources( + const SCameraAppResourceContext& context, + ILogger* logger) +{ + if (!context) + return false; + + auto sharedEnvmapDirectory = getSharedEnvmapDirectory(context.localInputCWD); + 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(context.localInputCWD); + 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..e796d74f8 --- /dev/null +++ b/61_UI/AppSceneDebugInstances.cpp @@ -0,0 +1,102 @@ +#include "app/App.hpp" + +std::optional App::findFrustumSourceBindingIx(const uint32_t planarIx) const +{ + if (m_viewports.activeRenderWindowIx < m_viewports.windowBindings.size()) + { + const auto& activeBinding = m_viewports.windowBindings[m_viewports.activeRenderWindowIx]; + if (activeBinding.activePlanarIx == planarIx && activeBinding.boundProjectionIx.has_value()) + return m_viewports.activeRenderWindowIx; + } + + for (uint32_t bindingIx = 0u; bindingIx < m_viewports.windowBindings.size(); ++bindingIx) + { + const auto& binding = m_viewports.windowBindings[bindingIx]; + if (binding.activePlanarIx != planarIx) + continue; + if (!binding.boundProjectionIx.has_value()) + continue; + return bindingIx; + } + + return std::nullopt; +} + +std::optional App::tryBuildFrustumOverlaySourceBindingIx() const +{ + if (m_sceneInteraction.boundPlanarCameraIxToManipulate.has_value()) + { + if (const auto sourceBindingIx = findFrustumSourceBindingIx(m_sceneInteraction.boundPlanarCameraIxToManipulate.value()); sourceBindingIx.has_value()) + return sourceBindingIx; + } + + if (m_viewports.activeRenderWindowIx < m_viewports.windowBindings.size()) + { + const auto& activeBinding = m_viewports.windowBindings[m_viewports.activeRenderWindowIx]; + if (activeBinding.boundProjectionIx.has_value() && activeBinding.activePlanarIx < m_planarProjections.size()) + return m_viewports.activeRenderWindowIx; + } + + return std::nullopt; +} + +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; + hlsl::setTranslation( + gridWorld, + float32_t3( + -0.5f * SCameraAppSceneDebugDefaults::GridExtent, + SCameraAppSceneDebugDefaults::GridVerticalOffset, + -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..6d4f42c09 --- /dev/null +++ b/61_UI/AppSceneFramebufferResources.cpp @@ -0,0 +1,147 @@ +#include "app/App.hpp" + +namespace +{ + +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)); +} + +} // namespace + +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..4219bd637 --- /dev/null +++ b/61_UI/AppSceneRenderPasses.cpp @@ -0,0 +1,102 @@ +#include "app/App.hpp" +#include "app/AppRenderPassUtilities.hpp" + +bool App::recordSceneFramebufferPass(IGPUCommandBuffer* cmdbuf, SWindowControlBinding& binding, const uint32_t bindingIx) +{ + 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); + + const bool drawScriptFrustum = m_scriptedInput.enabled && m_scriptedInput.visualDebug; + if (!m_debugScene.frustumDrawer || !drawScriptFrustum) + return finalize(); + + const auto sourceBindingIx = tryBuildFrustumOverlaySourceBindingIx(); + if (!sourceBindingIx.has_value()) + return finalize(); + + const auto& sourceBinding = m_viewports.windowBindings[sourceBindingIx.value()]; + const bool sameCameraAsView = binding.activePlanarIx == sourceBinding.activePlanarIx; + const bool sameWindow = bindingIx == sourceBindingIx.value(); + if (sameCameraAsView || sameWindow) + return finalize(); + + ext::frustum::CDrawFrustum::DrawParameters drawParams = {}; + drawParams.commandBuffer = cmdbuf; + drawParams.viewProjectionMatrix = binding.viewProjMatrix; + drawParams.lineWidth = SCameraAppSceneDebugDefaults::FrustumLineWidth; + + const float32_t4 color = SCameraAppFrameRuntimeDefaults::FrustumColor; + success = success && m_debugScene.frustumDrawer->renderSingle(drawParams, hlsl::inverse(sourceBinding.viewProjMatrix), color); + 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..f39e717d4 --- /dev/null +++ b/61_UI/AppScriptedInitialization.cpp @@ -0,0 +1,236 @@ +#include "app/App.hpp" + +#include "app/AppCameraConfigUtilities.hpp" +#include "app/AppResourceUtilities.hpp" +#include "camera/CCameraScriptedRuntimePersistence.hpp" + +void App::resetScriptedInputRuntimeState() +{ + m_scriptedInput.nextEventIndex = 0u; + m_scriptedInput.checkRuntime = {}; + m_scriptedInput.nextCaptureIndex = 0u; + m_scriptedInput.failed = false; + m_scriptedInput.summaryReported = false; +} + +void App::finalizeScriptedInputRuntimeState() +{ + nbl::system::finalizeScriptedTimeline(m_scriptedInput.timeline, m_cliRuntime.disableScreenshotsCli); +} + +void App::applyParsedScriptedInput( + nbl::system::CCameraScriptedInputParseResult parsed, + std::optional& pendingScriptedSequence) +{ + pendingScriptedSequence.reset(); + m_scriptedInput.timeline.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); + 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::system::CCameraScriptedInputParseResult parsed = {}; + std::string scriptedInputParseError; + if (!nbl::system::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; + resetScriptedInputRuntimeState(); + + const bool useWindowMode = nbl::core::sequenceScriptUsesMultiplePresentations(sequence); + nbl::system::appendScriptedActionEvent( + timeline, + 0u, + CCameraScriptedInputEvent::ActionData::Kind::SetUseWindow, + useWindowMode ? 1 : 0); + + const CCameraSequenceTrackedTargetPose referenceTrackedTargetPose = { + .position = getDefaultFollowTargetPosition(), + .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(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::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::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::system::appendCompiledSequenceSegmentToScriptedTimeline( + timeline, + 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::finalizeScriptedTimeline(timeline, m_cliRuntime.disableScreenshotsCli); + m_scriptedInput.timeline = std::move(timeline); + return true; +} + diff --git a/61_UI/AppScriptedInputRuntime.cpp b/61_UI/AppScriptedInputRuntime.cpp new file mode 100644 index 000000000..deadad6c1 --- /dev/null +++ b/61_UI/AppScriptedInputRuntime.cpp @@ -0,0 +1,312 @@ +#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::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::dequeueScriptedFrameEvents( + m_scriptedInput.timeline.events, + m_scriptedInput.nextEventIndex, + m_realFrameIx, + outFrame.frameEvents); + } + + nbl::ui::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(const CCameraScriptedFrameEvents& scriptedFrameEvents) +{ + if (!(m_scriptedInput.enabled && !scriptedFrameEvents.actions.empty())) + return; + + auto applyAction = [&](const CCameraScriptedInputEvent::ActionData& action) -> void + { + switch (action.kind) + { + case CCameraScriptedInputEvent::ActionData::Kind::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 CCameraScriptedInputEvent::ActionData::Kind::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 CCameraScriptedInputEvent::ActionData::Kind::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 CCameraScriptedInputEvent::ActionData::Kind::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 CCameraScriptedInputEvent::ActionData::Kind::SetUseWindow: + m_viewports.useWindow = action.value != 0; + break; + + case CCameraScriptedInputEvent::ActionData::Kind::SetLeftHanded: + m_viewports.windowBindings[m_viewports.activeRenderWindowIx].leftHandedProjection = action.value != 0; + break; + + case CCameraScriptedInputEvent::ActionData::Kind::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::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 : scriptedFrameEvents.actions) + { + if (action.kind == CCameraScriptedInputEvent::ActionData::Kind::SetActiveRenderWindow) + applyAction(action); + } + + for (const auto& action : scriptedFrameEvents.actions) + { + if (action.kind != CCameraScriptedInputEvent::ActionData::Kind::SetActiveRenderWindow) + applyAction(action); + } + + if (m_scriptedInput.log) + { + m_logger->log( + "[script] frame %llu actions=%zu", + ILogger::ELL_INFO, + static_cast(m_realFrameIx), + scriptedFrameEvents.actions.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::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; + 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, + 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..c8b72a6a7 --- /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::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::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..4375856c5 --- /dev/null +++ b/61_UI/AppSpaceEnvironmentResources.cpp @@ -0,0 +1,254 @@ +#include "app/App.hpp" + +#include "app/AppResourceUtilities.hpp" + +namespace +{ + +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; +} + +} // namespace + +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 index 5d2fde78a..bf30b9408 100644 --- a/61_UI/AppTransformEditor.cpp +++ b/61_UI/AppTransformEditor.cpp @@ -1,162 +1,124 @@ #include "app/App.hpp" +#include "app/AppGizmoUtilities.hpp" void App::TransformEditorContents() { - 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; - - const size_t objectsCount = getManipulableObjectCount(); - assert(objectsCount); - - std::vector sbels(objectsCount); - for (size_t i = 0; i < objectsCount; ++i) - sbels[i] = getManipulableObjectLabel(static_cast(i)); - - std::vector labels(objectsCount); - for (size_t i = 0; i < objectsCount; ++i) - labels[i] = sbels[i].c_str(); - - int activeObject = static_cast(getManipulatedObjectIx()); - if (ImGui::Combo("Active Object", &activeObject, labels.data(), static_cast(labels.size()))) - bindManipulatedObjectByIx(static_cast(activeObject)); - - ImGuizmoModelM16InOut imguizmoModel; - - imguizmoModel.inTRS = getManipulableObjectTransform(static_cast(activeObject)); - - imguizmoModel.outTRS = imguizmoModel.inTRS; - float* m16TRSmatrix = &imguizmoModel.outTRS[0][0]; - - std::string indent; - if (m_manipulatedObjectKind == SceneManipulatedObjectKind::Camera && boundCameraToManipulate) - indent = boundCameraToManipulate->getIdentifier(); - else if (m_manipulatedObjectKind == SceneManipulatedObjectKind::FollowTarget) - indent = m_followTarget.getIdentifier(); - else - indent = "Geometry Creator Object"; - - ImGui::Text("Identifier: \"%s\"", indent.c_str()); + 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)) { - if (ImGuizmo::IsUsingAny()) - ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), "Gizmo: In Use"); - else - ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.0f, 1.0f), "Gizmo: Idle"); - - if (ImGui::IsItemHovered()) - { - nbl::ui::beginHoverInfoOverlay("HoverOverlay", ImGui::GetMousePos()); - ImGui::Text("Right-click and drag on the gizmo to manipulate the object."); - nbl::ui::endHoverInfoOverlay(); - } + activeObject = static_cast(i); + bindManipulatedObjectByIx(static_cast(activeObject)); } - ImGui::Separator(); - - if (m_manipulatedObjectKind == SceneManipulatedObjectKind::Model) + 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::beginHoverInfoOverlay("HoverOverlay", ImGui::GetMousePos()); + ImGui::Text("Right-click and drag on the gizmo to manipulate the object."); + nbl::ui::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())) { - const auto& names = m_scene->getInitParams().geometryNames; - if (!names.empty()) + for (uint32_t i = 0u; i < names.size(); ++i) { - if (gcIndex >= names.size()) - gcIndex = 0; - - if (ImGui::BeginCombo("Object Type", names[gcIndex].c_str())) - { - for (uint32_t i = 0u; i < names.size(); ++i) - { - const bool isSelected = (gcIndex == i); - if (ImGui::Selectable(names[i].c_str(), isSelected)) - gcIndex = static_cast(i); - - if (isSelected) - ImGui::SetItemDefaultFocus(); - } - ImGui::EndCombo(); - } - } - - } - - addMatrixTable("Model (TRS) Matrix", "ModelMatrixTable", 4, 4, m16TRSmatrix); - - 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; - - hlsl::SRigidTransformComponents transformState = {}; - float32_t3 matrixRotation = float32_t3(0.0f); - imguizmoModel.outDeltaTRS = hlsl::float32_t4x4(1.0f); + const bool isSelected = (m_debugScene.geometrySelectionIx == i); + if (ImGui::Selectable(names[i].c_str(), isSelected)) + m_debugScene.geometrySelectionIx = static_cast(i); - if (!hlsl::tryExtractRigidTransformComponents(imguizmoModel.outTRS, transformState)) - { - transformState.translation = float32_t3(imguizmoModel.outTRS[3].x, imguizmoModel.outTRS[3].y, imguizmoModel.outTRS[3].z); - transformState.orientation = hlsl::makeIdentityQuaternion(); - transformState.scale = float32_t3(1.0f); - } - matrixRotation = hlsl::getQuaternionEulerDegrees(transformState.orientation); - { - ImGuiInputTextFlags flags = 0; - - ImGui::InputFloat3("Tr", &transformState.translation[0], "%.3f", flags); - ImGui::InputFloat3("Rt", &matrixRotation[0], "%.3f", flags); - ImGui::InputFloat3("Sc", &transformState.scale[0], "%.3f", flags); - } - imguizmoModel.outTRS = hlsl::composeTransformMatrix( - transformState.translation, - hlsl::makeQuaternionFromEulerDegrees(matrixRotation), - transformState.scale); - m16TRSmatrix = &imguizmoModel.outTRS[0][0]; - - 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; - } - - ImGui::Checkbox(" ", &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; - } - - // generate virtual events given delta TRS matrix - if (m_manipulatedObjectKind == SceneManipulatedObjectKind::Camera && boundCameraToManipulate) - { - auto referenceFrame = getCastedMatrix(imguizmoModel.outTRS); - nbl::core::applyReferenceFrameToCamera(boundCameraToManipulate.get(), referenceFrame); - if (boundPlanarCameraIxToManipulate.has_value()) - refreshFollowOffsetConfigForPlanar(boundPlanarCameraIxToManipulate.value()); - } - else if (m_manipulatedObjectKind == SceneManipulatedObjectKind::FollowTarget) - { - setFollowTargetTransform(getCastedMatrix(imguizmoModel.outTRS)); - applyFollowToConfiguredCameras(); - } - else - { - m_model = float32_t3x4(hlsl::transpose(imguizmoModel.outTRS)); + 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::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..3861f52d0 --- /dev/null +++ b/61_UI/AppUiInputCapture.cpp @@ -0,0 +1,93 @@ +#include "app/App.hpp" + +namespace +{ + +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; + } + } +} + +} // namespace + +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..bba35599d --- /dev/null +++ b/61_UI/AppUiResources.cpp @@ -0,0 +1,160 @@ +#include "app/App.hpp" +#include "app/AppResourceUtilities.hpp" + +namespace +{ + +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 }; + } +} + +} // namespace + +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 vertexKey = nbl::this_example::builtin::build::get_spirv_key<"imgui_vertex">(m_device.get()); + const auto fragmentKey = nbl::this_example::builtin::build::get_spirv_key<"imgui_fragment">(m_device.get()); + auto vertexShader = nbl::system::loadPrecompiledShaderFromAppResources(*m_assetMgr, m_logger.get(), vertexKey); + auto fragmentShader = nbl::system::loadPrecompiledShaderFromAppResources(*m_assetMgr, m_logger.get(), fragmentKey); + if (!vertexShader || !fragmentShader) + return logFail("Failed to load precompiled ImGui shaders."); + + params.spirv = nbl::ext::imgui::UI::SCreationParameters::PrecompiledShaders{ + .vertex = std::move(vertexShader), + .fragment = std::move(fragmentShader) + }; + + 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 index 00817a012..9af1ea0c9 100644 --- a/61_UI/AppUpdate.cpp +++ b/61_UI/AppUpdate.cpp @@ -1,666 +1,119 @@ #include "app/App.hpp" -void App::update() +namespace { - m_inputSystem->getDefaultMouse(&mouse); - m_inputSystem->getDefaultKeyboard(&keyboard); - - auto updatePresentationTimestamp = [&]() - { - oracle.reportEndFrameRecord(); - const auto timestamp = oracle.getNextPresentationTimeStamp(); - oracle.reportBeginFrameRecord(); - - return timestamp; - }; - - m_nextPresentationTimestamp = updatePresentationTimestamp(); - if (m_haveLastPresentationTimestamp) - { - const auto delta = m_nextPresentationTimestamp - m_lastPresentationTimestamp; - if (delta.count() < 0) - m_frameDeltaSec = 0.0; - else - m_frameDeltaSec = static_cast(delta.count()) / 1000000.0; - } - m_lastPresentationTimestamp = m_nextPresentationTimestamp; - m_haveLastPresentationTimestamp = true; - - updatePlayback(m_frameDeltaSec); - const bool skipCameraInput = m_playback.playing && m_playback.overrideInput; - - struct - { - std::vector mouse {}; - std::vector keyboard {}; - } capturedEvents; - { - mouse.consumeEvents([&](const IMouseEventChannel::range_t& events) -> void - { - if (m_window->hasInputFocus()) - for (const auto& e : events) - capturedEvents.mouse.emplace_back(e); - }, m_logger.get()); - - keyboard.consumeEvents([&](const IKeyboardEventChannel::range_t& events) -> void - { - if (m_window->hasInputFocus()) - for (const auto& e : events) - capturedEvents.keyboard.emplace_back(e); - }, m_logger.get()); - } - - if (m_scriptedInput.enabled && m_scriptedInput.exclusive) - { - capturedEvents.mouse.clear(); - capturedEvents.keyboard.clear(); - } - - std::vector scriptedMouse; - std::vector scriptedKeyboard; - CCameraScriptedFrameEvents scriptedFrameEvents; - const CVirtualGimbalEvent* scriptedImguizmoVirtual = nullptr; - uint32_t scriptedImguizmoVirtualCount = 0u; - - if (m_scriptedInput.enabled && m_scriptedInput.nextEventIndex < m_scriptedInput.timeline.events.size()) - nbl::system::dequeueScriptedFrameEvents( - m_scriptedInput.timeline.events, - m_scriptedInput.nextEventIndex, - m_realFrameIx, - scriptedFrameEvents); - - for (const auto& authoredKeyboard : scriptedFrameEvents.keyboard) - { - SKeyboardEvent e(m_nextPresentationTimestamp); - e.keyCode = authoredKeyboard.key; - e.action = - authoredKeyboard.action == CCameraScriptedInputEvent::KeyboardData::Action::Pressed ? - ui::SKeyboardEvent::ECA_PRESSED : - ui::SKeyboardEvent::ECA_RELEASED; - e.window = m_window.get(); - scriptedKeyboard.emplace_back(e); - } - for (const auto& authoredMouse : scriptedFrameEvents.mouse) - { - SMouseEvent e(m_nextPresentationTimestamp); - e.window = m_window.get(); - switch (authoredMouse.type) - { - case CCameraScriptedInputEvent::MouseData::Type::Click: - e.type = ui::SMouseEvent::EET_CLICK; - e.clickEvent.mouseButton = authoredMouse.button; - e.clickEvent.action = - authoredMouse.action == CCameraScriptedInputEvent::MouseData::ClickAction::Pressed ? - ui::SMouseEvent::SClickEvent::EA_PRESSED : - ui::SMouseEvent::SClickEvent::EA_RELEASED; - e.clickEvent.clickPosX = authoredMouse.x; - e.clickEvent.clickPosY = authoredMouse.y; - break; - case CCameraScriptedInputEvent::MouseData::Type::Scroll: - e.type = ui::SMouseEvent::EET_SCROLL; - e.scrollEvent.verticalScroll = authoredMouse.v; - e.scrollEvent.horizontalScroll = authoredMouse.h; - break; - case CCameraScriptedInputEvent::MouseData::Type::Movement: - e.type = ui::SMouseEvent::EET_MOVEMENT; - e.movementEvent.relativeMovementX = authoredMouse.dx; - e.movementEvent.relativeMovementY = authoredMouse.dy; - break; - default: - continue; - } - scriptedMouse.emplace_back(e); - } - - if (!scriptedFrameEvents.segmentLabels.empty()) - m_scriptedInput.visualSegmentLabel = scriptedFrameEvents.segmentLabels.back(); - - if (m_scriptedInput.enabled && scriptedFrameEvents.actions.size()) - { - auto applyAction = [&](const CCameraScriptedInputEvent::ActionData& action) -> void - { - switch (action.kind) - { - case CCameraScriptedInputEvent::ActionData::Kind::SetActiveRenderWindow: - { - if (action.value < 0 || static_cast(action.value) >= windowBindings.size()) - { - m_logger->log("[script][warn] action set_active_render_window out of range: %d", ILogger::ELL_WARNING, action.value); - return; - } - activeRenderWindowIx = static_cast(action.value); - } break; - - case CCameraScriptedInputEvent::ActionData::Kind::SetActivePlanar: - { - if (action.value < 0 || static_cast(action.value) >= m_planarProjections.size()) - { - m_logger->log("[script][warn] action set_active_planar out of range: %d", ILogger::ELL_WARNING, action.value); - return; - } - auto& binding = windowBindings[activeRenderWindowIx]; - binding.activePlanarIx = static_cast(action.value); - binding.pickDefaultProjections(m_planarProjections[binding.activePlanarIx]->getPlanarProjections()); - m_scriptedInput.visualActivePlanarValid = true; - m_scriptedInput.visualActivePlanarIx = binding.activePlanarIx; - m_scriptedInput.visualActivePlanarStartFrame = m_realFrameIx; - } break; - - case CCameraScriptedInputEvent::ActionData::Kind::SetProjectionType: - { - auto& binding = windowBindings[activeRenderWindowIx]; - if (!binding.lastBoundPerspectivePresetProjectionIx.has_value() || !binding.lastBoundOrthoPresetProjectionIx.has_value()) - binding.pickDefaultProjections(m_planarProjections[binding.activePlanarIx]->getPlanarProjections()); - - const auto type = static_cast(action.value); - switch (type) - { - case IPlanarProjection::CProjection::Perspective: - binding.boundProjectionIx = binding.lastBoundPerspectivePresetProjectionIx.value(); - break; - case IPlanarProjection::CProjection::Orthographic: - binding.boundProjectionIx = binding.lastBoundOrthoPresetProjectionIx.value(); - break; - default: - m_logger->log("[script][warn] action set_projection_type invalid value: %d", ILogger::ELL_WARNING, action.value); - break; - } - } break; - - case CCameraScriptedInputEvent::ActionData::Kind::SetProjectionIndex: - { - auto& binding = windowBindings[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; - } - const auto ix = static_cast(action.value); - const auto type = projections[ix].getParameters().m_type; - binding.boundProjectionIx = ix; - if (type == IPlanarProjection::CProjection::Perspective) - binding.lastBoundPerspectivePresetProjectionIx = ix; - else if (type == IPlanarProjection::CProjection::Orthographic) - binding.lastBoundOrthoPresetProjectionIx = ix; - } break; - - case CCameraScriptedInputEvent::ActionData::Kind::SetUseWindow: - { - useWindow = action.value != 0; - } break; - - case CCameraScriptedInputEvent::ActionData::Kind::SetLeftHanded: - { - auto& binding = windowBindings[activeRenderWindowIx]; - binding.leftHandedProjection = action.value != 0; - } break; - - case CCameraScriptedInputEvent::ActionData::Kind::ResetActiveCamera: - { - auto& binding = windowBindings[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_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::applyPreset(m_cameraGoalSolver, camera, m_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 : scriptedFrameEvents.actions) - if (action.kind == CCameraScriptedInputEvent::ActionData::Kind::SetActiveRenderWindow) - applyAction(action); - - for (const auto& action : scriptedFrameEvents.actions) - if (action.kind != CCameraScriptedInputEvent::ActionData::Kind::SetActiveRenderWindow) - applyAction(action); - - if (m_scriptedInput.log) - m_logger->log("[script] frame %llu actions=%zu", ILogger::ELL_INFO, static_cast(m_realFrameIx), scriptedFrameEvents.actions.size()); - } - - if (m_scriptedInput.enabled && m_scriptedInput.visualDebug && !m_scriptedInput.visualActivePlanarValid) - { - if (activeRenderWindowIx < windowBindings.size()) - { - m_scriptedInput.visualActivePlanarValid = true; - m_scriptedInput.visualActivePlanarIx = windowBindings[activeRenderWindowIx].activePlanarIx; - m_scriptedInput.visualActivePlanarStartFrame = m_realFrameIx; - } - } - - if (!m_scriptedInput.enabled) - { - m_scriptedInput.scriptedLeftMouseDown = false; - m_scriptedInput.scriptedRightMouseDown = false; - } - else - { - for (const auto& ev : scriptedMouse) - { - if (ev.type != ui::SMouseEvent::EET_CLICK) - continue; - if (ev.clickEvent.mouseButton == ui::EMB_LEFT_BUTTON) - { - if (ev.clickEvent.action == ui::SMouseEvent::SClickEvent::EA_PRESSED) - m_scriptedInput.scriptedLeftMouseDown = true; - else if (ev.clickEvent.action == ui::SMouseEvent::SClickEvent::EA_RELEASED) - m_scriptedInput.scriptedLeftMouseDown = false; - } - else if (ev.clickEvent.mouseButton == ui::EMB_RIGHT_BUTTON) - { - if (ev.clickEvent.action == ui::SMouseEvent::SClickEvent::EA_PRESSED) - m_scriptedInput.scriptedRightMouseDown = true; - else if (ev.clickEvent.action == ui::SMouseEvent::SClickEvent::EA_RELEASED) - m_scriptedInput.scriptedRightMouseDown = false; - } - } - } - - if (!scriptedMouse.empty()) - capturedEvents.mouse.insert(capturedEvents.mouse.end(), scriptedMouse.begin(), scriptedMouse.end()); - if (!scriptedKeyboard.empty()) - capturedEvents.keyboard.insert(capturedEvents.keyboard.end(), scriptedKeyboard.begin(), scriptedKeyboard.end()); - - m_uiInputEventsThisFrame = static_cast(capturedEvents.mouse.size() + capturedEvents.keyboard.size()); - - auto cameraKeyboardEvents = capturedEvents.keyboard; - auto cameraMouseEvents = capturedEvents.mouse; - for (auto& ev : cameraMouseEvents) - { - if (ev.type == ui::SMouseEvent::EET_SCROLL) - { - ev.scrollEvent.verticalScroll *= m_cameraControls.mouseScrollScale; - ev.scrollEvent.horizontalScroll *= m_cameraControls.mouseScrollScale; - } - else if (ev.type == ui::SMouseEvent::EET_MOVEMENT) - { - ev.movementEvent.relativeMovementX *= m_cameraControls.mouseMoveScale; - ev.movementEvent.relativeMovementY *= m_cameraControls.mouseMoveScale; - } - } - - const auto cursorPosition = m_window->getCursorControl()->getPosition(); - - nbl::ext::imgui::UI::SUpdateParameters params = - { - .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() } - }; - - if (m_scriptedInput.log && (scriptedKeyboard.size() || scriptedMouse.size() || scriptedFrameEvents.imguizmo.size() || scriptedFrameEvents.goals.size() || scriptedFrameEvents.trackedTargetTransforms.size())) - { - m_logger->log("[script] frame %llu input kb=%zu mouse=%zu imguizmo=%zu goals=%zu target=%zu", ILogger::ELL_INFO, - static_cast(m_realFrameIx), - scriptedKeyboard.size(), - scriptedMouse.size(), - scriptedFrameEvents.imguizmo.size(), - scriptedFrameEvents.goals.size(), - scriptedFrameEvents.trackedTargetTransforms.size()); - } - - if (enableActiveCameraMovement && !skipCameraInput) - { - auto& binding = windowBindings[activeRenderWindowIx]; - auto& planar = m_planarProjections[binding.activePlanarIx]; - auto* camera = planar->getCamera(); - - assert(binding.boundProjectionIx.has_value()); - auto& projection = planar->getPlanarProjections()[binding.boundProjectionIx.value()]; - - syncWindowInputBinding(binding); - auto& inputBinder = binding.inputBinding; - - std::vector filteredOrbitMouseEvents; - std::span mouseInput = { cameraMouseEvents.data(), cameraMouseEvents.size() }; - if (isOrbitLikeCamera(camera)) - { - const bool orbitLookDown = ImGui::IsMouseDown(ImGuiMouseButton_Right) || - (m_scriptedInput.enabled && (m_scriptedInput.scriptedLeftMouseDown || m_scriptedInput.scriptedRightMouseDown)); - filteredOrbitMouseEvents.reserve(cameraMouseEvents.size()); - for (const auto& ev : cameraMouseEvents) - { - if (ev.type == ui::SMouseEvent::EET_MOVEMENT && !orbitLookDown) - continue; - filteredOrbitMouseEvents.emplace_back(ev); - } - mouseInput = { filteredOrbitMouseEvents.data(), filteredOrbitMouseEvents.size() }; - } - - auto collectedEvents = inputBinder.collectVirtualEvents(m_nextPresentationTimestamp, { - .keyboardEvents = { cameraKeyboardEvents.data(), cameraKeyboardEvents.size() }, - .mouseEvents = mouseInput - }); - auto& virtualEvents = collectedEvents.events; - const uint32_t vCount = collectedEvents.totalCount(); - const uint32_t vKeyboardEventsCount = collectedEvents.keyboardCount; - - if (vCount) - { - for (uint32_t i = 0u; i < vKeyboardEventsCount; ++i) - virtualEvents[i].magnitude *= m_cameraControls.keyboardScale; - - nbl::core::scaleVirtualEvents(virtualEvents, vCount, m_cameraControls.translationScale, m_cameraControls.rotationScale); - - const char* bindingLabel = "Keyboard/Mouse"; - auto applyEventsToCamera = [&](ICamera* target, uint32_t planarIx) - { - if (!target) - return; - - if (m_cameraControls.worldTranslate) - { - std::vector perCameraEvents(virtualEvents.begin(), virtualEvents.begin() + vCount); - uint32_t perCount = vCount; - nbl::core::remapTranslationEventsFromWorldToCameraLocal(target, perCameraEvents, perCount); - if (perCount) - target->manipulate({ perCameraEvents.data(), perCount }); - } - else - { - target->manipulate({ virtualEvents.data(), vCount }); - } - nbl::core::applyCameraConstraints(m_cameraGoalSolver, target, m_cameraConstraints); - if (!m_scriptedInput.enabled) - refreshFollowOffsetConfigForPlanar(planarIx); - appendVirtualEventLog("input", bindingLabel, planarIx, target, virtualEvents.data(), vCount); - }; - - if (m_cameraControls.mirrorInput) - { - std::unordered_set visited; - for (size_t bindingIx = 0u; bindingIx < windowBindings.size(); ++bindingIx) - { - auto& bindingIt = windowBindings[bindingIx]; - auto& planarIt = m_planarProjections[bindingIt.activePlanarIx]; - if (!planarIt) - continue; - auto* target = planarIt->getCamera(); - if (!target) - continue; - if (!visited.insert(target).second) - continue; - applyEventsToCamera(target, bindingIt.activePlanarIx); - } - } - else - { - applyEventsToCamera(camera, binding.activePlanarIx); - } - - if (m_scriptedInput.log) - { - for (uint32_t i = 0u; i < vCount; ++i) - { - const auto& ev = virtualEvents[i]; - m_logger->log("[script] virtual %s magnitude=%.6f", ILogger::ELL_INFO, CVirtualGimbalEvent::virtualEventToString(ev.type).data(), ev.magnitude); - } - - const auto& gimbal = camera->getGimbal(); - const auto pos = gimbal.getPosition(); - const auto euler = getQuaternionEulerDegrees(gimbal.getOrientation()); - m_logger->log("[script] gimbal pos=(%.3f, %.3f, %.3f) euler_deg=(%.3f, %.3f, %.3f)", ILogger::ELL_INFO, - pos.x, pos.y, pos.z, euler.x, euler.y, euler.z); - } - } - } - - if (m_scriptedInput.enabled && scriptedFrameEvents.imguizmo.size() && !skipCameraInput) - { - auto& binding = windowBindings[activeRenderWindowIx]; - auto& planar = m_planarProjections[binding.activePlanarIx]; - auto* camera = planar->getCamera(); - - CGimbalInputBinder imguizmoBinding; - applyDefaultCameraInputBindingPreset(imguizmoBinding, *camera); - auto collectedEvents = imguizmoBinding.collectVirtualEvents(m_nextPresentationTimestamp, { - .imguizmoEvents = { scriptedFrameEvents.imguizmo.data(), scriptedFrameEvents.imguizmo.size() } - }); - auto& imguizmoEvents = collectedEvents.events; - const uint32_t vCount = collectedEvents.imguizmoCount; - - if (vCount) - { - camera->manipulate({ imguizmoEvents.data(), vCount }); - appendVirtualEventLog("imguizmo", "ImGuizmo", binding.activePlanarIx, camera, imguizmoEvents.data(), vCount); - - if (m_scriptedInput.log) - { - for (uint32_t i = 0u; i < vCount; ++i) - { - const auto& ev = imguizmoEvents[i]; - m_logger->log("[script] imguizmo virtual %s magnitude=%.6f", ILogger::ELL_INFO, CVirtualGimbalEvent::virtualEventToString(ev.type).data(), ev.magnitude); - } - - const auto& gimbal = camera->getGimbal(); - const auto pos = gimbal.getPosition(); - const auto euler = getQuaternionEulerDegrees(gimbal.getOrientation()); - m_logger->log("[script] imguizmo gimbal pos=(%.3f, %.3f, %.3f) euler_deg=(%.3f, %.3f, %.3f)", ILogger::ELL_INFO, - pos.x, pos.y, pos.z, euler.x, euler.y, euler.z); - } - } - - scriptedImguizmoVirtual = vCount ? imguizmoEvents.data() : nullptr; - scriptedImguizmoVirtualCount = vCount; - } - - if (m_scriptedInput.enabled && scriptedFrameEvents.goals.size() && !skipCameraInput) - { - auto& binding = windowBindings[activeRenderWindowIx]; - auto& planar = m_planarProjections[binding.activePlanarIx]; - auto* camera = planar->getCamera(); - - 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, - describeApplyResult(result).c_str()); - continue; - } - } - - if (camera) - { - for (auto& projection : planar->getPlanarProjections()) - nbl::core::syncDynamicPerspectiveProjection(camera, projection); - } - - if (m_scriptedInput.log && camera) - { - const auto& gimbal = camera->getGimbal(); - const auto pos = gimbal.getPosition(); - const auto euler = getQuaternionEulerDegrees(gimbal.getOrientation()); - m_logger->log("[script] goal_apply gimbal pos=(%.3f, %.3f, %.3f) euler_deg=(%.3f, %.3f, %.3f)", ILogger::ELL_INFO, - pos.x, pos.y, pos.z, euler.x, euler.y, euler.z); - } - } - - auto tryBuildFollowViewProjForPlanar = [&](const uint32_t planarIx, float32_t4x4& outViewProjMatrix) -> bool - { - if (activeRenderWindowIx >= windowBindings.size()) - return false; - - const auto& binding = windowBindings[activeRenderWindowIx]; - if (binding.activePlanarIx != planarIx || !binding.boundProjectionIx.has_value()) - return false; - if (planarIx >= m_planarProjections.size() || !m_planarProjections[planarIx]) - return false; +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()); +} - auto& planar = m_planarProjections[planarIx]; - auto* camera = planar->getCamera(); - if (!camera) - return false; +} // namespace - const auto projectionIx = binding.boundProjectionIx.value(); - auto& projections = planar->getPlanarProjections(); - if (projectionIx >= projections.size()) - return false; +SAppFrameUpdateState App::buildFrameUpdateState() +{ + SAppFrameUpdateState frameState = {}; + prepareScriptedFrameState(frameState.scripted); + prepareCameraAndUiInput(frameState.scripted, frameState.cameraInput, frameState.ui); + return frameState; +} - const auto viewMatrix = getMatrix3x4As4x4(getCastedMatrix(camera->getGimbal().getViewMatrix())); - const auto projectionMatrix = getCastedMatrix(projections[projectionIx].getProjectionMatrix()); - outViewProjMatrix = mul(projectionMatrix, viewMatrix); - return true; - }; +void App::prepareScriptedFrameState(SAppFrameUpdateState::SPreparedScriptedFrame& outState) +{ + outState = {}; + outState.skipCameraInput = m_playbackAuthoring.playback.playing && m_playbackAuthoring.playback.overrideInput; + dequeueScriptedFrameInput(outState.frame); + applyScriptedFrameActions(outState.frame.frameEvents); + ensureScriptedVisualPlanarState(); +} - auto setScriptedVisualFollowState = [&](const SCameraFollowVisualMetrics& metrics) -> void - { - m_scriptedInput.visualFollowActive = metrics.active; - m_scriptedInput.visualFollowMode = metrics.mode; - m_scriptedInput.visualFollowLockValid = metrics.lockValid; - m_scriptedInput.visualFollowLockAngleDeg = metrics.lockAngleDeg; - m_scriptedInput.visualFollowTargetDistance = metrics.targetDistance; - m_scriptedInput.visualFollowProjectedValid = metrics.projectedValid; - m_scriptedInput.visualFollowTargetCenterNdcX = metrics.projectedNdcX; - m_scriptedInput.visualFollowTargetCenterNdcY = metrics.projectedNdcY; - m_scriptedInput.visualFollowTargetCenterNdcRadius = metrics.projectedNdcRadius; - }; +void App::prepareCameraAndUiInput( + const SAppFrameUpdateState::SPreparedScriptedFrame& scriptedState, + SAppFrameUpdateState::SPreparedCapturedInput& outCameraInput, + SAppFrameUpdateState::SUiRuntimeState& outUiState) +{ + prepareCapturedCameraInput(scriptedState, outCameraInput); + prepareUiRuntimeState(outCameraInput, outUiState); +} - if (!scriptedFrameEvents.trackedTargetTransforms.empty()) - { - setFollowTargetTransform(scriptedFrameEvents.trackedTargetTransforms.back().transform); - applyFollowToConfiguredCameras(true); - SCameraFollowVisualMetrics followMetrics = {}; - if (activeRenderWindowIx < windowBindings.size()) - { - const auto planarIx = windowBindings[activeRenderWindowIx].activePlanarIx; - if (planarIx < m_planarFollowConfigs.size()) - { - auto* activeCamera = planarIx < m_planarProjections.size() && m_planarProjections[planarIx] ? - m_planarProjections[planarIx]->getCamera() : nullptr; - float32_t4x4 viewProjMatrix = float32_t4x4(1.0f); - const float32_t4x4* viewProjMatrixPtr = tryBuildFollowViewProjForPlanar(planarIx, viewProjMatrix) ? &viewProjMatrix : nullptr; - followMetrics = nbl::system::buildFollowVisualMetrics( - activeCamera, - m_followTarget, - &m_planarFollowConfigs[planarIx], - viewProjMatrixPtr); - } - } - setScriptedVisualFollowState(followMetrics); - } - else - { - applyFollowToConfiguredCameras(); - setScriptedVisualFollowState({}); - } +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); +} - if (m_scriptedInput.enabled && m_scriptedInput.checkRuntime.nextCheckIndex < m_scriptedInput.timeline.checks.size()) - { - auto* camera = [&]() -> ICamera* - { - if (m_planarProjections.empty()) - return nullptr; - auto& binding = windowBindings[activeRenderWindowIx]; - if (binding.activePlanarIx >= m_planarProjections.size()) - return nullptr; - return m_planarProjections[binding.activePlanarIx]->getCamera(); - }(); +void App::prepareUiRuntimeState( + const SAppFrameUpdateState::SPreparedCapturedInput& cameraInput, + SAppFrameUpdateState::SUiRuntimeState& outUiState) +{ + outUiState = {}; + outUiState.updateParams = buildUiUpdateParameters(cameraInput.capturedEvents); +} - auto logFail = [&](const char* fmt, auto&&... args) -> void - { - m_scriptedInput.failed = true; - m_logger->log(fmt, ILogger::ELL_ERROR, std::forward(args)...); - }; +void App::runCameraFramePasses(SAppFrameUpdateState& frameState) +{ + applyPreparedCameraInput(frameState.cameraInput, frameState.scripted.skipCameraInput); + runPreparedScriptedFrame(frameState.scripted); +} - auto logPass = [&](const char* fmt, auto&&... args) -> void - { - if (!m_scriptedInput.log) - return; - m_logger->log(fmt, ILogger::ELL_INFO, std::forward(args)...); - }; - SCameraFollowConfig activeFollowConfig = {}; - bool hasActiveFollowConfig = false; - bool hasFollowViewProjMatrix = false; - float32_t4x4 followViewProjMatrix = float32_t4x4(1.0f); - if (activeRenderWindowIx < windowBindings.size()) - { - const auto& binding = windowBindings[activeRenderWindowIx]; - const auto planarIx = binding.activePlanarIx; - if (planarIx < m_planarFollowConfigs.size()) - { - activeFollowConfig = m_planarFollowConfigs[planarIx]; - hasActiveFollowConfig = true; - } - if (camera && planarIx < m_planarProjections.size() && m_planarProjections[planarIx] && binding.boundProjectionIx.has_value()) - { - auto& planar = m_planarProjections[planarIx]; - const auto projectionIx = binding.boundProjectionIx.value(); - auto& projections = planar->getPlanarProjections(); - if (projectionIx < projections.size()) - { - const auto viewMatrix = getMatrix3x4As4x4(getCastedMatrix(camera->getGimbal().getViewMatrix())); - const auto projectionMatrix = getCastedMatrix(projections[projectionIx].getProjectionMatrix()); - followViewProjMatrix = mul(projectionMatrix, viewMatrix); - hasFollowViewProjMatrix = true; - } - } - } +void App::applyPreparedCameraInput( + const SAppFrameUpdateState::SPreparedCapturedInput& cameraInput, + const bool skipCameraInput) +{ + applyActiveCameraInput(cameraInput.keyboardEvents, cameraInput.mouseEvents, skipCameraInput); +} - const auto checkResult = nbl::system::evaluateScriptedChecksForFrame( - m_scriptedInput.timeline.checks, - m_scriptedInput.checkRuntime, - { - .frame = m_realFrameIx, - .camera = camera, - .imguizmoVirtual = scriptedImguizmoVirtual, - .imguizmoVirtualCount = scriptedImguizmoVirtualCount, - .trackedTarget = &m_followTarget, - .followConfig = hasActiveFollowConfig ? &activeFollowConfig : nullptr, - .followViewProjMatrix = hasFollowViewProjMatrix ? &followViewProjMatrix : nullptr, - .goalSolver = &m_cameraGoalSolver - }); +void App::runPreparedScriptedFrame(SAppFrameUpdateState::SPreparedScriptedFrame& scriptedState) +{ + if (m_scriptedInput.log && scriptedState.frame.hasRuntimePayload()) + logScriptedFramePayload(*m_logger, m_realFrameIx, scriptedState.frame); - for (const auto& entry : checkResult.logs) - { - if (entry.failure) - { - m_scriptedInput.failed = true; - logFail("%s", entry.text.c_str()); - } - else - { - logPass("%s", entry.text.c_str()); - } - } + applyScriptedImguizmoInput(scriptedState.frame, scriptedState.skipCameraInput); + applyScriptedGoals(scriptedState.frame.frameEvents, scriptedState.skipCameraInput); + updateScriptedFollowVisualState(scriptedState.frame.frameEvents); + runActiveFrameScriptedChecks(scriptedState.frame); +} - 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); - } - } +void App::updateUiFrame(const SAppFrameUpdateState::SUiRuntimeState& uiState) +{ + UpdateUiMetrics(); + m_ui.manager->update(uiState.updateParams); +} - UpdateUiMetrics(); - m_ui.manager->update(params); +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..5eef8fa23 --- /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::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::endHoverInfoOverlay(); +} diff --git a/61_UI/AppViewportWindows.cpp b/61_UI/AppViewportWindows.cpp new file mode 100644 index 000000000..9271f68f2 --- /dev/null +++ b/61_UI/AppViewportWindows.cpp @@ -0,0 +1,152 @@ +#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::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::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::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(getCameraTypeLabel(viewportState.camera)) + ": " + std::string(getCameraTypeDescription(viewportState.camera)); + overlayData.detail = "Frustum: active camera (hidden in owner view)"; + nbl::ui::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::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 index dc1d7fce7..3f0d2fa9e 100644 --- a/61_UI/AppWorkLoop.cpp +++ b/61_UI/AppWorkLoop.cpp @@ -2,425 +2,18 @@ void App::workLoopBody() { - paceScriptedVisualDebugFrame(); - - // framesInFlight: ensuring safe execution of command buffers and acquires, `framesInFlight` only affect semaphore waits, don't use this to index your resources because it can change with swapchain recreation. - const uint32_t framesInFlight = core::min(MaxFramesInFlight, m_surface->getMaxAcquiresInFlight()); - // We block for semaphores for 2 reasons here: - // A) Resource: Can't use resource like a command buffer BEFORE previous use is finished! [MaxFramesInFlight] - // B) Acquire: Can't have more acquires in flight than a certain threshold returned by swapchain or your surface helper class. [MaxAcquiresInFlight] - if (m_realFrameIx >= framesInFlight) - { - const ISemaphore::SWaitInfo cmdbufDonePending[] = { - { - .semaphore = m_semaphore.get(), - .value = m_realFrameIx + 1 - framesInFlight - } - }; - if (m_device->blockForSemaphores(cmdbufDonePending) != ISemaphore::WAIT_RESULT::SUCCESS) - return; - } - - // Predict size of next render, and bail if nothing to do - const auto currentSwapchainExtent = m_surface->getCurrentExtent(); - if (currentSwapchainExtent.width * currentSwapchainExtent.height <= 0) - return; - // The extent of the swapchain might change between now and `present` but the blit should adapt nicely - const VkRect2D currentRenderArea = { .offset = {0,0},.extent = currentSwapchainExtent }; - - // You explicitly should not use `getAcquireCount()` see the comment on `m_realFrameIx` - const auto resourceIx = m_realFrameIx % MaxFramesInFlight; - - // We will be using this command buffer to produce the frame - auto frame = m_tripleBuffers[resourceIx].get(); - auto cmdbuf = m_cmdBufs[resourceIx].get(); - - // update CPU stuff - input bindings, events, UI state - update(); - - bool willSubmit = true; - { - willSubmit &= cmdbuf->reset(IGPUCommandBuffer::RESET_FLAGS::RELEASE_RESOURCES_BIT); - willSubmit &= cmdbuf->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT); - willSubmit &= cmdbuf->beginDebugMarker("UIApp Frame"); - - auto renderScene = [&](SWindowControlBinding& binding, const uint32_t bindingIx) - { - if (!binding.sceneFramebuffer) - return; - - const auto& fbParams = binding.sceneFramebuffer->getCreationParameters(); - const VkRect2D renderArea = { .offset = {0,0}, .extent = {fbParams.width, fbParams.height} }; - const IGPUCommandBuffer::SRenderpassBeginInfo info = { - .framebuffer = binding.sceneFramebuffer.get(), - .colorClearValues = &SceneClearColor, - .depthStencilClearValues = &SceneClearDepth, - .renderArea = renderArea - }; - - willSubmit &= cmdbuf->beginRenderPass(info, IGPUCommandBuffer::SUBPASS_CONTENTS::INLINE); - { - asset::SViewport viewport = {}; - viewport.minDepth = 1.f; - viewport.maxDepth = 0.f; - viewport.x = 0u; - viewport.y = 0u; - viewport.width = fbParams.width; - viewport.height = fbParams.height; - - willSubmit &= cmdbuf->setViewport(0u, 1u, &viewport); - willSubmit &= cmdbuf->setScissor(0u, 1u, &renderArea); - - if (m_spaceEnvPipeline && m_spaceEnvDescriptorSet) - { - auto* pipelineLayout = m_spaceEnvPipeline->getLayout(); - const IGPUDescriptorSet* descriptorSets[] = { m_spaceEnvDescriptorSet.get() }; - SpaceEnvPushConstants pc = {}; - pc.invProj = hlsl::inverse(binding.projectionMatrix); - pc.invViewRot = hlsl::transpose(getMatrix3x4As4x4(binding.viewMatrix)); - pc.invViewRot[0].w = 0.0f; - pc.invViewRot[1].w = 0.0f; - pc.invViewRot[2].w = 0.0f; - pc.invViewRot[3] = float32_t4(0.0f, 0.0f, 0.0f, 1.0f); - pc.orthoMode = binding.isOrthographicProjection ? 1u : 0u; - - willSubmit &= cmdbuf->bindGraphicsPipeline(m_spaceEnvPipeline.get()); - willSubmit &= cmdbuf->bindDescriptorSets(EPBP_GRAPHICS, pipelineLayout, 0u, 1u, descriptorSets); - willSubmit &= cmdbuf->pushConstants(pipelineLayout, IShader::E_SHADER_STAGE::ESS_FRAGMENT, 0u, sizeof(pc), &pc); - willSubmit &= nbl::ext::FullScreenTriangle::recordDrawCall(cmdbuf); - } - - const auto viewParams = CSimpleDebugRenderer::SViewParams(binding.viewMatrix, binding.viewProjMatrix); - m_renderer->render(cmdbuf, viewParams); - const bool drawScriptFrustum = m_scriptedInput.enabled && m_scriptedInput.visualDebug; - if (m_drawFrustum && drawScriptFrustum) - { - const auto findSourceBindingIxForPlanar = [&](const uint32_t planarIx) -> std::optional - { - if (activeRenderWindowIx < windowBindings.size()) - { - const auto& activeBinding = windowBindings[activeRenderWindowIx]; - if (activeBinding.activePlanarIx == planarIx && activeBinding.boundProjectionIx.has_value()) - return activeRenderWindowIx; - } - - for (uint32_t i = 0u; i < windowBindings.size(); ++i) - { - const auto& candidate = windowBindings[i]; - if (candidate.activePlanarIx != planarIx) - continue; - if (!candidate.boundProjectionIx.has_value()) - continue; - return i; - } - return std::nullopt; - }; - - std::optional sourceBindingIx = std::nullopt; - if (boundPlanarCameraIxToManipulate.has_value()) - sourceBindingIx = findSourceBindingIxForPlanar(boundPlanarCameraIxToManipulate.value()); - if (!sourceBindingIx.has_value() && activeRenderWindowIx < windowBindings.size()) - { - const auto& activeBinding = windowBindings[activeRenderWindowIx]; - if (activeBinding.boundProjectionIx.has_value() && activeBinding.activePlanarIx < m_planarProjections.size()) - sourceBindingIx = activeRenderWindowIx; - } - - if (sourceBindingIx.has_value()) - { - const auto& sourceBinding = windowBindings[sourceBindingIx.value()]; - const bool sameCameraAsView = binding.activePlanarIx == sourceBinding.activePlanarIx; - const bool sameWindow = bindingIx == sourceBindingIx.value(); - if (!sameCameraAsView && !sameWindow) - { - ext::frustum::CDrawFrustum::DrawParameters drawParams = {}; - drawParams.commandBuffer = cmdbuf; - drawParams.viewProjectionMatrix = binding.viewProjMatrix; - drawParams.lineWidth = 1.0f; - - const float32_t4 color = float32_t4(1.0f, 0.95f, 0.25f, 1.0f); - willSubmit &= m_drawFrustum->renderSingle(drawParams, hlsl::inverse(sourceBinding.viewProjMatrix), color); - } - } - } - - } - willSubmit &= cmdbuf->endRenderPass(); - }; - - if (m_renderer && !m_renderer->m_instances.empty()) - { - auto& instance = m_renderer->m_instances[0]; - instance.world = m_model; - const auto geomCount = m_renderer->getGeometries().size(); - if (geomCount) - { - if (gcIndex >= geomCount) - gcIndex = 0; - instance.packedGeo = m_renderer->getGeometries().data() + gcIndex; - } - - const uint32_t gridInstanceIx = 1u; - if (m_gridGeometryIx.has_value() && m_renderer->m_instances.size() > gridInstanceIx) - { - const auto gridIx = m_gridGeometryIx.value(); - if (gridIx < geomCount) - { - auto& gridInstance = m_renderer->m_instances[gridInstanceIx]; - gridInstance.packedGeo = m_renderer->getGeometries().data() + gridIx; - - constexpr float gridExtent = 32.0f; - float32_t3x4 gridWorld = float32_t3x4(1.0f); - gridWorld[0] = float32_t4(gridExtent, 0.0f, 0.0f, -0.5f * gridExtent); - gridWorld[1] = float32_t4(0.0f, 1.0f, 0.0f, -0.5f); - gridWorld[2] = float32_t4(0.0f, 0.0f, gridExtent, -0.5f * gridExtent); - gridInstance.world = gridWorld; - } - } - - const uint32_t followInstanceIx = 1u + (m_gridGeometryIx.has_value() ? 1u : 0u); - if (m_renderer->m_instances.size() > followInstanceIx) - { - auto& followInstance = m_renderer->m_instances[followInstanceIx]; - if (m_followTargetVisible && m_followTargetGeometryIx.has_value() && m_followTargetGeometryIx.value() < geomCount) - { - followInstance.packedGeo = m_renderer->getGeometries().data() + m_followTargetGeometryIx.value(); - followInstance.world = computeFollowTargetMarkerWorld(); - } - else - { - followInstance.packedGeo = nullptr; - followInstance.world = float32_t3x4(1.0f); - } - } - } - - if (useWindow) - for (uint32_t i = 0u; i < windowBindings.size(); ++i) - renderScene(windowBindings[i], i); - else - renderScene(windowBindings[activeRenderWindowIx], activeRenderWindowIx); - - const IGPUCommandBuffer::SClearColorValue clearValue = { .float32 = {0.f,0.f,0.f,1.f} }; - const IGPUCommandBuffer::SRenderpassBeginInfo info = { - .framebuffer = m_framebuffers[resourceIx].get(), - .colorClearValues = &clearValue, - .depthStencilClearValues = nullptr, - .renderArea = currentRenderArea - }; - - // UI renderpass - willSubmit &= cmdbuf->beginRenderPass(info, IGPUCommandBuffer::SUBPASS_CONTENTS::INLINE); - { - asset::SViewport viewport; - { - viewport.minDepth = 1.f; - viewport.maxDepth = 0.f; - viewport.x = 0u; - viewport.y = 0u; - viewport.width = m_window->getWidth(); - viewport.height = m_window->getHeight(); - } - - willSubmit &= cmdbuf->setViewport(0u, 1u, &viewport); - - const VkRect2D currentRenderArea = - { - .offset = {0,0}, - .extent = {m_window->getWidth(),m_window->getHeight()} - }; - - IQueue::SSubmitInfo::SCommandBufferInfo commandBuffersInfo[] = { {.cmdbuf = cmdbuf } }; - - const IGPUCommandBuffer::SRenderpassBeginInfo info = - { - .framebuffer = m_framebuffers[resourceIx].get(), - .colorClearValues = &clearValue, - .depthStencilClearValues = nullptr, - .renderArea = currentRenderArea - }; - - nbl::video::ISemaphore::SWaitInfo waitInfo = { .semaphore = m_semaphore.get(), .value = m_realFrameIx + 1u }; - const auto uiParams = m_ui.manager->getCreationParameters(); - auto* pipeline = m_ui.manager->getPipeline(); - - cmdbuf->bindGraphicsPipeline(pipeline); - cmdbuf->bindDescriptorSets(EPBP_GRAPHICS, pipeline->getLayout(), uiParams.resources.texturesInfo.setIx, 1u, &m_ui.descriptorSet.get()); // note that we use default UI pipeline layout where uiParams.resources.textures.setIx == uiParams.resources.samplers.setIx - - if (!keepRunning()) - return; - - willSubmit &= m_ui.manager->render(cmdbuf, waitInfo); - } - willSubmit &= cmdbuf->endRenderPass(); - - // If the Rendering and Blit/Present Queues don't come from the same family we need to transfer ownership, because we need to preserve contents between them. - auto blitQueueFamily = m_surface->getAssignedQueue()->getFamilyIndex(); - // Also should crash/error if concurrent sharing enabled but would-be-user-queue is not in the share set, but oh well. - const bool needOwnershipRelease = cmdbuf->getQueueFamilyIndex() != blitQueueFamily && !frame->getCachedCreationParams().isConcurrentSharing(); - if (needOwnershipRelease) - { - const IGPUCommandBuffer::SPipelineBarrierDependencyInfo::image_barrier_t barrier[] = { { - .barrier = { - .dep = { - // Normally I'd put `COLOR_ATTACHMENT` on the masks, but we want this to happen after Layout Transition :( - // https://github.com/KhronosGroup/Vulkan-Docs/issues/2319 - .srcStageMask = asset::PIPELINE_STAGE_FLAGS::ALL_COMMANDS_BITS, - .srcAccessMask = asset::ACCESS_FLAGS::MEMORY_READ_BITS | asset::ACCESS_FLAGS::MEMORY_WRITE_BITS, - // For a Queue Family Ownership Release the destination access masks are irrelevant - // and source stage mask can be NONE as long as the semaphore signals ALL_COMMANDS_BIT - .dstStageMask = asset::PIPELINE_STAGE_FLAGS::NONE, - .dstAccessMask = asset::ACCESS_FLAGS::NONE - }, - .ownershipOp = IGPUCommandBuffer::SOwnershipTransferBarrier::OWNERSHIP_OP::RELEASE, - .otherQueueFamilyIndex = blitQueueFamily - }, - .image = frame, - .subresourceRange = TripleBufferUsedSubresourceRange - // there will be no layout transition, already done by the Renderpass End - } }; - const IGPUCommandBuffer::SPipelineBarrierDependencyInfo depInfo = { .imgBarriers = barrier }; - willSubmit &= cmdbuf->pipelineBarrier(asset::EDF_NONE, depInfo); - } - } - willSubmit &= cmdbuf->end(); - - // submit and present under a mutex ASAP - if (willSubmit) - { - // We will signal a semaphore in the rendering queue, and await it with the presentation/blit queue - const IQueue::SSubmitInfo::SSemaphoreInfo rendered = - { - .semaphore = m_semaphore.get(), - .value = m_realFrameIx + 1, - // Normally I'd put `COLOR_ATTACHMENT` on the masks, but we want to signal after Layout Transitions and optional Ownership Release - // https://github.com/KhronosGroup/Vulkan-Docs/issues/2319 - .stageMask = asset::PIPELINE_STAGE_FLAGS::ALL_COMMANDS_BITS - }; - const IQueue::SSubmitInfo::SCommandBufferInfo cmdbufs[1] = - { { - .cmdbuf = cmdbuf - } }; - // We need to wait on previous triple buffer blits/presents from our source image to complete - auto* pBlitWaitValue = m_blitWaitValues.data() + resourceIx; - auto swapchainLock = m_surface->pseudoAcquire(pBlitWaitValue); - const IQueue::SSubmitInfo::SSemaphoreInfo blitted = - { - .semaphore = m_surface->getPresentSemaphore(), - .value = pBlitWaitValue->load(), - // Normally I'd put `BLIT` on the masks, but we want to wait before Implicit Layout Transitions and optional Implicit Ownership Acquire - // https://github.com/KhronosGroup/Vulkan-Docs/issues/2319 - .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; - - m_realFrameIx++; - - const uint64_t renderedFrameIx = m_realFrameIx - 1u; - auto captureScreenshot = [&](const nbl::system::path& outPath, const char* tag) -> void - { - if (!m_device || !m_assetMgr || !m_surface) - 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; - } - - if (!frame) - { - m_logger->log("%s screenshot failed: missing frame image.", 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); - }; - - // only present if there's successful content to show - const ISmoothResizeSurface::SPresentInfo presentInfo = { - { - .source = {.image = frame,.rect = currentRenderArea}, - .waitSemaphore = rendered.semaphore, - .waitValue = rendered.value, - .pPresentSemaphoreWaitValue = pBlitWaitValue, - }, - // The Graphics Queue will be the the most recent owner just before it releases ownership - cmdbuf->getQueueFamilyIndex() - }; - if (m_ciMode && !m_ciScreenshotDone) - { - ++m_ciFrameCounter; - if (m_ciFrameCounter >= CiFramesBeforeCapture) - { - m_ciScreenshotDone = true; - if (!m_disableScreenshotsCli) - captureScreenshot(m_ciScreenshotPath, "CI"); - } - } - - if (!m_disableScreenshotsCli && m_scriptedInput.enabled && !m_scriptedInput.timeline.captureFrames.empty()) - { - 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"); - captureScreenshot(outPath, "Script"); - ++m_scriptedInput.nextCaptureIndex; - } - } - - m_surface->present(std::move(swapchainLock), presentInfo); - } - firstFrame = false; - + 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/CMakeLists.txt b/61_UI/CMakeLists.txt index 60b2ed74c..4f139cd68 100644 --- a/61_UI/CMakeLists.txt +++ b/61_UI/CMakeLists.txt @@ -1,10 +1,47 @@ if(NBL_BUILD_IMGUI AND NBL_EXT_FRUSTUM_LIB) set(NBL_EXTRA_SOURCES + "${CMAKE_CURRENT_SOURCE_DIR}/AppCameraConfigUtilities.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/AppCameraConfigJsonParsing.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/AppCameraPlanarRuntime.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/AppCameraConfiguration.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/AppCameraUiState.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/AppFollowRuntime.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/AppManipulableObjects.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/AppDebugSceneRendererResources.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/AppFrameCapture.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/AppFrameRuntime.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/AppSceneDebugInstances.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/AppSceneRenderPasses.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/AppControlPanel.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/AppControlPanelCameraTab.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/AppControlPanelPlaybackTab.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/AppControlPanelPresetsTab.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/AppControlPanelProjectionTab.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/AppControlPanelStatusTab.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/AppControlPanelTabs.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/AppControlPanelUtilityTabs.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/AppPresentationResources.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/AppPresetPlayback.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/AppHeadlessCameraSmoke.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/AppInputRuntime.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/AppUiInputCapture.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/AppScriptedInitialization.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/AppScriptedInputRuntime.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/AppScriptedValidation.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/AppImGuiListen.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/AppInit.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/AppResourceBootstrap.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/AppTextResourceUtilities.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/AppResourceUtilities.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/AppSceneResources.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/AppSceneFramebufferResources.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/AppSpaceEnvironmentResources.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/AppTransformEditor.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/AppUiResources.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/AppUiRuntime.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/AppUpdate.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/AppViewportGizmos.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/AppViewportWindows.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/AppWorkLoop.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/keysmapping.cpp" ) diff --git a/61_UI/README.md b/61_UI/README.md index 86890e6f2..a515949aa 100644 --- a/61_UI/README.md +++ b/61_UI/README.md @@ -56,7 +56,7 @@ and `61_UI` owns: - Chase - Dolly - DollyZoom -- Path +- Path Rig These are exposed through the active planar/view configuration in the example UI. @@ -83,7 +83,7 @@ and not around a mesh reference. Current default setup: -- `Orbit`, `Arcball`, `Turntable`, `TopDown`, `Isometric`, `DollyZoom`, `Path` +- `Orbit`, `Arcball`, `Turntable`, `TopDown`, `Isometric`, `DollyZoom`, `Path Rig` use `OrbitTarget` - `Chase`, `Dolly` use `KeepLocalOffset` diff --git a/61_UI/app_resources/cameras.json b/61_UI/app_resources/cameras.json index 04a61a69b..6e31e25f5 100644 --- a/61_UI/app_resources/cameras.json +++ b/61_UI/app_resources/cameras.json @@ -52,7 +52,7 @@ "baseFov": 40.0 }, { - "type": "Path", + "type": "PathRig", "position": [12.000, 1.500, 12.000], "target": [0, 0, 0] } diff --git a/61_UI/app_resources/cameraz_continuity.json b/61_UI/app_resources/cameraz_continuity.json index 9ca9f8224..18b98e12e 100644 --- a/61_UI/app_resources/cameraz_continuity.json +++ b/61_UI/app_resources/cameraz_continuity.json @@ -524,7 +524,7 @@ }, { "name": "path_arc", - "camera_kind": "Path", + "camera_kind": "PathRig", "keyframes": [ { "time": 0.0 diff --git a/61_UI/include/app/App.hpp b/61_UI/include/app/App.hpp index f19ddf1ba..5bf26c550 100644 --- a/61_UI/include/app/App.hpp +++ b/61_UI/include/app/App.hpp @@ -14,230 +14,26 @@ #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 "camera/CCubeProjection.hpp" #include "nbl/ext/Frustum/CDrawFrustum.h" #include "nbl/ext/FullScreenTriangle/FullScreenTriangle.h" #include "nbl/ext/ScreenShot/ScreenShot.h" #include "nbl/this_example/builtin/build/spirv/keys.hpp" -class CUIEventCallback : public nbl::video::ISmoothResizeSurface::ICallback +namespace nbl::system { -public: - CUIEventCallback(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)) {} - CUIEventCallback() {} - - void setLogger(nbl::system::logger_opt_smart_ptr& logger) - { - m_logger = logger; - } - void setInputSystem(nbl::core::smart_refctd_ptr&& m_inputSystem) - { - m_inputSystem = std::move(m_inputSystem); - } -private: - - void onMouseConnected_impl(nbl::core::smart_refctd_ptr&& mch) override - { - m_logger.log("A mouse %p has been connected", nbl::system::ILogger::ELL_INFO, mch.get()); - m_inputSystem.get()->add(m_inputSystem.get()->m_mouse, std::move(mch)); - } - void onMouseDisconnected_impl(nbl::ui::IMouseEventChannel* mch) override - { - m_logger.log("A mouse %p has been disconnected", nbl::system::ILogger::ELL_INFO, mch); - m_inputSystem.get()->remove(m_inputSystem.get()->m_mouse, mch); - } - void onKeyboardConnected_impl(nbl::core::smart_refctd_ptr&& kbch) override - { - m_logger.log("A keyboard %p has been connected", nbl::system::ILogger::ELL_INFO, kbch.get()); - m_inputSystem.get()->add(m_inputSystem.get()->m_keyboard, std::move(kbch)); - } - void onKeyboardDisconnected_impl(nbl::ui::IKeyboardEventChannel* kbch) override - { - m_logger.log("A keyboard %p has been disconnected", nbl::system::ILogger::ELL_INFO, kbch); - m_inputSystem.get()->remove(m_inputSystem.get()->m_keyboard, kbch); - } - -private: - nbl::core::smart_refctd_ptr m_inputSystem = nullptr; - nbl::system::logger_opt_smart_ptr m_logger = nullptr; -}; - -class CSwapchainResources final : public ISmoothResizeSurface::ISwapchainResources -{ -public: - // Because we blit to the swapchain image asynchronously, we need a queue which can not only present but also perform graphics commands. - // If we for example used a compute shader to tonemap and MSAA resolve, we'd request the COMPUTE_BIT here. - constexpr static inline IQueue::FAMILY_FLAGS RequiredQueueFlags = IQueue::FAMILY_FLAGS::GRAPHICS_BIT; - - inline uint8_t getLastImageIndex() const { return m_lastImageIndex; } - -protected: - // We can return `BLIT_BIT` here, because the Source Image will be already in the correct layout to be used for the present - 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; - - // Ownership of the Source Blit Image, not the Swapchain Image - const bool needToAcquireSrcOwnership = qFamToAcquireSrcFrom != IQueue::FamilyIgnored; - // Should never get asked to transfer ownership if the source is concurrent sharing - assert(!source.image->getCachedCreationParams().isConcurrentSharing() || !needToAcquireSrcOwnership); - - const auto blitDstLayout = IGPUImage::LAYOUT::TRANSFER_DST_OPTIMAL; - IGPUCommandBuffer::SPipelineBarrierDependencyInfo depInfo = {}; - - // barrier before to transition the swapchain image layout - using image_barrier_t = decltype(depInfo.imgBarriers)::element_type; - const image_barrier_t preBarriers[2] = { - { - .barrier = { - .dep = { - .srcStageMask = asset::PIPELINE_STAGE_FLAGS::NONE, // acquire isn't a stage - .srcAccessMask = asset::ACCESS_FLAGS::NONE, // performs no accesses - .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 = { - // when acquiring ownership the source access masks don't matter - .srcStageMask = asset::PIPELINE_STAGE_FLAGS::NONE, - // Acquire must Happen-Before Semaphore wait, but neither has a true stage so NONE here - // https://github.com/KhronosGroup/Vulkan-Docs/issues/2319 - // If no ownership acquire needed then this dep info won't be used at all - .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 - // no layout transition, already in the correct layout for the blit - } - }; - // We only barrier the source image if we need to acquire ownership, otherwise thanks to Timeline Semaphores all sync is good - 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); - } - - // Barrier after, note that I don't care about preserving the contents of the Triple Buffer when the Render queue starts writing to it again. - // Therefore no ownership release, and no layout transition. - const image_barrier_t postBarrier[1] = { - { - .barrier = { - // When transitioning the image to VK_IMAGE_LAYOUT_SHARED_PRESENT_KHR or VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, there is no need to delay subsequent processing, - // or perform any visibility operations (as vkQueuePresentKHR performs automatic visibility operations). - // To achieve this, the dstAccessMask member of the VkImageMemoryBarrier should be set to 0, and the dstStageMask parameter should be set to VK_PIPELINE_STAGE_2_NONE - .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; -}; - -static smart_refctd_ptr createAttachmentView(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)); -} - -static 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)); + struct SCameraAppResourceContext; + struct SCameraConfigCollections; + struct SCameraPlanarRuntimeBootstrap; + struct CCameraScriptedInputParseResult; } class App final : public examples::SimpleWindowedApplication, public examples::BuiltinResourcesApplication @@ -246,11 +42,6 @@ class App final : public examples::SimpleWindowedApplication, public examples::B using asset_base_t = examples::BuiltinResourcesApplication; using clock_t = std::chrono::steady_clock; - constexpr static inline clock_t::duration DisplayImageDuration = std::chrono::milliseconds(900); - constexpr static inline auto sceneRenderDepthFormat = EF_D32_SFLOAT; - constexpr static inline auto finalSceneRenderFormat = EF_R8G8B8A8_SRGB; - constexpr static inline IGPUCommandBuffer::SClearColorValue SceneClearColor = { .float32 = {0.014f,0.018f,0.030f,1.f} }; - constexpr static inline IGPUCommandBuffer::SClearDepthStencilValue SceneClearDepth = { .depth = 0.f }; struct SpaceEnvPushConstants { float32_t4x4 invProj = float32_t4x4(1.f); @@ -262,203 +53,39 @@ class App final : public examples::SimpleWindowedApplication, public examples::B }; 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 - { - // So let's create our Window and Surface then! - if (!m_surface) - { - { - const auto dpyInfo = m_winMgr->getPrimaryDisplayInfo(); - auto windowCallback = core::make_smart_refctd_ptr(smart_refctd_ptr(m_inputSystem), smart_refctd_ptr(m_logger)); - - IWindow::SCreationParams params = {}; - params.callback = core::make_smart_refctd_ptr(); - params.width = dpyInfo.resX; - params.height = dpyInfo.resY; - params.x = 32; - params.y = 32; - params.flags = IWindow::ECF_INPUT_FOCUS | IWindow::ECF_CAN_RESIZE | IWindow::ECF_CAN_MAXIMIZE | IWindow::ECF_CAN_MINIMIZE; - params.windowCaption = "[Nabla Engine] UI App"; - params.callback = windowCallback; - - 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) - { - m_window->getManager()->maximize(m_window.get()); - auto* cc = m_window->getCursorControl(); - cc->setVisible(true); - - return { {m_surface->getSurface()/*,EQF_NONE*/} }; - } - - return {}; - } - - 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() - { - // UI texture atlas + our camera scene textures, note we don't create info & write pair for the font sampler because UI extension's is immutable and baked into DS layout - static std::array descriptorInfo; - static IGPUDescriptorSet::SWriteDescriptorSet writes[TotalUISampleTexturesAmount]; - - descriptorInfo[nbl::ext::imgui::UI::FontAtlasTexId].info.image.imageLayout = IImage::LAYOUT::READ_ONLY_OPTIMAL; - descriptorInfo[nbl::ext::imgui::UI::FontAtlasTexId].desc = core::smart_refctd_ptr(m_ui.manager->getFontAtlasView()); - writes[nbl::ext::imgui::UI::FontAtlasTexId].info = descriptorInfo.data() + nbl::ext::imgui::UI::FontAtlasTexId; - - for (uint32_t i = 0; i < windowBindings.size(); ++i) - { - const auto textureIx = i + 1u; + using base_t::base_t; - descriptorInfo[textureIx].info.image.imageLayout = IImage::LAYOUT::READ_ONLY_OPTIMAL; - descriptorInfo[textureIx].desc = windowBindings[i].sceneColorView; + inline App(const path& _localInputCWD, const path& _localOutputCWD, const path& _sharedInputCWD, const path& _sharedOutputCWD) + : IApplicationFramework(_localInputCWD, _localOutputCWD, _sharedInputCWD, _sharedOutputCWD) {} - writes[textureIx].info = descriptorInfo.data() + textureIx; - writes[textureIx].info = descriptorInfo.data() + textureIx; - } + // Will get called mid-initialization, via `filterDevices` between when the API Connection is created and Physical Device is chosen + core::vector getSurfaces() const override; - for (uint32_t i = 0; i < descriptorInfo.size(); ++i) - { - writes[i].dstSet = m_ui.descriptorSet.get(); - writes[i].binding = 0u; - writes[i].arrayElement = i; - writes[i].count = 1u; - } + 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; + } - return m_device->updateDescriptorSets(writes, {}); - } + bool updateGUIDescriptorSet(); void workLoopBody() override; - inline void paceScriptedVisualDebugFrame() - { - if (!(m_scriptedInput.enabled && m_scriptedInput.visualDebug)) - { - m_scriptedInput.framePacerInitialized = 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.framePacerInitialized) - { - m_scriptedInput.framePacerInitialized = true; - m_scriptedInput.framePacerNext = now + frameDuration; - return; - } - - if (now < m_scriptedInput.framePacerNext) - std::this_thread::sleep_until(m_scriptedInput.framePacerNext); + void paceScriptedVisualDebugFrame(); - auto postSleepNow = std::chrono::steady_clock::now(); - while (m_scriptedInput.framePacerNext < postSleepNow) - m_scriptedInput.framePacerNext += frameDuration; - } - - inline bool keepRunning() override - { - if (m_headlessCameraSmokeMode) - return false; - - if (m_scriptedInput.enabled && m_scriptedInput.hardFail && m_scriptedInput.failed) - { - if (!m_ciMode || m_ciScreenshotDone) - std::exit(EXIT_FAILURE); - } - if (m_ciMode && m_ciStartedAt != clock_t::time_point::min()) - { - const auto elapsed = clock_t::now() - m_ciStartedAt; - if (elapsed > 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_ciMode && m_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; - } - if (m_surface->irrecoverable()) - return false; - - return true; - } - - inline bool onAppTerminated() override - { - if (m_headlessCameraSmokeMode) - return m_headlessCameraSmokePassed; - - return base_t::onAppTerminated(); - } + bool keepRunning() override; + bool onAppTerminated() override; void update(); + bool runHeadlessCameraSmoke(argparse::ArgumentParser& program, smart_refctd_ptr&& system); private: - struct CUILogFormatter final : public nbl::system::ILogger - { - CUILogFormatter() : ILogger(ILogger::DefaultLogMask()) {} - - std::string format(E_LOG_LEVEL level, 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&, E_LOG_LEVEL, va_list) override {} - }; - - struct VirtualEventLogEntry - { - uint64_t frame = 0; - 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; - }; - + bool initializeMountedCameraResources(smart_refctd_ptr&& system); + nbl::hlsl::uint32_t2 getPresentationRenderExtent() const; + bool shouldMaximizePresentationWindow() const; using CameraPreset = CCameraPreset; using CameraKeyframe = CCameraKeyframe; using CameraKeyframeTrack = CCameraKeyframeTrack; @@ -467,654 +94,80 @@ class App final : public examples::SimpleWindowedApplication, public examples::B using PresetUiAnalysis = SCameraGoalApplyPresentation; using CaptureUiAnalysis = SCameraCapturePresentation; - struct CameraPlaybackState : CCameraPlaybackCursor - { - bool overrideInput = true; - }; - - struct ApplyStatusBanner - { - std::string summary; - bool succeeded = false; - bool approximate = false; - - inline bool visible() const - { - return !summary.empty(); - } - }; - - enum class SceneManipulatedObjectKind : uint8_t - { - Model, - FollowTarget, - Camera - }; - - struct CameraControlSettings - { - bool mirrorInput = false; - bool worldTranslate = false; - float keyboardScale = 0.00625f; - float mouseMoveScale = 1.0f; - float mouseScrollScale = 1.0f; - float translationScale = 1.0f; - float rotationScale = 1.0f; - }; - using CameraConstraintSettings = SCameraConstraintSettings; - inline ICamera* getActiveCamera() - { - auto& binding = windowBindings[activeRenderWindowIx]; - auto& planar = m_planarProjections[binding.activePlanarIx]; - return planar ? planar->getCamera() : nullptr; - } - - inline uint32_t getActivePlanarIx() const - { - return windowBindings[activeRenderWindowIx].activePlanarIx; - } - - inline SCameraFollowConfig* getActiveFollowConfig() - { - const auto planarIx = getActivePlanarIx(); - if (planarIx >= m_planarFollowConfigs.size()) - return nullptr; - return &m_planarFollowConfigs[planarIx]; - } - - inline const SCameraFollowConfig* getActiveFollowConfig() const - { - const auto planarIx = getActivePlanarIx(); - if (planarIx >= m_planarFollowConfigs.size()) - return nullptr; - return &m_planarFollowConfigs[planarIx]; - } - - inline uint32_t getManipulableObjectCount() const - { - return 2u + static_cast(m_planarProjections.size()); - } - - inline bool isManipulableObjectFollowTarget(const uint32_t objectIx) const - { - return objectIx == 1u; - } - - inline std::optional getManipulableObjectPlanarIx(const uint32_t objectIx) const - { - if (objectIx < 2u) - return std::nullopt; - const auto planarIx = objectIx - 2u; - if (planarIx >= m_planarProjections.size()) - return std::nullopt; - return planarIx; - } - - inline uint32_t getManipulatedObjectIx() const - { - switch (m_manipulatedObjectKind) - { - case SceneManipulatedObjectKind::Model: - return 0u; - case SceneManipulatedObjectKind::FollowTarget: - return 1u; - case SceneManipulatedObjectKind::Camera: - default: - return boundPlanarCameraIxToManipulate.has_value() ? (boundPlanarCameraIxToManipulate.value() + 2u) : 0u; - } - } - - inline void bindManipulatedModel() - { - m_manipulatedObjectKind = SceneManipulatedObjectKind::Model; - boundCameraToManipulate = nullptr; - boundPlanarCameraIxToManipulate = std::nullopt; - } - - inline void bindManipulatedFollowTarget() - { - m_manipulatedObjectKind = SceneManipulatedObjectKind::FollowTarget; - boundCameraToManipulate = nullptr; - boundPlanarCameraIxToManipulate = std::nullopt; - } - - inline void 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_manipulatedObjectKind = SceneManipulatedObjectKind::Camera; - boundPlanarCameraIxToManipulate = planarIx; - boundCameraToManipulate = smart_refctd_ptr(camera); - } - - inline void bindManipulatedObjectByIx(const uint32_t objectIx) - { - if (objectIx == 0u) - return bindManipulatedModel(); - if (isManipulableObjectFollowTarget(objectIx)) - return bindManipulatedFollowTarget(); - if (const auto planarIx = getManipulableObjectPlanarIx(objectIx); planarIx.has_value()) - return bindManipulatedCamera(planarIx.value()); - bindManipulatedModel(); - } - - inline std::string getManipulableObjectLabel(const uint32_t objectIx) const - { - if (objectIx == 0u) - return "Model"; - if (isManipulableObjectFollowTarget(objectIx)) - return m_followTarget.getIdentifier(); - if (const auto planarIx = getManipulableObjectPlanarIx(objectIx); planarIx.has_value()) - { - auto* camera = m_planarProjections[planarIx.value()] ? m_planarProjections[planarIx.value()]->getCamera() : nullptr; - if (!camera) - return "Camera " + std::to_string(planarIx.value()); - return std::string(getCameraTypeLabel(camera)) + " Camera"; - } - return "Unknown"; - } - - inline float32_t4x4 getManipulableObjectTransform(const uint32_t objectIx) const - { - if (objectIx == 0u) - return hlsl::transpose(getMatrix3x4As4x4(m_model)); - if (isManipulableObjectFollowTarget(objectIx)) - return getCastedMatrix(m_followTarget.getGimbal().template operator()()); - - if (const auto planarIx = getManipulableObjectPlanarIx(objectIx); planarIx.has_value()) - { - auto* camera = m_planarProjections[planarIx.value()] ? m_planarProjections[planarIx.value()]->getCamera() : nullptr; - if (camera) - return getCastedMatrix(camera->getGimbal().template operator()()); - } - - return float32_t4x4(1.0f); - } - - inline float32_t3 getManipulableObjectWorldPosition(const uint32_t objectIx) const - { - if (objectIx == 0u) - { - const auto modelPos = hlsl::transpose(getMatrix3x4As4x4(m_model))[3]; - return float32_t3(modelPos.x, modelPos.y, modelPos.z); - } - if (isManipulableObjectFollowTarget(objectIx)) - return getCastedVector(m_followTarget.getGimbal().getPosition()); - - if (const auto planarIx = getManipulableObjectPlanarIx(objectIx); planarIx.has_value()) - { - auto* camera = m_planarProjections[planarIx.value()] ? m_planarProjections[planarIx.value()]->getCamera() : nullptr; - if (camera) - return getCastedVector(camera->getGimbal().getPosition()); - } - - return float32_t3(0.0f); - } - - inline float32_t3x4 computeFollowTargetMarkerWorld() const - { - const auto& targetGimbal = m_followTarget.getGimbal(); - const auto position = getCastedVector(targetGimbal.getPosition()); - const auto axisX = getCastedVector(targetGimbal.getXAxis()); - const auto axisY = getCastedVector(targetGimbal.getYAxis()); - const auto axisZ = getCastedVector(targetGimbal.getZAxis()); - const float markerScale = (m_scriptedInput.enabled && m_scriptedInput.visualDebug) ? 0.6f : 0.28f; - return { - float32_t4(axisX * markerScale, position.x), - float32_t4(axisY * markerScale, position.y), - float32_t4(axisZ * markerScale, position.z) - }; - } - - inline void setFollowTargetTransform(const float64_t4x4& transform) - { - m_followTarget.trySetFromTransform(transform); - } - - inline bool captureFollowOffsetsForPlanar(const uint32_t planarIx) - { - if (planarIx >= m_planarProjections.size() || planarIx >= m_planarFollowConfigs.size()) - return false; - auto* camera = m_planarProjections[planarIx] ? m_planarProjections[planarIx]->getCamera() : nullptr; - return nbl::core::captureFollowOffsetsFromCamera(m_cameraGoalSolver, camera, m_followTarget, m_planarFollowConfigs[planarIx]); - } - - inline bool followConfigUsesCapturedOffset(const SCameraFollowConfig& config) const - { - return config.enabled && nbl::core::cameraFollowModeUsesCapturedOffset(config.mode); - } - - inline void refreshFollowOffsetConfigForPlanar(const uint32_t planarIx) - { - if (planarIx >= m_planarProjections.size() || planarIx >= m_planarFollowConfigs.size()) - return; - - auto& config = m_planarFollowConfigs[planarIx]; - if (!followConfigUsesCapturedOffset(config)) - return; - - auto* camera = m_planarProjections[planarIx] ? m_planarProjections[planarIx]->getCamera() : nullptr; - if (!camera) - return; - - nbl::core::captureFollowOffsetsFromCamera(m_cameraGoalSolver, camera, m_followTarget, config); - } - - inline void refreshFollowOffsetConfigsForCamera(ICamera* camera) - { - if (!camera) - return; - - for (uint32_t planarIx = 0u; planarIx < m_planarProjections.size() && planarIx < m_planarFollowConfigs.size(); ++planarIx) - { - auto* planarCamera = m_planarProjections[planarIx] ? m_planarProjections[planarIx]->getCamera() : nullptr; - if (planarCamera != camera) - continue; - refreshFollowOffsetConfigForPlanar(planarIx); - } - } - - inline void refreshAllFollowOffsetConfigs() - { - for (uint32_t planarIx = 0u; planarIx < m_planarProjections.size() && planarIx < m_planarFollowConfigs.size(); ++planarIx) - refreshFollowOffsetConfigForPlanar(planarIx); - } - - inline float64_t3 getDefaultFollowTargetPosition() const - { - return float64_t3(6.0, -4.5, 2.25); - } - - inline camera_quaternion_t getDefaultFollowTargetOrientation() const - { - return makeIdentityQuaternion(); - } - - inline void resetFollowTargetToDefault() - { - m_followTarget.setPose(getDefaultFollowTargetPosition(), getDefaultFollowTargetOrientation()); - } - - inline void snapFollowTargetToModel() - { - const auto modelTransform = hlsl::transpose(getMatrix3x4As4x4(m_model)); - setFollowTargetTransform(getCastedMatrix(modelTransform)); - } - - inline SCameraFollowConfig makeDefaultFollowConfig(ICamera* camera) - { - SCameraFollowConfig config = {}; - if (!camera) - return config; - - switch (camera->getKind()) - { - case ICamera::CameraKind::Orbit: - case ICamera::CameraKind::Arcball: - case ICamera::CameraKind::Turntable: - case ICamera::CameraKind::TopDown: - case ICamera::CameraKind::Isometric: - case ICamera::CameraKind::DollyZoom: - case ICamera::CameraKind::Path: - config.enabled = true; - config.mode = ECameraFollowMode::OrbitTarget; - break; - case ICamera::CameraKind::Chase: - case ICamera::CameraKind::Dolly: - config.enabled = true; - config.mode = ECameraFollowMode::KeepLocalOffset; - break; - default: - break; - } - - return config; - } - - inline void applyFollowToConfiguredCameras(const bool allowDuringScriptedInput = false) - { - if (m_scriptedInput.enabled && !allowDuringScriptedInput) - return; - if (m_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_planarFollowConfigs[planarIx]; - if (!config.enabled || config.mode == ECameraFollowMode::Disabled) - continue; - - const auto result = nbl::core::applyFollowToCamera(m_cameraGoalSolver, camera, m_followTarget, config); - if (!result.succeeded()) - continue; - - for (auto& projection : planar->getPlanarProjections()) - nbl::core::syncDynamicPerspectiveProjection(camera, projection); - } - } - - inline bool isOrbitLikeCamera(ICamera* camera) - { - return camera && camera->hasCapability(ICamera::SphericalTarget); - } - - inline void syncVisualDebugWindowBindings() - { - if (!m_scriptedInput.enabled) - return; - if (windowBindings.size() < 2u || m_planarProjections.empty()) - return; - - auto& perspectiveBinding = windowBindings[0u]; - if (perspectiveBinding.activePlanarIx >= m_planarProjections.size()) - return; - auto& perspectivePlanar = m_planarProjections[perspectiveBinding.activePlanarIx]; - if (!perspectivePlanar) - return; - if (!perspectiveBinding.lastBoundPerspectivePresetProjectionIx.has_value()) - perspectiveBinding.pickDefaultProjections(perspectivePlanar->getPlanarProjections()); - if (perspectiveBinding.lastBoundPerspectivePresetProjectionIx.has_value()) - perspectiveBinding.boundProjectionIx = perspectiveBinding.lastBoundPerspectivePresetProjectionIx.value(); - - auto& orthoBinding = windowBindings[1u]; - if (orthoBinding.activePlanarIx != perspectiveBinding.activePlanarIx) - { - orthoBinding.activePlanarIx = perspectiveBinding.activePlanarIx; - auto& orthoPlanar = m_planarProjections[orthoBinding.activePlanarIx]; - if (!orthoPlanar) - return; - orthoBinding.pickDefaultProjections(orthoPlanar->getPlanarProjections()); - } - if (orthoBinding.activePlanarIx >= m_planarProjections.size()) - return; - auto& orthoPlanar = m_planarProjections[orthoBinding.activePlanarIx]; - if (!orthoPlanar) - return; - if (!orthoBinding.lastBoundOrthoPresetProjectionIx.has_value()) - orthoBinding.pickDefaultProjections(orthoPlanar->getPlanarProjections()); - if (orthoBinding.lastBoundOrthoPresetProjectionIx.has_value()) - orthoBinding.boundProjectionIx = orthoBinding.lastBoundOrthoPresetProjectionIx.value(); - } - - inline void drawScriptVisualDebugOverlay(const ImVec2& displaySize) - { - if (!(m_scriptedInput.enabled && m_scriptedInput.visualDebug)) - return; - if (windowBindings.empty() || m_planarProjections.empty()) - return; - if (activeRenderWindowIx >= windowBindings.size()) - return; - - const auto& binding = windowBindings[activeRenderWindowIx]; - if (binding.activePlanarIx >= m_planarProjections.size()) - return; - - auto& planar = m_planarProjections[binding.activePlanarIx]; - if (!planar) - return; - auto* camera = planar->getCamera(); - if (!camera) - return; - - if (!m_scriptedInput.visualActivePlanarValid) - { - m_scriptedInput.visualActivePlanarValid = true; - m_scriptedInput.visualActivePlanarIx = binding.activePlanarIx; - m_scriptedInput.visualActivePlanarStartFrame = m_realFrameIx; - } - - const uint64_t elapsedFrames = (m_realFrameIx >= m_scriptedInput.visualActivePlanarStartFrame) ? - (m_realFrameIx - m_scriptedInput.visualActivePlanarStartFrame) : 0ull; - const float fps = std::max(1.f, m_scriptedInput.visualTargetFps); - const uint64_t holdFrames = static_cast(std::round(std::max(0.f, m_scriptedInput.visualCameraHoldSeconds) * fps)); - const uint64_t progressFrames = holdFrames ? std::min(elapsedFrames, holdFrames) : elapsedFrames; - - nbl::ui::SCameraScriptVisualDebugStatus debugStatus = {}; - debugStatus.cameraLabel = getCameraTypeLabel(camera); - debugStatus.cameraHint = getCameraTypeDescription(camera); - debugStatus.cameraIndex = binding.activePlanarIx; - debugStatus.cameraCount = static_cast(m_planarProjections.size()); - debugStatus.planarIndex = binding.activePlanarIx; - debugStatus.hasHoldFrames = holdFrames > 0u; - debugStatus.progressFrames = progressFrames; - debugStatus.holdFrames = holdFrames; - debugStatus.targetFps = fps; - debugStatus.absoluteFrame = m_realFrameIx; - debugStatus.segmentLabel = m_scriptedInput.visualSegmentLabel; - debugStatus.followActive = m_scriptedInput.visualFollowActive; - debugStatus.followModeDescription = nbl::ui::getCameraFollowModeDescription(m_scriptedInput.visualFollowMode); - debugStatus.followLockValid = m_scriptedInput.visualFollowLockValid; - debugStatus.followLockAngleDeg = m_scriptedInput.visualFollowLockAngleDeg; - debugStatus.followTargetDistance = m_scriptedInput.visualFollowTargetDistance; - debugStatus.followTargetCenterNdcRadius = m_scriptedInput.visualFollowTargetCenterNdcRadius; - - float dynamicFov = 0.0f; - if (camera && camera->tryGetDynamicPerspectiveFov(dynamicFov)) - { - debugStatus.hasDynamicFov = true; - debugStatus.dynamicFovDeg = dynamicFov; - } - - nbl::ui::drawScriptVisualDebugOverlay(displaySize, nbl::ui::buildScriptVisualDebugOverlayData(debugStatus)); - } - - inline bool tryCaptureGoal(ICamera* camera, CCameraGoal& out) const - { - const auto capture = m_cameraGoalSolver.captureDetailed(camera); - out = capture.goal; - return capture.captured; - } - - inline PresetUiAnalysis analyzePresetForUi(ICamera* camera, const CameraPreset& preset) const - { - return nbl::ui::analyzePresetPresentation(m_cameraGoalSolver, camera, preset); - } - - inline CaptureUiAnalysis analyzeCameraCaptureForUi(ICamera* camera) const - { - return nbl::ui::analyzeCapturePresentation(m_cameraGoalSolver, camera); - } - - inline CCameraGoalSolver::SCompatibilityResult analyzePresetCompatibility(ICamera* camera, const CameraPreset& preset) const - { - return nbl::core::analyzePresetApply(m_cameraGoalSolver, camera, preset).compatibility; - } - - inline bool presetMatchesFilter(ICamera* camera, const CameraPreset& preset) const - { - return analyzePresetForUi(camera, preset).matchesFilter(m_presetFilterMode); - } - - inline CCameraGoalSolver::SApplyResult applyPresetFromUi(ICamera* camera, const CameraPreset& preset) - { - const auto result = nbl::core::applyPresetDetailed(m_cameraGoalSolver, camera, preset); - if (result.succeeded()) - refreshFollowOffsetConfigsForCamera(camera); - const auto presetUi = analyzePresetForUi(camera, preset); - storeApplyStatusBanner(m_manualPresetApplyBanner, - describeApplyResult(result) + " | " + presetUi.compatibilityLabel, - result.succeeded(), - result.approximate()); - return result; - } - - inline void storeApplyStatusBanner(ApplyStatusBanner& banner, std::string summary, const bool succeeded, const bool approximate) - { - banner.summary = std::move(summary); - banner.succeeded = succeeded; - banner.approximate = approximate; - } - - inline void clearApplyStatusBanner(ApplyStatusBanner& banner) - { - banner.summary.clear(); - banner.succeeded = false; - banner.approximate = false; - } - - inline void storePlaybackApplySummary(const SCameraPresetApplySummary& summary) - { - storeApplyStatusBanner(m_playbackApplyBanner, - nbl::ui::describePresetApplySummary(summary, m_playbackAffectsAll ? "Playback apply | no cameras available" : "Playback apply | no active camera"), - summary.succeeded(), - summary.approximate()); - } - - inline void appendVirtualEventLog(std::string_view source, std::string_view inputSource, uint32_t planarIx, ICamera* camera, const CVirtualGimbalEvent* events, uint32_t count) - { - m_uiVirtualEventsThisFrame += 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_virtualEventLog.push_back({ - m_realFrameIx, - events[i].type, - events[i].magnitude, - sourceStr, - inputSourceStr, - cameraName, - planarIx, - std::move(line) - }); - } - - while (m_virtualEventLog.size() > m_virtualEventLogMax) - m_virtualEventLog.pop_front(); - } - - inline SCameraPresetApplySummary applyPresetToTargets(const CameraPreset& preset) - { - SCameraPresetApplySummary summary = {}; - if (!m_playbackAffectsAll) - { - ICamera* activeCamera = getActiveCamera(); - summary = nbl::core::applyPresetToCameraRange(m_cameraGoalSolver, std::span(&activeCamera, activeCamera ? 1u : 0u), preset); - if (summary.succeeded()) - refreshFollowOffsetConfigsForCamera(activeCamera); - return summary; - } - - std::vector cameras; - cameras.reserve(windowBindings.size()); - std::unordered_set visited; - for (auto& binding : 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::applyPresetToCameraRange(m_cameraGoalSolver, std::span(cameras.data(), cameras.size()), preset); - if (summary.succeeded()) - refreshAllFollowOffsetConfigs(); - return summary; - } - - inline bool tryBuildPlaybackPresetAtTime(const float time, CameraPreset& preset) - { - return nbl::core::tryBuildKeyframeTrackPresetAtTime(m_keyframeTrack, time, preset); - } - - inline bool applyPlaybackAtTime(const float time) - { - CameraPreset preset; - if (!tryBuildPlaybackPresetAtTime(time, preset)) - { - clearApplyStatusBanner(m_playbackApplyBanner); - return false; - } - - storePlaybackApplySummary(applyPresetToTargets(preset)); - return true; - } - - inline void sortKeyframesByTime() - { - nbl::core::sortKeyframeTrackByTime(m_keyframeTrack); - } - - inline void clampPlaybackTimeToKeyframes() - { - nbl::core::clampPlaybackCursorToTrack(m_keyframeTrack, m_playback); - } - - inline int selectKeyframeNearestTime(const float time) - { - return nbl::core::selectKeyframeTrackNearestTime(m_keyframeTrack, time); - } - - inline void normalizeSelectedKeyframe() - { - nbl::core::normalizeSelectedKeyframeTrack(m_keyframeTrack); - } - - inline CameraKeyframe* getSelectedKeyframe() - { - return nbl::core::getSelectedKeyframe(m_keyframeTrack); - } - - inline const CameraKeyframe* getSelectedKeyframe() const - { - return nbl::core::getSelectedKeyframe(m_keyframeTrack); - } - - inline bool 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::tryCapturePreset(m_cameraGoalSolver, camera, keyframeName, updatedPreset)) - return false; - - return nbl::core::replaceSelectedKeyframePreset(m_keyframeTrack, std::move(updatedPreset)); - } - - inline void updatePlayback(double dtSec) - { - const auto advance = nbl::core::advancePlaybackCursor(m_playback, m_keyframeTrack, dtSec); - if (!advance.hasTrack || !advance.changedTime) - return; - - applyPlaybackAtTime(m_playback.time); - } + 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; + 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); @@ -1123,118 +176,106 @@ class App final : public examples::SimpleWindowedApplication, public examples::B 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(); - - inline bool shouldCaptureOSCursor() - { - if (!enableActiveCameraMovement || !captureCursorInMoveMode) - return false; - if (m_ciMode || m_scriptedInput.enabled) - return false; - if (!m_window || !m_window->hasInputFocus() || !m_window->hasMouseFocus()) - return false; - return true; - } - - inline void UpdateBoundCameraMovement() - { - ImGuiIO& io = ImGui::GetIO(); - - if (ImGui::IsKeyPressed(ImGuiKey_Space)) - enableActiveCameraMovement = !enableActiveCameraMovement; - - if (enableActiveCameraMovement) - { - io.ConfigFlags |= ImGuiConfigFlags_NoMouse; - io.MouseDrawCursor = false; - io.WantCaptureMouse = false; - - if (shouldCaptureOSCursor()) - { - ImVec2 viewportSize = io.DisplaySize; - auto* cc = m_window->getCursorControl(); - if (cc) - { - int32_t posX = m_window->getX(); - int32_t posY = m_window->getY(); - - if (resetCursorToCenter) - { - const ICursorControl::SPosition middle{ static_cast(viewportSize.x / 2 + posX), static_cast(viewportSize.y / 2 + posY) }; - cc->setPosition(middle); - } - else - { - auto currentCursorPos = cc->getPosition(); - ICursorControl::SPosition newPos{}; - newPos.x = std::clamp(currentCursorPos.x, posX, viewportSize.x + posX); - newPos.y = std::clamp(currentCursorPos.y, posY, viewportSize.y + posY); - cc->setPosition(newPos); - } - } - } - } - else - { - io.ConfigFlags &= ~ImGuiConfigFlags_NoMouse; - io.MouseDrawCursor = false; - io.WantCaptureMouse = true; - } - } - - inline void UpdateCursorVisibility() - { - auto* cc = m_window ? m_window->getCursorControl() : nullptr; - if (!cc) - return; - cc->setVisible(!shouldCaptureOSCursor()); - } - - inline void UpdateUiMetrics() - { - m_uiLastFrameMs = static_cast(m_frameDeltaSec * 1000.0); - m_uiLastInputEvents = m_uiInputEventsThisFrame; - m_uiLastVirtualEvents = m_uiVirtualEventsThisFrame; - - m_uiFrameMs[m_uiMetricIndex] = m_uiLastFrameMs; - m_uiInputCounts[m_uiMetricIndex] = static_cast(m_uiInputEventsThisFrame); - m_uiVirtualCounts[m_uiMetricIndex] = static_cast(m_uiVirtualEventsThisFrame); - - m_uiMetricIndex = (m_uiMetricIndex + 1u) % UiMetricSamples; - m_uiInputEventsThisFrame = 0u; - m_uiVirtualEventsThisFrame = 0u; - } + 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::system::CCameraScriptedInputParseResult parsed, std::optional& pendingScriptedSequence); + std::optional resolveSequenceSegmentPlanarIx(const CCameraSequenceSegment& segment) const; + bool expandPendingScriptedSequence(const CCameraSequenceScript& sequence); + void dequeueScriptedFrameInput(SScriptedFrameInputState& outFrame); + void applyScriptedFrameActions(const CCameraScriptedFrameEvents& scriptedFrameEvents); + 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); + std::optional findFrustumSourceBindingIx(uint32_t planarIx) const; + std::optional tryBuildFrustumOverlaySourceBindingIx() const; + 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(); - inline void addMatrixTable(const char* topText, const char* tableName, int rows, int columns, const float* pointer, bool withSeparator = true) - { - 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 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; @@ -1246,7 +287,7 @@ class App final : public examples::SimpleWindowedApplication, public examples::B // 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 = 3u; + 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 @@ -1282,202 +323,37 @@ class App final : public examples::SimpleWindowedApplication, public examples::B core::smart_refctd_ptr descriptorSet; }; - // Demo scene object rendered into the viewports alongside the tracked target and cameras. - nbl::hlsl::float32_t3x4 m_model = nbl::hlsl::float32_t3x4(1.f); - CTrackedTarget m_followTarget; - std::vector m_planarFollowConfigs; - bool m_followTargetVisible = true; - std::optional m_followTargetGeometryIx = std::nullopt; - SceneManipulatedObjectKind m_manipulatedObjectKind = SceneManipulatedObjectKind::Model; - - nbl::core::smart_refctd_ptr boundCameraToManipulate = nullptr; - std::optional boundPlanarCameraIxToManipulate = std::nullopt; + SCameraAppSceneInteractionState m_sceneInteraction; std::vector> m_planarProjections; - bool enableActiveCameraMovement = false; - bool captureCursorInMoveMode = false; - - bool resetCursorToCenter = true; - - inline void 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; - } - - inline void 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; - } - - struct ScriptedInputState - { - 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 = {}; - size_t nextEventIndex = 0; - CCameraScriptedCheckRuntimeState checkRuntime = {}; - size_t nextCaptureIndex = 0; - std::string capturePrefix = "script"; - nbl::system::path captureOutputDir; - bool failed = false; - bool summaryReported = false; - bool visualActivePlanarValid = false; - uint32_t visualActivePlanarIx = 0u; - uint64_t visualActivePlanarStartFrame = 0u; - std::string visualSegmentLabel; - bool visualFollowActive = false; - ECameraFollowMode visualFollowMode = ECameraFollowMode::Disabled; - bool visualFollowLockValid = false; - float visualFollowLockAngleDeg = 0.0f; - float visualFollowTargetDistance = 0.0f; - bool visualFollowProjectedValid = false; - float visualFollowTargetCenterNdcX = 0.0f; - float visualFollowTargetCenterNdcY = 0.0f; - float visualFollowTargetCenterNdcRadius = 0.0f; - bool scriptedLeftMouseDown = false; - bool scriptedRightMouseDown = false; - bool framePacerInitialized = false; - std::chrono::steady_clock::time_point framePacerNext = {}; - }; + void syncWindowInputBinding(SWindowControlBinding& binding); + void syncWindowInputBindingToProjection(SWindowControlBinding& binding); static constexpr inline auto MaxSceneFBOs = 2u; - std::array windowBindings; - uint32_t activeRenderWindowIx = 0u; + SCameraAppViewportSessionState m_viewports; // UI font atlas + viewport FBO color attachment textures constexpr static inline auto TotalUISampleTexturesAmount = 1u + MaxSceneFBOs; - nbl::core::smart_refctd_ptr m_scene; - nbl::core::smart_refctd_ptr m_sceneRenderpass; - nbl::core::smart_refctd_ptr m_renderer; - nbl::core::smart_refctd_ptr m_drawFrustum; - std::optional m_gridGeometryIx = std::nullopt; - core::smart_refctd_ptr m_spaceEnvPipeline; - core::smart_refctd_ptr m_spaceEnvDescriptorSetLayout; - core::smart_refctd_ptr m_spaceEnvDescriptorPool; - core::smart_refctd_ptr m_spaceEnvDescriptorSet; - core::smart_refctd_ptr m_spaceEnvImage; - core::smart_refctd_ptr m_spaceEnvImageView; - core::smart_refctd_ptr m_spaceEnvSampler; + SCameraAppDebugSceneState m_debugScene; + SCameraAppSpaceEnvironmentState m_spaceEnvironment; CRenderUI m_ui; video::CDumbPresentationOracle oracle; - uint16_t gcIndex = {}; - static constexpr uint32_t CiFramesBeforeCapture = 10u; - static constexpr auto CiMaxRuntime = std::chrono::minutes(2); - bool m_ciMode = false; - bool m_ciScreenshotDone = false; - uint32_t m_ciFrameCounter = 0u; - nbl::system::path m_ciScreenshotPath; - clock_t::time_point m_ciStartedAt = clock_t::time_point::min(); - bool m_scriptVisualDebugCli = false; - bool m_disableScreenshotsCli = false; - bool m_headlessCameraSmokeMode = false; - bool m_headlessCameraSmokePassed = false; - ScriptedInputState m_scriptedInput; + SCameraAppCliRuntimeState m_cliRuntime; + SScriptedInputRuntimeState m_scriptedInput; CameraControlSettings m_cameraControls; CameraConstraintSettings m_cameraConstraints; core::smart_refctd_ptr m_logFormatter; - std::deque m_virtualEventLog; - size_t m_virtualEventLogMax = 128u; - bool m_showHud = true; - bool m_showEventLog = false; - bool m_logAutoScroll = true; - bool m_logWrap = true; - std::vector m_presets; - std::vector m_initialPlanarPresets; - CameraKeyframeTrack m_keyframeTrack; - CameraPlaybackState m_playback; + SCameraAppEventLogState m_eventLog; + SCameraAppPresetAuthoringState m_presetAuthoring; + SCameraAppPlaybackAuthoringState m_playbackAuthoring; CCameraGoalSolver m_cameraGoalSolver; - ApplyStatusBanner m_manualPresetApplyBanner; - ApplyStatusBanner m_playbackApplyBanner; - PresetFilterMode m_presetFilterMode = PresetFilterMode::All; - int m_selectedPresetIx = -1; - bool m_playbackAffectsAll = false; - float m_newKeyframeTime = 0.f; - char m_presetName[64] = "Preset"; - char m_presetPath[260] = "camera_presets.json"; - char m_keyframePath[260] = "camera_keyframes.json"; - std::chrono::microseconds m_lastPresentationTimestamp = {}; - bool m_haveLastPresentationTimestamp = false; - double m_frameDeltaSec = 0.0; - static constexpr size_t UiMetricSamples = 96u; - std::array m_uiFrameMs = {}; - std::array m_uiInputCounts = {}; - std::array m_uiVirtualCounts = {}; - uint32_t m_uiMetricIndex = 0u; - uint32_t m_uiVirtualEventsThisFrame = 0u; - uint32_t m_uiInputEventsThisFrame = 0u; - uint32_t m_uiLastInputEvents = 0u; - uint32_t m_uiLastVirtualEvents = 0u; - float m_uiLastFrameMs = 0.0f; - - const bool flipGizmoY = true; - - float camYAngle = 165.f / 180.f * 3.14159f; - float camXAngle = 32.f / 180.f * 3.14159f; - float camDistance = 8.f; - bool useWindow = true, useSnap = false; - ImGuizmo::OPERATION mCurrentGizmoOperation = ImGuizmo::TRANSLATE; - ImGuizmo::MODE mCurrentGizmoMode = ImGuizmo::LOCAL; - float snap[3] = { 1.f, 1.f, 1.f }; - - bool firstFrame = true; - const float32_t2 iPaddingOffset = float32_t2(10, 10); - - struct ImWindowInit - { - float32_t2 iPos, iSize; - }; - - struct - { - ImWindowInit trsEditor; - ImWindowInit planars; - std::array renderWindows; - } wInit; + SCameraAppPresentationTimingState m_presentationTiming; + SCameraAppUiMetricsState m_uiMetrics; + SCameraAppGizmoState m_gizmoState; }; diff --git a/61_UI/include/app/AppCameraConfigUtilities.hpp b/61_UI/include/app/AppCameraConfigUtilities.hpp index 34a7ef95e..91dd02986 100644 --- a/61_UI/include/app/AppCameraConfigUtilities.hpp +++ b/61_UI/include/app/AppCameraConfigUtilities.hpp @@ -1,166 +1,105 @@ -#ifndef _APP_CAMERA_CONFIG_UTILITIES_HPP_ +#ifndef _APP_CAMERA_CONFIG_UTILITIES_HPP_ #define _APP_CAMERA_CONFIG_UTILITIES_HPP_ -#include +#include #include +#include #include #include "app/AppResourceUtilities.hpp" -#include "camera/CArcballCamera.hpp" -#include "camera/CChaseCamera.hpp" -#include "camera/CDollyCamera.hpp" -#include "camera/CDollyZoomCamera.hpp" -#include "camera/CFPSCamera.hpp" -#include "camera/CFreeLockCamera.hpp" -#include "camera/CIsometricCamera.hpp" -#include "camera/COrbitCamera.hpp" -#include "camera/CPathCamera.hpp" -#include "camera/CTopDownCamera.hpp" -#include "camera/CTurntableCamera.hpp" +#include "app/AppTypes.hpp" namespace nbl::system { -struct SCameraConfigFactoryMotionScales final +struct SCameraInputBindingCollections final { - static constexpr double DefaultMove = core::ICamera::DefaultMoveSpeedScale; - static constexpr double DefaultRotate = core::ICamera::DefaultRotationSpeedScale; - static constexpr double TargetRigMove = 0.5; + std::vector keyboard; + std::vector mouse; }; -inline void initializeCameraMotionConfig(core::ICamera& camera, const double moveScale, const double rotationScale) +struct SCameraViewportBindingSelection final { - camera.setMotionScales(moveScale, rotationScale); -} + std::optional keyboard = std::nullopt; + std::optional mouse = std::nullopt; +}; -inline bool tryCreateCameraFromJson( - const camera_json_t& jCamera, - std::string& error, - core::smart_refctd_ptr& outCamera) +struct SCameraViewportConfig final { - if (!jCamera.contains("type")) - { - error = "Camera entry missing \"type\"."; - return false; - } - - if (!jCamera.contains("position")) - { - error = "Camera entry missing \"position\"."; - return false; - } - - const std::string type = jCamera["type"].get(); - const bool withOrientation = jCamera.contains("orientation"); - const bool withTarget = jCamera.contains("target"); - - const auto position = [&]() - { - const auto value = jCamera["position"].get>(); - return hlsl::float64_t3(value[0], value[1], value[2]); - }(); - - const auto getOrientation = [&]() - { - const auto value = jCamera["orientation"].get>(); - return hlsl::makeQuaternionFromComponents(value[0], value[1], value[2], value[3]); - }; - - const auto getTarget = [&]() - { - const auto value = jCamera["target"].get>(); - return hlsl::float64_t3(value[0], value[1], value[2]); - }; - - const auto finalize = [&](auto&& camera, const double moveScale, const double rotationScale) - { - initializeCameraMotionConfig(*camera, moveScale, rotationScale); - outCamera = std::move(camera); - return true; - }; - - if (type == "FPS") - { - if (!withOrientation) - { - error = "FPS camera requires \"orientation\"."; - return false; - } - return finalize(core::make_smart_refctd_ptr(position, getOrientation()), SCameraConfigFactoryMotionScales::DefaultMove, SCameraConfigFactoryMotionScales::DefaultRotate); - } + uint32_t projectionIx = 0u; + SCameraViewportBindingSelection bindings = {}; +}; - if (type == "Free") - { - if (!withOrientation) - { - error = "Free camera requires \"orientation\"."; - return false; - } - return finalize(core::make_smart_refctd_ptr(position, getOrientation()), SCameraConfigFactoryMotionScales::DefaultMove, SCameraConfigFactoryMotionScales::DefaultRotate); - } +struct SCameraPlanarConfig final +{ + uint32_t cameraIx = 0u; + std::vector viewportIxs = {}; +}; - if (!withTarget) - { - error = "Camera type \"" + type + "\" requires \"target\"."; - return false; - } +struct SCameraPlanarConfigCollections final +{ + std::vector viewports = {}; + std::vector planars = {}; - if (type == "Orbit") - return finalize(core::make_smart_refctd_ptr(position, getTarget()), SCameraConfigFactoryMotionScales::TargetRigMove, SCameraConfigFactoryMotionScales::DefaultRotate); - if (type == "Arcball") - return finalize(core::make_smart_refctd_ptr(position, getTarget()), SCameraConfigFactoryMotionScales::TargetRigMove, SCameraConfigFactoryMotionScales::DefaultRotate); - if (type == "Turntable") - return finalize(core::make_smart_refctd_ptr(position, getTarget()), SCameraConfigFactoryMotionScales::TargetRigMove, SCameraConfigFactoryMotionScales::DefaultRotate); - if (type == "TopDown") - return finalize(core::make_smart_refctd_ptr(position, getTarget()), SCameraConfigFactoryMotionScales::TargetRigMove, SCameraConfigFactoryMotionScales::DefaultRotate); - if (type == "Isometric") - return finalize(core::make_smart_refctd_ptr(position, getTarget()), SCameraConfigFactoryMotionScales::TargetRigMove, SCameraConfigFactoryMotionScales::DefaultRotate); - if (type == "Chase") - return finalize(core::make_smart_refctd_ptr(position, getTarget()), SCameraConfigFactoryMotionScales::TargetRigMove, SCameraConfigFactoryMotionScales::DefaultRotate); - if (type == "Dolly") - return finalize(core::make_smart_refctd_ptr(position, getTarget()), SCameraConfigFactoryMotionScales::TargetRigMove, SCameraConfigFactoryMotionScales::DefaultRotate); - if (type == "Path") - return finalize(core::make_smart_refctd_ptr(position, getTarget()), SCameraConfigFactoryMotionScales::TargetRigMove, SCameraConfigFactoryMotionScales::DefaultRotate); - if (type == "DollyZoom") + inline bool valid() const { - if (jCamera.contains("baseFov")) - return finalize(core::make_smart_refctd_ptr(position, getTarget(), jCamera["baseFov"].get()), SCameraConfigFactoryMotionScales::TargetRigMove, SCameraConfigFactoryMotionScales::DefaultRotate); - return finalize(core::make_smart_refctd_ptr(position, getTarget()), SCameraConfigFactoryMotionScales::TargetRigMove, SCameraConfigFactoryMotionScales::DefaultRotate); + return !planars.empty(); } +}; - error = "Unsupported camera type \"" + type + "\"."; - return false; -} - -inline bool tryLoadCameraCollectionFromJson( - const camera_json_t& json, - std::string& error, - std::vector>& outCameras) +struct SCameraConfigCollections final { - outCameras.clear(); - if (!json.contains("cameras") || !json["cameras"].is_array()) - { - error = "Missing \"cameras\" array in config."; - return false; - } + std::string embeddedScriptedInputText = {}; + std::vector> cameras = {}; + std::vector projections = {}; + SCameraInputBindingCollections bindings = {}; + SCameraPlanarConfigCollections planarConfig = {}; - outCameras.reserve(json["cameras"].size()); - for (const auto& jCamera : json["cameras"]) + inline bool hasEmbeddedScriptedInputText() const { - core::smart_refctd_ptr camera; - if (!tryCreateCameraFromJson(jCamera, error, camera)) - return false; - outCameras.emplace_back(std::move(camera)); + return !embeddedScriptedInputText.empty(); } +}; - if (outCameras.empty()) - { - error = "No cameras defined."; - return false; - } +struct SCameraPlanarRuntimeBootstrap final +{ + SCameraConfigLoadResult loadResult = {}; + SCameraConfigCollections collections = {}; + std::vector> planars = {}; +}; - return true; -} +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 diff --git a/61_UI/include/app/AppControlPanelAuthoringUtilities.hpp b/61_UI/include/app/AppControlPanelAuthoringUtilities.hpp new file mode 100644 index 000000000..625702013 --- /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 "camera/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) + { + drawBadge("EXACT", panelStyle.GoodColor, panelStyle.BadgeTextColor, panelStyle); + } + else if (presentation.badges.bestEffort) + { + drawBadge("BEST-EFFORT", panelStyle.WarnColor, panelStyle.BadgeTextColor, panelStyle); + } + + if (presentation.badges.dropsState) + { + ImGui::SameLine(); + drawBadge("DROPS STATE", panelStyle.WarnColor, panelStyle.BadgeTextColor, panelStyle); + } + else if (presentation.badges.sharedStateOnly) + { + ImGui::SameLine(); + drawBadge("SHARED STATE", panelStyle.AccentColor, panelStyle.BadgeTextColor, panelStyle); + } + + if (presentation.badges.blocked) + { + ImGui::SameLine(); + 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..0dac0de61 --- /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::tryExtractRigidTransformComponents(transform, components)) + return components; + + components.translation = float32_t3(transform[3].x, transform[3].y, transform[3].z); + components.orientation = hlsl::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::composeTransformMatrix( + translation, + hlsl::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..8302d1a35 --- /dev/null +++ b/61_UI/include/app/AppProjectionControlPanelUiUtilities.hpp @@ -0,0 +1,303 @@ +#ifndef _NBL_THIS_EXAMPLE_APP_PROJECTION_CONTROL_PANEL_UI_UTILITIES_HPP_ +#define _NBL_THIS_EXAMPLE_APP_PROJECTION_CONTROL_PANEL_UI_UTILITIES_HPP_ + +#include + +#include "app/AppViewportBindingUtilities.hpp" + +namespace nbl::ui +{ + +using camera_panel_slider_spec_t = SCameraControlPanelSliderSpec; + +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; + } + + 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; + drawHoverHint("Toggle left or right handed projection"); +} + +inline void drawProjectionParameterControls( + SWindowControlBinding& binding, + IPlanarProjection::CProjection& boundProjection, + const bool useWindow) +{ + auto updateParameters = boundProjection.getParameters(); + if (useWindow) + drawCheckboxWithHint({ .label = "Allow axes to flip##allowAxesToFlip", .value = &binding.allowGizmoAxesToFlip, .hint = "Allow ImGuizmo axes to flip based on view" }); + if (useWindow) + 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" } + }) + { + drawSliderFloatWithHint(spec); + } + + switch (boundProjection.getParameters().m_type) + { + case IPlanarProjection::CProjection::Perspective: + 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: + 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; + + 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); + 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); + } + 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; + ImGui::SliderFloat( + "Distance", + &distance, + sphericalState.minDistance, + sphericalState.maxDistance, + "%.4f", + ImGuiSliderFlags_Logarithmic); + drawHoverHint("Current orbit distance"); + camera.trySetSphericalDistance(distance); + } + + if (ImGui::TreeNodeEx("World Data", flags)) + { + auto& gimbal = camera.getGimbal(); + const auto position = getCastedVector(gimbal.getPosition()); + const auto orientation = getCastedVector(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..be84eb5d2 --- /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(getMatrix3x4As4x4(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..a21de41b4 --- /dev/null +++ b/61_UI/include/app/AppResourcePathUtilities.hpp @@ -0,0 +1,218 @@ +#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 "camera/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 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 getSharedEnvmapDirectory(const path& localInputCWD) +{ + return resolveInputPath( + localInputCWD, + path(SCameraMountedResourcePaths::SharedEnvmapRelativeDirectory)); +} + +inline SCameraAppResourcePathCandidates makeSpaceEnvBlobCandidates(const path& localInputCWD) +{ + return makeResourcePathCandidates( + localInputCWD, + path(SCameraMountedResourcePaths::MountedSharedEnvmapWorkingDirectory) / + path(SCameraEnvmapResourcePaths::SpaceEnvBlobCandidate), + EResourceLookupPolicy::MountedOnly); +} + +} // 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 index 18458802d..7b1ffc350 100644 --- a/61_UI/include/app/AppResourceUtilities.hpp +++ b/61_UI/include/app/AppResourceUtilities.hpp @@ -1,27 +1,17 @@ -// 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_THIS_EXAMPLE_APP_RESOURCE_UTILITIES_HPP_ #define _NBL_THIS_EXAMPLE_APP_RESOURCE_UTILITIES_HPP_ -#include -#include -#include -#include +#include #include #include #include #include #include "common.hpp" -#include "nlohmann/json.hpp" namespace nbl::system { -using camera_json_t = nlohmann::json; - struct SSpaceEnvBlobHeader final { uint32_t magic = 0u; @@ -31,246 +21,109 @@ struct SSpaceEnvBlobHeader final uint64_t payloadSize = 0ull; }; -struct SCameraAppResourcePaths final +struct SCameraMountedResourcePaths final { - static constexpr uint32_t SpaceEnvBlobMagic = 0x31425645u; // "EVB1" - static constexpr uint32_t SpaceEnvBlobFormatRgba16Sfloat = 2u; static constexpr std::string_view AppResourcesWorkingDirectory = "app_resources"; - static constexpr std::string_view DefaultCameraConfigRelativePath = "app_resources/cameras.json"; - static constexpr std::string_view SpaceEnvBlobCandidate = "rich_blue_nebulae_1_8k.rgba16f.envblob"; + static constexpr std::string_view MountedSharedEnvmapWorkingDirectory = "app_resources/shared_envmap"; + static constexpr std::string_view SharedEnvmapRelativeDirectory = "../media/envmap"; }; -inline bool loadFileBytes(ISystem& system, const std::filesystem::path& path, std::vector& outPayload) +struct SCameraConfigResourcePaths final { - ISystem::future_t> future; - system.createFile(future, path, IFile::ECF_READ | IFile::ECF_MAPPABLE); - auto file = future.acquire(); - if (!file || !file->get()) - return false; - - auto& input = *file->get(); - const auto fileSize = input.getSize(); - outPayload.resize(fileSize); - if (outPayload.empty()) - return true; - - IFile::success_t readResult; - input.read(readResult, outPayload.data(), 0, fileSize); - return static_cast(readResult); -} + static constexpr size_t CandidateCount = 2u; + static constexpr std::string_view DefaultCameraConfigRelativePath = "cameras.json"; +}; -inline bool loadFileText(ISystem& system, const std::filesystem::path& path, std::string& outText) +struct SCameraEnvmapResourcePaths final { - std::vector payload; - if (!loadFileBytes(system, path, payload)) - return false; - - outText.assign(reinterpret_cast(payload.data()), payload.size()); - return true; -} + static constexpr uint32_t SpaceEnvBlobMagic = 0x31425645u; + static constexpr uint32_t SpaceEnvBlobFormatRgba16Sfloat = 2u; + static constexpr size_t CandidateCount = 2u; + static constexpr std::string_view SpaceEnvBlobCandidate = "rich_blue_nebulae_1_8k.rgba16f.envblob"; +}; -inline bool parseJsonText(std::string_view text, camera_json_t& outJson, std::string* error = nullptr) +struct SCameraAppResourceContext final { - 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; - } -} + ISystem* system = nullptr; + path localInputCWD = {}; -inline bool loadJsonFromPath( - ISystem& system, - const std::filesystem::path& path, - camera_json_t& outJson, - std::string* error = nullptr) -{ - std::string jsonText; - if (!loadFileText(system, path, jsonText)) + inline explicit operator bool() const { - if (error) - *error = "Cannot open config \"" + path.string() + "\"."; - return false; + return system != nullptr; } +}; - return parseJsonText(jsonText, outJson, error); -} - -inline path resolveInputPath(const path& localInputCWD, path pathValue) +inline SCameraAppResourceContext makeCameraAppResourceContext(ISystem& system, const path& localInputCWD) { - if (pathValue.is_relative()) - pathValue = (localInputCWD / pathValue).lexically_normal(); - return pathValue; + return { + .system = &system, + .localInputCWD = localInputCWD + }; } -inline bool loadTextFromMountedResourceOrResolvedPath( - ISystem& system, - const path& localInputCWD, - const path& pathValue, - std::string& outText, - path* outLoadedPath = nullptr) +enum class ECameraConfigLoadSource : uint8_t { - if (loadFileText(system, pathValue, outText)) - { - if (outLoadedPath) - *outLoadedPath = pathValue; - return true; - } - - const auto resolvedPath = resolveInputPath(localInputCWD, pathValue); - if (resolvedPath == pathValue) - return false; - - if (!loadFileText(system, resolvedPath, outText)) - return false; - - if (outLoadedPath) - *outLoadedPath = resolvedPath; - return true; -} + RequestedPath, + DefaultConfig +}; -inline bool loadJsonFromMountedResourceOrResolvedPath( - ISystem& system, - const path& localInputCWD, - const path& pathValue, - camera_json_t& outJson, - path* outLoadedPath = nullptr, - std::string* error = nullptr) +struct SCameraConfigLoadRequest final { - if (loadJsonFromPath(system, pathValue, outJson, error)) - { - if (outLoadedPath) - *outLoadedPath = pathValue; - return true; - } - - const auto resolvedPath = resolveInputPath(localInputCWD, pathValue); - if (resolvedPath == pathValue) - return false; - - if (!loadJsonFromPath(system, resolvedPath, outJson, error)) - return false; - - if (outLoadedPath) - *outLoadedPath = resolvedPath; - return true; -} + std::optional requestedPath = std::nullopt; + bool fallbackToDefault = false; +}; -inline bool parseSpaceEnvBlobBytes( - std::span blobBytes, - SSpaceEnvBlobHeader& outHeader, - std::vector& outPayload) +struct SCameraConfigLoadResult final { - if (blobBytes.size() < sizeof(SSpaceEnvBlobHeader)) - return false; - - std::memcpy(&outHeader, blobBytes.data(), sizeof(outHeader)); + std::string text = {}; + path loadedPath = {}; + ECameraConfigLoadSource source = ECameraConfigLoadSource::DefaultConfig; + bool requestedPathLoadFailed = false; + std::string requestedPathError = {}; - if (outHeader.magic != SCameraAppResourcePaths::SpaceEnvBlobMagic || - outHeader.format != SCameraAppResourcePaths::SpaceEnvBlobFormatRgba16Sfloat) + inline bool usedRequestedPath() const { - return false; + return source == ECameraConfigLoadSource::RequestedPath; } - 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( - ISystem& system, - const std::filesystem::path& blobPath, - SSpaceEnvBlobHeader& outHeader, - std::vector& outPayload) -{ - std::vector blobBytes; - if (!loadFileBytes(system, blobPath, blobBytes)) - return false; - return parseSpaceEnvBlobBytes(blobBytes, outHeader, outPayload); -} - -inline std::array makeSpaceEnvSearchRoots(const path& localInputCWD) -{ - return { - (localInputCWD / ".." / "media" / "envmap").lexically_normal(), - (localInputCWD / ".." / "media").lexically_normal(), - (localInputCWD / SCameraAppResourcePaths::AppResourcesWorkingDirectory).lexically_normal() - }; -} -inline bool loadFirstSpaceEnvBlobFromRoots( - ISystem& system, - const std::array& searchRoots, - SSpaceEnvBlobHeader& outHeader, - std::vector& outPayload) -{ - for (const auto& root : searchRoots) + inline bool usedDefaultConfig() const { - if (loadSpaceEnvBlob(system, root / SCameraAppResourcePaths::SpaceEnvBlobCandidate, outHeader, outPayload)) - return true; + return source == ECameraConfigLoadSource::DefaultConfig; } - return false; -} +}; -inline bool loadPreferredSpaceEnvBlob( - ISystem& system, - const path& localInputCWD, - SSpaceEnvBlobHeader& outHeader, - std::vector& outPayload, - path* outLoadedPath = nullptr) +struct SCameraScriptTextLoadResult final { - const path mountedPath = path(SCameraAppResourcePaths::AppResourcesWorkingDirectory) / path(SCameraAppResourcePaths::SpaceEnvBlobCandidate); - if (loadSpaceEnvBlob(system, mountedPath, outHeader, outPayload)) - { - if (outLoadedPath) - *outLoadedPath = mountedPath; - return true; - } + std::string text = {}; + path loadedPath = {}; +}; - const auto searchRoots = makeSpaceEnvSearchRoots(localInputCWD); - for (const auto& root : searchRoots) - { - const auto candidate = root / path(SCameraAppResourcePaths::SpaceEnvBlobCandidate); - if (!loadSpaceEnvBlob(system, candidate, outHeader, outPayload)) - continue; +bool tryLoadCameraConfigText( + const SCameraAppResourceContext& context, + const SCameraConfigLoadRequest& request, + SCameraConfigLoadResult& outResult, + std::string* error = nullptr); - if (outLoadedPath) - *outLoadedPath = candidate; - return true; - } +bool tryLoadCameraScriptText( + const SCameraAppResourceContext& context, + const path& scriptPath, + SCameraScriptTextLoadResult& outResult, + std::string* error = nullptr); - return false; -} +bool mountOptionalSharedEnvmapResources( + const SCameraAppResourceContext& context, + ILogger* logger = nullptr); + +bool loadPreferredSpaceEnvBlob( + const SCameraAppResourceContext& context, + SSpaceEnvBlobHeader& outHeader, + std::vector& outPayload, + path* outLoadedPath = nullptr); -inline core::smart_refctd_ptr loadPrecompiledShaderFromAppResources( +core::smart_refctd_ptr loadPrecompiledShaderFromAppResources( asset::IAssetManager& assetManager, ILogger* logger, - const std::string_view key) -{ - asset::IAssetLoader::SAssetLoadParams loadParams = {}; - loadParams.logger = logger; - loadParams.workingDirectory = SCameraAppResourcePaths::AppResourcesWorkingDirectory; - auto bundle = assetManager.getAsset(key.data(), loadParams); - const auto& contents = bundle.getContents(); - if (contents.empty()) - return nullptr; - return asset::IAsset::castDown(contents[0]); -} + std::string_view key); } // namespace nbl::system 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 index 6b6e5cb08..cbcf795ca 100644 --- a/61_UI/include/app/AppTypes.hpp +++ b/61_UI/include/app/AppTypes.hpp @@ -1,6 +1,17 @@ #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; @@ -18,6 +29,8 @@ struct ImGuizmoModelM16InOut 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; @@ -37,7 +50,7 @@ struct SWindowControlBinding final std::optional lastBoundPerspectivePresetProjectionIx = std::nullopt; std::optional lastBoundOrthoPresetProjectionIx = std::nullopt; std::optional inputBindingProjectionIx = std::nullopt; - uint32_t inputBindingPlanarIx = std::numeric_limits::max(); + uint32_t inputBindingPlanarIx = InvalidPlanarIx; inline void pickDefaultProjections(const planar_projections_range_t& projections) { @@ -60,7 +73,637 @@ struct SWindowControlBinding final init(lastBoundOrthoPresetProjectionIx = std::nullopt, IPlanarProjection::CProjection::Orthographic); boundProjectionIx = lastBoundPerspectivePresetProjectionIx.value(); inputBindingProjectionIx = std::nullopt; - inputBindingPlanarIx = std::numeric_limits::max(); + 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 = makeIdentityQuaternion(); +}; + +inline float32_t3x4 buildFollowTargetMarkerWorldTransform( + const CTrackedTarget& trackedTarget, + const float markerScale) +{ + const auto& targetGimbal = trackedTarget.getGimbal(); + const auto position = getCastedVector(targetGimbal.getPosition()); + const auto orientation = getCastedVector(targetGimbal.getOrientation().data); + const auto markerTransform = hlsl::composeTransformMatrix( + position, + 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 float32_t4 FrustumColor = float32_t4(1.0f, 0.95f, 0.25f, 1.0f); + static inline constexpr IGPUCommandBuffer::SClearColorValue UiClearColor = { .float32 = { 0.0f, 0.0f, 0.0f, 1.0f } }; +}; + +struct SCameraAppSceneDebugDefaults final +{ + static inline constexpr float FrustumLineWidth = 1.0f; + 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 = nbl::core::ICamera::DefaultMoveSpeedScale; + static inline constexpr double DefaultRotateScale = nbl::core::ICamera::DefaultRotationSpeedScale; + 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 = {}; + size_t nextEventIndex = 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 = {}; + nbl::core::smart_refctd_ptr frustumDrawer = {}; + 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 mouse; + std::vector keyboard; + std::vector imguizmoVirtualEvents; + + inline bool hasRuntimePayload() const + { + return !keyboard.empty() || + !mouse.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; } }; diff --git a/61_UI/include/app/AppViewportBindingUtilities.hpp b/61_UI/include/app/AppViewportBindingUtilities.hpp index 59c21bd34..ad3eef52c 100644 --- a/61_UI/include/app/AppViewportBindingUtilities.hpp +++ b/61_UI/include/app/AppViewportBindingUtilities.hpp @@ -5,6 +5,7 @@ #include #include "app/AppTypes.hpp" +#include "camera/CCameraFollowRegressionUtilities.hpp" namespace nbl::ui { @@ -19,6 +20,137 @@ struct SBoundViewportCameraState final 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, @@ -48,7 +180,7 @@ inline bool tryBuildWindowBindingMatrices( outState.camera = camera; outState.projection = &projection; - outState.viewMatrix = getMatrix3x4As4x4(getCastedMatrix(camera->getGimbal().getViewMatrix())); + outState.viewMatrix = getCastedMatrix(getMatrix3x4As4x4(camera->getGimbal().getViewMatrix())); outState.projectionMatrix = getCastedMatrix(projection.getProjectionMatrix()); outState.viewProjMatrix = mul(outState.projectionMatrix, outState.viewMatrix); @@ -59,6 +191,54 @@ inline bool tryBuildWindowBindingMatrices( 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, @@ -81,6 +261,43 @@ inline bool tryBuildViewportBoundCameraState( 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..5ad9b9888 --- /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; + + 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/common.hpp b/61_UI/include/common.hpp index a2ae9c595..9206c36ee 100644 --- a/61_UI/include/common.hpp +++ b/61_UI/include/common.hpp @@ -25,12 +25,14 @@ #include "camera/CCameraSequenceScript.hpp" #include "camera/CCameraSequenceScriptedBuilder.hpp" #include "camera/CCameraScriptedRuntime.hpp" +#include "camera/CCameraScriptedUiInputUtilities.hpp" #include "camera/CCameraScriptedCheckRunner.hpp" #include "camera/CCameraGoalAnalysis.hpp" #include "camera/CCameraGoalSolver.hpp" #include "camera/CCameraManipulationUtilities.hpp" #include "camera/CCameraPresentationUtilities.hpp" #include "camera/CCameraProjectionUtilities.hpp" +#include "camera/CCameraKindUtilities.hpp" #include "camera/CCameraFollowUtilities.hpp" #include "camera/CCameraFollowRegressionUtilities.hpp" #include "camera/CCameraControlPanelUiUtilities.hpp" diff --git a/61_UI/src/keysmapping.cpp b/61_UI/src/keysmapping.cpp index a4a9d3990..f2305f2f8 100644 --- a/61_UI/src/keysmapping.cpp +++ b/61_UI/src/keysmapping.cpp @@ -1,14 +1,31 @@ #include "keysmapping.hpp" +#include "app/AppTypes.hpp" + #include #include +namespace +{ + +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; +} + +} // namespace + 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, 0.33f); - ImGui::TableSetupColumn("Key", ImGuiTableColumnFlags_WidthStretch, 0.33f); - ImGui::TableSetupColumn("Actions", ImGuiTableColumnFlags_WidthStretch, 0.33f); + 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(); @@ -30,14 +47,14 @@ bool handleAddMapping(const char* tableID, IGimbalBindingLayout* layout, IGimbal ImGui::TableSetColumnIndex(1); if (activeBindingDomain == IGimbalBindingLayout::Keyboard) { - char newKeyDisplay[2] = { ui::keyCodeToChar(newKey, true), '\0' }; - if (ImGui::BeginCombo("##selectKey", newKeyDisplay)) + 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)); - char label[2] = { ui::keyCodeToChar(static_cast(i), true), '\0' }; - if (ImGui::Selectable(label, isSelected)) + const auto label = buildKeyCodeLabel(static_cast(i)); + if (ImGui::Selectable(label.c_str(), isSelected)) newKey = static_cast(i); if (isSelected) ImGui::SetItemDefaultFocus(); @@ -62,7 +79,7 @@ bool handleAddMapping(const char* tableID, IGimbalBindingLayout* layout, IGimbal } ImGui::TableSetColumnIndex(2); - if (ImGui::Button("Confirm Add", ImVec2(100, 30))) + if (ImGui::Button("Confirm Add", SCameraAppBindingEditorUiDefaults::ActionButtonSize)) { anyMapUpdated |= true; if (activeBindingDomain == IGimbalBindingLayout::Keyboard) @@ -100,7 +117,7 @@ bool displayKeyMappingsAndVirtualStatesInline(IGimbalBindingLayout* layout, bool if (spawnWindow) { - ImGui::SetNextWindowSize(ImVec2(600, 400), ImGuiCond_FirstUseEver); + ImGui::SetNextWindowSize(SCameraAppBindingEditorUiDefaults::WindowInitialSize, ImGuiCond_FirstUseEver); ImGui::Begin("Binding Layouts & Virtual States", nullptr, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_AlwaysVerticalScrollbar); } @@ -111,7 +128,7 @@ bool displayKeyMappingsAndVirtualStatesInline(IGimbalBindingLayout* layout, bool state.activeBindingDomain = IGimbalBindingLayout::Keyboard; ImGui::Separator(); - if (ImGui::Button("Add Key", ImVec2(100, 30))) + if (ImGui::Button("Add Key", SCameraAppBindingEditorUiDefaults::ActionButtonSize)) state.addMode = !state.addMode; ImGui::Separator(); @@ -133,13 +150,13 @@ bool displayKeyMappingsAndVirtualStatesInline(IGimbalBindingLayout* layout, bool ImGui::TextWrapped("%s", eventName); ImGui::TableSetColumnIndex(1); - std::string keyString(1, ui::keyCodeToChar(keyboardCode, true)); + const auto keyString = buildKeyCodeLabel(keyboardCode); ImGui::AlignTextToFramePadding(); ImGui::TextWrapped("%s", keyString.c_str()); ImGui::TableSetColumnIndex(2); bool isActive = (hash.event.magnitude > 0); - ImVec4 statusColor = isActive ? ImVec4(0.0f, 1.0f, 0.0f, 1.0f) : ImVec4(1.0f, 0.0f, 0.0f, 1.0f); + const ImVec4 statusColor = getBindingActiveStatusColor(isActive); ImGui::TextColored(statusColor, "%s", isActive ? "Active" : "Inactive"); ImGui::TableSetColumnIndex(3); @@ -169,7 +186,7 @@ bool displayKeyMappingsAndVirtualStatesInline(IGimbalBindingLayout* layout, bool state.activeBindingDomain = IGimbalBindingLayout::Mouse; ImGui::Separator(); - if (ImGui::Button("Add Key", ImVec2(100, 30))) + if (ImGui::Button("Add Key", SCameraAppBindingEditorUiDefaults::ActionButtonSize)) state.addMode = !state.addMode; ImGui::Separator(); @@ -197,7 +214,7 @@ bool displayKeyMappingsAndVirtualStatesInline(IGimbalBindingLayout* layout, bool ImGui::TableSetColumnIndex(2); bool isActive = (hash.event.magnitude > 0); - ImVec4 statusColor = isActive ? ImVec4(0.0f, 1.0f, 0.0f, 1.0f) : ImVec4(1.0f, 0.0f, 0.0f, 1.0f); + const ImVec4 statusColor = getBindingActiveStatusColor(isActive); ImGui::TextColored(statusColor, "%s", isActive ? "Active" : "Inactive"); ImGui::TableSetColumnIndex(3); diff --git a/common/include/camera/CArcballCamera.hpp b/common/include/camera/CArcballCamera.hpp index a4b367b8e..94b2af05c 100644 --- a/common/include/camera/CArcballCamera.hpp +++ b/common/include/camera/CArcballCamera.hpp @@ -35,21 +35,16 @@ class CArcballCamera final : public CSphericalTargetCamera const auto impulse = m_gimbal.accumulate(virtualEvents); - const double deltaYaw = scaleVirtualRotation(impulse.dVirtualRotation.y); - const double deltaPitch = scaleVirtualRotation(impulse.dVirtualRotation.x); - - const double deltaPanX = scaleVirtualTranslation(impulse.dVirtualTranslate.x); - const double deltaPanY = scaleVirtualTranslation(impulse.dVirtualTranslate.y); + const auto deltaRotation = scaleVirtualRotation(impulse.dVirtualRotation); + const auto deltaTranslation = scaleVirtualTranslation(impulse.dVirtualTranslate); const double deltaDistance = scaleUnscaledVirtualTranslation(impulse.dVirtualTranslate.z); - m_u += deltaYaw; - m_v = std::clamp(m_v + deltaPitch, MinPitch, MaxPitch); + m_u += deltaRotation.y; + m_v = std::clamp(m_v + deltaRotation.x, MinPitch, MaxPitch); m_distance = std::clamp(m_distance + static_cast(deltaDistance), MinDistance, MaxDistance); const auto basis = computeBasis(m_u, m_v, m_distance); - - if (deltaPanX != 0.0 || deltaPanY != 0.0) - m_targetPosition += basis.right * deltaPanX + basis.up * deltaPanY; + applyPlanarTargetTranslation(deltaTranslation, basis); return applyPose(); } @@ -64,7 +59,7 @@ class CArcballCamera final : public CSphericalTargetCamera private: static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate | CVirtualGimbalEvent::Rotate; - static inline constexpr double MaxPitch = hlsl::numbers::pi * (89.0 / 180.0); + static inline constexpr double MaxPitch = SCameraTargetRelativeRigDefaults::ArcballPitchLimitRad; static inline constexpr double MinPitch = -MaxPitch; }; diff --git a/common/include/camera/CCameraControlPanelUiUtilities.hpp b/common/include/camera/CCameraControlPanelUiUtilities.hpp index 2b81aada4..7fc79b7f6 100644 --- a/common/include/camera/CCameraControlPanelUiUtilities.hpp +++ b/common/include/camera/CCameraControlPanelUiUtilities.hpp @@ -8,8 +8,11 @@ #include #include #include +#include +#include #include "imgui/imgui.h" +#include "imgui/misc/cpp/imgui_stdlib.h" namespace nbl::ui { @@ -17,6 +20,7 @@ namespace nbl::ui //! 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.19f; static constexpr float WindowMinWidth = 200.0f; static constexpr float WindowMaxWidthRatio = 0.25f; @@ -107,6 +111,10 @@ struct SCameraControlPanelStyle final static constexpr float HeaderMetricFontScale = 1.05f; static constexpr float HeaderDummyY = 1.0f; static constexpr float HeaderGapSmall = 2.0f; + static constexpr float TabChildRounding = 4.0f; + static constexpr ImVec2 TogglePadding = ImVec2(6.0f, 2.0f); + static constexpr float KeyframeListHeight = 120.0f; + static constexpr float EventLogBottomThreshold = 5.0f; static constexpr float DefaultFrameMetricMin = 16.0f; static constexpr float DefaultEventMetricMin = 4.0f; @@ -135,6 +143,79 @@ struct SCameraControlPanelMiniStatSpec final 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" }; +}; + +template +inline void drawSummaryRow(const char* label, DrawValueFn&& drawValue) +{ + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::TextUnformatted(label); + ImGui::TableSetColumnIndex(1); + drawValue(); +} + +inline void drawDot(const ImVec4& color, const SCameraControlPanelStyle& style); + +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()); + }); +} + +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()); +} + inline ImVec2 calcControlPanelWindowSize(const ImVec2& displaySize, const SCameraControlPanelStyle& style = {}) { return ImVec2( @@ -142,6 +223,11 @@ inline ImVec2 calcControlPanelWindowSize(const ImVec2& displaySize, const SCamer std::clamp(displaySize.y * style.WindowHeightRatio, style.WindowMinHeight, displaySize.y * style.WindowMaxHeightRatio)); } +inline float calcFramesPerSecond(const float frameMs, const SCameraControlPanelStyle& style = {}) +{ + return frameMs > 0.0f ? (style.MillisecondsPerSecond / frameMs) : 0.0f; +} + inline float calcPillWidth(const char* label, const ImVec2& padding) { return ImGui::CalcTextSize(label).x + padding.x * 2.0f; @@ -191,6 +277,14 @@ inline void popControlPanelWindowStyle() ImGui::PopStyleVar(9); } +inline bool inputTextString( + const char* label, + std::string& value, + ImGuiInputTextFlags flags = 0) +{ + return ImGui::InputText(label, &value, flags); +} + 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); @@ -377,6 +471,48 @@ inline void drawHoverHint(const char* text) ImGui::EndTooltip(); } +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; +} + +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; +} + +inline bool drawActionButtonWithHint(const char* label, const char* hint) +{ + const bool pressed = ImGui::Button(label); + if (hint && hint[0] != '\0') + drawHoverHint(hint); + return pressed; +} + +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); +} + +inline void endControlPanelTabChild() +{ + ImGui::EndChild(); + ImGui::PopStyleVar(); +} + inline void drawDot(const ImVec4& color, const SCameraControlPanelStyle& style = {}) { const ImVec2 cursor = ImGui::GetCursorScreenPos(); diff --git a/common/include/camera/CCameraFileUtilities.hpp b/common/include/camera/CCameraFileUtilities.hpp new file mode 100644 index 000000000..19c9f8eaa --- /dev/null +++ b/common/include/camera/CCameraFileUtilities.hpp @@ -0,0 +1,82 @@ +#ifndef _C_CAMERA_FILE_UTILITIES_HPP_ +#define _C_CAMERA_FILE_UTILITIES_HPP_ + +#include +#include +#include + +#include "nbl/system/ISystem.h" + +namespace nbl::system +{ + +inline bool readBinaryFile( + ISystem& system, + const path& filePath, + std::vector& outPayload, + std::string* error = nullptr, + const std::string_view openError = {}) +{ + ISystem::future_t> future; + system.createFile(future, filePath, IFile::ECF_READ | IFile::ECF_MAPPABLE); + auto file = future.acquire(); + if (!file || !file->get()) + { + if (error && !openError.empty()) + *error = std::string(openError); + return false; + } + + auto& input = *file->get(); + const auto fileSize = input.getSize(); + outPayload.resize(fileSize); + if (outPayload.empty()) + return true; + + IFile::success_t readResult; + input.read(readResult, outPayload.data(), 0, fileSize); + if (!static_cast(readResult)) + { + if (error && !openError.empty()) + *error = std::string(openError); + return false; + } + return true; +} + +inline bool readTextFile( + ISystem& system, + const path& filePath, + std::string& outText, + std::string* error = nullptr, + const std::string_view openError = {}) +{ + std::vector payload; + if (!readBinaryFile(system, filePath, payload, error, openError)) + return false; + + outText.assign(reinterpret_cast(payload.data()), payload.size()); + return true; +} + +inline bool writeTextFile( + ISystem& system, + const path& filePath, + const std::string_view text) +{ + ISystem::future_t> future; + system.createFile(future, filePath, IFile::ECF_WRITE); + auto file = future.acquire(); + if (!file || !file->get()) + return false; + if (text.empty()) + return true; + + IFile::success_t writeResult; + (*file)->write(writeResult, text.data(), 0, text.size()); + return static_cast(writeResult); +} + +} // namespace nbl::system + +#endif // _C_CAMERA_FILE_UTILITIES_HPP_ diff --git a/common/include/camera/CCameraFollowRegressionUtilities.hpp b/common/include/camera/CCameraFollowRegressionUtilities.hpp index a8e1b1d63..e0e3a60c8 100644 --- a/common/include/camera/CCameraFollowRegressionUtilities.hpp +++ b/common/include/camera/CCameraFollowRegressionUtilities.hpp @@ -12,6 +12,12 @@ namespace nbl::system { +struct SCameraProjectedTargetMetrics final +{ + hlsl::float32_t2 ndc = hlsl::float32_t2(0.0f); + float radius = 0.0f; +}; + /** * Reusable follow-contract validation helpers. * @@ -29,9 +35,7 @@ struct SCameraFollowRegressionResult float lockAngleDeg = 0.0f; double targetDistance = 0.0; bool hasProjectedMetrics = false; - float projectedNdcX = 0.0f; - float projectedNdcY = 0.0f; - float projectedNdcRadius = 0.0f; + SCameraProjectedTargetMetrics projectedTarget = {}; bool hasSphericalState = false; hlsl::float64_t3 sphericalTarget = hlsl::float64_t3(0.0); float sphericalDistance = 0.0f; @@ -46,9 +50,14 @@ struct SCameraFollowVisualMetrics float lockAngleDeg = 0.0f; float targetDistance = 0.0f; bool projectedValid = false; - float projectedNdcX = 0.0f; - float projectedNdcY = 0.0f; - float projectedNdcRadius = 0.0f; + SCameraProjectedTargetMetrics projectedTarget = {}; +}; + +//! Shared view/projection bundle for CPU-side projected target metrics. +struct SCameraProjectionContext +{ + hlsl::float32_t4x4 viewMatrix = hlsl::float32_t4x4(1.0f); + hlsl::float32_t4x4 projectionMatrix = hlsl::float32_t4x4(1.0f); }; //! Shared tolerances for follow target lock, writeback, and projected-center checks. @@ -73,6 +82,16 @@ struct SCameraFollowRegressionThresholds double scalarTolerance = DefaultScalarTolerance; }; +inline SCameraFollowRegressionThresholds makeFollowRegressionThresholds( + const float projectedNdcTolerance = SCameraFollowRegressionThresholds::DefaultProjectedNdcTolerance, + const float lockAngleToleranceDeg = SCameraFollowRegressionThresholds::DefaultLockAngleToleranceDeg) +{ + auto thresholds = SCameraFollowRegressionThresholds{}; + thresholds.projectedNdcTolerance = projectedNdcTolerance; + thresholds.lockAngleToleranceDeg = lockAngleToleranceDeg; + return thresholds; +} + //! Bundled reusable follow regression flow. //! The helper builds a follow goal, applies it, verifies the resulting camera state, //! and then checks the lock/writeback follow contract. @@ -87,56 +106,53 @@ struct SCameraFollowApplyValidationResult }; inline bool tryComputeProjectedFollowTargetMetrics( - const hlsl::float32_t4x4& viewProjMatrix, + const SCameraProjectionContext& projectionContext, const core::CTrackedTarget& trackedTarget, - float& outNdcX, - float& outNdcY, - float* outNdcRadius = nullptr, + SCameraProjectedTargetMetrics& outMetrics, const float clipWEpsilon = SCameraFollowRegressionThresholds::DefaultClipWEpsilon) { + outMetrics = {}; const auto target = hlsl::getCastedVector(trackedTarget.getGimbal().getPosition()); - const auto clip = hlsl::mul(viewProjMatrix, hlsl::float32_t4(target.x, target.y, target.z, 1.0f)); - if (!std::isfinite(clip.x) || !std::isfinite(clip.y) || !std::isfinite(clip.z) || !std::isfinite(clip.w)) + const auto viewSpace = hlsl::mul(projectionContext.viewMatrix, hlsl::float32_t4(target.x, target.y, target.z, 1.0f)); + const auto clipProjection = hlsl::transpose(projectionContext.projectionMatrix); + const auto clip = hlsl::mul(clipProjection, viewSpace); + if (!hlsl::isFiniteScalar(clip.x) || !hlsl::isFiniteScalar(clip.y) || !hlsl::isFiniteScalar(clip.z) || !hlsl::isFiniteScalar(clip.w)) return false; - const auto absW = std::abs(clip.w); + const auto absW = hlsl::abs(clip.w); if (absW < clipWEpsilon) return false; const float invW = 1.0f / clip.w; - outNdcX = clip.x * invW; - outNdcY = clip.y * invW; - if (!std::isfinite(outNdcX) || !std::isfinite(outNdcY)) + outMetrics.ndc = hlsl::float32_t2(clip.x, clip.y) * invW; + if (!hlsl::isFiniteScalar(outMetrics.ndc.x) || !hlsl::isFiniteScalar(outMetrics.ndc.y)) return false; - if (outNdcRadius) - *outNdcRadius = hlsl::length(hlsl::float32_t2(outNdcX, outNdcY)); + outMetrics.radius = hlsl::length(outMetrics.ndc); return true; } inline bool validateProjectedFollowTargetContract( - const hlsl::float32_t4x4& viewProjMatrix, + const SCameraProjectionContext& projectionContext, const core::CTrackedTarget& trackedTarget, - float& outNdcRadius, + SCameraProjectedTargetMetrics& outMetrics, std::string* error = nullptr, const SCameraFollowRegressionThresholds& thresholds = {}) { - float ndcX = 0.0f; - float ndcY = 0.0f; - if (!tryComputeProjectedFollowTargetMetrics(viewProjMatrix, trackedTarget, ndcX, ndcY, &outNdcRadius, thresholds.clipWEpsilon)) + if (!tryComputeProjectedFollowTargetMetrics(projectionContext, trackedTarget, outMetrics, thresholds.clipWEpsilon)) { if (error) *error = "failed to project follow target"; return false; } - if (outNdcRadius > thresholds.projectedNdcTolerance) + if (outMetrics.radius > thresholds.projectedNdcTolerance) { if (error) { - *error = "projected target mismatch ndc=(" + std::to_string(ndcX) + - "," + std::to_string(ndcY) + ") radius=" + std::to_string(outNdcRadius); + *error = "projected target mismatch ndc=(" + std::to_string(outMetrics.ndc.x) + + "," + std::to_string(outMetrics.ndc.y) + ") radius=" + std::to_string(outMetrics.radius); } return false; } @@ -148,7 +164,7 @@ inline SCameraFollowVisualMetrics buildFollowVisualMetrics( core::ICamera* camera, const core::CTrackedTarget& trackedTarget, const core::SCameraFollowConfig* followConfig, - const hlsl::float32_t4x4* viewProjMatrix = nullptr) + const SCameraProjectionContext* projectionContext = nullptr) { SCameraFollowVisualMetrics out = {}; if (!camera || !followConfig || !followConfig->enabled || followConfig->mode == core::ECameraFollowMode::Disabled) @@ -163,14 +179,9 @@ inline SCameraFollowVisualMetrics buildFollowVisualMetrics( if (out.lockValid) out.targetDistance = static_cast(targetDistance); - if (out.lockValid && viewProjMatrix) + if (out.lockValid && projectionContext) { - out.projectedValid = tryComputeProjectedFollowTargetMetrics( - *viewProjMatrix, - trackedTarget, - out.projectedNdcX, - out.projectedNdcY, - &out.projectedNdcRadius); + out.projectedValid = tryComputeProjectedFollowTargetMetrics(*projectionContext, trackedTarget, out.projectedTarget); } return out; @@ -183,7 +194,7 @@ inline bool validateFollowTargetContract( const core::CCameraGoal& followGoal, SCameraFollowRegressionResult& out, std::string* error = nullptr, - const hlsl::float32_t4x4* viewProjMatrix = nullptr, + const SCameraProjectionContext* projectionContext = nullptr, const SCameraFollowRegressionThresholds& thresholds = {}) { out = {}; @@ -205,7 +216,7 @@ inline bool validateFollowTargetContract( } const auto expectedTargetDistance = hlsl::length(trackedTarget.getGimbal().getPosition() - camera->getGimbal().getPosition()); - if (!std::isfinite(expectedTargetDistance) || std::abs(expectedTargetDistance - out.targetDistance) > thresholds.distanceTolerance) + if (!hlsl::isFiniteScalar(expectedTargetDistance) || hlsl::abs(expectedTargetDistance - out.targetDistance) > thresholds.distanceTolerance) { if (error) { @@ -222,14 +233,12 @@ inline bool validateFollowTargetContract( return false; } - if (viewProjMatrix) + if (projectionContext) { out.hasProjectedMetrics = tryComputeProjectedFollowTargetMetrics( - *viewProjMatrix, + *projectionContext, trackedTarget, - out.projectedNdcX, - out.projectedNdcY, - &out.projectedNdcRadius, + out.projectedTarget, thresholds.clipWEpsilon); if (!out.hasProjectedMetrics) { @@ -238,13 +247,11 @@ inline bool validateFollowTargetContract( return false; } - if (out.projectedNdcRadius > thresholds.projectedNdcTolerance) + if (out.projectedTarget.radius > thresholds.projectedNdcTolerance) { if (error) - { - *error = "projected target mismatch ndc=(" + std::to_string(out.projectedNdcX) + - "," + std::to_string(out.projectedNdcY) + ") radius=" + std::to_string(out.projectedNdcRadius); - } + *error = "projected target mismatch ndc=(" + std::to_string(out.projectedTarget.ndc.x) + + "," + std::to_string(out.projectedTarget.ndc.y) + ") radius=" + std::to_string(out.projectedTarget.radius); return false; } } @@ -267,7 +274,7 @@ inline bool validateFollowTargetContract( const auto trackedTargetPosition = trackedTarget.getGimbal().getPosition(); const auto targetDelta = state.target - trackedTargetPosition; const auto targetDeltaLen = hlsl::length(targetDelta); - if (!std::isfinite(targetDeltaLen) || targetDeltaLen > thresholds.targetTolerance) + if (!hlsl::isFiniteScalar(targetDeltaLen) || targetDeltaLen > thresholds.targetTolerance) { if (error) *error = "spherical target writeback mismatch"; @@ -277,9 +284,9 @@ inline bool validateFollowTargetContract( const auto actualDistance = hlsl::length(camera->getGimbal().getPosition() - trackedTargetPosition); const auto expectedDistance = followGoal.hasOrbitState ? static_cast(followGoal.orbitDistance) : (followGoal.hasDistance ? static_cast(followGoal.distance) : actualDistance); - if (!std::isfinite(actualDistance) || !std::isfinite(expectedDistance) || - std::abs(actualDistance - expectedDistance) > thresholds.distanceTolerance || - std::abs(static_cast(state.distance) - expectedDistance) > thresholds.distanceTolerance) + if (!hlsl::isFiniteScalar(actualDistance) || !hlsl::isFiniteScalar(expectedDistance) || + hlsl::abs(actualDistance - expectedDistance) > thresholds.distanceTolerance || + hlsl::abs(static_cast(state.distance) - expectedDistance) > thresholds.distanceTolerance) { if (error) { @@ -302,7 +309,7 @@ inline bool buildApplyAndValidateFollowTargetContract( const core::SCameraFollowConfig& followConfig, SCameraFollowApplyValidationResult& out, std::string* error = nullptr, - const hlsl::float32_t4x4* viewProjMatrix = nullptr, + const SCameraProjectionContext* projectionContext = nullptr, const SCameraFollowRegressionThresholds& thresholds = {}) { out = {}; @@ -347,7 +354,7 @@ inline bool buildApplyAndValidateFollowTargetContract( out.goal, out.regression, error, - viewProjMatrix, + projectionContext, thresholds)) { return false; diff --git a/common/include/camera/CCameraFollowUtilities.hpp b/common/include/camera/CCameraFollowUtilities.hpp index 6e19e59d2..29bdbc140 100644 --- a/common/include/camera/CCameraFollowUtilities.hpp +++ b/common/include/camera/CCameraFollowUtilities.hpp @@ -9,6 +9,8 @@ #include #include "CCameraGoalSolver.hpp" +#include "CCameraTargetRelativeUtilities.hpp" +#include "CCameraKindUtilities.hpp" namespace nbl::core { @@ -156,19 +158,48 @@ inline constexpr bool cameraFollowModeUsesCapturedOffset(const ECameraFollowMode return cameraFollowModeUsesWorldOffset(mode) || cameraFollowModeUsesLocalOffset(mode); } +inline constexpr ECameraFollowMode getDefaultCameraFollowMode(const ICamera::CameraKind kind) +{ + switch (kind) + { + case ICamera::CameraKind::Orbit: + case ICamera::CameraKind::Arcball: + case ICamera::CameraKind::Turntable: + case ICamera::CameraKind::TopDown: + case ICamera::CameraKind::Isometric: + case ICamera::CameraKind::DollyZoom: + case ICamera::CameraKind::Path: + return ECameraFollowMode::OrbitTarget; + case ICamera::CameraKind::Chase: + case ICamera::CameraKind::Dolly: + return ECameraFollowMode::KeepLocalOffset; + default: + return ECameraFollowMode::Disabled; + } +} + +inline constexpr SCameraFollowConfig makeDefaultFollowConfig(const ICamera::CameraKind kind) +{ + const auto mode = getDefaultCameraFollowMode(kind); + return { + .enabled = mode != ECameraFollowMode::Disabled, + .mode = mode + }; +} + +inline constexpr SCameraFollowConfig makeDefaultFollowConfig(const ICamera* const camera) +{ + return camera ? makeDefaultFollowConfig(camera->getKind()) : SCameraFollowConfig{}; +} + inline hlsl::float64_t3 transformFollowLocalOffset(const ICamera::CGimbal& gimbal, const hlsl::float64_t3& localOffset) { - return gimbal.getXAxis() * localOffset.x + - gimbal.getYAxis() * localOffset.y + - gimbal.getZAxis() * localOffset.z; + return hlsl::rotateVectorByQuaternion(gimbal.getOrientation(), localOffset); } inline hlsl::float64_t3 projectFollowWorldOffsetToLocal(const ICamera::CGimbal& gimbal, const hlsl::float64_t3& worldOffset) { - return hlsl::float64_t3( - hlsl::dot(worldOffset, gimbal.getXAxis()), - hlsl::dot(worldOffset, gimbal.getYAxis()), - hlsl::dot(worldOffset, gimbal.getZAxis())); + return hlsl::projectWorldVectorToLocalQuaternionFrame(gimbal.getOrientation(), worldOffset); } inline bool buildFollowLookAtOrientation( @@ -180,59 +211,6 @@ inline bool buildFollowLookAtOrientation( return hlsl::tryBuildLookAtOrientation(position, targetPosition, preferredUp, outOrientation); } -inline bool applyFollowSphericalPose( - CCameraGoal& goal, - const hlsl::float64_t3& targetPosition, - const double orbitU, - const double orbitV, - const float distance) -{ - hlsl::float64_t appliedDistance = 0.0; - if (!hlsl::tryBuildSphericalPoseFromOrbit( - targetPosition, - orbitU, - orbitV, - static_cast(distance), - static_cast(ICamera::SphericalMinDistance), - static_cast(ICamera::SphericalMaxDistance), - goal.position, - goal.orientation, - &appliedDistance)) - { - return false; - } - - goal.hasTargetPosition = true; - goal.targetPosition = targetPosition; - goal.hasDistance = true; - goal.distance = static_cast(appliedDistance); - goal.hasOrbitState = true; - goal.orbitU = orbitU; - goal.orbitV = orbitV; - goal.orbitDistance = static_cast(appliedDistance); - return true; -} - -inline bool buildFollowSphericalGoalFromPose(CCameraGoal& goal, const hlsl::float64_t3& targetPosition, const hlsl::float64_t3& position) -{ - hlsl::float64_t orbitU = 0.0; - hlsl::float64_t orbitV = 0.0; - hlsl::float64_t distance = 0.0; - if (!hlsl::tryBuildOrbitFromPosition( - targetPosition, - position, - static_cast(ICamera::SphericalMinDistance), - static_cast(ICamera::SphericalMaxDistance), - orbitU, - orbitV, - distance)) - { - return false; - } - - return applyFollowSphericalPose(goal, targetPosition, orbitU, orbitV, static_cast(distance)); -} - inline bool captureFollowOffsetsFromCamera( const CCameraGoalSolver& solver, ICamera* camera, @@ -257,17 +235,19 @@ inline bool tryComputeFollowTargetLockMetrics( { const auto toTarget = trackedTarget.getGimbal().getPosition() - cameraGimbal.getPosition(); const auto targetDistance = hlsl::length(toTarget); - if (!std::isfinite(targetDistance) || targetDistance <= ICamera::TinyScalarEpsilon) + if (!hlsl::isFiniteScalar(targetDistance) || targetDistance <= ICamera::TinyScalarEpsilon) return false; - const auto forward = hlsl::normalize(cameraGimbal.getZAxis()); - if (!hlsl::isFiniteVec3(forward) || hlsl::length(forward) <= ICamera::TinyScalarEpsilon) + const auto forward = cameraGimbal.getZAxis(); + const auto forwardLength = hlsl::length(forward); + if (!hlsl::isFiniteVec3(forward) || !hlsl::isFiniteScalar(forwardLength) || forwardLength <= ICamera::TinyScalarEpsilon) return false; + const auto forwardDirection = forward / forwardLength; const auto targetDir = toTarget / targetDistance; - const auto dotForward = std::clamp(hlsl::dot(forward, targetDir), -1.0, 1.0); - outAngleDeg = static_cast(hlsl::degrees(std::acos(dotForward))); - if (!std::isfinite(outAngleDeg)) + const auto dotForward = std::clamp(hlsl::dot(forwardDirection, targetDir), -1.0, 1.0); + outAngleDeg = static_cast(hlsl::degrees(hlsl::acos(dotForward))); + if (!hlsl::isFiniteScalar(outAngleDeg)) return false; if (outDistance) @@ -275,6 +255,20 @@ inline bool tryComputeFollowTargetLockMetrics( return true; } +inline bool tryBuildFollowPositionGoal( + ICamera* camera, + CCameraGoal& outGoal, + const hlsl::float64_t3& targetPosition, + const hlsl::float64_t3& position, + const hlsl::float64_t3& preferredUp) +{ + if (camera->supportsGoalState(ICamera::GoalStateSphericalTarget)) + return buildCanonicalTargetRelativeGoalFromPosition(outGoal, targetPosition, position); + + outGoal.position = position; + return buildFollowLookAtOrientation(outGoal.position, targetPosition, preferredUp, outGoal.orientation) && isGoalFinite(outGoal); +} + inline bool tryBuildFollowGoal( const CCameraGoalSolver& solver, ICamera* camera, @@ -303,10 +297,7 @@ inline bool tryBuildFollowGoal( if (outGoal.hasPathState) { - outGoal.hasTargetPosition = true; - outGoal.targetPosition = targetPosition; - outGoal = canonicalizeGoal(outGoal); - return isGoalFinite(outGoal); + return applyCanonicalPathGoalFields(outGoal, targetPosition, outGoal.pathState) && isGoalFinite(outGoal); } const bool hasSphericalState = outGoal.hasOrbitState || outGoal.hasDistance; @@ -314,36 +305,31 @@ inline bool tryBuildFollowGoal( return false; const auto orbitDistance = outGoal.hasOrbitState ? outGoal.orbitDistance : outGoal.distance; - return applyFollowSphericalPose(outGoal, targetPosition, outGoal.orbitU, outGoal.orbitV, orbitDistance); + return applyCanonicalTargetRelativeGoal( + outGoal, + { + .target = targetPosition, + .orbitU = outGoal.orbitU, + .orbitV = outGoal.orbitV, + .distance = orbitDistance + }); } case ECameraFollowMode::LookAtTarget: { - if (camera->supportsGoalState(ICamera::GoalStateSphericalTarget)) - return buildFollowSphericalGoalFromPose(outGoal, targetPosition, capture.goal.position); - - outGoal.position = capture.goal.position; - return buildFollowLookAtOrientation(outGoal.position, targetPosition, targetGimbal.getYAxis(), outGoal.orientation) && isGoalFinite(outGoal); + return tryBuildFollowPositionGoal(camera, outGoal, targetPosition, capture.goal.position, targetGimbal.getYAxis()); } case ECameraFollowMode::KeepWorldOffset: { const auto position = targetPosition + config.worldOffset; - if (camera->supportsGoalState(ICamera::GoalStateSphericalTarget)) - return buildFollowSphericalGoalFromPose(outGoal, targetPosition, position); - - outGoal.position = position; - return buildFollowLookAtOrientation(outGoal.position, targetPosition, targetGimbal.getYAxis(), outGoal.orientation) && isGoalFinite(outGoal); + return tryBuildFollowPositionGoal(camera, outGoal, targetPosition, position, targetGimbal.getYAxis()); } case ECameraFollowMode::KeepLocalOffset: { const auto position = targetPosition + transformFollowLocalOffset(targetGimbal, config.localOffset); - if (camera->supportsGoalState(ICamera::GoalStateSphericalTarget)) - return buildFollowSphericalGoalFromPose(outGoal, targetPosition, position); - - outGoal.position = position; - return buildFollowLookAtOrientation(outGoal.position, targetPosition, targetGimbal.getYAxis(), outGoal.orientation) && isGoalFinite(outGoal); + return tryBuildFollowPositionGoal(camera, outGoal, targetPosition, position, targetGimbal.getYAxis()); } default: diff --git a/common/include/camera/CCameraGoal.hpp b/common/include/camera/CCameraGoal.hpp index 0e76e4288..a04b9358c 100644 --- a/common/include/camera/CCameraGoal.hpp +++ b/common/include/camera/CCameraGoal.hpp @@ -7,9 +7,12 @@ #include #include +#include #include #include +#include "CCameraPathUtilities.hpp" +#include "CCameraTargetRelativeUtilities.hpp" #include "ICamera.hpp" namespace nbl::core @@ -39,11 +42,6 @@ struct CCameraGoal ICamera::DynamicPerspectiveState dynamicPerspectiveState = {}; }; -inline double lerpWrappedAngleRad(double a, double b, double alpha) -{ - return a + hlsl::wrapAngleRad(b - a) * alpha; -} - inline uint32_t getRequiredGoalStateMask(const CCameraGoal& target) { uint32_t mask = ICamera::GoalStateNone; @@ -56,41 +54,158 @@ inline uint32_t getRequiredGoalStateMask(const CCameraGoal& target) return mask; } -inline bool applyCanonicalPathGoal(CCameraGoal& goal) +inline void applyCanonicalTargetRelativeGoalFields( + CCameraGoal& goal, + const SCameraTargetRelativeState& state, + const SCameraTargetRelativePose& pose) { - if (!(goal.hasPathState && goal.hasTargetPosition)) + goal.position = pose.position; + goal.orientation = pose.orientation; + goal.hasTargetPosition = true; + goal.targetPosition = state.target; + goal.hasDistance = true; + goal.distance = static_cast(pose.appliedDistance); + goal.hasOrbitState = true; + goal.orbitU = state.orbitU; + goal.orbitV = state.orbitV; + goal.orbitDistance = static_cast(pose.appliedDistance); +} + +inline bool applyCanonicalTargetRelativeGoal(CCameraGoal& goal, const SCameraTargetRelativeState& state) +{ + SCameraTargetRelativePose pose = {}; + if (!tryBuildTargetRelativePoseFromState(state, ICamera::SphericalMinDistance, ICamera::SphericalMaxDistance, pose)) return false; - if (!std::isfinite(goal.pathState.angle) || !std::isfinite(goal.pathState.radius) || !std::isfinite(goal.pathState.height)) + + applyCanonicalTargetRelativeGoalFields(goal, state, pose); + return true; +} + +inline bool applyCanonicalPathGoalFields( + CCameraGoal& goal, + const hlsl::float64_t3& targetPosition, + const ICamera::PathState& pathState, + const SCameraPathLimits& limits = makeDefaultPathLimits()) +{ + SCameraCanonicalPathState canonicalPathState = {}; + if (!tryBuildCanonicalPathState(targetPosition, pathState, limits, canonicalPathState)) return false; - hlsl::float64_t appliedOrbitDistance = 0.0; - if (!hlsl::tryBuildPathPoseFromState( - goal.targetPosition, - goal.pathState.angle, - goal.pathState.radius, - goal.pathState.height, - static_cast(ICamera::SphericalMinDistance), - static_cast(ICamera::SphericalMinDistance), - static_cast(ICamera::SphericalMaxDistance), - goal.position, - goal.orientation, - &appliedOrbitDistance, - &goal.orbitU, - &goal.orbitV)) + goal.hasTargetPosition = true; + goal.targetPosition = targetPosition; + goal.hasPathState = true; + goal.pathState = pathState; + applyCanonicalTargetRelativeGoalFields( + goal, + canonicalPathState.targetRelative, + { + .position = canonicalPathState.pose.position, + .orientation = canonicalPathState.pose.orientation, + .appliedDistance = canonicalPathState.pose.appliedDistance + }); + return true; +} + +inline bool applyCanonicalSphericalGoal(CCameraGoal& goal) +{ + if (!(goal.hasTargetPosition && goal.hasOrbitState)) + return false; + if (!hlsl::isFiniteScalar(goal.orbitU) || !hlsl::isFiniteScalar(goal.orbitV) || !hlsl::isFiniteScalar(goal.orbitDistance)) + return false; + + return applyCanonicalTargetRelativeGoal( + goal, + { + .target = goal.targetPosition, + .orbitU = goal.orbitU, + .orbitV = goal.orbitV, + .distance = goal.orbitDistance + }); +} + +inline bool buildCanonicalTargetRelativeGoalFromPosition( + CCameraGoal& goal, + const hlsl::float64_t3& targetPosition, + const hlsl::float64_t3& position) +{ + SCameraTargetRelativeState state = {}; + if (!tryBuildTargetRelativeStateFromPosition( + targetPosition, + position, + ICamera::SphericalMinDistance, + ICamera::SphericalMaxDistance, + state)) { return false; } - goal.hasDistance = true; - goal.distance = static_cast(appliedOrbitDistance); - goal.hasOrbitState = true; - goal.orbitDistance = static_cast(appliedOrbitDistance); + return applyCanonicalTargetRelativeGoal(goal, state); +} + +inline bool tryResolveCanonicalTargetRelativeState( + const CCameraGoal& goal, + const ICamera::SphericalTargetState& currentState, + SCameraTargetRelativeState& outState) +{ + outState.target = goal.hasTargetPosition ? goal.targetPosition : currentState.target; + outState.orbitU = currentState.u; + outState.orbitV = currentState.v; + outState.distance = currentState.distance; + + if (goal.hasOrbitState) + { + outState.orbitU = goal.orbitU; + outState.orbitV = goal.orbitV; + outState.distance = goal.orbitDistance; + } + else + { + SCameraTargetRelativeState resolvedState = {}; + if (!tryBuildTargetRelativeStateFromPosition( + outState.target, + goal.position, + currentState.minDistance, + currentState.maxDistance, + resolvedState)) + { + return false; + } + + outState.orbitU = resolvedState.orbitU; + outState.orbitV = resolvedState.orbitV; + outState.distance = resolvedState.distance; + } + + if (goal.hasDistance && !goal.hasOrbitState) + outState.distance = goal.distance; + + outState.distance = std::clamp(outState.distance, currentState.minDistance, currentState.maxDistance); + return true; +} + +inline bool applyCanonicalPathGoal(CCameraGoal& goal) +{ + if (!(goal.hasPathState && goal.hasTargetPosition)) + return false; + if (!isPathStateFinite(goal.pathState)) + return false; + return applyCanonicalPathGoalFields(goal, goal.targetPosition, goal.pathState); +} + +inline bool applyCanonicalGoalState(CCameraGoal& goal) +{ + if (goal.hasPathState) + return applyCanonicalPathGoal(goal); + + if (goal.hasTargetPosition && goal.hasOrbitState) + return applyCanonicalSphericalGoal(goal); + return true; } inline CCameraGoal canonicalizeGoal(CCameraGoal goal) { - applyCanonicalPathGoal(goal); + applyCanonicalGoalState(goal); return goal; } @@ -100,14 +215,14 @@ inline bool isGoalFinite(const CCameraGoal& goal) return false; if (goal.hasTargetPosition && !hlsl::isFiniteVec3(goal.targetPosition)) return false; - if (goal.hasDistance && !std::isfinite(goal.distance)) + if (goal.hasDistance && !hlsl::isFiniteScalar(goal.distance)) return false; - if (goal.hasOrbitState && (!std::isfinite(goal.orbitU) || !std::isfinite(goal.orbitV) || !std::isfinite(goal.orbitDistance))) + if (goal.hasOrbitState && (!hlsl::isFiniteScalar(goal.orbitU) || !hlsl::isFiniteScalar(goal.orbitV) || !hlsl::isFiniteScalar(goal.orbitDistance))) return false; - if (goal.hasPathState && (!std::isfinite(goal.pathState.angle) || !std::isfinite(goal.pathState.radius) || !std::isfinite(goal.pathState.height))) + if (goal.hasPathState && !isPathStateFinite(goal.pathState)) return false; if (goal.hasDynamicPerspectiveState && - (!std::isfinite(goal.dynamicPerspectiveState.baseFov) || !std::isfinite(goal.dynamicPerspectiveState.referenceDistance))) + (!hlsl::isFiniteScalar(goal.dynamicPerspectiveState.baseFov) || !hlsl::isFiniteScalar(goal.dynamicPerspectiveState.referenceDistance))) return false; return true; } @@ -115,14 +230,10 @@ inline bool isGoalFinite(const CCameraGoal& goal) inline bool compareGoals(const CCameraGoal& actual, const CCameraGoal& expected, const double posEps, const double rotEpsDeg, const double scalarEps) { - const auto currentOrientation = hlsl::normalizeQuaternion(actual.orientation); - const auto expectedOrientation = hlsl::normalizeQuaternion(expected.orientation); - if (!hlsl::isFiniteVec3(actual.position) || !hlsl::isFiniteVec3(expected.position) || !hlsl::isFiniteQuaternion(currentOrientation) || !hlsl::isFiniteQuaternion(expectedOrientation)) + hlsl::SCameraPoseDelta poseDelta = {}; + if (!hlsl::tryComputePoseDelta(actual.position, actual.orientation, expected.position, expected.orientation, poseDelta)) return false; - - const double posDelta = hlsl::length(actual.position - expected.position); - const double rotDeltaDeg = hlsl::getQuaternionAngularDistanceDegrees(currentOrientation, expectedOrientation); - if (posDelta > posEps || rotDeltaDeg > rotEpsDeg) + if (poseDelta.position > posEps || poseDelta.rotationDeg > rotEpsDeg) return false; if (expected.hasTargetPosition) @@ -139,9 +250,9 @@ inline bool compareGoals(const CCameraGoal& actual, const CCameraGoal& expected, { if (!actual.hasOrbitState) return false; - if (hlsl::degrees(hlsl::getWrappedAngleDistanceRadians(expected.orbitU, actual.orbitU)) > rotEpsDeg) + if (hlsl::getWrappedAngleDistanceDegrees(expected.orbitU, actual.orbitU) > rotEpsDeg) return false; - if (hlsl::degrees(hlsl::getWrappedAngleDistanceRadians(expected.orbitV, actual.orbitV)) > rotEpsDeg) + if (hlsl::getWrappedAngleDistanceDegrees(expected.orbitV, actual.orbitV) > rotEpsDeg) return false; if (!hlsl::nearlyEqualScalar(static_cast(actual.orbitDistance), static_cast(expected.orbitDistance), scalarEps)) return false; @@ -150,11 +261,7 @@ inline bool compareGoals(const CCameraGoal& actual, const CCameraGoal& expected, { if (!actual.hasPathState) return false; - if (hlsl::degrees(hlsl::getWrappedAngleDistanceRadians(expected.pathState.angle, actual.pathState.angle)) > rotEpsDeg) - return false; - if (!hlsl::nearlyEqualScalar(actual.pathState.radius, expected.pathState.radius, scalarEps)) - return false; - if (!hlsl::nearlyEqualScalar(actual.pathState.height, expected.pathState.height, scalarEps)) + if (!pathStatesNearlyEqual(actual.pathState, expected.pathState, makePathComparisonThresholds(rotEpsDeg, scalarEps))) return false; } if (expected.hasDynamicPerspectiveState) @@ -173,12 +280,12 @@ inline bool compareGoals(const CCameraGoal& actual, const CCameraGoal& expected, inline std::string describeGoalMismatch(const CCameraGoal& actual, const CCameraGoal& expected) { std::ostringstream oss; + hlsl::SCameraPoseDelta poseDelta = {}; + const bool hasPoseDelta = hlsl::tryComputePoseDelta(actual.position, actual.orientation, expected.position, expected.orientation, poseDelta); const auto currentOrientation = hlsl::normalizeQuaternion(actual.orientation); const auto expectedOrientation = hlsl::normalizeQuaternion(expected.orientation); - const double posDelta = hlsl::length(actual.position - expected.position); - const double rotDeltaDeg = hlsl::getQuaternionAngularDistanceDegrees(currentOrientation, expectedOrientation); - oss << "pos_delta=" << posDelta - << " rot_delta_deg=" << rotDeltaDeg + oss << "pos_delta=" << (hasPoseDelta ? poseDelta.position : std::numeric_limits::quiet_NaN()) + << " rot_delta_deg=" << (hasPoseDelta ? poseDelta.rotationDeg : std::numeric_limits::quiet_NaN()) << " current_pos=(" << actual.position.x << "," << actual.position.y << "," << actual.position.z << ")" << " expected_pos=(" << expected.position.x << "," << expected.position.y << "," << expected.position.z << ")" << " current_quat=(" << currentOrientation.data.x << "," << currentOrientation.data.y << "," << currentOrientation.data.z << "," << currentOrientation.data.w << ")" @@ -252,8 +359,8 @@ inline CCameraGoal blendGoals(const CCameraGoal& a, const CCameraGoal& b, double const float da = a.hasOrbitState ? a.orbitDistance : b.orbitDistance; const float db = b.hasOrbitState ? b.orbitDistance : a.orbitDistance; - blended.orbitU = lerpWrappedAngleRad(ua, ub, alpha); - blended.orbitV = lerpWrappedAngleRad(va, vb, alpha); + blended.orbitU = hlsl::lerpWrappedAngleRad(ua, ub, alpha); + blended.orbitV = hlsl::lerpWrappedAngleRad(va, vb, alpha); blended.orbitDistance = da + (db - da) * static_cast(alpha); } blended.hasDynamicPerspectiveState = a.hasDynamicPerspectiveState || b.hasDynamicPerspectiveState; @@ -270,9 +377,7 @@ inline CCameraGoal blendGoals(const CCameraGoal& a, const CCameraGoal& b, double { const auto pathA = a.hasPathState ? a.pathState : b.pathState; const auto pathB = b.hasPathState ? b.pathState : a.pathState; - blended.pathState.angle = lerpWrappedAngleRad(pathA.angle, pathB.angle, alpha); - blended.pathState.radius = pathA.radius + (pathB.radius - pathA.radius) * alpha; - blended.pathState.height = pathA.height + (pathB.height - pathA.height) * alpha; + blended.pathState = blendPathStates(pathA, pathB, alpha); } return canonicalizeGoal(blended); } diff --git a/common/include/camera/CCameraGoalSolver.hpp b/common/include/camera/CCameraGoalSolver.hpp index de4103dac..731826721 100644 --- a/common/include/camera/CCameraGoalSolver.hpp +++ b/common/include/camera/CCameraGoalSolver.hpp @@ -2,11 +2,14 @@ #define _C_CAMERA_GOAL_SOLVER_HPP_ #include +#include #include #include #include #include "CCameraGoal.hpp" +#include "CCameraTargetRelativeUtilities.hpp" +#include "CCameraVirtualEventUtilities.hpp" namespace nbl::core { @@ -271,7 +274,7 @@ class CCameraGoalSolver else { absoluteChanged = absoluteChanged || afterState.distance != beforeDistance; - exact = exact && std::abs(static_cast(afterState.distance - desiredDistance)) <= ICamera::ScalarTolerance; + exact = exact && hlsl::abs(static_cast(afterState.distance - desiredDistance)) <= ICamera::ScalarTolerance; } } } @@ -300,12 +303,9 @@ class CCameraGoalSolver } else { - const bool pathChanged = !hlsl::nearlyEqualScalar(beforeState.angle, afterState.angle, static_cast(ICamera::ScalarTolerance)) || - !hlsl::nearlyEqualScalar(beforeState.radius, afterState.radius, static_cast(ICamera::ScalarTolerance)) || - !hlsl::nearlyEqualScalar(beforeState.height, afterState.height, static_cast(ICamera::ScalarTolerance)); - const bool pathExact = hlsl::nearlyEqualScalar(afterState.angle, canonicalTarget.pathState.angle, static_cast(ICamera::ScalarTolerance)) && - hlsl::nearlyEqualScalar(afterState.radius, canonicalTarget.pathState.radius, static_cast(ICamera::ScalarTolerance)) && - hlsl::nearlyEqualScalar(afterState.height, canonicalTarget.pathState.height, static_cast(ICamera::ScalarTolerance)); + const auto thresholds = SCameraPathDefaults::ComparisonThresholds; + const bool pathChanged = pathStatesChanged(beforeState, afterState, thresholds); + const bool pathExact = pathStatesNearlyEqual(afterState, canonicalTarget.pathState, thresholds); absoluteChanged = absoluteChanged || pathChanged; exact = exact && pathExact; @@ -388,62 +388,61 @@ class CCameraGoalSolver } private: - struct SSphericalGoal + struct SGoalSolverDefaults final { - hlsl::float64_t3 target = hlsl::float64_t3(0.0); - double u = 0.0; - double v = 0.0; - float distance = 0.f; + static constexpr double UnitScale = 1.0; + static inline const hlsl::float64_t3 UnitAxisDenominator = hlsl::float64_t3(UnitScale); + static inline const hlsl::float64_t3 ScalarToleranceVec = hlsl::float64_t3(ICamera::ScalarTolerance); + static inline const hlsl::float64_t3 AngularToleranceDegVec = hlsl::float64_t3(ICamera::DefaultAngularToleranceDeg); }; - inline void appendSignedEvent(std::vector& events, double value, - CVirtualGimbalEvent::VirtualEventType positive, CVirtualGimbalEvent::VirtualEventType negative) const + inline void appendYawPitchRollEvents( + std::vector& events, + const hlsl::float64_t3& eulerRadians, + const double denominator, + const bool includeRoll = true) const { - if (!std::isfinite(value) || std::abs(value) <= ICamera::TinyScalarEpsilon) - return; - auto& ev = events.emplace_back(); - ev.type = (value > 0.0) ? positive : negative; - ev.magnitude = std::abs(value); + static constexpr std::array RotationBindings = {{ + { CVirtualGimbalEvent::TiltUp, CVirtualGimbalEvent::TiltDown }, + { CVirtualGimbalEvent::PanRight, CVirtualGimbalEvent::PanLeft }, + { CVirtualGimbalEvent::RollRight, CVirtualGimbalEvent::RollLeft } + }}; + + auto tolerances = SGoalSolverDefaults::AngularToleranceDegVec; + if (!includeRoll) + tolerances.z = std::numeric_limits::infinity(); + + appendAngularAxisEvents( + events, + eulerRadians, + hlsl::float64_t3(denominator), + tolerances, + RotationBindings); } - inline void appendScalarDeltaEvent(std::vector& events, const double delta, const double denominator, - const double tolerance, CVirtualGimbalEvent::VirtualEventType positive, CVirtualGimbalEvent::VirtualEventType negative) const + inline void appendPathDeltaEvents( + std::vector& events, + const SCameraPathDelta& delta, + const double moveDenominator) const { - if (!std::isfinite(delta) || std::abs(delta) <= tolerance) - return; - appendSignedEvent(events, delta / denominator, positive, negative); - } - - inline void appendAngularDeltaEvent(std::vector& events, const double deltaRadians, const double denominator, - const double toleranceDeg, CVirtualGimbalEvent::VirtualEventType positive, CVirtualGimbalEvent::VirtualEventType negative) const - { - if (!std::isfinite(deltaRadians) || std::abs(hlsl::degrees(deltaRadians)) <= toleranceDeg) - return; - appendSignedEvent(events, deltaRadians / denominator, positive, negative); + appendPathAdvanceEvents( + events, + delta, + moveDenominator, + SCameraPathDefaults::ComparisonThresholds.angleToleranceDeg, + SCameraPathDefaults::ComparisonThresholds.scalarTolerance); } inline double getMoveMagnitudeDenominator(const ICamera* camera) const { const double moveScale = camera->getMoveSpeedScale(); - return ICamera::VirtualTranslationStep * (moveScale == 0.0 ? 1.0 : moveScale); + return ICamera::VirtualTranslationStep * (moveScale == 0.0 ? SGoalSolverDefaults::UnitScale : moveScale); } inline double getRotationMagnitudeDenominator(const ICamera* camera) const { const double rotationScale = camera->getRotationSpeedScale(); - return rotationScale == 0.0 ? 1.0 : rotationScale; - } - - inline std::pair computePitchYawFromOrientation(const hlsl::camera_quaternion_t& orientation) const - { - const auto mat = hlsl::getQuaternionBasisMatrix(orientation); - const auto pitchYaw = hlsl::getPitchYawFromForwardVector(hlsl::float64_t3(mat[2][0], mat[2][1], mat[2][2])); - return { pitchYaw.x, pitchYaw.y }; - } - - inline hlsl::float64_t3 extractYawPitchRollYXZ(const hlsl::camera_quaternion_t& delta) const - { - return hlsl::getQuaternionEulerRadiansYXZ(delta); + return rotationScale == 0.0 ? SGoalSolverDefaults::UnitScale : rotationScale; } inline bool computePoseMismatch(ICamera* camera, const CCameraGoal& target, double& outPositionDelta, double& outRotationDeltaDeg) const @@ -454,14 +453,13 @@ class CCameraGoalSolver return false; const auto& gimbal = camera->getGimbal(); - const auto currentPos = gimbal.getPosition(); - const auto currentOrientation = hlsl::normalizeQuaternion(gimbal.getOrientation()); - const auto targetOrientation = hlsl::normalizeQuaternion(target.orientation); - - outPositionDelta = hlsl::length(currentPos - target.position); + hlsl::SCameraPoseDelta poseDelta = {}; + if (!hlsl::tryComputePoseDelta(gimbal.getPosition(), gimbal.getOrientation(), target.position, target.orientation, poseDelta)) + return false; - outRotationDeltaDeg = hlsl::getQuaternionAngularDistanceDegrees(currentOrientation, targetOrientation); - return std::isfinite(outPositionDelta) && std::isfinite(outRotationDeltaDeg); + outPositionDelta = poseDelta.position; + outRotationDeltaDeg = poseDelta.rotationDeg; + return true; } inline bool tryApplyAbsoluteReferencePose(ICamera* camera, const CCameraGoal& target, bool& outChanged, bool& outExact) const @@ -491,8 +489,7 @@ class CCameraGoalSolver return true; } - auto targetFrame = hlsl::getMatrix3x3As4x4(hlsl::getQuaternionBasisMatrix(target.orientation)); - targetFrame[3] = hlsl::float64_t4(target.position, 1.0); + const auto targetFrame = hlsl::composeTransformMatrix(target.position, target.orientation); camera->manipulate({}, &targetFrame); @@ -501,78 +498,28 @@ class CCameraGoalSolver if (!computePoseMismatch(camera, target, afterPosDelta, afterRotDeltaDeg)) return false; - outChanged = (std::abs(afterPosDelta - beforePosDelta) > ICamera::TinyScalarEpsilon) || - (std::abs(afterRotDeltaDeg - beforeRotDeltaDeg) > ICamera::TinyScalarEpsilon); + outChanged = !hlsl::isNearlyZeroScalar(afterPosDelta - beforePosDelta, static_cast(ICamera::TinyScalarEpsilon)) || + !hlsl::isNearlyZeroScalar(afterRotDeltaDeg - beforeRotDeltaDeg, static_cast(ICamera::TinyScalarEpsilon)); outExact = afterPosDelta <= ICamera::DefaultPositionTolerance && afterRotDeltaDeg <= ICamera::DefaultAngularToleranceDeg; return true; } - inline bool computeOrbitStateFromPositionTarget(const hlsl::float64_t3& position, const hlsl::float64_t3& target, - double& outU, double& outV, float& outDistance, float minDistance, float maxDistance) const + inline bool buildTargetRelativeEvents( + ICamera* camera, + const ICamera::SphericalTargetState& sphericalState, + const SCameraTargetRelativeState& goal, + std::vector& out, + const SCameraTargetRelativeEventPolicy& policy) const { - hlsl::float64_t clampedDistance = static_cast(outDistance); - if (!hlsl::tryBuildOrbitFromPosition( - target, - position, - static_cast(minDistance), - static_cast(maxDistance), - outU, - outV, - clampedDistance)) - { - return false; - } - - outDistance = static_cast(clampedDistance); - return true; - } - - inline bool resolveSphericalGoal(ICamera* camera, const CCameraGoal& target, const ICamera::SphericalTargetState& sphericalState, SSphericalGoal& outGoal) const - { - outGoal.target = target.hasTargetPosition ? target.targetPosition : sphericalState.target; - outGoal.u = sphericalState.u; - outGoal.v = sphericalState.v; - outGoal.distance = sphericalState.distance; - - if (target.hasOrbitState) - { - outGoal.u = target.orbitU; - outGoal.v = target.orbitV; - outGoal.distance = target.orbitDistance; - } - else - { - if (!computeOrbitStateFromPositionTarget(target.position, outGoal.target, outGoal.u, outGoal.v, outGoal.distance, sphericalState.minDistance, sphericalState.maxDistance)) - return false; - } - - if (target.hasDistance && !target.hasOrbitState) - outGoal.distance = target.distance; - - outGoal.distance = std::clamp(outGoal.distance, sphericalState.minDistance, sphericalState.maxDistance); - return true; - } - - inline bool buildOrbitTranslateEvents(ICamera* camera, const ICamera::SphericalTargetState& sphericalState, const SSphericalGoal& goal, std::vector& out) const - { - const double moveDenom = getMoveMagnitudeDenominator(camera); - appendAngularDeltaEvent(out, hlsl::wrapAngleRad(goal.v - sphericalState.v), moveDenom, ICamera::DefaultAngularToleranceDeg, CVirtualGimbalEvent::MoveRight, CVirtualGimbalEvent::MoveLeft); - appendAngularDeltaEvent(out, hlsl::wrapAngleRad(goal.u - sphericalState.u), moveDenom, ICamera::DefaultAngularToleranceDeg, CVirtualGimbalEvent::MoveUp, CVirtualGimbalEvent::MoveDown); - appendScalarDeltaEvent(out, static_cast(goal.distance - sphericalState.distance), ICamera::VirtualTranslationStep, ICamera::ScalarTolerance, CVirtualGimbalEvent::MoveForward, CVirtualGimbalEvent::MoveBackward); - return !out.empty(); - } - - inline bool buildRotateDistanceEvents(ICamera* camera, const ICamera::SphericalTargetState& sphericalState, const SSphericalGoal& goal, - std::vector& out, bool allowYaw, bool allowPitch, - CVirtualGimbalEvent::VirtualEventType distancePositive, CVirtualGimbalEvent::VirtualEventType distanceNegative) const - { - const double rotationDenom = getRotationMagnitudeDenominator(camera); - if (allowYaw) - appendAngularDeltaEvent(out, hlsl::wrapAngleRad(goal.u - sphericalState.u), rotationDenom, ICamera::DefaultAngularToleranceDeg, CVirtualGimbalEvent::PanRight, CVirtualGimbalEvent::PanLeft); - if (allowPitch) - appendAngularDeltaEvent(out, hlsl::wrapAngleRad(goal.v - sphericalState.v), rotationDenom, ICamera::DefaultAngularToleranceDeg, CVirtualGimbalEvent::TiltUp, CVirtualGimbalEvent::TiltDown); - if (distancePositive != CVirtualGimbalEvent::None && distanceNegative != CVirtualGimbalEvent::None) - appendScalarDeltaEvent(out, static_cast(goal.distance - sphericalState.distance), ICamera::VirtualTranslationStep, ICamera::ScalarTolerance, distancePositive, distanceNegative); + const auto delta = buildTargetRelativeDelta(sphericalState, goal); + appendTargetRelativeDeltaEvents( + out, + delta, + policy.translateOrbit ? getMoveMagnitudeDenominator(camera) : getRotationMagnitudeDenominator(camera), + ICamera::DefaultAngularToleranceDeg, + ICamera::VirtualTranslationStep, + ICamera::ScalarTolerance, + policy); return !out.empty(); } @@ -582,23 +529,23 @@ class CCameraGoalSolver return false; const auto effectiveTarget = target.hasTargetPosition ? target.targetPosition : sphericalState.target; - double currentAngle = 0.0; - double desiredAngle = 0.0; - double currentRadius = 0.0; - double desiredRadius = 0.0; - double currentHeight = 0.0; - double desiredHeight = 0.0; - constexpr double MinPathRadius = static_cast(ICamera::SphericalMinDistance); - - if (!hlsl::tryBuildPathStateFromPosition(effectiveTarget, camera->getGimbal().getPosition(), MinPathRadius, currentAngle, currentRadius, currentHeight)) - return false; - if (!hlsl::tryBuildPathStateFromPosition(effectiveTarget, target.position, MinPathRadius, desiredAngle, desiredRadius, desiredHeight)) + ICamera::PathState currentState = {}; + const ICamera::PathState* currentStateOverride = camera->tryGetPathState(currentState) ? ¤tState : nullptr; + SCameraPathStateTransition transition = {}; + if (!tryBuildPathStateTransition( + effectiveTarget, + camera->getGimbal().getPosition(), + target.position, + SCameraPathDefaults::Limits, + currentStateOverride, + target.hasPathState ? &target.pathState : nullptr, + transition)) + { return false; + } - const double moveDenom = getMoveMagnitudeDenominator(camera); - appendScalarDeltaEvent(out, desiredRadius - currentRadius, moveDenom, ICamera::ScalarTolerance, CVirtualGimbalEvent::MoveRight, CVirtualGimbalEvent::MoveLeft); - appendScalarDeltaEvent(out, desiredHeight - currentHeight, moveDenom, ICamera::ScalarTolerance, CVirtualGimbalEvent::MoveUp, CVirtualGimbalEvent::MoveDown); - appendAngularDeltaEvent(out, hlsl::wrapAngleRad(desiredAngle - currentAngle), moveDenom, ICamera::DefaultAngularToleranceDeg, CVirtualGimbalEvent::MoveForward, CVirtualGimbalEvent::MoveBackward); + const auto moveDenom = getMoveMagnitudeDenominator(camera); + appendPathDeltaEvents(out, transition.delta, moveDenom); return !out.empty(); } @@ -611,34 +558,34 @@ class CCameraGoalSolver if (camera->getKind() == ICamera::CameraKind::Path) return buildPathEvents(camera, target, sphericalState, out); - SSphericalGoal goal; - if (!resolveSphericalGoal(camera, target, sphericalState, goal)) + SCameraTargetRelativeState goal; + if (!tryResolveCanonicalTargetRelativeState(target, sphericalState, goal)) return false; switch (camera->getKind()) { case ICamera::CameraKind::Orbit: case ICamera::CameraKind::DollyZoom: - return buildOrbitTranslateEvents(camera, sphericalState, goal, out); + return buildTargetRelativeEvents(camera, sphericalState, goal, out, SCameraTargetRelativeRigDefaults::OrbitTranslatePolicy); case ICamera::CameraKind::Turntable: case ICamera::CameraKind::Arcball: - return buildRotateDistanceEvents(camera, sphericalState, goal, out, true, true, CVirtualGimbalEvent::MoveForward, CVirtualGimbalEvent::MoveBackward); + return buildTargetRelativeEvents(camera, sphericalState, goal, out, SCameraTargetRelativeRigDefaults::RotateDistancePolicy); case ICamera::CameraKind::TopDown: - return buildRotateDistanceEvents(camera, sphericalState, goal, out, true, false, CVirtualGimbalEvent::MoveForward, CVirtualGimbalEvent::MoveBackward); + return buildTargetRelativeEvents(camera, sphericalState, goal, out, SCameraTargetRelativeRigDefaults::TopDownPolicy); case ICamera::CameraKind::Isometric: - return buildRotateDistanceEvents(camera, sphericalState, goal, out, false, false, CVirtualGimbalEvent::MoveForward, CVirtualGimbalEvent::MoveBackward); + return buildTargetRelativeEvents(camera, sphericalState, goal, out, SCameraTargetRelativeRigDefaults::IsometricPolicy); case ICamera::CameraKind::Dolly: - return buildRotateDistanceEvents(camera, sphericalState, goal, out, true, true, CVirtualGimbalEvent::None, CVirtualGimbalEvent::None); + return buildTargetRelativeEvents(camera, sphericalState, goal, out, SCameraTargetRelativeRigDefaults::DollyPolicy); case ICamera::CameraKind::Chase: - return buildRotateDistanceEvents(camera, sphericalState, goal, out, true, true, CVirtualGimbalEvent::MoveUp, CVirtualGimbalEvent::MoveDown); + return buildTargetRelativeEvents(camera, sphericalState, goal, out, SCameraTargetRelativeRigDefaults::ChasePolicy); default: - return buildOrbitTranslateEvents(camera, sphericalState, goal, out); + return buildTargetRelativeEvents(camera, sphericalState, goal, out, SCameraTargetRelativeRigDefaults::OrbitTranslatePolicy); } } @@ -646,45 +593,40 @@ class CCameraGoalSolver { const auto& gimbal = camera->getGimbal(); const auto currentPos = gimbal.getPosition(); - const auto right = gimbal.getXAxis(); - const auto up = gimbal.getYAxis(); - const auto forward = gimbal.getZAxis(); - const auto deltaWorld = target.position - currentPos; - const hlsl::float64_t3 localDelta( - hlsl::dot(deltaWorld, right), - hlsl::dot(deltaWorld, up), - hlsl::dot(deltaWorld, forward)); - - appendScalarDeltaEvent(out, localDelta.x, 1.0, ICamera::ScalarTolerance, CVirtualGimbalEvent::MoveRight, CVirtualGimbalEvent::MoveLeft); - appendScalarDeltaEvent(out, localDelta.y, 1.0, ICamera::ScalarTolerance, CVirtualGimbalEvent::MoveUp, CVirtualGimbalEvent::MoveDown); - appendScalarDeltaEvent(out, localDelta.z, 1.0, ICamera::ScalarTolerance, CVirtualGimbalEvent::MoveForward, CVirtualGimbalEvent::MoveBackward); + appendWorldTranslationAsLocalEvents( + out, + gimbal.getOrientation(), + deltaWorld, + SGoalSolverDefaults::UnitAxisDenominator, + SGoalSolverDefaults::ScalarToleranceVec); switch (camera->getKind()) { case ICamera::CameraKind::FPS: { - const auto [curPitch, curYaw] = computePitchYawFromOrientation(gimbal.getOrientation()); - const auto [tgtPitch, tgtYaw] = computePitchYawFromOrientation(target.orientation); + const auto currentPitchYaw = hlsl::getPitchYawFromOrientation(gimbal.getOrientation()); + const auto targetPitchYaw = hlsl::getPitchYawFromOrientation(target.orientation); const double rotScale = camera->getRotationSpeedScale(); - const double invScale = rotScale == 0.0 ? 1.0 : (1.0 / rotScale); - - const double deltaPitch = hlsl::wrapAngleRad(tgtPitch - curPitch) * invScale; - const double deltaYaw = hlsl::wrapAngleRad(tgtYaw - curYaw) * invScale; - - appendAngularDeltaEvent(out, deltaPitch, 1.0, ICamera::DefaultAngularToleranceDeg, CVirtualGimbalEvent::TiltUp, CVirtualGimbalEvent::TiltDown); - appendAngularDeltaEvent(out, deltaYaw, 1.0, ICamera::DefaultAngularToleranceDeg, CVirtualGimbalEvent::PanRight, CVirtualGimbalEvent::PanLeft); + const double invScale = rotScale == 0.0 ? SGoalSolverDefaults::UnitScale : (SGoalSolverDefaults::UnitScale / rotScale); + + appendYawPitchRollEvents( + out, + hlsl::float64_t3( + hlsl::wrapAngleRad(targetPitchYaw.x - currentPitchYaw.x) * invScale, + hlsl::wrapAngleRad(targetPitchYaw.y - currentPitchYaw.y) * invScale, + 0.0), + SGoalSolverDefaults::UnitScale, + false); } break; case ICamera::CameraKind::Free: { - const auto deltaQuat = hlsl::inverseQuaternion(gimbal.getOrientation()) * hlsl::normalizeQuaternion(target.orientation); - const auto angles = extractYawPitchRollYXZ(deltaQuat); - - appendAngularDeltaEvent(out, angles.x, 1.0, ICamera::DefaultAngularToleranceDeg, CVirtualGimbalEvent::TiltUp, CVirtualGimbalEvent::TiltDown); - appendAngularDeltaEvent(out, angles.y, 1.0, ICamera::DefaultAngularToleranceDeg, CVirtualGimbalEvent::PanRight, CVirtualGimbalEvent::PanLeft); - appendAngularDeltaEvent(out, angles.z, 1.0, ICamera::DefaultAngularToleranceDeg, CVirtualGimbalEvent::RollRight, CVirtualGimbalEvent::RollLeft); + appendYawPitchRollEvents( + out, + hlsl::getOrientationDeltaEulerRadiansYXZ(gimbal.getOrientation(), target.orientation), + SGoalSolverDefaults::UnitScale); } break; default: diff --git a/common/include/camera/CCameraInputBindingUtilities.hpp b/common/include/camera/CCameraInputBindingUtilities.hpp index f7bb8435c..c472f2064 100644 --- a/common/include/camera/CCameraInputBindingUtilities.hpp +++ b/common/include/camera/CCameraInputBindingUtilities.hpp @@ -2,13 +2,40 @@ #define _NBL_C_CAMERA_INPUT_BINDING_UTILITIES_HPP_ #include +#include +#include "CCameraKindUtilities.hpp" #include "ICamera.hpp" #include "IGimbalBindingLayout.hpp" namespace nbl::ui { +template +inline constexpr std::array concatBindingCodeArrays( + const std::array& lhs, + const std::array& rhs) +{ + std::array output = {}; + for (size_t i = 0u; i < N; ++i) + output[i] = lhs[i]; + for (size_t i = 0u; i < M; ++i) + output[N + i] = rhs[i]; + return output; +} + +template +inline constexpr auto concatBindingCodeArrays( + const std::array& lhs, + const std::array& rhs, + const std::array&... rest) +{ + if constexpr (sizeof...(rest) == 0u) + return concatBindingCodeArrays(lhs, rhs); + else + return concatBindingCodeArrays(concatBindingCodeArrays(lhs, rhs), rest...); +} + //! Reusable keyboard, mouse, and ImGuizmo binding preset grouped for one camera kind. struct SCameraInputBindingPreset { @@ -17,22 +44,102 @@ struct SCameraInputBindingPreset IGimbalBindingLayout::imguizmo_to_virtual_events_t imguizmo; }; +//! Shared physical input bundles reused by default presets and smoke probing. +struct SCameraInputBindingPhysicalGroups final +{ + static inline constexpr std::array KeyboardWasdCodes = { + ui::E_KEY_CODE::EKC_W, + ui::E_KEY_CODE::EKC_S, + ui::E_KEY_CODE::EKC_A, + ui::E_KEY_CODE::EKC_D + }; + static inline constexpr std::array KeyboardQeCodes = { + ui::E_KEY_CODE::EKC_Q, + ui::E_KEY_CODE::EKC_E + }; + static inline constexpr std::array KeyboardIjklCodes = { + ui::E_KEY_CODE::EKC_I, + ui::E_KEY_CODE::EKC_K, + ui::E_KEY_CODE::EKC_J, + ui::E_KEY_CODE::EKC_L + }; + static inline constexpr auto KeyboardProbeMoveCodes = KeyboardWasdCodes; + static inline constexpr auto KeyboardProbeLookCodes = KeyboardIjklCodes; + static inline constexpr std::array KeyboardProbeExtraCodes = { + ui::E_KEY_CODE::EKC_U, + ui::E_KEY_CODE::EKC_O + }; + static inline constexpr auto KeyboardProbeCodes = concatBindingCodeArrays( + KeyboardProbeMoveCodes, + KeyboardQeCodes, + KeyboardProbeLookCodes, + KeyboardProbeExtraCodes); + static inline constexpr std::array RelativeMouseCodes = { + ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_X, + ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_X, + ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_Y, + ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_Y + }; + static inline constexpr std::array PositiveScrollCodes = { + ui::E_MOUSE_CODE::EMC_VERTICAL_POSITIVE_SCROLL, + ui::E_MOUSE_CODE::EMC_HORIZONTAL_POSITIVE_SCROLL + }; + static inline constexpr std::array NegativeScrollCodes = { + ui::E_MOUSE_CODE::EMC_VERTICAL_NEGATIVE_SCROLL, + ui::E_MOUSE_CODE::EMC_HORIZONTAL_NEGATIVE_SCROLL + }; +}; + +template +inline bool containsBindingForAnyCode(const Map& preset, const Codes& codes) +{ + for (const auto code : codes) + { + if (preset.find(code) != preset.end()) + return true; + } + return false; +} + +template +inline bool containsBindingForAnyCodeGroups(const Map& preset, const Codes&... codes) +{ + return (containsBindingForAnyCode(preset, codes) || ...); +} + +inline bool hasMouseRelativeMovementBinding(const IGimbalBindingLayout::mouse_to_virtual_events_t& mousePreset) +{ + return containsBindingForAnyCodeGroups(mousePreset, SCameraInputBindingPhysicalGroups::RelativeMouseCodes); +} + +inline bool hasMouseScrollBinding(const IGimbalBindingLayout::mouse_to_virtual_events_t& mousePreset) +{ + return containsBindingForAnyCodeGroups( + mousePreset, + SCameraInputBindingPhysicalGroups::PositiveScrollCodes, + SCameraInputBindingPhysicalGroups::NegativeScrollCodes); +} + namespace impl { +using virtual_event_t = core::CVirtualGimbalEvent::VirtualEventType; +using keyboard_axis_group_t = std::array; +using mouse_axis_group_t = std::array; +using scalar_axis_pair_t = std::array; -struct SKeyboardPresetSpec +struct SKeyboardPresetSpec final { - std::array wasd = { + keyboard_axis_group_t wasd = { core::CVirtualGimbalEvent::None, core::CVirtualGimbalEvent::None, core::CVirtualGimbalEvent::None, core::CVirtualGimbalEvent::None }; - std::array qe = { + scalar_axis_pair_t qe = { core::CVirtualGimbalEvent::None, core::CVirtualGimbalEvent::None }; - std::array ijkl = { + keyboard_axis_group_t ijkl = { core::CVirtualGimbalEvent::None, core::CVirtualGimbalEvent::None, core::CVirtualGimbalEvent::None, @@ -40,20 +147,109 @@ struct SKeyboardPresetSpec }; }; -struct SMousePresetSpec +struct SMousePresetSpec final { - std::array relative = { + mouse_axis_group_t relative = { core::CVirtualGimbalEvent::None, core::CVirtualGimbalEvent::None, core::CVirtualGimbalEvent::None, core::CVirtualGimbalEvent::None }; - std::array scroll = { + scalar_axis_pair_t scroll = { core::CVirtualGimbalEvent::None, core::CVirtualGimbalEvent::None }; }; +//! Shared virtual-event bundles reused across interaction families. +struct SCameraInputBindingEventGroups final +{ + static inline constexpr std::array FpsMove = { + core::CVirtualGimbalEvent::MoveForward, + core::CVirtualGimbalEvent::MoveBackward, + core::CVirtualGimbalEvent::MoveLeft, + core::CVirtualGimbalEvent::MoveRight + }; + static inline constexpr std::array OrbitTranslate = { + core::CVirtualGimbalEvent::MoveUp, + core::CVirtualGimbalEvent::MoveDown, + core::CVirtualGimbalEvent::MoveLeft, + core::CVirtualGimbalEvent::MoveRight + }; + static inline constexpr std::array OrbitZoom = { + core::CVirtualGimbalEvent::MoveForward, + core::CVirtualGimbalEvent::MoveBackward + }; + static inline constexpr std::array VerticalMove = { + core::CVirtualGimbalEvent::MoveDown, + core::CVirtualGimbalEvent::MoveUp + }; + static inline constexpr std::array PathRigAdvanceAndRadius = { + core::CVirtualGimbalEvent::MoveForward, + core::CVirtualGimbalEvent::MoveBackward, + core::CVirtualGimbalEvent::MoveLeft, + core::CVirtualGimbalEvent::MoveRight + }; + static inline constexpr std::array PathRigHeight = VerticalMove; + static inline constexpr std::array TurntableMove = { + core::CVirtualGimbalEvent::MoveForward, + core::CVirtualGimbalEvent::MoveBackward, + core::CVirtualGimbalEvent::PanLeft, + core::CVirtualGimbalEvent::PanRight + }; + static inline constexpr std::array LookYawPitch = { + core::CVirtualGimbalEvent::TiltDown, + core::CVirtualGimbalEvent::TiltUp, + core::CVirtualGimbalEvent::PanLeft, + core::CVirtualGimbalEvent::PanRight + }; + static inline constexpr std::array Roll = { + core::CVirtualGimbalEvent::RollLeft, + core::CVirtualGimbalEvent::RollRight + }; + static inline constexpr std::array PanOnly = { + core::CVirtualGimbalEvent::None, + core::CVirtualGimbalEvent::None, + core::CVirtualGimbalEvent::PanLeft, + core::CVirtualGimbalEvent::PanRight + }; + static inline constexpr std::array RelativeLook = { + core::CVirtualGimbalEvent::PanRight, + core::CVirtualGimbalEvent::PanLeft, + core::CVirtualGimbalEvent::TiltUp, + core::CVirtualGimbalEvent::TiltDown + }; + static inline constexpr std::array RelativeOrbitTranslate = { + core::CVirtualGimbalEvent::MoveRight, + core::CVirtualGimbalEvent::MoveLeft, + core::CVirtualGimbalEvent::MoveUp, + core::CVirtualGimbalEvent::MoveDown + }; + static inline constexpr std::array RelativeTopDown = { + core::CVirtualGimbalEvent::PanRight, + core::CVirtualGimbalEvent::PanLeft, + core::CVirtualGimbalEvent::MoveUp, + core::CVirtualGimbalEvent::MoveDown + }; +}; + +struct SCameraInteractionBindingSpec +{ + SKeyboardPresetSpec keyboard = {}; + SMousePresetSpec mouse = {}; +}; + +struct SCameraMappedInteractionBindingSpec +{ + IGimbalBindingLayout::keyboard_to_virtual_events_t keyboard; + IGimbalBindingLayout::mouse_to_virtual_events_t mouse; +}; + +inline constexpr size_t interactionFamilyIndex(const core::ECameraInteractionFamily family) +{ + return static_cast(family); +} + template inline void appendBindingSpec(Map& preset, const Codes& codes, const Events& events) { @@ -66,65 +262,32 @@ inline void appendBindingSpec(Map& preset, const Codes& codes, const Events& eve } } -inline IGimbalBindingLayout::keyboard_to_virtual_events_t buildKeyboardPreset(const SKeyboardPresetSpec& spec) +template +inline void appendMirroredBindingSpec(Map& preset, const Codes& codes, const virtual_event_t event) { - static constexpr std::array KeyboardWasdCodes = { - ui::E_KEY_CODE::EKC_W, - ui::E_KEY_CODE::EKC_S, - ui::E_KEY_CODE::EKC_A, - ui::E_KEY_CODE::EKC_D - }; - static constexpr std::array KeyboardQeCodes = { - ui::E_KEY_CODE::EKC_Q, - ui::E_KEY_CODE::EKC_E - }; - static constexpr std::array KeyboardIjklCodes = { - ui::E_KEY_CODE::EKC_I, - ui::E_KEY_CODE::EKC_K, - ui::E_KEY_CODE::EKC_J, - ui::E_KEY_CODE::EKC_L - }; + if (event == core::CVirtualGimbalEvent::None) + return; + std::array> duplicatedEvents = {}; + duplicatedEvents.fill(event); + appendBindingSpec(preset, codes, duplicatedEvents); +} + +inline IGimbalBindingLayout::keyboard_to_virtual_events_t buildKeyboardPreset(const SKeyboardPresetSpec& spec) +{ IGimbalBindingLayout::keyboard_to_virtual_events_t preset; - appendBindingSpec(preset, KeyboardWasdCodes, spec.wasd); - appendBindingSpec(preset, KeyboardQeCodes, spec.qe); - appendBindingSpec(preset, KeyboardIjklCodes, spec.ijkl); + appendBindingSpec(preset, SCameraInputBindingPhysicalGroups::KeyboardWasdCodes, spec.wasd); + appendBindingSpec(preset, SCameraInputBindingPhysicalGroups::KeyboardQeCodes, spec.qe); + appendBindingSpec(preset, SCameraInputBindingPhysicalGroups::KeyboardIjklCodes, spec.ijkl); return preset; } inline IGimbalBindingLayout::mouse_to_virtual_events_t buildMousePreset(const SMousePresetSpec& spec) { - static constexpr std::array RelativeMouseCodes = { - ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_X, - ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_X, - ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_Y, - ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_Y - }; - static constexpr std::array PositiveScrollCodes = { - ui::E_MOUSE_CODE::EMC_VERTICAL_POSITIVE_SCROLL, - ui::E_MOUSE_CODE::EMC_HORIZONTAL_POSITIVE_SCROLL - }; - static constexpr std::array NegativeScrollCodes = { - ui::E_MOUSE_CODE::EMC_VERTICAL_NEGATIVE_SCROLL, - ui::E_MOUSE_CODE::EMC_HORIZONTAL_NEGATIVE_SCROLL - }; - IGimbalBindingLayout::mouse_to_virtual_events_t preset; - appendBindingSpec(preset, RelativeMouseCodes, spec.relative); - if (spec.scroll[0] != core::CVirtualGimbalEvent::None) - { - appendBindingSpec( - preset, - PositiveScrollCodes, - std::array{ spec.scroll[0], spec.scroll[0] }); - } - if (spec.scroll[1] != core::CVirtualGimbalEvent::None) - { - appendBindingSpec( - preset, - NegativeScrollCodes, - std::array{ spec.scroll[1], spec.scroll[1] }); - } + appendBindingSpec(preset, SCameraInputBindingPhysicalGroups::RelativeMouseCodes, spec.relative); + appendMirroredBindingSpec(preset, SCameraInputBindingPhysicalGroups::PositiveScrollCodes, spec.scroll[0]); + appendMirroredBindingSpec(preset, SCameraInputBindingPhysicalGroups::NegativeScrollCodes, spec.scroll[1]); return preset; } @@ -142,305 +305,255 @@ inline IGimbalBindingLayout::imguizmo_to_virtual_events_t makeImguizmoPreset(con return preset; } -inline const IGimbalBindingLayout::keyboard_to_virtual_events_t& emptyKeyboardPreset() +inline constexpr SKeyboardPresetSpec makeKeyboardPresetSpec( + const keyboard_axis_group_t& wasd = {}, + const scalar_axis_pair_t& qe = {}, + const keyboard_axis_group_t& ijkl = {}) { - static const auto preset = IGimbalBindingLayout::keyboard_to_virtual_events_t{}; + return { + .wasd = wasd, + .qe = qe, + .ijkl = ijkl + }; +} + +inline constexpr SKeyboardPresetSpec withKeyboardQe( + const SKeyboardPresetSpec& base, + const scalar_axis_pair_t& qe) +{ + auto preset = base; + preset.qe = qe; return preset; } -inline const IGimbalBindingLayout::mouse_to_virtual_events_t& emptyMousePreset() +inline constexpr SKeyboardPresetSpec withKeyboardIjkl( + const SKeyboardPresetSpec& base, + const keyboard_axis_group_t& ijkl) { - static const auto preset = IGimbalBindingLayout::mouse_to_virtual_events_t{}; + auto preset = base; + preset.ijkl = ijkl; return preset; } -inline const IGimbalBindingLayout::keyboard_to_virtual_events_t& keyboardPresetForKind(const core::ICamera::CameraKind kind) +inline constexpr SMousePresetSpec makeMousePresetSpec( + const mouse_axis_group_t& relative = {}, + const scalar_axis_pair_t& scroll = {}) { - switch (kind) - { - case core::ICamera::CameraKind::FPS: - { - static const auto preset = buildKeyboardPreset({ - .wasd = { - core::CVirtualGimbalEvent::MoveForward, - core::CVirtualGimbalEvent::MoveBackward, - core::CVirtualGimbalEvent::MoveLeft, - core::CVirtualGimbalEvent::MoveRight - }, - .ijkl = { - core::CVirtualGimbalEvent::TiltDown, - core::CVirtualGimbalEvent::TiltUp, - core::CVirtualGimbalEvent::PanLeft, - core::CVirtualGimbalEvent::PanRight - } - }); - return preset; - } - case core::ICamera::CameraKind::Free: - { - static const auto preset = buildKeyboardPreset({ - .wasd = { - core::CVirtualGimbalEvent::MoveForward, - core::CVirtualGimbalEvent::MoveBackward, - core::CVirtualGimbalEvent::MoveLeft, - core::CVirtualGimbalEvent::MoveRight - }, - .qe = { - core::CVirtualGimbalEvent::RollLeft, - core::CVirtualGimbalEvent::RollRight - }, - .ijkl = { - core::CVirtualGimbalEvent::TiltDown, - core::CVirtualGimbalEvent::TiltUp, - core::CVirtualGimbalEvent::PanLeft, - core::CVirtualGimbalEvent::PanRight - } - }); - return preset; - } - case core::ICamera::CameraKind::Orbit: - { - static const auto preset = buildKeyboardPreset({ - .wasd = { - core::CVirtualGimbalEvent::MoveUp, - core::CVirtualGimbalEvent::MoveDown, - core::CVirtualGimbalEvent::MoveLeft, - core::CVirtualGimbalEvent::MoveRight - }, - .qe = { - core::CVirtualGimbalEvent::MoveBackward, - core::CVirtualGimbalEvent::MoveForward - } - }); - return preset; - } - case core::ICamera::CameraKind::Arcball: - case core::ICamera::CameraKind::Chase: - case core::ICamera::CameraKind::Dolly: - { - static const auto preset = buildKeyboardPreset({ - .wasd = { - core::CVirtualGimbalEvent::MoveForward, - core::CVirtualGimbalEvent::MoveBackward, - core::CVirtualGimbalEvent::MoveLeft, - core::CVirtualGimbalEvent::MoveRight - }, - .qe = { - core::CVirtualGimbalEvent::MoveDown, - core::CVirtualGimbalEvent::MoveUp - }, - .ijkl = { - core::CVirtualGimbalEvent::TiltDown, - core::CVirtualGimbalEvent::TiltUp, - core::CVirtualGimbalEvent::PanLeft, - core::CVirtualGimbalEvent::PanRight - } - }); - return preset; - } - case core::ICamera::CameraKind::Turntable: - { - static const auto preset = buildKeyboardPreset({ - .wasd = { - core::CVirtualGimbalEvent::MoveForward, - core::CVirtualGimbalEvent::MoveBackward, - core::CVirtualGimbalEvent::PanLeft, - core::CVirtualGimbalEvent::PanRight - }, - .ijkl = { - core::CVirtualGimbalEvent::TiltDown, - core::CVirtualGimbalEvent::TiltUp, - core::CVirtualGimbalEvent::PanLeft, - core::CVirtualGimbalEvent::PanRight - } - }); - return preset; - } - case core::ICamera::CameraKind::TopDown: - { - static const auto preset = buildKeyboardPreset({ - .wasd = { - core::CVirtualGimbalEvent::MoveUp, - core::CVirtualGimbalEvent::MoveDown, - core::CVirtualGimbalEvent::MoveLeft, - core::CVirtualGimbalEvent::MoveRight - }, - .qe = { - core::CVirtualGimbalEvent::MoveBackward, - core::CVirtualGimbalEvent::MoveForward - }, - .ijkl = { - core::CVirtualGimbalEvent::None, - core::CVirtualGimbalEvent::None, - core::CVirtualGimbalEvent::PanLeft, - core::CVirtualGimbalEvent::PanRight - } - }); - return preset; - } - case core::ICamera::CameraKind::Isometric: - case core::ICamera::CameraKind::DollyZoom: - { - static const auto preset = buildKeyboardPreset({ - .wasd = { - core::CVirtualGimbalEvent::MoveUp, - core::CVirtualGimbalEvent::MoveDown, - core::CVirtualGimbalEvent::MoveLeft, - core::CVirtualGimbalEvent::MoveRight - }, - .qe = { - core::CVirtualGimbalEvent::MoveBackward, - core::CVirtualGimbalEvent::MoveForward - } - }); - return preset; - } - case core::ICamera::CameraKind::Path: - { - static const auto preset = buildKeyboardPreset({ - .wasd = { - core::CVirtualGimbalEvent::MoveForward, - core::CVirtualGimbalEvent::MoveBackward, - core::CVirtualGimbalEvent::MoveLeft, - core::CVirtualGimbalEvent::MoveRight - }, - .qe = { - core::CVirtualGimbalEvent::MoveDown, - core::CVirtualGimbalEvent::MoveUp - } - }); - return preset; - } - default: - return emptyKeyboardPreset(); - } + return { + .relative = relative, + .scroll = scroll + }; } -inline const IGimbalBindingLayout::mouse_to_virtual_events_t& mousePresetForKind(const core::ICamera::CameraKind kind) +inline constexpr SMousePresetSpec withMouseScroll( + const SMousePresetSpec& base, + const scalar_axis_pair_t& scroll) { - switch (kind) - { - case core::ICamera::CameraKind::FPS: - case core::ICamera::CameraKind::Free: - { - static const auto preset = buildMousePreset({ - .relative = { - core::CVirtualGimbalEvent::PanRight, - core::CVirtualGimbalEvent::PanLeft, - core::CVirtualGimbalEvent::TiltUp, - core::CVirtualGimbalEvent::TiltDown - } - }); - return preset; - } - case core::ICamera::CameraKind::Orbit: - case core::ICamera::CameraKind::Isometric: - case core::ICamera::CameraKind::DollyZoom: - case core::ICamera::CameraKind::Path: - { - static const auto preset = buildMousePreset({ - .relative = { - core::CVirtualGimbalEvent::MoveRight, - core::CVirtualGimbalEvent::MoveLeft, - core::CVirtualGimbalEvent::MoveUp, - core::CVirtualGimbalEvent::MoveDown - }, - .scroll = { - core::CVirtualGimbalEvent::MoveForward, - core::CVirtualGimbalEvent::MoveBackward - } - }); - return preset; - } - case core::ICamera::CameraKind::Arcball: - case core::ICamera::CameraKind::Turntable: - case core::ICamera::CameraKind::Dolly: - { - static const auto preset = buildMousePreset({ - .relative = { - core::CVirtualGimbalEvent::PanRight, - core::CVirtualGimbalEvent::PanLeft, - core::CVirtualGimbalEvent::TiltUp, - core::CVirtualGimbalEvent::TiltDown - }, - .scroll = { - core::CVirtualGimbalEvent::MoveForward, - core::CVirtualGimbalEvent::MoveBackward - } - }); - return preset; - } - case core::ICamera::CameraKind::TopDown: - { - static const auto preset = buildMousePreset({ - .relative = { - core::CVirtualGimbalEvent::PanRight, - core::CVirtualGimbalEvent::PanLeft, - core::CVirtualGimbalEvent::MoveUp, - core::CVirtualGimbalEvent::MoveDown - }, - .scroll = { - core::CVirtualGimbalEvent::MoveForward, - core::CVirtualGimbalEvent::MoveBackward - } - }); - return preset; - } - case core::ICamera::CameraKind::Chase: - { - static const auto preset = buildMousePreset({ - .relative = { - core::CVirtualGimbalEvent::PanRight, - core::CVirtualGimbalEvent::PanLeft, - core::CVirtualGimbalEvent::TiltUp, - core::CVirtualGimbalEvent::TiltDown - }, - .scroll = { - core::CVirtualGimbalEvent::MoveUp, - core::CVirtualGimbalEvent::MoveDown - } - }); - return preset; - } - default: - return emptyMousePreset(); - } + auto preset = base; + preset.scroll = scroll; + return preset; +} + +inline constexpr SCameraInteractionBindingSpec makeInteractionBindingSpec( + const SKeyboardPresetSpec& keyboard = {}, + const SMousePresetSpec& mouse = {}) +{ + return { + .keyboard = keyboard, + .mouse = mouse + }; +} + +inline constexpr SCameraInteractionBindingSpec withInteractionKeyboard( + const SCameraInteractionBindingSpec& base, + const SKeyboardPresetSpec& keyboard) +{ + auto spec = base; + spec.keyboard = keyboard; + return spec; +} + +inline constexpr SCameraInteractionBindingSpec withInteractionMouse( + const SCameraInteractionBindingSpec& base, + const SMousePresetSpec& mouse) +{ + auto spec = base; + spec.mouse = mouse; + return spec; +} + +inline constexpr SCameraInteractionBindingSpec EmptyInteractionBindingSpec = makeInteractionBindingSpec(); + +inline constexpr SKeyboardPresetSpec FpsKeyboardSpec = makeKeyboardPresetSpec( + SCameraInputBindingEventGroups::FpsMove, + {}, + SCameraInputBindingEventGroups::LookYawPitch); + +inline constexpr SKeyboardPresetSpec FreeKeyboardSpec = withKeyboardQe( + FpsKeyboardSpec, + SCameraInputBindingEventGroups::Roll); + +inline constexpr SKeyboardPresetSpec OrbitKeyboardSpec = makeKeyboardPresetSpec( + SCameraInputBindingEventGroups::OrbitTranslate, + SCameraInputBindingEventGroups::OrbitZoom); + +inline constexpr SKeyboardPresetSpec TargetRigKeyboardSpec = withKeyboardQe( + FpsKeyboardSpec, + SCameraInputBindingEventGroups::VerticalMove); + +inline constexpr SKeyboardPresetSpec TurntableKeyboardSpec = makeKeyboardPresetSpec( + SCameraInputBindingEventGroups::TurntableMove, + {}, + FpsKeyboardSpec.ijkl); + +inline constexpr SKeyboardPresetSpec TopDownKeyboardSpec = withKeyboardIjkl( + OrbitKeyboardSpec, + SCameraInputBindingEventGroups::PanOnly); + +inline constexpr SKeyboardPresetSpec PathKeyboardSpec = withKeyboardQe( + makeKeyboardPresetSpec(SCameraInputBindingEventGroups::PathRigAdvanceAndRadius), + SCameraInputBindingEventGroups::PathRigHeight); + +inline constexpr SMousePresetSpec FpsMouseSpec = makeMousePresetSpec( + SCameraInputBindingEventGroups::RelativeLook); + +inline constexpr SMousePresetSpec OrbitMouseSpec = makeMousePresetSpec( + SCameraInputBindingEventGroups::RelativeOrbitTranslate, + SCameraInputBindingEventGroups::OrbitZoom); + +inline constexpr SMousePresetSpec TargetRigMouseSpec = withMouseScroll(FpsMouseSpec, OrbitMouseSpec.scroll); + +inline constexpr SMousePresetSpec TopDownMouseSpec = makeMousePresetSpec( + SCameraInputBindingEventGroups::RelativeTopDown, + OrbitMouseSpec.scroll); + +inline constexpr SCameraInteractionBindingSpec FpsInteractionBindingSpec = makeInteractionBindingSpec( + FpsKeyboardSpec, + FpsMouseSpec); + +inline constexpr SCameraInteractionBindingSpec FreeInteractionBindingSpec = withInteractionKeyboard( + FpsInteractionBindingSpec, + FreeKeyboardSpec); + +inline constexpr SCameraInteractionBindingSpec OrbitInteractionBindingSpec = makeInteractionBindingSpec( + OrbitKeyboardSpec, + OrbitMouseSpec); + +inline constexpr SCameraInteractionBindingSpec TargetRigInteractionBindingSpec = withInteractionKeyboard( + withInteractionMouse(FpsInteractionBindingSpec, TargetRigMouseSpec), + TargetRigKeyboardSpec); + +inline constexpr SCameraInteractionBindingSpec TurntableInteractionBindingSpec = withInteractionKeyboard( + TargetRigInteractionBindingSpec, + TurntableKeyboardSpec); + +inline constexpr SCameraInteractionBindingSpec TopDownInteractionBindingSpec = withInteractionKeyboard( + withInteractionMouse(OrbitInteractionBindingSpec, TopDownMouseSpec), + TopDownKeyboardSpec); + +inline constexpr SCameraInteractionBindingSpec PathInteractionBindingSpec = withInteractionKeyboard( + OrbitInteractionBindingSpec, + PathKeyboardSpec); + +template +inline auto makePresetCache(const SpecArray& specs, Builder&& builder) +{ + std::array> cache = {}; + for (size_t i = 0u; i < specs.size(); ++i) + cache[i] = builder(specs[i]); + return cache; +} + +inline SCameraMappedInteractionBindingSpec mapInteractionBindingSpec(const SCameraInteractionBindingSpec& spec) +{ + return { + .keyboard = buildKeyboardPreset(spec.keyboard), + .mouse = buildMousePreset(spec.mouse) + }; +} + +inline constexpr std::array InteractionFamilyPresetSpecs = {{ + EmptyInteractionBindingSpec, + FpsInteractionBindingSpec, + FreeInteractionBindingSpec, + OrbitInteractionBindingSpec, + TargetRigInteractionBindingSpec, + TurntableInteractionBindingSpec, + TopDownInteractionBindingSpec, + PathInteractionBindingSpec +}}; + +inline const SCameraMappedInteractionBindingSpec& interactionBindingPresetForKind(const core::ICamera::CameraKind kind) +{ + const auto familyIx = interactionFamilyIndex(core::getCameraInteractionFamily(kind)); + static const auto cache = makePresetCache( + InteractionFamilyPresetSpecs, + [](const SCameraInteractionBindingSpec& spec) { return mapInteractionBindingSpec(spec); }); + return cache[familyIx < cache.size() ? familyIx : 0u]; } } // namespace impl +inline const IGimbalBindingLayout::keyboard_to_virtual_events_t& getDefaultCameraKeyboardMappingPreset(const core::ICamera::CameraKind kind) +{ + return impl::interactionBindingPresetForKind(kind).keyboard; +} + inline const IGimbalBindingLayout::keyboard_to_virtual_events_t& getDefaultCameraKeyboardMappingPreset(const core::ICamera& camera) { - return impl::keyboardPresetForKind(camera.getKind()); + return getDefaultCameraKeyboardMappingPreset(camera.getKind()); +} + +inline const IGimbalBindingLayout::mouse_to_virtual_events_t& getDefaultCameraMouseMappingPreset(const core::ICamera::CameraKind kind) +{ + return impl::interactionBindingPresetForKind(kind).mouse; } inline const IGimbalBindingLayout::mouse_to_virtual_events_t& getDefaultCameraMouseMappingPreset(const core::ICamera& camera) { - return impl::mousePresetForKind(camera.getKind()); + return getDefaultCameraMouseMappingPreset(camera.getKind()); +} + +inline IGimbalBindingLayout::imguizmo_to_virtual_events_t buildDefaultCameraImguizmoMappingPreset(const uint32_t allowedVirtualEvents) +{ + return impl::makeImguizmoPreset(allowedVirtualEvents); } inline IGimbalBindingLayout::imguizmo_to_virtual_events_t buildDefaultCameraImguizmoMappingPreset(const core::ICamera& camera) { - return impl::makeImguizmoPreset(camera.getAllowedVirtualEvents()); + return buildDefaultCameraImguizmoMappingPreset(camera.getAllowedVirtualEvents()); } -inline SCameraInputBindingPreset buildDefaultCameraInputBindingPreset(const core::ICamera& camera) +inline SCameraInputBindingPreset buildDefaultCameraInputBindingPreset(const core::ICamera::CameraKind kind, const uint32_t allowedVirtualEvents) { SCameraInputBindingPreset preset; - preset.keyboard = getDefaultCameraKeyboardMappingPreset(camera); - preset.mouse = getDefaultCameraMouseMappingPreset(camera); - preset.imguizmo = buildDefaultCameraImguizmoMappingPreset(camera); + preset.keyboard = getDefaultCameraKeyboardMappingPreset(kind); + preset.mouse = getDefaultCameraMouseMappingPreset(kind); + preset.imguizmo = buildDefaultCameraImguizmoMappingPreset(allowedVirtualEvents); return preset; } -inline void applyDefaultCameraInputBindingPreset(IGimbalBindingLayout& layout, const core::ICamera& camera) +inline SCameraInputBindingPreset buildDefaultCameraInputBindingPreset(const core::ICamera& camera) { - const auto preset = buildDefaultCameraInputBindingPreset(camera); + return buildDefaultCameraInputBindingPreset(camera.getKind(), camera.getAllowedVirtualEvents()); +} + +inline void applyDefaultCameraInputBindingPreset( + IGimbalBindingLayout& layout, + const core::ICamera::CameraKind kind, + const uint32_t allowedVirtualEvents) +{ + const auto preset = buildDefaultCameraInputBindingPreset(kind, allowedVirtualEvents); layout.updateKeyboardMapping([&](auto& map) { map = preset.keyboard; }); layout.updateMouseMapping([&](auto& map) { map = preset.mouse; }); layout.updateImguizmoMapping([&](auto& map) { map = preset.imguizmo; }); } +inline void applyDefaultCameraInputBindingPreset(IGimbalBindingLayout& layout, const core::ICamera& camera) +{ + applyDefaultCameraInputBindingPreset(layout, camera.getKind(), camera.getAllowedVirtualEvents()); +} + } // namespace nbl::ui #endif // _NBL_C_CAMERA_INPUT_BINDING_UTILITIES_HPP_ diff --git a/common/include/camera/CCameraKeyframeTrack.hpp b/common/include/camera/CCameraKeyframeTrack.hpp index 822467e68..396512c86 100644 --- a/common/include/camera/CCameraKeyframeTrack.hpp +++ b/common/include/camera/CCameraKeyframeTrack.hpp @@ -25,7 +25,7 @@ struct CCameraKeyframeTrack inline bool compareKeyframes(const CCameraKeyframe& lhs, const CCameraKeyframe& rhs, const double timeEps, const double posEps, const double rotEpsDeg, const double scalarEps) { - return std::abs(static_cast(lhs.time - rhs.time)) <= timeEps && + return hlsl::abs(static_cast(lhs.time - rhs.time)) <= timeEps && comparePresets(lhs.preset, rhs.preset, posEps, rotEpsDeg, scalarEps); } @@ -107,10 +107,10 @@ inline int selectKeyframeTrackNearestTime(CCameraKeyframeTrack& track, const flo } size_t bestIx = 0u; - float bestDelta = std::abs(track.keyframes.front().time - time); + float bestDelta = hlsl::abs(track.keyframes.front().time - time); for (size_t i = 1u; i < track.keyframes.size(); ++i) { - const float delta = std::abs(track.keyframes[i].time - time); + const float delta = hlsl::abs(track.keyframes[i].time - time); if (delta < bestDelta) { bestDelta = delta; diff --git a/common/include/camera/CCameraKeyframeTrackPersistence.hpp b/common/include/camera/CCameraKeyframeTrackPersistence.hpp index 7b8c718f9..963450c7d 100644 --- a/common/include/camera/CCameraKeyframeTrackPersistence.hpp +++ b/common/include/camera/CCameraKeyframeTrackPersistence.hpp @@ -13,15 +13,17 @@ namespace nbl::system { +class ISystem; + //! Serialize one camera keyframe track into an existing stream. bool writeKeyframeTrack(std::ostream& out, const core::CCameraKeyframeTrack& track, int indent = 2); //! Deserialize one camera keyframe track from an existing stream. bool readKeyframeTrack(std::istream& in, core::CCameraKeyframeTrack& track); //! Save one camera keyframe track to a file. -bool saveKeyframeTrackToFile(const path& path, const core::CCameraKeyframeTrack& track, int indent = 2); +bool saveKeyframeTrackToFile(ISystem& system, const path& path, const core::CCameraKeyframeTrack& track, int indent = 2); //! Load one camera keyframe track from a file. -bool loadKeyframeTrackFromFile(const path& path, core::CCameraKeyframeTrack& track); +bool loadKeyframeTrackFromFile(ISystem& system, const path& path, core::CCameraKeyframeTrack& track); } // namespace nbl::system diff --git a/common/include/camera/CCameraKindUtilities.hpp b/common/include/camera/CCameraKindUtilities.hpp new file mode 100644 index 000000000..d461ab3b7 --- /dev/null +++ b/common/include/camera/CCameraKindUtilities.hpp @@ -0,0 +1,139 @@ +#ifndef _C_CAMERA_KIND_UTILITIES_HPP_ +#define _C_CAMERA_KIND_UTILITIES_HPP_ + +#include +#include + +#include "CCameraPathUtilities.hpp" + +namespace nbl::core +{ + +//! Interaction family used to group camera kinds with matching control semantics. +enum class ECameraInteractionFamily : uint8_t +{ + None, + Fps, + Free, + Orbit, + TargetRig, + Turntable, + TopDown, + Path +}; + +//! Shared metadata for one concrete `CameraKind`. +struct SCameraKindTraits final +{ + ICamera::CameraKind kind = ICamera::CameraKind::Unknown; + std::string_view label = "Unknown"; + std::string_view description = "Unspecified camera behavior"; + ECameraInteractionFamily interactionFamily = ECameraInteractionFamily::None; +}; + +namespace impl +{ + +inline constexpr std::array(ICamera::CameraKind::Path) + 1u> CameraKindTraitsTable = {{ + { + .kind = ICamera::CameraKind::Unknown, + .label = "Unknown", + .description = "Unspecified camera behavior", + .interactionFamily = ECameraInteractionFamily::None + }, + { + .kind = ICamera::CameraKind::FPS, + .label = "FPS", + .description = "First-person WASD + mouse look", + .interactionFamily = ECameraInteractionFamily::Fps + }, + { + .kind = ICamera::CameraKind::Free, + .label = "Free", + .description = "Free-fly 6DOF with full rotation", + .interactionFamily = ECameraInteractionFamily::Free + }, + { + .kind = ICamera::CameraKind::Orbit, + .label = "Orbit", + .description = "Orbit around target with dolly", + .interactionFamily = ECameraInteractionFamily::Orbit + }, + { + .kind = ICamera::CameraKind::Arcball, + .label = "Arcball", + .description = "Arcball trackball around target", + .interactionFamily = ECameraInteractionFamily::Orbit + }, + { + .kind = ICamera::CameraKind::Turntable, + .label = "Turntable", + .description = "Turntable yaw/pitch around target", + .interactionFamily = ECameraInteractionFamily::Turntable + }, + { + .kind = ICamera::CameraKind::TopDown, + .label = "TopDown", + .description = "Fixed pitch top-down pan", + .interactionFamily = ECameraInteractionFamily::TopDown + }, + { + .kind = ICamera::CameraKind::Isometric, + .label = "Isometric", + .description = "Fixed isometric view with pan", + .interactionFamily = ECameraInteractionFamily::Orbit + }, + { + .kind = ICamera::CameraKind::Chase, + .label = "Chase", + .description = "Target follow with chase controls", + .interactionFamily = ECameraInteractionFamily::TargetRig + }, + { + .kind = ICamera::CameraKind::Dolly, + .label = "Dolly", + .description = "Rig truck/dolly with look-at", + .interactionFamily = ECameraInteractionFamily::TargetRig + }, + { + .kind = ICamera::CameraKind::DollyZoom, + .label = "Dolly Zoom", + .description = "Orbit with dolly-zoom FOV", + .interactionFamily = ECameraInteractionFamily::Orbit + }, + { + .kind = ICamera::CameraKind::Path, + .label = "Path Rig", + .description = SCameraPathDefaults::Description, + .interactionFamily = ECameraInteractionFamily::Path + } +}}; + +} // namespace impl + +inline constexpr const SCameraKindTraits& getCameraKindTraits(const ICamera::CameraKind kind) +{ + const auto ix = static_cast(kind); + if (ix >= impl::CameraKindTraitsTable.size()) + return impl::CameraKindTraitsTable[0u]; + return impl::CameraKindTraitsTable[ix]; +} + +inline constexpr std::string_view getCameraKindLabel(const ICamera::CameraKind kind) +{ + return getCameraKindTraits(kind).label; +} + +inline constexpr std::string_view getCameraKindDescription(const ICamera::CameraKind kind) +{ + return getCameraKindTraits(kind).description; +} + +inline constexpr ECameraInteractionFamily getCameraInteractionFamily(const ICamera::CameraKind kind) +{ + return getCameraKindTraits(kind).interactionFamily; +} + +} // namespace nbl::core + +#endif // _C_CAMERA_KIND_UTILITIES_HPP_ diff --git a/common/include/camera/CCameraManipulationUtilities.hpp b/common/include/camera/CCameraManipulationUtilities.hpp index 07cdbf7b8..d0aa8fc17 100644 --- a/common/include/camera/CCameraManipulationUtilities.hpp +++ b/common/include/camera/CCameraManipulationUtilities.hpp @@ -9,10 +9,23 @@ #include #include "CCameraPresetFlow.hpp" +#include "CCameraVirtualEventUtilities.hpp" namespace nbl::core { +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 = ICamera::SphericalMinDistance; + static constexpr float MaxDistance = ICamera::SphericalMaxDistance; +}; + //! Reusable constraint settings for post-manipulation camera clamping. struct SCameraConstraintSettings { @@ -21,14 +34,14 @@ struct SCameraConstraintSettings bool clampYaw = false; bool clampRoll = false; bool clampDistance = false; - float pitchMinDeg = -80.f; - float pitchMaxDeg = 80.f; - float yawMinDeg = -180.f; - float yawMaxDeg = 180.f; - float rollMinDeg = -180.f; - float rollMaxDeg = 180.f; - float minDistance = 0.1f; - float maxDistance = 1000.f; + 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; }; //! Apply an authored world-space reference frame through the shared camera runtime entry point. @@ -46,17 +59,11 @@ inline void scaleVirtualEvents(std::vector& events, const u for (uint32_t i = 0u; i < count; ++i) { auto& ev = events[i]; - const auto type = ev.type; - - if (type == CVirtualGimbalEvent::MoveForward || type == CVirtualGimbalEvent::MoveBackward || - type == CVirtualGimbalEvent::MoveLeft || type == CVirtualGimbalEvent::MoveRight || - type == CVirtualGimbalEvent::MoveUp || type == CVirtualGimbalEvent::MoveDown) + if (CVirtualGimbalEvent::isTranslationEvent(ev.type)) { ev.magnitude *= translationScale; } - else if (type == CVirtualGimbalEvent::TiltUp || type == CVirtualGimbalEvent::TiltDown || - type == CVirtualGimbalEvent::PanLeft || type == CVirtualGimbalEvent::PanRight || - type == CVirtualGimbalEvent::RollLeft || type == CVirtualGimbalEvent::RollRight) + else if (CVirtualGimbalEvent::isRotationEvent(ev.type)) { ev.magnitude *= rotationScale; } @@ -69,57 +76,25 @@ inline void remapTranslationEventsFromWorldToCameraLocal(ICamera* camera, std::v if (!camera) return; - hlsl::float64_t3 worldDelta = hlsl::float64_t3(0.0); std::vector filtered; filtered.reserve(events.size()); for (uint32_t i = 0u; i < count; ++i) { const auto& ev = events[i]; - switch (ev.type) - { - case CVirtualGimbalEvent::MoveRight: worldDelta.x += ev.magnitude; break; - case CVirtualGimbalEvent::MoveLeft: worldDelta.x -= ev.magnitude; break; - case CVirtualGimbalEvent::MoveUp: worldDelta.y += ev.magnitude; break; - case CVirtualGimbalEvent::MoveDown: worldDelta.y -= ev.magnitude; break; - case CVirtualGimbalEvent::MoveForward: worldDelta.z += ev.magnitude; break; - case CVirtualGimbalEvent::MoveBackward: worldDelta.z -= ev.magnitude; break; - default: - filtered.emplace_back(ev); - break; - } + if (!CVirtualGimbalEvent::isTranslationEvent(ev.type)) + filtered.emplace_back(ev); } - if (worldDelta.x == 0.0 && worldDelta.y == 0.0 && worldDelta.z == 0.0) + const auto worldDelta = collectSignedTranslationDelta({ events.data(), count }); + if (hlsl::isNearlyZeroVector(worldDelta, static_cast(ICamera::TinyScalarEpsilon))) { events = std::move(filtered); count = static_cast(events.size()); return; } - const auto& gimbal = camera->getGimbal(); - const auto right = gimbal.getXAxis(); - const auto up = gimbal.getYAxis(); - const auto forward = gimbal.getZAxis(); - - const hlsl::float64_t3 localDelta = hlsl::float64_t3( - hlsl::dot(worldDelta, right), - hlsl::dot(worldDelta, up), - hlsl::dot(worldDelta, forward) - ); - - auto emitAxis = [&](double v, CVirtualGimbalEvent::VirtualEventType pos, CVirtualGimbalEvent::VirtualEventType neg) - { - if (v == 0.0) - return; - auto& ev = filtered.emplace_back(); - ev.type = (v > 0.0) ? pos : neg; - ev.magnitude = std::abs(v); - }; - - emitAxis(localDelta.x, CVirtualGimbalEvent::MoveRight, CVirtualGimbalEvent::MoveLeft); - emitAxis(localDelta.y, CVirtualGimbalEvent::MoveUp, CVirtualGimbalEvent::MoveDown); - emitAxis(localDelta.z, CVirtualGimbalEvent::MoveForward, CVirtualGimbalEvent::MoveBackward); + appendWorldTranslationAsLocalEvents(filtered, camera->getGimbal().getOrientation(), worldDelta); events = std::move(filtered); count = static_cast(events.size()); @@ -152,7 +127,7 @@ inline bool applyCameraConstraints(const CCameraGoalSolver& solver, ICamera* cam const auto& gimbal = camera->getGimbal(); const auto pos = gimbal.getPosition(); - const auto eulerDeg = hlsl::getQuaternionEulerDegrees(gimbal.getOrientation()); + const auto eulerDeg = hlsl::getCameraOrientationEulerDegrees(gimbal.getOrientation()); auto clamped = eulerDeg; if (constraints.clampPitch) @@ -167,7 +142,7 @@ inline bool applyCameraConstraints(const CCameraGoalSolver& solver, ICamera* cam CCameraPreset preset; preset.goal.position = pos; - preset.goal.orientation = hlsl::makeQuaternionFromEulerDegrees(clamped); + preset.goal.orientation = hlsl::makeQuaternionFromEulerDegreesYXZ(clamped); return applyPreset(solver, camera, preset); } diff --git a/common/include/camera/CCameraMathUtilities.hpp b/common/include/camera/CCameraMathUtilities.hpp index 3d4051718..bfc85dfe6 100644 --- a/common/include/camera/CCameraMathUtilities.hpp +++ b/common/include/camera/CCameraMathUtilities.hpp @@ -28,7 +28,7 @@ inline T wrapAngleRad(T angle) template inline T getWrappedAngleDistanceRadians(const T a, const T b) { - return std::abs(wrapAngleRad(a - b)); + return hlsl::abs(wrapAngleRad(a - b)); } template @@ -40,7 +40,13 @@ inline T getWrappedAngleDistanceDegrees(const T a, const T b) T angle = std::fmod(a - b + HalfTurn, FullTurn); if (angle < static_cast(0)) angle += FullTurn; - return std::abs(angle - HalfTurn); + return hlsl::abs(angle - HalfTurn); +} + +template +inline T lerpWrappedAngleRad(const T a, const T b, const T alpha) +{ + return a + wrapAngleRad(b - a) * alpha; } template @@ -49,6 +55,12 @@ inline bool isFiniteScalar(const T value) return std::isfinite(value); } +template +inline constexpr T getCameraMathEpsilon() +{ + return std::numeric_limits::epsilon(); +} + template using camera_vector_t = vector; @@ -58,6 +70,59 @@ using camera_matrix_t = matrix; template using camera_quaternion_t = math::quaternion; +struct SCameraViewRigDefaults final +{ + static constexpr double DegreesToRadians = numbers::pi / 180.0; + static constexpr double ArcballPitchLimitDeg = 89.0; + static constexpr double TurntablePitchLimitDeg = ArcballPitchLimitDeg; + static constexpr double ChaseMaxPitchDeg = 70.0; + static constexpr double ChaseMinPitchDeg = -60.0; + static constexpr double DollyPitchLimitDeg = 85.0; + static constexpr double FpsVerticalPitchLimitDeg = 88.0; + static constexpr double TopDownPitchDeg = -90.0; + static constexpr double IsometricYawDeg = 45.0; + static constexpr double IsometricPitchDeg = 35.264389682754654; + + static inline constexpr double ArcballPitchLimitRad = ArcballPitchLimitDeg * DegreesToRadians; + static inline constexpr double TurntablePitchLimitRad = TurntablePitchLimitDeg * DegreesToRadians; + static inline constexpr double ChaseMaxPitchRad = ChaseMaxPitchDeg * DegreesToRadians; + static inline constexpr double ChaseMinPitchRad = ChaseMinPitchDeg * DegreesToRadians; + static inline constexpr double DollyPitchLimitRad = DollyPitchLimitDeg * DegreesToRadians; + static inline constexpr double FpsVerticalPitchLimitRad = FpsVerticalPitchLimitDeg * DegreesToRadians; + static inline constexpr double TopDownPitchRad = TopDownPitchDeg * DegreesToRadians; + static inline constexpr double IsometricYawRad = IsometricYawDeg * DegreesToRadians; + static inline constexpr double IsometricPitchRad = IsometricPitchDeg * DegreesToRadians; +}; + +struct SCameraRigidMathDefaults final +{ + static constexpr double LookAtParallelThreshold = 0.99; +}; + +template +inline constexpr camera_vector_t getCameraWorldRight() +{ + return camera_vector_t(T(1), T(0), T(0)); +} + +template +inline constexpr camera_vector_t getCameraWorldUp() +{ + return camera_vector_t(T(0), T(1), T(0)); +} + +template +inline constexpr camera_vector_t getCameraWorldForward() +{ + return camera_vector_t(T(0), T(0), T(1)); +} + +template +inline constexpr T getCameraLookAtParallelThreshold() +{ + return static_cast(SCameraRigidMathDefaults::LookAtParallelThreshold); +} + template inline camera_quaternion_t makeIdentityQuaternion(); @@ -97,12 +162,18 @@ inline camera_quaternion_t normalizeQuaternion(const camera_quaternion_t& template inline bool isFiniteQuaternion(const camera_quaternion_t& q) { - return std::isfinite(q.data.x) && - std::isfinite(q.data.y) && - std::isfinite(q.data.z) && - std::isfinite(q.data.w); + return isFiniteScalar(q.data.x) && + isFiniteScalar(q.data.y) && + isFiniteScalar(q.data.z) && + isFiniteScalar(q.data.w); } +template +inline bool isFiniteVec3(const camera_vector_t& value); + +template +inline camera_vector_t safeNormalizeVec3(const camera_vector_t& value, const camera_vector_t& fallback); + template inline camera_quaternion_t makeQuaternionFromAxisAngle(const camera_vector_t& axis, const T radians) { @@ -124,34 +195,44 @@ inline camera_quaternion_t makeQuaternionFromEulerDegrees(const camera_vector radians(eulerDegrees.z))); } +template +inline camera_quaternion_t makeQuaternionFromEulerRadiansYXZ(const camera_vector_t& eulerRadians) +{ + const auto pitch = makeQuaternionFromAxisAngle(getCameraWorldRight(), eulerRadians.x); + const auto yaw = makeQuaternionFromAxisAngle(getCameraWorldUp(), eulerRadians.y); + const auto roll = makeQuaternionFromAxisAngle(getCameraWorldForward(), eulerRadians.z); + return normalizeQuaternion(yaw * pitch * roll); +} + +template +inline camera_quaternion_t makeQuaternionFromEulerDegreesYXZ(const camera_vector_t& eulerDegrees) +{ + return makeQuaternionFromEulerRadiansYXZ(camera_vector_t( + radians(eulerDegrees.x), + radians(eulerDegrees.y), + radians(eulerDegrees.z))); +} + template inline camera_quaternion_t makeQuaternionFromBasis( const camera_vector_t& right, const camera_vector_t& up, const camera_vector_t& forward) { - const auto safeNormalize = [](const camera_vector_t& v, const camera_vector_t& fallback) - { - const auto len = length(v); - if (!std::isfinite(len) || len <= std::numeric_limits::epsilon()) - return fallback; - return v / len; - }; - - const auto canonicalForward = safeNormalize(forward, camera_vector_t(T(0), T(0), T(1))); + const auto canonicalForward = safeNormalizeVec3(forward, getCameraWorldForward()); auto canonicalRight = right - canonicalForward * dot(right, canonicalForward); - canonicalRight = safeNormalize( + canonicalRight = safeNormalizeVec3( canonicalRight, - safeNormalize(cross(up, canonicalForward), camera_vector_t(T(1), T(0), T(0)))); + safeNormalizeVec3(cross(up, canonicalForward), getCameraWorldRight())); auto canonicalUp = cross(canonicalForward, canonicalRight); - canonicalUp = safeNormalize( + canonicalUp = safeNormalizeVec3( canonicalUp, - safeNormalize(up - canonicalForward * dot(up, canonicalForward), camera_vector_t(T(0), T(1), T(0)))); + safeNormalizeVec3(up - canonicalForward * dot(up, canonicalForward), getCameraWorldUp())); - canonicalRight = safeNormalize(cross(canonicalUp, canonicalForward), canonicalRight); - canonicalUp = safeNormalize(cross(canonicalForward, canonicalRight), canonicalUp); + canonicalRight = safeNormalizeVec3(cross(canonicalUp, canonicalForward), canonicalRight); + canonicalUp = safeNormalizeVec3(cross(canonicalForward, canonicalRight), canonicalUp); const camera_matrix_t basis { canonicalRight, canonicalUp, canonicalForward }; const auto desiredRight = canonicalRight; @@ -222,11 +303,9 @@ inline camera_quaternion_t makeQuaternionFromBasis( }; const camera_matrix_t transposedBasis = hlsl::transpose(basis); - const camera_quaternion_t castCandidates[] = { - normalizeQuaternion(hlsl::_static_cast>(basis)), - normalizeQuaternion(hlsl::_static_cast>(transposedBasis)) - }; - const camera_quaternion_t fallbackCandidates[] = { + const camera_quaternion_t candidates[] = { + camera_quaternion_t::create(basis, true), + camera_quaternion_t::create(transposedBasis, true), quaternionFromMatrixFallback(basis), quaternionFromMatrixFallback(transposedBasis) }; @@ -234,8 +313,7 @@ inline camera_quaternion_t makeQuaternionFromBasis( camera_quaternion_t bestCandidate = makeIdentityQuaternion(); T bestScore = std::numeric_limits::infinity(); bool foundFiniteCandidate = false; - - for (const auto& candidate : castCandidates) + const auto considerCandidate = [&](const camera_quaternion_t& candidate) { const T score = scoreCandidate(candidate); if (score < bestScore) @@ -244,23 +322,12 @@ inline camera_quaternion_t makeQuaternionFromBasis( bestCandidate = candidate; foundFiniteCandidate = true; } - } + }; - if (!foundFiniteCandidate) - { - for (const auto& candidate : fallbackCandidates) - { - const T score = scoreCandidate(candidate); - if (score < bestScore) - { - bestScore = score; - bestCandidate = candidate; - foundFiniteCandidate = true; - } - } - } + for (const auto& candidate : candidates) + considerCandidate(candidate); - if (!foundFiniteCandidate) + if (!foundFiniteCandidate || !isFiniteQuaternion(bestCandidate)) return makeIdentityQuaternion(); return normalizeQuaternion(bestCandidate); @@ -275,9 +342,34 @@ inline bool isFiniteVec3(const camera_vector_t& value) template inline bool nearlyEqualScalar(const T a, const T b, const T epsilon) { - return std::abs(a - b) <= epsilon; + return hlsl::abs(a - b) <= epsilon; +} + +template +inline bool isNearlyZeroScalar(const T value, const T epsilon = getCameraMathEpsilon()) +{ + return hlsl::abs(value) <= epsilon; +} + +template +inline bool isNearlyZeroVector(const camera_vector_t& value, const T epsilon = getCameraMathEpsilon()) +{ + return length(value) <= epsilon; } +template +inline bool hasPlanarDeltaXY(const camera_vector_t& value, const T epsilon = std::numeric_limits::epsilon()) +{ + return !isNearlyZeroVector(camera_vector_t(value.x, value.y), epsilon); +} + +template +struct SCameraPoseDelta +{ + T position = T(0); + T rotationDeg = T(0); +}; + template inline bool nearlyEqualVec3(const VecA& a, const VecB& b, const T epsilon) { @@ -292,11 +384,47 @@ template inline camera_vector_t safeNormalizeVec3(const camera_vector_t& value, const camera_vector_t& fallback) { const auto len = length(value); - if (!isFiniteScalar(len) || len <= std::numeric_limits::epsilon()) + if (!isFiniteScalar(len) || len <= getCameraMathEpsilon()) return fallback; return value / len; } +template +inline bool tryBuildCameraBasisFromForwardUpHint( + const camera_vector_t& forwardHint, + const camera_vector_t& upHint, + camera_vector_t& outRight, + camera_vector_t& outUp, + camera_vector_t& outForward) +{ + const auto forward = safeNormalizeVec3(forwardHint, getCameraWorldForward()); + if (!isFiniteVec3(forward) || isNearlyZeroVector(forward)) + return false; + + const auto preferredUp = safeNormalizeVec3(upHint, getCameraWorldForward()); + auto right = cross(preferredUp, forward); + if (!isFiniteVec3(right) || isNearlyZeroVector(right)) + { + const auto fallbackUp = hlsl::abs(forward.z) < getCameraLookAtParallelThreshold() ? + getCameraWorldForward() : + getCameraWorldUp(); + right = cross(fallbackUp, forward); + if (!isFiniteVec3(right) || isNearlyZeroVector(right)) + return false; + } + + right = safeNormalizeVec3(right, getCameraWorldRight()); + auto up = safeNormalizeVec3(cross(forward, right), preferredUp); + right = safeNormalizeVec3(cross(up, forward), right); + if (!isOrthoBase(right, up, forward)) + return false; + + outRight = right; + outUp = up; + outForward = forward; + return true; +} + template inline camera_vector_t makeSphericalOffsetFromOrbit(const T orbitU, const T orbitV, const T distance) { @@ -330,8 +458,9 @@ inline bool sanitizePathState(T& angle, T& radius, T& height, const T minRadius) if (!isFiniteScalar(angle) || !isFiniteScalar(radius) || !isFiniteScalar(height)) return false; + angle = wrapAngleRad(angle); radius = std::max(minRadius, radius); - return isFiniteScalar(radius); + return isFiniteScalar(angle) && isFiniteScalar(radius) && isFiniteScalar(height); } template @@ -378,7 +507,7 @@ inline bool tryBuildPathStateFromPosition( if (!isFiniteScalar(radius) || !isFiniteScalar(offset.y)) return false; - outAngle = hlsl::atan2(offset.z, offset.x); + outAngle = wrapAngleRad(hlsl::atan2(offset.z, offset.x)); outRadius = std::max(minRadius, radius); outHeight = offset.y; return isFiniteScalar(outAngle) && isFiniteScalar(outRadius) && isFiniteScalar(outHeight); @@ -392,26 +521,10 @@ inline bool tryBuildLookAtOrientation( camera_quaternion_t& outOrientation) { const auto toTarget = targetPosition - position; - const auto toTargetLength = length(toTarget); - if (!isFiniteScalar(toTargetLength) || toTargetLength <= std::numeric_limits::epsilon()) - return false; - - const auto forward = toTarget / toTargetLength; - auto up = safeNormalizeVec3(preferredUp, camera_vector_t(T(0), T(0), T(1))); - auto right = cross(up, forward); - if (!isFiniteVec3(right) || length(right) <= std::numeric_limits::epsilon()) - { - const auto fallbackUp = std::abs(forward.z) < T(0.99) ? - camera_vector_t(T(0), T(0), T(1)) : - camera_vector_t(T(0), T(1), T(0)); - right = cross(fallbackUp, forward); - if (!isFiniteVec3(right) || length(right) <= std::numeric_limits::epsilon()) - return false; - } - - right = normalize(right); - up = normalize(cross(forward, right)); - if (!isOrthoBase(right, up, forward)) + camera_vector_t right = camera_vector_t(T(0)); + camera_vector_t up = camera_vector_t(T(0)); + camera_vector_t forward = camera_vector_t(T(0)); + if (!tryBuildCameraBasisFromForwardUpHint(toTarget, preferredUp, right, up, forward)) return false; outOrientation = makeQuaternionFromBasis(right, up, forward); @@ -450,17 +563,16 @@ inline bool tryBuildSphericalPoseFromOrbit( const T appliedDistance = std::clamp(distance, minDistance, maxDistance); const auto spherePosition = makeSphericalOffsetFromOrbit(orbitU, orbitV, appliedDistance); - const auto forward = safeNormalizeVec3(-spherePosition, camera_vector_t(T(0), T(0), T(1))); - auto up = safeNormalizeVec3( + const auto upHint = safeNormalizeVec3( camera_vector_t( -hlsl::sin(orbitV) * hlsl::cos(orbitU), -hlsl::sin(orbitV) * hlsl::sin(orbitU), hlsl::cos(orbitV)), - camera_vector_t(T(0), T(0), T(1))); - auto right = safeNormalizeVec3(cross(up, forward), camera_vector_t(T(1), T(0), T(0))); - up = safeNormalizeVec3(cross(forward, right), up); - right = safeNormalizeVec3(cross(up, forward), right); - if (!isOrthoBase(right, up, forward)) + getCameraWorldForward()); + camera_vector_t right = camera_vector_t(T(0)); + camera_vector_t up = camera_vector_t(T(0)); + camera_vector_t forward = camera_vector_t(T(0)); + if (!tryBuildCameraBasisFromForwardUpHint(-spherePosition, upHint, right, up, forward)) return false; outPosition = targetPosition + spherePosition; @@ -482,7 +594,7 @@ inline bool tryBuildOrbitFromPosition( { const auto offset = position - targetPosition; const auto distance = length(offset); - if (!isFiniteScalar(distance) || distance <= std::numeric_limits::epsilon()) + if (!isFiniteScalar(distance) || distance <= getCameraMathEpsilon()) return false; outDistance = std::clamp(distance, minDistance, maxDistance); @@ -501,6 +613,13 @@ inline camera_vector_t getPitchYawFromForwardVector(const camera_vector_t< hlsl::atan2(forward.x, forward.z)); } +template +inline camera_vector_t getPitchYawFromOrientation(const camera_quaternion_t& orientation) +{ + const auto forward = normalizeQuaternion(orientation).transformVector(camera_vector_t(T(0), T(0), T(1)), true); + return getPitchYawFromForwardVector(forward); +} + template inline bool tryBuildPathPoseFromState( const camera_vector_t& targetPosition, @@ -545,6 +664,29 @@ inline camera_vector_t rotateVectorByQuaternion(const camera_quaternion_t< return normalizeQuaternion(orientation).transformVector(vectorToRotate, true); } +template +inline camera_vector_t projectWorldVectorToLocalBasis( + const camera_vector_t& worldVector, + const camera_vector_t& right, + const camera_vector_t& up, + const camera_vector_t& forward) +{ + return camera_vector_t( + dot(worldVector, right), + dot(worldVector, up), + dot(worldVector, forward)); +} + +template +inline camera_vector_t transformLocalVectorToWorldBasis( + const camera_vector_t& localVector, + const camera_vector_t& right, + const camera_vector_t& up, + const camera_vector_t& forward) +{ + return right * localVector.x + up * localVector.y + forward * localVector.z; +} + template inline camera_vector_t getQuaternionEulerRadians(const camera_quaternion_t& orientation) { @@ -584,7 +726,7 @@ inline T getQuaternionAngularDistanceRadians(const camera_quaternion_t& lhs, const auto lhsNormalized = normalizeQuaternion(lhs); const auto rhsNormalized = normalizeQuaternion(rhs); const T orientationDot = std::clamp( - static_cast(std::abs(dot(lhsNormalized.data, rhsNormalized.data))), + static_cast(hlsl::abs(dot(lhsNormalized.data, rhsNormalized.data))), T(0), T(1)); return T(2) * hlsl::acos(orientationDot); @@ -596,6 +738,29 @@ inline T getQuaternionAngularDistanceDegrees(const camera_quaternion_t& lhs, return degrees(getQuaternionAngularDistanceRadians(lhs, rhs)); } +template +inline bool tryComputePoseDelta( + const camera_vector_t& lhsPosition, + const camera_quaternion_t& lhsOrientation, + const camera_vector_t& rhsPosition, + const camera_quaternion_t& rhsOrientation, + SCameraPoseDelta& outDelta) +{ + outDelta = {}; + + const auto lhsNormalized = normalizeQuaternion(lhsOrientation); + const auto rhsNormalized = normalizeQuaternion(rhsOrientation); + if (!isFiniteVec3(lhsPosition) || !isFiniteVec3(rhsPosition) || + !isFiniteQuaternion(lhsNormalized) || !isFiniteQuaternion(rhsNormalized)) + { + return false; + } + + outDelta.position = length(lhsPosition - rhsPosition); + outDelta.rotationDeg = getQuaternionAngularDistanceDegrees(lhsNormalized, rhsNormalized); + return isFiniteScalar(outDelta.position) && isFiniteScalar(outDelta.rotationDeg); +} + template inline camera_quaternion_t slerpQuaternion(const camera_quaternion_t& lhs, const camera_quaternion_t& rhs, const T alpha) { @@ -608,6 +773,14 @@ inline camera_quaternion_t inverseQuaternion(const camera_quaternion_t& q) return inverse(q); } +template +inline camera_vector_t projectWorldVectorToLocalQuaternionFrame( + const camera_quaternion_t& orientation, + const camera_vector_t& worldVector) +{ + return rotateVectorByQuaternion(inverseQuaternion(orientation), worldVector); +} + template inline camera_matrix_t getQuaternionBasisMatrix(const camera_quaternion_t& orientation) { @@ -643,6 +816,27 @@ inline camera_vector_t getQuaternionEulerDegreesYXZ(const camera_quaternio degrees(eulerRadians.z)); } +template +inline camera_vector_t getCameraOrientationEulerRadians(const camera_quaternion_t& orientation) +{ + return getQuaternionEulerRadiansYXZ(orientation); +} + +template +inline camera_vector_t getCameraOrientationEulerDegrees(const camera_quaternion_t& orientation) +{ + return getQuaternionEulerDegreesYXZ(orientation); +} + +template +inline camera_vector_t getOrientationDeltaEulerRadiansYXZ( + const camera_quaternion_t& from, + const camera_quaternion_t& to) +{ + const auto deltaQuat = inverseQuaternion(from) * normalizeQuaternion(to); + return getQuaternionEulerRadiansYXZ(deltaQuat); +} + template inline camera_vector_t getWrappedEulerDistanceDegrees( const camera_vector_t& a, @@ -733,10 +927,8 @@ inline bool decomposeTransformMatrix( outTranslation = components.translation; outScale = components.scale; - outRotationEulerDegrees = getQuaternionEulerDegrees(components.orientation); - return std::isfinite(outRotationEulerDegrees.x) && - std::isfinite(outRotationEulerDegrees.y) && - std::isfinite(outRotationEulerDegrees.z); + outRotationEulerDegrees = getCameraOrientationEulerDegrees(components.orientation); + return isFiniteVec3(outRotationEulerDegrees); } } // namespace nbl::hlsl diff --git a/common/include/camera/CCameraPathUtilities.hpp b/common/include/camera/CCameraPathUtilities.hpp new file mode 100644 index 000000000..3349d2e01 --- /dev/null +++ b/common/include/camera/CCameraPathUtilities.hpp @@ -0,0 +1,381 @@ +#ifndef _C_CAMERA_PATH_UTILITIES_HPP_ +#define _C_CAMERA_PATH_UTILITIES_HPP_ + +#include +#include +#include + +#include "CCameraTargetRelativeUtilities.hpp" +#include "CCameraVirtualEventUtilities.hpp" +#include "ICamera.hpp" + +namespace nbl::core +{ + +//! Shared helpers for the target-relative cylindrical path rig exposed as `PathRig`. +struct SCameraPathPose final +{ + hlsl::float64_t3 position = hlsl::float64_t3(0.0); + hlsl::camera_quaternion_t orientation = hlsl::makeIdentityQuaternion(); + hlsl::float64_t appliedDistance = 0.0; + double orbitU = 0.0; + double orbitV = 0.0; +}; + +struct SCameraPathDelta final +{ + double radius = 0.0; + double height = 0.0; + double angle = 0.0; + + inline hlsl::float64_t3 asVector() const + { + return hlsl::float64_t3(radius, height, angle); + } + + inline hlsl::float64_t3 translationVector() const + { + return hlsl::float64_t3(radius, height, 0.0); + } + + static inline SCameraPathDelta fromVector(const hlsl::float64_t3& value) + { + return { + .radius = value.x, + .height = value.y, + .angle = value.z + }; + } +}; + +struct SCameraPathStateTransition final +{ + ICamera::PathState current = {}; + ICamera::PathState desired = {}; + SCameraPathDelta delta = {}; +}; + +struct SCameraCanonicalPathState final +{ + SCameraPathPose pose = {}; + SCameraTargetRelativeState targetRelative = {}; +}; + +struct SCameraPathComparisonThresholds final +{ + double angleToleranceDeg = ICamera::DefaultAngularToleranceDeg; + double scalarTolerance = ICamera::ScalarTolerance; +}; + +struct SCameraPathDistanceUpdateResult final +{ + bool exact = false; + hlsl::float64_t appliedDistance = 0.0; +}; + +struct SCameraPathDefaults final +{ + static constexpr double MinRadius = static_cast(ICamera::SphericalMinDistance); + static constexpr double ScalarTolerance = ICamera::ScalarTolerance; + static constexpr double ExactStateTolerance = ICamera::TinyScalarEpsilon; + static constexpr double ExactAngleToleranceDeg = ExactStateTolerance * 180.0 / hlsl::numbers::pi; + static constexpr double AngleToleranceDeg = ICamera::DefaultAngularToleranceDeg; + static inline constexpr std::string_view Identifier = "Target-relative Cylindrical Path Rig"; + static inline constexpr std::string_view Description = "Adjust a target-relative cylindrical path rig around a target"; + struct SLimits final + { + double minRadius = SCameraPathDefaults::MinRadius; + hlsl::float64_t minDistance = static_cast(ICamera::SphericalMinDistance); + hlsl::float64_t maxDistance = static_cast(ICamera::SphericalMaxDistance); + }; + + static inline constexpr SLimits Limits = {}; + static inline constexpr SCameraPathComparisonThresholds ComparisonThresholds = { + .angleToleranceDeg = AngleToleranceDeg, + .scalarTolerance = ScalarTolerance + }; + static inline constexpr SCameraPathComparisonThresholds ExactComparisonThresholds = { + .angleToleranceDeg = ExactAngleToleranceDeg, + .scalarTolerance = ExactStateTolerance + }; +}; + +using SCameraPathLimits = SCameraPathDefaults::SLimits; + +inline ICamera::PathState makeDefaultPathState(const double minRadius = SCameraPathDefaults::MinRadius) +{ + return { + .angle = 0.0, + .radius = minRadius, + .height = 0.0 + }; +} + +inline SCameraPathComparisonThresholds makePathComparisonThresholds( + const double angleToleranceDeg = SCameraPathDefaults::AngleToleranceDeg, + const double scalarTolerance = SCameraPathDefaults::ScalarTolerance) +{ + return { + .angleToleranceDeg = angleToleranceDeg, + .scalarTolerance = scalarTolerance + }; +} + +inline constexpr SCameraPathLimits makeDefaultPathLimits() +{ + return SCameraPathDefaults::Limits; +} + +inline bool isPathStateFinite(const ICamera::PathState& state) +{ + return hlsl::isFiniteScalar(state.angle) && + hlsl::isFiniteScalar(state.radius) && + hlsl::isFiniteScalar(state.height); +} + +inline bool sanitizePathState(ICamera::PathState& state, const double minRadius) +{ + return hlsl::sanitizePathState(state.angle, state.radius, state.height, minRadius); +} + +inline bool tryScalePathStateDistance( + const double desiredDistance, + const double minRadius, + ICamera::PathState& ioState, + double* outAppliedDistance = nullptr) +{ + return hlsl::tryScalePathStateDistance( + desiredDistance, + minRadius, + ioState.radius, + ioState.height, + outAppliedDistance); +} + +inline bool tryUpdatePathStateDistance( + const float desiredDistance, + const SCameraPathLimits& limits, + ICamera::PathState& ioState, + SCameraPathDistanceUpdateResult* outResult = nullptr) +{ + const auto clampedDistance = std::clamp(desiredDistance, limits.minDistance, limits.maxDistance); + double appliedDistance = 0.0; + if (!tryScalePathStateDistance(static_cast(clampedDistance), limits.minRadius, ioState, &appliedDistance)) + return false; + + if (outResult) + { + outResult->appliedDistance = appliedDistance; + outResult->exact = (clampedDistance == desiredDistance) && + hlsl::nearlyEqualScalar(appliedDistance, static_cast(desiredDistance), SCameraPathDefaults::ScalarTolerance); + } + return true; +} + +inline bool tryBuildPathStateFromPosition( + const hlsl::float64_t3& targetPosition, + const hlsl::float64_t3& position, + const double minRadius, + ICamera::PathState& outState) +{ + return hlsl::tryBuildPathStateFromPosition( + targetPosition, + position, + minRadius, + outState.angle, + outState.radius, + outState.height); +} + +inline bool tryResolvePathState( + const hlsl::float64_t3& targetPosition, + const hlsl::float64_t3& position, + const SCameraPathLimits& limits, + const ICamera::PathState* requestedState, + ICamera::PathState& outState) +{ + if (requestedState) + { + outState = *requestedState; + return sanitizePathState(outState, limits.minRadius); + } + + if (tryBuildPathStateFromPosition(targetPosition, position, limits.minRadius, outState)) + return true; + + outState = makeDefaultPathState(limits.minRadius); + return sanitizePathState(outState, limits.minRadius); +} + +inline bool tryBuildPathPoseFromState( + const hlsl::float64_t3& targetPosition, + const ICamera::PathState& state, + const SCameraPathLimits& limits, + SCameraPathPose& outPose) +{ + return hlsl::tryBuildPathPoseFromState( + targetPosition, + state.angle, + state.radius, + state.height, + limits.minRadius, + limits.minDistance, + limits.maxDistance, + outPose.position, + outPose.orientation, + &outPose.appliedDistance, + &outPose.orbitU, + &outPose.orbitV); +} + +inline bool tryBuildPathPoseFromState( + const hlsl::float64_t3& targetPosition, + const ICamera::PathState& state, + const SCameraPathLimits& limits, + hlsl::float64_t3& outPosition, + hlsl::camera_quaternion_t& outOrientation, + hlsl::float64_t* outAppliedDistance = nullptr, + double* outOrbitU = nullptr, + double* outOrbitV = nullptr) +{ + SCameraPathPose pathPose = {}; + if (!tryBuildPathPoseFromState(targetPosition, state, limits, pathPose)) + return false; + + outPosition = pathPose.position; + outOrientation = pathPose.orientation; + if (outAppliedDistance) + *outAppliedDistance = pathPose.appliedDistance; + if (outOrbitU) + *outOrbitU = pathPose.orbitU; + if (outOrbitV) + *outOrbitV = pathPose.orbitV; + return true; +} + +inline bool pathStatesNearlyEqual( + const ICamera::PathState& lhs, + const ICamera::PathState& rhs, + const SCameraPathComparisonThresholds& thresholds = {}) +{ + return hlsl::getWrappedAngleDistanceDegrees(lhs.angle, rhs.angle) <= thresholds.angleToleranceDeg && + hlsl::nearlyEqualScalar(lhs.radius, rhs.radius, thresholds.scalarTolerance) && + hlsl::nearlyEqualScalar(lhs.height, rhs.height, thresholds.scalarTolerance); +} + +inline bool pathStatesChanged( + const ICamera::PathState& lhs, + const ICamera::PathState& rhs, + const SCameraPathComparisonThresholds& thresholds = {}) +{ + return !pathStatesNearlyEqual(lhs, rhs, thresholds); +} + +inline hlsl::float64_t3 buildPathStateDeltaVector( + const ICamera::PathState& currentState, + const ICamera::PathState& desiredState) +{ + auto deltaVector = desiredState.asVector() - currentState.asVector(); + deltaVector.z = hlsl::wrapAngleRad(deltaVector.z); + return deltaVector; +} + +inline SCameraPathDelta buildPathStateDelta( + const ICamera::PathState& currentState, + const ICamera::PathState& desiredState) +{ + return SCameraPathDelta::fromVector(buildPathStateDeltaVector(currentState, desiredState)); +} + +inline SCameraPathDelta makePathDeltaFromVirtualPathTranslate(const hlsl::float64_t3& delta) +{ + return SCameraPathDelta::fromVector(delta); +} + +inline void appendPathAdvanceEvents( + std::vector& events, + const SCameraPathDelta& delta, + const double moveDenominator, + const double angleToleranceDeg = SCameraPathDefaults::AngleToleranceDeg, + const double scalarTolerance = SCameraPathDefaults::ScalarTolerance) +{ + appendLocalTranslationEvents( + events, + delta.translationVector(), + hlsl::float64_t3(moveDenominator), + hlsl::float64_t3(scalarTolerance)); + appendAngularDeltaEvent( + events, + delta.angle, + moveDenominator, + angleToleranceDeg, + CVirtualGimbalEvent::MoveForward, + CVirtualGimbalEvent::MoveBackward); +} + +inline bool tryBuildCanonicalPathState( + const hlsl::float64_t3& targetPosition, + const ICamera::PathState& state, + const SCameraPathLimits& limits, + SCameraCanonicalPathState& outState) +{ + outState = {}; + if (!tryBuildPathPoseFromState(targetPosition, state, limits, outState.pose)) + return false; + + outState.targetRelative = { + .target = targetPosition, + .orbitU = outState.pose.orbitU, + .orbitV = outState.pose.orbitV, + .distance = static_cast(outState.pose.appliedDistance) + }; + return true; +} + +inline bool tryApplyPathStateDelta( + const ICamera::PathState& currentState, + const SCameraPathDelta& delta, + const SCameraPathLimits& limits, + ICamera::PathState& outState) +{ + auto stateVector = currentState.asVector() + delta.asVector(); + stateVector.z = hlsl::wrapAngleRad(stateVector.z); + outState = ICamera::PathState::fromVector(stateVector); + return sanitizePathState(outState, limits.minRadius); +} + +inline ICamera::PathState blendPathStates( + const ICamera::PathState& from, + const ICamera::PathState& to, + const double alpha) +{ + const auto fromVector = from.asVector(); + const auto toVector = to.asVector(); + return { + .angle = hlsl::lerpWrappedAngleRad(fromVector.z, toVector.z, alpha), + .radius = fromVector.x + (toVector.x - fromVector.x) * alpha, + .height = fromVector.y + (toVector.y - fromVector.y) * alpha + }; +} + +inline bool tryBuildPathStateTransition( + const hlsl::float64_t3& targetPosition, + const hlsl::float64_t3& currentPosition, + const hlsl::float64_t3& desiredPosition, + const SCameraPathLimits& limits, + const ICamera::PathState* currentStateOverride, + const ICamera::PathState* desiredStateOverride, + SCameraPathStateTransition& outTransition) +{ + if (!tryResolvePathState(targetPosition, currentPosition, limits, currentStateOverride, outTransition.current)) + return false; + if (!tryResolvePathState(targetPosition, desiredPosition, limits, desiredStateOverride, outTransition.desired)) + return false; + + outTransition.delta = buildPathStateDelta(outTransition.current, outTransition.desired); + return true; +} + +} // namespace nbl::core + +#endif // _C_CAMERA_PATH_UTILITIES_HPP_ diff --git a/common/include/camera/CCameraPersistence.hpp b/common/include/camera/CCameraPersistence.hpp index 046476504..001635b5c 100644 --- a/common/include/camera/CCameraPersistence.hpp +++ b/common/include/camera/CCameraPersistence.hpp @@ -16,11 +16,17 @@ namespace nbl::system { +class ISystem; + +//! Serialize a preset collection to JSON. bool writePresetCollection(std::ostream& out, std::span presets, int indent = 2); +//! Parse a preset collection from JSON. bool readPresetCollection(std::istream& in, std::vector& presets); -bool savePresetCollectionToFile(const path& path, std::span presets, int indent = 2); -bool loadPresetCollectionFromFile(const path& path, std::vector& presets); +//! Save a preset collection to disk as JSON. +bool savePresetCollectionToFile(ISystem& system, const path& path, std::span presets, int indent = 2); +//! Load a preset collection from disk. +bool loadPresetCollectionFromFile(ISystem& system, const path& path, std::vector& presets); } // namespace nbl::system diff --git a/common/include/camera/CCameraPresetPersistence.hpp b/common/include/camera/CCameraPresetPersistence.hpp index 58ad19103..27e671c70 100644 --- a/common/include/camera/CCameraPresetPersistence.hpp +++ b/common/include/camera/CCameraPresetPersistence.hpp @@ -13,15 +13,17 @@ namespace nbl::system { +class ISystem; + //! Serialize one camera goal into an existing stream. bool writeGoal(std::ostream& out, const core::CCameraGoal& goal, int indent = 2); //! Deserialize one camera goal from an existing stream. bool readGoal(std::istream& in, core::CCameraGoal& goal); //! Save one camera goal to a file. -bool saveGoalToFile(const path& path, const core::CCameraGoal& goal, int indent = 2); +bool saveGoalToFile(ISystem& system, const path& path, const core::CCameraGoal& goal, int indent = 2); //! Load one camera goal from a file. -bool loadGoalFromFile(const path& path, core::CCameraGoal& goal); +bool loadGoalFromFile(ISystem& system, const path& path, core::CCameraGoal& goal); //! Serialize one camera preset into an existing stream. bool writePreset(std::ostream& out, const core::CCameraPreset& preset, int indent = 2); @@ -29,9 +31,9 @@ bool writePreset(std::ostream& out, const core::CCameraPreset& preset, int inden bool readPreset(std::istream& in, core::CCameraPreset& preset); //! Save one camera preset to a file. -bool savePresetToFile(const path& path, const core::CCameraPreset& preset, int indent = 2); +bool savePresetToFile(ISystem& system, const path& path, const core::CCameraPreset& preset, int indent = 2); //! Load one camera preset from a file. -bool loadPresetFromFile(const path& path, core::CCameraPreset& preset); +bool loadPresetFromFile(ISystem& system, const path& path, core::CCameraPreset& preset); } // namespace nbl::system diff --git a/common/include/camera/CCameraScriptVisualDebugOverlayUtilities.hpp b/common/include/camera/CCameraScriptVisualDebugOverlayUtilities.hpp index 950d8c386..c89326f22 100644 --- a/common/include/camera/CCameraScriptVisualDebugOverlayUtilities.hpp +++ b/common/include/camera/CCameraScriptVisualDebugOverlayUtilities.hpp @@ -6,8 +6,9 @@ #define _C_CAMERA_SCRIPT_VISUAL_DEBUG_OVERLAY_UTILITIES_HPP_ #include -#include +#include #include +#include #include #include @@ -33,6 +34,7 @@ struct SCameraScriptVisualDebugOverlayData final //! 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"; @@ -42,7 +44,7 @@ struct SCameraScriptVisualDebugStatus final bool hasHoldFrames = false; uint64_t progressFrames = 0u; uint64_t holdFrames = 0u; - float targetFps = 60.0f; + float targetFps = DefaultTargetFps; uint64_t absoluteFrame = 0u; std::string_view segmentLabel = {}; bool hasDynamicFov = false; @@ -135,64 +137,57 @@ inline void drawScriptVisualDebugOverlay( drawList->AddText(font, style.hintSize, ImVec2(hintX, hintY), style.hintColor, data.hintLine.c_str()); } -//! Build the display strings for one scripted visual debug HUD snapshot. -inline SCameraScriptVisualDebugOverlayData buildScriptVisualDebugOverlayData(const SCameraScriptVisualDebugStatus& status) +inline std::string formatFixedScalar(const float value, const int precision) { - 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); + std::ostringstream oss; + oss << std::fixed << std::setprecision(precision) << value; + return oss.str(); +} - char progressBuffer[256] = {}; +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::snprintf( - progressBuffer, - sizeof(progressBuffer), - "Planar %u Segment %.1f/%.1f s Frame %llu/%llu", - status.planarIndex, - elapsedSeconds, - holdSeconds, - static_cast(status.progressFrames), - static_cast(status.holdFrames)); - } - else - { - std::snprintf( - progressBuffer, - sizeof(progressBuffer), - "Planar %u Frame %llu", - status.planarIndex, - static_cast(status.absoluteFrame)); + + std::ostringstream oss; + oss << "Planar " << status.planarIndex + << " Segment " << std::fixed << std::setprecision(1) + << elapsedSeconds << "/" << holdSeconds + << " s Frame " << status.progressFrames << "/" << status.holdFrames; + return oss.str(); } - out.progressLine = progressBuffer; + + std::ostringstream oss; + oss << "Planar " << status.planarIndex << " Frame " << status.absoluteFrame; + return oss.str(); +} + +//! Build the display strings for one scripted visual debug HUD snapshot. +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) - { - char fovBuffer[96] = {}; - std::snprintf(fovBuffer, sizeof(fovBuffer), " | Dynamic FOV %.2f deg", status.dynamicFovDeg); - out.hintLine += fovBuffer; - } + out.hintLine += " | Dynamic FOV " + formatFixedScalar(status.dynamicFovDeg, 2) + " deg"; if (status.followActive) { out.hintLine += " | " + std::string(status.followModeDescription); if (status.followLockValid) { - char followBuffer[192] = {}; - std::snprintf( - followBuffer, - sizeof(followBuffer), - " | lock %.2f deg | target %.2f | center err %.3f", - status.followLockAngleDeg, - status.followTargetDistance, - status.followTargetCenterNdcRadius); - out.hintLine += followBuffer; + out.hintLine += + " | lock " + formatFixedScalar(status.followLockAngleDeg, 2) + + " deg | target " + formatFixedScalar(status.followTargetDistance, 2) + + " | center err " + formatFixedScalar(status.followTargetCenterNdcRadius, 3); } else { diff --git a/common/include/camera/CCameraScriptedCheckRunner.hpp b/common/include/camera/CCameraScriptedCheckRunner.hpp index 89248b008..33b172ea6 100644 --- a/common/include/camera/CCameraScriptedCheckRunner.hpp +++ b/common/include/camera/CCameraScriptedCheckRunner.hpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -51,7 +52,7 @@ struct CCameraScriptedCheckContext uint32_t imguizmoVirtualCount = 0u; const core::CTrackedTarget* trackedTarget = nullptr; const core::SCameraFollowConfig* followConfig = nullptr; - const hlsl::float32_t4x4* followViewProjMatrix = nullptr; + const SCameraProjectionContext* followProjectionContext = nullptr; const core::CCameraGoalSolver* goalSolver = nullptr; }; @@ -90,13 +91,19 @@ inline void scriptedCheckSetBaselineReference( scriptedCheckSetStepReference(state, position, orientation); } -inline float scriptedCheckComputeRotationDeltaDegrees( +inline bool scriptedCheckComputePoseDelta( + const hlsl::float64_t3& currentPosition, const hlsl::camera_quaternion_t& currentOrientation, - const hlsl::camera_quaternion_t& referenceOrientation) + const hlsl::float64_t3& referencePosition, + const hlsl::camera_quaternion_t& referenceOrientation, + hlsl::SCameraPoseDelta& outDelta) { - return static_cast(hlsl::getQuaternionAngularDistanceDegrees( - hlsl::normalizeQuaternion(currentOrientation), - hlsl::normalizeQuaternion(referenceOrientation))); + return hlsl::tryComputePoseDelta( + currentPosition, + currentOrientation, + referencePosition, + referenceOrientation, + outDelta); } template @@ -147,7 +154,7 @@ inline CCameraScriptedCheckFrameResult evaluateScriptedChecksForFrame( const auto& gimbal = context.camera->getGimbal(); const auto pos = gimbal.getPosition(); const auto orientation = hlsl::normalizeQuaternion(gimbal.getOrientation()); - const auto eulerDeg = hlsl::getCastedVector(hlsl::getQuaternionEulerDegrees(orientation)); + const auto eulerDeg = hlsl::getCastedVector(hlsl::getCameraOrientationEulerDegrees(orientation)); if (!hlsl::isFiniteVec3(pos) || !hlsl::isFiniteQuaternion(orientation) || !hlsl::isFiniteVec3(eulerDeg)) { @@ -202,7 +209,7 @@ inline CCameraScriptedCheckFrameResult evaluateScriptedChecksForFrame( } } - if (!found || std::abs(actual - expected.magnitude) > check.tolerance) + if (!found || hlsl::abs(actual - expected.magnitude) > check.tolerance) { ok = false; appendScriptedCheckLog( @@ -239,8 +246,7 @@ inline CCameraScriptedCheckFrameResult evaluateScriptedChecksForFrame( bool ok = true; if (check.hasExpectedPos) { - const hlsl::float64_t3 diff = pos - hlsl::getCastedVector(check.expectedPos); - const double distance = hlsl::length(diff); + const double distance = hlsl::length(pos - hlsl::getCastedVector(check.expectedPos)); if (distance > check.posTolerance) { ok = false; @@ -258,9 +264,12 @@ inline CCameraScriptedCheckFrameResult evaluateScriptedChecksForFrame( } if (check.hasExpectedEuler) { - const auto expectedOrientation = hlsl::makeQuaternionFromEulerDegrees( + const auto expectedOrientation = hlsl::makeQuaternionFromEulerDegreesYXZ( hlsl::getCastedVector(check.expectedEulerDeg)); - const auto rotationDeltaDeg = scriptedCheckComputeRotationDeltaDegrees(orientation, expectedOrientation); + hlsl::SCameraPoseDelta poseDelta = {}; + if (!scriptedCheckComputePoseDelta(pos, orientation, pos, expectedOrientation, poseDelta)) + poseDelta.rotationDeg = std::numeric_limits::infinity(); + const auto rotationDeltaDeg = poseDelta.rotationDeg; if (rotationDeltaDeg > check.eulerToleranceDeg) { ok = false; @@ -303,11 +312,20 @@ inline CCameraScriptedCheckFrameResult evaluateScriptedChecksForFrame( break; } - const hlsl::float64_t3 diff = pos - state.baseline.position; - const double dpos = hlsl::length(diff); - const auto rotationDeltaDeg = scriptedCheckComputeRotationDeltaDegrees(orientation, state.baseline.orientation); + hlsl::SCameraPoseDelta poseDelta = {}; + if (!scriptedCheckComputePoseDelta(pos, orientation, state.baseline.position, state.baseline.orientation, poseDelta)) + { + appendScriptedCheckLog( + result, + true, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << "[script][fail] gimbal_delta frame=" << context.frame << " non-finite pose delta"; + })); + break; + } - if (dpos > check.posTolerance || rotationDeltaDeg > check.eulerToleranceDeg) + if (poseDelta.position > check.posTolerance || poseDelta.rotationDeg > check.eulerToleranceDeg) { appendScriptedCheckLog( result, @@ -316,9 +334,9 @@ inline CCameraScriptedCheckFrameResult evaluateScriptedChecksForFrame( { oss << std::fixed << std::setprecision(6); oss << "[script][fail] gimbal_delta frame=" << context.frame - << " pos_diff=" << dpos + << " pos_diff=" << poseDelta.position << " tol=" << check.posTolerance - << " rot_delta_deg=" << rotationDeltaDeg + << " rot_delta_deg=" << poseDelta.rotationDeg << " tol=" << check.eulerToleranceDeg; })); } @@ -331,8 +349,8 @@ inline CCameraScriptedCheckFrameResult evaluateScriptedChecksForFrame( { oss << std::fixed << std::setprecision(6); oss << "[script][pass] gimbal_delta frame=" << context.frame - << " pos_diff=" << dpos - << " rot_delta_deg=" << rotationDeltaDeg; + << " pos_diff=" << poseDelta.position + << " rot_delta_deg=" << poseDelta.rotationDeg; })); } break; @@ -360,16 +378,26 @@ inline CCameraScriptedCheckFrameResult evaluateScriptedChecksForFrame( } } - const hlsl::float64_t3 diff = pos - state.step.position; - const double dpos = hlsl::length(diff); - const auto rotationDeltaDeg = scriptedCheckComputeRotationDeltaDegrees(orientation, state.step.orientation); + hlsl::SCameraPoseDelta poseDelta = {}; + if (!scriptedCheckComputePoseDelta(pos, orientation, state.step.position, state.step.orientation, poseDelta)) + { + appendScriptedCheckLog( + result, + true, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << "[script][fail] gimbal_step frame=" << context.frame << " non-finite pose delta"; + })); + scriptedCheckSetStepReference(state, pos, orientation); + break; + } bool ok = true; bool requiresProgress = false; bool hasProgress = false; if (check.hasPosDeltaConstraint) { - if (dpos > check.posTolerance) + if (poseDelta.position > check.posTolerance) { ok = false; appendScriptedCheckLog( @@ -377,21 +405,21 @@ inline CCameraScriptedCheckFrameResult evaluateScriptedChecksForFrame( true, buildScriptedCheckMessage([&](std::ostringstream& oss) { - oss << std::fixed << std::setprecision(6); - oss << "[script][fail] gimbal_step frame=" << context.frame - << " pos_delta=" << dpos + oss << std::fixed << std::setprecision(6); + oss << "[script][fail] gimbal_step frame=" << context.frame + << " pos_delta=" << poseDelta.position << " max=" << check.posTolerance; })); } if (check.minPosDelta > 0.0f) { requiresProgress = true; - hasProgress = hasProgress || dpos >= check.minPosDelta; + hasProgress = hasProgress || poseDelta.position >= check.minPosDelta; } } if (check.hasEulerDeltaConstraint) { - if (rotationDeltaDeg > check.eulerToleranceDeg) + if (poseDelta.rotationDeg > check.eulerToleranceDeg) { ok = false; appendScriptedCheckLog( @@ -399,16 +427,16 @@ inline CCameraScriptedCheckFrameResult evaluateScriptedChecksForFrame( true, buildScriptedCheckMessage([&](std::ostringstream& oss) { - oss << std::fixed << std::setprecision(6); - oss << "[script][fail] gimbal_step frame=" << context.frame - << " rot_delta_deg=" << rotationDeltaDeg + oss << std::fixed << std::setprecision(6); + oss << "[script][fail] gimbal_step frame=" << context.frame + << " rot_delta_deg=" << poseDelta.rotationDeg << " max=" << check.eulerToleranceDeg; })); } if (check.minEulerDeltaDeg > 0.0f) { requiresProgress = true; - hasProgress = hasProgress || rotationDeltaDeg >= check.minEulerDeltaDeg; + hasProgress = hasProgress || poseDelta.rotationDeg >= check.minEulerDeltaDeg; } } if (requiresProgress && !hasProgress) @@ -421,8 +449,8 @@ inline CCameraScriptedCheckFrameResult evaluateScriptedChecksForFrame( { oss << std::fixed << std::setprecision(6); oss << "[script][fail] gimbal_step frame=" << context.frame - << " missing progress pos_delta=" << dpos - << " rot_delta_deg=" << rotationDeltaDeg; + << " missing progress pos_delta=" << poseDelta.position + << " rot_delta_deg=" << poseDelta.rotationDeg; })); } @@ -435,8 +463,8 @@ inline CCameraScriptedCheckFrameResult evaluateScriptedChecksForFrame( { oss << std::fixed << std::setprecision(6); oss << "[script][pass] gimbal_step frame=" << context.frame - << " pos_delta=" << dpos - << " rot_delta_deg=" << rotationDeltaDeg; + << " pos_delta=" << poseDelta.position + << " rot_delta_deg=" << poseDelta.rotationDeg; })); } scriptedCheckSetStepReference(state, pos, orientation); @@ -481,9 +509,7 @@ inline CCameraScriptedCheckFrameResult evaluateScriptedChecksForFrame( SCameraFollowRegressionResult regression = {}; std::string regressionError; core::CCameraGoal expectedFollowGoal = {}; - SCameraFollowRegressionThresholds thresholds = {}; - thresholds.lockAngleToleranceDeg = check.eulerToleranceDeg; - thresholds.projectedNdcTolerance = check.posTolerance; + const auto thresholds = makeFollowRegressionThresholds(check.posTolerance, check.eulerToleranceDeg); const bool ok = core::tryBuildFollowGoal( *context.goalSolver, context.camera, @@ -497,7 +523,7 @@ inline CCameraScriptedCheckFrameResult evaluateScriptedChecksForFrame( expectedFollowGoal, regression, ®ressionError, - context.followViewProjMatrix, + context.followProjectionContext, thresholds); if (!ok) @@ -522,7 +548,7 @@ inline CCameraScriptedCheckFrameResult evaluateScriptedChecksForFrame( oss << "[script][pass] follow_lock frame=" << context.frame << " angle_deg=" << regression.lockAngleDeg << " target_distance=" << regression.targetDistance - << " screen_ndc=" << regression.projectedNdcRadius; + << " screen_ndc=" << regression.projectedTarget.radius; })); } break; diff --git a/common/include/camera/CCameraScriptedRuntime.hpp b/common/include/camera/CCameraScriptedRuntime.hpp index db6c0c5bc..a0f83ef5d 100644 --- a/common/include/camera/CCameraScriptedRuntime.hpp +++ b/common/include/camera/CCameraScriptedRuntime.hpp @@ -123,6 +123,14 @@ struct CCameraScriptedInputEvent SegmentLabelData segmentLabel; }; +struct CCameraScriptedCheckDefaults final +{ + static constexpr float VirtualEventTolerance = 1e-3f; + static constexpr float PositionTolerance = static_cast(core::ICamera::DefaultPositionTolerance); + static constexpr float EulerToleranceDeg = static_cast(core::ICamera::DefaultAngularToleranceDeg); + static constexpr float FollowScreenToleranceNdc = SCameraFollowRegressionThresholds::DefaultProjectedNdcTolerance; +}; + struct CCameraScriptedInputCheck { enum class Kind : uint8_t @@ -143,15 +151,15 @@ struct CCameraScriptedInputCheck uint64_t frame = 0; Kind kind = Kind::Baseline; - float tolerance = 1e-3f; + float tolerance = CCameraScriptedCheckDefaults::VirtualEventTolerance; std::vector expectedVirtualEvents; hlsl::float32_t3 expectedPos = hlsl::float32_t3(0.f); hlsl::float32_t3 expectedEulerDeg = hlsl::float32_t3(0.f); bool hasExpectedPos = false; bool hasExpectedEuler = false; - float posTolerance = 0.05f; - float eulerToleranceDeg = 1.0f; + float posTolerance = CCameraScriptedCheckDefaults::PositionTolerance; + float eulerToleranceDeg = CCameraScriptedCheckDefaults::EulerToleranceDeg; float minPosDelta = 0.0f; float minEulerDeltaDeg = 0.0f; bool hasPosDeltaConstraint = false; @@ -293,8 +301,8 @@ inline void appendScriptedGimbalStepCheck( inline void appendScriptedFollowTargetLockCheck( CCameraScriptedTimeline& timeline, const uint64_t frame, - const float toleranceDeg = static_cast(core::ICamera::DefaultAngularToleranceDeg), - const float screenToleranceNdc = SCameraFollowRegressionThresholds::DefaultProjectedNdcTolerance) + const float toleranceDeg = CCameraScriptedCheckDefaults::EulerToleranceDeg, + const float screenToleranceNdc = CCameraScriptedCheckDefaults::FollowScreenToleranceNdc) { CCameraScriptedInputCheck entry; entry.frame = frame; diff --git a/common/include/camera/CCameraScriptedRuntimePersistence.hpp b/common/include/camera/CCameraScriptedRuntimePersistence.hpp index 094d35966..71f636c22 100644 --- a/common/include/camera/CCameraScriptedRuntimePersistence.hpp +++ b/common/include/camera/CCameraScriptedRuntimePersistence.hpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include "CCameraScriptedRuntime.hpp" @@ -16,6 +17,8 @@ namespace nbl::system { +class ISystem; + //! Optional scripted control overrides parsed alongside one runtime payload. struct CCameraScriptedControlOverrides { @@ -58,8 +61,10 @@ inline void appendScriptedInputParseWarning(CCameraScriptedInputParseResult& out //! Parse one low-level scripted runtime payload from an existing stream. bool readCameraScriptedInput(std::istream& in, CCameraScriptedInputParseResult& out, std::string* error = nullptr); +//! Parse one low-level scripted runtime payload directly from text. +bool readCameraScriptedInput(std::string_view text, CCameraScriptedInputParseResult& out, std::string* error = nullptr); //! Load one low-level scripted runtime payload from a file. -bool loadCameraScriptedInputFromFile(const path& path, CCameraScriptedInputParseResult& out, std::string* error = nullptr); +bool loadCameraScriptedInputFromFile(ISystem& system, const path& path, CCameraScriptedInputParseResult& out, std::string* error = nullptr); } // namespace nbl::system diff --git a/common/include/camera/CCameraScriptedUiInputUtilities.hpp b/common/include/camera/CCameraScriptedUiInputUtilities.hpp new file mode 100644 index 000000000..a447f701e --- /dev/null +++ b/common/include/camera/CCameraScriptedUiInputUtilities.hpp @@ -0,0 +1,87 @@ +#ifndef _C_CAMERA_SCRIPTED_UI_INPUT_UTILITIES_HPP_ +#define _C_CAMERA_SCRIPTED_UI_INPUT_UTILITIES_HPP_ + +#include +#include + +#include "CCameraScriptedRuntime.hpp" +#include "nbl/ui/SInputEvent.h" + +namespace nbl::ui +{ + +inline SKeyboardEvent makeScriptedKeyboardEvent( + const std::chrono::microseconds timestamp, + IWindow* const window, + const system::CCameraScriptedInputEvent::KeyboardData& authoredKeyboard) +{ + SKeyboardEvent event(timestamp); + event.keyCode = authoredKeyboard.key; + event.action = + authoredKeyboard.action == system::CCameraScriptedInputEvent::KeyboardData::Action::Pressed ? + SKeyboardEvent::ECA_PRESSED : + SKeyboardEvent::ECA_RELEASED; + event.window = window; + return event; +} + +inline bool tryBuildScriptedMouseEvent( + const std::chrono::microseconds timestamp, + IWindow* const window, + const system::CCameraScriptedInputEvent::MouseData& authoredMouse, + SMouseEvent& outEvent) +{ + outEvent = SMouseEvent(timestamp); + outEvent.window = window; + + switch (authoredMouse.type) + { + case system::CCameraScriptedInputEvent::MouseData::Type::Click: + outEvent.type = SMouseEvent::EET_CLICK; + outEvent.clickEvent.mouseButton = authoredMouse.button; + outEvent.clickEvent.action = + authoredMouse.action == system::CCameraScriptedInputEvent::MouseData::ClickAction::Pressed ? + SMouseEvent::SClickEvent::EA_PRESSED : + SMouseEvent::SClickEvent::EA_RELEASED; + outEvent.clickEvent.clickPosX = authoredMouse.x; + outEvent.clickEvent.clickPosY = authoredMouse.y; + return true; + case system::CCameraScriptedInputEvent::MouseData::Type::Scroll: + outEvent.type = SMouseEvent::EET_SCROLL; + outEvent.scrollEvent.verticalScroll = authoredMouse.v; + outEvent.scrollEvent.horizontalScroll = authoredMouse.h; + return true; + case system::CCameraScriptedInputEvent::MouseData::Type::Movement: + outEvent.type = SMouseEvent::EET_MOVEMENT; + outEvent.movementEvent.relativeMovementX = authoredMouse.dx; + outEvent.movementEvent.relativeMovementY = authoredMouse.dy; + return true; + default: + return false; + } +} + +inline void appendScriptedUiInputEvents( + const std::chrono::microseconds timestamp, + IWindow* const window, + const std::vector& authoredKeyboard, + const std::vector& authoredMouse, + std::vector& outKeyboard, + std::vector& outMouse) +{ + outKeyboard.reserve(outKeyboard.size() + authoredKeyboard.size()); + for (const auto& keyboardEvent : authoredKeyboard) + outKeyboard.emplace_back(makeScriptedKeyboardEvent(timestamp, window, keyboardEvent)); + + outMouse.reserve(outMouse.size() + authoredMouse.size()); + for (const auto& mouseEvent : authoredMouse) + { + SMouseEvent builtEvent(timestamp); + if (tryBuildScriptedMouseEvent(timestamp, window, mouseEvent, builtEvent)) + outMouse.emplace_back(builtEvent); + } +} + +} // namespace nbl::ui + +#endif // _C_CAMERA_SCRIPTED_UI_INPUT_UTILITIES_HPP_ diff --git a/common/include/camera/CCameraSequenceScript.hpp b/common/include/camera/CCameraSequenceScript.hpp index 145272db6..18e231c4d 100644 --- a/common/include/camera/CCameraSequenceScript.hpp +++ b/common/include/camera/CCameraSequenceScript.hpp @@ -14,6 +14,7 @@ #include "CCameraMathUtilities.hpp" #include "CCameraKeyframeTrack.hpp" +#include "CCameraTargetRelativeUtilities.hpp" #include "IPlanarProjection.hpp" namespace nbl::core @@ -263,7 +264,7 @@ inline bool tryParseCameraKind(std::string_view value, ICamera::CameraKind& outK outKind = ICamera::CameraKind::Dolly; else if (value == "DollyZoom" || value == "Dolly Zoom") outKind = ICamera::CameraKind::DollyZoom; - else if (value == "Path") + else if (value == "PathRig" || value == "Path Rig") outKind = ICamera::CameraKind::Path; else return false; @@ -290,38 +291,10 @@ inline void normalizeCaptureFractions(std::vector& fractions) std::sort(fractions.begin(), fractions.end()); fractions.erase(std::unique(fractions.begin(), fractions.end(), - [](const float lhs, const float rhs) { return std::abs(lhs - rhs) <= static_cast(ICamera::ScalarTolerance); }), + [](const float lhs, const float rhs) { return hlsl::nearlyEqualScalar(lhs, rhs, static_cast(ICamera::ScalarTolerance)); }), fractions.end()); } -inline bool applyCanonicalSphericalGoal(CCameraGoal& goal) -{ - if (!(goal.hasTargetPosition && goal.hasOrbitState)) - return false; - if (!std::isfinite(goal.orbitU) || !std::isfinite(goal.orbitV) || !std::isfinite(goal.orbitDistance)) - return false; - - hlsl::float64_t appliedDistance = 0.0; - if (!hlsl::tryBuildSphericalPoseFromOrbit( - goal.targetPosition, - goal.orbitU, - goal.orbitV, - static_cast(goal.orbitDistance), - static_cast(ICamera::SphericalMinDistance), - static_cast(ICamera::SphericalMaxDistance), - goal.position, - goal.orientation, - &appliedDistance)) - { - return false; - } - - goal.hasDistance = true; - goal.distance = static_cast(appliedDistance); - goal.orbitDistance = static_cast(appliedDistance); - return true; -} - inline bool buildSequenceKeyframePreset(const CCameraPreset& reference, const CCameraSequenceKeyframe& authored, CCameraPreset& outPreset, std::string* error = nullptr) { if (authored.hasAbsolutePreset) @@ -357,7 +330,7 @@ inline bool buildSequenceKeyframePreset(const CCameraPreset& reference, const CC if (delta.hasRotationEulerDegOffset) { - goal.orientation = hlsl::normalizeQuaternion(goal.orientation * hlsl::makeQuaternionFromEulerDegrees(hlsl::getCastedVector(delta.rotationEulerDegOffset))); + goal.orientation = hlsl::normalizeQuaternion(goal.orientation * hlsl::makeQuaternionFromEulerDegreesYXZ(hlsl::getCastedVector(delta.rotationEulerDegOffset))); } if (delta.hasTargetOffset) @@ -383,8 +356,10 @@ inline bool buildSequenceKeyframePreset(const CCameraPreset& reference, const CC goal.orbitU = hlsl::wrapAngleRad(goal.orbitU + hlsl::radians(delta.orbitUDeltaDeg)); if (delta.hasOrbitVDeltaDeg) { - constexpr double OrbitPitchLimit = hlsl::numbers::pi * (89.0 / 180.0); - goal.orbitV = std::clamp(goal.orbitV + hlsl::radians(delta.orbitVDeltaDeg), -OrbitPitchLimit, OrbitPitchLimit); + goal.orbitV = std::clamp( + goal.orbitV + hlsl::radians(delta.orbitVDeltaDeg), + -SCameraTargetRelativeRigDefaults::ArcballPitchLimitRad, + SCameraTargetRelativeRigDefaults::ArcballPitchLimitRad); } if (delta.hasOrbitDistanceDelta) goal.orbitDistance += delta.orbitDistanceDelta; @@ -398,12 +373,17 @@ inline bool buildSequenceKeyframePreset(const CCameraPreset& reference, const CC *error = "Sequence keyframe path deltas require path state."; return false; } - if (delta.hasPathAngleDeltaDeg) - goal.pathState.angle = hlsl::wrapAngleRad(goal.pathState.angle + hlsl::radians(delta.pathAngleDeltaDeg)); - if (delta.hasPathRadiusDelta) - goal.pathState.radius += delta.pathRadiusDelta; - if (delta.hasPathHeightDelta) - goal.pathState.height += delta.pathHeightDelta; + + const auto pathDelta = SCameraPathDelta::fromVector(hlsl::float64_t3( + delta.hasPathRadiusDelta ? static_cast(delta.pathRadiusDelta) : hlsl::float64_t(0.0), + delta.hasPathHeightDelta ? static_cast(delta.pathHeightDelta) : hlsl::float64_t(0.0), + delta.hasPathAngleDeltaDeg ? static_cast(hlsl::radians(delta.pathAngleDeltaDeg)) : hlsl::float64_t(0.0))); + if (!tryApplyPathStateDelta(goal.pathState, pathDelta, makeDefaultPathLimits(), goal.pathState)) + { + if (error) + *error = "Sequence keyframe path deltas produced an invalid path state."; + return false; + } } if (delta.hasDynamicBaseFovDelta || delta.hasDynamicReferenceDistanceDelta) @@ -420,21 +400,14 @@ inline bool buildSequenceKeyframePreset(const CCameraPreset& reference, const CC goal.dynamicPerspectiveState.referenceDistance = std::max(0.001f, goal.dynamicPerspectiveState.referenceDistance + delta.dynamicReferenceDistanceDelta); } - if (hasPathDelta) - { - if (!applyCanonicalPathGoal(goal)) - { - if (error) - *error = "Sequence keyframe failed to canonicalize path state."; - return false; - } - } - else if (hasSphericalDelta) + if (hasPathDelta || hasSphericalDelta) { - if (!applyCanonicalSphericalGoal(goal)) + if (!applyCanonicalGoalState(goal)) { if (error) - *error = "Sequence keyframe failed to canonicalize spherical state."; + *error = hasPathDelta ? + "Sequence keyframe failed to canonicalize path state." : + "Sequence keyframe failed to canonicalize spherical state."; return false; } } @@ -472,10 +445,7 @@ inline bool buildSequenceTrackFromReference(const CCameraPreset& reference, cons inline bool isSequenceTrackedTargetPoseFinite(const CCameraSequenceTrackedTargetPose& pose) { return hlsl::isFiniteVec3(pose.position) && - std::isfinite(pose.orientation.data.x) && - std::isfinite(pose.orientation.data.y) && - std::isfinite(pose.orientation.data.z) && - std::isfinite(pose.orientation.data.w); + hlsl::isFiniteQuaternion(pose.orientation); } inline bool buildSequenceTrackedTargetPoseFromReference( @@ -489,14 +459,14 @@ inline bool buildSequenceTrackedTargetPoseFromReference( if (authored.hasAbsolutePosition) outPose.position = authored.absolutePosition; if (authored.hasAbsoluteRotationEulerDeg) - outPose.orientation = hlsl::makeQuaternionFromEulerDegrees(hlsl::getCastedVector(authored.absoluteRotationEulerDeg)); + outPose.orientation = hlsl::makeQuaternionFromEulerDegreesYXZ(hlsl::getCastedVector(authored.absoluteRotationEulerDeg)); if (authored.hasDelta) { if (authored.delta.hasPositionOffset) outPose.position += authored.delta.positionOffset; if (authored.delta.hasRotationEulerDegOffset) - outPose.orientation = hlsl::normalizeQuaternion(outPose.orientation * hlsl::makeQuaternionFromEulerDegrees(hlsl::getCastedVector(authored.delta.rotationEulerDegOffset))); + outPose.orientation = hlsl::normalizeQuaternion(outPose.orientation * hlsl::makeQuaternionFromEulerDegreesYXZ(hlsl::getCastedVector(authored.delta.rotationEulerDegOffset))); } if (!isSequenceTrackedTargetPoseFinite(outPose)) @@ -539,7 +509,7 @@ inline bool buildSequenceTrackedTargetTrackFromReference( normalized.reserve(outTrack.keyframes.size()); for (const auto& keyframe : outTrack.keyframes) { - if (!normalized.empty() && std::abs(normalized.back().time - keyframe.time) <= static_cast(ICamera::ScalarTolerance)) + if (!normalized.empty() && hlsl::nearlyEqualScalar(normalized.back().time, keyframe.time, static_cast(ICamera::ScalarTolerance))) normalized.back() = keyframe; else normalized.emplace_back(keyframe); diff --git a/common/include/camera/CCameraSequenceScriptPersistence.hpp b/common/include/camera/CCameraSequenceScriptPersistence.hpp index 7b76807cc..8a6a6a4e1 100644 --- a/common/include/camera/CCameraSequenceScriptPersistence.hpp +++ b/common/include/camera/CCameraSequenceScriptPersistence.hpp @@ -7,6 +7,7 @@ #include #include +#include #include "CCameraSequenceScript.hpp" #include "nbl/system/path.h" @@ -14,10 +15,14 @@ namespace nbl::system { +class ISystem; + //! Parse one compact camera-sequence script from an existing stream. bool readCameraSequenceScript(std::istream& in, core::CCameraSequenceScript& out, std::string* error = nullptr); +//! Parse one compact camera-sequence script directly from text. +bool readCameraSequenceScript(std::string_view text, core::CCameraSequenceScript& out, std::string* error = nullptr); //! Load one compact camera-sequence script from a file. -bool loadCameraSequenceScriptFromFile(const path& path, core::CCameraSequenceScript& out, std::string* error = nullptr); +bool loadCameraSequenceScriptFromFile(ISystem& system, const path& path, core::CCameraSequenceScript& out, std::string* error = nullptr); } // namespace nbl::system diff --git a/common/include/camera/CCameraSmokeRegressionUtilities.hpp b/common/include/camera/CCameraSmokeRegressionUtilities.hpp index 5ec9a7a14..524ee0029 100644 --- a/common/include/camera/CCameraSmokeRegressionUtilities.hpp +++ b/common/include/camera/CCameraSmokeRegressionUtilities.hpp @@ -15,12 +15,7 @@ namespace nbl::system { -//! Small shared bundle for smoke/regression pose deltas measured around one camera manipulation. -struct SCameraManipulationDelta -{ - double position = 0.0; - double rotationDeg = 0.0; -}; +using SCameraManipulationDelta = hlsl::SCameraPoseDelta; struct SCameraSmokeComparisonThresholds final { @@ -48,12 +43,7 @@ inline bool tryComputeCameraManipulationDelta( const auto& gimbal = camera->getGimbal(); const auto afterPosition = gimbal.getPosition(); const auto afterOrientation = hlsl::normalizeQuaternion(gimbal.getOrientation()); - if (!hlsl::isFiniteVec3(afterPosition) || !hlsl::isFiniteQuaternion(beforeOrientation) || !hlsl::isFiniteQuaternion(afterOrientation)) - return false; - - outDelta.position = hlsl::length(afterPosition - beforePosition); - outDelta.rotationDeg = hlsl::getQuaternionAngularDistanceDegrees(beforeOrientation, afterOrientation); - return true; + return hlsl::tryComputePoseDelta(afterPosition, afterOrientation, beforePosition, beforeOrientation, outDelta); } //! Manipulate a camera and report how far its pose moved in position and Euler-angle terms. diff --git a/common/include/camera/CCameraTargetRelativeUtilities.hpp b/common/include/camera/CCameraTargetRelativeUtilities.hpp new file mode 100644 index 000000000..b94ca5381 --- /dev/null +++ b/common/include/camera/CCameraTargetRelativeUtilities.hpp @@ -0,0 +1,268 @@ +#ifndef _C_CAMERA_TARGET_RELATIVE_UTILITIES_HPP_ +#define _C_CAMERA_TARGET_RELATIVE_UTILITIES_HPP_ + +#include + +#include "CCameraVirtualEventUtilities.hpp" + +namespace nbl::core +{ + +//! Canonical target-relative orbit state shared by spherical cameras, follow, and goal solving. +struct SCameraTargetRelativeState final +{ + hlsl::float64_t3 target = hlsl::float64_t3(0.0); + double orbitU = 0.0; + double orbitV = 0.0; + float distance = ICamera::SphericalMinDistance; +}; + +//! Pose reconstructed from a target-relative orbit state. +struct SCameraTargetRelativePose final +{ + hlsl::float64_t3 position = hlsl::float64_t3(0.0); + hlsl::camera_quaternion_t orientation = hlsl::makeIdentityQuaternion(); + hlsl::float64_t appliedDistance = static_cast(ICamera::SphericalMinDistance); +}; + +//! Derived basis for target-relative orbit rigs. +struct SCameraTargetRelativeBasis final +{ + hlsl::float64_t3 localOffset = hlsl::float64_t3(0.0); + hlsl::float64_t3 right = hlsl::float64_t3(1.0, 0.0, 0.0); + hlsl::float64_t3 up = hlsl::float64_t3(0.0, 0.0, 1.0); + hlsl::float64_t3 forward = hlsl::float64_t3(0.0, 1.0, 0.0); +}; + +//! Delta between current spherical target state and canonical target-relative goal. +struct SCameraTargetRelativeDelta final +{ + double orbitU = 0.0; + double orbitV = 0.0; + double distance = 0.0; + + inline hlsl::float64_t3 orbitVector() const + { + return hlsl::float64_t3(orbitV, orbitU, 0.0); + } +}; + +struct SCameraTargetRelativeEventPolicy final +{ + bool translateOrbit = false; + bool allowYaw = true; + bool allowPitch = true; + SCameraVirtualEventAxisBinding distanceBinding = { + CVirtualGimbalEvent::MoveForward, + CVirtualGimbalEvent::MoveBackward + }; +}; + +//! Shared authored/default constants for target-relative rigs. +struct SCameraTargetRelativeRigDefaults final +{ + static constexpr float InitialDistance = 1.0f; + static constexpr double ArcballPitchLimitRad = hlsl::SCameraViewRigDefaults::ArcballPitchLimitRad; + static constexpr double TurntablePitchLimitRad = hlsl::SCameraViewRigDefaults::TurntablePitchLimitRad; + static constexpr double ChaseMaxPitchRad = hlsl::SCameraViewRigDefaults::ChaseMaxPitchRad; + static constexpr double ChaseMinPitchRad = hlsl::SCameraViewRigDefaults::ChaseMinPitchRad; + static constexpr double DollyPitchLimitRad = hlsl::SCameraViewRigDefaults::DollyPitchLimitRad; + static constexpr double TopDownPitchRad = hlsl::SCameraViewRigDefaults::TopDownPitchRad; + static constexpr double IsometricYawRad = hlsl::SCameraViewRigDefaults::IsometricYawRad; + static constexpr double IsometricPitchRad = hlsl::SCameraViewRigDefaults::IsometricPitchRad; + + static inline constexpr SCameraTargetRelativeEventPolicy OrbitTranslatePolicy = { + .translateOrbit = true + }; + static inline constexpr SCameraTargetRelativeEventPolicy RotateDistancePolicy = { + .translateOrbit = false, + .allowYaw = true, + .allowPitch = true + }; + static inline constexpr SCameraTargetRelativeEventPolicy TopDownPolicy = { + .translateOrbit = false, + .allowYaw = true, + .allowPitch = false + }; + static inline constexpr SCameraTargetRelativeEventPolicy IsometricPolicy = { + .translateOrbit = false, + .allowYaw = false, + .allowPitch = false + }; + static inline constexpr SCameraTargetRelativeEventPolicy DollyPolicy = { + .translateOrbit = false, + .allowYaw = true, + .allowPitch = true, + .distanceBinding = { + CVirtualGimbalEvent::None, + CVirtualGimbalEvent::None + } + }; + static inline constexpr SCameraTargetRelativeEventPolicy ChasePolicy = { + .translateOrbit = false, + .allowYaw = true, + .allowPitch = true, + .distanceBinding = { + CVirtualGimbalEvent::MoveUp, + CVirtualGimbalEvent::MoveDown + } + }; +}; + +inline bool tryBuildTargetRelativeStateFromPosition( + const hlsl::float64_t3& targetPosition, + const hlsl::float64_t3& position, + const float minDistance, + const float maxDistance, + SCameraTargetRelativeState& outState) +{ + outState = {}; + outState.target = targetPosition; + + hlsl::float64_t appliedDistance = static_cast(minDistance); + if (!hlsl::tryBuildOrbitFromPosition( + targetPosition, + position, + static_cast(minDistance), + static_cast(maxDistance), + outState.orbitU, + outState.orbitV, + appliedDistance)) + { + return false; + } + + outState.distance = static_cast(appliedDistance); + return true; +} + +inline bool tryBuildTargetRelativePoseFromState( + const SCameraTargetRelativeState& state, + const float minDistance, + const float maxDistance, + SCameraTargetRelativePose& outPose) +{ + outPose = {}; + return hlsl::tryBuildSphericalPoseFromOrbit( + state.target, + state.orbitU, + state.orbitV, + static_cast(state.distance), + static_cast(minDistance), + static_cast(maxDistance), + outPose.position, + outPose.orientation, + &outPose.appliedDistance); +} + +inline bool tryBuildTargetRelativeBasis( + const SCameraTargetRelativeState& state, + const float minDistance, + const float maxDistance, + SCameraTargetRelativeBasis& outBasis) +{ + SCameraTargetRelativePose pose = {}; + if (!tryBuildTargetRelativePoseFromState(state, minDistance, maxDistance, pose)) + return false; + + outBasis.localOffset = pose.position - state.target; + const auto basis = hlsl::getQuaternionBasisMatrix(pose.orientation); + outBasis.right = basis[0]; + outBasis.up = basis[1]; + outBasis.forward = basis[2]; + return true; +} + +inline bool tryBuildTargetRelativePoseFromPosition( + const hlsl::float64_t3& targetPosition, + const hlsl::float64_t3& position, + const float minDistance, + const float maxDistance, + SCameraTargetRelativePose& outPose, + SCameraTargetRelativeState* outState = nullptr) +{ + SCameraTargetRelativeState state = {}; + if (!tryBuildTargetRelativeStateFromPosition(targetPosition, position, minDistance, maxDistance, state)) + return false; + + if (!tryBuildTargetRelativePoseFromState(state, minDistance, maxDistance, outPose)) + return false; + + if (outState) + *outState = state; + return true; +} + +inline SCameraTargetRelativeDelta buildTargetRelativeDelta( + const ICamera::SphericalTargetState& currentState, + const SCameraTargetRelativeState& desiredState) +{ + return { + .orbitU = hlsl::wrapAngleRad(desiredState.orbitU - currentState.u), + .orbitV = hlsl::wrapAngleRad(desiredState.orbitV - currentState.v), + .distance = static_cast(desiredState.distance - currentState.distance) + }; +} + +inline void appendTargetRelativeDeltaEvents( + std::vector& events, + const SCameraTargetRelativeDelta& delta, + const double angularDenominator, + const double angularToleranceDeg, + const double distanceDenominator, + const double distanceTolerance, + const SCameraTargetRelativeEventPolicy& policy) +{ + if (policy.translateOrbit) + { + appendAngularAxisEvents( + events, + delta.orbitVector(), + hlsl::float64_t3(angularDenominator), + hlsl::float64_t3(angularToleranceDeg, angularToleranceDeg, std::numeric_limits::infinity()), + {{ + { CVirtualGimbalEvent::MoveRight, CVirtualGimbalEvent::MoveLeft }, + { CVirtualGimbalEvent::MoveUp, CVirtualGimbalEvent::MoveDown }, + { CVirtualGimbalEvent::None, CVirtualGimbalEvent::None } + }}); + } + else + { + if (policy.allowYaw) + { + appendAngularDeltaEvent( + events, + delta.orbitU, + angularDenominator, + angularToleranceDeg, + CVirtualGimbalEvent::PanRight, + CVirtualGimbalEvent::PanLeft); + } + if (policy.allowPitch) + { + appendAngularDeltaEvent( + events, + delta.orbitV, + angularDenominator, + angularToleranceDeg, + CVirtualGimbalEvent::TiltUp, + CVirtualGimbalEvent::TiltDown); + } + } + + if (policy.distanceBinding.positive != CVirtualGimbalEvent::None && + policy.distanceBinding.negative != CVirtualGimbalEvent::None) + { + appendScaledVirtualEvent( + events, + delta.distance, + distanceDenominator, + distanceTolerance, + policy.distanceBinding.positive, + policy.distanceBinding.negative); + } +} + +} // namespace nbl::core + +#endif // _C_CAMERA_TARGET_RELATIVE_UTILITIES_HPP_ diff --git a/common/include/camera/CCameraTextUtilities.hpp b/common/include/camera/CCameraTextUtilities.hpp index 7d23a9571..32e09eca1 100644 --- a/common/include/camera/CCameraTextUtilities.hpp +++ b/common/include/camera/CCameraTextUtilities.hpp @@ -19,21 +19,7 @@ namespace nbl::ui //! Return a short human-readable label for a camera kind. inline std::string_view getCameraTypeLabel(const core::ICamera::CameraKind kind) { - switch (kind) - { - case core::ICamera::CameraKind::FPS: return "FPS"; - case core::ICamera::CameraKind::Free: return "Free"; - case core::ICamera::CameraKind::Orbit: return "Orbit"; - case core::ICamera::CameraKind::Arcball: return "Arcball"; - case core::ICamera::CameraKind::Turntable: return "Turntable"; - case core::ICamera::CameraKind::TopDown: return "TopDown"; - case core::ICamera::CameraKind::Isometric: return "Isometric"; - case core::ICamera::CameraKind::Chase: return "Chase"; - case core::ICamera::CameraKind::Dolly: return "Dolly"; - case core::ICamera::CameraKind::DollyZoom: return "Dolly Zoom"; - case core::ICamera::CameraKind::Path: return "Path"; - default: return "Unknown"; - } + return core::getCameraKindLabel(kind); } //! Return a short human-readable label for a concrete camera instance. @@ -45,21 +31,7 @@ inline std::string_view getCameraTypeLabel(const core::ICamera* camera) //! Return a short human-readable description for a camera kind. inline std::string_view getCameraTypeDescription(const core::ICamera::CameraKind kind) { - switch (kind) - { - case core::ICamera::CameraKind::FPS: return "First-person WASD + mouse look"; - case core::ICamera::CameraKind::Free: return "Free-fly 6DOF with full rotation"; - case core::ICamera::CameraKind::Orbit: return "Orbit around target with dolly"; - case core::ICamera::CameraKind::Arcball: return "Arcball trackball around target"; - case core::ICamera::CameraKind::Turntable: return "Turntable yaw/pitch around target"; - case core::ICamera::CameraKind::TopDown: return "Fixed pitch top-down pan"; - case core::ICamera::CameraKind::Isometric: return "Fixed isometric view with pan"; - case core::ICamera::CameraKind::Chase: return "Target follow with chase controls"; - case core::ICamera::CameraKind::Dolly: return "Rig truck/dolly with look-at"; - case core::ICamera::CameraKind::DollyZoom: return "Orbit with dolly-zoom FOV"; - case core::ICamera::CameraKind::Path: return "Move along a target path"; - default: return "Unspecified camera behavior"; - } + return core::getCameraKindDescription(kind); } //! Return a short human-readable description for a concrete camera instance. @@ -114,7 +86,7 @@ inline std::string describeGoalStateMask(const uint32_t mask) append("Spherical target", core::ICamera::GoalStateSphericalTarget); append("Dynamic perspective", core::ICamera::GoalStateDynamicPerspective); - append("Path", core::ICamera::GoalStatePath); + append("Path rig state", core::ICamera::GoalStatePath); return out; } diff --git a/common/include/camera/CCameraViewportOverlayUtilities.hpp b/common/include/camera/CCameraViewportOverlayUtilities.hpp index c46bf43ad..fa594980e 100644 --- a/common/include/camera/CCameraViewportOverlayUtilities.hpp +++ b/common/include/camera/CCameraViewportOverlayUtilities.hpp @@ -252,7 +252,7 @@ inline void drawViewportSplitOverlay( //! Draw one follow-target overlay in the active viewport using projected tracked-target metrics. inline void drawFollowTargetViewportOverlay( ImDrawList& drawList, - const hlsl::float32_t4x4& viewProjMatrix, + const system::SCameraProjectionContext& projectionContext, const core::CTrackedTarget& trackedTarget, const SViewportOverlayRect& viewportRect, const SCameraFollowTargetViewportOverlayStyle& style = {}) @@ -260,15 +260,13 @@ inline void drawFollowTargetViewportOverlay( if (!viewportRect.valid()) return; - float ndcX = 0.0f; - float ndcY = 0.0f; - float ndcRadius = 0.0f; - if (!system::tryComputeProjectedFollowTargetMetrics(viewProjMatrix, trackedTarget, ndcX, ndcY, &ndcRadius)) + system::SCameraProjectedTargetMetrics projectedTarget = {}; + if (!system::tryComputeProjectedFollowTargetMetrics(projectionContext, trackedTarget, projectedTarget)) return; - const bool centered = ndcRadius <= style.centeredNdcRadius; + const bool centered = projectedTarget.radius <= style.centeredNdcRadius; const ImVec2 center = viewportRect.getCenter(); - const ImVec2 target = viewportRect.ndcToScreen(ImVec2(ndcX, ndcY)); + 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; diff --git a/common/include/camera/CCameraVirtualEventUtilities.hpp b/common/include/camera/CCameraVirtualEventUtilities.hpp new file mode 100644 index 000000000..81695a14e --- /dev/null +++ b/common/include/camera/CCameraVirtualEventUtilities.hpp @@ -0,0 +1,168 @@ +#ifndef _C_CAMERA_VIRTUAL_EVENT_UTILITIES_HPP_ +#define _C_CAMERA_VIRTUAL_EVENT_UTILITIES_HPP_ + +#include +#include +#include + +#include "CCameraMathUtilities.hpp" +#include "ICamera.hpp" + +namespace nbl::core +{ + +struct SCameraVirtualEventAxisBinding final +{ + CVirtualGimbalEvent::VirtualEventType positive = CVirtualGimbalEvent::None; + CVirtualGimbalEvent::VirtualEventType negative = CVirtualGimbalEvent::None; +}; + +struct SCameraVirtualEventBindings final +{ + static inline constexpr std::array LocalTranslation = {{ + { CVirtualGimbalEvent::MoveRight, CVirtualGimbalEvent::MoveLeft }, + { CVirtualGimbalEvent::MoveUp, CVirtualGimbalEvent::MoveDown }, + { CVirtualGimbalEvent::MoveForward, CVirtualGimbalEvent::MoveBackward } + }}; +}; + +inline void appendSignedVirtualEvent( + std::vector& events, + const double value, + const CVirtualGimbalEvent::VirtualEventType positive, + const CVirtualGimbalEvent::VirtualEventType negative, + const double tolerance = static_cast(ICamera::TinyScalarEpsilon)) +{ + if (!hlsl::isFiniteScalar(value) || hlsl::isNearlyZeroScalar(value, tolerance)) + return; + + auto& ev = events.emplace_back(); + ev.type = (value > 0.0) ? positive : negative; + ev.magnitude = hlsl::abs(value); +} + +inline void appendScaledVirtualEvent( + std::vector& events, + const double value, + const double denominator, + const double tolerance, + const CVirtualGimbalEvent::VirtualEventType positive, + const CVirtualGimbalEvent::VirtualEventType negative) +{ + if (!hlsl::isFiniteScalar(denominator) || hlsl::isNearlyZeroScalar(denominator, static_cast(ICamera::TinyScalarEpsilon))) + return; + + appendSignedVirtualEvent(events, value / denominator, positive, negative, tolerance); +} + +inline void appendAngularDeltaEvent( + std::vector& events, + const double deltaRadians, + const double denominator, + const double toleranceDeg, + const CVirtualGimbalEvent::VirtualEventType positive, + const CVirtualGimbalEvent::VirtualEventType negative) +{ + if (!hlsl::isFiniteScalar(deltaRadians) || + hlsl::isNearlyZeroScalar(hlsl::degrees(deltaRadians), toleranceDeg)) + { + return; + } + + appendScaledVirtualEvent( + events, + deltaRadians, + denominator, + hlsl::radians(toleranceDeg), + positive, + negative); +} + +inline void appendScaledVirtualAxisEvents( + std::vector& events, + const hlsl::float64_t3& values, + const hlsl::float64_t3& denominators, + const hlsl::float64_t3& tolerances, + const std::array& axisBindings) +{ + for (size_t axisIx = 0u; axisIx < axisBindings.size(); ++axisIx) + { + appendScaledVirtualEvent( + events, + values[axisIx], + denominators[axisIx], + tolerances[axisIx], + axisBindings[axisIx].positive, + axisBindings[axisIx].negative); + } +} + +inline void appendLocalTranslationEvents( + std::vector& events, + const hlsl::float64_t3& localDelta, + const hlsl::float64_t3& denominators = hlsl::float64_t3(1.0), + const hlsl::float64_t3& tolerances = hlsl::float64_t3(ICamera::TinyScalarEpsilon)) +{ + appendScaledVirtualAxisEvents( + events, + localDelta, + denominators, + tolerances, + SCameraVirtualEventBindings::LocalTranslation); +} + +inline void appendWorldTranslationAsLocalEvents( + std::vector& events, + const hlsl::camera_quaternion_t& orientation, + const hlsl::float64_t3& worldDelta, + const hlsl::float64_t3& denominators = hlsl::float64_t3(1.0), + const hlsl::float64_t3& tolerances = hlsl::float64_t3(ICamera::TinyScalarEpsilon)) +{ + appendLocalTranslationEvents( + events, + hlsl::projectWorldVectorToLocalQuaternionFrame(orientation, worldDelta), + denominators, + tolerances); +} + +inline void appendAngularAxisEvents( + std::vector& events, + const hlsl::float64_t3& deltaRadians, + const hlsl::float64_t3& denominators, + const hlsl::float64_t3& toleranceDeg, + const std::array& axisBindings) +{ + for (size_t axisIx = 0u; axisIx < axisBindings.size(); ++axisIx) + { + appendAngularDeltaEvent( + events, + deltaRadians[axisIx], + denominators[axisIx], + toleranceDeg[axisIx], + axisBindings[axisIx].positive, + axisBindings[axisIx].negative); + } +} + +inline hlsl::float64_t3 collectSignedTranslationDelta(std::span events) +{ + hlsl::float64_t3 delta = hlsl::float64_t3(0.0); + for (const auto& ev : events) + { + switch (ev.type) + { + case CVirtualGimbalEvent::MoveRight: delta.x += ev.magnitude; break; + case CVirtualGimbalEvent::MoveLeft: delta.x -= ev.magnitude; break; + case CVirtualGimbalEvent::MoveUp: delta.y += ev.magnitude; break; + case CVirtualGimbalEvent::MoveDown: delta.y -= ev.magnitude; break; + case CVirtualGimbalEvent::MoveForward: delta.z += ev.magnitude; break; + case CVirtualGimbalEvent::MoveBackward: delta.z -= ev.magnitude; break; + default: break; + } + } + return delta; +} + +} // namespace nbl::core + +#endif // _C_CAMERA_VIRTUAL_EVENT_UTILITIES_HPP_ diff --git a/common/include/camera/CChaseCamera.hpp b/common/include/camera/CChaseCamera.hpp index b6f098eda..0c389156f 100644 --- a/common/include/camera/CChaseCamera.hpp +++ b/common/include/camera/CChaseCamera.hpp @@ -31,8 +31,9 @@ class CChaseCamera final : public CSphericalTargetCamera const auto impulse = m_gimbal.accumulate(virtualEvents); - const double deltaYaw = scaleVirtualRotation(impulse.dVirtualRotation.y); - const double deltaPitch = scaleVirtualRotation(impulse.dVirtualRotation.x); + const auto deltaRotation = scaleVirtualRotation(impulse.dVirtualRotation); + const auto deltaTranslation = scaleVirtualTranslation(impulse.dVirtualTranslate); + const auto deltaDistance = scaleUnscaledVirtualTranslation(impulse.dVirtualTranslate.y); const auto basis = computeBasis(m_u, m_v, m_distance); @@ -43,12 +44,11 @@ class CChaseCamera final : public CSphericalTargetCamera hlsl::float64_t3(basis.right.x, 0.0, basis.right.z), hlsl::float64_t3(1.0, 0.0, 0.0)); - m_targetPosition += planarRight * scaleVirtualTranslation(impulse.dVirtualTranslate.x) + - planarForward * scaleVirtualTranslation(impulse.dVirtualTranslate.z); - m_distance = std::clamp(m_distance + static_cast(scaleUnscaledVirtualTranslation(impulse.dVirtualTranslate.y)), MinDistance, MaxDistance); + m_targetPosition += planarRight * deltaTranslation.x + planarForward * deltaTranslation.z; + m_distance = std::clamp(m_distance + static_cast(deltaDistance), MinDistance, MaxDistance); - m_u += deltaYaw; - m_v = std::clamp(m_v + deltaPitch, MinPitch, MaxPitch); + m_u += deltaRotation.y; + m_v = std::clamp(m_v + deltaRotation.x, MinPitch, MaxPitch); return applyPose(); } @@ -59,8 +59,8 @@ class CChaseCamera final : public CSphericalTargetCamera private: static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate | CVirtualGimbalEvent::Rotate; - static inline constexpr double MaxPitch = hlsl::numbers::pi * (70.0 / 180.0); - static inline constexpr double MinPitch = -hlsl::numbers::pi / 3.0; + static inline constexpr double MaxPitch = SCameraTargetRelativeRigDefaults::ChaseMaxPitchRad; + static inline constexpr double MinPitch = SCameraTargetRelativeRigDefaults::ChaseMinPitchRad; }; } diff --git a/common/include/camera/CDollyCamera.hpp b/common/include/camera/CDollyCamera.hpp index af28c3c47..e50d3e83c 100644 --- a/common/include/camera/CDollyCamera.hpp +++ b/common/include/camera/CDollyCamera.hpp @@ -31,18 +31,15 @@ class CDollyCamera final : public CSphericalTargetCamera const auto impulse = m_gimbal.accumulate(virtualEvents); - const double deltaYaw = scaleVirtualRotation(impulse.dVirtualRotation.y); - const double deltaPitch = scaleVirtualRotation(impulse.dVirtualRotation.x); + const auto deltaRotation = scaleVirtualRotation(impulse.dVirtualRotation); + const auto deltaTranslation = scaleVirtualTranslation(impulse.dVirtualTranslate); const auto basis = computeBasis(m_u, m_v, m_distance); - const auto delta = - basis.right * scaleVirtualTranslation(impulse.dVirtualTranslate.x) + - basis.up * scaleVirtualTranslation(impulse.dVirtualTranslate.y) + - basis.forward * scaleVirtualTranslation(impulse.dVirtualTranslate.z); + const auto delta = hlsl::transformLocalVectorToWorldBasis(deltaTranslation, basis.right, basis.up, basis.forward); m_targetPosition += delta; - m_u += deltaYaw; - m_v = std::clamp(m_v + deltaPitch, MinPitch, MaxPitch); + m_u += deltaRotation.y; + m_v = std::clamp(m_v + deltaRotation.x, MinPitch, MaxPitch); return applyPose(); } @@ -53,7 +50,7 @@ class CDollyCamera final : public CSphericalTargetCamera private: static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate | CVirtualGimbalEvent::Rotate; - static inline constexpr double MaxPitch = hlsl::numbers::pi * (85.0 / 180.0); + static inline constexpr double MaxPitch = SCameraTargetRelativeRigDefaults::DollyPitchLimitRad; static inline constexpr double MinPitch = -MaxPitch; }; diff --git a/common/include/camera/CDollyZoomCamera.hpp b/common/include/camera/CDollyZoomCamera.hpp index 66b1ebef0..293be07cd 100644 --- a/common/include/camera/CDollyZoomCamera.hpp +++ b/common/include/camera/CDollyZoomCamera.hpp @@ -44,12 +44,11 @@ class CDollyZoomCamera final : public CSphericalTargetCamera return false; const auto impulse = m_gimbal.accumulate(virtualEvents); - const double deltaU = scaleVirtualTranslation(impulse.dVirtualTranslate.y); - const double deltaV = scaleVirtualTranslation(impulse.dVirtualTranslate.x); + const auto deltaTranslation = scaleVirtualTranslation(impulse.dVirtualTranslate); const double deltaDistance = scaleUnscaledVirtualTranslation(impulse.dVirtualTranslate.z); - m_u += deltaU; - m_v += deltaV; + m_u += deltaTranslation.y; + m_v += deltaTranslation.x; m_distance = std::clamp(m_distance + static_cast(deltaDistance), MinDistance, MaxDistance); return applyPose(); @@ -71,7 +70,7 @@ class CDollyZoomCamera final : public CSphericalTargetCamera } virtual bool trySetDynamicPerspectiveState(const DynamicPerspectiveState& state) override { - if (!std::isfinite(state.baseFov) || !std::isfinite(state.referenceDistance) || state.referenceDistance <= 0.f) + if (!hlsl::isFiniteScalar(state.baseFov) || !hlsl::isFiniteScalar(state.referenceDistance) || state.referenceDistance <= 0.f) return false; m_baseFov = state.baseFov; diff --git a/common/include/camera/CFPSCamera.hpp b/common/include/camera/CFPSCamera.hpp index 8b26eeabc..ed8e869fc 100644 --- a/common/include/camera/CFPSCamera.hpp +++ b/common/include/camera/CFPSCamera.hpp @@ -12,10 +12,17 @@ namespace nbl::core { +//! Free-position camera that keeps the view upright and exposes only yaw/pitch rotation. class CFPSCamera final : public ICamera { public: using base_t = ICamera; + struct SFpsCameraDefaults final + { + static inline constexpr float RollValidationEpsilonDeg = 1.e-4f; + static inline constexpr float StraightRollDeg = 0.0f; + static inline constexpr float InvertedRollDeg = 180.0f; + }; CFPSCamera(const hlsl::float64_t3& position, const hlsl::camera_quaternion_t& orientation = hlsl::makeIdentityQuaternion()) : base_t(), m_gimbal({ .position = position, .orientation = orientation }) @@ -23,7 +30,7 @@ class CFPSCamera final : public ICamera m_gimbal.begin(); { const auto pitchYaw = hlsl::getPitchYawFromForwardVector(m_gimbal.getZAxis()); - m_gimbal.setOrientation(hlsl::makeQuaternionFromEulerRadians(hlsl::float64_t3(pitchYaw.x, pitchYaw.y, 0.0))); + m_gimbal.setOrientation(hlsl::makeQuaternionFromEulerRadiansYXZ(hlsl::float64_t3(pitchYaw.x, pitchYaw.y, 0.0))); } m_gimbal.end(); } @@ -48,10 +55,12 @@ class CFPSCamera final : public ICamera if (referenceFrame) { const float roll = static_cast(hlsl::degrees(hlsl::getQuaternionEulerRadiansYXZ(reference.orientation).z)); - const float absRoll = std::abs(roll); - constexpr float epsilon = 1.e-4f; + const bool matchesStraightRoll = + hlsl::getWrappedAngleDistanceDegrees(roll, SFpsCameraDefaults::StraightRollDeg) <= SFpsCameraDefaults::RollValidationEpsilonDeg; + const bool matchesInvertedRoll = + hlsl::getWrappedAngleDistanceDegrees(roll, SFpsCameraDefaults::InvertedRollDeg) <= SFpsCameraDefaults::RollValidationEpsilonDeg; - if (not ((absRoll <= epsilon) || (std::abs(absRoll - 180.f) <= epsilon))) + if (!(matchesStraightRoll || matchesInvertedRoll)) return false; } @@ -69,7 +78,7 @@ class CFPSCamera final : public ICamera const float newYaw = static_cast(pitchYaw.y + scaleVirtualRotation(impulse.dVirtualRotation.y)); if (validateReference()) - m_gimbal.setOrientation(hlsl::makeQuaternionFromEulerRadians(hlsl::float64_t3(newPitch, newYaw, 0.0f))); + m_gimbal.setOrientation(hlsl::makeQuaternionFromEulerRadiansYXZ(hlsl::float64_t3(newPitch, newYaw, 0.0f))); m_gimbal.setPosition(hlsl::float64_t3(reference.frame[3]) + hlsl::rotateVectorByQuaternion(reference.orientation, hlsl::float64_t3(impulse.dVirtualTranslate))); } m_gimbal.end(); @@ -102,7 +111,7 @@ class CFPSCamera final : public ICamera typename base_t::CGimbal m_gimbal; static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate | CVirtualGimbalEvent::Rotate; - static inline constexpr float MaxVerticalAngle = static_cast(hlsl::numbers::pi * (88.0 / 180.0)); + static inline constexpr float MaxVerticalAngle = static_cast(hlsl::SCameraViewRigDefaults::FpsVerticalPitchLimitRad); static inline constexpr float MinVerticalAngle = -MaxVerticalAngle; }; diff --git a/common/include/camera/CFreeLockCamera.hpp b/common/include/camera/CFreeLockCamera.hpp index 3c4c57a8c..ad6971ef9 100644 --- a/common/include/camera/CFreeLockCamera.hpp +++ b/common/include/camera/CFreeLockCamera.hpp @@ -9,7 +9,8 @@ namespace nbl::core { -// Free Lock Camera + +//! Free-position camera that allows full yaw/pitch/roll rotation. class CFreeCamera final : public ICamera { public: diff --git a/common/include/camera/CIsometricCamera.hpp b/common/include/camera/CIsometricCamera.hpp index 647198aa5..e35b9fc38 100644 --- a/common/include/camera/CIsometricCamera.hpp +++ b/common/include/camera/CIsometricCamera.hpp @@ -32,8 +32,7 @@ class CIsometricCamera final : public CSphericalTargetCamera const auto impulse = m_gimbal.accumulate(virtualEvents); - const double deltaPanX = scaleVirtualTranslation(impulse.dVirtualTranslate.x); - const double deltaPanY = scaleVirtualTranslation(impulse.dVirtualTranslate.y); + const auto deltaTranslation = scaleVirtualTranslation(impulse.dVirtualTranslate); const double deltaDistance = scaleUnscaledVirtualTranslation(impulse.dVirtualTranslate.z); m_u = IsoYaw; @@ -41,8 +40,7 @@ class CIsometricCamera final : public CSphericalTargetCamera m_distance = std::clamp(m_distance + static_cast(deltaDistance), MinDistance, MaxDistance); const auto basis = computeBasis(m_u, m_v, m_distance); - if (deltaPanX != 0.0 || deltaPanY != 0.0) - m_targetPosition += basis.right * deltaPanX + basis.up * deltaPanY; + applyPlanarTargetTranslation(deltaTranslation, basis); return applyPose(); } @@ -53,8 +51,8 @@ class CIsometricCamera final : public CSphericalTargetCamera private: static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate; - static inline constexpr double IsoYaw = hlsl::numbers::pi * 0.25; - static inline constexpr double IsoPitch = hlsl::numbers::pi * (35.264389682754654 / 180.0); + static inline constexpr double IsoYaw = SCameraTargetRelativeRigDefaults::IsometricYawRad; + static inline constexpr double IsoPitch = SCameraTargetRelativeRigDefaults::IsometricPitchRad; }; } diff --git a/common/include/camera/COrbitCamera.hpp b/common/include/camera/COrbitCamera.hpp index 0e4b0f895..fdee73196 100644 --- a/common/include/camera/COrbitCamera.hpp +++ b/common/include/camera/COrbitCamera.hpp @@ -26,12 +26,11 @@ class COrbitCamera final : public CSphericalTargetCamera virtual bool manipulate(std::span virtualEvents, const hlsl::float64_t4x4* referenceFrame = nullptr) override { const auto impulse = m_gimbal.accumulate(virtualEvents); - const double deltaU = scaleVirtualTranslation(impulse.dVirtualTranslate.y); - const double deltaV = scaleVirtualTranslation(impulse.dVirtualTranslate.x); + const auto deltaTranslation = scaleVirtualTranslation(impulse.dVirtualTranslate); const double deltaDistance = scaleUnscaledVirtualTranslation(impulse.dVirtualTranslate.z); - m_u += deltaU; - m_v += deltaV; + m_u += deltaTranslation.y; + m_v += deltaTranslation.x; m_distance = std::clamp(m_distance + static_cast(deltaDistance), MinDistance, MaxDistance); diff --git a/common/include/camera/CPathCamera.hpp b/common/include/camera/CPathCamera.hpp index ccbf15737..d66980b83 100644 --- a/common/include/camera/CPathCamera.hpp +++ b/common/include/camera/CPathCamera.hpp @@ -2,11 +2,17 @@ #define _C_PATH_CAMERA_HPP_ #include +#include "CCameraPathUtilities.hpp" #include "CSphericalTargetCamera.hpp" namespace nbl::core { +//! Target-relative cylindrical path rig camera driven by `PathState`. +//! +//! The authored surface exposes this kind as `PathRig`. It stores a +//! cylindrical target-relative state `(angle, radius, height)` and rebuilds the +//! camera pose from that state through the shared path utilities. class CPathCamera final : public CSphericalTargetCamera { public: @@ -15,21 +21,8 @@ class CPathCamera final : public CSphericalTargetCamera CPathCamera(const hlsl::float64_t3& position, const hlsl::float64_t3& target) : base_t(position, target) { - if (!hlsl::tryBuildPathStateFromPosition( - target, - position, - MinPathRadius, - m_pathState.angle, - m_pathState.radius, - m_pathState.height)) - { - m_pathState = { - .angle = 0.0, - .radius = MinPathRadius, - .height = 0.0 - }; - } - updateFromPath(); + tryResolvePathState(target, position, SCameraPathDefaults::Limits, nullptr, m_pathState); + updateFromPathState(); } ~CPathCamera() = default; @@ -40,15 +33,23 @@ class CPathCamera final : public CSphericalTargetCamera if (not virtualEvents.size() and not referenceFrame) return false; - const auto impulse = m_gimbal.accumulate(virtualEvents); + PathState nextPathState = m_pathState; + if (referenceFrame) + { + CReferenceTransform reference = {}; + if (!m_gimbal.extractReferenceTransform(&reference, referenceFrame)) + return false; + if (!tryResolvePathState(m_targetPosition, hlsl::float64_t3(reference.frame[3]), SCameraPathDefaults::Limits, nullptr, nextPathState)) + return false; + } - m_pathState.angle += scaleVirtualTranslation(impulse.dVirtualTranslate.z); - m_pathState.radius += scaleVirtualTranslation(impulse.dVirtualTranslate.x); - m_pathState.height += scaleVirtualTranslation(impulse.dVirtualTranslate.y); - if (!hlsl::sanitizePathState(m_pathState.angle, m_pathState.radius, m_pathState.height, MinPathRadius)) + const auto impulse = m_gimbal.accumulate(virtualEvents); + const auto stateDelta = makePathDeltaFromVirtualPathTranslate(scaleVirtualTranslation(impulse.dVirtualTranslate)); + if (!tryApplyPathStateDelta(nextPathState, stateDelta, SCameraPathDefaults::Limits, nextPathState)) return false; - return updateFromPath(); + m_pathState = nextPathState; + return updateFromPathState(); } virtual uint32_t getAllowedVirtualEvents() const override { return AllowedVirtualEvents; } @@ -62,74 +63,46 @@ class CPathCamera final : public CSphericalTargetCamera virtual bool trySetPathState(const PathState& state) override { auto sanitized = state; - if (!hlsl::sanitizePathState(sanitized.angle, sanitized.radius, sanitized.height, MinPathRadius)) + if (!sanitizePathState(sanitized, SCameraPathDefaults::Limits.minRadius)) return false; - const bool exact = hlsl::nearlyEqualScalar( - static_cast(sanitized.radius), - static_cast(state.radius), - static_cast(ICamera::TinyScalarEpsilon)); + const bool exact = pathStatesNearlyEqual(sanitized, state, SCameraPathDefaults::ExactComparisonThresholds); m_pathState = sanitized; - updateFromPath(); + updateFromPathState(); return exact; } virtual bool trySetSphericalDistance(float distance) override { - const auto clamped = std::clamp(distance, MinDistance, MaxDistance); - const bool inRange = clamped == distance; - - if (!hlsl::tryScalePathStateDistance( - static_cast(clamped), - MinPathRadius, - m_pathState.radius, - m_pathState.height)) + SCameraPathDistanceUpdateResult distanceUpdate = {}; + if (!tryUpdatePathStateDistance(distance, SCameraPathDefaults::Limits, m_pathState, &distanceUpdate)) return false; - updateFromPath(); - - const double appliedDistance = hlsl::getPathDistance(m_pathState.radius, m_pathState.height); - return inRange && std::abs(appliedDistance - static_cast(clamped)) <= ICamera::ScalarTolerance; + updateFromPathState(); + return distanceUpdate.exact; } - virtual std::string_view getIdentifier() const override { return "Path Camera"; } + virtual std::string_view getIdentifier() const override { return SCameraPathDefaults::Identifier; } private: static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate; - static inline constexpr double MinPathRadius = ICamera::SphericalMinDistance; - PathState m_pathState = { .angle = 0.0, .radius = 1.0, .height = 0.0 }; + PathState m_pathState = makeDefaultPathState(SCameraPathDefaults::Limits.minRadius); - bool updateFromPath() + bool updateFromPathState() { - hlsl::float64_t3 position = hlsl::float64_t3(0.0); - hlsl::camera_quaternion_t orientation = hlsl::makeIdentityQuaternion(); - hlsl::float64_t appliedDistance = static_cast(m_distance); - double orbitU = m_u; - double orbitV = m_v; - if (!hlsl::tryBuildPathPoseFromState( - m_targetPosition, - m_pathState.angle, - m_pathState.radius, - m_pathState.height, - MinPathRadius, - static_cast(MinDistance), - static_cast(MaxDistance), - position, - orientation, - &appliedDistance, - &orbitU, - &orbitV)) + SCameraCanonicalPathState canonicalPathState = {}; + if (!tryBuildCanonicalPathState(m_targetPosition, m_pathState, SCameraPathDefaults::Limits, canonicalPathState)) { return false; } - m_distance = static_cast(appliedDistance); - m_u = orbitU; - m_v = orbitV; + m_distance = canonicalPathState.targetRelative.distance; + m_u = canonicalPathState.targetRelative.orbitU; + m_v = canonicalPathState.targetRelative.orbitV; m_gimbal.begin(); { - m_gimbal.setPosition(position); - m_gimbal.setOrientation(orientation); + m_gimbal.setPosition(canonicalPathState.pose.position); + m_gimbal.setOrientation(canonicalPathState.pose.orientation); } m_gimbal.end(); diff --git a/common/include/camera/CSphericalTargetCamera.hpp b/common/include/camera/CSphericalTargetCamera.hpp index 1ee1c795a..b6f7d7fcf 100644 --- a/common/include/camera/CSphericalTargetCamera.hpp +++ b/common/include/camera/CSphericalTargetCamera.hpp @@ -2,7 +2,7 @@ #define _C_SPHERICAL_TARGET_CAMERA_HPP_ #include -#include "ICamera.hpp" +#include "CCameraTargetRelativeUtilities.hpp" namespace nbl::core { @@ -18,7 +18,7 @@ class CSphericalTargetCamera : public ICamera using base_t = ICamera; CSphericalTargetCamera(const hlsl::float64_t3& position, const hlsl::float64_t3& target) - : base_t(), m_targetPosition(target), m_distance(1.0f), + : base_t(), m_targetPosition(target), m_distance(SCameraTargetRelativeRigDefaults::InitialDistance), m_gimbal({ .position = position, .orientation = hlsl::makeIdentityQuaternion() }) { initFromPosition(position); @@ -80,52 +80,26 @@ class CSphericalTargetCamera : public ICamera } protected: - struct SphericalBasis - { - hlsl::float64_t3 localSpherePosition = hlsl::float64_t3(0.0); - hlsl::float64_t3 right = hlsl::float64_t3(1.0, 0.0, 0.0); - hlsl::float64_t3 up = hlsl::float64_t3(0.0, 0.0, 1.0); - hlsl::float64_t3 forward = hlsl::float64_t3(0.0, 1.0, 0.0); - }; + using SphericalBasis = SCameraTargetRelativeBasis; inline SphericalBasis computeBasis(double orbitU, double orbitV, float distance) const { SphericalBasis basis; - hlsl::float64_t3 position = hlsl::float64_t3(0.0); - hlsl::camera_quaternion_t orientation = hlsl::makeIdentityQuaternion(); - if (!hlsl::tryBuildSphericalPoseFromOrbit( - m_targetPosition, - orbitU, - orbitV, - static_cast(distance), - static_cast(MinDistance), - static_cast(MaxDistance), - position, - orientation)) - { + const SCameraTargetRelativeState state = { + .target = m_targetPosition, + .orbitU = orbitU, + .orbitV = orbitV, + .distance = distance + }; + if (!tryBuildTargetRelativeBasis(state, MinDistance, MaxDistance, basis)) return basis; - } - - basis.localSpherePosition = position - m_targetPosition; - basis.right = orientation.transformVector(hlsl::float64_t3(1.0, 0.0, 0.0), true); - basis.up = orientation.transformVector(hlsl::float64_t3(0.0, 1.0, 0.0), true); - basis.forward = orientation.transformVector(hlsl::float64_t3(0.0, 0.0, 1.0), true); return basis; } inline void initFromPosition(const hlsl::float64_t3& position) { - double orbitU = 0.0; - double orbitV = 0.0; - hlsl::float64_t appliedDistance = static_cast(MinDistance); - if (!hlsl::tryBuildOrbitFromPosition( - m_targetPosition, - position, - static_cast(MinDistance), - static_cast(MaxDistance), - orbitU, - orbitV, - appliedDistance)) + SCameraTargetRelativeState state = {}; + if (!tryBuildTargetRelativeStateFromPosition(m_targetPosition, position, MinDistance, MaxDistance, state)) { m_distance = MinDistance; m_u = 0.0; @@ -133,35 +107,40 @@ class CSphericalTargetCamera : public ICamera return; } - m_distance = static_cast(appliedDistance); - m_u = orbitU; - m_v = orbitV; + m_distance = state.distance; + m_u = state.orbitU; + m_v = state.orbitV; + } + + inline void applyPlanarTargetTranslation(const hlsl::float64_t3& deltaTranslation, const SphericalBasis& basis) + { + if (!hlsl::hasPlanarDeltaXY(deltaTranslation, static_cast(base_t::TinyScalarEpsilon))) + return; + + m_targetPosition += hlsl::transformLocalVectorToWorldBasis( + hlsl::float64_t3(deltaTranslation.x, deltaTranslation.y, 0.0), + basis.right, + basis.up, + basis.forward); } inline bool applyPose() { - hlsl::float64_t3 newPosition = hlsl::float64_t3(0.0); - hlsl::camera_quaternion_t newOrientation = hlsl::makeIdentityQuaternion(); - hlsl::float64_t appliedDistance = static_cast(m_distance); - if (!hlsl::tryBuildSphericalPoseFromOrbit( - m_targetPosition, - m_u, - m_v, - static_cast(m_distance), - static_cast(MinDistance), - static_cast(MaxDistance), - newPosition, - newOrientation, - &appliedDistance)) - { + const SCameraTargetRelativeState state = { + .target = m_targetPosition, + .orbitU = m_u, + .orbitV = m_v, + .distance = m_distance + }; + SCameraTargetRelativePose pose = {}; + if (!tryBuildTargetRelativePoseFromState(state, MinDistance, MaxDistance, pose)) return false; - } - m_distance = static_cast(appliedDistance); + m_distance = static_cast(pose.appliedDistance); m_gimbal.begin(); { - m_gimbal.setPosition(newPosition); - m_gimbal.setOrientation(newOrientation); + m_gimbal.setPosition(pose.position); + m_gimbal.setOrientation(pose.orientation); } m_gimbal.end(); diff --git a/common/include/camera/CTopDownCamera.hpp b/common/include/camera/CTopDownCamera.hpp index 07b8d0ee9..f46caeac5 100644 --- a/common/include/camera/CTopDownCamera.hpp +++ b/common/include/camera/CTopDownCamera.hpp @@ -31,18 +31,16 @@ class CTopDownCamera final : public CSphericalTargetCamera const auto impulse = m_gimbal.accumulate(virtualEvents); - const double deltaYaw = scaleVirtualRotation(impulse.dVirtualRotation.y); - const double deltaPanX = scaleVirtualTranslation(impulse.dVirtualTranslate.x); - const double deltaPanY = scaleVirtualTranslation(impulse.dVirtualTranslate.y); + const auto deltaRotation = scaleVirtualRotation(impulse.dVirtualRotation); + const auto deltaTranslation = scaleVirtualTranslation(impulse.dVirtualTranslate); const double deltaDistance = scaleUnscaledVirtualTranslation(impulse.dVirtualTranslate.z); - m_u += deltaYaw; + m_u += deltaRotation.y; m_v = TopDownPitch; m_distance = std::clamp(m_distance + static_cast(deltaDistance), MinDistance, MaxDistance); const auto basis = computeBasis(m_u, m_v, m_distance); - if (deltaPanX != 0.0 || deltaPanY != 0.0) - m_targetPosition += basis.right * deltaPanX + basis.up * deltaPanY; + applyPlanarTargetTranslation(deltaTranslation, basis); return applyPose(); } @@ -53,7 +51,7 @@ class CTopDownCamera final : public CSphericalTargetCamera private: static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate | CVirtualGimbalEvent::Rotate; - static inline constexpr double TopDownPitch = -hlsl::numbers::pi * 0.5; + static inline constexpr double TopDownPitch = SCameraTargetRelativeRigDefaults::TopDownPitchRad; }; } diff --git a/common/include/camera/CTurntableCamera.hpp b/common/include/camera/CTurntableCamera.hpp index 1b4bb8b45..8176f579e 100644 --- a/common/include/camera/CTurntableCamera.hpp +++ b/common/include/camera/CTurntableCamera.hpp @@ -56,7 +56,7 @@ class CTurntableCamera final : public CSphericalTargetCamera private: static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate | CVirtualGimbalEvent::Rotate; - static inline constexpr double MaxPitch = hlsl::numbers::pi * (89.0 / 180.0); + static inline constexpr double MaxPitch = SCameraTargetRelativeRigDefaults::TurntablePitchLimitRad; static inline constexpr double MinPitch = -MaxPitch; }; diff --git a/common/include/camera/CVirtualGimbalEvent.hpp b/common/include/camera/CVirtualGimbalEvent.hpp index 0d43ac7cd..27cbb1880 100644 --- a/common/include/camera/CVirtualGimbalEvent.hpp +++ b/common/include/camera/CVirtualGimbalEvent.hpp @@ -113,6 +113,21 @@ struct CVirtualGimbalEvent return None; } + static constexpr bool isTranslationEvent(const VirtualEventType event) + { + return event != None && (event & Translate) == event; + } + + static constexpr bool isRotationEvent(const VirtualEventType event) + { + return event != None && (event & Rotate) == event; + } + + static constexpr bool isScaleEvent(const VirtualEventType event) + { + return event != None && (event & Scale) == event; + } + static inline constexpr auto VirtualEventsTypeTable = []() { std::array output; diff --git a/common/include/camera/ICamera.hpp b/common/include/camera/ICamera.hpp index 409fee652..9140bcd4b 100644 --- a/common/include/camera/ICamera.hpp +++ b/common/include/camera/ICamera.hpp @@ -87,11 +87,26 @@ class ICamera : virtual public core::IReferenceCounted float referenceDistance = 0.f; }; + //! Target-relative cylindrical rig state used by the `Path Rig` camera kind. struct PathState { double angle = 0.0; double radius = 0.0; double height = 0.0; + + inline hlsl::float64_t3 asVector() const + { + return hlsl::float64_t3(radius, height, angle); + } + + static inline PathState fromVector(const hlsl::float64_t3& value) + { + return { + .angle = value.z, + .radius = value.x, + .height = value.y + }; + } }; //! Gimbal that models the camera pose and cached view matrix in world space. @@ -274,14 +289,29 @@ class ICamera : virtual public core::IReferenceCounted { return magnitude * getScaledVirtualTranslationMagnitude(); } + template + inline hlsl::camera_vector_t scaleVirtualTranslation(const hlsl::camera_vector_t& magnitude) const + { + return magnitude * static_cast(getScaledVirtualTranslationMagnitude()); + } inline double scaleUnscaledVirtualTranslation(const double magnitude) const { return magnitude * getUnscaledVirtualTranslationMagnitude(); } + template + inline hlsl::camera_vector_t scaleUnscaledVirtualTranslation(const hlsl::camera_vector_t& magnitude) const + { + return magnitude * static_cast(getUnscaledVirtualTranslationMagnitude()); + } inline double scaleVirtualRotation(const double magnitude) const { return magnitude * getRotationSpeedScale(); } + template + inline hlsl::camera_vector_t scaleVirtualRotation(const hlsl::camera_vector_t& magnitude) const + { + return magnitude * static_cast(getRotationSpeedScale()); + } inline SScopedMotionScaleOverride overrideMotionScales(const double moveScale, const double rotationScale) { return SScopedMotionScaleOverride(this, moveScale, rotationScale); diff --git a/common/include/camera/IGimbal.hpp b/common/include/camera/IGimbal.hpp index bc764c313..00a4acf29 100644 --- a/common/include/camera/IGimbal.hpp +++ b/common/include/camera/IGimbal.hpp @@ -175,7 +175,7 @@ namespace nbl::core inline void transform(const CReferenceTransform& reference, const VirtualImpulse& impulse) { - setOrientation(reference.orientation * hlsl::makeQuaternionFromEulerRadians(impulse.dVirtualRotation)); + setOrientation(reference.orientation * hlsl::makeQuaternionFromEulerRadiansYXZ(impulse.dVirtualRotation)); setPosition(hlsl::mul(hlsl::float64_t4(impulse.dVirtualTranslate, 1), reference.frame).xyz); } diff --git a/common/include/camera/IGimbalInputProcessor.hpp b/common/include/camera/IGimbalInputProcessor.hpp index 76e61b7c3..5a3d3e262 100644 --- a/common/include/camera/IGimbalInputProcessor.hpp +++ b/common/include/camera/IGimbalInputProcessor.hpp @@ -1,6 +1,9 @@ #ifndef _NBL_I_GIMBAL_INPUT_PROCESSOR_HPP_ #define _NBL_I_GIMBAL_INPUT_PROCESSOR_HPP_ +#include +#include + #include "nbl/ui/KeyCodes.h" #include "nbl/ui/SInputEvent.h" @@ -15,7 +18,15 @@ namespace nbl::ui class IGimbalInputProcessor : public CGimbalBindingLayoutStorage { public: - static inline constexpr double MaxFrameDeltaMs = 200.0; + struct SInputProcessorDefaults final + { + static inline constexpr double MaxFrameDeltaMs = 200.0; + static inline constexpr float ZeroPivot = 0.0f; + static inline constexpr float UnitPivot = 1.0f; + }; + static inline constexpr double MaxFrameDeltaMs = SInputProcessorDefaults::MaxFrameDeltaMs; + static inline constexpr float ZeroPivot = SInputProcessorDefaults::ZeroPivot; + static inline constexpr float UnitPivot = SInputProcessorDefaults::UnitPivot; using CGimbalBindingLayoutStorage::CGimbalBindingLayoutStorage; @@ -34,13 +45,7 @@ class IGimbalInputProcessor : public CGimbalBindingLayoutStorage void beginInputProcessing(const std::chrono::microseconds nextPresentationTimeStamp) { m_nextPresentationTimeStamp = nextPresentationTimeStamp; - const auto deltaMs = std::chrono::duration_cast(m_nextPresentationTimeStamp - m_lastVirtualUpTimeStamp).count(); - if (deltaMs < 0) - m_frameDeltaTime = 0.0; - else if (static_cast(deltaMs) > MaxFrameDeltaMs) - m_frameDeltaTime = MaxFrameDeltaMs; - else - m_frameDeltaTime = static_cast(deltaMs); + m_frameDeltaTime = clampFrameDeltaTimeMs(m_nextPresentationTimeStamp, m_lastVirtualUpTimeStamp); } void endInputProcessing() @@ -120,38 +125,20 @@ class IGimbalInputProcessor : public CGimbalBindingLayoutStorage */ void processKeyboard(gimbal_event_t* output, uint32_t& count, std::span events) { - count = 0u; - const auto mappedVirtualEventsCount = m_keyboardVirtualEventMap.size(); - - if (!output) - { - count = mappedVirtualEventsCount; - return; - } - - if (mappedVirtualEventsCount) - { - preprocess(m_keyboardVirtualEventMap); - - for (const auto& keyboardEvent : events) + processBindingMap( + m_keyboardVirtualEventMap, + output, + count, + [&](auto& map) { - auto request = m_keyboardVirtualEventMap.find(keyboardEvent.keyCode); - if (request != std::end(m_keyboardVirtualEventMap)) + for (const auto& keyboardEvent : events) { - auto& hash = request->second; - if (keyboardEvent.action == input_keyboard_event_t::ECA_PRESSED) - { - if (!hash.active) - hash.active = true; - } + setBindingActiveState(map, keyboardEvent.keyCode, true); else if (keyboardEvent.action == input_keyboard_event_t::ECA_RELEASED) - hash.active = false; + setBindingActiveState(map, keyboardEvent.keyCode, false); } - } - - postprocess(m_keyboardVirtualEventMap, output, count); - } + }); } /** @@ -175,71 +162,45 @@ class IGimbalInputProcessor : public CGimbalBindingLayoutStorage */ void processMouse(gimbal_event_t* output, uint32_t& count, std::span events) { - count = 0u; - const auto mappedVirtualEventsCount = m_mouseVirtualEventMap.size(); - - if (!output) - { - count = mappedVirtualEventsCount; - return; - } - - if (mappedVirtualEventsCount) - { - preprocess(m_mouseVirtualEventMap); - - for (const auto& mouseEvent : events) + processBindingMap( + m_mouseVirtualEventMap, + output, + count, + [&](auto& map) { - ui::E_MOUSE_CODE mouseCode = ui::EMC_NONE; - - switch (mouseEvent.type) + for (const auto& mouseEvent : events) { - case input_mouse_event_t::EET_CLICK: - { - switch (mouseEvent.clickEvent.mouseButton) - { - case ui::EMB_LEFT_BUTTON: mouseCode = ui::EMC_LEFT_BUTTON; break; - case ui::EMB_RIGHT_BUTTON: mouseCode = ui::EMC_RIGHT_BUTTON; break; - case ui::EMB_MIDDLE_BUTTON: mouseCode = ui::EMC_MIDDLE_BUTTON; break; - case ui::EMB_BUTTON_4: mouseCode = ui::EMC_BUTTON_4; break; - case ui::EMB_BUTTON_5: mouseCode = ui::EMC_BUTTON_5; break; - default: continue; - } - - auto request = m_mouseVirtualEventMap.find(mouseCode); - if (request != std::end(m_mouseVirtualEventMap)) - { - auto& hash = request->second; - - if (mouseEvent.clickEvent.action == input_mouse_event_t::SClickEvent::EA_PRESSED) - { - if (!hash.active) - hash.active = true; - } - else if (mouseEvent.clickEvent.action == input_mouse_event_t::SClickEvent::EA_RELEASED) - hash.active = false; - } - } break; - - case input_mouse_event_t::EET_SCROLL: + switch (mouseEvent.type) { - requestMagnitudeUpdateWithScalar(0.f, float(mouseEvent.scrollEvent.verticalScroll), float(std::abs(mouseEvent.scrollEvent.verticalScroll)), ui::EMC_VERTICAL_POSITIVE_SCROLL, ui::EMC_VERTICAL_NEGATIVE_SCROLL, m_mouseVirtualEventMap); - requestMagnitudeUpdateWithScalar(0.f, mouseEvent.scrollEvent.horizontalScroll, std::abs(mouseEvent.scrollEvent.horizontalScroll), ui::EMC_HORIZONTAL_POSITIVE_SCROLL, ui::EMC_HORIZONTAL_NEGATIVE_SCROLL, m_mouseVirtualEventMap); - } break; - - case input_mouse_event_t::EET_MOVEMENT: - { - requestMagnitudeUpdateWithScalar(0.f, mouseEvent.movementEvent.relativeMovementX, std::abs(mouseEvent.movementEvent.relativeMovementX), ui::EMC_RELATIVE_POSITIVE_MOVEMENT_X, ui::EMC_RELATIVE_NEGATIVE_MOVEMENT_X, m_mouseVirtualEventMap); - requestMagnitudeUpdateWithScalar(0.f, mouseEvent.movementEvent.relativeMovementY, std::abs(mouseEvent.movementEvent.relativeMovementY), ui::EMC_RELATIVE_POSITIVE_MOVEMENT_Y, ui::EMC_RELATIVE_NEGATIVE_MOVEMENT_Y, m_mouseVirtualEventMap); - } break; - - default: - break; + case input_mouse_event_t::EET_CLICK: + updateMouseButtonState(map, mouseEvent.clickEvent); + break; + + case input_mouse_event_t::EET_SCROLL: + requestMagnitudeUpdateWithSignedComponents( + ZeroPivot, + hlsl::float32_t2( + static_cast(mouseEvent.scrollEvent.verticalScroll), + mouseEvent.scrollEvent.horizontalScroll), + SInputProcessorBindingGroups::MouseScroll, + map); + break; + + case input_mouse_event_t::EET_MOVEMENT: + requestMagnitudeUpdateWithSignedComponents( + ZeroPivot, + hlsl::float32_t2( + mouseEvent.movementEvent.relativeMovementX, + mouseEvent.movementEvent.relativeMovementY), + SInputProcessorBindingGroups::MouseRelativeMovement, + map); + break; + + default: + break; + } } - } - - postprocess(m_mouseVirtualEventMap, output, count); - } + }); } /** @@ -264,49 +225,181 @@ class IGimbalInputProcessor : public CGimbalBindingLayoutStorage */ void processImguizmo(gimbal_event_t* output, uint32_t& count, std::span events) { - count = 0u; - const auto mappedVirtualEventsCount = m_imguizmoVirtualEventMap.size(); + processBindingMap( + m_imguizmoVirtualEventMap, + output, + count, + [&](auto& map) + { + for (const auto& ev : events) + { + const auto& deltaWorldTRS = ev; + + hlsl::SRigidTransformComponents world = {}; + if (!hlsl::tryExtractRigidTransformComponents(deltaWorldTRS, world)) + continue; + + requestMagnitudeUpdateWithSignedComponents( + ZeroPivot, + world.translation, + SInputProcessorBindingGroups::ImguizmoTranslation, + map); + + const auto dRotationRad = hlsl::getCameraOrientationEulerRadians(world.orientation); + requestMagnitudeUpdateWithSignedComponents( + ZeroPivot, + dRotationRad, + SInputProcessorBindingGroups::ImguizmoRotation, + map); + + requestMagnitudeUpdateWithSignedComponents( + UnitPivot, + world.scale, + SInputProcessorBindingGroups::ImguizmoScale, + map); + } + }); + } + +private: + template + struct SEncodedAxisBindingGroup final + { + std::array positive = {}; + std::array negative = {}; + }; + struct SInputProcessorBindingGroups final + { + static inline constexpr SEncodedAxisBindingGroup MouseScroll = { + .positive = { + ui::EMC_VERTICAL_POSITIVE_SCROLL, + ui::EMC_HORIZONTAL_POSITIVE_SCROLL + }, + .negative = { + ui::EMC_VERTICAL_NEGATIVE_SCROLL, + ui::EMC_HORIZONTAL_NEGATIVE_SCROLL + } + }; + + static inline constexpr SEncodedAxisBindingGroup MouseRelativeMovement = { + .positive = { + ui::EMC_RELATIVE_POSITIVE_MOVEMENT_X, + ui::EMC_RELATIVE_POSITIVE_MOVEMENT_Y + }, + .negative = { + ui::EMC_RELATIVE_NEGATIVE_MOVEMENT_X, + ui::EMC_RELATIVE_NEGATIVE_MOVEMENT_Y + } + }; + + static inline constexpr SEncodedAxisBindingGroup ImguizmoTranslation = { + .positive = { + gimbal_event_t::MoveRight, + gimbal_event_t::MoveUp, + gimbal_event_t::MoveForward + }, + .negative = { + gimbal_event_t::MoveLeft, + gimbal_event_t::MoveDown, + gimbal_event_t::MoveBackward + } + }; + + static inline constexpr SEncodedAxisBindingGroup ImguizmoRotation = { + .positive = { + gimbal_event_t::TiltUp, + gimbal_event_t::PanRight, + gimbal_event_t::RollRight + }, + .negative = { + gimbal_event_t::TiltDown, + gimbal_event_t::PanLeft, + gimbal_event_t::RollLeft + } + }; + + static inline constexpr SEncodedAxisBindingGroup ImguizmoScale = { + .positive = { + gimbal_event_t::ScaleXInc, + gimbal_event_t::ScaleYInc, + gimbal_event_t::ScaleZInc + }, + .negative = { + gimbal_event_t::ScaleXDec, + gimbal_event_t::ScaleYDec, + gimbal_event_t::ScaleZDec + } + }; + }; + + static double clampFrameDeltaTimeMs( + const std::chrono::microseconds nextPresentationTimeStamp, + const std::chrono::microseconds lastVirtualUpTimeStamp) + { + const auto deltaMs = std::chrono::duration_cast( + nextPresentationTimeStamp - lastVirtualUpTimeStamp).count(); + if (deltaMs < 0) + return 0.0; + return std::min(static_cast(deltaMs), MaxFrameDeltaMs); + } + + template + void processBindingMap(Map& map, gimbal_event_t* output, uint32_t& count, ConsumeFn&& consume) + { + count = 0u; + const auto mappedVirtualEventsCount = static_cast(map.size()); if (!output) { count = mappedVirtualEventsCount; return; } + if (!mappedVirtualEventsCount) + return; - if (mappedVirtualEventsCount) + preprocess(map); + consume(map); + postprocess(map, output, count); + } + + static bool tryGetMouseButtonCode( + const ui::E_MOUSE_BUTTON button, + ui::E_MOUSE_CODE& outCode) + { + switch (button) { - preprocess(m_imguizmoVirtualEventMap); + case ui::EMB_LEFT_BUTTON: outCode = ui::EMC_LEFT_BUTTON; return true; + case ui::EMB_RIGHT_BUTTON: outCode = ui::EMC_RIGHT_BUTTON; return true; + case ui::EMB_MIDDLE_BUTTON: outCode = ui::EMC_MIDDLE_BUTTON; return true; + case ui::EMB_BUTTON_4: outCode = ui::EMC_BUTTON_4; return true; + case ui::EMB_BUTTON_5: outCode = ui::EMC_BUTTON_5; return true; + default: + return false; + } + } - for (const auto& ev : events) - { - const auto& deltaWorldTRS = ev; - - hlsl::SRigidTransformComponents world = {}; - if (!hlsl::tryExtractRigidTransformComponents(deltaWorldTRS, world)) - continue; - - // Delta translation impulse - requestMagnitudeUpdateWithScalar(0.f, world.translation[0], std::abs(world.translation[0]), gimbal_event_t::MoveRight, gimbal_event_t::MoveLeft, m_imguizmoVirtualEventMap); - requestMagnitudeUpdateWithScalar(0.f, world.translation[1], std::abs(world.translation[1]), gimbal_event_t::MoveUp, gimbal_event_t::MoveDown, m_imguizmoVirtualEventMap); - requestMagnitudeUpdateWithScalar(0.f, world.translation[2], std::abs(world.translation[2]), gimbal_event_t::MoveForward, gimbal_event_t::MoveBackward, m_imguizmoVirtualEventMap); - - // Delta rotation impulse - const auto dRotationRad = hlsl::getQuaternionEulerRadians(world.orientation); - requestMagnitudeUpdateWithScalar(0.f, dRotationRad[0], std::abs(dRotationRad[0]), gimbal_event_t::TiltUp , gimbal_event_t::TiltDown, m_imguizmoVirtualEventMap); - requestMagnitudeUpdateWithScalar(0.f, dRotationRad[1], std::abs(dRotationRad[1]), gimbal_event_t::PanRight, gimbal_event_t::PanLeft, m_imguizmoVirtualEventMap); - requestMagnitudeUpdateWithScalar(0.f, dRotationRad[2], std::abs(dRotationRad[2]), gimbal_event_t::RollRight, gimbal_event_t::RollLeft, m_imguizmoVirtualEventMap); - - // Delta scale impulse - requestMagnitudeUpdateWithScalar(1.f, world.scale[0], std::abs(world.scale[0]), gimbal_event_t::ScaleXInc, gimbal_event_t::ScaleXDec, m_imguizmoVirtualEventMap); - requestMagnitudeUpdateWithScalar(1.f, world.scale[1], std::abs(world.scale[1]), gimbal_event_t::ScaleYInc, gimbal_event_t::ScaleYDec, m_imguizmoVirtualEventMap); - requestMagnitudeUpdateWithScalar(1.f, world.scale[2], std::abs(world.scale[2]), gimbal_event_t::ScaleZInc, gimbal_event_t::ScaleZDec, m_imguizmoVirtualEventMap); - } + template + void updateMouseButtonState(Map& map, const input_mouse_event_t::SClickEvent& clickEvent) + { + ui::E_MOUSE_CODE mouseCode = ui::EMC_NONE; + if (!tryGetMouseButtonCode(clickEvent.mouseButton, mouseCode)) + return; - postprocess(m_imguizmoVirtualEventMap, output, count); - } + if (clickEvent.action == input_mouse_event_t::SClickEvent::EA_PRESSED) + setBindingActiveState(map, mouseCode, true); + else if (clickEvent.action == input_mouse_event_t::SClickEvent::EA_RELEASED) + setBindingActiveState(map, mouseCode, false); } -private: + template + void setBindingActiveState(Map& map, const Code code, const bool active) + { + const auto request = map.find(code); + if (request == map.end()) + return; + + request->second.active = active; + } void preprocess(auto& map) { @@ -332,10 +425,11 @@ class IGimbalInputProcessor : public CGimbalBindingLayoutStorage } template - void requestMagnitudeUpdateWithScalar(float signPivot, float dScalar, float dMagnitude, EncodeType positive, EncodeType negative, Map& map) + void requestMagnitudeUpdateWithScalar(float signPivot, float dScalar, EncodeType positive, EncodeType negative, Map& map) { if (dScalar != signPivot) { + const auto dMagnitude = hlsl::abs(dScalar); auto code = (dScalar > signPivot) ? positive : negative; auto request = map.find(code); if (request != map.end()) @@ -343,6 +437,33 @@ class IGimbalInputProcessor : public CGimbalBindingLayoutStorage } } + template + void requestMagnitudeUpdateWithSignedComponents( + float signPivot, + const hlsl::vector& components, + const std::array& positive, + const std::array& negative, + Map& map) + { + for (uint32_t i = 0u; i < N; ++i) + requestMagnitudeUpdateWithScalar(signPivot, components[i], positive[i], negative[i], map); + } + + template + void requestMagnitudeUpdateWithSignedComponents( + float signPivot, + const hlsl::vector& components, + const SEncodedAxisBindingGroup& bindings, + Map& map) + { + requestMagnitudeUpdateWithSignedComponents( + signPivot, + components, + bindings.positive, + bindings.negative, + map); + } + double m_frameDeltaTime = {}; std::chrono::microseconds m_nextPresentationTimeStamp = {}, m_lastVirtualUpTimeStamp = {}; }; diff --git a/common/include/camera/IRange.hpp b/common/include/camera/IRange.hpp index 91a3d9ed7..4f1e40c23 100644 --- a/common/include/camera/IRange.hpp +++ b/common/include/camera/IRange.hpp @@ -4,6 +4,7 @@ namespace nbl::core { +//! Minimal concepts used by camera persistence and tooling helpers. template concept GeneralPurposeRange = requires { @@ -17,4 +18,4 @@ std::same_as, T>; } // namespace nbl::core -#endif // _NBL_IRANGE_HPP_ \ No newline at end of file +#endif // _NBL_IRANGE_HPP_ diff --git a/common/include/camera/README.md b/common/include/camera/README.md index 3ccc82139..bd8fbc50c 100644 --- a/common/include/camera/README.md +++ b/common/include/camera/README.md @@ -84,7 +84,7 @@ It also keeps camera semantics inside the camera type: - `Orbit` means orbit - `FPS` means FPS -- `Path` means path-driven camera +- `Path Rig` means target-relative cylindrical path rig instead of allowing every caller to overwrite camera internals arbitrarily. @@ -369,7 +369,7 @@ That is why: - `Chase` and `Dolly` currently stay on shared spherical state - `DollyZoom` has dynamic perspective state -- `Path` has path state +- `Path Rig` has target-relative cylindrical path state ## Follow model 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/src/nbl/examples/camera/CCameraJsonPersistenceUtilities.hpp b/common/src/nbl/examples/camera/CCameraJsonPersistenceUtilities.hpp new file mode 100644 index 000000000..0195e19c5 --- /dev/null +++ b/common/src/nbl/examples/camera/CCameraJsonPersistenceUtilities.hpp @@ -0,0 +1,97 @@ +#ifndef _NBL_EXAMPLES_CAMERA_JSON_PERSISTENCE_UTILITIES_HPP_INCLUDED_ +#define _NBL_EXAMPLES_CAMERA_JSON_PERSISTENCE_UTILITIES_HPP_INCLUDED_ + +#include + +#include "camera/CCameraFileUtilities.hpp" +#include "camera/CCameraGoal.hpp" +#include "camera/CCameraPresetFlow.hpp" +#include "nlohmann/json.hpp" + +namespace nbl::system +{ + +template +inline void deserializeGoalJson(const Json& entry, core::CCameraGoal& goal) +{ + goal = {}; + + if (entry.contains("camera_kind")) + goal.sourceKind = static_cast(entry["camera_kind"].get()); + if (entry.contains("camera_capabilities")) + goal.sourceCapabilities = entry["camera_capabilities"].get(); + if (entry.contains("camera_goal_state_mask")) + goal.sourceGoalStateMask = entry["camera_goal_state_mask"].get(); + + if (entry.contains("position") && entry["position"].is_array()) + { + const auto values = entry["position"].get>(); + goal.position = hlsl::float64_t3(values[0], values[1], values[2]); + } + if (entry.contains("orientation") && entry["orientation"].is_array()) + { + const auto values = entry["orientation"].get>(); + goal.orientation = hlsl::makeQuaternionFromComponents(values[0], values[1], values[2], values[3]); + } + if (entry.contains("target_position") && entry["target_position"].is_array()) + { + const auto values = entry["target_position"].get>(); + goal.targetPosition = hlsl::float64_t3(values[0], values[1], values[2]); + goal.hasTargetPosition = true; + } + if (entry.contains("distance")) + { + goal.distance = entry["distance"].get(); + goal.hasDistance = true; + } + if (entry.contains("orbit_u")) + { + goal.orbitU = entry["orbit_u"].get(); + goal.hasOrbitState = true; + } + if (entry.contains("orbit_v")) + { + goal.orbitV = entry["orbit_v"].get(); + goal.hasOrbitState = true; + } + if (entry.contains("orbit_distance")) + { + goal.orbitDistance = entry["orbit_distance"].get(); + goal.hasOrbitState = true; + } + if (entry.contains("path_angle") && entry.contains("path_radius") && entry.contains("path_height")) + { + goal.pathState.angle = entry["path_angle"].get(); + goal.pathState.radius = entry["path_radius"].get(); + goal.pathState.height = entry["path_height"].get(); + goal.hasPathState = true; + } + if (entry.contains("dynamic_base_fov")) + { + goal.dynamicPerspectiveState.baseFov = entry["dynamic_base_fov"].get(); + goal.hasDynamicPerspectiveState = true; + } + if (entry.contains("dynamic_reference_distance")) + { + goal.dynamicPerspectiveState.referenceDistance = entry["dynamic_reference_distance"].get(); + goal.hasDynamicPerspectiveState = true; + } +} + +template +inline void deserializePresetJson(const Json& entry, core::CCameraPreset& preset) +{ + preset = {}; + if (entry.contains("name")) + preset.name = entry["name"].get(); + if (entry.contains("identifier")) + preset.identifier = entry["identifier"].get(); + + core::CCameraGoal goal = {}; + deserializeGoalJson(entry, goal); + core::assignGoalToPreset(preset, goal); +} + +} // namespace nbl::system + +#endif // _NBL_EXAMPLES_CAMERA_JSON_PERSISTENCE_UTILITIES_HPP_INCLUDED_ diff --git a/common/src/nbl/examples/camera/CCameraPersistence.cpp b/common/src/nbl/examples/camera/CCameraPersistence.cpp index fa959088a..f13f01e31 100644 --- a/common/src/nbl/examples/camera/CCameraPersistence.cpp +++ b/common/src/nbl/examples/camera/CCameraPersistence.cpp @@ -5,8 +5,9 @@ #include "camera/CCameraPersistence.hpp" #include -#include +#include +#include "CCameraJsonPersistenceUtilities.hpp" #include "nlohmann/json.hpp" namespace @@ -52,76 +53,6 @@ json_t serializeGoalJson(const nbl::core::CCameraGoal& goal) return json; } -void deserializeGoalJson(const json_t& entry, nbl::core::CCameraGoal& goal) -{ - goal = {}; - - if (entry.contains("camera_kind")) - goal.sourceKind = static_cast(entry["camera_kind"].get()); - if (entry.contains("camera_capabilities")) - goal.sourceCapabilities = entry["camera_capabilities"].get(); - if (entry.contains("camera_goal_state_mask")) - goal.sourceGoalStateMask = entry["camera_goal_state_mask"].get(); - - if (entry.contains("position") && entry["position"].is_array()) - { - const auto values = entry["position"].get>(); - goal.position = nbl::hlsl::float64_t3(values[0], values[1], values[2]); - } - if (entry.contains("orientation") && entry["orientation"].is_array()) - { - const auto values = entry["orientation"].get>(); - goal.orientation = nbl::hlsl::makeQuaternionFromComponents( - values[0], - values[1], - values[2], - values[3]); - } - if (entry.contains("target_position") && entry["target_position"].is_array()) - { - const auto values = entry["target_position"].get>(); - goal.targetPosition = nbl::hlsl::float64_t3(values[0], values[1], values[2]); - goal.hasTargetPosition = true; - } - if (entry.contains("distance")) - { - goal.distance = entry["distance"].get(); - goal.hasDistance = true; - } - if (entry.contains("orbit_u")) - { - goal.orbitU = entry["orbit_u"].get(); - goal.hasOrbitState = true; - } - if (entry.contains("orbit_v")) - { - goal.orbitV = entry["orbit_v"].get(); - goal.hasOrbitState = true; - } - if (entry.contains("orbit_distance")) - { - goal.orbitDistance = entry["orbit_distance"].get(); - goal.hasOrbitState = true; - } - if (entry.contains("path_angle") && entry.contains("path_radius") && entry.contains("path_height")) - { - goal.pathState.angle = entry["path_angle"].get(); - goal.pathState.radius = entry["path_radius"].get(); - goal.pathState.height = entry["path_height"].get(); - goal.hasPathState = true; - } - if (entry.contains("dynamic_base_fov")) - { - goal.dynamicPerspectiveState.baseFov = entry["dynamic_base_fov"].get(); - goal.hasDynamicPerspectiveState = true; - } - if (entry.contains("dynamic_reference_distance")) - { - goal.dynamicPerspectiveState.referenceDistance = entry["dynamic_reference_distance"].get(); - goal.hasDynamicPerspectiveState = true; - } -} - json_t serializePresetJson(const nbl::core::CCameraPreset& preset) { auto json = serializeGoalJson(nbl::core::makeGoalFromPreset(preset)); @@ -130,19 +61,6 @@ json_t serializePresetJson(const nbl::core::CCameraPreset& preset) return json; } -void deserializePresetJson(const json_t& entry, nbl::core::CCameraPreset& preset) -{ - preset = {}; - if (entry.contains("name")) - preset.name = entry["name"].get(); - if (entry.contains("identifier")) - preset.identifier = entry["identifier"].get(); - - nbl::core::CCameraGoal goal; - deserializeGoalJson(entry, goal); - nbl::core::assignGoalToPreset(preset, goal); -} - json_t serializeKeyframeTrackJson(const nbl::core::CCameraKeyframeTrack& track) { json_t root; @@ -169,7 +87,7 @@ bool deserializeKeyframeTrackJson(const json_t& root, nbl::core::CCameraKeyframe nbl::core::CCameraKeyframe keyframe; if (entry.contains("time")) keyframe.time = std::max(0.f, entry["time"].get()); - deserializePresetJson(entry, keyframe.preset); + nbl::system::deserializePresetJson(entry, keyframe.preset); track.keyframes.emplace_back(std::move(keyframe)); } @@ -197,7 +115,7 @@ bool deserializePresetCollectionJson(const json_t& root, std::vector> root; - deserializeGoalJson(root, goal); + nbl::system::deserializeGoalJson(root, goal); return true; } -bool saveGoalToFile(const path& filePath, const core::CCameraGoal& goal, const int indent) +bool saveGoalToFile(ISystem& system, const path& filePath, const core::CCameraGoal& goal, const int indent) { - std::ofstream out(filePath.string(), std::ios::binary); - return writeGoal(out, goal, indent); + std::ostringstream out; + if (!writeGoal(out, goal, indent)) + return false; + return writeTextFile(system, filePath, out.str()); } -bool loadGoalFromFile(const path& filePath, core::CCameraGoal& goal) +bool loadGoalFromFile(ISystem& system, const path& filePath, core::CCameraGoal& goal) { - std::ifstream in(filePath.string(), std::ios::binary); + std::string text; + if (!readTextFile(system, filePath, text)) + return false; + + std::istringstream in(text); return readGoal(in, goal); } @@ -257,19 +181,25 @@ bool readPreset(std::istream& in, core::CCameraPreset& preset) json_t root; in >> root; - deserializePresetJson(root, preset); + nbl::system::deserializePresetJson(root, preset); return true; } -bool savePresetToFile(const path& filePath, const core::CCameraPreset& preset, const int indent) +bool savePresetToFile(ISystem& system, const path& filePath, const core::CCameraPreset& preset, const int indent) { - std::ofstream out(filePath.string(), std::ios::binary); - return writePreset(out, preset, indent); + std::ostringstream out; + if (!writePreset(out, preset, indent)) + return false; + return writeTextFile(system, filePath, out.str()); } -bool loadPresetFromFile(const path& filePath, core::CCameraPreset& preset) +bool loadPresetFromFile(ISystem& system, const path& filePath, core::CCameraPreset& preset) { - std::ifstream in(filePath.string(), std::ios::binary); + std::string text; + if (!readTextFile(system, filePath, text)) + return false; + + std::istringstream in(text); return readPreset(in, preset); } @@ -292,15 +222,21 @@ bool readKeyframeTrack(std::istream& in, core::CCameraKeyframeTrack& track) return deserializeKeyframeTrackJson(root, track); } -bool saveKeyframeTrackToFile(const path& filePath, const core::CCameraKeyframeTrack& track, const int indent) +bool saveKeyframeTrackToFile(ISystem& system, const path& filePath, const core::CCameraKeyframeTrack& track, const int indent) { - std::ofstream out(filePath.string(), std::ios::binary); - return writeKeyframeTrack(out, track, indent); + std::ostringstream out; + if (!writeKeyframeTrack(out, track, indent)) + return false; + return writeTextFile(system, filePath, out.str()); } -bool loadKeyframeTrackFromFile(const path& filePath, core::CCameraKeyframeTrack& track) +bool loadKeyframeTrackFromFile(ISystem& system, const path& filePath, core::CCameraKeyframeTrack& track) { - std::ifstream in(filePath.string(), std::ios::binary); + std::string text; + if (!readTextFile(system, filePath, text)) + return false; + + std::istringstream in(text); return readKeyframeTrack(in, track); } @@ -323,15 +259,21 @@ bool readPresetCollection(std::istream& in, std::vector& pr return deserializePresetCollectionJson(root, presets); } -bool savePresetCollectionToFile(const path& filePath, std::span presets, const int indent) +bool savePresetCollectionToFile(ISystem& system, const path& filePath, std::span presets, const int indent) { - std::ofstream out(filePath.string(), std::ios::binary); - return writePresetCollection(out, presets, indent); + std::ostringstream out; + if (!writePresetCollection(out, presets, indent)) + return false; + return writeTextFile(system, filePath, out.str()); } -bool loadPresetCollectionFromFile(const path& filePath, std::vector& presets) +bool loadPresetCollectionFromFile(ISystem& system, const path& filePath, std::vector& presets) { - std::ifstream in(filePath.string(), std::ios::binary); + std::string text; + if (!readTextFile(system, filePath, text)) + return false; + + std::istringstream in(text); return readPresetCollection(in, presets); } diff --git a/common/src/nbl/examples/camera/CCameraScriptedRuntimePersistence.cpp b/common/src/nbl/examples/camera/CCameraScriptedRuntimePersistence.cpp index f3e7a4cab..bc599a077 100644 --- a/common/src/nbl/examples/camera/CCameraScriptedRuntimePersistence.cpp +++ b/common/src/nbl/examples/camera/CCameraScriptedRuntimePersistence.cpp @@ -6,99 +6,17 @@ #include #include -#include +#include #include #include +#include "CCameraJsonPersistenceUtilities.hpp" #include "nlohmann/json.hpp" namespace { using json_t = nlohmann::json; -void deserializeGoalJson(const json_t& entry, nbl::core::CCameraGoal& goal) -{ - goal = {}; - - if (entry.contains("camera_kind")) - goal.sourceKind = static_cast(entry["camera_kind"].get()); - if (entry.contains("camera_capabilities")) - goal.sourceCapabilities = entry["camera_capabilities"].get(); - if (entry.contains("camera_goal_state_mask")) - goal.sourceGoalStateMask = entry["camera_goal_state_mask"].get(); - - if (entry.contains("position") && entry["position"].is_array()) - { - const auto values = entry["position"].get>(); - goal.position = nbl::hlsl::float64_t3(values[0], values[1], values[2]); - } - if (entry.contains("orientation") && entry["orientation"].is_array()) - { - const auto values = entry["orientation"].get>(); - goal.orientation = nbl::hlsl::makeQuaternionFromComponents( - values[0], - values[1], - values[2], - values[3]); - } - if (entry.contains("target_position") && entry["target_position"].is_array()) - { - const auto values = entry["target_position"].get>(); - goal.targetPosition = nbl::hlsl::float64_t3(values[0], values[1], values[2]); - goal.hasTargetPosition = true; - } - if (entry.contains("distance")) - { - goal.distance = entry["distance"].get(); - goal.hasDistance = true; - } - if (entry.contains("orbit_u")) - { - goal.orbitU = entry["orbit_u"].get(); - goal.hasOrbitState = true; - } - if (entry.contains("orbit_v")) - { - goal.orbitV = entry["orbit_v"].get(); - goal.hasOrbitState = true; - } - if (entry.contains("orbit_distance")) - { - goal.orbitDistance = entry["orbit_distance"].get(); - goal.hasOrbitState = true; - } - if (entry.contains("path_angle") && entry.contains("path_radius") && entry.contains("path_height")) - { - goal.pathState.angle = entry["path_angle"].get(); - goal.pathState.radius = entry["path_radius"].get(); - goal.pathState.height = entry["path_height"].get(); - goal.hasPathState = true; - } - if (entry.contains("dynamic_base_fov")) - { - goal.dynamicPerspectiveState.baseFov = entry["dynamic_base_fov"].get(); - goal.hasDynamicPerspectiveState = true; - } - if (entry.contains("dynamic_reference_distance")) - { - goal.dynamicPerspectiveState.referenceDistance = entry["dynamic_reference_distance"].get(); - goal.hasDynamicPerspectiveState = true; - } -} - -void deserializePresetJson(const json_t& entry, nbl::core::CCameraPreset& preset) -{ - preset = {}; - if (entry.contains("name")) - preset.name = entry["name"].get(); - if (entry.contains("identifier")) - preset.identifier = entry["identifier"].get(); - - nbl::core::CCameraGoal goal; - deserializeGoalJson(entry, goal); - nbl::core::assignGoalToPreset(preset, goal); -} - bool tryParseCaptureFractionJson(const json_t& entry, float& outFraction) { if (entry.is_number()) @@ -318,7 +236,7 @@ bool deserializeSequenceKeyframeJson(const json_t& root, nbl::core::CCameraSeque if (root.contains("preset")) { - deserializePresetJson(root["preset"], out.absolutePreset); + nbl::system::deserializePresetJson(root["preset"], out.absolutePreset); out.hasAbsolutePreset = true; } else if (root.contains("position") || root.contains("orientation") || root.contains("target_position") || @@ -326,7 +244,7 @@ bool deserializeSequenceKeyframeJson(const json_t& root, nbl::core::CCameraSeque root.contains("orbit_distance") || root.contains("path_angle") || root.contains("path_radius") || root.contains("path_height") || root.contains("dynamic_base_fov") || root.contains("dynamic_reference_distance")) { - deserializePresetJson(root, out.absolutePreset); + nbl::system::deserializePresetJson(root, out.absolutePreset); out.hasAbsolutePreset = true; } @@ -1151,17 +1069,19 @@ bool readCameraSequenceScript(std::istream& in, core::CCameraSequenceScript& out return deserializeCameraSequenceScriptJson(root, out, error); } -bool loadCameraSequenceScriptFromFile(const path& filePath, core::CCameraSequenceScript& out, std::string* error) +bool readCameraSequenceScript(std::string_view text, core::CCameraSequenceScript& out, std::string* error) { - std::ifstream in(filePath.string(), std::ios::binary); - if (!in.is_open()) - { - if (error) - *error = "Cannot open camera sequence script file."; + std::istringstream stream{std::string(text)}; + return readCameraSequenceScript(stream, out, error); +} + +bool loadCameraSequenceScriptFromFile(ISystem& system, const path& filePath, core::CCameraSequenceScript& out, std::string* error) +{ + std::string text; + if (!readTextFile(system, filePath, text, error, "Cannot open camera sequence script file.")) return false; - } - return readCameraSequenceScript(in, out, error); + return readCameraSequenceScript(text, out, error); } bool readCameraScriptedInput(std::istream& in, CCameraScriptedInputParseResult& out, std::string* error) @@ -1222,17 +1142,19 @@ bool readCameraScriptedInput(std::istream& in, CCameraScriptedInputParseResult& return true; } -bool loadCameraScriptedInputFromFile(const path& filePath, CCameraScriptedInputParseResult& out, std::string* error) +bool readCameraScriptedInput(std::string_view text, CCameraScriptedInputParseResult& out, std::string* error) { - std::ifstream in(filePath.string(), std::ios::binary); - if (!in.is_open()) - { - if (error) - *error = "Cannot open scripted input file."; + std::istringstream stream{std::string(text)}; + return readCameraScriptedInput(stream, out, error); +} + +bool loadCameraScriptedInputFromFile(ISystem& system, const path& filePath, CCameraScriptedInputParseResult& out, std::string* error) +{ + std::string text; + if (!readTextFile(system, filePath, text, error, "Cannot open scripted input file.")) return false; - } - return readCameraScriptedInput(in, out, error); + return readCameraScriptedInput(text, out, error); } } // namespace nbl::system From c83678708d6cf9fa190f7058bc94979408b92dc4 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Thu, 9 Apr 2026 10:22:11 +0200 Subject: [PATCH 182/205] Save WIP cleanup checkpoint --- 61_UI/AppCameraConfiguration.cpp | 2 +- 61_UI/AppCameraPlanarRuntime.cpp | 2 +- 61_UI/AppCameraUiState.cpp | 52 +- 61_UI/AppControlPanelCameraTab.cpp | 6 +- 61_UI/AppFollowRuntime.cpp | 43 +- 61_UI/AppHeadlessCameraSmoke.cpp | 2 +- 61_UI/AppHeadlessCameraSmokeChecks.inl | 16 +- 61_UI/AppHeadlessCameraSmokeHelpers.inl | 62 +- 61_UI/AppPresentationResources.cpp | 8 +- 61_UI/AppPresetPlayback.cpp | 2 +- 61_UI/AppScriptedInitialization.cpp | 6 +- 61_UI/AppScriptedValidation.cpp | 8 +- 61_UI/include/common.hpp | 3 +- .../CCameraFollowRegressionUtilities.hpp | 416 +++++++------ .../include/camera/CCameraFollowUtilities.hpp | 428 ++++++------- common/include/camera/CCameraGoal.hpp | 585 +++++++++-------- common/include/camera/CCameraGoalAnalysis.hpp | 54 +- common/include/camera/CCameraGoalSolver.hpp | 29 +- .../include/camera/CCameraKeyframeTrack.hpp | 2 +- .../include/camera/CCameraPathUtilities.hpp | 514 ++++++++------- .../camera/CCameraPresentationUtilities.hpp | 4 +- common/include/camera/CCameraPreset.hpp | 6 +- common/include/camera/CCameraPresetFlow.hpp | 6 +- .../camera/CCameraScriptedCheckRunner.hpp | 6 +- .../include/camera/CCameraSequenceScript.hpp | 589 +++++++++--------- .../camera/CCameraSequenceScriptedBuilder.hpp | 4 +- .../camera/CCameraTargetRelativeUtilities.hpp | 276 ++++---- .../CCameraViewportOverlayUtilities.hpp | 2 +- common/include/camera/CPathCamera.hpp | 22 +- .../include/camera/CSphericalTargetCamera.hpp | 19 +- common/include/camera/ICamera.hpp | 3 +- common/include/camera/README.md | 2 +- .../CCameraJsonPersistenceUtilities.hpp | 4 +- .../examples/camera/CCameraPersistence.cpp | 4 +- .../CCameraScriptedRuntimePersistence.cpp | 12 +- 35 files changed, 1602 insertions(+), 1597 deletions(-) diff --git a/61_UI/AppCameraConfiguration.cpp b/61_UI/AppCameraConfiguration.cpp index ae5ff6ff6..dd0e9ddbd 100644 --- a/61_UI/AppCameraConfiguration.cpp +++ b/61_UI/AppCameraConfiguration.cpp @@ -103,7 +103,7 @@ void App::initializePlanarFollowConfigs() for (uint32_t planarIx = 0u; planarIx < m_planarProjections.size(); ++planarIx) { auto* camera = m_planarProjections[planarIx] ? m_planarProjections[planarIx]->getCamera() : nullptr; - auto config = nbl::core::makeDefaultFollowConfig(camera); + auto config = nbl::core::CCameraFollowUtilities::makeDefaultFollowConfig(camera); m_sceneInteraction.planarFollowConfigs.emplace_back(config); if (config.enabled) captureFollowOffsetsForPlanar(planarIx); diff --git a/61_UI/AppCameraPlanarRuntime.cpp b/61_UI/AppCameraPlanarRuntime.cpp index 5d3ca174b..4b4c7d865 100644 --- a/61_UI/AppCameraPlanarRuntime.cpp +++ b/61_UI/AppCameraPlanarRuntime.cpp @@ -43,7 +43,7 @@ bool tryCaptureInitialPlanarPresets( { auto* camera = planars[planarIx] ? planars[planarIx]->getCamera() : nullptr; const std::string presetName = "Planar " + std::to_string(planarIx); - const auto captureAnalysis = core::analyzeCameraCapture(goalSolver, camera); + const auto captureAnalysis = core::CCameraGoalAnalysisUtilities::analyzeCameraCapture(goalSolver, camera); if (!captureAnalysis.canCapture) { const auto kindLabel = camera ? std::string(core::getCameraKindLabel(camera->getKind())) : std::string("Unknown"); diff --git a/61_UI/AppCameraUiState.cpp b/61_UI/AppCameraUiState.cpp index 777542e03..87135a0d8 100644 --- a/61_UI/AppCameraUiState.cpp +++ b/61_UI/AppCameraUiState.cpp @@ -1,6 +1,33 @@ #include "app/App.hpp" #include "app/AppResourceUtilities.hpp" +namespace +{ + +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]; +} + +} // namespace + nbl::system::SCameraAppResourceContext App::getCameraAppResourceContext() const { return m_system ? @@ -23,10 +50,9 @@ uint32_t App::getActivePlanarIx() const SCameraFollowConfig* App::getActiveFollowConfig() { - const auto planarIx = getActivePlanarIx(); - if (planarIx >= m_sceneInteraction.planarFollowConfigs.size()) - return nullptr; - return &m_sceneInteraction.planarFollowConfigs[planarIx]; + return tryGetViewportFollowConfig( + std::span(m_sceneInteraction.planarFollowConfigs.data(), m_sceneInteraction.planarFollowConfigs.size()), + tryGetActiveViewportRuntimeState()); } const SCameraFollowConfig* App::getActiveFollowConfig() const @@ -50,31 +76,27 @@ SActiveViewportRuntimeState App::tryGetActiveViewportRuntimeState() bool App::tryBuildActiveCameraInputContext(SActiveCameraInputContext& outContext) { - outContext = {}; - outContext.viewport = tryGetActiveViewportRuntimeState(); - return outContext.valid(); + return tryBindActiveViewportContext(outContext, tryGetActiveViewportRuntimeState()); } bool App::tryBuildActiveProjectionTabContext(SActiveProjectionTabContext& outContext) { - outContext = {}; - outContext.viewport = tryGetActiveViewportRuntimeState(); - if (!outContext.valid()) + if (!tryBindActiveViewportContext(outContext, tryGetActiveViewportRuntimeState())) return false; outContext.activeRenderWindowIxString = std::to_string(m_viewports.activeRenderWindowIx); - outContext.activePlanarIxString = std::to_string(outContext.viewport.binding->activePlanarIx); + outContext.activePlanarIxString = std::to_string(outContext.requireBinding().activePlanarIx); return true; } bool App::tryBuildActiveScriptedCameraContext(SActiveScriptedCameraContext& outContext) { - outContext = {}; - outContext.viewport = tryGetActiveViewportRuntimeState(); - if (!outContext.valid()) + if (!tryBindActiveViewportContext(outContext, tryGetActiveViewportRuntimeState())) return false; - outContext.followConfig = getActiveFollowConfig(); + 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, diff --git a/61_UI/AppControlPanelCameraTab.cpp b/61_UI/AppControlPanelCameraTab.cpp index 3ad0fe1a3..66b51c43f 100644 --- a/61_UI/AppControlPanelCameraTab.cpp +++ b/61_UI/AppControlPanelCameraTab.cpp @@ -111,7 +111,7 @@ void App::drawControlPanelCameraTab(const nbl::ui::SCameraControlPanelStyle& pan followConfig.mode = static_cast(followModeIx); const bool followStateChanged = followConfig.enabled != prevFollowEnabled || followConfig.mode != prevFollowMode; - if (followStateChanged && followConfig.enabled && nbl::core::cameraFollowModeUsesCapturedOffset(followConfig.mode)) + if (followStateChanged && followConfig.enabled && nbl::core::CCameraFollowUtilities::cameraFollowModeUsesCapturedOffset(followConfig.mode)) captureFollowOffsetsForPlanar(getActivePlanarIx()); if (followStateChanged && followConfig.enabled) applyFollowToConfiguredCameras(); @@ -134,13 +134,13 @@ void App::drawControlPanelCameraTab(const nbl::ui::SCameraControlPanelStyle& pan if (nbl::ui::drawActionButtonWithHint("Capture current offset", "Store current camera-to-target relation into the active follow config")) captureFollowOffsetsForPlanar(getActivePlanarIx()); - if (cameraFollowModeUsesWorldOffset(followConfig.mode)) + if (CCameraFollowUtilities::cameraFollowModeUsesWorldOffset(followConfig.mode)) { auto worldOffset = getCastedVector(followConfig.worldOffset); if (ImGui::InputFloat3("World offset", &worldOffset[0])) followConfig.worldOffset = getCastedVector(worldOffset); } - if (cameraFollowModeUsesLocalOffset(followConfig.mode)) + if (CCameraFollowUtilities::cameraFollowModeUsesLocalOffset(followConfig.mode)) { auto localOffset = getCastedVector(followConfig.localOffset); if (ImGui::InputFloat3("Local offset", &localOffset[0])) diff --git a/61_UI/AppFollowRuntime.cpp b/61_UI/AppFollowRuntime.cpp index 80e5f1e57..0b6355d28 100644 --- a/61_UI/AppFollowRuntime.cpp +++ b/61_UI/AppFollowRuntime.cpp @@ -23,25 +23,6 @@ inline uint64_t computeProgressFrames(const uint64_t elapsedFrames, const uint64 return holdFrames ? std::min(elapsedFrames, holdFrames) : elapsedFrames; } -inline bool ensureScriptVisualDebugCamera( - const std::vector>& planarProjections, - std::span windowBindings, - const uint32_t activeRenderWindowIx, - SWindowControlBinding*& outBinding, - ICamera*& outCamera) -{ - outBinding = nullptr; - outCamera = nullptr; - - SActiveViewportRuntimeState viewportState = {}; - if (!nbl::ui::tryBuildActiveViewportRuntimeState(planarProjections, windowBindings, activeRenderWindowIx, viewportState)) - return false; - - outBinding = viewportState.binding; - outCamera = viewportState.camera; - return true; -} - inline nbl::ui::SCameraScriptVisualDebugStatus buildScriptVisualDebugStatus( const ICamera& camera, const uint32_t planarIx, @@ -109,7 +90,7 @@ bool App::captureFollowOffsetsForPlanar(const uint32_t planarIx) return false; auto* camera = m_planarProjections[planarIx] ? m_planarProjections[planarIx]->getCamera() : nullptr; - return nbl::core::captureFollowOffsetsFromCamera( + return nbl::core::CCameraFollowUtilities::captureFollowOffsetsFromCamera( m_cameraGoalSolver, camera, m_sceneInteraction.followTarget, @@ -118,7 +99,7 @@ bool App::captureFollowOffsetsForPlanar(const uint32_t planarIx) bool App::followConfigUsesCapturedOffset(const SCameraFollowConfig& config) const { - return config.enabled && nbl::core::cameraFollowModeUsesCapturedOffset(config.mode); + return config.enabled && nbl::core::CCameraFollowUtilities::cameraFollowModeUsesCapturedOffset(config.mode); } void App::refreshFollowOffsetConfigForPlanar(const uint32_t planarIx) @@ -134,7 +115,7 @@ void App::refreshFollowOffsetConfigForPlanar(const uint32_t planarIx) if (!camera) return; - nbl::core::captureFollowOffsetsFromCamera(m_cameraGoalSolver, camera, m_sceneInteraction.followTarget, config); + nbl::core::CCameraFollowUtilities::captureFollowOffsetsFromCamera(m_cameraGoalSolver, camera, m_sceneInteraction.followTarget, config); } void App::refreshFollowOffsetConfigsForCamera(ICamera* camera) @@ -196,7 +177,7 @@ void App::applyFollowToConfiguredCameras(const bool allowDuringScriptedInput) if (!config.enabled || config.mode == ECameraFollowMode::Disabled) continue; - const auto result = nbl::core::applyFollowToCamera(m_cameraGoalSolver, camera, m_sceneInteraction.followTarget, config); + const auto result = nbl::core::CCameraFollowUtilities::applyFollowToCamera(m_cameraGoalSolver, camera, m_sceneInteraction.followTarget, config); if (!result.succeeded()) continue; @@ -262,26 +243,20 @@ void App::drawScriptVisualDebugOverlay(const ImVec2& displaySize) if (!(m_scriptedInput.enabled && m_scriptedInput.visualDebug)) return; - SWindowControlBinding* activeBinding = nullptr; - ICamera* camera = nullptr; - if (!ensureScriptVisualDebugCamera( - m_planarProjections, - std::span(m_viewports.windowBindings.data(), m_viewports.windowBindings.size()), - m_viewports.activeRenderWindowIx, - activeBinding, - camera)) + const auto viewportState = tryGetActiveViewportRuntimeState(); + if (!viewportState.valid()) return; if (!m_scriptedInput.visualPlanar.valid) { m_scriptedInput.visualPlanar.valid = true; - m_scriptedInput.visualPlanar.planarIx = activeBinding->activePlanarIx; + m_scriptedInput.visualPlanar.planarIx = viewportState.requireBinding().activePlanarIx; m_scriptedInput.visualPlanar.startFrame = m_realFrameIx; } const auto debugStatus = buildScriptVisualDebugStatus( - *camera, - activeBinding->activePlanarIx, + viewportState.requireCamera(), + viewportState.requireBinding().activePlanarIx, m_planarProjections.size(), m_realFrameIx, m_scriptedInput); diff --git a/61_UI/AppHeadlessCameraSmoke.cpp b/61_UI/AppHeadlessCameraSmoke.cpp index f95c8d0aa..1742e8475 100644 --- a/61_UI/AppHeadlessCameraSmoke.cpp +++ b/61_UI/AppHeadlessCameraSmoke.cpp @@ -88,7 +88,7 @@ bool App::runHeadlessCameraSmoke(argparse::ArgumentParser& program, smart_refctd resolvedSmokeState, { cameras.data(), cameras.size() }, { smokePlanars.data(), smokePlanars.size() }, - [](ICamera* camera) { return nbl::core::makeDefaultFollowConfig(camera); }, + [](ICamera* camera) { return nbl::core::CCameraFollowUtilities::makeDefaultFollowConfig(camera); }, [](const CTrackedTarget& trackedTarget, const std::string_view label, std::string& error) { return verifyFollowTargetMarkerAlignmentForSmoke(trackedTarget, label, error); diff --git a/61_UI/AppHeadlessCameraSmokeChecks.inl b/61_UI/AppHeadlessCameraSmokeChecks.inl index ca4023a27..0aa956a87 100644 --- a/61_UI/AppHeadlessCameraSmokeChecks.inl +++ b/61_UI/AppHeadlessCameraSmokeChecks.inl @@ -407,7 +407,7 @@ } sequence.segments.push_back(segment); - if (!nbl::core::sequenceScriptUsesMultiplePresentations(sequence)) + if (!nbl::core::CCameraSequenceScriptUtilities::sequenceScriptUsesMultiplePresentations(sequence)) { outError = "Sequence compile smoke failed to detect multi-presentation authored defaults."; return false; @@ -420,7 +420,7 @@ nbl::core::CCameraSequenceCompiledSegment compiledSegment; std::string compileError; - if (!nbl::core::compileSequenceSegmentFromReference( + if (!nbl::core::CCameraSequenceScriptUtilities::compileSequenceSegmentFromReference( sequence, sequence.segments.front(), state.initialPresets.orbit.value(), @@ -457,7 +457,7 @@ } std::vector framePolicies; - if (!nbl::core::buildCompiledSegmentFramePolicies(compiledSegment, framePolicies, true)) + if (!nbl::core::CCameraSequenceScriptUtilities::buildCompiledSegmentFramePolicies(compiledSegment, framePolicies, true)) { outError = "Sequence compile smoke failed to build shared frame policies."; return false; @@ -484,7 +484,7 @@ } CCameraSequenceTrackedTargetPose poseAtOne; - if (!nbl::core::tryBuildSequenceTrackedTargetPoseAtTime(compiledSegment.trackedTargetTrack, 1.f, poseAtOne)) + if (!nbl::core::CCameraSequenceScriptUtilities::tryBuildSequenceTrackedTargetPoseAtTime(compiledSegment.trackedTargetTrack, 1.f, poseAtOne)) { outError = "Sequence compile smoke failed to sample normalized tracked-target track."; return false; @@ -863,7 +863,7 @@ if (!defaultFollowCamera) continue; - auto followConfig = nbl::core::makeDefaultFollowConfig(defaultFollowCamera); + auto followConfig = nbl::core::CCameraFollowUtilities::makeDefaultFollowConfig(defaultFollowCamera); if (!followConfig.enabled || followConfig.mode == ECameraFollowMode::Disabled) continue; @@ -873,8 +873,8 @@ trackedTarget.setPose( SCameraSmokeFollowScenario::InitialTargetPosition, SCameraSmokeFollowScenario::InitialTargetOrientation); - if ((nbl::core::cameraFollowModeUsesLocalOffset(followConfig.mode) || nbl::core::cameraFollowModeUsesWorldOffset(followConfig.mode)) && - !nbl::core::captureFollowOffsetsFromCamera(state.goalSolver, defaultFollowCamera, trackedTarget, followConfig)) + 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; @@ -929,7 +929,7 @@ SCameraFollowConfig followConfig = {}; followConfig.enabled = true; followConfig.mode = ECameraFollowMode::KeepLocalOffset; - if (!nbl::core::captureFollowOffsetsFromCamera(state.goalSolver, state.chaseCamera, trackedTarget, followConfig)) + if (!nbl::core::CCameraFollowUtilities::captureFollowOffsetsFromCamera(state.goalSolver, state.chaseCamera, trackedTarget, followConfig)) { outError = "Chase follow smoke failed to capture local offset."; return false; diff --git a/61_UI/AppHeadlessCameraSmokeHelpers.inl b/61_UI/AppHeadlessCameraSmokeHelpers.inl index d96fb0abd..34bb9bdad 100644 --- a/61_UI/AppHeadlessCameraSmokeHelpers.inl +++ b/61_UI/AppHeadlessCameraSmokeHelpers.inl @@ -459,10 +459,10 @@ namespace std::string* const outError) { std::string regressionError; - if (nbl::system::buildApplyAndValidateFollowTargetContract( - solver, - camera, - trackedTarget, + if (nbl::system::CCameraFollowRegressionUtilities::buildApplyAndValidateFollowTargetContract( + solver, + camera, + trackedTarget, followConfig, outResult, ®ressionError, @@ -473,10 +473,10 @@ namespace return true; nbl::system::SCameraFollowRegressionResult postApplyRegression = {}; - if (!nbl::system::validateFollowTargetContract( - camera, - trackedTarget, - followConfig, + if (!nbl::system::CCameraFollowRegressionUtilities::validateFollowTargetContract( + camera, + trackedTarget, + followConfig, outResult.goal, postApplyRegression, ®ressionError, @@ -520,7 +520,7 @@ namespace *outError = std::string("Follow visual metrics smoke was inactive for ") + label + "."; return false; } - if (nbl::core::cameraFollowModeLocksViewToTarget(followConfig.mode) && !metrics.lockValid) + if (nbl::core::CCameraFollowUtilities::cameraFollowModeLocksViewToTarget(followConfig.mode) && !metrics.lockValid) { if (outError) *outError = std::string("Follow visual metrics smoke was missing lock metrics for ") + label + "."; @@ -607,7 +607,7 @@ namespace const std::string cameraIdentifier(camera->getIdentifier()); const auto initialPreset = nbl::core::capturePreset(goalSolver, camera, "smoke-initial"); - const auto initialCompatibility = nbl::core::analyzePresetApply(goalSolver, camera, initialPreset).compatibility; + 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 + @@ -878,7 +878,7 @@ namespace return true; const uint32_t expectedMissingGoalStateMask = expectedMissingGoalStateMaskForIssue(expectedIssue); - const auto compatibility = nbl::core::analyzePresetApply(goalSolver, targetCamera, sourcePreset).compatibility; + 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 + @@ -915,7 +915,7 @@ namespace if (!targetCamera) return true; - const auto compatibility = nbl::core::analyzePresetApply(goalSolver, targetCamera, sourcePreset).compatibility; + 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 + @@ -996,10 +996,10 @@ namespace { nbl::system::SCameraProjectionContext projectionContext = {}; const bool hasProjectionContext = nbl::ui::tryBuildCameraProjectionContext(planarProjections, camera, projectionContext); - return nbl::system::buildFollowVisualMetrics( - camera, - trackedTarget, - &followConfig, + return nbl::system::CCameraFollowRegressionUtilities::buildFollowVisualMetrics( + camera, + trackedTarget, + &followConfig, hasProjectionContext ? &projectionContext : nullptr); } @@ -1024,10 +1024,10 @@ namespace std::string regressionError; nbl::system::SCameraProjectionContext projectionContext = {}; const bool hasProjectionContext = nbl::ui::tryBuildCameraProjectionContext(planarProjections, camera, projectionContext); - if (nbl::system::validateFollowTargetContract( - camera, - trackedTarget, - followConfig, + if (nbl::system::CCameraFollowRegressionUtilities::validateFollowTargetContract( + camera, + trackedTarget, + followConfig, followGoal, regression, ®ressionError, @@ -1074,13 +1074,13 @@ namespace followConfig.enabled = true; followConfig.mode = ECameraFollowMode::KeepLocalOffset; - if (!nbl::core::captureFollowOffsetsFromCamera(goalSolver, camera, trackedTarget, followConfig)) + 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::applyFollowToCamera(goalSolver, camera, trackedTarget, followConfig); + 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) + "."; @@ -1094,14 +1094,14 @@ namespace return false; } - editedPreset.goal.orbitU = hlsl::wrapAngleRad( - editedPreset.goal.orbitU + hlsl::radians(SCameraSmokeFollowScenario::OrbitRecaptureDeltaDeg)); + editedPreset.goal.orbitUv.x = hlsl::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::canonicalizeGoal(editedPreset.goal); - if (!nbl::core::isGoalFinite(editedPreset.goal)) + 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; @@ -1117,20 +1117,20 @@ namespace const auto reachedEditedPreset = nbl::core::capturePreset(goalSolver, camera, std::string(label) + " reached"); - if (!nbl::core::captureFollowOffsetsFromCamera(goalSolver, camera, trackedTarget, followConfig)) + 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::tryBuildFollowGoal(goalSolver, camera, trackedTarget, followConfig, 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::applyFollowToCamera(goalSolver, camera, trackedTarget, followConfig); + 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) + @@ -1365,7 +1365,7 @@ namespace followConfig.enabled = true; followConfig.mode = ECameraFollowMode::OrbitTarget; CCameraGoal followGoal = {}; - if (!nbl::core::applyFollowToCamera(goalSolver, orbitCamera.get(), trackedTarget, followConfig, &followGoal).succeeded()) + 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."; @@ -1411,7 +1411,7 @@ namespace const auto goalBasis = getQuaternionBasisMatrix(followGoal.orientation); float lockAngle = 0.0f; double targetDistance = 0.0; - const bool hasLockMetrics = nbl::core::tryComputeFollowTargetLockMetrics(gimbal, trackedTarget, lockAngle, &targetDistance); + 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 diff --git a/61_UI/AppPresentationResources.cpp b/61_UI/AppPresentationResources.cpp index 694b5b78a..61efc2264 100644 --- a/61_UI/AppPresentationResources.cpp +++ b/61_UI/AppPresentationResources.cpp @@ -2,7 +2,7 @@ nbl::hlsl::uint32_t2 App::getPresentationRenderExtent() const { - if (m_cliRuntime.ciMode) + if (m_cliRuntime.ciMode && !m_cliRuntime.scriptVisualDebugCli) return SCameraAppPresentationDefaults::CiWindowExtent; const auto dpyInfo = m_winMgr->getPrimaryDisplayInfo(); @@ -11,7 +11,7 @@ nbl::hlsl::uint32_t2 App::getPresentationRenderExtent() const bool App::shouldMaximizePresentationWindow() const { - return !m_cliRuntime.ciMode; + return !m_cliRuntime.ciMode || m_cliRuntime.scriptVisualDebugCli; } core::vector App::getSurfaces() const @@ -105,7 +105,9 @@ bool App::initializePresentationResources() return logFail("Failed to Create a Renderpass!"); } - if (!m_surface || !m_surface->init(m_surface->pickQueue(m_device.get()), std::make_unique(), { .imageUsage = IGPUImage::EUF_TRANSFER_SRC_BIT })) + 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(); diff --git a/61_UI/AppPresetPlayback.cpp b/61_UI/AppPresetPlayback.cpp index 54d37c0ae..be3ac685a 100644 --- a/61_UI/AppPresetPlayback.cpp +++ b/61_UI/AppPresetPlayback.cpp @@ -21,7 +21,7 @@ App::CaptureUiAnalysis App::analyzeCameraCaptureForUi(ICamera* camera) const CCameraGoalSolver::SCompatibilityResult App::analyzePresetCompatibility(ICamera* camera, const CameraPreset& preset) const { - return nbl::core::analyzePresetApply(m_cameraGoalSolver, camera, preset).compatibility; + return nbl::core::CCameraGoalAnalysisUtilities::analyzePresetApply(m_cameraGoalSolver, camera, preset).compatibility; } bool App::presetMatchesFilter(ICamera* camera, const CameraPreset& preset) const diff --git a/61_UI/AppScriptedInitialization.cpp b/61_UI/AppScriptedInitialization.cpp index f39e717d4..37dc649bd 100644 --- a/61_UI/AppScriptedInitialization.cpp +++ b/61_UI/AppScriptedInitialization.cpp @@ -152,7 +152,7 @@ bool App::expandPendingScriptedSequence(const CCameraSequenceScript& sequence) CCameraScriptedTimeline timeline; resetScriptedInputRuntimeState(); - const bool useWindowMode = nbl::core::sequenceScriptUsesMultiplePresentations(sequence); + const bool useWindowMode = nbl::core::CCameraSequenceScriptUtilities::sequenceScriptUsesMultiplePresentations(sequence); nbl::system::appendScriptedActionEvent( timeline, 0u, @@ -179,14 +179,14 @@ bool App::expandPendingScriptedSequence(const CCameraSequenceScript& sequence) } const bool useTrackedTargetFollow = - nbl::core::sequenceSegmentUsesTrackedTargetTrack(segment) && + 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::compileSequenceSegmentFromReference( + if (!nbl::core::CCameraSequenceScriptUtilities::compileSequenceSegmentFromReference( sequence, segment, m_presetAuthoring.initialPlanarPresets[planarIx.value()], diff --git a/61_UI/AppScriptedValidation.cpp b/61_UI/AppScriptedValidation.cpp index c8b72a6a7..1f9789ccb 100644 --- a/61_UI/AppScriptedValidation.cpp +++ b/61_UI/AppScriptedValidation.cpp @@ -9,10 +9,10 @@ void App::updateScriptedFollowVisualState(const CCameraScriptedFrameEvents& scri SActiveScriptedCameraContext runtimeContext = {}; if (tryBuildActiveScriptedCameraContext(runtimeContext) && runtimeContext.followConfig) { - followMetrics = nbl::system::buildFollowVisualMetrics( - runtimeContext.viewport.camera, - m_sceneInteraction.followTarget, - runtimeContext.followConfig, + followMetrics = nbl::system::CCameraFollowRegressionUtilities::buildFollowVisualMetrics( + runtimeContext.viewport.camera, + m_sceneInteraction.followTarget, + runtimeContext.followConfig, runtimeContext.getProjectionContext()); } m_scriptedInput.visualFollow = followMetrics; diff --git a/61_UI/include/common.hpp b/61_UI/include/common.hpp index 9206c36ee..ecac143fa 100644 --- a/61_UI/include/common.hpp +++ b/61_UI/include/common.hpp @@ -208,8 +208,7 @@ using nbl::hlsl::mul; using nbl::hlsl::camera_quaternion_t; using nbl::hlsl::makeIdentityQuaternion; using nbl::hlsl::normalizeQuaternion; -using nbl::core::cameraFollowModeUsesLocalOffset; -using nbl::core::cameraFollowModeUsesWorldOffset; +using nbl::core::CCameraFollowUtilities; using nbl::core::syncDynamicPerspectiveProjection; using nbl::ui::applyDefaultCameraInputBindingPreset; using nbl::ui::getDefaultCameraMouseMappingPreset; diff --git a/common/include/camera/CCameraFollowRegressionUtilities.hpp b/common/include/camera/CCameraFollowRegressionUtilities.hpp index e0e3a60c8..a1a809729 100644 --- a/common/include/camera/CCameraFollowRegressionUtilities.hpp +++ b/common/include/camera/CCameraFollowRegressionUtilities.hpp @@ -82,16 +82,6 @@ struct SCameraFollowRegressionThresholds double scalarTolerance = DefaultScalarTolerance; }; -inline SCameraFollowRegressionThresholds makeFollowRegressionThresholds( - const float projectedNdcTolerance = SCameraFollowRegressionThresholds::DefaultProjectedNdcTolerance, - const float lockAngleToleranceDeg = SCameraFollowRegressionThresholds::DefaultLockAngleToleranceDeg) -{ - auto thresholds = SCameraFollowRegressionThresholds{}; - thresholds.projectedNdcTolerance = projectedNdcTolerance; - thresholds.lockAngleToleranceDeg = lockAngleToleranceDeg; - return thresholds; -} - //! Bundled reusable follow regression flow. //! The helper builds a follow goal, applies it, verifies the resulting camera state, //! and then checks the lock/writeback follow contract. @@ -105,263 +95,277 @@ struct SCameraFollowApplyValidationResult SCameraFollowRegressionResult regression = {}; }; -inline bool tryComputeProjectedFollowTargetMetrics( - const SCameraProjectionContext& projectionContext, - const core::CTrackedTarget& trackedTarget, - SCameraProjectedTargetMetrics& outMetrics, - const float clipWEpsilon = SCameraFollowRegressionThresholds::DefaultClipWEpsilon) -{ - outMetrics = {}; - const auto target = hlsl::getCastedVector(trackedTarget.getGimbal().getPosition()); - const auto viewSpace = hlsl::mul(projectionContext.viewMatrix, hlsl::float32_t4(target.x, target.y, target.z, 1.0f)); - const auto clipProjection = hlsl::transpose(projectionContext.projectionMatrix); - const auto clip = hlsl::mul(clipProjection, viewSpace); - if (!hlsl::isFiniteScalar(clip.x) || !hlsl::isFiniteScalar(clip.y) || !hlsl::isFiniteScalar(clip.z) || !hlsl::isFiniteScalar(clip.w)) - return false; - - const auto absW = hlsl::abs(clip.w); - if (absW < clipWEpsilon) - return false; - - const float invW = 1.0f / clip.w; - outMetrics.ndc = hlsl::float32_t2(clip.x, clip.y) * invW; - if (!hlsl::isFiniteScalar(outMetrics.ndc.x) || !hlsl::isFiniteScalar(outMetrics.ndc.y)) - return false; - - outMetrics.radius = hlsl::length(outMetrics.ndc); - - return true; -} - -inline bool validateProjectedFollowTargetContract( - const SCameraProjectionContext& projectionContext, - const core::CTrackedTarget& trackedTarget, - SCameraProjectedTargetMetrics& outMetrics, - std::string* error = nullptr, - const SCameraFollowRegressionThresholds& thresholds = {}) +struct CCameraFollowRegressionUtilities final { - if (!tryComputeProjectedFollowTargetMetrics(projectionContext, trackedTarget, outMetrics, thresholds.clipWEpsilon)) +public: + static inline SCameraFollowRegressionThresholds makeFollowRegressionThresholds( + const float projectedNdcTolerance = SCameraFollowRegressionThresholds::DefaultProjectedNdcTolerance, + const float lockAngleToleranceDeg = SCameraFollowRegressionThresholds::DefaultLockAngleToleranceDeg) { - if (error) - *error = "failed to project follow target"; - return false; + auto thresholds = SCameraFollowRegressionThresholds{}; + thresholds.projectedNdcTolerance = projectedNdcTolerance; + thresholds.lockAngleToleranceDeg = lockAngleToleranceDeg; + return thresholds; } - if (outMetrics.radius > thresholds.projectedNdcTolerance) + static inline bool tryComputeProjectedFollowTargetMetrics( + const SCameraProjectionContext& projectionContext, + const core::CTrackedTarget& trackedTarget, + SCameraProjectedTargetMetrics& outMetrics, + const float clipWEpsilon = SCameraFollowRegressionThresholds::DefaultClipWEpsilon) { - if (error) - { - *error = "projected target mismatch ndc=(" + std::to_string(outMetrics.ndc.x) + - "," + std::to_string(outMetrics.ndc.y) + ") radius=" + std::to_string(outMetrics.radius); - } - return false; - } - - return true; -} - -inline SCameraFollowVisualMetrics buildFollowVisualMetrics( - core::ICamera* camera, - const core::CTrackedTarget& trackedTarget, - const core::SCameraFollowConfig* followConfig, - const SCameraProjectionContext* projectionContext = nullptr) -{ - SCameraFollowVisualMetrics out = {}; - if (!camera || !followConfig || !followConfig->enabled || followConfig->mode == core::ECameraFollowMode::Disabled) - return out; + outMetrics = {}; + const auto target = hlsl::getCastedVector(trackedTarget.getGimbal().getPosition()); + const auto viewSpace = hlsl::mul(projectionContext.viewMatrix, hlsl::float32_t4(target.x, target.y, target.z, 1.0f)); + const auto clipProjection = hlsl::transpose(projectionContext.projectionMatrix); + const auto clip = hlsl::mul(clipProjection, viewSpace); + if (!hlsl::isFiniteScalar(clip.x) || !hlsl::isFiniteScalar(clip.y) || !hlsl::isFiniteScalar(clip.z) || !hlsl::isFiniteScalar(clip.w)) + return false; - out.active = true; - out.mode = followConfig->mode; + const auto absW = hlsl::abs(clip.w); + if (absW < clipWEpsilon) + return false; - double targetDistance = 0.0; - out.lockValid = core::cameraFollowModeLocksViewToTarget(followConfig->mode) && - core::tryComputeFollowTargetLockMetrics(camera->getGimbal(), trackedTarget, out.lockAngleDeg, &targetDistance); - if (out.lockValid) - out.targetDistance = static_cast(targetDistance); + const float invW = 1.0f / clip.w; + outMetrics.ndc = hlsl::float32_t2(clip.x, clip.y) * invW; + if (!hlsl::isFiniteScalar(outMetrics.ndc.x) || !hlsl::isFiniteScalar(outMetrics.ndc.y)) + return false; - if (out.lockValid && projectionContext) - { - out.projectedValid = tryComputeProjectedFollowTargetMetrics(*projectionContext, trackedTarget, out.projectedTarget); - } + outMetrics.radius = hlsl::length(outMetrics.ndc); - return out; -} - -inline bool validateFollowTargetContract( - core::ICamera* camera, - const core::CTrackedTarget& trackedTarget, - const core::SCameraFollowConfig& followConfig, - const core::CCameraGoal& followGoal, - SCameraFollowRegressionResult& out, - std::string* error = nullptr, - const SCameraProjectionContext* projectionContext = nullptr, - const SCameraFollowRegressionThresholds& thresholds = {}) -{ - out = {}; - if (!camera) - { - if (error) - *error = "missing camera"; - return false; + return true; } - if (core::cameraFollowModeLocksViewToTarget(followConfig.mode)) + static inline bool validateProjectedFollowTargetContract( + const SCameraProjectionContext& projectionContext, + const core::CTrackedTarget& trackedTarget, + SCameraProjectedTargetMetrics& outMetrics, + std::string* error = nullptr, + const SCameraFollowRegressionThresholds& thresholds = {}) { - out.hasLockMetrics = core::tryComputeFollowTargetLockMetrics(camera->getGimbal(), trackedTarget, out.lockAngleDeg, &out.targetDistance); - if (!out.hasLockMetrics) + if (!tryComputeProjectedFollowTargetMetrics(projectionContext, trackedTarget, outMetrics, thresholds.clipWEpsilon)) { if (error) - *error = "failed to compute follow lock metrics"; + *error = "failed to project follow target"; return false; } - const auto expectedTargetDistance = hlsl::length(trackedTarget.getGimbal().getPosition() - camera->getGimbal().getPosition()); - if (!hlsl::isFiniteScalar(expectedTargetDistance) || hlsl::abs(expectedTargetDistance - out.targetDistance) > thresholds.distanceTolerance) + if (outMetrics.radius > thresholds.projectedNdcTolerance) { if (error) { - *error = "target distance mismatch actual=" + std::to_string(out.targetDistance) + - " expected=" + std::to_string(expectedTargetDistance); + *error = "projected target mismatch ndc=(" + std::to_string(outMetrics.ndc.x) + + "," + std::to_string(outMetrics.ndc.y) + ") radius=" + std::to_string(outMetrics.radius); } return false; } - if (out.lockAngleDeg > thresholds.lockAngleToleranceDeg) + return true; + } + + static inline SCameraFollowVisualMetrics buildFollowVisualMetrics( + core::ICamera* camera, + const core::CTrackedTarget& trackedTarget, + const core::SCameraFollowConfig* followConfig, + const SCameraProjectionContext* projectionContext = nullptr) + { + SCameraFollowVisualMetrics out = {}; + if (!camera || !followConfig || !followConfig->enabled || followConfig->mode == core::ECameraFollowMode::Disabled) + return out; + + out.active = true; + out.mode = followConfig->mode; + + double targetDistance = 0.0; + out.lockValid = core::CCameraFollowUtilities::cameraFollowModeLocksViewToTarget(followConfig->mode) && + core::CCameraFollowUtilities::tryComputeFollowTargetLockMetrics(camera->getGimbal(), trackedTarget, out.lockAngleDeg, &targetDistance); + if (out.lockValid) + out.targetDistance = static_cast(targetDistance); + + if (out.lockValid && projectionContext) + { + out.projectedValid = tryComputeProjectedFollowTargetMetrics(*projectionContext, trackedTarget, out.projectedTarget); + } + + return out; + } + + static inline bool validateFollowTargetContract( + core::ICamera* camera, + const core::CTrackedTarget& trackedTarget, + const core::SCameraFollowConfig& followConfig, + const core::CCameraGoal& followGoal, + SCameraFollowRegressionResult& out, + std::string* error = nullptr, + const SCameraProjectionContext* projectionContext = nullptr, + const SCameraFollowRegressionThresholds& thresholds = {}) + { + out = {}; + if (!camera) { if (error) - *error = "lock angle mismatch angle_deg=" + std::to_string(out.lockAngleDeg); + *error = "missing camera"; return false; } - if (projectionContext) + if (core::CCameraFollowUtilities::cameraFollowModeLocksViewToTarget(followConfig.mode)) { - out.hasProjectedMetrics = tryComputeProjectedFollowTargetMetrics( - *projectionContext, - trackedTarget, - out.projectedTarget, - thresholds.clipWEpsilon); - if (!out.hasProjectedMetrics) + out.hasLockMetrics = core::CCameraFollowUtilities::tryComputeFollowTargetLockMetrics(camera->getGimbal(), trackedTarget, out.lockAngleDeg, &out.targetDistance); + if (!out.hasLockMetrics) { if (error) - *error = "failed to compute projected follow target metrics"; + *error = "failed to compute follow lock metrics"; return false; } - if (out.projectedTarget.radius > thresholds.projectedNdcTolerance) + const auto expectedTargetDistance = hlsl::length(trackedTarget.getGimbal().getPosition() - camera->getGimbal().getPosition()); + if (!hlsl::isFiniteScalar(expectedTargetDistance) || hlsl::abs(expectedTargetDistance - out.targetDistance) > thresholds.distanceTolerance) { if (error) - *error = "projected target mismatch ndc=(" + std::to_string(out.projectedTarget.ndc.x) + - "," + std::to_string(out.projectedTarget.ndc.y) + ") radius=" + std::to_string(out.projectedTarget.radius); + { + *error = "target distance mismatch actual=" + std::to_string(out.targetDistance) + + " expected=" + std::to_string(expectedTargetDistance); + } return false; } + + if (out.lockAngleDeg > thresholds.lockAngleToleranceDeg) + { + if (error) + *error = "lock angle mismatch angle_deg=" + std::to_string(out.lockAngleDeg); + return false; + } + + if (projectionContext) + { + out.hasProjectedMetrics = tryComputeProjectedFollowTargetMetrics( + *projectionContext, + trackedTarget, + out.projectedTarget, + thresholds.clipWEpsilon); + if (!out.hasProjectedMetrics) + { + if (error) + *error = "failed to compute projected follow target metrics"; + return false; + } + + if (out.projectedTarget.radius > thresholds.projectedNdcTolerance) + { + if (error) + *error = "projected target mismatch ndc=(" + std::to_string(out.projectedTarget.ndc.x) + + "," + std::to_string(out.projectedTarget.ndc.y) + ") radius=" + std::to_string(out.projectedTarget.radius); + return false; + } + } } + + if (camera->supportsGoalState(core::ICamera::GoalStateSphericalTarget)) + { + core::ICamera::SphericalTargetState state; + if (!camera->tryGetSphericalTargetState(state)) + { + if (error) + *error = "missing spherical target state"; + return false; + } + + out.hasSphericalState = true; + out.sphericalTarget = state.target; + out.sphericalDistance = state.distance; + + const auto trackedTargetPosition = trackedTarget.getGimbal().getPosition(); + const auto targetDelta = state.target - trackedTargetPosition; + const auto targetDeltaLen = hlsl::length(targetDelta); + if (!hlsl::isFiniteScalar(targetDeltaLen) || targetDeltaLen > thresholds.targetTolerance) + { + if (error) + *error = "spherical target writeback mismatch"; + return false; + } + + const auto actualDistance = hlsl::length(camera->getGimbal().getPosition() - trackedTargetPosition); + const auto expectedDistance = followGoal.hasOrbitState ? static_cast(followGoal.orbitDistance) : + (followGoal.hasDistance ? static_cast(followGoal.distance) : actualDistance); + if (!hlsl::isFiniteScalar(actualDistance) || !hlsl::isFiniteScalar(expectedDistance) || + hlsl::abs(actualDistance - expectedDistance) > thresholds.distanceTolerance || + hlsl::abs(static_cast(state.distance) - expectedDistance) > thresholds.distanceTolerance) + { + if (error) + { + *error = "spherical distance mismatch actual=" + std::to_string(actualDistance) + + " state=" + std::to_string(state.distance) + + " expected=" + std::to_string(expectedDistance); + } + return false; + } + } + + out.passed = true; + return true; } - if (camera->supportsGoalState(core::ICamera::GoalStateSphericalTarget)) + static inline bool buildApplyAndValidateFollowTargetContract( + const core::CCameraGoalSolver& solver, + core::ICamera* camera, + const core::CTrackedTarget& trackedTarget, + const core::SCameraFollowConfig& followConfig, + SCameraFollowApplyValidationResult& out, + std::string* error = nullptr, + const SCameraProjectionContext* projectionContext = nullptr, + const SCameraFollowRegressionThresholds& thresholds = {}) { - core::ICamera::SphericalTargetState state; - if (!camera->tryGetSphericalTargetState(state)) + out = {}; + + if (!core::CCameraFollowUtilities::tryBuildFollowGoal(solver, camera, trackedTarget, followConfig, out.goal)) { if (error) - *error = "missing spherical target state"; + *error = "failed to build follow goal"; return false; } + out.hasGoal = true; - out.hasSphericalState = true; - out.sphericalTarget = state.target; - out.sphericalDistance = state.distance; - - const auto trackedTargetPosition = trackedTarget.getGimbal().getPosition(); - const auto targetDelta = state.target - trackedTargetPosition; - const auto targetDeltaLen = hlsl::length(targetDelta); - if (!hlsl::isFiniteScalar(targetDeltaLen) || targetDeltaLen > thresholds.targetTolerance) + out.applyResult = core::CCameraFollowUtilities::applyFollowToCamera(solver, camera, trackedTarget, followConfig); + if (!out.applyResult.succeeded()) { if (error) - *error = "spherical target writeback mismatch"; + *error = "failed to apply follow goal"; return false; } - const auto actualDistance = hlsl::length(camera->getGimbal().getPosition() - trackedTargetPosition); - const auto expectedDistance = followGoal.hasOrbitState ? static_cast(followGoal.orbitDistance) : - (followGoal.hasDistance ? static_cast(followGoal.distance) : actualDistance); - if (!hlsl::isFiniteScalar(actualDistance) || !hlsl::isFiniteScalar(expectedDistance) || - hlsl::abs(actualDistance - expectedDistance) > thresholds.distanceTolerance || - hlsl::abs(static_cast(state.distance) - expectedDistance) > thresholds.distanceTolerance) + const auto capture = solver.captureDetailed(camera); + if (!capture.canUseGoal()) { if (error) - { - *error = "spherical distance mismatch actual=" + std::to_string(actualDistance) + - " state=" + std::to_string(state.distance) + - " expected=" + std::to_string(expectedDistance); - } + *error = "failed to capture camera state after follow apply"; return false; } - } - - out.passed = true; - return true; -} - -inline bool buildApplyAndValidateFollowTargetContract( - const core::CCameraGoalSolver& solver, - core::ICamera* camera, - const core::CTrackedTarget& trackedTarget, - const core::SCameraFollowConfig& followConfig, - SCameraFollowApplyValidationResult& out, - std::string* error = nullptr, - const SCameraProjectionContext* projectionContext = nullptr, - const SCameraFollowRegressionThresholds& thresholds = {}) -{ - out = {}; - - if (!core::tryBuildFollowGoal(solver, camera, trackedTarget, followConfig, out.goal)) - { - if (error) - *error = "failed to build follow goal"; - return false; - } - out.hasGoal = true; - out.applyResult = core::applyFollowToCamera(solver, camera, trackedTarget, followConfig); - if (!out.applyResult.succeeded()) - { - if (error) - *error = "failed to apply follow goal"; - return false; - } - - const auto capture = solver.captureDetailed(camera); - if (!capture.canUseGoal()) - { - if (error) - *error = "failed to capture camera state after follow apply"; - return false; - } + out.hasCapturedGoal = true; + out.capturedGoal = capture.goal; + if (!core::CCameraGoalUtilities::compareGoals(out.capturedGoal, out.goal, thresholds.positionTolerance, thresholds.rotationToleranceDeg, thresholds.scalarTolerance)) + { + if (error) + *error = std::string("follow goal mismatch. ") + core::CCameraGoalUtilities::describeGoalMismatch(out.capturedGoal, out.goal); + return false; + } - out.hasCapturedGoal = true; - out.capturedGoal = capture.goal; - if (!core::compareGoals(out.capturedGoal, out.goal, thresholds.positionTolerance, thresholds.rotationToleranceDeg, thresholds.scalarTolerance)) - { - if (error) - *error = std::string("follow goal mismatch. ") + core::describeGoalMismatch(out.capturedGoal, out.goal); - return false; - } + if (!validateFollowTargetContract( + camera, + trackedTarget, + followConfig, + out.goal, + out.regression, + error, + projectionContext, + thresholds)) + { + return false; + } - if (!validateFollowTargetContract( - camera, - trackedTarget, - followConfig, - out.goal, - out.regression, - error, - projectionContext, - thresholds)) - { - return false; + return true; } - - return true; -} +}; } // namespace nbl::system diff --git a/common/include/camera/CCameraFollowUtilities.hpp b/common/include/camera/CCameraFollowUtilities.hpp index 29bdbc140..1c324bcab 100644 --- a/common/include/camera/CCameraFollowUtilities.hpp +++ b/common/include/camera/CCameraFollowUtilities.hpp @@ -106,253 +106,255 @@ struct SCameraFollowConfig hlsl::float64_t3 localOffset = hlsl::float64_t3(0.0); }; -inline constexpr bool cameraFollowModeLocksViewToTarget(const ECameraFollowMode mode) +struct CCameraFollowUtilities final { - switch (mode) + static inline constexpr bool cameraFollowModeLocksViewToTarget(const ECameraFollowMode mode) { - case ECameraFollowMode::OrbitTarget: - case ECameraFollowMode::LookAtTarget: - case ECameraFollowMode::KeepWorldOffset: - case ECameraFollowMode::KeepLocalOffset: - return true; - default: - return false; + switch (mode) + { + case ECameraFollowMode::OrbitTarget: + case ECameraFollowMode::LookAtTarget: + case ECameraFollowMode::KeepWorldOffset: + case ECameraFollowMode::KeepLocalOffset: + return true; + default: + return false; + } } -} -inline constexpr bool cameraFollowModeMovesCameraPosition(const ECameraFollowMode mode) -{ - switch (mode) + static inline constexpr bool cameraFollowModeMovesCameraPosition(const ECameraFollowMode mode) { - case ECameraFollowMode::OrbitTarget: - case ECameraFollowMode::KeepWorldOffset: - case ECameraFollowMode::KeepLocalOffset: - return true; - default: - return false; + switch (mode) + { + case ECameraFollowMode::OrbitTarget: + case ECameraFollowMode::KeepWorldOffset: + case ECameraFollowMode::KeepLocalOffset: + return true; + default: + return false; + } } -} -inline constexpr bool cameraFollowModeKeepsCameraWorldPosition(const ECameraFollowMode mode) -{ - return mode == ECameraFollowMode::LookAtTarget; -} + static inline constexpr bool cameraFollowModeKeepsCameraWorldPosition(const ECameraFollowMode mode) + { + return mode == ECameraFollowMode::LookAtTarget; + } -inline constexpr bool cameraFollowModeUsesWorldOffset(const ECameraFollowMode mode) -{ - return mode == ECameraFollowMode::KeepWorldOffset; -} + static inline constexpr bool cameraFollowModeUsesWorldOffset(const ECameraFollowMode mode) + { + return mode == ECameraFollowMode::KeepWorldOffset; + } -inline constexpr bool cameraFollowModeUsesLocalOffset(const ECameraFollowMode mode) -{ - return mode == ECameraFollowMode::KeepLocalOffset; -} + static inline constexpr bool cameraFollowModeUsesLocalOffset(const ECameraFollowMode mode) + { + return mode == ECameraFollowMode::KeepLocalOffset; + } -inline constexpr bool cameraFollowModeUsesTrackedTargetLocalFrame(const ECameraFollowMode mode) -{ - return mode == ECameraFollowMode::KeepLocalOffset; -} + static inline constexpr bool cameraFollowModeUsesTrackedTargetLocalFrame(const ECameraFollowMode mode) + { + return mode == ECameraFollowMode::KeepLocalOffset; + } -inline constexpr bool cameraFollowModeUsesCapturedOffset(const ECameraFollowMode mode) -{ - return cameraFollowModeUsesWorldOffset(mode) || cameraFollowModeUsesLocalOffset(mode); -} + static inline constexpr bool cameraFollowModeUsesCapturedOffset(const ECameraFollowMode mode) + { + return cameraFollowModeUsesWorldOffset(mode) || cameraFollowModeUsesLocalOffset(mode); + } -inline constexpr ECameraFollowMode getDefaultCameraFollowMode(const ICamera::CameraKind kind) -{ - switch (kind) + static inline constexpr ECameraFollowMode getDefaultCameraFollowMode(const ICamera::CameraKind kind) { - case ICamera::CameraKind::Orbit: - case ICamera::CameraKind::Arcball: - case ICamera::CameraKind::Turntable: - case ICamera::CameraKind::TopDown: - case ICamera::CameraKind::Isometric: - case ICamera::CameraKind::DollyZoom: - case ICamera::CameraKind::Path: - return ECameraFollowMode::OrbitTarget; - case ICamera::CameraKind::Chase: - case ICamera::CameraKind::Dolly: - return ECameraFollowMode::KeepLocalOffset; - default: - return ECameraFollowMode::Disabled; + switch (kind) + { + case ICamera::CameraKind::Orbit: + case ICamera::CameraKind::Arcball: + case ICamera::CameraKind::Turntable: + case ICamera::CameraKind::TopDown: + case ICamera::CameraKind::Isometric: + case ICamera::CameraKind::DollyZoom: + case ICamera::CameraKind::Path: + return ECameraFollowMode::OrbitTarget; + case ICamera::CameraKind::Chase: + case ICamera::CameraKind::Dolly: + return ECameraFollowMode::KeepLocalOffset; + default: + return ECameraFollowMode::Disabled; + } } -} -inline constexpr SCameraFollowConfig makeDefaultFollowConfig(const ICamera::CameraKind kind) -{ - const auto mode = getDefaultCameraFollowMode(kind); - return { - .enabled = mode != ECameraFollowMode::Disabled, - .mode = mode - }; -} - -inline constexpr SCameraFollowConfig makeDefaultFollowConfig(const ICamera* const camera) -{ - return camera ? makeDefaultFollowConfig(camera->getKind()) : SCameraFollowConfig{}; -} + static inline constexpr SCameraFollowConfig makeDefaultFollowConfig(const ICamera::CameraKind kind) + { + const auto mode = getDefaultCameraFollowMode(kind); + return { + .enabled = mode != ECameraFollowMode::Disabled, + .mode = mode + }; + } -inline hlsl::float64_t3 transformFollowLocalOffset(const ICamera::CGimbal& gimbal, const hlsl::float64_t3& localOffset) -{ - return hlsl::rotateVectorByQuaternion(gimbal.getOrientation(), localOffset); -} + static inline constexpr SCameraFollowConfig makeDefaultFollowConfig(const ICamera* const camera) + { + return camera ? makeDefaultFollowConfig(camera->getKind()) : SCameraFollowConfig{}; + } -inline hlsl::float64_t3 projectFollowWorldOffsetToLocal(const ICamera::CGimbal& gimbal, const hlsl::float64_t3& worldOffset) -{ - return hlsl::projectWorldVectorToLocalQuaternionFrame(gimbal.getOrientation(), worldOffset); -} - -inline bool buildFollowLookAtOrientation( - const hlsl::float64_t3& position, - const hlsl::float64_t3& targetPosition, - const hlsl::float64_t3& preferredUp, - hlsl::camera_quaternion_t& outOrientation) -{ - return hlsl::tryBuildLookAtOrientation(position, targetPosition, preferredUp, outOrientation); -} - -inline bool captureFollowOffsetsFromCamera( - const CCameraGoalSolver& solver, - ICamera* camera, - const CTrackedTarget& trackedTarget, - SCameraFollowConfig& ioConfig) -{ - const auto capture = solver.captureDetailed(camera); - if (!capture.canUseGoal()) - return false; - - const auto& targetGimbal = trackedTarget.getGimbal(); - ioConfig.worldOffset = capture.goal.position - targetGimbal.getPosition(); - ioConfig.localOffset = projectFollowWorldOffsetToLocal(targetGimbal, ioConfig.worldOffset); - return true; -} - -inline bool tryComputeFollowTargetLockMetrics( - const ICamera::CGimbal& cameraGimbal, - const CTrackedTarget& trackedTarget, - float& outAngleDeg, - double* outDistance = nullptr) -{ - const auto toTarget = trackedTarget.getGimbal().getPosition() - cameraGimbal.getPosition(); - const auto targetDistance = hlsl::length(toTarget); - if (!hlsl::isFiniteScalar(targetDistance) || targetDistance <= ICamera::TinyScalarEpsilon) - return false; - - const auto forward = cameraGimbal.getZAxis(); - const auto forwardLength = hlsl::length(forward); - if (!hlsl::isFiniteVec3(forward) || !hlsl::isFiniteScalar(forwardLength) || forwardLength <= ICamera::TinyScalarEpsilon) - return false; - - const auto forwardDirection = forward / forwardLength; - const auto targetDir = toTarget / targetDistance; - const auto dotForward = std::clamp(hlsl::dot(forwardDirection, targetDir), -1.0, 1.0); - outAngleDeg = static_cast(hlsl::degrees(hlsl::acos(dotForward))); - if (!hlsl::isFiniteScalar(outAngleDeg)) - return false; - - if (outDistance) - *outDistance = targetDistance; - return true; -} - -inline bool tryBuildFollowPositionGoal( - ICamera* camera, - CCameraGoal& outGoal, - const hlsl::float64_t3& targetPosition, - const hlsl::float64_t3& position, - const hlsl::float64_t3& preferredUp) -{ - if (camera->supportsGoalState(ICamera::GoalStateSphericalTarget)) - return buildCanonicalTargetRelativeGoalFromPosition(outGoal, targetPosition, position); - - outGoal.position = position; - return buildFollowLookAtOrientation(outGoal.position, targetPosition, preferredUp, outGoal.orientation) && isGoalFinite(outGoal); -} - -inline bool tryBuildFollowGoal( - const CCameraGoalSolver& solver, - ICamera* camera, - const CTrackedTarget& trackedTarget, - const SCameraFollowConfig& config, - CCameraGoal& outGoal) -{ - if (!camera || !config.enabled || config.mode == ECameraFollowMode::Disabled) - return false; + static inline hlsl::float64_t3 transformFollowLocalOffset(const ICamera::CGimbal& gimbal, const hlsl::float64_t3& localOffset) + { + return hlsl::rotateVectorByQuaternion(gimbal.getOrientation(), localOffset); + } + + static inline hlsl::float64_t3 projectFollowWorldOffsetToLocal(const ICamera::CGimbal& gimbal, const hlsl::float64_t3& worldOffset) + { + return hlsl::projectWorldVectorToLocalQuaternionFrame(gimbal.getOrientation(), worldOffset); + } + + static inline bool buildFollowLookAtOrientation( + const hlsl::float64_t3& position, + const hlsl::float64_t3& targetPosition, + const hlsl::float64_t3& preferredUp, + hlsl::camera_quaternion_t& outOrientation) + { + return hlsl::tryBuildLookAtOrientation(position, targetPosition, preferredUp, outOrientation); + } - const auto capture = solver.captureDetailed(camera); - if (!capture.canUseGoal()) - return false; + static inline bool captureFollowOffsetsFromCamera( + const CCameraGoalSolver& solver, + ICamera* camera, + const CTrackedTarget& trackedTarget, + SCameraFollowConfig& ioConfig) + { + const auto capture = solver.captureDetailed(camera); + if (!capture.canUseGoal()) + return false; + + const auto& targetGimbal = trackedTarget.getGimbal(); + ioConfig.worldOffset = capture.goal.position - targetGimbal.getPosition(); + ioConfig.localOffset = projectFollowWorldOffsetToLocal(targetGimbal, ioConfig.worldOffset); + return true; + } + + static inline bool tryComputeFollowTargetLockMetrics( + const ICamera::CGimbal& cameraGimbal, + const CTrackedTarget& trackedTarget, + float& outAngleDeg, + double* outDistance = nullptr) + { + const auto toTarget = trackedTarget.getGimbal().getPosition() - cameraGimbal.getPosition(); + const auto targetDistance = hlsl::length(toTarget); + if (!hlsl::isFiniteScalar(targetDistance) || targetDistance <= ICamera::TinyScalarEpsilon) + return false; + + const auto forward = cameraGimbal.getZAxis(); + const auto forwardLength = hlsl::length(forward); + if (!hlsl::isFiniteVec3(forward) || !hlsl::isFiniteScalar(forwardLength) || forwardLength <= ICamera::TinyScalarEpsilon) + return false; + + const auto forwardDirection = forward / forwardLength; + const auto targetDir = toTarget / targetDistance; + const auto dotForward = std::clamp(hlsl::dot(forwardDirection, targetDir), -1.0, 1.0); + outAngleDeg = static_cast(hlsl::degrees(hlsl::acos(dotForward))); + if (!hlsl::isFiniteScalar(outAngleDeg)) + return false; - outGoal = capture.goal; + if (outDistance) + *outDistance = targetDistance; + return true; + } + + static inline bool tryBuildFollowPositionGoal( + ICamera* camera, + CCameraGoal& outGoal, + const hlsl::float64_t3& targetPosition, + const hlsl::float64_t3& position, + const hlsl::float64_t3& preferredUp) + { + if (camera->supportsGoalState(ICamera::GoalStateSphericalTarget)) + return CCameraGoalUtilities::buildCanonicalTargetRelativeGoalFromPosition(outGoal, targetPosition, position); - const auto& targetGimbal = trackedTarget.getGimbal(); - const auto targetPosition = targetGimbal.getPosition(); + outGoal.position = position; + return buildFollowLookAtOrientation(outGoal.position, targetPosition, preferredUp, outGoal.orientation) && CCameraGoalUtilities::isGoalFinite(outGoal); + } - switch (config.mode) + static inline bool tryBuildFollowGoal( + const CCameraGoalSolver& solver, + ICamera* camera, + const CTrackedTarget& trackedTarget, + const SCameraFollowConfig& config, + CCameraGoal& outGoal) { - case ECameraFollowMode::OrbitTarget: + if (!camera || !config.enabled || config.mode == ECameraFollowMode::Disabled) + return false; + + const auto capture = solver.captureDetailed(camera); + if (!capture.canUseGoal()) + return false; + + outGoal = capture.goal; + + const auto& targetGimbal = trackedTarget.getGimbal(); + const auto targetPosition = targetGimbal.getPosition(); + + switch (config.mode) { - if (!camera->supportsGoalState(ICamera::GoalStateSphericalTarget)) - return false; + case ECameraFollowMode::OrbitTarget: + { + if (!camera->supportsGoalState(ICamera::GoalStateSphericalTarget)) + return false; + + if (outGoal.hasPathState) + { + return CCameraGoalUtilities::applyCanonicalPathGoalFields(outGoal, targetPosition, outGoal.pathState) && CCameraGoalUtilities::isGoalFinite(outGoal); + } + + const bool hasSphericalState = outGoal.hasOrbitState || outGoal.hasDistance; + if (!hasSphericalState) + return false; + + const auto orbitDistance = outGoal.hasOrbitState ? outGoal.orbitDistance : outGoal.distance; + return CCameraGoalUtilities::applyCanonicalTargetRelativeGoal( + outGoal, + { + .target = targetPosition, + .orbitUv = outGoal.orbitUv, + .distance = orbitDistance + }); + } - if (outGoal.hasPathState) + case ECameraFollowMode::LookAtTarget: { - return applyCanonicalPathGoalFields(outGoal, targetPosition, outGoal.pathState) && isGoalFinite(outGoal); + return tryBuildFollowPositionGoal(camera, outGoal, targetPosition, capture.goal.position, targetGimbal.getYAxis()); } - const bool hasSphericalState = outGoal.hasOrbitState || outGoal.hasDistance; - if (!hasSphericalState) - return false; + case ECameraFollowMode::KeepWorldOffset: + { + const auto position = targetPosition + config.worldOffset; + return tryBuildFollowPositionGoal(camera, outGoal, targetPosition, position, targetGimbal.getYAxis()); + } - const auto orbitDistance = outGoal.hasOrbitState ? outGoal.orbitDistance : outGoal.distance; - return applyCanonicalTargetRelativeGoal( - outGoal, - { - .target = targetPosition, - .orbitU = outGoal.orbitU, - .orbitV = outGoal.orbitV, - .distance = orbitDistance - }); - } + case ECameraFollowMode::KeepLocalOffset: + { + const auto position = targetPosition + transformFollowLocalOffset(targetGimbal, config.localOffset); + return tryBuildFollowPositionGoal(camera, outGoal, targetPosition, position, targetGimbal.getYAxis()); + } - case ECameraFollowMode::LookAtTarget: - { - return tryBuildFollowPositionGoal(camera, outGoal, targetPosition, capture.goal.position, targetGimbal.getYAxis()); + default: + return false; } + } - case ECameraFollowMode::KeepWorldOffset: - { - const auto position = targetPosition + config.worldOffset; - return tryBuildFollowPositionGoal(camera, outGoal, targetPosition, position, targetGimbal.getYAxis()); - } + static inline CCameraGoalSolver::SApplyResult applyFollowToCamera( + const CCameraGoalSolver& solver, + ICamera* camera, + const CTrackedTarget& trackedTarget, + const SCameraFollowConfig& config, + CCameraGoal* outGoal = nullptr) + { + CCameraGoal goal = {}; + if (!tryBuildFollowGoal(solver, camera, trackedTarget, config, goal)) + return {}; - case ECameraFollowMode::KeepLocalOffset: - { - const auto position = targetPosition + transformFollowLocalOffset(targetGimbal, config.localOffset); - return tryBuildFollowPositionGoal(camera, outGoal, targetPosition, position, targetGimbal.getYAxis()); - } + if (outGoal) + *outGoal = goal; - default: - return false; + return solver.applyDetailed(camera, goal); } -} - -inline CCameraGoalSolver::SApplyResult applyFollowToCamera( - const CCameraGoalSolver& solver, - ICamera* camera, - const CTrackedTarget& trackedTarget, - const SCameraFollowConfig& config, - CCameraGoal* outGoal = nullptr) -{ - CCameraGoal goal = {}; - if (!tryBuildFollowGoal(solver, camera, trackedTarget, config, goal)) - return {}; - - if (outGoal) - *outGoal = goal; - - return solver.applyDetailed(camera, goal); -} +}; } // namespace nbl::core diff --git a/common/include/camera/CCameraGoal.hpp b/common/include/camera/CCameraGoal.hpp index a04b9358c..e111831f9 100644 --- a/common/include/camera/CCameraGoal.hpp +++ b/common/include/camera/CCameraGoal.hpp @@ -33,8 +33,7 @@ struct CCameraGoal bool hasDistance = false; float distance = 0.f; bool hasOrbitState = false; - double orbitU = 0.0; - double orbitV = 0.0; + hlsl::float64_t2 orbitUv = hlsl::float64_t2(0.0); float orbitDistance = 0.f; bool hasPathState = false; ICamera::PathState pathState = {}; @@ -42,345 +41,343 @@ struct CCameraGoal ICamera::DynamicPerspectiveState dynamicPerspectiveState = {}; }; -inline uint32_t getRequiredGoalStateMask(const CCameraGoal& target) +struct CCameraGoalUtilities final { - uint32_t mask = ICamera::GoalStateNone; - if (target.hasTargetPosition || target.hasDistance || target.hasOrbitState) - mask |= ICamera::GoalStateSphericalTarget; - if (target.hasDynamicPerspectiveState) - mask |= ICamera::GoalStateDynamicPerspective; - if (target.hasPathState) - mask |= ICamera::GoalStatePath; - return mask; -} - -inline void applyCanonicalTargetRelativeGoalFields( - CCameraGoal& goal, - const SCameraTargetRelativeState& state, - const SCameraTargetRelativePose& pose) -{ - goal.position = pose.position; - goal.orientation = pose.orientation; - goal.hasTargetPosition = true; - goal.targetPosition = state.target; - goal.hasDistance = true; - goal.distance = static_cast(pose.appliedDistance); - goal.hasOrbitState = true; - goal.orbitU = state.orbitU; - goal.orbitV = state.orbitV; - goal.orbitDistance = static_cast(pose.appliedDistance); -} - -inline bool applyCanonicalTargetRelativeGoal(CCameraGoal& goal, const SCameraTargetRelativeState& state) -{ - SCameraTargetRelativePose pose = {}; - if (!tryBuildTargetRelativePoseFromState(state, ICamera::SphericalMinDistance, ICamera::SphericalMaxDistance, pose)) - return false; - - applyCanonicalTargetRelativeGoalFields(goal, state, pose); - return true; -} - -inline bool applyCanonicalPathGoalFields( - CCameraGoal& goal, - const hlsl::float64_t3& targetPosition, - const ICamera::PathState& pathState, - const SCameraPathLimits& limits = makeDefaultPathLimits()) -{ - SCameraCanonicalPathState canonicalPathState = {}; - if (!tryBuildCanonicalPathState(targetPosition, pathState, limits, canonicalPathState)) - return false; - - goal.hasTargetPosition = true; - goal.targetPosition = targetPosition; - goal.hasPathState = true; - goal.pathState = pathState; - applyCanonicalTargetRelativeGoalFields( - goal, - canonicalPathState.targetRelative, - { - .position = canonicalPathState.pose.position, - .orientation = canonicalPathState.pose.orientation, - .appliedDistance = canonicalPathState.pose.appliedDistance - }); - return true; -} - -inline bool applyCanonicalSphericalGoal(CCameraGoal& goal) -{ - if (!(goal.hasTargetPosition && goal.hasOrbitState)) - return false; - if (!hlsl::isFiniteScalar(goal.orbitU) || !hlsl::isFiniteScalar(goal.orbitV) || !hlsl::isFiniteScalar(goal.orbitDistance)) - return false; +public: + static inline uint32_t getRequiredGoalStateMask(const CCameraGoal& target) + { + uint32_t mask = ICamera::GoalStateNone; + if (target.hasTargetPosition || target.hasDistance || target.hasOrbitState) + mask |= ICamera::GoalStateSphericalTarget; + if (target.hasDynamicPerspectiveState) + mask |= ICamera::GoalStateDynamicPerspective; + if (target.hasPathState) + mask |= ICamera::GoalStatePath; + return mask; + } - return applyCanonicalTargetRelativeGoal( - goal, - { - .target = goal.targetPosition, - .orbitU = goal.orbitU, - .orbitV = goal.orbitV, - .distance = goal.orbitDistance - }); -} - -inline bool buildCanonicalTargetRelativeGoalFromPosition( - CCameraGoal& goal, - const hlsl::float64_t3& targetPosition, - const hlsl::float64_t3& position) -{ - SCameraTargetRelativeState state = {}; - if (!tryBuildTargetRelativeStateFromPosition( - targetPosition, - position, - ICamera::SphericalMinDistance, - ICamera::SphericalMaxDistance, - state)) + static inline void applyCanonicalTargetRelativeGoalFields( + CCameraGoal& goal, + const SCameraTargetRelativeState& state, + const SCameraTargetRelativePose& pose) { - return false; + goal.position = pose.position; + goal.orientation = pose.orientation; + goal.hasTargetPosition = true; + goal.targetPosition = state.target; + goal.hasDistance = true; + goal.distance = static_cast(pose.appliedDistance); + goal.hasOrbitState = true; + goal.orbitUv = state.orbitUv; + goal.orbitDistance = static_cast(pose.appliedDistance); } - return applyCanonicalTargetRelativeGoal(goal, state); -} + static inline bool applyCanonicalTargetRelativeGoal(CCameraGoal& goal, const SCameraTargetRelativeState& state) + { + SCameraTargetRelativePose pose = {}; + if (!CCameraTargetRelativeUtilities::tryBuildTargetRelativePoseFromState(state, ICamera::SphericalMinDistance, ICamera::SphericalMaxDistance, pose)) + return false; -inline bool tryResolveCanonicalTargetRelativeState( - const CCameraGoal& goal, - const ICamera::SphericalTargetState& currentState, - SCameraTargetRelativeState& outState) -{ - outState.target = goal.hasTargetPosition ? goal.targetPosition : currentState.target; - outState.orbitU = currentState.u; - outState.orbitV = currentState.v; - outState.distance = currentState.distance; + applyCanonicalTargetRelativeGoalFields(goal, state, pose); + return true; + } - if (goal.hasOrbitState) + static inline bool applyCanonicalPathGoalFields( + CCameraGoal& goal, + const hlsl::float64_t3& targetPosition, + const ICamera::PathState& pathState, + const SCameraPathLimits& limits = CCameraPathUtilities::makeDefaultPathLimits()) { - outState.orbitU = goal.orbitU; - outState.orbitV = goal.orbitV; - outState.distance = goal.orbitDistance; + SCameraCanonicalPathState canonicalPathState = {}; + if (!CCameraPathUtilities::tryBuildCanonicalPathState(targetPosition, pathState, limits, canonicalPathState)) + return false; + + goal.hasTargetPosition = true; + goal.targetPosition = targetPosition; + goal.hasPathState = true; + goal.pathState = pathState; + applyCanonicalTargetRelativeGoalFields( + goal, + canonicalPathState.targetRelative, + { + .position = canonicalPathState.pose.position, + .orientation = canonicalPathState.pose.orientation, + .appliedDistance = canonicalPathState.pose.appliedDistance + }); + return true; } - else + + static inline bool applyCanonicalSphericalGoal(CCameraGoal& goal) { - SCameraTargetRelativeState resolvedState = {}; - if (!tryBuildTargetRelativeStateFromPosition( - outState.target, - goal.position, - currentState.minDistance, - currentState.maxDistance, - resolvedState)) + if (!(goal.hasTargetPosition && goal.hasOrbitState)) + return false; + if (!hlsl::isFiniteScalar(goal.orbitUv.x) || !hlsl::isFiniteScalar(goal.orbitUv.y) || !hlsl::isFiniteScalar(goal.orbitDistance)) + return false; + + return applyCanonicalTargetRelativeGoal( + goal, + { + .target = goal.targetPosition, + .orbitUv = goal.orbitUv, + .distance = goal.orbitDistance + }); + } + + static inline bool buildCanonicalTargetRelativeGoalFromPosition( + CCameraGoal& goal, + const hlsl::float64_t3& targetPosition, + const hlsl::float64_t3& position) + { + SCameraTargetRelativeState state = {}; + if (!CCameraTargetRelativeUtilities::tryBuildTargetRelativeStateFromPosition( + targetPosition, + position, + ICamera::SphericalMinDistance, + ICamera::SphericalMaxDistance, + state)) { return false; } - outState.orbitU = resolvedState.orbitU; - outState.orbitV = resolvedState.orbitV; - outState.distance = resolvedState.distance; + return applyCanonicalTargetRelativeGoal(goal, state); } - if (goal.hasDistance && !goal.hasOrbitState) - outState.distance = goal.distance; + static inline bool tryResolveCanonicalTargetRelativeState( + const CCameraGoal& goal, + const ICamera::SphericalTargetState& currentState, + SCameraTargetRelativeState& outState) + { + outState.target = goal.hasTargetPosition ? goal.targetPosition : currentState.target; + outState.orbitUv = currentState.orbitUv; + outState.distance = currentState.distance; - outState.distance = std::clamp(outState.distance, currentState.minDistance, currentState.maxDistance); - return true; -} + if (goal.hasOrbitState) + { + outState.orbitUv = goal.orbitUv; + outState.distance = goal.orbitDistance; + } + else + { + SCameraTargetRelativeState resolvedState = {}; + if (!CCameraTargetRelativeUtilities::tryBuildTargetRelativeStateFromPosition( + outState.target, + goal.position, + currentState.minDistance, + currentState.maxDistance, + resolvedState)) + { + return false; + } + + outState.orbitUv = resolvedState.orbitUv; + outState.distance = resolvedState.distance; + } -inline bool applyCanonicalPathGoal(CCameraGoal& goal) -{ - if (!(goal.hasPathState && goal.hasTargetPosition)) - return false; - if (!isPathStateFinite(goal.pathState)) - return false; - return applyCanonicalPathGoalFields(goal, goal.targetPosition, goal.pathState); -} - -inline bool applyCanonicalGoalState(CCameraGoal& goal) -{ - if (goal.hasPathState) - return applyCanonicalPathGoal(goal); + if (goal.hasDistance && !goal.hasOrbitState) + outState.distance = goal.distance; - if (goal.hasTargetPosition && goal.hasOrbitState) - return applyCanonicalSphericalGoal(goal); + outState.distance = std::clamp(outState.distance, currentState.minDistance, currentState.maxDistance); + return true; + } - return true; -} + static inline bool applyCanonicalPathGoal(CCameraGoal& goal) + { + if (!(goal.hasPathState && goal.hasTargetPosition)) + return false; + if (!CCameraPathUtilities::isPathStateFinite(goal.pathState)) + return false; + return applyCanonicalPathGoalFields(goal, goal.targetPosition, goal.pathState); + } -inline CCameraGoal canonicalizeGoal(CCameraGoal goal) -{ - applyCanonicalGoalState(goal); - return goal; -} + static inline bool applyCanonicalGoalState(CCameraGoal& goal) + { + if (goal.hasPathState) + return applyCanonicalPathGoal(goal); -inline bool isGoalFinite(const CCameraGoal& goal) -{ - if (!hlsl::isFiniteVec3(goal.position) || !hlsl::isFiniteQuaternion(goal.orientation)) - return false; - if (goal.hasTargetPosition && !hlsl::isFiniteVec3(goal.targetPosition)) - return false; - if (goal.hasDistance && !hlsl::isFiniteScalar(goal.distance)) - return false; - if (goal.hasOrbitState && (!hlsl::isFiniteScalar(goal.orbitU) || !hlsl::isFiniteScalar(goal.orbitV) || !hlsl::isFiniteScalar(goal.orbitDistance))) - return false; - if (goal.hasPathState && !isPathStateFinite(goal.pathState)) - return false; - if (goal.hasDynamicPerspectiveState && - (!hlsl::isFiniteScalar(goal.dynamicPerspectiveState.baseFov) || !hlsl::isFiniteScalar(goal.dynamicPerspectiveState.referenceDistance))) - return false; - return true; -} - -inline bool compareGoals(const CCameraGoal& actual, const CCameraGoal& expected, - const double posEps, const double rotEpsDeg, const double scalarEps) -{ - hlsl::SCameraPoseDelta poseDelta = {}; - if (!hlsl::tryComputePoseDelta(actual.position, actual.orientation, expected.position, expected.orientation, poseDelta)) - return false; - if (poseDelta.position > posEps || poseDelta.rotationDeg > rotEpsDeg) - return false; + if (goal.hasTargetPosition && goal.hasOrbitState) + return applyCanonicalSphericalGoal(goal); - if (expected.hasTargetPosition) - { - if (!actual.hasTargetPosition || !hlsl::nearlyEqualVec3(actual.targetPosition, expected.targetPosition, scalarEps)) - return false; + return true; } - if (expected.hasDistance) + + static inline CCameraGoal canonicalizeGoal(CCameraGoal goal) { - if (!actual.hasDistance || !hlsl::nearlyEqualScalar(static_cast(actual.distance), static_cast(expected.distance), scalarEps)) - return false; + applyCanonicalGoalState(goal); + return goal; } - if (expected.hasOrbitState) + + static inline bool isGoalFinite(const CCameraGoal& goal) { - if (!actual.hasOrbitState) + if (!hlsl::isFiniteVec3(goal.position) || !hlsl::isFiniteQuaternion(goal.orientation)) return false; - if (hlsl::getWrappedAngleDistanceDegrees(expected.orbitU, actual.orbitU) > rotEpsDeg) + if (goal.hasTargetPosition && !hlsl::isFiniteVec3(goal.targetPosition)) return false; - if (hlsl::getWrappedAngleDistanceDegrees(expected.orbitV, actual.orbitV) > rotEpsDeg) + if (goal.hasDistance && !hlsl::isFiniteScalar(goal.distance)) return false; - if (!hlsl::nearlyEqualScalar(static_cast(actual.orbitDistance), static_cast(expected.orbitDistance), scalarEps)) + if (goal.hasOrbitState && (!hlsl::isFiniteScalar(goal.orbitUv.x) || !hlsl::isFiniteScalar(goal.orbitUv.y) || !hlsl::isFiniteScalar(goal.orbitDistance))) return false; - } - if (expected.hasPathState) - { - if (!actual.hasPathState) + if (goal.hasPathState && !CCameraPathUtilities::isPathStateFinite(goal.pathState)) return false; - if (!pathStatesNearlyEqual(actual.pathState, expected.pathState, makePathComparisonThresholds(rotEpsDeg, scalarEps))) + if (goal.hasDynamicPerspectiveState && + (!hlsl::isFiniteScalar(goal.dynamicPerspectiveState.baseFov) || !hlsl::isFiniteScalar(goal.dynamicPerspectiveState.referenceDistance))) return false; + return true; } - if (expected.hasDynamicPerspectiveState) + + static inline bool compareGoals(const CCameraGoal& actual, const CCameraGoal& expected, + const double posEps, const double rotEpsDeg, const double scalarEps) { - if (!actual.hasDynamicPerspectiveState) - return false; - if (!hlsl::nearlyEqualScalar(static_cast(actual.dynamicPerspectiveState.baseFov), static_cast(expected.dynamicPerspectiveState.baseFov), scalarEps)) + hlsl::SCameraPoseDelta poseDelta = {}; + if (!hlsl::tryComputePoseDelta(actual.position, actual.orientation, expected.position, expected.orientation, poseDelta)) return false; - if (!hlsl::nearlyEqualScalar(static_cast(actual.dynamicPerspectiveState.referenceDistance), static_cast(expected.dynamicPerspectiveState.referenceDistance), scalarEps)) + if (poseDelta.position > posEps || poseDelta.rotationDeg > rotEpsDeg) return false; - } - return true; -} + if (expected.hasTargetPosition) + { + if (!actual.hasTargetPosition || !hlsl::nearlyEqualVec3(actual.targetPosition, expected.targetPosition, scalarEps)) + return false; + } + if (expected.hasDistance) + { + if (!actual.hasDistance || !hlsl::nearlyEqualScalar(static_cast(actual.distance), static_cast(expected.distance), scalarEps)) + return false; + } + if (expected.hasOrbitState) + { + if (!actual.hasOrbitState) + return false; + if (hlsl::getWrappedAngleDistanceDegrees(expected.orbitUv.x, actual.orbitUv.x) > rotEpsDeg) + return false; + if (hlsl::getWrappedAngleDistanceDegrees(expected.orbitUv.y, actual.orbitUv.y) > rotEpsDeg) + return false; + if (!hlsl::nearlyEqualScalar(static_cast(actual.orbitDistance), static_cast(expected.orbitDistance), scalarEps)) + return false; + } + if (expected.hasPathState) + { + if (!actual.hasPathState) + return false; + if (!CCameraPathUtilities::pathStatesNearlyEqual(actual.pathState, expected.pathState, CCameraPathUtilities::makePathComparisonThresholds(rotEpsDeg, scalarEps))) + return false; + } + if (expected.hasDynamicPerspectiveState) + { + if (!actual.hasDynamicPerspectiveState) + return false; + if (!hlsl::nearlyEqualScalar(static_cast(actual.dynamicPerspectiveState.baseFov), static_cast(expected.dynamicPerspectiveState.baseFov), scalarEps)) + return false; + if (!hlsl::nearlyEqualScalar(static_cast(actual.dynamicPerspectiveState.referenceDistance), static_cast(expected.dynamicPerspectiveState.referenceDistance), scalarEps)) + return false; + } -inline std::string describeGoalMismatch(const CCameraGoal& actual, const CCameraGoal& expected) -{ - std::ostringstream oss; - hlsl::SCameraPoseDelta poseDelta = {}; - const bool hasPoseDelta = hlsl::tryComputePoseDelta(actual.position, actual.orientation, expected.position, expected.orientation, poseDelta); - const auto currentOrientation = hlsl::normalizeQuaternion(actual.orientation); - const auto expectedOrientation = hlsl::normalizeQuaternion(expected.orientation); - oss << "pos_delta=" << (hasPoseDelta ? poseDelta.position : std::numeric_limits::quiet_NaN()) - << " rot_delta_deg=" << (hasPoseDelta ? poseDelta.rotationDeg : std::numeric_limits::quiet_NaN()) - << " current_pos=(" << actual.position.x << "," << actual.position.y << "," << actual.position.z << ")" - << " expected_pos=(" << expected.position.x << "," << expected.position.y << "," << expected.position.z << ")" - << " current_quat=(" << currentOrientation.data.x << "," << currentOrientation.data.y << "," << currentOrientation.data.z << "," << currentOrientation.data.w << ")" - << " expected_quat=(" << expectedOrientation.data.x << "," << expectedOrientation.data.y << "," << expectedOrientation.data.z << "," << expectedOrientation.data.w << ")"; - - if (actual.hasTargetPosition) - { - oss << " target=(" << actual.targetPosition.x << "," << actual.targetPosition.y << "," << actual.targetPosition.z << ")"; - if (actual.hasDistance) - oss << " distance=" << actual.distance; - if (actual.hasOrbitState) - oss << " orbit_u=" << actual.orbitU << " orbit_v=" << actual.orbitV; - } - else if (expected.hasTargetPosition || expected.hasDistance || expected.hasOrbitState) - { - oss << " spherical_state=unavailable"; - } - if (actual.hasPathState) - { - oss << " path_angle=" << actual.pathState.angle - << " path_radius=" << actual.pathState.radius - << " path_height=" << actual.pathState.height; - } - else if (expected.hasPathState) - { - oss << " path_state=unavailable"; + return true; } - if (actual.hasDynamicPerspectiveState) - { - oss << " dynamic_base_fov=" << actual.dynamicPerspectiveState.baseFov - << " dynamic_reference_distance=" << actual.dynamicPerspectiveState.referenceDistance; - } - else if (expected.hasDynamicPerspectiveState) + static inline std::string describeGoalMismatch(const CCameraGoal& actual, const CCameraGoal& expected) { - oss << " dynamic_perspective_state=unavailable"; - } + std::ostringstream oss; + hlsl::SCameraPoseDelta poseDelta = {}; + const bool hasPoseDelta = hlsl::tryComputePoseDelta(actual.position, actual.orientation, expected.position, expected.orientation, poseDelta); + const auto currentOrientation = hlsl::normalizeQuaternion(actual.orientation); + const auto expectedOrientation = hlsl::normalizeQuaternion(expected.orientation); + oss << "pos_delta=" << (hasPoseDelta ? poseDelta.position : std::numeric_limits::quiet_NaN()) + << " rot_delta_deg=" << (hasPoseDelta ? poseDelta.rotationDeg : std::numeric_limits::quiet_NaN()) + << " current_pos=(" << actual.position.x << "," << actual.position.y << "," << actual.position.z << ")" + << " expected_pos=(" << expected.position.x << "," << expected.position.y << "," << expected.position.z << ")" + << " current_quat=(" << currentOrientation.data.x << "," << currentOrientation.data.y << "," << currentOrientation.data.z << "," << currentOrientation.data.w << ")" + << " expected_quat=(" << expectedOrientation.data.x << "," << expectedOrientation.data.y << "," << expectedOrientation.data.z << "," << expectedOrientation.data.w << ")"; + + if (actual.hasTargetPosition) + { + oss << " target=(" << actual.targetPosition.x << "," << actual.targetPosition.y << "," << actual.targetPosition.z << ")"; + if (actual.hasDistance) + oss << " distance=" << actual.distance; + if (actual.hasOrbitState) + oss << " orbit_u=" << actual.orbitUv.x << " orbit_v=" << actual.orbitUv.y; + } + else if (expected.hasTargetPosition || expected.hasDistance || expected.hasOrbitState) + { + oss << " spherical_state=unavailable"; + } + if (actual.hasPathState) + { + oss << " path_angle=" << actual.pathState.angle + << " path_radius=" << actual.pathState.radius + << " path_height=" << actual.pathState.height; + } + else if (expected.hasPathState) + { + oss << " path_state=unavailable"; + } - return oss.str(); -} + if (actual.hasDynamicPerspectiveState) + { + oss << " dynamic_base_fov=" << actual.dynamicPerspectiveState.baseFov + << " dynamic_reference_distance=" << actual.dynamicPerspectiveState.referenceDistance; + } + else if (expected.hasDynamicPerspectiveState) + { + oss << " dynamic_perspective_state=unavailable"; + } -inline CCameraGoal blendGoals(const CCameraGoal& a, const CCameraGoal& b, double alpha) -{ - CCameraGoal blended; - blended.position = a.position + (b.position - a.position) * alpha; - blended.orientation = hlsl::slerpQuaternion(a.orientation, b.orientation, static_cast(alpha)); - blended.sourceKind = (a.sourceKind == b.sourceKind) ? a.sourceKind : ICamera::CameraKind::Unknown; - blended.sourceCapabilities = a.sourceCapabilities & b.sourceCapabilities; - blended.sourceGoalStateMask = a.sourceGoalStateMask | b.sourceGoalStateMask; - blended.hasTargetPosition = a.hasTargetPosition || b.hasTargetPosition; - if (blended.hasTargetPosition) - { - const auto ta = a.hasTargetPosition ? a.targetPosition : b.targetPosition; - const auto tb = b.hasTargetPosition ? b.targetPosition : a.targetPosition; - blended.targetPosition = ta + (tb - ta) * alpha; - } - blended.hasDistance = a.hasDistance || b.hasDistance; - if (blended.hasDistance) - { - const float da = a.hasDistance ? a.distance : b.distance; - const float db = b.hasDistance ? b.distance : a.distance; - blended.distance = da + (db - da) * static_cast(alpha); - } - blended.hasOrbitState = a.hasOrbitState || b.hasOrbitState; - if (blended.hasOrbitState) - { - const double ua = a.hasOrbitState ? a.orbitU : b.orbitU; - const double ub = b.hasOrbitState ? b.orbitU : a.orbitU; - const double va = a.hasOrbitState ? a.orbitV : b.orbitV; - const double vb = b.hasOrbitState ? b.orbitV : a.orbitV; - const float da = a.hasOrbitState ? a.orbitDistance : b.orbitDistance; - const float db = b.hasOrbitState ? b.orbitDistance : a.orbitDistance; - - blended.orbitU = hlsl::lerpWrappedAngleRad(ua, ub, alpha); - blended.orbitV = hlsl::lerpWrappedAngleRad(va, vb, alpha); - blended.orbitDistance = da + (db - da) * static_cast(alpha); - } - blended.hasDynamicPerspectiveState = a.hasDynamicPerspectiveState || b.hasDynamicPerspectiveState; - if (blended.hasDynamicPerspectiveState) - { - const auto dynamicA = a.hasDynamicPerspectiveState ? a.dynamicPerspectiveState : b.dynamicPerspectiveState; - const auto dynamicB = b.hasDynamicPerspectiveState ? b.dynamicPerspectiveState : a.dynamicPerspectiveState; - blended.dynamicPerspectiveState.baseFov = dynamicA.baseFov + (dynamicB.baseFov - dynamicA.baseFov) * static_cast(alpha); - blended.dynamicPerspectiveState.referenceDistance = - dynamicA.referenceDistance + (dynamicB.referenceDistance - dynamicA.referenceDistance) * static_cast(alpha); + return oss.str(); } - blended.hasPathState = a.hasPathState || b.hasPathState; - if (blended.hasPathState) + + static inline CCameraGoal blendGoals(const CCameraGoal& a, const CCameraGoal& b, double alpha) { - const auto pathA = a.hasPathState ? a.pathState : b.pathState; - const auto pathB = b.hasPathState ? b.pathState : a.pathState; - blended.pathState = blendPathStates(pathA, pathB, alpha); + CCameraGoal blended; + blended.position = a.position + (b.position - a.position) * alpha; + blended.orientation = hlsl::slerpQuaternion(a.orientation, b.orientation, static_cast(alpha)); + blended.sourceKind = (a.sourceKind == b.sourceKind) ? a.sourceKind : ICamera::CameraKind::Unknown; + blended.sourceCapabilities = a.sourceCapabilities & b.sourceCapabilities; + blended.sourceGoalStateMask = a.sourceGoalStateMask | b.sourceGoalStateMask; + blended.hasTargetPosition = a.hasTargetPosition || b.hasTargetPosition; + if (blended.hasTargetPosition) + { + const auto ta = a.hasTargetPosition ? a.targetPosition : b.targetPosition; + const auto tb = b.hasTargetPosition ? b.targetPosition : a.targetPosition; + blended.targetPosition = ta + (tb - ta) * alpha; + } + blended.hasDistance = a.hasDistance || b.hasDistance; + if (blended.hasDistance) + { + const float da = a.hasDistance ? a.distance : b.distance; + const float db = b.hasDistance ? b.distance : a.distance; + blended.distance = da + (db - da) * static_cast(alpha); + } + blended.hasOrbitState = a.hasOrbitState || b.hasOrbitState; + if (blended.hasOrbitState) + { + const auto orbitUvA = a.hasOrbitState ? a.orbitUv : b.orbitUv; + const auto orbitUvB = b.hasOrbitState ? b.orbitUv : a.orbitUv; + const float da = a.hasOrbitState ? a.orbitDistance : b.orbitDistance; + const float db = b.hasOrbitState ? b.orbitDistance : a.orbitDistance; + + blended.orbitUv = hlsl::float64_t2( + hlsl::lerpWrappedAngleRad(orbitUvA.x, orbitUvB.x, alpha), + hlsl::lerpWrappedAngleRad(orbitUvA.y, orbitUvB.y, alpha)); + blended.orbitDistance = da + (db - da) * static_cast(alpha); + } + blended.hasDynamicPerspectiveState = a.hasDynamicPerspectiveState || b.hasDynamicPerspectiveState; + if (blended.hasDynamicPerspectiveState) + { + const auto dynamicA = a.hasDynamicPerspectiveState ? a.dynamicPerspectiveState : b.dynamicPerspectiveState; + const auto dynamicB = b.hasDynamicPerspectiveState ? b.dynamicPerspectiveState : a.dynamicPerspectiveState; + blended.dynamicPerspectiveState.baseFov = dynamicA.baseFov + (dynamicB.baseFov - dynamicA.baseFov) * static_cast(alpha); + blended.dynamicPerspectiveState.referenceDistance = + dynamicA.referenceDistance + (dynamicB.referenceDistance - dynamicA.referenceDistance) * static_cast(alpha); + } + blended.hasPathState = a.hasPathState || b.hasPathState; + if (blended.hasPathState) + { + const auto pathA = a.hasPathState ? a.pathState : b.pathState; + const auto pathB = b.hasPathState ? b.pathState : a.pathState; + blended.pathState = CCameraPathUtilities::blendPathStates(pathA, pathB, alpha); + } + return canonicalizeGoal(blended); } - return canonicalizeGoal(blended); -} +}; } // namespace nbl::core diff --git a/common/include/camera/CCameraGoalAnalysis.hpp b/common/include/camera/CCameraGoalAnalysis.hpp index 7146df065..3546a2bd0 100644 --- a/common/include/camera/CCameraGoalAnalysis.hpp +++ b/common/include/camera/CCameraGoalAnalysis.hpp @@ -51,34 +51,38 @@ struct SCameraCaptureAnalysis bool canCapture = false; }; -inline SCameraGoalApplyAnalysis analyzeGoalApply(const CCameraGoalSolver& solver, const ICamera* camera, const CCameraGoal& goal) +struct CCameraGoalAnalysisUtilities final { - SCameraGoalApplyAnalysis analysis; - analysis.goal = canonicalizeGoal(goal); - analysis.hasCamera = camera != nullptr; - analysis.finiteGoal = isGoalFinite(analysis.goal); - analysis.canApply = analysis.hasCamera && analysis.finiteGoal; - if (analysis.hasCamera) - analysis.compatibility = solver.analyzeCompatibility(camera, analysis.goal); - return analysis; -} +public: + static inline SCameraGoalApplyAnalysis analyzeGoalApply(const CCameraGoalSolver& solver, const ICamera* camera, const CCameraGoal& goal) + { + SCameraGoalApplyAnalysis analysis; + analysis.goal = CCameraGoalUtilities::canonicalizeGoal(goal); + analysis.hasCamera = camera != nullptr; + analysis.finiteGoal = CCameraGoalUtilities::isGoalFinite(analysis.goal); + analysis.canApply = analysis.hasCamera && analysis.finiteGoal; + if (analysis.hasCamera) + analysis.compatibility = solver.analyzeCompatibility(camera, analysis.goal); + return analysis; + } -inline SCameraGoalApplyAnalysis analyzePresetApply(const CCameraGoalSolver& solver, const ICamera* camera, const CCameraPreset& preset) -{ - return analyzeGoalApply(solver, camera, makeGoalFromPreset(preset)); -} + static inline SCameraGoalApplyAnalysis analyzePresetApply(const CCameraGoalSolver& solver, const ICamera* camera, const CCameraPreset& preset) + { + return analyzeGoalApply(solver, camera, makeGoalFromPreset(preset)); + } -inline SCameraCaptureAnalysis analyzeCameraCapture(const CCameraGoalSolver& solver, ICamera* camera) -{ - SCameraCaptureAnalysis analysis; - const auto capture = solver.captureDetailed(camera); - analysis.goal = capture.goal; - analysis.hasCamera = capture.hasCamera; - analysis.capturedGoal = capture.captured; - analysis.finiteGoal = capture.finiteGoal; - analysis.canCapture = capture.canUseGoal(); - return analysis; -} + static inline SCameraCaptureAnalysis analyzeCameraCapture(const CCameraGoalSolver& solver, ICamera* camera) + { + SCameraCaptureAnalysis analysis; + const auto capture = solver.captureDetailed(camera); + analysis.goal = capture.goal; + analysis.hasCamera = capture.hasCamera; + analysis.capturedGoal = capture.captured; + analysis.finiteGoal = capture.finiteGoal; + analysis.canCapture = capture.canUseGoal(); + return analysis; + } +}; } // namespace nbl::core diff --git a/common/include/camera/CCameraGoalSolver.hpp b/common/include/camera/CCameraGoalSolver.hpp index 731826721..206395bd4 100644 --- a/common/include/camera/CCameraGoalSolver.hpp +++ b/common/include/camera/CCameraGoalSolver.hpp @@ -104,7 +104,7 @@ class CCameraGoalSolver if (!camera) return false; - const auto canonicalTarget = canonicalizeGoal(target); + const auto canonicalTarget = CCameraGoalUtilities::canonicalizeGoal(target); if (camera->hasCapability(ICamera::SphericalTarget)) return buildSphericalEvents(camera, canonicalTarget, out); @@ -133,8 +133,7 @@ class CCameraGoalSolver out.distance = sphericalState.distance; out.hasDistance = true; out.orbitDistance = sphericalState.distance; - out.orbitU = sphericalState.u; - out.orbitV = sphericalState.v; + out.orbitUv = sphericalState.orbitUv; out.hasOrbitState = true; } @@ -152,7 +151,7 @@ class CCameraGoalSolver out.pathState = pathState; } - out = canonicalizeGoal(out); + out = CCameraGoalUtilities::canonicalizeGoal(out); return true; } @@ -164,7 +163,7 @@ class CCameraGoalSolver return result; result.captured = capture(camera, result.goal); - result.finiteGoal = result.captured && isGoalFinite(result.goal); + result.finiteGoal = result.captured && CCameraGoalUtilities::isGoalFinite(result.goal); return result; } @@ -174,10 +173,10 @@ class CCameraGoalSolver if (!camera) return result; - const auto canonicalTarget = canonicalizeGoal(target); + const auto canonicalTarget = CCameraGoalUtilities::canonicalizeGoal(target); result.sameKind = canonicalTarget.sourceKind == ICamera::CameraKind::Unknown || canonicalTarget.sourceKind == camera->getKind(); result.supportedGoalStateMask = camera->getGoalStateMask(); - result.requiredGoalStateMask = getRequiredGoalStateMask(canonicalTarget); + result.requiredGoalStateMask = CCameraGoalUtilities::getRequiredGoalStateMask(canonicalTarget); result.missingGoalStateMask = result.requiredGoalStateMask & ~result.supportedGoalStateMask; result.exact = result.missingGoalStateMask == ICamera::GoalStateNone; return result; @@ -189,7 +188,7 @@ class CCameraGoalSolver if (!camera) return result; - const auto canonicalTarget = canonicalizeGoal(target); + const auto canonicalTarget = CCameraGoalUtilities::canonicalizeGoal(target); bool exact = true; bool absoluteChanged = false; @@ -304,8 +303,8 @@ class CCameraGoalSolver else { const auto thresholds = SCameraPathDefaults::ComparisonThresholds; - const bool pathChanged = pathStatesChanged(beforeState, afterState, thresholds); - const bool pathExact = pathStatesNearlyEqual(afterState, canonicalTarget.pathState, thresholds); + const bool pathChanged = CCameraPathUtilities::pathStatesChanged(beforeState, afterState, thresholds); + const bool pathExact = CCameraPathUtilities::pathStatesNearlyEqual(afterState, canonicalTarget.pathState, thresholds); absoluteChanged = absoluteChanged || pathChanged; exact = exact && pathExact; @@ -425,7 +424,7 @@ class CCameraGoalSolver const SCameraPathDelta& delta, const double moveDenominator) const { - appendPathAdvanceEvents( + CCameraPathUtilities::appendPathAdvanceEvents( events, delta, moveDenominator, @@ -511,8 +510,8 @@ class CCameraGoalSolver std::vector& out, const SCameraTargetRelativeEventPolicy& policy) const { - const auto delta = buildTargetRelativeDelta(sphericalState, goal); - appendTargetRelativeDeltaEvents( + const auto delta = CCameraTargetRelativeUtilities::buildTargetRelativeDelta(sphericalState, goal); + CCameraTargetRelativeUtilities::appendTargetRelativeDeltaEvents( out, delta, policy.translateOrbit ? getMoveMagnitudeDenominator(camera) : getRotationMagnitudeDenominator(camera), @@ -532,7 +531,7 @@ class CCameraGoalSolver ICamera::PathState currentState = {}; const ICamera::PathState* currentStateOverride = camera->tryGetPathState(currentState) ? ¤tState : nullptr; SCameraPathStateTransition transition = {}; - if (!tryBuildPathStateTransition( + if (!CCameraPathUtilities::tryBuildPathStateTransition( effectiveTarget, camera->getGimbal().getPosition(), target.position, @@ -559,7 +558,7 @@ class CCameraGoalSolver return buildPathEvents(camera, target, sphericalState, out); SCameraTargetRelativeState goal; - if (!tryResolveCanonicalTargetRelativeState(target, sphericalState, goal)) + if (!CCameraGoalUtilities::tryResolveCanonicalTargetRelativeState(target, sphericalState, goal)) return false; switch (camera->getKind()) diff --git a/common/include/camera/CCameraKeyframeTrack.hpp b/common/include/camera/CCameraKeyframeTrack.hpp index 396512c86..28de97da1 100644 --- a/common/include/camera/CCameraKeyframeTrack.hpp +++ b/common/include/camera/CCameraKeyframeTrack.hpp @@ -78,7 +78,7 @@ inline bool tryBuildKeyframeTrackPresetAtTime(const CCameraKeyframeTrack& track, const double alpha = static_cast(clampedTime - a.time) / static_cast(b.time - a.time); preset = a.preset; - assignGoalToPreset(preset, blendGoals(makeGoalFromPreset(a.preset), makeGoalFromPreset(b.preset), alpha)); + assignGoalToPreset(preset, CCameraGoalUtilities::blendGoals(makeGoalFromPreset(a.preset), makeGoalFromPreset(b.preset), alpha)); return true; } diff --git a/common/include/camera/CCameraPathUtilities.hpp b/common/include/camera/CCameraPathUtilities.hpp index 3349d2e01..9f158b874 100644 --- a/common/include/camera/CCameraPathUtilities.hpp +++ b/common/include/camera/CCameraPathUtilities.hpp @@ -18,8 +18,7 @@ struct SCameraPathPose final hlsl::float64_t3 position = hlsl::float64_t3(0.0); hlsl::camera_quaternion_t orientation = hlsl::makeIdentityQuaternion(); hlsl::float64_t appliedDistance = 0.0; - double orbitU = 0.0; - double orbitV = 0.0; + hlsl::float64_t2 orbitUv = hlsl::float64_t2(0.0); }; struct SCameraPathDelta final @@ -102,279 +101,278 @@ struct SCameraPathDefaults final using SCameraPathLimits = SCameraPathDefaults::SLimits; -inline ICamera::PathState makeDefaultPathState(const double minRadius = SCameraPathDefaults::MinRadius) +struct CCameraPathUtilities final { - return { - .angle = 0.0, - .radius = minRadius, - .height = 0.0 - }; -} + static inline ICamera::PathState makeDefaultPathState(const double minRadius = SCameraPathDefaults::MinRadius) + { + return { + .angle = 0.0, + .radius = minRadius, + .height = 0.0 + }; + } -inline SCameraPathComparisonThresholds makePathComparisonThresholds( - const double angleToleranceDeg = SCameraPathDefaults::AngleToleranceDeg, - const double scalarTolerance = SCameraPathDefaults::ScalarTolerance) -{ - return { - .angleToleranceDeg = angleToleranceDeg, - .scalarTolerance = scalarTolerance - }; -} + static inline SCameraPathComparisonThresholds makePathComparisonThresholds( + const double angleToleranceDeg = SCameraPathDefaults::AngleToleranceDeg, + const double scalarTolerance = SCameraPathDefaults::ScalarTolerance) + { + return { + .angleToleranceDeg = angleToleranceDeg, + .scalarTolerance = scalarTolerance + }; + } -inline constexpr SCameraPathLimits makeDefaultPathLimits() -{ - return SCameraPathDefaults::Limits; -} + static inline constexpr SCameraPathLimits makeDefaultPathLimits() + { + return SCameraPathDefaults::Limits; + } -inline bool isPathStateFinite(const ICamera::PathState& state) -{ - return hlsl::isFiniteScalar(state.angle) && - hlsl::isFiniteScalar(state.radius) && - hlsl::isFiniteScalar(state.height); -} + static inline bool isPathStateFinite(const ICamera::PathState& state) + { + return hlsl::isFiniteScalar(state.angle) && + hlsl::isFiniteScalar(state.radius) && + hlsl::isFiniteScalar(state.height); + } -inline bool sanitizePathState(ICamera::PathState& state, const double minRadius) -{ - return hlsl::sanitizePathState(state.angle, state.radius, state.height, minRadius); -} - -inline bool tryScalePathStateDistance( - const double desiredDistance, - const double minRadius, - ICamera::PathState& ioState, - double* outAppliedDistance = nullptr) -{ - return hlsl::tryScalePathStateDistance( - desiredDistance, - minRadius, - ioState.radius, - ioState.height, - outAppliedDistance); -} - -inline bool tryUpdatePathStateDistance( - const float desiredDistance, - const SCameraPathLimits& limits, - ICamera::PathState& ioState, - SCameraPathDistanceUpdateResult* outResult = nullptr) -{ - const auto clampedDistance = std::clamp(desiredDistance, limits.minDistance, limits.maxDistance); - double appliedDistance = 0.0; - if (!tryScalePathStateDistance(static_cast(clampedDistance), limits.minRadius, ioState, &appliedDistance)) - return false; + static inline bool sanitizePathState(ICamera::PathState& state, const double minRadius) + { + return hlsl::sanitizePathState(state.angle, state.radius, state.height, minRadius); + } - if (outResult) + static inline bool tryScalePathStateDistance( + const double desiredDistance, + const double minRadius, + ICamera::PathState& ioState, + double* outAppliedDistance = nullptr) { - outResult->appliedDistance = appliedDistance; - outResult->exact = (clampedDistance == desiredDistance) && - hlsl::nearlyEqualScalar(appliedDistance, static_cast(desiredDistance), SCameraPathDefaults::ScalarTolerance); + return hlsl::tryScalePathStateDistance( + desiredDistance, + minRadius, + ioState.radius, + ioState.height, + outAppliedDistance); } - return true; -} - -inline bool tryBuildPathStateFromPosition( - const hlsl::float64_t3& targetPosition, - const hlsl::float64_t3& position, - const double minRadius, - ICamera::PathState& outState) -{ - return hlsl::tryBuildPathStateFromPosition( - targetPosition, - position, - minRadius, - outState.angle, - outState.radius, - outState.height); -} - -inline bool tryResolvePathState( - const hlsl::float64_t3& targetPosition, - const hlsl::float64_t3& position, - const SCameraPathLimits& limits, - const ICamera::PathState* requestedState, - ICamera::PathState& outState) -{ - if (requestedState) + + static inline bool tryUpdatePathStateDistance( + const float desiredDistance, + const SCameraPathLimits& limits, + ICamera::PathState& ioState, + SCameraPathDistanceUpdateResult* outResult = nullptr) { - outState = *requestedState; + const auto clampedDistance = std::clamp(desiredDistance, limits.minDistance, limits.maxDistance); + double appliedDistance = 0.0; + if (!tryScalePathStateDistance(static_cast(clampedDistance), limits.minRadius, ioState, &appliedDistance)) + return false; + + if (outResult) + { + outResult->appliedDistance = appliedDistance; + outResult->exact = (clampedDistance == desiredDistance) && + hlsl::nearlyEqualScalar(appliedDistance, static_cast(desiredDistance), SCameraPathDefaults::ScalarTolerance); + } + return true; + } + + static inline bool tryBuildPathStateFromPosition( + const hlsl::float64_t3& targetPosition, + const hlsl::float64_t3& position, + const double minRadius, + ICamera::PathState& outState) + { + return hlsl::tryBuildPathStateFromPosition( + targetPosition, + position, + minRadius, + outState.angle, + outState.radius, + outState.height); + } + + static inline bool tryResolvePathState( + const hlsl::float64_t3& targetPosition, + const hlsl::float64_t3& position, + const SCameraPathLimits& limits, + const ICamera::PathState* requestedState, + ICamera::PathState& outState) + { + if (requestedState) + { + outState = *requestedState; + return sanitizePathState(outState, limits.minRadius); + } + + if (tryBuildPathStateFromPosition(targetPosition, position, limits.minRadius, outState)) + return true; + + outState = makeDefaultPathState(limits.minRadius); return sanitizePathState(outState, limits.minRadius); } - if (tryBuildPathStateFromPosition(targetPosition, position, limits.minRadius, outState)) + static inline bool tryBuildPathPoseFromState( + const hlsl::float64_t3& targetPosition, + const ICamera::PathState& state, + const SCameraPathLimits& limits, + SCameraPathPose& outPose) + { + return hlsl::tryBuildPathPoseFromState( + targetPosition, + state.angle, + state.radius, + state.height, + limits.minRadius, + limits.minDistance, + limits.maxDistance, + outPose.position, + outPose.orientation, + &outPose.appliedDistance, + &outPose.orbitUv.x, + &outPose.orbitUv.y); + } + + static inline bool tryBuildPathPoseFromState( + const hlsl::float64_t3& targetPosition, + const ICamera::PathState& state, + const SCameraPathLimits& limits, + hlsl::float64_t3& outPosition, + hlsl::camera_quaternion_t& outOrientation, + hlsl::float64_t* outAppliedDistance = nullptr, + hlsl::float64_t2* outOrbitUv = nullptr) + { + SCameraPathPose pathPose = {}; + if (!tryBuildPathPoseFromState(targetPosition, state, limits, pathPose)) + return false; + + outPosition = pathPose.position; + outOrientation = pathPose.orientation; + if (outAppliedDistance) + *outAppliedDistance = pathPose.appliedDistance; + if (outOrbitUv) + *outOrbitUv = pathPose.orbitUv; return true; + } - outState = makeDefaultPathState(limits.minRadius); - return sanitizePathState(outState, limits.minRadius); -} + static inline bool pathStatesNearlyEqual( + const ICamera::PathState& lhs, + const ICamera::PathState& rhs, + const SCameraPathComparisonThresholds& thresholds = {}) + { + return hlsl::getWrappedAngleDistanceDegrees(lhs.angle, rhs.angle) <= thresholds.angleToleranceDeg && + hlsl::nearlyEqualScalar(lhs.radius, rhs.radius, thresholds.scalarTolerance) && + hlsl::nearlyEqualScalar(lhs.height, rhs.height, thresholds.scalarTolerance); + } -inline bool tryBuildPathPoseFromState( - const hlsl::float64_t3& targetPosition, - const ICamera::PathState& state, - const SCameraPathLimits& limits, - SCameraPathPose& outPose) -{ - return hlsl::tryBuildPathPoseFromState( - targetPosition, - state.angle, - state.radius, - state.height, - limits.minRadius, - limits.minDistance, - limits.maxDistance, - outPose.position, - outPose.orientation, - &outPose.appliedDistance, - &outPose.orbitU, - &outPose.orbitV); -} - -inline bool tryBuildPathPoseFromState( - const hlsl::float64_t3& targetPosition, - const ICamera::PathState& state, - const SCameraPathLimits& limits, - hlsl::float64_t3& outPosition, - hlsl::camera_quaternion_t& outOrientation, - hlsl::float64_t* outAppliedDistance = nullptr, - double* outOrbitU = nullptr, - double* outOrbitV = nullptr) -{ - SCameraPathPose pathPose = {}; - if (!tryBuildPathPoseFromState(targetPosition, state, limits, pathPose)) - return false; - - outPosition = pathPose.position; - outOrientation = pathPose.orientation; - if (outAppliedDistance) - *outAppliedDistance = pathPose.appliedDistance; - if (outOrbitU) - *outOrbitU = pathPose.orbitU; - if (outOrbitV) - *outOrbitV = pathPose.orbitV; - return true; -} - -inline bool pathStatesNearlyEqual( - const ICamera::PathState& lhs, - const ICamera::PathState& rhs, - const SCameraPathComparisonThresholds& thresholds = {}) -{ - return hlsl::getWrappedAngleDistanceDegrees(lhs.angle, rhs.angle) <= thresholds.angleToleranceDeg && - hlsl::nearlyEqualScalar(lhs.radius, rhs.radius, thresholds.scalarTolerance) && - hlsl::nearlyEqualScalar(lhs.height, rhs.height, thresholds.scalarTolerance); -} - -inline bool pathStatesChanged( - const ICamera::PathState& lhs, - const ICamera::PathState& rhs, - const SCameraPathComparisonThresholds& thresholds = {}) -{ - return !pathStatesNearlyEqual(lhs, rhs, thresholds); -} + static inline bool pathStatesChanged( + const ICamera::PathState& lhs, + const ICamera::PathState& rhs, + const SCameraPathComparisonThresholds& thresholds = {}) + { + return !pathStatesNearlyEqual(lhs, rhs, thresholds); + } -inline hlsl::float64_t3 buildPathStateDeltaVector( - const ICamera::PathState& currentState, - const ICamera::PathState& desiredState) -{ - auto deltaVector = desiredState.asVector() - currentState.asVector(); - deltaVector.z = hlsl::wrapAngleRad(deltaVector.z); - return deltaVector; -} - -inline SCameraPathDelta buildPathStateDelta( - const ICamera::PathState& currentState, - const ICamera::PathState& desiredState) -{ - return SCameraPathDelta::fromVector(buildPathStateDeltaVector(currentState, desiredState)); -} + static inline hlsl::float64_t3 buildPathStateDeltaVector( + const ICamera::PathState& currentState, + const ICamera::PathState& desiredState) + { + auto deltaVector = desiredState.asVector() - currentState.asVector(); + deltaVector.z = hlsl::wrapAngleRad(deltaVector.z); + return deltaVector; + } -inline SCameraPathDelta makePathDeltaFromVirtualPathTranslate(const hlsl::float64_t3& delta) -{ - return SCameraPathDelta::fromVector(delta); -} - -inline void appendPathAdvanceEvents( - std::vector& events, - const SCameraPathDelta& delta, - const double moveDenominator, - const double angleToleranceDeg = SCameraPathDefaults::AngleToleranceDeg, - const double scalarTolerance = SCameraPathDefaults::ScalarTolerance) -{ - appendLocalTranslationEvents( - events, - delta.translationVector(), - hlsl::float64_t3(moveDenominator), - hlsl::float64_t3(scalarTolerance)); - appendAngularDeltaEvent( - events, - delta.angle, - moveDenominator, - angleToleranceDeg, - CVirtualGimbalEvent::MoveForward, - CVirtualGimbalEvent::MoveBackward); -} - -inline bool tryBuildCanonicalPathState( - const hlsl::float64_t3& targetPosition, - const ICamera::PathState& state, - const SCameraPathLimits& limits, - SCameraCanonicalPathState& outState) -{ - outState = {}; - if (!tryBuildPathPoseFromState(targetPosition, state, limits, outState.pose)) - return false; - - outState.targetRelative = { - .target = targetPosition, - .orbitU = outState.pose.orbitU, - .orbitV = outState.pose.orbitV, - .distance = static_cast(outState.pose.appliedDistance) - }; - return true; -} - -inline bool tryApplyPathStateDelta( - const ICamera::PathState& currentState, - const SCameraPathDelta& delta, - const SCameraPathLimits& limits, - ICamera::PathState& outState) -{ - auto stateVector = currentState.asVector() + delta.asVector(); - stateVector.z = hlsl::wrapAngleRad(stateVector.z); - outState = ICamera::PathState::fromVector(stateVector); - return sanitizePathState(outState, limits.minRadius); -} - -inline ICamera::PathState blendPathStates( - const ICamera::PathState& from, - const ICamera::PathState& to, - const double alpha) -{ - const auto fromVector = from.asVector(); - const auto toVector = to.asVector(); - return { - .angle = hlsl::lerpWrappedAngleRad(fromVector.z, toVector.z, alpha), - .radius = fromVector.x + (toVector.x - fromVector.x) * alpha, - .height = fromVector.y + (toVector.y - fromVector.y) * alpha - }; -} - -inline bool tryBuildPathStateTransition( - const hlsl::float64_t3& targetPosition, - const hlsl::float64_t3& currentPosition, - const hlsl::float64_t3& desiredPosition, - const SCameraPathLimits& limits, - const ICamera::PathState* currentStateOverride, - const ICamera::PathState* desiredStateOverride, - SCameraPathStateTransition& outTransition) -{ - if (!tryResolvePathState(targetPosition, currentPosition, limits, currentStateOverride, outTransition.current)) - return false; - if (!tryResolvePathState(targetPosition, desiredPosition, limits, desiredStateOverride, outTransition.desired)) - return false; - - outTransition.delta = buildPathStateDelta(outTransition.current, outTransition.desired); - return true; -} + static inline SCameraPathDelta buildPathStateDelta( + const ICamera::PathState& currentState, + const ICamera::PathState& desiredState) + { + return SCameraPathDelta::fromVector(buildPathStateDeltaVector(currentState, desiredState)); + } + + static inline SCameraPathDelta makePathDeltaFromVirtualPathTranslate(const hlsl::float64_t3& delta) + { + return SCameraPathDelta::fromVector(delta); + } + + static inline void appendPathAdvanceEvents( + std::vector& events, + const SCameraPathDelta& delta, + const double moveDenominator, + const double angleToleranceDeg = SCameraPathDefaults::AngleToleranceDeg, + const double scalarTolerance = SCameraPathDefaults::ScalarTolerance) + { + appendLocalTranslationEvents( + events, + delta.translationVector(), + hlsl::float64_t3(moveDenominator), + hlsl::float64_t3(scalarTolerance)); + appendAngularDeltaEvent( + events, + delta.angle, + moveDenominator, + angleToleranceDeg, + CVirtualGimbalEvent::MoveForward, + CVirtualGimbalEvent::MoveBackward); + } + + static inline bool tryBuildCanonicalPathState( + const hlsl::float64_t3& targetPosition, + const ICamera::PathState& state, + const SCameraPathLimits& limits, + SCameraCanonicalPathState& outState) + { + outState = {}; + if (!tryBuildPathPoseFromState(targetPosition, state, limits, outState.pose)) + return false; + + outState.targetRelative = { + .target = targetPosition, + .orbitUv = outState.pose.orbitUv, + .distance = static_cast(outState.pose.appliedDistance) + }; + return true; + } + + static inline bool tryApplyPathStateDelta( + const ICamera::PathState& currentState, + const SCameraPathDelta& delta, + const SCameraPathLimits& limits, + ICamera::PathState& outState) + { + auto stateVector = currentState.asVector() + delta.asVector(); + stateVector.z = hlsl::wrapAngleRad(stateVector.z); + outState = ICamera::PathState::fromVector(stateVector); + return sanitizePathState(outState, limits.minRadius); + } + + static inline ICamera::PathState blendPathStates( + const ICamera::PathState& from, + const ICamera::PathState& to, + const double alpha) + { + const auto fromVector = from.asVector(); + const auto toVector = to.asVector(); + return { + .angle = hlsl::lerpWrappedAngleRad(fromVector.z, toVector.z, alpha), + .radius = fromVector.x + (toVector.x - fromVector.x) * alpha, + .height = fromVector.y + (toVector.y - fromVector.y) * alpha + }; + } + + static inline bool tryBuildPathStateTransition( + const hlsl::float64_t3& targetPosition, + const hlsl::float64_t3& currentPosition, + const hlsl::float64_t3& desiredPosition, + const SCameraPathLimits& limits, + const ICamera::PathState* currentStateOverride, + const ICamera::PathState* desiredStateOverride, + SCameraPathStateTransition& outTransition) + { + if (!tryResolvePathState(targetPosition, currentPosition, limits, currentStateOverride, outTransition.current)) + return false; + if (!tryResolvePathState(targetPosition, desiredPosition, limits, desiredStateOverride, outTransition.desired)) + return false; + + outTransition.delta = buildPathStateDelta(outTransition.current, outTransition.desired); + return true; + } +}; } // namespace nbl::core diff --git a/common/include/camera/CCameraPresentationUtilities.hpp b/common/include/camera/CCameraPresentationUtilities.hpp index a1b1c83d7..9dad90b10 100644 --- a/common/include/camera/CCameraPresentationUtilities.hpp +++ b/common/include/camera/CCameraPresentationUtilities.hpp @@ -95,7 +95,7 @@ inline SCameraGoalApplyPresentation makeGoalApplyPresentation(const core::SCamer //! Analyze one preset against one camera and return reusable presentation data. inline SCameraGoalApplyPresentation analyzePresetPresentation(const core::CCameraGoalSolver& solver, const core::ICamera* camera, const core::CCameraPreset& preset) { - return makeGoalApplyPresentation(core::analyzePresetApply(solver, camera, preset), camera); + return makeGoalApplyPresentation(core::CCameraGoalAnalysisUtilities::analyzePresetApply(solver, camera, preset), camera); } //! Build reusable badge flags for one preset/keyframe compatibility answer. @@ -114,7 +114,7 @@ inline SCameraGoalApplyPresentationBadges collectGoalApplyPresentationBadges(con inline SCameraCapturePresentation analyzeCapturePresentation(const core::CCameraGoalSolver& solver, core::ICamera* camera) { SCameraCapturePresentation presentation; - static_cast(presentation) = core::analyzeCameraCapture(solver, camera); + static_cast(presentation) = core::CCameraGoalAnalysisUtilities::analyzeCameraCapture(solver, camera); presentation.policyLabel = describeCameraCapturePolicy(presentation, camera); return presentation; } diff --git a/common/include/camera/CCameraPreset.hpp b/common/include/camera/CCameraPreset.hpp index e9a4e218c..0343109b2 100644 --- a/common/include/camera/CCameraPreset.hpp +++ b/common/include/camera/CCameraPreset.hpp @@ -30,12 +30,12 @@ struct CCameraKeyframe inline void assignGoalToPreset(CCameraPreset& preset, const CCameraGoal& goal) { - preset.goal = canonicalizeGoal(goal); + preset.goal = CCameraGoalUtilities::canonicalizeGoal(goal); } inline CCameraGoal makeGoalFromPreset(const CCameraPreset& preset) { - return canonicalizeGoal(preset.goal); + return CCameraGoalUtilities::canonicalizeGoal(preset.goal); } //! Compare two named presets through their shared canonical goal state. @@ -44,7 +44,7 @@ inline bool comparePresets(const CCameraPreset& lhs, const CCameraPreset& rhs, { return lhs.name == rhs.name && lhs.identifier == rhs.identifier && - compareGoals(makeGoalFromPreset(lhs), makeGoalFromPreset(rhs), posEps, rotEpsDeg, scalarEps); + CCameraGoalUtilities::compareGoals(makeGoalFromPreset(lhs), makeGoalFromPreset(rhs), posEps, rotEpsDeg, scalarEps); } //! Compare two preset collections element-by-element through the shared canonical goal state. diff --git a/common/include/camera/CCameraPresetFlow.hpp b/common/include/camera/CCameraPresetFlow.hpp index 95aa029df..7ce71024d 100644 --- a/common/include/camera/CCameraPresetFlow.hpp +++ b/common/include/camera/CCameraPresetFlow.hpp @@ -47,7 +47,7 @@ inline bool comparePresetToCameraState(const CCameraGoalSolver& solver, ICamera* if (!capture.canUseGoal()) return false; - return compareGoals(capture.goal, makeGoalFromPreset(preset), posEps, rotEpsDeg, scalarEps); + return CCameraGoalUtilities::compareGoals(capture.goal, makeGoalFromPreset(preset), posEps, rotEpsDeg, scalarEps); } //! Explain the first visible mismatch between a camera state and a preset. @@ -61,7 +61,7 @@ inline std::string describePresetCameraMismatch(const CCameraGoalSolver& solver, if (!capture.finiteGoal) return "goal_state=invalid"; - return describeGoalMismatch(capture.goal, makeGoalFromPreset(preset)); + return CCameraGoalUtilities::describeGoalMismatch(capture.goal, makeGoalFromPreset(preset)); } //! Build a preset from an already analyzed capture result. @@ -80,7 +80,7 @@ inline bool tryCapturePreset(const SCameraCaptureAnalysis& captureAnalysis, ICam //! Capture a preset directly from a camera through the shared goal solver. inline bool tryCapturePreset(const CCameraGoalSolver& solver, ICamera* camera, std::string_view name, CCameraPreset& preset) { - return tryCapturePreset(analyzeCameraCapture(solver, camera), camera, name, preset); + return tryCapturePreset(CCameraGoalAnalysisUtilities::analyzeCameraCapture(solver, camera), camera, name, preset); } //! Value-returning convenience wrapper around `tryCapturePreset`. diff --git a/common/include/camera/CCameraScriptedCheckRunner.hpp b/common/include/camera/CCameraScriptedCheckRunner.hpp index 33b172ea6..a09499257 100644 --- a/common/include/camera/CCameraScriptedCheckRunner.hpp +++ b/common/include/camera/CCameraScriptedCheckRunner.hpp @@ -509,14 +509,14 @@ inline CCameraScriptedCheckFrameResult evaluateScriptedChecksForFrame( SCameraFollowRegressionResult regression = {}; std::string regressionError; core::CCameraGoal expectedFollowGoal = {}; - const auto thresholds = makeFollowRegressionThresholds(check.posTolerance, check.eulerToleranceDeg); - const bool ok = core::tryBuildFollowGoal( + const auto thresholds = CCameraFollowRegressionUtilities::makeFollowRegressionThresholds(check.posTolerance, check.eulerToleranceDeg); + const bool ok = core::CCameraFollowUtilities::tryBuildFollowGoal( *context.goalSolver, context.camera, *context.trackedTarget, *context.followConfig, expectedFollowGoal) && - validateFollowTargetContract( + CCameraFollowRegressionUtilities::validateFollowTargetContract( context.camera, *context.trackedTarget, *context.followConfig, diff --git a/common/include/camera/CCameraSequenceScript.hpp b/common/include/camera/CCameraSequenceScript.hpp index 18e231c4d..f4e4b44bd 100644 --- a/common/include/camera/CCameraSequenceScript.hpp +++ b/common/include/camera/CCameraSequenceScript.hpp @@ -76,10 +76,8 @@ struct CCameraSequenceGoalDelta hlsl::float64_t3 targetOffset = hlsl::float64_t3(0.0); bool hasOrbitUDeltaDeg = false; - double orbitUDeltaDeg = 0.0; - bool hasOrbitVDeltaDeg = false; - double orbitVDeltaDeg = 0.0; + hlsl::float64_t2 orbitUvDeltaDeg = hlsl::float64_t2(0.0); bool hasOrbitDistanceDelta = false; float orbitDistanceDelta = 0.f; @@ -242,219 +240,225 @@ struct CCameraSequenceCompiledFramePolicy bool followTargetLock = false; }; -inline bool tryParseCameraKind(std::string_view value, ICamera::CameraKind& outKind) -{ - if (value == "FPS") - outKind = ICamera::CameraKind::FPS; - else if (value == "Free") - outKind = ICamera::CameraKind::Free; - else if (value == "Orbit") - outKind = ICamera::CameraKind::Orbit; - else if (value == "Arcball") - outKind = ICamera::CameraKind::Arcball; - else if (value == "Turntable") - outKind = ICamera::CameraKind::Turntable; - else if (value == "TopDown") - outKind = ICamera::CameraKind::TopDown; - else if (value == "Isometric") - outKind = ICamera::CameraKind::Isometric; - else if (value == "Chase") - outKind = ICamera::CameraKind::Chase; - else if (value == "Dolly") - outKind = ICamera::CameraKind::Dolly; - else if (value == "DollyZoom" || value == "Dolly Zoom") - outKind = ICamera::CameraKind::DollyZoom; - else if (value == "PathRig" || value == "Path Rig") - outKind = ICamera::CameraKind::Path; - else - return false; - - return true; -} - -inline bool tryParseProjectionType(std::string_view value, IPlanarProjection::CProjection::ProjectionType& outType) +struct CCameraSequenceScriptUtilities final { - if (value == "perspective" || value == "Perspective") - outType = IPlanarProjection::CProjection::Perspective; - else if (value == "orthographic" || value == "Orthographic") - outType = IPlanarProjection::CProjection::Orthographic; - else - return false; - - return true; -} - -inline void normalizeCaptureFractions(std::vector& fractions) -{ - for (auto& fraction : fractions) - fraction = std::clamp(fraction, 0.f, 1.f); + static inline bool tryParseCameraKind(std::string_view value, ICamera::CameraKind& outKind) + { + if (value == "FPS") + outKind = ICamera::CameraKind::FPS; + else if (value == "Free") + outKind = ICamera::CameraKind::Free; + else if (value == "Orbit") + outKind = ICamera::CameraKind::Orbit; + else if (value == "Arcball") + outKind = ICamera::CameraKind::Arcball; + else if (value == "Turntable") + outKind = ICamera::CameraKind::Turntable; + else if (value == "TopDown") + outKind = ICamera::CameraKind::TopDown; + else if (value == "Isometric") + outKind = ICamera::CameraKind::Isometric; + else if (value == "Chase") + outKind = ICamera::CameraKind::Chase; + else if (value == "Dolly") + outKind = ICamera::CameraKind::Dolly; + else if (value == "DollyZoom" || value == "Dolly Zoom") + outKind = ICamera::CameraKind::DollyZoom; + else if (value == "PathRig" || value == "Path Rig") + outKind = ICamera::CameraKind::Path; + else + return false; - std::sort(fractions.begin(), fractions.end()); - fractions.erase(std::unique(fractions.begin(), fractions.end(), - [](const float lhs, const float rhs) { return hlsl::nearlyEqualScalar(lhs, rhs, static_cast(ICamera::ScalarTolerance)); }), - fractions.end()); -} + return true; + } -inline bool buildSequenceKeyframePreset(const CCameraPreset& reference, const CCameraSequenceKeyframe& authored, CCameraPreset& outPreset, std::string* error = nullptr) -{ - if (authored.hasAbsolutePreset) + static inline bool tryParseProjectionType(std::string_view value, IPlanarProjection::CProjection::ProjectionType& outType) { - outPreset = authored.absolutePreset; - if (outPreset.identifier.empty()) - outPreset.identifier = reference.identifier; - if (outPreset.name.empty()) - outPreset.name = reference.name; - return isGoalFinite(makeGoalFromPreset(outPreset)); - } + if (value == "perspective" || value == "Perspective") + outType = IPlanarProjection::CProjection::Perspective; + else if (value == "orthographic" || value == "Orthographic") + outType = IPlanarProjection::CProjection::Orthographic; + else + return false; - outPreset = reference; - if (!authored.hasDelta) return true; + } - auto goal = makeGoalFromPreset(reference); - const auto& delta = authored.delta; + static inline void normalizeCaptureFractions(std::vector& fractions) + { + for (auto& fraction : fractions) + fraction = std::clamp(fraction, 0.f, 1.f); - const bool hasPoseDelta = delta.hasPositionOffset || delta.hasRotationEulerDegOffset; - const bool hasSphericalDelta = delta.hasTargetOffset || delta.hasOrbitUDeltaDeg || delta.hasOrbitVDeltaDeg || delta.hasOrbitDistanceDelta; - const bool hasPathDelta = delta.hasPathAngleDeltaDeg || delta.hasPathRadiusDelta || delta.hasPathHeightDelta; + std::sort(fractions.begin(), fractions.end()); + fractions.erase(std::unique(fractions.begin(), fractions.end(), + [](const float lhs, const float rhs) { return hlsl::nearlyEqualScalar(lhs, rhs, static_cast(ICamera::ScalarTolerance)); }), + fractions.end()); + } - if (hasPoseDelta && (hasSphericalDelta || hasPathDelta)) + static inline bool buildSequenceKeyframePreset(const CCameraPreset& reference, const CCameraSequenceKeyframe& authored, CCameraPreset& outPreset, std::string* error = nullptr) { - if (error) - *error = "Sequence keyframe delta cannot mix pose offsets with spherical/path deltas."; - return false; - } + if (authored.hasAbsolutePreset) + { + outPreset = authored.absolutePreset; + if (outPreset.identifier.empty()) + outPreset.identifier = reference.identifier; + if (outPreset.name.empty()) + outPreset.name = reference.name; + return CCameraGoalUtilities::isGoalFinite(makeGoalFromPreset(outPreset)); + } - if (delta.hasPositionOffset) - goal.position += delta.positionOffset; + outPreset = reference; + if (!authored.hasDelta) + return true; - if (delta.hasRotationEulerDegOffset) - { - goal.orientation = hlsl::normalizeQuaternion(goal.orientation * hlsl::makeQuaternionFromEulerDegreesYXZ(hlsl::getCastedVector(delta.rotationEulerDegOffset))); - } + auto goal = makeGoalFromPreset(reference); + const auto& delta = authored.delta; - if (delta.hasTargetOffset) - { - if (!goal.hasTargetPosition) + const bool hasPoseDelta = delta.hasPositionOffset || delta.hasRotationEulerDegOffset; + const bool hasSphericalDelta = delta.hasTargetOffset || delta.hasOrbitUDeltaDeg || delta.hasOrbitVDeltaDeg || delta.hasOrbitDistanceDelta; + const bool hasPathDelta = delta.hasPathAngleDeltaDeg || delta.hasPathRadiusDelta || delta.hasPathHeightDelta; + + if (hasPoseDelta && (hasSphericalDelta || hasPathDelta)) { if (error) - *error = "Sequence keyframe target_offset requires target state."; + *error = "Sequence keyframe delta cannot mix pose offsets with spherical/path deltas."; return false; } - goal.targetPosition += delta.targetOffset; - } - if (delta.hasOrbitUDeltaDeg || delta.hasOrbitVDeltaDeg || delta.hasOrbitDistanceDelta) - { - if (!goal.hasOrbitState) + if (delta.hasPositionOffset) + goal.position += delta.positionOffset; + + if (delta.hasRotationEulerDegOffset) { - if (error) - *error = "Sequence keyframe orbit deltas require spherical orbit state."; - return false; + goal.orientation = hlsl::normalizeQuaternion(goal.orientation * hlsl::makeQuaternionFromEulerDegreesYXZ(hlsl::getCastedVector(delta.rotationEulerDegOffset))); + } + + if (delta.hasTargetOffset) + { + if (!goal.hasTargetPosition) + { + if (error) + *error = "Sequence keyframe target_offset requires target state."; + return false; + } + goal.targetPosition += delta.targetOffset; } - if (delta.hasOrbitUDeltaDeg) - goal.orbitU = hlsl::wrapAngleRad(goal.orbitU + hlsl::radians(delta.orbitUDeltaDeg)); - if (delta.hasOrbitVDeltaDeg) + + if (delta.hasOrbitUDeltaDeg || delta.hasOrbitVDeltaDeg || delta.hasOrbitDistanceDelta) { - goal.orbitV = std::clamp( - goal.orbitV + hlsl::radians(delta.orbitVDeltaDeg), - -SCameraTargetRelativeRigDefaults::ArcballPitchLimitRad, - SCameraTargetRelativeRigDefaults::ArcballPitchLimitRad); + if (!goal.hasOrbitState) + { + if (error) + *error = "Sequence keyframe orbit deltas require spherical orbit state."; + return false; + } + + const auto orbitUvDeltaRad = hlsl::float64_t2( + delta.hasOrbitUDeltaDeg ? static_cast(hlsl::radians(delta.orbitUvDeltaDeg.x)) : hlsl::float64_t(0.0), + delta.hasOrbitVDeltaDeg ? static_cast(hlsl::radians(delta.orbitUvDeltaDeg.y)) : hlsl::float64_t(0.0)); + if (delta.hasOrbitUDeltaDeg) + goal.orbitUv.x = hlsl::wrapAngleRad(goal.orbitUv.x + orbitUvDeltaRad.x); + if (delta.hasOrbitVDeltaDeg) + { + goal.orbitUv.y = std::clamp( + goal.orbitUv.y + orbitUvDeltaRad.y, + -SCameraTargetRelativeRigDefaults::ArcballPitchLimitRad, + SCameraTargetRelativeRigDefaults::ArcballPitchLimitRad); + } + if (delta.hasOrbitDistanceDelta) + goal.orbitDistance += delta.orbitDistanceDelta; } - if (delta.hasOrbitDistanceDelta) - goal.orbitDistance += delta.orbitDistanceDelta; - } - if (delta.hasPathAngleDeltaDeg || delta.hasPathRadiusDelta || delta.hasPathHeightDelta) - { - if (!goal.hasPathState) + if (delta.hasPathAngleDeltaDeg || delta.hasPathRadiusDelta || delta.hasPathHeightDelta) { - if (error) - *error = "Sequence keyframe path deltas require path state."; - return false; + if (!goal.hasPathState) + { + if (error) + *error = "Sequence keyframe path deltas require path state."; + return false; + } + + const auto pathDelta = SCameraPathDelta::fromVector(hlsl::float64_t3( + delta.hasPathRadiusDelta ? static_cast(delta.pathRadiusDelta) : hlsl::float64_t(0.0), + delta.hasPathHeightDelta ? static_cast(delta.pathHeightDelta) : hlsl::float64_t(0.0), + delta.hasPathAngleDeltaDeg ? static_cast(hlsl::radians(delta.pathAngleDeltaDeg)) : hlsl::float64_t(0.0))); + if (!CCameraPathUtilities::tryApplyPathStateDelta(goal.pathState, pathDelta, CCameraPathUtilities::makeDefaultPathLimits(), goal.pathState)) + { + if (error) + *error = "Sequence keyframe path deltas produced an invalid path state."; + return false; + } } - const auto pathDelta = SCameraPathDelta::fromVector(hlsl::float64_t3( - delta.hasPathRadiusDelta ? static_cast(delta.pathRadiusDelta) : hlsl::float64_t(0.0), - delta.hasPathHeightDelta ? static_cast(delta.pathHeightDelta) : hlsl::float64_t(0.0), - delta.hasPathAngleDeltaDeg ? static_cast(hlsl::radians(delta.pathAngleDeltaDeg)) : hlsl::float64_t(0.0))); - if (!tryApplyPathStateDelta(goal.pathState, pathDelta, makeDefaultPathLimits(), goal.pathState)) + if (delta.hasDynamicBaseFovDelta || delta.hasDynamicReferenceDistanceDelta) { - if (error) - *error = "Sequence keyframe path deltas produced an invalid path state."; - return false; + if (!goal.hasDynamicPerspectiveState) + { + if (error) + *error = "Sequence keyframe dynamic perspective deltas require dynamic perspective state."; + return false; + } + if (delta.hasDynamicBaseFovDelta) + goal.dynamicPerspectiveState.baseFov = std::clamp(goal.dynamicPerspectiveState.baseFov + delta.dynamicBaseFovDelta, 1.f, 179.f); + if (delta.hasDynamicReferenceDistanceDelta) + goal.dynamicPerspectiveState.referenceDistance = std::max(0.001f, goal.dynamicPerspectiveState.referenceDistance + delta.dynamicReferenceDistanceDelta); } - } - if (delta.hasDynamicBaseFovDelta || delta.hasDynamicReferenceDistanceDelta) - { - if (!goal.hasDynamicPerspectiveState) + if (hasPathDelta || hasSphericalDelta) { - if (error) - *error = "Sequence keyframe dynamic perspective deltas require dynamic perspective state."; - return false; + if (!CCameraGoalUtilities::applyCanonicalGoalState(goal)) + { + if (error) + *error = hasPathDelta ? + "Sequence keyframe failed to canonicalize path state." : + "Sequence keyframe failed to canonicalize spherical state."; + return false; + } } - if (delta.hasDynamicBaseFovDelta) - goal.dynamicPerspectiveState.baseFov = std::clamp(goal.dynamicPerspectiveState.baseFov + delta.dynamicBaseFovDelta, 1.f, 179.f); - if (delta.hasDynamicReferenceDistanceDelta) - goal.dynamicPerspectiveState.referenceDistance = std::max(0.001f, goal.dynamicPerspectiveState.referenceDistance + delta.dynamicReferenceDistanceDelta); - } - if (hasPathDelta || hasSphericalDelta) - { - if (!applyCanonicalGoalState(goal)) + if (!CCameraGoalUtilities::isGoalFinite(goal)) { if (error) - *error = hasPathDelta ? - "Sequence keyframe failed to canonicalize path state." : - "Sequence keyframe failed to canonicalize spherical state."; + *error = "Sequence keyframe produced a non-finite goal."; return false; } + + assignGoalToPreset(outPreset, goal); + return true; } - if (!isGoalFinite(goal)) + static inline bool buildSequenceTrackFromReference(const CCameraPreset& reference, const CCameraSequenceSegment& segment, CCameraKeyframeTrack& outTrack, std::string* error = nullptr) { - if (error) - *error = "Sequence keyframe produced a non-finite goal."; - return false; - } + outTrack = {}; + outTrack.keyframes.reserve(segment.keyframes.size()); - assignGoalToPreset(outPreset, goal); - return true; -} + for (const auto& entry : segment.keyframes) + { + CCameraKeyframe keyframe; + keyframe.time = std::max(0.f, entry.time); + if (!buildSequenceKeyframePreset(reference, entry, keyframe.preset, error)) + return false; + outTrack.keyframes.emplace_back(std::move(keyframe)); + } -inline bool buildSequenceTrackFromReference(const CCameraPreset& reference, const CCameraSequenceSegment& segment, CCameraKeyframeTrack& outTrack, std::string* error = nullptr) -{ - outTrack = {}; - outTrack.keyframes.reserve(segment.keyframes.size()); + sortKeyframeTrackByTime(outTrack); + normalizeSelectedKeyframeTrack(outTrack); + return !outTrack.keyframes.empty(); + } - for (const auto& entry : segment.keyframes) + static inline bool isSequenceTrackedTargetPoseFinite(const CCameraSequenceTrackedTargetPose& pose) { - CCameraKeyframe keyframe; - keyframe.time = std::max(0.f, entry.time); - if (!buildSequenceKeyframePreset(reference, entry, keyframe.preset, error)) - return false; - outTrack.keyframes.emplace_back(std::move(keyframe)); + return hlsl::isFiniteVec3(pose.position) && + hlsl::isFiniteQuaternion(pose.orientation); } - sortKeyframeTrackByTime(outTrack); - normalizeSelectedKeyframeTrack(outTrack); - return !outTrack.keyframes.empty(); -} - -inline bool isSequenceTrackedTargetPoseFinite(const CCameraSequenceTrackedTargetPose& pose) -{ - return hlsl::isFiniteVec3(pose.position) && - hlsl::isFiniteQuaternion(pose.orientation); -} - -inline bool buildSequenceTrackedTargetPoseFromReference( + static inline bool buildSequenceTrackedTargetPoseFromReference( const CCameraSequenceTrackedTargetPose& reference, const CCameraSequenceTrackedTargetKeyframe& authored, CCameraSequenceTrackedTargetPose& outPose, std::string* error = nullptr) -{ - outPose = reference; + { + outPose = reference; if (authored.hasAbsolutePosition) outPose.position = authored.absolutePosition; @@ -476,17 +480,17 @@ inline bool buildSequenceTrackedTargetPoseFromReference( return false; } - return true; -} + return true; + } -inline bool buildSequenceTrackedTargetTrackFromReference( + static inline bool buildSequenceTrackedTargetTrackFromReference( const CCameraSequenceTrackedTargetPose& reference, const CCameraSequenceSegment& segment, CCameraSequenceTrackedTargetTrack& outTrack, std::string* error = nullptr) -{ - outTrack = {}; - outTrack.keyframes.reserve(segment.targetKeyframes.size()); + { + outTrack = {}; + outTrack.keyframes.reserve(segment.targetKeyframes.size()); for (const auto& entry : segment.targetKeyframes) { @@ -516,125 +520,125 @@ inline bool buildSequenceTrackedTargetTrackFromReference( } outTrack.keyframes = std::move(normalized); - return !outTrack.keyframes.empty(); -} + return !outTrack.keyframes.empty(); + } -inline bool tryBuildSequenceTrackedTargetPoseAtTime( + static inline bool tryBuildSequenceTrackedTargetPoseAtTime( const CCameraSequenceTrackedTargetTrack& track, const float time, CCameraSequenceTrackedTargetPose& outPose) -{ - if (track.keyframes.empty()) - return false; - if (track.keyframes.size() == 1u || time <= track.keyframes.front().time) { - outPose = track.keyframes.front().pose; + if (track.keyframes.empty()) + return false; + if (track.keyframes.size() == 1u || time <= track.keyframes.front().time) + { + outPose = track.keyframes.front().pose; + return true; + } + if (time >= track.keyframes.back().time) + { + outPose = track.keyframes.back().pose; + return true; + } + + for (size_t ix = 1u; ix < track.keyframes.size(); ++ix) + { + const auto& lhs = track.keyframes[ix - 1u]; + const auto& rhs = track.keyframes[ix]; + if (time > rhs.time) + continue; + + const auto span = std::max(static_cast(ICamera::ScalarTolerance), rhs.time - lhs.time); + const auto alpha = std::clamp((time - lhs.time) / span, 0.f, 1.f); + outPose.position = lhs.pose.position + (rhs.pose.position - lhs.pose.position) * static_cast(alpha); + outPose.orientation = hlsl::slerpQuaternion(lhs.pose.orientation, rhs.pose.orientation, static_cast(alpha)); + return true; + } + + outPose = track.keyframes.back().pose; return true; } - if (time >= track.keyframes.back().time) + + static inline bool sequenceSegmentUsesTrackedTargetTrack(const CCameraSequenceSegment& segment) { - outPose = track.keyframes.back().pose; - return true; + return !segment.targetKeyframes.empty(); } - for (size_t ix = 1u; ix < track.keyframes.size(); ++ix) + static inline float getSequenceSegmentDurationSeconds(const CCameraSequenceScript& script, const CCameraSequenceSegment& segment, const CCameraKeyframeTrack* track = nullptr) { - const auto& lhs = track.keyframes[ix - 1u]; - const auto& rhs = track.keyframes[ix]; - if (time > rhs.time) - continue; - - const auto span = std::max(static_cast(ICamera::ScalarTolerance), rhs.time - lhs.time); - const auto alpha = std::clamp((time - lhs.time) / span, 0.f, 1.f); - outPose.position = lhs.pose.position + (rhs.pose.position - lhs.pose.position) * static_cast(alpha); - outPose.orientation = hlsl::slerpQuaternion(lhs.pose.orientation, rhs.pose.orientation, static_cast(alpha)); - return true; + if (segment.hasDurationSeconds) + return std::max(0.f, segment.durationSeconds); + if (script.defaults.durationSeconds > 0.f) + return script.defaults.durationSeconds; + if (track) + return track->keyframes.empty() ? 0.f : track->keyframes.back().time; + return 0.f; } - outPose = track.keyframes.back().pose; - return true; -} - -inline bool sequenceSegmentUsesTrackedTargetTrack(const CCameraSequenceSegment& segment) -{ - return !segment.targetKeyframes.empty(); -} - -inline float getSequenceSegmentDurationSeconds(const CCameraSequenceScript& script, const CCameraSequenceSegment& segment, const CCameraKeyframeTrack* track = nullptr) -{ - if (segment.hasDurationSeconds) - return std::max(0.f, segment.durationSeconds); - if (script.defaults.durationSeconds > 0.f) - return script.defaults.durationSeconds; - if (track) - return track->keyframes.empty() ? 0.f : track->keyframes.back().time; - return 0.f; -} - -inline const std::vector& getSequenceSegmentPresentations(const CCameraSequenceScript& script, const CCameraSequenceSegment& segment) -{ - return segment.presentations.empty() ? script.defaults.presentations : segment.presentations; -} - -inline CCameraSequenceContinuitySettings getSequenceSegmentContinuity(const CCameraSequenceScript& script, const CCameraSequenceSegment& segment) -{ - return segment.hasContinuity ? segment.continuity : script.defaults.continuity; -} + static inline const std::vector& getSequenceSegmentPresentations(const CCameraSequenceScript& script, const CCameraSequenceSegment& segment) + { + return segment.presentations.empty() ? script.defaults.presentations : segment.presentations; + } -inline std::vector getSequenceSegmentCaptureFractions(const CCameraSequenceScript& script, const CCameraSequenceSegment& segment) -{ - auto captures = segment.hasCaptureFractions ? segment.captureFractions : script.defaults.captureFractions; - normalizeCaptureFractions(captures); - return captures; -} + static inline CCameraSequenceContinuitySettings getSequenceSegmentContinuity(const CCameraSequenceScript& script, const CCameraSequenceSegment& segment) + { + return segment.hasContinuity ? segment.continuity : script.defaults.continuity; + } -inline bool getSequenceSegmentResetCamera(const CCameraSequenceScript& script, const CCameraSequenceSegment& segment) -{ - return segment.hasResetCamera ? segment.resetCamera : script.defaults.resetCamera; -} + static inline std::vector getSequenceSegmentCaptureFractions(const CCameraSequenceScript& script, const CCameraSequenceSegment& segment) + { + auto captures = segment.hasCaptureFractions ? segment.captureFractions : script.defaults.captureFractions; + normalizeCaptureFractions(captures); + return captures; + } -inline bool sequenceScriptUsesMultiplePresentations(const CCameraSequenceScript& script) -{ - if (script.defaults.presentations.size() > 1u) - return true; + static inline bool getSequenceSegmentResetCamera(const CCameraSequenceScript& script, const CCameraSequenceSegment& segment) + { + return segment.hasResetCamera ? segment.resetCamera : script.defaults.resetCamera; + } - for (const auto& segment : script.segments) + static inline bool sequenceScriptUsesMultiplePresentations(const CCameraSequenceScript& script) { - if (getSequenceSegmentPresentations(script, segment).size() > 1u) + if (script.defaults.presentations.size() > 1u) return true; - } - return false; -} + for (const auto& segment : script.segments) + { + if (getSequenceSegmentPresentations(script, segment).size() > 1u) + return true; + } -inline uint64_t buildSequenceDurationFrames(const float durationSeconds, const float fps) -{ - const auto safeDuration = std::max(0.f, durationSeconds); - const auto safeFps = std::max(1.f, fps); - return std::max(1ull, static_cast(std::llround(static_cast(safeDuration) * static_cast(safeFps)))); -} + return false; + } -//! Build one sampled time per authored frame in the compiled segment. -inline void buildSequenceSampleTimes(const float durationSeconds, const uint64_t durationFrames, std::vector& outTimes) -{ - outTimes.clear(); - outTimes.reserve(durationFrames); + static inline uint64_t buildSequenceDurationFrames(const float durationSeconds, const float fps) + { + const auto safeDuration = std::max(0.f, durationSeconds); + const auto safeFps = std::max(1.f, fps); + return std::max(1ull, static_cast(std::llround(static_cast(safeDuration) * static_cast(safeFps)))); + } - for (uint64_t frameOffset = 0u; frameOffset < durationFrames; ++frameOffset) + //! Build one sampled time per authored frame in the compiled segment. + static inline void buildSequenceSampleTimes(const float durationSeconds, const uint64_t durationFrames, std::vector& outTimes) { - const float alpha = durationFrames > 1u ? static_cast(frameOffset) / static_cast(durationFrames - 1u) : 0.f; - outTimes.emplace_back(durationSeconds * alpha); + outTimes.clear(); + outTimes.reserve(durationFrames); + + for (uint64_t frameOffset = 0u; frameOffset < durationFrames; ++frameOffset) + { + const float alpha = durationFrames > 1u ? static_cast(frameOffset) / static_cast(durationFrames - 1u) : 0.f; + outTimes.emplace_back(durationSeconds * alpha); + } } -} -//! Expand normalized capture fractions into concrete frame offsets inside the compiled segment. -inline void buildSequenceCaptureFrameOffsets( + //! Expand normalized capture fractions into concrete frame offsets inside the compiled segment. + static inline void buildSequenceCaptureFrameOffsets( const uint64_t durationFrames, const std::vector& captureFractions, std::vector& outOffsets) -{ - outOffsets.clear(); - outOffsets.reserve(captureFractions.size()); + { + outOffsets.clear(); + outOffsets.reserve(captureFractions.size()); for (const auto fraction : captureFractions) { @@ -645,47 +649,47 @@ inline void buildSequenceCaptureFrameOffsets( } std::sort(outOffsets.begin(), outOffsets.end()); - outOffsets.erase(std::unique(outOffsets.begin(), outOffsets.end()), outOffsets.end()); -} + outOffsets.erase(std::unique(outOffsets.begin(), outOffsets.end()), outOffsets.end()); + } -//! Compile one authored sequence segment into normalized reusable data for runtime consumers. -inline bool compileSequenceSegmentFromReference( + //! Compile one authored sequence segment into normalized reusable data for runtime consumers. + static inline bool compileSequenceSegmentFromReference( const CCameraSequenceScript& script, const CCameraSequenceSegment& segment, const CCameraPreset& referencePreset, const CCameraSequenceTrackedTargetPose& referenceTrackedTargetPose, CCameraSequenceCompiledSegment& outSegment, std::string* error = nullptr) -{ - outSegment = {}; - outSegment.name = segment.name; - outSegment.presentations = getSequenceSegmentPresentations(script, segment); - outSegment.continuity = getSequenceSegmentContinuity(script, segment); - outSegment.resetCamera = getSequenceSegmentResetCamera(script, segment); + { + outSegment = {}; + outSegment.name = segment.name; + outSegment.presentations = getSequenceSegmentPresentations(script, segment); + outSegment.continuity = getSequenceSegmentContinuity(script, segment); + outSegment.resetCamera = getSequenceSegmentResetCamera(script, segment); - if (!buildSequenceTrackFromReference(referencePreset, segment, outSegment.track, error)) - return false; + if (!buildSequenceTrackFromReference(referencePreset, segment, outSegment.track, error)) + return false; - if (sequenceSegmentUsesTrackedTargetTrack(segment) && - !buildSequenceTrackedTargetTrackFromReference(referenceTrackedTargetPose, segment, outSegment.trackedTargetTrack, error)) - { - return false; - } + if (sequenceSegmentUsesTrackedTargetTrack(segment) && + !buildSequenceTrackedTargetTrackFromReference(referenceTrackedTargetPose, segment, outSegment.trackedTargetTrack, error)) + { + return false; + } - outSegment.durationSeconds = getSequenceSegmentDurationSeconds(script, segment, &outSegment.track); - outSegment.durationFrames = buildSequenceDurationFrames(outSegment.durationSeconds, script.fps); - buildSequenceSampleTimes(outSegment.durationSeconds, outSegment.durationFrames, outSegment.sampleTimes); - buildSequenceCaptureFrameOffsets(outSegment.durationFrames, getSequenceSegmentCaptureFractions(script, segment), outSegment.captureFrameOffsets); - return true; -} + outSegment.durationSeconds = getSequenceSegmentDurationSeconds(script, segment, &outSegment.track); + outSegment.durationFrames = buildSequenceDurationFrames(outSegment.durationSeconds, script.fps); + buildSequenceSampleTimes(outSegment.durationSeconds, outSegment.durationFrames, outSegment.sampleTimes); + buildSequenceCaptureFrameOffsets(outSegment.durationFrames, getSequenceSegmentCaptureFractions(script, segment), outSegment.captureFrameOffsets); + return true; + } -inline bool buildCompiledSegmentFramePolicies( + static inline bool buildCompiledSegmentFramePolicies( const CCameraSequenceCompiledSegment& segment, std::vector& outPolicies, const bool includeFollowTargetLock = false) -{ - if (segment.sampleTimes.size() != segment.durationFrames) - return false; + { + if (segment.sampleTimes.size() != segment.durationFrames) + return false; outPolicies.clear(); outPolicies.reserve(segment.durationFrames); @@ -709,8 +713,9 @@ inline bool buildCompiledSegmentFramePolicies( outPolicies.emplace_back(std::move(policy)); } - return true; -} + return true; + } +}; } // namespace nbl::core diff --git a/common/include/camera/CCameraSequenceScriptedBuilder.hpp b/common/include/camera/CCameraSequenceScriptedBuilder.hpp index db09943aa..6c1e26fce 100644 --- a/common/include/camera/CCameraSequenceScriptedBuilder.hpp +++ b/common/include/camera/CCameraSequenceScriptedBuilder.hpp @@ -42,7 +42,7 @@ inline bool appendCompiledSequenceSegmentToScriptedTimeline( std::string* error = nullptr) { std::vector framePolicies; - if (!buildCompiledSegmentFramePolicies(compiledSegment, framePolicies, buildInfo.includeFollowTargetLock)) + if (!core::CCameraSequenceScriptUtilities::buildCompiledSegmentFramePolicies(compiledSegment, framePolicies, buildInfo.includeFollowTargetLock)) { if (error) *error = "Failed to build compiled frame policies."; @@ -86,7 +86,7 @@ inline bool appendCompiledSequenceSegmentToScriptedTimeline( if (compiledSegment.usesTrackedTargetTrack()) { core::CCameraSequenceTrackedTargetPose trackedTargetPose; - if (!tryBuildSequenceTrackedTargetPoseAtTime(compiledSegment.trackedTargetTrack, policy.sampleTime, trackedTargetPose)) + if (!core::CCameraSequenceScriptUtilities::tryBuildSequenceTrackedTargetPoseAtTime(compiledSegment.trackedTargetTrack, policy.sampleTime, trackedTargetPose)) { if (error) *error = "Failed to sample compiled tracked-target track."; diff --git a/common/include/camera/CCameraTargetRelativeUtilities.hpp b/common/include/camera/CCameraTargetRelativeUtilities.hpp index b94ca5381..616adf301 100644 --- a/common/include/camera/CCameraTargetRelativeUtilities.hpp +++ b/common/include/camera/CCameraTargetRelativeUtilities.hpp @@ -12,8 +12,7 @@ namespace nbl::core struct SCameraTargetRelativeState final { hlsl::float64_t3 target = hlsl::float64_t3(0.0); - double orbitU = 0.0; - double orbitV = 0.0; + hlsl::float64_t2 orbitUv = hlsl::float64_t2(0.0); float distance = ICamera::SphericalMinDistance; }; @@ -37,13 +36,12 @@ struct SCameraTargetRelativeBasis final //! Delta between current spherical target state and canonical target-relative goal. struct SCameraTargetRelativeDelta final { - double orbitU = 0.0; - double orbitV = 0.0; + hlsl::float64_t2 orbitUv = hlsl::float64_t2(0.0); double distance = 0.0; inline hlsl::float64_t3 orbitVector() const { - return hlsl::float64_t3(orbitV, orbitU, 0.0); + return hlsl::float64_t3(orbitUv.y, orbitUv.x, 0.0); } }; @@ -109,159 +107,163 @@ struct SCameraTargetRelativeRigDefaults final }; }; -inline bool tryBuildTargetRelativeStateFromPosition( - const hlsl::float64_t3& targetPosition, - const hlsl::float64_t3& position, - const float minDistance, - const float maxDistance, - SCameraTargetRelativeState& outState) +struct CCameraTargetRelativeUtilities final { - outState = {}; - outState.target = targetPosition; - - hlsl::float64_t appliedDistance = static_cast(minDistance); - if (!hlsl::tryBuildOrbitFromPosition( - targetPosition, - position, - static_cast(minDistance), - static_cast(maxDistance), - outState.orbitU, - outState.orbitV, - appliedDistance)) + static inline bool tryBuildTargetRelativeStateFromPosition( + const hlsl::float64_t3& targetPosition, + const hlsl::float64_t3& position, + const float minDistance, + const float maxDistance, + SCameraTargetRelativeState& outState) { - return false; - } + outState = {}; + outState.target = targetPosition; - outState.distance = static_cast(appliedDistance); - return true; -} + hlsl::float64_t appliedDistance = static_cast(minDistance); + if (!hlsl::tryBuildOrbitFromPosition( + targetPosition, + position, + static_cast(minDistance), + static_cast(maxDistance), + outState.orbitUv.x, + outState.orbitUv.y, + appliedDistance)) + { + return false; + } -inline bool tryBuildTargetRelativePoseFromState( - const SCameraTargetRelativeState& state, - const float minDistance, - const float maxDistance, - SCameraTargetRelativePose& outPose) -{ - outPose = {}; - return hlsl::tryBuildSphericalPoseFromOrbit( - state.target, - state.orbitU, - state.orbitV, - static_cast(state.distance), - static_cast(minDistance), - static_cast(maxDistance), - outPose.position, - outPose.orientation, - &outPose.appliedDistance); -} + outState.distance = static_cast(appliedDistance); + return true; + } -inline bool tryBuildTargetRelativeBasis( - const SCameraTargetRelativeState& state, - const float minDistance, - const float maxDistance, - SCameraTargetRelativeBasis& outBasis) -{ - SCameraTargetRelativePose pose = {}; - if (!tryBuildTargetRelativePoseFromState(state, minDistance, maxDistance, pose)) - return false; + static inline bool tryBuildTargetRelativePoseFromState( + const SCameraTargetRelativeState& state, + const float minDistance, + const float maxDistance, + SCameraTargetRelativePose& outPose) + { + outPose = {}; + return hlsl::tryBuildSphericalPoseFromOrbit( + state.target, + state.orbitUv.x, + state.orbitUv.y, + static_cast(state.distance), + static_cast(minDistance), + static_cast(maxDistance), + outPose.position, + outPose.orientation, + &outPose.appliedDistance); + } - outBasis.localOffset = pose.position - state.target; - const auto basis = hlsl::getQuaternionBasisMatrix(pose.orientation); - outBasis.right = basis[0]; - outBasis.up = basis[1]; - outBasis.forward = basis[2]; - return true; -} + static inline bool tryBuildTargetRelativeBasis( + const SCameraTargetRelativeState& state, + const float minDistance, + const float maxDistance, + SCameraTargetRelativeBasis& outBasis) + { + SCameraTargetRelativePose pose = {}; + if (!tryBuildTargetRelativePoseFromState(state, minDistance, maxDistance, pose)) + return false; -inline bool tryBuildTargetRelativePoseFromPosition( - const hlsl::float64_t3& targetPosition, - const hlsl::float64_t3& position, - const float minDistance, - const float maxDistance, - SCameraTargetRelativePose& outPose, - SCameraTargetRelativeState* outState = nullptr) -{ - SCameraTargetRelativeState state = {}; - if (!tryBuildTargetRelativeStateFromPosition(targetPosition, position, minDistance, maxDistance, state)) - return false; + outBasis.localOffset = pose.position - state.target; + const auto basis = hlsl::getQuaternionBasisMatrix(pose.orientation); + outBasis.right = basis[0]; + outBasis.up = basis[1]; + outBasis.forward = basis[2]; + return true; + } - if (!tryBuildTargetRelativePoseFromState(state, minDistance, maxDistance, outPose)) - return false; + static inline bool tryBuildTargetRelativePoseFromPosition( + const hlsl::float64_t3& targetPosition, + const hlsl::float64_t3& position, + const float minDistance, + const float maxDistance, + SCameraTargetRelativePose& outPose, + SCameraTargetRelativeState* outState = nullptr) + { + SCameraTargetRelativeState state = {}; + if (!tryBuildTargetRelativeStateFromPosition(targetPosition, position, minDistance, maxDistance, state)) + return false; - if (outState) - *outState = state; - return true; -} + if (!tryBuildTargetRelativePoseFromState(state, minDistance, maxDistance, outPose)) + return false; -inline SCameraTargetRelativeDelta buildTargetRelativeDelta( - const ICamera::SphericalTargetState& currentState, - const SCameraTargetRelativeState& desiredState) -{ - return { - .orbitU = hlsl::wrapAngleRad(desiredState.orbitU - currentState.u), - .orbitV = hlsl::wrapAngleRad(desiredState.orbitV - currentState.v), - .distance = static_cast(desiredState.distance - currentState.distance) - }; -} + if (outState) + *outState = state; + return true; + } -inline void appendTargetRelativeDeltaEvents( - std::vector& events, - const SCameraTargetRelativeDelta& delta, - const double angularDenominator, - const double angularToleranceDeg, - const double distanceDenominator, - const double distanceTolerance, - const SCameraTargetRelativeEventPolicy& policy) -{ - if (policy.translateOrbit) + static inline SCameraTargetRelativeDelta buildTargetRelativeDelta( + const ICamera::SphericalTargetState& currentState, + const SCameraTargetRelativeState& desiredState) { - appendAngularAxisEvents( - events, - delta.orbitVector(), - hlsl::float64_t3(angularDenominator), - hlsl::float64_t3(angularToleranceDeg, angularToleranceDeg, std::numeric_limits::infinity()), - {{ - { CVirtualGimbalEvent::MoveRight, CVirtualGimbalEvent::MoveLeft }, - { CVirtualGimbalEvent::MoveUp, CVirtualGimbalEvent::MoveDown }, - { CVirtualGimbalEvent::None, CVirtualGimbalEvent::None } - }}); + return { + .orbitUv = hlsl::float64_t2( + hlsl::wrapAngleRad(desiredState.orbitUv.x - currentState.orbitUv.x), + hlsl::wrapAngleRad(desiredState.orbitUv.y - currentState.orbitUv.y)), + .distance = static_cast(desiredState.distance - currentState.distance) + }; } - else + + static inline void appendTargetRelativeDeltaEvents( + std::vector& events, + const SCameraTargetRelativeDelta& delta, + const double angularDenominator, + const double angularToleranceDeg, + const double distanceDenominator, + const double distanceTolerance, + const SCameraTargetRelativeEventPolicy& policy) { - if (policy.allowYaw) + if (policy.translateOrbit) { - appendAngularDeltaEvent( + appendAngularAxisEvents( events, - delta.orbitU, - angularDenominator, - angularToleranceDeg, - CVirtualGimbalEvent::PanRight, - CVirtualGimbalEvent::PanLeft); + delta.orbitVector(), + hlsl::float64_t3(angularDenominator), + hlsl::float64_t3(angularToleranceDeg, angularToleranceDeg, std::numeric_limits::infinity()), + {{ + { CVirtualGimbalEvent::MoveRight, CVirtualGimbalEvent::MoveLeft }, + { CVirtualGimbalEvent::MoveUp, CVirtualGimbalEvent::MoveDown }, + { CVirtualGimbalEvent::None, CVirtualGimbalEvent::None } + }}); } - if (policy.allowPitch) + else { - appendAngularDeltaEvent( - events, - delta.orbitV, - angularDenominator, - angularToleranceDeg, - CVirtualGimbalEvent::TiltUp, - CVirtualGimbalEvent::TiltDown); + if (policy.allowYaw) + { + appendAngularDeltaEvent( + events, + delta.orbitUv.x, + angularDenominator, + angularToleranceDeg, + CVirtualGimbalEvent::PanRight, + CVirtualGimbalEvent::PanLeft); + } + if (policy.allowPitch) + { + appendAngularDeltaEvent( + events, + delta.orbitUv.y, + angularDenominator, + angularToleranceDeg, + CVirtualGimbalEvent::TiltUp, + CVirtualGimbalEvent::TiltDown); + } } - } - if (policy.distanceBinding.positive != CVirtualGimbalEvent::None && - policy.distanceBinding.negative != CVirtualGimbalEvent::None) - { - appendScaledVirtualEvent( - events, - delta.distance, - distanceDenominator, - distanceTolerance, - policy.distanceBinding.positive, - policy.distanceBinding.negative); + if (policy.distanceBinding.positive != CVirtualGimbalEvent::None && + policy.distanceBinding.negative != CVirtualGimbalEvent::None) + { + appendScaledVirtualEvent( + events, + delta.distance, + distanceDenominator, + distanceTolerance, + policy.distanceBinding.positive, + policy.distanceBinding.negative); + } } -} +}; } // namespace nbl::core diff --git a/common/include/camera/CCameraViewportOverlayUtilities.hpp b/common/include/camera/CCameraViewportOverlayUtilities.hpp index fa594980e..2284c3fae 100644 --- a/common/include/camera/CCameraViewportOverlayUtilities.hpp +++ b/common/include/camera/CCameraViewportOverlayUtilities.hpp @@ -261,7 +261,7 @@ inline void drawFollowTargetViewportOverlay( return; system::SCameraProjectedTargetMetrics projectedTarget = {}; - if (!system::tryComputeProjectedFollowTargetMetrics(projectionContext, trackedTarget, projectedTarget)) + if (!system::CCameraFollowRegressionUtilities::tryComputeProjectedFollowTargetMetrics(projectionContext, trackedTarget, projectedTarget)) return; const bool centered = projectedTarget.radius <= style.centeredNdcRadius; diff --git a/common/include/camera/CPathCamera.hpp b/common/include/camera/CPathCamera.hpp index d66980b83..faf6272f0 100644 --- a/common/include/camera/CPathCamera.hpp +++ b/common/include/camera/CPathCamera.hpp @@ -21,7 +21,7 @@ class CPathCamera final : public CSphericalTargetCamera CPathCamera(const hlsl::float64_t3& position, const hlsl::float64_t3& target) : base_t(position, target) { - tryResolvePathState(target, position, SCameraPathDefaults::Limits, nullptr, m_pathState); + CCameraPathUtilities::tryResolvePathState(target, position, SCameraPathDefaults::Limits, nullptr, m_pathState); updateFromPathState(); } ~CPathCamera() = default; @@ -39,13 +39,13 @@ class CPathCamera final : public CSphericalTargetCamera CReferenceTransform reference = {}; if (!m_gimbal.extractReferenceTransform(&reference, referenceFrame)) return false; - if (!tryResolvePathState(m_targetPosition, hlsl::float64_t3(reference.frame[3]), SCameraPathDefaults::Limits, nullptr, nextPathState)) + if (!CCameraPathUtilities::tryResolvePathState(m_targetPosition, hlsl::float64_t3(reference.frame[3]), SCameraPathDefaults::Limits, nullptr, nextPathState)) return false; } const auto impulse = m_gimbal.accumulate(virtualEvents); - const auto stateDelta = makePathDeltaFromVirtualPathTranslate(scaleVirtualTranslation(impulse.dVirtualTranslate)); - if (!tryApplyPathStateDelta(nextPathState, stateDelta, SCameraPathDefaults::Limits, nextPathState)) + const auto stateDelta = CCameraPathUtilities::makePathDeltaFromVirtualPathTranslate(scaleVirtualTranslation(impulse.dVirtualTranslate)); + if (!CCameraPathUtilities::tryApplyPathStateDelta(nextPathState, stateDelta, SCameraPathDefaults::Limits, nextPathState)) return false; m_pathState = nextPathState; @@ -63,10 +63,10 @@ class CPathCamera final : public CSphericalTargetCamera virtual bool trySetPathState(const PathState& state) override { auto sanitized = state; - if (!sanitizePathState(sanitized, SCameraPathDefaults::Limits.minRadius)) + if (!CCameraPathUtilities::sanitizePathState(sanitized, SCameraPathDefaults::Limits.minRadius)) return false; - const bool exact = pathStatesNearlyEqual(sanitized, state, SCameraPathDefaults::ExactComparisonThresholds); + const bool exact = CCameraPathUtilities::pathStatesNearlyEqual(sanitized, state, SCameraPathDefaults::ExactComparisonThresholds); m_pathState = sanitized; updateFromPathState(); return exact; @@ -74,7 +74,7 @@ class CPathCamera final : public CSphericalTargetCamera virtual bool trySetSphericalDistance(float distance) override { SCameraPathDistanceUpdateResult distanceUpdate = {}; - if (!tryUpdatePathStateDistance(distance, SCameraPathDefaults::Limits, m_pathState, &distanceUpdate)) + if (!CCameraPathUtilities::tryUpdatePathStateDistance(distance, SCameraPathDefaults::Limits, m_pathState, &distanceUpdate)) return false; updateFromPathState(); @@ -85,19 +85,19 @@ class CPathCamera final : public CSphericalTargetCamera private: static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate; - PathState m_pathState = makeDefaultPathState(SCameraPathDefaults::Limits.minRadius); + PathState m_pathState = CCameraPathUtilities::makeDefaultPathState(SCameraPathDefaults::Limits.minRadius); bool updateFromPathState() { SCameraCanonicalPathState canonicalPathState = {}; - if (!tryBuildCanonicalPathState(m_targetPosition, m_pathState, SCameraPathDefaults::Limits, canonicalPathState)) + if (!CCameraPathUtilities::tryBuildCanonicalPathState(m_targetPosition, m_pathState, SCameraPathDefaults::Limits, canonicalPathState)) { return false; } m_distance = canonicalPathState.targetRelative.distance; - m_u = canonicalPathState.targetRelative.orbitU; - m_v = canonicalPathState.targetRelative.orbitV; + m_u = canonicalPathState.targetRelative.orbitUv.x; + m_v = canonicalPathState.targetRelative.orbitUv.y; m_gimbal.begin(); { diff --git a/common/include/camera/CSphericalTargetCamera.hpp b/common/include/camera/CSphericalTargetCamera.hpp index b6f7d7fcf..62317d36f 100644 --- a/common/include/camera/CSphericalTargetCamera.hpp +++ b/common/include/camera/CSphericalTargetCamera.hpp @@ -61,8 +61,7 @@ class CSphericalTargetCamera : public ICamera { out.target = m_targetPosition; out.distance = m_distance; - out.u = m_u; - out.v = m_v; + out.orbitUv = hlsl::float64_t2(m_u, m_v); out.minDistance = MinDistance; out.maxDistance = MaxDistance; return true; @@ -87,11 +86,10 @@ class CSphericalTargetCamera : public ICamera SphericalBasis basis; const SCameraTargetRelativeState state = { .target = m_targetPosition, - .orbitU = orbitU, - .orbitV = orbitV, + .orbitUv = hlsl::float64_t2(orbitU, orbitV), .distance = distance }; - if (!tryBuildTargetRelativeBasis(state, MinDistance, MaxDistance, basis)) + if (!CCameraTargetRelativeUtilities::tryBuildTargetRelativeBasis(state, MinDistance, MaxDistance, basis)) return basis; return basis; } @@ -99,7 +97,7 @@ class CSphericalTargetCamera : public ICamera inline void initFromPosition(const hlsl::float64_t3& position) { SCameraTargetRelativeState state = {}; - if (!tryBuildTargetRelativeStateFromPosition(m_targetPosition, position, MinDistance, MaxDistance, state)) + if (!CCameraTargetRelativeUtilities::tryBuildTargetRelativeStateFromPosition(m_targetPosition, position, MinDistance, MaxDistance, state)) { m_distance = MinDistance; m_u = 0.0; @@ -108,8 +106,8 @@ class CSphericalTargetCamera : public ICamera } m_distance = state.distance; - m_u = state.orbitU; - m_v = state.orbitV; + m_u = state.orbitUv.x; + m_v = state.orbitUv.y; } inline void applyPlanarTargetTranslation(const hlsl::float64_t3& deltaTranslation, const SphericalBasis& basis) @@ -128,12 +126,11 @@ class CSphericalTargetCamera : public ICamera { const SCameraTargetRelativeState state = { .target = m_targetPosition, - .orbitU = m_u, - .orbitV = m_v, + .orbitUv = hlsl::float64_t2(m_u, m_v), .distance = m_distance }; SCameraTargetRelativePose pose = {}; - if (!tryBuildTargetRelativePoseFromState(state, MinDistance, MaxDistance, pose)) + if (!CCameraTargetRelativeUtilities::tryBuildTargetRelativePoseFromState(state, MinDistance, MaxDistance, pose)) return false; m_distance = static_cast(pose.appliedDistance); diff --git a/common/include/camera/ICamera.hpp b/common/include/camera/ICamera.hpp index 9140bcd4b..4c119cb4a 100644 --- a/common/include/camera/ICamera.hpp +++ b/common/include/camera/ICamera.hpp @@ -74,9 +74,8 @@ class ICamera : virtual public core::IReferenceCounted struct SphericalTargetState { hlsl::float64_t3 target = hlsl::float64_t3(0.0); + hlsl::float64_t2 orbitUv = hlsl::float64_t2(0.0); float distance = 0.f; - double u = 0.0; - double v = 0.0; float minDistance = 0.f; float maxDistance = 0.f; }; diff --git a/common/include/camera/README.md b/common/include/camera/README.md index bd8fbc50c..8c7b0fbe6 100644 --- a/common/include/camera/README.md +++ b/common/include/camera/README.md @@ -508,7 +508,7 @@ follow.mode = ECameraFollowMode::KeepLocalOffset; follow.localOffset = float64_t3(-4.0, 0.0, 1.0); CCameraGoalSolver solver; -auto result = applyFollowToCamera(solver, camera.get(), trackedTarget, follow); +auto result = CCameraFollowUtilities::applyFollowToCamera(solver, camera.get(), trackedTarget, follow); ``` ### Sequence / CI integration diff --git a/common/src/nbl/examples/camera/CCameraJsonPersistenceUtilities.hpp b/common/src/nbl/examples/camera/CCameraJsonPersistenceUtilities.hpp index 0195e19c5..7189f259c 100644 --- a/common/src/nbl/examples/camera/CCameraJsonPersistenceUtilities.hpp +++ b/common/src/nbl/examples/camera/CCameraJsonPersistenceUtilities.hpp @@ -46,12 +46,12 @@ inline void deserializeGoalJson(const Json& entry, core::CCameraGoal& goal) } if (entry.contains("orbit_u")) { - goal.orbitU = entry["orbit_u"].get(); + goal.orbitUv.x = entry["orbit_u"].get(); goal.hasOrbitState = true; } if (entry.contains("orbit_v")) { - goal.orbitV = entry["orbit_v"].get(); + goal.orbitUv.y = entry["orbit_v"].get(); goal.hasOrbitState = true; } if (entry.contains("orbit_distance")) diff --git a/common/src/nbl/examples/camera/CCameraPersistence.cpp b/common/src/nbl/examples/camera/CCameraPersistence.cpp index f13f01e31..0f0584bed 100644 --- a/common/src/nbl/examples/camera/CCameraPersistence.cpp +++ b/common/src/nbl/examples/camera/CCameraPersistence.cpp @@ -34,8 +34,8 @@ json_t serializeGoalJson(const nbl::core::CCameraGoal& goal) json["distance"] = goal.distance; if (goal.hasOrbitState) { - json["orbit_u"] = goal.orbitU; - json["orbit_v"] = goal.orbitV; + json["orbit_u"] = goal.orbitUv.x; + json["orbit_v"] = goal.orbitUv.y; json["orbit_distance"] = goal.orbitDistance; } if (goal.hasPathState) diff --git a/common/src/nbl/examples/camera/CCameraScriptedRuntimePersistence.cpp b/common/src/nbl/examples/camera/CCameraScriptedRuntimePersistence.cpp index bc599a077..b4d82b9f4 100644 --- a/common/src/nbl/examples/camera/CCameraScriptedRuntimePersistence.cpp +++ b/common/src/nbl/examples/camera/CCameraScriptedRuntimePersistence.cpp @@ -69,7 +69,7 @@ bool deserializeSequencePresentationsJson(const json_t& root, std::vector(), presentation.projection)) + if (!nbl::core::CCameraSequenceScriptUtilities::tryParseProjectionType(entry["projection"].get(), presentation.projection)) { if (error) *error = "Sequence presentation has invalid projection type."; @@ -172,12 +172,12 @@ bool deserializeSequenceGoalDeltaJson(const json_t& root, nbl::core::CCameraSequ } if (root.contains("orbit_u_delta_deg")) { - out.orbitUDeltaDeg = root["orbit_u_delta_deg"].get(); + out.orbitUvDeltaDeg.x = root["orbit_u_delta_deg"].get(); out.hasOrbitUDeltaDeg = true; } if (root.contains("orbit_v_delta_deg")) { - out.orbitVDeltaDeg = root["orbit_v_delta_deg"].get(); + out.orbitUvDeltaDeg.y = root["orbit_v_delta_deg"].get(); out.hasOrbitVDeltaDeg = true; } if (root.contains("orbit_distance_delta")) @@ -325,7 +325,7 @@ bool deserializeSequenceSegmentJson(const json_t& root, nbl::core::CCameraSequen out.cameraIdentifier = root["camera_identifier"].get(); if (root.contains("camera_kind")) { - if (!nbl::core::tryParseCameraKind(root["camera_kind"].get(), out.cameraKind)) + if (!nbl::core::CCameraSequenceScriptUtilities::tryParseCameraKind(root["camera_kind"].get(), out.cameraKind)) { if (error) *error = "Sequence segment has invalid camera_kind."; @@ -374,7 +374,7 @@ bool deserializeSequenceSegmentJson(const json_t& root, nbl::core::CCameraSequen } out.captureFractions.emplace_back(fraction); } - nbl::core::normalizeCaptureFractions(out.captureFractions); + nbl::core::CCameraSequenceScriptUtilities::normalizeCaptureFractions(out.captureFractions); out.hasCaptureFractions = true; } if (root.contains("keyframes")) @@ -507,7 +507,7 @@ bool deserializeCameraSequenceScriptJson(const json_t& root, nbl::core::CCameraS } out.defaults.captureFractions.emplace_back(fraction); } - nbl::core::normalizeCaptureFractions(out.defaults.captureFractions); + nbl::core::CCameraSequenceScriptUtilities::normalizeCaptureFractions(out.defaults.captureFractions); } } From 5763d911912157d758aa4978e479528e95a90f47 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Thu, 9 Apr 2026 11:00:54 +0200 Subject: [PATCH 183/205] Refactor camera utility helpers --- 61_UI/AppControlPanelCameraTab.cpp | 10 +- 61_UI/AppFollowRuntime.cpp | 6 +- 61_UI/AppHeadlessCameraSmokeChecks.inl | 12 +- 61_UI/AppHeadlessCameraSmokeHelpers.inl | 30 +- 61_UI/AppManipulableObjects.cpp | 2 +- 61_UI/AppPresetPlayback.cpp | 4 +- 61_UI/AppScriptedInitialization.cpp | 2 +- 61_UI/AppScriptedInputRuntime.cpp | 4 +- 61_UI/AppViewportWindows.cpp | 2 +- 61_UI/include/common.hpp | 10 +- common/include/camera/CCameraGoalSolver.hpp | 4 +- .../camera/CCameraInputBindingUtilities.hpp | 804 ++++++++---------- .../camera/CCameraManipulationUtilities.hpp | 4 +- .../include/camera/CCameraPathUtilities.hpp | 4 +- .../camera/CCameraPresentationUtilities.hpp | 10 +- .../camera/CCameraTargetRelativeUtilities.hpp | 8 +- .../include/camera/CCameraTextUtilities.hpp | 322 +++---- .../camera/CCameraVirtualEventUtilities.hpp | 242 +++--- common/include/camera/README.md | 2 +- 19 files changed, 709 insertions(+), 773 deletions(-) diff --git a/61_UI/AppControlPanelCameraTab.cpp b/61_UI/AppControlPanelCameraTab.cpp index 66b51c43f..0113c8177 100644 --- a/61_UI/AppControlPanelCameraTab.cpp +++ b/61_UI/AppControlPanelCameraTab.cpp @@ -100,11 +100,11 @@ void App::drawControlPanelCameraTab(const nbl::ui::SCameraControlPanelStyle& pan nbl::ui::drawCheckboxWithHint({ .label = "Enable follow", .value = &followConfig.enabled, .hint = "Apply tracked-target follow to the active planar camera" }); const char* followModeLabels[] = { - getCameraFollowModeLabel(ECameraFollowMode::Disabled), - getCameraFollowModeLabel(ECameraFollowMode::OrbitTarget), - getCameraFollowModeLabel(ECameraFollowMode::LookAtTarget), - getCameraFollowModeLabel(ECameraFollowMode::KeepWorldOffset), - getCameraFollowModeLabel(ECameraFollowMode::KeepLocalOffset) + 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))) diff --git a/61_UI/AppFollowRuntime.cpp b/61_UI/AppFollowRuntime.cpp index 0b6355d28..b8e6f4d64 100644 --- a/61_UI/AppFollowRuntime.cpp +++ b/61_UI/AppFollowRuntime.cpp @@ -35,8 +35,8 @@ inline nbl::ui::SCameraScriptVisualDebugStatus buildScriptVisualDebugStatus( const auto elapsedFrames = computeElapsedFrames(absoluteFrame, scriptedInput.visualPlanar); nbl::ui::SCameraScriptVisualDebugStatus status = {}; - status.cameraLabel = getCameraTypeLabel(&camera); - status.cameraHint = getCameraTypeDescription(&camera); + status.cameraLabel = CCameraTextUtilities::getCameraTypeLabel(&camera); + status.cameraHint = CCameraTextUtilities::getCameraTypeDescription(&camera); status.cameraIndex = planarIx; status.cameraCount = static_cast(planarCount); status.planarIndex = planarIx; @@ -47,7 +47,7 @@ inline nbl::ui::SCameraScriptVisualDebugStatus buildScriptVisualDebugStatus( status.absoluteFrame = absoluteFrame; status.segmentLabel = scriptedInput.visualPlanar.segmentLabel; status.followActive = scriptedInput.visualFollow.active; - status.followModeDescription = nbl::ui::getCameraFollowModeDescription(scriptedInput.visualFollow.mode); + status.followModeDescription = nbl::ui::CCameraTextUtilities::getCameraFollowModeDescription(scriptedInput.visualFollow.mode); status.followLockValid = scriptedInput.visualFollow.lockValid; status.followLockAngleDeg = scriptedInput.visualFollow.lockAngleDeg; status.followTargetDistance = scriptedInput.visualFollow.targetDistance; diff --git a/61_UI/AppHeadlessCameraSmokeChecks.inl b/61_UI/AppHeadlessCameraSmokeChecks.inl index 0aa956a87..5b27eb130 100644 --- a/61_UI/AppHeadlessCameraSmokeChecks.inl +++ b/61_UI/AppHeadlessCameraSmokeChecks.inl @@ -771,29 +771,29 @@ } } - if (getCameraTypeLabel(ICamera::CameraKind::DollyZoom) != "Dolly Zoom") + if (CCameraTextUtilities::getCameraTypeLabel(ICamera::CameraKind::DollyZoom) != "Dolly Zoom") { outError = "Camera text utilities smoke failed for Dolly Zoom label."; return false; } - if (getCameraTypeDescription(ICamera::CameraKind::Path) != std::string(nbl::core::SCameraPathDefaults::Description)) + if (CCameraTextUtilities::getCameraTypeDescription(ICamera::CameraKind::Path) != std::string(nbl::core::SCameraPathDefaults::Description)) { outError = "Camera text utilities smoke failed for Path description."; return false; } - if (describeGoalStateMask(ICamera::GoalStateNone) != "Pose only") + if (CCameraTextUtilities::describeGoalStateMask(ICamera::GoalStateNone) != "Pose only") { outError = "Camera text utilities smoke failed for empty goal-state description."; return false; } - if (describeGoalStateMask(ICamera::GoalStateSphericalTarget | ICamera::GoalStateDynamicPerspective) != "Spherical target, Dynamic perspective") + if (CCameraTextUtilities::describeGoalStateMask(ICamera::GoalStateSphericalTarget | ICamera::GoalStateDynamicPerspective) != "Spherical target, Dynamic perspective") { outError = "Camera text utilities smoke failed for combined goal-state description."; return false; } CCameraGoalSolver::SApplyResult defaultApplyResult; - const auto applyResultText = describeApplyResult(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."; @@ -804,7 +804,7 @@ summary.targetCount = 2u; summary.successCount = 2u; summary.approximateCount = 1u; - const auto summaryText = nbl::ui::describePresetApplySummary(summary, "none"); + 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."; diff --git a/61_UI/AppHeadlessCameraSmokeHelpers.inl b/61_UI/AppHeadlessCameraSmokeHelpers.inl index 34bb9bdad..fa4569fec 100644 --- a/61_UI/AppHeadlessCameraSmokeHelpers.inl +++ b/61_UI/AppHeadlessCameraSmokeHelpers.inl @@ -419,7 +419,7 @@ namespace (requireChanged && !applyResult.changed()) || (requireExact && !applyResult.exact)) { - outError = std::string(failurePrefix) + ". " + describeApplyResult(applyResult); + outError = std::string(failurePrefix) + ". " + CCameraTextUtilities::describeApplyResult(applyResult); return false; } @@ -443,7 +443,7 @@ namespace if (restoreResult.succeeded() && nbl::system::comparePresetToCameraStateWithStrictThresholds(goalSolver, camera, preset)) return true; - outError = std::string(failurePrefix) + ". " + describeApplyResult(restoreResult); + outError = std::string(failurePrefix) + ". " + CCameraTextUtilities::describeApplyResult(restoreResult); if (camera) outError += " " + nbl::core::describePresetCameraMismatch(goalSolver, camera, preset); return false; @@ -603,7 +603,7 @@ namespace } CGimbalInputBinder inputBinder; - applyDefaultCameraInputBindingPreset(inputBinder, *camera); + CCameraInputBindingUtilities::applyDefaultCameraInputBindingPreset(inputBinder, *camera); const std::string cameraIdentifier(camera->getIdentifier()); const auto initialPreset = nbl::core::capturePreset(goalSolver, camera, "smoke-initial"); @@ -611,7 +611,7 @@ namespace if (!initialCompatibility.exact || initialCompatibility.missingGoalStateMask != ICamera::GoalStateNone) { outError = "Preset compatibility smoke failed for camera \"" + cameraIdentifier + - "\". missing=" + describeGoalStateMask(initialCompatibility.missingGoalStateMask); + "\". missing=" + CCameraTextUtilities::describeGoalStateMask(initialCompatibility.missingGoalStateMask); return false; } @@ -782,7 +782,7 @@ namespace nbl::system::SCameraManipulationDelta keyboardDelta = {}; for (const auto key : nbl::ui::SCameraInputBindingPhysicalGroups::KeyboardProbeCodes) { - applyDefaultCameraInputBindingPreset(inputBinder, *camera); + CCameraInputBindingUtilities::applyDefaultCameraInputBindingPreset(inputBinder, *camera); auto keyboardEvents = collectKeyboardVirtualEvents(inputBinder, key); if (keyboardEvents.empty()) continue; @@ -798,9 +798,9 @@ namespace return false; } - const auto& mousePreset = getDefaultCameraMouseMappingPreset(*camera); - const bool hasMoveMapping = nbl::ui::hasMouseRelativeMovementBinding(mousePreset); - const bool hasScrollMapping = nbl::ui::hasMouseScrollBinding(mousePreset); + 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) @@ -816,7 +816,7 @@ namespace return false; } - applyDefaultCameraInputBindingPreset(inputBinder, *camera); + CCameraInputBindingUtilities::applyDefaultCameraInputBindingPreset(inputBinder, *camera); auto mouseMoveEvents = collectMouseVirtualEvents(inputBinder, { filteredMoveLookDown.data(), filteredMoveLookDown.size() }); if (mouseMoveEvents.empty()) { @@ -837,7 +837,7 @@ namespace const std::array rawScroll = { scrollEv }; auto filteredScroll = filterOrbitMouseEvents(camera, rawScroll, false); - applyDefaultCameraInputBindingPreset(inputBinder, *camera); + CCameraInputBindingUtilities::applyDefaultCameraInputBindingPreset(inputBinder, *camera); auto mouseScrollEvents = collectMouseVirtualEvents(inputBinder, { filteredScroll.data(), filteredScroll.size() }); if (mouseScrollEvents.empty()) { @@ -882,7 +882,7 @@ namespace if (compatibility.exact || compatibility.missingGoalStateMask != expectedMissingGoalStateMask) { outError = std::string("Cross-kind preset compatibility smoke failed for ") + label + - ". missing=" + describeGoalStateMask(compatibility.missingGoalStateMask); + ". missing=" + CCameraTextUtilities::describeGoalStateMask(compatibility.missingGoalStateMask); return false; } @@ -890,7 +890,7 @@ namespace const auto applyResult = nbl::core::applyPresetDetailed(goalSolver, targetCamera, sourcePreset); if (!applyResult.succeeded() || !applyResult.approximate() || !applyResult.hasIssue(expectedIssue)) { - outError = std::string("Cross-kind preset smoke failed for ") + label + ". " + describeApplyResult(applyResult); + outError = std::string("Cross-kind preset smoke failed for ") + label + ". " + CCameraTextUtilities::describeApplyResult(applyResult); return false; } @@ -919,7 +919,7 @@ namespace if (!compatibility.exact || compatibility.missingGoalStateMask != ICamera::GoalStateNone) { outError = std::string("Exact cross-kind preset compatibility smoke failed for ") + label + - ". missing=" + describeGoalStateMask(compatibility.missingGoalStateMask); + ". missing=" + CCameraTextUtilities::describeGoalStateMask(compatibility.missingGoalStateMask); return false; } @@ -1111,7 +1111,7 @@ namespace if (!editedApply.succeeded() || !editedApply.changed()) { outError = std::string("Follow recapture smoke failed to apply edited preset for ") + std::string(label) + - ". " + describeApplyResult(editedApply); + ". " + CCameraTextUtilities::describeApplyResult(editedApply); return false; } @@ -1134,7 +1134,7 @@ namespace if (!recapturedApply.succeeded()) { outError = std::string("Follow recapture smoke failed to apply recaptured follow for ") + std::string(label) + - ". " + describeApplyResult(recapturedApply); + ". " + CCameraTextUtilities::describeApplyResult(recapturedApply); return false; } diff --git a/61_UI/AppManipulableObjects.cpp b/61_UI/AppManipulableObjects.cpp index 55b7be33e..8c7d36947 100644 --- a/61_UI/AppManipulableObjects.cpp +++ b/61_UI/AppManipulableObjects.cpp @@ -91,7 +91,7 @@ bool App::tryBuildManipulableObjectContext(const uint32_t objectIx, SManipulable outContext.kind = SceneManipulatedObjectKind::Camera; outContext.planarIx = planarIx; outContext.camera = camera; - outContext.label = std::string(getCameraTypeLabel(camera)) + " Camera"; + outContext.label = std::string(CCameraTextUtilities::getCameraTypeLabel(camera)) + " Camera"; outContext.transform = buildCameraManipulationTransform(*camera); outContext.worldPosition = buildCameraWorldPosition(*camera); return true; diff --git a/61_UI/AppPresetPlayback.cpp b/61_UI/AppPresetPlayback.cpp index be3ac685a..4a693d0a5 100644 --- a/61_UI/AppPresetPlayback.cpp +++ b/61_UI/AppPresetPlayback.cpp @@ -38,7 +38,7 @@ CCameraGoalSolver::SApplyResult App::applyPresetFromUi(ICamera* camera, const Ca const auto presetUi = analyzePresetForUi(camera, preset); storeApplyStatusBanner( m_presetAuthoring.applyBanner, - describeApplyResult(result) + " | " + presetUi.compatibilityLabel, + CCameraTextUtilities::describeApplyResult(result) + " | " + presetUi.compatibilityLabel, result.succeeded(), result.approximate()); return result; @@ -63,7 +63,7 @@ void App::storePlaybackApplySummary(const SCameraPresetApplySummary& summary) const auto& playbackAuthoring = m_playbackAuthoring; storeApplyStatusBanner( m_playbackAuthoring.applyBanner, - nbl::ui::describePresetApplySummary( + nbl::ui::CCameraTextUtilities::describePresetApplySummary( summary, playbackAuthoring.affectsAll ? "Playback apply | no cameras available" : "Playback apply | no active camera"), summary.succeeded(), diff --git a/61_UI/AppScriptedInitialization.cpp b/61_UI/AppScriptedInitialization.cpp index 37dc649bd..150f6b200 100644 --- a/61_UI/AppScriptedInitialization.cpp +++ b/61_UI/AppScriptedInitialization.cpp @@ -170,7 +170,7 @@ bool App::expandPendingScriptedSequence(const CCameraSequenceScript& sequence) const auto planarIx = resolveSequenceSegmentPlanarIx(segment); if (!planarIx.has_value()) { - const auto kindLabel = segment.cameraKind != ICamera::CameraKind::Unknown ? std::string(getCameraTypeLabel(segment.cameraKind)) : std::string("Unknown"); + 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(), diff --git a/61_UI/AppScriptedInputRuntime.cpp b/61_UI/AppScriptedInputRuntime.cpp index deadad6c1..50d379786 100644 --- a/61_UI/AppScriptedInputRuntime.cpp +++ b/61_UI/AppScriptedInputRuntime.cpp @@ -257,7 +257,7 @@ void App::applyScriptedImguizmoInput(SScriptedFrameInputState& scriptedFrame, co auto* camera = runtimeContext.viewport.camera; CGimbalInputBinder imguizmoBinding; - applyDefaultCameraInputBindingPreset(imguizmoBinding, *camera); + CCameraInputBindingUtilities::applyDefaultCameraInputBindingPreset(imguizmoBinding, *camera); auto collectedEvents = imguizmoBinding.collectVirtualEvents(m_nextPresentationTimestamp, { .imguizmoEvents = { scriptedFrame.frameEvents.imguizmo.data(), scriptedFrame.frameEvents.imguizmo.size() } }); @@ -301,7 +301,7 @@ void App::applyScriptedGoals(const CCameraScriptedFrameEvents& scriptedFrameEven static_cast(m_realFrameIx), result.succeeded() ? "inexact" : "failed", result.exact ? 1 : 0, - describeApplyResult(result).c_str()); + CCameraTextUtilities::describeApplyResult(result).c_str()); } } diff --git a/61_UI/AppViewportWindows.cpp b/61_UI/AppViewportWindows.cpp index 9271f68f2..e5016a016 100644 --- a/61_UI/AppViewportWindows.cpp +++ b/61_UI/AppViewportWindows.cpp @@ -81,7 +81,7 @@ void App::drawViewportWindowOverlay( 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(getCameraTypeLabel(viewportState.camera)) + ": " + std::string(getCameraTypeDescription(viewportState.camera)); + overlayData.description = std::string(CCameraTextUtilities::getCameraTypeLabel(viewportState.camera)) + ": " + std::string(CCameraTextUtilities::getCameraTypeDescription(viewportState.camera)); overlayData.detail = "Frustum: active camera (hidden in owner view)"; nbl::ui::drawViewportInfoOverlay(drawList, viewportRect, overlayData); } diff --git a/61_UI/include/common.hpp b/61_UI/include/common.hpp index ecac143fa..aa5ece83d 100644 --- a/61_UI/include/common.hpp +++ b/61_UI/include/common.hpp @@ -189,12 +189,8 @@ using nbl::hlsl::float64_t4; using nbl::hlsl::float64_t4x4; using nbl::hlsl::uint16_t2; using nbl::hlsl::getQuaternionEulerDegrees; -using nbl::ui::describeApplyResult; -using nbl::ui::describeGoalStateMask; -using nbl::ui::getCameraFollowModeLabel; -using nbl::ui::getCameraFollowModeDescription; -using nbl::ui::getCameraTypeDescription; -using nbl::ui::getCameraTypeLabel; +using nbl::ui::CCameraInputBindingUtilities; +using nbl::ui::CCameraTextUtilities; using nbl::hlsl::getCastedMatrix; using nbl::hlsl::getCastedVector; using nbl::hlsl::getMatrix3x4As4x4; @@ -210,7 +206,5 @@ using nbl::hlsl::makeIdentityQuaternion; using nbl::hlsl::normalizeQuaternion; using nbl::core::CCameraFollowUtilities; using nbl::core::syncDynamicPerspectiveProjection; -using nbl::ui::applyDefaultCameraInputBindingPreset; -using nbl::ui::getDefaultCameraMouseMappingPreset; #endif // _NBL_THIS_EXAMPLE_COMMON_H_INCLUDED_ diff --git a/common/include/camera/CCameraGoalSolver.hpp b/common/include/camera/CCameraGoalSolver.hpp index 206395bd4..cfc13a0f0 100644 --- a/common/include/camera/CCameraGoalSolver.hpp +++ b/common/include/camera/CCameraGoalSolver.hpp @@ -411,7 +411,7 @@ class CCameraGoalSolver if (!includeRoll) tolerances.z = std::numeric_limits::infinity(); - appendAngularAxisEvents( + CCameraVirtualEventUtilities::appendAngularAxisEvents( events, eulerRadians, hlsl::float64_t3(denominator), @@ -593,7 +593,7 @@ class CCameraGoalSolver const auto& gimbal = camera->getGimbal(); const auto currentPos = gimbal.getPosition(); const auto deltaWorld = target.position - currentPos; - appendWorldTranslationAsLocalEvents( + CCameraVirtualEventUtilities::appendWorldTranslationAsLocalEvents( out, gimbal.getOrientation(), deltaWorld, diff --git a/common/include/camera/CCameraInputBindingUtilities.hpp b/common/include/camera/CCameraInputBindingUtilities.hpp index c472f2064..b2a1817a5 100644 --- a/common/include/camera/CCameraInputBindingUtilities.hpp +++ b/common/include/camera/CCameraInputBindingUtilities.hpp @@ -11,31 +11,6 @@ namespace nbl::ui { -template -inline constexpr std::array concatBindingCodeArrays( - const std::array& lhs, - const std::array& rhs) -{ - std::array output = {}; - for (size_t i = 0u; i < N; ++i) - output[i] = lhs[i]; - for (size_t i = 0u; i < M; ++i) - output[N + i] = rhs[i]; - return output; -} - -template -inline constexpr auto concatBindingCodeArrays( - const std::array& lhs, - const std::array& rhs, - const std::array&... rest) -{ - if constexpr (sizeof...(rest) == 0u) - return concatBindingCodeArrays(lhs, rhs); - else - return concatBindingCodeArrays(concatBindingCodeArrays(lhs, rhs), rest...); -} - //! Reusable keyboard, mouse, and ImGuizmo binding preset grouped for one camera kind. struct SCameraInputBindingPreset { @@ -69,11 +44,20 @@ struct SCameraInputBindingPhysicalGroups final ui::E_KEY_CODE::EKC_U, ui::E_KEY_CODE::EKC_O }; - static inline constexpr auto KeyboardProbeCodes = concatBindingCodeArrays( - KeyboardProbeMoveCodes, - KeyboardQeCodes, - KeyboardProbeLookCodes, - KeyboardProbeExtraCodes); + static inline constexpr std::array KeyboardProbeCodes = { + ui::E_KEY_CODE::EKC_W, + ui::E_KEY_CODE::EKC_S, + ui::E_KEY_CODE::EKC_A, + ui::E_KEY_CODE::EKC_D, + ui::E_KEY_CODE::EKC_Q, + ui::E_KEY_CODE::EKC_E, + ui::E_KEY_CODE::EKC_I, + ui::E_KEY_CODE::EKC_K, + ui::E_KEY_CODE::EKC_J, + ui::E_KEY_CODE::EKC_L, + ui::E_KEY_CODE::EKC_U, + ui::E_KEY_CODE::EKC_O + }; static inline constexpr std::array RelativeMouseCodes = { ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_X, ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_X, @@ -90,469 +74,419 @@ struct SCameraInputBindingPhysicalGroups final }; }; -template -inline bool containsBindingForAnyCode(const Map& preset, const Codes& codes) +struct CCameraInputBindingUtilities final { - for (const auto code : codes) +public: + static inline bool hasMouseRelativeMovementBinding(const IGimbalBindingLayout::mouse_to_virtual_events_t& mousePreset) { - if (preset.find(code) != preset.end()) - return true; + return containsBindingForAnyCodeGroups(mousePreset, SCameraInputBindingPhysicalGroups::RelativeMouseCodes); } - return false; -} -template -inline bool containsBindingForAnyCodeGroups(const Map& preset, const Codes&... codes) -{ - return (containsBindingForAnyCode(preset, codes) || ...); -} - -inline bool hasMouseRelativeMovementBinding(const IGimbalBindingLayout::mouse_to_virtual_events_t& mousePreset) -{ - return containsBindingForAnyCodeGroups(mousePreset, SCameraInputBindingPhysicalGroups::RelativeMouseCodes); -} + static inline bool hasMouseScrollBinding(const IGimbalBindingLayout::mouse_to_virtual_events_t& mousePreset) + { + return containsBindingForAnyCodeGroups( + mousePreset, + SCameraInputBindingPhysicalGroups::PositiveScrollCodes, + SCameraInputBindingPhysicalGroups::NegativeScrollCodes); + } -inline bool hasMouseScrollBinding(const IGimbalBindingLayout::mouse_to_virtual_events_t& mousePreset) -{ - return containsBindingForAnyCodeGroups( - mousePreset, - SCameraInputBindingPhysicalGroups::PositiveScrollCodes, - SCameraInputBindingPhysicalGroups::NegativeScrollCodes); -} + static inline const IGimbalBindingLayout::keyboard_to_virtual_events_t& getDefaultCameraKeyboardMappingPreset(const core::ICamera::CameraKind kind) + { + return interactionBindingPresetForKind(kind).keyboard; + } -namespace impl -{ -using virtual_event_t = core::CVirtualGimbalEvent::VirtualEventType; -using keyboard_axis_group_t = std::array; -using mouse_axis_group_t = std::array; -using scalar_axis_pair_t = std::array; + static inline const IGimbalBindingLayout::keyboard_to_virtual_events_t& getDefaultCameraKeyboardMappingPreset(const core::ICamera& camera) + { + return getDefaultCameraKeyboardMappingPreset(camera.getKind()); + } -struct SKeyboardPresetSpec final -{ - keyboard_axis_group_t wasd = { - core::CVirtualGimbalEvent::None, - core::CVirtualGimbalEvent::None, - core::CVirtualGimbalEvent::None, - core::CVirtualGimbalEvent::None - }; - scalar_axis_pair_t qe = { - core::CVirtualGimbalEvent::None, - core::CVirtualGimbalEvent::None - }; - keyboard_axis_group_t ijkl = { - core::CVirtualGimbalEvent::None, - core::CVirtualGimbalEvent::None, - core::CVirtualGimbalEvent::None, - core::CVirtualGimbalEvent::None - }; -}; + static inline const IGimbalBindingLayout::mouse_to_virtual_events_t& getDefaultCameraMouseMappingPreset(const core::ICamera::CameraKind kind) + { + return interactionBindingPresetForKind(kind).mouse; + } -struct SMousePresetSpec final -{ - mouse_axis_group_t relative = { - core::CVirtualGimbalEvent::None, - core::CVirtualGimbalEvent::None, - core::CVirtualGimbalEvent::None, - core::CVirtualGimbalEvent::None - }; - scalar_axis_pair_t scroll = { - core::CVirtualGimbalEvent::None, - core::CVirtualGimbalEvent::None - }; -}; + static inline const IGimbalBindingLayout::mouse_to_virtual_events_t& getDefaultCameraMouseMappingPreset(const core::ICamera& camera) + { + return getDefaultCameraMouseMappingPreset(camera.getKind()); + } -//! Shared virtual-event bundles reused across interaction families. -struct SCameraInputBindingEventGroups final -{ - static inline constexpr std::array FpsMove = { - core::CVirtualGimbalEvent::MoveForward, - core::CVirtualGimbalEvent::MoveBackward, - core::CVirtualGimbalEvent::MoveLeft, - core::CVirtualGimbalEvent::MoveRight - }; - static inline constexpr std::array OrbitTranslate = { - core::CVirtualGimbalEvent::MoveUp, - core::CVirtualGimbalEvent::MoveDown, - core::CVirtualGimbalEvent::MoveLeft, - core::CVirtualGimbalEvent::MoveRight - }; - static inline constexpr std::array OrbitZoom = { - core::CVirtualGimbalEvent::MoveForward, - core::CVirtualGimbalEvent::MoveBackward - }; - static inline constexpr std::array VerticalMove = { - core::CVirtualGimbalEvent::MoveDown, - core::CVirtualGimbalEvent::MoveUp - }; - static inline constexpr std::array PathRigAdvanceAndRadius = { - core::CVirtualGimbalEvent::MoveForward, - core::CVirtualGimbalEvent::MoveBackward, - core::CVirtualGimbalEvent::MoveLeft, - core::CVirtualGimbalEvent::MoveRight - }; - static inline constexpr std::array PathRigHeight = VerticalMove; - static inline constexpr std::array TurntableMove = { - core::CVirtualGimbalEvent::MoveForward, - core::CVirtualGimbalEvent::MoveBackward, - core::CVirtualGimbalEvent::PanLeft, - core::CVirtualGimbalEvent::PanRight - }; - static inline constexpr std::array LookYawPitch = { - core::CVirtualGimbalEvent::TiltDown, - core::CVirtualGimbalEvent::TiltUp, - core::CVirtualGimbalEvent::PanLeft, - core::CVirtualGimbalEvent::PanRight - }; - static inline constexpr std::array Roll = { - core::CVirtualGimbalEvent::RollLeft, - core::CVirtualGimbalEvent::RollRight - }; - static inline constexpr std::array PanOnly = { - core::CVirtualGimbalEvent::None, - core::CVirtualGimbalEvent::None, - core::CVirtualGimbalEvent::PanLeft, - core::CVirtualGimbalEvent::PanRight - }; - static inline constexpr std::array RelativeLook = { - core::CVirtualGimbalEvent::PanRight, - core::CVirtualGimbalEvent::PanLeft, - core::CVirtualGimbalEvent::TiltUp, - core::CVirtualGimbalEvent::TiltDown - }; - static inline constexpr std::array RelativeOrbitTranslate = { - core::CVirtualGimbalEvent::MoveRight, - core::CVirtualGimbalEvent::MoveLeft, - core::CVirtualGimbalEvent::MoveUp, - core::CVirtualGimbalEvent::MoveDown - }; - static inline constexpr std::array RelativeTopDown = { - core::CVirtualGimbalEvent::PanRight, - core::CVirtualGimbalEvent::PanLeft, - core::CVirtualGimbalEvent::MoveUp, - core::CVirtualGimbalEvent::MoveDown - }; -}; + static inline IGimbalBindingLayout::imguizmo_to_virtual_events_t buildDefaultCameraImguizmoMappingPreset(const uint32_t allowedVirtualEvents) + { + return makeImguizmoPreset(allowedVirtualEvents); + } -struct SCameraInteractionBindingSpec -{ - SKeyboardPresetSpec keyboard = {}; - SMousePresetSpec mouse = {}; -}; + static inline IGimbalBindingLayout::imguizmo_to_virtual_events_t buildDefaultCameraImguizmoMappingPreset(const core::ICamera& camera) + { + return buildDefaultCameraImguizmoMappingPreset(camera.getAllowedVirtualEvents()); + } -struct SCameraMappedInteractionBindingSpec -{ - IGimbalBindingLayout::keyboard_to_virtual_events_t keyboard; - IGimbalBindingLayout::mouse_to_virtual_events_t mouse; -}; + static inline SCameraInputBindingPreset buildDefaultCameraInputBindingPreset(const core::ICamera::CameraKind kind, const uint32_t allowedVirtualEvents) + { + SCameraInputBindingPreset preset; + preset.keyboard = getDefaultCameraKeyboardMappingPreset(kind); + preset.mouse = getDefaultCameraMouseMappingPreset(kind); + preset.imguizmo = buildDefaultCameraImguizmoMappingPreset(allowedVirtualEvents); + return preset; + } -inline constexpr size_t interactionFamilyIndex(const core::ECameraInteractionFamily family) -{ - return static_cast(family); -} + static inline SCameraInputBindingPreset buildDefaultCameraInputBindingPreset(const core::ICamera& camera) + { + return buildDefaultCameraInputBindingPreset(camera.getKind(), camera.getAllowedVirtualEvents()); + } -template -inline void appendBindingSpec(Map& preset, const Codes& codes, const Events& events) -{ - for (size_t i = 0u; i < codes.size() && i < events.size(); ++i) + static inline void applyDefaultCameraInputBindingPreset( + IGimbalBindingLayout& layout, + const core::ICamera::CameraKind kind, + const uint32_t allowedVirtualEvents) { - const auto event = events[i]; - if (event == core::CVirtualGimbalEvent::None) - continue; - preset.emplace(codes[i], IGimbalBindingLayout::CHashInfo(event)); + const auto preset = buildDefaultCameraInputBindingPreset(kind, allowedVirtualEvents); + layout.updateKeyboardMapping([&](auto& map) { map = preset.keyboard; }); + layout.updateMouseMapping([&](auto& map) { map = preset.mouse; }); + layout.updateImguizmoMapping([&](auto& map) { map = preset.imguizmo; }); } -} -template -inline void appendMirroredBindingSpec(Map& preset, const Codes& codes, const virtual_event_t event) -{ - if (event == core::CVirtualGimbalEvent::None) - return; + static inline void applyDefaultCameraInputBindingPreset(IGimbalBindingLayout& layout, const core::ICamera& camera) + { + applyDefaultCameraInputBindingPreset(layout, camera.getKind(), camera.getAllowedVirtualEvents()); + } - std::array> duplicatedEvents = {}; - duplicatedEvents.fill(event); - appendBindingSpec(preset, codes, duplicatedEvents); -} +private: + using virtual_event_t = core::CVirtualGimbalEvent::VirtualEventType; + using keyboard_axis_group_t = std::array; + using mouse_axis_group_t = std::array; + using scalar_axis_pair_t = std::array; -inline IGimbalBindingLayout::keyboard_to_virtual_events_t buildKeyboardPreset(const SKeyboardPresetSpec& spec) -{ - IGimbalBindingLayout::keyboard_to_virtual_events_t preset; - appendBindingSpec(preset, SCameraInputBindingPhysicalGroups::KeyboardWasdCodes, spec.wasd); - appendBindingSpec(preset, SCameraInputBindingPhysicalGroups::KeyboardQeCodes, spec.qe); - appendBindingSpec(preset, SCameraInputBindingPhysicalGroups::KeyboardIjklCodes, spec.ijkl); - return preset; -} - -inline IGimbalBindingLayout::mouse_to_virtual_events_t buildMousePreset(const SMousePresetSpec& spec) -{ - IGimbalBindingLayout::mouse_to_virtual_events_t preset; - appendBindingSpec(preset, SCameraInputBindingPhysicalGroups::RelativeMouseCodes, spec.relative); - appendMirroredBindingSpec(preset, SCameraInputBindingPhysicalGroups::PositiveScrollCodes, spec.scroll[0]); - appendMirroredBindingSpec(preset, SCameraInputBindingPhysicalGroups::NegativeScrollCodes, spec.scroll[1]); - return preset; -} - -inline IGimbalBindingLayout::imguizmo_to_virtual_events_t makeImguizmoPreset(const uint32_t allowedVirtualEvents) -{ - IGimbalBindingLayout::imguizmo_to_virtual_events_t preset; - for (const auto event : core::CVirtualGimbalEvent::VirtualEventsTypeTable) + struct SKeyboardPresetSpec final { - if (event == core::CVirtualGimbalEvent::None) - continue; - if ((allowedVirtualEvents & event) != event) - continue; - preset.emplace(event, IGimbalBindingLayout::CHashInfo(event)); - } - return preset; -} + keyboard_axis_group_t wasd = { + core::CVirtualGimbalEvent::None, + core::CVirtualGimbalEvent::None, + core::CVirtualGimbalEvent::None, + core::CVirtualGimbalEvent::None + }; + scalar_axis_pair_t qe = { + core::CVirtualGimbalEvent::None, + core::CVirtualGimbalEvent::None + }; + keyboard_axis_group_t ijkl = { + core::CVirtualGimbalEvent::None, + core::CVirtualGimbalEvent::None, + core::CVirtualGimbalEvent::None, + core::CVirtualGimbalEvent::None + }; + }; -inline constexpr SKeyboardPresetSpec makeKeyboardPresetSpec( - const keyboard_axis_group_t& wasd = {}, - const scalar_axis_pair_t& qe = {}, - const keyboard_axis_group_t& ijkl = {}) -{ - return { - .wasd = wasd, - .qe = qe, - .ijkl = ijkl + struct SMousePresetSpec final + { + mouse_axis_group_t relative = { + core::CVirtualGimbalEvent::None, + core::CVirtualGimbalEvent::None, + core::CVirtualGimbalEvent::None, + core::CVirtualGimbalEvent::None + }; + scalar_axis_pair_t scroll = { + core::CVirtualGimbalEvent::None, + core::CVirtualGimbalEvent::None + }; }; -} -inline constexpr SKeyboardPresetSpec withKeyboardQe( - const SKeyboardPresetSpec& base, - const scalar_axis_pair_t& qe) -{ - auto preset = base; - preset.qe = qe; - return preset; -} - -inline constexpr SKeyboardPresetSpec withKeyboardIjkl( - const SKeyboardPresetSpec& base, - const keyboard_axis_group_t& ijkl) -{ - auto preset = base; - preset.ijkl = ijkl; - return preset; -} - -inline constexpr SMousePresetSpec makeMousePresetSpec( - const mouse_axis_group_t& relative = {}, - const scalar_axis_pair_t& scroll = {}) -{ - return { - .relative = relative, - .scroll = scroll + //! Shared virtual-event bundles reused across interaction families. + struct SCameraInputBindingEventGroups final + { + static inline constexpr std::array FpsMove = { + core::CVirtualGimbalEvent::MoveForward, + core::CVirtualGimbalEvent::MoveBackward, + core::CVirtualGimbalEvent::MoveLeft, + core::CVirtualGimbalEvent::MoveRight + }; + static inline constexpr std::array OrbitTranslate = { + core::CVirtualGimbalEvent::MoveUp, + core::CVirtualGimbalEvent::MoveDown, + core::CVirtualGimbalEvent::MoveLeft, + core::CVirtualGimbalEvent::MoveRight + }; + static inline constexpr std::array OrbitZoom = { + core::CVirtualGimbalEvent::MoveForward, + core::CVirtualGimbalEvent::MoveBackward + }; + static inline constexpr std::array VerticalMove = { + core::CVirtualGimbalEvent::MoveDown, + core::CVirtualGimbalEvent::MoveUp + }; + static inline constexpr std::array PathRigAdvanceAndRadius = { + core::CVirtualGimbalEvent::MoveForward, + core::CVirtualGimbalEvent::MoveBackward, + core::CVirtualGimbalEvent::MoveLeft, + core::CVirtualGimbalEvent::MoveRight + }; + static inline constexpr std::array PathRigHeight = VerticalMove; + static inline constexpr std::array TurntableMove = { + core::CVirtualGimbalEvent::MoveForward, + core::CVirtualGimbalEvent::MoveBackward, + core::CVirtualGimbalEvent::PanLeft, + core::CVirtualGimbalEvent::PanRight + }; + static inline constexpr std::array LookYawPitch = { + core::CVirtualGimbalEvent::TiltDown, + core::CVirtualGimbalEvent::TiltUp, + core::CVirtualGimbalEvent::PanLeft, + core::CVirtualGimbalEvent::PanRight + }; + static inline constexpr std::array Roll = { + core::CVirtualGimbalEvent::RollLeft, + core::CVirtualGimbalEvent::RollRight + }; + static inline constexpr std::array PanOnly = { + core::CVirtualGimbalEvent::None, + core::CVirtualGimbalEvent::None, + core::CVirtualGimbalEvent::PanLeft, + core::CVirtualGimbalEvent::PanRight + }; + static inline constexpr std::array RelativeLook = { + core::CVirtualGimbalEvent::PanRight, + core::CVirtualGimbalEvent::PanLeft, + core::CVirtualGimbalEvent::TiltUp, + core::CVirtualGimbalEvent::TiltDown + }; + static inline constexpr std::array RelativeOrbitTranslate = { + core::CVirtualGimbalEvent::MoveRight, + core::CVirtualGimbalEvent::MoveLeft, + core::CVirtualGimbalEvent::MoveUp, + core::CVirtualGimbalEvent::MoveDown + }; + static inline constexpr std::array RelativeTopDown = { + core::CVirtualGimbalEvent::PanRight, + core::CVirtualGimbalEvent::PanLeft, + core::CVirtualGimbalEvent::MoveUp, + core::CVirtualGimbalEvent::MoveDown + }; }; -} -inline constexpr SMousePresetSpec withMouseScroll( - const SMousePresetSpec& base, - const scalar_axis_pair_t& scroll) -{ - auto preset = base; - preset.scroll = scroll; - return preset; -} - -inline constexpr SCameraInteractionBindingSpec makeInteractionBindingSpec( - const SKeyboardPresetSpec& keyboard = {}, - const SMousePresetSpec& mouse = {}) -{ - return { - .keyboard = keyboard, - .mouse = mouse + struct SCameraInteractionBindingSpec + { + SKeyboardPresetSpec keyboard = {}; + SMousePresetSpec mouse = {}; }; -} -inline constexpr SCameraInteractionBindingSpec withInteractionKeyboard( - const SCameraInteractionBindingSpec& base, - const SKeyboardPresetSpec& keyboard) -{ - auto spec = base; - spec.keyboard = keyboard; - return spec; -} - -inline constexpr SCameraInteractionBindingSpec withInteractionMouse( - const SCameraInteractionBindingSpec& base, - const SMousePresetSpec& mouse) -{ - auto spec = base; - spec.mouse = mouse; - return spec; -} + struct SCameraMappedInteractionBindingSpec + { + IGimbalBindingLayout::keyboard_to_virtual_events_t keyboard; + IGimbalBindingLayout::mouse_to_virtual_events_t mouse; + }; -inline constexpr SCameraInteractionBindingSpec EmptyInteractionBindingSpec = makeInteractionBindingSpec(); + template + static inline bool containsBindingForAnyCode(const Map& preset, const Codes& codes) + { + for (const auto code : codes) + { + if (preset.find(code) != preset.end()) + return true; + } + return false; + } -inline constexpr SKeyboardPresetSpec FpsKeyboardSpec = makeKeyboardPresetSpec( - SCameraInputBindingEventGroups::FpsMove, - {}, - SCameraInputBindingEventGroups::LookYawPitch); + template + static inline bool containsBindingForAnyCodeGroups(const Map& preset, const Codes&... codes) + { + return (containsBindingForAnyCode(preset, codes) || ...); + } -inline constexpr SKeyboardPresetSpec FreeKeyboardSpec = withKeyboardQe( - FpsKeyboardSpec, - SCameraInputBindingEventGroups::Roll); + static inline constexpr size_t interactionFamilyIndex(const core::ECameraInteractionFamily family) + { + return static_cast(family); + } -inline constexpr SKeyboardPresetSpec OrbitKeyboardSpec = makeKeyboardPresetSpec( - SCameraInputBindingEventGroups::OrbitTranslate, - SCameraInputBindingEventGroups::OrbitZoom); + template + static inline void appendBindingSpec(Map& preset, const Codes& codes, const Events& events) + { + for (size_t i = 0u; i < codes.size() && i < events.size(); ++i) + { + const auto event = events[i]; + if (event == core::CVirtualGimbalEvent::None) + continue; + preset.emplace(codes[i], IGimbalBindingLayout::CHashInfo(event)); + } + } -inline constexpr SKeyboardPresetSpec TargetRigKeyboardSpec = withKeyboardQe( - FpsKeyboardSpec, - SCameraInputBindingEventGroups::VerticalMove); + template + static inline void appendMirroredBindingSpec(Map& preset, const Codes& codes, const virtual_event_t event) + { + if (event == core::CVirtualGimbalEvent::None) + return; -inline constexpr SKeyboardPresetSpec TurntableKeyboardSpec = makeKeyboardPresetSpec( - SCameraInputBindingEventGroups::TurntableMove, - {}, - FpsKeyboardSpec.ijkl); + std::array> duplicatedEvents = {}; + duplicatedEvents.fill(event); + appendBindingSpec(preset, codes, duplicatedEvents); + } -inline constexpr SKeyboardPresetSpec TopDownKeyboardSpec = withKeyboardIjkl( - OrbitKeyboardSpec, - SCameraInputBindingEventGroups::PanOnly); + static inline IGimbalBindingLayout::keyboard_to_virtual_events_t buildKeyboardPreset(const SKeyboardPresetSpec& spec) + { + IGimbalBindingLayout::keyboard_to_virtual_events_t preset; + appendBindingSpec(preset, SCameraInputBindingPhysicalGroups::KeyboardWasdCodes, spec.wasd); + appendBindingSpec(preset, SCameraInputBindingPhysicalGroups::KeyboardQeCodes, spec.qe); + appendBindingSpec(preset, SCameraInputBindingPhysicalGroups::KeyboardIjklCodes, spec.ijkl); + return preset; + } -inline constexpr SKeyboardPresetSpec PathKeyboardSpec = withKeyboardQe( - makeKeyboardPresetSpec(SCameraInputBindingEventGroups::PathRigAdvanceAndRadius), - SCameraInputBindingEventGroups::PathRigHeight); + static inline IGimbalBindingLayout::mouse_to_virtual_events_t buildMousePreset(const SMousePresetSpec& spec) + { + IGimbalBindingLayout::mouse_to_virtual_events_t preset; + appendBindingSpec(preset, SCameraInputBindingPhysicalGroups::RelativeMouseCodes, spec.relative); + appendMirroredBindingSpec(preset, SCameraInputBindingPhysicalGroups::PositiveScrollCodes, spec.scroll[0]); + appendMirroredBindingSpec(preset, SCameraInputBindingPhysicalGroups::NegativeScrollCodes, spec.scroll[1]); + return preset; + } -inline constexpr SMousePresetSpec FpsMouseSpec = makeMousePresetSpec( - SCameraInputBindingEventGroups::RelativeLook); + static inline IGimbalBindingLayout::imguizmo_to_virtual_events_t makeImguizmoPreset(const uint32_t allowedVirtualEvents) + { + IGimbalBindingLayout::imguizmo_to_virtual_events_t preset; + for (const auto event : core::CVirtualGimbalEvent::VirtualEventsTypeTable) + { + if (event == core::CVirtualGimbalEvent::None) + continue; + if ((allowedVirtualEvents & event) != event) + continue; + preset.emplace(event, IGimbalBindingLayout::CHashInfo(event)); + } + return preset; + } -inline constexpr SMousePresetSpec OrbitMouseSpec = makeMousePresetSpec( - SCameraInputBindingEventGroups::RelativeOrbitTranslate, - SCameraInputBindingEventGroups::OrbitZoom); + static inline constexpr SCameraInteractionBindingSpec EmptyInteractionBindingSpec = {}; -inline constexpr SMousePresetSpec TargetRigMouseSpec = withMouseScroll(FpsMouseSpec, OrbitMouseSpec.scroll); + static inline constexpr SKeyboardPresetSpec FpsKeyboardSpec = { + SCameraInputBindingEventGroups::FpsMove, + {}, + SCameraInputBindingEventGroups::LookYawPitch + }; -inline constexpr SMousePresetSpec TopDownMouseSpec = makeMousePresetSpec( - SCameraInputBindingEventGroups::RelativeTopDown, - OrbitMouseSpec.scroll); + static inline constexpr SKeyboardPresetSpec FreeKeyboardSpec = { + FpsKeyboardSpec.wasd, + SCameraInputBindingEventGroups::Roll, + FpsKeyboardSpec.ijkl + }; -inline constexpr SCameraInteractionBindingSpec FpsInteractionBindingSpec = makeInteractionBindingSpec( - FpsKeyboardSpec, - FpsMouseSpec); + static inline constexpr SKeyboardPresetSpec OrbitKeyboardSpec = { + SCameraInputBindingEventGroups::OrbitTranslate, + SCameraInputBindingEventGroups::OrbitZoom, + {} + }; -inline constexpr SCameraInteractionBindingSpec FreeInteractionBindingSpec = withInteractionKeyboard( - FpsInteractionBindingSpec, - FreeKeyboardSpec); + static inline constexpr SKeyboardPresetSpec TargetRigKeyboardSpec = { + FpsKeyboardSpec.wasd, + SCameraInputBindingEventGroups::VerticalMove, + FpsKeyboardSpec.ijkl + }; -inline constexpr SCameraInteractionBindingSpec OrbitInteractionBindingSpec = makeInteractionBindingSpec( - OrbitKeyboardSpec, - OrbitMouseSpec); + static inline constexpr SKeyboardPresetSpec TurntableKeyboardSpec = { + SCameraInputBindingEventGroups::TurntableMove, + {}, + FpsKeyboardSpec.ijkl + }; -inline constexpr SCameraInteractionBindingSpec TargetRigInteractionBindingSpec = withInteractionKeyboard( - withInteractionMouse(FpsInteractionBindingSpec, TargetRigMouseSpec), - TargetRigKeyboardSpec); + static inline constexpr SKeyboardPresetSpec TopDownKeyboardSpec = { + OrbitKeyboardSpec.wasd, + OrbitKeyboardSpec.qe, + SCameraInputBindingEventGroups::PanOnly + }; -inline constexpr SCameraInteractionBindingSpec TurntableInteractionBindingSpec = withInteractionKeyboard( - TargetRigInteractionBindingSpec, - TurntableKeyboardSpec); + static inline constexpr SKeyboardPresetSpec PathKeyboardSpec = { + SCameraInputBindingEventGroups::PathRigAdvanceAndRadius, + SCameraInputBindingEventGroups::PathRigHeight, + {} + }; -inline constexpr SCameraInteractionBindingSpec TopDownInteractionBindingSpec = withInteractionKeyboard( - withInteractionMouse(OrbitInteractionBindingSpec, TopDownMouseSpec), - TopDownKeyboardSpec); + static inline constexpr SMousePresetSpec FpsMouseSpec = { + SCameraInputBindingEventGroups::RelativeLook, + {} + }; -inline constexpr SCameraInteractionBindingSpec PathInteractionBindingSpec = withInteractionKeyboard( - OrbitInteractionBindingSpec, - PathKeyboardSpec); + static inline constexpr SMousePresetSpec OrbitMouseSpec = { + SCameraInputBindingEventGroups::RelativeOrbitTranslate, + SCameraInputBindingEventGroups::OrbitZoom + }; -template -inline auto makePresetCache(const SpecArray& specs, Builder&& builder) -{ - std::array> cache = {}; - for (size_t i = 0u; i < specs.size(); ++i) - cache[i] = builder(specs[i]); - return cache; -} + static inline constexpr SMousePresetSpec TargetRigMouseSpec = { + FpsMouseSpec.relative, + OrbitMouseSpec.scroll + }; -inline SCameraMappedInteractionBindingSpec mapInteractionBindingSpec(const SCameraInteractionBindingSpec& spec) -{ - return { - .keyboard = buildKeyboardPreset(spec.keyboard), - .mouse = buildMousePreset(spec.mouse) + static inline constexpr SMousePresetSpec TopDownMouseSpec = { + SCameraInputBindingEventGroups::RelativeTopDown, + OrbitMouseSpec.scroll }; -} - -inline constexpr std::array InteractionFamilyPresetSpecs = {{ - EmptyInteractionBindingSpec, - FpsInteractionBindingSpec, - FreeInteractionBindingSpec, - OrbitInteractionBindingSpec, - TargetRigInteractionBindingSpec, - TurntableInteractionBindingSpec, - TopDownInteractionBindingSpec, - PathInteractionBindingSpec -}}; - -inline const SCameraMappedInteractionBindingSpec& interactionBindingPresetForKind(const core::ICamera::CameraKind kind) -{ - const auto familyIx = interactionFamilyIndex(core::getCameraInteractionFamily(kind)); - static const auto cache = makePresetCache( - InteractionFamilyPresetSpecs, - [](const SCameraInteractionBindingSpec& spec) { return mapInteractionBindingSpec(spec); }); - return cache[familyIx < cache.size() ? familyIx : 0u]; -} -} // namespace impl + static inline constexpr SCameraInteractionBindingSpec FpsInteractionBindingSpec = { + FpsKeyboardSpec, + FpsMouseSpec + }; -inline const IGimbalBindingLayout::keyboard_to_virtual_events_t& getDefaultCameraKeyboardMappingPreset(const core::ICamera::CameraKind kind) -{ - return impl::interactionBindingPresetForKind(kind).keyboard; -} + static inline constexpr SCameraInteractionBindingSpec FreeInteractionBindingSpec = { + FreeKeyboardSpec, + FpsMouseSpec + }; -inline const IGimbalBindingLayout::keyboard_to_virtual_events_t& getDefaultCameraKeyboardMappingPreset(const core::ICamera& camera) -{ - return getDefaultCameraKeyboardMappingPreset(camera.getKind()); -} + static inline constexpr SCameraInteractionBindingSpec OrbitInteractionBindingSpec = { + OrbitKeyboardSpec, + OrbitMouseSpec + }; -inline const IGimbalBindingLayout::mouse_to_virtual_events_t& getDefaultCameraMouseMappingPreset(const core::ICamera::CameraKind kind) -{ - return impl::interactionBindingPresetForKind(kind).mouse; -} + static inline constexpr SCameraInteractionBindingSpec TargetRigInteractionBindingSpec = { + TargetRigKeyboardSpec, + TargetRigMouseSpec + }; -inline const IGimbalBindingLayout::mouse_to_virtual_events_t& getDefaultCameraMouseMappingPreset(const core::ICamera& camera) -{ - return getDefaultCameraMouseMappingPreset(camera.getKind()); -} + static inline constexpr SCameraInteractionBindingSpec TurntableInteractionBindingSpec = { + TurntableKeyboardSpec, + TargetRigMouseSpec + }; -inline IGimbalBindingLayout::imguizmo_to_virtual_events_t buildDefaultCameraImguizmoMappingPreset(const uint32_t allowedVirtualEvents) -{ - return impl::makeImguizmoPreset(allowedVirtualEvents); -} + static inline constexpr SCameraInteractionBindingSpec TopDownInteractionBindingSpec = { + TopDownKeyboardSpec, + TopDownMouseSpec + }; -inline IGimbalBindingLayout::imguizmo_to_virtual_events_t buildDefaultCameraImguizmoMappingPreset(const core::ICamera& camera) -{ - return buildDefaultCameraImguizmoMappingPreset(camera.getAllowedVirtualEvents()); -} + static inline constexpr SCameraInteractionBindingSpec PathInteractionBindingSpec = { + PathKeyboardSpec, + OrbitMouseSpec + }; -inline SCameraInputBindingPreset buildDefaultCameraInputBindingPreset(const core::ICamera::CameraKind kind, const uint32_t allowedVirtualEvents) -{ - SCameraInputBindingPreset preset; - preset.keyboard = getDefaultCameraKeyboardMappingPreset(kind); - preset.mouse = getDefaultCameraMouseMappingPreset(kind); - preset.imguizmo = buildDefaultCameraImguizmoMappingPreset(allowedVirtualEvents); - return preset; -} - -inline SCameraInputBindingPreset buildDefaultCameraInputBindingPreset(const core::ICamera& camera) -{ - return buildDefaultCameraInputBindingPreset(camera.getKind(), camera.getAllowedVirtualEvents()); -} + template + static inline auto makePresetCache(const SpecArray& specs, Builder&& builder) + { + std::array> cache = {}; + for (size_t i = 0u; i < specs.size(); ++i) + cache[i] = builder(specs[i]); + return cache; + } -inline void applyDefaultCameraInputBindingPreset( - IGimbalBindingLayout& layout, - const core::ICamera::CameraKind kind, - const uint32_t allowedVirtualEvents) -{ - const auto preset = buildDefaultCameraInputBindingPreset(kind, allowedVirtualEvents); - layout.updateKeyboardMapping([&](auto& map) { map = preset.keyboard; }); - layout.updateMouseMapping([&](auto& map) { map = preset.mouse; }); - layout.updateImguizmoMapping([&](auto& map) { map = preset.imguizmo; }); -} + static inline SCameraMappedInteractionBindingSpec mapInteractionBindingSpec(const SCameraInteractionBindingSpec& spec) + { + return { + .keyboard = buildKeyboardPreset(spec.keyboard), + .mouse = buildMousePreset(spec.mouse) + }; + } -inline void applyDefaultCameraInputBindingPreset(IGimbalBindingLayout& layout, const core::ICamera& camera) -{ - applyDefaultCameraInputBindingPreset(layout, camera.getKind(), camera.getAllowedVirtualEvents()); -} + static inline constexpr std::array InteractionFamilyPresetSpecs = {{ + EmptyInteractionBindingSpec, + FpsInteractionBindingSpec, + FreeInteractionBindingSpec, + OrbitInteractionBindingSpec, + TargetRigInteractionBindingSpec, + TurntableInteractionBindingSpec, + TopDownInteractionBindingSpec, + PathInteractionBindingSpec + }}; + + static inline const SCameraMappedInteractionBindingSpec& interactionBindingPresetForKind(const core::ICamera::CameraKind kind) + { + const auto familyIx = interactionFamilyIndex(core::getCameraInteractionFamily(kind)); + static const auto cache = makePresetCache( + InteractionFamilyPresetSpecs, + [](const SCameraInteractionBindingSpec& spec) { return mapInteractionBindingSpec(spec); }); + return cache[familyIx < cache.size() ? familyIx : 0u]; + } +}; } // namespace nbl::ui diff --git a/common/include/camera/CCameraManipulationUtilities.hpp b/common/include/camera/CCameraManipulationUtilities.hpp index d0aa8fc17..0f63cf886 100644 --- a/common/include/camera/CCameraManipulationUtilities.hpp +++ b/common/include/camera/CCameraManipulationUtilities.hpp @@ -86,7 +86,7 @@ inline void remapTranslationEventsFromWorldToCameraLocal(ICamera* camera, std::v filtered.emplace_back(ev); } - const auto worldDelta = collectSignedTranslationDelta({ events.data(), count }); + const auto worldDelta = CCameraVirtualEventUtilities::collectSignedTranslationDelta({ events.data(), count }); if (hlsl::isNearlyZeroVector(worldDelta, static_cast(ICamera::TinyScalarEpsilon))) { events = std::move(filtered); @@ -94,7 +94,7 @@ inline void remapTranslationEventsFromWorldToCameraLocal(ICamera* camera, std::v return; } - appendWorldTranslationAsLocalEvents(filtered, camera->getGimbal().getOrientation(), worldDelta); + CCameraVirtualEventUtilities::appendWorldTranslationAsLocalEvents(filtered, camera->getGimbal().getOrientation(), worldDelta); events = std::move(filtered); count = static_cast(events.size()); diff --git a/common/include/camera/CCameraPathUtilities.hpp b/common/include/camera/CCameraPathUtilities.hpp index 9f158b874..046a55ed6 100644 --- a/common/include/camera/CCameraPathUtilities.hpp +++ b/common/include/camera/CCameraPathUtilities.hpp @@ -297,12 +297,12 @@ struct CCameraPathUtilities final const double angleToleranceDeg = SCameraPathDefaults::AngleToleranceDeg, const double scalarTolerance = SCameraPathDefaults::ScalarTolerance) { - appendLocalTranslationEvents( + CCameraVirtualEventUtilities::appendLocalTranslationEvents( events, delta.translationVector(), hlsl::float64_t3(moveDenominator), hlsl::float64_t3(scalarTolerance)); - appendAngularDeltaEvent( + CCameraVirtualEventUtilities::appendAngularDeltaEvent( events, delta.angle, moveDenominator, diff --git a/common/include/camera/CCameraPresentationUtilities.hpp b/common/include/camera/CCameraPresentationUtilities.hpp index 9dad90b10..fe61864ee 100644 --- a/common/include/camera/CCameraPresentationUtilities.hpp +++ b/common/include/camera/CCameraPresentationUtilities.hpp @@ -85,10 +85,10 @@ inline SCameraGoalApplyPresentation makeGoalApplyPresentation(const core::SCamer SCameraGoalApplyPresentation presentation; static_cast(presentation) = analysis; presentation.badges = collectGoalApplyPresentationBadges(presentation); - presentation.sourceKindLabel = std::string(getCameraTypeLabel(presentation.goal.sourceKind)); - presentation.goalStateLabel = describeGoalStateMask(presentation.goal.sourceGoalStateMask); - presentation.compatibilityLabel = describeGoalApplyCompatibility(analysis, targetCamera); - presentation.policyLabel = describeGoalApplyPolicy(analysis); + presentation.sourceKindLabel = std::string(CCameraTextUtilities::getCameraTypeLabel(presentation.goal.sourceKind)); + presentation.goalStateLabel = CCameraTextUtilities::describeGoalStateMask(presentation.goal.sourceGoalStateMask); + presentation.compatibilityLabel = CCameraTextUtilities::describeGoalApplyCompatibility(analysis, targetCamera); + presentation.policyLabel = CCameraTextUtilities::describeGoalApplyPolicy(analysis); return presentation; } @@ -115,7 +115,7 @@ inline SCameraCapturePresentation analyzeCapturePresentation(const core::CCamera { SCameraCapturePresentation presentation; static_cast(presentation) = core::CCameraGoalAnalysisUtilities::analyzeCameraCapture(solver, camera); - presentation.policyLabel = describeCameraCapturePolicy(presentation, camera); + presentation.policyLabel = CCameraTextUtilities::describeCameraCapturePolicy(presentation, camera); return presentation; } diff --git a/common/include/camera/CCameraTargetRelativeUtilities.hpp b/common/include/camera/CCameraTargetRelativeUtilities.hpp index 616adf301..656914822 100644 --- a/common/include/camera/CCameraTargetRelativeUtilities.hpp +++ b/common/include/camera/CCameraTargetRelativeUtilities.hpp @@ -216,7 +216,7 @@ struct CCameraTargetRelativeUtilities final { if (policy.translateOrbit) { - appendAngularAxisEvents( + CCameraVirtualEventUtilities::appendAngularAxisEvents( events, delta.orbitVector(), hlsl::float64_t3(angularDenominator), @@ -231,7 +231,7 @@ struct CCameraTargetRelativeUtilities final { if (policy.allowYaw) { - appendAngularDeltaEvent( + CCameraVirtualEventUtilities::appendAngularDeltaEvent( events, delta.orbitUv.x, angularDenominator, @@ -241,7 +241,7 @@ struct CCameraTargetRelativeUtilities final } if (policy.allowPitch) { - appendAngularDeltaEvent( + CCameraVirtualEventUtilities::appendAngularDeltaEvent( events, delta.orbitUv.y, angularDenominator, @@ -254,7 +254,7 @@ struct CCameraTargetRelativeUtilities final if (policy.distanceBinding.positive != CVirtualGimbalEvent::None && policy.distanceBinding.negative != CVirtualGimbalEvent::None) { - appendScaledVirtualEvent( + CCameraVirtualEventUtilities::appendScaledVirtualEvent( events, delta.distance, distanceDenominator, diff --git a/common/include/camera/CCameraTextUtilities.hpp b/common/include/camera/CCameraTextUtilities.hpp index 32e09eca1..878c6a41b 100644 --- a/common/include/camera/CCameraTextUtilities.hpp +++ b/common/include/camera/CCameraTextUtilities.hpp @@ -16,190 +16,194 @@ namespace nbl::ui { -//! Return a short human-readable label for a camera kind. -inline std::string_view getCameraTypeLabel(const core::ICamera::CameraKind kind) +struct CCameraTextUtilities final { - return core::getCameraKindLabel(kind); -} - -//! Return a short human-readable label for a concrete camera instance. -inline std::string_view getCameraTypeLabel(const core::ICamera* camera) -{ - return camera ? getCameraTypeLabel(camera->getKind()) : "Unknown"; -} - -//! Return a short human-readable description for a camera kind. -inline std::string_view getCameraTypeDescription(const core::ICamera::CameraKind kind) -{ - return core::getCameraKindDescription(kind); -} - -//! Return a short human-readable description for a concrete camera instance. -inline std::string_view getCameraTypeDescription(const core::ICamera* camera) -{ - return camera ? getCameraTypeDescription(camera->getKind()) : "Unspecified camera behavior"; -} +public: + //! Return a short human-readable label for a camera kind. + static inline std::string_view getCameraTypeLabel(const core::ICamera::CameraKind kind) + { + return core::getCameraKindLabel(kind); + } -//! Return a short human-readable label for a follow mode. -inline constexpr const char* getCameraFollowModeLabel(const core::ECameraFollowMode mode) -{ - switch (mode) + //! Return a short human-readable label for a concrete camera instance. + static inline std::string_view getCameraTypeLabel(const core::ICamera* camera) { - case core::ECameraFollowMode::Disabled: return "Disabled"; - case core::ECameraFollowMode::OrbitTarget: return "Orbit target"; - case core::ECameraFollowMode::LookAtTarget: return "Look at target"; - case core::ECameraFollowMode::KeepWorldOffset: return "Keep world offset"; - case core::ECameraFollowMode::KeepLocalOffset: return "Keep local offset"; - default: return "Unknown"; + return camera ? getCameraTypeLabel(camera->getKind()) : "Unknown"; } -} -//! Return a short human-readable description for a follow mode. -inline constexpr const char* getCameraFollowModeDescription(const core::ECameraFollowMode mode) -{ - switch (mode) + //! Return a short human-readable description for a camera kind. + static inline std::string_view getCameraTypeDescription(const core::ICamera::CameraKind kind) { - case core::ECameraFollowMode::Disabled: return "Follow disabled"; - case core::ECameraFollowMode::OrbitTarget: return "Keep orbit around moving target and keep it centered"; - case core::ECameraFollowMode::LookAtTarget: return "Keep camera position and lock the view onto the target"; - case core::ECameraFollowMode::KeepWorldOffset: return "Move with the target in world offset and keep it centered"; - case core::ECameraFollowMode::KeepLocalOffset: return "Move with the target in target-local offset and keep it centered"; - default: return "Unknown follow mode"; + return core::getCameraKindDescription(kind); } -} -//! Describe the typed goal-state mask in a stable human-readable format. -inline std::string describeGoalStateMask(const uint32_t mask) -{ - if (mask == core::ICamera::GoalStateNone) - return "Pose only"; + //! Return a short human-readable description for a concrete camera instance. + static inline std::string_view getCameraTypeDescription(const core::ICamera* camera) + { + return camera ? getCameraTypeDescription(camera->getKind()) : "Unspecified camera behavior"; + } - std::string out; - auto append = [&](const char* label, const uint32_t bit) -> void + //! Return a short human-readable label for a follow mode. + static inline constexpr const char* getCameraFollowModeLabel(const core::ECameraFollowMode mode) { - if ((mask & bit) != bit) - return; - if (!out.empty()) - out += ", "; - out += label; - }; - - append("Spherical target", core::ICamera::GoalStateSphericalTarget); - append("Dynamic perspective", core::ICamera::GoalStateDynamicPerspective); - append("Path rig state", core::ICamera::GoalStatePath); - return out; -} - -//! Describe a detailed goal-apply result for logs, smoke tests, and UI summaries. -inline std::string describeApplyResult(const core::CCameraGoalSolver::SApplyResult& result) -{ - std::ostringstream oss; - oss << "status="; - switch (result.status) + switch (mode) + { + case core::ECameraFollowMode::Disabled: return "Disabled"; + case core::ECameraFollowMode::OrbitTarget: return "Orbit target"; + case core::ECameraFollowMode::LookAtTarget: return "Look at target"; + case core::ECameraFollowMode::KeepWorldOffset: return "Keep world offset"; + case core::ECameraFollowMode::KeepLocalOffset: return "Keep local offset"; + default: return "Unknown"; + } + } + + //! Return a short human-readable description for a follow mode. + static inline constexpr const char* getCameraFollowModeDescription(const core::ECameraFollowMode mode) { - case core::CCameraGoalSolver::SApplyResult::EStatus::Unsupported: oss << "Unsupported"; break; - case core::CCameraGoalSolver::SApplyResult::EStatus::Failed: oss << "Failed"; break; - case core::CCameraGoalSolver::SApplyResult::EStatus::AlreadySatisfied: oss << "AlreadySatisfied"; break; - case core::CCameraGoalSolver::SApplyResult::EStatus::AppliedAbsoluteOnly: oss << "AppliedAbsoluteOnly"; break; - case core::CCameraGoalSolver::SApplyResult::EStatus::AppliedVirtualEvents: oss << "AppliedVirtualEvents"; break; - case core::CCameraGoalSolver::SApplyResult::EStatus::AppliedAbsoluteAndVirtualEvents: oss << "AppliedAbsoluteAndVirtualEvents"; break; + switch (mode) + { + case core::ECameraFollowMode::Disabled: return "Follow disabled"; + case core::ECameraFollowMode::OrbitTarget: return "Keep orbit around moving target and keep it centered"; + case core::ECameraFollowMode::LookAtTarget: return "Keep camera position and lock the view onto the target"; + case core::ECameraFollowMode::KeepWorldOffset: return "Move with the target in world offset and keep it centered"; + case core::ECameraFollowMode::KeepLocalOffset: return "Move with the target in target-local offset and keep it centered"; + default: return "Unknown follow mode"; + } } - oss << " exact=" << (result.exact ? "true" : "false") - << " events=" << result.eventCount; - if (result.issues != core::CCameraGoalSolver::SApplyResult::NoIssue) + //! Describe the typed goal-state mask in a stable human-readable format. + static inline std::string describeGoalStateMask(const uint32_t mask) { - oss << " issues="; - bool first = true; - auto appendIssue = [&](const char* label, const core::CCameraGoalSolver::SApplyResult::EIssue issue) -> void + if (mask == core::ICamera::GoalStateNone) + return "Pose only"; + + std::string out; + auto append = [&](const char* label, const uint32_t bit) -> void { - if (!result.hasIssue(issue)) + if ((mask & bit) != bit) return; - if (!first) - oss << ","; - oss << label; - first = false; + if (!out.empty()) + out += ", "; + out += label; }; - appendIssue("absolute_pose_fallback", core::CCameraGoalSolver::SApplyResult::UsedAbsolutePoseFallback); - appendIssue("missing_spherical_state", core::CCameraGoalSolver::SApplyResult::MissingSphericalTargetState); - appendIssue("missing_path_state", core::CCameraGoalSolver::SApplyResult::MissingPathState); - appendIssue("missing_dynamic_perspective_state", core::CCameraGoalSolver::SApplyResult::MissingDynamicPerspectiveState); - appendIssue("virtual_event_replay_failed", core::CCameraGoalSolver::SApplyResult::VirtualEventReplayFailed); + append("Spherical target", core::ICamera::GoalStateSphericalTarget); + append("Dynamic perspective", core::ICamera::GoalStateDynamicPerspective); + append("Path rig state", core::ICamera::GoalStatePath); + return out; } - return oss.str(); -} + //! Describe a detailed goal-apply result for logs, smoke tests, and UI summaries. + static inline std::string describeApplyResult(const core::CCameraGoalSolver::SApplyResult& result) + { + std::ostringstream oss; + oss << "status="; + switch (result.status) + { + case core::CCameraGoalSolver::SApplyResult::EStatus::Unsupported: oss << "Unsupported"; break; + case core::CCameraGoalSolver::SApplyResult::EStatus::Failed: oss << "Failed"; break; + case core::CCameraGoalSolver::SApplyResult::EStatus::AlreadySatisfied: oss << "AlreadySatisfied"; break; + case core::CCameraGoalSolver::SApplyResult::EStatus::AppliedAbsoluteOnly: oss << "AppliedAbsoluteOnly"; break; + case core::CCameraGoalSolver::SApplyResult::EStatus::AppliedVirtualEvents: oss << "AppliedVirtualEvents"; break; + case core::CCameraGoalSolver::SApplyResult::EStatus::AppliedAbsoluteAndVirtualEvents: oss << "AppliedAbsoluteAndVirtualEvents"; break; + } + oss << " exact=" << (result.exact ? "true" : "false") + << " events=" << result.eventCount; + + if (result.issues != core::CCameraGoalSolver::SApplyResult::NoIssue) + { + oss << " issues="; + bool first = true; + auto appendIssue = [&](const char* label, const core::CCameraGoalSolver::SApplyResult::EIssue issue) -> void + { + if (!result.hasIssue(issue)) + return; + if (!first) + oss << ","; + oss << label; + first = false; + }; + + appendIssue("absolute_pose_fallback", core::CCameraGoalSolver::SApplyResult::UsedAbsolutePoseFallback); + appendIssue("missing_spherical_state", core::CCameraGoalSolver::SApplyResult::MissingSphericalTargetState); + appendIssue("missing_path_state", core::CCameraGoalSolver::SApplyResult::MissingPathState); + appendIssue("missing_dynamic_perspective_state", core::CCameraGoalSolver::SApplyResult::MissingDynamicPerspectiveState); + appendIssue("virtual_event_replay_failed", core::CCameraGoalSolver::SApplyResult::VirtualEventReplayFailed); + } + + return oss.str(); + } -//! Describe compatibility preview for applying one analyzed goal to a target camera. -inline std::string describeGoalApplyCompatibility(const core::SCameraGoalApplyAnalysis& analysis, const core::ICamera* targetCamera) -{ - if (!analysis.hasCamera) - return "No active camera"; + //! Describe compatibility preview for applying one analyzed goal to a target camera. + static inline std::string describeGoalApplyCompatibility(const core::SCameraGoalApplyAnalysis& analysis, const core::ICamera* targetCamera) + { + if (!analysis.hasCamera) + return "No active camera"; - std::ostringstream oss; - oss << (analysis.compatibility.exact ? "Exact" : "Best-effort") - << " | source=" << getCameraTypeLabel(analysis.goal.sourceKind) - << " | target=" << getCameraTypeLabel(targetCamera); + std::ostringstream oss; + oss << (analysis.compatibility.exact ? "Exact" : "Best-effort") + << " | source=" << getCameraTypeLabel(analysis.goal.sourceKind) + << " | target=" << getCameraTypeLabel(targetCamera); - if (analysis.compatibility.missingGoalStateMask != core::ICamera::GoalStateNone) - oss << " | missing=" << describeGoalStateMask(analysis.compatibility.missingGoalStateMask); - else if (!analysis.compatibility.sameKind && analysis.goal.sourceKind != core::ICamera::CameraKind::Unknown) - oss << " | shared goal state only"; + if (analysis.compatibility.missingGoalStateMask != core::ICamera::GoalStateNone) + oss << " | missing=" << describeGoalStateMask(analysis.compatibility.missingGoalStateMask); + else if (!analysis.compatibility.sameKind && analysis.goal.sourceKind != core::ICamera::CameraKind::Unknown) + oss << " | shared goal state only"; - return oss.str(); -} + return oss.str(); + } -//! Describe whether an analyzed goal can be meaningfully applied to the target camera. -inline std::string describeGoalApplyPolicy(const core::SCameraGoalApplyAnalysis& analysis) -{ - if (!analysis.hasCamera) - return "Blocked | no active camera"; - if (!analysis.finiteGoal) - return "Blocked | invalid goal state"; - - std::ostringstream oss; - oss << (analysis.compatibility.exact ? "Exact apply" : "Best-effort apply"); - if (analysis.compatibility.missingGoalStateMask != core::ICamera::GoalStateNone) - oss << " | drops=" << describeGoalStateMask(analysis.compatibility.missingGoalStateMask); - else if (!analysis.compatibility.sameKind && analysis.goal.sourceKind != core::ICamera::CameraKind::Unknown) - oss << " | shared goal state only"; - else - oss << " | full preview available"; - - return oss.str(); -} - -//! Describe whether one analyzed camera state can be captured into a reusable goal. -inline std::string describeCameraCapturePolicy(const core::SCameraCaptureAnalysis& analysis, const core::ICamera* camera) -{ - if (!analysis.hasCamera) - return "Blocked | no active camera"; - if (!analysis.capturedGoal) - return "Blocked | goal capture failed"; - if (!analysis.finiteGoal) - return "Blocked | invalid goal state"; - - std::ostringstream oss; - oss << "Ready | source=" << getCameraTypeLabel(camera) - << " | goal=" << describeGoalStateMask(analysis.goal.sourceGoalStateMask); - return oss.str(); -} - -//! Describe the aggregate outcome of applying one preset to multiple cameras. -inline std::string describePresetApplySummary(const core::SCameraPresetApplySummary& summary, std::string_view noTargetsLabel, std::string_view prefix = "Playback apply") -{ - if (!summary.hasTargets()) - return std::string(noTargetsLabel); - - std::ostringstream oss; - oss << prefix << " | targets=" << summary.targetCount << " | ok=" << summary.successCount; - if (summary.approximateCount > 0u) - oss << " | approximate=" << summary.approximateCount; - if (summary.failureCount > 0u) - oss << " | failed=" << summary.failureCount; - return oss.str(); -} + //! Describe whether an analyzed goal can be meaningfully applied to the target camera. + static inline std::string describeGoalApplyPolicy(const core::SCameraGoalApplyAnalysis& analysis) + { + if (!analysis.hasCamera) + return "Blocked | no active camera"; + if (!analysis.finiteGoal) + return "Blocked | invalid goal state"; + + std::ostringstream oss; + oss << (analysis.compatibility.exact ? "Exact apply" : "Best-effort apply"); + if (analysis.compatibility.missingGoalStateMask != core::ICamera::GoalStateNone) + oss << " | drops=" << describeGoalStateMask(analysis.compatibility.missingGoalStateMask); + else if (!analysis.compatibility.sameKind && analysis.goal.sourceKind != core::ICamera::CameraKind::Unknown) + oss << " | shared goal state only"; + else + oss << " | full preview available"; + + return oss.str(); + } + + //! Describe whether one analyzed camera state can be captured into a reusable goal. + static inline std::string describeCameraCapturePolicy(const core::SCameraCaptureAnalysis& analysis, const core::ICamera* camera) + { + if (!analysis.hasCamera) + return "Blocked | no active camera"; + if (!analysis.capturedGoal) + return "Blocked | goal capture failed"; + if (!analysis.finiteGoal) + return "Blocked | invalid goal state"; + + std::ostringstream oss; + oss << "Ready | source=" << getCameraTypeLabel(camera) + << " | goal=" << describeGoalStateMask(analysis.goal.sourceGoalStateMask); + return oss.str(); + } + + //! Describe the aggregate outcome of applying one preset to multiple cameras. + static inline std::string describePresetApplySummary(const core::SCameraPresetApplySummary& summary, std::string_view noTargetsLabel, std::string_view prefix = "Playback apply") + { + if (!summary.hasTargets()) + return std::string(noTargetsLabel); + + std::ostringstream oss; + oss << prefix << " | targets=" << summary.targetCount << " | ok=" << summary.successCount; + if (summary.approximateCount > 0u) + oss << " | approximate=" << summary.approximateCount; + if (summary.failureCount > 0u) + oss << " | failed=" << summary.failureCount; + return oss.str(); + } +}; } // namespace nbl::ui diff --git a/common/include/camera/CCameraVirtualEventUtilities.hpp b/common/include/camera/CCameraVirtualEventUtilities.hpp index 81695a14e..e2e800c05 100644 --- a/common/include/camera/CCameraVirtualEventUtilities.hpp +++ b/common/include/camera/CCameraVirtualEventUtilities.hpp @@ -26,142 +26,146 @@ struct SCameraVirtualEventBindings final }}; }; -inline void appendSignedVirtualEvent( - std::vector& events, - const double value, - const CVirtualGimbalEvent::VirtualEventType positive, - const CVirtualGimbalEvent::VirtualEventType negative, - const double tolerance = static_cast(ICamera::TinyScalarEpsilon)) +struct CCameraVirtualEventUtilities final { - if (!hlsl::isFiniteScalar(value) || hlsl::isNearlyZeroScalar(value, tolerance)) - return; - - auto& ev = events.emplace_back(); - ev.type = (value > 0.0) ? positive : negative; - ev.magnitude = hlsl::abs(value); -} - -inline void appendScaledVirtualEvent( - std::vector& events, - const double value, - const double denominator, - const double tolerance, - const CVirtualGimbalEvent::VirtualEventType positive, - const CVirtualGimbalEvent::VirtualEventType negative) -{ - if (!hlsl::isFiniteScalar(denominator) || hlsl::isNearlyZeroScalar(denominator, static_cast(ICamera::TinyScalarEpsilon))) - return; - - appendSignedVirtualEvent(events, value / denominator, positive, negative, tolerance); -} - -inline void appendAngularDeltaEvent( - std::vector& events, - const double deltaRadians, - const double denominator, - const double toleranceDeg, - const CVirtualGimbalEvent::VirtualEventType positive, - const CVirtualGimbalEvent::VirtualEventType negative) -{ - if (!hlsl::isFiniteScalar(deltaRadians) || - hlsl::isNearlyZeroScalar(hlsl::degrees(deltaRadians), toleranceDeg)) +public: + static inline void appendSignedVirtualEvent( + std::vector& events, + const double value, + const CVirtualGimbalEvent::VirtualEventType positive, + const CVirtualGimbalEvent::VirtualEventType negative, + const double tolerance = static_cast(ICamera::TinyScalarEpsilon)) { - return; + if (!hlsl::isFiniteScalar(value) || hlsl::isNearlyZeroScalar(value, tolerance)) + return; + + auto& ev = events.emplace_back(); + ev.type = (value > 0.0) ? positive : negative; + ev.magnitude = hlsl::abs(value); } - appendScaledVirtualEvent( - events, - deltaRadians, - denominator, - hlsl::radians(toleranceDeg), - positive, - negative); -} - -inline void appendScaledVirtualAxisEvents( - std::vector& events, - const hlsl::float64_t3& values, - const hlsl::float64_t3& denominators, - const hlsl::float64_t3& tolerances, - const std::array& axisBindings) -{ - for (size_t axisIx = 0u; axisIx < axisBindings.size(); ++axisIx) + static inline void appendScaledVirtualEvent( + std::vector& events, + const double value, + const double denominator, + const double tolerance, + const CVirtualGimbalEvent::VirtualEventType positive, + const CVirtualGimbalEvent::VirtualEventType negative) + { + if (!hlsl::isFiniteScalar(denominator) || hlsl::isNearlyZeroScalar(denominator, static_cast(ICamera::TinyScalarEpsilon))) + return; + + appendSignedVirtualEvent(events, value / denominator, positive, negative, tolerance); + } + + static inline void appendAngularDeltaEvent( + std::vector& events, + const double deltaRadians, + const double denominator, + const double toleranceDeg, + const CVirtualGimbalEvent::VirtualEventType positive, + const CVirtualGimbalEvent::VirtualEventType negative) { + if (!hlsl::isFiniteScalar(deltaRadians) || + hlsl::isNearlyZeroScalar(hlsl::degrees(deltaRadians), toleranceDeg)) + { + return; + } + appendScaledVirtualEvent( events, - values[axisIx], - denominators[axisIx], - tolerances[axisIx], - axisBindings[axisIx].positive, - axisBindings[axisIx].negative); + deltaRadians, + denominator, + hlsl::radians(toleranceDeg), + positive, + negative); } -} -inline void appendLocalTranslationEvents( - std::vector& events, - const hlsl::float64_t3& localDelta, - const hlsl::float64_t3& denominators = hlsl::float64_t3(1.0), - const hlsl::float64_t3& tolerances = hlsl::float64_t3(ICamera::TinyScalarEpsilon)) -{ - appendScaledVirtualAxisEvents( - events, - localDelta, - denominators, - tolerances, - SCameraVirtualEventBindings::LocalTranslation); -} - -inline void appendWorldTranslationAsLocalEvents( - std::vector& events, - const hlsl::camera_quaternion_t& orientation, - const hlsl::float64_t3& worldDelta, - const hlsl::float64_t3& denominators = hlsl::float64_t3(1.0), - const hlsl::float64_t3& tolerances = hlsl::float64_t3(ICamera::TinyScalarEpsilon)) -{ - appendLocalTranslationEvents( - events, - hlsl::projectWorldVectorToLocalQuaternionFrame(orientation, worldDelta), - denominators, - tolerances); -} - -inline void appendAngularAxisEvents( - std::vector& events, - const hlsl::float64_t3& deltaRadians, - const hlsl::float64_t3& denominators, - const hlsl::float64_t3& toleranceDeg, - const std::array& axisBindings) -{ - for (size_t axisIx = 0u; axisIx < axisBindings.size(); ++axisIx) + static inline void appendScaledVirtualAxisEvents( + std::vector& events, + const hlsl::float64_t3& values, + const hlsl::float64_t3& denominators, + const hlsl::float64_t3& tolerances, + const std::array& axisBindings) + { + for (size_t axisIx = 0u; axisIx < axisBindings.size(); ++axisIx) + { + appendScaledVirtualEvent( + events, + values[axisIx], + denominators[axisIx], + tolerances[axisIx], + axisBindings[axisIx].positive, + axisBindings[axisIx].negative); + } + } + + static inline void appendLocalTranslationEvents( + std::vector& events, + const hlsl::float64_t3& localDelta, + const hlsl::float64_t3& denominators = hlsl::float64_t3(1.0), + const hlsl::float64_t3& tolerances = hlsl::float64_t3(ICamera::TinyScalarEpsilon)) { - appendAngularDeltaEvent( + appendScaledVirtualAxisEvents( events, - deltaRadians[axisIx], - denominators[axisIx], - toleranceDeg[axisIx], - axisBindings[axisIx].positive, - axisBindings[axisIx].negative); + localDelta, + denominators, + tolerances, + SCameraVirtualEventBindings::LocalTranslation); } -} -inline hlsl::float64_t3 collectSignedTranslationDelta(std::span events) -{ - hlsl::float64_t3 delta = hlsl::float64_t3(0.0); - for (const auto& ev : events) + static inline void appendWorldTranslationAsLocalEvents( + std::vector& events, + const hlsl::camera_quaternion_t& orientation, + const hlsl::float64_t3& worldDelta, + const hlsl::float64_t3& denominators = hlsl::float64_t3(1.0), + const hlsl::float64_t3& tolerances = hlsl::float64_t3(ICamera::TinyScalarEpsilon)) { - switch (ev.type) + appendLocalTranslationEvents( + events, + hlsl::projectWorldVectorToLocalQuaternionFrame(orientation, worldDelta), + denominators, + tolerances); + } + + static inline void appendAngularAxisEvents( + std::vector& events, + const hlsl::float64_t3& deltaRadians, + const hlsl::float64_t3& denominators, + const hlsl::float64_t3& toleranceDeg, + const std::array& axisBindings) + { + for (size_t axisIx = 0u; axisIx < axisBindings.size(); ++axisIx) { - case CVirtualGimbalEvent::MoveRight: delta.x += ev.magnitude; break; - case CVirtualGimbalEvent::MoveLeft: delta.x -= ev.magnitude; break; - case CVirtualGimbalEvent::MoveUp: delta.y += ev.magnitude; break; - case CVirtualGimbalEvent::MoveDown: delta.y -= ev.magnitude; break; - case CVirtualGimbalEvent::MoveForward: delta.z += ev.magnitude; break; - case CVirtualGimbalEvent::MoveBackward: delta.z -= ev.magnitude; break; - default: break; + appendAngularDeltaEvent( + events, + deltaRadians[axisIx], + denominators[axisIx], + toleranceDeg[axisIx], + axisBindings[axisIx].positive, + axisBindings[axisIx].negative); } } - return delta; -} + + static inline hlsl::float64_t3 collectSignedTranslationDelta(std::span events) + { + hlsl::float64_t3 delta = hlsl::float64_t3(0.0); + for (const auto& ev : events) + { + switch (ev.type) + { + case CVirtualGimbalEvent::MoveRight: delta.x += ev.magnitude; break; + case CVirtualGimbalEvent::MoveLeft: delta.x -= ev.magnitude; break; + case CVirtualGimbalEvent::MoveUp: delta.y += ev.magnitude; break; + case CVirtualGimbalEvent::MoveDown: delta.y -= ev.magnitude; break; + case CVirtualGimbalEvent::MoveForward: delta.z += ev.magnitude; break; + case CVirtualGimbalEvent::MoveBackward: delta.z -= ev.magnitude; break; + default: break; + } + } + return delta; + } +}; } // namespace nbl::core diff --git a/common/include/camera/README.md b/common/include/camera/README.md index 8c7b0fbe6..57367e418 100644 --- a/common/include/camera/README.md +++ b/common/include/camera/README.md @@ -468,7 +468,7 @@ This test now runs on the compact authored sequence format rather than a large e auto camera = core::make_smart_refctd_ptr(eye, target); CGimbalInputBinder binder; -applyDefaultCameraInputBindingPreset(binder, *camera); +CCameraInputBindingUtilities::applyDefaultCameraInputBindingPreset(binder, *camera); auto collected = binder.collectVirtualEvents(timestamp, { .mouseEvents = { mouseEvents.data(), mouseEvents.size() }, From d6cf91e8cd24de078017bf52e57b015a4d357e5b Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Thu, 9 Apr 2026 11:14:18 +0200 Subject: [PATCH 184/205] Refactor camera track and file helpers --- 61_UI/AppCameraPlanarRuntime.cpp | 4 +- 61_UI/AppControlPanelPlaybackTab.cpp | 6 +- 61_UI/AppHeadlessCameraSmokeChecks.inl | 26 +-- 61_UI/AppHeadlessCameraSmokeHelpers.inl | 16 +- 61_UI/AppInputRuntime.cpp | 6 +- 61_UI/AppManipulableObjects.cpp | 2 +- 61_UI/AppPresetPlayback.cpp | 18 +- 61_UI/AppResourceUtilities.cpp | 2 +- .../include/app/AppResourcePathUtilities.hpp | 2 +- .../include/camera/CCameraFileUtilities.hpp | 118 +++++----- .../camera/CCameraInputBindingUtilities.hpp | 2 +- .../include/camera/CCameraKeyframeTrack.hpp | 216 +++++++++--------- .../include/camera/CCameraKindUtilities.hpp | 190 +++++++-------- .../camera/CCameraManipulationUtilities.hpp | 158 ++++++------- .../camera/CCameraPlaybackTimeline.hpp | 100 ++++---- .../include/camera/CCameraSequenceScript.hpp | 4 +- .../camera/CCameraSequenceScriptedBuilder.hpp | 2 +- .../CCameraSmokeRegressionUtilities.hpp | 168 +++++++------- .../include/camera/CCameraTextUtilities.hpp | 4 +- .../examples/camera/CCameraPersistence.cpp | 20 +- .../CCameraScriptedRuntimePersistence.cpp | 4 +- 21 files changed, 544 insertions(+), 524 deletions(-) diff --git a/61_UI/AppCameraPlanarRuntime.cpp b/61_UI/AppCameraPlanarRuntime.cpp index 4b4c7d865..e0e42f80f 100644 --- a/61_UI/AppCameraPlanarRuntime.cpp +++ b/61_UI/AppCameraPlanarRuntime.cpp @@ -46,7 +46,7 @@ bool tryCaptureInitialPlanarPresets( const auto captureAnalysis = core::CCameraGoalAnalysisUtilities::analyzeCameraCapture(goalSolver, camera); if (!captureAnalysis.canCapture) { - const auto kindLabel = camera ? std::string(core::getCameraKindLabel(camera->getKind())) : std::string("Unknown"); + 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" : @@ -62,7 +62,7 @@ bool tryCaptureInitialPlanarPresets( { outError = "Failed to build initial planar preset " + std::to_string(planarIx) + - " for camera kind \"" + (camera ? std::string(core::getCameraKindLabel(camera->getKind())) : std::string("Unknown")) + "\"."; + " for camera kind \"" + (camera ? std::string(core::CCameraKindUtilities::getCameraKindLabel(camera->getKind())) : std::string("Unknown")) + "\"."; return false; } diff --git a/61_UI/AppControlPanelPlaybackTab.cpp b/61_UI/AppControlPanelPlaybackTab.cpp index 058a4d438..8b2105549 100644 --- a/61_UI/AppControlPanelPlaybackTab.cpp +++ b/61_UI/AppControlPanelPlaybackTab.cpp @@ -42,13 +42,13 @@ void App::drawControlPanelPlaybackTab(const nbl::ui::SCameraControlPanelStyle& p ImGui::SameLine(); if (nbl::ui::drawActionButtonWithHint("Stop", "Stop playback and reset time")) { - nbl::core::resetPlaybackCursor(playbackAuthoring.playback); + nbl::core::CCameraPlaybackTimelineUtilities::resetPlaybackCursor(playbackAuthoring.playback); applyPlaybackAtTime(playbackAuthoring.playback.time); } if (!playbackAuthoring.keyframeTrack.keyframes.empty()) { - const float duration = nbl::core::getPlaybackTrackDuration(playbackAuthoring.keyframeTrack); + const float duration = nbl::core::CCameraPlaybackTimelineUtilities::getPlaybackTrackDuration(playbackAuthoring.keyframeTrack); if (ImGui::SliderFloat("Time", &playbackAuthoring.playback.time, 0.f, duration, "%.3f")) applyPlaybackAtTime(playbackAuthoring.playback.time); } @@ -104,7 +104,7 @@ void App::drawControlPanelPlaybackTab(const nbl::ui::SCameraControlPanelStyle& p if (nbl::ui::drawActionButtonWithHint("Clear keyframes", "Remove all keyframes")) { playbackAuthoring.keyframeTrack = {}; - nbl::core::resetPlaybackCursor(playbackAuthoring.playback); + nbl::core::CCameraPlaybackTimelineUtilities::resetPlaybackCursor(playbackAuthoring.playback); clearApplyStatusBanner(playbackAuthoring.applyBanner); } diff --git a/61_UI/AppHeadlessCameraSmokeChecks.inl b/61_UI/AppHeadlessCameraSmokeChecks.inl index 5b27eb130..0526512e4 100644 --- a/61_UI/AppHeadlessCameraSmokeChecks.inl +++ b/61_UI/AppHeadlessCameraSmokeChecks.inl @@ -225,7 +225,7 @@ outError = "Keyframe persistence smoke failed to deserialize track."; return false; } - if (!nbl::system::compareKeyframeTrackContentWithStrictThresholds(sourceTrack, loadedTrack)) + if (!nbl::system::CCameraSmokeRegressionUtilities::compareKeyframeTrackContentWithStrictThresholds(sourceTrack, loadedTrack)) { outError = "Keyframe persistence smoke changed stream track content."; return false; @@ -292,7 +292,7 @@ outError = "Keyframe persistence smoke failed to load track file."; return false; } - if (!nbl::system::compareKeyframeTrackContentWithStrictThresholds(sourceTrack, fileLoadedTrack)) + if (!nbl::system::CCameraSmokeRegressionUtilities::compareKeyframeTrackContentWithStrictThresholds(sourceTrack, fileLoadedTrack)) { outError = "Keyframe persistence smoke changed file track content."; return false; @@ -321,7 +321,7 @@ .time = SCameraSmokePlaybackDefaults::MidPlaybackTime }; - const auto advanceToEnd = nbl::core::advancePlaybackCursor(cursor, playbackTrack, SCameraSmokePlaybackDefaults::AdvanceDt); + 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."; @@ -333,7 +333,7 @@ return false; } - nbl::core::resetPlaybackCursor(cursor, SCameraSmokePlaybackDefaults::ResetPlaybackTime); + 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."; @@ -344,7 +344,7 @@ cursor.loop = true; cursor.speed = 1.f; cursor.time = SCameraSmokePlaybackDefaults::MidPlaybackTime; - const auto advanceLoop = nbl::core::advancePlaybackCursor(cursor, playbackTrack, SCameraSmokePlaybackDefaults::AdvanceDt); + 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."; @@ -357,7 +357,7 @@ } cursor.time = SCameraSmokePlaybackDefaults::OvershootPlaybackTime; - nbl::core::clampPlaybackCursorToTrack(playbackTrack, cursor); + 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."; @@ -609,7 +609,7 @@ scaledEvents[1].magnitude = 3.0; scaledEvents[2].type = CVirtualGimbalEvent::ScaleXInc; scaledEvents[2].magnitude = 4.0; - nbl::core::scaleVirtualEvents(scaledEvents, static_cast(scaledEvents.size()), 0.5f, 2.0f); + 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) @@ -624,7 +624,7 @@ CameraPreset orientedPreset = state.initialPresets.free.value(); orientedPreset.goal.orientation = hlsl::makeQuaternionFromEulerDegreesYXZ(SCameraSmokeManipulationDefaults::FreeOrientationYawDeg); const auto orientResult = nbl::core::applyPresetDetailed(state.goalSolver, state.freeCamera, orientedPreset); - if (!orientResult.succeeded() || !nbl::system::comparePresetToCameraStateWithStrictThresholds(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; @@ -638,7 +638,7 @@ worldTranslationEvents[2].type = CVirtualGimbalEvent::MoveForward; worldTranslationEvents[2].magnitude = SCameraSmokeManipulationDefaults::WorldTranslationDelta.z; uint32_t remappedCount = static_cast(worldTranslationEvents.size()); - nbl::core::remapTranslationEventsFromWorldToCameraLocal(state.freeCamera, worldTranslationEvents, remappedCount); + nbl::core::CCameraManipulationUtilities::remapTranslationEventsFromWorldToCameraLocal(state.freeCamera, worldTranslationEvents, remappedCount); if (remappedCount == 0u) { outError = "Camera manipulation utilities smoke produced empty translation remap."; @@ -674,7 +674,7 @@ .pitchMinDeg = SCameraSmokeManipulationDefaults::PitchMinDeg, .pitchMaxDeg = SCameraSmokeManipulationDefaults::PitchMaxDeg }; - if (!nbl::core::applyCameraConstraints(state.goalSolver, state.freeCamera, freeConstraints)) + if (!nbl::core::CCameraManipulationUtilities::applyCameraConstraints(state.goalSolver, state.freeCamera, freeConstraints)) { outError = "Camera manipulation utilities smoke failed to clamp Free camera orientation."; return false; @@ -688,7 +688,7 @@ } const auto restoreFree = nbl::core::applyPresetDetailed(state.goalSolver, state.freeCamera, state.initialPresets.free.value()); - if (!restoreFree.succeeded() || !nbl::system::comparePresetToCameraStateWithStrictThresholds(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; @@ -714,7 +714,7 @@ state.initialPresets.orbit->goal.distance * SCameraSmokeManipulationDefaults::OrbitClampMinScale), .maxDistance = state.initialPresets.orbit->goal.distance * SCameraSmokeManipulationDefaults::OrbitClampMaxScale }; - if (!nbl::core::applyCameraConstraints(state.goalSolver, state.orbitCamera, orbitConstraints)) + if (!nbl::core::CCameraManipulationUtilities::applyCameraConstraints(state.goalSolver, state.orbitCamera, orbitConstraints)) { outError = "Camera manipulation utilities smoke failed to clamp Orbit distance."; return false; @@ -729,7 +729,7 @@ } const auto restoreOrbit = nbl::core::applyPresetDetailed(state.goalSolver, state.orbitCamera, state.initialPresets.orbit.value()); - if (!restoreOrbit.succeeded() || !nbl::system::comparePresetToCameraStateWithStrictThresholds(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; diff --git a/61_UI/AppHeadlessCameraSmokeHelpers.inl b/61_UI/AppHeadlessCameraSmokeHelpers.inl index fa4569fec..ea360e6a4 100644 --- a/61_UI/AppHeadlessCameraSmokeHelpers.inl +++ b/61_UI/AppHeadlessCameraSmokeHelpers.inl @@ -387,9 +387,9 @@ namespace case EPresetComparePolicy::None: return true; case EPresetComparePolicy::DefaultThresholds: - return nbl::system::comparePresetToCameraStateWithDefaultThresholds(goalSolver, camera, preset); + return nbl::system::CCameraSmokeRegressionUtilities::comparePresetToCameraStateWithDefaultThresholds(goalSolver, camera, preset); case EPresetComparePolicy::StrictThresholds: - return nbl::system::comparePresetToCameraStateWithStrictThresholds(goalSolver, camera, preset); + return nbl::system::CCameraSmokeRegressionUtilities::comparePresetToCameraStateWithStrictThresholds(goalSolver, camera, preset); default: return false; } @@ -440,7 +440,7 @@ namespace std::string& outError) { const auto restoreResult = nbl::core::applyPresetDetailed(goalSolver, camera, preset); - if (restoreResult.succeeded() && nbl::system::comparePresetToCameraStateWithStrictThresholds(goalSolver, camera, preset)) + if (restoreResult.succeeded() && nbl::system::CCameraSmokeRegressionUtilities::comparePresetToCameraStateWithStrictThresholds(goalSolver, camera, preset)) return true; outError = std::string(failurePrefix) + ". " + CCameraTextUtilities::describeApplyResult(restoreResult); @@ -730,7 +730,7 @@ namespace } nbl::system::SCameraManipulationDelta directDelta = {}; - if (!nbl::system::tryManipulateCameraAndMeasureDelta(camera, { directEvents.data(), directEvents.size() }, directDelta, CameraTinyScalarEpsilon)) + if (!nbl::system::CCameraSmokeRegressionUtilities::tryManipulateCameraAndMeasureDelta(camera, { directEvents.data(), directEvents.size() }, directDelta, CameraTinyScalarEpsilon)) { outError = "Direct manipulate smoke failed for camera \"" + cameraIdentifier + "\"."; return false; @@ -786,7 +786,7 @@ namespace auto keyboardEvents = collectKeyboardVirtualEvents(inputBinder, key); if (keyboardEvents.empty()) continue; - if (nbl::system::tryManipulateCameraAndMeasureDelta(camera, { keyboardEvents.data(), keyboardEvents.size() }, keyboardDelta, CameraTinyScalarEpsilon)) + if (nbl::system::CCameraSmokeRegressionUtilities::tryManipulateCameraAndMeasureDelta(camera, { keyboardEvents.data(), keyboardEvents.size() }, keyboardDelta, CameraTinyScalarEpsilon)) { keyboardOk = true; break; @@ -823,7 +823,7 @@ namespace outError = "Mouse move virtual events missing for camera \"" + cameraIdentifier + "\"."; return false; } - if (!nbl::system::tryManipulateCameraAndMeasureDelta(camera, { mouseMoveEvents.data(), mouseMoveEvents.size() }, mouseMoveDelta, CameraTinyScalarEpsilon)) + if (!nbl::system::CCameraSmokeRegressionUtilities::tryManipulateCameraAndMeasureDelta(camera, { mouseMoveEvents.data(), mouseMoveEvents.size() }, mouseMoveDelta, CameraTinyScalarEpsilon)) { outError = "Mouse move binding smoke failed for camera \"" + cameraIdentifier + "\"."; return false; @@ -844,7 +844,7 @@ namespace outError = "Mouse scroll virtual events missing for camera \"" + cameraIdentifier + "\"."; return false; } - if (!nbl::system::tryManipulateCameraAndMeasureDelta(camera, { mouseScrollEvents.data(), mouseScrollEvents.size() }, mouseScrollDelta, CameraTinyScalarEpsilon)) + if (!nbl::system::CCameraSmokeRegressionUtilities::tryManipulateCameraAndMeasureDelta(camera, { mouseScrollEvents.data(), mouseScrollEvents.size() }, mouseScrollDelta, CameraTinyScalarEpsilon)) { outError = "Mouse scroll binding smoke failed for camera \"" + cameraIdentifier + "\"."; return false; @@ -1138,7 +1138,7 @@ namespace return false; } - if (!nbl::system::comparePresetToCameraStateWithStrictThresholds(goalSolver, camera, reachedEditedPreset)) + if (!nbl::system::CCameraSmokeRegressionUtilities::comparePresetToCameraStateWithStrictThresholds(goalSolver, camera, reachedEditedPreset)) { outError = std::string("Follow recapture smoke mismatch for ") + std::string(label) + ". " + nbl::core::describePresetCameraMismatch(goalSolver, camera, reachedEditedPreset); diff --git a/61_UI/AppInputRuntime.cpp b/61_UI/AppInputRuntime.cpp index 90d6b81e6..a3cc955df 100644 --- a/61_UI/AppInputRuntime.cpp +++ b/61_UI/AppInputRuntime.cpp @@ -84,7 +84,7 @@ inline void scaleCollectedVirtualEvents( for (uint32_t i = 0u; i < virtualEvents.keyboardVirtualEventCount; ++i) virtualEvents.events[i].magnitude *= cameraControls.keyboardScale; - nbl::core::scaleVirtualEvents( + nbl::core::CCameraManipulationUtilities::scaleVirtualEvents( virtualEvents.events, virtualEvents.totalCount(), cameraControls.translationScale, @@ -150,7 +150,7 @@ inline void applyCollectedVirtualEventsToCamera( { std::vector perCameraEvents = collectedVirtualEvents.events; uint32_t perCount = collectedVirtualEvents.totalCount(); - nbl::core::remapTranslationEventsFromWorldToCameraLocal(target, perCameraEvents, perCount); + nbl::core::CCameraManipulationUtilities::remapTranslationEventsFromWorldToCameraLocal(target, perCameraEvents, perCount); if (perCount) target->manipulate({ perCameraEvents.data(), perCount }); } @@ -159,7 +159,7 @@ inline void applyCollectedVirtualEventsToCamera( target->manipulate({ collectedVirtualEvents.events.data(), collectedVirtualEvents.totalCount() }); } - nbl::core::applyCameraConstraints(goalSolver, target, cameraConstraints); + nbl::core::CCameraManipulationUtilities::applyCameraConstraints(goalSolver, target, cameraConstraints); if (!scriptedInputEnabled) refreshFollowOffsets(planarIx); appendVirtualEventLog(target, planarIx, collectedVirtualEvents); diff --git a/61_UI/AppManipulableObjects.cpp b/61_UI/AppManipulableObjects.cpp index 8c7d36947..aba9010d4 100644 --- a/61_UI/AppManipulableObjects.cpp +++ b/61_UI/AppManipulableObjects.cpp @@ -214,7 +214,7 @@ void App::applyManipulableObjectTransform(const SManipulableObjectContext& conte case SceneManipulatedObjectKind::Camera: if (context.camera) { - nbl::core::applyReferenceFrameToCamera(context.camera, transform); + nbl::core::CCameraManipulationUtilities::applyReferenceFrameToCamera(context.camera, transform); if (context.planarIx.has_value()) refreshFollowOffsetConfigForPlanar(context.planarIx.value()); } diff --git a/61_UI/AppPresetPlayback.cpp b/61_UI/AppPresetPlayback.cpp index 4a693d0a5..96d2521d5 100644 --- a/61_UI/AppPresetPlayback.cpp +++ b/61_UI/AppPresetPlayback.cpp @@ -155,7 +155,7 @@ SCameraPresetApplySummary App::applyPresetToTargets(const CameraPreset& preset) bool App::tryBuildPlaybackPresetAtTime(const float time, CameraPreset& preset) { - return nbl::core::tryBuildKeyframeTrackPresetAtTime(m_playbackAuthoring.keyframeTrack, time, preset); + return nbl::core::CCameraKeyframeTrackUtilities::tryBuildKeyframeTrackPresetAtTime(m_playbackAuthoring.keyframeTrack, time, preset); } bool App::applyPlaybackAtTime(const float time) @@ -173,32 +173,32 @@ bool App::applyPlaybackAtTime(const float time) void App::sortKeyframesByTime() { - nbl::core::sortKeyframeTrackByTime(m_playbackAuthoring.keyframeTrack); + nbl::core::CCameraKeyframeTrackUtilities::sortKeyframeTrackByTime(m_playbackAuthoring.keyframeTrack); } void App::clampPlaybackTimeToKeyframes() { - nbl::core::clampPlaybackCursorToTrack(m_playbackAuthoring.keyframeTrack, m_playbackAuthoring.playback); + nbl::core::CCameraPlaybackTimelineUtilities::clampPlaybackCursorToTrack(m_playbackAuthoring.keyframeTrack, m_playbackAuthoring.playback); } int App::selectKeyframeNearestTime(const float time) { - return nbl::core::selectKeyframeTrackNearestTime(m_playbackAuthoring.keyframeTrack, time); + return nbl::core::CCameraKeyframeTrackUtilities::selectKeyframeTrackNearestTime(m_playbackAuthoring.keyframeTrack, time); } void App::normalizeSelectedKeyframe() { - nbl::core::normalizeSelectedKeyframeTrack(m_playbackAuthoring.keyframeTrack); + nbl::core::CCameraKeyframeTrackUtilities::normalizeSelectedKeyframeTrack(m_playbackAuthoring.keyframeTrack); } App::CameraKeyframe* App::getSelectedKeyframe() { - return nbl::core::getSelectedKeyframe(m_playbackAuthoring.keyframeTrack); + return nbl::core::CCameraKeyframeTrackUtilities::getSelectedKeyframe(m_playbackAuthoring.keyframeTrack); } const App::CameraKeyframe* App::getSelectedKeyframe() const { - return nbl::core::getSelectedKeyframe(m_playbackAuthoring.keyframeTrack); + return nbl::core::CCameraKeyframeTrackUtilities::getSelectedKeyframe(m_playbackAuthoring.keyframeTrack); } bool App::replaceSelectedKeyframeFromCamera(ICamera* camera) @@ -212,12 +212,12 @@ bool App::replaceSelectedKeyframeFromCamera(ICamera* camera) if (!nbl::core::tryCapturePreset(m_cameraGoalSolver, camera, keyframeName, updatedPreset)) return false; - return nbl::core::replaceSelectedKeyframePreset(m_playbackAuthoring.keyframeTrack, std::move(updatedPreset)); + return nbl::core::CCameraKeyframeTrackUtilities::replaceSelectedKeyframePreset(m_playbackAuthoring.keyframeTrack, std::move(updatedPreset)); } void App::updatePlayback(const double dtSec) { - const auto advance = nbl::core::advancePlaybackCursor(m_playbackAuthoring.playback, m_playbackAuthoring.keyframeTrack, dtSec); + const auto advance = nbl::core::CCameraPlaybackTimelineUtilities::advancePlaybackCursor(m_playbackAuthoring.playback, m_playbackAuthoring.keyframeTrack, dtSec); if (!advance.hasTrack || !advance.changedTime) return; diff --git a/61_UI/AppResourceUtilities.cpp b/61_UI/AppResourceUtilities.cpp index a21bb4f19..1095a71b7 100644 --- a/61_UI/AppResourceUtilities.cpp +++ b/61_UI/AppResourceUtilities.cpp @@ -48,7 +48,7 @@ inline bool loadSpaceEnvBlob( std::vector& outPayload) { std::vector blobBytes; - if (!nbl::system::readBinaryFile(system, blobPath, blobBytes)) + if (!nbl::system::CCameraFileUtilities::readBinaryFile(system, blobPath, blobBytes)) return false; return parseSpaceEnvBlobBytes(blobBytes, outHeader, outPayload); } diff --git a/61_UI/include/app/AppResourcePathUtilities.hpp b/61_UI/include/app/AppResourcePathUtilities.hpp index a21de41b4..fc14ce3b5 100644 --- a/61_UI/include/app/AppResourcePathUtilities.hpp +++ b/61_UI/include/app/AppResourcePathUtilities.hpp @@ -144,7 +144,7 @@ inline bool loadTextResource( candidates.asSpan(), [&](const path& candidate) -> bool { - return readTextFile(system, candidate, outText); + return CCameraFileUtilities::readTextFile(system, candidate, outText); }, outLoadedPath)) { diff --git a/common/include/camera/CCameraFileUtilities.hpp b/common/include/camera/CCameraFileUtilities.hpp index 19c9f8eaa..97d103019 100644 --- a/common/include/camera/CCameraFileUtilities.hpp +++ b/common/include/camera/CCameraFileUtilities.hpp @@ -10,72 +10,76 @@ namespace nbl::system { -inline bool readBinaryFile( - ISystem& system, - const path& filePath, - std::vector& outPayload, - std::string* error = nullptr, - const std::string_view openError = {}) +struct CCameraFileUtilities final { - ISystem::future_t> future; - system.createFile(future, filePath, IFile::ECF_READ | IFile::ECF_MAPPABLE); - auto file = future.acquire(); - if (!file || !file->get()) +public: + static inline bool readBinaryFile( + ISystem& system, + const path& filePath, + std::vector& outPayload, + std::string* error = nullptr, + const std::string_view openError = {}) { - if (error && !openError.empty()) - *error = std::string(openError); - return false; - } + ISystem::future_t> future; + system.createFile(future, filePath, IFile::ECF_READ | IFile::ECF_MAPPABLE); + auto file = future.acquire(); + if (!file || !file->get()) + { + if (error && !openError.empty()) + *error = std::string(openError); + return false; + } - auto& input = *file->get(); - const auto fileSize = input.getSize(); - outPayload.resize(fileSize); - if (outPayload.empty()) - return true; + auto& input = *file->get(); + const auto fileSize = input.getSize(); + outPayload.resize(fileSize); + if (outPayload.empty()) + return true; - IFile::success_t readResult; - input.read(readResult, outPayload.data(), 0, fileSize); - if (!static_cast(readResult)) - { - if (error && !openError.empty()) - *error = std::string(openError); - return false; + IFile::success_t readResult; + input.read(readResult, outPayload.data(), 0, fileSize); + if (!static_cast(readResult)) + { + if (error && !openError.empty()) + *error = std::string(openError); + return false; + } + return true; } - return true; -} - -inline bool readTextFile( - ISystem& system, - const path& filePath, - std::string& outText, - std::string* error = nullptr, - const std::string_view openError = {}) -{ - std::vector payload; - if (!readBinaryFile(system, filePath, payload, error, openError)) - return false; - outText.assign(reinterpret_cast(payload.data()), payload.size()); - return true; -} + static inline bool readTextFile( + ISystem& system, + const path& filePath, + std::string& outText, + std::string* error = nullptr, + const std::string_view openError = {}) + { + std::vector payload; + if (!readBinaryFile(system, filePath, payload, error, openError)) + return false; -inline bool writeTextFile( - ISystem& system, - const path& filePath, - const std::string_view text) -{ - ISystem::future_t> future; - system.createFile(future, filePath, IFile::ECF_WRITE); - auto file = future.acquire(); - if (!file || !file->get()) - return false; - if (text.empty()) + outText.assign(reinterpret_cast(payload.data()), payload.size()); return true; + } - IFile::success_t writeResult; - (*file)->write(writeResult, text.data(), 0, text.size()); - return static_cast(writeResult); -} + static inline bool writeTextFile( + ISystem& system, + const path& filePath, + const std::string_view text) + { + ISystem::future_t> future; + system.createFile(future, filePath, IFile::ECF_WRITE); + auto file = future.acquire(); + if (!file || !file->get()) + return false; + if (text.empty()) + return true; + + IFile::success_t writeResult; + (*file)->write(writeResult, text.data(), 0, text.size()); + return static_cast(writeResult); + } +}; } // namespace nbl::system diff --git a/common/include/camera/CCameraInputBindingUtilities.hpp b/common/include/camera/CCameraInputBindingUtilities.hpp index b2a1817a5..c7183432f 100644 --- a/common/include/camera/CCameraInputBindingUtilities.hpp +++ b/common/include/camera/CCameraInputBindingUtilities.hpp @@ -480,7 +480,7 @@ struct CCameraInputBindingUtilities final static inline const SCameraMappedInteractionBindingSpec& interactionBindingPresetForKind(const core::ICamera::CameraKind kind) { - const auto familyIx = interactionFamilyIndex(core::getCameraInteractionFamily(kind)); + const auto familyIx = interactionFamilyIndex(core::CCameraKindUtilities::getCameraInteractionFamily(kind)); static const auto cache = makePresetCache( InteractionFamilyPresetSpecs, [](const SCameraInteractionBindingSpec& spec) { return mapInteractionBindingSpec(spec); }); diff --git a/common/include/camera/CCameraKeyframeTrack.hpp b/common/include/camera/CCameraKeyframeTrack.hpp index 28de97da1..734e48c95 100644 --- a/common/include/camera/CCameraKeyframeTrack.hpp +++ b/common/include/camera/CCameraKeyframeTrack.hpp @@ -21,145 +21,149 @@ struct CCameraKeyframeTrack int selectedKeyframeIx = -1; }; -//! Compare two keyframes by authored time and shared preset state. -inline bool compareKeyframes(const CCameraKeyframe& lhs, const CCameraKeyframe& rhs, - const double timeEps, const double posEps, const double rotEpsDeg, const double scalarEps) +struct CCameraKeyframeTrackUtilities final { - return hlsl::abs(static_cast(lhs.time - rhs.time)) <= timeEps && - comparePresets(lhs.preset, rhs.preset, posEps, rotEpsDeg, scalarEps); -} - -//! Compare two authored keyframe tracks with optional selection-state checking. -inline bool compareKeyframeTracks(const CCameraKeyframeTrack& lhs, const CCameraKeyframeTrack& rhs, - const double timeEps, const double posEps, const double rotEpsDeg, const double scalarEps, const bool compareSelection = true) -{ - if ((compareSelection && lhs.selectedKeyframeIx != rhs.selectedKeyframeIx) || lhs.keyframes.size() != rhs.keyframes.size()) - return false; - - for (size_t i = 0u; i < lhs.keyframes.size(); ++i) +public: + //! Compare two keyframes by authored time and shared preset state. + static inline bool compareKeyframes(const CCameraKeyframe& lhs, const CCameraKeyframe& rhs, + const double timeEps, const double posEps, const double rotEpsDeg, const double scalarEps) { - if (!compareKeyframes(lhs.keyframes[i], rhs.keyframes[i], timeEps, posEps, rotEpsDeg, scalarEps)) - return false; + return hlsl::abs(static_cast(lhs.time - rhs.time)) <= timeEps && + comparePresets(lhs.preset, rhs.preset, posEps, rotEpsDeg, scalarEps); } - return true; -} - -//! Compare only the serialized/authored content of two tracks and ignore transient UI selection state. -inline bool compareKeyframeTrackContent(const CCameraKeyframeTrack& lhs, const CCameraKeyframeTrack& rhs, - const double timeEps, const double posEps, const double rotEpsDeg, const double scalarEps) -{ - return compareKeyframeTracks(lhs, rhs, timeEps, posEps, rotEpsDeg, scalarEps, false); -} + //! Compare two authored keyframe tracks with optional selection-state checking. + static inline bool compareKeyframeTracks(const CCameraKeyframeTrack& lhs, const CCameraKeyframeTrack& rhs, + const double timeEps, const double posEps, const double rotEpsDeg, const double scalarEps, const bool compareSelection = true) + { + if ((compareSelection && lhs.selectedKeyframeIx != rhs.selectedKeyframeIx) || lhs.keyframes.size() != rhs.keyframes.size()) + return false; -inline bool tryBuildKeyframeTrackPresetAtTime(const CCameraKeyframeTrack& track, const float time, CCameraPreset& preset) -{ - if (track.keyframes.empty()) - return false; + for (size_t i = 0u; i < lhs.keyframes.size(); ++i) + { + if (!compareKeyframes(lhs.keyframes[i], rhs.keyframes[i], timeEps, posEps, rotEpsDeg, scalarEps)) + return false; + } - if (track.keyframes.size() == 1u) - { - preset = track.keyframes.front().preset; return true; } - const auto clampedTime = std::clamp(time, 0.f, track.keyframes.back().time); - size_t idx = 0u; - while (idx + 1u < track.keyframes.size() && track.keyframes[idx + 1u].time < clampedTime) - ++idx; + //! Compare only the serialized/authored content of two tracks and ignore transient UI selection state. + static inline bool compareKeyframeTrackContent(const CCameraKeyframeTrack& lhs, const CCameraKeyframeTrack& rhs, + const double timeEps, const double posEps, const double rotEpsDeg, const double scalarEps) + { + return compareKeyframeTracks(lhs, rhs, timeEps, posEps, rotEpsDeg, scalarEps, false); + } - const auto& a = track.keyframes[idx]; - const auto& b = track.keyframes[std::min(idx + 1u, track.keyframes.size() - 1u)]; - if (b.time <= a.time) + static inline bool tryBuildKeyframeTrackPresetAtTime(const CCameraKeyframeTrack& track, const float time, CCameraPreset& preset) { + if (track.keyframes.empty()) + return false; + + if (track.keyframes.size() == 1u) + { + preset = track.keyframes.front().preset; + return true; + } + + const auto clampedTime = std::clamp(time, 0.f, track.keyframes.back().time); + size_t idx = 0u; + while (idx + 1u < track.keyframes.size() && track.keyframes[idx + 1u].time < clampedTime) + ++idx; + + const auto& a = track.keyframes[idx]; + const auto& b = track.keyframes[std::min(idx + 1u, track.keyframes.size() - 1u)]; + if (b.time <= a.time) + { + preset = a.preset; + return true; + } + + const double alpha = static_cast(clampedTime - a.time) / static_cast(b.time - a.time); preset = a.preset; + assignGoalToPreset(preset, CCameraGoalUtilities::blendGoals(makeGoalFromPreset(a.preset), makeGoalFromPreset(b.preset), alpha)); return true; } - const double alpha = static_cast(clampedTime - a.time) / static_cast(b.time - a.time); - preset = a.preset; - assignGoalToPreset(preset, CCameraGoalUtilities::blendGoals(makeGoalFromPreset(a.preset), makeGoalFromPreset(b.preset), alpha)); - return true; -} - -inline void sortKeyframeTrackByTime(CCameraKeyframeTrack& track) -{ - std::sort(track.keyframes.begin(), track.keyframes.end(), [](const auto& a, const auto& b) { return a.time < b.time; }); -} - -inline void clampTrackTimeToKeyframes(const CCameraKeyframeTrack& track, float& time) -{ - if (track.keyframes.empty()) + static inline void sortKeyframeTrackByTime(CCameraKeyframeTrack& track) { - time = 0.f; - return; + std::sort(track.keyframes.begin(), track.keyframes.end(), [](const auto& a, const auto& b) { return a.time < b.time; }); } - time = std::clamp(time, 0.f, track.keyframes.back().time); -} + static inline void clampTrackTimeToKeyframes(const CCameraKeyframeTrack& track, float& time) + { + if (track.keyframes.empty()) + { + time = 0.f; + return; + } -inline int selectKeyframeTrackNearestTime(CCameraKeyframeTrack& track, const float time) -{ - if (track.keyframes.empty()) + time = std::clamp(time, 0.f, track.keyframes.back().time); + } + + static inline int selectKeyframeTrackNearestTime(CCameraKeyframeTrack& track, const float time) { - track.selectedKeyframeIx = -1; + if (track.keyframes.empty()) + { + track.selectedKeyframeIx = -1; + return track.selectedKeyframeIx; + } + + size_t bestIx = 0u; + float bestDelta = hlsl::abs(track.keyframes.front().time - time); + for (size_t i = 1u; i < track.keyframes.size(); ++i) + { + const float delta = hlsl::abs(track.keyframes[i].time - time); + if (delta < bestDelta) + { + bestDelta = delta; + bestIx = i; + } + } + + track.selectedKeyframeIx = static_cast(bestIx); return track.selectedKeyframeIx; } - size_t bestIx = 0u; - float bestDelta = hlsl::abs(track.keyframes.front().time - time); - for (size_t i = 1u; i < track.keyframes.size(); ++i) + static inline void normalizeSelectedKeyframeTrack(CCameraKeyframeTrack& track) { - const float delta = hlsl::abs(track.keyframes[i].time - time); - if (delta < bestDelta) + if (track.keyframes.empty()) { - bestDelta = delta; - bestIx = i; + track.selectedKeyframeIx = -1; + return; } - } - track.selectedKeyframeIx = static_cast(bestIx); - return track.selectedKeyframeIx; -} + if (track.selectedKeyframeIx < 0) + track.selectedKeyframeIx = 0; + else if (track.selectedKeyframeIx >= static_cast(track.keyframes.size())) + track.selectedKeyframeIx = static_cast(track.keyframes.size()) - 1; + } -inline void normalizeSelectedKeyframeTrack(CCameraKeyframeTrack& track) -{ - if (track.keyframes.empty()) + static inline CCameraKeyframe* getSelectedKeyframe(CCameraKeyframeTrack& track) { - track.selectedKeyframeIx = -1; - return; + normalizeSelectedKeyframeTrack(track); + if (track.selectedKeyframeIx < 0) + return nullptr; + return &track.keyframes[static_cast(track.selectedKeyframeIx)]; } - if (track.selectedKeyframeIx < 0) - track.selectedKeyframeIx = 0; - else if (track.selectedKeyframeIx >= static_cast(track.keyframes.size())) - track.selectedKeyframeIx = static_cast(track.keyframes.size()) - 1; -} - -inline CCameraKeyframe* getSelectedKeyframe(CCameraKeyframeTrack& track) -{ - normalizeSelectedKeyframeTrack(track); - if (track.selectedKeyframeIx < 0) - return nullptr; - return &track.keyframes[static_cast(track.selectedKeyframeIx)]; -} - -inline const CCameraKeyframe* getSelectedKeyframe(const CCameraKeyframeTrack& track) -{ - if (track.selectedKeyframeIx < 0 || track.selectedKeyframeIx >= static_cast(track.keyframes.size())) - return nullptr; - return &track.keyframes[static_cast(track.selectedKeyframeIx)]; -} + static inline const CCameraKeyframe* getSelectedKeyframe(const CCameraKeyframeTrack& track) + { + if (track.selectedKeyframeIx < 0 || track.selectedKeyframeIx >= static_cast(track.keyframes.size())) + return nullptr; + return &track.keyframes[static_cast(track.selectedKeyframeIx)]; + } -inline bool replaceSelectedKeyframePreset(CCameraKeyframeTrack& track, CCameraPreset preset) -{ - auto* selected = getSelectedKeyframe(track); - if (!selected) - return false; + static inline bool replaceSelectedKeyframePreset(CCameraKeyframeTrack& track, CCameraPreset preset) + { + auto* selected = getSelectedKeyframe(track); + if (!selected) + return false; - selected->preset = std::move(preset); - return true; -} + selected->preset = std::move(preset); + return true; + } +}; } // namespace nbl::core diff --git a/common/include/camera/CCameraKindUtilities.hpp b/common/include/camera/CCameraKindUtilities.hpp index d461ab3b7..57c6814e5 100644 --- a/common/include/camera/CCameraKindUtilities.hpp +++ b/common/include/camera/CCameraKindUtilities.hpp @@ -31,108 +31,108 @@ struct SCameraKindTraits final ECameraInteractionFamily interactionFamily = ECameraInteractionFamily::None; }; -namespace impl +struct CCameraKindUtilities final { - -inline constexpr std::array(ICamera::CameraKind::Path) + 1u> CameraKindTraitsTable = {{ - { - .kind = ICamera::CameraKind::Unknown, - .label = "Unknown", - .description = "Unspecified camera behavior", - .interactionFamily = ECameraInteractionFamily::None - }, - { - .kind = ICamera::CameraKind::FPS, - .label = "FPS", - .description = "First-person WASD + mouse look", - .interactionFamily = ECameraInteractionFamily::Fps - }, - { - .kind = ICamera::CameraKind::Free, - .label = "Free", - .description = "Free-fly 6DOF with full rotation", - .interactionFamily = ECameraInteractionFamily::Free - }, - { - .kind = ICamera::CameraKind::Orbit, - .label = "Orbit", - .description = "Orbit around target with dolly", - .interactionFamily = ECameraInteractionFamily::Orbit - }, - { - .kind = ICamera::CameraKind::Arcball, - .label = "Arcball", - .description = "Arcball trackball around target", - .interactionFamily = ECameraInteractionFamily::Orbit - }, - { - .kind = ICamera::CameraKind::Turntable, - .label = "Turntable", - .description = "Turntable yaw/pitch around target", - .interactionFamily = ECameraInteractionFamily::Turntable - }, - { - .kind = ICamera::CameraKind::TopDown, - .label = "TopDown", - .description = "Fixed pitch top-down pan", - .interactionFamily = ECameraInteractionFamily::TopDown - }, - { - .kind = ICamera::CameraKind::Isometric, - .label = "Isometric", - .description = "Fixed isometric view with pan", - .interactionFamily = ECameraInteractionFamily::Orbit - }, - { - .kind = ICamera::CameraKind::Chase, - .label = "Chase", - .description = "Target follow with chase controls", - .interactionFamily = ECameraInteractionFamily::TargetRig - }, - { - .kind = ICamera::CameraKind::Dolly, - .label = "Dolly", - .description = "Rig truck/dolly with look-at", - .interactionFamily = ECameraInteractionFamily::TargetRig - }, - { - .kind = ICamera::CameraKind::DollyZoom, - .label = "Dolly Zoom", - .description = "Orbit with dolly-zoom FOV", - .interactionFamily = ECameraInteractionFamily::Orbit - }, +public: + static inline constexpr const SCameraKindTraits& getCameraKindTraits(const ICamera::CameraKind kind) { - .kind = ICamera::CameraKind::Path, - .label = "Path Rig", - .description = SCameraPathDefaults::Description, - .interactionFamily = ECameraInteractionFamily::Path + const auto ix = static_cast(kind); + if (ix >= CameraKindTraitsTable.size()) + return CameraKindTraitsTable[0u]; + return CameraKindTraitsTable[ix]; } -}}; -} // namespace impl - -inline constexpr const SCameraKindTraits& getCameraKindTraits(const ICamera::CameraKind kind) -{ - const auto ix = static_cast(kind); - if (ix >= impl::CameraKindTraitsTable.size()) - return impl::CameraKindTraitsTable[0u]; - return impl::CameraKindTraitsTable[ix]; -} + static inline constexpr std::string_view getCameraKindLabel(const ICamera::CameraKind kind) + { + return getCameraKindTraits(kind).label; + } -inline constexpr std::string_view getCameraKindLabel(const ICamera::CameraKind kind) -{ - return getCameraKindTraits(kind).label; -} + static inline constexpr std::string_view getCameraKindDescription(const ICamera::CameraKind kind) + { + return getCameraKindTraits(kind).description; + } -inline constexpr std::string_view getCameraKindDescription(const ICamera::CameraKind kind) -{ - return getCameraKindTraits(kind).description; -} + static inline constexpr ECameraInteractionFamily getCameraInteractionFamily(const ICamera::CameraKind kind) + { + return getCameraKindTraits(kind).interactionFamily; + } -inline constexpr ECameraInteractionFamily getCameraInteractionFamily(const ICamera::CameraKind kind) -{ - return getCameraKindTraits(kind).interactionFamily; -} +private: + static inline constexpr std::array(ICamera::CameraKind::Path) + 1u> CameraKindTraitsTable = {{ + { + .kind = ICamera::CameraKind::Unknown, + .label = "Unknown", + .description = "Unspecified camera behavior", + .interactionFamily = ECameraInteractionFamily::None + }, + { + .kind = ICamera::CameraKind::FPS, + .label = "FPS", + .description = "First-person WASD + mouse look", + .interactionFamily = ECameraInteractionFamily::Fps + }, + { + .kind = ICamera::CameraKind::Free, + .label = "Free", + .description = "Free-fly 6DOF with full rotation", + .interactionFamily = ECameraInteractionFamily::Free + }, + { + .kind = ICamera::CameraKind::Orbit, + .label = "Orbit", + .description = "Orbit around target with dolly", + .interactionFamily = ECameraInteractionFamily::Orbit + }, + { + .kind = ICamera::CameraKind::Arcball, + .label = "Arcball", + .description = "Arcball trackball around target", + .interactionFamily = ECameraInteractionFamily::Orbit + }, + { + .kind = ICamera::CameraKind::Turntable, + .label = "Turntable", + .description = "Turntable yaw/pitch around target", + .interactionFamily = ECameraInteractionFamily::Turntable + }, + { + .kind = ICamera::CameraKind::TopDown, + .label = "TopDown", + .description = "Fixed pitch top-down pan", + .interactionFamily = ECameraInteractionFamily::TopDown + }, + { + .kind = ICamera::CameraKind::Isometric, + .label = "Isometric", + .description = "Fixed isometric view with pan", + .interactionFamily = ECameraInteractionFamily::Orbit + }, + { + .kind = ICamera::CameraKind::Chase, + .label = "Chase", + .description = "Target follow with chase controls", + .interactionFamily = ECameraInteractionFamily::TargetRig + }, + { + .kind = ICamera::CameraKind::Dolly, + .label = "Dolly", + .description = "Rig truck/dolly with look-at", + .interactionFamily = ECameraInteractionFamily::TargetRig + }, + { + .kind = ICamera::CameraKind::DollyZoom, + .label = "Dolly Zoom", + .description = "Orbit with dolly-zoom FOV", + .interactionFamily = ECameraInteractionFamily::Orbit + }, + { + .kind = ICamera::CameraKind::Path, + .label = "Path Rig", + .description = SCameraPathDefaults::Description, + .interactionFamily = ECameraInteractionFamily::Path + } + }}; +}; } // namespace nbl::core diff --git a/common/include/camera/CCameraManipulationUtilities.hpp b/common/include/camera/CCameraManipulationUtilities.hpp index 0f63cf886..5a4b0add7 100644 --- a/common/include/camera/CCameraManipulationUtilities.hpp +++ b/common/include/camera/CCameraManipulationUtilities.hpp @@ -44,107 +44,111 @@ struct SCameraConstraintSettings float maxDistance = SCameraConstraintDefaults::MaxDistance; }; -//! Apply an authored world-space reference frame through the shared camera runtime entry point. -inline bool applyReferenceFrameToCamera(ICamera* camera, const hlsl::float64_t4x4& referenceFrame) +struct CCameraManipulationUtilities final { - if (!camera) - return false; +public: + //! Apply an authored world-space reference frame through the shared camera runtime entry point. + static inline bool applyReferenceFrameToCamera(ICamera* camera, const hlsl::float64_t4x4& referenceFrame) + { + if (!camera) + return false; - return camera->manipulateWithUnitMotionScales({}, &referenceFrame); -} + return camera->manipulateWithUnitMotionScales({}, &referenceFrame); + } -//! Scale translation and rotation event magnitudes without touching unrelated event types. -inline void scaleVirtualEvents(std::vector& events, const uint32_t count, const float translationScale, const float rotationScale) -{ - for (uint32_t i = 0u; i < count; ++i) + //! Scale translation and rotation event magnitudes without touching unrelated event types. + static inline void scaleVirtualEvents(std::vector& events, const uint32_t count, const float translationScale, const float rotationScale) { - auto& ev = events[i]; - if (CVirtualGimbalEvent::isTranslationEvent(ev.type)) + for (uint32_t i = 0u; i < count; ++i) { - ev.magnitude *= translationScale; - } - else if (CVirtualGimbalEvent::isRotationEvent(ev.type)) - { - ev.magnitude *= rotationScale; + auto& ev = events[i]; + if (CVirtualGimbalEvent::isTranslationEvent(ev.type)) + { + ev.magnitude *= translationScale; + } + else if (CVirtualGimbalEvent::isRotationEvent(ev.type)) + { + ev.magnitude *= rotationScale; + } } } -} -//! Reinterpret world-space translation intents as local camera-space movement events. -inline void remapTranslationEventsFromWorldToCameraLocal(ICamera* camera, std::vector& events, uint32_t& count) -{ - if (!camera) - return; + //! Reinterpret world-space translation intents as local camera-space movement events. + static inline void remapTranslationEventsFromWorldToCameraLocal(ICamera* camera, std::vector& events, uint32_t& count) + { + if (!camera) + return; - std::vector filtered; - filtered.reserve(events.size()); + std::vector filtered; + filtered.reserve(events.size()); - for (uint32_t i = 0u; i < count; ++i) - { - const auto& ev = events[i]; - if (!CVirtualGimbalEvent::isTranslationEvent(ev.type)) - filtered.emplace_back(ev); - } + for (uint32_t i = 0u; i < count; ++i) + { + const auto& ev = events[i]; + if (!CVirtualGimbalEvent::isTranslationEvent(ev.type)) + filtered.emplace_back(ev); + } + + const auto worldDelta = CCameraVirtualEventUtilities::collectSignedTranslationDelta({ events.data(), count }); + if (hlsl::isNearlyZeroVector(worldDelta, static_cast(ICamera::TinyScalarEpsilon))) + { + events = std::move(filtered); + count = static_cast(events.size()); + return; + } + + CCameraVirtualEventUtilities::appendWorldTranslationAsLocalEvents(filtered, camera->getGimbal().getOrientation(), worldDelta); - const auto worldDelta = CCameraVirtualEventUtilities::collectSignedTranslationDelta({ events.data(), count }); - if (hlsl::isNearlyZeroVector(worldDelta, static_cast(ICamera::TinyScalarEpsilon))) - { events = std::move(filtered); count = static_cast(events.size()); - return; } - CCameraVirtualEventUtilities::appendWorldTranslationAsLocalEvents(filtered, camera->getGimbal().getOrientation(), worldDelta); + //! Apply shared distance and Euler-angle constraints after manipulation. + static inline bool applyCameraConstraints(const CCameraGoalSolver& solver, ICamera* camera, const SCameraConstraintSettings& constraints) + { + if (!constraints.enabled || !camera) + return false; - events = std::move(filtered); - count = static_cast(events.size()); -} + if (camera->hasCapability(ICamera::SphericalTarget)) + { + if (!constraints.clampDistance) + return false; -//! Apply shared distance and Euler-angle constraints after manipulation. -inline bool applyCameraConstraints(const CCameraGoalSolver& solver, ICamera* camera, const SCameraConstraintSettings& constraints) -{ - if (!constraints.enabled || !camera) - return false; + ICamera::SphericalTargetState sphericalState; + if (!camera->tryGetSphericalTargetState(sphericalState)) + return false; - if (camera->hasCapability(ICamera::SphericalTarget)) - { - if (!constraints.clampDistance) - return false; + const float clamped = std::clamp(sphericalState.distance, constraints.minDistance, constraints.maxDistance); + if (clamped == sphericalState.distance) + return false; + + return camera->trySetSphericalDistance(clamped); + } - ICamera::SphericalTargetState sphericalState; - if (!camera->tryGetSphericalTargetState(sphericalState)) + if (!(constraints.clampPitch || constraints.clampYaw || constraints.clampRoll)) return false; - const float clamped = std::clamp(sphericalState.distance, constraints.minDistance, constraints.maxDistance); - if (clamped == sphericalState.distance) + const auto& gimbal = camera->getGimbal(); + const auto pos = gimbal.getPosition(); + const auto eulerDeg = hlsl::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; - return camera->trySetSphericalDistance(clamped); + CCameraPreset preset; + preset.goal.position = pos; + preset.goal.orientation = hlsl::makeQuaternionFromEulerDegreesYXZ(clamped); + return applyPreset(solver, camera, preset); } - - if (!(constraints.clampPitch || constraints.clampYaw || constraints.clampRoll)) - return false; - - const auto& gimbal = camera->getGimbal(); - const auto pos = gimbal.getPosition(); - const auto eulerDeg = hlsl::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; - - CCameraPreset preset; - preset.goal.position = pos; - preset.goal.orientation = hlsl::makeQuaternionFromEulerDegreesYXZ(clamped); - return applyPreset(solver, camera, preset); -} +}; } // namespace nbl::core diff --git a/common/include/camera/CCameraPlaybackTimeline.hpp b/common/include/camera/CCameraPlaybackTimeline.hpp index 1dc39019c..aded91569 100644 --- a/common/include/camera/CCameraPlaybackTimeline.hpp +++ b/common/include/camera/CCameraPlaybackTimeline.hpp @@ -33,66 +33,70 @@ struct SCameraPlaybackAdvanceResult float time = 0.f; }; -//! Duration of the current playback track in seconds. -inline float getPlaybackTrackDuration(const CCameraKeyframeTrack& track) +struct CCameraPlaybackTimelineUtilities final { - if (track.keyframes.empty()) - return 0.f; +public: + //! Duration of the current playback track in seconds. + static inline float getPlaybackTrackDuration(const CCameraKeyframeTrack& track) + { + if (track.keyframes.empty()) + return 0.f; - return track.keyframes.back().time; -} + return track.keyframes.back().time; + } -//! Reset cursor time and stop playback without mutating loop or speed settings. -inline void resetPlaybackCursor(CCameraPlaybackCursor& cursor, const float time = 0.f) -{ - cursor.playing = false; - cursor.time = std::max(0.f, time); -} + //! Reset cursor time and stop playback without mutating loop or speed settings. + static inline void resetPlaybackCursor(CCameraPlaybackCursor& cursor, const float time = 0.f) + { + cursor.playing = false; + cursor.time = std::max(0.f, time); + } -//! Clamp cursor time into the valid time range of the current track. -inline void clampPlaybackCursorToTrack(const CCameraKeyframeTrack& track, CCameraPlaybackCursor& cursor) -{ - clampTrackTimeToKeyframes(track, cursor.time); -} + //! Clamp cursor time into the valid time range of the current track. + static inline void clampPlaybackCursorToTrack(const CCameraKeyframeTrack& track, CCameraPlaybackCursor& cursor) + { + CCameraKeyframeTrackUtilities::clampTrackTimeToKeyframes(track, cursor.time); + } -//! Advance cursor time by `dtSec * speed` and report whether playback wrapped or stopped. -inline SCameraPlaybackAdvanceResult advancePlaybackCursor(CCameraPlaybackCursor& cursor, const CCameraKeyframeTrack& track, const double dtSec) -{ - SCameraPlaybackAdvanceResult result; - result.hasTrack = !track.keyframes.empty(); - result.duration = getPlaybackTrackDuration(track); - result.time = cursor.time; + //! Advance cursor time by `dtSec * speed` and report whether playback wrapped or stopped. + static inline SCameraPlaybackAdvanceResult advancePlaybackCursor(CCameraPlaybackCursor& cursor, const CCameraKeyframeTrack& track, const double dtSec) + { + SCameraPlaybackAdvanceResult result; + result.hasTrack = !track.keyframes.empty(); + result.duration = getPlaybackTrackDuration(track); + result.time = cursor.time; - if (!result.hasTrack || !cursor.playing) - return result; + if (!result.hasTrack || !cursor.playing) + return result; - const auto previousTime = cursor.time; - cursor.time += static_cast(dtSec * cursor.speed); - result.changedTime = cursor.time != previousTime; - result.time = cursor.time; + const auto previousTime = cursor.time; + cursor.time += static_cast(dtSec * cursor.speed); + result.changedTime = cursor.time != previousTime; + result.time = cursor.time; - if (result.duration <= 0.f) - return result; + if (result.duration <= 0.f) + return result; - if (cursor.loop) - { - while (cursor.time > result.duration) + if (cursor.loop) { - cursor.time -= result.duration; - result.wrapped = true; + while (cursor.time > result.duration) + { + cursor.time -= result.duration; + result.wrapped = true; + } + } + else if (cursor.time > result.duration) + { + cursor.time = result.duration; + cursor.playing = false; + result.reachedEnd = true; + result.stopped = true; } - } - else if (cursor.time > result.duration) - { - cursor.time = result.duration; - cursor.playing = false; - result.reachedEnd = true; - result.stopped = true; - } - result.time = cursor.time; - return result; -} + result.time = cursor.time; + return result; + } +}; } // namespace nbl::core diff --git a/common/include/camera/CCameraSequenceScript.hpp b/common/include/camera/CCameraSequenceScript.hpp index f4e4b44bd..0b19a2149 100644 --- a/common/include/camera/CCameraSequenceScript.hpp +++ b/common/include/camera/CCameraSequenceScript.hpp @@ -441,8 +441,8 @@ struct CCameraSequenceScriptUtilities final outTrack.keyframes.emplace_back(std::move(keyframe)); } - sortKeyframeTrackByTime(outTrack); - normalizeSelectedKeyframeTrack(outTrack); + CCameraKeyframeTrackUtilities::sortKeyframeTrackByTime(outTrack); + CCameraKeyframeTrackUtilities::normalizeSelectedKeyframeTrack(outTrack); return !outTrack.keyframes.empty(); } diff --git a/common/include/camera/CCameraSequenceScriptedBuilder.hpp b/common/include/camera/CCameraSequenceScriptedBuilder.hpp index 6c1e26fce..ef2e85e2f 100644 --- a/common/include/camera/CCameraSequenceScriptedBuilder.hpp +++ b/common/include/camera/CCameraSequenceScriptedBuilder.hpp @@ -75,7 +75,7 @@ inline bool appendCompiledSequenceSegmentToScriptedTimeline( for (const auto& policy : framePolicies) { core::CCameraPreset preset; - if (!tryBuildKeyframeTrackPresetAtTime(compiledSegment.track, policy.sampleTime, preset)) + if (!core::CCameraKeyframeTrackUtilities::tryBuildKeyframeTrackPresetAtTime(compiledSegment.track, policy.sampleTime, preset)) { if (error) *error = "Failed to sample compiled segment track."; diff --git a/common/include/camera/CCameraSmokeRegressionUtilities.hpp b/common/include/camera/CCameraSmokeRegressionUtilities.hpp index 524ee0029..863300e71 100644 --- a/common/include/camera/CCameraSmokeRegressionUtilities.hpp +++ b/common/include/camera/CCameraSmokeRegressionUtilities.hpp @@ -29,89 +29,93 @@ struct SCameraSmokeComparisonThresholds final static constexpr double TrackTimeTolerance = core::ICamera::ScalarTolerance; }; -//! Measure one camera pose delta against an authored reference pose. -inline bool tryComputeCameraManipulationDelta( - core::ICamera* camera, - const hlsl::float64_t3& beforePosition, - const hlsl::camera_quaternion_t& beforeOrientation, - SCameraManipulationDelta& outDelta) +struct CCameraSmokeRegressionUtilities final { - outDelta = {}; - if (!camera) - return false; - - const auto& gimbal = camera->getGimbal(); - const auto afterPosition = gimbal.getPosition(); - const auto afterOrientation = hlsl::normalizeQuaternion(gimbal.getOrientation()); - return hlsl::tryComputePoseDelta(afterPosition, afterOrientation, beforePosition, beforeOrientation, outDelta); -} - -//! Manipulate a camera and report how far its pose moved in position and Euler-angle terms. -inline bool tryManipulateCameraAndMeasureDelta( - core::ICamera* camera, - std::span events, - SCameraManipulationDelta& outDelta, - const double tinyEpsilon = SCameraSmokeComparisonThresholds::TinyScalarEpsilon) -{ - outDelta = {}; - if (!camera || events.empty()) - return false; - - const auto& beforeGimbal = camera->getGimbal(); - const auto beforePosition = beforeGimbal.getPosition(); - const auto beforeOrientation = hlsl::normalizeQuaternion(beforeGimbal.getOrientation()); - if (!hlsl::isFiniteVec3(beforePosition) || !hlsl::isFiniteQuaternion(beforeOrientation)) - return false; - - if (!camera->manipulate(events)) - return false; - - if (!tryComputeCameraManipulationDelta(camera, beforePosition, beforeOrientation, outDelta)) - return false; - - return outDelta.position > tinyEpsilon || outDelta.rotationDeg > tinyEpsilon; -} - -inline bool comparePresetToCameraStateWithDefaultThresholds( - const core::CCameraGoalSolver& solver, - core::ICamera* camera, - const core::CCameraPreset& preset) -{ - return core::comparePresetToCameraState( - solver, - camera, - preset, - SCameraSmokeComparisonThresholds::DefaultPositionTolerance, - SCameraSmokeComparisonThresholds::DefaultAngularToleranceDeg, - SCameraSmokeComparisonThresholds::DefaultScalarTolerance); -} - -inline bool comparePresetToCameraStateWithStrictThresholds( - const core::CCameraGoalSolver& solver, - core::ICamera* camera, - const core::CCameraPreset& preset) -{ - return core::comparePresetToCameraState( - solver, - camera, - preset, - SCameraSmokeComparisonThresholds::StrictPositionTolerance, - SCameraSmokeComparisonThresholds::StrictAngularToleranceDeg, - SCameraSmokeComparisonThresholds::StrictScalarTolerance); -} - -inline bool compareKeyframeTrackContentWithStrictThresholds( - const core::CCameraKeyframeTrack& lhs, - const core::CCameraKeyframeTrack& rhs) -{ - return core::compareKeyframeTrackContent( - lhs, - rhs, - SCameraSmokeComparisonThresholds::TrackTimeTolerance, - SCameraSmokeComparisonThresholds::StrictPositionTolerance, - SCameraSmokeComparisonThresholds::StrictAngularToleranceDeg, - SCameraSmokeComparisonThresholds::StrictScalarTolerance); -} +public: + //! Measure one camera pose delta against an authored reference pose. + static inline bool tryComputeCameraManipulationDelta( + core::ICamera* camera, + const hlsl::float64_t3& beforePosition, + const hlsl::camera_quaternion_t& beforeOrientation, + SCameraManipulationDelta& outDelta) + { + outDelta = {}; + if (!camera) + return false; + + const auto& gimbal = camera->getGimbal(); + const auto afterPosition = gimbal.getPosition(); + const auto afterOrientation = hlsl::normalizeQuaternion(gimbal.getOrientation()); + return hlsl::tryComputePoseDelta(afterPosition, afterOrientation, beforePosition, beforeOrientation, outDelta); + } + + //! Manipulate a camera and report how far its pose moved in position and Euler-angle terms. + static inline bool tryManipulateCameraAndMeasureDelta( + core::ICamera* camera, + std::span events, + SCameraManipulationDelta& outDelta, + const double tinyEpsilon = SCameraSmokeComparisonThresholds::TinyScalarEpsilon) + { + outDelta = {}; + if (!camera || events.empty()) + return false; + + const auto& beforeGimbal = camera->getGimbal(); + const auto beforePosition = beforeGimbal.getPosition(); + const auto beforeOrientation = hlsl::normalizeQuaternion(beforeGimbal.getOrientation()); + if (!hlsl::isFiniteVec3(beforePosition) || !hlsl::isFiniteQuaternion(beforeOrientation)) + return false; + + if (!camera->manipulate(events)) + return false; + + if (!tryComputeCameraManipulationDelta(camera, beforePosition, beforeOrientation, outDelta)) + return false; + + return outDelta.position > tinyEpsilon || outDelta.rotationDeg > tinyEpsilon; + } + + static inline bool comparePresetToCameraStateWithDefaultThresholds( + const core::CCameraGoalSolver& solver, + core::ICamera* camera, + const core::CCameraPreset& preset) + { + return core::comparePresetToCameraState( + solver, + camera, + preset, + SCameraSmokeComparisonThresholds::DefaultPositionTolerance, + SCameraSmokeComparisonThresholds::DefaultAngularToleranceDeg, + SCameraSmokeComparisonThresholds::DefaultScalarTolerance); + } + + static inline bool comparePresetToCameraStateWithStrictThresholds( + const core::CCameraGoalSolver& solver, + core::ICamera* camera, + const core::CCameraPreset& preset) + { + return core::comparePresetToCameraState( + solver, + camera, + preset, + SCameraSmokeComparisonThresholds::StrictPositionTolerance, + SCameraSmokeComparisonThresholds::StrictAngularToleranceDeg, + SCameraSmokeComparisonThresholds::StrictScalarTolerance); + } + + static inline bool compareKeyframeTrackContentWithStrictThresholds( + const core::CCameraKeyframeTrack& lhs, + const core::CCameraKeyframeTrack& rhs) + { + return core::CCameraKeyframeTrackUtilities::compareKeyframeTrackContent( + lhs, + rhs, + SCameraSmokeComparisonThresholds::TrackTimeTolerance, + SCameraSmokeComparisonThresholds::StrictPositionTolerance, + SCameraSmokeComparisonThresholds::StrictAngularToleranceDeg, + SCameraSmokeComparisonThresholds::StrictScalarTolerance); + } +}; } // namespace nbl::system diff --git a/common/include/camera/CCameraTextUtilities.hpp b/common/include/camera/CCameraTextUtilities.hpp index 878c6a41b..1c64649ad 100644 --- a/common/include/camera/CCameraTextUtilities.hpp +++ b/common/include/camera/CCameraTextUtilities.hpp @@ -22,7 +22,7 @@ struct CCameraTextUtilities final //! Return a short human-readable label for a camera kind. static inline std::string_view getCameraTypeLabel(const core::ICamera::CameraKind kind) { - return core::getCameraKindLabel(kind); + return core::CCameraKindUtilities::getCameraKindLabel(kind); } //! Return a short human-readable label for a concrete camera instance. @@ -34,7 +34,7 @@ struct CCameraTextUtilities final //! Return a short human-readable description for a camera kind. static inline std::string_view getCameraTypeDescription(const core::ICamera::CameraKind kind) { - return core::getCameraKindDescription(kind); + return core::CCameraKindUtilities::getCameraKindDescription(kind); } //! Return a short human-readable description for a concrete camera instance. diff --git a/common/src/nbl/examples/camera/CCameraPersistence.cpp b/common/src/nbl/examples/camera/CCameraPersistence.cpp index 0f0584bed..2709b3bb0 100644 --- a/common/src/nbl/examples/camera/CCameraPersistence.cpp +++ b/common/src/nbl/examples/camera/CCameraPersistence.cpp @@ -91,8 +91,8 @@ bool deserializeKeyframeTrackJson(const json_t& root, nbl::core::CCameraKeyframe track.keyframes.emplace_back(std::move(keyframe)); } - nbl::core::sortKeyframeTrackByTime(track); - nbl::core::normalizeSelectedKeyframeTrack(track); + nbl::core::CCameraKeyframeTrackUtilities::sortKeyframeTrackByTime(track); + nbl::core::CCameraKeyframeTrackUtilities::normalizeSelectedKeyframeTrack(track); return true; } @@ -152,13 +152,13 @@ bool saveGoalToFile(ISystem& system, const path& filePath, const core::CCameraGo std::ostringstream out; if (!writeGoal(out, goal, indent)) return false; - return writeTextFile(system, filePath, out.str()); + return CCameraFileUtilities::writeTextFile(system, filePath, out.str()); } bool loadGoalFromFile(ISystem& system, const path& filePath, core::CCameraGoal& goal) { std::string text; - if (!readTextFile(system, filePath, text)) + if (!CCameraFileUtilities::readTextFile(system, filePath, text)) return false; std::istringstream in(text); @@ -190,13 +190,13 @@ bool savePresetToFile(ISystem& system, const path& filePath, const core::CCamera std::ostringstream out; if (!writePreset(out, preset, indent)) return false; - return writeTextFile(system, filePath, out.str()); + return CCameraFileUtilities::writeTextFile(system, filePath, out.str()); } bool loadPresetFromFile(ISystem& system, const path& filePath, core::CCameraPreset& preset) { std::string text; - if (!readTextFile(system, filePath, text)) + if (!CCameraFileUtilities::readTextFile(system, filePath, text)) return false; std::istringstream in(text); @@ -227,13 +227,13 @@ bool saveKeyframeTrackToFile(ISystem& system, const path& filePath, const core:: std::ostringstream out; if (!writeKeyframeTrack(out, track, indent)) return false; - return writeTextFile(system, filePath, out.str()); + return CCameraFileUtilities::writeTextFile(system, filePath, out.str()); } bool loadKeyframeTrackFromFile(ISystem& system, const path& filePath, core::CCameraKeyframeTrack& track) { std::string text; - if (!readTextFile(system, filePath, text)) + if (!CCameraFileUtilities::readTextFile(system, filePath, text)) return false; std::istringstream in(text); @@ -264,13 +264,13 @@ bool savePresetCollectionToFile(ISystem& system, const path& filePath, std::span std::ostringstream out; if (!writePresetCollection(out, presets, indent)) return false; - return writeTextFile(system, filePath, out.str()); + return CCameraFileUtilities::writeTextFile(system, filePath, out.str()); } bool loadPresetCollectionFromFile(ISystem& system, const path& filePath, std::vector& presets) { std::string text; - if (!readTextFile(system, filePath, text)) + if (!CCameraFileUtilities::readTextFile(system, filePath, text)) return false; std::istringstream in(text); diff --git a/common/src/nbl/examples/camera/CCameraScriptedRuntimePersistence.cpp b/common/src/nbl/examples/camera/CCameraScriptedRuntimePersistence.cpp index b4d82b9f4..c66308ee2 100644 --- a/common/src/nbl/examples/camera/CCameraScriptedRuntimePersistence.cpp +++ b/common/src/nbl/examples/camera/CCameraScriptedRuntimePersistence.cpp @@ -1078,7 +1078,7 @@ bool readCameraSequenceScript(std::string_view text, core::CCameraSequenceScript bool loadCameraSequenceScriptFromFile(ISystem& system, const path& filePath, core::CCameraSequenceScript& out, std::string* error) { std::string text; - if (!readTextFile(system, filePath, text, error, "Cannot open camera sequence script file.")) + if (!CCameraFileUtilities::readTextFile(system, filePath, text, error, "Cannot open camera sequence script file.")) return false; return readCameraSequenceScript(text, out, error); @@ -1151,7 +1151,7 @@ bool readCameraScriptedInput(std::string_view text, CCameraScriptedInputParseRes bool loadCameraScriptedInputFromFile(ISystem& system, const path& filePath, CCameraScriptedInputParseResult& out, std::string* error) { std::string text; - if (!readTextFile(system, filePath, text, error, "Cannot open scripted input file.")) + if (!CCameraFileUtilities::readTextFile(system, filePath, text, error, "Cannot open scripted input file.")) return false; return readCameraScriptedInput(text, out, error); From d75344d43d077c20f45f272b4b9ab0e45de5f3e4 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Thu, 9 Apr 2026 11:33:56 +0200 Subject: [PATCH 185/205] Refactor camera preset and scripted utilities --- 61_UI/AppCameraPlanarRuntime.cpp | 2 +- 61_UI/AppControlPanelPlaybackTab.cpp | 2 +- 61_UI/AppControlPanelPresetsTab.cpp | 8 +- 61_UI/AppFollowRuntime.cpp | 2 +- 61_UI/AppHeadlessCameraSmokeChecks.inl | 56 ++-- 61_UI/AppHeadlessCameraSmokeHelpers.inl | 56 ++-- 61_UI/AppPresetPlayback.cpp | 12 +- 61_UI/AppScriptedInitialization.cpp | 8 +- 61_UI/AppScriptedInputRuntime.cpp | 8 +- 61_UI/AppScriptedValidation.cpp | 2 +- .../app/AppViewportBindingUtilities.hpp | 2 +- 61_UI/include/common.hpp | 2 +- common/include/camera/CArcballCamera.hpp | 8 +- common/include/camera/CCameraGoalAnalysis.hpp | 2 +- .../include/camera/CCameraKeyframeTrack.hpp | 9 +- .../camera/CCameraManipulationUtilities.hpp | 2 +- .../camera/CCameraPresentationUtilities.hpp | 105 +++--- common/include/camera/CCameraPreset.hpp | 57 ++-- common/include/camera/CCameraPresetFlow.hpp | 170 +++++----- .../camera/CCameraProjectionUtilities.hpp | 35 +- .../camera/CCameraScriptedCheckRunner.hpp | 197 +++++------ .../include/camera/CCameraScriptedRuntime.hpp | 306 +++++++++--------- .../CCameraScriptedUiInputUtilities.hpp | 131 ++++---- .../include/camera/CCameraSequenceScript.hpp | 6 +- .../camera/CCameraSequenceScriptedBuilder.hpp | 142 ++++---- .../CCameraSmokeRegressionUtilities.hpp | 4 +- common/include/camera/CChaseCamera.hpp | 8 +- common/include/camera/CDollyCamera.hpp | 8 +- common/include/camera/CDollyZoomCamera.hpp | 3 +- common/include/camera/CIsometricCamera.hpp | 8 +- common/include/camera/COrbitCamera.hpp | 3 +- common/include/camera/CPathCamera.hpp | 3 +- .../include/camera/CSphericalTargetCamera.hpp | 24 +- common/include/camera/CTopDownCamera.hpp | 8 +- common/include/camera/CTurntableCamera.hpp | 6 +- common/include/camera/README.md | 10 +- .../CCameraJsonPersistenceUtilities.hpp | 2 +- .../examples/camera/CCameraPersistence.cpp | 2 +- .../CCameraScriptedRuntimePersistence.cpp | 2 +- 39 files changed, 726 insertions(+), 695 deletions(-) diff --git a/61_UI/AppCameraPlanarRuntime.cpp b/61_UI/AppCameraPlanarRuntime.cpp index e0e42f80f..4f75e9317 100644 --- a/61_UI/AppCameraPlanarRuntime.cpp +++ b/61_UI/AppCameraPlanarRuntime.cpp @@ -58,7 +58,7 @@ bool tryCaptureInitialPlanarPresets( } core::CCameraPreset preset = {}; - if (!core::tryCapturePreset(captureAnalysis, camera, presetName, preset)) + if (!core::CCameraPresetFlowUtilities::tryCapturePreset(captureAnalysis, camera, presetName, preset)) { outError = "Failed to build initial planar preset " + std::to_string(planarIx) + diff --git a/61_UI/AppControlPanelPlaybackTab.cpp b/61_UI/AppControlPanelPlaybackTab.cpp index 8b2105549..3b3de2ee5 100644 --- a/61_UI/AppControlPanelPlaybackTab.cpp +++ b/61_UI/AppControlPanelPlaybackTab.cpp @@ -86,7 +86,7 @@ void App::drawControlPanelPlaybackTab(const nbl::ui::SCameraControlPanelStyle& p const float authoredTime = std::max(0.f, playbackAuthoring.newKeyframeTime); keyframe.time = authoredTime; playbackAuthoring.newKeyframeTime = authoredTime; - if (nbl::core::tryCapturePreset(m_cameraGoalSolver, activeCamera, "Keyframe", keyframe.preset)) + if (nbl::core::CCameraPresetFlowUtilities::tryCapturePreset(m_cameraGoalSolver, activeCamera, "Keyframe", keyframe.preset)) { playbackAuthoring.keyframeTrack.keyframes.emplace_back(std::move(keyframe)); sortKeyframesByTime(); diff --git a/61_UI/AppControlPanelPresetsTab.cpp b/61_UI/AppControlPanelPresetsTab.cpp index c1c3a56e2..fe229d90f 100644 --- a/61_UI/AppControlPanelPresetsTab.cpp +++ b/61_UI/AppControlPanelPresetsTab.cpp @@ -25,7 +25,7 @@ void App::drawControlPanelPresetsTab(const nbl::ui::SCameraControlPanelStyle& pa if (nbl::ui::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::tryCapturePreset(m_cameraGoalSolver, activeCamera, presetAuthoring.presetName, 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; @@ -48,9 +48,9 @@ void App::drawControlPanelPresetsTab(const nbl::ui::SCameraControlPanelStyle& pa if (!presetAuthoring.presets.empty()) { const char* presetFilterLabels[] = { - nbl::ui::getPresetApplyPresentationFilterLabel(PresetFilterMode::All), - nbl::ui::getPresetApplyPresentationFilterLabel(PresetFilterMode::Exact), - nbl::ui::getPresetApplyPresentationFilterLabel(PresetFilterMode::BestEffort) + 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))) diff --git a/61_UI/AppFollowRuntime.cpp b/61_UI/AppFollowRuntime.cpp index b8e6f4d64..26082917c 100644 --- a/61_UI/AppFollowRuntime.cpp +++ b/61_UI/AppFollowRuntime.cpp @@ -182,7 +182,7 @@ void App::applyFollowToConfiguredCameras(const bool allowDuringScriptedInput) continue; for (auto& projection : planar->getPlanarProjections()) - nbl::core::syncDynamicPerspectiveProjection(camera, projection); + nbl::core::CCameraProjectionUtilities::syncDynamicPerspectiveProjection(camera, projection); } } diff --git a/61_UI/AppHeadlessCameraSmokeChecks.inl b/61_UI/AppHeadlessCameraSmokeChecks.inl index 0526512e4..e0462b55a 100644 --- a/61_UI/AppHeadlessCameraSmokeChecks.inl +++ b/61_UI/AppHeadlessCameraSmokeChecks.inl @@ -61,15 +61,15 @@ if (!state.initialPresets.orbit.has_value()) return true; - if (std::string_view(nbl::ui::getPresetApplyPresentationFilterLabel(EPresetApplyPresentationFilter::All)) != "All" || - std::string_view(nbl::ui::getPresetApplyPresentationFilterLabel(EPresetApplyPresentationFilter::Exact)) != "Exact" || - std::string_view(nbl::ui::getPresetApplyPresentationFilterLabel(EPresetApplyPresentationFilter::BestEffort)) != "Best-effort") + 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::analyzePresetPresentation(state.goalSolver, nullptr, state.initialPresets.orbit.value()); + const auto blockedPresentation = nbl::ui::CCameraPresentationUtilities::analyzePresetPresentation(state.goalSolver, nullptr, state.initialPresets.orbit.value()); if (blockedPresentation.matchesFilter(EPresetApplyPresentationFilter::Exact) || blockedPresentation.matchesFilter(EPresetApplyPresentationFilter::BestEffort)) { @@ -82,7 +82,7 @@ return false; } - const auto blockedBadges = nbl::ui::collectGoalApplyPresentationBadges(blockedPresentation); + 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."; @@ -91,7 +91,7 @@ if (state.orbitCamera) { - const auto exactPresentation = nbl::ui::analyzePresetPresentation(state.goalSolver, state.orbitCamera, state.initialPresets.orbit.value()); + 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)) @@ -100,7 +100,7 @@ return false; } - const auto exactBadges = nbl::ui::collectGoalApplyPresentationBadges(exactPresentation); + 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."; @@ -112,7 +112,7 @@ return false; } - const auto capturePresentation = nbl::ui::analyzeCapturePresentation(state.goalSolver, state.orbitCamera); + 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."; @@ -122,7 +122,7 @@ if (state.initialPresets.path.has_value() && state.orbitCamera) { - const auto approximatePresentation = nbl::ui::analyzePresetPresentation(state.goalSolver, state.orbitCamera, state.initialPresets.path.value()); + 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)) @@ -131,7 +131,7 @@ return false; } - const auto approximateBadges = nbl::ui::collectGoalApplyPresentationBadges(approximatePresentation); + 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."; @@ -190,7 +190,7 @@ outError = "Preset persistence smoke failed to deserialize preset collection."; return false; } - if (!nbl::core::comparePresetCollections( + if (!nbl::core::CCameraPresetUtilities::comparePresetCollections( sourcePresetSpan, std::span(loadedPresets.data(), loadedPresets.size()), SCameraSmokePersistenceThresholds::PositionTolerance, @@ -269,7 +269,7 @@ outError = "Preset persistence smoke failed to load preset collection file."; return false; } - if (!nbl::core::comparePresetCollections( + if (!nbl::core::CCameraPresetUtilities::comparePresetCollections( sourcePresetSpan, std::span(fileLoadedPresets.data(), fileLoadedPresets.size()), SCameraSmokePersistenceThresholds::PositionTolerance, @@ -497,7 +497,7 @@ CCameraScriptedTimeline scriptedTimeline; std::string runtimeBuildError; - if (!nbl::system::appendCompiledSequenceSegmentToScriptedTimeline( + if (!nbl::system::CCameraSequenceScriptedBuilderUtilities::appendCompiledSequenceSegmentToScriptedTimeline( scriptedTimeline, SCameraSmokeSequenceDefaults::StartFrame, compiledSegment, @@ -512,7 +512,7 @@ outError = "Sequence runtime builder smoke failed to append a compiled segment. " + runtimeBuildError; return false; } - nbl::system::finalizeScriptedTimeline(scriptedTimeline); + nbl::system::CCameraScriptedRuntimeUtilities::finalizeScriptedTimeline(scriptedTimeline); if (scriptedTimeline.captureFrames != std::vector( SCameraSmokeSequenceDefaults::CaptureFrames.begin(), @@ -552,7 +552,7 @@ size_t runtimeNextEventIndex = 0u; CCameraScriptedFrameEvents runtimeBatch; - nbl::system::dequeueScriptedFrameEvents(scriptedTimeline.events, runtimeNextEventIndex, SCameraSmokeSequenceDefaults::StartFrame, runtimeBatch); + nbl::system::CCameraScriptedFrameEventUtilities::dequeueScriptedFrameEvents(scriptedTimeline.events, runtimeNextEventIndex, SCameraSmokeSequenceDefaults::StartFrame, runtimeBatch); if (runtimeBatch.actions.size() != 10u || runtimeBatch.goals.size() != 1u || runtimeBatch.trackedTargetTransforms.size() != 1u || runtimeBatch.segmentLabels.size() != 1u) { @@ -576,7 +576,7 @@ if (state.initialPresets.orbit.has_value() && state.orbitCamera) { std::array exactTargets = { state.orbitCamera, nullptr }; - const auto exactSummary = nbl::core::applyPresetToCameraRange( + const auto exactSummary = nbl::core::CCameraPresetFlowUtilities::applyPresetToCameraRange( state.goalSolver, std::span(exactTargets.data(), exactTargets.size()), state.initialPresets.orbit.value()); @@ -590,7 +590,7 @@ if (state.initialPresets.path.has_value() && state.orbitCamera) { std::array approximateTargets = { state.orbitCamera }; - const auto approximateSummary = nbl::core::applyPresetToCameraRange( + const auto approximateSummary = nbl::core::CCameraPresetFlowUtilities::applyPresetToCameraRange( state.goalSolver, std::span(approximateTargets.data(), approximateTargets.size()), state.initialPresets.path.value()); @@ -623,7 +623,7 @@ { CameraPreset orientedPreset = state.initialPresets.free.value(); orientedPreset.goal.orientation = hlsl::makeQuaternionFromEulerDegreesYXZ(SCameraSmokeManipulationDefaults::FreeOrientationYawDeg); - const auto orientResult = nbl::core::applyPresetDetailed(state.goalSolver, state.freeCamera, orientedPreset); + 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."; @@ -661,7 +661,7 @@ CameraPreset pitchPreset = state.initialPresets.free.value(); pitchPreset.goal.orientation = hlsl::makeQuaternionFromEulerDegreesYXZ(SCameraSmokeManipulationDefaults::FreePitchClampSourceDeg); - const auto pitchResult = nbl::core::applyPresetDetailed(state.goalSolver, state.freeCamera, pitchPreset); + 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."; @@ -687,7 +687,7 @@ return false; } - const auto restoreFree = nbl::core::applyPresetDetailed(state.goalSolver, state.freeCamera, state.initialPresets.free.value()); + 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."; @@ -699,7 +699,7 @@ { CameraPreset farOrbitPreset = state.initialPresets.orbit.value(); farOrbitPreset.goal.distance = state.initialPresets.orbit->goal.distance + SCameraSmokeManipulationDefaults::OrbitDistanceDelta; - const auto farOrbitResult = nbl::core::applyPresetDetailed(state.goalSolver, state.orbitCamera, farOrbitPreset); + 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."; @@ -728,7 +728,7 @@ return false; } - const auto restoreOrbit = nbl::core::applyPresetDetailed(state.goalSolver, state.orbitCamera, state.initialPresets.orbit.value()); + 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."; @@ -749,7 +749,7 @@ SCameraSmokeManipulationDefaults::PerspectiveNearPlane, SCameraSmokeManipulationDefaults::PerspectiveFarPlane, SCameraSmokeManipulationDefaults::PerspectiveFovDeg); - if (!nbl::core::syncDynamicPerspectiveProjection(state.dollyZoomCamera, perspectiveProjection)) + if (!nbl::core::CCameraProjectionUtilities::syncDynamicPerspectiveProjection(state.dollyZoomCamera, perspectiveProjection)) { outError = "Camera projection utilities smoke failed to sync dynamic perspective projection."; return false; @@ -764,7 +764,7 @@ SCameraSmokeManipulationDefaults::PerspectiveNearPlane, SCameraSmokeManipulationDefaults::PerspectiveFarPlane, SCameraSmokeManipulationDefaults::OrthoExtent); - if (nbl::core::syncDynamicPerspectiveProjection(state.dollyZoomCamera, orthographicProjection)) + if (nbl::core::CCameraProjectionUtilities::syncDynamicPerspectiveProjection(state.dollyZoomCamera, orthographicProjection)) { outError = "Camera projection utilities smoke unexpectedly synced orthographic projection."; return false; @@ -834,7 +834,7 @@ if (state.orbitCamera) { - const auto baselinePreset = nbl::core::capturePreset(state.goalSolver, state.orbitCamera, "orbit-follow-baseline"); + const auto baselinePreset = nbl::core::CCameraPresetFlowUtilities::capturePreset(state.goalSolver, state.orbitCamera, "orbit-follow-baseline"); SCameraFollowConfig followConfig = {}; followConfig.enabled = true; followConfig.mode = ECameraFollowMode::OrbitTarget; @@ -868,7 +868,7 @@ continue; const auto label = std::string(defaultFollowCamera->getIdentifier()) + " default follow"; - const auto baselinePreset = nbl::core::capturePreset(state.goalSolver, defaultFollowCamera, label + " baseline"); + const auto baselinePreset = nbl::core::CCameraPresetFlowUtilities::capturePreset(state.goalSolver, defaultFollowCamera, label + " baseline"); trackedTarget.setPose( SCameraSmokeFollowScenario::InitialTargetPosition, @@ -900,7 +900,7 @@ if (state.freeCamera) { - const auto baselinePreset = nbl::core::capturePreset(state.goalSolver, state.freeCamera, "free-follow-baseline"); + const auto baselinePreset = nbl::core::CCameraPresetFlowUtilities::capturePreset(state.goalSolver, state.freeCamera, "free-follow-baseline"); SCameraFollowConfig followConfig = {}; followConfig.enabled = true; followConfig.mode = ECameraFollowMode::LookAtTarget; @@ -925,7 +925,7 @@ if (state.chaseCamera) { - const auto baselinePreset = nbl::core::capturePreset(state.goalSolver, state.chaseCamera, "chase-follow-baseline"); + const auto baselinePreset = nbl::core::CCameraPresetFlowUtilities::capturePreset(state.goalSolver, state.chaseCamera, "chase-follow-baseline"); SCameraFollowConfig followConfig = {}; followConfig.enabled = true; followConfig.mode = ECameraFollowMode::KeepLocalOffset; diff --git a/61_UI/AppHeadlessCameraSmokeHelpers.inl b/61_UI/AppHeadlessCameraSmokeHelpers.inl index ea360e6a4..dd73c1940 100644 --- a/61_UI/AppHeadlessCameraSmokeHelpers.inl +++ b/61_UI/AppHeadlessCameraSmokeHelpers.inl @@ -401,7 +401,7 @@ namespace ICamera* const camera, const CameraPreset& preset) { - return std::string(prefix) + " " + nbl::core::describePresetCameraMismatch(goalSolver, camera, preset); + return std::string(prefix) + " " + nbl::core::CCameraPresetFlowUtilities::describePresetCameraMismatch(goalSolver, camera, preset); } inline bool applyPresetAndValidate( @@ -414,7 +414,7 @@ namespace const std::string_view failurePrefix, std::string& outError) { - const auto applyResult = nbl::core::applyPresetDetailed(goalSolver, camera, preset); + const auto applyResult = nbl::core::CCameraPresetFlowUtilities::applyPresetDetailed(goalSolver, camera, preset); if (!applyResult.succeeded() || (requireChanged && !applyResult.changed()) || (requireExact && !applyResult.exact)) @@ -439,13 +439,13 @@ namespace const std::string_view failurePrefix, std::string& outError) { - const auto restoreResult = nbl::core::applyPresetDetailed(goalSolver, camera, preset); + 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::describePresetCameraMismatch(goalSolver, camera, preset); + outError += " " + nbl::core::CCameraPresetFlowUtilities::describePresetCameraMismatch(goalSolver, camera, preset); return false; } @@ -606,7 +606,7 @@ namespace CCameraInputBindingUtilities::applyDefaultCameraInputBindingPreset(inputBinder, *camera); const std::string cameraIdentifier(camera->getIdentifier()); - const auto initialPreset = nbl::core::capturePreset(goalSolver, camera, "smoke-initial"); + 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) { @@ -617,7 +617,7 @@ namespace storeInitialPresetForKind(camera->getKind(), initialPreset, initialPresets); - if (!nbl::core::applyPreset(goalSolver, camera, initialPreset)) + if (!nbl::core::CCameraPresetFlowUtilities::applyPreset(goalSolver, camera, initialPreset)) { outError = "Preset no-op smoke failed for camera \"" + cameraIdentifier + "\"."; return false; @@ -737,7 +737,7 @@ namespace } { - const auto modifiedPreset = nbl::core::capturePreset(goalSolver, camera, "smoke-direct"); + const auto modifiedPreset = nbl::core::CCameraPresetFlowUtilities::capturePreset(goalSolver, camera, "smoke-direct"); if (!applyPresetAndValidate( goalSolver, camera, @@ -886,8 +886,8 @@ namespace return false; } - const auto baselinePreset = nbl::core::capturePreset(goalSolver, targetCamera, std::string(label) + "-baseline"); - const auto applyResult = nbl::core::applyPresetDetailed(goalSolver, targetCamera, sourcePreset); + 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); @@ -923,7 +923,7 @@ namespace return false; } - const auto baselinePreset = nbl::core::capturePreset(goalSolver, targetCamera, std::string(label) + "-baseline"); + const auto baselinePreset = nbl::core::CCameraPresetFlowUtilities::capturePreset(goalSolver, targetCamera, std::string(label) + "-baseline"); if (!applyPresetAndValidate( goalSolver, targetCamera, @@ -1069,7 +1069,7 @@ namespace if (!camera) return true; - const auto baselinePreset = nbl::core::capturePreset(goalSolver, camera, std::string(label) + " baseline"); + const auto baselinePreset = nbl::core::CCameraPresetFlowUtilities::capturePreset(goalSolver, camera, std::string(label) + " baseline"); SCameraFollowConfig followConfig = {}; followConfig.enabled = true; followConfig.mode = ECameraFollowMode::KeepLocalOffset; @@ -1087,7 +1087,7 @@ namespace return false; } - auto editedPreset = nbl::core::capturePreset(goalSolver, camera, std::string(label) + " edited"); + 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) + "."; @@ -1107,7 +1107,7 @@ namespace return false; } - const auto editedApply = nbl::core::applyPresetDetailed(goalSolver, camera, editedPreset); + 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) + @@ -1115,7 +1115,7 @@ namespace return false; } - const auto reachedEditedPreset = nbl::core::capturePreset(goalSolver, camera, std::string(label) + " reached"); + const auto reachedEditedPreset = nbl::core::CCameraPresetFlowUtilities::capturePreset(goalSolver, camera, std::string(label) + " reached"); if (!nbl::core::CCameraFollowUtilities::captureFollowOffsetsFromCamera(goalSolver, camera, trackedTarget, followConfig)) { @@ -1141,7 +1141,7 @@ namespace if (!nbl::system::CCameraSmokeRegressionUtilities::comparePresetToCameraStateWithStrictThresholds(goalSolver, camera, reachedEditedPreset)) { outError = std::string("Follow recapture smoke mismatch for ") + std::string(label) + ". " + - nbl::core::describePresetCameraMismatch(goalSolver, camera, reachedEditedPreset); + nbl::core::CCameraPresetFlowUtilities::describePresetCameraMismatch(goalSolver, camera, reachedEditedPreset); return false; } @@ -1159,7 +1159,7 @@ namespace inline bool verifyScriptedRuntimeFrameBatch(std::string* const outError) { CCameraScriptedTimeline timeline = {}; - nbl::system::appendScriptedActionEvent( + nbl::system::CCameraScriptedRuntimeUtilities::appendScriptedActionEvent( timeline, SCameraSmokeRuntimeDefaults::ActionFrame, CCameraScriptedInputEvent::ActionData::Kind::SetActivePlanar, @@ -1167,21 +1167,21 @@ namespace { CCameraGoal goal = {}; goal.position = SCameraSmokeRuntimeDefaults::GoalPosition; - nbl::system::appendScriptedGoalEvent(timeline, SCameraSmokeRuntimeDefaults::ActionFrame, goal, true); + nbl::system::CCameraScriptedRuntimeUtilities::appendScriptedGoalEvent(timeline, SCameraSmokeRuntimeDefaults::ActionFrame, goal, true); } - nbl::system::appendScriptedSegmentLabelEvent( + 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::appendScriptedTrackedTargetTransformEvent(timeline, SCameraSmokeRuntimeDefaults::FollowFrame, transform); + nbl::system::CCameraScriptedRuntimeUtilities::appendScriptedTrackedTargetTransformEvent(timeline, SCameraSmokeRuntimeDefaults::FollowFrame, transform); } size_t nextEventIndex = 0u; CCameraScriptedFrameEvents batch; - nbl::system::dequeueScriptedFrameEvents(timeline.events, nextEventIndex, SCameraSmokeRuntimeDefaults::ActionFrame, batch); + nbl::system::CCameraScriptedFrameEventUtilities::dequeueScriptedFrameEvents(timeline.events, nextEventIndex, SCameraSmokeRuntimeDefaults::ActionFrame, batch); if (nextEventIndex != 3u || batch.actions.size() != 1u || batch.goals.size() != 1u || batch.segmentLabels.size() != 1u || !batch.mouse.empty() || !batch.keyboard.empty()) { @@ -1198,7 +1198,7 @@ namespace return false; } - nbl::system::dequeueScriptedFrameEvents(timeline.events, nextEventIndex, SCameraSmokeRuntimeDefaults::FollowFrame, batch); + nbl::system::CCameraScriptedFrameEventUtilities::dequeueScriptedFrameEvents(timeline.events, nextEventIndex, SCameraSmokeRuntimeDefaults::FollowFrame, batch); if (nextEventIndex != timeline.events.size() || batch.trackedTargetTransforms.size() != 1u || !batch.actions.empty() || !batch.goals.empty()) { @@ -1252,7 +1252,7 @@ namespace size_t nextEventIndex = 0u; CCameraScriptedFrameEvents batch; - nbl::system::dequeueScriptedFrameEvents(parsed.timeline.events, nextEventIndex, SCameraSmokeRuntimeParserDefaults::EventFrame, batch); + nbl::system::CCameraScriptedFrameEventUtilities::dequeueScriptedFrameEvents(parsed.timeline.events, nextEventIndex, SCameraSmokeRuntimeParserDefaults::EventFrame, batch); if (batch.actions.size() != 1u || batch.keyboard.size() != 1u || batch.actions.front().value != SCameraSmokeRuntimeParserDefaults::ActivePlanarValue) @@ -1282,8 +1282,8 @@ namespace SCameraSmokeScriptedCheckDefaults::InitialTrackedTargetOrientation); CCameraScriptedTimeline timeline = {}; - nbl::system::appendScriptedBaselineCheck(timeline, SCameraSmokeScriptedCheckDefaults::BaselineFrame); - nbl::system::appendScriptedGimbalStepCheck( + nbl::system::CCameraScriptedRuntimeUtilities::appendScriptedBaselineCheck(timeline, SCameraSmokeScriptedCheckDefaults::BaselineFrame); + nbl::system::CCameraScriptedRuntimeUtilities::appendScriptedGimbalStepCheck( timeline, SCameraSmokeScriptedCheckDefaults::StepFrame, true, @@ -1292,7 +1292,7 @@ namespace true, SCameraSmokeScriptedCheckDefaults::AngularToleranceDeg, SCameraSmokeScriptedCheckDefaults::MinAngularDeltaDeg); - nbl::system::appendScriptedFollowTargetLockCheck( + nbl::system::CCameraScriptedRuntimeUtilities::appendScriptedFollowTargetLockCheck( timeline, SCameraSmokeScriptedCheckDefaults::FollowLockFrame, CameraFollowRegressionThresholds.lockAngleToleranceDeg, @@ -1300,7 +1300,7 @@ namespace CCameraScriptedCheckRuntimeState state = {}; { - const auto frameResult = evaluateScriptedChecksForFrame( + const auto frameResult = nbl::system::CCameraScriptedCheckRunnerUtilities::evaluateScriptedChecksForFrame( timeline.checks, state, { @@ -1345,7 +1345,7 @@ namespace return false; } - const auto frameResult = evaluateScriptedChecksForFrame( + const auto frameResult = nbl::system::CCameraScriptedCheckRunnerUtilities::evaluateScriptedChecksForFrame( timeline.checks, state, { @@ -1373,7 +1373,7 @@ namespace } { - const auto frameResult = evaluateScriptedChecksForFrame( + const auto frameResult = nbl::system::CCameraScriptedCheckRunnerUtilities::evaluateScriptedChecksForFrame( timeline.checks, state, { diff --git a/61_UI/AppPresetPlayback.cpp b/61_UI/AppPresetPlayback.cpp index 96d2521d5..141c9044b 100644 --- a/61_UI/AppPresetPlayback.cpp +++ b/61_UI/AppPresetPlayback.cpp @@ -11,12 +11,12 @@ bool App::tryCaptureGoal(ICamera* camera, CCameraGoal& out) const App::PresetUiAnalysis App::analyzePresetForUi(ICamera* camera, const CameraPreset& preset) const { - return nbl::ui::analyzePresetPresentation(m_cameraGoalSolver, camera, preset); + return nbl::ui::CCameraPresentationUtilities::analyzePresetPresentation(m_cameraGoalSolver, camera, preset); } App::CaptureUiAnalysis App::analyzeCameraCaptureForUi(ICamera* camera) const { - return nbl::ui::analyzeCapturePresentation(m_cameraGoalSolver, camera); + return nbl::ui::CCameraPresentationUtilities::analyzeCapturePresentation(m_cameraGoalSolver, camera); } CCameraGoalSolver::SCompatibilityResult App::analyzePresetCompatibility(ICamera* camera, const CameraPreset& preset) const @@ -31,7 +31,7 @@ bool App::presetMatchesFilter(ICamera* camera, const CameraPreset& preset) const CCameraGoalSolver::SApplyResult App::applyPresetFromUi(ICamera* camera, const CameraPreset& preset) { - const auto result = nbl::core::applyPresetDetailed(m_cameraGoalSolver, camera, preset); + const auto result = nbl::core::CCameraPresetFlowUtilities::applyPresetDetailed(m_cameraGoalSolver, camera, preset); if (result.succeeded()) refreshFollowOffsetConfigsForCamera(camera); @@ -118,7 +118,7 @@ SCameraPresetApplySummary App::applyPresetToTargets(const CameraPreset& preset) if (!playbackAuthoring.affectsAll) { ICamera* activeCamera = getActiveCamera(); - summary = nbl::core::applyPresetToCameraRange( + summary = nbl::core::CCameraPresetFlowUtilities::applyPresetToCameraRange( m_cameraGoalSolver, std::span(&activeCamera, activeCamera ? 1u : 0u), preset); @@ -144,7 +144,7 @@ SCameraPresetApplySummary App::applyPresetToTargets(const CameraPreset& preset) cameras.push_back(camera); } - summary = nbl::core::applyPresetToCameraRange( + summary = nbl::core::CCameraPresetFlowUtilities::applyPresetToCameraRange( m_cameraGoalSolver, std::span(cameras.data(), cameras.size()), preset); @@ -209,7 +209,7 @@ bool App::replaceSelectedKeyframeFromCamera(ICamera* camera) CameraPreset updatedPreset; const auto keyframeName = selected->preset.name.empty() ? std::string("Keyframe") : selected->preset.name; - if (!nbl::core::tryCapturePreset(m_cameraGoalSolver, camera, keyframeName, updatedPreset)) + if (!nbl::core::CCameraPresetFlowUtilities::tryCapturePreset(m_cameraGoalSolver, camera, keyframeName, updatedPreset)) return false; return nbl::core::CCameraKeyframeTrackUtilities::replaceSelectedKeyframePreset(m_playbackAuthoring.keyframeTrack, std::move(updatedPreset)); diff --git a/61_UI/AppScriptedInitialization.cpp b/61_UI/AppScriptedInitialization.cpp index 150f6b200..7df78bd02 100644 --- a/61_UI/AppScriptedInitialization.cpp +++ b/61_UI/AppScriptedInitialization.cpp @@ -15,7 +15,7 @@ void App::resetScriptedInputRuntimeState() void App::finalizeScriptedInputRuntimeState() { - nbl::system::finalizeScriptedTimeline(m_scriptedInput.timeline, m_cliRuntime.disableScreenshotsCli); + nbl::system::CCameraScriptedRuntimeUtilities::finalizeScriptedTimeline(m_scriptedInput.timeline, m_cliRuntime.disableScreenshotsCli); } void App::applyParsedScriptedInput( @@ -153,7 +153,7 @@ bool App::expandPendingScriptedSequence(const CCameraSequenceScript& sequence) resetScriptedInputRuntimeState(); const bool useWindowMode = nbl::core::CCameraSequenceScriptUtilities::sequenceScriptUsesMultiplePresentations(sequence); - nbl::system::appendScriptedActionEvent( + nbl::system::CCameraScriptedRuntimeUtilities::appendScriptedActionEvent( timeline, 0u, CCameraScriptedInputEvent::ActionData::Kind::SetUseWindow, @@ -208,7 +208,7 @@ bool App::expandPendingScriptedSequence(const CCameraSequenceScript& sequence) } std::string buildError; - if (!nbl::system::appendCompiledSequenceSegmentToScriptedTimeline( + if (!nbl::system::CCameraSequenceScriptedBuilderUtilities::appendCompiledSequenceSegmentToScriptedTimeline( timeline, frameCursor, compiledSegment, @@ -229,7 +229,7 @@ bool App::expandPendingScriptedSequence(const CCameraSequenceScript& sequence) frameCursor += compiledSegment.durationFrames; } - nbl::system::finalizeScriptedTimeline(timeline, m_cliRuntime.disableScreenshotsCli); + nbl::system::CCameraScriptedRuntimeUtilities::finalizeScriptedTimeline(timeline, m_cliRuntime.disableScreenshotsCli); m_scriptedInput.timeline = std::move(timeline); return true; } diff --git a/61_UI/AppScriptedInputRuntime.cpp b/61_UI/AppScriptedInputRuntime.cpp index 50d379786..c8411ea77 100644 --- a/61_UI/AppScriptedInputRuntime.cpp +++ b/61_UI/AppScriptedInputRuntime.cpp @@ -26,14 +26,14 @@ void App::dequeueScriptedFrameInput(SScriptedFrameInputState& outFrame) if (m_scriptedInput.enabled && m_scriptedInput.nextEventIndex < m_scriptedInput.timeline.events.size()) { - nbl::system::dequeueScriptedFrameEvents( + nbl::system::CCameraScriptedFrameEventUtilities::dequeueScriptedFrameEvents( m_scriptedInput.timeline.events, m_scriptedInput.nextEventIndex, m_realFrameIx, outFrame.frameEvents); } - nbl::ui::appendScriptedUiInputEvents( + nbl::ui::CCameraScriptedUiInputUtilities::appendScriptedUiInputEvents( m_nextPresentationTimestamp, m_window.get(), outFrame.frameEvents.keyboard, @@ -138,7 +138,7 @@ void App::applyScriptedFrameActions(const CCameraScriptedFrameEvents& scriptedFr } auto* camera = m_planarProjections[binding.activePlanarIx]->getCamera(); - if (!nbl::core::applyPreset(m_cameraGoalSolver, camera, m_presetAuthoring.initialPlanarPresets[binding.activePlanarIx])) + 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; } @@ -225,7 +225,7 @@ void App::syncDynamicPerspectiveForPlanar(planar_projection_t* planar, ICamera* return; for (auto& projection : planar->getPlanarProjections()) - nbl::core::syncDynamicPerspectiveProjection(camera, projection); + nbl::core::CCameraProjectionUtilities::syncDynamicPerspectiveProjection(camera, projection); } void App::logScriptedVirtualEvents(const char* label, std::span events) const diff --git a/61_UI/AppScriptedValidation.cpp b/61_UI/AppScriptedValidation.cpp index 1f9789ccb..49a45e124 100644 --- a/61_UI/AppScriptedValidation.cpp +++ b/61_UI/AppScriptedValidation.cpp @@ -44,7 +44,7 @@ void App::runActiveFrameScriptedChecks(const SScriptedFrameInputState& scriptedF SActiveScriptedCameraContext runtimeContext = {}; const bool hasRuntimeContext = tryBuildActiveScriptedCameraContext(runtimeContext); - const auto checkResult = nbl::system::evaluateScriptedChecksForFrame( + const auto checkResult = nbl::system::CCameraScriptedCheckRunnerUtilities::evaluateScriptedChecksForFrame( m_scriptedInput.timeline.checks, m_scriptedInput.checkRuntime, { diff --git a/61_UI/include/app/AppViewportBindingUtilities.hpp b/61_UI/include/app/AppViewportBindingUtilities.hpp index ad3eef52c..779c274df 100644 --- a/61_UI/include/app/AppViewportBindingUtilities.hpp +++ b/61_UI/include/app/AppViewportBindingUtilities.hpp @@ -175,7 +175,7 @@ inline bool tryBuildWindowBindingMatrices( return false; auto& projection = projections[projectionIx]; - nbl::core::syncDynamicPerspectiveProjection(camera, projection); + nbl::core::CCameraProjectionUtilities::syncDynamicPerspectiveProjection(camera, projection); projection.update(binding.leftHandedProjection, binding.aspectRatio); outState.camera = camera; diff --git a/61_UI/include/common.hpp b/61_UI/include/common.hpp index aa5ece83d..fd50514c6 100644 --- a/61_UI/include/common.hpp +++ b/61_UI/include/common.hpp @@ -205,6 +205,6 @@ using nbl::hlsl::camera_quaternion_t; using nbl::hlsl::makeIdentityQuaternion; using nbl::hlsl::normalizeQuaternion; using nbl::core::CCameraFollowUtilities; -using nbl::core::syncDynamicPerspectiveProjection; +using nbl::core::CCameraProjectionUtilities; #endif // _NBL_THIS_EXAMPLE_COMMON_H_INCLUDED_ diff --git a/common/include/camera/CArcballCamera.hpp b/common/include/camera/CArcballCamera.hpp index 94b2af05c..4a2c0c17d 100644 --- a/common/include/camera/CArcballCamera.hpp +++ b/common/include/camera/CArcballCamera.hpp @@ -21,7 +21,7 @@ class CArcballCamera final : public CSphericalTargetCamera CArcballCamera(const hlsl::float64_t3& position, const hlsl::float64_t3& target) : base_t(position, target) { - m_v = std::clamp(m_v, MinPitch, MaxPitch); + m_orbitUv.y = std::clamp(m_orbitUv.y, MinPitch, MaxPitch); applyPose(); } ~CArcballCamera() = default; @@ -39,11 +39,11 @@ class CArcballCamera final : public CSphericalTargetCamera const auto deltaTranslation = scaleVirtualTranslation(impulse.dVirtualTranslate); const double deltaDistance = scaleUnscaledVirtualTranslation(impulse.dVirtualTranslate.z); - m_u += deltaRotation.y; - m_v = std::clamp(m_v + deltaRotation.x, MinPitch, MaxPitch); + m_orbitUv.x += deltaRotation.y; + m_orbitUv.y = std::clamp(m_orbitUv.y + deltaRotation.x, MinPitch, MaxPitch); m_distance = std::clamp(m_distance + static_cast(deltaDistance), MinDistance, MaxDistance); - const auto basis = computeBasis(m_u, m_v, m_distance); + const auto basis = computeBasis(m_orbitUv, m_distance); applyPlanarTargetTranslation(deltaTranslation, basis); return applyPose(); diff --git a/common/include/camera/CCameraGoalAnalysis.hpp b/common/include/camera/CCameraGoalAnalysis.hpp index 3546a2bd0..bc81e3439 100644 --- a/common/include/camera/CCameraGoalAnalysis.hpp +++ b/common/include/camera/CCameraGoalAnalysis.hpp @@ -68,7 +68,7 @@ struct CCameraGoalAnalysisUtilities final static inline SCameraGoalApplyAnalysis analyzePresetApply(const CCameraGoalSolver& solver, const ICamera* camera, const CCameraPreset& preset) { - return analyzeGoalApply(solver, camera, makeGoalFromPreset(preset)); + return analyzeGoalApply(solver, camera, CCameraPresetUtilities::makeGoalFromPreset(preset)); } static inline SCameraCaptureAnalysis analyzeCameraCapture(const CCameraGoalSolver& solver, ICamera* camera) diff --git a/common/include/camera/CCameraKeyframeTrack.hpp b/common/include/camera/CCameraKeyframeTrack.hpp index 734e48c95..2bcefaae7 100644 --- a/common/include/camera/CCameraKeyframeTrack.hpp +++ b/common/include/camera/CCameraKeyframeTrack.hpp @@ -29,7 +29,7 @@ struct CCameraKeyframeTrackUtilities final const double timeEps, const double posEps, const double rotEpsDeg, const double scalarEps) { return hlsl::abs(static_cast(lhs.time - rhs.time)) <= timeEps && - comparePresets(lhs.preset, rhs.preset, posEps, rotEpsDeg, scalarEps); + CCameraPresetUtilities::comparePresets(lhs.preset, rhs.preset, posEps, rotEpsDeg, scalarEps); } //! Compare two authored keyframe tracks with optional selection-state checking. @@ -81,7 +81,12 @@ struct CCameraKeyframeTrackUtilities final const double alpha = static_cast(clampedTime - a.time) / static_cast(b.time - a.time); preset = a.preset; - assignGoalToPreset(preset, CCameraGoalUtilities::blendGoals(makeGoalFromPreset(a.preset), makeGoalFromPreset(b.preset), alpha)); + CCameraPresetUtilities::assignGoalToPreset( + preset, + CCameraGoalUtilities::blendGoals( + CCameraPresetUtilities::makeGoalFromPreset(a.preset), + CCameraPresetUtilities::makeGoalFromPreset(b.preset), + alpha)); return true; } diff --git a/common/include/camera/CCameraManipulationUtilities.hpp b/common/include/camera/CCameraManipulationUtilities.hpp index 5a4b0add7..e71e1e800 100644 --- a/common/include/camera/CCameraManipulationUtilities.hpp +++ b/common/include/camera/CCameraManipulationUtilities.hpp @@ -146,7 +146,7 @@ struct CCameraManipulationUtilities final CCameraPreset preset; preset.goal.position = pos; preset.goal.orientation = hlsl::makeQuaternionFromEulerDegreesYXZ(clamped); - return applyPreset(solver, camera, preset); + return CCameraPresetFlowUtilities::applyPreset(solver, camera, preset); } }; diff --git a/common/include/camera/CCameraPresentationUtilities.hpp b/common/include/camera/CCameraPresentationUtilities.hpp index fe61864ee..29cabf2c3 100644 --- a/common/include/camera/CCameraPresentationUtilities.hpp +++ b/common/include/camera/CCameraPresentationUtilities.hpp @@ -61,63 +61,64 @@ struct SCameraCapturePresentation final : core::SCameraCaptureAnalysis std::string policyLabel; }; -inline SCameraGoalApplyPresentationBadges collectGoalApplyPresentationBadges(const SCameraGoalApplyPresentation& presentation); - -//! Shared user-facing label for the exactness filter selector. -inline const char* getPresetApplyPresentationFilterLabel(const EPresetApplyPresentationFilter mode) +struct CCameraPresentationUtilities final { - switch (mode) + //! Shared user-facing label for the exactness filter selector. + static inline const char* getPresetApplyPresentationFilterLabel(const EPresetApplyPresentationFilter mode) { - case EPresetApplyPresentationFilter::All: - return "All"; - case EPresetApplyPresentationFilter::Exact: - return "Exact"; - case EPresetApplyPresentationFilter::BestEffort: - return "Best-effort"; - default: - return "All"; + switch (mode) + { + case EPresetApplyPresentationFilter::All: + return "All"; + case EPresetApplyPresentationFilter::Exact: + return "Exact"; + case EPresetApplyPresentationFilter::BestEffort: + return "Best-effort"; + default: + return "All"; + } } -} -//! Build presentation text for one analyzed goal-apply result. -inline SCameraGoalApplyPresentation makeGoalApplyPresentation(const core::SCameraGoalApplyAnalysis& analysis, const core::ICamera* targetCamera) -{ - SCameraGoalApplyPresentation presentation; - static_cast(presentation) = analysis; - presentation.badges = collectGoalApplyPresentationBadges(presentation); - presentation.sourceKindLabel = std::string(CCameraTextUtilities::getCameraTypeLabel(presentation.goal.sourceKind)); - presentation.goalStateLabel = CCameraTextUtilities::describeGoalStateMask(presentation.goal.sourceGoalStateMask); - presentation.compatibilityLabel = CCameraTextUtilities::describeGoalApplyCompatibility(analysis, targetCamera); - presentation.policyLabel = CCameraTextUtilities::describeGoalApplyPolicy(analysis); - return presentation; -} - -//! Analyze one preset against one camera and return reusable presentation data. -inline SCameraGoalApplyPresentation analyzePresetPresentation(const core::CCameraGoalSolver& solver, const core::ICamera* camera, const core::CCameraPreset& preset) -{ - return makeGoalApplyPresentation(core::CCameraGoalAnalysisUtilities::analyzePresetApply(solver, camera, preset), camera); -} + //! Build reusable badge flags for one preset/keyframe compatibility answer. + static inline SCameraGoalApplyPresentationBadges collectGoalApplyPresentationBadges(const SCameraGoalApplyPresentation& presentation) + { + SCameraGoalApplyPresentationBadges badges; + badges.exact = presentation.exact(); + badges.bestEffort = presentation.hasCamera && !presentation.exact(); + badges.dropsState = presentation.dropsGoalState(); + badges.sharedStateOnly = presentation.usesSharedStateOnly(); + badges.blocked = !presentation.canApply; + return badges; + } -//! Build reusable badge flags for one preset/keyframe compatibility answer. -inline SCameraGoalApplyPresentationBadges collectGoalApplyPresentationBadges(const SCameraGoalApplyPresentation& presentation) -{ - SCameraGoalApplyPresentationBadges badges; - badges.exact = presentation.exact(); - badges.bestEffort = presentation.hasCamera && !presentation.exact(); - badges.dropsState = presentation.dropsGoalState(); - badges.sharedStateOnly = presentation.usesSharedStateOnly(); - badges.blocked = !presentation.canApply; - return badges; -} - -//! Analyze one camera capture path and return reusable presentation data. -inline SCameraCapturePresentation analyzeCapturePresentation(const core::CCameraGoalSolver& solver, core::ICamera* camera) -{ - SCameraCapturePresentation presentation; - static_cast(presentation) = core::CCameraGoalAnalysisUtilities::analyzeCameraCapture(solver, camera); - presentation.policyLabel = CCameraTextUtilities::describeCameraCapturePolicy(presentation, camera); - return presentation; -} + //! Build presentation text for one analyzed goal-apply result. + static inline SCameraGoalApplyPresentation makeGoalApplyPresentation(const core::SCameraGoalApplyAnalysis& analysis, const core::ICamera* targetCamera) + { + SCameraGoalApplyPresentation presentation; + static_cast(presentation) = analysis; + presentation.badges = collectGoalApplyPresentationBadges(presentation); + presentation.sourceKindLabel = std::string(CCameraTextUtilities::getCameraTypeLabel(presentation.goal.sourceKind)); + presentation.goalStateLabel = CCameraTextUtilities::describeGoalStateMask(presentation.goal.sourceGoalStateMask); + presentation.compatibilityLabel = CCameraTextUtilities::describeGoalApplyCompatibility(analysis, targetCamera); + presentation.policyLabel = CCameraTextUtilities::describeGoalApplyPolicy(analysis); + return presentation; + } + + //! Analyze one preset against one camera and return reusable presentation data. + static inline SCameraGoalApplyPresentation analyzePresetPresentation(const core::CCameraGoalSolver& solver, const core::ICamera* camera, const core::CCameraPreset& preset) + { + return makeGoalApplyPresentation(core::CCameraGoalAnalysisUtilities::analyzePresetApply(solver, camera, preset), camera); + } + + //! Analyze one camera capture path and return reusable presentation data. + static inline SCameraCapturePresentation analyzeCapturePresentation(const core::CCameraGoalSolver& solver, core::ICamera* camera) + { + SCameraCapturePresentation presentation; + static_cast(presentation) = core::CCameraGoalAnalysisUtilities::analyzeCameraCapture(solver, camera); + presentation.policyLabel = CCameraTextUtilities::describeCameraCapturePolicy(presentation, camera); + return presentation; + } +}; } // namespace nbl::ui diff --git a/common/include/camera/CCameraPreset.hpp b/common/include/camera/CCameraPreset.hpp index 0343109b2..cc33d09d8 100644 --- a/common/include/camera/CCameraPreset.hpp +++ b/common/include/camera/CCameraPreset.hpp @@ -28,40 +28,43 @@ struct CCameraKeyframe float time = 0.f; }; -inline void assignGoalToPreset(CCameraPreset& preset, const CCameraGoal& goal) +struct CCameraPresetUtilities final { - preset.goal = CCameraGoalUtilities::canonicalizeGoal(goal); -} - -inline CCameraGoal makeGoalFromPreset(const CCameraPreset& preset) -{ - return CCameraGoalUtilities::canonicalizeGoal(preset.goal); -} + static inline void assignGoalToPreset(CCameraPreset& preset, const CCameraGoal& goal) + { + preset.goal = CCameraGoalUtilities::canonicalizeGoal(goal); + } -//! Compare two named presets through their shared canonical goal state. -inline bool comparePresets(const CCameraPreset& lhs, const CCameraPreset& rhs, - const double posEps, const double rotEpsDeg, const double scalarEps) -{ - return lhs.name == rhs.name && - lhs.identifier == rhs.identifier && - CCameraGoalUtilities::compareGoals(makeGoalFromPreset(lhs), makeGoalFromPreset(rhs), posEps, rotEpsDeg, scalarEps); -} + static inline CCameraGoal makeGoalFromPreset(const CCameraPreset& preset) + { + return CCameraGoalUtilities::canonicalizeGoal(preset.goal); + } -//! Compare two preset collections element-by-element through the shared canonical goal state. -inline bool comparePresetCollections(std::span lhs, std::span rhs, - const double posEps, const double rotEpsDeg, const double scalarEps) -{ - if (lhs.size() != rhs.size()) - return false; + //! Compare two named presets through their shared canonical goal state. + static inline bool comparePresets(const CCameraPreset& lhs, const CCameraPreset& rhs, + const double posEps, const double rotEpsDeg, const double scalarEps) + { + return lhs.name == rhs.name && + lhs.identifier == rhs.identifier && + CCameraGoalUtilities::compareGoals(makeGoalFromPreset(lhs), makeGoalFromPreset(rhs), posEps, rotEpsDeg, scalarEps); + } - for (size_t i = 0u; i < lhs.size(); ++i) + //! Compare two preset collections element-by-element through the shared canonical goal state. + static inline bool comparePresetCollections(std::span lhs, std::span rhs, + const double posEps, const double rotEpsDeg, const double scalarEps) { - if (!comparePresets(lhs[i], rhs[i], posEps, rotEpsDeg, scalarEps)) + if (lhs.size() != rhs.size()) return false; - } - return true; -} + for (size_t i = 0u; i < lhs.size(); ++i) + { + if (!comparePresets(lhs[i], rhs[i], posEps, rotEpsDeg, scalarEps)) + return false; + } + + return true; + } +}; } // namespace nbl::core diff --git a/common/include/camera/CCameraPresetFlow.hpp b/common/include/camera/CCameraPresetFlow.hpp index 7ce71024d..1b282d599 100644 --- a/common/include/camera/CCameraPresetFlow.hpp +++ b/common/include/camera/CCameraPresetFlow.hpp @@ -39,103 +39,111 @@ struct SCameraPresetApplySummary } }; -//! Compare the current camera state against a preset using the shared goal representation. -inline bool comparePresetToCameraState(const CCameraGoalSolver& solver, ICamera* camera, const CCameraPreset& preset, - const double posEps, const double rotEpsDeg, const double scalarEps) +struct CCameraPresetFlowUtilities final { - const auto capture = solver.captureDetailed(camera); - if (!capture.canUseGoal()) - return false; + //! Compare the current camera state against a preset using the shared goal representation. + static inline bool comparePresetToCameraState(const CCameraGoalSolver& solver, ICamera* camera, const CCameraPreset& preset, + const double posEps, const double rotEpsDeg, const double scalarEps) + { + const auto capture = solver.captureDetailed(camera); + if (!capture.canUseGoal()) + return false; + + return CCameraGoalUtilities::compareGoals( + capture.goal, + CCameraPresetUtilities::makeGoalFromPreset(preset), + posEps, + rotEpsDeg, + scalarEps); + } - return CCameraGoalUtilities::compareGoals(capture.goal, makeGoalFromPreset(preset), posEps, rotEpsDeg, scalarEps); -} + //! Explain the first visible mismatch between a camera state and a preset. + static inline std::string describePresetCameraMismatch(const CCameraGoalSolver& solver, ICamera* camera, const CCameraPreset& preset) + { + const auto capture = solver.captureDetailed(camera); + if (!capture.hasCamera) + return "camera=null"; + if (!capture.captured) + return "goal_state=unavailable"; + if (!capture.finiteGoal) + return "goal_state=invalid"; + + return CCameraGoalUtilities::describeGoalMismatch(capture.goal, CCameraPresetUtilities::makeGoalFromPreset(preset)); + } -//! Explain the first visible mismatch between a camera state and a preset. -inline std::string describePresetCameraMismatch(const CCameraGoalSolver& solver, ICamera* camera, const CCameraPreset& preset) -{ - const auto capture = solver.captureDetailed(camera); - if (!capture.hasCamera) - return "camera=null"; - if (!capture.captured) - return "goal_state=unavailable"; - if (!capture.finiteGoal) - return "goal_state=invalid"; - - return CCameraGoalUtilities::describeGoalMismatch(capture.goal, makeGoalFromPreset(preset)); -} - -//! Build a preset from an already analyzed capture result. -inline bool tryCapturePreset(const SCameraCaptureAnalysis& captureAnalysis, ICamera* camera, std::string_view name, CCameraPreset& preset) -{ - preset = {}; - preset.name = std::string(name); - if (!captureAnalysis.canCapture || !camera) - return false; - - preset.identifier = std::string(camera->getIdentifier()); - assignGoalToPreset(preset, captureAnalysis.goal); - return true; -} - -//! Capture a preset directly from a camera through the shared goal solver. -inline bool tryCapturePreset(const CCameraGoalSolver& solver, ICamera* camera, std::string_view name, CCameraPreset& preset) -{ - return tryCapturePreset(CCameraGoalAnalysisUtilities::analyzeCameraCapture(solver, camera), camera, name, preset); -} + //! Build a preset from an already analyzed capture result. + static inline bool tryCapturePreset(const SCameraCaptureAnalysis& captureAnalysis, ICamera* camera, std::string_view name, CCameraPreset& preset) + { + preset = {}; + preset.name = std::string(name); + if (!captureAnalysis.canCapture || !camera) + return false; + + preset.identifier = std::string(camera->getIdentifier()); + CCameraPresetUtilities::assignGoalToPreset(preset, captureAnalysis.goal); + return true; + } -//! Value-returning convenience wrapper around `tryCapturePreset`. -inline CCameraPreset capturePreset(const CCameraGoalSolver& solver, ICamera* camera, std::string_view name) -{ - CCameraPreset preset; - tryCapturePreset(solver, camera, name, preset); - return preset; -} + //! Capture a preset directly from a camera through the shared goal solver. + static inline bool tryCapturePreset(const CCameraGoalSolver& solver, ICamera* camera, std::string_view name, CCameraPreset& preset) + { + return tryCapturePreset(CCameraGoalAnalysisUtilities::analyzeCameraCapture(solver, camera), camera, name, preset); + } -//! Apply a preset through the shared goal solver and preserve detailed apply diagnostics. -inline CCameraGoalSolver::SApplyResult applyPresetDetailed(const CCameraGoalSolver& solver, ICamera* camera, const CCameraPreset& preset) -{ - if (!camera) - return {}; + //! Value-returning convenience wrapper around `tryCapturePreset`. + static inline CCameraPreset capturePreset(const CCameraGoalSolver& solver, ICamera* camera, std::string_view name) + { + CCameraPreset preset; + tryCapturePreset(solver, camera, name, preset); + return preset; + } - return solver.applyDetailed(camera, makeGoalFromPreset(preset)); -} + //! Apply a preset through the shared goal solver and preserve detailed apply diagnostics. + static inline CCameraGoalSolver::SApplyResult applyPresetDetailed(const CCameraGoalSolver& solver, ICamera* camera, const CCameraPreset& preset) + { + if (!camera) + return {}; -//! Bool-returning convenience wrapper around `applyPresetDetailed`. -inline bool applyPreset(const CCameraGoalSolver& solver, ICamera* camera, const CCameraPreset& preset) -{ - return applyPresetDetailed(solver, camera, preset).succeeded(); -} + return solver.applyDetailed(camera, CCameraPresetUtilities::makeGoalFromPreset(preset)); + } -//! Fold one detailed apply result into an aggregate preset-apply summary. -inline void accumulatePresetApplySummary(SCameraPresetApplySummary& summary, const CCameraGoalSolver::SApplyResult& result) -{ - ++summary.targetCount; - if (result.succeeded()) + //! Bool-returning convenience wrapper around `applyPresetDetailed`. + static inline bool applyPreset(const CCameraGoalSolver& solver, ICamera* camera, const CCameraPreset& preset) { - ++summary.successCount; - if (result.approximate()) - ++summary.approximateCount; + return applyPresetDetailed(solver, camera, preset).succeeded(); } - else + + //! Fold one detailed apply result into an aggregate preset-apply summary. + static inline void accumulatePresetApplySummary(SCameraPresetApplySummary& summary, const CCameraGoalSolver::SApplyResult& result) { - ++summary.failureCount; + ++summary.targetCount; + if (result.succeeded()) + { + ++summary.successCount; + if (result.approximate()) + ++summary.approximateCount; + } + else + { + ++summary.failureCount; + } } -} -//! Apply one preset to a camera range and collect a typed aggregate summary. -inline SCameraPresetApplySummary applyPresetToCameraRange(const CCameraGoalSolver& solver, std::span cameras, const CCameraPreset& preset) -{ - SCameraPresetApplySummary summary; - for (auto* camera : cameras) + //! Apply one preset to a camera range and collect a typed aggregate summary. + static inline SCameraPresetApplySummary applyPresetToCameraRange(const CCameraGoalSolver& solver, std::span cameras, const CCameraPreset& preset) { - if (!camera) - continue; + SCameraPresetApplySummary summary; + for (auto* camera : cameras) + { + if (!camera) + continue; - accumulatePresetApplySummary(summary, applyPresetDetailed(solver, camera, preset)); - } + accumulatePresetApplySummary(summary, applyPresetDetailed(solver, camera, preset)); + } - return summary; -} + return summary; + } +}; } // namespace nbl::core diff --git a/common/include/camera/CCameraProjectionUtilities.hpp b/common/include/camera/CCameraProjectionUtilities.hpp index d35815ac4..803fb2096 100644 --- a/common/include/camera/CCameraProjectionUtilities.hpp +++ b/common/include/camera/CCameraProjectionUtilities.hpp @@ -10,23 +10,26 @@ namespace nbl::core { -//! Apply a camera-provided dynamic perspective FOV to one planar projection entry. -inline bool syncDynamicPerspectiveProjection(ICamera* camera, IPlanarProjection::CProjection& projection) +struct CCameraProjectionUtilities final { - if (!camera) - return false; - - const auto& params = projection.getParameters(); - if (params.m_type != IPlanarProjection::CProjection::Perspective) - return false; - - float dynamicFov = 0.0f; - if (!camera->tryGetDynamicPerspectiveFov(dynamicFov)) - return false; - - projection.setPerspective(params.m_zNear, params.m_zFar, dynamicFov); - return true; -} + //! Apply a camera-provided dynamic perspective FOV to one planar projection entry. + static inline bool syncDynamicPerspectiveProjection(ICamera* camera, IPlanarProjection::CProjection& projection) + { + if (!camera) + return false; + + const auto& params = projection.getParameters(); + if (params.m_type != IPlanarProjection::CProjection::Perspective) + return false; + + float dynamicFov = 0.0f; + if (!camera->tryGetDynamicPerspectiveFov(dynamicFov)) + return false; + + projection.setPerspective(params.m_zNear, params.m_zFar, dynamicFov); + return true; + } +}; } // namespace nbl::core diff --git a/common/include/camera/CCameraScriptedCheckRunner.hpp b/common/include/camera/CCameraScriptedCheckRunner.hpp index a09499257..96e0986f3 100644 --- a/common/include/camera/CCameraScriptedCheckRunner.hpp +++ b/common/include/camera/CCameraScriptedCheckRunner.hpp @@ -70,107 +70,109 @@ struct CCameraScriptedCheckFrameResult bool hadFailures = false; }; -inline void scriptedCheckSetStepReference( - CCameraScriptedCheckRuntimeState& state, - const hlsl::float64_t3& position, - const hlsl::camera_quaternion_t& orientation) +struct CCameraScriptedCheckRunnerUtilities final { - state.step.valid = true; - state.step.position = position; - state.step.orientation = hlsl::normalizeQuaternion(orientation); -} - -inline void scriptedCheckSetBaselineReference( - CCameraScriptedCheckRuntimeState& state, - const hlsl::float64_t3& position, - const hlsl::camera_quaternion_t& orientation) -{ - state.baseline.valid = true; - state.baseline.position = position; - state.baseline.orientation = hlsl::normalizeQuaternion(orientation); - scriptedCheckSetStepReference(state, position, orientation); -} - -inline bool scriptedCheckComputePoseDelta( - const hlsl::float64_t3& currentPosition, - const hlsl::camera_quaternion_t& currentOrientation, - const hlsl::float64_t3& referencePosition, - const hlsl::camera_quaternion_t& referenceOrientation, - hlsl::SCameraPoseDelta& outDelta) -{ - return hlsl::tryComputePoseDelta( - currentPosition, - currentOrientation, - referencePosition, - referenceOrientation, - outDelta); -} - -template -inline std::string buildScriptedCheckMessage(Fn&& formatter) -{ - std::ostringstream oss; - formatter(oss); - return oss.str(); -} - -inline void appendScriptedCheckLog( - CCameraScriptedCheckFrameResult& result, - const bool failure, - std::string&& text) -{ - result.logs.push_back({ - .failure = failure, - .text = std::move(text) - }); - result.hadFailures = result.hadFailures || failure; -} - -//! Evaluate all authored scripted checks scheduled for the current frame. -inline CCameraScriptedCheckFrameResult evaluateScriptedChecksForFrame( - const std::vector& checks, - CCameraScriptedCheckRuntimeState& state, - const CCameraScriptedCheckContext& context) -{ - CCameraScriptedCheckFrameResult result = {}; + static inline void scriptedCheckSetStepReference( + CCameraScriptedCheckRuntimeState& state, + const hlsl::float64_t3& position, + const hlsl::camera_quaternion_t& orientation) + { + state.step.valid = true; + state.step.position = position; + state.step.orientation = hlsl::normalizeQuaternion(orientation); + } - while (state.nextCheckIndex < checks.size() && checks[state.nextCheckIndex].frame == context.frame) + static inline void scriptedCheckSetBaselineReference( + CCameraScriptedCheckRuntimeState& state, + const hlsl::float64_t3& position, + const hlsl::camera_quaternion_t& orientation) { - const auto& check = checks[state.nextCheckIndex]; + state.baseline.valid = true; + state.baseline.position = position; + state.baseline.orientation = hlsl::normalizeQuaternion(orientation); + scriptedCheckSetStepReference(state, position, orientation); + } - if (!context.camera) - { - appendScriptedCheckLog( - result, - true, - buildScriptedCheckMessage([&](std::ostringstream& oss) - { - oss << "[script][fail] check frame=" << context.frame << " no active camera"; - })); - ++state.nextCheckIndex; - continue; - } + static inline bool scriptedCheckComputePoseDelta( + const hlsl::float64_t3& currentPosition, + const hlsl::camera_quaternion_t& currentOrientation, + const hlsl::float64_t3& referencePosition, + const hlsl::camera_quaternion_t& referenceOrientation, + hlsl::SCameraPoseDelta& outDelta) + { + return hlsl::tryComputePoseDelta( + currentPosition, + currentOrientation, + referencePosition, + referenceOrientation, + outDelta); + } - const auto& gimbal = context.camera->getGimbal(); - const auto pos = gimbal.getPosition(); - const auto orientation = hlsl::normalizeQuaternion(gimbal.getOrientation()); - const auto eulerDeg = hlsl::getCastedVector(hlsl::getCameraOrientationEulerDegrees(orientation)); + template + static inline std::string buildScriptedCheckMessage(Fn&& formatter) + { + std::ostringstream oss; + formatter(oss); + return oss.str(); + } - if (!hlsl::isFiniteVec3(pos) || !hlsl::isFiniteQuaternion(orientation) || !hlsl::isFiniteVec3(eulerDeg)) - { - appendScriptedCheckLog( - result, - true, - buildScriptedCheckMessage([&](std::ostringstream& oss) - { - oss << "[script][fail] check frame=" << context.frame << " non-finite gimbal state"; - })); - ++state.nextCheckIndex; - continue; - } + static inline void appendScriptedCheckLog( + CCameraScriptedCheckFrameResult& result, + const bool failure, + std::string&& text) + { + result.logs.push_back({ + .failure = failure, + .text = std::move(text) + }); + result.hadFailures = result.hadFailures || failure; + } + + //! Evaluate all authored scripted checks scheduled for the current frame. + static inline CCameraScriptedCheckFrameResult evaluateScriptedChecksForFrame( + const std::vector& checks, + CCameraScriptedCheckRuntimeState& state, + const CCameraScriptedCheckContext& context) + { + CCameraScriptedCheckFrameResult result = {}; - switch (check.kind) + while (state.nextCheckIndex < checks.size() && checks[state.nextCheckIndex].frame == context.frame) { + const auto& check = checks[state.nextCheckIndex]; + + if (!context.camera) + { + appendScriptedCheckLog( + result, + true, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << "[script][fail] check frame=" << context.frame << " no active camera"; + })); + ++state.nextCheckIndex; + continue; + } + + const auto& gimbal = context.camera->getGimbal(); + const auto pos = gimbal.getPosition(); + const auto orientation = hlsl::normalizeQuaternion(gimbal.getOrientation()); + const auto eulerDeg = hlsl::getCastedVector(hlsl::getCameraOrientationEulerDegrees(orientation)); + + if (!hlsl::isFiniteVec3(pos) || !hlsl::isFiniteQuaternion(orientation) || !hlsl::isFiniteVec3(eulerDeg)) + { + appendScriptedCheckLog( + result, + true, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << "[script][fail] check frame=" << context.frame << " non-finite gimbal state"; + })); + ++state.nextCheckIndex; + continue; + } + + switch (check.kind) + { case CCameraScriptedInputCheck::Kind::Baseline: { scriptedCheckSetBaselineReference(state, pos, orientation); @@ -555,11 +557,12 @@ inline CCameraScriptedCheckFrameResult evaluateScriptedChecksForFrame( } } - ++state.nextCheckIndex; - } + ++state.nextCheckIndex; + } - return result; -} + return result; + } +}; } // namespace nbl::system diff --git a/common/include/camera/CCameraScriptedRuntime.hpp b/common/include/camera/CCameraScriptedRuntime.hpp index a0f83ef5d..54d690c99 100644 --- a/common/include/camera/CCameraScriptedRuntime.hpp +++ b/common/include/camera/CCameraScriptedRuntime.hpp @@ -186,131 +186,134 @@ struct CCameraScriptedTimeline } }; -inline void finalizeScriptedTimeline( - std::vector& events, - std::vector& checks, - std::vector& captureFrames, - const bool disableCaptureFrames = false) +struct CCameraScriptedRuntimeUtilities final { - std::stable_sort(events.begin(), events.end(), - [](const CCameraScriptedInputEvent& a, const CCameraScriptedInputEvent& b) { return a.frame < b.frame; }); - std::stable_sort(checks.begin(), checks.end(), - [](const CCameraScriptedInputCheck& a, const CCameraScriptedInputCheck& b) { return a.frame < b.frame; }); - if (!captureFrames.empty()) + static inline void finalizeScriptedTimeline( + std::vector& events, + std::vector& checks, + std::vector& captureFrames, + const bool disableCaptureFrames = false) { - std::sort(captureFrames.begin(), captureFrames.end()); - captureFrames.erase(std::unique(captureFrames.begin(), captureFrames.end()), captureFrames.end()); + std::stable_sort(events.begin(), events.end(), + [](const CCameraScriptedInputEvent& a, const CCameraScriptedInputEvent& b) { return a.frame < b.frame; }); + std::stable_sort(checks.begin(), checks.end(), + [](const CCameraScriptedInputCheck& a, const CCameraScriptedInputCheck& b) { return a.frame < b.frame; }); + if (!captureFrames.empty()) + { + std::sort(captureFrames.begin(), captureFrames.end()); + captureFrames.erase(std::unique(captureFrames.begin(), captureFrames.end()), captureFrames.end()); + } + if (disableCaptureFrames) + captureFrames.clear(); } - if (disableCaptureFrames) - captureFrames.clear(); -} -inline void finalizeScriptedTimeline(CCameraScriptedTimeline& timeline, const bool disableCaptureFrames = false) -{ - finalizeScriptedTimeline(timeline.events, timeline.checks, timeline.captureFrames, disableCaptureFrames); -} - -inline void appendScriptedActionEvent( - CCameraScriptedTimeline& timeline, - const uint64_t frame, - const CCameraScriptedInputEvent::ActionData::Kind kind, - const int32_t value) -{ - CCameraScriptedInputEvent entry; - entry.frame = frame; - entry.type = CCameraScriptedInputEvent::Type::Action; - entry.action.kind = kind; - entry.action.value = value; - timeline.events.emplace_back(std::move(entry)); -} - -inline void appendScriptedGoalEvent( - CCameraScriptedTimeline& timeline, - const uint64_t frame, - const core::CCameraGoal& goal, - const bool requireExact = true) -{ - CCameraScriptedInputEvent entry; - entry.frame = frame; - entry.type = CCameraScriptedInputEvent::Type::Goal; - entry.goal.goal = goal; - entry.goal.requireExact = requireExact; - timeline.events.emplace_back(std::move(entry)); -} - -inline void appendScriptedTrackedTargetTransformEvent( - CCameraScriptedTimeline& timeline, - const uint64_t frame, - const hlsl::float64_t4x4& transform) -{ - CCameraScriptedInputEvent entry; - entry.frame = frame; - entry.type = CCameraScriptedInputEvent::Type::TrackedTargetTransform; - entry.trackedTargetTransform.transform = transform; - timeline.events.emplace_back(std::move(entry)); -} - -inline void appendScriptedSegmentLabelEvent( - CCameraScriptedTimeline& timeline, - const uint64_t frame, - std::string label) -{ - CCameraScriptedInputEvent entry; - entry.frame = frame; - entry.type = CCameraScriptedInputEvent::Type::SegmentLabel; - entry.segmentLabel.label = std::move(label); - timeline.events.emplace_back(std::move(entry)); -} - -inline void appendScriptedBaselineCheck(CCameraScriptedTimeline& timeline, const uint64_t frame) -{ - CCameraScriptedInputCheck entry; - entry.frame = frame; - entry.kind = CCameraScriptedInputCheck::Kind::Baseline; - timeline.checks.emplace_back(std::move(entry)); -} - -inline void appendScriptedGimbalStepCheck( - CCameraScriptedTimeline& timeline, - const uint64_t frame, - const bool hasPosDeltaConstraint, - const float posTolerance, - const float minPosDelta, - const bool hasEulerDeltaConstraint, - const float eulerToleranceDeg, - const float minEulerDeltaDeg) -{ - CCameraScriptedInputCheck entry; - entry.frame = frame; - entry.kind = CCameraScriptedInputCheck::Kind::GimbalStep; - if (hasPosDeltaConstraint) + static inline void finalizeScriptedTimeline(CCameraScriptedTimeline& timeline, const bool disableCaptureFrames = false) { - entry.hasPosDeltaConstraint = true; - entry.posTolerance = posTolerance; - entry.minPosDelta = minPosDelta; + finalizeScriptedTimeline(timeline.events, timeline.checks, timeline.captureFrames, disableCaptureFrames); } - if (hasEulerDeltaConstraint) + + static inline void appendScriptedActionEvent( + CCameraScriptedTimeline& timeline, + const uint64_t frame, + const CCameraScriptedInputEvent::ActionData::Kind kind, + const int32_t value) { - entry.hasEulerDeltaConstraint = true; - entry.eulerToleranceDeg = eulerToleranceDeg; - entry.minEulerDeltaDeg = minEulerDeltaDeg; + CCameraScriptedInputEvent entry; + entry.frame = frame; + entry.type = CCameraScriptedInputEvent::Type::Action; + entry.action.kind = kind; + entry.action.value = value; + timeline.events.emplace_back(std::move(entry)); + } + + static inline void appendScriptedGoalEvent( + CCameraScriptedTimeline& timeline, + const uint64_t frame, + const core::CCameraGoal& goal, + const bool requireExact = true) + { + CCameraScriptedInputEvent entry; + entry.frame = frame; + entry.type = CCameraScriptedInputEvent::Type::Goal; + entry.goal.goal = goal; + entry.goal.requireExact = requireExact; + timeline.events.emplace_back(std::move(entry)); } - timeline.checks.emplace_back(std::move(entry)); -} - -inline void appendScriptedFollowTargetLockCheck( - CCameraScriptedTimeline& timeline, - const uint64_t frame, - const float toleranceDeg = CCameraScriptedCheckDefaults::EulerToleranceDeg, - const float screenToleranceNdc = CCameraScriptedCheckDefaults::FollowScreenToleranceNdc) -{ - CCameraScriptedInputCheck entry; - entry.frame = frame; - entry.kind = CCameraScriptedInputCheck::Kind::FollowTargetLock; - entry.eulerToleranceDeg = toleranceDeg; - entry.posTolerance = screenToleranceNdc; - timeline.checks.emplace_back(std::move(entry)); -} + + static inline void appendScriptedTrackedTargetTransformEvent( + CCameraScriptedTimeline& timeline, + const uint64_t frame, + const hlsl::float64_t4x4& transform) + { + CCameraScriptedInputEvent entry; + entry.frame = frame; + entry.type = CCameraScriptedInputEvent::Type::TrackedTargetTransform; + entry.trackedTargetTransform.transform = transform; + timeline.events.emplace_back(std::move(entry)); + } + + static inline void appendScriptedSegmentLabelEvent( + CCameraScriptedTimeline& timeline, + const uint64_t frame, + std::string label) + { + CCameraScriptedInputEvent entry; + entry.frame = frame; + entry.type = CCameraScriptedInputEvent::Type::SegmentLabel; + entry.segmentLabel.label = std::move(label); + timeline.events.emplace_back(std::move(entry)); + } + + static inline void appendScriptedBaselineCheck(CCameraScriptedTimeline& timeline, const uint64_t frame) + { + CCameraScriptedInputCheck entry; + entry.frame = frame; + entry.kind = CCameraScriptedInputCheck::Kind::Baseline; + timeline.checks.emplace_back(std::move(entry)); + } + + static inline void appendScriptedGimbalStepCheck( + CCameraScriptedTimeline& timeline, + const uint64_t frame, + const bool hasPosDeltaConstraint, + const float posTolerance, + const float minPosDelta, + const bool hasEulerDeltaConstraint, + const float eulerToleranceDeg, + const float minEulerDeltaDeg) + { + CCameraScriptedInputCheck entry; + entry.frame = frame; + entry.kind = CCameraScriptedInputCheck::Kind::GimbalStep; + if (hasPosDeltaConstraint) + { + entry.hasPosDeltaConstraint = true; + entry.posTolerance = posTolerance; + entry.minPosDelta = minPosDelta; + } + if (hasEulerDeltaConstraint) + { + entry.hasEulerDeltaConstraint = true; + entry.eulerToleranceDeg = eulerToleranceDeg; + entry.minEulerDeltaDeg = minEulerDeltaDeg; + } + timeline.checks.emplace_back(std::move(entry)); + } + + static inline void appendScriptedFollowTargetLockCheck( + CCameraScriptedTimeline& timeline, + const uint64_t frame, + const float toleranceDeg = CCameraScriptedCheckDefaults::EulerToleranceDeg, + const float screenToleranceNdc = CCameraScriptedCheckDefaults::FollowScreenToleranceNdc) + { + CCameraScriptedInputCheck entry; + entry.frame = frame; + entry.kind = CCameraScriptedInputCheck::Kind::FollowTargetLock; + entry.eulerToleranceDeg = toleranceDeg; + entry.posTolerance = screenToleranceNdc; + timeline.checks.emplace_back(std::move(entry)); + } +}; /** * Per-frame scripted runtime batch already partitioned by payload kind. @@ -347,44 +350,47 @@ struct CCameraScriptedFrameEvents }; //! Dequeue all authored scripted events scheduled for one frame. -inline void dequeueScriptedFrameEvents( - const std::vector& events, - size_t& nextEventIndex, - const uint64_t frame, - CCameraScriptedFrameEvents& out) +struct CCameraScriptedFrameEventUtilities final { - out.clear(); - while (nextEventIndex < events.size() && events[nextEventIndex].frame == frame) + static inline void dequeueScriptedFrameEvents( + const std::vector& events, + size_t& nextEventIndex, + const uint64_t frame, + CCameraScriptedFrameEvents& out) { - const auto& ev = events[nextEventIndex]; - switch (ev.type) + out.clear(); + while (nextEventIndex < events.size() && events[nextEventIndex].frame == frame) { - case CCameraScriptedInputEvent::Type::Keyboard: - out.keyboard.emplace_back(ev.keyboard); - break; - case CCameraScriptedInputEvent::Type::Mouse: - out.mouse.emplace_back(ev.mouse); - break; - case CCameraScriptedInputEvent::Type::Imguizmo: - out.imguizmo.emplace_back(ev.imguizmo); - break; - case CCameraScriptedInputEvent::Type::Action: - out.actions.emplace_back(ev.action); - break; - case CCameraScriptedInputEvent::Type::Goal: - out.goals.emplace_back(ev.goal); - break; - case CCameraScriptedInputEvent::Type::TrackedTargetTransform: - out.trackedTargetTransforms.emplace_back(ev.trackedTargetTransform); - break; - case CCameraScriptedInputEvent::Type::SegmentLabel: - out.segmentLabels.emplace_back(ev.segmentLabel.label); - break; + const auto& ev = events[nextEventIndex]; + switch (ev.type) + { + case CCameraScriptedInputEvent::Type::Keyboard: + out.keyboard.emplace_back(ev.keyboard); + break; + case CCameraScriptedInputEvent::Type::Mouse: + out.mouse.emplace_back(ev.mouse); + break; + case CCameraScriptedInputEvent::Type::Imguizmo: + out.imguizmo.emplace_back(ev.imguizmo); + break; + case CCameraScriptedInputEvent::Type::Action: + out.actions.emplace_back(ev.action); + break; + case CCameraScriptedInputEvent::Type::Goal: + out.goals.emplace_back(ev.goal); + break; + case CCameraScriptedInputEvent::Type::TrackedTargetTransform: + out.trackedTargetTransforms.emplace_back(ev.trackedTargetTransform); + break; + case CCameraScriptedInputEvent::Type::SegmentLabel: + out.segmentLabels.emplace_back(ev.segmentLabel.label); + break; + } + + ++nextEventIndex; } - - ++nextEventIndex; } -} +}; } // namespace nbl::system diff --git a/common/include/camera/CCameraScriptedUiInputUtilities.hpp b/common/include/camera/CCameraScriptedUiInputUtilities.hpp index a447f701e..f5bc8932a 100644 --- a/common/include/camera/CCameraScriptedUiInputUtilities.hpp +++ b/common/include/camera/CCameraScriptedUiInputUtilities.hpp @@ -10,77 +10,80 @@ namespace nbl::ui { -inline SKeyboardEvent makeScriptedKeyboardEvent( - const std::chrono::microseconds timestamp, - IWindow* const window, - const system::CCameraScriptedInputEvent::KeyboardData& authoredKeyboard) +struct CCameraScriptedUiInputUtilities final { - SKeyboardEvent event(timestamp); - event.keyCode = authoredKeyboard.key; - event.action = - authoredKeyboard.action == system::CCameraScriptedInputEvent::KeyboardData::Action::Pressed ? - SKeyboardEvent::ECA_PRESSED : - SKeyboardEvent::ECA_RELEASED; - event.window = window; - return event; -} - -inline bool tryBuildScriptedMouseEvent( - const std::chrono::microseconds timestamp, - IWindow* const window, - const system::CCameraScriptedInputEvent::MouseData& authoredMouse, - SMouseEvent& outEvent) -{ - outEvent = SMouseEvent(timestamp); - outEvent.window = window; - - switch (authoredMouse.type) + static inline SKeyboardEvent makeScriptedKeyboardEvent( + const std::chrono::microseconds timestamp, + IWindow* const window, + const system::CCameraScriptedInputEvent::KeyboardData& authoredKeyboard) { - case system::CCameraScriptedInputEvent::MouseData::Type::Click: - outEvent.type = SMouseEvent::EET_CLICK; - outEvent.clickEvent.mouseButton = authoredMouse.button; - outEvent.clickEvent.action = - authoredMouse.action == system::CCameraScriptedInputEvent::MouseData::ClickAction::Pressed ? - SMouseEvent::SClickEvent::EA_PRESSED : - SMouseEvent::SClickEvent::EA_RELEASED; - outEvent.clickEvent.clickPosX = authoredMouse.x; - outEvent.clickEvent.clickPosY = authoredMouse.y; - return true; - case system::CCameraScriptedInputEvent::MouseData::Type::Scroll: - outEvent.type = SMouseEvent::EET_SCROLL; - outEvent.scrollEvent.verticalScroll = authoredMouse.v; - outEvent.scrollEvent.horizontalScroll = authoredMouse.h; - return true; - case system::CCameraScriptedInputEvent::MouseData::Type::Movement: - outEvent.type = SMouseEvent::EET_MOVEMENT; - outEvent.movementEvent.relativeMovementX = authoredMouse.dx; - outEvent.movementEvent.relativeMovementY = authoredMouse.dy; - return true; - default: - return false; + SKeyboardEvent event(timestamp); + event.keyCode = authoredKeyboard.key; + event.action = + authoredKeyboard.action == system::CCameraScriptedInputEvent::KeyboardData::Action::Pressed ? + SKeyboardEvent::ECA_PRESSED : + SKeyboardEvent::ECA_RELEASED; + event.window = window; + return event; } -} -inline void appendScriptedUiInputEvents( - const std::chrono::microseconds timestamp, - IWindow* const window, - const std::vector& authoredKeyboard, - const std::vector& authoredMouse, - std::vector& outKeyboard, - std::vector& outMouse) -{ - outKeyboard.reserve(outKeyboard.size() + authoredKeyboard.size()); - for (const auto& keyboardEvent : authoredKeyboard) - outKeyboard.emplace_back(makeScriptedKeyboardEvent(timestamp, window, keyboardEvent)); + static inline bool tryBuildScriptedMouseEvent( + const std::chrono::microseconds timestamp, + IWindow* const window, + const system::CCameraScriptedInputEvent::MouseData& authoredMouse, + SMouseEvent& outEvent) + { + outEvent = SMouseEvent(timestamp); + outEvent.window = window; + + switch (authoredMouse.type) + { + case system::CCameraScriptedInputEvent::MouseData::Type::Click: + outEvent.type = SMouseEvent::EET_CLICK; + outEvent.clickEvent.mouseButton = authoredMouse.button; + outEvent.clickEvent.action = + authoredMouse.action == system::CCameraScriptedInputEvent::MouseData::ClickAction::Pressed ? + SMouseEvent::SClickEvent::EA_PRESSED : + SMouseEvent::SClickEvent::EA_RELEASED; + outEvent.clickEvent.clickPosX = authoredMouse.x; + outEvent.clickEvent.clickPosY = authoredMouse.y; + return true; + case system::CCameraScriptedInputEvent::MouseData::Type::Scroll: + outEvent.type = SMouseEvent::EET_SCROLL; + outEvent.scrollEvent.verticalScroll = authoredMouse.v; + outEvent.scrollEvent.horizontalScroll = authoredMouse.h; + return true; + case system::CCameraScriptedInputEvent::MouseData::Type::Movement: + outEvent.type = SMouseEvent::EET_MOVEMENT; + outEvent.movementEvent.relativeMovementX = authoredMouse.dx; + outEvent.movementEvent.relativeMovementY = authoredMouse.dy; + return true; + default: + return false; + } + } - outMouse.reserve(outMouse.size() + authoredMouse.size()); - for (const auto& mouseEvent : authoredMouse) + static inline void appendScriptedUiInputEvents( + const std::chrono::microseconds timestamp, + IWindow* const window, + const std::vector& authoredKeyboard, + const std::vector& authoredMouse, + std::vector& outKeyboard, + std::vector& outMouse) { - SMouseEvent builtEvent(timestamp); - if (tryBuildScriptedMouseEvent(timestamp, window, mouseEvent, builtEvent)) - outMouse.emplace_back(builtEvent); + outKeyboard.reserve(outKeyboard.size() + authoredKeyboard.size()); + for (const auto& keyboardEvent : authoredKeyboard) + outKeyboard.emplace_back(makeScriptedKeyboardEvent(timestamp, window, keyboardEvent)); + + outMouse.reserve(outMouse.size() + authoredMouse.size()); + for (const auto& mouseEvent : authoredMouse) + { + SMouseEvent builtEvent(timestamp); + if (tryBuildScriptedMouseEvent(timestamp, window, mouseEvent, builtEvent)) + outMouse.emplace_back(builtEvent); + } } -} +}; } // namespace nbl::ui diff --git a/common/include/camera/CCameraSequenceScript.hpp b/common/include/camera/CCameraSequenceScript.hpp index 0b19a2149..50ddd83cd 100644 --- a/common/include/camera/CCameraSequenceScript.hpp +++ b/common/include/camera/CCameraSequenceScript.hpp @@ -304,14 +304,14 @@ struct CCameraSequenceScriptUtilities final outPreset.identifier = reference.identifier; if (outPreset.name.empty()) outPreset.name = reference.name; - return CCameraGoalUtilities::isGoalFinite(makeGoalFromPreset(outPreset)); + return CCameraGoalUtilities::isGoalFinite(CCameraPresetUtilities::makeGoalFromPreset(outPreset)); } outPreset = reference; if (!authored.hasDelta) return true; - auto goal = makeGoalFromPreset(reference); + auto goal = CCameraPresetUtilities::makeGoalFromPreset(reference); const auto& delta = authored.delta; const bool hasPoseDelta = delta.hasPositionOffset || delta.hasRotationEulerDegOffset; @@ -423,7 +423,7 @@ struct CCameraSequenceScriptUtilities final return false; } - assignGoalToPreset(outPreset, goal); + CCameraPresetUtilities::assignGoalToPreset(outPreset, goal); return true; } diff --git a/common/include/camera/CCameraSequenceScriptedBuilder.hpp b/common/include/camera/CCameraSequenceScriptedBuilder.hpp index ef2e85e2f..fae12d6a0 100644 --- a/common/include/camera/CCameraSequenceScriptedBuilder.hpp +++ b/common/include/camera/CCameraSequenceScriptedBuilder.hpp @@ -33,92 +33,98 @@ struct CCameraSequenceScriptedSegmentBuildInfo bool includeFollowTargetLock = false; }; -//! Append one compiled segment as expanded scripted runtime payloads. -inline bool appendCompiledSequenceSegmentToScriptedTimeline( - CCameraScriptedTimeline& timeline, - const uint64_t baseFrame, - const core::CCameraSequenceCompiledSegment& compiledSegment, - const CCameraSequenceScriptedSegmentBuildInfo& buildInfo, - std::string* error = nullptr) +struct CCameraSequenceScriptedBuilderUtilities final { - std::vector framePolicies; - if (!core::CCameraSequenceScriptUtilities::buildCompiledSegmentFramePolicies(compiledSegment, framePolicies, buildInfo.includeFollowTargetLock)) + //! Append one compiled segment as expanded scripted runtime payloads. + static inline bool appendCompiledSequenceSegmentToScriptedTimeline( + CCameraScriptedTimeline& timeline, + const uint64_t baseFrame, + const core::CCameraSequenceCompiledSegment& compiledSegment, + const CCameraSequenceScriptedSegmentBuildInfo& buildInfo, + std::string* error = nullptr) { - if (error) - *error = "Failed to build compiled frame policies."; - return false; - } - - appendScriptedSegmentLabelEvent(timeline, baseFrame, compiledSegment.name); - appendScriptedActionEvent(timeline, baseFrame, CCameraScriptedInputEvent::ActionData::Kind::SetActiveRenderWindow, 0); - appendScriptedActionEvent(timeline, baseFrame, CCameraScriptedInputEvent::ActionData::Kind::SetActivePlanar, static_cast(buildInfo.planarIx)); - if (!compiledSegment.presentations.empty()) - { - appendScriptedActionEvent(timeline, baseFrame, CCameraScriptedInputEvent::ActionData::Kind::SetProjectionType, static_cast(compiledSegment.presentations[0].projection)); - appendScriptedActionEvent(timeline, baseFrame, CCameraScriptedInputEvent::ActionData::Kind::SetLeftHanded, compiledSegment.presentations[0].leftHanded ? 1 : 0); - } - if (compiledSegment.resetCamera) - appendScriptedActionEvent(timeline, baseFrame, CCameraScriptedInputEvent::ActionData::Kind::ResetActiveCamera, 1); - - if (buildInfo.useWindow) - { - for (size_t windowIx = 1u; windowIx < std::min(compiledSegment.presentations.size(), buildInfo.availableWindowCount); ++windowIx) + std::vector framePolicies; + if (!core::CCameraSequenceScriptUtilities::buildCompiledSegmentFramePolicies(compiledSegment, framePolicies, buildInfo.includeFollowTargetLock)) { - appendScriptedActionEvent(timeline, baseFrame, CCameraScriptedInputEvent::ActionData::Kind::SetActiveRenderWindow, static_cast(windowIx)); - appendScriptedActionEvent(timeline, baseFrame, CCameraScriptedInputEvent::ActionData::Kind::SetActivePlanar, static_cast(buildInfo.planarIx)); - appendScriptedActionEvent(timeline, baseFrame, CCameraScriptedInputEvent::ActionData::Kind::SetProjectionType, static_cast(compiledSegment.presentations[windowIx].projection)); - appendScriptedActionEvent(timeline, baseFrame, CCameraScriptedInputEvent::ActionData::Kind::SetLeftHanded, compiledSegment.presentations[windowIx].leftHanded ? 1 : 0); + if (error) + *error = "Failed to build compiled frame policies."; + return false; } - appendScriptedActionEvent(timeline, baseFrame, CCameraScriptedInputEvent::ActionData::Kind::SetActiveRenderWindow, 0); - } - for (const auto& policy : framePolicies) - { - core::CCameraPreset preset; - if (!core::CCameraKeyframeTrackUtilities::tryBuildKeyframeTrackPresetAtTime(compiledSegment.track, policy.sampleTime, preset)) + CCameraScriptedRuntimeUtilities::appendScriptedSegmentLabelEvent(timeline, baseFrame, compiledSegment.name); + CCameraScriptedRuntimeUtilities::appendScriptedActionEvent(timeline, baseFrame, CCameraScriptedInputEvent::ActionData::Kind::SetActiveRenderWindow, 0); + CCameraScriptedRuntimeUtilities::appendScriptedActionEvent(timeline, baseFrame, CCameraScriptedInputEvent::ActionData::Kind::SetActivePlanar, static_cast(buildInfo.planarIx)); + if (!compiledSegment.presentations.empty()) { - if (error) - *error = "Failed to sample compiled segment track."; - return false; + CCameraScriptedRuntimeUtilities::appendScriptedActionEvent(timeline, baseFrame, CCameraScriptedInputEvent::ActionData::Kind::SetProjectionType, static_cast(compiledSegment.presentations[0].projection)); + CCameraScriptedRuntimeUtilities::appendScriptedActionEvent(timeline, baseFrame, CCameraScriptedInputEvent::ActionData::Kind::SetLeftHanded, compiledSegment.presentations[0].leftHanded ? 1 : 0); } - appendScriptedGoalEvent(timeline, baseFrame + policy.frameOffset, makeGoalFromPreset(preset)); + if (compiledSegment.resetCamera) + CCameraScriptedRuntimeUtilities::appendScriptedActionEvent(timeline, baseFrame, CCameraScriptedInputEvent::ActionData::Kind::ResetActiveCamera, 1); - if (compiledSegment.usesTrackedTargetTrack()) + if (buildInfo.useWindow) { - core::CCameraSequenceTrackedTargetPose trackedTargetPose; - if (!core::CCameraSequenceScriptUtilities::tryBuildSequenceTrackedTargetPoseAtTime(compiledSegment.trackedTargetTrack, policy.sampleTime, trackedTargetPose)) + for (size_t windowIx = 1u; windowIx < std::min(compiledSegment.presentations.size(), buildInfo.availableWindowCount); ++windowIx) { - if (error) - *error = "Failed to sample compiled tracked-target track."; - return false; + CCameraScriptedRuntimeUtilities::appendScriptedActionEvent(timeline, baseFrame, CCameraScriptedInputEvent::ActionData::Kind::SetActiveRenderWindow, static_cast(windowIx)); + CCameraScriptedRuntimeUtilities::appendScriptedActionEvent(timeline, baseFrame, CCameraScriptedInputEvent::ActionData::Kind::SetActivePlanar, static_cast(buildInfo.planarIx)); + CCameraScriptedRuntimeUtilities::appendScriptedActionEvent(timeline, baseFrame, CCameraScriptedInputEvent::ActionData::Kind::SetProjectionType, static_cast(compiledSegment.presentations[windowIx].projection)); + CCameraScriptedRuntimeUtilities::appendScriptedActionEvent(timeline, baseFrame, CCameraScriptedInputEvent::ActionData::Kind::SetLeftHanded, compiledSegment.presentations[windowIx].leftHanded ? 1 : 0); } - - core::ICamera::CGimbal gimbal({ .position = trackedTargetPose.position, .orientation = trackedTargetPose.orientation }); - appendScriptedTrackedTargetTransformEvent(timeline, baseFrame + policy.frameOffset, gimbal.operator()()); + CCameraScriptedRuntimeUtilities::appendScriptedActionEvent(timeline, baseFrame, CCameraScriptedInputEvent::ActionData::Kind::SetActiveRenderWindow, 0); } - if (policy.baseline) - appendScriptedBaselineCheck(timeline, baseFrame + policy.frameOffset); - if (policy.continuityStep) + for (const auto& policy : framePolicies) { - appendScriptedGimbalStepCheck( + core::CCameraPreset preset; + if (!core::CCameraKeyframeTrackUtilities::tryBuildKeyframeTrackPresetAtTime(compiledSegment.track, policy.sampleTime, preset)) + { + if (error) + *error = "Failed to sample compiled segment track."; + return false; + } + CCameraScriptedRuntimeUtilities::appendScriptedGoalEvent( timeline, baseFrame + policy.frameOffset, - compiledSegment.continuity.hasPosDeltaConstraint, - compiledSegment.continuity.maxPosDelta, - compiledSegment.continuity.minPosDelta, - compiledSegment.continuity.hasEulerDeltaConstraint, - compiledSegment.continuity.maxEulerDeltaDeg, - compiledSegment.continuity.minEulerDeltaDeg); + core::CCameraPresetUtilities::makeGoalFromPreset(preset)); + + if (compiledSegment.usesTrackedTargetTrack()) + { + core::CCameraSequenceTrackedTargetPose trackedTargetPose; + if (!core::CCameraSequenceScriptUtilities::tryBuildSequenceTrackedTargetPoseAtTime(compiledSegment.trackedTargetTrack, policy.sampleTime, trackedTargetPose)) + { + if (error) + *error = "Failed to sample compiled tracked-target track."; + return false; + } + + core::ICamera::CGimbal gimbal({ .position = trackedTargetPose.position, .orientation = trackedTargetPose.orientation }); + CCameraScriptedRuntimeUtilities::appendScriptedTrackedTargetTransformEvent(timeline, baseFrame + policy.frameOffset, gimbal.operator()()); + } + + if (policy.baseline) + CCameraScriptedRuntimeUtilities::appendScriptedBaselineCheck(timeline, baseFrame + policy.frameOffset); + if (policy.continuityStep) + { + 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) + CCameraScriptedRuntimeUtilities::appendScriptedFollowTargetLockCheck(timeline, baseFrame + policy.frameOffset); + if (policy.capture) + timeline.captureFrames.emplace_back(baseFrame + policy.frameOffset); } - if (policy.followTargetLock) - appendScriptedFollowTargetLockCheck(timeline, baseFrame + policy.frameOffset); - if (policy.capture) - timeline.captureFrames.emplace_back(baseFrame + policy.frameOffset); - } - return true; -} + return true; + } +}; } // namespace nbl::system diff --git a/common/include/camera/CCameraSmokeRegressionUtilities.hpp b/common/include/camera/CCameraSmokeRegressionUtilities.hpp index 863300e71..6931c5f35 100644 --- a/common/include/camera/CCameraSmokeRegressionUtilities.hpp +++ b/common/include/camera/CCameraSmokeRegressionUtilities.hpp @@ -80,7 +80,7 @@ struct CCameraSmokeRegressionUtilities final core::ICamera* camera, const core::CCameraPreset& preset) { - return core::comparePresetToCameraState( + return core::CCameraPresetFlowUtilities::comparePresetToCameraState( solver, camera, preset, @@ -94,7 +94,7 @@ struct CCameraSmokeRegressionUtilities final core::ICamera* camera, const core::CCameraPreset& preset) { - return core::comparePresetToCameraState( + return core::CCameraPresetFlowUtilities::comparePresetToCameraState( solver, camera, preset, diff --git a/common/include/camera/CChaseCamera.hpp b/common/include/camera/CChaseCamera.hpp index 0c389156f..f9a1ab211 100644 --- a/common/include/camera/CChaseCamera.hpp +++ b/common/include/camera/CChaseCamera.hpp @@ -17,7 +17,7 @@ class CChaseCamera final : public CSphericalTargetCamera CChaseCamera(const hlsl::float64_t3& position, const hlsl::float64_t3& target) : base_t(position, target) { - m_v = std::clamp(m_v, MinPitch, MaxPitch); + m_orbitUv.y = std::clamp(m_orbitUv.y, MinPitch, MaxPitch); applyPose(); } ~CChaseCamera() = default; @@ -35,7 +35,7 @@ class CChaseCamera final : public CSphericalTargetCamera const auto deltaTranslation = scaleVirtualTranslation(impulse.dVirtualTranslate); const auto deltaDistance = scaleUnscaledVirtualTranslation(impulse.dVirtualTranslate.y); - const auto basis = computeBasis(m_u, m_v, m_distance); + const auto basis = computeBasis(m_orbitUv, m_distance); const auto planarForward = hlsl::safeNormalizeVec3( hlsl::float64_t3(basis.forward.x, 0.0, basis.forward.z), @@ -47,8 +47,8 @@ class CChaseCamera final : public CSphericalTargetCamera m_targetPosition += planarRight * deltaTranslation.x + planarForward * deltaTranslation.z; m_distance = std::clamp(m_distance + static_cast(deltaDistance), MinDistance, MaxDistance); - m_u += deltaRotation.y; - m_v = std::clamp(m_v + deltaRotation.x, MinPitch, MaxPitch); + m_orbitUv.x += deltaRotation.y; + m_orbitUv.y = std::clamp(m_orbitUv.y + deltaRotation.x, MinPitch, MaxPitch); return applyPose(); } diff --git a/common/include/camera/CDollyCamera.hpp b/common/include/camera/CDollyCamera.hpp index e50d3e83c..5ed2be2be 100644 --- a/common/include/camera/CDollyCamera.hpp +++ b/common/include/camera/CDollyCamera.hpp @@ -17,7 +17,7 @@ class CDollyCamera final : public CSphericalTargetCamera CDollyCamera(const hlsl::float64_t3& position, const hlsl::float64_t3& target) : base_t(position, target) { - m_v = std::clamp(m_v, MinPitch, MaxPitch); + m_orbitUv.y = std::clamp(m_orbitUv.y, MinPitch, MaxPitch); applyPose(); } ~CDollyCamera() = default; @@ -34,12 +34,12 @@ class CDollyCamera final : public CSphericalTargetCamera const auto deltaRotation = scaleVirtualRotation(impulse.dVirtualRotation); const auto deltaTranslation = scaleVirtualTranslation(impulse.dVirtualTranslate); - const auto basis = computeBasis(m_u, m_v, m_distance); + const auto basis = computeBasis(m_orbitUv, m_distance); const auto delta = hlsl::transformLocalVectorToWorldBasis(deltaTranslation, basis.right, basis.up, basis.forward); m_targetPosition += delta; - m_u += deltaRotation.y; - m_v = std::clamp(m_v + deltaRotation.x, MinPitch, MaxPitch); + m_orbitUv.x += deltaRotation.y; + m_orbitUv.y = std::clamp(m_orbitUv.y + deltaRotation.x, MinPitch, MaxPitch); return applyPose(); } diff --git a/common/include/camera/CDollyZoomCamera.hpp b/common/include/camera/CDollyZoomCamera.hpp index 293be07cd..4cfe41170 100644 --- a/common/include/camera/CDollyZoomCamera.hpp +++ b/common/include/camera/CDollyZoomCamera.hpp @@ -47,8 +47,7 @@ class CDollyZoomCamera final : public CSphericalTargetCamera const auto deltaTranslation = scaleVirtualTranslation(impulse.dVirtualTranslate); const double deltaDistance = scaleUnscaledVirtualTranslation(impulse.dVirtualTranslate.z); - m_u += deltaTranslation.y; - m_v += deltaTranslation.x; + m_orbitUv += hlsl::float64_t2(deltaTranslation.y, deltaTranslation.x); m_distance = std::clamp(m_distance + static_cast(deltaDistance), MinDistance, MaxDistance); return applyPose(); diff --git a/common/include/camera/CIsometricCamera.hpp b/common/include/camera/CIsometricCamera.hpp index e35b9fc38..270c23d58 100644 --- a/common/include/camera/CIsometricCamera.hpp +++ b/common/include/camera/CIsometricCamera.hpp @@ -17,8 +17,7 @@ class CIsometricCamera final : public CSphericalTargetCamera CIsometricCamera(const hlsl::float64_t3& position, const hlsl::float64_t3& target) : base_t(position, target) { - m_u = IsoYaw; - m_v = IsoPitch; + m_orbitUv = hlsl::float64_t2(IsoYaw, IsoPitch); applyPose(); } ~CIsometricCamera() = default; @@ -35,11 +34,10 @@ class CIsometricCamera final : public CSphericalTargetCamera const auto deltaTranslation = scaleVirtualTranslation(impulse.dVirtualTranslate); const double deltaDistance = scaleUnscaledVirtualTranslation(impulse.dVirtualTranslate.z); - m_u = IsoYaw; - m_v = IsoPitch; + m_orbitUv = hlsl::float64_t2(IsoYaw, IsoPitch); m_distance = std::clamp(m_distance + static_cast(deltaDistance), MinDistance, MaxDistance); - const auto basis = computeBasis(m_u, m_v, m_distance); + const auto basis = computeBasis(m_orbitUv, m_distance); applyPlanarTargetTranslation(deltaTranslation, basis); return applyPose(); diff --git a/common/include/camera/COrbitCamera.hpp b/common/include/camera/COrbitCamera.hpp index fdee73196..74083046b 100644 --- a/common/include/camera/COrbitCamera.hpp +++ b/common/include/camera/COrbitCamera.hpp @@ -29,8 +29,7 @@ class COrbitCamera final : public CSphericalTargetCamera const auto deltaTranslation = scaleVirtualTranslation(impulse.dVirtualTranslate); const double deltaDistance = scaleUnscaledVirtualTranslation(impulse.dVirtualTranslate.z); - m_u += deltaTranslation.y; - m_v += deltaTranslation.x; + m_orbitUv += hlsl::float64_t2(deltaTranslation.y, deltaTranslation.x); m_distance = std::clamp(m_distance + static_cast(deltaDistance), MinDistance, MaxDistance); diff --git a/common/include/camera/CPathCamera.hpp b/common/include/camera/CPathCamera.hpp index faf6272f0..9dbc29315 100644 --- a/common/include/camera/CPathCamera.hpp +++ b/common/include/camera/CPathCamera.hpp @@ -96,8 +96,7 @@ class CPathCamera final : public CSphericalTargetCamera } m_distance = canonicalPathState.targetRelative.distance; - m_u = canonicalPathState.targetRelative.orbitUv.x; - m_v = canonicalPathState.targetRelative.orbitUv.y; + m_orbitUv = canonicalPathState.targetRelative.orbitUv; m_gimbal.begin(); { diff --git a/common/include/camera/CSphericalTargetCamera.hpp b/common/include/camera/CSphericalTargetCamera.hpp index 62317d36f..261d28e2d 100644 --- a/common/include/camera/CSphericalTargetCamera.hpp +++ b/common/include/camera/CSphericalTargetCamera.hpp @@ -10,7 +10,7 @@ namespace nbl::core /** * Common base for cameras orbiting or tracking a target with spherical coordinates. * -* The shared state is target position, distance, and orbit angles `u/v`. +* The shared state is target position, distance, and orbit angles stored in `orbitUv`. */ class CSphericalTargetCamera : public ICamera { @@ -46,8 +46,9 @@ class CSphericalTargetCamera : public ICamera inline hlsl::float64_t3 getTarget() const { return m_targetPosition; } inline float getDistance() const { return m_distance; } - inline double getU() const { return m_u; } - inline double getV() const { return m_v; } + inline double getU() const { return m_orbitUv.x; } + inline double getV() const { return m_orbitUv.y; } + inline const hlsl::float64_t2& getOrbitUv() const { return m_orbitUv; } static inline constexpr float MinDistance = base_t::SphericalMinDistance; static inline constexpr float MaxDistance = base_t::SphericalMaxDistance; @@ -61,7 +62,7 @@ class CSphericalTargetCamera : public ICamera { out.target = m_targetPosition; out.distance = m_distance; - out.orbitUv = hlsl::float64_t2(m_u, m_v); + out.orbitUv = m_orbitUv; out.minDistance = MinDistance; out.maxDistance = MaxDistance; return true; @@ -81,12 +82,12 @@ class CSphericalTargetCamera : public ICamera protected: using SphericalBasis = SCameraTargetRelativeBasis; - inline SphericalBasis computeBasis(double orbitU, double orbitV, float distance) const + inline SphericalBasis computeBasis(const hlsl::float64_t2& orbitUv, float distance) const { SphericalBasis basis; const SCameraTargetRelativeState state = { .target = m_targetPosition, - .orbitUv = hlsl::float64_t2(orbitU, orbitV), + .orbitUv = orbitUv, .distance = distance }; if (!CCameraTargetRelativeUtilities::tryBuildTargetRelativeBasis(state, MinDistance, MaxDistance, basis)) @@ -100,14 +101,12 @@ class CSphericalTargetCamera : public ICamera if (!CCameraTargetRelativeUtilities::tryBuildTargetRelativeStateFromPosition(m_targetPosition, position, MinDistance, MaxDistance, state)) { m_distance = MinDistance; - m_u = 0.0; - m_v = 0.0; + m_orbitUv = hlsl::float64_t2(0.0); return; } m_distance = state.distance; - m_u = state.orbitUv.x; - m_v = state.orbitUv.y; + m_orbitUv = state.orbitUv; } inline void applyPlanarTargetTranslation(const hlsl::float64_t3& deltaTranslation, const SphericalBasis& basis) @@ -126,7 +125,7 @@ class CSphericalTargetCamera : public ICamera { const SCameraTargetRelativeState state = { .target = m_targetPosition, - .orbitUv = hlsl::float64_t2(m_u, m_v), + .orbitUv = m_orbitUv, .distance = m_distance }; SCameraTargetRelativePose pose = {}; @@ -151,8 +150,7 @@ class CSphericalTargetCamera : public ICamera hlsl::float64_t3 m_targetPosition; float m_distance; typename base_t::CGimbal m_gimbal; - double m_u = {}; - double m_v = {}; + hlsl::float64_t2 m_orbitUv = hlsl::float64_t2(0.0); }; } diff --git a/common/include/camera/CTopDownCamera.hpp b/common/include/camera/CTopDownCamera.hpp index f46caeac5..a8cc20fed 100644 --- a/common/include/camera/CTopDownCamera.hpp +++ b/common/include/camera/CTopDownCamera.hpp @@ -17,7 +17,7 @@ class CTopDownCamera final : public CSphericalTargetCamera CTopDownCamera(const hlsl::float64_t3& position, const hlsl::float64_t3& target) : base_t(position, target) { - m_v = TopDownPitch; + m_orbitUv.y = TopDownPitch; applyPose(); } ~CTopDownCamera() = default; @@ -35,11 +35,11 @@ class CTopDownCamera final : public CSphericalTargetCamera const auto deltaTranslation = scaleVirtualTranslation(impulse.dVirtualTranslate); const double deltaDistance = scaleUnscaledVirtualTranslation(impulse.dVirtualTranslate.z); - m_u += deltaRotation.y; - m_v = TopDownPitch; + m_orbitUv.x += deltaRotation.y; + m_orbitUv.y = TopDownPitch; m_distance = std::clamp(m_distance + static_cast(deltaDistance), MinDistance, MaxDistance); - const auto basis = computeBasis(m_u, m_v, m_distance); + const auto basis = computeBasis(m_orbitUv, m_distance); applyPlanarTargetTranslation(deltaTranslation, basis); return applyPose(); diff --git a/common/include/camera/CTurntableCamera.hpp b/common/include/camera/CTurntableCamera.hpp index 8176f579e..315526dd6 100644 --- a/common/include/camera/CTurntableCamera.hpp +++ b/common/include/camera/CTurntableCamera.hpp @@ -21,7 +21,7 @@ class CTurntableCamera final : public CSphericalTargetCamera CTurntableCamera(const hlsl::float64_t3& position, const hlsl::float64_t3& target) : base_t(position, target) { - m_v = std::clamp(m_v, MinPitch, MaxPitch); + m_orbitUv.y = std::clamp(m_orbitUv.y, MinPitch, MaxPitch); applyPose(); } ~CTurntableCamera() = default; @@ -39,8 +39,8 @@ class CTurntableCamera final : public CSphericalTargetCamera const double deltaPitch = scaleVirtualRotation(impulse.dVirtualRotation.x); const double deltaDistance = scaleUnscaledVirtualTranslation(impulse.dVirtualTranslate.z); - m_u += deltaYaw; - m_v = std::clamp(m_v + deltaPitch, MinPitch, MaxPitch); + m_orbitUv.x += deltaYaw; + m_orbitUv.y = std::clamp(m_orbitUv.y + deltaPitch, MinPitch, MaxPitch); m_distance = std::clamp(m_distance + static_cast(deltaDistance), MinDistance, MaxDistance); return applyPose(); diff --git a/common/include/camera/README.md b/common/include/camera/README.md index 57367e418..3271c3e45 100644 --- a/common/include/camera/README.md +++ b/common/include/camera/README.md @@ -487,9 +487,9 @@ auto capture = solver.captureDetailed(camera.get()); if (capture.canUseGoal()) { CCameraPreset preset; - assignGoalToPreset(preset, capture.goal); + CCameraPresetUtilities::assignGoalToPreset(preset, capture.goal); - auto apply = applyPresetDetailed(solver, camera.get(), preset); + auto apply = CCameraPresetFlowUtilities::applyPresetDetailed(solver, camera.get(), preset); if (!apply.succeeded()) { // report exact vs best-effort or unsupported state @@ -518,16 +518,16 @@ CCameraSequenceScript script = ...; CCameraSequenceCompiledSegment segment = ...; CCameraScriptedTimeline timeline; -appendCompiledSequenceSegmentToScriptedTimeline( +CCameraSequenceScriptedBuilderUtilities::appendCompiledSequenceSegmentToScriptedTimeline( timeline, baseFrame, segment, buildInfo); -finalizeScriptedTimeline(timeline); +CCameraScriptedRuntimeUtilities::finalizeScriptedTimeline(timeline); CCameraScriptedCheckRuntimeState state = {}; -auto frameResult = evaluateScriptedChecksForFrame( +auto frameResult = CCameraScriptedCheckRunnerUtilities::evaluateScriptedChecksForFrame( timeline.checks, state, context); diff --git a/common/src/nbl/examples/camera/CCameraJsonPersistenceUtilities.hpp b/common/src/nbl/examples/camera/CCameraJsonPersistenceUtilities.hpp index 7189f259c..f3dda6942 100644 --- a/common/src/nbl/examples/camera/CCameraJsonPersistenceUtilities.hpp +++ b/common/src/nbl/examples/camera/CCameraJsonPersistenceUtilities.hpp @@ -89,7 +89,7 @@ inline void deserializePresetJson(const Json& entry, core::CCameraPreset& preset core::CCameraGoal goal = {}; deserializeGoalJson(entry, goal); - core::assignGoalToPreset(preset, goal); + core::CCameraPresetUtilities::assignGoalToPreset(preset, goal); } } // namespace nbl::system diff --git a/common/src/nbl/examples/camera/CCameraPersistence.cpp b/common/src/nbl/examples/camera/CCameraPersistence.cpp index 2709b3bb0..b253e14b1 100644 --- a/common/src/nbl/examples/camera/CCameraPersistence.cpp +++ b/common/src/nbl/examples/camera/CCameraPersistence.cpp @@ -55,7 +55,7 @@ json_t serializeGoalJson(const nbl::core::CCameraGoal& goal) json_t serializePresetJson(const nbl::core::CCameraPreset& preset) { - auto json = serializeGoalJson(nbl::core::makeGoalFromPreset(preset)); + auto json = serializeGoalJson(nbl::core::CCameraPresetUtilities::makeGoalFromPreset(preset)); json["name"] = preset.name; json["identifier"] = preset.identifier; return json; diff --git a/common/src/nbl/examples/camera/CCameraScriptedRuntimePersistence.cpp b/common/src/nbl/examples/camera/CCameraScriptedRuntimePersistence.cpp index c66308ee2..489e63ae2 100644 --- a/common/src/nbl/examples/camera/CCameraScriptedRuntimePersistence.cpp +++ b/common/src/nbl/examples/camera/CCameraScriptedRuntimePersistence.cpp @@ -1138,7 +1138,7 @@ bool readCameraScriptedInput(std::istream& in, CCameraScriptedInputParseResult& parseScriptedInputEventsJson(script, out); parseScriptedChecksJson(script, out); - finalizeScriptedTimeline(out.timeline); + CCameraScriptedRuntimeUtilities::finalizeScriptedTimeline(out.timeline); return true; } From 84aab631597fac141f221abf1f56633c9408b781 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Thu, 9 Apr 2026 12:11:17 +0200 Subject: [PATCH 186/205] Refactor camera math utilities --- 61_UI/AppCameraConfigJsonParsing.cpp | 2 +- 61_UI/AppControlPanelCameraTab.cpp | 2 +- 61_UI/AppControlPanelStatusTab.cpp | 2 +- 61_UI/AppHeadlessCameraSmokeChecks.inl | 9 +- 61_UI/AppHeadlessCameraSmokeHelpers.inl | 33 +- 61_UI/AppScriptedInputRuntime.cpp | 2 +- 61_UI/AppTransformEditor.cpp | 2 +- 61_UI/include/app/AppGizmoUtilities.hpp | 8 +- 61_UI/include/app/AppTypes.hpp | 6 +- 61_UI/include/common.hpp | 9 +- .../CCameraFollowRegressionUtilities.hpp | 11 +- .../include/camera/CCameraFollowUtilities.hpp | 19 +- common/include/camera/CCameraGoal.hpp | 43 +- common/include/camera/CCameraGoalSolver.hpp | 27 +- .../camera/CCameraManipulationUtilities.hpp | 7 +- .../include/camera/CCameraMathUtilities.hpp | 1551 +++++++++-------- .../include/camera/CCameraPathUtilities.hpp | 31 +- .../camera/CCameraScriptedCheckRunner.hpp | 16 +- .../include/camera/CCameraSequenceScript.hpp | 21 +- .../CCameraSmokeRegressionUtilities.hpp | 8 +- .../camera/CCameraTargetRelativeUtilities.hpp | 13 +- .../camera/CCameraVirtualEventUtilities.hpp | 11 +- common/include/camera/CChaseCamera.hpp | 4 +- common/include/camera/CDollyCamera.hpp | 2 +- common/include/camera/CDollyZoomCamera.hpp | 3 +- common/include/camera/CFPSCamera.hpp | 19 +- common/include/camera/CFreeLockCamera.hpp | 12 +- .../include/camera/CSphericalTargetCamera.hpp | 7 +- common/include/camera/IGimbal.hpp | 18 +- .../include/camera/IGimbalInputProcessor.hpp | 4 +- .../CCameraJsonPersistenceUtilities.hpp | 2 +- .../CCameraScriptedRuntimePersistence.cpp | 4 +- 32 files changed, 959 insertions(+), 949 deletions(-) diff --git a/61_UI/AppCameraConfigJsonParsing.cpp b/61_UI/AppCameraConfigJsonParsing.cpp index bd7722173..00b4e6238 100644 --- a/61_UI/AppCameraConfigJsonParsing.cpp +++ b/61_UI/AppCameraConfigJsonParsing.cpp @@ -149,7 +149,7 @@ inline hlsl::float64_t3 readJsonFloat64Vec3(const camera_json_t& json, const std inline hlsl::camera_quaternion_t readJsonQuaternion(const camera_json_t& json, const std::string_view key) { const auto value = readJsonArray(json, key); - return hlsl::makeQuaternionFromComponents(value[0], value[1], value[2], value[3]); + return hlsl::CCameraMathUtilities::makeQuaternionFromComponents(value[0], value[1], value[2], value[3]); } template diff --git a/61_UI/AppControlPanelCameraTab.cpp b/61_UI/AppControlPanelCameraTab.cpp index 0113c8177..4b08da22a 100644 --- a/61_UI/AppControlPanelCameraTab.cpp +++ b/61_UI/AppControlPanelCameraTab.cpp @@ -129,7 +129,7 @@ void App::drawControlPanelCameraTab(const nbl::ui::SCameraControlPanelStyle& pan snapFollowTargetToModel(); ImGui::SameLine(); if (nbl::ui::drawActionButtonWithHint("Target origin", "Reset tracked target to identity at world origin")) - m_sceneInteraction.followTarget.setPose(float64_t3(0.0), makeIdentityQuaternion()); + m_sceneInteraction.followTarget.setPose(float64_t3(0.0), CCameraMathUtilities::makeIdentityQuaternion()); ImGui::SameLine(); if (nbl::ui::drawActionButtonWithHint("Capture current offset", "Store current camera-to-target relation into the active follow config")) captureFollowOffsetsForPlanar(getActivePlanarIx()); diff --git a/61_UI/AppControlPanelStatusTab.cpp b/61_UI/AppControlPanelStatusTab.cpp index bf4b6338d..eab6455ca 100644 --- a/61_UI/AppControlPanelStatusTab.cpp +++ b/61_UI/AppControlPanelStatusTab.cpp @@ -36,7 +36,7 @@ void App::drawControlPanelStatusTab(const nbl::ui::SCameraControlPanelStyle& pan { const auto& gimbal = activeCamera->getGimbal(); const auto pos = gimbal.getPosition(); - const auto euler = hlsl::getCameraOrientationEulerDegrees(gimbal.getOrientation()); + const auto euler = hlsl::CCameraMathUtilities::getCameraOrientationEulerDegrees(gimbal.getOrientation()); if (nbl::ui::beginCard("CameraCard", nbl::ui::calcCameraControlPanelCardHeight(5, panelStyle), panelStyle.CardTopColor, panelStyle.CardBottomColor, panelStyle.CardBorderColor, panelStyle)) { diff --git a/61_UI/AppHeadlessCameraSmokeChecks.inl b/61_UI/AppHeadlessCameraSmokeChecks.inl index e0462b55a..18b90128c 100644 --- a/61_UI/AppHeadlessCameraSmokeChecks.inl +++ b/61_UI/AppHeadlessCameraSmokeChecks.inl @@ -622,7 +622,7 @@ if (state.initialPresets.free.has_value() && state.freeCamera) { CameraPreset orientedPreset = state.initialPresets.free.value(); - orientedPreset.goal.orientation = hlsl::makeQuaternionFromEulerDegreesYXZ(SCameraSmokeManipulationDefaults::FreeOrientationYawDeg); + 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)) { @@ -653,14 +653,14 @@ const auto remappedPosition = state.freeCamera->getGimbal().getPosition(); const auto positionDelta = remappedPosition - orientedPreset.goal.position; - if (!hlsl::nearlyEqualVec3(positionDelta, SCameraSmokeManipulationDefaults::WorldTranslationDelta, SCameraSmokeUtilityThresholds::PositionWriteback)) + 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::makeQuaternionFromEulerDegreesYXZ(SCameraSmokeManipulationDefaults::FreePitchClampSourceDeg); + pitchPreset.goal.orientation = hlsl::CCameraMathUtilities::makeQuaternionFromEulerDegreesYXZ(SCameraSmokeManipulationDefaults::FreePitchClampSourceDeg); const auto pitchResult = nbl::core::CCameraPresetFlowUtilities::applyPresetDetailed(state.goalSolver, state.freeCamera, pitchPreset); if (!pitchResult.succeeded()) { @@ -680,7 +680,7 @@ return false; } - const auto freeEulerDeg = hlsl::getCameraOrientationEulerDegrees(state.freeCamera->getGimbal().getOrientation()); + 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."; @@ -954,3 +954,4 @@ return true; } } + diff --git a/61_UI/AppHeadlessCameraSmokeHelpers.inl b/61_UI/AppHeadlessCameraSmokeHelpers.inl index dd73c1940..0a7378cf1 100644 --- a/61_UI/AppHeadlessCameraSmokeHelpers.inl +++ b/61_UI/AppHeadlessCameraSmokeHelpers.inl @@ -27,7 +27,7 @@ namespace 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 = - makeQuaternionFromAxisAngle(float64_t3(0.0, 1.0, 0.0), hlsl::radians(35.0)); + 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; @@ -42,10 +42,10 @@ namespace { static inline const float64_t3 InitialTargetPosition = float64_t3(2.25, -0.75, 1.25); static inline const camera_quaternion_t InitialTargetOrientation = - makeQuaternionFromEulerRadians(float64_t3(0.18, -0.22, 0.41)); + 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 = - makeQuaternionFromEulerRadians(float64_t3(-0.12, 0.35, 0.27)); + 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; @@ -643,7 +643,7 @@ namespace ICamera::SphericalTargetState shiftedState; if (!camera->tryGetSphericalTargetState(shiftedState) || - !hlsl::nearlyEqualVec3(shiftedState.target, shiftedPreset.goal.targetPosition, SCameraSmokeUtilityThresholds::PositionWriteback)) + !hlsl::CCameraMathUtilities::nearlyEqualVec3(shiftedState.target, shiftedPreset.goal.targetPosition, SCameraSmokeUtilityThresholds::PositionWriteback)) { outError = "Preset target writeback smoke failed for camera \"" + cameraIdentifier + "\"."; return false; @@ -664,7 +664,7 @@ namespace ICamera::SphericalTargetState restoredState; if (!camera->tryGetSphericalTargetState(restoredState) || - !hlsl::nearlyEqualVec3(restoredState.target, initialPreset.goal.targetPosition, SCameraSmokeUtilityThresholds::PositionWriteback)) + !hlsl::CCameraMathUtilities::nearlyEqualVec3(restoredState.target, initialPreset.goal.targetPosition, SCameraSmokeUtilityThresholds::PositionWriteback)) { outError = "Preset target restore smoke failed for camera \"" + cameraIdentifier + "\"."; return false; @@ -1051,7 +1051,7 @@ namespace const auto markerPosition = getCastedVector(float32_t3(markerTransform[3])); const auto positionDelta = markerPosition - trackedTarget.getGimbal().getPosition(); const auto errorLength = length(positionDelta); - if (hlsl::isFiniteScalar(errorLength) && errorLength <= CameraTinyScalarEpsilon) + if (hlsl::CCameraMathUtilities::isFiniteScalar(errorLength) && errorLength <= CameraTinyScalarEpsilon) return true; outError = std::string("Follow target marker alignment smoke failed for ") + std::string(label) + "."; @@ -1094,7 +1094,7 @@ namespace return false; } - editedPreset.goal.orbitUv.x = hlsl::wrapAngleRad( + 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, @@ -1207,7 +1207,7 @@ namespace return false; } const auto trackedTargetPosition = float64_t3(batch.trackedTargetTransforms.front().transform[3]); - if (!hlsl::nearlyEqualVec3(trackedTargetPosition, SCameraSmokeRuntimeDefaults::TrackedTargetPosition, CameraTinyScalarEpsilon)) + if (!hlsl::CCameraMathUtilities::nearlyEqualVec3(trackedTargetPosition, SCameraSmokeRuntimeDefaults::TrackedTargetPosition, CameraTinyScalarEpsilon)) { if (outError) *outError = "Scripted runtime tracked-target payload smoke failed."; @@ -1313,7 +1313,7 @@ namespace const auto pos = gimbal.getPosition(); const auto orientation = gimbal.getOrientation(); const auto basis = gimbal.getOrthonornalMatrix(); - const auto eulerDeg = hlsl::getCameraOrientationEulerDegrees(gimbal.getOrientation()); + const auto eulerDeg = hlsl::CCameraMathUtilities::getCameraOrientationEulerDegrees(gimbal.getOrientation()); std::ostringstream oss; oss << std::fixed << std::setprecision(6) << "Scripted check runner baseline smoke failed." @@ -1391,8 +1391,8 @@ namespace const auto cameraForward = gimbal.getZAxis(); const auto targetPos = trackedTarget.getGimbal().getPosition(); const auto desiredForward = normalize(targetPos - cameraPos); - camera_quaternion_t desiredOrientation = makeIdentityQuaternion(); - if (!nbl::hlsl::tryBuildLookAtOrientation( + camera_quaternion_t desiredOrientation = hlsl::CCameraMathUtilities::makeIdentityQuaternion(); + if (!nbl::hlsl::CCameraMathUtilities::tryBuildLookAtOrientation( cameraPos, targetPos, float64_t3(0.0, 1.0, 0.0), @@ -1402,13 +1402,13 @@ namespace *outError = "Scripted check runner follow-lock smoke failed to build desired look-at orientation."; return false; } - const auto desiredBasis = getQuaternionBasisMatrix(desiredOrientation); + const auto desiredBasis = hlsl::CCameraMathUtilities::getQuaternionBasisMatrix(desiredOrientation); const auto desiredRight = desiredBasis[0]; const auto desiredUp = desiredBasis[1]; - const auto goalRightVec = normalizeQuaternion(followGoal.orientation).transformVector(float64_t3(1.0, 0.0, 0.0), true); - const auto goalUpVec = normalizeQuaternion(followGoal.orientation).transformVector(float64_t3(0.0, 1.0, 0.0), true); - const auto goalForwardVec = normalizeQuaternion(followGoal.orientation).transformVector(float64_t3(0.0, 0.0, 1.0), true); - const auto goalBasis = getQuaternionBasisMatrix(followGoal.orientation); + 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); @@ -1441,3 +1441,4 @@ namespace return true; } + diff --git a/61_UI/AppScriptedInputRuntime.cpp b/61_UI/AppScriptedInputRuntime.cpp index c8411ea77..bed7bab0f 100644 --- a/61_UI/AppScriptedInputRuntime.cpp +++ b/61_UI/AppScriptedInputRuntime.cpp @@ -7,7 +7,7 @@ void App::logScriptedCameraPose(const char* label, ICamera* camera) const const auto& gimbal = camera->getGimbal(); const auto position = gimbal.getPosition(); - const auto euler = hlsl::getCameraOrientationEulerDegrees(gimbal.getOrientation()); + 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, diff --git a/61_UI/AppTransformEditor.cpp b/61_UI/AppTransformEditor.cpp index bf30b9408..1610a5650 100644 --- a/61_UI/AppTransformEditor.cpp +++ b/61_UI/AppTransformEditor.cpp @@ -85,7 +85,7 @@ void App::TransformEditorContents() auto transformState = nbl::ui::extractRigidTransformComponentsOrDefault(imguizmoModel.outTRS); - float32_t3 matrixRotation = hlsl::getQuaternionEulerDegrees(transformState.orientation); + 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"); diff --git a/61_UI/include/app/AppGizmoUtilities.hpp b/61_UI/include/app/AppGizmoUtilities.hpp index 0dac0de61..e20b1f6c7 100644 --- a/61_UI/include/app/AppGizmoUtilities.hpp +++ b/61_UI/include/app/AppGizmoUtilities.hpp @@ -19,11 +19,11 @@ inline ImGuizmoModelM16InOut makeImGuizmoModel(const float32_t4x4& transform) inline hlsl::SRigidTransformComponents extractRigidTransformComponentsOrDefault(const float32_t4x4& transform) { hlsl::SRigidTransformComponents components = {}; - if (hlsl::tryExtractRigidTransformComponents(transform, 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::makeIdentityQuaternion(); + components.orientation = hlsl::CCameraMathUtilities::makeIdentityQuaternion(); components.scale = SCameraAppTransformEditorUiDefaults::IdentityScale; return components; } @@ -33,9 +33,9 @@ inline float32_t4x4 composeRigidTransform( const hlsl::float32_t3& eulerDegrees, const hlsl::float32_t3& scale) { - return hlsl::composeTransformMatrix( + return hlsl::CCameraMathUtilities::composeTransformMatrix( translation, - hlsl::makeQuaternionFromEulerDegrees(eulerDegrees), + hlsl::CCameraMathUtilities::makeQuaternionFromEulerDegrees(eulerDegrees), scale); } diff --git a/61_UI/include/app/AppTypes.hpp b/61_UI/include/app/AppTypes.hpp index cbcf795ca..1e38a3717 100644 --- a/61_UI/include/app/AppTypes.hpp +++ b/61_UI/include/app/AppTypes.hpp @@ -85,7 +85,7 @@ struct SCameraAppSceneDefaults final 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 = makeIdentityQuaternion(); + static inline const camera_quaternion_t DefaultFollowTargetOrientation = CCameraMathUtilities::makeIdentityQuaternion(); }; inline float32_t3x4 buildFollowTargetMarkerWorldTransform( @@ -95,9 +95,9 @@ inline float32_t3x4 buildFollowTargetMarkerWorldTransform( const auto& targetGimbal = trackedTarget.getGimbal(); const auto position = getCastedVector(targetGimbal.getPosition()); const auto orientation = getCastedVector(targetGimbal.getOrientation().data); - const auto markerTransform = hlsl::composeTransformMatrix( + const auto markerTransform = hlsl::CCameraMathUtilities::composeTransformMatrix( position, - makeQuaternionFromComponents(orientation.x, orientation.y, orientation.z, orientation.w), + CCameraMathUtilities::makeQuaternionFromComponents(orientation.x, orientation.y, orientation.z, orientation.w), float32_t3(markerScale, markerScale, markerScale)); return float32_t3x4(hlsl::transpose(markerTransform)); } diff --git a/61_UI/include/common.hpp b/61_UI/include/common.hpp index fd50514c6..250b7f27d 100644 --- a/61_UI/include/common.hpp +++ b/61_UI/include/common.hpp @@ -188,22 +188,15 @@ using nbl::hlsl::float64_t3; using nbl::hlsl::float64_t4; using nbl::hlsl::float64_t4x4; using nbl::hlsl::uint16_t2; -using nbl::hlsl::getQuaternionEulerDegrees; +using nbl::hlsl::CCameraMathUtilities; using nbl::ui::CCameraInputBindingUtilities; using nbl::ui::CCameraTextUtilities; using nbl::hlsl::getCastedMatrix; using nbl::hlsl::getCastedVector; using nbl::hlsl::getMatrix3x4As4x4; -using nbl::hlsl::getQuaternionBasisMatrix; -using nbl::hlsl::makeQuaternionFromAxisAngle; -using nbl::hlsl::makeQuaternionFromComponents; using nbl::hlsl::concatenateBFollowedByA; -using nbl::hlsl::makeQuaternionFromEulerDegrees; -using nbl::hlsl::makeQuaternionFromEulerRadians; using nbl::hlsl::mul; using nbl::hlsl::camera_quaternion_t; -using nbl::hlsl::makeIdentityQuaternion; -using nbl::hlsl::normalizeQuaternion; using nbl::core::CCameraFollowUtilities; using nbl::core::CCameraProjectionUtilities; diff --git a/common/include/camera/CCameraFollowRegressionUtilities.hpp b/common/include/camera/CCameraFollowRegressionUtilities.hpp index a1a809729..b8c729e5b 100644 --- a/common/include/camera/CCameraFollowRegressionUtilities.hpp +++ b/common/include/camera/CCameraFollowRegressionUtilities.hpp @@ -119,7 +119,7 @@ struct CCameraFollowRegressionUtilities final const auto viewSpace = hlsl::mul(projectionContext.viewMatrix, hlsl::float32_t4(target.x, target.y, target.z, 1.0f)); const auto clipProjection = hlsl::transpose(projectionContext.projectionMatrix); const auto clip = hlsl::mul(clipProjection, viewSpace); - if (!hlsl::isFiniteScalar(clip.x) || !hlsl::isFiniteScalar(clip.y) || !hlsl::isFiniteScalar(clip.z) || !hlsl::isFiniteScalar(clip.w)) + if (!hlsl::CCameraMathUtilities::isFiniteScalar(clip.x) || !hlsl::CCameraMathUtilities::isFiniteScalar(clip.y) || !hlsl::CCameraMathUtilities::isFiniteScalar(clip.z) || !hlsl::CCameraMathUtilities::isFiniteScalar(clip.w)) return false; const auto absW = hlsl::abs(clip.w); @@ -128,7 +128,7 @@ struct CCameraFollowRegressionUtilities final const float invW = 1.0f / clip.w; outMetrics.ndc = hlsl::float32_t2(clip.x, clip.y) * invW; - if (!hlsl::isFiniteScalar(outMetrics.ndc.x) || !hlsl::isFiniteScalar(outMetrics.ndc.y)) + if (!hlsl::CCameraMathUtilities::isFiniteScalar(outMetrics.ndc.x) || !hlsl::CCameraMathUtilities::isFiniteScalar(outMetrics.ndc.y)) return false; outMetrics.radius = hlsl::length(outMetrics.ndc); @@ -219,7 +219,7 @@ struct CCameraFollowRegressionUtilities final } const auto expectedTargetDistance = hlsl::length(trackedTarget.getGimbal().getPosition() - camera->getGimbal().getPosition()); - if (!hlsl::isFiniteScalar(expectedTargetDistance) || hlsl::abs(expectedTargetDistance - out.targetDistance) > thresholds.distanceTolerance) + if (!hlsl::CCameraMathUtilities::isFiniteScalar(expectedTargetDistance) || hlsl::abs(expectedTargetDistance - out.targetDistance) > thresholds.distanceTolerance) { if (error) { @@ -277,7 +277,7 @@ struct CCameraFollowRegressionUtilities final const auto trackedTargetPosition = trackedTarget.getGimbal().getPosition(); const auto targetDelta = state.target - trackedTargetPosition; const auto targetDeltaLen = hlsl::length(targetDelta); - if (!hlsl::isFiniteScalar(targetDeltaLen) || targetDeltaLen > thresholds.targetTolerance) + if (!hlsl::CCameraMathUtilities::isFiniteScalar(targetDeltaLen) || targetDeltaLen > thresholds.targetTolerance) { if (error) *error = "spherical target writeback mismatch"; @@ -287,7 +287,7 @@ struct CCameraFollowRegressionUtilities final const auto actualDistance = hlsl::length(camera->getGimbal().getPosition() - trackedTargetPosition); const auto expectedDistance = followGoal.hasOrbitState ? static_cast(followGoal.orbitDistance) : (followGoal.hasDistance ? static_cast(followGoal.distance) : actualDistance); - if (!hlsl::isFiniteScalar(actualDistance) || !hlsl::isFiniteScalar(expectedDistance) || + if (!hlsl::CCameraMathUtilities::isFiniteScalar(actualDistance) || !hlsl::CCameraMathUtilities::isFiniteScalar(expectedDistance) || hlsl::abs(actualDistance - expectedDistance) > thresholds.distanceTolerance || hlsl::abs(static_cast(state.distance) - expectedDistance) > thresholds.distanceTolerance) { @@ -370,3 +370,4 @@ struct CCameraFollowRegressionUtilities final } // namespace nbl::system #endif // _C_CAMERA_FOLLOW_REGRESSION_UTILITIES_HPP_ + diff --git a/common/include/camera/CCameraFollowUtilities.hpp b/common/include/camera/CCameraFollowUtilities.hpp index 1c324bcab..1fed7a51b 100644 --- a/common/include/camera/CCameraFollowUtilities.hpp +++ b/common/include/camera/CCameraFollowUtilities.hpp @@ -28,7 +28,7 @@ class CTrackedTarget CTrackedTarget( const hlsl::float64_t3& position = hlsl::float64_t3(0.0), - const hlsl::camera_quaternion_t& orientation = hlsl::makeIdentityQuaternion(), + const hlsl::camera_quaternion_t& orientation = hlsl::CCameraMathUtilities::makeIdentityQuaternion(), std::string identifier = "Follow Target") : m_identifier(std::move(identifier)), m_gimbal({ .position = position, .orientation = orientation }) @@ -62,8 +62,8 @@ class CTrackedTarget inline bool trySetFromTransform(const hlsl::float64_t4x4& transform) { hlsl::float64_t3 position = hlsl::float64_t3(0.0); - hlsl::camera_quaternion_t orientation = hlsl::makeIdentityQuaternion(); - if (!hlsl::tryExtractRigidPoseFromTransform(transform, position, orientation)) + hlsl::camera_quaternion_t orientation = hlsl::CCameraMathUtilities::makeIdentityQuaternion(); + if (!hlsl::CCameraMathUtilities::tryExtractRigidPoseFromTransform(transform, position, orientation)) return false; setPose(position, orientation); @@ -196,12 +196,12 @@ struct CCameraFollowUtilities final static inline hlsl::float64_t3 transformFollowLocalOffset(const ICamera::CGimbal& gimbal, const hlsl::float64_t3& localOffset) { - return hlsl::rotateVectorByQuaternion(gimbal.getOrientation(), localOffset); + return hlsl::CCameraMathUtilities::rotateVectorByQuaternion(gimbal.getOrientation(), localOffset); } static inline hlsl::float64_t3 projectFollowWorldOffsetToLocal(const ICamera::CGimbal& gimbal, const hlsl::float64_t3& worldOffset) { - return hlsl::projectWorldVectorToLocalQuaternionFrame(gimbal.getOrientation(), worldOffset); + return hlsl::CCameraMathUtilities::projectWorldVectorToLocalQuaternionFrame(gimbal.getOrientation(), worldOffset); } static inline bool buildFollowLookAtOrientation( @@ -210,7 +210,7 @@ struct CCameraFollowUtilities final const hlsl::float64_t3& preferredUp, hlsl::camera_quaternion_t& outOrientation) { - return hlsl::tryBuildLookAtOrientation(position, targetPosition, preferredUp, outOrientation); + return hlsl::CCameraMathUtilities::tryBuildLookAtOrientation(position, targetPosition, preferredUp, outOrientation); } static inline bool captureFollowOffsetsFromCamera( @@ -237,19 +237,19 @@ struct CCameraFollowUtilities final { const auto toTarget = trackedTarget.getGimbal().getPosition() - cameraGimbal.getPosition(); const auto targetDistance = hlsl::length(toTarget); - if (!hlsl::isFiniteScalar(targetDistance) || targetDistance <= ICamera::TinyScalarEpsilon) + if (!hlsl::CCameraMathUtilities::isFiniteScalar(targetDistance) || targetDistance <= ICamera::TinyScalarEpsilon) return false; const auto forward = cameraGimbal.getZAxis(); const auto forwardLength = hlsl::length(forward); - if (!hlsl::isFiniteVec3(forward) || !hlsl::isFiniteScalar(forwardLength) || forwardLength <= ICamera::TinyScalarEpsilon) + if (!hlsl::CCameraMathUtilities::isFiniteVec3(forward) || !hlsl::CCameraMathUtilities::isFiniteScalar(forwardLength) || forwardLength <= ICamera::TinyScalarEpsilon) return false; const auto forwardDirection = forward / forwardLength; const auto targetDir = toTarget / targetDistance; const auto dotForward = std::clamp(hlsl::dot(forwardDirection, targetDir), -1.0, 1.0); outAngleDeg = static_cast(hlsl::degrees(hlsl::acos(dotForward))); - if (!hlsl::isFiniteScalar(outAngleDeg)) + if (!hlsl::CCameraMathUtilities::isFiniteScalar(outAngleDeg)) return false; if (outDistance) @@ -359,3 +359,4 @@ struct CCameraFollowUtilities final } // namespace nbl::core #endif // _C_CAMERA_FOLLOW_UTILITIES_HPP_ + diff --git a/common/include/camera/CCameraGoal.hpp b/common/include/camera/CCameraGoal.hpp index e111831f9..335aff771 100644 --- a/common/include/camera/CCameraGoal.hpp +++ b/common/include/camera/CCameraGoal.hpp @@ -24,7 +24,7 @@ namespace nbl::core struct CCameraGoal { hlsl::float64_t3 position = hlsl::float64_t3(0.0); - hlsl::camera_quaternion_t orientation = hlsl::makeIdentityQuaternion(); + hlsl::camera_quaternion_t orientation = hlsl::CCameraMathUtilities::makeIdentityQuaternion(); ICamera::CameraKind sourceKind = ICamera::CameraKind::Unknown; uint32_t sourceCapabilities = ICamera::None; uint32_t sourceGoalStateMask = ICamera::GoalStateNone; @@ -111,7 +111,7 @@ struct CCameraGoalUtilities final { if (!(goal.hasTargetPosition && goal.hasOrbitState)) return false; - if (!hlsl::isFiniteScalar(goal.orbitUv.x) || !hlsl::isFiniteScalar(goal.orbitUv.y) || !hlsl::isFiniteScalar(goal.orbitDistance)) + if (!hlsl::CCameraMathUtilities::isFiniteScalar(goal.orbitUv.x) || !hlsl::CCameraMathUtilities::isFiniteScalar(goal.orbitUv.y) || !hlsl::CCameraMathUtilities::isFiniteScalar(goal.orbitDistance)) return false; return applyCanonicalTargetRelativeGoal( @@ -208,18 +208,18 @@ struct CCameraGoalUtilities final static inline bool isGoalFinite(const CCameraGoal& goal) { - if (!hlsl::isFiniteVec3(goal.position) || !hlsl::isFiniteQuaternion(goal.orientation)) + if (!hlsl::CCameraMathUtilities::isFiniteVec3(goal.position) || !hlsl::CCameraMathUtilities::isFiniteQuaternion(goal.orientation)) return false; - if (goal.hasTargetPosition && !hlsl::isFiniteVec3(goal.targetPosition)) + if (goal.hasTargetPosition && !hlsl::CCameraMathUtilities::isFiniteVec3(goal.targetPosition)) return false; - if (goal.hasDistance && !hlsl::isFiniteScalar(goal.distance)) + if (goal.hasDistance && !hlsl::CCameraMathUtilities::isFiniteScalar(goal.distance)) return false; - if (goal.hasOrbitState && (!hlsl::isFiniteScalar(goal.orbitUv.x) || !hlsl::isFiniteScalar(goal.orbitUv.y) || !hlsl::isFiniteScalar(goal.orbitDistance))) + if (goal.hasOrbitState && (!hlsl::CCameraMathUtilities::isFiniteScalar(goal.orbitUv.x) || !hlsl::CCameraMathUtilities::isFiniteScalar(goal.orbitUv.y) || !hlsl::CCameraMathUtilities::isFiniteScalar(goal.orbitDistance))) return false; if (goal.hasPathState && !CCameraPathUtilities::isPathStateFinite(goal.pathState)) return false; if (goal.hasDynamicPerspectiveState && - (!hlsl::isFiniteScalar(goal.dynamicPerspectiveState.baseFov) || !hlsl::isFiniteScalar(goal.dynamicPerspectiveState.referenceDistance))) + (!hlsl::CCameraMathUtilities::isFiniteScalar(goal.dynamicPerspectiveState.baseFov) || !hlsl::CCameraMathUtilities::isFiniteScalar(goal.dynamicPerspectiveState.referenceDistance))) return false; return true; } @@ -228,30 +228,30 @@ struct CCameraGoalUtilities final const double posEps, const double rotEpsDeg, const double scalarEps) { hlsl::SCameraPoseDelta poseDelta = {}; - if (!hlsl::tryComputePoseDelta(actual.position, actual.orientation, expected.position, expected.orientation, poseDelta)) + if (!hlsl::CCameraMathUtilities::tryComputePoseDelta(actual.position, actual.orientation, expected.position, expected.orientation, poseDelta)) return false; if (poseDelta.position > posEps || poseDelta.rotationDeg > rotEpsDeg) return false; if (expected.hasTargetPosition) { - if (!actual.hasTargetPosition || !hlsl::nearlyEqualVec3(actual.targetPosition, expected.targetPosition, scalarEps)) + if (!actual.hasTargetPosition || !hlsl::CCameraMathUtilities::nearlyEqualVec3(actual.targetPosition, expected.targetPosition, scalarEps)) return false; } if (expected.hasDistance) { - if (!actual.hasDistance || !hlsl::nearlyEqualScalar(static_cast(actual.distance), static_cast(expected.distance), scalarEps)) + if (!actual.hasDistance || !hlsl::CCameraMathUtilities::nearlyEqualScalar(static_cast(actual.distance), static_cast(expected.distance), scalarEps)) return false; } if (expected.hasOrbitState) { if (!actual.hasOrbitState) return false; - if (hlsl::getWrappedAngleDistanceDegrees(expected.orbitUv.x, actual.orbitUv.x) > rotEpsDeg) + if (hlsl::CCameraMathUtilities::getWrappedAngleDistanceDegrees(expected.orbitUv.x, actual.orbitUv.x) > rotEpsDeg) return false; - if (hlsl::getWrappedAngleDistanceDegrees(expected.orbitUv.y, actual.orbitUv.y) > rotEpsDeg) + if (hlsl::CCameraMathUtilities::getWrappedAngleDistanceDegrees(expected.orbitUv.y, actual.orbitUv.y) > rotEpsDeg) return false; - if (!hlsl::nearlyEqualScalar(static_cast(actual.orbitDistance), static_cast(expected.orbitDistance), scalarEps)) + if (!hlsl::CCameraMathUtilities::nearlyEqualScalar(static_cast(actual.orbitDistance), static_cast(expected.orbitDistance), scalarEps)) return false; } if (expected.hasPathState) @@ -265,9 +265,9 @@ struct CCameraGoalUtilities final { if (!actual.hasDynamicPerspectiveState) return false; - if (!hlsl::nearlyEqualScalar(static_cast(actual.dynamicPerspectiveState.baseFov), static_cast(expected.dynamicPerspectiveState.baseFov), scalarEps)) + if (!hlsl::CCameraMathUtilities::nearlyEqualScalar(static_cast(actual.dynamicPerspectiveState.baseFov), static_cast(expected.dynamicPerspectiveState.baseFov), scalarEps)) return false; - if (!hlsl::nearlyEqualScalar(static_cast(actual.dynamicPerspectiveState.referenceDistance), static_cast(expected.dynamicPerspectiveState.referenceDistance), scalarEps)) + if (!hlsl::CCameraMathUtilities::nearlyEqualScalar(static_cast(actual.dynamicPerspectiveState.referenceDistance), static_cast(expected.dynamicPerspectiveState.referenceDistance), scalarEps)) return false; } @@ -278,9 +278,9 @@ struct CCameraGoalUtilities final { std::ostringstream oss; hlsl::SCameraPoseDelta poseDelta = {}; - const bool hasPoseDelta = hlsl::tryComputePoseDelta(actual.position, actual.orientation, expected.position, expected.orientation, poseDelta); - const auto currentOrientation = hlsl::normalizeQuaternion(actual.orientation); - const auto expectedOrientation = hlsl::normalizeQuaternion(expected.orientation); + const bool hasPoseDelta = hlsl::CCameraMathUtilities::tryComputePoseDelta(actual.position, actual.orientation, expected.position, expected.orientation, poseDelta); + const auto currentOrientation = hlsl::CCameraMathUtilities::normalizeQuaternion(actual.orientation); + const auto expectedOrientation = hlsl::CCameraMathUtilities::normalizeQuaternion(expected.orientation); oss << "pos_delta=" << (hasPoseDelta ? poseDelta.position : std::numeric_limits::quiet_NaN()) << " rot_delta_deg=" << (hasPoseDelta ? poseDelta.rotationDeg : std::numeric_limits::quiet_NaN()) << " current_pos=(" << actual.position.x << "," << actual.position.y << "," << actual.position.z << ")" @@ -328,7 +328,7 @@ struct CCameraGoalUtilities final { CCameraGoal blended; blended.position = a.position + (b.position - a.position) * alpha; - blended.orientation = hlsl::slerpQuaternion(a.orientation, b.orientation, static_cast(alpha)); + blended.orientation = hlsl::CCameraMathUtilities::slerpQuaternion(a.orientation, b.orientation, static_cast(alpha)); blended.sourceKind = (a.sourceKind == b.sourceKind) ? a.sourceKind : ICamera::CameraKind::Unknown; blended.sourceCapabilities = a.sourceCapabilities & b.sourceCapabilities; blended.sourceGoalStateMask = a.sourceGoalStateMask | b.sourceGoalStateMask; @@ -355,8 +355,8 @@ struct CCameraGoalUtilities final const float db = b.hasOrbitState ? b.orbitDistance : a.orbitDistance; blended.orbitUv = hlsl::float64_t2( - hlsl::lerpWrappedAngleRad(orbitUvA.x, orbitUvB.x, alpha), - hlsl::lerpWrappedAngleRad(orbitUvA.y, orbitUvB.y, alpha)); + hlsl::CCameraMathUtilities::lerpWrappedAngleRad(orbitUvA.x, orbitUvB.x, alpha), + hlsl::CCameraMathUtilities::lerpWrappedAngleRad(orbitUvA.y, orbitUvB.y, alpha)); blended.orbitDistance = da + (db - da) * static_cast(alpha); } blended.hasDynamicPerspectiveState = a.hasDynamicPerspectiveState || b.hasDynamicPerspectiveState; @@ -382,3 +382,4 @@ struct CCameraGoalUtilities final } // namespace nbl::core #endif // _C_CAMERA_GOAL_HPP_ + diff --git a/common/include/camera/CCameraGoalSolver.hpp b/common/include/camera/CCameraGoalSolver.hpp index cfc13a0f0..2891a7a06 100644 --- a/common/include/camera/CCameraGoalSolver.hpp +++ b/common/include/camera/CCameraGoalSolver.hpp @@ -335,10 +335,10 @@ class CCameraGoalSolver } else { - const bool dynamicChanged = !hlsl::nearlyEqualScalar(beforeState.baseFov, afterState.baseFov, static_cast(ICamera::ScalarTolerance)) || - !hlsl::nearlyEqualScalar(beforeState.referenceDistance, afterState.referenceDistance, static_cast(ICamera::ScalarTolerance)); - const bool dynamicExact = hlsl::nearlyEqualScalar(afterState.baseFov, canonicalTarget.dynamicPerspectiveState.baseFov, static_cast(ICamera::ScalarTolerance)) && - hlsl::nearlyEqualScalar(afterState.referenceDistance, canonicalTarget.dynamicPerspectiveState.referenceDistance, static_cast(ICamera::ScalarTolerance)); + const bool dynamicChanged = !hlsl::CCameraMathUtilities::nearlyEqualScalar(beforeState.baseFov, afterState.baseFov, static_cast(ICamera::ScalarTolerance)) || + !hlsl::CCameraMathUtilities::nearlyEqualScalar(beforeState.referenceDistance, afterState.referenceDistance, static_cast(ICamera::ScalarTolerance)); + const bool dynamicExact = hlsl::CCameraMathUtilities::nearlyEqualScalar(afterState.baseFov, canonicalTarget.dynamicPerspectiveState.baseFov, static_cast(ICamera::ScalarTolerance)) && + hlsl::CCameraMathUtilities::nearlyEqualScalar(afterState.referenceDistance, canonicalTarget.dynamicPerspectiveState.referenceDistance, static_cast(ICamera::ScalarTolerance)); absoluteChanged = absoluteChanged || dynamicChanged; exact = exact && dynamicExact; @@ -453,7 +453,7 @@ class CCameraGoalSolver const auto& gimbal = camera->getGimbal(); hlsl::SCameraPoseDelta poseDelta = {}; - if (!hlsl::tryComputePoseDelta(gimbal.getPosition(), gimbal.getOrientation(), target.position, target.orientation, poseDelta)) + if (!hlsl::CCameraMathUtilities::tryComputePoseDelta(gimbal.getPosition(), gimbal.getOrientation(), target.position, target.orientation, poseDelta)) return false; outPositionDelta = poseDelta.position; @@ -488,7 +488,7 @@ class CCameraGoalSolver return true; } - const auto targetFrame = hlsl::composeTransformMatrix(target.position, target.orientation); + const auto targetFrame = hlsl::CCameraMathUtilities::composeTransformMatrix(target.position, target.orientation); camera->manipulate({}, &targetFrame); @@ -497,8 +497,8 @@ class CCameraGoalSolver if (!computePoseMismatch(camera, target, afterPosDelta, afterRotDeltaDeg)) return false; - outChanged = !hlsl::isNearlyZeroScalar(afterPosDelta - beforePosDelta, static_cast(ICamera::TinyScalarEpsilon)) || - !hlsl::isNearlyZeroScalar(afterRotDeltaDeg - beforeRotDeltaDeg, static_cast(ICamera::TinyScalarEpsilon)); + outChanged = !hlsl::CCameraMathUtilities::isNearlyZeroScalar(afterPosDelta - beforePosDelta, static_cast(ICamera::TinyScalarEpsilon)) || + !hlsl::CCameraMathUtilities::isNearlyZeroScalar(afterRotDeltaDeg - beforeRotDeltaDeg, static_cast(ICamera::TinyScalarEpsilon)); outExact = afterPosDelta <= ICamera::DefaultPositionTolerance && afterRotDeltaDeg <= ICamera::DefaultAngularToleranceDeg; return true; } @@ -604,8 +604,8 @@ class CCameraGoalSolver { case ICamera::CameraKind::FPS: { - const auto currentPitchYaw = hlsl::getPitchYawFromOrientation(gimbal.getOrientation()); - const auto targetPitchYaw = hlsl::getPitchYawFromOrientation(target.orientation); + const auto currentPitchYaw = hlsl::CCameraMathUtilities::getPitchYawFromOrientation(gimbal.getOrientation()); + const auto targetPitchYaw = hlsl::CCameraMathUtilities::getPitchYawFromOrientation(target.orientation); const double rotScale = camera->getRotationSpeedScale(); const double invScale = rotScale == 0.0 ? SGoalSolverDefaults::UnitScale : (SGoalSolverDefaults::UnitScale / rotScale); @@ -613,8 +613,8 @@ class CCameraGoalSolver appendYawPitchRollEvents( out, hlsl::float64_t3( - hlsl::wrapAngleRad(targetPitchYaw.x - currentPitchYaw.x) * invScale, - hlsl::wrapAngleRad(targetPitchYaw.y - currentPitchYaw.y) * invScale, + hlsl::CCameraMathUtilities::wrapAngleRad(targetPitchYaw.x - currentPitchYaw.x) * invScale, + hlsl::CCameraMathUtilities::wrapAngleRad(targetPitchYaw.y - currentPitchYaw.y) * invScale, 0.0), SGoalSolverDefaults::UnitScale, false); @@ -624,7 +624,7 @@ class CCameraGoalSolver { appendYawPitchRollEvents( out, - hlsl::getOrientationDeltaEulerRadiansYXZ(gimbal.getOrientation(), target.orientation), + hlsl::CCameraMathUtilities::getOrientationDeltaEulerRadiansYXZ(gimbal.getOrientation(), target.orientation), SGoalSolverDefaults::UnitScale); } break; @@ -639,3 +639,4 @@ class CCameraGoalSolver } // namespace nbl::core #endif // _C_CAMERA_GOAL_SOLVER_HPP_ + diff --git a/common/include/camera/CCameraManipulationUtilities.hpp b/common/include/camera/CCameraManipulationUtilities.hpp index e71e1e800..ab2cbe84d 100644 --- a/common/include/camera/CCameraManipulationUtilities.hpp +++ b/common/include/camera/CCameraManipulationUtilities.hpp @@ -90,7 +90,7 @@ struct CCameraManipulationUtilities final } const auto worldDelta = CCameraVirtualEventUtilities::collectSignedTranslationDelta({ events.data(), count }); - if (hlsl::isNearlyZeroVector(worldDelta, static_cast(ICamera::TinyScalarEpsilon))) + if (hlsl::CCameraMathUtilities::isNearlyZeroVector(worldDelta, static_cast(ICamera::TinyScalarEpsilon))) { events = std::move(filtered); count = static_cast(events.size()); @@ -130,7 +130,7 @@ struct CCameraManipulationUtilities final const auto& gimbal = camera->getGimbal(); const auto pos = gimbal.getPosition(); - const auto eulerDeg = hlsl::getCameraOrientationEulerDegrees(gimbal.getOrientation()); + const auto eulerDeg = hlsl::CCameraMathUtilities::getCameraOrientationEulerDegrees(gimbal.getOrientation()); auto clamped = eulerDeg; if (constraints.clampPitch) @@ -145,7 +145,7 @@ struct CCameraManipulationUtilities final CCameraPreset preset; preset.goal.position = pos; - preset.goal.orientation = hlsl::makeQuaternionFromEulerDegreesYXZ(clamped); + preset.goal.orientation = hlsl::CCameraMathUtilities::makeQuaternionFromEulerDegreesYXZ(clamped); return CCameraPresetFlowUtilities::applyPreset(solver, camera, preset); } }; @@ -153,3 +153,4 @@ struct CCameraManipulationUtilities final } // namespace nbl::core #endif // _C_CAMERA_MANIPULATION_UTILITIES_HPP_ + diff --git a/common/include/camera/CCameraMathUtilities.hpp b/common/include/camera/CCameraMathUtilities.hpp index bfc85dfe6..19ef7b785 100644 --- a/common/include/camera/CCameraMathUtilities.hpp +++ b/common/include/camera/CCameraMathUtilities.hpp @@ -13,62 +13,29 @@ namespace nbl::hlsl { //! Camera-oriented math aliases and helpers built on top of Nabla `nbl::hlsl` types. -template -inline T wrapAngleRad(T angle) -{ - constexpr T Pi = numbers::pi; - constexpr T TwoPi = Pi * static_cast(2); - - angle = std::fmod(angle + Pi, TwoPi); - if (angle < static_cast(0)) - angle += TwoPi; - return angle - Pi; -} - -template -inline T getWrappedAngleDistanceRadians(const T a, const T b) -{ - return hlsl::abs(wrapAngleRad(a - b)); -} - -template -inline T getWrappedAngleDistanceDegrees(const T a, const T b) -{ - constexpr T HalfTurn = static_cast(180); - constexpr T FullTurn = static_cast(360); +template +using camera_vector_t = vector; - T angle = std::fmod(a - b + HalfTurn, FullTurn); - if (angle < static_cast(0)) - angle += FullTurn; - return hlsl::abs(angle - HalfTurn); -} +template +using camera_matrix_t = matrix; template -inline T lerpWrappedAngleRad(const T a, const T b, const T alpha) -{ - return a + wrapAngleRad(b - a) * alpha; -} +using camera_quaternion_t = math::quaternion; template -inline bool isFiniteScalar(const T value) +struct SRigidTransformComponents { - return std::isfinite(value); -} + camera_vector_t translation = camera_vector_t(T(0)); + camera_quaternion_t orientation = camera_quaternion_t::create(); + camera_vector_t scale = camera_vector_t(T(1)); +}; template -inline constexpr T getCameraMathEpsilon() +struct SCameraPoseDelta { - return std::numeric_limits::epsilon(); -} - -template -using camera_vector_t = vector; - -template -using camera_matrix_t = matrix; - -template -using camera_quaternion_t = math::quaternion; + T position = T(0); + T rotationDeg = T(0); +}; struct SCameraViewRigDefaults final { @@ -99,837 +66,873 @@ struct SCameraRigidMathDefaults final static constexpr double LookAtParallelThreshold = 0.99; }; -template -inline constexpr camera_vector_t getCameraWorldRight() +struct CCameraMathUtilities final { - return camera_vector_t(T(1), T(0), T(0)); -} - -template -inline constexpr camera_vector_t getCameraWorldUp() -{ - return camera_vector_t(T(0), T(1), T(0)); -} + template + static inline T wrapAngleRad(T angle) + { + constexpr T Pi = numbers::pi; + constexpr T TwoPi = Pi * static_cast(2); -template -inline constexpr camera_vector_t getCameraWorldForward() -{ - return camera_vector_t(T(0), T(0), T(1)); -} + angle = std::fmod(angle + Pi, TwoPi); + if (angle < static_cast(0)) + angle += TwoPi; + return angle - Pi; + } -template -inline constexpr T getCameraLookAtParallelThreshold() -{ - return static_cast(SCameraRigidMathDefaults::LookAtParallelThreshold); -} + template + static inline T getWrappedAngleDistanceRadians(const T a, const T b) + { + return hlsl::abs(wrapAngleRad(a - b)); + } -template -inline camera_quaternion_t makeIdentityQuaternion(); + template + static inline T getWrappedAngleDistanceDegrees(const T a, const T b) + { + constexpr T HalfTurn = static_cast(180); + constexpr T FullTurn = static_cast(360); -template -struct SRigidTransformComponents -{ - camera_vector_t translation = camera_vector_t(T(0)); - camera_quaternion_t orientation = camera_quaternion_t::create(); - camera_vector_t scale = camera_vector_t(T(1)); -}; + T angle = std::fmod(a - b + HalfTurn, FullTurn); + if (angle < static_cast(0)) + angle += FullTurn; + return hlsl::abs(angle - HalfTurn); + } -template -inline bool tryExtractRigidTransformComponents( - const camera_matrix_t& transform, - SRigidTransformComponents& outComponents); + template + static inline T lerpWrappedAngleRad(const T a, const T b, const T alpha) + { + return a + wrapAngleRad(b - a) * alpha; + } -template -inline camera_quaternion_t makeIdentityQuaternion() -{ - return camera_quaternion_t::create(); -} + template + static inline bool isFiniteScalar(const T value) + { + return std::isfinite(value); + } -template -inline camera_quaternion_t makeQuaternionFromComponents(const T x, const T y, const T z, const T w) -{ - camera_quaternion_t output; - output.data = camera_vector_t(x, y, z, w); - return output; -} + template + static inline constexpr T getCameraMathEpsilon() + { + return std::numeric_limits::epsilon(); + } -template -inline camera_quaternion_t normalizeQuaternion(const camera_quaternion_t& q) -{ - return normalize(q); -} + template + static inline bool nearlyEqualScalar(const T a, const T b, const T epsilon) + { + return hlsl::abs(a - b) <= epsilon; + } -template -inline bool isFiniteQuaternion(const camera_quaternion_t& q) -{ - return isFiniteScalar(q.data.x) && - isFiniteScalar(q.data.y) && - isFiniteScalar(q.data.z) && - isFiniteScalar(q.data.w); -} + template + static inline bool isNearlyZeroScalar(const T value, const T epsilon = getCameraMathEpsilon()) + { + return hlsl::abs(value) <= epsilon; + } -template -inline bool isFiniteVec3(const camera_vector_t& value); + template + static inline bool isNearlyZeroVector(const camera_vector_t& value, const T epsilon = getCameraMathEpsilon()) + { + return length(value) <= epsilon; + } -template -inline camera_vector_t safeNormalizeVec3(const camera_vector_t& value, const camera_vector_t& fallback); + template + static inline bool hasPlanarDeltaXY(const camera_vector_t& value, const T epsilon = std::numeric_limits::epsilon()) + { + return !isNearlyZeroVector(camera_vector_t(value.x, value.y), epsilon); + } -template -inline camera_quaternion_t makeQuaternionFromAxisAngle(const camera_vector_t& axis, const T radians) -{ - return camera_quaternion_t::create(axis, radians); -} + template + static inline bool nearlyEqualVec3(const VecA& a, const VecB& b, const T epsilon) + { + const camera_vector_t delta( + static_cast(a.x - b.x), + static_cast(a.y - b.y), + static_cast(a.z - b.z)); + return length(delta) <= epsilon; + } -template -inline camera_quaternion_t makeQuaternionFromEulerRadians(const camera_vector_t& eulerRadians) -{ - return camera_quaternion_t::create(eulerRadians.x, eulerRadians.y, eulerRadians.z); -} + template + static inline constexpr camera_vector_t getCameraWorldRight() + { + return camera_vector_t(T(1), T(0), T(0)); + } -template -inline camera_quaternion_t makeQuaternionFromEulerDegrees(const camera_vector_t& eulerDegrees) -{ - return makeQuaternionFromEulerRadians(camera_vector_t( - radians(eulerDegrees.x), - radians(eulerDegrees.y), - radians(eulerDegrees.z))); -} + template + static inline constexpr camera_vector_t getCameraWorldUp() + { + return camera_vector_t(T(0), T(1), T(0)); + } -template -inline camera_quaternion_t makeQuaternionFromEulerRadiansYXZ(const camera_vector_t& eulerRadians) -{ - const auto pitch = makeQuaternionFromAxisAngle(getCameraWorldRight(), eulerRadians.x); - const auto yaw = makeQuaternionFromAxisAngle(getCameraWorldUp(), eulerRadians.y); - const auto roll = makeQuaternionFromAxisAngle(getCameraWorldForward(), eulerRadians.z); - return normalizeQuaternion(yaw * pitch * roll); -} + template + static inline constexpr camera_vector_t getCameraWorldForward() + { + return camera_vector_t(T(0), T(0), T(1)); + } -template -inline camera_quaternion_t makeQuaternionFromEulerDegreesYXZ(const camera_vector_t& eulerDegrees) -{ - return makeQuaternionFromEulerRadiansYXZ(camera_vector_t( - radians(eulerDegrees.x), - radians(eulerDegrees.y), - radians(eulerDegrees.z))); -} + template + static inline constexpr T getCameraLookAtParallelThreshold() + { + return static_cast(SCameraRigidMathDefaults::LookAtParallelThreshold); + } -template -inline camera_quaternion_t makeQuaternionFromBasis( - const camera_vector_t& right, - const camera_vector_t& up, - const camera_vector_t& forward) -{ - const auto canonicalForward = safeNormalizeVec3(forward, getCameraWorldForward()); + template + static inline camera_quaternion_t makeIdentityQuaternion() + { + return camera_quaternion_t::create(); + } - auto canonicalRight = right - canonicalForward * dot(right, canonicalForward); - canonicalRight = safeNormalizeVec3( - canonicalRight, - safeNormalizeVec3(cross(up, canonicalForward), getCameraWorldRight())); + template + static inline camera_quaternion_t makeQuaternionFromComponents(const T x, const T y, const T z, const T w) + { + camera_quaternion_t output; + output.data = camera_vector_t(x, y, z, w); + return output; + } - auto canonicalUp = cross(canonicalForward, canonicalRight); - canonicalUp = safeNormalizeVec3( - canonicalUp, - safeNormalizeVec3(up - canonicalForward * dot(up, canonicalForward), getCameraWorldUp())); + template + static inline camera_quaternion_t normalizeQuaternion(const camera_quaternion_t& q) + { + return normalize(q); + } - canonicalRight = safeNormalizeVec3(cross(canonicalUp, canonicalForward), canonicalRight); - canonicalUp = safeNormalizeVec3(cross(canonicalForward, canonicalRight), canonicalUp); + template + static inline bool isFiniteQuaternion(const camera_quaternion_t& q) + { + return isFiniteScalar(q.data.x) && + isFiniteScalar(q.data.y) && + isFiniteScalar(q.data.z) && + isFiniteScalar(q.data.w); + } - const camera_matrix_t basis { canonicalRight, canonicalUp, canonicalForward }; - const auto desiredRight = canonicalRight; - const auto desiredUp = canonicalUp; - const auto desiredForward = canonicalForward; + template + static inline bool isFiniteVec3(const camera_vector_t& value) + { + return isFiniteScalar(value.x) && + isFiniteScalar(value.y) && + isFiniteScalar(value.z); + } - const auto scoreCandidate = [&](const camera_quaternion_t& candidate) + template + static inline camera_vector_t safeNormalizeVec3(const camera_vector_t& value, const camera_vector_t& fallback) { - if (!isFiniteQuaternion(candidate)) - return std::numeric_limits::infinity(); + const auto len = length(value); + if (!isFiniteScalar(len) || len <= getCameraMathEpsilon()) + return fallback; + return value / len; + } - const auto normalizedCandidate = normalizeQuaternion(candidate); - const auto rebuiltRight = normalizedCandidate.transformVector(camera_vector_t(T(1), T(0), T(0)), true); - const auto rebuiltUp = normalizedCandidate.transformVector(camera_vector_t(T(0), T(1), T(0)), true); - const auto rebuiltForward = normalizedCandidate.transformVector(camera_vector_t(T(0), T(0), T(1)), true); + template + static inline camera_quaternion_t makeQuaternionFromAxisAngle(const camera_vector_t& axis, const T radians) + { + return camera_quaternion_t::create(axis, radians); + } - const T rightError = length(rebuiltRight - desiredRight); - const T upError = length(rebuiltUp - desiredUp); - const T forwardError = length(rebuiltForward - desiredForward); - return rightError + upError + forwardError; - }; + template + static inline camera_quaternion_t makeQuaternionFromEulerRadians(const camera_vector_t& eulerRadians) + { + return camera_quaternion_t::create(eulerRadians.x, eulerRadians.y, eulerRadians.z); + } - const auto quaternionFromMatrixFallback = [&](const camera_matrix_t& m) + template + static inline camera_quaternion_t makeQuaternionFromEulerDegrees(const camera_vector_t& eulerDegrees) { - const T m00 = m[0][0]; - const T m11 = m[1][1]; - const T m22 = m[2][2]; - const T trace = m00 + m11 + m22; + return makeQuaternionFromEulerRadians(camera_vector_t( + radians(eulerDegrees.x), + radians(eulerDegrees.y), + radians(eulerDegrees.z))); + } - camera_quaternion_t output = makeIdentityQuaternion(); - if (trace > T(0)) - { - const T scale = hlsl::sqrt(trace + T(1)); - const T invScale = T(0.5) / scale; - output.data.x = (m[2][1] - m[1][2]) * invScale; - output.data.y = (m[0][2] - m[2][0]) * invScale; - output.data.z = (m[1][0] - m[0][1]) * invScale; - output.data.w = scale * T(0.5); - } - else if (m00 >= m11 && m00 >= m22) - { - const T scale = hlsl::sqrt(T(1) + m00 - m11 - m22); - const T invScale = T(0.5) / scale; - output.data.x = scale * T(0.5); - output.data.y = (m[0][1] + m[1][0]) * invScale; - output.data.z = (m[2][0] + m[0][2]) * invScale; - output.data.w = (m[2][1] - m[1][2]) * invScale; - } - else if (m11 >= m22) - { - const T scale = hlsl::sqrt(T(1) + m11 - m00 - m22); - const T invScale = T(0.5) / scale; - output.data.x = (m[0][1] + m[1][0]) * invScale; - output.data.y = scale * T(0.5); - output.data.z = (m[1][2] + m[2][1]) * invScale; - output.data.w = (m[0][2] - m[2][0]) * invScale; - } - else - { - const T scale = hlsl::sqrt(T(1) + m22 - m00 - m11); - const T invScale = T(0.5) / scale; - output.data.x = (m[2][0] + m[0][2]) * invScale; - output.data.y = (m[1][2] + m[2][1]) * invScale; - output.data.z = scale * T(0.5); - output.data.w = (m[1][0] - m[0][1]) * invScale; - } - return normalizeQuaternion(output); - }; - - const camera_matrix_t transposedBasis = hlsl::transpose(basis); - const camera_quaternion_t candidates[] = { - camera_quaternion_t::create(basis, true), - camera_quaternion_t::create(transposedBasis, true), - quaternionFromMatrixFallback(basis), - quaternionFromMatrixFallback(transposedBasis) - }; - - camera_quaternion_t bestCandidate = makeIdentityQuaternion(); - T bestScore = std::numeric_limits::infinity(); - bool foundFiniteCandidate = false; - const auto considerCandidate = [&](const camera_quaternion_t& candidate) - { - const T score = scoreCandidate(candidate); - if (score < bestScore) - { - bestScore = score; - bestCandidate = candidate; - foundFiniteCandidate = true; - } - }; + template + static inline camera_quaternion_t makeQuaternionFromEulerRadiansYXZ(const camera_vector_t& eulerRadians) + { + const auto pitch = makeQuaternionFromAxisAngle(getCameraWorldRight(), eulerRadians.x); + const auto yaw = makeQuaternionFromAxisAngle(getCameraWorldUp(), eulerRadians.y); + const auto roll = makeQuaternionFromAxisAngle(getCameraWorldForward(), eulerRadians.z); + return normalizeQuaternion(yaw * pitch * roll); + } - for (const auto& candidate : candidates) - considerCandidate(candidate); + template + static inline camera_quaternion_t makeQuaternionFromEulerDegreesYXZ(const camera_vector_t& eulerDegrees) + { + return makeQuaternionFromEulerRadiansYXZ(camera_vector_t( + radians(eulerDegrees.x), + radians(eulerDegrees.y), + radians(eulerDegrees.z))); + } - if (!foundFiniteCandidate || !isFiniteQuaternion(bestCandidate)) - return makeIdentityQuaternion(); + template + static inline camera_quaternion_t makeQuaternionFromBasis( + const camera_vector_t& right, + const camera_vector_t& up, + const camera_vector_t& forward) + { + const auto canonicalForward = safeNormalizeVec3(forward, getCameraWorldForward()); - return normalizeQuaternion(bestCandidate); -} + auto canonicalRight = right - canonicalForward * dot(right, canonicalForward); + canonicalRight = safeNormalizeVec3( + canonicalRight, + safeNormalizeVec3(cross(up, canonicalForward), getCameraWorldRight())); -template -inline bool isFiniteVec3(const camera_vector_t& value) -{ - return isFiniteScalar(value.x) && isFiniteScalar(value.y) && isFiniteScalar(value.z); -} + auto canonicalUp = cross(canonicalForward, canonicalRight); + canonicalUp = safeNormalizeVec3( + canonicalUp, + safeNormalizeVec3(up - canonicalForward * dot(up, canonicalForward), getCameraWorldUp())); -template -inline bool nearlyEqualScalar(const T a, const T b, const T epsilon) -{ - return hlsl::abs(a - b) <= epsilon; -} + canonicalRight = safeNormalizeVec3(cross(canonicalUp, canonicalForward), canonicalRight); + canonicalUp = safeNormalizeVec3(cross(canonicalForward, canonicalRight), canonicalUp); -template -inline bool isNearlyZeroScalar(const T value, const T epsilon = getCameraMathEpsilon()) -{ - return hlsl::abs(value) <= epsilon; -} + const camera_matrix_t basis { canonicalRight, canonicalUp, canonicalForward }; + const auto desiredRight = canonicalRight; + const auto desiredUp = canonicalUp; + const auto desiredForward = canonicalForward; -template -inline bool isNearlyZeroVector(const camera_vector_t& value, const T epsilon = getCameraMathEpsilon()) -{ - return length(value) <= epsilon; -} + const auto scoreCandidate = [&](const camera_quaternion_t& candidate) + { + if (!isFiniteQuaternion(candidate)) + return std::numeric_limits::infinity(); -template -inline bool hasPlanarDeltaXY(const camera_vector_t& value, const T epsilon = std::numeric_limits::epsilon()) -{ - return !isNearlyZeroVector(camera_vector_t(value.x, value.y), epsilon); -} + const auto normalizedCandidate = normalizeQuaternion(candidate); + const auto rebuiltRight = normalizedCandidate.transformVector(camera_vector_t(T(1), T(0), T(0)), true); + const auto rebuiltUp = normalizedCandidate.transformVector(camera_vector_t(T(0), T(1), T(0)), true); + const auto rebuiltForward = normalizedCandidate.transformVector(camera_vector_t(T(0), T(0), T(1)), true); -template -struct SCameraPoseDelta -{ - T position = T(0); - T rotationDeg = T(0); -}; + const T rightError = length(rebuiltRight - desiredRight); + const T upError = length(rebuiltUp - desiredUp); + const T forwardError = length(rebuiltForward - desiredForward); + return rightError + upError + forwardError; + }; -template -inline bool nearlyEqualVec3(const VecA& a, const VecB& b, const T epsilon) -{ - const camera_vector_t delta( - static_cast(a.x - b.x), - static_cast(a.y - b.y), - static_cast(a.z - b.z)); - return length(delta) <= epsilon; -} + const auto quaternionFromMatrixFallback = [&](const camera_matrix_t& m) + { + const T m00 = m[0][0]; + const T m11 = m[1][1]; + const T m22 = m[2][2]; + const T trace = m00 + m11 + m22; + + camera_quaternion_t output = makeIdentityQuaternion(); + if (trace > T(0)) + { + const T scale = hlsl::sqrt(trace + T(1)); + const T invScale = T(0.5) / scale; + output.data.x = (m[2][1] - m[1][2]) * invScale; + output.data.y = (m[0][2] - m[2][0]) * invScale; + output.data.z = (m[1][0] - m[0][1]) * invScale; + output.data.w = scale * T(0.5); + } + else if (m00 >= m11 && m00 >= m22) + { + const T scale = hlsl::sqrt(T(1) + m00 - m11 - m22); + const T invScale = T(0.5) / scale; + output.data.x = scale * T(0.5); + output.data.y = (m[0][1] + m[1][0]) * invScale; + output.data.z = (m[2][0] + m[0][2]) * invScale; + output.data.w = (m[2][1] - m[1][2]) * invScale; + } + else if (m11 >= m22) + { + const T scale = hlsl::sqrt(T(1) + m11 - m00 - m22); + const T invScale = T(0.5) / scale; + output.data.x = (m[0][1] + m[1][0]) * invScale; + output.data.y = scale * T(0.5); + output.data.z = (m[1][2] + m[2][1]) * invScale; + output.data.w = (m[0][2] - m[2][0]) * invScale; + } + else + { + const T scale = hlsl::sqrt(T(1) + m22 - m00 - m11); + const T invScale = T(0.5) / scale; + output.data.x = (m[2][0] + m[0][2]) * invScale; + output.data.y = (m[1][2] + m[2][1]) * invScale; + output.data.z = scale * T(0.5); + output.data.w = (m[1][0] - m[0][1]) * invScale; + } + return normalizeQuaternion(output); + }; + + const camera_matrix_t transposedBasis = hlsl::transpose(basis); + const camera_quaternion_t candidates[] = { + camera_quaternion_t::create(basis, true), + camera_quaternion_t::create(transposedBasis, true), + quaternionFromMatrixFallback(basis), + quaternionFromMatrixFallback(transposedBasis) + }; + + camera_quaternion_t bestCandidate = makeIdentityQuaternion(); + T bestScore = std::numeric_limits::infinity(); + bool foundFiniteCandidate = false; + const auto considerCandidate = [&](const camera_quaternion_t& candidate) + { + const T score = scoreCandidate(candidate); + if (score < bestScore) + { + bestScore = score; + bestCandidate = candidate; + foundFiniteCandidate = true; + } + }; + + for (const auto& candidate : candidates) + considerCandidate(candidate); + + if (!foundFiniteCandidate || !isFiniteQuaternion(bestCandidate)) + return makeIdentityQuaternion(); + + return normalizeQuaternion(bestCandidate); + } -template -inline camera_vector_t safeNormalizeVec3(const camera_vector_t& value, const camera_vector_t& fallback) -{ - const auto len = length(value); - if (!isFiniteScalar(len) || len <= getCameraMathEpsilon()) - return fallback; - return value / len; -} + template + static inline bool tryBuildCameraBasisFromForwardUpHint( + const camera_vector_t& forwardHint, + const camera_vector_t& upHint, + camera_vector_t& outRight, + camera_vector_t& outUp, + camera_vector_t& outForward) + { + const auto forward = safeNormalizeVec3(forwardHint, getCameraWorldForward()); + if (!isFiniteVec3(forward) || isNearlyZeroVector(forward)) + return false; -template -inline bool tryBuildCameraBasisFromForwardUpHint( - const camera_vector_t& forwardHint, - const camera_vector_t& upHint, - camera_vector_t& outRight, - camera_vector_t& outUp, - camera_vector_t& outForward) -{ - const auto forward = safeNormalizeVec3(forwardHint, getCameraWorldForward()); - if (!isFiniteVec3(forward) || isNearlyZeroVector(forward)) - return false; - - const auto preferredUp = safeNormalizeVec3(upHint, getCameraWorldForward()); - auto right = cross(preferredUp, forward); - if (!isFiniteVec3(right) || isNearlyZeroVector(right)) - { - const auto fallbackUp = hlsl::abs(forward.z) < getCameraLookAtParallelThreshold() ? - getCameraWorldForward() : - getCameraWorldUp(); - right = cross(fallbackUp, forward); + const auto preferredUp = safeNormalizeVec3(upHint, getCameraWorldForward()); + auto right = cross(preferredUp, forward); if (!isFiniteVec3(right) || isNearlyZeroVector(right)) + { + const auto fallbackUp = hlsl::abs(forward.z) < getCameraLookAtParallelThreshold() ? + getCameraWorldForward() : + getCameraWorldUp(); + right = cross(fallbackUp, forward); + if (!isFiniteVec3(right) || isNearlyZeroVector(right)) + return false; + } + + right = safeNormalizeVec3(right, getCameraWorldRight()); + auto up = safeNormalizeVec3(cross(forward, right), preferredUp); + right = safeNormalizeVec3(cross(up, forward), right); + if (!isOrthoBase(right, up, forward)) return false; + + outRight = right; + outUp = up; + outForward = forward; + return true; } - right = safeNormalizeVec3(right, getCameraWorldRight()); - auto up = safeNormalizeVec3(cross(forward, right), preferredUp); - right = safeNormalizeVec3(cross(up, forward), right); - if (!isOrthoBase(right, up, forward)) - return false; + template + static inline camera_vector_t makeSphericalOffsetFromOrbit(const T orbitU, const T orbitV, const T distance) + { + return camera_vector_t( + hlsl::cos(orbitV) * hlsl::cos(orbitU) * distance, + hlsl::cos(orbitV) * hlsl::sin(orbitU) * distance, + hlsl::sin(orbitV) * distance); + } - outRight = right; - outUp = up; - outForward = forward; - return true; -} + template + static inline T getPlanarRadiusXZ(const camera_vector_t& offset) + { + return length(camera_vector_t(offset.x, offset.z)); + } -template -inline camera_vector_t makeSphericalOffsetFromOrbit(const T orbitU, const T orbitV, const T distance) -{ - return camera_vector_t( - hlsl::cos(orbitV) * hlsl::cos(orbitU) * distance, - hlsl::cos(orbitV) * hlsl::sin(orbitU) * distance, - hlsl::sin(orbitV) * distance); -} + template + static inline T getPathDistance(const T radius, const T height) + { + return length(camera_vector_t(radius, height)); + } -template -inline T getPlanarRadiusXZ(const camera_vector_t& offset) -{ - return length(camera_vector_t(offset.x, offset.z)); -} + template + static inline camera_vector_t makePathOffsetFromState(const T angle, const T radius, const T height) + { + return camera_vector_t(hlsl::cos(angle) * radius, height, hlsl::sin(angle) * radius); + } -template -inline T getPathDistance(const T radius, const T height) -{ - return length(camera_vector_t(radius, height)); -} + template + static inline bool sanitizePathState(T& angle, T& radius, T& height, const T minRadius) + { + if (!isFiniteScalar(angle) || !isFiniteScalar(radius) || !isFiniteScalar(height)) + return false; -template -inline camera_vector_t makePathOffsetFromState(const T angle, const T radius, const T height) -{ - return camera_vector_t(hlsl::cos(angle) * radius, height, hlsl::sin(angle) * radius); -} + angle = wrapAngleRad(angle); + radius = std::max(minRadius, radius); + return isFiniteScalar(angle) && + isFiniteScalar(radius) && + isFiniteScalar(height); + } -template -inline bool sanitizePathState(T& angle, T& radius, T& height, const T minRadius) -{ - if (!isFiniteScalar(angle) || !isFiniteScalar(radius) || !isFiniteScalar(height)) - return false; + template + static inline bool tryScalePathStateDistance( + const T desiredDistance, + const T minRadius, + T& radius, + T& height, + T* outAppliedDistance = nullptr) + { + if (!isFiniteScalar(desiredDistance) || + !isFiniteScalar(radius) || + !isFiniteScalar(height)) + return false; - angle = wrapAngleRad(angle); - radius = std::max(minRadius, radius); - return isFiniteScalar(angle) && isFiniteScalar(radius) && isFiniteScalar(height); -} + const T currentDistance = getPathDistance(radius, height); + constexpr T Epsilon = std::numeric_limits::epsilon(); + if (currentDistance > Epsilon) + { + const T scale = desiredDistance / currentDistance; + radius = std::max(minRadius, radius * scale); + height *= scale; + } + else + { + radius = std::max(minRadius, desiredDistance); + height = T(0); + } -template -inline bool tryScalePathStateDistance( - const T desiredDistance, - const T minRadius, - T& radius, - T& height, - T* outAppliedDistance = nullptr) -{ - if (!isFiniteScalar(desiredDistance) || !isFiniteScalar(radius) || !isFiniteScalar(height)) - return false; + if (outAppliedDistance) + *outAppliedDistance = getPathDistance(radius, height); + return isFiniteScalar(radius) && isFiniteScalar(height); + } - const T currentDistance = getPathDistance(radius, height); - constexpr T Epsilon = std::numeric_limits::epsilon(); - if (currentDistance > Epsilon) + template + static inline bool tryBuildPathStateFromPosition( + const camera_vector_t& targetPosition, + const camera_vector_t& position, + const T minRadius, + T& outAngle, + T& outRadius, + T& outHeight) { - const T scale = desiredDistance / currentDistance; - radius = std::max(minRadius, radius * scale); - height *= scale; + const auto offset = position - targetPosition; + const auto radius = getPlanarRadiusXZ(offset); + if (!isFiniteScalar(radius) || !isFiniteScalar(offset.y)) + return false; + + outAngle = wrapAngleRad(hlsl::atan2(offset.z, offset.x)); + outRadius = std::max(minRadius, radius); + outHeight = offset.y; + return isFiniteScalar(outAngle) && + isFiniteScalar(outRadius) && + isFiniteScalar(outHeight); } - else + + template + static inline bool tryBuildLookAtOrientation( + const camera_vector_t& position, + const camera_vector_t& targetPosition, + const camera_vector_t& preferredUp, + camera_quaternion_t& outOrientation) { - radius = std::max(minRadius, desiredDistance); - height = T(0); + const auto toTarget = targetPosition - position; + camera_vector_t right = camera_vector_t(T(0)); + camera_vector_t up = camera_vector_t(T(0)); + camera_vector_t forward = camera_vector_t(T(0)); + if (!tryBuildCameraBasisFromForwardUpHint(toTarget, preferredUp, right, up, forward)) + return false; + + outOrientation = makeQuaternionFromBasis(right, up, forward); + return true; } - if (outAppliedDistance) - *outAppliedDistance = getPathDistance(radius, height); - return isFiniteScalar(radius) && isFiniteScalar(height); -} + template + static inline bool tryExtractRigidPoseFromTransform( + const camera_matrix_t& transform, + camera_vector_t& outTranslation, + camera_quaternion_t& outOrientation) + { + SRigidTransformComponents components; + if (!tryExtractRigidTransformComponents(transform, components)) + return false; -template -inline bool tryBuildPathStateFromPosition( - const camera_vector_t& targetPosition, - const camera_vector_t& position, - const T minRadius, - T& outAngle, - T& outRadius, - T& outHeight) -{ - const auto offset = position - targetPosition; - const auto radius = getPlanarRadiusXZ(offset); - if (!isFiniteScalar(radius) || !isFiniteScalar(offset.y)) - return false; + outTranslation = components.translation; + outOrientation = components.orientation; + return true; + } - outAngle = wrapAngleRad(hlsl::atan2(offset.z, offset.x)); - outRadius = std::max(minRadius, radius); - outHeight = offset.y; - return isFiniteScalar(outAngle) && isFiniteScalar(outRadius) && isFiniteScalar(outHeight); -} + template + static inline bool tryBuildSphericalPoseFromOrbit( + const camera_vector_t& targetPosition, + const T orbitU, + const T orbitV, + const T distance, + const T minDistance, + const T maxDistance, + camera_vector_t& outPosition, + camera_quaternion_t& outOrientation, + T* outAppliedDistance = nullptr) + { + if (!isFiniteScalar(orbitU) || + !isFiniteScalar(orbitV) || + !isFiniteScalar(distance)) + return false; -template -inline bool tryBuildLookAtOrientation( - const camera_vector_t& position, - const camera_vector_t& targetPosition, - const camera_vector_t& preferredUp, - camera_quaternion_t& outOrientation) -{ - const auto toTarget = targetPosition - position; - camera_vector_t right = camera_vector_t(T(0)); - camera_vector_t up = camera_vector_t(T(0)); - camera_vector_t forward = camera_vector_t(T(0)); - if (!tryBuildCameraBasisFromForwardUpHint(toTarget, preferredUp, right, up, forward)) - return false; + const T appliedDistance = std::clamp(distance, minDistance, maxDistance); + const auto spherePosition = makeSphericalOffsetFromOrbit(orbitU, orbitV, appliedDistance); + const auto upHint = safeNormalizeVec3( + camera_vector_t( + -hlsl::sin(orbitV) * hlsl::cos(orbitU), + -hlsl::sin(orbitV) * hlsl::sin(orbitU), + hlsl::cos(orbitV)), + getCameraWorldForward()); + camera_vector_t right = camera_vector_t(T(0)); + camera_vector_t up = camera_vector_t(T(0)); + camera_vector_t forward = camera_vector_t(T(0)); + if (!tryBuildCameraBasisFromForwardUpHint(-spherePosition, upHint, right, up, forward)) + return false; - outOrientation = makeQuaternionFromBasis(right, up, forward); - return true; -} + outPosition = targetPosition + spherePosition; + outOrientation = makeQuaternionFromBasis(right, up, forward); + if (outAppliedDistance) + *outAppliedDistance = appliedDistance; + return true; + } -template -inline bool tryExtractRigidPoseFromTransform( - const camera_matrix_t& transform, - camera_vector_t& outTranslation, - camera_quaternion_t& outOrientation) -{ - SRigidTransformComponents components; - if (!tryExtractRigidTransformComponents(transform, components)) - return false; + template + static inline bool tryBuildOrbitFromPosition( + const camera_vector_t& targetPosition, + const camera_vector_t& position, + const T minDistance, + const T maxDistance, + T& outOrbitU, + T& outOrbitV, + T& outDistance) + { + const auto offset = position - targetPosition; + const auto distance = length(offset); + if (!isFiniteScalar(distance) || distance <= getCameraMathEpsilon()) + return false; - outTranslation = components.translation; - outOrientation = components.orientation; - return true; -} + outDistance = std::clamp(distance, minDistance, maxDistance); + const auto local = offset / outDistance; + outOrbitU = hlsl::atan2(local.y, local.x); + outOrbitV = hlsl::asin(std::clamp(local.z, T(-1), T(1))); + return isFiniteScalar(outOrbitU) && + isFiniteScalar(outOrbitV) && + isFiniteScalar(outDistance); + } -template -inline bool tryBuildSphericalPoseFromOrbit( - const camera_vector_t& targetPosition, - const T orbitU, - const T orbitV, - const T distance, - const T minDistance, - const T maxDistance, - camera_vector_t& outPosition, - camera_quaternion_t& outOrientation, - T* outAppliedDistance = nullptr) -{ - if (!isFiniteScalar(orbitU) || !isFiniteScalar(orbitV) || !isFiniteScalar(distance)) - return false; - - const T appliedDistance = std::clamp(distance, minDistance, maxDistance); - const auto spherePosition = makeSphericalOffsetFromOrbit(orbitU, orbitV, appliedDistance); - const auto upHint = safeNormalizeVec3( - camera_vector_t( - -hlsl::sin(orbitV) * hlsl::cos(orbitU), - -hlsl::sin(orbitV) * hlsl::sin(orbitU), - hlsl::cos(orbitV)), - getCameraWorldForward()); - camera_vector_t right = camera_vector_t(T(0)); - camera_vector_t up = camera_vector_t(T(0)); - camera_vector_t forward = camera_vector_t(T(0)); - if (!tryBuildCameraBasisFromForwardUpHint(-spherePosition, upHint, right, up, forward)) - return false; - - outPosition = targetPosition + spherePosition; - outOrientation = makeQuaternionFromBasis(right, up, forward); - if (outAppliedDistance) - *outAppliedDistance = appliedDistance; - return true; -} + template + static inline camera_vector_t getPitchYawFromForwardVector(const camera_vector_t& forward) + { + const T planarLength = length(camera_vector_t(forward.x, forward.z)); + return camera_vector_t( + hlsl::atan2(planarLength, forward.y) - numbers::pi * T(0.5), + hlsl::atan2(forward.x, forward.z)); + } -template -inline bool tryBuildOrbitFromPosition( - const camera_vector_t& targetPosition, - const camera_vector_t& position, - const T minDistance, - const T maxDistance, - T& outOrbitU, - T& outOrbitV, - T& outDistance) -{ - const auto offset = position - targetPosition; - const auto distance = length(offset); - if (!isFiniteScalar(distance) || distance <= getCameraMathEpsilon()) - return false; - - outDistance = std::clamp(distance, minDistance, maxDistance); - const auto local = offset / outDistance; - outOrbitU = hlsl::atan2(local.y, local.x); - outOrbitV = hlsl::asin(std::clamp(local.z, T(-1), T(1))); - return isFiniteScalar(outOrbitU) && isFiniteScalar(outOrbitV) && isFiniteScalar(outDistance); -} + template + static inline camera_vector_t getPitchYawFromOrientation(const camera_quaternion_t& orientation) + { + const auto forward = normalizeQuaternion(orientation).transformVector(camera_vector_t(T(0), T(0), T(1)), true); + return getPitchYawFromForwardVector(forward); + } -template -inline camera_vector_t getPitchYawFromForwardVector(const camera_vector_t& forward) -{ - const T planarLength = length(camera_vector_t(forward.x, forward.z)); - return camera_vector_t( - hlsl::atan2(planarLength, forward.y) - numbers::pi * T(0.5), - hlsl::atan2(forward.x, forward.z)); -} + template + static inline bool tryBuildPathPoseFromState( + const camera_vector_t& targetPosition, + const T pathAngle, + const T pathRadius, + const T pathHeight, + const T minRadius, + const T minDistance, + const T maxDistance, + camera_vector_t& outPosition, + camera_quaternion_t& outOrientation, + T* outAppliedDistance = nullptr, + T* outOrbitU = nullptr, + T* outOrbitV = nullptr) + { + if (!isFiniteScalar(pathAngle) || + !isFiniteScalar(pathRadius) || + !isFiniteScalar(pathHeight)) + return false; -template -inline camera_vector_t getPitchYawFromOrientation(const camera_quaternion_t& orientation) -{ - const auto forward = normalizeQuaternion(orientation).transformVector(camera_vector_t(T(0), T(0), T(1)), true); - return getPitchYawFromForwardVector(forward); -} + const T appliedRadius = std::max(minRadius, pathRadius); + const auto offset = makePathOffsetFromState(pathAngle, appliedRadius, pathHeight); -template -inline bool tryBuildPathPoseFromState( - const camera_vector_t& targetPosition, - const T pathAngle, - const T pathRadius, - const T pathHeight, - const T minRadius, - const T minDistance, - const T maxDistance, - camera_vector_t& outPosition, - camera_quaternion_t& outOrientation, - T* outAppliedDistance = nullptr, - T* outOrbitU = nullptr, - T* outOrbitV = nullptr) -{ - if (!isFiniteScalar(pathAngle) || !isFiniteScalar(pathRadius) || !isFiniteScalar(pathHeight)) - return false; - - const T appliedRadius = std::max(minRadius, pathRadius); - const auto offset = makePathOffsetFromState(pathAngle, appliedRadius, pathHeight); - - T orbitU = T(0); - T orbitV = T(0); - T distance = T(0); - if (!tryBuildOrbitFromPosition(targetPosition, targetPosition + offset, minDistance, maxDistance, orbitU, orbitV, distance)) - return false; - if (!tryBuildSphericalPoseFromOrbit(targetPosition, orbitU, orbitV, distance, minDistance, maxDistance, outPosition, outOrientation, &distance)) - return false; - - if (outAppliedDistance) - *outAppliedDistance = distance; - if (outOrbitU) - *outOrbitU = orbitU; - if (outOrbitV) - *outOrbitV = orbitV; - return true; -} + T orbitU = T(0); + T orbitV = T(0); + T distance = T(0); + if (!tryBuildOrbitFromPosition(targetPosition, targetPosition + offset, minDistance, maxDistance, orbitU, orbitV, distance)) + return false; + if (!tryBuildSphericalPoseFromOrbit(targetPosition, orbitU, orbitV, distance, minDistance, maxDistance, outPosition, outOrientation, &distance)) + return false; -template -inline camera_vector_t rotateVectorByQuaternion(const camera_quaternion_t& orientation, const camera_vector_t& vectorToRotate) -{ - return normalizeQuaternion(orientation).transformVector(vectorToRotate, true); -} + if (outAppliedDistance) + *outAppliedDistance = distance; + if (outOrbitU) + *outOrbitU = orbitU; + if (outOrbitV) + *outOrbitV = orbitV; + return true; + } -template -inline camera_vector_t projectWorldVectorToLocalBasis( - const camera_vector_t& worldVector, - const camera_vector_t& right, - const camera_vector_t& up, - const camera_vector_t& forward) -{ - return camera_vector_t( - dot(worldVector, right), - dot(worldVector, up), - dot(worldVector, forward)); -} + template + static inline camera_vector_t rotateVectorByQuaternion(const camera_quaternion_t& orientation, const camera_vector_t& vectorToRotate) + { + return normalizeQuaternion(orientation).transformVector(vectorToRotate, true); + } -template -inline camera_vector_t transformLocalVectorToWorldBasis( - const camera_vector_t& localVector, - const camera_vector_t& right, - const camera_vector_t& up, - const camera_vector_t& forward) -{ - return right * localVector.x + up * localVector.y + forward * localVector.z; -} + template + static inline camera_vector_t projectWorldVectorToLocalBasis( + const camera_vector_t& worldVector, + const camera_vector_t& right, + const camera_vector_t& up, + const camera_vector_t& forward) + { + return camera_vector_t( + dot(worldVector, right), + dot(worldVector, up), + dot(worldVector, forward)); + } -template -inline camera_vector_t getQuaternionEulerRadians(const camera_quaternion_t& orientation) -{ - const auto q = normalizeQuaternion(orientation); - const T x = q.data.x; - const T y = q.data.y; - const T z = q.data.z; - const T w = q.data.w; - - const T pitch = hlsl::atan2( - T(2) * (y * z + w * x), - w * w - x * x - y * y + z * z); - const T yaw = hlsl::asin(std::clamp( - T(-2) * (x * z - w * y), - T(-1), - T(1))); - const T roll = hlsl::atan2( - T(2) * (x * y + w * z), - w * w + x * x - y * y - z * z); - - return camera_vector_t(pitch, yaw, roll); -} + template + static inline camera_vector_t transformLocalVectorToWorldBasis( + const camera_vector_t& localVector, + const camera_vector_t& right, + const camera_vector_t& up, + const camera_vector_t& forward) + { + return right * localVector.x + up * localVector.y + forward * localVector.z; + } -template -inline camera_vector_t getQuaternionEulerDegrees(const camera_quaternion_t& orientation) -{ - const auto eulerRadians = getQuaternionEulerRadians(orientation); - return camera_vector_t( - degrees(eulerRadians.x), - degrees(eulerRadians.y), - degrees(eulerRadians.z)); -} + template + static inline camera_vector_t getQuaternionEulerRadians(const camera_quaternion_t& orientation) + { + const auto q = normalizeQuaternion(orientation); + const T x = q.data.x; + const T y = q.data.y; + const T z = q.data.z; + const T w = q.data.w; + + const T pitch = hlsl::atan2( + T(2) * (y * z + w * x), + w * w - x * x - y * y + z * z); + const T yaw = hlsl::asin(std::clamp( + T(-2) * (x * z - w * y), + T(-1), + T(1))); + const T roll = hlsl::atan2( + T(2) * (x * y + w * z), + w * w + x * x - y * y - z * z); + + return camera_vector_t(pitch, yaw, roll); + } -template -inline T getQuaternionAngularDistanceRadians(const camera_quaternion_t& lhs, const camera_quaternion_t& rhs) -{ - const auto lhsNormalized = normalizeQuaternion(lhs); - const auto rhsNormalized = normalizeQuaternion(rhs); - const T orientationDot = std::clamp( - static_cast(hlsl::abs(dot(lhsNormalized.data, rhsNormalized.data))), - T(0), - T(1)); - return T(2) * hlsl::acos(orientationDot); -} + template + static inline camera_vector_t getQuaternionEulerDegrees(const camera_quaternion_t& orientation) + { + const auto eulerRadians = getQuaternionEulerRadians(orientation); + return camera_vector_t( + degrees(eulerRadians.x), + degrees(eulerRadians.y), + degrees(eulerRadians.z)); + } -template -inline T getQuaternionAngularDistanceDegrees(const camera_quaternion_t& lhs, const camera_quaternion_t& rhs) -{ - return degrees(getQuaternionAngularDistanceRadians(lhs, rhs)); -} + template + static inline T getQuaternionAngularDistanceRadians(const camera_quaternion_t& lhs, const camera_quaternion_t& rhs) + { + const auto lhsNormalized = normalizeQuaternion(lhs); + const auto rhsNormalized = normalizeQuaternion(rhs); + const T orientationDot = std::clamp( + static_cast(hlsl::abs(dot(lhsNormalized.data, rhsNormalized.data))), + T(0), + T(1)); + return T(2) * hlsl::acos(orientationDot); + } -template -inline bool tryComputePoseDelta( - const camera_vector_t& lhsPosition, - const camera_quaternion_t& lhsOrientation, - const camera_vector_t& rhsPosition, - const camera_quaternion_t& rhsOrientation, - SCameraPoseDelta& outDelta) -{ - outDelta = {}; + template + static inline T getQuaternionAngularDistanceDegrees(const camera_quaternion_t& lhs, const camera_quaternion_t& rhs) + { + return degrees(getQuaternionAngularDistanceRadians(lhs, rhs)); + } - const auto lhsNormalized = normalizeQuaternion(lhsOrientation); - const auto rhsNormalized = normalizeQuaternion(rhsOrientation); - if (!isFiniteVec3(lhsPosition) || !isFiniteVec3(rhsPosition) || - !isFiniteQuaternion(lhsNormalized) || !isFiniteQuaternion(rhsNormalized)) + template + static inline bool tryComputePoseDelta( + const camera_vector_t& lhsPosition, + const camera_quaternion_t& lhsOrientation, + const camera_vector_t& rhsPosition, + const camera_quaternion_t& rhsOrientation, + SCameraPoseDelta& outDelta) { - return false; + outDelta = {}; + + const auto lhsNormalized = normalizeQuaternion(lhsOrientation); + const auto rhsNormalized = normalizeQuaternion(rhsOrientation); + if (!isFiniteVec3(lhsPosition) || !isFiniteVec3(rhsPosition) || + !isFiniteQuaternion(lhsNormalized) || !isFiniteQuaternion(rhsNormalized)) + { + return false; + } + + outDelta.position = length(lhsPosition - rhsPosition); + outDelta.rotationDeg = getQuaternionAngularDistanceDegrees(lhsNormalized, rhsNormalized); + return isFiniteScalar(outDelta.position) && isFiniteScalar(outDelta.rotationDeg); } - outDelta.position = length(lhsPosition - rhsPosition); - outDelta.rotationDeg = getQuaternionAngularDistanceDegrees(lhsNormalized, rhsNormalized); - return isFiniteScalar(outDelta.position) && isFiniteScalar(outDelta.rotationDeg); -} + template + static inline camera_quaternion_t slerpQuaternion(const camera_quaternion_t& lhs, const camera_quaternion_t& rhs, const T alpha) + { + return camera_quaternion_t::slerp(normalizeQuaternion(lhs), normalizeQuaternion(rhs), alpha); + } -template -inline camera_quaternion_t slerpQuaternion(const camera_quaternion_t& lhs, const camera_quaternion_t& rhs, const T alpha) -{ - return camera_quaternion_t::slerp(normalizeQuaternion(lhs), normalizeQuaternion(rhs), alpha); -} + template + static inline camera_quaternion_t inverseQuaternion(const camera_quaternion_t& q) + { + return inverse(q); + } -template -inline camera_quaternion_t inverseQuaternion(const camera_quaternion_t& q) -{ - return inverse(q); -} + template + static inline camera_vector_t projectWorldVectorToLocalQuaternionFrame( + const camera_quaternion_t& orientation, + const camera_vector_t& worldVector) + { + return rotateVectorByQuaternion(inverseQuaternion(orientation), worldVector); + } -template -inline camera_vector_t projectWorldVectorToLocalQuaternionFrame( - const camera_quaternion_t& orientation, - const camera_vector_t& worldVector) -{ - return rotateVectorByQuaternion(inverseQuaternion(orientation), worldVector); -} + template + static inline camera_matrix_t getQuaternionBasisMatrix(const camera_quaternion_t& orientation) + { + const auto q = normalizeQuaternion(orientation); + return camera_matrix_t( + q.transformVector(camera_vector_t(T(1), T(0), T(0)), true), + q.transformVector(camera_vector_t(T(0), T(1), T(0)), true), + q.transformVector(camera_vector_t(T(0), T(0), T(1)), true)); + } -template -inline camera_matrix_t getQuaternionBasisMatrix(const camera_quaternion_t& orientation) -{ - const auto q = normalizeQuaternion(orientation); - return camera_matrix_t( - q.transformVector(camera_vector_t(T(1), T(0), T(0)), true), - q.transformVector(camera_vector_t(T(0), T(1), T(0)), true), - q.transformVector(camera_vector_t(T(0), T(0), T(1)), true)); -} + template + static inline camera_vector_t getQuaternionEulerRadiansYXZ(const camera_quaternion_t& orientation) + { + const auto basis = getQuaternionBasisMatrix(orientation); + const T yaw = hlsl::atan2(basis[2][0], basis[2][2]); + const T c2 = hlsl::length(camera_vector_t(basis[0][1], basis[1][1])); + const T pitch = hlsl::atan2(-basis[2][1], c2); + const T s1 = hlsl::sin(yaw); + const T c1 = hlsl::cos(yaw); + const T roll = hlsl::atan2( + s1 * basis[1][2] - c1 * basis[1][0], + c1 * basis[0][0] - s1 * basis[0][2]); + return camera_vector_t(pitch, yaw, roll); + } -template -inline camera_vector_t getQuaternionEulerRadiansYXZ(const camera_quaternion_t& orientation) -{ - const auto basis = getQuaternionBasisMatrix(orientation); - const T yaw = hlsl::atan2(basis[2][0], basis[2][2]); - const T c2 = hlsl::length(camera_vector_t(basis[0][1], basis[1][1])); - const T pitch = hlsl::atan2(-basis[2][1], c2); - const T s1 = hlsl::sin(yaw); - const T c1 = hlsl::cos(yaw); - const T roll = hlsl::atan2( - s1 * basis[1][2] - c1 * basis[1][0], - c1 * basis[0][0] - s1 * basis[0][2]); - return camera_vector_t(pitch, yaw, roll); -} + template + static inline camera_vector_t getQuaternionEulerDegreesYXZ(const camera_quaternion_t& orientation) + { + const auto eulerRadians = getQuaternionEulerRadiansYXZ(orientation); + return camera_vector_t( + degrees(eulerRadians.x), + degrees(eulerRadians.y), + degrees(eulerRadians.z)); + } -template -inline camera_vector_t getQuaternionEulerDegreesYXZ(const camera_quaternion_t& orientation) -{ - const auto eulerRadians = getQuaternionEulerRadiansYXZ(orientation); - return camera_vector_t( - degrees(eulerRadians.x), - degrees(eulerRadians.y), - degrees(eulerRadians.z)); -} + template + static inline camera_vector_t getCameraOrientationEulerRadians(const camera_quaternion_t& orientation) + { + return getQuaternionEulerRadiansYXZ(orientation); + } -template -inline camera_vector_t getCameraOrientationEulerRadians(const camera_quaternion_t& orientation) -{ - return getQuaternionEulerRadiansYXZ(orientation); -} + template + static inline camera_vector_t getCameraOrientationEulerDegrees(const camera_quaternion_t& orientation) + { + return getQuaternionEulerDegreesYXZ(orientation); + } -template -inline camera_vector_t getCameraOrientationEulerDegrees(const camera_quaternion_t& orientation) -{ - return getQuaternionEulerDegreesYXZ(orientation); -} + template + static inline camera_vector_t getOrientationDeltaEulerRadiansYXZ( + const camera_quaternion_t& from, + const camera_quaternion_t& to) + { + const auto deltaQuat = inverseQuaternion(from) * normalizeQuaternion(to); + return getQuaternionEulerRadiansYXZ(deltaQuat); + } -template -inline camera_vector_t getOrientationDeltaEulerRadiansYXZ( - const camera_quaternion_t& from, - const camera_quaternion_t& to) -{ - const auto deltaQuat = inverseQuaternion(from) * normalizeQuaternion(to); - return getQuaternionEulerRadiansYXZ(deltaQuat); -} + template + static inline camera_vector_t getWrappedEulerDistanceDegrees( + const camera_vector_t& a, + const camera_vector_t& b) + { + return camera_vector_t( + getWrappedAngleDistanceDegrees(a.x, b.x), + getWrappedAngleDistanceDegrees(a.y, b.y), + getWrappedAngleDistanceDegrees(a.z, b.z)); + } -template -inline camera_vector_t getWrappedEulerDistanceDegrees( - const camera_vector_t& a, - const camera_vector_t& b) -{ - return camera_vector_t( - getWrappedAngleDistanceDegrees(a.x, b.x), - getWrappedAngleDistanceDegrees(a.y, b.y), - getWrappedAngleDistanceDegrees(a.z, b.z)); -} + template + static inline T getMaxVectorComponent(const camera_vector_t& value) + { + return std::max(value.x, std::max(value.y, value.z)); + } -template -inline T getMaxVectorComponent(const camera_vector_t& value) -{ - return std::max(value.x, std::max(value.y, value.z)); -} + template + static inline camera_matrix_t composeTransformMatrix( + const camera_vector_t& translation, + const camera_quaternion_t& orientation, + const camera_vector_t& scale = camera_vector_t(T(1))) + { + camera_matrix_t output = camera_matrix_t(1); + const auto basis = getQuaternionBasisMatrix(orientation); + output[0] = camera_vector_t(basis[0] * scale.x, T(0)); + output[1] = camera_vector_t(basis[1] * scale.y, T(0)); + output[2] = camera_vector_t(basis[2] * scale.z, T(0)); + output[3] = camera_vector_t(translation, T(1)); + return output; + } -template -inline camera_matrix_t composeTransformMatrix( - const camera_vector_t& translation, - const camera_quaternion_t& orientation, - const camera_vector_t& scale = camera_vector_t(T(1))) -{ - camera_matrix_t output = camera_matrix_t(1); - const auto basis = getQuaternionBasisMatrix(orientation); - output[0] = camera_vector_t(basis[0] * scale.x, T(0)); - output[1] = camera_vector_t(basis[1] * scale.y, T(0)); - output[2] = camera_vector_t(basis[2] * scale.z, T(0)); - output[3] = camera_vector_t(translation, T(1)); - return output; -} + template + static inline bool tryExtractRigidTransformComponents( + const camera_matrix_t& transform, + SRigidTransformComponents& outComponents) + { + outComponents.translation = camera_vector_t(transform[3].x, transform[3].y, transform[3].z); -template -inline bool tryExtractRigidTransformComponents( - const camera_matrix_t& transform, - SRigidTransformComponents& outComponents) -{ - outComponents.translation = camera_vector_t(transform[3].x, transform[3].y, transform[3].z); + auto right = camera_vector_t(transform[0].x, transform[0].y, transform[0].z); + auto up = camera_vector_t(transform[1].x, transform[1].y, transform[1].z); + auto forward = camera_vector_t(transform[2].x, transform[2].y, transform[2].z); - auto right = camera_vector_t(transform[0].x, transform[0].y, transform[0].z); - auto up = camera_vector_t(transform[1].x, transform[1].y, transform[1].z); - auto forward = camera_vector_t(transform[2].x, transform[2].y, transform[2].z); + outComponents.scale = camera_vector_t(length(right), length(up), length(forward)); - outComponents.scale = camera_vector_t(length(right), length(up), length(forward)); + if (!isFiniteVec3(outComponents.translation) || !isFiniteVec3(outComponents.scale)) + return false; - if (!isFiniteVec3(outComponents.translation) || !isFiniteVec3(outComponents.scale)) - return false; + constexpr T Epsilon = std::numeric_limits::epsilon(); + if (outComponents.scale.x <= Epsilon || outComponents.scale.y <= Epsilon || outComponents.scale.z <= Epsilon) + return false; - constexpr T Epsilon = std::numeric_limits::epsilon(); - if (outComponents.scale.x <= Epsilon || outComponents.scale.y <= Epsilon || outComponents.scale.z <= Epsilon) - return false; + right /= outComponents.scale.x; + up /= outComponents.scale.y; + forward /= outComponents.scale.z; + if (!isOrthoBase(right, up, forward)) + return false; - right /= outComponents.scale.x; - up /= outComponents.scale.y; - forward /= outComponents.scale.z; - if (!isOrthoBase(right, up, forward)) - return false; + outComponents.orientation = makeQuaternionFromBasis(right, up, forward); + return isFiniteQuaternion(outComponents.orientation); + } - outComponents.orientation = makeQuaternionFromBasis(right, up, forward); - return isFiniteQuaternion(outComponents.orientation); -} + template + static inline bool tryBuildRigidFrameFromTransform( + const camera_matrix_t& transform, + camera_matrix_t& outFrame, + camera_quaternion_t& outOrientation) + { + SRigidTransformComponents components; + if (!tryExtractRigidTransformComponents(transform, components)) + return false; -template -inline bool tryBuildRigidFrameFromTransform( - const camera_matrix_t& transform, - camera_matrix_t& outFrame, - camera_quaternion_t& outOrientation) -{ - SRigidTransformComponents components; - if (!tryExtractRigidTransformComponents(transform, components)) - return false; + outOrientation = components.orientation; + outFrame = composeTransformMatrix(components.translation, components.orientation); + return true; + } - outOrientation = components.orientation; - outFrame = composeTransformMatrix(components.translation, components.orientation); - return true; -} + template + static inline bool decomposeTransformMatrix( + const camera_matrix_t& transform, + camera_vector_t& outTranslation, + camera_vector_t& outRotationEulerDegrees, + camera_vector_t& outScale) + { + SRigidTransformComponents components; + if (!tryExtractRigidTransformComponents(transform, components)) + return false; -template -inline bool decomposeTransformMatrix( - const camera_matrix_t& transform, - camera_vector_t& outTranslation, - camera_vector_t& outRotationEulerDegrees, - camera_vector_t& outScale) -{ - SRigidTransformComponents components; - if (!tryExtractRigidTransformComponents(transform, components)) - return false; - - outTranslation = components.translation; - outScale = components.scale; - outRotationEulerDegrees = getCameraOrientationEulerDegrees(components.orientation); - return isFiniteVec3(outRotationEulerDegrees); -} + outTranslation = components.translation; + outScale = components.scale; + outRotationEulerDegrees = getCameraOrientationEulerDegrees(components.orientation); + return isFiniteVec3(outRotationEulerDegrees); + } +}; } // namespace nbl::hlsl diff --git a/common/include/camera/CCameraPathUtilities.hpp b/common/include/camera/CCameraPathUtilities.hpp index 046a55ed6..4e7f20f2d 100644 --- a/common/include/camera/CCameraPathUtilities.hpp +++ b/common/include/camera/CCameraPathUtilities.hpp @@ -16,7 +16,7 @@ namespace nbl::core struct SCameraPathPose final { hlsl::float64_t3 position = hlsl::float64_t3(0.0); - hlsl::camera_quaternion_t orientation = hlsl::makeIdentityQuaternion(); + hlsl::camera_quaternion_t orientation = hlsl::CCameraMathUtilities::makeIdentityQuaternion(); hlsl::float64_t appliedDistance = 0.0; hlsl::float64_t2 orbitUv = hlsl::float64_t2(0.0); }; @@ -129,14 +129,14 @@ struct CCameraPathUtilities final static inline bool isPathStateFinite(const ICamera::PathState& state) { - return hlsl::isFiniteScalar(state.angle) && - hlsl::isFiniteScalar(state.radius) && - hlsl::isFiniteScalar(state.height); + return hlsl::CCameraMathUtilities::isFiniteScalar(state.angle) && + hlsl::CCameraMathUtilities::isFiniteScalar(state.radius) && + hlsl::CCameraMathUtilities::isFiniteScalar(state.height); } static inline bool sanitizePathState(ICamera::PathState& state, const double minRadius) { - return hlsl::sanitizePathState(state.angle, state.radius, state.height, minRadius); + return hlsl::CCameraMathUtilities::sanitizePathState(state.angle, state.radius, state.height, minRadius); } static inline bool tryScalePathStateDistance( @@ -145,7 +145,7 @@ struct CCameraPathUtilities final ICamera::PathState& ioState, double* outAppliedDistance = nullptr) { - return hlsl::tryScalePathStateDistance( + return hlsl::CCameraMathUtilities::tryScalePathStateDistance( desiredDistance, minRadius, ioState.radius, @@ -168,7 +168,7 @@ struct CCameraPathUtilities final { outResult->appliedDistance = appliedDistance; outResult->exact = (clampedDistance == desiredDistance) && - hlsl::nearlyEqualScalar(appliedDistance, static_cast(desiredDistance), SCameraPathDefaults::ScalarTolerance); + hlsl::CCameraMathUtilities::nearlyEqualScalar(appliedDistance, static_cast(desiredDistance), SCameraPathDefaults::ScalarTolerance); } return true; } @@ -179,7 +179,7 @@ struct CCameraPathUtilities final const double minRadius, ICamera::PathState& outState) { - return hlsl::tryBuildPathStateFromPosition( + return hlsl::CCameraMathUtilities::tryBuildPathStateFromPosition( targetPosition, position, minRadius, @@ -214,7 +214,7 @@ struct CCameraPathUtilities final const SCameraPathLimits& limits, SCameraPathPose& outPose) { - return hlsl::tryBuildPathPoseFromState( + return hlsl::CCameraMathUtilities::tryBuildPathPoseFromState( targetPosition, state.angle, state.radius, @@ -256,9 +256,9 @@ struct CCameraPathUtilities final const ICamera::PathState& rhs, const SCameraPathComparisonThresholds& thresholds = {}) { - return hlsl::getWrappedAngleDistanceDegrees(lhs.angle, rhs.angle) <= thresholds.angleToleranceDeg && - hlsl::nearlyEqualScalar(lhs.radius, rhs.radius, thresholds.scalarTolerance) && - hlsl::nearlyEqualScalar(lhs.height, rhs.height, thresholds.scalarTolerance); + return hlsl::CCameraMathUtilities::getWrappedAngleDistanceDegrees(lhs.angle, rhs.angle) <= thresholds.angleToleranceDeg && + hlsl::CCameraMathUtilities::nearlyEqualScalar(lhs.radius, rhs.radius, thresholds.scalarTolerance) && + hlsl::CCameraMathUtilities::nearlyEqualScalar(lhs.height, rhs.height, thresholds.scalarTolerance); } static inline bool pathStatesChanged( @@ -274,7 +274,7 @@ struct CCameraPathUtilities final const ICamera::PathState& desiredState) { auto deltaVector = desiredState.asVector() - currentState.asVector(); - deltaVector.z = hlsl::wrapAngleRad(deltaVector.z); + deltaVector.z = hlsl::CCameraMathUtilities::wrapAngleRad(deltaVector.z); return deltaVector; } @@ -336,7 +336,7 @@ struct CCameraPathUtilities final ICamera::PathState& outState) { auto stateVector = currentState.asVector() + delta.asVector(); - stateVector.z = hlsl::wrapAngleRad(stateVector.z); + stateVector.z = hlsl::CCameraMathUtilities::wrapAngleRad(stateVector.z); outState = ICamera::PathState::fromVector(stateVector); return sanitizePathState(outState, limits.minRadius); } @@ -349,7 +349,7 @@ struct CCameraPathUtilities final const auto fromVector = from.asVector(); const auto toVector = to.asVector(); return { - .angle = hlsl::lerpWrappedAngleRad(fromVector.z, toVector.z, alpha), + .angle = hlsl::CCameraMathUtilities::lerpWrappedAngleRad(fromVector.z, toVector.z, alpha), .radius = fromVector.x + (toVector.x - fromVector.x) * alpha, .height = fromVector.y + (toVector.y - fromVector.y) * alpha }; @@ -377,3 +377,4 @@ struct CCameraPathUtilities final } // namespace nbl::core #endif // _C_CAMERA_PATH_UTILITIES_HPP_ + diff --git a/common/include/camera/CCameraScriptedCheckRunner.hpp b/common/include/camera/CCameraScriptedCheckRunner.hpp index 96e0986f3..d486b033e 100644 --- a/common/include/camera/CCameraScriptedCheckRunner.hpp +++ b/common/include/camera/CCameraScriptedCheckRunner.hpp @@ -35,7 +35,7 @@ struct CCameraScriptedCheckRuntimeState { bool valid = false; hlsl::float64_t3 position = hlsl::float64_t3(0.0); - hlsl::camera_quaternion_t orientation = hlsl::makeIdentityQuaternion(); + hlsl::camera_quaternion_t orientation = hlsl::CCameraMathUtilities::makeIdentityQuaternion(); }; size_t nextCheckIndex = 0u; @@ -79,7 +79,7 @@ struct CCameraScriptedCheckRunnerUtilities final { state.step.valid = true; state.step.position = position; - state.step.orientation = hlsl::normalizeQuaternion(orientation); + state.step.orientation = hlsl::CCameraMathUtilities::normalizeQuaternion(orientation); } static inline void scriptedCheckSetBaselineReference( @@ -89,7 +89,7 @@ struct CCameraScriptedCheckRunnerUtilities final { state.baseline.valid = true; state.baseline.position = position; - state.baseline.orientation = hlsl::normalizeQuaternion(orientation); + state.baseline.orientation = hlsl::CCameraMathUtilities::normalizeQuaternion(orientation); scriptedCheckSetStepReference(state, position, orientation); } @@ -100,7 +100,7 @@ struct CCameraScriptedCheckRunnerUtilities final const hlsl::camera_quaternion_t& referenceOrientation, hlsl::SCameraPoseDelta& outDelta) { - return hlsl::tryComputePoseDelta( + return hlsl::CCameraMathUtilities::tryComputePoseDelta( currentPosition, currentOrientation, referencePosition, @@ -155,10 +155,10 @@ struct CCameraScriptedCheckRunnerUtilities final const auto& gimbal = context.camera->getGimbal(); const auto pos = gimbal.getPosition(); - const auto orientation = hlsl::normalizeQuaternion(gimbal.getOrientation()); - const auto eulerDeg = hlsl::getCastedVector(hlsl::getCameraOrientationEulerDegrees(orientation)); + const auto orientation = hlsl::CCameraMathUtilities::normalizeQuaternion(gimbal.getOrientation()); + const auto eulerDeg = hlsl::getCastedVector(hlsl::CCameraMathUtilities::getCameraOrientationEulerDegrees(orientation)); - if (!hlsl::isFiniteVec3(pos) || !hlsl::isFiniteQuaternion(orientation) || !hlsl::isFiniteVec3(eulerDeg)) + if (!hlsl::CCameraMathUtilities::isFiniteVec3(pos) || !hlsl::CCameraMathUtilities::isFiniteQuaternion(orientation) || !hlsl::CCameraMathUtilities::isFiniteVec3(eulerDeg)) { appendScriptedCheckLog( result, @@ -266,7 +266,7 @@ struct CCameraScriptedCheckRunnerUtilities final } if (check.hasExpectedEuler) { - const auto expectedOrientation = hlsl::makeQuaternionFromEulerDegreesYXZ( + const auto expectedOrientation = hlsl::CCameraMathUtilities::makeQuaternionFromEulerDegreesYXZ( hlsl::getCastedVector(check.expectedEulerDeg)); hlsl::SCameraPoseDelta poseDelta = {}; if (!scriptedCheckComputePoseDelta(pos, orientation, pos, expectedOrientation, poseDelta)) diff --git a/common/include/camera/CCameraSequenceScript.hpp b/common/include/camera/CCameraSequenceScript.hpp index 50ddd83cd..dab01b62e 100644 --- a/common/include/camera/CCameraSequenceScript.hpp +++ b/common/include/camera/CCameraSequenceScript.hpp @@ -113,7 +113,7 @@ struct CCameraSequenceKeyframe struct CCameraSequenceTrackedTargetPose { hlsl::float64_t3 position = hlsl::float64_t3(0.0); - hlsl::camera_quaternion_t orientation = hlsl::makeIdentityQuaternion(); + hlsl::camera_quaternion_t orientation = hlsl::CCameraMathUtilities::makeIdentityQuaternion(); }; //! Relative tracked-target adjustment authored against an initial tracked-target pose. @@ -291,7 +291,7 @@ struct CCameraSequenceScriptUtilities final std::sort(fractions.begin(), fractions.end()); fractions.erase(std::unique(fractions.begin(), fractions.end(), - [](const float lhs, const float rhs) { return hlsl::nearlyEqualScalar(lhs, rhs, static_cast(ICamera::ScalarTolerance)); }), + [](const float lhs, const float rhs) { return hlsl::CCameraMathUtilities::nearlyEqualScalar(lhs, rhs, static_cast(ICamera::ScalarTolerance)); }), fractions.end()); } @@ -330,7 +330,7 @@ struct CCameraSequenceScriptUtilities final if (delta.hasRotationEulerDegOffset) { - goal.orientation = hlsl::normalizeQuaternion(goal.orientation * hlsl::makeQuaternionFromEulerDegreesYXZ(hlsl::getCastedVector(delta.rotationEulerDegOffset))); + goal.orientation = hlsl::CCameraMathUtilities::normalizeQuaternion(goal.orientation * hlsl::CCameraMathUtilities::makeQuaternionFromEulerDegreesYXZ(hlsl::getCastedVector(delta.rotationEulerDegOffset))); } if (delta.hasTargetOffset) @@ -357,7 +357,7 @@ struct CCameraSequenceScriptUtilities final delta.hasOrbitUDeltaDeg ? static_cast(hlsl::radians(delta.orbitUvDeltaDeg.x)) : hlsl::float64_t(0.0), delta.hasOrbitVDeltaDeg ? static_cast(hlsl::radians(delta.orbitUvDeltaDeg.y)) : hlsl::float64_t(0.0)); if (delta.hasOrbitUDeltaDeg) - goal.orbitUv.x = hlsl::wrapAngleRad(goal.orbitUv.x + orbitUvDeltaRad.x); + goal.orbitUv.x = hlsl::CCameraMathUtilities::wrapAngleRad(goal.orbitUv.x + orbitUvDeltaRad.x); if (delta.hasOrbitVDeltaDeg) { goal.orbitUv.y = std::clamp( @@ -448,8 +448,8 @@ struct CCameraSequenceScriptUtilities final static inline bool isSequenceTrackedTargetPoseFinite(const CCameraSequenceTrackedTargetPose& pose) { - return hlsl::isFiniteVec3(pose.position) && - hlsl::isFiniteQuaternion(pose.orientation); + return hlsl::CCameraMathUtilities::isFiniteVec3(pose.position) && + hlsl::CCameraMathUtilities::isFiniteQuaternion(pose.orientation); } static inline bool buildSequenceTrackedTargetPoseFromReference( @@ -463,14 +463,14 @@ struct CCameraSequenceScriptUtilities final if (authored.hasAbsolutePosition) outPose.position = authored.absolutePosition; if (authored.hasAbsoluteRotationEulerDeg) - outPose.orientation = hlsl::makeQuaternionFromEulerDegreesYXZ(hlsl::getCastedVector(authored.absoluteRotationEulerDeg)); + outPose.orientation = hlsl::CCameraMathUtilities::makeQuaternionFromEulerDegreesYXZ(hlsl::getCastedVector(authored.absoluteRotationEulerDeg)); if (authored.hasDelta) { if (authored.delta.hasPositionOffset) outPose.position += authored.delta.positionOffset; if (authored.delta.hasRotationEulerDegOffset) - outPose.orientation = hlsl::normalizeQuaternion(outPose.orientation * hlsl::makeQuaternionFromEulerDegreesYXZ(hlsl::getCastedVector(authored.delta.rotationEulerDegOffset))); + outPose.orientation = hlsl::CCameraMathUtilities::normalizeQuaternion(outPose.orientation * hlsl::CCameraMathUtilities::makeQuaternionFromEulerDegreesYXZ(hlsl::getCastedVector(authored.delta.rotationEulerDegOffset))); } if (!isSequenceTrackedTargetPoseFinite(outPose)) @@ -513,7 +513,7 @@ struct CCameraSequenceScriptUtilities final normalized.reserve(outTrack.keyframes.size()); for (const auto& keyframe : outTrack.keyframes) { - if (!normalized.empty() && hlsl::nearlyEqualScalar(normalized.back().time, keyframe.time, static_cast(ICamera::ScalarTolerance))) + if (!normalized.empty() && hlsl::CCameraMathUtilities::nearlyEqualScalar(normalized.back().time, keyframe.time, static_cast(ICamera::ScalarTolerance))) normalized.back() = keyframe; else normalized.emplace_back(keyframe); @@ -551,7 +551,7 @@ struct CCameraSequenceScriptUtilities final const auto span = std::max(static_cast(ICamera::ScalarTolerance), rhs.time - lhs.time); const auto alpha = std::clamp((time - lhs.time) / span, 0.f, 1.f); outPose.position = lhs.pose.position + (rhs.pose.position - lhs.pose.position) * static_cast(alpha); - outPose.orientation = hlsl::slerpQuaternion(lhs.pose.orientation, rhs.pose.orientation, static_cast(alpha)); + outPose.orientation = hlsl::CCameraMathUtilities::slerpQuaternion(lhs.pose.orientation, rhs.pose.orientation, static_cast(alpha)); return true; } @@ -720,3 +720,4 @@ struct CCameraSequenceScriptUtilities final } // namespace nbl::core #endif // _C_CAMERA_SEQUENCE_SCRIPT_HPP_ + diff --git a/common/include/camera/CCameraSmokeRegressionUtilities.hpp b/common/include/camera/CCameraSmokeRegressionUtilities.hpp index 6931c5f35..63c7fccd3 100644 --- a/common/include/camera/CCameraSmokeRegressionUtilities.hpp +++ b/common/include/camera/CCameraSmokeRegressionUtilities.hpp @@ -45,8 +45,8 @@ struct CCameraSmokeRegressionUtilities final const auto& gimbal = camera->getGimbal(); const auto afterPosition = gimbal.getPosition(); - const auto afterOrientation = hlsl::normalizeQuaternion(gimbal.getOrientation()); - return hlsl::tryComputePoseDelta(afterPosition, afterOrientation, beforePosition, beforeOrientation, outDelta); + const auto afterOrientation = hlsl::CCameraMathUtilities::normalizeQuaternion(gimbal.getOrientation()); + return hlsl::CCameraMathUtilities::tryComputePoseDelta(afterPosition, afterOrientation, beforePosition, beforeOrientation, outDelta); } //! Manipulate a camera and report how far its pose moved in position and Euler-angle terms. @@ -62,8 +62,8 @@ struct CCameraSmokeRegressionUtilities final const auto& beforeGimbal = camera->getGimbal(); const auto beforePosition = beforeGimbal.getPosition(); - const auto beforeOrientation = hlsl::normalizeQuaternion(beforeGimbal.getOrientation()); - if (!hlsl::isFiniteVec3(beforePosition) || !hlsl::isFiniteQuaternion(beforeOrientation)) + const auto beforeOrientation = hlsl::CCameraMathUtilities::normalizeQuaternion(beforeGimbal.getOrientation()); + if (!hlsl::CCameraMathUtilities::isFiniteVec3(beforePosition) || !hlsl::CCameraMathUtilities::isFiniteQuaternion(beforeOrientation)) return false; if (!camera->manipulate(events)) diff --git a/common/include/camera/CCameraTargetRelativeUtilities.hpp b/common/include/camera/CCameraTargetRelativeUtilities.hpp index 656914822..5ede838f0 100644 --- a/common/include/camera/CCameraTargetRelativeUtilities.hpp +++ b/common/include/camera/CCameraTargetRelativeUtilities.hpp @@ -20,7 +20,7 @@ struct SCameraTargetRelativeState final struct SCameraTargetRelativePose final { hlsl::float64_t3 position = hlsl::float64_t3(0.0); - hlsl::camera_quaternion_t orientation = hlsl::makeIdentityQuaternion(); + hlsl::camera_quaternion_t orientation = hlsl::CCameraMathUtilities::makeIdentityQuaternion(); hlsl::float64_t appliedDistance = static_cast(ICamera::SphericalMinDistance); }; @@ -120,7 +120,7 @@ struct CCameraTargetRelativeUtilities final outState.target = targetPosition; hlsl::float64_t appliedDistance = static_cast(minDistance); - if (!hlsl::tryBuildOrbitFromPosition( + if (!hlsl::CCameraMathUtilities::tryBuildOrbitFromPosition( targetPosition, position, static_cast(minDistance), @@ -143,7 +143,7 @@ struct CCameraTargetRelativeUtilities final SCameraTargetRelativePose& outPose) { outPose = {}; - return hlsl::tryBuildSphericalPoseFromOrbit( + return hlsl::CCameraMathUtilities::tryBuildSphericalPoseFromOrbit( state.target, state.orbitUv.x, state.orbitUv.y, @@ -166,7 +166,7 @@ struct CCameraTargetRelativeUtilities final return false; outBasis.localOffset = pose.position - state.target; - const auto basis = hlsl::getQuaternionBasisMatrix(pose.orientation); + const auto basis = hlsl::CCameraMathUtilities::getQuaternionBasisMatrix(pose.orientation); outBasis.right = basis[0]; outBasis.up = basis[1]; outBasis.forward = basis[2]; @@ -199,8 +199,8 @@ struct CCameraTargetRelativeUtilities final { return { .orbitUv = hlsl::float64_t2( - hlsl::wrapAngleRad(desiredState.orbitUv.x - currentState.orbitUv.x), - hlsl::wrapAngleRad(desiredState.orbitUv.y - currentState.orbitUv.y)), + hlsl::CCameraMathUtilities::wrapAngleRad(desiredState.orbitUv.x - currentState.orbitUv.x), + hlsl::CCameraMathUtilities::wrapAngleRad(desiredState.orbitUv.y - currentState.orbitUv.y)), .distance = static_cast(desiredState.distance - currentState.distance) }; } @@ -268,3 +268,4 @@ struct CCameraTargetRelativeUtilities final } // namespace nbl::core #endif // _C_CAMERA_TARGET_RELATIVE_UTILITIES_HPP_ + diff --git a/common/include/camera/CCameraVirtualEventUtilities.hpp b/common/include/camera/CCameraVirtualEventUtilities.hpp index e2e800c05..134a7d0ff 100644 --- a/common/include/camera/CCameraVirtualEventUtilities.hpp +++ b/common/include/camera/CCameraVirtualEventUtilities.hpp @@ -36,7 +36,7 @@ struct CCameraVirtualEventUtilities final const CVirtualGimbalEvent::VirtualEventType negative, const double tolerance = static_cast(ICamera::TinyScalarEpsilon)) { - if (!hlsl::isFiniteScalar(value) || hlsl::isNearlyZeroScalar(value, tolerance)) + if (!hlsl::CCameraMathUtilities::isFiniteScalar(value) || hlsl::CCameraMathUtilities::isNearlyZeroScalar(value, tolerance)) return; auto& ev = events.emplace_back(); @@ -52,7 +52,7 @@ struct CCameraVirtualEventUtilities final const CVirtualGimbalEvent::VirtualEventType positive, const CVirtualGimbalEvent::VirtualEventType negative) { - if (!hlsl::isFiniteScalar(denominator) || hlsl::isNearlyZeroScalar(denominator, static_cast(ICamera::TinyScalarEpsilon))) + if (!hlsl::CCameraMathUtilities::isFiniteScalar(denominator) || hlsl::CCameraMathUtilities::isNearlyZeroScalar(denominator, static_cast(ICamera::TinyScalarEpsilon))) return; appendSignedVirtualEvent(events, value / denominator, positive, negative, tolerance); @@ -66,8 +66,8 @@ struct CCameraVirtualEventUtilities final const CVirtualGimbalEvent::VirtualEventType positive, const CVirtualGimbalEvent::VirtualEventType negative) { - if (!hlsl::isFiniteScalar(deltaRadians) || - hlsl::isNearlyZeroScalar(hlsl::degrees(deltaRadians), toleranceDeg)) + if (!hlsl::CCameraMathUtilities::isFiniteScalar(deltaRadians) || + hlsl::CCameraMathUtilities::isNearlyZeroScalar(hlsl::degrees(deltaRadians), toleranceDeg)) { return; } @@ -123,7 +123,7 @@ struct CCameraVirtualEventUtilities final { appendLocalTranslationEvents( events, - hlsl::projectWorldVectorToLocalQuaternionFrame(orientation, worldDelta), + hlsl::CCameraMathUtilities::projectWorldVectorToLocalQuaternionFrame(orientation, worldDelta), denominators, tolerances); } @@ -170,3 +170,4 @@ struct CCameraVirtualEventUtilities final } // namespace nbl::core #endif // _C_CAMERA_VIRTUAL_EVENT_UTILITIES_HPP_ + diff --git a/common/include/camera/CChaseCamera.hpp b/common/include/camera/CChaseCamera.hpp index f9a1ab211..164fb5791 100644 --- a/common/include/camera/CChaseCamera.hpp +++ b/common/include/camera/CChaseCamera.hpp @@ -37,10 +37,10 @@ class CChaseCamera final : public CSphericalTargetCamera const auto basis = computeBasis(m_orbitUv, m_distance); - const auto planarForward = hlsl::safeNormalizeVec3( + const auto planarForward = hlsl::CCameraMathUtilities::safeNormalizeVec3( hlsl::float64_t3(basis.forward.x, 0.0, basis.forward.z), hlsl::float64_t3(0.0, 0.0, 1.0)); - const auto planarRight = hlsl::safeNormalizeVec3( + const auto planarRight = hlsl::CCameraMathUtilities::safeNormalizeVec3( hlsl::float64_t3(basis.right.x, 0.0, basis.right.z), hlsl::float64_t3(1.0, 0.0, 0.0)); diff --git a/common/include/camera/CDollyCamera.hpp b/common/include/camera/CDollyCamera.hpp index 5ed2be2be..2ddefc0a2 100644 --- a/common/include/camera/CDollyCamera.hpp +++ b/common/include/camera/CDollyCamera.hpp @@ -35,7 +35,7 @@ class CDollyCamera final : public CSphericalTargetCamera const auto deltaTranslation = scaleVirtualTranslation(impulse.dVirtualTranslate); const auto basis = computeBasis(m_orbitUv, m_distance); - const auto delta = hlsl::transformLocalVectorToWorldBasis(deltaTranslation, basis.right, basis.up, basis.forward); + const auto delta = hlsl::CCameraMathUtilities::transformLocalVectorToWorldBasis(deltaTranslation, basis.right, basis.up, basis.forward); m_targetPosition += delta; m_orbitUv.x += deltaRotation.y; diff --git a/common/include/camera/CDollyZoomCamera.hpp b/common/include/camera/CDollyZoomCamera.hpp index 4cfe41170..231c9f230 100644 --- a/common/include/camera/CDollyZoomCamera.hpp +++ b/common/include/camera/CDollyZoomCamera.hpp @@ -69,7 +69,7 @@ class CDollyZoomCamera final : public CSphericalTargetCamera } virtual bool trySetDynamicPerspectiveState(const DynamicPerspectiveState& state) override { - if (!hlsl::isFiniteScalar(state.baseFov) || !hlsl::isFiniteScalar(state.referenceDistance) || state.referenceDistance <= 0.f) + if (!hlsl::CCameraMathUtilities::isFiniteScalar(state.baseFov) || !hlsl::CCameraMathUtilities::isFiniteScalar(state.referenceDistance) || state.referenceDistance <= 0.f) return false; m_baseFov = state.baseFov; @@ -91,3 +91,4 @@ class CDollyZoomCamera final : public CSphericalTargetCamera } #endif + diff --git a/common/include/camera/CFPSCamera.hpp b/common/include/camera/CFPSCamera.hpp index ed8e869fc..f6f0741f5 100644 --- a/common/include/camera/CFPSCamera.hpp +++ b/common/include/camera/CFPSCamera.hpp @@ -24,13 +24,13 @@ class CFPSCamera final : public ICamera static inline constexpr float InvertedRollDeg = 180.0f; }; - CFPSCamera(const hlsl::float64_t3& position, const hlsl::camera_quaternion_t& orientation = hlsl::makeIdentityQuaternion()) + CFPSCamera(const hlsl::float64_t3& position, const hlsl::camera_quaternion_t& orientation = hlsl::CCameraMathUtilities::makeIdentityQuaternion()) : base_t(), m_gimbal({ .position = position, .orientation = orientation }) { m_gimbal.begin(); { - const auto pitchYaw = hlsl::getPitchYawFromForwardVector(m_gimbal.getZAxis()); - m_gimbal.setOrientation(hlsl::makeQuaternionFromEulerRadiansYXZ(hlsl::float64_t3(pitchYaw.x, pitchYaw.y, 0.0))); + const auto pitchYaw = hlsl::CCameraMathUtilities::getPitchYawFromForwardVector(m_gimbal.getZAxis()); + m_gimbal.setOrientation(hlsl::CCameraMathUtilities::makeQuaternionFromEulerRadiansYXZ(hlsl::float64_t3(pitchYaw.x, pitchYaw.y, 0.0))); } m_gimbal.end(); } @@ -54,11 +54,11 @@ class CFPSCamera final : public ICamera { if (referenceFrame) { - const float roll = static_cast(hlsl::degrees(hlsl::getQuaternionEulerRadiansYXZ(reference.orientation).z)); + const float roll = static_cast(hlsl::degrees(hlsl::CCameraMathUtilities::getQuaternionEulerRadiansYXZ(reference.orientation).z)); const bool matchesStraightRoll = - hlsl::getWrappedAngleDistanceDegrees(roll, SFpsCameraDefaults::StraightRollDeg) <= SFpsCameraDefaults::RollValidationEpsilonDeg; + hlsl::CCameraMathUtilities::getWrappedAngleDistanceDegrees(roll, SFpsCameraDefaults::StraightRollDeg) <= SFpsCameraDefaults::RollValidationEpsilonDeg; const bool matchesInvertedRoll = - hlsl::getWrappedAngleDistanceDegrees(roll, SFpsCameraDefaults::InvertedRollDeg) <= SFpsCameraDefaults::RollValidationEpsilonDeg; + hlsl::CCameraMathUtilities::getWrappedAngleDistanceDegrees(roll, SFpsCameraDefaults::InvertedRollDeg) <= SFpsCameraDefaults::RollValidationEpsilonDeg; if (!(matchesStraightRoll || matchesInvertedRoll)) return false; @@ -73,13 +73,13 @@ class CFPSCamera final : public ICamera m_gimbal.begin(); { - const auto pitchYaw = hlsl::getPitchYawFromForwardVector(hlsl::float64_t3(reference.frame[2])); + const auto pitchYaw = hlsl::CCameraMathUtilities::getPitchYawFromForwardVector(hlsl::float64_t3(reference.frame[2])); const float newPitch = std::clamp(static_cast(pitchYaw.x + scaleVirtualRotation(impulse.dVirtualRotation.x)), MinVerticalAngle, MaxVerticalAngle); const float newYaw = static_cast(pitchYaw.y + scaleVirtualRotation(impulse.dVirtualRotation.y)); if (validateReference()) - m_gimbal.setOrientation(hlsl::makeQuaternionFromEulerRadiansYXZ(hlsl::float64_t3(newPitch, newYaw, 0.0f))); - m_gimbal.setPosition(hlsl::float64_t3(reference.frame[3]) + hlsl::rotateVectorByQuaternion(reference.orientation, hlsl::float64_t3(impulse.dVirtualTranslate))); + m_gimbal.setOrientation(hlsl::CCameraMathUtilities::makeQuaternionFromEulerRadiansYXZ(hlsl::float64_t3(newPitch, newYaw, 0.0f))); + m_gimbal.setPosition(hlsl::float64_t3(reference.frame[3]) + hlsl::CCameraMathUtilities::rotateVectorByQuaternion(reference.orientation, hlsl::float64_t3(impulse.dVirtualTranslate))); } m_gimbal.end(); @@ -118,3 +118,4 @@ class CFPSCamera final : public ICamera } #endif // _C_FPS_CAMERA_HPP_ + diff --git a/common/include/camera/CFreeLockCamera.hpp b/common/include/camera/CFreeLockCamera.hpp index ad6971ef9..81b2599b6 100644 --- a/common/include/camera/CFreeLockCamera.hpp +++ b/common/include/camera/CFreeLockCamera.hpp @@ -16,7 +16,7 @@ class CFreeCamera final : public ICamera public: using base_t = ICamera; - CFreeCamera(const hlsl::float64_t3& position, const hlsl::camera_quaternion_t& orientation = hlsl::makeIdentityQuaternion()) + CFreeCamera(const hlsl::float64_t3& position, const hlsl::camera_quaternion_t& orientation = hlsl::CCameraMathUtilities::makeIdentityQuaternion()) : base_t(), m_gimbal({ .position = position, .orientation = orientation }) {} ~CFreeCamera() = default; @@ -40,12 +40,12 @@ class CFreeCamera final : public ICamera m_gimbal.begin(); { - const auto pitch = hlsl::makeQuaternionFromAxisAngle(hlsl::normalize(hlsl::float64_t3(reference.frame[0])), impulse.dVirtualRotation.x); - const auto yaw = hlsl::makeQuaternionFromAxisAngle(hlsl::normalize(hlsl::float64_t3(reference.frame[1])), impulse.dVirtualRotation.y); - const auto roll = hlsl::makeQuaternionFromAxisAngle(hlsl::normalize(hlsl::float64_t3(reference.frame[2])), impulse.dVirtualRotation.z); + const auto pitch = hlsl::CCameraMathUtilities::makeQuaternionFromAxisAngle(hlsl::normalize(hlsl::float64_t3(reference.frame[0])), impulse.dVirtualRotation.x); + const auto yaw = hlsl::CCameraMathUtilities::makeQuaternionFromAxisAngle(hlsl::normalize(hlsl::float64_t3(reference.frame[1])), impulse.dVirtualRotation.y); + const auto roll = hlsl::CCameraMathUtilities::makeQuaternionFromAxisAngle(hlsl::normalize(hlsl::float64_t3(reference.frame[2])), impulse.dVirtualRotation.z); - m_gimbal.setOrientation(hlsl::normalizeQuaternion(yaw * pitch * roll * reference.orientation)); - m_gimbal.setPosition(hlsl::float64_t3(reference.frame[3]) + hlsl::rotateVectorByQuaternion(reference.orientation, hlsl::float64_t3(impulse.dVirtualTranslate))); + m_gimbal.setOrientation(hlsl::CCameraMathUtilities::normalizeQuaternion(yaw * pitch * roll * reference.orientation)); + m_gimbal.setPosition(hlsl::float64_t3(reference.frame[3]) + hlsl::CCameraMathUtilities::rotateVectorByQuaternion(reference.orientation, hlsl::float64_t3(impulse.dVirtualTranslate))); } m_gimbal.end(); diff --git a/common/include/camera/CSphericalTargetCamera.hpp b/common/include/camera/CSphericalTargetCamera.hpp index 261d28e2d..b2cde8efe 100644 --- a/common/include/camera/CSphericalTargetCamera.hpp +++ b/common/include/camera/CSphericalTargetCamera.hpp @@ -19,7 +19,7 @@ class CSphericalTargetCamera : public ICamera CSphericalTargetCamera(const hlsl::float64_t3& position, const hlsl::float64_t3& target) : base_t(), m_targetPosition(target), m_distance(SCameraTargetRelativeRigDefaults::InitialDistance), - m_gimbal({ .position = position, .orientation = hlsl::makeIdentityQuaternion() }) + m_gimbal({ .position = position, .orientation = hlsl::CCameraMathUtilities::makeIdentityQuaternion() }) { initFromPosition(position); } @@ -111,10 +111,10 @@ class CSphericalTargetCamera : public ICamera inline void applyPlanarTargetTranslation(const hlsl::float64_t3& deltaTranslation, const SphericalBasis& basis) { - if (!hlsl::hasPlanarDeltaXY(deltaTranslation, static_cast(base_t::TinyScalarEpsilon))) + if (!hlsl::CCameraMathUtilities::hasPlanarDeltaXY(deltaTranslation, static_cast(base_t::TinyScalarEpsilon))) return; - m_targetPosition += hlsl::transformLocalVectorToWorldBasis( + m_targetPosition += hlsl::CCameraMathUtilities::transformLocalVectorToWorldBasis( hlsl::float64_t3(deltaTranslation.x, deltaTranslation.y, 0.0), basis.right, basis.up, @@ -156,3 +156,4 @@ class CSphericalTargetCamera : public ICamera } #endif + diff --git a/common/include/camera/IGimbal.hpp b/common/include/camera/IGimbal.hpp index 00a4acf29..e4f880008 100644 --- a/common/include/camera/IGimbal.hpp +++ b/common/include/camera/IGimbal.hpp @@ -9,7 +9,7 @@ namespace nbl::core struct CReferenceTransform { hlsl::float64_t4x4 frame; - hlsl::camera_quaternion_t orientation = hlsl::makeIdentityQuaternion(); + hlsl::camera_quaternion_t orientation = hlsl::CCameraMathUtilities::makeIdentityQuaternion(); }; template @@ -127,7 +127,7 @@ namespace nbl::core struct SCreationParameters { vector_t<3u> position; - quaternion_t orientation = hlsl::makeIdentityQuaternion(); + quaternion_t orientation = hlsl::CCameraMathUtilities::makeIdentityQuaternion(); }; IGimbal(const IGimbal&) = default; @@ -169,13 +169,13 @@ namespace nbl::core if (m_orientation.data != orientation.data) m_counter++; - m_orientation = hlsl::normalizeQuaternion(orientation); + m_orientation = hlsl::CCameraMathUtilities::normalizeQuaternion(orientation); updateOrthonormalOrientationBase(); } inline void transform(const CReferenceTransform& reference, const VirtualImpulse& impulse) { - setOrientation(reference.orientation * hlsl::makeQuaternionFromEulerRadiansYXZ(impulse.dVirtualRotation)); + setOrientation(reference.orientation * hlsl::CCameraMathUtilities::makeQuaternionFromEulerRadiansYXZ(impulse.dVirtualRotation)); setPosition(hlsl::mul(hlsl::float64_t4(impulse.dVirtualTranslate, 1), reference.frame).xyz); } @@ -186,8 +186,8 @@ namespace nbl::core if(dRadians) m_counter++; - const auto dRotation = hlsl::makeQuaternionFromAxisAngle(axis, static_cast(dRadians)); - m_orientation = hlsl::normalizeQuaternion(dRotation * m_orientation); + const auto dRotation = hlsl::CCameraMathUtilities::makeQuaternionFromAxisAngle(axis, static_cast(dRadians)); + m_orientation = hlsl::CCameraMathUtilities::normalizeQuaternion(dRotation * m_orientation); updateOrthonormalOrientationBase(); } @@ -293,13 +293,13 @@ namespace nbl::core if (referenceFrame) { - if (!hlsl::tryBuildRigidFrameFromTransform(*referenceFrame, out->frame, out->orientation)) + if (!hlsl::CCameraMathUtilities::tryBuildRigidFrameFromTransform(*referenceFrame, out->frame, out->orientation)) return false; } else { out->orientation = getOrientation(); - out->frame = hlsl::composeTransformMatrix(getPosition(), out->orientation); + out->frame = hlsl::CCameraMathUtilities::composeTransformMatrix(getPosition(), out->orientation); } return true; @@ -308,7 +308,7 @@ namespace nbl::core private: inline void updateOrthonormalOrientationBase() { - m_orthonormal = hlsl::getQuaternionBasisMatrix(m_orientation); + m_orthonormal = hlsl::CCameraMathUtilities::getQuaternionBasisMatrix(m_orientation); } //! Position of a gimbal in world space diff --git a/common/include/camera/IGimbalInputProcessor.hpp b/common/include/camera/IGimbalInputProcessor.hpp index 5a3d3e262..613352ea1 100644 --- a/common/include/camera/IGimbalInputProcessor.hpp +++ b/common/include/camera/IGimbalInputProcessor.hpp @@ -236,7 +236,7 @@ class IGimbalInputProcessor : public CGimbalBindingLayoutStorage const auto& deltaWorldTRS = ev; hlsl::SRigidTransformComponents world = {}; - if (!hlsl::tryExtractRigidTransformComponents(deltaWorldTRS, world)) + if (!hlsl::CCameraMathUtilities::tryExtractRigidTransformComponents(deltaWorldTRS, world)) continue; requestMagnitudeUpdateWithSignedComponents( @@ -245,7 +245,7 @@ class IGimbalInputProcessor : public CGimbalBindingLayoutStorage SInputProcessorBindingGroups::ImguizmoTranslation, map); - const auto dRotationRad = hlsl::getCameraOrientationEulerRadians(world.orientation); + const auto dRotationRad = hlsl::CCameraMathUtilities::getCameraOrientationEulerRadians(world.orientation); requestMagnitudeUpdateWithSignedComponents( ZeroPivot, dRotationRad, diff --git a/common/src/nbl/examples/camera/CCameraJsonPersistenceUtilities.hpp b/common/src/nbl/examples/camera/CCameraJsonPersistenceUtilities.hpp index f3dda6942..91da90a92 100644 --- a/common/src/nbl/examples/camera/CCameraJsonPersistenceUtilities.hpp +++ b/common/src/nbl/examples/camera/CCameraJsonPersistenceUtilities.hpp @@ -31,7 +31,7 @@ inline void deserializeGoalJson(const Json& entry, core::CCameraGoal& goal) if (entry.contains("orientation") && entry["orientation"].is_array()) { const auto values = entry["orientation"].get>(); - goal.orientation = hlsl::makeQuaternionFromComponents(values[0], values[1], values[2], values[3]); + goal.orientation = hlsl::CCameraMathUtilities::makeQuaternionFromComponents(values[0], values[1], values[2], values[3]); } if (entry.contains("target_position") && entry["target_position"].is_array()) { diff --git a/common/src/nbl/examples/camera/CCameraScriptedRuntimePersistence.cpp b/common/src/nbl/examples/camera/CCameraScriptedRuntimePersistence.cpp index 489e63ae2..0df587d67 100644 --- a/common/src/nbl/examples/camera/CCameraScriptedRuntimePersistence.cpp +++ b/common/src/nbl/examples/camera/CCameraScriptedRuntimePersistence.cpp @@ -541,9 +541,9 @@ nbl::hlsl::float32_t4x4 composeScriptedImguizmoTransform( const std::array& rotationDeg, const std::array& scale) { - return nbl::hlsl::composeTransformMatrix( + return nbl::hlsl::CCameraMathUtilities::composeTransformMatrix( nbl::hlsl::float32_t3(translation[0], translation[1], translation[2]), - nbl::hlsl::makeQuaternionFromEulerDegrees(nbl::hlsl::float32_t3(rotationDeg[0], rotationDeg[1], rotationDeg[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])); } From 853998bd04bbdbc21f3a922e51592f2987e43a40 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Thu, 9 Apr 2026 12:30:17 +0200 Subject: [PATCH 187/205] Refactor camera overlay utilities --- 61_UI/AppControlPanel.cpp | 8 +- 61_UI/AppControlPanelCameraTab.cpp | 42 +- 61_UI/AppControlPanelPlaybackTab.cpp | 48 +- 61_UI/AppControlPanelPresetsTab.cpp | 30 +- 61_UI/AppControlPanelProjectionTab.cpp | 26 +- 61_UI/AppControlPanelStatusTab.cpp | 46 +- 61_UI/AppControlPanelTabs.cpp | 20 +- 61_UI/AppControlPanelUtilityTabs.cpp | 16 +- 61_UI/AppFollowRuntime.cpp | 2 +- 61_UI/AppTransformEditor.cpp | 4 +- 61_UI/AppViewportGizmos.cpp | 4 +- 61_UI/AppViewportWindows.cpp | 10 +- .../app/AppControlPanelAuthoringUtilities.hpp | 10 +- .../AppProjectionControlPanelUiUtilities.hpp | 22 +- .../app/AppViewportWindowUtilities.hpp | 2 +- 61_UI/include/common.hpp | 3 + .../camera/CCameraControlPanelUiUtilities.hpp | 685 +++++++++--------- ...ameraScriptVisualDebugOverlayUtilities.hpp | 183 ++--- .../CCameraScriptedRuntimePersistence.hpp | 9 +- .../CCameraViewportOverlayUtilities.hpp | 270 +++---- .../CCameraScriptedRuntimePersistence.cpp | 38 +- 21 files changed, 744 insertions(+), 734 deletions(-) diff --git a/61_UI/AppControlPanel.cpp b/61_UI/AppControlPanel.cpp index 1387c6b18..f05acf883 100644 --- a/61_UI/AppControlPanel.cpp +++ b/61_UI/AppControlPanel.cpp @@ -34,12 +34,12 @@ void App::DrawControlPanel() { const nbl::ui::SCameraControlPanelStyle panelStyle = {}; const ImVec2 displaySize = ImGui::GetIO().DisplaySize; - const ImVec2 panelSize = nbl::ui::calcControlPanelWindowSize(displaySize, panelStyle); + 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::pushControlPanelWindowStyle(panelStyle); + nbl::ui::CCameraControlPanelUiUtilities::pushControlPanelWindowStyle(panelStyle); ImGui::SetNextWindowCollapsed(false, ImGuiCond_Always); ImGui::SetNextWindowBgAlpha(0.0f); if (m_cliRuntime.ciMode) @@ -47,7 +47,7 @@ void App::DrawControlPanel() ImGui::Begin("Control Panel", nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar); if (auto* drawList = ImGui::GetWindowDrawList(); drawList) - nbl::ui::drawControlPanelWindowBackdrop(*drawList, ImGui::GetWindowPos(), ImGui::GetWindowSize(), panelStyle); + nbl::ui::CCameraControlPanelUiUtilities::drawControlPanelWindowBackdrop(*drawList, ImGui::GetWindowPos(), ImGui::GetWindowSize(), panelStyle); drawControlPanelHeader(panelStyle); ImGui::Spacing(); @@ -56,6 +56,6 @@ void App::DrawControlPanel() drawControlPanelTabs(panelStyle); ImGui::End(); - nbl::ui::popControlPanelWindowStyle(); + nbl::ui::CCameraControlPanelUiUtilities::popControlPanelWindowStyle(); } diff --git a/61_UI/AppControlPanelCameraTab.cpp b/61_UI/AppControlPanelCameraTab.cpp index 4b08da22a..82e80a89b 100644 --- a/61_UI/AppControlPanelCameraTab.cpp +++ b/61_UI/AppControlPanelCameraTab.cpp @@ -5,20 +5,20 @@ void App::drawControlPanelCameraTab(const nbl::ui::SCameraControlPanelStyle& pan using checkbox_spec_t = nbl::ui::SCameraControlPanelCheckboxSpec; using slider_spec_t = nbl::ui::SCameraControlPanelSliderSpec; - if (!nbl::ui::beginControlPanelTabChild("CameraPanel", panelStyle)) + if (!nbl::ui::CCameraControlPanelUiUtilities::beginControlPanelTabChild("CameraPanel", panelStyle)) { - nbl::ui::endControlPanelTabChild(); + nbl::ui::CCameraControlPanelUiUtilities::endControlPanelTabChild(); return; } ImGui::PushItemWidth(-1.0f); - nbl::ui::drawSectionHeader("CameraInputHeader", "Input", panelStyle.AccentColor, panelStyle); + 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::drawCheckboxWithHint(spec); + 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" }, @@ -28,23 +28,23 @@ void App::drawControlPanelCameraTab(const nbl::ui::SCameraControlPanelStyle& pan 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::drawSliderFloatWithHint(spec); + nbl::ui::CCameraControlPanelUiUtilities::drawSliderFloatWithHint(spec); } - nbl::ui::drawSectionHeader("CameraConstraintsHeader", "Constraints", panelStyle.AccentColor, panelStyle); + 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::drawCheckboxWithHint(spec); + 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::drawSliderFloatWithHint(spec); + nbl::ui::CCameraControlPanelUiUtilities::drawSliderFloatWithHint(spec); } ImGui::Separator(); for (const auto& spec : { @@ -53,7 +53,7 @@ void App::drawControlPanelCameraTab(const nbl::ui::SCameraControlPanelStyle& pan checkbox_spec_t{ .label = "Clamp roll", .value = &m_cameraConstraints.clampRoll, .hint = "Clamp roll angle" } }) { - nbl::ui::drawCheckboxWithHint(spec); + 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" }, @@ -64,10 +64,10 @@ void App::drawControlPanelCameraTab(const nbl::ui::SCameraControlPanelStyle& pan 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::drawSliderFloatWithHint(spec); + nbl::ui::CCameraControlPanelUiUtilities::drawSliderFloatWithHint(spec); } - nbl::ui::drawSectionHeader("OrbitHeader", "Orbit Target", panelStyle.AccentColor, panelStyle); + nbl::ui::CCameraControlPanelUiUtilities::drawSectionHeader("OrbitHeader", "Orbit Target", panelStyle.AccentColor, panelStyle); auto* activeCamera = getActiveCamera(); ICamera::SphericalTargetState orbitState; const bool hasOrbitTarget = activeCamera && activeCamera->tryGetSphericalTargetState(orbitState); @@ -77,13 +77,13 @@ void App::drawControlPanelCameraTab(const nbl::ui::SCameraControlPanelStyle& pan if (ImGui::InputFloat3("Target", &target[0])) activeCamera->trySetSphericalTarget(getCastedVector(target)); - if (nbl::ui::drawActionButtonWithHint("Target model", "Set orbit target to the model position")) + if (nbl::ui::CCameraControlPanelUiUtilities::drawActionButtonWithHint("Target model", "Set orbit target to the model position")) { const auto targetPos = hlsl::transpose(getMatrix3x4As4x4(m_sceneInteraction.model))[3]; activeCamera->trySetSphericalTarget(float64_t3(targetPos.x, targetPos.y, targetPos.z)); } ImGui::SameLine(); - if (nbl::ui::drawActionButtonWithHint("Target origin", "Set orbit target to world origin")) + if (nbl::ui::CCameraControlPanelUiUtilities::drawActionButtonWithHint("Target origin", "Set orbit target to world origin")) activeCamera->trySetSphericalTarget(float64_t3(0.0)); } else @@ -91,13 +91,13 @@ void App::drawControlPanelCameraTab(const nbl::ui::SCameraControlPanelStyle& pan ImGui::TextDisabled("Active camera is not orbit."); } - nbl::ui::drawSectionHeader("FollowHeader", "Follow Target", panelStyle.AccentColor, panelStyle); + 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::drawCheckboxWithHint({ .label = "Enable follow", .value = &followConfig.enabled, .hint = "Apply tracked-target follow to the active planar camera" }); + 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), @@ -120,18 +120,18 @@ void App::drawControlPanelCameraTab(const nbl::ui::SCameraControlPanelStyle& pan if (ImGui::InputFloat3("Tracked target", &trackedTarget[0])) m_sceneInteraction.followTarget.setPosition(getCastedVector(trackedTarget)); - nbl::ui::drawCheckboxWithHint({ .label = "Show target marker", .value = &m_sceneInteraction.followTargetVisible, .hint = "Render the tracked target marker in the scene" }); + nbl::ui::CCameraControlPanelUiUtilities::drawCheckboxWithHint({ .label = "Show target marker", .value = &m_sceneInteraction.followTargetVisible, .hint = "Render the tracked target marker in the scene" }); - if (nbl::ui::drawActionButtonWithHint("Reset target", "Reset tracked target gimbal to the default world-space follow pose")) + if (nbl::ui::CCameraControlPanelUiUtilities::drawActionButtonWithHint("Reset target", "Reset tracked target gimbal to the default world-space follow pose")) resetFollowTargetToDefault(); ImGui::SameLine(); - if (nbl::ui::drawActionButtonWithHint("Snap to model", "Optionally snap tracked target gimbal to the model transform")) + if (nbl::ui::CCameraControlPanelUiUtilities::drawActionButtonWithHint("Snap to model", "Optionally snap tracked target gimbal to the model transform")) snapFollowTargetToModel(); ImGui::SameLine(); - if (nbl::ui::drawActionButtonWithHint("Target origin", "Reset tracked target to identity at world origin")) + 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::drawActionButtonWithHint("Capture current offset", "Store current camera-to-target relation into the active follow config")) + 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)) @@ -153,5 +153,5 @@ void App::drawControlPanelCameraTab(const nbl::ui::SCameraControlPanelStyle& pan } ImGui::PopItemWidth(); - nbl::ui::endControlPanelTabChild(); + nbl::ui::CCameraControlPanelUiUtilities::endControlPanelTabChild(); } diff --git a/61_UI/AppControlPanelPlaybackTab.cpp b/61_UI/AppControlPanelPlaybackTab.cpp index 3b3de2ee5..d1ef7c4cb 100644 --- a/61_UI/AppControlPanelPlaybackTab.cpp +++ b/61_UI/AppControlPanelPlaybackTab.cpp @@ -11,24 +11,24 @@ void App::drawControlPanelPlaybackTab(const nbl::ui::SCameraControlPanelStyle& p auto& playbackAuthoring = m_playbackAuthoring; - if (!nbl::ui::beginControlPanelTabChild("PlaybackPanel", panelStyle)) + if (!nbl::ui::CCameraControlPanelUiUtilities::beginControlPanelTabChild("PlaybackPanel", panelStyle)) { - nbl::ui::endControlPanelTabChild(); + nbl::ui::CCameraControlPanelUiUtilities::endControlPanelTabChild(); return; } ImGui::PushItemWidth(-1.0f); auto* activeCamera = getActiveCamera(); - nbl::ui::drawSectionHeader("PlaybackHeader", "Playback", panelStyle.AccentColor, panelStyle); + 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::drawCheckboxWithHint(spec); + nbl::ui::CCameraControlPanelUiUtilities::drawCheckboxWithHint(spec); } - nbl::ui::drawSliderFloatWithHint({ + nbl::ui::CCameraControlPanelUiUtilities::drawSliderFloatWithHint({ .label = "Speed", .value = &playbackAuthoring.playback.speed, .minValue = SCameraAppAuthoringDefaults::PlaybackSpeedMin, @@ -37,10 +37,10 @@ void App::drawControlPanelPlaybackTab(const nbl::ui::SCameraControlPanelStyle& p .hint = "Playback speed multiplier" }); - if (nbl::ui::drawActionButtonWithHint(playbackAuthoring.playback.playing ? "Pause" : "Play", "Start or pause playback")) + 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::drawActionButtonWithHint("Stop", "Stop playback and reset time")) + if (nbl::ui::CCameraControlPanelUiUtilities::drawActionButtonWithHint("Stop", "Stop playback and reset time")) { nbl::core::CCameraPlaybackTimelineUtilities::resetPlaybackCursor(playbackAuthoring.playback); applyPlaybackAtTime(playbackAuthoring.playback.time); @@ -63,7 +63,7 @@ void App::drawControlPanelPlaybackTab(const nbl::ui::SCameraControlPanelStyle& p if (tryBuildPlaybackPresetAtTime(playbackAuthoring.playback.time, playbackPreviewPreset)) { const auto playbackPreviewUi = analyzePresetForUi(activeCamera, playbackPreviewPreset); - nbl::ui::drawPolicyStatus({ + nbl::ui::CCameraControlPanelUiUtilities::drawPolicyStatus({ .label = "Preview", .value = playbackPreviewUi.policyLabel, .active = playbackPreviewUi.canApply @@ -71,16 +71,16 @@ void App::drawControlPanelPlaybackTab(const nbl::ui::SCameraControlPanelStyle& p } } - nbl::ui::drawSectionHeader("KeyframesHeader", "Keyframes", panelStyle.AccentColor, panelStyle); + nbl::ui::CCameraControlPanelUiUtilities::drawSectionHeader("KeyframesHeader", "Keyframes", panelStyle.AccentColor, panelStyle); ImGui::InputFloat("New keyframe time", &playbackAuthoring.newKeyframeTime, SCameraAppAuthoringDefaults::KeyframeTimeStep, SCameraAppAuthoringDefaults::KeyframeTimeFastStep, "%.3f"); - nbl::ui::drawHoverHint("Time value for new keyframe"); + nbl::ui::CCameraControlPanelUiUtilities::drawHoverHint("Time value for new keyframe"); ImGui::SameLine(); - if (nbl::ui::drawActionButtonWithHint("Use playback time", "Set new keyframe time from current playback position")) + 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::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")) + 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); @@ -95,13 +95,13 @@ void App::drawControlPanelPlaybackTab(const nbl::ui::SCameraControlPanelStyle& p } if (!keyframeCaptureUi.canCapture) ImGui::EndDisabled(); - nbl::ui::drawPolicyStatus({ + nbl::ui::CCameraControlPanelUiUtilities::drawPolicyStatus({ .label = "Capture", .value = keyframeCaptureUi.policyLabel, .active = keyframeCaptureUi.canCapture }, panelStyle); ImGui::SameLine(); - if (nbl::ui::drawActionButtonWithHint("Clear keyframes", "Remove all keyframes")) + if (nbl::ui::CCameraControlPanelUiUtilities::drawActionButtonWithHint("Clear keyframes", "Remove all keyframes")) { playbackAuthoring.keyframeTrack = {}; nbl::core::CCameraPlaybackTimelineUtilities::resetPlaybackCursor(playbackAuthoring.playback); @@ -134,31 +134,31 @@ void App::drawControlPanelPlaybackTab(const nbl::ui::SCameraControlPanelStyle& p selectKeyframeNearestTime(selectedTime); clampPlaybackTimeToKeyframes(); } - nbl::ui::drawHoverHint("Edit selected keyframe time"); + nbl::ui::CCameraControlPanelUiUtilities::drawHoverHint("Edit selected keyframe time"); nbl::ui::drawGoalApplyPresentationSummary(keyframeUi, panelStyle); if (!keyframeUi.canApply) ImGui::BeginDisabled(); - if (nbl::ui::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")) + 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::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")) + 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::drawActionButtonWithHint("Jump to selected", "Set playback time to selected keyframe and preview it")) + 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::drawActionButtonWithHint("Remove selected", "Remove selected keyframe")) + if (nbl::ui::CCameraControlPanelUiUtilities::drawActionButtonWithHint("Remove selected", "Remove selected keyframe")) { playbackAuthoring.keyframeTrack.keyframes.erase(playbackAuthoring.keyframeTrack.keyframes.begin() + playbackAuthoring.keyframeTrack.selectedKeyframeIx); normalizeSelectedKeyframe(); @@ -168,15 +168,15 @@ void App::drawControlPanelPlaybackTab(const nbl::ui::SCameraControlPanelStyle& p } } - nbl::ui::drawSectionHeader("KeyframesStorageHeader", "Keyframe Storage", panelStyle.AccentColor, panelStyle); - nbl::ui::inputTextString("Keyframe file", playbackAuthoring.keyframePath); - if (nbl::ui::drawActionButtonWithHint("Save keyframes", "Save keyframes to JSON file")) + 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::drawActionButtonWithHint("Load keyframes", "Load keyframes from JSON file")) + 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()); @@ -184,5 +184,5 @@ void App::drawControlPanelPlaybackTab(const nbl::ui::SCameraControlPanelStyle& p } ImGui::PopItemWidth(); - nbl::ui::endControlPanelTabChild(); + nbl::ui::CCameraControlPanelUiUtilities::endControlPanelTabChild(); } diff --git a/61_UI/AppControlPanelPresetsTab.cpp b/61_UI/AppControlPanelPresetsTab.cpp index fe229d90f..32a178745 100644 --- a/61_UI/AppControlPanelPresetsTab.cpp +++ b/61_UI/AppControlPanelPresetsTab.cpp @@ -9,20 +9,20 @@ void App::drawControlPanelPresetsTab(const nbl::ui::SCameraControlPanelStyle& pa { auto& presetAuthoring = m_presetAuthoring; - if (!nbl::ui::beginControlPanelTabChild("PresetsPanel", panelStyle)) + if (!nbl::ui::CCameraControlPanelUiUtilities::beginControlPanelTabChild("PresetsPanel", panelStyle)) { - nbl::ui::endControlPanelTabChild(); + nbl::ui::CCameraControlPanelUiUtilities::endControlPanelTabChild(); return; } ImGui::PushItemWidth(-1.0f); - nbl::ui::drawSectionHeader("PresetsHeader", "Presets", panelStyle.AccentColor, panelStyle); - nbl::ui::inputTextString("Preset name", presetAuthoring.presetName); + 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::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")) + 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)) @@ -34,12 +34,12 @@ void App::drawControlPanelPresetsTab(const nbl::ui::SCameraControlPanelStyle& pa if (!presetCaptureUi.canCapture) ImGui::EndDisabled(); ImGui::SameLine(); - if (nbl::ui::drawActionButtonWithHint("Clear presets", "Remove all presets")) + if (nbl::ui::CCameraControlPanelUiUtilities::drawActionButtonWithHint("Clear presets", "Remove all presets")) { presetAuthoring.presets.clear(); presetAuthoring.selectedPresetIx = -1; } - nbl::ui::drawPolicyStatus({ + nbl::ui::CCameraControlPanelUiUtilities::drawPolicyStatus({ .label = "Capture", .value = presetCaptureUi.policyLabel, .active = presetCaptureUi.canCapture @@ -55,7 +55,7 @@ void App::drawControlPanelPresetsTab(const nbl::ui::SCameraControlPanelStyle& pa int presetFilterIx = static_cast(presetAuthoring.filterMode); if (ImGui::Combo("Visibility", &presetFilterIx, presetFilterLabels, IM_ARRAYSIZE(presetFilterLabels))) presetAuthoring.filterMode = static_cast(presetFilterIx); - nbl::ui::drawHoverHint("Filter presets for the active camera using exact or best-effort compatibility"); + nbl::ui::CCameraControlPanelUiUtilities::drawHoverHint("Filter presets for the active camera using exact or best-effort compatibility"); std::vector filteredPresetIndices; filteredPresetIndices.reserve(presetAuthoring.presets.size()); @@ -112,12 +112,12 @@ void App::drawControlPanelPresetsTab(const nbl::ui::SCameraControlPanelStyle& pa if (!presetUi.canApply) ImGui::BeginDisabled(); - if (nbl::ui::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")) + 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::drawActionButtonWithHint("Remove preset", "Remove selected preset")) + if (nbl::ui::CCameraControlPanelUiUtilities::drawActionButtonWithHint("Remove preset", "Remove selected preset")) { presetAuthoring.presets.erase(presetAuthoring.presets.begin() + presetAuthoring.selectedPresetIx); presetAuthoring.selectedPresetIx = -1; @@ -132,20 +132,20 @@ void App::drawControlPanelPresetsTab(const nbl::ui::SCameraControlPanelStyle& pa presetAuthoring.applyBanner.approximate, panelStyle); - nbl::ui::drawSectionHeader("PresetsStorageHeader", "Storage", panelStyle.AccentColor, panelStyle); - nbl::ui::inputTextString("Preset file", presetAuthoring.presetPath); - if (nbl::ui::drawActionButtonWithHint("Save presets", "Save presets to JSON file")) + 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::drawActionButtonWithHint("Load presets", "Load presets from JSON file")) + 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::endControlPanelTabChild(); + nbl::ui::CCameraControlPanelUiUtilities::endControlPanelTabChild(); } diff --git a/61_UI/AppControlPanelProjectionTab.cpp b/61_UI/AppControlPanelProjectionTab.cpp index 5f7df8fe3..24271ad93 100644 --- a/61_UI/AppControlPanelProjectionTab.cpp +++ b/61_UI/AppControlPanelProjectionTab.cpp @@ -3,9 +3,9 @@ void App::drawControlPanelProjectionTab(const nbl::ui::SCameraControlPanelStyle& panelStyle) { - if (!nbl::ui::beginControlPanelTabChild("ProjectionPanel", panelStyle)) + if (!nbl::ui::CCameraControlPanelUiUtilities::beginControlPanelTabChild("ProjectionPanel", panelStyle)) { - nbl::ui::endControlPanelTabChild(); + nbl::ui::CCameraControlPanelUiUtilities::endControlPanelTabChild(); return; } @@ -15,13 +15,13 @@ void App::drawControlPanelProjectionTab(const nbl::ui::SCameraControlPanelStyle& { ImGui::TextDisabled("No active viewport."); ImGui::PopItemWidth(); - nbl::ui::endControlPanelTabChild(); + nbl::ui::CCameraControlPanelUiUtilities::endControlPanelTabChild(); return; } - nbl::ui::drawSectionHeader("PlanarSelectHeader", "Planar Selection", panelStyle.AccentColor, panelStyle); + nbl::ui::CCameraControlPanelUiUtilities::drawSectionHeader("PlanarSelectHeader", "Planar Selection", panelStyle.AccentColor, panelStyle); ImGui::Text("Active Render Window: %s", runtime.activeRenderWindowIxString.c_str()); - nbl::ui::drawHoverHint("Window that receives input and camera switching"); + nbl::ui::CCameraControlPanelUiUtilities::drawHoverHint("Window that receives input and camera switching"); auto refreshRuntime = [&]() -> bool { @@ -33,20 +33,20 @@ void App::drawControlPanelProjectionTab(const nbl::ui::SCameraControlPanelStyle& if (!nbl::ui::drawProjectionPlanarSelector(getPlanarProjectionSpan(), runtime, refreshRuntime)) { ImGui::PopItemWidth(); - nbl::ui::endControlPanelTabChild(); + nbl::ui::CCameraControlPanelUiUtilities::endControlPanelTabChild(); return; } - nbl::ui::drawHoverHint("Select which camera the window renders"); + 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::drawSectionHeader("ProjectionParamsHeader", "Projection Parameters", panelStyle.AccentColor, panelStyle); + nbl::ui::CCameraControlPanelUiUtilities::drawSectionHeader("ProjectionParamsHeader", "Projection Parameters", panelStyle.AccentColor, panelStyle); if (!nbl::ui::drawProjectionTypeSelector(getPlanarProjectionSpan(), runtime, refreshRuntime)) { ImGui::PopItemWidth(); - nbl::ui::endControlPanelTabChild(); + nbl::ui::CCameraControlPanelUiUtilities::endControlPanelTabChild(); return; } @@ -54,13 +54,13 @@ void App::drawControlPanelProjectionTab(const nbl::ui::SCameraControlPanelStyle& const bool updateBoundVirtualMaps = nbl::ui::drawProjectionPresetSelector(getPlanarProjectionSpan(), runtime, selectedProjectionType); if (updateBoundVirtualMaps) syncWindowInputBinding(binding); - nbl::ui::drawHoverHint("Switch preset projection for this planar"); + 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::drawSectionHeader("CursorHeader", "Cursor Behaviour", panelStyle.AccentColor, panelStyle); + nbl::ui::CCameraControlPanelUiUtilities::drawSectionHeader("CursorHeader", "Cursor Behaviour", panelStyle.AccentColor, panelStyle); nbl::ui::drawCursorBehaviourControls(m_viewports.captureCursorInMoveMode, m_viewports.resetCursorToCenter); ImGui::TextColored( @@ -69,7 +69,7 @@ void App::drawControlPanelProjectionTab(const nbl::ui::SCameraControlPanelStyle& m_viewports.enableActiveCameraMovement ? "Enabled" : "Disabled"); ImGui::Separator(); - nbl::ui::drawSectionHeader("BoundCameraHeader", "Bound Camera", panelStyle.AccentColor, panelStyle); + nbl::ui::CCameraControlPanelUiUtilities::drawSectionHeader("BoundCameraHeader", "Bound Camera", panelStyle.AccentColor, panelStyle); nbl::ui::drawBoundCameraSection( runtime, binding.activePlanarIx, @@ -81,5 +81,5 @@ void App::drawControlPanelProjectionTab(const nbl::ui::SCameraControlPanelStyle& [this](SWindowControlBinding& windowBinding) { syncWindowInputBindingToProjection(windowBinding); }); ImGui::PopItemWidth(); - nbl::ui::endControlPanelTabChild(); + nbl::ui::CCameraControlPanelUiUtilities::endControlPanelTabChild(); } diff --git a/61_UI/AppControlPanelStatusTab.cpp b/61_UI/AppControlPanelStatusTab.cpp index eab6455ca..690975ae6 100644 --- a/61_UI/AppControlPanelStatusTab.cpp +++ b/61_UI/AppControlPanelStatusTab.cpp @@ -4,15 +4,15 @@ void App::drawControlPanelStatusTab(const nbl::ui::SCameraControlPanelStyle& panelStyle) { - if (!nbl::ui::beginControlPanelTabChild("StatusPanel", panelStyle)) + if (!nbl::ui::CCameraControlPanelUiUtilities::beginControlPanelTabChild("StatusPanel", panelStyle)) { - nbl::ui::endControlPanelTabChild(); + nbl::ui::CCameraControlPanelUiUtilities::endControlPanelTabChild(); return; } ImGui::PushItemWidth(-1.0f); - nbl::ui::drawSectionHeader("SessionHeader", "Session", panelStyle.AccentColor, panelStyle); - if (nbl::ui::beginCard("SessionCard", nbl::ui::calcCameraControlPanelCardHeight(3, panelStyle), panelStyle.CardTopColor, panelStyle.CardBottomColor, panelStyle.CardBorderColor, panelStyle)) + 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)) { @@ -25,20 +25,20 @@ void App::drawControlPanelStatusTab(const nbl::ui::SCameraControlPanelStyle& pan { .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::drawStatusLine(row, panelStyle); + nbl::ui::CCameraControlPanelUiUtilities::drawStatusLine(row, panelStyle); ImGui::EndTable(); } } - nbl::ui::endCard(); + nbl::ui::CCameraControlPanelUiUtilities::endCard(); - nbl::ui::drawSectionHeader("CameraHeader", "Camera", panelStyle.AccentColor, panelStyle); + 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::beginCard("CameraCard", nbl::ui::calcCameraControlPanelCardHeight(5, panelStyle), panelStyle.CardTopColor, panelStyle.CardBottomColor, panelStyle.CardBorderColor, panelStyle)) + 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)) { @@ -56,26 +56,26 @@ void App::drawControlPanelStatusTab(const nbl::ui::SCameraControlPanelStyle& pan { .label = "Rotate scale", .value = rotateScaleText, .dotColor = panelStyle.MutedColor, .valueColor = panelStyle.MutedColor } }}; for (const auto& row : cameraRows) - nbl::ui::drawStatusLine(row, panelStyle); + nbl::ui::CCameraControlPanelUiUtilities::drawStatusLine(row, panelStyle); ImGui::EndTable(); } } - nbl::ui::endCard(); + nbl::ui::CCameraControlPanelUiUtilities::endCard(); } - else if (nbl::ui::beginCard("CameraCard", nbl::ui::calcCameraControlPanelCardHeight(2, panelStyle), panelStyle.CardTopColor, panelStyle.CardBottomColor, panelStyle.CardBorderColor, panelStyle)) + 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::endCard(); + nbl::ui::CCameraControlPanelUiUtilities::endCard(); } - nbl::ui::drawSectionHeader("ProjectionHeader", "Projection", panelStyle.AccentColor, panelStyle); + 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::beginCard("ProjectionCard", nbl::ui::calcCameraControlPanelCardHeight(4, panelStyle), panelStyle.CardTopColor, panelStyle.CardBottomColor, panelStyle.CardBorderColor, panelStyle)) + 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)) { @@ -84,30 +84,30 @@ void App::drawControlPanelStatusTab(const nbl::ui::SCameraControlPanelStyle& pan 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::drawStatusLine({ .label = "Type", .value = typeText, .dotColor = panelStyle.AccentColor, .valueColor = panelStyle.MutedColor }, panelStyle); - nbl::ui::drawStatusLine({ .label = "zNear", .value = zNearText, .dotColor = panelStyle.MutedColor, .valueColor = panelStyle.MutedColor }, panelStyle); - nbl::ui::drawStatusLine({ .label = "zFar", .value = zFarText, .dotColor = panelStyle.MutedColor, .valueColor = panelStyle.MutedColor }, panelStyle); + 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::drawStatusLine({ .label = "Fov", .value = fovText, .dotColor = panelStyle.MutedColor, .valueColor = panelStyle.MutedColor }, panelStyle); + 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::drawStatusLine({ .label = "Ortho width", .value = orthoWidthText, .dotColor = panelStyle.MutedColor, .valueColor = panelStyle.MutedColor }, panelStyle); + nbl::ui::CCameraControlPanelUiUtilities::drawStatusLine({ .label = "Ortho width", .value = orthoWidthText, .dotColor = panelStyle.MutedColor, .valueColor = panelStyle.MutedColor }, panelStyle); } ImGui::EndTable(); } } - nbl::ui::endCard(); + nbl::ui::CCameraControlPanelUiUtilities::endCard(); } - else if (nbl::ui::beginCard("ProjectionCard", nbl::ui::calcCameraControlPanelCardHeight(2, panelStyle), panelStyle.CardTopColor, panelStyle.CardBottomColor, panelStyle.CardBorderColor, panelStyle)) + 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::endCard(); + nbl::ui::CCameraControlPanelUiUtilities::endCard(); } ImGui::PopItemWidth(); - nbl::ui::endControlPanelTabChild(); + nbl::ui::CCameraControlPanelUiUtilities::endControlPanelTabChild(); } diff --git a/61_UI/AppControlPanelTabs.cpp b/61_UI/AppControlPanelTabs.cpp index f81483895..218a43fa4 100644 --- a/61_UI/AppControlPanelTabs.cpp +++ b/61_UI/AppControlPanelTabs.cpp @@ -53,7 +53,7 @@ inline float calcControlPanelToggleRowWidth( { if (toggleIx > 0u) rowWidth += gap; - rowWidth += nbl::ui::calcPillWidth(toggles[toggleIx].label, panelStyle.TogglePadding); + rowWidth += nbl::ui::CCameraControlPanelUiUtilities::calcPillWidth(toggles[toggleIx].label, panelStyle.TogglePadding); } return rowWidth; } @@ -78,7 +78,7 @@ void App::drawControlPanelHeader(const nbl::ui::SCameraControlPanelStyle& panelS { "CI", panelStyle.WarnColor } }}; const size_t headerBadgeCount = m_cliRuntime.ciMode ? headerBadges.size() : headerBadges.size() - 1u; - nbl::ui::drawBadgeRow(std::span(headerBadges.data(), headerBadgeCount), panelStyle.BadgeTextColor, gap, panelStyle); + nbl::ui::CCameraControlPanelUiUtilities::drawBadgeRow(std::span(headerBadges.data(), headerBadgeCount), panelStyle.BadgeTextColor, gap, panelStyle); ImGui::Dummy(ImVec2(0.0f, panelStyle.HeaderGapSmall)); const std::array keyHintGroups = {{ @@ -86,13 +86,13 @@ void App::drawControlPanelHeader(const nbl::ui::SCameraControlPanelStyle& panelS { "Look", nbl::ui::SCameraControlPanelHeaderHints::LookKeys }, { "Zoom", nbl::ui::SCameraControlPanelHeaderHints::ZoomKeys } }}; - nbl::ui::drawKeyHintGroupRow(keyHintGroups, gap, gap * 2.0f, panelStyle.KeyBackgroundColor, panelStyle.KeyTextColor, panelStyle); + 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::calcFramesPerSecond(frameMs, panelStyle); + 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 }, @@ -101,19 +101,19 @@ void App::drawControlPanelHeader(const nbl::ui::SCameraControlPanelStyle& panelS ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); - nbl::ui::drawMiniStat(miniStats[0], m_uiMetrics.frameMs, m_uiMetrics.sampleIndex, [&] + 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::drawMiniStat(miniStats[1], m_uiMetrics.inputCounts, m_uiMetrics.sampleIndex, [&] + 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::drawMiniStat(miniStats[2], m_uiMetrics.virtualCounts, m_uiMetrics.sampleIndex, [&] + nbl::ui::CCameraControlPanelUiUtilities::drawMiniStat(miniStats[2], m_uiMetrics.virtualCounts, m_uiMetrics.sampleIndex, [&] { ImGui::TextColored(panelStyle.AccentColor, "%u ev", m_uiMetrics.lastVirtualEvents); }, panelStyle); @@ -142,21 +142,21 @@ void App::drawControlPanelToggles(const nbl::ui::SCameraControlPanelStyle& panel }; const float rowWidth = calcControlPanelToggleRowWidth(ControlPanelToggles, panelStyle, gap); - nbl::ui::centerControlPanelRow(rowWidth); + 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::drawTogglePill( + nbl::ui::CCameraControlPanelUiUtilities::drawTogglePill( toggle.label, getToggleValue(toggle.binding), panelStyle.AccentColor, panelStyle.InactiveBadgeColor, panelStyle.BadgeTextColor, panelStyle.TogglePadding); - nbl::ui::drawHoverHint(toggle.hint); + nbl::ui::CCameraControlPanelUiUtilities::drawHoverHint(toggle.hint); } } diff --git a/61_UI/AppControlPanelUtilityTabs.cpp b/61_UI/AppControlPanelUtilityTabs.cpp index 89993fc07..f19d7ae91 100644 --- a/61_UI/AppControlPanelUtilityTabs.cpp +++ b/61_UI/AppControlPanelUtilityTabs.cpp @@ -2,28 +2,28 @@ void App::drawControlPanelGizmoTab(const nbl::ui::SCameraControlPanelStyle& panelStyle) { - if (!nbl::ui::beginControlPanelTabChild("GizmoPanel", panelStyle)) + if (!nbl::ui::CCameraControlPanelUiUtilities::beginControlPanelTabChild("GizmoPanel", panelStyle)) { - nbl::ui::endControlPanelTabChild(); + nbl::ui::CCameraControlPanelUiUtilities::endControlPanelTabChild(); return; } - nbl::ui::drawSectionHeader("GizmoHeader", "Gizmo", panelStyle.AccentColor, panelStyle); + nbl::ui::CCameraControlPanelUiUtilities::drawSectionHeader("GizmoHeader", "Gizmo", panelStyle.AccentColor, panelStyle); TransformEditorContents(); - nbl::ui::endControlPanelTabChild(); + nbl::ui::CCameraControlPanelUiUtilities::endControlPanelTabChild(); } void App::drawControlPanelLogTab(const nbl::ui::SCameraControlPanelStyle& panelStyle) { auto& eventLog = m_eventLog; - if (!nbl::ui::beginControlPanelTabChild("LogPanel", panelStyle)) + if (!nbl::ui::CCameraControlPanelUiUtilities::beginControlPanelTabChild("LogPanel", panelStyle)) { - nbl::ui::endControlPanelTabChild(); + nbl::ui::CCameraControlPanelUiUtilities::endControlPanelTabChild(); return; } - nbl::ui::drawSectionHeader("LogHeader", "Virtual Events", panelStyle.AccentColor, panelStyle); + nbl::ui::CCameraControlPanelUiUtilities::drawSectionHeader("LogHeader", "Virtual Events", panelStyle.AccentColor, panelStyle); ImGui::Checkbox("Auto-scroll", &eventLog.autoScroll); ImGui::SameLine(); ImGui::Checkbox("Wrap", &eventLog.wrap); @@ -48,5 +48,5 @@ void App::drawControlPanelLogTab(const nbl::ui::SCameraControlPanelStyle& panelS ImGui::SetScrollHereY(1.0f); } ImGui::EndChild(); - nbl::ui::endControlPanelTabChild(); + nbl::ui::CCameraControlPanelUiUtilities::endControlPanelTabChild(); } diff --git a/61_UI/AppFollowRuntime.cpp b/61_UI/AppFollowRuntime.cpp index 26082917c..2bcbdf3c2 100644 --- a/61_UI/AppFollowRuntime.cpp +++ b/61_UI/AppFollowRuntime.cpp @@ -261,5 +261,5 @@ void App::drawScriptVisualDebugOverlay(const ImVec2& displaySize) m_realFrameIx, m_scriptedInput); - nbl::ui::drawScriptVisualDebugOverlay(displaySize, nbl::ui::buildScriptVisualDebugOverlayData(debugStatus)); + nbl::ui::CCameraScriptVisualDebugOverlayUtilities::drawScriptVisualDebugOverlay(displaySize, nbl::ui::CCameraScriptVisualDebugOverlayUtilities::buildScriptVisualDebugOverlayData(debugStatus)); } diff --git a/61_UI/AppTransformEditor.cpp b/61_UI/AppTransformEditor.cpp index 1610a5650..2fc585c77 100644 --- a/61_UI/AppTransformEditor.cpp +++ b/61_UI/AppTransformEditor.cpp @@ -41,9 +41,9 @@ void App::TransformEditorContents() if (ImGui::IsItemHovered()) { - nbl::ui::beginHoverInfoOverlay("HoverOverlay", ImGui::GetMousePos()); + nbl::ui::CCameraViewportOverlayUtilities::beginHoverInfoOverlay("HoverOverlay", ImGui::GetMousePos()); ImGui::Text("Right-click and drag on the gizmo to manipulate the object."); - nbl::ui::endHoverInfoOverlay(); + nbl::ui::CCameraViewportOverlayUtilities::endHoverInfoOverlay(); } ImGui::Separator(); diff --git a/61_UI/AppViewportGizmos.cpp b/61_UI/AppViewportGizmos.cpp index 5eef8fa23..96659044e 100644 --- a/61_UI/AppViewportGizmos.cpp +++ b/61_UI/AppViewportGizmos.cpp @@ -81,7 +81,7 @@ void App::drawViewportManipulationGizmos( void App::drawManipulableObjectHoverOverlay(const SManipulableObjectContext& objectContext) const { const ImVec2 mousePos = ImGui::GetIO().MousePos; - nbl::ui::beginHoverInfoOverlay("InfoOverlay", mousePos); + nbl::ui::CCameraViewportOverlayUtilities::beginHoverInfoOverlay("InfoOverlay", mousePos); ImGui::Text("Identifier: %s", objectContext.label.c_str()); ImGui::Text("Object Ix: %u", objectContext.objectIx); @@ -100,5 +100,5 @@ void App::drawManipulableObjectHoverOverlay(const SManipulableObjectContext& obj ImGui::TextDisabled("Enabled follow cameras update on the next frame"); } - nbl::ui::endHoverInfoOverlay(); + nbl::ui::CCameraViewportOverlayUtilities::endHoverInfoOverlay(); } diff --git a/61_UI/AppViewportWindows.cpp b/61_UI/AppViewportWindows.cpp index e5016a016..0da195974 100644 --- a/61_UI/AppViewportWindows.cpp +++ b/61_UI/AppViewportWindows.cpp @@ -25,7 +25,7 @@ void App::drawWindowedViewportWindow(uint32_t windowIx, ImGuiCond windowCond, bo ImGui::SetNextWindowSize({ rw.iSize.x, rw.iSize.y }, windowCond); ImGui::SetNextWindowSizeConstraints(SCameraAppViewportDefaults::MinWindowSize, SCameraAppViewportDefaults::MaxWindowSize); - nbl::ui::pushViewportWindowStyle(); + 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); @@ -41,7 +41,7 @@ void App::drawWindowedViewportWindow(uint32_t windowIx, ImGuiCond windowCond, bo if (!viewportValid) { ImGui::End(); - nbl::ui::popViewportWindowStyle(); + nbl::ui::CCameraViewportOverlayUtilities::popViewportWindowStyle(); return; } const auto& viewportState = viewportRuntime.viewportState; @@ -68,7 +68,7 @@ void App::drawWindowedViewportWindow(uint32_t windowIx, ImGuiCond windowCond, bo drawViewportManipulationGizmos(windowIx, binding, viewportState, gizmoIx); ImGui::End(); - nbl::ui::popViewportWindowStyle(); + nbl::ui::CCameraViewportOverlayUtilities::popViewportWindowStyle(); } void App::drawViewportWindowOverlay( @@ -83,7 +83,7 @@ void App::drawViewportWindowOverlay( 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)); overlayData.detail = "Frustum: active camera (hidden in owner view)"; - nbl::ui::drawViewportInfoOverlay(drawList, viewportRect, overlayData); + nbl::ui::CCameraViewportOverlayUtilities::drawViewportInfoOverlay(drawList, viewportRect, overlayData); } void App::updateActiveRenderWindowFromViewport(uint32_t windowIx, bool windowHovered, bool windowFocused) @@ -106,7 +106,7 @@ void App::drawViewportSplitOverlayWindow(const ImVec2& displaySize) 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::drawViewportSplitOverlay(*drawList, displaySize, splitY, gap); + nbl::ui::CCameraViewportOverlayUtilities::drawViewportSplitOverlay(*drawList, displaySize, splitY, gap); ImGui::End(); } diff --git a/61_UI/include/app/AppControlPanelAuthoringUtilities.hpp b/61_UI/include/app/AppControlPanelAuthoringUtilities.hpp index 625702013..63eb2d344 100644 --- a/61_UI/include/app/AppControlPanelAuthoringUtilities.hpp +++ b/61_UI/include/app/AppControlPanelAuthoringUtilities.hpp @@ -27,28 +27,28 @@ inline void drawGoalApplyPresentationBadges(const SCameraGoalApplyPresentation& { if (presentation.badges.exact) { - drawBadge("EXACT", panelStyle.GoodColor, panelStyle.BadgeTextColor, panelStyle); + CCameraControlPanelUiUtilities::drawBadge("EXACT", panelStyle.GoodColor, panelStyle.BadgeTextColor, panelStyle); } else if (presentation.badges.bestEffort) { - drawBadge("BEST-EFFORT", panelStyle.WarnColor, panelStyle.BadgeTextColor, panelStyle); + CCameraControlPanelUiUtilities::drawBadge("BEST-EFFORT", panelStyle.WarnColor, panelStyle.BadgeTextColor, panelStyle); } if (presentation.badges.dropsState) { ImGui::SameLine(); - drawBadge("DROPS STATE", panelStyle.WarnColor, panelStyle.BadgeTextColor, panelStyle); + CCameraControlPanelUiUtilities::drawBadge("DROPS STATE", panelStyle.WarnColor, panelStyle.BadgeTextColor, panelStyle); } else if (presentation.badges.sharedStateOnly) { ImGui::SameLine(); - drawBadge("SHARED STATE", panelStyle.AccentColor, panelStyle.BadgeTextColor, panelStyle); + CCameraControlPanelUiUtilities::drawBadge("SHARED STATE", panelStyle.AccentColor, panelStyle.BadgeTextColor, panelStyle); } if (presentation.badges.blocked) { ImGui::SameLine(); - drawBadge("BLOCKED", panelStyle.BadColor, panelStyle.BadgeTextColor, panelStyle); + CCameraControlPanelUiUtilities::drawBadge("BLOCKED", panelStyle.BadColor, panelStyle.BadgeTextColor, panelStyle); } } diff --git a/61_UI/include/app/AppProjectionControlPanelUiUtilities.hpp b/61_UI/include/app/AppProjectionControlPanelUiUtilities.hpp index 8302d1a35..5ee31aff4 100644 --- a/61_UI/include/app/AppProjectionControlPanelUiUtilities.hpp +++ b/61_UI/include/app/AppProjectionControlPanelUiUtilities.hpp @@ -118,7 +118,7 @@ inline bool drawProjectionTypeSelector( return false; } - drawHoverHint("Switch projection type for this planar"); + CCameraControlPanelUiUtilities::drawHoverHint("Switch projection type for this planar"); return true; } @@ -129,7 +129,7 @@ inline void drawProjectionHandednessControls(SWindowControlBinding& binding) ImGui::SameLine(); if (ImGui::RadioButton("RH", !binding.leftHandedProjection)) binding.leftHandedProjection = false; - drawHoverHint("Toggle left or right handed projection"); + CCameraControlPanelUiUtilities::drawHoverHint("Toggle left or right handed projection"); } inline void drawProjectionParameterControls( @@ -139,9 +139,9 @@ inline void drawProjectionParameterControls( { auto updateParameters = boundProjection.getParameters(); if (useWindow) - drawCheckboxWithHint({ .label = "Allow axes to flip##allowAxesToFlip", .value = &binding.allowGizmoAxesToFlip, .hint = "Allow ImGuizmo axes to flip based on view" }); + CCameraControlPanelUiUtilities::drawCheckboxWithHint({ .label = "Allow axes to flip##allowAxesToFlip", .value = &binding.allowGizmoAxesToFlip, .hint = "Allow ImGuizmo axes to flip based on view" }); if (useWindow) - drawCheckboxWithHint({ .label = "Draw debug grid##drawDebugGrid", .value = &binding.enableDebugGridDraw, .hint = "Toggle debug grid in the render window" }); + CCameraControlPanelUiUtilities::drawCheckboxWithHint({ .label = "Draw debug grid##drawDebugGrid", .value = &binding.enableDebugGridDraw, .hint = "Toggle debug grid in the render window" }); drawProjectionHandednessControls(binding); @@ -158,13 +158,13 @@ inline void drawProjectionParameterControls( 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" } }) { - drawSliderFloatWithHint(spec); + CCameraControlPanelUiUtilities::drawSliderFloatWithHint(spec); } switch (boundProjection.getParameters().m_type) { case IPlanarProjection::CProjection::Perspective: - drawSliderFloatWithHint({ + CCameraControlPanelUiUtilities::drawSliderFloatWithHint({ .label = "Fov", .value = &updateParameters.m_planar.perspective.fov, .minValue = SCameraAppProjectionUiDefaults::PerspectiveFovMinDeg, @@ -176,7 +176,7 @@ inline void drawProjectionParameterControls( boundProjection.setPerspective(updateParameters.m_zNear, updateParameters.m_zFar, updateParameters.m_planar.perspective.fov); break; case IPlanarProjection::CProjection::Orthographic: - drawSliderFloatWithHint({ + CCameraControlPanelUiUtilities::drawSliderFloatWithHint({ .label = "Ortho width", .value = &updateParameters.m_planar.orthographic.orthoWidth, .minValue = SCameraAppProjectionUiDefaults::OrthoWidthMin, @@ -197,7 +197,7 @@ inline void drawCursorBehaviourControls(bool& captureCursorInMoveMode, bool& res if (!ImGui::TreeNodeEx("Cursor Behaviour")) return; - drawCheckboxWithHint({ .label = "Capture OS cursor in move mode", .value = &captureCursorInMoveMode, .hint = "When disabled the app never warps or clamps system cursor" }); + 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)) @@ -224,7 +224,7 @@ inline void drawBoundCameraMotionControls(ICamera& camera) SCameraAppControlPanelRangeDefaults::MotionScaleMax, "%.4f", ImGuiSliderFlags_Logarithmic); - drawHoverHint("Scale translation speed for this camera"); + CCameraControlPanelUiUtilities::drawHoverHint("Scale translation speed for this camera"); if (camera.getAllowedVirtualEvents() & CVirtualGimbalEvent::Rotate) { ImGui::SliderFloat( @@ -235,7 +235,7 @@ inline void drawBoundCameraMotionControls(ICamera& camera) "%.4f", ImGuiSliderFlags_Logarithmic); } - drawHoverHint("Scale rotation speed for this camera"); + CCameraControlPanelUiUtilities::drawHoverHint("Scale rotation speed for this camera"); camera.setMotionScales(moveSpeed, rotationSpeed); } @@ -270,7 +270,7 @@ inline void drawBoundCameraSection( sphericalState.maxDistance, "%.4f", ImGuiSliderFlags_Logarithmic); - drawHoverHint("Current orbit distance"); + CCameraControlPanelUiUtilities::drawHoverHint("Current orbit distance"); camera.trySetSphericalDistance(distance); } diff --git a/61_UI/include/app/AppViewportWindowUtilities.hpp b/61_UI/include/app/AppViewportWindowUtilities.hpp index 5ad9b9888..f1262fe5e 100644 --- a/61_UI/include/app/AppViewportWindowUtilities.hpp +++ b/61_UI/include/app/AppViewportWindowUtilities.hpp @@ -69,7 +69,7 @@ inline void drawFollowTargetOverlayIfActive( if (!(scriptedInput.enabled && scriptedInput.visualDebug && scriptedInput.visualFollow.active)) return; - drawFollowTargetViewportOverlay( + CCameraViewportOverlayUtilities::drawFollowTargetViewportOverlay( *drawList, { .viewMatrix = viewportState.viewMatrix, diff --git a/61_UI/include/common.hpp b/61_UI/include/common.hpp index 250b7f27d..a48a3ba70 100644 --- a/61_UI/include/common.hpp +++ b/61_UI/include/common.hpp @@ -190,7 +190,10 @@ 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::hlsl::getCastedMatrix; using nbl::hlsl::getCastedVector; using nbl::hlsl::getMatrix3x4As4x4; diff --git a/common/include/camera/CCameraControlPanelUiUtilities.hpp b/common/include/camera/CCameraControlPanelUiUtilities.hpp index 7fc79b7f6..385601d3d 100644 --- a/common/include/camera/CCameraControlPanelUiUtilities.hpp +++ b/common/include/camera/CCameraControlPanelUiUtilities.hpp @@ -188,390 +188,391 @@ struct SCameraControlPanelToggleLabels final static inline constexpr std::array Labels = { "WINDOW", "STATUS", "EVENT LOG" }; }; -template -inline void drawSummaryRow(const char* label, DrawValueFn&& drawValue) +struct CCameraControlPanelUiUtilities final { - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::TextUnformatted(label); - ImGui::TableSetColumnIndex(1); - drawValue(); -} + template + static inline void drawSummaryRow(const char* label, DrawValueFn&& drawValue) + { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::TextUnformatted(label); + ImGui::TableSetColumnIndex(1); + drawValue(); + } -inline void drawDot(const ImVec4& color, const SCameraControlPanelStyle& style); + 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); + } -inline void drawStatusLine(const SCameraControlPanelStatusLineSpec& spec, const SCameraControlPanelStyle& style = {}) -{ - drawSummaryRow(spec.label, [&]() + static inline void drawStatusLine(const SCameraControlPanelStatusLineSpec& spec, const SCameraControlPanelStyle& style = {}) { - drawDot(spec.dotColor, style); - ImGui::TextColored(spec.valueColor, "%.*s", static_cast(spec.value.size()), spec.value.data()); - }); -} + drawSummaryRow(spec.label, [&]() + { + drawDot(spec.dotColor, style); + ImGui::TextColored(spec.valueColor, "%.*s", static_cast(spec.value.size()), spec.value.data()); + }); + } -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 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()); + } -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 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)); + } -inline float calcFramesPerSecond(const float frameMs, const SCameraControlPanelStyle& style = {}) -{ - return frameMs > 0.0f ? (style.MillisecondsPerSecond / frameMs) : 0.0f; -} + static inline float calcFramesPerSecond(const float frameMs, const SCameraControlPanelStyle& style = {}) + { + return frameMs > 0.0f ? (style.MillisecondsPerSecond / frameMs) : 0.0f; + } -inline float calcPillWidth(const char* label, const ImVec2& padding) -{ - return ImGui::CalcTextSize(label).x + padding.x * 2.0f; -} + static inline float calcPillWidth(const char* label, const ImVec2& padding) + { + return ImGui::CalcTextSize(label).x + padding.x * 2.0f; + } -inline void centerControlPanelRow(const float contentWidth) -{ - ImGui::SetCursorPosX(ImGui::GetCursorPosX() + std::max(0.0f, (ImGui::GetContentRegionAvail().x - contentWidth) * 0.5f)); -} + static inline void centerControlPanelRow(const float contentWidth) + { + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + std::max(0.0f, (ImGui::GetContentRegionAvail().x - contentWidth) * 0.5f)); + } -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); -} - -inline void popControlPanelWindowStyle() -{ - ImGui::PopStyleColor(19); - ImGui::PopStyleVar(9); -} - -inline bool inputTextString( - const char* label, - std::string& value, - ImGuiInputTextFlags flags = 0) -{ - return ImGui::InputText(label, &value, flags); -} + 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); + } -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); -} - -inline float calcCameraControlPanelCardHeight(const int rows, const SCameraControlPanelStyle& style = {}) -{ - return ImGui::GetFrameHeightWithSpacing() * (static_cast(rows) + style.CardExtraRows) + style.CardHeightPadding; -} + static inline void popControlPanelWindowStyle() + { + ImGui::PopStyleColor(19); + ImGui::PopStyleVar(9); + } -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); -} - -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) + static inline bool inputTextString( + const char* label, + std::string& value, + ImGuiInputTextFlags flags = 0) { - if (i > 0u) - width += gap; - width += calcPillWidth(badges[i].label, badgePadding); + return ImGui::InputText(label, &value, flags); } - return width; -} - -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) + static inline void drawControlPanelWindowBackdrop(ImDrawList& drawList, const ImVec2& panelPos, const ImVec2& panelSize, const SCameraControlPanelStyle& style = {}) { - if (i > 0u) - ImGui::SameLine(0.0f, gap); - drawBadge(badges[i].label, badges[i].background, textColor, 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); } -} -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); -} - -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; -} - -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) + static inline float calcCameraControlPanelCardHeight(const int rows, const SCameraControlPanelStyle& style = {}) { - ImGui::SameLine(0.0f, gap); - drawKeyHint(key, keyBackground, keyText, style); + return ImGui::GetFrameHeightWithSpacing() * (static_cast(rows) + style.CardExtraRows) + style.CardHeightPadding; } -} - -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) + + static inline void drawBadge(const char* label, const ImVec4& bg, const ImVec4& fg, const SCameraControlPanelStyle& style = {}) { - if (i > 0u) - rowWidth += groupGap; - rowWidth += calcKeyHintGroupWidth(groups[i], gap, style.KeyHintPadding); + 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); } - centerControlPanelRow(rowWidth); - for (size_t i = 0; i < groups.size(); ++i) + static inline float calcBadgeRowWidth( + const std::span badges, + const float gap, + const ImVec2& badgePadding) { - if (i > 0u) - ImGui::SameLine(0.0f, groupGap); - drawKeyHintGroup(groups[i], gap, keyBackground, keyText, style); + 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; } -} - -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 -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)) + + static inline void drawBadgeRow( + const std::span badges, + const ImVec4& textColor, + const float gap, + const SCameraControlPanelStyle& style = {}) { - 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)); + 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(); } - ImGui::EndChild(); - ImGui::PopStyleVar(); - ImGui::PopStyleColor(); -} -inline void drawHoverHint(const char* text) -{ - if (!ImGui::IsItemHovered(ImGuiHoveredFlags_DelayShort)) - return; - ImGui::BeginTooltip(); - ImGui::TextUnformatted(text); - ImGui::EndTooltip(); -} - -inline bool drawCheckboxWithHint(const SCameraControlPanelCheckboxSpec& spec) -{ - if (!spec.value) - return false; + static inline void drawHoverHint(const char* text) + { + if (!ImGui::IsItemHovered(ImGuiHoveredFlags_DelayShort)) + return; + ImGui::BeginTooltip(); + ImGui::TextUnformatted(text); + ImGui::EndTooltip(); + } - const bool changed = ImGui::Checkbox(spec.label, spec.value); - if (spec.hint && spec.hint[0] != '\0') - drawHoverHint(spec.hint); - return changed; -} + static inline bool drawCheckboxWithHint(const SCameraControlPanelCheckboxSpec& spec) + { + if (!spec.value) + return false; -inline bool drawSliderFloatWithHint(const SCameraControlPanelSliderSpec& 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; + } - 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 drawSliderFloatWithHint(const SCameraControlPanelSliderSpec& spec) + { + if (!spec.value) + return false; -inline bool drawActionButtonWithHint(const char* label, const char* hint) -{ - const bool pressed = ImGui::Button(label); - if (hint && hint[0] != '\0') - drawHoverHint(hint); - return pressed; -} + 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; + } -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 bool drawActionButtonWithHint(const char* label, const char* hint) + { + const bool pressed = ImGui::Button(label); + if (hint && hint[0] != '\0') + drawHoverHint(hint); + return pressed; + } -inline void endControlPanelTabChild() -{ - ImGui::EndChild(); - ImGui::PopStyleVar(); -} + 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); + } -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); -} - -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)) + 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()->AddRectFilled( + ImGui::GetWindowDrawList()->AddRectFilledMultiColor( 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); + 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; } - ImGui::EndChild(); - ImGui::PopStyleColor(); - ImGui::PopStyleVar(); - ImGui::Spacing(); -} -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; -} - -inline void endCard() -{ - ImGui::EndChild(); - ImGui::PopStyleColor(); - ImGui::PopStyleVar(2); -} + static inline void endCard() + { + ImGui::EndChild(); + ImGui::PopStyleColor(); + ImGui::PopStyleVar(2); + } +}; } // namespace nbl::ui diff --git a/common/include/camera/CCameraScriptVisualDebugOverlayUtilities.hpp b/common/include/camera/CCameraScriptVisualDebugOverlayUtilities.hpp index c89326f22..57bd3993d 100644 --- a/common/include/camera/CCameraScriptVisualDebugOverlayUtilities.hpp +++ b/common/include/camera/CCameraScriptVisualDebugOverlayUtilities.hpp @@ -95,112 +95,113 @@ struct SCameraScriptVisualDebugOverlayStyle final ImU32 hintColor = HintColor; }; -//! Draw the scripted visual debug HUD on the foreground draw list. -inline void drawScriptVisualDebugOverlay( - const ImVec2& displaySize, - const SCameraScriptVisualDebugOverlayData& data, - const SCameraScriptVisualDebugOverlayStyle& style = {}) +struct CCameraScriptVisualDebugOverlayUtilities final { - 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()); -} - -inline std::string formatFixedScalar(const float value, const int precision) -{ - std::ostringstream oss; - oss << std::fixed << std::setprecision(precision) << value; - return oss.str(); -} - -inline std::string buildScriptVisualDebugProgressLine(const SCameraScriptVisualDebugStatus& status) -{ - if (status.hasHoldFrames) + static inline void drawScriptVisualDebugOverlay( + const ImVec2& displaySize, + const SCameraScriptVisualDebugOverlayData& data, + const SCameraScriptVisualDebugOverlayStyle& style = {}) { - 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); + 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 << "Planar " << status.planarIndex - << " Segment " << std::fixed << std::setprecision(1) - << elapsedSeconds << "/" << holdSeconds - << " s Frame " << status.progressFrames << "/" << status.holdFrames; + oss << std::fixed << std::setprecision(precision) << value; return oss.str(); } - std::ostringstream oss; - oss << "Planar " << status.planarIndex << " Frame " << status.absoluteFrame; - 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(); + } -//! Build the display strings for one scripted visual debug HUD snapshot. -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) + std::ostringstream oss; + oss << "Planar " << status.planarIndex << " Frame " << status.absoluteFrame; + return oss.str(); + } + + static inline SCameraScriptVisualDebugOverlayData buildScriptVisualDebugOverlayData(const SCameraScriptVisualDebugStatus& status) { - out.hintLine += " | " + std::string(status.followModeDescription); - if (status.followLockValid) + 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 += - " | lock " + formatFixedScalar(status.followLockAngleDeg, 2) + - " deg | target " + formatFixedScalar(status.followTargetDistance, 2) + - " | center err " + formatFixedScalar(status.followTargetCenterNdcRadius, 3); + 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 += " | lock n/a | target n/a | center err n/a"; + out.hintLine += " | Follow off"; } - } - else - { - out.hintLine += " | Follow off"; - } - return out; -} + return out; + } +}; } // namespace nbl::ui diff --git a/common/include/camera/CCameraScriptedRuntimePersistence.hpp b/common/include/camera/CCameraScriptedRuntimePersistence.hpp index 71f636c22..077fb61d0 100644 --- a/common/include/camera/CCameraScriptedRuntimePersistence.hpp +++ b/common/include/camera/CCameraScriptedRuntimePersistence.hpp @@ -54,10 +54,13 @@ struct CCameraScriptedInputParseResult std::vector warnings; }; -inline void appendScriptedInputParseWarning(CCameraScriptedInputParseResult& out, std::string warning) +struct CCameraScriptedRuntimePersistenceUtilities final { - out.warnings.emplace_back(std::move(warning)); -} + static inline void appendScriptedInputParseWarning(CCameraScriptedInputParseResult& out, std::string warning) + { + out.warnings.emplace_back(std::move(warning)); + } +}; //! Parse one low-level scripted runtime payload from an existing stream. bool readCameraScriptedInput(std::istream& in, CCameraScriptedInputParseResult& out, std::string* error = nullptr); diff --git a/common/include/camera/CCameraViewportOverlayUtilities.hpp b/common/include/camera/CCameraViewportOverlayUtilities.hpp index 2284c3fae..c602e0bba 100644 --- a/common/include/camera/CCameraViewportOverlayUtilities.hpp +++ b/common/include/camera/CCameraViewportOverlayUtilities.hpp @@ -163,146 +163,148 @@ struct SCameraViewportSplitOverlayStyle final ImU32 dividerLineColor = DividerLineColor; }; -inline void pushViewportWindowStyle(const SCameraViewportWindowStyle& style = {}) +struct CCameraViewportOverlayUtilities final { - 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 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); + } -inline void popViewportWindowStyle() -{ - ImGui::PopStyleColor(); - ImGui::PopStyleVar(3); -} - -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 ImVec2 detailSize = ImGui::CalcTextSize(data.detail.c_str()); - const float width = std::max(std::max(headlineSize.x, descriptionSize.x), detailSize.x); - const float height = headlineSize.y + descriptionSize.y + detailSize.y + style.lineGap * 2.0f + style.padding.y * 2.0f; - 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()); - 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()); -} - -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); -} - -inline void endHoverInfoOverlay() -{ - ImGui::End(); - ImGui::PopStyleVar(); - ImGui::PopStyleColor(2); -} - -inline void drawViewportSplitOverlay( - ImDrawList& drawList, - const ImVec2& displaySize, - const float splitY, - const float gap, - const SCameraViewportSplitOverlayStyle& style = {}) -{ - if (gap >= style.minimumGapFill) + static inline void popViewportWindowStyle() + { + ImGui::PopStyleColor(); + ImGui::PopStyleVar(3); + } + + static inline void drawViewportInfoOverlay( + ImDrawList& drawList, + const SViewportOverlayRect& viewportRect, + const SCameraViewportInfoOverlayData& data, + const SCameraViewportInfoOverlayStyle& style = {}) { - drawList.AddRectFilled( + 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 ImVec2 detailSize = ImGui::CalcTextSize(data.detail.c_str()); + const float width = std::max(std::max(headlineSize.x, descriptionSize.x), detailSize.x); + const float height = headlineSize.y + descriptionSize.y + detailSize.y + style.lineGap * 2.0f + style.padding.y * 2.0f; + 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()); + 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 + gap), - style.gapFillColor); - return; + ImVec2(displaySize.x, splitY), + style.dividerLineColor, + style.dividerLineThickness); } - drawList.AddLine( - ImVec2(0.0f, splitY), - ImVec2(displaySize.x, splitY), - style.dividerLineColor, - style.dividerLineThickness); -} - -//! Draw one follow-target overlay in the active viewport using projected tracked-target metrics. -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"); -} + 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 diff --git a/common/src/nbl/examples/camera/CCameraScriptedRuntimePersistence.cpp b/common/src/nbl/examples/camera/CCameraScriptedRuntimePersistence.cpp index 0df587d67..06835b45b 100644 --- a/common/src/nbl/examples/camera/CCameraScriptedRuntimePersistence.cpp +++ b/common/src/nbl/examples/camera/CCameraScriptedRuntimePersistence.cpp @@ -667,7 +667,7 @@ void parseScriptedKeyboardEventJson(const json_t& event, const uint64_t frame, c { if (!event.contains("key") || !event.contains("action")) { - nbl::system::appendScriptedInputParseWarning(out, "Scripted keyboard event missing \"key\" or \"action\"."); + nbl::system::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Scripted keyboard event missing \"key\" or \"action\"."); return; } @@ -676,14 +676,14 @@ void parseScriptedKeyboardEventJson(const json_t& event, const uint64_t frame, c const auto key = parseScriptedKeyCode(keyText); if (key == nbl::ui::EKC_NONE) { - nbl::system::appendScriptedInputParseWarning(out, "Scripted keyboard event has invalid key \"" + keyText + "\"."); + nbl::system::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Scripted keyboard event has invalid key \"" + keyText + "\"."); return; } const auto action = parseScriptedKeyboardAction(actionText); if (!action.has_value()) { - nbl::system::appendScriptedInputParseWarning(out, "Scripted keyboard event has invalid action \"" + actionText + "\"."); + nbl::system::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Scripted keyboard event has invalid action \"" + actionText + "\"."); return; } @@ -700,7 +700,7 @@ void parseScriptedMouseEventJson(const json_t& event, const uint64_t frame, cons { if (!event.contains("kind")) { - nbl::system::appendScriptedInputParseWarning(out, "Scripted mouse event missing \"kind\"."); + nbl::system::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Scripted mouse event missing \"kind\"."); return; } @@ -725,7 +725,7 @@ void parseScriptedMouseEventJson(const json_t& event, const uint64_t frame, cons { if (!event.contains("button") || !event.contains("action")) { - nbl::system::appendScriptedInputParseWarning(out, "Scripted click event missing \"button\" or \"action\"."); + nbl::system::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Scripted click event missing \"button\" or \"action\"."); return; } @@ -734,14 +734,14 @@ void parseScriptedMouseEventJson(const json_t& event, const uint64_t frame, cons const auto button = parseScriptedMouseButton(buttonText); if (!button.has_value()) { - nbl::system::appendScriptedInputParseWarning(out, "Scripted click event has invalid button \"" + buttonText + "\"."); + nbl::system::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Scripted click event has invalid button \"" + buttonText + "\"."); return; } const auto action = parseScriptedMouseClickAction(actionText); if (!action.has_value()) { - nbl::system::appendScriptedInputParseWarning(out, "Scripted click event has invalid action \"" + actionText + "\"."); + nbl::system::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Scripted click event has invalid action \"" + actionText + "\"."); return; } @@ -753,7 +753,7 @@ void parseScriptedMouseEventJson(const json_t& event, const uint64_t frame, cons } else { - nbl::system::appendScriptedInputParseWarning(out, "Scripted mouse event has invalid kind \"" + kind + "\"."); + nbl::system::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Scripted mouse event has invalid kind \"" + kind + "\"."); return; } @@ -804,7 +804,7 @@ bool parseScriptedProjectionActionValue(const json_t& event, nbl::system::CCamer action.value = static_cast(nbl::core::IPlanarProjection::CProjection::Orthographic); else { - nbl::system::appendScriptedInputParseWarning(out, "Scripted action projection type has invalid value \"" + valueText + "\"."); + nbl::system::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Scripted action projection type has invalid value \"" + valueText + "\"."); return false; } } @@ -820,7 +820,7 @@ void parseScriptedActionEventJson(const json_t& event, const uint64_t frame, con { if (!event.contains("action")) { - nbl::system::appendScriptedInputParseWarning(out, "Scripted action event missing \"action\"."); + nbl::system::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Scripted action event missing \"action\"."); return; } @@ -867,7 +867,7 @@ void parseScriptedActionEventJson(const json_t& event, const uint64_t frame, con } else { - nbl::system::appendScriptedInputParseWarning(out, "Scripted action event has invalid action \"" + actionText + "\"."); + nbl::system::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Scripted action event has invalid action \"" + actionText + "\"."); return; } @@ -879,7 +879,7 @@ void parseScriptedInputEventJson(const json_t& event, nbl::system::CCameraScript { if (!event.contains("frame") || !event.contains("type")) { - nbl::system::appendScriptedInputParseWarning(out, "Scripted input event missing \"frame\" or \"type\"."); + nbl::system::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Scripted input event missing \"frame\" or \"type\"."); return; } @@ -896,7 +896,7 @@ void parseScriptedInputEventJson(const json_t& event, nbl::system::CCameraScript else if (type == "action") parseScriptedActionEventJson(event, frame, captureFrame, out); else - nbl::system::appendScriptedInputParseWarning(out, "Scripted input event has invalid type \"" + type + "\"."); + nbl::system::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Scripted input event has invalid type \"" + type + "\"."); } void parseScriptedInputEventsJson(const json_t& script, nbl::system::CCameraScriptedInputParseResult& out) @@ -915,7 +915,7 @@ bool parseScriptedImguizmoVirtualCheckJson(const json_t& check, nbl::system::CCa if (!check.contains("events")) { - nbl::system::appendScriptedInputParseWarning(out, "Imguizmo virtual check missing \"events\"."); + nbl::system::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Imguizmo virtual check missing \"events\"."); return false; } @@ -923,7 +923,7 @@ bool parseScriptedImguizmoVirtualCheckJson(const json_t& check, nbl::system::CCa { if (!expectedEvent.contains("type") || !expectedEvent.contains("magnitude")) { - nbl::system::appendScriptedInputParseWarning(out, "Imguizmo virtual check event missing \"type\" or \"magnitude\"."); + nbl::system::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Imguizmo virtual check event missing \"type\" or \"magnitude\"."); continue; } @@ -931,7 +931,7 @@ bool parseScriptedImguizmoVirtualCheckJson(const json_t& check, nbl::system::CCa const auto type = nbl::core::CVirtualGimbalEvent::stringToVirtualEvent(typeText); if (type == nbl::core::CVirtualGimbalEvent::None) { - nbl::system::appendScriptedInputParseWarning(out, "Imguizmo virtual check event has invalid type \"" + typeText + "\"."); + nbl::system::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Imguizmo virtual check event has invalid type \"" + typeText + "\"."); continue; } @@ -948,7 +948,7 @@ bool parseScriptedCheckJson(const json_t& check, nbl::system::CCameraScriptedInp { if (!check.contains("frame") || !check.contains("kind")) { - nbl::system::appendScriptedInputParseWarning(out, "Scripted check missing \"frame\" or \"kind\"."); + nbl::system::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Scripted check missing \"frame\" or \"kind\"."); return false; } @@ -1028,13 +1028,13 @@ bool parseScriptedCheckJson(const json_t& check, nbl::system::CCameraScriptedInp if (!entry.hasPosDeltaConstraint && !entry.hasEulerDeltaConstraint) { - nbl::system::appendScriptedInputParseWarning(out, "gimbal_step check requires at least one delta constraint."); + nbl::system::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "gimbal_step check requires at least one delta constraint."); return false; } } else { - nbl::system::appendScriptedInputParseWarning(out, "Scripted check has invalid kind \"" + kind + "\"."); + nbl::system::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Scripted check has invalid kind \"" + kind + "\"."); return false; } From 55b4607cd9b44e9a0aeddf2f7b80c23c091e66b4 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Thu, 9 Apr 2026 12:40:03 +0200 Subject: [PATCH 188/205] Refactor camera orbit vector math --- .../include/camera/CCameraMathUtilities.hpp | 51 +++++++++---------- .../include/camera/CCameraPathUtilities.hpp | 3 +- .../camera/CCameraTargetRelativeUtilities.hpp | 6 +-- .../include/camera/CSphericalTargetCamera.hpp | 2 - 4 files changed, 26 insertions(+), 36 deletions(-) diff --git a/common/include/camera/CCameraMathUtilities.hpp b/common/include/camera/CCameraMathUtilities.hpp index 19ef7b785..5b60351db 100644 --- a/common/include/camera/CCameraMathUtilities.hpp +++ b/common/include/camera/CCameraMathUtilities.hpp @@ -416,12 +416,12 @@ struct CCameraMathUtilities final } template - static inline camera_vector_t makeSphericalOffsetFromOrbit(const T orbitU, const T orbitV, const T distance) + static inline camera_vector_t makeSphericalOffsetFromOrbit(const camera_vector_t& orbitUv, const T distance) { return camera_vector_t( - hlsl::cos(orbitV) * hlsl::cos(orbitU) * distance, - hlsl::cos(orbitV) * hlsl::sin(orbitU) * distance, - hlsl::sin(orbitV) * distance); + hlsl::cos(orbitUv.y) * hlsl::cos(orbitUv.x) * distance, + hlsl::cos(orbitUv.y) * hlsl::sin(orbitUv.x) * distance, + hlsl::sin(orbitUv.y) * distance); } template @@ -545,8 +545,7 @@ struct CCameraMathUtilities final template static inline bool tryBuildSphericalPoseFromOrbit( const camera_vector_t& targetPosition, - const T orbitU, - const T orbitV, + const camera_vector_t& orbitUv, const T distance, const T minDistance, const T maxDistance, @@ -554,18 +553,18 @@ struct CCameraMathUtilities final camera_quaternion_t& outOrientation, T* outAppliedDistance = nullptr) { - if (!isFiniteScalar(orbitU) || - !isFiniteScalar(orbitV) || + if (!isFiniteScalar(orbitUv.x) || + !isFiniteScalar(orbitUv.y) || !isFiniteScalar(distance)) return false; const T appliedDistance = std::clamp(distance, minDistance, maxDistance); - const auto spherePosition = makeSphericalOffsetFromOrbit(orbitU, orbitV, appliedDistance); + const auto spherePosition = makeSphericalOffsetFromOrbit(orbitUv, appliedDistance); const auto upHint = safeNormalizeVec3( camera_vector_t( - -hlsl::sin(orbitV) * hlsl::cos(orbitU), - -hlsl::sin(orbitV) * hlsl::sin(orbitU), - hlsl::cos(orbitV)), + -hlsl::sin(orbitUv.y) * hlsl::cos(orbitUv.x), + -hlsl::sin(orbitUv.y) * hlsl::sin(orbitUv.x), + hlsl::cos(orbitUv.y)), getCameraWorldForward()); camera_vector_t right = camera_vector_t(T(0)); camera_vector_t up = camera_vector_t(T(0)); @@ -586,8 +585,7 @@ struct CCameraMathUtilities final const camera_vector_t& position, const T minDistance, const T maxDistance, - T& outOrbitU, - T& outOrbitV, + camera_vector_t& outOrbitUv, T& outDistance) { const auto offset = position - targetPosition; @@ -597,10 +595,11 @@ struct CCameraMathUtilities final outDistance = std::clamp(distance, minDistance, maxDistance); const auto local = offset / outDistance; - outOrbitU = hlsl::atan2(local.y, local.x); - outOrbitV = hlsl::asin(std::clamp(local.z, T(-1), T(1))); - return isFiniteScalar(outOrbitU) && - isFiniteScalar(outOrbitV) && + outOrbitUv = camera_vector_t( + hlsl::atan2(local.y, local.x), + hlsl::asin(std::clamp(local.z, T(-1), T(1)))); + return isFiniteScalar(outOrbitUv.x) && + isFiniteScalar(outOrbitUv.y) && isFiniteScalar(outDistance); } @@ -632,8 +631,7 @@ struct CCameraMathUtilities final camera_vector_t& outPosition, camera_quaternion_t& outOrientation, T* outAppliedDistance = nullptr, - T* outOrbitU = nullptr, - T* outOrbitV = nullptr) + camera_vector_t* outOrbitUv = nullptr) { if (!isFiniteScalar(pathAngle) || !isFiniteScalar(pathRadius) || @@ -643,20 +641,17 @@ struct CCameraMathUtilities final const T appliedRadius = std::max(minRadius, pathRadius); const auto offset = makePathOffsetFromState(pathAngle, appliedRadius, pathHeight); - T orbitU = T(0); - T orbitV = T(0); + camera_vector_t orbitUv = camera_vector_t(T(0)); T distance = T(0); - if (!tryBuildOrbitFromPosition(targetPosition, targetPosition + offset, minDistance, maxDistance, orbitU, orbitV, distance)) + if (!tryBuildOrbitFromPosition(targetPosition, targetPosition + offset, minDistance, maxDistance, orbitUv, distance)) return false; - if (!tryBuildSphericalPoseFromOrbit(targetPosition, orbitU, orbitV, distance, minDistance, maxDistance, outPosition, outOrientation, &distance)) + if (!tryBuildSphericalPoseFromOrbit(targetPosition, orbitUv, distance, minDistance, maxDistance, outPosition, outOrientation, &distance)) return false; if (outAppliedDistance) *outAppliedDistance = distance; - if (outOrbitU) - *outOrbitU = orbitU; - if (outOrbitV) - *outOrbitV = orbitV; + if (outOrbitUv) + *outOrbitUv = orbitUv; return true; } diff --git a/common/include/camera/CCameraPathUtilities.hpp b/common/include/camera/CCameraPathUtilities.hpp index 4e7f20f2d..c1036497d 100644 --- a/common/include/camera/CCameraPathUtilities.hpp +++ b/common/include/camera/CCameraPathUtilities.hpp @@ -225,8 +225,7 @@ struct CCameraPathUtilities final outPose.position, outPose.orientation, &outPose.appliedDistance, - &outPose.orbitUv.x, - &outPose.orbitUv.y); + &outPose.orbitUv); } static inline bool tryBuildPathPoseFromState( diff --git a/common/include/camera/CCameraTargetRelativeUtilities.hpp b/common/include/camera/CCameraTargetRelativeUtilities.hpp index 5ede838f0..c86cea123 100644 --- a/common/include/camera/CCameraTargetRelativeUtilities.hpp +++ b/common/include/camera/CCameraTargetRelativeUtilities.hpp @@ -125,8 +125,7 @@ struct CCameraTargetRelativeUtilities final position, static_cast(minDistance), static_cast(maxDistance), - outState.orbitUv.x, - outState.orbitUv.y, + outState.orbitUv, appliedDistance)) { return false; @@ -145,8 +144,7 @@ struct CCameraTargetRelativeUtilities final outPose = {}; return hlsl::CCameraMathUtilities::tryBuildSphericalPoseFromOrbit( state.target, - state.orbitUv.x, - state.orbitUv.y, + state.orbitUv, static_cast(state.distance), static_cast(minDistance), static_cast(maxDistance), diff --git a/common/include/camera/CSphericalTargetCamera.hpp b/common/include/camera/CSphericalTargetCamera.hpp index b2cde8efe..34a7304a8 100644 --- a/common/include/camera/CSphericalTargetCamera.hpp +++ b/common/include/camera/CSphericalTargetCamera.hpp @@ -46,8 +46,6 @@ class CSphericalTargetCamera : public ICamera inline hlsl::float64_t3 getTarget() const { return m_targetPosition; } inline float getDistance() const { return m_distance; } - inline double getU() const { return m_orbitUv.x; } - inline double getV() const { return m_orbitUv.y; } inline const hlsl::float64_t2& getOrbitUv() const { return m_orbitUv; } static inline constexpr float MinDistance = base_t::SphericalMinDistance; From f8a96a7896ca22808eb9644ddc3d9e90a5819352 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Thu, 9 Apr 2026 12:45:12 +0200 Subject: [PATCH 189/205] Deduplicate camera pose structs --- common/include/camera/CCameraGoal.hpp | 10 +++++----- common/include/camera/CCameraPathUtilities.hpp | 4 +--- .../include/camera/CCameraTargetRelativeUtilities.hpp | 9 +++++++-- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/common/include/camera/CCameraGoal.hpp b/common/include/camera/CCameraGoal.hpp index 335aff771..d492e4330 100644 --- a/common/include/camera/CCameraGoal.hpp +++ b/common/include/camera/CCameraGoal.hpp @@ -96,14 +96,14 @@ struct CCameraGoalUtilities final goal.targetPosition = targetPosition; goal.hasPathState = true; goal.pathState = pathState; + SCameraTargetRelativePose canonicalPose = {}; + canonicalPose.position = canonicalPathState.pose.position; + canonicalPose.orientation = canonicalPathState.pose.orientation; + canonicalPose.appliedDistance = canonicalPathState.pose.appliedDistance; applyCanonicalTargetRelativeGoalFields( goal, canonicalPathState.targetRelative, - { - .position = canonicalPathState.pose.position, - .orientation = canonicalPathState.pose.orientation, - .appliedDistance = canonicalPathState.pose.appliedDistance - }); + canonicalPose); return true; } diff --git a/common/include/camera/CCameraPathUtilities.hpp b/common/include/camera/CCameraPathUtilities.hpp index c1036497d..e17bbab60 100644 --- a/common/include/camera/CCameraPathUtilities.hpp +++ b/common/include/camera/CCameraPathUtilities.hpp @@ -13,10 +13,8 @@ namespace nbl::core { //! Shared helpers for the target-relative cylindrical path rig exposed as `PathRig`. -struct SCameraPathPose final +struct SCameraPathPose final : SCameraRigPose { - hlsl::float64_t3 position = hlsl::float64_t3(0.0); - hlsl::camera_quaternion_t orientation = hlsl::CCameraMathUtilities::makeIdentityQuaternion(); hlsl::float64_t appliedDistance = 0.0; hlsl::float64_t2 orbitUv = hlsl::float64_t2(0.0); }; diff --git a/common/include/camera/CCameraTargetRelativeUtilities.hpp b/common/include/camera/CCameraTargetRelativeUtilities.hpp index c86cea123..d1656fdca 100644 --- a/common/include/camera/CCameraTargetRelativeUtilities.hpp +++ b/common/include/camera/CCameraTargetRelativeUtilities.hpp @@ -16,11 +16,16 @@ struct SCameraTargetRelativeState final float distance = ICamera::SphericalMinDistance; }; -//! Pose reconstructed from a target-relative orbit state. -struct SCameraTargetRelativePose final +//! Shared reconstructed camera pose used by target-relative and path rigs. +struct SCameraRigPose { hlsl::float64_t3 position = hlsl::float64_t3(0.0); hlsl::camera_quaternion_t orientation = hlsl::CCameraMathUtilities::makeIdentityQuaternion(); +}; + +//! Pose reconstructed from a target-relative orbit state. +struct SCameraTargetRelativePose final : SCameraRigPose +{ hlsl::float64_t appliedDistance = static_cast(ICamera::SphericalMinDistance); }; From 51e690a01ee0f9367a73428ef809a57201007ab9 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Thu, 9 Apr 2026 12:50:44 +0200 Subject: [PATCH 190/205] Deduplicate camera tracked poses --- 61_UI/AppHeadlessCameraSmokeChecks.inl | 7 +++---- 61_UI/AppScriptedInitialization.cpp | 7 +++---- common/include/camera/CCameraPathUtilities.hpp | 18 +++++++----------- .../camera/CCameraScriptedCheckRunner.hpp | 4 +--- .../include/camera/CCameraSequenceScript.hpp | 4 +--- 5 files changed, 15 insertions(+), 25 deletions(-) diff --git a/61_UI/AppHeadlessCameraSmokeChecks.inl b/61_UI/AppHeadlessCameraSmokeChecks.inl index 18b90128c..3d3d412bd 100644 --- a/61_UI/AppHeadlessCameraSmokeChecks.inl +++ b/61_UI/AppHeadlessCameraSmokeChecks.inl @@ -413,10 +413,9 @@ return false; } - const CCameraSequenceTrackedTargetPose referenceTrackedTargetPose = { - .position = SCameraAppSceneDefaults::DefaultFollowTargetPosition, - .orientation = SCameraAppSceneDefaults::DefaultFollowTargetOrientation - }; + CCameraSequenceTrackedTargetPose referenceTrackedTargetPose = {}; + referenceTrackedTargetPose.position = SCameraAppSceneDefaults::DefaultFollowTargetPosition; + referenceTrackedTargetPose.orientation = SCameraAppSceneDefaults::DefaultFollowTargetOrientation; nbl::core::CCameraSequenceCompiledSegment compiledSegment; std::string compileError; diff --git a/61_UI/AppScriptedInitialization.cpp b/61_UI/AppScriptedInitialization.cpp index 7df78bd02..82c12f760 100644 --- a/61_UI/AppScriptedInitialization.cpp +++ b/61_UI/AppScriptedInitialization.cpp @@ -159,10 +159,9 @@ bool App::expandPendingScriptedSequence(const CCameraSequenceScript& sequence) CCameraScriptedInputEvent::ActionData::Kind::SetUseWindow, useWindowMode ? 1 : 0); - const CCameraSequenceTrackedTargetPose referenceTrackedTargetPose = { - .position = getDefaultFollowTargetPosition(), - .orientation = getDefaultFollowTargetOrientation() - }; + CCameraSequenceTrackedTargetPose referenceTrackedTargetPose = {}; + referenceTrackedTargetPose.position = getDefaultFollowTargetPosition(); + referenceTrackedTargetPose.orientation = getDefaultFollowTargetOrientation(); uint64_t frameCursor = 0u; for (const auto& segment : sequence.segments) diff --git a/common/include/camera/CCameraPathUtilities.hpp b/common/include/camera/CCameraPathUtilities.hpp index e17bbab60..b14be14e6 100644 --- a/common/include/camera/CCameraPathUtilities.hpp +++ b/common/include/camera/CCameraPathUtilities.hpp @@ -19,15 +19,11 @@ struct SCameraPathPose final : SCameraRigPose hlsl::float64_t2 orbitUv = hlsl::float64_t2(0.0); }; -struct SCameraPathDelta final +struct SCameraPathDelta final : ICamera::PathState { - double radius = 0.0; - double height = 0.0; - double angle = 0.0; - inline hlsl::float64_t3 asVector() const { - return hlsl::float64_t3(radius, height, angle); + return ICamera::PathState::asVector(); } inline hlsl::float64_t3 translationVector() const @@ -37,11 +33,11 @@ struct SCameraPathDelta final static inline SCameraPathDelta fromVector(const hlsl::float64_t3& value) { - return { - .radius = value.x, - .height = value.y, - .angle = value.z - }; + SCameraPathDelta delta = {}; + delta.angle = value.z; + delta.radius = value.x; + delta.height = value.y; + return delta; } }; diff --git a/common/include/camera/CCameraScriptedCheckRunner.hpp b/common/include/camera/CCameraScriptedCheckRunner.hpp index d486b033e..f46aae882 100644 --- a/common/include/camera/CCameraScriptedCheckRunner.hpp +++ b/common/include/camera/CCameraScriptedCheckRunner.hpp @@ -31,11 +31,9 @@ namespace nbl::system */ struct CCameraScriptedCheckRuntimeState { - struct SPoseReference + struct SPoseReference final : core::SCameraRigPose { bool valid = false; - hlsl::float64_t3 position = hlsl::float64_t3(0.0); - hlsl::camera_quaternion_t orientation = hlsl::CCameraMathUtilities::makeIdentityQuaternion(); }; size_t nextCheckIndex = 0u; diff --git a/common/include/camera/CCameraSequenceScript.hpp b/common/include/camera/CCameraSequenceScript.hpp index dab01b62e..52e161888 100644 --- a/common/include/camera/CCameraSequenceScript.hpp +++ b/common/include/camera/CCameraSequenceScript.hpp @@ -110,10 +110,8 @@ struct CCameraSequenceKeyframe }; //! Concrete tracked-target pose sampled from a shared authored sequence. -struct CCameraSequenceTrackedTargetPose +struct CCameraSequenceTrackedTargetPose final : SCameraRigPose { - hlsl::float64_t3 position = hlsl::float64_t3(0.0); - hlsl::camera_quaternion_t orientation = hlsl::CCameraMathUtilities::makeIdentityQuaternion(); }; //! Relative tracked-target adjustment authored against an initial tracked-target pose. From 96f4232bbadfafe2b52d4a5a3543f3031794cd2b Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Thu, 9 Apr 2026 12:53:28 +0200 Subject: [PATCH 191/205] Deduplicate camera goal pose --- common/include/camera/CCameraGoal.hpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/common/include/camera/CCameraGoal.hpp b/common/include/camera/CCameraGoal.hpp index d492e4330..2e0730c7a 100644 --- a/common/include/camera/CCameraGoal.hpp +++ b/common/include/camera/CCameraGoal.hpp @@ -21,10 +21,8 @@ namespace nbl::core /** * Typed transport object for camera state used by capture, comparison, presets, and playback. */ -struct CCameraGoal +struct CCameraGoal : SCameraRigPose { - hlsl::float64_t3 position = hlsl::float64_t3(0.0); - hlsl::camera_quaternion_t orientation = hlsl::CCameraMathUtilities::makeIdentityQuaternion(); ICamera::CameraKind sourceKind = ICamera::CameraKind::Unknown; uint32_t sourceCapabilities = ICamera::None; uint32_t sourceGoalStateMask = ICamera::GoalStateNone; From c9c4d6186b3f55ed61ee4f4fda172967d83f5d3b Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Thu, 9 Apr 2026 14:07:44 +0200 Subject: [PATCH 192/205] Refactor path camera to typed state model --- 61_UI/AppHeadlessCameraSmoke.cpp | 1 + 61_UI/AppHeadlessCameraSmokeChecks.inl | 129 +++++++++ 61_UI/AppHeadlessCameraSmokeHelpers.inl | 5 +- common/include/camera/CCameraGoal.hpp | 7 +- common/include/camera/CCameraGoalSolver.hpp | 12 +- .../camera/CCameraInputBindingUtilities.hpp | 15 +- .../include/camera/CCameraMathUtilities.hpp | 94 ++++--- .../include/camera/CCameraPathUtilities.hpp | 260 +++++++++++++----- .../include/camera/CCameraSequenceScript.hpp | 28 +- common/include/camera/CPathCamera.hpp | 105 +++++-- common/include/camera/ICamera.hpp | 42 ++- common/include/camera/README.md | 4 +- .../CCameraJsonPersistenceUtilities.hpp | 17 +- .../examples/camera/CCameraPersistence.cpp | 7 +- .../CCameraScriptedRuntimePersistence.cpp | 44 ++- 15 files changed, 588 insertions(+), 182 deletions(-) diff --git a/61_UI/AppHeadlessCameraSmoke.cpp b/61_UI/AppHeadlessCameraSmoke.cpp index 1742e8475..3490f9990 100644 --- a/61_UI/AppHeadlessCameraSmoke.cpp +++ b/61_UI/AppHeadlessCameraSmoke.cpp @@ -79,6 +79,7 @@ bool App::runHeadlessCameraSmoke(argparse::ArgumentParser& program, smart_refctd .freeCamera = cameraInventory.free, .chaseCamera = cameraInventory.chase, .dollyCamera = cameraInventory.dolly, + .pathCamera = cameraInventory.path, .dollyZoomCamera = cameraInventory.dollyZoom }; diff --git a/61_UI/AppHeadlessCameraSmokeChecks.inl b/61_UI/AppHeadlessCameraSmokeChecks.inl index 3d3d412bd..4d4363158 100644 --- a/61_UI/AppHeadlessCameraSmokeChecks.inl +++ b/61_UI/AppHeadlessCameraSmokeChecks.inl @@ -7,6 +7,7 @@ ICamera* freeCamera = nullptr; ICamera* chaseCamera = nullptr; ICamera* dollyCamera = nullptr; + ICamera* pathCamera = nullptr; ICamera* dollyZoomCamera = nullptr; }; @@ -600,6 +601,134 @@ } } + 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; + } + + 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, + nbl::core::CCameraPathUtilities::makeDefaultPathLimits(), + 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; + } + } + { std::vector scaledEvents(3u); scaledEvents[0].type = CVirtualGimbalEvent::MoveForward; diff --git a/61_UI/AppHeadlessCameraSmokeHelpers.inl b/61_UI/AppHeadlessCameraSmokeHelpers.inl index 0a7378cf1..2d7cea23b 100644 --- a/61_UI/AppHeadlessCameraSmokeHelpers.inl +++ b/61_UI/AppHeadlessCameraSmokeHelpers.inl @@ -160,6 +160,7 @@ namespace ICamera* free = nullptr; ICamera* chase = nullptr; ICamera* dolly = nullptr; + ICamera* path = nullptr; ICamera* dollyZoom = nullptr; }; @@ -372,6 +373,7 @@ namespace .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) }; } @@ -738,6 +740,7 @@ namespace { const auto modifiedPreset = nbl::core::CCameraPresetFlowUtilities::capturePreset(goalSolver, camera, "smoke-direct"); + const bool requireDirectPresetChange = camera->getKind() != ICamera::CameraKind::Path; if (!applyPresetAndValidate( goalSolver, camera, @@ -756,7 +759,7 @@ namespace camera, modifiedPreset, EPresetComparePolicy::StrictThresholds, - true, + requireDirectPresetChange, false, "Preset apply from direct smoke failed for camera \"" + cameraIdentifier + "\"", outError)) diff --git a/common/include/camera/CCameraGoal.hpp b/common/include/camera/CCameraGoal.hpp index 2e0730c7a..21e71f126 100644 --- a/common/include/camera/CCameraGoal.hpp +++ b/common/include/camera/CCameraGoal.hpp @@ -300,9 +300,10 @@ struct CCameraGoalUtilities final } if (actual.hasPathState) { - oss << " path_angle=" << actual.pathState.angle - << " path_radius=" << actual.pathState.radius - << " path_height=" << actual.pathState.height; + oss << " path_s=" << actual.pathState.s + << " path_u=" << actual.pathState.u + << " path_v=" << actual.pathState.v + << " path_roll=" << actual.pathState.roll; } else if (expected.hasPathState) { diff --git a/common/include/camera/CCameraGoalSolver.hpp b/common/include/camera/CCameraGoalSolver.hpp index 2891a7a06..71f4c5f2f 100644 --- a/common/include/camera/CCameraGoalSolver.hpp +++ b/common/include/camera/CCameraGoalSolver.hpp @@ -422,14 +422,15 @@ class CCameraGoalSolver inline void appendPathDeltaEvents( std::vector& events, const SCameraPathDelta& delta, - const double moveDenominator) const + const double moveDenominator, + const double rotationDenominator) const { - CCameraPathUtilities::appendPathAdvanceEvents( + CCameraPathUtilities::appendPathDeltaEvents( events, delta, moveDenominator, - SCameraPathDefaults::ComparisonThresholds.angleToleranceDeg, - SCameraPathDefaults::ComparisonThresholds.scalarTolerance); + rotationDenominator, + SCameraPathDefaults::ExactComparisonThresholds); } inline double getMoveMagnitudeDenominator(const ICamera* camera) const @@ -544,7 +545,8 @@ class CCameraGoalSolver } const auto moveDenom = getMoveMagnitudeDenominator(camera); - appendPathDeltaEvents(out, transition.delta, moveDenom); + const auto rotationDenom = getRotationMagnitudeDenominator(camera); + appendPathDeltaEvents(out, transition.delta, moveDenom, rotationDenom); return !out.empty(); } diff --git a/common/include/camera/CCameraInputBindingUtilities.hpp b/common/include/camera/CCameraInputBindingUtilities.hpp index c7183432f..727a888c0 100644 --- a/common/include/camera/CCameraInputBindingUtilities.hpp +++ b/common/include/camera/CCameraInputBindingUtilities.hpp @@ -213,13 +213,13 @@ struct CCameraInputBindingUtilities final core::CVirtualGimbalEvent::MoveDown, core::CVirtualGimbalEvent::MoveUp }; - static inline constexpr std::array PathRigAdvanceAndRadius = { + static inline constexpr std::array PathRigProgressAndU = { core::CVirtualGimbalEvent::MoveForward, core::CVirtualGimbalEvent::MoveBackward, core::CVirtualGimbalEvent::MoveLeft, core::CVirtualGimbalEvent::MoveRight }; - static inline constexpr std::array PathRigHeight = VerticalMove; + static inline constexpr std::array PathRigV = VerticalMove; static inline constexpr std::array TurntableMove = { core::CVirtualGimbalEvent::MoveForward, core::CVirtualGimbalEvent::MoveBackward, @@ -390,8 +390,8 @@ struct CCameraInputBindingUtilities final }; static inline constexpr SKeyboardPresetSpec PathKeyboardSpec = { - SCameraInputBindingEventGroups::PathRigAdvanceAndRadius, - SCameraInputBindingEventGroups::PathRigHeight, + SCameraInputBindingEventGroups::PathRigProgressAndU, + SCameraInputBindingEventGroups::PathRigV, {} }; @@ -415,6 +415,11 @@ struct CCameraInputBindingUtilities final OrbitMouseSpec.scroll }; + static inline constexpr SMousePresetSpec PathMouseSpec = { + SCameraInputBindingEventGroups::RelativeOrbitTranslate, + SCameraInputBindingEventGroups::OrbitZoom + }; + static inline constexpr SCameraInteractionBindingSpec FpsInteractionBindingSpec = { FpsKeyboardSpec, FpsMouseSpec @@ -447,7 +452,7 @@ struct CCameraInputBindingUtilities final static inline constexpr SCameraInteractionBindingSpec PathInteractionBindingSpec = { PathKeyboardSpec, - OrbitMouseSpec + PathMouseSpec }; template diff --git a/common/include/camera/CCameraMathUtilities.hpp b/common/include/camera/CCameraMathUtilities.hpp index 5b60351db..a346db24f 100644 --- a/common/include/camera/CCameraMathUtilities.hpp +++ b/common/include/camera/CCameraMathUtilities.hpp @@ -431,60 +431,62 @@ struct CCameraMathUtilities final } template - static inline T getPathDistance(const T radius, const T height) + static inline T getPathDistance(const T pathU, const T pathV) { - return length(camera_vector_t(radius, height)); + return length(camera_vector_t(pathU, pathV)); } template - static inline camera_vector_t makePathOffsetFromState(const T angle, const T radius, const T height) + static inline camera_vector_t makePathOffsetFromState(const T pathS, const T pathU, const T pathV) { - return camera_vector_t(hlsl::cos(angle) * radius, height, hlsl::sin(angle) * radius); + return camera_vector_t(hlsl::cos(pathS) * pathU, pathV, hlsl::sin(pathS) * pathU); } template - static inline bool sanitizePathState(T& angle, T& radius, T& height, const T minRadius) + static inline bool sanitizePathState(T& pathS, T& pathU, T& pathV, T& pathRoll, const T minU) { - if (!isFiniteScalar(angle) || !isFiniteScalar(radius) || !isFiniteScalar(height)) + if (!isFiniteScalar(pathS) || !isFiniteScalar(pathU) || !isFiniteScalar(pathV) || !isFiniteScalar(pathRoll)) return false; - angle = wrapAngleRad(angle); - radius = std::max(minRadius, radius); - return isFiniteScalar(angle) && - isFiniteScalar(radius) && - isFiniteScalar(height); + pathS = wrapAngleRad(pathS); + pathU = std::max(minU, pathU); + pathRoll = wrapAngleRad(pathRoll); + return isFiniteScalar(pathS) && + isFiniteScalar(pathU) && + isFiniteScalar(pathV) && + isFiniteScalar(pathRoll); } template static inline bool tryScalePathStateDistance( const T desiredDistance, - const T minRadius, - T& radius, - T& height, + const T minU, + T& pathU, + T& pathV, T* outAppliedDistance = nullptr) { if (!isFiniteScalar(desiredDistance) || - !isFiniteScalar(radius) || - !isFiniteScalar(height)) + !isFiniteScalar(pathU) || + !isFiniteScalar(pathV)) return false; - const T currentDistance = getPathDistance(radius, height); + const T currentDistance = getPathDistance(pathU, pathV); constexpr T Epsilon = std::numeric_limits::epsilon(); if (currentDistance > Epsilon) { const T scale = desiredDistance / currentDistance; - radius = std::max(minRadius, radius * scale); - height *= scale; + pathU = std::max(minU, pathU * scale); + pathV *= scale; } else { - radius = std::max(minRadius, desiredDistance); - height = T(0); + pathU = std::max(minU, desiredDistance); + pathV = T(0); } if (outAppliedDistance) - *outAppliedDistance = getPathDistance(radius, height); - return isFiniteScalar(radius) && isFiniteScalar(height); + *outAppliedDistance = getPathDistance(pathU, pathV); + return isFiniteScalar(pathU) && isFiniteScalar(pathV); } template @@ -492,21 +494,21 @@ struct CCameraMathUtilities final const camera_vector_t& targetPosition, const camera_vector_t& position, const T minRadius, - T& outAngle, - T& outRadius, - T& outHeight) + T& outS, + T& outU, + T& outV) { const auto offset = position - targetPosition; const auto radius = getPlanarRadiusXZ(offset); if (!isFiniteScalar(radius) || !isFiniteScalar(offset.y)) return false; - outAngle = wrapAngleRad(hlsl::atan2(offset.z, offset.x)); - outRadius = std::max(minRadius, radius); - outHeight = offset.y; - return isFiniteScalar(outAngle) && - isFiniteScalar(outRadius) && - isFiniteScalar(outHeight); + outS = wrapAngleRad(hlsl::atan2(offset.z, offset.x)); + outU = std::max(minRadius, radius); + outV = offset.y; + return isFiniteScalar(outS) && + isFiniteScalar(outU) && + isFiniteScalar(outV); } template @@ -622,9 +624,10 @@ struct CCameraMathUtilities final template static inline bool tryBuildPathPoseFromState( const camera_vector_t& targetPosition, - const T pathAngle, - const T pathRadius, - const T pathHeight, + const T pathS, + const T pathU, + const T pathV, + const T pathRoll, const T minRadius, const T minDistance, const T maxDistance, @@ -633,13 +636,14 @@ struct CCameraMathUtilities final T* outAppliedDistance = nullptr, camera_vector_t* outOrbitUv = nullptr) { - if (!isFiniteScalar(pathAngle) || - !isFiniteScalar(pathRadius) || - !isFiniteScalar(pathHeight)) + if (!isFiniteScalar(pathS) || + !isFiniteScalar(pathU) || + !isFiniteScalar(pathV) || + !isFiniteScalar(pathRoll)) return false; - const T appliedRadius = std::max(minRadius, pathRadius); - const auto offset = makePathOffsetFromState(pathAngle, appliedRadius, pathHeight); + const T appliedU = std::max(minRadius, pathU); + const auto offset = makePathOffsetFromState(pathS, appliedU, pathV); camera_vector_t orbitUv = camera_vector_t(T(0)); T distance = T(0); @@ -648,6 +652,16 @@ struct CCameraMathUtilities final if (!tryBuildSphericalPoseFromOrbit(targetPosition, orbitUv, distance, minDistance, maxDistance, outPosition, outOrientation, &distance)) return false; + if (!isNearlyZeroScalar(pathRoll, std::numeric_limits::epsilon())) + { + const auto basis = getQuaternionBasisMatrix(outOrientation); + const T rollCos = hlsl::cos(pathRoll); + const T rollSin = hlsl::sin(pathRoll); + const auto right = basis[0u] * rollCos + basis[1u] * rollSin; + const auto up = basis[1u] * rollCos - basis[0u] * rollSin; + outOrientation = makeQuaternionFromBasis(right, up, basis[2u]); + } + if (outAppliedDistance) *outAppliedDistance = distance; if (outOrbitUv) diff --git a/common/include/camera/CCameraPathUtilities.hpp b/common/include/camera/CCameraPathUtilities.hpp index b14be14e6..6f20f828c 100644 --- a/common/include/camera/CCameraPathUtilities.hpp +++ b/common/include/camera/CCameraPathUtilities.hpp @@ -1,7 +1,9 @@ #ifndef _C_CAMERA_PATH_UTILITIES_HPP_ #define _C_CAMERA_PATH_UTILITIES_HPP_ +#include #include +#include #include #include @@ -12,7 +14,7 @@ namespace nbl::core { -//! Shared helpers for the target-relative cylindrical path rig exposed as `PathRig`. +//! Shared helpers for the reusable `PathRig` camera kind. struct SCameraPathPose final : SCameraRigPose { hlsl::float64_t appliedDistance = 0.0; @@ -21,22 +23,33 @@ struct SCameraPathPose final : SCameraRigPose struct SCameraPathDelta final : ICamera::PathState { - inline hlsl::float64_t3 asVector() const + inline hlsl::float64_t4 asVector() const { return ICamera::PathState::asVector(); } inline hlsl::float64_t3 translationVector() const { - return hlsl::float64_t3(radius, height, 0.0); + return ICamera::PathState::asTranslationVector(); } - static inline SCameraPathDelta fromVector(const hlsl::float64_t3& value) + static inline SCameraPathDelta fromVector(const hlsl::float64_t4& value) { SCameraPathDelta delta = {}; - delta.angle = value.z; - delta.radius = value.x; - delta.height = value.y; + delta.s = value.x; + delta.u = value.y; + delta.v = value.z; + delta.roll = value.w; + return delta; + } + + static inline SCameraPathDelta fromMotion(const hlsl::float64_t3& translation, const double pathRoll = 0.0) + { + SCameraPathDelta delta = {}; + delta.s = translation.z; + delta.u = translation.x; + delta.v = translation.y; + delta.roll = pathRoll; return delta; } }; @@ -56,7 +69,8 @@ struct SCameraCanonicalPathState final struct SCameraPathComparisonThresholds final { - double angleToleranceDeg = ICamera::DefaultAngularToleranceDeg; + double sToleranceDeg = ICamera::DefaultAngularToleranceDeg; + double rollToleranceDeg = ICamera::DefaultAngularToleranceDeg; double scalarTolerance = ICamera::ScalarTolerance; }; @@ -68,50 +82,97 @@ struct SCameraPathDistanceUpdateResult final struct SCameraPathDefaults final { - static constexpr double MinRadius = static_cast(ICamera::SphericalMinDistance); + static constexpr double MinU = static_cast(ICamera::SphericalMinDistance); static constexpr double ScalarTolerance = ICamera::ScalarTolerance; static constexpr double ExactStateTolerance = ICamera::TinyScalarEpsilon; static constexpr double ExactAngleToleranceDeg = ExactStateTolerance * 180.0 / hlsl::numbers::pi; static constexpr double AngleToleranceDeg = ICamera::DefaultAngularToleranceDeg; - static inline constexpr std::string_view Identifier = "Target-relative Cylindrical Path Rig"; - static inline constexpr std::string_view Description = "Adjust a target-relative cylindrical path rig around a target"; + static inline constexpr std::string_view Identifier = "Target-relative Path Rig"; + static inline constexpr std::string_view Description = "Adjust a target-relative path rig with s/u/v/roll state"; + struct SLimits final { - double minRadius = SCameraPathDefaults::MinRadius; + double minU = SCameraPathDefaults::MinU; hlsl::float64_t minDistance = static_cast(ICamera::SphericalMinDistance); hlsl::float64_t maxDistance = static_cast(ICamera::SphericalMaxDistance); }; static inline constexpr SLimits Limits = {}; static inline constexpr SCameraPathComparisonThresholds ComparisonThresholds = { - .angleToleranceDeg = AngleToleranceDeg, + .sToleranceDeg = AngleToleranceDeg, + .rollToleranceDeg = AngleToleranceDeg, .scalarTolerance = ScalarTolerance }; static inline constexpr SCameraPathComparisonThresholds ExactComparisonThresholds = { - .angleToleranceDeg = ExactAngleToleranceDeg, + .sToleranceDeg = ExactAngleToleranceDeg, + .rollToleranceDeg = ExactAngleToleranceDeg, .scalarTolerance = ExactStateTolerance }; }; using SCameraPathLimits = SCameraPathDefaults::SLimits; +struct SCameraPathControlContext final +{ + ICamera::PathState currentState = {}; + hlsl::float64_t3 translation = hlsl::float64_t3(0.0); + hlsl::float64_t3 rotation = hlsl::float64_t3(0.0); + hlsl::float64_t3 targetPosition = hlsl::float64_t3(0.0); + const CReferenceTransform* reference = nullptr; + SCameraPathLimits limits = SCameraPathDefaults::Limits; +}; + +struct SCameraPathModel final +{ + using resolve_state_t = std::function; + using control_law_t = std::function; + using integrate_t = std::function; + using evaluate_t = std::function; + using update_distance_t = std::function; + + resolve_state_t resolveState; + control_law_t controlLaw; + integrate_t integrate; + evaluate_t evaluate; + update_distance_t updateDistance; +}; + struct CCameraPathUtilities final { - static inline ICamera::PathState makeDefaultPathState(const double minRadius = SCameraPathDefaults::MinRadius) + static inline ICamera::PathState makeDefaultPathState(const double minU = SCameraPathDefaults::MinU) { return { - .angle = 0.0, - .radius = minRadius, - .height = 0.0 + .s = 0.0, + .u = minU, + .v = 0.0, + .roll = 0.0 }; } static inline SCameraPathComparisonThresholds makePathComparisonThresholds( - const double angleToleranceDeg = SCameraPathDefaults::AngleToleranceDeg, + const double angularToleranceDeg = SCameraPathDefaults::AngleToleranceDeg, const double scalarTolerance = SCameraPathDefaults::ScalarTolerance) { return { - .angleToleranceDeg = angleToleranceDeg, + .sToleranceDeg = angularToleranceDeg, + .rollToleranceDeg = angularToleranceDeg, .scalarTolerance = scalarTolerance }; } @@ -123,27 +184,28 @@ struct CCameraPathUtilities final static inline bool isPathStateFinite(const ICamera::PathState& state) { - return hlsl::CCameraMathUtilities::isFiniteScalar(state.angle) && - hlsl::CCameraMathUtilities::isFiniteScalar(state.radius) && - hlsl::CCameraMathUtilities::isFiniteScalar(state.height); + return hlsl::CCameraMathUtilities::isFiniteScalar(state.s) && + hlsl::CCameraMathUtilities::isFiniteScalar(state.u) && + hlsl::CCameraMathUtilities::isFiniteScalar(state.v) && + hlsl::CCameraMathUtilities::isFiniteScalar(state.roll); } - static inline bool sanitizePathState(ICamera::PathState& state, const double minRadius) + static inline bool sanitizePathState(ICamera::PathState& state, const double minU) { - return hlsl::CCameraMathUtilities::sanitizePathState(state.angle, state.radius, state.height, minRadius); + return hlsl::CCameraMathUtilities::sanitizePathState(state.s, state.u, state.v, state.roll, minU); } static inline bool tryScalePathStateDistance( const double desiredDistance, - const double minRadius, + const double minU, ICamera::PathState& ioState, double* outAppliedDistance = nullptr) { return hlsl::CCameraMathUtilities::tryScalePathStateDistance( desiredDistance, - minRadius, - ioState.radius, - ioState.height, + minU, + ioState.u, + ioState.v, outAppliedDistance); } @@ -155,7 +217,7 @@ struct CCameraPathUtilities final { const auto clampedDistance = std::clamp(desiredDistance, limits.minDistance, limits.maxDistance); double appliedDistance = 0.0; - if (!tryScalePathStateDistance(static_cast(clampedDistance), limits.minRadius, ioState, &appliedDistance)) + if (!tryScalePathStateDistance(static_cast(clampedDistance), limits.minU, ioState, &appliedDistance)) return false; if (outResult) @@ -170,16 +232,23 @@ struct CCameraPathUtilities final static inline bool tryBuildPathStateFromPosition( const hlsl::float64_t3& targetPosition, const hlsl::float64_t3& position, - const double minRadius, + const double minU, ICamera::PathState& outState) { - return hlsl::CCameraMathUtilities::tryBuildPathStateFromPosition( - targetPosition, - position, - minRadius, - outState.angle, - outState.radius, - outState.height); + outState = {}; + if (!hlsl::CCameraMathUtilities::tryBuildPathStateFromPosition( + targetPosition, + position, + minU, + outState.s, + outState.u, + outState.v)) + { + return false; + } + + outState.roll = 0.0; + return true; } static inline bool tryResolvePathState( @@ -192,14 +261,14 @@ struct CCameraPathUtilities final if (requestedState) { outState = *requestedState; - return sanitizePathState(outState, limits.minRadius); + return sanitizePathState(outState, limits.minU); } - if (tryBuildPathStateFromPosition(targetPosition, position, limits.minRadius, outState)) + if (tryBuildPathStateFromPosition(targetPosition, position, limits.minU, outState)) return true; - outState = makeDefaultPathState(limits.minRadius); - return sanitizePathState(outState, limits.minRadius); + outState = makeDefaultPathState(limits.minU); + return sanitizePathState(outState, limits.minU); } static inline bool tryBuildPathPoseFromState( @@ -210,10 +279,11 @@ struct CCameraPathUtilities final { return hlsl::CCameraMathUtilities::tryBuildPathPoseFromState( targetPosition, - state.angle, - state.radius, - state.height, - limits.minRadius, + state.s, + state.u, + state.v, + state.roll, + limits.minU, limits.minDistance, limits.maxDistance, outPose.position, @@ -249,9 +319,10 @@ struct CCameraPathUtilities final const ICamera::PathState& rhs, const SCameraPathComparisonThresholds& thresholds = {}) { - return hlsl::CCameraMathUtilities::getWrappedAngleDistanceDegrees(lhs.angle, rhs.angle) <= thresholds.angleToleranceDeg && - hlsl::CCameraMathUtilities::nearlyEqualScalar(lhs.radius, rhs.radius, thresholds.scalarTolerance) && - hlsl::CCameraMathUtilities::nearlyEqualScalar(lhs.height, rhs.height, thresholds.scalarTolerance); + return hlsl::CCameraMathUtilities::getWrappedAngleDistanceDegrees(lhs.s, rhs.s) <= thresholds.sToleranceDeg && + hlsl::CCameraMathUtilities::nearlyEqualScalar(lhs.u, rhs.u, thresholds.scalarTolerance) && + hlsl::CCameraMathUtilities::nearlyEqualScalar(lhs.v, rhs.v, thresholds.scalarTolerance) && + hlsl::CCameraMathUtilities::getWrappedAngleDistanceDegrees(lhs.roll, rhs.roll) <= thresholds.rollToleranceDeg; } static inline bool pathStatesChanged( @@ -262,12 +333,13 @@ struct CCameraPathUtilities final return !pathStatesNearlyEqual(lhs, rhs, thresholds); } - static inline hlsl::float64_t3 buildPathStateDeltaVector( + static inline hlsl::float64_t4 buildPathStateDeltaVector( const ICamera::PathState& currentState, const ICamera::PathState& desiredState) { auto deltaVector = desiredState.asVector() - currentState.asVector(); - deltaVector.z = hlsl::CCameraMathUtilities::wrapAngleRad(deltaVector.z); + deltaVector.x = hlsl::CCameraMathUtilities::wrapAngleRad(deltaVector.x); + deltaVector.w = hlsl::CCameraMathUtilities::wrapAngleRad(deltaVector.w); return deltaVector; } @@ -278,30 +350,37 @@ struct CCameraPathUtilities final return SCameraPathDelta::fromVector(buildPathStateDeltaVector(currentState, desiredState)); } - static inline SCameraPathDelta makePathDeltaFromVirtualPathTranslate(const hlsl::float64_t3& delta) + static inline SCameraPathDelta makePathDeltaFromVirtualPathMotion( + const hlsl::float64_t3& translation, + const hlsl::float64_t3& rotation = hlsl::float64_t3(0.0)) { - return SCameraPathDelta::fromVector(delta); + return SCameraPathDelta::fromMotion(translation, rotation.z); } - static inline void appendPathAdvanceEvents( + static inline SCameraPathDelta buildDefaultPathControlDelta(const SCameraPathControlContext& context) + { + return makePathDeltaFromVirtualPathMotion(context.translation, context.rotation); + } + + static inline void appendPathDeltaEvents( std::vector& events, const SCameraPathDelta& delta, const double moveDenominator, - const double angleToleranceDeg = SCameraPathDefaults::AngleToleranceDeg, - const double scalarTolerance = SCameraPathDefaults::ScalarTolerance) + const double rotationDenominator, + const SCameraPathComparisonThresholds& thresholds = {}) { CCameraVirtualEventUtilities::appendLocalTranslationEvents( events, delta.translationVector(), hlsl::float64_t3(moveDenominator), - hlsl::float64_t3(scalarTolerance)); + hlsl::float64_t3(thresholds.scalarTolerance)); CCameraVirtualEventUtilities::appendAngularDeltaEvent( events, - delta.angle, - moveDenominator, - angleToleranceDeg, - CVirtualGimbalEvent::MoveForward, - CVirtualGimbalEvent::MoveBackward); + delta.roll, + rotationDenominator, + thresholds.rollToleranceDeg, + CVirtualGimbalEvent::RollRight, + CVirtualGimbalEvent::RollLeft); } static inline bool tryBuildCanonicalPathState( @@ -329,9 +408,10 @@ struct CCameraPathUtilities final ICamera::PathState& outState) { auto stateVector = currentState.asVector() + delta.asVector(); - stateVector.z = hlsl::CCameraMathUtilities::wrapAngleRad(stateVector.z); + stateVector.x = hlsl::CCameraMathUtilities::wrapAngleRad(stateVector.x); + stateVector.w = hlsl::CCameraMathUtilities::wrapAngleRad(stateVector.w); outState = ICamera::PathState::fromVector(stateVector); - return sanitizePathState(outState, limits.minRadius); + return sanitizePathState(outState, limits.minU); } static inline ICamera::PathState blendPathStates( @@ -342,9 +422,10 @@ struct CCameraPathUtilities final const auto fromVector = from.asVector(); const auto toVector = to.asVector(); return { - .angle = hlsl::CCameraMathUtilities::lerpWrappedAngleRad(fromVector.z, toVector.z, alpha), - .radius = fromVector.x + (toVector.x - fromVector.x) * alpha, - .height = fromVector.y + (toVector.y - fromVector.y) * alpha + .s = hlsl::CCameraMathUtilities::lerpWrappedAngleRad(fromVector.x, toVector.x, alpha), + .u = fromVector.y + (toVector.y - fromVector.y) * alpha, + .v = fromVector.z + (toVector.z - fromVector.z) * alpha, + .roll = hlsl::CCameraMathUtilities::lerpWrappedAngleRad(fromVector.w, toVector.w, alpha) }; } @@ -365,9 +446,52 @@ struct CCameraPathUtilities final outTransition.delta = buildPathStateDelta(outTransition.current, outTransition.desired); return true; } + + static inline SCameraPathModel makeDefaultPathModel() + { + return { + .resolveState = + [](const hlsl::float64_t3& targetPosition, + const hlsl::float64_t3& position, + const SCameraPathLimits& limits, + const ICamera::PathState* requestedState, + ICamera::PathState& outState) -> bool + { + return tryResolvePathState(targetPosition, position, limits, requestedState, outState); + }, + .controlLaw = + [](const SCameraPathControlContext& context) -> SCameraPathDelta + { + return buildDefaultPathControlDelta(context); + }, + .integrate = + [](const ICamera::PathState& currentState, + const SCameraPathDelta& delta, + const SCameraPathLimits& limits, + ICamera::PathState& outState) -> bool + { + return tryApplyPathStateDelta(currentState, delta, limits, outState); + }, + .evaluate = + [](const hlsl::float64_t3& targetPosition, + const ICamera::PathState& state, + const SCameraPathLimits& limits, + SCameraCanonicalPathState& outState) -> bool + { + return tryBuildCanonicalPathState(targetPosition, state, limits, outState); + }, + .updateDistance = + [](const float desiredDistance, + const SCameraPathLimits& limits, + ICamera::PathState& ioState, + SCameraPathDistanceUpdateResult* outResult) -> bool + { + return tryUpdatePathStateDistance(desiredDistance, limits, ioState, outResult); + } + }; + } }; } // namespace nbl::core #endif // _C_CAMERA_PATH_UTILITIES_HPP_ - diff --git a/common/include/camera/CCameraSequenceScript.hpp b/common/include/camera/CCameraSequenceScript.hpp index 52e161888..3be290afe 100644 --- a/common/include/camera/CCameraSequenceScript.hpp +++ b/common/include/camera/CCameraSequenceScript.hpp @@ -82,14 +82,17 @@ struct CCameraSequenceGoalDelta bool hasOrbitDistanceDelta = false; float orbitDistanceDelta = 0.f; - bool hasPathAngleDeltaDeg = false; - double pathAngleDeltaDeg = 0.0; + bool hasPathSDeltaDeg = false; + double pathSDeltaDeg = 0.0; - bool hasPathRadiusDelta = false; - double pathRadiusDelta = 0.0; + bool hasPathUDelta = false; + double pathUDelta = 0.0; - bool hasPathHeightDelta = false; - double pathHeightDelta = 0.0; + bool hasPathVDelta = false; + double pathVDelta = 0.0; + + bool hasPathRollDeltaDeg = false; + double pathRollDeltaDeg = 0.0; bool hasDynamicBaseFovDelta = false; float dynamicBaseFovDelta = 0.f; @@ -314,7 +317,7 @@ struct CCameraSequenceScriptUtilities final const bool hasPoseDelta = delta.hasPositionOffset || delta.hasRotationEulerDegOffset; const bool hasSphericalDelta = delta.hasTargetOffset || delta.hasOrbitUDeltaDeg || delta.hasOrbitVDeltaDeg || delta.hasOrbitDistanceDelta; - const bool hasPathDelta = delta.hasPathAngleDeltaDeg || delta.hasPathRadiusDelta || delta.hasPathHeightDelta; + const bool hasPathDelta = delta.hasPathSDeltaDeg || delta.hasPathUDelta || delta.hasPathVDelta || delta.hasPathRollDeltaDeg; if (hasPoseDelta && (hasSphericalDelta || hasPathDelta)) { @@ -367,7 +370,7 @@ struct CCameraSequenceScriptUtilities final goal.orbitDistance += delta.orbitDistanceDelta; } - if (delta.hasPathAngleDeltaDeg || delta.hasPathRadiusDelta || delta.hasPathHeightDelta) + if (delta.hasPathSDeltaDeg || delta.hasPathUDelta || delta.hasPathVDelta || delta.hasPathRollDeltaDeg) { if (!goal.hasPathState) { @@ -376,10 +379,11 @@ struct CCameraSequenceScriptUtilities final return false; } - const auto pathDelta = SCameraPathDelta::fromVector(hlsl::float64_t3( - delta.hasPathRadiusDelta ? static_cast(delta.pathRadiusDelta) : hlsl::float64_t(0.0), - delta.hasPathHeightDelta ? static_cast(delta.pathHeightDelta) : hlsl::float64_t(0.0), - delta.hasPathAngleDeltaDeg ? static_cast(hlsl::radians(delta.pathAngleDeltaDeg)) : hlsl::float64_t(0.0))); + SCameraPathDelta pathDelta = {}; + pathDelta.s = delta.hasPathSDeltaDeg ? static_cast(hlsl::radians(delta.pathSDeltaDeg)) : hlsl::float64_t(0.0); + pathDelta.u = delta.hasPathUDelta ? static_cast(delta.pathUDelta) : hlsl::float64_t(0.0); + pathDelta.v = delta.hasPathVDelta ? static_cast(delta.pathVDelta) : hlsl::float64_t(0.0); + pathDelta.roll = delta.hasPathRollDeltaDeg ? static_cast(hlsl::radians(delta.pathRollDeltaDeg)) : hlsl::float64_t(0.0); if (!CCameraPathUtilities::tryApplyPathStateDelta(goal.pathState, pathDelta, CCameraPathUtilities::makeDefaultPathLimits(), goal.pathState)) { if (error) diff --git a/common/include/camera/CPathCamera.hpp b/common/include/camera/CPathCamera.hpp index 9dbc29315..a51f6a05e 100644 --- a/common/include/camera/CPathCamera.hpp +++ b/common/include/camera/CPathCamera.hpp @@ -2,50 +2,87 @@ #define _C_PATH_CAMERA_HPP_ #include +#include + #include "CCameraPathUtilities.hpp" #include "CSphericalTargetCamera.hpp" namespace nbl::core { -//! Target-relative cylindrical path rig camera driven by `PathState`. +//! Path-rig camera driven by typed `PathState` plus an injected path model. //! -//! The authored surface exposes this kind as `PathRig`. It stores a -//! cylindrical target-relative state `(angle, radius, height)` and rebuilds the -//! camera pose from that state through the shared path utilities. +//! The public runtime contract stays event-only through `manipulate(...)`. +//! `CPathCamera` only interprets the accumulated impulse through `m_pathModel` +//! instead of hardcoding one default target-relative mapping in the method body. class CPathCamera final : public CSphericalTargetCamera { public: using base_t = CSphericalTargetCamera; + using path_model_t = SCameraPathModel; CPathCamera(const hlsl::float64_t3& position, const hlsl::float64_t3& target) : base_t(position, target) { - CCameraPathUtilities::tryResolvePathState(target, position, SCameraPathDefaults::Limits, nullptr, m_pathState); + m_pathModel = CCameraPathUtilities::makeDefaultPathModel(); + m_pathModel.resolveState(target, position, SCameraPathDefaults::Limits, nullptr, m_pathState); updateFromPathState(); } + + CPathCamera(const hlsl::float64_t3& position, const hlsl::float64_t3& target, path_model_t pathModel) + : base_t(position, target) + , m_pathModel(std::move(pathModel)) + { + if (!m_pathModel.resolveState) + m_pathModel = CCameraPathUtilities::makeDefaultPathModel(); + m_pathModel.resolveState(target, position, SCameraPathDefaults::Limits, nullptr, m_pathState); + updateFromPathState(); + } + ~CPathCamera() = default; const typename base_t::CGimbal& getGimbal() override { return m_gimbal; } virtual bool manipulate(std::span virtualEvents, const hlsl::float64_t4x4* referenceFrame = nullptr) override { - if (not virtualEvents.size() and not referenceFrame) + if (virtualEvents.empty() && !referenceFrame) return false; PathState nextPathState = m_pathState; + CReferenceTransform reference = {}; + const CReferenceTransform* resolvedReference = nullptr; if (referenceFrame) { - CReferenceTransform reference = {}; if (!m_gimbal.extractReferenceTransform(&reference, referenceFrame)) return false; - if (!CCameraPathUtilities::tryResolvePathState(m_targetPosition, hlsl::float64_t3(reference.frame[3]), SCameraPathDefaults::Limits, nullptr, nextPathState)) + resolvedReference = &reference; + if (!m_pathModel.resolveState || + !m_pathModel.resolveState( + m_targetPosition, + hlsl::float64_t3(reference.frame[3]), + SCameraPathDefaults::Limits, + nullptr, + nextPathState)) + { return false; + } } const auto impulse = m_gimbal.accumulate(virtualEvents); - const auto stateDelta = CCameraPathUtilities::makePathDeltaFromVirtualPathTranslate(scaleVirtualTranslation(impulse.dVirtualTranslate)); - if (!CCameraPathUtilities::tryApplyPathStateDelta(nextPathState, stateDelta, SCameraPathDefaults::Limits, nextPathState)) + const SCameraPathControlContext context = { + .currentState = nextPathState, + .translation = scaleVirtualTranslation(impulse.dVirtualTranslate), + .rotation = scaleVirtualRotation(impulse.dVirtualRotation), + .targetPosition = m_targetPosition, + .reference = resolvedReference, + .limits = SCameraPathDefaults::Limits + }; + + if (!m_pathModel.controlLaw || !m_pathModel.integrate) + return false; + + const auto stateDelta = m_pathModel.controlLaw(context); + if (!m_pathModel.integrate(nextPathState, stateDelta, SCameraPathDefaults::Limits, nextPathState)) return false; m_pathState = nextPathState; @@ -55,15 +92,20 @@ class CPathCamera final : public CSphericalTargetCamera virtual uint32_t getAllowedVirtualEvents() const override { return AllowedVirtualEvents; } virtual CameraKind getKind() const override { return CameraKind::Path; } virtual uint32_t getGoalStateMask() const override { return base_t::getGoalStateMask() | base_t::GoalStatePath; } + virtual bool tryGetPathState(PathState& out) const override { out = m_pathState; return true; } + virtual bool trySetPathState(const PathState& state) override { - auto sanitized = state; - if (!CCameraPathUtilities::sanitizePathState(sanitized, SCameraPathDefaults::Limits.minRadius)) + if (!m_pathModel.resolveState) + return false; + + PathState sanitized = {}; + if (!m_pathModel.resolveState(m_targetPosition, m_gimbal.getPosition(), SCameraPathDefaults::Limits, &state, sanitized)) return false; const bool exact = CCameraPathUtilities::pathStatesNearlyEqual(sanitized, state, SCameraPathDefaults::ExactComparisonThresholds); @@ -71,29 +113,56 @@ class CPathCamera final : public CSphericalTargetCamera updateFromPathState(); return exact; } + virtual bool trySetSphericalDistance(float distance) override { SCameraPathDistanceUpdateResult distanceUpdate = {}; - if (!CCameraPathUtilities::tryUpdatePathStateDistance(distance, SCameraPathDefaults::Limits, m_pathState, &distanceUpdate)) + if (!m_pathModel.updateDistance || + !m_pathModel.updateDistance(distance, SCameraPathDefaults::Limits, m_pathState, &distanceUpdate)) + { return false; + } updateFromPathState(); return distanceUpdate.exact; } + virtual std::string_view getIdentifier() const override { return SCameraPathDefaults::Identifier; } + inline const path_model_t& getPathModel() const + { + return m_pathModel; + } + + inline bool setPathModel(path_model_t pathModel) + { + if (!pathModel.resolveState || !pathModel.controlLaw || !pathModel.integrate || !pathModel.evaluate || !pathModel.updateDistance) + return false; + + PathState sanitized = {}; + if (!pathModel.resolveState(m_targetPosition, m_gimbal.getPosition(), SCameraPathDefaults::Limits, &m_pathState, sanitized)) + return false; + + m_pathModel = std::move(pathModel); + m_pathState = sanitized; + return updateFromPathState(); + } + private: - static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate; + static inline constexpr auto AllowedVirtualEvents = + CVirtualGimbalEvent::Translate | CVirtualGimbalEvent::RollLeft | CVirtualGimbalEvent::RollRight; - PathState m_pathState = CCameraPathUtilities::makeDefaultPathState(SCameraPathDefaults::Limits.minRadius); + path_model_t m_pathModel = CCameraPathUtilities::makeDefaultPathModel(); + PathState m_pathState = CCameraPathUtilities::makeDefaultPathState(SCameraPathDefaults::Limits.minU); bool updateFromPathState() { + if (!m_pathModel.evaluate) + return false; + SCameraCanonicalPathState canonicalPathState = {}; - if (!CCameraPathUtilities::tryBuildCanonicalPathState(m_targetPosition, m_pathState, SCameraPathDefaults::Limits, canonicalPathState)) - { + if (!m_pathModel.evaluate(m_targetPosition, m_pathState, SCameraPathDefaults::Limits, canonicalPathState)) return false; - } m_distance = canonicalPathState.targetRelative.distance; m_orbitUv = canonicalPathState.targetRelative.orbitUv; diff --git a/common/include/camera/ICamera.hpp b/common/include/camera/ICamera.hpp index 4c119cb4a..2f3662016 100644 --- a/common/include/camera/ICamera.hpp +++ b/common/include/camera/ICamera.hpp @@ -86,24 +86,46 @@ class ICamera : virtual public core::IReferenceCounted float referenceDistance = 0.f; }; - //! Target-relative cylindrical rig state used by the `Path Rig` camera kind. + //! Parametric path-rig state used by the `Path Rig` camera kind. + //! + //! The default shared model interprets `(s, u, v, roll)` as angular progress, + //! radial component, vertical component, and view-axis roll around a target. + //! Concrete path models may reuse the same coordinates differently, but the + //! hot runtime contract still stays event-only through `manipulate(...)`. struct PathState { - double angle = 0.0; - double radius = 0.0; - double height = 0.0; + double s = 0.0; + double u = 0.0; + double v = 0.0; + double roll = 0.0; - inline hlsl::float64_t3 asVector() const + inline hlsl::float64_t4 asVector() const { - return hlsl::float64_t3(radius, height, angle); + return hlsl::float64_t4(s, u, v, roll); } - static inline PathState fromVector(const hlsl::float64_t3& value) + inline hlsl::float64_t3 asTranslationVector() const + { + return hlsl::float64_t3(u, v, s); + } + + static inline PathState fromVector(const hlsl::float64_t4& value) + { + return { + .s = value.x, + .u = value.y, + .v = value.z, + .roll = value.w + }; + } + + static inline PathState fromTranslationVector(const hlsl::float64_t3& value, const double pathRoll = 0.0) { return { - .angle = value.z, - .radius = value.x, - .height = value.y + .s = value.z, + .u = value.x, + .v = value.y, + .roll = pathRoll }; } }; diff --git a/common/include/camera/README.md b/common/include/camera/README.md index 3271c3e45..7b76c94a4 100644 --- a/common/include/camera/README.md +++ b/common/include/camera/README.md @@ -84,7 +84,7 @@ It also keeps camera semantics inside the camera type: - `Orbit` means orbit - `FPS` means FPS -- `Path Rig` means target-relative cylindrical path rig +- `Path Rig` means a path-model camera with typed `s/u/v/roll` state instead of allowing every caller to overwrite camera internals arbitrarily. @@ -369,7 +369,7 @@ That is why: - `Chase` and `Dolly` currently stay on shared spherical state - `DollyZoom` has dynamic perspective state -- `Path Rig` has target-relative cylindrical path state +- `Path Rig` has typed `PathState` and the shared default model interprets it as target-relative `s/u/v/roll` ## Follow model diff --git a/common/src/nbl/examples/camera/CCameraJsonPersistenceUtilities.hpp b/common/src/nbl/examples/camera/CCameraJsonPersistenceUtilities.hpp index 91da90a92..83d7973d6 100644 --- a/common/src/nbl/examples/camera/CCameraJsonPersistenceUtilities.hpp +++ b/common/src/nbl/examples/camera/CCameraJsonPersistenceUtilities.hpp @@ -59,11 +59,20 @@ inline void deserializeGoalJson(const Json& entry, core::CCameraGoal& goal) goal.orbitDistance = entry["orbit_distance"].get(); goal.hasOrbitState = true; } - if (entry.contains("path_angle") && entry.contains("path_radius") && entry.contains("path_height")) + if (entry.contains("path_s") && entry.contains("path_u") && entry.contains("path_v")) { - goal.pathState.angle = entry["path_angle"].get(); - goal.pathState.radius = entry["path_radius"].get(); - goal.pathState.height = entry["path_height"].get(); + goal.pathState.s = entry["path_s"].get(); + goal.pathState.u = entry["path_u"].get(); + goal.pathState.v = entry["path_v"].get(); + goal.pathState.roll = entry.contains("path_roll") ? entry["path_roll"].get() : 0.0; + goal.hasPathState = true; + } + else if (entry.contains("path_angle") && entry.contains("path_radius") && entry.contains("path_height")) + { + goal.pathState.s = entry["path_angle"].get(); + goal.pathState.u = entry["path_radius"].get(); + goal.pathState.v = entry["path_height"].get(); + goal.pathState.roll = entry.contains("path_roll") ? entry["path_roll"].get() : 0.0; goal.hasPathState = true; } if (entry.contains("dynamic_base_fov")) diff --git a/common/src/nbl/examples/camera/CCameraPersistence.cpp b/common/src/nbl/examples/camera/CCameraPersistence.cpp index b253e14b1..a86ebdc7c 100644 --- a/common/src/nbl/examples/camera/CCameraPersistence.cpp +++ b/common/src/nbl/examples/camera/CCameraPersistence.cpp @@ -40,9 +40,10 @@ json_t serializeGoalJson(const nbl::core::CCameraGoal& goal) } if (goal.hasPathState) { - json["path_angle"] = goal.pathState.angle; - json["path_radius"] = goal.pathState.radius; - json["path_height"] = goal.pathState.height; + json["path_s"] = goal.pathState.s; + json["path_u"] = goal.pathState.u; + json["path_v"] = goal.pathState.v; + json["path_roll"] = goal.pathState.roll; } if (goal.hasDynamicPerspectiveState) { diff --git a/common/src/nbl/examples/camera/CCameraScriptedRuntimePersistence.cpp b/common/src/nbl/examples/camera/CCameraScriptedRuntimePersistence.cpp index 06835b45b..15f7f4ad1 100644 --- a/common/src/nbl/examples/camera/CCameraScriptedRuntimePersistence.cpp +++ b/common/src/nbl/examples/camera/CCameraScriptedRuntimePersistence.cpp @@ -185,20 +185,40 @@ bool deserializeSequenceGoalDeltaJson(const json_t& root, nbl::core::CCameraSequ out.orbitDistanceDelta = root["orbit_distance_delta"].get(); out.hasOrbitDistanceDelta = true; } - if (root.contains("path_angle_delta_deg")) + if (root.contains("path_s_delta_deg")) { - out.pathAngleDeltaDeg = root["path_angle_delta_deg"].get(); - out.hasPathAngleDeltaDeg = true; + out.pathSDeltaDeg = root["path_s_delta_deg"].get(); + out.hasPathSDeltaDeg = true; } - if (root.contains("path_radius_delta")) + else if (root.contains("path_angle_delta_deg")) { - out.pathRadiusDelta = root["path_radius_delta"].get(); - out.hasPathRadiusDelta = true; + out.pathSDeltaDeg = root["path_angle_delta_deg"].get(); + out.hasPathSDeltaDeg = true; } - if (root.contains("path_height_delta")) + if (root.contains("path_u_delta")) { - out.pathHeightDelta = root["path_height_delta"].get(); - out.hasPathHeightDelta = true; + out.pathUDelta = root["path_u_delta"].get(); + out.hasPathUDelta = true; + } + else if (root.contains("path_radius_delta")) + { + out.pathUDelta = root["path_radius_delta"].get(); + out.hasPathUDelta = true; + } + if (root.contains("path_v_delta")) + { + out.pathVDelta = root["path_v_delta"].get(); + out.hasPathVDelta = true; + } + else if (root.contains("path_height_delta")) + { + out.pathVDelta = root["path_height_delta"].get(); + out.hasPathVDelta = true; + } + if (root.contains("path_roll_delta_deg")) + { + out.pathRollDeltaDeg = root["path_roll_delta_deg"].get(); + out.hasPathRollDeltaDeg = true; } if (root.contains("dynamic_base_fov_delta")) { @@ -241,8 +261,10 @@ bool deserializeSequenceKeyframeJson(const json_t& root, nbl::core::CCameraSeque } else if (root.contains("position") || root.contains("orientation") || root.contains("target_position") || root.contains("distance") || root.contains("orbit_u") || root.contains("orbit_v") || - root.contains("orbit_distance") || root.contains("path_angle") || root.contains("path_radius") || - root.contains("path_height") || root.contains("dynamic_base_fov") || root.contains("dynamic_reference_distance")) + root.contains("orbit_distance") || root.contains("path_s") || root.contains("path_u") || + root.contains("path_v") || root.contains("path_roll") || + root.contains("path_angle") || root.contains("path_radius") || root.contains("path_height") || + root.contains("dynamic_base_fov") || root.contains("dynamic_reference_distance")) { nbl::system::deserializePresetJson(root, out.absolutePreset); out.hasAbsolutePreset = true; From eb6422dbcc23ddec9a35e9c200d544cbbf1552cc Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Thu, 9 Apr 2026 14:40:51 +0200 Subject: [PATCH 193/205] Finalize path camera cleanup --- 61_UI/AppHeadlessCameraSmokeChecks.inl | 4 +- 61_UI/app_resources/cameraz_continuity.json | 12 +-- .../include/camera/CCameraKindUtilities.hpp | 7 +- common/include/camera/CCameraPathMetadata.hpp | 22 +++++ .../include/camera/CCameraPathUtilities.hpp | 5 +- .../camera/CCameraScriptedCheckRunner.hpp | 1 + .../include/camera/CCameraSequenceScript.hpp | 80 ++++++++++++++----- .../camera/CCameraTargetRelativeUtilities.hpp | 8 +- common/include/camera/SCameraRigPose.hpp | 20 +++++ .../CCameraJsonPersistenceUtilities.hpp | 8 -- .../CCameraScriptedRuntimePersistence.cpp | 28 +------ 11 files changed, 124 insertions(+), 71 deletions(-) create mode 100644 common/include/camera/CCameraPathMetadata.hpp create mode 100644 common/include/camera/SCameraRigPose.hpp diff --git a/61_UI/AppHeadlessCameraSmokeChecks.inl b/61_UI/AppHeadlessCameraSmokeChecks.inl index 4d4363158..3da205079 100644 --- a/61_UI/AppHeadlessCameraSmokeChecks.inl +++ b/61_UI/AppHeadlessCameraSmokeChecks.inl @@ -904,8 +904,8 @@ outError = "Camera text utilities smoke failed for Dolly Zoom label."; return false; } - if (CCameraTextUtilities::getCameraTypeDescription(ICamera::CameraKind::Path) != std::string(nbl::core::SCameraPathDefaults::Description)) - { + if (CCameraTextUtilities::getCameraTypeDescription(ICamera::CameraKind::Path) != std::string(nbl::core::SCameraPathRigMetadata::KindDescription)) + { outError = "Camera text utilities smoke failed for Path description."; return false; } diff --git a/61_UI/app_resources/cameraz_continuity.json b/61_UI/app_resources/cameraz_continuity.json index 18b98e12e..f0448862a 100644 --- a/61_UI/app_resources/cameraz_continuity.json +++ b/61_UI/app_resources/cameraz_continuity.json @@ -532,17 +532,17 @@ { "time": 1.5, "delta": { - "path_angle_delta_deg": 42.0, - "path_radius_delta": -3.5, - "path_height_delta": 1.0 + "path_s_delta_deg": 42.0, + "path_u_delta": -3.5, + "path_v_delta": 1.0 } }, { "time": 4.0, "delta": { - "path_angle_delta_deg": 115.0, - "path_radius_delta": -8.0, - "path_height_delta": 2.2 + "path_s_delta_deg": 115.0, + "path_u_delta": -8.0, + "path_v_delta": 2.2 } } ], diff --git a/common/include/camera/CCameraKindUtilities.hpp b/common/include/camera/CCameraKindUtilities.hpp index 57c6814e5..114d4fbfa 100644 --- a/common/include/camera/CCameraKindUtilities.hpp +++ b/common/include/camera/CCameraKindUtilities.hpp @@ -4,7 +4,8 @@ #include #include -#include "CCameraPathUtilities.hpp" +#include "CCameraPathMetadata.hpp" +#include "ICamera.hpp" namespace nbl::core { @@ -127,8 +128,8 @@ struct CCameraKindUtilities final }, { .kind = ICamera::CameraKind::Path, - .label = "Path Rig", - .description = SCameraPathDefaults::Description, + .label = SCameraPathRigMetadata::KindLabel, + .description = SCameraPathRigMetadata::KindDescription, .interactionFamily = ECameraInteractionFamily::Path } }}; diff --git a/common/include/camera/CCameraPathMetadata.hpp b/common/include/camera/CCameraPathMetadata.hpp new file mode 100644 index 000000000..258037c83 --- /dev/null +++ b/common/include/camera/CCameraPathMetadata.hpp @@ -0,0 +1,22 @@ +// This file is part of the "Nabla Engine". +// For conditions of distribution and use, see copyright notice in nabla.h + +#ifndef _C_CAMERA_PATH_METADATA_HPP_ +#define _C_CAMERA_PATH_METADATA_HPP_ + +#include + +namespace nbl::core +{ + +struct SCameraPathRigMetadata final +{ + static inline constexpr std::string_view KindLabel = "Path Rig"; + static inline constexpr std::string_view KindDescription = "Path-model camera with typed s/u/v/roll state"; + static inline constexpr std::string_view Identifier = "Target-relative Path Rig"; + static inline constexpr std::string_view DefaultModelDescription = "Adjust a target-relative path rig with s/u/v/roll state"; +}; + +} // namespace nbl::core + +#endif // _C_CAMERA_PATH_METADATA_HPP_ diff --git a/common/include/camera/CCameraPathUtilities.hpp b/common/include/camera/CCameraPathUtilities.hpp index 6f20f828c..cd513bc8f 100644 --- a/common/include/camera/CCameraPathUtilities.hpp +++ b/common/include/camera/CCameraPathUtilities.hpp @@ -7,6 +7,7 @@ #include #include +#include "CCameraPathMetadata.hpp" #include "CCameraTargetRelativeUtilities.hpp" #include "CCameraVirtualEventUtilities.hpp" #include "ICamera.hpp" @@ -87,8 +88,8 @@ struct SCameraPathDefaults final static constexpr double ExactStateTolerance = ICamera::TinyScalarEpsilon; static constexpr double ExactAngleToleranceDeg = ExactStateTolerance * 180.0 / hlsl::numbers::pi; static constexpr double AngleToleranceDeg = ICamera::DefaultAngularToleranceDeg; - static inline constexpr std::string_view Identifier = "Target-relative Path Rig"; - static inline constexpr std::string_view Description = "Adjust a target-relative path rig with s/u/v/roll state"; + static inline constexpr std::string_view Identifier = SCameraPathRigMetadata::Identifier; + static inline constexpr std::string_view Description = SCameraPathRigMetadata::DefaultModelDescription; struct SLimits final { diff --git a/common/include/camera/CCameraScriptedCheckRunner.hpp b/common/include/camera/CCameraScriptedCheckRunner.hpp index f46aae882..8fb99a091 100644 --- a/common/include/camera/CCameraScriptedCheckRunner.hpp +++ b/common/include/camera/CCameraScriptedCheckRunner.hpp @@ -15,6 +15,7 @@ #include "CCameraFollowRegressionUtilities.hpp" #include "CCameraScriptedRuntime.hpp" +#include "SCameraRigPose.hpp" namespace nbl::system { diff --git a/common/include/camera/CCameraSequenceScript.hpp b/common/include/camera/CCameraSequenceScript.hpp index 3be290afe..640a2d291 100644 --- a/common/include/camera/CCameraSequenceScript.hpp +++ b/common/include/camera/CCameraSequenceScript.hpp @@ -14,6 +14,7 @@ #include "CCameraMathUtilities.hpp" #include "CCameraKeyframeTrack.hpp" +#include "CCameraPathUtilities.hpp" #include "CCameraTargetRelativeUtilities.hpp" #include "IPlanarProjection.hpp" @@ -66,6 +67,58 @@ struct CCameraSequenceContinuitySettings //! Deltas stay camera-domain and avoid binding the authored file to any specific input device or consumer. struct CCameraSequenceGoalDelta { + struct SPathDelta final + { + SCameraPathDelta value = {}; + bool hasS = false; + bool hasU = false; + bool hasV = false; + bool hasRoll = false; + + inline bool hasAny() const + { + return hasS || hasU || hasV || hasRoll; + } + + inline void setSDeltaDeg(const double valueDeg) + { + value.s = static_cast(hlsl::radians(valueDeg)); + hasS = true; + } + + inline void setUDelta(const double valueScalar) + { + value.u = static_cast(valueScalar); + hasU = true; + } + + inline void setVDelta(const double valueScalar) + { + value.v = static_cast(valueScalar); + hasV = true; + } + + inline void setRollDeltaDeg(const double valueDeg) + { + value.roll = static_cast(hlsl::radians(valueDeg)); + hasRoll = true; + } + + inline SCameraPathDelta buildAppliedDelta() const + { + SCameraPathDelta delta = {}; + if (hasS) + delta.s = value.s; + if (hasU) + delta.u = value.u; + if (hasV) + delta.v = value.v; + if (hasRoll) + delta.roll = value.roll; + return delta; + } + }; + bool hasPositionOffset = false; hlsl::float64_t3 positionOffset = hlsl::float64_t3(0.0); @@ -82,17 +135,7 @@ struct CCameraSequenceGoalDelta bool hasOrbitDistanceDelta = false; float orbitDistanceDelta = 0.f; - bool hasPathSDeltaDeg = false; - double pathSDeltaDeg = 0.0; - - bool hasPathUDelta = false; - double pathUDelta = 0.0; - - bool hasPathVDelta = false; - double pathVDelta = 0.0; - - bool hasPathRollDeltaDeg = false; - double pathRollDeltaDeg = 0.0; + SPathDelta pathDelta = {}; bool hasDynamicBaseFovDelta = false; float dynamicBaseFovDelta = 0.f; @@ -317,7 +360,7 @@ struct CCameraSequenceScriptUtilities final const bool hasPoseDelta = delta.hasPositionOffset || delta.hasRotationEulerDegOffset; const bool hasSphericalDelta = delta.hasTargetOffset || delta.hasOrbitUDeltaDeg || delta.hasOrbitVDeltaDeg || delta.hasOrbitDistanceDelta; - const bool hasPathDelta = delta.hasPathSDeltaDeg || delta.hasPathUDelta || delta.hasPathVDelta || delta.hasPathRollDeltaDeg; + const bool hasPathDelta = delta.pathDelta.hasAny(); if (hasPoseDelta && (hasSphericalDelta || hasPathDelta)) { @@ -370,7 +413,7 @@ struct CCameraSequenceScriptUtilities final goal.orbitDistance += delta.orbitDistanceDelta; } - if (delta.hasPathSDeltaDeg || delta.hasPathUDelta || delta.hasPathVDelta || delta.hasPathRollDeltaDeg) + if (delta.pathDelta.hasAny()) { if (!goal.hasPathState) { @@ -379,12 +422,11 @@ struct CCameraSequenceScriptUtilities final return false; } - SCameraPathDelta pathDelta = {}; - pathDelta.s = delta.hasPathSDeltaDeg ? static_cast(hlsl::radians(delta.pathSDeltaDeg)) : hlsl::float64_t(0.0); - pathDelta.u = delta.hasPathUDelta ? static_cast(delta.pathUDelta) : hlsl::float64_t(0.0); - pathDelta.v = delta.hasPathVDelta ? static_cast(delta.pathVDelta) : hlsl::float64_t(0.0); - pathDelta.roll = delta.hasPathRollDeltaDeg ? static_cast(hlsl::radians(delta.pathRollDeltaDeg)) : hlsl::float64_t(0.0); - if (!CCameraPathUtilities::tryApplyPathStateDelta(goal.pathState, pathDelta, CCameraPathUtilities::makeDefaultPathLimits(), goal.pathState)) + if (!CCameraPathUtilities::tryApplyPathStateDelta( + goal.pathState, + delta.pathDelta.buildAppliedDelta(), + CCameraPathUtilities::makeDefaultPathLimits(), + goal.pathState)) { if (error) *error = "Sequence keyframe path deltas produced an invalid path state."; diff --git a/common/include/camera/CCameraTargetRelativeUtilities.hpp b/common/include/camera/CCameraTargetRelativeUtilities.hpp index d1656fdca..ee914d5a7 100644 --- a/common/include/camera/CCameraTargetRelativeUtilities.hpp +++ b/common/include/camera/CCameraTargetRelativeUtilities.hpp @@ -3,6 +3,7 @@ #include +#include "SCameraRigPose.hpp" #include "CCameraVirtualEventUtilities.hpp" namespace nbl::core @@ -16,13 +17,6 @@ struct SCameraTargetRelativeState final float distance = ICamera::SphericalMinDistance; }; -//! Shared reconstructed camera pose used by target-relative and path rigs. -struct SCameraRigPose -{ - hlsl::float64_t3 position = hlsl::float64_t3(0.0); - hlsl::camera_quaternion_t orientation = hlsl::CCameraMathUtilities::makeIdentityQuaternion(); -}; - //! Pose reconstructed from a target-relative orbit state. struct SCameraTargetRelativePose final : SCameraRigPose { diff --git a/common/include/camera/SCameraRigPose.hpp b/common/include/camera/SCameraRigPose.hpp new file mode 100644 index 000000000..31eeb0282 --- /dev/null +++ b/common/include/camera/SCameraRigPose.hpp @@ -0,0 +1,20 @@ +// This file is part of the "Nabla Engine". +// For conditions of distribution and use, see copyright notice in nabla.h + +#ifndef _S_CAMERA_RIG_POSE_HPP_ +#define _S_CAMERA_RIG_POSE_HPP_ + +#include "CCameraMathUtilities.hpp" + +namespace nbl::core +{ + +struct SCameraRigPose +{ + hlsl::float64_t3 position = hlsl::float64_t3(0.0); + hlsl::camera_quaternion_t orientation = hlsl::CCameraMathUtilities::makeIdentityQuaternion(); +}; + +} // namespace nbl::core + +#endif // _S_CAMERA_RIG_POSE_HPP_ diff --git a/common/src/nbl/examples/camera/CCameraJsonPersistenceUtilities.hpp b/common/src/nbl/examples/camera/CCameraJsonPersistenceUtilities.hpp index 83d7973d6..36f09fd82 100644 --- a/common/src/nbl/examples/camera/CCameraJsonPersistenceUtilities.hpp +++ b/common/src/nbl/examples/camera/CCameraJsonPersistenceUtilities.hpp @@ -67,14 +67,6 @@ inline void deserializeGoalJson(const Json& entry, core::CCameraGoal& goal) goal.pathState.roll = entry.contains("path_roll") ? entry["path_roll"].get() : 0.0; goal.hasPathState = true; } - else if (entry.contains("path_angle") && entry.contains("path_radius") && entry.contains("path_height")) - { - goal.pathState.s = entry["path_angle"].get(); - goal.pathState.u = entry["path_radius"].get(); - goal.pathState.v = entry["path_height"].get(); - goal.pathState.roll = entry.contains("path_roll") ? entry["path_roll"].get() : 0.0; - goal.hasPathState = true; - } if (entry.contains("dynamic_base_fov")) { goal.dynamicPerspectiveState.baseFov = entry["dynamic_base_fov"].get(); diff --git a/common/src/nbl/examples/camera/CCameraScriptedRuntimePersistence.cpp b/common/src/nbl/examples/camera/CCameraScriptedRuntimePersistence.cpp index 15f7f4ad1..85924271b 100644 --- a/common/src/nbl/examples/camera/CCameraScriptedRuntimePersistence.cpp +++ b/common/src/nbl/examples/camera/CCameraScriptedRuntimePersistence.cpp @@ -187,38 +187,19 @@ bool deserializeSequenceGoalDeltaJson(const json_t& root, nbl::core::CCameraSequ } if (root.contains("path_s_delta_deg")) { - out.pathSDeltaDeg = root["path_s_delta_deg"].get(); - out.hasPathSDeltaDeg = true; - } - else if (root.contains("path_angle_delta_deg")) - { - out.pathSDeltaDeg = root["path_angle_delta_deg"].get(); - out.hasPathSDeltaDeg = true; + out.pathDelta.setSDeltaDeg(root["path_s_delta_deg"].get()); } if (root.contains("path_u_delta")) { - out.pathUDelta = root["path_u_delta"].get(); - out.hasPathUDelta = true; - } - else if (root.contains("path_radius_delta")) - { - out.pathUDelta = root["path_radius_delta"].get(); - out.hasPathUDelta = true; + out.pathDelta.setUDelta(root["path_u_delta"].get()); } if (root.contains("path_v_delta")) { - out.pathVDelta = root["path_v_delta"].get(); - out.hasPathVDelta = true; - } - else if (root.contains("path_height_delta")) - { - out.pathVDelta = root["path_height_delta"].get(); - out.hasPathVDelta = true; + out.pathDelta.setVDelta(root["path_v_delta"].get()); } if (root.contains("path_roll_delta_deg")) { - out.pathRollDeltaDeg = root["path_roll_delta_deg"].get(); - out.hasPathRollDeltaDeg = true; + out.pathDelta.setRollDeltaDeg(root["path_roll_delta_deg"].get()); } if (root.contains("dynamic_base_fov_delta")) { @@ -263,7 +244,6 @@ bool deserializeSequenceKeyframeJson(const json_t& root, nbl::core::CCameraSeque root.contains("distance") || root.contains("orbit_u") || root.contains("orbit_v") || root.contains("orbit_distance") || root.contains("path_s") || root.contains("path_u") || root.contains("path_v") || root.contains("path_roll") || - root.contains("path_angle") || root.contains("path_radius") || root.contains("path_height") || root.contains("dynamic_base_fov") || root.contains("dynamic_reference_distance")) { nbl::system::deserializePresetJson(root, out.absolutePreset); From ada1b28af263548b3fdb929f06bdbf6414d3f329 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Thu, 9 Apr 2026 15:32:58 +0200 Subject: [PATCH 194/205] Normalize camera API doxygen docs --- 61_UI/AppHeadlessCameraSmokeChecks.inl | 135 +++++++++++- 61_UI/AppHeadlessCameraSmokeHelpers.inl | 4 +- 61_UI/README.md | 4 +- 61_UI/include/app/App.hpp | 4 +- .../camera/CCameraControlPanelUiUtilities.hpp | 2 +- .../CCameraFollowRegressionUtilities.hpp | 30 ++- .../include/camera/CCameraFollowUtilities.hpp | 36 ++-- common/include/camera/CCameraGoal.hpp | 4 +- common/include/camera/CCameraGoalAnalysis.hpp | 4 +- common/include/camera/CCameraGoalSolver.hpp | 20 +- .../camera/CCameraInputBindingUtilities.hpp | 6 +- .../include/camera/CCameraKeyframeTrack.hpp | 8 +- .../CCameraKeyframeTrackPersistence.hpp | 8 +- .../include/camera/CCameraKindUtilities.hpp | 4 +- .../camera/CCameraManipulationUtilities.hpp | 10 +- .../include/camera/CCameraMathUtilities.hpp | 2 +- .../include/camera/CCameraPathUtilities.hpp | 91 ++++++-- common/include/camera/CCameraPersistence.hpp | 8 +- .../camera/CCameraPlaybackTimeline.hpp | 16 +- .../camera/CCameraPresentationUtilities.hpp | 18 +- common/include/camera/CCameraPreset.hpp | 8 +- common/include/camera/CCameraPresetFlow.hpp | 20 +- .../camera/CCameraPresetPersistence.hpp | 16 +- .../camera/CCameraProjectionUtilities.hpp | 2 +- ...ameraScriptVisualDebugOverlayUtilities.hpp | 6 +- .../camera/CCameraScriptedCheckRunner.hpp | 28 ++- .../include/camera/CCameraScriptedRuntime.hpp | 26 +-- .../CCameraScriptedRuntimePersistence.hpp | 10 +- .../include/camera/CCameraSequenceScript.hpp | 148 +++++++------ .../CCameraSequenceScriptPersistence.hpp | 6 +- .../camera/CCameraSequenceScriptedBuilder.hpp | 22 +- .../CCameraSmokeRegressionUtilities.hpp | 4 +- .../camera/CCameraTargetRelativeUtilities.hpp | 10 +- .../include/camera/CCameraTextUtilities.hpp | 24 +-- .../CCameraViewportOverlayUtilities.hpp | 14 +- common/include/camera/CCubeProjection.hpp | 27 ++- common/include/camera/CFPSCamera.hpp | 2 +- common/include/camera/CFreeLockCamera.hpp | 2 +- common/include/camera/CGimbalInputBinder.hpp | 14 +- common/include/camera/CPathCamera.hpp | 198 +++++++++++++++--- .../include/camera/CSphericalTargetCamera.hpp | 8 +- common/include/camera/CVirtualGimbalEvent.hpp | 10 +- common/include/camera/ICamera.hpp | 44 ++-- common/include/camera/IGimbal.hpp | 40 ++-- .../include/camera/IGimbalBindingLayout.hpp | 10 +- .../include/camera/IGimbalInputProcessor.hpp | 128 ++++------- common/include/camera/ILinearProjection.hpp | 69 +++--- .../include/camera/IPerspectiveProjection.hpp | 30 ++- common/include/camera/IPlanarProjection.hpp | 12 +- common/include/camera/IProjection.hpp | 51 ++--- common/include/camera/IRange.hpp | 2 +- common/include/camera/README.md | 14 +- .../CCameraScriptedRuntimePersistence.cpp | 9 +- 53 files changed, 844 insertions(+), 584 deletions(-) diff --git a/61_UI/AppHeadlessCameraSmokeChecks.inl b/61_UI/AppHeadlessCameraSmokeChecks.inl index 3da205079..169452ee7 100644 --- a/61_UI/AppHeadlessCameraSmokeChecks.inl +++ b/61_UI/AppHeadlessCameraSmokeChecks.inl @@ -642,6 +642,8 @@ 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))); @@ -649,7 +651,7 @@ if (!nbl::core::CCameraPathUtilities::tryApplyPathStateDelta( baselinePathState, expectedPathDelta, - nbl::core::CCameraPathUtilities::makeDefaultPathLimits(), + activePathLimits, expectedPathState) || !nbl::core::CCameraPathUtilities::pathStatesNearlyEqual( manipulatedPathState, @@ -727,6 +729,137 @@ { 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; + } } { diff --git a/61_UI/AppHeadlessCameraSmokeHelpers.inl b/61_UI/AppHeadlessCameraSmokeHelpers.inl index 2d7cea23b..c5a87a898 100644 --- a/61_UI/AppHeadlessCameraSmokeHelpers.inl +++ b/61_UI/AppHeadlessCameraSmokeHelpers.inl @@ -578,7 +578,7 @@ namespace followResult, &followError)) { - outError = std::string("Follow smoke contract failed for ") + std::string(label) + ". " + followError; + outError = std::string("Follow smoke validation failed for ") + std::string(label) + ". " + followError; return false; } if (!verifyFollowVisualMetrics(planarSpan, camera, trackedTarget, followConfig, label.data(), &followError)) @@ -1040,7 +1040,7 @@ namespace return true; } - outError = std::string("Follow smoke contract failed for ") + std::string(label) + ". " + regressionError; + outError = std::string("Follow smoke validation failed for ") + std::string(label) + ". " + regressionError; return false; } diff --git a/61_UI/README.md b/61_UI/README.md index a515949aa..1465f50fc 100644 --- a/61_UI/README.md +++ b/61_UI/README.md @@ -20,7 +20,7 @@ Its job is to consume the shared camera APIs and expose them through a visible, ## What `61_UI` contributes locally -The reusable camera layer stops at shared camera-domain contracts. +The reusable camera layer stops at shared camera-domain interfaces. `61_UI` adds the local glue needed to turn that into an example application: - scene setup and demo geometry @@ -102,7 +102,7 @@ Manual runtime and scripted continuity both drive the same follow layer. Purpose: - validate basic camera selection and movement -- validate helper contracts in a small, cheap run +- validate helper behavior in a small, cheap run ### Continuity diff --git a/61_UI/include/app/App.hpp b/61_UI/include/app/App.hpp index 5bf26c550..8f97639ac 100644 --- a/61_UI/include/app/App.hpp +++ b/61_UI/include/app/App.hpp @@ -280,7 +280,7 @@ class App final : public examples::SimpleWindowedApplication, public examples::B std::chrono::seconds timeout = std::chrono::seconds(0x7fffFFFFu); clock_t::time_point start; - //! One window & surface + /// @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. @@ -306,7 +306,7 @@ class App final : public examples::SimpleWindowedApplication, public examples::B InputSystem::ChannelReader mouse; // Handles keyboard events InputSystem::ChannelReader keyboard; - //! next presentation timestamp + /// @brief Next presentation timestamp. std::chrono::microseconds m_nextPresentationTimestamp = {}; core::smart_refctd_ptr m_descriptorSetPool; diff --git a/common/include/camera/CCameraControlPanelUiUtilities.hpp b/common/include/camera/CCameraControlPanelUiUtilities.hpp index 385601d3d..f050a78d6 100644 --- a/common/include/camera/CCameraControlPanelUiUtilities.hpp +++ b/common/include/camera/CCameraControlPanelUiUtilities.hpp @@ -17,7 +17,7 @@ namespace nbl::ui { -//! Shared visual theme and layout constants for the control panel consumer UI. +/// @brief Shared visual theme and layout constants for the control panel consumer UI. struct SCameraControlPanelStyle final { static constexpr float MillisecondsPerSecond = 1000.0f; diff --git a/common/include/camera/CCameraFollowRegressionUtilities.hpp b/common/include/camera/CCameraFollowRegressionUtilities.hpp index b8c729e5b..210348ebf 100644 --- a/common/include/camera/CCameraFollowRegressionUtilities.hpp +++ b/common/include/camera/CCameraFollowRegressionUtilities.hpp @@ -18,16 +18,14 @@ struct SCameraProjectedTargetMetrics final float radius = 0.0f; }; -/** -* Reusable follow-contract validation helpers. -* -* The checks stay camera-domain: -* -* - camera-to-target direction must match the camera forward axis for locking modes -* - target distance must be finite and internally consistent -* - spherical cameras must write the tracked target back into spherical target state -* - spherical distance must match the goal-derived distance when present -*/ +/// @brief Reusable follow validation helpers. +/// +/// The checks stay camera-domain: +/// +/// - camera-to-target direction must match the camera forward axis for locking modes +/// - target distance must be finite and internally consistent +/// - spherical cameras must write the tracked target back into spherical target state +/// - spherical distance must match the goal-derived distance when present struct SCameraFollowRegressionResult { bool passed = false; @@ -41,7 +39,7 @@ struct SCameraFollowRegressionResult float sphericalDistance = 0.0f; }; -//! Reusable visual/debug metrics for one active follow configuration. +/// @brief Reusable visual/debug metrics for one active follow configuration. struct SCameraFollowVisualMetrics { bool active = false; @@ -53,14 +51,14 @@ struct SCameraFollowVisualMetrics SCameraProjectedTargetMetrics projectedTarget = {}; }; -//! Shared view/projection bundle for CPU-side projected target metrics. +/// @brief Shared view/projection bundle for CPU-side projected target metrics. struct SCameraProjectionContext { hlsl::float32_t4x4 viewMatrix = hlsl::float32_t4x4(1.0f); hlsl::float32_t4x4 projectionMatrix = hlsl::float32_t4x4(1.0f); }; -//! Shared tolerances for follow target lock, writeback, and projected-center checks. +/// @brief Shared tolerances for follow target lock, writeback, and projected-center checks. struct SCameraFollowRegressionThresholds { static inline constexpr float DefaultClipWEpsilon = 1e-5f; @@ -82,9 +80,9 @@ struct SCameraFollowRegressionThresholds double scalarTolerance = DefaultScalarTolerance; }; -//! Bundled reusable follow regression flow. -//! The helper builds a follow goal, applies it, verifies the resulting camera state, -//! and then checks the lock/writeback follow contract. +/// @brief Bundled reusable follow regression flow. +/// The helper builds a follow goal, applies it, verifies the resulting camera state, +/// and then checks lock/writeback follow consistency. struct SCameraFollowApplyValidationResult { bool hasGoal = false; diff --git a/common/include/camera/CCameraFollowUtilities.hpp b/common/include/camera/CCameraFollowUtilities.hpp index 1fed7a51b..bea2ad432 100644 --- a/common/include/camera/CCameraFollowUtilities.hpp +++ b/common/include/camera/CCameraFollowUtilities.hpp @@ -15,12 +15,10 @@ namespace nbl::core { -/** -* Reusable tracked-target and follow helpers layered on top of the shared camera API. -* -* The tracked subject owns its own gimbal. Follow stays outside `ICamera` and maps -* a camera plus tracked target into a `CCameraGoal`. -*/ +/// @brief Reusable tracked-target and follow helpers layered on top of the shared camera API. +/// +/// The tracked subject owns its own gimbal. Follow stays outside `ICamera` and maps +/// a camera plus tracked target into a `CCameraGoal`. class CTrackedTarget { public: @@ -75,18 +73,16 @@ class CTrackedTarget gimbal_t m_gimbal; }; -/** -* Follow policy layered on top of a tracked target gimbal. -* -* The modes are intentionally explicit because `follow` is not one behavior: -* -* - `OrbitTarget` keeps a target-relative orbit/path rig and re-centers the tracked target -* - `LookAtTarget` keeps the camera world position and only rotates the view toward the target -* - `KeepWorldOffset` keeps a world-space camera offset from the target and locks the view onto it -* - `KeepLocalOffset` keeps a target-local camera offset and locks the view onto it -* -* The tracked target remains the source of truth. The camera does not own the tracked subject. -*/ +/// @brief Follow policy layered on top of a tracked target gimbal. +/// +/// The modes are intentionally explicit because `follow` is not one behavior: +/// +/// - `OrbitTarget` keeps a target-relative orbit/path rig and re-centers the tracked target +/// - `LookAtTarget` keeps the camera world position and only rotates the view toward the target +/// - `KeepWorldOffset` keeps a world-space camera offset from the target and locks the view onto it +/// - `KeepLocalOffset` keeps a target-local camera offset and locks the view onto it +/// +/// The tracked target remains the source of truth. The camera does not own the tracked subject. enum class ECameraFollowMode : uint8_t { Disabled, @@ -96,8 +92,8 @@ enum class ECameraFollowMode : uint8_t KeepLocalOffset }; -//! Reusable follow configuration interpreted against a tracked target gimbal. -//! `worldOffset` and `localOffset` are only meaningful for their matching offset-based modes. +/// @brief Reusable follow configuration interpreted against a tracked target gimbal. +/// `worldOffset` and `localOffset` are only meaningful for their matching offset-based modes. struct SCameraFollowConfig { bool enabled = false; diff --git a/common/include/camera/CCameraGoal.hpp b/common/include/camera/CCameraGoal.hpp index 21e71f126..6c9da21ea 100644 --- a/common/include/camera/CCameraGoal.hpp +++ b/common/include/camera/CCameraGoal.hpp @@ -18,9 +18,7 @@ namespace nbl::core { -/** -* Typed transport object for camera state used by capture, comparison, presets, and playback. -*/ +/// @brief Typed transport object for camera state used by capture, comparison, presets, and playback. struct CCameraGoal : SCameraRigPose { ICamera::CameraKind sourceKind = ICamera::CameraKind::Unknown; diff --git a/common/include/camera/CCameraGoalAnalysis.hpp b/common/include/camera/CCameraGoalAnalysis.hpp index bc81e3439..4653cc318 100644 --- a/common/include/camera/CCameraGoalAnalysis.hpp +++ b/common/include/camera/CCameraGoalAnalysis.hpp @@ -11,7 +11,7 @@ namespace nbl::core { -//! Reusable typed answer for `goal/preset -> camera` compatibility checks. +/// @brief Reusable typed answer for `goal/preset -> camera` compatibility checks. struct SCameraGoalApplyAnalysis { CCameraGoal goal = {}; @@ -41,7 +41,7 @@ struct SCameraGoalApplyAnalysis } }; -//! Reusable typed answer for `camera -> goal` capture viability. +/// @brief Reusable typed answer for `camera -> goal` capture viability. struct SCameraCaptureAnalysis { CCameraGoal goal = {}; diff --git a/common/include/camera/CCameraGoalSolver.hpp b/common/include/camera/CCameraGoalSolver.hpp index 71f4c5f2f..b9a54aa9d 100644 --- a/common/include/camera/CCameraGoalSolver.hpp +++ b/common/include/camera/CCameraGoalSolver.hpp @@ -14,16 +14,14 @@ namespace nbl::core { -/** -* Best-effort absolute layer built on top of the event-only camera core. -* -* It captures typed camera state into `CCameraGoal`, analyzes compatibility, -* and tries to apply goals back to cameras using typed hooks and virtual-event replay. -*/ +/// @brief Best-effort absolute layer built on top of the event-only camera core. +/// +/// It captures typed camera state into `CCameraGoal`, analyzes compatibility, +/// and tries to apply goals back to cameras using typed hooks and virtual-event replay. class CCameraGoalSolver { public: - //! Detailed capture result for tooling code. + /// @brief Detailed capture result for tooling code. struct SCaptureResult { bool hasCamera = false; @@ -37,7 +35,7 @@ class CCameraGoalSolver } }; - //! Compatibility of a goal with a target camera kind and state mask. + /// @brief Compatibility of a goal with a target camera kind and state mask. struct SCompatibilityResult { bool sameKind = false; @@ -47,7 +45,7 @@ class CCameraGoalSolver uint32_t missingGoalStateMask = ICamera::GoalStateNone; }; - //! Outcome of a best-effort goal apply attempt. + /// @brief Outcome of a best-effort goal apply attempt. struct SApplyResult { enum class EStatus : uint8_t @@ -531,12 +529,14 @@ class CCameraGoalSolver const auto effectiveTarget = target.hasTargetPosition ? target.targetPosition : sphericalState.target; ICamera::PathState currentState = {}; const ICamera::PathState* currentStateOverride = camera->tryGetPathState(currentState) ? ¤tState : nullptr; + ICamera::PathStateLimits pathLimits = CCameraPathUtilities::makeDefaultPathLimits(); + camera->tryGetPathStateLimits(pathLimits); SCameraPathStateTransition transition = {}; if (!CCameraPathUtilities::tryBuildPathStateTransition( effectiveTarget, camera->getGimbal().getPosition(), target.position, - SCameraPathDefaults::Limits, + pathLimits, currentStateOverride, target.hasPathState ? &target.pathState : nullptr, transition)) diff --git a/common/include/camera/CCameraInputBindingUtilities.hpp b/common/include/camera/CCameraInputBindingUtilities.hpp index 727a888c0..3288d925c 100644 --- a/common/include/camera/CCameraInputBindingUtilities.hpp +++ b/common/include/camera/CCameraInputBindingUtilities.hpp @@ -11,7 +11,7 @@ namespace nbl::ui { -//! Reusable keyboard, mouse, and ImGuizmo binding preset grouped for one camera kind. +/// @brief Reusable keyboard, mouse, and ImGuizmo binding preset grouped for one camera kind. struct SCameraInputBindingPreset { IGimbalBindingLayout::keyboard_to_virtual_events_t keyboard; @@ -19,7 +19,7 @@ struct SCameraInputBindingPreset IGimbalBindingLayout::imguizmo_to_virtual_events_t imguizmo; }; -//! Shared physical input bundles reused by default presets and smoke probing. +/// @brief Shared physical input bundles reused by default presets and smoke probing. struct SCameraInputBindingPhysicalGroups final { static inline constexpr std::array KeyboardWasdCodes = { @@ -190,7 +190,7 @@ struct CCameraInputBindingUtilities final }; }; - //! Shared virtual-event bundles reused across interaction families. + /// @brief Shared virtual-event bundles reused across interaction families. struct SCameraInputBindingEventGroups final { static inline constexpr std::array FpsMove = { diff --git a/common/include/camera/CCameraKeyframeTrack.hpp b/common/include/camera/CCameraKeyframeTrack.hpp index 2bcefaae7..44894889c 100644 --- a/common/include/camera/CCameraKeyframeTrack.hpp +++ b/common/include/camera/CCameraKeyframeTrack.hpp @@ -14,7 +14,7 @@ namespace nbl::core { -//! Reusable keyframe container plus selection state for playback tooling. +/// @brief Reusable keyframe container plus selection state for playback tooling. struct CCameraKeyframeTrack { std::vector keyframes; @@ -24,7 +24,7 @@ struct CCameraKeyframeTrack struct CCameraKeyframeTrackUtilities final { public: - //! Compare two keyframes by authored time and shared preset state. + /// @brief Compare two keyframes by authored time and shared preset state. static inline bool compareKeyframes(const CCameraKeyframe& lhs, const CCameraKeyframe& rhs, const double timeEps, const double posEps, const double rotEpsDeg, const double scalarEps) { @@ -32,7 +32,7 @@ struct CCameraKeyframeTrackUtilities final CCameraPresetUtilities::comparePresets(lhs.preset, rhs.preset, posEps, rotEpsDeg, scalarEps); } - //! Compare two authored keyframe tracks with optional selection-state checking. + /// @brief Compare two authored keyframe tracks with optional selection-state checking. static inline bool compareKeyframeTracks(const CCameraKeyframeTrack& lhs, const CCameraKeyframeTrack& rhs, const double timeEps, const double posEps, const double rotEpsDeg, const double scalarEps, const bool compareSelection = true) { @@ -48,7 +48,7 @@ struct CCameraKeyframeTrackUtilities final return true; } - //! Compare only the serialized/authored content of two tracks and ignore transient UI selection state. + /// @brief Compare only the serialized/authored content of two tracks and ignore transient UI selection state. static inline bool compareKeyframeTrackContent(const CCameraKeyframeTrack& lhs, const CCameraKeyframeTrack& rhs, const double timeEps, const double posEps, const double rotEpsDeg, const double scalarEps) { diff --git a/common/include/camera/CCameraKeyframeTrackPersistence.hpp b/common/include/camera/CCameraKeyframeTrackPersistence.hpp index 963450c7d..7ce8a45cb 100644 --- a/common/include/camera/CCameraKeyframeTrackPersistence.hpp +++ b/common/include/camera/CCameraKeyframeTrackPersistence.hpp @@ -15,14 +15,14 @@ namespace nbl::system class ISystem; -//! Serialize one camera keyframe track into an existing stream. +/// @brief Serialize one camera keyframe track into an existing stream. bool writeKeyframeTrack(std::ostream& out, const core::CCameraKeyframeTrack& track, int indent = 2); -//! Deserialize one camera keyframe track from an existing stream. +/// @brief Deserialize one camera keyframe track from an existing stream. bool readKeyframeTrack(std::istream& in, core::CCameraKeyframeTrack& track); -//! Save one camera keyframe track to a file. +/// @brief Save one camera keyframe track to a file. bool saveKeyframeTrackToFile(ISystem& system, const path& path, const core::CCameraKeyframeTrack& track, int indent = 2); -//! Load one camera keyframe track from a file. +/// @brief Load one camera keyframe track from a file. bool loadKeyframeTrackFromFile(ISystem& system, const path& path, core::CCameraKeyframeTrack& track); } // namespace nbl::system diff --git a/common/include/camera/CCameraKindUtilities.hpp b/common/include/camera/CCameraKindUtilities.hpp index 114d4fbfa..e09fb6ce5 100644 --- a/common/include/camera/CCameraKindUtilities.hpp +++ b/common/include/camera/CCameraKindUtilities.hpp @@ -10,7 +10,7 @@ namespace nbl::core { -//! Interaction family used to group camera kinds with matching control semantics. +/// @brief Interaction family used to group camera kinds with matching control semantics. enum class ECameraInteractionFamily : uint8_t { None, @@ -23,7 +23,7 @@ enum class ECameraInteractionFamily : uint8_t Path }; -//! Shared metadata for one concrete `CameraKind`. +/// @brief Shared metadata for one concrete `CameraKind`. struct SCameraKindTraits final { ICamera::CameraKind kind = ICamera::CameraKind::Unknown; diff --git a/common/include/camera/CCameraManipulationUtilities.hpp b/common/include/camera/CCameraManipulationUtilities.hpp index ab2cbe84d..43b53a226 100644 --- a/common/include/camera/CCameraManipulationUtilities.hpp +++ b/common/include/camera/CCameraManipulationUtilities.hpp @@ -26,7 +26,7 @@ struct SCameraConstraintDefaults final static constexpr float MaxDistance = ICamera::SphericalMaxDistance; }; -//! Reusable constraint settings for post-manipulation camera clamping. +/// @brief Reusable constraint settings for post-manipulation camera clamping. struct SCameraConstraintSettings { bool enabled = false; @@ -47,7 +47,7 @@ struct SCameraConstraintSettings struct CCameraManipulationUtilities final { public: - //! Apply an authored world-space reference frame through the shared camera runtime entry point. + /// @brief Apply an authored world-space reference frame through the shared camera runtime entry point. static inline bool applyReferenceFrameToCamera(ICamera* camera, const hlsl::float64_t4x4& referenceFrame) { if (!camera) @@ -56,7 +56,7 @@ struct CCameraManipulationUtilities final return camera->manipulateWithUnitMotionScales({}, &referenceFrame); } - //! Scale translation and rotation event magnitudes without touching unrelated event types. + /// @brief Scale translation and rotation event magnitudes without touching unrelated event types. static inline void scaleVirtualEvents(std::vector& events, const uint32_t count, const float translationScale, const float rotationScale) { for (uint32_t i = 0u; i < count; ++i) @@ -73,7 +73,7 @@ struct CCameraManipulationUtilities final } } - //! Reinterpret world-space translation intents as local camera-space movement events. + /// @brief Reinterpret world-space translation intents as local camera-space movement events. static inline void remapTranslationEventsFromWorldToCameraLocal(ICamera* camera, std::vector& events, uint32_t& count) { if (!camera) @@ -103,7 +103,7 @@ struct CCameraManipulationUtilities final count = static_cast(events.size()); } - //! Apply shared distance and Euler-angle constraints after manipulation. + /// @brief Apply shared distance and Euler-angle constraints after manipulation. static inline bool applyCameraConstraints(const CCameraGoalSolver& solver, ICamera* camera, const SCameraConstraintSettings& constraints) { if (!constraints.enabled || !camera) diff --git a/common/include/camera/CCameraMathUtilities.hpp b/common/include/camera/CCameraMathUtilities.hpp index a346db24f..7e89d4b92 100644 --- a/common/include/camera/CCameraMathUtilities.hpp +++ b/common/include/camera/CCameraMathUtilities.hpp @@ -12,7 +12,7 @@ namespace nbl::hlsl { -//! Camera-oriented math aliases and helpers built on top of Nabla `nbl::hlsl` types. +/// @brief Camera-oriented math aliases and helpers built on top of Nabla `nbl::hlsl` types. template using camera_vector_t = vector; diff --git a/common/include/camera/CCameraPathUtilities.hpp b/common/include/camera/CCameraPathUtilities.hpp index cd513bc8f..58f5da789 100644 --- a/common/include/camera/CCameraPathUtilities.hpp +++ b/common/include/camera/CCameraPathUtilities.hpp @@ -15,7 +15,7 @@ namespace nbl::core { -//! Shared helpers for the reusable `PathRig` camera kind. +/// @brief Shared helpers for the reusable `PathRig` camera kind. struct SCameraPathPose final : SCameraRigPose { hlsl::float64_t appliedDistance = 0.0; @@ -90,15 +90,7 @@ struct SCameraPathDefaults final static constexpr double AngleToleranceDeg = ICamera::DefaultAngularToleranceDeg; static inline constexpr std::string_view Identifier = SCameraPathRigMetadata::Identifier; static inline constexpr std::string_view Description = SCameraPathRigMetadata::DefaultModelDescription; - - struct SLimits final - { - double minU = SCameraPathDefaults::MinU; - hlsl::float64_t minDistance = static_cast(ICamera::SphericalMinDistance); - hlsl::float64_t maxDistance = static_cast(ICamera::SphericalMaxDistance); - }; - - static inline constexpr SLimits Limits = {}; + static inline constexpr ICamera::PathStateLimits Limits = {}; static inline constexpr SCameraPathComparisonThresholds ComparisonThresholds = { .sToleranceDeg = AngleToleranceDeg, .rollToleranceDeg = AngleToleranceDeg, @@ -111,7 +103,7 @@ struct SCameraPathDefaults final }; }; -using SCameraPathLimits = SCameraPathDefaults::SLimits; +using SCameraPathLimits = ICamera::PathStateLimits; struct SCameraPathControlContext final { @@ -191,11 +183,54 @@ struct CCameraPathUtilities final hlsl::CCameraMathUtilities::isFiniteScalar(state.roll); } + static inline bool isPathLimitsFinite(const SCameraPathLimits& limits) + { + return hlsl::CCameraMathUtilities::isFiniteScalar(limits.minU) && + hlsl::CCameraMathUtilities::isFiniteScalar(limits.minDistance) && + hlsl::CCameraMathUtilities::isFiniteScalar(limits.maxDistance); + } + + static inline bool sanitizePathLimits(SCameraPathLimits& limits) + { + if (!isPathLimitsFinite(limits)) + return false; + + limits.minU = std::clamp( + limits.minU, + 0.0, + static_cast(ICamera::SphericalMaxDistance)); + limits.minDistance = std::clamp( + std::max(limits.minDistance, static_cast(limits.minU)), + static_cast(ICamera::SphericalMinDistance), + static_cast(ICamera::SphericalMaxDistance)); + limits.maxDistance = std::clamp( + limits.maxDistance, + limits.minDistance, + static_cast(ICamera::SphericalMaxDistance)); + return true; + } + static inline bool sanitizePathState(ICamera::PathState& state, const double minU) { return hlsl::CCameraMathUtilities::sanitizePathState(state.s, state.u, state.v, state.roll, minU); } + static inline bool sanitizePathState(ICamera::PathState& state, const SCameraPathLimits& limits, double* outAppliedDistance = nullptr) + { + SCameraPathLimits sanitizedLimits = limits; + if (!sanitizePathLimits(sanitizedLimits)) + return false; + + if (!sanitizePathState(state, sanitizedLimits.minU)) + return false; + + const auto desiredDistance = std::clamp( + hlsl::CCameraMathUtilities::getPathDistance(state.u, state.v), + sanitizedLimits.minDistance, + sanitizedLimits.maxDistance); + return tryScalePathStateDistance(desiredDistance, sanitizedLimits.minU, state, outAppliedDistance); + } + static inline bool tryScalePathStateDistance( const double desiredDistance, const double minU, @@ -216,9 +251,13 @@ struct CCameraPathUtilities final ICamera::PathState& ioState, SCameraPathDistanceUpdateResult* outResult = nullptr) { - const auto clampedDistance = std::clamp(desiredDistance, limits.minDistance, limits.maxDistance); + SCameraPathLimits sanitizedLimits = limits; + if (!sanitizePathLimits(sanitizedLimits) || !sanitizePathState(ioState, sanitizedLimits)) + return false; + + const auto clampedDistance = std::clamp(desiredDistance, sanitizedLimits.minDistance, sanitizedLimits.maxDistance); double appliedDistance = 0.0; - if (!tryScalePathStateDistance(static_cast(clampedDistance), limits.minU, ioState, &appliedDistance)) + if (!tryScalePathStateDistance(static_cast(clampedDistance), sanitizedLimits.minU, ioState, &appliedDistance)) return false; if (outResult) @@ -259,17 +298,21 @@ struct CCameraPathUtilities final const ICamera::PathState* requestedState, ICamera::PathState& outState) { + SCameraPathLimits sanitizedLimits = limits; + if (!sanitizePathLimits(sanitizedLimits)) + return false; + if (requestedState) { outState = *requestedState; - return sanitizePathState(outState, limits.minU); + return sanitizePathState(outState, sanitizedLimits); } - if (tryBuildPathStateFromPosition(targetPosition, position, limits.minU, outState)) - return true; + if (tryBuildPathStateFromPosition(targetPosition, position, sanitizedLimits.minU, outState)) + return sanitizePathState(outState, sanitizedLimits); - outState = makeDefaultPathState(limits.minU); - return sanitizePathState(outState, limits.minU); + outState = makeDefaultPathState(sanitizedLimits.minU); + return sanitizePathState(outState, sanitizedLimits); } static inline bool tryBuildPathPoseFromState( @@ -278,15 +321,19 @@ struct CCameraPathUtilities final const SCameraPathLimits& limits, SCameraPathPose& outPose) { + SCameraPathLimits sanitizedLimits = limits; + if (!sanitizePathLimits(sanitizedLimits)) + return false; + return hlsl::CCameraMathUtilities::tryBuildPathPoseFromState( targetPosition, state.s, state.u, state.v, state.roll, - limits.minU, - limits.minDistance, - limits.maxDistance, + sanitizedLimits.minU, + sanitizedLimits.minDistance, + sanitizedLimits.maxDistance, outPose.position, outPose.orientation, &outPose.appliedDistance, @@ -412,7 +459,7 @@ struct CCameraPathUtilities final stateVector.x = hlsl::CCameraMathUtilities::wrapAngleRad(stateVector.x); stateVector.w = hlsl::CCameraMathUtilities::wrapAngleRad(stateVector.w); outState = ICamera::PathState::fromVector(stateVector); - return sanitizePathState(outState, limits.minU); + return sanitizePathState(outState, limits); } static inline ICamera::PathState blendPathStates( diff --git a/common/include/camera/CCameraPersistence.hpp b/common/include/camera/CCameraPersistence.hpp index 001635b5c..a1f2487ac 100644 --- a/common/include/camera/CCameraPersistence.hpp +++ b/common/include/camera/CCameraPersistence.hpp @@ -18,14 +18,14 @@ namespace nbl::system class ISystem; -//! Serialize a preset collection to JSON. +/// @brief Serialize a preset collection to JSON. bool writePresetCollection(std::ostream& out, std::span presets, int indent = 2); -//! Parse a preset collection from JSON. +/// @brief Parse a preset collection from JSON. bool readPresetCollection(std::istream& in, std::vector& presets); -//! Save a preset collection to disk as JSON. +/// @brief Save a preset collection to disk as JSON. bool savePresetCollectionToFile(ISystem& system, const path& path, std::span presets, int indent = 2); -//! Load a preset collection from disk. +/// @brief Load a preset collection from disk. bool loadPresetCollectionFromFile(ISystem& system, const path& path, std::vector& presets); } // namespace nbl::system diff --git a/common/include/camera/CCameraPlaybackTimeline.hpp b/common/include/camera/CCameraPlaybackTimeline.hpp index aded91569..3dec5dc6f 100644 --- a/common/include/camera/CCameraPlaybackTimeline.hpp +++ b/common/include/camera/CCameraPlaybackTimeline.hpp @@ -10,8 +10,8 @@ namespace nbl::core { -//! Shared playback cursor state for camera keyframe tracks. -//! The cursor is intentionally transport-only so consumers can own higher-level playback policy. +/// @brief Shared playback cursor state for camera keyframe tracks. +/// The cursor is intentionally transport-only so consumers can own higher-level playback policy. struct CCameraPlaybackCursor { bool playing = false; @@ -20,8 +20,8 @@ struct CCameraPlaybackCursor float time = 0.f; }; -//! Outcome of advancing a playback cursor against a keyframe track. -//! This separates raw time stepping from higher-level consumer policy and UI feedback. +/// @brief Outcome of advancing a playback cursor against a keyframe track. +/// This separates raw time stepping from higher-level consumer policy and UI feedback. struct SCameraPlaybackAdvanceResult { bool hasTrack = false; @@ -36,7 +36,7 @@ struct SCameraPlaybackAdvanceResult struct CCameraPlaybackTimelineUtilities final { public: - //! Duration of the current playback track in seconds. + /// @brief Duration of the current playback track in seconds. static inline float getPlaybackTrackDuration(const CCameraKeyframeTrack& track) { if (track.keyframes.empty()) @@ -45,20 +45,20 @@ struct CCameraPlaybackTimelineUtilities final return track.keyframes.back().time; } - //! Reset cursor time and stop playback without mutating loop or speed settings. + /// @brief Reset cursor time and stop playback without mutating loop or speed settings. static inline void resetPlaybackCursor(CCameraPlaybackCursor& cursor, const float time = 0.f) { cursor.playing = false; cursor.time = std::max(0.f, time); } - //! Clamp cursor time into the valid time range of the current track. + /// @brief Clamp cursor time into the valid time range of the current track. static inline void clampPlaybackCursorToTrack(const CCameraKeyframeTrack& track, CCameraPlaybackCursor& cursor) { CCameraKeyframeTrackUtilities::clampTrackTimeToKeyframes(track, cursor.time); } - //! Advance cursor time by `dtSec * speed` and report whether playback wrapped or stopped. + /// @brief Advance cursor time by `dtSec * speed` and report whether playback wrapped or stopped. static inline SCameraPlaybackAdvanceResult advancePlaybackCursor(CCameraPlaybackCursor& cursor, const CCameraKeyframeTrack& track, const double dtSec) { SCameraPlaybackAdvanceResult result; diff --git a/common/include/camera/CCameraPresentationUtilities.hpp b/common/include/camera/CCameraPresentationUtilities.hpp index 29cabf2c3..9b26db9b7 100644 --- a/common/include/camera/CCameraPresentationUtilities.hpp +++ b/common/include/camera/CCameraPresentationUtilities.hpp @@ -12,7 +12,7 @@ namespace nbl::ui { -//! Shared exactness-oriented filter used by preset presentation surfaces. +/// @brief Shared exactness-oriented filter used by preset presentation surfaces. enum class EPresetApplyPresentationFilter : uint8_t { All, @@ -20,7 +20,7 @@ enum class EPresetApplyPresentationFilter : uint8_t BestEffort }; -//! Shared badge/pill policy derived from one analyzed presentation answer. +/// @brief Shared badge/pill policy derived from one analyzed presentation answer. struct SCameraGoalApplyPresentationBadges final { bool exact = false; @@ -30,7 +30,7 @@ struct SCameraGoalApplyPresentationBadges final bool blocked = false; }; -//! Presentation-ready wrapper around analyzed goal apply compatibility. +/// @brief Presentation-ready wrapper around analyzed goal apply compatibility. struct SCameraGoalApplyPresentation final : core::SCameraGoalApplyAnalysis { SCameraGoalApplyPresentationBadges badges; @@ -55,7 +55,7 @@ struct SCameraGoalApplyPresentation final : core::SCameraGoalApplyAnalysis } }; -//! Presentation-ready wrapper around analyzed camera capture viability. +/// @brief Presentation-ready wrapper around analyzed camera capture viability. struct SCameraCapturePresentation final : core::SCameraCaptureAnalysis { std::string policyLabel; @@ -63,7 +63,7 @@ struct SCameraCapturePresentation final : core::SCameraCaptureAnalysis struct CCameraPresentationUtilities final { - //! Shared user-facing label for the exactness filter selector. + /// @brief Shared user-facing label for the exactness filter selector. static inline const char* getPresetApplyPresentationFilterLabel(const EPresetApplyPresentationFilter mode) { switch (mode) @@ -79,7 +79,7 @@ struct CCameraPresentationUtilities final } } - //! Build reusable badge flags for one preset/keyframe compatibility answer. + /// @brief Build reusable badge flags for one preset/keyframe compatibility answer. static inline SCameraGoalApplyPresentationBadges collectGoalApplyPresentationBadges(const SCameraGoalApplyPresentation& presentation) { SCameraGoalApplyPresentationBadges badges; @@ -91,7 +91,7 @@ struct CCameraPresentationUtilities final return badges; } - //! Build presentation text for one analyzed goal-apply result. + /// @brief Build presentation text for one analyzed goal-apply result. static inline SCameraGoalApplyPresentation makeGoalApplyPresentation(const core::SCameraGoalApplyAnalysis& analysis, const core::ICamera* targetCamera) { SCameraGoalApplyPresentation presentation; @@ -104,13 +104,13 @@ struct CCameraPresentationUtilities final return presentation; } - //! Analyze one preset against one camera and return reusable presentation data. + /// @brief Analyze one preset against one camera and return reusable presentation data. static inline SCameraGoalApplyPresentation analyzePresetPresentation(const core::CCameraGoalSolver& solver, const core::ICamera* camera, const core::CCameraPreset& preset) { return makeGoalApplyPresentation(core::CCameraGoalAnalysisUtilities::analyzePresetApply(solver, camera, preset), camera); } - //! Analyze one camera capture path and return reusable presentation data. + /// @brief Analyze one camera capture path and return reusable presentation data. static inline SCameraCapturePresentation analyzeCapturePresentation(const core::CCameraGoalSolver& solver, core::ICamera* camera) { SCameraCapturePresentation presentation; diff --git a/common/include/camera/CCameraPreset.hpp b/common/include/camera/CCameraPreset.hpp index cc33d09d8..a04be5e8c 100644 --- a/common/include/camera/CCameraPreset.hpp +++ b/common/include/camera/CCameraPreset.hpp @@ -13,7 +13,7 @@ namespace nbl::core { -//! Named persisted camera state built on top of `CCameraGoal`. +/// @brief Named persisted camera state built on top of `CCameraGoal`. struct CCameraPreset { std::string name; @@ -21,7 +21,7 @@ struct CCameraPreset CCameraGoal goal = {}; }; -//! Time-stamped preset entry used by playback and authoring tools. +/// @brief Time-stamped preset entry used by playback and authoring tools. struct CCameraKeyframe { CCameraPreset preset; @@ -40,7 +40,7 @@ struct CCameraPresetUtilities final return CCameraGoalUtilities::canonicalizeGoal(preset.goal); } - //! Compare two named presets through their shared canonical goal state. + /// @brief Compare two named presets through their shared canonical goal state. static inline bool comparePresets(const CCameraPreset& lhs, const CCameraPreset& rhs, const double posEps, const double rotEpsDeg, const double scalarEps) { @@ -49,7 +49,7 @@ struct CCameraPresetUtilities final CCameraGoalUtilities::compareGoals(makeGoalFromPreset(lhs), makeGoalFromPreset(rhs), posEps, rotEpsDeg, scalarEps); } - //! Compare two preset collections element-by-element through the shared canonical goal state. + /// @brief Compare two preset collections element-by-element through the shared canonical goal state. static inline bool comparePresetCollections(std::span lhs, std::span rhs, const double posEps, const double rotEpsDeg, const double scalarEps) { diff --git a/common/include/camera/CCameraPresetFlow.hpp b/common/include/camera/CCameraPresetFlow.hpp index 1b282d599..8655b6bd2 100644 --- a/common/include/camera/CCameraPresetFlow.hpp +++ b/common/include/camera/CCameraPresetFlow.hpp @@ -15,7 +15,7 @@ namespace nbl::core { -//! Reusable aggregate summary for applying one preset to multiple cameras. +/// @brief Reusable aggregate summary for applying one preset to multiple cameras. struct SCameraPresetApplySummary { uint32_t targetCount = 0u; @@ -41,7 +41,7 @@ struct SCameraPresetApplySummary struct CCameraPresetFlowUtilities final { - //! Compare the current camera state against a preset using the shared goal representation. + /// @brief Compare the current camera state against a preset using the shared goal representation. static inline bool comparePresetToCameraState(const CCameraGoalSolver& solver, ICamera* camera, const CCameraPreset& preset, const double posEps, const double rotEpsDeg, const double scalarEps) { @@ -57,7 +57,7 @@ struct CCameraPresetFlowUtilities final scalarEps); } - //! Explain the first visible mismatch between a camera state and a preset. + /// @brief Explain the first visible mismatch between a camera state and a preset. static inline std::string describePresetCameraMismatch(const CCameraGoalSolver& solver, ICamera* camera, const CCameraPreset& preset) { const auto capture = solver.captureDetailed(camera); @@ -71,7 +71,7 @@ struct CCameraPresetFlowUtilities final return CCameraGoalUtilities::describeGoalMismatch(capture.goal, CCameraPresetUtilities::makeGoalFromPreset(preset)); } - //! Build a preset from an already analyzed capture result. + /// @brief Build a preset from an already analyzed capture result. static inline bool tryCapturePreset(const SCameraCaptureAnalysis& captureAnalysis, ICamera* camera, std::string_view name, CCameraPreset& preset) { preset = {}; @@ -84,13 +84,13 @@ struct CCameraPresetFlowUtilities final return true; } - //! Capture a preset directly from a camera through the shared goal solver. + /// @brief Capture a preset directly from a camera through the shared goal solver. static inline bool tryCapturePreset(const CCameraGoalSolver& solver, ICamera* camera, std::string_view name, CCameraPreset& preset) { return tryCapturePreset(CCameraGoalAnalysisUtilities::analyzeCameraCapture(solver, camera), camera, name, preset); } - //! Value-returning convenience wrapper around `tryCapturePreset`. + /// @brief Value-returning convenience wrapper around `tryCapturePreset`. static inline CCameraPreset capturePreset(const CCameraGoalSolver& solver, ICamera* camera, std::string_view name) { CCameraPreset preset; @@ -98,7 +98,7 @@ struct CCameraPresetFlowUtilities final return preset; } - //! Apply a preset through the shared goal solver and preserve detailed apply diagnostics. + /// @brief Apply a preset through the shared goal solver and preserve detailed apply diagnostics. static inline CCameraGoalSolver::SApplyResult applyPresetDetailed(const CCameraGoalSolver& solver, ICamera* camera, const CCameraPreset& preset) { if (!camera) @@ -107,13 +107,13 @@ struct CCameraPresetFlowUtilities final return solver.applyDetailed(camera, CCameraPresetUtilities::makeGoalFromPreset(preset)); } - //! Bool-returning convenience wrapper around `applyPresetDetailed`. + /// @brief Bool-returning convenience wrapper around `applyPresetDetailed`. static inline bool applyPreset(const CCameraGoalSolver& solver, ICamera* camera, const CCameraPreset& preset) { return applyPresetDetailed(solver, camera, preset).succeeded(); } - //! Fold one detailed apply result into an aggregate preset-apply summary. + /// @brief Fold one detailed apply result into an aggregate preset-apply summary. static inline void accumulatePresetApplySummary(SCameraPresetApplySummary& summary, const CCameraGoalSolver::SApplyResult& result) { ++summary.targetCount; @@ -129,7 +129,7 @@ struct CCameraPresetFlowUtilities final } } - //! Apply one preset to a camera range and collect a typed aggregate summary. + /// @brief Apply one preset to a camera range and collect a typed aggregate summary. static inline SCameraPresetApplySummary applyPresetToCameraRange(const CCameraGoalSolver& solver, std::span cameras, const CCameraPreset& preset) { SCameraPresetApplySummary summary; diff --git a/common/include/camera/CCameraPresetPersistence.hpp b/common/include/camera/CCameraPresetPersistence.hpp index 27e671c70..298163ab6 100644 --- a/common/include/camera/CCameraPresetPersistence.hpp +++ b/common/include/camera/CCameraPresetPersistence.hpp @@ -15,24 +15,24 @@ namespace nbl::system class ISystem; -//! Serialize one camera goal into an existing stream. +/// @brief Serialize one camera goal into an existing stream. bool writeGoal(std::ostream& out, const core::CCameraGoal& goal, int indent = 2); -//! Deserialize one camera goal from an existing stream. +/// @brief Deserialize one camera goal from an existing stream. bool readGoal(std::istream& in, core::CCameraGoal& goal); -//! Save one camera goal to a file. +/// @brief Save one camera goal to a file. bool saveGoalToFile(ISystem& system, const path& path, const core::CCameraGoal& goal, int indent = 2); -//! Load one camera goal from a file. +/// @brief Load one camera goal from a file. bool loadGoalFromFile(ISystem& system, const path& path, core::CCameraGoal& goal); -//! Serialize one camera preset into an existing stream. +/// @brief Serialize one camera preset into an existing stream. bool writePreset(std::ostream& out, const core::CCameraPreset& preset, int indent = 2); -//! Deserialize one camera preset from an existing stream. +/// @brief Deserialize one camera preset from an existing stream. bool readPreset(std::istream& in, core::CCameraPreset& preset); -//! Save one camera preset to a file. +/// @brief Save one camera preset to a file. bool savePresetToFile(ISystem& system, const path& path, const core::CCameraPreset& preset, int indent = 2); -//! Load one camera preset from a file. +/// @brief Load one camera preset from a file. bool loadPresetFromFile(ISystem& system, const path& path, core::CCameraPreset& preset); } // namespace nbl::system diff --git a/common/include/camera/CCameraProjectionUtilities.hpp b/common/include/camera/CCameraProjectionUtilities.hpp index 803fb2096..220cdc2fc 100644 --- a/common/include/camera/CCameraProjectionUtilities.hpp +++ b/common/include/camera/CCameraProjectionUtilities.hpp @@ -12,7 +12,7 @@ namespace nbl::core struct CCameraProjectionUtilities final { - //! Apply a camera-provided dynamic perspective FOV to one planar projection entry. + /// @brief Apply a camera-provided dynamic perspective FOV to one planar projection entry. static inline bool syncDynamicPerspectiveProjection(ICamera* camera, IPlanarProjection::CProjection& projection) { if (!camera) diff --git a/common/include/camera/CCameraScriptVisualDebugOverlayUtilities.hpp b/common/include/camera/CCameraScriptVisualDebugOverlayUtilities.hpp index 57bd3993d..bad69f7ad 100644 --- a/common/include/camera/CCameraScriptVisualDebugOverlayUtilities.hpp +++ b/common/include/camera/CCameraScriptVisualDebugOverlayUtilities.hpp @@ -17,7 +17,7 @@ namespace nbl::ui { -//! Shared data bundle for the scripted visual debug HUD. +/// @brief Shared data bundle for the scripted visual debug HUD. struct SCameraScriptVisualDebugOverlayData final { std::string title; @@ -31,7 +31,7 @@ struct SCameraScriptVisualDebugOverlayData final } }; -//! Shared camera/debug state used to format one scripted visual debug HUD payload. +/// @brief Shared camera/debug state used to format one scripted visual debug HUD payload. struct SCameraScriptVisualDebugStatus final { static constexpr float DefaultTargetFps = 60.0f; @@ -57,7 +57,7 @@ struct SCameraScriptVisualDebugStatus final float followTargetCenterNdcRadius = 0.0f; }; -//! Shared style bundle for the scripted visual debug HUD. +/// @brief Shared style bundle for the scripted visual debug HUD. struct SCameraScriptVisualDebugOverlayStyle final { static constexpr float TitleSize = 50.0f; diff --git a/common/include/camera/CCameraScriptedCheckRunner.hpp b/common/include/camera/CCameraScriptedCheckRunner.hpp index 8fb99a091..0fe68760b 100644 --- a/common/include/camera/CCameraScriptedCheckRunner.hpp +++ b/common/include/camera/CCameraScriptedCheckRunner.hpp @@ -20,16 +20,14 @@ namespace nbl::system { -/** -* Runtime state for authored scripted checks. -* -* The state is intentionally small: -* -* - authored check data stays in `CCameraScriptedInputCheck` -* - the runner only remembers where the next check starts -* - baseline and step references are maintained here so consumers do not have to -* keep duplicating the same bookkeeping -*/ +/// @brief Runtime state for authored scripted checks. +/// +/// The state is intentionally small: +/// +/// - authored check data stays in `CCameraScriptedInputCheck` +/// - the runner only remembers where the next check starts +/// - baseline and step references are maintained here so consumers do not have to +/// keep duplicating the same bookkeeping struct CCameraScriptedCheckRuntimeState { struct SPoseReference final : core::SCameraRigPose @@ -42,7 +40,7 @@ struct CCameraScriptedCheckRuntimeState SPoseReference step = {}; }; -//! Shared per-frame evaluation context for authored scripted checks. +/// @brief Shared per-frame evaluation context for authored scripted checks. struct CCameraScriptedCheckContext { uint64_t frame = 0ull; @@ -55,14 +53,14 @@ struct CCameraScriptedCheckContext const core::CCameraGoalSolver* goalSolver = nullptr; }; -//! Reusable log entry produced by scripted check evaluation. +/// @brief Reusable log entry produced by scripted check evaluation. struct CCameraScriptedCheckLogEntry { bool failure = false; std::string text; }; -//! Result for one frame worth of scripted checks. +/// @brief Result for one frame worth of scripted checks. struct CCameraScriptedCheckFrameResult { std::vector logs; @@ -127,7 +125,7 @@ struct CCameraScriptedCheckRunnerUtilities final result.hadFailures = result.hadFailures || failure; } - //! Evaluate all authored scripted checks scheduled for the current frame. + /// @brief Evaluate all authored scripted checks scheduled for the current frame. static inline CCameraScriptedCheckFrameResult evaluateScriptedChecksForFrame( const std::vector& checks, CCameraScriptedCheckRuntimeState& state, @@ -535,7 +533,7 @@ struct CCameraScriptedCheckRunnerUtilities final buildScriptedCheckMessage([&](std::ostringstream& oss) { oss << "[script][fail] follow_lock frame=" << context.frame << ' ' - << (regressionError.empty() ? "follow contract mismatch" : regressionError); + << (regressionError.empty() ? "follow validation mismatch" : regressionError); })); } else diff --git a/common/include/camera/CCameraScriptedRuntime.hpp b/common/include/camera/CCameraScriptedRuntime.hpp index 54d690c99..d15b53d43 100644 --- a/common/include/camera/CCameraScriptedRuntime.hpp +++ b/common/include/camera/CCameraScriptedRuntime.hpp @@ -18,13 +18,11 @@ namespace nbl::system { -/** -* Shared scripted runtime contract used by camera-sequence consumers. -* -* The compact authored sequence remains camera-domain. A concrete runtime may still expand it -* into low-level per-frame events and checks, but those expanded payloads live in this shared -* header rather than inside one consumer. -*/ +/// @brief Shared scripted runtime payload used by camera-sequence consumers. +/// +/// The compact authored sequence remains camera-domain. A concrete runtime may still expand it +/// into low-level per-frame events and checks, but those expanded payloads live in this shared +/// header rather than inside one consumer. struct CCameraScriptedInputEvent { enum class Type : uint8_t @@ -166,7 +164,7 @@ struct CCameraScriptedInputCheck bool hasEulerDeltaConstraint = false; }; -//! Fully expanded scripted timeline shared between authored parsers and runtime consumers. +/// @brief Fully expanded scripted timeline shared between authored parsers and runtime consumers. struct CCameraScriptedTimeline { std::vector events; @@ -315,12 +313,10 @@ struct CCameraScriptedRuntimeUtilities final } }; -/** -* Per-frame scripted runtime batch already partitioned by payload kind. -* -* Consumers can dequeue authored events for one frame and then adapt only the buckets they care -* about, without repeatedly switching on `CCameraScriptedInputEvent::Type` in local glue. -*/ +/// @brief Per-frame scripted runtime batch already partitioned by payload kind. +/// +/// Consumers can dequeue authored events for one frame and then adapt only the buckets they care +/// about, without repeatedly switching on `CCameraScriptedInputEvent::Type` in local glue. struct CCameraScriptedFrameEvents { std::vector keyboard; @@ -349,7 +345,7 @@ struct CCameraScriptedFrameEvents } }; -//! Dequeue all authored scripted events scheduled for one frame. +/// @brief Dequeue all authored scripted events scheduled for one frame. struct CCameraScriptedFrameEventUtilities final { static inline void dequeueScriptedFrameEvents( diff --git a/common/include/camera/CCameraScriptedRuntimePersistence.hpp b/common/include/camera/CCameraScriptedRuntimePersistence.hpp index 077fb61d0..58e6f2dbb 100644 --- a/common/include/camera/CCameraScriptedRuntimePersistence.hpp +++ b/common/include/camera/CCameraScriptedRuntimePersistence.hpp @@ -19,7 +19,7 @@ namespace nbl::system class ISystem; -//! Optional scripted control overrides parsed alongside one runtime payload. +/// @brief Optional scripted control overrides parsed alongside one runtime payload. struct CCameraScriptedControlOverrides { bool hasKeyboardScale = false; @@ -34,7 +34,7 @@ struct CCameraScriptedControlOverrides float rotationScale = 1.f; }; -//! Parsed low-level scripted runtime payload plus optional compact authored sequence. +/// @brief Parsed low-level scripted runtime payload plus optional compact authored sequence. struct CCameraScriptedInputParseResult { bool enabled = true; @@ -62,11 +62,11 @@ struct CCameraScriptedRuntimePersistenceUtilities final } }; -//! Parse one low-level scripted runtime payload from an existing stream. +/// @brief Parse one low-level scripted runtime payload from an existing stream. bool readCameraScriptedInput(std::istream& in, CCameraScriptedInputParseResult& out, std::string* error = nullptr); -//! Parse one low-level scripted runtime payload directly from text. +/// @brief Parse one low-level scripted runtime payload directly from text. bool readCameraScriptedInput(std::string_view text, CCameraScriptedInputParseResult& out, std::string* error = nullptr); -//! Load one low-level scripted runtime payload from a file. +/// @brief Load one low-level scripted runtime payload from a file. bool loadCameraScriptedInputFromFile(ISystem& system, const path& path, CCameraScriptedInputParseResult& out, std::string* error = nullptr); } // namespace nbl::system diff --git a/common/include/camera/CCameraSequenceScript.hpp b/common/include/camera/CCameraSequenceScript.hpp index 640a2d291..b2e47a348 100644 --- a/common/include/camera/CCameraSequenceScript.hpp +++ b/common/include/camera/CCameraSequenceScript.hpp @@ -21,36 +21,34 @@ namespace nbl::core { -/** -* Compact authored camera-sequence format shared by playback, scripting, and validation tooling. -* -* The authored file describes: -* -* - which camera kind a segment targets -* - which reusable projection presentations should be shown -* - which keyframed camera goals should be sampled over time -* - which tracked-target poses should be sampled over time -* - which continuity thresholds and capture points should be generated -* -* The format intentionally does not store: -* -* - per-frame low-level event dumps -* - runtime-specific window actions as authored source data -* - ImGuizmo transforms as the primary authored primitive -* -* A consumer may expand the compact sequence into its own runtime event/check representation, but -* the authored source of truth stays camera-domain and reusable. -*/ - -//! Authored projection view request for camera-sequence playback. +/// @brief Compact authored camera-sequence format shared by playback, scripting, and validation tooling. +/// +/// The authored file describes: +/// +/// - which camera kind a segment targets +/// - which reusable projection presentations should be shown +/// - which keyframed camera goals should be sampled over time +/// - which tracked-target poses should be sampled over time +/// - which continuity thresholds and capture points should be generated +/// +/// The format intentionally does not store: +/// +/// - per-frame low-level event dumps +/// - runtime-specific window actions as authored source data +/// - ImGuizmo transforms as the primary authored primitive +/// +/// A consumer may expand the compact sequence into its own runtime event/check representation, but +/// the authored source of truth stays camera-domain and reusable. + +/// @brief Authored projection view request for camera-sequence playback. struct CCameraSequencePresentation { IPlanarProjection::CProjection::ProjectionType projection = IPlanarProjection::CProjection::Perspective; bool leftHanded = true; }; -//! Shared continuity thresholds authored once and reused per sequence segment. -//! Max bounds are enforced per-step, while minimum progress can be satisfied by either position or rotation change. +/// @brief Shared continuity thresholds authored once and reused per sequence segment. +/// Max bounds are enforced per-step, while minimum progress can be satisfied by either position or rotation change. struct CCameraSequenceContinuitySettings { bool baseline = true; @@ -63,10 +61,42 @@ struct CCameraSequenceContinuitySettings float maxEulerDeltaDeg = 1.f; }; -//! Relative goal adjustment authored against an initial preset captured from the target camera. -//! Deltas stay camera-domain and avoid binding the authored file to any specific input device or consumer. +/// @brief Relative goal adjustment authored against an initial preset captured from the target camera. +/// Deltas stay camera-domain and avoid binding the authored file to any specific input device or consumer. struct CCameraSequenceGoalDelta { + struct SOrbitDelta final + { + hlsl::float64_t2 uvDeltaRad = hlsl::float64_t2(0.0); + float distanceDelta = 0.f; + bool hasU = false; + bool hasV = false; + bool hasDistance = false; + + inline bool hasAny() const + { + return hasU || hasV || hasDistance; + } + + inline void setUDeltaDeg(const double valueDeg) + { + uvDeltaRad.x = static_cast(hlsl::radians(valueDeg)); + hasU = true; + } + + inline void setVDeltaDeg(const double valueDeg) + { + uvDeltaRad.y = static_cast(hlsl::radians(valueDeg)); + hasV = true; + } + + inline void setDistanceDelta(const float valueScalar) + { + distanceDelta = valueScalar; + hasDistance = true; + } + }; + struct SPathDelta final { SCameraPathDelta value = {}; @@ -128,12 +158,7 @@ struct CCameraSequenceGoalDelta bool hasTargetOffset = false; hlsl::float64_t3 targetOffset = hlsl::float64_t3(0.0); - bool hasOrbitUDeltaDeg = false; - bool hasOrbitVDeltaDeg = false; - hlsl::float64_t2 orbitUvDeltaDeg = hlsl::float64_t2(0.0); - - bool hasOrbitDistanceDelta = false; - float orbitDistanceDelta = 0.f; + SOrbitDelta orbitDelta = {}; SPathDelta pathDelta = {}; @@ -144,8 +169,8 @@ struct CCameraSequenceGoalDelta float dynamicReferenceDistanceDelta = 0.f; }; -//! One authored keyframe inside a reusable camera-sequence segment. -//! A keyframe can be described either as an absolute preset or as a delta relative to the captured reference preset. +/// @brief One authored keyframe inside a reusable camera-sequence segment. +/// A keyframe can be described either as an absolute preset or as a delta relative to the captured reference preset. struct CCameraSequenceKeyframe { float time = 0.f; @@ -155,12 +180,12 @@ struct CCameraSequenceKeyframe CCameraSequenceGoalDelta delta = {}; }; -//! Concrete tracked-target pose sampled from a shared authored sequence. +/// @brief Concrete tracked-target pose sampled from a shared authored sequence. struct CCameraSequenceTrackedTargetPose final : SCameraRigPose { }; -//! Relative tracked-target adjustment authored against an initial tracked-target pose. +/// @brief Relative tracked-target adjustment authored against an initial tracked-target pose. struct CCameraSequenceTrackedTargetDelta { bool hasPositionOffset = false; @@ -170,8 +195,8 @@ struct CCameraSequenceTrackedTargetDelta hlsl::float32_t3 rotationEulerDegOffset = hlsl::float32_t3(0.f); }; -//! One authored tracked-target keyframe inside a reusable camera-sequence segment. -//! Target keyframes stay camera-domain and can drive follow behavior without runtime-object references. +/// @brief One authored tracked-target keyframe inside a reusable camera-sequence segment. +/// Target keyframes stay camera-domain and can drive follow behavior without runtime-object references. struct CCameraSequenceTrackedTargetKeyframe { float time = 0.f; @@ -183,8 +208,8 @@ struct CCameraSequenceTrackedTargetKeyframe CCameraSequenceTrackedTargetDelta delta = {}; }; -//! Runtime sampled tracked-target track built from an authored segment plus a reference pose. -//! Keyframes are normalized by time before sampling. Duplicate times collapse to the last authored pose. +/// @brief Runtime sampled tracked-target track built from an authored segment plus a reference pose. +/// Keyframes are normalized by time before sampling. Duplicate times collapse to the last authored pose. struct CCameraSequenceTrackedTargetTrack { struct SKeyframe @@ -196,7 +221,7 @@ struct CCameraSequenceTrackedTargetTrack std::vector keyframes; }; -//! Defaults shared by all camera-sequence segments unless overridden locally. +/// @brief Defaults shared by all camera-sequence segments unless overridden locally. struct CCameraSequenceSegmentDefaults { float durationSeconds = 4.f; @@ -206,8 +231,8 @@ struct CCameraSequenceSegmentDefaults bool resetCamera = true; }; -//! Authored reusable camera-sequence segment. -//! A segment is the main unit of authored playback and validation and usually maps to one camera showcase chunk. +/// @brief Authored reusable camera-sequence segment. +/// A segment is the main unit of authored playback and validation and usually maps to one camera showcase chunk. struct CCameraSequenceSegment { std::string name; @@ -232,8 +257,8 @@ struct CCameraSequenceSegment std::vector targetKeyframes; }; -//! Top-level reusable camera-sequence script. -//! Consumers are expected to expand this compact description into their own runtime playback/check pipeline. +/// @brief Top-level reusable camera-sequence script. +/// Consumers are expected to expand this compact description into their own runtime playback/check pipeline. struct CCameraSequenceScript { bool enabled = true; @@ -251,8 +276,8 @@ struct CCameraSequenceScript std::vector segments; }; -//! Reusable compiled sequence segment derived from authored data plus captured references. -//! Consumers can build their own runtime actions/checks from this normalized representation. +/// @brief Reusable compiled sequence segment derived from authored data plus captured references. +/// Consumers can build their own runtime actions/checks from this normalized representation. struct CCameraSequenceCompiledSegment { std::string name; @@ -272,8 +297,8 @@ struct CCameraSequenceCompiledSegment } }; -//! One compiled frame policy entry derived from a reusable compiled segment. -//! Consumers can map these booleans to their own runtime checks and capture requests. +/// @brief One compiled frame policy entry derived from a reusable compiled segment. +/// Consumers can map these booleans to their own runtime checks and capture requests. struct CCameraSequenceCompiledFramePolicy { uint64_t frameOffset = 0ull; @@ -359,7 +384,7 @@ struct CCameraSequenceScriptUtilities final const auto& delta = authored.delta; const bool hasPoseDelta = delta.hasPositionOffset || delta.hasRotationEulerDegOffset; - const bool hasSphericalDelta = delta.hasTargetOffset || delta.hasOrbitUDeltaDeg || delta.hasOrbitVDeltaDeg || delta.hasOrbitDistanceDelta; + const bool hasSphericalDelta = delta.hasTargetOffset || delta.orbitDelta.hasAny(); const bool hasPathDelta = delta.pathDelta.hasAny(); if (hasPoseDelta && (hasSphericalDelta || hasPathDelta)) @@ -388,7 +413,7 @@ struct CCameraSequenceScriptUtilities final goal.targetPosition += delta.targetOffset; } - if (delta.hasOrbitUDeltaDeg || delta.hasOrbitVDeltaDeg || delta.hasOrbitDistanceDelta) + if (delta.orbitDelta.hasAny()) { if (!goal.hasOrbitState) { @@ -397,20 +422,17 @@ struct CCameraSequenceScriptUtilities final return false; } - const auto orbitUvDeltaRad = hlsl::float64_t2( - delta.hasOrbitUDeltaDeg ? static_cast(hlsl::radians(delta.orbitUvDeltaDeg.x)) : hlsl::float64_t(0.0), - delta.hasOrbitVDeltaDeg ? static_cast(hlsl::radians(delta.orbitUvDeltaDeg.y)) : hlsl::float64_t(0.0)); - if (delta.hasOrbitUDeltaDeg) - goal.orbitUv.x = hlsl::CCameraMathUtilities::wrapAngleRad(goal.orbitUv.x + orbitUvDeltaRad.x); - if (delta.hasOrbitVDeltaDeg) + if (delta.orbitDelta.hasU) + goal.orbitUv.x = hlsl::CCameraMathUtilities::wrapAngleRad(goal.orbitUv.x + delta.orbitDelta.uvDeltaRad.x); + if (delta.orbitDelta.hasV) { goal.orbitUv.y = std::clamp( - goal.orbitUv.y + orbitUvDeltaRad.y, + goal.orbitUv.y + delta.orbitDelta.uvDeltaRad.y, -SCameraTargetRelativeRigDefaults::ArcballPitchLimitRad, SCameraTargetRelativeRigDefaults::ArcballPitchLimitRad); } - if (delta.hasOrbitDistanceDelta) - goal.orbitDistance += delta.orbitDistanceDelta; + if (delta.orbitDelta.hasDistance) + goal.orbitDistance += delta.orbitDelta.distanceDelta; } if (delta.pathDelta.hasAny()) @@ -662,7 +684,7 @@ struct CCameraSequenceScriptUtilities final return std::max(1ull, static_cast(std::llround(static_cast(safeDuration) * static_cast(safeFps)))); } - //! Build one sampled time per authored frame in the compiled segment. + /// @brief Build one sampled time per authored frame in the compiled segment. static inline void buildSequenceSampleTimes(const float durationSeconds, const uint64_t durationFrames, std::vector& outTimes) { outTimes.clear(); @@ -675,7 +697,7 @@ struct CCameraSequenceScriptUtilities final } } - //! Expand normalized capture fractions into concrete frame offsets inside the compiled segment. + /// @brief Expand normalized capture fractions into concrete frame offsets inside the compiled segment. static inline void buildSequenceCaptureFrameOffsets( const uint64_t durationFrames, const std::vector& captureFractions, @@ -696,7 +718,7 @@ struct CCameraSequenceScriptUtilities final outOffsets.erase(std::unique(outOffsets.begin(), outOffsets.end()), outOffsets.end()); } - //! Compile one authored sequence segment into normalized reusable data for runtime consumers. + /// @brief Compile one authored sequence segment into normalized reusable data for runtime consumers. static inline bool compileSequenceSegmentFromReference( const CCameraSequenceScript& script, const CCameraSequenceSegment& segment, diff --git a/common/include/camera/CCameraSequenceScriptPersistence.hpp b/common/include/camera/CCameraSequenceScriptPersistence.hpp index 8a6a6a4e1..1ec56871e 100644 --- a/common/include/camera/CCameraSequenceScriptPersistence.hpp +++ b/common/include/camera/CCameraSequenceScriptPersistence.hpp @@ -17,11 +17,11 @@ namespace nbl::system class ISystem; -//! Parse one compact camera-sequence script from an existing stream. +/// @brief Parse one compact camera-sequence script from an existing stream. bool readCameraSequenceScript(std::istream& in, core::CCameraSequenceScript& out, std::string* error = nullptr); -//! Parse one compact camera-sequence script directly from text. +/// @brief Parse one compact camera-sequence script directly from text. bool readCameraSequenceScript(std::string_view text, core::CCameraSequenceScript& out, std::string* error = nullptr); -//! Load one compact camera-sequence script from a file. +/// @brief Load one compact camera-sequence script from a file. bool loadCameraSequenceScriptFromFile(ISystem& system, const path& path, core::CCameraSequenceScript& out, std::string* error = nullptr); } // namespace nbl::system diff --git a/common/include/camera/CCameraSequenceScriptedBuilder.hpp b/common/include/camera/CCameraSequenceScriptedBuilder.hpp index fae12d6a0..ae27fa690 100644 --- a/common/include/camera/CCameraSequenceScriptedBuilder.hpp +++ b/common/include/camera/CCameraSequenceScriptedBuilder.hpp @@ -14,28 +14,26 @@ namespace nbl::system { -/** -* Build expanded scripted runtime data from a compiled camera-sequence segment. -* -* This keeps authored sequence semantics in shared camera helpers instead of re-encoding -* `Goal`, `TrackedTargetTransform`, `Baseline`, `GimbalStep`, and capture scheduling inside -* one consumer. -*/ +/// @brief Build expanded scripted runtime data from a compiled camera-sequence segment. +/// +/// This keeps authored sequence semantics in shared camera helpers instead of re-encoding +/// `Goal`, `TrackedTargetTransform`, `Baseline`, `GimbalStep`, and capture scheduling inside +/// one consumer. struct CCameraSequenceScriptedSegmentBuildInfo { - //! Planar that should receive the compiled segment. + /// @brief Planar that should receive the compiled segment. uint32_t planarIx = 0u; - //! Number of windows the consumer can actually route presentation actions to. + /// @brief Number of windows the consumer can actually route presentation actions to. size_t availableWindowCount = 1u; - //! Whether secondary window presentation actions should be emitted. + /// @brief Whether secondary window presentation actions should be emitted. bool useWindow = false; - //! Whether per-frame follow-lock checks should be generated for this segment. + /// @brief Whether per-frame follow-lock checks should be generated for this segment. bool includeFollowTargetLock = false; }; struct CCameraSequenceScriptedBuilderUtilities final { - //! Append one compiled segment as expanded scripted runtime payloads. + /// @brief Append one compiled segment as expanded scripted runtime payloads. static inline bool appendCompiledSequenceSegmentToScriptedTimeline( CCameraScriptedTimeline& timeline, const uint64_t baseFrame, diff --git a/common/include/camera/CCameraSmokeRegressionUtilities.hpp b/common/include/camera/CCameraSmokeRegressionUtilities.hpp index 63c7fccd3..387249519 100644 --- a/common/include/camera/CCameraSmokeRegressionUtilities.hpp +++ b/common/include/camera/CCameraSmokeRegressionUtilities.hpp @@ -32,7 +32,7 @@ struct SCameraSmokeComparisonThresholds final struct CCameraSmokeRegressionUtilities final { public: - //! Measure one camera pose delta against an authored reference pose. + /// @brief Measure one camera pose delta against an authored reference pose. static inline bool tryComputeCameraManipulationDelta( core::ICamera* camera, const hlsl::float64_t3& beforePosition, @@ -49,7 +49,7 @@ struct CCameraSmokeRegressionUtilities final return hlsl::CCameraMathUtilities::tryComputePoseDelta(afterPosition, afterOrientation, beforePosition, beforeOrientation, outDelta); } - //! Manipulate a camera and report how far its pose moved in position and Euler-angle terms. + /// @brief Manipulate a camera and report how far its pose moved in position and Euler-angle terms. static inline bool tryManipulateCameraAndMeasureDelta( core::ICamera* camera, std::span events, diff --git a/common/include/camera/CCameraTargetRelativeUtilities.hpp b/common/include/camera/CCameraTargetRelativeUtilities.hpp index ee914d5a7..12bfbb846 100644 --- a/common/include/camera/CCameraTargetRelativeUtilities.hpp +++ b/common/include/camera/CCameraTargetRelativeUtilities.hpp @@ -9,7 +9,7 @@ namespace nbl::core { -//! Canonical target-relative orbit state shared by spherical cameras, follow, and goal solving. +/// @brief Canonical target-relative orbit state shared by spherical cameras, follow, and goal solving. struct SCameraTargetRelativeState final { hlsl::float64_t3 target = hlsl::float64_t3(0.0); @@ -17,13 +17,13 @@ struct SCameraTargetRelativeState final float distance = ICamera::SphericalMinDistance; }; -//! Pose reconstructed from a target-relative orbit state. +/// @brief Pose reconstructed from a target-relative orbit state. struct SCameraTargetRelativePose final : SCameraRigPose { hlsl::float64_t appliedDistance = static_cast(ICamera::SphericalMinDistance); }; -//! Derived basis for target-relative orbit rigs. +/// @brief Derived basis for target-relative orbit rigs. struct SCameraTargetRelativeBasis final { hlsl::float64_t3 localOffset = hlsl::float64_t3(0.0); @@ -32,7 +32,7 @@ struct SCameraTargetRelativeBasis final hlsl::float64_t3 forward = hlsl::float64_t3(0.0, 1.0, 0.0); }; -//! Delta between current spherical target state and canonical target-relative goal. +/// @brief Delta between current spherical target state and canonical target-relative goal. struct SCameraTargetRelativeDelta final { hlsl::float64_t2 orbitUv = hlsl::float64_t2(0.0); @@ -55,7 +55,7 @@ struct SCameraTargetRelativeEventPolicy final }; }; -//! Shared authored/default constants for target-relative rigs. +/// @brief Shared authored/default constants for target-relative rigs. struct SCameraTargetRelativeRigDefaults final { static constexpr float InitialDistance = 1.0f; diff --git a/common/include/camera/CCameraTextUtilities.hpp b/common/include/camera/CCameraTextUtilities.hpp index 1c64649ad..d07212b39 100644 --- a/common/include/camera/CCameraTextUtilities.hpp +++ b/common/include/camera/CCameraTextUtilities.hpp @@ -19,31 +19,31 @@ namespace nbl::ui struct CCameraTextUtilities final { public: - //! Return a short human-readable label for a camera kind. + /// @brief Return a short human-readable label for a camera kind. static inline std::string_view getCameraTypeLabel(const core::ICamera::CameraKind kind) { return core::CCameraKindUtilities::getCameraKindLabel(kind); } - //! Return a short human-readable label for a concrete camera instance. + /// @brief Return a short human-readable label for a concrete camera instance. static inline std::string_view getCameraTypeLabel(const core::ICamera* camera) { return camera ? getCameraTypeLabel(camera->getKind()) : "Unknown"; } - //! Return a short human-readable description for a camera kind. + /// @brief Return a short human-readable description for a camera kind. static inline std::string_view getCameraTypeDescription(const core::ICamera::CameraKind kind) { return core::CCameraKindUtilities::getCameraKindDescription(kind); } - //! Return a short human-readable description for a concrete camera instance. + /// @brief Return a short human-readable description for a concrete camera instance. static inline std::string_view getCameraTypeDescription(const core::ICamera* camera) { return camera ? getCameraTypeDescription(camera->getKind()) : "Unspecified camera behavior"; } - //! Return a short human-readable label for a follow mode. + /// @brief Return a short human-readable label for a follow mode. static inline constexpr const char* getCameraFollowModeLabel(const core::ECameraFollowMode mode) { switch (mode) @@ -57,7 +57,7 @@ struct CCameraTextUtilities final } } - //! Return a short human-readable description for a follow mode. + /// @brief Return a short human-readable description for a follow mode. static inline constexpr const char* getCameraFollowModeDescription(const core::ECameraFollowMode mode) { switch (mode) @@ -71,7 +71,7 @@ struct CCameraTextUtilities final } } - //! Describe the typed goal-state mask in a stable human-readable format. + /// @brief Describe the typed goal-state mask in a stable human-readable format. static inline std::string describeGoalStateMask(const uint32_t mask) { if (mask == core::ICamera::GoalStateNone) @@ -93,7 +93,7 @@ struct CCameraTextUtilities final return out; } - //! Describe a detailed goal-apply result for logs, smoke tests, and UI summaries. + /// @brief Describe a detailed goal-apply result for logs, smoke tests, and UI summaries. static inline std::string describeApplyResult(const core::CCameraGoalSolver::SApplyResult& result) { std::ostringstream oss; @@ -134,7 +134,7 @@ struct CCameraTextUtilities final return oss.str(); } - //! Describe compatibility preview for applying one analyzed goal to a target camera. + /// @brief Describe compatibility preview for applying one analyzed goal to a target camera. static inline std::string describeGoalApplyCompatibility(const core::SCameraGoalApplyAnalysis& analysis, const core::ICamera* targetCamera) { if (!analysis.hasCamera) @@ -153,7 +153,7 @@ struct CCameraTextUtilities final return oss.str(); } - //! Describe whether an analyzed goal can be meaningfully applied to the target camera. + /// @brief Describe whether an analyzed goal can be meaningfully applied to the target camera. static inline std::string describeGoalApplyPolicy(const core::SCameraGoalApplyAnalysis& analysis) { if (!analysis.hasCamera) @@ -173,7 +173,7 @@ struct CCameraTextUtilities final return oss.str(); } - //! Describe whether one analyzed camera state can be captured into a reusable goal. + /// @brief Describe whether one analyzed camera state can be captured into a reusable goal. static inline std::string describeCameraCapturePolicy(const core::SCameraCaptureAnalysis& analysis, const core::ICamera* camera) { if (!analysis.hasCamera) @@ -189,7 +189,7 @@ struct CCameraTextUtilities final return oss.str(); } - //! Describe the aggregate outcome of applying one preset to multiple cameras. + /// @brief Describe the aggregate outcome of applying one preset to multiple cameras. static inline std::string describePresetApplySummary(const core::SCameraPresetApplySummary& summary, std::string_view noTargetsLabel, std::string_view prefix = "Playback apply") { if (!summary.hasTargets()) diff --git a/common/include/camera/CCameraViewportOverlayUtilities.hpp b/common/include/camera/CCameraViewportOverlayUtilities.hpp index c602e0bba..507e79f13 100644 --- a/common/include/camera/CCameraViewportOverlayUtilities.hpp +++ b/common/include/camera/CCameraViewportOverlayUtilities.hpp @@ -14,7 +14,7 @@ namespace nbl::ui { -//! Screen-space viewport rectangle used by debug overlay helpers. +/// @brief Screen-space viewport rectangle used by debug overlay helpers. struct SViewportOverlayRect final { ImVec2 position = ImVec2(0.0f, 0.0f); @@ -38,7 +38,7 @@ struct SViewportOverlayRect final } }; -//! Shared style bundle for the follow-target viewport overlay. +/// @brief Shared style bundle for the follow-target viewport overlay. struct SCameraFollowTargetViewportOverlayStyle final { static constexpr float CenteredNdcRadius = system::SCameraFollowRegressionThresholds::DefaultProjectedNdcTolerance; @@ -84,7 +84,7 @@ struct SCameraFollowTargetViewportOverlayStyle final ImU32 defaultLinkColor = DefaultLinkColor; }; -//! Shared visual style for transparent viewport windows used by scene image panels. +/// @brief Shared visual style for transparent viewport windows used by scene image panels. struct SCameraViewportWindowStyle final { static constexpr float WindowRounding = 0.0f; @@ -98,7 +98,7 @@ struct SCameraViewportWindowStyle final ImVec4 windowBackgroundColor = WindowBackgroundColor; }; -//! Shared data bundle for the top-right camera/projection overlay rendered inside one viewport window. +/// @brief Shared data bundle for the top-right camera/projection overlay rendered inside one viewport window. struct SCameraViewportInfoOverlayData final { std::string headline; @@ -111,7 +111,7 @@ struct SCameraViewportInfoOverlayData final } }; -//! Shared style bundle for the top-right camera/projection overlay rendered inside one viewport window. +/// @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); @@ -135,7 +135,7 @@ struct SCameraViewportInfoOverlayStyle final ImU32 detailColor = DetailColor; }; -//! Shared style bundle for small hover-info popups near the mouse cursor. +/// @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); @@ -149,7 +149,7 @@ struct SCameraHoverInfoOverlayStyle final ImVec2 mouseOffset = MouseOffset; }; -//! Shared style bundle for the split divider overlay between stacked viewport windows. +/// @brief Shared style bundle for the split divider overlay between stacked viewport windows. struct SCameraViewportSplitOverlayStyle final { static constexpr float MinimumGapFill = 2.0f; diff --git a/common/include/camera/CCubeProjection.hpp b/common/include/camera/CCubeProjection.hpp index 3e0bb0126..7017e49e0 100644 --- a/common/include/camera/CCubeProjection.hpp +++ b/common/include/camera/CCubeProjection.hpp @@ -7,36 +7,33 @@ namespace nbl::core { -/** -* @brief A projection where each cube face is a perspective quad we project onto. -* -* Represents a cube projection given direction vector where each face of -* the cube is treated as a quad. The projection onto the cube is done using -* these quads and each face has its own unique pre-transform and -* view-port linear matrix. -*/ +/// @brief Projection where each cube face is a perspective quad. +/// +/// This represents a cube projection for a direction vector where each face of +/// the cube is treated as a quad. Projection onto the cube is done through +/// those quads, each with its own pre-transform and viewport linear matrix. class CCubeProjection final : public IPerspectiveProjection, public IProjection { public: - //! Represents six face identifiers of a cube. + /// @brief Represents six face identifiers of a cube. enum CubeFaces : uint8_t { - //! Cube face in the +X base direction + /// @brief Cube face in the +X base direction PositiveX = 0, - //! Cube face in the -X base direction + /// @brief Cube face in the -X base direction NegativeX, - //! Cube face in the +Y base direction + /// @brief Cube face in the +Y base direction PositiveY, - //! Cube face in the -Y base direction + /// @brief Cube face in the -Y base direction NegativeY, - //! Cube face in the +Z base direction + /// @brief Cube face in the +Z base direction PositiveZ, - //! Cube face in the -Z base direction + /// @brief Cube face in the -Z base direction NegativeZ, CubeFacesCount diff --git a/common/include/camera/CFPSCamera.hpp b/common/include/camera/CFPSCamera.hpp index f6f0741f5..009215e1b 100644 --- a/common/include/camera/CFPSCamera.hpp +++ b/common/include/camera/CFPSCamera.hpp @@ -12,7 +12,7 @@ namespace nbl::core { -//! Free-position camera that keeps the view upright and exposes only yaw/pitch rotation. +/// @brief Free-position camera that keeps the view upright and exposes only yaw/pitch rotation. class CFPSCamera final : public ICamera { public: diff --git a/common/include/camera/CFreeLockCamera.hpp b/common/include/camera/CFreeLockCamera.hpp index 81b2599b6..fc0b1e67b 100644 --- a/common/include/camera/CFreeLockCamera.hpp +++ b/common/include/camera/CFreeLockCamera.hpp @@ -10,7 +10,7 @@ namespace nbl::core { -//! Free-position camera that allows full yaw/pitch/roll rotation. +/// @brief Free-position camera that allows full yaw/pitch/roll rotation. class CFreeCamera final : public ICamera { public: diff --git a/common/include/camera/CGimbalInputBinder.hpp b/common/include/camera/CGimbalInputBinder.hpp index 3473d3669..0dc44894f 100644 --- a/common/include/camera/CGimbalInputBinder.hpp +++ b/common/include/camera/CGimbalInputBinder.hpp @@ -8,12 +8,10 @@ namespace nbl::ui { -/** -* High-level runtime binder for consumers and viewport glue. -* -* It owns active runtime mappings and collects one frame of virtual events -* from raw keyboard, mouse, and ImGuizmo input. -*/ +/// @brief High-level runtime binder for consumers and viewport glue. +/// +/// It owns active runtime mappings and collects one frame of virtual events +/// from raw keyboard, mouse, and ImGuizmo input. class CGimbalInputBinder final : public IGimbalInputProcessor { public: @@ -25,7 +23,7 @@ class CGimbalInputBinder final : public IGimbalInputProcessor struct SCollectedVirtualEvents { - //! Concatenated output buffer plus per-domain counts for diagnostics. + /// @brief Concatenated output buffer plus per-domain counts for diagnostics. std::vector events; uint32_t keyboardCount = 0u; uint32_t mouseCount = 0u; @@ -37,7 +35,7 @@ class CGimbalInputBinder final : public IGimbalInputProcessor } }; - // Runtime input binder. It translates external keyboard/mouse/ImGuizmo input into virtual events. + /// @brief Translate one frame of external keyboard, mouse, and ImGuizmo input into virtual events. inline void clearActiveBindings() { updateKeyboardMapping([](auto& map) { map.clear(); }); diff --git a/common/include/camera/CPathCamera.hpp b/common/include/camera/CPathCamera.hpp index a51f6a05e..f929ac3be 100644 --- a/common/include/camera/CPathCamera.hpp +++ b/common/include/camera/CPathCamera.hpp @@ -10,33 +10,37 @@ namespace nbl::core { -//! Path-rig camera driven by typed `PathState` plus an injected path model. -//! -//! The public runtime contract stays event-only through `manipulate(...)`. -//! `CPathCamera` only interprets the accumulated impulse through `m_pathModel` -//! instead of hardcoding one default target-relative mapping in the method body. +/// @brief Path-rig camera driven by typed `PathState` plus an injected path model. +/// +/// The public runtime path stays event-only through `manipulate(...)`. +/// `CPathCamera` only interprets the accumulated impulse through `m_pathModel` +/// instead of hardcoding one default target-relative mapping in the method body. class CPathCamera final : public CSphericalTargetCamera { public: using base_t = CSphericalTargetCamera; using path_model_t = SCameraPathModel; + using path_limits_t = PathStateLimits; CPathCamera(const hlsl::float64_t3& position, const hlsl::float64_t3& target) - : base_t(position, target) + : CPathCamera(position, target, CCameraPathUtilities::makeDefaultPathModel(), CCameraPathUtilities::makeDefaultPathLimits()) { - m_pathModel = CCameraPathUtilities::makeDefaultPathModel(); - m_pathModel.resolveState(target, position, SCameraPathDefaults::Limits, nullptr, m_pathState); - updateFromPathState(); } CPathCamera(const hlsl::float64_t3& position, const hlsl::float64_t3& target, path_model_t pathModel) + : CPathCamera(position, target, std::move(pathModel), CCameraPathUtilities::makeDefaultPathLimits()) + { + } + + CPathCamera(const hlsl::float64_t3& position, const hlsl::float64_t3& target, path_limits_t pathLimits) + : CPathCamera(position, target, CCameraPathUtilities::makeDefaultPathModel(), pathLimits) + { + } + + CPathCamera(const hlsl::float64_t3& position, const hlsl::float64_t3& target, path_model_t pathModel, path_limits_t pathLimits) : base_t(position, target) - , m_pathModel(std::move(pathModel)) { - if (!m_pathModel.resolveState) - m_pathModel = CCameraPathUtilities::makeDefaultPathModel(); - m_pathModel.resolveState(target, position, SCameraPathDefaults::Limits, nullptr, m_pathState); - updateFromPathState(); + initializePathRig(position, std::move(pathModel), pathLimits); } ~CPathCamera() = default; @@ -60,7 +64,7 @@ class CPathCamera final : public CSphericalTargetCamera !m_pathModel.resolveState( m_targetPosition, hlsl::float64_t3(reference.frame[3]), - SCameraPathDefaults::Limits, + m_pathLimits, nullptr, nextPathState)) { @@ -75,18 +79,25 @@ class CPathCamera final : public CSphericalTargetCamera .rotation = scaleVirtualRotation(impulse.dVirtualRotation), .targetPosition = m_targetPosition, .reference = resolvedReference, - .limits = SCameraPathDefaults::Limits + .limits = m_pathLimits }; if (!m_pathModel.controlLaw || !m_pathModel.integrate) return false; const auto stateDelta = m_pathModel.controlLaw(context); - if (!m_pathModel.integrate(nextPathState, stateDelta, SCameraPathDefaults::Limits, nextPathState)) + if (!m_pathModel.integrate(nextPathState, stateDelta, m_pathLimits, nextPathState)) return false; + const auto previousPathState = m_pathState; m_pathState = nextPathState; - return updateFromPathState(); + bool manipulated = false; + if (refreshFromPathState(&manipulated)) + return manipulated; + + m_pathState = previousPathState; + refreshFromPathState(); + return false; } virtual uint32_t getAllowedVirtualEvents() const override { return AllowedVirtualEvents; } @@ -99,31 +110,75 @@ class CPathCamera final : public CSphericalTargetCamera return true; } + virtual bool tryGetPathStateLimits(PathStateLimits& out) const override + { + out = m_pathLimits; + return true; + } + + virtual bool tryGetSphericalTargetState(typename base_t::SphericalTargetState& out) const override + { + out.target = m_targetPosition; + out.distance = m_distance; + out.orbitUv = m_orbitUv; + out.minDistance = static_cast(m_pathLimits.minDistance); + out.maxDistance = static_cast(m_pathLimits.maxDistance); + return true; + } + + virtual bool trySetSphericalTarget(const hlsl::float64_t3& targetPosition) override + { + if (m_targetPosition == targetPosition) + return true; + + const auto previousTarget = m_targetPosition; + m_targetPosition = targetPosition; + if (refreshFromPathState()) + return true; + + m_targetPosition = previousTarget; + refreshFromPathState(); + return false; + } + virtual bool trySetPathState(const PathState& state) override { if (!m_pathModel.resolveState) return false; PathState sanitized = {}; - if (!m_pathModel.resolveState(m_targetPosition, m_gimbal.getPosition(), SCameraPathDefaults::Limits, &state, sanitized)) + if (!m_pathModel.resolveState(m_targetPosition, m_gimbal.getPosition(), m_pathLimits, &state, sanitized)) return false; const bool exact = CCameraPathUtilities::pathStatesNearlyEqual(sanitized, state, SCameraPathDefaults::ExactComparisonThresholds); + const auto previousState = m_pathState; m_pathState = sanitized; - updateFromPathState(); - return exact; + if (refreshFromPathState()) + return exact; + + m_pathState = previousState; + refreshFromPathState(); + return false; } virtual bool trySetSphericalDistance(float distance) override { SCameraPathDistanceUpdateResult distanceUpdate = {}; - if (!m_pathModel.updateDistance || - !m_pathModel.updateDistance(distance, SCameraPathDefaults::Limits, m_pathState, &distanceUpdate)) + if (!m_pathModel.updateDistance) { return false; } - updateFromPathState(); + const auto previousState = m_pathState; + if (!m_pathModel.updateDistance(distance, m_pathLimits, m_pathState, &distanceUpdate)) + return false; + if (!refreshFromPathState()) + { + m_pathState = previousState; + refreshFromPathState(); + return false; + } + return distanceUpdate.exact; } @@ -134,34 +189,113 @@ class CPathCamera final : public CSphericalTargetCamera return m_pathModel; } + inline const path_limits_t& getPathStateLimits() const + { + return m_pathLimits; + } + + inline bool setPathStateLimits(path_limits_t pathLimits) + { + if (!CCameraPathUtilities::sanitizePathLimits(pathLimits) || !m_pathModel.resolveState) + return false; + + PathState sanitizedState = {}; + if (!m_pathModel.resolveState(m_targetPosition, m_gimbal.getPosition(), pathLimits, &m_pathState, sanitizedState)) + return false; + + const auto previousLimits = m_pathLimits; + const auto previousState = m_pathState; + m_pathLimits = pathLimits; + m_pathState = sanitizedState; + if (refreshFromPathState()) + return true; + + m_pathLimits = previousLimits; + m_pathState = previousState; + refreshFromPathState(); + return false; + } + inline bool setPathModel(path_model_t pathModel) { - if (!pathModel.resolveState || !pathModel.controlLaw || !pathModel.integrate || !pathModel.evaluate || !pathModel.updateDistance) + if (!isPathModelComplete(pathModel)) return false; PathState sanitized = {}; - if (!pathModel.resolveState(m_targetPosition, m_gimbal.getPosition(), SCameraPathDefaults::Limits, &m_pathState, sanitized)) + if (!pathModel.resolveState(m_targetPosition, m_gimbal.getPosition(), m_pathLimits, &m_pathState, sanitized)) return false; + const auto previousModel = m_pathModel; + const auto previousState = m_pathState; m_pathModel = std::move(pathModel); m_pathState = sanitized; - return updateFromPathState(); + if (refreshFromPathState()) + return true; + + m_pathModel = previousModel; + m_pathState = previousState; + refreshFromPathState(); + return false; } private: static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate | CVirtualGimbalEvent::RollLeft | CVirtualGimbalEvent::RollRight; + static inline bool isPathModelComplete(const path_model_t& pathModel) + { + return pathModel.resolveState && pathModel.controlLaw && pathModel.integrate && pathModel.evaluate && pathModel.updateDistance; + } + + inline bool tryInitializePathRig(const hlsl::float64_t3& position, path_model_t pathModel, path_limits_t pathLimits) + { + if (!CCameraPathUtilities::sanitizePathLimits(pathLimits)) + return false; + + if (!isPathModelComplete(pathModel)) + return false; + + PathState resolvedState = {}; + if (!pathModel.resolveState(m_targetPosition, position, pathLimits, nullptr, resolvedState)) + return false; + + m_pathLimits = pathLimits; + m_pathModel = std::move(pathModel); + m_pathState = resolvedState; + return refreshFromPathState(); + } + + inline void initializePathRig(const hlsl::float64_t3& position, path_model_t pathModel, path_limits_t pathLimits) + { + path_limits_t sanitizedLimits = pathLimits; + const bool hasCustomLimits = CCameraPathUtilities::sanitizePathLimits(sanitizedLimits); + if (!hasCustomLimits) + sanitizedLimits = CCameraPathUtilities::makeDefaultPathLimits(); + + if (tryInitializePathRig(position, std::move(pathModel), sanitizedLimits)) + return; + + if (tryInitializePathRig(position, CCameraPathUtilities::makeDefaultPathModel(), sanitizedLimits)) + return; + + m_pathLimits = CCameraPathUtilities::makeDefaultPathLimits(); + m_pathModel = CCameraPathUtilities::makeDefaultPathModel(); + m_pathState = CCameraPathUtilities::makeDefaultPathState(m_pathLimits.minU); + m_pathModel.resolveState(m_targetPosition, position, m_pathLimits, nullptr, m_pathState); + refreshFromPathState(); + } + path_model_t m_pathModel = CCameraPathUtilities::makeDefaultPathModel(); - PathState m_pathState = CCameraPathUtilities::makeDefaultPathState(SCameraPathDefaults::Limits.minU); + path_limits_t m_pathLimits = CCameraPathUtilities::makeDefaultPathLimits(); + PathState m_pathState = CCameraPathUtilities::makeDefaultPathState(CCameraPathUtilities::makeDefaultPathLimits().minU); - bool updateFromPathState() + bool refreshFromPathState(bool* outManipulated = nullptr) { if (!m_pathModel.evaluate) return false; SCameraCanonicalPathState canonicalPathState = {}; - if (!m_pathModel.evaluate(m_targetPosition, m_pathState, SCameraPathDefaults::Limits, canonicalPathState)) + if (!m_pathModel.evaluate(m_targetPosition, m_pathState, m_pathLimits, canonicalPathState)) return false; m_distance = canonicalPathState.targetRelative.distance; @@ -178,7 +312,9 @@ class CPathCamera final : public CSphericalTargetCamera if (manipulated) m_gimbal.updateView(); - return manipulated; + if (outManipulated) + *outManipulated = manipulated; + return true; } }; diff --git a/common/include/camera/CSphericalTargetCamera.hpp b/common/include/camera/CSphericalTargetCamera.hpp index 34a7304a8..7094f9d1b 100644 --- a/common/include/camera/CSphericalTargetCamera.hpp +++ b/common/include/camera/CSphericalTargetCamera.hpp @@ -7,11 +7,9 @@ namespace nbl::core { -/** -* Common base for cameras orbiting or tracking a target with spherical coordinates. -* -* The shared state is target position, distance, and orbit angles stored in `orbitUv`. -*/ +/// @brief Common base for cameras orbiting or tracking a target with spherical coordinates. +/// +/// The shared state is target position, distance, and orbit angles stored in `orbitUv`. class CSphericalTargetCamera : public ICamera { public: diff --git a/common/include/camera/CVirtualGimbalEvent.hpp b/common/include/camera/CVirtualGimbalEvent.hpp index 27cbb1880..f3cf65ee0 100644 --- a/common/include/camera/CVirtualGimbalEvent.hpp +++ b/common/include/camera/CVirtualGimbalEvent.hpp @@ -11,12 +11,10 @@ namespace nbl::core { -/** -* Shared semantic camera command. -* -* Input processors and scripted tools emit these events. -* Camera implementations consume them through `ICamera::manipulate(...)`. -*/ +/// @brief Shared semantic camera command. +/// +/// Input processors and scripted tools emit these events. +/// Camera implementations consume them through `ICamera::manipulate(...)`. struct CVirtualGimbalEvent { enum VirtualEventType : uint32_t diff --git a/common/include/camera/ICamera.hpp b/common/include/camera/ICamera.hpp index 2f3662016..b483fad5c 100644 --- a/common/include/camera/ICamera.hpp +++ b/common/include/camera/ICamera.hpp @@ -13,13 +13,11 @@ namespace nbl::core { -/** -* Shared camera contract. -* -* The hot runtime path is event-only: cameras consume `CVirtualGimbalEvent` -* streams through `manipulate(...)`. Optional typed state hooks exist only for -* tooling features such as capture, compatibility analysis, presets, and playback. -*/ +/// @brief Shared camera interface. +/// +/// The hot runtime path is event-only: cameras consume `CVirtualGimbalEvent` +/// streams through `manipulate(...)`. Optional typed state hooks exist only for +/// tooling features such as capture, compatibility analysis, presets, and playback. class ICamera : virtual public core::IReferenceCounted { public: @@ -35,7 +33,7 @@ class ICamera : virtual public core::IReferenceCounted struct SMotionConfig { - //! Camera-local scales applied by implementations to virtual motion magnitude. + /// @brief Camera-local scales applied by implementations to virtual motion magnitude. double moveSpeedScale = DefaultMoveSpeedScale; double rotationSpeedScale = DefaultRotationSpeedScale; }; @@ -86,12 +84,19 @@ class ICamera : virtual public core::IReferenceCounted float referenceDistance = 0.f; }; - //! Parametric path-rig state used by the `Path Rig` camera kind. - //! - //! The default shared model interprets `(s, u, v, roll)` as angular progress, - //! radial component, vertical component, and view-axis roll around a target. - //! Concrete path models may reuse the same coordinates differently, but the - //! hot runtime contract still stays event-only through `manipulate(...)`. + struct PathStateLimits + { + double minU = static_cast(SphericalMinDistance); + hlsl::float64_t minDistance = static_cast(SphericalMinDistance); + hlsl::float64_t maxDistance = static_cast(SphericalMaxDistance); + }; + + /// @brief Parametric path-rig state used by the `Path Rig` camera kind. + /// + /// The default shared model interprets `(s, u, v, roll)` as angular progress, + /// radial component, vertical component, and view-axis roll around a target. + /// Concrete path models may reuse the same coordinates differently, while the + /// hot runtime path still stays event-only through `manipulate(...)`. struct PathState { double s = 0.0; @@ -130,7 +135,7 @@ class ICamera : virtual public core::IReferenceCounted } }; - //! Gimbal that models the camera pose and cached view matrix in world space. + /// @brief Gimbal that models the camera pose and cached view matrix in world space. class CGimbal : public IGimbal { public: @@ -201,7 +206,9 @@ class ICamera : virtual public core::IReferenceCounted virtual const CGimbal& getGimbal() = 0u; - // Camera core contract: consume virtual events only. Raw input binding and absolute goal solving live outside ICamera. + /// @brief Consume virtual events only. + /// + /// Raw input binding and absolute goal solving live outside `ICamera`. virtual bool manipulate(std::span virtualEvents, const hlsl::float64_t4x4* referenceFrame = nullptr) = 0; inline bool manipulateWithMotionScales(std::span virtualEvents, const hlsl::float64_t4x4* referenceFrame, const double moveScale, const double rotationScale) { @@ -274,6 +281,11 @@ class ICamera : virtual public core::IReferenceCounted return false; } + virtual bool tryGetPathStateLimits(PathStateLimits& out) const + { + return false; + } + virtual bool trySetPathState(const PathState& state) { return false; diff --git a/common/include/camera/IGimbal.hpp b/common/include/camera/IGimbal.hpp index e4f880008..be0b96fe4 100644 --- a/common/include/camera/IGimbal.hpp +++ b/common/include/camera/IGimbal.hpp @@ -21,7 +21,7 @@ namespace nbl::core using quaternion_t = hlsl::camera_quaternion_t; template using vector_t = hlsl::camera_vector_t; - //! underlying type for world matrix (TRS) + /// @brief underlying type for world matrix (TRS) using model_matrix_t = hlsl::matrix; struct VirtualImpulse @@ -29,7 +29,7 @@ namespace nbl::core vector_t<3u> dVirtualTranslate { 0.0f }, dVirtualRotation { 0.0f }, dVirtualScale { 1.0f }; }; - //! Accumulates one frame of virtual events into a translation/rotation/scale impulse. + /// @brief Accumulates one frame of virtual events into a translation/rotation/scale impulse. template VirtualImpulse accumulate(std::span virtualEvents, const vector_t<3u>& gRightOverride, const vector_t<3u>& gUpOverride, const vector_t<3u>& gForwardOverride) { @@ -223,16 +223,16 @@ namespace nbl::core m_isManipulating = false; } - //! Position of gimbal in world space + /// @brief Position of gimbal in world space inline const auto& getPosition() const { return m_position; } - //! Orientation of gimbal + /// @brief Orientation of gimbal inline const auto& getOrientation() const { return m_orientation; } - //! Scale transform component + /// @brief Scale transform component inline const auto& getScale() const { return m_scale; } - //! World matrix (TRS) + /// @brief World matrix (TRS) template requires is_any_of_v> const TRS operator()() const @@ -262,28 +262,28 @@ namespace nbl::core } } - //! Orthonormal [getXAxis(), getYAxis(), getZAxis()] orientation matrix + /// @brief Orthonormal [getXAxis(), getYAxis(), getZAxis()] orientation matrix inline const auto& getOrthonornalMatrix() const { return m_orthonormal; } - //! Base "right" vector in orthonormal orientation basis (X-axis) + /// @brief Base "right" vector in orthonormal orientation basis (X-axis) inline const auto& getXAxis() const { return m_orthonormal[0u]; } - //! Base "up" vector in orthonormal orientation basis (Y-axis) + /// @brief Base "up" vector in orthonormal orientation basis (Y-axis) inline const auto& getYAxis() const { return m_orthonormal[1u]; } - //! Base "forward" vector in orthonormal orientation basis (Z-axis) + /// @brief Base "forward" vector in orthonormal orientation basis (Z-axis) inline const auto& getZAxis() const { return m_orthonormal[2u]; } - //! Target vector in local space, alias for getZAxis() + /// @brief Target vector in local space, alias for getZAxis() inline const auto getLocalTarget() const { return getZAxis(); } - //! Target vector in world space + /// @brief Target vector in world space inline const auto getWorldTarget() const { return getPosition() + getLocalTarget(); } - //! Counts how many times a valid manipulation has been performed, the counter resets when begin() is called + /// @brief Counts how many times a valid manipulation has been performed, the counter resets when begin() is called inline const auto& getManipulationCounter() { return m_counter; } - //! Returns true if gimbal records a manipulation + /// @brief Returns true if gimbal records a manipulation inline bool isManipulating() const { return m_isManipulating; } bool extractReferenceTransform(CReferenceTransform* out, const hlsl::float64_t4x4* referenceFrame = nullptr) @@ -311,22 +311,22 @@ namespace nbl::core m_orthonormal = hlsl::CCameraMathUtilities::getQuaternionBasisMatrix(m_orientation); } - //! Position of a gimbal in world space + /// @brief Position of a gimbal in world space vector_t<3u> m_position; - //! Normalized orientation of gimbal + /// @brief Normalized orientation of gimbal quaternion_t m_orientation; - //! Scale transform component + /// @brief Scale transform component vector_t<3u> m_scale = { 1.f, 1.f , 1.f }; - //! Orthonormal basis reconstructed from the current orientation. + /// @brief Orthonormal basis reconstructed from the current orientation. hlsl::matrix m_orthonormal; - //! Counter that increments for each performed manipulation, resets with each begin() call + /// @brief Counter that increments for each performed manipulation, resets with each begin() call size_t m_counter = {}; - //! Tracks whether gimbal is currently in manipulation mode + /// @brief Tracks whether gimbal is currently in manipulation mode bool m_isManipulating = false; }; diff --git a/common/include/camera/IGimbalBindingLayout.hpp b/common/include/camera/IGimbalBindingLayout.hpp index 2bc8e86da..4686fc961 100644 --- a/common/include/camera/IGimbalBindingLayout.hpp +++ b/common/include/camera/IGimbalBindingLayout.hpp @@ -10,11 +10,9 @@ namespace nbl::ui { -/** -* Static mapping contract from external input domains to virtual gimbal events. -* -* This type stores binding layout only. It does not process runtime input. -*/ +/// @brief Static mapping from external input domains to virtual gimbal events. +/// +/// This type stores binding layout only. It does not process runtime input. struct IGimbalBindingLayout { IGimbalBindingLayout() {} @@ -100,7 +98,7 @@ struct IGimbalBindingLayout class CGimbalBindingLayoutStorage : public IGimbalBindingLayout { public: - //! Mutable storage for active or preset binding layout. + /// @brief Mutable storage for active or preset binding layout. using IGimbalBindingLayout::IGimbalBindingLayout; CGimbalBindingLayoutStorage() {} diff --git a/common/include/camera/IGimbalInputProcessor.hpp b/common/include/camera/IGimbalInputProcessor.hpp index 613352ea1..031e8cc02 100644 --- a/common/include/camera/IGimbalInputProcessor.hpp +++ b/common/include/camera/IGimbalInputProcessor.hpp @@ -12,9 +12,7 @@ namespace nbl::ui { -/** -* Runtime processor that turns keyboard, mouse, and ImGuizmo input into virtual events. -*/ +/// @brief Runtime processor that turns keyboard, mouse, and ImGuizmo input into virtual events. class IGimbalInputProcessor : public CGimbalBindingLayoutStorage { public: @@ -33,13 +31,13 @@ class IGimbalInputProcessor : public CGimbalBindingLayoutStorage IGimbalInputProcessor() = default; virtual ~IGimbalInputProcessor() = default; - //! Keyboard events consumed by the processor. + /// @brief Keyboard events consumed by the processor. using input_keyboard_event_t = ui::SKeyboardEvent; - //! Mouse events consumed by the processor. + /// @brief Mouse events consumed by the processor. using input_mouse_event_t = ui::SMouseEvent; - //! ImGuizmo world-space delta transforms consumed by the processor. + /// @brief ImGuizmo world-space delta transforms consumed by the processor. using input_imguizmo_event_t = hlsl::float32_t4x4; void beginInputProcessing(const std::chrono::microseconds nextPresentationTimeStamp) @@ -60,29 +58,16 @@ class IGimbalInputProcessor : public CGimbalBindingLayoutStorage std::span imguizmoEvents = {}; }; - /** - * @brief Processes combined events from SUpdateParameters to generate virtual manipulation events. - * - * @note This function combines the processing of events from keyboards, mouse and ImGuizmo. - * It delegates the actual processing to the respective functions: - * - @ref processKeyboard for keyboard events - * - @ref processMouse for mouse events - * - @ref processImguizmo for ImGuizmo events - * The results are accumulated into the output array and the total count. - * - * @param "output" is a pointer to the array where all generated gimbal events will be stored. - * If nullptr, the function will only calculate the total count of potential - * output events without processing. - * - * @param "count" is a uint32_t reference to store the total count of generated gimbal events. - * - * @param "parameters" is an SUpdateParameters structure containing the individual event arrays - * for keyboard, mouse, and ImGuizmo inputs. - * - * @return void. If "count" > 0 and "output" is a valid pointer, use it to dereference your "output" - * containing "count" events. If "output" is nullptr, "count" tells you the total size of "output" - * you must guarantee to be valid. - */ + /// @brief Process combined events from `SUpdateParameters` into virtual manipulation events. + /// + /// @note This function combines keyboard, mouse, and ImGuizmo processing. + /// It delegates the actual work to `processKeyboard`, `processMouse`, and + /// `processImguizmo`, then accumulates their output and total count. + /// + /// @param output Pointer to the destination array for generated gimbal events. + /// Pass `nullptr` to query only the total event count. + /// @param count Output total number of generated gimbal events. + /// @param parameters Individual keyboard, mouse, and ImGuizmo input spans. void process(gimbal_event_t* output, uint32_t& count, const SUpdateParameters parameters = {}) { count = 0u; @@ -104,25 +89,15 @@ class IGimbalInputProcessor : public CGimbalBindingLayoutStorage count = vKeyboardEventsCount + vMouseEventsCount + vImguizmoEventsCount; } - /** - * @brief Processes keyboard events to generate virtual manipulation events. - * - * @note This function maps keyboard events into virtual gimbal manipulation events - * based on predefined mappings. It supports event types such as key press and key release - * to trigger corresponding actions. - * - * @param "output" is a pointer to the array where generated gimbal events will be stored. - * If nullptr, the function will only calculate the count of potential - * output events without processing. - * - * @param "count" is a uint32_t reference to store the count of generated gimbal events. - * - * @param "events" is a span of input_keyboard_event_t. Each such event contains a key code and action, - * such as key press or release. - * - * @return void. If "count" > 0 and "output" is a valid pointer, use it to dereference your "output" - * containing "count" events. If "output" is nullptr, "count" tells you the size of "output" you must guarantee to be valid. - */ + /// @brief Process keyboard events into virtual manipulation events. + /// + /// @note This function maps keyboard press and release events into virtual + /// gimbal manipulation events through the active keyboard bindings. + /// + /// @param output Pointer to the destination array for generated gimbal events. + /// Pass `nullptr` to query only the total event count. + /// @param count Output number of generated gimbal events. + /// @param events Keyboard events to process. void processKeyboard(gimbal_event_t* output, uint32_t& count, std::span events) { processBindingMap( @@ -141,25 +116,15 @@ class IGimbalInputProcessor : public CGimbalBindingLayoutStorage }); } - /** - * @brief Processes mouse events to generate virtual manipulation events. - * - * @note This function processes mouse input, including clicks, scrolls, and movements, - * and maps them into virtual gimbal manipulation events. Mouse actions are processed - * using predefined mappings to determine corresponding gimbal manipulations. - * - * @param "output" is a pointer to the array where generated gimbal events will be stored. - * If nullptr, the function will only calculate the count of potential - * output events without processing. - * - * @param "count" is a uint32_t reference to store the count of generated gimbal events. - * - * @param "events" is a span of input_mouse_event_t. Each such event represents a mouse action, - * including clicks, scrolls, or movements. - * - * @return void. If "count" > 0 and "output" is a valid pointer, use it to dereference your "output" - * containing "count" events. If "output" is nullptr, "count" tells you the size of "output" you must guarantee to be valid. - */ + /// @brief Process mouse events into virtual manipulation events. + /// + /// @note This function maps mouse clicks, scrolls, and movements into + /// virtual gimbal manipulation events through the active mouse bindings. + /// + /// @param output Pointer to the destination array for generated gimbal events. + /// Pass `nullptr` to query only the total event count. + /// @param count Output number of generated gimbal events. + /// @param events Mouse events to process. void processMouse(gimbal_event_t* output, uint32_t& count, std::span events) { processBindingMap( @@ -203,26 +168,15 @@ class IGimbalInputProcessor : public CGimbalBindingLayoutStorage }); } - /** - * @brief Processes input events from ImGuizmo and generates virtual gimbal events. - * - * @note This function processes world-space delta transforms authored by ImGuizmo and converts - * them into virtual gimbal events for world-space camera manipulation. - * The function computes translation, rotation, and scale deltas from each transform matrix, - * which are then mapped to corresponding virtual events using a predefined mapping. - * - * @param "output" is pointer to the array where generated gimbal events will be stored. - * If nullptr, the function will only calculate the count of potential - * output events without processing. - * - * @param "count" is uint32_t reference to store the count of generated gimbal events. - * - * @param "events" is a span of input_imguizmo_event_t. Each such event contains a delta - * transformation matrix that represents changes in world space. - * - * @return void. If "count" > 0 & "output" was valid pointer then use it to dereference your "output" containing "count" events. - * If "output" is nullptr then "count" tells you about size of "output" you must guarantee to be valid. - */ + /// @brief Process ImGuizmo transforms into virtual gimbal events. + /// + /// @note This function converts world-space delta transforms authored by + /// ImGuizmo into translation, rotation, and scale virtual events. + /// + /// @param output Pointer to the destination array for generated gimbal events. + /// Pass `nullptr` to query only the total event count. + /// @param count Output number of generated gimbal events. + /// @param events ImGuizmo delta transforms to process. void processImguizmo(gimbal_event_t* output, uint32_t& count, std::span events) { processBindingMap( diff --git a/common/include/camera/ILinearProjection.hpp b/common/include/camera/ILinearProjection.hpp index 6e58964d3..251336c19 100644 --- a/common/include/camera/ILinearProjection.hpp +++ b/common/include/camera/ILinearProjection.hpp @@ -7,10 +7,10 @@ namespace nbl::core { -/** - * @brief Interface class for any custom linear projection transformation (matrix elements are already evaluated scalars) - * referencing a camera, great for Perspective, Orthographic, Oblique, Axonometric and Shear projections - */ +/// @brief Interface for any custom linear projection transformation. +/// +/// Matrix elements are already evaluated scalars referencing a camera. +/// This covers perspective, orthographic, oblique, axonometric, and shear projections. class ILinearProjection : virtual public core::IReferenceCounted { protected: @@ -20,13 +20,13 @@ class ILinearProjection : virtual public core::IReferenceCounted core::smart_refctd_ptr m_camera; public: - //! underlying type for linear world TRS matrix + /// @brief underlying type for linear world TRS matrix using model_matrix_t = typename decltype(m_camera)::pointee::CGimbal::model_matrix_t; - //! underlying type for linear concatenated matrix + /// @brief underlying type for linear concatenated matrix using concatenated_matrix_t = hlsl::float64_t4x4; - //! underlying type for linear inverse of concatenated matrix + /// @brief underlying type for linear inverse of concatenated matrix using inv_concatenated_matrix_t = std::optional; struct CProjection : public IProjection @@ -38,10 +38,10 @@ class ILinearProjection : virtual public core::IReferenceCounted CProjection() : CProjection(projection_matrix_t(1)) {} CProjection(const projection_matrix_t& matrix) { setProjectionMatrix(matrix); } - //! Returns P (Projection matrix) + /// @brief Returns P (Projection matrix) inline const projection_matrix_t& getProjectionMatrix() const { return m_projectionMatrix; } - //! Returns Pâ»Â¹ (Inverse of Projection matrix) *if it exists* + /// @brief Returns Pâ»Â¹ (Inverse of Projection matrix) *if it exists* inline const inv_projection_matrix_t& getInvProjectionMatrix() const { return m_invProjectionMatrix; } inline const std::optional& isProjectionLeftHanded() const { return m_isProjectionLeftHanded; } @@ -111,23 +111,21 @@ class ILinearProjection : virtual public core::IReferenceCounted return m_camera.get(); } - /** - * @brief Computes Model View (MV) matrix - * @param "model" is world TRS matrix - * @return Returns MV matrix - */ + /// @brief Compute the model-view matrix. + /// + /// @param model World TRS matrix. + /// @return The model-view matrix. inline concatenated_matrix_t getMV(const model_matrix_t& model) const { const auto& v = m_camera->getGimbal().getViewMatrix(); return hlsl::mul(hlsl::getMatrix3x4As4x4(v), hlsl::getMatrix3x4As4x4(model)); } - /** - * @brief Computes Model View Projection (MVP) matrix - * @param "projection" is linear projection - * @param "model" is world TRS matrix - * @return Returns MVP matrix - */ + /// @brief Compute the model-view-projection matrix from a model matrix. + /// + /// @param projection Linear projection. + /// @param model World TRS matrix. + /// @return The model-view-projection matrix. inline concatenated_matrix_t getMVP(const CProjection& projection, const model_matrix_t& model) const { const auto& v = m_camera->getGimbal().getViewMatrix(); @@ -136,23 +134,21 @@ class ILinearProjection : virtual public core::IReferenceCounted return hlsl::mul(p, mv); } - /** - * @brief Computes Model View Projection (MVP) matrix - * @param "projection" is linear projection - * @param "mv" is Model View (MV) matrix - * @return Returns MVP matrix - */ + /// @brief Compute the model-view-projection matrix from a model-view matrix. + /// + /// @param projection Linear projection. + /// @param mv Model-view matrix. + /// @return The model-view-projection matrix. inline concatenated_matrix_t getMVP(const CProjection& projection, const concatenated_matrix_t& mv) const { const auto& p = projection.getProjectionMatrix(); return hlsl::mul(p, mv); } - /** - * @brief Computes Inverse of Model View ((MV)â»Â¹) matrix - * @param "mv" is Model View (MV) matrix - * @return Returns ((MV)â»Â¹) matrix *if it exists*, otherwise returns std::nullopt - */ + /// @brief Compute the inverse model-view matrix. + /// + /// @param model World TRS matrix. + /// @return The inverse model-view matrix when it exists, otherwise `std::nullopt`. inline inv_concatenated_matrix_t getMVInverse(const model_matrix_t& model) const { const auto mv = getMV(model); @@ -161,12 +157,11 @@ class ILinearProjection : virtual public core::IReferenceCounted return std::nullopt; } - /** - * @brief Computes Inverse of Model View Projection ((MVP)â»Â¹) matrix - * @param "projection" is linear projection - * @param "model" is world TRS matrix - * @return Returns ((MVP)â»Â¹) matrix *if it exists*, otherwise returns std::nullopt - */ + /// @brief Compute the inverse model-view-projection matrix. + /// + /// @param projection Linear projection. + /// @param model World TRS matrix. + /// @return The inverse model-view-projection matrix when it exists, otherwise `std::nullopt`. inline inv_concatenated_matrix_t getMVPInverse(const CProjection& projection, const model_matrix_t& model) const { const auto mvp = getMVP(projection, model); diff --git a/common/include/camera/IPerspectiveProjection.hpp b/common/include/camera/IPerspectiveProjection.hpp index 2c319d0c0..85035d282 100644 --- a/common/include/camera/IPerspectiveProjection.hpp +++ b/common/include/camera/IPerspectiveProjection.hpp @@ -6,21 +6,19 @@ namespace nbl::core { -/** -* @brief Interface class for quad projections. -* -* This projection transforms a vector into the **model space of a perspective quad** -* (defined by the pre-transform matrix) and then projects it onto the perspective quad -* using the linear view-port transform. -* -* A perspective quad projection is represented by: -* - A **pre-transform matrix** (non-linear/skewed transformation). -* - A **linear view-port transform matrix**. -* -* The final projection matrix is the concatenation of the pre-transform and the linear view-port transform. -* -* @note Single perspective quad projection can represent a face quad of a CAVE-like system. -*/ +/// @brief Interface for quad projections. +/// +/// This projection transforms a vector into the model space of a perspective +/// quad defined by the pre-transform matrix and then projects it onto the quad +/// using the linear viewport transform. +/// +/// A perspective quad projection is represented by: +/// - a pre-transform matrix +/// - a linear viewport transform matrix +/// +/// The final projection matrix is the concatenation of those two transforms. +/// +/// @note One perspective quad projection can represent a face quad of a CAVE-like system. class IPerspectiveProjection : public ILinearProjection { public: @@ -59,4 +57,4 @@ class IPerspectiveProjection : public ILinearProjection } // nbl::hlsl namespace -#endif // _NBL_I_QUAD_PROJECTION_HPP_ \ No newline at end of file +#endif // _NBL_I_QUAD_PROJECTION_HPP_ diff --git a/common/include/camera/IPlanarProjection.hpp b/common/include/camera/IPlanarProjection.hpp index 8f58ce2b0..abd236686 100644 --- a/common/include/camera/IPlanarProjection.hpp +++ b/common/include/camera/IPlanarProjection.hpp @@ -7,16 +7,14 @@ namespace nbl::core { -/** -* Linear projection wrapper for one camera-facing planar viewport. -* -* The projection owns viewport-local binding layout storage, while runtime input -* processing is expected to happen through `CGimbalInputBinder`. -*/ +/// @brief Linear projection wrapper for one camera-facing planar viewport. +/// +/// The projection owns viewport-local binding layout storage, while runtime input +/// processing is expected to happen through `CGimbalInputBinder`. class IPlanarProjection : public ILinearProjection { public: - //! One perspective or orthographic projection entry plus its viewport-local bindings. + /// @brief One perspective or orthographic projection entry plus its viewport-local bindings. struct CProjection : public ILinearProjection::CProjection { using base_t = ILinearProjection::CProjection; diff --git a/common/include/camera/IProjection.hpp b/common/include/camera/IProjection.hpp index 3621784d0..32a252e52 100644 --- a/common/include/camera/IProjection.hpp +++ b/common/include/camera/IProjection.hpp @@ -6,25 +6,25 @@ namespace nbl::core { -//! Interface class for any type of projection +/// @brief Interface class for any type of projection class IProjection { public: - //! underlying type for all vectors we project or un-project (inverse), projections *may* transform vectors in less dimensions + /// @brief underlying type for all vectors we project or un-project (inverse), projections *may* transform vectors in less dimensions using projection_vector_t = hlsl::float64_t4; enum class ProjectionType { - //! Any raw linear transformation, for example it may represent Perspective, Orthographic, Oblique, Axonometric, Shear projections + /// @brief Any raw linear transformation, for example it may represent Perspective, Orthographic, Oblique, Axonometric, Shear projections Linear, - //! Specialized linear projection for planar projections with parameters + /// @brief Specialized linear projection for planar projections with parameters Planar, - //! Extension of planar projection represented by pre-transform & planar transform combined projecting onto R3 cave quad + /// @brief Extension of planar projection represented by pre-transform & planar transform combined projecting onto R3 cave quad CaveQuad, - //! Specialized CaveQuad projection, represents planar projections onto cube with 6 quad cube faces + /// @brief Specialized CaveQuad projection, represents planar projections onto cube with 6 quad cube faces Cube, Spherical, @@ -36,32 +36,27 @@ class IProjection IProjection() = default; virtual ~IProjection() = default; - /** - * @brief Transforms a vector from its input space into the projection space. - * - * @param "vecToProjectionSpace" is a vector to transform from its space into projection space. - * @param "output" is a vector which is "vecToProjectionSpace" transformed into projection space. - * @return void. "output" is the vector in projection space. - */ + /// @brief Transform a vector from its input space into projection space. + /// + /// @param vecToProjectionSpace Vector to transform into projection space. + /// @param output Result vector in projection space. virtual void project(const projection_vector_t& vecToProjectionSpace, projection_vector_t& output) const = 0; - /** - * @brief Transforms a vector from the projection space back to the original space. - * Note the inverse transform may fail because original projection may be singular. - * - * @param "vecFromProjectionSpace" is a vector in the projection space to transform back to original space. - * @param "output" is a vector which is "vecFromProjectionSpace" transformed back to its original space. - * @return true if inverse succeeded and then "output" is the vector in the original space. False otherwise. - */ + /// @brief Transform a vector from projection space back to the original space. + /// + /// The inverse transform may fail because the original projection may be singular. + /// + /// @param vecFromProjectionSpace Vector in projection space. + /// @param output Result vector in the original space. + /// @return `true` when the inverse transform succeeded, otherwise `false`. virtual bool unproject(const projection_vector_t& vecFromProjectionSpace, projection_vector_t& output) const = 0; - /** - * @brief Returns the specific type of the projection - * (e.g., linear, spherical, thin-lens) as defined by the - * ProjectionType enumeration. - * - * @return The type of this projection. - */ + /// @brief Return the specific type of the projection. + /// + /// Examples include linear, spherical, and thin-lens projections as defined + /// by `ProjectionType`. + /// + /// @return The type of this projection. virtual ProjectionType getProjectionType() const = 0; }; diff --git a/common/include/camera/IRange.hpp b/common/include/camera/IRange.hpp index 4f1e40c23..b4084db2a 100644 --- a/common/include/camera/IRange.hpp +++ b/common/include/camera/IRange.hpp @@ -4,7 +4,7 @@ namespace nbl::core { -//! Minimal concepts used by camera persistence and tooling helpers. +/// @brief Minimal concepts used by camera persistence and tooling helpers. template concept GeneralPurposeRange = requires { diff --git a/common/include/camera/README.md b/common/include/camera/README.md index 7b76c94a4..25179c439 100644 --- a/common/include/camera/README.md +++ b/common/include/camera/README.md @@ -48,14 +48,14 @@ The stack is split across existing Nabla namespaces: - `nbl::ui` binding layouts, input processors, binders, default input mappings, and user-facing presentation/text helpers - `nbl::system` - persistence, scripted runtime payloads, scripted parsing, scripted check execution, and follow-contract validation helpers + persistence, scripted runtime payloads, scripted parsing, scripted check execution, and follow validation helpers The shared camera math is written against `nbl::hlsl`. Consumers of this stack are expected to talk to camera math through `nbl::hlsl` types and helpers rather than through direct `glm::...` calls. ## Why virtual events and not absolute setters -The runtime contract is intentionally built around virtual events such as: +The runtime path is intentionally built around virtual events such as: - `MoveForward` - `PanLeft` @@ -70,7 +70,7 @@ instead of runtime methods like: The reason is architectural, not cosmetic. -### What the event-driven contract buys us +### What the event-driven design buys us It gives us one shared runtime path for: @@ -215,12 +215,12 @@ This layer: - [`ICamera.hpp`](ICamera.hpp) -This is the core contract that camera models implement. +This is the core interface that camera models implement. Important properties: - runtime entry point is `manipulate(...)` -- the runtime contract consumes virtual events only +- the runtime path consumes virtual events only - cameras own their own gimbal and view state - cameras expose typed optional state hooks only for tooling @@ -349,7 +349,7 @@ These extend the shared base with typed extra state: ## Capabilities and typed state -The core camera contract exposes: +The core camera interface exposes: - `CameraCapability` - `GoalStateMask` @@ -449,7 +449,7 @@ The current camera-focused validation is exercised through scripted smoke and co Purpose: - prove that camera selection and basic scripted manipulation still work -- validate preset, sequence, runtime, and follow helper contracts with small regression checks +- validate preset, sequence, runtime, and follow helper behavior with small regression checks ### Continuity diff --git a/common/src/nbl/examples/camera/CCameraScriptedRuntimePersistence.cpp b/common/src/nbl/examples/camera/CCameraScriptedRuntimePersistence.cpp index 85924271b..500804572 100644 --- a/common/src/nbl/examples/camera/CCameraScriptedRuntimePersistence.cpp +++ b/common/src/nbl/examples/camera/CCameraScriptedRuntimePersistence.cpp @@ -172,18 +172,15 @@ bool deserializeSequenceGoalDeltaJson(const json_t& root, nbl::core::CCameraSequ } if (root.contains("orbit_u_delta_deg")) { - out.orbitUvDeltaDeg.x = root["orbit_u_delta_deg"].get(); - out.hasOrbitUDeltaDeg = true; + out.orbitDelta.setUDeltaDeg(root["orbit_u_delta_deg"].get()); } if (root.contains("orbit_v_delta_deg")) { - out.orbitUvDeltaDeg.y = root["orbit_v_delta_deg"].get(); - out.hasOrbitVDeltaDeg = true; + out.orbitDelta.setVDeltaDeg(root["orbit_v_delta_deg"].get()); } if (root.contains("orbit_distance_delta")) { - out.orbitDistanceDelta = root["orbit_distance_delta"].get(); - out.hasOrbitDistanceDelta = true; + out.orbitDelta.setDistanceDelta(root["orbit_distance_delta"].get()); } if (root.contains("path_s_delta_deg")) { From 5c39ff160156e3bb8e10329d06e1d408f911381b Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Thu, 9 Apr 2026 17:46:31 +0200 Subject: [PATCH 195/205] Implement reference frame support across cameras --- 61_UI/AppCameraConfigJsonParsing.cpp | 5 - 61_UI/AppCameraConfiguration.cpp | 25 +- 61_UI/AppCameraPlanarRuntime.cpp | 5 - 61_UI/AppCameraUiState.cpp | 5 - 61_UI/AppControlPanelProjectionTab.cpp | 27 +- 61_UI/AppControlPanelTabs.cpp | 5 - 61_UI/AppDebugSceneRendererResources.cpp | 17 - 61_UI/AppFollowRuntime.cpp | 5 - 61_UI/AppHeadlessCameraSmoke.cpp | 7 +- 61_UI/AppHeadlessCameraSmokeChecks.inl | 416 +++++++++- 61_UI/AppHeadlessCameraSmokeHelpers.inl | 12 +- 61_UI/AppInputRuntime.cpp | 5 - 61_UI/AppManipulableObjects.cpp | 5 - 61_UI/AppResourceUtilities.cpp | 5 - 61_UI/AppSceneDebugInstances.cpp | 40 - 61_UI/AppSceneFramebufferResources.cpp | 5 - 61_UI/AppSceneRenderPasses.cpp | 24 +- 61_UI/AppSpaceEnvironmentResources.cpp | 5 - 61_UI/AppUiInputCapture.cpp | 5 - 61_UI/AppUiResources.cpp | 5 - 61_UI/AppUpdate.cpp | 5 - 61_UI/AppViewportWindows.cpp | 1 - 61_UI/README.md | 151 ++-- 61_UI/include/app/App.hpp | 4 +- .../AppProjectionControlPanelUiUtilities.hpp | 40 + 61_UI/include/app/AppTypes.hpp | 3 - 61_UI/include/common.hpp | 3 - 61_UI/src/keysmapping.cpp | 5 - common/include/camera/CArcballCamera.hpp | 21 + .../camera/CCameraControlPanelUiUtilities.hpp | 60 +- .../include/camera/CCameraFileUtilities.hpp | 7 + .../include/camera/CCameraFollowUtilities.hpp | 32 + common/include/camera/CCameraGoal.hpp | 27 + common/include/camera/CCameraPathMetadata.hpp | 9 + .../include/camera/CCameraPathUtilities.hpp | 29 + .../CCameraScriptedUiInputUtilities.hpp | 8 + .../CCameraViewportOverlayUtilities.hpp | 20 +- .../camera/CCameraVirtualEventUtilities.hpp | 15 + common/include/camera/CChaseCamera.hpp | 21 + common/include/camera/CDollyCamera.hpp | 21 + common/include/camera/CDollyZoomCamera.hpp | 28 + .../include/camera/CGeneralPurposeGimbal.hpp | 5 + common/include/camera/CIsometricCamera.hpp | 19 + common/include/camera/CLinearProjection.hpp | 8 + common/include/camera/COrbitCamera.hpp | 21 + common/include/camera/CPathCamera.hpp | 19 + common/include/camera/CPlanarProjection.hpp | 8 + .../include/camera/CSphericalTargetCamera.hpp | 84 ++ common/include/camera/CTopDownCamera.hpp | 19 + common/include/camera/CTurntableCamera.hpp | 20 + common/include/camera/ICamera.hpp | 75 ++ common/include/camera/IGimbal.hpp | 25 + common/include/camera/ILinearProjection.hpp | 12 +- .../include/camera/IPerspectiveProjection.hpp | 4 + common/include/camera/IPlanarProjection.hpp | 8 + common/include/camera/IProjection.hpp | 14 +- common/include/camera/README.md | 730 +++++++++--------- common/include/camera/SCameraRigPose.hpp | 8 + .../examples/camera/CCameraPersistence.cpp | 3 - .../CCameraScriptedRuntimePersistence.cpp | 3 - 60 files changed, 1572 insertions(+), 651 deletions(-) diff --git a/61_UI/AppCameraConfigJsonParsing.cpp b/61_UI/AppCameraConfigJsonParsing.cpp index 00b4e6238..13202421f 100644 --- a/61_UI/AppCameraConfigJsonParsing.cpp +++ b/61_UI/AppCameraConfigJsonParsing.cpp @@ -24,9 +24,6 @@ namespace nbl::system using camera_json_t = nlohmann::json; -namespace -{ - struct SCameraConfigJsonKeys final { static constexpr std::string_view Type = "type"; @@ -666,8 +663,6 @@ bool tryBuildCameraConfigCollections( return true; } -} // namespace - bool tryBuildCameraConfigCollections( const std::string_view text, SCameraConfigCollections& outCollections, diff --git a/61_UI/AppCameraConfiguration.cpp b/61_UI/AppCameraConfiguration.cpp index dd0e9ddbd..f1a38ad15 100644 --- a/61_UI/AppCameraConfiguration.cpp +++ b/61_UI/AppCameraConfiguration.cpp @@ -11,10 +11,6 @@ #include "camera/CCameraPersistence.hpp" #include "camera/CCameraScriptedRuntimePersistence.hpp" -namespace -{ -} // namespace - bool App::initializeCameraConfiguration(const argparse::ArgumentParser& program) { nbl::system::SCameraPlanarRuntimeBootstrap runtimeBootstrap = {}; @@ -95,6 +91,25 @@ bool App::initializePlanarRuntimeState( 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(); @@ -103,7 +118,7 @@ void App::initializePlanarFollowConfigs() for (uint32_t planarIx = 0u; planarIx < m_planarProjections.size(); ++planarIx) { auto* camera = m_planarProjections[planarIx] ? m_planarProjections[planarIx]->getCamera() : nullptr; - auto config = nbl::core::CCameraFollowUtilities::makeDefaultFollowConfig(camera); + 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 index 4f75e9317..075ce5dd6 100644 --- a/61_UI/AppCameraPlanarRuntime.cpp +++ b/61_UI/AppCameraPlanarRuntime.cpp @@ -3,9 +3,6 @@ #include #include -namespace -{ - template bool tryApplyProjectionBindingSelection( const std::optional& bindingIx, @@ -26,8 +23,6 @@ bool tryApplyProjectionBindingSelection( return true; } -} // namespace - namespace nbl::system { diff --git a/61_UI/AppCameraUiState.cpp b/61_UI/AppCameraUiState.cpp index 87135a0d8..41162624e 100644 --- a/61_UI/AppCameraUiState.cpp +++ b/61_UI/AppCameraUiState.cpp @@ -1,9 +1,6 @@ #include "app/App.hpp" #include "app/AppResourceUtilities.hpp" -namespace -{ - template inline bool tryBindActiveViewportContext(TContext& outContext, const SActiveViewportRuntimeState& viewportState) { @@ -26,8 +23,6 @@ inline SCameraFollowConfig* tryGetViewportFollowConfig( return &followConfigs[planarIx]; } -} // namespace - nbl::system::SCameraAppResourceContext App::getCameraAppResourceContext() const { return m_system ? diff --git a/61_UI/AppControlPanelProjectionTab.cpp b/61_UI/AppControlPanelProjectionTab.cpp index 24271ad93..0cc2a2a95 100644 --- a/61_UI/AppControlPanelProjectionTab.cpp +++ b/61_UI/AppControlPanelProjectionTab.cpp @@ -10,23 +10,32 @@ void App::drawControlPanelProjectionTab(const nbl::ui::SCameraControlPanelStyle& } ImGui::PushItemWidth(-1.0f); + nbl::ui::CCameraControlPanelUiUtilities::drawSectionHeader("PlanarSelectHeader", "Planar Selection", panelStyle.AccentColor, panelStyle); + SActiveProjectionTabContext runtime = {}; - if (!tryBuildActiveProjectionTabContext(runtime)) + auto refreshRuntime = [&]() -> bool + { + return tryBuildActiveProjectionTabContext(runtime); + }; + + if (!nbl::ui::drawRenderWindowSelector(m_viewports.windowBindings.size(), m_viewports.activeRenderWindowIx, refreshRuntime)) { - ImGui::TextDisabled("No active viewport."); ImGui::PopItemWidth(); nbl::ui::CCameraControlPanelUiUtilities::endControlPanelTabChild(); return; } + nbl::ui::CCameraControlPanelUiUtilities::drawHoverHint("Choose which render window the panel edits"); - nbl::ui::CCameraControlPanelUiUtilities::drawSectionHeader("PlanarSelectHeader", "Planar Selection", panelStyle.AccentColor, panelStyle); - ImGui::Text("Active Render Window: %s", runtime.activeRenderWindowIxString.c_str()); - nbl::ui::CCameraControlPanelUiUtilities::drawHoverHint("Window that receives input and camera switching"); - - auto refreshRuntime = [&]() -> bool + if (!refreshRuntime()) { - return tryBuildActiveProjectionTabContext(runtime); - }; + 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(); diff --git a/61_UI/AppControlPanelTabs.cpp b/61_UI/AppControlPanelTabs.cpp index 218a43fa4..322d6a775 100644 --- a/61_UI/AppControlPanelTabs.cpp +++ b/61_UI/AppControlPanelTabs.cpp @@ -4,9 +4,6 @@ #include #include -namespace -{ - using control_panel_style_t = nbl::ui::SCameraControlPanelStyle; enum class EControlPanelToggleBinding : uint8_t @@ -58,8 +55,6 @@ inline float calcControlPanelToggleRowWidth( return rowWidth; } -} // namespace - void App::drawControlPanelHeader(const nbl::ui::SCameraControlPanelStyle& panelStyle) { ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, panelStyle.CardChildRounding); diff --git a/61_UI/AppDebugSceneRendererResources.cpp b/61_UI/AppDebugSceneRendererResources.cpp index 3a4be7cd6..0b9fd5b56 100644 --- a/61_UI/AppDebugSceneRendererResources.cpp +++ b/61_UI/AppDebugSceneRendererResources.cpp @@ -25,23 +25,6 @@ bool App::initializeDebugSceneRendererResources() if (!m_debugScene.renderer) return logFail("Failed to create debug renderer!"); - const asset::SPushConstantRange singlePcRange = { - .stageFlags = IShader::E_SHADER_STAGE::ESS_VERTEX, - .offset = offsetof(ext::frustum::PushConstants, spc), - .size = sizeof(ext::frustum::SSinglePC) - }; - - ext::frustum::CDrawFrustum::SCreationParameters frustumParams = {}; - frustumParams.transfer = getTransferUpQueue(); - frustumParams.assetManager = m_assetMgr; - frustumParams.drawMode = ext::frustum::CDrawFrustum::DrawMode::DM_SINGLE; - frustumParams.singlePipelineLayout = ext::frustum::CDrawFrustum::createPipelineLayoutFromPCRange(m_device.get(), singlePcRange); - frustumParams.renderpass = core::smart_refctd_ptr(m_debugScene.renderpass); - frustumParams.utilities = m_utils; - m_debugScene.frustumDrawer = ext::frustum::CDrawFrustum::create(std::move(frustumParams)); - if (!m_debugScene.frustumDrawer) - return logFail("Failed to create frustum drawer."); - const auto& pipelines = m_debugScene.renderer->getInitParams().pipelines; m_debugScene.gridGeometryIx = std::nullopt; m_debugScene.followTargetGeometryIx = std::nullopt; diff --git a/61_UI/AppFollowRuntime.cpp b/61_UI/AppFollowRuntime.cpp index 2bcbdf3c2..5cf15e034 100644 --- a/61_UI/AppFollowRuntime.cpp +++ b/61_UI/AppFollowRuntime.cpp @@ -1,8 +1,5 @@ #include "app/App.hpp" -namespace -{ - inline float getScriptVisualDebugFps(const SScriptedInputRuntimeState& scriptedInput) { return std::max(1.f, scriptedInput.visualTargetFps); @@ -70,8 +67,6 @@ inline float getFollowTargetMarkerScale(const SScriptedInputRuntimeState& script SCameraAppSceneDefaults::FollowTargetMarkerScale; } -} // namespace - void App::setFollowTargetTransform(const float64_t4x4& transform) { m_sceneInteraction.followTarget.trySetFromTransform(transform); diff --git a/61_UI/AppHeadlessCameraSmoke.cpp b/61_UI/AppHeadlessCameraSmoke.cpp index 3490f9990..3611b0e8e 100644 --- a/61_UI/AppHeadlessCameraSmoke.cpp +++ b/61_UI/AppHeadlessCameraSmoke.cpp @@ -75,7 +75,12 @@ bool App::runHeadlessCameraSmoke(argparse::ArgumentParser& program, smart_refctd .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, @@ -89,7 +94,7 @@ bool App::runHeadlessCameraSmoke(argparse::ArgumentParser& program, smart_refctd resolvedSmokeState, { cameras.data(), cameras.size() }, { smokePlanars.data(), smokePlanars.size() }, - [](ICamera* camera) { return nbl::core::CCameraFollowUtilities::makeDefaultFollowConfig(camera); }, + [this](ICamera* camera) { return makeExampleDefaultFollowConfig(camera); }, [](const CTrackedTarget& trackedTarget, const std::string_view label, std::string& error) { return verifyFollowTargetMarkerAlignmentForSmoke(trackedTarget, label, error); diff --git a/61_UI/AppHeadlessCameraSmokeChecks.inl b/61_UI/AppHeadlessCameraSmokeChecks.inl index 169452ee7..7e2c4f111 100644 --- a/61_UI/AppHeadlessCameraSmokeChecks.inl +++ b/61_UI/AppHeadlessCameraSmokeChecks.inl @@ -3,7 +3,12 @@ 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; @@ -165,6 +170,411 @@ 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, + ICamera::SphericalMinDistance, + ICamera::SphericalMaxDistance, + 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 (!nbl::core::CCameraManipulationUtilities::applyReferenceFrameToCamera(camera, referenceFrame)) + { + outError = std::string(label) + " reference-frame smoke failed to apply the reference pose."; + 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 (!nbl::core::CCameraManipulationUtilities::applyReferenceFrameToCamera(camera, baselineReferenceFrame)) + { + outError = std::string(label) + " reference-frame smoke failed to restore the baseline reference pose."; + 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 (!nbl::core::CCameraManipulationUtilities::applyReferenceFrameToCamera(camera, referenceFrame)) + { + outError = std::string(label) + " reference-frame smoke failed to apply the rigid reference pose."; + 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 (!nbl::core::CCameraManipulationUtilities::applyReferenceFrameToCamera(camera, baselineReferenceFrame)) + { + outError = std::string(label) + " reference-frame smoke failed to restore the baseline rigid pose."; + 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 (!nbl::core::CCameraManipulationUtilities::applyReferenceFrameToCamera(state.pathCamera, referenceFrame)) + { + outError = "Path reference-frame smoke failed to apply the projected path pose."; + 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 (!nbl::core::CCameraManipulationUtilities::applyReferenceFrameToCamera(state.pathCamera, baselineReferenceFrame)) + { + outError = "Path reference-frame smoke failed to restore the baseline reference pose."; + return false; + } + } + + return true; + } + inline bool verifyPersistenceAndPlaybackSmoke( const SCameraSmokeResolvedState& state, std::string& outError) @@ -956,6 +1366,9 @@ } } + 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(); @@ -1124,7 +1537,7 @@ if (!defaultFollowCamera) continue; - auto followConfig = nbl::core::CCameraFollowUtilities::makeDefaultFollowConfig(defaultFollowCamera); + auto followConfig = makeDefaultFollowConfig(defaultFollowCamera); if (!followConfig.enabled || followConfig.mode == ECameraFollowMode::Disabled) continue; @@ -1214,5 +1627,4 @@ return true; } -} diff --git a/61_UI/AppHeadlessCameraSmokeHelpers.inl b/61_UI/AppHeadlessCameraSmokeHelpers.inl index c5a87a898..8dc5a8c58 100644 --- a/61_UI/AppHeadlessCameraSmokeHelpers.inl +++ b/61_UI/AppHeadlessCameraSmokeHelpers.inl @@ -1,5 +1,3 @@ -namespace -{ using camera_json_t = nlohmann::json; using CameraPreset = nbl::core::CCameraPreset; constexpr double CameraTinyScalarEpsilon = nbl::system::SCameraSmokeComparisonThresholds::TinyScalarEpsilon; @@ -156,7 +154,12 @@ namespace 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; @@ -369,7 +372,12 @@ namespace 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), diff --git a/61_UI/AppInputRuntime.cpp b/61_UI/AppInputRuntime.cpp index a3cc955df..876f51ea8 100644 --- a/61_UI/AppInputRuntime.cpp +++ b/61_UI/AppInputRuntime.cpp @@ -2,9 +2,6 @@ #include -namespace -{ - struct SCollectedCameraVirtualEvents final { std::vector events = {}; @@ -165,8 +162,6 @@ inline void applyCollectedVirtualEventsToCamera( appendVirtualEventLog(target, planarIx, collectedVirtualEvents); } -} // namespace - void App::applyActiveCameraInput( std::span keyboardEvents, std::span mouseEvents, diff --git a/61_UI/AppManipulableObjects.cpp b/61_UI/AppManipulableObjects.cpp index aba9010d4..06117665b 100644 --- a/61_UI/AppManipulableObjects.cpp +++ b/61_UI/AppManipulableObjects.cpp @@ -1,8 +1,5 @@ #include "app/App.hpp" -namespace -{ - inline float32_t4x4 buildModelManipulationTransform(const float32_t3x4& model) { return hlsl::transpose(getMatrix3x4As4x4(model)); @@ -33,8 +30,6 @@ inline float32_t3 buildFollowTargetWorldPosition(const CTrackedTarget& trackedTa return getCastedVector(trackedTarget.getGimbal().getPosition()); } -} // namespace - uint32_t App::getManipulableObjectCount() const { return SCameraAppSceneDefaults::CameraObjectIxOffset + static_cast(m_planarProjections.size()); diff --git a/61_UI/AppResourceUtilities.cpp b/61_UI/AppResourceUtilities.cpp index 1095a71b7..7d06e9dcb 100644 --- a/61_UI/AppResourceUtilities.cpp +++ b/61_UI/AppResourceUtilities.cpp @@ -7,9 +7,6 @@ #include "app/AppResourcePathUtilities.hpp" #include "camera/CCameraFileUtilities.hpp" -namespace -{ - inline bool parseSpaceEnvBlobBytes( std::span blobBytes, nbl::system::SSpaceEnvBlobHeader& outHeader, @@ -53,8 +50,6 @@ inline bool loadSpaceEnvBlob( return parseSpaceEnvBlobBytes(blobBytes, outHeader, outPayload); } -} // namespace - namespace nbl::system { diff --git a/61_UI/AppSceneDebugInstances.cpp b/61_UI/AppSceneDebugInstances.cpp index e796d74f8..b64f15f50 100644 --- a/61_UI/AppSceneDebugInstances.cpp +++ b/61_UI/AppSceneDebugInstances.cpp @@ -1,45 +1,5 @@ #include "app/App.hpp" -std::optional App::findFrustumSourceBindingIx(const uint32_t planarIx) const -{ - if (m_viewports.activeRenderWindowIx < m_viewports.windowBindings.size()) - { - const auto& activeBinding = m_viewports.windowBindings[m_viewports.activeRenderWindowIx]; - if (activeBinding.activePlanarIx == planarIx && activeBinding.boundProjectionIx.has_value()) - return m_viewports.activeRenderWindowIx; - } - - for (uint32_t bindingIx = 0u; bindingIx < m_viewports.windowBindings.size(); ++bindingIx) - { - const auto& binding = m_viewports.windowBindings[bindingIx]; - if (binding.activePlanarIx != planarIx) - continue; - if (!binding.boundProjectionIx.has_value()) - continue; - return bindingIx; - } - - return std::nullopt; -} - -std::optional App::tryBuildFrustumOverlaySourceBindingIx() const -{ - if (m_sceneInteraction.boundPlanarCameraIxToManipulate.has_value()) - { - if (const auto sourceBindingIx = findFrustumSourceBindingIx(m_sceneInteraction.boundPlanarCameraIxToManipulate.value()); sourceBindingIx.has_value()) - return sourceBindingIx; - } - - if (m_viewports.activeRenderWindowIx < m_viewports.windowBindings.size()) - { - const auto& activeBinding = m_viewports.windowBindings[m_viewports.activeRenderWindowIx]; - if (activeBinding.boundProjectionIx.has_value() && activeBinding.activePlanarIx < m_planarProjections.size()) - return m_viewports.activeRenderWindowIx; - } - - return std::nullopt; -} - void App::updateAuxSceneInstances(const size_t geometryCount) { const uint32_t gridInstanceIx = SCameraAppSceneDefaults::CameraObjectIxOffset - 1u; diff --git a/61_UI/AppSceneFramebufferResources.cpp b/61_UI/AppSceneFramebufferResources.cpp index 6d4f42c09..8ec5b86b3 100644 --- a/61_UI/AppSceneFramebufferResources.cpp +++ b/61_UI/AppSceneFramebufferResources.cpp @@ -1,8 +1,5 @@ #include "app/App.hpp" -namespace -{ - smart_refctd_ptr createSceneAttachmentView(ILogicalDevice* device, E_FORMAT format, uint32_t width, uint32_t height, const char* debugName) { if (!device) @@ -60,8 +57,6 @@ smart_refctd_ptr createSceneFramebuffer( return device->createFramebuffer(std::move(params)); } -} // namespace - bool App::initializeSceneRenderpass() { IGPURenderpass::SCreationParams params = {}; diff --git a/61_UI/AppSceneRenderPasses.cpp b/61_UI/AppSceneRenderPasses.cpp index 4219bd637..bc502a065 100644 --- a/61_UI/AppSceneRenderPasses.cpp +++ b/61_UI/AppSceneRenderPasses.cpp @@ -1,7 +1,7 @@ #include "app/App.hpp" #include "app/AppRenderPassUtilities.hpp" -bool App::recordSceneFramebufferPass(IGPUCommandBuffer* cmdbuf, SWindowControlBinding& binding, const uint32_t bindingIx) +bool App::recordSceneFramebufferPass(IGPUCommandBuffer* cmdbuf, SWindowControlBinding& binding, const uint32_t) { if (!cmdbuf || !binding.sceneFramebuffer) return true; @@ -43,28 +43,6 @@ bool App::recordSceneFramebufferPass(IGPUCommandBuffer* cmdbuf, SWindowControlBi const auto viewParams = CSimpleDebugRenderer::SViewParams(binding.viewMatrix, binding.viewProjMatrix); m_debugScene.renderer->render(cmdbuf, viewParams); - - const bool drawScriptFrustum = m_scriptedInput.enabled && m_scriptedInput.visualDebug; - if (!m_debugScene.frustumDrawer || !drawScriptFrustum) - return finalize(); - - const auto sourceBindingIx = tryBuildFrustumOverlaySourceBindingIx(); - if (!sourceBindingIx.has_value()) - return finalize(); - - const auto& sourceBinding = m_viewports.windowBindings[sourceBindingIx.value()]; - const bool sameCameraAsView = binding.activePlanarIx == sourceBinding.activePlanarIx; - const bool sameWindow = bindingIx == sourceBindingIx.value(); - if (sameCameraAsView || sameWindow) - return finalize(); - - ext::frustum::CDrawFrustum::DrawParameters drawParams = {}; - drawParams.commandBuffer = cmdbuf; - drawParams.viewProjectionMatrix = binding.viewProjMatrix; - drawParams.lineWidth = SCameraAppSceneDebugDefaults::FrustumLineWidth; - - const float32_t4 color = SCameraAppFrameRuntimeDefaults::FrustumColor; - success = success && m_debugScene.frustumDrawer->renderSingle(drawParams, hlsl::inverse(sourceBinding.viewProjMatrix), color); return finalize(); } diff --git a/61_UI/AppSpaceEnvironmentResources.cpp b/61_UI/AppSpaceEnvironmentResources.cpp index 4375856c5..b2df564b0 100644 --- a/61_UI/AppSpaceEnvironmentResources.cpp +++ b/61_UI/AppSpaceEnvironmentResources.cpp @@ -2,9 +2,6 @@ #include "app/AppResourceUtilities.hpp" -namespace -{ - struct SSpaceEnvironmentTextureSpec final { E_FORMAT format = EF_R16G16B16A16_SFLOAT; @@ -37,8 +34,6 @@ inline SSpaceEnvironmentTextureSpec buildSpaceEnvironmentTextureSpec(const nbl:: return textureSpec; } -} // namespace - bool App::initializeSpaceEnvironmentResources() { nbl::system::SSpaceEnvBlobHeader envBlobHeader = {}; diff --git a/61_UI/AppUiInputCapture.cpp b/61_UI/AppUiInputCapture.cpp index 3861f52d0..def9b6c1f 100644 --- a/61_UI/AppUiInputCapture.cpp +++ b/61_UI/AppUiInputCapture.cpp @@ -1,8 +1,5 @@ #include "app/App.hpp" -namespace -{ - template inline void appendFocusedChannelEvents( ChannelReader& reader, @@ -39,8 +36,6 @@ inline void scaleCapturedMouseEvents( } } -} // namespace - void App::updatePresentationTiming() { m_inputSystem->getDefaultMouse(&mouse); diff --git a/61_UI/AppUiResources.cpp b/61_UI/AppUiResources.cpp index bba35599d..d3bb72726 100644 --- a/61_UI/AppUiResources.cpp +++ b/61_UI/AppUiResources.cpp @@ -1,9 +1,6 @@ #include "app/App.hpp" #include "app/AppResourceUtilities.hpp" -namespace -{ - template struct SUiSampledDescriptorWrites final { @@ -103,8 +100,6 @@ inline void initializeViewportLayoutFromDisplaySize( } } -} // namespace - bool App::updateGUIDescriptorSet() { auto sampledWrites = buildUiSampledDescriptorWrites( diff --git a/61_UI/AppUpdate.cpp b/61_UI/AppUpdate.cpp index 9af1ea0c9..5f829cef4 100644 --- a/61_UI/AppUpdate.cpp +++ b/61_UI/AppUpdate.cpp @@ -1,8 +1,5 @@ #include "app/App.hpp" -namespace -{ - inline void logScriptedFramePayload( ILogger& logger, const uint64_t frameIx, @@ -19,8 +16,6 @@ inline void logScriptedFramePayload( scriptedFrame.frameEvents.trackedTargetTransforms.size()); } -} // namespace - SAppFrameUpdateState App::buildFrameUpdateState() { SAppFrameUpdateState frameState = {}; diff --git a/61_UI/AppViewportWindows.cpp b/61_UI/AppViewportWindows.cpp index 0da195974..55d957c7e 100644 --- a/61_UI/AppViewportWindows.cpp +++ b/61_UI/AppViewportWindows.cpp @@ -82,7 +82,6 @@ void App::drawViewportWindowOverlay( 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)); - overlayData.detail = "Frustum: active camera (hidden in owner view)"; nbl::ui::CCameraViewportOverlayUtilities::drawViewportInfoOverlay(drawList, viewportRect, overlayData); } diff --git a/61_UI/README.md b/61_UI/README.md index 1465f50fc..c148dfd33 100644 --- a/61_UI/README.md +++ b/61_UI/README.md @@ -1,50 +1,49 @@ # 61_UI Cameraz -`61_UI` is a full integration example and validation target for the shared camera stack in -[`../common/include/camera`](../common/include/camera/README.md). +`61_UI` is the full runnable integration and validation target for the shared camera stack documented in [`../common/include/camera/README.md`](../common/include/camera/README.md). -If you want the architecture, design rationale, and reusable API breakdown, start there first. +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 -It is used to: +`61_UI` is used to: -- exercise all current camera models in one scene -- validate the shared input, goal, preset, playback, follow, and scripting layers -- provide an interactive manual playground +- exercise all current camera kinds in one visible scene +- validate the shared input, goal, preset, playback, follow, and scripted layers +- provide a manual playground for camera behavior - provide CI-oriented smoke and continuity coverage -The example is intentionally not the source of truth for camera semantics. -Its job is to consume the shared camera APIs and expose them through a visible, testable UI. +It is intentionally not the source of truth for camera semantics. +Its job is to consume the shared camera API and expose it through one concrete, testable app. -## What `61_UI` contributes locally +## What `61_UI` owns locally -The reusable camera layer stops at shared camera-domain interfaces. -`61_UI` adds the local glue needed to turn that into an example application: +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 -- ImGui control panel and transform editor +- planar / window routing +- ImGui control panel +- transform editor and gizmo glue - screenshot capture -- scripted visual-debug HUD -- local logging and failure reporting +- runtime logging and failure reporting +- local visual-debug presentation -That means the shared camera layer owns: +The shared layer owns: - camera semantics - follow semantics -- compact sequence semantics +- compact sequence authoring - scripted runtime payloads - scripted check semantics -and `61_UI` owns: +`61_UI` owns how those pieces are presented, visualized, and driven in one sample. -- how those things are presented and driven in one concrete sample +## Camera set -## Cameras in the scene - -`app_resources/cameras.json` configures the currently showcased camera set: +`app_resources/cameras.json` configures the showcased cameras. +The current set is: - FPS - Orbit @@ -58,41 +57,41 @@ and `61_UI` owns: - DollyZoom - Path Rig -These are exposed through the active planar/view configuration in the example UI. +These are exposed through the active planar / viewport configuration in the UI. -## Follow target in `61_UI` +## Follow target `61_UI` exposes one tracked target in the default scene. Important rule: -- the tracked target is the reusable `CTrackedTarget` gimbal -- it is not the large cone -- it is not any scene object id -- the rendered marker is only a visualization of that gimbal +- 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 is important because the shared follow layer is intentionally modeled around: +This matters because the shared follow layer is modeled around: -- tracked target pose +- tracked-target pose - follow mode - follow config -and not around a mesh reference. +and not around a scene-node or mesh id. -### Default follow usage in the scene +### Default follow usage -Current default setup: +The default scene uses: - `Orbit`, `Arcball`, `Turntable`, `TopDown`, `Isometric`, `DollyZoom`, `Path Rig` - use `OrbitTarget` + with `OrbitTarget` - `Chase`, `Dolly` - use `KeepLocalOffset` + with `KeepLocalOffset` -Manual runtime and scripted continuity both drive the same follow layer. +Manual runtime and scripted continuity both drive the same shared follow layer. ## Scripted assets -`61_UI` currently uses two camera-focused scripted assets: +`61_UI` currently ships two camera-focused scripted assets: - `app_resources/cameraz_smoke_all.json` - `app_resources/cameraz_continuity.json` @@ -102,78 +101,90 @@ Manual runtime and scripted continuity both drive the same follow layer. Purpose: - validate basic camera selection and movement -- validate helper behavior in a small, cheap run +- validate shared helpers in a short, cheap run ### Continuity Purpose: - validate smooth frame-to-frame motion -- validate tracked-target follow lock during scripted target motion +- validate follow lock while the tracked target moves - provide a readable visual-debug showcase -The continuity asset is now a compact authored camera-sequence script. +The continuity asset is a compact authored camera-sequence script. It is no longer a giant committed frame dump. -## Shared pieces consumed by `61_UI` +## Shared pieces consumed directly by `61_UI` -The example now consumes these shared scripting and follow pieces directly: +`61_UI` consumes the shared stack directly rather than carrying private copies of camera logic: +- [`CCameraInputBindingUtilities.hpp`](../common/include/camera/CCameraInputBindingUtilities.hpp) +- [`CCameraPresetFlow.hpp`](../common/include/camera/CCameraPresetFlow.hpp) +- [`CCameraFollowUtilities.hpp`](../common/include/camera/CCameraFollowUtilities.hpp) +- [`CCameraFollowRegressionUtilities.hpp`](../common/include/camera/CCameraFollowRegressionUtilities.hpp) - [`CCameraSequenceScript.hpp`](../common/include/camera/CCameraSequenceScript.hpp) - [`CCameraScriptedRuntime.hpp`](../common/include/camera/CCameraScriptedRuntime.hpp) - [`CCameraScriptedRuntimePersistence.hpp`](../common/include/camera/CCameraScriptedRuntimePersistence.hpp) - [`CCameraSequenceScriptedBuilder.hpp`](../common/include/camera/CCameraSequenceScriptedBuilder.hpp) - [`CCameraScriptedCheckRunner.hpp`](../common/include/camera/CCameraScriptedCheckRunner.hpp) -- [`CCameraFollowUtilities.hpp`](../common/include/camera/CCameraFollowUtilities.hpp) -- [`CCameraFollowRegressionUtilities.hpp`](../common/include/camera/CCameraFollowRegressionUtilities.hpp) -That means `61_UI` no longer owns a private scripting model or private follow math. +That means `61_UI` does not own a private scripting model, private follow math, or private camera restore logic. -## Manual usage +## Local build and run -Typical manual workflow: +Current local setup uses the Visual Studio 2022 dynamic preset. -1. Pick a camera/planar in the UI. -2. Manipulate the camera through mouse, keyboard, or ImGuizmo-backed controls. -3. Use presets and playback tools if needed. -4. Move the tracked target marker. -5. Observe how follow-enabled cameras react. +Configure: -## CI and validation +```powershell +cmake --preset user-configure-dynamic-msvc +``` -`CMakeLists.txt` registers two dedicated tests: +Build: -- `NBL_61_UI_CAMERA_SMOKE` -- `NBL_61_UI_CAMERA_CONTINUITY` +```powershell +cmake --build build/dynamic/examples_tests/61_UI --config Debug --target 61_ui -- /m:1 +``` -Run from `build_vs2026/examples_tests/61_UI`: +Run tests: ```powershell -ctest -C Debug --output-on-failure -R NBL_61_UI_CAMERA_ +ctest --test-dir build/dynamic/examples_tests/61_UI -C Debug --output-on-failure -R NBL_61_UI_CAMERA_ ``` -## Build and run +Run the example: -Build: +```powershell +examples_tests/61_UI/bin/61_ui_d.exe +``` + +Run CI-style screenshot capture: ```powershell -cmake --build build_vs2026/examples_tests/61_UI --config Debug --target 61_ui -- /m:1 +examples_tests/61_UI/bin/61_ui_d.exe --ci ``` -Run manual smoke-style playback: +Run smoke-style scripted playback: ```powershell -./61_ui_d.exe --script app_resources/cameraz_smoke_all.json --script-log +examples_tests/61_UI/bin/61_ui_d.exe --script app_resources/cameraz_smoke_all.json --script-log ``` -Run continuity in CI-style mode: +Run continuity with visual debug: ```powershell -./61_ui_d.exe --ci --script app_resources/cameraz_continuity.json --script-log --script-visual-debug +examples_tests/61_UI/bin/61_ui_d.exe --ci --script app_resources/cameraz_continuity.json --script-log --script-visual-debug ``` -Notes: +## 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 -- continuity visual run is about `47 s` -- `visual_debug` can also be authored in JSON -- the compact continuity asset stays camera-domain and reusable instead of storing example-specific frame dumps +`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/include/app/App.hpp b/61_UI/include/app/App.hpp index 8f97639ac..ad3369c47 100644 --- a/61_UI/include/app/App.hpp +++ b/61_UI/include/app/App.hpp @@ -23,7 +23,6 @@ #include "app/AppTypes.hpp" #include "app/AppViewportBindingUtilities.hpp" #include "camera/CCubeProjection.hpp" -#include "nbl/ext/Frustum/CDrawFrustum.h" #include "nbl/ext/FullScreenTriangle/FullScreenTriangle.h" #include "nbl/ext/ScreenShot/ScreenShot.h" #include "nbl/this_example/builtin/build/spirv/keys.hpp" @@ -140,6 +139,7 @@ class App final : public examples::SimpleWindowedApplication, public examples::B 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); @@ -247,8 +247,6 @@ class App final : public examples::SimpleWindowedApplication, public examples::B void logScriptedCameraPose(const char* label, ICamera* camera) const; void updateScriptedFollowVisualState(const CCameraScriptedFrameEvents& scriptedFrameEvents); void runActiveFrameScriptedChecks(const SScriptedFrameInputState& scriptedFrame); - std::optional findFrustumSourceBindingIx(uint32_t planarIx) const; - std::optional tryBuildFrustumOverlaySourceBindingIx() const; void updateSceneDebugInstances(); void updateAuxSceneInstances(size_t geometryCount); bool recordSceneFramebufferPass(IGPUCommandBuffer* cmdbuf, SWindowControlBinding& binding, uint32_t bindingIx); diff --git a/61_UI/include/app/AppProjectionControlPanelUiUtilities.hpp b/61_UI/include/app/AppProjectionControlPanelUiUtilities.hpp index 5ee31aff4..5bc4fa3b8 100644 --- a/61_UI/include/app/AppProjectionControlPanelUiUtilities.hpp +++ b/61_UI/include/app/AppProjectionControlPanelUiUtilities.hpp @@ -10,6 +10,46 @@ 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, diff --git a/61_UI/include/app/AppTypes.hpp b/61_UI/include/app/AppTypes.hpp index 1e38a3717..39d019a1e 100644 --- a/61_UI/include/app/AppTypes.hpp +++ b/61_UI/include/app/AppTypes.hpp @@ -161,13 +161,11 @@ struct SCameraAppFrameRuntimeDefaults final 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 float32_t4 FrustumColor = float32_t4(1.0f, 0.95f, 0.25f, 1.0f); static inline constexpr IGPUCommandBuffer::SClearColorValue UiClearColor = { .float32 = { 0.0f, 0.0f, 0.0f, 1.0f } }; }; struct SCameraAppSceneDebugDefaults final { - static inline constexpr float FrustumLineWidth = 1.0f; static inline constexpr float GridExtent = 32.0f; static inline constexpr float GridVerticalOffset = -0.5f; }; @@ -481,7 +479,6 @@ struct SCameraAppDebugSceneState final nbl::core::smart_refctd_ptr scene = {}; nbl::core::smart_refctd_ptr renderpass = {}; nbl::core::smart_refctd_ptr renderer = {}; - nbl::core::smart_refctd_ptr frustumDrawer = {}; std::optional gridGeometryIx = std::nullopt; std::optional followTargetGeometryIx = std::nullopt; uint16_t geometrySelectionIx = 0u; diff --git a/61_UI/include/common.hpp b/61_UI/include/common.hpp index a48a3ba70..d810434cd 100644 --- a/61_UI/include/common.hpp +++ b/61_UI/include/common.hpp @@ -45,9 +45,6 @@ #include "camera/CCubeProjection.hpp" #include "camera/CLinearProjection.hpp" #include "camera/CPlanarProjection.hpp" -// extensions -#include "nbl/ext/Frustum/CDrawFrustum.h" - // the example's headers #include "nbl/ui/ICursorControl.h" #include "nbl/ext/ImGui/ImGui.h" diff --git a/61_UI/src/keysmapping.cpp b/61_UI/src/keysmapping.cpp index f2305f2f8..b0aa2b9a8 100644 --- a/61_UI/src/keysmapping.cpp +++ b/61_UI/src/keysmapping.cpp @@ -4,9 +4,6 @@ #include #include -namespace -{ - inline std::string buildKeyCodeLabel(const ui::E_KEY_CODE keyCode) { return std::string(1u, ui::keyCodeToChar(keyCode, true)); @@ -17,8 +14,6 @@ inline ImVec4 getBindingActiveStatusColor(const bool active) return active ? SCameraAppBindingEditorUiDefaults::ActiveStatusColor : SCameraAppBindingEditorUiDefaults::InactiveStatusColor; } -} // namespace - 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; diff --git a/common/include/camera/CArcballCamera.hpp b/common/include/camera/CArcballCamera.hpp index 4a2c0c17d..2e1def047 100644 --- a/common/include/camera/CArcballCamera.hpp +++ b/common/include/camera/CArcballCamera.hpp @@ -13,6 +13,11 @@ namespace nbl::core { +/// @brief Target-relative camera that supports planar target translation plus bounded arcball orbiting. +/// +/// The camera keeps a target position, orbit angles, and distance inherited from +/// `CSphericalTargetCamera`. Translation moves the target in the view plane while +/// rotation changes the orbit around that target with a symmetric pitch limit. class CArcballCamera final : public CSphericalTargetCamera { public: @@ -28,11 +33,26 @@ class CArcballCamera final : public CSphericalTargetCamera const typename base_t::CGimbal& getGimbal() override { return m_gimbal; } + /// @brief Apply one frame of semantic translation and rotation input to the arcball rig. virtual bool manipulate(std::span virtualEvents, const hlsl::float64_t4x4* referenceFrame = nullptr) override { if (not virtualEvents.size() and not referenceFrame) return false; + if (referenceFrame) + { + CReferenceTransform reference = {}; + SCameraTargetRelativeState resolvedState = {}; + if (!tryExtractReferenceTransform(reference, referenceFrame) || + !tryResolveReferenceTargetRelativeState(reference, resolvedState)) + { + return false; + } + + resolvedState.orbitUv.y = std::clamp(resolvedState.orbitUv.y, MinPitch, MaxPitch); + adoptTargetRelativeState(resolvedState); + } + const auto impulse = m_gimbal.accumulate(virtualEvents); const auto deltaRotation = scaleVirtualRotation(impulse.dVirtualRotation); @@ -51,6 +71,7 @@ class CArcballCamera final : public CSphericalTargetCamera virtual uint32_t getAllowedVirtualEvents() const override { return AllowedVirtualEvents; } virtual CameraKind getKind() const override { return CameraKind::Arcball; } + /// @brief Return the stable user-facing identifier for this concrete camera kind. virtual std::string_view getIdentifier() const override { return "Arcball Camera"; } static inline constexpr float MinDistance = base_t::MinDistance; diff --git a/common/include/camera/CCameraControlPanelUiUtilities.hpp b/common/include/camera/CCameraControlPanelUiUtilities.hpp index f050a78d6..f77395537 100644 --- a/common/include/camera/CCameraControlPanelUiUtilities.hpp +++ b/common/include/camera/CCameraControlPanelUiUtilities.hpp @@ -21,17 +21,17 @@ namespace nbl::ui struct SCameraControlPanelStyle final { static constexpr float MillisecondsPerSecond = 1000.0f; - static constexpr float WindowWidthRatio = 0.19f; - static constexpr float WindowMinWidth = 200.0f; - static constexpr float WindowMaxWidthRatio = 0.25f; - static constexpr float WindowHeightRatio = 0.34f; + 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(5.0f, 4.0f); - static constexpr ImVec2 FramePadding = ImVec2(4.0f, 1.0f); - static constexpr ImVec2 ItemSpacing = ImVec2(3.0f, 2.0f); - static constexpr ImVec2 CellPadding = ImVec2(3.0f, 2.0f); + 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; @@ -78,18 +78,18 @@ struct SCameraControlPanelStyle final 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(6.0f, 2.0f); - static constexpr ImVec2 KeyHintPadding = ImVec2(4.0f, 1.0f); - static constexpr float BadgeFramePaddingX = 6.0f; - static constexpr float BadgeFramePaddingY = 2.0f; - static constexpr float KeyHintFramePaddingX = 4.0f; + 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.5f; + static constexpr float DotRadius = 3.0f; static constexpr float DotYOffset = 1.0f; - static constexpr float DotSpacing = 6.0f; + static constexpr float DotSpacing = 5.0f; static constexpr float SectionChildRounding = 4.0f; static constexpr float CardChildRounding = 6.0f; - static constexpr ImVec2 CardWindowPadding = ImVec2(10.0f, 8.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; @@ -98,29 +98,29 @@ struct SCameraControlPanelStyle final static constexpr float PanelShadowRounding = 8.0f; static constexpr float PanelRounding = 6.0f; static constexpr float SectionHeaderWidth = 2.0f; - static constexpr float SectionHeaderTextOffsetX = 8.0f; - static constexpr float SectionHeaderHeight = 20.0f; + static constexpr float SectionHeaderTextOffsetX = 7.0f; + static constexpr float SectionHeaderHeight = 18.0f; static constexpr float SectionSpacingY = 0.0f; - static constexpr float CardExtraRows = 1.0f; - static constexpr float CardHeightPadding = 10.0f; - static constexpr float MiniStatHeight = 56.0f; - static constexpr float MiniStatPlotHeight = 24.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 = 64.0f; - static constexpr float HeaderTitleFontScale = 1.08f; - static constexpr float HeaderMetricFontScale = 1.05f; - static constexpr float HeaderDummyY = 1.0f; - static constexpr float HeaderGapSmall = 2.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(6.0f, 2.0f); - static constexpr float KeyframeListHeight = 120.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 = 120.0f; + static constexpr float SummaryLabelColumnWidth = 108.0f; }; struct SCameraControlPanelBadgeData final diff --git a/common/include/camera/CCameraFileUtilities.hpp b/common/include/camera/CCameraFileUtilities.hpp index 97d103019..9a2a0356a 100644 --- a/common/include/camera/CCameraFileUtilities.hpp +++ b/common/include/camera/CCameraFileUtilities.hpp @@ -10,9 +10,14 @@ namespace nbl::system { +/// @brief Shared file I/O helpers used by camera persistence and scripted-runtime loaders. +/// +/// The helpers keep camera-facing persistence code independent from ad-hoc file +/// handling and provide one place for consistent error propagation. struct CCameraFileUtilities final { public: + /// @brief Read a whole file into a byte buffer. static inline bool readBinaryFile( ISystem& system, const path& filePath, @@ -47,6 +52,7 @@ struct CCameraFileUtilities final return true; } + /// @brief Read a whole file and interpret its payload as UTF-8 text. static inline bool readTextFile( ISystem& system, const path& filePath, @@ -62,6 +68,7 @@ struct CCameraFileUtilities final return true; } + /// @brief Overwrite a file with the provided text payload. static inline bool writeTextFile( ISystem& system, const path& filePath, diff --git a/common/include/camera/CCameraFollowUtilities.hpp b/common/include/camera/CCameraFollowUtilities.hpp index bea2ad432..0501673c3 100644 --- a/common/include/camera/CCameraFollowUtilities.hpp +++ b/common/include/camera/CCameraFollowUtilities.hpp @@ -24,6 +24,7 @@ class CTrackedTarget public: using gimbal_t = ICamera::CGimbal; + /// @brief Construct a tracked target from an initial pose and optional identifier. CTrackedTarget( const hlsl::float64_t3& position = hlsl::float64_t3(0.0), const hlsl::camera_quaternion_t& orientation = hlsl::CCameraMathUtilities::makeIdentityQuaternion(), @@ -34,10 +35,14 @@ class CTrackedTarget m_gimbal.updateView(); } + /// @brief Return the stable human-readable identifier of the tracked target. inline const std::string& getIdentifier() const { return m_identifier; } + /// @brief Return read-only access to the tracked target gimbal. inline const gimbal_t& getGimbal() const { return m_gimbal; } + /// @brief Return mutable access to the tracked target gimbal. inline gimbal_t& getGimbal() { return m_gimbal; } + /// @brief Replace the tracked target pose in world space. inline void setPose(const hlsl::float64_t3& position, const hlsl::camera_quaternion_t& orientation) { m_gimbal.begin(); @@ -47,16 +52,19 @@ class CTrackedTarget m_gimbal.updateView(); } + /// @brief Replace only the tracked target position. inline void setPosition(const hlsl::float64_t3& position) { setPose(position, m_gimbal.getOrientation()); } + /// @brief Replace only the tracked target orientation. inline void setOrientation(const hlsl::camera_quaternion_t& orientation) { setPose(m_gimbal.getPosition(), orientation); } + /// @brief Replace the tracked target pose from a rigid transform matrix when possible. inline bool trySetFromTransform(const hlsl::float64_t4x4& transform) { hlsl::float64_t3 position = hlsl::float64_t3(0.0); @@ -96,14 +104,24 @@ enum class ECameraFollowMode : uint8_t /// `worldOffset` and `localOffset` are only meaningful for their matching offset-based modes. struct SCameraFollowConfig { + /// @brief Whether follow should be applied at all. bool enabled = false; + /// @brief Follow policy used when the configuration is enabled. ECameraFollowMode mode = ECameraFollowMode::OrbitTarget; + /// @brief World-space offset preserved by `KeepWorldOffset`. hlsl::float64_t3 worldOffset = hlsl::float64_t3(0.0); + /// @brief Target-local offset preserved by `KeepLocalOffset`. hlsl::float64_t3 localOffset = hlsl::float64_t3(0.0); }; +/// @brief Shared policy helpers for tracked-target follow. +/// +/// The helpers decide which follow modes lock the view, which ones move the +/// camera, how offsets are captured, and how a tracked target is translated into +/// a `CCameraGoal` that can then be applied through the shared goal solver. struct CCameraFollowUtilities final { + /// @brief Return whether the follow mode keeps the camera view locked onto the target. static inline constexpr bool cameraFollowModeLocksViewToTarget(const ECameraFollowMode mode) { switch (mode) @@ -118,6 +136,7 @@ struct CCameraFollowUtilities final } } + /// @brief Return whether the follow mode moves the camera world position together with the target. static inline constexpr bool cameraFollowModeMovesCameraPosition(const ECameraFollowMode mode) { switch (mode) @@ -131,31 +150,37 @@ struct CCameraFollowUtilities final } } + /// @brief Return whether the follow mode preserves the current camera world position. static inline constexpr bool cameraFollowModeKeepsCameraWorldPosition(const ECameraFollowMode mode) { return mode == ECameraFollowMode::LookAtTarget; } + /// @brief Return whether the follow mode interprets `worldOffset`. static inline constexpr bool cameraFollowModeUsesWorldOffset(const ECameraFollowMode mode) { return mode == ECameraFollowMode::KeepWorldOffset; } + /// @brief Return whether the follow mode interprets `localOffset`. static inline constexpr bool cameraFollowModeUsesLocalOffset(const ECameraFollowMode mode) { return mode == ECameraFollowMode::KeepLocalOffset; } + /// @brief Return whether the follow mode needs the tracked target local frame. static inline constexpr bool cameraFollowModeUsesTrackedTargetLocalFrame(const ECameraFollowMode mode) { return mode == ECameraFollowMode::KeepLocalOffset; } + /// @brief Return whether the follow mode requires a captured offset before it can be replayed. static inline constexpr bool cameraFollowModeUsesCapturedOffset(const ECameraFollowMode mode) { return cameraFollowModeUsesWorldOffset(mode) || cameraFollowModeUsesLocalOffset(mode); } + /// @brief Return the shared default follow mode for one camera kind. static inline constexpr ECameraFollowMode getDefaultCameraFollowMode(const ICamera::CameraKind kind) { switch (kind) @@ -176,6 +201,7 @@ struct CCameraFollowUtilities final } } + /// @brief Build the shared default follow configuration for one camera kind. static inline constexpr SCameraFollowConfig makeDefaultFollowConfig(const ICamera::CameraKind kind) { const auto mode = getDefaultCameraFollowMode(kind); @@ -185,21 +211,25 @@ struct CCameraFollowUtilities final }; } + /// @brief Build the shared default follow configuration for one concrete camera instance. static inline constexpr SCameraFollowConfig makeDefaultFollowConfig(const ICamera* const camera) { return camera ? makeDefaultFollowConfig(camera->getKind()) : SCameraFollowConfig{}; } + /// @brief Transform a tracked-target local offset into world space. static inline hlsl::float64_t3 transformFollowLocalOffset(const ICamera::CGimbal& gimbal, const hlsl::float64_t3& localOffset) { return hlsl::CCameraMathUtilities::rotateVectorByQuaternion(gimbal.getOrientation(), localOffset); } + /// @brief Project a world-space offset into the tracked target local frame. static inline hlsl::float64_t3 projectFollowWorldOffsetToLocal(const ICamera::CGimbal& gimbal, const hlsl::float64_t3& worldOffset) { return hlsl::CCameraMathUtilities::projectWorldVectorToLocalQuaternionFrame(gimbal.getOrientation(), worldOffset); } + /// @brief Build a look-at orientation that points from `position` toward the tracked target. static inline bool buildFollowLookAtOrientation( const hlsl::float64_t3& position, const hlsl::float64_t3& targetPosition, @@ -209,6 +239,7 @@ struct CCameraFollowUtilities final return hlsl::CCameraMathUtilities::tryBuildLookAtOrientation(position, targetPosition, preferredUp, outOrientation); } + /// @brief Capture world-space and target-local follow offsets from the current camera pose. static inline bool captureFollowOffsetsFromCamera( const CCameraGoalSolver& solver, ICamera* camera, @@ -225,6 +256,7 @@ struct CCameraFollowUtilities final return true; } + /// @brief Measure the angular lock error between a camera forward axis and a tracked target. static inline bool tryComputeFollowTargetLockMetrics( const ICamera::CGimbal& cameraGimbal, const CTrackedTarget& trackedTarget, diff --git a/common/include/camera/CCameraGoal.hpp b/common/include/camera/CCameraGoal.hpp index 6c9da21ea..98487defd 100644 --- a/common/include/camera/CCameraGoal.hpp +++ b/common/include/camera/CCameraGoal.hpp @@ -21,25 +21,41 @@ namespace nbl::core /// @brief Typed transport object for camera state used by capture, comparison, presets, and playback. struct CCameraGoal : SCameraRigPose { + /// @brief Camera kind that originally produced this goal. ICamera::CameraKind sourceKind = ICamera::CameraKind::Unknown; + /// @brief Capability mask captured from the source camera. uint32_t sourceCapabilities = ICamera::None; + /// @brief Goal-state fragments that were valid on the source camera. uint32_t sourceGoalStateMask = ICamera::GoalStateNone; + /// @brief Whether `targetPosition` is present in this goal. bool hasTargetPosition = false; + /// @brief Tracked target position in world space. hlsl::float64_t3 targetPosition = hlsl::float64_t3(0.0); + /// @brief Whether `distance` is present in this goal. bool hasDistance = false; + /// @brief Explicit target-relative distance when present. float distance = 0.f; + /// @brief Whether the canonical orbit state is present in this goal. bool hasOrbitState = false; + /// @brief Canonical orbit yaw and pitch, expressed in radians. hlsl::float64_t2 orbitUv = hlsl::float64_t2(0.0); + /// @brief Distance associated with `orbitUv` when the orbit state is present. float orbitDistance = 0.f; + /// @brief Whether a typed path state is present in this goal. bool hasPathState = false; + /// @brief Typed path state captured from or authored for a `Path Rig` camera. ICamera::PathState pathState = {}; + /// @brief Whether a dynamic perspective state is present in this goal. bool hasDynamicPerspectiveState = false; + /// @brief Typed dynamic perspective state captured from or authored for the source camera. ICamera::DynamicPerspectiveState dynamicPerspectiveState = {}; }; +/// @brief Shared canonicalization, comparison, and conversion helpers for `CCameraGoal`. struct CCameraGoalUtilities final { public: + /// @brief Compute which typed goal-state fragments are required by the current goal payload. static inline uint32_t getRequiredGoalStateMask(const CCameraGoal& target) { uint32_t mask = ICamera::GoalStateNone; @@ -52,6 +68,7 @@ struct CCameraGoalUtilities final return mask; } + /// @brief Overwrite the canonical target-relative fields of a goal from prebuilt state and pose data. static inline void applyCanonicalTargetRelativeGoalFields( CCameraGoal& goal, const SCameraTargetRelativeState& state, @@ -68,6 +85,7 @@ struct CCameraGoalUtilities final goal.orbitDistance = static_cast(pose.appliedDistance); } + /// @brief Rebuild the canonical target-relative portion of a goal from typed target-relative state. static inline bool applyCanonicalTargetRelativeGoal(CCameraGoal& goal, const SCameraTargetRelativeState& state) { SCameraTargetRelativePose pose = {}; @@ -78,6 +96,7 @@ struct CCameraGoalUtilities final return true; } + /// @brief Rebuild the canonical pose and orbit fields of a goal from typed path state. static inline bool applyCanonicalPathGoalFields( CCameraGoal& goal, const hlsl::float64_t3& targetPosition, @@ -103,6 +122,7 @@ struct CCameraGoalUtilities final return true; } + /// @brief Rebuild the canonical pose fields from the goal's current spherical-target payload. static inline bool applyCanonicalSphericalGoal(CCameraGoal& goal) { if (!(goal.hasTargetPosition && goal.hasOrbitState)) @@ -119,6 +139,7 @@ struct CCameraGoalUtilities final }); } + /// @brief Infer a target-relative goal from a target position and a desired camera position. static inline bool buildCanonicalTargetRelativeGoalFromPosition( CCameraGoal& goal, const hlsl::float64_t3& targetPosition, @@ -138,6 +159,7 @@ struct CCameraGoalUtilities final return applyCanonicalTargetRelativeGoal(goal, state); } + /// @brief Resolve the effective target-relative state of a goal against the current camera state. static inline bool tryResolveCanonicalTargetRelativeState( const CCameraGoal& goal, const ICamera::SphericalTargetState& currentState, @@ -176,6 +198,7 @@ struct CCameraGoalUtilities final return true; } + /// @brief Rebuild the canonical pose fields from the goal's current path payload. static inline bool applyCanonicalPathGoal(CCameraGoal& goal) { if (!(goal.hasPathState && goal.hasTargetPosition)) @@ -185,6 +208,7 @@ struct CCameraGoalUtilities final return applyCanonicalPathGoalFields(goal, goal.targetPosition, goal.pathState); } + /// @brief Canonicalize whichever typed state fragments are currently present on the goal. static inline bool applyCanonicalGoalState(CCameraGoal& goal) { if (goal.hasPathState) @@ -196,12 +220,14 @@ struct CCameraGoalUtilities final return true; } + /// @brief Return a value-copied goal after canonicalizing its typed state. static inline CCameraGoal canonicalizeGoal(CCameraGoal goal) { applyCanonicalGoalState(goal); return goal; } + /// @brief Check whether every populated scalar and vector stored by the goal is finite. static inline bool isGoalFinite(const CCameraGoal& goal) { if (!hlsl::CCameraMathUtilities::isFiniteVec3(goal.position) || !hlsl::CCameraMathUtilities::isFiniteQuaternion(goal.orientation)) @@ -220,6 +246,7 @@ struct CCameraGoalUtilities final return true; } + /// @brief Compare two goals using caller-provided pose and scalar tolerances. static inline bool compareGoals(const CCameraGoal& actual, const CCameraGoal& expected, const double posEps, const double rotEpsDeg, const double scalarEps) { diff --git a/common/include/camera/CCameraPathMetadata.hpp b/common/include/camera/CCameraPathMetadata.hpp index 258037c83..3b1ce2b60 100644 --- a/common/include/camera/CCameraPathMetadata.hpp +++ b/common/include/camera/CCameraPathMetadata.hpp @@ -9,11 +9,20 @@ namespace nbl::core { +/// @brief Stable descriptive strings shared by the reusable `Path Rig` camera kind. +/// +/// These labels are intentionally kept outside the heavy path utility header so +/// simple metadata consumers can describe the camera kind without pulling in the +/// full path-model implementation. struct SCameraPathRigMetadata final { + /// @brief User-facing camera kind label. static inline constexpr std::string_view KindLabel = "Path Rig"; + /// @brief Short user-facing description of the camera kind. static inline constexpr std::string_view KindDescription = "Path-model camera with typed s/u/v/roll state"; + /// @brief Default runtime identifier used by the concrete camera instance. static inline constexpr std::string_view Identifier = "Target-relative Path Rig"; + /// @brief Default description of the built-in path model shipped by the shared API. static inline constexpr std::string_view DefaultModelDescription = "Adjust a target-relative path rig with s/u/v/roll state"; }; diff --git a/common/include/camera/CCameraPathUtilities.hpp b/common/include/camera/CCameraPathUtilities.hpp index 58f5da789..b1676bed1 100644 --- a/common/include/camera/CCameraPathUtilities.hpp +++ b/common/include/camera/CCameraPathUtilities.hpp @@ -18,22 +18,28 @@ namespace nbl::core /// @brief Shared helpers for the reusable `PathRig` camera kind. struct SCameraPathPose final : SCameraRigPose { + /// @brief Final radial distance actually applied after clamping and path-state sanitization. hlsl::float64_t appliedDistance = 0.0; + /// @brief Canonical orbit yaw/pitch derived from the evaluated path state. hlsl::float64_t2 orbitUv = hlsl::float64_t2(0.0); }; +/// @brief Typed delta applied to `ICamera::PathState`. struct SCameraPathDelta final : ICamera::PathState { + /// @brief Pack the delta into one four-component vector. inline hlsl::float64_t4 asVector() const { return ICamera::PathState::asVector(); } + /// @brief Reinterpret the delta as the translation-style helper representation. inline hlsl::float64_t3 translationVector() const { return ICamera::PathState::asTranslationVector(); } + /// @brief Rebuild the delta from the packed vector representation. static inline SCameraPathDelta fromVector(const hlsl::float64_t4& value) { SCameraPathDelta delta = {}; @@ -44,6 +50,7 @@ struct SCameraPathDelta final : ICamera::PathState return delta; } + /// @brief Rebuild the delta from a translation-style helper vector and optional roll value. static inline SCameraPathDelta fromMotion(const hlsl::float64_t3& translation, const double pathRoll = 0.0) { SCameraPathDelta delta = {}; @@ -55,6 +62,7 @@ struct SCameraPathDelta final : ICamera::PathState } }; +/// @brief One desired path-state change expressed as current state, desired state, and their delta. struct SCameraPathStateTransition final { ICamera::PathState current = {}; @@ -62,12 +70,14 @@ struct SCameraPathStateTransition final SCameraPathDelta delta = {}; }; +/// @brief Canonical evaluated path state combining a final pose and target-relative view of that pose. struct SCameraCanonicalPathState final { SCameraPathPose pose = {}; SCameraTargetRelativeState targetRelative = {}; }; +/// @brief Comparison tolerances used when matching two path states. struct SCameraPathComparisonThresholds final { double sToleranceDeg = ICamera::DefaultAngularToleranceDeg; @@ -75,12 +85,14 @@ struct SCameraPathComparisonThresholds final double scalarTolerance = ICamera::ScalarTolerance; }; +/// @brief Result of updating the path distance while preserving the rest of the path state. struct SCameraPathDistanceUpdateResult final { bool exact = false; hlsl::float64_t appliedDistance = 0.0; }; +/// @brief Shared default constants used by the built-in `Path Rig` model. struct SCameraPathDefaults final { static constexpr double MinU = static_cast(ICamera::SphericalMinDistance); @@ -105,6 +117,7 @@ struct SCameraPathDefaults final using SCameraPathLimits = ICamera::PathStateLimits; +/// @brief Evaluation context passed into the active path-model control law. struct SCameraPathControlContext final { ICamera::PathState currentState = {}; @@ -115,6 +128,11 @@ struct SCameraPathControlContext final SCameraPathLimits limits = SCameraPathDefaults::Limits; }; +/// @brief Callback bundle defining how a concrete `Path Rig` model behaves. +/// +/// The model is intentionally split into state resolution, control law, +/// integration, canonical evaluation, and distance updates so the runtime camera +/// can stay event-driven while tooling still works with a typed path state. struct SCameraPathModel final { using resolve_state_t = std::function LocalTranslation = {{ @@ -26,9 +28,15 @@ struct SCameraVirtualEventBindings final }}; }; +/// @brief Shared helpers for building and analyzing `CVirtualGimbalEvent` batches. +/// +/// These utilities are reused by goal replay, path-model control translation, +/// scripted tooling, and smoke checks whenever code needs to convert typed deltas +/// into semantic event streams or inspect those streams on the CPU. struct CCameraVirtualEventUtilities final { public: + /// @brief Append one signed scalar as either the positive or negative event variant. static inline void appendSignedVirtualEvent( std::vector& events, const double value, @@ -44,6 +52,7 @@ struct CCameraVirtualEventUtilities final ev.magnitude = hlsl::abs(value); } + /// @brief Append one signed scalar after normalizing it by a caller-provided denominator. static inline void appendScaledVirtualEvent( std::vector& events, const double value, @@ -58,6 +67,7 @@ struct CCameraVirtualEventUtilities final appendSignedVirtualEvent(events, value / denominator, positive, negative, tolerance); } + /// @brief Append one angular delta by comparing it against a tolerance expressed in degrees. static inline void appendAngularDeltaEvent( std::vector& events, const double deltaRadians, @@ -81,6 +91,7 @@ struct CCameraVirtualEventUtilities final negative); } + /// @brief Append one 3-axis scalar bundle through a caller-provided binding table. static inline void appendScaledVirtualAxisEvents( std::vector& events, const hlsl::float64_t3& values, @@ -100,6 +111,7 @@ struct CCameraVirtualEventUtilities final } } + /// @brief Append a local-space translation delta as semantic move events. static inline void appendLocalTranslationEvents( std::vector& events, const hlsl::float64_t3& localDelta, @@ -114,6 +126,7 @@ struct CCameraVirtualEventUtilities final SCameraVirtualEventBindings::LocalTranslation); } + /// @brief Reinterpret a world-space translation delta in the local frame of a camera orientation. static inline void appendWorldTranslationAsLocalEvents( std::vector& events, const hlsl::camera_quaternion_t& orientation, @@ -128,6 +141,7 @@ struct CCameraVirtualEventUtilities final tolerances); } + /// @brief Append one 3-axis angular delta through a caller-provided binding table. static inline void appendAngularAxisEvents( std::vector& events, const hlsl::float64_t3& deltaRadians, @@ -147,6 +161,7 @@ struct CCameraVirtualEventUtilities final } } + /// @brief Accumulate only translation-related virtual events back into a signed delta vector. static inline hlsl::float64_t3 collectSignedTranslationDelta(std::span events) { hlsl::float64_t3 delta = hlsl::float64_t3(0.0); diff --git a/common/include/camera/CChaseCamera.hpp b/common/include/camera/CChaseCamera.hpp index 164fb5791..51eaaa134 100644 --- a/common/include/camera/CChaseCamera.hpp +++ b/common/include/camera/CChaseCamera.hpp @@ -9,6 +9,11 @@ namespace nbl::core { +/// @brief Target-relative camera that slides the target on the ground plane while chasing it from behind. +/// +/// The camera keeps a bounded orbit around the target, but translation is resolved +/// in the planar forward/right frame so the tracked subject can be moved across a +/// horizontal surface without changing the follow style. class CChaseCamera final : public CSphericalTargetCamera { public: @@ -24,11 +29,26 @@ class CChaseCamera final : public CSphericalTargetCamera const typename base_t::CGimbal& getGimbal() override { return m_gimbal; } + /// @brief Apply chase-style planar translation, pitch/yaw orbiting, and distance changes. virtual bool manipulate(std::span virtualEvents, const hlsl::float64_t4x4* referenceFrame = nullptr) override { if (not virtualEvents.size() and not referenceFrame) return false; + if (referenceFrame) + { + CReferenceTransform reference = {}; + SCameraTargetRelativeState resolvedState = {}; + if (!tryExtractReferenceTransform(reference, referenceFrame) || + !tryResolveReferenceTargetRelativeState(reference, resolvedState)) + { + return false; + } + + resolvedState.orbitUv.y = std::clamp(resolvedState.orbitUv.y, MinPitch, MaxPitch); + adoptTargetRelativeState(resolvedState); + } + const auto impulse = m_gimbal.accumulate(virtualEvents); const auto deltaRotation = scaleVirtualRotation(impulse.dVirtualRotation); @@ -55,6 +75,7 @@ class CChaseCamera final : public CSphericalTargetCamera virtual uint32_t getAllowedVirtualEvents() const override { return AllowedVirtualEvents; } virtual CameraKind getKind() const override { return CameraKind::Chase; } + /// @brief Return the stable user-facing identifier for this concrete camera kind. virtual std::string_view getIdentifier() const override { return "Chase Camera"; } private: diff --git a/common/include/camera/CDollyCamera.hpp b/common/include/camera/CDollyCamera.hpp index 2ddefc0a2..2ef482a4d 100644 --- a/common/include/camera/CDollyCamera.hpp +++ b/common/include/camera/CDollyCamera.hpp @@ -9,6 +9,11 @@ namespace nbl::core { +/// @brief Target-relative camera that translates the tracked target in the full local camera frame. +/// +/// Unlike the chase camera, dolly translation preserves the full local basis, +/// which makes the target move along the current right/up/forward frame while the +/// camera keeps looking back at it from the maintained spherical offset. class CDollyCamera final : public CSphericalTargetCamera { public: @@ -24,11 +29,26 @@ class CDollyCamera final : public CSphericalTargetCamera const typename base_t::CGimbal& getGimbal() override { return m_gimbal; } + /// @brief Apply one frame of local-frame dolly translation plus orbit rotation. virtual bool manipulate(std::span virtualEvents, const hlsl::float64_t4x4* referenceFrame = nullptr) override { if (not virtualEvents.size() and not referenceFrame) return false; + if (referenceFrame) + { + CReferenceTransform reference = {}; + SCameraTargetRelativeState resolvedState = {}; + if (!tryExtractReferenceTransform(reference, referenceFrame) || + !tryResolveReferenceTargetRelativeState(reference, resolvedState)) + { + return false; + } + + resolvedState.orbitUv.y = std::clamp(resolvedState.orbitUv.y, MinPitch, MaxPitch); + adoptTargetRelativeState(resolvedState); + } + const auto impulse = m_gimbal.accumulate(virtualEvents); const auto deltaRotation = scaleVirtualRotation(impulse.dVirtualRotation); @@ -46,6 +66,7 @@ class CDollyCamera final : public CSphericalTargetCamera virtual uint32_t getAllowedVirtualEvents() const override { return AllowedVirtualEvents; } virtual CameraKind getKind() const override { return CameraKind::Dolly; } + /// @brief Return the stable user-facing identifier for this concrete camera kind. virtual std::string_view getIdentifier() const override { return "Dolly Camera"; } private: diff --git a/common/include/camera/CDollyZoomCamera.hpp b/common/include/camera/CDollyZoomCamera.hpp index 231c9f230..86e16a3de 100644 --- a/common/include/camera/CDollyZoomCamera.hpp +++ b/common/include/camera/CDollyZoomCamera.hpp @@ -9,6 +9,11 @@ namespace nbl::core { +/// @brief Target-relative camera that preserves subject framing by coupling distance with a derived perspective FOV. +/// +/// The rig reuses spherical target-relative manipulation but exposes an additional +/// dynamic-perspective state describing the authored base FOV and the reference +/// distance used to compute the current dolly-zoom FOV. class CDollyZoomCamera final : public CSphericalTargetCamera { public: @@ -23,12 +28,17 @@ class CDollyZoomCamera final : public CSphericalTargetCamera const typename base_t::CGimbal& getGimbal() override { return m_gimbal; } + /// @brief Return the authored FOV used as the reference value for dolly-zoom evaluation. float getBaseFov() const { return m_baseFov; } + /// @brief Update the authored reference FOV used for dolly-zoom evaluation. void setBaseFov(float fov) { m_baseFov = fov; } + /// @brief Return the reference distance that preserves the authored framing. float getReferenceDistance() const { return m_referenceDistance; } + /// @brief Update the reference distance used by dolly-zoom FOV evaluation. void setReferenceDistance(float distance) { m_referenceDistance = distance; } + /// @brief Evaluate the effective perspective FOV required to preserve subject framing at the current distance. float computeDollyFov() const { const double base = std::tan(hlsl::radians(static_cast(m_baseFov)) * 0.5); @@ -38,11 +48,25 @@ class CDollyZoomCamera final : public CSphericalTargetCamera return static_cast(std::clamp(fovDeg, static_cast(MinDynamicFovDeg), static_cast(MaxDynamicFovDeg))); } + /// @brief Apply one frame of orbit translation and distance input for the dolly-zoom rig. virtual bool manipulate(std::span virtualEvents, const hlsl::float64_t4x4* referenceFrame = nullptr) override { if (not virtualEvents.size() and not referenceFrame) return false; + if (referenceFrame) + { + CReferenceTransform reference = {}; + SCameraTargetRelativeState resolvedState = {}; + if (!tryExtractReferenceTransform(reference, referenceFrame) || + !tryResolveReferenceTargetRelativeState(reference, resolvedState)) + { + return false; + } + + adoptTargetRelativeState(resolvedState); + } + const auto impulse = m_gimbal.accumulate(virtualEvents); const auto deltaTranslation = scaleVirtualTranslation(impulse.dVirtualTranslate); const double deltaDistance = scaleUnscaledVirtualTranslation(impulse.dVirtualTranslate.z); @@ -56,17 +80,20 @@ class CDollyZoomCamera final : public CSphericalTargetCamera virtual uint32_t getAllowedVirtualEvents() const override { return AllowedVirtualEvents; } virtual CameraKind getKind() const override { return CameraKind::DollyZoom; } virtual uint32_t getCapabilities() const override { return base_t::getCapabilities() | base_t::DynamicPerspectiveFov; } + /// @brief Query the current derived FOV produced by the dolly-zoom state. virtual bool tryGetDynamicPerspectiveFov(float& outFov) const override { outFov = computeDollyFov(); return true; } + /// @brief Query the authored dolly-zoom state used to derive the current dynamic FOV. virtual bool tryGetDynamicPerspectiveState(DynamicPerspectiveState& out) const override { out.baseFov = m_baseFov; out.referenceDistance = m_referenceDistance; return true; } + /// @brief Replace the authored dolly-zoom state after validating both scalars. virtual bool trySetDynamicPerspectiveState(const DynamicPerspectiveState& state) override { if (!hlsl::CCameraMathUtilities::isFiniteScalar(state.baseFov) || !hlsl::CCameraMathUtilities::isFiniteScalar(state.referenceDistance) || state.referenceDistance <= 0.f) @@ -76,6 +103,7 @@ class CDollyZoomCamera final : public CSphericalTargetCamera m_referenceDistance = state.referenceDistance; return true; } + /// @brief Return the stable user-facing identifier for this concrete camera kind. virtual std::string_view getIdentifier() const override { return "Dolly Zoom Camera"; } private: diff --git a/common/include/camera/CGeneralPurposeGimbal.hpp b/common/include/camera/CGeneralPurposeGimbal.hpp index d48add267..d0f7c72b9 100644 --- a/common/include/camera/CGeneralPurposeGimbal.hpp +++ b/common/include/camera/CGeneralPurposeGimbal.hpp @@ -5,12 +5,17 @@ namespace nbl::core { + /// @brief Minimal concrete gimbal wrapper for code that only needs the generic `IGimbal` behavior. + /// + /// The class exists mainly as a convenient instantiable type when no additional + /// camera-specific state or manipulation policy is required on top of `IGimbal`. template class CGeneralPurposeGimbal : public IGimbal { public: using base_t = IGimbal; + /// @brief Construct the gimbal from an initial world-space pose. CGeneralPurposeGimbal(typename base_t::SCreationParameters&& parameters) : base_t(std::move(parameters)) {} ~CGeneralPurposeGimbal() = default; }; diff --git a/common/include/camera/CIsometricCamera.hpp b/common/include/camera/CIsometricCamera.hpp index 270c23d58..3ac1d41e8 100644 --- a/common/include/camera/CIsometricCamera.hpp +++ b/common/include/camera/CIsometricCamera.hpp @@ -9,6 +9,10 @@ namespace nbl::core { +/// @brief Target-relative camera locked to the shared isometric yaw and pitch. +/// +/// Translation moves the tracked target in the current view plane while the +/// authored isometric orientation stays fixed. Distance changes are still allowed. class CIsometricCamera final : public CSphericalTargetCamera { public: @@ -24,11 +28,25 @@ class CIsometricCamera final : public CSphericalTargetCamera const typename base_t::CGimbal& getGimbal() override { return m_gimbal; } + /// @brief Apply one frame of planar target translation and distance changes while preserving the fixed isometric angles. virtual bool manipulate(std::span virtualEvents, const hlsl::float64_t4x4* referenceFrame = nullptr) override { if (not virtualEvents.size() and not referenceFrame) return false; + if (referenceFrame) + { + CReferenceTransform reference = {}; + SCameraTargetRelativeState resolvedState = {}; + if (!tryExtractReferenceTransform(reference, referenceFrame) || + !tryResolveReferenceIsometricState(reference, resolvedState)) + { + return false; + } + + adoptTargetRelativeState(resolvedState); + } + const auto impulse = m_gimbal.accumulate(virtualEvents); const auto deltaTranslation = scaleVirtualTranslation(impulse.dVirtualTranslate); @@ -45,6 +63,7 @@ class CIsometricCamera final : public CSphericalTargetCamera virtual uint32_t getAllowedVirtualEvents() const override { return AllowedVirtualEvents; } virtual CameraKind getKind() const override { return CameraKind::Isometric; } + /// @brief Return the stable user-facing identifier for this concrete camera kind. virtual std::string_view getIdentifier() const override { return "Isometric Camera"; } private: diff --git a/common/include/camera/CLinearProjection.hpp b/common/include/camera/CLinearProjection.hpp index 40e9813dd..d6e0dd4e0 100644 --- a/common/include/camera/CLinearProjection.hpp +++ b/common/include/camera/CLinearProjection.hpp @@ -6,6 +6,10 @@ namespace nbl::core { + /// @brief Range-backed concrete implementation of `ILinearProjection`. + /// + /// The template owns a caller-selected contiguous container of linear projection + /// entries and exposes it through the generic `ILinearProjection` interface. template ProjectionsRange> class CLinearProjection : public ILinearProjection { @@ -14,6 +18,7 @@ namespace nbl::core CLinearProjection() = default; + /// @brief Create a projection wrapper only when a valid camera instance is available. inline static core::smart_refctd_ptr create(core::smart_refctd_ptr&& camera) { if (!camera) @@ -22,17 +27,20 @@ namespace nbl::core return core::smart_refctd_ptr(new CLinearProjection(core::smart_refctd_ptr(camera)), core::dont_grab); } + /// @brief Return the number of stored linear projection entries. virtual uint32_t getLinearProjectionCount() const override { return static_cast(m_projections.size()); } + /// @brief Return one stored projection entry by index. virtual const CProjection& getLinearProjection(uint32_t index) const override { assert(index < m_projections.size()); return m_projections[index]; } + /// @brief Expose mutable access to the owned projection range. inline std::span getLinearProjections() { return std::span(m_projections.data(), m_projections.size()); diff --git a/common/include/camera/COrbitCamera.hpp b/common/include/camera/COrbitCamera.hpp index 74083046b..b6e0d6de9 100644 --- a/common/include/camera/COrbitCamera.hpp +++ b/common/include/camera/COrbitCamera.hpp @@ -8,6 +8,10 @@ namespace nbl::core { +/// @brief Target-relative camera that interprets translation input as orbit-angle and distance changes. +/// +/// This is the simplest spherical-target camera in the stack. It keeps the target +/// fixed and adjusts only orbit yaw, orbit pitch, and camera distance. class COrbitCamera final : public CSphericalTargetCamera { public: @@ -23,8 +27,25 @@ class COrbitCamera final : public CSphericalTargetCamera const typename base_t::CGimbal& getGimbal() override { return m_gimbal; } + /// @brief Apply one frame of orbit-angle and distance input around the current target. virtual bool manipulate(std::span virtualEvents, const hlsl::float64_t4x4* referenceFrame = nullptr) override { + if (virtualEvents.empty() && !referenceFrame) + return false; + + if (referenceFrame) + { + CReferenceTransform reference = {}; + SCameraTargetRelativeState resolvedState = {}; + if (!tryExtractReferenceTransform(reference, referenceFrame) || + !tryResolveReferenceTargetRelativeState(reference, resolvedState)) + { + return false; + } + + adoptTargetRelativeState(resolvedState); + } + const auto impulse = m_gimbal.accumulate(virtualEvents); const auto deltaTranslation = scaleVirtualTranslation(impulse.dVirtualTranslate); const double deltaDistance = scaleUnscaledVirtualTranslation(impulse.dVirtualTranslate.z); diff --git a/common/include/camera/CPathCamera.hpp b/common/include/camera/CPathCamera.hpp index f929ac3be..aa8b33558 100644 --- a/common/include/camera/CPathCamera.hpp +++ b/common/include/camera/CPathCamera.hpp @@ -22,21 +22,25 @@ class CPathCamera final : public CSphericalTargetCamera using path_model_t = SCameraPathModel; using path_limits_t = PathStateLimits; + /// @brief Construct the path rig with the shared default path model and default limits. CPathCamera(const hlsl::float64_t3& position, const hlsl::float64_t3& target) : CPathCamera(position, target, CCameraPathUtilities::makeDefaultPathModel(), CCameraPathUtilities::makeDefaultPathLimits()) { } + /// @brief Construct the path rig with a caller-provided model and default limits. CPathCamera(const hlsl::float64_t3& position, const hlsl::float64_t3& target, path_model_t pathModel) : CPathCamera(position, target, std::move(pathModel), CCameraPathUtilities::makeDefaultPathLimits()) { } + /// @brief Construct the path rig with the shared default model and caller-provided limits. CPathCamera(const hlsl::float64_t3& position, const hlsl::float64_t3& target, path_limits_t pathLimits) : CPathCamera(position, target, CCameraPathUtilities::makeDefaultPathModel(), pathLimits) { } + /// @brief Construct the path rig with fully caller-provided model and path-state limits. CPathCamera(const hlsl::float64_t3& position, const hlsl::float64_t3& target, path_model_t pathModel, path_limits_t pathLimits) : base_t(position, target) { @@ -47,6 +51,7 @@ class CPathCamera final : public CSphericalTargetCamera const typename base_t::CGimbal& getGimbal() override { return m_gimbal; } + /// @brief Consume virtual events through the active path model and update the runtime pose from the resulting path state. virtual bool manipulate(std::span virtualEvents, const hlsl::float64_t4x4* referenceFrame = nullptr) override { if (virtualEvents.empty() && !referenceFrame) @@ -104,18 +109,21 @@ class CPathCamera final : public CSphericalTargetCamera virtual CameraKind getKind() const override { return CameraKind::Path; } virtual uint32_t getGoalStateMask() const override { return base_t::getGoalStateMask() | base_t::GoalStatePath; } + /// @brief Query the current typed path state. virtual bool tryGetPathState(PathState& out) const override { out = m_pathState; return true; } + /// @brief Query the active path-state limits used by this camera instance. virtual bool tryGetPathStateLimits(PathStateLimits& out) const override { out = m_pathLimits; return true; } + /// @brief Query the derived spherical-target state corresponding to the current path-state evaluation. virtual bool tryGetSphericalTargetState(typename base_t::SphericalTargetState& out) const override { out.target = m_targetPosition; @@ -126,6 +134,7 @@ class CPathCamera final : public CSphericalTargetCamera return true; } + /// @brief Replace only the tracked target position and rebuild the current path pose against it. virtual bool trySetSphericalTarget(const hlsl::float64_t3& targetPosition) override { if (m_targetPosition == targetPosition) @@ -141,6 +150,7 @@ class CPathCamera final : public CSphericalTargetCamera return false; } + /// @brief Replace the current path state after sanitizing it through the active path model. virtual bool trySetPathState(const PathState& state) override { if (!m_pathModel.resolveState) @@ -161,6 +171,7 @@ class CPathCamera final : public CSphericalTargetCamera return false; } + /// @brief Replace the derived path distance while preserving the rest of the typed path state. virtual bool trySetSphericalDistance(float distance) override { SCameraPathDistanceUpdateResult distanceUpdate = {}; @@ -184,16 +195,19 @@ class CPathCamera final : public CSphericalTargetCamera virtual std::string_view getIdentifier() const override { return SCameraPathDefaults::Identifier; } + /// @brief Return the currently installed path model. inline const path_model_t& getPathModel() const { return m_pathModel; } + /// @brief Return the current path-state limits enforced by this camera instance. inline const path_limits_t& getPathStateLimits() const { return m_pathLimits; } + /// @brief Replace the active path-state limits after sanitizing the current path state against them. inline bool setPathStateLimits(path_limits_t pathLimits) { if (!CCameraPathUtilities::sanitizePathLimits(pathLimits) || !m_pathModel.resolveState) @@ -216,6 +230,7 @@ class CPathCamera final : public CSphericalTargetCamera return false; } + /// @brief Replace the active path model after validating that it can resolve the current path state. inline bool setPathModel(path_model_t pathModel) { if (!isPathModelComplete(pathModel)) @@ -242,11 +257,13 @@ class CPathCamera final : public CSphericalTargetCamera static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate | CVirtualGimbalEvent::RollLeft | CVirtualGimbalEvent::RollRight; + /// @brief Check whether a path model provides all callbacks required by the runtime camera. static inline bool isPathModelComplete(const path_model_t& pathModel) { return pathModel.resolveState && pathModel.controlLaw && pathModel.integrate && pathModel.evaluate && pathModel.updateDistance; } + /// @brief Attempt to initialize the runtime path state and pose from one model/limit pair. inline bool tryInitializePathRig(const hlsl::float64_t3& position, path_model_t pathModel, path_limits_t pathLimits) { if (!CCameraPathUtilities::sanitizePathLimits(pathLimits)) @@ -265,6 +282,7 @@ class CPathCamera final : public CSphericalTargetCamera return refreshFromPathState(); } + /// @brief Initialize the path rig with graceful fallback to the shared default model and limits. inline void initializePathRig(const hlsl::float64_t3& position, path_model_t pathModel, path_limits_t pathLimits) { path_limits_t sanitizedLimits = pathLimits; @@ -289,6 +307,7 @@ class CPathCamera final : public CSphericalTargetCamera path_limits_t m_pathLimits = CCameraPathUtilities::makeDefaultPathLimits(); PathState m_pathState = CCameraPathUtilities::makeDefaultPathState(CCameraPathUtilities::makeDefaultPathLimits().minU); + /// @brief Evaluate the current path state into a canonical pose and write it back to the runtime gimbal. bool refreshFromPathState(bool* outManipulated = nullptr) { if (!m_pathModel.evaluate) diff --git a/common/include/camera/CPlanarProjection.hpp b/common/include/camera/CPlanarProjection.hpp index 7a5255363..722484bb4 100644 --- a/common/include/camera/CPlanarProjection.hpp +++ b/common/include/camera/CPlanarProjection.hpp @@ -6,12 +6,17 @@ namespace nbl::core { + /// @brief Range-backed concrete implementation of `IPlanarProjection`. + /// + /// The template owns a caller-selected contiguous container of planar projection + /// entries and keeps their viewport-local binding layouts together with the camera. template ProjectionsRange> class CPlanarProjection : public IPlanarProjection { public: virtual ~CPlanarProjection() = default; + /// @brief Create a planar projection wrapper only when a valid camera instance is available. inline static core::smart_refctd_ptr create(core::smart_refctd_ptr&& camera) { if (!camera) @@ -20,17 +25,20 @@ namespace nbl::core return core::smart_refctd_ptr(new CPlanarProjection(core::smart_refctd_ptr(camera)), core::dont_grab); } + /// @brief Return the number of stored planar projection entries. virtual uint32_t getLinearProjectionCount() const override { return static_cast(m_projections.size()); } + /// @brief Return one stored planar projection entry through the linear base interface. virtual const ILinearProjection::CProjection& getLinearProjection(uint32_t index) const override { assert(index < m_projections.size()); return m_projections[index]; } + /// @brief Expose mutable access to the owned planar projection range. inline ProjectionsRange& getPlanarProjections() { return m_projections; diff --git a/common/include/camera/CSphericalTargetCamera.hpp b/common/include/camera/CSphericalTargetCamera.hpp index 7094f9d1b..97a88b5b9 100644 --- a/common/include/camera/CSphericalTargetCamera.hpp +++ b/common/include/camera/CSphericalTargetCamera.hpp @@ -78,6 +78,90 @@ class CSphericalTargetCamera : public ICamera protected: using SphericalBasis = SCameraTargetRelativeBasis; + /// @brief Return the current canonical target-relative state stored by the spherical rig. + inline SCameraTargetRelativeState currentTargetRelativeState() const + { + return { + .target = m_targetPosition, + .orbitUv = m_orbitUv, + .distance = m_distance + }; + } + + /// @brief Replace the stored target-relative state without touching the gimbal pose yet. + inline void adoptTargetRelativeState(const SCameraTargetRelativeState& state) + { + m_targetPosition = state.target; + m_orbitUv = state.orbitUv; + m_distance = state.distance; + } + + /// @brief Extract one rigid reference transform from the optional external override or the current gimbal pose. + inline bool tryExtractReferenceTransform(CReferenceTransform& outReference, const hlsl::float64_t4x4* referenceFrame) + { + return m_gimbal.extractReferenceTransform(&outReference, referenceFrame); + } + + /// @brief Resolve the current target-relative state from one rigid reference position around the current target. + inline bool tryResolveReferenceTargetRelativeState(const CReferenceTransform& reference, SCameraTargetRelativeState& outState) const + { + return CCameraTargetRelativeUtilities::tryBuildTargetRelativeStateFromPosition( + m_targetPosition, + hlsl::float64_t3(reference.frame[3]), + MinDistance, + MaxDistance, + outState); + } + + /// @brief Resolve the top-down yaw encoded by a rigid reference orientation. + static inline double resolveTopDownYawFromReference(const CReferenceTransform& reference, const double fallbackYaw) + { + const auto basis = hlsl::CCameraMathUtilities::getQuaternionBasisMatrix(reference.orientation); + const auto planarUp = hlsl::float64_t2(basis[1].x, basis[1].y); + constexpr auto Epsilon = static_cast(base_t::TinyScalarEpsilon); + if (!hlsl::CCameraMathUtilities::isNearlyZeroVector(planarUp, Epsilon)) + return hlsl::atan2(planarUp.y, planarUp.x); + + const auto planarRight = hlsl::float64_t2(basis[0].x, basis[0].y); + if (!hlsl::CCameraMathUtilities::isNearlyZeroVector(planarRight, Epsilon)) + return hlsl::atan2(planarRight.x, -planarRight.y); + + return fallbackYaw; + } + + /// @brief Project one rigid reference pose onto the legal top-down state manifold around the current target. + inline bool tryResolveReferenceTopDownState(const CReferenceTransform& reference, SCameraTargetRelativeState& outState) const + { + const auto offset = hlsl::float64_t3(reference.frame[3]) - m_targetPosition; + const auto distance = hlsl::length(offset); + if (!hlsl::CCameraMathUtilities::isFiniteScalar(distance) || + distance <= static_cast(base_t::TinyScalarEpsilon)) + { + return false; + } + + outState = currentTargetRelativeState(); + outState.distance = static_cast(std::clamp( + distance, + static_cast(MinDistance), + static_cast(MaxDistance))); + outState.orbitUv.x = resolveTopDownYawFromReference(reference, m_orbitUv.x); + outState.orbitUv.y = SCameraTargetRelativeRigDefaults::TopDownPitchRad; + return true; + } + + /// @brief Project one rigid reference pose onto the legal fixed-angle isometric manifold around the current target. + inline bool tryResolveReferenceIsometricState(const CReferenceTransform& reference, SCameraTargetRelativeState& outState) const + { + if (!tryResolveReferenceTargetRelativeState(reference, outState)) + return false; + + outState.orbitUv = hlsl::float64_t2( + SCameraTargetRelativeRigDefaults::IsometricYawRad, + SCameraTargetRelativeRigDefaults::IsometricPitchRad); + return true; + } + inline SphericalBasis computeBasis(const hlsl::float64_t2& orbitUv, float distance) const { SphericalBasis basis; diff --git a/common/include/camera/CTopDownCamera.hpp b/common/include/camera/CTopDownCamera.hpp index a8cc20fed..63d16872c 100644 --- a/common/include/camera/CTopDownCamera.hpp +++ b/common/include/camera/CTopDownCamera.hpp @@ -9,6 +9,10 @@ namespace nbl::core { +/// @brief Target-relative camera constrained to look straight down at the tracked target. +/// +/// Yaw may still rotate the view around the vertical axis, while pitch is fixed to +/// the top-down angle and translation moves the tracked target in the view plane. class CTopDownCamera final : public CSphericalTargetCamera { public: @@ -24,11 +28,25 @@ class CTopDownCamera final : public CSphericalTargetCamera const typename base_t::CGimbal& getGimbal() override { return m_gimbal; } + /// @brief Apply one frame of top-down yaw rotation, planar translation, and distance changes. virtual bool manipulate(std::span virtualEvents, const hlsl::float64_t4x4* referenceFrame = nullptr) override { if (not virtualEvents.size() and not referenceFrame) return false; + if (referenceFrame) + { + CReferenceTransform reference = {}; + SCameraTargetRelativeState resolvedState = {}; + if (!tryExtractReferenceTransform(reference, referenceFrame) || + !tryResolveReferenceTopDownState(reference, resolvedState)) + { + return false; + } + + adoptTargetRelativeState(resolvedState); + } + const auto impulse = m_gimbal.accumulate(virtualEvents); const auto deltaRotation = scaleVirtualRotation(impulse.dVirtualRotation); @@ -47,6 +65,7 @@ class CTopDownCamera final : public CSphericalTargetCamera virtual uint32_t getAllowedVirtualEvents() const override { return AllowedVirtualEvents; } virtual CameraKind getKind() const override { return CameraKind::TopDown; } + /// @brief Return the stable user-facing identifier for this concrete camera kind. virtual std::string_view getIdentifier() const override { return "Top-Down Camera"; } private: diff --git a/common/include/camera/CTurntableCamera.hpp b/common/include/camera/CTurntableCamera.hpp index 315526dd6..dc44d23a3 100644 --- a/common/include/camera/CTurntableCamera.hpp +++ b/common/include/camera/CTurntableCamera.hpp @@ -13,6 +13,10 @@ namespace nbl::core { +/// @brief Target-relative camera that behaves like a classic turntable around a fixed target. +/// +/// The camera exposes yaw, bounded pitch, and distance changes while keeping the +/// target fixed in space and avoiding arbitrary planar target translation. class CTurntableCamera final : public CSphericalTargetCamera { public: @@ -28,11 +32,26 @@ class CTurntableCamera final : public CSphericalTargetCamera const typename base_t::CGimbal& getGimbal() override { return m_gimbal; } + /// @brief Apply one frame of yaw, bounded pitch, and distance input around the tracked target. virtual bool manipulate(std::span virtualEvents, const hlsl::float64_t4x4* referenceFrame = nullptr) override { if (not virtualEvents.size() and not referenceFrame) return false; + if (referenceFrame) + { + CReferenceTransform reference = {}; + SCameraTargetRelativeState resolvedState = {}; + if (!tryExtractReferenceTransform(reference, referenceFrame) || + !tryResolveReferenceTargetRelativeState(reference, resolvedState)) + { + return false; + } + + resolvedState.orbitUv.y = std::clamp(resolvedState.orbitUv.y, MinPitch, MaxPitch); + adoptTargetRelativeState(resolvedState); + } + const auto impulse = m_gimbal.accumulate(virtualEvents); const double deltaYaw = scaleVirtualRotation(impulse.dVirtualRotation.y); @@ -48,6 +67,7 @@ class CTurntableCamera final : public CSphericalTargetCamera virtual uint32_t getAllowedVirtualEvents() const override { return AllowedVirtualEvents; } virtual CameraKind getKind() const override { return CameraKind::Turntable; } + /// @brief Return the stable user-facing identifier for this concrete camera kind. virtual std::string_view getIdentifier() const override { return "Turntable Camera"; } static inline constexpr float MinDistance = base_t::MinDistance; diff --git a/common/include/camera/ICamera.hpp b/common/include/camera/ICamera.hpp index b483fad5c..8d2d3aef0 100644 --- a/common/include/camera/ICamera.hpp +++ b/common/include/camera/ICamera.hpp @@ -21,16 +21,26 @@ namespace nbl::core class ICamera : virtual public core::IReferenceCounted { public: + /// @brief Shared lower bound used by spherical and path rigs for valid camera distance. static inline constexpr float SphericalMinDistance = 0.1f; + /// @brief Shared upper bound used by spherical and path rigs for valid camera distance. static inline constexpr float SphericalMaxDistance = 10000.f; + /// @brief Base runtime translation magnitude represented by a unit virtual move event. static inline constexpr double VirtualTranslationStep = 0.01; + /// @brief Default multiplier applied to virtual translation magnitudes. static inline constexpr double DefaultMoveSpeedScale = VirtualTranslationStep; + /// @brief Default multiplier applied to virtual rotation magnitudes. static inline constexpr double DefaultRotationSpeedScale = 0.003; + /// @brief Shared scalar epsilon used by typed tooling comparisons. static inline constexpr double ScalarTolerance = 1e-6; + /// @brief Very small epsilon used when exact replay helpers need stricter comparisons. static inline constexpr double TinyScalarEpsilon = 1e-9; + /// @brief Default world-space position tolerance used by pose comparisons. static inline constexpr double DefaultPositionTolerance = 2.0 * ScalarTolerance; + /// @brief Default angular tolerance in degrees used by pose and state comparisons. static inline constexpr double DefaultAngularToleranceDeg = 0.1; + /// @brief Camera-local multipliers applied when translating virtual events into motion. struct SMotionConfig { /// @brief Camera-local scales applied by implementations to virtual motion magnitude. @@ -38,6 +48,7 @@ class ICamera : virtual public core::IReferenceCounted double rotationSpeedScale = DefaultRotationSpeedScale; }; + /// @brief Stable runtime camera-family identifier used by tooling, metadata, and default presets. enum class CameraKind : uint8_t { Unknown, @@ -54,6 +65,7 @@ class ICamera : virtual public core::IReferenceCounted Path }; + /// @brief Optional typed capabilities exposed by a concrete runtime camera implementation. enum CameraCapability : uint32_t { None = 0u, @@ -61,6 +73,7 @@ class ICamera : virtual public core::IReferenceCounted DynamicPerspectiveFov = core::createBitmask({ 1 }) }; + /// @brief Typed goal-state fragments that tooling may capture from or apply to a camera. enum GoalStateMask : uint32_t { GoalStateNone = 0u, @@ -69,25 +82,42 @@ class ICamera : virtual public core::IReferenceCounted GoalStatePath = core::createBitmask({ 2 }) }; + /// @brief Canonical spherical-target state shared by orbit-like cameras. + /// + /// The state stores the tracked target position, orbit angles in `orbitUv`, + /// and distance limits needed by tooling that wants to capture or reapply a + /// target-relative camera pose without going through free-form setters. struct SphericalTargetState { + /// @brief Tracked target position in world space. hlsl::float64_t3 target = hlsl::float64_t3(0.0); + /// @brief Orbit yaw and pitch around the target, expressed in radians. hlsl::float64_t2 orbitUv = hlsl::float64_t2(0.0); + /// @brief Current camera-to-target distance. float distance = 0.f; + /// @brief Lowest distance that remains valid for the current camera. float minDistance = 0.f; + /// @brief Highest distance that remains valid for the current camera. float maxDistance = 0.f; }; + /// @brief Typed authored state used by cameras with derived perspective behavior. struct DynamicPerspectiveState { + /// @brief Authored reference FOV in degrees. float baseFov = 0.f; + /// @brief Distance at which `baseFov` should be preserved. float referenceDistance = 0.f; }; + /// @brief Limits constraining reusable `PathState` coordinates for `Path Rig` cameras. struct PathStateLimits { + /// @brief Minimal valid `u` coordinate after path-state sanitization. double minU = static_cast(SphericalMinDistance); + /// @brief Minimal valid radial distance derived from the `(u, v)` pair. hlsl::float64_t minDistance = static_cast(SphericalMinDistance); + /// @brief Maximal valid radial distance derived from the `(u, v)` pair. hlsl::float64_t maxDistance = static_cast(SphericalMaxDistance); }; @@ -99,21 +129,28 @@ class ICamera : virtual public core::IReferenceCounted /// hot runtime path still stays event-only through `manipulate(...)`. struct PathState { + /// @brief Primary path-progress coordinate interpreted by the active path model. double s = 0.0; + /// @brief First lateral/shape coordinate interpreted by the active path model. double u = 0.0; + /// @brief Second lateral/shape coordinate interpreted by the active path model. double v = 0.0; + /// @brief Roll around the path-model forward axis, expressed in radians. double roll = 0.0; + /// @brief Pack the state into one four-component vector for math helpers and persistence tooling. inline hlsl::float64_t4 asVector() const { return hlsl::float64_t4(s, u, v, roll); } + /// @brief Project the state onto the shared translation-style view used by replay helpers. inline hlsl::float64_t3 asTranslationVector() const { return hlsl::float64_t3(u, v, s); } + /// @brief Rebuild one path state from the packed vector representation. static inline PathState fromVector(const hlsl::float64_t4& value) { return { @@ -124,6 +161,7 @@ class ICamera : virtual public core::IReferenceCounted }; } + /// @brief Rebuild one path state from the translation-style helper representation. static inline PathState fromTranslationVector(const hlsl::float64_t3& value, const double pathRoll = 0.0) { return { @@ -144,6 +182,7 @@ class ICamera : virtual public core::IReferenceCounted CGimbal(typename base_t::SCreationParameters&& parameters) : base_t(std::move(parameters)) { updateView(); } ~CGimbal() = default; + /// @brief Rebuild the cached world-to-view matrix from the current gimbal pose. inline void updateView() { const auto& gRight = base_t::getXAxis(), gUp = base_t::getYAxis(), gForward = base_t::getZAxis(); @@ -157,6 +196,7 @@ class ICamera : virtual public core::IReferenceCounted m_viewMatrix[2u] = hlsl::float64_t4(gForward, -hlsl::dot(gForward, position)); } + /// @brief Return the cached world-to-view matrix derived from the current pose. inline const hlsl::float64_t3x4& getViewMatrix() const { return m_viewMatrix; } private: @@ -166,6 +206,7 @@ class ICamera : virtual public core::IReferenceCounted class SScopedMotionScaleOverride { public: + /// @brief Temporarily override both motion scales and restore the previous values on destruction. SScopedMotionScaleOverride(ICamera* camera, const double moveScale, const double rotationScale) : m_camera(camera) { @@ -204,26 +245,33 @@ class ICamera : virtual public core::IReferenceCounted ICamera() {} virtual ~ICamera() = default; + /// @brief Return the mutable gimbal backing the runtime camera pose. virtual const CGimbal& getGimbal() = 0u; /// @brief Consume virtual events only. /// /// Raw input binding and absolute goal solving live outside `ICamera`. virtual bool manipulate(std::span virtualEvents, const hlsl::float64_t4x4* referenceFrame = nullptr) = 0; + /// @brief Apply one frame of virtual events while temporarily overriding the camera-local motion scales. inline bool manipulateWithMotionScales(std::span virtualEvents, const hlsl::float64_t4x4* referenceFrame, const double moveScale, const double rotationScale) { auto scopedOverride = overrideMotionScales(moveScale, rotationScale); return manipulate(virtualEvents, referenceFrame); } + /// @brief Apply one frame of virtual events with unit translation and rotation scales. inline bool manipulateWithUnitMotionScales(std::span virtualEvents, const hlsl::float64_t4x4* referenceFrame = nullptr) { return manipulateWithMotionScales(virtualEvents, referenceFrame, 1.0, 1.0); } + /// @brief Return the semantic virtual-event mask accepted by this camera kind. virtual uint32_t getAllowedVirtualEvents() const = 0u; + /// @brief Return the stable camera-family identifier for this concrete runtime camera. virtual CameraKind getKind() const = 0; + /// @brief Return the optional typed capabilities exposed by this camera implementation. virtual uint32_t getCapabilities() const { return None; } + /// @brief Return the typed goal-state fragments that tooling may safely use with this camera. virtual uint32_t getGoalStateMask() const { uint32_t mask = GoalStateNone; @@ -234,117 +282,144 @@ class ICamera : virtual public core::IReferenceCounted return mask; } + /// @brief Return the stable human-readable identifier for this concrete camera instance. virtual std::string_view getIdentifier() const = 0u; + /// @brief Check whether the camera exposes the requested optional capability. inline bool hasCapability(CameraCapability capability) const { return (getCapabilities() & capability) == capability; } + /// @brief Check whether the camera can exchange the requested typed goal-state fragment. inline bool supportsGoalState(GoalStateMask goalState) const { return (getGoalStateMask() & goalState) == goalState; } + /// @brief Query the current spherical-target state when the camera exposes it. virtual bool tryGetSphericalTargetState(SphericalTargetState& out) const { return false; } + /// @brief Replace only the tracked target position for spherical-target cameras. virtual bool trySetSphericalTarget(const hlsl::float64_t3& target) { return false; } + /// @brief Replace only the tracked target distance for spherical-target cameras. virtual bool trySetSphericalDistance(float distance) { return false; } + /// @brief Query the current derived dynamic perspective FOV when the camera exposes it. virtual bool tryGetDynamicPerspectiveFov(float& outFov) const { return false; } + /// @brief Query the current authored dynamic perspective state when the camera exposes it. virtual bool tryGetDynamicPerspectiveState(DynamicPerspectiveState& out) const { return false; } + /// @brief Replace the authored dynamic perspective state when the camera exposes it. virtual bool trySetDynamicPerspectiveState(const DynamicPerspectiveState& state) { return false; } + /// @brief Query the current typed path state when the camera exposes it. virtual bool tryGetPathState(PathState& out) const { return false; } + /// @brief Query the active typed limits constraining the current path state. virtual bool tryGetPathStateLimits(PathStateLimits& out) const { return false; } + /// @brief Replace the current typed path state when the camera exposes it. virtual bool trySetPathState(const PathState& state) { return false; } + /// @brief Update only the translation motion scale used by the camera runtime. inline void setMoveSpeedScale(double scalar) { m_motionConfig.moveSpeedScale = scalar; } + /// @brief Update only the rotation motion scale used by the camera runtime. inline void setRotationSpeedScale(double scalar) { m_motionConfig.rotationSpeedScale = scalar; } + /// @brief Update both translation and rotation motion scales at once. inline void setMotionScales(const double moveScale, const double rotationScale) { setMoveSpeedScale(moveScale); setRotationSpeedScale(rotationScale); } + /// @brief Return the current translation motion scale. inline double getMoveSpeedScale() const { return m_motionConfig.moveSpeedScale; } + /// @brief Return the current rotation motion scale. inline double getRotationSpeedScale() const { return m_motionConfig.rotationSpeedScale; } + /// @brief Return the full motion-scale bundle. inline const SMotionConfig& getMotionConfig() const { return m_motionConfig; } + /// @brief Return the effective world-space translation represented by a unit virtual move event. inline double getScaledVirtualTranslationMagnitude() const { return VirtualTranslationStep * getMoveSpeedScale(); } + /// @brief Return the raw translation magnitude before applying the camera-local move scale. inline double getUnscaledVirtualTranslationMagnitude() const { return VirtualTranslationStep; } + /// @brief Scale one scalar translation magnitude through the active move scale. inline double scaleVirtualTranslation(const double magnitude) const { return magnitude * getScaledVirtualTranslationMagnitude(); } + /// @brief Scale one translation vector through the active move scale. template inline hlsl::camera_vector_t scaleVirtualTranslation(const hlsl::camera_vector_t& magnitude) const { return magnitude * static_cast(getScaledVirtualTranslationMagnitude()); } + /// @brief Scale one scalar translation magnitude without applying the camera-local move scale. inline double scaleUnscaledVirtualTranslation(const double magnitude) const { return magnitude * getUnscaledVirtualTranslationMagnitude(); } + /// @brief Scale one translation vector without applying the camera-local move scale. template inline hlsl::camera_vector_t scaleUnscaledVirtualTranslation(const hlsl::camera_vector_t& magnitude) const { return magnitude * static_cast(getUnscaledVirtualTranslationMagnitude()); } + /// @brief Scale one scalar rotation magnitude through the active rotation scale. inline double scaleVirtualRotation(const double magnitude) const { return magnitude * getRotationSpeedScale(); } + /// @brief Scale one rotation vector through the active rotation scale. template inline hlsl::camera_vector_t scaleVirtualRotation(const hlsl::camera_vector_t& magnitude) const { return magnitude * static_cast(getRotationSpeedScale()); } + /// @brief Create a scoped helper that restores the previous motion scales on destruction. inline SScopedMotionScaleOverride overrideMotionScales(const double moveScale, const double rotationScale) { return SScopedMotionScaleOverride(this, moveScale, rotationScale); diff --git a/common/include/camera/IGimbal.hpp b/common/include/camera/IGimbal.hpp index be0b96fe4..7e426300d 100644 --- a/common/include/camera/IGimbal.hpp +++ b/common/include/camera/IGimbal.hpp @@ -6,12 +6,22 @@ namespace nbl::core { + /// @brief Optional rigid reference frame used to reinterpret a frame of semantic camera input. + /// + /// Some camera consumers replay authored input relative to an external frame + /// instead of the current camera pose. This bundle stores the rigid transform + /// and its orientation in a form ready for `IGimbal::transform(...)`. struct CReferenceTransform { hlsl::float64_t4x4 frame; hlsl::camera_quaternion_t orientation = hlsl::CCameraMathUtilities::makeIdentityQuaternion(); }; + /// @brief Generic world-space gimbal used by runtime cameras and tracked targets. + /// + /// The gimbal owns position, orientation, scale, and an orthonormal local basis. + /// It also provides the shared `accumulate(...)` helper that turns semantic + /// `CVirtualGimbalEvent` batches into translation, rotation, and scale impulses. template requires is_any_of_v class IGimbal @@ -24,6 +34,7 @@ namespace nbl::core /// @brief underlying type for world matrix (TRS) using model_matrix_t = hlsl::matrix; + /// @brief One frame of accumulated virtual translation, rotation, and scaling intent. struct VirtualImpulse { vector_t<3u> dVirtualTranslate { 0.0f }, dVirtualRotation { 0.0f }, dVirtualScale { 1.0f }; @@ -118,12 +129,14 @@ namespace nbl::core return impulse; } + /// @brief Accumulate one frame of virtual events using the current gimbal basis as the reference frame. template VirtualImpulse accumulate(std::span virtualEvents) { return accumulate(virtualEvents, getXAxis(), getYAxis(), getZAxis()); } + /// @brief Construction-time pose for one gimbal instance. struct SCreationParameters { vector_t<3u> position; @@ -141,12 +154,14 @@ namespace nbl::core updateOrthonormalOrientationBase(); } + /// @brief Enter manipulation mode and reset the per-frame manipulation counter. void begin() { m_isManipulating = true; m_counter = 0u; } + /// @brief Replace the world-space position while the gimbal is in manipulation mode. inline void setPosition(const vector_t<3u>& position) { assert(m_isManipulating); @@ -157,11 +172,13 @@ namespace nbl::core m_position = position; } + /// @brief Replace the scale component stored by the gimbal. inline void setScale(const vector_t<3u>& scale) { m_scale = scale; } + /// @brief Replace the orientation while keeping the orthonormal basis normalized. inline void setOrientation(const quaternion_t& orientation) { assert(m_isManipulating); @@ -173,12 +190,14 @@ namespace nbl::core updateOrthonormalOrientationBase(); } + /// @brief Apply a prebuilt rigid reference transform and an accumulated impulse in one step. inline void transform(const CReferenceTransform& reference, const VirtualImpulse& impulse) { setOrientation(reference.orientation * hlsl::CCameraMathUtilities::makeQuaternionFromEulerRadiansYXZ(impulse.dVirtualRotation)); setPosition(hlsl::mul(hlsl::float64_t4(impulse.dVirtualTranslate, 1), reference.frame).xyz); } + /// @brief Rotate the gimbal around a world-space axis by the requested angle in radians. inline void rotate(const vector_t<3u>& axis, float dRadians) { assert(m_isManipulating); @@ -191,6 +210,7 @@ namespace nbl::core updateOrthonormalOrientationBase(); } + /// @brief Translate the gimbal directly in world space. inline void move(vector_t<3u> delta) { assert(m_isManipulating); @@ -203,21 +223,25 @@ namespace nbl::core m_position = newPosition; } + /// @brief Translate the gimbal along its local right axis. inline void strafe(precision_t distance) { move(getXAxis() * distance); } + /// @brief Translate the gimbal along its local up axis. inline void climb(precision_t distance) { move(getYAxis() * distance); } + /// @brief Translate the gimbal along its local forward axis. inline void advance(precision_t distance) { move(getZAxis() * distance); } + /// @brief Leave manipulation mode after all pose updates for the current frame are finished. inline void end() { m_isManipulating = false; @@ -286,6 +310,7 @@ namespace nbl::core /// @brief Returns true if gimbal records a manipulation inline bool isManipulating() const { return m_isManipulating; } + /// @brief Build a rigid reference transform either from an external frame or from the current gimbal pose. bool extractReferenceTransform(CReferenceTransform* out, const hlsl::float64_t4x4* referenceFrame = nullptr) { if (not out) diff --git a/common/include/camera/ILinearProjection.hpp b/common/include/camera/ILinearProjection.hpp index 251336c19..5c229c7a7 100644 --- a/common/include/camera/ILinearProjection.hpp +++ b/common/include/camera/ILinearProjection.hpp @@ -20,15 +20,16 @@ class ILinearProjection : virtual public core::IReferenceCounted core::smart_refctd_ptr m_camera; public: - /// @brief underlying type for linear world TRS matrix + /// @brief World transform type expected by the linear projection helpers. using model_matrix_t = typename decltype(m_camera)::pointee::CGimbal::model_matrix_t; - /// @brief underlying type for linear concatenated matrix + /// @brief Matrix type used for fully concatenated linear transforms. using concatenated_matrix_t = hlsl::float64_t4x4; - /// @brief underlying type for linear inverse of concatenated matrix + /// @brief Optional inverse of a concatenated transform when the matrix is not singular. using inv_concatenated_matrix_t = std::optional; + /// @brief One concrete linear projection matrix together with cached inverse metadata. struct CProjection : public IProjection { using IProjection::IProjection; @@ -64,6 +65,7 @@ class ILinearProjection : virtual public core::IReferenceCounted } protected: + /// @brief Replace the projection matrix and rebuild cached handedness and inverse information. inline void setProjectionMatrix(const projection_matrix_t& matrix) { m_projectionMatrix = matrix; @@ -92,9 +94,12 @@ class ILinearProjection : virtual public core::IReferenceCounted bool m_isProjectionSingular; }; + /// @brief Return the number of linear projection entries owned by the concrete wrapper. virtual uint32_t getLinearProjectionCount() const = 0; + /// @brief Return one linear projection entry by index. virtual const CProjection& getLinearProjection(uint32_t index) const = 0; + /// @brief Replace the camera referenced by this projection wrapper. inline bool setCamera(core::smart_refctd_ptr&& camera) { if (camera) @@ -106,6 +111,7 @@ class ILinearProjection : virtual public core::IReferenceCounted return false; } + /// @brief Return the camera referenced by this projection wrapper. inline ICamera* getCamera() { return m_camera.get(); diff --git a/common/include/camera/IPerspectiveProjection.hpp b/common/include/camera/IPerspectiveProjection.hpp index 85035d282..5806089f8 100644 --- a/common/include/camera/IPerspectiveProjection.hpp +++ b/common/include/camera/IPerspectiveProjection.hpp @@ -22,6 +22,7 @@ namespace nbl::core class IPerspectiveProjection : public ILinearProjection { public: + /// @brief One quad projection entry described by a pretransform and a viewport projection. struct CProjection : ILinearProjection::CProjection { using base_t = ILinearProjection::CProjection; @@ -32,6 +33,7 @@ class IPerspectiveProjection : public ILinearProjection setQuadTransform(pretransform, viewport); } + /// @brief Rebuild the concatenated quad projection from its authored components. inline void setQuadTransform(const ILinearProjection::model_matrix_t& pretransform, ILinearProjection::concatenated_matrix_t viewport) { auto concatenated = hlsl::mul(hlsl::getMatrix3x4As4x4(pretransform), viewport); @@ -41,7 +43,9 @@ class IPerspectiveProjection : public ILinearProjection m_viewport = viewport; } + /// @brief Return the authored pretransform applied before the viewport projection. inline const ILinearProjection::model_matrix_t& getPretransform() const { return m_pretransform; } + /// @brief Return the authored viewport projection matrix stored for this quad. inline const ILinearProjection::concatenated_matrix_t& getViewportProjection() const { return m_viewport; } private: diff --git a/common/include/camera/IPlanarProjection.hpp b/common/include/camera/IPlanarProjection.hpp index abd236686..4b79f6f66 100644 --- a/common/include/camera/IPlanarProjection.hpp +++ b/common/include/camera/IPlanarProjection.hpp @@ -19,6 +19,7 @@ class IPlanarProjection : public ILinearProjection { using base_t = ILinearProjection::CProjection; + /// @brief Stable runtime classification of supported planar projection parameterizations. enum ProjectionType : uint8_t { Perspective, @@ -42,6 +43,7 @@ class IPlanarProjection : public ILinearProjection CProjection(const CProjection& other) = default; CProjection(CProjection&& other) noexcept = default; + /// @brief Authored parameter bundle stored by one planar projection entry. struct ProjectionParameters { ProjectionType m_type; @@ -66,6 +68,7 @@ class IPlanarProjection : public ILinearProjection float m_zFar; }; + /// @brief Rebuild the concrete projection matrix from the stored parameters. inline void update(bool leftHanded, float aspectRatio) { switch (m_parameters.m_type) @@ -93,6 +96,7 @@ class IPlanarProjection : public ILinearProjection } } + /// @brief Switch the entry to perspective mode and store its authored parameters. inline void setPerspective(float zNear = 0.1f, float zFar = 100.f, float fov = 60.f) { m_parameters.m_type = Perspective; @@ -101,6 +105,7 @@ class IPlanarProjection : public ILinearProjection m_parameters.m_zFar = zFar; } + /// @brief Switch the entry to orthographic mode and store its authored parameters. inline void setOrthographic(float zNear = 0.1f, float zFar = 100.f, float orthoWidth = 10.f) { m_parameters.m_type = Orthographic; @@ -109,8 +114,11 @@ class IPlanarProjection : public ILinearProjection m_parameters.m_zFar = zFar; } + /// @brief Return the authored planar projection parameters. inline const ProjectionParameters& getParameters() const { return m_parameters; } + /// @brief Return the viewport-local input binding layout stored next to this projection entry. inline const ui::IGimbalBindingLayout& getInputBinding() const { return m_inputBinding; } + /// @brief Return mutable access to the viewport-local input binding layout. inline ui::IGimbalBindingLayout& getInputBinding() { return m_inputBinding; } private: CProjection() = default; diff --git a/common/include/camera/IProjection.hpp b/common/include/camera/IProjection.hpp index 32a252e52..b42cb01a3 100644 --- a/common/include/camera/IProjection.hpp +++ b/common/include/camera/IProjection.hpp @@ -6,13 +6,19 @@ namespace nbl::core { -/// @brief Interface class for any type of projection +/// @brief Base interface for any reusable projection model in the camera stack. +/// +/// A projection transforms vectors between some input space and the projection +/// space understood by a concrete viewport or projection consumer. Specialized +/// interfaces such as `ILinearProjection`, `IPlanarProjection`, and +/// `IPerspectiveProjection` refine this abstraction with additional structure. class IProjection { public: - /// @brief underlying type for all vectors we project or un-project (inverse), projections *may* transform vectors in less dimensions + /// @brief Common vector type used by projection and unprojection operations. using projection_vector_t = hlsl::float64_t4; + /// @brief Stable runtime classification of supported projection families. enum class ProjectionType { /// @brief Any raw linear transformation, for example it may represent Perspective, Orthographic, Oblique, Axonometric, Shear projections @@ -32,7 +38,7 @@ class IProjection Count }; - + IProjection() = default; virtual ~IProjection() = default; @@ -51,7 +57,7 @@ class IProjection /// @return `true` when the inverse transform succeeded, otherwise `false`. virtual bool unproject(const projection_vector_t& vecFromProjectionSpace, projection_vector_t& output) const = 0; - /// @brief Return the specific type of the projection. + /// @brief Return the specific projection family implemented by the concrete instance. /// /// Examples include linear, spherical, and thin-lens projections as defined /// by `ProjectionType`. diff --git a/common/include/camera/README.md b/common/include/camera/README.md index 25179c439..0f5824c12 100644 --- a/common/include/camera/README.md +++ b/common/include/camera/README.md @@ -1,474 +1,521 @@ # Shared Camera API -This directory contains the shared camera stack. -A complete runnable integration is [`61_UI`](../../../61_UI/README.md). +This directory contains the reusable camera stack used by [`61_UI`](../../../61_UI/README.md). +It is the source of truth for camera semantics, typed camera state, scripted playback, follow behavior, and camera-focused validation. -## What this stack is +## Scope -This stack is a reusable camera framework with two clearly separated halves: +This stack is: -1. Runtime camera control - The hot path that reacts to input and updates camera pose. -2. Tooling and validation - The sidecar layer used for capture, restore, presets, tracks, playback, scripted validation, and follow. +- a reusable camera runtime based on semantic virtual events +- a typed tooling layer for capture, restore, presets, playback, follow, and scripted validation +- a shared authoring and CI surface reused by `61_UI` -The runtime half is intentionally event-driven. -The tooling half is intentionally state-driven. +This stack is not: -That split is the core design decision. +- the engine-wide Nabla scene graph API +- a generic animation system for arbitrary scene objects +- a setter-heavy runtime camera API +- a `61_UI`-local convenience layer -## What this stack is not +## Design pillars -This stack is not: +The current design is built around five rules. + +### 1. Runtime is event-driven + +The hot path stays on: + +```text +input -> virtual events -> ICamera::manipulate(...) +``` + +Camera models do not expose arbitrary absolute runtime setters for position, target, yaw/pitch/roll, or similar mutable bags of state. + +### 2. Tooling is typed-state driven + +Capture, presets, restore, tracks, and validation use typed sidecar state: + +- [`CCameraGoal.hpp`](CCameraGoal.hpp) +- [`CCameraPreset.hpp`](CCameraPreset.hpp) +- [`CCameraKeyframeTrack.hpp`](CCameraKeyframeTrack.hpp) +- [`CCameraPlaybackTimeline.hpp`](CCameraPlaybackTimeline.hpp) +- [`CCameraSequenceScript.hpp`](CCameraSequenceScript.hpp) + +That layer is allowed to reason in terms of canonical camera state because it is not the hot input path. + +### 3. Input mapping is separate from camera semantics -- engine-core Nabla camera API -- a generic scene animation system -- a direct scene-object-follow system -- a setter-heavy camera API with arbitrary absolute pose mutation in the hot runtime path +The reusable stack keeps these responsibilities separate: -## Design goals +- physical input mapping +- virtual event processing +- camera semantics +- absolute state capture / restore +- scripted playback and validation + +This keeps camera models reusable across keyboard, mouse, ImGuizmo, CI, and headless tools. -The design goals are: +### 4. Projection-local state is allowed, runtime input processing is not -- one semantic command language for keyboard, mouse, gizmo, scripts, and CI -- no input-device assumptions inside camera models -- no viewport glue inside camera models -- no direct dependence on application-specific UI concepts in the reusable camera layer -- best-effort absolute restore for tooling without turning cameras into mutable state bags -- reusable persistence, analysis, playback, follow, and validation helpers +Projection types may own viewport-local binding layouts, but raw input processing stays in the input layer. + +### 5. Follow and scripts stay above camera models + +Follow and scripted playback are shared layers built on top of camera semantics. +They are not hardwired into `ICamera`. ## Namespace split -The stack is split across existing Nabla namespaces: +The stack is intentionally spread across existing Nabla namespaces. - `nbl::hlsl` - math types and camera math helpers + camera math, transform math, pose deltas, interpolation helpers, and reusable vector/quaternion types - `nbl::core` - camera runtime model, goals, presets, tracks, playback, follow, and authored sequence data + runtime camera model, typed goal state, presets, tracks, follow, playback, path-rig helpers, and authored sequence data - `nbl::ui` - binding layouts, input processors, binders, default input mappings, and user-facing presentation/text helpers + input binding layouts, input processors, runtime binders, default input presets, and presentation-facing UI helpers - `nbl::system` - persistence, scripted runtime payloads, scripted parsing, scripted check execution, and follow validation helpers + persistence, scripted runtime parsing, scripted timeline building, scripted check execution, and follow validation The shared camera math is written against `nbl::hlsl`. -Consumers of this stack are expected to talk to camera math through `nbl::hlsl` types and helpers rather than through direct `glm::...` calls. +Consumers are expected to use those `nbl::hlsl` types and helpers directly rather than duplicating local math wrappers. -## Why virtual events and not absolute setters +## Runtime pipeline -The runtime path is intentionally built around virtual events such as: +The hot runtime path is: -- `MoveForward` -- `PanLeft` -- `TiltUp` -- `RollRight` +```text +raw input + -> IGimbalBindingLayout + -> IGimbalInputProcessor / CGimbalInputBinder + -> CVirtualGimbalEvent[] + -> ICamera::manipulate(...) + -> updated camera gimbal and cached view matrix +``` -instead of runtime methods like: +The main building blocks are: -- `setPosition(...)` -- `setTarget(...)` -- `setYawPitchRoll(...)` +- [`CVirtualGimbalEvent.hpp`](CVirtualGimbalEvent.hpp) + shared semantic command language +- [`IGimbal.hpp`](IGimbal.hpp) + gimbal math, event accumulation, and world-space pose handling +- [`ICamera.hpp`](ICamera.hpp) + camera runtime interface and typed tooling hooks +- [`SCameraRigPose.hpp`](SCameraRigPose.hpp) + shared typed pose transport used outside the hot runtime path -The reason is architectural, not cosmetic. +## Input layer -### What the event-driven design buys us +The input layer is split into three parts. -It gives us one shared runtime path for: +### Binding layout -- live keyboard and mouse input -- ImGuizmo manipulation -- scripted playback -- CI validation -- future headless or tool-driven input sources +- [`IGimbalBindingLayout.hpp`](IGimbalBindingLayout.hpp) -It also keeps camera semantics inside the camera type: +This layer stores static mappings from physical inputs to semantic virtual events. +It does not process runtime input. -- `Orbit` means orbit -- `FPS` means FPS -- `Path Rig` means a path-model camera with typed `s/u/v/roll` state +### Input processing -instead of allowing every caller to overwrite camera internals arbitrarily. +- [`IGimbalInputProcessor.hpp`](IGimbalInputProcessor.hpp) +- [`CGimbalInputBinder.hpp`](CGimbalInputBinder.hpp) -### Why the absolute layer still exists +`IGimbalInputProcessor` converts keyboard, mouse, and ImGuizmo input into virtual events. +`CGimbalInputBinder` is the convenience runtime wrapper that collects one frame of virtual events together with per-domain counts. -Tooling still needs absolute-ish operations: +### Shared presets -- capture current state -- restore preset -- scrub playback -- compare two camera states -- run validation against expected state +- [`CCameraInputBindingUtilities.hpp`](CCameraInputBindingUtilities.hpp) +- [`CCameraScriptedUiInputUtilities.hpp`](CCameraScriptedUiInputUtilities.hpp) -That is why the absolute layer exists, but it is kept outside `ICamera`. +These helpers provide shared default input presets for camera kinds and the scripted/UI-facing glue that reuses the same semantic event language. -The intended pattern is: +## Core camera interface -`camera <-> goal/preset/track/solver` +The main runtime interface is [`ICamera.hpp`](ICamera.hpp). -not: +Important properties: -`camera exposes public setters for everything` +- runtime entry point is `manipulate(std::span, const hlsl::float64_t4x4*)` +- cameras own their own `CGimbal` +- motion scaling stays camera-local through `SMotionConfig` +- typed state hooks are optional and exist for tooling, not for direct runtime driving -This is why the design uses: +`ICamera` currently exposes these shared typed states: -- [`CCameraGoal.hpp`](CCameraGoal.hpp) -- [`CCameraGoalSolver.hpp`](CCameraGoalSolver.hpp) -- [`CCameraPreset.hpp`](CCameraPreset.hpp) -- [`CCameraKeyframeTrack.hpp`](CCameraKeyframeTrack.hpp) +- `SphericalTargetState` +- `DynamicPerspectiveState` +- `PathState` +- `PathStateLimits` -instead of making `ICamera` setter-heavy. +The capability and state-mask surface is: -## High-level architecture +- `CameraKind` +- `CameraCapability` +- `GoalStateMask` -There are two main paths. +## Camera families -### Runtime path +### Free cameras -```text -raw input - -> binding layout - -> input processor / binder - -> virtual gimbal events - -> ICamera::manipulate(...) - -> updated camera gimbal / view -``` +- [`CFPSCamera.hpp`](CFPSCamera.hpp) +- [`CFreeLockCamera.hpp`](CFreeLockCamera.hpp) -### Tooling path +These are pose-driven cameras without spherical target semantics. -```text -ICamera - <-> CCameraGoal - <-> CCameraPreset - <-> CCameraKeyframeTrack - <-> CCameraPlaybackTimeline - <-> CCameraSequenceScript -``` +### Spherical-target family -### Follow path +- [`CSphericalTargetCamera.hpp`](CSphericalTargetCamera.hpp) +- [`COrbitCamera.hpp`](COrbitCamera.hpp) +- [`CArcballCamera.hpp`](CArcballCamera.hpp) +- [`CTurntableCamera.hpp`](CTurntableCamera.hpp) +- [`CTopDownCamera.hpp`](CTopDownCamera.hpp) +- [`CIsometricCamera.hpp`](CIsometricCamera.hpp) +- [`CChaseCamera.hpp`](CChaseCamera.hpp) +- [`CDollyCamera.hpp`](CDollyCamera.hpp) -```text -CTrackedTarget + SCameraFollowConfig - -> CCameraGoal - -> CCameraGoalSolver - -> camera state -``` +These cameras share: -### Scripted sequence path +- target position +- distance +- orbit angles in `orbitUv` -```text -CCameraSequenceScript - -> compiled sequence segment - -> scripted runtime timeline - -> scripted check runner - -> runtime logging / CI / screenshots -``` +### Extended-state cameras -## Stack breakdown +- [`CDollyZoomCamera.hpp`](CDollyZoomCamera.hpp) +- [`CPathCamera.hpp`](CPathCamera.hpp) -### 1. Gimbal and semantic commands +`DollyZoom` adds dynamic perspective state. +`Path Rig` adds typed path-rig state and a pluggable path model. -- [`CVirtualGimbalEvent.hpp`](CVirtualGimbalEvent.hpp) -- [`IGimbal.hpp`](IGimbal.hpp) -- [`CCameraMathUtilities.hpp`](CCameraMathUtilities.hpp) +## Path Rig design -This is the mathematical foundation. +`Path Rig` is described explicitly by typed state, typed limits, and a path model. -It defines: +### Typed path state -- the shared semantic event language in `CVirtualGimbalEvent` -- low-level gimbal math -- reusable camera-oriented math helpers in `nbl::hlsl` -- accumulation of multiple semantic commands into one camera impulse +`ICamera::PathState` stores: -This is the shared command language used by all camera types and all runtime input sources. +- `s` + path progress / angular progress in the default model +- `u` + lateral / radial component in the default model +- `v` + vertical component +- `roll` + roll around the view axis / path tangent -### 2. Binding layout +The default shared target-relative model interprets `s/u/v/roll` as a target-relative path rig, but custom models may reuse the same coordinates differently. -- [`IGimbalBindingLayout.hpp`](IGimbalBindingLayout.hpp) +### Typed path limits -This layer stores static mappings such as: +`ICamera::PathStateLimits` stores per-instance limits: -- keyboard key -> virtual event -- mouse input -> virtual event -- ImGuizmo delta -> virtual event +- `minU` +- `minDistance` +- `maxDistance` -This layer does not process runtime input. -It only stores how input should map to the semantic command language. +Those limits are part of the active camera state surface. +They are queried through `tryGetPathStateLimits(...)` and used by the solver, validation, and path-state sanitization. -### 3. Runtime input processing +### Shared path helpers -- [`IGimbalInputProcessor.hpp`](IGimbalInputProcessor.hpp) -- [`CGimbalInputBinder.hpp`](CGimbalInputBinder.hpp) -- [`CCameraInputBindingUtilities.hpp`](CCameraInputBindingUtilities.hpp) +- [`CCameraPathMetadata.hpp`](CCameraPathMetadata.hpp) +- [`CCameraPathUtilities.hpp`](CCameraPathUtilities.hpp) -The main runtime type is `IGimbalInputProcessor`. +`CCameraPathUtilities` provides: -This layer: +- default path metadata and identifiers +- path-state sanitization and comparison +- position-to-state and state-to-pose conversion +- distance updates +- delta and transition helpers +- the default `Path Rig` model -- receives actual keyboard and mouse streams -- receives ImGuizmo transforms -- emits virtual events for the current frame -- stores reusable default keyboard, mouse, and ImGuizmo binding presets for camera kinds +### Path model seam -`CGimbalInputBinder` is the convenience runtime binder that a consumer should usually use. +The path seam is [`SCameraPathModel`](CCameraPathUtilities.hpp). +It is a named typed model, not an ad-hoc runtime hack. -### 4. Camera core +It contains five callbacks: -- [`ICamera.hpp`](ICamera.hpp) +- `resolveState` + normalize requested state or derive initial state from target + position +- `controlLaw` + turn accumulated runtime motion into a `SCameraPathDelta` +- `integrate` + apply one delta to the current state under typed limits +- `evaluate` + convert path state into canonical pose plus target-relative data +- `updateDistance` + retarget state to a requested spherical distance -This is the core interface that camera models implement. +The default model is created by `CCameraPathUtilities::makeDefaultPathModel()`. -Important properties: +### Runtime behavior of `CPathCamera` + +[`CPathCamera.hpp`](CPathCamera.hpp) keeps the public runtime entry point unchanged: -- runtime entry point is `manipulate(...)` -- the runtime path consumes virtual events only -- cameras own their own gimbal and view state -- cameras expose typed optional state hooks only for tooling +```text +virtual events -> gimbal accumulation -> path controlLaw -> integrate -> evaluate -> gimbal pose +``` -`ICamera` also exposes: +`CPathCamera` owns: -- `CameraKind` -- `CameraCapability` -- `GoalStateMask` -- motion config -- typed state hooks used by tooling +- one active `SCameraPathModel` +- one active `PathState` +- one active `PathStateLimits` -### 5. Projection layer +and exposes: -- [`ILinearProjection.hpp`](ILinearProjection.hpp) -- [`IPlanarProjection.hpp`](IPlanarProjection.hpp) -- [`CPlanarProjection.hpp`](CPlanarProjection.hpp) -- [`CLinearProjection.hpp`](CLinearProjection.hpp) -- [`CCubeProjection.hpp`](CCubeProjection.hpp) +- `getPathModel()` +- `getPathStateLimits()` +- `setPathModel(...)` +- `setPathStateLimits(...)` +- `tryGetPathState(...)` +- `trySetPathState(...)` -This layer handles projection state. +Construction is resilient: -Important rule: +- a complete custom model is accepted directly +- an incomplete model falls back to the shared default model +- invalid custom limits are sanitized or replaced with shared defaults + +That means `Path Rig` is first-class and pluggable without changing `ICamera::manipulate(...)`. -- projection may own viewport-local binding layout state -- projection does not own raw input processing +## Goals, presets, tracks, and playback -That separation was one of the major cleanup goals of the refactor. +The tooling side of the stack is built around a shared canonical camera state. -### 6. Goal / preset / track tooling +### Goal layer - [`CCameraGoal.hpp`](CCameraGoal.hpp) - [`CCameraGoalSolver.hpp`](CCameraGoalSolver.hpp) - [`CCameraGoalAnalysis.hpp`](CCameraGoalAnalysis.hpp) -- [`CCameraPreset.hpp`](CCameraPreset.hpp) -- [`CCameraPresetFlow.hpp`](CCameraPresetFlow.hpp) -- [`CCameraKeyframeTrack.hpp`](CCameraKeyframeTrack.hpp) -- [`CCameraPlaybackTimeline.hpp`](CCameraPlaybackTimeline.hpp) -- [`CCameraPersistence.hpp`](CCameraPersistence.hpp) -This is the tooling half of the stack. +`CCameraGoal` is the canonical typed transport object. +It extends [`SCameraRigPose.hpp`](SCameraRigPose.hpp) with optional state such as: -It covers: +- target position +- orbit state +- path state +- dynamic perspective state -- state capture -- compatibility analysis -- best-effort restore -- preset storage -- keyframe playback -- persistence -- UI-facing diagnostics and presentation helpers +`CCameraGoalSolver` is the bridge between typed state and the event-driven runtime: -### 7. Follow +- capture camera state into a goal +- analyze compatibility +- apply what can be applied through typed hooks +- replay virtual events when needed -- [`CCameraFollowUtilities.hpp`](CCameraFollowUtilities.hpp) +This is the main best-effort absolute layer. -Follow is deliberately not part of `ICamera`. +### Presets and preset flow -The tracked subject owns its own gimbal through `CTrackedTarget`. -Follow stays as a policy layer above the camera. +- [`CCameraPreset.hpp`](CCameraPreset.hpp) +- [`CCameraPresetFlow.hpp`](CCameraPresetFlow.hpp) +- [`CCameraPresetPersistence.hpp`](CCameraPresetPersistence.hpp) -That means the camera API does not know about meshes, scene nodes, or any particular UI harness. -It only knows about: +Presets are named `CCameraGoal` wrappers. +`CCameraPresetFlowUtilities` provides the high-level capture/apply helpers that most consumers should call. -- camera -- tracked target gimbal -- follow config -- best-effort goal application +### Keyframe tracks and playback -### 8. Scripted sequence and validation +- [`CCameraKeyframeTrack.hpp`](CCameraKeyframeTrack.hpp) +- [`CCameraKeyframeTrackPersistence.hpp`](CCameraKeyframeTrackPersistence.hpp) +- [`CCameraPlaybackTimeline.hpp`](CCameraPlaybackTimeline.hpp) -- [`CCameraSequenceScript.hpp`](CCameraSequenceScript.hpp) -- [`CCameraScriptedRuntime.hpp`](CCameraScriptedRuntime.hpp) -- [`CCameraScriptedRuntimePersistence.hpp`](CCameraScriptedRuntimePersistence.hpp) -- [`CCameraSequenceScriptedBuilder.hpp`](CCameraSequenceScriptedBuilder.hpp) -- [`CCameraScriptedCheckRunner.hpp`](CCameraScriptedCheckRunner.hpp) -- [`CCameraFollowRegressionUtilities.hpp`](CCameraFollowRegressionUtilities.hpp) +Keyframe tracks are preset-based reusable playback data. +`CCameraPlaybackTimeline` owns only transport-like playback cursor logic. +Higher-level playback policy remains on the consumer side. -This is the reusable scripting and CI half. +### Persistence and file helpers -It supports two levels of representation: +- [`CCameraPersistence.hpp`](CCameraPersistence.hpp) +- [`CCameraFileUtilities.hpp`](CCameraFileUtilities.hpp) -1. Compact authored camera-domain script -2. Expanded frame-by-frame scripted runtime payload +These helpers cover camera presets, tracks, and related shared persistence tasks. -That separation is important. -Authored assets stay short and meaningful. -Expanded runtime payloads stay normalized and reusable. +## Follow layer -## Camera families +Follow is deliberately not baked into `ICamera`. -### Free cameras +### Tracked target -- [`CFPSCamera.hpp`](CFPSCamera.hpp) -- [`CFreeLockCamera.hpp`](CFreeLockCamera.hpp) +- [`CCameraFollowUtilities.hpp`](CCameraFollowUtilities.hpp) -These are pose-driven cameras without spherical target semantics. +The tracked subject is `CTrackedTarget`. +It owns its own `ICamera::CGimbal`. -### Spherical-target family +The follow source of truth is: -- [`CSphericalTargetCamera.hpp`](CSphericalTargetCamera.hpp) -- [`COrbitCamera.hpp`](COrbitCamera.hpp) -- [`CArcballCamera.hpp`](CArcballCamera.hpp) -- [`CTurntableCamera.hpp`](CTurntableCamera.hpp) -- [`CTopDownCamera.hpp`](CTopDownCamera.hpp) -- [`CIsometricCamera.hpp`](CIsometricCamera.hpp) -- [`CChaseCamera.hpp`](CChaseCamera.hpp) -- [`CDollyCamera.hpp`](CDollyCamera.hpp) +- tracked-target pose +- follow mode +- follow config -These cameras share: +not a scene-node id or mesh handle. -- target position -- distance -- orbit angles +### Follow modes -They participate in the shared spherical goal flow. +Current reusable modes are: -### Extended-state cameras +- `OrbitTarget` +- `LookAtTarget` +- `KeepWorldOffset` +- `KeepLocalOffset` -- [`CDollyZoomCamera.hpp`](CDollyZoomCamera.hpp) -- [`CPathCamera.hpp`](CPathCamera.hpp) +### Follow application and validation + +- [`CCameraFollowUtilities.hpp`](CCameraFollowUtilities.hpp) +- [`CCameraFollowRegressionUtilities.hpp`](CCameraFollowRegressionUtilities.hpp) -These extend the shared base with typed extra state: +`CCameraFollowUtilities` builds goal state from tracked target + policy and applies it through `CCameraGoalSolver`. +`CCameraFollowRegressionUtilities` validates lock angle, projected target placement, distance consistency, and spherical writeback behavior. -- `DynamicPerspectiveState` -- `PathState` +## Scripted authoring and validation -## Capabilities and typed state +The scripting side is split into compact authored data and expanded runtime payloads. -The core camera interface exposes: +### Compact authored sequence -- `CameraCapability` -- `GoalStateMask` +- [`CCameraSequenceScript.hpp`](CCameraSequenceScript.hpp) +- [`CCameraSequenceScriptPersistence.hpp`](CCameraSequenceScriptPersistence.hpp) -The currently relevant typed state is: +This is the human-maintainable authored representation. +It stores: -- `SphericalTargetState` -- `DynamicPerspectiveState` -- `PathState` +- camera kind or identifier +- projection presentation requests +- compact goal keyframes +- compact tracked-target keyframes +- continuity thresholds +- capture fractions -The rule is: +It intentionally does not store frame-by-frame low-level event dumps. -- if a camera can round-trip through shared spherical state, do not add fake extra state -- if a camera has real additional semantics that would be lost, add typed state explicitly +### Expanded runtime payload -That is why: +- [`CCameraScriptedRuntime.hpp`](CCameraScriptedRuntime.hpp) +- [`CCameraScriptedRuntimePersistence.hpp`](CCameraScriptedRuntimePersistence.hpp) -- `Chase` and `Dolly` currently stay on shared spherical state -- `DollyZoom` has dynamic perspective state -- `Path Rig` has typed `PathState` and the shared default model interprets it as target-relative `s/u/v/roll` +This layer stores: -## Follow model +- low-level scripted input events +- per-frame checks +- capture frame scheduling +- optional compact sequence payload parsed alongside the low-level runtime payload -Follow is modeled around a tracked target gimbal, not around a scene object id. +### Sequence expansion -### Source of truth +- [`CCameraSequenceScriptedBuilder.hpp`](CCameraSequenceScriptedBuilder.hpp) -The source of truth is: +This converts one compiled sequence segment into frame-by-frame scripted runtime payloads. -- `CTrackedTarget` +### Frame check execution -which literally owns a gimbal. +- [`CCameraScriptedCheckRunner.hpp`](CCameraScriptedCheckRunner.hpp) +- [`CCameraSmokeRegressionUtilities.hpp`](CCameraSmokeRegressionUtilities.hpp) -### Follow modes +This layer evaluates authored per-frame checks against the active runtime state. +It is used by shared smoke and continuity coverage in `61_UI`. -Current modes: +## Projection layer -- `LookAtTarget` -- `OrbitTarget` -- `KeepWorldOffset` -- `KeepLocalOffset` +Projection state is a separate reusable layer. -### Follow invariants +- [`IProjection.hpp`](IProjection.hpp) +- [`ILinearProjection.hpp`](ILinearProjection.hpp) +- [`IPerspectiveProjection.hpp`](IPerspectiveProjection.hpp) +- [`IPlanarProjection.hpp`](IPlanarProjection.hpp) +- [`CLinearProjection.hpp`](CLinearProjection.hpp) +- [`CPlanarProjection.hpp`](CPlanarProjection.hpp) +- [`CCubeProjection.hpp`](CCubeProjection.hpp) +- [`IRange.hpp`](IRange.hpp) -For enabled modes, the camera must stay logically locked to the tracked target: +Important rule: -- camera-to-target direction must match the expected view direction -- projected target center error must stay small when projection is available -- spherical cameras must write target state back consistently -- camera-target distance must remain internally consistent +- projections may own viewport-local binding layouts +- projections do not process raw runtime input -Those invariants are reusable and validated through: +That separation keeps viewport glue out of reusable camera semantics. -- [`CCameraFollowRegressionUtilities.hpp`](CCameraFollowRegressionUtilities.hpp) +## Presentation and UI-facing helpers -## Compact sequence design +The shared layer also exposes small reusable helpers used by consumers such as `61_UI`: -The compact sequence format is deliberately camera-domain. +- [`CCameraPresentationUtilities.hpp`](CCameraPresentationUtilities.hpp) +- [`CCameraProjectionUtilities.hpp`](CCameraProjectionUtilities.hpp) +- [`CCameraTextUtilities.hpp`](CCameraTextUtilities.hpp) +- [`CCameraViewportOverlayUtilities.hpp`](CCameraViewportOverlayUtilities.hpp) +- [`CCameraControlPanelUiUtilities.hpp`](CCameraControlPanelUiUtilities.hpp) +- [`CCameraScriptVisualDebugOverlayUtilities.hpp`](CCameraScriptVisualDebugOverlayUtilities.hpp) -It describes: +These are still shared camera helpers. +They are presentation-facing, but they remain camera-domain rather than `61_UI`-local glue. -- camera kind or identifier -- projection presentation requests -- goal keyframes -- tracked-target keyframes -- continuity thresholds -- capture fractions +## 61_UI integration -It deliberately does not describe: +`61_UI` is the full runnable integration for this stack: -- runtime-specific window actions as authored source data -- frame-by-frame event dumps -- ImGuizmo matrices as authored motion primitives +- [`61_UI/README.md`](../../../61_UI/README.md) -This is why the new continuity asset became small and maintainable instead of being a giant generated dump. +`61_UI` provides: -## Scripted runtime design +- scene setup +- active planar / window routing +- ImGui control panel +- tracked-target visualization +- scripted smoke and continuity assets +- screenshot capture +- local logging -The expanded scripted runtime exists so that a consumer can execute frame-by-frame logic without redefining runtime types locally. +The shared camera layer remains the source of truth for camera semantics. +`61_UI` is the concrete harness that exercises them. -It is split into: +## Local configure, build, and test -- authored parsing and normalization -- timeline finalization -- segment-to-runtime expansion -- per-frame dequeue -- per-frame check evaluation +Current local setup uses the Visual Studio 2022 dynamic preset. -This keeps one runtime from owning a private scripting subsystem. +Configure: -## Current validation story +```powershell +cmake --preset user-configure-dynamic-msvc +``` -The current camera-focused validation is exercised through scripted smoke and continuity tests. +Build `61_UI`: -### Smoke +```powershell +cmake --build build/dynamic/examples_tests/61_UI --config Debug --target 61_ui -- /m:1 +``` -Purpose: +Run the camera-focused tests: -- prove that camera selection and basic scripted manipulation still work -- validate preset, sequence, runtime, and follow helper behavior with small regression checks +```powershell +ctest --test-dir build/dynamic/examples_tests/61_UI -C Debug --output-on-failure -R NBL_61_UI_CAMERA_ +``` -### Continuity +Run the example manually: -Purpose: +```powershell +examples_tests/61_UI/bin/61_ui_d.exe +``` -- prove that camera motion remains smooth frame-to-frame -- prove that follow target lock remains valid during scripted target motion +Run CI-style screenshot capture: -This test now runs on the compact authored sequence format rather than a large expanded frame dump. +```powershell +examples_tests/61_UI/bin/61_ui_d.exe --ci +``` -## Recommended integration patterns +## Minimal integration examples -### Minimal runtime integration +### Runtime input to camera ```cpp auto camera = core::make_smart_refctd_ptr(eye, target); -CGimbalInputBinder binder; -CCameraInputBindingUtilities::applyDefaultCameraInputBindingPreset(binder, *camera); +ui::CGimbalInputBinder binder; +ui::CCameraInputBindingUtilities::applyDefaultCameraInputBindingPreset(binder, *camera); auto collected = binder.collectVirtualEvents(timestamp, { .mouseEvents = { mouseEvents.data(), mouseEvents.size() }, @@ -478,86 +525,65 @@ auto collected = binder.collectVirtualEvents(timestamp, { camera->manipulate(collected.events); ``` -### Preset / tooling integration +### Capture and apply a preset ```cpp -CCameraGoalSolver solver; +core::CCameraGoalSolver solver; auto capture = solver.captureDetailed(camera.get()); if (capture.canUseGoal()) { - CCameraPreset preset; - CCameraPresetUtilities::assignGoalToPreset(preset, capture.goal); + core::CCameraPreset preset; + core::CCameraPresetUtilities::assignGoalToPreset(preset, capture.goal); - auto apply = CCameraPresetFlowUtilities::applyPresetDetailed(solver, camera.get(), preset); + auto apply = core::CCameraPresetFlowUtilities::applyPresetDetailed(solver, camera.get(), preset); if (!apply.succeeded()) { - // report exact vs best-effort or unsupported state + // report unsupported or approximate apply } } ``` -### Follow integration +### Apply follow ```cpp -CTrackedTarget trackedTarget(position, orientation); +core::CTrackedTarget trackedTarget(position, orientation); -SCameraFollowConfig follow = {}; +core::SCameraFollowConfig follow = {}; follow.enabled = true; -follow.mode = ECameraFollowMode::KeepLocalOffset; -follow.localOffset = float64_t3(-4.0, 0.0, 1.0); +follow.mode = core::ECameraFollowMode::KeepLocalOffset; +follow.localOffset = hlsl::float64_t3(-4.0, 0.0, 1.0); -CCameraGoalSolver solver; -auto result = CCameraFollowUtilities::applyFollowToCamera(solver, camera.get(), trackedTarget, follow); +auto result = core::CCameraFollowUtilities::applyFollowToCamera(solver, camera.get(), trackedTarget, follow); ``` -### Sequence / CI integration +### Expand a compact sequence into runtime payloads ```cpp -CCameraSequenceScript script = ...; -CCameraSequenceCompiledSegment segment = ...; +system::CCameraScriptedTimeline timeline; -CCameraScriptedTimeline timeline; -CCameraSequenceScriptedBuilderUtilities::appendCompiledSequenceSegmentToScriptedTimeline( +system::CCameraSequenceScriptedBuilderUtilities::appendCompiledSequenceSegmentToScriptedTimeline( timeline, baseFrame, - segment, + compiledSegment, buildInfo); -CCameraScriptedRuntimeUtilities::finalizeScriptedTimeline(timeline); - -CCameraScriptedCheckRuntimeState state = {}; -auto frameResult = CCameraScriptedCheckRunnerUtilities::evaluateScriptedChecksForFrame( - timeline.checks, - state, - context); +system::CCameraScriptedRuntimeUtilities::finalizeScriptedTimeline(timeline); ``` -## Why this split matters - -The design deliberately keeps these concerns separate: - -- input binding -- camera semantics -- absolute/tooling state -- follow policy -- scripted playback and validation - -It also keeps the math side explicit: - -- camera-space vectors, matrices, and quaternions come from `nbl::hlsl` -- runtime camera semantics stay in `nbl::core` -- input-device mappings stay in `nbl::ui` -- scripting, persistence, and validation helpers stay in `nbl::system` +## Summary -That separation is what keeps the stack reusable. +The current stack is organized as: -If any one of those concerns leaks into the others: - -- cameras become setter-heavy -- projections become input processors -- consumers own private copies of state math -- scripts become runtime-specific -- follow becomes scene-object-specific +```text +input layer + -> semantic virtual events + -> runtime camera models + -> typed goal / preset / track tooling + -> compact sequence authoring + -> expanded scripted runtime + -> shared validation +``` -The current refactor was mostly about removing exactly those leaks. +That split is intentional. +It is what keeps the runtime path small, the tooling path expressive, and the `61_UI` integration reusable instead of example-local. diff --git a/common/include/camera/SCameraRigPose.hpp b/common/include/camera/SCameraRigPose.hpp index 31eeb0282..a8d5e67bd 100644 --- a/common/include/camera/SCameraRigPose.hpp +++ b/common/include/camera/SCameraRigPose.hpp @@ -9,9 +9,17 @@ namespace nbl::core { +/// @brief Canonical camera pose consisting of world-space position and orientation. +/// +/// This is the smallest pose transport shared by the typed tooling layer. It is +/// intentionally independent from any concrete camera model so goals, follow, +/// path evaluation, playback, and scripted validation can exchange poses without +/// reaching into camera-specific runtime state. struct SCameraRigPose { + /// @brief Camera origin in world space. hlsl::float64_t3 position = hlsl::float64_t3(0.0); + /// @brief Camera orientation in world space expressed as a unit quaternion. hlsl::camera_quaternion_t orientation = hlsl::CCameraMathUtilities::makeIdentityQuaternion(); }; diff --git a/common/src/nbl/examples/camera/CCameraPersistence.cpp b/common/src/nbl/examples/camera/CCameraPersistence.cpp index a86ebdc7c..c924136ae 100644 --- a/common/src/nbl/examples/camera/CCameraPersistence.cpp +++ b/common/src/nbl/examples/camera/CCameraPersistence.cpp @@ -10,8 +10,6 @@ #include "CCameraJsonPersistenceUtilities.hpp" #include "nlohmann/json.hpp" -namespace -{ using json_t = nlohmann::json; json_t serializeGoalJson(const nbl::core::CCameraGoal& goal) @@ -123,7 +121,6 @@ bool deserializePresetCollectionJson(const json_t& root, std::vector Date: Thu, 9 Apr 2026 18:16:30 +0200 Subject: [PATCH 196/205] Refine camera API traits and documentation --- 61_UI/AppHeadlessCameraSmokeChecks.inl | 4 +- 61_UI/README.md | 31 ++++- .../AppProjectionControlPanelUiUtilities.hpp | 7 +- 61_UI/include/app/AppTypes.hpp | 4 +- common/include/camera/CArcballCamera.hpp | 8 +- .../CCameraFollowRegressionUtilities.hpp | 12 +- .../include/camera/CCameraFollowUtilities.hpp | 25 ++-- common/include/camera/CCameraGoal.hpp | 6 +- common/include/camera/CCameraGoalSolver.hpp | 42 ++++--- .../camera/CCameraManipulationUtilities.hpp | 6 +- common/include/camera/CCameraPathMetadata.hpp | 7 +- .../include/camera/CCameraPathUtilities.hpp | 61 ++++----- .../camera/CCameraPlaybackTimeline.hpp | 4 +- .../camera/CCameraScriptedCheckRunner.hpp | 10 +- .../include/camera/CCameraScriptedRuntime.hpp | 9 +- .../include/camera/CCameraSequenceScript.hpp | 27 ++-- .../camera/CCameraSequenceScriptedBuilder.hpp | 11 +- .../CCameraSmokeRegressionUtilities.hpp | 16 +-- .../camera/CCameraTargetRelativeUtilities.hpp | 10 +- common/include/camera/CCameraTraits.hpp | 52 ++++++++ .../camera/CCameraVirtualEventUtilities.hpp | 8 +- common/include/camera/CChaseCamera.hpp | 8 +- common/include/camera/CDollyCamera.hpp | 8 +- common/include/camera/CFPSCamera.hpp | 6 +- common/include/camera/COrbitCamera.hpp | 6 +- common/include/camera/CPlanarProjection.hpp | 4 +- .../include/camera/CSphericalTargetCamera.hpp | 15 +-- common/include/camera/CVirtualGimbalEvent.hpp | 17 ++- common/include/camera/ICamera.hpp | 115 +++++++++-------- common/include/camera/IGimbal.hpp | 7 +- common/include/camera/IPlanarProjection.hpp | 4 +- common/include/camera/README.md | 119 +++++++++++++----- common/include/camera/SCameraRigPose.hpp | 6 +- 33 files changed, 419 insertions(+), 256 deletions(-) create mode 100644 common/include/camera/CCameraTraits.hpp diff --git a/61_UI/AppHeadlessCameraSmokeChecks.inl b/61_UI/AppHeadlessCameraSmokeChecks.inl index 7e2c4f111..a9f2a55c1 100644 --- a/61_UI/AppHeadlessCameraSmokeChecks.inl +++ b/61_UI/AppHeadlessCameraSmokeChecks.inl @@ -189,8 +189,8 @@ nbl::core::SCameraTargetRelativePose pose = {}; if (!nbl::core::CCameraTargetRelativeUtilities::tryBuildTargetRelativePoseFromState( desiredState, - ICamera::SphericalMinDistance, - ICamera::SphericalMaxDistance, + nbl::core::SCameraTargetRelativeTraits::MinDistance, + nbl::core::SCameraTargetRelativeTraits::DefaultMaxDistance, pose) || !nbl::core::CCameraGoalUtilities::applyCanonicalTargetRelativeGoal(outExpectedGoal, desiredState)) { diff --git a/61_UI/README.md b/61_UI/README.md index c148dfd33..b8aefd3fa 100644 --- a/61_UI/README.md +++ b/61_UI/README.md @@ -11,11 +11,12 @@ This README focuses on what `61_UI` adds on top of the shared layer. - 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 is intentionally not the source of truth for camera semantics. -Its job is to consume the shared camera API and expose it through one concrete, testable app. +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 @@ -24,6 +25,7 @@ The shared camera layer stops at reusable camera-domain APIs. - 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 @@ -63,7 +65,7 @@ These are exposed through the active planar / viewport configuration in the UI. `61_UI` exposes one tracked target in the default scene. -Important rule: +Tracked-target rule: - the reusable tracked subject is `core::CTrackedTarget` - it owns its own gimbal @@ -82,6 +84,8 @@ and not around a scene-node or mesh id. The default scene uses: +- `Free` + with `LookAtTarget` - `Orbit`, `Arcball`, `Turntable`, `TopDown`, `Isometric`, `DollyZoom`, `Path Rig` with `OrbitTarget` - `Chase`, `Dolly` @@ -101,6 +105,7 @@ Manual runtime and scripted continuity both drive the same shared follow layer. Purpose: - validate basic camera selection and movement +- validate `referenceFrame` application for every runtime camera kind - validate shared helpers in a short, cheap run ### Continuity @@ -109,6 +114,7 @@ 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. @@ -116,7 +122,7 @@ It is no longer a giant committed frame dump. ## Shared pieces consumed directly by `61_UI` -`61_UI` consumes the shared stack directly rather than carrying private copies of camera logic: +`61_UI` consumes the shared stack directly: - [`CCameraInputBindingUtilities.hpp`](../common/include/camera/CCameraInputBindingUtilities.hpp) - [`CCameraPresetFlow.hpp`](../common/include/camera/CCameraPresetFlow.hpp) @@ -128,7 +134,20 @@ It is no longer a giant committed frame dump. - [`CCameraSequenceScriptedBuilder.hpp`](../common/include/camera/CCameraSequenceScriptedBuilder.hpp) - [`CCameraScriptedCheckRunner.hpp`](../common/include/camera/CCameraScriptedCheckRunner.hpp) -That means `61_UI` does not own a private scripting model, private follow math, or private camera restore logic. +`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 @@ -143,7 +162,7 @@ cmake --preset user-configure-dynamic-msvc Build: ```powershell -cmake --build build/dynamic/examples_tests/61_UI --config Debug --target 61_ui -- /m:1 +cmake --build build/dynamic/examples_tests/61_UI --config Debug --target 61_ui -- /m ``` Run tests: diff --git a/61_UI/include/app/AppProjectionControlPanelUiUtilities.hpp b/61_UI/include/app/AppProjectionControlPanelUiUtilities.hpp index 5bc4fa3b8..5cea675ee 100644 --- a/61_UI/include/app/AppProjectionControlPanelUiUtilities.hpp +++ b/61_UI/include/app/AppProjectionControlPanelUiUtilities.hpp @@ -1,6 +1,7 @@ #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" @@ -303,11 +304,15 @@ inline void drawBoundCameraSection( 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, - sphericalState.maxDistance, + uiMaxDistance, "%.4f", ImGuiSliderFlags_Logarithmic); CCameraControlPanelUiUtilities::drawHoverHint("Current orbit distance"); diff --git a/61_UI/include/app/AppTypes.hpp b/61_UI/include/app/AppTypes.hpp index 39d019a1e..b9f185f98 100644 --- a/61_UI/include/app/AppTypes.hpp +++ b/61_UI/include/app/AppTypes.hpp @@ -178,8 +178,8 @@ struct SCameraAppInputDefaults final struct SCameraAppCameraFactoryDefaults final { - static inline constexpr double DefaultMoveScale = nbl::core::ICamera::DefaultMoveSpeedScale; - static inline constexpr double DefaultRotateScale = nbl::core::ICamera::DefaultRotationSpeedScale; + static inline constexpr double DefaultMoveScale = nbl::core::SCameraRuntimeTraits::DefaultMoveSpeedScale; + static inline constexpr double DefaultRotateScale = nbl::core::SCameraRuntimeTraits::DefaultRotationSpeedScale; static inline constexpr double TargetRigMoveScale = 0.5; }; diff --git a/common/include/camera/CArcballCamera.hpp b/common/include/camera/CArcballCamera.hpp index 2e1def047..d88d3743f 100644 --- a/common/include/camera/CArcballCamera.hpp +++ b/common/include/camera/CArcballCamera.hpp @@ -13,11 +13,11 @@ namespace nbl::core { -/// @brief Target-relative camera that supports planar target translation plus bounded arcball orbiting. +/// @brief Target-relative camera with planar target translation and bounded arcball orbiting. /// -/// The camera keeps a target position, orbit angles, and distance inherited from -/// `CSphericalTargetCamera`. Translation moves the target in the view plane while -/// rotation changes the orbit around that target with a symmetric pitch limit. +/// The runtime state is inherited from `CSphericalTargetCamera`. Translation +/// moves the target in the current view plane. Rotation updates orbit yaw and +/// pitch under a symmetric pitch limit. class CArcballCamera final : public CSphericalTargetCamera { public: diff --git a/common/include/camera/CCameraFollowRegressionUtilities.hpp b/common/include/camera/CCameraFollowRegressionUtilities.hpp index 210348ebf..f238af62a 100644 --- a/common/include/camera/CCameraFollowRegressionUtilities.hpp +++ b/common/include/camera/CCameraFollowRegressionUtilities.hpp @@ -63,12 +63,12 @@ struct SCameraFollowRegressionThresholds { static inline constexpr float DefaultClipWEpsilon = 1e-5f; static inline constexpr float DefaultProjectedNdcTolerance = 0.03f; - static inline constexpr float DefaultLockAngleToleranceDeg = static_cast(core::ICamera::DefaultAngularToleranceDeg); - static inline constexpr double DefaultDistanceTolerance = core::ICamera::ScalarTolerance; - static inline constexpr double DefaultTargetTolerance = core::ICamera::TinyScalarEpsilon; - static inline constexpr double DefaultPositionTolerance = core::ICamera::DefaultPositionTolerance; - static inline constexpr double DefaultRotationToleranceDeg = core::ICamera::DefaultAngularToleranceDeg; - static inline constexpr double DefaultScalarTolerance = core::ICamera::ScalarTolerance; + static inline constexpr float DefaultLockAngleToleranceDeg = static_cast(core::SCameraToolingThresholds::DefaultAngularToleranceDeg); + static inline constexpr double DefaultDistanceTolerance = core::SCameraToolingThresholds::ScalarTolerance; + static inline constexpr double DefaultTargetTolerance = core::SCameraToolingThresholds::TinyScalarEpsilon; + static inline constexpr double DefaultPositionTolerance = core::SCameraToolingThresholds::DefaultPositionTolerance; + static inline constexpr double DefaultRotationToleranceDeg = core::SCameraToolingThresholds::DefaultAngularToleranceDeg; + static inline constexpr double DefaultScalarTolerance = core::SCameraToolingThresholds::ScalarTolerance; float clipWEpsilon = DefaultClipWEpsilon; float projectedNdcTolerance = DefaultProjectedNdcTolerance; diff --git a/common/include/camera/CCameraFollowUtilities.hpp b/common/include/camera/CCameraFollowUtilities.hpp index 0501673c3..3f177f5d1 100644 --- a/common/include/camera/CCameraFollowUtilities.hpp +++ b/common/include/camera/CCameraFollowUtilities.hpp @@ -15,10 +15,10 @@ namespace nbl::core { -/// @brief Reusable tracked-target and follow helpers layered on top of the shared camera API. +/// @brief Reusable tracked-target and follow helpers. /// -/// The tracked subject owns its own gimbal. Follow stays outside `ICamera` and maps -/// a camera plus tracked target into a `CCameraGoal`. +/// The tracked subject owns its own gimbal. Follow code reads that pose and +/// maps one camera plus one tracked target into a `CCameraGoal`. class CTrackedTarget { public: @@ -83,14 +83,15 @@ class CTrackedTarget /// @brief Follow policy layered on top of a tracked target gimbal. /// -/// The modes are intentionally explicit because `follow` is not one behavior: +/// Each mode defines how tracked-target motion updates the camera: /// -/// - `OrbitTarget` keeps a target-relative orbit/path rig and re-centers the tracked target -/// - `LookAtTarget` keeps the camera world position and only rotates the view toward the target -/// - `KeepWorldOffset` keeps a world-space camera offset from the target and locks the view onto it -/// - `KeepLocalOffset` keeps a target-local camera offset and locks the view onto it +/// - `OrbitTarget` rewrites target-relative camera state so the tracked target becomes the camera target +/// - `LookAtTarget` preserves camera position and rebuilds orientation toward the tracked target +/// - `KeepWorldOffset` places the camera at `trackedTarget.position + worldOffset` and looks at the target +/// - `KeepLocalOffset` transforms `localOffset` by the tracked-target local frame and looks at the target /// -/// The tracked target remains the source of truth. The camera does not own the tracked subject. +/// The tracked target provides pose data. The camera reads that data and does +/// not own the tracked subject. enum class ECameraFollowMode : uint8_t { Disabled, @@ -121,7 +122,7 @@ struct SCameraFollowConfig /// a `CCameraGoal` that can then be applied through the shared goal solver. struct CCameraFollowUtilities final { - /// @brief Return whether the follow mode keeps the camera view locked onto the target. + /// @brief Return whether the follow mode rebuilds camera orientation toward the tracked target. static inline constexpr bool cameraFollowModeLocksViewToTarget(const ECameraFollowMode mode) { switch (mode) @@ -265,12 +266,12 @@ struct CCameraFollowUtilities final { const auto toTarget = trackedTarget.getGimbal().getPosition() - cameraGimbal.getPosition(); const auto targetDistance = hlsl::length(toTarget); - if (!hlsl::CCameraMathUtilities::isFiniteScalar(targetDistance) || targetDistance <= ICamera::TinyScalarEpsilon) + if (!hlsl::CCameraMathUtilities::isFiniteScalar(targetDistance) || targetDistance <= SCameraToolingThresholds::TinyScalarEpsilon) return false; const auto forward = cameraGimbal.getZAxis(); const auto forwardLength = hlsl::length(forward); - if (!hlsl::CCameraMathUtilities::isFiniteVec3(forward) || !hlsl::CCameraMathUtilities::isFiniteScalar(forwardLength) || forwardLength <= ICamera::TinyScalarEpsilon) + if (!hlsl::CCameraMathUtilities::isFiniteVec3(forward) || !hlsl::CCameraMathUtilities::isFiniteScalar(forwardLength) || forwardLength <= SCameraToolingThresholds::TinyScalarEpsilon) return false; const auto forwardDirection = forward / forwardLength; diff --git a/common/include/camera/CCameraGoal.hpp b/common/include/camera/CCameraGoal.hpp index 98487defd..bbf1eae3f 100644 --- a/common/include/camera/CCameraGoal.hpp +++ b/common/include/camera/CCameraGoal.hpp @@ -89,7 +89,7 @@ struct CCameraGoalUtilities final static inline bool applyCanonicalTargetRelativeGoal(CCameraGoal& goal, const SCameraTargetRelativeState& state) { SCameraTargetRelativePose pose = {}; - if (!CCameraTargetRelativeUtilities::tryBuildTargetRelativePoseFromState(state, ICamera::SphericalMinDistance, ICamera::SphericalMaxDistance, pose)) + if (!CCameraTargetRelativeUtilities::tryBuildTargetRelativePoseFromState(state, SCameraTargetRelativeTraits::MinDistance, SCameraTargetRelativeTraits::DefaultMaxDistance, pose)) return false; applyCanonicalTargetRelativeGoalFields(goal, state, pose); @@ -149,8 +149,8 @@ struct CCameraGoalUtilities final if (!CCameraTargetRelativeUtilities::tryBuildTargetRelativeStateFromPosition( targetPosition, position, - ICamera::SphericalMinDistance, - ICamera::SphericalMaxDistance, + SCameraTargetRelativeTraits::MinDistance, + SCameraTargetRelativeTraits::DefaultMaxDistance, state)) { return false; diff --git a/common/include/camera/CCameraGoalSolver.hpp b/common/include/camera/CCameraGoalSolver.hpp index b9a54aa9d..f8d65a0db 100644 --- a/common/include/camera/CCameraGoalSolver.hpp +++ b/common/include/camera/CCameraGoalSolver.hpp @@ -14,14 +14,16 @@ namespace nbl::core { -/// @brief Best-effort absolute layer built on top of the event-only camera core. +/// @brief Goal capture, compatibility analysis, and goal application helper. /// -/// It captures typed camera state into `CCameraGoal`, analyzes compatibility, -/// and tries to apply goals back to cameras using typed hooks and virtual-event replay. +/// The solver captures canonical state into `CCameraGoal`, compares a goal +/// against one target camera, applies typed fragments directly when the camera +/// exposes them, and builds virtual-event replay when a typed fragment must be +/// approximated through `manipulate(...)`. class CCameraGoalSolver { public: - /// @brief Detailed capture result for tooling code. + /// @brief Detailed result returned by one goal-capture attempt. struct SCaptureResult { bool hasCamera = false; @@ -45,7 +47,7 @@ class CCameraGoalSolver uint32_t missingGoalStateMask = ICamera::GoalStateNone; }; - /// @brief Outcome of a best-effort goal apply attempt. + /// @brief Outcome of one goal-application attempt. struct SApplyResult { enum class EStatus : uint8_t @@ -271,7 +273,7 @@ class CCameraGoalSolver else { absoluteChanged = absoluteChanged || afterState.distance != beforeDistance; - exact = exact && hlsl::abs(static_cast(afterState.distance - desiredDistance)) <= ICamera::ScalarTolerance; + exact = exact && hlsl::abs(static_cast(afterState.distance - desiredDistance)) <= SCameraToolingThresholds::ScalarTolerance; } } } @@ -333,10 +335,10 @@ class CCameraGoalSolver } else { - const bool dynamicChanged = !hlsl::CCameraMathUtilities::nearlyEqualScalar(beforeState.baseFov, afterState.baseFov, static_cast(ICamera::ScalarTolerance)) || - !hlsl::CCameraMathUtilities::nearlyEqualScalar(beforeState.referenceDistance, afterState.referenceDistance, static_cast(ICamera::ScalarTolerance)); - const bool dynamicExact = hlsl::CCameraMathUtilities::nearlyEqualScalar(afterState.baseFov, canonicalTarget.dynamicPerspectiveState.baseFov, static_cast(ICamera::ScalarTolerance)) && - hlsl::CCameraMathUtilities::nearlyEqualScalar(afterState.referenceDistance, canonicalTarget.dynamicPerspectiveState.referenceDistance, static_cast(ICamera::ScalarTolerance)); + const bool dynamicChanged = !hlsl::CCameraMathUtilities::nearlyEqualScalar(beforeState.baseFov, afterState.baseFov, static_cast(SCameraToolingThresholds::ScalarTolerance)) || + !hlsl::CCameraMathUtilities::nearlyEqualScalar(beforeState.referenceDistance, afterState.referenceDistance, static_cast(SCameraToolingThresholds::ScalarTolerance)); + const bool dynamicExact = hlsl::CCameraMathUtilities::nearlyEqualScalar(afterState.baseFov, canonicalTarget.dynamicPerspectiveState.baseFov, static_cast(SCameraToolingThresholds::ScalarTolerance)) && + hlsl::CCameraMathUtilities::nearlyEqualScalar(afterState.referenceDistance, canonicalTarget.dynamicPerspectiveState.referenceDistance, static_cast(SCameraToolingThresholds::ScalarTolerance)); absoluteChanged = absoluteChanged || dynamicChanged; exact = exact && dynamicExact; @@ -389,8 +391,8 @@ class CCameraGoalSolver { static constexpr double UnitScale = 1.0; static inline const hlsl::float64_t3 UnitAxisDenominator = hlsl::float64_t3(UnitScale); - static inline const hlsl::float64_t3 ScalarToleranceVec = hlsl::float64_t3(ICamera::ScalarTolerance); - static inline const hlsl::float64_t3 AngularToleranceDegVec = hlsl::float64_t3(ICamera::DefaultAngularToleranceDeg); + static inline const hlsl::float64_t3 ScalarToleranceVec = hlsl::float64_t3(SCameraToolingThresholds::ScalarTolerance); + static inline const hlsl::float64_t3 AngularToleranceDegVec = hlsl::float64_t3(SCameraToolingThresholds::DefaultAngularToleranceDeg); }; inline void appendYawPitchRollEvents( @@ -434,7 +436,7 @@ class CCameraGoalSolver inline double getMoveMagnitudeDenominator(const ICamera* camera) const { const double moveScale = camera->getMoveSpeedScale(); - return ICamera::VirtualTranslationStep * (moveScale == 0.0 ? SGoalSolverDefaults::UnitScale : moveScale); + return SCameraRuntimeTraits::VirtualTranslationStep * (moveScale == 0.0 ? SGoalSolverDefaults::UnitScale : moveScale); } inline double getRotationMagnitudeDenominator(const ICamera* camera) const @@ -481,7 +483,7 @@ class CCameraGoalSolver if (!computePoseMismatch(camera, target, beforePosDelta, beforeRotDeltaDeg)) return false; - if (beforePosDelta <= ICamera::DefaultPositionTolerance && beforeRotDeltaDeg <= ICamera::DefaultAngularToleranceDeg) + if (beforePosDelta <= SCameraToolingThresholds::DefaultPositionTolerance && beforeRotDeltaDeg <= SCameraToolingThresholds::DefaultAngularToleranceDeg) { outExact = true; return true; @@ -496,9 +498,9 @@ class CCameraGoalSolver if (!computePoseMismatch(camera, target, afterPosDelta, afterRotDeltaDeg)) return false; - outChanged = !hlsl::CCameraMathUtilities::isNearlyZeroScalar(afterPosDelta - beforePosDelta, static_cast(ICamera::TinyScalarEpsilon)) || - !hlsl::CCameraMathUtilities::isNearlyZeroScalar(afterRotDeltaDeg - beforeRotDeltaDeg, static_cast(ICamera::TinyScalarEpsilon)); - outExact = afterPosDelta <= ICamera::DefaultPositionTolerance && afterRotDeltaDeg <= ICamera::DefaultAngularToleranceDeg; + outChanged = !hlsl::CCameraMathUtilities::isNearlyZeroScalar(afterPosDelta - beforePosDelta, static_cast(SCameraToolingThresholds::TinyScalarEpsilon)) || + !hlsl::CCameraMathUtilities::isNearlyZeroScalar(afterRotDeltaDeg - beforeRotDeltaDeg, static_cast(SCameraToolingThresholds::TinyScalarEpsilon)); + outExact = afterPosDelta <= SCameraToolingThresholds::DefaultPositionTolerance && afterRotDeltaDeg <= SCameraToolingThresholds::DefaultAngularToleranceDeg; return true; } @@ -514,9 +516,9 @@ class CCameraGoalSolver out, delta, policy.translateOrbit ? getMoveMagnitudeDenominator(camera) : getRotationMagnitudeDenominator(camera), - ICamera::DefaultAngularToleranceDeg, - ICamera::VirtualTranslationStep, - ICamera::ScalarTolerance, + SCameraToolingThresholds::DefaultAngularToleranceDeg, + SCameraRuntimeTraits::VirtualTranslationStep, + SCameraToolingThresholds::ScalarTolerance, policy); return !out.empty(); } diff --git a/common/include/camera/CCameraManipulationUtilities.hpp b/common/include/camera/CCameraManipulationUtilities.hpp index 43b53a226..9ca865a0d 100644 --- a/common/include/camera/CCameraManipulationUtilities.hpp +++ b/common/include/camera/CCameraManipulationUtilities.hpp @@ -22,8 +22,8 @@ struct SCameraConstraintDefaults final static constexpr float YawMaxDeg = 180.0f; static constexpr float RollMinDeg = -180.0f; static constexpr float RollMaxDeg = 180.0f; - static constexpr float MinDistance = ICamera::SphericalMinDistance; - static constexpr float MaxDistance = ICamera::SphericalMaxDistance; + static constexpr float MinDistance = SCameraTargetRelativeTraits::MinDistance; + static constexpr float MaxDistance = SCameraTargetRelativeTraits::DefaultMaxDistance; }; /// @brief Reusable constraint settings for post-manipulation camera clamping. @@ -90,7 +90,7 @@ struct CCameraManipulationUtilities final } const auto worldDelta = CCameraVirtualEventUtilities::collectSignedTranslationDelta({ events.data(), count }); - if (hlsl::CCameraMathUtilities::isNearlyZeroVector(worldDelta, static_cast(ICamera::TinyScalarEpsilon))) + if (hlsl::CCameraMathUtilities::isNearlyZeroVector(worldDelta, static_cast(SCameraToolingThresholds::TinyScalarEpsilon))) { events = std::move(filtered); count = static_cast(events.size()); diff --git a/common/include/camera/CCameraPathMetadata.hpp b/common/include/camera/CCameraPathMetadata.hpp index 3b1ce2b60..0ccf2ac6e 100644 --- a/common/include/camera/CCameraPathMetadata.hpp +++ b/common/include/camera/CCameraPathMetadata.hpp @@ -9,11 +9,10 @@ namespace nbl::core { -/// @brief Stable descriptive strings shared by the reusable `Path Rig` camera kind. +/// @brief Stable descriptive strings used by the reusable `Path Rig` camera kind. /// -/// These labels are intentionally kept outside the heavy path utility header so -/// simple metadata consumers can describe the camera kind without pulling in the -/// full path-model implementation. +/// This metadata lives in a lightweight header so code that only needs labels +/// or identifiers does not have to include the full path-model implementation. struct SCameraPathRigMetadata final { /// @brief User-facing camera kind label. diff --git a/common/include/camera/CCameraPathUtilities.hpp b/common/include/camera/CCameraPathUtilities.hpp index b1676bed1..736b944f5 100644 --- a/common/include/camera/CCameraPathUtilities.hpp +++ b/common/include/camera/CCameraPathUtilities.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -80,9 +81,9 @@ struct SCameraCanonicalPathState final /// @brief Comparison tolerances used when matching two path states. struct SCameraPathComparisonThresholds final { - double sToleranceDeg = ICamera::DefaultAngularToleranceDeg; - double rollToleranceDeg = ICamera::DefaultAngularToleranceDeg; - double scalarTolerance = ICamera::ScalarTolerance; + double sToleranceDeg = SCameraToolingThresholds::DefaultAngularToleranceDeg; + double rollToleranceDeg = SCameraToolingThresholds::DefaultAngularToleranceDeg; + double scalarTolerance = SCameraToolingThresholds::ScalarTolerance; }; /// @brief Result of updating the path distance while preserving the rest of the path state. @@ -92,14 +93,14 @@ struct SCameraPathDistanceUpdateResult final hlsl::float64_t appliedDistance = 0.0; }; -/// @brief Shared default constants used by the built-in `Path Rig` model. +/// @brief Default constants used by the built-in `Path Rig` model. struct SCameraPathDefaults final { - static constexpr double MinU = static_cast(ICamera::SphericalMinDistance); - static constexpr double ScalarTolerance = ICamera::ScalarTolerance; - static constexpr double ExactStateTolerance = ICamera::TinyScalarEpsilon; + static constexpr double MinU = static_cast(SCameraTargetRelativeTraits::MinDistance); + static constexpr double ScalarTolerance = SCameraToolingThresholds::ScalarTolerance; + static constexpr double ExactStateTolerance = SCameraToolingThresholds::TinyScalarEpsilon; static constexpr double ExactAngleToleranceDeg = ExactStateTolerance * 180.0 / hlsl::numbers::pi; - static constexpr double AngleToleranceDeg = ICamera::DefaultAngularToleranceDeg; + static constexpr double AngleToleranceDeg = SCameraToolingThresholds::DefaultAngularToleranceDeg; static inline constexpr std::string_view Identifier = SCameraPathRigMetadata::Identifier; static inline constexpr std::string_view Description = SCameraPathRigMetadata::DefaultModelDescription; static inline constexpr ICamera::PathStateLimits Limits = {}; @@ -128,11 +129,14 @@ struct SCameraPathControlContext final SCameraPathLimits limits = SCameraPathDefaults::Limits; }; -/// @brief Callback bundle defining how a concrete `Path Rig` model behaves. +/// @brief Callback bundle defining path-state resolution, input response, evaluation, and distance updates. /// -/// The model is intentionally split into state resolution, control law, -/// integration, canonical evaluation, and distance updates so the runtime camera -/// can stay event-driven while tooling still works with a typed path state. +/// A concrete `Path Rig` model provides: +/// - state resolution from target position, world position, and optional typed input +/// - one control law turning accumulated runtime motion into `SCameraPathDelta` +/// - one state integrator +/// - one canonical evaluator producing pose and target-relative view data +/// - one distance-update rule for typed helpers that adjust distance directly struct SCameraPathModel final { using resolve_state_t = std::function(limits.maxDistance)); } - /// @brief Clamp and normalize path-state limits into the valid shared domain. + /// @brief Clamp and normalize path-state limits into a valid numeric domain. static inline bool sanitizePathLimits(SCameraPathLimits& limits) { - if (!isPathLimitsFinite(limits)) + if (!isPathLimitsWellFormed(limits)) return false; - limits.minU = std::clamp( - limits.minU, - 0.0, - static_cast(ICamera::SphericalMaxDistance)); - limits.minDistance = std::clamp( + limits.minU = std::max(limits.minU, 0.0); + limits.minDistance = std::max( std::max(limits.minDistance, static_cast(limits.minU)), - static_cast(ICamera::SphericalMinDistance), - static_cast(ICamera::SphericalMaxDistance)); - limits.maxDistance = std::clamp( - limits.maxDistance, - limits.minDistance, - static_cast(ICamera::SphericalMaxDistance)); + static_cast(SCameraTargetRelativeTraits::MinDistance)); + + if (!std::isfinite(static_cast(limits.maxDistance))) + limits.maxDistance = std::numeric_limits::infinity(); + else + limits.maxDistance = std::max(limits.maxDistance, limits.minDistance); return true; } diff --git a/common/include/camera/CCameraPlaybackTimeline.hpp b/common/include/camera/CCameraPlaybackTimeline.hpp index 3dec5dc6f..64dfc811e 100644 --- a/common/include/camera/CCameraPlaybackTimeline.hpp +++ b/common/include/camera/CCameraPlaybackTimeline.hpp @@ -11,7 +11,7 @@ namespace nbl::core { /// @brief Shared playback cursor state for camera keyframe tracks. -/// The cursor is intentionally transport-only so consumers can own higher-level playback policy. +/// The cursor stores playback state only: playing flag, looping mode, speed, and current time. struct CCameraPlaybackCursor { bool playing = false; @@ -21,7 +21,7 @@ struct CCameraPlaybackCursor }; /// @brief Outcome of advancing a playback cursor against a keyframe track. -/// This separates raw time stepping from higher-level consumer policy and UI feedback. +/// This result reports how time changed during one advance step. struct SCameraPlaybackAdvanceResult { bool hasTrack = false; diff --git a/common/include/camera/CCameraScriptedCheckRunner.hpp b/common/include/camera/CCameraScriptedCheckRunner.hpp index 0fe68760b..a4a23c7ed 100644 --- a/common/include/camera/CCameraScriptedCheckRunner.hpp +++ b/common/include/camera/CCameraScriptedCheckRunner.hpp @@ -22,12 +22,10 @@ namespace nbl::system /// @brief Runtime state for authored scripted checks. /// -/// The state is intentionally small: -/// -/// - authored check data stays in `CCameraScriptedInputCheck` -/// - the runner only remembers where the next check starts -/// - baseline and step references are maintained here so consumers do not have to -/// keep duplicating the same bookkeeping +/// This state stores: +/// - the index of the next authored check to evaluate +/// - one baseline pose reference +/// - one step pose reference struct CCameraScriptedCheckRuntimeState { struct SPoseReference final : core::SCameraRigPose diff --git a/common/include/camera/CCameraScriptedRuntime.hpp b/common/include/camera/CCameraScriptedRuntime.hpp index d15b53d43..99d8c34f0 100644 --- a/common/include/camera/CCameraScriptedRuntime.hpp +++ b/common/include/camera/CCameraScriptedRuntime.hpp @@ -20,9 +20,8 @@ namespace nbl::system /// @brief Shared scripted runtime payload used by camera-sequence consumers. /// -/// The compact authored sequence remains camera-domain. A concrete runtime may still expand it -/// into low-level per-frame events and checks, but those expanded payloads live in this shared -/// header rather than inside one consumer. +/// This type stores the expanded per-frame events and checks produced from a +/// compact authored camera sequence. struct CCameraScriptedInputEvent { enum class Type : uint8_t @@ -124,8 +123,8 @@ struct CCameraScriptedInputEvent struct CCameraScriptedCheckDefaults final { static constexpr float VirtualEventTolerance = 1e-3f; - static constexpr float PositionTolerance = static_cast(core::ICamera::DefaultPositionTolerance); - static constexpr float EulerToleranceDeg = static_cast(core::ICamera::DefaultAngularToleranceDeg); + static constexpr float PositionTolerance = static_cast(core::SCameraToolingThresholds::DefaultPositionTolerance); + static constexpr float EulerToleranceDeg = static_cast(core::SCameraToolingThresholds::DefaultAngularToleranceDeg); static constexpr float FollowScreenToleranceNdc = SCameraFollowRegressionThresholds::DefaultProjectedNdcTolerance; }; diff --git a/common/include/camera/CCameraSequenceScript.hpp b/common/include/camera/CCameraSequenceScript.hpp index b2e47a348..e4e459d69 100644 --- a/common/include/camera/CCameraSequenceScript.hpp +++ b/common/include/camera/CCameraSequenceScript.hpp @@ -21,24 +21,25 @@ namespace nbl::core { -/// @brief Compact authored camera-sequence format shared by playback, scripting, and validation tooling. +/// @brief Compact authored camera-sequence format shared by playback, scripting, and validation helpers. /// /// The authored file describes: /// /// - which camera kind a segment targets -/// - which reusable projection presentations should be shown -/// - which keyframed camera goals should be sampled over time -/// - which tracked-target poses should be sampled over time -/// - which continuity thresholds and capture points should be generated +/// - which reusable projection presentations are shown +/// - which keyframed camera goals are sampled over time +/// - which tracked-target poses are sampled over time +/// - which continuity thresholds and capture points are generated /// -/// The format intentionally does not store: +/// The format does not store: /// /// - per-frame low-level event dumps /// - runtime-specific window actions as authored source data /// - ImGuizmo transforms as the primary authored primitive /// -/// A consumer may expand the compact sequence into its own runtime event/check representation, but -/// the authored source of truth stays camera-domain and reusable. +/// Consumers may expand the compact sequence into runtime events and per-frame +/// checks. The authored data remains camera-domain data and is not a device- or +/// UI-specific event dump. /// @brief Authored projection view request for camera-sequence playback. struct CCameraSequencePresentation @@ -258,7 +259,9 @@ struct CCameraSequenceSegment }; /// @brief Top-level reusable camera-sequence script. -/// Consumers are expected to expand this compact description into their own runtime playback/check pipeline. +/// +/// This type stores the compact authored description that is later expanded +/// into runtime playback and check payloads. struct CCameraSequenceScript { bool enabled = true; @@ -360,7 +363,7 @@ struct CCameraSequenceScriptUtilities final std::sort(fractions.begin(), fractions.end()); fractions.erase(std::unique(fractions.begin(), fractions.end(), - [](const float lhs, const float rhs) { return hlsl::CCameraMathUtilities::nearlyEqualScalar(lhs, rhs, static_cast(ICamera::ScalarTolerance)); }), + [](const float lhs, const float rhs) { return hlsl::CCameraMathUtilities::nearlyEqualScalar(lhs, rhs, static_cast(SCameraToolingThresholds::ScalarTolerance)); }), fractions.end()); } @@ -579,7 +582,7 @@ struct CCameraSequenceScriptUtilities final normalized.reserve(outTrack.keyframes.size()); for (const auto& keyframe : outTrack.keyframes) { - if (!normalized.empty() && hlsl::CCameraMathUtilities::nearlyEqualScalar(normalized.back().time, keyframe.time, static_cast(ICamera::ScalarTolerance))) + if (!normalized.empty() && hlsl::CCameraMathUtilities::nearlyEqualScalar(normalized.back().time, keyframe.time, static_cast(SCameraToolingThresholds::ScalarTolerance))) normalized.back() = keyframe; else normalized.emplace_back(keyframe); @@ -614,7 +617,7 @@ struct CCameraSequenceScriptUtilities final if (time > rhs.time) continue; - const auto span = std::max(static_cast(ICamera::ScalarTolerance), rhs.time - lhs.time); + const auto span = std::max(static_cast(SCameraToolingThresholds::ScalarTolerance), rhs.time - lhs.time); const auto alpha = std::clamp((time - lhs.time) / span, 0.f, 1.f); outPose.position = lhs.pose.position + (rhs.pose.position - lhs.pose.position) * static_cast(alpha); outPose.orientation = hlsl::CCameraMathUtilities::slerpQuaternion(lhs.pose.orientation, rhs.pose.orientation, static_cast(alpha)); diff --git a/common/include/camera/CCameraSequenceScriptedBuilder.hpp b/common/include/camera/CCameraSequenceScriptedBuilder.hpp index ae27fa690..f8dd5cf97 100644 --- a/common/include/camera/CCameraSequenceScriptedBuilder.hpp +++ b/common/include/camera/CCameraSequenceScriptedBuilder.hpp @@ -16,18 +16,17 @@ namespace nbl::system /// @brief Build expanded scripted runtime data from a compiled camera-sequence segment. /// -/// This keeps authored sequence semantics in shared camera helpers instead of re-encoding -/// `Goal`, `TrackedTargetTransform`, `Baseline`, `GimbalStep`, and capture scheduling inside -/// one consumer. +/// The builder converts compiled sequence frames into the shared runtime event +/// and check payloads used by camera-sequence consumers. struct CCameraSequenceScriptedSegmentBuildInfo { - /// @brief Planar that should receive the compiled segment. + /// @brief Planar index that receives the compiled segment. uint32_t planarIx = 0u; /// @brief Number of windows the consumer can actually route presentation actions to. size_t availableWindowCount = 1u; - /// @brief Whether secondary window presentation actions should be emitted. + /// @brief Whether secondary-window presentation actions are emitted. bool useWindow = false; - /// @brief Whether per-frame follow-lock checks should be generated for this segment. + /// @brief Whether per-frame follow-lock checks are generated for this segment. bool includeFollowTargetLock = false; }; diff --git a/common/include/camera/CCameraSmokeRegressionUtilities.hpp b/common/include/camera/CCameraSmokeRegressionUtilities.hpp index 387249519..9ca40340b 100644 --- a/common/include/camera/CCameraSmokeRegressionUtilities.hpp +++ b/common/include/camera/CCameraSmokeRegressionUtilities.hpp @@ -19,14 +19,14 @@ using SCameraManipulationDelta = hlsl::SCameraPoseDelta; struct SCameraSmokeComparisonThresholds final { - static constexpr double TinyScalarEpsilon = core::ICamera::TinyScalarEpsilon; - static constexpr double DefaultPositionTolerance = core::ICamera::DefaultPositionTolerance; - static constexpr double DefaultAngularToleranceDeg = core::ICamera::DefaultAngularToleranceDeg; - static constexpr double DefaultScalarTolerance = core::ICamera::ScalarTolerance; - static constexpr double StrictPositionTolerance = core::ICamera::ScalarTolerance; - static constexpr double StrictAngularToleranceDeg = core::ICamera::DefaultAngularToleranceDeg; - static constexpr double StrictScalarTolerance = core::ICamera::ScalarTolerance; - static constexpr double TrackTimeTolerance = core::ICamera::ScalarTolerance; + static constexpr double TinyScalarEpsilon = core::SCameraToolingThresholds::TinyScalarEpsilon; + static constexpr double DefaultPositionTolerance = core::SCameraToolingThresholds::DefaultPositionTolerance; + static constexpr double DefaultAngularToleranceDeg = core::SCameraToolingThresholds::DefaultAngularToleranceDeg; + static constexpr double DefaultScalarTolerance = core::SCameraToolingThresholds::ScalarTolerance; + static constexpr double StrictPositionTolerance = core::SCameraToolingThresholds::ScalarTolerance; + static constexpr double StrictAngularToleranceDeg = core::SCameraToolingThresholds::DefaultAngularToleranceDeg; + static constexpr double StrictScalarTolerance = core::SCameraToolingThresholds::ScalarTolerance; + static constexpr double TrackTimeTolerance = core::SCameraToolingThresholds::ScalarTolerance; }; struct CCameraSmokeRegressionUtilities final diff --git a/common/include/camera/CCameraTargetRelativeUtilities.hpp b/common/include/camera/CCameraTargetRelativeUtilities.hpp index 12bfbb846..7271685f5 100644 --- a/common/include/camera/CCameraTargetRelativeUtilities.hpp +++ b/common/include/camera/CCameraTargetRelativeUtilities.hpp @@ -9,18 +9,18 @@ namespace nbl::core { -/// @brief Canonical target-relative orbit state shared by spherical cameras, follow, and goal solving. +/// @brief Canonical target-relative orbit state used by spherical cameras, follow, and goal solving. struct SCameraTargetRelativeState final { hlsl::float64_t3 target = hlsl::float64_t3(0.0); hlsl::float64_t2 orbitUv = hlsl::float64_t2(0.0); - float distance = ICamera::SphericalMinDistance; + float distance = SCameraTargetRelativeTraits::MinDistance; }; /// @brief Pose reconstructed from a target-relative orbit state. struct SCameraTargetRelativePose final : SCameraRigPose { - hlsl::float64_t appliedDistance = static_cast(ICamera::SphericalMinDistance); + hlsl::float64_t appliedDistance = static_cast(SCameraTargetRelativeTraits::MinDistance); }; /// @brief Derived basis for target-relative orbit rigs. @@ -44,6 +44,7 @@ struct SCameraTargetRelativeDelta final } }; +/// @brief Mapping policy describing how a target-relative delta is converted into virtual events. struct SCameraTargetRelativeEventPolicy final { bool translateOrbit = false; @@ -55,7 +56,7 @@ struct SCameraTargetRelativeEventPolicy final }; }; -/// @brief Shared authored/default constants for target-relative rigs. +/// @brief Default constants and event policies used by target-relative rigs. struct SCameraTargetRelativeRigDefaults final { static constexpr float InitialDistance = 1.0f; @@ -106,6 +107,7 @@ struct SCameraTargetRelativeRigDefaults final }; }; +/// @brief Helpers for converting between target-relative state, pose, basis, and virtual-event deltas. struct CCameraTargetRelativeUtilities final { static inline bool tryBuildTargetRelativeStateFromPosition( diff --git a/common/include/camera/CCameraTraits.hpp b/common/include/camera/CCameraTraits.hpp new file mode 100644 index 000000000..6cab1a4db --- /dev/null +++ b/common/include/camera/CCameraTraits.hpp @@ -0,0 +1,52 @@ +// 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_TRAITS_HPP_ +#define _C_CAMERA_TRAITS_HPP_ + +#include + +namespace nbl::core +{ + +/// @brief Constants used when converting unit virtual events into camera-local motion. +struct SCameraRuntimeTraits final +{ + /// @brief Base world-space translation magnitude represented by a unit virtual move event. + static inline constexpr double VirtualTranslationStep = 0.01; + /// @brief Default per-camera multiplier applied to virtual translation magnitudes. + static inline constexpr double DefaultMoveSpeedScale = VirtualTranslationStep; + /// @brief Default per-camera multiplier applied to virtual rotation magnitudes. + static inline constexpr double DefaultRotationSpeedScale = 0.003; +}; + +/// @brief Geometric constants used by target-relative camera families. +/// +/// `MinDistance` prevents zero-distance target-relative states. +/// `DefaultMaxDistance` is unbounded. Individual cameras and tools may apply +/// their own finite limits on top of it. +struct SCameraTargetRelativeTraits final +{ + /// @brief Smallest valid target-relative distance shared by spherical and path-style rigs. + static inline constexpr float MinDistance = 0.1f; + /// @brief Default upper bound for target-relative distance when no camera-specific cap is requested. + static inline constexpr float DefaultMaxDistance = std::numeric_limits::infinity(); +}; + +/// @brief Comparison thresholds used by helper layers outside the runtime camera interface. +struct SCameraToolingThresholds final +{ + /// @brief Default scalar tolerance used by typed state comparisons. + static inline constexpr double ScalarTolerance = 1e-6; + /// @brief Small epsilon used by replay and comparison helpers that need stricter zero tests. + static inline constexpr double TinyScalarEpsilon = 1e-9; + /// @brief Default world-space position tolerance used by pose comparisons. + static inline constexpr double DefaultPositionTolerance = 2.0 * ScalarTolerance; + /// @brief Default angular tolerance in degrees used by pose and state comparisons. + static inline constexpr double DefaultAngularToleranceDeg = 0.1; +}; + +} // namespace nbl::core + +#endif // _C_CAMERA_TRAITS_HPP_ diff --git a/common/include/camera/CCameraVirtualEventUtilities.hpp b/common/include/camera/CCameraVirtualEventUtilities.hpp index 2e0dfb0c6..31baae246 100644 --- a/common/include/camera/CCameraVirtualEventUtilities.hpp +++ b/common/include/camera/CCameraVirtualEventUtilities.hpp @@ -42,7 +42,7 @@ struct CCameraVirtualEventUtilities final const double value, const CVirtualGimbalEvent::VirtualEventType positive, const CVirtualGimbalEvent::VirtualEventType negative, - const double tolerance = static_cast(ICamera::TinyScalarEpsilon)) + const double tolerance = static_cast(SCameraToolingThresholds::TinyScalarEpsilon)) { if (!hlsl::CCameraMathUtilities::isFiniteScalar(value) || hlsl::CCameraMathUtilities::isNearlyZeroScalar(value, tolerance)) return; @@ -61,7 +61,7 @@ struct CCameraVirtualEventUtilities final const CVirtualGimbalEvent::VirtualEventType positive, const CVirtualGimbalEvent::VirtualEventType negative) { - if (!hlsl::CCameraMathUtilities::isFiniteScalar(denominator) || hlsl::CCameraMathUtilities::isNearlyZeroScalar(denominator, static_cast(ICamera::TinyScalarEpsilon))) + if (!hlsl::CCameraMathUtilities::isFiniteScalar(denominator) || hlsl::CCameraMathUtilities::isNearlyZeroScalar(denominator, static_cast(SCameraToolingThresholds::TinyScalarEpsilon))) return; appendSignedVirtualEvent(events, value / denominator, positive, negative, tolerance); @@ -116,7 +116,7 @@ struct CCameraVirtualEventUtilities final std::vector& events, const hlsl::float64_t3& localDelta, const hlsl::float64_t3& denominators = hlsl::float64_t3(1.0), - const hlsl::float64_t3& tolerances = hlsl::float64_t3(ICamera::TinyScalarEpsilon)) + const hlsl::float64_t3& tolerances = hlsl::float64_t3(SCameraToolingThresholds::TinyScalarEpsilon)) { appendScaledVirtualAxisEvents( events, @@ -132,7 +132,7 @@ struct CCameraVirtualEventUtilities final const hlsl::camera_quaternion_t& orientation, const hlsl::float64_t3& worldDelta, const hlsl::float64_t3& denominators = hlsl::float64_t3(1.0), - const hlsl::float64_t3& tolerances = hlsl::float64_t3(ICamera::TinyScalarEpsilon)) + const hlsl::float64_t3& tolerances = hlsl::float64_t3(SCameraToolingThresholds::TinyScalarEpsilon)) { appendLocalTranslationEvents( events, diff --git a/common/include/camera/CChaseCamera.hpp b/common/include/camera/CChaseCamera.hpp index 51eaaa134..36d269e07 100644 --- a/common/include/camera/CChaseCamera.hpp +++ b/common/include/camera/CChaseCamera.hpp @@ -9,11 +9,11 @@ namespace nbl::core { -/// @brief Target-relative camera that slides the target on the ground plane while chasing it from behind. +/// @brief Target-relative camera with planar target translation on the ground plane. /// -/// The camera keeps a bounded orbit around the target, but translation is resolved -/// in the planar forward/right frame so the tracked subject can be moved across a -/// horizontal surface without changing the follow style. +/// Translation is resolved in a planar forward/right frame derived from the +/// current orbit basis. Rotation updates orbit yaw and pitch. Distance remains +/// clamped to the chase-camera limits. class CChaseCamera final : public CSphericalTargetCamera { public: diff --git a/common/include/camera/CDollyCamera.hpp b/common/include/camera/CDollyCamera.hpp index 2ef482a4d..043c65610 100644 --- a/common/include/camera/CDollyCamera.hpp +++ b/common/include/camera/CDollyCamera.hpp @@ -9,11 +9,11 @@ namespace nbl::core { -/// @brief Target-relative camera that translates the tracked target in the full local camera frame. +/// @brief Target-relative camera that translates the target in the full local camera basis. /// -/// Unlike the chase camera, dolly translation preserves the full local basis, -/// which makes the target move along the current right/up/forward frame while the -/// camera keeps looking back at it from the maintained spherical offset. +/// Translation uses the current right/up/forward basis. Rotation updates orbit +/// yaw and pitch while the camera pose is rebuilt from the maintained +/// target-relative offset. class CDollyCamera final : public CSphericalTargetCamera { public: diff --git a/common/include/camera/CFPSCamera.hpp b/common/include/camera/CFPSCamera.hpp index 009215e1b..af8e7b85b 100644 --- a/common/include/camera/CFPSCamera.hpp +++ b/common/include/camera/CFPSCamera.hpp @@ -12,7 +12,11 @@ namespace nbl::core { -/// @brief Free-position camera that keeps the view upright and exposes only yaw/pitch rotation. +/// @brief Free-position camera with world-space translation and yaw/pitch rotation. +/// +/// The runtime state consists of position plus an upright orientation derived +/// from yaw and pitch. Reference-frame application rejects arbitrary roll and +/// rebuilds the legal FPS orientation from the extracted forward axis. class CFPSCamera final : public ICamera { public: diff --git a/common/include/camera/COrbitCamera.hpp b/common/include/camera/COrbitCamera.hpp index b6e0d6de9..df411c4b8 100644 --- a/common/include/camera/COrbitCamera.hpp +++ b/common/include/camera/COrbitCamera.hpp @@ -8,10 +8,10 @@ namespace nbl::core { -/// @brief Target-relative camera that interprets translation input as orbit-angle and distance changes. +/// @brief Target-relative camera with state `(target, orbitUv, distance)`. /// -/// This is the simplest spherical-target camera in the stack. It keeps the target -/// fixed and adjusts only orbit yaw, orbit pitch, and camera distance. +/// Runtime input updates only orbit yaw, orbit pitch, and camera distance. +/// The target position remains unchanged during `manipulate(...)`. class COrbitCamera final : public CSphericalTargetCamera { public: diff --git a/common/include/camera/CPlanarProjection.hpp b/common/include/camera/CPlanarProjection.hpp index 722484bb4..5407b9dd4 100644 --- a/common/include/camera/CPlanarProjection.hpp +++ b/common/include/camera/CPlanarProjection.hpp @@ -8,8 +8,8 @@ namespace nbl::core { /// @brief Range-backed concrete implementation of `IPlanarProjection`. /// - /// The template owns a caller-selected contiguous container of planar projection - /// entries and keeps their viewport-local binding layouts together with the camera. + /// The template owns a caller-selected contiguous container of planar + /// projection entries together with their viewport-local binding layouts. template ProjectionsRange> class CPlanarProjection : public IPlanarProjection { diff --git a/common/include/camera/CSphericalTargetCamera.hpp b/common/include/camera/CSphericalTargetCamera.hpp index 97a88b5b9..55cb5a292 100644 --- a/common/include/camera/CSphericalTargetCamera.hpp +++ b/common/include/camera/CSphericalTargetCamera.hpp @@ -7,9 +7,10 @@ namespace nbl::core { -/// @brief Common base for cameras orbiting or tracking a target with spherical coordinates. +/// @brief Common base for target-relative cameras represented by target position, distance, and `orbitUv`. /// -/// The shared state is target position, distance, and orbit angles stored in `orbitUv`. +/// Derived cameras keep the same target-relative storage but apply different +/// constraints and event policies in `manipulate(...)`. class CSphericalTargetCamera : public ICamera { public: @@ -46,8 +47,8 @@ class CSphericalTargetCamera : public ICamera inline float getDistance() const { return m_distance; } inline const hlsl::float64_t2& getOrbitUv() const { return m_orbitUv; } - static inline constexpr float MinDistance = base_t::SphericalMinDistance; - static inline constexpr float MaxDistance = base_t::SphericalMaxDistance; + static inline constexpr float MinDistance = SCameraTargetRelativeTraits::MinDistance; + static inline constexpr float MaxDistance = SCameraTargetRelativeTraits::DefaultMaxDistance; virtual uint32_t getCapabilities() const override { @@ -118,7 +119,7 @@ class CSphericalTargetCamera : public ICamera { const auto basis = hlsl::CCameraMathUtilities::getQuaternionBasisMatrix(reference.orientation); const auto planarUp = hlsl::float64_t2(basis[1].x, basis[1].y); - constexpr auto Epsilon = static_cast(base_t::TinyScalarEpsilon); + constexpr auto Epsilon = static_cast(SCameraToolingThresholds::TinyScalarEpsilon); if (!hlsl::CCameraMathUtilities::isNearlyZeroVector(planarUp, Epsilon)) return hlsl::atan2(planarUp.y, planarUp.x); @@ -135,7 +136,7 @@ class CSphericalTargetCamera : public ICamera const auto offset = hlsl::float64_t3(reference.frame[3]) - m_targetPosition; const auto distance = hlsl::length(offset); if (!hlsl::CCameraMathUtilities::isFiniteScalar(distance) || - distance <= static_cast(base_t::TinyScalarEpsilon)) + distance <= static_cast(SCameraToolingThresholds::TinyScalarEpsilon)) { return false; } @@ -191,7 +192,7 @@ class CSphericalTargetCamera : public ICamera inline void applyPlanarTargetTranslation(const hlsl::float64_t3& deltaTranslation, const SphericalBasis& basis) { - if (!hlsl::CCameraMathUtilities::hasPlanarDeltaXY(deltaTranslation, static_cast(base_t::TinyScalarEpsilon))) + if (!hlsl::CCameraMathUtilities::hasPlanarDeltaXY(deltaTranslation, static_cast(SCameraToolingThresholds::TinyScalarEpsilon))) return; m_targetPosition += hlsl::CCameraMathUtilities::transformLocalVectorToWorldBasis( diff --git a/common/include/camera/CVirtualGimbalEvent.hpp b/common/include/camera/CVirtualGimbalEvent.hpp index f3cf65ee0..37a9ff107 100644 --- a/common/include/camera/CVirtualGimbalEvent.hpp +++ b/common/include/camera/CVirtualGimbalEvent.hpp @@ -11,12 +11,14 @@ namespace nbl::core { -/// @brief Shared semantic camera command. +/// @brief One semantic camera command passed to `ICamera::manipulate(...)`. /// -/// Input processors and scripted tools emit these events. -/// Camera implementations consume them through `ICamera::manipulate(...)`. +/// `type` selects the command family. `magnitude` stores the non-negative +/// scalar amount for that command. Input binders, scripted playback, replay +/// helpers, and gizmo-driven tools all use the same event representation. struct CVirtualGimbalEvent { + /// @brief Bitmask identifiers for semantic movement, rotation, and scale commands. enum VirtualEventType : uint32_t { None = 0, @@ -49,11 +51,15 @@ struct CVirtualGimbalEvent All = Translate | Rotate | Scale }; + /// @brief Scalar type used to encode one event magnitude. using manipulation_encode_t = hlsl::float64_t; + /// @brief Semantic event identifier. VirtualEventType type = None; + /// @brief Non-negative scalar amount associated with `type`. manipulation_encode_t magnitude = {}; + /// @brief Convert one event identifier to its stable string form. static constexpr std::string_view virtualEventToString(VirtualEventType event) { switch (event) @@ -84,6 +90,7 @@ struct CVirtualGimbalEvent } } + /// @brief Convert one stable string identifier back to an event identifier. static constexpr VirtualEventType stringToVirtualEvent(std::string_view event) { if (event == "MoveForward") return MoveForward; @@ -111,21 +118,25 @@ struct CVirtualGimbalEvent return None; } + /// @brief Return whether `event` belongs to the translation subset. static constexpr bool isTranslationEvent(const VirtualEventType event) { return event != None && (event & Translate) == event; } + /// @brief Return whether `event` belongs to the rotation subset. static constexpr bool isRotationEvent(const VirtualEventType event) { return event != None && (event & Rotate) == event; } + /// @brief Return whether `event` belongs to the scale subset. static constexpr bool isScaleEvent(const VirtualEventType event) { return event != None && (event & Scale) == event; } + /// @brief Table listing every individual event bit in declaration order. static inline constexpr auto VirtualEventsTypeTable = []() { std::array output; diff --git a/common/include/camera/ICamera.hpp b/common/include/camera/ICamera.hpp index 8d2d3aef0..06dbcd641 100644 --- a/common/include/camera/ICamera.hpp +++ b/common/include/camera/ICamera.hpp @@ -8,47 +8,42 @@ #include #include +#include "CCameraTraits.hpp" #include "IGimbal.hpp" namespace nbl::core { -/// @brief Shared camera interface. +/// @brief Shared runtime camera interface. /// -/// The hot runtime path is event-only: cameras consume `CVirtualGimbalEvent` -/// streams through `manipulate(...)`. Optional typed state hooks exist only for -/// tooling features such as capture, compatibility analysis, presets, and playback. +/// `ICamera` consumes batches of `CVirtualGimbalEvent` values and updates one +/// camera pose stored in `CGimbal`. A `CVirtualGimbalEvent` identifies one +/// semantic command such as `MoveForward`, `PanLeft`, or `RollRight` and carries +/// one scalar magnitude for that command. +/// +/// Keyboard input, mouse input, ImGuizmo interaction, scripted playback, +/// preset replay, follow helpers, and goal solving all drive cameras through +/// the same `manipulate(...)` entry point. +/// +/// The optional typed hooks expose camera-family state for code that needs +/// capture, restore, compatibility analysis, persistence, or validation. class ICamera : virtual public core::IReferenceCounted { public: - /// @brief Shared lower bound used by spherical and path rigs for valid camera distance. - static inline constexpr float SphericalMinDistance = 0.1f; - /// @brief Shared upper bound used by spherical and path rigs for valid camera distance. - static inline constexpr float SphericalMaxDistance = 10000.f; - /// @brief Base runtime translation magnitude represented by a unit virtual move event. - static inline constexpr double VirtualTranslationStep = 0.01; - /// @brief Default multiplier applied to virtual translation magnitudes. - static inline constexpr double DefaultMoveSpeedScale = VirtualTranslationStep; - /// @brief Default multiplier applied to virtual rotation magnitudes. - static inline constexpr double DefaultRotationSpeedScale = 0.003; - /// @brief Shared scalar epsilon used by typed tooling comparisons. - static inline constexpr double ScalarTolerance = 1e-6; - /// @brief Very small epsilon used when exact replay helpers need stricter comparisons. - static inline constexpr double TinyScalarEpsilon = 1e-9; - /// @brief Default world-space position tolerance used by pose comparisons. - static inline constexpr double DefaultPositionTolerance = 2.0 * ScalarTolerance; - /// @brief Default angular tolerance in degrees used by pose and state comparisons. - static inline constexpr double DefaultAngularToleranceDeg = 0.1; - - /// @brief Camera-local multipliers applied when translating virtual events into motion. + /// @brief Camera-local multipliers applied when semantic virtual events are converted into motion. + /// + /// The shared runtime traits define the base unit magnitude of a virtual + /// event. Concrete cameras multiply those base units by this per-camera + /// configuration before applying them to their own state model. struct SMotionConfig { - /// @brief Camera-local scales applied by implementations to virtual motion magnitude. - double moveSpeedScale = DefaultMoveSpeedScale; - double rotationSpeedScale = DefaultRotationSpeedScale; + /// @brief Camera-local scale applied to virtual translation magnitudes. + double moveSpeedScale = SCameraRuntimeTraits::DefaultMoveSpeedScale; + /// @brief Camera-local scale applied to virtual rotation magnitudes. + double rotationSpeedScale = SCameraRuntimeTraits::DefaultRotationSpeedScale; }; - /// @brief Stable runtime camera-family identifier used by tooling, metadata, and default presets. + /// @brief Stable camera-family identifier used by metadata, presets, follow, and scripted helpers. enum class CameraKind : uint8_t { Unknown, @@ -73,7 +68,7 @@ class ICamera : virtual public core::IReferenceCounted DynamicPerspectiveFov = core::createBitmask({ 1 }) }; - /// @brief Typed goal-state fragments that tooling may capture from or apply to a camera. + /// @brief Typed state fragments that helper layers may capture from or apply to a camera. enum GoalStateMask : uint32_t { GoalStateNone = 0u, @@ -82,11 +77,13 @@ class ICamera : virtual public core::IReferenceCounted GoalStatePath = core::createBitmask({ 2 }) }; - /// @brief Canonical spherical-target state shared by orbit-like cameras. + /// @brief Canonical target-relative state reported by spherical camera families. /// /// The state stores the tracked target position, orbit angles in `orbitUv`, /// and distance limits needed by tooling that wants to capture or reapply a /// target-relative camera pose without going through free-form setters. + /// `maxDistance` is an optional upper bound and may be infinite when the + /// active camera family does not impose a finite cap. struct SphericalTargetState { /// @brief Tracked target position in world space. @@ -97,11 +94,11 @@ class ICamera : virtual public core::IReferenceCounted float distance = 0.f; /// @brief Lowest distance that remains valid for the current camera. float minDistance = 0.f; - /// @brief Highest distance that remains valid for the current camera. - float maxDistance = 0.f; + /// @brief Highest distance that remains valid for the current camera, or infinity when unbounded. + float maxDistance = SCameraTargetRelativeTraits::DefaultMaxDistance; }; - /// @brief Typed authored state used by cameras with derived perspective behavior. + /// @brief Typed perspective state reported by cameras with derived FOV behavior. struct DynamicPerspectiveState { /// @brief Authored reference FOV in degrees. @@ -111,22 +108,25 @@ class ICamera : virtual public core::IReferenceCounted }; /// @brief Limits constraining reusable `PathState` coordinates for `Path Rig` cameras. + /// + /// These limits are part of the typed path-model surface. They are not + /// global engine rules. A concrete `Path Rig` instance may expose an + /// unbounded `maxDistance` by returning infinity. struct PathStateLimits { /// @brief Minimal valid `u` coordinate after path-state sanitization. - double minU = static_cast(SphericalMinDistance); + double minU = static_cast(SCameraTargetRelativeTraits::MinDistance); /// @brief Minimal valid radial distance derived from the `(u, v)` pair. - hlsl::float64_t minDistance = static_cast(SphericalMinDistance); - /// @brief Maximal valid radial distance derived from the `(u, v)` pair. - hlsl::float64_t maxDistance = static_cast(SphericalMaxDistance); + hlsl::float64_t minDistance = static_cast(SCameraTargetRelativeTraits::MinDistance); + /// @brief Maximal valid radial distance derived from the `(u, v)` pair, or infinity when unbounded. + hlsl::float64_t maxDistance = static_cast(SCameraTargetRelativeTraits::DefaultMaxDistance); }; /// @brief Parametric path-rig state used by the `Path Rig` camera kind. /// - /// The default shared model interprets `(s, u, v, roll)` as angular progress, - /// radial component, vertical component, and view-axis roll around a target. - /// Concrete path models may reuse the same coordinates differently, while the - /// hot runtime path still stays event-only through `manipulate(...)`. + /// The built-in path model interprets `(s, u, v, roll)` as path progress, + /// lateral shape coordinates, and roll around the local forward axis. + /// Other path models may map the same coordinates onto different geometry. struct PathState { /// @brief Primary path-progress coordinate interpreted by the active path model. @@ -138,13 +138,13 @@ class ICamera : virtual public core::IReferenceCounted /// @brief Roll around the path-model forward axis, expressed in radians. double roll = 0.0; - /// @brief Pack the state into one four-component vector for math helpers and persistence tooling. + /// @brief Pack the state into one four-component vector. inline hlsl::float64_t4 asVector() const { return hlsl::float64_t4(s, u, v, roll); } - /// @brief Project the state onto the shared translation-style view used by replay helpers. + /// @brief Project the state onto the translation-style representation used by replay helpers. inline hlsl::float64_t3 asTranslationVector() const { return hlsl::float64_t3(u, v, s); @@ -173,7 +173,11 @@ class ICamera : virtual public core::IReferenceCounted } }; - /// @brief Gimbal that models the camera pose and cached view matrix in world space. + /// @brief Gimbal that stores the runtime camera pose and cached world-to-view transform. + /// + /// Camera implementations own one `CGimbal` instance and update it after + /// applying their internal state model. The gimbal stores world-space + /// position, orientation, and the cached view matrix derived from them. class CGimbal : public IGimbal { public: @@ -248,9 +252,18 @@ class ICamera : virtual public core::IReferenceCounted /// @brief Return the mutable gimbal backing the runtime camera pose. virtual const CGimbal& getGimbal() = 0u; - /// @brief Consume virtual events only. + /// @brief Apply one frame of semantic virtual events and an optional rigid reference-frame anchor. + /// + /// `virtualEvents` stores one frame of semantic movement, rotation, and + /// scale commands. Translation commands use `Move*`, rotation commands use + /// `Tilt*`, `Pan*`, and `Roll*`, and scale commands use `Scale*`. Cameras + /// interpret only the subset advertised by `getAllowedVirtualEvents()`. /// - /// Raw input binding and absolute goal solving live outside `ICamera`. + /// `referenceFrame` is an optional rigid world-space transform used as the + /// anchor for this manipulation step. Free-like cameras may apply it + /// directly as pose input. Constrained cameras may first resolve it into + /// their own typed legal state and then apply event deltas in that state + /// space. virtual bool manipulate(std::span virtualEvents, const hlsl::float64_t4x4* referenceFrame = nullptr) = 0; /// @brief Apply one frame of virtual events while temporarily overriding the camera-local motion scales. inline bool manipulateWithMotionScales(std::span virtualEvents, const hlsl::float64_t4x4* referenceFrame, const double moveScale, const double rotationScale) @@ -265,13 +278,17 @@ class ICamera : virtual public core::IReferenceCounted } /// @brief Return the semantic virtual-event mask accepted by this camera kind. + /// + /// Input binders, scripted replay, and restore helpers use this mask to + /// decide which `CVirtualGimbalEvent` categories may be passed to + /// `manipulate(...)`. virtual uint32_t getAllowedVirtualEvents() const = 0u; /// @brief Return the stable camera-family identifier for this concrete runtime camera. virtual CameraKind getKind() const = 0; /// @brief Return the optional typed capabilities exposed by this camera implementation. virtual uint32_t getCapabilities() const { return None; } - /// @brief Return the typed goal-state fragments that tooling may safely use with this camera. + /// @brief Return the typed goal-state fragments that helper layers may safely use with this camera. virtual uint32_t getGoalStateMask() const { uint32_t mask = GoalStateNone; @@ -379,12 +396,12 @@ class ICamera : virtual public core::IReferenceCounted /// @brief Return the effective world-space translation represented by a unit virtual move event. inline double getScaledVirtualTranslationMagnitude() const { - return VirtualTranslationStep * getMoveSpeedScale(); + return SCameraRuntimeTraits::VirtualTranslationStep * getMoveSpeedScale(); } /// @brief Return the raw translation magnitude before applying the camera-local move scale. inline double getUnscaledVirtualTranslationMagnitude() const { - return VirtualTranslationStep; + return SCameraRuntimeTraits::VirtualTranslationStep; } /// @brief Scale one scalar translation magnitude through the active move scale. inline double scaleVirtualTranslation(const double magnitude) const diff --git a/common/include/camera/IGimbal.hpp b/common/include/camera/IGimbal.hpp index 7e426300d..168ff394a 100644 --- a/common/include/camera/IGimbal.hpp +++ b/common/include/camera/IGimbal.hpp @@ -19,9 +19,10 @@ namespace nbl::core /// @brief Generic world-space gimbal used by runtime cameras and tracked targets. /// - /// The gimbal owns position, orientation, scale, and an orthonormal local basis. - /// It also provides the shared `accumulate(...)` helper that turns semantic - /// `CVirtualGimbalEvent` batches into translation, rotation, and scale impulses. + /// The gimbal stores position, orientation, scale, and an orthonormal local + /// basis. It also exposes `accumulate(...)`, which converts one batch of + /// semantic `CVirtualGimbalEvent` values into translation, rotation, and + /// scale impulses for a single manipulation step. template requires is_any_of_v class IGimbal diff --git a/common/include/camera/IPlanarProjection.hpp b/common/include/camera/IPlanarProjection.hpp index 4b79f6f66..04f718f39 100644 --- a/common/include/camera/IPlanarProjection.hpp +++ b/common/include/camera/IPlanarProjection.hpp @@ -9,8 +9,8 @@ namespace nbl::core /// @brief Linear projection wrapper for one camera-facing planar viewport. /// -/// The projection owns viewport-local binding layout storage, while runtime input -/// processing is expected to happen through `CGimbalInputBinder`. +/// The projection stores viewport-local binding layouts. Runtime input +/// processing is handled by `CGimbalInputBinder`. class IPlanarProjection : public ILinearProjection { public: diff --git a/common/include/camera/README.md b/common/include/camera/README.md index 0f5824c12..edc09c7b3 100644 --- a/common/include/camera/README.md +++ b/common/include/camera/README.md @@ -1,7 +1,7 @@ # Shared Camera API This directory contains the reusable camera stack used by [`61_UI`](../../../61_UI/README.md). -It is the source of truth for camera semantics, typed camera state, scripted playback, follow behavior, and camera-focused validation. +It defines the camera runtime, typed camera state, follow helpers, scripted playback data, and validation helpers reused by the example and by camera-domain tooling. ## Scope @@ -18,13 +18,11 @@ This stack is not: - a setter-heavy runtime camera API - a `61_UI`-local convenience layer -## Design pillars - -The current design is built around five rules. +## Design rules ### 1. Runtime is event-driven -The hot path stays on: +The runtime entry path is: ```text input -> virtual events -> ICamera::manipulate(...) @@ -42,11 +40,11 @@ Capture, presets, restore, tracks, and validation use typed sidecar state: - [`CCameraPlaybackTimeline.hpp`](CCameraPlaybackTimeline.hpp) - [`CCameraSequenceScript.hpp`](CCameraSequenceScript.hpp) -That layer is allowed to reason in terms of canonical camera state because it is not the hot input path. +That layer stores canonical camera state and helper data outside the per-frame runtime input path. ### 3. Input mapping is separate from camera semantics -The reusable stack keeps these responsibilities separate: +The stack separates these responsibilities: - physical input mapping - virtual event processing @@ -54,7 +52,7 @@ The reusable stack keeps these responsibilities separate: - absolute state capture / restore - scripted playback and validation -This keeps camera models reusable across keyboard, mouse, ImGuizmo, CI, and headless tools. +The same camera model can then be driven from keyboard input, mouse input, ImGuizmo, CI playback, and headless tools. ### 4. Projection-local state is allowed, runtime input processing is not @@ -65,9 +63,23 @@ Projection types may own viewport-local binding layouts, but raw input processin Follow and scripted playback are shared layers built on top of camera semantics. They are not hardwired into `ICamera`. +### 6. Reference-frame manipulation resolves legal camera state + +`referenceFrame` is part of the public runtime seam on `ICamera::manipulate(...)`. +All camera kinds consume it, but each family resolves it through its own state model. + +- `Free` and `FPS` + treat the extracted rigid reference as the pose anchor for one manipulation step +- constrained target-relative cameras + project the rigid reference onto the nearest legal state of that camera family and then apply event deltas in that state space +- `Path Rig` + resolves the extracted rigid reference through the active typed path model + +World-space and local-space gizmo workflows therefore call the same runtime seam across camera kinds. + ## Namespace split -The stack is intentionally spread across existing Nabla namespaces. +The code is split across existing Nabla namespaces. - `nbl::hlsl` camera math, transform math, pose deltas, interpolation helpers, and reusable vector/quaternion types @@ -79,11 +91,11 @@ The stack is intentionally spread across existing Nabla namespaces. persistence, scripted runtime parsing, scripted timeline building, scripted check execution, and follow validation The shared camera math is written against `nbl::hlsl`. -Consumers are expected to use those `nbl::hlsl` types and helpers directly rather than duplicating local math wrappers. +Consumers can use the same `nbl::hlsl` types and helpers when they need to exchange typed camera-domain math with this stack. ## Runtime pipeline -The hot runtime path is: +The runtime pipeline is: ```text raw input @@ -103,7 +115,24 @@ The main building blocks are: - [`ICamera.hpp`](ICamera.hpp) camera runtime interface and typed tooling hooks - [`SCameraRigPose.hpp`](SCameraRigPose.hpp) - shared typed pose transport used outside the hot runtime path + shared typed pose transport used outside the runtime pipeline + +### Reference-frame runtime seam + +The optional `referenceFrame` argument on `ICamera::manipulate(...)` is a rigid-frame manipulation anchor. + +At runtime the shared pattern is: + +```text +referenceFrame + -> CGimbal::extractReferenceTransform(...) + -> resolve the nearest legal camera state for this camera kind + -> accumulate virtual events + -> apply camera-local deltas in that state space + -> rebuild pose +``` + +The seam is stable across all camera kinds. The legal-state projection remains camera-specific. ## Input layer @@ -135,7 +164,7 @@ These helpers provide shared default input presets for camera kinds and the scri The main runtime interface is [`ICamera.hpp`](ICamera.hpp). -Important properties: +Shared properties: - runtime entry point is `manipulate(std::span, const hlsl::float64_t4x4*)` - cameras own their own `CGimbal` @@ -163,6 +192,7 @@ The capability and state-mask surface is: - [`CFreeLockCamera.hpp`](CFreeLockCamera.hpp) These are pose-driven cameras without spherical target semantics. +They consume `referenceFrame` as a rigid pose anchor directly. ### Spherical-target family @@ -181,13 +211,22 @@ These cameras share: - distance - orbit angles in `orbitUv` +They also share the same reference-frame rule: + +- extract one rigid reference transform +- resolve a legal target-relative state against the current target +- apply virtual-event deltas on top of that resolved state + ### Extended-state cameras - [`CDollyZoomCamera.hpp`](CDollyZoomCamera.hpp) - [`CPathCamera.hpp`](CPathCamera.hpp) `DollyZoom` adds dynamic perspective state. -`Path Rig` adds typed path-rig state and a pluggable path model. +`Path Rig` adds typed path-rig state and a path model. + +`DollyZoom` resolves reference frames through target-relative state plus dynamic perspective state. +`Path Rig` resolves reference frames through its typed path-model callbacks. ## Path Rig design @@ -235,8 +274,8 @@ They are queried through `tryGetPathStateLimits(...)` and used by the solver, va ### Path model seam -The path seam is [`SCameraPathModel`](CCameraPathUtilities.hpp). -It is a named typed model, not an ad-hoc runtime hack. +The path-model interface is [`SCameraPathModel`](CCameraPathUtilities.hpp). +`CPathCamera` and path utilities call this typed callback bundle to resolve, integrate, and evaluate path state. It contains five callbacks: @@ -252,10 +291,11 @@ It contains five callbacks: retarget state to a requested spherical distance The default model is created by `CCameraPathUtilities::makeDefaultPathModel()`. +That same model is also responsible for resolving `referenceFrame` into legal typed path state. ### Runtime behavior of `CPathCamera` -[`CPathCamera.hpp`](CPathCamera.hpp) keeps the public runtime entry point unchanged: +[`CPathCamera.hpp`](CPathCamera.hpp) uses the same public runtime entry point as the other camera kinds: ```text virtual events -> gimbal accumulation -> path controlLaw -> integrate -> evaluate -> gimbal pose @@ -276,13 +316,13 @@ and exposes: - `tryGetPathState(...)` - `trySetPathState(...)` -Construction is resilient: +Construction handles three cases: - a complete custom model is accepted directly - an incomplete model falls back to the shared default model - invalid custom limits are sanitized or replaced with shared defaults -That means `Path Rig` is first-class and pluggable without changing `ICamera::manipulate(...)`. +`Path Rig` therefore uses `ICamera::manipulate(...)` like the other camera kinds while changing only the path-state callbacks behind it. ## Goals, presets, tracks, and playback @@ -309,7 +349,7 @@ It extends [`SCameraRigPose.hpp`](SCameraRigPose.hpp) with optional state such a - apply what can be applied through typed hooks - replay virtual events when needed -This is the main best-effort absolute layer. +This layer converts between typed goal state and the event-driven runtime surface. ### Presets and preset flow @@ -318,7 +358,7 @@ This is the main best-effort absolute layer. - [`CCameraPresetPersistence.hpp`](CCameraPresetPersistence.hpp) Presets are named `CCameraGoal` wrappers. -`CCameraPresetFlowUtilities` provides the high-level capture/apply helpers that most consumers should call. +`CCameraPresetFlowUtilities` provides the high-level capture/apply helpers used by camera-stack consumers. ### Keyframe tracks and playback @@ -348,17 +388,17 @@ Follow is deliberately not baked into `ICamera`. The tracked subject is `CTrackedTarget`. It owns its own `ICamera::CGimbal`. -The follow source of truth is: +Follow uses: - tracked-target pose - follow mode - follow config -not a scene-node id or mesh handle. +It does not use a scene-node id or mesh handle. ### Follow modes -Current reusable modes are: +Reusable modes: - `OrbitTarget` - `LookAtTarget` @@ -382,7 +422,7 @@ The scripting side is split into compact authored data and expanded runtime payl - [`CCameraSequenceScript.hpp`](CCameraSequenceScript.hpp) - [`CCameraSequenceScriptPersistence.hpp`](CCameraSequenceScriptPersistence.hpp) -This is the human-maintainable authored representation. +This layer stores the authored sequence description. It stores: - camera kind or identifier @@ -392,7 +432,7 @@ It stores: - continuity thresholds - capture fractions -It intentionally does not store frame-by-frame low-level event dumps. +It stores compact keyframes and presentation requests. It does not store frame-by-frame low-level event dumps. ### Expanded runtime payload @@ -433,12 +473,12 @@ Projection state is a separate reusable layer. - [`CCubeProjection.hpp`](CCubeProjection.hpp) - [`IRange.hpp`](IRange.hpp) -Important rule: +Projection-layer rule: - projections may own viewport-local binding layouts - projections do not process raw runtime input -That separation keeps viewport glue out of reusable camera semantics. +Projection-local viewport glue stays in the projection layer. Reusable camera semantics stay in the camera helpers. ## Presentation and UI-facing helpers @@ -452,7 +492,7 @@ The shared layer also exposes small reusable helpers used by consumers such as ` - [`CCameraScriptVisualDebugOverlayUtilities.hpp`](CCameraScriptVisualDebugOverlayUtilities.hpp) These are still shared camera helpers. -They are presentation-facing, but they remain camera-domain rather than `61_UI`-local glue. +They are presentation-facing helpers built on shared camera-domain data. They are not `61_UI`-local copies. ## 61_UI integration @@ -470,8 +510,7 @@ They are presentation-facing, but they remain camera-domain rather than `61_UI`- - screenshot capture - local logging -The shared camera layer remains the source of truth for camera semantics. -`61_UI` is the concrete harness that exercises them. +`61_UI` exercises the shared camera layer in one runnable scene. ## Local configure, build, and test @@ -486,7 +525,7 @@ cmake --preset user-configure-dynamic-msvc Build `61_UI`: ```powershell -cmake --build build/dynamic/examples_tests/61_UI --config Debug --target 61_ui -- /m:1 +cmake --build build/dynamic/examples_tests/61_UI --config Debug --target 61_ui -- /m ``` Run the camera-focused tests: @@ -495,6 +534,14 @@ Run the camera-focused tests: ctest --test-dir build/dynamic/examples_tests/61_UI -C Debug --output-on-failure -R NBL_61_UI_CAMERA_ ``` +Those smoke and continuity tests cover: + +- all runtime camera kinds +- typed goal capture and replay +- follow behavior +- `Path Rig` typed-state manipulation +- `referenceFrame` application across rigid, target-relative, and path-model cameras + Run the example manually: ```powershell @@ -585,5 +632,9 @@ input layer -> shared validation ``` -That split is intentional. -It is what keeps the runtime path small, the tooling path expressive, and the `61_UI` integration reusable instead of example-local. +This split yields: + +- one event-driven runtime path +- one typed tooling path +- one shared scripting and validation layer +- one runnable example that consumes those shared pieces diff --git a/common/include/camera/SCameraRigPose.hpp b/common/include/camera/SCameraRigPose.hpp index a8d5e67bd..f8f3d7dfe 100644 --- a/common/include/camera/SCameraRigPose.hpp +++ b/common/include/camera/SCameraRigPose.hpp @@ -11,10 +11,8 @@ namespace nbl::core /// @brief Canonical camera pose consisting of world-space position and orientation. /// -/// This is the smallest pose transport shared by the typed tooling layer. It is -/// intentionally independent from any concrete camera model so goals, follow, -/// path evaluation, playback, and scripted validation can exchange poses without -/// reaching into camera-specific runtime state. +/// This type stores only pose data. Higher-level types add target-relative, +/// dynamic-perspective, or path-specific state around it. struct SCameraRigPose { /// @brief Camera origin in world space. From 873976bbb24cfc22b8e6565c761c0a6f01498412 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Thu, 9 Apr 2026 19:07:55 +0200 Subject: [PATCH 197/205] Clarify camera reference frame semantics --- 61_UI/AppHeadlessCameraSmokeChecks.inl | 24 +- common/include/camera/README.md | 1086 +++++++++++++++--------- 2 files changed, 684 insertions(+), 426 deletions(-) diff --git a/61_UI/AppHeadlessCameraSmokeChecks.inl b/61_UI/AppHeadlessCameraSmokeChecks.inl index a9f2a55c1..432900d5f 100644 --- a/61_UI/AppHeadlessCameraSmokeChecks.inl +++ b/61_UI/AppHeadlessCameraSmokeChecks.inl @@ -232,9 +232,9 @@ return false; } - if (!nbl::core::CCameraManipulationUtilities::applyReferenceFrameToCamera(camera, referenceFrame)) + if (!camera->manipulate({}, &referenceFrame)) { - outError = std::string(label) + " reference-frame smoke failed to apply the reference pose."; + outError = std::string(label) + " reference-frame smoke failed to apply the reference pose through manipulate({}, &referenceFrame)."; return false; } @@ -282,9 +282,9 @@ return false; } - if (!nbl::core::CCameraManipulationUtilities::applyReferenceFrameToCamera(camera, baselineReferenceFrame)) + if (!camera->manipulate({}, &baselineReferenceFrame)) { - outError = std::string(label) + " reference-frame smoke failed to restore the baseline reference pose."; + outError = std::string(label) + " reference-frame smoke failed to restore the baseline reference pose through manipulate({}, &referenceFrame)."; return false; } @@ -316,9 +316,9 @@ const auto referenceFrame = hlsl::CCameraMathUtilities::composeTransformMatrix( desiredPosition, expectedGoal.orientation); - if (!nbl::core::CCameraManipulationUtilities::applyReferenceFrameToCamera(camera, referenceFrame)) + if (!camera->manipulate({}, &referenceFrame)) { - outError = std::string(label) + " reference-frame smoke failed to apply the rigid reference pose."; + outError = std::string(label) + " reference-frame smoke failed to apply the rigid reference pose through manipulate({}, &referenceFrame)."; return false; } @@ -336,9 +336,9 @@ return false; } - if (!nbl::core::CCameraManipulationUtilities::applyReferenceFrameToCamera(camera, baselineReferenceFrame)) + if (!camera->manipulate({}, &baselineReferenceFrame)) { - outError = std::string(label) + " reference-frame smoke failed to restore the baseline rigid pose."; + outError = std::string(label) + " reference-frame smoke failed to restore the baseline rigid pose through manipulate({}, &referenceFrame)."; return false; } @@ -545,9 +545,9 @@ const auto referenceFrame = hlsl::CCameraMathUtilities::composeTransformMatrix( canonicalPathState.pose.position, canonicalPathState.pose.orientation); - if (!nbl::core::CCameraManipulationUtilities::applyReferenceFrameToCamera(state.pathCamera, referenceFrame)) + if (!state.pathCamera->manipulate({}, &referenceFrame)) { - outError = "Path reference-frame smoke failed to apply the projected path pose."; + outError = "Path reference-frame smoke failed to apply the projected path pose through manipulate({}, &referenceFrame)."; return false; } @@ -565,9 +565,9 @@ return false; } - if (!nbl::core::CCameraManipulationUtilities::applyReferenceFrameToCamera(state.pathCamera, baselineReferenceFrame)) + if (!state.pathCamera->manipulate({}, &baselineReferenceFrame)) { - outError = "Path reference-frame smoke failed to restore the baseline reference pose."; + outError = "Path reference-frame smoke failed to restore the baseline reference pose through manipulate({}, &referenceFrame)."; return false; } } diff --git a/common/include/camera/README.md b/common/include/camera/README.md index edc09c7b3..341003d5c 100644 --- a/common/include/camera/README.md +++ b/common/include/camera/README.md @@ -1,640 +1,898 @@ # Shared Camera API This directory contains the reusable camera stack used by [`61_UI`](../../../61_UI/README.md). -It defines the camera runtime, typed camera state, follow helpers, scripted playback data, and validation helpers reused by the example and by camera-domain tooling. -## Scope +The stack has two public faces: -This stack is: +- a runtime face used to move cameras during one frame +- a typed face used to capture, store, restore, compare, replay, and validate camera state -- a reusable camera runtime based on semantic virtual events -- a typed tooling layer for capture, restore, presets, playback, follow, and scripted validation -- a shared authoring and CI surface reused by `61_UI` +The runtime face is centered on [`ICamera.hpp`](ICamera.hpp). +The typed face is centered on [`CCameraGoal.hpp`](CCameraGoal.hpp) and [`CCameraGoalSolver.hpp`](CCameraGoalSolver.hpp). -This stack is not: +## TL;DR -- the engine-wide Nabla scene graph API -- a generic animation system for arbitrary scene objects -- a setter-heavy runtime camera API -- a `61_UI`-local convenience layer +If you want to know which type to touch first, use this table. -## Design rules +| I want to... | Use | +|---|---| +| apply one absolute rigid pose request at runtime | `camera->manipulate({}, &referenceFrame)` | +| set exact position or exact orientation on `Free` and `FPS` | `referenceFrame` built from `camera->getGimbal()` | +| set one absolute typed state that can be reused later | `CCameraGoal` + `CCameraGoalSolver` | +| move a camera from live input this frame | `ICamera::manipulate(...)` | +| convert keyboard or mouse input into camera commands | `IGimbalInputProcessor` or `CGimbalInputBinder` | +| capture current camera state | `CCameraGoalSolver::capture...` | +| restore a camera from typed state | `CCameraGoalSolver::apply...` | +| save a named camera state | `CCameraPreset` | +| store camera states over time | `CCameraKeyframeTrack` | +| keep playback cursor state | `CCameraPlaybackTimeline` | +| make a camera follow a moving target | `CCameraFollowUtilities` | +| author compact scripted camera sequences | `CCameraSequenceScript` | +| execute frame-by-frame scripted payloads | `CCameraScriptedRuntime` | +| use the path-rig camera | `CPathCamera` and `SCameraPathModel` | -### 1. Runtime is event-driven +## Quick start -The runtime entry path is: +This section shows the common entry points before any deeper explanation. -```text -input -> virtual events -> ICamera::manipulate(...) +### 1. Apply one absolute rigid pose request + +Use this when you already have one rigid transform and want the camera to consume it through the normal runtime entry point. + +```cpp +const auto referenceFrame = + hlsl::CCameraMathUtilities::composeTransformMatrix(desiredPosition, desiredOrientation); + +camera->manipulate({}, &referenceFrame); ``` -Camera models do not expose arbitrary absolute runtime setters for position, target, yaw/pitch/roll, or similar mutable bags of state. +#### Why not just expose `setPosition(...)` and `setOrientation(...)` everywhere? -### 2. Tooling is typed-state driven +Because not every camera kind stores arbitrary rigid pose as its native state. -Capture, presets, restore, tracks, and validation use typed sidecar state: +`Free` can represent arbitrary position and orientation directly. -- [`CCameraGoal.hpp`](CCameraGoal.hpp) -- [`CCameraPreset.hpp`](CCameraPreset.hpp) -- [`CCameraKeyframeTrack.hpp`](CCameraKeyframeTrack.hpp) -- [`CCameraPlaybackTimeline.hpp`](CCameraPlaybackTimeline.hpp) -- [`CCameraSequenceScript.hpp`](CCameraSequenceScript.hpp) +`FPS` cannot. Its legal runtime state is: + +- world-space position +- yaw +- pitch +- upright orientation reconstructed from yaw and pitch + +Consider this `FPS` example: -That layer stores canonical camera state and helper data outside the per-frame runtime input path. +```cpp +const auto desiredPosition = hlsl::float64_t3(2.0, 1.0, -3.0); +const auto desiredOrientation = + hlsl::CCameraMathUtilities::makeQuaternionFromEulerDegreesYXZ( + hlsl::float64_t3(-15.0, 40.0, 25.0)); +``` -### 3. Input mapping is separate from camera semantics +The requested rigid pose contains `roll = 25 deg`. -The stack separates these responsibilities: +That roll is not legal for `FPS`. -- physical input mapping -- virtual event processing -- camera semantics -- absolute state capture / restore -- scripted playback and validation +If the API exposed unrestricted `setOrientation(...)` and accepted that quaternion as-is, the runtime camera would no longer match the rules of the `FPS` rig. -The same camera model can then be driven from keyboard input, mouse input, ImGuizmo, CI playback, and headless tools. +The current API does this instead: -### 4. Projection-local state is allowed, runtime input processing is not +1. accept one rigid pose request through `referenceFrame` +2. project that pose onto the legal state space of the concrete camera kind +3. rebuild the final runtime pose from that legal state -Projection types may own viewport-local binding layouts, but raw input processing stays in the input layer. +For `FPS` that means: -### 5. Follow and scripts stay above camera models +- keep the requested position +- read forward direction from the rigid reference +- rebuild legal `pitch/yaw` +- reject arbitrary roll +- write back one upright `FPS` pose -Follow and scripted playback are shared layers built on top of camera semantics. -They are not hardwired into `ICamera`. +The same pattern applies to every camera family: -### 6. Reference-frame manipulation resolves legal camera state +- `Free` keeps the rigid pose directly +- `FPS` legalizes to upright `position + pitch/yaw` +- target-relative cameras legalize to `target + orbitUv + distance` +- `Path Rig` legalizes to `PathState` -`referenceFrame` is part of the public runtime seam on `ICamera::manipulate(...)`. -All camera kinds consume it, but each family resolves it through its own state model. +That is why `camera->manipulate({}, &referenceFrame)` is the shared absolute runtime path. -- `Free` and `FPS` - treat the extracted rigid reference as the pose anchor for one manipulation step -- constrained target-relative cameras - project the rigid reference onto the nearest legal state of that camera family and then apply event deltas in that state space -- `Path Rig` - resolves the extracted rigid reference through the active typed path model +It accepts one rigid pose request at the API boundary and lets each camera family legalize it according to its own runtime model. -World-space and local-space gizmo workflows therefore call the same runtime seam across camera kinds. +Use this path for: -## Namespace split +- one-shot runtime pose application +- ImGuizmo +- world-space or local-space pose anchoring -The code is split across existing Nabla namespaces. +### 2. Set exact position or exact orientation on `Free` and `FPS` -- `nbl::hlsl` - camera math, transform math, pose deltas, interpolation helpers, and reusable vector/quaternion types -- `nbl::core` - runtime camera model, typed goal state, presets, tracks, follow, playback, path-rig helpers, and authored sequence data -- `nbl::ui` - input binding layouts, input processors, runtime binders, default input presets, and presentation-facing UI helpers -- `nbl::system` - persistence, scripted runtime parsing, scripted timeline building, scripted check execution, and follow validation +Use this when the target camera is `Free` or `FPS` and you want to replace only one rigid-pose component. -The shared camera math is written against `nbl::hlsl`. -Consumers can use the same `nbl::hlsl` types and helpers when they need to exchange typed camera-domain math with this stack. +```cpp +const auto& gimbal = camera->getGimbal(); -## Runtime pipeline +const auto newPosition = desiredPosition; +const auto keepOrientation = gimbal.getOrientation(); -The runtime pipeline is: +const auto referenceFrame = + hlsl::CCameraMathUtilities::composeTransformMatrix(newPosition, keepOrientation); -```text -raw input - -> IGimbalBindingLayout - -> IGimbalInputProcessor / CGimbalInputBinder - -> CVirtualGimbalEvent[] - -> ICamera::manipulate(...) - -> updated camera gimbal and cached view matrix +camera->manipulate({}, &referenceFrame); ``` -The main building blocks are: +```cpp +const auto& gimbal = camera->getGimbal(); -- [`CVirtualGimbalEvent.hpp`](CVirtualGimbalEvent.hpp) - shared semantic command language -- [`IGimbal.hpp`](IGimbal.hpp) - gimbal math, event accumulation, and world-space pose handling -- [`ICamera.hpp`](ICamera.hpp) - camera runtime interface and typed tooling hooks -- [`SCameraRigPose.hpp`](SCameraRigPose.hpp) - shared typed pose transport used outside the runtime pipeline +const auto keepPosition = gimbal.getPosition(); +const auto newOrientation = desiredOrientation; -### Reference-frame runtime seam +const auto referenceFrame = + hlsl::CCameraMathUtilities::composeTransformMatrix(keepPosition, newOrientation); -The optional `referenceFrame` argument on `ICamera::manipulate(...)` is a rigid-frame manipulation anchor. +camera->manipulate({}, &referenceFrame); +``` -At runtime the shared pattern is: +`Free` applies these requests exactly. -```text -referenceFrame - -> CGimbal::extractReferenceTransform(...) - -> resolve the nearest legal camera state for this camera kind - -> accumulate virtual events - -> apply camera-local deltas in that state space - -> rebuild pose +`FPS` keeps the exact position but legalizes orientation to its upright `pitch/yaw` state. + +Do not describe this path as exact position-only or exact orientation-only for constrained target-relative or path cameras. Those cameras legalize the rigid pose request into their own family state. + +### 3. Set one absolute typed state + +Use this when the state should survive beyond one frame or should be reused by presets, follow, playback, persistence, or scripts. + +```cpp +core::CCameraGoal goal = {}; +goal.position = desiredPosition; +goal.orientation = desiredOrientation; + +core::CCameraGoalSolver solver; +auto apply = solver.applyDetailed(camera.get(), goal); ``` -The seam is stable across all camera kinds. The legal-state projection remains camera-specific. +Rule of thumb: -## Input layer +- use `referenceFrame` for one runtime rigid pose request now +- use `CCameraGoal` for one typed camera state that should be stored, compared, serialized, replayed, or applied later -The input layer is split into three parts. +### 4. Set one absolute camera-family state -### Binding layout +Use this when you do not want a generic rigid pose and instead want to write the native state of one camera family. -- [`IGimbalBindingLayout.hpp`](IGimbalBindingLayout.hpp) +Target-relative cameras: + +```cpp +camera->trySetSphericalTarget(targetPosition); +camera->trySetSphericalDistance(distance); +``` + +Path camera: -This layer stores static mappings from physical inputs to semantic virtual events. -It does not process runtime input. +```cpp +core::ICamera::PathState path = { + .s = desiredS, + .u = desiredU, + .v = desiredV, + .roll = desiredRoll +}; + +camera->trySetPathState(path); +``` + +Use this path when you already have: -### Input processing +- target-relative state +- path-rig state +- one other family-specific typed fragment exposed by `ICamera` +### 5. Live runtime camera control + +Use this when keyboard, mouse, or ImGuizmo should move the camera right now. + +```cpp +auto camera = core::make_smart_refctd_ptr(eye, target); + +ui::CGimbalInputBinder binder; +ui::CCameraInputBindingUtilities::applyDefaultCameraInputBindingPreset(binder, *camera); + +auto collected = binder.collectVirtualEvents(timestamp, { + .mouseEvents = { mouseEvents.data(), mouseEvents.size() }, + .keyboardEvents = { keyEvents.data(), keyEvents.size() } +}); + +camera->manipulate(collected.events); +``` + +What happens here: + +1. device input is converted into semantic camera commands +2. the camera consumes those commands through `manipulate(...)` +3. the camera updates its gimbal pose + +Main types involved: + +- [`CVirtualGimbalEvent.hpp`](CVirtualGimbalEvent.hpp) - [`IGimbalInputProcessor.hpp`](IGimbalInputProcessor.hpp) - [`CGimbalInputBinder.hpp`](CGimbalInputBinder.hpp) +- [`ICamera.hpp`](ICamera.hpp) -`IGimbalInputProcessor` converts keyboard, mouse, and ImGuizmo input into virtual events. -`CGimbalInputBinder` is the convenience runtime wrapper that collects one frame of virtual events together with per-domain counts. +### 6. Capture a camera and restore it later -### Shared presets +Use this when you want explicit camera state instead of one-frame runtime input. -- [`CCameraInputBindingUtilities.hpp`](CCameraInputBindingUtilities.hpp) -- [`CCameraScriptedUiInputUtilities.hpp`](CCameraScriptedUiInputUtilities.hpp) +```cpp +core::CCameraGoalSolver solver; -These helpers provide shared default input presets for camera kinds and the scripted/UI-facing glue that reuses the same semantic event language. +auto capture = solver.captureDetailed(camera.get()); +if (capture.canUseGoal()) +{ + auto apply = solver.applyDetailed(camera.get(), capture.goal); +} +``` -## Core camera interface +What happens here: -The main runtime interface is [`ICamera.hpp`](ICamera.hpp). +1. the solver reads runtime camera state +2. the solver writes that state into one `CCameraGoal` +3. the solver later applies that goal back to a camera -Shared properties: +Main types involved: -- runtime entry point is `manipulate(std::span, const hlsl::float64_t4x4*)` -- cameras own their own `CGimbal` -- motion scaling stays camera-local through `SMotionConfig` -- typed state hooks are optional and exist for tooling, not for direct runtime driving +- [`CCameraGoal.hpp`](CCameraGoal.hpp) +- [`CCameraGoalSolver.hpp`](CCameraGoalSolver.hpp) -`ICamera` currently exposes these shared typed states: +### 7. Save a named camera state -- `SphericalTargetState` -- `DynamicPerspectiveState` -- `PathState` -- `PathStateLimits` +Use this when one camera state needs a user-facing name or identifier. -The capability and state-mask surface is: +```cpp +core::CCameraGoalSolver solver; -- `CameraKind` -- `CameraCapability` -- `GoalStateMask` +auto capture = solver.captureDetailed(camera.get()); +if (capture.canUseGoal()) +{ + core::CCameraPreset preset; + preset.name = "Overview"; + preset.identifier = "overview"; + core::CCameraPresetUtilities::assignGoalToPreset(preset, capture.goal); +} +``` -## Camera families +Main types involved: -### Free cameras +- [`CCameraPreset.hpp`](CCameraPreset.hpp) +- [`CCameraPresetFlow.hpp`](CCameraPresetFlow.hpp) -- [`CFPSCamera.hpp`](CFPSCamera.hpp) -- [`CFreeLockCamera.hpp`](CFreeLockCamera.hpp) +### 8. Make a camera follow a moving target -These are pose-driven cameras without spherical target semantics. -They consume `referenceFrame` as a rigid pose anchor directly. +Use this when one tracked subject should drive camera behavior. -### Spherical-target family +```cpp +core::CTrackedTarget trackedTarget(position, orientation); -- [`CSphericalTargetCamera.hpp`](CSphericalTargetCamera.hpp) -- [`COrbitCamera.hpp`](COrbitCamera.hpp) -- [`CArcballCamera.hpp`](CArcballCamera.hpp) -- [`CTurntableCamera.hpp`](CTurntableCamera.hpp) -- [`CTopDownCamera.hpp`](CTopDownCamera.hpp) -- [`CIsometricCamera.hpp`](CIsometricCamera.hpp) -- [`CChaseCamera.hpp`](CChaseCamera.hpp) -- [`CDollyCamera.hpp`](CDollyCamera.hpp) +core::SCameraFollowConfig follow = {}; +follow.enabled = true; +follow.mode = core::ECameraFollowMode::LookAtTarget; -These cameras share: +core::CCameraGoalSolver solver; +core::CCameraFollowUtilities::applyFollowToCamera(solver, camera.get(), trackedTarget, follow); +``` -- target position -- distance -- orbit angles in `orbitUv` +Main types involved: -They also share the same reference-frame rule: +- [`CCameraFollowUtilities.hpp`](CCameraFollowUtilities.hpp) +- [`CCameraFollowRegressionUtilities.hpp`](CCameraFollowRegressionUtilities.hpp) -- extract one rigid reference transform -- resolve a legal target-relative state against the current target -- apply virtual-event deltas on top of that resolved state +### 9. Build scripted runtime payloads from compact authored data -### Extended-state cameras +Use this when camera playback is authored as sequence data and then expanded into per-frame runtime actions and checks. -- [`CDollyZoomCamera.hpp`](CDollyZoomCamera.hpp) -- [`CPathCamera.hpp`](CPathCamera.hpp) +```cpp +system::CCameraScriptedTimeline timeline; + +system::CCameraSequenceScriptedBuilderUtilities::appendCompiledSequenceSegmentToScriptedTimeline( + timeline, + baseFrame, + compiledSegment, + buildInfo); -`DollyZoom` adds dynamic perspective state. -`Path Rig` adds typed path-rig state and a path model. +system::CCameraScriptedRuntimeUtilities::finalizeScriptedTimeline(timeline); +``` -`DollyZoom` resolves reference frames through target-relative state plus dynamic perspective state. -`Path Rig` resolves reference frames through its typed path-model callbacks. +Main types involved: -## Path Rig design +- [`CCameraSequenceScript.hpp`](CCameraSequenceScript.hpp) +- [`CCameraSequenceScriptedBuilder.hpp`](CCameraSequenceScriptedBuilder.hpp) +- [`CCameraScriptedRuntime.hpp`](CCameraScriptedRuntime.hpp) +- [`CCameraScriptedCheckRunner.hpp`](CCameraScriptedCheckRunner.hpp) -`Path Rig` is described explicitly by typed state, typed limits, and a path model. +## Mental model -### Typed path state +The stack uses two complementary representations of camera behavior. -`ICamera::PathState` stores: +### Runtime representation -- `s` - path progress / angular progress in the default model -- `u` - lateral / radial component in the default model -- `v` - vertical component -- `roll` - roll around the view axis / path tangent +The runtime representation answers: -The default shared target-relative model interprets `s/u/v/roll` as a target-relative path rig, but custom models may reuse the same coordinates differently. +> what commands are applied to the camera during this frame -### Typed path limits +It is event-driven. -`ICamera::PathStateLimits` stores per-instance limits: +Core types: -- `minU` -- `minDistance` -- `maxDistance` +- `CVirtualGimbalEvent` +- `IGimbal` +- `ICamera` -Those limits are part of the active camera state surface. -They are queried through `tryGetPathStateLimits(...)` and used by the solver, validation, and path-state sanitization. +This representation is used by: -### Shared path helpers +- keyboard input +- mouse input +- ImGuizmo +- replay helpers that need to mimic runtime movement -- [`CCameraPathMetadata.hpp`](CCameraPathMetadata.hpp) -- [`CCameraPathUtilities.hpp`](CCameraPathUtilities.hpp) +### Typed representation -`CCameraPathUtilities` provides: +The typed representation answers: -- default path metadata and identifiers -- path-state sanitization and comparison -- position-to-state and state-to-pose conversion -- distance updates -- delta and transition helpers -- the default `Path Rig` model +> what explicit camera state should be captured, stored, restored, compared, blended, or validated -### Path model seam +It is state-driven. -The path-model interface is [`SCameraPathModel`](CCameraPathUtilities.hpp). -`CPathCamera` and path utilities call this typed callback bundle to resolve, integrate, and evaluate path state. +Core types: -It contains five callbacks: +- `SCameraRigPose` +- `CCameraGoal` +- `CCameraPreset` +- `CCameraKeyframeTrack` +- `CCameraSequenceScript` -- `resolveState` - normalize requested state or derive initial state from target + position -- `controlLaw` - turn accumulated runtime motion into a `SCameraPathDelta` -- `integrate` - apply one delta to the current state under typed limits -- `evaluate` - convert path state into canonical pose plus target-relative data -- `updateDistance` - retarget state to a requested spherical distance +This representation is used by: -The default model is created by `CCameraPathUtilities::makeDefaultPathModel()`. -That same model is also responsible for resolving `referenceFrame` into legal typed path state. +- capture +- restore +- persistence +- presets +- playback authoring +- follow +- scripted validation -### Runtime behavior of `CPathCamera` +### Bridge between both representations -[`CPathCamera.hpp`](CPathCamera.hpp) uses the same public runtime entry point as the other camera kinds: +`CCameraGoalSolver` is the main bridge. -```text -virtual events -> gimbal accumulation -> path controlLaw -> integrate -> evaluate -> gimbal pose -``` +It converts: -`CPathCamera` owns: +- runtime camera state into typed state +- typed state back into runtime camera behavior -- one active `SCameraPathModel` -- one active `PathState` -- one active `PathStateLimits` +## Core concepts -and exposes: +### `CVirtualGimbalEvent` -- `getPathModel()` -- `getPathStateLimits()` -- `setPathModel(...)` -- `setPathStateLimits(...)` -- `tryGetPathState(...)` -- `trySetPathState(...)` +File: -Construction handles three cases: +- [`CVirtualGimbalEvent.hpp`](CVirtualGimbalEvent.hpp) -- a complete custom model is accepted directly -- an incomplete model falls back to the shared default model -- invalid custom limits are sanitized or replaced with shared defaults +`CVirtualGimbalEvent` is one semantic camera command plus one scalar magnitude. -`Path Rig` therefore uses `ICamera::manipulate(...)` like the other camera kinds while changing only the path-state callbacks behind it. +Examples: -## Goals, presets, tracks, and playback +- `MoveForward` +- `MoveLeft` +- `MoveUp` +- `TiltUp` +- `PanRight` +- `RollLeft` +- `ScaleZInc` -The tooling side of the stack is built around a shared canonical camera state. +The event does not store device-specific origin. +The same event type can come from keyboard input, mouse input, ImGuizmo, scripted playback, or replay helpers. -### Goal layer +### `IGimbal` + +Files: + +- [`IGimbal.hpp`](IGimbal.hpp) +- [`ICamera.hpp`](ICamera.hpp) + +The gimbal stores runtime pose: + +- position +- orientation +- scale +- orthonormal basis + +It also accumulates one frame of semantic events into a `VirtualImpulse`. + +`ICamera::CGimbal` extends the base gimbal with a cached world-to-view matrix. + +Every runtime camera owns one `CGimbal`. + +### `ICamera` + +File: + +- [`ICamera.hpp`](ICamera.hpp) + +`ICamera` is the shared runtime interface implemented by every camera kind. + +Its main job is: + +- consume one frame of semantic virtual events +- optionally consume one rigid reference frame +- update internal camera state +- update runtime pose in the gimbal + +Important members: + +- `manipulate(...)` +- `getGimbal()` +- `getAllowedVirtualEvents()` +- `getKind()` +- `getCapabilities()` +- typed hooks such as `tryGetSphericalTargetState(...)` and `tryGetPathState(...)` + +### `referenceFrame` + +Files: + +- [`ICamera.hpp`](ICamera.hpp) +- [`IGimbal.hpp`](IGimbal.hpp) + +`referenceFrame` is the optional rigid transform passed to `ICamera::manipulate(...)`. + +It is the runtime pose anchor for one manipulation step. + +Typical producers: + +- ImGuizmo +- restore helpers +- replay helpers +- code that wants world-space or local-space manipulation anchored to a specific rigid transform + +When you already have one absolute rigid pose, `referenceFrame` is the direct runtime entry point for requesting that pose through the runtime camera path. + +See Quick start sections 1 and 2 for the concrete absolute-pose usage patterns. + +### `SCameraRigPose` + +File: + +- [`SCameraRigPose.hpp`](SCameraRigPose.hpp) + +`SCameraRigPose` stores only: + +- world-space position +- world-space orientation + +It is the smallest typed pose object reused across the stack. + +### `CCameraGoal` + +File: - [`CCameraGoal.hpp`](CCameraGoal.hpp) -- [`CCameraGoalSolver.hpp`](CCameraGoalSolver.hpp) -- [`CCameraGoalAnalysis.hpp`](CCameraGoalAnalysis.hpp) -`CCameraGoal` is the canonical typed transport object. -It extends [`SCameraRigPose.hpp`](SCameraRigPose.hpp) with optional state such as: +`CCameraGoal` is the canonical typed transport for camera state. + +You can think of it as: + +> one explicit camera-state snapshot used by higher-level tools + +It may contain: +- pose - target position +- target-relative distance - orbit state - path state - dynamic perspective state +- source camera metadata -`CCameraGoalSolver` is the bridge between typed state and the event-driven runtime: +It is used by: -- capture camera state into a goal -- analyze compatibility -- apply what can be applied through typed hooks -- replay virtual events when needed +- capture +- restore +- preset flow +- playback +- follow +- scripted checks -This layer converts between typed goal state and the event-driven runtime surface. +When you want to set a camera absolutely in a reusable, serializable, or comparable way, `CCameraGoal` is the main public state object for that job. -### Presets and preset flow +It is not: -- [`CCameraPreset.hpp`](CCameraPreset.hpp) -- [`CCameraPresetFlow.hpp`](CCameraPresetFlow.hpp) -- [`CCameraPresetPersistence.hpp`](CCameraPresetPersistence.hpp) +- a live input object +- a replacement for `manipulate(...)` +- a promise that every camera can represent every arbitrary pose exactly -Presets are named `CCameraGoal` wrappers. -`CCameraPresetFlowUtilities` provides the high-level capture/apply helpers used by camera-stack consumers. +For constrained cameras, the solver may project the goal onto legal camera-family state before or during apply. -### Keyframe tracks and playback +### `CCameraGoalSolver` -- [`CCameraKeyframeTrack.hpp`](CCameraKeyframeTrack.hpp) -- [`CCameraKeyframeTrackPersistence.hpp`](CCameraKeyframeTrackPersistence.hpp) -- [`CCameraPlaybackTimeline.hpp`](CCameraPlaybackTimeline.hpp) +File: -Keyframe tracks are preset-based reusable playback data. -`CCameraPlaybackTimeline` owns only transport-like playback cursor logic. -Higher-level playback policy remains on the consumer side. +- [`CCameraGoalSolver.hpp`](CCameraGoalSolver.hpp) -### Persistence and file helpers +`CCameraGoalSolver` converts between typed camera state and runtime cameras. -- [`CCameraPersistence.hpp`](CCameraPersistence.hpp) -- [`CCameraFileUtilities.hpp`](CCameraFileUtilities.hpp) +It performs: -These helpers cover camera presets, tracks, and related shared persistence tasks. +1. capture runtime camera into `CCameraGoal` +2. analyze compatibility between goal and target camera +3. apply typed fragments directly when the target camera supports them +4. replay runtime movement when direct typed apply is not sufficient -## Follow layer +If you want to restore one absolute camera state and you are not sure which family-specific hook to call, use `CCameraGoalSolver`. -Follow is deliberately not baked into `ICamera`. +### `CCameraPreset` -### Tracked target +File: -- [`CCameraFollowUtilities.hpp`](CCameraFollowUtilities.hpp) +- [`CCameraPreset.hpp`](CCameraPreset.hpp) -The tracked subject is `CTrackedTarget`. -It owns its own `ICamera::CGimbal`. +`CCameraPreset` is a named saved `CCameraGoal`. -Follow uses: +It contains: -- tracked-target pose -- follow mode -- follow config +- `name` +- `identifier` +- `goal` -It does not use a scene-node id or mesh handle. +### `CCameraKeyframeTrack` -### Follow modes +File: -Reusable modes: +- [`CCameraKeyframeTrack.hpp`](CCameraKeyframeTrack.hpp) -- `OrbitTarget` -- `LookAtTarget` -- `KeepWorldOffset` -- `KeepLocalOffset` +`CCameraKeyframeTrack` is a sequence of time-stamped presets. + +Each keyframe contains: + +- one preset +- one authored time + +### `CCameraPlaybackTimeline` + +File: + +- [`CCameraPlaybackTimeline.hpp`](CCameraPlaybackTimeline.hpp) + +`CCameraPlaybackTimeline` stores playback cursor state over time-based camera data. + +It tracks things such as: -### Follow application and validation +- current time +- direction +- looping +- paused or playing state + +### `CTrackedTarget` + +File: - [`CCameraFollowUtilities.hpp`](CCameraFollowUtilities.hpp) -- [`CCameraFollowRegressionUtilities.hpp`](CCameraFollowRegressionUtilities.hpp) -`CCameraFollowUtilities` builds goal state from tracked target + policy and applies it through `CCameraGoalSolver`. -`CCameraFollowRegressionUtilities` validates lock angle, projected target placement, distance consistency, and spherical writeback behavior. +`CTrackedTarget` is the reusable tracked subject used by follow. -## Scripted authoring and validation +It owns its own gimbal. +It is not a mesh id and not a scene-node handle. -The scripting side is split into compact authored data and expanded runtime payloads. +### `CCameraSequenceScript` -### Compact authored sequence +File: - [`CCameraSequenceScript.hpp`](CCameraSequenceScript.hpp) -- [`CCameraSequenceScriptPersistence.hpp`](CCameraSequenceScriptPersistence.hpp) -This layer stores the authored sequence description. -It stores: +`CCameraSequenceScript` is the compact authored format for camera sequences. + +It stores camera-domain data such as: -- camera kind or identifier +- targeted camera - projection presentation requests -- compact goal keyframes -- compact tracked-target keyframes -- continuity thresholds +- camera keyframes +- tracked-target keyframes +- continuity settings - capture fractions -It stores compact keyframes and presentation requests. It does not store frame-by-frame low-level event dumps. +It does not store frame-by-frame low-level input. + +### `CCameraScriptedRuntime` -### Expanded runtime payload +File: - [`CCameraScriptedRuntime.hpp`](CCameraScriptedRuntime.hpp) -- [`CCameraScriptedRuntimePersistence.hpp`](CCameraScriptedRuntimePersistence.hpp) -This layer stores: +`CCameraScriptedRuntime` is the expanded executable form used during scripted playback and validation. + +It stores runtime payloads such as: -- low-level scripted input events +- low-level input events +- action events - per-frame checks -- capture frame scheduling -- optional compact sequence payload parsed alongside the low-level runtime payload +- capture scheduling -### Sequence expansion +### `Path Rig` -- [`CCameraSequenceScriptedBuilder.hpp`](CCameraSequenceScriptedBuilder.hpp) +Files: -This converts one compiled sequence segment into frame-by-frame scripted runtime payloads. +- [`CPathCamera.hpp`](CPathCamera.hpp) +- [`CCameraPathUtilities.hpp`](CCameraPathUtilities.hpp) +- [`CCameraPathMetadata.hpp`](CCameraPathMetadata.hpp) -### Frame check execution +`Path Rig` is the camera family with typed state: -- [`CCameraScriptedCheckRunner.hpp`](CCameraScriptedCheckRunner.hpp) -- [`CCameraSmokeRegressionUtilities.hpp`](CCameraSmokeRegressionUtilities.hpp) +- `s` +- `u` +- `v` +- `roll` -This layer evaluates authored per-frame checks against the active runtime state. -It is used by shared smoke and continuity coverage in `61_UI`. +Its runtime and typed tooling are driven by `SCameraPathModel`. -## Projection layer +Model callbacks: -Projection state is a separate reusable layer. +- `resolveState` +- `controlLaw` +- `integrate` +- `evaluate` +- `updateDistance` -- [`IProjection.hpp`](IProjection.hpp) -- [`ILinearProjection.hpp`](ILinearProjection.hpp) -- [`IPerspectiveProjection.hpp`](IPerspectiveProjection.hpp) -- [`IPlanarProjection.hpp`](IPlanarProjection.hpp) -- [`CLinearProjection.hpp`](CLinearProjection.hpp) -- [`CPlanarProjection.hpp`](CPlanarProjection.hpp) -- [`CCubeProjection.hpp`](CCubeProjection.hpp) -- [`IRange.hpp`](IRange.hpp) +## Runtime behavior -Projection-layer rule: +### Input flow -- projections may own viewport-local binding layouts -- projections do not process raw runtime input +Runtime input flow is: -Projection-local viewport glue stays in the projection layer. Reusable camera semantics stay in the camera helpers. +```text +physical input + -> binding layout + -> input processor or binder + -> CVirtualGimbalEvent[] + -> ICamera::manipulate(...) + -> updated gimbal + -> updated view matrix +``` -## Presentation and UI-facing helpers +Main files: -The shared layer also exposes small reusable helpers used by consumers such as `61_UI`: +- [`IGimbalBindingLayout.hpp`](IGimbalBindingLayout.hpp) +- [`IGimbalInputProcessor.hpp`](IGimbalInputProcessor.hpp) +- [`CGimbalInputBinder.hpp`](CGimbalInputBinder.hpp) +- [`CCameraInputBindingUtilities.hpp`](CCameraInputBindingUtilities.hpp) -- [`CCameraPresentationUtilities.hpp`](CCameraPresentationUtilities.hpp) -- [`CCameraProjectionUtilities.hpp`](CCameraProjectionUtilities.hpp) -- [`CCameraTextUtilities.hpp`](CCameraTextUtilities.hpp) -- [`CCameraViewportOverlayUtilities.hpp`](CCameraViewportOverlayUtilities.hpp) -- [`CCameraControlPanelUiUtilities.hpp`](CCameraControlPanelUiUtilities.hpp) -- [`CCameraScriptVisualDebugOverlayUtilities.hpp`](CCameraScriptVisualDebugOverlayUtilities.hpp) +### `manipulate(...)` -These are still shared camera helpers. -They are presentation-facing helpers built on shared camera-domain data. They are not `61_UI`-local copies. +`ICamera::manipulate(...)` is the shared runtime entry point for every camera kind. -## 61_UI integration +Its responsibilities are: -`61_UI` is the full runnable integration for this stack: +- consume one frame of semantic virtual events +- optionally consume one rigid reference frame +- update camera-family state +- update the runtime gimbal pose -- [`61_UI/README.md`](../../../61_UI/README.md) +It is used by: -`61_UI` provides: +- live input +- ImGuizmo +- replay helpers +- code that wants one frame of camera movement -- scene setup -- active planar / window routing -- ImGui control panel -- tracked-target visualization -- scripted smoke and continuity assets -- screenshot capture -- local logging +### Motion scales -`61_UI` exercises the shared camera layer in one runnable scene. +`ICamera` stores per-camera motion scales in `SMotionConfig`. -## Local configure, build, and test +Those scales are applied on top of shared runtime units defined in [`CCameraTraits.hpp`](CCameraTraits.hpp). -Current local setup uses the Visual Studio 2022 dynamic preset. +### Typed hooks -Configure: +Some runtime cameras expose typed state directly. -```powershell -cmake --preset user-configure-dynamic-msvc -``` +Examples: + +- `tryGetSphericalTargetState(...)` +- `trySetSphericalTarget(...)` +- `tryGetDynamicPerspectiveState(...)` +- `tryGetPathState(...)` -Build `61_UI`: +The capability mask and goal-state mask report which typed fragments are valid for a given camera. -```powershell -cmake --build build/dynamic/examples_tests/61_UI --config Debug --target 61_ui -- /m -``` +## `referenceFrame` + +`referenceFrame` is the optional rigid transform passed into `ICamera::manipulate(...)`. -Run the camera-focused tests: +Shared runtime pattern: -```powershell -ctest --test-dir build/dynamic/examples_tests/61_UI -C Debug --output-on-failure -R NBL_61_UI_CAMERA_ +```text +referenceFrame + -> extract rigid reference transform + -> resolve legal state for this camera kind + -> accumulate virtual events + -> apply deltas in that state space + -> rebuild pose ``` -Those smoke and continuity tests cover: +This seam is supported across all current camera kinds. -- all runtime camera kinds -- typed goal capture and replay -- follow behavior -- `Path Rig` typed-state manipulation -- `referenceFrame` application across rigid, target-relative, and path-model cameras +Per camera family: -Run the example manually: +- `Free` and `FPS` + use the extracted rigid reference as the runtime pose anchor, with `FPS` legalizing orientation to upright `pitch/yaw` +- target-relative cameras + resolve target-relative state from the rigid reference while preserving target-relative constraints +- `Path Rig` + resolves typed path state through the active path model -```powershell -examples_tests/61_UI/bin/61_ui_d.exe -``` +## Camera families -Run CI-style screenshot capture: +### Free cameras -```powershell -examples_tests/61_UI/bin/61_ui_d.exe --ci -``` +Files: -## Minimal integration examples +- [`CFPSCamera.hpp`](CFPSCamera.hpp) +- [`CFreeLockCamera.hpp`](CFreeLockCamera.hpp) -### Runtime input to camera +State: -```cpp -auto camera = core::make_smart_refctd_ptr(eye, target); +- world-space position +- orientation or FPS-constrained yaw/pitch orientation -ui::CGimbalInputBinder binder; -ui::CCameraInputBindingUtilities::applyDefaultCameraInputBindingPreset(binder, *camera); +Typical use: -auto collected = binder.collectVirtualEvents(timestamp, { - .mouseEvents = { mouseEvents.data(), mouseEvents.size() }, - .keyboardEvents = { keyEvents.data(), keyEvents.size() } -}); +- free-fly navigation +- direct pose-driven manipulation -camera->manipulate(collected.events); -``` +### Target-relative cameras -### Capture and apply a preset +Base: -```cpp -core::CCameraGoalSolver solver; +- [`CSphericalTargetCamera.hpp`](CSphericalTargetCamera.hpp) -auto capture = solver.captureDetailed(camera.get()); -if (capture.canUseGoal()) -{ - core::CCameraPreset preset; - core::CCameraPresetUtilities::assignGoalToPreset(preset, capture.goal); +Derived: - auto apply = core::CCameraPresetFlowUtilities::applyPresetDetailed(solver, camera.get(), preset); - if (!apply.succeeded()) - { - // report unsupported or approximate apply - } -} -``` +- [`COrbitCamera.hpp`](COrbitCamera.hpp) +- [`CArcballCamera.hpp`](CArcballCamera.hpp) +- [`CTurntableCamera.hpp`](CTurntableCamera.hpp) +- [`CTopDownCamera.hpp`](CTopDownCamera.hpp) +- [`CIsometricCamera.hpp`](CIsometricCamera.hpp) +- [`CChaseCamera.hpp`](CChaseCamera.hpp) +- [`CDollyCamera.hpp`](CDollyCamera.hpp) +- [`CDollyZoomCamera.hpp`](CDollyZoomCamera.hpp) -### Apply follow +Shared state: -```cpp -core::CTrackedTarget trackedTarget(position, orientation); +- target position +- `orbitUv` +- distance -core::SCameraFollowConfig follow = {}; -follow.enabled = true; -follow.mode = core::ECameraFollowMode::KeepLocalOffset; -follow.localOffset = hlsl::float64_t3(-4.0, 0.0, 1.0); +These cameras resolve pose through target-relative state instead of arbitrary free pose. -auto result = core::CCameraFollowUtilities::applyFollowToCamera(solver, camera.get(), trackedTarget, follow); -``` +### DollyZoom -### Expand a compact sequence into runtime payloads +File: -```cpp -system::CCameraScriptedTimeline timeline; +- [`CDollyZoomCamera.hpp`](CDollyZoomCamera.hpp) -system::CCameraSequenceScriptedBuilderUtilities::appendCompiledSequenceSegmentToScriptedTimeline( - timeline, - baseFrame, - compiledSegment, - buildInfo); +This camera adds dynamic perspective state on top of target-relative state. -system::CCameraScriptedRuntimeUtilities::finalizeScriptedTimeline(timeline); -``` +Typed dynamic perspective state: + +- `baseFov` +- `referenceDistance` + +### Path Rig + +Files: + +- [`CPathCamera.hpp`](CPathCamera.hpp) +- [`CCameraPathUtilities.hpp`](CCameraPathUtilities.hpp) +- [`CCameraPathMetadata.hpp`](CCameraPathMetadata.hpp) + +Typed path state: + +- `s` +- `u` +- `v` +- `roll` + +Typed path limits: + +- `minU` +- `minDistance` +- `maxDistance` + +## Typed tooling + +The key typed types are introduced in the `Core concepts` section above. + +This section focuses on how they fit together in one workflow: + +1. `SCameraRigPose` is the smallest typed pose fragment. +2. `CCameraGoal` is the canonical typed state transport built on top of pose and optional family-specific fragments. +3. `CCameraGoalSolver` captures runtime cameras into goals and applies goals back to runtime cameras. +4. `CCameraPreset` gives one goal a stable user-facing identity. +5. `CCameraKeyframeTrack` stores presets over authored time. +6. `CCameraPlaybackTimeline` stores playback cursor state while a track is being evaluated. -## Summary +Use this layer when camera state must outlive the current frame or be exchanged between tools. -The current stack is organized as: +## Follow + +Files: + +- [`CCameraFollowUtilities.hpp`](CCameraFollowUtilities.hpp) +- [`CCameraFollowRegressionUtilities.hpp`](CCameraFollowRegressionUtilities.hpp) + +Follow is built from: + +- one tracked target +- one follow mode +- one follow configuration + +Tracked target type: + +- `CTrackedTarget` + +Follow modes: + +- `OrbitTarget` +- `LookAtTarget` +- `KeepWorldOffset` +- `KeepLocalOffset` + +`CCameraFollowUtilities` reads tracked-target pose, builds resulting camera goal state, and applies it through the shared goal solver. + +## Scripting + +### Compact authored format + +Files: + +- [`CCameraSequenceScript.hpp`](CCameraSequenceScript.hpp) +- [`CCameraSequenceScriptPersistence.hpp`](CCameraSequenceScriptPersistence.hpp) + +This layer stores authored camera-domain data. + +### Expanded runtime format + +Files: + +- [`CCameraScriptedRuntime.hpp`](CCameraScriptedRuntime.hpp) +- [`CCameraScriptedRuntimePersistence.hpp`](CCameraScriptedRuntimePersistence.hpp) +- [`CCameraSequenceScriptedBuilder.hpp`](CCameraSequenceScriptedBuilder.hpp) +- [`CCameraScriptedCheckRunner.hpp`](CCameraScriptedCheckRunner.hpp) + +This layer stores executable per-frame runtime payloads and validation checks. + +Common flow: ```text -input layer - -> semantic virtual events - -> runtime camera models - -> typed goal / preset / track tooling - -> compact sequence authoring - -> expanded scripted runtime - -> shared validation +compact authored sequence + -> compile or expand + -> scripted runtime payload + -> execute against runtime camera state ``` -This split yields: +## Projection and presentation helpers + +Projection layer: -- one event-driven runtime path -- one typed tooling path -- one shared scripting and validation layer -- one runnable example that consumes those shared pieces +- [`IProjection.hpp`](IProjection.hpp) +- [`ILinearProjection.hpp`](ILinearProjection.hpp) +- [`IPerspectiveProjection.hpp`](IPerspectiveProjection.hpp) +- [`IPlanarProjection.hpp`](IPlanarProjection.hpp) +- [`CLinearProjection.hpp`](CLinearProjection.hpp) +- [`CPlanarProjection.hpp`](CPlanarProjection.hpp) +- [`CCubeProjection.hpp`](CCubeProjection.hpp) + +Camera-facing presentation helpers: + +- [`CCameraPresentationUtilities.hpp`](CCameraPresentationUtilities.hpp) +- [`CCameraProjectionUtilities.hpp`](CCameraProjectionUtilities.hpp) +- [`CCameraTextUtilities.hpp`](CCameraTextUtilities.hpp) +- [`CCameraViewportOverlayUtilities.hpp`](CCameraViewportOverlayUtilities.hpp) +- [`CCameraControlPanelUiUtilities.hpp`](CCameraControlPanelUiUtilities.hpp) +- [`CCameraScriptVisualDebugOverlayUtilities.hpp`](CCameraScriptVisualDebugOverlayUtilities.hpp) From 3d09c6822b94146567f618db6ba579af3d12d078 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Thu, 9 Apr 2026 19:49:52 +0200 Subject: [PATCH 198/205] Finalize camera API input semantics --- 61_UI/AppHeadlessCameraSmokeChecks.inl | 95 +++++++ 61_UI/include/app/AppTypes.hpp | 4 +- common/include/camera/CCameraGoalSolver.hpp | 4 +- .../camera/CCameraInputBindingUtilities.hpp | 109 +++++-- common/include/camera/CCameraTraits.hpp | 11 - common/include/camera/CGimbalInputBinder.hpp | 2 +- common/include/camera/CVirtualGimbalEvent.hpp | 10 +- common/include/camera/ICamera.hpp | 21 +- .../include/camera/IGimbalBindingLayout.hpp | 13 +- .../include/camera/IGimbalInputProcessor.hpp | 31 +- common/include/camera/README.md | 265 +++++++----------- 11 files changed, 343 insertions(+), 222 deletions(-) diff --git a/61_UI/AppHeadlessCameraSmokeChecks.inl b/61_UI/AppHeadlessCameraSmokeChecks.inl index 432900d5f..beb2a91e6 100644 --- a/61_UI/AppHeadlessCameraSmokeChecks.inl +++ b/61_UI/AppHeadlessCameraSmokeChecks.inl @@ -1290,6 +1290,101 @@ } } + { + 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.initialPresets.free.has_value() && state.freeCamera) { CameraPreset orientedPreset = state.initialPresets.free.value(); diff --git a/61_UI/include/app/AppTypes.hpp b/61_UI/include/app/AppTypes.hpp index b9f185f98..0a29ca245 100644 --- a/61_UI/include/app/AppTypes.hpp +++ b/61_UI/include/app/AppTypes.hpp @@ -178,8 +178,8 @@ struct SCameraAppInputDefaults final struct SCameraAppCameraFactoryDefaults final { - static inline constexpr double DefaultMoveScale = nbl::core::SCameraRuntimeTraits::DefaultMoveSpeedScale; - static inline constexpr double DefaultRotateScale = nbl::core::SCameraRuntimeTraits::DefaultRotationSpeedScale; + static inline constexpr double DefaultMoveScale = 0.01; + static inline constexpr double DefaultRotateScale = 0.003; static inline constexpr double TargetRigMoveScale = 0.5; }; diff --git a/common/include/camera/CCameraGoalSolver.hpp b/common/include/camera/CCameraGoalSolver.hpp index f8d65a0db..7132354b0 100644 --- a/common/include/camera/CCameraGoalSolver.hpp +++ b/common/include/camera/CCameraGoalSolver.hpp @@ -436,7 +436,7 @@ class CCameraGoalSolver inline double getMoveMagnitudeDenominator(const ICamera* camera) const { const double moveScale = camera->getMoveSpeedScale(); - return SCameraRuntimeTraits::VirtualTranslationStep * (moveScale == 0.0 ? SGoalSolverDefaults::UnitScale : moveScale); + return camera->getUnscaledVirtualTranslationMagnitude() * (moveScale == 0.0 ? SGoalSolverDefaults::UnitScale : moveScale); } inline double getRotationMagnitudeDenominator(const ICamera* camera) const @@ -517,7 +517,7 @@ class CCameraGoalSolver delta, policy.translateOrbit ? getMoveMagnitudeDenominator(camera) : getRotationMagnitudeDenominator(camera), SCameraToolingThresholds::DefaultAngularToleranceDeg, - SCameraRuntimeTraits::VirtualTranslationStep, + camera->getUnscaledVirtualTranslationMagnitude(), SCameraToolingThresholds::ScalarTolerance, policy); return !out.empty(); diff --git a/common/include/camera/CCameraInputBindingUtilities.hpp b/common/include/camera/CCameraInputBindingUtilities.hpp index 3288d925c..3529925e6 100644 --- a/common/include/camera/CCameraInputBindingUtilities.hpp +++ b/common/include/camera/CCameraInputBindingUtilities.hpp @@ -77,6 +77,22 @@ struct SCameraInputBindingPhysicalGroups final struct CCameraInputBindingUtilities final { public: + /// @brief Default gains used by the shared binding presets. + /// + /// These gains convert producer-local units into virtual-event magnitudes: + /// held keys into units per second, mouse deltas into units per mouse + /// step, scroll into units per scroll step, and ImGuizmo deltas into + /// virtual translation, rotation, or scale magnitudes. + struct SInputMagnitudeDefaults final + { + static inline constexpr double KeyboardHeldUnitsPerSecond = 1000.0; + static inline constexpr double RelativeMouseUnitsPerStep = 1.0; + static inline constexpr double ScrollUnitsPerStep = 1.0; + static inline constexpr double ImguizmoTranslationUnitsPerWorldUnit = 1.0; + static inline constexpr double ImguizmoRotationUnitsPerRadian = 1.0; + static inline constexpr double ImguizmoScaleUnitsPerFactor = 1.0; + }; + static inline bool hasMouseRelativeMovementBinding(const IGimbalBindingLayout::mouse_to_virtual_events_t& mousePreset) { return containsBindingForAnyCodeGroups(mousePreset, SCameraInputBindingPhysicalGroups::RelativeMouseCodes); @@ -164,16 +180,19 @@ struct CCameraInputBindingUtilities final core::CVirtualGimbalEvent::None, core::CVirtualGimbalEvent::None }; + double wasdScale = IGimbalBindingLayout::CHashInfo::DefaultMagnitudeScale; scalar_axis_pair_t qe = { core::CVirtualGimbalEvent::None, core::CVirtualGimbalEvent::None }; + double qeScale = IGimbalBindingLayout::CHashInfo::DefaultMagnitudeScale; keyboard_axis_group_t ijkl = { core::CVirtualGimbalEvent::None, core::CVirtualGimbalEvent::None, core::CVirtualGimbalEvent::None, core::CVirtualGimbalEvent::None }; + double ijklScale = IGimbalBindingLayout::CHashInfo::DefaultMagnitudeScale; }; struct SMousePresetSpec final @@ -184,10 +203,12 @@ struct CCameraInputBindingUtilities final core::CVirtualGimbalEvent::None, core::CVirtualGimbalEvent::None }; + double relativeScale = IGimbalBindingLayout::CHashInfo::DefaultMagnitudeScale; scalar_axis_pair_t scroll = { core::CVirtualGimbalEvent::None, core::CVirtualGimbalEvent::None }; + double scrollScale = IGimbalBindingLayout::CHashInfo::DefaultMagnitudeScale; }; /// @brief Shared virtual-event bundles reused across interaction families. @@ -297,43 +318,43 @@ struct CCameraInputBindingUtilities final } template - static inline void appendBindingSpec(Map& preset, const Codes& codes, const Events& events) + static inline void appendBindingSpec(Map& preset, const Codes& codes, const Events& events, const double magnitudeScale) { for (size_t i = 0u; i < codes.size() && i < events.size(); ++i) { const auto event = events[i]; if (event == core::CVirtualGimbalEvent::None) continue; - preset.emplace(codes[i], IGimbalBindingLayout::CHashInfo(event)); + preset.emplace(codes[i], IGimbalBindingLayout::CHashInfo(event, magnitudeScale)); } } template - static inline void appendMirroredBindingSpec(Map& preset, const Codes& codes, const virtual_event_t event) + static inline void appendMirroredBindingSpec(Map& preset, const Codes& codes, const virtual_event_t event, const double magnitudeScale) { if (event == core::CVirtualGimbalEvent::None) return; std::array> duplicatedEvents = {}; duplicatedEvents.fill(event); - appendBindingSpec(preset, codes, duplicatedEvents); + appendBindingSpec(preset, codes, duplicatedEvents, magnitudeScale); } static inline IGimbalBindingLayout::keyboard_to_virtual_events_t buildKeyboardPreset(const SKeyboardPresetSpec& spec) { IGimbalBindingLayout::keyboard_to_virtual_events_t preset; - appendBindingSpec(preset, SCameraInputBindingPhysicalGroups::KeyboardWasdCodes, spec.wasd); - appendBindingSpec(preset, SCameraInputBindingPhysicalGroups::KeyboardQeCodes, spec.qe); - appendBindingSpec(preset, SCameraInputBindingPhysicalGroups::KeyboardIjklCodes, spec.ijkl); + appendBindingSpec(preset, SCameraInputBindingPhysicalGroups::KeyboardWasdCodes, spec.wasd, spec.wasdScale); + appendBindingSpec(preset, SCameraInputBindingPhysicalGroups::KeyboardQeCodes, spec.qe, spec.qeScale); + appendBindingSpec(preset, SCameraInputBindingPhysicalGroups::KeyboardIjklCodes, spec.ijkl, spec.ijklScale); return preset; } static inline IGimbalBindingLayout::mouse_to_virtual_events_t buildMousePreset(const SMousePresetSpec& spec) { IGimbalBindingLayout::mouse_to_virtual_events_t preset; - appendBindingSpec(preset, SCameraInputBindingPhysicalGroups::RelativeMouseCodes, spec.relative); - appendMirroredBindingSpec(preset, SCameraInputBindingPhysicalGroups::PositiveScrollCodes, spec.scroll[0]); - appendMirroredBindingSpec(preset, SCameraInputBindingPhysicalGroups::NegativeScrollCodes, spec.scroll[1]); + appendBindingSpec(preset, SCameraInputBindingPhysicalGroups::RelativeMouseCodes, spec.relative, spec.relativeScale); + appendMirroredBindingSpec(preset, SCameraInputBindingPhysicalGroups::PositiveScrollCodes, spec.scroll[0], spec.scrollScale); + appendMirroredBindingSpec(preset, SCameraInputBindingPhysicalGroups::NegativeScrollCodes, spec.scroll[1], spec.scrollScale); return preset; } @@ -346,78 +367,120 @@ struct CCameraInputBindingUtilities final continue; if ((allowedVirtualEvents & event) != event) continue; - preset.emplace(event, IGimbalBindingLayout::CHashInfo(event)); + preset.emplace(event, IGimbalBindingLayout::CHashInfo(event, getDefaultImguizmoMagnitudeScale(event))); } return preset; } + static inline double getDefaultImguizmoMagnitudeScale(const virtual_event_t event) + { + if (core::CVirtualGimbalEvent::isTranslationEvent(event)) + return SInputMagnitudeDefaults::ImguizmoTranslationUnitsPerWorldUnit; + if (core::CVirtualGimbalEvent::isRotationEvent(event)) + return SInputMagnitudeDefaults::ImguizmoRotationUnitsPerRadian; + if (core::CVirtualGimbalEvent::isScaleEvent(event)) + return SInputMagnitudeDefaults::ImguizmoScaleUnitsPerFactor; + return IGimbalBindingLayout::CHashInfo::DefaultMagnitudeScale; + } + static inline constexpr SCameraInteractionBindingSpec EmptyInteractionBindingSpec = {}; static inline constexpr SKeyboardPresetSpec FpsKeyboardSpec = { SCameraInputBindingEventGroups::FpsMove, + SInputMagnitudeDefaults::KeyboardHeldUnitsPerSecond, {}, - SCameraInputBindingEventGroups::LookYawPitch + IGimbalBindingLayout::CHashInfo::DefaultMagnitudeScale, + SCameraInputBindingEventGroups::LookYawPitch, + SInputMagnitudeDefaults::KeyboardHeldUnitsPerSecond }; static inline constexpr SKeyboardPresetSpec FreeKeyboardSpec = { FpsKeyboardSpec.wasd, + FpsKeyboardSpec.wasdScale, SCameraInputBindingEventGroups::Roll, - FpsKeyboardSpec.ijkl + SInputMagnitudeDefaults::KeyboardHeldUnitsPerSecond, + FpsKeyboardSpec.ijkl, + FpsKeyboardSpec.ijklScale }; static inline constexpr SKeyboardPresetSpec OrbitKeyboardSpec = { SCameraInputBindingEventGroups::OrbitTranslate, + SInputMagnitudeDefaults::KeyboardHeldUnitsPerSecond, SCameraInputBindingEventGroups::OrbitZoom, - {} + SInputMagnitudeDefaults::KeyboardHeldUnitsPerSecond, + {}, + IGimbalBindingLayout::CHashInfo::DefaultMagnitudeScale }; static inline constexpr SKeyboardPresetSpec TargetRigKeyboardSpec = { FpsKeyboardSpec.wasd, + FpsKeyboardSpec.wasdScale, SCameraInputBindingEventGroups::VerticalMove, - FpsKeyboardSpec.ijkl + SInputMagnitudeDefaults::KeyboardHeldUnitsPerSecond, + FpsKeyboardSpec.ijkl, + FpsKeyboardSpec.ijklScale }; static inline constexpr SKeyboardPresetSpec TurntableKeyboardSpec = { SCameraInputBindingEventGroups::TurntableMove, + SInputMagnitudeDefaults::KeyboardHeldUnitsPerSecond, {}, - FpsKeyboardSpec.ijkl + IGimbalBindingLayout::CHashInfo::DefaultMagnitudeScale, + FpsKeyboardSpec.ijkl, + FpsKeyboardSpec.ijklScale }; static inline constexpr SKeyboardPresetSpec TopDownKeyboardSpec = { OrbitKeyboardSpec.wasd, + OrbitKeyboardSpec.wasdScale, OrbitKeyboardSpec.qe, - SCameraInputBindingEventGroups::PanOnly + OrbitKeyboardSpec.qeScale, + SCameraInputBindingEventGroups::PanOnly, + SInputMagnitudeDefaults::KeyboardHeldUnitsPerSecond }; static inline constexpr SKeyboardPresetSpec PathKeyboardSpec = { SCameraInputBindingEventGroups::PathRigProgressAndU, + SInputMagnitudeDefaults::KeyboardHeldUnitsPerSecond, SCameraInputBindingEventGroups::PathRigV, - {} + SInputMagnitudeDefaults::KeyboardHeldUnitsPerSecond, + {}, + IGimbalBindingLayout::CHashInfo::DefaultMagnitudeScale }; static inline constexpr SMousePresetSpec FpsMouseSpec = { SCameraInputBindingEventGroups::RelativeLook, - {} + SInputMagnitudeDefaults::RelativeMouseUnitsPerStep, + {}, + IGimbalBindingLayout::CHashInfo::DefaultMagnitudeScale }; static inline constexpr SMousePresetSpec OrbitMouseSpec = { SCameraInputBindingEventGroups::RelativeOrbitTranslate, - SCameraInputBindingEventGroups::OrbitZoom + SInputMagnitudeDefaults::RelativeMouseUnitsPerStep, + SCameraInputBindingEventGroups::OrbitZoom, + SInputMagnitudeDefaults::ScrollUnitsPerStep }; static inline constexpr SMousePresetSpec TargetRigMouseSpec = { FpsMouseSpec.relative, - OrbitMouseSpec.scroll + FpsMouseSpec.relativeScale, + OrbitMouseSpec.scroll, + OrbitMouseSpec.scrollScale }; static inline constexpr SMousePresetSpec TopDownMouseSpec = { SCameraInputBindingEventGroups::RelativeTopDown, - OrbitMouseSpec.scroll + SInputMagnitudeDefaults::RelativeMouseUnitsPerStep, + OrbitMouseSpec.scroll, + OrbitMouseSpec.scrollScale }; static inline constexpr SMousePresetSpec PathMouseSpec = { SCameraInputBindingEventGroups::RelativeOrbitTranslate, - SCameraInputBindingEventGroups::OrbitZoom + SInputMagnitudeDefaults::RelativeMouseUnitsPerStep, + SCameraInputBindingEventGroups::OrbitZoom, + SInputMagnitudeDefaults::ScrollUnitsPerStep }; static inline constexpr SCameraInteractionBindingSpec FpsInteractionBindingSpec = { diff --git a/common/include/camera/CCameraTraits.hpp b/common/include/camera/CCameraTraits.hpp index 6cab1a4db..ecc9c2d7a 100644 --- a/common/include/camera/CCameraTraits.hpp +++ b/common/include/camera/CCameraTraits.hpp @@ -10,17 +10,6 @@ namespace nbl::core { -/// @brief Constants used when converting unit virtual events into camera-local motion. -struct SCameraRuntimeTraits final -{ - /// @brief Base world-space translation magnitude represented by a unit virtual move event. - static inline constexpr double VirtualTranslationStep = 0.01; - /// @brief Default per-camera multiplier applied to virtual translation magnitudes. - static inline constexpr double DefaultMoveSpeedScale = VirtualTranslationStep; - /// @brief Default per-camera multiplier applied to virtual rotation magnitudes. - static inline constexpr double DefaultRotationSpeedScale = 0.003; -}; - /// @brief Geometric constants used by target-relative camera families. /// /// `MinDistance` prevents zero-distance target-relative states. diff --git a/common/include/camera/CGimbalInputBinder.hpp b/common/include/camera/CGimbalInputBinder.hpp index 0dc44894f..785bb0269 100644 --- a/common/include/camera/CGimbalInputBinder.hpp +++ b/common/include/camera/CGimbalInputBinder.hpp @@ -121,7 +121,7 @@ class CGimbalInputBinder final : public IGimbalInputProcessor { Map result; for (const auto& [code, hash] : source) - result.emplace(code, typename Map::mapped_type(hash.event.type)); + result.emplace(code, typename Map::mapped_type(hash.event.type, hash.magnitudeScale)); return result; } }; diff --git a/common/include/camera/CVirtualGimbalEvent.hpp b/common/include/camera/CVirtualGimbalEvent.hpp index 37a9ff107..e1384ed42 100644 --- a/common/include/camera/CVirtualGimbalEvent.hpp +++ b/common/include/camera/CVirtualGimbalEvent.hpp @@ -14,8 +14,11 @@ namespace nbl::core /// @brief One semantic camera command passed to `ICamera::manipulate(...)`. /// /// `type` selects the command family. `magnitude` stores the non-negative -/// scalar amount for that command. Input binders, scripted playback, replay -/// helpers, and gizmo-driven tools all use the same event representation. +/// source-normalized scalar amount for that command. Input binders convert +/// raw keyboard, mouse, scroll, and ImGuizmo data into this representation +/// before the camera sees it. Cameras then convert these virtual magnitudes +/// into camera-local motion through their runtime scales and family-specific +/// legalization rules. struct CVirtualGimbalEvent { /// @brief Bitmask identifiers for semantic movement, rotation, and scale commands. @@ -57,6 +60,9 @@ struct CVirtualGimbalEvent /// @brief Semantic event identifier. VirtualEventType type = None; /// @brief Non-negative scalar amount associated with `type`. + /// + /// The value is not a raw device unit. It is the virtual amount emitted by + /// the active input path after applying binding-local gains. manipulation_encode_t magnitude = {}; /// @brief Convert one event identifier to its stable string form. diff --git a/common/include/camera/ICamera.hpp b/common/include/camera/ICamera.hpp index 06dbcd641..62b33bcf1 100644 --- a/common/include/camera/ICamera.hpp +++ b/common/include/camera/ICamera.hpp @@ -19,7 +19,7 @@ namespace nbl::core /// `ICamera` consumes batches of `CVirtualGimbalEvent` values and updates one /// camera pose stored in `CGimbal`. A `CVirtualGimbalEvent` identifies one /// semantic command such as `MoveForward`, `PanLeft`, or `RollRight` and carries -/// one scalar magnitude for that command. +/// one source-normalized scalar magnitude for that command. /// /// Keyboard input, mouse input, ImGuizmo interaction, scripted playback, /// preset replay, follow helpers, and goal solving all drive cameras through @@ -29,18 +29,23 @@ namespace nbl::core /// capture, restore, compatibility analysis, persistence, or validation. class ICamera : virtual public core::IReferenceCounted { +private: + static inline constexpr double DefaultMoveSpeedScaleValue = 0.01; + static inline constexpr double DefaultRotationSpeedScaleValue = 0.003; + static inline constexpr double VirtualTranslationUnit = 0.01; + public: /// @brief Camera-local multipliers applied when semantic virtual events are converted into motion. /// - /// The shared runtime traits define the base unit magnitude of a virtual - /// event. Concrete cameras multiply those base units by this per-camera - /// configuration before applying them to their own state model. + /// Input binders emit virtual magnitudes. Concrete cameras multiply those + /// magnitudes by this per-camera configuration before applying them to + /// their own state model. struct SMotionConfig { /// @brief Camera-local scale applied to virtual translation magnitudes. - double moveSpeedScale = SCameraRuntimeTraits::DefaultMoveSpeedScale; + double moveSpeedScale = DefaultMoveSpeedScaleValue; /// @brief Camera-local scale applied to virtual rotation magnitudes. - double rotationSpeedScale = SCameraRuntimeTraits::DefaultRotationSpeedScale; + double rotationSpeedScale = DefaultRotationSpeedScaleValue; }; /// @brief Stable camera-family identifier used by metadata, presets, follow, and scripted helpers. @@ -396,12 +401,12 @@ class ICamera : virtual public core::IReferenceCounted /// @brief Return the effective world-space translation represented by a unit virtual move event. inline double getScaledVirtualTranslationMagnitude() const { - return SCameraRuntimeTraits::VirtualTranslationStep * getMoveSpeedScale(); + return getUnscaledVirtualTranslationMagnitude() * getMoveSpeedScale(); } /// @brief Return the raw translation magnitude before applying the camera-local move scale. inline double getUnscaledVirtualTranslationMagnitude() const { - return SCameraRuntimeTraits::VirtualTranslationStep; + return VirtualTranslationUnit; } /// @brief Scale one scalar translation magnitude through the active move scale. inline double scaleVirtualTranslation(const double magnitude) const diff --git a/common/include/camera/IGimbalBindingLayout.hpp b/common/include/camera/IGimbalBindingLayout.hpp index 4686fc961..c1b09427b 100644 --- a/common/include/camera/IGimbalBindingLayout.hpp +++ b/common/include/camera/IGimbalBindingLayout.hpp @@ -13,6 +13,8 @@ namespace nbl::ui /// @brief Static mapping from external input domains to virtual gimbal events. /// /// This type stores binding layout only. It does not process runtime input. +/// Each binding chooses both the semantic virtual event type and the gain used +/// to convert raw producer values into `CVirtualGimbalEvent::magnitude`. struct IGimbalBindingLayout { IGimbalBindingLayout() {} @@ -50,11 +52,18 @@ struct IGimbalBindingLayout struct CHashInfo { + static inline constexpr double DefaultMagnitudeScale = 1.0; + CHashInfo() {} - CHashInfo(gimbal_event_t::VirtualEventType _type) : event({ .type = _type }) {} + CHashInfo(gimbal_event_t::VirtualEventType _type, const double _magnitudeScale = DefaultMagnitudeScale) + : event({ .type = _type }), magnitudeScale(_magnitudeScale) {} ~CHashInfo() = default; + /// @brief Virtual event emitted by this binding. gimbal_event_t event = {}; + /// @brief Per-binding gain applied when raw input is converted into one virtual-event magnitude. + double magnitudeScale = DefaultMagnitudeScale; + /// @brief Runtime latch used by held keyboard and mouse-button bindings. bool active = false; }; @@ -90,7 +99,7 @@ struct IGimbalBindingLayout { Map result; for (const auto& [code, hash] : source) - result.emplace(code, typename Map::mapped_type(hash.event.type)); + result.emplace(code, typename Map::mapped_type(hash.event.type, hash.magnitudeScale)); return result; } }; diff --git a/common/include/camera/IGimbalInputProcessor.hpp b/common/include/camera/IGimbalInputProcessor.hpp index 031e8cc02..4e6f7bfd4 100644 --- a/common/include/camera/IGimbalInputProcessor.hpp +++ b/common/include/camera/IGimbalInputProcessor.hpp @@ -13,16 +13,22 @@ namespace nbl::ui { /// @brief Runtime processor that turns keyboard, mouse, and ImGuizmo input into virtual events. +/// +/// Held keyboard and mouse-button bindings emit `frameDeltaSeconds * magnitudeScale`. +/// Relative mouse movement, mouse scroll, and ImGuizmo deltas emit +/// `abs(rawDelta) * magnitudeScale` per bound axis. The result is written into +/// `CVirtualGimbalEvent::magnitude`. class IGimbalInputProcessor : public CGimbalBindingLayoutStorage { public: struct SInputProcessorDefaults final { - static inline constexpr double MaxFrameDeltaMs = 200.0; + /// @brief Largest frame interval, in seconds, accepted from held-input accumulation. + static inline constexpr double MaxFrameDeltaSeconds = 0.2; static inline constexpr float ZeroPivot = 0.0f; static inline constexpr float UnitPivot = 1.0f; }; - static inline constexpr double MaxFrameDeltaMs = SInputProcessorDefaults::MaxFrameDeltaMs; + static inline constexpr double MaxFrameDeltaSeconds = SInputProcessorDefaults::MaxFrameDeltaSeconds; static inline constexpr float ZeroPivot = SInputProcessorDefaults::ZeroPivot; static inline constexpr float UnitPivot = SInputProcessorDefaults::UnitPivot; @@ -43,7 +49,7 @@ class IGimbalInputProcessor : public CGimbalBindingLayoutStorage void beginInputProcessing(const std::chrono::microseconds nextPresentationTimeStamp) { m_nextPresentationTimeStamp = nextPresentationTimeStamp; - m_frameDeltaTime = clampFrameDeltaTimeMs(m_nextPresentationTimeStamp, m_lastVirtualUpTimeStamp); + m_frameDeltaSeconds = clampFrameDeltaTimeSeconds(m_nextPresentationTimeStamp, m_lastVirtualUpTimeStamp); } void endInputProcessing() @@ -93,6 +99,7 @@ class IGimbalInputProcessor : public CGimbalBindingLayoutStorage /// /// @note This function maps keyboard press and release events into virtual /// gimbal manipulation events through the active keyboard bindings. + /// Held keys contribute elapsed seconds scaled by the binding gain. /// /// @param output Pointer to the destination array for generated gimbal events. /// Pass `nullptr` to query only the total event count. @@ -120,6 +127,8 @@ class IGimbalInputProcessor : public CGimbalBindingLayoutStorage /// /// @note This function maps mouse clicks, scrolls, and movements into /// virtual gimbal manipulation events through the active mouse bindings. + /// Relative movement and scroll contribute absolute signed deltas scaled by + /// the matching binding gain. /// /// @param output Pointer to the destination array for generated gimbal events. /// Pass `nullptr` to query only the total event count. @@ -172,6 +181,8 @@ class IGimbalInputProcessor : public CGimbalBindingLayoutStorage /// /// @note This function converts world-space delta transforms authored by /// ImGuizmo into translation, rotation, and scale virtual events. + /// Translation uses world-space delta components. Rotation uses extracted + /// Euler radians. Scale uses multiplicative components around pivot `1`. /// /// @param output Pointer to the destination array for generated gimbal events. /// Pass `nullptr` to query only the total event count. @@ -287,15 +298,15 @@ class IGimbalInputProcessor : public CGimbalBindingLayoutStorage }; }; - static double clampFrameDeltaTimeMs( + static double clampFrameDeltaTimeSeconds( const std::chrono::microseconds nextPresentationTimeStamp, const std::chrono::microseconds lastVirtualUpTimeStamp) { - const auto deltaMs = std::chrono::duration_cast( + const auto deltaSeconds = std::chrono::duration( nextPresentationTimeStamp - lastVirtualUpTimeStamp).count(); - if (deltaMs < 0) + if (deltaSeconds < 0.0) return 0.0; - return std::min(static_cast(deltaMs), MaxFrameDeltaMs); + return std::min(deltaSeconds, MaxFrameDeltaSeconds); } template @@ -362,7 +373,7 @@ class IGimbalInputProcessor : public CGimbalBindingLayoutStorage hash.event.magnitude = 0.0f; if (hash.active) - hash.event.magnitude = m_frameDeltaTime; + hash.event.magnitude = m_frameDeltaSeconds * hash.magnitudeScale; } } @@ -387,7 +398,7 @@ class IGimbalInputProcessor : public CGimbalBindingLayoutStorage auto code = (dScalar > signPivot) ? positive : negative; auto request = map.find(code); if (request != map.end()) - request->second.event.magnitude += dMagnitude; + request->second.event.magnitude += dMagnitude * request->second.magnitudeScale; } } @@ -418,7 +429,7 @@ class IGimbalInputProcessor : public CGimbalBindingLayoutStorage map); } - double m_frameDeltaTime = {}; + double m_frameDeltaSeconds = {}; std::chrono::microseconds m_nextPresentationTimeStamp = {}, m_lastVirtualUpTimeStamp = {}; }; diff --git a/common/include/camera/README.md b/common/include/camera/README.md index 341003d5c..8c443676c 100644 --- a/common/include/camera/README.md +++ b/common/include/camera/README.md @@ -214,10 +214,94 @@ What happens here: Main types involved: - [`CVirtualGimbalEvent.hpp`](CVirtualGimbalEvent.hpp) +- [`IGimbalBindingLayout.hpp`](IGimbalBindingLayout.hpp) - [`IGimbalInputProcessor.hpp`](IGimbalInputProcessor.hpp) - [`CGimbalInputBinder.hpp`](CGimbalInputBinder.hpp) +- [`CCameraInputBindingUtilities.hpp`](CCameraInputBindingUtilities.hpp) - [`ICamera.hpp`](ICamera.hpp) +The controller-side stack is: + +- `IGimbalBindingLayout` for the static mapping from device inputs to virtual events +- `IGimbalInputProcessor` for converting one frame of raw input into event magnitudes +- `CGimbalInputBinder` for the common runtime object that owns a layout and collects one frame of events +- `CCameraInputBindingUtilities` for shared preset layouts such as default `FPS`, `Orbit`, or `Path Rig` bindings + +#### How do I bind `FPS` to `WASD`? + +Use the shared default binding preset for the active camera kind. + +```cpp +auto camera = core::make_smart_refctd_ptr(position, orientation); + +ui::CGimbalInputBinder binder; +ui::CCameraInputBindingUtilities::applyDefaultCameraInputBindingPreset(binder, *camera); +``` + +For `FPS`, the default preset gives you: + +- keyboard `W/S/A/D` -> forward, backward, left, right +- keyboard `I/K/J/L` -> tilt up, tilt down, pan left, pan right +- mouse relative movement -> look yaw and pitch + +For `Free`, the default preset adds `Q/E` for roll. + +For target-relative families and `Path Rig`, the default preset keeps the same physical inputs but maps them to the legal state space of that family. + +#### How do I make my own bindings? + +Use one `IGimbalBindingLayout` implementation such as `CGimbalInputBinder` and write the mapping you want. + +```cpp +ui::CGimbalInputBinder binder; +const double customMoveGain = /* choose a sensitivity for this binding */; + +binder.updateKeyboardMapping([customMoveGain](auto& map) +{ + map.clear(); + map.emplace(ui::E_KEY_CODE::EKC_W, ui::IGimbalBindingLayout::CHashInfo(core::CVirtualGimbalEvent::MoveForward, customMoveGain)); + map.emplace(ui::E_KEY_CODE::EKC_S, ui::IGimbalBindingLayout::CHashInfo(core::CVirtualGimbalEvent::MoveBackward, customMoveGain)); + map.emplace(ui::E_KEY_CODE::EKC_A, ui::IGimbalBindingLayout::CHashInfo(core::CVirtualGimbalEvent::MoveLeft, customMoveGain)); + map.emplace(ui::E_KEY_CODE::EKC_D, ui::IGimbalBindingLayout::CHashInfo(core::CVirtualGimbalEvent::MoveRight, customMoveGain)); +}); +``` + +The same pattern works for: + +- mouse bindings through `updateMouseMapping(...)` +- ImGuizmo bindings through `updateImguizmoMapping(...)` + +#### How are `magnitude` values generated? + +`CVirtualGimbalEvent::magnitude` is one non-negative scalar attached to one semantic command. + +It is not a raw device unit and it is not, by itself, the final world-space or angular motion applied by a camera. + +What stays stable at the API level is the meaning by event family: + +- translation events carry one controller-side translation amount +- rotation events carry one controller-side angular amount +- scale events carry one controller-side scale amount + +The binding layer maps raw producer values onto those amounts. Different sources may start from: + +- elapsed time for held input +- cursor deltas for relative mouse input +- scroll steps for wheel input +- world-space translation or angular deltas for gizmo-driven input + +That means exact numeric gains are binding policy, not API contract. The binding layer owns sensitivity and repeat-rate tuning. + +After the controller side emits virtual magnitudes, the camera runtime applies its own motion scales and legalizes the result to the concrete camera family. + +The motion pipeline is therefore: + +1. raw device input +2. binding-local gain +3. `CVirtualGimbalEvent { type, magnitude }` +4. camera-local motion scale +5. family-specific legalization and state update + ### 6. Capture a camera and restore it later Use this when you want explicit camera state instead of one-frame runtime input. @@ -308,66 +392,6 @@ Main types involved: - [`CCameraScriptedRuntime.hpp`](CCameraScriptedRuntime.hpp) - [`CCameraScriptedCheckRunner.hpp`](CCameraScriptedCheckRunner.hpp) -## Mental model - -The stack uses two complementary representations of camera behavior. - -### Runtime representation - -The runtime representation answers: - -> what commands are applied to the camera during this frame - -It is event-driven. - -Core types: - -- `CVirtualGimbalEvent` -- `IGimbal` -- `ICamera` - -This representation is used by: - -- keyboard input -- mouse input -- ImGuizmo -- replay helpers that need to mimic runtime movement - -### Typed representation - -The typed representation answers: - -> what explicit camera state should be captured, stored, restored, compared, blended, or validated - -It is state-driven. - -Core types: - -- `SCameraRigPose` -- `CCameraGoal` -- `CCameraPreset` -- `CCameraKeyframeTrack` -- `CCameraSequenceScript` - -This representation is used by: - -- capture -- restore -- persistence -- presets -- playback authoring -- follow -- scripted validation - -### Bridge between both representations - -`CCameraGoalSolver` is the main bridge. - -It converts: - -- runtime camera state into typed state -- typed state back into runtime camera behavior - ## Core concepts ### `CVirtualGimbalEvent` @@ -378,6 +402,10 @@ File: `CVirtualGimbalEvent` is one semantic camera command plus one scalar magnitude. +The scalar magnitude is a controller-side virtual amount emitted after binding +gains are applied. It is not a raw device delta and it is not, by itself, the +final world-space motion applied by a camera. + Examples: - `MoveForward` @@ -435,6 +463,9 @@ Important members: - `getCapabilities()` - typed hooks such as `tryGetSphericalTargetState(...)` and `tryGetPathState(...)` +Each camera also stores one local motion-scale bundle in `SMotionConfig`. +Those scales are applied after the binding layer emits virtual magnitudes. + ### `referenceFrame` Files: @@ -457,6 +488,17 @@ When you already have one absolute rigid pose, `referenceFrame` is the direct ru See Quick start sections 1 and 2 for the concrete absolute-pose usage patterns. +Shared runtime pattern: + +```text +referenceFrame + -> extract rigid reference transform + -> resolve legal state for this camera kind + -> accumulate virtual events + -> apply deltas in that state space + -> rebuild pose +``` + ### `SCameraRigPose` File: @@ -519,12 +561,7 @@ File: `CCameraGoalSolver` converts between typed camera state and runtime cameras. -It performs: - -1. capture runtime camera into `CCameraGoal` -2. analyze compatibility between goal and target camera -3. apply typed fragments directly when the target camera supports them -4. replay runtime movement when direct typed apply is not sufficient +It captures runtime cameras into `CCameraGoal`, analyzes whether a target camera can represent that goal directly, and applies the result either through typed state or through runtime replay when needed. If you want to restore one absolute camera state and you are not sure which family-specific hook to call, use `CCameraGoalSolver`. @@ -630,101 +667,7 @@ Files: - `v` - `roll` -Its runtime and typed tooling are driven by `SCameraPathModel`. - -Model callbacks: - -- `resolveState` -- `controlLaw` -- `integrate` -- `evaluate` -- `updateDistance` - -## Runtime behavior - -### Input flow - -Runtime input flow is: - -```text -physical input - -> binding layout - -> input processor or binder - -> CVirtualGimbalEvent[] - -> ICamera::manipulate(...) - -> updated gimbal - -> updated view matrix -``` - -Main files: - -- [`IGimbalBindingLayout.hpp`](IGimbalBindingLayout.hpp) -- [`IGimbalInputProcessor.hpp`](IGimbalInputProcessor.hpp) -- [`CGimbalInputBinder.hpp`](CGimbalInputBinder.hpp) -- [`CCameraInputBindingUtilities.hpp`](CCameraInputBindingUtilities.hpp) - -### `manipulate(...)` - -`ICamera::manipulate(...)` is the shared runtime entry point for every camera kind. - -Its responsibilities are: - -- consume one frame of semantic virtual events -- optionally consume one rigid reference frame -- update camera-family state -- update the runtime gimbal pose - -It is used by: - -- live input -- ImGuizmo -- replay helpers -- code that wants one frame of camera movement - -### Motion scales - -`ICamera` stores per-camera motion scales in `SMotionConfig`. - -Those scales are applied on top of shared runtime units defined in [`CCameraTraits.hpp`](CCameraTraits.hpp). - -### Typed hooks - -Some runtime cameras expose typed state directly. - -Examples: - -- `tryGetSphericalTargetState(...)` -- `trySetSphericalTarget(...)` -- `tryGetDynamicPerspectiveState(...)` -- `tryGetPathState(...)` - -The capability mask and goal-state mask report which typed fragments are valid for a given camera. - -## `referenceFrame` - -`referenceFrame` is the optional rigid transform passed into `ICamera::manipulate(...)`. - -Shared runtime pattern: - -```text -referenceFrame - -> extract rigid reference transform - -> resolve legal state for this camera kind - -> accumulate virtual events - -> apply deltas in that state space - -> rebuild pose -``` - -This seam is supported across all current camera kinds. - -Per camera family: - -- `Free` and `FPS` - use the extracted rigid reference as the runtime pose anchor, with `FPS` legalizing orientation to upright `pitch/yaw` -- target-relative cameras - resolve target-relative state from the rigid reference while preserving target-relative constraints -- `Path Rig` - resolves typed path state through the active path model +Its runtime and typed tooling are driven by `SCameraPathModel`, which defines how path state is resolved, updated, and converted back into camera pose. ## Camera families From 0736f23fefb4411cc6f53fd69b03a5a2619e939c Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Fri, 10 Apr 2026 10:00:44 +0200 Subject: [PATCH 199/205] Move camera stack into Nabla ext module --- 61_UI/AppCameraConfigJsonParsing.cpp | 22 +- 61_UI/AppCameraConfiguration.cpp | 4 +- 61_UI/AppControlPanel.cpp | 2 +- 61_UI/AppHeadlessCameraSmoke.cpp | 8 +- 61_UI/AppResourceUtilities.cpp | 2 +- 61_UI/AppScriptedInitialization.cpp | 2 +- 61_UI/AppUiResources.cpp | 12 +- 61_UI/CMakeLists.txt | 110 +- 61_UI/README.md | 20 +- 61_UI/app_resources/imgui.unified.hlsl | 13 + 61_UI/include/app/App.hpp | 2 +- .../app/AppControlPanelAuthoringUtilities.hpp | 2 +- .../include/app/AppResourcePathUtilities.hpp | 2 +- .../app/AppViewportBindingUtilities.hpp | 2 +- 61_UI/include/common.hpp | 70 +- common/CMakeLists.txt | 5 - common/include/camera/CArcballCamera.hpp | 89 -- .../include/camera/CCameraFileUtilities.hpp | 93 -- .../CCameraFollowRegressionUtilities.hpp | 371 ------ .../include/camera/CCameraFollowUtilities.hpp | 391 ------ common/include/camera/CCameraGoal.hpp | 409 ------ common/include/camera/CCameraGoalAnalysis.hpp | 89 -- common/include/camera/CCameraGoalSolver.hpp | 646 --------- .../camera/CCameraInputBindingUtilities.hpp | 561 -------- .../include/camera/CCameraKeyframeTrack.hpp | 175 --- .../CCameraKeyframeTrackPersistence.hpp | 30 - .../include/camera/CCameraKindUtilities.hpp | 140 -- .../camera/CCameraManipulationUtilities.hpp | 156 --- .../include/camera/CCameraMathUtilities.hpp | 948 -------------- common/include/camera/CCameraPathMetadata.hpp | 30 - .../include/camera/CCameraPathUtilities.hpp | 575 -------- common/include/camera/CCameraPersistence.hpp | 33 - .../camera/CCameraPlaybackTimeline.hpp | 103 -- .../camera/CCameraPresentationUtilities.hpp | 125 -- common/include/camera/CCameraPreset.hpp | 71 - common/include/camera/CCameraPresetFlow.hpp | 150 --- .../camera/CCameraPresetPersistence.hpp | 40 - .../camera/CCameraProjectionUtilities.hpp | 36 - .../camera/CCameraScriptedCheckRunner.hpp | 564 -------- .../include/camera/CCameraScriptedRuntime.hpp | 392 ------ .../CCameraScriptedRuntimePersistence.hpp | 74 -- .../CCameraScriptedUiInputUtilities.hpp | 98 -- .../include/camera/CCameraSequenceScript.hpp | 792 ----------- .../CCameraSequenceScriptPersistence.hpp | 29 - .../camera/CCameraSequenceScriptedBuilder.hpp | 128 -- .../CCameraSmokeRegressionUtilities.hpp | 122 -- .../camera/CCameraTargetRelativeUtilities.hpp | 270 ---- .../include/camera/CCameraTextUtilities.hpp | 210 --- common/include/camera/CCameraTraits.hpp | 41 - .../CCameraViewportOverlayUtilities.hpp | 2 +- .../camera/CCameraVirtualEventUtilities.hpp | 188 --- common/include/camera/CChaseCamera.hpp | 89 -- common/include/camera/CCubeProjection.hpp | 97 -- common/include/camera/CDollyCamera.hpp | 80 -- common/include/camera/CDollyZoomCamera.hpp | 122 -- common/include/camera/CFPSCamera.hpp | 125 -- common/include/camera/CFreeLockCamera.hpp | 83 -- .../include/camera/CGeneralPurposeGimbal.hpp | 24 - common/include/camera/CGimbalInputBinder.hpp | 131 -- common/include/camera/CIsometricCamera.hpp | 77 -- common/include/camera/CLinearProjection.hpp | 59 - common/include/camera/COrbitCamera.hpp | 83 -- common/include/camera/CPathCamera.hpp | 342 ----- common/include/camera/CPlanarProjection.hpp | 56 - .../include/camera/CSphericalTargetCamera.hpp | 240 ---- common/include/camera/CTopDownCamera.hpp | 78 -- common/include/camera/CTurntableCamera.hpp | 85 -- common/include/camera/CVirtualGimbalEvent.hpp | 159 --- common/include/camera/ICamera.hpp | 456 ------- common/include/camera/IGimbal.hpp | 361 ----- .../include/camera/IGimbalBindingLayout.hpp | 131 -- .../include/camera/IGimbalInputProcessor.hpp | 438 ------- common/include/camera/ILinearProjection.hpp | 182 --- .../include/camera/IPerspectiveProjection.hpp | 64 - common/include/camera/IPlanarProjection.hpp | 137 -- common/include/camera/IProjection.hpp | 71 - common/include/camera/IRange.hpp | 21 - common/include/camera/README.md | 841 ------------ common/include/camera/SCameraRigPose.hpp | 26 - .../CCameraJsonPersistenceUtilities.hpp | 98 -- .../examples/camera/CCameraPersistence.cpp | 278 ---- .../CCameraScriptedRuntimePersistence.cpp | 1156 ----------------- 82 files changed, 115 insertions(+), 14524 deletions(-) create mode 100644 61_UI/app_resources/imgui.unified.hlsl delete mode 100644 common/include/camera/CArcballCamera.hpp delete mode 100644 common/include/camera/CCameraFileUtilities.hpp delete mode 100644 common/include/camera/CCameraFollowRegressionUtilities.hpp delete mode 100644 common/include/camera/CCameraFollowUtilities.hpp delete mode 100644 common/include/camera/CCameraGoal.hpp delete mode 100644 common/include/camera/CCameraGoalAnalysis.hpp delete mode 100644 common/include/camera/CCameraGoalSolver.hpp delete mode 100644 common/include/camera/CCameraInputBindingUtilities.hpp delete mode 100644 common/include/camera/CCameraKeyframeTrack.hpp delete mode 100644 common/include/camera/CCameraKeyframeTrackPersistence.hpp delete mode 100644 common/include/camera/CCameraKindUtilities.hpp delete mode 100644 common/include/camera/CCameraManipulationUtilities.hpp delete mode 100644 common/include/camera/CCameraMathUtilities.hpp delete mode 100644 common/include/camera/CCameraPathMetadata.hpp delete mode 100644 common/include/camera/CCameraPathUtilities.hpp delete mode 100644 common/include/camera/CCameraPersistence.hpp delete mode 100644 common/include/camera/CCameraPlaybackTimeline.hpp delete mode 100644 common/include/camera/CCameraPresentationUtilities.hpp delete mode 100644 common/include/camera/CCameraPreset.hpp delete mode 100644 common/include/camera/CCameraPresetFlow.hpp delete mode 100644 common/include/camera/CCameraPresetPersistence.hpp delete mode 100644 common/include/camera/CCameraProjectionUtilities.hpp delete mode 100644 common/include/camera/CCameraScriptedCheckRunner.hpp delete mode 100644 common/include/camera/CCameraScriptedRuntime.hpp delete mode 100644 common/include/camera/CCameraScriptedRuntimePersistence.hpp delete mode 100644 common/include/camera/CCameraScriptedUiInputUtilities.hpp delete mode 100644 common/include/camera/CCameraSequenceScript.hpp delete mode 100644 common/include/camera/CCameraSequenceScriptPersistence.hpp delete mode 100644 common/include/camera/CCameraSequenceScriptedBuilder.hpp delete mode 100644 common/include/camera/CCameraSmokeRegressionUtilities.hpp delete mode 100644 common/include/camera/CCameraTargetRelativeUtilities.hpp delete mode 100644 common/include/camera/CCameraTextUtilities.hpp delete mode 100644 common/include/camera/CCameraTraits.hpp delete mode 100644 common/include/camera/CCameraVirtualEventUtilities.hpp delete mode 100644 common/include/camera/CChaseCamera.hpp delete mode 100644 common/include/camera/CCubeProjection.hpp delete mode 100644 common/include/camera/CDollyCamera.hpp delete mode 100644 common/include/camera/CDollyZoomCamera.hpp delete mode 100644 common/include/camera/CFPSCamera.hpp delete mode 100644 common/include/camera/CFreeLockCamera.hpp delete mode 100644 common/include/camera/CGeneralPurposeGimbal.hpp delete mode 100644 common/include/camera/CGimbalInputBinder.hpp delete mode 100644 common/include/camera/CIsometricCamera.hpp delete mode 100644 common/include/camera/CLinearProjection.hpp delete mode 100644 common/include/camera/COrbitCamera.hpp delete mode 100644 common/include/camera/CPathCamera.hpp delete mode 100644 common/include/camera/CPlanarProjection.hpp delete mode 100644 common/include/camera/CSphericalTargetCamera.hpp delete mode 100644 common/include/camera/CTopDownCamera.hpp delete mode 100644 common/include/camera/CTurntableCamera.hpp delete mode 100644 common/include/camera/CVirtualGimbalEvent.hpp delete mode 100644 common/include/camera/ICamera.hpp delete mode 100644 common/include/camera/IGimbal.hpp delete mode 100644 common/include/camera/IGimbalBindingLayout.hpp delete mode 100644 common/include/camera/IGimbalInputProcessor.hpp delete mode 100644 common/include/camera/ILinearProjection.hpp delete mode 100644 common/include/camera/IPerspectiveProjection.hpp delete mode 100644 common/include/camera/IPlanarProjection.hpp delete mode 100644 common/include/camera/IProjection.hpp delete mode 100644 common/include/camera/IRange.hpp delete mode 100644 common/include/camera/README.md delete mode 100644 common/include/camera/SCameraRigPose.hpp delete mode 100644 common/src/nbl/examples/camera/CCameraJsonPersistenceUtilities.hpp delete mode 100644 common/src/nbl/examples/camera/CCameraPersistence.cpp delete mode 100644 common/src/nbl/examples/camera/CCameraScriptedRuntimePersistence.cpp diff --git a/61_UI/AppCameraConfigJsonParsing.cpp b/61_UI/AppCameraConfigJsonParsing.cpp index 13202421f..b05411bbf 100644 --- a/61_UI/AppCameraConfigJsonParsing.cpp +++ b/61_UI/AppCameraConfigJsonParsing.cpp @@ -7,17 +7,17 @@ #include "keysmapping.hpp" #include "nlohmann/json.hpp" -#include "camera/CArcballCamera.hpp" -#include "camera/CChaseCamera.hpp" -#include "camera/CDollyCamera.hpp" -#include "camera/CDollyZoomCamera.hpp" -#include "camera/CFPSCamera.hpp" -#include "camera/CFreeLockCamera.hpp" -#include "camera/CIsometricCamera.hpp" -#include "camera/COrbitCamera.hpp" -#include "camera/CPathCamera.hpp" -#include "camera/CTopDownCamera.hpp" -#include "camera/CTurntableCamera.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 { diff --git a/61_UI/AppCameraConfiguration.cpp b/61_UI/AppCameraConfiguration.cpp index f1a38ad15..406c86a0b 100644 --- a/61_UI/AppCameraConfiguration.cpp +++ b/61_UI/AppCameraConfiguration.cpp @@ -8,8 +8,8 @@ #include "app/AppCameraConfigUtilities.hpp" #include "app/AppResourceUtilities.hpp" #include "app/AppViewportBindingUtilities.hpp" -#include "camera/CCameraPersistence.hpp" -#include "camera/CCameraScriptedRuntimePersistence.hpp" +#include "nbl/ext/Cameras/CCameraPersistence.hpp" +#include "nbl/ext/Cameras/CCameraScriptedRuntimePersistence.hpp" bool App::initializeCameraConfiguration(const argparse::ArgumentParser& program) { diff --git a/61_UI/AppControlPanel.cpp b/61_UI/AppControlPanel.cpp index f05acf883..eed7a548f 100644 --- a/61_UI/AppControlPanel.cpp +++ b/61_UI/AppControlPanel.cpp @@ -1,5 +1,5 @@ #include "app/App.hpp" -#include "camera/CCameraPersistence.hpp" +#include "nbl/ext/Cameras/CCameraPersistence.hpp" bool App::savePresetsToFile(const nbl::system::path& path) { diff --git a/61_UI/AppHeadlessCameraSmoke.cpp b/61_UI/AppHeadlessCameraSmoke.cpp index 3611b0e8e..281d71e7b 100644 --- a/61_UI/AppHeadlessCameraSmoke.cpp +++ b/61_UI/AppHeadlessCameraSmoke.cpp @@ -10,10 +10,10 @@ #include #include "app/AppCameraConfigUtilities.hpp" #include "app/AppResourceUtilities.hpp" -#include "camera/CCameraPathUtilities.hpp" -#include "camera/CCameraPersistence.hpp" -#include "camera/CCameraScriptedRuntimePersistence.hpp" -#include "camera/CCameraSmokeRegressionUtilities.hpp" +#include "nbl/ext/Cameras/CCameraPathUtilities.hpp" +#include "nbl/ext/Cameras/CCameraPersistence.hpp" +#include "nbl/ext/Cameras/CCameraScriptedRuntimePersistence.hpp" +#include "nbl/ext/Cameras/CCameraSmokeRegressionUtilities.hpp" #include "nlohmann/json.hpp" #include "AppHeadlessCameraSmokeHelpers.inl" #include "AppHeadlessCameraSmokeChecks.inl" diff --git a/61_UI/AppResourceUtilities.cpp b/61_UI/AppResourceUtilities.cpp index 7d06e9dcb..40469b220 100644 --- a/61_UI/AppResourceUtilities.cpp +++ b/61_UI/AppResourceUtilities.cpp @@ -5,7 +5,7 @@ #include #include "app/AppResourcePathUtilities.hpp" -#include "camera/CCameraFileUtilities.hpp" +#include "nbl/ext/Cameras/CCameraFileUtilities.hpp" inline bool parseSpaceEnvBlobBytes( std::span blobBytes, diff --git a/61_UI/AppScriptedInitialization.cpp b/61_UI/AppScriptedInitialization.cpp index 82c12f760..4ac1b10f1 100644 --- a/61_UI/AppScriptedInitialization.cpp +++ b/61_UI/AppScriptedInitialization.cpp @@ -2,7 +2,7 @@ #include "app/AppCameraConfigUtilities.hpp" #include "app/AppResourceUtilities.hpp" -#include "camera/CCameraScriptedRuntimePersistence.hpp" +#include "nbl/ext/Cameras/CCameraScriptedRuntimePersistence.hpp" void App::resetScriptedInputRuntimeState() { diff --git a/61_UI/AppUiResources.cpp b/61_UI/AppUiResources.cpp index d3bb72726..e04d0efac 100644 --- a/61_UI/AppUiResources.cpp +++ b/61_UI/AppUiResources.cpp @@ -123,16 +123,14 @@ bool App::initializeUiResources() params.transfer = getTransferUpQueue(); params.utilities = m_utils; - const auto vertexKey = nbl::this_example::builtin::build::get_spirv_key<"imgui_vertex">(m_device.get()); - const auto fragmentKey = nbl::this_example::builtin::build::get_spirv_key<"imgui_fragment">(m_device.get()); - auto vertexShader = nbl::system::loadPrecompiledShaderFromAppResources(*m_assetMgr, m_logger.get(), vertexKey); - auto fragmentShader = nbl::system::loadPrecompiledShaderFromAppResources(*m_assetMgr, m_logger.get(), fragmentKey); - if (!vertexShader || !fragmentShader) + 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 = std::move(vertexShader), - .fragment = std::move(fragmentShader) + .vertex = imguiShader, + .fragment = std::move(imguiShader) }; m_ui.manager = nbl::ext::imgui::UI::create(std::move(params)); diff --git a/61_UI/CMakeLists.txt b/61_UI/CMakeLists.txt index 4f139cd68..aee61c176 100644 --- a/61_UI/CMakeLists.txt +++ b/61_UI/CMakeLists.txt @@ -1,49 +1,15 @@ -if(NBL_BUILD_IMGUI AND NBL_EXT_FRUSTUM_LIB) - set(NBL_EXTRA_SOURCES - "${CMAKE_CURRENT_SOURCE_DIR}/AppCameraConfigUtilities.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/AppCameraConfigJsonParsing.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/AppCameraPlanarRuntime.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/AppCameraConfiguration.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/AppCameraUiState.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/AppFollowRuntime.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/AppManipulableObjects.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/AppDebugSceneRendererResources.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/AppFrameCapture.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/AppFrameRuntime.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/AppSceneDebugInstances.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/AppSceneRenderPasses.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/AppControlPanel.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/AppControlPanelCameraTab.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/AppControlPanelPlaybackTab.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/AppControlPanelPresetsTab.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/AppControlPanelProjectionTab.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/AppControlPanelStatusTab.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/AppControlPanelTabs.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/AppControlPanelUtilityTabs.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/AppPresentationResources.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/AppPresetPlayback.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/AppHeadlessCameraSmoke.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/AppInputRuntime.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/AppUiInputCapture.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/AppScriptedInitialization.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/AppScriptedInputRuntime.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/AppScriptedValidation.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/AppImGuiListen.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/AppInit.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/AppResourceBootstrap.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/AppTextResourceUtilities.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/AppResourceUtilities.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/AppSceneResources.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/AppSceneFramebufferResources.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/AppSpaceEnvironmentResources.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/AppTransformEditor.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/AppUiResources.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/AppUiRuntime.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/AppUpdate.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/AppViewportGizmos.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/AppViewportWindows.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/AppWorkLoop.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/src/keysmapping.cpp" +if(TARGET Nabla::ext::FullScreenTriangle AND TARGET Nabla::ext::Cameras AND TARGET Nabla::ext::ImGUI) + 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 @@ -51,60 +17,41 @@ if(NBL_BUILD_IMGUI AND NBL_EXT_FRUSTUM_LIB) ) list(APPEND NBL_LIBRARIES - imtestengine - imguizmo - "${NBL_EXT_IMGUI_UI_LIB}" + argparse + Nabla::ext::ImGUI Nabla::ext::FullScreenTriangle - "${NBL_EXT_FRUSTUM_LIB}" + Nabla::ext::Cameras ) nbl_create_executable_project("${NBL_EXTRA_SOURCES}" "" "${NBL_INCLUDE_SERACH_DIRECTORIES}" "${NBL_LIBRARIES}") - if(NBL_EMBED_BUILTIN_RESOURCES) - set(_BR_TARGET_ ${EXECUTABLE_NAME}_builtinResourceData) - set(RESOURCE_DIR "app_resources") - - get_filename_component(_SEARCH_DIRECTORIES_ "${CMAKE_CURRENT_SOURCE_DIR}" ABSOLUTE) - get_filename_component(_OUTPUT_DIRECTORY_SOURCE_ "${CMAKE_CURRENT_BINARY_DIR}/src" ABSOLUTE) - get_filename_component(_OUTPUT_DIRECTORY_HEADER_ "${CMAKE_CURRENT_BINARY_DIR}/include" ABSOLUTE) - - file(GLOB_RECURSE BUILTIN_RESOURCE_FILES RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}/${RESOURCE_DIR}" CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/${RESOURCE_DIR}/*") - foreach(RES_FILE ${BUILTIN_RESOURCE_FILES}) - LIST_BUILTIN_RESOURCE(RESOURCES_TO_EMBED "${RES_FILE}") - endforeach() - - ADD_CUSTOM_BUILTIN_RESOURCES(${_BR_TARGET_} RESOURCES_TO_EMBED "${_SEARCH_DIRECTORIES_}" "${RESOURCE_DIR}" "nbl::this_example::builtin" "${_OUTPUT_DIRECTORY_HEADER_}" "${_OUTPUT_DIRECTORY_SOURCE_}") - - LINK_BUILTIN_RESOURCES_TO_TARGET(${EXECUTABLE_NAME} ${_BR_TARGET_}) - endif() - 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_vertex.hlsl", - "KEY": "imgui_vertex", - "COMPILE_OPTIONS": ["-T", "vs_6_7", "-E", "VSMain", "-O3"] - }, - { - "INPUT": "app_resources/imgui_fragment.hlsl", - "KEY": "imgui_fragment", - "COMPILE_OPTIONS": ["-T", "ps_6_7", "-E", "PSMain", "-O3"] + "INPUT": "app_resources/imgui.unified.hlsl", + "KEY": "imgui.unified" }, { "INPUT": "app_resources/sky_env_fragment.hlsl", - "KEY": "sky_env_fragment", - "COMPILE_OPTIONS": ["-T", "ps_6_7", "-E", "main", "-O3"] + "KEY": "sky_env_fragment" } ] ]=]) string(CONFIGURE "${JSON}" JSON) set(COMPILE_OPTIONS + -isystem "${NBL_ROOT_PATH}/include" -I "${CMAKE_CURRENT_SOURCE_DIR}" - -I "${NBL_ROOT_PATH}/include" - -I "${NBL_ROOT_PATH}/include/nbl/ext/ImGui/builtin/hlsl" + -T lib_${SM} ) NBL_CREATE_NSC_COMPILE_RULES( @@ -127,9 +74,6 @@ if(NBL_BUILD_IMGUI AND NBL_EXT_FRUSTUM_LIB) BUILTINS ${KEYS} ) - add_dependencies(${EXECUTABLE_NAME} argparse) - target_include_directories(${EXECUTABLE_NAME} PUBLIC $) - enable_testing() set(CAMERA_CONTINUITY_SCRIPT "${CMAKE_CURRENT_SOURCE_DIR}/app_resources/cameraz_continuity.json") diff --git a/61_UI/README.md b/61_UI/README.md index b8aefd3fa..94246954d 100644 --- a/61_UI/README.md +++ b/61_UI/README.md @@ -1,6 +1,6 @@ # 61_UI Cameraz -`61_UI` is the full runnable integration and validation target for the shared camera stack documented in [`../common/include/camera/README.md`](../common/include/camera/README.md). +`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. @@ -124,15 +124,15 @@ It is no longer a giant committed frame dump. `61_UI` consumes the shared stack directly: -- [`CCameraInputBindingUtilities.hpp`](../common/include/camera/CCameraInputBindingUtilities.hpp) -- [`CCameraPresetFlow.hpp`](../common/include/camera/CCameraPresetFlow.hpp) -- [`CCameraFollowUtilities.hpp`](../common/include/camera/CCameraFollowUtilities.hpp) -- [`CCameraFollowRegressionUtilities.hpp`](../common/include/camera/CCameraFollowRegressionUtilities.hpp) -- [`CCameraSequenceScript.hpp`](../common/include/camera/CCameraSequenceScript.hpp) -- [`CCameraScriptedRuntime.hpp`](../common/include/camera/CCameraScriptedRuntime.hpp) -- [`CCameraScriptedRuntimePersistence.hpp`](../common/include/camera/CCameraScriptedRuntimePersistence.hpp) -- [`CCameraSequenceScriptedBuilder.hpp`](../common/include/camera/CCameraSequenceScriptedBuilder.hpp) -- [`CCameraScriptedCheckRunner.hpp`](../common/include/camera/CCameraScriptedCheckRunner.hpp) +- [`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/nbl/ext/Cameras/CCameraScriptedRuntimePersistence.hpp) +- [`CCameraSequenceScriptedBuilder.hpp`](../../include/nbl/ext/Cameras/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. 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/include/app/App.hpp b/61_UI/include/app/App.hpp index ad3369c47..2ba8bdfc5 100644 --- a/61_UI/include/app/App.hpp +++ b/61_UI/include/app/App.hpp @@ -22,7 +22,7 @@ #include "keysmapping.hpp" #include "app/AppTypes.hpp" #include "app/AppViewportBindingUtilities.hpp" -#include "camera/CCubeProjection.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" diff --git a/61_UI/include/app/AppControlPanelAuthoringUtilities.hpp b/61_UI/include/app/AppControlPanelAuthoringUtilities.hpp index 63eb2d344..55443d9cd 100644 --- a/61_UI/include/app/AppControlPanelAuthoringUtilities.hpp +++ b/61_UI/include/app/AppControlPanelAuthoringUtilities.hpp @@ -5,7 +5,7 @@ #include #include "camera/CCameraControlPanelUiUtilities.hpp" -#include "camera/CCameraPresentationUtilities.hpp" +#include "nbl/ext/Cameras/CCameraPresentationUtilities.hpp" namespace nbl::ui { diff --git a/61_UI/include/app/AppResourcePathUtilities.hpp b/61_UI/include/app/AppResourcePathUtilities.hpp index fc14ce3b5..be868ff1f 100644 --- a/61_UI/include/app/AppResourcePathUtilities.hpp +++ b/61_UI/include/app/AppResourcePathUtilities.hpp @@ -5,7 +5,7 @@ #include #include "app/AppResourceUtilities.hpp" -#include "camera/CCameraFileUtilities.hpp" +#include "nbl/ext/Cameras/CCameraFileUtilities.hpp" namespace nbl::system { diff --git a/61_UI/include/app/AppViewportBindingUtilities.hpp b/61_UI/include/app/AppViewportBindingUtilities.hpp index 779c274df..442a2a99f 100644 --- a/61_UI/include/app/AppViewportBindingUtilities.hpp +++ b/61_UI/include/app/AppViewportBindingUtilities.hpp @@ -5,7 +5,7 @@ #include #include "app/AppTypes.hpp" -#include "camera/CCameraFollowRegressionUtilities.hpp" +#include "nbl/ext/Cameras/CCameraFollowRegressionUtilities.hpp" namespace nbl::ui { diff --git a/61_UI/include/common.hpp b/61_UI/include/common.hpp index d810434cd..1563ef9e7 100644 --- a/61_UI/include/common.hpp +++ b/61_UI/include/common.hpp @@ -6,45 +6,45 @@ #include "nbl/examples/examples.hpp" // common api -#include "camera/CFPSCamera.hpp" -#include "camera/CFreeLockCamera.hpp" -#include "camera/CSphericalTargetCamera.hpp" -#include "camera/COrbitCamera.hpp" -#include "camera/CArcballCamera.hpp" -#include "camera/CTurntableCamera.hpp" -#include "camera/CTopDownCamera.hpp" -#include "camera/CIsometricCamera.hpp" -#include "camera/CChaseCamera.hpp" -#include "camera/CDollyCamera.hpp" -#include "camera/CDollyZoomCamera.hpp" -#include "camera/CPathCamera.hpp" -#include "camera/CCameraPreset.hpp" -#include "camera/CCameraPresetFlow.hpp" -#include "camera/CCameraKeyframeTrack.hpp" -#include "camera/CCameraPlaybackTimeline.hpp" -#include "camera/CCameraSequenceScript.hpp" -#include "camera/CCameraSequenceScriptedBuilder.hpp" -#include "camera/CCameraScriptedRuntime.hpp" -#include "camera/CCameraScriptedUiInputUtilities.hpp" -#include "camera/CCameraScriptedCheckRunner.hpp" -#include "camera/CCameraGoalAnalysis.hpp" -#include "camera/CCameraGoalSolver.hpp" -#include "camera/CCameraManipulationUtilities.hpp" -#include "camera/CCameraPresentationUtilities.hpp" -#include "camera/CCameraProjectionUtilities.hpp" -#include "camera/CCameraKindUtilities.hpp" -#include "camera/CCameraFollowUtilities.hpp" -#include "camera/CCameraFollowRegressionUtilities.hpp" +#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/CCameraSequenceScriptedBuilder.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/CCameraControlPanelUiUtilities.hpp" #include "camera/CCameraScriptVisualDebugOverlayUtilities.hpp" #include "camera/CCameraViewportOverlayUtilities.hpp" -#include "camera/CCameraTextUtilities.hpp" -#include "camera/CCameraInputBindingUtilities.hpp" -#include "camera/CGimbalInputBinder.hpp" +#include "nbl/ext/Cameras/CCameraTextUtilities.hpp" +#include "nbl/ext/Cameras/CCameraInputBindingUtilities.hpp" +#include "nbl/ext/Cameras/CGimbalInputBinder.hpp" -#include "camera/CCubeProjection.hpp" -#include "camera/CLinearProjection.hpp" -#include "camera/CPlanarProjection.hpp" +#include "nbl/ext/Cameras/CCubeProjection.hpp" +#include "nbl/ext/Cameras/CLinearProjection.hpp" +#include "nbl/ext/Cameras/CPlanarProjection.hpp" // the example's headers #include "nbl/ui/ICursorControl.h" #include "nbl/ext/ImGui/ImGui.h" diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index 21bbbb814..67665c476 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -10,11 +10,6 @@ nbl_create_ext_library_project(ExamplesAPI "" "${CMAKE_CURRENT_SOURCE_DIR}/src/nbl/examples/pch.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/include" "" "") -target_sources(${LIB_NAME} PRIVATE - "${CMAKE_CURRENT_SOURCE_DIR}/src/nbl/examples/camera/CCameraPersistence.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/src/nbl/examples/camera/CCameraScriptedRuntimePersistence.cpp" -) - set_target_properties(${LIB_NAME} PROPERTIES DISABLE_PRECOMPILE_HEADERS OFF) target_precompile_headers(${LIB_NAME} PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include/nbl/examples/PCH.hpp") diff --git a/common/include/camera/CArcballCamera.hpp b/common/include/camera/CArcballCamera.hpp deleted file mode 100644 index d88d3743f..000000000 --- a/common/include/camera/CArcballCamera.hpp +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright (C) 2018-2024 - 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_ARCBALL_CAMERA_HPP_ -#define _C_ARCBALL_CAMERA_HPP_ - -#include -#include - -#include "CSphericalTargetCamera.hpp" - -namespace nbl::core -{ - -/// @brief Target-relative camera with planar target translation and bounded arcball orbiting. -/// -/// The runtime state is inherited from `CSphericalTargetCamera`. Translation -/// moves the target in the current view plane. Rotation updates orbit yaw and -/// pitch under a symmetric pitch limit. -class CArcballCamera final : public CSphericalTargetCamera -{ -public: - using base_t = CSphericalTargetCamera; - - CArcballCamera(const hlsl::float64_t3& position, const hlsl::float64_t3& target) - : base_t(position, target) - { - m_orbitUv.y = std::clamp(m_orbitUv.y, MinPitch, MaxPitch); - applyPose(); - } - ~CArcballCamera() = default; - - const typename base_t::CGimbal& getGimbal() override { return m_gimbal; } - - /// @brief Apply one frame of semantic translation and rotation input to the arcball rig. - virtual bool manipulate(std::span virtualEvents, const hlsl::float64_t4x4* referenceFrame = nullptr) override - { - if (not virtualEvents.size() and not referenceFrame) - return false; - - if (referenceFrame) - { - CReferenceTransform reference = {}; - SCameraTargetRelativeState resolvedState = {}; - if (!tryExtractReferenceTransform(reference, referenceFrame) || - !tryResolveReferenceTargetRelativeState(reference, resolvedState)) - { - return false; - } - - resolvedState.orbitUv.y = std::clamp(resolvedState.orbitUv.y, MinPitch, MaxPitch); - adoptTargetRelativeState(resolvedState); - } - - const auto impulse = m_gimbal.accumulate(virtualEvents); - - const auto deltaRotation = scaleVirtualRotation(impulse.dVirtualRotation); - const auto deltaTranslation = scaleVirtualTranslation(impulse.dVirtualTranslate); - const double deltaDistance = scaleUnscaledVirtualTranslation(impulse.dVirtualTranslate.z); - - m_orbitUv.x += deltaRotation.y; - m_orbitUv.y = std::clamp(m_orbitUv.y + deltaRotation.x, MinPitch, MaxPitch); - m_distance = std::clamp(m_distance + static_cast(deltaDistance), MinDistance, MaxDistance); - - const auto basis = computeBasis(m_orbitUv, m_distance); - applyPlanarTargetTranslation(deltaTranslation, basis); - - return applyPose(); - } - - virtual uint32_t getAllowedVirtualEvents() const override { return AllowedVirtualEvents; } - virtual CameraKind getKind() const override { return CameraKind::Arcball; } - /// @brief Return the stable user-facing identifier for this concrete camera kind. - virtual std::string_view getIdentifier() const override { return "Arcball Camera"; } - - static inline constexpr float MinDistance = base_t::MinDistance; - static inline constexpr float MaxDistance = base_t::MaxDistance; - -private: - - static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate | CVirtualGimbalEvent::Rotate; - static inline constexpr double MaxPitch = SCameraTargetRelativeRigDefaults::ArcballPitchLimitRad; - static inline constexpr double MinPitch = -MaxPitch; -}; - -} - -#endif diff --git a/common/include/camera/CCameraFileUtilities.hpp b/common/include/camera/CCameraFileUtilities.hpp deleted file mode 100644 index 9a2a0356a..000000000 --- a/common/include/camera/CCameraFileUtilities.hpp +++ /dev/null @@ -1,93 +0,0 @@ -#ifndef _C_CAMERA_FILE_UTILITIES_HPP_ -#define _C_CAMERA_FILE_UTILITIES_HPP_ - -#include -#include -#include - -#include "nbl/system/ISystem.h" - -namespace nbl::system -{ - -/// @brief Shared file I/O helpers used by camera persistence and scripted-runtime loaders. -/// -/// The helpers keep camera-facing persistence code independent from ad-hoc file -/// handling and provide one place for consistent error propagation. -struct CCameraFileUtilities final -{ -public: - /// @brief Read a whole file into a byte buffer. - static inline bool readBinaryFile( - ISystem& system, - const path& filePath, - std::vector& outPayload, - std::string* error = nullptr, - const std::string_view openError = {}) - { - ISystem::future_t> future; - system.createFile(future, filePath, IFile::ECF_READ | IFile::ECF_MAPPABLE); - auto file = future.acquire(); - if (!file || !file->get()) - { - if (error && !openError.empty()) - *error = std::string(openError); - return false; - } - - auto& input = *file->get(); - const auto fileSize = input.getSize(); - outPayload.resize(fileSize); - if (outPayload.empty()) - return true; - - IFile::success_t readResult; - input.read(readResult, outPayload.data(), 0, fileSize); - if (!static_cast(readResult)) - { - if (error && !openError.empty()) - *error = std::string(openError); - return false; - } - return true; - } - - /// @brief Read a whole file and interpret its payload as UTF-8 text. - static inline bool readTextFile( - ISystem& system, - const path& filePath, - std::string& outText, - std::string* error = nullptr, - const std::string_view openError = {}) - { - std::vector payload; - if (!readBinaryFile(system, filePath, payload, error, openError)) - return false; - - outText.assign(reinterpret_cast(payload.data()), payload.size()); - return true; - } - - /// @brief Overwrite a file with the provided text payload. - static inline bool writeTextFile( - ISystem& system, - const path& filePath, - const std::string_view text) - { - ISystem::future_t> future; - system.createFile(future, filePath, IFile::ECF_WRITE); - auto file = future.acquire(); - if (!file || !file->get()) - return false; - if (text.empty()) - return true; - - IFile::success_t writeResult; - (*file)->write(writeResult, text.data(), 0, text.size()); - return static_cast(writeResult); - } -}; - -} // namespace nbl::system - -#endif // _C_CAMERA_FILE_UTILITIES_HPP_ diff --git a/common/include/camera/CCameraFollowRegressionUtilities.hpp b/common/include/camera/CCameraFollowRegressionUtilities.hpp deleted file mode 100644 index f238af62a..000000000 --- a/common/include/camera/CCameraFollowRegressionUtilities.hpp +++ /dev/null @@ -1,371 +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 _C_CAMERA_FOLLOW_REGRESSION_UTILITIES_HPP_ -#define _C_CAMERA_FOLLOW_REGRESSION_UTILITIES_HPP_ - -#include - -#include "CCameraFollowUtilities.hpp" - -namespace nbl::system -{ - -struct SCameraProjectedTargetMetrics final -{ - hlsl::float32_t2 ndc = hlsl::float32_t2(0.0f); - float radius = 0.0f; -}; - -/// @brief Reusable follow validation helpers. -/// -/// The checks stay camera-domain: -/// -/// - camera-to-target direction must match the camera forward axis for locking modes -/// - target distance must be finite and internally consistent -/// - spherical cameras must write the tracked target back into spherical target state -/// - spherical distance must match the goal-derived distance when present -struct SCameraFollowRegressionResult -{ - bool passed = false; - bool hasLockMetrics = false; - float lockAngleDeg = 0.0f; - double targetDistance = 0.0; - bool hasProjectedMetrics = false; - SCameraProjectedTargetMetrics projectedTarget = {}; - bool hasSphericalState = false; - hlsl::float64_t3 sphericalTarget = hlsl::float64_t3(0.0); - float sphericalDistance = 0.0f; -}; - -/// @brief Reusable visual/debug metrics for one active follow configuration. -struct SCameraFollowVisualMetrics -{ - bool active = false; - core::ECameraFollowMode mode = core::ECameraFollowMode::Disabled; - bool lockValid = false; - float lockAngleDeg = 0.0f; - float targetDistance = 0.0f; - bool projectedValid = false; - SCameraProjectedTargetMetrics projectedTarget = {}; -}; - -/// @brief Shared view/projection bundle for CPU-side projected target metrics. -struct SCameraProjectionContext -{ - hlsl::float32_t4x4 viewMatrix = hlsl::float32_t4x4(1.0f); - hlsl::float32_t4x4 projectionMatrix = hlsl::float32_t4x4(1.0f); -}; - -/// @brief Shared tolerances for follow target lock, writeback, and projected-center checks. -struct SCameraFollowRegressionThresholds -{ - static inline constexpr float DefaultClipWEpsilon = 1e-5f; - static inline constexpr float DefaultProjectedNdcTolerance = 0.03f; - static inline constexpr float DefaultLockAngleToleranceDeg = static_cast(core::SCameraToolingThresholds::DefaultAngularToleranceDeg); - static inline constexpr double DefaultDistanceTolerance = core::SCameraToolingThresholds::ScalarTolerance; - static inline constexpr double DefaultTargetTolerance = core::SCameraToolingThresholds::TinyScalarEpsilon; - static inline constexpr double DefaultPositionTolerance = core::SCameraToolingThresholds::DefaultPositionTolerance; - static inline constexpr double DefaultRotationToleranceDeg = core::SCameraToolingThresholds::DefaultAngularToleranceDeg; - static inline constexpr double DefaultScalarTolerance = core::SCameraToolingThresholds::ScalarTolerance; - - float clipWEpsilon = DefaultClipWEpsilon; - float projectedNdcTolerance = DefaultProjectedNdcTolerance; - float lockAngleToleranceDeg = DefaultLockAngleToleranceDeg; - double distanceTolerance = DefaultDistanceTolerance; - double targetTolerance = DefaultTargetTolerance; - double positionTolerance = DefaultPositionTolerance; - double rotationToleranceDeg = DefaultRotationToleranceDeg; - double scalarTolerance = DefaultScalarTolerance; -}; - -/// @brief Bundled reusable follow regression flow. -/// The helper builds a follow goal, applies it, verifies the resulting camera state, -/// and then checks lock/writeback follow consistency. -struct SCameraFollowApplyValidationResult -{ - bool hasGoal = false; - core::CCameraGoal goal = {}; - core::CCameraGoalSolver::SApplyResult applyResult = {}; - bool hasCapturedGoal = false; - core::CCameraGoal capturedGoal = {}; - SCameraFollowRegressionResult regression = {}; -}; - -struct CCameraFollowRegressionUtilities final -{ -public: - static inline SCameraFollowRegressionThresholds makeFollowRegressionThresholds( - const float projectedNdcTolerance = SCameraFollowRegressionThresholds::DefaultProjectedNdcTolerance, - const float lockAngleToleranceDeg = SCameraFollowRegressionThresholds::DefaultLockAngleToleranceDeg) - { - auto thresholds = SCameraFollowRegressionThresholds{}; - thresholds.projectedNdcTolerance = projectedNdcTolerance; - thresholds.lockAngleToleranceDeg = lockAngleToleranceDeg; - return thresholds; - } - - static inline bool tryComputeProjectedFollowTargetMetrics( - const SCameraProjectionContext& projectionContext, - const core::CTrackedTarget& trackedTarget, - SCameraProjectedTargetMetrics& outMetrics, - const float clipWEpsilon = SCameraFollowRegressionThresholds::DefaultClipWEpsilon) - { - outMetrics = {}; - const auto target = hlsl::getCastedVector(trackedTarget.getGimbal().getPosition()); - const auto viewSpace = hlsl::mul(projectionContext.viewMatrix, hlsl::float32_t4(target.x, target.y, target.z, 1.0f)); - const auto clipProjection = hlsl::transpose(projectionContext.projectionMatrix); - const auto clip = hlsl::mul(clipProjection, viewSpace); - if (!hlsl::CCameraMathUtilities::isFiniteScalar(clip.x) || !hlsl::CCameraMathUtilities::isFiniteScalar(clip.y) || !hlsl::CCameraMathUtilities::isFiniteScalar(clip.z) || !hlsl::CCameraMathUtilities::isFiniteScalar(clip.w)) - return false; - - const auto absW = hlsl::abs(clip.w); - if (absW < clipWEpsilon) - return false; - - const float invW = 1.0f / clip.w; - outMetrics.ndc = hlsl::float32_t2(clip.x, clip.y) * invW; - if (!hlsl::CCameraMathUtilities::isFiniteScalar(outMetrics.ndc.x) || !hlsl::CCameraMathUtilities::isFiniteScalar(outMetrics.ndc.y)) - return false; - - outMetrics.radius = hlsl::length(outMetrics.ndc); - - return true; - } - - static inline bool validateProjectedFollowTargetContract( - const SCameraProjectionContext& projectionContext, - const core::CTrackedTarget& trackedTarget, - SCameraProjectedTargetMetrics& outMetrics, - std::string* error = nullptr, - const SCameraFollowRegressionThresholds& thresholds = {}) - { - if (!tryComputeProjectedFollowTargetMetrics(projectionContext, trackedTarget, outMetrics, thresholds.clipWEpsilon)) - { - if (error) - *error = "failed to project follow target"; - return false; - } - - if (outMetrics.radius > thresholds.projectedNdcTolerance) - { - if (error) - { - *error = "projected target mismatch ndc=(" + std::to_string(outMetrics.ndc.x) + - "," + std::to_string(outMetrics.ndc.y) + ") radius=" + std::to_string(outMetrics.radius); - } - return false; - } - - return true; - } - - static inline SCameraFollowVisualMetrics buildFollowVisualMetrics( - core::ICamera* camera, - const core::CTrackedTarget& trackedTarget, - const core::SCameraFollowConfig* followConfig, - const SCameraProjectionContext* projectionContext = nullptr) - { - SCameraFollowVisualMetrics out = {}; - if (!camera || !followConfig || !followConfig->enabled || followConfig->mode == core::ECameraFollowMode::Disabled) - return out; - - out.active = true; - out.mode = followConfig->mode; - - double targetDistance = 0.0; - out.lockValid = core::CCameraFollowUtilities::cameraFollowModeLocksViewToTarget(followConfig->mode) && - core::CCameraFollowUtilities::tryComputeFollowTargetLockMetrics(camera->getGimbal(), trackedTarget, out.lockAngleDeg, &targetDistance); - if (out.lockValid) - out.targetDistance = static_cast(targetDistance); - - if (out.lockValid && projectionContext) - { - out.projectedValid = tryComputeProjectedFollowTargetMetrics(*projectionContext, trackedTarget, out.projectedTarget); - } - - return out; - } - - static inline bool validateFollowTargetContract( - core::ICamera* camera, - const core::CTrackedTarget& trackedTarget, - const core::SCameraFollowConfig& followConfig, - const core::CCameraGoal& followGoal, - SCameraFollowRegressionResult& out, - std::string* error = nullptr, - const SCameraProjectionContext* projectionContext = nullptr, - const SCameraFollowRegressionThresholds& thresholds = {}) - { - out = {}; - if (!camera) - { - if (error) - *error = "missing camera"; - return false; - } - - if (core::CCameraFollowUtilities::cameraFollowModeLocksViewToTarget(followConfig.mode)) - { - out.hasLockMetrics = core::CCameraFollowUtilities::tryComputeFollowTargetLockMetrics(camera->getGimbal(), trackedTarget, out.lockAngleDeg, &out.targetDistance); - if (!out.hasLockMetrics) - { - if (error) - *error = "failed to compute follow lock metrics"; - return false; - } - - const auto expectedTargetDistance = hlsl::length(trackedTarget.getGimbal().getPosition() - camera->getGimbal().getPosition()); - if (!hlsl::CCameraMathUtilities::isFiniteScalar(expectedTargetDistance) || hlsl::abs(expectedTargetDistance - out.targetDistance) > thresholds.distanceTolerance) - { - if (error) - { - *error = "target distance mismatch actual=" + std::to_string(out.targetDistance) + - " expected=" + std::to_string(expectedTargetDistance); - } - return false; - } - - if (out.lockAngleDeg > thresholds.lockAngleToleranceDeg) - { - if (error) - *error = "lock angle mismatch angle_deg=" + std::to_string(out.lockAngleDeg); - return false; - } - - if (projectionContext) - { - out.hasProjectedMetrics = tryComputeProjectedFollowTargetMetrics( - *projectionContext, - trackedTarget, - out.projectedTarget, - thresholds.clipWEpsilon); - if (!out.hasProjectedMetrics) - { - if (error) - *error = "failed to compute projected follow target metrics"; - return false; - } - - if (out.projectedTarget.radius > thresholds.projectedNdcTolerance) - { - if (error) - *error = "projected target mismatch ndc=(" + std::to_string(out.projectedTarget.ndc.x) + - "," + std::to_string(out.projectedTarget.ndc.y) + ") radius=" + std::to_string(out.projectedTarget.radius); - return false; - } - } - } - - if (camera->supportsGoalState(core::ICamera::GoalStateSphericalTarget)) - { - core::ICamera::SphericalTargetState state; - if (!camera->tryGetSphericalTargetState(state)) - { - if (error) - *error = "missing spherical target state"; - return false; - } - - out.hasSphericalState = true; - out.sphericalTarget = state.target; - out.sphericalDistance = state.distance; - - const auto trackedTargetPosition = trackedTarget.getGimbal().getPosition(); - const auto targetDelta = state.target - trackedTargetPosition; - const auto targetDeltaLen = hlsl::length(targetDelta); - if (!hlsl::CCameraMathUtilities::isFiniteScalar(targetDeltaLen) || targetDeltaLen > thresholds.targetTolerance) - { - if (error) - *error = "spherical target writeback mismatch"; - return false; - } - - const auto actualDistance = hlsl::length(camera->getGimbal().getPosition() - trackedTargetPosition); - const auto expectedDistance = followGoal.hasOrbitState ? static_cast(followGoal.orbitDistance) : - (followGoal.hasDistance ? static_cast(followGoal.distance) : actualDistance); - if (!hlsl::CCameraMathUtilities::isFiniteScalar(actualDistance) || !hlsl::CCameraMathUtilities::isFiniteScalar(expectedDistance) || - hlsl::abs(actualDistance - expectedDistance) > thresholds.distanceTolerance || - hlsl::abs(static_cast(state.distance) - expectedDistance) > thresholds.distanceTolerance) - { - if (error) - { - *error = "spherical distance mismatch actual=" + std::to_string(actualDistance) + - " state=" + std::to_string(state.distance) + - " expected=" + std::to_string(expectedDistance); - } - return false; - } - } - - out.passed = true; - return true; - } - - static inline bool buildApplyAndValidateFollowTargetContract( - const core::CCameraGoalSolver& solver, - core::ICamera* camera, - const core::CTrackedTarget& trackedTarget, - const core::SCameraFollowConfig& followConfig, - SCameraFollowApplyValidationResult& out, - std::string* error = nullptr, - const SCameraProjectionContext* projectionContext = nullptr, - const SCameraFollowRegressionThresholds& thresholds = {}) - { - out = {}; - - if (!core::CCameraFollowUtilities::tryBuildFollowGoal(solver, camera, trackedTarget, followConfig, out.goal)) - { - if (error) - *error = "failed to build follow goal"; - return false; - } - out.hasGoal = true; - - out.applyResult = core::CCameraFollowUtilities::applyFollowToCamera(solver, camera, trackedTarget, followConfig); - if (!out.applyResult.succeeded()) - { - if (error) - *error = "failed to apply follow goal"; - return false; - } - - const auto capture = solver.captureDetailed(camera); - if (!capture.canUseGoal()) - { - if (error) - *error = "failed to capture camera state after follow apply"; - return false; - } - - out.hasCapturedGoal = true; - out.capturedGoal = capture.goal; - if (!core::CCameraGoalUtilities::compareGoals(out.capturedGoal, out.goal, thresholds.positionTolerance, thresholds.rotationToleranceDeg, thresholds.scalarTolerance)) - { - if (error) - *error = std::string("follow goal mismatch. ") + core::CCameraGoalUtilities::describeGoalMismatch(out.capturedGoal, out.goal); - return false; - } - - if (!validateFollowTargetContract( - camera, - trackedTarget, - followConfig, - out.goal, - out.regression, - error, - projectionContext, - thresholds)) - { - return false; - } - - return true; - } -}; - -} // namespace nbl::system - -#endif // _C_CAMERA_FOLLOW_REGRESSION_UTILITIES_HPP_ - diff --git a/common/include/camera/CCameraFollowUtilities.hpp b/common/include/camera/CCameraFollowUtilities.hpp deleted file mode 100644 index 3f177f5d1..000000000 --- a/common/include/camera/CCameraFollowUtilities.hpp +++ /dev/null @@ -1,391 +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 _C_CAMERA_FOLLOW_UTILITIES_HPP_ -#define _C_CAMERA_FOLLOW_UTILITIES_HPP_ - -#include -#include - -#include "CCameraGoalSolver.hpp" -#include "CCameraTargetRelativeUtilities.hpp" -#include "CCameraKindUtilities.hpp" - -namespace nbl::core -{ - -/// @brief Reusable tracked-target and follow helpers. -/// -/// The tracked subject owns its own gimbal. Follow code reads that pose and -/// maps one camera plus one tracked target into a `CCameraGoal`. -class CTrackedTarget -{ -public: - using gimbal_t = ICamera::CGimbal; - - /// @brief Construct a tracked target from an initial pose and optional identifier. - CTrackedTarget( - const hlsl::float64_t3& position = hlsl::float64_t3(0.0), - const hlsl::camera_quaternion_t& orientation = hlsl::CCameraMathUtilities::makeIdentityQuaternion(), - std::string identifier = "Follow Target") - : m_identifier(std::move(identifier)), - m_gimbal({ .position = position, .orientation = orientation }) - { - m_gimbal.updateView(); - } - - /// @brief Return the stable human-readable identifier of the tracked target. - inline const std::string& getIdentifier() const { return m_identifier; } - /// @brief Return read-only access to the tracked target gimbal. - inline const gimbal_t& getGimbal() const { return m_gimbal; } - /// @brief Return mutable access to the tracked target gimbal. - inline gimbal_t& getGimbal() { return m_gimbal; } - - /// @brief Replace the tracked target pose in world space. - inline void setPose(const hlsl::float64_t3& position, const hlsl::camera_quaternion_t& orientation) - { - m_gimbal.begin(); - m_gimbal.setPosition(position); - m_gimbal.setOrientation(orientation); - m_gimbal.end(); - m_gimbal.updateView(); - } - - /// @brief Replace only the tracked target position. - inline void setPosition(const hlsl::float64_t3& position) - { - setPose(position, m_gimbal.getOrientation()); - } - - /// @brief Replace only the tracked target orientation. - inline void setOrientation(const hlsl::camera_quaternion_t& orientation) - { - setPose(m_gimbal.getPosition(), orientation); - } - - /// @brief Replace the tracked target pose from a rigid transform matrix when possible. - inline bool trySetFromTransform(const hlsl::float64_t4x4& transform) - { - hlsl::float64_t3 position = hlsl::float64_t3(0.0); - hlsl::camera_quaternion_t orientation = hlsl::CCameraMathUtilities::makeIdentityQuaternion(); - if (!hlsl::CCameraMathUtilities::tryExtractRigidPoseFromTransform(transform, position, orientation)) - return false; - - setPose(position, orientation); - return true; - } - -private: - std::string m_identifier; - gimbal_t m_gimbal; -}; - -/// @brief Follow policy layered on top of a tracked target gimbal. -/// -/// Each mode defines how tracked-target motion updates the camera: -/// -/// - `OrbitTarget` rewrites target-relative camera state so the tracked target becomes the camera target -/// - `LookAtTarget` preserves camera position and rebuilds orientation toward the tracked target -/// - `KeepWorldOffset` places the camera at `trackedTarget.position + worldOffset` and looks at the target -/// - `KeepLocalOffset` transforms `localOffset` by the tracked-target local frame and looks at the target -/// -/// The tracked target provides pose data. The camera reads that data and does -/// not own the tracked subject. -enum class ECameraFollowMode : uint8_t -{ - Disabled, - OrbitTarget, - LookAtTarget, - KeepWorldOffset, - KeepLocalOffset -}; - -/// @brief Reusable follow configuration interpreted against a tracked target gimbal. -/// `worldOffset` and `localOffset` are only meaningful for their matching offset-based modes. -struct SCameraFollowConfig -{ - /// @brief Whether follow should be applied at all. - bool enabled = false; - /// @brief Follow policy used when the configuration is enabled. - ECameraFollowMode mode = ECameraFollowMode::OrbitTarget; - /// @brief World-space offset preserved by `KeepWorldOffset`. - hlsl::float64_t3 worldOffset = hlsl::float64_t3(0.0); - /// @brief Target-local offset preserved by `KeepLocalOffset`. - hlsl::float64_t3 localOffset = hlsl::float64_t3(0.0); -}; - -/// @brief Shared policy helpers for tracked-target follow. -/// -/// The helpers decide which follow modes lock the view, which ones move the -/// camera, how offsets are captured, and how a tracked target is translated into -/// a `CCameraGoal` that can then be applied through the shared goal solver. -struct CCameraFollowUtilities final -{ - /// @brief Return whether the follow mode rebuilds camera orientation toward the tracked target. - static inline constexpr bool cameraFollowModeLocksViewToTarget(const ECameraFollowMode mode) - { - switch (mode) - { - case ECameraFollowMode::OrbitTarget: - case ECameraFollowMode::LookAtTarget: - case ECameraFollowMode::KeepWorldOffset: - case ECameraFollowMode::KeepLocalOffset: - return true; - default: - return false; - } - } - - /// @brief Return whether the follow mode moves the camera world position together with the target. - static inline constexpr bool cameraFollowModeMovesCameraPosition(const ECameraFollowMode mode) - { - switch (mode) - { - case ECameraFollowMode::OrbitTarget: - case ECameraFollowMode::KeepWorldOffset: - case ECameraFollowMode::KeepLocalOffset: - return true; - default: - return false; - } - } - - /// @brief Return whether the follow mode preserves the current camera world position. - static inline constexpr bool cameraFollowModeKeepsCameraWorldPosition(const ECameraFollowMode mode) - { - return mode == ECameraFollowMode::LookAtTarget; - } - - /// @brief Return whether the follow mode interprets `worldOffset`. - static inline constexpr bool cameraFollowModeUsesWorldOffset(const ECameraFollowMode mode) - { - return mode == ECameraFollowMode::KeepWorldOffset; - } - - /// @brief Return whether the follow mode interprets `localOffset`. - static inline constexpr bool cameraFollowModeUsesLocalOffset(const ECameraFollowMode mode) - { - return mode == ECameraFollowMode::KeepLocalOffset; - } - - /// @brief Return whether the follow mode needs the tracked target local frame. - static inline constexpr bool cameraFollowModeUsesTrackedTargetLocalFrame(const ECameraFollowMode mode) - { - return mode == ECameraFollowMode::KeepLocalOffset; - } - - /// @brief Return whether the follow mode requires a captured offset before it can be replayed. - static inline constexpr bool cameraFollowModeUsesCapturedOffset(const ECameraFollowMode mode) - { - return cameraFollowModeUsesWorldOffset(mode) || cameraFollowModeUsesLocalOffset(mode); - } - - /// @brief Return the shared default follow mode for one camera kind. - static inline constexpr ECameraFollowMode getDefaultCameraFollowMode(const ICamera::CameraKind kind) - { - switch (kind) - { - case ICamera::CameraKind::Orbit: - case ICamera::CameraKind::Arcball: - case ICamera::CameraKind::Turntable: - case ICamera::CameraKind::TopDown: - case ICamera::CameraKind::Isometric: - case ICamera::CameraKind::DollyZoom: - case ICamera::CameraKind::Path: - return ECameraFollowMode::OrbitTarget; - case ICamera::CameraKind::Chase: - case ICamera::CameraKind::Dolly: - return ECameraFollowMode::KeepLocalOffset; - default: - return ECameraFollowMode::Disabled; - } - } - - /// @brief Build the shared default follow configuration for one camera kind. - static inline constexpr SCameraFollowConfig makeDefaultFollowConfig(const ICamera::CameraKind kind) - { - const auto mode = getDefaultCameraFollowMode(kind); - return { - .enabled = mode != ECameraFollowMode::Disabled, - .mode = mode - }; - } - - /// @brief Build the shared default follow configuration for one concrete camera instance. - static inline constexpr SCameraFollowConfig makeDefaultFollowConfig(const ICamera* const camera) - { - return camera ? makeDefaultFollowConfig(camera->getKind()) : SCameraFollowConfig{}; - } - - /// @brief Transform a tracked-target local offset into world space. - static inline hlsl::float64_t3 transformFollowLocalOffset(const ICamera::CGimbal& gimbal, const hlsl::float64_t3& localOffset) - { - return hlsl::CCameraMathUtilities::rotateVectorByQuaternion(gimbal.getOrientation(), localOffset); - } - - /// @brief Project a world-space offset into the tracked target local frame. - static inline hlsl::float64_t3 projectFollowWorldOffsetToLocal(const ICamera::CGimbal& gimbal, const hlsl::float64_t3& worldOffset) - { - return hlsl::CCameraMathUtilities::projectWorldVectorToLocalQuaternionFrame(gimbal.getOrientation(), worldOffset); - } - - /// @brief Build a look-at orientation that points from `position` toward the tracked target. - static inline bool buildFollowLookAtOrientation( - const hlsl::float64_t3& position, - const hlsl::float64_t3& targetPosition, - const hlsl::float64_t3& preferredUp, - hlsl::camera_quaternion_t& outOrientation) - { - return hlsl::CCameraMathUtilities::tryBuildLookAtOrientation(position, targetPosition, preferredUp, outOrientation); - } - - /// @brief Capture world-space and target-local follow offsets from the current camera pose. - static inline bool captureFollowOffsetsFromCamera( - const CCameraGoalSolver& solver, - ICamera* camera, - const CTrackedTarget& trackedTarget, - SCameraFollowConfig& ioConfig) - { - const auto capture = solver.captureDetailed(camera); - if (!capture.canUseGoal()) - return false; - - const auto& targetGimbal = trackedTarget.getGimbal(); - ioConfig.worldOffset = capture.goal.position - targetGimbal.getPosition(); - ioConfig.localOffset = projectFollowWorldOffsetToLocal(targetGimbal, ioConfig.worldOffset); - return true; - } - - /// @brief Measure the angular lock error between a camera forward axis and a tracked target. - static inline bool tryComputeFollowTargetLockMetrics( - const ICamera::CGimbal& cameraGimbal, - const CTrackedTarget& trackedTarget, - float& outAngleDeg, - double* outDistance = nullptr) - { - const auto toTarget = trackedTarget.getGimbal().getPosition() - cameraGimbal.getPosition(); - const auto targetDistance = hlsl::length(toTarget); - if (!hlsl::CCameraMathUtilities::isFiniteScalar(targetDistance) || targetDistance <= SCameraToolingThresholds::TinyScalarEpsilon) - return false; - - const auto forward = cameraGimbal.getZAxis(); - const auto forwardLength = hlsl::length(forward); - if (!hlsl::CCameraMathUtilities::isFiniteVec3(forward) || !hlsl::CCameraMathUtilities::isFiniteScalar(forwardLength) || forwardLength <= SCameraToolingThresholds::TinyScalarEpsilon) - return false; - - const auto forwardDirection = forward / forwardLength; - const auto targetDir = toTarget / targetDistance; - const auto dotForward = std::clamp(hlsl::dot(forwardDirection, targetDir), -1.0, 1.0); - outAngleDeg = static_cast(hlsl::degrees(hlsl::acos(dotForward))); - if (!hlsl::CCameraMathUtilities::isFiniteScalar(outAngleDeg)) - return false; - - if (outDistance) - *outDistance = targetDistance; - return true; - } - - static inline bool tryBuildFollowPositionGoal( - ICamera* camera, - CCameraGoal& outGoal, - const hlsl::float64_t3& targetPosition, - const hlsl::float64_t3& position, - const hlsl::float64_t3& preferredUp) - { - if (camera->supportsGoalState(ICamera::GoalStateSphericalTarget)) - return CCameraGoalUtilities::buildCanonicalTargetRelativeGoalFromPosition(outGoal, targetPosition, position); - - outGoal.position = position; - return buildFollowLookAtOrientation(outGoal.position, targetPosition, preferredUp, outGoal.orientation) && CCameraGoalUtilities::isGoalFinite(outGoal); - } - - static inline bool tryBuildFollowGoal( - const CCameraGoalSolver& solver, - ICamera* camera, - const CTrackedTarget& trackedTarget, - const SCameraFollowConfig& config, - CCameraGoal& outGoal) - { - if (!camera || !config.enabled || config.mode == ECameraFollowMode::Disabled) - return false; - - const auto capture = solver.captureDetailed(camera); - if (!capture.canUseGoal()) - return false; - - outGoal = capture.goal; - - const auto& targetGimbal = trackedTarget.getGimbal(); - const auto targetPosition = targetGimbal.getPosition(); - - switch (config.mode) - { - case ECameraFollowMode::OrbitTarget: - { - if (!camera->supportsGoalState(ICamera::GoalStateSphericalTarget)) - return false; - - if (outGoal.hasPathState) - { - return CCameraGoalUtilities::applyCanonicalPathGoalFields(outGoal, targetPosition, outGoal.pathState) && CCameraGoalUtilities::isGoalFinite(outGoal); - } - - const bool hasSphericalState = outGoal.hasOrbitState || outGoal.hasDistance; - if (!hasSphericalState) - return false; - - const auto orbitDistance = outGoal.hasOrbitState ? outGoal.orbitDistance : outGoal.distance; - return CCameraGoalUtilities::applyCanonicalTargetRelativeGoal( - outGoal, - { - .target = targetPosition, - .orbitUv = outGoal.orbitUv, - .distance = orbitDistance - }); - } - - case ECameraFollowMode::LookAtTarget: - { - return tryBuildFollowPositionGoal(camera, outGoal, targetPosition, capture.goal.position, targetGimbal.getYAxis()); - } - - case ECameraFollowMode::KeepWorldOffset: - { - const auto position = targetPosition + config.worldOffset; - return tryBuildFollowPositionGoal(camera, outGoal, targetPosition, position, targetGimbal.getYAxis()); - } - - case ECameraFollowMode::KeepLocalOffset: - { - const auto position = targetPosition + transformFollowLocalOffset(targetGimbal, config.localOffset); - return tryBuildFollowPositionGoal(camera, outGoal, targetPosition, position, targetGimbal.getYAxis()); - } - - default: - return false; - } - } - - static inline CCameraGoalSolver::SApplyResult applyFollowToCamera( - const CCameraGoalSolver& solver, - ICamera* camera, - const CTrackedTarget& trackedTarget, - const SCameraFollowConfig& config, - CCameraGoal* outGoal = nullptr) - { - CCameraGoal goal = {}; - if (!tryBuildFollowGoal(solver, camera, trackedTarget, config, goal)) - return {}; - - if (outGoal) - *outGoal = goal; - - return solver.applyDetailed(camera, goal); - } -}; - -} // namespace nbl::core - -#endif // _C_CAMERA_FOLLOW_UTILITIES_HPP_ - diff --git a/common/include/camera/CCameraGoal.hpp b/common/include/camera/CCameraGoal.hpp deleted file mode 100644 index bbf1eae3f..000000000 --- a/common/include/camera/CCameraGoal.hpp +++ /dev/null @@ -1,409 +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 _C_CAMERA_GOAL_HPP_ -#define _C_CAMERA_GOAL_HPP_ - -#include -#include -#include -#include -#include - -#include "CCameraPathUtilities.hpp" -#include "CCameraTargetRelativeUtilities.hpp" -#include "ICamera.hpp" - -namespace nbl::core -{ - -/// @brief Typed transport object for camera state used by capture, comparison, presets, and playback. -struct CCameraGoal : SCameraRigPose -{ - /// @brief Camera kind that originally produced this goal. - ICamera::CameraKind sourceKind = ICamera::CameraKind::Unknown; - /// @brief Capability mask captured from the source camera. - uint32_t sourceCapabilities = ICamera::None; - /// @brief Goal-state fragments that were valid on the source camera. - uint32_t sourceGoalStateMask = ICamera::GoalStateNone; - /// @brief Whether `targetPosition` is present in this goal. - bool hasTargetPosition = false; - /// @brief Tracked target position in world space. - hlsl::float64_t3 targetPosition = hlsl::float64_t3(0.0); - /// @brief Whether `distance` is present in this goal. - bool hasDistance = false; - /// @brief Explicit target-relative distance when present. - float distance = 0.f; - /// @brief Whether the canonical orbit state is present in this goal. - bool hasOrbitState = false; - /// @brief Canonical orbit yaw and pitch, expressed in radians. - hlsl::float64_t2 orbitUv = hlsl::float64_t2(0.0); - /// @brief Distance associated with `orbitUv` when the orbit state is present. - float orbitDistance = 0.f; - /// @brief Whether a typed path state is present in this goal. - bool hasPathState = false; - /// @brief Typed path state captured from or authored for a `Path Rig` camera. - ICamera::PathState pathState = {}; - /// @brief Whether a dynamic perspective state is present in this goal. - bool hasDynamicPerspectiveState = false; - /// @brief Typed dynamic perspective state captured from or authored for the source camera. - ICamera::DynamicPerspectiveState dynamicPerspectiveState = {}; -}; - -/// @brief Shared canonicalization, comparison, and conversion helpers for `CCameraGoal`. -struct CCameraGoalUtilities final -{ -public: - /// @brief Compute which typed goal-state fragments are required by the current goal payload. - static inline uint32_t getRequiredGoalStateMask(const CCameraGoal& target) - { - uint32_t mask = ICamera::GoalStateNone; - if (target.hasTargetPosition || target.hasDistance || target.hasOrbitState) - mask |= ICamera::GoalStateSphericalTarget; - if (target.hasDynamicPerspectiveState) - mask |= ICamera::GoalStateDynamicPerspective; - if (target.hasPathState) - mask |= ICamera::GoalStatePath; - return mask; - } - - /// @brief Overwrite the canonical target-relative fields of a goal from prebuilt state and pose data. - static inline void applyCanonicalTargetRelativeGoalFields( - CCameraGoal& goal, - const SCameraTargetRelativeState& state, - const SCameraTargetRelativePose& pose) - { - goal.position = pose.position; - goal.orientation = pose.orientation; - goal.hasTargetPosition = true; - goal.targetPosition = state.target; - goal.hasDistance = true; - goal.distance = static_cast(pose.appliedDistance); - goal.hasOrbitState = true; - goal.orbitUv = state.orbitUv; - goal.orbitDistance = static_cast(pose.appliedDistance); - } - - /// @brief Rebuild the canonical target-relative portion of a goal from typed target-relative state. - static inline bool applyCanonicalTargetRelativeGoal(CCameraGoal& goal, const SCameraTargetRelativeState& state) - { - SCameraTargetRelativePose pose = {}; - if (!CCameraTargetRelativeUtilities::tryBuildTargetRelativePoseFromState(state, SCameraTargetRelativeTraits::MinDistance, SCameraTargetRelativeTraits::DefaultMaxDistance, pose)) - return false; - - applyCanonicalTargetRelativeGoalFields(goal, state, pose); - return true; - } - - /// @brief Rebuild the canonical pose and orbit fields of a goal from typed path state. - static inline bool applyCanonicalPathGoalFields( - CCameraGoal& goal, - const hlsl::float64_t3& targetPosition, - const ICamera::PathState& pathState, - const SCameraPathLimits& limits = CCameraPathUtilities::makeDefaultPathLimits()) - { - SCameraCanonicalPathState canonicalPathState = {}; - if (!CCameraPathUtilities::tryBuildCanonicalPathState(targetPosition, pathState, limits, canonicalPathState)) - return false; - - goal.hasTargetPosition = true; - goal.targetPosition = targetPosition; - goal.hasPathState = true; - goal.pathState = pathState; - SCameraTargetRelativePose canonicalPose = {}; - canonicalPose.position = canonicalPathState.pose.position; - canonicalPose.orientation = canonicalPathState.pose.orientation; - canonicalPose.appliedDistance = canonicalPathState.pose.appliedDistance; - applyCanonicalTargetRelativeGoalFields( - goal, - canonicalPathState.targetRelative, - canonicalPose); - return true; - } - - /// @brief Rebuild the canonical pose fields from the goal's current spherical-target payload. - static inline bool applyCanonicalSphericalGoal(CCameraGoal& goal) - { - if (!(goal.hasTargetPosition && goal.hasOrbitState)) - return false; - if (!hlsl::CCameraMathUtilities::isFiniteScalar(goal.orbitUv.x) || !hlsl::CCameraMathUtilities::isFiniteScalar(goal.orbitUv.y) || !hlsl::CCameraMathUtilities::isFiniteScalar(goal.orbitDistance)) - return false; - - return applyCanonicalTargetRelativeGoal( - goal, - { - .target = goal.targetPosition, - .orbitUv = goal.orbitUv, - .distance = goal.orbitDistance - }); - } - - /// @brief Infer a target-relative goal from a target position and a desired camera position. - static inline bool buildCanonicalTargetRelativeGoalFromPosition( - CCameraGoal& goal, - const hlsl::float64_t3& targetPosition, - const hlsl::float64_t3& position) - { - SCameraTargetRelativeState state = {}; - if (!CCameraTargetRelativeUtilities::tryBuildTargetRelativeStateFromPosition( - targetPosition, - position, - SCameraTargetRelativeTraits::MinDistance, - SCameraTargetRelativeTraits::DefaultMaxDistance, - state)) - { - return false; - } - - return applyCanonicalTargetRelativeGoal(goal, state); - } - - /// @brief Resolve the effective target-relative state of a goal against the current camera state. - static inline bool tryResolveCanonicalTargetRelativeState( - const CCameraGoal& goal, - const ICamera::SphericalTargetState& currentState, - SCameraTargetRelativeState& outState) - { - outState.target = goal.hasTargetPosition ? goal.targetPosition : currentState.target; - outState.orbitUv = currentState.orbitUv; - outState.distance = currentState.distance; - - if (goal.hasOrbitState) - { - outState.orbitUv = goal.orbitUv; - outState.distance = goal.orbitDistance; - } - else - { - SCameraTargetRelativeState resolvedState = {}; - if (!CCameraTargetRelativeUtilities::tryBuildTargetRelativeStateFromPosition( - outState.target, - goal.position, - currentState.minDistance, - currentState.maxDistance, - resolvedState)) - { - return false; - } - - outState.orbitUv = resolvedState.orbitUv; - outState.distance = resolvedState.distance; - } - - if (goal.hasDistance && !goal.hasOrbitState) - outState.distance = goal.distance; - - outState.distance = std::clamp(outState.distance, currentState.minDistance, currentState.maxDistance); - return true; - } - - /// @brief Rebuild the canonical pose fields from the goal's current path payload. - static inline bool applyCanonicalPathGoal(CCameraGoal& goal) - { - if (!(goal.hasPathState && goal.hasTargetPosition)) - return false; - if (!CCameraPathUtilities::isPathStateFinite(goal.pathState)) - return false; - return applyCanonicalPathGoalFields(goal, goal.targetPosition, goal.pathState); - } - - /// @brief Canonicalize whichever typed state fragments are currently present on the goal. - static inline bool applyCanonicalGoalState(CCameraGoal& goal) - { - if (goal.hasPathState) - return applyCanonicalPathGoal(goal); - - if (goal.hasTargetPosition && goal.hasOrbitState) - return applyCanonicalSphericalGoal(goal); - - return true; - } - - /// @brief Return a value-copied goal after canonicalizing its typed state. - static inline CCameraGoal canonicalizeGoal(CCameraGoal goal) - { - applyCanonicalGoalState(goal); - return goal; - } - - /// @brief Check whether every populated scalar and vector stored by the goal is finite. - static inline bool isGoalFinite(const CCameraGoal& goal) - { - if (!hlsl::CCameraMathUtilities::isFiniteVec3(goal.position) || !hlsl::CCameraMathUtilities::isFiniteQuaternion(goal.orientation)) - return false; - if (goal.hasTargetPosition && !hlsl::CCameraMathUtilities::isFiniteVec3(goal.targetPosition)) - return false; - if (goal.hasDistance && !hlsl::CCameraMathUtilities::isFiniteScalar(goal.distance)) - return false; - if (goal.hasOrbitState && (!hlsl::CCameraMathUtilities::isFiniteScalar(goal.orbitUv.x) || !hlsl::CCameraMathUtilities::isFiniteScalar(goal.orbitUv.y) || !hlsl::CCameraMathUtilities::isFiniteScalar(goal.orbitDistance))) - return false; - if (goal.hasPathState && !CCameraPathUtilities::isPathStateFinite(goal.pathState)) - return false; - if (goal.hasDynamicPerspectiveState && - (!hlsl::CCameraMathUtilities::isFiniteScalar(goal.dynamicPerspectiveState.baseFov) || !hlsl::CCameraMathUtilities::isFiniteScalar(goal.dynamicPerspectiveState.referenceDistance))) - return false; - return true; - } - - /// @brief Compare two goals using caller-provided pose and scalar tolerances. - static inline bool compareGoals(const CCameraGoal& actual, const CCameraGoal& expected, - const double posEps, const double rotEpsDeg, const double scalarEps) - { - hlsl::SCameraPoseDelta poseDelta = {}; - if (!hlsl::CCameraMathUtilities::tryComputePoseDelta(actual.position, actual.orientation, expected.position, expected.orientation, poseDelta)) - return false; - if (poseDelta.position > posEps || poseDelta.rotationDeg > rotEpsDeg) - return false; - - if (expected.hasTargetPosition) - { - if (!actual.hasTargetPosition || !hlsl::CCameraMathUtilities::nearlyEqualVec3(actual.targetPosition, expected.targetPosition, scalarEps)) - return false; - } - if (expected.hasDistance) - { - if (!actual.hasDistance || !hlsl::CCameraMathUtilities::nearlyEqualScalar(static_cast(actual.distance), static_cast(expected.distance), scalarEps)) - return false; - } - if (expected.hasOrbitState) - { - if (!actual.hasOrbitState) - return false; - if (hlsl::CCameraMathUtilities::getWrappedAngleDistanceDegrees(expected.orbitUv.x, actual.orbitUv.x) > rotEpsDeg) - return false; - if (hlsl::CCameraMathUtilities::getWrappedAngleDistanceDegrees(expected.orbitUv.y, actual.orbitUv.y) > rotEpsDeg) - return false; - if (!hlsl::CCameraMathUtilities::nearlyEqualScalar(static_cast(actual.orbitDistance), static_cast(expected.orbitDistance), scalarEps)) - return false; - } - if (expected.hasPathState) - { - if (!actual.hasPathState) - return false; - if (!CCameraPathUtilities::pathStatesNearlyEqual(actual.pathState, expected.pathState, CCameraPathUtilities::makePathComparisonThresholds(rotEpsDeg, scalarEps))) - return false; - } - if (expected.hasDynamicPerspectiveState) - { - if (!actual.hasDynamicPerspectiveState) - return false; - if (!hlsl::CCameraMathUtilities::nearlyEqualScalar(static_cast(actual.dynamicPerspectiveState.baseFov), static_cast(expected.dynamicPerspectiveState.baseFov), scalarEps)) - return false; - if (!hlsl::CCameraMathUtilities::nearlyEqualScalar(static_cast(actual.dynamicPerspectiveState.referenceDistance), static_cast(expected.dynamicPerspectiveState.referenceDistance), scalarEps)) - return false; - } - - return true; - } - - static inline std::string describeGoalMismatch(const CCameraGoal& actual, const CCameraGoal& expected) - { - std::ostringstream oss; - hlsl::SCameraPoseDelta poseDelta = {}; - const bool hasPoseDelta = hlsl::CCameraMathUtilities::tryComputePoseDelta(actual.position, actual.orientation, expected.position, expected.orientation, poseDelta); - const auto currentOrientation = hlsl::CCameraMathUtilities::normalizeQuaternion(actual.orientation); - const auto expectedOrientation = hlsl::CCameraMathUtilities::normalizeQuaternion(expected.orientation); - oss << "pos_delta=" << (hasPoseDelta ? poseDelta.position : std::numeric_limits::quiet_NaN()) - << " rot_delta_deg=" << (hasPoseDelta ? poseDelta.rotationDeg : std::numeric_limits::quiet_NaN()) - << " current_pos=(" << actual.position.x << "," << actual.position.y << "," << actual.position.z << ")" - << " expected_pos=(" << expected.position.x << "," << expected.position.y << "," << expected.position.z << ")" - << " current_quat=(" << currentOrientation.data.x << "," << currentOrientation.data.y << "," << currentOrientation.data.z << "," << currentOrientation.data.w << ")" - << " expected_quat=(" << expectedOrientation.data.x << "," << expectedOrientation.data.y << "," << expectedOrientation.data.z << "," << expectedOrientation.data.w << ")"; - - if (actual.hasTargetPosition) - { - oss << " target=(" << actual.targetPosition.x << "," << actual.targetPosition.y << "," << actual.targetPosition.z << ")"; - if (actual.hasDistance) - oss << " distance=" << actual.distance; - if (actual.hasOrbitState) - oss << " orbit_u=" << actual.orbitUv.x << " orbit_v=" << actual.orbitUv.y; - } - else if (expected.hasTargetPosition || expected.hasDistance || expected.hasOrbitState) - { - oss << " spherical_state=unavailable"; - } - if (actual.hasPathState) - { - oss << " path_s=" << actual.pathState.s - << " path_u=" << actual.pathState.u - << " path_v=" << actual.pathState.v - << " path_roll=" << actual.pathState.roll; - } - else if (expected.hasPathState) - { - oss << " path_state=unavailable"; - } - - if (actual.hasDynamicPerspectiveState) - { - oss << " dynamic_base_fov=" << actual.dynamicPerspectiveState.baseFov - << " dynamic_reference_distance=" << actual.dynamicPerspectiveState.referenceDistance; - } - else if (expected.hasDynamicPerspectiveState) - { - oss << " dynamic_perspective_state=unavailable"; - } - - return oss.str(); - } - - static inline CCameraGoal blendGoals(const CCameraGoal& a, const CCameraGoal& b, double alpha) - { - CCameraGoal blended; - blended.position = a.position + (b.position - a.position) * alpha; - blended.orientation = hlsl::CCameraMathUtilities::slerpQuaternion(a.orientation, b.orientation, static_cast(alpha)); - blended.sourceKind = (a.sourceKind == b.sourceKind) ? a.sourceKind : ICamera::CameraKind::Unknown; - blended.sourceCapabilities = a.sourceCapabilities & b.sourceCapabilities; - blended.sourceGoalStateMask = a.sourceGoalStateMask | b.sourceGoalStateMask; - blended.hasTargetPosition = a.hasTargetPosition || b.hasTargetPosition; - if (blended.hasTargetPosition) - { - const auto ta = a.hasTargetPosition ? a.targetPosition : b.targetPosition; - const auto tb = b.hasTargetPosition ? b.targetPosition : a.targetPosition; - blended.targetPosition = ta + (tb - ta) * alpha; - } - blended.hasDistance = a.hasDistance || b.hasDistance; - if (blended.hasDistance) - { - const float da = a.hasDistance ? a.distance : b.distance; - const float db = b.hasDistance ? b.distance : a.distance; - blended.distance = da + (db - da) * static_cast(alpha); - } - blended.hasOrbitState = a.hasOrbitState || b.hasOrbitState; - if (blended.hasOrbitState) - { - const auto orbitUvA = a.hasOrbitState ? a.orbitUv : b.orbitUv; - const auto orbitUvB = b.hasOrbitState ? b.orbitUv : a.orbitUv; - const float da = a.hasOrbitState ? a.orbitDistance : b.orbitDistance; - const float db = b.hasOrbitState ? b.orbitDistance : a.orbitDistance; - - blended.orbitUv = hlsl::float64_t2( - hlsl::CCameraMathUtilities::lerpWrappedAngleRad(orbitUvA.x, orbitUvB.x, alpha), - hlsl::CCameraMathUtilities::lerpWrappedAngleRad(orbitUvA.y, orbitUvB.y, alpha)); - blended.orbitDistance = da + (db - da) * static_cast(alpha); - } - blended.hasDynamicPerspectiveState = a.hasDynamicPerspectiveState || b.hasDynamicPerspectiveState; - if (blended.hasDynamicPerspectiveState) - { - const auto dynamicA = a.hasDynamicPerspectiveState ? a.dynamicPerspectiveState : b.dynamicPerspectiveState; - const auto dynamicB = b.hasDynamicPerspectiveState ? b.dynamicPerspectiveState : a.dynamicPerspectiveState; - blended.dynamicPerspectiveState.baseFov = dynamicA.baseFov + (dynamicB.baseFov - dynamicA.baseFov) * static_cast(alpha); - blended.dynamicPerspectiveState.referenceDistance = - dynamicA.referenceDistance + (dynamicB.referenceDistance - dynamicA.referenceDistance) * static_cast(alpha); - } - blended.hasPathState = a.hasPathState || b.hasPathState; - if (blended.hasPathState) - { - const auto pathA = a.hasPathState ? a.pathState : b.pathState; - const auto pathB = b.hasPathState ? b.pathState : a.pathState; - blended.pathState = CCameraPathUtilities::blendPathStates(pathA, pathB, alpha); - } - return canonicalizeGoal(blended); - } -}; - -} // namespace nbl::core - -#endif // _C_CAMERA_GOAL_HPP_ - diff --git a/common/include/camera/CCameraGoalAnalysis.hpp b/common/include/camera/CCameraGoalAnalysis.hpp deleted file mode 100644 index 4653cc318..000000000 --- a/common/include/camera/CCameraGoalAnalysis.hpp +++ /dev/null @@ -1,89 +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 _C_CAMERA_GOAL_ANALYSIS_HPP_ -#define _C_CAMERA_GOAL_ANALYSIS_HPP_ - -#include "CCameraPreset.hpp" -#include "CCameraGoalSolver.hpp" - -namespace nbl::core -{ - -/// @brief Reusable typed answer for `goal/preset -> camera` compatibility checks. -struct SCameraGoalApplyAnalysis -{ - CCameraGoal goal = {}; - CCameraGoalSolver::SCompatibilityResult compatibility = {}; - bool hasCamera = false; - bool finiteGoal = false; - bool canApply = false; - - inline bool exact() const - { - return compatibility.exact; - } - - inline bool dropsGoalState() const - { - return compatibility.missingGoalStateMask != ICamera::GoalStateNone; - } - - inline bool usesSharedStateOnly() const - { - return !compatibility.sameKind && goal.sourceKind != ICamera::CameraKind::Unknown && !dropsGoalState(); - } - - inline bool isMeaningfulApply() const - { - return canApply; - } -}; - -/// @brief Reusable typed answer for `camera -> goal` capture viability. -struct SCameraCaptureAnalysis -{ - CCameraGoal goal = {}; - bool hasCamera = false; - bool capturedGoal = false; - bool finiteGoal = false; - bool canCapture = false; -}; - -struct CCameraGoalAnalysisUtilities final -{ -public: - static inline SCameraGoalApplyAnalysis analyzeGoalApply(const CCameraGoalSolver& solver, const ICamera* camera, const CCameraGoal& goal) - { - SCameraGoalApplyAnalysis analysis; - analysis.goal = CCameraGoalUtilities::canonicalizeGoal(goal); - analysis.hasCamera = camera != nullptr; - analysis.finiteGoal = CCameraGoalUtilities::isGoalFinite(analysis.goal); - analysis.canApply = analysis.hasCamera && analysis.finiteGoal; - if (analysis.hasCamera) - analysis.compatibility = solver.analyzeCompatibility(camera, analysis.goal); - return analysis; - } - - static inline SCameraGoalApplyAnalysis analyzePresetApply(const CCameraGoalSolver& solver, const ICamera* camera, const CCameraPreset& preset) - { - return analyzeGoalApply(solver, camera, CCameraPresetUtilities::makeGoalFromPreset(preset)); - } - - static inline SCameraCaptureAnalysis analyzeCameraCapture(const CCameraGoalSolver& solver, ICamera* camera) - { - SCameraCaptureAnalysis analysis; - const auto capture = solver.captureDetailed(camera); - analysis.goal = capture.goal; - analysis.hasCamera = capture.hasCamera; - analysis.capturedGoal = capture.captured; - analysis.finiteGoal = capture.finiteGoal; - analysis.canCapture = capture.canUseGoal(); - return analysis; - } -}; - -} // namespace nbl::core - -#endif // _C_CAMERA_GOAL_ANALYSIS_HPP_ diff --git a/common/include/camera/CCameraGoalSolver.hpp b/common/include/camera/CCameraGoalSolver.hpp deleted file mode 100644 index 7132354b0..000000000 --- a/common/include/camera/CCameraGoalSolver.hpp +++ /dev/null @@ -1,646 +0,0 @@ -#ifndef _C_CAMERA_GOAL_SOLVER_HPP_ -#define _C_CAMERA_GOAL_SOLVER_HPP_ - -#include -#include -#include -#include -#include - -#include "CCameraGoal.hpp" -#include "CCameraTargetRelativeUtilities.hpp" -#include "CCameraVirtualEventUtilities.hpp" - -namespace nbl::core -{ - -/// @brief Goal capture, compatibility analysis, and goal application helper. -/// -/// The solver captures canonical state into `CCameraGoal`, compares a goal -/// against one target camera, applies typed fragments directly when the camera -/// exposes them, and builds virtual-event replay when a typed fragment must be -/// approximated through `manipulate(...)`. -class CCameraGoalSolver -{ -public: - /// @brief Detailed result returned by one goal-capture attempt. - struct SCaptureResult - { - bool hasCamera = false; - bool captured = false; - bool finiteGoal = false; - CCameraGoal goal = {}; - - inline bool canUseGoal() const - { - return hasCamera && captured && finiteGoal; - } - }; - - /// @brief Compatibility of a goal with a target camera kind and state mask. - struct SCompatibilityResult - { - bool sameKind = false; - bool exact = false; - uint32_t requiredGoalStateMask = ICamera::GoalStateNone; - uint32_t supportedGoalStateMask = ICamera::GoalStateNone; - uint32_t missingGoalStateMask = ICamera::GoalStateNone; - }; - - /// @brief Outcome of one goal-application attempt. - struct SApplyResult - { - enum class EStatus : uint8_t - { - Unsupported, - Failed, - AlreadySatisfied, - AppliedAbsoluteOnly, - AppliedVirtualEvents, - AppliedAbsoluteAndVirtualEvents - }; - - enum EIssue : uint32_t - { - NoIssue = 0u, - UsedAbsolutePoseFallback = 1u << 0, - MissingSphericalTargetState = 1u << 1, - MissingPathState = 1u << 2, - MissingDynamicPerspectiveState = 1u << 3, - VirtualEventReplayFailed = 1u << 4 - }; - - EStatus status = EStatus::Unsupported; - bool exact = false; - uint32_t eventCount = 0u; - uint32_t issues = NoIssue; - - inline bool succeeded() const - { - return status != EStatus::Unsupported && status != EStatus::Failed; - } - - inline bool changed() const - { - return status == EStatus::AppliedAbsoluteOnly || - status == EStatus::AppliedVirtualEvents || - status == EStatus::AppliedAbsoluteAndVirtualEvents; - } - - inline bool approximate() const - { - return succeeded() && !exact; - } - - inline bool hasIssue(EIssue issue) const - { - return (issues & issue) == issue; - } - }; - - bool buildEvents(ICamera* camera, const CCameraGoal& target, std::vector& out) const - { - out.clear(); - if (!camera) - return false; - - const auto canonicalTarget = CCameraGoalUtilities::canonicalizeGoal(target); - - if (camera->hasCapability(ICamera::SphericalTarget)) - return buildSphericalEvents(camera, canonicalTarget, out); - - return buildFreeEvents(camera, canonicalTarget, out); - } - - bool capture(ICamera* camera, CCameraGoal& out) const - { - out = {}; - if (!camera) - return false; - - const auto& gimbal = camera->getGimbal(); - out.position = gimbal.getPosition(); - out.orientation = gimbal.getOrientation(); - out.sourceKind = camera->getKind(); - out.sourceCapabilities = camera->getCapabilities(); - out.sourceGoalStateMask = camera->getGoalStateMask(); - - ICamera::SphericalTargetState sphericalState; - if (camera->tryGetSphericalTargetState(sphericalState)) - { - out.targetPosition = sphericalState.target; - out.hasTargetPosition = true; - out.distance = sphericalState.distance; - out.hasDistance = true; - out.orbitDistance = sphericalState.distance; - out.orbitUv = sphericalState.orbitUv; - out.hasOrbitState = true; - } - - ICamera::DynamicPerspectiveState dynamicState; - if (camera->tryGetDynamicPerspectiveState(dynamicState)) - { - out.hasDynamicPerspectiveState = true; - out.dynamicPerspectiveState = dynamicState; - } - - ICamera::PathState pathState; - if (camera->tryGetPathState(pathState)) - { - out.hasPathState = true; - out.pathState = pathState; - } - - out = CCameraGoalUtilities::canonicalizeGoal(out); - return true; - } - - SCaptureResult captureDetailed(ICamera* camera) const - { - SCaptureResult result; - result.hasCamera = camera != nullptr; - if (!result.hasCamera) - return result; - - result.captured = capture(camera, result.goal); - result.finiteGoal = result.captured && CCameraGoalUtilities::isGoalFinite(result.goal); - return result; - } - - SCompatibilityResult analyzeCompatibility(const ICamera* camera, const CCameraGoal& target) const - { - SCompatibilityResult result; - if (!camera) - return result; - - const auto canonicalTarget = CCameraGoalUtilities::canonicalizeGoal(target); - result.sameKind = canonicalTarget.sourceKind == ICamera::CameraKind::Unknown || canonicalTarget.sourceKind == camera->getKind(); - result.supportedGoalStateMask = camera->getGoalStateMask(); - result.requiredGoalStateMask = CCameraGoalUtilities::getRequiredGoalStateMask(canonicalTarget); - result.missingGoalStateMask = result.requiredGoalStateMask & ~result.supportedGoalStateMask; - result.exact = result.missingGoalStateMask == ICamera::GoalStateNone; - return result; - } - - SApplyResult applyDetailed(ICamera* camera, const CCameraGoal& target) const - { - SApplyResult result; - if (!camera) - return result; - - const auto canonicalTarget = CCameraGoalUtilities::canonicalizeGoal(target); - - bool exact = true; - bool absoluteChanged = false; - - if (!camera->hasCapability(ICamera::SphericalTarget)) - { - bool poseChanged = false; - bool poseExact = false; - if (tryApplyAbsoluteReferencePose(camera, canonicalTarget, poseChanged, poseExact)) - { - result.issues |= SApplyResult::UsedAbsolutePoseFallback; - absoluteChanged = absoluteChanged || poseChanged; - if (poseExact && !canonicalTarget.hasDynamicPerspectiveState) - { - result.status = poseChanged ? - SApplyResult::EStatus::AppliedAbsoluteOnly : - SApplyResult::EStatus::AlreadySatisfied; - result.exact = true; - return result; - } - } - } - - if (canonicalTarget.hasTargetPosition) - { - ICamera::SphericalTargetState beforeState; - if (!camera->tryGetSphericalTargetState(beforeState)) - { - result.issues |= SApplyResult::MissingSphericalTargetState; - exact = false; - } - else - { - const auto beforeTarget = beforeState.target; - if (!camera->trySetSphericalTarget(canonicalTarget.targetPosition)) - { - result.issues |= SApplyResult::MissingSphericalTargetState; - exact = false; - } - else - { - ICamera::SphericalTargetState afterState; - if (!camera->tryGetSphericalTargetState(afterState)) - { - result.issues |= SApplyResult::MissingSphericalTargetState; - exact = false; - } - else - { - absoluteChanged = afterState.target != beforeTarget; - exact = exact && afterState.target == canonicalTarget.targetPosition; - } - } - } - } - - if (canonicalTarget.hasDistance || canonicalTarget.hasOrbitState) - { - ICamera::SphericalTargetState beforeState; - if (!camera->tryGetSphericalTargetState(beforeState)) - { - result.issues |= SApplyResult::MissingSphericalTargetState; - exact = false; - } - else - { - const float desiredDistance = canonicalTarget.hasOrbitState ? canonicalTarget.orbitDistance : canonicalTarget.distance; - const float beforeDistance = beforeState.distance; - if (!camera->trySetSphericalDistance(desiredDistance)) - { - result.issues |= SApplyResult::MissingSphericalTargetState; - exact = false; - } - else - { - ICamera::SphericalTargetState afterState; - if (!camera->tryGetSphericalTargetState(afterState)) - { - result.issues |= SApplyResult::MissingSphericalTargetState; - exact = false; - } - else - { - absoluteChanged = absoluteChanged || afterState.distance != beforeDistance; - exact = exact && hlsl::abs(static_cast(afterState.distance - desiredDistance)) <= SCameraToolingThresholds::ScalarTolerance; - } - } - } - } - - if (canonicalTarget.hasPathState) - { - ICamera::PathState beforeState; - if (!camera->tryGetPathState(beforeState)) - { - result.issues |= SApplyResult::MissingPathState; - exact = false; - } - else if (!camera->trySetPathState(canonicalTarget.pathState)) - { - result.issues |= SApplyResult::MissingPathState; - exact = false; - } - else - { - ICamera::PathState afterState; - if (!camera->tryGetPathState(afterState)) - { - result.issues |= SApplyResult::MissingPathState; - exact = false; - } - else - { - const auto thresholds = SCameraPathDefaults::ComparisonThresholds; - const bool pathChanged = CCameraPathUtilities::pathStatesChanged(beforeState, afterState, thresholds); - const bool pathExact = CCameraPathUtilities::pathStatesNearlyEqual(afterState, canonicalTarget.pathState, thresholds); - - absoluteChanged = absoluteChanged || pathChanged; - exact = exact && pathExact; - } - } - } - - if (canonicalTarget.hasDynamicPerspectiveState) - { - ICamera::DynamicPerspectiveState beforeState; - if (!camera->tryGetDynamicPerspectiveState(beforeState)) - { - result.issues |= SApplyResult::MissingDynamicPerspectiveState; - exact = false; - } - else if (!camera->trySetDynamicPerspectiveState(canonicalTarget.dynamicPerspectiveState)) - { - result.issues |= SApplyResult::MissingDynamicPerspectiveState; - exact = false; - } - else - { - ICamera::DynamicPerspectiveState afterState; - if (!camera->tryGetDynamicPerspectiveState(afterState)) - { - result.issues |= SApplyResult::MissingDynamicPerspectiveState; - exact = false; - } - else - { - const bool dynamicChanged = !hlsl::CCameraMathUtilities::nearlyEqualScalar(beforeState.baseFov, afterState.baseFov, static_cast(SCameraToolingThresholds::ScalarTolerance)) || - !hlsl::CCameraMathUtilities::nearlyEqualScalar(beforeState.referenceDistance, afterState.referenceDistance, static_cast(SCameraToolingThresholds::ScalarTolerance)); - const bool dynamicExact = hlsl::CCameraMathUtilities::nearlyEqualScalar(afterState.baseFov, canonicalTarget.dynamicPerspectiveState.baseFov, static_cast(SCameraToolingThresholds::ScalarTolerance)) && - hlsl::CCameraMathUtilities::nearlyEqualScalar(afterState.referenceDistance, canonicalTarget.dynamicPerspectiveState.referenceDistance, static_cast(SCameraToolingThresholds::ScalarTolerance)); - - absoluteChanged = absoluteChanged || dynamicChanged; - exact = exact && dynamicExact; - } - } - } - - std::vector events; - buildEvents(camera, canonicalTarget, events); - result.eventCount = static_cast(events.size()); - result.exact = exact; - - if (events.empty()) - { - if (absoluteChanged) - result.status = SApplyResult::EStatus::AppliedAbsoluteOnly; - else if (exact) - result.status = SApplyResult::EStatus::AlreadySatisfied; - return result; - } - - if (camera->manipulate({ events.data(), events.size() })) - { - result.status = absoluteChanged ? - SApplyResult::EStatus::AppliedAbsoluteAndVirtualEvents : - SApplyResult::EStatus::AppliedVirtualEvents; - return result; - } - - if (absoluteChanged) - { - result.status = SApplyResult::EStatus::AppliedAbsoluteOnly; - result.exact = false; - return result; - } - - result.issues |= SApplyResult::VirtualEventReplayFailed; - result.status = SApplyResult::EStatus::Failed; - result.exact = false; - return result; - } - - bool apply(ICamera* camera, const CCameraGoal& target) const - { - return applyDetailed(camera, target).succeeded(); - } - -private: - struct SGoalSolverDefaults final - { - static constexpr double UnitScale = 1.0; - static inline const hlsl::float64_t3 UnitAxisDenominator = hlsl::float64_t3(UnitScale); - static inline const hlsl::float64_t3 ScalarToleranceVec = hlsl::float64_t3(SCameraToolingThresholds::ScalarTolerance); - static inline const hlsl::float64_t3 AngularToleranceDegVec = hlsl::float64_t3(SCameraToolingThresholds::DefaultAngularToleranceDeg); - }; - - inline void appendYawPitchRollEvents( - std::vector& events, - const hlsl::float64_t3& eulerRadians, - const double denominator, - const bool includeRoll = true) const - { - static constexpr std::array RotationBindings = {{ - { CVirtualGimbalEvent::TiltUp, CVirtualGimbalEvent::TiltDown }, - { CVirtualGimbalEvent::PanRight, CVirtualGimbalEvent::PanLeft }, - { CVirtualGimbalEvent::RollRight, CVirtualGimbalEvent::RollLeft } - }}; - - auto tolerances = SGoalSolverDefaults::AngularToleranceDegVec; - if (!includeRoll) - tolerances.z = std::numeric_limits::infinity(); - - CCameraVirtualEventUtilities::appendAngularAxisEvents( - events, - eulerRadians, - hlsl::float64_t3(denominator), - tolerances, - RotationBindings); - } - - inline void appendPathDeltaEvents( - std::vector& events, - const SCameraPathDelta& delta, - const double moveDenominator, - const double rotationDenominator) const - { - CCameraPathUtilities::appendPathDeltaEvents( - events, - delta, - moveDenominator, - rotationDenominator, - SCameraPathDefaults::ExactComparisonThresholds); - } - - inline double getMoveMagnitudeDenominator(const ICamera* camera) const - { - const double moveScale = camera->getMoveSpeedScale(); - return camera->getUnscaledVirtualTranslationMagnitude() * (moveScale == 0.0 ? SGoalSolverDefaults::UnitScale : moveScale); - } - - inline double getRotationMagnitudeDenominator(const ICamera* camera) const - { - const double rotationScale = camera->getRotationSpeedScale(); - return rotationScale == 0.0 ? SGoalSolverDefaults::UnitScale : rotationScale; - } - - inline bool computePoseMismatch(ICamera* camera, const CCameraGoal& target, double& outPositionDelta, double& outRotationDeltaDeg) const - { - outPositionDelta = 0.0; - outRotationDeltaDeg = 0.0; - if (!camera) - return false; - - const auto& gimbal = camera->getGimbal(); - hlsl::SCameraPoseDelta poseDelta = {}; - if (!hlsl::CCameraMathUtilities::tryComputePoseDelta(gimbal.getPosition(), gimbal.getOrientation(), target.position, target.orientation, poseDelta)) - return false; - - outPositionDelta = poseDelta.position; - outRotationDeltaDeg = poseDelta.rotationDeg; - return true; - } - - inline bool tryApplyAbsoluteReferencePose(ICamera* camera, const CCameraGoal& target, bool& outChanged, bool& outExact) const - { - outChanged = false; - outExact = false; - if (!camera) - return false; - - switch (camera->getKind()) - { - case ICamera::CameraKind::Free: - case ICamera::CameraKind::FPS: - break; - default: - return false; - } - - double beforePosDelta = 0.0; - double beforeRotDeltaDeg = 0.0; - if (!computePoseMismatch(camera, target, beforePosDelta, beforeRotDeltaDeg)) - return false; - - if (beforePosDelta <= SCameraToolingThresholds::DefaultPositionTolerance && beforeRotDeltaDeg <= SCameraToolingThresholds::DefaultAngularToleranceDeg) - { - outExact = true; - return true; - } - - const auto targetFrame = hlsl::CCameraMathUtilities::composeTransformMatrix(target.position, target.orientation); - - camera->manipulate({}, &targetFrame); - - double afterPosDelta = 0.0; - double afterRotDeltaDeg = 0.0; - if (!computePoseMismatch(camera, target, afterPosDelta, afterRotDeltaDeg)) - return false; - - outChanged = !hlsl::CCameraMathUtilities::isNearlyZeroScalar(afterPosDelta - beforePosDelta, static_cast(SCameraToolingThresholds::TinyScalarEpsilon)) || - !hlsl::CCameraMathUtilities::isNearlyZeroScalar(afterRotDeltaDeg - beforeRotDeltaDeg, static_cast(SCameraToolingThresholds::TinyScalarEpsilon)); - outExact = afterPosDelta <= SCameraToolingThresholds::DefaultPositionTolerance && afterRotDeltaDeg <= SCameraToolingThresholds::DefaultAngularToleranceDeg; - return true; - } - - inline bool buildTargetRelativeEvents( - ICamera* camera, - const ICamera::SphericalTargetState& sphericalState, - const SCameraTargetRelativeState& goal, - std::vector& out, - const SCameraTargetRelativeEventPolicy& policy) const - { - const auto delta = CCameraTargetRelativeUtilities::buildTargetRelativeDelta(sphericalState, goal); - CCameraTargetRelativeUtilities::appendTargetRelativeDeltaEvents( - out, - delta, - policy.translateOrbit ? getMoveMagnitudeDenominator(camera) : getRotationMagnitudeDenominator(camera), - SCameraToolingThresholds::DefaultAngularToleranceDeg, - camera->getUnscaledVirtualTranslationMagnitude(), - SCameraToolingThresholds::ScalarTolerance, - policy); - return !out.empty(); - } - - inline bool buildPathEvents(ICamera* camera, const CCameraGoal& target, const ICamera::SphericalTargetState& sphericalState, std::vector& out) const - { - if (!camera) - return false; - - const auto effectiveTarget = target.hasTargetPosition ? target.targetPosition : sphericalState.target; - ICamera::PathState currentState = {}; - const ICamera::PathState* currentStateOverride = camera->tryGetPathState(currentState) ? ¤tState : nullptr; - ICamera::PathStateLimits pathLimits = CCameraPathUtilities::makeDefaultPathLimits(); - camera->tryGetPathStateLimits(pathLimits); - SCameraPathStateTransition transition = {}; - if (!CCameraPathUtilities::tryBuildPathStateTransition( - effectiveTarget, - camera->getGimbal().getPosition(), - target.position, - pathLimits, - currentStateOverride, - target.hasPathState ? &target.pathState : nullptr, - transition)) - { - return false; - } - - const auto moveDenom = getMoveMagnitudeDenominator(camera); - const auto rotationDenom = getRotationMagnitudeDenominator(camera); - appendPathDeltaEvents(out, transition.delta, moveDenom, rotationDenom); - return !out.empty(); - } - - inline bool buildSphericalEvents(ICamera* camera, const CCameraGoal& target, std::vector& out) const - { - ICamera::SphericalTargetState sphericalState; - if (!camera || !camera->tryGetSphericalTargetState(sphericalState)) - return false; - - if (camera->getKind() == ICamera::CameraKind::Path) - return buildPathEvents(camera, target, sphericalState, out); - - SCameraTargetRelativeState goal; - if (!CCameraGoalUtilities::tryResolveCanonicalTargetRelativeState(target, sphericalState, goal)) - return false; - - switch (camera->getKind()) - { - case ICamera::CameraKind::Orbit: - case ICamera::CameraKind::DollyZoom: - return buildTargetRelativeEvents(camera, sphericalState, goal, out, SCameraTargetRelativeRigDefaults::OrbitTranslatePolicy); - - case ICamera::CameraKind::Turntable: - case ICamera::CameraKind::Arcball: - return buildTargetRelativeEvents(camera, sphericalState, goal, out, SCameraTargetRelativeRigDefaults::RotateDistancePolicy); - - case ICamera::CameraKind::TopDown: - return buildTargetRelativeEvents(camera, sphericalState, goal, out, SCameraTargetRelativeRigDefaults::TopDownPolicy); - - case ICamera::CameraKind::Isometric: - return buildTargetRelativeEvents(camera, sphericalState, goal, out, SCameraTargetRelativeRigDefaults::IsometricPolicy); - - case ICamera::CameraKind::Dolly: - return buildTargetRelativeEvents(camera, sphericalState, goal, out, SCameraTargetRelativeRigDefaults::DollyPolicy); - - case ICamera::CameraKind::Chase: - return buildTargetRelativeEvents(camera, sphericalState, goal, out, SCameraTargetRelativeRigDefaults::ChasePolicy); - - default: - return buildTargetRelativeEvents(camera, sphericalState, goal, out, SCameraTargetRelativeRigDefaults::OrbitTranslatePolicy); - } - } - - inline bool buildFreeEvents(ICamera* camera, const CCameraGoal& target, std::vector& out) const - { - const auto& gimbal = camera->getGimbal(); - const auto currentPos = gimbal.getPosition(); - const auto deltaWorld = target.position - currentPos; - CCameraVirtualEventUtilities::appendWorldTranslationAsLocalEvents( - out, - gimbal.getOrientation(), - deltaWorld, - SGoalSolverDefaults::UnitAxisDenominator, - SGoalSolverDefaults::ScalarToleranceVec); - - switch (camera->getKind()) - { - case ICamera::CameraKind::FPS: - { - const auto currentPitchYaw = hlsl::CCameraMathUtilities::getPitchYawFromOrientation(gimbal.getOrientation()); - const auto targetPitchYaw = hlsl::CCameraMathUtilities::getPitchYawFromOrientation(target.orientation); - - const double rotScale = camera->getRotationSpeedScale(); - const double invScale = rotScale == 0.0 ? SGoalSolverDefaults::UnitScale : (SGoalSolverDefaults::UnitScale / rotScale); - - appendYawPitchRollEvents( - out, - hlsl::float64_t3( - hlsl::CCameraMathUtilities::wrapAngleRad(targetPitchYaw.x - currentPitchYaw.x) * invScale, - hlsl::CCameraMathUtilities::wrapAngleRad(targetPitchYaw.y - currentPitchYaw.y) * invScale, - 0.0), - SGoalSolverDefaults::UnitScale, - false); - } break; - - case ICamera::CameraKind::Free: - { - appendYawPitchRollEvents( - out, - hlsl::CCameraMathUtilities::getOrientationDeltaEulerRadiansYXZ(gimbal.getOrientation(), target.orientation), - SGoalSolverDefaults::UnitScale); - } break; - - default: - break; - } - - return !out.empty(); - } -}; - -} // namespace nbl::core - -#endif // _C_CAMERA_GOAL_SOLVER_HPP_ - diff --git a/common/include/camera/CCameraInputBindingUtilities.hpp b/common/include/camera/CCameraInputBindingUtilities.hpp deleted file mode 100644 index 3529925e6..000000000 --- a/common/include/camera/CCameraInputBindingUtilities.hpp +++ /dev/null @@ -1,561 +0,0 @@ -#ifndef _NBL_C_CAMERA_INPUT_BINDING_UTILITIES_HPP_ -#define _NBL_C_CAMERA_INPUT_BINDING_UTILITIES_HPP_ - -#include -#include - -#include "CCameraKindUtilities.hpp" -#include "ICamera.hpp" -#include "IGimbalBindingLayout.hpp" - -namespace nbl::ui -{ - -/// @brief Reusable keyboard, mouse, and ImGuizmo binding preset grouped for one camera kind. -struct SCameraInputBindingPreset -{ - IGimbalBindingLayout::keyboard_to_virtual_events_t keyboard; - IGimbalBindingLayout::mouse_to_virtual_events_t mouse; - IGimbalBindingLayout::imguizmo_to_virtual_events_t imguizmo; -}; - -/// @brief Shared physical input bundles reused by default presets and smoke probing. -struct SCameraInputBindingPhysicalGroups final -{ - static inline constexpr std::array KeyboardWasdCodes = { - ui::E_KEY_CODE::EKC_W, - ui::E_KEY_CODE::EKC_S, - ui::E_KEY_CODE::EKC_A, - ui::E_KEY_CODE::EKC_D - }; - static inline constexpr std::array KeyboardQeCodes = { - ui::E_KEY_CODE::EKC_Q, - ui::E_KEY_CODE::EKC_E - }; - static inline constexpr std::array KeyboardIjklCodes = { - ui::E_KEY_CODE::EKC_I, - ui::E_KEY_CODE::EKC_K, - ui::E_KEY_CODE::EKC_J, - ui::E_KEY_CODE::EKC_L - }; - static inline constexpr auto KeyboardProbeMoveCodes = KeyboardWasdCodes; - static inline constexpr auto KeyboardProbeLookCodes = KeyboardIjklCodes; - static inline constexpr std::array KeyboardProbeExtraCodes = { - ui::E_KEY_CODE::EKC_U, - ui::E_KEY_CODE::EKC_O - }; - static inline constexpr std::array KeyboardProbeCodes = { - ui::E_KEY_CODE::EKC_W, - ui::E_KEY_CODE::EKC_S, - ui::E_KEY_CODE::EKC_A, - ui::E_KEY_CODE::EKC_D, - ui::E_KEY_CODE::EKC_Q, - ui::E_KEY_CODE::EKC_E, - ui::E_KEY_CODE::EKC_I, - ui::E_KEY_CODE::EKC_K, - ui::E_KEY_CODE::EKC_J, - ui::E_KEY_CODE::EKC_L, - ui::E_KEY_CODE::EKC_U, - ui::E_KEY_CODE::EKC_O - }; - static inline constexpr std::array RelativeMouseCodes = { - ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_X, - ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_X, - ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_Y, - ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_Y - }; - static inline constexpr std::array PositiveScrollCodes = { - ui::E_MOUSE_CODE::EMC_VERTICAL_POSITIVE_SCROLL, - ui::E_MOUSE_CODE::EMC_HORIZONTAL_POSITIVE_SCROLL - }; - static inline constexpr std::array NegativeScrollCodes = { - ui::E_MOUSE_CODE::EMC_VERTICAL_NEGATIVE_SCROLL, - ui::E_MOUSE_CODE::EMC_HORIZONTAL_NEGATIVE_SCROLL - }; -}; - -struct CCameraInputBindingUtilities final -{ -public: - /// @brief Default gains used by the shared binding presets. - /// - /// These gains convert producer-local units into virtual-event magnitudes: - /// held keys into units per second, mouse deltas into units per mouse - /// step, scroll into units per scroll step, and ImGuizmo deltas into - /// virtual translation, rotation, or scale magnitudes. - struct SInputMagnitudeDefaults final - { - static inline constexpr double KeyboardHeldUnitsPerSecond = 1000.0; - static inline constexpr double RelativeMouseUnitsPerStep = 1.0; - static inline constexpr double ScrollUnitsPerStep = 1.0; - static inline constexpr double ImguizmoTranslationUnitsPerWorldUnit = 1.0; - static inline constexpr double ImguizmoRotationUnitsPerRadian = 1.0; - static inline constexpr double ImguizmoScaleUnitsPerFactor = 1.0; - }; - - static inline bool hasMouseRelativeMovementBinding(const IGimbalBindingLayout::mouse_to_virtual_events_t& mousePreset) - { - return containsBindingForAnyCodeGroups(mousePreset, SCameraInputBindingPhysicalGroups::RelativeMouseCodes); - } - - static inline bool hasMouseScrollBinding(const IGimbalBindingLayout::mouse_to_virtual_events_t& mousePreset) - { - return containsBindingForAnyCodeGroups( - mousePreset, - SCameraInputBindingPhysicalGroups::PositiveScrollCodes, - SCameraInputBindingPhysicalGroups::NegativeScrollCodes); - } - - static inline const IGimbalBindingLayout::keyboard_to_virtual_events_t& getDefaultCameraKeyboardMappingPreset(const core::ICamera::CameraKind kind) - { - return interactionBindingPresetForKind(kind).keyboard; - } - - static inline const IGimbalBindingLayout::keyboard_to_virtual_events_t& getDefaultCameraKeyboardMappingPreset(const core::ICamera& camera) - { - return getDefaultCameraKeyboardMappingPreset(camera.getKind()); - } - - static inline const IGimbalBindingLayout::mouse_to_virtual_events_t& getDefaultCameraMouseMappingPreset(const core::ICamera::CameraKind kind) - { - return interactionBindingPresetForKind(kind).mouse; - } - - static inline const IGimbalBindingLayout::mouse_to_virtual_events_t& getDefaultCameraMouseMappingPreset(const core::ICamera& camera) - { - return getDefaultCameraMouseMappingPreset(camera.getKind()); - } - - static inline IGimbalBindingLayout::imguizmo_to_virtual_events_t buildDefaultCameraImguizmoMappingPreset(const uint32_t allowedVirtualEvents) - { - return makeImguizmoPreset(allowedVirtualEvents); - } - - static inline IGimbalBindingLayout::imguizmo_to_virtual_events_t buildDefaultCameraImguizmoMappingPreset(const core::ICamera& camera) - { - return buildDefaultCameraImguizmoMappingPreset(camera.getAllowedVirtualEvents()); - } - - static inline SCameraInputBindingPreset buildDefaultCameraInputBindingPreset(const core::ICamera::CameraKind kind, const uint32_t allowedVirtualEvents) - { - SCameraInputBindingPreset preset; - preset.keyboard = getDefaultCameraKeyboardMappingPreset(kind); - preset.mouse = getDefaultCameraMouseMappingPreset(kind); - preset.imguizmo = buildDefaultCameraImguizmoMappingPreset(allowedVirtualEvents); - return preset; - } - - static inline SCameraInputBindingPreset buildDefaultCameraInputBindingPreset(const core::ICamera& camera) - { - return buildDefaultCameraInputBindingPreset(camera.getKind(), camera.getAllowedVirtualEvents()); - } - - static inline void applyDefaultCameraInputBindingPreset( - IGimbalBindingLayout& layout, - const core::ICamera::CameraKind kind, - const uint32_t allowedVirtualEvents) - { - const auto preset = buildDefaultCameraInputBindingPreset(kind, allowedVirtualEvents); - layout.updateKeyboardMapping([&](auto& map) { map = preset.keyboard; }); - layout.updateMouseMapping([&](auto& map) { map = preset.mouse; }); - layout.updateImguizmoMapping([&](auto& map) { map = preset.imguizmo; }); - } - - static inline void applyDefaultCameraInputBindingPreset(IGimbalBindingLayout& layout, const core::ICamera& camera) - { - applyDefaultCameraInputBindingPreset(layout, camera.getKind(), camera.getAllowedVirtualEvents()); - } - -private: - using virtual_event_t = core::CVirtualGimbalEvent::VirtualEventType; - using keyboard_axis_group_t = std::array; - using mouse_axis_group_t = std::array; - using scalar_axis_pair_t = std::array; - - struct SKeyboardPresetSpec final - { - keyboard_axis_group_t wasd = { - core::CVirtualGimbalEvent::None, - core::CVirtualGimbalEvent::None, - core::CVirtualGimbalEvent::None, - core::CVirtualGimbalEvent::None - }; - double wasdScale = IGimbalBindingLayout::CHashInfo::DefaultMagnitudeScale; - scalar_axis_pair_t qe = { - core::CVirtualGimbalEvent::None, - core::CVirtualGimbalEvent::None - }; - double qeScale = IGimbalBindingLayout::CHashInfo::DefaultMagnitudeScale; - keyboard_axis_group_t ijkl = { - core::CVirtualGimbalEvent::None, - core::CVirtualGimbalEvent::None, - core::CVirtualGimbalEvent::None, - core::CVirtualGimbalEvent::None - }; - double ijklScale = IGimbalBindingLayout::CHashInfo::DefaultMagnitudeScale; - }; - - struct SMousePresetSpec final - { - mouse_axis_group_t relative = { - core::CVirtualGimbalEvent::None, - core::CVirtualGimbalEvent::None, - core::CVirtualGimbalEvent::None, - core::CVirtualGimbalEvent::None - }; - double relativeScale = IGimbalBindingLayout::CHashInfo::DefaultMagnitudeScale; - scalar_axis_pair_t scroll = { - core::CVirtualGimbalEvent::None, - core::CVirtualGimbalEvent::None - }; - double scrollScale = IGimbalBindingLayout::CHashInfo::DefaultMagnitudeScale; - }; - - /// @brief Shared virtual-event bundles reused across interaction families. - struct SCameraInputBindingEventGroups final - { - static inline constexpr std::array FpsMove = { - core::CVirtualGimbalEvent::MoveForward, - core::CVirtualGimbalEvent::MoveBackward, - core::CVirtualGimbalEvent::MoveLeft, - core::CVirtualGimbalEvent::MoveRight - }; - static inline constexpr std::array OrbitTranslate = { - core::CVirtualGimbalEvent::MoveUp, - core::CVirtualGimbalEvent::MoveDown, - core::CVirtualGimbalEvent::MoveLeft, - core::CVirtualGimbalEvent::MoveRight - }; - static inline constexpr std::array OrbitZoom = { - core::CVirtualGimbalEvent::MoveForward, - core::CVirtualGimbalEvent::MoveBackward - }; - static inline constexpr std::array VerticalMove = { - core::CVirtualGimbalEvent::MoveDown, - core::CVirtualGimbalEvent::MoveUp - }; - static inline constexpr std::array PathRigProgressAndU = { - core::CVirtualGimbalEvent::MoveForward, - core::CVirtualGimbalEvent::MoveBackward, - core::CVirtualGimbalEvent::MoveLeft, - core::CVirtualGimbalEvent::MoveRight - }; - static inline constexpr std::array PathRigV = VerticalMove; - static inline constexpr std::array TurntableMove = { - core::CVirtualGimbalEvent::MoveForward, - core::CVirtualGimbalEvent::MoveBackward, - core::CVirtualGimbalEvent::PanLeft, - core::CVirtualGimbalEvent::PanRight - }; - static inline constexpr std::array LookYawPitch = { - core::CVirtualGimbalEvent::TiltDown, - core::CVirtualGimbalEvent::TiltUp, - core::CVirtualGimbalEvent::PanLeft, - core::CVirtualGimbalEvent::PanRight - }; - static inline constexpr std::array Roll = { - core::CVirtualGimbalEvent::RollLeft, - core::CVirtualGimbalEvent::RollRight - }; - static inline constexpr std::array PanOnly = { - core::CVirtualGimbalEvent::None, - core::CVirtualGimbalEvent::None, - core::CVirtualGimbalEvent::PanLeft, - core::CVirtualGimbalEvent::PanRight - }; - static inline constexpr std::array RelativeLook = { - core::CVirtualGimbalEvent::PanRight, - core::CVirtualGimbalEvent::PanLeft, - core::CVirtualGimbalEvent::TiltUp, - core::CVirtualGimbalEvent::TiltDown - }; - static inline constexpr std::array RelativeOrbitTranslate = { - core::CVirtualGimbalEvent::MoveRight, - core::CVirtualGimbalEvent::MoveLeft, - core::CVirtualGimbalEvent::MoveUp, - core::CVirtualGimbalEvent::MoveDown - }; - static inline constexpr std::array RelativeTopDown = { - core::CVirtualGimbalEvent::PanRight, - core::CVirtualGimbalEvent::PanLeft, - core::CVirtualGimbalEvent::MoveUp, - core::CVirtualGimbalEvent::MoveDown - }; - }; - - struct SCameraInteractionBindingSpec - { - SKeyboardPresetSpec keyboard = {}; - SMousePresetSpec mouse = {}; - }; - - struct SCameraMappedInteractionBindingSpec - { - IGimbalBindingLayout::keyboard_to_virtual_events_t keyboard; - IGimbalBindingLayout::mouse_to_virtual_events_t mouse; - }; - - template - static inline bool containsBindingForAnyCode(const Map& preset, const Codes& codes) - { - for (const auto code : codes) - { - if (preset.find(code) != preset.end()) - return true; - } - return false; - } - - template - static inline bool containsBindingForAnyCodeGroups(const Map& preset, const Codes&... codes) - { - return (containsBindingForAnyCode(preset, codes) || ...); - } - - static inline constexpr size_t interactionFamilyIndex(const core::ECameraInteractionFamily family) - { - return static_cast(family); - } - - template - static inline void appendBindingSpec(Map& preset, const Codes& codes, const Events& events, const double magnitudeScale) - { - for (size_t i = 0u; i < codes.size() && i < events.size(); ++i) - { - const auto event = events[i]; - if (event == core::CVirtualGimbalEvent::None) - continue; - preset.emplace(codes[i], IGimbalBindingLayout::CHashInfo(event, magnitudeScale)); - } - } - - template - static inline void appendMirroredBindingSpec(Map& preset, const Codes& codes, const virtual_event_t event, const double magnitudeScale) - { - if (event == core::CVirtualGimbalEvent::None) - return; - - std::array> duplicatedEvents = {}; - duplicatedEvents.fill(event); - appendBindingSpec(preset, codes, duplicatedEvents, magnitudeScale); - } - - static inline IGimbalBindingLayout::keyboard_to_virtual_events_t buildKeyboardPreset(const SKeyboardPresetSpec& spec) - { - IGimbalBindingLayout::keyboard_to_virtual_events_t preset; - appendBindingSpec(preset, SCameraInputBindingPhysicalGroups::KeyboardWasdCodes, spec.wasd, spec.wasdScale); - appendBindingSpec(preset, SCameraInputBindingPhysicalGroups::KeyboardQeCodes, spec.qe, spec.qeScale); - appendBindingSpec(preset, SCameraInputBindingPhysicalGroups::KeyboardIjklCodes, spec.ijkl, spec.ijklScale); - return preset; - } - - static inline IGimbalBindingLayout::mouse_to_virtual_events_t buildMousePreset(const SMousePresetSpec& spec) - { - IGimbalBindingLayout::mouse_to_virtual_events_t preset; - appendBindingSpec(preset, SCameraInputBindingPhysicalGroups::RelativeMouseCodes, spec.relative, spec.relativeScale); - appendMirroredBindingSpec(preset, SCameraInputBindingPhysicalGroups::PositiveScrollCodes, spec.scroll[0], spec.scrollScale); - appendMirroredBindingSpec(preset, SCameraInputBindingPhysicalGroups::NegativeScrollCodes, spec.scroll[1], spec.scrollScale); - return preset; - } - - static inline IGimbalBindingLayout::imguizmo_to_virtual_events_t makeImguizmoPreset(const uint32_t allowedVirtualEvents) - { - IGimbalBindingLayout::imguizmo_to_virtual_events_t preset; - for (const auto event : core::CVirtualGimbalEvent::VirtualEventsTypeTable) - { - if (event == core::CVirtualGimbalEvent::None) - continue; - if ((allowedVirtualEvents & event) != event) - continue; - preset.emplace(event, IGimbalBindingLayout::CHashInfo(event, getDefaultImguizmoMagnitudeScale(event))); - } - return preset; - } - - static inline double getDefaultImguizmoMagnitudeScale(const virtual_event_t event) - { - if (core::CVirtualGimbalEvent::isTranslationEvent(event)) - return SInputMagnitudeDefaults::ImguizmoTranslationUnitsPerWorldUnit; - if (core::CVirtualGimbalEvent::isRotationEvent(event)) - return SInputMagnitudeDefaults::ImguizmoRotationUnitsPerRadian; - if (core::CVirtualGimbalEvent::isScaleEvent(event)) - return SInputMagnitudeDefaults::ImguizmoScaleUnitsPerFactor; - return IGimbalBindingLayout::CHashInfo::DefaultMagnitudeScale; - } - - static inline constexpr SCameraInteractionBindingSpec EmptyInteractionBindingSpec = {}; - - static inline constexpr SKeyboardPresetSpec FpsKeyboardSpec = { - SCameraInputBindingEventGroups::FpsMove, - SInputMagnitudeDefaults::KeyboardHeldUnitsPerSecond, - {}, - IGimbalBindingLayout::CHashInfo::DefaultMagnitudeScale, - SCameraInputBindingEventGroups::LookYawPitch, - SInputMagnitudeDefaults::KeyboardHeldUnitsPerSecond - }; - - static inline constexpr SKeyboardPresetSpec FreeKeyboardSpec = { - FpsKeyboardSpec.wasd, - FpsKeyboardSpec.wasdScale, - SCameraInputBindingEventGroups::Roll, - SInputMagnitudeDefaults::KeyboardHeldUnitsPerSecond, - FpsKeyboardSpec.ijkl, - FpsKeyboardSpec.ijklScale - }; - - static inline constexpr SKeyboardPresetSpec OrbitKeyboardSpec = { - SCameraInputBindingEventGroups::OrbitTranslate, - SInputMagnitudeDefaults::KeyboardHeldUnitsPerSecond, - SCameraInputBindingEventGroups::OrbitZoom, - SInputMagnitudeDefaults::KeyboardHeldUnitsPerSecond, - {}, - IGimbalBindingLayout::CHashInfo::DefaultMagnitudeScale - }; - - static inline constexpr SKeyboardPresetSpec TargetRigKeyboardSpec = { - FpsKeyboardSpec.wasd, - FpsKeyboardSpec.wasdScale, - SCameraInputBindingEventGroups::VerticalMove, - SInputMagnitudeDefaults::KeyboardHeldUnitsPerSecond, - FpsKeyboardSpec.ijkl, - FpsKeyboardSpec.ijklScale - }; - - static inline constexpr SKeyboardPresetSpec TurntableKeyboardSpec = { - SCameraInputBindingEventGroups::TurntableMove, - SInputMagnitudeDefaults::KeyboardHeldUnitsPerSecond, - {}, - IGimbalBindingLayout::CHashInfo::DefaultMagnitudeScale, - FpsKeyboardSpec.ijkl, - FpsKeyboardSpec.ijklScale - }; - - static inline constexpr SKeyboardPresetSpec TopDownKeyboardSpec = { - OrbitKeyboardSpec.wasd, - OrbitKeyboardSpec.wasdScale, - OrbitKeyboardSpec.qe, - OrbitKeyboardSpec.qeScale, - SCameraInputBindingEventGroups::PanOnly, - SInputMagnitudeDefaults::KeyboardHeldUnitsPerSecond - }; - - static inline constexpr SKeyboardPresetSpec PathKeyboardSpec = { - SCameraInputBindingEventGroups::PathRigProgressAndU, - SInputMagnitudeDefaults::KeyboardHeldUnitsPerSecond, - SCameraInputBindingEventGroups::PathRigV, - SInputMagnitudeDefaults::KeyboardHeldUnitsPerSecond, - {}, - IGimbalBindingLayout::CHashInfo::DefaultMagnitudeScale - }; - - static inline constexpr SMousePresetSpec FpsMouseSpec = { - SCameraInputBindingEventGroups::RelativeLook, - SInputMagnitudeDefaults::RelativeMouseUnitsPerStep, - {}, - IGimbalBindingLayout::CHashInfo::DefaultMagnitudeScale - }; - - static inline constexpr SMousePresetSpec OrbitMouseSpec = { - SCameraInputBindingEventGroups::RelativeOrbitTranslate, - SInputMagnitudeDefaults::RelativeMouseUnitsPerStep, - SCameraInputBindingEventGroups::OrbitZoom, - SInputMagnitudeDefaults::ScrollUnitsPerStep - }; - - static inline constexpr SMousePresetSpec TargetRigMouseSpec = { - FpsMouseSpec.relative, - FpsMouseSpec.relativeScale, - OrbitMouseSpec.scroll, - OrbitMouseSpec.scrollScale - }; - - static inline constexpr SMousePresetSpec TopDownMouseSpec = { - SCameraInputBindingEventGroups::RelativeTopDown, - SInputMagnitudeDefaults::RelativeMouseUnitsPerStep, - OrbitMouseSpec.scroll, - OrbitMouseSpec.scrollScale - }; - - static inline constexpr SMousePresetSpec PathMouseSpec = { - SCameraInputBindingEventGroups::RelativeOrbitTranslate, - SInputMagnitudeDefaults::RelativeMouseUnitsPerStep, - SCameraInputBindingEventGroups::OrbitZoom, - SInputMagnitudeDefaults::ScrollUnitsPerStep - }; - - static inline constexpr SCameraInteractionBindingSpec FpsInteractionBindingSpec = { - FpsKeyboardSpec, - FpsMouseSpec - }; - - static inline constexpr SCameraInteractionBindingSpec FreeInteractionBindingSpec = { - FreeKeyboardSpec, - FpsMouseSpec - }; - - static inline constexpr SCameraInteractionBindingSpec OrbitInteractionBindingSpec = { - OrbitKeyboardSpec, - OrbitMouseSpec - }; - - static inline constexpr SCameraInteractionBindingSpec TargetRigInteractionBindingSpec = { - TargetRigKeyboardSpec, - TargetRigMouseSpec - }; - - static inline constexpr SCameraInteractionBindingSpec TurntableInteractionBindingSpec = { - TurntableKeyboardSpec, - TargetRigMouseSpec - }; - - static inline constexpr SCameraInteractionBindingSpec TopDownInteractionBindingSpec = { - TopDownKeyboardSpec, - TopDownMouseSpec - }; - - static inline constexpr SCameraInteractionBindingSpec PathInteractionBindingSpec = { - PathKeyboardSpec, - PathMouseSpec - }; - - template - static inline auto makePresetCache(const SpecArray& specs, Builder&& builder) - { - std::array> cache = {}; - for (size_t i = 0u; i < specs.size(); ++i) - cache[i] = builder(specs[i]); - return cache; - } - - static inline SCameraMappedInteractionBindingSpec mapInteractionBindingSpec(const SCameraInteractionBindingSpec& spec) - { - return { - .keyboard = buildKeyboardPreset(spec.keyboard), - .mouse = buildMousePreset(spec.mouse) - }; - } - - static inline constexpr std::array InteractionFamilyPresetSpecs = {{ - EmptyInteractionBindingSpec, - FpsInteractionBindingSpec, - FreeInteractionBindingSpec, - OrbitInteractionBindingSpec, - TargetRigInteractionBindingSpec, - TurntableInteractionBindingSpec, - TopDownInteractionBindingSpec, - PathInteractionBindingSpec - }}; - - static inline const SCameraMappedInteractionBindingSpec& interactionBindingPresetForKind(const core::ICamera::CameraKind kind) - { - const auto familyIx = interactionFamilyIndex(core::CCameraKindUtilities::getCameraInteractionFamily(kind)); - static const auto cache = makePresetCache( - InteractionFamilyPresetSpecs, - [](const SCameraInteractionBindingSpec& spec) { return mapInteractionBindingSpec(spec); }); - return cache[familyIx < cache.size() ? familyIx : 0u]; - } -}; - -} // namespace nbl::ui - -#endif // _NBL_C_CAMERA_INPUT_BINDING_UTILITIES_HPP_ diff --git a/common/include/camera/CCameraKeyframeTrack.hpp b/common/include/camera/CCameraKeyframeTrack.hpp deleted file mode 100644 index 44894889c..000000000 --- a/common/include/camera/CCameraKeyframeTrack.hpp +++ /dev/null @@ -1,175 +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 _C_CAMERA_KEYFRAME_TRACK_HPP_ -#define _C_CAMERA_KEYFRAME_TRACK_HPP_ - -#include -#include -#include - -#include "CCameraPreset.hpp" - -namespace nbl::core -{ - -/// @brief Reusable keyframe container plus selection state for playback tooling. -struct CCameraKeyframeTrack -{ - std::vector keyframes; - int selectedKeyframeIx = -1; -}; - -struct CCameraKeyframeTrackUtilities final -{ -public: - /// @brief Compare two keyframes by authored time and shared preset state. - static inline bool compareKeyframes(const CCameraKeyframe& lhs, const CCameraKeyframe& rhs, - const double timeEps, const double posEps, const double rotEpsDeg, const double scalarEps) - { - return hlsl::abs(static_cast(lhs.time - rhs.time)) <= timeEps && - CCameraPresetUtilities::comparePresets(lhs.preset, rhs.preset, posEps, rotEpsDeg, scalarEps); - } - - /// @brief Compare two authored keyframe tracks with optional selection-state checking. - static inline bool compareKeyframeTracks(const CCameraKeyframeTrack& lhs, const CCameraKeyframeTrack& rhs, - const double timeEps, const double posEps, const double rotEpsDeg, const double scalarEps, const bool compareSelection = true) - { - if ((compareSelection && lhs.selectedKeyframeIx != rhs.selectedKeyframeIx) || lhs.keyframes.size() != rhs.keyframes.size()) - return false; - - for (size_t i = 0u; i < lhs.keyframes.size(); ++i) - { - if (!compareKeyframes(lhs.keyframes[i], rhs.keyframes[i], timeEps, posEps, rotEpsDeg, scalarEps)) - return false; - } - - return true; - } - - /// @brief Compare only the serialized/authored content of two tracks and ignore transient UI selection state. - static inline bool compareKeyframeTrackContent(const CCameraKeyframeTrack& lhs, const CCameraKeyframeTrack& rhs, - const double timeEps, const double posEps, const double rotEpsDeg, const double scalarEps) - { - return compareKeyframeTracks(lhs, rhs, timeEps, posEps, rotEpsDeg, scalarEps, false); - } - - static inline bool tryBuildKeyframeTrackPresetAtTime(const CCameraKeyframeTrack& track, const float time, CCameraPreset& preset) - { - if (track.keyframes.empty()) - return false; - - if (track.keyframes.size() == 1u) - { - preset = track.keyframes.front().preset; - return true; - } - - const auto clampedTime = std::clamp(time, 0.f, track.keyframes.back().time); - size_t idx = 0u; - while (idx + 1u < track.keyframes.size() && track.keyframes[idx + 1u].time < clampedTime) - ++idx; - - const auto& a = track.keyframes[idx]; - const auto& b = track.keyframes[std::min(idx + 1u, track.keyframes.size() - 1u)]; - if (b.time <= a.time) - { - preset = a.preset; - return true; - } - - const double alpha = static_cast(clampedTime - a.time) / static_cast(b.time - a.time); - preset = a.preset; - CCameraPresetUtilities::assignGoalToPreset( - preset, - CCameraGoalUtilities::blendGoals( - CCameraPresetUtilities::makeGoalFromPreset(a.preset), - CCameraPresetUtilities::makeGoalFromPreset(b.preset), - alpha)); - return true; - } - - static inline void sortKeyframeTrackByTime(CCameraKeyframeTrack& track) - { - std::sort(track.keyframes.begin(), track.keyframes.end(), [](const auto& a, const auto& b) { return a.time < b.time; }); - } - - static inline void clampTrackTimeToKeyframes(const CCameraKeyframeTrack& track, float& time) - { - if (track.keyframes.empty()) - { - time = 0.f; - return; - } - - time = std::clamp(time, 0.f, track.keyframes.back().time); - } - - static inline int selectKeyframeTrackNearestTime(CCameraKeyframeTrack& track, const float time) - { - if (track.keyframes.empty()) - { - track.selectedKeyframeIx = -1; - return track.selectedKeyframeIx; - } - - size_t bestIx = 0u; - float bestDelta = hlsl::abs(track.keyframes.front().time - time); - for (size_t i = 1u; i < track.keyframes.size(); ++i) - { - const float delta = hlsl::abs(track.keyframes[i].time - time); - if (delta < bestDelta) - { - bestDelta = delta; - bestIx = i; - } - } - - track.selectedKeyframeIx = static_cast(bestIx); - return track.selectedKeyframeIx; - } - - static inline void normalizeSelectedKeyframeTrack(CCameraKeyframeTrack& track) - { - if (track.keyframes.empty()) - { - track.selectedKeyframeIx = -1; - return; - } - - if (track.selectedKeyframeIx < 0) - track.selectedKeyframeIx = 0; - else if (track.selectedKeyframeIx >= static_cast(track.keyframes.size())) - track.selectedKeyframeIx = static_cast(track.keyframes.size()) - 1; - } - - static inline CCameraKeyframe* getSelectedKeyframe(CCameraKeyframeTrack& track) - { - normalizeSelectedKeyframeTrack(track); - if (track.selectedKeyframeIx < 0) - return nullptr; - return &track.keyframes[static_cast(track.selectedKeyframeIx)]; - } - - static inline const CCameraKeyframe* getSelectedKeyframe(const CCameraKeyframeTrack& track) - { - if (track.selectedKeyframeIx < 0 || track.selectedKeyframeIx >= static_cast(track.keyframes.size())) - return nullptr; - return &track.keyframes[static_cast(track.selectedKeyframeIx)]; - } - - static inline bool replaceSelectedKeyframePreset(CCameraKeyframeTrack& track, CCameraPreset preset) - { - auto* selected = getSelectedKeyframe(track); - if (!selected) - return false; - - selected->preset = std::move(preset); - return true; - } -}; - -} // namespace nbl::core - -#endif // _C_CAMERA_KEYFRAME_TRACK_HPP_ diff --git a/common/include/camera/CCameraKeyframeTrackPersistence.hpp b/common/include/camera/CCameraKeyframeTrackPersistence.hpp deleted file mode 100644 index 7ce8a45cb..000000000 --- a/common/include/camera/CCameraKeyframeTrackPersistence.hpp +++ /dev/null @@ -1,30 +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 _C_CAMERA_KEYFRAME_TRACK_PERSISTENCE_HPP_ -#define _C_CAMERA_KEYFRAME_TRACK_PERSISTENCE_HPP_ - -#include - -#include "CCameraKeyframeTrack.hpp" -#include "nbl/system/path.h" - -namespace nbl::system -{ - -class ISystem; - -/// @brief Serialize one camera keyframe track into an existing stream. -bool writeKeyframeTrack(std::ostream& out, const core::CCameraKeyframeTrack& track, int indent = 2); -/// @brief Deserialize one camera keyframe track from an existing stream. -bool readKeyframeTrack(std::istream& in, core::CCameraKeyframeTrack& track); - -/// @brief Save one camera keyframe track to a file. -bool saveKeyframeTrackToFile(ISystem& system, const path& path, const core::CCameraKeyframeTrack& track, int indent = 2); -/// @brief Load one camera keyframe track from a file. -bool loadKeyframeTrackFromFile(ISystem& system, const path& path, core::CCameraKeyframeTrack& track); - -} // namespace nbl::system - -#endif // _C_CAMERA_KEYFRAME_TRACK_PERSISTENCE_HPP_ diff --git a/common/include/camera/CCameraKindUtilities.hpp b/common/include/camera/CCameraKindUtilities.hpp deleted file mode 100644 index e09fb6ce5..000000000 --- a/common/include/camera/CCameraKindUtilities.hpp +++ /dev/null @@ -1,140 +0,0 @@ -#ifndef _C_CAMERA_KIND_UTILITIES_HPP_ -#define _C_CAMERA_KIND_UTILITIES_HPP_ - -#include -#include - -#include "CCameraPathMetadata.hpp" -#include "ICamera.hpp" - -namespace nbl::core -{ - -/// @brief Interaction family used to group camera kinds with matching control semantics. -enum class ECameraInteractionFamily : uint8_t -{ - None, - Fps, - Free, - Orbit, - TargetRig, - Turntable, - TopDown, - Path -}; - -/// @brief Shared metadata for one concrete `CameraKind`. -struct SCameraKindTraits final -{ - ICamera::CameraKind kind = ICamera::CameraKind::Unknown; - std::string_view label = "Unknown"; - std::string_view description = "Unspecified camera behavior"; - ECameraInteractionFamily interactionFamily = ECameraInteractionFamily::None; -}; - -struct CCameraKindUtilities final -{ -public: - static inline constexpr const SCameraKindTraits& getCameraKindTraits(const ICamera::CameraKind kind) - { - const auto ix = static_cast(kind); - if (ix >= CameraKindTraitsTable.size()) - return CameraKindTraitsTable[0u]; - return CameraKindTraitsTable[ix]; - } - - static inline constexpr std::string_view getCameraKindLabel(const ICamera::CameraKind kind) - { - return getCameraKindTraits(kind).label; - } - - static inline constexpr std::string_view getCameraKindDescription(const ICamera::CameraKind kind) - { - return getCameraKindTraits(kind).description; - } - - static inline constexpr ECameraInteractionFamily getCameraInteractionFamily(const ICamera::CameraKind kind) - { - return getCameraKindTraits(kind).interactionFamily; - } - -private: - static inline constexpr std::array(ICamera::CameraKind::Path) + 1u> CameraKindTraitsTable = {{ - { - .kind = ICamera::CameraKind::Unknown, - .label = "Unknown", - .description = "Unspecified camera behavior", - .interactionFamily = ECameraInteractionFamily::None - }, - { - .kind = ICamera::CameraKind::FPS, - .label = "FPS", - .description = "First-person WASD + mouse look", - .interactionFamily = ECameraInteractionFamily::Fps - }, - { - .kind = ICamera::CameraKind::Free, - .label = "Free", - .description = "Free-fly 6DOF with full rotation", - .interactionFamily = ECameraInteractionFamily::Free - }, - { - .kind = ICamera::CameraKind::Orbit, - .label = "Orbit", - .description = "Orbit around target with dolly", - .interactionFamily = ECameraInteractionFamily::Orbit - }, - { - .kind = ICamera::CameraKind::Arcball, - .label = "Arcball", - .description = "Arcball trackball around target", - .interactionFamily = ECameraInteractionFamily::Orbit - }, - { - .kind = ICamera::CameraKind::Turntable, - .label = "Turntable", - .description = "Turntable yaw/pitch around target", - .interactionFamily = ECameraInteractionFamily::Turntable - }, - { - .kind = ICamera::CameraKind::TopDown, - .label = "TopDown", - .description = "Fixed pitch top-down pan", - .interactionFamily = ECameraInteractionFamily::TopDown - }, - { - .kind = ICamera::CameraKind::Isometric, - .label = "Isometric", - .description = "Fixed isometric view with pan", - .interactionFamily = ECameraInteractionFamily::Orbit - }, - { - .kind = ICamera::CameraKind::Chase, - .label = "Chase", - .description = "Target follow with chase controls", - .interactionFamily = ECameraInteractionFamily::TargetRig - }, - { - .kind = ICamera::CameraKind::Dolly, - .label = "Dolly", - .description = "Rig truck/dolly with look-at", - .interactionFamily = ECameraInteractionFamily::TargetRig - }, - { - .kind = ICamera::CameraKind::DollyZoom, - .label = "Dolly Zoom", - .description = "Orbit with dolly-zoom FOV", - .interactionFamily = ECameraInteractionFamily::Orbit - }, - { - .kind = ICamera::CameraKind::Path, - .label = SCameraPathRigMetadata::KindLabel, - .description = SCameraPathRigMetadata::KindDescription, - .interactionFamily = ECameraInteractionFamily::Path - } - }}; -}; - -} // namespace nbl::core - -#endif // _C_CAMERA_KIND_UTILITIES_HPP_ diff --git a/common/include/camera/CCameraManipulationUtilities.hpp b/common/include/camera/CCameraManipulationUtilities.hpp deleted file mode 100644 index 9ca865a0d..000000000 --- a/common/include/camera/CCameraManipulationUtilities.hpp +++ /dev/null @@ -1,156 +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 _C_CAMERA_MANIPULATION_UTILITIES_HPP_ -#define _C_CAMERA_MANIPULATION_UTILITIES_HPP_ - -#include -#include - -#include "CCameraPresetFlow.hpp" -#include "CCameraVirtualEventUtilities.hpp" - -namespace nbl::core -{ - -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 = SCameraTargetRelativeTraits::MinDistance; - static constexpr float MaxDistance = SCameraTargetRelativeTraits::DefaultMaxDistance; -}; - -/// @brief Reusable constraint settings for post-manipulation camera clamping. -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 CCameraManipulationUtilities final -{ -public: - /// @brief Apply an authored world-space reference frame through the shared camera runtime entry point. - static inline bool applyReferenceFrameToCamera(ICamera* camera, const hlsl::float64_t4x4& referenceFrame) - { - if (!camera) - return false; - - return camera->manipulateWithUnitMotionScales({}, &referenceFrame); - } - - /// @brief Scale translation and rotation event magnitudes without touching unrelated event types. - static inline void scaleVirtualEvents(std::vector& events, const uint32_t count, const float translationScale, const float rotationScale) - { - for (uint32_t i = 0u; i < count; ++i) - { - auto& ev = events[i]; - if (CVirtualGimbalEvent::isTranslationEvent(ev.type)) - { - ev.magnitude *= translationScale; - } - else if (CVirtualGimbalEvent::isRotationEvent(ev.type)) - { - ev.magnitude *= rotationScale; - } - } - } - - /// @brief Reinterpret world-space translation intents as local camera-space movement events. - static inline void remapTranslationEventsFromWorldToCameraLocal(ICamera* camera, std::vector& events, uint32_t& count) - { - if (!camera) - return; - - std::vector filtered; - filtered.reserve(events.size()); - - for (uint32_t i = 0u; i < count; ++i) - { - const auto& ev = events[i]; - if (!CVirtualGimbalEvent::isTranslationEvent(ev.type)) - filtered.emplace_back(ev); - } - - const auto worldDelta = CCameraVirtualEventUtilities::collectSignedTranslationDelta({ events.data(), count }); - if (hlsl::CCameraMathUtilities::isNearlyZeroVector(worldDelta, static_cast(SCameraToolingThresholds::TinyScalarEpsilon))) - { - events = std::move(filtered); - count = static_cast(events.size()); - return; - } - - CCameraVirtualEventUtilities::appendWorldTranslationAsLocalEvents(filtered, camera->getGimbal().getOrientation(), worldDelta); - - events = std::move(filtered); - count = static_cast(events.size()); - } - - /// @brief Apply shared distance and Euler-angle constraints after manipulation. - static inline bool applyCameraConstraints(const CCameraGoalSolver& solver, ICamera* camera, const SCameraConstraintSettings& constraints) - { - if (!constraints.enabled || !camera) - return false; - - if (camera->hasCapability(ICamera::SphericalTarget)) - { - if (!constraints.clampDistance) - return false; - - 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 = 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; - - CCameraPreset preset; - preset.goal.position = pos; - preset.goal.orientation = hlsl::CCameraMathUtilities::makeQuaternionFromEulerDegreesYXZ(clamped); - return CCameraPresetFlowUtilities::applyPreset(solver, camera, preset); - } -}; - -} // namespace nbl::core - -#endif // _C_CAMERA_MANIPULATION_UTILITIES_HPP_ - diff --git a/common/include/camera/CCameraMathUtilities.hpp b/common/include/camera/CCameraMathUtilities.hpp deleted file mode 100644 index 7e89d4b92..000000000 --- a/common/include/camera/CCameraMathUtilities.hpp +++ /dev/null @@ -1,948 +0,0 @@ -#ifndef _C_CAMERA_MATH_UTILITIES_HPP_ -#define _C_CAMERA_MATH_UTILITIES_HPP_ - -#include -#include -#include -#include - -#include "nbl/builtin/hlsl/numbers.hlsl" -#include "nbl/builtin/hlsl/matrix_utils/transformation_matrix_utils.hlsl" - -namespace nbl::hlsl -{ - -/// @brief Camera-oriented math aliases and helpers built on top of Nabla `nbl::hlsl` types. -template -using camera_vector_t = vector; - -template -using camera_matrix_t = matrix; - -template -using camera_quaternion_t = math::quaternion; - -template -struct SRigidTransformComponents -{ - camera_vector_t translation = camera_vector_t(T(0)); - camera_quaternion_t orientation = camera_quaternion_t::create(); - camera_vector_t scale = camera_vector_t(T(1)); -}; - -template -struct SCameraPoseDelta -{ - T position = T(0); - T rotationDeg = T(0); -}; - -struct SCameraViewRigDefaults final -{ - static constexpr double DegreesToRadians = numbers::pi / 180.0; - static constexpr double ArcballPitchLimitDeg = 89.0; - static constexpr double TurntablePitchLimitDeg = ArcballPitchLimitDeg; - static constexpr double ChaseMaxPitchDeg = 70.0; - static constexpr double ChaseMinPitchDeg = -60.0; - static constexpr double DollyPitchLimitDeg = 85.0; - static constexpr double FpsVerticalPitchLimitDeg = 88.0; - static constexpr double TopDownPitchDeg = -90.0; - static constexpr double IsometricYawDeg = 45.0; - static constexpr double IsometricPitchDeg = 35.264389682754654; - - static inline constexpr double ArcballPitchLimitRad = ArcballPitchLimitDeg * DegreesToRadians; - static inline constexpr double TurntablePitchLimitRad = TurntablePitchLimitDeg * DegreesToRadians; - static inline constexpr double ChaseMaxPitchRad = ChaseMaxPitchDeg * DegreesToRadians; - static inline constexpr double ChaseMinPitchRad = ChaseMinPitchDeg * DegreesToRadians; - static inline constexpr double DollyPitchLimitRad = DollyPitchLimitDeg * DegreesToRadians; - static inline constexpr double FpsVerticalPitchLimitRad = FpsVerticalPitchLimitDeg * DegreesToRadians; - static inline constexpr double TopDownPitchRad = TopDownPitchDeg * DegreesToRadians; - static inline constexpr double IsometricYawRad = IsometricYawDeg * DegreesToRadians; - static inline constexpr double IsometricPitchRad = IsometricPitchDeg * DegreesToRadians; -}; - -struct SCameraRigidMathDefaults final -{ - static constexpr double LookAtParallelThreshold = 0.99; -}; - -struct CCameraMathUtilities final -{ - template - static inline T wrapAngleRad(T angle) - { - constexpr T Pi = numbers::pi; - constexpr T TwoPi = Pi * static_cast(2); - - angle = std::fmod(angle + Pi, TwoPi); - if (angle < static_cast(0)) - angle += TwoPi; - return angle - Pi; - } - - template - static inline T getWrappedAngleDistanceRadians(const T a, const T b) - { - return hlsl::abs(wrapAngleRad(a - b)); - } - - template - static inline T getWrappedAngleDistanceDegrees(const T a, const T b) - { - constexpr T HalfTurn = static_cast(180); - constexpr T FullTurn = static_cast(360); - - T angle = std::fmod(a - b + HalfTurn, FullTurn); - if (angle < static_cast(0)) - angle += FullTurn; - return hlsl::abs(angle - HalfTurn); - } - - template - static inline T lerpWrappedAngleRad(const T a, const T b, const T alpha) - { - return a + wrapAngleRad(b - a) * alpha; - } - - template - static inline bool isFiniteScalar(const T value) - { - return std::isfinite(value); - } - - template - static inline constexpr T getCameraMathEpsilon() - { - return std::numeric_limits::epsilon(); - } - - template - static inline bool nearlyEqualScalar(const T a, const T b, const T epsilon) - { - return hlsl::abs(a - b) <= epsilon; - } - - template - static inline bool isNearlyZeroScalar(const T value, const T epsilon = getCameraMathEpsilon()) - { - return hlsl::abs(value) <= epsilon; - } - - template - static inline bool isNearlyZeroVector(const camera_vector_t& value, const T epsilon = getCameraMathEpsilon()) - { - return length(value) <= epsilon; - } - - template - static inline bool hasPlanarDeltaXY(const camera_vector_t& value, const T epsilon = std::numeric_limits::epsilon()) - { - return !isNearlyZeroVector(camera_vector_t(value.x, value.y), epsilon); - } - - template - static inline bool nearlyEqualVec3(const VecA& a, const VecB& b, const T epsilon) - { - const camera_vector_t delta( - static_cast(a.x - b.x), - static_cast(a.y - b.y), - static_cast(a.z - b.z)); - return length(delta) <= epsilon; - } - - template - static inline constexpr camera_vector_t getCameraWorldRight() - { - return camera_vector_t(T(1), T(0), T(0)); - } - - template - static inline constexpr camera_vector_t getCameraWorldUp() - { - return camera_vector_t(T(0), T(1), T(0)); - } - - template - static inline constexpr camera_vector_t getCameraWorldForward() - { - return camera_vector_t(T(0), T(0), T(1)); - } - - template - static inline constexpr T getCameraLookAtParallelThreshold() - { - return static_cast(SCameraRigidMathDefaults::LookAtParallelThreshold); - } - - template - static inline camera_quaternion_t makeIdentityQuaternion() - { - return camera_quaternion_t::create(); - } - - template - static inline camera_quaternion_t makeQuaternionFromComponents(const T x, const T y, const T z, const T w) - { - camera_quaternion_t output; - output.data = camera_vector_t(x, y, z, w); - return output; - } - - template - static inline camera_quaternion_t normalizeQuaternion(const camera_quaternion_t& q) - { - return normalize(q); - } - - template - static inline bool isFiniteQuaternion(const camera_quaternion_t& q) - { - return isFiniteScalar(q.data.x) && - isFiniteScalar(q.data.y) && - isFiniteScalar(q.data.z) && - isFiniteScalar(q.data.w); - } - - template - static inline bool isFiniteVec3(const camera_vector_t& value) - { - return isFiniteScalar(value.x) && - isFiniteScalar(value.y) && - isFiniteScalar(value.z); - } - - template - static inline camera_vector_t safeNormalizeVec3(const camera_vector_t& value, const camera_vector_t& fallback) - { - const auto len = length(value); - if (!isFiniteScalar(len) || len <= getCameraMathEpsilon()) - return fallback; - return value / len; - } - - template - static inline camera_quaternion_t makeQuaternionFromAxisAngle(const camera_vector_t& axis, const T radians) - { - return camera_quaternion_t::create(axis, radians); - } - - template - static inline camera_quaternion_t makeQuaternionFromEulerRadians(const camera_vector_t& eulerRadians) - { - return camera_quaternion_t::create(eulerRadians.x, eulerRadians.y, eulerRadians.z); - } - - template - static inline camera_quaternion_t makeQuaternionFromEulerDegrees(const camera_vector_t& eulerDegrees) - { - return makeQuaternionFromEulerRadians(camera_vector_t( - radians(eulerDegrees.x), - radians(eulerDegrees.y), - radians(eulerDegrees.z))); - } - - template - static inline camera_quaternion_t makeQuaternionFromEulerRadiansYXZ(const camera_vector_t& eulerRadians) - { - const auto pitch = makeQuaternionFromAxisAngle(getCameraWorldRight(), eulerRadians.x); - const auto yaw = makeQuaternionFromAxisAngle(getCameraWorldUp(), eulerRadians.y); - const auto roll = makeQuaternionFromAxisAngle(getCameraWorldForward(), eulerRadians.z); - return normalizeQuaternion(yaw * pitch * roll); - } - - template - static inline camera_quaternion_t makeQuaternionFromEulerDegreesYXZ(const camera_vector_t& eulerDegrees) - { - return makeQuaternionFromEulerRadiansYXZ(camera_vector_t( - radians(eulerDegrees.x), - radians(eulerDegrees.y), - radians(eulerDegrees.z))); - } - - template - static inline camera_quaternion_t makeQuaternionFromBasis( - const camera_vector_t& right, - const camera_vector_t& up, - const camera_vector_t& forward) - { - const auto canonicalForward = safeNormalizeVec3(forward, getCameraWorldForward()); - - auto canonicalRight = right - canonicalForward * dot(right, canonicalForward); - canonicalRight = safeNormalizeVec3( - canonicalRight, - safeNormalizeVec3(cross(up, canonicalForward), getCameraWorldRight())); - - auto canonicalUp = cross(canonicalForward, canonicalRight); - canonicalUp = safeNormalizeVec3( - canonicalUp, - safeNormalizeVec3(up - canonicalForward * dot(up, canonicalForward), getCameraWorldUp())); - - canonicalRight = safeNormalizeVec3(cross(canonicalUp, canonicalForward), canonicalRight); - canonicalUp = safeNormalizeVec3(cross(canonicalForward, canonicalRight), canonicalUp); - - const camera_matrix_t basis { canonicalRight, canonicalUp, canonicalForward }; - const auto desiredRight = canonicalRight; - const auto desiredUp = canonicalUp; - const auto desiredForward = canonicalForward; - - const auto scoreCandidate = [&](const camera_quaternion_t& candidate) - { - if (!isFiniteQuaternion(candidate)) - return std::numeric_limits::infinity(); - - const auto normalizedCandidate = normalizeQuaternion(candidate); - const auto rebuiltRight = normalizedCandidate.transformVector(camera_vector_t(T(1), T(0), T(0)), true); - const auto rebuiltUp = normalizedCandidate.transformVector(camera_vector_t(T(0), T(1), T(0)), true); - const auto rebuiltForward = normalizedCandidate.transformVector(camera_vector_t(T(0), T(0), T(1)), true); - - const T rightError = length(rebuiltRight - desiredRight); - const T upError = length(rebuiltUp - desiredUp); - const T forwardError = length(rebuiltForward - desiredForward); - return rightError + upError + forwardError; - }; - - const auto quaternionFromMatrixFallback = [&](const camera_matrix_t& m) - { - const T m00 = m[0][0]; - const T m11 = m[1][1]; - const T m22 = m[2][2]; - const T trace = m00 + m11 + m22; - - camera_quaternion_t output = makeIdentityQuaternion(); - if (trace > T(0)) - { - const T scale = hlsl::sqrt(trace + T(1)); - const T invScale = T(0.5) / scale; - output.data.x = (m[2][1] - m[1][2]) * invScale; - output.data.y = (m[0][2] - m[2][0]) * invScale; - output.data.z = (m[1][0] - m[0][1]) * invScale; - output.data.w = scale * T(0.5); - } - else if (m00 >= m11 && m00 >= m22) - { - const T scale = hlsl::sqrt(T(1) + m00 - m11 - m22); - const T invScale = T(0.5) / scale; - output.data.x = scale * T(0.5); - output.data.y = (m[0][1] + m[1][0]) * invScale; - output.data.z = (m[2][0] + m[0][2]) * invScale; - output.data.w = (m[2][1] - m[1][2]) * invScale; - } - else if (m11 >= m22) - { - const T scale = hlsl::sqrt(T(1) + m11 - m00 - m22); - const T invScale = T(0.5) / scale; - output.data.x = (m[0][1] + m[1][0]) * invScale; - output.data.y = scale * T(0.5); - output.data.z = (m[1][2] + m[2][1]) * invScale; - output.data.w = (m[0][2] - m[2][0]) * invScale; - } - else - { - const T scale = hlsl::sqrt(T(1) + m22 - m00 - m11); - const T invScale = T(0.5) / scale; - output.data.x = (m[2][0] + m[0][2]) * invScale; - output.data.y = (m[1][2] + m[2][1]) * invScale; - output.data.z = scale * T(0.5); - output.data.w = (m[1][0] - m[0][1]) * invScale; - } - return normalizeQuaternion(output); - }; - - const camera_matrix_t transposedBasis = hlsl::transpose(basis); - const camera_quaternion_t candidates[] = { - camera_quaternion_t::create(basis, true), - camera_quaternion_t::create(transposedBasis, true), - quaternionFromMatrixFallback(basis), - quaternionFromMatrixFallback(transposedBasis) - }; - - camera_quaternion_t bestCandidate = makeIdentityQuaternion(); - T bestScore = std::numeric_limits::infinity(); - bool foundFiniteCandidate = false; - const auto considerCandidate = [&](const camera_quaternion_t& candidate) - { - const T score = scoreCandidate(candidate); - if (score < bestScore) - { - bestScore = score; - bestCandidate = candidate; - foundFiniteCandidate = true; - } - }; - - for (const auto& candidate : candidates) - considerCandidate(candidate); - - if (!foundFiniteCandidate || !isFiniteQuaternion(bestCandidate)) - return makeIdentityQuaternion(); - - return normalizeQuaternion(bestCandidate); - } - - template - static inline bool tryBuildCameraBasisFromForwardUpHint( - const camera_vector_t& forwardHint, - const camera_vector_t& upHint, - camera_vector_t& outRight, - camera_vector_t& outUp, - camera_vector_t& outForward) - { - const auto forward = safeNormalizeVec3(forwardHint, getCameraWorldForward()); - if (!isFiniteVec3(forward) || isNearlyZeroVector(forward)) - return false; - - const auto preferredUp = safeNormalizeVec3(upHint, getCameraWorldForward()); - auto right = cross(preferredUp, forward); - if (!isFiniteVec3(right) || isNearlyZeroVector(right)) - { - const auto fallbackUp = hlsl::abs(forward.z) < getCameraLookAtParallelThreshold() ? - getCameraWorldForward() : - getCameraWorldUp(); - right = cross(fallbackUp, forward); - if (!isFiniteVec3(right) || isNearlyZeroVector(right)) - return false; - } - - right = safeNormalizeVec3(right, getCameraWorldRight()); - auto up = safeNormalizeVec3(cross(forward, right), preferredUp); - right = safeNormalizeVec3(cross(up, forward), right); - if (!isOrthoBase(right, up, forward)) - return false; - - outRight = right; - outUp = up; - outForward = forward; - return true; - } - - template - static inline camera_vector_t makeSphericalOffsetFromOrbit(const camera_vector_t& orbitUv, const T distance) - { - return camera_vector_t( - hlsl::cos(orbitUv.y) * hlsl::cos(orbitUv.x) * distance, - hlsl::cos(orbitUv.y) * hlsl::sin(orbitUv.x) * distance, - hlsl::sin(orbitUv.y) * distance); - } - - template - static inline T getPlanarRadiusXZ(const camera_vector_t& offset) - { - return length(camera_vector_t(offset.x, offset.z)); - } - - template - static inline T getPathDistance(const T pathU, const T pathV) - { - return length(camera_vector_t(pathU, pathV)); - } - - template - static inline camera_vector_t makePathOffsetFromState(const T pathS, const T pathU, const T pathV) - { - return camera_vector_t(hlsl::cos(pathS) * pathU, pathV, hlsl::sin(pathS) * pathU); - } - - template - static inline bool sanitizePathState(T& pathS, T& pathU, T& pathV, T& pathRoll, const T minU) - { - if (!isFiniteScalar(pathS) || !isFiniteScalar(pathU) || !isFiniteScalar(pathV) || !isFiniteScalar(pathRoll)) - return false; - - pathS = wrapAngleRad(pathS); - pathU = std::max(minU, pathU); - pathRoll = wrapAngleRad(pathRoll); - return isFiniteScalar(pathS) && - isFiniteScalar(pathU) && - isFiniteScalar(pathV) && - isFiniteScalar(pathRoll); - } - - template - static inline bool tryScalePathStateDistance( - const T desiredDistance, - const T minU, - T& pathU, - T& pathV, - T* outAppliedDistance = nullptr) - { - if (!isFiniteScalar(desiredDistance) || - !isFiniteScalar(pathU) || - !isFiniteScalar(pathV)) - return false; - - const T currentDistance = getPathDistance(pathU, pathV); - constexpr T Epsilon = std::numeric_limits::epsilon(); - if (currentDistance > Epsilon) - { - const T scale = desiredDistance / currentDistance; - pathU = std::max(minU, pathU * scale); - pathV *= scale; - } - else - { - pathU = std::max(minU, desiredDistance); - pathV = T(0); - } - - if (outAppliedDistance) - *outAppliedDistance = getPathDistance(pathU, pathV); - return isFiniteScalar(pathU) && isFiniteScalar(pathV); - } - - template - static inline bool tryBuildPathStateFromPosition( - const camera_vector_t& targetPosition, - const camera_vector_t& position, - const T minRadius, - T& outS, - T& outU, - T& outV) - { - const auto offset = position - targetPosition; - const auto radius = getPlanarRadiusXZ(offset); - if (!isFiniteScalar(radius) || !isFiniteScalar(offset.y)) - return false; - - outS = wrapAngleRad(hlsl::atan2(offset.z, offset.x)); - outU = std::max(minRadius, radius); - outV = offset.y; - return isFiniteScalar(outS) && - isFiniteScalar(outU) && - isFiniteScalar(outV); - } - - template - static inline bool tryBuildLookAtOrientation( - const camera_vector_t& position, - const camera_vector_t& targetPosition, - const camera_vector_t& preferredUp, - camera_quaternion_t& outOrientation) - { - const auto toTarget = targetPosition - position; - camera_vector_t right = camera_vector_t(T(0)); - camera_vector_t up = camera_vector_t(T(0)); - camera_vector_t forward = camera_vector_t(T(0)); - if (!tryBuildCameraBasisFromForwardUpHint(toTarget, preferredUp, right, up, forward)) - return false; - - outOrientation = makeQuaternionFromBasis(right, up, forward); - return true; - } - - template - static inline bool tryExtractRigidPoseFromTransform( - const camera_matrix_t& transform, - camera_vector_t& outTranslation, - camera_quaternion_t& outOrientation) - { - SRigidTransformComponents components; - if (!tryExtractRigidTransformComponents(transform, components)) - return false; - - outTranslation = components.translation; - outOrientation = components.orientation; - return true; - } - - template - static inline bool tryBuildSphericalPoseFromOrbit( - const camera_vector_t& targetPosition, - const camera_vector_t& orbitUv, - const T distance, - const T minDistance, - const T maxDistance, - camera_vector_t& outPosition, - camera_quaternion_t& outOrientation, - T* outAppliedDistance = nullptr) - { - if (!isFiniteScalar(orbitUv.x) || - !isFiniteScalar(orbitUv.y) || - !isFiniteScalar(distance)) - return false; - - const T appliedDistance = std::clamp(distance, minDistance, maxDistance); - const auto spherePosition = makeSphericalOffsetFromOrbit(orbitUv, appliedDistance); - const auto upHint = safeNormalizeVec3( - camera_vector_t( - -hlsl::sin(orbitUv.y) * hlsl::cos(orbitUv.x), - -hlsl::sin(orbitUv.y) * hlsl::sin(orbitUv.x), - hlsl::cos(orbitUv.y)), - getCameraWorldForward()); - camera_vector_t right = camera_vector_t(T(0)); - camera_vector_t up = camera_vector_t(T(0)); - camera_vector_t forward = camera_vector_t(T(0)); - if (!tryBuildCameraBasisFromForwardUpHint(-spherePosition, upHint, right, up, forward)) - return false; - - outPosition = targetPosition + spherePosition; - outOrientation = makeQuaternionFromBasis(right, up, forward); - if (outAppliedDistance) - *outAppliedDistance = appliedDistance; - return true; - } - - template - static inline bool tryBuildOrbitFromPosition( - const camera_vector_t& targetPosition, - const camera_vector_t& position, - const T minDistance, - const T maxDistance, - camera_vector_t& outOrbitUv, - T& outDistance) - { - const auto offset = position - targetPosition; - const auto distance = length(offset); - if (!isFiniteScalar(distance) || distance <= getCameraMathEpsilon()) - return false; - - outDistance = std::clamp(distance, minDistance, maxDistance); - const auto local = offset / outDistance; - outOrbitUv = camera_vector_t( - hlsl::atan2(local.y, local.x), - hlsl::asin(std::clamp(local.z, T(-1), T(1)))); - return isFiniteScalar(outOrbitUv.x) && - isFiniteScalar(outOrbitUv.y) && - isFiniteScalar(outDistance); - } - - template - static inline camera_vector_t getPitchYawFromForwardVector(const camera_vector_t& forward) - { - const T planarLength = length(camera_vector_t(forward.x, forward.z)); - return camera_vector_t( - hlsl::atan2(planarLength, forward.y) - numbers::pi * T(0.5), - hlsl::atan2(forward.x, forward.z)); - } - - template - static inline camera_vector_t getPitchYawFromOrientation(const camera_quaternion_t& orientation) - { - const auto forward = normalizeQuaternion(orientation).transformVector(camera_vector_t(T(0), T(0), T(1)), true); - return getPitchYawFromForwardVector(forward); - } - - template - static inline bool tryBuildPathPoseFromState( - const camera_vector_t& targetPosition, - const T pathS, - const T pathU, - const T pathV, - const T pathRoll, - const T minRadius, - const T minDistance, - const T maxDistance, - camera_vector_t& outPosition, - camera_quaternion_t& outOrientation, - T* outAppliedDistance = nullptr, - camera_vector_t* outOrbitUv = nullptr) - { - if (!isFiniteScalar(pathS) || - !isFiniteScalar(pathU) || - !isFiniteScalar(pathV) || - !isFiniteScalar(pathRoll)) - return false; - - const T appliedU = std::max(minRadius, pathU); - const auto offset = makePathOffsetFromState(pathS, appliedU, pathV); - - camera_vector_t orbitUv = camera_vector_t(T(0)); - T distance = T(0); - if (!tryBuildOrbitFromPosition(targetPosition, targetPosition + offset, minDistance, maxDistance, orbitUv, distance)) - return false; - if (!tryBuildSphericalPoseFromOrbit(targetPosition, orbitUv, distance, minDistance, maxDistance, outPosition, outOrientation, &distance)) - return false; - - if (!isNearlyZeroScalar(pathRoll, std::numeric_limits::epsilon())) - { - const auto basis = getQuaternionBasisMatrix(outOrientation); - const T rollCos = hlsl::cos(pathRoll); - const T rollSin = hlsl::sin(pathRoll); - const auto right = basis[0u] * rollCos + basis[1u] * rollSin; - const auto up = basis[1u] * rollCos - basis[0u] * rollSin; - outOrientation = makeQuaternionFromBasis(right, up, basis[2u]); - } - - if (outAppliedDistance) - *outAppliedDistance = distance; - if (outOrbitUv) - *outOrbitUv = orbitUv; - return true; - } - - template - static inline camera_vector_t rotateVectorByQuaternion(const camera_quaternion_t& orientation, const camera_vector_t& vectorToRotate) - { - return normalizeQuaternion(orientation).transformVector(vectorToRotate, true); - } - - template - static inline camera_vector_t projectWorldVectorToLocalBasis( - const camera_vector_t& worldVector, - const camera_vector_t& right, - const camera_vector_t& up, - const camera_vector_t& forward) - { - return camera_vector_t( - dot(worldVector, right), - dot(worldVector, up), - dot(worldVector, forward)); - } - - template - static inline camera_vector_t transformLocalVectorToWorldBasis( - const camera_vector_t& localVector, - const camera_vector_t& right, - const camera_vector_t& up, - const camera_vector_t& forward) - { - return right * localVector.x + up * localVector.y + forward * localVector.z; - } - - template - static inline camera_vector_t getQuaternionEulerRadians(const camera_quaternion_t& orientation) - { - const auto q = normalizeQuaternion(orientation); - const T x = q.data.x; - const T y = q.data.y; - const T z = q.data.z; - const T w = q.data.w; - - const T pitch = hlsl::atan2( - T(2) * (y * z + w * x), - w * w - x * x - y * y + z * z); - const T yaw = hlsl::asin(std::clamp( - T(-2) * (x * z - w * y), - T(-1), - T(1))); - const T roll = hlsl::atan2( - T(2) * (x * y + w * z), - w * w + x * x - y * y - z * z); - - return camera_vector_t(pitch, yaw, roll); - } - - template - static inline camera_vector_t getQuaternionEulerDegrees(const camera_quaternion_t& orientation) - { - const auto eulerRadians = getQuaternionEulerRadians(orientation); - return camera_vector_t( - degrees(eulerRadians.x), - degrees(eulerRadians.y), - degrees(eulerRadians.z)); - } - - template - static inline T getQuaternionAngularDistanceRadians(const camera_quaternion_t& lhs, const camera_quaternion_t& rhs) - { - const auto lhsNormalized = normalizeQuaternion(lhs); - const auto rhsNormalized = normalizeQuaternion(rhs); - const T orientationDot = std::clamp( - static_cast(hlsl::abs(dot(lhsNormalized.data, rhsNormalized.data))), - T(0), - T(1)); - return T(2) * hlsl::acos(orientationDot); - } - - template - static inline T getQuaternionAngularDistanceDegrees(const camera_quaternion_t& lhs, const camera_quaternion_t& rhs) - { - return degrees(getQuaternionAngularDistanceRadians(lhs, rhs)); - } - - template - static inline bool tryComputePoseDelta( - const camera_vector_t& lhsPosition, - const camera_quaternion_t& lhsOrientation, - const camera_vector_t& rhsPosition, - const camera_quaternion_t& rhsOrientation, - SCameraPoseDelta& outDelta) - { - outDelta = {}; - - const auto lhsNormalized = normalizeQuaternion(lhsOrientation); - const auto rhsNormalized = normalizeQuaternion(rhsOrientation); - if (!isFiniteVec3(lhsPosition) || !isFiniteVec3(rhsPosition) || - !isFiniteQuaternion(lhsNormalized) || !isFiniteQuaternion(rhsNormalized)) - { - return false; - } - - outDelta.position = length(lhsPosition - rhsPosition); - outDelta.rotationDeg = getQuaternionAngularDistanceDegrees(lhsNormalized, rhsNormalized); - return isFiniteScalar(outDelta.position) && isFiniteScalar(outDelta.rotationDeg); - } - - template - static inline camera_quaternion_t slerpQuaternion(const camera_quaternion_t& lhs, const camera_quaternion_t& rhs, const T alpha) - { - return camera_quaternion_t::slerp(normalizeQuaternion(lhs), normalizeQuaternion(rhs), alpha); - } - - template - static inline camera_quaternion_t inverseQuaternion(const camera_quaternion_t& q) - { - return inverse(q); - } - - template - static inline camera_vector_t projectWorldVectorToLocalQuaternionFrame( - const camera_quaternion_t& orientation, - const camera_vector_t& worldVector) - { - return rotateVectorByQuaternion(inverseQuaternion(orientation), worldVector); - } - - template - static inline camera_matrix_t getQuaternionBasisMatrix(const camera_quaternion_t& orientation) - { - const auto q = normalizeQuaternion(orientation); - return camera_matrix_t( - q.transformVector(camera_vector_t(T(1), T(0), T(0)), true), - q.transformVector(camera_vector_t(T(0), T(1), T(0)), true), - q.transformVector(camera_vector_t(T(0), T(0), T(1)), true)); - } - - template - static inline camera_vector_t getQuaternionEulerRadiansYXZ(const camera_quaternion_t& orientation) - { - const auto basis = getQuaternionBasisMatrix(orientation); - const T yaw = hlsl::atan2(basis[2][0], basis[2][2]); - const T c2 = hlsl::length(camera_vector_t(basis[0][1], basis[1][1])); - const T pitch = hlsl::atan2(-basis[2][1], c2); - const T s1 = hlsl::sin(yaw); - const T c1 = hlsl::cos(yaw); - const T roll = hlsl::atan2( - s1 * basis[1][2] - c1 * basis[1][0], - c1 * basis[0][0] - s1 * basis[0][2]); - return camera_vector_t(pitch, yaw, roll); - } - - template - static inline camera_vector_t getQuaternionEulerDegreesYXZ(const camera_quaternion_t& orientation) - { - const auto eulerRadians = getQuaternionEulerRadiansYXZ(orientation); - return camera_vector_t( - degrees(eulerRadians.x), - degrees(eulerRadians.y), - degrees(eulerRadians.z)); - } - - template - static inline camera_vector_t getCameraOrientationEulerRadians(const camera_quaternion_t& orientation) - { - return getQuaternionEulerRadiansYXZ(orientation); - } - - template - static inline camera_vector_t getCameraOrientationEulerDegrees(const camera_quaternion_t& orientation) - { - return getQuaternionEulerDegreesYXZ(orientation); - } - - template - static inline camera_vector_t getOrientationDeltaEulerRadiansYXZ( - const camera_quaternion_t& from, - const camera_quaternion_t& to) - { - const auto deltaQuat = inverseQuaternion(from) * normalizeQuaternion(to); - return getQuaternionEulerRadiansYXZ(deltaQuat); - } - - template - static inline camera_vector_t getWrappedEulerDistanceDegrees( - const camera_vector_t& a, - const camera_vector_t& b) - { - return camera_vector_t( - getWrappedAngleDistanceDegrees(a.x, b.x), - getWrappedAngleDistanceDegrees(a.y, b.y), - getWrappedAngleDistanceDegrees(a.z, b.z)); - } - - template - static inline T getMaxVectorComponent(const camera_vector_t& value) - { - return std::max(value.x, std::max(value.y, value.z)); - } - - template - static inline camera_matrix_t composeTransformMatrix( - const camera_vector_t& translation, - const camera_quaternion_t& orientation, - const camera_vector_t& scale = camera_vector_t(T(1))) - { - camera_matrix_t output = camera_matrix_t(1); - const auto basis = getQuaternionBasisMatrix(orientation); - output[0] = camera_vector_t(basis[0] * scale.x, T(0)); - output[1] = camera_vector_t(basis[1] * scale.y, T(0)); - output[2] = camera_vector_t(basis[2] * scale.z, T(0)); - output[3] = camera_vector_t(translation, T(1)); - return output; - } - - template - static inline bool tryExtractRigidTransformComponents( - const camera_matrix_t& transform, - SRigidTransformComponents& outComponents) - { - outComponents.translation = camera_vector_t(transform[3].x, transform[3].y, transform[3].z); - - auto right = camera_vector_t(transform[0].x, transform[0].y, transform[0].z); - auto up = camera_vector_t(transform[1].x, transform[1].y, transform[1].z); - auto forward = camera_vector_t(transform[2].x, transform[2].y, transform[2].z); - - outComponents.scale = camera_vector_t(length(right), length(up), length(forward)); - - if (!isFiniteVec3(outComponents.translation) || !isFiniteVec3(outComponents.scale)) - return false; - - constexpr T Epsilon = std::numeric_limits::epsilon(); - if (outComponents.scale.x <= Epsilon || outComponents.scale.y <= Epsilon || outComponents.scale.z <= Epsilon) - return false; - - right /= outComponents.scale.x; - up /= outComponents.scale.y; - forward /= outComponents.scale.z; - if (!isOrthoBase(right, up, forward)) - return false; - - outComponents.orientation = makeQuaternionFromBasis(right, up, forward); - return isFiniteQuaternion(outComponents.orientation); - } - - template - static inline bool tryBuildRigidFrameFromTransform( - const camera_matrix_t& transform, - camera_matrix_t& outFrame, - camera_quaternion_t& outOrientation) - { - SRigidTransformComponents components; - if (!tryExtractRigidTransformComponents(transform, components)) - return false; - - outOrientation = components.orientation; - outFrame = composeTransformMatrix(components.translation, components.orientation); - return true; - } - - template - static inline bool decomposeTransformMatrix( - const camera_matrix_t& transform, - camera_vector_t& outTranslation, - camera_vector_t& outRotationEulerDegrees, - camera_vector_t& outScale) - { - SRigidTransformComponents components; - if (!tryExtractRigidTransformComponents(transform, components)) - return false; - - outTranslation = components.translation; - outScale = components.scale; - outRotationEulerDegrees = getCameraOrientationEulerDegrees(components.orientation); - return isFiniteVec3(outRotationEulerDegrees); - } -}; - -} // namespace nbl::hlsl - -#endif // _C_CAMERA_MATH_UTILITIES_HPP_ diff --git a/common/include/camera/CCameraPathMetadata.hpp b/common/include/camera/CCameraPathMetadata.hpp deleted file mode 100644 index 0ccf2ac6e..000000000 --- a/common/include/camera/CCameraPathMetadata.hpp +++ /dev/null @@ -1,30 +0,0 @@ -// This file is part of the "Nabla Engine". -// For conditions of distribution and use, see copyright notice in nabla.h - -#ifndef _C_CAMERA_PATH_METADATA_HPP_ -#define _C_CAMERA_PATH_METADATA_HPP_ - -#include - -namespace nbl::core -{ - -/// @brief Stable descriptive strings used by the reusable `Path Rig` camera kind. -/// -/// This metadata lives in a lightweight header so code that only needs labels -/// or identifiers does not have to include the full path-model implementation. -struct SCameraPathRigMetadata final -{ - /// @brief User-facing camera kind label. - static inline constexpr std::string_view KindLabel = "Path Rig"; - /// @brief Short user-facing description of the camera kind. - static inline constexpr std::string_view KindDescription = "Path-model camera with typed s/u/v/roll state"; - /// @brief Default runtime identifier used by the concrete camera instance. - static inline constexpr std::string_view Identifier = "Target-relative Path Rig"; - /// @brief Default description of the built-in path model shipped by the shared API. - static inline constexpr std::string_view DefaultModelDescription = "Adjust a target-relative path rig with s/u/v/roll state"; -}; - -} // namespace nbl::core - -#endif // _C_CAMERA_PATH_METADATA_HPP_ diff --git a/common/include/camera/CCameraPathUtilities.hpp b/common/include/camera/CCameraPathUtilities.hpp deleted file mode 100644 index 736b944f5..000000000 --- a/common/include/camera/CCameraPathUtilities.hpp +++ /dev/null @@ -1,575 +0,0 @@ -#ifndef _C_CAMERA_PATH_UTILITIES_HPP_ -#define _C_CAMERA_PATH_UTILITIES_HPP_ - -#include -#include -#include -#include -#include -#include - -#include "CCameraPathMetadata.hpp" -#include "CCameraTargetRelativeUtilities.hpp" -#include "CCameraVirtualEventUtilities.hpp" -#include "ICamera.hpp" - -namespace nbl::core -{ - -/// @brief Shared helpers for the reusable `PathRig` camera kind. -struct SCameraPathPose final : SCameraRigPose -{ - /// @brief Final radial distance actually applied after clamping and path-state sanitization. - hlsl::float64_t appliedDistance = 0.0; - /// @brief Canonical orbit yaw/pitch derived from the evaluated path state. - hlsl::float64_t2 orbitUv = hlsl::float64_t2(0.0); -}; - -/// @brief Typed delta applied to `ICamera::PathState`. -struct SCameraPathDelta final : ICamera::PathState -{ - /// @brief Pack the delta into one four-component vector. - inline hlsl::float64_t4 asVector() const - { - return ICamera::PathState::asVector(); - } - - /// @brief Reinterpret the delta as the translation-style helper representation. - inline hlsl::float64_t3 translationVector() const - { - return ICamera::PathState::asTranslationVector(); - } - - /// @brief Rebuild the delta from the packed vector representation. - static inline SCameraPathDelta fromVector(const hlsl::float64_t4& value) - { - SCameraPathDelta delta = {}; - delta.s = value.x; - delta.u = value.y; - delta.v = value.z; - delta.roll = value.w; - return delta; - } - - /// @brief Rebuild the delta from a translation-style helper vector and optional roll value. - static inline SCameraPathDelta fromMotion(const hlsl::float64_t3& translation, const double pathRoll = 0.0) - { - SCameraPathDelta delta = {}; - delta.s = translation.z; - delta.u = translation.x; - delta.v = translation.y; - delta.roll = pathRoll; - return delta; - } -}; - -/// @brief One desired path-state change expressed as current state, desired state, and their delta. -struct SCameraPathStateTransition final -{ - ICamera::PathState current = {}; - ICamera::PathState desired = {}; - SCameraPathDelta delta = {}; -}; - -/// @brief Canonical evaluated path state combining a final pose and target-relative view of that pose. -struct SCameraCanonicalPathState final -{ - SCameraPathPose pose = {}; - SCameraTargetRelativeState targetRelative = {}; -}; - -/// @brief Comparison tolerances used when matching two path states. -struct SCameraPathComparisonThresholds final -{ - double sToleranceDeg = SCameraToolingThresholds::DefaultAngularToleranceDeg; - double rollToleranceDeg = SCameraToolingThresholds::DefaultAngularToleranceDeg; - double scalarTolerance = SCameraToolingThresholds::ScalarTolerance; -}; - -/// @brief Result of updating the path distance while preserving the rest of the path state. -struct SCameraPathDistanceUpdateResult final -{ - bool exact = false; - hlsl::float64_t appliedDistance = 0.0; -}; - -/// @brief Default constants used by the built-in `Path Rig` model. -struct SCameraPathDefaults final -{ - static constexpr double MinU = static_cast(SCameraTargetRelativeTraits::MinDistance); - static constexpr double ScalarTolerance = SCameraToolingThresholds::ScalarTolerance; - static constexpr double ExactStateTolerance = SCameraToolingThresholds::TinyScalarEpsilon; - static constexpr double ExactAngleToleranceDeg = ExactStateTolerance * 180.0 / hlsl::numbers::pi; - static constexpr double AngleToleranceDeg = SCameraToolingThresholds::DefaultAngularToleranceDeg; - static inline constexpr std::string_view Identifier = SCameraPathRigMetadata::Identifier; - static inline constexpr std::string_view Description = SCameraPathRigMetadata::DefaultModelDescription; - static inline constexpr ICamera::PathStateLimits Limits = {}; - static inline constexpr SCameraPathComparisonThresholds ComparisonThresholds = { - .sToleranceDeg = AngleToleranceDeg, - .rollToleranceDeg = AngleToleranceDeg, - .scalarTolerance = ScalarTolerance - }; - static inline constexpr SCameraPathComparisonThresholds ExactComparisonThresholds = { - .sToleranceDeg = ExactAngleToleranceDeg, - .rollToleranceDeg = ExactAngleToleranceDeg, - .scalarTolerance = ExactStateTolerance - }; -}; - -using SCameraPathLimits = ICamera::PathStateLimits; - -/// @brief Evaluation context passed into the active path-model control law. -struct SCameraPathControlContext final -{ - ICamera::PathState currentState = {}; - hlsl::float64_t3 translation = hlsl::float64_t3(0.0); - hlsl::float64_t3 rotation = hlsl::float64_t3(0.0); - hlsl::float64_t3 targetPosition = hlsl::float64_t3(0.0); - const CReferenceTransform* reference = nullptr; - SCameraPathLimits limits = SCameraPathDefaults::Limits; -}; - -/// @brief Callback bundle defining path-state resolution, input response, evaluation, and distance updates. -/// -/// A concrete `Path Rig` model provides: -/// - state resolution from target position, world position, and optional typed input -/// - one control law turning accumulated runtime motion into `SCameraPathDelta` -/// - one state integrator -/// - one canonical evaluator producing pose and target-relative view data -/// - one distance-update rule for typed helpers that adjust distance directly -struct SCameraPathModel final -{ - using resolve_state_t = std::function; - using control_law_t = std::function; - using integrate_t = std::function; - using evaluate_t = std::function; - using update_distance_t = std::function; - - resolve_state_t resolveState; - control_law_t controlLaw; - integrate_t integrate; - evaluate_t evaluate; - update_distance_t updateDistance; -}; - -/// @brief Shared state, comparison, and model-building helpers for `Path Rig`. -struct CCameraPathUtilities final -{ - /// @brief Build the default path state used by the built-in model. - static inline ICamera::PathState makeDefaultPathState(const double minU = SCameraPathDefaults::MinU) - { - return { - .s = 0.0, - .u = minU, - .v = 0.0, - .roll = 0.0 - }; - } - - /// @brief Build path-state comparison tolerances from caller-provided angular and scalar thresholds. - static inline SCameraPathComparisonThresholds makePathComparisonThresholds( - const double angularToleranceDeg = SCameraPathDefaults::AngleToleranceDeg, - const double scalarTolerance = SCameraPathDefaults::ScalarTolerance) - { - return { - .sToleranceDeg = angularToleranceDeg, - .rollToleranceDeg = angularToleranceDeg, - .scalarTolerance = scalarTolerance - }; - } - - /// @brief Return the default path-state limits used when a camera does not expose custom ones. - static inline constexpr SCameraPathLimits makeDefaultPathLimits() - { - return SCameraPathDefaults::Limits; - } - - /// @brief Check whether every scalar stored in the path state is finite. - static inline bool isPathStateFinite(const ICamera::PathState& state) - { - return hlsl::CCameraMathUtilities::isFiniteScalar(state.s) && - hlsl::CCameraMathUtilities::isFiniteScalar(state.u) && - hlsl::CCameraMathUtilities::isFiniteScalar(state.v) && - hlsl::CCameraMathUtilities::isFiniteScalar(state.roll); - } - - /// @brief Check whether the path limits can be sanitized into a valid numeric domain. - static inline bool isPathLimitsWellFormed(const SCameraPathLimits& limits) - { - return hlsl::CCameraMathUtilities::isFiniteScalar(limits.minU) && - hlsl::CCameraMathUtilities::isFiniteScalar(limits.minDistance) && - !std::isnan(static_cast(limits.maxDistance)); - } - - /// @brief Clamp and normalize path-state limits into a valid numeric domain. - static inline bool sanitizePathLimits(SCameraPathLimits& limits) - { - if (!isPathLimitsWellFormed(limits)) - return false; - - limits.minU = std::max(limits.minU, 0.0); - limits.minDistance = std::max( - std::max(limits.minDistance, static_cast(limits.minU)), - static_cast(SCameraTargetRelativeTraits::MinDistance)); - - if (!std::isfinite(static_cast(limits.maxDistance))) - limits.maxDistance = std::numeric_limits::infinity(); - else - limits.maxDistance = std::max(limits.maxDistance, limits.minDistance); - return true; - } - - /// @brief Sanitize a path state against a caller-provided `minU` lower bound. - static inline bool sanitizePathState(ICamera::PathState& state, const double minU) - { - return hlsl::CCameraMathUtilities::sanitizePathState(state.s, state.u, state.v, state.roll, minU); - } - - /// @brief Sanitize a path state against a full limit bundle and optionally report the applied distance. - static inline bool sanitizePathState(ICamera::PathState& state, const SCameraPathLimits& limits, double* outAppliedDistance = nullptr) - { - SCameraPathLimits sanitizedLimits = limits; - if (!sanitizePathLimits(sanitizedLimits)) - return false; - - if (!sanitizePathState(state, sanitizedLimits.minU)) - return false; - - const auto desiredDistance = std::clamp( - hlsl::CCameraMathUtilities::getPathDistance(state.u, state.v), - sanitizedLimits.minDistance, - sanitizedLimits.maxDistance); - return tryScalePathStateDistance(desiredDistance, sanitizedLimits.minU, state, outAppliedDistance); - } - - /// @brief Rescale the `(u, v)` pair so the path state reaches the requested radial distance. - static inline bool tryScalePathStateDistance( - const double desiredDistance, - const double minU, - ICamera::PathState& ioState, - double* outAppliedDistance = nullptr) - { - return hlsl::CCameraMathUtilities::tryScalePathStateDistance( - desiredDistance, - minU, - ioState.u, - ioState.v, - outAppliedDistance); - } - - /// @brief Update the distance encoded by a path state while respecting the provided limits. - static inline bool tryUpdatePathStateDistance( - const float desiredDistance, - const SCameraPathLimits& limits, - ICamera::PathState& ioState, - SCameraPathDistanceUpdateResult* outResult = nullptr) - { - SCameraPathLimits sanitizedLimits = limits; - if (!sanitizePathLimits(sanitizedLimits) || !sanitizePathState(ioState, sanitizedLimits)) - return false; - - const auto clampedDistance = std::clamp(desiredDistance, sanitizedLimits.minDistance, sanitizedLimits.maxDistance); - double appliedDistance = 0.0; - if (!tryScalePathStateDistance(static_cast(clampedDistance), sanitizedLimits.minU, ioState, &appliedDistance)) - return false; - - if (outResult) - { - outResult->appliedDistance = appliedDistance; - outResult->exact = (clampedDistance == desiredDistance) && - hlsl::CCameraMathUtilities::nearlyEqualScalar(appliedDistance, static_cast(desiredDistance), SCameraPathDefaults::ScalarTolerance); - } - return true; - } - - static inline bool tryBuildPathStateFromPosition( - const hlsl::float64_t3& targetPosition, - const hlsl::float64_t3& position, - const double minU, - ICamera::PathState& outState) - { - outState = {}; - if (!hlsl::CCameraMathUtilities::tryBuildPathStateFromPosition( - targetPosition, - position, - minU, - outState.s, - outState.u, - outState.v)) - { - return false; - } - - outState.roll = 0.0; - return true; - } - - static inline bool tryResolvePathState( - const hlsl::float64_t3& targetPosition, - const hlsl::float64_t3& position, - const SCameraPathLimits& limits, - const ICamera::PathState* requestedState, - ICamera::PathState& outState) - { - SCameraPathLimits sanitizedLimits = limits; - if (!sanitizePathLimits(sanitizedLimits)) - return false; - - if (requestedState) - { - outState = *requestedState; - return sanitizePathState(outState, sanitizedLimits); - } - - if (tryBuildPathStateFromPosition(targetPosition, position, sanitizedLimits.minU, outState)) - return sanitizePathState(outState, sanitizedLimits); - - outState = makeDefaultPathState(sanitizedLimits.minU); - return sanitizePathState(outState, sanitizedLimits); - } - - static inline bool tryBuildPathPoseFromState( - const hlsl::float64_t3& targetPosition, - const ICamera::PathState& state, - const SCameraPathLimits& limits, - SCameraPathPose& outPose) - { - SCameraPathLimits sanitizedLimits = limits; - if (!sanitizePathLimits(sanitizedLimits)) - return false; - - return hlsl::CCameraMathUtilities::tryBuildPathPoseFromState( - targetPosition, - state.s, - state.u, - state.v, - state.roll, - sanitizedLimits.minU, - sanitizedLimits.minDistance, - sanitizedLimits.maxDistance, - outPose.position, - outPose.orientation, - &outPose.appliedDistance, - &outPose.orbitUv); - } - - static inline bool tryBuildPathPoseFromState( - const hlsl::float64_t3& targetPosition, - const ICamera::PathState& state, - const SCameraPathLimits& limits, - hlsl::float64_t3& outPosition, - hlsl::camera_quaternion_t& outOrientation, - hlsl::float64_t* outAppliedDistance = nullptr, - hlsl::float64_t2* outOrbitUv = nullptr) - { - SCameraPathPose pathPose = {}; - if (!tryBuildPathPoseFromState(targetPosition, state, limits, pathPose)) - return false; - - outPosition = pathPose.position; - outOrientation = pathPose.orientation; - if (outAppliedDistance) - *outAppliedDistance = pathPose.appliedDistance; - if (outOrbitUv) - *outOrbitUv = pathPose.orbitUv; - return true; - } - - static inline bool pathStatesNearlyEqual( - const ICamera::PathState& lhs, - const ICamera::PathState& rhs, - const SCameraPathComparisonThresholds& thresholds = {}) - { - return hlsl::CCameraMathUtilities::getWrappedAngleDistanceDegrees(lhs.s, rhs.s) <= thresholds.sToleranceDeg && - hlsl::CCameraMathUtilities::nearlyEqualScalar(lhs.u, rhs.u, thresholds.scalarTolerance) && - hlsl::CCameraMathUtilities::nearlyEqualScalar(lhs.v, rhs.v, thresholds.scalarTolerance) && - hlsl::CCameraMathUtilities::getWrappedAngleDistanceDegrees(lhs.roll, rhs.roll) <= thresholds.rollToleranceDeg; - } - - static inline bool pathStatesChanged( - const ICamera::PathState& lhs, - const ICamera::PathState& rhs, - const SCameraPathComparisonThresholds& thresholds = {}) - { - return !pathStatesNearlyEqual(lhs, rhs, thresholds); - } - - static inline hlsl::float64_t4 buildPathStateDeltaVector( - const ICamera::PathState& currentState, - const ICamera::PathState& desiredState) - { - auto deltaVector = desiredState.asVector() - currentState.asVector(); - deltaVector.x = hlsl::CCameraMathUtilities::wrapAngleRad(deltaVector.x); - deltaVector.w = hlsl::CCameraMathUtilities::wrapAngleRad(deltaVector.w); - return deltaVector; - } - - static inline SCameraPathDelta buildPathStateDelta( - const ICamera::PathState& currentState, - const ICamera::PathState& desiredState) - { - return SCameraPathDelta::fromVector(buildPathStateDeltaVector(currentState, desiredState)); - } - - static inline SCameraPathDelta makePathDeltaFromVirtualPathMotion( - const hlsl::float64_t3& translation, - const hlsl::float64_t3& rotation = hlsl::float64_t3(0.0)) - { - return SCameraPathDelta::fromMotion(translation, rotation.z); - } - - static inline SCameraPathDelta buildDefaultPathControlDelta(const SCameraPathControlContext& context) - { - return makePathDeltaFromVirtualPathMotion(context.translation, context.rotation); - } - - static inline void appendPathDeltaEvents( - std::vector& events, - const SCameraPathDelta& delta, - const double moveDenominator, - const double rotationDenominator, - const SCameraPathComparisonThresholds& thresholds = {}) - { - CCameraVirtualEventUtilities::appendLocalTranslationEvents( - events, - delta.translationVector(), - hlsl::float64_t3(moveDenominator), - hlsl::float64_t3(thresholds.scalarTolerance)); - CCameraVirtualEventUtilities::appendAngularDeltaEvent( - events, - delta.roll, - rotationDenominator, - thresholds.rollToleranceDeg, - CVirtualGimbalEvent::RollRight, - CVirtualGimbalEvent::RollLeft); - } - - static inline bool tryBuildCanonicalPathState( - const hlsl::float64_t3& targetPosition, - const ICamera::PathState& state, - const SCameraPathLimits& limits, - SCameraCanonicalPathState& outState) - { - outState = {}; - if (!tryBuildPathPoseFromState(targetPosition, state, limits, outState.pose)) - return false; - - outState.targetRelative = { - .target = targetPosition, - .orbitUv = outState.pose.orbitUv, - .distance = static_cast(outState.pose.appliedDistance) - }; - return true; - } - - static inline bool tryApplyPathStateDelta( - const ICamera::PathState& currentState, - const SCameraPathDelta& delta, - const SCameraPathLimits& limits, - ICamera::PathState& outState) - { - auto stateVector = currentState.asVector() + delta.asVector(); - stateVector.x = hlsl::CCameraMathUtilities::wrapAngleRad(stateVector.x); - stateVector.w = hlsl::CCameraMathUtilities::wrapAngleRad(stateVector.w); - outState = ICamera::PathState::fromVector(stateVector); - return sanitizePathState(outState, limits); - } - - static inline ICamera::PathState blendPathStates( - const ICamera::PathState& from, - const ICamera::PathState& to, - const double alpha) - { - const auto fromVector = from.asVector(); - const auto toVector = to.asVector(); - return { - .s = hlsl::CCameraMathUtilities::lerpWrappedAngleRad(fromVector.x, toVector.x, alpha), - .u = fromVector.y + (toVector.y - fromVector.y) * alpha, - .v = fromVector.z + (toVector.z - fromVector.z) * alpha, - .roll = hlsl::CCameraMathUtilities::lerpWrappedAngleRad(fromVector.w, toVector.w, alpha) - }; - } - - static inline bool tryBuildPathStateTransition( - const hlsl::float64_t3& targetPosition, - const hlsl::float64_t3& currentPosition, - const hlsl::float64_t3& desiredPosition, - const SCameraPathLimits& limits, - const ICamera::PathState* currentStateOverride, - const ICamera::PathState* desiredStateOverride, - SCameraPathStateTransition& outTransition) - { - if (!tryResolvePathState(targetPosition, currentPosition, limits, currentStateOverride, outTransition.current)) - return false; - if (!tryResolvePathState(targetPosition, desiredPosition, limits, desiredStateOverride, outTransition.desired)) - return false; - - outTransition.delta = buildPathStateDelta(outTransition.current, outTransition.desired); - return true; - } - - static inline SCameraPathModel makeDefaultPathModel() - { - return { - .resolveState = - [](const hlsl::float64_t3& targetPosition, - const hlsl::float64_t3& position, - const SCameraPathLimits& limits, - const ICamera::PathState* requestedState, - ICamera::PathState& outState) -> bool - { - return tryResolvePathState(targetPosition, position, limits, requestedState, outState); - }, - .controlLaw = - [](const SCameraPathControlContext& context) -> SCameraPathDelta - { - return buildDefaultPathControlDelta(context); - }, - .integrate = - [](const ICamera::PathState& currentState, - const SCameraPathDelta& delta, - const SCameraPathLimits& limits, - ICamera::PathState& outState) -> bool - { - return tryApplyPathStateDelta(currentState, delta, limits, outState); - }, - .evaluate = - [](const hlsl::float64_t3& targetPosition, - const ICamera::PathState& state, - const SCameraPathLimits& limits, - SCameraCanonicalPathState& outState) -> bool - { - return tryBuildCanonicalPathState(targetPosition, state, limits, outState); - }, - .updateDistance = - [](const float desiredDistance, - const SCameraPathLimits& limits, - ICamera::PathState& ioState, - SCameraPathDistanceUpdateResult* outResult) -> bool - { - return tryUpdatePathStateDistance(desiredDistance, limits, ioState, outResult); - } - }; - } -}; - -} // namespace nbl::core - -#endif // _C_CAMERA_PATH_UTILITIES_HPP_ diff --git a/common/include/camera/CCameraPersistence.hpp b/common/include/camera/CCameraPersistence.hpp deleted file mode 100644 index a1f2487ac..000000000 --- a/common/include/camera/CCameraPersistence.hpp +++ /dev/null @@ -1,33 +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 _C_CAMERA_PERSISTENCE_HPP_ -#define _C_CAMERA_PERSISTENCE_HPP_ - -#include -#include -#include - -#include "CCameraKeyframeTrackPersistence.hpp" -#include "CCameraPresetPersistence.hpp" -#include "nbl/system/path.h" - -namespace nbl::system -{ - -class ISystem; - -/// @brief Serialize a preset collection to JSON. -bool writePresetCollection(std::ostream& out, std::span presets, int indent = 2); -/// @brief Parse a preset collection from JSON. -bool readPresetCollection(std::istream& in, std::vector& presets); - -/// @brief Save a preset collection to disk as JSON. -bool savePresetCollectionToFile(ISystem& system, const path& path, std::span presets, int indent = 2); -/// @brief Load a preset collection from disk. -bool loadPresetCollectionFromFile(ISystem& system, const path& path, std::vector& presets); - -} // namespace nbl::system - -#endif // _C_CAMERA_PERSISTENCE_HPP_ diff --git a/common/include/camera/CCameraPlaybackTimeline.hpp b/common/include/camera/CCameraPlaybackTimeline.hpp deleted file mode 100644 index 64dfc811e..000000000 --- a/common/include/camera/CCameraPlaybackTimeline.hpp +++ /dev/null @@ -1,103 +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 _C_CAMERA_PLAYBACK_TIMELINE_HPP_ -#define _C_CAMERA_PLAYBACK_TIMELINE_HPP_ - -#include "CCameraKeyframeTrack.hpp" - -namespace nbl::core -{ - -/// @brief Shared playback cursor state for camera keyframe tracks. -/// The cursor stores playback state only: playing flag, looping mode, speed, and current time. -struct CCameraPlaybackCursor -{ - bool playing = false; - bool loop = true; - float speed = 1.f; - float time = 0.f; -}; - -/// @brief Outcome of advancing a playback cursor against a keyframe track. -/// This result reports how time changed during one advance step. -struct SCameraPlaybackAdvanceResult -{ - bool hasTrack = false; - bool changedTime = false; - bool wrapped = false; - bool reachedEnd = false; - bool stopped = false; - float duration = 0.f; - float time = 0.f; -}; - -struct CCameraPlaybackTimelineUtilities final -{ -public: - /// @brief Duration of the current playback track in seconds. - static inline float getPlaybackTrackDuration(const CCameraKeyframeTrack& track) - { - if (track.keyframes.empty()) - return 0.f; - - return track.keyframes.back().time; - } - - /// @brief Reset cursor time and stop playback without mutating loop or speed settings. - static inline void resetPlaybackCursor(CCameraPlaybackCursor& cursor, const float time = 0.f) - { - cursor.playing = false; - cursor.time = std::max(0.f, time); - } - - /// @brief Clamp cursor time into the valid time range of the current track. - static inline void clampPlaybackCursorToTrack(const CCameraKeyframeTrack& track, CCameraPlaybackCursor& cursor) - { - CCameraKeyframeTrackUtilities::clampTrackTimeToKeyframes(track, cursor.time); - } - - /// @brief Advance cursor time by `dtSec * speed` and report whether playback wrapped or stopped. - static inline SCameraPlaybackAdvanceResult advancePlaybackCursor(CCameraPlaybackCursor& cursor, const CCameraKeyframeTrack& track, const double dtSec) - { - SCameraPlaybackAdvanceResult result; - result.hasTrack = !track.keyframes.empty(); - result.duration = getPlaybackTrackDuration(track); - result.time = cursor.time; - - if (!result.hasTrack || !cursor.playing) - return result; - - const auto previousTime = cursor.time; - cursor.time += static_cast(dtSec * cursor.speed); - result.changedTime = cursor.time != previousTime; - result.time = cursor.time; - - if (result.duration <= 0.f) - return result; - - if (cursor.loop) - { - while (cursor.time > result.duration) - { - cursor.time -= result.duration; - result.wrapped = true; - } - } - else if (cursor.time > result.duration) - { - cursor.time = result.duration; - cursor.playing = false; - result.reachedEnd = true; - result.stopped = true; - } - - result.time = cursor.time; - return result; - } -}; - -} // namespace nbl::core - -#endif // _C_CAMERA_PLAYBACK_TIMELINE_HPP_ diff --git a/common/include/camera/CCameraPresentationUtilities.hpp b/common/include/camera/CCameraPresentationUtilities.hpp deleted file mode 100644 index 9b26db9b7..000000000 --- a/common/include/camera/CCameraPresentationUtilities.hpp +++ /dev/null @@ -1,125 +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 _C_CAMERA_PRESENTATION_UTILITIES_HPP_ -#define _C_CAMERA_PRESENTATION_UTILITIES_HPP_ - -#include - -#include "CCameraTextUtilities.hpp" - -namespace nbl::ui -{ - -/// @brief Shared exactness-oriented filter used by preset presentation surfaces. -enum class EPresetApplyPresentationFilter : uint8_t -{ - All, - Exact, - BestEffort -}; - -/// @brief Shared badge/pill policy derived from one analyzed presentation answer. -struct SCameraGoalApplyPresentationBadges final -{ - bool exact = false; - bool bestEffort = false; - bool dropsState = false; - bool sharedStateOnly = false; - bool blocked = false; -}; - -/// @brief Presentation-ready wrapper around analyzed goal apply compatibility. -struct SCameraGoalApplyPresentation final : core::SCameraGoalApplyAnalysis -{ - SCameraGoalApplyPresentationBadges badges; - std::string sourceKindLabel; - std::string goalStateLabel; - std::string compatibilityLabel; - std::string policyLabel; - - inline bool matchesFilter(const EPresetApplyPresentationFilter mode) const - { - switch (mode) - { - case EPresetApplyPresentationFilter::All: - return true; - case EPresetApplyPresentationFilter::Exact: - return hasCamera && exact(); - case EPresetApplyPresentationFilter::BestEffort: - return hasCamera && !exact(); - default: - return true; - } - } -}; - -/// @brief Presentation-ready wrapper around analyzed camera capture viability. -struct SCameraCapturePresentation final : core::SCameraCaptureAnalysis -{ - std::string policyLabel; -}; - -struct CCameraPresentationUtilities final -{ - /// @brief Shared user-facing label for the exactness filter selector. - static inline const char* getPresetApplyPresentationFilterLabel(const EPresetApplyPresentationFilter mode) - { - switch (mode) - { - case EPresetApplyPresentationFilter::All: - return "All"; - case EPresetApplyPresentationFilter::Exact: - return "Exact"; - case EPresetApplyPresentationFilter::BestEffort: - return "Best-effort"; - default: - return "All"; - } - } - - /// @brief Build reusable badge flags for one preset/keyframe compatibility answer. - static inline SCameraGoalApplyPresentationBadges collectGoalApplyPresentationBadges(const SCameraGoalApplyPresentation& presentation) - { - SCameraGoalApplyPresentationBadges badges; - badges.exact = presentation.exact(); - badges.bestEffort = presentation.hasCamera && !presentation.exact(); - badges.dropsState = presentation.dropsGoalState(); - badges.sharedStateOnly = presentation.usesSharedStateOnly(); - badges.blocked = !presentation.canApply; - return badges; - } - - /// @brief Build presentation text for one analyzed goal-apply result. - static inline SCameraGoalApplyPresentation makeGoalApplyPresentation(const core::SCameraGoalApplyAnalysis& analysis, const core::ICamera* targetCamera) - { - SCameraGoalApplyPresentation presentation; - static_cast(presentation) = analysis; - presentation.badges = collectGoalApplyPresentationBadges(presentation); - presentation.sourceKindLabel = std::string(CCameraTextUtilities::getCameraTypeLabel(presentation.goal.sourceKind)); - presentation.goalStateLabel = CCameraTextUtilities::describeGoalStateMask(presentation.goal.sourceGoalStateMask); - presentation.compatibilityLabel = CCameraTextUtilities::describeGoalApplyCompatibility(analysis, targetCamera); - presentation.policyLabel = CCameraTextUtilities::describeGoalApplyPolicy(analysis); - return presentation; - } - - /// @brief Analyze one preset against one camera and return reusable presentation data. - static inline SCameraGoalApplyPresentation analyzePresetPresentation(const core::CCameraGoalSolver& solver, const core::ICamera* camera, const core::CCameraPreset& preset) - { - return makeGoalApplyPresentation(core::CCameraGoalAnalysisUtilities::analyzePresetApply(solver, camera, preset), camera); - } - - /// @brief Analyze one camera capture path and return reusable presentation data. - static inline SCameraCapturePresentation analyzeCapturePresentation(const core::CCameraGoalSolver& solver, core::ICamera* camera) - { - SCameraCapturePresentation presentation; - static_cast(presentation) = core::CCameraGoalAnalysisUtilities::analyzeCameraCapture(solver, camera); - presentation.policyLabel = CCameraTextUtilities::describeCameraCapturePolicy(presentation, camera); - return presentation; - } -}; - -} // namespace nbl::ui - -#endif // _C_CAMERA_PRESENTATION_UTILITIES_HPP_ diff --git a/common/include/camera/CCameraPreset.hpp b/common/include/camera/CCameraPreset.hpp deleted file mode 100644 index a04be5e8c..000000000 --- a/common/include/camera/CCameraPreset.hpp +++ /dev/null @@ -1,71 +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 _C_CAMERA_PRESET_HPP_ -#define _C_CAMERA_PRESET_HPP_ - -#include -#include - -#include "CCameraGoal.hpp" - -namespace nbl::core -{ - -/// @brief Named persisted camera state built on top of `CCameraGoal`. -struct CCameraPreset -{ - std::string name; - std::string identifier; - CCameraGoal goal = {}; -}; - -/// @brief Time-stamped preset entry used by playback and authoring tools. -struct CCameraKeyframe -{ - CCameraPreset preset; - float time = 0.f; -}; - -struct CCameraPresetUtilities final -{ - static inline void assignGoalToPreset(CCameraPreset& preset, const CCameraGoal& goal) - { - preset.goal = CCameraGoalUtilities::canonicalizeGoal(goal); - } - - static inline CCameraGoal makeGoalFromPreset(const CCameraPreset& preset) - { - return CCameraGoalUtilities::canonicalizeGoal(preset.goal); - } - - /// @brief Compare two named presets through their shared canonical goal state. - static inline bool comparePresets(const CCameraPreset& lhs, const CCameraPreset& rhs, - const double posEps, const double rotEpsDeg, const double scalarEps) - { - return lhs.name == rhs.name && - lhs.identifier == rhs.identifier && - CCameraGoalUtilities::compareGoals(makeGoalFromPreset(lhs), makeGoalFromPreset(rhs), posEps, rotEpsDeg, scalarEps); - } - - /// @brief Compare two preset collections element-by-element through the shared canonical goal state. - static inline bool comparePresetCollections(std::span lhs, std::span rhs, - const double posEps, const double rotEpsDeg, const double scalarEps) - { - if (lhs.size() != rhs.size()) - return false; - - for (size_t i = 0u; i < lhs.size(); ++i) - { - if (!comparePresets(lhs[i], rhs[i], posEps, rotEpsDeg, scalarEps)) - return false; - } - - return true; - } -}; - -} // namespace nbl::core - -#endif // _C_CAMERA_PRESET_HPP_ diff --git a/common/include/camera/CCameraPresetFlow.hpp b/common/include/camera/CCameraPresetFlow.hpp deleted file mode 100644 index 8655b6bd2..000000000 --- a/common/include/camera/CCameraPresetFlow.hpp +++ /dev/null @@ -1,150 +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 _C_CAMERA_PRESET_FLOW_HPP_ -#define _C_CAMERA_PRESET_FLOW_HPP_ - -#include -#include -#include -#include - -#include "CCameraGoalAnalysis.hpp" - -namespace nbl::core -{ - -/// @brief Reusable aggregate summary for applying one preset to multiple cameras. -struct SCameraPresetApplySummary -{ - uint32_t targetCount = 0u; - uint32_t successCount = 0u; - uint32_t approximateCount = 0u; - uint32_t failureCount = 0u; - - inline bool hasTargets() const - { - return targetCount > 0u; - } - - inline bool succeeded() const - { - return hasTargets() && failureCount == 0u; - } - - inline bool approximate() const - { - return approximateCount > 0u; - } -}; - -struct CCameraPresetFlowUtilities final -{ - /// @brief Compare the current camera state against a preset using the shared goal representation. - static inline bool comparePresetToCameraState(const CCameraGoalSolver& solver, ICamera* camera, const CCameraPreset& preset, - const double posEps, const double rotEpsDeg, const double scalarEps) - { - const auto capture = solver.captureDetailed(camera); - if (!capture.canUseGoal()) - return false; - - return CCameraGoalUtilities::compareGoals( - capture.goal, - CCameraPresetUtilities::makeGoalFromPreset(preset), - posEps, - rotEpsDeg, - scalarEps); - } - - /// @brief Explain the first visible mismatch between a camera state and a preset. - static inline std::string describePresetCameraMismatch(const CCameraGoalSolver& solver, ICamera* camera, const CCameraPreset& preset) - { - const auto capture = solver.captureDetailed(camera); - if (!capture.hasCamera) - return "camera=null"; - if (!capture.captured) - return "goal_state=unavailable"; - if (!capture.finiteGoal) - return "goal_state=invalid"; - - return CCameraGoalUtilities::describeGoalMismatch(capture.goal, CCameraPresetUtilities::makeGoalFromPreset(preset)); - } - - /// @brief Build a preset from an already analyzed capture result. - static inline bool tryCapturePreset(const SCameraCaptureAnalysis& captureAnalysis, ICamera* camera, std::string_view name, CCameraPreset& preset) - { - preset = {}; - preset.name = std::string(name); - if (!captureAnalysis.canCapture || !camera) - return false; - - preset.identifier = std::string(camera->getIdentifier()); - CCameraPresetUtilities::assignGoalToPreset(preset, captureAnalysis.goal); - return true; - } - - /// @brief Capture a preset directly from a camera through the shared goal solver. - static inline bool tryCapturePreset(const CCameraGoalSolver& solver, ICamera* camera, std::string_view name, CCameraPreset& preset) - { - return tryCapturePreset(CCameraGoalAnalysisUtilities::analyzeCameraCapture(solver, camera), camera, name, preset); - } - - /// @brief Value-returning convenience wrapper around `tryCapturePreset`. - static inline CCameraPreset capturePreset(const CCameraGoalSolver& solver, ICamera* camera, std::string_view name) - { - CCameraPreset preset; - tryCapturePreset(solver, camera, name, preset); - return preset; - } - - /// @brief Apply a preset through the shared goal solver and preserve detailed apply diagnostics. - static inline CCameraGoalSolver::SApplyResult applyPresetDetailed(const CCameraGoalSolver& solver, ICamera* camera, const CCameraPreset& preset) - { - if (!camera) - return {}; - - return solver.applyDetailed(camera, CCameraPresetUtilities::makeGoalFromPreset(preset)); - } - - /// @brief Bool-returning convenience wrapper around `applyPresetDetailed`. - static inline bool applyPreset(const CCameraGoalSolver& solver, ICamera* camera, const CCameraPreset& preset) - { - return applyPresetDetailed(solver, camera, preset).succeeded(); - } - - /// @brief Fold one detailed apply result into an aggregate preset-apply summary. - static inline void accumulatePresetApplySummary(SCameraPresetApplySummary& summary, const CCameraGoalSolver::SApplyResult& result) - { - ++summary.targetCount; - if (result.succeeded()) - { - ++summary.successCount; - if (result.approximate()) - ++summary.approximateCount; - } - else - { - ++summary.failureCount; - } - } - - /// @brief Apply one preset to a camera range and collect a typed aggregate summary. - static inline SCameraPresetApplySummary applyPresetToCameraRange(const CCameraGoalSolver& solver, std::span cameras, const CCameraPreset& preset) - { - SCameraPresetApplySummary summary; - for (auto* camera : cameras) - { - if (!camera) - continue; - - accumulatePresetApplySummary(summary, applyPresetDetailed(solver, camera, preset)); - } - - return summary; - } -}; - -} // namespace nbl::core - -#endif // _C_CAMERA_PRESET_FLOW_HPP_ diff --git a/common/include/camera/CCameraPresetPersistence.hpp b/common/include/camera/CCameraPresetPersistence.hpp deleted file mode 100644 index 298163ab6..000000000 --- a/common/include/camera/CCameraPresetPersistence.hpp +++ /dev/null @@ -1,40 +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 _C_CAMERA_PRESET_PERSISTENCE_HPP_ -#define _C_CAMERA_PRESET_PERSISTENCE_HPP_ - -#include - -#include "CCameraPreset.hpp" -#include "nbl/system/path.h" - -namespace nbl::system -{ - -class ISystem; - -/// @brief Serialize one camera goal into an existing stream. -bool writeGoal(std::ostream& out, const core::CCameraGoal& goal, int indent = 2); -/// @brief Deserialize one camera goal from an existing stream. -bool readGoal(std::istream& in, core::CCameraGoal& goal); - -/// @brief Save one camera goal to a file. -bool saveGoalToFile(ISystem& system, const path& path, const core::CCameraGoal& goal, int indent = 2); -/// @brief Load one camera goal from a file. -bool loadGoalFromFile(ISystem& system, const path& path, core::CCameraGoal& goal); - -/// @brief Serialize one camera preset into an existing stream. -bool writePreset(std::ostream& out, const core::CCameraPreset& preset, int indent = 2); -/// @brief Deserialize one camera preset from an existing stream. -bool readPreset(std::istream& in, core::CCameraPreset& preset); - -/// @brief Save one camera preset to a file. -bool savePresetToFile(ISystem& system, const path& path, const core::CCameraPreset& preset, int indent = 2); -/// @brief Load one camera preset from a file. -bool loadPresetFromFile(ISystem& system, const path& path, core::CCameraPreset& preset); - -} // namespace nbl::system - -#endif // _C_CAMERA_PRESET_PERSISTENCE_HPP_ diff --git a/common/include/camera/CCameraProjectionUtilities.hpp b/common/include/camera/CCameraProjectionUtilities.hpp deleted file mode 100644 index 220cdc2fc..000000000 --- a/common/include/camera/CCameraProjectionUtilities.hpp +++ /dev/null @@ -1,36 +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 _C_CAMERA_PROJECTION_UTILITIES_HPP_ -#define _C_CAMERA_PROJECTION_UTILITIES_HPP_ - -#include "IPlanarProjection.hpp" - -namespace nbl::core -{ - -struct CCameraProjectionUtilities final -{ - /// @brief Apply a camera-provided dynamic perspective FOV to one planar projection entry. - static inline bool syncDynamicPerspectiveProjection(ICamera* camera, IPlanarProjection::CProjection& projection) - { - if (!camera) - return false; - - const auto& params = projection.getParameters(); - if (params.m_type != IPlanarProjection::CProjection::Perspective) - return false; - - float dynamicFov = 0.0f; - if (!camera->tryGetDynamicPerspectiveFov(dynamicFov)) - return false; - - projection.setPerspective(params.m_zNear, params.m_zFar, dynamicFov); - return true; - } -}; - -} // namespace nbl::core - -#endif // _C_CAMERA_PROJECTION_UTILITIES_HPP_ diff --git a/common/include/camera/CCameraScriptedCheckRunner.hpp b/common/include/camera/CCameraScriptedCheckRunner.hpp deleted file mode 100644 index a4a23c7ed..000000000 --- a/common/include/camera/CCameraScriptedCheckRunner.hpp +++ /dev/null @@ -1,564 +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 _C_CAMERA_SCRIPTED_CHECK_RUNNER_HPP_ -#define _C_CAMERA_SCRIPTED_CHECK_RUNNER_HPP_ - -#include -#include -#include -#include -#include -#include -#include - -#include "CCameraFollowRegressionUtilities.hpp" -#include "CCameraScriptedRuntime.hpp" -#include "SCameraRigPose.hpp" - -namespace nbl::system -{ - -/// @brief Runtime state for authored scripted checks. -/// -/// This state stores: -/// - the index of the next authored check to evaluate -/// - one baseline pose reference -/// - one step pose reference -struct CCameraScriptedCheckRuntimeState -{ - struct SPoseReference final : core::SCameraRigPose - { - bool valid = false; - }; - - size_t nextCheckIndex = 0u; - SPoseReference baseline = {}; - SPoseReference step = {}; -}; - -/// @brief Shared per-frame evaluation context for authored scripted checks. -struct CCameraScriptedCheckContext -{ - uint64_t frame = 0ull; - core::ICamera* camera = nullptr; - const core::CVirtualGimbalEvent* imguizmoVirtual = nullptr; - uint32_t imguizmoVirtualCount = 0u; - const core::CTrackedTarget* trackedTarget = nullptr; - const core::SCameraFollowConfig* followConfig = nullptr; - const SCameraProjectionContext* followProjectionContext = nullptr; - const core::CCameraGoalSolver* goalSolver = nullptr; -}; - -/// @brief Reusable log entry produced by scripted check evaluation. -struct CCameraScriptedCheckLogEntry -{ - bool failure = false; - std::string text; -}; - -/// @brief Result for one frame worth of scripted checks. -struct CCameraScriptedCheckFrameResult -{ - std::vector logs; - bool hadFailures = false; -}; - -struct CCameraScriptedCheckRunnerUtilities final -{ - static inline void scriptedCheckSetStepReference( - CCameraScriptedCheckRuntimeState& state, - const hlsl::float64_t3& position, - const hlsl::camera_quaternion_t& orientation) - { - state.step.valid = true; - state.step.position = position; - state.step.orientation = hlsl::CCameraMathUtilities::normalizeQuaternion(orientation); - } - - static inline void scriptedCheckSetBaselineReference( - CCameraScriptedCheckRuntimeState& state, - const hlsl::float64_t3& position, - const hlsl::camera_quaternion_t& orientation) - { - state.baseline.valid = true; - state.baseline.position = position; - state.baseline.orientation = hlsl::CCameraMathUtilities::normalizeQuaternion(orientation); - scriptedCheckSetStepReference(state, position, orientation); - } - - static inline bool scriptedCheckComputePoseDelta( - const hlsl::float64_t3& currentPosition, - const hlsl::camera_quaternion_t& currentOrientation, - const hlsl::float64_t3& referencePosition, - const hlsl::camera_quaternion_t& referenceOrientation, - hlsl::SCameraPoseDelta& outDelta) - { - return hlsl::CCameraMathUtilities::tryComputePoseDelta( - currentPosition, - currentOrientation, - referencePosition, - referenceOrientation, - outDelta); - } - - template - static inline std::string buildScriptedCheckMessage(Fn&& formatter) - { - std::ostringstream oss; - formatter(oss); - return oss.str(); - } - - static inline void appendScriptedCheckLog( - CCameraScriptedCheckFrameResult& result, - const bool failure, - std::string&& text) - { - result.logs.push_back({ - .failure = failure, - .text = std::move(text) - }); - result.hadFailures = result.hadFailures || failure; - } - - /// @brief Evaluate all authored scripted checks scheduled for the current frame. - static inline CCameraScriptedCheckFrameResult evaluateScriptedChecksForFrame( - const std::vector& checks, - CCameraScriptedCheckRuntimeState& state, - const CCameraScriptedCheckContext& context) - { - CCameraScriptedCheckFrameResult result = {}; - - while (state.nextCheckIndex < checks.size() && checks[state.nextCheckIndex].frame == context.frame) - { - const auto& check = checks[state.nextCheckIndex]; - - if (!context.camera) - { - appendScriptedCheckLog( - result, - true, - buildScriptedCheckMessage([&](std::ostringstream& oss) - { - oss << "[script][fail] check frame=" << context.frame << " no active camera"; - })); - ++state.nextCheckIndex; - continue; - } - - const auto& gimbal = context.camera->getGimbal(); - const auto pos = gimbal.getPosition(); - const auto orientation = hlsl::CCameraMathUtilities::normalizeQuaternion(gimbal.getOrientation()); - const auto eulerDeg = hlsl::getCastedVector(hlsl::CCameraMathUtilities::getCameraOrientationEulerDegrees(orientation)); - - if (!hlsl::CCameraMathUtilities::isFiniteVec3(pos) || !hlsl::CCameraMathUtilities::isFiniteQuaternion(orientation) || !hlsl::CCameraMathUtilities::isFiniteVec3(eulerDeg)) - { - appendScriptedCheckLog( - result, - true, - buildScriptedCheckMessage([&](std::ostringstream& oss) - { - oss << "[script][fail] check frame=" << context.frame << " non-finite gimbal state"; - })); - ++state.nextCheckIndex; - continue; - } - - switch (check.kind) - { - case CCameraScriptedInputCheck::Kind::Baseline: - { - scriptedCheckSetBaselineReference(state, pos, orientation); - appendScriptedCheckLog( - result, - false, - buildScriptedCheckMessage([&](std::ostringstream& oss) - { - oss << std::fixed << std::setprecision(3); - oss << "[script][pass] baseline frame=" << context.frame - << " pos=(" << pos.x << ", " << pos.y << ", " << pos.z << ")" - << " euler_deg=(" << eulerDeg.x << ", " << eulerDeg.y << ", " << eulerDeg.z << ")"; - })); - break; - } - case CCameraScriptedInputCheck::Kind::ImguizmoVirtual: - { - bool ok = true; - if (!context.imguizmoVirtual || context.imguizmoVirtualCount == 0u) - { - ok = false; - } - else - { - for (const auto& expected : check.expectedVirtualEvents) - { - bool found = false; - double actual = 0.0; - for (uint32_t i = 0u; i < context.imguizmoVirtualCount; ++i) - { - if (context.imguizmoVirtual[i].type == expected.type) - { - found = true; - actual = context.imguizmoVirtual[i].magnitude; - break; - } - } - - if (!found || hlsl::abs(actual - expected.magnitude) > check.tolerance) - { - ok = false; - appendScriptedCheckLog( - result, - true, - buildScriptedCheckMessage([&](std::ostringstream& oss) - { - oss << std::fixed << std::setprecision(6); - oss << "[script][fail] imguizmo_virtual frame=" << context.frame - << " type=" << core::CVirtualGimbalEvent::virtualEventToString(expected.type).data() - << " expected=" << expected.magnitude - << " actual=" << actual - << " tol=" << check.tolerance; - })); - } - } - } - - if (ok) - { - appendScriptedCheckLog( - result, - false, - buildScriptedCheckMessage([&](std::ostringstream& oss) - { - oss << "[script][pass] imguizmo_virtual frame=" << context.frame - << " events=" << check.expectedVirtualEvents.size(); - })); - } - break; - } - case CCameraScriptedInputCheck::Kind::GimbalNear: - { - bool ok = true; - if (check.hasExpectedPos) - { - const double distance = hlsl::length(pos - hlsl::getCastedVector(check.expectedPos)); - if (distance > check.posTolerance) - { - ok = false; - appendScriptedCheckLog( - result, - true, - buildScriptedCheckMessage([&](std::ostringstream& oss) - { - oss << std::fixed << std::setprecision(6); - oss << "[script][fail] gimbal_near frame=" << context.frame - << " pos_diff=" << distance - << " tol=" << check.posTolerance; - })); - } - } - if (check.hasExpectedEuler) - { - const auto expectedOrientation = hlsl::CCameraMathUtilities::makeQuaternionFromEulerDegreesYXZ( - hlsl::getCastedVector(check.expectedEulerDeg)); - hlsl::SCameraPoseDelta poseDelta = {}; - if (!scriptedCheckComputePoseDelta(pos, orientation, pos, expectedOrientation, poseDelta)) - poseDelta.rotationDeg = std::numeric_limits::infinity(); - const auto rotationDeltaDeg = poseDelta.rotationDeg; - if (rotationDeltaDeg > check.eulerToleranceDeg) - { - ok = false; - appendScriptedCheckLog( - result, - true, - buildScriptedCheckMessage([&](std::ostringstream& oss) - { - oss << std::fixed << std::setprecision(6); - oss << "[script][fail] gimbal_near frame=" << context.frame - << " rot_delta_deg=" << rotationDeltaDeg - << " tol=" << check.eulerToleranceDeg; - })); - } - } - - if (ok) - { - appendScriptedCheckLog( - result, - false, - buildScriptedCheckMessage([&](std::ostringstream& oss) - { - oss << "[script][pass] gimbal_near frame=" << context.frame; - })); - } - break; - } - case CCameraScriptedInputCheck::Kind::GimbalDelta: - { - if (!state.baseline.valid) - { - appendScriptedCheckLog( - result, - true, - buildScriptedCheckMessage([&](std::ostringstream& oss) - { - oss << "[script][fail] gimbal_delta frame=" << context.frame << " missing baseline"; - })); - break; - } - - hlsl::SCameraPoseDelta poseDelta = {}; - if (!scriptedCheckComputePoseDelta(pos, orientation, state.baseline.position, state.baseline.orientation, poseDelta)) - { - appendScriptedCheckLog( - result, - true, - buildScriptedCheckMessage([&](std::ostringstream& oss) - { - oss << "[script][fail] gimbal_delta frame=" << context.frame << " non-finite pose delta"; - })); - break; - } - - if (poseDelta.position > check.posTolerance || poseDelta.rotationDeg > check.eulerToleranceDeg) - { - appendScriptedCheckLog( - result, - true, - buildScriptedCheckMessage([&](std::ostringstream& oss) - { - oss << std::fixed << std::setprecision(6); - oss << "[script][fail] gimbal_delta frame=" << context.frame - << " pos_diff=" << poseDelta.position - << " tol=" << check.posTolerance - << " rot_delta_deg=" << poseDelta.rotationDeg - << " tol=" << check.eulerToleranceDeg; - })); - } - else - { - appendScriptedCheckLog( - result, - false, - buildScriptedCheckMessage([&](std::ostringstream& oss) - { - oss << std::fixed << std::setprecision(6); - oss << "[script][pass] gimbal_delta frame=" << context.frame - << " pos_diff=" << poseDelta.position - << " rot_delta_deg=" << poseDelta.rotationDeg; - })); - } - break; - } - case CCameraScriptedInputCheck::Kind::GimbalStep: - { - if (!state.step.valid) - { - if (state.baseline.valid) - { - scriptedCheckSetStepReference(state, state.baseline.position, state.baseline.orientation); - } - else - { - appendScriptedCheckLog( - result, - true, - buildScriptedCheckMessage([&](std::ostringstream& oss) - { - oss << "[script][fail] gimbal_step frame=" << context.frame << " missing step reference"; - })); - scriptedCheckSetStepReference(state, pos, orientation); - ++state.nextCheckIndex; - continue; - } - } - - hlsl::SCameraPoseDelta poseDelta = {}; - if (!scriptedCheckComputePoseDelta(pos, orientation, state.step.position, state.step.orientation, poseDelta)) - { - appendScriptedCheckLog( - result, - true, - buildScriptedCheckMessage([&](std::ostringstream& oss) - { - oss << "[script][fail] gimbal_step frame=" << context.frame << " non-finite pose delta"; - })); - scriptedCheckSetStepReference(state, pos, orientation); - break; - } - - bool ok = true; - bool requiresProgress = false; - bool hasProgress = false; - if (check.hasPosDeltaConstraint) - { - if (poseDelta.position > check.posTolerance) - { - ok = false; - appendScriptedCheckLog( - result, - true, - buildScriptedCheckMessage([&](std::ostringstream& oss) - { - oss << std::fixed << std::setprecision(6); - oss << "[script][fail] gimbal_step frame=" << context.frame - << " pos_delta=" << poseDelta.position - << " max=" << check.posTolerance; - })); - } - if (check.minPosDelta > 0.0f) - { - requiresProgress = true; - hasProgress = hasProgress || poseDelta.position >= check.minPosDelta; - } - } - if (check.hasEulerDeltaConstraint) - { - if (poseDelta.rotationDeg > check.eulerToleranceDeg) - { - ok = false; - appendScriptedCheckLog( - result, - true, - buildScriptedCheckMessage([&](std::ostringstream& oss) - { - oss << std::fixed << std::setprecision(6); - oss << "[script][fail] gimbal_step frame=" << context.frame - << " rot_delta_deg=" << poseDelta.rotationDeg - << " max=" << check.eulerToleranceDeg; - })); - } - if (check.minEulerDeltaDeg > 0.0f) - { - requiresProgress = true; - hasProgress = hasProgress || poseDelta.rotationDeg >= check.minEulerDeltaDeg; - } - } - if (requiresProgress && !hasProgress) - { - ok = false; - appendScriptedCheckLog( - result, - true, - buildScriptedCheckMessage([&](std::ostringstream& oss) - { - oss << std::fixed << std::setprecision(6); - oss << "[script][fail] gimbal_step frame=" << context.frame - << " missing progress pos_delta=" << poseDelta.position - << " rot_delta_deg=" << poseDelta.rotationDeg; - })); - } - - if (ok) - { - appendScriptedCheckLog( - result, - false, - buildScriptedCheckMessage([&](std::ostringstream& oss) - { - oss << std::fixed << std::setprecision(6); - oss << "[script][pass] gimbal_step frame=" << context.frame - << " pos_delta=" << poseDelta.position - << " rot_delta_deg=" << poseDelta.rotationDeg; - })); - } - scriptedCheckSetStepReference(state, pos, orientation); - break; - } - case CCameraScriptedInputCheck::Kind::FollowTargetLock: - { - if (!context.followConfig) - { - appendScriptedCheckLog( - result, - true, - buildScriptedCheckMessage([&](std::ostringstream& oss) - { - oss << "[script][fail] follow_lock frame=" << context.frame << " missing follow config"; - })); - break; - } - if (!context.trackedTarget) - { - appendScriptedCheckLog( - result, - true, - buildScriptedCheckMessage([&](std::ostringstream& oss) - { - oss << "[script][fail] follow_lock frame=" << context.frame << " missing tracked target"; - })); - break; - } - if (!context.goalSolver) - { - appendScriptedCheckLog( - result, - true, - buildScriptedCheckMessage([&](std::ostringstream& oss) - { - oss << "[script][fail] follow_lock frame=" << context.frame << " missing goal solver"; - })); - break; - } - - SCameraFollowRegressionResult regression = {}; - std::string regressionError; - core::CCameraGoal expectedFollowGoal = {}; - const auto thresholds = CCameraFollowRegressionUtilities::makeFollowRegressionThresholds(check.posTolerance, check.eulerToleranceDeg); - const bool ok = core::CCameraFollowUtilities::tryBuildFollowGoal( - *context.goalSolver, - context.camera, - *context.trackedTarget, - *context.followConfig, - expectedFollowGoal) && - CCameraFollowRegressionUtilities::validateFollowTargetContract( - context.camera, - *context.trackedTarget, - *context.followConfig, - expectedFollowGoal, - regression, - ®ressionError, - context.followProjectionContext, - thresholds); - - if (!ok) - { - appendScriptedCheckLog( - result, - true, - buildScriptedCheckMessage([&](std::ostringstream& oss) - { - oss << "[script][fail] follow_lock frame=" << context.frame << ' ' - << (regressionError.empty() ? "follow validation mismatch" : regressionError); - })); - } - else - { - appendScriptedCheckLog( - result, - false, - buildScriptedCheckMessage([&](std::ostringstream& oss) - { - oss << std::fixed << std::setprecision(6); - oss << "[script][pass] follow_lock frame=" << context.frame - << " angle_deg=" << regression.lockAngleDeg - << " target_distance=" << regression.targetDistance - << " screen_ndc=" << regression.projectedTarget.radius; - })); - } - break; - } - } - - ++state.nextCheckIndex; - } - - return result; - } -}; - -} // namespace nbl::system - -#endif // _C_CAMERA_SCRIPTED_CHECK_RUNNER_HPP_ diff --git a/common/include/camera/CCameraScriptedRuntime.hpp b/common/include/camera/CCameraScriptedRuntime.hpp deleted file mode 100644 index 99d8c34f0..000000000 --- a/common/include/camera/CCameraScriptedRuntime.hpp +++ /dev/null @@ -1,392 +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 _C_CAMERA_SCRIPTED_RUNTIME_HPP_ -#define _C_CAMERA_SCRIPTED_RUNTIME_HPP_ - -#include -#include -#include -#include - -#include "CCameraGoal.hpp" -#include "CCameraFollowRegressionUtilities.hpp" -#include "CVirtualGimbalEvent.hpp" -#include "nbl/ui/KeyCodes.h" - -namespace nbl::system -{ - -/// @brief Shared scripted runtime payload used by camera-sequence consumers. -/// -/// This type stores the expanded per-frame events and checks produced from a -/// compact authored camera sequence. -struct CCameraScriptedInputEvent -{ - enum class Type : uint8_t - { - Keyboard, - Mouse, - Imguizmo, - Action, - Goal, - TrackedTargetTransform, - SegmentLabel - }; - - struct KeyboardData - { - enum class Action : uint8_t - { - Uninitialized = 0, - Pressed = 1, - Released = 2 - }; - - ui::E_KEY_CODE key = ui::EKC_NONE; - Action action = Action::Uninitialized; - }; - - struct MouseData - { - enum class Type : uint8_t - { - Uninitialized = 0, - Click = 1, - Scroll = 2, - Movement = 4 - }; - - enum class ClickAction : uint8_t - { - Uninitialized = 0, - Pressed = 1, - Released = 2 - }; - - Type type = Type::Uninitialized; - ui::E_MOUSE_BUTTON button = ui::EMB_LEFT_BUTTON; - ClickAction action = ClickAction::Uninitialized; - int16_t x = 0; - int16_t y = 0; - int16_t dx = 0; - int16_t dy = 0; - int16_t v = 0; - int16_t h = 0; - }; - - struct ActionData - { - enum class Kind : uint8_t - { - SetActiveRenderWindow, - SetActivePlanar, - SetProjectionType, - SetProjectionIndex, - SetUseWindow, - SetLeftHanded, - ResetActiveCamera - }; - - Kind kind = Kind::SetActiveRenderWindow; - int32_t value = 0; - }; - - struct GoalData - { - core::CCameraGoal goal = {}; - bool requireExact = true; - }; - - struct TrackedTargetTransformData - { - hlsl::float64_t4x4 transform = hlsl::float64_t4x4(1.0); - }; - - struct SegmentLabelData - { - std::string label; - }; - - uint64_t frame = 0; - Type type = Type::Keyboard; - KeyboardData keyboard; - MouseData mouse; - hlsl::float32_t4x4 imguizmo = hlsl::float32_t4x4(1.f); - ActionData action; - GoalData goal; - TrackedTargetTransformData trackedTargetTransform; - SegmentLabelData segmentLabel; -}; - -struct CCameraScriptedCheckDefaults final -{ - static constexpr float VirtualEventTolerance = 1e-3f; - static constexpr float PositionTolerance = static_cast(core::SCameraToolingThresholds::DefaultPositionTolerance); - static constexpr float EulerToleranceDeg = static_cast(core::SCameraToolingThresholds::DefaultAngularToleranceDeg); - static constexpr float FollowScreenToleranceNdc = SCameraFollowRegressionThresholds::DefaultProjectedNdcTolerance; -}; - -struct CCameraScriptedInputCheck -{ - enum class Kind : uint8_t - { - Baseline, - ImguizmoVirtual, - GimbalNear, - GimbalDelta, - GimbalStep, - FollowTargetLock - }; - - struct ExpectedVirtualEvent - { - core::CVirtualGimbalEvent::VirtualEventType type = core::CVirtualGimbalEvent::None; - hlsl::float64_t magnitude = 0.0; - }; - - uint64_t frame = 0; - Kind kind = Kind::Baseline; - float tolerance = CCameraScriptedCheckDefaults::VirtualEventTolerance; - std::vector expectedVirtualEvents; - - hlsl::float32_t3 expectedPos = hlsl::float32_t3(0.f); - hlsl::float32_t3 expectedEulerDeg = hlsl::float32_t3(0.f); - bool hasExpectedPos = false; - bool hasExpectedEuler = false; - float posTolerance = CCameraScriptedCheckDefaults::PositionTolerance; - float eulerToleranceDeg = CCameraScriptedCheckDefaults::EulerToleranceDeg; - float minPosDelta = 0.0f; - float minEulerDeltaDeg = 0.0f; - bool hasPosDeltaConstraint = false; - bool hasEulerDeltaConstraint = false; -}; - -/// @brief Fully expanded scripted timeline shared between authored parsers and runtime consumers. -struct CCameraScriptedTimeline -{ - std::vector events; - std::vector checks; - std::vector captureFrames; - - inline void clear() - { - events.clear(); - checks.clear(); - captureFrames.clear(); - } - - inline bool empty() const - { - return events.empty() && checks.empty() && captureFrames.empty(); - } -}; - -struct CCameraScriptedRuntimeUtilities final -{ - static inline void finalizeScriptedTimeline( - std::vector& events, - std::vector& checks, - std::vector& captureFrames, - const bool disableCaptureFrames = false) - { - std::stable_sort(events.begin(), events.end(), - [](const CCameraScriptedInputEvent& a, const CCameraScriptedInputEvent& b) { return a.frame < b.frame; }); - std::stable_sort(checks.begin(), checks.end(), - [](const CCameraScriptedInputCheck& a, const CCameraScriptedInputCheck& b) { return a.frame < b.frame; }); - if (!captureFrames.empty()) - { - std::sort(captureFrames.begin(), captureFrames.end()); - captureFrames.erase(std::unique(captureFrames.begin(), captureFrames.end()), captureFrames.end()); - } - if (disableCaptureFrames) - captureFrames.clear(); - } - - static inline void finalizeScriptedTimeline(CCameraScriptedTimeline& timeline, const bool disableCaptureFrames = false) - { - finalizeScriptedTimeline(timeline.events, timeline.checks, timeline.captureFrames, disableCaptureFrames); - } - - static inline void appendScriptedActionEvent( - CCameraScriptedTimeline& timeline, - const uint64_t frame, - const CCameraScriptedInputEvent::ActionData::Kind kind, - const int32_t value) - { - CCameraScriptedInputEvent entry; - entry.frame = frame; - entry.type = CCameraScriptedInputEvent::Type::Action; - entry.action.kind = kind; - entry.action.value = value; - timeline.events.emplace_back(std::move(entry)); - } - - static inline void appendScriptedGoalEvent( - CCameraScriptedTimeline& timeline, - const uint64_t frame, - const core::CCameraGoal& goal, - const bool requireExact = true) - { - CCameraScriptedInputEvent entry; - entry.frame = frame; - entry.type = CCameraScriptedInputEvent::Type::Goal; - entry.goal.goal = goal; - entry.goal.requireExact = requireExact; - timeline.events.emplace_back(std::move(entry)); - } - - static inline void appendScriptedTrackedTargetTransformEvent( - CCameraScriptedTimeline& timeline, - const uint64_t frame, - const hlsl::float64_t4x4& transform) - { - CCameraScriptedInputEvent entry; - entry.frame = frame; - entry.type = CCameraScriptedInputEvent::Type::TrackedTargetTransform; - entry.trackedTargetTransform.transform = transform; - timeline.events.emplace_back(std::move(entry)); - } - - static inline void appendScriptedSegmentLabelEvent( - CCameraScriptedTimeline& timeline, - const uint64_t frame, - std::string label) - { - CCameraScriptedInputEvent entry; - entry.frame = frame; - entry.type = CCameraScriptedInputEvent::Type::SegmentLabel; - entry.segmentLabel.label = std::move(label); - timeline.events.emplace_back(std::move(entry)); - } - - static inline void appendScriptedBaselineCheck(CCameraScriptedTimeline& timeline, const uint64_t frame) - { - CCameraScriptedInputCheck entry; - entry.frame = frame; - entry.kind = CCameraScriptedInputCheck::Kind::Baseline; - timeline.checks.emplace_back(std::move(entry)); - } - - static inline void appendScriptedGimbalStepCheck( - CCameraScriptedTimeline& timeline, - const uint64_t frame, - const bool hasPosDeltaConstraint, - const float posTolerance, - const float minPosDelta, - const bool hasEulerDeltaConstraint, - const float eulerToleranceDeg, - const float minEulerDeltaDeg) - { - CCameraScriptedInputCheck entry; - entry.frame = frame; - entry.kind = CCameraScriptedInputCheck::Kind::GimbalStep; - if (hasPosDeltaConstraint) - { - entry.hasPosDeltaConstraint = true; - entry.posTolerance = posTolerance; - entry.minPosDelta = minPosDelta; - } - if (hasEulerDeltaConstraint) - { - entry.hasEulerDeltaConstraint = true; - entry.eulerToleranceDeg = eulerToleranceDeg; - entry.minEulerDeltaDeg = minEulerDeltaDeg; - } - timeline.checks.emplace_back(std::move(entry)); - } - - static inline void appendScriptedFollowTargetLockCheck( - CCameraScriptedTimeline& timeline, - const uint64_t frame, - const float toleranceDeg = CCameraScriptedCheckDefaults::EulerToleranceDeg, - const float screenToleranceNdc = CCameraScriptedCheckDefaults::FollowScreenToleranceNdc) - { - CCameraScriptedInputCheck entry; - entry.frame = frame; - entry.kind = CCameraScriptedInputCheck::Kind::FollowTargetLock; - entry.eulerToleranceDeg = toleranceDeg; - entry.posTolerance = screenToleranceNdc; - timeline.checks.emplace_back(std::move(entry)); - } -}; - -/// @brief Per-frame scripted runtime batch already partitioned by payload kind. -/// -/// Consumers can dequeue authored events for one frame and then adapt only the buckets they care -/// about, without repeatedly switching on `CCameraScriptedInputEvent::Type` in local glue. -struct CCameraScriptedFrameEvents -{ - std::vector keyboard; - std::vector mouse; - std::vector imguizmo; - std::vector actions; - std::vector goals; - std::vector trackedTargetTransforms; - std::vector segmentLabels; - - inline void clear() - { - keyboard.clear(); - mouse.clear(); - imguizmo.clear(); - actions.clear(); - goals.clear(); - trackedTargetTransforms.clear(); - segmentLabels.clear(); - } - - inline bool empty() const - { - return keyboard.empty() && mouse.empty() && imguizmo.empty() && actions.empty() && - goals.empty() && trackedTargetTransforms.empty() && segmentLabels.empty(); - } -}; - -/// @brief Dequeue all authored scripted events scheduled for one frame. -struct CCameraScriptedFrameEventUtilities final -{ - static inline void dequeueScriptedFrameEvents( - const std::vector& events, - size_t& nextEventIndex, - const uint64_t frame, - CCameraScriptedFrameEvents& out) - { - out.clear(); - while (nextEventIndex < events.size() && events[nextEventIndex].frame == frame) - { - const auto& ev = events[nextEventIndex]; - switch (ev.type) - { - case CCameraScriptedInputEvent::Type::Keyboard: - out.keyboard.emplace_back(ev.keyboard); - break; - case CCameraScriptedInputEvent::Type::Mouse: - out.mouse.emplace_back(ev.mouse); - break; - case CCameraScriptedInputEvent::Type::Imguizmo: - out.imguizmo.emplace_back(ev.imguizmo); - break; - case CCameraScriptedInputEvent::Type::Action: - out.actions.emplace_back(ev.action); - break; - case CCameraScriptedInputEvent::Type::Goal: - out.goals.emplace_back(ev.goal); - break; - case CCameraScriptedInputEvent::Type::TrackedTargetTransform: - out.trackedTargetTransforms.emplace_back(ev.trackedTargetTransform); - break; - case CCameraScriptedInputEvent::Type::SegmentLabel: - out.segmentLabels.emplace_back(ev.segmentLabel.label); - break; - } - - ++nextEventIndex; - } - } -}; - -} // namespace nbl::system - -#endif diff --git a/common/include/camera/CCameraScriptedRuntimePersistence.hpp b/common/include/camera/CCameraScriptedRuntimePersistence.hpp deleted file mode 100644 index 58e6f2dbb..000000000 --- a/common/include/camera/CCameraScriptedRuntimePersistence.hpp +++ /dev/null @@ -1,74 +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 _C_CAMERA_SCRIPTED_RUNTIME_PERSISTENCE_HPP_ -#define _C_CAMERA_SCRIPTED_RUNTIME_PERSISTENCE_HPP_ - -#include -#include -#include -#include -#include - -#include "CCameraScriptedRuntime.hpp" -#include "CCameraSequenceScriptPersistence.hpp" - -namespace nbl::system -{ - -class ISystem; - -/// @brief Optional scripted control overrides parsed alongside one runtime payload. -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; -}; - -/// @brief Parsed low-level scripted runtime payload plus optional compact authored sequence. -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 = {}; - CCameraScriptedTimeline timeline = {}; - 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)); - } -}; - -/// @brief Parse one low-level scripted runtime payload from an existing stream. -bool readCameraScriptedInput(std::istream& in, CCameraScriptedInputParseResult& out, std::string* error = nullptr); -/// @brief Parse one low-level scripted runtime payload directly from text. -bool readCameraScriptedInput(std::string_view text, CCameraScriptedInputParseResult& out, std::string* error = nullptr); -/// @brief Load one low-level scripted runtime payload from a file. -bool loadCameraScriptedInputFromFile(ISystem& system, const path& path, CCameraScriptedInputParseResult& out, std::string* error = nullptr); - -} // namespace nbl::system - -#endif // _C_CAMERA_SCRIPTED_RUNTIME_PERSISTENCE_HPP_ diff --git a/common/include/camera/CCameraScriptedUiInputUtilities.hpp b/common/include/camera/CCameraScriptedUiInputUtilities.hpp deleted file mode 100644 index 22dd66cf4..000000000 --- a/common/include/camera/CCameraScriptedUiInputUtilities.hpp +++ /dev/null @@ -1,98 +0,0 @@ -#ifndef _C_CAMERA_SCRIPTED_UI_INPUT_UTILITIES_HPP_ -#define _C_CAMERA_SCRIPTED_UI_INPUT_UTILITIES_HPP_ - -#include -#include - -#include "CCameraScriptedRuntime.hpp" -#include "nbl/ui/SInputEvent.h" - -namespace nbl::ui -{ - -/// @brief Convert authored scripted keyboard and mouse payloads into runtime UI input events. -/// -/// The scripted runtime stores compact authoring-friendly payloads. This helper -/// expands them into the concrete `SKeyboardEvent` and `SMouseEvent` objects -/// consumed by the same input path as live window events. -struct CCameraScriptedUiInputUtilities final -{ - /// @brief Build one runtime keyboard event from authored scripted keyboard data. - static inline SKeyboardEvent makeScriptedKeyboardEvent( - const std::chrono::microseconds timestamp, - IWindow* const window, - const system::CCameraScriptedInputEvent::KeyboardData& authoredKeyboard) - { - SKeyboardEvent event(timestamp); - event.keyCode = authoredKeyboard.key; - event.action = - authoredKeyboard.action == system::CCameraScriptedInputEvent::KeyboardData::Action::Pressed ? - SKeyboardEvent::ECA_PRESSED : - SKeyboardEvent::ECA_RELEASED; - event.window = window; - return event; - } - - /// @brief Build one runtime mouse event from authored scripted mouse data. - static inline bool tryBuildScriptedMouseEvent( - const std::chrono::microseconds timestamp, - IWindow* const window, - const system::CCameraScriptedInputEvent::MouseData& authoredMouse, - SMouseEvent& outEvent) - { - outEvent = SMouseEvent(timestamp); - outEvent.window = window; - - switch (authoredMouse.type) - { - case system::CCameraScriptedInputEvent::MouseData::Type::Click: - outEvent.type = SMouseEvent::EET_CLICK; - outEvent.clickEvent.mouseButton = authoredMouse.button; - outEvent.clickEvent.action = - authoredMouse.action == system::CCameraScriptedInputEvent::MouseData::ClickAction::Pressed ? - SMouseEvent::SClickEvent::EA_PRESSED : - SMouseEvent::SClickEvent::EA_RELEASED; - outEvent.clickEvent.clickPosX = authoredMouse.x; - outEvent.clickEvent.clickPosY = authoredMouse.y; - return true; - case system::CCameraScriptedInputEvent::MouseData::Type::Scroll: - outEvent.type = SMouseEvent::EET_SCROLL; - outEvent.scrollEvent.verticalScroll = authoredMouse.v; - outEvent.scrollEvent.horizontalScroll = authoredMouse.h; - return true; - case system::CCameraScriptedInputEvent::MouseData::Type::Movement: - outEvent.type = SMouseEvent::EET_MOVEMENT; - outEvent.movementEvent.relativeMovementX = authoredMouse.dx; - outEvent.movementEvent.relativeMovementY = authoredMouse.dy; - return true; - default: - return false; - } - } - - /// @brief Append one authored scripted input batch to existing runtime event buffers. - static inline void appendScriptedUiInputEvents( - const std::chrono::microseconds timestamp, - IWindow* const window, - const std::vector& authoredKeyboard, - const std::vector& authoredMouse, - std::vector& outKeyboard, - std::vector& outMouse) - { - outKeyboard.reserve(outKeyboard.size() + authoredKeyboard.size()); - for (const auto& keyboardEvent : authoredKeyboard) - outKeyboard.emplace_back(makeScriptedKeyboardEvent(timestamp, window, keyboardEvent)); - - outMouse.reserve(outMouse.size() + authoredMouse.size()); - for (const auto& mouseEvent : authoredMouse) - { - SMouseEvent builtEvent(timestamp); - if (tryBuildScriptedMouseEvent(timestamp, window, mouseEvent, builtEvent)) - outMouse.emplace_back(builtEvent); - } - } -}; - -} // namespace nbl::ui - -#endif // _C_CAMERA_SCRIPTED_UI_INPUT_UTILITIES_HPP_ diff --git a/common/include/camera/CCameraSequenceScript.hpp b/common/include/camera/CCameraSequenceScript.hpp deleted file mode 100644 index e4e459d69..000000000 --- a/common/include/camera/CCameraSequenceScript.hpp +++ /dev/null @@ -1,792 +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 _C_CAMERA_SEQUENCE_SCRIPT_HPP_ -#define _C_CAMERA_SEQUENCE_SCRIPT_HPP_ - -#include -#include -#include -#include -#include -#include - -#include "CCameraMathUtilities.hpp" -#include "CCameraKeyframeTrack.hpp" -#include "CCameraPathUtilities.hpp" -#include "CCameraTargetRelativeUtilities.hpp" -#include "IPlanarProjection.hpp" - -namespace nbl::core -{ - -/// @brief Compact authored camera-sequence format shared by playback, scripting, and validation helpers. -/// -/// The authored file describes: -/// -/// - which camera kind a segment targets -/// - which reusable projection presentations are shown -/// - which keyframed camera goals are sampled over time -/// - which tracked-target poses are sampled over time -/// - which continuity thresholds and capture points are generated -/// -/// The format does not store: -/// -/// - per-frame low-level event dumps -/// - runtime-specific window actions as authored source data -/// - ImGuizmo transforms as the primary authored primitive -/// -/// Consumers may expand the compact sequence into runtime events and per-frame -/// checks. The authored data remains camera-domain data and is not a device- or -/// UI-specific event dump. - -/// @brief Authored projection view request for camera-sequence playback. -struct CCameraSequencePresentation -{ - IPlanarProjection::CProjection::ProjectionType projection = IPlanarProjection::CProjection::Perspective; - bool leftHanded = true; -}; - -/// @brief Shared continuity thresholds authored once and reused per sequence segment. -/// Max bounds are enforced per-step, while minimum progress can be satisfied by either position or rotation change. -struct CCameraSequenceContinuitySettings -{ - bool baseline = true; - bool step = true; - bool hasPosDeltaConstraint = true; - float minPosDelta = 0.00025f; - float maxPosDelta = 2.f; - bool hasEulerDeltaConstraint = false; - float minEulerDeltaDeg = 0.f; - float maxEulerDeltaDeg = 1.f; -}; - -/// @brief Relative goal adjustment authored against an initial preset captured from the target camera. -/// Deltas stay camera-domain and avoid binding the authored file to any specific input device or consumer. -struct CCameraSequenceGoalDelta -{ - struct SOrbitDelta final - { - hlsl::float64_t2 uvDeltaRad = hlsl::float64_t2(0.0); - float distanceDelta = 0.f; - bool hasU = false; - bool hasV = false; - bool hasDistance = false; - - inline bool hasAny() const - { - return hasU || hasV || hasDistance; - } - - inline void setUDeltaDeg(const double valueDeg) - { - uvDeltaRad.x = static_cast(hlsl::radians(valueDeg)); - hasU = true; - } - - inline void setVDeltaDeg(const double valueDeg) - { - uvDeltaRad.y = static_cast(hlsl::radians(valueDeg)); - hasV = true; - } - - inline void setDistanceDelta(const float valueScalar) - { - distanceDelta = valueScalar; - hasDistance = true; - } - }; - - struct SPathDelta final - { - SCameraPathDelta value = {}; - bool hasS = false; - bool hasU = false; - bool hasV = false; - bool hasRoll = false; - - inline bool hasAny() const - { - return hasS || hasU || hasV || hasRoll; - } - - inline void setSDeltaDeg(const double valueDeg) - { - value.s = static_cast(hlsl::radians(valueDeg)); - hasS = true; - } - - inline void setUDelta(const double valueScalar) - { - value.u = static_cast(valueScalar); - hasU = true; - } - - inline void setVDelta(const double valueScalar) - { - value.v = static_cast(valueScalar); - hasV = true; - } - - inline void setRollDeltaDeg(const double valueDeg) - { - value.roll = static_cast(hlsl::radians(valueDeg)); - hasRoll = true; - } - - inline SCameraPathDelta buildAppliedDelta() const - { - SCameraPathDelta delta = {}; - if (hasS) - delta.s = value.s; - if (hasU) - delta.u = value.u; - if (hasV) - delta.v = value.v; - if (hasRoll) - delta.roll = value.roll; - return delta; - } - }; - - bool hasPositionOffset = false; - hlsl::float64_t3 positionOffset = hlsl::float64_t3(0.0); - - bool hasRotationEulerDegOffset = false; - hlsl::float32_t3 rotationEulerDegOffset = hlsl::float32_t3(0.f); - - bool hasTargetOffset = false; - hlsl::float64_t3 targetOffset = hlsl::float64_t3(0.0); - - SOrbitDelta orbitDelta = {}; - - SPathDelta pathDelta = {}; - - bool hasDynamicBaseFovDelta = false; - float dynamicBaseFovDelta = 0.f; - - bool hasDynamicReferenceDistanceDelta = false; - float dynamicReferenceDistanceDelta = 0.f; -}; - -/// @brief One authored keyframe inside a reusable camera-sequence segment. -/// A keyframe can be described either as an absolute preset or as a delta relative to the captured reference preset. -struct CCameraSequenceKeyframe -{ - float time = 0.f; - bool hasAbsolutePreset = false; - CCameraPreset absolutePreset = {}; - bool hasDelta = false; - CCameraSequenceGoalDelta delta = {}; -}; - -/// @brief Concrete tracked-target pose sampled from a shared authored sequence. -struct CCameraSequenceTrackedTargetPose final : SCameraRigPose -{ -}; - -/// @brief Relative tracked-target adjustment authored against an initial tracked-target pose. -struct CCameraSequenceTrackedTargetDelta -{ - bool hasPositionOffset = false; - hlsl::float64_t3 positionOffset = hlsl::float64_t3(0.0); - - bool hasRotationEulerDegOffset = false; - hlsl::float32_t3 rotationEulerDegOffset = hlsl::float32_t3(0.f); -}; - -/// @brief One authored tracked-target keyframe inside a reusable camera-sequence segment. -/// Target keyframes stay camera-domain and can drive follow behavior without runtime-object references. -struct CCameraSequenceTrackedTargetKeyframe -{ - float time = 0.f; - bool hasAbsolutePosition = false; - hlsl::float64_t3 absolutePosition = hlsl::float64_t3(0.0); - bool hasAbsoluteRotationEulerDeg = false; - hlsl::float32_t3 absoluteRotationEulerDeg = hlsl::float32_t3(0.f); - bool hasDelta = false; - CCameraSequenceTrackedTargetDelta delta = {}; -}; - -/// @brief Runtime sampled tracked-target track built from an authored segment plus a reference pose. -/// Keyframes are normalized by time before sampling. Duplicate times collapse to the last authored pose. -struct CCameraSequenceTrackedTargetTrack -{ - struct SKeyframe - { - float time = 0.f; - CCameraSequenceTrackedTargetPose pose = {}; - }; - - std::vector keyframes; -}; - -/// @brief Defaults shared by all camera-sequence segments unless overridden locally. -struct CCameraSequenceSegmentDefaults -{ - float durationSeconds = 4.f; - std::vector presentations; - CCameraSequenceContinuitySettings continuity = {}; - std::vector captureFractions = { 1.f }; - bool resetCamera = true; -}; - -/// @brief Authored reusable camera-sequence segment. -/// A segment is the main unit of authored playback and validation and usually maps to one camera showcase chunk. -struct CCameraSequenceSegment -{ - std::string name; - ICamera::CameraKind cameraKind = ICamera::CameraKind::Unknown; - std::string cameraIdentifier; - - bool hasDurationSeconds = false; - float durationSeconds = 0.f; - - bool hasResetCamera = false; - bool resetCamera = true; - - std::vector presentations; - - bool hasContinuity = false; - CCameraSequenceContinuitySettings continuity = {}; - - bool hasCaptureFractions = false; - std::vector captureFractions; - - std::vector keyframes; - std::vector targetKeyframes; -}; - -/// @brief Top-level reusable camera-sequence script. -/// -/// This type stores the compact authored description that is later expanded -/// into runtime playback and check payloads. -struct CCameraSequenceScript -{ - bool enabled = true; - bool log = false; - bool exclusive = false; - bool hardFail = false; - bool visualDebug = false; - float visualDebugTargetFps = 0.f; - float visualDebugHoldSeconds = 0.f; - bool hasEnableActiveCameraMovement = false; - bool enableActiveCameraMovement = true; - std::string capturePrefix = "script"; - float fps = 60.f; - CCameraSequenceSegmentDefaults defaults = {}; - std::vector segments; -}; - -/// @brief Reusable compiled sequence segment derived from authored data plus captured references. -/// Consumers can build their own runtime actions/checks from this normalized representation. -struct CCameraSequenceCompiledSegment -{ - std::string name; - std::vector presentations; - CCameraSequenceContinuitySettings continuity = {}; - bool resetCamera = true; - float durationSeconds = 0.f; - uint64_t durationFrames = 0ull; - std::vector sampleTimes; - std::vector captureFrameOffsets; - CCameraKeyframeTrack track = {}; - CCameraSequenceTrackedTargetTrack trackedTargetTrack = {}; - - inline bool usesTrackedTargetTrack() const - { - return !trackedTargetTrack.keyframes.empty(); - } -}; - -/// @brief One compiled frame policy entry derived from a reusable compiled segment. -/// Consumers can map these booleans to their own runtime checks and capture requests. -struct CCameraSequenceCompiledFramePolicy -{ - uint64_t frameOffset = 0ull; - float sampleTime = 0.f; - bool capture = false; - bool baseline = false; - bool continuityStep = false; - bool followTargetLock = false; -}; - -struct CCameraSequenceScriptUtilities final -{ - static inline bool tryParseCameraKind(std::string_view value, ICamera::CameraKind& outKind) - { - if (value == "FPS") - outKind = ICamera::CameraKind::FPS; - else if (value == "Free") - outKind = ICamera::CameraKind::Free; - else if (value == "Orbit") - outKind = ICamera::CameraKind::Orbit; - else if (value == "Arcball") - outKind = ICamera::CameraKind::Arcball; - else if (value == "Turntable") - outKind = ICamera::CameraKind::Turntable; - else if (value == "TopDown") - outKind = ICamera::CameraKind::TopDown; - else if (value == "Isometric") - outKind = ICamera::CameraKind::Isometric; - else if (value == "Chase") - outKind = ICamera::CameraKind::Chase; - else if (value == "Dolly") - outKind = ICamera::CameraKind::Dolly; - else if (value == "DollyZoom" || value == "Dolly Zoom") - outKind = ICamera::CameraKind::DollyZoom; - else if (value == "PathRig" || value == "Path Rig") - outKind = ICamera::CameraKind::Path; - else - return false; - - return true; - } - - static inline bool tryParseProjectionType(std::string_view value, IPlanarProjection::CProjection::ProjectionType& outType) - { - if (value == "perspective" || value == "Perspective") - outType = IPlanarProjection::CProjection::Perspective; - else if (value == "orthographic" || value == "Orthographic") - outType = IPlanarProjection::CProjection::Orthographic; - else - return false; - - return true; - } - - static inline void normalizeCaptureFractions(std::vector& fractions) - { - for (auto& fraction : fractions) - fraction = std::clamp(fraction, 0.f, 1.f); - - std::sort(fractions.begin(), fractions.end()); - fractions.erase(std::unique(fractions.begin(), fractions.end(), - [](const float lhs, const float rhs) { return hlsl::CCameraMathUtilities::nearlyEqualScalar(lhs, rhs, static_cast(SCameraToolingThresholds::ScalarTolerance)); }), - fractions.end()); - } - - static inline bool buildSequenceKeyframePreset(const CCameraPreset& reference, const CCameraSequenceKeyframe& authored, CCameraPreset& outPreset, std::string* error = nullptr) - { - if (authored.hasAbsolutePreset) - { - outPreset = authored.absolutePreset; - if (outPreset.identifier.empty()) - outPreset.identifier = reference.identifier; - if (outPreset.name.empty()) - outPreset.name = reference.name; - return CCameraGoalUtilities::isGoalFinite(CCameraPresetUtilities::makeGoalFromPreset(outPreset)); - } - - outPreset = reference; - if (!authored.hasDelta) - return true; - - auto goal = CCameraPresetUtilities::makeGoalFromPreset(reference); - const auto& delta = authored.delta; - - const bool hasPoseDelta = delta.hasPositionOffset || delta.hasRotationEulerDegOffset; - const bool hasSphericalDelta = delta.hasTargetOffset || delta.orbitDelta.hasAny(); - const bool hasPathDelta = delta.pathDelta.hasAny(); - - if (hasPoseDelta && (hasSphericalDelta || hasPathDelta)) - { - if (error) - *error = "Sequence keyframe delta cannot mix pose offsets with spherical/path deltas."; - return false; - } - - if (delta.hasPositionOffset) - goal.position += delta.positionOffset; - - if (delta.hasRotationEulerDegOffset) - { - goal.orientation = hlsl::CCameraMathUtilities::normalizeQuaternion(goal.orientation * hlsl::CCameraMathUtilities::makeQuaternionFromEulerDegreesYXZ(hlsl::getCastedVector(delta.rotationEulerDegOffset))); - } - - if (delta.hasTargetOffset) - { - if (!goal.hasTargetPosition) - { - if (error) - *error = "Sequence keyframe target_offset requires target state."; - return false; - } - goal.targetPosition += delta.targetOffset; - } - - if (delta.orbitDelta.hasAny()) - { - if (!goal.hasOrbitState) - { - if (error) - *error = "Sequence keyframe orbit deltas require spherical orbit state."; - return false; - } - - if (delta.orbitDelta.hasU) - goal.orbitUv.x = hlsl::CCameraMathUtilities::wrapAngleRad(goal.orbitUv.x + delta.orbitDelta.uvDeltaRad.x); - if (delta.orbitDelta.hasV) - { - goal.orbitUv.y = std::clamp( - goal.orbitUv.y + delta.orbitDelta.uvDeltaRad.y, - -SCameraTargetRelativeRigDefaults::ArcballPitchLimitRad, - SCameraTargetRelativeRigDefaults::ArcballPitchLimitRad); - } - if (delta.orbitDelta.hasDistance) - goal.orbitDistance += delta.orbitDelta.distanceDelta; - } - - if (delta.pathDelta.hasAny()) - { - if (!goal.hasPathState) - { - if (error) - *error = "Sequence keyframe path deltas require path state."; - return false; - } - - if (!CCameraPathUtilities::tryApplyPathStateDelta( - goal.pathState, - delta.pathDelta.buildAppliedDelta(), - CCameraPathUtilities::makeDefaultPathLimits(), - goal.pathState)) - { - if (error) - *error = "Sequence keyframe path deltas produced an invalid path state."; - return false; - } - } - - if (delta.hasDynamicBaseFovDelta || delta.hasDynamicReferenceDistanceDelta) - { - if (!goal.hasDynamicPerspectiveState) - { - if (error) - *error = "Sequence keyframe dynamic perspective deltas require dynamic perspective state."; - return false; - } - if (delta.hasDynamicBaseFovDelta) - goal.dynamicPerspectiveState.baseFov = std::clamp(goal.dynamicPerspectiveState.baseFov + delta.dynamicBaseFovDelta, 1.f, 179.f); - if (delta.hasDynamicReferenceDistanceDelta) - goal.dynamicPerspectiveState.referenceDistance = std::max(0.001f, goal.dynamicPerspectiveState.referenceDistance + delta.dynamicReferenceDistanceDelta); - } - - if (hasPathDelta || hasSphericalDelta) - { - if (!CCameraGoalUtilities::applyCanonicalGoalState(goal)) - { - if (error) - *error = hasPathDelta ? - "Sequence keyframe failed to canonicalize path state." : - "Sequence keyframe failed to canonicalize spherical state."; - return false; - } - } - - if (!CCameraGoalUtilities::isGoalFinite(goal)) - { - if (error) - *error = "Sequence keyframe produced a non-finite goal."; - return false; - } - - CCameraPresetUtilities::assignGoalToPreset(outPreset, goal); - return true; - } - - static inline bool buildSequenceTrackFromReference(const CCameraPreset& reference, const CCameraSequenceSegment& segment, CCameraKeyframeTrack& outTrack, std::string* error = nullptr) - { - outTrack = {}; - outTrack.keyframes.reserve(segment.keyframes.size()); - - for (const auto& entry : segment.keyframes) - { - CCameraKeyframe keyframe; - keyframe.time = std::max(0.f, entry.time); - if (!buildSequenceKeyframePreset(reference, entry, keyframe.preset, error)) - return false; - outTrack.keyframes.emplace_back(std::move(keyframe)); - } - - CCameraKeyframeTrackUtilities::sortKeyframeTrackByTime(outTrack); - CCameraKeyframeTrackUtilities::normalizeSelectedKeyframeTrack(outTrack); - return !outTrack.keyframes.empty(); - } - - static inline bool isSequenceTrackedTargetPoseFinite(const CCameraSequenceTrackedTargetPose& pose) - { - return hlsl::CCameraMathUtilities::isFiniteVec3(pose.position) && - hlsl::CCameraMathUtilities::isFiniteQuaternion(pose.orientation); - } - - static inline bool buildSequenceTrackedTargetPoseFromReference( - const CCameraSequenceTrackedTargetPose& reference, - const CCameraSequenceTrackedTargetKeyframe& authored, - CCameraSequenceTrackedTargetPose& outPose, - std::string* error = nullptr) - { - outPose = reference; - - if (authored.hasAbsolutePosition) - outPose.position = authored.absolutePosition; - if (authored.hasAbsoluteRotationEulerDeg) - outPose.orientation = hlsl::CCameraMathUtilities::makeQuaternionFromEulerDegreesYXZ(hlsl::getCastedVector(authored.absoluteRotationEulerDeg)); - - if (authored.hasDelta) - { - if (authored.delta.hasPositionOffset) - outPose.position += authored.delta.positionOffset; - if (authored.delta.hasRotationEulerDegOffset) - outPose.orientation = hlsl::CCameraMathUtilities::normalizeQuaternion(outPose.orientation * hlsl::CCameraMathUtilities::makeQuaternionFromEulerDegreesYXZ(hlsl::getCastedVector(authored.delta.rotationEulerDegOffset))); - } - - if (!isSequenceTrackedTargetPoseFinite(outPose)) - { - if (error) - *error = "Sequence target keyframe produced a non-finite pose."; - return false; - } - - return true; - } - - static inline bool buildSequenceTrackedTargetTrackFromReference( - const CCameraSequenceTrackedTargetPose& reference, - const CCameraSequenceSegment& segment, - CCameraSequenceTrackedTargetTrack& outTrack, - std::string* error = nullptr) - { - outTrack = {}; - outTrack.keyframes.reserve(segment.targetKeyframes.size()); - - for (const auto& entry : segment.targetKeyframes) - { - CCameraSequenceTrackedTargetTrack::SKeyframe keyframe; - keyframe.time = std::max(0.f, entry.time); - if (!buildSequenceTrackedTargetPoseFromReference(reference, entry, keyframe.pose, error)) - return false; - outTrack.keyframes.emplace_back(std::move(keyframe)); - } - - std::stable_sort(outTrack.keyframes.begin(), outTrack.keyframes.end(), - [](const auto& lhs, const auto& rhs) - { - if (lhs.time == rhs.time) - return false; - return lhs.time < rhs.time; - }); - - std::vector normalized; - normalized.reserve(outTrack.keyframes.size()); - for (const auto& keyframe : outTrack.keyframes) - { - if (!normalized.empty() && hlsl::CCameraMathUtilities::nearlyEqualScalar(normalized.back().time, keyframe.time, static_cast(SCameraToolingThresholds::ScalarTolerance))) - normalized.back() = keyframe; - else - normalized.emplace_back(keyframe); - } - outTrack.keyframes = std::move(normalized); - - return !outTrack.keyframes.empty(); - } - - static inline bool tryBuildSequenceTrackedTargetPoseAtTime( - const CCameraSequenceTrackedTargetTrack& track, - const float time, - CCameraSequenceTrackedTargetPose& outPose) - { - if (track.keyframes.empty()) - return false; - if (track.keyframes.size() == 1u || time <= track.keyframes.front().time) - { - outPose = track.keyframes.front().pose; - return true; - } - if (time >= track.keyframes.back().time) - { - outPose = track.keyframes.back().pose; - return true; - } - - for (size_t ix = 1u; ix < track.keyframes.size(); ++ix) - { - const auto& lhs = track.keyframes[ix - 1u]; - const auto& rhs = track.keyframes[ix]; - if (time > rhs.time) - continue; - - const auto span = std::max(static_cast(SCameraToolingThresholds::ScalarTolerance), rhs.time - lhs.time); - const auto alpha = std::clamp((time - lhs.time) / span, 0.f, 1.f); - outPose.position = lhs.pose.position + (rhs.pose.position - lhs.pose.position) * static_cast(alpha); - outPose.orientation = hlsl::CCameraMathUtilities::slerpQuaternion(lhs.pose.orientation, rhs.pose.orientation, static_cast(alpha)); - return true; - } - - outPose = track.keyframes.back().pose; - return true; - } - - static inline bool sequenceSegmentUsesTrackedTargetTrack(const CCameraSequenceSegment& segment) - { - return !segment.targetKeyframes.empty(); - } - - static inline float getSequenceSegmentDurationSeconds(const CCameraSequenceScript& script, const CCameraSequenceSegment& segment, const CCameraKeyframeTrack* track = nullptr) - { - if (segment.hasDurationSeconds) - return std::max(0.f, segment.durationSeconds); - if (script.defaults.durationSeconds > 0.f) - return script.defaults.durationSeconds; - if (track) - return track->keyframes.empty() ? 0.f : track->keyframes.back().time; - return 0.f; - } - - static inline const std::vector& getSequenceSegmentPresentations(const CCameraSequenceScript& script, const CCameraSequenceSegment& segment) - { - return segment.presentations.empty() ? script.defaults.presentations : segment.presentations; - } - - static inline CCameraSequenceContinuitySettings getSequenceSegmentContinuity(const CCameraSequenceScript& script, const CCameraSequenceSegment& segment) - { - return segment.hasContinuity ? segment.continuity : script.defaults.continuity; - } - - static inline std::vector getSequenceSegmentCaptureFractions(const CCameraSequenceScript& script, const CCameraSequenceSegment& segment) - { - auto captures = segment.hasCaptureFractions ? segment.captureFractions : script.defaults.captureFractions; - normalizeCaptureFractions(captures); - return captures; - } - - static inline bool getSequenceSegmentResetCamera(const CCameraSequenceScript& script, const CCameraSequenceSegment& segment) - { - return segment.hasResetCamera ? segment.resetCamera : script.defaults.resetCamera; - } - - static inline bool sequenceScriptUsesMultiplePresentations(const CCameraSequenceScript& script) - { - if (script.defaults.presentations.size() > 1u) - return true; - - for (const auto& segment : script.segments) - { - if (getSequenceSegmentPresentations(script, segment).size() > 1u) - return true; - } - - return false; - } - - static inline uint64_t buildSequenceDurationFrames(const float durationSeconds, const float fps) - { - const auto safeDuration = std::max(0.f, durationSeconds); - const auto safeFps = std::max(1.f, fps); - return std::max(1ull, static_cast(std::llround(static_cast(safeDuration) * static_cast(safeFps)))); - } - - /// @brief Build one sampled time per authored frame in the compiled segment. - static inline void buildSequenceSampleTimes(const float durationSeconds, const uint64_t durationFrames, std::vector& outTimes) - { - outTimes.clear(); - outTimes.reserve(durationFrames); - - for (uint64_t frameOffset = 0u; frameOffset < durationFrames; ++frameOffset) - { - const float alpha = durationFrames > 1u ? static_cast(frameOffset) / static_cast(durationFrames - 1u) : 0.f; - outTimes.emplace_back(durationSeconds * alpha); - } - } - - /// @brief Expand normalized capture fractions into concrete frame offsets inside the compiled segment. - static inline void buildSequenceCaptureFrameOffsets( - const uint64_t durationFrames, - const std::vector& captureFractions, - std::vector& outOffsets) - { - outOffsets.clear(); - outOffsets.reserve(captureFractions.size()); - - for (const auto fraction : captureFractions) - { - const auto offset = durationFrames > 1u ? - static_cast(std::llround(static_cast(fraction) * static_cast(durationFrames - 1u))) : - 0ull; - outOffsets.emplace_back(offset); - } - - std::sort(outOffsets.begin(), outOffsets.end()); - outOffsets.erase(std::unique(outOffsets.begin(), outOffsets.end()), outOffsets.end()); - } - - /// @brief Compile one authored sequence segment into normalized reusable data for runtime consumers. - static inline bool compileSequenceSegmentFromReference( - const CCameraSequenceScript& script, - const CCameraSequenceSegment& segment, - const CCameraPreset& referencePreset, - const CCameraSequenceTrackedTargetPose& referenceTrackedTargetPose, - CCameraSequenceCompiledSegment& outSegment, - std::string* error = nullptr) - { - outSegment = {}; - outSegment.name = segment.name; - outSegment.presentations = getSequenceSegmentPresentations(script, segment); - outSegment.continuity = getSequenceSegmentContinuity(script, segment); - outSegment.resetCamera = getSequenceSegmentResetCamera(script, segment); - - if (!buildSequenceTrackFromReference(referencePreset, segment, outSegment.track, error)) - return false; - - if (sequenceSegmentUsesTrackedTargetTrack(segment) && - !buildSequenceTrackedTargetTrackFromReference(referenceTrackedTargetPose, segment, outSegment.trackedTargetTrack, error)) - { - return false; - } - - outSegment.durationSeconds = getSequenceSegmentDurationSeconds(script, segment, &outSegment.track); - outSegment.durationFrames = buildSequenceDurationFrames(outSegment.durationSeconds, script.fps); - buildSequenceSampleTimes(outSegment.durationSeconds, outSegment.durationFrames, outSegment.sampleTimes); - buildSequenceCaptureFrameOffsets(outSegment.durationFrames, getSequenceSegmentCaptureFractions(script, segment), outSegment.captureFrameOffsets); - return true; - } - - static inline bool buildCompiledSegmentFramePolicies( - const CCameraSequenceCompiledSegment& segment, - std::vector& outPolicies, - const bool includeFollowTargetLock = false) - { - if (segment.sampleTimes.size() != segment.durationFrames) - return false; - - outPolicies.clear(); - outPolicies.reserve(segment.durationFrames); - - size_t captureIx = 0u; - for (uint64_t frameOffset = 0u; frameOffset < segment.durationFrames; ++frameOffset) - { - CCameraSequenceCompiledFramePolicy policy; - policy.frameOffset = frameOffset; - policy.sampleTime = segment.sampleTimes[frameOffset]; - policy.baseline = segment.continuity.baseline && frameOffset == 0u; - policy.continuityStep = segment.continuity.step && frameOffset > 0u; - policy.followTargetLock = includeFollowTargetLock && segment.usesTrackedTargetTrack() && policy.continuityStep; - - while (captureIx < segment.captureFrameOffsets.size() && segment.captureFrameOffsets[captureIx] < frameOffset) - ++captureIx; - policy.capture = captureIx < segment.captureFrameOffsets.size() && segment.captureFrameOffsets[captureIx] == frameOffset; - if (policy.capture) - ++captureIx; - - outPolicies.emplace_back(std::move(policy)); - } - - return true; - } -}; - -} // namespace nbl::core - -#endif // _C_CAMERA_SEQUENCE_SCRIPT_HPP_ - diff --git a/common/include/camera/CCameraSequenceScriptPersistence.hpp b/common/include/camera/CCameraSequenceScriptPersistence.hpp deleted file mode 100644 index 1ec56871e..000000000 --- a/common/include/camera/CCameraSequenceScriptPersistence.hpp +++ /dev/null @@ -1,29 +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 _C_CAMERA_SEQUENCE_SCRIPT_PERSISTENCE_HPP_ -#define _C_CAMERA_SEQUENCE_SCRIPT_PERSISTENCE_HPP_ - -#include -#include -#include - -#include "CCameraSequenceScript.hpp" -#include "nbl/system/path.h" - -namespace nbl::system -{ - -class ISystem; - -/// @brief Parse one compact camera-sequence script from an existing stream. -bool readCameraSequenceScript(std::istream& in, core::CCameraSequenceScript& out, std::string* error = nullptr); -/// @brief Parse one compact camera-sequence script directly from text. -bool readCameraSequenceScript(std::string_view text, core::CCameraSequenceScript& out, std::string* error = nullptr); -/// @brief Load one compact camera-sequence script from a file. -bool loadCameraSequenceScriptFromFile(ISystem& system, const path& path, core::CCameraSequenceScript& out, std::string* error = nullptr); - -} // namespace nbl::system - -#endif // _C_CAMERA_SEQUENCE_SCRIPT_PERSISTENCE_HPP_ diff --git a/common/include/camera/CCameraSequenceScriptedBuilder.hpp b/common/include/camera/CCameraSequenceScriptedBuilder.hpp deleted file mode 100644 index f8dd5cf97..000000000 --- a/common/include/camera/CCameraSequenceScriptedBuilder.hpp +++ /dev/null @@ -1,128 +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 _C_CAMERA_SEQUENCE_SCRIPTED_BUILDER_HPP_ -#define _C_CAMERA_SEQUENCE_SCRIPTED_BUILDER_HPP_ - -#include - -#include "CCameraScriptedRuntime.hpp" -#include "CCameraSequenceScript.hpp" -#include "ICamera.hpp" - -namespace nbl::system -{ - -/// @brief Build expanded scripted runtime data from a compiled camera-sequence segment. -/// -/// The builder converts compiled sequence frames into the shared runtime event -/// and check payloads used by camera-sequence consumers. -struct CCameraSequenceScriptedSegmentBuildInfo -{ - /// @brief Planar index that receives the compiled segment. - uint32_t planarIx = 0u; - /// @brief Number of windows the consumer can actually route presentation actions to. - size_t availableWindowCount = 1u; - /// @brief Whether secondary-window presentation actions are emitted. - bool useWindow = false; - /// @brief Whether per-frame follow-lock checks are generated for this segment. - bool includeFollowTargetLock = false; -}; - -struct CCameraSequenceScriptedBuilderUtilities final -{ - /// @brief Append one compiled segment as expanded scripted runtime payloads. - static inline bool appendCompiledSequenceSegmentToScriptedTimeline( - CCameraScriptedTimeline& timeline, - const uint64_t baseFrame, - const core::CCameraSequenceCompiledSegment& compiledSegment, - const CCameraSequenceScriptedSegmentBuildInfo& buildInfo, - std::string* error = nullptr) - { - std::vector framePolicies; - if (!core::CCameraSequenceScriptUtilities::buildCompiledSegmentFramePolicies(compiledSegment, framePolicies, buildInfo.includeFollowTargetLock)) - { - if (error) - *error = "Failed to build compiled frame policies."; - return false; - } - - CCameraScriptedRuntimeUtilities::appendScriptedSegmentLabelEvent(timeline, baseFrame, compiledSegment.name); - CCameraScriptedRuntimeUtilities::appendScriptedActionEvent(timeline, baseFrame, CCameraScriptedInputEvent::ActionData::Kind::SetActiveRenderWindow, 0); - CCameraScriptedRuntimeUtilities::appendScriptedActionEvent(timeline, baseFrame, CCameraScriptedInputEvent::ActionData::Kind::SetActivePlanar, static_cast(buildInfo.planarIx)); - if (!compiledSegment.presentations.empty()) - { - CCameraScriptedRuntimeUtilities::appendScriptedActionEvent(timeline, baseFrame, CCameraScriptedInputEvent::ActionData::Kind::SetProjectionType, static_cast(compiledSegment.presentations[0].projection)); - CCameraScriptedRuntimeUtilities::appendScriptedActionEvent(timeline, baseFrame, CCameraScriptedInputEvent::ActionData::Kind::SetLeftHanded, compiledSegment.presentations[0].leftHanded ? 1 : 0); - } - if (compiledSegment.resetCamera) - CCameraScriptedRuntimeUtilities::appendScriptedActionEvent(timeline, baseFrame, CCameraScriptedInputEvent::ActionData::Kind::ResetActiveCamera, 1); - - if (buildInfo.useWindow) - { - for (size_t windowIx = 1u; windowIx < std::min(compiledSegment.presentations.size(), buildInfo.availableWindowCount); ++windowIx) - { - CCameraScriptedRuntimeUtilities::appendScriptedActionEvent(timeline, baseFrame, CCameraScriptedInputEvent::ActionData::Kind::SetActiveRenderWindow, static_cast(windowIx)); - CCameraScriptedRuntimeUtilities::appendScriptedActionEvent(timeline, baseFrame, CCameraScriptedInputEvent::ActionData::Kind::SetActivePlanar, static_cast(buildInfo.planarIx)); - CCameraScriptedRuntimeUtilities::appendScriptedActionEvent(timeline, baseFrame, CCameraScriptedInputEvent::ActionData::Kind::SetProjectionType, static_cast(compiledSegment.presentations[windowIx].projection)); - CCameraScriptedRuntimeUtilities::appendScriptedActionEvent(timeline, baseFrame, CCameraScriptedInputEvent::ActionData::Kind::SetLeftHanded, compiledSegment.presentations[windowIx].leftHanded ? 1 : 0); - } - CCameraScriptedRuntimeUtilities::appendScriptedActionEvent(timeline, baseFrame, CCameraScriptedInputEvent::ActionData::Kind::SetActiveRenderWindow, 0); - } - - for (const auto& policy : framePolicies) - { - core::CCameraPreset preset; - if (!core::CCameraKeyframeTrackUtilities::tryBuildKeyframeTrackPresetAtTime(compiledSegment.track, policy.sampleTime, preset)) - { - if (error) - *error = "Failed to sample compiled segment track."; - return false; - } - CCameraScriptedRuntimeUtilities::appendScriptedGoalEvent( - timeline, - baseFrame + policy.frameOffset, - core::CCameraPresetUtilities::makeGoalFromPreset(preset)); - - if (compiledSegment.usesTrackedTargetTrack()) - { - core::CCameraSequenceTrackedTargetPose trackedTargetPose; - if (!core::CCameraSequenceScriptUtilities::tryBuildSequenceTrackedTargetPoseAtTime(compiledSegment.trackedTargetTrack, policy.sampleTime, trackedTargetPose)) - { - if (error) - *error = "Failed to sample compiled tracked-target track."; - return false; - } - - core::ICamera::CGimbal gimbal({ .position = trackedTargetPose.position, .orientation = trackedTargetPose.orientation }); - CCameraScriptedRuntimeUtilities::appendScriptedTrackedTargetTransformEvent(timeline, baseFrame + policy.frameOffset, gimbal.operator()()); - } - - if (policy.baseline) - CCameraScriptedRuntimeUtilities::appendScriptedBaselineCheck(timeline, baseFrame + policy.frameOffset); - if (policy.continuityStep) - { - 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) - CCameraScriptedRuntimeUtilities::appendScriptedFollowTargetLockCheck(timeline, baseFrame + policy.frameOffset); - if (policy.capture) - timeline.captureFrames.emplace_back(baseFrame + policy.frameOffset); - } - - return true; - } -}; - -} // namespace nbl::system - -#endif diff --git a/common/include/camera/CCameraSmokeRegressionUtilities.hpp b/common/include/camera/CCameraSmokeRegressionUtilities.hpp deleted file mode 100644 index 9ca40340b..000000000 --- a/common/include/camera/CCameraSmokeRegressionUtilities.hpp +++ /dev/null @@ -1,122 +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 _C_CAMERA_SMOKE_REGRESSION_UTILITIES_HPP_ -#define _C_CAMERA_SMOKE_REGRESSION_UTILITIES_HPP_ - -#include - -#include "CCameraKeyframeTrack.hpp" -#include "CCameraMathUtilities.hpp" -#include "CCameraPresetFlow.hpp" -#include "ICamera.hpp" - -namespace nbl::system -{ - -using SCameraManipulationDelta = hlsl::SCameraPoseDelta; - -struct SCameraSmokeComparisonThresholds final -{ - static constexpr double TinyScalarEpsilon = core::SCameraToolingThresholds::TinyScalarEpsilon; - static constexpr double DefaultPositionTolerance = core::SCameraToolingThresholds::DefaultPositionTolerance; - static constexpr double DefaultAngularToleranceDeg = core::SCameraToolingThresholds::DefaultAngularToleranceDeg; - static constexpr double DefaultScalarTolerance = core::SCameraToolingThresholds::ScalarTolerance; - static constexpr double StrictPositionTolerance = core::SCameraToolingThresholds::ScalarTolerance; - static constexpr double StrictAngularToleranceDeg = core::SCameraToolingThresholds::DefaultAngularToleranceDeg; - static constexpr double StrictScalarTolerance = core::SCameraToolingThresholds::ScalarTolerance; - static constexpr double TrackTimeTolerance = core::SCameraToolingThresholds::ScalarTolerance; -}; - -struct CCameraSmokeRegressionUtilities final -{ -public: - /// @brief Measure one camera pose delta against an authored reference pose. - static inline bool tryComputeCameraManipulationDelta( - core::ICamera* camera, - const hlsl::float64_t3& beforePosition, - const hlsl::camera_quaternion_t& beforeOrientation, - SCameraManipulationDelta& outDelta) - { - outDelta = {}; - if (!camera) - return false; - - const auto& gimbal = camera->getGimbal(); - const auto afterPosition = gimbal.getPosition(); - const auto afterOrientation = hlsl::CCameraMathUtilities::normalizeQuaternion(gimbal.getOrientation()); - return hlsl::CCameraMathUtilities::tryComputePoseDelta(afterPosition, afterOrientation, beforePosition, beforeOrientation, outDelta); - } - - /// @brief Manipulate a camera and report how far its pose moved in position and Euler-angle terms. - static inline bool tryManipulateCameraAndMeasureDelta( - core::ICamera* camera, - std::span events, - SCameraManipulationDelta& outDelta, - const double tinyEpsilon = SCameraSmokeComparisonThresholds::TinyScalarEpsilon) - { - outDelta = {}; - if (!camera || events.empty()) - return false; - - const auto& beforeGimbal = camera->getGimbal(); - const auto beforePosition = beforeGimbal.getPosition(); - const auto beforeOrientation = hlsl::CCameraMathUtilities::normalizeQuaternion(beforeGimbal.getOrientation()); - if (!hlsl::CCameraMathUtilities::isFiniteVec3(beforePosition) || !hlsl::CCameraMathUtilities::isFiniteQuaternion(beforeOrientation)) - return false; - - if (!camera->manipulate(events)) - return false; - - if (!tryComputeCameraManipulationDelta(camera, beforePosition, beforeOrientation, outDelta)) - return false; - - return outDelta.position > tinyEpsilon || outDelta.rotationDeg > tinyEpsilon; - } - - static inline bool comparePresetToCameraStateWithDefaultThresholds( - const core::CCameraGoalSolver& solver, - core::ICamera* camera, - const core::CCameraPreset& preset) - { - return core::CCameraPresetFlowUtilities::comparePresetToCameraState( - solver, - camera, - preset, - SCameraSmokeComparisonThresholds::DefaultPositionTolerance, - SCameraSmokeComparisonThresholds::DefaultAngularToleranceDeg, - SCameraSmokeComparisonThresholds::DefaultScalarTolerance); - } - - static inline bool comparePresetToCameraStateWithStrictThresholds( - const core::CCameraGoalSolver& solver, - core::ICamera* camera, - const core::CCameraPreset& preset) - { - return core::CCameraPresetFlowUtilities::comparePresetToCameraState( - solver, - camera, - preset, - SCameraSmokeComparisonThresholds::StrictPositionTolerance, - SCameraSmokeComparisonThresholds::StrictAngularToleranceDeg, - SCameraSmokeComparisonThresholds::StrictScalarTolerance); - } - - static inline bool compareKeyframeTrackContentWithStrictThresholds( - const core::CCameraKeyframeTrack& lhs, - const core::CCameraKeyframeTrack& rhs) - { - return core::CCameraKeyframeTrackUtilities::compareKeyframeTrackContent( - lhs, - rhs, - SCameraSmokeComparisonThresholds::TrackTimeTolerance, - SCameraSmokeComparisonThresholds::StrictPositionTolerance, - SCameraSmokeComparisonThresholds::StrictAngularToleranceDeg, - SCameraSmokeComparisonThresholds::StrictScalarTolerance); - } -}; - -} // namespace nbl::system - -#endif // _C_CAMERA_SMOKE_REGRESSION_UTILITIES_HPP_ diff --git a/common/include/camera/CCameraTargetRelativeUtilities.hpp b/common/include/camera/CCameraTargetRelativeUtilities.hpp deleted file mode 100644 index 7271685f5..000000000 --- a/common/include/camera/CCameraTargetRelativeUtilities.hpp +++ /dev/null @@ -1,270 +0,0 @@ -#ifndef _C_CAMERA_TARGET_RELATIVE_UTILITIES_HPP_ -#define _C_CAMERA_TARGET_RELATIVE_UTILITIES_HPP_ - -#include - -#include "SCameraRigPose.hpp" -#include "CCameraVirtualEventUtilities.hpp" - -namespace nbl::core -{ - -/// @brief Canonical target-relative orbit state used by spherical cameras, follow, and goal solving. -struct SCameraTargetRelativeState final -{ - hlsl::float64_t3 target = hlsl::float64_t3(0.0); - hlsl::float64_t2 orbitUv = hlsl::float64_t2(0.0); - float distance = SCameraTargetRelativeTraits::MinDistance; -}; - -/// @brief Pose reconstructed from a target-relative orbit state. -struct SCameraTargetRelativePose final : SCameraRigPose -{ - hlsl::float64_t appliedDistance = static_cast(SCameraTargetRelativeTraits::MinDistance); -}; - -/// @brief Derived basis for target-relative orbit rigs. -struct SCameraTargetRelativeBasis final -{ - hlsl::float64_t3 localOffset = hlsl::float64_t3(0.0); - hlsl::float64_t3 right = hlsl::float64_t3(1.0, 0.0, 0.0); - hlsl::float64_t3 up = hlsl::float64_t3(0.0, 0.0, 1.0); - hlsl::float64_t3 forward = hlsl::float64_t3(0.0, 1.0, 0.0); -}; - -/// @brief Delta between current spherical target state and canonical target-relative goal. -struct SCameraTargetRelativeDelta final -{ - hlsl::float64_t2 orbitUv = hlsl::float64_t2(0.0); - double distance = 0.0; - - inline hlsl::float64_t3 orbitVector() const - { - return hlsl::float64_t3(orbitUv.y, orbitUv.x, 0.0); - } -}; - -/// @brief Mapping policy describing how a target-relative delta is converted into virtual events. -struct SCameraTargetRelativeEventPolicy final -{ - bool translateOrbit = false; - bool allowYaw = true; - bool allowPitch = true; - SCameraVirtualEventAxisBinding distanceBinding = { - CVirtualGimbalEvent::MoveForward, - CVirtualGimbalEvent::MoveBackward - }; -}; - -/// @brief Default constants and event policies used by target-relative rigs. -struct SCameraTargetRelativeRigDefaults final -{ - static constexpr float InitialDistance = 1.0f; - static constexpr double ArcballPitchLimitRad = hlsl::SCameraViewRigDefaults::ArcballPitchLimitRad; - static constexpr double TurntablePitchLimitRad = hlsl::SCameraViewRigDefaults::TurntablePitchLimitRad; - static constexpr double ChaseMaxPitchRad = hlsl::SCameraViewRigDefaults::ChaseMaxPitchRad; - static constexpr double ChaseMinPitchRad = hlsl::SCameraViewRigDefaults::ChaseMinPitchRad; - static constexpr double DollyPitchLimitRad = hlsl::SCameraViewRigDefaults::DollyPitchLimitRad; - static constexpr double TopDownPitchRad = hlsl::SCameraViewRigDefaults::TopDownPitchRad; - static constexpr double IsometricYawRad = hlsl::SCameraViewRigDefaults::IsometricYawRad; - static constexpr double IsometricPitchRad = hlsl::SCameraViewRigDefaults::IsometricPitchRad; - - static inline constexpr SCameraTargetRelativeEventPolicy OrbitTranslatePolicy = { - .translateOrbit = true - }; - static inline constexpr SCameraTargetRelativeEventPolicy RotateDistancePolicy = { - .translateOrbit = false, - .allowYaw = true, - .allowPitch = true - }; - static inline constexpr SCameraTargetRelativeEventPolicy TopDownPolicy = { - .translateOrbit = false, - .allowYaw = true, - .allowPitch = false - }; - static inline constexpr SCameraTargetRelativeEventPolicy IsometricPolicy = { - .translateOrbit = false, - .allowYaw = false, - .allowPitch = false - }; - static inline constexpr SCameraTargetRelativeEventPolicy DollyPolicy = { - .translateOrbit = false, - .allowYaw = true, - .allowPitch = true, - .distanceBinding = { - CVirtualGimbalEvent::None, - CVirtualGimbalEvent::None - } - }; - static inline constexpr SCameraTargetRelativeEventPolicy ChasePolicy = { - .translateOrbit = false, - .allowYaw = true, - .allowPitch = true, - .distanceBinding = { - CVirtualGimbalEvent::MoveUp, - CVirtualGimbalEvent::MoveDown - } - }; -}; - -/// @brief Helpers for converting between target-relative state, pose, basis, and virtual-event deltas. -struct CCameraTargetRelativeUtilities final -{ - static inline bool tryBuildTargetRelativeStateFromPosition( - const hlsl::float64_t3& targetPosition, - const hlsl::float64_t3& position, - const float minDistance, - const float maxDistance, - SCameraTargetRelativeState& outState) - { - outState = {}; - outState.target = targetPosition; - - hlsl::float64_t appliedDistance = static_cast(minDistance); - if (!hlsl::CCameraMathUtilities::tryBuildOrbitFromPosition( - targetPosition, - position, - static_cast(minDistance), - static_cast(maxDistance), - outState.orbitUv, - appliedDistance)) - { - return false; - } - - outState.distance = static_cast(appliedDistance); - return true; - } - - static inline bool tryBuildTargetRelativePoseFromState( - const SCameraTargetRelativeState& state, - const float minDistance, - const float maxDistance, - SCameraTargetRelativePose& outPose) - { - outPose = {}; - return hlsl::CCameraMathUtilities::tryBuildSphericalPoseFromOrbit( - state.target, - state.orbitUv, - static_cast(state.distance), - static_cast(minDistance), - static_cast(maxDistance), - outPose.position, - outPose.orientation, - &outPose.appliedDistance); - } - - static inline bool tryBuildTargetRelativeBasis( - const SCameraTargetRelativeState& state, - const float minDistance, - const float maxDistance, - SCameraTargetRelativeBasis& outBasis) - { - SCameraTargetRelativePose pose = {}; - if (!tryBuildTargetRelativePoseFromState(state, minDistance, maxDistance, pose)) - return false; - - outBasis.localOffset = pose.position - state.target; - const auto basis = hlsl::CCameraMathUtilities::getQuaternionBasisMatrix(pose.orientation); - outBasis.right = basis[0]; - outBasis.up = basis[1]; - outBasis.forward = basis[2]; - return true; - } - - static inline bool tryBuildTargetRelativePoseFromPosition( - const hlsl::float64_t3& targetPosition, - const hlsl::float64_t3& position, - const float minDistance, - const float maxDistance, - SCameraTargetRelativePose& outPose, - SCameraTargetRelativeState* outState = nullptr) - { - SCameraTargetRelativeState state = {}; - if (!tryBuildTargetRelativeStateFromPosition(targetPosition, position, minDistance, maxDistance, state)) - return false; - - if (!tryBuildTargetRelativePoseFromState(state, minDistance, maxDistance, outPose)) - return false; - - if (outState) - *outState = state; - return true; - } - - static inline SCameraTargetRelativeDelta buildTargetRelativeDelta( - const ICamera::SphericalTargetState& currentState, - const SCameraTargetRelativeState& desiredState) - { - return { - .orbitUv = hlsl::float64_t2( - hlsl::CCameraMathUtilities::wrapAngleRad(desiredState.orbitUv.x - currentState.orbitUv.x), - hlsl::CCameraMathUtilities::wrapAngleRad(desiredState.orbitUv.y - currentState.orbitUv.y)), - .distance = static_cast(desiredState.distance - currentState.distance) - }; - } - - static inline void appendTargetRelativeDeltaEvents( - std::vector& events, - const SCameraTargetRelativeDelta& delta, - const double angularDenominator, - const double angularToleranceDeg, - const double distanceDenominator, - const double distanceTolerance, - const SCameraTargetRelativeEventPolicy& policy) - { - if (policy.translateOrbit) - { - CCameraVirtualEventUtilities::appendAngularAxisEvents( - events, - delta.orbitVector(), - hlsl::float64_t3(angularDenominator), - hlsl::float64_t3(angularToleranceDeg, angularToleranceDeg, std::numeric_limits::infinity()), - {{ - { CVirtualGimbalEvent::MoveRight, CVirtualGimbalEvent::MoveLeft }, - { CVirtualGimbalEvent::MoveUp, CVirtualGimbalEvent::MoveDown }, - { CVirtualGimbalEvent::None, CVirtualGimbalEvent::None } - }}); - } - else - { - if (policy.allowYaw) - { - CCameraVirtualEventUtilities::appendAngularDeltaEvent( - events, - delta.orbitUv.x, - angularDenominator, - angularToleranceDeg, - CVirtualGimbalEvent::PanRight, - CVirtualGimbalEvent::PanLeft); - } - if (policy.allowPitch) - { - CCameraVirtualEventUtilities::appendAngularDeltaEvent( - events, - delta.orbitUv.y, - angularDenominator, - angularToleranceDeg, - CVirtualGimbalEvent::TiltUp, - CVirtualGimbalEvent::TiltDown); - } - } - - if (policy.distanceBinding.positive != CVirtualGimbalEvent::None && - policy.distanceBinding.negative != CVirtualGimbalEvent::None) - { - CCameraVirtualEventUtilities::appendScaledVirtualEvent( - events, - delta.distance, - distanceDenominator, - distanceTolerance, - policy.distanceBinding.positive, - policy.distanceBinding.negative); - } - } -}; - -} // namespace nbl::core - -#endif // _C_CAMERA_TARGET_RELATIVE_UTILITIES_HPP_ - diff --git a/common/include/camera/CCameraTextUtilities.hpp b/common/include/camera/CCameraTextUtilities.hpp deleted file mode 100644 index d07212b39..000000000 --- a/common/include/camera/CCameraTextUtilities.hpp +++ /dev/null @@ -1,210 +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 _C_CAMERA_TEXT_UTILITIES_HPP_ -#define _C_CAMERA_TEXT_UTILITIES_HPP_ - -#include -#include -#include - -#include "CCameraFollowUtilities.hpp" -#include "CCameraGoalAnalysis.hpp" -#include "CCameraPresetFlow.hpp" - -namespace nbl::ui -{ - -struct CCameraTextUtilities final -{ -public: - /// @brief Return a short human-readable label for a camera kind. - static inline std::string_view getCameraTypeLabel(const core::ICamera::CameraKind kind) - { - return core::CCameraKindUtilities::getCameraKindLabel(kind); - } - - /// @brief Return a short human-readable label for a concrete camera instance. - static inline std::string_view getCameraTypeLabel(const core::ICamera* camera) - { - return camera ? getCameraTypeLabel(camera->getKind()) : "Unknown"; - } - - /// @brief Return a short human-readable description for a camera kind. - static inline std::string_view getCameraTypeDescription(const core::ICamera::CameraKind kind) - { - return core::CCameraKindUtilities::getCameraKindDescription(kind); - } - - /// @brief Return a short human-readable description for a concrete camera instance. - static inline std::string_view getCameraTypeDescription(const core::ICamera* camera) - { - return camera ? getCameraTypeDescription(camera->getKind()) : "Unspecified camera behavior"; - } - - /// @brief Return a short human-readable label for a follow mode. - static inline constexpr const char* getCameraFollowModeLabel(const core::ECameraFollowMode mode) - { - switch (mode) - { - case core::ECameraFollowMode::Disabled: return "Disabled"; - case core::ECameraFollowMode::OrbitTarget: return "Orbit target"; - case core::ECameraFollowMode::LookAtTarget: return "Look at target"; - case core::ECameraFollowMode::KeepWorldOffset: return "Keep world offset"; - case core::ECameraFollowMode::KeepLocalOffset: return "Keep local offset"; - default: return "Unknown"; - } - } - - /// @brief Return a short human-readable description for a follow mode. - static inline constexpr const char* getCameraFollowModeDescription(const core::ECameraFollowMode mode) - { - switch (mode) - { - case core::ECameraFollowMode::Disabled: return "Follow disabled"; - case core::ECameraFollowMode::OrbitTarget: return "Keep orbit around moving target and keep it centered"; - case core::ECameraFollowMode::LookAtTarget: return "Keep camera position and lock the view onto the target"; - case core::ECameraFollowMode::KeepWorldOffset: return "Move with the target in world offset and keep it centered"; - case core::ECameraFollowMode::KeepLocalOffset: return "Move with the target in target-local offset and keep it centered"; - default: return "Unknown follow mode"; - } - } - - /// @brief Describe the typed goal-state mask in a stable human-readable format. - static inline std::string describeGoalStateMask(const uint32_t mask) - { - if (mask == core::ICamera::GoalStateNone) - return "Pose only"; - - std::string out; - auto append = [&](const char* label, const uint32_t bit) -> void - { - if ((mask & bit) != bit) - return; - if (!out.empty()) - out += ", "; - out += label; - }; - - append("Spherical target", core::ICamera::GoalStateSphericalTarget); - append("Dynamic perspective", core::ICamera::GoalStateDynamicPerspective); - append("Path rig state", core::ICamera::GoalStatePath); - return out; - } - - /// @brief Describe a detailed goal-apply result for logs, smoke tests, and UI summaries. - static inline std::string describeApplyResult(const core::CCameraGoalSolver::SApplyResult& result) - { - std::ostringstream oss; - oss << "status="; - switch (result.status) - { - case core::CCameraGoalSolver::SApplyResult::EStatus::Unsupported: oss << "Unsupported"; break; - case core::CCameraGoalSolver::SApplyResult::EStatus::Failed: oss << "Failed"; break; - case core::CCameraGoalSolver::SApplyResult::EStatus::AlreadySatisfied: oss << "AlreadySatisfied"; break; - case core::CCameraGoalSolver::SApplyResult::EStatus::AppliedAbsoluteOnly: oss << "AppliedAbsoluteOnly"; break; - case core::CCameraGoalSolver::SApplyResult::EStatus::AppliedVirtualEvents: oss << "AppliedVirtualEvents"; break; - case core::CCameraGoalSolver::SApplyResult::EStatus::AppliedAbsoluteAndVirtualEvents: oss << "AppliedAbsoluteAndVirtualEvents"; break; - } - oss << " exact=" << (result.exact ? "true" : "false") - << " events=" << result.eventCount; - - if (result.issues != core::CCameraGoalSolver::SApplyResult::NoIssue) - { - oss << " issues="; - bool first = true; - auto appendIssue = [&](const char* label, const core::CCameraGoalSolver::SApplyResult::EIssue issue) -> void - { - if (!result.hasIssue(issue)) - return; - if (!first) - oss << ","; - oss << label; - first = false; - }; - - appendIssue("absolute_pose_fallback", core::CCameraGoalSolver::SApplyResult::UsedAbsolutePoseFallback); - appendIssue("missing_spherical_state", core::CCameraGoalSolver::SApplyResult::MissingSphericalTargetState); - appendIssue("missing_path_state", core::CCameraGoalSolver::SApplyResult::MissingPathState); - appendIssue("missing_dynamic_perspective_state", core::CCameraGoalSolver::SApplyResult::MissingDynamicPerspectiveState); - appendIssue("virtual_event_replay_failed", core::CCameraGoalSolver::SApplyResult::VirtualEventReplayFailed); - } - - return oss.str(); - } - - /// @brief Describe compatibility preview for applying one analyzed goal to a target camera. - static inline std::string describeGoalApplyCompatibility(const core::SCameraGoalApplyAnalysis& analysis, const core::ICamera* targetCamera) - { - if (!analysis.hasCamera) - return "No active camera"; - - std::ostringstream oss; - oss << (analysis.compatibility.exact ? "Exact" : "Best-effort") - << " | source=" << getCameraTypeLabel(analysis.goal.sourceKind) - << " | target=" << getCameraTypeLabel(targetCamera); - - if (analysis.compatibility.missingGoalStateMask != core::ICamera::GoalStateNone) - oss << " | missing=" << describeGoalStateMask(analysis.compatibility.missingGoalStateMask); - else if (!analysis.compatibility.sameKind && analysis.goal.sourceKind != core::ICamera::CameraKind::Unknown) - oss << " | shared goal state only"; - - return oss.str(); - } - - /// @brief Describe whether an analyzed goal can be meaningfully applied to the target camera. - static inline std::string describeGoalApplyPolicy(const core::SCameraGoalApplyAnalysis& analysis) - { - if (!analysis.hasCamera) - return "Blocked | no active camera"; - if (!analysis.finiteGoal) - return "Blocked | invalid goal state"; - - std::ostringstream oss; - oss << (analysis.compatibility.exact ? "Exact apply" : "Best-effort apply"); - if (analysis.compatibility.missingGoalStateMask != core::ICamera::GoalStateNone) - oss << " | drops=" << describeGoalStateMask(analysis.compatibility.missingGoalStateMask); - else if (!analysis.compatibility.sameKind && analysis.goal.sourceKind != core::ICamera::CameraKind::Unknown) - oss << " | shared goal state only"; - else - oss << " | full preview available"; - - return oss.str(); - } - - /// @brief Describe whether one analyzed camera state can be captured into a reusable goal. - static inline std::string describeCameraCapturePolicy(const core::SCameraCaptureAnalysis& analysis, const core::ICamera* camera) - { - if (!analysis.hasCamera) - return "Blocked | no active camera"; - if (!analysis.capturedGoal) - return "Blocked | goal capture failed"; - if (!analysis.finiteGoal) - return "Blocked | invalid goal state"; - - std::ostringstream oss; - oss << "Ready | source=" << getCameraTypeLabel(camera) - << " | goal=" << describeGoalStateMask(analysis.goal.sourceGoalStateMask); - return oss.str(); - } - - /// @brief Describe the aggregate outcome of applying one preset to multiple cameras. - static inline std::string describePresetApplySummary(const core::SCameraPresetApplySummary& summary, std::string_view noTargetsLabel, std::string_view prefix = "Playback apply") - { - if (!summary.hasTargets()) - return std::string(noTargetsLabel); - - std::ostringstream oss; - oss << prefix << " | targets=" << summary.targetCount << " | ok=" << summary.successCount; - if (summary.approximateCount > 0u) - oss << " | approximate=" << summary.approximateCount; - if (summary.failureCount > 0u) - oss << " | failed=" << summary.failureCount; - return oss.str(); - } -}; - -} // namespace nbl::ui - -#endif // _C_CAMERA_TEXT_UTILITIES_HPP_ diff --git a/common/include/camera/CCameraTraits.hpp b/common/include/camera/CCameraTraits.hpp deleted file mode 100644 index ecc9c2d7a..000000000 --- a/common/include/camera/CCameraTraits.hpp +++ /dev/null @@ -1,41 +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 _C_CAMERA_TRAITS_HPP_ -#define _C_CAMERA_TRAITS_HPP_ - -#include - -namespace nbl::core -{ - -/// @brief Geometric constants used by target-relative camera families. -/// -/// `MinDistance` prevents zero-distance target-relative states. -/// `DefaultMaxDistance` is unbounded. Individual cameras and tools may apply -/// their own finite limits on top of it. -struct SCameraTargetRelativeTraits final -{ - /// @brief Smallest valid target-relative distance shared by spherical and path-style rigs. - static inline constexpr float MinDistance = 0.1f; - /// @brief Default upper bound for target-relative distance when no camera-specific cap is requested. - static inline constexpr float DefaultMaxDistance = std::numeric_limits::infinity(); -}; - -/// @brief Comparison thresholds used by helper layers outside the runtime camera interface. -struct SCameraToolingThresholds final -{ - /// @brief Default scalar tolerance used by typed state comparisons. - static inline constexpr double ScalarTolerance = 1e-6; - /// @brief Small epsilon used by replay and comparison helpers that need stricter zero tests. - static inline constexpr double TinyScalarEpsilon = 1e-9; - /// @brief Default world-space position tolerance used by pose comparisons. - static inline constexpr double DefaultPositionTolerance = 2.0 * ScalarTolerance; - /// @brief Default angular tolerance in degrees used by pose and state comparisons. - static inline constexpr double DefaultAngularToleranceDeg = 0.1; -}; - -} // namespace nbl::core - -#endif // _C_CAMERA_TRAITS_HPP_ diff --git a/common/include/camera/CCameraViewportOverlayUtilities.hpp b/common/include/camera/CCameraViewportOverlayUtilities.hpp index 1902ea882..a1c2d364a 100644 --- a/common/include/camera/CCameraViewportOverlayUtilities.hpp +++ b/common/include/camera/CCameraViewportOverlayUtilities.hpp @@ -8,7 +8,7 @@ #include #include -#include "CCameraFollowRegressionUtilities.hpp" +#include "nbl/ext/Cameras/CCameraFollowRegressionUtilities.hpp" #include "imgui/imgui.h" namespace nbl::ui diff --git a/common/include/camera/CCameraVirtualEventUtilities.hpp b/common/include/camera/CCameraVirtualEventUtilities.hpp deleted file mode 100644 index 31baae246..000000000 --- a/common/include/camera/CCameraVirtualEventUtilities.hpp +++ /dev/null @@ -1,188 +0,0 @@ -#ifndef _C_CAMERA_VIRTUAL_EVENT_UTILITIES_HPP_ -#define _C_CAMERA_VIRTUAL_EVENT_UTILITIES_HPP_ - -#include -#include -#include - -#include "CCameraMathUtilities.hpp" -#include "ICamera.hpp" - -namespace nbl::core -{ - -/// @brief Positive and negative semantic virtual-event pair for one scalar axis. -struct SCameraVirtualEventAxisBinding final -{ - CVirtualGimbalEvent::VirtualEventType positive = CVirtualGimbalEvent::None; - CVirtualGimbalEvent::VirtualEventType negative = CVirtualGimbalEvent::None; -}; - -/// @brief Reusable axis-binding presets shared by helpers that synthesize virtual events. -struct SCameraVirtualEventBindings final -{ - static inline constexpr std::array LocalTranslation = {{ - { CVirtualGimbalEvent::MoveRight, CVirtualGimbalEvent::MoveLeft }, - { CVirtualGimbalEvent::MoveUp, CVirtualGimbalEvent::MoveDown }, - { CVirtualGimbalEvent::MoveForward, CVirtualGimbalEvent::MoveBackward } - }}; -}; - -/// @brief Shared helpers for building and analyzing `CVirtualGimbalEvent` batches. -/// -/// These utilities are reused by goal replay, path-model control translation, -/// scripted tooling, and smoke checks whenever code needs to convert typed deltas -/// into semantic event streams or inspect those streams on the CPU. -struct CCameraVirtualEventUtilities final -{ -public: - /// @brief Append one signed scalar as either the positive or negative event variant. - static inline void appendSignedVirtualEvent( - std::vector& events, - const double value, - const CVirtualGimbalEvent::VirtualEventType positive, - const CVirtualGimbalEvent::VirtualEventType negative, - const double tolerance = static_cast(SCameraToolingThresholds::TinyScalarEpsilon)) - { - if (!hlsl::CCameraMathUtilities::isFiniteScalar(value) || hlsl::CCameraMathUtilities::isNearlyZeroScalar(value, tolerance)) - return; - - auto& ev = events.emplace_back(); - ev.type = (value > 0.0) ? positive : negative; - ev.magnitude = hlsl::abs(value); - } - - /// @brief Append one signed scalar after normalizing it by a caller-provided denominator. - static inline void appendScaledVirtualEvent( - std::vector& events, - const double value, - const double denominator, - const double tolerance, - const CVirtualGimbalEvent::VirtualEventType positive, - const CVirtualGimbalEvent::VirtualEventType negative) - { - if (!hlsl::CCameraMathUtilities::isFiniteScalar(denominator) || hlsl::CCameraMathUtilities::isNearlyZeroScalar(denominator, static_cast(SCameraToolingThresholds::TinyScalarEpsilon))) - return; - - appendSignedVirtualEvent(events, value / denominator, positive, negative, tolerance); - } - - /// @brief Append one angular delta by comparing it against a tolerance expressed in degrees. - static inline void appendAngularDeltaEvent( - std::vector& events, - const double deltaRadians, - const double denominator, - const double toleranceDeg, - const CVirtualGimbalEvent::VirtualEventType positive, - const CVirtualGimbalEvent::VirtualEventType negative) - { - if (!hlsl::CCameraMathUtilities::isFiniteScalar(deltaRadians) || - hlsl::CCameraMathUtilities::isNearlyZeroScalar(hlsl::degrees(deltaRadians), toleranceDeg)) - { - return; - } - - appendScaledVirtualEvent( - events, - deltaRadians, - denominator, - hlsl::radians(toleranceDeg), - positive, - negative); - } - - /// @brief Append one 3-axis scalar bundle through a caller-provided binding table. - static inline void appendScaledVirtualAxisEvents( - std::vector& events, - const hlsl::float64_t3& values, - const hlsl::float64_t3& denominators, - const hlsl::float64_t3& tolerances, - const std::array& axisBindings) - { - for (size_t axisIx = 0u; axisIx < axisBindings.size(); ++axisIx) - { - appendScaledVirtualEvent( - events, - values[axisIx], - denominators[axisIx], - tolerances[axisIx], - axisBindings[axisIx].positive, - axisBindings[axisIx].negative); - } - } - - /// @brief Append a local-space translation delta as semantic move events. - static inline void appendLocalTranslationEvents( - std::vector& events, - const hlsl::float64_t3& localDelta, - const hlsl::float64_t3& denominators = hlsl::float64_t3(1.0), - const hlsl::float64_t3& tolerances = hlsl::float64_t3(SCameraToolingThresholds::TinyScalarEpsilon)) - { - appendScaledVirtualAxisEvents( - events, - localDelta, - denominators, - tolerances, - SCameraVirtualEventBindings::LocalTranslation); - } - - /// @brief Reinterpret a world-space translation delta in the local frame of a camera orientation. - static inline void appendWorldTranslationAsLocalEvents( - std::vector& events, - const hlsl::camera_quaternion_t& orientation, - const hlsl::float64_t3& worldDelta, - const hlsl::float64_t3& denominators = hlsl::float64_t3(1.0), - const hlsl::float64_t3& tolerances = hlsl::float64_t3(SCameraToolingThresholds::TinyScalarEpsilon)) - { - appendLocalTranslationEvents( - events, - hlsl::CCameraMathUtilities::projectWorldVectorToLocalQuaternionFrame(orientation, worldDelta), - denominators, - tolerances); - } - - /// @brief Append one 3-axis angular delta through a caller-provided binding table. - static inline void appendAngularAxisEvents( - std::vector& events, - const hlsl::float64_t3& deltaRadians, - const hlsl::float64_t3& denominators, - const hlsl::float64_t3& toleranceDeg, - const std::array& axisBindings) - { - for (size_t axisIx = 0u; axisIx < axisBindings.size(); ++axisIx) - { - appendAngularDeltaEvent( - events, - deltaRadians[axisIx], - denominators[axisIx], - toleranceDeg[axisIx], - axisBindings[axisIx].positive, - axisBindings[axisIx].negative); - } - } - - /// @brief Accumulate only translation-related virtual events back into a signed delta vector. - static inline hlsl::float64_t3 collectSignedTranslationDelta(std::span events) - { - hlsl::float64_t3 delta = hlsl::float64_t3(0.0); - for (const auto& ev : events) - { - switch (ev.type) - { - case CVirtualGimbalEvent::MoveRight: delta.x += ev.magnitude; break; - case CVirtualGimbalEvent::MoveLeft: delta.x -= ev.magnitude; break; - case CVirtualGimbalEvent::MoveUp: delta.y += ev.magnitude; break; - case CVirtualGimbalEvent::MoveDown: delta.y -= ev.magnitude; break; - case CVirtualGimbalEvent::MoveForward: delta.z += ev.magnitude; break; - case CVirtualGimbalEvent::MoveBackward: delta.z -= ev.magnitude; break; - default: break; - } - } - return delta; - } -}; - -} // namespace nbl::core - -#endif // _C_CAMERA_VIRTUAL_EVENT_UTILITIES_HPP_ - diff --git a/common/include/camera/CChaseCamera.hpp b/common/include/camera/CChaseCamera.hpp deleted file mode 100644 index 36d269e07..000000000 --- a/common/include/camera/CChaseCamera.hpp +++ /dev/null @@ -1,89 +0,0 @@ -#ifndef _C_CHASE_CAMERA_HPP_ -#define _C_CHASE_CAMERA_HPP_ - -#include -#include - -#include "CSphericalTargetCamera.hpp" - -namespace nbl::core -{ - -/// @brief Target-relative camera with planar target translation on the ground plane. -/// -/// Translation is resolved in a planar forward/right frame derived from the -/// current orbit basis. Rotation updates orbit yaw and pitch. Distance remains -/// clamped to the chase-camera limits. -class CChaseCamera final : public CSphericalTargetCamera -{ -public: - using base_t = CSphericalTargetCamera; - - CChaseCamera(const hlsl::float64_t3& position, const hlsl::float64_t3& target) - : base_t(position, target) - { - m_orbitUv.y = std::clamp(m_orbitUv.y, MinPitch, MaxPitch); - applyPose(); - } - ~CChaseCamera() = default; - - const typename base_t::CGimbal& getGimbal() override { return m_gimbal; } - - /// @brief Apply chase-style planar translation, pitch/yaw orbiting, and distance changes. - virtual bool manipulate(std::span virtualEvents, const hlsl::float64_t4x4* referenceFrame = nullptr) override - { - if (not virtualEvents.size() and not referenceFrame) - return false; - - if (referenceFrame) - { - CReferenceTransform reference = {}; - SCameraTargetRelativeState resolvedState = {}; - if (!tryExtractReferenceTransform(reference, referenceFrame) || - !tryResolveReferenceTargetRelativeState(reference, resolvedState)) - { - return false; - } - - resolvedState.orbitUv.y = std::clamp(resolvedState.orbitUv.y, MinPitch, MaxPitch); - adoptTargetRelativeState(resolvedState); - } - - const auto impulse = m_gimbal.accumulate(virtualEvents); - - const auto deltaRotation = scaleVirtualRotation(impulse.dVirtualRotation); - const auto deltaTranslation = scaleVirtualTranslation(impulse.dVirtualTranslate); - const auto deltaDistance = scaleUnscaledVirtualTranslation(impulse.dVirtualTranslate.y); - - const auto basis = computeBasis(m_orbitUv, m_distance); - - const auto planarForward = hlsl::CCameraMathUtilities::safeNormalizeVec3( - hlsl::float64_t3(basis.forward.x, 0.0, basis.forward.z), - hlsl::float64_t3(0.0, 0.0, 1.0)); - const auto planarRight = hlsl::CCameraMathUtilities::safeNormalizeVec3( - hlsl::float64_t3(basis.right.x, 0.0, basis.right.z), - hlsl::float64_t3(1.0, 0.0, 0.0)); - - m_targetPosition += planarRight * deltaTranslation.x + planarForward * deltaTranslation.z; - m_distance = std::clamp(m_distance + static_cast(deltaDistance), MinDistance, MaxDistance); - - m_orbitUv.x += deltaRotation.y; - m_orbitUv.y = std::clamp(m_orbitUv.y + deltaRotation.x, MinPitch, MaxPitch); - - return applyPose(); - } - - virtual uint32_t getAllowedVirtualEvents() const override { return AllowedVirtualEvents; } - virtual CameraKind getKind() const override { return CameraKind::Chase; } - /// @brief Return the stable user-facing identifier for this concrete camera kind. - virtual std::string_view getIdentifier() const override { return "Chase Camera"; } - -private: - static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate | CVirtualGimbalEvent::Rotate; - static inline constexpr double MaxPitch = SCameraTargetRelativeRigDefaults::ChaseMaxPitchRad; - static inline constexpr double MinPitch = SCameraTargetRelativeRigDefaults::ChaseMinPitchRad; -}; - -} - -#endif diff --git a/common/include/camera/CCubeProjection.hpp b/common/include/camera/CCubeProjection.hpp deleted file mode 100644 index 7017e49e0..000000000 --- a/common/include/camera/CCubeProjection.hpp +++ /dev/null @@ -1,97 +0,0 @@ -#ifndef _NBL_CCUBE_PROJECTION_HPP_ -#define _NBL_CCUBE_PROJECTION_HPP_ - -#include "IRange.hpp" -#include "IPerspectiveProjection.hpp" - -namespace nbl::core -{ - -/// @brief Projection where each cube face is a perspective quad. -/// -/// This represents a cube projection for a direction vector where each face of -/// the cube is treated as a quad. Projection onto the cube is done through -/// those quads, each with its own pre-transform and viewport linear matrix. -class CCubeProjection final : public IPerspectiveProjection, public IProjection -{ -public: - /// @brief Represents six face identifiers of a cube. - enum CubeFaces : uint8_t - { - /// @brief Cube face in the +X base direction - PositiveX = 0, - - /// @brief Cube face in the -X base direction - NegativeX, - - /// @brief Cube face in the +Y base direction - PositiveY, - - /// @brief Cube face in the -Y base direction - NegativeY, - - /// @brief Cube face in the +Z base direction - PositiveZ, - - /// @brief Cube face in the -Z base direction - NegativeZ, - - CubeFacesCount - }; - - inline static core::smart_refctd_ptr create(core::smart_refctd_ptr&& camera) - { - if (!camera) - return nullptr; - - return core::smart_refctd_ptr(new CCubeProjection(core::smart_refctd_ptr(camera)), core::dont_grab); - } - - virtual uint32_t getLinearProjectionCount() const override - { - return static_cast(m_quads.size()); - } - - virtual const ILinearProjection::CProjection& getLinearProjection(uint32_t index) const override - { - assert(index < m_quads.size()); - return m_quads[index]; - } - - void transformCube() - { - // Cube-face quad generation is not implemented yet. - } - - virtual ProjectionType getProjectionType() const override { return ProjectionType::Cube; } - - virtual void project(const projection_vector_t& vecToProjectionSpace, projection_vector_t& output) const override - { - auto direction = hlsl::normalize(vecToProjectionSpace); - - // Cube-face projection is not implemented yet. - } - - virtual bool unproject(const projection_vector_t& vecFromProjectionSpace, projection_vector_t& output) const override - { - // Reverse projection is not implemented yet. - } - - template - requires (FaceIx != CubeFacesCount) - inline const CProjection& getProjectionQuad() - { - return m_quads[FaceIx]; - } - -private: - CCubeProjection(core::smart_refctd_ptr&& camera) - : IPerspectiveProjection(core::smart_refctd_ptr(camera)) {} - virtual ~CCubeProjection() = default; - - std::array m_quads; -}; - -} // namespace nbl::core - -#endif // _NBL_CCUBE_PROJECTION_HPP_ diff --git a/common/include/camera/CDollyCamera.hpp b/common/include/camera/CDollyCamera.hpp deleted file mode 100644 index 043c65610..000000000 --- a/common/include/camera/CDollyCamera.hpp +++ /dev/null @@ -1,80 +0,0 @@ -#ifndef _C_DOLLY_CAMERA_HPP_ -#define _C_DOLLY_CAMERA_HPP_ - -#include -#include - -#include "CSphericalTargetCamera.hpp" - -namespace nbl::core -{ - -/// @brief Target-relative camera that translates the target in the full local camera basis. -/// -/// Translation uses the current right/up/forward basis. Rotation updates orbit -/// yaw and pitch while the camera pose is rebuilt from the maintained -/// target-relative offset. -class CDollyCamera final : public CSphericalTargetCamera -{ -public: - using base_t = CSphericalTargetCamera; - - CDollyCamera(const hlsl::float64_t3& position, const hlsl::float64_t3& target) - : base_t(position, target) - { - m_orbitUv.y = std::clamp(m_orbitUv.y, MinPitch, MaxPitch); - applyPose(); - } - ~CDollyCamera() = default; - - const typename base_t::CGimbal& getGimbal() override { return m_gimbal; } - - /// @brief Apply one frame of local-frame dolly translation plus orbit rotation. - virtual bool manipulate(std::span virtualEvents, const hlsl::float64_t4x4* referenceFrame = nullptr) override - { - if (not virtualEvents.size() and not referenceFrame) - return false; - - if (referenceFrame) - { - CReferenceTransform reference = {}; - SCameraTargetRelativeState resolvedState = {}; - if (!tryExtractReferenceTransform(reference, referenceFrame) || - !tryResolveReferenceTargetRelativeState(reference, resolvedState)) - { - return false; - } - - resolvedState.orbitUv.y = std::clamp(resolvedState.orbitUv.y, MinPitch, MaxPitch); - adoptTargetRelativeState(resolvedState); - } - - const auto impulse = m_gimbal.accumulate(virtualEvents); - - const auto deltaRotation = scaleVirtualRotation(impulse.dVirtualRotation); - - const auto deltaTranslation = scaleVirtualTranslation(impulse.dVirtualTranslate); - const auto basis = computeBasis(m_orbitUv, m_distance); - const auto delta = hlsl::CCameraMathUtilities::transformLocalVectorToWorldBasis(deltaTranslation, basis.right, basis.up, basis.forward); - - m_targetPosition += delta; - m_orbitUv.x += deltaRotation.y; - m_orbitUv.y = std::clamp(m_orbitUv.y + deltaRotation.x, MinPitch, MaxPitch); - - return applyPose(); - } - - virtual uint32_t getAllowedVirtualEvents() const override { return AllowedVirtualEvents; } - virtual CameraKind getKind() const override { return CameraKind::Dolly; } - /// @brief Return the stable user-facing identifier for this concrete camera kind. - virtual std::string_view getIdentifier() const override { return "Dolly Camera"; } - -private: - static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate | CVirtualGimbalEvent::Rotate; - static inline constexpr double MaxPitch = SCameraTargetRelativeRigDefaults::DollyPitchLimitRad; - static inline constexpr double MinPitch = -MaxPitch; -}; - -} - -#endif diff --git a/common/include/camera/CDollyZoomCamera.hpp b/common/include/camera/CDollyZoomCamera.hpp deleted file mode 100644 index 86e16a3de..000000000 --- a/common/include/camera/CDollyZoomCamera.hpp +++ /dev/null @@ -1,122 +0,0 @@ -#ifndef _C_DOLLY_ZOOM_CAMERA_HPP_ -#define _C_DOLLY_ZOOM_CAMERA_HPP_ - -#include -#include - -#include "CSphericalTargetCamera.hpp" - -namespace nbl::core -{ - -/// @brief Target-relative camera that preserves subject framing by coupling distance with a derived perspective FOV. -/// -/// The rig reuses spherical target-relative manipulation but exposes an additional -/// dynamic-perspective state describing the authored base FOV and the reference -/// distance used to compute the current dolly-zoom FOV. -class CDollyZoomCamera final : public CSphericalTargetCamera -{ -public: - using base_t = CSphericalTargetCamera; - - CDollyZoomCamera(const hlsl::float64_t3& position, const hlsl::float64_t3& target, float baseFov = DefaultBaseFovDeg) - : base_t(position, target), m_baseFov(baseFov), m_referenceDistance(m_distance) - { - applyPose(); - } - ~CDollyZoomCamera() = default; - - const typename base_t::CGimbal& getGimbal() override { return m_gimbal; } - - /// @brief Return the authored FOV used as the reference value for dolly-zoom evaluation. - float getBaseFov() const { return m_baseFov; } - /// @brief Update the authored reference FOV used for dolly-zoom evaluation. - void setBaseFov(float fov) { m_baseFov = fov; } - - /// @brief Return the reference distance that preserves the authored framing. - float getReferenceDistance() const { return m_referenceDistance; } - /// @brief Update the reference distance used by dolly-zoom FOV evaluation. - void setReferenceDistance(float distance) { m_referenceDistance = distance; } - - /// @brief Evaluate the effective perspective FOV required to preserve subject framing at the current distance. - float computeDollyFov() const - { - const double base = std::tan(hlsl::radians(static_cast(m_baseFov)) * 0.5); - const double ratio = static_cast(m_referenceDistance) / std::max(static_cast(m_distance), static_cast(MinDistance)); - const double fov = 2.0 * std::atan(base * ratio); - const double fovDeg = hlsl::degrees(fov); - return static_cast(std::clamp(fovDeg, static_cast(MinDynamicFovDeg), static_cast(MaxDynamicFovDeg))); - } - - /// @brief Apply one frame of orbit translation and distance input for the dolly-zoom rig. - virtual bool manipulate(std::span virtualEvents, const hlsl::float64_t4x4* referenceFrame = nullptr) override - { - if (not virtualEvents.size() and not referenceFrame) - return false; - - if (referenceFrame) - { - CReferenceTransform reference = {}; - SCameraTargetRelativeState resolvedState = {}; - if (!tryExtractReferenceTransform(reference, referenceFrame) || - !tryResolveReferenceTargetRelativeState(reference, resolvedState)) - { - return false; - } - - adoptTargetRelativeState(resolvedState); - } - - const auto impulse = m_gimbal.accumulate(virtualEvents); - const auto deltaTranslation = scaleVirtualTranslation(impulse.dVirtualTranslate); - const double deltaDistance = scaleUnscaledVirtualTranslation(impulse.dVirtualTranslate.z); - - m_orbitUv += hlsl::float64_t2(deltaTranslation.y, deltaTranslation.x); - m_distance = std::clamp(m_distance + static_cast(deltaDistance), MinDistance, MaxDistance); - - return applyPose(); - } - - virtual uint32_t getAllowedVirtualEvents() const override { return AllowedVirtualEvents; } - virtual CameraKind getKind() const override { return CameraKind::DollyZoom; } - virtual uint32_t getCapabilities() const override { return base_t::getCapabilities() | base_t::DynamicPerspectiveFov; } - /// @brief Query the current derived FOV produced by the dolly-zoom state. - virtual bool tryGetDynamicPerspectiveFov(float& outFov) const override - { - outFov = computeDollyFov(); - return true; - } - /// @brief Query the authored dolly-zoom state used to derive the current dynamic FOV. - virtual bool tryGetDynamicPerspectiveState(DynamicPerspectiveState& out) const override - { - out.baseFov = m_baseFov; - out.referenceDistance = m_referenceDistance; - return true; - } - /// @brief Replace the authored dolly-zoom state after validating both scalars. - virtual bool trySetDynamicPerspectiveState(const DynamicPerspectiveState& state) override - { - if (!hlsl::CCameraMathUtilities::isFiniteScalar(state.baseFov) || !hlsl::CCameraMathUtilities::isFiniteScalar(state.referenceDistance) || state.referenceDistance <= 0.f) - return false; - - m_baseFov = state.baseFov; - m_referenceDistance = state.referenceDistance; - return true; - } - /// @brief Return the stable user-facing identifier for this concrete camera kind. - virtual std::string_view getIdentifier() const override { return "Dolly Zoom Camera"; } - -private: - static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate; - static inline constexpr float DefaultBaseFovDeg = 40.0f; - static inline constexpr float MinDynamicFovDeg = 10.0f; - static inline constexpr float MaxDynamicFovDeg = 150.0f; - - float m_baseFov = DefaultBaseFovDeg; - float m_referenceDistance = 1.0f; -}; - -} - -#endif - diff --git a/common/include/camera/CFPSCamera.hpp b/common/include/camera/CFPSCamera.hpp deleted file mode 100644 index af8e7b85b..000000000 --- a/common/include/camera/CFPSCamera.hpp +++ /dev/null @@ -1,125 +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 _C_FPS_CAMERA_HPP_ -#define _C_FPS_CAMERA_HPP_ - -#include - -#include "ICamera.hpp" - -namespace nbl::core -{ - -/// @brief Free-position camera with world-space translation and yaw/pitch rotation. -/// -/// The runtime state consists of position plus an upright orientation derived -/// from yaw and pitch. Reference-frame application rejects arbitrary roll and -/// rebuilds the legal FPS orientation from the extracted forward axis. -class CFPSCamera final : public ICamera -{ -public: - using base_t = ICamera; - struct SFpsCameraDefaults final - { - static inline constexpr float RollValidationEpsilonDeg = 1.e-4f; - static inline constexpr float StraightRollDeg = 0.0f; - static inline constexpr float InvertedRollDeg = 180.0f; - }; - - CFPSCamera(const hlsl::float64_t3& position, const hlsl::camera_quaternion_t& orientation = hlsl::CCameraMathUtilities::makeIdentityQuaternion()) - : base_t(), m_gimbal({ .position = position, .orientation = orientation }) - { - m_gimbal.begin(); - { - const auto pitchYaw = hlsl::CCameraMathUtilities::getPitchYawFromForwardVector(m_gimbal.getZAxis()); - m_gimbal.setOrientation(hlsl::CCameraMathUtilities::makeQuaternionFromEulerRadiansYXZ(hlsl::float64_t3(pitchYaw.x, pitchYaw.y, 0.0))); - } - m_gimbal.end(); - } - ~CFPSCamera() = default; - - const typename base_t::CGimbal& getGimbal() override - { - return m_gimbal; - } - - virtual bool manipulate(std::span virtualEvents, const hlsl::float64_t4x4* referenceFrame = nullptr) override - { - if (not virtualEvents.size() and not referenceFrame) - return false; - - CReferenceTransform reference; - if (not m_gimbal.extractReferenceTransform(&reference, referenceFrame)) - return false; - - auto validateReference = [&]() - { - if (referenceFrame) - { - const float roll = static_cast(hlsl::degrees(hlsl::CCameraMathUtilities::getQuaternionEulerRadiansYXZ(reference.orientation).z)); - const bool matchesStraightRoll = - hlsl::CCameraMathUtilities::getWrappedAngleDistanceDegrees(roll, SFpsCameraDefaults::StraightRollDeg) <= SFpsCameraDefaults::RollValidationEpsilonDeg; - const bool matchesInvertedRoll = - hlsl::CCameraMathUtilities::getWrappedAngleDistanceDegrees(roll, SFpsCameraDefaults::InvertedRollDeg) <= SFpsCameraDefaults::RollValidationEpsilonDeg; - - if (!(matchesStraightRoll || matchesInvertedRoll)) - return false; - } - - return true; - }; - - auto impulse = m_gimbal.accumulate(virtualEvents); - - bool manipulated = true; - - m_gimbal.begin(); - { - const auto pitchYaw = hlsl::CCameraMathUtilities::getPitchYawFromForwardVector(hlsl::float64_t3(reference.frame[2])); - const float newPitch = std::clamp(static_cast(pitchYaw.x + scaleVirtualRotation(impulse.dVirtualRotation.x)), MinVerticalAngle, MaxVerticalAngle); - const float newYaw = static_cast(pitchYaw.y + scaleVirtualRotation(impulse.dVirtualRotation.y)); - - if (validateReference()) - m_gimbal.setOrientation(hlsl::CCameraMathUtilities::makeQuaternionFromEulerRadiansYXZ(hlsl::float64_t3(newPitch, newYaw, 0.0f))); - m_gimbal.setPosition(hlsl::float64_t3(reference.frame[3]) + hlsl::CCameraMathUtilities::rotateVectorByQuaternion(reference.orientation, hlsl::float64_t3(impulse.dVirtualTranslate))); - } - m_gimbal.end(); - - manipulated &= bool(m_gimbal.getManipulationCounter()); - - if (manipulated) - m_gimbal.updateView(); - - return manipulated; - } - - virtual uint32_t getAllowedVirtualEvents() const override - { - return AllowedVirtualEvents; - } - - virtual CameraKind getKind() const override - { - return CameraKind::FPS; - } - - virtual std::string_view getIdentifier() const override - { - return "FPS Camera"; - } - -private: - - typename base_t::CGimbal m_gimbal; - - static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate | CVirtualGimbalEvent::Rotate; - static inline constexpr float MaxVerticalAngle = static_cast(hlsl::SCameraViewRigDefaults::FpsVerticalPitchLimitRad); - static inline constexpr float MinVerticalAngle = -MaxVerticalAngle; -}; - -} - -#endif // _C_FPS_CAMERA_HPP_ - diff --git a/common/include/camera/CFreeLockCamera.hpp b/common/include/camera/CFreeLockCamera.hpp deleted file mode 100644 index fc0b1e67b..000000000 --- a/common/include/camera/CFreeLockCamera.hpp +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright (C) 2018-2024 - 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_FREE_CAMERA_HPP_ -#define _C_FREE_CAMERA_HPP_ - -#include "ICamera.hpp" - -namespace nbl::core -{ - -/// @brief Free-position camera that allows full yaw/pitch/roll rotation. -class CFreeCamera final : public ICamera -{ -public: - using base_t = ICamera; - - CFreeCamera(const hlsl::float64_t3& position, const hlsl::camera_quaternion_t& orientation = hlsl::CCameraMathUtilities::makeIdentityQuaternion()) - : base_t(), m_gimbal({ .position = position, .orientation = orientation }) {} - ~CFreeCamera() = default; - - const typename base_t::CGimbal& getGimbal() override - { - return m_gimbal; - } - - virtual bool manipulate(std::span virtualEvents, const hlsl::float64_t4x4* referenceFrame = nullptr) override - { - if (not virtualEvents.size() and not referenceFrame) - return false; - - CReferenceTransform reference; - if (not m_gimbal.extractReferenceTransform(&reference, referenceFrame)) - return false; - - auto impulse = m_gimbal.accumulate(virtualEvents); - - bool manipulated = true; - - m_gimbal.begin(); - { - const auto pitch = hlsl::CCameraMathUtilities::makeQuaternionFromAxisAngle(hlsl::normalize(hlsl::float64_t3(reference.frame[0])), impulse.dVirtualRotation.x); - const auto yaw = hlsl::CCameraMathUtilities::makeQuaternionFromAxisAngle(hlsl::normalize(hlsl::float64_t3(reference.frame[1])), impulse.dVirtualRotation.y); - const auto roll = hlsl::CCameraMathUtilities::makeQuaternionFromAxisAngle(hlsl::normalize(hlsl::float64_t3(reference.frame[2])), impulse.dVirtualRotation.z); - - m_gimbal.setOrientation(hlsl::CCameraMathUtilities::normalizeQuaternion(yaw * pitch * roll * reference.orientation)); - m_gimbal.setPosition(hlsl::float64_t3(reference.frame[3]) + hlsl::CCameraMathUtilities::rotateVectorByQuaternion(reference.orientation, hlsl::float64_t3(impulse.dVirtualTranslate))); - } - m_gimbal.end(); - - manipulated &= bool(m_gimbal.getManipulationCounter()); - - if (manipulated) - m_gimbal.updateView(); - - return manipulated; - } - - virtual uint32_t getAllowedVirtualEvents() const override - { - return AllowedVirtualEvents; - } - - virtual CameraKind getKind() const override - { - return CameraKind::Free; - } - - virtual std::string_view getIdentifier() const override - { - return "Free-Look Camera"; - } - -private: - typename base_t::CGimbal m_gimbal; - - static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate | CVirtualGimbalEvent::Rotate; -}; - -} - -#endif // _C_FREE_CAMERA_HPP_ diff --git a/common/include/camera/CGeneralPurposeGimbal.hpp b/common/include/camera/CGeneralPurposeGimbal.hpp deleted file mode 100644 index d0f7c72b9..000000000 --- a/common/include/camera/CGeneralPurposeGimbal.hpp +++ /dev/null @@ -1,24 +0,0 @@ -#ifndef _NBL_CGENERAL_PURPOSE_GIMBAL_HPP_ -#define _NBL_CGENERAL_PURPOSE_GIMBAL_HPP_ - -#include "IGimbal.hpp" - -namespace nbl::core -{ - /// @brief Minimal concrete gimbal wrapper for code that only needs the generic `IGimbal` behavior. - /// - /// The class exists mainly as a convenient instantiable type when no additional - /// camera-specific state or manipulation policy is required on top of `IGimbal`. - template - class CGeneralPurposeGimbal : public IGimbal - { - public: - using base_t = IGimbal; - - /// @brief Construct the gimbal from an initial world-space pose. - CGeneralPurposeGimbal(typename base_t::SCreationParameters&& parameters) : base_t(std::move(parameters)) {} - ~CGeneralPurposeGimbal() = default; - }; -} - -#endif // _NBL_CGENERAL_PURPOSE_GIMBAL_HPP_ diff --git a/common/include/camera/CGimbalInputBinder.hpp b/common/include/camera/CGimbalInputBinder.hpp deleted file mode 100644 index 785bb0269..000000000 --- a/common/include/camera/CGimbalInputBinder.hpp +++ /dev/null @@ -1,131 +0,0 @@ -#ifndef _NBL_C_GIMBAL_INPUT_BINDER_HPP_ -#define _NBL_C_GIMBAL_INPUT_BINDER_HPP_ - -#include - -#include "IGimbalInputProcessor.hpp" - -namespace nbl::ui -{ - -/// @brief High-level runtime binder for consumers and viewport glue. -/// -/// It owns active runtime mappings and collects one frame of virtual events -/// from raw keyboard, mouse, and ImGuizmo input. -class CGimbalInputBinder final : public IGimbalInputProcessor -{ -public: - using base_t = IGimbalInputProcessor; - using base_t::base_t; - using input_keyboard_event_t = base_t::input_keyboard_event_t; - using input_mouse_event_t = base_t::input_mouse_event_t; - using input_imguizmo_event_t = base_t::input_imguizmo_event_t; - - struct SCollectedVirtualEvents - { - /// @brief Concatenated output buffer plus per-domain counts for diagnostics. - std::vector events; - uint32_t keyboardCount = 0u; - uint32_t mouseCount = 0u; - uint32_t imguizmoCount = 0u; - - inline uint32_t totalCount() const - { - return keyboardCount + mouseCount + imguizmoCount; - } - }; - - /// @brief Translate one frame of external keyboard, mouse, and ImGuizmo input into virtual events. - inline void clearActiveBindings() - { - updateKeyboardMapping([](auto& map) { map.clear(); }); - updateMouseMapping([](auto& map) { map.clear(); }); - updateImguizmoMapping([](auto& map) { map.clear(); }); - } - - inline void clearBindingLayout() - { - clearActiveBindings(); - } - - inline void copyActiveBindingsFromLayout(const IGimbalBindingLayout& layout) - { - updateKeyboardMapping([&](auto& map) { map = sanitizeMapping(layout.getKeyboardVirtualEventMap()); }); - updateMouseMapping([&](auto& map) { map = sanitizeMapping(layout.getMouseVirtualEventMap()); }); - updateImguizmoMapping([&](auto& map) { map = sanitizeMapping(layout.getImguizmoVirtualEventMap()); }); - } - - inline void copyBindingLayoutFrom(const IGimbalBindingLayout& layout) - { - copyActiveBindingsFromLayout(layout); - } - - inline void copyActiveBindingsToLayout(IGimbalBindingLayout& layout) const - { - layout.updateKeyboardMapping([&](auto& map) { map = sanitizeMapping(getKeyboardVirtualEventMap()); }); - layout.updateMouseMapping([&](auto& map) { map = sanitizeMapping(getMouseVirtualEventMap()); }); - layout.updateImguizmoMapping([&](auto& map) { map = sanitizeMapping(getImguizmoVirtualEventMap()); }); - } - - inline void copyBindingLayoutTo(IGimbalBindingLayout& layout) const - { - copyActiveBindingsToLayout(layout); - } - - inline SCollectedVirtualEvents collectVirtualEvents( - const std::chrono::microseconds nextPresentationTimeStamp, - const SUpdateParameters parameters = {}) - { - beginInputProcessing(nextPresentationTimeStamp); - - SCollectedVirtualEvents output; - uint32_t keyboardPotentialCount = 0u; - uint32_t mousePotentialCount = 0u; - uint32_t imguizmoPotentialCount = 0u; - - processKeyboard(nullptr, keyboardPotentialCount, {}); - processMouse(nullptr, mousePotentialCount, {}); - processImguizmo(nullptr, imguizmoPotentialCount, {}); - - output.events.resize(keyboardPotentialCount + mousePotentialCount + imguizmoPotentialCount); - auto* dst = output.events.data(); - - if (keyboardPotentialCount) - { - output.keyboardCount = keyboardPotentialCount; - processKeyboard(dst, output.keyboardCount, parameters.keyboardEvents); - dst += output.keyboardCount; - } - - if (mousePotentialCount) - { - output.mouseCount = mousePotentialCount; - processMouse(dst, output.mouseCount, parameters.mouseEvents); - dst += output.mouseCount; - } - - if (imguizmoPotentialCount) - { - output.imguizmoCount = imguizmoPotentialCount; - processImguizmo(dst, output.imguizmoCount, parameters.imguizmoEvents); - } - - endInputProcessing(); - output.events.resize(output.totalCount()); - return output; - } - -private: - template - inline static Map sanitizeMapping(const Map& source) - { - Map result; - for (const auto& [code, hash] : source) - result.emplace(code, typename Map::mapped_type(hash.event.type, hash.magnitudeScale)); - return result; - } -}; - -} // namespace nbl::ui - -#endif // _NBL_C_GIMBAL_INPUT_BINDER_HPP_ diff --git a/common/include/camera/CIsometricCamera.hpp b/common/include/camera/CIsometricCamera.hpp deleted file mode 100644 index 3ac1d41e8..000000000 --- a/common/include/camera/CIsometricCamera.hpp +++ /dev/null @@ -1,77 +0,0 @@ -#ifndef _C_ISOMETRIC_CAMERA_HPP_ -#define _C_ISOMETRIC_CAMERA_HPP_ - -#include -#include - -#include "CSphericalTargetCamera.hpp" - -namespace nbl::core -{ - -/// @brief Target-relative camera locked to the shared isometric yaw and pitch. -/// -/// Translation moves the tracked target in the current view plane while the -/// authored isometric orientation stays fixed. Distance changes are still allowed. -class CIsometricCamera final : public CSphericalTargetCamera -{ -public: - using base_t = CSphericalTargetCamera; - - CIsometricCamera(const hlsl::float64_t3& position, const hlsl::float64_t3& target) - : base_t(position, target) - { - m_orbitUv = hlsl::float64_t2(IsoYaw, IsoPitch); - applyPose(); - } - ~CIsometricCamera() = default; - - const typename base_t::CGimbal& getGimbal() override { return m_gimbal; } - - /// @brief Apply one frame of planar target translation and distance changes while preserving the fixed isometric angles. - virtual bool manipulate(std::span virtualEvents, const hlsl::float64_t4x4* referenceFrame = nullptr) override - { - if (not virtualEvents.size() and not referenceFrame) - return false; - - if (referenceFrame) - { - CReferenceTransform reference = {}; - SCameraTargetRelativeState resolvedState = {}; - if (!tryExtractReferenceTransform(reference, referenceFrame) || - !tryResolveReferenceIsometricState(reference, resolvedState)) - { - return false; - } - - adoptTargetRelativeState(resolvedState); - } - - const auto impulse = m_gimbal.accumulate(virtualEvents); - - const auto deltaTranslation = scaleVirtualTranslation(impulse.dVirtualTranslate); - const double deltaDistance = scaleUnscaledVirtualTranslation(impulse.dVirtualTranslate.z); - - m_orbitUv = hlsl::float64_t2(IsoYaw, IsoPitch); - m_distance = std::clamp(m_distance + static_cast(deltaDistance), MinDistance, MaxDistance); - - const auto basis = computeBasis(m_orbitUv, m_distance); - applyPlanarTargetTranslation(deltaTranslation, basis); - - return applyPose(); - } - - virtual uint32_t getAllowedVirtualEvents() const override { return AllowedVirtualEvents; } - virtual CameraKind getKind() const override { return CameraKind::Isometric; } - /// @brief Return the stable user-facing identifier for this concrete camera kind. - virtual std::string_view getIdentifier() const override { return "Isometric Camera"; } - -private: - static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate; - static inline constexpr double IsoYaw = SCameraTargetRelativeRigDefaults::IsometricYawRad; - static inline constexpr double IsoPitch = SCameraTargetRelativeRigDefaults::IsometricPitchRad; -}; - -} - -#endif diff --git a/common/include/camera/CLinearProjection.hpp b/common/include/camera/CLinearProjection.hpp deleted file mode 100644 index d6e0dd4e0..000000000 --- a/common/include/camera/CLinearProjection.hpp +++ /dev/null @@ -1,59 +0,0 @@ -#ifndef _NBL_C_LINEAR_PROJECTION_HPP_ -#define _NBL_C_LINEAR_PROJECTION_HPP_ - -#include "ILinearProjection.hpp" -#include "IRange.hpp" - -namespace nbl::core -{ - /// @brief Range-backed concrete implementation of `ILinearProjection`. - /// - /// The template owns a caller-selected contiguous container of linear projection - /// entries and exposes it through the generic `ILinearProjection` interface. - template ProjectionsRange> - class CLinearProjection : public ILinearProjection - { - public: - using ILinearProjection::ILinearProjection; - - CLinearProjection() = default; - - /// @brief Create a projection wrapper only when a valid camera instance is available. - inline static core::smart_refctd_ptr create(core::smart_refctd_ptr&& camera) - { - if (!camera) - return nullptr; - - return core::smart_refctd_ptr(new CLinearProjection(core::smart_refctd_ptr(camera)), core::dont_grab); - } - - /// @brief Return the number of stored linear projection entries. - virtual uint32_t getLinearProjectionCount() const override - { - return static_cast(m_projections.size()); - } - - /// @brief Return one stored projection entry by index. - virtual const CProjection& getLinearProjection(uint32_t index) const override - { - assert(index < m_projections.size()); - return m_projections[index]; - } - - /// @brief Expose mutable access to the owned projection range. - inline std::span getLinearProjections() - { - return std::span(m_projections.data(), m_projections.size()); - } - - private: - CLinearProjection(core::smart_refctd_ptr&& camera) - : ILinearProjection(core::smart_refctd_ptr(camera)) {} - virtual ~CLinearProjection() = default; - - ProjectionsRange m_projections; - }; - -} // nbl::hlsl namespace - -#endif // _NBL_C_LINEAR_PROJECTION_HPP_ diff --git a/common/include/camera/COrbitCamera.hpp b/common/include/camera/COrbitCamera.hpp deleted file mode 100644 index df411c4b8..000000000 --- a/common/include/camera/COrbitCamera.hpp +++ /dev/null @@ -1,83 +0,0 @@ -#ifndef _C_ORBIT_CAMERA_HPP_ -#define _C_ORBIT_CAMERA_HPP_ - -#include -#include -#include "CSphericalTargetCamera.hpp" - -namespace nbl::core -{ - -/// @brief Target-relative camera with state `(target, orbitUv, distance)`. -/// -/// Runtime input updates only orbit yaw, orbit pitch, and camera distance. -/// The target position remains unchanged during `manipulate(...)`. -class COrbitCamera final : public CSphericalTargetCamera -{ -public: - using base_t = CSphericalTargetCamera; - - COrbitCamera(const hlsl::float64_t3& position, const hlsl::float64_t3& target) - : base_t(position, target) - { - m_distance = std::clamp(hlsl::length(m_targetPosition - position), MinDistance, MaxDistance); - applyPose(); - } - ~COrbitCamera() = default; - - const typename base_t::CGimbal& getGimbal() override { return m_gimbal; } - - /// @brief Apply one frame of orbit-angle and distance input around the current target. - virtual bool manipulate(std::span virtualEvents, const hlsl::float64_t4x4* referenceFrame = nullptr) override - { - if (virtualEvents.empty() && !referenceFrame) - return false; - - if (referenceFrame) - { - CReferenceTransform reference = {}; - SCameraTargetRelativeState resolvedState = {}; - if (!tryExtractReferenceTransform(reference, referenceFrame) || - !tryResolveReferenceTargetRelativeState(reference, resolvedState)) - { - return false; - } - - adoptTargetRelativeState(resolvedState); - } - - const auto impulse = m_gimbal.accumulate(virtualEvents); - const auto deltaTranslation = scaleVirtualTranslation(impulse.dVirtualTranslate); - const double deltaDistance = scaleUnscaledVirtualTranslation(impulse.dVirtualTranslate.z); - - m_orbitUv += hlsl::float64_t2(deltaTranslation.y, deltaTranslation.x); - - m_distance = std::clamp(m_distance + static_cast(deltaDistance), MinDistance, MaxDistance); - - return applyPose(); - } - - virtual uint32_t getAllowedVirtualEvents() const override - { - return AllowedVirtualEvents; - } - - virtual CameraKind getKind() const override - { - return CameraKind::Orbit; - } - - virtual std::string_view getIdentifier() const override - { - return "Orbit Camera"; - } - - static inline constexpr float MinDistance = base_t::MinDistance; - static inline constexpr float MaxDistance = base_t::MaxDistance; - - static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate; -}; - -} - -#endif // _C_ORBIT_CAMERA_HPP_ diff --git a/common/include/camera/CPathCamera.hpp b/common/include/camera/CPathCamera.hpp deleted file mode 100644 index aa8b33558..000000000 --- a/common/include/camera/CPathCamera.hpp +++ /dev/null @@ -1,342 +0,0 @@ -#ifndef _C_PATH_CAMERA_HPP_ -#define _C_PATH_CAMERA_HPP_ - -#include -#include - -#include "CCameraPathUtilities.hpp" -#include "CSphericalTargetCamera.hpp" - -namespace nbl::core -{ - -/// @brief Path-rig camera driven by typed `PathState` plus an injected path model. -/// -/// The public runtime path stays event-only through `manipulate(...)`. -/// `CPathCamera` only interprets the accumulated impulse through `m_pathModel` -/// instead of hardcoding one default target-relative mapping in the method body. -class CPathCamera final : public CSphericalTargetCamera -{ -public: - using base_t = CSphericalTargetCamera; - using path_model_t = SCameraPathModel; - using path_limits_t = PathStateLimits; - - /// @brief Construct the path rig with the shared default path model and default limits. - CPathCamera(const hlsl::float64_t3& position, const hlsl::float64_t3& target) - : CPathCamera(position, target, CCameraPathUtilities::makeDefaultPathModel(), CCameraPathUtilities::makeDefaultPathLimits()) - { - } - - /// @brief Construct the path rig with a caller-provided model and default limits. - CPathCamera(const hlsl::float64_t3& position, const hlsl::float64_t3& target, path_model_t pathModel) - : CPathCamera(position, target, std::move(pathModel), CCameraPathUtilities::makeDefaultPathLimits()) - { - } - - /// @brief Construct the path rig with the shared default model and caller-provided limits. - CPathCamera(const hlsl::float64_t3& position, const hlsl::float64_t3& target, path_limits_t pathLimits) - : CPathCamera(position, target, CCameraPathUtilities::makeDefaultPathModel(), pathLimits) - { - } - - /// @brief Construct the path rig with fully caller-provided model and path-state limits. - CPathCamera(const hlsl::float64_t3& position, const hlsl::float64_t3& target, path_model_t pathModel, path_limits_t pathLimits) - : base_t(position, target) - { - initializePathRig(position, std::move(pathModel), pathLimits); - } - - ~CPathCamera() = default; - - const typename base_t::CGimbal& getGimbal() override { return m_gimbal; } - - /// @brief Consume virtual events through the active path model and update the runtime pose from the resulting path state. - virtual bool manipulate(std::span virtualEvents, const hlsl::float64_t4x4* referenceFrame = nullptr) override - { - if (virtualEvents.empty() && !referenceFrame) - return false; - - PathState nextPathState = m_pathState; - CReferenceTransform reference = {}; - const CReferenceTransform* resolvedReference = nullptr; - if (referenceFrame) - { - if (!m_gimbal.extractReferenceTransform(&reference, referenceFrame)) - return false; - resolvedReference = &reference; - if (!m_pathModel.resolveState || - !m_pathModel.resolveState( - m_targetPosition, - hlsl::float64_t3(reference.frame[3]), - m_pathLimits, - nullptr, - nextPathState)) - { - return false; - } - } - - const auto impulse = m_gimbal.accumulate(virtualEvents); - const SCameraPathControlContext context = { - .currentState = nextPathState, - .translation = scaleVirtualTranslation(impulse.dVirtualTranslate), - .rotation = scaleVirtualRotation(impulse.dVirtualRotation), - .targetPosition = m_targetPosition, - .reference = resolvedReference, - .limits = m_pathLimits - }; - - if (!m_pathModel.controlLaw || !m_pathModel.integrate) - return false; - - const auto stateDelta = m_pathModel.controlLaw(context); - if (!m_pathModel.integrate(nextPathState, stateDelta, m_pathLimits, nextPathState)) - return false; - - const auto previousPathState = m_pathState; - m_pathState = nextPathState; - bool manipulated = false; - if (refreshFromPathState(&manipulated)) - return manipulated; - - m_pathState = previousPathState; - refreshFromPathState(); - return false; - } - - virtual uint32_t getAllowedVirtualEvents() const override { return AllowedVirtualEvents; } - virtual CameraKind getKind() const override { return CameraKind::Path; } - virtual uint32_t getGoalStateMask() const override { return base_t::getGoalStateMask() | base_t::GoalStatePath; } - - /// @brief Query the current typed path state. - virtual bool tryGetPathState(PathState& out) const override - { - out = m_pathState; - return true; - } - - /// @brief Query the active path-state limits used by this camera instance. - virtual bool tryGetPathStateLimits(PathStateLimits& out) const override - { - out = m_pathLimits; - return true; - } - - /// @brief Query the derived spherical-target state corresponding to the current path-state evaluation. - virtual bool tryGetSphericalTargetState(typename base_t::SphericalTargetState& out) const override - { - out.target = m_targetPosition; - out.distance = m_distance; - out.orbitUv = m_orbitUv; - out.minDistance = static_cast(m_pathLimits.minDistance); - out.maxDistance = static_cast(m_pathLimits.maxDistance); - return true; - } - - /// @brief Replace only the tracked target position and rebuild the current path pose against it. - virtual bool trySetSphericalTarget(const hlsl::float64_t3& targetPosition) override - { - if (m_targetPosition == targetPosition) - return true; - - const auto previousTarget = m_targetPosition; - m_targetPosition = targetPosition; - if (refreshFromPathState()) - return true; - - m_targetPosition = previousTarget; - refreshFromPathState(); - return false; - } - - /// @brief Replace the current path state after sanitizing it through the active path model. - virtual bool trySetPathState(const PathState& state) override - { - if (!m_pathModel.resolveState) - return false; - - PathState sanitized = {}; - if (!m_pathModel.resolveState(m_targetPosition, m_gimbal.getPosition(), m_pathLimits, &state, sanitized)) - return false; - - const bool exact = CCameraPathUtilities::pathStatesNearlyEqual(sanitized, state, SCameraPathDefaults::ExactComparisonThresholds); - const auto previousState = m_pathState; - m_pathState = sanitized; - if (refreshFromPathState()) - return exact; - - m_pathState = previousState; - refreshFromPathState(); - return false; - } - - /// @brief Replace the derived path distance while preserving the rest of the typed path state. - virtual bool trySetSphericalDistance(float distance) override - { - SCameraPathDistanceUpdateResult distanceUpdate = {}; - if (!m_pathModel.updateDistance) - { - return false; - } - - const auto previousState = m_pathState; - if (!m_pathModel.updateDistance(distance, m_pathLimits, m_pathState, &distanceUpdate)) - return false; - if (!refreshFromPathState()) - { - m_pathState = previousState; - refreshFromPathState(); - return false; - } - - return distanceUpdate.exact; - } - - virtual std::string_view getIdentifier() const override { return SCameraPathDefaults::Identifier; } - - /// @brief Return the currently installed path model. - inline const path_model_t& getPathModel() const - { - return m_pathModel; - } - - /// @brief Return the current path-state limits enforced by this camera instance. - inline const path_limits_t& getPathStateLimits() const - { - return m_pathLimits; - } - - /// @brief Replace the active path-state limits after sanitizing the current path state against them. - inline bool setPathStateLimits(path_limits_t pathLimits) - { - if (!CCameraPathUtilities::sanitizePathLimits(pathLimits) || !m_pathModel.resolveState) - return false; - - PathState sanitizedState = {}; - if (!m_pathModel.resolveState(m_targetPosition, m_gimbal.getPosition(), pathLimits, &m_pathState, sanitizedState)) - return false; - - const auto previousLimits = m_pathLimits; - const auto previousState = m_pathState; - m_pathLimits = pathLimits; - m_pathState = sanitizedState; - if (refreshFromPathState()) - return true; - - m_pathLimits = previousLimits; - m_pathState = previousState; - refreshFromPathState(); - return false; - } - - /// @brief Replace the active path model after validating that it can resolve the current path state. - inline bool setPathModel(path_model_t pathModel) - { - if (!isPathModelComplete(pathModel)) - return false; - - PathState sanitized = {}; - if (!pathModel.resolveState(m_targetPosition, m_gimbal.getPosition(), m_pathLimits, &m_pathState, sanitized)) - return false; - - const auto previousModel = m_pathModel; - const auto previousState = m_pathState; - m_pathModel = std::move(pathModel); - m_pathState = sanitized; - if (refreshFromPathState()) - return true; - - m_pathModel = previousModel; - m_pathState = previousState; - refreshFromPathState(); - return false; - } - -private: - static inline constexpr auto AllowedVirtualEvents = - CVirtualGimbalEvent::Translate | CVirtualGimbalEvent::RollLeft | CVirtualGimbalEvent::RollRight; - - /// @brief Check whether a path model provides all callbacks required by the runtime camera. - static inline bool isPathModelComplete(const path_model_t& pathModel) - { - return pathModel.resolveState && pathModel.controlLaw && pathModel.integrate && pathModel.evaluate && pathModel.updateDistance; - } - - /// @brief Attempt to initialize the runtime path state and pose from one model/limit pair. - inline bool tryInitializePathRig(const hlsl::float64_t3& position, path_model_t pathModel, path_limits_t pathLimits) - { - if (!CCameraPathUtilities::sanitizePathLimits(pathLimits)) - return false; - - if (!isPathModelComplete(pathModel)) - return false; - - PathState resolvedState = {}; - if (!pathModel.resolveState(m_targetPosition, position, pathLimits, nullptr, resolvedState)) - return false; - - m_pathLimits = pathLimits; - m_pathModel = std::move(pathModel); - m_pathState = resolvedState; - return refreshFromPathState(); - } - - /// @brief Initialize the path rig with graceful fallback to the shared default model and limits. - inline void initializePathRig(const hlsl::float64_t3& position, path_model_t pathModel, path_limits_t pathLimits) - { - path_limits_t sanitizedLimits = pathLimits; - const bool hasCustomLimits = CCameraPathUtilities::sanitizePathLimits(sanitizedLimits); - if (!hasCustomLimits) - sanitizedLimits = CCameraPathUtilities::makeDefaultPathLimits(); - - if (tryInitializePathRig(position, std::move(pathModel), sanitizedLimits)) - return; - - if (tryInitializePathRig(position, CCameraPathUtilities::makeDefaultPathModel(), sanitizedLimits)) - return; - - m_pathLimits = CCameraPathUtilities::makeDefaultPathLimits(); - m_pathModel = CCameraPathUtilities::makeDefaultPathModel(); - m_pathState = CCameraPathUtilities::makeDefaultPathState(m_pathLimits.minU); - m_pathModel.resolveState(m_targetPosition, position, m_pathLimits, nullptr, m_pathState); - refreshFromPathState(); - } - - path_model_t m_pathModel = CCameraPathUtilities::makeDefaultPathModel(); - path_limits_t m_pathLimits = CCameraPathUtilities::makeDefaultPathLimits(); - PathState m_pathState = CCameraPathUtilities::makeDefaultPathState(CCameraPathUtilities::makeDefaultPathLimits().minU); - - /// @brief Evaluate the current path state into a canonical pose and write it back to the runtime gimbal. - bool refreshFromPathState(bool* outManipulated = nullptr) - { - if (!m_pathModel.evaluate) - return false; - - SCameraCanonicalPathState canonicalPathState = {}; - if (!m_pathModel.evaluate(m_targetPosition, m_pathState, m_pathLimits, canonicalPathState)) - return false; - - m_distance = canonicalPathState.targetRelative.distance; - m_orbitUv = canonicalPathState.targetRelative.orbitUv; - - m_gimbal.begin(); - { - m_gimbal.setPosition(canonicalPathState.pose.position); - m_gimbal.setOrientation(canonicalPathState.pose.orientation); - } - m_gimbal.end(); - - const bool manipulated = bool(m_gimbal.getManipulationCounter()); - if (manipulated) - m_gimbal.updateView(); - - if (outManipulated) - *outManipulated = manipulated; - return true; - } -}; - -} - -#endif diff --git a/common/include/camera/CPlanarProjection.hpp b/common/include/camera/CPlanarProjection.hpp deleted file mode 100644 index 5407b9dd4..000000000 --- a/common/include/camera/CPlanarProjection.hpp +++ /dev/null @@ -1,56 +0,0 @@ -#ifndef _NBL_C_PLANAR_PROJECTION_HPP_ -#define _NBL_C_PLANAR_PROJECTION_HPP_ - -#include "IPlanarProjection.hpp" -#include "IRange.hpp" - -namespace nbl::core -{ - /// @brief Range-backed concrete implementation of `IPlanarProjection`. - /// - /// The template owns a caller-selected contiguous container of planar - /// projection entries together with their viewport-local binding layouts. - template ProjectionsRange> - class CPlanarProjection : public IPlanarProjection - { - public: - virtual ~CPlanarProjection() = default; - - /// @brief Create a planar projection wrapper only when a valid camera instance is available. - inline static core::smart_refctd_ptr create(core::smart_refctd_ptr&& camera) - { - if (!camera) - return nullptr; - - return core::smart_refctd_ptr(new CPlanarProjection(core::smart_refctd_ptr(camera)), core::dont_grab); - } - - /// @brief Return the number of stored planar projection entries. - virtual uint32_t getLinearProjectionCount() const override - { - return static_cast(m_projections.size()); - } - - /// @brief Return one stored planar projection entry through the linear base interface. - virtual const ILinearProjection::CProjection& getLinearProjection(uint32_t index) const override - { - assert(index < m_projections.size()); - return m_projections[index]; - } - - /// @brief Expose mutable access to the owned planar projection range. - inline ProjectionsRange& getPlanarProjections() - { - return m_projections; - } - - protected: - CPlanarProjection(core::smart_refctd_ptr&& camera) - : IPlanarProjection(core::smart_refctd_ptr(camera)) {} - - ProjectionsRange m_projections; - }; - -} // nbl::hlsl namespace - -#endif // _NBL_C_PLANAR_PROJECTION_HPP_ diff --git a/common/include/camera/CSphericalTargetCamera.hpp b/common/include/camera/CSphericalTargetCamera.hpp deleted file mode 100644 index 55cb5a292..000000000 --- a/common/include/camera/CSphericalTargetCamera.hpp +++ /dev/null @@ -1,240 +0,0 @@ -#ifndef _C_SPHERICAL_TARGET_CAMERA_HPP_ -#define _C_SPHERICAL_TARGET_CAMERA_HPP_ - -#include -#include "CCameraTargetRelativeUtilities.hpp" - -namespace nbl::core -{ - -/// @brief Common base for target-relative cameras represented by target position, distance, and `orbitUv`. -/// -/// Derived cameras keep the same target-relative storage but apply different -/// constraints and event policies in `manipulate(...)`. -class CSphericalTargetCamera : public ICamera -{ -public: - using base_t = ICamera; - - CSphericalTargetCamera(const hlsl::float64_t3& position, const hlsl::float64_t3& target) - : base_t(), m_targetPosition(target), m_distance(SCameraTargetRelativeRigDefaults::InitialDistance), - m_gimbal({ .position = position, .orientation = hlsl::CCameraMathUtilities::makeIdentityQuaternion() }) - { - initFromPosition(position); - } - ~CSphericalTargetCamera() = default; - - inline bool setDistance(float d) - { - const auto clamped = std::clamp(d, MinDistance, MaxDistance); - const bool ok = clamped == d; - if (m_distance == clamped) - return ok; - m_distance = clamped; - applyPose(); - return ok; - } - - inline void target(const hlsl::float64_t3& p) - { - if (m_targetPosition == p) - return; - m_targetPosition = p; - applyPose(); - } - inline hlsl::float64_t3 getTarget() const { return m_targetPosition; } - - inline float getDistance() const { return m_distance; } - inline const hlsl::float64_t2& getOrbitUv() const { return m_orbitUv; } - - static inline constexpr float MinDistance = SCameraTargetRelativeTraits::MinDistance; - static inline constexpr float MaxDistance = SCameraTargetRelativeTraits::DefaultMaxDistance; - - virtual uint32_t getCapabilities() const override - { - return base_t::SphericalTarget; - } - - virtual bool tryGetSphericalTargetState(typename base_t::SphericalTargetState& out) const override - { - out.target = m_targetPosition; - out.distance = m_distance; - out.orbitUv = m_orbitUv; - out.minDistance = MinDistance; - out.maxDistance = MaxDistance; - return true; - } - - virtual bool trySetSphericalTarget(const hlsl::float64_t3& targetPosition) override - { - target(targetPosition); - return true; - } - - virtual bool trySetSphericalDistance(float distance) override - { - return setDistance(distance); - } - -protected: - using SphericalBasis = SCameraTargetRelativeBasis; - - /// @brief Return the current canonical target-relative state stored by the spherical rig. - inline SCameraTargetRelativeState currentTargetRelativeState() const - { - return { - .target = m_targetPosition, - .orbitUv = m_orbitUv, - .distance = m_distance - }; - } - - /// @brief Replace the stored target-relative state without touching the gimbal pose yet. - inline void adoptTargetRelativeState(const SCameraTargetRelativeState& state) - { - m_targetPosition = state.target; - m_orbitUv = state.orbitUv; - m_distance = state.distance; - } - - /// @brief Extract one rigid reference transform from the optional external override or the current gimbal pose. - inline bool tryExtractReferenceTransform(CReferenceTransform& outReference, const hlsl::float64_t4x4* referenceFrame) - { - return m_gimbal.extractReferenceTransform(&outReference, referenceFrame); - } - - /// @brief Resolve the current target-relative state from one rigid reference position around the current target. - inline bool tryResolveReferenceTargetRelativeState(const CReferenceTransform& reference, SCameraTargetRelativeState& outState) const - { - return CCameraTargetRelativeUtilities::tryBuildTargetRelativeStateFromPosition( - m_targetPosition, - hlsl::float64_t3(reference.frame[3]), - MinDistance, - MaxDistance, - outState); - } - - /// @brief Resolve the top-down yaw encoded by a rigid reference orientation. - static inline double resolveTopDownYawFromReference(const CReferenceTransform& reference, const double fallbackYaw) - { - const auto basis = hlsl::CCameraMathUtilities::getQuaternionBasisMatrix(reference.orientation); - const auto planarUp = hlsl::float64_t2(basis[1].x, basis[1].y); - constexpr auto Epsilon = static_cast(SCameraToolingThresholds::TinyScalarEpsilon); - if (!hlsl::CCameraMathUtilities::isNearlyZeroVector(planarUp, Epsilon)) - return hlsl::atan2(planarUp.y, planarUp.x); - - const auto planarRight = hlsl::float64_t2(basis[0].x, basis[0].y); - if (!hlsl::CCameraMathUtilities::isNearlyZeroVector(planarRight, Epsilon)) - return hlsl::atan2(planarRight.x, -planarRight.y); - - return fallbackYaw; - } - - /// @brief Project one rigid reference pose onto the legal top-down state manifold around the current target. - inline bool tryResolveReferenceTopDownState(const CReferenceTransform& reference, SCameraTargetRelativeState& outState) const - { - const auto offset = hlsl::float64_t3(reference.frame[3]) - m_targetPosition; - const auto distance = hlsl::length(offset); - if (!hlsl::CCameraMathUtilities::isFiniteScalar(distance) || - distance <= static_cast(SCameraToolingThresholds::TinyScalarEpsilon)) - { - return false; - } - - outState = currentTargetRelativeState(); - outState.distance = static_cast(std::clamp( - distance, - static_cast(MinDistance), - static_cast(MaxDistance))); - outState.orbitUv.x = resolveTopDownYawFromReference(reference, m_orbitUv.x); - outState.orbitUv.y = SCameraTargetRelativeRigDefaults::TopDownPitchRad; - return true; - } - - /// @brief Project one rigid reference pose onto the legal fixed-angle isometric manifold around the current target. - inline bool tryResolveReferenceIsometricState(const CReferenceTransform& reference, SCameraTargetRelativeState& outState) const - { - if (!tryResolveReferenceTargetRelativeState(reference, outState)) - return false; - - outState.orbitUv = hlsl::float64_t2( - SCameraTargetRelativeRigDefaults::IsometricYawRad, - SCameraTargetRelativeRigDefaults::IsometricPitchRad); - return true; - } - - inline SphericalBasis computeBasis(const hlsl::float64_t2& orbitUv, float distance) const - { - SphericalBasis basis; - const SCameraTargetRelativeState state = { - .target = m_targetPosition, - .orbitUv = orbitUv, - .distance = distance - }; - if (!CCameraTargetRelativeUtilities::tryBuildTargetRelativeBasis(state, MinDistance, MaxDistance, basis)) - return basis; - return basis; - } - - inline void initFromPosition(const hlsl::float64_t3& position) - { - SCameraTargetRelativeState state = {}; - if (!CCameraTargetRelativeUtilities::tryBuildTargetRelativeStateFromPosition(m_targetPosition, position, MinDistance, MaxDistance, state)) - { - m_distance = MinDistance; - m_orbitUv = hlsl::float64_t2(0.0); - return; - } - - m_distance = state.distance; - m_orbitUv = state.orbitUv; - } - - inline void applyPlanarTargetTranslation(const hlsl::float64_t3& deltaTranslation, const SphericalBasis& basis) - { - if (!hlsl::CCameraMathUtilities::hasPlanarDeltaXY(deltaTranslation, static_cast(SCameraToolingThresholds::TinyScalarEpsilon))) - return; - - m_targetPosition += hlsl::CCameraMathUtilities::transformLocalVectorToWorldBasis( - hlsl::float64_t3(deltaTranslation.x, deltaTranslation.y, 0.0), - basis.right, - basis.up, - basis.forward); - } - - inline bool applyPose() - { - const SCameraTargetRelativeState state = { - .target = m_targetPosition, - .orbitUv = m_orbitUv, - .distance = m_distance - }; - SCameraTargetRelativePose pose = {}; - if (!CCameraTargetRelativeUtilities::tryBuildTargetRelativePoseFromState(state, MinDistance, MaxDistance, pose)) - return false; - m_distance = static_cast(pose.appliedDistance); - - m_gimbal.begin(); - { - m_gimbal.setPosition(pose.position); - m_gimbal.setOrientation(pose.orientation); - } - m_gimbal.end(); - - const bool manipulated = bool(m_gimbal.getManipulationCounter()); - if (manipulated) - m_gimbal.updateView(); - - return manipulated; - } - - hlsl::float64_t3 m_targetPosition; - float m_distance; - typename base_t::CGimbal m_gimbal; - hlsl::float64_t2 m_orbitUv = hlsl::float64_t2(0.0); -}; - -} - -#endif - diff --git a/common/include/camera/CTopDownCamera.hpp b/common/include/camera/CTopDownCamera.hpp deleted file mode 100644 index 63d16872c..000000000 --- a/common/include/camera/CTopDownCamera.hpp +++ /dev/null @@ -1,78 +0,0 @@ -#ifndef _C_TOPDOWN_CAMERA_HPP_ -#define _C_TOPDOWN_CAMERA_HPP_ - -#include -#include - -#include "CSphericalTargetCamera.hpp" - -namespace nbl::core -{ - -/// @brief Target-relative camera constrained to look straight down at the tracked target. -/// -/// Yaw may still rotate the view around the vertical axis, while pitch is fixed to -/// the top-down angle and translation moves the tracked target in the view plane. -class CTopDownCamera final : public CSphericalTargetCamera -{ -public: - using base_t = CSphericalTargetCamera; - - CTopDownCamera(const hlsl::float64_t3& position, const hlsl::float64_t3& target) - : base_t(position, target) - { - m_orbitUv.y = TopDownPitch; - applyPose(); - } - ~CTopDownCamera() = default; - - const typename base_t::CGimbal& getGimbal() override { return m_gimbal; } - - /// @brief Apply one frame of top-down yaw rotation, planar translation, and distance changes. - virtual bool manipulate(std::span virtualEvents, const hlsl::float64_t4x4* referenceFrame = nullptr) override - { - if (not virtualEvents.size() and not referenceFrame) - return false; - - if (referenceFrame) - { - CReferenceTransform reference = {}; - SCameraTargetRelativeState resolvedState = {}; - if (!tryExtractReferenceTransform(reference, referenceFrame) || - !tryResolveReferenceTopDownState(reference, resolvedState)) - { - return false; - } - - adoptTargetRelativeState(resolvedState); - } - - const auto impulse = m_gimbal.accumulate(virtualEvents); - - const auto deltaRotation = scaleVirtualRotation(impulse.dVirtualRotation); - const auto deltaTranslation = scaleVirtualTranslation(impulse.dVirtualTranslate); - const double deltaDistance = scaleUnscaledVirtualTranslation(impulse.dVirtualTranslate.z); - - m_orbitUv.x += deltaRotation.y; - m_orbitUv.y = TopDownPitch; - m_distance = std::clamp(m_distance + static_cast(deltaDistance), MinDistance, MaxDistance); - - const auto basis = computeBasis(m_orbitUv, m_distance); - applyPlanarTargetTranslation(deltaTranslation, basis); - - return applyPose(); - } - - virtual uint32_t getAllowedVirtualEvents() const override { return AllowedVirtualEvents; } - virtual CameraKind getKind() const override { return CameraKind::TopDown; } - /// @brief Return the stable user-facing identifier for this concrete camera kind. - virtual std::string_view getIdentifier() const override { return "Top-Down Camera"; } - -private: - static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate | CVirtualGimbalEvent::Rotate; - static inline constexpr double TopDownPitch = SCameraTargetRelativeRigDefaults::TopDownPitchRad; -}; - -} - -#endif diff --git a/common/include/camera/CTurntableCamera.hpp b/common/include/camera/CTurntableCamera.hpp deleted file mode 100644 index dc44d23a3..000000000 --- a/common/include/camera/CTurntableCamera.hpp +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright (C) 2018-2024 - 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_TURNTABLE_CAMERA_HPP_ -#define _C_TURNTABLE_CAMERA_HPP_ - -#include -#include - -#include "CSphericalTargetCamera.hpp" - -namespace nbl::core -{ - -/// @brief Target-relative camera that behaves like a classic turntable around a fixed target. -/// -/// The camera exposes yaw, bounded pitch, and distance changes while keeping the -/// target fixed in space and avoiding arbitrary planar target translation. -class CTurntableCamera final : public CSphericalTargetCamera -{ -public: - using base_t = CSphericalTargetCamera; - - CTurntableCamera(const hlsl::float64_t3& position, const hlsl::float64_t3& target) - : base_t(position, target) - { - m_orbitUv.y = std::clamp(m_orbitUv.y, MinPitch, MaxPitch); - applyPose(); - } - ~CTurntableCamera() = default; - - const typename base_t::CGimbal& getGimbal() override { return m_gimbal; } - - /// @brief Apply one frame of yaw, bounded pitch, and distance input around the tracked target. - virtual bool manipulate(std::span virtualEvents, const hlsl::float64_t4x4* referenceFrame = nullptr) override - { - if (not virtualEvents.size() and not referenceFrame) - return false; - - if (referenceFrame) - { - CReferenceTransform reference = {}; - SCameraTargetRelativeState resolvedState = {}; - if (!tryExtractReferenceTransform(reference, referenceFrame) || - !tryResolveReferenceTargetRelativeState(reference, resolvedState)) - { - return false; - } - - resolvedState.orbitUv.y = std::clamp(resolvedState.orbitUv.y, MinPitch, MaxPitch); - adoptTargetRelativeState(resolvedState); - } - - const auto impulse = m_gimbal.accumulate(virtualEvents); - - const double deltaYaw = scaleVirtualRotation(impulse.dVirtualRotation.y); - const double deltaPitch = scaleVirtualRotation(impulse.dVirtualRotation.x); - const double deltaDistance = scaleUnscaledVirtualTranslation(impulse.dVirtualTranslate.z); - - m_orbitUv.x += deltaYaw; - m_orbitUv.y = std::clamp(m_orbitUv.y + deltaPitch, MinPitch, MaxPitch); - m_distance = std::clamp(m_distance + static_cast(deltaDistance), MinDistance, MaxDistance); - - return applyPose(); - } - - virtual uint32_t getAllowedVirtualEvents() const override { return AllowedVirtualEvents; } - virtual CameraKind getKind() const override { return CameraKind::Turntable; } - /// @brief Return the stable user-facing identifier for this concrete camera kind. - virtual std::string_view getIdentifier() const override { return "Turntable Camera"; } - - static inline constexpr float MinDistance = base_t::MinDistance; - static inline constexpr float MaxDistance = base_t::MaxDistance; - -private: - - static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate | CVirtualGimbalEvent::Rotate; - static inline constexpr double MaxPitch = SCameraTargetRelativeRigDefaults::TurntablePitchLimitRad; - static inline constexpr double MinPitch = -MaxPitch; -}; - -} - -#endif diff --git a/common/include/camera/CVirtualGimbalEvent.hpp b/common/include/camera/CVirtualGimbalEvent.hpp deleted file mode 100644 index e1384ed42..000000000 --- a/common/include/camera/CVirtualGimbalEvent.hpp +++ /dev/null @@ -1,159 +0,0 @@ -#ifndef _NBL_C_VIRTUAL_GIMBAL_EVENT_HPP_ -#define _NBL_C_VIRTUAL_GIMBAL_EVENT_HPP_ - -#include -#include -#include - -#include "nbl/builtin/hlsl/cpp_compat/vector.hlsl" -#include "nbl/core/math/intutil.h" - -namespace nbl::core -{ - -/// @brief One semantic camera command passed to `ICamera::manipulate(...)`. -/// -/// `type` selects the command family. `magnitude` stores the non-negative -/// source-normalized scalar amount for that command. Input binders convert -/// raw keyboard, mouse, scroll, and ImGuizmo data into this representation -/// before the camera sees it. Cameras then convert these virtual magnitudes -/// into camera-local motion through their runtime scales and family-specific -/// legalization rules. -struct CVirtualGimbalEvent -{ - /// @brief Bitmask identifiers for semantic movement, rotation, and scale commands. - enum VirtualEventType : uint32_t - { - None = 0, - - MoveForward = core::createBitmask({ 0 }), - MoveBackward = core::createBitmask({ 1 }), - MoveLeft = core::createBitmask({ 2 }), - MoveRight = core::createBitmask({ 3 }), - MoveUp = core::createBitmask({ 4 }), - MoveDown = core::createBitmask({ 5 }), - TiltUp = core::createBitmask({ 6 }), - TiltDown = core::createBitmask({ 7 }), - PanLeft = core::createBitmask({ 8 }), - PanRight = core::createBitmask({ 9 }), - RollLeft = core::createBitmask({ 10 }), - RollRight = core::createBitmask({ 11 }), - ScaleXInc = core::createBitmask({ 12 }), - ScaleXDec = core::createBitmask({ 13 }), - ScaleYInc = core::createBitmask({ 14 }), - ScaleYDec = core::createBitmask({ 15 }), - ScaleZInc = core::createBitmask({ 16 }), - ScaleZDec = core::createBitmask({ 17 }), - - EventsCount = 18, - - Translate = MoveForward | MoveBackward | MoveLeft | MoveRight | MoveUp | MoveDown, - Rotate = TiltUp | TiltDown | PanLeft | PanRight | RollLeft | RollRight, - Scale = ScaleXInc | ScaleXDec | ScaleYInc | ScaleYDec | ScaleZInc | ScaleZDec, - - All = Translate | Rotate | Scale - }; - - /// @brief Scalar type used to encode one event magnitude. - using manipulation_encode_t = hlsl::float64_t; - - /// @brief Semantic event identifier. - VirtualEventType type = None; - /// @brief Non-negative scalar amount associated with `type`. - /// - /// The value is not a raw device unit. It is the virtual amount emitted by - /// the active input path after applying binding-local gains. - manipulation_encode_t magnitude = {}; - - /// @brief Convert one event identifier to its stable string form. - static constexpr std::string_view virtualEventToString(VirtualEventType event) - { - switch (event) - { - case MoveForward: return "MoveForward"; - case MoveBackward: return "MoveBackward"; - case MoveLeft: return "MoveLeft"; - case MoveRight: return "MoveRight"; - case MoveUp: return "MoveUp"; - case MoveDown: return "MoveDown"; - case TiltUp: return "TiltUp"; - case TiltDown: return "TiltDown"; - case PanLeft: return "PanLeft"; - case PanRight: return "PanRight"; - case RollLeft: return "RollLeft"; - case RollRight: return "RollRight"; - case ScaleXInc: return "ScaleXInc"; - case ScaleXDec: return "ScaleXDec"; - case ScaleYInc: return "ScaleYInc"; - case ScaleYDec: return "ScaleYDec"; - case ScaleZInc: return "ScaleZInc"; - case ScaleZDec: return "ScaleZDec"; - case Translate: return "Translate"; - case Rotate: return "Rotate"; - case Scale: return "Scale"; - case None: return "None"; - default: return "Unknown"; - } - } - - /// @brief Convert one stable string identifier back to an event identifier. - static constexpr VirtualEventType stringToVirtualEvent(std::string_view event) - { - if (event == "MoveForward") return MoveForward; - if (event == "MoveBackward") return MoveBackward; - if (event == "MoveLeft") return MoveLeft; - if (event == "MoveRight") return MoveRight; - if (event == "MoveUp") return MoveUp; - if (event == "MoveDown") return MoveDown; - if (event == "TiltUp") return TiltUp; - if (event == "TiltDown") return TiltDown; - if (event == "PanLeft") return PanLeft; - if (event == "PanRight") return PanRight; - if (event == "RollLeft") return RollLeft; - if (event == "RollRight") return RollRight; - if (event == "ScaleXInc") return ScaleXInc; - if (event == "ScaleXDec") return ScaleXDec; - if (event == "ScaleYInc") return ScaleYInc; - if (event == "ScaleYDec") return ScaleYDec; - if (event == "ScaleZInc") return ScaleZInc; - if (event == "ScaleZDec") return ScaleZDec; - if (event == "Translate") return Translate; - if (event == "Rotate") return Rotate; - if (event == "Scale") return Scale; - if (event == "None") return None; - return None; - } - - /// @brief Return whether `event` belongs to the translation subset. - static constexpr bool isTranslationEvent(const VirtualEventType event) - { - return event != None && (event & Translate) == event; - } - - /// @brief Return whether `event` belongs to the rotation subset. - static constexpr bool isRotationEvent(const VirtualEventType event) - { - return event != None && (event & Rotate) == event; - } - - /// @brief Return whether `event` belongs to the scale subset. - static constexpr bool isScaleEvent(const VirtualEventType event) - { - return event != None && (event & Scale) == event; - } - - /// @brief Table listing every individual event bit in declaration order. - static inline constexpr auto VirtualEventsTypeTable = []() - { - std::array output; - - for (uint16_t i = 0u; i < EventsCount; ++i) - output[i] = static_cast(core::createBitmask({ i })); - - return output; - }(); -}; - -} // namespace nbl::core - -#endif // _NBL_C_VIRTUAL_GIMBAL_EVENT_HPP_ diff --git a/common/include/camera/ICamera.hpp b/common/include/camera/ICamera.hpp deleted file mode 100644 index 62b33bcf1..000000000 --- a/common/include/camera/ICamera.hpp +++ /dev/null @@ -1,456 +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 _I_CAMERA_HPP_ -#define _I_CAMERA_HPP_ - -#include -#include - -#include "CCameraTraits.hpp" -#include "IGimbal.hpp" - -namespace nbl::core -{ - -/// @brief Shared runtime camera interface. -/// -/// `ICamera` consumes batches of `CVirtualGimbalEvent` values and updates one -/// camera pose stored in `CGimbal`. A `CVirtualGimbalEvent` identifies one -/// semantic command such as `MoveForward`, `PanLeft`, or `RollRight` and carries -/// one source-normalized scalar magnitude for that command. -/// -/// Keyboard input, mouse input, ImGuizmo interaction, scripted playback, -/// preset replay, follow helpers, and goal solving all drive cameras through -/// the same `manipulate(...)` entry point. -/// -/// The optional typed hooks expose camera-family state for code that needs -/// capture, restore, compatibility analysis, persistence, or validation. -class ICamera : virtual public core::IReferenceCounted -{ -private: - static inline constexpr double DefaultMoveSpeedScaleValue = 0.01; - static inline constexpr double DefaultRotationSpeedScaleValue = 0.003; - static inline constexpr double VirtualTranslationUnit = 0.01; - -public: - /// @brief Camera-local multipliers applied when semantic virtual events are converted into motion. - /// - /// Input binders emit virtual magnitudes. Concrete cameras multiply those - /// magnitudes by this per-camera configuration before applying them to - /// their own state model. - struct SMotionConfig - { - /// @brief Camera-local scale applied to virtual translation magnitudes. - double moveSpeedScale = DefaultMoveSpeedScaleValue; - /// @brief Camera-local scale applied to virtual rotation magnitudes. - double rotationSpeedScale = DefaultRotationSpeedScaleValue; - }; - - /// @brief Stable camera-family identifier used by metadata, presets, follow, and scripted helpers. - enum class CameraKind : uint8_t - { - Unknown, - FPS, - Free, - Orbit, - Arcball, - Turntable, - TopDown, - Isometric, - Chase, - Dolly, - DollyZoom, - Path - }; - - /// @brief Optional typed capabilities exposed by a concrete runtime camera implementation. - enum CameraCapability : uint32_t - { - None = 0u, - SphericalTarget = core::createBitmask({ 0 }), - DynamicPerspectiveFov = core::createBitmask({ 1 }) - }; - - /// @brief Typed state fragments that helper layers may capture from or apply to a camera. - enum GoalStateMask : uint32_t - { - GoalStateNone = 0u, - GoalStateSphericalTarget = core::createBitmask({ 0 }), - GoalStateDynamicPerspective = core::createBitmask({ 1 }), - GoalStatePath = core::createBitmask({ 2 }) - }; - - /// @brief Canonical target-relative state reported by spherical camera families. - /// - /// The state stores the tracked target position, orbit angles in `orbitUv`, - /// and distance limits needed by tooling that wants to capture or reapply a - /// target-relative camera pose without going through free-form setters. - /// `maxDistance` is an optional upper bound and may be infinite when the - /// active camera family does not impose a finite cap. - struct SphericalTargetState - { - /// @brief Tracked target position in world space. - hlsl::float64_t3 target = hlsl::float64_t3(0.0); - /// @brief Orbit yaw and pitch around the target, expressed in radians. - hlsl::float64_t2 orbitUv = hlsl::float64_t2(0.0); - /// @brief Current camera-to-target distance. - float distance = 0.f; - /// @brief Lowest distance that remains valid for the current camera. - float minDistance = 0.f; - /// @brief Highest distance that remains valid for the current camera, or infinity when unbounded. - float maxDistance = SCameraTargetRelativeTraits::DefaultMaxDistance; - }; - - /// @brief Typed perspective state reported by cameras with derived FOV behavior. - struct DynamicPerspectiveState - { - /// @brief Authored reference FOV in degrees. - float baseFov = 0.f; - /// @brief Distance at which `baseFov` should be preserved. - float referenceDistance = 0.f; - }; - - /// @brief Limits constraining reusable `PathState` coordinates for `Path Rig` cameras. - /// - /// These limits are part of the typed path-model surface. They are not - /// global engine rules. A concrete `Path Rig` instance may expose an - /// unbounded `maxDistance` by returning infinity. - struct PathStateLimits - { - /// @brief Minimal valid `u` coordinate after path-state sanitization. - double minU = static_cast(SCameraTargetRelativeTraits::MinDistance); - /// @brief Minimal valid radial distance derived from the `(u, v)` pair. - hlsl::float64_t minDistance = static_cast(SCameraTargetRelativeTraits::MinDistance); - /// @brief Maximal valid radial distance derived from the `(u, v)` pair, or infinity when unbounded. - hlsl::float64_t maxDistance = static_cast(SCameraTargetRelativeTraits::DefaultMaxDistance); - }; - - /// @brief Parametric path-rig state used by the `Path Rig` camera kind. - /// - /// The built-in path model interprets `(s, u, v, roll)` as path progress, - /// lateral shape coordinates, and roll around the local forward axis. - /// Other path models may map the same coordinates onto different geometry. - struct PathState - { - /// @brief Primary path-progress coordinate interpreted by the active path model. - double s = 0.0; - /// @brief First lateral/shape coordinate interpreted by the active path model. - double u = 0.0; - /// @brief Second lateral/shape coordinate interpreted by the active path model. - double v = 0.0; - /// @brief Roll around the path-model forward axis, expressed in radians. - double roll = 0.0; - - /// @brief Pack the state into one four-component vector. - inline hlsl::float64_t4 asVector() const - { - return hlsl::float64_t4(s, u, v, roll); - } - - /// @brief Project the state onto the translation-style representation used by replay helpers. - inline hlsl::float64_t3 asTranslationVector() const - { - return hlsl::float64_t3(u, v, s); - } - - /// @brief Rebuild one path state from the packed vector representation. - static inline PathState fromVector(const hlsl::float64_t4& value) - { - return { - .s = value.x, - .u = value.y, - .v = value.z, - .roll = value.w - }; - } - - /// @brief Rebuild one path state from the translation-style helper representation. - static inline PathState fromTranslationVector(const hlsl::float64_t3& value, const double pathRoll = 0.0) - { - return { - .s = value.z, - .u = value.x, - .v = value.y, - .roll = pathRoll - }; - } - }; - - /// @brief Gimbal that stores the runtime camera pose and cached world-to-view transform. - /// - /// Camera implementations own one `CGimbal` instance and update it after - /// applying their internal state model. The gimbal stores world-space - /// position, orientation, and the cached view matrix derived from them. - class CGimbal : public IGimbal - { - public: - using base_t = IGimbal; - - CGimbal(typename base_t::SCreationParameters&& parameters) : base_t(std::move(parameters)) { updateView(); } - ~CGimbal() = default; - - /// @brief Rebuild the cached world-to-view matrix from the current gimbal pose. - inline void updateView() - { - const auto& gRight = base_t::getXAxis(), gUp = base_t::getYAxis(), gForward = base_t::getZAxis(); - - assert(hlsl::isOrthoBase(gRight, gUp, gForward)); - - const auto& position = base_t::getPosition(); - - m_viewMatrix[0u] = hlsl::float64_t4(gRight, -hlsl::dot(gRight, position)); - m_viewMatrix[1u] = hlsl::float64_t4(gUp, -hlsl::dot(gUp, position)); - m_viewMatrix[2u] = hlsl::float64_t4(gForward, -hlsl::dot(gForward, position)); - } - - /// @brief Return the cached world-to-view matrix derived from the current pose. - inline const hlsl::float64_t3x4& getViewMatrix() const { return m_viewMatrix; } - - private: - hlsl::float64_t3x4 m_viewMatrix; - }; - - class SScopedMotionScaleOverride - { - public: - /// @brief Temporarily override both motion scales and restore the previous values on destruction. - SScopedMotionScaleOverride(ICamera* camera, const double moveScale, const double rotationScale) - : m_camera(camera) - { - if (!m_camera) - return; - - m_prevMoveScale = m_camera->getMoveSpeedScale(); - m_prevRotationScale = m_camera->getRotationSpeedScale(); - m_camera->setMotionScales(moveScale, rotationScale); - } - - SScopedMotionScaleOverride(const SScopedMotionScaleOverride&) = delete; - SScopedMotionScaleOverride& operator=(const SScopedMotionScaleOverride&) = delete; - - SScopedMotionScaleOverride(SScopedMotionScaleOverride&& other) noexcept - : m_camera(std::exchange(other.m_camera, nullptr)), - m_prevMoveScale(other.m_prevMoveScale), - m_prevRotationScale(other.m_prevRotationScale) - { - } - - SScopedMotionScaleOverride& operator=(SScopedMotionScaleOverride&& other) = delete; - - ~SScopedMotionScaleOverride() - { - if (m_camera) - m_camera->setMotionScales(m_prevMoveScale, m_prevRotationScale); - } - - private: - ICamera* m_camera = nullptr; - double m_prevMoveScale = 0.0; - double m_prevRotationScale = 0.0; - }; - - ICamera() {} - virtual ~ICamera() = default; - - /// @brief Return the mutable gimbal backing the runtime camera pose. - virtual const CGimbal& getGimbal() = 0u; - - /// @brief Apply one frame of semantic virtual events and an optional rigid reference-frame anchor. - /// - /// `virtualEvents` stores one frame of semantic movement, rotation, and - /// scale commands. Translation commands use `Move*`, rotation commands use - /// `Tilt*`, `Pan*`, and `Roll*`, and scale commands use `Scale*`. Cameras - /// interpret only the subset advertised by `getAllowedVirtualEvents()`. - /// - /// `referenceFrame` is an optional rigid world-space transform used as the - /// anchor for this manipulation step. Free-like cameras may apply it - /// directly as pose input. Constrained cameras may first resolve it into - /// their own typed legal state and then apply event deltas in that state - /// space. - virtual bool manipulate(std::span virtualEvents, const hlsl::float64_t4x4* referenceFrame = nullptr) = 0; - /// @brief Apply one frame of virtual events while temporarily overriding the camera-local motion scales. - inline bool manipulateWithMotionScales(std::span virtualEvents, const hlsl::float64_t4x4* referenceFrame, const double moveScale, const double rotationScale) - { - auto scopedOverride = overrideMotionScales(moveScale, rotationScale); - return manipulate(virtualEvents, referenceFrame); - } - /// @brief Apply one frame of virtual events with unit translation and rotation scales. - inline bool manipulateWithUnitMotionScales(std::span virtualEvents, const hlsl::float64_t4x4* referenceFrame = nullptr) - { - return manipulateWithMotionScales(virtualEvents, referenceFrame, 1.0, 1.0); - } - - /// @brief Return the semantic virtual-event mask accepted by this camera kind. - /// - /// Input binders, scripted replay, and restore helpers use this mask to - /// decide which `CVirtualGimbalEvent` categories may be passed to - /// `manipulate(...)`. - virtual uint32_t getAllowedVirtualEvents() const = 0u; - - /// @brief Return the stable camera-family identifier for this concrete runtime camera. - virtual CameraKind getKind() const = 0; - /// @brief Return the optional typed capabilities exposed by this camera implementation. - virtual uint32_t getCapabilities() const { return None; } - /// @brief Return the typed goal-state fragments that helper layers may safely use with this camera. - virtual uint32_t getGoalStateMask() const - { - uint32_t mask = GoalStateNone; - if (hasCapability(SphericalTarget)) - mask |= GoalStateSphericalTarget; - if (hasCapability(DynamicPerspectiveFov)) - mask |= GoalStateDynamicPerspective; - return mask; - } - - /// @brief Return the stable human-readable identifier for this concrete camera instance. - virtual std::string_view getIdentifier() const = 0u; - - /// @brief Check whether the camera exposes the requested optional capability. - inline bool hasCapability(CameraCapability capability) const - { - return (getCapabilities() & capability) == capability; - } - - /// @brief Check whether the camera can exchange the requested typed goal-state fragment. - inline bool supportsGoalState(GoalStateMask goalState) const - { - return (getGoalStateMask() & goalState) == goalState; - } - - /// @brief Query the current spherical-target state when the camera exposes it. - virtual bool tryGetSphericalTargetState(SphericalTargetState& out) const - { - return false; - } - - /// @brief Replace only the tracked target position for spherical-target cameras. - virtual bool trySetSphericalTarget(const hlsl::float64_t3& target) - { - return false; - } - - /// @brief Replace only the tracked target distance for spherical-target cameras. - virtual bool trySetSphericalDistance(float distance) - { - return false; - } - - /// @brief Query the current derived dynamic perspective FOV when the camera exposes it. - virtual bool tryGetDynamicPerspectiveFov(float& outFov) const - { - return false; - } - - /// @brief Query the current authored dynamic perspective state when the camera exposes it. - virtual bool tryGetDynamicPerspectiveState(DynamicPerspectiveState& out) const - { - return false; - } - - /// @brief Replace the authored dynamic perspective state when the camera exposes it. - virtual bool trySetDynamicPerspectiveState(const DynamicPerspectiveState& state) - { - return false; - } - - /// @brief Query the current typed path state when the camera exposes it. - virtual bool tryGetPathState(PathState& out) const - { - return false; - } - - /// @brief Query the active typed limits constraining the current path state. - virtual bool tryGetPathStateLimits(PathStateLimits& out) const - { - return false; - } - - /// @brief Replace the current typed path state when the camera exposes it. - virtual bool trySetPathState(const PathState& state) - { - return false; - } - - /// @brief Update only the translation motion scale used by the camera runtime. - inline void setMoveSpeedScale(double scalar) - { - m_motionConfig.moveSpeedScale = scalar; - } - - /// @brief Update only the rotation motion scale used by the camera runtime. - inline void setRotationSpeedScale(double scalar) - { - m_motionConfig.rotationSpeedScale = scalar; - } - - /// @brief Update both translation and rotation motion scales at once. - inline void setMotionScales(const double moveScale, const double rotationScale) - { - setMoveSpeedScale(moveScale); - setRotationSpeedScale(rotationScale); - } - - /// @brief Return the current translation motion scale. - inline double getMoveSpeedScale() const { return m_motionConfig.moveSpeedScale; } - /// @brief Return the current rotation motion scale. - inline double getRotationSpeedScale() const { return m_motionConfig.rotationSpeedScale; } - /// @brief Return the full motion-scale bundle. - inline const SMotionConfig& getMotionConfig() const { return m_motionConfig; } - /// @brief Return the effective world-space translation represented by a unit virtual move event. - inline double getScaledVirtualTranslationMagnitude() const - { - return getUnscaledVirtualTranslationMagnitude() * getMoveSpeedScale(); - } - /// @brief Return the raw translation magnitude before applying the camera-local move scale. - inline double getUnscaledVirtualTranslationMagnitude() const - { - return VirtualTranslationUnit; - } - /// @brief Scale one scalar translation magnitude through the active move scale. - inline double scaleVirtualTranslation(const double magnitude) const - { - return magnitude * getScaledVirtualTranslationMagnitude(); - } - /// @brief Scale one translation vector through the active move scale. - template - inline hlsl::camera_vector_t scaleVirtualTranslation(const hlsl::camera_vector_t& magnitude) const - { - return magnitude * static_cast(getScaledVirtualTranslationMagnitude()); - } - /// @brief Scale one scalar translation magnitude without applying the camera-local move scale. - inline double scaleUnscaledVirtualTranslation(const double magnitude) const - { - return magnitude * getUnscaledVirtualTranslationMagnitude(); - } - /// @brief Scale one translation vector without applying the camera-local move scale. - template - inline hlsl::camera_vector_t scaleUnscaledVirtualTranslation(const hlsl::camera_vector_t& magnitude) const - { - return magnitude * static_cast(getUnscaledVirtualTranslationMagnitude()); - } - /// @brief Scale one scalar rotation magnitude through the active rotation scale. - inline double scaleVirtualRotation(const double magnitude) const - { - return magnitude * getRotationSpeedScale(); - } - /// @brief Scale one rotation vector through the active rotation scale. - template - inline hlsl::camera_vector_t scaleVirtualRotation(const hlsl::camera_vector_t& magnitude) const - { - return magnitude * static_cast(getRotationSpeedScale()); - } - /// @brief Create a scoped helper that restores the previous motion scales on destruction. - inline SScopedMotionScaleOverride overrideMotionScales(const double moveScale, const double rotationScale) - { - return SScopedMotionScaleOverride(this, moveScale, rotationScale); - } - -protected: - SMotionConfig m_motionConfig; -}; - -} - -#endif // _I_CAMERA_HPP_ diff --git a/common/include/camera/IGimbal.hpp b/common/include/camera/IGimbal.hpp deleted file mode 100644 index 168ff394a..000000000 --- a/common/include/camera/IGimbal.hpp +++ /dev/null @@ -1,361 +0,0 @@ -#ifndef _NBL_IGIMBAL_HPP_ -#define _NBL_IGIMBAL_HPP_ - -#include "CCameraMathUtilities.hpp" -#include "CVirtualGimbalEvent.hpp" - -namespace nbl::core -{ - /// @brief Optional rigid reference frame used to reinterpret a frame of semantic camera input. - /// - /// Some camera consumers replay authored input relative to an external frame - /// instead of the current camera pose. This bundle stores the rigid transform - /// and its orientation in a form ready for `IGimbal::transform(...)`. - struct CReferenceTransform - { - hlsl::float64_t4x4 frame; - hlsl::camera_quaternion_t orientation = hlsl::CCameraMathUtilities::makeIdentityQuaternion(); - }; - - /// @brief Generic world-space gimbal used by runtime cameras and tracked targets. - /// - /// The gimbal stores position, orientation, scale, and an orthonormal local - /// basis. It also exposes `accumulate(...)`, which converts one batch of - /// semantic `CVirtualGimbalEvent` values into translation, rotation, and - /// scale impulses for a single manipulation step. - template - requires is_any_of_v - class IGimbal - { - public: - using precision_t = T; - using quaternion_t = hlsl::camera_quaternion_t; - template - using vector_t = hlsl::camera_vector_t; - /// @brief underlying type for world matrix (TRS) - using model_matrix_t = hlsl::matrix; - - /// @brief One frame of accumulated virtual translation, rotation, and scaling intent. - struct VirtualImpulse - { - vector_t<3u> dVirtualTranslate { 0.0f }, dVirtualRotation { 0.0f }, dVirtualScale { 1.0f }; - }; - - /// @brief Accumulates one frame of virtual events into a translation/rotation/scale impulse. - template - VirtualImpulse accumulate(std::span virtualEvents, const vector_t<3u>& gRightOverride, const vector_t<3u>& gUpOverride, const vector_t<3u>& gForwardOverride) - { - VirtualImpulse impulse; - - for (const auto& event : virtualEvents) - { - assert(event.magnitude >= 0); - - // translation events - if constexpr (AllowedEvents & CVirtualGimbalEvent::MoveRight) - if (event.type == CVirtualGimbalEvent::MoveRight) - impulse.dVirtualTranslate.x += static_cast(event.magnitude); - - if constexpr (AllowedEvents & CVirtualGimbalEvent::MoveLeft) - if (event.type == CVirtualGimbalEvent::MoveLeft) - impulse.dVirtualTranslate.x -= static_cast(event.magnitude); - - if constexpr (AllowedEvents & CVirtualGimbalEvent::MoveUp) - if (event.type == CVirtualGimbalEvent::MoveUp) - impulse.dVirtualTranslate.y += static_cast(event.magnitude); - - if constexpr (AllowedEvents & CVirtualGimbalEvent::MoveDown) - if (event.type == CVirtualGimbalEvent::MoveDown) - impulse.dVirtualTranslate.y -= static_cast(event.magnitude); - - if constexpr (AllowedEvents & CVirtualGimbalEvent::MoveForward) - if (event.type == CVirtualGimbalEvent::MoveForward) - impulse.dVirtualTranslate.z += static_cast(event.magnitude); - - if constexpr (AllowedEvents & CVirtualGimbalEvent::MoveBackward) - if (event.type == CVirtualGimbalEvent::MoveBackward) - impulse.dVirtualTranslate.z -= static_cast(event.magnitude); - - // rotation events - if constexpr (AllowedEvents & CVirtualGimbalEvent::TiltUp) - if (event.type == CVirtualGimbalEvent::TiltUp) - impulse.dVirtualRotation.x += static_cast(event.magnitude); - - if constexpr (AllowedEvents & CVirtualGimbalEvent::TiltDown) - if (event.type == CVirtualGimbalEvent::TiltDown) - impulse.dVirtualRotation.x -= static_cast(event.magnitude); - - if constexpr (AllowedEvents & CVirtualGimbalEvent::PanRight) - if (event.type == CVirtualGimbalEvent::PanRight) - impulse.dVirtualRotation.y += static_cast(event.magnitude); - - if constexpr (AllowedEvents & CVirtualGimbalEvent::PanLeft) - if (event.type == CVirtualGimbalEvent::PanLeft) - impulse.dVirtualRotation.y -= static_cast(event.magnitude); - - if constexpr (AllowedEvents & CVirtualGimbalEvent::RollRight) - if (event.type == CVirtualGimbalEvent::RollRight) - impulse.dVirtualRotation.z += static_cast(event.magnitude); - - if constexpr (AllowedEvents & CVirtualGimbalEvent::RollLeft) - if (event.type == CVirtualGimbalEvent::RollLeft) - impulse.dVirtualRotation.z -= static_cast(event.magnitude); - - // scaling events - if constexpr (AllowedEvents & CVirtualGimbalEvent::ScaleXInc) - if (event.type == CVirtualGimbalEvent::ScaleXInc) - impulse.dVirtualScale.x *= static_cast(event.magnitude); - - if constexpr (AllowedEvents & CVirtualGimbalEvent::ScaleXDec) - if (event.type == CVirtualGimbalEvent::ScaleXDec) - impulse.dVirtualScale.x *= static_cast(event.magnitude); - - if constexpr (AllowedEvents & CVirtualGimbalEvent::ScaleYInc) - if (event.type == CVirtualGimbalEvent::ScaleYInc) - impulse.dVirtualScale.y *= static_cast(event.magnitude); - - if constexpr (AllowedEvents & CVirtualGimbalEvent::ScaleYDec) - if (event.type == CVirtualGimbalEvent::ScaleYDec) - impulse.dVirtualScale.y *= static_cast(event.magnitude); - - if constexpr (AllowedEvents & CVirtualGimbalEvent::ScaleZInc) - if (event.type == CVirtualGimbalEvent::ScaleZInc) - impulse.dVirtualScale.z *= static_cast(event.magnitude); - - if constexpr (AllowedEvents & CVirtualGimbalEvent::ScaleZDec) - if (event.type == CVirtualGimbalEvent::ScaleZDec) - impulse.dVirtualScale.z *= static_cast(event.magnitude); - } - - return impulse; - } - - /// @brief Accumulate one frame of virtual events using the current gimbal basis as the reference frame. - template - VirtualImpulse accumulate(std::span virtualEvents) - { - return accumulate(virtualEvents, getXAxis(), getYAxis(), getZAxis()); - } - - /// @brief Construction-time pose for one gimbal instance. - struct SCreationParameters - { - vector_t<3u> position; - quaternion_t orientation = hlsl::CCameraMathUtilities::makeIdentityQuaternion(); - }; - - IGimbal(const IGimbal&) = default; - IGimbal(IGimbal&&) noexcept = default; - IGimbal& operator=(const IGimbal&) = default; - IGimbal& operator=(IGimbal&&) noexcept = default; - - IGimbal(SCreationParameters&& parameters) - : m_position(parameters.position), m_orientation(parameters.orientation) - { - updateOrthonormalOrientationBase(); - } - - /// @brief Enter manipulation mode and reset the per-frame manipulation counter. - void begin() - { - m_isManipulating = true; - m_counter = 0u; - } - - /// @brief Replace the world-space position while the gimbal is in manipulation mode. - inline void setPosition(const vector_t<3u>& position) - { - assert(m_isManipulating); - - if (m_position != position) - m_counter++; - - m_position = position; - } - - /// @brief Replace the scale component stored by the gimbal. - inline void setScale(const vector_t<3u>& scale) - { - m_scale = scale; - } - - /// @brief Replace the orientation while keeping the orthonormal basis normalized. - inline void setOrientation(const quaternion_t& orientation) - { - assert(m_isManipulating); - - if (m_orientation.data != orientation.data) - m_counter++; - - m_orientation = hlsl::CCameraMathUtilities::normalizeQuaternion(orientation); - updateOrthonormalOrientationBase(); - } - - /// @brief Apply a prebuilt rigid reference transform and an accumulated impulse in one step. - inline void transform(const CReferenceTransform& reference, const VirtualImpulse& impulse) - { - setOrientation(reference.orientation * hlsl::CCameraMathUtilities::makeQuaternionFromEulerRadiansYXZ(impulse.dVirtualRotation)); - setPosition(hlsl::mul(hlsl::float64_t4(impulse.dVirtualTranslate, 1), reference.frame).xyz); - } - - /// @brief Rotate the gimbal around a world-space axis by the requested angle in radians. - inline void rotate(const vector_t<3u>& axis, float dRadians) - { - assert(m_isManipulating); - - if(dRadians) - m_counter++; - - const auto dRotation = hlsl::CCameraMathUtilities::makeQuaternionFromAxisAngle(axis, static_cast(dRadians)); - m_orientation = hlsl::CCameraMathUtilities::normalizeQuaternion(dRotation * m_orientation); - updateOrthonormalOrientationBase(); - } - - /// @brief Translate the gimbal directly in world space. - inline void move(vector_t<3u> delta) - { - assert(m_isManipulating); - - auto newPosition = m_position + delta; - - if (newPosition != m_position) - m_counter++; - - m_position = newPosition; - } - - /// @brief Translate the gimbal along its local right axis. - inline void strafe(precision_t distance) - { - move(getXAxis() * distance); - } - - /// @brief Translate the gimbal along its local up axis. - inline void climb(precision_t distance) - { - move(getYAxis() * distance); - } - - /// @brief Translate the gimbal along its local forward axis. - inline void advance(precision_t distance) - { - move(getZAxis() * distance); - } - - /// @brief Leave manipulation mode after all pose updates for the current frame are finished. - inline void end() - { - m_isManipulating = false; - } - - /// @brief Position of gimbal in world space - inline const auto& getPosition() const { return m_position; } - - /// @brief Orientation of gimbal - inline const auto& getOrientation() const { return m_orientation; } - - /// @brief Scale transform component - inline const auto& getScale() const { return m_scale; } - - /// @brief World matrix (TRS) - template - requires is_any_of_v> - const TRS operator()() const - { - const auto& position = getPosition(); - const auto& rotation = getOrthonornalMatrix(); - const auto& scale = getScale(); - - if constexpr (std::is_same_v) - { - return - { - hlsl::camera_vector_t(rotation[0] * scale.x, position.x), - hlsl::camera_vector_t(rotation[1] * scale.y, position.y), - hlsl::camera_vector_t(rotation[2] * scale.z, position.z) - }; - } - else - { - return - { - hlsl::camera_vector_t(rotation[0] * scale.x, T(0)), - hlsl::camera_vector_t(rotation[1] * scale.y, T(0)), - hlsl::camera_vector_t(rotation[2] * scale.z, T(0)), - hlsl::camera_vector_t(position, T(1)) - }; - } - } - - /// @brief Orthonormal [getXAxis(), getYAxis(), getZAxis()] orientation matrix - inline const auto& getOrthonornalMatrix() const { return m_orthonormal; } - - /// @brief Base "right" vector in orthonormal orientation basis (X-axis) - inline const auto& getXAxis() const { return m_orthonormal[0u]; } - - /// @brief Base "up" vector in orthonormal orientation basis (Y-axis) - inline const auto& getYAxis() const { return m_orthonormal[1u]; } - - /// @brief Base "forward" vector in orthonormal orientation basis (Z-axis) - inline const auto& getZAxis() const { return m_orthonormal[2u]; } - - /// @brief Target vector in local space, alias for getZAxis() - inline const auto getLocalTarget() const { return getZAxis(); } - - /// @brief Target vector in world space - inline const auto getWorldTarget() const { return getPosition() + getLocalTarget(); } - - /// @brief Counts how many times a valid manipulation has been performed, the counter resets when begin() is called - inline const auto& getManipulationCounter() { return m_counter; } - - /// @brief Returns true if gimbal records a manipulation - inline bool isManipulating() const { return m_isManipulating; } - - /// @brief Build a rigid reference transform either from an external frame or from the current gimbal pose. - bool extractReferenceTransform(CReferenceTransform* out, const hlsl::float64_t4x4* referenceFrame = nullptr) - { - if (not out) - return false; - - if (referenceFrame) - { - if (!hlsl::CCameraMathUtilities::tryBuildRigidFrameFromTransform(*referenceFrame, out->frame, out->orientation)) - return false; - } - else - { - out->orientation = getOrientation(); - out->frame = hlsl::CCameraMathUtilities::composeTransformMatrix(getPosition(), out->orientation); - } - - return true; - } - - private: - inline void updateOrthonormalOrientationBase() - { - m_orthonormal = hlsl::CCameraMathUtilities::getQuaternionBasisMatrix(m_orientation); - } - - /// @brief Position of a gimbal in world space - vector_t<3u> m_position; - - /// @brief Normalized orientation of gimbal - quaternion_t m_orientation; - - /// @brief Scale transform component - vector_t<3u> m_scale = { 1.f, 1.f , 1.f }; - - /// @brief Orthonormal basis reconstructed from the current orientation. - hlsl::matrix m_orthonormal; - - /// @brief Counter that increments for each performed manipulation, resets with each begin() call - size_t m_counter = {}; - - /// @brief Tracks whether gimbal is currently in manipulation mode - bool m_isManipulating = false; - - }; -} // namespace nbl::core - -#endif // _NBL_IGIMBAL_HPP_ diff --git a/common/include/camera/IGimbalBindingLayout.hpp b/common/include/camera/IGimbalBindingLayout.hpp deleted file mode 100644 index c1b09427b..000000000 --- a/common/include/camera/IGimbalBindingLayout.hpp +++ /dev/null @@ -1,131 +0,0 @@ -#ifndef _NBL_I_GIMBAL_BINDING_LAYOUT_HPP_ -#define _NBL_I_GIMBAL_BINDING_LAYOUT_HPP_ - -#include -#include - -#include "CVirtualGimbalEvent.hpp" -#include "nbl/ui/KeyCodes.h" - -namespace nbl::ui -{ - -/// @brief Static mapping from external input domains to virtual gimbal events. -/// -/// This type stores binding layout only. It does not process runtime input. -/// Each binding chooses both the semantic virtual event type and the gain used -/// to convert raw producer values into `CVirtualGimbalEvent::magnitude`. -struct IGimbalBindingLayout -{ - IGimbalBindingLayout() {} - virtual ~IGimbalBindingLayout() {} - - using gimbal_event_t = core::CVirtualGimbalEvent; - using encode_keyboard_code_t = ui::E_KEY_CODE; - using encode_mouse_code_t = ui::E_MOUSE_CODE; - using encode_imguizmo_code_t = gimbal_event_t::VirtualEventType; - - enum BindingDomain : uint8_t - { - Keyboard, - Mouse, - Imguizmo, - - Count - }; - - struct CKeyInfo - { - union - { - encode_keyboard_code_t keyboardCode; - encode_mouse_code_t mouseCode; - encode_imguizmo_code_t imguizmoCode; - }; - - CKeyInfo(encode_keyboard_code_t code) : keyboardCode(code), type(Keyboard) {} - CKeyInfo(encode_mouse_code_t code) : mouseCode(code), type(Mouse) {} - CKeyInfo(encode_imguizmo_code_t code) : imguizmoCode(code), type(Imguizmo) {} - - BindingDomain type; - }; - - struct CHashInfo - { - static inline constexpr double DefaultMagnitudeScale = 1.0; - - CHashInfo() {} - CHashInfo(gimbal_event_t::VirtualEventType _type, const double _magnitudeScale = DefaultMagnitudeScale) - : event({ .type = _type }), magnitudeScale(_magnitudeScale) {} - ~CHashInfo() = default; - - /// @brief Virtual event emitted by this binding. - gimbal_event_t event = {}; - /// @brief Per-binding gain applied when raw input is converted into one virtual-event magnitude. - double magnitudeScale = DefaultMagnitudeScale; - /// @brief Runtime latch used by held keyboard and mouse-button bindings. - bool active = false; - }; - - using keyboard_to_virtual_events_t = std::unordered_map; - using mouse_to_virtual_events_t = std::unordered_map; - using imguizmo_to_virtual_events_t = std::unordered_map; - - virtual const keyboard_to_virtual_events_t& getKeyboardVirtualEventMap() const = 0; - virtual const mouse_to_virtual_events_t& getMouseVirtualEventMap() const = 0; - virtual const imguizmo_to_virtual_events_t& getImguizmoVirtualEventMap() const = 0; - - virtual void updateKeyboardMapping(const std::function& mapKeys) = 0; - virtual void updateMouseMapping(const std::function& mapKeys) = 0; - virtual void updateImguizmoMapping(const std::function& mapKeys) = 0; - - inline void copyBindingLayoutFrom(const IGimbalBindingLayout& layout) - { - updateKeyboardMapping([&](auto& map) { map = sanitizeMapping(layout.getKeyboardVirtualEventMap()); }); - updateMouseMapping([&](auto& map) { map = sanitizeMapping(layout.getMouseVirtualEventMap()); }); - updateImguizmoMapping([&](auto& map) { map = sanitizeMapping(layout.getImguizmoVirtualEventMap()); }); - } - - inline void copyBindingLayoutTo(IGimbalBindingLayout& layout) const - { - layout.updateKeyboardMapping([&](auto& map) { map = sanitizeMapping(getKeyboardVirtualEventMap()); }); - layout.updateMouseMapping([&](auto& map) { map = sanitizeMapping(getMouseVirtualEventMap()); }); - layout.updateImguizmoMapping([&](auto& map) { map = sanitizeMapping(getImguizmoVirtualEventMap()); }); - } - -protected: - template - inline static Map sanitizeMapping(const Map& source) - { - Map result; - for (const auto& [code, hash] : source) - result.emplace(code, typename Map::mapped_type(hash.event.type, hash.magnitudeScale)); - return result; - } -}; - -class CGimbalBindingLayoutStorage : public IGimbalBindingLayout -{ -public: - /// @brief Mutable storage for active or preset binding layout. - using IGimbalBindingLayout::IGimbalBindingLayout; - - CGimbalBindingLayoutStorage() {} - virtual ~CGimbalBindingLayoutStorage() {} - - virtual void updateKeyboardMapping(const std::function& mapKeys) override { mapKeys(m_keyboardVirtualEventMap); } - virtual void updateMouseMapping(const std::function& mapKeys) override { mapKeys(m_mouseVirtualEventMap); } - virtual void updateImguizmoMapping(const std::function& mapKeys) override { mapKeys(m_imguizmoVirtualEventMap); } - - virtual const keyboard_to_virtual_events_t& getKeyboardVirtualEventMap() const override { return m_keyboardVirtualEventMap; } - virtual const mouse_to_virtual_events_t& getMouseVirtualEventMap() const override { return m_mouseVirtualEventMap; } - virtual const imguizmo_to_virtual_events_t& getImguizmoVirtualEventMap() const override { return m_imguizmoVirtualEventMap; } - - keyboard_to_virtual_events_t m_keyboardVirtualEventMap; - mouse_to_virtual_events_t m_mouseVirtualEventMap; - imguizmo_to_virtual_events_t m_imguizmoVirtualEventMap; -}; - -} // namespace nbl::ui - -#endif diff --git a/common/include/camera/IGimbalInputProcessor.hpp b/common/include/camera/IGimbalInputProcessor.hpp deleted file mode 100644 index 4e6f7bfd4..000000000 --- a/common/include/camera/IGimbalInputProcessor.hpp +++ /dev/null @@ -1,438 +0,0 @@ -#ifndef _NBL_I_GIMBAL_INPUT_PROCESSOR_HPP_ -#define _NBL_I_GIMBAL_INPUT_PROCESSOR_HPP_ - -#include -#include - -#include "nbl/ui/KeyCodes.h" -#include "nbl/ui/SInputEvent.h" - -#include "IGimbalBindingLayout.hpp" - -namespace nbl::ui -{ - -/// @brief Runtime processor that turns keyboard, mouse, and ImGuizmo input into virtual events. -/// -/// Held keyboard and mouse-button bindings emit `frameDeltaSeconds * magnitudeScale`. -/// Relative mouse movement, mouse scroll, and ImGuizmo deltas emit -/// `abs(rawDelta) * magnitudeScale` per bound axis. The result is written into -/// `CVirtualGimbalEvent::magnitude`. -class IGimbalInputProcessor : public CGimbalBindingLayoutStorage -{ -public: - struct SInputProcessorDefaults final - { - /// @brief Largest frame interval, in seconds, accepted from held-input accumulation. - static inline constexpr double MaxFrameDeltaSeconds = 0.2; - static inline constexpr float ZeroPivot = 0.0f; - static inline constexpr float UnitPivot = 1.0f; - }; - static inline constexpr double MaxFrameDeltaSeconds = SInputProcessorDefaults::MaxFrameDeltaSeconds; - static inline constexpr float ZeroPivot = SInputProcessorDefaults::ZeroPivot; - static inline constexpr float UnitPivot = SInputProcessorDefaults::UnitPivot; - - using CGimbalBindingLayoutStorage::CGimbalBindingLayoutStorage; - - IGimbalInputProcessor() = default; - virtual ~IGimbalInputProcessor() = default; - - /// @brief Keyboard events consumed by the processor. - using input_keyboard_event_t = ui::SKeyboardEvent; - - /// @brief Mouse events consumed by the processor. - using input_mouse_event_t = ui::SMouseEvent; - - /// @brief ImGuizmo world-space delta transforms consumed by the processor. - using input_imguizmo_event_t = hlsl::float32_t4x4; - - void beginInputProcessing(const std::chrono::microseconds nextPresentationTimeStamp) - { - m_nextPresentationTimeStamp = nextPresentationTimeStamp; - m_frameDeltaSeconds = clampFrameDeltaTimeSeconds(m_nextPresentationTimeStamp, m_lastVirtualUpTimeStamp); - } - - void endInputProcessing() - { - m_lastVirtualUpTimeStamp = m_nextPresentationTimeStamp; - } - - struct SUpdateParameters - { - std::span keyboardEvents = {}; - std::span mouseEvents = {}; - std::span imguizmoEvents = {}; - }; - - /// @brief Process combined events from `SUpdateParameters` into virtual manipulation events. - /// - /// @note This function combines keyboard, mouse, and ImGuizmo processing. - /// It delegates the actual work to `processKeyboard`, `processMouse`, and - /// `processImguizmo`, then accumulates their output and total count. - /// - /// @param output Pointer to the destination array for generated gimbal events. - /// Pass `nullptr` to query only the total event count. - /// @param count Output total number of generated gimbal events. - /// @param parameters Individual keyboard, mouse, and ImGuizmo input spans. - void process(gimbal_event_t* output, uint32_t& count, const SUpdateParameters parameters = {}) - { - count = 0u; - uint32_t vKeyboardEventsCount = {}, vMouseEventsCount = {}, vImguizmoEventsCount = {}; - - if (output) - { - processKeyboard(output, vKeyboardEventsCount, parameters.keyboardEvents); output += vKeyboardEventsCount; - processMouse(output, vMouseEventsCount, parameters.mouseEvents); output += vMouseEventsCount; - processImguizmo(output, vImguizmoEventsCount, parameters.imguizmoEvents); - } - else - { - processKeyboard(nullptr, vKeyboardEventsCount, {}); - processMouse(nullptr, vMouseEventsCount, {}); - processImguizmo(nullptr, vImguizmoEventsCount, {}); - } - - count = vKeyboardEventsCount + vMouseEventsCount + vImguizmoEventsCount; - } - - /// @brief Process keyboard events into virtual manipulation events. - /// - /// @note This function maps keyboard press and release events into virtual - /// gimbal manipulation events through the active keyboard bindings. - /// Held keys contribute elapsed seconds scaled by the binding gain. - /// - /// @param output Pointer to the destination array for generated gimbal events. - /// Pass `nullptr` to query only the total event count. - /// @param count Output number of generated gimbal events. - /// @param events Keyboard events to process. - void processKeyboard(gimbal_event_t* output, uint32_t& count, std::span events) - { - processBindingMap( - m_keyboardVirtualEventMap, - output, - count, - [&](auto& map) - { - for (const auto& keyboardEvent : events) - { - if (keyboardEvent.action == input_keyboard_event_t::ECA_PRESSED) - setBindingActiveState(map, keyboardEvent.keyCode, true); - else if (keyboardEvent.action == input_keyboard_event_t::ECA_RELEASED) - setBindingActiveState(map, keyboardEvent.keyCode, false); - } - }); - } - - /// @brief Process mouse events into virtual manipulation events. - /// - /// @note This function maps mouse clicks, scrolls, and movements into - /// virtual gimbal manipulation events through the active mouse bindings. - /// Relative movement and scroll contribute absolute signed deltas scaled by - /// the matching binding gain. - /// - /// @param output Pointer to the destination array for generated gimbal events. - /// Pass `nullptr` to query only the total event count. - /// @param count Output number of generated gimbal events. - /// @param events Mouse events to process. - void processMouse(gimbal_event_t* output, uint32_t& count, std::span events) - { - processBindingMap( - m_mouseVirtualEventMap, - output, - count, - [&](auto& map) - { - for (const auto& mouseEvent : events) - { - switch (mouseEvent.type) - { - case input_mouse_event_t::EET_CLICK: - updateMouseButtonState(map, mouseEvent.clickEvent); - break; - - case input_mouse_event_t::EET_SCROLL: - requestMagnitudeUpdateWithSignedComponents( - ZeroPivot, - hlsl::float32_t2( - static_cast(mouseEvent.scrollEvent.verticalScroll), - mouseEvent.scrollEvent.horizontalScroll), - SInputProcessorBindingGroups::MouseScroll, - map); - break; - - case input_mouse_event_t::EET_MOVEMENT: - requestMagnitudeUpdateWithSignedComponents( - ZeroPivot, - hlsl::float32_t2( - mouseEvent.movementEvent.relativeMovementX, - mouseEvent.movementEvent.relativeMovementY), - SInputProcessorBindingGroups::MouseRelativeMovement, - map); - break; - - default: - break; - } - } - }); - } - - /// @brief Process ImGuizmo transforms into virtual gimbal events. - /// - /// @note This function converts world-space delta transforms authored by - /// ImGuizmo into translation, rotation, and scale virtual events. - /// Translation uses world-space delta components. Rotation uses extracted - /// Euler radians. Scale uses multiplicative components around pivot `1`. - /// - /// @param output Pointer to the destination array for generated gimbal events. - /// Pass `nullptr` to query only the total event count. - /// @param count Output number of generated gimbal events. - /// @param events ImGuizmo delta transforms to process. - void processImguizmo(gimbal_event_t* output, uint32_t& count, std::span events) - { - processBindingMap( - m_imguizmoVirtualEventMap, - output, - count, - [&](auto& map) - { - for (const auto& ev : events) - { - const auto& deltaWorldTRS = ev; - - hlsl::SRigidTransformComponents world = {}; - if (!hlsl::CCameraMathUtilities::tryExtractRigidTransformComponents(deltaWorldTRS, world)) - continue; - - requestMagnitudeUpdateWithSignedComponents( - ZeroPivot, - world.translation, - SInputProcessorBindingGroups::ImguizmoTranslation, - map); - - const auto dRotationRad = hlsl::CCameraMathUtilities::getCameraOrientationEulerRadians(world.orientation); - requestMagnitudeUpdateWithSignedComponents( - ZeroPivot, - dRotationRad, - SInputProcessorBindingGroups::ImguizmoRotation, - map); - - requestMagnitudeUpdateWithSignedComponents( - UnitPivot, - world.scale, - SInputProcessorBindingGroups::ImguizmoScale, - map); - } - }); - } - -private: - template - struct SEncodedAxisBindingGroup final - { - std::array positive = {}; - std::array negative = {}; - }; - - struct SInputProcessorBindingGroups final - { - static inline constexpr SEncodedAxisBindingGroup MouseScroll = { - .positive = { - ui::EMC_VERTICAL_POSITIVE_SCROLL, - ui::EMC_HORIZONTAL_POSITIVE_SCROLL - }, - .negative = { - ui::EMC_VERTICAL_NEGATIVE_SCROLL, - ui::EMC_HORIZONTAL_NEGATIVE_SCROLL - } - }; - - static inline constexpr SEncodedAxisBindingGroup MouseRelativeMovement = { - .positive = { - ui::EMC_RELATIVE_POSITIVE_MOVEMENT_X, - ui::EMC_RELATIVE_POSITIVE_MOVEMENT_Y - }, - .negative = { - ui::EMC_RELATIVE_NEGATIVE_MOVEMENT_X, - ui::EMC_RELATIVE_NEGATIVE_MOVEMENT_Y - } - }; - - static inline constexpr SEncodedAxisBindingGroup ImguizmoTranslation = { - .positive = { - gimbal_event_t::MoveRight, - gimbal_event_t::MoveUp, - gimbal_event_t::MoveForward - }, - .negative = { - gimbal_event_t::MoveLeft, - gimbal_event_t::MoveDown, - gimbal_event_t::MoveBackward - } - }; - - static inline constexpr SEncodedAxisBindingGroup ImguizmoRotation = { - .positive = { - gimbal_event_t::TiltUp, - gimbal_event_t::PanRight, - gimbal_event_t::RollRight - }, - .negative = { - gimbal_event_t::TiltDown, - gimbal_event_t::PanLeft, - gimbal_event_t::RollLeft - } - }; - - static inline constexpr SEncodedAxisBindingGroup ImguizmoScale = { - .positive = { - gimbal_event_t::ScaleXInc, - gimbal_event_t::ScaleYInc, - gimbal_event_t::ScaleZInc - }, - .negative = { - gimbal_event_t::ScaleXDec, - gimbal_event_t::ScaleYDec, - gimbal_event_t::ScaleZDec - } - }; - }; - - static double clampFrameDeltaTimeSeconds( - const std::chrono::microseconds nextPresentationTimeStamp, - const std::chrono::microseconds lastVirtualUpTimeStamp) - { - const auto deltaSeconds = std::chrono::duration( - nextPresentationTimeStamp - lastVirtualUpTimeStamp).count(); - if (deltaSeconds < 0.0) - return 0.0; - return std::min(deltaSeconds, MaxFrameDeltaSeconds); - } - - template - void processBindingMap(Map& map, gimbal_event_t* output, uint32_t& count, ConsumeFn&& consume) - { - count = 0u; - const auto mappedVirtualEventsCount = static_cast(map.size()); - if (!output) - { - count = mappedVirtualEventsCount; - return; - } - if (!mappedVirtualEventsCount) - return; - - preprocess(map); - consume(map); - postprocess(map, output, count); - } - - static bool tryGetMouseButtonCode( - const ui::E_MOUSE_BUTTON button, - ui::E_MOUSE_CODE& outCode) - { - switch (button) - { - case ui::EMB_LEFT_BUTTON: outCode = ui::EMC_LEFT_BUTTON; return true; - case ui::EMB_RIGHT_BUTTON: outCode = ui::EMC_RIGHT_BUTTON; return true; - case ui::EMB_MIDDLE_BUTTON: outCode = ui::EMC_MIDDLE_BUTTON; return true; - case ui::EMB_BUTTON_4: outCode = ui::EMC_BUTTON_4; return true; - case ui::EMB_BUTTON_5: outCode = ui::EMC_BUTTON_5; return true; - default: - return false; - } - } - - template - void updateMouseButtonState(Map& map, const input_mouse_event_t::SClickEvent& clickEvent) - { - ui::E_MOUSE_CODE mouseCode = ui::EMC_NONE; - if (!tryGetMouseButtonCode(clickEvent.mouseButton, mouseCode)) - return; - - if (clickEvent.action == input_mouse_event_t::SClickEvent::EA_PRESSED) - setBindingActiveState(map, mouseCode, true); - else if (clickEvent.action == input_mouse_event_t::SClickEvent::EA_RELEASED) - setBindingActiveState(map, mouseCode, false); - } - - template - void setBindingActiveState(Map& map, const Code code, const bool active) - { - const auto request = map.find(code); - if (request == map.end()) - return; - - request->second.active = active; - } - - void preprocess(auto& map) - { - for (auto& [key, hash] : map) - { - hash.event.magnitude = 0.0f; - - if (hash.active) - hash.event.magnitude = m_frameDeltaSeconds * hash.magnitudeScale; - } - } - - void postprocess(const auto& map, gimbal_event_t* output, uint32_t& count) - { - for (const auto& [key, hash] : map) - if (hash.event.magnitude) - { - auto* virtualEvent = output + count; - virtualEvent->type = hash.event.type; - virtualEvent->magnitude = hash.event.magnitude; - ++count; - } - } - - template - void requestMagnitudeUpdateWithScalar(float signPivot, float dScalar, EncodeType positive, EncodeType negative, Map& map) - { - if (dScalar != signPivot) - { - const auto dMagnitude = hlsl::abs(dScalar); - auto code = (dScalar > signPivot) ? positive : negative; - auto request = map.find(code); - if (request != map.end()) - request->second.event.magnitude += dMagnitude * request->second.magnitudeScale; - } - } - - template - void requestMagnitudeUpdateWithSignedComponents( - float signPivot, - const hlsl::vector& components, - const std::array& positive, - const std::array& negative, - Map& map) - { - for (uint32_t i = 0u; i < N; ++i) - requestMagnitudeUpdateWithScalar(signPivot, components[i], positive[i], negative[i], map); - } - - template - void requestMagnitudeUpdateWithSignedComponents( - float signPivot, - const hlsl::vector& components, - const SEncodedAxisBindingGroup& bindings, - Map& map) - { - requestMagnitudeUpdateWithSignedComponents( - signPivot, - components, - bindings.positive, - bindings.negative, - map); - } - - double m_frameDeltaSeconds = {}; - std::chrono::microseconds m_nextPresentationTimeStamp = {}, m_lastVirtualUpTimeStamp = {}; -}; - -} // namespace nbl::ui - -#endif // _NBL_I_GIMBAL_INPUT_PROCESSOR_HPP_ diff --git a/common/include/camera/ILinearProjection.hpp b/common/include/camera/ILinearProjection.hpp deleted file mode 100644 index 5c229c7a7..000000000 --- a/common/include/camera/ILinearProjection.hpp +++ /dev/null @@ -1,182 +0,0 @@ -#ifndef _NBL_I_LINEAR_PROJECTION_HPP_ -#define _NBL_I_LINEAR_PROJECTION_HPP_ - -#include "IProjection.hpp" -#include "ICamera.hpp" - -namespace nbl::core -{ - -/// @brief Interface for any custom linear projection transformation. -/// -/// Matrix elements are already evaluated scalars referencing a camera. -/// This covers perspective, orthographic, oblique, axonometric, and shear projections. -class ILinearProjection : virtual public core::IReferenceCounted -{ -protected: - ILinearProjection(core::smart_refctd_ptr&& camera) - : m_camera(core::smart_refctd_ptr(camera)) {} - virtual ~ILinearProjection() = default; - - core::smart_refctd_ptr m_camera; -public: - /// @brief World transform type expected by the linear projection helpers. - using model_matrix_t = typename decltype(m_camera)::pointee::CGimbal::model_matrix_t; - - /// @brief Matrix type used for fully concatenated linear transforms. - using concatenated_matrix_t = hlsl::float64_t4x4; - - /// @brief Optional inverse of a concatenated transform when the matrix is not singular. - using inv_concatenated_matrix_t = std::optional; - - /// @brief One concrete linear projection matrix together with cached inverse metadata. - struct CProjection : public IProjection - { - using IProjection::IProjection; - using projection_matrix_t = concatenated_matrix_t; - using inv_projection_matrix_t = inv_concatenated_matrix_t; - - CProjection() : CProjection(projection_matrix_t(1)) {} - CProjection(const projection_matrix_t& matrix) { setProjectionMatrix(matrix); } - - /// @brief Returns P (Projection matrix) - inline const projection_matrix_t& getProjectionMatrix() const { return m_projectionMatrix; } - - /// @brief Returns Pâ»Â¹ (Inverse of Projection matrix) *if it exists* - inline const inv_projection_matrix_t& getInvProjectionMatrix() const { return m_invProjectionMatrix; } - - inline const std::optional& isProjectionLeftHanded() const { return m_isProjectionLeftHanded; } - inline bool isProjectionSingular() const { return m_isProjectionSingular; } - virtual ProjectionType getProjectionType() const override { return ProjectionType::Linear; } - - virtual void project(const projection_vector_t& vecToProjectionSpace, projection_vector_t& output) const override - { - output = hlsl::mul(m_projectionMatrix, vecToProjectionSpace); - } - - virtual bool unproject(const projection_vector_t& vecFromProjectionSpace, projection_vector_t& output) const override - { - if (m_isProjectionSingular) - return false; - - output = hlsl::mul(m_invProjectionMatrix.value(), vecFromProjectionSpace); - - return true; - } - - protected: - /// @brief Replace the projection matrix and rebuild cached handedness and inverse information. - inline void setProjectionMatrix(const projection_matrix_t& matrix) - { - m_projectionMatrix = matrix; - const auto det = hlsl::determinant(m_projectionMatrix); - - // we will allow you to lose a dimension since such a projection itself *may* - // be valid, however then you cannot un-project because the inverse doesn't exist! - m_isProjectionSingular = not det; - - if (m_isProjectionSingular) - { - m_isProjectionLeftHanded = std::nullopt; - m_invProjectionMatrix = std::nullopt; - } - else - { - m_isProjectionLeftHanded = det < 0.0; - m_invProjectionMatrix = hlsl::inverse(m_projectionMatrix); - } - } - - private: - projection_matrix_t m_projectionMatrix; - inv_projection_matrix_t m_invProjectionMatrix; - std::optional m_isProjectionLeftHanded; - bool m_isProjectionSingular; - }; - - /// @brief Return the number of linear projection entries owned by the concrete wrapper. - virtual uint32_t getLinearProjectionCount() const = 0; - /// @brief Return one linear projection entry by index. - virtual const CProjection& getLinearProjection(uint32_t index) const = 0; - - /// @brief Replace the camera referenced by this projection wrapper. - inline bool setCamera(core::smart_refctd_ptr&& camera) - { - if (camera) - { - m_camera = camera; - return true; - } - - return false; - } - - /// @brief Return the camera referenced by this projection wrapper. - inline ICamera* getCamera() - { - return m_camera.get(); - } - - /// @brief Compute the model-view matrix. - /// - /// @param model World TRS matrix. - /// @return The model-view matrix. - inline concatenated_matrix_t getMV(const model_matrix_t& model) const - { - const auto& v = m_camera->getGimbal().getViewMatrix(); - return hlsl::mul(hlsl::getMatrix3x4As4x4(v), hlsl::getMatrix3x4As4x4(model)); - } - - /// @brief Compute the model-view-projection matrix from a model matrix. - /// - /// @param projection Linear projection. - /// @param model World TRS matrix. - /// @return The model-view-projection matrix. - inline concatenated_matrix_t getMVP(const CProjection& projection, const model_matrix_t& model) const - { - const auto& v = m_camera->getGimbal().getViewMatrix(); - const auto& p = projection.getProjectionMatrix(); - auto mv = hlsl::mul(hlsl::getMatrix3x4As4x4(v), hlsl::getMatrix3x4As4x4(model)); - return hlsl::mul(p, mv); - } - - /// @brief Compute the model-view-projection matrix from a model-view matrix. - /// - /// @param projection Linear projection. - /// @param mv Model-view matrix. - /// @return The model-view-projection matrix. - inline concatenated_matrix_t getMVP(const CProjection& projection, const concatenated_matrix_t& mv) const - { - const auto& p = projection.getProjectionMatrix(); - return hlsl::mul(p, mv); - } - - /// @brief Compute the inverse model-view matrix. - /// - /// @param model World TRS matrix. - /// @return The inverse model-view matrix when it exists, otherwise `std::nullopt`. - inline inv_concatenated_matrix_t getMVInverse(const model_matrix_t& model) const - { - const auto mv = getMV(model); - if (auto det = hlsl::determinant(mv); det) - return hlsl::inverse(mv); - return std::nullopt; - } - - /// @brief Compute the inverse model-view-projection matrix. - /// - /// @param projection Linear projection. - /// @param model World TRS matrix. - /// @return The inverse model-view-projection matrix when it exists, otherwise `std::nullopt`. - inline inv_concatenated_matrix_t getMVPInverse(const CProjection& projection, const model_matrix_t& model) const - { - const auto mvp = getMVP(projection, model); - if (auto det = hlsl::determinant(mvp); det) - return hlsl::inverse(mvp); - return std::nullopt; - } -}; - -} // nbl::hlsl namespace - -#endif // _NBL_I_LINEAR_PROJECTION_HPP_ diff --git a/common/include/camera/IPerspectiveProjection.hpp b/common/include/camera/IPerspectiveProjection.hpp deleted file mode 100644 index 5806089f8..000000000 --- a/common/include/camera/IPerspectiveProjection.hpp +++ /dev/null @@ -1,64 +0,0 @@ -#ifndef _NBL_I_QUAD_PROJECTION_HPP_ -#define _NBL_I_QUAD_PROJECTION_HPP_ - -#include "ILinearProjection.hpp" - -namespace nbl::core -{ - -/// @brief Interface for quad projections. -/// -/// This projection transforms a vector into the model space of a perspective -/// quad defined by the pre-transform matrix and then projects it onto the quad -/// using the linear viewport transform. -/// -/// A perspective quad projection is represented by: -/// - a pre-transform matrix -/// - a linear viewport transform matrix -/// -/// The final projection matrix is the concatenation of those two transforms. -/// -/// @note One perspective quad projection can represent a face quad of a CAVE-like system. -class IPerspectiveProjection : public ILinearProjection -{ -public: - /// @brief One quad projection entry described by a pretransform and a viewport projection. - struct CProjection : ILinearProjection::CProjection - { - using base_t = ILinearProjection::CProjection; - - CProjection() = default; - CProjection(const ILinearProjection::model_matrix_t& pretransform, ILinearProjection::concatenated_matrix_t viewport) - { - setQuadTransform(pretransform, viewport); - } - - /// @brief Rebuild the concatenated quad projection from its authored components. - inline void setQuadTransform(const ILinearProjection::model_matrix_t& pretransform, ILinearProjection::concatenated_matrix_t viewport) - { - auto concatenated = hlsl::mul(hlsl::getMatrix3x4As4x4(pretransform), viewport); - base_t::setProjectionMatrix(concatenated); - - m_pretransform = pretransform; - m_viewport = viewport; - } - - /// @brief Return the authored pretransform applied before the viewport projection. - inline const ILinearProjection::model_matrix_t& getPretransform() const { return m_pretransform; } - /// @brief Return the authored viewport projection matrix stored for this quad. - inline const ILinearProjection::concatenated_matrix_t& getViewportProjection() const { return m_viewport; } - - private: - ILinearProjection::model_matrix_t m_pretransform = ILinearProjection::model_matrix_t(1); - ILinearProjection::concatenated_matrix_t m_viewport = ILinearProjection::concatenated_matrix_t(1); - }; - -protected: - IPerspectiveProjection(core::smart_refctd_ptr&& camera) - : ILinearProjection(core::smart_refctd_ptr(camera)) {} - virtual ~IPerspectiveProjection() = default; -}; - -} // nbl::hlsl namespace - -#endif // _NBL_I_QUAD_PROJECTION_HPP_ diff --git a/common/include/camera/IPlanarProjection.hpp b/common/include/camera/IPlanarProjection.hpp deleted file mode 100644 index 04f718f39..000000000 --- a/common/include/camera/IPlanarProjection.hpp +++ /dev/null @@ -1,137 +0,0 @@ -#ifndef _NBL_I_PLANAR_PROJECTION_HPP_ -#define _NBL_I_PLANAR_PROJECTION_HPP_ - -#include "IGimbalBindingLayout.hpp" -#include "ILinearProjection.hpp" - -namespace nbl::core -{ - -/// @brief Linear projection wrapper for one camera-facing planar viewport. -/// -/// The projection stores viewport-local binding layouts. Runtime input -/// processing is handled by `CGimbalInputBinder`. -class IPlanarProjection : public ILinearProjection -{ -public: - /// @brief One perspective or orthographic projection entry plus its viewport-local bindings. - struct CProjection : public ILinearProjection::CProjection - { - using base_t = ILinearProjection::CProjection; - - /// @brief Stable runtime classification of supported planar projection parameterizations. - enum ProjectionType : uint8_t - { - Perspective, - Orthographic, - - Count - }; - - template - static CProjection create(Args&&... args) - requires (T != Count) - { - CProjection output; - - if constexpr (T == Perspective) output.setPerspective(std::forward(args)...); - else if (T == Orthographic) output.setOrthographic(std::forward(args)...); - - return output; - } - - CProjection(const CProjection& other) = default; - CProjection(CProjection&& other) noexcept = default; - - /// @brief Authored parameter bundle stored by one planar projection entry. - struct ProjectionParameters - { - ProjectionType m_type; - - union PlanarParameters - { - struct - { - float fov; - } perspective; - - struct - { - float orthoWidth; - } orthographic; - - PlanarParameters() {} - ~PlanarParameters() {} - } m_planar; - - float m_zNear; - float m_zFar; - }; - - /// @brief Rebuild the concrete projection matrix from the stored parameters. - inline void update(bool leftHanded, float aspectRatio) - { - switch (m_parameters.m_type) - { - case Perspective: - { - const auto& fov = m_parameters.m_planar.perspective.fov; - - if (leftHanded) - base_t::setProjectionMatrix(hlsl::buildProjectionMatrixPerspectiveFovLH(hlsl::radians(fov), aspectRatio, m_parameters.m_zNear, m_parameters.m_zFar)); - else - base_t::setProjectionMatrix(hlsl::buildProjectionMatrixPerspectiveFovRH(hlsl::radians(fov), aspectRatio, m_parameters.m_zNear, m_parameters.m_zFar)); - } break; - - case Orthographic: - { - const auto& orthoW = m_parameters.m_planar.orthographic.orthoWidth; - const auto viewHeight = orthoW * core::reciprocal(aspectRatio); - - if (leftHanded) - base_t::setProjectionMatrix(hlsl::buildProjectionMatrixOrthoLH(orthoW, viewHeight, m_parameters.m_zNear, m_parameters.m_zFar)); - else - base_t::setProjectionMatrix(hlsl::buildProjectionMatrixOrthoRH(orthoW, viewHeight, m_parameters.m_zNear, m_parameters.m_zFar)); - } break; - } - } - - /// @brief Switch the entry to perspective mode and store its authored parameters. - inline void setPerspective(float zNear = 0.1f, float zFar = 100.f, float fov = 60.f) - { - m_parameters.m_type = Perspective; - m_parameters.m_planar.perspective.fov = fov; - m_parameters.m_zNear = zNear; - m_parameters.m_zFar = zFar; - } - - /// @brief Switch the entry to orthographic mode and store its authored parameters. - inline void setOrthographic(float zNear = 0.1f, float zFar = 100.f, float orthoWidth = 10.f) - { - m_parameters.m_type = Orthographic; - m_parameters.m_planar.orthographic.orthoWidth = orthoWidth; - m_parameters.m_zNear = zNear; - m_parameters.m_zFar = zFar; - } - - /// @brief Return the authored planar projection parameters. - inline const ProjectionParameters& getParameters() const { return m_parameters; } - /// @brief Return the viewport-local input binding layout stored next to this projection entry. - inline const ui::IGimbalBindingLayout& getInputBinding() const { return m_inputBinding; } - /// @brief Return mutable access to the viewport-local input binding layout. - inline ui::IGimbalBindingLayout& getInputBinding() { return m_inputBinding; } - private: - CProjection() = default; - ProjectionParameters m_parameters; - ui::CGimbalBindingLayoutStorage m_inputBinding; - }; - -protected: - IPlanarProjection(core::smart_refctd_ptr&& camera) - : ILinearProjection(core::smart_refctd_ptr(camera)) {} - virtual ~IPlanarProjection() = default; -}; - -} // namespace nbl::core - -#endif // _NBL_I_PLANAR_PROJECTION_HPP_ diff --git a/common/include/camera/IProjection.hpp b/common/include/camera/IProjection.hpp deleted file mode 100644 index b42cb01a3..000000000 --- a/common/include/camera/IProjection.hpp +++ /dev/null @@ -1,71 +0,0 @@ -#ifndef _NBL_I_PROJECTION_HPP_ -#define _NBL_I_PROJECTION_HPP_ - -#include - -namespace nbl::core -{ - -/// @brief Base interface for any reusable projection model in the camera stack. -/// -/// A projection transforms vectors between some input space and the projection -/// space understood by a concrete viewport or projection consumer. Specialized -/// interfaces such as `ILinearProjection`, `IPlanarProjection`, and -/// `IPerspectiveProjection` refine this abstraction with additional structure. -class IProjection -{ -public: - /// @brief Common vector type used by projection and unprojection operations. - using projection_vector_t = hlsl::float64_t4; - - /// @brief Stable runtime classification of supported projection families. - enum class ProjectionType - { - /// @brief Any raw linear transformation, for example it may represent Perspective, Orthographic, Oblique, Axonometric, Shear projections - Linear, - - /// @brief Specialized linear projection for planar projections with parameters - Planar, - - /// @brief Extension of planar projection represented by pre-transform & planar transform combined projecting onto R3 cave quad - CaveQuad, - - /// @brief Specialized CaveQuad projection, represents planar projections onto cube with 6 quad cube faces - Cube, - - Spherical, - ThinLens, - - Count - }; - - IProjection() = default; - virtual ~IProjection() = default; - - /// @brief Transform a vector from its input space into projection space. - /// - /// @param vecToProjectionSpace Vector to transform into projection space. - /// @param output Result vector in projection space. - virtual void project(const projection_vector_t& vecToProjectionSpace, projection_vector_t& output) const = 0; - - /// @brief Transform a vector from projection space back to the original space. - /// - /// The inverse transform may fail because the original projection may be singular. - /// - /// @param vecFromProjectionSpace Vector in projection space. - /// @param output Result vector in the original space. - /// @return `true` when the inverse transform succeeded, otherwise `false`. - virtual bool unproject(const projection_vector_t& vecFromProjectionSpace, projection_vector_t& output) const = 0; - - /// @brief Return the specific projection family implemented by the concrete instance. - /// - /// Examples include linear, spherical, and thin-lens projections as defined - /// by `ProjectionType`. - /// - /// @return The type of this projection. - virtual ProjectionType getProjectionType() const = 0; -}; - -} // namespace nbl::core - -#endif // _NBL_IPROJECTION_HPP_ diff --git a/common/include/camera/IRange.hpp b/common/include/camera/IRange.hpp deleted file mode 100644 index b4084db2a..000000000 --- a/common/include/camera/IRange.hpp +++ /dev/null @@ -1,21 +0,0 @@ -#ifndef _NBL_IRANGE_HPP_ -#define _NBL_IRANGE_HPP_ - -namespace nbl::core -{ - -/// @brief Minimal concepts used by camera persistence and tooling helpers. -template -concept GeneralPurposeRange = requires -{ - typename std::ranges::range_value_t; -}; - -template -concept ContiguousGeneralPurposeRangeOf = GeneralPurposeRange && -std::ranges::contiguous_range && -std::same_as, T>; - -} // namespace nbl::core - -#endif // _NBL_IRANGE_HPP_ diff --git a/common/include/camera/README.md b/common/include/camera/README.md deleted file mode 100644 index 8c443676c..000000000 --- a/common/include/camera/README.md +++ /dev/null @@ -1,841 +0,0 @@ -# Shared Camera API - -This directory contains the reusable camera stack used by [`61_UI`](../../../61_UI/README.md). - -The stack has two public faces: - -- a runtime face used to move cameras during one frame -- a typed face used to capture, store, restore, compare, replay, and validate camera state - -The runtime face is centered on [`ICamera.hpp`](ICamera.hpp). -The typed face is centered on [`CCameraGoal.hpp`](CCameraGoal.hpp) and [`CCameraGoalSolver.hpp`](CCameraGoalSolver.hpp). - -## TL;DR - -If you want to know which type to touch first, use this table. - -| I want to... | Use | -|---|---| -| apply one absolute rigid pose request at runtime | `camera->manipulate({}, &referenceFrame)` | -| set exact position or exact orientation on `Free` and `FPS` | `referenceFrame` built from `camera->getGimbal()` | -| set one absolute typed state that can be reused later | `CCameraGoal` + `CCameraGoalSolver` | -| move a camera from live input this frame | `ICamera::manipulate(...)` | -| convert keyboard or mouse input into camera commands | `IGimbalInputProcessor` or `CGimbalInputBinder` | -| capture current camera state | `CCameraGoalSolver::capture...` | -| restore a camera from typed state | `CCameraGoalSolver::apply...` | -| save a named camera state | `CCameraPreset` | -| store camera states over time | `CCameraKeyframeTrack` | -| keep playback cursor state | `CCameraPlaybackTimeline` | -| make a camera follow a moving target | `CCameraFollowUtilities` | -| author compact scripted camera sequences | `CCameraSequenceScript` | -| execute frame-by-frame scripted payloads | `CCameraScriptedRuntime` | -| use the path-rig camera | `CPathCamera` and `SCameraPathModel` | - -## Quick start - -This section shows the common entry points before any deeper explanation. - -### 1. Apply one absolute rigid pose request - -Use this when you already have one rigid transform and want the camera to consume it through the normal runtime entry point. - -```cpp -const auto referenceFrame = - hlsl::CCameraMathUtilities::composeTransformMatrix(desiredPosition, desiredOrientation); - -camera->manipulate({}, &referenceFrame); -``` - -#### Why not just expose `setPosition(...)` and `setOrientation(...)` everywhere? - -Because not every camera kind stores arbitrary rigid pose as its native state. - -`Free` can represent arbitrary position and orientation directly. - -`FPS` cannot. Its legal runtime state is: - -- world-space position -- yaw -- pitch -- upright orientation reconstructed from yaw and pitch - -Consider this `FPS` example: - -```cpp -const auto desiredPosition = hlsl::float64_t3(2.0, 1.0, -3.0); -const auto desiredOrientation = - hlsl::CCameraMathUtilities::makeQuaternionFromEulerDegreesYXZ( - hlsl::float64_t3(-15.0, 40.0, 25.0)); -``` - -The requested rigid pose contains `roll = 25 deg`. - -That roll is not legal for `FPS`. - -If the API exposed unrestricted `setOrientation(...)` and accepted that quaternion as-is, the runtime camera would no longer match the rules of the `FPS` rig. - -The current API does this instead: - -1. accept one rigid pose request through `referenceFrame` -2. project that pose onto the legal state space of the concrete camera kind -3. rebuild the final runtime pose from that legal state - -For `FPS` that means: - -- keep the requested position -- read forward direction from the rigid reference -- rebuild legal `pitch/yaw` -- reject arbitrary roll -- write back one upright `FPS` pose - -The same pattern applies to every camera family: - -- `Free` keeps the rigid pose directly -- `FPS` legalizes to upright `position + pitch/yaw` -- target-relative cameras legalize to `target + orbitUv + distance` -- `Path Rig` legalizes to `PathState` - -That is why `camera->manipulate({}, &referenceFrame)` is the shared absolute runtime path. - -It accepts one rigid pose request at the API boundary and lets each camera family legalize it according to its own runtime model. - -Use this path for: - -- one-shot runtime pose application -- ImGuizmo -- world-space or local-space pose anchoring - -### 2. Set exact position or exact orientation on `Free` and `FPS` - -Use this when the target camera is `Free` or `FPS` and you want to replace only one rigid-pose component. - -```cpp -const auto& gimbal = camera->getGimbal(); - -const auto newPosition = desiredPosition; -const auto keepOrientation = gimbal.getOrientation(); - -const auto referenceFrame = - hlsl::CCameraMathUtilities::composeTransformMatrix(newPosition, keepOrientation); - -camera->manipulate({}, &referenceFrame); -``` - -```cpp -const auto& gimbal = camera->getGimbal(); - -const auto keepPosition = gimbal.getPosition(); -const auto newOrientation = desiredOrientation; - -const auto referenceFrame = - hlsl::CCameraMathUtilities::composeTransformMatrix(keepPosition, newOrientation); - -camera->manipulate({}, &referenceFrame); -``` - -`Free` applies these requests exactly. - -`FPS` keeps the exact position but legalizes orientation to its upright `pitch/yaw` state. - -Do not describe this path as exact position-only or exact orientation-only for constrained target-relative or path cameras. Those cameras legalize the rigid pose request into their own family state. - -### 3. Set one absolute typed state - -Use this when the state should survive beyond one frame or should be reused by presets, follow, playback, persistence, or scripts. - -```cpp -core::CCameraGoal goal = {}; -goal.position = desiredPosition; -goal.orientation = desiredOrientation; - -core::CCameraGoalSolver solver; -auto apply = solver.applyDetailed(camera.get(), goal); -``` - -Rule of thumb: - -- use `referenceFrame` for one runtime rigid pose request now -- use `CCameraGoal` for one typed camera state that should be stored, compared, serialized, replayed, or applied later - -### 4. Set one absolute camera-family state - -Use this when you do not want a generic rigid pose and instead want to write the native state of one camera family. - -Target-relative cameras: - -```cpp -camera->trySetSphericalTarget(targetPosition); -camera->trySetSphericalDistance(distance); -``` - -Path camera: - -```cpp -core::ICamera::PathState path = { - .s = desiredS, - .u = desiredU, - .v = desiredV, - .roll = desiredRoll -}; - -camera->trySetPathState(path); -``` - -Use this path when you already have: - -- target-relative state -- path-rig state -- one other family-specific typed fragment exposed by `ICamera` - -### 5. Live runtime camera control - -Use this when keyboard, mouse, or ImGuizmo should move the camera right now. - -```cpp -auto camera = core::make_smart_refctd_ptr(eye, target); - -ui::CGimbalInputBinder binder; -ui::CCameraInputBindingUtilities::applyDefaultCameraInputBindingPreset(binder, *camera); - -auto collected = binder.collectVirtualEvents(timestamp, { - .mouseEvents = { mouseEvents.data(), mouseEvents.size() }, - .keyboardEvents = { keyEvents.data(), keyEvents.size() } -}); - -camera->manipulate(collected.events); -``` - -What happens here: - -1. device input is converted into semantic camera commands -2. the camera consumes those commands through `manipulate(...)` -3. the camera updates its gimbal pose - -Main types involved: - -- [`CVirtualGimbalEvent.hpp`](CVirtualGimbalEvent.hpp) -- [`IGimbalBindingLayout.hpp`](IGimbalBindingLayout.hpp) -- [`IGimbalInputProcessor.hpp`](IGimbalInputProcessor.hpp) -- [`CGimbalInputBinder.hpp`](CGimbalInputBinder.hpp) -- [`CCameraInputBindingUtilities.hpp`](CCameraInputBindingUtilities.hpp) -- [`ICamera.hpp`](ICamera.hpp) - -The controller-side stack is: - -- `IGimbalBindingLayout` for the static mapping from device inputs to virtual events -- `IGimbalInputProcessor` for converting one frame of raw input into event magnitudes -- `CGimbalInputBinder` for the common runtime object that owns a layout and collects one frame of events -- `CCameraInputBindingUtilities` for shared preset layouts such as default `FPS`, `Orbit`, or `Path Rig` bindings - -#### How do I bind `FPS` to `WASD`? - -Use the shared default binding preset for the active camera kind. - -```cpp -auto camera = core::make_smart_refctd_ptr(position, orientation); - -ui::CGimbalInputBinder binder; -ui::CCameraInputBindingUtilities::applyDefaultCameraInputBindingPreset(binder, *camera); -``` - -For `FPS`, the default preset gives you: - -- keyboard `W/S/A/D` -> forward, backward, left, right -- keyboard `I/K/J/L` -> tilt up, tilt down, pan left, pan right -- mouse relative movement -> look yaw and pitch - -For `Free`, the default preset adds `Q/E` for roll. - -For target-relative families and `Path Rig`, the default preset keeps the same physical inputs but maps them to the legal state space of that family. - -#### How do I make my own bindings? - -Use one `IGimbalBindingLayout` implementation such as `CGimbalInputBinder` and write the mapping you want. - -```cpp -ui::CGimbalInputBinder binder; -const double customMoveGain = /* choose a sensitivity for this binding */; - -binder.updateKeyboardMapping([customMoveGain](auto& map) -{ - map.clear(); - map.emplace(ui::E_KEY_CODE::EKC_W, ui::IGimbalBindingLayout::CHashInfo(core::CVirtualGimbalEvent::MoveForward, customMoveGain)); - map.emplace(ui::E_KEY_CODE::EKC_S, ui::IGimbalBindingLayout::CHashInfo(core::CVirtualGimbalEvent::MoveBackward, customMoveGain)); - map.emplace(ui::E_KEY_CODE::EKC_A, ui::IGimbalBindingLayout::CHashInfo(core::CVirtualGimbalEvent::MoveLeft, customMoveGain)); - map.emplace(ui::E_KEY_CODE::EKC_D, ui::IGimbalBindingLayout::CHashInfo(core::CVirtualGimbalEvent::MoveRight, customMoveGain)); -}); -``` - -The same pattern works for: - -- mouse bindings through `updateMouseMapping(...)` -- ImGuizmo bindings through `updateImguizmoMapping(...)` - -#### How are `magnitude` values generated? - -`CVirtualGimbalEvent::magnitude` is one non-negative scalar attached to one semantic command. - -It is not a raw device unit and it is not, by itself, the final world-space or angular motion applied by a camera. - -What stays stable at the API level is the meaning by event family: - -- translation events carry one controller-side translation amount -- rotation events carry one controller-side angular amount -- scale events carry one controller-side scale amount - -The binding layer maps raw producer values onto those amounts. Different sources may start from: - -- elapsed time for held input -- cursor deltas for relative mouse input -- scroll steps for wheel input -- world-space translation or angular deltas for gizmo-driven input - -That means exact numeric gains are binding policy, not API contract. The binding layer owns sensitivity and repeat-rate tuning. - -After the controller side emits virtual magnitudes, the camera runtime applies its own motion scales and legalizes the result to the concrete camera family. - -The motion pipeline is therefore: - -1. raw device input -2. binding-local gain -3. `CVirtualGimbalEvent { type, magnitude }` -4. camera-local motion scale -5. family-specific legalization and state update - -### 6. Capture a camera and restore it later - -Use this when you want explicit camera state instead of one-frame runtime input. - -```cpp -core::CCameraGoalSolver solver; - -auto capture = solver.captureDetailed(camera.get()); -if (capture.canUseGoal()) -{ - auto apply = solver.applyDetailed(camera.get(), capture.goal); -} -``` - -What happens here: - -1. the solver reads runtime camera state -2. the solver writes that state into one `CCameraGoal` -3. the solver later applies that goal back to a camera - -Main types involved: - -- [`CCameraGoal.hpp`](CCameraGoal.hpp) -- [`CCameraGoalSolver.hpp`](CCameraGoalSolver.hpp) - -### 7. Save a named camera state - -Use this when one camera state needs a user-facing name or identifier. - -```cpp -core::CCameraGoalSolver solver; - -auto capture = solver.captureDetailed(camera.get()); -if (capture.canUseGoal()) -{ - core::CCameraPreset preset; - preset.name = "Overview"; - preset.identifier = "overview"; - core::CCameraPresetUtilities::assignGoalToPreset(preset, capture.goal); -} -``` - -Main types involved: - -- [`CCameraPreset.hpp`](CCameraPreset.hpp) -- [`CCameraPresetFlow.hpp`](CCameraPresetFlow.hpp) - -### 8. Make a camera follow a moving target - -Use this when one tracked subject should drive camera behavior. - -```cpp -core::CTrackedTarget trackedTarget(position, orientation); - -core::SCameraFollowConfig follow = {}; -follow.enabled = true; -follow.mode = core::ECameraFollowMode::LookAtTarget; - -core::CCameraGoalSolver solver; -core::CCameraFollowUtilities::applyFollowToCamera(solver, camera.get(), trackedTarget, follow); -``` - -Main types involved: - -- [`CCameraFollowUtilities.hpp`](CCameraFollowUtilities.hpp) -- [`CCameraFollowRegressionUtilities.hpp`](CCameraFollowRegressionUtilities.hpp) - -### 9. Build scripted runtime payloads from compact authored data - -Use this when camera playback is authored as sequence data and then expanded into per-frame runtime actions and checks. - -```cpp -system::CCameraScriptedTimeline timeline; - -system::CCameraSequenceScriptedBuilderUtilities::appendCompiledSequenceSegmentToScriptedTimeline( - timeline, - baseFrame, - compiledSegment, - buildInfo); - -system::CCameraScriptedRuntimeUtilities::finalizeScriptedTimeline(timeline); -``` - -Main types involved: - -- [`CCameraSequenceScript.hpp`](CCameraSequenceScript.hpp) -- [`CCameraSequenceScriptedBuilder.hpp`](CCameraSequenceScriptedBuilder.hpp) -- [`CCameraScriptedRuntime.hpp`](CCameraScriptedRuntime.hpp) -- [`CCameraScriptedCheckRunner.hpp`](CCameraScriptedCheckRunner.hpp) - -## Core concepts - -### `CVirtualGimbalEvent` - -File: - -- [`CVirtualGimbalEvent.hpp`](CVirtualGimbalEvent.hpp) - -`CVirtualGimbalEvent` is one semantic camera command plus one scalar magnitude. - -The scalar magnitude is a controller-side virtual amount emitted after binding -gains are applied. It is not a raw device delta and it is not, by itself, the -final world-space motion applied by a camera. - -Examples: - -- `MoveForward` -- `MoveLeft` -- `MoveUp` -- `TiltUp` -- `PanRight` -- `RollLeft` -- `ScaleZInc` - -The event does not store device-specific origin. -The same event type can come from keyboard input, mouse input, ImGuizmo, scripted playback, or replay helpers. - -### `IGimbal` - -Files: - -- [`IGimbal.hpp`](IGimbal.hpp) -- [`ICamera.hpp`](ICamera.hpp) - -The gimbal stores runtime pose: - -- position -- orientation -- scale -- orthonormal basis - -It also accumulates one frame of semantic events into a `VirtualImpulse`. - -`ICamera::CGimbal` extends the base gimbal with a cached world-to-view matrix. - -Every runtime camera owns one `CGimbal`. - -### `ICamera` - -File: - -- [`ICamera.hpp`](ICamera.hpp) - -`ICamera` is the shared runtime interface implemented by every camera kind. - -Its main job is: - -- consume one frame of semantic virtual events -- optionally consume one rigid reference frame -- update internal camera state -- update runtime pose in the gimbal - -Important members: - -- `manipulate(...)` -- `getGimbal()` -- `getAllowedVirtualEvents()` -- `getKind()` -- `getCapabilities()` -- typed hooks such as `tryGetSphericalTargetState(...)` and `tryGetPathState(...)` - -Each camera also stores one local motion-scale bundle in `SMotionConfig`. -Those scales are applied after the binding layer emits virtual magnitudes. - -### `referenceFrame` - -Files: - -- [`ICamera.hpp`](ICamera.hpp) -- [`IGimbal.hpp`](IGimbal.hpp) - -`referenceFrame` is the optional rigid transform passed to `ICamera::manipulate(...)`. - -It is the runtime pose anchor for one manipulation step. - -Typical producers: - -- ImGuizmo -- restore helpers -- replay helpers -- code that wants world-space or local-space manipulation anchored to a specific rigid transform - -When you already have one absolute rigid pose, `referenceFrame` is the direct runtime entry point for requesting that pose through the runtime camera path. - -See Quick start sections 1 and 2 for the concrete absolute-pose usage patterns. - -Shared runtime pattern: - -```text -referenceFrame - -> extract rigid reference transform - -> resolve legal state for this camera kind - -> accumulate virtual events - -> apply deltas in that state space - -> rebuild pose -``` - -### `SCameraRigPose` - -File: - -- [`SCameraRigPose.hpp`](SCameraRigPose.hpp) - -`SCameraRigPose` stores only: - -- world-space position -- world-space orientation - -It is the smallest typed pose object reused across the stack. - -### `CCameraGoal` - -File: - -- [`CCameraGoal.hpp`](CCameraGoal.hpp) - -`CCameraGoal` is the canonical typed transport for camera state. - -You can think of it as: - -> one explicit camera-state snapshot used by higher-level tools - -It may contain: - -- pose -- target position -- target-relative distance -- orbit state -- path state -- dynamic perspective state -- source camera metadata - -It is used by: - -- capture -- restore -- preset flow -- playback -- follow -- scripted checks - -When you want to set a camera absolutely in a reusable, serializable, or comparable way, `CCameraGoal` is the main public state object for that job. - -It is not: - -- a live input object -- a replacement for `manipulate(...)` -- a promise that every camera can represent every arbitrary pose exactly - -For constrained cameras, the solver may project the goal onto legal camera-family state before or during apply. - -### `CCameraGoalSolver` - -File: - -- [`CCameraGoalSolver.hpp`](CCameraGoalSolver.hpp) - -`CCameraGoalSolver` converts between typed camera state and runtime cameras. - -It captures runtime cameras into `CCameraGoal`, analyzes whether a target camera can represent that goal directly, and applies the result either through typed state or through runtime replay when needed. - -If you want to restore one absolute camera state and you are not sure which family-specific hook to call, use `CCameraGoalSolver`. - -### `CCameraPreset` - -File: - -- [`CCameraPreset.hpp`](CCameraPreset.hpp) - -`CCameraPreset` is a named saved `CCameraGoal`. - -It contains: - -- `name` -- `identifier` -- `goal` - -### `CCameraKeyframeTrack` - -File: - -- [`CCameraKeyframeTrack.hpp`](CCameraKeyframeTrack.hpp) - -`CCameraKeyframeTrack` is a sequence of time-stamped presets. - -Each keyframe contains: - -- one preset -- one authored time - -### `CCameraPlaybackTimeline` - -File: - -- [`CCameraPlaybackTimeline.hpp`](CCameraPlaybackTimeline.hpp) - -`CCameraPlaybackTimeline` stores playback cursor state over time-based camera data. - -It tracks things such as: - -- current time -- direction -- looping -- paused or playing state - -### `CTrackedTarget` - -File: - -- [`CCameraFollowUtilities.hpp`](CCameraFollowUtilities.hpp) - -`CTrackedTarget` is the reusable tracked subject used by follow. - -It owns its own gimbal. -It is not a mesh id and not a scene-node handle. - -### `CCameraSequenceScript` - -File: - -- [`CCameraSequenceScript.hpp`](CCameraSequenceScript.hpp) - -`CCameraSequenceScript` is the compact authored format for camera sequences. - -It stores camera-domain data such as: - -- targeted camera -- projection presentation requests -- camera keyframes -- tracked-target keyframes -- continuity settings -- capture fractions - -It does not store frame-by-frame low-level input. - -### `CCameraScriptedRuntime` - -File: - -- [`CCameraScriptedRuntime.hpp`](CCameraScriptedRuntime.hpp) - -`CCameraScriptedRuntime` is the expanded executable form used during scripted playback and validation. - -It stores runtime payloads such as: - -- low-level input events -- action events -- per-frame checks -- capture scheduling - -### `Path Rig` - -Files: - -- [`CPathCamera.hpp`](CPathCamera.hpp) -- [`CCameraPathUtilities.hpp`](CCameraPathUtilities.hpp) -- [`CCameraPathMetadata.hpp`](CCameraPathMetadata.hpp) - -`Path Rig` is the camera family with typed state: - -- `s` -- `u` -- `v` -- `roll` - -Its runtime and typed tooling are driven by `SCameraPathModel`, which defines how path state is resolved, updated, and converted back into camera pose. - -## Camera families - -### Free cameras - -Files: - -- [`CFPSCamera.hpp`](CFPSCamera.hpp) -- [`CFreeLockCamera.hpp`](CFreeLockCamera.hpp) - -State: - -- world-space position -- orientation or FPS-constrained yaw/pitch orientation - -Typical use: - -- free-fly navigation -- direct pose-driven manipulation - -### Target-relative cameras - -Base: - -- [`CSphericalTargetCamera.hpp`](CSphericalTargetCamera.hpp) - -Derived: - -- [`COrbitCamera.hpp`](COrbitCamera.hpp) -- [`CArcballCamera.hpp`](CArcballCamera.hpp) -- [`CTurntableCamera.hpp`](CTurntableCamera.hpp) -- [`CTopDownCamera.hpp`](CTopDownCamera.hpp) -- [`CIsometricCamera.hpp`](CIsometricCamera.hpp) -- [`CChaseCamera.hpp`](CChaseCamera.hpp) -- [`CDollyCamera.hpp`](CDollyCamera.hpp) -- [`CDollyZoomCamera.hpp`](CDollyZoomCamera.hpp) - -Shared state: - -- target position -- `orbitUv` -- distance - -These cameras resolve pose through target-relative state instead of arbitrary free pose. - -### DollyZoom - -File: - -- [`CDollyZoomCamera.hpp`](CDollyZoomCamera.hpp) - -This camera adds dynamic perspective state on top of target-relative state. - -Typed dynamic perspective state: - -- `baseFov` -- `referenceDistance` - -### Path Rig - -Files: - -- [`CPathCamera.hpp`](CPathCamera.hpp) -- [`CCameraPathUtilities.hpp`](CCameraPathUtilities.hpp) -- [`CCameraPathMetadata.hpp`](CCameraPathMetadata.hpp) - -Typed path state: - -- `s` -- `u` -- `v` -- `roll` - -Typed path limits: - -- `minU` -- `minDistance` -- `maxDistance` - -## Typed tooling - -The key typed types are introduced in the `Core concepts` section above. - -This section focuses on how they fit together in one workflow: - -1. `SCameraRigPose` is the smallest typed pose fragment. -2. `CCameraGoal` is the canonical typed state transport built on top of pose and optional family-specific fragments. -3. `CCameraGoalSolver` captures runtime cameras into goals and applies goals back to runtime cameras. -4. `CCameraPreset` gives one goal a stable user-facing identity. -5. `CCameraKeyframeTrack` stores presets over authored time. -6. `CCameraPlaybackTimeline` stores playback cursor state while a track is being evaluated. - -Use this layer when camera state must outlive the current frame or be exchanged between tools. - -## Follow - -Files: - -- [`CCameraFollowUtilities.hpp`](CCameraFollowUtilities.hpp) -- [`CCameraFollowRegressionUtilities.hpp`](CCameraFollowRegressionUtilities.hpp) - -Follow is built from: - -- one tracked target -- one follow mode -- one follow configuration - -Tracked target type: - -- `CTrackedTarget` - -Follow modes: - -- `OrbitTarget` -- `LookAtTarget` -- `KeepWorldOffset` -- `KeepLocalOffset` - -`CCameraFollowUtilities` reads tracked-target pose, builds resulting camera goal state, and applies it through the shared goal solver. - -## Scripting - -### Compact authored format - -Files: - -- [`CCameraSequenceScript.hpp`](CCameraSequenceScript.hpp) -- [`CCameraSequenceScriptPersistence.hpp`](CCameraSequenceScriptPersistence.hpp) - -This layer stores authored camera-domain data. - -### Expanded runtime format - -Files: - -- [`CCameraScriptedRuntime.hpp`](CCameraScriptedRuntime.hpp) -- [`CCameraScriptedRuntimePersistence.hpp`](CCameraScriptedRuntimePersistence.hpp) -- [`CCameraSequenceScriptedBuilder.hpp`](CCameraSequenceScriptedBuilder.hpp) -- [`CCameraScriptedCheckRunner.hpp`](CCameraScriptedCheckRunner.hpp) - -This layer stores executable per-frame runtime payloads and validation checks. - -Common flow: - -```text -compact authored sequence - -> compile or expand - -> scripted runtime payload - -> execute against runtime camera state -``` - -## Projection and presentation helpers - -Projection layer: - -- [`IProjection.hpp`](IProjection.hpp) -- [`ILinearProjection.hpp`](ILinearProjection.hpp) -- [`IPerspectiveProjection.hpp`](IPerspectiveProjection.hpp) -- [`IPlanarProjection.hpp`](IPlanarProjection.hpp) -- [`CLinearProjection.hpp`](CLinearProjection.hpp) -- [`CPlanarProjection.hpp`](CPlanarProjection.hpp) -- [`CCubeProjection.hpp`](CCubeProjection.hpp) - -Camera-facing presentation helpers: - -- [`CCameraPresentationUtilities.hpp`](CCameraPresentationUtilities.hpp) -- [`CCameraProjectionUtilities.hpp`](CCameraProjectionUtilities.hpp) -- [`CCameraTextUtilities.hpp`](CCameraTextUtilities.hpp) -- [`CCameraViewportOverlayUtilities.hpp`](CCameraViewportOverlayUtilities.hpp) -- [`CCameraControlPanelUiUtilities.hpp`](CCameraControlPanelUiUtilities.hpp) -- [`CCameraScriptVisualDebugOverlayUtilities.hpp`](CCameraScriptVisualDebugOverlayUtilities.hpp) diff --git a/common/include/camera/SCameraRigPose.hpp b/common/include/camera/SCameraRigPose.hpp deleted file mode 100644 index f8f3d7dfe..000000000 --- a/common/include/camera/SCameraRigPose.hpp +++ /dev/null @@ -1,26 +0,0 @@ -// This file is part of the "Nabla Engine". -// For conditions of distribution and use, see copyright notice in nabla.h - -#ifndef _S_CAMERA_RIG_POSE_HPP_ -#define _S_CAMERA_RIG_POSE_HPP_ - -#include "CCameraMathUtilities.hpp" - -namespace nbl::core -{ - -/// @brief Canonical camera pose consisting of world-space position and orientation. -/// -/// This type stores only pose data. Higher-level types add target-relative, -/// dynamic-perspective, or path-specific state around it. -struct SCameraRigPose -{ - /// @brief Camera origin in world space. - hlsl::float64_t3 position = hlsl::float64_t3(0.0); - /// @brief Camera orientation in world space expressed as a unit quaternion. - hlsl::camera_quaternion_t orientation = hlsl::CCameraMathUtilities::makeIdentityQuaternion(); -}; - -} // namespace nbl::core - -#endif // _S_CAMERA_RIG_POSE_HPP_ diff --git a/common/src/nbl/examples/camera/CCameraJsonPersistenceUtilities.hpp b/common/src/nbl/examples/camera/CCameraJsonPersistenceUtilities.hpp deleted file mode 100644 index 36f09fd82..000000000 --- a/common/src/nbl/examples/camera/CCameraJsonPersistenceUtilities.hpp +++ /dev/null @@ -1,98 +0,0 @@ -#ifndef _NBL_EXAMPLES_CAMERA_JSON_PERSISTENCE_UTILITIES_HPP_INCLUDED_ -#define _NBL_EXAMPLES_CAMERA_JSON_PERSISTENCE_UTILITIES_HPP_INCLUDED_ - -#include - -#include "camera/CCameraFileUtilities.hpp" -#include "camera/CCameraGoal.hpp" -#include "camera/CCameraPresetFlow.hpp" -#include "nlohmann/json.hpp" - -namespace nbl::system -{ - -template -inline void deserializeGoalJson(const Json& entry, core::CCameraGoal& goal) -{ - goal = {}; - - if (entry.contains("camera_kind")) - goal.sourceKind = static_cast(entry["camera_kind"].get()); - if (entry.contains("camera_capabilities")) - goal.sourceCapabilities = entry["camera_capabilities"].get(); - if (entry.contains("camera_goal_state_mask")) - goal.sourceGoalStateMask = entry["camera_goal_state_mask"].get(); - - if (entry.contains("position") && entry["position"].is_array()) - { - const auto values = entry["position"].get>(); - goal.position = hlsl::float64_t3(values[0], values[1], values[2]); - } - if (entry.contains("orientation") && entry["orientation"].is_array()) - { - const auto values = entry["orientation"].get>(); - goal.orientation = hlsl::CCameraMathUtilities::makeQuaternionFromComponents(values[0], values[1], values[2], values[3]); - } - if (entry.contains("target_position") && entry["target_position"].is_array()) - { - const auto values = entry["target_position"].get>(); - goal.targetPosition = hlsl::float64_t3(values[0], values[1], values[2]); - goal.hasTargetPosition = true; - } - if (entry.contains("distance")) - { - goal.distance = entry["distance"].get(); - goal.hasDistance = true; - } - if (entry.contains("orbit_u")) - { - goal.orbitUv.x = entry["orbit_u"].get(); - goal.hasOrbitState = true; - } - if (entry.contains("orbit_v")) - { - goal.orbitUv.y = entry["orbit_v"].get(); - goal.hasOrbitState = true; - } - if (entry.contains("orbit_distance")) - { - goal.orbitDistance = entry["orbit_distance"].get(); - goal.hasOrbitState = true; - } - if (entry.contains("path_s") && entry.contains("path_u") && entry.contains("path_v")) - { - goal.pathState.s = entry["path_s"].get(); - goal.pathState.u = entry["path_u"].get(); - goal.pathState.v = entry["path_v"].get(); - goal.pathState.roll = entry.contains("path_roll") ? entry["path_roll"].get() : 0.0; - goal.hasPathState = true; - } - if (entry.contains("dynamic_base_fov")) - { - goal.dynamicPerspectiveState.baseFov = entry["dynamic_base_fov"].get(); - goal.hasDynamicPerspectiveState = true; - } - if (entry.contains("dynamic_reference_distance")) - { - goal.dynamicPerspectiveState.referenceDistance = entry["dynamic_reference_distance"].get(); - goal.hasDynamicPerspectiveState = true; - } -} - -template -inline void deserializePresetJson(const Json& entry, core::CCameraPreset& preset) -{ - preset = {}; - if (entry.contains("name")) - preset.name = entry["name"].get(); - if (entry.contains("identifier")) - preset.identifier = entry["identifier"].get(); - - core::CCameraGoal goal = {}; - deserializeGoalJson(entry, goal); - core::CCameraPresetUtilities::assignGoalToPreset(preset, goal); -} - -} // namespace nbl::system - -#endif // _NBL_EXAMPLES_CAMERA_JSON_PERSISTENCE_UTILITIES_HPP_INCLUDED_ diff --git a/common/src/nbl/examples/camera/CCameraPersistence.cpp b/common/src/nbl/examples/camera/CCameraPersistence.cpp deleted file mode 100644 index c924136ae..000000000 --- a/common/src/nbl/examples/camera/CCameraPersistence.cpp +++ /dev/null @@ -1,278 +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 - -#include "camera/CCameraPersistence.hpp" - -#include -#include - -#include "CCameraJsonPersistenceUtilities.hpp" -#include "nlohmann/json.hpp" - -using json_t = nlohmann::json; - -json_t serializeGoalJson(const nbl::core::CCameraGoal& goal) -{ - json_t json; - json["position"] = { goal.position.x, goal.position.y, goal.position.z }; - json["orientation"] = { - goal.orientation.data.x, - goal.orientation.data.y, - goal.orientation.data.z, - goal.orientation.data.w - }; - json["camera_kind"] = static_cast(goal.sourceKind); - json["camera_capabilities"] = goal.sourceCapabilities; - json["camera_goal_state_mask"] = goal.sourceGoalStateMask; - - if (goal.hasTargetPosition) - json["target_position"] = { goal.targetPosition.x, goal.targetPosition.y, goal.targetPosition.z }; - if (goal.hasDistance) - json["distance"] = goal.distance; - if (goal.hasOrbitState) - { - json["orbit_u"] = goal.orbitUv.x; - json["orbit_v"] = goal.orbitUv.y; - json["orbit_distance"] = goal.orbitDistance; - } - if (goal.hasPathState) - { - json["path_s"] = goal.pathState.s; - json["path_u"] = goal.pathState.u; - json["path_v"] = goal.pathState.v; - json["path_roll"] = goal.pathState.roll; - } - if (goal.hasDynamicPerspectiveState) - { - json["dynamic_base_fov"] = goal.dynamicPerspectiveState.baseFov; - json["dynamic_reference_distance"] = goal.dynamicPerspectiveState.referenceDistance; - } - - return json; -} - -json_t serializePresetJson(const nbl::core::CCameraPreset& preset) -{ - auto json = serializeGoalJson(nbl::core::CCameraPresetUtilities::makeGoalFromPreset(preset)); - json["name"] = preset.name; - json["identifier"] = preset.identifier; - return json; -} - -json_t serializeKeyframeTrackJson(const nbl::core::CCameraKeyframeTrack& track) -{ - json_t root; - root["keyframes"] = json_t::array(); - - for (const auto& keyframe : track.keyframes) - { - auto json = serializePresetJson(keyframe.preset); - json["time"] = keyframe.time; - root["keyframes"].push_back(std::move(json)); - } - - return root; -} - -bool deserializeKeyframeTrackJson(const json_t& root, nbl::core::CCameraKeyframeTrack& track) -{ - if (!root.contains("keyframes") || !root["keyframes"].is_array()) - return false; - - track = {}; - for (const auto& entry : root["keyframes"]) - { - nbl::core::CCameraKeyframe keyframe; - if (entry.contains("time")) - keyframe.time = std::max(0.f, entry["time"].get()); - nbl::system::deserializePresetJson(entry, keyframe.preset); - track.keyframes.emplace_back(std::move(keyframe)); - } - - nbl::core::CCameraKeyframeTrackUtilities::sortKeyframeTrackByTime(track); - nbl::core::CCameraKeyframeTrackUtilities::normalizeSelectedKeyframeTrack(track); - return true; -} - -json_t serializePresetCollectionJson(std::span presets) -{ - json_t root; - root["presets"] = json_t::array(); - for (const auto& preset : presets) - root["presets"].push_back(serializePresetJson(preset)); - return root; -} - -bool deserializePresetCollectionJson(const json_t& root, std::vector& presets) -{ - if (!root.contains("presets") || !root["presets"].is_array()) - return false; - - std::vector loadedPresets; - loadedPresets.reserve(root["presets"].size()); - for (const auto& entry : root["presets"]) - { - nbl::core::CCameraPreset preset; - nbl::system::deserializePresetJson(entry, preset); - loadedPresets.emplace_back(std::move(preset)); - } - - presets = std::move(loadedPresets); - return true; -} - -namespace nbl::system -{ - -bool writeGoal(std::ostream& out, const core::CCameraGoal& goal, const int indent) -{ - if (!out) - return false; - - out << serializeGoalJson(goal).dump(indent); - return static_cast(out); -} - -bool readGoal(std::istream& in, core::CCameraGoal& goal) -{ - if (!in) - return false; - - json_t root; - in >> root; - nbl::system::deserializeGoalJson(root, goal); - return true; -} - -bool saveGoalToFile(ISystem& system, const path& filePath, const core::CCameraGoal& goal, const int indent) -{ - std::ostringstream out; - if (!writeGoal(out, goal, indent)) - return false; - return CCameraFileUtilities::writeTextFile(system, filePath, out.str()); -} - -bool loadGoalFromFile(ISystem& system, const path& filePath, core::CCameraGoal& goal) -{ - std::string text; - if (!CCameraFileUtilities::readTextFile(system, filePath, text)) - return false; - - std::istringstream in(text); - return readGoal(in, goal); -} - -bool writePreset(std::ostream& out, const core::CCameraPreset& preset, const int indent) -{ - if (!out) - return false; - - out << serializePresetJson(preset).dump(indent); - return static_cast(out); -} - -bool readPreset(std::istream& in, core::CCameraPreset& preset) -{ - if (!in) - return false; - - json_t root; - in >> root; - nbl::system::deserializePresetJson(root, preset); - return true; -} - -bool savePresetToFile(ISystem& system, const path& filePath, const core::CCameraPreset& preset, const int indent) -{ - std::ostringstream out; - if (!writePreset(out, preset, indent)) - return false; - return CCameraFileUtilities::writeTextFile(system, filePath, out.str()); -} - -bool loadPresetFromFile(ISystem& system, const path& filePath, core::CCameraPreset& preset) -{ - std::string text; - if (!CCameraFileUtilities::readTextFile(system, filePath, text)) - return false; - - std::istringstream in(text); - return readPreset(in, preset); -} - -bool writeKeyframeTrack(std::ostream& out, const core::CCameraKeyframeTrack& track, const int indent) -{ - if (!out) - return false; - - out << serializeKeyframeTrackJson(track).dump(indent); - return static_cast(out); -} - -bool readKeyframeTrack(std::istream& in, core::CCameraKeyframeTrack& track) -{ - if (!in) - return false; - - json_t root; - in >> root; - return deserializeKeyframeTrackJson(root, track); -} - -bool saveKeyframeTrackToFile(ISystem& system, const path& filePath, const core::CCameraKeyframeTrack& track, const int indent) -{ - std::ostringstream out; - if (!writeKeyframeTrack(out, track, indent)) - return false; - return CCameraFileUtilities::writeTextFile(system, filePath, out.str()); -} - -bool loadKeyframeTrackFromFile(ISystem& system, const path& filePath, core::CCameraKeyframeTrack& track) -{ - std::string text; - if (!CCameraFileUtilities::readTextFile(system, filePath, text)) - return false; - - std::istringstream in(text); - return readKeyframeTrack(in, track); -} - -bool writePresetCollection(std::ostream& out, std::span presets, const int indent) -{ - if (!out) - return false; - - out << serializePresetCollectionJson(presets).dump(indent); - return static_cast(out); -} - -bool readPresetCollection(std::istream& in, std::vector& presets) -{ - if (!in) - return false; - - json_t root; - in >> root; - return deserializePresetCollectionJson(root, presets); -} - -bool savePresetCollectionToFile(ISystem& system, const path& filePath, std::span presets, const int indent) -{ - std::ostringstream out; - if (!writePresetCollection(out, presets, indent)) - return false; - return CCameraFileUtilities::writeTextFile(system, filePath, out.str()); -} - -bool loadPresetCollectionFromFile(ISystem& system, const path& filePath, std::vector& presets) -{ - std::string text; - if (!CCameraFileUtilities::readTextFile(system, filePath, text)) - return false; - - std::istringstream in(text); - return readPresetCollection(in, presets); -} - -} // namespace nbl::system diff --git a/common/src/nbl/examples/camera/CCameraScriptedRuntimePersistence.cpp b/common/src/nbl/examples/camera/CCameraScriptedRuntimePersistence.cpp deleted file mode 100644 index 6ca8c5ea5..000000000 --- a/common/src/nbl/examples/camera/CCameraScriptedRuntimePersistence.cpp +++ /dev/null @@ -1,1156 +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 - -#include "camera/CCameraScriptedRuntimePersistence.hpp" - -#include -#include -#include -#include -#include - -#include "CCameraJsonPersistenceUtilities.hpp" -#include "nlohmann/json.hpp" - -using json_t = nlohmann::json; - -bool tryParseCaptureFractionJson(const json_t& entry, float& outFraction) -{ - if (entry.is_number()) - { - outFraction = std::clamp(entry.get(), 0.f, 1.f); - return true; - } - - if (!entry.is_string()) - return false; - - const auto tag = entry.get(); - if (tag == "start") - outFraction = 0.f; - else if (tag == "mid" || tag == "middle") - outFraction = 0.5f; - else if (tag == "end") - outFraction = 1.f; - else - return false; - - return true; -} - -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]); -} - -bool deserializeSequencePresentationsJson(const json_t& root, std::vector& out, std::string* error) -{ - out.clear(); - if (!root.is_array()) - { - if (error) - *error = "Sequence presentations must be an array."; - return false; - } - - for (const auto& entry : root) - { - if (!entry.is_object() || !entry.contains("projection")) - { - if (error) - *error = "Sequence presentation entry missing \"projection\"."; - return false; - } - - nbl::core::CCameraSequencePresentation presentation; - if (!nbl::core::CCameraSequenceScriptUtilities::tryParseProjectionType(entry["projection"].get(), presentation.projection)) - { - if (error) - *error = "Sequence presentation has invalid projection type."; - return false; - } - if (entry.contains("left_handed")) - presentation.leftHanded = entry["left_handed"].get(); - out.emplace_back(presentation); - } - - return true; -} - -bool deserializeSequenceContinuityJson(const json_t& root, nbl::core::CCameraSequenceContinuitySettings& out, std::string* error) -{ - if (!root.is_object()) - { - if (error) - *error = "Sequence continuity settings must be an object."; - return false; - } - - out = {}; - if (root.contains("baseline")) - out.baseline = root["baseline"].get(); - if (root.contains("step")) - out.step = root["step"].get(); - - if (root.contains("min_pos_delta")) - { - out.minPosDelta = root["min_pos_delta"].get(); - out.hasPosDeltaConstraint = true; - } - if (root.contains("max_pos_delta")) - { - out.maxPosDelta = root["max_pos_delta"].get(); - out.hasPosDeltaConstraint = true; - } - else if (root.contains("pos_tolerance")) - { - out.maxPosDelta = root["pos_tolerance"].get(); - out.hasPosDeltaConstraint = true; - } - - if (root.contains("min_euler_delta_deg")) - { - out.minEulerDeltaDeg = root["min_euler_delta_deg"].get(); - out.hasEulerDeltaConstraint = true; - } - if (root.contains("max_euler_delta_deg")) - { - out.maxEulerDeltaDeg = root["max_euler_delta_deg"].get(); - out.hasEulerDeltaConstraint = true; - } - else if (root.contains("euler_tolerance_deg")) - { - out.maxEulerDeltaDeg = root["euler_tolerance_deg"].get(); - out.hasEulerDeltaConstraint = true; - } - - if (root.contains("disable_pos_delta")) - out.hasPosDeltaConstraint = !root["disable_pos_delta"].get(); - if (root.contains("disable_euler_delta")) - out.hasEulerDeltaConstraint = !root["disable_euler_delta"].get(); - - if (out.step && !(out.hasPosDeltaConstraint || out.hasEulerDeltaConstraint)) - { - if (error) - *error = "Sequence continuity step checks require at least one delta constraint."; - return false; - } - - return true; -} - -bool deserializeSequenceGoalDeltaJson(const json_t& root, nbl::core::CCameraSequenceGoalDelta& out, std::string* error) -{ - if (!root.is_object()) - { - if (error) - *error = "Sequence keyframe delta must be an object."; - return false; - } - - out = {}; - if (root.contains("position_offset")) - { - readVector3(root["position_offset"], out.positionOffset); - out.hasPositionOffset = true; - } - if (root.contains("rotation_euler_deg_offset")) - { - readVector3(root["rotation_euler_deg_offset"], out.rotationEulerDegOffset); - out.hasRotationEulerDegOffset = true; - } - if (root.contains("target_offset")) - { - readVector3(root["target_offset"], out.targetOffset); - out.hasTargetOffset = true; - } - if (root.contains("orbit_u_delta_deg")) - { - out.orbitDelta.setUDeltaDeg(root["orbit_u_delta_deg"].get()); - } - if (root.contains("orbit_v_delta_deg")) - { - out.orbitDelta.setVDeltaDeg(root["orbit_v_delta_deg"].get()); - } - if (root.contains("orbit_distance_delta")) - { - out.orbitDelta.setDistanceDelta(root["orbit_distance_delta"].get()); - } - if (root.contains("path_s_delta_deg")) - { - out.pathDelta.setSDeltaDeg(root["path_s_delta_deg"].get()); - } - if (root.contains("path_u_delta")) - { - out.pathDelta.setUDelta(root["path_u_delta"].get()); - } - if (root.contains("path_v_delta")) - { - out.pathDelta.setVDelta(root["path_v_delta"].get()); - } - if (root.contains("path_roll_delta_deg")) - { - out.pathDelta.setRollDeltaDeg(root["path_roll_delta_deg"].get()); - } - if (root.contains("dynamic_base_fov_delta")) - { - out.dynamicBaseFovDelta = root["dynamic_base_fov_delta"].get(); - out.hasDynamicBaseFovDelta = true; - } - if (root.contains("dynamic_reference_distance_delta")) - { - out.dynamicReferenceDistanceDelta = root["dynamic_reference_distance_delta"].get(); - out.hasDynamicReferenceDistanceDelta = true; - } - - return true; -} - -bool deserializeSequenceKeyframeJson(const json_t& root, nbl::core::CCameraSequenceKeyframe& out, std::string* error) -{ - if (!root.is_object()) - { - if (error) - *error = "Sequence keyframe must be an object."; - return false; - } - - out = {}; - if (root.contains("time")) - out.time = std::max(0.f, root["time"].get()); - - if (root.contains("delta")) - { - if (!deserializeSequenceGoalDeltaJson(root["delta"], out.delta, error)) - return false; - out.hasDelta = true; - } - - if (root.contains("preset")) - { - nbl::system::deserializePresetJson(root["preset"], out.absolutePreset); - out.hasAbsolutePreset = true; - } - else if (root.contains("position") || root.contains("orientation") || root.contains("target_position") || - root.contains("distance") || root.contains("orbit_u") || root.contains("orbit_v") || - root.contains("orbit_distance") || root.contains("path_s") || root.contains("path_u") || - root.contains("path_v") || root.contains("path_roll") || - root.contains("dynamic_base_fov") || root.contains("dynamic_reference_distance")) - { - nbl::system::deserializePresetJson(root, out.absolutePreset); - out.hasAbsolutePreset = true; - } - - return true; -} - -bool deserializeSequenceTrackedTargetDeltaJson(const json_t& root, nbl::core::CCameraSequenceTrackedTargetDelta& out, std::string* error) -{ - if (!root.is_object()) - { - if (error) - *error = "Sequence target delta must be an object."; - return false; - } - - out = {}; - if (root.contains("position_offset")) - { - readVector3(root["position_offset"], out.positionOffset); - out.hasPositionOffset = true; - } - if (root.contains("rotation_euler_deg_offset")) - { - readVector3(root["rotation_euler_deg_offset"], out.rotationEulerDegOffset); - out.hasRotationEulerDegOffset = true; - } - - return true; -} - -bool deserializeSequenceTrackedTargetKeyframeJson(const json_t& root, nbl::core::CCameraSequenceTrackedTargetKeyframe& out, std::string* error) -{ - if (!root.is_object()) - { - if (error) - *error = "Sequence target keyframe must be an object."; - return false; - } - - out = {}; - if (root.contains("time")) - out.time = std::max(0.f, root["time"].get()); - - if (root.contains("delta")) - { - if (!deserializeSequenceTrackedTargetDeltaJson(root["delta"], out.delta, error)) - return false; - out.hasDelta = true; - } - - if (root.contains("position")) - { - readVector3(root["position"], out.absolutePosition); - out.hasAbsolutePosition = true; - } - if (root.contains("rotation_euler_deg")) - { - readVector3(root["rotation_euler_deg"], out.absoluteRotationEulerDeg); - out.hasAbsoluteRotationEulerDeg = true; - } - - return true; -} - -bool deserializeSequenceSegmentJson(const json_t& root, nbl::core::CCameraSequenceSegment& out, std::string* error) -{ - if (!root.is_object()) - { - if (error) - *error = "Sequence segment must be an object."; - return false; - } - - out = {}; - if (root.contains("name")) - out.name = root["name"].get(); - if (root.contains("camera_identifier")) - out.cameraIdentifier = root["camera_identifier"].get(); - if (root.contains("camera_kind")) - { - if (!nbl::core::CCameraSequenceScriptUtilities::tryParseCameraKind(root["camera_kind"].get(), out.cameraKind)) - { - if (error) - *error = "Sequence segment has invalid camera_kind."; - return false; - } - } - if (root.contains("duration_seconds")) - { - out.durationSeconds = std::max(0.f, root["duration_seconds"].get()); - out.hasDurationSeconds = true; - } - if (root.contains("reset_camera")) - { - out.resetCamera = root["reset_camera"].get(); - out.hasResetCamera = true; - } - if (root.contains("presentations")) - { - if (!deserializeSequencePresentationsJson(root["presentations"], out.presentations, error)) - return false; - } - if (root.contains("continuity")) - { - if (!deserializeSequenceContinuityJson(root["continuity"], out.continuity, error)) - return false; - out.hasContinuity = true; - } - if (root.contains("captures")) - { - if (!root["captures"].is_array()) - { - if (error) - *error = "Sequence segment captures must be an array."; - return false; - } - - out.captureFractions.clear(); - for (const auto& entry : root["captures"]) - { - float fraction = 0.f; - if (!tryParseCaptureFractionJson(entry, fraction)) - { - if (error) - *error = "Sequence segment capture entry is invalid."; - return false; - } - out.captureFractions.emplace_back(fraction); - } - nbl::core::CCameraSequenceScriptUtilities::normalizeCaptureFractions(out.captureFractions); - out.hasCaptureFractions = true; - } - if (root.contains("keyframes")) - { - if (!root["keyframes"].is_array()) - { - if (error) - *error = "Sequence segment keyframes must be an array."; - return false; - } - for (const auto& entry : root["keyframes"]) - { - nbl::core::CCameraSequenceKeyframe keyframe; - if (!deserializeSequenceKeyframeJson(entry, keyframe, error)) - return false; - out.keyframes.emplace_back(std::move(keyframe)); - } - } - if (root.contains("target_keyframes")) - { - if (!root["target_keyframes"].is_array()) - { - if (error) - *error = "Sequence segment target_keyframes must be an array."; - return false; - } - for (const auto& entry : root["target_keyframes"]) - { - nbl::core::CCameraSequenceTrackedTargetKeyframe keyframe; - if (!deserializeSequenceTrackedTargetKeyframeJson(entry, keyframe, error)) - return false; - out.targetKeyframes.emplace_back(std::move(keyframe)); - } - } - - if (out.keyframes.empty()) - { - if (error) - *error = "Sequence segment requires at least one keyframe."; - return false; - } - if (out.cameraKind == nbl::core::ICamera::CameraKind::Unknown && out.cameraIdentifier.empty()) - { - if (error) - *error = "Sequence segment requires camera_kind or camera_identifier."; - return false; - } - - return true; -} - -bool deserializeCameraSequenceScriptJson(const json_t& root, nbl::core::CCameraSequenceScript& out, std::string* error) -{ - if (!root.is_object()) - { - if (error) - *error = "Camera sequence script must be an object."; - return false; - } - - out = {}; - if (root.contains("enabled")) - out.enabled = root["enabled"].get(); - if (root.contains("log")) - out.log = root["log"].get(); - if (root.contains("exclusive")) - out.exclusive = root["exclusive"].get(); - if (root.contains("exclusive_input")) - out.exclusive = root["exclusive_input"].get() || out.exclusive; - if (root.contains("hard_fail")) - out.hardFail = root["hard_fail"].get(); - if (root.contains("visual_debug")) - out.visualDebug = root["visual_debug"].get(); - if (root.contains("visual_debug_target_fps")) - out.visualDebugTargetFps = root["visual_debug_target_fps"].get(); - if (root.contains("visual_debug_hold_seconds")) - out.visualDebugHoldSeconds = root["visual_debug_hold_seconds"].get(); - if (root.contains("enableActiveCameraMovement")) - { - out.enableActiveCameraMovement = root["enableActiveCameraMovement"].get(); - out.hasEnableActiveCameraMovement = true; - } - if (root.contains("capture_prefix")) - out.capturePrefix = root["capture_prefix"].get(); - if (root.contains("fps")) - out.fps = std::max(1.f, root["fps"].get()); - - if (root.contains("defaults")) - { - const auto& defaults = root["defaults"]; - if (!defaults.is_object()) - { - if (error) - *error = "Camera sequence defaults must be an object."; - return false; - } - - if (defaults.contains("duration_seconds")) - out.defaults.durationSeconds = std::max(0.f, defaults["duration_seconds"].get()); - if (defaults.contains("reset_camera")) - out.defaults.resetCamera = defaults["reset_camera"].get(); - if (defaults.contains("presentations")) - { - if (!deserializeSequencePresentationsJson(defaults["presentations"], out.defaults.presentations, error)) - return false; - } - if (defaults.contains("continuity")) - { - if (!deserializeSequenceContinuityJson(defaults["continuity"], out.defaults.continuity, error)) - return false; - } - if (defaults.contains("captures")) - { - if (!defaults["captures"].is_array()) - { - if (error) - *error = "Camera sequence default captures must be an array."; - return false; - } - - out.defaults.captureFractions.clear(); - for (const auto& entry : defaults["captures"]) - { - float fraction = 0.f; - if (!tryParseCaptureFractionJson(entry, fraction)) - { - if (error) - *error = "Camera sequence default capture entry is invalid."; - return false; - } - out.defaults.captureFractions.emplace_back(fraction); - } - nbl::core::CCameraSequenceScriptUtilities::normalizeCaptureFractions(out.defaults.captureFractions); - } - } - - if (!root.contains("segments") || !root["segments"].is_array()) - { - if (error) - *error = "Camera sequence script requires a \"segments\" array."; - return false; - } - - for (const auto& entry : root["segments"]) - { - nbl::core::CCameraSequenceSegment segment; - if (!deserializeSequenceSegmentJson(entry, segment, error)) - return false; - out.segments.emplace_back(std::move(segment)); - } - - if (out.segments.empty()) - { - if (error) - *error = "Camera sequence script must contain at least one segment."; - return false; - } - - return true; -} - -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) -{ - if (button == "LEFT_BUTTON") - return nbl::ui::EMB_LEFT_BUTTON; - if (button == "RIGHT_BUTTON") - return nbl::ui::EMB_RIGHT_BUTTON; - if (button == "MIDDLE_BUTTON") - return nbl::ui::EMB_MIDDLE_BUTTON; - if (button == "BUTTON_4") - return nbl::ui::EMB_BUTTON_4; - if (button == "BUTTON_5") - return nbl::ui::EMB_BUTTON_5; - return std::nullopt; -} - -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 parseScriptedCaptureFramesJson(const json_t& script, nbl::system::CCameraScriptedInputParseResult& out) -{ - if (!script.contains("capture_frames")) - return; - - for (const auto& frame : script["capture_frames"]) - out.timeline.captureFrames.emplace_back(frame.get()); -} - -void parseScriptedControlOverridesJson(const json_t& controls, nbl::system::CCameraScriptedControlOverrides& out) -{ - if (controls.contains("keyboard_scale")) - { - out.hasKeyboardScale = true; - out.keyboardScale = controls["keyboard_scale"].get(); - } - if (controls.contains("mouse_move_scale")) - { - out.hasMouseMoveScale = true; - out.mouseMoveScale = controls["mouse_move_scale"].get(); - } - if (controls.contains("mouse_scroll_scale")) - { - out.hasMouseScrollScale = true; - out.mouseScrollScale = controls["mouse_scroll_scale"].get(); - } - if (controls.contains("translation_scale")) - { - out.hasTranslationScale = true; - out.translationScale = controls["translation_scale"].get(); - } - if (controls.contains("rotation_scale")) - { - out.hasRotationScale = true; - out.rotationScale = controls["rotation_scale"].get(); - } -} - -bool parseScriptedSequenceIfPresentJson(const json_t& script, nbl::system::CCameraScriptedInputParseResult& out, std::string* error) -{ - if (!script.contains("segments")) - return true; - - nbl::core::CCameraSequenceScript sequence; - if (!deserializeCameraSequenceScriptJson(script, sequence, error)) - return false; - - out.sequence = std::move(sequence); - return true; -} - -void appendScriptedCaptureFrame(nbl::system::CCameraScriptedInputParseResult& out, const uint64_t frame, const bool captureFrame) -{ - if (captureFrame) - out.timeline.captureFrames.emplace_back(frame); -} - -void parseScriptedKeyboardEventJson(const json_t& event, const uint64_t frame, const bool captureFrame, nbl::system::CCameraScriptedInputParseResult& out) -{ - if (!event.contains("key") || !event.contains("action")) - { - nbl::system::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::system::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Scripted keyboard event has invalid key \"" + keyText + "\"."); - return; - } - - const auto action = parseScriptedKeyboardAction(actionText); - if (!action.has_value()) - { - nbl::system::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::system::CCameraScriptedInputParseResult& out) -{ - if (!event.contains("kind")) - { - nbl::system::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.dx = event.value("dx", 0); - entry.mouse.dy = event.value("dy", 0); - } - else if (kind == "scroll") - { - entry.mouse.type = nbl::system::CCameraScriptedInputEvent::MouseData::Type::Scroll; - entry.mouse.v = event.value("v", 0); - entry.mouse.h = event.value("h", 0); - } - else if (kind == "click") - { - if (!event.contains("button") || !event.contains("action")) - { - nbl::system::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::system::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Scripted click event has invalid button \"" + buttonText + "\"."); - return; - } - - const auto action = parseScriptedMouseClickAction(actionText); - if (!action.has_value()) - { - nbl::system::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.x = event.value("x", 0); - entry.mouse.y = event.value("y", 0); - } - else - { - nbl::system::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::system::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::system::CCameraScriptedInputEvent::ActionData& action, nbl::system::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::system::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::system::CCameraScriptedInputParseResult& out) -{ - if (!event.contains("action")) - { - nbl::system::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Scripted action event missing \"action\"."); - return; - } - - const auto actionText = event["action"].get(); - nbl::system::CCameraScriptedInputEvent entry; - entry.frame = frame; - entry.type = nbl::system::CCameraScriptedInputEvent::Type::Action; - - if (actionText == "set_active_render_window") - { - entry.action.kind = nbl::system::CCameraScriptedInputEvent::ActionData::Kind::SetActiveRenderWindow; - entry.action.value = parseScriptedActionIntValue(event); - } - else if (actionText == "set_active_planar") - { - entry.action.kind = nbl::system::CCameraScriptedInputEvent::ActionData::Kind::SetActivePlanar; - entry.action.value = parseScriptedActionIntValue(event); - } - else if (actionText == "set_projection_type") - { - entry.action.kind = nbl::system::CCameraScriptedInputEvent::ActionData::Kind::SetProjectionType; - if (!parseScriptedProjectionActionValue(event, entry.action, out)) - return; - } - else if (actionText == "set_projection_index") - { - entry.action.kind = nbl::system::CCameraScriptedInputEvent::ActionData::Kind::SetProjectionIndex; - entry.action.value = parseScriptedActionIntValue(event); - } - else if (actionText == "set_use_window") - { - entry.action.kind = nbl::system::CCameraScriptedInputEvent::ActionData::Kind::SetUseWindow; - entry.action.value = event.value("value", false) ? 1 : 0; - } - else if (actionText == "set_left_handed") - { - entry.action.kind = nbl::system::CCameraScriptedInputEvent::ActionData::Kind::SetLeftHanded; - entry.action.value = event.value("value", false) ? 1 : 0; - } - else if (actionText == "reset_active_camera") - { - entry.action.kind = nbl::system::CCameraScriptedInputEvent::ActionData::Kind::ResetActiveCamera; - entry.action.value = 1; - } - else - { - nbl::system::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Scripted action event has invalid action \"" + actionText + "\"."); - return; - } - - out.timeline.events.emplace_back(std::move(entry)); - appendScriptedCaptureFrame(out, frame, captureFrame); -} - -void parseScriptedInputEventJson(const json_t& event, nbl::system::CCameraScriptedInputParseResult& out) -{ - if (!event.contains("frame") || !event.contains("type")) - { - nbl::system::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::system::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Scripted input event has invalid type \"" + type + "\"."); -} - -void parseScriptedInputEventsJson(const json_t& script, nbl::system::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::system::CCameraScriptedInputParseResult& out) -{ - outCheck.kind = nbl::system::CCameraScriptedInputCheck::Kind::ImguizmoVirtual; - outCheck.tolerance = check.value("tolerance", outCheck.tolerance); - - if (!check.contains("events")) - { - nbl::system::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Imguizmo virtual check missing \"events\"."); - return false; - } - - for (const auto& expectedEvent : check["events"]) - { - if (!expectedEvent.contains("type") || !expectedEvent.contains("magnitude")) - { - nbl::system::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::system::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::system::CCameraScriptedInputParseResult& out) -{ - if (!check.contains("frame") || !check.contains("kind")) - { - nbl::system::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::system::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "gimbal_step check requires at least one delta constraint."); - return false; - } - } - else - { - nbl::system::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::system::CCameraScriptedInputParseResult& out) -{ - if (!script.contains("checks")) - return; - - for (const auto& check : script["checks"]) - parseScriptedCheckJson(check, out); -} - -namespace nbl::system -{ - -bool readCameraSequenceScript(std::istream& in, core::CCameraSequenceScript& out, std::string* error) -{ - if (!in) - { - if (error) - *error = "Input stream is not readable."; - return false; - } - - json_t root; - in >> root; - return deserializeCameraSequenceScriptJson(root, out, error); -} - -bool readCameraSequenceScript(std::string_view text, core::CCameraSequenceScript& out, std::string* error) -{ - std::istringstream stream{std::string(text)}; - return readCameraSequenceScript(stream, out, error); -} - -bool loadCameraSequenceScriptFromFile(ISystem& system, const path& filePath, core::CCameraSequenceScript& out, std::string* error) -{ - std::string text; - if (!CCameraFileUtilities::readTextFile(system, filePath, text, error, "Cannot open camera sequence script file.")) - return false; - - return readCameraSequenceScript(text, out, error); -} - -bool readCameraScriptedInput(std::istream& in, CCameraScriptedInputParseResult& out, std::string* error) -{ - if (!in) - { - if (error) - *error = "Input stream is not readable."; - return false; - } - - json_t script; - in >> script; - - 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"; - - parseScriptedCaptureFramesJson(script, out); - - if (script.contains("camera_controls")) - parseScriptedControlOverridesJson(script["camera_controls"], out.cameraControls); - - if (!parseScriptedSequenceIfPresentJson(script, out, error)) - return false; - - parseScriptedInputEventsJson(script, out); - parseScriptedChecksJson(script, out); - - CCameraScriptedRuntimeUtilities::finalizeScriptedTimeline(out.timeline); - return true; -} - -bool readCameraScriptedInput(std::string_view text, CCameraScriptedInputParseResult& out, std::string* error) -{ - std::istringstream stream{std::string(text)}; - return readCameraScriptedInput(stream, out, error); -} - -bool loadCameraScriptedInputFromFile(ISystem& system, const path& filePath, CCameraScriptedInputParseResult& out, std::string* error) -{ - std::string text; - if (!CCameraFileUtilities::readTextFile(system, filePath, text, error, "Cannot open scripted input file.")) - return false; - - return readCameraScriptedInput(text, out, error); -} - -} // namespace nbl::system From 7f78de408ae1ebb8a06868f3f3399af1b867754e Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Fri, 10 Apr 2026 13:29:38 +0200 Subject: [PATCH 200/205] Adapt 61_UI to cameras ext cleanup --- 61_UI/AppCameraConfiguration.cpp | 1 - 61_UI/AppCameraPlanarRuntime.cpp | 16 +- 61_UI/AppControlPanel.cpp | 8 +- 61_UI/AppControlPanelCameraTab.cpp | 18 +- 61_UI/AppFollowRuntime.cpp | 2 +- 61_UI/AppHeadlessCameraSmoke.cpp | 1 - 61_UI/AppHeadlessCameraSmokeChecks.inl | 43 +- 61_UI/AppHeadlessCameraSmokeHelpers.inl | 47 +- 61_UI/AppInputRuntime.cpp | 2 +- 61_UI/AppManipulableObjects.cpp | 6 +- 61_UI/AppSceneDebugInstances.cpp | 9 +- 61_UI/AppScriptedInitialization.cpp | 22 +- 61_UI/AppScriptedInputRuntime.cpp | 40 +- 61_UI/AppUpdate.cpp | 2 +- 61_UI/CCameraScriptedRuntimePersistence.cpp | 646 ++++++++++++++++++ 61_UI/README.md | 4 +- 61_UI/include/app/App.hpp | 8 +- .../AppProjectionControlPanelUiUtilities.hpp | 4 +- 61_UI/include/app/AppRenderPassUtilities.hpp | 2 +- 61_UI/include/app/AppTypes.hpp | 8 +- .../app/AppViewportBindingUtilities.hpp | 2 +- .../camera/CCameraConstraintUtilities.hpp | 94 +++ .../camera/CCameraScriptedActionUtilities.hpp | 82 +++ .../CCameraScriptedRuntimePersistence.hpp | 64 ++ .../camera/CCameraSequenceScriptedBuilder.hpp | 117 ++++ 61_UI/include/common.hpp | 35 +- 26 files changed, 1177 insertions(+), 106 deletions(-) create mode 100644 61_UI/CCameraScriptedRuntimePersistence.cpp create mode 100644 61_UI/include/camera/CCameraConstraintUtilities.hpp create mode 100644 61_UI/include/camera/CCameraScriptedActionUtilities.hpp create mode 100644 61_UI/include/camera/CCameraScriptedRuntimePersistence.hpp create mode 100644 61_UI/include/camera/CCameraSequenceScriptedBuilder.hpp diff --git a/61_UI/AppCameraConfiguration.cpp b/61_UI/AppCameraConfiguration.cpp index 406c86a0b..788c8962a 100644 --- a/61_UI/AppCameraConfiguration.cpp +++ b/61_UI/AppCameraConfiguration.cpp @@ -9,7 +9,6 @@ #include "app/AppResourceUtilities.hpp" #include "app/AppViewportBindingUtilities.hpp" #include "nbl/ext/Cameras/CCameraPersistence.hpp" -#include "nbl/ext/Cameras/CCameraScriptedRuntimePersistence.hpp" bool App::initializeCameraConfiguration(const argparse::ArgumentParser& program) { diff --git a/61_UI/AppCameraPlanarRuntime.cpp b/61_UI/AppCameraPlanarRuntime.cpp index 075ce5dd6..16a3dc4fd 100644 --- a/61_UI/AppCameraPlanarRuntime.cpp +++ b/61_UI/AppCameraPlanarRuntime.cpp @@ -46,9 +46,23 @@ bool tryCaptureInitialPlanarPresets( !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; + " for camera kind \"" + kindLabel + "\": " + reason + goalDetails; return false; } diff --git a/61_UI/AppControlPanel.cpp b/61_UI/AppControlPanel.cpp index eed7a548f..0b6e559ae 100644 --- a/61_UI/AppControlPanel.cpp +++ b/61_UI/AppControlPanel.cpp @@ -3,7 +3,7 @@ bool App::savePresetsToFile(const nbl::system::path& path) { - return nbl::system::savePresetCollectionToFile( + return nbl::system::CCameraPersistenceUtilities::savePresetCollectionToFile( *m_system, path, std::span(m_presetAuthoring.presets.data(), m_presetAuthoring.presets.size())); @@ -11,17 +11,17 @@ bool App::savePresetsToFile(const nbl::system::path& path) bool App::loadPresetsFromFile(const nbl::system::path& path) { - return nbl::system::loadPresetCollectionFromFile(*m_system, path, m_presetAuthoring.presets); + return nbl::system::CCameraPersistenceUtilities::loadPresetCollectionFromFile(*m_system, path, m_presetAuthoring.presets); } bool App::saveKeyframesToFile(const nbl::system::path& path) { - return nbl::system::saveKeyframeTrackToFile(*m_system, path, m_playbackAuthoring.keyframeTrack); + return nbl::system::CCameraKeyframeTrackPersistenceUtilities::saveKeyframeTrackToFile(*m_system, path, m_playbackAuthoring.keyframeTrack); } bool App::loadKeyframesFromFile(const nbl::system::path& path) { - if (!nbl::system::loadKeyframeTrackFromFile(*m_system, path, m_playbackAuthoring.keyframeTrack)) + if (!nbl::system::CCameraKeyframeTrackPersistenceUtilities::loadKeyframeTrackFromFile(*m_system, path, m_playbackAuthoring.keyframeTrack)) return false; clampPlaybackTimeToKeyframes(); diff --git a/61_UI/AppControlPanelCameraTab.cpp b/61_UI/AppControlPanelCameraTab.cpp index 82e80a89b..a6b6edf15 100644 --- a/61_UI/AppControlPanelCameraTab.cpp +++ b/61_UI/AppControlPanelCameraTab.cpp @@ -73,13 +73,13 @@ void App::drawControlPanelCameraTab(const nbl::ui::SCameraControlPanelStyle& pan const bool hasOrbitTarget = activeCamera && activeCamera->tryGetSphericalTargetState(orbitState); if (hasOrbitTarget) { - auto target = getCastedVector(orbitState.target); + auto target = hlsl::CCameraMathUtilities::castVector(orbitState.target); if (ImGui::InputFloat3("Target", &target[0])) - activeCamera->trySetSphericalTarget(getCastedVector(target)); + 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(getMatrix3x4As4x4(m_sceneInteraction.model))[3]; + const auto targetPos = hlsl::transpose(hlsl::CCameraMathUtilities::promoteAffine3x4To4x4(m_sceneInteraction.model))[3]; activeCamera->trySetSphericalTarget(float64_t3(targetPos.x, targetPos.y, targetPos.z)); } ImGui::SameLine(); @@ -116,9 +116,9 @@ void App::drawControlPanelCameraTab(const nbl::ui::SCameraControlPanelStyle& pan if (followStateChanged && followConfig.enabled) applyFollowToConfiguredCameras(); - auto trackedTarget = getCastedVector(m_sceneInteraction.followTarget.getGimbal().getPosition()); + auto trackedTarget = hlsl::CCameraMathUtilities::castVector(m_sceneInteraction.followTarget.getGimbal().getPosition()); if (ImGui::InputFloat3("Tracked target", &trackedTarget[0])) - m_sceneInteraction.followTarget.setPosition(getCastedVector(trackedTarget)); + 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" }); @@ -136,15 +136,15 @@ void App::drawControlPanelCameraTab(const nbl::ui::SCameraControlPanelStyle& pan if (CCameraFollowUtilities::cameraFollowModeUsesWorldOffset(followConfig.mode)) { - auto worldOffset = getCastedVector(followConfig.worldOffset); + auto worldOffset = hlsl::CCameraMathUtilities::castVector(followConfig.worldOffset); if (ImGui::InputFloat3("World offset", &worldOffset[0])) - followConfig.worldOffset = getCastedVector(worldOffset); + followConfig.worldOffset = hlsl::CCameraMathUtilities::castVector(worldOffset); } if (CCameraFollowUtilities::cameraFollowModeUsesLocalOffset(followConfig.mode)) { - auto localOffset = getCastedVector(followConfig.localOffset); + auto localOffset = hlsl::CCameraMathUtilities::castVector(followConfig.localOffset); if (ImGui::InputFloat3("Local offset", &localOffset[0])) - followConfig.localOffset = getCastedVector(localOffset); + followConfig.localOffset = hlsl::CCameraMathUtilities::castVector(localOffset); } } else diff --git a/61_UI/AppFollowRuntime.cpp b/61_UI/AppFollowRuntime.cpp index 5cf15e034..984f8ce43 100644 --- a/61_UI/AppFollowRuntime.cpp +++ b/61_UI/AppFollowRuntime.cpp @@ -150,7 +150,7 @@ void App::resetFollowTargetToDefault() void App::snapFollowTargetToModel() { - const auto modelTransform = hlsl::transpose(getMatrix3x4As4x4(m_sceneInteraction.model)); + const auto modelTransform = hlsl::transpose(hlsl::CCameraMathUtilities::promoteAffine3x4To4x4(m_sceneInteraction.model)); setFollowTargetTransform(getCastedMatrix(modelTransform)); } diff --git a/61_UI/AppHeadlessCameraSmoke.cpp b/61_UI/AppHeadlessCameraSmoke.cpp index 281d71e7b..a05de83ac 100644 --- a/61_UI/AppHeadlessCameraSmoke.cpp +++ b/61_UI/AppHeadlessCameraSmoke.cpp @@ -12,7 +12,6 @@ #include "app/AppResourceUtilities.hpp" #include "nbl/ext/Cameras/CCameraPathUtilities.hpp" #include "nbl/ext/Cameras/CCameraPersistence.hpp" -#include "nbl/ext/Cameras/CCameraScriptedRuntimePersistence.hpp" #include "nbl/ext/Cameras/CCameraSmokeRegressionUtilities.hpp" #include "nlohmann/json.hpp" #include "AppHeadlessCameraSmokeHelpers.inl" diff --git a/61_UI/AppHeadlessCameraSmokeChecks.inl b/61_UI/AppHeadlessCameraSmokeChecks.inl index beb2a91e6..e47dacedb 100644 --- a/61_UI/AppHeadlessCameraSmokeChecks.inl +++ b/61_UI/AppHeadlessCameraSmokeChecks.inl @@ -42,7 +42,7 @@ state.goalSolver, state.orbitCamera, state.initialPresets.path.value(), - CCameraGoalSolver::SApplyResult::MissingPathState, + CCameraGoalSolver::SApplyResult::EIssue::MissingPathState, "Path->Orbit", outError)) { @@ -56,7 +56,7 @@ state.goalSolver, state.orbitCamera, state.initialPresets.dollyZoom.value(), - CCameraGoalSolver::SApplyResult::MissingDynamicPerspectiveState, + CCameraGoalSolver::SApplyResult::EIssue::MissingDynamicPerspectiveState, "DollyZoom->Orbit", outError)) { @@ -588,15 +588,15 @@ const auto sourcePresetSpan = std::span(sourcePresets.data(), sourcePresets.size()); - std::stringstream presetBuffer; - if (!nbl::system::writePresetCollection(presetBuffer, sourcePresetSpan)) + 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::readPresetCollection(presetBuffer, loadedPresets)) + if (!nbl::system::CCameraPersistenceUtilities::deserializePresetCollection(presetText, loadedPresets)) { outError = "Preset persistence smoke failed to deserialize preset collection."; return false; @@ -623,15 +623,15 @@ } sourceTrack.selectedKeyframeIx = static_cast(sourceTrack.keyframes.size()) - 1; - std::stringstream keyframeBuffer; - if (!nbl::system::writeKeyframeTrack(keyframeBuffer, sourceTrack)) + 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::readKeyframeTrack(keyframeBuffer, loadedTrack)) + if (!nbl::system::CCameraKeyframeTrackPersistenceUtilities::deserializeKeyframeTrack(keyframeText, loadedTrack)) { outError = "Keyframe persistence smoke failed to deserialize track."; return false; @@ -668,14 +668,14 @@ auto& system = *state.system; - if (!nbl::system::savePresetCollectionToFile(system, presetFile, sourcePresetSpan)) + 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::loadPresetCollectionFromFile(system, presetFile, fileLoadedPresets)) + if (!nbl::system::CCameraPersistenceUtilities::loadPresetCollectionFromFile(system, presetFile, fileLoadedPresets)) { outError = "Preset persistence smoke failed to load preset collection file."; return false; @@ -691,14 +691,14 @@ return false; } - if (!nbl::system::saveKeyframeTrackToFile(system, keyframeFile, sourceTrack)) + if (!nbl::system::CCameraKeyframeTrackPersistenceUtilities::saveKeyframeTrackToFile(system, keyframeFile, sourceTrack)) { outError = "Keyframe persistence smoke failed to save track file."; return false; } CCameraKeyframeTrack fileLoadedTrack; - if (!nbl::system::loadKeyframeTrackFromFile(system, keyframeFile, fileLoadedTrack)) + if (!nbl::system::CCameraKeyframeTrackPersistenceUtilities::loadKeyframeTrackFromFile(system, keyframeFile, fileLoadedTrack)) { outError = "Keyframe persistence smoke failed to load track file."; return false; @@ -906,9 +906,11 @@ } CCameraScriptedTimeline scriptedTimeline; + std::vector actionEvents; std::string runtimeBuildError; - if (!nbl::system::CCameraSequenceScriptedBuilderUtilities::appendCompiledSequenceSegmentToScriptedTimeline( + if (!nbl::this_example::CCameraSequenceScriptedBuilderUtilities::appendCompiledSequenceSegmentToScriptedTimeline( scriptedTimeline, + actionEvents, SCameraSmokeSequenceDefaults::StartFrame, compiledSegment, { @@ -923,6 +925,7 @@ return false; } nbl::system::CCameraScriptedRuntimeUtilities::finalizeScriptedTimeline(scriptedTimeline); + nbl::this_example::CCameraScriptedActionUtilities::finalizeActionEvents(actionEvents); if (scriptedTimeline.captureFrames != std::vector( SCameraSmokeSequenceDefaults::CaptureFrames.begin(), @@ -961,15 +964,18 @@ } size_t runtimeNextEventIndex = 0u; + size_t runtimeNextActionIndex = 0u; CCameraScriptedFrameEvents runtimeBatch; + std::vector runtimeActions; nbl::system::CCameraScriptedFrameEventUtilities::dequeueScriptedFrameEvents(scriptedTimeline.events, runtimeNextEventIndex, SCameraSmokeSequenceDefaults::StartFrame, runtimeBatch); - if (runtimeBatch.actions.size() != 10u || runtimeBatch.goals.size() != 1u || + 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 (runtimeBatch.actions.front().kind != CCameraScriptedInputEvent::ActionData::Kind::SetActiveRenderWindow || + 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."; @@ -1440,7 +1446,7 @@ .pitchMinDeg = SCameraSmokeManipulationDefaults::PitchMinDeg, .pitchMaxDeg = SCameraSmokeManipulationDefaults::PitchMaxDeg }; - if (!nbl::core::CCameraManipulationUtilities::applyCameraConstraints(state.goalSolver, state.freeCamera, freeConstraints)) + if (!nbl::this_example::CCameraConstraintUtilities::applyCameraConstraints(state.goalSolver, state.freeCamera, freeConstraints)) { outError = "Camera manipulation utilities smoke failed to clamp Free camera orientation."; return false; @@ -1483,7 +1489,7 @@ state.initialPresets.orbit->goal.distance * SCameraSmokeManipulationDefaults::OrbitClampMinScale), .maxDistance = state.initialPresets.orbit->goal.distance * SCameraSmokeManipulationDefaults::OrbitClampMaxScale }; - if (!nbl::core::CCameraManipulationUtilities::applyCameraConstraints(state.goalSolver, state.orbitCamera, orbitConstraints)) + if (!nbl::this_example::CCameraConstraintUtilities::applyCameraConstraints(state.goalSolver, state.orbitCamera, orbitConstraints)) { outError = "Camera manipulation utilities smoke failed to clamp Orbit distance."; return false; @@ -1555,7 +1561,8 @@ outError = "Camera text utilities smoke failed for empty goal-state description."; return false; } - if (CCameraTextUtilities::describeGoalStateMask(ICamera::GoalStateSphericalTarget | ICamera::GoalStateDynamicPerspective) != "Spherical target, Dynamic perspective") + 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; diff --git a/61_UI/AppHeadlessCameraSmokeHelpers.inl b/61_UI/AppHeadlessCameraSmokeHelpers.inl index 8dc5a8c58..dc3393aa8 100644 --- a/61_UI/AppHeadlessCameraSmokeHelpers.inl +++ b/61_UI/AppHeadlessCameraSmokeHelpers.inl @@ -328,11 +328,11 @@ { switch (issue) { - case CCameraGoalSolver::SApplyResult::MissingPathState: + case CCameraGoalSolver::SApplyResult::EIssue::MissingPathState: return ICamera::GoalStatePath; - case CCameraGoalSolver::SApplyResult::MissingDynamicPerspectiveState: + case CCameraGoalSolver::SApplyResult::EIssue::MissingDynamicPerspectiveState: return ICamera::GoalStateDynamicPerspective; - case CCameraGoalSolver::SApplyResult::MissingSphericalTargetState: + case CCameraGoalSolver::SApplyResult::EIssue::MissingSphericalTargetState: return ICamera::GoalStateSphericalTarget; default: return ICamera::GoalStateNone; @@ -548,7 +548,7 @@ { const auto targetPosition = trackedTarget.getGimbal().getPosition(); const auto cameraPosition = camera ? camera->getGimbal().getPosition() : float64_t3(0.0); - const auto viewMatrix = camera ? hlsl::getMatrix3x4As4x4(camera->getGimbal().getViewMatrix()) : float64_t4x4(1.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 @@ -888,7 +888,7 @@ if (!targetCamera) return true; - const uint32_t expectedMissingGoalStateMask = expectedMissingGoalStateMaskForIssue(expectedIssue); + 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) { @@ -1058,8 +1058,8 @@ std::string& outError) { const auto markerWorld = buildFollowTargetMarkerWorldForSmoke(trackedTarget); - const auto markerTransform = hlsl::transpose(getMatrix3x4As4x4(markerWorld)); - const auto markerPosition = getCastedVector(float32_t3(markerTransform[3])); + 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) @@ -1170,10 +1170,11 @@ inline bool verifyScriptedRuntimeFrameBatch(std::string* const outError) { CCameraScriptedTimeline timeline = {}; - nbl::system::CCameraScriptedRuntimeUtilities::appendScriptedActionEvent( - timeline, + std::vector actionEvents; + nbl::this_example::CCameraScriptedActionUtilities::appendActionEvent( + actionEvents, SCameraSmokeRuntimeDefaults::ActionFrame, - CCameraScriptedInputEvent::ActionData::Kind::SetActivePlanar, + nbl::this_example::ECameraScriptedActionCode::SetActivePlanar, SCameraSmokeRuntimeDefaults::ActivePlanarValue); { CCameraGoal goal = {}; @@ -1189,19 +1190,23 @@ 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); - if (nextEventIndex != 3u || batch.actions.size() != 1u || batch.goals.size() != 1u || + 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 (batch.actions.front().kind != CCameraScriptedInputEvent::ActionData::Kind::SetActivePlanar || - batch.actions.front().value != SCameraSmokeRuntimeDefaults::ActivePlanarValue || + 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) @@ -1210,8 +1215,9 @@ } 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 || - !batch.actions.empty() || !batch.goals.empty()) + !actions.empty() || !batch.goals.empty()) { if (outError) *outError = "Scripted runtime frame batch smoke failed for frame 4."; @@ -1230,10 +1236,10 @@ inline bool verifyScriptedRuntimeParser(std::string* const outError) { - nbl::system::CCameraScriptedInputParseResult parsed; + nbl::this_example::CCameraScriptedInputParseResult parsed; std::string parseError; const std::string scriptText = makeScriptedRuntimeParserSmokeJson().dump(); - if (!nbl::system::readCameraScriptedInput(scriptText, parsed, &parseError)) + if (!nbl::this_example::CCameraScriptedRuntimePersistenceUtilities::readCameraScriptedInput(scriptText, parsed, &parseError)) { if (outError) *outError = "Scripted runtime parser smoke failed to parse low-level runtime payload. " + parseError; @@ -1248,7 +1254,7 @@ *outError = "Scripted runtime parser smoke lost top-level metadata."; return false; } - if (parsed.timeline.events.size() != 2u || parsed.timeline.checks.size() != 2u || parsed.timeline.captureFrames.size() != 1u) + 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."; @@ -1262,11 +1268,14 @@ } size_t nextEventIndex = 0u; + size_t nextActionIndex = 0u; CCameraScriptedFrameEvents batch; + std::vector actions; nbl::system::CCameraScriptedFrameEventUtilities::dequeueScriptedFrameEvents(parsed.timeline.events, nextEventIndex, SCameraSmokeRuntimeParserDefaults::EventFrame, batch); - if (batch.actions.size() != 1u || + nbl::this_example::CCameraScriptedActionUtilities::dequeueFrameActions(parsed.actionEvents, nextActionIndex, SCameraSmokeRuntimeParserDefaults::EventFrame, actions); + if (actions.size() != 1u || batch.keyboard.size() != 1u || - batch.actions.front().value != SCameraSmokeRuntimeParserDefaults::ActivePlanarValue) + actions.front().value != SCameraSmokeRuntimeParserDefaults::ActivePlanarValue) { if (outError) *outError = "Scripted runtime parser smoke produced wrong frame-two batch."; diff --git a/61_UI/AppInputRuntime.cpp b/61_UI/AppInputRuntime.cpp index 876f51ea8..a403a62a6 100644 --- a/61_UI/AppInputRuntime.cpp +++ b/61_UI/AppInputRuntime.cpp @@ -156,7 +156,7 @@ inline void applyCollectedVirtualEventsToCamera( target->manipulate({ collectedVirtualEvents.events.data(), collectedVirtualEvents.totalCount() }); } - nbl::core::CCameraManipulationUtilities::applyCameraConstraints(goalSolver, target, cameraConstraints); + nbl::this_example::CCameraConstraintUtilities::applyCameraConstraints(goalSolver, target, cameraConstraints); if (!scriptedInputEnabled) refreshFollowOffsets(planarIx); appendVirtualEventLog(target, planarIx, collectedVirtualEvents); diff --git a/61_UI/AppManipulableObjects.cpp b/61_UI/AppManipulableObjects.cpp index 06117665b..36b494469 100644 --- a/61_UI/AppManipulableObjects.cpp +++ b/61_UI/AppManipulableObjects.cpp @@ -2,7 +2,7 @@ inline float32_t4x4 buildModelManipulationTransform(const float32_t3x4& model) { - return hlsl::transpose(getMatrix3x4As4x4(model)); + return hlsl::transpose(hlsl::CCameraMathUtilities::promoteAffine3x4To4x4(model)); } inline float32_t3 extractWorldPosition(const float32_t4x4& transform) @@ -17,7 +17,7 @@ inline float32_t4x4 buildCameraManipulationTransform(ICamera& camera) inline float32_t3 buildCameraWorldPosition(ICamera& camera) { - return getCastedVector(camera.getGimbal().getPosition()); + return hlsl::CCameraMathUtilities::castVector(camera.getGimbal().getPosition()); } inline float32_t4x4 buildFollowTargetTransform(const CTrackedTarget& trackedTarget) @@ -27,7 +27,7 @@ inline float32_t4x4 buildFollowTargetTransform(const CTrackedTarget& trackedTarg inline float32_t3 buildFollowTargetWorldPosition(const CTrackedTarget& trackedTarget) { - return getCastedVector(trackedTarget.getGimbal().getPosition()); + return hlsl::CCameraMathUtilities::castVector(trackedTarget.getGimbal().getPosition()); } uint32_t App::getManipulableObjectCount() const diff --git a/61_UI/AppSceneDebugInstances.cpp b/61_UI/AppSceneDebugInstances.cpp index b64f15f50..719832109 100644 --- a/61_UI/AppSceneDebugInstances.cpp +++ b/61_UI/AppSceneDebugInstances.cpp @@ -14,12 +14,9 @@ void App::updateAuxSceneInstances(const size_t geometryCount) float32_t3x4 gridWorld = float32_t3x4(1.0f); gridWorld[0][0] = SCameraAppSceneDebugDefaults::GridExtent; gridWorld[2][2] = SCameraAppSceneDebugDefaults::GridExtent; - hlsl::setTranslation( - gridWorld, - float32_t3( - -0.5f * SCameraAppSceneDebugDefaults::GridExtent, - SCameraAppSceneDebugDefaults::GridVerticalOffset, - -0.5f * SCameraAppSceneDebugDefaults::GridExtent)); + gridWorld[0][3] = -0.5f * SCameraAppSceneDebugDefaults::GridExtent; + gridWorld[1][3] = SCameraAppSceneDebugDefaults::GridVerticalOffset; + gridWorld[2][3] = -0.5f * SCameraAppSceneDebugDefaults::GridExtent; gridInstance.world = gridWorld; } } diff --git a/61_UI/AppScriptedInitialization.cpp b/61_UI/AppScriptedInitialization.cpp index 4ac1b10f1..74fbe8925 100644 --- a/61_UI/AppScriptedInitialization.cpp +++ b/61_UI/AppScriptedInitialization.cpp @@ -2,11 +2,11 @@ #include "app/AppCameraConfigUtilities.hpp" #include "app/AppResourceUtilities.hpp" -#include "nbl/ext/Cameras/CCameraScriptedRuntimePersistence.hpp" void App::resetScriptedInputRuntimeState() { m_scriptedInput.nextEventIndex = 0u; + m_scriptedInput.nextActionIndex = 0u; m_scriptedInput.checkRuntime = {}; m_scriptedInput.nextCaptureIndex = 0u; m_scriptedInput.failed = false; @@ -19,11 +19,12 @@ void App::finalizeScriptedInputRuntimeState() } void App::applyParsedScriptedInput( - nbl::system::CCameraScriptedInputParseResult parsed, + 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; @@ -78,6 +79,7 @@ void App::applyParsedScriptedInput( pendingScriptedSequence = std::move(parsed.sequence); m_scriptedInput.timeline = std::move(parsed.timeline); + m_scriptedInput.actionEvents = std::move(parsed.actionEvents); finalizeScriptedInputRuntimeState(); } @@ -93,9 +95,9 @@ bool App::tryLoadConfiguredScriptedInput( if (scriptedText.empty()) return true; - nbl::system::CCameraScriptedInputParseResult parsed = {}; + nbl::this_example::CCameraScriptedInputParseResult parsed = {}; std::string scriptedInputParseError; - if (!nbl::system::readCameraScriptedInput(scriptedText, parsed, &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); @@ -150,13 +152,14 @@ std::optional App::resolveSequenceSegmentPlanarIx(const CCameraSequenc bool App::expandPendingScriptedSequence(const CCameraSequenceScript& sequence) { CCameraScriptedTimeline timeline; + std::vector actionEvents; resetScriptedInputRuntimeState(); const bool useWindowMode = nbl::core::CCameraSequenceScriptUtilities::sequenceScriptUsesMultiplePresentations(sequence); - nbl::system::CCameraScriptedRuntimeUtilities::appendScriptedActionEvent( - timeline, + nbl::this_example::CCameraScriptedActionUtilities::appendActionEvent( + actionEvents, 0u, - CCameraScriptedInputEvent::ActionData::Kind::SetUseWindow, + nbl::this_example::ECameraScriptedActionCode::SetUseWindow, useWindowMode ? 1 : 0); CCameraSequenceTrackedTargetPose referenceTrackedTargetPose = {}; @@ -207,8 +210,9 @@ bool App::expandPendingScriptedSequence(const CCameraSequenceScript& sequence) } std::string buildError; - if (!nbl::system::CCameraSequenceScriptedBuilderUtilities::appendCompiledSequenceSegmentToScriptedTimeline( + if (!nbl::this_example::CCameraSequenceScriptedBuilderUtilities::appendCompiledSequenceSegmentToScriptedTimeline( timeline, + actionEvents, frameCursor, compiledSegment, { @@ -229,7 +233,9 @@ bool App::expandPendingScriptedSequence(const CCameraSequenceScript& sequence) } 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 index bed7bab0f..8c749a395 100644 --- a/61_UI/AppScriptedInputRuntime.cpp +++ b/61_UI/AppScriptedInputRuntime.cpp @@ -32,6 +32,14 @@ void App::dequeueScriptedFrameInput(SScriptedFrameInputState& outFrame) 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, @@ -45,16 +53,16 @@ void App::dequeueScriptedFrameInput(SScriptedFrameInputState& outFrame) m_scriptedInput.visualPlanar.segmentLabel = outFrame.frameEvents.segmentLabels.back(); } -void App::applyScriptedFrameActions(const CCameraScriptedFrameEvents& scriptedFrameEvents) +void App::applyScriptedFrameActions(std::span scriptedActions) { - if (!(m_scriptedInput.enabled && !scriptedFrameEvents.actions.empty())) + if (!(m_scriptedInput.enabled && !scriptedActions.empty())) return; - auto applyAction = [&](const CCameraScriptedInputEvent::ActionData& action) -> void + auto applyAction = [&](const nbl::this_example::CCameraScriptedActionEvent& action) -> void { - switch (action.kind) + switch (static_cast(action.code)) { - case CCameraScriptedInputEvent::ActionData::Kind::SetActiveRenderWindow: + case nbl::this_example::ECameraScriptedActionCode::SetActiveRenderWindow: { if (action.value < 0 || static_cast(action.value) >= m_viewports.windowBindings.size()) { @@ -64,7 +72,7 @@ void App::applyScriptedFrameActions(const CCameraScriptedFrameEvents& scriptedFr m_viewports.activeRenderWindowIx = static_cast(action.value); } break; - case CCameraScriptedInputEvent::ActionData::Kind::SetActivePlanar: + case nbl::this_example::ECameraScriptedActionCode::SetActivePlanar: { if (action.value < 0) { @@ -86,7 +94,7 @@ void App::applyScriptedFrameActions(const CCameraScriptedFrameEvents& scriptedFr m_scriptedInput.visualPlanar.startFrame = m_realFrameIx; } break; - case CCameraScriptedInputEvent::ActionData::Kind::SetProjectionType: + case nbl::this_example::ECameraScriptedActionCode::SetProjectionType: { auto& binding = m_viewports.windowBindings[m_viewports.activeRenderWindowIx]; const auto type = static_cast(action.value); @@ -99,7 +107,7 @@ void App::applyScriptedFrameActions(const CCameraScriptedFrameEvents& scriptedFr } } break; - case CCameraScriptedInputEvent::ActionData::Kind::SetProjectionIndex: + case nbl::this_example::ECameraScriptedActionCode::SetProjectionIndex: { auto& binding = m_viewports.windowBindings[m_viewports.activeRenderWindowIx]; auto& projections = m_planarProjections[binding.activePlanarIx]->getPlanarProjections(); @@ -115,15 +123,15 @@ void App::applyScriptedFrameActions(const CCameraScriptedFrameEvents& scriptedFr static_cast(action.value)); } break; - case CCameraScriptedInputEvent::ActionData::Kind::SetUseWindow: + case nbl::this_example::ECameraScriptedActionCode::SetUseWindow: m_viewports.useWindow = action.value != 0; break; - case CCameraScriptedInputEvent::ActionData::Kind::SetLeftHanded: + case nbl::this_example::ECameraScriptedActionCode::SetLeftHanded: m_viewports.windowBindings[m_viewports.activeRenderWindowIx].leftHandedProjection = action.value != 0; break; - case CCameraScriptedInputEvent::ActionData::Kind::ResetActiveCamera: + case nbl::this_example::ECameraScriptedActionCode::ResetActiveCamera: { auto& binding = m_viewports.windowBindings[m_viewports.activeRenderWindowIx]; if (binding.activePlanarIx >= m_planarProjections.size()) @@ -144,15 +152,15 @@ void App::applyScriptedFrameActions(const CCameraScriptedFrameEvents& scriptedFr } }; - for (const auto& action : scriptedFrameEvents.actions) + for (const auto& action : scriptedActions) { - if (action.kind == CCameraScriptedInputEvent::ActionData::Kind::SetActiveRenderWindow) + if (nbl::this_example::CCameraScriptedActionUtilities::hasCode(action, nbl::this_example::ECameraScriptedActionCode::SetActiveRenderWindow)) applyAction(action); } - for (const auto& action : scriptedFrameEvents.actions) + for (const auto& action : scriptedActions) { - if (action.kind != CCameraScriptedInputEvent::ActionData::Kind::SetActiveRenderWindow) + if (!nbl::this_example::CCameraScriptedActionUtilities::hasCode(action, nbl::this_example::ECameraScriptedActionCode::SetActiveRenderWindow)) applyAction(action); } @@ -162,7 +170,7 @@ void App::applyScriptedFrameActions(const CCameraScriptedFrameEvents& scriptedFr "[script] frame %llu actions=%zu", ILogger::ELL_INFO, static_cast(m_realFrameIx), - scriptedFrameEvents.actions.size()); + scriptedActions.size()); } } diff --git a/61_UI/AppUpdate.cpp b/61_UI/AppUpdate.cpp index 5f829cef4..779fd6102 100644 --- a/61_UI/AppUpdate.cpp +++ b/61_UI/AppUpdate.cpp @@ -29,7 +29,7 @@ void App::prepareScriptedFrameState(SAppFrameUpdateState::SPreparedScriptedFrame outState = {}; outState.skipCameraInput = m_playbackAuthoring.playback.playing && m_playbackAuthoring.playback.overrideInput; dequeueScriptedFrameInput(outState.frame); - applyScriptedFrameActions(outState.frame.frameEvents); + applyScriptedFrameActions(outState.frame.actions); ensureScriptedVisualPlanarState(); } 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/README.md b/61_UI/README.md index 94246954d..acca847f2 100644 --- a/61_UI/README.md +++ b/61_UI/README.md @@ -130,8 +130,8 @@ It is no longer a giant committed frame dump. - [`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/nbl/ext/Cameras/CCameraScriptedRuntimePersistence.hpp) -- [`CCameraSequenceScriptedBuilder.hpp`](../../include/nbl/ext/Cameras/CCameraSequenceScriptedBuilder.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. diff --git a/61_UI/include/app/App.hpp b/61_UI/include/app/App.hpp index 2ba8bdfc5..8d1f467f6 100644 --- a/61_UI/include/app/App.hpp +++ b/61_UI/include/app/App.hpp @@ -32,6 +32,10 @@ namespace nbl::system struct SCameraAppResourceContext; struct SCameraConfigCollections; struct SCameraPlanarRuntimeBootstrap; +} + +namespace nbl::this_example +{ struct CCameraScriptedInputParseResult; } @@ -231,11 +235,11 @@ class App final : public examples::SimpleWindowedApplication, public examples::B bool submitAndPresentFrame(const SFrameSubmissionContext& frameContext); void resetScriptedInputRuntimeState(); void finalizeScriptedInputRuntimeState(); - void applyParsedScriptedInput(nbl::system::CCameraScriptedInputParseResult parsed, std::optional& pendingScriptedSequence); + 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(const CCameraScriptedFrameEvents& scriptedFrameEvents); + void applyScriptedFrameActions(std::span scriptedActions); void ensureScriptedVisualPlanarState(); void updateScriptedMouseButtons(std::span scriptedMouse); void appendScriptedInputEvents(const SScriptedFrameInputState& scriptedFrame, SCapturedUiEvents& capturedEvents); diff --git a/61_UI/include/app/AppProjectionControlPanelUiUtilities.hpp b/61_UI/include/app/AppProjectionControlPanelUiUtilities.hpp index 5cea675ee..a6418bd0d 100644 --- a/61_UI/include/app/AppProjectionControlPanelUiUtilities.hpp +++ b/61_UI/include/app/AppProjectionControlPanelUiUtilities.hpp @@ -322,8 +322,8 @@ inline void drawBoundCameraSection( if (ImGui::TreeNodeEx("World Data", flags)) { auto& gimbal = camera.getGimbal(); - const auto position = getCastedVector(gimbal.getPosition()); - const auto orientation = getCastedVector(gimbal.getOrientation().data); + 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); diff --git a/61_UI/include/app/AppRenderPassUtilities.hpp b/61_UI/include/app/AppRenderPassUtilities.hpp index be84eb5d2..925d1dccd 100644 --- a/61_UI/include/app/AppRenderPassUtilities.hpp +++ b/61_UI/include/app/AppRenderPassUtilities.hpp @@ -26,7 +26,7 @@ inline VkRect2D makeRenderArea(const uint32_t width, const uint32_t height) inline float32_t4x4 buildInverseViewRotation(const float32_t3x4& viewMatrix) { - auto inverseViewRotation = hlsl::transpose(getMatrix3x4As4x4(viewMatrix)); + auto inverseViewRotation = hlsl::transpose(hlsl::CCameraMathUtilities::promoteAffine3x4To4x4(viewMatrix)); const auto xyzMask = SCameraAppFrameRuntimeDefaults::InverseViewRotationXyzMask; inverseViewRotation[0] *= xyzMask; inverseViewRotation[1] *= xyzMask; diff --git a/61_UI/include/app/AppTypes.hpp b/61_UI/include/app/AppTypes.hpp index 0a29ca245..0a586d4aa 100644 --- a/61_UI/include/app/AppTypes.hpp +++ b/61_UI/include/app/AppTypes.hpp @@ -93,8 +93,8 @@ inline float32_t3x4 buildFollowTargetMarkerWorldTransform( const float markerScale) { const auto& targetGimbal = trackedTarget.getGimbal(); - const auto position = getCastedVector(targetGimbal.getPosition()); - const auto orientation = getCastedVector(targetGimbal.getOrientation().data); + 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), @@ -314,7 +314,9 @@ struct SScriptedInputRuntimeState final 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"; @@ -509,6 +511,7 @@ struct CameraControlSettings final struct SScriptedFrameInputState final { CCameraScriptedFrameEvents frameEvents = {}; + std::vector actions = {}; std::vector mouse; std::vector keyboard; std::vector imguizmoVirtualEvents; @@ -517,6 +520,7 @@ struct SScriptedFrameInputState final { return !keyboard.empty() || !mouse.empty() || + !actions.empty() || !frameEvents.imguizmo.empty() || !frameEvents.goals.empty() || !frameEvents.trackedTargetTransforms.empty(); diff --git a/61_UI/include/app/AppViewportBindingUtilities.hpp b/61_UI/include/app/AppViewportBindingUtilities.hpp index 442a2a99f..d2e27dfd9 100644 --- a/61_UI/include/app/AppViewportBindingUtilities.hpp +++ b/61_UI/include/app/AppViewportBindingUtilities.hpp @@ -180,7 +180,7 @@ inline bool tryBuildWindowBindingMatrices( outState.camera = camera; outState.projection = &projection; - outState.viewMatrix = getCastedMatrix(getMatrix3x4As4x4(camera->getGimbal().getViewMatrix())); + outState.viewMatrix = getCastedMatrix(hlsl::CCameraMathUtilities::promoteAffine3x4To4x4(camera->getGimbal().getViewMatrix())); outState.projectionMatrix = getCastedMatrix(projection.getProjectionMatrix()); outState.viewProjMatrix = mul(outState.projectionMatrix, outState.viewMatrix); 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 1563ef9e7..92cceeaf6 100644 --- a/61_UI/include/common.hpp +++ b/61_UI/include/common.hpp @@ -23,7 +23,6 @@ #include "nbl/ext/Cameras/CCameraKeyframeTrack.hpp" #include "nbl/ext/Cameras/CCameraPlaybackTimeline.hpp" #include "nbl/ext/Cameras/CCameraSequenceScript.hpp" -#include "nbl/ext/Cameras/CCameraSequenceScriptedBuilder.hpp" #include "nbl/ext/Cameras/CCameraScriptedRuntime.hpp" #include "nbl/ext/Cameras/CCameraScriptedUiInputUtilities.hpp" #include "nbl/ext/Cameras/CCameraScriptedCheckRunner.hpp" @@ -35,6 +34,10 @@ #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" @@ -51,6 +54,20 @@ #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; @@ -146,7 +163,6 @@ using nbl::core::CCameraSequenceTrackedTargetPose; using nbl::core::CCameraSequenceTrackedTargetTrack; using nbl::core::CCameraSequencePresentation; using nbl::core::CCameraSequenceContinuitySettings; -using nbl::system::CCameraSequenceScriptedSegmentBuildInfo; using nbl::system::CCameraScriptedInputEvent; using nbl::system::CCameraScriptedInputCheck; using nbl::system::CCameraScriptedFrameEvents; @@ -164,7 +180,15 @@ using nbl::system::SCameraFollowVisualMetrics; using nbl::ui::SCameraGoalApplyPresentation; using nbl::ui::SCameraGoalApplyPresentationBadges; using nbl::ui::SCameraCapturePresentation; -using nbl::core::SCameraConstraintSettings; +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; @@ -191,10 +215,7 @@ using nbl::ui::CCameraControlPanelUiUtilities; using nbl::ui::CCameraScriptVisualDebugOverlayUtilities; using nbl::ui::CCameraTextUtilities; using nbl::ui::CCameraViewportOverlayUtilities; -using nbl::hlsl::getCastedMatrix; -using nbl::hlsl::getCastedVector; -using nbl::hlsl::getMatrix3x4As4x4; -using nbl::hlsl::concatenateBFollowedByA; +using nbl::this_example::getCastedMatrix; using nbl::hlsl::mul; using nbl::hlsl::camera_quaternion_t; using nbl::core::CCameraFollowUtilities; From 95aec58fa2a79c418ce88fad020963c20e1f736a Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Fri, 10 Apr 2026 20:18:28 +0200 Subject: [PATCH 201/205] Materialize envmaps through NAM --- .gitignore | 2 ++ 61_UI/AppResourceUtilities.cpp | 4 +-- 61_UI/CMakeLists.txt | 16 ++++++++++ .../include/app/AppResourcePathUtilities.hpp | 29 ++++++++++++------- 61_UI/include/app/AppResourceUtilities.hpp | 4 ++- CMakeLists.txt | 4 +-- .../envmaps/space_spheremaps/LICENSE.txt.dvc | 5 ++++ ...rich_blue_nebulae_1_8k.rgba16f.envblob.dvc | 5 ++++ 8 files changed, 54 insertions(+), 15 deletions(-) create mode 100644 manifests/envmaps/space_spheremaps/LICENSE.txt.dvc create mode 100644 manifests/envmaps/space_spheremaps/rich_blue_nebulae_1_8k.rgba16f.envblob.dvc 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/61_UI/AppResourceUtilities.cpp b/61_UI/AppResourceUtilities.cpp index 40469b220..99892f507 100644 --- a/61_UI/AppResourceUtilities.cpp +++ b/61_UI/AppResourceUtilities.cpp @@ -60,7 +60,7 @@ bool mountOptionalSharedEnvmapResources( if (!context) return false; - auto sharedEnvmapDirectory = getSharedEnvmapDirectory(context.localInputCWD); + auto sharedEnvmapDirectory = getSharedEnvmapDirectory(); std::error_code ec; if (!std::filesystem::exists(sharedEnvmapDirectory, ec) || ec) return false; @@ -84,7 +84,7 @@ bool loadPreferredSpaceEnvBlob( if (!context) return false; - const auto candidates = makeSpaceEnvBlobCandidates(context.localInputCWD); + const auto candidates = makeSpaceEnvBlobCandidates(); return loadFirstCandidatePath( candidates.asSpan(), [&](const path& candidate) -> bool diff --git a/61_UI/CMakeLists.txt b/61_UI/CMakeLists.txt index aee61c176..835e58471 100644 --- a/61_UI/CMakeLists.txt +++ b/61_UI/CMakeLists.txt @@ -1,4 +1,6 @@ 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" @@ -25,6 +27,20 @@ if(TARGET Nabla::ext::FullScreenTriangle AND TARGET Nabla::ext::Cameras AND TARG nbl_create_executable_project("${NBL_EXTRA_SOURCES}" "" "${NBL_INCLUDE_SERACH_DIRECTORIES}" "${NBL_LIBRARIES}") + 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" diff --git a/61_UI/include/app/AppResourcePathUtilities.hpp b/61_UI/include/app/AppResourcePathUtilities.hpp index be868ff1f..9971b6eb2 100644 --- a/61_UI/include/app/AppResourcePathUtilities.hpp +++ b/61_UI/include/app/AppResourcePathUtilities.hpp @@ -5,6 +5,7 @@ #include #include "app/AppResourceUtilities.hpp" +#include "nbl/system/ModuleLookupUtils.h" #include "nbl/ext/Cameras/CCameraFileUtilities.hpp" namespace nbl::system @@ -197,20 +198,28 @@ inline bool loadRequestedCameraConfigText( error); } -inline path getSharedEnvmapDirectory(const path& localInputCWD) +inline path getExamplesRuntimeDirectory() { - return resolveInputPath( - localInputCWD, - path(SCameraMountedResourcePaths::SharedEnvmapRelativeDirectory)); + return (executableDirectory() / path(SCameraMountedResourcePaths::RuntimeRelativeDirectoryFromExecutable)).lexically_normal(); } -inline SCameraAppResourcePathCandidates makeSpaceEnvBlobCandidates(const path& localInputCWD) +inline path getSharedEnvmapDirectory() { - return makeResourcePathCandidates( - localInputCWD, - path(SCameraMountedResourcePaths::MountedSharedEnvmapWorkingDirectory) / - path(SCameraEnvmapResourcePaths::SpaceEnvBlobCandidate), - EResourceLookupPolicy::MountedOnly); + 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 diff --git a/61_UI/include/app/AppResourceUtilities.hpp b/61_UI/include/app/AppResourceUtilities.hpp index 7b1ffc350..812d3b8f0 100644 --- a/61_UI/include/app/AppResourceUtilities.hpp +++ b/61_UI/include/app/AppResourceUtilities.hpp @@ -25,7 +25,8 @@ 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 SharedEnvmapRelativeDirectory = "../media/envmap"; + static constexpr std::string_view RuntimeRelativeDirectoryFromExecutable = "../../runtime"; + static constexpr std::string_view SharedEnvmapChannelDirectory = "envmaps"; }; struct SCameraConfigResourcePaths final @@ -39,6 +40,7 @@ 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"; }; 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/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 From cf16ce753c79d9bd12501af3ad647db710cf7b82 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Fri, 10 Apr 2026 22:14:38 +0200 Subject: [PATCH 202/205] Final cleanups --- 08_HelloSwapchain/main.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/08_HelloSwapchain/main.cpp b/08_HelloSwapchain/main.cpp index 9060c1561..cd294b0d2 100644 --- a/08_HelloSwapchain/main.cpp +++ b/08_HelloSwapchain/main.cpp @@ -160,7 +160,6 @@ class HelloSwapchainApp final : public examples::SimpleWindowedApplication auto window = m_winMgr->createWindow(std::move(params)); // uncomment for some nasty testing of swapchain creation! //m_winMgr->minimize(window.get()); - const_cast&>(m_surface) = CSmoothResizeSurface::create(CSurfaceVulkanWin32::create(smart_refctd_ptr(m_api),move_and_static_cast(window))); } return {{m_surface->getSurface()/*,EQF_NONE*/}}; From 1c83dbe3ef7381cce4336fa72342200a3b232bb0 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 11 Apr 2026 00:47:12 +0200 Subject: [PATCH 203/205] Fix media submodule pointer --- media | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/media b/media index d458d8023..0f7ad42b3 160000 --- a/media +++ b/media @@ -1 +1 @@ -Subproject commit d458d802347ecf4dcb4c401728ebb192347e0de4 +Subproject commit 0f7ad42b33abe3143a5d69c4d14b26cf3e538c88 From 49512f20e55f511188969d5532df89f38adace37 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 11 Apr 2026 02:41:27 +0200 Subject: [PATCH 204/205] Fix HLSL path tracer debug build --- 31_HLSLPathTracer/app_resources/hlsl/render_common.hlsl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/31_HLSLPathTracer/app_resources/hlsl/render_common.hlsl b/31_HLSLPathTracer/app_resources/hlsl/render_common.hlsl index 95c3e2c3d..bd753d055 100644 --- a/31_HLSLPathTracer/app_resources/hlsl/render_common.hlsl +++ b/31_HLSLPathTracer/app_resources/hlsl/render_common.hlsl @@ -21,7 +21,7 @@ struct RenderPushConstants const float32_t3 recon = hlsl::cross(matT[0],matT[1]); lightZscale = hlsl::sign(hlsl::dot(recon,matT[2]))*hlsl::length(matT[2])/hlsl::length(recon); lightPos = matT[3]; - assert(lightMatrix()==hlsl::transpose(matT)); + assert(getLightMatrix()==hlsl::transpose(matT)); } float32_t3x4 getLightMatrix() From d3c2f73c70f71bcfefe54d6251614857c8f9ddbe Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 11 Apr 2026 19:17:35 +0200 Subject: [PATCH 205/205] Replace legacy example camera wrapper --- 09_GeometryCreator/include/common.hpp | 3 +- 09_GeometryCreator/main.cpp | 45 ++- 0_ImportanceSamplingEnvMaps/main.cpp | 54 ++- 12_MeshLoaders/include/common.hpp | 7 +- 12_MeshLoaders/main.cpp | 46 ++- .../include/nbl/this_example/common.hpp | 8 +- 31_HLSLPathTracer/main.cpp | 49 ++- 34_DebugDraw/include/common.hpp | 8 +- 34_DebugDraw/main.cpp | 38 +- 45_BRDFEvalTest/main.cpp | 33 +- 50.IESViewer/App.hpp | 10 +- 50.IESViewer/AppInit.cpp | 20 +- 50.IESViewer/AppRender.cpp | 50 ++- 50.IESViewer/AppUI.cpp | 15 +- 54_Transformations/main.cpp | 3 +- 61_UI/AppHeadlessCameraSmokeChecks.inl | 103 +++++- 62_SchusslerTest/main.cpp | 33 +- 67_RayQueryGeometry/include/common.hpp | 7 +- 67_RayQueryGeometry/main.cpp | 45 ++- 70_FLIPFluids/main.cpp | 50 ++- 71_RayTracingPipeline/include/common.hpp | 5 + 71_RayTracingPipeline/main.cpp | 69 ++-- 73_GeometryInspector/include/common.hpp | 3 +- 73_GeometryInspector/main.cpp | 79 ++-- common/CMakeLists.txt | 1 + common/include/nbl/examples/PCH.hpp | 4 +- .../include/nbl/examples/cameras/CCamera.hpp | 338 ------------------ .../cameras/CCameraSimpleFPSUtilities.hpp | 174 +++++++++ 28 files changed, 707 insertions(+), 593 deletions(-) delete mode 100644 common/include/nbl/examples/cameras/CCamera.hpp create mode 100644 common/include/nbl/examples/cameras/CCameraSimpleFPSUtilities.hpp 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/AppHeadlessCameraSmokeChecks.inl b/61_UI/AppHeadlessCameraSmokeChecks.inl index e47dacedb..730a9709a 100644 --- a/61_UI/AppHeadlessCameraSmokeChecks.inl +++ b/61_UI/AppHeadlessCameraSmokeChecks.inl @@ -1391,8 +1391,109 @@ } } - if (state.initialPresets.free.has_value() && state.freeCamera) + 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); 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/common/CMakeLists.txt b/common/CMakeLists.txt index 67665c476..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) 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_